From d1c7c2a98a133bdae7747601699a6b9ae0f90c8b Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 24 Jul 2019 23:21:52 -0400 Subject: [PATCH 0001/1623] allow devices to be marked as "hidden" This is a prerequisite for cross-signing, as it allows us to create other things that live within the device namespace, so they can be used for signatures. --- synapse/storage/devices.py | 63 ++++++++++++++----- .../storage/schema/delta/56/signing_keys.sql | 18 ++++++ 2 files changed, 65 insertions(+), 16 deletions(-) create mode 100644 synapse/storage/schema/delta/56/signing_keys.sql diff --git a/synapse/storage/devices.py b/synapse/storage/devices.py index d2b113a4e7..b73401bc26 100644 --- a/synapse/storage/devices.py +++ b/synapse/storage/devices.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd +# Copyright 2019 New Vector Ltd +# Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,7 +22,7 @@ from canonicaljson import json from twisted.internet import defer -from synapse.api.errors import StoreError +from synapse.api.errors import Codes, StoreError from synapse.metrics.background_process_metrics import run_as_background_process from synapse.storage._base import Cache, SQLBaseStore, db_to_json from synapse.storage.background_updates import BackgroundUpdateStore @@ -35,6 +37,7 @@ DROP_DEVICE_LIST_STREAMS_NON_UNIQUE_INDEXES = ( class DeviceWorkerStore(SQLBaseStore): + @defer.inlineCallbacks def get_device(self, user_id, device_id): """Retrieve a device. @@ -46,12 +49,15 @@ class DeviceWorkerStore(SQLBaseStore): Raises: StoreError: if the device is not found """ - return self._simple_select_one( + ret = yield self._simple_select_one( table="devices", keyvalues={"user_id": user_id, "device_id": device_id}, - retcols=("user_id", "device_id", "display_name"), + retcols=("user_id", "device_id", "display_name", "hidden"), desc="get_device", ) + if ret["hidden"]: + raise StoreError(404, "No row found (devices)") + return ret @defer.inlineCallbacks def get_devices_by_user(self, user_id): @@ -67,11 +73,11 @@ class DeviceWorkerStore(SQLBaseStore): devices = yield self._simple_select_list( table="devices", keyvalues={"user_id": user_id}, - retcols=("user_id", "device_id", "display_name"), + retcols=("user_id", "device_id", "display_name", "hidden"), desc="get_devices_by_user", ) - defer.returnValue({d["device_id"]: d for d in devices}) + defer.returnValue({d["device_id"]: d for d in devices if not d["hidden"]}) @defer.inlineCallbacks def get_devices_by_remote(self, destination, from_stream_id, limit): @@ -540,6 +546,8 @@ class DeviceStore(DeviceWorkerStore, BackgroundUpdateStore): Returns: defer.Deferred: boolean whether the device was inserted or an existing device existed with that ID. + Raises: + StoreError: if the device is already in use """ key = (user_id, device_id) if self.device_id_exists_cache.get(key, None): @@ -552,12 +560,25 @@ class DeviceStore(DeviceWorkerStore, BackgroundUpdateStore): "user_id": user_id, "device_id": device_id, "display_name": initial_device_display_name, + "hidden": False, }, desc="store_device", or_ignore=True, ) + if not inserted: + # if the device already exists, check if it's a real device, or + # if the device ID is reserved by something else + hidden = yield self._simple_select_one_onecol( + "devices", + keyvalues={"user_id": user_id, "device_id": device_id}, + retcol="hidden", + ) + if hidden: + raise StoreError(400, "The device ID is in use", Codes.FORBIDDEN) self.device_id_exists_cache.prefill(key, True) defer.returnValue(inserted) + except StoreError: + raise except Exception as e: logger.error( "store_device with device_id=%s(%r) user_id=%s(%r)" @@ -582,11 +603,11 @@ class DeviceStore(DeviceWorkerStore, BackgroundUpdateStore): Returns: defer.Deferred """ - yield self._simple_delete_one( - table="devices", - keyvalues={"user_id": user_id, "device_id": device_id}, - desc="delete_device", - ) + sql = """ + DELETE FROM devices + WHERE user_id = ? AND device_id = ? AND NOT COALESCE(hidden, ?) + """ + yield self._execute("delete_device", None, sql, user_id, device_id, False) self.device_id_exists_cache.invalidate((user_id, device_id)) @@ -600,13 +621,21 @@ class DeviceStore(DeviceWorkerStore, BackgroundUpdateStore): Returns: defer.Deferred """ - yield self._simple_delete_many( - table="devices", - column="device_id", - iterable=device_ids, - keyvalues={"user_id": user_id}, - desc="delete_devices", + + if not device_ids or len(device_ids) == 0: + return + sql = """ + DELETE FROM devices + WHERE user_id = ? AND device_id IN (%s) AND NOT COALESCE(hidden, ?) + """ % ( + ",".join("?" for _ in device_ids) ) + values = [user_id] + values.extend(device_ids) + values.append(False) + + yield self._execute("delete_devices", None, sql, *values) + for device_id in device_ids: self.device_id_exists_cache.invalidate((user_id, device_id)) @@ -628,6 +657,8 @@ class DeviceStore(DeviceWorkerStore, BackgroundUpdateStore): updates["display_name"] = new_display_name if not updates: return defer.succeed(None) + # FIXME: should only update if hidden is not True. But updating the + # display name of a hidden device should be harmless return self._simple_update_one( table="devices", keyvalues={"user_id": user_id, "device_id": device_id}, diff --git a/synapse/storage/schema/delta/56/signing_keys.sql b/synapse/storage/schema/delta/56/signing_keys.sql new file mode 100644 index 0000000000..51c96d3116 --- /dev/null +++ b/synapse/storage/schema/delta/56/signing_keys.sql @@ -0,0 +1,18 @@ +/* Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +-- device list needs to know which ones are "real" devices, and which ones are +-- just used to avoid collisions +ALTER TABLE devices ADD COLUMN hidden BOOLEAN NULLABLE; From c659b9f94fff29adfb2abe4f6b345710b65e8741 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 25 Jul 2019 11:08:24 -0400 Subject: [PATCH 0002/1623] allow uploading keys for cross-signing --- synapse/api/errors.py | 1 + synapse/handlers/device.py | 17 ++ synapse/handlers/e2e_keys.py | 198 +++++++++++++++++- synapse/handlers/sync.py | 7 +- synapse/rest/client/v2_alpha/keys.py | 46 +++- synapse/storage/__init__.py | 5 +- synapse/storage/devices.py | 57 +++++ synapse/storage/end_to_end_keys.py | 174 ++++++++++++++- .../storage/schema/delta/56/signing_keys.sql | 41 ++++ synapse/types.py | 24 +++ tests/handlers/test_e2e_keys.py | 63 ++++++ 11 files changed, 621 insertions(+), 12 deletions(-) diff --git a/synapse/api/errors.py b/synapse/api/errors.py index ad3e262041..be15921bc6 100644 --- a/synapse/api/errors.py +++ b/synapse/api/errors.py @@ -61,6 +61,7 @@ class Codes(object): INCOMPATIBLE_ROOM_VERSION = "M_INCOMPATIBLE_ROOM_VERSION" WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION" EXPIRED_ACCOUNT = "ORG_MATRIX_EXPIRED_ACCOUNT" + INVALID_SIGNATURE = "M_INVALID_SIGNATURE" class CodeMessageException(RuntimeError): diff --git a/synapse/handlers/device.py b/synapse/handlers/device.py index 99e8413092..2a8fa9c818 100644 --- a/synapse/handlers/device.py +++ b/synapse/handlers/device.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd +# Copyright 2019 New Vector Ltd +# Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -408,6 +410,21 @@ class DeviceHandler(DeviceWorkerHandler): for host in hosts: self.federation_sender.send_device_messages(host) + @defer.inlineCallbacks + def notify_user_signature_update(self, from_user_id, user_ids): + """Notify a user that they have made new signatures of other users. + + Args: + from_user_id (str): the user who made the signature + user_ids (list[str]): the users IDs that have new signatures + """ + + position = yield self.store.add_user_signature_change_to_streams( + from_user_id, user_ids + ) + + self.notifier.on_new_event("device_list_key", position, users=[from_user_id]) + @defer.inlineCallbacks def on_federation_query_user_devices(self, user_id): stream_id, devices = yield self.store.get_devices_with_keys_by_user(user_id) diff --git a/synapse/handlers/e2e_keys.py b/synapse/handlers/e2e_keys.py index fdfe8611b6..6187f879ef 100644 --- a/synapse/handlers/e2e_keys.py +++ b/synapse/handlers/e2e_keys.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd -# Copyright 2018 New Vector Ltd +# Copyright 2018-2019 New Vector Ltd +# Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,12 +20,17 @@ import logging from six import iteritems from canonicaljson import encode_canonical_json, json +from signedjson.sign import SignatureVerifyException, verify_signed_json from twisted.internet import defer -from synapse.api.errors import CodeMessageException, SynapseError +from synapse.api.errors import CodeMessageException, Codes, SynapseError from synapse.logging.context import make_deferred_yieldable, run_in_background -from synapse.types import UserID, get_domain_from_id +from synapse.types import ( + UserID, + get_domain_from_id, + get_verify_key_from_cross_signing_key, +) from synapse.util.retryutils import NotRetryingDestination logger = logging.getLogger(__name__) @@ -46,7 +52,7 @@ class E2eKeysHandler(object): ) @defer.inlineCallbacks - def query_devices(self, query_body, timeout): + def query_devices(self, query_body, timeout, from_user_id): """ Handle a device key query from a client { @@ -64,6 +70,11 @@ class E2eKeysHandler(object): } } } + + Args: + from_user_id (str): the user making the query. This is used when + adding cross-signing signatures to limit what signatures users + can see. """ device_keys_query = query_body.get("device_keys", {}) @@ -118,6 +129,11 @@ class E2eKeysHandler(object): r = remote_queries_not_in_cache.setdefault(domain, {}) r[user_id] = remote_queries[user_id] + # Get cached cross-signing keys + cross_signing_keys = yield self.query_cross_signing_keys( + device_keys_query, from_user_id + ) + # Now fetch any devices that we don't have in our cache @defer.inlineCallbacks def do_remote_query(destination): @@ -131,6 +147,14 @@ class E2eKeysHandler(object): if user_id in destination_query: results[user_id] = keys + for user_id, key in remote_result["master_keys"].items(): + if user_id in destination_query: + cross_signing_keys["master"][user_id] = key + + for user_id, key in remote_result["self_signing_keys"].items(): + if user_id in destination_query: + cross_signing_keys["self_signing"][user_id] = key + except Exception as e: failures[destination] = _exception_to_failure(e) @@ -144,7 +168,73 @@ class E2eKeysHandler(object): ) ) - defer.returnValue({"device_keys": results, "failures": failures}) + ret = {"device_keys": results, "failures": failures} + + for key, value in iteritems(cross_signing_keys): + ret[key + "_keys"] = value + + defer.returnValue(ret) + + @defer.inlineCallbacks + def query_cross_signing_keys(self, query, from_user_id): + """Get cross-signing keys for users + + Args: + query (Iterable[string]) an iterable of user IDs. A dict whose keys + are user IDs satisfies this, so the query format used for + query_devices can be used here. + from_user_id (str): the user making the query. This is used when + adding cross-signing signatures to limit what signatures users + can see. + + Returns: + defer.Deferred[dict[str, dict[str, dict]]]: map from + (master|self_signing|user_signing) -> user_id -> key + """ + master_keys = {} + self_signing_keys = {} + user_signing_keys = {} + + for user_id in query: + # XXX: consider changing the store functions to allow querying + # multiple users simultaneously. + try: + key = yield self.store.get_e2e_cross_signing_key( + user_id, "master", from_user_id + ) + if key: + master_keys[user_id] = key + except Exception as e: + logger.info("Error getting master key: %s", e) + + try: + key = yield self.store.get_e2e_cross_signing_key( + user_id, "self_signing", from_user_id + ) + if key: + self_signing_keys[user_id] = key + except Exception as e: + logger.info("Error getting self-signing key: %s", e) + + # users can see other users' master and self-signing keys, but can + # only see their own user-signing keys + if from_user_id == user_id: + try: + key = yield self.store.get_e2e_cross_signing_key( + user_id, "user_signing", from_user_id + ) + if key: + user_signing_keys[user_id] = key + except Exception as e: + logger.info("Error getting user-signing key: %s", e) + + defer.returnValue( + { + "master": master_keys, + "self_signing": self_signing_keys, + "user_signing": user_signing_keys, + } + ) @defer.inlineCallbacks def query_local_devices(self, query): @@ -342,6 +432,104 @@ class E2eKeysHandler(object): yield self.store.add_e2e_one_time_keys(user_id, device_id, time_now, new_keys) + @defer.inlineCallbacks + def upload_signing_keys_for_user(self, user_id, keys): + """Upload signing keys for cross-signing + + Args: + user_id (string): the user uploading the keys + keys (dict[string, dict]): the signing keys + """ + + # if a master key is uploaded, then check it. Otherwise, load the + # stored master key, to check signatures on other keys + if "master_key" in keys: + master_key = keys["master_key"] + + _check_cross_signing_key(master_key, user_id, "master") + else: + master_key = yield self.store.get_e2e_cross_signing_key(user_id, "master") + + # if there is no master key, then we can't do anything, because all the + # other cross-signing keys need to be signed by the master key + if not master_key: + raise SynapseError(400, "No master key available", Codes.MISSING_PARAM) + + master_key_id, master_verify_key = get_verify_key_from_cross_signing_key( + master_key + ) + + # for the other cross-signing keys, make sure that they have valid + # signatures from the master key + if "self_signing_key" in keys: + self_signing_key = keys["self_signing_key"] + + _check_cross_signing_key( + self_signing_key, user_id, "self_signing", master_verify_key + ) + + if "user_signing_key" in keys: + user_signing_key = keys["user_signing_key"] + + _check_cross_signing_key( + user_signing_key, user_id, "user_signing", master_verify_key + ) + + # if everything checks out, then store the keys and send notifications + deviceids = [] + if "master_key" in keys: + yield self.store.set_e2e_cross_signing_key(user_id, "master", master_key) + deviceids.append(master_verify_key.version) + if "self_signing_key" in keys: + yield self.store.set_e2e_cross_signing_key( + user_id, "self_signing", self_signing_key + ) + deviceids.append( + get_verify_key_from_cross_signing_key(self_signing_key)[1].version + ) + if "user_signing_key" in keys: + yield self.store.set_e2e_cross_signing_key( + user_id, "user_signing", user_signing_key + ) + # the signature stream matches the semantics that we want for + # user-signing key updates: only the user themselves is notified of + # their own user-signing key updates + yield self.device_handler.notify_user_signature_update(user_id, [user_id]) + + # master key and self-signing key updates match the semantics of device + # list updates: all users who share an encrypted room are notified + if len(deviceids): + yield self.device_handler.notify_device_update(user_id, deviceids) + + defer.returnValue({}) + + +def _check_cross_signing_key(key, user_id, key_type, signing_key=None): + """Check a cross-signing key uploaded by a user. Performs some basic sanity + checking, and ensures that it is signed, if a signature is required. + + Args: + key (dict): the key data to verify + user_id (str): the user whose key is being checked + key_type (str): the type of key that the key should be + signing_key (VerifyKey): (optional) the signing key that the key should + be signed with. If omitted, signatures will not be checked. + """ + if ( + key.get("user_id") != user_id + or key_type not in key.get("usage", []) + or len(key.get("keys", {})) != 1 + ): + raise SynapseError(400, ("Invalid %s key" % (key_type,)), Codes.INVALID_PARAM) + + if signing_key: + try: + verify_signed_json(key, user_id, signing_key) + except SignatureVerifyException: + raise SynapseError( + 400, ("Invalid signature on %s key" % key_type), Codes.INVALID_SIGNATURE + ) + def _exception_to_failure(e): if isinstance(e, CodeMessageException): diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index cd1ac0a27a..c1c28a5fa1 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd -# Copyright 2018 New Vector Ltd +# Copyright 2018, 2019 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -1116,6 +1116,11 @@ class SyncHandler(object): # weren't in the previous sync *or* they left and rejoined. users_that_have_changed.update(newly_joined_or_invited_users) + user_signatures_changed = yield self.store.get_users_whose_signatures_changed( + user_id, since_token.device_list_key + ) + users_that_have_changed.update(user_signatures_changed) + # Now find users that we no longer track for room_id in newly_left_rooms: left_users = yield self.state.get_current_users_in_room(room_id) diff --git a/synapse/rest/client/v2_alpha/keys.py b/synapse/rest/client/v2_alpha/keys.py index 45c9928b65..3eaf1fd8a4 100644 --- a/synapse/rest/client/v2_alpha/keys.py +++ b/synapse/rest/client/v2_alpha/keys.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd +# Copyright 2019 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -26,7 +27,7 @@ from synapse.http.servlet import ( ) from synapse.types import StreamToken -from ._base import client_patterns +from ._base import client_patterns, interactive_auth_handler logger = logging.getLogger(__name__) @@ -145,10 +146,11 @@ class KeyQueryServlet(RestServlet): @defer.inlineCallbacks def on_POST(self, request): - yield self.auth.get_user_by_req(request, allow_guest=True) + requester = yield self.auth.get_user_by_req(request, allow_guest=True) + user_id = requester.user.to_string() timeout = parse_integer(request, "timeout", 10 * 1000) body = parse_json_object_from_request(request) - result = yield self.e2e_keys_handler.query_devices(body, timeout) + result = yield self.e2e_keys_handler.query_devices(body, timeout, user_id) defer.returnValue((200, result)) @@ -227,8 +229,46 @@ class OneTimeKeyServlet(RestServlet): defer.returnValue((200, result)) +class SigningKeyUploadServlet(RestServlet): + """ + POST /keys/device_signing/upload HTTP/1.1 + Content-Type: application/json + + { + } + """ + + PATTERNS = client_patterns("/keys/device_signing/upload$", releases=()) + + def __init__(self, hs): + """ + Args: + hs (synapse.server.HomeServer): server + """ + super(SigningKeyUploadServlet, self).__init__() + self.hs = hs + self.auth = hs.get_auth() + self.e2e_keys_handler = hs.get_e2e_keys_handler() + self.auth_handler = hs.get_auth_handler() + + @interactive_auth_handler + @defer.inlineCallbacks + def on_POST(self, request): + requester = yield self.auth.get_user_by_req(request) + user_id = requester.user.to_string() + body = parse_json_object_from_request(request) + + yield self.auth_handler.validate_user_via_ui_auth( + requester, body, self.hs.get_ip_from_request(request) + ) + + result = yield self.e2e_keys_handler.upload_signing_keys_for_user(user_id, body) + defer.returnValue((200, result)) + + def register_servlets(hs, http_server): KeyUploadServlet(hs).register(http_server) KeyQueryServlet(hs).register(http_server) KeyChangesServlet(hs).register(http_server) OneTimeKeyServlet(hs).register(http_server) + SigningKeyUploadServlet(hs).register(http_server) diff --git a/synapse/storage/__init__.py b/synapse/storage/__init__.py index 6b0ca80087..c20ba1001c 100644 --- a/synapse/storage/__init__.py +++ b/synapse/storage/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd -# Copyright 2018 New Vector Ltd +# Copyright 2018,2019 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -207,6 +207,9 @@ class DataStore( self._device_list_stream_cache = StreamChangeCache( "DeviceListStreamChangeCache", device_list_max ) + self._user_signature_stream_cache = StreamChangeCache( + "UserSignatureStreamChangeCache", device_list_max + ) self._device_list_federation_stream_cache = StreamChangeCache( "DeviceListFederationStreamChangeCache", device_list_max ) diff --git a/synapse/storage/devices.py b/synapse/storage/devices.py index b73401bc26..ed372e2fc4 100644 --- a/synapse/storage/devices.py +++ b/synapse/storage/devices.py @@ -302,6 +302,41 @@ class DeviceWorkerStore(SQLBaseStore): """ txn.execute(sql, (destination, stream_id)) + @defer.inlineCallbacks + def add_user_signature_change_to_streams(self, from_user_id, user_ids): + """Persist that a user has made new signatures + + Args: + from_user_id (str): the user who made the signatures + user_ids (list[str]): the users who were signed + """ + + with self._device_list_id_gen.get_next() as stream_id: + yield self.runInteraction( + "add_user_sig_change_to_streams", + self._add_user_signature_change_txn, + from_user_id, + user_ids, + stream_id, + ) + defer.returnValue(stream_id) + + def _add_user_signature_change_txn(self, txn, from_user_id, user_ids, stream_id): + txn.call_after( + self._user_signature_stream_cache.entity_has_changed, + from_user_id, + stream_id, + ) + self._simple_insert_txn( + txn, + "user_signature_stream", + values={ + "stream_id": stream_id, + "from_user_id": from_user_id, + "user_ids": json.dumps(user_ids), + }, + ) + def get_device_stream_token(self): return self._device_list_id_gen.get_current_token() @@ -440,6 +475,28 @@ class DeviceWorkerStore(SQLBaseStore): "get_users_whose_devices_changed", _get_users_whose_devices_changed_txn ) + @defer.inlineCallbacks + def get_users_whose_signatures_changed(self, user_id, from_key): + """Get the users who have new cross-signing signatures made by `user_id` since + `from_key`. + + Args: + user_id (str): the user who made the signatures + from_key (str): The device lists stream token + """ + from_key = int(from_key) + if self._user_signature_stream_cache.has_entity_changed(user_id, from_key): + sql = """ + SELECT DISTINCT user_ids FROM user_signature_stream + WHERE from_user_id = ? AND stream_id > ? + """ + rows = yield self._execute( + "get_users_whose_signatures_changed", None, sql, user_id, from_key + ) + defer.returnValue(set(user for row in rows for user in json.loads(row[0]))) + else: + defer.returnValue(set()) + def get_all_device_list_changes_for_remotes(self, from_key, to_key): """Return a list of `(stream_id, user_id, destination)` which is the combined list of changes to devices, and which destinations need to be diff --git a/synapse/storage/end_to_end_keys.py b/synapse/storage/end_to_end_keys.py index 2fabb9e2cb..bb5f7d94eb 100644 --- a/synapse/storage/end_to_end_keys.py +++ b/synapse/storage/end_to_end_keys.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd +# Copyright 2019 New Vector Ltd +# Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,9 +14,11 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import time + from six import iteritems -from canonicaljson import encode_canonical_json +from canonicaljson import encode_canonical_json, json from twisted.internet import defer @@ -85,11 +89,12 @@ class EndToEndKeyWorkerStore(SQLBaseStore): " k.key_json" " FROM devices d" " %s JOIN e2e_device_keys_json k USING (user_id, device_id)" - " WHERE %s" + " WHERE (%s) AND NOT COALESCE(d.hidden, ?)" ) % ( "LEFT" if include_all_devices else "INNER", " OR ".join("(" + q + ")" for q in query_clauses), ) + query_params.append(False) txn.execute(sql, query_params) rows = self.cursor_to_dict(txn) @@ -281,3 +286,168 @@ class EndToEndKeyStore(EndToEndKeyWorkerStore, SQLBaseStore): return self.runInteraction( "delete_e2e_keys_by_device", delete_e2e_keys_by_device_txn ) + + def _set_e2e_cross_signing_key_txn(self, txn, user_id, key_type, key): + """Set a user's cross-signing key. + + Args: + txn (twisted.enterprise.adbapi.Connection): db connection + user_id (str): the user to set the signing key for + key_type (str): the type of key that is being set: either 'master' + for a master key, 'self_signing' for a self-signing key, or + 'user_signing' for a user-signing key + key (dict): the key data + """ + # the cross-signing keys need to occupy the same namespace as devices, + # since signatures are identified by device ID. So add an entry to the + # device table to make sure that we don't have a collision with device + # IDs + + # the 'key' dict will look something like: + # { + # "user_id": "@alice:example.com", + # "usage": ["self_signing"], + # "keys": { + # "ed25519:base64+self+signing+public+key": "base64+self+signing+public+key", + # }, + # "signatures": { + # "@alice:example.com": { + # "ed25519:base64+master+public+key": "base64+signature" + # } + # } + # } + # The "keys" property must only have one entry, which will be the public + # key, so we just grab the first value in there + pubkey = next(iter(key["keys"].values())) + self._simple_insert( + "devices", + values={ + "user_id": user_id, + "device_id": pubkey, + "display_name": key_type + " signing key", + "hidden": True, + }, + desc="store_master_key_device", + ) + + # and finally, store the key itself + self._simple_insert( + "e2e_cross_signing_keys", + values={ + "user_id": user_id, + "keytype": key_type, + "keydata": json.dumps(key), + "added_ts": time.time() * 1000, + }, + desc="store_master_key", + ) + + def set_e2e_cross_signing_key(self, user_id, key_type, key): + """Set a user's cross-signing key. + + Args: + user_id (str): the user to set the user-signing key for + key_type (str): the type of cross-signing key to set + key (dict): the key data + """ + return self.runInteraction( + "add_e2e_cross_signing_key", + self._set_e2e_cross_signing_key_txn, + user_id, + key_type, + key, + ) + + def _get_e2e_cross_signing_key_txn(self, txn, user_id, key_type, from_user_id=None): + """Returns a user's cross-signing key. + + Args: + txn (twisted.enterprise.adbapi.Connection): db connection + user_id (str): the user whose key is being requested + key_type (str): the type of key that is being set: either 'master' + for a master key, 'self_signing' for a self-signing key, or + 'user_signing' for a user-signing key + from_user_id (str): if specified, signatures made by this user on + the key will be included in the result + + Returns: + dict of the key data + """ + sql = ( + "SELECT keydata " + " FROM e2e_cross_signing_keys " + " WHERE user_id = ? AND keytype = ? ORDER BY added_ts DESC LIMIT 1" + ) + txn.execute(sql, (user_id, key_type)) + row = txn.fetchone() + if not row: + return None + key = json.loads(row[0]) + + device_id = None + for k in key["keys"].values(): + device_id = k + + if from_user_id is not None: + sql = ( + "SELECT key_id, signature " + " FROM e2e_cross_signing_signatures " + " WHERE user_id = ? " + " AND target_user_id = ? " + " AND target_device_id = ? " + ) + txn.execute(sql, (from_user_id, user_id, device_id)) + row = txn.fetchone() + if row: + key.setdefault("signatures", {}).setdefault(from_user_id, {})[ + row[0] + ] = row[1] + + return key + + def get_e2e_cross_signing_key(self, user_id, key_type, from_user_id=None): + """Returns a user's cross-signing key. + + Args: + user_id (str): the user whose self-signing key is being requested + key_type (str): the type of cross-signing key to get + from_user_id (str): if specified, signatures made by this user on + the self-signing key will be included in the result + + Returns: + dict of the key data + """ + return self.runInteraction( + "get_e2e_cross_signing_key", + self._get_e2e_cross_signing_key_txn, + user_id, + key_type, + from_user_id, + ) + + def store_e2e_cross_signing_signatures(self, user_id, signatures): + """Stores cross-signing signatures. + + Args: + user_id (str): the user who made the signatures + signatures (iterable[(str, str, str, str)]): signatures to add - each + a tuple of (key_id, target_user_id, target_device_id, signature), + where key_id is the ID of the key (including the signature + algorithm) that made the signature, target_user_id and + target_device_id indicate the device being signed, and signature + is the signature of the device + """ + return self._simple_insert_many( + "e2e_cross_signing_signatures", + [ + { + "user_id": user_id, + "key_id": key_id, + "target_user_id": target_user_id, + "target_device_id": target_device_id, + "signature": signature, + } + for (key_id, target_user_id, target_device_id, signature) in signatures + ], + "add_e2e_signing_key", + ) diff --git a/synapse/storage/schema/delta/56/signing_keys.sql b/synapse/storage/schema/delta/56/signing_keys.sql index 51c96d3116..771740e970 100644 --- a/synapse/storage/schema/delta/56/signing_keys.sql +++ b/synapse/storage/schema/delta/56/signing_keys.sql @@ -13,6 +13,47 @@ * limitations under the License. */ +-- cross-signing keys +CREATE TABLE IF NOT EXISTS e2e_cross_signing_keys ( + user_id TEXT NOT NULL, + -- the type of cross-signing key (master, user_signing, or self_signing) + keytype TEXT NOT NULL, + -- the full key information, as a json-encoded dict + keydata TEXT NOT NULL, + -- time that the key was added + added_ts BIGINT NOT NULL +); + +CREATE UNIQUE INDEX e2e_cross_signing_keys_idx ON e2e_cross_signing_keys(user_id, keytype, added_ts); + +-- cross-signing signatures +CREATE TABLE IF NOT EXISTS e2e_cross_signing_signatures ( + -- user who did the signing + user_id TEXT NOT NULL, + -- key used to sign + key_id TEXT NOT NULL, + -- user who was signed + target_user_id TEXT NOT NULL, + -- device/key that was signed + target_device_id TEXT NOT NULL, + -- the actual signature + signature TEXT NOT NULL +); + +CREATE UNIQUE INDEX e2e_cross_signing_signatures_idx ON e2e_cross_signing_signatures(user_id, target_user_id, target_device_id); + +-- stream of user signature updates +CREATE TABLE IF NOT EXISTS user_signature_stream ( + -- uses the same stream ID as device list stream + stream_id BIGINT NOT NULL, + -- user who did the signing + from_user_id TEXT NOT NULL, + -- list of users who were signed, as a JSON array + user_ids TEXT NOT NULL +); + +CREATE UNIQUE INDEX user_signature_stream_idx ON user_signature_stream(stream_id); + -- device list needs to know which ones are "real" devices, and which ones are -- just used to avoid collisions ALTER TABLE devices ADD COLUMN hidden BOOLEAN NULLABLE; diff --git a/synapse/types.py b/synapse/types.py index 51eadb6ad4..7a80471a0c 100644 --- a/synapse/types.py +++ b/synapse/types.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd +# Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,6 +18,8 @@ import string from collections import namedtuple import attr +from signedjson.key import decode_verify_key_bytes +from unpaddedbase64 import decode_base64 from synapse.api.errors import SynapseError @@ -475,3 +478,24 @@ class ReadReceipt(object): user_id = attr.ib() event_ids = attr.ib() data = attr.ib() + + +def get_verify_key_from_cross_signing_key(key_info): + """Get the key ID and signedjson verify key from a cross-signing key dict + + Args: + key_info (dict): a cross-signing key dict, which must have a "keys" + property that has exactly one item in it + + Returns: + (str, VerifyKey): the key ID and verify key for the cross-signing key + """ + # make sure that exactly one key is provided + if "keys" not in key_info: + raise SynapseError(400, "Invalid key") + keys = key_info["keys"] + if len(keys) != 1: + raise SynapseError(400, "Invalid key") + # and return that one key + for key_id, key_data in keys.items(): + return (key_id, decode_verify_key_bytes(key_id, decode_base64(key_data))) diff --git a/tests/handlers/test_e2e_keys.py b/tests/handlers/test_e2e_keys.py index 8dccc6826e..9ae4cb6ea2 100644 --- a/tests/handlers/test_e2e_keys.py +++ b/tests/handlers/test_e2e_keys.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd +# Copyright 2019 New Vector Ltd +# Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -145,3 +147,64 @@ class E2eKeysHandlerTestCase(unittest.TestCase): "one_time_keys": {local_user: {device_id: {"alg1:k1": "key1"}}}, }, ) + + @defer.inlineCallbacks + def test_replace_master_key(self): + """uploading a new signing key should make the old signing key unavailable""" + local_user = "@boris:" + self.hs.hostname + keys1 = { + "master_key": { + # private key: 2lonYOM6xYKdEsO+6KrC766xBcHnYnim1x/4LFGF8B0 + "user_id": local_user, + "usage": ["master"], + "keys": { + "ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk": "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk" + }, + } + } + yield self.handler.upload_signing_keys_for_user(local_user, keys1) + + keys2 = { + "master_key": { + # private key: 4TL4AjRYwDVwD3pqQzcor+ez/euOB1/q78aTJ+czDNs + "user_id": local_user, + "usage": ["master"], + "keys": { + "ed25519:Hq6gL+utB4ET+UvD5ci0kgAwsX6qP/zvf8v6OInU5iw": "Hq6gL+utB4ET+UvD5ci0kgAwsX6qP/zvf8v6OInU5iw" + }, + } + } + yield self.handler.upload_signing_keys_for_user(local_user, keys2) + + devices = yield self.handler.query_devices({"device_keys": {local_user: []}}, 0, local_user) + self.assertDictEqual(devices["master_keys"], {local_user: keys2["master_key"]}) + + @defer.inlineCallbacks + def test_self_signing_key_doesnt_show_up_as_device(self): + """signing keys should be hidden when fetching a user's devices""" + local_user = "@boris:" + self.hs.hostname + keys1 = { + "master_key": { + # private key: 2lonYOM6xYKdEsO+6KrC766xBcHnYnim1x/4LFGF8B0 + "user_id": local_user, + "usage": ["master"], + "keys": { + "ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk": "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk" + }, + } + } + yield self.handler.upload_signing_keys_for_user(local_user, keys1) + + res = None + try: + yield self.hs.get_device_handler().check_device_registered( + user_id=local_user, + device_id="nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk", + initial_device_display_name="new display name", + ) + except errors.SynapseError as e: + res = e.code + self.assertEqual(res, 400) + + res = yield self.handler.query_local_devices({local_user: None}) + self.assertDictEqual(res, {local_user: {}}) From 781ade836b05b7e327baa7f927c553941edcc368 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Tue, 30 Jul 2019 23:09:50 -0400 Subject: [PATCH 0003/1623] apply changes from PR review --- synapse/storage/devices.py | 33 +++++++++---------- synapse/storage/end_to_end_keys.py | 2 +- .../{signing_keys.sql => hidden_devices.sql} | 2 +- 3 files changed, 17 insertions(+), 20 deletions(-) rename synapse/storage/schema/delta/56/{signing_keys.sql => hidden_devices.sql} (92%) diff --git a/synapse/storage/devices.py b/synapse/storage/devices.py index b73401bc26..f62ed12386 100644 --- a/synapse/storage/devices.py +++ b/synapse/storage/devices.py @@ -37,9 +37,9 @@ DROP_DEVICE_LIST_STREAMS_NON_UNIQUE_INDEXES = ( class DeviceWorkerStore(SQLBaseStore): - @defer.inlineCallbacks def get_device(self, user_id, device_id): - """Retrieve a device. + """Retrieve a device. Only returns devices that are not marked as + hidden. Args: user_id (str): The ID of the user which owns the device @@ -49,19 +49,17 @@ class DeviceWorkerStore(SQLBaseStore): Raises: StoreError: if the device is not found """ - ret = yield self._simple_select_one( + return self._simple_select_one( table="devices", - keyvalues={"user_id": user_id, "device_id": device_id}, + keyvalues={"user_id": user_id, "device_id": device_id, "hidden": False}, retcols=("user_id", "device_id", "display_name", "hidden"), desc="get_device", ) - if ret["hidden"]: - raise StoreError(404, "No row found (devices)") - return ret @defer.inlineCallbacks def get_devices_by_user(self, user_id): - """Retrieve all of a user's registered devices. + """Retrieve all of a user's registered devices. Only returns devices + that are not marked as hidden. Args: user_id (str): @@ -72,12 +70,12 @@ class DeviceWorkerStore(SQLBaseStore): """ devices = yield self._simple_select_list( table="devices", - keyvalues={"user_id": user_id}, - retcols=("user_id", "device_id", "display_name", "hidden"), + keyvalues={"user_id": user_id, "hidden": False}, + retcols=("user_id", "device_id", "display_name"), desc="get_devices_by_user", ) - defer.returnValue({d["device_id"]: d for d in devices if not d["hidden"]}) + defer.returnValue({d["device_id"]: d for d in devices}) @defer.inlineCallbacks def get_devices_by_remote(self, destination, from_stream_id, limit): @@ -605,9 +603,9 @@ class DeviceStore(DeviceWorkerStore, BackgroundUpdateStore): """ sql = """ DELETE FROM devices - WHERE user_id = ? AND device_id = ? AND NOT COALESCE(hidden, ?) + WHERE user_id = ? AND device_id = ? AND NOT hidden """ - yield self._execute("delete_device", None, sql, user_id, device_id, False) + yield self._execute("delete_device", None, sql, user_id, device_id) self.device_id_exists_cache.invalidate((user_id, device_id)) @@ -626,7 +624,7 @@ class DeviceStore(DeviceWorkerStore, BackgroundUpdateStore): return sql = """ DELETE FROM devices - WHERE user_id = ? AND device_id IN (%s) AND NOT COALESCE(hidden, ?) + WHERE user_id = ? AND device_id IN (%s) AND NOT hidden """ % ( ",".join("?" for _ in device_ids) ) @@ -640,7 +638,8 @@ class DeviceStore(DeviceWorkerStore, BackgroundUpdateStore): self.device_id_exists_cache.invalidate((user_id, device_id)) def update_device(self, user_id, device_id, new_display_name=None): - """Update a device. + """Update a device. Only updates the device if it is not marked as + hidden. Args: user_id (str): The ID of the user which owns the device @@ -657,11 +656,9 @@ class DeviceStore(DeviceWorkerStore, BackgroundUpdateStore): updates["display_name"] = new_display_name if not updates: return defer.succeed(None) - # FIXME: should only update if hidden is not True. But updating the - # display name of a hidden device should be harmless return self._simple_update_one( table="devices", - keyvalues={"user_id": user_id, "device_id": device_id}, + keyvalues={"user_id": user_id, "device_id": device_id, "hidden": False}, updatevalues=updates, desc="update_device", ) diff --git a/synapse/storage/end_to_end_keys.py b/synapse/storage/end_to_end_keys.py index 2fabb9e2cb..66eb509588 100644 --- a/synapse/storage/end_to_end_keys.py +++ b/synapse/storage/end_to_end_keys.py @@ -85,7 +85,7 @@ class EndToEndKeyWorkerStore(SQLBaseStore): " k.key_json" " FROM devices d" " %s JOIN e2e_device_keys_json k USING (user_id, device_id)" - " WHERE %s" + " WHERE %s AND NOT d.hidden" ) % ( "LEFT" if include_all_devices else "INNER", " OR ".join("(" + q + ")" for q in query_clauses), diff --git a/synapse/storage/schema/delta/56/signing_keys.sql b/synapse/storage/schema/delta/56/hidden_devices.sql similarity index 92% rename from synapse/storage/schema/delta/56/signing_keys.sql rename to synapse/storage/schema/delta/56/hidden_devices.sql index 51c96d3116..67f8b20297 100644 --- a/synapse/storage/schema/delta/56/signing_keys.sql +++ b/synapse/storage/schema/delta/56/hidden_devices.sql @@ -15,4 +15,4 @@ -- device list needs to know which ones are "real" devices, and which ones are -- just used to avoid collisions -ALTER TABLE devices ADD COLUMN hidden BOOLEAN NULLABLE; +ALTER TABLE devices ADD COLUMN hidden BOOLEAN DEFAULT FALSE; From 2997a91250c4af915be70d2be38df1b3889c4c2d Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Tue, 30 Jul 2019 23:14:00 -0400 Subject: [PATCH 0004/1623] add changelog file --- changelog.d/5759.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/5759.misc diff --git a/changelog.d/5759.misc b/changelog.d/5759.misc new file mode 100644 index 0000000000..c0bc566c4c --- /dev/null +++ b/changelog.d/5759.misc @@ -0,0 +1 @@ +Allow devices to be marked as hidden, for use by features such as cross-signing. \ No newline at end of file From 185188be03e278a5a9a24f7e206e7fc2410415a7 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 31 Jul 2019 15:18:15 -0400 Subject: [PATCH 0005/1623] remove extra SQL query param --- synapse/storage/devices.py | 1 - 1 file changed, 1 deletion(-) diff --git a/synapse/storage/devices.py b/synapse/storage/devices.py index e7800da4f7..b3e8c7396d 100644 --- a/synapse/storage/devices.py +++ b/synapse/storage/devices.py @@ -630,7 +630,6 @@ class DeviceStore(DeviceWorkerStore, BackgroundUpdateStore): ) values = [user_id] values.extend(device_ids) - values.append(False) yield self._execute("delete_devices", None, sql, *values) From 430ea08186750ef67899bc302c0b6bb32c2f111c Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 31 Jul 2019 15:38:11 -0400 Subject: [PATCH 0006/1623] PostgreSQL, Y U no like? --- synapse/storage/devices.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/synapse/storage/devices.py b/synapse/storage/devices.py index b3e8c7396d..a1f12df907 100644 --- a/synapse/storage/devices.py +++ b/synapse/storage/devices.py @@ -603,9 +603,9 @@ class DeviceStore(DeviceWorkerStore, BackgroundUpdateStore): """ sql = """ DELETE FROM devices - WHERE user_id = ? AND device_id = ? AND NOT hidden + WHERE user_id = ? AND device_id = ? AND hidden = ? """ - yield self._execute("delete_device", None, sql, user_id, device_id) + yield self._execute("delete_device", None, sql, user_id, device_id, False) self.device_id_exists_cache.invalidate((user_id, device_id)) @@ -624,12 +624,13 @@ class DeviceStore(DeviceWorkerStore, BackgroundUpdateStore): return sql = """ DELETE FROM devices - WHERE user_id = ? AND device_id IN (%s) AND NOT hidden + WHERE user_id = ? AND device_id IN (%s) AND hidden = ? """ % ( ",".join("?" for _ in device_ids) ) values = [user_id] values.extend(device_ids) + values.append(False) yield self._execute("delete_devices", None, sql, *values) From 73b26f827ccb96a10629ecb0737bd3db4915bb14 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 31 Jul 2019 18:37:05 -0400 Subject: [PATCH 0007/1623] really fix queries to work with Postgres (by going back to not using SQL directly) --- synapse/storage/devices.py | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/synapse/storage/devices.py b/synapse/storage/devices.py index a1f12df907..9f2bb40834 100644 --- a/synapse/storage/devices.py +++ b/synapse/storage/devices.py @@ -601,11 +601,11 @@ class DeviceStore(DeviceWorkerStore, BackgroundUpdateStore): Returns: defer.Deferred """ - sql = """ - DELETE FROM devices - WHERE user_id = ? AND device_id = ? AND hidden = ? - """ - yield self._execute("delete_device", None, sql, user_id, device_id, False) + yield self._simple_delete_one( + table="devices", + keyvalues={"user_id": user_id, "device_id": device_id, "hidden": False}, + desc="delete_device", + ) self.device_id_exists_cache.invalidate((user_id, device_id)) @@ -619,21 +619,13 @@ class DeviceStore(DeviceWorkerStore, BackgroundUpdateStore): Returns: defer.Deferred """ - - if not device_ids or len(device_ids) == 0: - return - sql = """ - DELETE FROM devices - WHERE user_id = ? AND device_id IN (%s) AND hidden = ? - """ % ( - ",".join("?" for _ in device_ids) + yield self._simple_delete_many( + table="devices", + column="device_id", + iterable=device_ids, + keyvalues={"user_id": user_id, "hidden": False}, + desc="delete_devices", ) - values = [user_id] - values.extend(device_ids) - values.append(False) - - yield self._execute("delete_devices", None, sql, *values) - for device_id in device_ids: self.device_id_exists_cache.invalidate((user_id, device_id)) From d78a4fe156e574ad1f28cf3b12ed0bb11bc077f4 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 1 Aug 2019 02:16:09 -0400 Subject: [PATCH 0008/1623] don't need to return the hidden column any more --- synapse/storage/devices.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/storage/devices.py b/synapse/storage/devices.py index 9f2bb40834..991e28ea24 100644 --- a/synapse/storage/devices.py +++ b/synapse/storage/devices.py @@ -52,7 +52,7 @@ class DeviceWorkerStore(SQLBaseStore): return self._simple_select_one( table="devices", keyvalues={"user_id": user_id, "device_id": device_id, "hidden": False}, - retcols=("user_id", "device_id", "display_name", "hidden"), + retcols=("user_id", "device_id", "display_name"), desc="get_device", ) From fac1cdc5626ab2d59861a6aead8a44e7638934ba Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 1 Aug 2019 21:51:19 -0400 Subject: [PATCH 0009/1623] make changes from PR review --- synapse/handlers/e2e_keys.py | 24 ++++++-- .../schema/delta/56/hidden_devices.sql | 41 -------------- .../storage/schema/delta/56/signing_keys.sql | 55 +++++++++++++++++++ synapse/types.py | 4 +- 4 files changed, 75 insertions(+), 49 deletions(-) create mode 100644 synapse/storage/schema/delta/56/signing_keys.sql diff --git a/synapse/handlers/e2e_keys.py b/synapse/handlers/e2e_keys.py index 39f4ec8e60..9081c3f64c 100644 --- a/synapse/handlers/e2e_keys.py +++ b/synapse/handlers/e2e_keys.py @@ -510,9 +510,18 @@ class E2eKeysHandler(object): if not master_key: raise SynapseError(400, "No master key available", Codes.MISSING_PARAM) - master_key_id, master_verify_key = get_verify_key_from_cross_signing_key( - master_key - ) + try: + master_key_id, master_verify_key = get_verify_key_from_cross_signing_key( + master_key + ) + except ValueError: + if "master_key" in keys: + # the invalid key came from the request + raise SynapseError(400, "Invalid master key", Codes.INVALID_PARAM) + else: + # the invalid key came from the database + logger.error("Invalid master key found for user %s", user_id) + raise SynapseError(500, "Invalid master key") # for the other cross-signing keys, make sure that they have valid # signatures from the master key @@ -539,9 +548,12 @@ class E2eKeysHandler(object): yield self.store.set_e2e_cross_signing_key( user_id, "self_signing", self_signing_key ) - deviceids.append( - get_verify_key_from_cross_signing_key(self_signing_key)[1].version - ) + try: + deviceids.append( + get_verify_key_from_cross_signing_key(self_signing_key)[1].version + ) + except ValueError: + raise SynapseError(400, "Invalid self-signing key", Codes.INVALID_PARAM) if "user_signing_key" in keys: yield self.store.set_e2e_cross_signing_key( user_id, "user_signing", user_signing_key diff --git a/synapse/storage/schema/delta/56/hidden_devices.sql b/synapse/storage/schema/delta/56/hidden_devices.sql index e1cd8cc2c1..67f8b20297 100644 --- a/synapse/storage/schema/delta/56/hidden_devices.sql +++ b/synapse/storage/schema/delta/56/hidden_devices.sql @@ -13,47 +13,6 @@ * limitations under the License. */ --- cross-signing keys -CREATE TABLE IF NOT EXISTS e2e_cross_signing_keys ( - user_id TEXT NOT NULL, - -- the type of cross-signing key (master, user_signing, or self_signing) - keytype TEXT NOT NULL, - -- the full key information, as a json-encoded dict - keydata TEXT NOT NULL, - -- time that the key was added - added_ts BIGINT NOT NULL -); - -CREATE UNIQUE INDEX e2e_cross_signing_keys_idx ON e2e_cross_signing_keys(user_id, keytype, added_ts); - --- cross-signing signatures -CREATE TABLE IF NOT EXISTS e2e_cross_signing_signatures ( - -- user who did the signing - user_id TEXT NOT NULL, - -- key used to sign - key_id TEXT NOT NULL, - -- user who was signed - target_user_id TEXT NOT NULL, - -- device/key that was signed - target_device_id TEXT NOT NULL, - -- the actual signature - signature TEXT NOT NULL -); - -CREATE UNIQUE INDEX e2e_cross_signing_signatures_idx ON e2e_cross_signing_signatures(user_id, target_user_id, target_device_id); - --- stream of user signature updates -CREATE TABLE IF NOT EXISTS user_signature_stream ( - -- uses the same stream ID as device list stream - stream_id BIGINT NOT NULL, - -- user who did the signing - from_user_id TEXT NOT NULL, - -- list of users who were signed, as a JSON array - user_ids TEXT NOT NULL -); - -CREATE UNIQUE INDEX user_signature_stream_idx ON user_signature_stream(stream_id); - -- device list needs to know which ones are "real" devices, and which ones are -- just used to avoid collisions ALTER TABLE devices ADD COLUMN hidden BOOLEAN DEFAULT FALSE; diff --git a/synapse/storage/schema/delta/56/signing_keys.sql b/synapse/storage/schema/delta/56/signing_keys.sql new file mode 100644 index 0000000000..6a9ef1782e --- /dev/null +++ b/synapse/storage/schema/delta/56/signing_keys.sql @@ -0,0 +1,55 @@ +/* Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +-- cross-signing keys +CREATE TABLE IF NOT EXISTS e2e_cross_signing_keys ( + user_id TEXT NOT NULL, + -- the type of cross-signing key (master, user_signing, or self_signing) + keytype TEXT NOT NULL, + -- the full key information, as a json-encoded dict + keydata TEXT NOT NULL, + -- time that the key was added + added_ts BIGINT NOT NULL +); + +CREATE UNIQUE INDEX e2e_cross_signing_keys_idx ON e2e_cross_signing_keys(user_id, keytype, added_ts); + +-- cross-signing signatures +CREATE TABLE IF NOT EXISTS e2e_cross_signing_signatures ( + -- user who did the signing + user_id TEXT NOT NULL, + -- key used to sign + key_id TEXT NOT NULL, + -- user who was signed + target_user_id TEXT NOT NULL, + -- device/key that was signed + target_device_id TEXT NOT NULL, + -- the actual signature + signature TEXT NOT NULL +); + +CREATE UNIQUE INDEX e2e_cross_signing_signatures_idx ON e2e_cross_signing_signatures(user_id, target_user_id, target_device_id); + +-- stream of user signature updates +CREATE TABLE IF NOT EXISTS user_signature_stream ( + -- uses the same stream ID as device list stream + stream_id BIGINT NOT NULL, + -- user who did the signing + from_user_id TEXT NOT NULL, + -- list of users who were signed, as a JSON array + user_ids TEXT NOT NULL +); + +CREATE UNIQUE INDEX user_signature_stream_idx ON user_signature_stream(stream_id); diff --git a/synapse/types.py b/synapse/types.py index 7a80471a0c..00bb0743ff 100644 --- a/synapse/types.py +++ b/synapse/types.py @@ -492,10 +492,10 @@ def get_verify_key_from_cross_signing_key(key_info): """ # make sure that exactly one key is provided if "keys" not in key_info: - raise SynapseError(400, "Invalid key") + raise ValueError("Invalid key") keys = key_info["keys"] if len(keys) != 1: - raise SynapseError(400, "Invalid key") + raise ValueError("Invalid key") # and return that one key for key_id, key_data in keys.items(): return (key_id, decode_verify_key_bytes(key_id, decode_base64(key_data))) From d28d1e2d1b056a0c9e2b9f2c92013515a56dd9fb Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 1 Aug 2019 21:52:35 -0400 Subject: [PATCH 0010/1623] add changelog --- changelog.d/5769.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/5769.feature diff --git a/changelog.d/5769.feature b/changelog.d/5769.feature new file mode 100644 index 0000000000..c34257cb8f --- /dev/null +++ b/changelog.d/5769.feature @@ -0,0 +1 @@ +allow uploading of cross-signing keys \ No newline at end of file From 8c9adcc95dee892f90d6acbbe5c54acbf621720b Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 1 Aug 2019 22:09:05 -0400 Subject: [PATCH 0011/1623] fix formatting --- changelog.d/5769.feature | 2 +- tests/handlers/test_e2e_keys.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/changelog.d/5769.feature b/changelog.d/5769.feature index c34257cb8f..bf994ca327 100644 --- a/changelog.d/5769.feature +++ b/changelog.d/5769.feature @@ -1 +1 @@ -allow uploading of cross-signing keys \ No newline at end of file +Allow uploading of cross-signing keys. \ No newline at end of file diff --git a/tests/handlers/test_e2e_keys.py b/tests/handlers/test_e2e_keys.py index 9ae4cb6ea2..a62c52eefa 100644 --- a/tests/handlers/test_e2e_keys.py +++ b/tests/handlers/test_e2e_keys.py @@ -176,7 +176,9 @@ class E2eKeysHandlerTestCase(unittest.TestCase): } yield self.handler.upload_signing_keys_for_user(local_user, keys2) - devices = yield self.handler.query_devices({"device_keys": {local_user: []}}, 0, local_user) + devices = yield self.handler.query_devices( + {"device_keys": {local_user: []}}, 0, local_user + ) self.assertDictEqual(devices["master_keys"], {local_user: keys2["master_key"]}) @defer.inlineCallbacks From f63ba7a7955d077224d4d602cd33bb31fad92fbc Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 12 Aug 2019 15:14:37 -0700 Subject: [PATCH 0012/1623] Cross-signing [1/4] -- hidden devices (#5759) * allow devices to be marked as "hidden" This is a prerequisite for cross-signing, as it allows us to create other things that live within the device namespace, so they can be used for signatures. --- changelog.d/5759.misc | 1 + synapse/storage/devices.py | 38 ++++++++++++++----- synapse/storage/end_to_end_keys.py | 2 +- .../schema/delta/56/hidden_devices.sql | 18 +++++++++ 4 files changed, 49 insertions(+), 10 deletions(-) create mode 100644 changelog.d/5759.misc create mode 100644 synapse/storage/schema/delta/56/hidden_devices.sql diff --git a/changelog.d/5759.misc b/changelog.d/5759.misc new file mode 100644 index 0000000000..c0bc566c4c --- /dev/null +++ b/changelog.d/5759.misc @@ -0,0 +1 @@ +Allow devices to be marked as hidden, for use by features such as cross-signing. \ No newline at end of file diff --git a/synapse/storage/devices.py b/synapse/storage/devices.py index 8f72d92895..991e28ea24 100644 --- a/synapse/storage/devices.py +++ b/synapse/storage/devices.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd +# Copyright 2019 New Vector Ltd +# Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,7 +22,7 @@ from canonicaljson import json from twisted.internet import defer -from synapse.api.errors import StoreError +from synapse.api.errors import Codes, StoreError from synapse.metrics.background_process_metrics import run_as_background_process from synapse.storage._base import Cache, SQLBaseStore, db_to_json from synapse.storage.background_updates import BackgroundUpdateStore @@ -36,7 +38,8 @@ DROP_DEVICE_LIST_STREAMS_NON_UNIQUE_INDEXES = ( class DeviceWorkerStore(SQLBaseStore): def get_device(self, user_id, device_id): - """Retrieve a device. + """Retrieve a device. Only returns devices that are not marked as + hidden. Args: user_id (str): The ID of the user which owns the device @@ -48,14 +51,15 @@ class DeviceWorkerStore(SQLBaseStore): """ return self._simple_select_one( table="devices", - keyvalues={"user_id": user_id, "device_id": device_id}, + keyvalues={"user_id": user_id, "device_id": device_id, "hidden": False}, retcols=("user_id", "device_id", "display_name"), desc="get_device", ) @defer.inlineCallbacks def get_devices_by_user(self, user_id): - """Retrieve all of a user's registered devices. + """Retrieve all of a user's registered devices. Only returns devices + that are not marked as hidden. Args: user_id (str): @@ -66,7 +70,7 @@ class DeviceWorkerStore(SQLBaseStore): """ devices = yield self._simple_select_list( table="devices", - keyvalues={"user_id": user_id}, + keyvalues={"user_id": user_id, "hidden": False}, retcols=("user_id", "device_id", "display_name"), desc="get_devices_by_user", ) @@ -540,6 +544,8 @@ class DeviceStore(DeviceWorkerStore, BackgroundUpdateStore): Returns: defer.Deferred: boolean whether the device was inserted or an existing device existed with that ID. + Raises: + StoreError: if the device is already in use """ key = (user_id, device_id) if self.device_id_exists_cache.get(key, None): @@ -552,12 +558,25 @@ class DeviceStore(DeviceWorkerStore, BackgroundUpdateStore): "user_id": user_id, "device_id": device_id, "display_name": initial_device_display_name, + "hidden": False, }, desc="store_device", or_ignore=True, ) + if not inserted: + # if the device already exists, check if it's a real device, or + # if the device ID is reserved by something else + hidden = yield self._simple_select_one_onecol( + "devices", + keyvalues={"user_id": user_id, "device_id": device_id}, + retcol="hidden", + ) + if hidden: + raise StoreError(400, "The device ID is in use", Codes.FORBIDDEN) self.device_id_exists_cache.prefill(key, True) return inserted + except StoreError: + raise except Exception as e: logger.error( "store_device with device_id=%s(%r) user_id=%s(%r)" @@ -584,7 +603,7 @@ class DeviceStore(DeviceWorkerStore, BackgroundUpdateStore): """ yield self._simple_delete_one( table="devices", - keyvalues={"user_id": user_id, "device_id": device_id}, + keyvalues={"user_id": user_id, "device_id": device_id, "hidden": False}, desc="delete_device", ) @@ -604,14 +623,15 @@ class DeviceStore(DeviceWorkerStore, BackgroundUpdateStore): table="devices", column="device_id", iterable=device_ids, - keyvalues={"user_id": user_id}, + keyvalues={"user_id": user_id, "hidden": False}, desc="delete_devices", ) for device_id in device_ids: self.device_id_exists_cache.invalidate((user_id, device_id)) def update_device(self, user_id, device_id, new_display_name=None): - """Update a device. + """Update a device. Only updates the device if it is not marked as + hidden. Args: user_id (str): The ID of the user which owns the device @@ -630,7 +650,7 @@ class DeviceStore(DeviceWorkerStore, BackgroundUpdateStore): return defer.succeed(None) return self._simple_update_one( table="devices", - keyvalues={"user_id": user_id, "device_id": device_id}, + keyvalues={"user_id": user_id, "device_id": device_id, "hidden": False}, updatevalues=updates, desc="update_device", ) diff --git a/synapse/storage/end_to_end_keys.py b/synapse/storage/end_to_end_keys.py index 1e07474e70..6f524cedd9 100644 --- a/synapse/storage/end_to_end_keys.py +++ b/synapse/storage/end_to_end_keys.py @@ -85,7 +85,7 @@ class EndToEndKeyWorkerStore(SQLBaseStore): " k.key_json" " FROM devices d" " %s JOIN e2e_device_keys_json k USING (user_id, device_id)" - " WHERE %s" + " WHERE %s AND NOT d.hidden" ) % ( "LEFT" if include_all_devices else "INNER", " OR ".join("(" + q + ")" for q in query_clauses), diff --git a/synapse/storage/schema/delta/56/hidden_devices.sql b/synapse/storage/schema/delta/56/hidden_devices.sql new file mode 100644 index 0000000000..67f8b20297 --- /dev/null +++ b/synapse/storage/schema/delta/56/hidden_devices.sql @@ -0,0 +1,18 @@ +/* Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +-- device list needs to know which ones are "real" devices, and which ones are +-- just used to avoid collisions +ALTER TABLE devices ADD COLUMN hidden BOOLEAN DEFAULT FALSE; From 7c3abc65728af052b0d484f9669b1c084cd2faf5 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 21 Aug 2019 13:19:35 -0700 Subject: [PATCH 0013/1623] apply PR review suggestions --- synapse/handlers/e2e_keys.py | 76 +++++++++++++--------------- synapse/rest/client/v2_alpha/keys.py | 2 +- synapse/storage/devices.py | 6 +-- synapse/storage/end_to_end_keys.py | 15 +++--- 4 files changed, 46 insertions(+), 53 deletions(-) diff --git a/synapse/handlers/e2e_keys.py b/synapse/handlers/e2e_keys.py index 9081c3f64c..53ca8330ad 100644 --- a/synapse/handlers/e2e_keys.py +++ b/synapse/handlers/e2e_keys.py @@ -17,6 +17,8 @@ import logging +import time + from six import iteritems from canonicaljson import encode_canonical_json, json @@ -132,7 +134,7 @@ class E2eKeysHandler(object): r[user_id] = remote_queries[user_id] # Get cached cross-signing keys - cross_signing_keys = yield self.query_cross_signing_keys( + cross_signing_keys = yield self.get_cross_signing_keys_from_cache( device_keys_query, from_user_id ) @@ -200,11 +202,11 @@ class E2eKeysHandler(object): for user_id, key in remote_result["master_keys"].items(): if user_id in destination_query: - cross_signing_keys["master"][user_id] = key + cross_signing_keys["master_keys"][user_id] = key for user_id, key in remote_result["self_signing_keys"].items(): if user_id in destination_query: - cross_signing_keys["self_signing"][user_id] = key + cross_signing_keys["self_signing_keys"][user_id] = key except Exception as e: failure = _exception_to_failure(e) @@ -222,14 +224,13 @@ class E2eKeysHandler(object): ret = {"device_keys": results, "failures": failures} - for key, value in iteritems(cross_signing_keys): - ret[key + "_keys"] = value + ret.update(cross_signing_keys) return ret @defer.inlineCallbacks - def query_cross_signing_keys(self, query, from_user_id): - """Get cross-signing keys for users + def get_cross_signing_keys_from_cache(self, query, from_user_id): + """Get cross-signing keys for users from the database Args: query (Iterable[string]) an iterable of user IDs. A dict whose keys @@ -250,43 +251,32 @@ class E2eKeysHandler(object): for user_id in query: # XXX: consider changing the store functions to allow querying # multiple users simultaneously. - try: - key = yield self.store.get_e2e_cross_signing_key( - user_id, "master", from_user_id - ) - if key: - master_keys[user_id] = key - except Exception as e: - logger.info("Error getting master key: %s", e) + key = yield self.store.get_e2e_cross_signing_key( + user_id, "master", from_user_id + ) + if key: + master_keys[user_id] = key - try: - key = yield self.store.get_e2e_cross_signing_key( - user_id, "self_signing", from_user_id - ) - if key: - self_signing_keys[user_id] = key - except Exception as e: - logger.info("Error getting self-signing key: %s", e) + key = yield self.store.get_e2e_cross_signing_key( + user_id, "self_signing", from_user_id + ) + if key: + self_signing_keys[user_id] = key # users can see other users' master and self-signing keys, but can # only see their own user-signing keys if from_user_id == user_id: - try: - key = yield self.store.get_e2e_cross_signing_key( - user_id, "user_signing", from_user_id - ) - if key: - user_signing_keys[user_id] = key - except Exception as e: - logger.info("Error getting user-signing key: %s", e) + key = yield self.store.get_e2e_cross_signing_key( + user_id, "user_signing", from_user_id + ) + if key: + user_signing_keys[user_id] = key - defer.returnValue( - { - "master": master_keys, - "self_signing": self_signing_keys, - "user_signing": user_signing_keys, - } - ) + return { + "master_keys": master_keys, + "self_signing_keys": self_signing_keys, + "user_signing_keys": user_signing_keys, + } @defer.inlineCallbacks def query_local_devices(self, query): @@ -542,11 +532,13 @@ class E2eKeysHandler(object): # if everything checks out, then store the keys and send notifications deviceids = [] if "master_key" in keys: - yield self.store.set_e2e_cross_signing_key(user_id, "master", master_key) + yield self.store.set_e2e_cross_signing_key( + user_id, "master", master_key, time.time() * 1000 + ) deviceids.append(master_verify_key.version) if "self_signing_key" in keys: yield self.store.set_e2e_cross_signing_key( - user_id, "self_signing", self_signing_key + user_id, "self_signing", self_signing_key, time.time() * 1000 ) try: deviceids.append( @@ -556,7 +548,7 @@ class E2eKeysHandler(object): raise SynapseError(400, "Invalid self-signing key", Codes.INVALID_PARAM) if "user_signing_key" in keys: yield self.store.set_e2e_cross_signing_key( - user_id, "user_signing", user_signing_key + user_id, "user_signing", user_signing_key, time.time() * 1000 ) # the signature stream matches the semantics that we want for # user-signing key updates: only the user themselves is notified of @@ -568,7 +560,7 @@ class E2eKeysHandler(object): if len(deviceids): yield self.device_handler.notify_device_update(user_id, deviceids) - defer.returnValue({}) + return {} def _check_cross_signing_key(key, user_id, key_type, signing_key=None): diff --git a/synapse/rest/client/v2_alpha/keys.py b/synapse/rest/client/v2_alpha/keys.py index f40a785598..1340d2c80d 100644 --- a/synapse/rest/client/v2_alpha/keys.py +++ b/synapse/rest/client/v2_alpha/keys.py @@ -263,7 +263,7 @@ class SigningKeyUploadServlet(RestServlet): ) result = yield self.e2e_keys_handler.upload_signing_keys_for_user(user_id, body) - defer.returnValue((200, result)) + return (200, result) def register_servlets(hs, http_server): diff --git a/synapse/storage/devices.py b/synapse/storage/devices.py index da23a350a1..6a5572e001 100644 --- a/synapse/storage/devices.py +++ b/synapse/storage/devices.py @@ -317,7 +317,7 @@ class DeviceWorkerStore(SQLBaseStore): user_ids, stream_id, ) - defer.returnValue(stream_id) + return stream_id def _add_user_signature_change_txn(self, txn, from_user_id, user_ids, stream_id): txn.call_after( @@ -491,9 +491,9 @@ class DeviceWorkerStore(SQLBaseStore): rows = yield self._execute( "get_users_whose_signatures_changed", None, sql, user_id, from_key ) - defer.returnValue(set(user for row in rows for user in json.loads(row[0]))) + return set(user for row in rows for user in json.loads(row[0])) else: - defer.returnValue(set()) + return set() def get_all_device_list_changes_for_remotes(self, from_key, to_key): """Return a list of `(stream_id, user_id, destination)` which is the diff --git a/synapse/storage/end_to_end_keys.py b/synapse/storage/end_to_end_keys.py index de2d1bbb9f..b218b7b2e8 100644 --- a/synapse/storage/end_to_end_keys.py +++ b/synapse/storage/end_to_end_keys.py @@ -14,8 +14,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import time - from six import iteritems from canonicaljson import encode_canonical_json, json @@ -284,7 +282,7 @@ class EndToEndKeyStore(EndToEndKeyWorkerStore, SQLBaseStore): "delete_e2e_keys_by_device", delete_e2e_keys_by_device_txn ) - def _set_e2e_cross_signing_key_txn(self, txn, user_id, key_type, key): + def _set_e2e_cross_signing_key_txn(self, txn, user_id, key_type, key, added_ts): """Set a user's cross-signing key. Args: @@ -294,6 +292,7 @@ class EndToEndKeyStore(EndToEndKeyWorkerStore, SQLBaseStore): for a master key, 'self_signing' for a self-signing key, or 'user_signing' for a user-signing key key (dict): the key data + added_ts (int): the timestamp for when the key was added """ # the cross-signing keys need to occupy the same namespace as devices, # since signatures are identified by device ID. So add an entry to the @@ -334,18 +333,19 @@ class EndToEndKeyStore(EndToEndKeyWorkerStore, SQLBaseStore): "user_id": user_id, "keytype": key_type, "keydata": json.dumps(key), - "added_ts": time.time() * 1000, + "added_ts": added_ts, }, desc="store_master_key", ) - def set_e2e_cross_signing_key(self, user_id, key_type, key): + def set_e2e_cross_signing_key(self, user_id, key_type, key, added_ts): """Set a user's cross-signing key. Args: user_id (str): the user to set the user-signing key for key_type (str): the type of cross-signing key to set key (dict): the key data + added_ts (int): the timestamp for when the key was added """ return self.runInteraction( "add_e2e_cross_signing_key", @@ -353,6 +353,7 @@ class EndToEndKeyStore(EndToEndKeyWorkerStore, SQLBaseStore): user_id, key_type, key, + added_ts, ) def _get_e2e_cross_signing_key_txn(self, txn, user_id, key_type, from_user_id=None): @@ -368,7 +369,7 @@ class EndToEndKeyStore(EndToEndKeyWorkerStore, SQLBaseStore): the key will be included in the result Returns: - dict of the key data + dict of the key data or None if not found """ sql = ( "SELECT keydata " @@ -412,7 +413,7 @@ class EndToEndKeyStore(EndToEndKeyWorkerStore, SQLBaseStore): the self-signing key will be included in the result Returns: - dict of the key data + dict of the key data or None if not found """ return self.runInteraction( "get_e2e_cross_signing_key", From 814f253f1b102475fe0baace8b65e2281e7b6a89 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 21 Aug 2019 13:22:15 -0700 Subject: [PATCH 0014/1623] make isort happy --- synapse/handlers/e2e_keys.py | 1 - 1 file changed, 1 deletion(-) diff --git a/synapse/handlers/e2e_keys.py b/synapse/handlers/e2e_keys.py index 53ca8330ad..be15597ee8 100644 --- a/synapse/handlers/e2e_keys.py +++ b/synapse/handlers/e2e_keys.py @@ -16,7 +16,6 @@ # limitations under the License. import logging - import time from six import iteritems From 3b0b22cb059f7dfd1d7a7878fe391be38ee91d71 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 28 Aug 2019 17:17:21 -0700 Subject: [PATCH 0015/1623] use stream ID generator instead of timestamp --- synapse/handlers/e2e_keys.py | 7 ++--- synapse/storage/__init__.py | 3 ++ synapse/storage/end_to_end_keys.py | 30 +++++++++---------- .../storage/schema/delta/56/signing_keys.sql | 6 ++-- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/synapse/handlers/e2e_keys.py b/synapse/handlers/e2e_keys.py index be15597ee8..d2d9bef1fe 100644 --- a/synapse/handlers/e2e_keys.py +++ b/synapse/handlers/e2e_keys.py @@ -16,7 +16,6 @@ # limitations under the License. import logging -import time from six import iteritems @@ -532,12 +531,12 @@ class E2eKeysHandler(object): deviceids = [] if "master_key" in keys: yield self.store.set_e2e_cross_signing_key( - user_id, "master", master_key, time.time() * 1000 + user_id, "master", master_key ) deviceids.append(master_verify_key.version) if "self_signing_key" in keys: yield self.store.set_e2e_cross_signing_key( - user_id, "self_signing", self_signing_key, time.time() * 1000 + user_id, "self_signing", self_signing_key ) try: deviceids.append( @@ -547,7 +546,7 @@ class E2eKeysHandler(object): raise SynapseError(400, "Invalid self-signing key", Codes.INVALID_PARAM) if "user_signing_key" in keys: yield self.store.set_e2e_cross_signing_key( - user_id, "user_signing", user_signing_key, time.time() * 1000 + user_id, "user_signing", user_signing_key ) # the signature stream matches the semantics that we want for # user-signing key updates: only the user themselves is notified of diff --git a/synapse/storage/__init__.py b/synapse/storage/__init__.py index 0a64f90624..e9a9c2cd8d 100644 --- a/synapse/storage/__init__.py +++ b/synapse/storage/__init__.py @@ -136,6 +136,9 @@ class DataStore( self._device_list_id_gen = StreamIdGenerator( db_conn, "device_lists_stream", "stream_id" ) + self._cross_signing_id_gen = StreamIdGenerator( + db_conn, "e2e_cross_signing_keys", "stream_id" + ) self._access_tokens_id_gen = IdGenerator(db_conn, "access_tokens", "id") self._event_reports_id_gen = IdGenerator(db_conn, "event_reports", "id") diff --git a/synapse/storage/end_to_end_keys.py b/synapse/storage/end_to_end_keys.py index b218b7b2e8..4b37bffb0b 100644 --- a/synapse/storage/end_to_end_keys.py +++ b/synapse/storage/end_to_end_keys.py @@ -282,7 +282,7 @@ class EndToEndKeyStore(EndToEndKeyWorkerStore, SQLBaseStore): "delete_e2e_keys_by_device", delete_e2e_keys_by_device_txn ) - def _set_e2e_cross_signing_key_txn(self, txn, user_id, key_type, key, added_ts): + def _set_e2e_cross_signing_key_txn(self, txn, user_id, key_type, key): """Set a user's cross-signing key. Args: @@ -292,7 +292,6 @@ class EndToEndKeyStore(EndToEndKeyWorkerStore, SQLBaseStore): for a master key, 'self_signing' for a self-signing key, or 'user_signing' for a user-signing key key (dict): the key data - added_ts (int): the timestamp for when the key was added """ # the cross-signing keys need to occupy the same namespace as devices, # since signatures are identified by device ID. So add an entry to the @@ -327,25 +326,25 @@ class EndToEndKeyStore(EndToEndKeyWorkerStore, SQLBaseStore): ) # and finally, store the key itself - self._simple_insert( - "e2e_cross_signing_keys", - values={ - "user_id": user_id, - "keytype": key_type, - "keydata": json.dumps(key), - "added_ts": added_ts, - }, - desc="store_master_key", - ) + with self._cross_signing_id_gen.get_next() as stream_id: + self._simple_insert( + "e2e_cross_signing_keys", + values={ + "user_id": user_id, + "keytype": key_type, + "keydata": json.dumps(key), + "stream_id": stream_id, + }, + desc="store_master_key", + ) - def set_e2e_cross_signing_key(self, user_id, key_type, key, added_ts): + def set_e2e_cross_signing_key(self, user_id, key_type, key): """Set a user's cross-signing key. Args: user_id (str): the user to set the user-signing key for key_type (str): the type of cross-signing key to set key (dict): the key data - added_ts (int): the timestamp for when the key was added """ return self.runInteraction( "add_e2e_cross_signing_key", @@ -353,7 +352,6 @@ class EndToEndKeyStore(EndToEndKeyWorkerStore, SQLBaseStore): user_id, key_type, key, - added_ts, ) def _get_e2e_cross_signing_key_txn(self, txn, user_id, key_type, from_user_id=None): @@ -374,7 +372,7 @@ class EndToEndKeyStore(EndToEndKeyWorkerStore, SQLBaseStore): sql = ( "SELECT keydata " " FROM e2e_cross_signing_keys " - " WHERE user_id = ? AND keytype = ? ORDER BY added_ts DESC LIMIT 1" + " WHERE user_id = ? AND keytype = ? ORDER BY stream_id DESC LIMIT 1" ) txn.execute(sql, (user_id, key_type)) row = txn.fetchone() diff --git a/synapse/storage/schema/delta/56/signing_keys.sql b/synapse/storage/schema/delta/56/signing_keys.sql index 6a9ef1782e..27a96123e3 100644 --- a/synapse/storage/schema/delta/56/signing_keys.sql +++ b/synapse/storage/schema/delta/56/signing_keys.sql @@ -20,11 +20,11 @@ CREATE TABLE IF NOT EXISTS e2e_cross_signing_keys ( keytype TEXT NOT NULL, -- the full key information, as a json-encoded dict keydata TEXT NOT NULL, - -- time that the key was added - added_ts BIGINT NOT NULL + -- for keeping the keys in order, so that we can fetch the latest one + stream_id BIGINT NOT NULL ); -CREATE UNIQUE INDEX e2e_cross_signing_keys_idx ON e2e_cross_signing_keys(user_id, keytype, added_ts); +CREATE UNIQUE INDEX e2e_cross_signing_keys_idx ON e2e_cross_signing_keys(user_id, keytype, stream_id); -- cross-signing signatures CREATE TABLE IF NOT EXISTS e2e_cross_signing_signatures ( From 96bda563701795537c39d56d82869d953a6bf167 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 28 Aug 2019 17:18:40 -0700 Subject: [PATCH 0016/1623] black --- synapse/handlers/e2e_keys.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/synapse/handlers/e2e_keys.py b/synapse/handlers/e2e_keys.py index d2d9bef1fe..870810e6ea 100644 --- a/synapse/handlers/e2e_keys.py +++ b/synapse/handlers/e2e_keys.py @@ -530,9 +530,7 @@ class E2eKeysHandler(object): # if everything checks out, then store the keys and send notifications deviceids = [] if "master_key" in keys: - yield self.store.set_e2e_cross_signing_key( - user_id, "master", master_key - ) + yield self.store.set_e2e_cross_signing_key(user_id, "master", master_key) deviceids.append(master_verify_key.version) if "self_signing_key" in keys: yield self.store.set_e2e_cross_signing_key( From a22d58c96c714e5f97b3e68f3ec7f2aeee854a81 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 4 Sep 2019 19:32:35 -0400 Subject: [PATCH 0017/1623] add user signature stream change cache to slaved device store --- synapse/replication/slave/storage/devices.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/synapse/replication/slave/storage/devices.py b/synapse/replication/slave/storage/devices.py index d9300fce33..f045e1b937 100644 --- a/synapse/replication/slave/storage/devices.py +++ b/synapse/replication/slave/storage/devices.py @@ -33,6 +33,9 @@ class SlavedDeviceStore(EndToEndKeyWorkerStore, DeviceWorkerStore, BaseSlavedSto self._device_list_stream_cache = StreamChangeCache( "DeviceListStreamChangeCache", device_list_max ) + self._user_signature_stream_cache = StreamChangeCache( + "UserSignatureStreamChangeCache", device_list_max + ) self._device_list_federation_stream_cache = StreamChangeCache( "DeviceListFederationStreamChangeCache", device_list_max ) From 4bb454478470c6b707d33292113ac3a23010db8b Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 22 May 2019 16:41:24 -0400 Subject: [PATCH 0018/1623] implement device signature uploading/fetching --- synapse/handlers/e2e_keys.py | 250 +++++++++++++++++++++++++++ synapse/rest/client/v2_alpha/keys.py | 50 ++++++ synapse/storage/end_to_end_keys.py | 38 ++++ 3 files changed, 338 insertions(+) diff --git a/synapse/handlers/e2e_keys.py b/synapse/handlers/e2e_keys.py index 997ad66f8f..9747b517ff 100644 --- a/synapse/handlers/e2e_keys.py +++ b/synapse/handlers/e2e_keys.py @@ -608,6 +608,194 @@ class E2eKeysHandler(object): return {} + @defer.inlineCallbacks + def upload_signatures_for_device_keys(self, user_id, signatures): + """Upload device signatures for cross-signing + + Args: + user_id (string): the user uploading the signatures + signatures (dict[string, dict[string, dict]]): map of users to + devices to signed keys + """ + failures = {} + + signature_list = [] # signatures to be stored + self_device_ids = [] # what devices have been updated, for notifying + + # split between checking signatures for own user and signatures for + # other users, since we verify them with different keys + if user_id in signatures: + self_signatures = signatures[user_id] + del signatures[user_id] + self_device_ids = list(self_signatures.keys()) + try: + # get our self-signing key to verify the signatures + self_signing_key = yield self.store.get_e2e_cross_signing_key( + user_id, "self_signing" + ) + if self_signing_key is None: + raise SynapseError( + 404, + "No self-signing key found", + Codes.NOT_FOUND + ) + + self_signing_key_id, self_signing_verify_key \ + = get_verify_key_from_cross_signing_key(self_signing_key) + + # fetch our stored devices so that we can compare with what was sent + user_devices = [] + for device in self_signatures.keys(): + user_devices.append((user_id, device)) + devices = yield self.store.get_e2e_device_keys(user_devices) + + if user_id not in devices: + raise SynapseError( + 404, + "No device key found", + Codes.NOT_FOUND + ) + + devices = devices[user_id] + for device_id, device in self_signatures.items(): + try: + if ("signatures" not in device or + user_id not in device["signatures"] or + self_signing_key_id not in device["signatures"][user_id]): + # no signature was sent + raise SynapseError( + 400, + "Invalid signature", + Codes.INVALID_SIGNATURE + ) + + stored_device = devices[device_id]["keys"] + if self_signing_key_id in stored_device.get("signatures", {}) \ + .get(user_id, {}): + # we already have a signature on this device, so we + # can skip it, since it should be exactly the same + continue + + _check_device_signature( + user_id, self_signing_verify_key, device, stored_device + ) + + signature = device["signatures"][user_id][self_signing_key_id] + signature_list.append( + (self_signing_key_id, user_id, device_id, signature) + ) + except SynapseError as e: + failures.setdefault(user_id, {})[device_id] \ + = _exception_to_failure(e) + except SynapseError as e: + failures[user_id] = { + device: _exception_to_failure(e) + for device in self_signatures.keys() + } + + signed_users = [] # what user have been signed, for notifying + if len(signatures): + # if signatures isn't empty, then we have signatures for other + # users. These signatures will be signed by the user signing key + + # get our user-signing key to verify the signatures + user_signing_key = yield self.store.get_e2e_cross_signing_key( + user_id, "user_signing" + ) + if user_signing_key is None: + for user, devicemap in signatures.items(): + failures[user] = { + device: _exception_to_failure(SynapseError( + 404, + "No user-signing key found", + Codes.NOT_FOUND + )) + for device in devicemap.keys() + } + else: + user_signing_key_id, user_signing_verify_key \ + = get_verify_key_from_cross_signing_key(user_signing_key) + + for user, devicemap in signatures.items(): + device_id = None + try: + # get the user's master key, to make sure it matches + # what was sent + stored_key = yield self.store.get_e2e_cross_signing_key( + user, "master", user_id + ) + if stored_key is None: + logger.error( + "upload signature: no user key found for %s", user + ) + raise SynapseError( + 404, + "User's master key not found", + Codes.NOT_FOUND + ) + + # make sure that the user's master key is the one that + # was signed (and no others) + device_id = get_verify_key_from_cross_signing_key(stored_key)[0] \ + .split(":", 1)[1] + if device_id not in devicemap: + logger.error( + "upload signature: wrong device: %s vs %s", + device, devicemap + ) + raise SynapseError( + 404, + "Unknown device", + Codes.NOT_FOUND + ) + if len(devicemap) > 1: + logger.error("upload signature: too many devices specified") + failures[user] = { + device: _exception_to_failure(SynapseError( + 404, + "Unknown device", + Codes.NOT_FOUND + )) + for device in devicemap.keys() + } + + key = devicemap[device_id] + + if user_signing_key_id in stored_key.get("signatures", {}) \ + .get(user_id, {}): + # we already have the signature, so we can skip it + continue + + _check_device_signature( + user_id, user_signing_verify_key, key, stored_key + ) + + signature = key["signatures"][user_id][user_signing_key_id] + + signed_users.append(user) + signature_list.append( + (user_signing_key_id, user, device_id, signature) + ) + except SynapseError as e: + if device_id is None: + failures[user] = { + device_id: _exception_to_failure(e) + for device_id in devicemap.keys() + } + else: + failures.setdefault(user, {})[device_id] \ + = _exception_to_failure(e) + + # store the signature, and send the appropriate notifications for sync + logger.debug("upload signature failures: %r", failures) + yield self.store.store_e2e_device_signatures(user_id, signature_list) + + if len(self_device_ids): + yield self.device_handler.notify_device_update(user_id, self_device_ids) + if len(signed_users): + yield self.device_handler.notify_user_signature_update(user_id, signed_users) + + defer.returnValue({"failures": failures}) def _check_cross_signing_key(key, user_id, key_type, signing_key=None): """Check a cross-signing key uploaded by a user. Performs some basic sanity @@ -636,6 +824,68 @@ def _check_cross_signing_key(key, user_id, key_type, signing_key=None): ) +def _check_device_signature(user_id, verify_key, signed_device, stored_device): + """Check that a device signature is correct and matches the copy of the device + that we have. Throws an exception if an error is detected. + + Args: + user_id (str): the user ID whose signature is being checked + verify_key (VerifyKey): the key to verify the device with + signed_device (dict): the signed device data + stored_device (dict): our previous copy of the device + """ + + key_id = "%s:%s" % (verify_key.alg, verify_key.version) + + # make sure the device is signed + if ("signatures" not in signed_device or user_id not in signed_device["signatures"] + or key_id not in signed_device["signatures"][user_id]): + logger.error("upload signature: user not found in signatures") + raise SynapseError( + 400, + "Invalid signature", + Codes.INVALID_SIGNATURE + ) + + signature = signed_device["signatures"][user_id][key_id] + + # make sure that the device submitted matches what we have stored + del signed_device["signatures"] + if "unsigned" in signed_device: + del signed_device["unsigned"] + if "signatures" in stored_device: + del stored_device["signatures"] + if "unsigned" in stored_device: + del stored_device["unsigned"] + if signed_device != stored_device: + logger.error( + "upload signatures: key does not match %s vs %s", + signed_device, stored_device + ) + raise SynapseError( + 400, + "Key does not match", + "M_MISMATCHED_KEY" + ) + + # check the signature + signed_device["signatures"] = { + user_id: { + key_id: signature + } + } + + try: + verify_signed_json(signed_device, user_id, verify_key) + except SignatureVerifyException: + logger.error("invalid signature on key") + raise SynapseError( + 400, + "Invalid signature", + Codes.INVALID_SIGNATURE + ) + + def _exception_to_failure(e): if isinstance(e, CodeMessageException): return {"status": e.code, "message": str(e)} diff --git a/synapse/rest/client/v2_alpha/keys.py b/synapse/rest/client/v2_alpha/keys.py index 151a70d449..5c288d48b7 100644 --- a/synapse/rest/client/v2_alpha/keys.py +++ b/synapse/rest/client/v2_alpha/keys.py @@ -277,9 +277,59 @@ class SigningKeyUploadServlet(RestServlet): return (200, result) +class SignaturesUploadServlet(RestServlet): + """ + POST /keys/signatures/upload HTTP/1.1 + Content-Type: application/json + + { + "@alice:example.com": { + "": { + "user_id": "", + "device_id": "", + "algorithms": [ + "m.olm.curve25519-aes-sha256", + "m.megolm.v1.aes-sha" + ], + "keys": { + ":": "", + }, + "signatures": { + "": { + ":": ">" + } + } + } + } + } + """ + PATTERNS = client_v2_patterns("/keys/signatures/upload$") + + def __init__(self, hs): + """ + Args: + hs (synapse.server.HomeServer): server + """ + super(SignaturesUploadServlet, self).__init__() + self.auth = hs.get_auth() + self.e2e_keys_handler = hs.get_e2e_keys_handler() + + @defer.inlineCallbacks + def on_POST(self, request): + requester = yield self.auth.get_user_by_req(request, allow_guest=True) + user_id = requester.user.to_string() + body = parse_json_object_from_request(request) + + result = yield self.e2e_keys_handler.upload_signatures_for_device_keys( + user_id, body + ) + defer.returnValue((200, result)) + + def register_servlets(hs, http_server): KeyUploadServlet(hs).register(http_server) KeyQueryServlet(hs).register(http_server) KeyChangesServlet(hs).register(http_server) OneTimeKeyServlet(hs).register(http_server) SigningKeyUploadServlet(hs).register(http_server) + SignaturesUploadServlet(hs).register(http_server) diff --git a/synapse/storage/end_to_end_keys.py b/synapse/storage/end_to_end_keys.py index 8ce5dd8bf9..fe786f3093 100644 --- a/synapse/storage/end_to_end_keys.py +++ b/synapse/storage/end_to_end_keys.py @@ -59,6 +59,12 @@ class EndToEndKeyWorkerStore(SQLBaseStore): for user_id, device_keys in iteritems(results): for device_id, device_info in iteritems(device_keys): device_info["keys"] = db_to_json(device_info.pop("key_json")) + # add cross-signing signatures to the keys + if "signatures" in device_info: + for sig_user_id, sigs in device_info["signatures"].items(): + device_info["keys"].setdefault("signatures", {}) \ + .setdefault(sig_user_id, {}) \ + .update(sigs) return results @@ -71,6 +77,8 @@ class EndToEndKeyWorkerStore(SQLBaseStore): query_clauses = [] query_params = [] + signature_query_clauses = [] + signature_query_params = [] if include_all_devices is False: include_deleted_devices = False @@ -81,12 +89,20 @@ class EndToEndKeyWorkerStore(SQLBaseStore): for (user_id, device_id) in query_list: query_clause = "user_id = ?" query_params.append(user_id) + signature_query_clause = "target_user_id = ?" + signature_query_params.append(user_id) if device_id is not None: query_clause += " AND device_id = ?" query_params.append(device_id) + signature_query_clause += " AND target_device_id = ?" + signature_query_params.append(device_id) + + signature_query_clause += " AND user_id = ?" + signature_query_params.append(user_id) query_clauses.append(query_clause) + signature_query_clauses.append(signature_query_clause) sql = ( "SELECT user_id, device_id, " @@ -113,6 +129,28 @@ class EndToEndKeyWorkerStore(SQLBaseStore): for user_id, device_id in deleted_devices: result.setdefault(user_id, {})[device_id] = None + # get signatures on the device + signature_sql = ( + "SELECT * " + " FROM e2e_device_signatures " + " WHERE %s" + ) % ( + " OR ".join("(" + q + ")" for q in signature_query_clauses) + ) + + txn.execute(signature_sql, signature_query_params) + rows = self.cursor_to_dict(txn) + + for row in rows: + target_user_id = row["target_user_id"] + target_device_id = row["target_device_id"] + if target_user_id in result \ + and target_device_id in result[target_user_id]: + result[target_user_id][target_device_id] \ + .setdefault("signatures", {}) \ + .setdefault(row["user_id"], {})[row["key_id"]] \ + = row["signature"] + log_kv(result) return result From ac4746ac4bb4d9371c5a25e94ecccd83effb8b9a Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 17 Jul 2019 22:11:31 -0400 Subject: [PATCH 0019/1623] allow uploading signatures of master key signed by devices --- synapse/handlers/e2e_keys.py | 232 +++++++++++++++++---------- synapse/rest/client/v2_alpha/keys.py | 2 +- synapse/storage/end_to_end_keys.py | 2 +- tests/handlers/test_e2e_keys.py | 227 +++++++++++++++++++++++++- 4 files changed, 378 insertions(+), 85 deletions(-) diff --git a/synapse/handlers/e2e_keys.py b/synapse/handlers/e2e_keys.py index 9747b517ff..1148803c1e 100644 --- a/synapse/handlers/e2e_keys.py +++ b/synapse/handlers/e2e_keys.py @@ -20,7 +20,9 @@ import logging from six import iteritems from canonicaljson import encode_canonical_json, json +from signedjson.key import decode_verify_key_bytes from signedjson.sign import SignatureVerifyException, verify_signed_json +from unpaddedbase64 import decode_base64 from twisted.internet import defer @@ -619,8 +621,11 @@ class E2eKeysHandler(object): """ failures = {} - signature_list = [] # signatures to be stored - self_device_ids = [] # what devices have been updated, for notifying + # signatures to be stored. Each item will be a tuple of + # (signing_key_id, target_user_id, target_device_id, signature) + signature_list = [] + # what devices have been updated, for notifying + self_device_ids = [] # split between checking signatures for own user and signatures for # other users, since we verify them with different keys @@ -630,46 +635,107 @@ class E2eKeysHandler(object): self_device_ids = list(self_signatures.keys()) try: # get our self-signing key to verify the signatures - self_signing_key = yield self.store.get_e2e_cross_signing_key( - user_id, "self_signing" - ) - if self_signing_key is None: - raise SynapseError( - 404, - "No self-signing key found", - Codes.NOT_FOUND + self_signing_key, self_signing_key_id, self_signing_verify_key \ + = yield self._get_e2e_cross_signing_verify_key( + user_id, "self_signing" ) - self_signing_key_id, self_signing_verify_key \ - = get_verify_key_from_cross_signing_key(self_signing_key) + # get our master key, since it may be signed + master_key, master_key_id, master_verify_key \ + = yield self._get_e2e_cross_signing_verify_key( + user_id, "master" + ) - # fetch our stored devices so that we can compare with what was sent - user_devices = [] - for device in self_signatures.keys(): - user_devices.append((user_id, device)) - devices = yield self.store.get_e2e_device_keys(user_devices) + # fetch our stored devices. This is used to 1. verify + # signatures on the master key, and 2. to can compare with what + # was sent if the device was signed + devices = yield self.store.get_e2e_device_keys([(user_id, None)]) if user_id not in devices: raise SynapseError( - 404, - "No device key found", - Codes.NOT_FOUND + 404, "No device keys found", Codes.NOT_FOUND ) devices = devices[user_id] for device_id, device in self_signatures.items(): try: if ("signatures" not in device or - user_id not in device["signatures"] or - self_signing_key_id not in device["signatures"][user_id]): + user_id not in device["signatures"]): # no signature was sent raise SynapseError( - 400, - "Invalid signature", - Codes.INVALID_SIGNATURE + 400, "Invalid signature", Codes.INVALID_SIGNATURE ) - stored_device = devices[device_id]["keys"] + if device_id == master_verify_key.version: + # we have master key signed by devices: for each + # device that signed, check the signature. Since + # the "failures" property in the response only has + # granularity up to the signed device, either all + # of the signatures on the master key succeed, or + # all fail. So loop over the signatures and add + # them to a separate signature list. If everything + # works out, then add them all to the main + # signature list. (In practice, we're likely to + # only have only one signature anyways.) + master_key_signature_list = [] + for signing_key_id, signature in device["signatures"][user_id].items(): + alg, signing_device_id = signing_key_id.split(":", 1) + if (signing_device_id not in devices or + signing_key_id not in + devices[signing_device_id]["keys"]["keys"]): + # signed by an unknown device, or the + # device does not have the key + raise SynapseError( + 400, "Invalid signature", Codes.INVALID_SIGNATURE + ) + + sigs = device["signatures"] + del device["signatures"] + # use pop to avoid exception if key doesn't exist + device.pop("unsigned", None) + master_key.pop("signature", None) + master_key.pop("unsigned", None) + + if master_key != device: + raise SynapseError( + 400, "Key does not match" + ) + + # get the key and check the signature + pubkey = devices[signing_device_id]["keys"]["keys"][signing_key_id] + verify_key = decode_verify_key_bytes( + signing_key_id, decode_base64(pubkey) + ) + device["signatures"] = sigs + try: + verify_signed_json(device, user_id, verify_key) + except SignatureVerifyException: + raise SynapseError( + 400, "Invalid signature", Codes.INVALID_SIGNATURE + ) + + master_key_signature_list.append( + (signing_key_id, user_id, device_id, signature) + ) + + signature_list.extend(master_key_signature_list) + continue + + # at this point, we have a device that should be signed + # by the self-signing key + if self_signing_key_id not in device["signatures"][user_id]: + # no signature was sent + raise SynapseError( + 400, "Invalid signature", Codes.INVALID_SIGNATURE + ) + + stored_device = None + try: + stored_device = devices[device_id]["keys"] + except KeyError: + raise SynapseError( + 404, "Unknown device", Codes.NOT_FOUND + ) if self_signing_key_id in stored_device.get("signatures", {}) \ .get(user_id, {}): # we already have a signature on this device, so we @@ -698,69 +764,50 @@ class E2eKeysHandler(object): # if signatures isn't empty, then we have signatures for other # users. These signatures will be signed by the user signing key - # get our user-signing key to verify the signatures - user_signing_key = yield self.store.get_e2e_cross_signing_key( - user_id, "user_signing" - ) - if user_signing_key is None: - for user, devicemap in signatures.items(): - failures[user] = { - device: _exception_to_failure(SynapseError( - 404, - "No user-signing key found", - Codes.NOT_FOUND - )) - for device in devicemap.keys() - } - else: - user_signing_key_id, user_signing_verify_key \ - = get_verify_key_from_cross_signing_key(user_signing_key) + try: + # get our user-signing key to verify the signatures + user_signing_key, user_signing_key_id, user_signing_verify_key \ + = yield self._get_e2e_cross_signing_verify_key( + user_id, "user_signing" + ) for user, devicemap in signatures.items(): device_id = None try: # get the user's master key, to make sure it matches # what was sent - stored_key = yield self.store.get_e2e_cross_signing_key( - user, "master", user_id - ) - if stored_key is None: - logger.error( - "upload signature: no user key found for %s", user - ) - raise SynapseError( - 404, - "User's master key not found", - Codes.NOT_FOUND + stored_key, stored_key_id, _ \ + = yield self._get_e2e_cross_signing_verify_key( + user, "master", user_id ) # make sure that the user's master key is the one that # was signed (and no others) - device_id = get_verify_key_from_cross_signing_key(stored_key)[0] \ - .split(":", 1)[1] + device_id = stored_key_id.split(":", 1)[1] if device_id not in devicemap: + # set device to None so that the failure gets + # marked on all the signatures + device_id = None logger.error( "upload signature: wrong device: %s vs %s", device, devicemap ) raise SynapseError( - 404, - "Unknown device", - Codes.NOT_FOUND + 404, "Unknown device", Codes.NOT_FOUND ) - if len(devicemap) > 1: + key = devicemap[device_id] + del devicemap[device_id] + if len(devicemap) > 0: + # other devices were signed -- mark those as failures logger.error("upload signature: too many devices specified") + failure = _exception_to_failure(SynapseError( + 404, "Unknown device", Codes.NOT_FOUND + )) failures[user] = { - device: _exception_to_failure(SynapseError( - 404, - "Unknown device", - Codes.NOT_FOUND - )) + device: failure for device in devicemap.keys() } - key = devicemap[device_id] - if user_signing_key_id in stored_key.get("signatures", {}) \ .get(user_id, {}): # we already have the signature, so we can skip it @@ -770,25 +817,31 @@ class E2eKeysHandler(object): user_id, user_signing_verify_key, key, stored_key ) - signature = key["signatures"][user_id][user_signing_key_id] - signed_users.append(user) + signature = key["signatures"][user_id][user_signing_key_id] signature_list.append( (user_signing_key_id, user, device_id, signature) ) except SynapseError as e: + failure = _exception_to_failure(e) if device_id is None: failures[user] = { - device_id: _exception_to_failure(e) + device_id: failure for device_id in devicemap.keys() } else: - failures.setdefault(user, {})[device_id] \ - = _exception_to_failure(e) + failures.setdefault(user, {})[device_id] = failure + except SynapseError as e: + failure = _exception_to_failure(e) + for user, devicemap in signature.items(): + failures[user] = { + device_id: failure + for device_id in devicemap.keys() + } # store the signature, and send the appropriate notifications for sync logger.debug("upload signature failures: %r", failures) - yield self.store.store_e2e_device_signatures(user_id, signature_list) + yield self.store.store_e2e_cross_signing_signatures(user_id, signature_list) if len(self_device_ids): yield self.device_handler.notify_device_update(user_id, self_device_ids) @@ -797,6 +850,22 @@ class E2eKeysHandler(object): defer.returnValue({"failures": failures}) + @defer.inlineCallbacks + def _get_e2e_cross_signing_verify_key(self, user_id, key_type, from_user_id=None): + key = yield self.store.get_e2e_cross_signing_key( + user_id, key_type, from_user_id + ) + if key is None: + logger.error("no %s key found for %s", key_type, user_id) + raise SynapseError( + 404, + "No %s key found for %s" % (key_type, user_id), + Codes.NOT_FOUND + ) + key_id, verify_key = get_verify_key_from_cross_signing_key(key) + return key, key_id, verify_key + + def _check_cross_signing_key(key, user_id, key_type, signing_key=None): """Check a cross-signing key uploaded by a user. Performs some basic sanity checking, and ensures that it is signed, if a signature is required. @@ -851,21 +920,17 @@ def _check_device_signature(user_id, verify_key, signed_device, stored_device): # make sure that the device submitted matches what we have stored del signed_device["signatures"] - if "unsigned" in signed_device: - del signed_device["unsigned"] - if "signatures" in stored_device: - del stored_device["signatures"] - if "unsigned" in stored_device: - del stored_device["unsigned"] + # use pop to avoid exception if key doesn't exist + signed_device.pop("unsigned", None) + stored_device.pop("signatures", None) + stored_device.pop("unsigned", None) if signed_device != stored_device: logger.error( "upload signatures: key does not match %s vs %s", signed_device, stored_device ) raise SynapseError( - 400, - "Key does not match", - "M_MISMATCHED_KEY" + 400, "Key does not match", ) # check the signature @@ -887,6 +952,9 @@ def _check_device_signature(user_id, verify_key, signed_device, stored_device): def _exception_to_failure(e): + if isinstance(e, SynapseError): + return {"status": e.code, "errcode": e.errcode, "message": str(e)} + if isinstance(e, CodeMessageException): return {"status": e.code, "message": str(e)} diff --git a/synapse/rest/client/v2_alpha/keys.py b/synapse/rest/client/v2_alpha/keys.py index 5c288d48b7..cb3c52cb8e 100644 --- a/synapse/rest/client/v2_alpha/keys.py +++ b/synapse/rest/client/v2_alpha/keys.py @@ -303,7 +303,7 @@ class SignaturesUploadServlet(RestServlet): } } """ - PATTERNS = client_v2_patterns("/keys/signatures/upload$") + PATTERNS = client_patterns("/keys/signatures/upload$") def __init__(self, hs): """ diff --git a/synapse/storage/end_to_end_keys.py b/synapse/storage/end_to_end_keys.py index fe786f3093..e68ce318af 100644 --- a/synapse/storage/end_to_end_keys.py +++ b/synapse/storage/end_to_end_keys.py @@ -132,7 +132,7 @@ class EndToEndKeyWorkerStore(SQLBaseStore): # get signatures on the device signature_sql = ( "SELECT * " - " FROM e2e_device_signatures " + " FROM e2e_cross_signing_signatures " " WHERE %s" ) % ( " OR ".join("(" + q + ")" for q in signature_query_clauses) diff --git a/tests/handlers/test_e2e_keys.py b/tests/handlers/test_e2e_keys.py index a62c52eefa..b1d3a4cfae 100644 --- a/tests/handlers/test_e2e_keys.py +++ b/tests/handlers/test_e2e_keys.py @@ -17,9 +17,10 @@ import mock +import signedjson.key as key +import signedjson.sign as sign from twisted.internet import defer -import synapse.api.errors import synapse.handlers.e2e_keys import synapse.storage from synapse.api import errors @@ -210,3 +211,227 @@ class E2eKeysHandlerTestCase(unittest.TestCase): res = yield self.handler.query_local_devices({local_user: None}) self.assertDictEqual(res, {local_user: {}}) + + @defer.inlineCallbacks + def test_upload_signatures(self): + """should check signatures that are uploaded""" + # set up a user with cross-signing keys and a device. This user will + # try uploading signatures + local_user = "@boris:" + self.hs.hostname + device_id = "xyz" + # private key: OMkooTr76ega06xNvXIGPbgvvxAOzmQncN8VObS7aBA + device_pubkey = "NnHhnqiMFQkq969szYkooLaBAXW244ZOxgukCvm2ZeY" + device_key = { + "user_id": local_user, + "device_id": device_id, + "algorithms": ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"], + "keys": { + "curve25519:xyz": "curve25519+key", + "ed25519:xyz": device_pubkey + }, + "signatures": { + local_user: { + "ed25519:xyz": "something" + } + } + } + device_signing_key = key.decode_signing_key_base64( + "ed25519", + "xyz", + "OMkooTr76ega06xNvXIGPbgvvxAOzmQncN8VObS7aBA" + ) + + yield self.handler.upload_keys_for_user( + local_user, device_id, {"device_keys": device_key} + ) + + # private key: 2lonYOM6xYKdEsO+6KrC766xBcHnYnim1x/4LFGF8B0 + master_pubkey = "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk" + master_key = { + "user_id": local_user, + "usage": ["master"], + "keys": { + "ed25519:" + master_pubkey: master_pubkey + } + } + master_signing_key = key.decode_signing_key_base64( + "ed25519", master_pubkey, + "2lonYOM6xYKdEsO+6KrC766xBcHnYnim1x/4LFGF8B0" + ) + usersigning_pubkey = "Hq6gL+utB4ET+UvD5ci0kgAwsX6qP/zvf8v6OInU5iw" + usersigning_key = { + # private key: 4TL4AjRYwDVwD3pqQzcor+ez/euOB1/q78aTJ+czDNs + "user_id": local_user, + "usage": ["user_signing"], + "keys": { + "ed25519:" + usersigning_pubkey: usersigning_pubkey, + } + } + usersigning_signing_key = key.decode_signing_key_base64( + "ed25519", usersigning_pubkey, + "4TL4AjRYwDVwD3pqQzcor+ez/euOB1/q78aTJ+czDNs" + ) + sign.sign_json(usersigning_key, local_user, master_signing_key) + # private key: HvQBbU+hc2Zr+JP1sE0XwBe1pfZZEYtJNPJLZJtS+F8 + selfsigning_pubkey = "EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ" + selfsigning_key = { + "user_id": local_user, + "usage": ["self_signing"], + "keys": { + "ed25519:" + selfsigning_pubkey: selfsigning_pubkey, + } + } + selfsigning_signing_key = key.decode_signing_key_base64( + "ed25519", selfsigning_pubkey, + "HvQBbU+hc2Zr+JP1sE0XwBe1pfZZEYtJNPJLZJtS+F8" + ) + sign.sign_json(selfsigning_key, local_user, master_signing_key) + cross_signing_keys = { + "master_key": master_key, + "user_signing_key": usersigning_key, + "self_signing_key": selfsigning_key, + } + yield self.handler.upload_signing_keys_for_user(local_user, cross_signing_keys) + + # set up another user with a master key. This user will be signed by + # the first user + other_user = "@otherboris:" + self.hs.hostname + other_master_pubkey = "fHZ3NPiKxoLQm5OoZbKa99SYxprOjNs4TwJUKP+twCM" + other_master_key = { + # private key: oyw2ZUx0O4GifbfFYM0nQvj9CL0b8B7cyN4FprtK8OI + "user_id": other_user, + "usage": ["master"], + "keys": { + "ed25519:" + other_master_pubkey: other_master_pubkey + } + } + yield self.handler.upload_signing_keys_for_user(other_user, { + "master_key": other_master_key + }) + + # test various signature failures (see below) + ret = yield self.handler.upload_signatures_for_device_keys( + local_user, + { + local_user: { + # fails because the signature is invalid + # should fail with INVALID_SIGNATURE + device_id: { + "user_id": local_user, + "device_id": device_id, + "algorithms": ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"], + "keys": { + "curve25519:xyz": "curve25519+key", + # private key: OMkooTr76ega06xNvXIGPbgvvxAOzmQncN8VObS7aBA + "ed25519:xyz": device_pubkey + }, + "signatures": { + local_user: { + "ed25519:" + selfsigning_pubkey: "something", + } + } + }, + # fails because device is unknown + # should fail with NOT_FOUND + "unknown": { + "user_id": local_user, + "device_id": "unknown", + "signatures": { + local_user: { + "ed25519:" + selfsigning_pubkey: "something", + } + } + }, + # fails because the signature is invalid + # should fail with INVALID_SIGNATURE + master_pubkey: { + "user_id": local_user, + "usage": ["master"], + "keys": { + "ed25519:" + master_pubkey: master_pubkey + }, + "signatures": { + local_user: { + "ed25519:" + device_pubkey: "something", + } + } + } + }, + other_user: { + # fails because the device is not the user's master-signing key + # should fail with NOT_FOUND + "unknown": { + "user_id": other_user, + "device_id": "unknown", + "signatures": { + local_user: { + "ed25519:" + usersigning_pubkey: "something", + } + } + }, + other_master_pubkey: { + # fails because the key doesn't match what the server has + # should fail with UNKNOWN + "user_id": other_user, + "usage": ["master"], + "keys": { + "ed25519:" + other_master_pubkey: other_master_pubkey + }, + "something": "random", + "signatures": { + local_user: { + "ed25519:" + usersigning_pubkey: "something", + } + } + } + } + } + ) + + user_failures = ret["failures"][local_user] + self.assertEqual(user_failures[device_id]["errcode"], errors.Codes.INVALID_SIGNATURE) + self.assertEqual(user_failures[master_pubkey]["errcode"], errors.Codes.INVALID_SIGNATURE) + self.assertEqual(user_failures["unknown"]["errcode"], errors.Codes.NOT_FOUND) + + other_user_failures = ret["failures"][other_user] + self.assertEqual(other_user_failures["unknown"]["errcode"], errors.Codes.NOT_FOUND) + self.assertEqual(other_user_failures[other_master_pubkey]["errcode"], errors.Codes.UNKNOWN) + + # test successful signatures + del device_key["signatures"] + sign.sign_json(device_key, local_user, selfsigning_signing_key) + sign.sign_json(master_key, local_user, device_signing_key) + sign.sign_json(other_master_key, local_user, usersigning_signing_key) + ret = yield self.handler.upload_signatures_for_device_keys( + local_user, + { + local_user: { + device_id: device_key, + master_pubkey: master_key + }, + other_user: { + other_master_pubkey: other_master_key + } + } + ) + + self.assertEqual(ret["failures"], {}) + + # fetch the signed keys/devices and make sure that the signatures are there + ret = yield self.handler.query_devices( + {"device_keys": {local_user: [], other_user: []}}, + 0, local_user + ) + + self.assertEqual( + ret["device_keys"][local_user]["xyz"]["signatures"][local_user]["ed25519:" + selfsigning_pubkey], + device_key["signatures"][local_user]["ed25519:" + selfsigning_pubkey] + ) + self.assertEqual( + ret["master_keys"][local_user]["signatures"][local_user]["ed25519:" + device_id], + master_key["signatures"][local_user]["ed25519:" + device_id] + ) + self.assertEqual( + ret["master_keys"][other_user]["signatures"][local_user]["ed25519:" + usersigning_pubkey], + other_master_key["signatures"][local_user]["ed25519:" + usersigning_pubkey] + ) From 7d6c70fc7ad08b94b8b577c537953a8d9b568562 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 22 Jul 2019 12:52:39 -0400 Subject: [PATCH 0020/1623] make black happy --- synapse/handlers/e2e_keys.py | 147 +++++++++++++-------------- synapse/rest/client/v2_alpha/keys.py | 1 + synapse/storage/end_to_end_keys.py | 24 ++--- tests/handlers/test_e2e_keys.py | 147 +++++++++++---------------- 4 files changed, 141 insertions(+), 178 deletions(-) diff --git a/synapse/handlers/e2e_keys.py b/synapse/handlers/e2e_keys.py index 1148803c1e..74bceddc46 100644 --- a/synapse/handlers/e2e_keys.py +++ b/synapse/handlers/e2e_keys.py @@ -635,16 +635,14 @@ class E2eKeysHandler(object): self_device_ids = list(self_signatures.keys()) try: # get our self-signing key to verify the signatures - self_signing_key, self_signing_key_id, self_signing_verify_key \ - = yield self._get_e2e_cross_signing_verify_key( - user_id, "self_signing" - ) + self_signing_key, self_signing_key_id, self_signing_verify_key = yield self._get_e2e_cross_signing_verify_key( + user_id, "self_signing" + ) # get our master key, since it may be signed - master_key, master_key_id, master_verify_key \ - = yield self._get_e2e_cross_signing_verify_key( - user_id, "master" - ) + master_key, master_key_id, master_verify_key = yield self._get_e2e_cross_signing_verify_key( + user_id, "master" + ) # fetch our stored devices. This is used to 1. verify # signatures on the master key, and 2. to can compare with what @@ -652,15 +650,15 @@ class E2eKeysHandler(object): devices = yield self.store.get_e2e_device_keys([(user_id, None)]) if user_id not in devices: - raise SynapseError( - 404, "No device keys found", Codes.NOT_FOUND - ) + raise SynapseError(404, "No device keys found", Codes.NOT_FOUND) devices = devices[user_id] for device_id, device in self_signatures.items(): try: - if ("signatures" not in device or - user_id not in device["signatures"]): + if ( + "signatures" not in device + or user_id not in device["signatures"] + ): # no signature was sent raise SynapseError( 400, "Invalid signature", Codes.INVALID_SIGNATURE @@ -678,15 +676,21 @@ class E2eKeysHandler(object): # signature list. (In practice, we're likely to # only have only one signature anyways.) master_key_signature_list = [] - for signing_key_id, signature in device["signatures"][user_id].items(): + for signing_key_id, signature in device["signatures"][ + user_id + ].items(): alg, signing_device_id = signing_key_id.split(":", 1) - if (signing_device_id not in devices or - signing_key_id not in - devices[signing_device_id]["keys"]["keys"]): + if ( + signing_device_id not in devices + or signing_key_id + not in devices[signing_device_id]["keys"]["keys"] + ): # signed by an unknown device, or the # device does not have the key raise SynapseError( - 400, "Invalid signature", Codes.INVALID_SIGNATURE + 400, + "Invalid signature", + Codes.INVALID_SIGNATURE, ) sigs = device["signatures"] @@ -697,12 +701,12 @@ class E2eKeysHandler(object): master_key.pop("unsigned", None) if master_key != device: - raise SynapseError( - 400, "Key does not match" - ) + raise SynapseError(400, "Key does not match") # get the key and check the signature - pubkey = devices[signing_device_id]["keys"]["keys"][signing_key_id] + pubkey = devices[signing_device_id]["keys"]["keys"][ + signing_key_id + ] verify_key = decode_verify_key_bytes( signing_key_id, decode_base64(pubkey) ) @@ -711,7 +715,9 @@ class E2eKeysHandler(object): verify_signed_json(device, user_id, verify_key) except SignatureVerifyException: raise SynapseError( - 400, "Invalid signature", Codes.INVALID_SIGNATURE + 400, + "Invalid signature", + Codes.INVALID_SIGNATURE, ) master_key_signature_list.append( @@ -733,11 +739,10 @@ class E2eKeysHandler(object): try: stored_device = devices[device_id]["keys"] except KeyError: - raise SynapseError( - 404, "Unknown device", Codes.NOT_FOUND - ) - if self_signing_key_id in stored_device.get("signatures", {}) \ - .get(user_id, {}): + raise SynapseError(404, "Unknown device", Codes.NOT_FOUND) + if self_signing_key_id in stored_device.get( + "signatures", {} + ).get(user_id, {}): # we already have a signature on this device, so we # can skip it, since it should be exactly the same continue @@ -751,8 +756,9 @@ class E2eKeysHandler(object): (self_signing_key_id, user_id, device_id, signature) ) except SynapseError as e: - failures.setdefault(user_id, {})[device_id] \ - = _exception_to_failure(e) + failures.setdefault(user_id, {})[ + device_id + ] = _exception_to_failure(e) except SynapseError as e: failures[user_id] = { device: _exception_to_failure(e) @@ -766,20 +772,18 @@ class E2eKeysHandler(object): try: # get our user-signing key to verify the signatures - user_signing_key, user_signing_key_id, user_signing_verify_key \ - = yield self._get_e2e_cross_signing_verify_key( - user_id, "user_signing" - ) + user_signing_key, user_signing_key_id, user_signing_verify_key = yield self._get_e2e_cross_signing_verify_key( + user_id, "user_signing" + ) for user, devicemap in signatures.items(): device_id = None try: # get the user's master key, to make sure it matches # what was sent - stored_key, stored_key_id, _ \ - = yield self._get_e2e_cross_signing_verify_key( - user, "master", user_id - ) + stored_key, stored_key_id, _ = yield self._get_e2e_cross_signing_verify_key( + user, "master", user_id + ) # make sure that the user's master key is the one that # was signed (and no others) @@ -790,26 +794,25 @@ class E2eKeysHandler(object): device_id = None logger.error( "upload signature: wrong device: %s vs %s", - device, devicemap - ) - raise SynapseError( - 404, "Unknown device", Codes.NOT_FOUND + device, + devicemap, ) + raise SynapseError(404, "Unknown device", Codes.NOT_FOUND) key = devicemap[device_id] del devicemap[device_id] if len(devicemap) > 0: # other devices were signed -- mark those as failures logger.error("upload signature: too many devices specified") - failure = _exception_to_failure(SynapseError( - 404, "Unknown device", Codes.NOT_FOUND - )) + failure = _exception_to_failure( + SynapseError(404, "Unknown device", Codes.NOT_FOUND) + ) failures[user] = { - device: failure - for device in devicemap.keys() + device: failure for device in devicemap.keys() } - if user_signing_key_id in stored_key.get("signatures", {}) \ - .get(user_id, {}): + if user_signing_key_id in stored_key.get("signatures", {}).get( + user_id, {} + ): # we already have the signature, so we can skip it continue @@ -826,8 +829,7 @@ class E2eKeysHandler(object): failure = _exception_to_failure(e) if device_id is None: failures[user] = { - device_id: failure - for device_id in devicemap.keys() + device_id: failure for device_id in devicemap.keys() } else: failures.setdefault(user, {})[device_id] = failure @@ -835,8 +837,7 @@ class E2eKeysHandler(object): failure = _exception_to_failure(e) for user, devicemap in signature.items(): failures[user] = { - device_id: failure - for device_id in devicemap.keys() + device_id: failure for device_id in devicemap.keys() } # store the signature, and send the appropriate notifications for sync @@ -846,7 +847,9 @@ class E2eKeysHandler(object): if len(self_device_ids): yield self.device_handler.notify_device_update(user_id, self_device_ids) if len(signed_users): - yield self.device_handler.notify_user_signature_update(user_id, signed_users) + yield self.device_handler.notify_user_signature_update( + user_id, signed_users + ) defer.returnValue({"failures": failures}) @@ -858,9 +861,7 @@ class E2eKeysHandler(object): if key is None: logger.error("no %s key found for %s", key_type, user_id) raise SynapseError( - 404, - "No %s key found for %s" % (key_type, user_id), - Codes.NOT_FOUND + 404, "No %s key found for %s" % (key_type, user_id), Codes.NOT_FOUND ) key_id, verify_key = get_verify_key_from_cross_signing_key(key) return key, key_id, verify_key @@ -907,14 +908,13 @@ def _check_device_signature(user_id, verify_key, signed_device, stored_device): key_id = "%s:%s" % (verify_key.alg, verify_key.version) # make sure the device is signed - if ("signatures" not in signed_device or user_id not in signed_device["signatures"] - or key_id not in signed_device["signatures"][user_id]): + if ( + "signatures" not in signed_device + or user_id not in signed_device["signatures"] + or key_id not in signed_device["signatures"][user_id] + ): logger.error("upload signature: user not found in signatures") - raise SynapseError( - 400, - "Invalid signature", - Codes.INVALID_SIGNATURE - ) + raise SynapseError(400, "Invalid signature", Codes.INVALID_SIGNATURE) signature = signed_device["signatures"][user_id][key_id] @@ -927,28 +927,19 @@ def _check_device_signature(user_id, verify_key, signed_device, stored_device): if signed_device != stored_device: logger.error( "upload signatures: key does not match %s vs %s", - signed_device, stored_device - ) - raise SynapseError( - 400, "Key does not match", + signed_device, + stored_device, ) + raise SynapseError(400, "Key does not match") # check the signature - signed_device["signatures"] = { - user_id: { - key_id: signature - } - } + signed_device["signatures"] = {user_id: {key_id: signature}} try: verify_signed_json(signed_device, user_id, verify_key) except SignatureVerifyException: logger.error("invalid signature on key") - raise SynapseError( - 400, - "Invalid signature", - Codes.INVALID_SIGNATURE - ) + raise SynapseError(400, "Invalid signature", Codes.INVALID_SIGNATURE) def _exception_to_failure(e): diff --git a/synapse/rest/client/v2_alpha/keys.py b/synapse/rest/client/v2_alpha/keys.py index cb3c52cb8e..a205281830 100644 --- a/synapse/rest/client/v2_alpha/keys.py +++ b/synapse/rest/client/v2_alpha/keys.py @@ -303,6 +303,7 @@ class SignaturesUploadServlet(RestServlet): } } """ + PATTERNS = client_patterns("/keys/signatures/upload$") def __init__(self, hs): diff --git a/synapse/storage/end_to_end_keys.py b/synapse/storage/end_to_end_keys.py index e68ce318af..258e8dcb47 100644 --- a/synapse/storage/end_to_end_keys.py +++ b/synapse/storage/end_to_end_keys.py @@ -62,9 +62,9 @@ class EndToEndKeyWorkerStore(SQLBaseStore): # add cross-signing signatures to the keys if "signatures" in device_info: for sig_user_id, sigs in device_info["signatures"].items(): - device_info["keys"].setdefault("signatures", {}) \ - .setdefault(sig_user_id, {}) \ - .update(sigs) + device_info["keys"].setdefault("signatures", {}).setdefault( + sig_user_id, {} + ).update(sigs) return results @@ -131,12 +131,8 @@ class EndToEndKeyWorkerStore(SQLBaseStore): # get signatures on the device signature_sql = ( - "SELECT * " - " FROM e2e_cross_signing_signatures " - " WHERE %s" - ) % ( - " OR ".join("(" + q + ")" for q in signature_query_clauses) - ) + "SELECT * " " FROM e2e_cross_signing_signatures " " WHERE %s" + ) % (" OR ".join("(" + q + ")" for q in signature_query_clauses)) txn.execute(signature_sql, signature_query_params) rows = self.cursor_to_dict(txn) @@ -144,12 +140,10 @@ class EndToEndKeyWorkerStore(SQLBaseStore): for row in rows: target_user_id = row["target_user_id"] target_device_id = row["target_device_id"] - if target_user_id in result \ - and target_device_id in result[target_user_id]: - result[target_user_id][target_device_id] \ - .setdefault("signatures", {}) \ - .setdefault(row["user_id"], {})[row["key_id"]] \ - = row["signature"] + if target_user_id in result and target_device_id in result[target_user_id]: + result[target_user_id][target_device_id].setdefault( + "signatures", {} + ).setdefault(row["user_id"], {})[row["key_id"]] = row["signature"] log_kv(result) return result diff --git a/tests/handlers/test_e2e_keys.py b/tests/handlers/test_e2e_keys.py index b1d3a4cfae..8c0ee3f7d3 100644 --- a/tests/handlers/test_e2e_keys.py +++ b/tests/handlers/test_e2e_keys.py @@ -225,20 +225,11 @@ class E2eKeysHandlerTestCase(unittest.TestCase): "user_id": local_user, "device_id": device_id, "algorithms": ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"], - "keys": { - "curve25519:xyz": "curve25519+key", - "ed25519:xyz": device_pubkey - }, - "signatures": { - local_user: { - "ed25519:xyz": "something" - } - } + "keys": {"curve25519:xyz": "curve25519+key", "ed25519:xyz": device_pubkey}, + "signatures": {local_user: {"ed25519:xyz": "something"}}, } device_signing_key = key.decode_signing_key_base64( - "ed25519", - "xyz", - "OMkooTr76ega06xNvXIGPbgvvxAOzmQncN8VObS7aBA" + "ed25519", "xyz", "OMkooTr76ega06xNvXIGPbgvvxAOzmQncN8VObS7aBA" ) yield self.handler.upload_keys_for_user( @@ -250,26 +241,20 @@ class E2eKeysHandlerTestCase(unittest.TestCase): master_key = { "user_id": local_user, "usage": ["master"], - "keys": { - "ed25519:" + master_pubkey: master_pubkey - } + "keys": {"ed25519:" + master_pubkey: master_pubkey}, } master_signing_key = key.decode_signing_key_base64( - "ed25519", master_pubkey, - "2lonYOM6xYKdEsO+6KrC766xBcHnYnim1x/4LFGF8B0" + "ed25519", master_pubkey, "2lonYOM6xYKdEsO+6KrC766xBcHnYnim1x/4LFGF8B0" ) usersigning_pubkey = "Hq6gL+utB4ET+UvD5ci0kgAwsX6qP/zvf8v6OInU5iw" usersigning_key = { # private key: 4TL4AjRYwDVwD3pqQzcor+ez/euOB1/q78aTJ+czDNs "user_id": local_user, "usage": ["user_signing"], - "keys": { - "ed25519:" + usersigning_pubkey: usersigning_pubkey, - } + "keys": {"ed25519:" + usersigning_pubkey: usersigning_pubkey}, } usersigning_signing_key = key.decode_signing_key_base64( - "ed25519", usersigning_pubkey, - "4TL4AjRYwDVwD3pqQzcor+ez/euOB1/q78aTJ+czDNs" + "ed25519", usersigning_pubkey, "4TL4AjRYwDVwD3pqQzcor+ez/euOB1/q78aTJ+czDNs" ) sign.sign_json(usersigning_key, local_user, master_signing_key) # private key: HvQBbU+hc2Zr+JP1sE0XwBe1pfZZEYtJNPJLZJtS+F8 @@ -277,13 +262,10 @@ class E2eKeysHandlerTestCase(unittest.TestCase): selfsigning_key = { "user_id": local_user, "usage": ["self_signing"], - "keys": { - "ed25519:" + selfsigning_pubkey: selfsigning_pubkey, - } + "keys": {"ed25519:" + selfsigning_pubkey: selfsigning_pubkey}, } selfsigning_signing_key = key.decode_signing_key_base64( - "ed25519", selfsigning_pubkey, - "HvQBbU+hc2Zr+JP1sE0XwBe1pfZZEYtJNPJLZJtS+F8" + "ed25519", selfsigning_pubkey, "HvQBbU+hc2Zr+JP1sE0XwBe1pfZZEYtJNPJLZJtS+F8" ) sign.sign_json(selfsigning_key, local_user, master_signing_key) cross_signing_keys = { @@ -301,13 +283,11 @@ class E2eKeysHandlerTestCase(unittest.TestCase): # private key: oyw2ZUx0O4GifbfFYM0nQvj9CL0b8B7cyN4FprtK8OI "user_id": other_user, "usage": ["master"], - "keys": { - "ed25519:" + other_master_pubkey: other_master_pubkey - } + "keys": {"ed25519:" + other_master_pubkey: other_master_pubkey}, } - yield self.handler.upload_signing_keys_for_user(other_user, { - "master_key": other_master_key - }) + yield self.handler.upload_signing_keys_for_user( + other_user, {"master_key": other_master_key} + ) # test various signature failures (see below) ret = yield self.handler.upload_signatures_for_device_keys( @@ -319,17 +299,18 @@ class E2eKeysHandlerTestCase(unittest.TestCase): device_id: { "user_id": local_user, "device_id": device_id, - "algorithms": ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"], + "algorithms": [ + "m.olm.curve25519-aes-sha256", + "m.megolm.v1.aes-sha", + ], "keys": { "curve25519:xyz": "curve25519+key", # private key: OMkooTr76ega06xNvXIGPbgvvxAOzmQncN8VObS7aBA - "ed25519:xyz": device_pubkey + "ed25519:xyz": device_pubkey, }, "signatures": { - local_user: { - "ed25519:" + selfsigning_pubkey: "something", - } - } + local_user: {"ed25519:" + selfsigning_pubkey: "something"} + }, }, # fails because device is unknown # should fail with NOT_FOUND @@ -337,25 +318,19 @@ class E2eKeysHandlerTestCase(unittest.TestCase): "user_id": local_user, "device_id": "unknown", "signatures": { - local_user: { - "ed25519:" + selfsigning_pubkey: "something", - } - } + local_user: {"ed25519:" + selfsigning_pubkey: "something"} + }, }, # fails because the signature is invalid # should fail with INVALID_SIGNATURE master_pubkey: { "user_id": local_user, "usage": ["master"], - "keys": { - "ed25519:" + master_pubkey: master_pubkey - }, + "keys": {"ed25519:" + master_pubkey: master_pubkey}, "signatures": { - local_user: { - "ed25519:" + device_pubkey: "something", - } - } - } + local_user: {"ed25519:" + device_pubkey: "something"} + }, + }, }, other_user: { # fails because the device is not the user's master-signing key @@ -364,38 +339,40 @@ class E2eKeysHandlerTestCase(unittest.TestCase): "user_id": other_user, "device_id": "unknown", "signatures": { - local_user: { - "ed25519:" + usersigning_pubkey: "something", - } - } + local_user: {"ed25519:" + usersigning_pubkey: "something"} + }, }, other_master_pubkey: { # fails because the key doesn't match what the server has # should fail with UNKNOWN "user_id": other_user, "usage": ["master"], - "keys": { - "ed25519:" + other_master_pubkey: other_master_pubkey - }, + "keys": {"ed25519:" + other_master_pubkey: other_master_pubkey}, "something": "random", "signatures": { - local_user: { - "ed25519:" + usersigning_pubkey: "something", - } - } - } - } - } + local_user: {"ed25519:" + usersigning_pubkey: "something"} + }, + }, + }, + }, ) user_failures = ret["failures"][local_user] - self.assertEqual(user_failures[device_id]["errcode"], errors.Codes.INVALID_SIGNATURE) - self.assertEqual(user_failures[master_pubkey]["errcode"], errors.Codes.INVALID_SIGNATURE) + self.assertEqual( + user_failures[device_id]["errcode"], errors.Codes.INVALID_SIGNATURE + ) + self.assertEqual( + user_failures[master_pubkey]["errcode"], errors.Codes.INVALID_SIGNATURE + ) self.assertEqual(user_failures["unknown"]["errcode"], errors.Codes.NOT_FOUND) other_user_failures = ret["failures"][other_user] - self.assertEqual(other_user_failures["unknown"]["errcode"], errors.Codes.NOT_FOUND) - self.assertEqual(other_user_failures[other_master_pubkey]["errcode"], errors.Codes.UNKNOWN) + self.assertEqual( + other_user_failures["unknown"]["errcode"], errors.Codes.NOT_FOUND + ) + self.assertEqual( + other_user_failures[other_master_pubkey]["errcode"], errors.Codes.UNKNOWN + ) # test successful signatures del device_key["signatures"] @@ -405,33 +382,33 @@ class E2eKeysHandlerTestCase(unittest.TestCase): ret = yield self.handler.upload_signatures_for_device_keys( local_user, { - local_user: { - device_id: device_key, - master_pubkey: master_key - }, - other_user: { - other_master_pubkey: other_master_key - } - } + local_user: {device_id: device_key, master_pubkey: master_key}, + other_user: {other_master_pubkey: other_master_key}, + }, ) self.assertEqual(ret["failures"], {}) # fetch the signed keys/devices and make sure that the signatures are there ret = yield self.handler.query_devices( - {"device_keys": {local_user: [], other_user: []}}, - 0, local_user + {"device_keys": {local_user: [], other_user: []}}, 0, local_user ) self.assertEqual( - ret["device_keys"][local_user]["xyz"]["signatures"][local_user]["ed25519:" + selfsigning_pubkey], - device_key["signatures"][local_user]["ed25519:" + selfsigning_pubkey] + ret["device_keys"][local_user]["xyz"]["signatures"][local_user][ + "ed25519:" + selfsigning_pubkey + ], + device_key["signatures"][local_user]["ed25519:" + selfsigning_pubkey], ) self.assertEqual( - ret["master_keys"][local_user]["signatures"][local_user]["ed25519:" + device_id], - master_key["signatures"][local_user]["ed25519:" + device_id] + ret["master_keys"][local_user]["signatures"][local_user][ + "ed25519:" + device_id + ], + master_key["signatures"][local_user]["ed25519:" + device_id], ) self.assertEqual( - ret["master_keys"][other_user]["signatures"][local_user]["ed25519:" + usersigning_pubkey], - other_master_key["signatures"][local_user]["ed25519:" + usersigning_pubkey] + ret["master_keys"][other_user]["signatures"][local_user][ + "ed25519:" + usersigning_pubkey + ], + other_master_key["signatures"][local_user]["ed25519:" + usersigning_pubkey], ) From 9061b4198af4b30bb99d98aab7ad227f8ed636f8 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 22 Jul 2019 12:58:04 -0400 Subject: [PATCH 0021/1623] make isort happy --- tests/handlers/test_e2e_keys.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/handlers/test_e2e_keys.py b/tests/handlers/test_e2e_keys.py index 8c0ee3f7d3..c900451e03 100644 --- a/tests/handlers/test_e2e_keys.py +++ b/tests/handlers/test_e2e_keys.py @@ -19,6 +19,7 @@ import mock import signedjson.key as key import signedjson.sign as sign + from twisted.internet import defer import synapse.handlers.e2e_keys From 5914fd09c725342d03f702a50ec1da6290e946a9 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 22 Jul 2019 13:01:10 -0400 Subject: [PATCH 0022/1623] add test --- tests/handlers/test_e2e_keys.py | 88 +++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/tests/handlers/test_e2e_keys.py b/tests/handlers/test_e2e_keys.py index c900451e03..316dd6259d 100644 --- a/tests/handlers/test_e2e_keys.py +++ b/tests/handlers/test_e2e_keys.py @@ -183,6 +183,94 @@ class E2eKeysHandlerTestCase(unittest.TestCase): ) self.assertDictEqual(devices["master_keys"], {local_user: keys2["master_key"]}) + @defer.inlineCallbacks + def test_reupload_signatures(self): + """re-uploading a signature should not fail""" + local_user = "@boris:" + self.hs.hostname + keys1 = { + "master_key": { + # private key: HvQBbU+hc2Zr+JP1sE0XwBe1pfZZEYtJNPJLZJtS+F8 + "user_id": local_user, + "usage": ["master"], + "keys": { + "ed25519:EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ": "EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ" + }, + }, + "self_signing_key": { + # private key: 2lonYOM6xYKdEsO+6KrC766xBcHnYnim1x/4LFGF8B0 + "user_id": local_user, + "usage": ["self_signing"], + "keys": { + "ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk": "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk" + }, + }, + } + master_signing_key = key.decode_signing_key_base64( + "ed25519", + "EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ", + "HvQBbU+hc2Zr+JP1sE0XwBe1pfZZEYtJNPJLZJtS+F8", + ) + sign.sign_json(keys1["self_signing_key"], local_user, master_signing_key) + signing_key = key.decode_signing_key_base64( + "ed25519", + "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk", + "2lonYOM6xYKdEsO+6KrC766xBcHnYnim1x/4LFGF8B0", + ) + yield self.handler.upload_signing_keys_for_user(local_user, keys1) + + # upload two device keys, which will be signed later by the self-signing key + device_key_1 = { + "user_id": local_user, + "device_id": "abc", + "algorithms": ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"], + "keys": { + "ed25519:abc": "base64+ed25519+key", + "curve25519:abc": "base64+curve25519+key", + }, + "signatures": {local_user: {"ed25519:abc": "base64+signature"}}, + } + device_key_2 = { + "user_id": local_user, + "device_id": "def", + "algorithms": ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"], + "keys": { + "ed25519:def": "base64+ed25519+key", + "curve25519:def": "base64+curve25519+key", + }, + "signatures": {local_user: {"ed25519:def": "base64+signature"}}, + } + + yield self.handler.upload_keys_for_user( + local_user, "abc", {"device_keys": device_key_1} + ) + yield self.handler.upload_keys_for_user( + local_user, "def", {"device_keys": device_key_2} + ) + + # sign the first device key and upload it + del device_key_1["signatures"] + sign.sign_json(device_key_1, local_user, signing_key) + yield self.handler.upload_signatures_for_device_keys( + local_user, {local_user: {"abc": device_key_1}} + ) + + # sign the second device key and upload both device keys. The server + # should ignore the first device key since it already has a valid + # signature for it + del device_key_2["signatures"] + sign.sign_json(device_key_2, local_user, signing_key) + yield self.handler.upload_signatures_for_device_keys( + local_user, {local_user: {"abc": device_key_1, "def": device_key_2}} + ) + + device_key_1["signatures"][local_user]["ed25519:abc"] = "base64+signature" + device_key_2["signatures"][local_user]["ed25519:def"] = "base64+signature" + devices = yield self.handler.query_devices({"device_keys": {local_user: []}}, 0) + del devices["device_keys"][local_user]["abc"]["unsigned"] + del devices["device_keys"][local_user]["def"]["unsigned"] + self.assertDictEqual(devices["device_keys"][local_user]["abc"], device_key_1) + self.assertDictEqual(devices["device_keys"][local_user]["def"], device_key_2) + @defer.inlineCallbacks def test_self_signing_key_doesnt_show_up_as_device(self): """signing keys should be hidden when fetching a user's devices""" From c8dc740a94f20c0bca9aaa30b9d0fd211361a21e Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 4 Sep 2019 22:30:45 -0400 Subject: [PATCH 0023/1623] update with newer coding style --- synapse/handlers/e2e_keys.py | 2 +- synapse/rest/client/v2_alpha/keys.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/synapse/handlers/e2e_keys.py b/synapse/handlers/e2e_keys.py index 74bceddc46..d5d6e6e027 100644 --- a/synapse/handlers/e2e_keys.py +++ b/synapse/handlers/e2e_keys.py @@ -851,7 +851,7 @@ class E2eKeysHandler(object): user_id, signed_users ) - defer.returnValue({"failures": failures}) + return {"failures": failures} @defer.inlineCallbacks def _get_e2e_cross_signing_verify_key(self, user_id, key_type, from_user_id=None): diff --git a/synapse/rest/client/v2_alpha/keys.py b/synapse/rest/client/v2_alpha/keys.py index a205281830..341567ae21 100644 --- a/synapse/rest/client/v2_alpha/keys.py +++ b/synapse/rest/client/v2_alpha/keys.py @@ -274,7 +274,7 @@ class SigningKeyUploadServlet(RestServlet): ) result = yield self.e2e_keys_handler.upload_signing_keys_for_user(user_id, body) - return (200, result) + return 200, result class SignaturesUploadServlet(RestServlet): @@ -324,7 +324,7 @@ class SignaturesUploadServlet(RestServlet): result = yield self.e2e_keys_handler.upload_signatures_for_device_keys( user_id, body ) - defer.returnValue((200, result)) + return 200, result def register_servlets(hs, http_server): From e47af0f086839c5d22a0de87a32a49386abef8df Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 5 Sep 2019 17:03:14 -0400 Subject: [PATCH 0024/1623] fix test --- tests/handlers/test_e2e_keys.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/handlers/test_e2e_keys.py b/tests/handlers/test_e2e_keys.py index 316dd6259d..7a59ec5085 100644 --- a/tests/handlers/test_e2e_keys.py +++ b/tests/handlers/test_e2e_keys.py @@ -265,7 +265,9 @@ class E2eKeysHandlerTestCase(unittest.TestCase): device_key_1["signatures"][local_user]["ed25519:abc"] = "base64+signature" device_key_2["signatures"][local_user]["ed25519:def"] = "base64+signature" - devices = yield self.handler.query_devices({"device_keys": {local_user: []}}, 0) + devices = yield self.handler.query_devices( + {"device_keys": {local_user: []}}, 0, 0 + ) del devices["device_keys"][local_user]["abc"]["unsigned"] del devices["device_keys"][local_user]["def"]["unsigned"] self.assertDictEqual(devices["device_keys"][local_user]["abc"], device_key_1) From 369462da7488772ea6d2fdd076ff355bc09db28c Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 5 Sep 2019 17:03:31 -0400 Subject: [PATCH 0025/1623] avoid modifying input parameter --- synapse/handlers/e2e_keys.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/synapse/handlers/e2e_keys.py b/synapse/handlers/e2e_keys.py index d5d6e6e027..2c21cb9828 100644 --- a/synapse/handlers/e2e_keys.py +++ b/synapse/handlers/e2e_keys.py @@ -629,9 +629,9 @@ class E2eKeysHandler(object): # split between checking signatures for own user and signatures for # other users, since we verify them with different keys - if user_id in signatures: - self_signatures = signatures[user_id] - del signatures[user_id] + self_signatures = signatures.get(user_id, {}) + other_signatures = {k: v for k, v in signatures.items() if k != user_id} + if self_signatures: self_device_ids = list(self_signatures.keys()) try: # get our self-signing key to verify the signatures @@ -766,9 +766,9 @@ class E2eKeysHandler(object): } signed_users = [] # what user have been signed, for notifying - if len(signatures): - # if signatures isn't empty, then we have signatures for other - # users. These signatures will be signed by the user signing key + if other_signatures: + # now check non-self signatures. These signatures will be signed + # by the user-signing key try: # get our user-signing key to verify the signatures @@ -776,7 +776,7 @@ class E2eKeysHandler(object): user_id, "user_signing" ) - for user, devicemap in signatures.items(): + for user, devicemap in other_signatures.items(): device_id = None try: # get the user's master key, to make sure it matches From 561cbba0577b63f340050362144bef8527c1fc0e Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Fri, 6 Sep 2019 16:44:24 -0400 Subject: [PATCH 0026/1623] split out signature processing into separate functions --- synapse/handlers/e2e_keys.py | 429 ++++++++++++++++++----------------- 1 file changed, 219 insertions(+), 210 deletions(-) diff --git a/synapse/handlers/e2e_keys.py b/synapse/handlers/e2e_keys.py index 2c21cb9828..6500bf3e16 100644 --- a/synapse/handlers/e2e_keys.py +++ b/synapse/handlers/e2e_keys.py @@ -624,235 +624,244 @@ class E2eKeysHandler(object): # signatures to be stored. Each item will be a tuple of # (signing_key_id, target_user_id, target_device_id, signature) signature_list = [] - # what devices have been updated, for notifying - self_device_ids = [] # split between checking signatures for own user and signatures for # other users, since we verify them with different keys self_signatures = signatures.get(user_id, {}) other_signatures = {k: v for k, v in signatures.items() if k != user_id} - if self_signatures: - self_device_ids = list(self_signatures.keys()) - try: - # get our self-signing key to verify the signatures - self_signing_key, self_signing_key_id, self_signing_verify_key = yield self._get_e2e_cross_signing_verify_key( - user_id, "self_signing" - ) - # get our master key, since it may be signed - master_key, master_key_id, master_verify_key = yield self._get_e2e_cross_signing_verify_key( - user_id, "master" - ) + self_signature_list, self_failures = yield self._process_self_signatures( + user_id, self_signatures + ) + signature_list.extend(self_signature_list) + failures.update(self_failures) - # fetch our stored devices. This is used to 1. verify - # signatures on the master key, and 2. to can compare with what - # was sent if the device was signed - devices = yield self.store.get_e2e_device_keys([(user_id, None)]) - - if user_id not in devices: - raise SynapseError(404, "No device keys found", Codes.NOT_FOUND) - - devices = devices[user_id] - for device_id, device in self_signatures.items(): - try: - if ( - "signatures" not in device - or user_id not in device["signatures"] - ): - # no signature was sent - raise SynapseError( - 400, "Invalid signature", Codes.INVALID_SIGNATURE - ) - - if device_id == master_verify_key.version: - # we have master key signed by devices: for each - # device that signed, check the signature. Since - # the "failures" property in the response only has - # granularity up to the signed device, either all - # of the signatures on the master key succeed, or - # all fail. So loop over the signatures and add - # them to a separate signature list. If everything - # works out, then add them all to the main - # signature list. (In practice, we're likely to - # only have only one signature anyways.) - master_key_signature_list = [] - for signing_key_id, signature in device["signatures"][ - user_id - ].items(): - alg, signing_device_id = signing_key_id.split(":", 1) - if ( - signing_device_id not in devices - or signing_key_id - not in devices[signing_device_id]["keys"]["keys"] - ): - # signed by an unknown device, or the - # device does not have the key - raise SynapseError( - 400, - "Invalid signature", - Codes.INVALID_SIGNATURE, - ) - - sigs = device["signatures"] - del device["signatures"] - # use pop to avoid exception if key doesn't exist - device.pop("unsigned", None) - master_key.pop("signature", None) - master_key.pop("unsigned", None) - - if master_key != device: - raise SynapseError(400, "Key does not match") - - # get the key and check the signature - pubkey = devices[signing_device_id]["keys"]["keys"][ - signing_key_id - ] - verify_key = decode_verify_key_bytes( - signing_key_id, decode_base64(pubkey) - ) - device["signatures"] = sigs - try: - verify_signed_json(device, user_id, verify_key) - except SignatureVerifyException: - raise SynapseError( - 400, - "Invalid signature", - Codes.INVALID_SIGNATURE, - ) - - master_key_signature_list.append( - (signing_key_id, user_id, device_id, signature) - ) - - signature_list.extend(master_key_signature_list) - continue - - # at this point, we have a device that should be signed - # by the self-signing key - if self_signing_key_id not in device["signatures"][user_id]: - # no signature was sent - raise SynapseError( - 400, "Invalid signature", Codes.INVALID_SIGNATURE - ) - - stored_device = None - try: - stored_device = devices[device_id]["keys"] - except KeyError: - raise SynapseError(404, "Unknown device", Codes.NOT_FOUND) - if self_signing_key_id in stored_device.get( - "signatures", {} - ).get(user_id, {}): - # we already have a signature on this device, so we - # can skip it, since it should be exactly the same - continue - - _check_device_signature( - user_id, self_signing_verify_key, device, stored_device - ) - - signature = device["signatures"][user_id][self_signing_key_id] - signature_list.append( - (self_signing_key_id, user_id, device_id, signature) - ) - except SynapseError as e: - failures.setdefault(user_id, {})[ - device_id - ] = _exception_to_failure(e) - except SynapseError as e: - failures[user_id] = { - device: _exception_to_failure(e) - for device in self_signatures.keys() - } - - signed_users = [] # what user have been signed, for notifying - if other_signatures: - # now check non-self signatures. These signatures will be signed - # by the user-signing key - - try: - # get our user-signing key to verify the signatures - user_signing_key, user_signing_key_id, user_signing_verify_key = yield self._get_e2e_cross_signing_verify_key( - user_id, "user_signing" - ) - - for user, devicemap in other_signatures.items(): - device_id = None - try: - # get the user's master key, to make sure it matches - # what was sent - stored_key, stored_key_id, _ = yield self._get_e2e_cross_signing_verify_key( - user, "master", user_id - ) - - # make sure that the user's master key is the one that - # was signed (and no others) - device_id = stored_key_id.split(":", 1)[1] - if device_id not in devicemap: - # set device to None so that the failure gets - # marked on all the signatures - device_id = None - logger.error( - "upload signature: wrong device: %s vs %s", - device, - devicemap, - ) - raise SynapseError(404, "Unknown device", Codes.NOT_FOUND) - key = devicemap[device_id] - del devicemap[device_id] - if len(devicemap) > 0: - # other devices were signed -- mark those as failures - logger.error("upload signature: too many devices specified") - failure = _exception_to_failure( - SynapseError(404, "Unknown device", Codes.NOT_FOUND) - ) - failures[user] = { - device: failure for device in devicemap.keys() - } - - if user_signing_key_id in stored_key.get("signatures", {}).get( - user_id, {} - ): - # we already have the signature, so we can skip it - continue - - _check_device_signature( - user_id, user_signing_verify_key, key, stored_key - ) - - signed_users.append(user) - signature = key["signatures"][user_id][user_signing_key_id] - signature_list.append( - (user_signing_key_id, user, device_id, signature) - ) - except SynapseError as e: - failure = _exception_to_failure(e) - if device_id is None: - failures[user] = { - device_id: failure for device_id in devicemap.keys() - } - else: - failures.setdefault(user, {})[device_id] = failure - except SynapseError as e: - failure = _exception_to_failure(e) - for user, devicemap in signature.items(): - failures[user] = { - device_id: failure for device_id in devicemap.keys() - } + other_signature_list, other_failures = yield self._process_other_signatures( + user_id, other_signatures + ) + signature_list.extend(other_signature_list) + failures.update(other_failures) # store the signature, and send the appropriate notifications for sync logger.debug("upload signature failures: %r", failures) yield self.store.store_e2e_cross_signing_signatures(user_id, signature_list) - if len(self_device_ids): + self_device_ids = [device_id for (_, _, device_id, _) in self_signature_list] + if self_device_ids: yield self.device_handler.notify_device_update(user_id, self_device_ids) - if len(signed_users): + signed_users = [user_id for (_, user_id, _, _) in other_signature_list] + if signed_users: yield self.device_handler.notify_user_signature_update( user_id, signed_users ) return {"failures": failures} + @defer.inlineCallbacks + def _process_self_signatures(self, user_id, signatures): + signature_list = [] + failures = {} + if not signatures: + return signature_list, failures + + try: + # get our self-signing key to verify the signatures + self_signing_key, self_signing_key_id, self_signing_verify_key = yield self._get_e2e_cross_signing_verify_key( + user_id, "self_signing" + ) + + # get our master key, since it may be signed + master_key, master_key_id, master_verify_key = yield self._get_e2e_cross_signing_verify_key( + user_id, "master" + ) + + # fetch our stored devices. This is used to 1. verify + # signatures on the master key, and 2. to can compare with what + # was sent if the device was signed + devices = yield self.store.get_e2e_device_keys([(user_id, None)]) + + if user_id not in devices: + raise SynapseError(404, "No device keys found", Codes.NOT_FOUND) + + devices = devices[user_id] + except SynapseError as e: + failures[user_id] = { + device: _exception_to_failure(e) + for device in signatures.keys() + } + return signature_list, failures + + for device_id, device in signatures.items(): + try: + if ( + "signatures" not in device + or user_id not in device["signatures"] + ): + # no signature was sent + raise SynapseError( + 400, "Invalid signature", Codes.INVALID_SIGNATURE + ) + + if device_id == master_verify_key.version: + # we have master key signed by devices: for each + # device that signed, check the signature. Since + # the "failures" property in the response only has + # granularity up to the signed device, either all + # of the signatures on the master key succeed, or + # all fail. So loop over the signatures and add + # them to a separate signature list. If everything + # works out, then add them all to the main + # signature list. (In practice, we're likely to + # only have only one signature anyways.) + master_key_signature_list = [] + sigs = device["signatures"] + for signing_key_id, signature in sigs[user_id].items(): + alg, signing_device_id = signing_key_id.split(":", 1) + if ( + signing_device_id not in devices + or signing_key_id + not in devices[signing_device_id]["keys"]["keys"] + ): + # signed by an unknown device, or the + # device does not have the key + raise SynapseError( + 400, + "Invalid signature", + Codes.INVALID_SIGNATURE, + ) + + # get the key and check the signature + pubkey = devices[signing_device_id]["keys"]["keys"][ + signing_key_id + ] + verify_key = decode_verify_key_bytes( + signing_key_id, decode_base64(pubkey) + ) + _check_device_signature(user_id, verify_key, device, master_key) + device["signatures"] = sigs + + master_key_signature_list.append( + (signing_key_id, user_id, device_id, signature) + ) + + signature_list.extend(master_key_signature_list) + continue + + # at this point, we have a device that should be signed + # by the self-signing key + if self_signing_key_id not in device["signatures"][user_id]: + # no signature was sent + raise SynapseError( + 400, "Invalid signature", Codes.INVALID_SIGNATURE + ) + + stored_device = None + try: + stored_device = devices[device_id]["keys"] + except KeyError: + raise SynapseError(404, "Unknown device", Codes.NOT_FOUND) + if self_signing_key_id in stored_device.get( + "signatures", {} + ).get(user_id, {}): + # we already have a signature on this device, so we + # can skip it, since it should be exactly the same + continue + + _check_device_signature( + user_id, self_signing_verify_key, device, stored_device + ) + + signature = device["signatures"][user_id][self_signing_key_id] + signature_list.append( + (self_signing_key_id, user_id, device_id, signature) + ) + except SynapseError as e: + failures.setdefault(user_id, {})[ + device_id + ] = _exception_to_failure(e) + + return signature_list, failures + + @defer.inlineCallbacks + def _process_other_signatures(self, user_id, signatures): + # now check non-self signatures. These signatures will be signed + # by the user-signing key + signature_list = [] + failures = {} + if not signatures: + return signature_list, failures + + try: + # get our user-signing key to verify the signatures + user_signing_key, user_signing_key_id, user_signing_verify_key = yield self._get_e2e_cross_signing_verify_key( + user_id, "user_signing" + ) + except SynapseError as e: + failure = _exception_to_failure(e) + for user, devicemap in signatures.items(): + failures[user] = { + device_id: failure for device_id in devicemap.keys() + } + return signature_list, failures + + for user, devicemap in signatures.items(): + device_id = None + try: + # get the user's master key, to make sure it matches + # what was sent + stored_key, stored_key_id, _ = yield self._get_e2e_cross_signing_verify_key( + user, "master", user_id + ) + + # make sure that the user's master key is the one that + # was signed (and no others) + device_id = stored_key_id.split(":", 1)[1] + if device_id not in devicemap: + logger.error( + "upload signature: could not find signature for device %s", + device_id, + ) + # set device to None so that the failure gets + # marked on all the signatures + device_id = None + raise SynapseError(404, "Unknown device", Codes.NOT_FOUND) + key = devicemap[device_id] + other_devices = [k for k in devicemap.keys() if k != device_id] + if other_devices: + # other devices were signed -- mark those as failures + logger.error("upload signature: too many devices specified") + failure = _exception_to_failure( + SynapseError(404, "Unknown device", Codes.NOT_FOUND) + ) + failures[user] = { + device: failure for device in other_devices + } + + if user_signing_key_id in stored_key.get("signatures", {}).get( + user_id, {} + ): + # we already have the signature, so we can skip it + continue + + _check_device_signature( + user_id, user_signing_verify_key, key, stored_key + ) + + signature = key["signatures"][user_id][user_signing_key_id] + signature_list.append( + (user_signing_key_id, user, device_id, signature) + ) + except SynapseError as e: + failure = _exception_to_failure(e) + if device_id is None: + failures[user] = { + device_id: failure for device_id in devicemap.keys() + } + else: + failures.setdefault(user, {})[device_id] = failure + + return signature_list, failures + @defer.inlineCallbacks def _get_e2e_cross_signing_verify_key(self, user_id, key_type, from_user_id=None): key = yield self.store.get_e2e_cross_signing_key( From 415d0a00e0845654b34542b9914ea01224dd8ed6 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Fri, 6 Sep 2019 16:46:45 -0400 Subject: [PATCH 0027/1623] run black --- synapse/handlers/e2e_keys.py | 34 ++++++++++------------------------ 1 file changed, 10 insertions(+), 24 deletions(-) diff --git a/synapse/handlers/e2e_keys.py b/synapse/handlers/e2e_keys.py index 6500bf3e16..95f3cc891b 100644 --- a/synapse/handlers/e2e_keys.py +++ b/synapse/handlers/e2e_keys.py @@ -686,17 +686,13 @@ class E2eKeysHandler(object): devices = devices[user_id] except SynapseError as e: failures[user_id] = { - device: _exception_to_failure(e) - for device in signatures.keys() + device: _exception_to_failure(e) for device in signatures.keys() } return signature_list, failures for device_id, device in signatures.items(): try: - if ( - "signatures" not in device - or user_id not in device["signatures"] - ): + if "signatures" not in device or user_id not in device["signatures"]: # no signature was sent raise SynapseError( 400, "Invalid signature", Codes.INVALID_SIGNATURE @@ -725,9 +721,7 @@ class E2eKeysHandler(object): # signed by an unknown device, or the # device does not have the key raise SynapseError( - 400, - "Invalid signature", - Codes.INVALID_SIGNATURE, + 400, "Invalid signature", Codes.INVALID_SIGNATURE ) # get the key and check the signature @@ -760,9 +754,9 @@ class E2eKeysHandler(object): stored_device = devices[device_id]["keys"] except KeyError: raise SynapseError(404, "Unknown device", Codes.NOT_FOUND) - if self_signing_key_id in stored_device.get( - "signatures", {} - ).get(user_id, {}): + if self_signing_key_id in stored_device.get("signatures", {}).get( + user_id, {} + ): # we already have a signature on this device, so we # can skip it, since it should be exactly the same continue @@ -776,9 +770,7 @@ class E2eKeysHandler(object): (self_signing_key_id, user_id, device_id, signature) ) except SynapseError as e: - failures.setdefault(user_id, {})[ - device_id - ] = _exception_to_failure(e) + failures.setdefault(user_id, {})[device_id] = _exception_to_failure(e) return signature_list, failures @@ -799,9 +791,7 @@ class E2eKeysHandler(object): except SynapseError as e: failure = _exception_to_failure(e) for user, devicemap in signatures.items(): - failures[user] = { - device_id: failure for device_id in devicemap.keys() - } + failures[user] = {device_id: failure for device_id in devicemap.keys()} return signature_list, failures for user, devicemap in signatures.items(): @@ -833,9 +823,7 @@ class E2eKeysHandler(object): failure = _exception_to_failure( SynapseError(404, "Unknown device", Codes.NOT_FOUND) ) - failures[user] = { - device: failure for device in other_devices - } + failures[user] = {device: failure for device in other_devices} if user_signing_key_id in stored_key.get("signatures", {}).get( user_id, {} @@ -848,9 +836,7 @@ class E2eKeysHandler(object): ) signature = key["signatures"][user_id][user_signing_key_id] - signature_list.append( - (user_signing_key_id, user, device_id, signature) - ) + signature_list.append((user_signing_key_id, user, device_id, signature)) except SynapseError as e: failure = _exception_to_failure(e) if device_id is None: From ab729e31cfca4d1a958937bb576010271b9c8044 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Fri, 6 Sep 2019 17:52:37 -0400 Subject: [PATCH 0028/1623] use something that's the right type for user_id --- tests/handlers/test_e2e_keys.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/handlers/test_e2e_keys.py b/tests/handlers/test_e2e_keys.py index 7a59ec5085..854eb6c024 100644 --- a/tests/handlers/test_e2e_keys.py +++ b/tests/handlers/test_e2e_keys.py @@ -266,7 +266,7 @@ class E2eKeysHandlerTestCase(unittest.TestCase): device_key_1["signatures"][local_user]["ed25519:abc"] = "base64+signature" device_key_2["signatures"][local_user]["ed25519:def"] = "base64+signature" devices = yield self.handler.query_devices( - {"device_keys": {local_user: []}}, 0, 0 + {"device_keys": {local_user: []}}, 0, local_user ) del devices["device_keys"][local_user]["abc"]["unsigned"] del devices["device_keys"][local_user]["def"]["unsigned"] From d3f2fbcfe577f42d0208d15a57bd66e56186742a Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Sat, 7 Sep 2019 14:13:18 -0400 Subject: [PATCH 0029/1623] add function docs --- synapse/handlers/e2e_keys.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/synapse/handlers/e2e_keys.py b/synapse/handlers/e2e_keys.py index 95f3cc891b..cca361b15b 100644 --- a/synapse/handlers/e2e_keys.py +++ b/synapse/handlers/e2e_keys.py @@ -659,6 +659,18 @@ class E2eKeysHandler(object): @defer.inlineCallbacks def _process_self_signatures(self, user_id, signatures): + """Process uploaded signatures of the user's own keys. + + Args: + user_id (string): the user uploading the keys + signatures (dict[string, dict]): map of devices to signed keys + + Returns: + (list[(string, string, string, string)], dict[string, dict[string, dict]]): + a list of signatures to upload, in the form (signing_key_id, target_user_id, + target_device_id, signature), and a map of users to devices to failure + reasons + """ signature_list = [] failures = {} if not signatures: @@ -776,8 +788,18 @@ class E2eKeysHandler(object): @defer.inlineCallbacks def _process_other_signatures(self, user_id, signatures): - # now check non-self signatures. These signatures will be signed - # by the user-signing key + """Process uploaded signatures of other users' keys. + + Args: + user_id (string): the user uploading the keys + signatures (dict[string, dict]): map of users to devices to signed keys + + Returns: + (list[(string, string, string, string)], dict[string, dict[string, dict]]): + a list of signatures to upload, in the form (signing_key_id, target_user_id, + target_device_id, signature), and a map of users to devices to failure + reasons + """ signature_list = [] failures = {} if not signatures: From 9eaa5d6d2427a6c3edcdf18c0868c697c17fd6d4 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 12 Sep 2019 21:13:31 +0100 Subject: [PATCH 0030/1623] README: link to reverse_proxy.rst (#6027) --- README.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.rst b/README.rst index bbff8de5ab..fbbf958d6c 100644 --- a/README.rst +++ b/README.rst @@ -381,3 +381,16 @@ indicate that your server is also issuing far more outgoing federation requests than can be accounted for by your users' activity, this is a likely cause. The misbehavior can be worked around by setting ``use_presence: false`` in the Synapse config file. + +People can't accept room invitations from me +-------------------------------------------- + +The typical failure mode here is that you send an invitation to someone +to join a room or direct chat, but when they go to accept it, they get an +error (typically along the lines of "Invalid signature"). They might see +something like the following in their logs:: + + 2019-09-11 19:32:04,271 - synapse.federation.transport.server - 288 - WARNING - GET-11752 - authenticate_request failed: 401: Invalid signature for server with key ed25519:a_EqML: Unable to verify signature for + +This is normally caused by a misconfiguration in your reverse-proxy. See +``_ and double-check that your settings are correct. From 1c7df13e7b26f249726380cbec5a6bc7bb3daeb6 Mon Sep 17 00:00:00 2001 From: axel simon Date: Fri, 13 Sep 2019 09:50:17 +0200 Subject: [PATCH 0031/1623] add explanations on how to actually include an access_token (#6031) --- docs/admin_api/README.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/admin_api/README.rst b/docs/admin_api/README.rst index d4f564cfae..191806c5b4 100644 --- a/docs/admin_api/README.rst +++ b/docs/admin_api/README.rst @@ -10,3 +10,15 @@ server admin by updating the database directly, e.g.: ``UPDATE users SET admin = 1 WHERE name = '@foo:bar.com'`` Restarting may be required for the changes to register. + +Using an admin access_token +########################### + +Many of the API calls listed in the documentation here will require to include an admin `access_token`. +Finding your user's `access_token` is client-dependent, but will usually be shown in the client's settings. + +Once you have your `access_token`, to include it in a request, the best option is to add the token to a request header: + +``curl --header "Authorization: Bearer " `` + +Fore more details, please refer to the complete `matrix spec documentation `_. From 785cbd3999ab011440b453e07992d3b0c92a4059 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 13 Sep 2019 12:07:03 +0100 Subject: [PATCH 0032/1623] Make the sample saml config closer to our standards It' still not great, thanks to the nested dictionaries, but it's better. --- docs/sample_config.yaml | 110 +++++++++++++++++--------------- synapse/config/saml2_config.py | 113 ++++++++++++++++++--------------- 2 files changed, 121 insertions(+), 102 deletions(-) diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 0c6be30e51..8cfc5c312a 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -1031,12 +1031,13 @@ signing_key_path: "CONFDIR/SERVERNAME.signing.key" # Enable SAML2 for registration and login. Uses pysaml2. # -# `sp_config` is the configuration for the pysaml2 Service Provider. -# See pysaml2 docs for format of config. +# At least one of `sp_config` or `config_path` must be set in this section to +# enable SAML login. # -# Default values will be used for the 'entityid' and 'service' settings, -# so it is not normally necessary to specify them unless you need to -# override them. +# (You will probably also want to set the following options to `false` to +# disable the regular login/registration flows: +# * enable_registration +# * password_config.enabled # # Once SAML support is enabled, a metadata file will be exposed at # https://:/_matrix/saml2/metadata.xml, which you may be able to @@ -1044,52 +1045,59 @@ signing_key_path: "CONFDIR/SERVERNAME.signing.key" # the IdP to use an ACS location of # https://:/_matrix/saml2/authn_response. # -#saml2_config: -# sp_config: -# # point this to the IdP's metadata. You can use either a local file or -# # (preferably) a URL. -# metadata: -# #local: ["saml2/idp.xml"] -# remote: -# - url: https://our_idp/metadata.xml -# -# # By default, the user has to go to our login page first. If you'd like to -# # allow IdP-initiated login, set 'allow_unsolicited: True' in a -# # 'service.sp' section: -# # -# #service: -# # sp: -# # allow_unsolicited: True -# -# # The examples below are just used to generate our metadata xml, and you -# # may well not need it, depending on your setup. Alternatively you -# # may need a whole lot more detail - see the pysaml2 docs! -# -# description: ["My awesome SP", "en"] -# name: ["Test SP", "en"] -# -# organization: -# name: Example com -# display_name: -# - ["Example co", "en"] -# url: "http://example.com" -# -# contact_person: -# - given_name: Bob -# sur_name: "the Sysadmin" -# email_address": ["admin@example.com"] -# contact_type": technical -# -# # Instead of putting the config inline as above, you can specify a -# # separate pysaml2 configuration file: -# # -# config_path: "CONFDIR/sp_conf.py" -# -# # the lifetime of a SAML session. This defines how long a user has to -# # complete the authentication process, if allow_unsolicited is unset. -# # The default is 5 minutes. -# # -# # saml_session_lifetime: 5m +saml2_config: + # `sp_config` is the configuration for the pysaml2 Service Provider. + # See pysaml2 docs for format of config. + # + # Default values will be used for the 'entityid' and 'service' settings, + # so it is not normally necessary to specify them unless you need to + # override them. + # + #sp_config: + # # point this to the IdP's metadata. You can use either a local file or + # # (preferably) a URL. + # metadata: + # #local: ["saml2/idp.xml"] + # remote: + # - url: https://our_idp/metadata.xml + # + # # By default, the user has to go to our login page first. If you'd like + # # to allow IdP-initiated login, set 'allow_unsolicited: True' in a + # # 'service.sp' section: + # # + # #service: + # # sp: + # # allow_unsolicited: true + # + # # The examples below are just used to generate our metadata xml, and you + # # may well not need them, depending on your setup. Alternatively you + # # may need a whole lot more detail - see the pysaml2 docs! + # + # description: ["My awesome SP", "en"] + # name: ["Test SP", "en"] + # + # organization: + # name: Example com + # display_name: + # - ["Example co", "en"] + # url: "http://example.com" + # + # contact_person: + # - given_name: Bob + # sur_name: "the Sysadmin" + # email_address": ["admin@example.com"] + # contact_type": technical + + # Instead of putting the config inline as above, you can specify a + # separate pysaml2 configuration file: + # + #config_path: "CONFDIR/sp_conf.py" + + # the lifetime of a SAML session. This defines how long a user has to + # complete the authentication process, if allow_unsolicited is unset. + # The default is 5 minutes. + # + #saml_session_lifetime: 5m diff --git a/synapse/config/saml2_config.py b/synapse/config/saml2_config.py index 6a8161547a..c46ac087db 100644 --- a/synapse/config/saml2_config.py +++ b/synapse/config/saml2_config.py @@ -26,6 +26,9 @@ class SAML2Config(Config): if not saml2_config or not saml2_config.get("enabled", True): return + if not saml2_config.get("sp_config") and not saml2_config.get("config_path"): + return + try: check_requirements("saml2") except DependencyException as e: @@ -76,12 +79,13 @@ class SAML2Config(Config): return """\ # Enable SAML2 for registration and login. Uses pysaml2. # - # `sp_config` is the configuration for the pysaml2 Service Provider. - # See pysaml2 docs for format of config. + # At least one of `sp_config` or `config_path` must be set in this section to + # enable SAML login. # - # Default values will be used for the 'entityid' and 'service' settings, - # so it is not normally necessary to specify them unless you need to - # override them. + # (You will probably also want to set the following options to `false` to + # disable the regular login/registration flows: + # * enable_registration + # * password_config.enabled # # Once SAML support is enabled, a metadata file will be exposed at # https://:/_matrix/saml2/metadata.xml, which you may be able to @@ -89,52 +93,59 @@ class SAML2Config(Config): # the IdP to use an ACS location of # https://:/_matrix/saml2/authn_response. # - #saml2_config: - # sp_config: - # # point this to the IdP's metadata. You can use either a local file or - # # (preferably) a URL. - # metadata: - # #local: ["saml2/idp.xml"] - # remote: - # - url: https://our_idp/metadata.xml - # - # # By default, the user has to go to our login page first. If you'd like to - # # allow IdP-initiated login, set 'allow_unsolicited: True' in a - # # 'service.sp' section: - # # - # #service: - # # sp: - # # allow_unsolicited: True - # - # # The examples below are just used to generate our metadata xml, and you - # # may well not need it, depending on your setup. Alternatively you - # # may need a whole lot more detail - see the pysaml2 docs! - # - # description: ["My awesome SP", "en"] - # name: ["Test SP", "en"] - # - # organization: - # name: Example com - # display_name: - # - ["Example co", "en"] - # url: "http://example.com" - # - # contact_person: - # - given_name: Bob - # sur_name: "the Sysadmin" - # email_address": ["admin@example.com"] - # contact_type": technical - # - # # Instead of putting the config inline as above, you can specify a - # # separate pysaml2 configuration file: - # # - # config_path: "%(config_dir_path)s/sp_conf.py" - # - # # the lifetime of a SAML session. This defines how long a user has to - # # complete the authentication process, if allow_unsolicited is unset. - # # The default is 5 minutes. - # # - # # saml_session_lifetime: 5m + saml2_config: + # `sp_config` is the configuration for the pysaml2 Service Provider. + # See pysaml2 docs for format of config. + # + # Default values will be used for the 'entityid' and 'service' settings, + # so it is not normally necessary to specify them unless you need to + # override them. + # + #sp_config: + # # point this to the IdP's metadata. You can use either a local file or + # # (preferably) a URL. + # metadata: + # #local: ["saml2/idp.xml"] + # remote: + # - url: https://our_idp/metadata.xml + # + # # By default, the user has to go to our login page first. If you'd like + # # to allow IdP-initiated login, set 'allow_unsolicited: True' in a + # # 'service.sp' section: + # # + # #service: + # # sp: + # # allow_unsolicited: true + # + # # The examples below are just used to generate our metadata xml, and you + # # may well not need them, depending on your setup. Alternatively you + # # may need a whole lot more detail - see the pysaml2 docs! + # + # description: ["My awesome SP", "en"] + # name: ["Test SP", "en"] + # + # organization: + # name: Example com + # display_name: + # - ["Example co", "en"] + # url: "http://example.com" + # + # contact_person: + # - given_name: Bob + # sur_name: "the Sysadmin" + # email_address": ["admin@example.com"] + # contact_type": technical + + # Instead of putting the config inline as above, you can specify a + # separate pysaml2 configuration file: + # + #config_path: "%(config_dir_path)s/sp_conf.py" + + # the lifetime of a SAML session. This defines how long a user has to + # complete the authentication process, if allow_unsolicited is unset. + # The default is 5 minutes. + # + #saml_session_lifetime: 5m """ % { "config_dir_path": config_dir_path } From a8ac40445c98b9e1fc2538d7d4ec49c80b0298ac Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 13 Sep 2019 15:20:49 +0100 Subject: [PATCH 0033/1623] Record mappings from saml users in an external table We want to assign unique mxids to saml users based on an incrementing suffix. For that to work, we need to record the allocated mxid in a separate table. --- docs/sample_config.yaml | 26 +++++ synapse/config/saml2_config.py | 78 ++++++++++++- synapse/handlers/saml_handler.py | 103 ++++++++++++++++-- synapse/rest/client/v1/login.py | 14 +++ synapse/storage/registration.py | 41 +++++++ .../schema/delta/56/user_external_ids.sql | 24 ++++ 6 files changed, 276 insertions(+), 10 deletions(-) create mode 100644 synapse/storage/schema/delta/56/user_external_ids.sql diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 8cfc5c312a..9021fe2cb8 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -1099,6 +1099,32 @@ saml2_config: # #saml_session_lifetime: 5m + # The SAML attribute (after mapping via the attribute maps) to use to derive + # the Matrix ID from. 'uid' by default. + # + #mxid_source_attribute: displayName + + # The mapping system to use for mapping the saml attribute onto a matrix ID. + # Options include: + # * 'hexencode' (which maps unpermitted characters to '=xx') + # * 'dotreplace' (which replaces unpermitted characters with '.'). + # The default is 'hexencode'. + # + #mxid_mapping: dotreplace + + # In previous versions of synapse, the mapping from SAML attribute to MXID was + # always calculated dynamically rather than stored in a table. For backwards- + # compatibility, we will look for user_ids matching such a pattern before + # creating a new account. + # + # This setting controls the SAML attribute which will be used for this + # backwards-compatibility lookup. Typically it should be 'uid', but if the + # attribute maps are changed, it may be necessary to change it. + # + # The default is 'uid'. + # + #grandfathered_mxid_source_attribute: upn + # Enable CAS for registration and login. diff --git a/synapse/config/saml2_config.py b/synapse/config/saml2_config.py index c46ac087db..a022470702 100644 --- a/synapse/config/saml2_config.py +++ b/synapse/config/saml2_config.py @@ -12,7 +12,13 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import re + from synapse.python_dependencies import DependencyException, check_requirements +from synapse.types import ( + map_username_to_mxid_localpart, + mxid_localpart_allowed_characters, +) from ._base import Config, ConfigError @@ -36,6 +42,14 @@ class SAML2Config(Config): self.saml2_enabled = True + self.saml2_mxid_source_attribute = saml2_config.get( + "mxid_source_attribute", "uid" + ) + + self.saml2_grandfathered_mxid_source_attribute = saml2_config.get( + "grandfathered_mxid_source_attribute", "uid" + ) + import saml2.config self.saml2_sp_config = saml2.config.SPConfig() @@ -51,6 +65,12 @@ class SAML2Config(Config): saml2_config.get("saml_session_lifetime", "5m") ) + mapping = saml2_config.get("mxid_mapping", "hexencode") + try: + self.saml2_mxid_mapper = MXID_MAPPER_MAP[mapping] + except KeyError: + raise ConfigError("%s is not a known mxid_mapping" % (mapping,)) + def _default_saml_config_dict(self): import saml2 @@ -58,6 +78,13 @@ class SAML2Config(Config): if public_baseurl is None: raise ConfigError("saml2_config requires a public_baseurl to be set") + required_attributes = {"uid", self.saml2_mxid_source_attribute} + + optional_attributes = {"displayName"} + if self.saml2_grandfathered_mxid_source_attribute: + optional_attributes.add(self.saml2_grandfathered_mxid_source_attribute) + optional_attributes -= required_attributes + metadata_url = public_baseurl + "_matrix/saml2/metadata.xml" response_url = public_baseurl + "_matrix/saml2/authn_response" return { @@ -69,8 +96,9 @@ class SAML2Config(Config): (response_url, saml2.BINDING_HTTP_POST) ] }, - "required_attributes": ["uid"], - "optional_attributes": ["mail", "surname", "givenname"], + "required_attributes": list(required_attributes), + "optional_attributes": list(optional_attributes), + # "name_id_format": saml2.saml.NAMEID_FORMAT_PERSISTENT, } }, } @@ -146,6 +174,52 @@ class SAML2Config(Config): # The default is 5 minutes. # #saml_session_lifetime: 5m + + # The SAML attribute (after mapping via the attribute maps) to use to derive + # the Matrix ID from. 'uid' by default. + # + #mxid_source_attribute: displayName + + # The mapping system to use for mapping the saml attribute onto a matrix ID. + # Options include: + # * 'hexencode' (which maps unpermitted characters to '=xx') + # * 'dotreplace' (which replaces unpermitted characters with '.'). + # The default is 'hexencode'. + # + #mxid_mapping: dotreplace + + # In previous versions of synapse, the mapping from SAML attribute to MXID was + # always calculated dynamically rather than stored in a table. For backwards- + # compatibility, we will look for user_ids matching such a pattern before + # creating a new account. + # + # This setting controls the SAML attribute which will be used for this + # backwards-compatibility lookup. Typically it should be 'uid', but if the + # attribute maps are changed, it may be necessary to change it. + # + # The default is 'uid'. + # + #grandfathered_mxid_source_attribute: upn """ % { "config_dir_path": config_dir_path } + + +DOT_REPLACE_PATTERN = re.compile( + ("[^%s]" % (re.escape("".join(mxid_localpart_allowed_characters)),)) +) + + +def dot_replace_for_mxid(username: str) -> str: + username = username.lower() + username = DOT_REPLACE_PATTERN.sub(".", username) + + # regular mxids aren't allowed to start with an underscore either + username = re.sub("^_", "", username) + return username + + +MXID_MAPPER_MAP = { + "hexencode": map_username_to_mxid_localpart, + "dotreplace": dot_replace_for_mxid, +} diff --git a/synapse/handlers/saml_handler.py b/synapse/handlers/saml_handler.py index a1ce6929cf..5fa8272dc9 100644 --- a/synapse/handlers/saml_handler.py +++ b/synapse/handlers/saml_handler.py @@ -21,6 +21,8 @@ from saml2.client import Saml2Client from synapse.api.errors import SynapseError from synapse.http.servlet import parse_string from synapse.rest.client.v1.login import SSOAuthHandler +from synapse.types import UserID, map_username_to_mxid_localpart +from synapse.util.async_helpers import Linearizer logger = logging.getLogger(__name__) @@ -29,12 +31,26 @@ class SamlHandler: def __init__(self, hs): self._saml_client = Saml2Client(hs.config.saml2_sp_config) self._sso_auth_handler = SSOAuthHandler(hs) + self._registration_handler = hs.get_registration_handler() + + self._clock = hs.get_clock() + self._datastore = hs.get_datastore() + self._hostname = hs.hostname + self._saml2_session_lifetime = hs.config.saml2_session_lifetime + self._mxid_source_attribute = hs.config.saml2_mxid_source_attribute + self._grandfathered_mxid_source_attribute = ( + hs.config.saml2_grandfathered_mxid_source_attribute + ) + self._mxid_mapper = hs.config.saml2_mxid_mapper + + # identifier for the external_ids table + self._auth_provider_id = "saml" # a map from saml session id to Saml2SessionData object self._outstanding_requests_dict = {} - self._clock = hs.get_clock() - self._saml2_session_lifetime = hs.config.saml2_session_lifetime + # a lock on the mappings + self._mapping_lock = Linearizer(name="saml_mapping", clock=self._clock) def handle_redirect_request(self, client_redirect_url): """Handle an incoming request to /login/sso/redirect @@ -60,7 +76,7 @@ class SamlHandler: # this shouldn't happen! raise Exception("prepare_for_authenticate didn't return a Location header") - def handle_saml_response(self, request): + async def handle_saml_response(self, request): """Handle an incoming request to /_matrix/saml2/authn_response Args: @@ -77,6 +93,10 @@ class SamlHandler: # the dict. self.expire_sessions() + user_id = await self._map_saml_response_to_user(resp_bytes) + self._sso_auth_handler.complete_sso_login(user_id, request, relay_state) + + async def _map_saml_response_to_user(self, resp_bytes): try: saml2_auth = self._saml_client.parse_authn_request_response( resp_bytes, @@ -91,18 +111,85 @@ class SamlHandler: logger.warning("SAML2 response was not signed") raise SynapseError(400, "SAML2 response was not signed") - if "uid" not in saml2_auth.ava: + try: + remote_user_id = saml2_auth.ava["uid"][0] + except KeyError: logger.warning("SAML2 response lacks a 'uid' attestation") raise SynapseError(400, "uid not in SAML2 response") + try: + mxid_source = saml2_auth.ava[self._mxid_source_attribute][0] + except KeyError: + logger.warning( + "SAML2 response lacks a '%s' attestation", self._mxid_source_attribute + ) + raise SynapseError( + 400, "%s not in SAML2 response" % (self._mxid_source_attribute,) + ) + self._outstanding_requests_dict.pop(saml2_auth.in_response_to, None) - username = saml2_auth.ava["uid"][0] displayName = saml2_auth.ava.get("displayName", [None])[0] - return self._sso_auth_handler.on_successful_auth( - username, request, relay_state, user_display_name=displayName - ) + with (await self._mapping_lock.queue(self._auth_provider_id)): + # first of all, check if we already have a mapping for this user + logger.info( + "Looking for existing mapping for user %s:%s", + self._auth_provider_id, + remote_user_id, + ) + registered_user_id = await self._datastore.get_user_by_external_id( + self._auth_provider_id, remote_user_id + ) + if registered_user_id is not None: + logger.info("Found existing mapping %s", registered_user_id) + return registered_user_id + + # backwards-compatibility hack: see if there is an existing user with a + # suitable mapping from the uid + if ( + self._grandfathered_mxid_source_attribute + and self._grandfathered_mxid_source_attribute in saml2_auth.ava + ): + attrval = saml2_auth.ava[self._grandfathered_mxid_source_attribute][0] + user_id = UserID( + map_username_to_mxid_localpart(attrval), self._hostname + ).to_string() + logger.info( + "Looking for existing account based on mapped %s %s", + self._grandfathered_mxid_source_attribute, + user_id, + ) + + users = await self._datastore.get_users_by_id_case_insensitive(user_id) + if users: + registered_user_id = list(users.keys())[0] + logger.info("Grandfathering mapping to %s", registered_user_id) + await self._datastore.record_user_external_id( + self._auth_provider_id, remote_user_id, registered_user_id + ) + return registered_user_id + + # figure out a new mxid for this user + base_mxid_localpart = self._mxid_mapper(mxid_source) + + suffix = 0 + while True: + localpart = base_mxid_localpart + (str(suffix) if suffix else "") + if not await self._datastore.get_users_by_id_case_insensitive( + UserID(localpart, self._hostname).to_string() + ): + break + suffix += 1 + logger.info("Allocating mxid for new user with localpart %s", localpart) + + registered_user_id = await self._registration_handler.register_user( + localpart=localpart, default_display_name=displayName + ) + await self._datastore.record_user_external_id( + self._auth_provider_id, remote_user_id, registered_user_id + ) + return registered_user_id def expire_sessions(self): expire_before = self._clock.time_msec() - self._saml2_session_lifetime diff --git a/synapse/rest/client/v1/login.py b/synapse/rest/client/v1/login.py index 5762b9fd06..eeaa72b205 100644 --- a/synapse/rest/client/v1/login.py +++ b/synapse/rest/client/v1/login.py @@ -29,6 +29,7 @@ from synapse.http.servlet import ( parse_json_object_from_request, parse_string, ) +from synapse.http.site import SynapseRequest from synapse.rest.client.v2_alpha._base import client_patterns from synapse.rest.well_known import WellKnownBuilder from synapse.types import UserID, map_username_to_mxid_localpart @@ -507,6 +508,19 @@ class SSOAuthHandler(object): localpart=localpart, default_display_name=user_display_name ) + self.complete_sso_login(registered_user_id, request, client_redirect_url) + + def complete_sso_login( + self, registered_user_id: str, request: SynapseRequest, client_redirect_url: str + ): + """Having figured out a mxid for this user, complete the HTTP request + + Args: + registered_user_id: + request: + client_redirect_url: + """ + login_token = self._macaroon_gen.generate_short_term_login_token( registered_user_id ) diff --git a/synapse/storage/registration.py b/synapse/storage/registration.py index 55e4e84d71..1e3c2148f6 100644 --- a/synapse/storage/registration.py +++ b/synapse/storage/registration.py @@ -22,6 +22,7 @@ from six import iterkeys from six.moves import range from twisted.internet import defer +from twisted.internet.defer import Deferred from synapse.api.constants import UserTypes from synapse.api.errors import Codes, StoreError, ThreepidValidationError @@ -337,6 +338,26 @@ class RegistrationWorkerStore(SQLBaseStore): return self.runInteraction("get_users_by_id_case_insensitive", f) + async def get_user_by_external_id( + self, auth_provider: str, external_id: str + ) -> str: + """Look up a user by their external auth id + + Args: + auth_provider: identifier for the remote auth provider + external_id: id on that system + + Returns: + str|None: the mxid of the user, or None if they are not known + """ + return await self._simple_select_one_onecol( + table="user_external_ids", + keyvalues={"auth_provider": auth_provider, "external_id": external_id}, + retcol="user_id", + allow_none=True, + desc="get_user_by_external_id", + ) + @defer.inlineCallbacks def count_all_users(self): """Counts all users registered on the homeserver.""" @@ -848,6 +869,26 @@ class RegistrationStore( self._invalidate_cache_and_stream(txn, self.get_user_by_id, (user_id,)) txn.call_after(self.is_guest.invalidate, (user_id,)) + def record_user_external_id( + self, auth_provider: str, external_id: str, user_id: str + ) -> Deferred: + """Record a mapping from an external user id to a mxid + + Args: + auth_provider: identifier for the remote auth provider + external_id: id on that system + user_id: complete mxid that it is mapped to + """ + return self._simple_insert( + table="user_external_ids", + values={ + "auth_provider": auth_provider, + "external_id": external_id, + "user_id": user_id, + }, + desc="record_user_external_id", + ) + def user_set_password_hash(self, user_id, password_hash): """ NB. This does *not* evict any cache because the one use for this diff --git a/synapse/storage/schema/delta/56/user_external_ids.sql b/synapse/storage/schema/delta/56/user_external_ids.sql new file mode 100644 index 0000000000..91390c4527 --- /dev/null +++ b/synapse/storage/schema/delta/56/user_external_ids.sql @@ -0,0 +1,24 @@ +/* Copyright 2019 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * a table which records mappings from external auth providers to mxids + */ +CREATE TABLE IF NOT EXISTS user_external_ids ( + auth_provider TEXT NOT NULL, + external_id TEXT NOT NULL, + user_id TEXT NOT NULL, + UNIQUE (auth_provider, external_id) +); From b9d57502da8ae4e11523a155e0fd608433e1025d Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 13 Sep 2019 16:06:03 +0100 Subject: [PATCH 0034/1623] changelog --- changelog.d/6037.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6037.feature diff --git a/changelog.d/6037.feature b/changelog.d/6037.feature new file mode 100644 index 0000000000..95d82bd4d8 --- /dev/null +++ b/changelog.d/6037.feature @@ -0,0 +1 @@ +Handle userid clashes when authenticating via SAML by appending an integer suffix. \ No newline at end of file From a136137b2efae0fa5b3344cb94759fe0f5913221 Mon Sep 17 00:00:00 2001 From: Pete Date: Thu, 19 Sep 2019 09:52:59 +0100 Subject: [PATCH 0035/1623] Update INSTALL.md with void-linux (#5873) --- INSTALL.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/INSTALL.md b/INSTALL.md index 5728882460..38c113b269 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -349,6 +349,13 @@ sudo pip uninstall py-bcrypt sudo pip install py-bcrypt ``` +### Void Linux + +Synapse can be found in the void repositories as 'synapse': + + xbps-install -Su + xbps-install -S synapse + ### FreeBSD Synapse can be installed via FreeBSD Ports or Packages contributed by Brendan Molloy from: From 62e3ff92fd3228b5c34f6cee691e22f9b1f85c9e Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Thu, 19 Sep 2019 10:53:14 +0100 Subject: [PATCH 0036/1623] Remove POST method from password reset submit_token endpoint (#6056) Removes the POST method from `/password_reset//submit_token/` as it's only used by phone number verification which Synapse does not support yet. --- changelog.d/6056.bugfix | 1 + synapse/rest/client/v2_alpha/account.py | 17 ----------------- 2 files changed, 1 insertion(+), 17 deletions(-) create mode 100644 changelog.d/6056.bugfix diff --git a/changelog.d/6056.bugfix b/changelog.d/6056.bugfix new file mode 100644 index 0000000000..4d9573a58d --- /dev/null +++ b/changelog.d/6056.bugfix @@ -0,0 +1 @@ +Remove POST method from password reset submit_token endpoint until we implement submit_url functionality. \ No newline at end of file diff --git a/synapse/rest/client/v2_alpha/account.py b/synapse/rest/client/v2_alpha/account.py index 2ea515d2f6..afaaeeacdd 100644 --- a/synapse/rest/client/v2_alpha/account.py +++ b/synapse/rest/client/v2_alpha/account.py @@ -272,23 +272,6 @@ class PasswordResetSubmitTokenServlet(RestServlet): request.write(html.encode("utf-8")) finish_request(request) - @defer.inlineCallbacks - def on_POST(self, request, medium): - if medium != "email": - raise SynapseError( - 400, "This medium is currently not supported for password resets" - ) - - body = parse_json_object_from_request(request) - assert_params_in_dict(body, ["sid", "client_secret", "token"]) - - valid, _ = yield self.store.validate_threepid_session( - body["sid"], body["client_secret"], body["token"], self.clock.time_msec() - ) - response_code = 200 if valid else 400 - - return response_code, {"success": valid} - class PasswordRestServlet(RestServlet): PATTERNS = client_patterns("/account/password$") From 84a2743e2eaf5402cef8b68327efaf54daf64150 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 19 Sep 2019 10:55:43 +0100 Subject: [PATCH 0037/1623] Add changelog --- changelog.d/6064.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6064.misc diff --git a/changelog.d/6064.misc b/changelog.d/6064.misc new file mode 100644 index 0000000000..28dc89111b --- /dev/null +++ b/changelog.d/6064.misc @@ -0,0 +1 @@ +Clean up the sample config for SAML authentication. From bcd91328692555d85df346c4571085c9b41b8f6a Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 19 Sep 2019 15:06:27 +0100 Subject: [PATCH 0038/1623] Undo the deletion of some tables (#6047) This is a partial revert of #5893. The problem is that if we drop these tables in the same release as removing the code that writes to them, it prevents users users from being able to roll back to a previous release. So let's leave the tables in place for now, and remember to drop them in a subsequent release. (Note that these tables haven't been *read* for *years*, so any missing rows resulting from a temporary upgrade to vNext won't cause a problem.) --- changelog.d/5893.misc | 2 +- changelog.d/6047.misc | 2 ++ .../delta/56/drop_unused_event_tables.sql | 20 ------------------- 3 files changed, 3 insertions(+), 21 deletions(-) create mode 100644 changelog.d/6047.misc delete mode 100644 synapse/storage/schema/delta/56/drop_unused_event_tables.sql diff --git a/changelog.d/5893.misc b/changelog.d/5893.misc index 07ee4888dc..5ef171cb3e 100644 --- a/changelog.d/5893.misc +++ b/changelog.d/5893.misc @@ -1 +1 @@ -Drop some unused tables. +Stop populating some unused tables. diff --git a/changelog.d/6047.misc b/changelog.d/6047.misc new file mode 100644 index 0000000000..a4cdb8abb3 --- /dev/null +++ b/changelog.d/6047.misc @@ -0,0 +1,2 @@ +Stop populating some unused tables. + diff --git a/synapse/storage/schema/delta/56/drop_unused_event_tables.sql b/synapse/storage/schema/delta/56/drop_unused_event_tables.sql deleted file mode 100644 index 9f09922c67..0000000000 --- a/synapse/storage/schema/delta/56/drop_unused_event_tables.sql +++ /dev/null @@ -1,20 +0,0 @@ -/* Copyright 2019 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - --- these tables are never used. -DROP TABLE IF EXISTS room_names; -DROP TABLE IF EXISTS topics; -DROP TABLE IF EXISTS history_visibility; -DROP TABLE IF EXISTS guest_access; From 35ce3bda7aaa6281f02123225ca63d913fa12df1 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 19 Sep 2019 15:06:48 +0100 Subject: [PATCH 0039/1623] Add some notes on rolling back to v1.3.1. (#6049) --- UPGRADE.rst | 25 +++++++++++++++++++++++++ changelog.d/6049.doc | 1 + 2 files changed, 26 insertions(+) create mode 100644 changelog.d/6049.doc diff --git a/UPGRADE.rst b/UPGRADE.rst index 5aaf804902..53f3af4ed1 100644 --- a/UPGRADE.rst +++ b/UPGRADE.rst @@ -99,6 +99,31 @@ Synapse will expect these files to exist inside the configured template director default templates, see `synapse/res/templates `_. +Rolling back to v1.3.1 +---------------------- + +If you encounter problems with v1.4.0, it should be possible to roll back to +v1.3.1, subject to the following: + +* The 'room statistics' engine was heavily reworked in this release (see + `#5971 `_), including + significant changes to the database schema, which are not easily + reverted. This will cause the room statistics engine to stop updating when + you downgrade. + + The room statistics are essentially unused in v1.3.1 (in future versions of + Synapse, they will be used to populate the room directory), so there should + be no loss of functionality. However, the statistics engine will write errors + to the logs, which can be avoided by setting the following in `homeserver.yaml`: + + .. code:: yaml + + stats: + enabled: false + + Don't forget to re-enable it when you upgrade again, in preparation for its + use in the room directory! + Upgrading to v1.2.0 =================== diff --git a/changelog.d/6049.doc b/changelog.d/6049.doc new file mode 100644 index 0000000000..e0307bf5c1 --- /dev/null +++ b/changelog.d/6049.doc @@ -0,0 +1 @@ +Add some notes on rolling back to v1.3.1. From 466866a1d9dd1fcf82348a36c0532cb0c6614767 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 19 Sep 2019 15:08:14 +0100 Subject: [PATCH 0040/1623] Update the issue template for new way of getting server version (#6051) cf #4878 --- .github/ISSUE_TEMPLATE/BUG_REPORT.md | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.md b/.github/ISSUE_TEMPLATE/BUG_REPORT.md index 5cf844bfb1..9dd05bcba3 100644 --- a/.github/ISSUE_TEMPLATE/BUG_REPORT.md +++ b/.github/ISSUE_TEMPLATE/BUG_REPORT.md @@ -7,7 +7,7 @@ about: Create a report to help us improve -- **Homeserver**: +- **Homeserver**: If not matrix.org: -- **Version**: + What version of Synapse is running? -- **Install method**: +You can find the Synapse version with this command: + +$ curl http://localhost:8008/_synapse/admin/v1/server_version + +(You may need to replace `localhost:8008` if Synapse is not configured to +listen on that port.) +--> +- **Version**: + +- **Install method**: -- **Platform**: +- **Platform**: T1g1G~;NnaRL~@G(M_vsiXDyZy$>FSQ=&UA-3UPd~!90^&O3u&TDqKnJ-+GG_lW0}UPtGN_*xWa)&M>C?< zGMxlNzT=_`zpdF&tkay{8G)Sq@M8bX6Lvmb-O=3H^`H!P3|&EDyB@$jRZRIw#ny@% z3b;9RiI$}*x|=Ab0rd6m0ap)2Rz$ac5r*ZDJx>pJtbRBa39<6-JMw|kDIwZ zS8K15TVH#e`V(}nK~nB<3KYfD`j7*5pz%xq%}{iu<@&P)yi?3{IEE4tAj!jVO;VCk z9dIeX>5a+y%sN#4BMCF+wlh-#uyS??6PGw)P~?kF~i$IV{HP-M$! zt{lcHdcvw}6au0AupaReuvpFxCvB)QlEiXtP7@d^LxSHy(~OaP7DjU->3{Qtoln<0 z#qP{IWlgL`YfRNTlOZffLZ#WW^ui~smJ3#v3mTp}8eumCGT~s7)hZA#`=Z@*;I=v= zN0KcK!mI=A2|Iu7gq=^nH4dy51>vXWl8nlUIBXbU1HHwCIkn zx*#JI6?T>-k9;uFg+mL^YBf6(5*>knjSz=JTVKvtuEVArG{n^`eD0yu`BzWa`El^tGF$D*nj1Ooln<0wYx4|&92@u(u!{=9lxKIlj}|@dcUaA*}xSsRkVsT)N!9W zjBI$`7|8=KMVt;ll^mScXRQ{c)FS&~cK-4SJD;w1irtxasxPC}jvF(5D2BD3r}3oe zYDI;e@;W{{iYkzsv`yaP<+dA66q8h1q}g)I{W;~S7~(diAS>v2vO0e$kN>}N>A%{0 z=^JnS`K#ZZ{qWA~tAGQafBwqzwy6EUhc4|iOr?x=q}riwHmo^dG*&iSGsUbVe#m*^ z44PGaES4wnc%~;}EAR$$j!p9E@%MXk^{_*ayqCZ~Ne#C=P97 zaukG?9cBgDvtL&}^~pQNJa&%k#P;JT`_0Gi>~sDQi$Rx!6RDlm0cy-DD;_^&JY*?! zCvdN28SO)_o6t*3x|2P1zd!DFq>$>gp_*%x1Gm-W(;gkH>~z*$RDxh)I0Hls;K~uU z{yc6~>eTRPTod>fw`#+>kkwUA24xnvTm7umG9{2ky(+CR8oTp}3}71C$%P5b9}uTg zI~&@a$=xuc=B{b@fj=@0@5t~*MwHW(|Nb7M?wrrq{0ottIpD(lp=T7hIBmV;Uorzf z&e)uNE93eaI{W^IU?4|7G_$#h?Nf7j=Tq6&EPEO|*_X=M&ud;parVjV7az|DxBkb` z*iLg|a+Iuq?r`Mi|MG%tkWLWAA43oTIQiRAR_ijc!=v?tgOwTplmQ4q%P5k4-7zA^ z&R4=G??jFPRjT=u#F3b?4Q6STtgQZuYgb9PJ+_k)q%{u^+i5atiwpYDU>=s&BuAs6 zZ^ZNP_(7)#$)9aoP^FTE8yH6|3k!0LzT9(5m99F5oNlQnQjG(HS=p5X(Q_N)akm0+ z<*Gv?C9aw^`!2^Yn_#%)HR@tRL?K`?W=35go+h9)>dl!pz!P(JLWp-hm9uc}=M>qw zBxI}~+8c)csZU-0f_*uc#2-GoR73{OUohK9VtJD!K5@w^Dw?MxYZ@%FW#whc#Cu9& zm6vh7w|KXe%a45!1c3D#$-jDJ*#l$l*j%4{N#P*;Tw}&^eXvZkM+qTqh3Gm z6_ipYB!;DqosZP|Un zeKN-*bA12j(6$USAKm&xBR?6fHOczn_w@{%5%(DN41xmrX0%byNJDX$l~m?Jqn<{M zDd~Y-XaUt?i)s#PqKJ6B-aBx4sg7tD)-&VYJQyQ(31en#3MJLHJjyDnFXskFq9ne7r(hP(M!E&4G6?joI#UBFN>qYIUb-O18(E2SiyFn630oKJ-k_ zd@gt{*E8QAUD_`oaJ$hn@K>s5@;&D*J8}_4l|SH=-k8{vezu!te|Yj6@jdLw8BH^( zxZ?)d)|t^^jiRY%q{q^JC$|_5PZjSz1grTc?@8#Kaxj{A*_PNI5n1mGJljgM zhg@W5mjV~b{2mpBl*4K!zpRl=(DI5m`L;E$rcgU{T85}PxQYx$eR#Y=9Fu4<&_WkQz=osm^-kcl{XyZXcO6vk0G#RIKGNvk{|Fur?*jIT>q$f$f!GYoqJpq^znZv^< zv%MkS1>LT}>0VQhk@6s(Af=flX18+y*k3IyM{)YN-(F@ByZxk8sm@V!VXp*Vg9moE zjBU5Ch+N_(L~tUR`8Ayz_d815c9!pEa-FSGzr1B7LMuSQLc{O7Boh zly;j`(N?RLO5Vt9b^J1KHCtJ8yxXGg2%#`twEFqgf7UdGK6NbK=0Jt+R_&fkQ?4Mt zVz0ngpV~#CGJK}npjSbtRcX{`w2mtUwVoqa6B$^p6a-Zrx>+_F(7{-6DPneUbAH~* zYW!m9T$nbcvF27Q2UZPsyE9s@Dyt#cBK)A#(AB0jkZO2EL;<4Q4pxn^3QH|v-kS;9 zY`!Q-3v@KLtM&1MpMhESrVBwTckg9 z7gd0&X9bssew|=|8RM54_*SOSdg!@K?p<5U-63+aHu2+EQq%uO=6!O`_p#B&23*|9$JWM`hQ^?JU48p#FZ z9D&Rg%tb{IK%m@@wowFql%TaJjHV2p6f$Q|)*7P$V~B!ao` z>L{FhPitplG{^;=Qao&eqANgcm2A?ij1Lbi zx>W-ut}8Az2f&@##U(BY7u}A%Qpsi9&~&5C2@9O=;OT8I&1S6Ac~>+Ww%{s3xm~)DL7b+kG4~PE^=|c*yFI&|@noIB=+Lo6uXx zl9-9ia=N3lDY(%KzaS1llSyzD)2U`?N;s*}S`FkRy{@T<(zNQm`k}^0_3^!X^a4lf zd?(w75)NIN$;_=#fZp*3N08i4KylRoId|qVW!8eBUdQB8c(M83Jz@Jm&>FQmiLQHT zo5A&z0Ilv3V8eXk9LhWcqgfcTG+0KP^wWl|IV0;~3;xL*vNzDN*+h8E3agwgu(K4_ z99k`W3WFOwBjE?&V9MEHN1kWR48eg8Rp7FYFB@2&al`DWnmyzqZh3cgun_6Va$I6c zqgH)VFM(Jy>*+FAobIwUiY%%P*7Q2#$#xpqQoYkeyU@5SB4eNd>*l0AUnZS|!5t5q zC|OoX8`iR%f~^!*KJ?r~nBL4~FgFnjtgYSnhrkVf`8)g1ybKm0@Dtg>ICXS>ut&e; z^!#LG?*V>pBOG5pKYUDnyfH2)j3Ve+A_s;H+7-YwDD{&lu9IO%5C={;=*DeNn}%Ao z35r9mTx!PDg+)%8*3o7Zr*_+bG^xo~n8CQ$nXb@euDM*NR6elDc5|UngYhz4%zDdZ zYam;wrqNV?n01+g*;&aUF;Iu#&Sa_yfZdhI?VMHl&@++y!r+BmUp8|7CSRB-GOkae3k=$-?&xgL=+i%?8+Y1QX zjuZ{>l`5Ki>pXQse`mjlrrNOgZjN$!GW+iD{wNm|2EaVrX5;zas)XU($r5JT)5?Jm zX(d6P@=Llx#w**PS3Q4V1}Vg>T>s+t|I?C%!?MgTF<(LIZk$9ZBZOVZan#r>*#m51 z20)dA1*|ue-74U4<6(QmO#Eh=g|rO7K9zRc`T;SrgQ&;bAQc>l+r_W0hyLLF|Eqg1 ze(uJfx%O?Be)a6b?>m2aMmg|>!MpC8XLy!7$H2TX6%&ExY`fFaq@_4$XI)k8Dwu{9 zNvWkZAe^DV?Wt!b1S9laVk|V9OTRb1z>PV$@#13C)#6yw1tTOYD|Oizi`BS6t4kG} zWT#Lg;Irnuz^nlg+B(8~KMEPCJQ*-8-w#^~#LueHn3{ze#v%@5;GkmhO~^)%WDnWw zWhnZ90_NGdY}&~s{yXN`GjE>NYPB;XBFl2sim)!BhQrYqYXMy$8cX6F8ckCi&oVWT z6cj+GM@00+%`-3LYJ5kd4HL4Ktrp14dIUGOfq7++^?q$j-hp1~4Q*Lv2fe7>LHkPp zQHX$%x+TcZIwwykzom(d+A@|#CfYdAsZO}%1!+F?wR!gPeeFauv!p`3Vn)dBd|R}SW;tM zWW_Lxl7W}1`V<@rTz67lPzTJxmZy+>==sjFcqw1zKXB>NeqEmQY%gE+CriVXttwqV3|&d;RJu!XM+g6nX>_Z9#cj?(dlkaeoSYTUy-qgjJ z-WpqWd%+|NeC1Ts^x`&MqcVpCL##FiBidBD7O@(3wP|8DlzCaNESmxE@qFf03jk~8 znZ$svmAbyqm~;;t)uAfUKM0j1nN4QN5OrW16!Y)(%R8UlskeD5)p zce+TIw4D{#X#&E+WTa4*mSss=0L@7y64&KRxQPjQet%L2ES2+EqA>;qpo77Zr>vN6 zc{F0njw?6X)9aqmTc7dXNb)^E4m{| zV1KSZ4U6!Z&p!wf3#O9q9@8KUy!^Af%=sue(XR!Hnk;s##m(LKJcUCs` z@iYp3!vgQKxqJqjPZx?k>74isv3Y0ZWFJqr$u}(EKAYh)*!-3PvJd(pK0|EYS((|# z(?jwNt8|~u&}KHjrB#oH%LJ{E&E7L)(Cx0FPoF}nyem~206Xc9qnakkacf;z?Udcz z7==T6G}n1|RdtRq*cZLTA}Z9NVvG}K5o-jA17X?d=wNQp-M!Cd@C-JeE|Quxr4mK5 z$F$dFbTw$1Xiyz66m-tg>z^ZoMpqUYlv^aWxK|=KE453@bBp#SS-IDgrDM;&SFvz$ z+;zM%N`K08W-+Sw^k^M!?^ z*0cIDAe7aoKFqw`p0Me7!X|ZXUYYq73}*w?iCIM$QwzSCTEj-@R*5>mGURx4I5H;0 zFwg(LeD#O3`2QO>vj4pEdWQq=aNr#dyu*Q~!-3B&-gEyVq(#J(MrtasLM#tzj6L)S zUmYD<-Abbykc@k@m}4Yr!u4LXoBKX!V+3S@kfQZPNP%n0_2&yj5^RqB32g~YMpYSE zIS^VWmbf7Wx>@(}Hmu@D!7{B6K?|Lgqw>Zi zDx$I;99!iTyf>I*s1h1r6nw#RcWb=-SWiIJco}x9j*`61{ahorPTA4Al&jYG9P*#1~C1a zTn6*pz5;7&M>4pvEY348gGC735UKMZx9%-L08eJ`dw~CRa{G|#+1p;jYd3XfJ_wM8o)1^TLrgI4wa4B-EE_B+bfb;wvQ(8u&=tYt`L~ zuP5{A6lyp@!=3Q15Q9VApFsgJOpO%f_!<#~DmR$I9W5o=LqkMsidWlm+slW3eEfV*tuk2lZ{qo*(uRphU>Gex{+1D@pdhL}fwk;MB_agh^`KsZy53KPEh*6Pf^)2w< zUl_lDg}nv-H{av`_7V8I4McqHo7VURqP~iRPMsei_S!cV#xD@+wFUk+6vi(QnzjZ0 z*RSyll(X0||G%y<{%-mIzQXvs<^Ox{;cs6iW5@je+QRs|<^OvM(_+ZIV_-9rDod~f{Q7s1?Z{rlH$6~-@6xc>agWUswc7{5TToh|TR zT;mrgc(`N!-z<#3TmHXL7=O3?zp=(IQ0;EV{J&lpf4BU9zA*l7`G0MVU!aWMj`@GJ zF#c}&f2A<~Zux(CjbGrIJLdm$h4FXG|4VE90zF-J+l?Z>|9`6I|L(8=88}YEY>pLZ(V@!?Ea1|)qLMbS4<7ao`4pFowt|g04+*5HQzq&B>~K^Pb|&l!8!QLZK2`AnTee7N*s;_qfOG|M7JJ2F;zC8k#!01^(|VjK5p{e{79k z;F&w-KUx@nxBN%e_yyL~{%-MWYkUJzWPs?8ERYR)rV>jHuU0#P=3XjdUe=+_#U$FW zP!S!m`-55sW(?G0?3F%-eZq1Ximf{c%62(+IJ8Yfj9rfom%Mm8u&^Xp@D9V7lX1ga z)=w9ic&%C(ziO;Pq5uo3jb-h>Sej+UCsB&(tVY=K`Xj6akb zTDO`No6(&bKR!Y-ITi}=1_(##vprzv3HKrXo<%ZGJ zRvZqf$q``(kfy?>jkWE=YF!_RJLW%J7=O3?hYI8GmjB=yUu`V93kqb0U4UwNo{f>c zK3NY~*dl7sYDgm~uht<`!lveq`41Gv-!1=3h4FXG|GR7a0?*tr{|^h}@0S1Hw#F}@ zQ}=g^f3U_!6|SE+t4Y9!qFf#bO|Z%u5aKRTu-`@+(yYNDM+-yfd(w{YKfbj%{`O%9 zTikzqOJV%o-oJdbFn)o%sx8LNG+ z#@{Xf?-a)0E&p$?@e72T?3n)_ER4TX{;%WzuYCR93*qzMpZ({Z*E<||hXe0$VDEF& z_uUW0D`1U5KsTZLkUE*MHWClG>PZiJV5SZACm>$q|@!_I_^fZ#z%o1ahe!^9T z{c5K^wqk#zuY@VKO_}|BL(f9-K5M-<7rDIFTY;szBaz$C+2Sjy_4Z`M;;XLKTUKwn ze0IlaQI7ixq_;ssFOSqX5v5K&Z8YlsqNel?Ce_ZOJPCouh@mdtSH0@h6rX4`czUs1 zsd6c+4n!yptimkXlmKEm1rW*@hAx-&bW56SN!Q9S?eSS5QGc?cIH}~;ToY_*u~-_cB`FbV>gKc2a1cOIBoB+*iA}Q zfz$lB4(mWxiTg7zkq!$(r}tlBkq@Bi7(VS=NaG zjQVjqOEV#11LEs=z0C~qS<^XySDe#b%m?IXfttjS8brDU5imNZt$KI7RVm>IL%%n% zM{5Nh#YUV@>c=w)PN(bXo!rg;a`!=Yz&bl&{kfyDo#qQ5XTKTvvF*p%KRQKGVyhKi%|E$ugHzjCRv zck_>C{r~=K_QN}`^EvQ2=Ns3DBlFUJfs4}}S!x?A?iaW}$yZPj19ScP0%yR@hpz$` zoSU|!wcgJNe>gk$$=I0pcbr~UFZF|+Oe;OP`lK*WuEJ-g1On-JIa*BkDYv2>vO0?5 zVc?Vj(Q_d!JCU^$#m zMZa2s`&C}hFb`|0!kop`Fc~TD%42M$a?iboitt!#z5BHG&e&p(sDB)C!u!0ARV|~2O)pvHxH`) zK^;q=N^?X^W59#UDS_}abvy=J7!X5%qbr)5OsNB)q`S+GrUT{-QrqshMR$EYZY}Zc z925D_Ga3Bc^y}B#U6z(xU~TP41~(L;zq9|$%U}@#b3e=58op0{(|g34-q4o&v9{dI zL=LQ;c|M|88RTlqD1~@wHjHH!6#Zc(gnN!I^i@(>X|rJFQ}V*TxG&I(XEuvhDuX^J zCVdWyO10GYEeA9PU?X-023PN;`e0#n4tzck{ZOk>2hAFV5G8y%0JMS7cUYrF5*%8` zB0}wUhV3oSNBPh*k^92n>vECHTW%IuN;?v{jRo-wxFmODF%~sBPl?dmwcLC%mg74; zou2WYPMPPuf}Yv`Z^2JJwjA*RHw6cLJ;H@ludNiW+#R7nRVk~cQ^hHo*Z_oSxj$i-o z<-JSrw}5+lH?Cj0dX>8V;y(0nq0BN=u2ha4Ka;F0g)y{mKkP|a|NX^FlOO!Y-~68Z zy?X7tkB%^*B^(`Pp1_BC*R4PJ0QI*|q2Abs9bvuI@MpF{{ST*5ukAw* z7i``N_1YTsYx4KIhg+fk&MDNZ`_RMni?>3(x<-9h{{C(Atx*5tDby?bZ=H_w=Jm=6 z>OpHO)c*Y1-%lZ4azGo}ce}4+~`abkMw?cjX1hv%P3iY>6q2Anor0>R7s5jTBYso49>#b1#uT!Wm>_czS0_Ux! z7fw*&m){Qc(;s?w24|PVW2N-*>sN}DB6*9O`utm}e4&5wmtW7{|JyTaj)M=67eN5v z)31I_P9=Oo<>ewpP>!j5*dRKe&RjIU_?PeI@4xgtTl%>_{mQ#?IzMtk=W|7Zlr8uVY5h*{nPxxBO|NOV+bpG_$ZcFDYFXVJSBIO0jM;z06?+)yIPOhc= zr+zG_@TdO8wiLeq#%U=pP&4D0z+)*tmxVcHe@B{%-y>IU)_bxT}e)8&jF8!y=|NJuc z+@E~8bL;1Cz5Au`#lQXHM{mB7o&4b&fB5=uT^FDK>E|zBlP>*%y}!TB$$w{``Shz_ zdulme+b9*9q|9z*IW&tPfqf7H#k7`cYfA{(I`#;^5z7Ksv&drCk`jsO0Psap4 zq}9(Qa;+-=)_3Jp{;l8MmdaPZ{?yvMT*PI6Oywi0@?1LCs`5AYayozWA8kwLD_@t> z`G~44;9@_f^H^1$OW|5o{>BG$3V-7-Zb{+QU)=k`&Q!)%@Ssn>^4?RNdcH`c>Z6=G zS8~^u&aeKjIj4T@+qdLYHn>0g6%6pPfA!M`AI&-Vh#sUy=(3wIk&S!d(C_*7Xw2JV z3o!@bm;?cl8fQIAO#_xm@tA`f^q?0Q9u+A@Pg`W&Pz8<;AM*R=u9CGL{B0`d=D%NT z$<0rH=p#9`kLVczc4j!`y9=JLgH6DN61Wc_0szY~!3{i1KC(HO8kqv59aH;|o;jDw zwVwHR|8q{|Z~er!RKEJ*oXST`vK|EvC2vXz4O-^w9X#k`SQNKyj%hEephXdK9X&+j zwvgWE=_7jPTsqf!=HLAOoX&r{wF~2wS5HaLbNfyJa*V^flRiuMc1#|k)J%1T{lxDb znJG@z;)>aU)CLUR=jO4VIhVq_p7ZS?sAjX)oIAQ9J2_Hqhn7U72 zFddoa6Sy|R{^>F&@SkmMb06H!34BET6euutOyCCfb8a=Mwfgxd@6DGXsa&g{|NTGYRQ}_weHO32l2iGJ`YBMf>6pq#)X%weuGP;!{Dqv( zfAr_JU7W9cAgA*YGp9gtrDHme)z7&UuGP;!ILs;h2d`~Q;rrizTKyEL=yXirvHCfe zz?`!G?b7=F6I;{w%{d1jQ9q$-m~f0t(T2XNF9%4UA4f-(L}Y?6tXC;IIutOMuTrFY zUxBaH&$(veTK#0fV6-#4A2?^=;4>|+MzVZG;v%dEA}|MZ`7`u^@OZcE?6 zH|F#`qJ0X4t{&63LHnG`&9(OV+drOD`**l4sm=5MuUz`~doTXw8-M58QTD?-uipb4 z_`=`=_ruEyDDm@m>uqxI;Eg$W1ytZVysUsu-<*(8^Z^CJ%g)ux`3Q>V>ttVgIOFM^ z@Umws5nMQQTmgq< zC+o^YGC<25M4aEyyS0T?=F3!#6QxoO=5Q)-7h#MFkyA25XgaPO9E@gIEGRJ-byi}~ z8=4JrG@UVNYuOyl7x;F`zWLDCq2uqrA39z@;C7^FHuf_tqG}hBxkJO{U-F`18aw2s>?E`jMQ`<%VOX8A7$)|*6W!6+#l%&DF4!~#$5y(r z(!f-w-W!M5aEnHUFpv*D6S>b?-<*qFUPhw8Qr(TnK^qzwZV;jk84vdkG*3q?)`FPW zVKfT-TrkQx8Eb-qF#q?Krt~9;f#CVlkfU65nOCa_de$=c7G~B*eSio&Fw9mE%XY&i zQm>_w)Tf#=B~4kF0@YYN=$9|N%v}qs^O7>N^>Umd!*Z=Vt*aqaOVLRPRVHnoX!zZ- zj71`)cHOF+Z65*5(FYZPuOsYIFw@~kX^koggChg8XVlTgwiy!n(DR*z{HFEJ@=aO4 z&h1a9=O6jDF=3&Ny#fm)YUES@!fGim;Xuhi82RQ8PfyUgYS z4i^=p3WA=QXq0vK18vs;*cm2C^w8~6l}hN7=)kX6sz9yBd4@tKVC>LYGF?8((J(8b zFc0l0(;1(<;{{oah7&~b$MSoL^{;FTs#LNlWg{z3j|I7A8FfwcAiDxV`MDn~X#}4T zSV?VG4`%Io*$fRN?s5Q!H^vwUS#=wg=e(&edb}}QHoIxF1kncN^+XSuPc$IAJZaBr z)h&06eCWsV|5u)q_FnwK8#k{0sqBY$Uhi<=E9JoF$UFCSbAgMC-IyR**7%0vaRs~_ zYu#MHa$0}Bz~N@IJEFh^%C@@siJU1On{EbyGKgeqb)zLu*WoGO0M9q256TQ zwRl<6;gVW!54~|#vAyO}edpqN*IALARhOBsC@1vh!>9(d1{~_MAr>M*yh@XHAFUDt zs1q!iNu>6gJ$kCm64V~EpPo>ZZ`ZkS_$ArXVG1}3+fu7V? zqGfu3z_x5d;R2ayN=pZtm;}*nFD_n|4zHw41;%6^Vo6c&xXiR^cyce~rZ6wjJ!@H> zcV89l-81YWi6fhK!tCJ z4VGmU9>uwZ23E3?VPyaY@}XzK_W4U6%!Ms4G*Vy@?MT=*7Ce3Cg{{CX^(J9^>lN42 zk-QCbWMUXOMwqRchxfPKv2>1yD<8o{4^6SNe4AVCMlAd z4ONrWWotPYIvsk@3+-?fDGXj|rKT$|Ss0RUuIg!X%VIzI(DMy%`pSC4%LW-FW@R-t|9r9eDoNpa0%#|KZvn zy7tklfAy+*r8|AX@o3vo=SEOVt47zyM>yO4%4Huv`Tpe>jU+~H?H`e31(?U7} zT0AP>j@pV$`=hBl8Y$v5T?nmHfuOH_&l-Oc3jE3*R<+fT)aYTIg2v;hhWj=}fIP66 zEbxR%=e1#N9PGCT13W&&z(q~wB?F;mi=>@GjsTOfy#kYt4QkafQwDiExJ{3iPFrdw zaigmQjm|JxoW}n|VSFoWVQ3e%4P?dW^Q;e*OLHdgHGC@#k!WOVeuZu@rm$QLx4{4X zh4CHS*DYpRogAihFj@#p!*_a=1%$z*$F~wEMWUM2!?G@li!Ja!zQ%W|f)zFnZBkQX ztyLGNJ-8njdJU3jFz9E8E4$mQ@>9s7zRiyL&lJYrE&u7__&eo)eT`orlW52MrwZfmmjC1$zd(l4 z{%-M!HGYBXpB?wV#tY*Y@Nt~)nt2T?j9 zm%ib~PhI_vOTY2RA^x3zd=+uvbJGvscb*p5P0!zzf2CnjkM=xy;?7eLs=#?2=#3ZjxH%4jAa**kO5w$>C7R^nROfj?6MqHDv9Yu zT(u@O=ctdXtDa}f)Gi^lC#&9~Pt15mgT1Lrb^z9$mi*XSklR&hUJpHUo_=BQYA$kl zmF5CVX-6WrF$VF8i%?M+Jazx@a6;(m8;B<(MDL{0JnLyRAcz9MS&C*ptR3`By;?({ zc2*5-HJ(DNWy*$@jVuCl7BYPhZ!1ZEU|;-}#+ebQ(h5e2u`~>a9%vIW0V+n^MQr(? zzm&q60ha_YDlMiXKE)TN4OfOyv)`e~lpnH8d6ix8c+j{R4qX-*tn7&Pw!0Wz4}E>(JCvEO zCj{d$gh>j3Fax~g`r4f1XJB@D=SD4g*tdn&0S2Q(ozVL2Mx)&qQ;Zl8rENtn8+s;k zpS3=ei(H;YQ(&p?NaQv&9@-#88%)y8X*5qpEWY~EXu#|i96oDjDNzi|X#-Dc4UIt1 zR9Y-+?XuEtY6i~0CbF_oSh0cz9$viBh*oDuDk`-HDJbd^im?{5^6XaKB`ty>EYb;U zQ){%KeWcE7n7{Jc?kpbiWK@P0L31&8I)f3RbtYA~#k9LsW}*e*c1?%ZL(i7^3)g;W z@0<3%Z|~~QU;5FPKXvPOuHL=!;p_EV^UFW+(!ag(GcWlse);*oe*HII9PEAHbARH7 zaIO0McV4=6^B>>*p35J2;qN{7zH7hqf}GV2sP5vJ{ORYe*|vE7x&1@hhTs7Zhtz5z z*P>xKo-gECkOuwo;0QojVqB}^9U5K%=6*lu4kpZi_tEJg)$H4?+R|lYk_JGgVfZ80 zSKw5qaK6F>x5bo~hQnpp^Ce!wT9G|Fbvt$CcTQ+%3gc$29%hA}CfV-2pvQ5~(Ai16 zn8DR5A*n|_1yC0ZftUB&F^MZF(g}MNp~iA3?r8l!q-pFNu5$y&A5>=J+PGe^XVGm( zVN$HOXbk2nKb-YO>HIVezw&^FgH%%*Gi7ADM0i1xV}!OoCgI6~o}ftqTY7&wqWv`{^$bhBC=Ba25gNeFIFlA**<(SV+j`C4dI_%PTpp;2^*@5w{PP)EhB;(tOPP;0liUDB+lIo8M;}joW_{<3nEOe-u z0$LHe@?mv6_S)5@1QN61L~tTc8K{_iN zES}naKk!WnI-D(~;cbVG+7m_X2TMKciRDF0?KBNPaY93Df<_F8&ZjG40+U34L?1%b z0%W~}toD=vmNnkcPy&b@YTxTdqazFD*%luWE z-=6e5g4ac2G9c$-#4VT8Q#4%rt`iz6m2z!ZF;{6PTPp1r$c{!UCDN@OLH%-h0fDzorZmS(k=j=eo9ol9Z z4c9om9$xr+Cp17{9kf!f>K|ptd(>`sxiKs;mff&)vS&kw5)V~|_~Bd>_uG|hzi#q| zUN3Vatwj%Kz7tF2+?vNir>6GjwChERZJ<-?c5m#><<-)*iGDNg*A=CEiVrU(Cp1(w zmmSrzz!V`i`h3j*NP_J6Er$0UP8`k-DVkXL5Ovtq_KoV;GJ6%=bRe`EWNGgzx?1uB zj30_ez9j6)HUO4k~!Ug<5vc`358Kl%H8m@oO2@T-LRz0^il#NN*k)3v% zsQ7wsLYha1!f+lm%o=S@1`xu-wmvape##yH}8o+8Rd%}}gZP=p2;gsd-AO~04 zpxkh!+fCJ31T#TR?4?bT-Im@vMZ?X@4`?`Wyd}j<4kTacazFZRc#40vMIfRmx^DcYb{UCpaTYWhyH0+Aq~MsUf+%7(FO-fpnjjeB!7 z?vNpu^^sgGPRAVF_>KoO9I_rTCOXg(-JbyU*MDAp?!jBnp>87T(vWu_C6W z1crHwShmI#+Nd|HYswbb>9>>U_K$%OQr#VRuT@9jPT| zv2~MhaogxNghrQE+jPM8yJ4RaPOZrov=bU2JRqi7^Y~$PAy)$5fY=J$^z5a&)LC%S zn3b3F_E2fnrrN#@3=}!=d`HD?i*Z+@)XXw2&2se!XY>^x4_lp~%vb7_8GGAz`Vwoh zeP^zYJF|gJp58BB?3~bm1Bnhxo;3*@6RdIc|Fiez@s6AIx%k=lb0+&9k~2AEJ2B(k zauO0G+p;WqUnJWD7)jP<%d2GB7MVK?W#0;vHqZhe=n4f|XiH0PfeVza3pbQ*ESIft zfnJtCTUxkKxUY^(&N5Dp-Xw%y`DOme$(eb-&!bn*_th(Xmlq!e-QdyL%NEr!&Iw6| zO~&&y6Cr72rzVt&W~bM!WSdF93!^z7jr%C0s^na_Ar+WJp)U0kQW5V++to}r-!MvY zlc|sxPF97ZIft7U>~lzPOvtUda)Gc?jFAS@DrHoKtI;e@)HKO7IhPMj^;)%ndq++q zmK^y((Uf_8{_s#_e^Ju(xGQi34LsUjIIt0g2A*0$+%y(jCPD0t867SRe`}ZPO;L1gJ(am(gzS1K$3qeLMwL;sEs zsaL!8um|l7`HYdI9FwCb2YH)wRW8Kz2i$H1G#vDjt6_G)zG zUd&y;&mn`Pa#*uG00*a6Dn8DOmE+-L<*4R^FrxILmrM!w#4 zja|4mBC#k>D`I3qwN=)fCX>GstfzvXxW>d~q756K1cjBpBlehxw1~b4a(t z$y~C;S964!m4j%v){XMTZYYpX#hPQ$MT~H!j?_zJY=`ChSjA9lBLbD!kJ-fndu7NgnxaA&L}LS7UbC&r@|F&MtNu28Qu_QaY9pG%E=BuX^E-T~uR9lAbP(J48+LQ$sbC&!h1~C+HrHs_t!?sieqJ%uuj&v`$6_)X^=> z>}U5m_?fnu^c$`y8sMck($-@N3idq)t#`9nLF#gqy3Y^;CAGAJB|t(vc<(iJtgZS( z0b*SB6jWUF(p5t)v?7(#(Bo^d)nO^OP4KC>(Bi>9xt;9xGbEWmG7Yo$?Q?KT2-&Q$ zLzm%GgWYDzY&UCto|d_++zKH@vYseam?+oBayxFJ87CPrh*1>c&$-LBbdAxx{XjTa z59%%{sevwtovsIXaA!M{YASSbFv!FN1xL$z>FDdl?5+E9a3{S2US2J<z-!Hmu12uxsiyf7)|xSW|7Z#Q&F=XOzjAym+Et>=nM4YJnnANSfxprjDHxw zzaMn=p~wB*V{yxND?Qdx>%s)BVms~wf4mA?V zc%$#$=`@SpVAtoD3;BG%UoZK(qj*j17Lo-sl}VvVZ-W(bfo3aNl(sbkl&Y1PDxv3c zrkdho*`xFDl?OPSqE3x>h1@AlAksoJU1IWqk;kpI%t4D#+k|ybD&%)qn0@I8Z4U_a!1xiT>kw9uddLvwqEQk^J_Mn%DrOIYqNDJjU*%}PxBOF$@ zAK>5;vV`d?>&-MihR~TD+nNc|Yo49=^HHVeF>^k(S^I;t8|UmdagL9dU$-P_bN1 zDJX6Zd}7kmnwRBwx`A}YU5!VBa$HJNqZT=&$E4A0hd3>qaFxwo){FL|ZX%IzZ|6aZ zyx43NVjLq$u|~U-I5H0lzqHT6sB>n(FnggPc*pZ~ST@L4SwU25y?U@hj6p&`HsL~* zd^EMw#PLBSSZPPZL6!`*$|Mr!8?8X9!WCVZSg0^sKLQ?ngNTW2^BpZtQ8HmrEInvq z5rsd(VdfY(^B&>HZu+sSbwmc)!PG13p11|$Q~_EsUz^%!if@9a(g_f zoGX!NB_c?=6VKkxCgWaZ;t{ZKbm4w%9c4m8PKOGI#{dHO=Pu2#4k5K8FFC z#cO(w5=^>6RD^UkzX(TE%PNe#{ zY$_QObG7m+&Ur{7%e8&{c3P;g5uuXC(PE^h_SN{&N9Xbz_BjCkCc**GXS>RdDg;sK z8bzM0q`d9)fHY0s6UvtGz9*3r!1@pvwj;*KH$ap?LTu>S6y1~RrBa|>>T21Blt=xc zx}y8jmCkkyNK36_-(Rjsu{88_G@ z@AOrq?W!B(05hp*wG=G}+p$tcOR#!N7b9s;Je(Mfe3>9uM7Ozgp!KoGFAlJ-UWRik_?w%MA>pUy*u-&%kqJBpCqr0>dgQm5K!m zm9~-fhD&3vm5EUSOf`=lPajWQxfD`>UL_7ja(l%a8 zNv6^0Gy+B=Q%&uJv-M6W+6=XmNG}!w&FXo-Nsu&aq{ESZHGn0MULY7yqyi^xqZq+i zUj~T?+0;@RThfor!^WHUIh55(Ys|7pt6X7n{(>lG%Wd#BBTGtmJkW4HK(yVl6wP=N zJ9UFV26Reyr?N_UK&Qd;C0g!Q$0{1gxFg9B*$!cDt?wht=ynUsb>nP8Ve7F0g_Xt3 z(W=AcXYF%P*hrEH=9?nlN{C&=S0X7%L_%DzHjwMSLSY!fLcU(dKSFm(e6-AudS+d8 zMLn3@q=!r-CQ%*{cVTD~6s4v+y_Q~X7pueVjEK(VM%cy4T4<6#PcpK5$+RO%aIi%x2zk?} zE^2aFWb&15@S>5TD$z2TR|`}lmu8NxQA?eD4i#PXP;Mll1u{}BWsc31nT?DaWw}vp zH~A2fBm)D?qm(&qC*G{3eKZPA7x5>;HL#MVJ#}$V*G8=tD6?-#ls;;hB~LRO=eM&F zsnn`whD=M3Y6HAiKKkapaNxOF6-#2g5gED#Hahb7z?WDlPWH!0xRQ@_8|}K7D+M*A ztQ7ej9ZTa#BrYQfvo7&tJw6(kd0hk_(3NtkP#RKZFIw>VeVoc~54cvbp2JbOFISuK zQZjn4~X(;>V-WD4GBBUELgeKeltH1N8I;%SWS#|LzEz(l0sdG}&=349fM z$)oX}^0oM`2`TkNroEj zcsM5snrGZqvp#y@E>}~XKGJF`<8lZeYSly-Po#9Pj}qdsn2wiAvcTEYF^!! ziuXVqD%FXHi#iW>KW<8DwuC}W>4Hu++r!eJQ#T~7g_1prEMbw{5e{?jJHVmn^MLSu zp;sOF`*N_<4sx=h2721K(QcLNrhub`WS95Er5%t6;48HIE(xq;e2}eZ0fynKY^st@ z=G)zLszmjx;L~&m#D=zoK9;RZdP!3uJ(rnJRDco>;hRx&zg@dW3FzLDHR!;(`n@$TS9Yfg( zVGi5G!qiW3+ZL5gVcRhkI{^qLWr@8WU_5bCxc>NJnaRxVwW@+?N!Gp&K-|44T;~|# z{GL28U75ru0K8M1!V?_hJx2Xal@u?)$t8`4;<(Ma}G@VN`L?~d8K27DVl^h*G0Sn z-~mlu;TZ3LCd<^YFpPN!sQ}u7sH%WQi(C}%Bz*Nq$ZLWR=i_7<_wpsQ%gdJo9H7a| z9pfC*Bt#SAbE}sDD4@y997Dlr65@DD?ox}&rf{iaDicjYoOpU}^%DDfFWEl!RL6-Z zAM$!bM;^v7hfK;B+jse5$5alUehYJWZu=sD1pMS8$4FEBq%V}hh7zO^6XfCGkrBq0 zI#nKTV_9Drt93?WT=LYzJ-_DX#R~x*@RJK2;~nr5%@@rzJ*=y)gz2)HNs(QmjJW7( zx9lG_c|075jf{9Np${Sz`2u?eFK~==$WJiS>=M2t+szs2I4KDEOrLGO#JgOV5Pn7&Ek;t^${S4zx_JUe7*6UZ$1R@hG z=3(l|lvfDXXcghMEGnDAmSZXtKT!%5U9VKrY(E+>%Ok3p3+hz7JSuxyK7W<%Btfqx zcaQQ$DsFkxp4rXqTvbl@)7@c)tHwzXS4WR#mQCE`kT!9;RRk$ZNLc2TN)3EqHfh`~ z;&EfdHt2dVj=TDeQYjxOjB@P_fCRL;;TUO(HVv#e=&4A-rBox}>nQkAT=k}li9pyR z%CSN=Qq3TvNH0f>x+QTP-~nx}JH|VpO(Vg%I~gUDH^5hmahAZU#0n-TkWTbDk(bo=7R7v+WTExc-BW&ZW^!MP956=v_BR)J z(RQ_L3<N_lk-69zd01KZkNsj=%^K=ZkwF1MJcP%^L`bV;LwrB3 zHGO(MO<-;=YEjt~qK>IdBt=!-d`H&D?R==N`ZYD8cg3pg8ZgCTHIx~233tDgEB8jh zI?{^(46w#Vj?YGV$Tc<_X3F4Dn6ccC*Zt;jU}V~nhVIX06rE}}MB1D1VVoHWf(T9< z1CU^i#T-LUSz{q`vP2Z%fi)I&jCWv-g~;cUp8{~eviB6nIEO^4217=n$+*V}wHKk9 z$+AwD+C>UWjRO^yiu=?aTJBf%V$}yWsdob?uNzUXtQ*0)SPjcbfodZU z0y^Zd;ZZqp#FUF_w;dWbxC{as?)RE8?kIDie`{7_`sFpbCg0`mk=SQmjN% z;t*3Zt#~Mv3>DaPH&~OaMHKYXd@{fQih1((U_^~ueWEIq^OZ<-C^Sn)s82*XMKMC# zSB!+IPBhT*5E+)q$J9irF8Xq4S^_Q1{b`*TR;fXi%<8l}#b+n94*kr7YqfCHQ2Ss0GU9e2PaS z{$c?c#*wU9Z4fbR+z2tfTD(IJSp;B!@1)4_&1WZyvA>f_Y%NwUCdMRffLLSIbh&-W zx*jRA$&gWuWN{26d{#25E97y zA5Z}hijO8jB`-l3u_lcc{kDcygZ(cv>-*0vQgyve0W+szq=e%6et<5Tsi>gzhv!y3 z01CLZ$1xO~TSK&j=G+#QO~LJ$%EYZ9T1mH00Sq9|Q^)r-e((nYM4M^xNdOPX^GS~J z4#*RtowDo#I6yHj$2f-+1JR85-0E%CZ8|O7<{0Xs6yrJX`xZnKUvAf;vMKC3rt*`m zgY)Lw+^qoP?#1z&N#;y2UZZ0hR3Ap4vS3sW$`LzRlFlj%^=O z#sk);x0pS1lXVA9r*3j=9Zn4U@KZa&(l=W0o2eTe!%qb8fZgexWZO>!cEK|7M8}cX zDNiwoF0dBt&)Mr1t+IZY z2s})L{P^iP`vjW+MB^No@VNs5*V+Uij>3Zpt{)$TJ$sE!0H(<(L_iuHsZ9XF=nh2S-vNP3YyuEQcOdcs91ytJCIDe{2OpLK@WfOofx&smIcR*m%CIDe{2O`ApfWXFN2At;&>_Ei-9S~Tz2|$GUcOb&_$H$M) zuGs`2jP5|h?;Q|Woe*%I6tDvk)^|W)#U=n@bO$1q?|{IvO#s5^PSJUw-vNQ8$qYCT zsiUX=&CHy=+rBV}I5|ul`}pwn*}H545W(>sh#dHUS8ukM5DkXK%9!G{aTdKL|$C35-pT zu_%&nbJ;`*GzaeXnpwoHl{%n{2s;>N(H)4u_VE$dv%59{2%|d?5pM?sZnX(O7~O#g zggYQ`%Y?ws>R8^hH`@dtLh6t1xwz6zi{M<`mHmS|Ljg?yJtdANe?1V6oqllc@zGuB z#v?$}4eb*NP7Rhj0D9t4ps9xTfu_gJ9RTed0h(^;0MOJ3xdWgZjsi_Jv=1~rWPUu* z*^{;!dwL)q=nq??0-B_QX*O3V4*f;YBix`yp;2`t<3g2=gO0-8;SNL$-9cz}+a>^E zbO$21?tsAcHUS8uFp>1*3E1`j&zU*#YbUn0eq$@J`PZALH@>>@?Dg-izi54X?d@x^ z)i10TR=&1!c4ce%H{S2+q&v@C zeSF|_BF(*gXNU-X=Sl!!Dbsl)fho#_h}cV4068kVe8?+)B(?s^ z#gjYjyzjuFf@+9Rf9E0qWx0#tWLa<`5TyH332kU}By z06H50%2KHF?gCR33K930)&ZcUQ0Gks4k#2NX+T+^MF(3l{{H(tkD~&2MeKV)M3*k8Iq& ze&4#d_T9CYug$N%diBAgFWs>CuEos4=N88E-j z=PWw&4>R(E6#}U7iDXj8N6se>cvLbF#r+CCkxbGt+9An6WG$Rqi%ld$ImUV@$vmR+ zem**p4C$E5L^2Sm5B&%LvDVVW@$u(}TuUJ`BRUwswANC>G3J!D6e1HsLII$)mc|_e z9#~5uG9Hwt03d61!yN-1k}5=Q!@0G)0hYD8#T;Wjq*NbCkw1Tz#RXh<)G?QdR3Q>1 zI!^{5mTV%&hsh^Wogy2EmgV2h`%b|5H$=(<@3y#r>vrCuV&dNr zHUFJc0K^ih|M-yogFhA^(io&C0YFQnKF5FuL<*6vptt~#B^$3}phL2O$aXllcAIsp zfNVUDu^vh`kErgS-?g}a>vlWl@{`?!^TL1rRsaGD|7V_bd}#Z@Wphks!|7W92&nv@ zaXAJ#AeDXNQCv+ExShvm;8BDna$ET}V& zZnuER!{gx6dv4@~S|6@a(_KtP21 zq4Ee?R|0^$0RTjJ-!Z`X%fE01fI9`?AcFjk;r17Q>2d&LDH0;U?-=H=A}<3-mLegd z_l}X^6?v(}23$8pnBOrQTao9sF4?={D`7hI|8(*Pl(wPhf^_lZUPFZV9rHODp>16R zfGk&m2<&5Q5TX5tl{?Vd06>;9Awv3&fu<;P9l%-2gb3_AhC86lwaN5B1oa)m z99HHkK(dqx5wUlS1h32$iw&6GN43&_vdOWP*S81aOu_Ap-i2;r1okSO73#00R-pcMNmn!>Ke6u&g}|L@3`e)?s&;14x!TK*Z@C zBf-1Fti=XgH$*_+F&o<*?Dzk-%`ARuv9Y+a@LLO)&A;u$Yfjv{^|xDDa2CL;Hv=1= z0A~SwdR<#zSbO8zMXPUKee%jbtVqk>U4GTFcj>;Rr+_a2AD?^r>OelBJ}x?!>oLR7ps{(g9P9XvT%2O-K?93#Q6gWHpJ5TfqIF&ldw ze8%+v;)!6Xg=muL_=Cn?aJY3HK(Mv}5Opq&5vFVdo&eyiZ2&~Ii(|O`ZNRyW+pT*8 zo|q8*4IGE=A4)0SqiF;dDK>1e0apzX?RU&(q7(!`SW1EDr{FkZyr+~}2q0Krz91SV zI7YCN2)aQ4VC{h+0{e~urlt_I0szqxKSX5TG2#^Q`vIIKeu((KW4L|sH+%rb+5^dT|K+yty!V%WDnHCFG~IvH(Cr1Uf=YfzGDWHRpN<2SJo;45yp4C z2|Zr`!VZA51OO4tcMP{L2J8KQVa_+RanEXCX&3zP_@BpT;Gt*W`GZ@RQ@PVGyL|PL z`}i|Y>Yb+0t||4Cw_ooM-FE#+P45eAtJdk)g}&03tY7=iJo6+w(B+z72yI;hzdC*T z>3k_0K%1$*eaJ1OrZXqA@&P#j1FVCiCuQHxUc;s|m_z;=X1oSU~B9d8)@t zWNF+vdFE+P14O!g!FX&D^>J6QxE>11gX=ii2S;|=Q*`q9MavqX32iWamaV5)XRf-u zZco_bc6)0c#1nG6UFSVGtwDUJt%Lamqn>%vUUW5C=~_mt<-+LRXi;|9ipDd$X(QtZrF4ZO!-I|J0ho*fI@4druUT z+1VFGVCqB1qSomv61W@ya`;T&{lBYeLf;VjwLPirKgQf49~dsPnk-lvJgBZx(APIe zaH?W9%t`{ot3%M%y&VY_xoilF@47|3%5{c%)}2F6S~H<3Qk&HW{fVI?L@e7v-5Usa z!nd=vOdai5Y)3hD*WFDA&1Qc@iKi;%M43TPr8}~+nu5Kg3!z9W&ygu#y{Bo}5w=T5(&G%8i%KWYSbPR;i;-YCA#gQGm~!c7&OC4V z7R&EnbjJa|pYD6di`0?dd(ZcKODIPi)B4pB&#@oa)(w5^J!cOQs7F&edm{5%H1-H} zpd8Y=EkAJ>5BT{Qhp|wmd&2Zvw6At}Pw)r4egHSs6Z-W*2ywG?n`a7-mB~mabKWVk zEg}c~S?#cDOk>z$W!R@nX)@VA(nQW0@JOG82i-6T?49q15BGWEcZV&%p6W$Kx)37@ z^;2v%k94VVXS^ItQ6CQfy73J*V#&Kpi*j~%92fOMpxnrsNZH?#Gzm%S z3NCda4ZoG11dHwb4p|y{$)mDGF+FM;&dE<(A`7f65TKddVcJZr=&sw~2;q42l z`47xj=Kf{w7v{ES-#hD@`O6u8%B$mJ^d;l53y!YX!BOyKwc{ZPIoW7&iAY<}qponQ zSC%EtXcucd=&T#k1AN|FrhJ~t@vGLMw?2pOKkVay(MDaZs1+(6Ir8wIOy~J4jqun6 zMjyM%fEBwk0DPDXwA}(-kIGWMUJkmELOR#e)k4P;VaxT5oF)4$JRV2KnP|yzq!;Sz z>*G}cB;RUM>IiQ-R^!iGRltf}6)^L&uL@uvgNlF^yCPubXI~NUcr8F|WW;+3eGsWQ zPFCNKgGi5AEx?Lh3jm56oFBjE_=4b;>&NQ==8@&eMPt<+hVxA|*zTr=&4uE~U2Egdhibrys z$vtI{`*;mNd{i$vp20&I3@dgGz|7CS27rBR^8a^c-ad2U6(?@rdiPdsYhm+^n|E#e z;Rd<>rFDJ%!nNnEUAy{@RdVGsE6U2s@^3Fkmfp3LS^V1Kc=57@7cZQg|Fikz+^6S; zv)`Egh1tu2p`Y>}ftaL4uxT$5lxl6HpP^hCi5?81Y$cg%R28(S8)_w^N2@tL9NpOoX&rHpKViTGOZJ(*)*rjPy`jkMo)uvQdi(xMqmHgN~Wl;lFMKKv+qx z5N!q>x1yV# zEO|bvXD~YWuI4@3$dJ9uVgjxiqSv6~Yo4T&JsCh)Vu9!`=r~h-idgOhaF$pgdJH;- zJ0O-j0E{ITi2j0(VGfJMt~&s?DDN(ELNVgiitQ@pV+w{ZBpb0JRVykLt}cJ=<245jMPYK?@i3j4{{HlM&4E(W%2+ttGO`WMkCG~;wd5Y|cburc z?_ocmH3utp&B4sizUF{^yxt&!JMW^mFU-fT-eAS9HvsztQN(l?>KTUUWQj#$y`hmE zd*!q*stcya$P1oIuu1qmNulF7!~da{{`1l%*~e=QIy|e&9ZkO-LEP@iUTa?zKh;`; zd+bt!gM|!Jvp$bcM|7}9Jn!EBe3Thj@&Ea`Yi8EIxb%GRpU3|^J_8TTzzbxTmGJwE zXAUI%KHNelN2!g$^K-HeCi+h=Qv&%v2UD@tM1qiuN^ZB2ZN`vvTd$@Ey5RrVkNTqVI_fCtS=KL`-arFJWUR?H_0@~Bj=);X-A z@ug9SQaQGlE5t%+*4xL!ZqXD(@-g zPJ!=4YB!|@#nEn&pCBGd&piKq)5^*^n5=2a5v{_rLxIN0WHLR(%p97VwoO_DylYhOVOLd)dLI25zwgtL4$+i-W7@Phoks5RWW1`+Liu%@XqfzEwBT z9dN1w)4Rq~H$W$}d&z-a2q(oK2B`L13q)!RwjTN>|X z3e9puO76zlkygSaBgKULszgxT(!|NN~i$2tDgw=SUCgG&wkuI7m2AN>0JvvK_as0*|! z{~V_0f+Ye}U0SQd-q2cI5ZQrhRam;g@Aj4e+ur2;b5d9tu>N-Pt3y}4XKw%RrtG?o z{EH)zMgV-!wq2yT_e+bT&F2V7d*RwAzBis@t+N5PsH@5RAzTnZAYCgkJhE1IkUF<-I>(PM`q@}IrnD^ zUtIY8g)=AaJMsDxtrK^w_*Q0DelYjKIcD+Av+tk%#rf+_tZx0|)^9D}w{-W4u%&IG zTPK$Oe(5(hKfC#s&GBY@^U{qkEyh;n3{&aiM1~4?f;=@& zdG&Ob3*$mJ=?X{tV#_ZSDpwlz2r^J_hgd!k^K%Hrw-HlxYplP5;(Zz`dI^mxj1p)e zWQ-CQuR>!C?Gc$)SR*{)LD!75e9;J}#9iLl(34P&Xt2nLsHY#Ugd=WUj%~ip9^u70 zkuKRorQnbbblR*p5%8r{VN@Pf6P^~;^N$8uiOGbzmCHW?J>qZd5plum6Ww}0W)A6) zCiRHeYJDgvvxxsLdMxN5@6gL~>@HQ26|8>6t0Es#xLiKq)y zy&5eFURNpU4M>=4kjq^qLXQykM(oP!C`Pn)OKqk=(mBpQ9(Y3<@PoRUM2Qm0yFECb zZlr|x@S@A0M_f8#<@|p9tvv!_cCWNY6tu<|O&7HkM^RplC6l7tY$G|&8>+V5SdwWp zI*owQ$W&8TH0==(x_S%rh_~7!%4(%GW?7_Ft}r=&K@_v)w$X^llF}UyG@K6*ZFek1 zGoHi=7xakR_D1ZED-6|R6rM|q;;E!yxQ%+87SsaRjMmjauGftOBe6hv6h|(43iOD( zp-0?hUkAkfQ0);2Gp57M9-13e(;9e>mU`~0hPcLDzmE>a)gGlrT&_ea>XI%gLSwY- z5nksWB~37Rv?~pxKGiLy36ZTd7{ryvJEM*sE>f)6A)7+J6Y>XK?khiQkAPT3NA`$7 zz#o!8rqwv#PiB+ea5-8YhJ5WpIMc`Yv3lDpAjZ$qYV%^b5G~dn;$+k@9q@F7lj||)1n5$5^paVUkZI4K(?q($KZK4f>O%kK1oTBQ% za>VOS^o2|@)bBQ;StdDFy!-_N=n*>f2+bY=F;^@02#EV(`=HCC2xYYql4!Q&muWeh zjRe}&Vk^qmTxn_0q4-do@9O;!&0osd7!V^^XjV_MM__KF>MN=BR-bC2R1F^m-C%L( zWsB+<=Y%A~CgXXUiI6mM<>U@RjF@;7#C7n{tR_lKa7@UpxpIN9QjC!X(<)_Dg{#pl zPSiBXG&z?KP4!x}fcvWJYz&AIPk(#6(wMQ|KuKi9)YB@b~3lsU75GMGf?{ zaiiTT*G&OO3&}3;iA&3qH4$ROWM7giaOI-A=w>=#*W?kKg&-rB+mfz?J#2o-2qEe*!(?kL?lZb~u?!miTIpFtc(H?bfDg zM}&H%u_x9<_*`n_BT=FO)?B1-@J%8>L?hl#hpBqRB$}!rGw>sx1C8+u_J{ypHsY*b z1S`I)EVN6VoIuu^CKfGHaiJ$HGJ<5F;ikff#Y2&C1WT&7|LOMbQ8+ z#gVoiQ_yI%V969F;G%V^WTFW@gLA5{@otomJqU8V}~xorv|&tl-X|9 z`aCUjS-BNLiex=esxVQmkLBk2?s;Jr=2(VR*a0iB1D4ljzB)tBymMyu+v|U{{_OSa z?0c7gwEWl0zq%|h2WM}d`S&$?ZD;jgR^PY!vel=q-n#O=rB5uqVNsuZ*WB~wic4#= z)wz@N>G^Bs{?FnC3!htf>w>vJfD;71JpTvt&zgDXiT`=xwaW{$FIoKL;%}d5oIp-2 zZT;OMw$xs_bL-c)x?6W`ZESve^G%z^W_0tSjW2Bc-pZ?1xE1%tJsaf46)PWJ|BB55 zSorGf`(|G=+n=Q-&#Jwdce}@GD>tC1aLv?NI#lMG*;s+kh45c;2NY!hJ$(`Lg45tJKwrh{T{Lo@I{^ z#%#P?sdJ^Ii;NE$PkJPgaot}G>3D<~1#~T;QaO%Eb@|ICo3{a)#cO(w5=^>6RD^U< zcJYIhLaR)cieZ?CEc-O@;=tBQCwlf}`6X?X&l|ypjv2Lv;0g4G3X^W=y=F8<4E(64 zk>(}QjG5BKziPAM15#_yOVDu|uYrp|)RAg-ftkI*Wpl+cE0E=;O* zPc!AqGU1WjNJLePrg|u<^7(EmYSx3Pg~{tF#E9RuF?xxn*!4CMCX&QSsV)|1If=Tx z9X8#l%OzJ&lS7R}GT!LBPrTb6VUQ}5Y(^B&>HZu+sSbwmc)!PG13p11|$Q~^Z zO1LXqe-1t31NI1rxw2JqiaIsk1?kzGK%|9cy2RuIBad5anS&Ogwh5HxOO=9!> z9f-69PY_R-owii&vg(K=ku=dK#=dz#_KFoLF)WJMVInFi|5bO}1vFJ@9*vRvq}B{T0ClrAgVtRO}( zHY*4pd@J;o|2Fi9-?CXj%vAw;MBZlQK?+8@7okd_go29wK_2Jx4a|=l9XylJYYH18 zKzFQhksDq$`Me47G?=W3LY6RnWxbik2Qe>R*BYX`km&~bdJTNNa0Mx{j8&OdF4r4Y2U-sI2|g9`nk3tF zHDXuTtEd}dguRNwjCjAzs-&eFqDbl^)O4Y8g7L<)ZlhAyM@E)s3*Hir);fbk6>Y|@ zxYoXMwFbghb)rxlX9}TCkFMd7q9^Oaas$KYSEQcdGw>W4Nd{80FNPlRB720wMv_D@ z-xT>)LhK^G5=luS65@Kbfm{b+o?!?J`Fb7y2)$$xdc*?sh`-O{t6N1Owj zvyUFwp#7Kq(Y|sBkJ_+DuwIwjHE7iPEJ~A7Elz|)#qFZa8Y`n>P>_SPg7%|1B~n?K z>_s8=a5wE?xamR1@2w_kNy=~3NPnw4$_7X#?e2Q>14EC9!5%S;woSa1Sp9-MBG=a> z;LDA1G@vmlRgF_4Pq@Lkxnj4{bcHHK1@NqA^@Mx*1<)hx*K9WghW(oDPL(s+Ze=(q zF`2TQR7Sx-CXEe)On@uaI-w%(>u}>lxI6Y~EA|I!m=Si+4rYWMN`x6`?4^pQ6# zkQ5s0dLkYq6bUC~Qt%QTlp=&mF4rOwS&y${i2UZ{${|MBp9^3x?9T--Bka!wZrrSi zNCmA?nHG|8Db=uqMub5&AZg(^WgRTCZ0AT-XI<93^J8^RGPJSmTrg<_RbP!1Y)xaHU>sI7{^ydYl$7(?Oi?!KD_mC7Pd18U74M^+G&n&UhUSKMqruXHAD509xQ1noG5+HOi zn(~OlZXw<7Rl_`4?SY%FrBmx`tO&iiGAJ^h1@DZ#Xo%OyN|9l!keY6$>+EhHJAh9(#PvqLYQjA3i@J^(4&UNYfV%n3c-PA3c)IX_r|Z&EtG* zmJA#Fzr^7d58LJ3ZmV1QRF`XciM~AO_~cx^JvPy#!ubmdFULD%HeXEO^?~W#-R((r z)_nMRjl(D%HJd>+jiZAeTMxSvAqvrvxR?rg6Iv>^+YI>eXkj-_xKmxDTQG;Qamv_j zhV^cOkyGRJsd7(8lB$R22ZfGLje9&nr!EIB9k|?cjZ>D(z3}WTjMg6|mqX5XxhXl+ zQ`}JX|mx?J3RI`6kTT``LxLCtU?8Z1d&dvc}W)eNUmC^8dxbpDy3{YRU&iP&{0=? z>(YVnU$Fb6y;7?A0|gJEfF@*g?8yG>Q7U){oiFyEa#_&fN~iydwOUhFgM)MSpcRG( zD=M@|;#65KAY8_ms}2ODl4sJqI0%m28St5>IG`h8JsQC}v^O=qYIEA58jnex<{7Yz z{G=dY`k@-r%cG{t*jkhx?s5?dJ@|4FQJ7OrccO`MS&6MCab?unjqS$kF~k_QL@L+{ zWKBxY(-9UV+yf2|=O~--uD3gmv87a>rO-w}!TY4r^ihpqrqC#wW1dKIthcU*y9GSp z%Y_ZI*ifrlyUvuyTCk~7d|ay(V~p4fnUUNnM6Bv#(&2JpUHX|b*8BgHW=1n7-gF|n z^}Ve(Y!RE^-hB1uQ#QV~@$wC1{R`_aSa+{|a_w1bH?Mwtb+mf@%12kaD_1PP599}2 z06GCaV`*ja?Th8bnT0nkWaqy(|Au*D?%Q*(o_osdzsu%P#xmR*rnlHJ`hswEeNg@sIy`rT?VYldi+I zrhAWV>+L7+e)=WvR=@di@vHA z{^N!Ji+F8Fq@TEFw;1zCCAAw%Q*kQZ+h_jZZ$G!Aedp^;bTxP82cOA=cIcfwyAUT$*mj@y(!>)k`qFFu zVD5EakNn_oZhiU{SHD;Pv37mrTc3MsT=>h+)AS8CUl*6HDb-Xv4yrDr!;xHM!#bCY zhlrHHhZ+qZ+rQ#zt(*5uRf!8Hv9?ZS3X8xyJr{T2nl;0E`H6w zjhV>v7M@mn+nvPU-TKGHH~rm>_v&BE-1w)@z2X^OBtT!YXE!%U#6v@~7N==noXg~7 zj?vt4DpF5HN`aoLhuxup94hzuYQnaQ{nOI@8$Z6XG`#W;U;f;8zV+kWv-S7A>ML(< z{YUy+&wuCFd#|Lg-m}{;v%F77x?!qn;v*5^l1;4^Fmgnv-^YWoxaLZujbtzuZB%T# zZ!=!|xzFG7Z@;Zwe)$j8KfUe8zxMGbyy)wfzj5ggzx0%sy!Sac@(pT=;t!XteQ^p2lSkPoY#ilAIiMh*0Eh2Qhm9mc)(5&DJ z;#Fm`KK$su&xgD1oBr{w&wuu>dS3Du%dz`^c+;Cc_1ypHec{A2zx7;khrVLZZkZYu zhA|Hz72L6mstRbd$VKr^!dH)kye7z&94E`TmoK5+iN8Jdtrxv76+h=%xV`f98UJTC zsw+V#Yf`ahMhcjZ5sJ|ef|w|+B~g6L*6Fqk z)`oGpt(WZiJ%_!T`u#6te)s|6+J6|kfAFt={XY5w@4NiQ*U_Wlh3~%i^?y%awr5ui z<$VMjRtdqZHht|#y+OnhxQv7o1u`K?0*~bMh8pjWl9PEj`$xlTe_y!v8=v~6@dpgg z2d^*R^KZ|2;mMUJXdiUFEcccV{t110LUA)DsZ$vk|QJN@2kzVZhB0_OQ2 z^F93q&CU%U`RwVtKk%wI{rWvWUV3S}L|?pTHxtogm1w2xHj%pOrJ8EEhM6^2y%3-Z zohZ?$4F_m6%=_@7X3sCO8+}Ib^1r?N+IzX*y!Z3(zwgJdqh9&SH+a7Enihto zN%y@?o2F$cX_M}2(xeM83@V^(iYN+#f}am6n+OOZqO#+HBBCNF2!e`=`T=B7wqKH_ z%$<8X++@6<{=VEl?rWBu_j8`*JnK2{W3Qk=1BoW9t+qW2(B?YCLCScz{&z=z=ZH_g zWD@=!ym9oZ4a{BHkvAV5|0nQ05^g?z+s$VaJIc76!5S(8l#b4u#c#gqfMcr)&6FbPb zXs1(i8oX33B={X}h6xAa(N;)|HnVvdBhY5yXbfp1dbVy>`fYsUANOM)ziRqNdwk~e z*5B`P-}pl(piln!glm6i`_>P?J%8;J#G0T@h~^}XrFpZzxbz>)1go6F~3d+qw2 zuKDG_$Jgz11t0(EgOB~~bRtR2$+&qu-e|-Veu}li9kS&qhKvo5gKUvN*#IS|syApb z>gyg5i!1tm>|ot3XZZ?W`^S$?bzeAl{RZZRWc(XXU;LHB-~8uKzj*89`NXV@YbVMP zF;vlH7IS9ClcJwmhp_82c8AcS3IuI**mZ0n8V$pEORbKnD`mwo8x zKl|>(fBE5e_WShVr@gtyqm66Vm}{r+|`{gw4mMqo_6Rh$DbIny|-7Q_M%( zBX(Wf%h`*TZol2Ra7*o!yG|Ihg5TI>#5k7zwkvcYnY{13M&!PKeCYYzjw2>zTn*U` zy10+Oc-oJC_@|Fw2r+y9=56Nq??w#s;q*1YYwMnP#kc$94^Jf~WZVc7wN^|%jJDat zc_*Ta3$b9(N}2-!q#YG0LIU9m0)obb49X1@TXI&OsLK$aw zo_F{o^j@ZW-iF>__WoJts6X9u!DaUxK6Bq5)-$dd8rbo2Vob(0rkhlg209Hfq&*~X z&SE=Jv6CM#J9V8lg#mva^l=$w-gzB; z<%qkl4PNr+(j%{&bL(EYOTRd8zS77X*1VTD>dw3IRboWObvj|l9F8KoMA}31ddkyb zTu3^EBU#p63e~~7v0`Zu$O+kc}x8IPaaGR%ed}J*3tk9R)Y`?mS8co8t!;eowHJ|u7nFxGJ!7c z!L5OBkoP?K@|w?Fa?Z7v{c!y0-@UYR_;ZaP^Z)bKr|0j#@8`sjjEmB+-Ih%j5x+O! z?gR}9f5>bVGLQ>p0;zfeu=%Z&-5Bjy9Lo3|ebTnmnrEN?>5mUFyh{J<=($&4+kf{9 zDqrGv_{rnDUu76LnHZFDQ$;)+Wo_Xgh!hxWrV#LX+Yv{#!?f#o6Bg5iQ@M;OR5id! z1-D|oFnr^V$5x(cA0r2`De`1DYbi!RLvXj7z4QmPkhkA8uI#FX+_@? z9~=lA^^@mM_3IN~_~c6`o%U$%_$NO1*1mt;{V!{|4?Vc<9)gf@Avlcrv3SfMvIJ9N zT!D`EyR8Y-ix+dC*Y7l;MQ=8S=k!^T!%Vi%XFi2O2k!~4x4(xq53L($D3<<Y+*NCu;>%~(hCIlFQ zR?r&?dG6?Vkx^#;p0Mc>yDmx`40%Dk#O_>J~#bK}wHS|+Ma+cU3x`JQK) zYp!|x$$dOC=>3medI%AcaWODkNd@X9CgkzZE+1w^Gmf0SkgWmqo-eabLEp4KQ_Kw>xMUopp4rnnqe`2X||O! zxkUyij4ok5QzY%A+S#VPBXU1@V~!%pN8L?D-&+Ii)LAFrzu#o+x*;KW-V4Zhaf5z$ z=*eH+a@K{XeCm*KA|T`Xa;(Se%4cGD-ox1S64d2FpcI_Frb2sg9+n3J|#ZDMu##-0hLcHPF{qpH2UmPXeGOmV%BVF7F zh%3Kw-fgEo7WmEy2b{Ir+O;>@PrcyzH{Sa7b;b2h{>-=cK7>og)sQ}U%j9EXO_;5fBX&MlyNoW4C&$?`}5Ci+ygx8#>=1C zc*Grly!(PDcQ|t1{dwf)zdZl0s{Z!p&4fe7)sPvai~IWRpZZ~F{R{WbJis2eFZBC0 zKlmGK`N!V+TfcbbB|m@hPQ%-TUB=ar0;G$3)&Jfw^y1Yg9&y++PwxGzeFy%2(o+); zzEHCtedpYzdmsDL;kO8zjH@B%M;G@qU;Ufg_Vw?5tT1u)^hrODTn?ULID*Vw`HhRO z*!}v?;n~v&Yqx#%CbWho(NGpn6vF0E(%mcorBtRBBO|3+1!)#i0aFKe){Dx$*s+1Z z|K0B{bS{4We!({`JMb68Z|-9rx%iUT?>7GUCFtyHECeRwYRK2o)wl8S<`X9$u4_?~s_ABCtO#Sqs>*rs-j{m{!W4f0%{Pw2F z@BBJV9NTSQ^~pB6xcojlm!G}+@q7P3U;hjDo3Ees!p`%5|843Azi=FTMd7#EUlfUB zy6q4&K`mF>OFEMxmogzv43DXc;$4C2eHH)pip?JB21XI=#V5H?RohMT7_z zw4-8}bv&SVRAb8d>5LohJrM*CI{wAdZrtm>9|}LYDEFPec@MmA{K9)q{>#{vWn!a@ z+d^xQ(;PM+P&6POdjX^1%!*tuyayJeL#-|(3Ob#Ftw>lR%6f6{UakFp*8JO!=4~l5)b3Np&s+`serX)jq1o~ zIaqF?kU3^5r_f@zeeb*K3)xs`!|Mm_w^!xKtIqr8%^&_0e9VcikAH55*2~p{OM4PW z%D66f!s7*52q`4YG*L~77sd@+BT%x%DsIsSI_cyxP%#<_*W(KA{ujF!3a|aucp9Hr z`{J3pogTSr$NcxrXP>_B1Fv0Zefa8+5oQ@TV)LPPcb#PFm3ShVEEafcVjHOy}xfX5W7Hf~Ouk_x0V!ANlO>n`)gAH3(XpI+gA z>h|m3uKZvh0+DgE1;JqT7HeTP-zn0lJ}3A<-Uqtf*-SK#IKnJUL^?hzlV_E2_MxL* zo%!aW_SRh9Pex9k15WMBQPaASh8uJN&(U$$`6JP2XRF;Pa(@_WEw&!hNTG_iy;hZiThN zZBJkF>pdPNU>O&V3PpbgN5%IChlPxzeo(wvD!TP4e+g>1i-`uG3z)(gM<}U`v)_51 zh5vrWhN-bZVGv2b`X{tedaU*JOZL3=IsXg0-;cdTKr(L8oXA1EQRLJ_DZiMh+!U^) zg08439<9}jL6e?~R()WF;+sL`9C^_%sNeqe!_ejgvOo61p1<07=0?h0_{7`LjqaJp zg|FWEe}qBCEwg@ahshe_ove6cleOExVv$483S+|nufADsqJpi>BUz-VyeD0Hv*$<8 z9ho)_pPU=rIlunquVq`u-Ll|4_|CQW{@|}q{nbr?GH%Z3r&*7V5wA)c@qj&4ix^T> zDuIAe2T)D%9UFuDj9yF-Q&)7y+1dYd;&nHCe)i12ocwg~s-u5(<(E!*%yP?>-@19n zXU}`?Yd1`sNB~`2w348mHDg)VwgY0Ya*L|uA~um_s9KJA146EC;*e;>A|@DD#=}SU ze5}#weD3)(fAQe#eOEv2bu_;Ev5OA>^V2{4;ZY&|K__2F=w)0DZq;3VkAC&8Z#)Ix zbBiZ*!^pMM5B)Ib`}I*LJ#_rTrw~VeViTP&oQ zWcr5b#PsOY*QSo28k@XPd|_lT#HdJ4qQ&LUafAr!TUgl{Y9(g{hfV@I zLJLrTPX))}5|B<4P+vC%$Gik|xE7%PJ_?S*B%pasKz$t)9EVCkhiL)o@11~e5P_IO z*OjVN$;QQ~NW94sQNP>EE^%_x;+lXTB7tsD0jj|-0Y6v*Jwyel2B!r4APMwf6`&eC z67cmB=s_w#HMk?-2TGvpRe(m+`y${6NT3I<^HScl&0+V}3UnzhPSOr+Y42-rM8MZc zpa-Y`)!>DI?=OL_Qvs^M1p(hr0^MH)s0RN7d|wH4KNX-FoDcAQB+z|TfNJnO!1tCw z_fY|=!R-KFD}nC4u1Z^kYSWu@y3^&N$OB?8E*(-Dd=BuvB+#`gKs7iV;Co7-d#M1` z;BA2KA%X6x0#t*m0lvEgx`zr-4SojrZW8G3DnK( z3gAN$=&%Y<4UPi%paeRk0#t*SfOP&pV&L}GKmVtP<_5nxNDrSy%C#V3` z;QoLoB+%nkfV$QDKHzZ)G@$}igX05EN}%y|g?NX}(*=>|qTCFkeAAm+x@Kwcdcb26 zD5(NegUbU>NT4wlpc?!g@Tdezr~uXA?0`ok(5MPf4W14-E`df=fNF4az{3(Kt^!np zj{_c(K*K6PH8?onK?yXZ0#t){10Ik-gDOBZxHjN^2{fPrRD)jw?vp_MDnKG2jjf)Tshgg9`(0mp~mVKsER;;5G@=t^!np^8#*_Ky4~OgX%pOa7+TV zssPpCwt%A&D7KDodD>;xnPA9p97{BrVsz+25$xY zSPAqv6`-_wR|WhS3G`SMpi%XH3i#0y=rQZKV2p57^0h)f9}CsmO@Cl%M`>_Uz&A>u zN2>tU;GuvYC4p{K1FGIV0Y6d#JxT?r2HynSEP)=W0#t)z0&bE(%_=}OcqQP71Zq+N zs=*}zH%g$03Q!IH2skW(8dZR5a7Mr(2^3zLPG!9YPXyc`fkG-kHMk+*pag1A0jj|V z0S6>dPz|Vh2LxO%fdVQ(HFzIL=l@-Y?i?7pbNH3vlgBR`9~wJ(^uf`?N6uM8&fPRO zGE2|gJ9EHvXX>G;Lnlv}_~nFY@cV;n`^WTaJLZqysqOm@zW$&Embbte`y9GvV30X5 zG%zqZF*rJU{KS}+7k?$os>Fn{@(cCYVEOzTU;DHC`)?{!z@D-5yr`e=kS+QRZSf<4 z`}#;II(1pfR`!$_lJt4wF6sB1Yt?GC<6&zCCKatFrB>UbRDXX9MX4@7)yhgq+Whk} z`S(j|b=nV*dUC6l(lC*2-D)c`W=XAf)6Bl|?~Ymx_tA^3rfAr0H4Xk)Teskf3QC$y zULpT}L#=*0E=Xf-D_a0yM6ZYR2B1H|ilUS}{#T^_k;eZ`<4*avSFKL_9UqD%&Mt^X|lY z4Mz?|PbuN`${WuKsnv7?@^7lYexQEIq3VqNH%j^)x~1*L*SFN4UKIVPQl8=%Ze~OVjnYE@x&<`@ z>Om$+)vc^s!&y*Kbt_9>u}S5|+!vGd+z(Z&=YEK!=T_^ohU2B8r?Q(_QA2rM-kX;+ z-1{eLHQdLzbY0ePRPFUS%TNs!laZv^o-a$k_sXi)50+YRt94n!u~$)0UZYpsHpr&E z+q|UTZuFomRfBOG)USmnutc$ zYdD%K+Af-iR@73q?i~)5wA|rQwOSqoN?LAZ-8uDVeMQTytb0X0W$T`OM$&Wcag};X z=l_wRUk%KhKRG#ixA=z-em-b{|9`Z=nKUdjKp#K2Uc+**@?yUg={gKZ%%5TeK2=Uu zS(;($qnbyd zaIBHwT97&(WV!018J#_7kj&`3T?e)7eOJkh!2e1!(y;69XGY5p4AO9a|1${n(L-F6 z#PC^`p|@JfU{SsIa4FNnh*Uz~rOfMZEkqbQ3zilP?)hesu@-`4jAY^>(HyJ|kUm z6R~KH%$RC=l||4tiJf2QnbgIkWl{~p_Wr+jH2SmRS7RpM4A2ffE8r-!8dX*Xsri~ zVV_m7Eo7Ni2qy&>Pjyh*jaKw!5FxRwvFH-hD{QC9h1@V<6q9_@X*56rsl3_8yFdc> z(;lY9SL+Bb&W7G}D!^skh_#&y*;_d;;A%v}mH-C&=IMoepHe^+Vt_$V^cRyoiH^%dDG;HlM@p+PnCsz9 zsnMB{Z;YfyW`}PZt_~kO^u*AohCG9>3|=|t9e7oQ{}+C`LH`RYTe+5X8<&FqnIlwy zYM6*{i2#7nsR31QwYWqEz#OguRKvCymxuwFc@>}<9yq%s0h(g_`#%77NdgXC`Qd8I zYnZX`JDo!!3Gi*u0@VLm%^?v3_zuwo)c0-QA&~+24%PzH|2@7-0H7vTUkg=%OaP!J zR$mKMflL5!;L7e5;&YPf{lq0M$ZOAQJ$niPhJ_Dj*X8sEO6r z!YUvW0H}%8*TO0w69A}*)z`u*AQJ%Wt%3kR3#))k0I*gCs1{ZMnE*gdtiEOf05!4t znh5~Z#OiA%08kUFubBWqO{~6V0)SoL3jzQ&vHF?`0Mx|lYbF3t6RWS806{f@>T4zdn0qe>0Mx|lYbF3t6|1j4k3g3IKuxT^pp^hXO{~6V0su9!`kDy< zCf*AI05!4tnh5~Z#OiA%08kUFubBWqO{~6V0su9!`kDy<)Wqs*AOHvtj!cSNQXN0$ zY9#!rS~20_`Aj&sEa3hBq4@t3*P&Jm?#2Ci2{DOd8qx6(%aLdWg@eARN1yVFNtERH{}WVzI>LO1WeW*Q8SuEhNh_I(C#xl| zE^8BlY_$4wd2gXyyWSb@YB{)fj za_wkS%oNm(|4*m@btXIl0aIwH&3RcM&co$Y7_jFlfi3tQK&tJi+DOP9ixFu{H~v4a z0u-WZ9!!@hW)Zy2)op~uMH!-aHIMlrJkz$KNXTsu>D*{asCMK3Nfo35T8J9t`2Uy+ zP%T6aa{ND`0#pl8gB<@KRROAns6meZkEj6ELewC~|Emer*Fw}F$N#Gd)z?DQAjki! z3Dws^)F8+Is|nTDLewC~|Emer*Fw}F$N#Gd)z?DQAjki!3Dws^)F8+Is|nTDLewC~ z|Emer*Fw}F$N#Gd)z?DQAjki^RK)*lA!?B0|D7s8wGcJP@&67LpjwC;L#s3sHj{|BtDN|JOp)AjkisDnPXmHOTS*78Rgch#KVh|Kn7E zY9VTnb-S~faWxrP~ z!Aafte@F$Wh8Uu5{J%j3sD_ZAZu~!}0#rkAPB;D^PywnTlBOH~uU7%8!QEN*{~tV5 z^#7k&KXRG)hYx=Gx4>C7bI&(P!;oFMv$k@x-Ih1Gsh@9>hVdvl_h@l~m%rCAH2b^R zYOq3WzDe(oH~dPzN$;^cAY?L1PS<^Xlf3Z;0GX_1ej)7TY_UMe>|D^t1hC=AVJW8I zGq;!ml?CYlw*4--F36s#Gg$#;a*>+3%3`eBU~}E4whid}gZbpNE$HZlt zVyeF|Y!`j)Z77{h1Y733(cH?!1J)`LEmcW4mo?!k-2tVlOD?&iCfOx-pFu4#oU3GV z$bY5DX_*sF6i3(6;1jA+t>n{GDJe%jFZBt0f9$SXwCE3bU@~d(2mHyTZfSJ)`{^SH zf!lV=P3u9DzylCgB+?Z#TeGC+VQ;>b4mC<%2H+YMKhTMk$Qm3qZMTnLltX<$&6J}$ zF(}Uv{>*#_$cO7GFXJ-kV{nnQmw8JhVsudTg15!gtxS&QjBWiw$89bm&L9&-yluDN z7XmE7TG?DFMNI`XtI9`Es=8>0AI%_=9ezAHxL(8fS|vL)_On9`8=Tcy%CxS9OYCZK z9Ou8ssCbTHCI8P;RL6#izM^`)>_{q!CTS)^@g*rvOfSwGj3CC0b@7x?Rf)<${7+c= z-#*c+lIy8{(f{>zy5H)HB|-R0uqlCRP*SWLEbG|SXNi*CAhRus%etkgN)M`-PZxWC z7JdDxv{XC!qkKik%fV_gcrWC}AXe=?^#3;)qKoefHTFTfh zX)KQyLLL(vj8nxhR_7P|;p%ozG<7@S^i@#aRh`e#CV;nCn&Cz#C>3CgJx}E7vs#OA;<@3QFYG9Qq^Vue`9Zt zt-;_{X`+Gu)rm%0Pvzn9j&U5XZPVRnLGIEf{ClR1ZrNNeUlYyZ0BJWY=|ShYT8U~a z=6-;*VbFnkz%1G>m;_9KNH-+@UHYo-KjB#@cpRX#WWI<7R6fg-P+!S0$xove0Uf6z`Jzta5Fas;>C|?>De{V9n+=19O|_24*+U4$N$x8JONYJutO- zYG88nj}DA%9vK+kJUlS8d1zp8^WcCezTH2YHjOY$XuXCT z;Po2rU{}7;-E^8Km#%zKy6F_nb8GVbNQ>={(@Y-2Fz7%e_nI>tK0q!xqJ;ay?%Ae=j8G=P*AU5 z)vehkR&)6p4u30e4L8*_m%plK;7v8Td<~C4tK9xo&E>Cd`&TrVzq;*T?v}5f8gP}{ zzofbR)ouTx=JHp!{R?vW8h8&^x&8B+%U|90&uK1yb=yBHm#^VTYn9tSqq+RmZGT2{ z`K#Ohv|PT1r?ORU|Fq`vSGWCBn#*6^_NV0XHDIoC`=6}2{8erL?)(2+12d;i{Asi^ z_||qG3-2^MqWQ;8}s2C3?zy`8=0D0!^|M0-;|j#kOu z^!*+e|KsgV!>PQVy(!rf{ynQ*uW*rY_T3zbPF(ybDf!#2u;!JjqIXoT$~BHOne_5A z=k~rm8WBX|@a$`mS)oSoZa!FZSq*8G%(YV0C5s$9QnpBua#O=dTOEsp&Ht4aso|cc zpGEfZx)Ddu`@a?KqldUJ?L)cQpXGMd*rQnb=MHCv!&3JYbY z$Ck0s^-PmVAnEo3O1cFmZQE`eq?=r#SgPlvzG}kkNrwq=-kHL)kxInp36^}#n%A45 z>|wBMiU@%=3p;6|qR+RY)2 zFgr4N;phhO4+J zmib|6QLv96);qBP+|u35ig}nNft85A`#Lo(BL0@WuaYcZl3Xm`f1Xr>-mq=gL^5XL zX%idOHxi_qbh}Wh>T^N{wviL}`iTU>wMu|Nh~}Yxfn|Sz2_~|7s=?KfpimA#VrE^T zQV1>-GK{-{Rp!e@%GOS0J$kn_wII?H7t2;5+lgl*NTJiFkxlhA39GtmL8h<^V1JM*Y`bEi+Z0wYR?y^!qbq z{6F9WZ5EyHw{-?W+O~WQsnxtxM_;rMZd)@H&U&awr%bazvy6H2m6i}HxGRP2k2A#l z@vJEurV9)e^QX|5fX6#U%EB+;nBC^D0Vr8Fo6YgWyaRPuIlZ-AC^SQ;o9AO>ZQjN& zZ~~77(1gcLG~LFKIp7E)WtFKvq^gT1cV_z-$>h!vU%@mC)m1XNJ{fc*8(6Y|?PC`G z6GgpGW^qJ@5mQZd6FOz6632)t%hw41UD=~d$hKqqu&BR^byzFdjw2q1ucoqwRJnk9 z9kx=ObTWC!l`-23DYvVm-|ppx5#}4Inz3O@ETjTd*hfc;P#CO*^U-3*%$ABaqqnxu z#M=pzzvIrvO0GgUkfxxxq3mpPZYF9YVx81{M2I`$PS_Re7#Is+QFkjYRsEgw|2M_= z|H)5}9WeOK|NO)62O7#P@X?H|cOcOaJh5^>#MXBJQkc&2>~=`s=L`*T8SywjR-TMBuq=_oECl~i@A2l@ytKGm;yAXx_RK~Sc60rEZuWf}~i zahn-pY_PADW>6c)3Mr?+Ry;Yu{9 z2pM!H0NRWR2-HYeqRm(>vtY7QIX|4wTUi5aLgPTdlQ0DRv6$UohKq4?KAoc5o(0D0 zwS;pOSBi;X!JI{W#yw3m?03%)BUfEMkXU;M5)B5oIunht?<b6qAZ5tst8AwOThe7J48-gLYJ=>7d|&|xv9aw> z>1k`P|JzLKDxF}*Gj8(dlo%awH zZy=h;Q`Ho|{qO%-EJQPqIn-eogsmceQ<`+S+5vs15J{04yc~)}YbKkAqK&y+&c*=s z2GqndP%&O&7R2OdSSFLQbBJ@HT{kp1PnAbPnUq!C$|6-=9Oh>aqS7#zQf_L9e_f?> zcVB)V4Ojc}7FNS`f&9IO8@>KpUJcKE{Z_6%@9yG&U;O_6{@fq*(WC$S|9i`^Wzd@p z+w_VHGy%vTH(HVT65FrIN*Lx7);3_9cgIU$DkYE( zW2Fw7YyjkTR8;Qnq^gTHI5qI_z%c_?4Xk-=%~#iy*6cI)^4$05&YXkiW@n$Cy?oX; z^XAay16K{jhb9N(gSx@{2G1CHV&)e!*Usc-_MCok`hn?>OvBT2Q_oCYF%=sA!_be0 zcNqTYuxDzo$wwzIn>>2r$%)e^+!M3ozZk!0oE#q=`_fo`?7-3IN3R~OjP5Y<$jD_Q z@sWMEI?-cW{jK|#`BB5d?9e&@Ty&lio%&!{A)49pIy-gl(0v0lJ&3@l;gAzWIm0&O zx|^x~JQJdi(jqCz*=y>eo*vu6TcpP)4-w0jbl%pt-Rrxl3o-FK+q-IIn>L3DLGzt@J_!cf}qJh8tg zq&`PSL&u73Zb6-?Oq4D>O1vIqX@4-H>F`8?X^bSUT4d#CRVbCO6f=>USCqc zGCQ2i^!h@j=<>dp%=VO43DH%0GTrNkSSCf}Er}W>dT^Dk6cOpK^x}RP_|A!9Lme;n zgG%9d>xX3%$iTOI?Wq!d%jg>TZm&I6iY{-@fjfKcsS={wo&$H-Mni`ls=i4S$J?zP zWe2|B>y?0wu8Z#5cA94EDNiuqUAb2#f7P=NmBR1VD@F)jQsU%eVX@Q2IoP5N{++1{ zTsA*VKGo}Km5^I{dh&^0PpcGN-qVx6>GiZq2*q+9?e&q*TlUuT9n@Yd)#{#nD!8P@ z`tZAry#|ej4wR-CmBLaK#MtY-j#Vl2ZXF8&i&qe7njd>}RpFP;s$*}gD*V#z#@MU9 zF{H9_wv3^%fAq$XO3~$GXzZW8F{BcrJBG$y@s9%1LbkXs7H`$wtp|bNb4Ep?pyky6KPeX7g_KUV6vc(PPoN5ir@o~P7t1-N(W z2rS#7PM)jOaa$0J>i99Gj@$ZFspC1lJ@4XSr@E2|v}9JC`pCbAxA>$W5B?AL_RuTn z_fD%pi{}uzB2ypgomsYpv8CCj&gkv8xAmHNu%UG;z`aw;W#3I6 zub9BH|5?8Ojw>dxtxpvbNKB0%Jpc||H!yn9sBd(3=(j^(82RZ4H?qs{qr(>spD?`V zn)}vd){M{nVD3|M;ki9#zcqXMta;`iGk4FNK4YE!-Sk=0uA$tZfAGSg$$`HOJ~&ny zxNdxCY;@x@}0T-Kz)Wg&J z_B2!p-$%oJlto{yhFga9^xn$$O|9tSgU|HZRc)EYqfTy_y_DUATHU(Cb9&F-KBiD$ z{Y|nQj9@J9Y*Frd|NdIuphJ_reT+(oZplL7;ndZM2hJ-eSYhOm8Qz5@YdD7#izsYgG!rn~?y} z(p7S3dR5_gRs$i>3bw%#0|Quy6E9$B^roVNDn0G05{9DuE=R!hgrw6izis?=)Bc$u~j z3=BT79qZT!BL7(}&>NQ-o84ta8@XJu$<#CAwAz1>>5j{hhc$|IJugOn*0a!bF_FoI zvTVAY_vLIBi@Semi!F3`W6u^;3cs6$g8HQ!@!_MK;zbxRv<}q<4=%kK89uIOR4ReC z7}fC6y*-pl(d7&8@G%;jILk9!BT1#x495*0+v`@9<#$KJ@R7YT9U>`s*&Qa_j#9!N zN+p<;yLISmy>3-0{BGR}EW36dx@A@27oWYPz1`3az0Ovt{g%!ix~bRMDn*xf_R!6} z{!$6i?XRJ)Dr-HeR&49cI72roYrR_e{%if$kCNVu23O9v-RH(Naf0+zpC@va(j%OD$>m*o0xVWc146e)NyPvyDtFt9=hX>Bu@JSw@79*^l9NAZ zo;Y$<;g=fVgt52fRax~doi$;S`2Y4BNQwOab8pXWntgNj)S1_3I@AA{ZcV*9B}~3N z*_e1~f*pThyf*f?vC8OQMoS}q9w`j}aX2^hhoQ{i?*{3CXGHOL`Pp>FJ~G9jh6K;+ zH6$)xIc4vrGxpZ3fQBr;Y7|(jSpf|Rk<}=$m#lz>bl9ui1$$~%V3oUI56ud!au@6_ zE1)5H_<9ZbtJREw-83t(%3ZLlW(8Kc3wDte(2#0K)&Ir6fy+pGN7prW<;#T^{X2FwCRj-%?hk)EXbxa#xyIi zs)44P&KQ*y&>{7x=*wI*r9-}!!EJFCB4w+I71Bi~?6hKFwHiwR8L!2cG_7_QjA&M1 zmAhbAvjVH!1w*m|H3}Cjf~Q8}P#rQuX|M%Z%yuM;`8ut%EpBdFjFi)w#4Sl^wYy+Y zvjVH!1p}HDSluo-T`pe(=IZu<%>O_1iGi8(Cnran;vYWv`Je^zinA?+&T zaNF#)2oOzV=LuIJjR|%in{~Ev*gNm&SXvM!fb)%dJ0y@X7;ZFhYl0}a@}*pzFMCo6 z7^Uag`IZ4@iM9c)n5;38CQMy~uT*s@^l^<-4t*5K$Te(1S0}<3=|eemfR+rb-B3mi zG4x_zYgx7Whdyf9C-w_{?D2t%x-5=&e1F1|dxdXJM!wX4?0~Jv$W>4uZyjSHb<6g~ z+{3V8o59GW2~Nc;P~Cx3WVseGTdbffNaktCg|U&`f}QsVO?f;)Sc&Z?LgoxvhW8Z$ zfvTWKZ2HE0eBJ<6ZE0)9%=(=sld;)oM4BAjNnqh%v>gH$a+ZMJ9weM-vtpblXirmL zUl4LJhu}>V3BZrl>gss+3{utQU}Q%x7+J$eTP2I^n+iw6p77seks6K-{Va0%%b!$@ z_un}C=<#m}MurULZ3ZL9+Kf9+0s)$b3LOAvb4`=WnR1XQS0GcxKr>)W8N&)mjh{D;kMA}1^w?L%s$<5{H%IRsJ$E!by64DKBVQh= zjKITh4Bs<+&TwdWkD=cTeQBsX1P#7Ec=zDP27`mU4?NlD@M#)h_8eNT0|7fgw@b!K zk}R~$Ob~Y$4N)h}c<6*R!hpJ{7lgB@jt_;zOcEe$oO`Y-s5fJ*i>-U?7M%~R7%^i$ z9Zq8bGO6QexLS_^xv;I4%oR(Gcvo=n?~0(Yzzcq(*`BA6M5WZC17%c~_4$CBSv=IW zf^g7Z&VyVm1=+fS;|TJBpybT`GrokH?(sG$;ZQmy@6k zPlphDA`Ae$m#)*{1a5M`O{K(vC%S?Qo`v>26Iw_yPCrxhM`K~Q1?ahQhc7!z;tD~e zqjAz_i!>LMR-GE`3hFwQVu^zgI~Z1wyc$r+_Y-V(GY8zB$VA>z(Bu?Vq3Q2PG`I(MAGiK53KlkF@;we zK!Q;_3%EjzQsQu3DX|*D{7x~w7}E@ix4D_5*-|%}V|+DB3Sttlw4WhO_F|dxC%A5j zgZ7@FnTqmIzF-od7GntjDStf`i!l+Njnw+f(A6i3j!O@)&+xL54kZf+#3KdDJAA9%do+3-k8EV_OOm~nOP@SHU!)LaWbWLY;>QD;v2Xwv3 zYOrU>RJ-26a!e5kw9Fn~*krCZvyNt~ZBGiNj(Biv(&bc;_Iq+rx5R-@DJ4>N)aosn zd=0wKYP6XLy2%MlNun9?|NSWght>2-X8W+{>XI-7JjkxX8% zBRWYcXUQ0uoKfVMV6CA%rpy3CK1DE*iDn!Htf^-xYYQ+~fNIv^X4@unJdD)}6*?1& z8aWf0k0>QhZB_*1jS}F|n_L_m$l83JcE{HVV(kK%DlxMlP&XH?zAY|m7wRk$*$-!Zvl`ke6cJY`FbWo)qhd$X8EXB)hiZYeScq|0r znM{zZ2n8&c&DYvIXHUbU>Jm#t~?P>eKhCi4x z+nJhIXSTALGQ{9YGY|a?17*n}?39fVPLZwX79ma0Ky|pypP*$tqD-=PTE3_^C z2rNmIoZcVrXT-A^=8Nv-$+ZM>gylA4#>ySnf zX)_sf2+I@6iqenMy|Eg?Q)EOR(Z{NwAp>NJxG|O^E7?Zg3=m0%B|JQy&w$2UQyJt# zy(J(}HgitE%-1?t*sWj&z_m@WGL3~JiLBM+LzT_Lh)WUlSIpj!H=0g_oSk6KXsIP@)^wzX zz+s=Q*v{yjEqjbM(NVWj;^1c$L7%_qL+mU~B+DHvEb{9%B2>avZZzu#qY;ae$w*93 zHk}x%)N%B9MbKRnOb8GLUEY9`Y8yzPR!Un!1dPK#J7MnEU$!CNj1jtDJfkWYO}5paX53s%DRFe~ zo*<};r~PKwj}tm$u~-TkTTuY3XUqN^SBht?m6)yVtyH;aZ^(UJ5p=ZT-ges*;6Z{6 zb#eud*KKrWaBDdzG66bhIOp-_j12(vDKq)>Z9PFo9HaUMYwI)#gv+u;zDd-p?P?0j zB=u$&Rz&P**kS1Cd#Zjx5kxHxFx?WYV9iQ2c!;hsG(ZUMh|mCj)Du%uOh&&Xp8Fao0qci*-wg zPxv_)b7t%M1ZUERJm;U2L&uR4?hjdAg@0ipG*zd@zE2&K1T)^H`@ z^j322P`0kKiR*F&wCQ@&>f{DR5Vn(o$VZo{HCP?VRHD@o<4zVks5RorS;a%>foo`*n6Q>krrcE@!*fHm@orLa|E6 z3kf=_1x{xjrb@$>w^*&XKs0JW6wlUDaoQkc;Alyi$cH8rK@hECHFw>TH8u&6YLx7>q|<*S}F z;z*12M+jxa&8$}h^;wgyml4GA?I4&E zju%O~1c&N1%!t`wvgJ^Q_*73YS8Cau<@snOhP#PS3TpEp!Ug7AbjNFPMlESQV7L1V zKE|Pp)T#Za#N)Pa;QE1q>y;aTz2}~pyJoIFXPSL`_Ws%PXCt#~XMQ_#^-OIBnSN{f z{^|3lBhzcAemiybRBZ~Gd~5Q)$@3=h$-O3?p7_c{b;3CQ=J>ti=Zfcl!}!i)kB{9x zcE*@g{ZsG|Ld=B{WWX`VK?uwCi;JU z?UJueAVk}9#cKbruUYan0va&XW2yE3`syWL8$qy*Mf|@0Uw>uU*MQZUX138LUtaPx z4C>u99Sruz{?gW8LkM7Mwn|=S|F1v4?*H)xOFjk>n=R_whSKLR z`4})#<$QP>ZGGOnsPmv=!FmILz#J23^f%xmmV9f3isBA`DHTY!>2yoKHDsM}57xJO zhcEdS2I&})u=M?Qe(ASBs^l*BH}k`md<*F_ZlTt{KMvh*#31$EV(x^+Gu;=v>jYrP z#DQQeh%{4qE|CoI9N{s#gOITtz{-B8RS%Y($s}2;x0F?TWV9#f=3%}NB3OUjZ$Ndn zVvA1Y1+Y@fW`p&po^=D_#;jn0^~zyr`e8+*#a<(eHM&v(20J>uQ0LHem_`UXNSEE> z!>!Hh^FdgkoT_8=SUj#7EOghtrdkUE^j?WkZG$g9Ll!! zMe$Tv&HEg6r78n=C>kveVqUD#X$mk9%LRp8Eoxz65pN8le0WS35Q+cUP_fpwLSn{B zFcmQslB}{Nn&|;Aew|+oe5z9Lww9q&y1NEI7twj!Xp{=ZEJb(8RE^eh5XLl$9#5L7 z@>JYzP6lWlx4|GtXCIzLA0bu7zqK(t_YQ67hdFc}AQfVa+wZY|MRwAc2^jgwv|hFWZa zt%fb`Osm1R%kBtX%oZycucy5UFhVum*;Y1ct0)_z;Ul}M>iLSr3pqPsmTdXF!Fn}S zj@oSEjg>6`&{;iAa4eN?o6USn5uEy`BG{;aIBoJ5P5vfmvv~a>$ea=HfC~m^rU|g( zEj5;j*;_5t)Z3>hyGA{mvNnozflXHeex#Vrg*s5$0wn@5JyJF2njs)5cmuAEJE|NT zr~FEZb%<_o{#L1ziR()=;YLs|6XMD)9&EDWMmL>qm1q+Hrg{gQ@v{`c3SP(tTX2=* zt9Bz!F^n6@SNUi;2L%%`A6+p<`7mIKa?#%AU+G7ECD4IFF_iN}>>=?;t;@zzm@}Nk zZLt#Uj=6o}wFppi`+|aU0XY5@rNlCc1F1Mb#G{}D z`K^%@)w>-{D1s%%SaBO^5H&g6c2_WCaoHMBwpwYxZrq$QSNSjzPBw5Cr|dV!ql#dj z)Th!`9;@h`$x05#aa}_^*!W!p$rTGZALC2woK2m@Vo^#QxuYj&uON*~I}7VeTP%a%LPjU-FfV#$&%S(abF=hiM;vb;!^W%tHqUjhxJd)S8za1xj>4B@~G z2}?*a1H(xsoC(VeWO5F05{Ahk5SFlICYhhEZ*^7OSMQbex|*KkP=EN;d*zS6pP%&o zE#L3&yR1#TBaI1x=hjS|K${J4T203trXl;wi=*+;J1>B)74+MUWwR#)V2Px&9u_s> zN#Ai|xYZM;u)zgXO^r$O;&mMSr;h^--JbS@b~vV*eJ)}J*fo?MUZ-d-;nh&_fV(kU zuRtG|bE?OG@Bk=HXN%d48LyU<9~f-hw3G_m6@9m1xA=9ZJ!B(kxLS=bOrvWTj$FtI znw;iV37*F)GSnZ6LoF^Z(0bLbCMB;GoE5jc7wpF5Ev?|KAIs8Vh6B<&q5FlyINb%1sGMbvwyY$uhcSmBxqDB|E@_i}!!) z!mSRe4d6A1*(OQ{wvYwNKo)R=3lmK^aqmmuBKAU}JYF}@SQEmq$!BnVY(l0o(}Pk?S4P0?ZA%Gz zu~%LByB9!-N4<7PXl%8~nc~Ey2HHfH=c~aomTAO6z>jV?BPfk89?tCttE13wOEZ17 zRJC^BZ;i4l+7^jbRYOf>IISrbGBww1eYQp(EIH_fL^jEQUB~e_HG@eQtC97V&pBmg zit~)4%kqR_B^a5e4~+4{-@gFTo1u&8_>gZ;Ft#)r)IFSZmJ;g4^(J4=##lD*w~!J6 z!pyej|MUTnX*&LNfK{rsl8$B5#(3FR<`yVo9MM)J5N$#wRIfMcU%Za14>n{f75FBy zq_`x`=3$u`m0VnE8(dZ!CvaNVO6-UkM}9iK(AAq4UR8)RhH!ad+eV_)sHPva^&x_afCE~pYHAxHzaV$~QVYGG+S$wt-yX-d6-7E`iZ1sz0| zZQX7>I7NMb9{>N@d*8En=Y6+-=JsdZ3U2<-H(z%nIr--&(D64O{q7NW{Rgfe9@^Lb z(X}^T{n9J{$CdKISM2}hJ`MnXIe*TcGw$r|J8zZu_KuJCuU&io=-`wY@M#YEfe)b4adir{W#-5_c*N6RISFw^M;!EFhP zrxVPXedizMpWoHpmERkN`4fEm!zXxn8g*!HfEtq~qNTWp3ZsxPm!%eHxC*sk(uQn! z;1jf;&1n7OPcV)R&$*kerfgeMniGx|JY<#wr_eNXM6u4+pDMKYA|j%qX{j6Af2o{?AE>E!_As?jWsqJ&gK=0FWC!~ zvc~I$X0@HNBS654RzhlZ@k}2rlJaz(S8|3QvGUJvVs_>E-19ll$0j&uD?~ODI(*Fz z=d+-}dCYXxH$Zqk%zDXY95Rd#FIH1Uz&@^?6Vq8do#FYL{)Y4C`3=)u8NPmSd4g|x z6KxXn9vjl$oLqEQ-C-FwAYm4xc#DZjrm1%+*dEto;ez1B1b^!E$)DZV4bMOS{;vF< z(=R>2Pdh~=Z-Fx{Wl$0uRa$8*hbwuaSzX#>7inuT?6K-fb|(6!{oqY+E80B0jKrtq z+`jTx^UtsPohLIaKk6hnpWn~*FEPAx+P7A?vPtC{leGnU?gpvoxVAmj>Sc52*Oqh% zm?V{!z*-L;;jRqt-?=NpTRFGQ`9|~p|9xkM>LrFZPr0s_u4bE% z?&D1eR2p#O`D*A*R%=4{tIbM%gB#k^?1yrV0CL^mL!;_rbv|2UOT7w z$dT;uv_RdQ3yM!Tl07vi^85h#zVpS=_}X3BzDdm4K5`_xR-}H_1=~lCWKU&zesFx@ z{rNL}@m;&Je4CK7tUo-GT|F&Oh2?_frJ}=6fBomDoxvaERGV=rSDiCc;PcP39r>N-|L?yYpw34Wpa}~$-PPI7?MkPVs6sB%^CNE9&oj0l?qYYB%S*(>TEjbV%hTg zJy8I$yaSLXZ%5b(z zD>>=hp=FIzA*H>zzhQ~he6XpMGaMO^J=mkpg{Jx|HK);dqgef@rseexMi zxhM7q{{Q`yD?;!I>429G7oVDWcYd(V=iHk<-U{dP3Hm(8^+BKRJZW~eaMEF%fL#&* zpU+2j{vX4@S?9l$59HOR#@Au&W#{3#-m9pA(+m3L6?CuKcUm&5a+nA%q3`vQj@8kz zd2zqxP7Q>*;gZDt`|Zr3M-nUDM~QyY8;eqGLB8E+>9J88&sNF2 zk0D+_AtEP7@;FA#2y*DPKCV+j-(FTB)Y!2deLi$B55LCc^LhA@;eL@l=NZkz>IdKd zMq`=yLnn2jB+g&&8;jI}%U^ww>UmGz%8KrV$$FM+;v1iE68_Xos%7jf@`v~Sop-}` zy#a=xyeP~$bZ9%Su?!RN6A<`+AAcF|e=x+npCkB3H2>Epjr#oO9{$@)O+x?%0v~?k z@>=xxZ*RLPoD0l3Zuwjyxm^Mt5rfPryvVV^BL&mhbB52 zgnLE{4f=^LH2JP`t{xws90gaZ{0A;A99z@$Q?q1jWyk(#2Hx5m^V4w7!yd)6&;VL; zKHcA#w?!zGA-E3aZg~h}FbKXbm680L+bKF+E<%8hmSLDH!h^X~27iCbTq9OXEk26D7^n{r_2dH(;=FYJBR{;j>MKX~T@xBu*Ra_isS3UB_-O?&^={x@Iw zZ?3%l;I#+eenegWhu6RO>YES$-XVJJ?_Mk2bZ-3SjTcUS?LYE?ouyer5>5Aed zL;=E#6*>bIJ6Tjm#rBdL6EbCxy`E$RMkS^C(R6yZOHi{#I2bL%5iNFIak9C@VZZT^ z!|w4tr!}iY*=;PDnCztN)0jJ$4r@YYm8m)@DMOiQcoM5u=Fn8Vt5{@f$r9=!q3538 z@Qn2(4mbbTha7efPCE5d5W%?QYm)9y`gJqbc&8OpSTKP%ZBSfR9`&s19HgYBqu{P< z*`iKs^g5f^Gae5j>k@}6k35In!<9}u>~Nt>I!T~dKF;}E6nbo(Wco|EMmrT6hrA3` zsvvQ23f%<_w*B6cWFyx#8`(0_(#srv^5JvXJ@V-k<=c!4vYIMAf=JerQm0#LO**7C z?^Krd2%gN7uE7u5aZQ}w6^*eR&Gkuht&B6D){Vtu9Ln{>`wuzn9t?NNE*XHJ@E0g(F0+0b9t=mL%8La$;!;BHPn$a$~K72aPb;sTMrFc6(H;GW~#`z-&{=^ z4V5DK#Jt>X^7;?GDh>rg3U}b}cOG)sJ&yA<)$9b)-C?$g6^X_BoUhqS5K@ZbtP(q> zuKA^@0k=UeF@85C(vYM2YFFk}9Yk7K?Z{}}*?DR#4AECj1MHTSjqH(a zq@_}&KJQpgCu3IbQVScfs`1zgx+}>8LFp+Dsn3PMDqzyf$wybiha7f~^*n7Zxd|F{ zH`bJN39#>{j%hc?aDOT#Bqopxer2H> ztG5Ij5e_>SMLU(rhDxw?Sf!ApKcPml#gKg! z#Ln&X2)a~YJC|xZWzuL!pnfZ;3W6h=I6X$A znYQepopIXicGm*8B&A*t4{Fd|eP&GzK@>LQH1YIg5nkqS;{y*l>|XWlwBB3!FtKcI z#41*rg%xB265>qGM?l^c=C>h}5H(n+_hekYtIZTY5viWPP)*e)ZQ*i{*^}4g_y5oB z8GCm=eEUD%{@|_u=hpjg{>9DpjX%4wI{A~64-3jF9P!4U>2}!g6{Os#JB1&U6O?{Vjn4KYChg zIuc5-TDn*-!vMj?9PheaPI6?~+-#P5qDK=`6+DcUcOal`2^9FzQ%_d#{-C6zU4|co zc&w<2FD|muyxOFh6=@FYawTf_1|nE;pRNPFEdeR3cRL}AwmZ%M&-!o;(`y1K&IVhve%^TAl2q;?u1%C9j zXPSOWhDr}14XU~>t(s-E)H zisX^(K%lWDP#`&_NSeqF1jsFc0=Xwel0bGKKx_#V$Sf(6*RcZud`qA}MEA1|Mg8!X zYzY(ym43D{c)9(5ckjFR?tI&wx845n+i$=1-M2pd=2zbM^Ba?s-vsyjzi{jv{Tit2 zcm4XeUPli9_Td*_`;}|Xwf(Eht2eHE-xcKG7Z1q&U)!JW?}MLs_J2(Oa9@7wEyuqr z`kL2P^bG}yY~oo*PA_}-9?O(CrBG#|Rr-TPHmyvfXb8=fSR)yC47tW?1k4P{sMJOa zzcmgb56F1WEoTQq{zAq}L)T7cNk3jKm3Z08>gu>xlR@F7L5)apF_JeBRyAh}a0iGI zks!D(kaz!<<38PPzmWIwIP|!P6UBFc9ORbc>@4Tebe$qDn6qO$e@)Lj_RgZVp+!#) z@Ifvo^cC^Om{v~Zma~Ij=y8oH;BbjdAOmQO`E;ZA3L_kv#3|pn9JFVMO-K5i#e5B&Wk%!+2-;W@s30< z02x4IUU<5N`tll6z;EHc7svw|^WL51J)$uM+!OxuKn~ED=XaL#n8pC8IL#2 zE#ggySRfK?$n4G{pI{~y@N>8fkOwwoW@mYiY{&)N8C&~b+Pm|uciwXQM{mFR*0*rhD-r|#Jvrj!B1iXyB+0GW{6W&Gvquq0XK=3y9raKFK zh7&R^B@&fa8aS+96vc**KgAmG8WaxP%p;m%wx zjz|Rz_o)elG_z(>@kfayMZ9}twQjlZ*?$JC~X?S3-M zUlmXrZ)aJrs5YO7@ov4Ca{=Racji*udY`@kgyd>t?`-frL2U|{@7{ZXz+7#todrIk zHU;eW(epr1t~Tb*f*w7i9X+gv=*q$yenq8Q`UQ zY6YMjTpZ<3#FQ@*k8pM(T|C9!Cy96wyZ@wpK}LNxWIiFo|3thXqdXfjpOEE$B3zJp zHZHXCCuI7cv@XazBOlsJPUl5z{}cX#%rkPLy=3z)VEivQzy%opd%HW)K4I&B$^x1B zUcS4t>#)magQjB3dH0J zQ{XtevzW&fM#*0hP?!QCt2;}2MTNP1T0U2pZq5dbyTIkt&TO^{b9SS1KH|?Pa_RI; z4iL!Y(=sR*b#_}mXY)BluAQCDdsvd}IzCBX!12K&K8g zqCgbe&U7x70rO}eE1z`*Djn`D>+xAfoj>H)7AYRLv!qv=bua0ka6aoAIU6wU0##gh zX0x4jGZKh+``h;fJW|@?rJYK+9w{tCIpa}Zwv)GW$!HU2^#|r#N`{q%;=_g%FjRxt$C$h zvC3HmuhTV;6gC;Co>O1pRfA~^yZErOv$#h#hA#nPaup4$wX_>((3%bBBp$^W7$nL7fp!-2xQf09NXj<`3BrgOF(hOUG*Rr-)V%w;b@KKiqp;su=oSqRUD&SgKrmX2^ir$A1^ zv!Qd@Q?RAex}a0QQhzpdF8d0$bodK81#I3`_DE*>G9S8d2QGj^!qU;!|4K4wjV_vsH=&UpH#NAz~MNr)LGP9LitZTbYai$2Kl=_9AJU3lZwNGCen9 zi_EI)xEl2&Z@g3`QgbUpNRyuk3Qo6=9B{c5K)Tt6>t{=`bhvOks911BI=wnItNd+98_ zzeGBl%KcspOnq93WH$Dyi%5I*-9+jn>}s`zBy=Z^=6CO>9Nl$lvh5r5)NJ$ZrGO_~ zdwdTa(~L<&t|Kd>L_?;^3{Qm0M7&3O$sFQvN?OG<+#fZn!h+nX(s3mpdOoG^wDT$b z*WvwVduDumO6Sceo-n1MPsNljP(%I6yL5q*?vtkU#W6acATdcJZ8}~u$5fkVAHrniwv;aBd8dXJ5P<#)c(w^OKNB1xV z-n68=mZ`N{?ePi>hMv#JeerxozIOlF+ElyD$XfkB%Nbep;C-o`z)Q`{PtD|f0uP{( zv2TDj9OnxCpiMya=`->ZnS&5o%{z`faSq;_@V2+Gt!wh*J~P7)zIF& z0o(2AD&hw<271+VP^J-cHJ><1YW6csj_EWD`JUn}ZP7DbcQlRHSekN`K^Q?22MXdQ zu!5?-)ft%5{Wd}Nq9sH=%#<;U*71P{QSum9sk;~c=WH2K6G^F`VU>dd-xNF^0nW(_Tj64eD&+Eo?iL! zE1iR%KX`HfPxrrO|IK^f@vJTUv!fC4fV}m2aILfgq_7b^e%Z05R-eh3*;s2EaoxK_K}^xBA_e$mLDUgS- zv$#i;C65ZrmF1Hju62|LhULmqAi-eg0llKKT(^T07M=1*d%RvrzOE6ZO}s8${s zmMhCAIaDi;2+Nh_lOC#dlm~_7%JL}>)iTb5#=w|AiD6nt=b>O=v;|5TKHW#UL+JiX z!nDqgwh?1{pZXZBv!iXe7?`GLhZIR&6J=a7F*YvQBG(uYt#06#TOxxILCe#+KIE;R zQ7Hc9=&ZA&ZJ^j*k!*!$@`7HHTXA-@4Ltxe<5L}aaCWo}I{-0cvAQ0XNvatSBRX0- zWJm0+s$E|7)o|6FWZt^ksX12Q54~seR4#`doE>dL4)%&v1$Z`8F55U~N84}%uojoS z$ZXF;N9K8(703qRHhnr7`4Bgpn&L8;nFAslR2IY4v!QbN1bue24K>(%w)GZwSXauI zx7`2Y!who!U)uY@y*s~n=ezF&cj|Yp-~N@`-*t9KUdU@958ue*WlV zN7fMx@(2F)^?z{v+povhh3lVp_^?f0+!^tG?QreAyO)qj8WA7B0E ztHZ0MEC1ojFI@T7E6x?<%08$l_}vG=LH*$R{;%wRAE+$&9?+cdUjTxTbj8^(q7XdP z&JhK!r8r2;5e2;7EF|QJ0$w==YUPLmk+MyQ&k+T#^l6C85e3};6vXC;0dJ+(wBML-o;t-W13Itw$3Dh_r$L;}??}5l1Q6R|i-4Kx@3dAgaF@)!c z0%3<=1bs=4C=fyT*P!>DkD~)ArlXMMScwKaqIFZG=BJdx50#M~FSO340+xUCT#{|h9cJEnl>Lcr`)OzeQ}QPj3L!m*frNn z*CsL8lpv&KC!}JuYNG=VY=y$v;|8iYY#lp``?R$=NLq*hVeYl(rYDdWXhY00Cy zZ8q4NT4tD(3VOIwsX>439ARkg0Jb~46o*VUTOxB!Seord*icJ-bs|kNqEgqeiAe-i z=v_I2lTn|vFo+A`WAZTvhcjl2k z7!HH+vN0G9K_`N`G)bkgS;4uGh$sG>u2di_M`%<#73=nlB_P=`f}|<2vo}v-Z(`Xtzgz!j5dKyKz=HemqMB2JXamnnwsoVVHy8DJ(5bW(OP|h(!5s7usZ<7$)f6olLURPFrh({o2_By| z@xD%oWeKSU9At`NCE?n$8E~P3ItVhL{K02m1VUdvV%7U7gJX z34>yFDwHE*xsvev5RxM%3$e9vysEzO8hX>Csd5m+H6n5-5^h1O$%;s+^|I#Gg6Vyz zoFn|IXfODZ)6n~g-lpZ5f|yfdP%EoVcxo;;l+zWeEixr(=$#OJjv%6e&CIHk(wc_j zm6#ckuTVC$m`;>dJN4ICTvHW1)cI8jf^vl9xbjn}(%$G|qsUA6&tHL%#_UL-4o=DBm61MF`x`EXj)4~ zPSPmVsd?Y`%rsJl7SQurf?Az&pGRQ*jnJFUWt2>5I2s61Q=-<5Rpu*39rWpJ))*nR zYUA<9ZGgOi2%*gBUFeNDV#!w>jBZ+7NvzUHBFJHl-IV=E@D;uXryPT%X0l!|z=F-+;0Wzus?-LjXdtPxt=xGYv!H^G^TJYi_xLWZ%DP;Y?VkR!^iE;^v!JMLyK^##xufNg zv)4nKW?S}6EryyrXkA6B=sZm7P;Keg`U`8oejfCBIbzW_EF{5Zb$+m+GZ!;kK|DlL znVBvYbQPF(B@rLDs#IgFe=hX7Il?GwJ%}_%Hq1^HOG{Kftks6)Ww@5{aJq8FxY{)A zx?CTzp9_6Xju^&6x7{a}T-}NN3U3Jdh9jVUIwYNGS??p3gG)}SN)JZz=RlvGBl`TR zyP{ym>w*;{3?00om<@W$A~snoZ@eb9ZuRN1rQ$Q|v!U1J2v6npZguD>!wuOb5Y15< zH4NL}y|{(bYs2nEKG3OkKT6csL7$Z)+*$`=jC#m8n?Zm?qz(37miDna+vQl#b1Y0w z#$j`jwdBu&UYjGT#%6(4OBm5p>Y`TW%%;0ksL6t#nxJJdyD+$>Dr9N5>_e}GJ~Kzu zHO@@^&1A~Ra6G{h5IJCj+5VWNU z>qBBQEZJR@?@k-cc(_{Pjk<-_IB+&<=v1p*1e#QrKHQ}3m(Bo)= zR0hVh1vXh|&X-9Yx((gR5p-{&IL#GS_pOmWtb%^TJrMdtH`B}*_iWH*sk9iYY6tCR z@-66Qjz|e*IS;XJX*3{F)|E@_pe!mZT8CYq>PqCGjb-Fq1_vPZCUoN*!J5#t4LTR5 zgFy(6Qp8|Q!ghBEIY=w@OT89@)SKc=b+h;ebdn=hWC^yIl<4CFN?2Gb0rqdb26O6w z*87+y*t9~I^j1s<_z85JBg!dP-&pB23&rD%?!^7NoK4vPcjg*y+#jTOm zF?5t86i01DpogJNQ9u)wlAdak)acjS?b=!mM^?KNRa&;t7W?WEbp4zayNaQ*Y_tVw zC`wrfB0;O|(i|SoHjNg(p_^2fn0qWv4WR4LVUDO;pdGpmZ;~*Z%4y65>p?)LFqmR1 zk?D=fcxow~blMxi-XU}?N0gvexfSyZm5UapD3wPG-7vWMh~G?naiY#`tOJXqvJku0 zHRx)NU>LMg@2IuXz~b#CWbhk@tyyxn<}J8To|os<2v2&GBpB&ep({Cpi#$Am#|Tr-^Ju9 zf)4TA{ulQC@!p+(cIUh9jPGD~4sZYR?eD!kyZ!FlH*fv=tsl6R-fG->&CTDq`NKCq zaFe_F**AXs#*f|jvK!KkFF5(#lfQrRH7C83)8jup{@LSiJbv%-JC6SB=;w|;dSrrk z_8+hR)9c@H-M?PFe)aI5AAZkacvwF?y!K1i{^qsGwRc^+e)X5Ge$Un5YVGQkE5CT< zJFj@4m%x8I_$LS7dSHPu{D=LY-~Su?!~J`Ee+sBQ?GJngHk*Uf^>n7M)(AEbdiW+- z49a>ofYlxst;)-;6U(#lnCh$>6dwwEAA`@pc5`sL&{?v@2fe}ATBc-T+bgm+?2RkY z+Kv&(Qo8(@#;Ti|*P8Bq6g~qR&cUhPiF|MJ)+hcSR>;U_=@b)*Olnm@ie<)u<-#eG7aB zHl2gh{svV_V_LCC)xeV{MmC%-g^|`+EbvHov&9Vao6|&sstByD!=LrJ^I>y$4>%pjxv)>7;>Hc*?1Y;5l%@EO>60LEsmYR?Z_H6s{X;s94U z(P!fz^T}wWOO-}Mbz-=^o>Zy5Z-mdl)^l)L)oFyUMw41C<-j~_IE&tV)DG%l08Rko z)sQM1rC`N{6&u?72KWqYJ_n~Ad;y_WX}RXjCR(nM?F}W59Rfk*tircveJ=6jvYKII zliK_1@EO>C4o*wPdcGljC#bcvIxlpv`h4XzS71vRrs1YWK`VP=O18-RmA$Wr&%g!* zo*UXl8=$B7s629wPAJseW{Ku!4LEA0OC(?#Ws5<44WR-QVF>J(xDjq<R28rINmGKj0-S$`>X zcD^k+$RJ$|qA4QQ>tfQ^2cu>}mzg>1_Qwlr-9QC!YSTyr$y+YTXVsv}RHuRMj|@`ohY3KXi7!H=%Qn zY(V`cWv+UTD0an(V`t?N6QG1XkLS8t#)fUiE!kCJ4V|5DPE})A=AA}D3l-4(b{r&% zZ0IWiPg!bvXEmlbLQhx$W9y3 zG(8HxXW>?VJUJen*bxZ=6XuuSwddYyQ&W#Q?*vM6y#8GoqWuag?p7>T{5v0(E zb3_fo%U!d;0&c(}2TPm@zT)g9)!2k|Gq#5FN_oYqtJ-G4i`D{q ze~!?V60i}``f%E`WR;vx6h&(WeR$&`3^&t}A@1rBtOxW)Tn-z}iL6l0EMejqw9XOAa@vfs%)x6|s@LR!5qqXcOa>yeZf9hp zi%yscKBWXh@TO3fBiP0~Aj9DjD+OeKRDoAgciuulFHorrCsq$xbR4rbo-md#Y5KKxvL>fe_?o;Dcc2^14f> zj#XU@`ZxqqyCJVi^G2r{jYv0MY*s-CEptSL8m2_%i-USqs-g>X#ZrmYFu^um!D_1k zE-%8s4X13qhKEp+Bj~m4^(Jj}F0B@zxu+5}GS=@yOI>5_Wu5o@sn5?!{c@&)bu-Qp z7~NxF*j?%*B^uObn3Sp}T&ktzrJd-bh<9VqxYSwH^-vx|iyToPdT|8Jb3}nCLm!&u zh=sRdsV0dTZA@0xUP>>>tZeo81VS|dZI?{hm!*+d>T;znFhdWDazuehK^L0j zhyp=>4ix5y0>OJW6y%5kL3S22&JhK=Ae+!AM-+&%Ga)}m6bO_vATLK0hZw1 zAYP~oX*r@muhI_G%Mk^lZ)8Z#5e2GfN|2Hx3dFLsp>B>S5ON|yog7i17=-}IIif%S zN9zPw*W%u9?Cm$M{5J3bfEa+QAGnSk&hFH&{_vga2RCp3>h15p_5)X>+l$*@a`iW^ zz2^4q>*M`jzsg-}-1PdvCnq%Hg$NKl#0rpE~)vlm5Z?p1fs0J$U!=A0Pjt<8M0nd4;zfur)#-t~WW{kyJXkGKHJkNkayzrwjGQ0jsk z7F<+f!Q}aJY;Y|X+C-RamTQ9uB3jybJRPIyxIGYV{rMJQfR^N~5EG(WD{;MO*z=Vz zop!tOrY)yTx3`?|SyPt@(z?x_BkuWOMlq9nYNU0!o@`du%W^Gl-^=Vx3ujc^f?%2! z#{%&8oxEWyV~I=jFp+s@(*p-{M^yZh1x);2s7X9U5HQzAVoX~bL-oiyM?h36fVx3T zic8{b9+sI=$;G9%!DY2^0;hGY#E!ri&9ow)lf8KfoHJHYnJRVl zrRm#REBDbER^HIIU$|89$zvw=G+2be~-XwI|UZqIMwP=jx2Ls1aC)`xinCZzI==D zab32CNkR}Ucj+ywCb^+IrI@WRn0SInYv2L|r)JXA@S_jB?h1Tx`e?DZN*NfmH~H-^ z5l}in)X)a5^$E@o>8uJtsXdpJa7_clPcvH0PERAb9(H@NMGRckZH~zmHK=7>TFrpKPs+Q*jLh)p?oG@X;nHo?hf;rOi=&qAtSZ_Vue4`Ok6I{gVaD~lQ$yW!;H7%=hqvS4P&=v_7D`o245Sod8yAX= zijLEZ9-)+(nD1sR-mRq>d-9W81Uz#3;kf4Yj7i1s`I1DU(Lk9rtYJy?mQ&uUHLY2) zsP(Lyr&|PyR%#wHME5oqjw=t32?xbm>J}>&!1TG8Mji1;e*m6m?#5yW=d!=d+gitu^QYdv#mB`tdTxE3oTsk0dEw%_N$dzsftg$&axS; zZhUNukTtq7VB7H&F)^f*uxqOAYAs?gS(a)^P+L20T_#7pVHDr^=oSH1gI+zD2_B&qK3?;l?=b}Mdt{p;>B5~lUBzGkq*ZWjLughO&lllpg}Rq3J#A&%X2r>#=CKQ zD?@9pdqG`nYx<04+I)RlwfMDAPJlUGr=3}=RBtTYNyX{vw^v(4nV6C@ZjO7^RS%s; zMwvwsA#}2SHeg}k6R%`T$0nheQ)!Kc(HOi`JTO?K>2NAe+rZvSvrskTh&(=tM72P7(BrDe) zj>oAPOu|@=thap5DKk@?XB=Gy*T<{`Bh%Ei4u5}(pf^Jo)A1qSo?tAvTdI3F=>P|s z7uTD7IU8fyyx&4fgtFS7ULcsJ<4*@zrCKZLST=2pmwjapOin4Htw|)N-cu63lDp?&_9gA%Dho#s?r*jSVJJf9G2VtV`|M3=4 z9WPU{TBm^m4Wcl!+Y_SX2?9K;J0m}nP)VGO%CTlvsOx{SML_FtA}@KqKHMZmuhusM ziRxN%jUIGhc-gY&c4yGVn&`^g`T?;QfwUs(F+43(-}serHaMrAW}$Na+{3>*Cdr*z^v>D zSLR#9R3=4c!K|H?3?l5@VI7}W>)LYa^;4WiEYShC8^Od72k}0Afv6kmvN9TDX$82j z+OR3kO$}V1ms?Y~+8<5%!7!?|A!21+zmO3QM^<1nU1iW^0#cKDrJgoS#O<(%y`d-M<-J+P3f zj9kPD>l8^?vr?5Q5e6v&U(Ra(;++?`9J$y(jB*JoEywo4z1eJQ5q2c3h z)$yyn5x!{1BT_#Qwyg9dgn0-gFQ9FY5;p6Nu#w31xbKe#QDD`u7Kbhgc+@xJYj4^j z`b)#Egrh~QmzsVSmn+pJGt;fk#z#A8HVTFb-5vDc4G|oC%LO9TCu_sNCVgF#D_s{{ z%SY*$^u5t+y^LuKWg>_YdK%0gy?cvL`;gw7@#|8g;;avq>n^j2n=4}7nDCQDQ<{>E zmEEf-nR?}OE)WU=cdJZyK3G;3epQXCraubNc?=xYlfJ!iXuR8+S#Z<4@;A2#xoh?n zNP)Cz8F67Z=vFnqA9=~B4ui;}MUP*0M{c?5uPO)I(|M^qsmBalU(Pn<1ZjW>lV+(* ztzb4x8&ySfaTvG->m-!;)b)d{j5a37M9^Paq^IkMStk0;I<>)t1q>3uwTVr3l!4d4 z1Y&jd;yf=#^_JXHt%1yJB)^K!hO>sH%xY*ypn|nqVlrB@F%ARQp3kgZe9mBr4aQ-?C!cT9(4yF{vT#K8k`X3A<|r<>nY% zJ#YFmt5nu!oWsSMb?x)Fh^7Oc5tJ;b&8k<6y+yEG_2$#{)U3EesLql>jV3xxWCK~( zzGsUd@MhV{(nfjK=q1aFJ#%chmvTy)z$+Gu@gBI6^vT((799NO7J-#YB--l8kds!G z_ORAmRx(`-mjsk?4m)tmv%%6;8lXY^m3vTb1{a7lZf-@ObU$qx6&D;gOMNF&r?SHh zgALJ8c)!tC#*(kqba4WXB?lL3RnekNtCE%)IBJ2Yo+zP4c%#uxd&x{9y57vxZ6tM3 zHB=9dwlW|OZRsOys_uPByuIF>k`Lykgwx>A$!tslanuBsU)<3lV%j@m+$FF_# z%=_1O&%9zYtFZ=LwvvtA_+uM#W^3a`w)@-v=I|pM-#C2B;d2fvhwj6N9DMWOg9pEH z@Y4r{1Lojf`(N7szx%J)AMOkLkKSL~`}p2l_nxyS?LA@dy4|nrzIXT4yR+To?v1+} zJD=M5y`2~A)OS2P58nRz_Fr!Q>h@1^_>3sKiF{N<=#Hy4j-!Q9B+AaXG_fl< zzPEPfvg?EgXncfo=;hwJ;C%OL@ZfSc5%Ay+@Zj>exZuH4!Gq&R#isxzE9GW3>Kg_# ze3Oe2fk}pG2hmnmmQ^&8;X7!NYx?Q5>MgnL|8+^Cn3FmgKB_eQ!7SHid9+eYWi-=R zfE$FP z=?$=aA#yXwi51Ed4l_g?$rglysA7H6MM>1aS&gU~rLNOOwQXC+mRNkH5Y~Yy%n)zh7JM{ZUk^B2q@}7wc1(YdDGTej7;i z2x*`S6J=J(AsQ)_qdgHhuC~}AHtuVu(sFsCNDPXVcxLMvK%$*&RAUpeqYgU(Dp{0e zj;?ox=~j)1PULv) zaD{d`J~nc1y&g!kvPra*Lo;GpV|U0YSGvom`ZLup4KaM=$}`{t8M+t9+wU z;RX};%;~PNA{LAxqntFDjAG?t5=#f%8dZnQyPN)l|GSj%X-k*N2FXBE;wQ~;Ym^&J zTlo-^W?FTslkO-)wVcGFUfu?7EKqFbOg4I|r<{=Jj1-ev+;}kXM|hs_Mq8D(IBRKC z4p*2`r|Im*uchifI)3x&6DroOHS0 z9hj_MvvvL2{a3UCs71W5RuvD{(0CB4$SX=PJV9L=38s|l=9*4Am#S;~Nhvm9k@epK z60vd}tKk^?))m!w!Mv*+L{tj0zml>T0JpP85(XmKcr(-U`(VjeYG|pv5}A zt2M^ux6douX7!&yNy%}6_0`*xCgxLw$=uU7UXKLoia;X;e~6z*sdi_?Yhmci+f5*m zz+|`7%SJky&?2?d&jZCmyvP)fOpovFI^~Mnr6J zni{JrH3H|86T)=zY+Gg zm&0h6iy*kLC~!vDC-pFfz^A@`V`hYXTo0oOEL!9zNh9oIdKgKdv66>v8DSsM!*Bu- zlJ&Z5g#EQ1hT*PAq*oglVIS1PP@EA;o?Oic`+y#X;B+9!2aL`k8}HY_7#B{>hFmgh zeA{2@VGK^TBJN|ZCL4dDhtV2axf_tG#<%^s4n`9=9__UIAtUT=Jq*LKvC2(FBkcd^ zVJMCc`r}UB2z$F8rg=$~TBFf1-F{C8qg>kk*GxV>7JgR`V=yM4=fq>Z@HRb+#^`t> z%{7d={f-Vsx-gO(hJ8lAnvLJk!x)T^0<~tx__kly!)OfmY7Sb*M%EkjFbcz_-ju)_ z-}ZVV4E6c6Fj+>}ujyeVhH#P;iy2|Rs)K3!Yi2xABFDP^ReBhO(j7JEJJ$8DG{UIZ zs2n;rvR`#rliK6-pw&KV;JAR| zl^hxqWAwOGSMvppL7E9u+7XLtMZGP7nDk<;P@$0R_9j)w#$xeBvEgi5%Su?J$ut_> zNt$f4kzOg2E&KRHygEgi+TBv8&IfzdwYwLm9tf8xu#-off;Wpe`+ZeK(~SUI4C4W$ z&ZY-B%^R|SQ~pG-5JtVZp*-GL-kXn_@kzhY?=W@GuT56+E`LzgeD`5F~2(^#0ghLvwb%3P-TqvHIs+8A_gWC4>R7~Uc~=j z`?PWp1M3hyL zdO;YfYZD997?fsS$_d5OXXfwvZ{z8s^APJ-7kzt`-d7vzm&WON81R*GM&q+a$U`&& zWx{`6iF#O!h`L<8a&0P7Oc6!cm@Ft#a{2-}&Cq-}GJRle5N5qZ3OWcaN)Ox|-vG z=EG?%p#Hq-^H{%y)M8A>p-5A(V|w1mE5n>s6pqeg6|WrOjbuJstNDVe3n7LLym`JHh{iO3$!?g) z%AqM1b6toJD;-42@mD>InK7^WVuU9zjc|xB z7j}ftL$t3L;lvN+2#26{-qR!87^&=(XH%2BXH0chTTz=EOUX*Rgdsc$*YCV;JQS6P|i!OhFo;b zvbAiLAGU)&dKL?I&!biMbmWiaT<&V5r$ta;a6FBmWxA@?c!~*0(|mT;Iv;N*i}_sD zJ1GY&#tNTTeLlkDiA(!(h%Xm*gwLnZ6(bz~p&a25RP1|tgc}0@8b|mKuxgW69@F!% z`&YLwU#0iej%h6l{(M1MIi|Dqkvr=Rv=WNDBMy{AY2wQ97ld3GEyiaQmueH5&)m4< zDT-G=rjuGSQdOvg!^L2H>aL@cFqieCb?=NzCI-oVd!TwXQvsqRQ-Z6B^v`pxM$Ds; z2F5ue)XoyrpjhsAJ9LGL%i)xe>U(i6VR2tRuevs-f5iXS^ZyUeF78mA5d43@Dudzw zp9f_?@c)-Ka|r(bl7R=o{|D?*Cj|c=U)IAq|Ns6#0)qd){{M3p5-|LK;B^7R|K}EE!0`Y79Ml7X{}1@hP6+-#xZiYI z=l|~?0IeYS|L+G95d8o5Eo*L_|Nk360)qel>p%j6|33y25d42Yf`j4zFG+9^{Qo8U z4}$;y4p0vW{{JTy5(otU|DQn_5d8nY01}vG{{NZlfCL2p|6f2oAo%|jkbvO-e-%hT z@c+9@3G4j-HvtI<{{IVs1O)&8bwC1w{|~&65D5PNzk@QY^Z%E;c?5$0|EWb8F#P}J zUIK#u4|q~e2>$=lYY&3|{}Z4d5d8n89~=b#e@WWySm*yg6=(&){|7!FP6+-#0?L5k z|GxrAK=A*UeGb9@|9Mab1pi+E5)l0VrFR|#|Nq6HjI4G3|B?j{!T)~{C?jp1|Nm+r z0m1*5fCL2pe@O<1;QxainG=Hl|1zKz1pog@KmvmQ|4<+S!T(=+S3vOpe;bqm!T-Mz zmj53DML_WXm!A~`|9=OR0m1)I!Seq%mql3T|KIk%i~kSE3yw33c7Wmk=PdI7f8UJ% z{}MC)|IeH8|A9*uVx9lLz6PpQDO=|Me`hISo&WzRAOXSu2VN=={Qso~DFpwY07^md z|ECKH82{{O>)R&DG2e=u7j5d44O^@Tw2|H1AKf#Cly zeX{G;`Tzd}Y7W8wUwS^&tn>d}*PQ>qtVP;7|NmRy%Z1?o$AAO`|NrA={Qq~I6#pOi z>>&{Re{jQrK=A*UCpZZHKk#WqAo%~^(^iywhW|eT-#Y~VpS+xdYR>=vtR7~@|2Mb^ znDPIQxe2W1{~O!{%=rI*ce!rn{C|U+fEoY)6Z*HA@&Eru4>RNce^d`MRNc ze@G8A*SiUr^8XEP0%rVwgPVXE|6lJWV9NjheO(na8vAPg|2yDH12u;*^DWI75MkGT8k7NHc6~aK zfH1qpfdquv)d?gZ%;5J05)ejha5Q#87`2xHwIBijf^ai1Au3&<9uUUht~M2*7N&zE z$g&8_cG|;t0SO2@?MoICF!p9}_2-1JHxGbT5SI3(bqs=u0=(m4m`}j|k3hr)cr$1# z2%9$G7$FcAYWN6szX7y@uu%UjkbtmIF9#Hag&NqQU@X*NYDcVFsJE84o)8x5WrR8i zcGdFRg@|eQ81S`2*tCCjDPcLL-PUr!8CvG9ZGRV(0m0f!0SO4Zc>zd3u-#sINy2iZ zJup6;5SI1;CB!bH6K_8oDkOh$HH3kmyHWSWCj}s z-v&(uVgJ7e;{QEi*Y@DG_}UlO9B+3#!;x~_u>RHccdb8nJ-_}~`#0CV_}^y*w7*!( z3;5>!JNCo&KiS8$Sbp!{d)wX%_L_Tvy|cUD+5On=Z|y#3SKhsK_W?Wqy7RuB*X}%Z zC%tpi&erzdZ~vd|7i~AUecKP+`o`7=wqCz=aVx)tZ|!gX>->FD)9_ zEDqMGB(U1~Id5>MW9T*lPQx&LYWZ zQAbzDH#1ly&ElIG0+*TDHw}Ty%rbd^U zeN%60G_`N)Ba$&@_RaTQ-t{zNX5aiX9n93eX)s)x**Ep3M%v82sW&y!X7)|JsnOKF zsgFoTnb|jgTUS%c%)a?n9n93eX)raK**6WQMl<`S-qc8%**EnO$u!t%`{wKPRWP$} z{)!G}YTq=N8qMsR22&%2uC{OLO^t+^eN!Kij4-or{(`;=X7R_h!&F33otL>Wx zYom#M(-4!)#J>4ly{**5zWE$I%EZ3;Y`y7nwSCiQx+I!fmMCMWMT6;*T4~=jnl3R( z%;epM7%2wRCAreRX)V5)8GY-yU?T)!_*`E~SdWwZhoJD|p|PI>UIi0F4M;!`*M1e& z>iRHH1Vq%e<%@1y}JFi?cUZ`w>ny0fY!!8Z!{fWa@5zq zuwJ!)&Mw>j!B$@TtR`;$^OF}J?~vsd1P0(U(Kun2>pu|SXxL&K}NJlmU45@SUH z6k35+AWTU4YN=Z4&eWQh=n`lmgO2_Ac%JC>Gh)GC&bH$tnW?6&D1a;!fJo+h(p_)? zS^Nz9Aks6#BxbhK1?Qm^IN>gM9JB%_+y#$aC;*Y~ z`rHYRg2zBBaKc^iXlMmaxCgM7_+66y~|EG`tcd+E4LB#*_F79by z=<(MtB%BaTLcmFaAy+RWr9j00Tk_B#XfsQmrgaYG&N7k^1OxAlpsgSnkxMQp1Vi;M zPzD6|@Tn&8|H9x+5UgtjNIFua?$5b2#ZWTR|8IE~g^2$LZY)si&Mb#FEZWx0?__u_D^R)Y6JazzmkmQ;KQsTBPw90t^N-QF z(U`~o`{dl38PC>Ah`C{bLN?HfH`YdT$$3|CqaVon;#T&k)$p%s)mS*w55IM(=H7>K~)` zwlVdOF?idU`N!zJZA|@R41xX3{9_D({mlGh4Bj?o{xJq`8#DhHgSU;De~jMS#?(LN zSYSW8qXvDV@v}4p_A~R3F?idU`NtT%ZOr^*4Bj?o{xN!Q8&m(77wSH8)A)ae$bKgN zF^0%~CjK!7UmFwu7=y2kiGR#<|9AVxfDI9Z8w?2L4HN(G?rZOfvb;Bk@I<*8>>;fC zmF#XW?Em(g*X*ydEB2e$zq9_n^JAb|N#+_&G6nAdfx!?9zw%@z`>h0OO zx*l17r2TvLkJ;a1f0n&uzr}uE+m~&BYWpSI#1^+b&bGexx7#1ve#`c=mTm~k!MV1! z^(u46gdaWm!CC-zzkctf{lxkX|K5O$0kL1-`7F@N`nltQ&j%9L14HcI2rY3QS^`*g zAd)JqeHfHcuBxNRIK~dz@nCRNuLN7vh-jBEuNX)mX=)&sT#;-&HD$+Ze+@11p@js* z+4KP*pe2mNdx2KgZ#)kB&=SB71d$`b{!CDY^;;GD@~#I$0)(f8$ir|5ctjAMZaXTd zhxMDAon`AnNGw~|x;Nd%G8_kl1h^$ZAS9NySnD2p8%xfWb&tI>2cUH!Bmf@@A_>gq z@`Bd-wecok&A^laEP0xQ)kHJ*`a@822#I%F+ClGr+ZyaYtiise_a0xpAK(3^CDb=8 zq5fTuTD`a5{kkR8zv)q{PXN1L)9>w9hwV;V0i~>fk_OO92l<2*P}~YArU$JKB_6c` z5)7b~{`majyGE@aCAJ?9?pBZa$6psPd*C;F-~)wQ_o%mZp(Q{tNQje>HpXGGaeY~xR+h7c9jbHx|J}9B+F@t^Q+xc* z-P`9ke{o}HePa8(_Q$LKx$E#@+qXZxc=qA`joXUtPTPqgs0&39tbj78W~6uGwBb^a z_qiuIY%6dD()E`Ys#o@3X z661Y=Lr3U<^;R=+*30HaAsa$5`X)sC4~@xv<@)p23-aLRo89WPUB3C|P%0I>fZRk7 zw$}6B`W5`H!-rm>L#Wf+A=pWENIuOHB~%&+{&+(Q3Zj@EQ>!~<Gr zLuINQ%7lv56&>P7Mj37><5L8irfaTAa->k5q_;4LD!o!HQp=XOQX!;B&ScA5%?IC(BJT?H!=GZWr&+ z9g3J#r)mEnOoe!SobSlNq}=sZeOVtHj|MW@PQ~h?{_@|v>+k_r=#a|~P^YXzCMVS) zQZ|p}8W}7Ts72x-VUSFdvG}SrB*=uFLX)iqx7zN{NXlXNXL`;jL!peo#D{&!;VS z$m}b39bR{Z4mrK&5cb5*Av_|C%b|2Xlb#Ui^cdk=q#9qfhIqqGR6r7LJgtbcP?D(E zS)$g+gkw@GUv#sB5+_HZ`DrRI$Gp{)D1-&S+Y?Dy?vT-Y?mE2R6*}bfol;DjsR^118I zuG3phd;DCO^@Rfw%N^4D=I-v^+Wy*IYmV!-Ke6?O&58XVZNIa2*GBO00|&3#Z|yx^ zdwi{b?!4f5deL@zcGK*M?G>`kyff|X5D~FDE+T4+_mlaROOw}~X}TjV3UPUS0ap$i z4O<q)5q?G`&zKHTmld7d98y;bzoHQ4y1*4nrt1j6mW zINRblzCOXzK4B=Xnn`>WUu{wiQnjhdCON0*p)fXw3~+jq6HAd|$e9?41&JeYZ{J%* zPF;hY@~M|Pbq#i62hd0=z?B1jQt+$$YEU%xB_sDxL9`(flCQQU)sKg2npBI=@*I=i7KTm7< zj^J{uUh9qFX@CD|`@&AU<;s2P8tlZj71zeHbla(Guv2dNx%JdF*eSQnJaKOS-&rrN z9lq=U+xzofY5TidFWJ1{c<*}A_U*M7{}0W;_V@n}75t-Of7Aje+yZw>w{6>QKl^ko zM~2sg3uC7SxzKCxS*=nT?c&BSZZu zYUUF&SX>)&!=2JCmzw#BCe3UlDjb>LPf;_Un3?0+P!{f#ZobsaPcUib%cD@u`5?)Z z=;soNjC;tv#;1i2*UotItC%4^rKHgbn|*my3!rhSqEOiq z?awLBtWuYQy`fskp)STFQ)$gOa;N0F)Xa}x-OQNF!15523BQ|7M03LFX=e76UwGds zF_)V87L#T+l4*`8BB!XCPx&?Z{QSRe53lV%dgI&L|9$VJGvm7BDK`{{gIYlz z$?buv{pt1_&Ov0#xIugEIf&FA3p_-6xEr3V70}gI7@dL;iO?1=7)-meR<#>iH}yN6 z)=e$BRg(L2Rp$Q$4Rphm8+0z9%5AOj=I=6@T#e_8xlp;&9%!wrNj>ETtq0Ujd;S7$ zs1B|l$m(F;6&ea?rPk`*`O{LdeZw8vUnRLasA+#dDeWJNhENB^!MrxN&zolPk3v(P z&Sh4sgd2A+^TIm)dbPe#L+O-@O0A;~Zl3RKj-C}%xj&Nog~dmKlJgLu2u>m>HCLoo zRBQ75YkA5IYORQo)V%oqtX3Y!QXZ9&lgUxH<(dhYfyWGWGqjbILZKSwZ%irf@lx9O|@qAaRL~HGaIPl6uoN+Z}x5s_M z9rLChw&xnj^WmZmimJS#>UZ3+{@llj^WplEs_h)aXL#kW%hGUV{wIbjL=Ln0=L3^^ zaP{zp$Z>bthPQqY*W07IHdeJpk?Lw!DNftP)?y-B>D-68=E&lxI4JfD!@i<@hwY)F z=w<;@LtND?aBOpZG}7ne$}sq}Qoszcauy(580pd=D`$Z?8Z5- zDXCJeeZDhrg)>7s$c>6*#Vzuw*)$YyxQI$eDMk|cgnP=M!Du>zkzPx)z@@5#o-Hia zkMB>&+7Q++H060e*R}bzP*mzgb>8H<348vB%NGZL(z@djg@Rj%3Wb8xc##6y40;vP zKB8mVH98tAwm(#~`S(hSeIGjiR=UHIQZJvNB1h}7u60%~XV{fiSp5*3Yd)`@r2&Y~ zkt+uv{JMX~leNX-nVHOdv3TCCwsR1Z8th^*AB!vYcJ%+mVgZpHbM@p8`vSv|JNlMR zes5xM3;z(YMlJD=~=9}=Vo4r>ReU*DSVjc-rQ)K1rO*a!MH8^rL8i`~E za}SGhi^k$b*Rbp=acZd8jW;fgOUZ~h%3byB-N=-aIW;Pk{6n!nx=@x~Tp);geH2X> zJ?wxjlLNZsn#QP@B)e5UIZczDKHu#X^DeI(RQZ6|>9qs73!O64QW9LXnNC-I+LrRl zd$>zg@5|a^T$a_srK$S{m|VG9N$d0T|9#i~VeQNl4rd3yxBs>M3wz4$8+N|7b75Q2 z`~g0<`G}35aJ+K;6YKZ4r))3OfIoyk7aw=u9r^a#fIl6@wR&(RRJQyt%+_%7cGmhAEXNO#oO+6 zR0W6(u_n41E&LW?KS&j*i?{vQQ57IEteWU%w4Pgp{UB8!FWz?Ms0v=J+Ac+VTC|UF zSYmmv&r=E30)u)D;R>CSFl=^9dCw$I^b2DX-HeuLi?AQ03dF_R4v(rZXu8N;Ctsb^ z7%oGNy^`YPnN&GcoL0wySxUwtVz8MVrnzz1L^tCu+9K=+sRDNKwu7T8KwNp5=w{sZ zT7>-|RlqOawtrNG7{YkEo`NS{CG(AhD_-En4OA&rCSo?Fbldc}QHnCNWT@Blo9Je= zR$7Fe74o63r#B4wDxAAfFOGvvgQ3WLQbjM`ws%woDS@O)a>|RTsdhe$i^&mA*GB$S zxR9#n)V`RQIjhAX*G5~aiEhTbG>fpa!%2AB>WA`jHj(q@$8s}mxeCa|+jfttz}I;( zj!OcaaZl;sSPJ%Cy=H=lq{_KY&y(*3@ghOw+vT8OHWse73TjNu1l#p)qh1%|-RW3} z!c<}Bs0y)4#WQfa(J`Hgq>GHdHy*{NSWUov19^sy5;B^jnjr#>$eq>Ai&|S3j=|0b z>yuh6&5s7*q}T~3s-v;hDxlOwVta?zc0-E|#-;mElw2A9?u6Bz)x2u1TpVjrb+|}G z7<*Pkr z8ug`5xC)VCr0fp{BHf0hXuXHc(?+nuj4< zt;%I4K4>Vx{v;AV;VN___oy?D)Iwaj)eI|>faQr)`~E$SMHL=6-(b5$t~9F6TAffl zo*^n)s1=GWVoYP=LQ63wobH7=rZ6rFCB>{4tnE&Z58P3Q~VakP{Va`~?04#JEE+ffxf5*hGU3r$*xM7dcE&kJ#vZ&V>zj~p%a@~B_% z6$4ay<_?-LC$G8+Gr`La@iapuze7eQRfKIefvvUmoo42lt-7`-eN< z+qq?Xvh}vjuWeq~P#nLu{txR9x5sSHTYKN>J^o+3?J-B4QQ`bp4J`+nT|YJGc$;dr z(+`#UvR@c@21&WY`(7C)-{h}9noZEbgP40bkJ^!g<&Btvn~9cyb{HC?j$J<-|* z>Cs13@Nu4QwS!C903If4z9y>03mpd88W&S?%|@LN#uKrb?y|LUb9EIcYZn#AU}uBD zl-Qnel~FA2sm6Og)^D{66mv21sAUy!=ebs~6^~8w9h&cwzJRZqj!W)rG#d3~)6=o! zjFA zXI)>DwSP^@H($6=>2xlDCl@`BTvq3ik3Q$Lo@CiXPiu<>WrSI-&Wd`Jiv5eXojdxh z5bIgBO)6pS7)=>rXZv|^EC>DGT5~cP%%&BeXGNc^T<<92qURAu{SfX6_y`$tX} zM^im-ww_HjXBwxwm&vBG`M5l4^@q-GeZZPj!P;d8WrSI)g7#UN^}`KE{gBJ3!w@l0 zNw2GbHgh2&TA|rmEGK)!X|CNK^$VlGh*8v0-(17yMSY`v(0AJ@6V*UX}da1_1qoNXSoX6UBJVSs(?j@YA6+$ zb_Zj;E7xj~W|I)(P9K^W^n`MnnT4J1L{!QZP?HY{H_}yC51M1Jvr@X9jszmXLA98X z#-Tym@^KD#UA*mKM^z}`-KaC#^-c3W%vtYKwZzPaHQf!Wnxlhhw$9;YS1~1dr>IFU z;MSJ9W3aPDu~g0r8BbB+>WbH|N|ujvxOUC`(4#7dq2jQR=#9uWlN?ci&_Owa_Y-o7ZuCMjWpa$b^~*4Vm{h?CJ3Ah= zD`8KvpH?bDE7y_hiq)}zYsZKOA620~^D`hOhigmQ1CFW?%7;f8vglE!YD9}rI3!!81RAIpNBxEnL`&0P z$DbuQQOuOhCRO8_Wzq{q*jbOyos86^a=4;x@VzN-vTC^(w59F-M^)gYlxI@s6Jc6X zdfldvtA;w`Y%w%VqHJ|AYbc>m4egIRDc)>2TU*+2Bkb&C++m}!RItUdN-A4yPAs2C zarEM2u3J>$K66_w1dZ6Fd99t|j!|*0Y;4eItrtD_Td2CYNP?hHu2%IJugwH@?3-`o zX8pRj#CD&9z`=d@KfC{y_2&LhYMJ~VwD*O*-`#uq+M8`}m}l`@`^@gY?B2cm?A@%* zzx$}2ukZZP&I|0f?)>=9joaVWa`?SuTiV99*S6lj^@^>=7PGav`Qi0fZoX!-yZOY; zM{Rt4`=sL~4#|NzZ0jFb-?D$i{#s4NOFxSQ2hL>K-K~y& zxPnxLP^GAm@EE5M=u9+{Tq<0wbhxIR=+Fe|MD{)pB*fq}({u?@G1m=*qG4BuDu<^$ zHV|SZS3vGYoGxF#(jw$!ZtopH!h?+RS{zKN)epDa;X+_SA;?JT#Zt;NP?95JF%nE< zL@$?N_TC00v{ZT$l4@h>7E&r-3WqVZ$VDp1 ztj)wr7@cn;o`~3JbQp1VqoGi0Y-#8?vN6?u}KP+27$FNu7&yzA9^^1YXO^N0EW%eF!Ts5Dim zb|I_X8OyOG*TmByNe)YSpJvZ>rXv_x^*D#|_&y3G*pf&isxwZy>M1`r5?kr0QZE%U z#XzW_R%f%46L<5yR)IwJcNP)|s_iJ-J#RQovvqf+KTHJ5iH3&=j5>iRJ1EqPbZA-^ zr>W9-e+x*+%^)XMC{H-d5OE}15DKD-^+^{cQ3Gc+qPiQAP8U@tu8!^cMSdKJM1vJj zg!L?r+b;qV*7LM)!{=$QfimKKwW#cF6)84g zk@aPkn0Zv;Zf-nGi_vUNi>3Z*ueyI$z0g& z@1kCmp}nq1jgzrZ#~lvIxp=%4Vq+-Y8CA-g4*?Q^S)sz@+=Xzo#jwtL-c@yb3h7FB`#~p48a39JKxs9}eO}KQ~Y(Jp~KZofUU1TI42lV}OoaqLZzM8tvMs z5#RY6P=>cyK(W3g6$dHWAFL@7@9Qc94@&7Ntpyj0vqerivxPzxm3g zM5&K9U5&BVEz~19c}$26HxccIGX8!l%6B=9bzMx)66i3!O92U@s!kG#5!D!YMumoq zlD%0Flgr^Xj@ z+xK5cIA`sqq7rlfOANG_ebIDOE|Pg_jtnFd@i>>n-O)DDi{;Z>`#@q!WJqsIMyUWh z>$I!cZc&+(LNg}pj~D96I#Ox|$bq0@#j@jtK*IW^gGK0poc4Ww&bORI~FJm;$eA~$%S&Vkg7S$L%yC-#8}wx@t|B5R~d0yETlKT3nbK> zyUMC#q2%He32pLtOmt0Z4c4c4GU1-vt&|g8-Zzsd-hN*o(PNvHF4km3ZzRA~rAjhW zi?|W2QxqCfE$!^7HM+q@f{i}1^=Kf`iF<`mKQxucafx+R3NCk#a*4xEH9K+#@UkFh znLx8u6!NNVS%y-mYZUuXGBu4R=}s?G2zXOy4EMx`qftLs?TzqJAkKLsB))wqDKCRQm!gbDuKZ)T^3rSNIf$VGo=n)Ql><*LD#d5F~e+p0Z5d| zYHQeIGr>%tpsBlu^W-h;!JG`bBq!s}_RV>{~~inOUJ&b5|(2 zMrG>mzAs+)=InMLksByz7-3kIj8^^pY&zp-G4HgItu^xPW|?Lp969u$^=wIXEX&9a zixQ#K`)bY^tW8vmpoXDtuAG?F$i!$;Zx#lDWIlt=JWYELl#x|(t!c5yXyHo5IFYPW zV%hen;jLBc-RV#bl*v#V@$(rGWjFs8NTjn!F4GN9o$a)nts$7J5}K*Ov1jUQge6I__cx~% zXJ`@-B}Phfej8Zlom_Xs&}af zBc(Q7ER%ks#DvRjW>!JeB9U_k`Wa6O3#sAcgmowB(S+T8Ignr#r0Gq}FZ@QuNND1# z@nMO|dNG9U%h43w?>2lfF*2=V<*n&bA}UTsXtLoI@o1JL0}L%`c{Z?ML$kWhvfZ*1 z%XK-KQ^m65E+7${v!N?jTm4jC@$vml+HRsVhrp zX2hjN19Z$XsyjnvYBg_nOgHUSP=+=`W?aplJ3h<{nk67q?IC%UaZXG9zI!;$_ri+T z>127Iv$}&UC7ez#8A-$uc|@h61v)q+COJVFdXNbxv-t|v%?~RnpLPpn|MX&O=cIe- zDe7-B*;FG| z6X~4L(oPJvk1Qofg07O~*)-9Q#3Gn0>&uR5yq$DK`<}Ak3}mZ9q9?GSX?5qdK!VUr zk`ZF$^ts7$HORCF{<_=eOO7&wZp@Rcc1t-4A63Y-lD7RRkU;BlR%y`HOtD4OGPPLD zOSbb`<;w-9P#t!{Wja{aG^R7~X8BDl`5 zE#$yTJH-wFcIKJd~yW<&-l;ei=$F6^M z{SRzEzxGe-&s+PD?Uwc1Z7rKayD@mfnWvvAoVoeT{SLo!_-ERU!5xR0!wZMo2cJ23 z$H9valmq|4!}h6Ww{tj&1uB+rPd2+--ULiQD(z`s&u7ZM|meDO;&6r*?Dj>CLxq zJZ=4_*MEGSUjH%s7wvy!f4O~N=k1TOeb4q$+nd(bOtO&f?wjNk-36;OigQD9*obHP zf@WUyIQ`9uRH~1#cDGuM_0mXR%fIKt8g3Hdcb;Z@#@fc(qOl<|r>?yfXmI%r&SD?E zd!wfJnad8t0C#M^y}P^dw6%v`hSUGL&<7%Y_+|-6SWoTkAeRz>HXWQcyy=863DzWf zifVIKK3;02D{`huh7q1POO|axbB)GQ!HYme-y6SE4ksz}{wn#AANhDSFC*sa%hZXo{ zP-Rl3L^g*y(MV7BjM18AIwNVBRJieA;E(V;;f=N`ZE@D3 z+)Y=QOLq1^!cWwCp4?pzY?iS!sdG z^Sv`TPLcUZOf=e`H*$IyrX@okq)HKkMP(zahanWMkt1SbBTO{HFt}vk}t6Xp)i}d7*18 zG^mGBB*~@SBcm~GBcOvJjFy#C^9@KD-}Xd3j3%*Mi0>V1kS7>nXpL9K$7=d`J&Yoe zzIKc?T8uYt(Zfi>)#5zdu?D$W4rsT6mF2;sS55ozX=UV}zrG0}j!l+bYSU%Rg zv>t{KWPG5ejV!wj@^TpMauEa<76s1Oyo4Uc5ct&BZyXy7m>xzGShUDblE$|odKgKd zv66>v8DTf+VK{*Z$$DKj!fw>VFx(Z1^lAem>~VS+iZepVldBnFkI};roDKx}fRV7f z@n{{4apBZ#$R)GJw>?S^V{oz+aZiuE?U8yIjT8B9KsxqWp3}i-0>`7Bc0Xh+^nQ96 zhGS!un~Fx*z4b5@M+cgz_SlfQmmY@TNU1d%9n!oyKF^8A zdf`wHqcJ+(NOML8*2aMjMrxrpxMA3rH`dglhcOr-1!~QX5w@;}(HQRagx$wRmR%2{ zFl_2g3B2)bHX{u6`7~;X5w@m>kr={BQY>bGIsQurBWRQvPn3wU>mA?J!zh&2NUA<# z*E_ypgi*0kIbeUgn(p3 zeO`~kXum(rA5-jedK5|(TlK`THv7kO>%48L7lNbOra`m18s~+_89;b7lnWb)4v*D? zNK6tldAC9L#~45>J+U6G2N6hx8+DCs^{B;;6~(o1l`%0!k4tqmU(o1*nJ~r02&xtJ zwgh6*i?u?9LbltRRM%g=yZ*K{VeL(}OZI>Jv(`Uvd&8M6`#TRmqMhjbhqoObuHQ9h z0j$00VB&b-B_6<>1+e~bn}5y(SdY)S00thwCok~;j4S~Am$%+;Kd|3A=K|QpEyi}A ztxel_^TP{1z^$7H8=u^G!^U{Szi}VOXB=;K+~EkXzs=@c`}_6ZTmwgLr}ewM?Jq7x zAZ+u%lmKI!Utcu4)4FYbcWF4YZkylz9Z&{@ZT@F~gmv5e&U+UU2xZwgw{y3NVK5Kg z1YyP$fwmBLcDDct()vx}BY;-c?df|vSqS^rovvNo}!hkX` z%WQ?crU&Bi`#{hiicGaRO2Rx$kPEP}iktNhh^iK9kmQoClB+XJWD>mlD=*tW+QYabra+C9L0Ab%8{s+{{LO!(fJQYV&hol406Gw3U@* z6^&&04qD`zembpsOKv+D&DtGTF(-90d{k-pgITW4@>+hqRL1S{2Q*fHT1izk^V?)P z_V~AcWm!h7)9rV=p~iUVuQiLT8Z4&>G%3_7J}DBOCZYwl9V?8-jkMzlKmvk2axYl+ z2(TU_5bTi~fKt}kBU>K>5}9F#!18P%5++6kj%al?CITl4NEb^CM+0w#>V?L>_AJn1 z9S;T)5bTlV{SySU<0eoB1WyB4L}7RuF9l^la3z4P3xVKDymL_o1Xp6~Eei<>pX_d_k(&s@ECp$NI>uymUKl39>X(18P<6Ww*LntAb1SRjSK{j;d)R8 z1drjTfCL1O;g^>Z)_DwvK*BnY!M3!UTjw!sfQ_>gg2#{qT3P2YY=3$wVV%ct9!Nm& z7#_5gu+C!un+#{rIxFFIpbQ9B!mU8UIxAs&Ikz*`SqX2w_N;^@JqCi6@Oz-*5FCTo zUwiw%Z=n=~{eSDDK*GBHf9pNF_Jg&;+6OnbKC|`atvfaz=lGVR?D$j1&u@Hf%3-v|IFsyn=jFr``qR^+vjY5U^{CIt$lFqTeg?jI%ht9 z=B)e(6vTav^t|`ky?5^Y++J%hu=nuYZ|#0?_t$st+I`Y4xqGgb zz|Xb0Z~`PT7_tSp!GYmfyjfRW?W%a?9lPTX^e~exh)c+*yF?ZTz6#KQjp*X z)!Y)}u_W>0hLup-#s&y*3`47RCgCfKSRg?PUvinnBQB zwmt*OfS|p+?()fkpg#ptKJp)=Vs8Vr-_$yCqyHl4|qAsGfO))E1uL2ly_ z`a)?2X{N{BV{f}b52I+8pv7f6Mj|O|~=}g@WZbmcGZDHrI)ifiWshcsQ89BbC)6I-#q&H488f+!aNN=2GP!pPw z-Z;&mCNv|xahgF*XhwSDG-F0Ha{QC7ricm6NN=2GG}ua-k>iW{x0%q49AD7GOlU@q z&+B0(G$Y67^e_{ek>ekYu$43;$3N&{CNv|*XD^4D(u^FR(Zft=MvhPGVJ0*q$KUB; zCNv|*C-pEBnvvt0u`PA;$-GFf;y; zoGf7OBEol7(j5u4ouyJP=|`o8OXU}MhzKXEQ?$S+_+(8(LyDGpV5OtHLzky% zxncyAx&Bv5#d9pM%3SF3EKA=n-;m7cQkoVESz6|=yvp<|B#vQ>CcZNLO0}J;;0lnc`o9`w7~ zrJho03<3h5YT?mnI^VyLZuQ4vLymO3{d`4>!#ay48sYP$2@N@H)sVZ#hY;)-Gz_6`XRM`WRJ5d>$|8B! zJwJ$sT6##30CIJ}o#UfW%VCKiiyS?3(SjsbibG|d6cf=#!}rsVKIKx9&Sk1Ra{hFg zw^LW^#p{lq)#~b?)1MYbb+ulqEA_!VBc@j15yF^^O-qV;*;A19OOqiNIs$zRN`mZU zM~W}x=ZQL(uWD*O@t1z@%TlpCuiXN)Oa!Rab>0A%CK_D({R-)zevpX{+xk#4ofU-d+TMMl~|r(43=u~2QP zL{Z}1m4J`n2POP`yUhErXa3?g;pg-tDGhaFvpU3wHhnAZcW~N%P_O4Ek zcF(N4`t)e?Re1h&TS?4^qjTiS#C(^=(tLw;g|TFHqSD@UwQD=3Wy#gVR}MKZ-mM_v zRJY?9mt0y5)1or-P;7@ph8I*M*Az!(bePl}(Np8AU)$LW6Sfmg6{!r9Nrle)(IP>* zT>ixDyo9USN}4{8RH)owELQ?WPqamx@1?smnv{h^qDAm6I-C&``B}zS2}@YLod~B# zT+(B4$Z1vAhTKm}x%rTL*6lW!PxXX`9CCcftr$crhDBLR%M22=-t#NzqC_pzvRVgu zp^5pozEG(1-z1tozP{d}iCr2E^LhUYqu~cxSx_9K%|7$6Fc`2(kD~qgki^n_C`$Y5 z7cilI!LLl2|Bt=z0C%Ih)|F;dM)f2RLdOMDjqFhwX+*VpuOp2L0U?cg@68xu0wEy@ z9TG}%6N)hqzywIh4WV3z-b@L-2M8ena_JZ_Z`6*@(K*VD@|?W;-t)QN{XU2NuYavs zd+jo_m-fm#T8Wq+(|Fru6Z%iDEGyGZjlCR7CH&-_X$eF@sA}H0#$#RCt;D4UnJ$er zldyYbK-y_mI?1FZpU=6QS~wN74T()PMKQfF=(uW4KX*|dWYAjqql|aJ$kSH zA1A{wGVW!}+&**8OngQ*!w^3rzEqqQqoNN*cZz-}3X2XTs{-CGJcIlN|6ziyf_urc z`KVwO|8xHR{2%j==N~}+a{n&gg}md)Gx@(lzk)i@8i>PvhOF=^@A#d-^Yk7~rFUF< zR_{?%ddDyFouk)>9!aHlTzXdT5mb7|FX^457ef!H(mO6at5;2>ckb~nio)<*y+@&a zX&!H4Sjs0x2X8+ZRZ*EeEZgn>~^|3K=}`WRu5lr``U*oL%gwf=cnY_(<_F z@}6QQ#GL4>@o8hbVgL{yde)j%eF7en2`)YdEGP%FodALqjLVmzQVQdx;}^S+j7MQA z$w?Ckx|&LI(iA|>>0I*vQ5gk+Y}{|NMoTCp7dUKI@-QmNN#ht@NhLXHB%+5>NlprB z^bjh^iNok%D#=OFj2=WKIjL7v2l7JyaN(U4gQ^241Sf@{Y6XShqzF{)Pa!yI6;Umx z5S$b~s$~>{lcML4rM#m?UagfoseH6Hejq*y?t_=`ju=5UAy zF$Sip7 zIWpqKWe{|-ZLny9f2HFxqX8-tF0MuXkDf*_LFOMkjh4^LjXVuYtBDl($(bBm{VtWk zQZ@OtSr(C!X&dnPtiRnOFmLgvTj=@G)4|Z<5)1oxR)V{@#N@#Sa~79a*iW-M*o#Xn z^!cm=Yl6fAkB=l)f;`P=qM0}lX2HOA@Weqj0@R)whGGxFxX)5tgfgZS_8Z{m0O$MC<-dz!ql zkKi2wy$9U_t%WoYm-{gHJZ^})g!2Yx11Hasus>km%nHz@g0dm^U!%Oa{}-ad77J)27Pxcqk2EGpIGo009R_i_U0dXP%>xcqk20F~D z*E6Y9kIRo#qw5(|qLcQ-N1i+|dg2!k)%X@GT`xcet#E3hV zN^(-fEf4cnkNi2YpF>pa6a62glANUTr~t*3x}e_Y_9?Zu?%@=Q7l@Chx1;=2ipN89 zzG5Gh;&Jg=#Tb?1@mSri7}`sPdZ`qTi_a?dP$?b{_3es#u69!?9v7cg?4nXUK2o+T z=InAb>ZDRUE&%cSO5X5)ut*?$SNOQ_*TRd)U+@z`vrr=3MevE> z1;Jf{4T2vCiUN;7A=rbw?f+%|f05_>UojtNUde0&FYzzrXZT0*XLzsiZsC28cPwue zdGG&|&{a?uazIPDA9C;IUd%0U*Kl{`{F(EgoF8#c;K(@~@>YUhvQK7X>;qY!u^wXm z2dl<1vi1c3M&3DaE|>sOPypOW-Ubk4?8}xj0brSQZin@M&kBg{5#7UdF!oyfYr{l2 zmgqszgA-dp(S4#`T|423bs~~2dPwvT<1n_TXP!oDjwSu^oLOhh)f}~`zdrVTlr3H^ zUQQRx5-$@kqYDPbOT|m+f&uXo@sgEn508$`;{C+?jfhK{W}(<}Q8zpTpOIQ(B=rV)Cvan1S3ZC0o!(ao<=plW77>CS5SZWH4tZ7%FBi<15BjbeTEiYViqOFq`o)<72vD z7ULtvM|8m;<3q-Wbin}Q1I7oVKB4jk<9){aBVv~!Ucqf{V?181#B6TY_|(M_F-5a| zLIq}v$g5T8GP8u+gxlzXLE#s|FX(~+;a1_+ku#`l7Je%HbVTg17wnZxS8L4W%N1KD z<{k^s86NpZ7cAyMJcuq>#N+a~biqO%hsU7{7Vy|SHeE2E$KtW*f_XfU2hs(TpQd>L zT`-r&hRi|)x4Bl+eQGwaeSJ3Q4Km}$&+o0J9r2>QGWldkweLw*E0{UVUp;XM! z=g{Z-u%*47)Z8y2{e2!Nx=M7_q(*=|-MV~lwzRjOSs*F8OmrF91})n53(+rzq;uah zhZ^}J{;+ZGyWp^qC*lno=f1D?8%4r5g>UXfJ_wV!Y(b;&{r4v|2|oB>QWO8f4<|M8 zKKf`<6ZG-NlbX1nd@`wt^XaFPn%JLxHmQlVb?c-i@bgi(F7O6GRv~|F*mDaSg|7-< z9roP9wpWC&4E;a%wYBemmhckcC6gLK;l;v>Cp7}X3xyXB%~?Pzyg+!t(42*B=L^r@ zhb`{~;KFwl(cPlE$;Ms;F4Fi1(H}-{7jxeVhher^w0UAHAi6_z$4Kh}S<&sH+eca# zHvV4p`w{QlH_Czk$jjV*J+T!O-6FbWVk;oJQFPNt`i;+szC~3}&Xl9W{xk{!a8wDZ(pH*aFj5`IKo53G} zCvcACtm3dh2t=5B0jCH(z;nQLOoRAt@lV7#W>o|VpW=LPro(uLaVPf(u9*D{=T+7> z*w=E?EDi5PvdZ6yGxnJ!;`afJtj6~l^I`GP;;)NdC%+k-Av#V33s=l+AioRf1mEPH z%G)G(K=4yRnfV{w|KeT%H6SzpFZ|#0&*l?+C7(C*1hfzLJ@7wyIGJ1U1@tI%8Cj2T z8S6vv&#e1cmyia}{_$QME-PAF*jg(Yo#~9-rw=)a{CJ@smN+R+(glO!gg9|1`G!tT z$67sEs5KoYY)NOUt@G)y1w}CkV#I5TSq%lN7q^=7VWTG&9xvJff`@{K(gkzDRp6=- zIM?;M$jYdiq|MR|l?!fsZ2QFl@<5(9TsE~ygVCbb*3;=+D^W?NGu1IP`yuv2BW5BO zPNqGnD$&U}(guxYyu1(uz66e5y0uhbF8Cri+a*+B4)_B2g3v?P&+K@7WHXhY0kUG{ zAtPr{fx&}8vI-krD}ZQ}J{XL}h9+yMK5cOLf8zg%E||@KjsM!n8D(F^k|@*y#;~R0 zBU0A!DqSFSA9NpGun>9-dTa!CJ3F>Yp+%HUZA;8#43C#U0mWyF&!!9Jh|d$B$3BVi z-PytWf9xM4lc_u>fVMze=z{s=JoPADFb{eJdW7@6o(JgP7B?cN>K2yx9Pv4$=%)gU z&lI0|2>D%)&P;d02}2VoqZ#7A!GD7;n9F~i|N6*Ys(D;Yh)2^cr4=mxtNd5#dJPbt zB|d8erm}(~PK(oY!EA9#oEm|t`hzV#SA6aWOf~C6KoJ;yLef%!xj+FZjF_oHAAs(K z?j7|B6&Sh)x@Sa8Wd$E71EbGrlqU0l5>Og3Q(6H%3_VO2%!M9;W|u%JW)Acq^dMbk zHuM1W09`N(x*xiqG4eB&?|>9A8%LB_APuBP#8fLW_!Rim$mCL|n-4g1UU!iw>$6(@ z_;!a)p3q!H7tDe-KpW_SLFh{8O1fYGx&pdl^n6q{LzhFBkBF%pCm`>-9(^39+(?Po zFSB1BF;nfjgzOjDvzsF-GuS{5$c>n(z$`M8HcJ-_0vRAP0#n&6V86hAfi9TGeun)F z-Hj9{kr?q(h9~$8_zYbz06q;qJpxnh{dgb@!gRq9xEfqN0#i8-Vn4;69aB_bF8fLL zlXRIm?8n)U(*?8HTiCN(7%DSZ>_^$NTNo-Z$bN+V2wgJ(_QUMixtXd{IG_ZSjF_pw zZ16DfFuGtCxDs4B0#ijD2YeoUo-UXTJ_kNW7t8{m1)m*(skRz`I4+)j@uBJzKKnWL ztQl0ZA_v+CZQObOKlU0j`eL*oZ(!&7|8Tjb%vsoZ{+~~mYW2f=zpvkU{!c`=dt>MM z{~TxRJpZ5LjGgEIbDgpC{D1qLvGe?Y?hLx~{C}=DcAo$5JpUigoBcE2o#+3cXy^I= z;XBX&>xV0Lp8wBXDR!R!&)Hw@JpZ5LjGgEIbDXjB{C|!!cAo#|OLOPYo#+3fuX;Pr z|L4ZV&h!5{lg7^T|2fXsdHz4g89UGa=S~_s&;Pf7;@o-uKQA(Np8wC8HU7ot|GoSF zB#e_8GaF~z;!njt7n?R|MY^!2D!P>}jgP(#w2Tj1czy-jO%s(;DU`iR!kktR^4_^^BdbR{|>7}FRA#9zOpctH6TF|-z6_tlkKm+a%e?Ag;!xwmr3jZ1F& z^q}4IOng13Ppk|jJb_rkUJGTju+r7&CNh|`l5A*{NtF>*XyX{iL0#dg!M8> zrm9SPyLwqplU1fdDwE%mvC0W`)EC!vq@DrsS6%0)Z|m+q`{S*LZar-)H*7o(mv?J*R%S>F%{;wcw(5W7V|}<8ACno$3o_iLWOw^5u)tQIh6id(;alQ zhyiiw=JS_v9)IqhC%y`v`s>|xIiZQZvqJyn1Fo|lIsaE5JT2$q>%l%TQc3CbaL{Ck zweW<~i8%|2cw3e+=MZhEQmcggGBn~cCna&^&=qfP1ZE8PeEqiF+D~|ush{4PKK;D2 zgV#Lp6RdO$+S=B-9A6I%d>oC)Yzckc=ulW2x@@ja=C^CjRa+-pv{*yhSga6}R4R>V zBr@{x4Idx!$~)h9gm>$~uiv6t%IZGwfcm}zzI|*lx$T;3FaGKSd_A*IOjH!%nze-? z>23j4>rB2Xc{xElQOQd3mXaPW<&nI{9Blduam&CJ)$8}%bm=Z{?(^mcTfXbO`LNRN z*PQhi^Useu_{SHy_g(wMMUUg_8GT}BR*kwf?wCuXHsJNJL0O4LamY1Cty6-U{bkgH4W1}aae z^Kyf|+OBB^_STy}I%?A%zk8|kHu1$V(NmO{AA-E|<>hv zljrF_{Gjy5t+y9Gd}SiWAkl)J@{#SZJ#&f zli<=OqK=eIgvYO|6nwsbDHoJ>Q*yJl9Fj*g$xuSCjC<3yfghsBT=3Mkb=%fos#$Z* zMT)2PGJN!i=-vY_RDHpHrhW8JjeFzWK5tP|i4`?5YmzK%sV*l?!D6!O>P9q{6gje; zjfmb<@c0aga@{=Sy_A2+rJVcMWq$ebkMB9Y^}!Lx+_@aO_O0jd5N6J{o36fjE#B$# z`Whuu4Q(ehDzq6=dBV|JD(2J{!cKF?)z;}Tq+l*+N(M*Nks8?Re4BIAL&t-6tR`;W zGkf3Oiajq zTwe7iVybM#oKT_z-ld0M!H>NA!C!#mS0Q(Pc+KjI9`Sv7%Pn`euG{^RcMm^(?G1RV z&sz^F6A^91+{Mf8WY(I-^!1Rh)#-MvaIw@*H);iSIaE{Qez#)4d&@~b`(nwx4~JbR z$)e^Xf-d1r5B>C@%l9b$s+5U6>UVF$n|)q;)nhm1()GAk*1?R;1fi^kgBn=jtivg# z(H8Y8iy2+AR_(TiaklDa!CvM>Qh33IRNSqqJ{a>A^=(T!2sc6jOT3}B zrP8Iett<)3+WsN$?kkq!#BVo#u~O80;?g6{r>$ERjqUrp$AA9a?^&;U*p-j=1tYwS-L{%Ni4;PKU`%O;=-aTQNC~g1h zH}78Z^t@6VpMzl{0tYM(dL)uD-0$zt)R>}b8xB`e8l@J>+g#3~-E zHw8P1L^|KWRBG>Vy!`;at;>g&nFa4Zcas5T1P<7Y9D4Qvdnk~5fAQlLzx!x+ywc}2 zAiioq5e!$`B}J)NjKCFNEub?evv4xn${0$RJE~A+s%d+6*l)X@|JP$rd-jIC(91es zJ$}XCg3De$%ksO#|J#m_sh*Ic?8FZOw1B2{bJTW(9e=rNg^nWU+pY$}Q|nJ!@T z!hU5v5mVcP86R2dbYSme_Z@cIZ8OWC-LgmSwG%V{dB^vkf98PGOoqd6dGkw2`@H*p zh!^_2Z6gt}A+>msD47%~vr<#etKz-*BK0QW82Y` zm|E6|GaD$HljWK}BFPQsojo_BGhcG9G;BWNL~nf=>xFOG?|607K_3v|%F7$>K4z;K z&-HmL>W(|5O$AKysLnzJ3^G?ZV8^4Xf~Tbm>onC2E^qs^Wq&v~^zXGVob~NJzPWev zlx26H`plcS1pnPdLLk0$<@b;KZ~I=pqz=#adFwJmTwcj~2ZQXa-jN~6?X*EfUNuEeE;9r<9gAVUjTueFQ09m6>LIqMz8QT3CX z{>y*SSheN7EMzZB2Djn`m3j(yB8lElbO=QL7~qc)=i1CZgWbfR}Z4xb~`K zsmyxE(|4`i#q`uok*9u|+2Ynz-bn8H?$0uN;mJO)%9L;;T1%i=?KJW^IhwWFl1a?! zPIx@AZnqJthl+)m-;qKs!+Ga#sk2_+<(s$PcKwBpFMfFLf2>>X>ag~H^oXC_^8Ggp zx}|?V6;JeeQy!lt7?y+(%xnwiH8r2Vo_1E-l8Vw~G`DoIu-c;_SEaDiKU^=?v+g|Z zKYsGYvImmx!yLA#^0t$%{l#fZ7?-4;L@#|KggEhdpV!@1W8S<4ad{JAO*<%6p&^yS zsMW*mP>!5+3ekW*L=@9Ta<&}!;g}^}qgSoo{UUI;kKViI2-vNA;+rdWIdwzkglDR4 z!=g*)N60VPzDhN4;vYKvOL?eN>+nB6uU$NT@K5q?)cjb-c6!*M`+}G`W*e~%|pV#DVbgR{DSW}SNto8`GWP~E6 zyj_wqG~Lxw-Id6eY#vvxUC|HwZO^@Ly8hN@fr53#9h;B)^)Z)uw|(38==DEvb`R&T z`HOVp;dr#qi^pQ=rY`Tp<+`Xr*^#tNGQT7(3&nDgP8E0Nkx0;!?)Y0W+i?E%|N7P2 z4#5KDi+BCy?ROT=-@5Nlimvys!xDe~Xv^sbz|UNbpU~$uhiy$9iE8tmn!PFwHIPEg zD>Y`K&1%+ZlEylwk|C(D1_C%ST;KLR_lCfQ>wmmba09;T&E#WG1~0z-)`$1T-`{Kf z?yp>Mq5n=i(&tq~%Ql@PnM5{S5c9zkBU*=s3ZX2i04o zV}q3|aQwJFFPgIy8fKj-T8{<8GMn3s22E18vz%7I`X+dTYZsK5y*4 z?==Q^#jVeO?aa4IuWG+_=^5{T_wlb%d-dM`S->R>;fc^k95MNioqsz$@IS)?-z}do zDtbJ{v_gGh!`kKbQZ7-9Wa4C1Yh|x?b*)a8O)W%9)p(+sDW=H(tX3_oz`A_J6tmFs zMe4OqPcT+3eet5lls8oLDx}sDMe^auCtJRFwK@6R3txIaT<8~XEfwpDVz2l&S^8j)0uLQWU8XavL4sJsOa%xX8*HAkCh5|Vl`L=T2@tSbwU&JVbSDjf4r$)Sub}> zCb`~M!CmfJQ>R*q){#Q`$Lq>!s-=)-8p)#19kvETs$4@;^c9L>gPvZ|O1eGZ#6+N~Rb`xT+lyyeA(}4t} zje60RBUbxzupwJ9!is{)Rq}=E4OhkRPZvGb$lR+HK4slz(EBR}8~NeG5vc2GwL%9+ zR3R+Zum>%Tm`s_;<9SWC8f>Lk8};Sx%8(3my2C^(nJc7?o;ch#D(rTBW_7_;c4^2` z<2@HPinUCtnCQ*yYnMk;iAcRh{!@yOrK}XN9O*qe8Rn<6j_-x}5BFxSm|_FCgKB#t z3&VW*ijFB9%Xd4LWVC~3BISz0V3WkFegl#1R0D)7Axk)5MXu({8(O`|befXneY`d9 z)3!4XuT2%tSIRlPHDxZff>Cn^lUh>sZZ>Z=r=`_a$>%^9U%95((Dn%ytX#Dk*&RJw z{{4l?W2Jbfw`iFY)x^?FZD~E7SXxN*4O&VJmpIa}LM^r3>S237$VbJfM$s;ViWj8hW+I zdyn25hRoyo!*Dm|iYdZt2Mt5T|CPgViub?8hGCkl^jE5OBF#)K6U*c?_0Ip95xIyR zWJHp0tW|RAp1Twxiw?P3Jz7sha*6(^?mfXjI|k({wSp8+M{DWf&>6fn_r^!Y)`?~d zHmg!y)m5FS*%|j2RyO@Ijd``XfE9f@=gO3NWinU|{qswqKUA>2W7&F(-;4@6|FU0{dAi7QzB=7pWUKkRx1UCr60ycjW z|5!eUcO&mxJTCMr=r{=C-o!ng%j4Y4iE#MrTi7SC1*}_G$FsQLCNKm7z_oyn`6Y7$ z)5Z9dB>&&}^Q{-K08fJP!)0-Q(Qh}}P4!e1kGIVAVt4FXyA?>A?OpEBzuBSmeMe2m_ zBRz=*=@cmw*oi&Tbu>t)$d|y@_DD}$)@l}$ zHZ4~5=abGtsFcX$$HR4sWC^U>BV9{_bc##~tkWay(jc88O#*B8NINu0r^u1OT0PP> z4bmwRB(P?Ww6)Bc4y0VZP}ku0wlr<8r-_Ys)D+nfSffYUq(M4GY6MpAkv3?MPLUUZ z)q13L8l+PsMPSt)X^jTy6d4g%rAJz&K{`b`1Xk{mR+iO>jTjL*7U_|mK!bFOV;1cA9%+R7``0udwfXEIS+ni{ z2D`{OK)4UH4&2ESLOjmzMLC{dd=G!{?tY4cBRw^`1Xv@VTgIYyO6bsQ+96rH8Il>{ zXh7YtnEL;&MNR5%TwhYS^qQ6~t7$oW&T`mVitNHbP&=6hUmAEESRyG$~Hs-qB>`}$aebZCz>9CX5ctl26J_ieQn z2wA=uD;VK;(3-ax>~Oamsi3Y15o*BUR3%|@IZUZI(Foc)c_o%e7iD<8QjHk3x|CF= zS_PB;CuO{Iij}ka)Z=>Y99y#gW!^L`>P=UR@!F!1{7nWAx#bbVQ|UB&=qc z++EXZ$!}nUZM!c|pY`Q@CJGj{Plsrx^(Er0VfI!) zlPutVBtoX5NsMv3)?2GNRIY*|3L}wj#no_yq@B6G{Qj&j|81ju(YW;QAYW=a4!zOs z)rUjQLb_|sgmcjhBA?cm?l6*$7jtrBt&U-3G?#?+sd~oW49eRzYf_W5C1fpqwAzRV z(K&JX10jd;&4r&mw@;-1ws2j1B87Le~cLkX-N4FvHdo^fW#-@3>Hss6-Hed{(UPTFjY)8#hUapQT*p$LJCSag4cNKWw*IBd(xL%N z?4W?vn!R#k9oJhiYeAtVmq1q_Xedk@uuXLkvBxqYU%{FWo03X-$sR_sWp^f}tEd}I zSuI+MX$t;I8f(o7*t2GRIk#}uzu%Xk9py_$xS02Na6)OvN-~EuUJ2$c*=c?0A#iER zFX_5Gu)}0g!GTgnR8!!vpX19P&H8d~iMW5iFH<|pm&&-@ z8F0wL8KUUzIOKV6%#@#cfhVHbMxf#e;9+Z6;Z!ISFcETf+If#ck#_1j)od+7{-UoO z_V`P4e0k0(1n0O@i6zX%)7GT$+JZc-nBV$g&^vR#d4-x zYwl|G-YKidZT|t}w*UW?S;47KDFQw#;=#Q^zbS`!f?->Kh8^45H|^ab+%m(GTCx}@ zN4+hK{Pjr~Zq^4WV(!R4m)t(BT)2f=^uL^fa#W{EN!Lc>g-r3^ z^j76sY%^do=3;?-V@CySm`wK}Us3~vlt9AzxF=!G$pZZuWb9qkT&eqVieOqLafM=9 zWkaIP#Nf8m4_64aTVcVJK7}M)4ZCWnbxu$MEBVYVj3DDh#>^Wt_s?86bIweI@#0K) z=9rlkGd$)B#%D|yv&*~^yjuK{_$Kl9#K(zcVy@@~vSj0TMaPO@5nK42@CM zAt-o8aGl^3K|pYrfXRP~e=T`yfS-RT?F^YC?WO_p;SdqKp2z{ zpw#iYzj!ygtgAs8UFa%MN*5{tVY<-6z}0l2E5TKCp@)JJy3j+w!{|Z}23H=;A}fMY zf74CGX#(Q}O&}4Y35-Q)0;4C;1fCEXIEb>?9{&x&GP=;$1xx8d|0GyK7y6oDKf2IY z1^d#4z9QI%F7#!=-gKca3HBO7v)QV%6)d=Yx@yDRj#?{YyBAk%U1$sF(1kXEHeF}~Xwik%fhJvO4QSAXR)P8u8ZuU@g?3AqH+u4f&lei+En!vo zGrG`<@TYX4W#OfCp(WuZbfHDz#Y1Qy8N)N0a5-AFWDEKz?i&k2vJf(pE|kG!(1bF+ zVthpx`U&F`y3mgqAJc_?#Q2CV^h3snbfF(GKA;PIpYi?>>N3PDxXo>h$E%f?&F!*} zb%BT}8cmv%P=;tUX;MLj+eD+8kP`ZZXf(Z1LbrAp$>b&UdeQ|#$3K! zv1MZJF%R%~JOf=Q#M9G-a(OzsP!3N^7s}>o=t5b%HFTjM?-;sJfOj-qD3f;-T_}Th zBuyyv74HbT&~3cK=|aEcsp&$$;GuM(pYv3Ev83etpn6Ws^{WVot`hy{plR+mutBkj zH%070)7 zR?efGE1-44{e@1*%R7YeF5Av=a`qRk6>SiGA$klngZqL%6CVWD_zeEj{9p3d3p?y( zd|Yt7;5&k#Kq3HyR{-y`tlYI+H}h`h#n3kB3E`v65>wCI6Sx<+l&rdH1ombv;U2*K zg8LYE!;Fvh;LIPzUe?FFKk}{-ubVkUyiNQBbP%*rypiYQ&;2ko=wvNbWo5Fk@~J~8 zRT)LHcIOL2Xth=|*d4KG4eR=hWlOLyr(b%Xo0)c|oi5bIw9$oHnbskcsyH$92#-$} z`Y?|-gi;m1Br5_xN7q&l(?b{PX1eJ@T}&5UsFUfW3w1CZLnx(R@8_|HP^$8~WcA%V zU1$c#(S;U(5?yExC=8*L9s3M83T;a0)8O+%R?1P}WqOBDO20n9jBnAQtD!gOLVpRpH8g;#m?~M0`jsJ+a&~!!P5ZH41YaAn zQufvh;H&hZBU>q1p9f#2`&ciqN48RW;5qil0LoeWJn?CC4S1OS1YPJu?8oUk>m~3{ z1E@}8ih4WVX0?>od$qM{z&y4Vh@TeUKV+pG1rPBcy3hxCT)GB4Cw_=7^jYzPbfM3P z9~eSy&Sc$Y(?wl6+}|Oy!p3vXXN2Ny11M$bboS$9t?>Rgcxvb)>?i3OAQBH>DYcYc zu!Sca8bAqslqaBT>yzx~$lVFP7`T@Q(uLl`1BOsa4{U^9rwhFb8csx7O4bd~ zYvPRz>y8P+p?#E-`lIYW(uHneKSlScFNa185M>YD$72myDWQ+?L_;X$0(6#mJzeOT z;%^V3l&4Q=ad-%&^y`c4QNL3{UuF-tHd@N@lm@ax11O;>AVU|L2Z{qI^)~EU=+AVa zH$ZxKRkd^zq_6z{tjK}+2T`% z22d^)*FbObNlz9Sul3IVADFpe=KPt?%<(h2nS*8o;`hW)h<_u#M0~QC5Szp+#WSK0 zL{E!;C;AW3cSI?XO|)9{4dG|PmxP;z8--^Ho5JISdf`gpE`ra8~ZNyh3p)A4SP4%+pIfS=d&`bqglIxZ-KuD&jnN9k>J;X zH-X;)X9Ee~aA1b{I`g;8GnoVvWr`TXJt_71)$9D>OiT^^6Mr}&siCj&X&dk=f4CS@ zv%bO~?w_flFY|}_so{o*8d?z!H$>FXvamdi0ZIc(!r`Y3YSyA~xPoYQ z9!gPpfbl=zP>MN>0je`05kthF2^DS=4&Nd*RII`;gu|7Y8oE`um97Dw3O^k}X&g!o zzo}{{q0s1^pBl;?z4KE;Iiv4s)KK>5lO{EkHQGy4L&4FD5j7MTy%YCQTmm^ z8$}is6#5DpejcDSfIOfW1tBHtm(Xx0NzM8NG>TVB*3Y5gPLfX2*Z#wJ)tX~Te>`W_ zS#vct{-xH|}hheQueY zyGL}-#J0OdcTa5l!|W;YVkgX-hjI(s?hxHEvF&!z?GxL6{|`>4e=Yj;P;TLg_AR1Y zCbr!ux^ZILCefyeZ8wN+7`81q?iO7yx_)BYHKJ=Kwp}f{dScr~(Z)fz%u}aOz=(+a zTck-wUQS0Iyr(8L5kg{8Q%o3})D#s)7ip5JQA9K&npxN+SILx!NFWmMSvB(f|1LnV zRq&W#qu^u#-aETz{)AazTY){9?=m+7e`o((uu|-0YMHw;{>HeAyOsMW_X_bkj-9iN z-QcvK1IQcz_sq%UiT%Fd`{2FMW6%cPHr^Awjl6Y2r*MDK2HceJJs42fnUjAeHO81vyC!9bx zN}r*h(DT&n`9EclTQx*eMaMz494F^~dA zuX&Wv!fbvmrL!J}M(YY?$G$*675DksR9wor`bG9I6P9|hc$xij@BaTKj3{H~WHP^g z9r@n9R=8Ez5qv6W@jvD_$dmdSc|uoVv379^xN3PvcL6rF z0Xw)0yi*&ngS)`fH((0V9o+@)sSVh{UEt~)FvZwjv4ah8_6?Zg9_tlTT>4CX+w)25 z9a9@H#RbuH46sjaz!Z0B(=ou-H(-iezjt&OSf@5%2X}#GY6Es~7nu77OhLM%yTCNH z0Xw)0jC}*97~3m$umOg?0aIM5zG8~|$Eh!=IcdFqY6GUYtDKGjx~UDA;#zk)259>R zOmPAIj_v}@)CTO}E?6_Q0Xw)0j_DgP1?i6Nf}^K4UQGElZ7~9iSA*UV%d~Tj$ zV{BruUSn;Vd1U6NGdYf!3vsG5>KQKhYw=U!E7;eFt1LUaEndSq2|SgtmHiE|n7je- z8c`ecat{|7nI8i0lD`CaO}Gg-P5mJ80QMXUj)AqoW}ew#(e@zu#Ep6|M$$} z`9A<|1K;9@_y;l12AA?aV(rB?a@O!R^UmQ#7@K$!_6yMG(EZSlp#&rc&H#d(r^p)u zf5y#&-tFHPb_b=vig~#fy@U$E!BQ{*tdL})I=flv%_WI~C0R!0eyc5&L{+g$q>+xx zF|4K1s^pcDqOIW{F_5q#@uDh9AO=MYwGlJQ%1MT?@C0Guu_8) zek11fc&wOQ<0>jD?y#Rbv?{F$d5B28uGQL;jZ{M!({+trNzJ0Jnrj8QyW=eQ%XPCZ z70_`1FwhY&b?}^;ykk04Hkw^FbxBFsIu2Rg>5i$5iLzay)>o4SMFI{!D2J3OQ$mBPNQZd5OWbWIPief z!d>5&SZ!O~S()&xjwQ^_M9%3Cc#*agt`)kCf;nF__xdFibQpZC)gj*Ufewt&lv6Fe zJP)Uw=43RK4WJUIvD7Zr)jnG^S+eF5-jYgzNMO-05-oW+r}N5knrt&_H@Ed^hb^uQ z>SVB`nsE74)pAzv4%j;x839EHI+UTTC8LVNRcAdKv#X-8UZ?jKU1obRZS|xo5(62K zMVUc@g`mhlqLD76NXO)Ao0^0+hFNsMhCLc>met;*%G}7fYZ|N8W71lQ5@hI0z*3Yf zPFc~}{3=UEldLwpM#7ubDvZsN(PgPeGEtSelL>Tuv6jd`&|zwa?41tkYRK@Qr<=~& z>{g{YiRlaOBvvrRk+jX3R@S952g=_tkg&u8D!3-`AnJ_IU~L8pepfydi{MeCIZ{h? zyD^De86xIPCRf`NO}H61;fXwk<%01%;vwsoD|H4W*fycO zV+RuHeA{3ytn?QHm=*WL6rF}lRdcOu$Gdio*{>mBsnO`nIufQ9@92S5)^Zb7Nxd?Q z-DQq;^1jH^tv|pu-}Qgb1e^abmbcnakzf%C=vstE39fbS)oB>5BnF z$6hSe{5AgN0|_#cadobu@ifBD8lp3&DsZ@lN;RnzE%E4I8&BC$C1k`zBYu*%SM z*i7<?`Prh{Ty{lx&2`M3n1Q6;24)10Bj(Rhm<0TjcfA8G9(vQUtOZg{y4F4OO4Z zQMLxLQq}ICsBob8M7)Gch@a2IGlVOL6^H6 zYBl@;S1@hQw3Ud0seFXI6zf>j<&L}t-Zs!-skde7gw>`ol%+Z;s?w?L zX=_4eE@^D$M9AYQm@RIpywpq*9AqG2>_(DkNE5NS3aCYr39C~YU4kslo2sczHd#B1 zwktuqT%z^0m_w^{(TG%5jmM*PpUPlL=i?52IbYX0Ol?P|)>a4H1+6vh>L!C0aIh_b z)gib{Uhru2$0UJBCFawrTJ~ltt_)QY`HrvIvbrU)d`ez4w7ATHRcc+;QmxpG8i_+& zRO*x&!b|8}!HA?5M@p4|H0?E%Bk5ee9%9};kWgkDO{Y?ABxhu@@~UqaGR;E1k}kSdLqZmF0GmZSN0%uLp$Hh3Z#ECa`KMx^en$)W`7f`LRc9_u2_ zZbj81moj-N8dVbEXd;=9RLG|{cM!S4ZINg)UcH0yqk%*%q)Az7EoV$^&BvuVhuj@d zx6@gRKCi`X6^$lOc*_obH?DL5`wk>37K25NMDrwbJ7hM8%1s|(4LM{*XT#d<5>l1h zZ>&l!o`eGw4J1k)ozq-3cM>g6+@elK)S8M?9cYwN!KTJ2C!C2OYRna)&Tx&nbSR;Y z`WuQ&7Hulp35`n{b5>%3K*A@pm!!&Ireq68iYgbZZ4sQ!1BrYjL*(LS%-3;Ys!|1y z7+X)0S48G*A(hNnlAM8?Qz+xHT0q zkrPJ-mf+n;G_6T06KN%Rn`Kq+$>>7Bu**aBywnDxmKx$p864eC*U@$BI@w@48!qMu z6>7I44Xrd2jMZ3Y4Riz>(YPX?sn$XgQ@UM~DU=O!IYfB7X~f%XXL6Cc5ewtet~LkQ z1`>XsT#6~pSS8$0RMSd-Q5j2T@<^rD$rcO2csF6xVD>U0HKjo1K*EE?f~ zO;JUX+}ucliK4&cNmwJfu2%1}rS(By)}0Qh$YqY)lrUc&NGJluu3gcT=rstDGNHw~ zA*0dju_j(GyR}%V91F$e%_QQ_`@{1}Z!K11PPZ&m4MoXof-|;mAx*y3RpO{u>deR-wIngv zjY(i@sZn%>0+ocrQ}s$Jm3BU8F_oKGHxThFt9eJU6s(p|3zB1>*8BZG2CQSS%H%KV zh2V4GMz94Mz+Hh?fg8y00hi8XXV4jr_(}0);s=2#x{>u3S=ck+|$Hh6A!(>0lzKQ)qc7lC0dw15~ z$-Mu|fJexC|Gy1{02v@4a}R#cypX&>(9B%M*vfc<@!9N-3Vd|-pF98YKiC8P5dnAW z!KeZy-<*7e!RwK@N*1RhXR4vTm^T=bN~HEmLf4e}1OAd|zkx(rF3VQ*xUrnEx1F7s z#_P^%9X4ynT&t568Oc}qQr2tMd(sV*TOUXyiD)QZ!X2)B)?f-+ix!788TH{BwZUku z+x&T7idjQtCjV$ z&KXD~awcmasnptRsKNsWBhE;mCaWT9Wn5X8r0g|K)?O-0$sJ;ed1!xRPjMDMcz=gO zMAcF^Sd12}DP_vF!zPhnx}-@NDary^E39sDZyXpkg~XcS9Aw$!B($n;zbA0|~rQQpm!Vh~1`WMy!fLncSdS15UUs^ER4wT~b*w zxAeuXv0z~p4g_7m3Mrz3VTBZx!%CT^Wy+1;j?A{tgGtqhnyh|UXpa5*m8~6zp_w+8 zPL0Df^I<5WBEKkVwBxWf^IA^ZhoK62M_4H!&C~HE9z)t z;vb+q;k|5(}vRH zY%Au&V7ba@au`kHu>I%3P_pi+p_sEm0S@n8{-S&ZRtFi zN+y@JYSvC*T>PQcb@5cM}VvS2Q1n%FuAw z8d%_7;e41%rt)O{*6jEr2Lr zIS!NR^+aO9BQWN}5E*QZ$KCERHr7}3U<##FRjQfvQf0J|@h))2Usiw)=5Seq zvVTV9o z*nWZFB)o*jOTbAU34w&|vHYsKYdo5kG^!epufr#vKjiDKs&l?oeNJ^%om1yzt8O+e zs~tGZTo z*`!nYRzD&zpSXU%))Gjmhr5w-lQZYSd3`(|%(L!ZyA9%Z`?Qt-3zE+bS;|x~dfbuv z`d3kl&Tvpl=GEScp;g({7KRqIZ=)!C?_yx&T#y;LAIaV35&C=^T@I0O8O zQMDW0^$yIjOfX@pxT_#xZlk2N(Cv!HlcVdMOv-ND59Hm+DyMuh4@%jsg!-*=s~$GF z3;lL$z`CQ6O0iP2bd-5-S{=>w+{Jz|W~+r{|4V4mshPZ5=P=w#_I=)9vz}#RHXAsN zvISK6qB0-nS{Y_wGT6#?_m+M#a29-MNGc4UNw5UbO#b94o=Q?#e70J-F zThG)xF}9=)rW3w=jf!=`DpQP$^<X`b2QW+Ud;Q9mQTjuzi-iY;9gE`^Z8F zTKr`Rc^6v!H|&D7fBn4dr3)WixOCmS{^W%hg1!HT)?Tz0UOTh;uhQ#RKf3xd$)Bwz zSDyxM5PWLo&XpTho~ihm;`53(DDn!mLL&L;@|UEqTE2U^ylhxrl74dOYfJYmHI|+$ z``*$z*!ABlf9&G77T>dY)1rIv;)U-@zj?Ta6?!-!Km217^ZzvhAp-MeRNz5ixC#p` z2n<)k;h2)ZoOYAY4+10K(*#rmM!p51g-)l{7j?PeAl&i>y=r69n(P79ix>kfbI!kO zPZs^*rr+fp6j%R7XhC4!yhmseO4zMwSAeT)vIb4K?ElQr+0L%MUC|Hdf-z)a z;`=pj^+iGp0)uFZZ=no&bj3i`oN!SNch=7qZ0Q6W3N?G_4i~a(XbrGY4c(c_ZrQf} zMS%x_kxx+bU9+;A*2JoLw&h_>o_%|tQUV)wGZ?K7>{;5I$ao#$c*5e28#XQyco4W_ z2qzS~1TF^GfV7a;A1cwK|2owX5MH1f-*$|QF$8`OaY1wm{Au1Pv>-6k{zzy+V5YSS zEeOoC9}!v*m}&Qf76fKmgv#!8(4~6Xpr!P?DMP<*Eq9X9W)9@os@ZcjpE+QTnPbH` zlguq%A@DHk%)Vf{pv~!>nwr%xQo|4X3`znn|RWCZ+t%1Rq3R@cGTD}m9QJf*kL~|?1xq`r^jijBx^YK=p zAF-q|6>oS3yS13fRZ^;r1?P}+^vnY*>j7KiVBKQv<>T6DKPD4qhFtZ_s&Fyz7p3+p3<+ zz?tGyyi~F@disj2An+it&3~EHHvepa1%b2c4MGb7Yrb&wu}fgh|FXb?z?v`E{C4rJ z`HMUY5?|M}(1O6%^*O7OpG$(0KbI_ickzRZFI)^QUb^tTg^w(}bRoI$H0j0DJpeZD z-RMgGe8al&xFhy|$=Y|Mwj}>AnyN@7OKE2;ittHlhV(nqkAp~sO{rV*bE%#0JBcy*6434` zvo-<(H3GBobc?3`j2-AAAQI*tI627H%*SCSg|g*i)^F~s#{r^XmIHca`xw&aY3 zwc%vJ)!y}ZtNE6-kTZs>HM7g>TN((U2wDhUgCslzCub73gLez42;2_P#Sa3vgO3Y5 z2%HG2LJI; z?5CUdMK*g%J=L@?w%O}dhF-tw6~C-WoBedtzQ|@zsivFuMK=3crhSpkewJx}!e+1P zg=^sn3;OrQ=U)9x)4s@PKf|;yGTP5D?Td`|Gfev;qrG0o(Sf$uS4wQOpK93`8SSa5 zmVL3&eyU|(WVF{!wd~I{+9SI;fzkd|vrOAJL(Z1(s5M<<+RzxXA-^>W617WJLJI=h{Jla80^7VAwZON{zfovGV4HtFYJu|HSa-=cI;vi3&B z_TpQ?KmHs3z2xG&f)0(OFaJ&HcCc@ZgsU!HAWWxC{iI57Na=mx-n+7&s@UogHj}WX zar&F9W$I_=Uv-KA@74m`W=>^4D=3`mzGOaxz_AOywP>j8sCi|!r)ND#}0C(s@= zXwbE@?A5J$oxPf+IWAMAAr|=m!l0d64)pQ{B*$Zf>!6wvRv^H&KQzxJwJeCopg&n!*FNXCHf31K6B9iWXW! zQl$dTOBt?2Ln%0SmMdk{Ito$>f%p_jwJEGD=fKT}N;B!uB)o}c-5fC1`%Q*XC1{5& zABl9jsk0Cv#}sRscBh31iGV=bbY88~sf^cTfD4(a4tY>bM>$|}l?Txxt@mqnl|Ycr z@AU)O9Fwh^bil#a9FD2HgKEL9kE@5;XxLTi#2dbywn5#l4SFi=j%rxYH`2bM$6Gf< z^rl+c)H{1K#M7xkL^RY4AnZkYO4o0`dEq6p+%g@y{^d`QZWD;te4&}t%xQY4{I%3W zf`CTT^svNm?Rsml-(@%^%T$^6;E_?uDL8;g0tILRp&8dX9grq|GCHDj{QvA%XkL9` z4^FCtz^hOXnom{2y}VnKuO#cycJI%J;Z3@}4n!nD(>YF*KYt)|pgB0+10jW9u9ugdAwg-C?T>R46Z=Ls~* zO{P=jf(3QD6;2kNz@XS@)Xn9l&Wfjmubh1@Okl%yPoED&Z7*$TMo&DW2RKWGv zZpzzNnz}|S-SSfbHCL~OxmG#SP)7WXhRxFTcKrHG{lqFa@EX%fFQf4STf9-P_7zHA zN|RQB&`3c`Ebb_=jbc5LFb-9{WHh56berj9ETXdcyjoK%SqtqALOzD6+B`Z&m30>) z-MTSLTSHjiJ=k@ia<>;UP~~2=FC|&`os!BKrl}l3*fS`zByrlNFDFUYqaqb*)O%n( zRRt^P>La8?9k8YoR&BV8Qn>b4@{1$bbU#lQGMy@1pen(LKDZYEt5speT+6UJ9>tbm{x2mc1&@k|c(jxS+qU4~yV^TZD(8n|bA7$+LapoZO_`fc9{`fPe{e{X0l zL<5y%B;Ai@LC^$O$ZP7BJmL7TmC-oT!D`Ze@`=VfP}A*x+-rB4fMZY3%(AuOUXwGm zEYWzSYt1(5E^WF?nbnrQ-)jo7+JP!vHRc*!SJFgja(1ORsVR1KO=T&~_F6j1(6jd} zMXZSib{$MKaQ}a?ty>6}GF-KalYQfK@>NF$1>ME16uek5q z{{(;kk9mehj%QYd8?@0S{VmA~Lbw7EZnu*Zy>H%k4&#krq3kY2tnE~^Hy94x%38f& z>Ts;Fk>ZT;SWt@q=D-HOP0t4ik$vkm{O`Zw;ycrWBlz^g5nS5#wnAPWCfTx-~?JUD{<&U=0$fh7p;n_>Rv4Rt1zCZwxg3kpAk$pMM|Nb5>zB5fYf=3j*RUEoNXh%aPU8pWXdt0eX_;etyQm7OPGXC9&JN4iQ3JT8U26ir)i@Ds4*X8Rad@0clP+q}5 z|1Ajd3(uYbas1||px_M%@rZ&GB>F%QrzrSbw7i0U_IDuK&;4&)w3pW*+9L{15QhVy z9aHeRD0v0{^vfX1&&>MnOpNIjHHh+vf)k{TKq!wW_*|U4f`8&1#QDkla7Xh}7UDdj z;EtS6Yu#5m8nL7`py@~Q7Mo3D@B6yDj-GqpUryAVg$5Vf->noM6i;5k=OW}4{9~Vk z2tR%)F2YNyVg)D2D1iVX1)mELBKyb>;qOO1xcJUg;0PX3@V45PH-nqQOjmEPYvW97 z5b7ARpKtQ)2WP27(P-RH<;w^RCT`DjsYm@G1Qr+En z44cjP>m04$3^-kR-4nLeAV`WcY9BP(JxzlymAsXTHWAvjs>1E2KNm9$J-eoGAfMV- znXoyrG@Nl~$beC_>W8|Jxe1~H{RW|r4XiKMq_iOLyVhi8ZNsXq?uoiw(OploO4qA; zU8J4nC>#Fp3ezil4zPG0U_oor-B!9**Z7S_e><5t;i`HUEAFpZl8-;qoX-_cf zRKn8NII>)LcercGC9@_QHoL>=qc{L;pukWmI+Or+`?*xPtfT^+AMG8D~XjPBpn^DyBB< z2b_gn5b7G6%~64A!B+$XCgIF<_QTDtt_HF>Q1xCQ;uyyA1skK@kMFj0ZDT;`>PJ(S zmo>D$zjF}iTdWZs>ts!Jn=j)lV)yE(z_9QY0f8Yng~#S} ztiXs5-uW4(3ekXm6CAl?qd@{w0W6@v5S&QQKPsIqFlE3F3JgIqWNdb00>c0{P+$mB z9%HjPDljGZih#fnoa$n8I#yuBW(HnhiVzLxH^C7uHX0-_1;7Fd48aNK{N%i|1%?Lf zpui9uQDUW(v&eqFqvzE=aZ>kXpCCvi0Gu zw{6|N)sX(R^b^u|N?$4MNS`m=S&+&ekbPP9A=z7GF9Li0@B+QyUC=B%c`Lbf?bcOW z7j6Dz^IMyr-+b@pU7NRVmNtW%hRrjZijD7Y+`sV&5G&x$jlS#}*;(1D^as*~h4TyV zSoqSyj~1W05!|?HL$ZGV`g_-JU%zqPuznGUA^3^4yVe?OP7p=#C#zpsy?6Ci5JOP8 zs#y8f%7;M|!P3gLD;F!i58?>EP0?2b6;~-F%lCum0k?zf0fyy^mL6F8#L`_Lj-Yet zjQl6^uYfp$x5|@prF;csH~h%r>lRti*~z~v7ecbjOlD`c5X)Db7I%g7ahX_Kw1nOG z5xg*TnQ9_t=f-PbPQuHMgzQuq4Z8Io&uZ}MSp|0r1xHB1^oAS!rK`(SlLfDw)#4Si z3SQ2m9X|r#!?$kSG2{8mW;}oCjOYBls>@X4+9NRF?`};ic&h+){8UnC@!PWs-ZHD; z?pXzI778X`-s@%+yjCcf?8ooUDtP;>0zPo;w4VPCrs(|`v))fI>-`k7-p?@W{XY=< z0)!ZR^pAW5gb}Wrx@@iItCUja5SO)Z;`9cq_hZ)k24=m#!mRgo%z9tLtoN5fy>hwb z?tWXGV%FH znDu^yS?@nF>-`62y&q!M`vGRXe;4WnZ22Y#q-oFRTg|K|7!c1_>;HmT@1HU2J%Cy7 zJDBzU0kht>g?c_O>uoYav%LZ`-FTR6P$X9hBnNZr{Q{HT&oS%$Kg@dQOFkDLS{cl( zhgKSM>!Fp)+m?0>I5X62vPzOZ)B+PSsJ<|j6tAmiU%n-9oJ z3->PGCHwIDx7I(re&>2={o3`5!LI(+%`?)V6zu+GcY@r3&aE4_4D#FMH!740#qzhd z-n(_X^lh?>aX0j(pY?8 z@e|7zFMWUc!z-UyzIOS}<iw(lUA=unvGn=X z8>JG-{Ts;*<*H%&nj3U0IKAX#Px*P)9P|%7O}3Y7IQp)v!=4r=Y}u?})+-D3D#LI9 zq!N z)SB|=s^VBYo6lN#+CoS=|9otpr)QeG5BnP+Z+)Epo^~C~@w-~6*N8S=mbY zGiJTt66(#>x36Q?6ZX6F`}<9S-rRBdTcO_E_xm}_dY{3p_i3Tt+;RD9%zEhk&Yk}n znDo%NfZ$6Y+Y<-^6d6YC4Odc}woe)q=*^Xvw_w)0Tc|hpef%kAy^mwo`xs`uj|lbV zzPG=?toJ{KdUNIUe+l*GzTXmNy&`74f>3X6fBz1%9@5LX{XpOSx%F6q?_BwN6K1`; zgnD!3@1tu$yboapuiA|pMBn|ndKnV>F}I!{vz`yLo)@#82eY1Q{bJ#WY5u3{O1zQI z<5kPyRm%$fm@B7Mq2Ao}F4DKT>#%DDdUNYN53}BLG3z}Cv);3XdUNG30M>Pn#J&Q} zlXKq(nkVPZ2cO04uUn`$moM6<%w4ZNU7$C&-ZL=k?O@is3bWobg?e+pZ)p8Dx8C1i z(!*UheMz7<_xtumq263QFGK(Tt0nhHw*Fu%z9rp!_hw>Kw(*BxXT7lgp7qrF;@Tgr zrPt(O-+kTc(#rc*p1-oJxKD9C*yF!{`G)0{r4KCaFRg+o{WpRe0Uui2UsNo-Zy~*~ zAbYngCjBMwAP7o+3Wz_XzvyUfQ5K>lcU;zWhf-{yA254+cF!l>knCL^~A*AXTjxw1HqOO{LY{Le?})`EAPRXo)9#8wS~~x19Al{Sm4bYDO4`eaLxj{c5@F zVRW%f!Dgr@!X~PdQzi7F=t$0!eK7`Ef0D`Q%f5DjswpeF{b64_h+FrqTpA>RRXT0v zbg&j{YcvC9AD~_2$-Zd*?TYPWm{F>NJ|FGzhtr;5-X87uD~)DJX|(FJY`ok7>38ew zkQq7+(a{1=_JtT^3GTE+M>3x53oytM++m51q&(RX23dj&E76gJCwnUfS%RA&Lh!Wf%iQEK<-h@Gv;HpUE93*-UgDAm` zkH`=b9nSaT->wZ^AfU>C_Bg|KN8hGrTJFf8Wp5d{Hfz=PEDf_VmDH45E@LV(fJ6rv zM0NOAEFyhKw2wiQ%IvE>w6$&bY26)58Dx3N70u3;XHc%W+$nQ5%Vu|*%}!>2-xujY zqCE_vxu!PFxqA^^X~-Ik7Du!N96|bYv%Fh%)vd~URaXr;lietrcSX98XcvPh!M&MC z2NLaI5GA-B6KO-DZ49D$XT~4Qno=F3PDimpLs0Fo_?y0lpVokIrX7QZHdZw@Z>!Il zA{->jVGt#__7Z79qAd)f1h-crO-Qsk|4stQeU(T95^Z1*CAgImsY9Z5459>gP$D%* zw1z>H;JQhKg+y5lq6D`_B2`GVI{z*J$vu%s1rn`b5GA+`5-CHXWelPOcRwNwB+6hA zCAj7hDM6wo459?LH6lewv^d`fpX7cTN05hB$@?_i8(hJNbW^MGLUEngDAo6hsceP=#3ae3GO;X_94-I459?r z8X`A9qBmd=CAgg+#Q*=CWaHwM-<4k@{oF&^sz2KJ5gLKpnk5(}9Y$y-5dHa*bTmvl zjQTq*OuBAbyjTKl;p4>;$ShxGdQxnxMtl8vy}6ws!Jj8_#I^!k*) z<|#yWeENQ#uBq(t^dM1iM~8)=J>^T-+kd21T=x#Q{Oswm#ux=~nOar7L zd4_9LGXoTp{p|EKL>gFAh@-yWX;ne{TBllt5#J92o&J#*W z8DQk~sb6=mQWLCpiiJe1R@OJWb#33M_jFi_b^tq2zvoCP%WT!1?GBA6kBbh}*Q4(2 z=sT3F8i=H;c2!+#%u||p%#_ZglpZU$7mk#I^_->Li#04cD~Rrt-!bHxtlB@Is7x>! z^s7qxM7`t9^#fpb&{gXetON{v*MZKxn5Lo5z4SWi>E89mTT!bgb#96lP0CJdR_2W`=O~~!II>7W-bJPKL_Jne-GaB_UW7kwUkI|WYXOM3B zD}H~9Ex4ODvr=z~Ig{p)!%nL_&0IMj+bcC2dF6>$v0hhGU5|APkw(a4Z6q}s+g`?O z$k?hOqn7c-Ov$t}w-ahPJ$to^$!jpGyfuAvuUX4m;_9ZW94NZ|`f#9?Y~~C)wrWTk z^vOJCZXein6#suw_5#WJImO?}e+2&V=-;Cwa6w1lwr2HUbwhA!Isdrw=u8WfgiSlU zn3`US;GlE**#ap6(Y|Q<>Sk{EX*d(kJy#IN!GVp&&lMiR>V}{2AEwpKG}tGc^(ie# z5I<|yH&^54p4*wBa@CezU$?7SOVO}1Fb~@~PcL-`>XGsnaAzhL!316S!*kJkxjUaj%PhjGM#)mJ5SRR4>`hUgTzLZ93n7Q@P zYAqJG*^*N!Eg_?W)o2XSW+%|4qM5YWp)3Y!`Aodu%2d4#?TM9>ZN=?9tF}!0qLu)c zOg2l6TF6pk)6H6Xr`a>8^Ltb%s`c$9T79*pk=Imm%1Aog-s@`PovgPN>HD=bOEpt^ zvtiFV*G6sHxU~XRm@j8iaPFwgyPs55Fu(pIRrjBSu~yJ6N@WBzKhDhl|tF4qnU88sENkdy0T|1s+4i3TUpStR#QI`%hls$6$37z z8=E`6w26u}Q@V_<;CJbh&Y=@8!8YIZgZ;mBMY8q=#o5KTfPegV{d>t{8eld$2WKOK z1Y0Lr5+AMkdi2vvltnB&v1iSC)iv)t0xYH&lmY%vuxG>B6Y(39s z3U|^i`V7OJX_x3T2+)~>?J-!ru;XiqHr;RY-`|77d-y@0Jgq$GHdU`t8vwznhm(!! zz}@I1EdHvhp$%&CtZq>Cx`UpMzUgjg+UXh_@RT_wlBl12o2m+fe#K~qrJgFKI&|5| zxJm&{$Pn70^V(jmrK_ZZw5yzN52OBSJVv_ELtix%w{Mjxl( zctlnRl-8)9rG&MiNgD&%3+(E4&rxE^8JDf=YddnCc*Ul(c6R*g3>|S-+^L}L#LIM= zv4uIVs@G)7X@;WvC10hkkL^(InBVOlTDh1y7<9S{)@VRuY6hdeTDNBjY9bx37|CT4 z;Zkldm^a!Zv<75?-D?Lmd%9)@Qwd?$ffC-6T4)rq1(P%1%{v3?J!_@F^!@JUpqefk znk8qtp||%us*_vg5-Qql+e@j{mb|Vv(Chncrcv^8#!$L9$RyMe5T!L{F91gni;)YY zY6Xy*+MwQH-F97d*sTQuAohrkikFMEgkzAYBsJb)tc!VW1-lM(?)D-Jbq@NL5s1=h z>6~(!&Ry`;BI+cFr3Ask=_I4XJmow+> z#EJuVvK`kYschR>^RcCNv&gu;O>ZTUQbE-QbNES4$Q_Af1H^O-8vH?rnsyAThDfL8 z)#@GPv?djes1rkPE*zr=y?z?<{g4qmOhs^YPt;Ol{SIL>(Fu`mFt#M|WmL(JRkTbh}3bU!S z4w+GUb)?YiM#z~xe}dTzT8GSZdX+0)42fm!via}qIx#bikunxSAm)S!_=C(e2Fef$ zcLR*5!e!k?hv~XPR$FP%DFwWVhB)M)JT}2>2CYM8T1ua&`14M3X3+^|GiV($(@+%S zv2)^#s2Dv9PB5E6Yn+)b-47;Cz$`F@*;HDG%+!dvI)R)a?wkKjXlhz?KgH|)Jw#pM9&-u)*o?teE)*&`V&jJ`OKaV z2a4wM_m}HS|GLD=|F68V_|J>X!UGFM*>_~L^dF=-$+toC*?+g(ET`#^f+U9CHbG## z`Jv!$xk&&(Ky;fRgdPTfa{>SYqT2+)_Amep(Fov2@Y^N`%ZCAAAOIjBx=j$k4+B77 z06-AFZ<`<{-~8ZqxAX)61Vpz9qW56{=n4P`h;9?a`NIIv5daVn-6n_#hykE20H8JL zZYy1@Yy3u|znx5_8Asc$b~aswWEmu!uu03aZ`O%VAI13*y#KtObxAW9+zfPw&kfao?sL`4h$v;e@{3V9usQac}34Lj~xP!Ok1pr#xe%IVD`O`)!Na9-X~0229V`b&voAetK5g5jR29}Z2J#UcK= z_w>helm6F>|EE{#RWl<5l|$4{(#&!0otQ>FY{wGSrj7NP_ksge#%yLhg{TfhO40^& zOzgy-Q*^@bP*pr8Ly7Y5nCPa@ZcrLD?k;Ebl+vMeNL!Dxv@66JDF>qoRl^3WzoB;f zdIMc1Q5&e8;Y=Z^izRxo2#BF#G!3=QbOtXf7vFW>N0U47(IgP1)6%)A*B39i4v{J$ zfsf{e(W1FNniFdHsE_7__0a@AYI+z2YU(wpfZ1vZ=M2@#j?)zCb=sbq+S9UGb&aYg z=;-gLQl&!Q;pzp4)zpb+8g)5kG`5S5IOFfQ2WrN}w5;xAGu1MfOAVV(73ei9(T>fU zueZ}$2S{SUhLvV69t)^r`HI<`4MrPHpQoHEt38?sSF^LddI!otj zz1T37iaT}~bwIDJ82cJ)*E;M`T6@RSZVc4@NH*X$NBtW2$s>mhHR^gvoiqe15ltu4 zHgt2Ea@vJ zAbAHjY9HSCf0V&EI|zrmOf@@wOJJC_#M&lfvs$mF1BRiqq)Y4svbjQzN~G;ceWDjU z`Aws!KIYdHx+#C!n`JsBcg9^;QbS$B)74R(gwgE}_Bym#m1?$vrf?#scgKeMu%nl4 z>h|p zLw&RYm^=?K!N?Yfm`vV_CGnOhjue379^iAr;ugV-mH{UX;DnJ|5OEs6 zvPI$nF4CpyVo|i80M4XPu4uO*ckR2&} zT|jm)QXe8_$H|VyD3%=pkB8hy0>OZuVLEY%sDdB0fi?hQKXiv~m+*1%w782P0y2tjrk1GDF}Y zkv#{YY~W;WL{N~#5O`LUhQi2VByyZKvIMyuO9O!uMs~+(oCq8{N_~M7M#{%&oCw@P zN88$4U$( zmKXvTjVueHY~bX_L{N~#5V&^Ks(=TO7>sn8h{qI(A#l1VQ~*C9F&LRN5x+5sA+Uy) z%771$7>wkah|dv;A+Q0BRv7r2uER;WiC7&gG2#;x5+@C~|352XBwO5;V)Lz=S8se_ z{v8@;{b`7C*HZTlnXN-oiz) zk?ir(cS$M9R{)XY{_>;MWnpP6dHrQwcPPaM`T?`IXZH+K6@OVI^r+iNI(IXJ=Fn22 zEBZj+=<$UO#%8bR==BHo{cd%rw<&vhHXF?QJtb{!v;x?`jl1M^^ZmjR8{(aNW269F zfD%X~uf^svMGXkp`*sP8y1Htu$(b`B3 zxB+dDNbbbucAP#uq}_Xdvqtze2oS;ky)=>m zmb?JH0-NQS01<59%OfdZ#S75Ou~{7vAd(&Y+DHOl85E#9u(=&4Ko4mPpT7lyf%5Y* zY%oZE2)6Kz^MDI4J};f`^*&pC2sZJhn*m2&dTz(&I3_&=+xYTLfDw>a#8#c3JW#^%6 zIYxY07YWai0VddGlU$z|J+XfP?1d)-MzAU0CfHVwp4dA8_JWfEBVa~y*`Uz{xcF&c zgpF&;SA<~4U+Msk{4{VYHpj7PfMCzhw3qqyxa1aWMn{B{WYfRO0bXzxIFHTi*h%1_ z?D}&phy_dnH)CT#a{$4%zup8)py1pz-&23~=YwG1Uu*z|P;Sm)GaM5ef{i~@2aMp? zV~EY@h}e+q{8wx6RROUXVDmawY#z$iKgU8Wpx=FLEJ$n!_Wt!MU;@RaJKxh`w%8DC z{)-jB5Q3` znm>_Yfwuw;rW*-~ivu=`$#oD7d=>_s!3g#?eVW9`06#d55uA=-^Bb2KDc}Po zhTvcXn~z9hB=9u>i6QV?z-D!v#7rE!WNv{-K&J@~MzE0}fjJLYz!`?%Bm{eFK1E(` z2JE1?5FCeKvl|nan*bZ(qeq7lS=!41M?obO=nQElqG}d8CaR#f8}z1LA?+OQs{J;N z51Y+VX*ma95fByvhXrg-#|n%1oFy}aNWjn%oP=N_5rm~UK*LIK{(-%R8z0tU|KR=M zhlw~DqaI>Ja6WM&Mr;=_f_g@9)PT)sik@`hL7N^0h`UZK#UX| z-~{!IzZaxVsK5gt*l?Zp zE1-!1oboHk69YKqSCHcY5D-1>SCAzJaLTVB!vi4LB%kg!_WbBZ9st3{mBh1q{&w*E zXrCCssRlzhKe~Y!z^QJC&yTL>0T68FPx}=-pBTWYz5+h}pX?6F`Z>jS<-Y>|c=Yel z5qM-q;I?M}Aj}cLoIL-y@MsW2n9X}i5JUCUMAHNxoasRZNeN(uIqG$51&=w>id74K zHdUt?ah@Wq0E6PQTG%L8ueIq~8$|dzcUCy)7Y%!HV@KDoI4YsK zt;D(7Y@zQP(9J|99c!9WdjTrxPwnkk&5=r0qt9zS4tAI+x-#lcw5wLtyJpkbn_&c< zP7NlcrStr|%oOSUHV6JG_^$I|j(P`SjtE5cv~+GtqUnbc=IFxc#RrFymG-PApI@b|HglzJb7-&IjXOgFRoC=XOm+}!>AX93y|-Oq3x*Si z(DNxnOrjG=R70^wU8Ad|&2)jS4kBKkwr5M$a+GeE&9F>7(4g8(&(FC69*=Q{@#&)- zd&J|g7s9HYjxy73ni-=z>176$oYe%Pi{Z#Q=sJ+|mprBm`?Y-rYTa2CKMl8AbcWrpxAJt0sTH9Ls*Ka}s}rniAzhNFWPE*s$br*8Wdz$4 zp1{PXjN`yG1hMcMB#hDg5#|Sxu7Tl$V>&%vk4EeP3mc7p%d++S05*5hE&2?@ooSco zGc3&mI&)ADG-0{oA?h~WZ}Sp=5Q_}HT|SN>*Uq%z*Y7$wS)KITqt+-P!Kpe)RriWL zU%a7b(}A+4G7RZ$;jYJDG3$!z2#8#3uj+cKSk&u?`%bKrE>lL^(zsIbzTZQ63znT$ zs_XOC)aiOKnTxwRF?*5?w3<2t$O9f#B7GOJBd8;EbY=V-E}Zk*MX zsG+~>t6K4N5_TQxq^twKJy0hJWNW^}OzPy6^`6S`YpH|;8`Wt_IBCy*Wb|N^B6d+6T5|Mrkc@CssngJrQS%#+m^HytG!6C3h+vQ4f5sfX>ru+Zk7>WG(3G zX)4yuI^Cw8#SkmBI}vx@R%}PD8I{r+_JhnB=~Bg}aq2ytsM^-ETkw<+b{#0;D%k7Y zDEp{n>w8=8+j_%Re`|m1xm!=(l5c)t^F5od-)wKDHm#eAjqh!IZR7nLcWpd>!?Cfp z{{8iDtbb_z?dz{x53lRjpSt#owSQXs+S*-fH?QT_ENfS<{$TYRs~=l^%j)f`-c{A= z#VbEv`S!{?R_<6~SE4JLl`X~36%Q!>Qt?K`O^Tettk_-t_vOD@{?p~Vm;1}P-23)uzN!c_~8 zlYK+>QQ6yNFOab^mF$VqUr4_x{WIwu(uOo4rKFch{thj9d+ESG9F zq&H5vp?c%9#!vHYhWe}w;<&%RA*AkU4oQ5QH4V(cU~fw8aJd4RFH$G zR}3J%&+~BzI$%eo<2qm$x@1E|$LWAtJfGME9k3oS1^dA^Z58@yK@X?)(<1cKjOtAG z(=7DUgen~N(ZJP#Ee+Y_-iKTqh%b5UDCo1ZJ7d=7dziSjuD%4eer zhbW&dpnMi87)SXm0p&HQ=vb8EDdHLdr4h9iP#Pa6mtL_UHK2$5^)ka_<<$}0twPeW}5lur{-UV$D?qP#*t zxs56uqTCixUXBXJQC=>fJd28mP@WY~UWVEVC@&Kf;0$`mE5I26`K9RDB=SoI5_&WV{7C}vi&2F` z;1>(Pe*+ba1OE*H_!ChP5%4Dpz@LEH3c#Nr5PUoiLGgG0@dD1rp|-^i&9(P4xg0pN zdYsVt$D*Ri&OcV@{9{ms!_GfOAbJrh81MW=0@f{5MAZ2$q4S%ltRf!79lINpH` zp#$rvXtD$ALI>7Rg~JZ42}D;>!FUH&g$}HsBBBng2pv$Mwn7IK!lz&vJ)HU}SQdJ+ zgz8N81nghou0W0|9QH&mde(TS-UJ0TArnih++nWz;esS{yo3GtGziHmQa&r^x|L@pE}(@U68^uid-$;c>~_T`jNrR@JLp zEB~?b*DIe|dC^L7#k=x!#lI-Nrg(#5ptwQttmR)Ue`opLjR>EZwox zUZR$sB>$=W@8lnmzh0h@Un`d_{^R0j7k_{8C5xHG%NPD};hhW2LU`fvvLDO7Df<)I zJ+i#aA-htxF8wFzr={%G)cV6Xt;NfB$%j$v zKgMZ2!TXO<>kpl%^@pyI%bxypndF10_0)M(@V0{s2`O8?!$^WA^)~NIYi0kH+j>sQk#7y$f~k zUQ~G8m{H=R6v@44%-(sxS{So;BBJlWA$oNG0LJVcsP*risP*rm5&vD(disd}E*kN- zqgqo&{OxGO??F|@M*JQ$;=hB6#3TMYXvE)!%8!ir+feu3iVBY%aq(hO@>VqBzkR@3 z81dgmMBjo#bYh}=3mT2PQR^wAaW@)`H=`NeOrT+N%zu7=ia$*WP(os&g(qF%lV5u4J>SE1IgL{-Lm`AXEw zSD>OJy?h1g<;zjwu@ZdrBcxPHUXFTs2WpLadB+pvvMa#M|1$KL-_}ZAhI;W*^mIxu zUW$5gJE}6)i`!8zUV@5_^x`F`7q_9pV|y|2A-)au;>D;n>cxu@-WQ?AQ}DhB;e8>h zGKTks2=5C}(Gk2aKzK)}@Ho5^@mC}xg!fj|8sWWF*!J9lo=(EOMcDS7M->jio)OV=!%37y0c8PII7C?xP|~Pi93?HF%%h@XQHsCEc>!e( zwG~k21e96ya1v#f_x}gG;)!nw`th)D;q@hF)lYgN9BW+S|(*;Ty}j+$7uVE zF+>&EX0g%V>F@P37Jn;m>0x>8!mjfkg69rA1PN~Io|bp|lq9@oNrLx45hS@W$k!*h z`7+&2m*7tA$vgxvjF%VjE*#D{JVTwC z&DA^YdVSb&vO6h9X~(7NoIFEt-M7;ndX25Do-4DiTCZlw7)|}yjy9681!4}rwH){R z@~tl8&zTKIkFrr~x(dl+LYsH{JOi8CUP>tgEVrZ8ns+>zoS{@udGIn}>tWY{65iGv zLM6O|lb&vXtfL>BS=X{O)7*3kE-gPYN=O25%7^lBu?XRhyUSk~<$I7n)VW3{TV--b zymH}Xl1WeaQ}fCNF_UM0_|$E;&dK%~#rz=d((mY!)RTyRMCa8yoyvGk2E>6j)nShv{%lZf@aWyzm`6P@I$~7E z=q^_sE_F6<@Ri+qZ!T_)SIkYLd#}@D&B12SxmR@cJP~u6wROW>+}hUmib`6UGTB|6 zIjf49F+Y5;>pJDm?1>6cKn*c{(iJu+I<;pmni zbu(_Qh_}N)_P{ktxWeR{?&s-3rc;HtF_qvM@ISBdclf~7Od+i~x`e5NrRdR-Ui0J* z%cg!(0mH)@q9bX*c$~Iyp_6|IF$#D@AcZ;L5IEmHo2ea;sxZ0A!M}m8eF#U8`u{hYdEs|NFWWxA$k+#b8yy5sh-1Aork zanXHKFRSzx1usRc<0R-00HLMG)wf zhd^Z{q0$jh;iP6hL>UB^eIx(-Z@3UI8EtJzRuIG$IF{Q45!4Yv;XG(QLI`dDb@2B_ zIWEAR(Iyc93lM@hvk2&v*>kP{z}fS@pDN++&+fp5 zd-+HL;T~Cl5X59faK~oPxiI1EdEbwv{O`AMVP0{I=&b9|NcJs`-5d%gqPebo;nH4b_n3X0)8Gqi0t29 z$p3x=7vIiJ6Y4`?Z$SX3sL$NfN?v{b*$2`7%RMvDicd*Km!F%^9s;8WLOZ5Cb5Ziz z^WA@gC?EJIT$EP~C$xv)R2ZQ=vhYqcmu@w9SbH-!Tx`hWc}s9{{J57RmX1Y z{~H&+L#7GX&lz3IMH# zh##f^&=WHiV0F^?e|qo-norfa&d?NN{i%WO|7Y*Z;~Xi<{*z4RzPN8;ci972W+!JS z;oPJ<-MPAxPCA`~W5|8qcR*lvkwXp z*qut!)7f{x{k%VB|Jj*-zR&mRs^_Vyu6n9Ut+RPch8m#>QL0$BTJzXRD@H^Uo=nqa zWRmkaaNr(LvMcNVNsA!}mj2yQzad^T#)3%0<)tYqM^rPUN*{^T)p(>%8me_o(niS| z^9r&Y%VpRIo{%|Wah09|8NeEo+2sl)Vo8~AUazTG1dgb=R|hdSA&rVW;!lWXgR{rZ z?lbes%mXvm&eUh~W7Auw51jhj)c2=8KlMSd-+z8;Z1T60w@+R^ znVvLG9y;;Qi62i~Ke27%qzTo;^!Rh*_l#dTUKqEG9}dp>`}x=nW9N;XHl`cfYxL#O z`$n%Gt&ZA9j~Eq?{9@!ABNvTCMot*nfA}xM4-a2AeEKjmJU2Wt^uW+HL)9T_Xrtt9 z$>Wk6BOV+o~>Q*4!;p)0VJnybTOLu!bEKu$tfpQNnm($8+T92omqha0$1j^mN zTuvjGX+nCcT!Ho7w_Hvsmnrq8I#Gt@?iDEaJ%MuHT`s4P%M>Nrj}kENcLd7aBT(+{ z<#KYlOm2=won7T}mq58YyX0h*2-dX1?{UX+IUOL5`J)a4EcXL}a^Dvy_sHx8qR;Hk zak+8Gz7%hAkoBz7Vj&X-yWiB=!oF{MsJIum{eR2Ne@ZXjG_) zq~HsKramA{>=0pM2g72@A{ubQ!;z_jgoz!vTny1DT+v#!tNS=$nV1r+7({8jS%Qmq zQkd9;FtPFFVp>E?JFE5tOgbh^Y*d)o$Z|0iqVc4tun8iad`phxD5qN7T*%9!#YCYi9N=3`}($AscR zqMDScJ*JGmTvo|K0RpKt1DZ%k8KtAng3BQ2dYh$JFGj#^mEhFmh1SMIE`?x_RBMo$ zGIXAAQNqRO#i)1?=xW7i&ud8sNW%W_#ONRf?v63q-hd0-<@9xZ@+iAjq1x-^I|nzn zvh;2Sw?fr-mwZ6aQYnqquT+H8o6R*#svdEWCQsgD(<7`rU}fq_m)3^U!HQ~^*RDz- zSG@`UZuitD2~m*1L6jq?U6ib7i)H1meLwifMEv zx!dF~QIYw4&4g$y=~_I7nHi zidMpi@SX8HZ-?*R-_O4tzh_qJvhDoKN*X#0AltscnP-20uQLdiZ`t2zUw`l2`@L_> zT&;6Lv@)ds?S004vjlEAZU(EVNIt%~;8rnuElS0cl?qnV%cE-49tb#0h+n_!EucPW zQR=KJYlHRVj9R%~nI`j~CX*(Z7u5z^#0l;Xe+iq%TPmLoW=)B+p4A;@^uyqGtiT z|IeA*4wObB^m?z_rN;DTCZR|~1I{$5i4t?THIK=&wrWM0GGU64Nx7g2Ch6KB8}L`& z(SGK(0|qu=fc^DC4A_5Q1JDGPM!Z;%qOoi)Rtp+5sGM{l>6BJwcewqCUr~()^-UjT zlnF6lzm@@LB@we(R4yM`H-;FpS&0QL?sO_;a;i*WDx9#TRoRTp;+iwYR=W!J9oT?X zu7Z6AHei*jVDFXzwz*_brb3lb8=6apP-CdMP@(H(1V?lXk}cC_OouC#8f(>NT{rIMg}%um8)R5WxxQjy?&JqkhTmM;E0v=1DwaP@=Da1 z+lB@LRRUjJJfYq&n#j^%BV0Eit@ywP111MeP zDp>sBzy_>x6`avBV1U?O-4*FG7f){)pf1EvOy(tw!5SN(C>NE9gzJigDWx{}3Z;V2 zt;fg!EGI`To?<>_f8`&!%VTS=H?RGjGiNYUXb7S0pD&)=7py z-u;7;Q^glbP8Vlqu9I9TrX}B${8X$Ox^C!0L-8T&(9uJ)Gw064C4U=wbLiJY_ec*C z?<>`aUXwD?g6JvfQ!}=iEi-#fzXom)ymR`R>21^DY18yk(=)?srMH1w1iv^{nmS=> za`N%X&rRkhkDVNwcx>V`6R8R9#L)PU$3Hcm9M_CX#(p$*)mUsyG5Vj;M@Fw04UW!_ zyfO0N$Ymo7BS#JYbNGA1A00kb`r5Kdh(n}usbs5C4W(@ap8T0eeD9%B`7n40-|{2Q zT=LSTBJpLN$M7vQoyV87RV&~d7+R|Hormzg%0b< z9c)ilvV(=s%IILd9W4B~#p~{1;gafLnGP0y+``fwENoN<>*!$NS88E>oriD=LhIov z?S~5Z!nM}JQ0F0BKdpy}&O^9vTMsk9McU7mJvDFdfO}TZ+3lBAzy^0-_N)#Te%vBH zqw}z5Ni;eSdsa`i^RQ?6m)a&NVQ=f0^x-zP>v7AZ^Ez0#AUfD{JJ_Cud|~Hd&qDr4 z=OOGAE!N9BtnkAQ>nA`IAGs`!`~7m5R0HBW5Y8mBJ)%jvu46>cGJOCj-P}(x^!MYW z1;DcqC~?JHDnygV?d#q&L0p5Eam`YtaI3~KRw zhnEL#5IXO34=)c4YVq8~%LBJc9iF>+d0X~;27=je4CdC2DNy;$;$)BXou%pygV?d#d8ZU4;-T%o^SB- zz@Qe-|M2p_vD4wXnU@C!wRmpgh4hS_uk)G)he5}*8+mzPP|LI%czNL7s>AbDULF|K z;<=ue2M(SN&)0Z)U{H(aD+f#GL4OU$7WYiN@dMI%(9goI-6j(s(MG~gTgb!PNZ46B z$aQTb{IrEUtc`@-uY)|ajf9`JkO#Msa8J=e9@Iv{Pg}_S+s}HrqIjRSR@mn{TK8@v z;ioOFvuz~oMIGcGZ6y4(g`8?X>*W{X$@XAU4pVm2+S+5luE#CC=FY>Oq_WP#o~-ko zhrL`#yy+m|LgKBxp83~EkL4aI(z#gALF-}1NK?Z=KwgZ*5@5X9&M<*Tj?ieg{jL)l zX-YaiAe1YI32?-#Mf8M3To&uazZ8E-^n`d~_J^}q&PHZsGyk4>Xy)THr_Ibwzdrro z^v9+H(??JJYwEtKk4~LBb>!r0llM+uGI`SE#)-dA+&yvegnweg_}|9w8vpRPXMEk* zpT}+oJO7K5 zq2CYPICRDkF|?258OhfqH3=p@QS|xFXgm&$%2)c8FZO$WxQl+m*T!@~+NCafOft*M z)XTXSyvFr*f6-5Qd0-Id45A-{a#gQXwq4Kpc#xhA$sv62gTSVB!udd6 z9vIXL@&kBz;M(c%?8nOkgIYZM^76n{+Tq!Ymj?#5c=qJwfg7C;&kQdQ3~KRA^9@#e zbsIOdEj8{@Q8VNU=YV6`VTJ;9BdsP5ibu6YVo|y%L4~nn@99- zULF|K;(3FY2M)Fl&s)4aFsQ}zI$wnLs?p7ShN<8P?HKkZpJ82~mSO+m<$=SX!}CvG z9vIZ(d5xC`E`biuKX`dyP>bhpygaZ^cX(dq<$*yhp1<=#diwOAc};`evt!y{d3j(^ z%d|i7^1$xd;rRx8fm(*W%*z9JAswEVczIw@ zi|2V>9ykm-JTLO{z@Qe-3%rnC{f_7lyr#jC(=qKiULF|KGVS-gkX}w8`Yo?%aO8AM z`yDS23~HJ73@;BHIUSy7d3j(^i|04Ike-qAOTNhI^)g5Cy^EqxZ}4kA!@5ANcli}B zPoD^VnwJL#wRoQ7<$;5(Q#Zfh<$*yho~L+u;9%?UJi*HYgIYX4N=)y4+5;qZ_1Y%ZV|x>~ z0OaraMdVtwqW|&07Hv|AW#u(ZHEgmL9bS!&^~(@P+?w$#>shQ;tD6fAWg6^50w;fS zg|Z8mBKv*1a+BR$M3TM~ol2H4lUwexq%8`TB-O5h-yRI&@?Hk_4qUDZg=y^V{EagYF9y}5#hCQ}$ zQ4ud%!a6G2^t$L~+N?2H5v9>Ik5)~pT@N}CJajo<3+0{8nwzEx%+I3psu*Lns5Oj= zbg_oAk&R~N1C1gbPs$o{r6nVi8A=hl$dKQ> z99Y-)G^Bt`S8SG}xG@^lXd1?fI*nN8Fx=>xh%tx2TOsExs$2B%u5uqU$t z*6f~-YaGP}=~UEwl)&R0xL0rY@54*|`v4KQT1}1Wf7{e>-DlVN|AVwuJ^J6?=l{Pq zjmH1t{C_PsT6mYv|Lf76yAL^+AKaDBI>uNqaoLn(Y8ud<)tXuueT-*@;nZPz(0FG-&{ZkSdM0N?o(59#lZ8K&n40D}`ss*&PCAe5H=b_tqlI_;)9rjJ_9 zO-D05f!Cium8gVfx4; zg;6#Ig%-PaKE_#jvBPsdwU~yUd_nUOEw7v@s+4b22Y5N{AyLz!^;~Z zzg$)I@KVUg6HD16a|flatG@1DDo}yycPh~DEET9gXpaK@?oxpYRM9EWyHL>Hq6TouWmpRJ3fhq5(U~ zx*I+Z@l6Oq2t$_oVX)q+9?e~n0g zy3}e2RKKGvq)#uk8Umr+H6eX!DRc#@h*iL@HZVK{N2HddPb_%?h3E2aBhBE!W-?=| zCgAhzI|JJu{C`{WL4hjTJ}CXuk{1x6l*1Hu`9kJg2(LQzh2BBEvlooqw-i5Sr`VfI&z5><3%jn|S)zl6zo{RTb3O|9i@2D;Dl`Bi^rBY1&+Ly#merLx_?=_2k!y!%hCV3%BlzS0?q~bV z5o0Hv6Igqgak%#Iwb3S1WO=qhHS_5V_U&^5zYPG!gRH8iQOkqnjh z6Ko|+q%#2z+b|R5bcD{;NX%ICDbgu35l#madYu_FT7k|c7#-yOs6if5unjNw9FxUp z0*X@iEU@cLSiPfp5zGh?Gb$g~Cw?7&1(6O*7UGuvD2Z1Mj3bq-lar|ar zKgX}G1~XKe_cSMTmHJjH^ zk@7Le=u3@k=$K$HH!1fb10^WF51U zQ97Za^Q18mnsWvmM$%g%ofd;Urhy&*5w^MZ{~QAx)-97E}n1d-bTxY8KvPAwS(dv;O@GiP_TmInd9^0z|$PRIuhS z7WV~uRfw%Q=anIpUP+?KY7nUqnOrW2B`m>2C`hQXtfJAZ2K_0=ToLpkdJAo*{a%O7 zBRE2kN4A4&eL<(bf8S1B1<#}GMiC-gtq2Jx%0-vap9nEwif;H*q^s<6^=h&R4Vq9W zYxa7WP}`biS*#XkC^CdN3Jye<&?#JaJnEtokzh)O#_Ek`vmv-H=6<_ZGh#K`Adufr~C^>}1p&2xKa!H*fW%mRFED=}Jem~`z(=;g45?MA-R9f?NuS}o~;#Ycln)2#lPS! zo3-+6gm!BP^IW{Cvm+s^KVi$7-E+Ps3f7&`7;d3S%FDP2;RCFFKev76uoWs~_Z?u7 zt2V%5+_*F1Q4=nsobY?%`fxfQ>G=slnC3!}T-+WF$@3XPp}>Q*Ha1s|XEMfQF_J6V zT}4aH7RUse9yEs9X*X(g`|Vc2eaNtc`~II44~vFnLl4hZ^C3#)6iw*3ibmgk+Q)&pWfuOxSBwyHj;1 z)0*3WK99*9<jNQkPclSniTUftT%vhgdhG)E4+Kih0 z9?EOljfz>hTJ1%I4Wpi{p~~Vh4QbT5*=#yV^=dE7vamgFDdO^yGi%GcW$9$S7>t|4 z#cDkms8kXr4_%}z@~XTHW^cBI%}d2>>s`z$I01q-yIIWUbUU!!rbn zfW2v@vdIOUis^74l}oq^IY!4gvz|t+5zz-?3Q){4VoW^0_8hRg($aM_l)+}VV}zIT zQSVZLHYlRU@&~3zf`l&V^VD-eB_*>bV=BPUEtk}(OiNxC@fuo77-0D%>Gx92*3wH7 zZ!LcWGrEdT;f^_61`93M)*^1TCFoVvNtwm&BJe~@HsfywJmHGZ>y(Ew?${g_3xs7x zJ?aDZffDwb&z^Idoi;y{!!p^LJXEq(^LkZjF5xp;x5@COA~Sat8RkKKR@}zYUIBW|bAAeZ~J zMPi4#qQB4?vvN*WN zohyr7IE#P^Y)%zT9_QweDWx?b>XL4=##;+l=<Gf$hDmPV_ns}xgKiikr=v&D3&)=YzR#cG~#yNyQIW@Duh@@3JORc{HX+^Db}n%t|m zw?$7Z_3r~j+$uFSN?osQ(StNKJ-0;xFX$9x)T4B{12(fR;HlQMdAjL0rORxd z)_aO5n;;mYidM-zmV$YggU%n-TIO+2vzf9}^X@dZS({JV)N!}EUi8V4Owbl>6pKhH z;L~p=Y@5-d(UeR$3RQ!zLTKir4y#MAE?^`YqV09eY&Fk&C23Po~WJ)df>LBPo6j>bSg6@Ko#Dfe@S1ahW-9gtQjJQ8Rfzi$h z|L+7w>vSNn+qjg-Wx3E$ab~m($`!fa1%Ac1ye1sUm*ZT$02Qq>6-s5WhZ_9hB<`hK zzbvF84Nfh`vdfMxQ};b|BUQ*6Ynq(Cs=y7jjzy6YA**{rYNQ-U`QvU&MO&r~J`Iua zX?NMtb9kj3Of?(kM#7?Tr*sW$UJ(maJ?fgb&gjT`!jPB`dpxPE%?OU-P%C}5Vkw=s zhsquoS_@!48o0XMgC|Q#HC1ge7@G2_P=SsPUL80(_x*pg=oZoJmu9J%f6RP(2AzIs z`lHj@sb{9notm5c+2rYy>n0wa$WQD)e)o7}d}{0)WBxJm=$A+BBd?8IJ%SCtJbc-( zPWl_^dD1OIKL_XcA1--BQji=VzDFDtPXop8!VkNsl12`+t*_={<$}7Z!_#SuHEPX_ zFS%fsTjZH|!5(6g(UeVZ&6E(mK1hP|+7xQ!?(^rq`l)~K=R+C$7nz-uGv&G+F7nLo=3;DE6IagcoIWW4OGscmif-&b(RJ_V~vQ@9qOv=iB0yCDJ zv9{v3KUsd}k0+k?mmfc{xc0@nE?;-S^trD-e$7AbTl2%Y@M-tmd;q(sXeo}UwTW^w zX1D14;W-uK3Yiv^wu+`uxKOT_z7*`I`8V4M%^4U7T+z_TdhzBH2vk77{hHrl^W%>rHtz7d5(Z zpVq8lVmZB$G$ho~WU8(BG}j})IPUkKd;O-vZ-4#VGfI!YaoC=E?_Vx?Y}?sV*#Wnm zsbd$nv=n0&Lq3*MhP}Abs)}PNxqObQ5azhWk@lsM2^CeVds6PaIbQ21p1L%#?c!gZ ze99xw?(zF47B0RuxOV-&yq9Tj)l9J;wV2KuXBX#MiuK+UT2#-ua5Ak8R^nQHim4*j zWYH4K2csdKB0?yQ6(*x$NMGAxaYJ(BwU_Pl@EO?`&X8b#e&Ne6Z=5*kgFmX)Gxy!5 zyTkSXyLfaX(`SnsuiRUxmtQ47yG)X}*}!X_()^Q@f&({>&d^nWV)jG?lT|x!+);1V2&w#mtEZ4QcPPCMICC*Tasx?pK#X_ zdd80?=e$mjEoM{}t)_UMrQGofR;jfWZ@u!Xudcg(qmGnpkex4mYU0EzjKSaR-F)QG zgWPIUcIJ=l;!!QdAUA_Vh??8$s5xkhEul)wfe#b5%tCa|XDaB-Io-lSGOw^3>9*nr zkN&|6>3vRqYj*2p&pvxf>V&|BpZe$9r#0_d_mQ*SKBlV0*u^7Tije}Lc7?(D3%Q)G zBCnf$Mt@Y6BHc|dLZkU=Z9xr zblH`+jGlP5;jDcwJCa>IqNTW?E~#BQV^fz?VR>_cNU0RTY}yqz;VDbGNCeUiB!iTS zjF$?vE&l4Kbk?6JU%O=J^XH#?`Nf~QhPnC>-}tv~zUqoCUwrA2$4-8NUEI`Ctn<%V zD++7GU36$wK6#C0)7}zel~LJ1vL4KN_KHW zOK}RvD<}d!N1_#n1<80VS|XIps7YUtnKMLPK2NELDlPg7+U!_-DV?4C;->L8KeFli zD>i-i)0bpzfm_}le)UTKBj5YwrCTOXWEa=B6w3^11L^QbRUwT@nYC%FrA$2$@+%1k zo5HCosz6vISF!uTSf>&C@u7OzYYV@9^ub>j-@5zZn=g2vcECTv`#k-gR91?fl<6|Mem5?dz}KidPKDtBxOgiE35$5SEBXV;N0=)P|fX zRme-SK9xz8!V(H=bHU>_C-7{cqgXbt)qUsL552zemCqelI8a+C?HgKr{Zj3>HlO&o z=@z_oxhAMZqD2LZxUvp?+v1BJ zS3L93nLj-Dt!>j^z4hlOU;oN@>6s@UJoV|nf9d6?PQC2Ye_!%~tOtCVTPn0_rAN9G~J=O~x5C7Hi` z&IZ^5nC=9~BOmtOJNH)j{;iK%o_gvc-_MQaYo~88zr4p6wf7Ng?mNoGE*{b@3ZEIZ zSyQY(8LCv#ayVQ=*#$PK$~M9!r7Xm_tVvvpG&M0>r_cNPHp$V~?;F)@xpVU=HPOfa z?S1{)P3dQdy-i=f|L>Qk6?;RJR~c`AjN%RAY`id=Z1E;mc;L z#f2(f)uv0;V#g~2Cv7?G!u0Rw&w3mm{mt`luG{;``o8}YZ2bAgH}Aja;q=`%v5N<_ z6o*w5IQc0QR$G{Sq@J}oWDOS;Nt@(KN5T*)$Vvvebxzd){eP!Du5Az1&ilacKlbA3 z2Y&xV+hyt(i6=_W4nK0sO&_qldC~3H-oh>(*i!6ms+qYY&V<37Zj8I=CFnebr6 zqCV@3){A*Q=1=^^~O$6kH; zHg@rVmSUGOT+e#7xIgHv+s(0k1Y?!<|ORS}R>@ZuXkIhxVe<7 z$I)!U=TKo;qq8KpH!?`4Gd2J8kQcp?`EPyx;(L8!19{Gc>mS(TOBZZ^eWtMIH(dYo zsn4>D`?VA!J}{s_vKfu0YN4D67AdlYggvE8rxxttWNpEy2+XBwmb5C|36K{TYn!is z?$38TtUh#m^P9=@Up)R3_D8G7ow#@O?#m^S-~NhS+_$CJ2~H4Ghvq5`YmiVYG^o94 zq>~YYQc2fUR(sxSDaUAqyRJ2ep+v%`SO*b`0SuLCiI>MASu4+f# zzrOaB9~^P&<=?pZ)z9tu_g~y}c3S?>!k@1=;l9U4qv?N=M;*#8?%h)CRlC%f-pnKv ziDKJdy;_RVN+M>ns9Zj>ZVWMGvl0th-04)xZ{Lz`?LAqAzne^0?Dw18CZ7HtWl0k65 zoD0d)1y_Q=nV;-q)nKs&tmH@ZDVfP=@H?{Z zvQpVdEOabZ*RT4Ma_+3_U%mX=zdv}<$7lcY$Jl4q|F|Fy|LbSp{@~v({5QKe-BRpz zCS-C4Nk*)c({FWzai(5Rg&?a{@^TjajKaG&AlfBwMN{$XSN z>09@D_%eFklV5GDeld?&lP$#qtlYE}Key*$&ph(n@2;Vq|NC>_nfuWb56IZN zzP@q)XHPrknmelV|8ld76D`F9%+t0NKl$?4J|HiC@t2w}yb#!8*7|0*|NP*j`u^j7 zaO2t7^WOhB*v0Xd;sNGO+lp^}`;03eJnH3hZoKwv@s+ke?6vLkHRrs2)b*!ccj~_^ zkEdTCCnS4{)`;Aq;jPlgq|xzh83O^->|w`bK9 zh(aru>!S6#K@Tm4?vT*P zvf0p=PAQkG@&*$FD_a|Jlu5h8VUO8E1~W!{a+w_w+AoF$MCeD;BX;|a20^YxG+M;Tg5|K@Y%KvW^P@MHAc3{fipgaOHM3QM z&gOhMo5KT13>RQBwN9%n8w2S+uO`<50Yy0BV&KDu)+Wu-DpRaQ!bOj*5-erYButj< zLDo}OryK#9Rx1yHoKDzo0b~i7jI$gInXo<$k6a5*mDZ5Xgx>+#z19~C5DsH85V4^J z6P*CrJdnijIbCFZbiMOtT7?d=vz~edQYCts{;A9_!$ux4E%x`nX zV8eHi$+SA$X0LVi@hb;)rHibOu6M`~omT5ig{(pN%~mEW!({z*?I6=?H6?41b`MTg z>LTl-YX@1M##2DsScKbsxkfJ2g!EJy4%!Zz0+Azf84^wT2^;)wD-V@E)J3P3%hYyj zGz%X?wu4S3m#J{0Ef#_4e1tp6hS_jFMCXVU*k1vUf5y(~qU)z@2c1$bQ|e810&YUr zu1vSRi>{xt9dru0Oi`l!Xm?Ap;@fTOqU)z@2OStG%FWTJ6RuqWZ@1V**H76pI$0%x zHLZi!-Pv7q{RXi+=(KW~)&ushbTwWpd**pvbp4d=pz9YZ0tWHt!gS!1q4yWTJ9G*e z5Ge;d-LA6o+kLo;uAj1Z==y$D2za{-y6F0SUcEyHF0< zJLrG}lDF!EdDt^o{=B@Ti>}|d>N2{X1_>A|edrZvB5w^i9$1^|Q{IH)lRSLr%XoeeJYq>aSB*O`(&2oc#FY2@}sxTsol} ze|G%Ban;zb#?Bp^AANFkarCH>$41T=*)aU*aBX;v^kHdHdhpPFLz$udB=<<-l0C(@ zi$mfmpnCOxm_@BrLPtcmt+fYGV;IvX0^pV;aE(ID4R`nJQ*{mPsUtNznbc?-b9zk# ztGNobG?UcSO}31-SSe&12HQdk%Q1_ZmfBkds0F7GdP07!!{7xsuVuXfwLKa)Iu`2q zfnzrT9&fB-N2ArWvbo^b zlTC#ofy7LRky%u?)P75V+Ni~-h|5EGDsM!}IW$%B=8C3ZF=C|3hO#RX(>okSIs=j% zF%<)nE4Z@#rU11ABw#QgwSrT7%UXf}Nfb>>wCbP@F}i@6jgZ@}A0YjL0f`ly+HVL@ zJ3#UU1JWuuwf`eP?EtA43`nZr)ZQ#at?PVLtOs2w2P zf&qyXoZ7DnP&+`f1q0G3IJI99pmu;%3kD=naB8m?pmu;n3kIZ6aB9CSKZm2wMDVfaD4Wq)l*YuM?nlfYb^GBu#K?KPy1(0Erb0NSWZ& zenx=W0n#cMkTAiiy>@NIQnnIah6)?AsDrW-SRHP`21u%4K)M8{_8I|d2S}-4K(Yj< z_G$rYi^3Bs7?3K#sr|G7wF9J6Fd$KaQ~N0aY6nQBU_hD#r}nC~RSWIQ*z0zKt%h5h zz7&;&t89Q&3I-%eaB4p(KW3$!KwY&+EglLG_r0>BO9wQ1%1wgb$NpD^a%zeMsR8` z7oc{4yH2@#yyj|fmZ zK+*&Q(jhptm#n-QWc>gs6AVa(;M86$KHp_xAlk6+pYBcD@3GGic{~ z%~x!`IlxLA_z^kO=5wH(H}J2t^SE2V`#X=pX0P||LS~grBUkmk8@6fBhkPkFE+b3? znr3{;C{gw_t=>RUmC+Ma$}30H1v%a@ghRW$H%wnM7fbImc#i!5Y*zoE|btxa*2yG#~T?N@y5MyS-`B?#)o8bW^AIxM;nQ zy)!RFdXO?yz58$h-6?Dqg-Ff`p1jerI zJnnYf4+779Pj()I%V%{mEnw?;u7%O(ZW^MdK!R=R?KUi1Rn;+{xuU3;B2}Z)9@k}p zrGP1HRT+2v=5e}KjpcGhi%XSrr6`<8HPE7kaxes##R$%)ELNgo))k7zfT@D3{1I7A z!6X_seKFWT5h?|;+R-Flwpjw;f&_bIK1S8c-kM&>xfbnL2abN8WO15v^o#Zt5Aq$f z^5@U8fo$J%s}$XizHa>h3E^CWI>^xgE%^cR!g~*V2l%pUDS&#f?knp!vvwZ-zBdQ& z^soc$+Hbse+G_{RS+9vORc?DDb0NADS(AGv?zl;Kx~Pm%sh zni%@e&}BnMNp6=A;%|#xqI*TYWna2Ox@Et<|I7O&@O}xbTms3(gGVB?Zd$Z{fTh%x z*Cac4_J+8Vi4vgzxYjV82$W)r2LUoAARAzTRWRAk^DK)~iw6R#O(*TUet<<+!Bp*U zquxi%An=L&+@Vc@#RE8Vz?%=SSS#3^_BlM@o%ip&^8ib?E3Ybc>XP@J;p;TGcg`&C z2h2GUyz>A{zJkr^?$jfT`vSgsz&F5JuwcG-4#5Ooybs{vym)}cVZl6*7w_Gv(E%2W z1yk|Xs2ZA6YnzE@t84Qwfc8|$~UP5|4&ecfahCjk%Vodc|dugt^aofDn+atU7|7tFV_ca8%d z&N~NKGZ)MQdFL3Q;=FT!b#%d0yxytk*`|$h=72XJVEtXNIc@JeZE*x}YycbsoMo`` z99ze0%DH?Zlvx}G6r5uYurM!}qE}}t1$uy(IyZP04 zmffwu$W^-*%-p0XQtp?z*|5#&4XfF-A=mRFYDdIuAutq+Rke{_EZBSqDC}!{BBoZjn%qGE$A*gEE&8O@`NKcHeYhHZk4mwb#=JMXjO#W zt`Inv6D`Yq`Mffx$swwGtl>76t%!+K*8I5$;fmzZ*m1jEfvwv0+F>s^SS`saSa2v8 zZKBF9x7tDXx)F`gTIZ;gIfmznlEp+ROEsBFiKS!4xuiFWn#`%JOPh%0>`2Lg#*W?X z3T!vOZ5iF&3XER0+mLPP{9M4p$^t~b6jZS0Fc$X(dU=?wIp>ujlwL`q$!ZX(5Sd&q zh$Sq+L?}q8vaF)ftOorl$6T=nE^y!73XF=cRvSiT@Dhu~8ZlRhR>~e(1&!^yyA{~(ei58;w=1yS z{7N@$cPlV*)e4Mm=8L2|T(n_&1?i@8mMm-V_iDqibkG_nQpHF$j?3wA6dWEH)EIr~ zId3`x4&2aPwD6K|Bo8I zUNrli*^kf0XSK6qGf&QZd8R&NnmJ(lh3RikpF8cIUO)Bsse3_Az{yh^CSRGnfAZ?d z@}zC@@QFW8d~@Q=3FE}f_!HyTjc3M@v44-hI%gXTs9N(eBfIf_2S+|$4d(?+;un+Wx!D%uc zpAU0V1h@!EeJ}?*q&E!Cm$G{?gT;U{tZmalQt|O?58hhJH)82P2|kk6;HE5olMQpa zHXbMyAG`LTtujUH9ijJT+-8y?Xs-$1bO4Y${J=hPnZ`n?l8+P@65);ea|#dWBZ!+& z18H*F9oWYGIJy1%$k}nH-iVW=*NN=Q#mRoX3qT1UA7*u1O}L&!8C1OwS8)6G#=%XG z6Qfv0@3k46>b*JGKD}XZJ7C6LPPftLrSL6#NyXN+dv6WJi(IY;w^edZshfj}(?r`1 zw8ihn*e!c<@_Q|lm&;&zFc;=094!T`O+f}5;fhes8L*dQ;%m{<#r>Wy2* zIr)iY@<`VUcxVGSF^zSa&0EGe`SE4)It8pBWjqw&BPkr)GRn!1EtBuA0?bFcTqfK} zVOvHx`O#(aYAvka@A853$4H;cwPl!-A6X`^?6TkDw$KcXVT@^ul#?G`F5gw2e!Z8) z>^{(LZ61<}^&6yHGmTI#m*#vL@@EkCofbb4K2idN*A8uMxqc-N8Usk!YxnAXq|;_4 zP7q7QskM@=GI=o)OO(nWR$3@IvuYvtf-O9*cT4Wh?JusFqi{}ZBHMKce~ zFf)ft|9<*w)8*;orlnIqnEDv_-d_(+0K8@Lj7e;AYT{=TpPmR$96kQ(_?_eDjN8Wd z8T;kf=f~1xma!v7|1$d!m_G>49yRm!%q63@j-EYg8r@^$$&t^D#7AVquMdA``21nV z@B!eSz%NVlQms@p^w7|yL*Ah^k{2X5N$QfVk}>g*#aD_?6CWY^3ovKJAEd}NWoPwn zTeNWFf_i+I)nvk4Mx?;u&g_GO%gM(u2A2zS>2V~_;Xc?Kr-8=`Mhk;ddOvHzkQ|3Q zqYtjDOLaL+R)^i|p^z+xJG~DM?&F*kX-2ISX&{jdhimr1!JV9o_1ahm>alr|G>2>S z!NG$);C%*y^?9r)lHzdnJ~+4@7?hzcMn8?4ktBz!^})gQ;2>z9o+3!28A))sYHu6@ z4~GcKs&}|CqZ>ow9Iny_2iJq0@)0%@?(vdHjKh`t;NbG49Ddy7wAg7g66J8EJ~+7k zEDn!fZ}VC`4kW_iihXc!J+L;0#RyDqvm;>+SLlO-`)G%Sv3slx^2 zGwQPYT`m$inZrf<;9x%^X_B@Rgq@<0lQ>*hAkJfT`5A}F%Mgg4!-e|bU_T?lcbyY8 z6Sy1kakzj$oC&2JPP^Xdr4TQNTj+y>{nP66>Oo6kHW?5PhdZ?o4)#y8#cid14#MO> zSPplxK%CKQVT~k-c`(Gy;rxAYuwNS}gURMZO>Qs3a5!IYoC@}9uh(xjc`?1;hqwm8 zF(iNxM!VOJI5`~Khp#IhaNMg$T~;%OSU4Qj2M5@VO`$VwUY1{;HDIoxr5aIn9)4MqzHd=P?&hQl4(2M7C$*-lzKpa*p_ z2*Tlx>4Ss)g#@4RF4XR%^oW|n>H6Th{DrccXo9laZHS7)Y5U+{e=%`$P+phcb9FixDN!562)ip+C3Wgi^uFRasRwmVIJqZir2;S_yvu)o;-B!%KmoWYSf4kzz} zgZ;&7^O{X=a34LPJsRLP$ok-5f7y9H5TQ9ncWR)-Qyho0H-`!?eGBS$fj-`5 zbIVD9+n?@(gG&({_S|g9Z5+hW2LiuN3xZfV>+F3XaIDd0H;dVvZU;6Yv5H)xZ-{5V zH~XpC@+>*KVOBcxyP3OZJ~>mKvCkYeGd%s9={u%BFzBZD3{~Cr*nA z@jB7Jr>vqU#lHmm|1Xn>B^yQGnA!wR4tQqrj>#(~E0gr(oaAarYG{Mx#}ltk+%d6j z!aT7D$Q8I|JTiXt*gwYZ9NP|N5B3DN30ylG9i0O=4&F6#&WLqnui>YLKQkN~-Xi^n z^iJt^sYSZ?&@YBQI~13^1||m9OXV9RTa~ci#wuX4urr4u-p~<(gLh{k(c5jIu9kh7 z&|4iL*!h+Tz0nbZym4CJ!$q$xy&ha;JF5~)+p3gsTewU}(h-7PdYRA?9qd%g zgho1=U}s$>bU3J-HGK0na;b5%Wb49$p0ZO53vqMvxZLY;~e`Oi}=5&hwcHJu^rP zKqz&RejIiJA+y)=?b#Y;v%En@{v9ULYKb7-nW2KGx+ni$2W@+}X*rTA7UUvVOnMM|pA4 zCwYNzbXYB@%}u*pZd!B|FA$DB)CMMJXws#(h_2)X!fgbhr+i?vXLHb^D|mr${adYG z7g!}SU`Ej=c!6D>>NOfnU{S?MiayQ@gguq=nP@kO>j{_WV|<uONcJ#1;U=Hw|hJW5BIq)`Y123%Tw(} zmU5Udw_kKAFA(-ruY>m4DX>at5PgIf*yX9D*-Q|$%WM)|!V82w6*HPigVSiH8PUbO zK-g0$i^pxm3|`==7x4mNhqLLOPH@dPPMJg(^7##n+mS7>fsaoH5BKe?12j_@s@ID? z%nOA5#%gnb`|mKT(ImQn7YO@}+3z$s^bQ8~h|cE)!hYk@qqN@zyc`#u#|wn3-{v7r z7;ZP}anZTFK)CwBl#t!y_G6$iK8F_wS3lvj>A`|2%i2UA;swIt@9=s_w-+~%glHQt z5UzjB1kyJc+-xF5+Ygt{gOMj((>srXL=UxP;7VO4bN^cDJQxhXGA@VD&43=!iisX< zYk_NcnU;IoGH_)sllg9229Eb-GWWD);JRHVb5~miuEu3DceG{T8e1lFTW1K&YtXA5 zAzq9A))C@0=`S52UYlO&2*Dn+%>z9R&C$TFcn zbcA@F9HJBPq`iGKL_%O?MT+yk56nz?$7?mOH&|^ zVprGyC&WJyp`uqrl2OqsVEuhTym9tHa2B9v_RyI>%zSO8G;_?17@P_C(P{7WVN=gd zeSNAtb?lU6^5Mx#Cq0vEz|DX+OjIV0n;05@Wc;J!-toi6UKqP^tU7l5m~`~}qnC~P zM%RqIIC9fSZRCWJ;o%<)Uq0*~UMqb;db6}HJyAL`^uwW#4V^T!PVyqi0;oy00z+D!#YppI7=LU&F729ovD@pkH)YX}#z=ULYKRj338ol(l+j(dT)Aa8QCZL>eW$ z|Bt=*fOF-l?#HEFZExTL2HW`g`MJRJ?oOBVY-39rwUIQVkwznpp6x}w_by{#{TxE? zm>N2v88D{Alq4j9gl^i8@FRrkgcMrxBmcAyiUjSkxd&hKJ%-_<094Nl|&p#X?owcB;y(IkWoP7L#4SPVJEsP2Dj5{?-h z>j$qH78b0ezpY6K8=Szgyx{cO8BzC_nuM^y*&~3|?T!jA-M6&t2G+*#=ww@q-&oj} zmtpKSUgUXB_rEj=VY>-<`G7lM4|oH*Z)y_4J#}#)n3A^G7`N^lnuM_3cqE(O9q_U~ zi|*^1gs|Os`H&EGM(qBu?rWNau-!x?aQ}o;2=QUvS2YP?yK(s}7Pl1i2$JqWO+t9s z2!_02H^&NWME4~v^~16mo`7v>@uT2j!{_h>JVH3&2+p#Z?$ertu=o4HsVNfyf%6XCA88VfdA}58!!+aZu)0ra62f*92-;nN5QqY!b)V3( z8(65r6VOd9eq&K?7FXC}@!35RtNQ~@LfCF1L65^~b+{~|?qiySu-!PocCX88_c{dK zM>Ppy*YJRl06G#5S!vxzGznqZc*O|q5j`R!={~GU2pe3KbvRrhuyy0q-LFXq8(hE= z_PSYclx@*{NRtpYI1sjCi?U!h#;LnclMpsIE@}m{aR3;P?t_|yV+I!n%PGH;4*7KN z*V+t+Wiz@Ne!Z5Z!i2*U@RM2{9gADDI2m6Uq<1)dx({d)!gk|e*?{O`MXy_TuO=bf zQ;*-~7wt47xpeQ^9RGjqOj|d5 z-z+op(9HX1+8eLmAlARKo?V|=d*d3l`qkC^s$u0#D|fEkx^ngM&zE0Qz!C+M>J_v#z^8})O#uYmu7AN>z?>G9Xj z!~bN~b@**Q*5YAHfIJ1xn03mB(>UxN!vHQGAUO92r_|T%vbP$0a<03+yk$XH1@@O=`i0EJdEsO(mR`^qq_Bgs$b985aQ?2tQm z^B+&uxukyr7ud~)c_1>Eo2KN#X>&OcASXv64dY#9F4PyMdX}g93^p*;P7~oudewEJ z5#V~G$8h?IS1V7g1ngdeVAW)9A5J9<(yj4? zu=fmuWJ%XKkPUNYu5f~rN8Q-TEAp1$Ogc+L6rVlkc34%S{uw zz&<`qQj)p6Z@M8hdE*2w@C-RjX_C3T;oj+<<%S7dVBa4mK*?NQurU>vwfPy{HM-C0 zRNnec%U@qEF3&H$W$D!7Hy6u`iwkdCxN-hl^Htqv^(W_-=iV?!7`|*sgPQcm2x``)zSzd&#!8sLZfCG6LgrfjDA+6o~4h-#R&!sFIxtTwr$;EI}b^mv_Bn zE0}mo5Tj50fs@YGMn|kpNb6_T^HpMHrbspyurtvQHua>PjE(j65#gHUp;Lq zZpqdJF7Rq2;EjM>M0YqU$Xwoc#dKVn6S%;3z<_#HZXiC^Dsy?EWjZd630z<^urjpO z%UVMAfXwB&`sujTCvbtSAmp}y=y9LlX_2|K+|zNXX>&2bgQwGO4f-P90PU8!)ZQ~4 zm+AyAu#IsJd(iFmfrA{GOKi%Cx-x+aJakF{SHK&z^HNyma`ShmdzSJ9F7Q0S`Xg3n z1Pld(MTfLL9hcGsF7Ws)g`8f&?`Q0+{47tNZjw$GCvbti(k*ZlApi@o@y9-P>`fB$d(3NKJqUwn~qCv0vFgThb5?F zE`M=qIxg7>T;LIASg=aw@+VK2j!QvQOyC0h%djk$%;kdTPRAuafeUQO z!y;ZXm)R+~#3pcoEqPe-3vl`H&$gy}mS<>l(I^lmbNSYk^Zw2RF0dsJ%ZbTc?$1p3 zEVobK0^8WI_?XP)jnkdWmd0V)vf&1${(z}Vx_d=0=EJDM)w2V zCzrpo{ITU%ESHy=bJS}@Fi zasCza()=-SH^2wL-2hLYn=^dL@Jd6}aOLdZ&3IPme3{&1Uqi)9H z5k^wpU#+FZv1o*mly}VxWDFj5MpE82GmtTOJv@@~u9--T!3H;y^8RwoXN1SDk(BqB zY7)W*HZ)6yb3W^lvIbh97Ss9?H7$Xwsm>L>!+&2T?e_Z%arccNBA1cB{lxI_AEO*+_Y zhFhM`(xih&o8cbjotkv8?uWaDcWBbVW6f|g^O>4-u;&l=^LmwdNi7D%Uw8sfn?SWQ4v4?@xcmVUj9tL*w1DKEPVPLf$z16Sn~(i{rf%$R`UVSzwU!zogM)F%ihCja=UM@p|N$yfrkFi zK1fsf|FjR%)a@_!L7K`vn`^JB-Ltv&np*k$eRi5!+0UaNoBI!l;GzANG_~@heUPSB zez2z%&4xa>*N}Gp|M&Jmnp*kpK1fq5-`NLgYUSJeAWf}&YagVkm2d8YG_~^eeUPU0 zes&+EDa_C8gEY1BY320(VUUeO888gfasD>v|I6>b`W>DALH#}Y9gzF~l8w;DWuOA! zd)I4&-2cz5y>v}nyL|Pb)%UH|SIyv^|1So)|4&)|o8^0#8_TDb^-G^ydg&nd|8E!H zzt~*7VR2^R^9wH<2w&5m&VfKr&ub7p<4FEp@ zx&Qqc3%CK`gzg`8_v?PF`~rSol*~v z?IZOD{zj7{Jl>Dg8~Cv%A#C0w^#*>ZNeIsiBlQNpuSp1-=}5hSzt$v#=Z}$k1AnDu zaIpS|^#(K(hqdYrXs6V}3`gn>XlKU51~*b~Kr>U_1RLB)y#dXHa}#WEBlQL}^TtiE z!Hv`#(99b*jTzjq-hgJ_xCu76k$MAP(XyMdYi3634SZS4ZeVQ;>kVio4v(F~jno_X zl2%8Ju-%N*8_>*DH^My~sW+gRscwYrW~AQ0=QXAb}l}w{*ihEf1=3|R{u!7fj`tFgta(Q zZ{U-fgs>Jz>J5BclMvS8NWB5gq@M(PcGNXu?u zp$_W}Xl8MbrE87U8_>++CSki7sW$-f)YVs-By2Y$^#<mYW~APL zX5Kgn8{9~}0nPMp5;nMzdIRs)d`8&dM(Pc`OOp^bxRH7TzpqI+W^luL1E4~gdYNcg zHly_hG|NQ8gu{9RPu1$ER=okuif9CEHzV~1-mb|J?&(OqfwyWB!s;KXH=tQAj)2ua zQg1-B*f#;If27{Pn>D)%tAC{4z?+s0uhr4I2WJkR|Igh#KVx{;`ujlrzZ=$P*S@&+ z%C*yL$5;P;_5Rh~>Mg4aD_>c8HK+%0)$%_qe`vYCY*}7hdT{BUrQ4QHEdJx-{fj-r z&Z2d3Y4&G^Yi2((d&jJG=Bu+yGp||r`poS!SL=VO@9RITx9A>Rc*8nFVDSkE}^>zbjtjQ-sUys0KXrORI|{uE)o`C;DGv|G;b1bDtADfU5ufdCUCb+=FxX%-uG3!tjrV`wczAErzApug<<^_I7Y9;7?~hynpil z;NIN>gZlqJR9YdAE-^s_z>AeIkA3#vAc0ZEAgCEdkcXEl*jRvY$j34~xU3ZvI@UUM z#>N7K1CkX~ofjikM)!~=NBC_+^7$-ozu)fQbXv#N*jRwD&o23FVV5QB*Q^1LjRgpM z?K~JH<-A3m);Tve79boDq^RKaSgfpO@pue{-_#vEXLSTT(V$PKbwG}x@G{=-3Oae7 zx6%&XUu(QJj772!ufRpLlrc7~dZmb6aC6|6X3Z-1*jQvS>!u?7u;6CgX_Drf&efC-l!3*LRfV8ISZ%zmZorEgBywe(TX0>jQ{!O z|A_b>%@_wW{F*uv|D#z{f2zN}$ws2MiXk@z3YC8(5Oz;NwNc@jxaD-O;k7jU$R{YOrwbZW_|D$DQMEB@zC zwCo1f#v#Q2Xx^!y760?cTB6a4|IxgRK`Z`8GjhZT4^$)ZKbp5V7~z3xB>qP;Crn zpkOVI#Q$gq)xcUDiT}~OH$p4^NAunYt@xk&G}Qsy%}D&u2es@57V2Td|H$?KS9BlL zt$cC$IcwO`lNRou_ZS|Uy>sRX>#qYJAGM#mlHo=D=_~J!>2y2F6=%oJ`G#eqeYB-3 zVcoWbIgnV`+;Tey|8B&b=6;f{5FBmk*z%T+PhhK2r`Sq5i=|jt?75xIVv4VHe6+nI zVa0;mnJmV07PFhSdqA6gDGcy7E1JN>UPS($Ml!t9(TZb4)0ek&Il&HSnY2&HblJY8 z;}t?US5o60&BwxgCh016z-3uxx6_fAFtMNL$o$=)|1y8F9WSv(nU9oDw!IFkk+DoEffdW~k^@Obn4+86tYD=)*J;L)2vJDo=};%VDY%79%Gl0OeOovk zDtTK>$}W0rnY)sqvtmB2B4*f8hZU`lkeHFf5_1f3xL6 z+&?Ta2g9NeDY1gj*{vrFV#^z3@sLHqn!UMP-BWQ{=>TGJAa-Y!X0U9oU=o5@A(zBN zbGX+x`DqW%; z-NO`>Ul48Y|@LeufQ8saD%< zuXDAyrC;}DWHIw+#Vk%K=3x>1-$pUB1(IV?F_Ls;?Ltq&J$b1s4yJ2pZ1(rbU_Z=t zoYj(DNTHc}kRvOdc2cs2(-m_R_0T~_rCB5!jgBm4|5-7IRKyIg*`Z@O4eSZnKx=qyD%HDY;EjI$V$~)_c}s zIc1C0xWwT+iS-ezjrl`c8waswnlC4-VlFBL?L{G-ueM@})12Umr`l{6m-^LwC({-1 zZa(A*xJ`|m+2Sl#EFroRY!Quau_%j~J1gdZikRVj;o-#{M@Y=XVGRu4Vs#U|CLu9r zwGqcecaqLZF{OdQ+uw{J6l=kKwtPEIwVb6&B~IsbOa7q32X z<$EjH<$qp&CaC`ZvZW_4zGHD~;eiF~{Fmm%xgX5s4Zkux%diMC_%ECJ{TW362|W$& z`HKv<3!2%r)4Owq#l70nDi^PfCBmM-;wKQ|Ly&t${|#$XAMM{ zcvWZ-VMlE%V!^Ues-2`It5FE0sy3n!^?NN@KL)sdwV9rVE4-Js&lPnrSXJEWWyMyo zSjWN=Ys|V*&Pb-wPn*guqD+%mmaDY7c;A)Gl)Ba4}2CGaa$h-Mu-q~z%btGo4Hf=3euYIajF*o3vn1(C-uDs8+ z+7*p`oG5R1`AW%A-x7P#Tx83`;3aQ0%%Lg2Q0KcT(t+{Kc8iHvS|*y8_4=u1d>XFs ztK&Xb!Ea6x0iU&JtOh!*La&SA0Ul)DV2!O9n`U`)XG=s=)>5u(tvS%`I9;=ww+38e z({P2~r1rV;eru$k3)>u4zc(YfOcH6qD_b?8@AkQ)owUbmi)|Idex$w4O6gECU88Ji znd^PcXH3HtUh(d8jb~fNc2C0SToA3AQt?z=YUENmEW!G`TU@(MigvJEGzDsCsO!j8 zEnJ)o_W;)qHg~4rI+pf)_F35gn(VsCEsHfTH4cw6uOma87ruWx-9*h(;sa5>tm*l?@Z zBciAgjieBNB-`)VQh@85o3~BFRmF3hWq&M%1)AB6)$dFtwmF+KV&+(zHBk4^xV4ik zx`LGkh2cd@H&RS)*%_LbpY`RZJJx6_ zB^7e1x-FLLR?4pJq{|fC5_pLNT<>n4o`$Q6zXrr;wX;>W7g(}gNLkU02cz*;RU%#8 zNzkOfxzSR~64;9Eh>M?T?F&SgPL*Ib2vf(BS%B zBTtljTd|ZsVX_vwMKjoUjr6O66p4s!dCuuIrD?dTcup~23R`>b?Sv!buNV{IXj@aoDY%Xu{Oo&9woc$BscJ#Ixk#zt z2-pd!=}NJP-w{ie!m(~F5pdx~tDo(qq7vT%Z^mIlw&6xIJPlVh&k3~x3}G|okZp60 zmuh%1Y^iJ&ocX9-D5c$%EmtPDT}{Q?65ngNV*L^qDYgLDd{dl;tBMVi+uQDVg%7p2 zEo>~hW!<*XHg9Xo+75KQyN*oT;)tcg?UIcsH%NOfHsC5u!&Sv|>;V&6 zcU8LfxZBik*F=hnl3ScJQfODXCXJH;F5rk|vU!uQ66TGaChywX4g;>wXa=X@sxs3h z+r@5<4);60c$dxx?QK`h)C4cCRivHna#<(FHap31(3_^fW}%RFc<6Lh)@!sGn1-v0 zzebQU9pZ^ZCFmCVUMk%#h`xHO=4&xh4{sE^TTQkbPe+^rn0U;*IltX41ZB?&HTh|{ zs`#t56UmnI-JGdW>+xvE6e>%$!WL66B&CMGQDI0cmrXMbORN$Qm`V%G`!=`ytU}X2 z1=q1m{e6G6RyrtEti@Q5!_mQ0Hdi%m@3&ZVJ5y;FSg>;Ndr&vK)k_qRXfy5E7Dc=( zkBEYiTRp8iy9i%Wd5wbKRsmtK$xhQ<6-RHgL6@)VG@H1Tgr-bQv=H&QIU=1ZI%177 z8e@FcSj1T3BThO}_hB_IkLM}CHDL5j(Or1;4hZ?Ccbe|1_^LhHF*f5N4()WCLZ-!a z0;WDVAlpKO_7=H~_*;!;BhAMt=8m=lM1YM!oTBXjKqc#*R z+Jc_6$6Vrg+~P*rUegm`t#rlGwF}N-6m0g<#X>b^l`V;Fx~Ji)GD3K2V9ZICsjY3K zN_8yW3|9^YTdb{A41}73w-WbBcy2r3sYvBsL2_G}h&e1vm~FbI;i@u1u!OJM_C{T9 z#v8AjN@&neb?O`vNtrw;PgaZsgMJ@~gCZ#^(KT6WUbYUH)xkk*rrwuai7vGzuOqxo#}r)0DkSWW z5Zl&3wNoU#-DWMiwdLOKo1@&l&w`O3F*3zsg&$;p5>Jr zb*s8FI>QszADsCc{YP|XmRA0D<{jyFEXw3WldY00aTRV;WX(8< zJ8hZBkrUXV?bx25Xom2S6W9^o6cmq|!1zOpFpw~wjG)nA5Q(;UJX-M}MLwyti)za- zJ59Rb=w~Xmd_3sOB=Jk#?e|r zL^_^$FVJlVnPxW>&S!0z+mD*SLWd@>^Amv_lJozUXBKqpuU{ipzOtNMnpt@ByxH)L z*%J8htw&hB`MRU0!;-< z{${Xiu2_@R`mlcWr97au7p3x_3Tt2d4Y ztO?qhiWMEG2Nwk~Tu`uO>yF4KY(+C0EZ90lq2rUNMo#pbvIItlH?yWdm9AzibTx-& z_z@{rCP^9I9zRl2rVmNVjzB(EayL191LNstJw-ROV%Vf0WjYe3lR>bQ3AD>tuxO?o zo4q{Y7lS(>$iFCVvhluCq{SU?!8~fq<-20@r|*tkp>mVn*xk41O~{JD(OMa9KJ2hI z(W49*S~E^{H0 zwX{lg@8;;BT%06j_`v=sNtr#YWt>CogO$Jc zrGnKci_$sNZ81j!JwDTFSez!tpJ;pR)F#&Pp!QK)E=-a#9HDTeqhTer!2hK+aa6nP!DOe^OcZK zXmDP($ND!VcRI*9LggT4?d*&$F!FNz|FTZhtyfmRx8hs+)Z)qc*Uv4@cJ)62A3oxr zGwawQShrkxg?&!>jdCo~>AY$0oL4TPBYyl(2ShIyjfVXE!)q9Ss$M*H@)(fo?lUH5#}rH(L3O~LGKj|rxLLRlb`rH+A#N$1N6+fm{NXU9-eS3mJ9<^!x+!bk*pNkN7#v zuzJqa3h{jV%=-0@DI^mM@l>SXK>0AyiRFF0kPT(LN<)@?bEnWs8*!$(*_On%t6UX3%H_UGVPCKy1r7tA5;uZNY*)0N(YcS=iEeFwv5+lMoXDW zf|IChz(rSjj;_^O6wD$GJVfN90UM}K7MfZij`yEgf9eEznJ}#*;me{W{u$Hz@N-~eCCBSx6N$N==I;wzf<4QyY-iXy8`|g zbYB0ZN4!h%7~zqUz@|ZeTCY;lih#E#0t2*4Wu`GSf2Bch(+4#Q3~__@)C#)5@8awL z%`5M15o7Nq1GHZS4KJF#1GHKNA9w-b8K7B}wqZ2$M1y`?@4E#)oOKUA_l|w;8no_F zX&pXDcMi~u3L56=7@*zCXdJ$)(>_4ERM4<>&;ztn1r2M{Hb6U6(C}-&b%3_3pkdux z254FZ4eS1v0ota5hP8R~0Bu!4!xQP#2WX258rJ1a1N1E_Xjqpw4$wC%qsQ)fykUTT zx(XWB<*5PsCKWWSOY;DIqY4_w^Y!uE}u3)Uq2=KsRQ&$6*O#B*A385Q$fRaa_s90{n!-M^a0r( zJb2=ZmC>+gUNit-qzs0g=fbNEdXN6XTN2rNGu3Fe8mamwZr+6WUob#jZ~%oOFzWHw z81z2<;}4)P9EV|#8^9iS0ESXyu#Ew1;~)$p;nvm%uyqJVz_7IeYz=~qF1^p)kwk0c!aGiW=+K(g3w|0A@5SsYE`RafEmJ|FD%X6f}r(O zCf8^J`x{`fgSWCl%`PwgT<6x`tXrDhxOwC8>px!q==$%1_x_V>zg~NA@zaa1SWGTb zi;D~2S-5xMSquJ!iZ0Pml>drq9Y&hYPsFBslnC>m}utj~Ub z_P*KO*}&{oGryYo?98iYQZvNNtp4lzx9Y3MM`b8>>!- z7{IoK?o}#71OgA`y5HR+c%>QvJm~74yGQT}H3E3d(*4dJ!OPVM;3b0Y?mdE+sS&_4 zx$ev!!AsQ$;E_=Wl5%EF&*)yFOkjp*f8FjL{>3WzF`+(Z3$(A-y+{QQPrlFIgTGJ( z4o{NL+JnD91rE#e&OP|^Rp78RpScHro(de6-4E5@gFjaV4okkb2mc)v zI4t8l?LdZ3K|x8ae(fs zpkWCX2I!6o8kS&wfNm?J$BN+P2I!Uw8kS&ofNrXwVVPtG=!Oa!mUnu9uB)J738n_< znhF}W*yI3RRYAk@P7Kf$6*MgG_yAp2MvsN|#Rlk-3L1Vxd&U4=P(j1m*%_eoDrk7X zx_z~-yLfB`F=#^N{*|$MOTymd_fBPwCLqy2w+9)?%5-F zn;HSEYu&5&2;Qnj0IOT~sy%|Ys1S_ZgQ$Du9>JT{2;hlF_liA&H>nZ8de^;tkKp&z z2w>IgUbaW@Ml}N1H+3)FBY1-v!I-9BvPbZGHG(lszj%+}b!r4-ntst9!E4nB#uWF$ zJ%ZP$5R6^Wt9!v7!98jOu(#@-zen(DHG(n4Jr5k&T|ef7kA0vhfj!~AM@6{Um~h`X z7|Sxs__2EVw++x~6*O$$w+_(BDbc3~=!6Oyc7^Bw9h(v@4bVF(XxM8a1N7}GXn1f8 z574(xi53Uws0tc($IzZS->nQsu`%a*!yduA)Ck7Ltk>@m{Jt8&*r@orz5D-Osat>D z8V2qFOfLR<;hyQW zrJ#eveM(ax9S;WF?R1&R8J$Rp!OGEanju>uq{!8>J3F+!QK|0;7|kPtmB#6_Vpe&5 z7{7#@5xB5NV5P_r67!+O|3+;j$SZ8N`9d-*qK=YiL&3FZO4r_gcAKJP( zfm*wfJ`;%{Od}d>rh~NAr&yGa>y*hzu7L*XTq+$&OQw(`++f?$3e&1dt~}?IifzBe zn{2gHvY4f_Vvef}8t}TA2}V7l#lc0vm25}l3UcYt*3D5dnr7|RhKSkwqF8K)iqVv$ zw4h^l;&z)5ibQJ7c(~#Mmy%FCvPm^c+)l2Dm8w0dQm*&~CLxApF-OjdS#4hc-bEZ{ zMjst9AN6c+&c`*>_llW9Y0i6>LhZ5G{rxBEliQy4S8$na&btcM8TV)9FgVR+{d|<_fCOozTCggM8 zmkk}-!sqOoi`PI-oFX(~Y|8p(5Gy6)XydY;)$k zCft{%1Eix+$>o`%kM2oKD#6){G1f?@@;qy``7^avaI~7&4<|_(K9xCAQa-HV!eJJ< zT(cbwv4v=aZ_XYiDIZ#8I=a~o+Ot7W?A$CyEVWdKDY2qbl|`G6cT^+oFi%&rxdIXj zm3tzG`g&B94q>o8^$r9Ze_6q+9T?SxW%ii=C*MTwJMIC+fH-VKIuMQvTi~DZ^W{ zM@q_vRhiD#Ohv(^grbXafUC<;K4c++HbOzlbjIDq?TNIl(CC+1jNgKU+?~c|+2S|a zT4J9h(6qk~?l=cK0Had=>q%0EcV>^0ln<>WxwAtC%0*vN%rv$a{>+(m(*%V)YMYYh zu8<=hUoecxm|7uk{MR$<#t8~})E2(yu8<=hi7<@bm|h{*oLQ$PDCALFBcHoMj(E(% zFwSFYg*@MLW}Td%kVkEY|J)UF#G@Ph4@ZPdt&qmA&a4v?6aq&QAE8y&5s!ly#*R#_ zklTNKW*wiPkfZw^a?~RxhEXO{E9BOf$mjop?i;#|uW!6#qrKtUc=GyB*Z*k!W$W>E zVtsz?+iUM#d*&Luc5L;ZS3k3Q52*WhYISwxdn@;?+_fUCT(kUd%U@W2<8pcVmgNhU z9$Na?(({(0OV=;{X7Q_wZ(D3GIudd@VrZ1}F>1BPcC{Dv!M|9SS$W?uzr{!z0FGvA(h&rE;DGjo~#XZkR6vxF>;h%#J);0VAsV_}<986G(ZN6jdTWTI)m%AjDI)P=rbsqE4?eJP5%_6d^fJ zx?EC*2Ov0sB80>4&7_p!JOsy41h=@lP8F^CAvlI2Sc8wMsfmN&Kuf4Qoero-h8>3^ ztrXq&DEHmBY2Zd&8%50KP|1^ni~Q_+HYlVxM587XX*SUb7p?rD^PoK50W@wl`U1JA zU$H0ipoHB4G-jqaG2B&t%z044?f@D!lg+G^3o5q_O4vbYqMfW+l^tUql(0L1M$CAq z8HlsWZG#eaqiCbq40123Dt0~(O4uDhqh=)P4P;c>1|`)Fpb?YV;pIBYBjY?MsWytH z%qCNyknqHn`wdE}9YC8*#)c=(hm_Hvq}l-qBaFss#vbqD*|C z&E=Hao`=u`WegYCq)Qn+1EFz>%6S|OnnCtWb$JOO_4~wRdXwQ@!XFfG)b6CjFnN5;13}* zL74i*XoOal$qyhjP8fZ@l0$i{n*073ni2x7go<{)2cZ#yT{|Z8z2)swv^U8iX_rD>uk-)f^ud6(=&HW{W250jAxYez^;GFwk5E>^CHkivP zuQ%tu0iiM6?Df{N%B!%suR&-OH?eW6lvS3=gAf|QjSi>Jp}bO>`^rJI$&6F|786u{ zKbiY7gf`)1$!qDUwEZOrZN!Oa)sa;3hA$pO8wnieD&@NJ_&xV22#w)br@{1;R|Rt) zhtMdFHtU^=iWfg1pZ^tvjrZqKpPF7}Ofq?0Vg{Ylo7C9Z5Uyj0@4oYmWLc=}R-%|j!{ z9mkuMLaMxzOCCRU3`LOZj~#D-i@Rf`ok~5Ks^`iX`E$a2=N-pmt!8#7)lQY04e;6N z+W@p$Pqm?sn!RcYe0{vMRj*V^TM5usGj$wb9G~1RnV^v>gC59jsic*`hX*oI zqEc>x9s;U6_BgiXx(}7I+zc@Q$Q`PjQ?qx1c9W@UGYc9)!M_Jj-Dt|swJ(RkA3KFq zPi7-?snm0s+<@u9s=&kVc0fr}(6^m|F!wg+5U^i`;j#*{u|_r}OZkrDdoS;DyJ5ep z@9?(V6j`R}R+73KW(&vVE;n+Sa;({^?>7W>L~c8k z#7Gj9FpuqUNlQiMQA2_!izg*JnPCgGXBKnjL#8aMRgCu}*oNJZ#4%hTmbrW3asZK%&r z6$>VU^M|5T6l- z5JAb;wS$|q`>jlz2^9+kSAcLO*<{XJB+4a(32xdmg9accZC0wegxvE{ADt*rnP8%i zqR@CF6OH=l9&2EE0w@vR36|IVN{%V9;lMkOFO-M;}BG)Cf2z z`d~LmWlJT0-6X_BI%;nB6OC#o*K#LJ;a)McZA1i9hW0h=$sYfReGvEIWfDX9P%l;K zh}$`PRIIqsN{}!TgpJD5{)9+}Z9bH%ZkwB~T8W7YO>5I=4BA5!xO+;p2pJ1Opj{gt z*cMz|&+06YR?wX(d{BOL*#~EKFBtgX#rktGt;6y`^ls+k2w}t1UR$M@u-Oy+ zofhA1QVz;2P~|4?rp*o0y|>fOHFEJ>G1u(<_c)|NKY(|FS*Q+1 zZrOy+dLemUy$~@`h)K;0+dYzFO3rS&Y61UeIe$8d6SW{X=Icv+TZIdsY?`@d&{nHg z9yioJ7SSRNQ)!j{(h!?DxwiITNT%E@00?AjpqBCxB_p(m6vlwdh ztJMIDf{`E)2$EnBwvebVV;1}f%1k#C4jvtN;d5)dkC(mhg$@0Qb1<&M@~v8y+zoT?yjZXH!k&1)U1)W^ z*-ACyZ8R(ute@f?)?Uf8?G-4m-<7ILG1_meI+JyZ^M5seYVI|L zpBtRBubugq8A1Oc{URWk;%E1|-KTEo^eHp1(=9FP=jKi?8cx*61{pNl`{oKkSKT>h zfxzPBVle4)6qsh+87uav64j^+5jJu5l`2-Qlyiw#aVJ*H#Tr*8Dh;Q{lXbN{F|k#! z+HwhK&1I19y#JH$$iM$orNB33glKlJ-n~xlD?aS&>E{sZiZ-A|$i}s#`=F zf(K#Cv3xgPCF8Wj^c&e~qG;OhYqin=?>I&9)>xdPqqA}sKXteK`_pQbamG4&#qPCo zN3R`rbmqinI~nhj?S9SJp&~ZC5{prZXe^a3?$nZ$aN&rqcZV{#_)CDJFw zg-vTZD1{q!8_rl-w*FaB?!8G*?xEbZkG_2H`@g2`;1#=9Lo%E{fmlha&mT<_38M=y zdrj>|shdkgai?D_IM}ug1BK@*Elx_(nS(N{N5h?z({9Tay4_~Km!{!;>Up_y_kLLZ z{eG4E?#8O7?Vj8{A$RfSp_=DTbmDm{Ut2yRLtqLm9)d3zM)OSPDv z&wHAA_kI`0S2tHq?CzZ0(Rclu{QEs>R!PF{bJgxua(B0f-8G!>*}(f^g3e{rnUXQ) zGbI8AA`$V465%2WbPn_Mcfuie&QY$N?e4?VL1S_^2VVY;d*p7u^T#TRAM57TyT|2j z9`N!!EA?Zc_-3Ejd2r|u45tc==nNE_d-QDmA#r3eN3bzIzPz@;bq~qoOCn75rvf)8Tgn`T=XtEM$z)25I7L zQCAx2_86bXi|mVX-^(ZVQ106A{ZRh>CY5V9$2)j(_lhwWaux|kfUKAjCBHRSBl}${ zL|THawzD2^JD85A=q7U=$!nth4Rsf4F-?y%B7n2YvdzzOPPz-Z!@z}J@in=-uliRN zmE#c2?p3=_8FQgtr7CrRbw+)nrC+E=F{v9xLn2e|V69-P!B_Knd!R?#J;k#F`-lro z?B>9QUiL}3n=e%c@gv7-YQddUrPM_;OPg~WJAuh_k8%!MSe(Q*;Z25B?zpascA zaA~7E>z7j2a8D|i+x4BcqirfS+V!)(zVAX4yEt&67rjXC;)@=dR^^xPUJAQVl3?>m zB9SB+p_=NJy?&$@WK%^Kiu-fcL=9KeX&PjfPF53eQlfI;G-8?W^ zQj!DrQ00=**k;%OMW^awEyoqi#RAw<&v$n0SZuS^?#6=z%41z*0xZ#Ad7Ki91QmXyf{JKA*DUINv2aTs6WpGdvdRn)30p^4$0I ziCr9cdiyWrE_QC7wu_hVUJQHsIh<=9^z?~6l)KiveenB}({}JFyBEnayn48R1D-zp zdBlDX2fNvmcVq@mUU{qB!^(fE^bp=j2Tp#W+`|J-ehvo#`#n@}@`*eD11HZPle=3~ zxkh)av?iF&FOa)?U`2cm+gjsJKCznvC(oReyP2J~oAUdAK=)nU#-}#kw(-1;LR7Js_94{{%+wT3(r~bE?hGI(ENS# z&zyJ7Uj%LrymziUN6$SDME`%&@D^}$pvhpE{p##TW}h?boxNn{p_%(;o;l;1xk z{k{6Gp4LAObn+a3uz=x)8+7vS!6tMG;wFWve2nqxG>9?{g85ZxNV6ghX~sBp8j~`O z31ii1jLI}d%%@I6DbrAxSDl7brXewpIt`&rLtu?U< zyFrx(SJpdj#!jizDC-?JVPUN%%o1EtXrJIjOsMXy2VM1Qm0YYElyyhI*qb! zaU3JmX_WmR$1q%-M%fN<6vNbMl)lj3MeY%65R6ux)i3Wjnx(*p@nt zvK@fbxlMH%Wjnw~?0R(?WjnwK?4&x4vK@dB$fv2(DBA&sVNX@3QMLmN#jaDQQMLmN z!LC)ML6z+QHDlMP(8)QFu>r%|>8l)|o3r%|>8l*Ep!(85L|eSG}~-ePi=2EDy38>o2sX_OFghHo!H7(Gn&ak<8 zpdAdy4}j#qPi(*_6kKmV%1-|K#QGQtBM+j$-zV0_Q3Qk{HczaMqacnLzIl8FenRs> z5cu=>@)!m^z|M?r9$$hn)WI%+KaVfM7&CUT9c=UX0*ryg1pYie4`WEvfp);3$LC-S zey~5_&*O$M%)w^|db?!SaOIV{QZG@d6lEhB{_Bz%`I~{^w5ruqefTe{-r(`*5VW4k zfMBXreRJ>?^r}1IYx(`(t^1g6<4YT_UH^{_Wc@Q6Ns#@2>-u$TzgqkB+KbjAYbREJ zx%#Qq=dB8>$5wv2^1#a7EB=+smj7<~Bg@ZO_AXxn^8P=t)LpVJtuB6R@hyw_MPlJM z3!htf#lo!%*UbNN{uA?OK*s;YbKjeL*IaeZJU3(bqTyAB+YHyt{$lol+1*)Y_QIL( z&b)J`JY$^E=|8W3g&u@_>wX5hr13-SUTV1MCgo8CC6tFT@Hw&jWDPQv>4LzC-6v^~ zsm*2tv3rRInezAoCP8BNi5g@ogA9QZyHC&{Q<Q8xzEy{JK^ zVv`_Yb$3C7OvOqtU?uY!WGYsI0V|o)AX8B{2GniPAX70e3>ep}2APVwF`(`lbuv^% zZz#~4UV}_|$qTBj5W6}JG8N-Ojl{Dxs8qC#QpB?~s8mb~C5byVs8mb@C5UHgP^nl5 ziW7HeP^lOQxOcv>V(P^oAcp@^0S zm5RO)f@o?`sYo8dh=vB0ik1{s%cQEi~NLtaHKEuu)M=D$$An-zsx(Grof^&9?dmkj`Y{@@+tg{4Wev(Y+^SBatW!`v;Iul8 zvVM#>7FDNFmbDSZBy}2PSsM`|Y`Ed-YZbS6D5I};Ox{O2CT~NPx(To=m9A8_l{pL+a)|M-DVx~>nXal_^2hDzTie|;mW*vzH zqbw0~B`R4D)$lT=TFhBsOSUA>r=rP}0+@(64FK;yUHZ8^!-3it@ zMImqOy1XS5s2o%7S-m?2x+3gigJ$oaqS?Hq6HY`;erp7$%jHOn)wC4_vs;I(*44a#DP`-I)CL9-vY(&-e! zc`*~}IuhDL}qRk23R@lLcg*w4IeZ8_byIJYsX*$JiMrziw-`&Xz7A%h0cACY4 zu)8^E_We^dyHhP9_-1EE^3!~`V$V`>OmJ5UY_<(L)Q;HQKBO6Sg4?D8KxNktn!R_5 zW>cgo*X(CF#vDy<5<)QH+QFNZeBEC%nF?eGsf2vqh^u1u_d>$%$w9O4o1$44T20if zrc$)tZsh+jdtV;s%2D5`IV6qddVj+SefVycuW-xRzaAPzT8yM4EUZd3N4hpLWi z%}izBGKh4!F&vZqX3-Y2r#P1(T56hXK63@+!UI!w zBf*}~bGXXxppdFnE3|(wOw?j6S9t)sybYc4)hvt%ricbkpE#F;=^^6blth$v_}rO~yup z!fuc$?(&rg8O!H^@Lo6V_BRxCo5}}jAu>Cl_;9wuC0qu*dFXJM%;hfUZ_tqn+2{{U ziM|lDfsJu{+U+|ObQ^M4M5D#ia#sbvsS+DhOwqKZx|<9qyd|4n^f=3{xIb3zS$l)% z56Df3rX0vJRkwaI?mM#)CUu>LYeYIAFFpqalp#~SLRlrKPX4>tqE9e#@fG|2a zJMH$Z3cAIJ^@~ofo_6~d1>Isq=0zu0O}l-wf^IRw>7tV>r``VA@gY?>F>ukz71M6t zq@Y`jxV0#F*@0KPUscd8MtoTmy!^m!->9HljCimpco~A-zCl5^7|~l%@NxvZeZ7Ki zF+#4Q;AIJR`zzNR8!NKS^b!Goppmy)L4EdgE&;Adt(eVh8JFtifFo$(146;UHJlD} zCkkGkV7ISR&@D#zRTR8T!EXPHf^IRwq@v*E3U>Qi1>ItVHATV87VP#vE9e#@2q_9) zzF@b%te{(r$e}2B8H3&a((&;)+cfJfka=q?UbRMJ{aVQzG|_eEZh~x>S$bSF_Sj-G z34&`ZaaR<)oWX8iqo7-iD55BMS%claT0yrMQ9n`e@&>#8MJ3%TkIspLmpRz&s}yvL z5ls^XFL$upS1RZhBlZRA|L0Ui)t0!W*?iOH&EU;H4&M87Yd=_f<=W-ItN(i4>p@2U zkFEr@AJslj^Jz^^{XO+BsGqU?w&hEgUbb|0@s}5`U-Q(VcV%(uc%CJ0_607oC3|_fZWlzuWbxv zW3oEA5fHoKE?%WT-#8$)AG>wud>f#&g7Y?pj*_3;0GRXFe&}fqz-K;1f^B;7kOl(5R= zEFcH>4h3r10Xbyvz|hRV%Doy8gL{VpN5{!Re^G;WLBlQP;k}U4a#?Vi}n4fVlW(6v$k}*s38Q`e3&B@aNGjJs17a7vqe#=CY-8&G z%L~`2)?cx5)$#-29~XbnFYxlE$z>obY37w6H!#N9J>E)#{Loo-uSs)c=iHybNO_pH^lFQhMYt6dXMpEysc-BvTV#d>a!DDA~L zx3@+%97E3NiKlV`(bhYgRKqf&yJ^v$Y*)Ljt|xEW3v_5+G!Mr?(=bA{h>_mhC`3Yq zwwMj_`Lw5MR+c6o4jm>37AMbuVhEE1V=S&y6GQ4}QVenS39RhEkB|60a+p6M*?~d0 z%(QI6T&Z&zEuC>UHi~uHuD-qE&9;)wy#Wy(2Yo4PJKU|rBZXp`^O+w-V*Err?kKcP zPGhXmPf%9ET8L7Pn5)wpnb?@OPx@+nGL?6SDvc~BR!7-dF2yCOe!5=m#XX%uupb&% zTcKdw>nZOz!V$vTGiL>C~i-oF{b{d9zOEFvC3! zB{{Z_UKAliN3(1{K5_)(Lieky$XLi{&cSJI9GHlE+B8$(i`kw4tIWV^Esxa1dBI`T z8|YyR{>Z?;&bc0gKkZ*|4*oXxSVa&Ld!wZ9Y(Lx0!jd)&0eRf#N5bTOiJJ3_PkGB_ zVU3#UKWmkI?eO0q)qge*M>qXTrO~fUOWjO6KTQ9x2ZSEOA{-zxO!kDgR|s!eKkzzj z?SREyYzISL%TBM;DC$d1zlF26`U#Ug&|>#&u@j&0MVEQ#ui9-zik5Zh1|HiitH8DxqpRQr#&TM!itLQxmJP zf=O8s4mkAv6aHz3Cp?D4o!VYQJleem9-xu}QFu4+IVbSG-uJ}p8vC?6^-jf-b%c3uOvtu|jl9R$-SH1Bwm`ZYO_4>L$C$1< z%@w1)ZB=%!2H~2p^bLIfzgm@5ZQZ;1)6JjX_{qi#)_=S{TKm!3VD$&9J>BYn=yQwA`e6mU%AN*G|V8NjfbD`9}aX5f^kfSxjd@z!$MQ=rFY;FPC;!e-!y!bE7oly8myP2+1n;?T7n=c$M-L?o+=9&j z#!HhD26mKe8QaWBEyXU@bYz8o_K1vw47Mp=nwSV1n-?=FR7%xhvJq6cbGjPgNa2A_^ zQ=WpW_Zd*`+I`wna1}NKr#uB$Vl!~cQ*gzU0gO&hdkUU~&A=&7!R1p1FbrNekGaS1 zgi!y#O7)3-{r`mi`D z=+d7r1s6ZP=w0~J1t)0yg#X+-d6pU&8CJG2V!XF80=JJ3tiE@0`9232y>4T~cq`%H znfn}I^tz1^4z7fQ%l0|I2-3g_uQ)zT_}->P{P5} z4meOA-+>dKp@f4=_c_3b`oM|FP{P4e_c_3b^uUSNP{P4e_Bp_aFyF=qP&htD_}u{IN03h03%`or|zs04mS2Vz=)&3 ziE>cF!TLT27{Ty31!I+Pu(ryS_c_3MhQP3wE8#%5&jCiSr~49f z?__14gD1=geeXoO&%qOB1ip8o+2;Vq3XEZ{Jl+O+??k=N0Yqymz8H-~ht}gJCOB@D1+W zX8@zq(=|Zcd(S=tPpJNXPE}QHwYQcx->`Z8#vg1%*FV0VT>IwQc_l zNBtYetGi01_R%CdXic68xS7h&I}~#>M}9Eu0c8-0cpyJ-S4{GV{9q{lRS<1;u%6$h zm=v;}F?4*eo}Yd=v%jvG+6CA1qe*s9m^=+~1K0Cg6?221?VvTe6p)!Nptl^aGaoGH zISUBGkWijH6_A{6z;9Me^2h?huozS)PXVN+8{)4iCWR~@4C4S;K$pNNgSY)RDW-P8 z1vHa60>e^Jn4E#!zyILOt-H6ltuvcn+x*Dp>o;4Q&dswMKic@r#seEK0(Ah1jg|E;uYX|u zHS6W|JJ&B;`}W!=*WS7|1l0gW9@IR=-Z&P}|j4E&pKo z)64H%zGpeUeCx7i=}Sw$xAcok#ii$h$^ze7{KVp$7yFBTkah5<3xB!r+Y9$Eaf@2IML*`x4Kw65s zLLu&13UQY!#61(mu_QypI^OVD%HL%QanDeQd%6@y8{PhRA}po*G=;cJ72=+X;uwfz&q;t5ENy+WJ?ihQxR%I5o#Ub zpEi=T-aQPO%m`-$amkuO+^RyHP9bgu#c?D}4kOOc41ZdMIE_M_8pRn%nh^cGttkC2 z%L;Kz3UP}ljwSWRc+#5+Na-#p#Hp0w&iua_9G9~78hr`fGe1>``@blT0i|J!puC85 z-#haYWR=n+k+)S7J%lP{JoE30P(N0L`Zt6#y%7|dAD63D>54k@BZashD#ZN&#c`yb ztYydPIx<#*IPaCT|3PsiSoo%x$)1$*_vZ?6|63vM zGboM#guB^Hekd#n&(1y70#fDb+uLZWR8sCWM9u zPhP5RjLLM!JD?vJp60%d)hBy^AeW%F}?GKt|}<4 zz=M}A6;AAfVmgv1u@We$zynK}3da{iF|ng2waE&orN9F-nd)?-BosUg&3JlbL9qqE z3K%tpFNWe#?;Czwpu)nwL|}McD1Nb*k%$`sNhlE*(GrSD&Xb5606i!X7_kwG=^d4b z>j51o5f~LN6w{fJi0c3`C=nRJ4vL9MBw`zof)ard)u5Qv1ue`{r;aQrkFYNh7;YDe zS=pBeumhYs7%xT)#pyhUOWiqIpk%_nbYgf|C}u@QI?n=TE7>VmT=b=Am%DhT_l_f*sW99;bNS8pBEKiFR} z7~T_#pYrFen5P1IaK&J_P$;H%bj3Uc(1H6MhWCVGI+7K037jUd-(h%0C?<8m6(jY{ z$buRO`zz*D)1)9Ph6mLT_Gb_yVB`4b{TZ)}I;eTDzcMj=C61?aFqYYs3HATwg+ErU zze@LZ%`W)I#h;(8F91?BUvZeC`Ba5oQlEeXX*N4uP@SrtX^I72tTaj>ttWV!pO-cj zLk?l4P@FAfe!T&VJ!oFNfulPcWawy$=3{H+!Uo4y8j$W;u+h;<1IMOlp6TFXie|9> z9?ulb29AM3FgG21q}%1uX@onUKZ7=(q%&tlw_!NO@my%YceTH2M>q7pQ8C$hbfvdB<}cAU*TTEKH9xzZi;4{ z%}Q~K=ErB(*uN&7C`Gd#?m&-GisqTU3XnQ!ie>{%!L0IgrH-Ro?wp@>)q|mdD5kph zYO-D6-L@8KN+;c|Zk>pdp>i;tNE0WPx|UE2#kvVL(qidcU*s#^W@9fxS+l%V->bX# zXmZp@G)J~UCGKc-%Z+?JlJ46Jb=St%W5Itp52Zj!$ z?xjT#_yCG9{K8!er<(^*q0}+6Qa4AA=19iAuJy#6r_&R>56T`?nlR=6IeL2F!s41u zSUBLEOmgv=mHYEx?H%SUhr9n{SOS1$4V2tUA2^)}p0*^?sfIV_GJ8c+SHHugd?CUz zB=%_DNR}*Quj97xCochDi3l|5^4CM%VY`s^d#G}%UdqR^$&NmiER{@*4g4wF^A7uN z7uzNiwOV?-lWWAI-c}?m76^-}(v9vB?Y<))7Y8MxoelfV%1Qvhp~Lf~34&^1k)Qg) z_Nl5_l9diOlzDeqdUmCs!>M}$*BDNEwkaMX^n3o@7ei4*?!Gg+r&LlOpW-ChOpPBw zk<4cLS)s>Icj`ltV;@_QV<`D6ohQE2E!tao!C>w*v$0O7OknRM- zw!N;)+pC@UaB=n9zMywljJRV?%2bS!qyDJI^{r{gsAdk1TO>GBV!>pn=yfxLoT(HE zdz{{&t!nRwyhNc`Nrmbx;L2RJI(yt+sW=isS*tRUgF`>a|G&KWc2NIMx1@O{_{YVc zi(lZ$^#wrbx3h;@$EjMUC8q*BeCM1B?A#($7`Ey8yEbkIlA==kH-G7lEv6F#-eI$p zYr|o8JStkH4ydrfv9=1SMIJUfYO7GBb@)bSVO6NJWnScA2Z*Y1u0G7)wzpSoHurL% zkkipi7Mz7+E?F{nw|Q)zQ0t&5nw7najVo>&*>ZV$)}blh2{O-vqW33+lCZtg6 z=9FW4G+M{wBN->uI%ZbxQGs}DwGN8p7}*j?q?Po0JRVCT#05>pJ!6eav$e1xUf1vS zE4hZT7vdeEl&vxv12YobSt_MQw%Zf={bF-MGoKa8%})0=jw1+g#e2ayzCbYyX}m3) z2^36CcCK zVx*xEZj1u)fO&5i5=ZsAF+hp~hNw9c)kr{@RcktQu+l+=->cwChZTNL^*KydwELn5c z-y^F#=6Zf7<__)*V+50^TU=hw88f)Nxo9F79@r>*IOKMb#&Dz_Gf|?c9(2V`BcRaO z`sF=qGTW^xtp5jx4wZiqy z#ipuOGT`aONBMEEm)7@o`qq}89J&kwq2DpP+BQ$c>``cPgF`>i|2?XYskT0~_4=*C z7P0x0%};K=cJuDdTR`Oh$2MNG@w|;2*T291k@Z)uC)c+@%>Reh?q7?pon8IL>IXn} zz~Jf?Alm;S-HUWy-DN8eue@*Ng)7dLOSNCtzE?ZcTD501U(`IH>1yuOtgAn#euuiH zzFobt{MqHVE(^=t^77JWmfo~fUZR&&i=P5<1cgOn;U^29TzKum-3zywl*W&F|XxOFz+homUV@?=Uc88UQzh=z@^qxUTNJq_Xe>o5j2bf+t-pfYprtp-v zJZzI{jI14RvE#{D$u1eK745-Lt~CgD+?7G4#&I<#Wljvbfv7nT>EvLWB2x%vId9Ek z$hF$p(b#Uw?p8XXZhp5yRa*Ws(JaLs!KQyui@`Qo*d`q>2ZCb7=}70gwM@o1G@4n0 z=G&!euHhjKjY5ZXwKIlrrZN52cf&URfDji7^^v3Dt) zwlQ&;64Txtlu194iscz!y5=av_3nN|?`8QzcU$fw7pgr zRf;h}x)#kaaTgQfytcYC(@}kloZJQ5FpP^I1gK`OIjD;Y+aVDqX!A}4X!MkwkdytUo)R-Jj#HOv;cWSkCk zM9$V_0xoN|W@~lhg+X;NcBwv0PGYc4Uug3Vw$v8WyQ5+)Uw3u%p`zPB=Ur8gpA3|% zZF8^McJ~;9gKeU)jo(6&`7~{%Tk&ADT?qtiL7{DP8<=6M*6Ejb^^RCGLVAZ>pN4HB zu#Gi3+O3-Pv9dE^3=8I1eOPP6iiu!?pobolbJU~pn6&lX=5$ccc`X?}G^8)R@LDNa^Yk>XN}~XuF&L_ReG&$wX3)_suayK_%MS(C!n{8vpVDXgf6>~n> zt_%{ckf0|$9oh^eJ~D0Nfo)tVuHGnI8T*#>J% zMyB7*4cj>TVFzDsMvGbdu89qKE1~i*Zxh*ZYRI^Vej^aE8eL&?KgUeJfeW_r_q^Z~ zCVa7Cx+WIP#aKQ=RU8$j-pl4GyTMZIn~lYexySC$j}x{rb#3kN$Qt&A(jq8-olcrY z7X8TH^;bLAQ8AQsxV^?)Cl)2h4*YHo*d`QC@=T)Ls#Gk47V92+=|;~~4|oZ3*zL3V z%&x%rT|Kkt@=nKNhiy7`zm2o#<9Q)$6PY1hvjxiaRCzQi8zLUg+RXC{h>b_wYuZ%u<8xB^c{zPT&E50uu$oQ3w+0AwRDX($eR_m$uz9FppX=8 z1Ii!jj$ORj4^;8KpB{v>p{Cv4%89m6+Yv8Vny`%pwkeLdg4O9FnS99v+Qw2zZr4yV zgh!=BEs+iTJiT3piib;?#8ei{unh>#Pl@h&K<^$~lJ!Wh<<8WCamq?KdjsFFWpY?| zcZFs%SjbMtV}fm5-Ig)QIrU;q7w^&xU{2W$iW+n9*y zxvq!ES*`te-Y0tcU_n&#tyHqklxxjIGQbCtskBe^2jt|r(>8H;w_w=qj*48{+hJOX zR5_69l>G%KK?h?sJKu3POarSiT}-L|8##H-w2i&pu7*14YNHYr{b7o<#7Y>z1M6E75 zniZd;T~d93oID%0vDO@;xV>Aq0Y@QUJuG@WCa#?jNq0RJ6#eN)%(&Z3QDnQBO{spL zoZJT6V1#I@{wp~#!ZsK&ma2!y2?yI?#7U|iBqs*g2BY4S>iy(|g>5h@G^u`s(y!@=wTa-;6K%GkrN8G!HC{dy_cMjunk58p6WNr2{CP>yq=Ki zJ>+B$w!sLBQ@xv<+zQ)ZM7OCPASb)94MrH6>RsgI7T5+Os!a7xa<@VH=EiD%G!(lN(?gjIb!xTgl1wunk6hlj<$xJ8-NYS;$j4~Au3A^USfzF>KYi`?)wPw+FFd4rvG(1Yf42A! zn&HNq!Ji-NpZoTfUy@z$Sxv(fHSDJ-JCRKodoIWH-<_<(I4)S5biisFSipAgi~3t> zN4!^X2=#0t4s@KZz=xUxpVc-_4$JfP2UDgp2oGnuF2qa^&hl_-TA z_Fj&nyS}Pv7>L?HTX)eHEe-u+)4&_elYzP%?Q}Jp2$c#>VUUgh8V-9kMa9SY0i^E& z{6qRoKUen)kWP9;bP8jjPFNVhT{8``H9zT^V%_yTB{Xt%J5flw1y@ignykf8)Z-RP zq}fZ^{FyTCWjvI?38_r65fyV{GnLI(JXK4Hf6{$0;f=dnoJ&}(T$!?3#B!=)?&^f|rDP^B*A1rlhA1obD7yUMKCr;VM@wm{B328nxPbA%$oXrxAC6n!LF41)M zTLCsi?6RGDAX5>jdTW$wHk!exJ0DM2N~tH^2cu7Gg@uIjFySy(z43ICceFYo2S+96 zeK51=cZ`{0&Qr6PeC3jou;=p3jpsaa& z(_IX@cz0;PwOW1F%C+*0vuKQ_Ev4dUAk6TFAKTjmRC6mt*uCxQ;w9#QPknONTsM&Jy9+z~FJg)qZVEbG%DB42} zOX;pB-3L3hJuFX}hsm)!nI+tvbfjC1Sk1oBybl%_G#C)mMux=zUrZKLT+E*6`1|Rm zyVp)Rs{uz!=m$;SLa)zTN@4;GF4C11Lbch3M*lDp_`9Y~njGN_Mliuv? zWs~l(rBs^6|EvC3wf5STtCoKS{Nrcs=jCBg!VI_#y-L4;V==<^Q`G}ElX>3ZbE4zU z;h2Y}-gp>ho+%!~WHsM=55rD2&wKB&^gfqn_kjb(_CC)xnvGqGAdMtJP#KaVK?#9f zb{YhTIHEUtf^gK?RzbXxjQ9CGwomAmL@}&;&jN zOD^UgxC``1a60sTpWJH?eR46@ zDz;S1#eB?-1jI1XQmM*3l;>2HVYCN9i)2#cLDCabPECyInE z5lEHxO!m=`u$oB5HZqlzBz%|-9Y`HW>2wX0x>siwaBh;PCUw+2sl&aZKI+2^w*$#x zcA=C`kB=N3o6>0}?CWAmClChkIHq)>D5F6(>ZTv}2QB7u%kBt{BZ*M6WB`6&UaL1O zc=YLfp6++uE&^l|5yv3l{)Br?m-PWZhSP-5&zE|!L967k?IcMMZR4_&^=P2o6deUyx<~Igm4oDk3u;=uvHaELKU(}asIxcF*fdwEf2jV9`rYdL)ORiY&Fa&1-_rew z?yb6!F09+pEwB8|%I~ebdZoN#TDe^NJ?*EozoETHo6r(ko#rc=KhV5ZBP=g3-?IF| z<>=BwORroiEZx3z>EbsqYYm#$FJJra+MlkyZEd_3Ub}g1arN`753Rm(wXk~o!b=vO zw?Hp_abaEcu)N`7>5Gdi@_+pg`7O0nFfxUwCIld{~|Do zV2P5cFr48HtwjV(10?X!CB)CLk*p${N(Zx`l zfg}uhlfT=Oj8+##aV$x&#ah}S9j-2d;uw-(0-j<+TJuR4UPitP4RY*6ii3VcLRYhe z_6I%-=_(e(GyM5c97PgTCzPJ4J)#SsI8c6zw%fcVX^kdb2*v41LT_>o9bE~3x*(9y zXURw@GD6bG7)`}`T{=_h??_=>Wf&>XtjcerFc27B%-3hC)~Wvmg&7!@ucrOdVw37` zAut+LX>`R)A+$OlL1;D+nBK@5iy;sJAo&OyEehjUE)?zuq-$KGL1AFd6E)y?EbVO( zh3Q$g;<0z6RdX~8QW%pS1`{(d6$+zRy4H>6r0Z6VR)YhTYY3vpTgM&=b2{IV?kFGy zMq{k*sbr-v?-3Zuag33(=ABmQ>d`zGh3Sn3U$~N%7An&`8--CuHV{t?r81zo4TXUf z*Dad^Ga_Y_!svdXZI)K)(Qqh?FzRDn5dS0@sm6f73`Qexz-Z0vGa43!0XM3&H(Z+O zZ5M?RoYC$L45XEsG`Aoy*2r<;THceB^0tG*IL^@V}C{Z;Mj1y(1cj z*3i^-P@G(COr410Xj0DxdZmU`cyw(PN0EA_=_`h$6$*7NWHw}KZR(n6Zv?5=4;*#T zE?IiI27;5Vqp7Q-IJvr)IswJW)v46gP@G&{NnI7i$<=|>RZyHgPU6~Qpck>xw?(I0*WI@ips^Jyi|+Oc`h>c> zP#i^)`g(UN=xUBw8)kZI?|D#qL zy{!I^T5a^Q`af#5(aY-psMSU=|z?KYIG{*T&i0F0zeF_S$hwWFbS8{h~{lHF`#_N^|zy4+W} zRNq-#_~P3C059w>0J;5k*EFkt3$pwD(rR_p0`mL)tM1dfcYzFlDIKL-13CWwaOHI? z%@wEWI}4XB6c_GTcxd5Oi=oAv7nc^_vN+uO>*c`mjmry5pI>?qy!i7=&s}=@;)WDKm_4Gw*C0pD%@3(gH_Z=i74+!vhd5t!^HRB|7bdxW|P+0EoN z(>qa|tfqU%3~v5Q<#UgW;sTn=bB@3+ps7ey5IIdnqBO{9DyY)H$!RL6(g0xdH5K%2 zk(1L@7-XcfnhLE_E&cfgrS#$-FMffGU*Kox3m}3otEWm{mgV%+GiYyedMau!qlzUtTXwqW#J0rKnG( zyk3g>RLbk6s86N5UW)ou%Il@5Po=zG8b-z|ua}~}jq-YF5baM^FGYPS<@Hk3M^j!e z^`UfS^-?d2lhsQ-C{9){b)z_0z0`%^u%zSY@Loa|ej4aLd6)mc%T z>|31$#mT;znNghVo0$ncF${8#0XrAp|9^%qS$M1>CxYzzKYI4bo>nst`xpKH(X&ea z{U1H6>f zOxAiVG33fwkI{#HU}T6 zy_-MX{P~TaY`kFo$LpiDAFT~mf3VuqeP7pE`R+Q%U2?J4V1~3Y$C}ALi&A=&7L3p1544m>51hE-7 zakQAY}=-jWgqoY)MU@)S6*893!BuumDlD9d%) zQ((hp;FPDpip{_&Pl09107jLw)1Cq|HUpH14#g;0|mCPI(HR zi_O3(Pr-Ah3}AG6+EZ{lHUp-)atUY`6Q>)Jh8UOCpomu(y z6-xVYZBp}3nz3e0{U-J9@`slLOJ7;)Eol~iW%0&^4=%V>Ur-4WLqJ0l3WZh<;-NtK zj}J-ye(^s~`UQ|LOH&34{22N2Fedf++9ny0{QdyJg=(g-G&9#VBX2%$PFAb7Y{xci zb&`TK@5IW#&LcOsv-9LYt`I}L>>o(e5Gf${{TXs{ot-B~P!v0eM9tEiG789jZ-$&) zXOEC0jTFTcBL0#zlb8Z>-xV|=-y>21W`^V_iuRckez!znI#14EFb87J2J&~xcug?h zuUo8qJQf+bbIDe;B28+y@*P3rJ1>He*!-@}lVZV2Y7~XGbP*^Z_HF5hB;`6gPmTf6 zPNuprlq!n~$o^wOZ_>KA- zl^J(r1?0XuLr$)<^W+$in#4QcQ|N9<5DLis!wfmO&d!sgDZ&UU5Z0rTc~L;_;Tdvr zogw77k++gS7mA|M{rx<0b2~dvPEQerQM)q}8mwS+Uzs5%*V%b;V0UKCu2e^wQd0rB zzndW^*V%b;ApV%i4NC!O0IdRYU!EaHPy}rm#*5ONkcZ@0khzo47oGO%%-q>??noun zOn1|@tbH&ZhopV1{H=tXd}rs$aTGySd*-$%U75#|`_c?Kxy}%BWX;-~$t8Nt@#Owy z9=W-lohN6Y2%;|9{3jUQ7iY-Hb#{atX*7ldG5^ek@A%PuVV0asXXnYWq>*dq8pA67 z+Xq*33C7lKm6HSS<^V2?(uS#xp_iFloj zZMI!0g?jW>Xrc=ID;*b?`JyQY?lZB<1RR|>3?Ku{0MRaNu01a zAqY8QVU@)B^_iNbmMhTDtj!sVyIs6n{vTMJ!m>{&6IM9 za|G(HP|XcW4kx0PmVfLkcb$oBaLm>_LZo5zBuceW-MG_olwD3X$8Kyo;(faXl#7C&u-AYUqj)F@FF2Pi@tJN1{Bz%cM$prt@7HI# z;jlgv#sWJvQRL={;^Qt_xUz{+pXu?Dt7Gdk%?Rej`b}lmVCh*4_WnI#@O&Cz3y~45N`WUyw?;PEzV1JErU{z9~JYyvadn* zIPZvdw~`I_s9&f$fZJWLCfPD{v+8reELaL@ThXW>83mU)qfe@1E!pQ{5~_ugKy+1$%z#p&8S zJi1{y+GCDxj7!7`Pk0dhKD`CbJ>j$QaWu&AH7Y=iquF3=Phk0Q*5KswBQ{D|62ia~ z8Ab}`X!GP!M=+kQ=nTdZ13O1F#H4L!P$Lq1RINFz8@h=a=dh)yq~2U;d3Fe|-IuIU zxx!8*;1?4+zPLUV>+I#NbbBvL1f1sHFp@F}W0Rs2emZm@buTSWu7gtd3wJHxtkqMK zI(Am-=ExEJUy@Tb4=(QD&IkVR_}^2f6#Kl`sWcl<2&UDxrwlc*uD|dib|?_!@dEKE z?y{5tSmgnQxhuUBX!vS%PslGAczex~w6_h_d?V6pRzX?7P$!s3?iu!&@*v+ld4=D8 zE^KN>EGYpU+pNxJF-{5jaIz^fRK-QQ zD(!~o_gPz1%V6c(=2&=8DqBFEU?q3i>CnMSpX&dr@2S>bp<7Znz(4-u|J*;A+_J)l zCVQ{mS=h$#g;gTm&0bfiOtrmYR_O3xZ9(-f(`;mNd^sx=GtD9t4WS8i%5rlWpG+Cy z3ZA2K8|GF&#!xt5zc}016p!JPH$9pWO|_rxX2ncT6yPAwA9EsKZ|?6(v*+ag-_YZ7 z&KY}jQ{d!8Hl!Dl!BpK+Nix2iKF$x5{(M!RYU^WZTHFcxxCA&Z2Ay`&N=%R4`3L0w zgzWz<)8R$1rAhQa+AvjtXRb9EUcKkd6^NF8`nxuZmYDih1oMht2*p zO;_&;>MT>;xG(Cox@sL^Z1<0CjjEF;1p{gJ4LhVUX&CriJ)bbH4GP|Dv>X|Wd!v|p z#~<%!J1&+=ChMHb8H$I}c0a+Dqn>`PIv7Ab9xM)tctK^JFHj6HOW&5w#D*>hT%LnK zqe**>LLgroBuh;?QlQyl%2eia%(xaFB>kZsBcJxLG{4i+H->gYWY6tvUzXd8s(6Ens+R&(jvB_A~y8G~NN?zzTOE``z=8?r#RLgpbhzj;?TmfFd}5FFX3T z`Fok3*6Zc=Q&Y0D3*M1H*7|M3YFPmh-~wgup`FA=D3>NTPlfVn3)>h=?bL*loW1Oy+kVmuCGI^z za(0v}9Z)-al<8MGm0U%rbVqOzpZ@VTW5WYfj$T>j@Bz+-PB|+AEAUaFgf{PEg!0jd zA*GkEL@tPujKNwV#m8yyjzFf313NhkL@aDQ#Ei`Poz#iNP>9gu5mj^8 zJq34Q#EI2$w;jlJVmpRLU&xb@AzQNxezDTZMFPE8VwA3z;}P55P9q=LGq-7PcE=U) z3ydq=FT3>#L#|_s?wAy5B6>JJ1Xs) zUn%OlJ$~%Z+D(oyd!gq@_x&LG zd9zKHxK2OSFbPAhnKzmQPBaZ|7I)8RY1^V>&&Zdg%q1oIza9?#K>xo?Ra9-IHs7!uuDtKm)9w$#oOc zmTH05hg2%v%7R*b*UHj1MOS-?M9CrA7%`vU?Kjv=*;k1h+gzD!+k}3{S)r2_UzBU4 z>bJs^;nr4j0CX_{+zo}!HPvFL9C!Gw!&<)`9NLEoX=eMSh5D-=y5cSH_myv)ujLqM zL^~pg$<>oPA)_~(XLNBJFkwu@^jz0NZbn6{z4jWl;ye<#sae?iLKY-BFQidpk@kktzpL zoieEWP0+zu&CYk+4b#ACOcxJ7<-Vctcn%?Jmj$Q4-=&nnvyZY_=CgJ+ z)Jazxm8j?slk8w*u?Nf<{isAo?47zV>y6Se4;QkA9{Gly4tRpSCXlt*3RY7k+ieX} z`D`gB^=;Nc?l$t%-=CG?k(iz5=Sb#xWt*gW<54+T0IsoIwb~lSn1;*S&c%~?t1lcX zjfh%ZbTlhIN4xY0kM$fc9?#+mWKOGszja@fVUe6=@%9-Otu@CeZtvD@Rd3l>4~rg; ziEAfB(p^slMSnUHGwwE16xnWO4=4IyDIWV3r!1=909n+CGAvREi&!RH9=79kbM367=C4}{p%9-gG`YGyV({5*2Ve0(K|B64Y9V(E55V7x zzbV7x%uMf0Zi77Daeg&wwgd5guvv3jEkw8P32^N$x$F1l19U7us@C$2fU(&s0h#`A10Z&yu& zL_W<0%}Kqr(MV>QK(TBKW`_0*MVLf3y=!-6Iksyzbq?i6;$L$-ugj3j@3-Oaf4xB| zuh&fskXPjJX&6%B3Slr8+ty5vPKJR)Um{uR1uVtBpdV+_Cf}GcSJ;x1JzS;8;d87w z!Ncdl@BA73{oPL~W%TL^3mHWYpQ77FmC3xl*0QnPrlr;a=6`2Sgc=6^-reM$( zs{8sCD~-7+!NcdlZ^Pa5!Ea}kvUuf$ksdz%W~tj|`bN7TSmQR%6-`w$0n$y@%k4lu zS}oSRCQrS?noPOF(yXE(Euv2mjFwe}Bz0pGTHMwoP!! z*qP`dkI0t9gQBF}aVg+7rVG1tG?I5_^xbAv4AwcW#>9wb%;!xyo5tWMCch;e%kFeb z`pQ2;b|3zpQg*MMP>@|@OTtKIG`l4o%jR@T`tog%&A$sNW%I@f3E4!pB&YI(KL>0| zx1?ivoo-2g^Fhe#mt@3fb_HBFAt0~FmV}WR=YUsaOFEX(>6Y|`5M=a=cPjm;S5NjJ zqsW$ok-+AF(Su9Ku`Euvq`!vO%?JPbgGyPva&oJ5OTx(8b2#nOhx6l8SwYtR@*m;v zzp^T2@aoB~YO0grPoa>uee-8Db)L&)wYe?uv|*G_JR z>>|4+hF$A`-6I#dW7(YUnjiZ`$mYjK3fWv;DyTxLhZnSKnuj%isCk{Hsc~wqQU7@H zo2$RS`kK|ss(JNUAPeCC(!CR80Z8gd-RjEUuY73bzpOM?94lwF|EB$O?R&KMY13Lt z_3+Z|OP4OJFVKr0TYS^P^A=vR*j@B4ZZG^~;j;_x-TIuQ0^l1r#ZC9-xs4xh{Q1Uv zHtySa-Uhv~zW(s~ht^-aF05PEuUPy3+NajuvG&5X_}ZI@bZzX7rf$kY@=YbMCmYS6w3#Q`rUW4qjn^(Cjb_AOEz zNtDvXpw#@JeJg^Kt^KHdJBpL5IjMaIf|IR{qJ0m7ldXB8efJD*ZY@sj11L_e7N_=I zC{C^xr}mvFPOj#O_BT+RTul+}+fbZbO%d&{qgBu4k}0dv+M}|0{k3mK=*s5x*Z!JH zwfH(LbJ<1k#V_!fe*q-ny`l`z)Or{i@ zx`e>wO0lW$Mqx6g*wh&mCR2({eHSYAaus;Azl!2ONIKOCrDrPeYTt8SUP{OvJPLr3j3bDO{_52?~>`y{mpP3X`dxpuSHE zn_E3WJwae{)f3bsDU2#qB2j5vx_XGhWU42q2PjOYdV;!-!dS!H>Iv$e6gIbtvl^|! z4)RnOsD7}Yy>>BM9v$W^jztS}V7txR0Ya zhGgkR%2StyB5OZ};%Ji9yG+hxLE7JcM{#N{?dJiJK>5ZhG8keipnXmGG6vvTzvX&jE>qvh``ajvCMod7V-{y*{I_OsY^olt z&B*wBQ5;D3L+5;hu{1Zn_BZ$A|6i-xSkV0^_0K z?Bk7>l2Cu>Tr)53U~bGGhIJbH!Qo^spW-o$%JaR1aTt6);85a_D6#uXy%CQ!UBd>) zhWblif?%Vg-iT8JDs+W9AL=jlP(j?I9^+7dkn%@1JgP)iicG&-(i1Lv>~qE~L_9!` z>j4iBo+CxBJnWR>duiu59XNS-R3*|c6FI%N-?EKH<5JSEx74iWot*%9QM4!8&1i`Z zaQ#km%=+!rULjkn7J5WPu7V?+IC1oL93KiGyI$54OZ1I)qm zXl^K`r5r35`iZ9KsO@xv9euJbunE%G+p%^_Zm%n?A2ID>g=OPO;BgH1;S+=-B|Ehp z?;ht03@qJPcQg$7dijBm2nn^8v)?n8YOOfgYyxiv6BVK2*|;a#Y1d=C)t^d7QZBED z=oOvmS|gb2JGrtYW*5SgOW|GvhraIsee0nEG={{T+FrxVbAZNpNu7Tz<8~m~JN93W zpO{@^pY}1K{xCz(E;uZ}a}hyhz-s}>+px=WkYgn3ztIzfqwxeM5A~nfs{pB!M*kQ< z_D(se+iUk+jL}KOErO?5qN*KVUj!*HMq#qvuJVi_mwKW zg2%Nt*rW6HaHzrddlWrL)2`u|Dv;4mop4kebUqSn81@p3#Z$8+(w!`oG#X4Gy3;TK z5t`j}vq6L@Z?GS*DszB_LkCj#(icp2q13%Xy?}FPJvFJLXQd9_P9?FW^DeA&L;WA0 zdusjGA`g;U?WdPIjC4L>{Qt~yJ}M=TZT!EUr5V{nr)G}?S$kM;IzjrVY*x>(2EE7+ zxm>MN$Ozg0jeA8xk)4RD1 zstSH~iUP*U$@>^x36CX>I{Bn?SZuyttHopwUyN`uYLgd9F$k@T)j;9 zZQUnzZ`BQTLEVj-2Q)9zBsGL)Mg3*<2h^`omk<2_4qyLOKT&-ay#If6U$q893LdLu zV*pTFimZ((jN)W%Od%8}Yhwz6aUf9nr|3ISoPi_^d6U13n#+KB>$xaS))nG8C{Ff~ z>vj|;`^fcd6es)0b=wSX{v((1i2eTpk6hdl*aaTBEGSIwk?T%`IlZjSF94R-gBZ)} z$pA&afZS8s?FdZvDec)POztU-LSS+w*VT8RFu6xVWN8~fxd?J@GI{NPIy#HssYfQI zy!I6+PA(U`_7{%YsSPrj*R}T}y%BOb%(X8^adOGWwJ$?)a#_Q*FP*{7P4}&R35t`; z)2)3mijzyQt-TM$$)(xWPLAT_Q(9|ZgyQ6~HEZuhaC%vLyC22L+S`3IxcT;WFN%}3 zw|fwryl<9!1~=a~%Z1`(eY2b>PS!Wef#PI+v+Ogt|Igl+$2V41{U>SDv}x0_h`fL> z%+Q%(=%mfQu+3!O_vB_7n2_AuY&ZL!0E3qom30KSSKL5E1X)#H1yN+*RZx*dRP;qf z1O)|NUlr%~+$5##&Gy`(ihh1m{u$cuJ?A{nIp^8WIkJL9745l)(5=ORM{AOT+UV`M z%hRsK4SegRS&JL^)=Rwx2R3H|-sq{QxB+ke){gy3`GEA@oIPvkDF@}<=A6A2Hz;8? z=d883L7A>OXRgH!%6H9at;G$>EzN1J#gW_FrYoN7xeINq!6}qrtoPwHThHxaeJxHQ zR45}2XSxShTZ;pgaTKwbD@%PL#Hse+tsSEHDvR)*k|*V2;; z6;gK+4fNdSmDb{fpmI+r<#to^igSuWwF24pRR|@6I zkQv1MQgOMpIB*)FFnWA>6BU(xxLsZQ5>gIgh9!=Mq1tmBim&Ag zre1NN+$gVo@c@>e=^kAF_ZX?QxPk96l524T-(w`!w06*YjQARAvVrd`upV6hcNVd= zxPk91(6zXM?<}HgaRc93MAqO2zq2@FEpFgDi!c>8;GM`11lQsQzMlxJ#SMHv;qSrqe?Nh&#SMHv;aiIv_rdI~N zpYV|P|L+^qF*pZJTsgsKf0nHrzixcT+(+jYX789a@?PM1XI`2)d*-m|FHVcNS8)%S zx@qc|$y+C_oaZ==v75(qtfyH87K3>Sa~tDwhP$(s;cQu#Ee-32bs=+r$blaC2lqgN zkWXMPF^@4H6B`@)rpe&adIYIN(Sc-Qa?B9sW0<>+I$J7Z7}45db&s}e+E~w(QqEQY zH`%f=uRU+JP*)M%5TMo9r^E~)1FW|0I&5BE)mT$Tk+>KF33{4Ap-vmEiyhTgG1hWr zF;@g>WcBuDDW+4ZpjmZpJo<+Y$)%J<1R0u?AU1VdzOJ|9{ae1V+wyh29P6?g*7Bb4 zO|s(k&Ou2ciROzCQc|-dpFqRo z3pV93)FP<|{VA+bYGm?ep<1Oug#4H+*T_e!tr|#=gF^dp$9%Gcq}zAk?PRcI&N{^A zmL`z(R14-X?nFddfw5Y*H3}+&me0@R^UhdAH995CkiHXl`_5D;C<6+kxP)iwRt;(> ztN4XHZnny;4zEJQH{-dorf&9#WAz|3zm>jA5F)^b9Byf(5Q64I8E%E7n!s>NBMA^R zt6_sqPimshpd%o~0WGJI@dsL*w+Ef2ZkuTKBs17gsTQ*+>uZ3ctjVpkp-*>BcI z9%{lHQl0|A2jY-}AdO5n(Bcdj1h)g0h2+LwV7eAhrQ!T2-Zy70zqfg*h-26 zT23QX3$!?$QKLX?0X#^rppk3^dO0+}D>eg`q*u_$odV6WpI6KSexz5>NPYs%Z?#t( z2l$X)v96E5yW=m#E86e>S?6{0|AT)V_&Lx6|KuJ3)m9|r6D+dY%DRqFlz0a+g>RpD z2SuL*%5l8D5Dft3T*u1Zmr%Qs4r zrobyJ%HvDk!h*PgdlDXP&|ecr1;L0K$8%~Kc-} zSp4Ojry;Dl^zk4X)JtV(I;K|^iwl^_j(UX-w3!lV$#wt2KoQjb)KYH`hdtDMc*99G)`_vBcyZQ(Z$|ygyE9Q{`3pszw0yI&(sf#k^w8k{DemM*WEeG@f*aL?P3D&y9Ao z%n@zMf~I5wsW&jU$!ZpE7io!FDTN#d?_MQYyG>V*$IWg*)D_8#tp;-`Dr&^d zp#_!0QPG7wd4thlDLM^UG@D4k&5dHR>A(#^{(m9qh8LAV)T^+77S)Pus8lPbg&u_{ zsBC4F<*XgBD8?niuq)~dVX~YYORSq4_N}V>ukVKN2`>A&;mWS_-EqIS`M*2){}qbC zlOJL&X5821bN2ch2nJXU_&N_0OF7%S;oz%dd0a9hq{XBC{* z7J$YZf%Ux}+jH)&wg5D4BIaov=8ry^*>i4)wgBt;+H%jiLD~Y)I1+;Cfk0aT8t2aI zs{$_AxpP?@#_Xqf^64kI_!OIS#e{PFhhtgr%c}=(>;Jib0jCTHe*Td?aBghk^z+aS zto~2GL5^U`vm%>T)d-3jwV+5Zi1?LNlcD|Za>Sr)#k6^eL#wK5!0c-C+6y5AQjHlh z*@zzjFXJm2%$2X(w7Pmwl)+7gSSFfMDNS0P1<-5-vjDGA3sDi$43K}<3KA}Zz8V!t zDz>z$;qdCfx8}Ibn`qcus=C$dlRC78a#UnbMh(dtDt3YL&xty?RU&za+B}SIDQfT~ zoE~*DD6(fzv5n-_wE5e-0BvbH)X0M^2EvvjtxG@g2|>6mcY0lv3rr63z1|kSy!KNSz8u_ z&BkW6UTP)FArlC!P&;CYgxy|8qGZuOr^hG9(>58BQ?DK|wv6g!@xSowXIrmvS&s-! zEp^eZjrl=Q;~a`-l5SDIaan6DxD_E|x&oF3GHpv8atdrYpUC8_ii>iy28`@db1Ts( ziwfD*w!C>%FH;}?|0~(DZM|$+i}*cRz15tNhAe_41~Ph89T~ijEvp=Io2Mns8^S4r z!Pm$LaG3-3WD;ouSWrc>`ed@?uDWA+ylPrKF3*qZW$L@af2m$xua%rpZ+3}rnK|bc z#zbg2q^Wp=c)njRE9*g@Ls&DbZTY%IVk-C%gH(@Wc2T5a*J6Ads#a!Y{9-BYU%iq$ z?%c@4uJa-rm;=_!|Fq?DX9-JZxh!#H2QAe@>4@Kzh#52W_Hx*uPX|Rv3$2q&X%krP zlgo8!Et<7w$Yps1i5ajg;sQ&2Q&gX?`Zac!t&KHjQBfur&FTfQfH?y$w8bt>L9SPd zy&jQO#gFFmKEEjv%v8l0Wy`EE)D@n%+;3EcbDG54t7YK`LslsNbq8gMW4(q8Lni1> z3qc-Qsm|e%)&gn01{qzL;23xx1sjPGr9c#O#kZt$fG zJ`EnIo08P}CEa+3)qy$l9!oaZDvCWVWmbr1kUZaS4TN;Enylr?MFYvGrhy8*jj%)< zun}Cs2AFN#g)zPh&1`6B4%J}iO^!y?mTQHRwn_$6Cw4c4hNe&}Xh~xZGv?G4(;i{Q zgk`l@N`ewrQW((k$DoDj1m?`A(xz-Zl6EGfhMdWyj})|~ENV(-?FxU+Xej8DVke#l zn`V{LS0XHc%eHM8{q$RG~toWJ+hX5yVUJw{HLT4b7RC)S`G;nc|~$gRyKh zM&<6TL|rdx5*oKs>`-K)1`%3I)>A>JE>D;N(;c8`k3ci+eYH0qDNAyhTC<4w@nk+% z5eAk1XgRAE0Hs=G4wWJqR_Vo}n5dXeBnT7WxCA&p49)TFhf3x7w8kW@cxsin1J`*G zrNE;I6|#Z6p)L#{p-5h(<10j{-Vn%zgDJuYIFSbX5HzPX2FuTcQz#xYdb~}CN1iVt zVZFvHbc)rkYOPiZCWDPI^HTc&`MoU6c!DwrZ`blp@g0k2DJPEXkoerYf0Bc8Jk~ZZb({kfm-dz zMY1YeEFPBWvjLkq4r< zQ|Ccla(&1x6jrUVV$2=#C=AkiB2Q=mQ_^7XgJ#;tV4Y?y-(}C13X-&PWf9wRzNdLGCn%PkQpl-hDq~t(} z9i%Xz<==r8rt2Rx5(eanQvny!RPP-9POUqj_c2u(=>rnPDS#tss^5m@xXM&%yb??i zCj(BT?R*QG(;C~M@f;4=&Pk-YfbHA?&1|UcP$NL-Jh(%N6G>q}%fAUNOxJd3qzwRV zR(9KbJ9IGc&^A+}>E~%Y#RAeOy3*XXKGIN+8|G=e%>&ZNyV87PeWZDZeIx5i^Ywp_ zG#PccS#o=nUID5w7>fy)JdqID6q1@v>QwUeNlgl`M-+UmT$3n)%qE>V`M;rs=|3ma zcqWzysZKfr-L23}`wVn6UPUDcG2loJbYFw!xN4xI@%SeL(w%hX;0KDJB%`UG_Nqit zktSTp;N<>4z<7=^_sHCJbLWHH{U^`8clOoUM`o`ErvZUk;VhT;KfG`8KF-VYOuYBa z{9)$qnJ>(oH{+iXXQ7kCrr{Tw%Ee&WrEhbBHXflr(~!DIi7eKY$L>;zlEo*I94{QB|p$AjZ1 zjQw@&v9ZsN<;Hf59mM(t>#MB)WO-SenQwwh1fOK4m?tu)fxr+y{J{9GsR38oJvqG? zxAOf%;MBbk;|4x51Wuja8DHf4hQO&SNygQD?+`fkK7w%t-!lZhc5V>}7@y+1hrp>5 zBjaOy*Z8iwfsd-;wbaOz1afZK<_d(^a-ZyN%qUIhc{)*`hkGz^86#FP%Jp|rI@v0&4K8oKt1l~vSJBGk}6@U1t<4d47`Y7R) zA$0n5*~vrTeW;%_cvukkQSpgG;5{n7fUg_^@6q3R+Z6yl;DSX=)!$j$T`KZx;+fQ?=>e zzBE{F5mlMK?TbUuR9$+v^M|0Ry7X+{J_JqGrF;9r5HwYn!R;puK~r_<+P-ZFnyO0^ z|M>A;%mKO-QFY1EY~MOqJwmE7kJ`Rv2%4(Q!?tf8f~G2SmOnpOa3NKhlhW5HwXk`?eoD1WncbzU>=^psCv2xBZwQXsR~%EgU@_9OTDTUG7_W z-(WCRmHQTs8Vsgta^J#{gTYiq?pruwFt|sJ3;!_~+@r;#4eiHbzK$OWRQq;-~`?eo8xZ}kUDy@b043?|MUKS1=4DK<2`9sDnKuR=Os$k^` zxrX73m3%7y`Gb2t#AW z-P2HI9~8G2CG252Kh=Yh^$IpW*@IGwdTDWbP<^^=exe5@RrJzg_n^dm^BK~5sFt&V)dsW;t6C`Z0wm0FoOBam2}`8P6<~jDETuFll`OgccQd}v zn0sRGs=4x~CX_j(ut@HKrKjuzt+?BCEpEunuAV zf_V$`9HyNqVIIl&Ef8|_&*8)g<4a)APaVx!cPtXy#!nw~8X%NWhiKMU4=0XqlcEj_ ztgjtTY;7;ksbc`^hDBn_`00ao%EF$#FYB5`V)GEGq}1V^ijvsAqj@k=Qtl19iA3MLKra2R*t295xL5phrWOFA~QL`=Cd0A6X=h9@6NZ z?JeuFMdE$KIP{p)-bLc5Ascki^z>45kvL-52R(W?bCLLuVU6zT*7zcE z_%IGV-CA5E-aG7ro?gl=5{C`>K-}XG*dp#jxO&|x3+IM8W}#34g^N!;TP z)bdg{U`9RpChp9z^IqU=Keup))NHB(d(Bq=a z#dC(?d-TRyI&1uN=795Tp^&Nt4)bSAXAb*-ss#@7`Ngw`Nklaa=C7BULl_8px`_GF z#nvzeRIM>DT--AZPxS@npBEcL@B*rmnddClhvBIzWA0h34Z~Aa#%wNDhv9oVw6a(k zhNo(bnOQ6kaT@{EB$%OCDP;im>uSM=ENR(^6Y^`0l^ zOg=v`1m4r{Tln!I@Sc9(%EyMldu;v~eryQ5M@<{}=n#01n%={Y4uSVL)WQ775O|N8 zxcoDQsHw*Y82s>H>T;@kF#gWpJp|seP+|@Zb^nLpSdYgtt7)(4a-%^ATQ>wO>`=jka;}mqzP%`X`~?OpQ4+_`yzU2NdGy>^xuA% z^{%8Ll}Kb_$)FtEIlaedl$Wi+z*5Q*u%Kn1KU4F{-Bp=RZw%&iA*C?n%A)1C+_vAz ze@m#suFN*Ic+|b5)2D^WrZ3wJ2=h{t1&O$=eqUMS$UE$crm7(?MC($8*OOI7+oTnyS|+o>FR!UVo_>fN++@?q9NdQz@^%hxDRX^m0YQ45NajZ#FB^#A5rNG4z`G0F`3$FYv3=Z6DSSj2dZ-r91Kp+uFBoa_idXQTd za~Z#1n``NOCIRY2&4q#&vuGVPOc~T^ni)%}X4D0Qag)a<*l)L#)?#TZE~>XAp+d`T ziI{Vqg(Yp?Bg)ssdPlZwlw_-^L`;vx3h}Zth1gqJkzHf4ImHrVF=(#JBo(7TS}kNW z7KJh%OJxeyrV1`KKG}5Omggr48R?cE4m0V@LF+adiu<`GjklKTGr7^79V^WOX^flt zL?FI9K1N;ykt>NaH}#KK?yU-Q;;}-3pgnw(pN3;tJW|b600{wbh__cJXD&ApL}-yM zmXd$%*XlO8mB01AyapcwhARl_(6p9zq!y_}N^ORuj2(F>LUu-h6~WuGMI+=e(EcY5 z;_8xoRdu+QPCsku_&VxD)JH1t{(B z3@=hBl;HEeUWci(M$wX1WfPgI9%(vlGUyVPrJB*FiD#o0XH$fR^l`6SlCkZ-*HvW( zB+ztJMSepy6%&<}jf^j6bS*U<#-)T9thwFlP)2|ja+2Af=4Z#oSx>RPJof9c+s7^*3y+Cddsr^kJoA5t@{SA5MfO_?*P-1GAUUW@dMQOa#B;-Nw6!a}sBE;ujOQPJC$6IC&K3 z_nf;qA7dWgJ*3#qb}?D3jT)69;%>TYrF>HBR+meDBjuID*z7a2&%lCNvro@Hy}))& z0m|B(&j@OgflLYwdp7Ms3PlQKj)UV^CTu`)r!nRUqNY5muenuJ?AYY>lh?z7S(9I$ z{4y+feDa3L8Mf6M;uGT~9(%$dA-@@81@#NaGjlVr;IX;sxoKE1YmPg|UGC>mGS5xTO)V21#VkDr0i*V^#xNPOX<0Brj2QX1&=YUOe-vy#k4Rj%QAb4I(xCD!dj6~ z&VW~M!m5ZthP7_+SG!t5!LYd9ca5T%{Q6i6XAK*T)3?4<9!_jecu;2-f zmZR;0wOA&uRvBuVk~fgBH?nqwDl>>Ni^77(xe;yz7Cgp1gL?)nn8giq!^?BTD4Dst zxx2fBHKTfNlB4J7VWr@56WjzWc#0e6#$myeT#Spsf;rq6HwFuy;G*EaE_&ohFu`;% z9m`%dYT%!|Zt^-cd!h+enOL&*Sg2#Cm^Dc%3kMS}O^*yqCcFl49oPGasDcPycB8%;`8mC zAemUUY0lLp{zz5hEG1ORiq})Omdfs&TqwtyAq4`eTX}WmWJDyDNq+15_%mPKa+V@^ z{kf;!#(48bKe+3gFYEWb@yg4WzvkT~6(R&EiV6s0Nom8{!jmDpL6B}rZ0?xARy6qd zB@^z_RejNVHm)}}!$`P>b_lnQDK05D#-6zK!;3SzpUzy1oqEFs2OaXOqwY!Ezjf)! zzyB{nfMTbBuu2h5NQ?Y>QfLLx&BM%|Lxi* zp4yqdXy%eDJ(mYB+wp(iz0Oy^vSq_x5dst)1%zoNCGQaa%}0*8>y>N2dGVGT?VlH& zcA@VF|Fsk@>kpH>cJEc2#y*?53?V=P$>NE zt_^Q!gwGu^dk;c@;-7%9ysELLj3RL{RLh{vpirlc*2Ru$s~Bs!vfz0S(#Y!V%~DLK zRCNgd{`rf}Kl>Zkefn4BEz*NzuRiy?uU+@t3Yl5A^h>Te$Y7arbXd%H(VU5-~Z(Ezgu|K z(p0?v*-On?-*d;^D?$ign{X{t#0;`Tu@vGr<2VXx{E1z0vs{9j(iXeWkxUoW)pXHZ zl?oIc!gq!~cht2f+<3)T_ZYTp`tIXreeTbN^B<9WKELgaOj+wqRuRrHVSyTmgI{KzPv|hZ76Dz4q*wuTKwGDFF)?EU%OYK`PH@R zGkf(sbf=c6Vkp|`a5*l3Ebo)AlUEEL9?|j@dNRQs+8h8Wc3c=-3QB$-u=Ppj~({DPed(;A6cb|Wr9*s zSN%o!^o!3Nb(?eHailW$qsDuD8q4encbxsnKb^F33?Xc7!jeivk@RVGAy3L=iI?=s zMl5A^W;}+hBW5>d(#n`dj|D*S8DY08e);2XePHK(*zxCY`^)|Rdw6vI|J+BSpOrMx zHy-~$v-DKLhY;2_;btjWiPmMMj6m(p7nNbT#*)yd)M%iXs{2i@j96$XOVnV(@^lG5 za$7b(d;Ndy^jvlGIWKRz^1a8Nw)@&!0_Sc1x%-Ca-*fsCn-Rj&CTt2Svbm%X!$mHU zBbY0@g%Zqa0c8rE2A!ZJjRa*mvn(gm1w37=*!bZspF4W@M`JSO1uq+RAG7_-?wzll z_k%k_=yeajwCB9j=Mlo(CTy&^4OlYojYsr+mCR*LxsnZ3SCX}Y4XH^`%Q-w+rOByLEe}2!{)zcSD77OOTeD~t!ahBgGk9}+7LHYb=5W>_ZY_D2XT2On= z7Z2g3m^$u5!=jWSCCyeNs7No@#A|A0ysWOumhD*f%e!XYnsL1uyYca`>-%q>pS=B3 zzy8eAyYJca>e+kl%Dn{a*w`kls_2UDrq*q9g-WHgFBDKUHR7heVo#T~&A2OIFk6+; zvd=3JmOJLgdg27bu}=sm@SCJZoHldr&o^deCyhP)`r-fak+*X9ANs>x2w`XwcDVzX z)L$&5Qkr@}W^P#}xvDDbunL5YN==4_`5CF*R8^Nu)@~oG4*S`!($`&h>@8oqs=Rc; z?^)llK$!; z#;Lv^Mb0_(_x2|qVP74*+IqqtZhyvm##>2*P`3%wD1y^S)V$~r-t_F*;k*C$CR1Vi)`?r@uDeQY zdHHJ}{ZsyekDST6>to-jPawq3HenhCE;@vNa{ljw$%z~6c=7u;Uvcv^BKx0$-ePTt$B8Y0WH3YSlEH&VQX|xaowIh>w-XCyajzBQf z9X3xp?v$YIZ@1of)XYgw79M%z&TptLc_gqq7x~Bs{!95k{}uTtLY&$mjF>AiIlt13 zD~i^#wBQdW9l>(a9y1CgP9Ltv%I2J^u2qKO;J4(8{&aV?{+$~i-gm;{_fo<0?a_y9 zm9rJ6nAoj*5@U?Fus0Fnln!Biv6yz1L+M<~Te7=_vU*dacc{bSWqJA&!6-2;}ynTTZC8r@E7WD?UW$I$!)?kibixK zehUBEBZvL;>Dy1JbC(EkLe~wKQLW?=Y zUGYOizYxXneedV0&%W@RUmUdWwZq@Km38Ok ze|8`Fr|WgBhY&*6CQKtRvqSibuOEDo{q8+i?z`gyPu%?8AMc*tJN4IJ{d~tWjK|)b zSiEw>=Mh5MCQKtRrbGCw%VZz&vp;g#iC=&J)n92X9R1Co9rP2`e{a2#@u&OL=+gP8 zlKcOSjE^wpE}h#v`_~a`|&7ubAA<`8wG1-!~cY>v*C8@-PSWRRt#{GD)5HpL*{IE!D z*SW+kvnp$kN^yNYq5vXVyCO37y+aXcu%%N~uS{4v5;7QnhprgdzZ12stiRdHq1J57 znAdw7b&K62vXxwXi>{%F3%$5qsFPt*eLzqy5N5!YtT)AY3z}w15p+K#=jj@6eHHl)Nku z8o;&fCT~J>?c*jiYF;J@HDK9xlQ*DQu5uF^MK9BY3b1OM?d#C2)|f4ws+ZHmPQa~g zwts@=Hq>mX`GDw@y^Ik%NNGs3{UfwAU9+W8_p(f!3fQ#G_Gi%L1N)EeG)|5S#3_Jl z+iZUd&9#r&(l}F25+?(eZL@t2n&m39rE$ufB~AjY+N1kVpjlBzcN&M!Y2rk{uk9w! zLi1bWCNvJDiD^O!xV7En8E9@p-GrJ4i_S524CGkqy2;bf(sbQ~#-VlrB$eqJ?7h&H z68jh|jjcbKFr#a*_dv5;Ww1031JZ;PuxcC7-O#Mo7!ZvE0$@NAQf1`w;4WxxLk){~yD+k1@Ar_HVN--k*83nb&7Z)2~kFKuy2&)QeN` z$rmQ0oaZ^aC!U)Kv7crm<3AdAk3BwSXFbL;Gaq8=!N>3N&pCvWjbX0Ic^bEe^E565 zM-MRP5Q>fjG^ozgxH*K8K;Dsn2Gw~QSBx+c$T||xpgK?EE)qrpX-5JYROe}2TEa*m z=}16>>O76xO&AHp9SLYqou_dP3L}B2BLNMn^EB>DVI&ZCB%nccp2o#0j0A#?1T?75 z)3||!kzlDK0S&72G_Go4Bv|Z7K!fT$jXPZ!3HTieXi%M}arp})!S;>>G^ozgxFv>> zV4))c4XX1ru9IOTIH4l}4XX1r?ww&I*w&GN2Gw~Q7t}Bk9N&?E2Gw~QH`y=}Z0$%u zgX%nueJ_jzTRIZZpgK=uI}9Vi=8gn3sLs>aBg05A-;sa@RXW>Z7zvK+NI>Hbf1bvU z8AgIlT?ydta%fm?UzK#SmsIG5*Y;yn4XZ$x~?z%bm>|bZEp0)Ab=` zxHjw%Tk5V%$g4KnlMck~kXDmOHR92$+;uMzYm&6 z3w3ANoT{gAg#tH<)c$(K;Z>tv0&SE19|*E>Y0Mc>SaFR!S|~=EExj(nPnBI2l%Eo% z3ihNRpFj-GyuFV5yhOB3_LmT3LrBu$ETxQwP_%-F!^(zIEfYwvVj>;QS%mUjtPC=@ zhUM;XF7G8GZL+_BAWI_v!wX_INR>SgL6%1Ng%?C@kYs-jL6%1Fg%`wYkYph)*XV>^ zctNxVNfzSvj85Q%7sP6iWFcgj{$*lmUJ#u@l6?|_ zER7RTFNn<`$^Hm}ERADLFNn+_$^H<6ERC~DFNn(^$^HO>ER6$5FNn$@$vy!=md2@~ z7sO?06lX&jV!K@3(?}s2u<3z~|VlPOt_d$@QaZKa| zkryOch$j|wPL#YL?t&x>@r;7bF_9NUU65oUo=ng=`|*O93z96v+Xy-bI$jWQL6U`d zRPmd?SKi=)154pjUKTvY$5;2pG zjf^&AN+mF|vQxQI!Xnvlz7)essayh-sStvq2=7AuJsQV9{p;`1I791K39R!@>mO5Q zg!gk__k#?bzh84->wGok>@_OHxHKe6|>%!ooRBL(SOW~8KW z;NFU;To3q@!BQoc@mb_Xxz16@p&}`w&P8o0kIa-zE=Y9yT}UjQG<%nn3zmr2A1Di! zDk7md)lwNu!I0mq!|_%%AT16O8*Y&uy;47{$L#9YMEhYx4c8BAf^I$y11QW#kdDjvy{G19NGTr8Z= zg`-$9l8J}&aqzb_wckWa(sKH_zvAD){a3qBPn8|eFsK?mBC-qmIN$0jJp-*)VK;bE zX`4>0SZE3ZXeF@}vN%NoxdBNPZ4P;(W%Ti6BiGEn7I zA{VJtOIW9#8>OHjIi6xMkyI*Fo)`%`VyZmZK6Ml|$5IZr#41@*Iz>WV-Ct9Cs(DRP zku(i3&B1gx}z4=CpFT ztm03BINb%O(3O?BP_Y>kXeGY-f~b%&XhbqeLR}MTA~In_DH>E>6!25F3yK~Mp4WqA z#Kk5iomJLoQ=lv(29@!ZzZ>HSjp)$+%Lp15Vf~g7-M77@;jh1YQEOL|T-FSPnN+0Q zc8guGwM0)}J=T#6!rozGO)=5c)-Gt*IO{*It^Qm8aZN61S3B&xA`r$c4m6@l#@(3! zYOtjW-iXHMQl$jGd=nEUGO0kYjG2IQnr#LW zHid;s$|tVpbE%?Ro=GIK3rN~-Ze^vWpx+-%i!=IY+3XD#3@VwbVH20qv6@q1E#nq) zNFY13nj;4(j2v3QQaXWAs;ZJr2P=XPje&?1(m@Yn(%G-9n}f>!Egh8ZMu9Sq^ge@s zcYL*v9AKUR5$m85XWOQi%LJ0c>j2TvE^*u-$CTNg@R#$Rc)k#5WYt>x zLd2pD3fxYg05utn38&0oFJv8{(lwA_e`aQtQIt|DJo0?N?kI@_ZnLOfSNV*|oYQ0m zo~6o~lt!URFUgwZa@137Cv8$hGPz>9s*eN}_M*UD@rEV+ z*Jn!V`fLD9Oexc`QP!gQxKg(vwbUQDn-Q3liH0qjBJFkl=jpe z22@~a`TdrFtL&(nQbCc{T=SYSYu(Y5wvs+cuxNMYA*F+ajcx=P=F-0jV&a4o+f6up zlEIiqd_{jr602uJ9#6pJcS%xOQBj4ds|}+lopigca&1T$wHk1jV1R7Y1pp*$qt~zV z27HOQJ`{;-W6`43L(xM9i_|betz0o0p%iS!!)961pRt6rxlkP#Ng%A&`ede5zT(Nd zP@~%w6Ng+;98!t_FzPA8XkmvLjeyvxSTPz~Gw98z{WYs6n3D_a;krPZ&=_OpfFK#I z#PdF_(_?ZwLq)Yk0Vy3MY;+?A8;y|IDPL)V)L55Pu%bDflVl}=kS&a-O>TKB)5-~j zIZ>(Pb}DKqZ_O&JyZXzfP*QXa3EOB?>Fw%B$xuZ?Rlg=t_uD9h1~x&VBzDSHnh-#% zNlO7#ls3jxI!(o@msJs60&j%l+EUzXF-U|aYq5Z4aYt}~Y}9ykNZ7_$Rb#0}YBsee z?v8r(&N2$!1UaFWu6To6kyYz#B5A6MqF#|!o-?Iz8{b|m$Mj~IxGXOeQq)A?iGd* zv?=8~lPzH}tHk4l5~|S!mD!kAD3TS;Sjs&}3)HCL0ZpL7-boWA#7^N#6TrYJ)0pa# zT(J?)Sk2zN&gqZJy?KSw;n8^&VNoU$4r{BpRy)WWs4Im5O`yUynhc215=1@KMhkIf z^dOBSv=+p~PQgkOB2j(K;Sf4R35(P&^+%i;hoe=r1*Nq_JRP!0WNC>4yauc_U4xpi zc1S)F);vJPQUg||JqdRDH=zeMI-kx!cb0?6jla?aOsx)PEH<4C zEHP?Mk>9B8F3C<=>)R#TiPss_jgH@V5Fu+h0y1~&oB3)@$kKqL9` zfF@8!OGw!07psGt00z~Cl_t;#6ZS+9f zO}9ZRC|s&$uyUGk13pT?r|in|wcvC(?uoimi3H9sSda!E!Q%qIO)XC}4a!u#VU!iB zQ3*63N|uEL;R4LWfSJXlk*8zfTtgwRhr=eMmTpCTKGdT#;TVDobjc7{Iq5x~kfaXH zY;~R%Vv=wIR$F&5=4niO(5!|fX;Dz!Dnd9&Q9!HR5}8$HP2@A?dR^x5H?vL5$Itqu zs5Pd|RLdb%LRL2=BSC-32Q5k`Lra0M10LJ9G3NaNQ(SAdg>(jCqa_VG(|$p;rKnmG zI)hv%YBYn1l*K2Pl*NVo=oz7FE`1Y_6bM_FC5@Dk@06vr%aTS4$al)p(q%~_r{n0= z!my6b5@x`12e20!IUS){_MRhSgbDB?hZ-7*9ijQH9%_t$4>{D(NazU7r)Q`!0A}P+ zLnEOhG&9OjqX(?Wp@zot6q?nrp{8e<8Xf#1^AFgN+W$EG#`qQ?gXqzBc-tcfo3(- zNU0l<2(g0{1+<7{)(FkApOKyn_>o3R zBV!{pzZD}*6DI*aM?=8kriQnNd7K30RRHL1RA)&1$GeP`AGk zLP3fGT1_J!4qB9sM`Q^(;6a)vjbsv|cdK2G=x3fXz>hRf8mS|o`K_2|f{+3}q{Yxk z5CP4HVlfg@O~7Jk93Mcl8fr1r(}oBkCPe|QUe`nuU2S$O2JE3k-S*NrYZ$$??lKwL zUg{nS$Rg~@LgUb2ePp4YF>q&^3_J672Jaf)nLHo?jD6;~ z=|4|@clxU7`q;^12eW?7x`TBo3uB$gnq$7eynQ@1eriXaUfwJ<bm~<{vg3y`p6Y)r@o)XxPRq49pE2JkVFs*W&!#;{p}4B$3WY$x*~KAX zp&`!u;i2ciLW7(=@X)hip#jcW@X#}1p?*#a7K(71uuvbT(Sd4Q7OTrw@B|}xvVmwb z6%9pOtm9cHz(TjOw!uQTu#Sg?Zf0$Th0e3KbfBY+@HEbU!a{d(&V?0V8*8CU)uUGz z@M1hyQ@f&uyv=8!^edv=0m9f(*5u?~TS&an=Lh0d}L zf`#%}b6u#XsIwPaDy$U=D7dmo9w1X1`O1FGiUs=b1pFReR7XJZJjDFpAgJkJ=2`A&u-=YZPq>hF>>w`AlH)Gvqk}b1xGm&jonb%st6eaIR^qy3<-+ z3UgN?8hyxY)|ec)+ga#Ny`4X6EcSTCV$nn#8pPKO`t3o=%se+WH`Rrz3=wzJT`T32 zTDQ7f@*648*`Lh*qytr#%4MC^7LAlWEw8>{3S=ozrkm;RKu2$qyc{nq)Wh+>%KFso zQ?OJK4gw4HaeS}>JUja=EcBV#XJDaE&pr(+tApd{LPxG565Q_AYxF82&h2i!Mnf@f zck4A88sm1iUZbHXcX{hIaxHxy_r7kwj)dOJy|)V;IfHD2KY%zM{s3Yt`~k$4vqoH4(>@Ma4ux&&(=1+eYQ4|>anb2y8?_{?DOW9N9~c&nYkHQs?&4R zuu$$Cw*wu$&}KYcQCxD0@@l_ZS#s%HAy8IeIsgA?vNQtzR`UOkCRs^fKGjP8|IuV+ z>SR-+Y>508li23V+f?#eF`fw~#0t46Tb72LSul(*=l>r~Hmfh%Wg%BKm&Dv!e@+`w@-3l+ zU)QvkqP3jCfS@4Lj?S6ERi>5v|D(y$2(MYm|38{6jS!lZ{QslL(ukB<$^So^ERDdJ zmHhvs$CvIXn|Nk8jWNE}m zEa(6KCJb4`T#3o~m1bN~w3ej>e=z9?mXr3FQ6O>pa6MKw=TvpAG86|}l1~2r+egO% z&?(u_&i~JhGiF3nM)nWDKMwr-1A5^6B(alBcKYEkla7UswjfG2Vlr2DpRDFeald4z zG=go&9Gi6Fg!*R}rr|{WvJ0;cU!tZh9XZ~RACPI`hi34m1}2dotxhpqErUV@?XR|Id%7LDYVN#Q9JU;a19-Woy4*0n*M z+-%_g+8wZ*(Nzo>{o^vaN+n_e$)Rsz!7ee_ zsy?SGS&M)`0-)sm&FET=qlP%jz834sxuy zsLZ6}vgI`Mh$&ZyDhyx8{t*T;{@Ur|zmcfw=e9nWYN38;ZNHI*mllTB@NZwr(s%ef3H zw9zxmu676^XyQL`ei4fXC+=@7_=-mDg13Yx3dKNLDs)%ODv4NAw)v&moS>zw`4*&6 zRWYOA|M>;SRCZ%PhZHmpvp{XA;K7`(7ROBnREmdG!HP($UXqxUO=GHD6Fbt4K-K~F zbg7D}PzYMH4VT4kPxu<%N=_NULcVmVSQH4=DwxFmQnKk};(jx63b6FEFJS{qhp!Dc zdfP%-%aW&FADncS;~v$BWur*85L?oVDuYekxWdcPRTVke+gvG^9My0jj@$=H~o#2JeqN0d1J;} zS&H~8`9jXQ(2xtz1wjaMwMaG{cr%ytIK#yF-WXHIJcj-G+279IGyCb;%B*d63vVCq zY2H^rY zDzBvIh=?xh>Uu@geY?Q%1o;>O5e|??o7}3_dDlz&hPwQ=l45j^Zi?|+sbVLTi(r2ZoX+#*?jrtGd!R7 zd}QN^jrVQ5VdLvIh>iQ~xB`!_f6uzU9$kO#+85S7zV@EAht~>g$l9L$>i>hQZ&}s$ zfIaW-CwJfPecbz2Z+rh!`ybqY%f7Zxc_ZGd2ag`S@&Gy5Tm6cyC$~O!@QH)>dLCO@ z@x0)x){|?8Ac_M30ulsF;tX3Wq#T{v^XN@52m=9<=zw`2@;r7E&~_ce7+UD3 zgrNiINlwG;L`ABZEybO%#PiqFXWT&;ih(G=8FAKGo%hF_IE0KQc!71)=Y8)q4h0cH z&e>6#owy&J#vver*D^+3bK>4Jje|i1D~AJ;?8N=ZSFSTRwF3Z?@qsxV9vvdR@1Cj+ zAwdL=_ESFR@uK&Kr*Q;`Kt{W38BVG{G=l^CnMiu3z8{>%;UEI&2{~AC*7pO`I1GeI zJ{Qj?ow)Cx!4dgjx7l;z-ZhOwK^U+2o2h~m_s(e?0>W6bTWva@qj(>ik}?7UcBC7j zJ5`%4h`eJJYAWXBvls5D|+8dQKbie)lvE0U=zb{QAJz-nY)+uzVw> zp1lcb8Ri3{!)@=spTdCz2oQqMZ>62}ed9C^2LW92CCq7k*!8`B8fOnKSVb*_XWH=% z(>PnbNGKSu&dBA_t?97^#;{r}WH`qRPim?@7y==r#8^Y;!*=g?P2)fif-~_!cxKG_ zchfikgrE|am1nHuEmJs%1R=0lXeGi<`Mr4t2UsQ_n(6i5IgKMgkgW9k{u%zhV;TqA z4e@clP|ivBTc&Y12olM7DC2xW?tRlV4g*2lsBpfSe(>!xIIJD>m1g?Yw@u?<5JcMk zNY<%0@3&6lAP|Hb=CE!$+wsOJ97x)eR!3=bGo#QOR#w(O{f}^f{iQ0uB!MqU;I5It z)TBT_B$nibzH>72T%U%4Bx)-Dq%-@G=XFjP(kplT&LQ0M&@>DnVa{Yr3CBp~d2kAb zlO&Y0+A|ZX=j*3o06~W0Ny|Add%9CFj3fwFm153qs>hs$5d?0;N@?d^36C)i!wHP# zt1;)~;?bQjv{Luc&K0SrGX+CQ0xH)sS?9|XPkkCj5TK~AA;BrP+B6I&0F|?5!g+a= zX&8o+iG-bv#93R_2_v*Y)W|wvl_?lP;HWSZ2hKcYp5inN$B{sUNjNtUo>x0zupSUu zXDlYqtEOQX4rMAC$mavFx>AkA0P)B6kn@VsIQ1``;OW_!t*xGUT=48d?gwcYGa zfn=xWSr7z3x>8}Crscgpg@XWl0b+_mz-ctzZ=S;0^EL=n3zgK&y8D}^aW)L-%GA&) zJMZhJaj@+<@MqOAR}Fv*FLs|v{SVeZu>R(C zZ9TaD?6tq%3+_F8_pf(9w)-Qy5AVKW7unt4`Si{&?!0xUW5@D)%GPJSMK7`Sp{=)X znOpI#!_6;leq!^zn~!e3aueCy^L)zlLC;$}nuqdy#l~N5e01Xn4jw=FjD21}*rx_x zxArSn-gofUgZ4q>;OhPt_CLP=p8bdS3;WRi&fXvHePHh`d)mfpH?kXFv$47UNt>hZ zT(7VD*Ppfa7gwHGd;6NX7GFDD{o?8;R^Pk&=;|w1k=4DGPfZ5r^*65ko^zO9`}5UL zI)C@2zyHgTz`^5Fy&Yb-dwi?+^())sc2A84_Fyo72y%=qBt};3rx!C5fn#i0Gz4X* zh{B78pwu*M(T>k?=(Ol;DluK#qO&Px8U|o+!oK!AGp)p@U=V--RbRP3GuZm4VD`F$ z^i>)S=Vry@bHa#PKciHgWAe+Vv^&~k&fhya_Jj-fj*h+F!ad>aUT^-M(6QG;KoD&f zOnPRFckJ~R?g^*&deFi>p<{2ma1T5^2Q8kf9COgZx#|s5`dGXdb)1*`nJhe8 zah#c;Ab=)gmFCRZ)T7h`FEA-7 zLOG}UsnhIR9D2`e2IjBwrj zeU1&?qJ7`YhHn18&#|E+aZC<+ zKWI7!H1E5oJRj%+|B}ORw9vma>o=P3Uvl`3Y(La||B}ORw9vma>o=P3Uvl`3Y(La| z|B}ORw9vow*p!qJ2!zoVQ%TQwNWO1M%8UF<(|#jlp?}HYH(Kala`=rF`j;GjqlNyZ zX}{58|HHIj0bc0OnD$fHOSA?4k!iojVtd|KDAC^UA?@??b!)WvA@@yRGlqJlXijy14oU``7;*KM$81-qlxJdsJLm z>CL~pNFa_qLY@o9v#C@xm|!ShKM(bLc_;*k0@m>ia=7LufVR-o>)m~5h5 z2=m=?SZjy^KcK^=CZ}5#Lj|ooWM?A`m$g#ON77UnF28yKJ<=WUKwe&}YT7RUp||M7vNIg>*BL+MUA%f|~1P-U`epQ(l~OSlrfX$ARP5&U zT3-!yxSo_4iCH}pv$;UbutUt5Y&@M0N039J?y z_k2fhj#-67iYzHyDTxsv(~bJ{F5C-&p_rX+TMajoH7?d6NotTaM`Es@iP=~n=IPWY z01ypkviZE7VlsIbiP;W8aXEuV5)3u7FrR0sDpm;DLGA+#&iLjG8jM-xvvD(82gM8= zj8)UewL!oiZ#M{tlKWY@m+f`UUbe0x!I7A2XJXbDh#7SB=75l=Vvz`y3Eo9w?q8Oe zIZ%o5kkW~3B+P1xnN!L-CrjAx`kG8W5`>9lg}Bh3b#&$33A3}#|lutCx7 z0FXXo^@nU|$>9m!})%;NPh7}hO{jRZoJ zY*iIt*s?n}?2mNCT$CO4=E|9vTMNYOOsg2mq$06YGAG+U*`j5p4;LRLE>$BxdaI3(DKDfGpm>MR$i29vHD2NM&qhNjzTGT`B_KO*Yz{PPm68JWSc7Ex9o%Z8;X+r8*}XrGR(tgb-Q@?ol5Z#S z;z-QWnV7W&Vs@^ZbH2Pk%;$wz;4TvLrLCI_xfT$E+Zj2N0GJS&k{TqHo;Nnf3S@>P zhK*w&tckst3LTr2(iD&7P>U|MtVBqrAet>#z+)m%-LZH0>6IT_@d8_azx7c&v!Aks zZEb9ReDkr*=H}OK?s%T?{E)}I^6OXLex+`E?lHQV& zTf8NY-*ex_?`^zy<;OPOyfNGmH|Py;{U6r<>-yW)YwP&BXYIeQy>qR#_L8;T)xhdg zR(^l=f2{tn%_z7ouN^{Iiz5O+uw$uQ8ujTyD_c-Wcpzq~oHdAONG_MxQJ@x1s?U1f zq(mes=s85Ibc1YF@<;sEu{^Xo#gt%lwE`6hWct~Z!C)POe%98~B{r8X;Te|ztwh*A zOtDhIXas`8ey*z^Mn6)P`(#R~HM7Nl)WZZjC>5LrS>VgxwCEqcebGOB+k{!9-W7yuSP!DQBpvsf zSW#hWJlP=>uoKjJMx=(=fhMvpPq2F zM2Y{kbcs(*N)%f<&BVe(!c6y5ea%PsRHK|K!VxN!i3lBrY@#}oH_{65wEd+^>@8hl zcTyr-)Dvwrsu5@)#9+sK53E3;beAsI3e`Bk(F|w+P)M!=s`urmB{)2lk4PXM9x$Pv zDbUHFo~S34C?B_Kun-XgO0ZsxhWpm~*DPJ)Xz3C!JS`C^4WSUxZxRh5Q_^KQk&=vd zUb5e;3iVo@Yy_Z`R?h|Ftt+P*%(^des=*~n{Lz>zXbFq@(4+)FR@zEG-vao4gKg2~ zAa8~wC1O^{J`k+rkVdeRFYtUx2R46q((-{Ei21r;I^MxEJ+@~X7NcdmJ~3O8{RBz{ zN}!+X^!=3iAR<$@$hMhAj2K&;X*{~bYppn>I4UwlI&wxfW=ewd{=|2m00|^ zOmR=0O!J`CO@Q^Tu9b2^Nlg(g*-$hXPM40g9$gphDV}bLNn9ybTKj)Jp|M1XQW|3p5{A%-B2~WL@FkNOBd(Y1`7#{E zsXt539P)QZ%COef!cSCMy8DZLVp2>x72h9B_Im70yLOP3~m+^mEX1rJ*~lOx1+qS0y3eP{{-E!B`u|wE#1l)GIBmIB ztXOcKKsjSrDGpOvf4fwd&|AM_w_oi2-Sx-pyZoNDU!Pnqc(o_}{8z1ATbRG^?5*x;JhVwPrTHg9_Vve` z_8FrGzH5JC6ZxwNp?^i+GnpzOPBE(Kl3!@(&2U3>zCU##Q#Tgx zAjs)PS%2G^He6PJu8sBYJ=2EEqO;mqf5(|XT~;v>==%4Z2~<#9Az4zDoR%2WyL8a` zKx9t9PtTjq1nRQztpo}~&hG7b^QBhr)R*Vm&-CSz`*VGHzT-?^E{o3U%k!P5jSJDq zZg1FTlva@KHu-$s`QX>ZEC|gEP3x)CF=ol?Vdtc=&YeohZAIr&T4ztCt^ECxV!wGXUHOL_s&+?zqUx0|oY8c-&b!%m);>Fl z|Nmzzd*8gh?&+`o*}tMq^1r^Oe&g14Y81NlTOL|HEHB2QGMryZpG(?ZiU?YcgK-Ymk72z!3P*p;9$ax z0VRs+@e-rQdV1Mkm)iMU169px8bgQ-xgrrbA2A~}o0%1lDDUTt<8C0V};jiYg&#a$hR z_nr-_e7kX1F*_`fUE$n#;XOz~5!yZq&1NH1Bjs;aa3Ii5NC^om2wAd#q}5^2tf~SP zyzO}5{RG);)-9QVnjKn^)COnDDq@_Z*b{-m6-Ecxem5Y(u!x;76}86>wG${RqB1Pu zK}4vD_KAHZ(oLo^7$UPkFX@+(F5`udnm&r6ymdWrW{OMns5>-8c%<)nrik5>rf7%6 z%$gsfK&X<4YMDel zKypwQC>Zr-CkCh;i%Yr;XpY%UADQC%b^j=I{~MpSx=gaG+cL#@wuj!6wzow7u6edM zyLcXT{Bc5nT6q)|{9^Hr$Fe(3=`)89o4BEfb}pN#2@suYbyJBpMhxBVjEG{u!Wl6D zFa)Ru2+qD#%?kNStN{6;EYg+jdrEStiu%n=9LgkhlHhLJ42_c#)1-h-0)QcWNYx7Q zP`%1TD_Av*Pz{Jkw^=Qa<3)nTef7HDkcnsw?=W~w5W1LLGej{><$6kv#akV$(~@do zQ}k6`%Pg1YWdAtd{{Hir-wpaB zlW%uRT5Xa9=4{!C{Nq|3LIR_FA#*1`wyQE>IB2xVMyiJ>4CqT!oKn`>I-3x>-L?H!_|~6_{lMrO-<*H70C)OMuK(NXxhpH% zrz~!dSUg-J1LrA+lQW!)IUKP@Q{(U3+%kCex-gRASD(x9nTJ{h&d5}!8?Q$so$iZe zBF7_1LnneJnxrXF4Yp0bY}KtHeM5%hgdGBuU_XJJU-*F zr*-5oR_*A8<3UB08ngkYD^VPg=z1&xCY5@}->r9|T@)aeGDqW>d8WAC<3Xp;EAZ*`TEcNVcdAVRO>Mx{VA|5Ep#b|L*U6#kEjktU1 z2gctIx#y8}@c7ijE~7Tu^>8dju(?KwvhP%PV!=*9FBQ0e9SsuGQk05O-8^ihm*w%P zBko=j7=LN^Jf~kPy{F)DI7DUNU*{OV-i|np@4MERRbzIV7X;^ z{IU^uFaGrS`)BTXOgVVG|1hTn;Rug~18AC$L<_Y{Un-ZASt?vGvvqs$InGr?pI$po zV%D-e-aq2*frrN5*SqI2>EQ7xhfTHLY4uutC|NIraU~ri{X*GS+X4E96Vk*9O#N!vM^l({MlBAEaHJ!HB|FUxlYXz=(Gld zF$3|lWgxROk5@+A!LJ&BCHFkW9XuW!(s`oMR6tpU(oizrG*}Rk+hA&rEO+^#2wHYe-{J*!spD_Q3sEEhly_7N2LIUs}xlGAics#MW`08p^dI6 zK_T4Ex#lM;hQEs~KR)h_xI5V%f3LWgBkkaE_t0ksJM4gBnRG$d>s%q{8wB7%$V}7? zYEVh%!qGTcGPo=N8q3=C?uff%ar{-?^B8gPxO13hb22J)+O=B1YGcu19BFoajbt1E z`(_W5ipLt7pu2vZjxXIGcShVDjn`&JUvST3*umrWp%F?1i2%$?8XMHnKB5MbN+Yif z1`3{u5mZZ(EdVd1s9t61HN{xVFU*fPec{)+=P~5q(RnFr473HyhJ-7d zBjw<6^Kc2b#IihYj=1~EuNr?{%kk)XV8r9g&i6B(WzK6)d7M4)yLfBk88Le8Cr01r zxgIK?cilf?_vD=2jb+ZYPT9S2Kw@0X=EjJ@XaCjc`<$P4srk|Ve`keXIcV+u-Y&EK zLGRJ#_jtC~`>TI!|MIqf9ythkU-77T?Kwfud_HC(_}x7b8}9bFednA!M&RwLCOADd#j}v1k0kpt=ig-l;<0)V?nA$NI@y7Rz(ZWZ!VfAywE4gwb_#CdV|E>g%{9?tGO z1fXk${OSXb9QZF#h%*G zPDbTauv5=~8Z9>{xrQ9p>U6^r>!~UlZqjr}Y*TbYqLoI15So#2JEXuVwT0QC{7LRw zG80iuHO$r9TtLlPwBme2_TVE=9W#LRN@|g;9ZOa?VniY(oNXX}s}raOSQ1VWS}6=k zJ;kc!Q^7;o@6T&7CWvM7&0Y#3xk0Is48~A@W!O$irUi7sPIx$gs~z7ppHM0BHD94E z>U6z9iG0@aQ1-z`zHDg*V)Y)RF#;6rBSReU`;$$v;bY2`A{N9OjFs#RGZa%m+K`-g z!@&JZGjOaGYm!>Fqx=k#8T(EbUknAZU<&r9yS<*CuXcKNDE4$T5Jwp|3_NAbfM_61 ztvBfKf~vHdCYUFMlF{%3N+}S>l0HHy`a&>Nwj&Q#FIxroEzQ6stKiDg3|z7b4#o`B z12k%8dS)f52D3Z~WQan$k!yu5Fvl0fmM?frKvF22YZmjDt%Cif8MtH>>@Cf}C97a} z%)kK^n32+WsKA0? zH(u_P*+Pa0qgHj9YiAeV<9hIst)&@Q;#Qd(1~!*wpbSU)Av;4(n#usRYA4BSWn2yy zk7b#)q}0$CDe@Fkwlb7KyJ5gHW*`-%j?;;T-bU&~ClSe|!<{%qLsbsd3Tn29@-&(z z;=x9wXk4}mHkM}Kl2x$2Gy|8cg0(RNC`!v#5^8l@Rzv47h0z8vz8uTNL2}3{96(6P z8eL76OVQwEt6+6$1}<3zD@!wQxhi=0p)mtJx!=TvmVLw64hJpv%E?*?>oCIv=97Yy zDnso=J(iNu7*ixJSp^S2xHJQotAdBWerX0SSp}~hGq6OZm#u==EX}|rt6(^0V2O*> z%Up4GF&8{M7&EZMJHA6ZwzJ#!yBB-RAAGpKJOg~xEaS)Kph9%w2HF<#N-}RKiE;!$ zQk)u+jcC(n$7C;5@@^QgmS$jyCm$CxFxvlbtbTlDuj~D+=hgNfzVvgCOW+&5*CV5s z#cy0)UFO=`#a(62JF4?X{>1n-o~g@X!w&CVmi1b*YQO2RvyI=A&#IQVi#P9eV`K7^ z{-5Vf)$HqG`-#(n=f>F78%J>T80zw#78585FM3_o>ewL`gn`&p5c`^AR#vkDX_5Ih z87st~xOI}tMLIN|7-quAZNIK!z(O}F6w+P63bU0kofTW@gfA)9ffy!5>sU5ZviBcw zL{G&cT`+k>6FHTKt5K2)^tgz{o#1#N(lXmBZK>4=Soi1bN6?n5H;tpFkDeBLuZPcU zafvQvPIyYw8bUvoZi#6h)&xgaJjY!K?|Sx=j`|MV1ui+jfxJ{SVjp{gQ$ST z+j1!ru(&eYvtMWAh#bmJ9%mZ0YEv}3x@_1Z{A->&9R+OsE4d7zC`dja7L#RPYxK4= z!L=er;Ut~&cVrxSJM7O`n)z@XQpK=uvlQ zis&L!B<@L5Tq1ziy<&>v@I<#ZMdETz5k?k9-2ihTSw;1Be~_tgd`s>OeRj$ja!@0h zh?MGt>rx{$?141`Yu&af#xukCk#47e;yVc2Dlmi&KGV<9EwqbN^#~f| zHAHBYrbwKZ1eM*AF;Cr_BT^2PprLsO<8-!{*(J3+kf{#UK1_nD2yy&v-OulSWcRTvPh5H5Zh!kTc3#03?7wb5w{zba{RyulzP(RQZ`+^0o?g2a$aI1U zTW|7EEHuM*4(y=AHNW%02`&+8wYgwA8!8R!jd-tCbKpFG>Js-^m$*N1iTlh17nami zq@2l1bV~~LSYOA%pZ5n`;=bP{?p+hyh4TAFm$;vIiTi~KE**=fxppQms!E@yG&A66 z@7gt&xT`L4&vA)+_5^offB8$7xc}=C_qhqK%KHUDVxz4{P_kNa$=MIqu1s(j%J2Cu zabM*U_mwVjhc0o?bBTNI1g9o?>1N5-PSH{~HmFjnqrYt7E^(Mk9O@E>OmG+K?S7ZI zr?|x3H^E&vZoFfH^Myrr!1nZ(La}~B7a|V%9sCEExc5zP7mgc0=Mwh;m$;vGiF^M9 zccFc}V2q<=t>U+zk_Pl_uA1mK61+n$I|Y}xyh~ipB~EaO<6Yu7m$u9IsyK68DOgmDQL3(`f!*CKZ=@i@U_d#<;+R>xjJx?!x)^mt5i= zcS-laC2oI=qcS0XTkTYuJ%73sZZTZUG4Ag7UE(a4xUNf_=@MtS#OW?^9hbQF1b5*$ z9&(8bPW=BLUD-3ef9xr&e)M0aHU8^p)i>^3kK4zp54~}Hb(sK6cX+G{Li5j6?UYIP ztZtPCxU3D z72uE)E}lxsWQa5xYRFC&9qwt#^q`J#Xs@7zTmE4*lJFBSOu6$nj+;Jma%^9ZjjhqX zhqFYVx>IW$2aul)GY$dwW_;r%0-)V>);Mc$wnZ%PLXy+Y|M_u`Z_^l25=RzZ#EB<* zgN7CY^g&*>G${$xYp6GS|G|;%T%P-% zgz<$lBdUv3IYv1T#S^1NnhiESIfVZE~oy0$w^oc?1=EX^CU}Jv7>vB%Ut&<4P}T zH8WV4k6Q?E67adYi8E^Y$QZrX>5=^*b$J-aX7`RLB8w*P$l`u6?a@Ao2Gzr4k7{_*B(HV-`ST*cP@V(pQ& zr>_3s%CCB$jStxso&Gd8)9JJwv>!xq06<`nL?Mh+q`^py0AlH$d(E!fLfe?mD9Xjju^}8VVqfT1G^=oxuz_TU4+v21U^Uz0o=phA>EB z2T3R5+QsC)eumtl%FfBz;s!ISG^1S?xgfVSLvB%J z=j31r!K6H2i!V=ZbB5fa%BIMnMkA7)S#w@2OwT-Wb1OS12SErjq#9{u`POaBkXuyQ zIXMtQ;Cwf!IB~$55M}68>{Ls{o$UHfbIKF zo%#NkNCtL?XR6?W=Zz3>Z$58aB1PFfeW(gfe0gVCR>4bs;AoGJ;OO!4T;D$wJV`Y5 zIy>y2)ETssZksh_V5K6K#^g0BTT;5!K%|i{tIF+rII^KU!_|b2eSll3qG_E~d$oS9 z(J;)AFDj;wauJiyg_uDwQ%?$AmZ_T^B$d!A6oznFDH0hNO&~>=IZ&jBG~Vv=3Eqx- z>Bhq`Zu-c>v2#6nW{peqsXMjC1uq;S;NE=UxI{9(yUrRHxEnziPaMHZedCDPNAC-B z9NW3sDs(o_vP0Z95GY#7wy9h+#l*w3WwrykG}@q5Dix?UZ$Gm*--p_2iO-M;g{WY5 z_O+<(+ULTIPiJYI;Vbb$`Y7yc`21$C-38c4&ahLRm&3Y|&uJ$y4c9{*T`3 zUz*1e18ry6qPYGX#)Qx`Kp(27TKe$36rw z-DHV55vrJ?Rw!IW$54`iN5xXa?aft-0_lz};uP$C- zjiVqomuHOtw)p*jw2&2vVHGPkcmY)v*w2s!LbKvDO1G3O-)5U(QfR@{z<>LpIAFR{ zuHvkSsU_2BVg6bLi33hodUA=z=47Z(eYu zvV-HgALf*ZK(d;jYxLD7R}oSIfb?U&$e?A<%25{ZldA8Y8C6}}aD*=Ph9d@(@aX>X z-2Fd8mK7-z$(sgAuw;w_L{$uia(b`VuG*{rzE!P8j3kjtv%%Y5<7Bu%x^bl9nSdBB zVHG$N;D&}MM;W!nN~M&qla?CMNR$|~YY7!@TP3s5#l!JHg$H;rEM??gld6%#u#qjL zhY;D5PVz&S`~OkXCu^L{nKdrar|!@i!9`ab!F%(HqtaFSq!AHvssI@n55k6!DD}Ek zJs0dn{e=`v!~;dS7%dK}_lz};V>8}*jRRfk9Y+kdBk?V=#!iXJLrDoq49RfGPKRBJ z8b`^Nj+>T9W)e}W&hd#zdwA4J-u_FRe8pELExuJs$rZ4Y83cT_kcJ=em8vS7GzOrN z5o>B(6Oe+ROzPEMMlcl_Xj_PA6#R*FBvm;XD*c>~t_}0NuOa*JaKYVu<8jj`*2tb& z;}U)9POWiKNL=vV42j!i)JD4=j-?1T*CB;7VDa2&7-dnZcEho1>O`wR9F7(Onph6E4YHzDO0l|p)Tu;FAY$Bp z98SP8lhqi&#LGwa!JppBt3$Jp9aLLLkHbR2C@1E+GHz$bf^)2$pvyd}G&Ok;m9wob zAtu{76o$nZ6w4W%y49+vjV>rMZs$h3=@V;ApIPG)edha1iHFRZ_Q z{r4dpsomSJqE?2S|l~8n0*jZxuVKhKp#qQb(OgzlA!*a3CWW~&EDR~RHqxSMf7;KkR&?ypOL)O7qYh?iFclIMskU_rFW!EtaINP z$xF?Saf?=>ohxS~FEt6kEl47rgENws8tnWQB;n5f8ObHWgx`@aq0ZhJ$;yBzQxO_O zrK&Hb<+yx~IL1|+9aIHX5LqVHJw_p(trmsq9gzgDukKE0GKaZPr&CWGg?h7=)e|g; zS%W|*=`X^ADv}8q4MvP-5iUlgsr0p^7zmarUrjXIR$daTT*+}Q_-4tntybgeX+EE; zm6pB4gmwE@$EUwCuCMHjd3Zj3m_sut<+ERuLbZy>Y7AZpABSRX3T(!EU097!kQ9N7 zc&VPYuBmD@805L=pjqu^JHD1cE#Sd+@71KNY?yXJrK{t=jJdGS$gVbJ>*|P?*KSXw z5IBtM>3CRG%rXlNGNGOr4W&I`D)F$IwE@}jMIOAt@n(&ULDfGaKJt%AkrMi_dh$f9%H=0Z&d-a$QtS1*pf3(H8RogeusHgg5hSg{^$;t&^ zKL_GO9%n*+3h3w6U@OxQ1u8E!l~BFOUGr1EOk62Tu}Z?~2eN^T^O=&{wtLQ~H`!3M zfQpSUEcAR;l&NX{QujEX350++7$^f}u%P%uO1~Y-U+Y^fI+@Cb8&O)z=0vscd>!JF z`fy`R{rN9Btct~8E=v!hd~ZFy;Pxty!k8iC{TK?6CeSaH?kSvTNTyl_#fLJe5HHhPG49%BI z6peFGB+?72yf2Hmr)?)&mq%o-j{a~}vHQNdHfij04hIC0WlKJK6UjaMJZY58>J`sD%r^&=5w94rA_baYR;`3m=~CND8PS?mQwc(iKz_kKMbG-B zB~DhYrd~@8b1Ka=%GpFK8gssc9Y?2;n|9{JQM#;`i_KQ^*v_6vf&c&&K|5~_0373} zW11QjIyLKY-mPU$Rvm#=E;Zs94%_d27agp!sSMf5^M!uFG+}}=aVRC7_`_;4+8ZiX zCZgxlO1~FRqiX)PuK-jA`jEC{BGzM@aI(~AQ8-nV;d+g&DjL~IT0WrM7pn$EP-%=3 zQX&=)`SN{PH3~<0I(lSk5+DY1l}g?u`DQeg=Sqg(RbZ8I(?@QO?d#H+F)q=i?$j6; zIUS+7PXO+EW2~U%lt{$Y0A6Z$#KABW6pw30+LVs%MRq%(0Iez?PPG!2%H4IwxWJ>~ z!smFo5yxlO5m*NeHppO)(3(L5L1_B{jBU}k zll!k-z_~#%RI0QRoK%%8Sxkpc?CZq6R4Li+NnL-QVj}$`vK2M}DG-V>yctwFroDR9 z=?2|zjjjavm}n@cc9BFiPWu59^0|7CGj96C7)xiyxI~w_Q)67@bA;x;0J!UovA`?I zlwOO33nf!67Rf&8#{fj`RO_W?48)sd1Ef1eoGmt6cbzdVGc!)R9KlPyj)U82FBd)l z@L^(=JZfi{dNf@y04&6%lqe91W&GK8^Q0FV`0bpvY$_}Rw|_Y=L%W6XsKVROuOtQVBKs%7EyCEM~TwLLqMUqL9&whYEp2 z%NIok(OkgY%s6WL#2CdhV_c$3-KjAyda7zC2Yty zlwosa3yEi0JG39!KxhS`>6Y!ZW^zn2R??6ZOG&DX!?j37_2mq_Z@R*4(6N*0L%3=Tz-Jt$Fzu*7iw}}?`y3n==TM9@VJaM`>j%U*y}a1$uYpp zIl{6Pn1i-F+WI$MG&*{GQ9~YGXc=wrM}IBW%Aq7sS8Ls%;7DN+_s`h}Fy3*fh(`E{r=X*={lBQt~x z`2pLCiP`1}5+x8UR~PbF*q#wWshU60YzTar7ju|zm^a~AsR8nVx+YW%@uX8_0FF*U zR6bWCK)4s9TZ>U@sMppMtuUK8?knQ3wIOnu*p4H7+cD>5(I}SS<{mNk( zk}lhQ<)6ng#7woHygzKCC!vUj2e5pJsq`Ar0n8z?>I_k zfG!+GTJeO^6{;0JsAg&dfvy@tRLv=aCKgTLaUQ~`=Iy_64AaeMy_3ld@nMq#W94C^ zU#|u#QZ`nG5nW06eJwpr7>#bU$<*UaHPS*u5he;(YeF<{zu<{>@HQ0i$Dp2=C3l~!?AOnA8$D9yf~M7Uv9dm1EZ^?H=TWBJ*GXUAB>l+uHizAnKE^fbRyxBU(8-&Yyag5j(Zx>l(T&;kKK3BD0 z`^Nz_%Eo1MKxLz4pB%A#%up<)lWm=3<0yOkuW{7y5h(?lRZ|THWG+~ff<9ka6j}kL zQ38^jkkd2e5ZqI9pp^mg!zSIf23g|dgrg ze{L*g`6J8<1!?Y%mpVz#BEZa-2L*ogH3CRDHV#h%8q z0zAO-Tw5-IWh$FKsiYXA9rEE=ILQWmNH@`_<&6rD#Dh{P+bslXgA@23L(>V8!%~E< zW@DUv{omc(IBL3WjXP`bN_^#y*AL!skUx0g{@?C@bpQMI+xx!#`}h86@2B>@VUOSY z>fJBy{iRb{pZ)e zZCzaX|-(3BnRcke}`phf8aphfCjCE*zeeDx#Ke*Og3tyqGJni7q2mf*9{pTA~ zo9&bHzg_sJ7j2}Ud6tX-!a>50oyz!yMAR{F7>Z~IwBG(NSB#bE640Wk<)0!=!xAK&kU!H~I&2C;_ z!25?Ca1a8qSUDJ6U;!U;zySzEMUfpkvtgVv^Pi^S011Ie%CaxW%$LkBP3sgOuo9*F zrZ05d(COojx^W1E;%dgn%&+?w9dHZ+!9=r0FBnLE!2w4h5QtlHe!7DJz-dE&-T_DK zK%LRF)tF8ua0C9qSvWC>*M#Y&-hls{0}ev~KIj->X9WFIWGY5T$Se67A`8cvWP3D`R*rZ*q{ zBMvwL5~SEq)fd3u?SR{h9HLxKHx^ja56{B!YRRWNZ>XLM@`oI73?#60ztdPS&itSQ zj)DZrDpF3K-_##)z+sR;vh9H443mG#%=bIs_DloUI4-wfee*5{9JH5zg&NB)=wk15 zzybU6T{bk(oyi?enR#p$4s^S8KRmyw-!~1%Nf0Ls(Ll!;2>rD7cR1koJwPIo;ac?k z+P`;dX2Ng`Z4b)wbdR|)GripjMyM96Ooz9>0e+hUjKYFHn3>k+4e<9kxJE%76JqR4 zEdSF+ezyaTfH+$1>WjvJw>sc3h$G2FT(Gdg+skmuQc7% z8*|>jbHG6m2Yu;+=uBI1+SIo=;C2!=AZt?X1(VpDr=^aOP&nT1Omlui>fbqCH%5RM z(G8UH&TMI?tbB(9j)R!JsmL@I?1|pwfMXzr(f)R7Fkg({J`1;B0jNrQKK$Dpa1_Ll zWR~-Vqh_XOR+4Hk%acHcD6|{7R@ee_d_ipag2x0Tg~GXJF@Mb`R7!l! zS7?hmU2jk#?_4iGbe*4Yutcqg$6B!_spUu}J4j~mR);T!0$DI+hZ^tpdV0Rv>A}5F zIvR+hj2jNP2?tBmdMNw-c`e2Su}r?%OF<+zC^eG780xPK+eyi^fDYIR4+nO}Sf3jX zvJ(!LsP!;b??DAfNU@IEUw9sBK7BvO~xWe|rL|V5hIA8%2qTb;3-+%u7JkSIvjS7!zlG z30SwMWc&Hpg%24Xx}KVFutcqg!A#SagDpWfeU(Ag@I#%hMu*XE$)pi}m_ev$yPuDf z2|4Q`SjR)xlM@b>sP%9lMvoJLOfzcLh=@_?k%n0~R;fWAOwf^GsKF0_WG|U+TC^(} zBqkgzQS0GgNcXT*2qIHOrszW{ovj6j7!-^vT_iI=h8$H-bd-h|=hAMpz?^d6zShGK zQ*DzzAXCP9lP1hYG*yAZovyEG2VOJUnG)k~cQxD?2x-I(2k{99OVoNu8(m+xngbKH zOjA)BA+(CcV{C~4%%%`zt$YgrN=-gpMwyly4q_7ymZbfF1l;ii8NPpU{XfKoPYBFR4Qe zHv@5_=Bgk3|LnbYoFjGpKb~x|y@5!_4|m5RWeqzsnM`I7k@VgtlY$!3>!eJ20AW`t zB3NjGU;`Azh6)O(U_+V|3kui}5os!-qA2C}p5zv8W;VH*+xsf~9(RA_0khBd>-~C{ z*So!%b-;lHs=L>H?X*?PfCC9maZ=pC#LPtVfCCAhKuf#TQQ@>z(|`jBRF`(Eb@pki z#sLQssP5jlQKzjM1{_GRonG3lVWdu5)ekt3Ky_)iMwL2kRX5;30@Wo5>Xv9<)8Qau zy+)kUh7(1vDUkGO%~3xLH=U_k${!8j7EL=VMix#Oa3F!|lGkoj`P?*9MGJV%Z4LV@ zHK);rWP_Sgv`EG@luhHqohI5E6Lm&<`+x%pRF^awTe|H*D$PopW`bDGRq8sZlq2kf ziJm_fgz%&%Z_9d83A?Cs;_(9xBv4&yr8{M9s6u;NaN8Ykr962vR&({-m@a8D7Sl-8 zU2u5Q8iy-}iy4Kt4LFcMbxDF#gcuHv8*m_j>h3iUK5g}_0}dn@9q(RK$enzqje=}YpR>csxiN!VbTm8GaO$o1aF$drz0*qz-<428fZeENtYy!a7dnlkrjv zJnF{(N1`{V8i?It4G;-R5NHPBY3Vqd;l?JoyG<;$sAi~xJXu<1OrC^WV+br8+@>1Z zb-4^4r_RQ7wYr=G*D*OiS}ep%sa^NUEs$PPtHZ+t9Y`AW&PbJv7Tx!k22W=$*SPB%3jmmXIq3DnV4r!ftkw_4pAOFm-sh>Xe2HXVrN z*}2sSE0!OO$#?ISOQcwC7%E1JJ0p80-Sin#DgmCCSQUgvV`3%X(_i`@$yl=v{KUZ4 zcCu)H#$&gDXiu!Ud%tKbmCnSQr3N6ux+=aBo#5nk7t!sWm>Tx4+r36Pt?rY1^=})V zc0q-&B9gqN>DVnh(jItq0&X;Y9tZsJm>jGEuQJ{*pNO-*a{d1-0*&YTe}hAIJju4M z|CfBZl?PsZpshRF`){0>YPYR57!D21RMoAP0)|#q4L7N#KG=iwEd*`BCK4qBlm)5K zS#>yNjr0(cVb|mTFDJBgy{9YK3z@vOL24r_uph0KsURH1EFh4ifd)ROrZ{ELW%IT) ztqtc3F1mo_-1VZpZ0I2&H=1i`Es=B?1J+x)6(*Q=8{49e0BqC2TEpJ|50IT9TMR9H zenB}Oocqe${A_;a)|su-r%pXMrJ4Nj#GfW?O@6ohC{SQ`{#)I$ zT3eLK(?|Javgs-L`1pxa2Q5iZ;=bZCU9ZQp>78E+4wBR7U3dDA+27SmL^>M-9;-Ya z50s)jmdfLSEO)1!(El}w=dr@$@c=36WvM(Kz;buWx&7Y{i|4V-<8gl}3TUZ3?$2`9 zzM}v8CGkAcJRbLxqN0|{<9;l6&8_|4qs8-B;_Ou5w*Ni&hU6#l%kNA z%Htx-W3E5qxdX)aW17d~f)tg#R2~;t9y3?U>CU`u~N-+$O%Hu4{-AR2OPr6Y&k8vK4Gg3?kr1Ch! z^7yV_^?!dap2w4TJWfk7E|ALOG|SyPW&K|XYj}*uG zJWfh6P}t66{{bv5!dPIEvvY=fe5l=ud~lQBkwpj1HsHH%AewR(CPPm=;E`p~3W; zd3(G@WQ}oGn1MWP4_Qs;YlyB>)O!rxQfS%T2+-ONsm1)kT@EnraMVcuN~i1aE+-hG zRn~_AgShw4X_DYWEgd6s}y3QEf0Uu(4 z483$dgmoO0$Y}p;(^*aG)M|_s#`m8lmvX4vtT6V&&ut`(8wNFc%?qOho7C5wFbV|@ zNZ}9(ML7CYHf-GPlSN324^?Rt?T{xRm`i~o z5{7YKVcDV~OHRa7N3lk#5%pVh_J-AN?KBZR;jw1L1sKz^O&`eOyVhk5lqFHX7J<oB8H1y_j)Yvkavh6mAa9PeW z9ydaCl9`|{Ya*bQ$6`&lb!1kfb1$2BeP7J4_)Km!UcyaHr^`g9oE0MEGn$=deZ%R8 z@K$C8Lj$@{BcAPbF^4y&N19pE(M?to>cq0e#W*Vlzt0$|LTP;hWKs**YJQhEQwET& zNZSv+`KHx(vZf5@+T>C$>ANJ08_6E>x|BT$ehC}N-uAbR|IMLvy>d2}mPq5GckU>G z+6^U;qwz0Qfb~?aQg0lK;}FC)a~BV;7=aEq0bI7N6!9_*B9lgI-XyH+xXOWup_A2= z$~|WeWQcOB9X7}k2|0FsvxJh?WK`pVi~)vGMuWy#2QI+oV#?JiI_OrU)up3WJc!eI ztP-r0&}=e*cnq$sblsH+q?|Tgt5^yIbme58XT7$b_>kQeTO#!X8lDKqEjWHGLtC9{MM){WzM1*XCsei%(l(d3DtCiI#o*$VPI?wIDY9=E`1p@ zVELp!Y4C(&s(MOOsfFO2-&Bp|ils(mQ*{!x6V|Kr8H_=_jevtDbt6oh^1h_i5Dw~* zf~J@;8e?k65p%2aDs2p8=iSV3;z z#>%EziPhe=UZpqaDVB^B9gJnV6_vhV&o$KQY#@+9>IRdJDHsg-wknZ{kREhXRcdO- ztyh^gxxyJHVKvktN)3fnwAJa>+Jb2&7wD?pXwTwrr4TCDP7~QpRf*Q#x?ZIzr^@Eq z&0GbyWHEQj?P^0bNoAvWukE+fWF|?ebD3_a*tY97Ri(Oi?0S_<4zVPAEp@&f)VsV` zlfjS#f`!U>uvW{e4CzwS$uvA@wo7lWW04xMUZtL9)R|m17mJ%+bi>uw_L51Tzh!i` za5z+MxN~JV;nHQ2p-ol^*YNc!wP?3#${PZzdM=U=61Y1~HNy^d*@|}E;ke%IiUlde z=q5lq=S_7i^lt5-O3PBBtYL74l466^M{3cy$KrGjPN^m#kgH zZPPh>J#($yChWMUmCkp|^^`+fW7JGl-84~oeYI|g1u9!9oynLr+Nm1d2}irSQXn*B zYw0KO?I()1Z2<~~E%kJxpDVlneM`KtrT>!Ow?^FpRW*aoSY9%gupS-NCrd@76f!_g zGfWbxrn6ly<_TRSV8gOeo5N%A5U6cy#vaZGtBHiCqKet@oPMNwc58AdAk|F&b13 zH{{euqQ*4ZDsFXja+V~SFoozw*5a}$yl#%qk{v%!0UBN z2==nMI&lVCuTw&Jm(A6wWmeYfln~Hmb9G|OG3#|o2;s81IyH=Hy-o>1TQ*lG%Dm;3 zI>m=+*<77!W_i6%34vKQS0}<8yo7bz95Kv`PRcdR8uU9D{g36|<#A-|HRZ0k%vZ*RHwZqn{ln^6j zQ&pm@{{Jnq>tu^pFS-~0v2gi$9C&3D9@@dB2v#J>{QqhO}1miRJqRBzH!)J`A z?W3~(;)tgVir+t;JnI*ieEbWS-}alYJ@?6dKm6Qv<9C0ATAS??jwj45j|cW3S%=20 z3B^G^Fi)@MifCHdOd;w*u>y*NJh!c`FFPQd|4Ie_ll!5u8?_gG^!UIB?z!@S$NqKb zc^BYM9)H}+*H!MAqSj{mgxiHW5;8k|Ij=E_q(fHBo>%EIc*Pt{G>xuWEL=y8Et=9$ zmdJqc_uh(y{rA21nFk;I!Ca0xo~KX0>(E~&zcqHoKIc64m8U;&>C>5gE~eHd`-E-f zPCHU6Xrm?*6}KC^$uOKYeZrVN*EJ9wB;>HEUBQY=r)||TrAStdYmt(_ zm0)72h$UrrwXn*7@Z$$Cv%hMEuh7EwImI`hd^fe_xA#1E(ZRp`@;}1|{=|99HPqU8 zpK#dIgc=dG+u}DfAmy4y-!K-Ob&S%KiZ*A82}CTV2&IPo0gY=w_#IDOcl~*HAGmih z-g~##ee-?iOfCKU{EKe;L+Uisxsx{_2S0G_58nN` z8*ir86n(-Dn?YMh#mXIA+m6L-RIAWS1cS+d$yQ8LMaYzk>P#hbAP_~{1Hwn1{>}4# z`^VevdFJJ3_r3YJU-~R>*z0}&N4b<|d`NjAd4Bd>YHh4f*d8}(?Am5GhZef6fF>CX z2Q0~uLEEj^8jdWQwIEHC4@!pH`1U?N{FBcg@y)NEb@sF0KIR?1#+kK~zIo@G>ga_n z2N&OKu2xF6yd$VZ>+0%ujw8&Wa~B-A7hSve;>GjmgYUiToYQsR z+^57Orp^i6{FdK*lv+KlPq-6`_WZ_rE!i~_MPsNJb{W%HGf_0VQ+BvbV<22l+*s5^ zisj;f<6ARz<6DoeZ$0%xny&-;2^)KKsM{+iopj1tF_WFP!yQ0ej8Ek-b?^*K z7TvH3OKZ$ws6HUPNL+ju@vCF9mQRv@{mzyC2kyM@U!Q{i`eNWizcxPcXH(2Ut)9{+ z9EsVz*|52i^#_V@+#JGz>K4^!+>wgbgVZc4+FXyudM+zzwG9X-pY^QPa;tB=>i$nr z&;0J;hxflb@qv3D`qa&roOs~N>PaUdAho1WV2M+nyw)fb! zn7>V}_WFeNaf~up^Oi<}j+jkFQ@O56l(H60wWPAde6en?={LtJIlUpdT|maabkS7j z%1qiJkf4;r!UVnZt_k7|PDfgG3La(MfC(iD# z@f`ZEU)QPCPM@&N(@D6>y;#oGD5=^pe+Ra7VI$Pjq&)Vt*HkM6;1ci@G^X?0*NQDq zpNO3D@_X+}|7Y>XxB0&Mv$v`L@YDBo{(AIzs`S<;-qSmVT5b0U=LM)mEQA#3mxY7bsYSde5Z*Um@0 zrorGn^4y~@KX7whe%7_MU;gR98zyJgPG36pSARLYL|=aMA=^&+E4A9}6P6I|cR;vs z&k+~Da7{43^@@|5$@?$+>Pd|=u6_UG4bA0y8H-CN0H81{U-!c8l1=K3jCoCc4?tpM-ukeLeDxY}s5#M_8+krPfhU;$} zf8hz@uIf*|SUC8&Z>)WiTCMd7ONh@qAbj*&pS$qZuOIP$>c5-QFHI7e*f#gw-gtC z;rZ+LrdG>+!V+Fw283Vy{QNP`?*G0=vc}x+zqa2+=`*%b_kVS&b6OgGs1Uy3%V620 z`-CMts|*NVc4zyLtw;axx;MXg@62bD2VQ>`divA>`(AObYOnd<+;GenC~CFTCoJLZ zWI%ZDqrMY6Wxp@ocj%q7@`pbE=`H1Q`6qY?y? zUAp>+sc-$?ZGU<0uE@Vncs3cZ9tvkds)cVodc=3mq*e<9!s1^+288cIRv!Or>+diB z=!FHt7Y}&vyhJS!XkSksc2{QU zpU)li`O`mk#tC1&@G|J2@<+ajL>8#kT%WLn_lyDIZFil1(D*;FrK`_8@QlY6-hb47 zFZ|@epYMIh0av;X{uueDNAILovwgx69xVohzjUO1;+8uey!4@&r$7Cf-bFWkEOaXT zW78>z9R1yMrVhd$C{e4KK4A%gU=4&@B?Dz98y;QB8^3*=*XK2Ngja?E;oJeImfx}MKL=cmo$&HonQQe_KzTu&Al;8W9=!3YGvtR*tI0lL z32zAl!iiJfbL}^tUVi2u-yk17C2(K)^2KW}x%M2-aUVK$^~71XbAM_z(I+h7#b7}A zw2yd--+OfS>rZ^{3txMq`Q~$epsGLq^>Z$W|K#j_5BT@|_e|7kyiZudyTE{OI_y6F zhO-X5_)izS{Nio*^?rKRHEY-(m*4fr*4@_6zx3mmdIS6apULK$Q%{b^I)0&N0OW|GvRyowk2-2!K^SCjm zDz~==7?-bVbea7ohu>v#wiEmuPaM|(wU(RkSd9!bM+U}V8`)uXN^~|{54O?;Dw?xb zX?HwaD3^dOM=U}oDa>PlEoRDH^yP^PYvi-!4XV-%g{3f$r_t!eNF!yBWvew;mTttV zOf#j^7W75fP^hHZeY_(?JCM592BKs*%X)C-&9xF*n-TF<^O}Oo@570#quMmL7?rPY zM%6d`dnL@uiQ7Ovi5L^lbTqDDD&)+$Q>eyoQ+H$aa95p#9C0+!uGW%mDBbSVH?qST z;vBHkUS^r!S;}KI2JzH?Y^m)ED-cEHWP$Cmx^F2p+WZk^&G>hd{UgX-s8ypw?H{d$ zPFRN)tqGeQTJe;_a8MWS8hWtR)lB!ycBl=a%&!X0II4xZNH&kZniZ z2}dtr%hPzH*z|dtCNxD?A-BO*uVc1w60?@0?xNLGAd;?#CGDi+5hR1^Nk>9o$kkF# zALJm5`I-S(d$IOv%`$5p4dO(yeGQ_H#2Rd@A6Nq{A3yVlmcG3vuCF^k)=1w}>+Si~c3WW|# zAij|@LU6nS)Vt9w0L({B3k?l+SWg9NspJZj%h*iID`><>_>+VoY@q|ri+USnSt*v%P&A5Y)P{(@Yu3gQW6h~|fW;Z}CFx|#SxiuN+_zjR z1e)z$*WOM03oH6~+FdDCX?>+(tE6=GpeJrzv55&Wy1nTGQMA7ziV~*NrbQ9yujdUW z9j9XzCY5ILpdXLj0&+TT=rZ@3-xd9BV;4r-=0@DpE7Mr8=X1B+M6rSvFmpR;Fc+zE zp{mih+v@gfe^*3sc65XSx}!<5dN+Cnu=5cl&;)gEm~=dtYPX9?TR>CRBGH`Fj+A zwpT<^!gSiSDE2)-Ht-Vwd-m&cSETd#mL*`vw3cqp7|m4kT5B^K50(?zdfpTC#;pl& zL~Dp?&AsGnPZWhhx6!Vs}O(BA6mAyDo~MT(Oa~B-@nRP=c20t`Jn}l0iEJ5m1?oMzW1; zlXlwT=H->JI;Rb$e1uaIt@;^%!dh0NH3wPkgzd0Pv+ND}sG6hU)Ead5t+Pn$e&;?EP`pSWa#p3qO3rw*QcX7bj_k4{!5jf>>s zQ423E{B+^73#Tsl7mk>JVgASSpP4^--aCKz+`s4Uox5tTJLjG|Z1!KXch6oi+n#mK zZk>5<<}P46@W_B0#kUpbE3%5?6nl?7HulZ24~?bAjvZT+KPtab+_hSnm;FK<@^yup zswv<1x?*?_v;WH;I9ll&8{2Af>a+T|D`0cEiv@F_Npn-lLeSzH7L{V5doL=)Libu6 z6AN8jl#7KfEXuY+GxlUCmnyh*{zTInVQT!;&dPTwlVYLoR3^ki-=U0)g`TK9X*)Dj zteDfuVlG^AcMDCMNzZ%7nYe7?YO&CdPJBiz^wNn>i-le?ag|u;M<%Y^4$YZ^xq8-@ z(1&OSoEilQkY`?+eXCgLi?hdyg}yLLh=u-V78eWs_pDYd^!ZtA9UAQD3*|tO3WLO> zz7oUFjXrbL%)?@#SI+!aEcA+*--v~NYUbBsp`V=jm00K}W_~Fa`tg~EwnKL+1s6|T zAr^Yk#HY4HT{*MCn`_hM5V(Uj!eJLrwv^vf-YypUUFB_Jq2E#dP%QKo<*j0&H!FW2 z7W!@F_r*fLrTm^)=r@($6$`yd`5m#)8Dh~BkDvMD%!g)(=|`sDKaEcPdg{!nmB|MtS0|62`02zc z6NiuAGu|HGqP#=NC=XWrKtU_^8@pvJH@3I@CV5gm2T1S!f8^R5lw-cM{0&=e7Ru^~ zBm>P(kMfqx))IfUU#bL+n8DUY%e79Xj%-Ad%jahPS z?>^bR#gI*wRkk>xYkGr$L?LQ-=qQk`x~wHkszSiwF=EL^j?^YIF}I6c+pAA@FA-$j zYHy)JS6Uv*8nDzVM#!ITG8L=K)v~ADM9hZkV?MJrju(nWa&56sc2NXb!r|A|U4>TK zUyIO|O1X;Ew02L;=r#v>R*+$??QXOq-A*L!s*`IAeX~kW>O7PSmfpZ5-_8%h1 zO7PPlfolg#_U|IdO7PMkfnx_t_HQD{O7PJjfm;Vl_E`~RC3t9%z^Q{J``4Yj3`>cl zK?0W!mh3Yk$V%|eAb~>%OZG1!$V%|dAb~pvOZLwq$V%|cAb~RnOZHD9$V%|bAb~3f zOZI6IWF>fIkie0HCHs^Zvf_O*$kjrh?2{tMO7O@affEPI?Gqx%O7O=ZfeQyq_Hhwp zC3s_yz=4A$`$rLECHP{Hzm_WyUv=IT?AjfdrT?*7aA9*X~+9yn`e zHP_DvppZ++4Yu>?#4-AZ`=R4s%?F?!W@xN_eTF@jAm|b-s@i2H@9l+)HW!^D-4(lo zub>uV@~_qKE=7Ece^CZi>wzy}j$IYJ8%-P`RErWAOK?NGst%ojeICSsLXK`J>IlPS zR}zD3O^TqXlDX&2(VkA%?x09$6V}GXhSRWO^6Pq8D^&_<$apw`6K`t=zIS;GGK`j$qBD~-F+%8`{u4KX|=fK^MDqULxef#jVSndxR* zek`F++mK==o6AtORHCfG<13c(u4g$8>MYbss+*uaWeW-y5;}XZsLQsz`sGZ>W=nP( zk%A4f8x4VuK47$UAtuv`;-t$MWR@)kPetR)r+oQFS5Kx4=B}gF3dgCeL4?8a_NKGa zIK7(bOC!jYBVkT$RvL$y8>``stc_puDFGxLD__^r$aTblF|d>ECZ9NVM#AV&N1SY_ z)@&Bwe6X%b$AQ8rF*STuWwl7hqa8t3rUhGPK4r`^5p=~`7;yPll z8biw~20Y^~rQ?ts8ZEmMc}d>t3}M_Wp_Bc zQZjlv+c4lmW}#Q z9kx}~D{f%*?`J$lDp@Qd26wlHdkK4$40YmR16@w^ti7Jbr_oi?K7S)yNPDu8c0~<0 zT5#TN4RjsZnuSSc+9>1i>e@SZPzCD{+jJm|gZuyOvN>kz(Q!(C`+w)Ix~G=i-2>ph zQCh!mNO)h|?4qjeUtER0jp_{@{$KkioVZ=(HU!G{b{@CZGcBUth^mUlZUlDQtUWVL zbs3=`48)jE}~Hgc?EI4eB6 zQ5s=<=!K&O$5Vc%MMKqkmJp^l=E}8l3a(Wg3=kTBTVeQqoK`pV zbfW~@Yds{cE=TjWoX9f{IO6m#95bthSENz=oZ7534$G7a@-BghA-(PN$~9d(pJPqjfpk9)o7NL&9N0zGWq- zlGa=61uAjpu1h1`Du zF{BIFlRB@~(>1!wMRzE#&Kmt>aXCe}DYqVO!wtQ-8wcC;!QPl(mqrP5YO~TfEH;&T zL(l)$yfjMCbH8q+v9G)kZk#)wRN>ti6^E5Zbzi(Uy#L3+@jvG?R!xnZMNR1I0bikM z%rxV6f6_V15YevK%1(0$i6?b&ADIcd z4E0XK=0$T6qOJ+8XwX70lS>ESpuO#n<4irI>w>gjT6CqV??rv>bVKCJINS8?_kV}% z2eQQ~nnecCyFz|^Bt*G`=>Wu1EC8f z8WSf>%#A-Ve${w&{J8Nc<2j;OI5ZG&7!;h5eS#BBLWBTxZJ=ls5uI_gUl z<#X^F!j{PgX>F_E)nXyEQm|{BEjUpz24S*UXj&5Crly$)sH!jQZg3| ztC9wP%vsR(noVs)r?*Gwpv#KH&BlVu8Ie!JYb$J-PCH>RMLj_{TK3rS2pZ6s8&0P` zQ%O41)n;Db$dH|Is|OhnNInIx9mAG!bt7a+A8I)Rk+K$Y$J&rFYp|y5VJMeqWPmS( zH(>L7qcyz)mrufLDz*&B)n>12azU$CjfaT1m$bE-o_f2aEI5I9kE%1xTc;7xix`sqh;$cDHk4vD;7s7>u8jkM50@fE8(@}ei>WF z?lZUQEfh^T9E69otMz!u->Nyfj!dEnB0CZ0Rys#EY7w~tUOSpCqe^PBbh8HHMp5P% zmN4KY6PnMclO@7r&?l=Z%&OAI4Q`Jr(UyEsGDaq2cL4R6%~`7^>7pkhdkJ1UoGs(W zX}!sXX!6Zul0=N!vMpb9sob@C%52Bfb#1klLBm8>R|sZhFT!g}Y#DPK1l)?Z!YHHl z>WiwLG3H7WSgYd>L`>O+hD@4Ec$lh2DyppP1$gZ+woJ@ptasZKNS3JaCTc~f={B07 zS{cOWX+w#8q1$a^*#sI)Lx!U4Kk(W%whYKtp{1=oKit-I)J2=Wl1J@?spzY;;+_tQ z`NO)ZEmSa6!uF)>-|*U2whY}vTh3%Oo%Tt}QOZG2#?NGLi zI!Oh*Y0?pJw$Qk`sCLGCI#(djGuoOdy(12E`bl@E+Qqb*itL~8+W)a-FcdADYED<7 zZfXXC^-9Bv7s;L{ukvW}F>|iq3}jMaQ?yo2m}LKe*WSdIfx)yOpllI5Fa+ujd)XQw zz0Pzf%T#P0mk*RQ>(!>9x*W5cWPgX(4q?l5iF#5;msHJowyJY`pDjyI`F6}%279SIxjkWTHXQeics)i&x( z3+q;7&%$d5v1M8{s*W%ORd%IubGG5MSh7UgV02q4U94_T#ezh`9yP@bB{wAdE4+3f zTc(uL6D1tR|f^SR~;nmF#L%-dqo^(L^y*Ws(L{ga8&fFoMAc@1dm3JQBU>h}kL0u}Z%?Z)c)Z@2Gpg%25FcdCv`kI}H-(KlL!<~YD46wi*;DY^ zK5Q9l9EHp=)P);86%XmxL%^Y`XLexDv?XZLcyirxupVv_9zBH1o`l!lz?Nww0*Jwd zA(>jqlFXRnrj#n0^<>jg3gl)8nH^rAIcD?e0v?x6_5{4PH(N%6N1N<%cx|tK8S(CF zvOmIWi)Hjk`z^dS$(E7eb0zx?yf(p>k>F4z`!&2a z&X$qjEhYOEyryK!NN|Oc{SsbNuw^9pImsS^*T&c~5}cT155jA5wu}T1CD{Y;nv5+Y z!Tm_~3wZT3wu}VdBH7R3)pxUHBsdPq?uS=TWy?tL3X=TB9jst}| zAhIhajw_jx4^LpOj#;;&>6*gKGFqfHIg2Ai(cM}nUvCqE64P@Mb*rvuboHD`Z@XxX zlnY4N+(|@jUeX5zec@0x9!+NuQv-z2RU5_WIM}8SbR3m+X_PRhHY<(8OdQeIr-`G4 z&HObdjY5F~lX;jLl569L>|Ps3LZca)gu@qVSJOD1ue!r6Lfat8P|)5@MRUvUF48k0 zq@SwCF)a<*1G}zq4Eu9BQ_bv&2eWDemPyf0Z?e%Tz)Ya)3gntZ$E~vlnFQi-!~?o4 zQKf07q_>qx*wCx11BMldDrLRljFTi$vY<(}EIG49q>h7a`al}X>(VG;PHj>e)%}Hc zBZFh&b*cZCa7=!EOCvYpIKsNl>^^H89LHFLf(s5LYGo&e7mJ3kM)NzHP`us|7IT^-G zCXE@m{&;oG41$J95+k*ZaNStdxLu(j5mD!CD>ZkiLRPXiTL<@(V)AeRWh>kCfi%+V z(kNk0ZB`olc^)>BM*MXtjS?=PuWxC@`cwTC+c@@Mx%;G1i$O#8#)c*nS|Ky;1kuK= z%YIFcr~|9jIvhsJsJaJyV{n7Bf;b}{3pg4Au6E_5y6gb?(~ZTH4d{o9z{J4gXVl$j z*j%#|ph`sJ0ctl_CmAaGaXk@blFdf2>tW(B&`J7h(PWgW%r zt;oHRZ90&~8Ts2}F%WEj@$SX1EMB(w9%Wk@Q(BZO%Ka5DEB>gsSMiO-lHwDJvlKeT zQ6K{U^JBjoyJPGtW0x)l$5zMkW4q$?r(EHoV#?cH23zoH_ZNF_8YVBn>}%MWmZ1()0t1tv}bHH z2T%WL`j+VrPba6f)6-MGociL_X;Z$b!zTYRdE4Zrll0`hfmvWVQ<5=)N$B4hX@2`NzPEcb!H*5Jm zgYSIzYx&-ai0s4Sss9&{T19Q_1aQP@r0We81Sdhxp+;H-sSpSzUVx9!FN7OvZwps5rU6@ za_}AJe}8=NUCaOe*x);VOl6M_zVpXa_Q>G7puGo56QMdw;3?pyZi{2zuwi z0d+y|{G$II7WB^jgYSag`RU-hpm%;e_%7(3dk5bIy>s{AyP$XO8hjV@&K-mAg5LSo z;Jcu`-vlyp&VNBpL{yxP;ZbOQm&iUe3N0AP4~#+! zM)HHB(1MXXcf?5Y8QD65vT%PpZxmWE?&pj`3&#EZqtJqJKYJ8fFz)Xgg%*taS)?APn*MyzVG4e+WCH z!2A(xj{@@tur&(IACu-NFn>(yqrm(zX^aB%Wu-O>%pVkHKgG(}3H;udeZBvg-_Nq^ z`k(ndD*I~xGr#X-*Y-d2r@8D){m=Z)lYOE8ncq#aYx~CYYj(ueDCyQ4twid06hb;Vg;X4cGFQgW*g{k?6=D#q1`n-RB zY3{kX+d-Cq(%kWLd(A#F`*q+M5S@L?%u6%(&0I0lm@&^BF#Y88&C?f5r#HCIUnaQE zzc_yHEBE=Ql;2i|dkM{Bf5(H)7oR;3G#+7MxRM{~U!DjQc-Ep#|gqw^3-pxc_w& zS}^W^8HE;%`=3Uk1>^qID70YQpBRM}jQbx)q4`58`@_*dmoel`j<;U!Gkl5&r2UBD zOu&R-1Nr?Zv|x(-ZWLNDMSeR9Etn#|9)%W6kzb8M3#Q0JqtJpW^1vvxV2b>F6k0Gv zel`j%m?A$JG4A}qdGiR$xM1Ax8-?bRmfbT7Eg1J7jY12?{mxNn!MNW(3N0A-+eV=U zRU)!Vq-{DT& ze~H?Eoiv3{?KS!M3+U7;~gdCc&tEpNoSKSQC%?dvSif-{asQ z0)bI)J4ZG7xbJdC1H!QcNLCT#pulK+hf7V1z=*RL(78A`2%5fyi^C8Yc9-)u!CbzX zi_;)5A9n*62O$t@=ujEKn(>VtaTq}$kUC>E7XjgNtXsom;KJGd$PK!Vg&43{BeB9S~IAF@{_XOR7wcx8<90n73Em7_i z`1HQQ#c5zd+e-H;0%^OJi$h@o^H}R4ffRk2i&MjdCg*mP1^xFWE)EDe%5)NyD4)MC za&a(BsGC&EE5Ln0fJ3_O5G9b0&vS9?%WBt`DGTJ|8V(L8U;+vjTdo$Lzt3@TIE)kV zj<+DdeU^*U!Z@Bv`Dnp>zFL6OW)nKbz_0f+TpUPzi1|7-S|Gol=HfIUL83pG45#_@ zuHxdk2LohH)gC_xlC&;8R>21mYEu<}TCW*ZWBU z4r(@S9Sa}#2@Xz6z*-_|H`Gjg+{d{%5G4t>27(nEANR2xS7%I%X{y~+nmf8+b-tX3 zLX8!Fj^@w;sLQx0jXI<^1-K&tP#@)Trh!4ODu+Mc<4^-D>7`s83TrjlCL@%zOSm{S ztVKN?OH;67e1wZbV68e_@6@gQdM_5>ke0WV;I;$scM%r{!&+G9i^c`*xR8s3K)fol zVXO*P@C!J-hY^UyS*>xo1HE@XhZ=^%7~V7_A`X5lKFq~wVG#8+7oY`ar4Mm&7>r>y zebw9L)BB(Rr?DD~xhfy`0WMAhV`w?zpatvld0ZR{V``hu8cy-)oy*0kVGODJ7?VJ5 z-_ON?!wp=rFo6)C-Z=ss6kzCvU>$uQ7Y75KRL9r03;OwN0S?b++g8DPaTXT`fm=Ym zWlRdzi!%i{tli1A1nc)19Gr%LH5%F!ObcfHdj&Yu+-syV{C;^47l*?db%QoU1oH)C z3VZbqgJ}Q`Ni-}~!TDyDi_^jyIB6q2f_?3DE)Ih=P_XXw3#8~YJ`N@9ZYt#Gx8vP| z`2RP{=J%Wa` zCJ#c0u>Z%kYI^l=F%Ukam1bg%IzSEHVIUOOKm?mEV8aN;8h0?DML|}=PSoj4)SPM4 zibV_CNU~y&gekISFVx#bW3wAJ?0R&6k1nXmr`r0E)8#Rdjxz#!=bsvn$r6k?i`ScJebsJ_ZmXMW)9bqY%e>RpPsba{J zqH3iLrdA2aW$Aw;M{IN)c4nU}S`dL0Ku~|4utj63bSB;`u|mm)^nPW9IeC4dD934b z-O#Xq-M)5kTHPo2>fdr8XitUd##;G$K9Miw8?2~;3SUJe*?5$BV3HY&m-2Cz_Z?}+ zTk%GmU05`I9tZsJm>jGEuQJ{*pNO-|K>t6B`DAhZznOF@o@86s|4Y6MvX-*V=@Va% z$GmYk=H0f|peQyplZeh?g%U2mX*uleFgm)C#M&Ws!I|$Y7a>1Pd28Be#sHanflcHt zu)@ZMu3e8#Qnk^7d)rw@<6lvNEt$ZFKQb@f7 z72+may=y5GxX+SYrnQVN*UTjeolewR!!{kPHDHHl)^~Ub61Usd8udoe(WR_4>;@{> zfu$1QtsQilPJk^GQ2y`lzJt2!xqKDKLVqu2`AFsK)l$5>UE;TG7&lg{Wsw6ZjWwAP zC|_=tO6+>FE>3JPa$$6HjyOp;H0^#IJ}-XhPie*Dtd#;%lcr-4LT zFu~|-zK&VrNwszHRtQSkijGJ+lW;Veu52CcB$$TFkSNg0&ay$A)UizmQg=pXwZ=-_ z`%aWgIiBt|sY8TPw}Bk7pLmini1_`#k&M19g7acMU+Et!QI?H9LlsHK-5mvjy?E?I zAlO=Vb*6)(EL+2dXIaz)+1r{GtnRb<;$?@0DeLTHBGYvj!ZmFm;V)E{OQ5ysYI4`( z|HFDx#~_xhAJUgRhDNrPjxV=Y98{~Aj^a&Hm+v(R(jN7h3a}$zpyC+^il-ymkTx0j zR0?T*GmywHr^Bum5m_$7YCGYCNWHkTEZcN8{y(@_#)0zu|IhmW9p6HCKMHVAT<>oQ-uNHm;&4Pw=v^K?_q`ar zp#DLC!&8l#P0#1=Q7%r4sI~cEDI&oAUVy`No_JpHHvR|~haqZBBA(L=G@-u};7}SI z83j7Ehq*YQ*H;G$hO*#|?Y9El#`^!?aB)NR|G(zqhU))+#l;QP|Nl~e+gShq5EnO8 z|NkHtH&p-s00%c*|NjdvZm9nM=R3ZnYlrFo@8?n*s{j8P7dKS@&$ZfshwA^iRvYk8 z{Xf@g19b8b{Xf@ggT-yE|L0n5z(e)_T&oRusQ#a8wJ}`(|0B+54AcML#pz{esQ#a8 zwE+#)|8uQ2prQJIuGI#x%Ne5o=UQ#BxQ+GyT&oResQ#a8wE;w9i2k2zwE+#)|8uQ2 zKxUL7`hSkq#t8lYcXu2Qj2No_=h$uZaU1LZId&TuF;xH0vD*N+VfufL-3A7>{vrB* zj@<@E4AuX0>^3lBsQ#a0w}BBu_5U2Z4WP^$rvK;IZD7Pu{Xfrc1K>8+|MToNhU)*h zb{oU>|6IEbe5n4PYqx<9)&FztHdx%o`hTw720m2(&$ZjYhwA^ib{qIm{Xf@k10SmY z=h|)HL-qe$yA6D({-0~N!QwX7|8wm&hU@>ib{pEE`v1@FxH=Ej|6k2RZLI(Q3>P&- z|IfA9&<@rAb1gQsL-qe$iw$rb9-{x}T5Ny~W{CcuYq7!NHrD@hEjF}6_5VDJjiLJg zk8^r&nEwA`oOOA){{M0=Zm9nMGA?eY{-0~LfdSmc`hTv~1~ydx&$ZgXhU)*hRvXw* z{Xf@g0~`y6=>NG^8!T>P{Xf@g0~@OUzku6+0JpLJpJ%r*RR7Pj+W@$Y_5WPEjp6!# zuH6QU+gSh4wcB9z`Wx&2xpo`F_5WPE4b4#fKi6(UGgSZ2wc7x=A^QLKvHJf5WJ%d# zY~foA`^=Z-zCCx~Y-i^FnYT=zKK0<#(UWIRJTh_o_(zq`D=Ebn74osD{PXfjP(b{@ z)h%n&i!yooD4$F=JtZF>KXGbm3D`0MTQN_p(JMl=ddTggskD`76k0lm*@QAQ$cyNq z(;eERiELM$^i>||`qoY^(W=>vnT{(^k9upZ65j}BN=~15-BV|=zfWs7elEpf-lZAv zIK|^}a>-Wiv?Ha0Hfl0aal5gb48v(d-jH(EERAxZ+=!#7I$5A_BB+zf<0Q-7V>J8w z$Glh`m)9m)9^d|IKTa%R`drsQbdZq4rgjA@E}gbj%akHnHLgWU{#JsCr6QJ;9lV_a z&2?XWEmSk@yh|roMjv@E`}^n>V%a@xZGvTYU>Ip~w$hC2nP2yEHl}mLW ztug|gdnS>atkdmWxM1OmKbJ`x!QO&LPX%oma8HKYB?lgIm_* zY#;8hY)&tQ!nTaj?uwc$aHpq8gs$iz66QuUM=^1izEv<6 zxMqaNnWS7Xx7UhOW9$#@Cq5)&ymM`8jJ=Ch7+16$?{Lv+ET>O25qBji65WE(=@w(|LflcJ0__&f_z_Rl ztE0&_n$!n!jGimJ@->s^dxf7;h=d7;iO7r(xH(xO8OT{eOE-=MB5t4GbWHQD{4lb2R!MT*L z3h%)^H2qA3*`IzsfXXmGxG^ovXY`&g!&FQ^)`*$6IWqQov>;Jrjx5%6(R|8oD(7|f zus7W4HhfgLl(yPp`$M{V`k9EeKmEM-oif(ggwwbrxEYESECbi%usMDHF~@%$Y7Qs8Dp+%T(IH+(m=LWMtT0eT zx0%J!bQq@$NF1qTQ=^BZiE#V2{yI#Cd)>9u!abwckl`L0k``wP+dVoYO@!Gu^H<+5 z!+g#EnHJ_#dvzJ6!pvugncah_F!PB(`)2;Ck_`0PtEUC}S zhC&YOwr<^e#a43b?5#&_{@doKHm~1oZZexs+xXMQ*Ec@6 zap}ed8|ICz^`EStTz}1aah+O!(%OHneQE7oYcE<8*Wk5P&G$98X|Bg1N^pIKhjA{|)_a`wH!T5AHh~4SG0h^GeDq8Q|)W@>tmjBPN4AC!~&q11OJe4F<$$j!Asx$dyUu zK_4}pp@x`hbw&H@D`p085ypiJff88hKDPB z?(jyg#b8DQ`H;7!*q;Htp*(&zm=GfxuNGTMIuInGJbpG95u?SQ47872d{G`hgVSe| z*(v!-M=sbZg8^hZQ^(uol-dI1DuV%xdc8}M_e9Sz`K5MqYo zxlToWg?7E8R9B;3-^Qy|<%JELZ6zHT8qiogBC2nlt+x)*A?DGiZOUEt(Wh<7UG~wZZOUEt(Wh<7 zUG~wZZOUEt(Wh<7UG~wZZOUEt(Wh;%I6O3rJZ*cqifZ_2n_`FEA`idAqn(ry4nU(u zL^~-X9AvuT(N4+;2RVz$@MtGxgv01;C&~y15TiCC+ley50pv~@k?lkg;a~y5P9w6N zC?XvCbi<>a6cG+`T*`=SCyEFM5Z?fda%i>_MT7&0Zy1&BL=oWt&dEn*JGt|)wv3E+ zQp7ia)4);LP89JC;M8VRwi88sgJpEIlOn#sGCJBx5#L}L9qpuuZ|Ktvk9JbTH-N+0 zQQ1xu@eP*I(M~rX9{DVzqn&P2^v0-YCq;CFWpuQYBD%pcI@;;*JcL+AM>`#!hXCEk zXeUKTayQB0_fw%pg!#!g$U|_zE3aRvw`~=@n(xHe+pK0W9wMVJ>7bP9Y zb_Qn)ZQ)4Xldmi3z+%%>D>wp2@&w$gqyv+ou}c)ZNAf^?`Jw!Ov+Bn~^bb}GD0FzV zN32}RkM@XF8!#h1Vx{ej_K3f%#0Ln6d&J6(n$aGyYNKYPN37ha8SN1(H)=+E#1G~F zdno^(a+NjOBUY}mMtj6}Dr{qP{J(OYHQFOquCqpa#L9KnXpdOA&Km6zE7n6)UYV@&AgI)|mKz z#Y$^T{J&zQ)u$Wo5i3?&=FuLpVx?ss?GYw{D1S8 zLmQpd`M%6>OM}uoC5C1BuDN{-wXjS)l8jl*Vj~?lgxWz9fhF0L5Rx+D1J8355|0b1RHxpe zF{_kt=WBU;!elVn9GJUa;sU%&Zxh7hc0#UT+l##rq%Rtr?CD0$pqO8d%b z{js7L~oqX)+_9$=oMBbxgl<~ahF z!AzqxJxVo>+ss1p7>t&Ye6$nb94(46GrkrRE9q?#onoyiA>n1E-87?ehRjI4Q3dHw z<=ly?UgxNS=lGCIW-I5dyHL8(F5+{w(98=>fZMOmU=G!GpENAeR zyf=jX|$UT58Lh3GB|7iVO1u~MCScd=FyWs9ccPx zo4uy2=~q2-Zk8zK$uXCz`DIk5T+2~SKYfRtZ}*2dy3}$TOvdOjB*?WoQZDGLHT_LDI5}a`xx5!~Ix{VUyKbr& zy5(ARN`sU?T{g&tUZZc2r_Ie0vVY19()>4RkXf74RQEUQNj66YcntQVp>jbi*;{&% z4@F8isY8(%WfzNKX_!GCy?pi8vCSbkz${TqM?lV5MJ{W3)|z0H@)tZ%Gc;kwk@KQi zq>D9A83{VOHMGbiY`E<>MvH=@SSm#_wb*f!)d1VwP9pZeb6MLQ(PXCPgJHMzm|c)c zwK!HrV!V^^lu<*dMi4ezs#4K+JFG2`K4#3A+Wtl>qK_YwT5+D?cz1(0dmM3_lQWl( z1zhxT!6)HhRFKEDNx7`Sr|;$eI|I4{+J3>-@3-okzux@NN`AArsoQwZMtbAv>u+C= zuJ1s1fY<%ryz=X{3pL-?h^t>)<(BWCJ9{|}-t0SX@f(Z5g?qsRVxOC*=I(hg7mmI8 zWNu|q{3Ym8|s*E@73z5;-PS);a>|!F_NYw+)Y|u`tUxV2c4=^}b zp(+~l$p-05#jFl%OPA=jDq&8jHS!_CVe_{GT+L&y)SOy-H?6BC>#2Oi8t)PqP8SQ7 zR;bp^(7FI_4w6+uAK|&AhjPbunr+e}NP%Fy+jMvM5Ytt^FtJG=VDRb?uH?ieFG+d~ zpfLhL7ZzYdmW#KGQcWVe9xRdc7~-XpR*(DYU0*aCO0(^>K;%tHu`AUXJzTCfB!AKr z4mb;qG>X;q-W{hq8stN4lH*#D5SWi#>VymPKR&>~C3rops}`f?iko)BQ7?}eD;!Gc z@+4<*dCftGj|)4pDKe!E#8UQH&0RFzX!AQ?cQ8pALg_O+o(Cft3l;@%TP|F)9zf(D0p>*-dC?M zu^`%|b-0g>OJO~(ZEz;E;E7etMt`$t#H|Ign6UV)cBz6i z)w(*6Xydd43?9MYi`nyuEN=85&8{xx@U>DoIzTuvZ?@wsGv{)7&fE^_ZIx7kN!LI&38E;F9e1%*3)Xhh_8M0Z#AvA<3f0+stu3kCs7jG+uIb8iGh9V6BODl*)x%3O5R9+^#qA zH99KhYv3NX!yfaw$Y#}7aCY%zJ4x=iBkmY!r`+XiJTK)X+OAIK`S^ooaAo+UH?Iq` zd0h_nRI8?PF99T?cfj3JYDcQ`qBra3QhYtyu6Bd0`bx;k>keU%vPe1A>g1h*E*cg?wrn<9 z3dU(iJWe$v0%^f1#7{<1Q%#H7f)%>nEaQ2R&Zc8o6wE#Oazvj^B$Ew?pQs0LHzImG zl&7@guX?;?!bAB;+UFtNt*H9a(aN(9VIWL)TQ(Kb)omRIMuZCiLp{$^4sU~i)4E`; zSm&!a7`hsyR!TG)W``Z@NSJf^TGi~bb+~vb&2YMU%ofWNLLi&zI$1H|Gw!6RRwfmf zLb;a2_*;p*Px%HD46l&~7@P%5JzsE0CZ~vGqggV?X9_WYi@-!*rs=HF65_E(6As$a z(xzzKRjC(}yr-47789LTs}aB~VivU*#IT#~#9eLz_tYziB)r3TQ;}x1kg;(=KEh{f zwT?<97oY>Il(dC}?AishvQg8c8)W_EM;nVC_^lubwTIpLT!& z&RGb8Df=RMH{U^B<|IK|om`SBh)y?YqAa==U-8y#mV8j_$e5&7KFbvwE*!O?q*RY( zMaIZ8nTnsr{P{`^$>*pFoCmW-*4H2s?JU)G5-c5U5iYN)9X9?hyln9HC(Xm#5#W}T z$+Y*xIqOghCh2uhN=1S;e`@rc!KbZT3nxVI5y1Pfv9ChT0ho-es=BIAmC4W6hDJjmA( zx>QxC&m@dHT`JBbT6yq>V8&ww^IBE?V(9$M2N(#9tp{13Kdy77<31OnkI>y%E86B_ zB33IV!{E8C9PJ4>fWF0eu)!OQ@U!*!I*f$X04@JV>*b(vm)t^Mcu`q7xCg6LoMnTd&yK49rbQ0l2Jct7V^$NbzhO|p5o+K!s>z2` z@cKaoN2s;P;SGgR2(BAcaD-Zm7^gsRXc{qQ8-kFsS4mSf7SlD0W{}JPxJ(r^tp~nq zv*yScMVlvp79}nCKPChXXq|?m1kvPUo+{m}#;w^{SrQATbgm?bY=RY~kY^{GtfhH7 zL$q_v3fHFEf;v?+^p3$t96_u_3{!l>YX=n^q1GZtbYnySd4H-cd3VHMlxT27#uE@-A)WjT=cOPJAw}~+5 zJV)6eW#dwn55$-#5iG-D7a#TGZes~47d?8m#FVsMv1PA&jFk?m^Vj)uv+ZdMqTkbD zbm5@gny}_Q^-?m|b$Z#@jzdgV?XjlI7WIMt^}2a=ZuiwE$oc=C1bqV9zG&+YTiwmy zZ#Fl6w^3dH^?Gsbmup$g{hH+Jk5{8B-(R_4`P<8(rEe_pi(gyxEqr-_ng7DPYwk01 z6kz@<|6JT_EXi8|+NS8?ia~?V)^Q0Lg0z|aHAX7b7(G>>;-08LM)+p9P0)q_s!gS( zn9l6gF^mzbg5Ar8tLFD$o>nd8DTF=XZ~BtQp0T-2cJEFgpP<!wS+2%xq=XgX0dPUO;!0$Eo@o%&b`i`_ zt2Sbl(iR=pwOF$($A#S?urx)yup`+d-3UsG1a6aZApaC)txbcVx{JUOF~U^X=s^j# z#0Gp;xYL%p6m51V>_O7RraQ%8O>AJS6-Kr7s2{O;%t*45x5tuHgpBJ#DK?b|@gV1w zK+&9+3Ol{HpY`B7PCM_QrI^3b?($roa^%(b%P;O#b`i`_tF{y*%r@dABQ2 zA~_*y36#Jp+)oD3Fp`gi!$c!T5UFVply?!#P^-2SE9#sP3~z8jKiH9R(Kw$jP(=$y z>au>CCCssUI$((x1nf&RswIcV({#ldEbK(B zVK3!~pxIn*8U)2%1T)mC#b_U1^JJR|Uqvd|OT{u$vAEa@!BGCLBVG%%n0iYBdX(_g zra@5HMKD9HTBD=Wc6-V+nPy{AL%x7grLNH^CMwyQG2m@=6QOw7Vqu!7J~$16{4RnS zYSp5oFHCmyPJJg?^|rXI+0Hujrc9#h>N*Tn4k=j$A6E5J-B4^A1i4)VGt{b$M5!jr z>RBX3n;4T2;qolot#~9;GnLGSJVtZYh=wh`X2&~~Eo65Q%uuVgm8~HH<@TmIB7`Ia zhuM|U;YqYa1)~kZQwfVTBhf7I;CM=!rVTQ?2skp8aW+k{2$u7dbyzqYE3i~9F}X%HlK5zLT%c9s;d(;$fNBA6ku>@100r$G?gMKD9U*jX~RPJ{07r_kqO|>)R z8J!%Qc5(0gT?8}Ks+}Qq=rjn<+eI)#t=bu~f=+|r++74S)T*5!*XJ|{Lc0iNs8xGP zLxV5wNxKMUsCAZ@;Az@G+(j@$V$D+` zL&M~5R>cU8h(R|EeQS3H>*&Y)%@D@hr=LOhhsOxcFoZp@kfaAEs(0#;q$%PA|ALSt zOph5*5WO=rC_R84Yd7gc#Yj1&awJ+v=*#6?C&8qUB#rug9Rt`sC^f{K)k8ku$9HSx z4w2~?>%O!Lk68TnMAMwgd z4I<)d>vIhu)WG6_u0Pu-GB)c}AxZsD2O&w|D#-I?GrY1kcWP(dDa~-ma1d@8E7Y? zMmLXCG8M${D%I=VdeBM6BQ;0N=5NAcBIb+NJMaUxIyx$`NZTygUEK&-Hg$%&f~^;qY!fQ;-tge7=D@k#-382TXB1UV@{gY*lL5 zrg{jt|LJl(?n@}_hND}7`&D?$`oy#c!a7DbRhwQBb`jP>$Frwk8n&( zTfK1G;IkNvk(Qeg(#6~ZHpQ+rYU0pLxs&P!MA8M$`aDgxhR5r?Rm^cUbHVFCOlb$2 zWX(v^>q}xF94Jo55Xs*rec-5q!@4YvA--c3j4P(VOGmDDXS&e%{-*;|ygc3O$fkI8 zcy5+?=al)O@Gw(+(EZ9;9aEKQ9Zo<0*RiWcEm7UQBQ+uy`{4uozLlN%6g>?0W4C42 za6FNWH1c&|n)2L~XeNsF-TL;@!WScj1o)##{_BvW``}NYx`V$B^>7sw>fuOP_wJ3( zzCan{E*=%gT0K%v9Qs`!TQpMKhndU7a{K=VvHp>m{B?bzOtA?b3kRis1ME-x1|aBv zZ>&Kv%p{LyL!Iqt+abgaZh>N3_9hL2R!e*{N|ZdPA8Cqpsw?rW6o@iO$72tC+K-we zCMwW{%dudIZ5t#<$wW{oI&SWkk*Jl5$6BtGAz_LM)o36aVH_dWEh0!kLLF(U$?H=H zc?>24$Fg;QAl=Ej{CP`&nf3-K_;fJV$m{Rmno6q0)pN-D?@7G%( zn}0*|J53Gb?ys!;dZo1dZ_9hsC&<_Fo=%0%&F)o6` z`B`@KPwlSnMVA<61~9`W`Q&Zqi!MR>0A|_ypJ9)D8UXs40nG3e+B5**nE{;QE;!x? zFhixM_VDAPOa6Xl0H?SMbbSCbDAuVq!)Nya%yRZJ!~NCCCm$DGa%^S*Gd$HY4S;9O z3}A+5ccuaG%szlw4xdhO7d&HT0H?SMo<1{xQ``kl>jRkO^z9UP!Bb}jaEiM?J2QY& z+y!U!0nBpzIKx_G@+$12OHQ8|zznOeX#niZ3}A+hj%ffqr4L|+LzYu}@^{fCPo5dT zDei(N%?#iacfk|;0A@J7In`b8gqZ=H;x2gn%m7Yt7d);HV1}LhQ{4rRof*I>?t;h6 z4B!-Z!K3>CW~lU3cfq4(25?HdU~Q=Y*`XVt`5({Uw)o2VtL879H_orleS7Z1i^;i{ z%!zZ)Uc74#g1)wN+TzcaaOj4`=d55Wo6A2~zGeA}<@1;Iiw|4)*M&P4u3m^QJZu45 zUfllp_KUY|+lyPD+`4qjzO}mf+083A-J6>mU)s2OgWGuc`d8MkT@S23YVGT5uLbV} zK2~#|<_((jG*4Xp?&_OYFI+u+R`jEnTg5o zzTeF}2Kv-Kt$G(*MSJoaFNaR<(;mLpB=5zkX+QQB=+1rG!?%Bi(O$6%-L_AA_@d7+ z+W6(rE&H^G@AM3#Wj+Soyhp3J>@$p3{}SlNecHo!=Z4Wfj)Fe4PkZ>9+z{Hk{)2=* zuupsVM%*ykuYDPM-#+bxzPRgszl7eiPdlM6?t1B;p?B}oPUwp}kB6Yw?$b`_i#v}+ zpw~QO>G%%NGj)^sIVl)$SV>xb*Zah~5$ILVS~@N(q?%BrHz(eC9`wpR(g{s_;<65O z^&aVjmOUXo4|@3?>4b(ofxZ>Ga*uREyPi1xQs|03(!;m(hIH*6fBYnL*&gYH9=PMX zw?Hr5Bc0FzcYNl3&?WnwH37k0+;gFK?$Ii4APzx$@_o;Q?%1cD&#sC-EYO_ zkL`W0VM5#7bHl$v|GrNBnji^yPip2|DsI!&9Iy?bA+B)4PB6 zQRoZ%v=j97?l-&~`rJP4ghBi6$|s@EoV|4Z&eGiQQy{%T{l%Qki`%>$?ZrWx-){J3 zWjwq3!(1F2q^+dK7m(SX_QUdccJ;UVS=Qm=oetI~vp>vHI&10N(csm>@qot>;C)`d z7jWPBvLCOH6@Fwk=P0`!54gcV3)pY_?JdRe>}s0};H1;R@gcV)v;VWEFrHmK6cB`y z=Oo@Abjs}aH1gxw)fT{j6kH)mVnj-2zxfNf@$71&pj|e1(ChaH<)*vsx>$BRyIN~( zZdRbZ9zX7q+4KLE8PBdZ9XrlQR?_9>`qJ~;>GAC9ey3^CPx){w(eJqM!;<6Kk0_l; zS{?o%<@3nk!>6Ujv#V{6ksRKjhr;cCnH{?<@w}yTpE}au)WY+nRHk^~Lf&}EcQj*l zUd?Z}^PC{L+>~suH@@U~Yh%G5@!V`29b}!X$L5y7Kk3Ku@$71yC+&8Jz}O_-F0-$E zDmI>7t@Ai52vz~N`DwuZ(f_$FI-dQA&a))QL*^iYA({PtYh*mTTIWU1E3u3ZC#^F3 zov91QvmeoUkJD+ld;Ly_%>Hg&cs#pW=Lyp3vHD1-R~{*E{m#k-}svC z``oz4#qqps?zcYk+TeJ0^!DD>j@P zn)T1FU$O37UtjzD+Ldec+PdcRnkzN5W_|VZt5>eltLrPDU%7IHURhuM{PLB{^z!=B z=a;TrqLrwEK3#`*)>X9gjGeLq62{w7|KKYsFLU)bTXrp>Cpcuvn5+m_$*=f7| zXBTwGv#SRKnzL~{Nw_3=B)k2)E1mJ|>cJpH`CV3*gQsPW^Y)XMw#T!p2Lr-6nzX*MFJiprYvHIPt4fq~Dd1ZYv z{dwrt@%(DV^#*M`Yx4_|+<7OxcS9c;&#%^8j&g85&g$po^}Q=ps1^Q%>t6*;it z@CR(N%YO1nFN8iko?orIwopj4;_iS;cD_z7JQ@1ncz(6=dYxp5WN@04`|qyr{}_7z zSbkJJ?)U_kE#UNk#jebM!UVl{Jil819A2KIXupq@UDvy=ori81&#%@$E)a6sfqY8# zLhcGb40_jiezpF2Y=l4tSkA%7{K!+GcZ}y(>mSehf<7xq=q9h&?s{Sndi!{Owf^yf z7i>}l?R>xemwpPpZ9M-G{bM-b#NkZe6S?!ZcId6+`PKRtU>R?~?j~J4z<=lW{s6sY zJil81NDNL)VYzSL=_@?e#f*q|Yty zv)p<0pP)C5=U3~GgXMirkf6sU&wF>ibOU!8<<=U3~GDEYm(JrMNv`5o_vt{cy<)*pul$DNX&4#+d_oq8+u zy7Bz#@xvc<3r>~+`H=ztiQhaO`iHUWFLm>=ArE7td{jU--y2`~sjacn&1&uOP_&N@ z2tG>g{S)`x0=;@Xzgm0Tl!upGbV!g}@5G1S16@0wU#&ehumZDsL~B5{-V@h;9J*#a zzqfX0=Hk)L3yM6NLdxHEkbk%r%b?-X`7Y9<#ae_Si zo_P8-(96d2t49ycW*1zd-P_-*JF#{XboqGxBcq2DVg$lP)AHzh$NletE*;OW);^z~ zbohdPDk#tXcYJXHddXPrQ#U{D7M+fuA7}fk|EoW^Iaaz!tv#ZjrmR-Vfy-m(9q<1% z^y2aSYUQV0f1%-y9k!CGWJ zy3i>H~3#HdpDySzuIVTEUdq2eS7WgYevoIK+gXktrl1QymB2_&%b#YUi#Eh zVDWp4-i2>3q!#ApYx65}ubcaOP|1V*Q|gtM=e^I{OhCV$oSts)g3QrNFxq_Disdn> z?m>&R$6k@(5=b;%tyFxKFcs6&sdNbm2J+QpwH8eENZCTGNIi1+$U0 z>2R70yF}XF(R2E|Gt_NbOGHb`=Mk%?;7JDBXepKN6@erSNcy*_C8@6O%#sc<(<=aR z1|aTNQ;R!%!)lgvioLa79teB-xe4f(Qw!UDj&As-0;)EWSyC&;dN~;k`0`&&4JKAi zfNM^(q))8&vOvl+pP7JuJ~?mMo~rIWV04hwRX*C*&X8TP+RN+-{n=kAbcTeApm)=I zy?g)EQpfhLdi0+qWn;CM+G~QJP7P*wyU&oNvD8Zfq4K!&lc|Lc8JA|rpP1?;fJAv* z`q!x?9vYWs$dj1w#epPwT>A0UlGNkU3`r3)y%-QDk4rzATAX5Bnj!rm(A+2xCXY)$ zoLbme%~d-dvm{uI^&&DD@Z~?48qD6fG(!@_eDA`&?)?7bYg~KdlDa!*$gWuJg@HJE z82sMU;)a{!3<(vhy$kk)e)lgFIztY{Qtx~qR2~DqGqunmW55hK6H~qOfJAu=`1aHi z4~+pcG2mNMOHwP%43{A@y$}#5j{)~hElx28%y8=ui~$l5CXWH%oLbn} zF+ja~nB`t!tS8D~z?XkxYA}0azzmlMt33fok&W_SCf^|&>zmAQx3Ajc_k@1^FBCe% zZMJGJxF__V|3aZN+~3Og0zjy2Enk~_GiJmP8?dEg(Pldp>~O&jcp%4cMq}8zr0T+t3ph9uI9fHow1F-uUCjbsL(scdnhK`JyJX`pwnS z>iqWWwx6+e%gWU&$Clr)tOqat{c^Fh^ohmO7GApW`1Log8|U9Q|M>o{$e6~ffZ zg0cRIdW|BFS`jd)132KzXIM&14QJOwDT5I;0lW_A)Mi{C=bse7(3$|#26So@obQP| zI>97*43Kvm$eY2rp4!nnm|n7AI!pi$1Jcg06q{O(YH~^S+(4LY6*IWiQwvjB1r0>W zRxv}?im647wF;p|(56UdOQVg_IOFY{%yJranNwPyym;eom) zsU87@$=Wl+mCLDxskFx?cNfr}Q+j)U&)Uan&yiSxUG1^TaKM+J()*ixaCWt)&^rgn zkO#O(%~;GgI~88YrAx)SUc!WEtzd=IQJdS$;%GXIQwAgsp1@2^tv$mAxaR?R@&Gr( zyW~^LI~d?ny|aNFS!+&dCWyVZJFGRpXL$Kv>NlJ+{w!Fo?po#yJEqf5!*YOTCKkzu z`|B~_gPaAV$@X$e6OK${FY4%oU3+=13E(X(LvRvxjh*}m$`-B znLwm$qMVD+d6`1BY|Nq+#vyv`6*u8Tz>}Q$P%aQQ@lKN$b&~~0xHh%OVJ3dP~8r`AzY zB`cN_(bnqVw6_p0RpW_jrkDa5DiM=qSccRY<|a8I=`7<~pMHj6d3dJ&8FosCr3l}% z3w1J8Tu4Uw3?3>+s?KbIi0}c`a40j>5=GE_pzTf{euQsPPXRJh40tXe=PBTN{^|R| z&wR?cr=M|N12OB>F;OzdK?Y-W$kXZP%Q;K;(~4r?-xJb`Dv#ae%1hM@xF{%p!>GT7 zLE6@4q8jd}Y1Ny+g$E;S#LTdMWNz_%sNJH-4ApM8ij|Nh+swBxQ$BY*QAxrc(QYZG z>bQ$ec94MQ0Vgzebij*o$C`Ctq*(15z4!SpF*4gCs(tBM_vu- zTqpoLy+kyH#k#pFb&Pd68t!ON5^6Qd?9d>O;-96qYn~QM7#8WObk4GG}=5ZsO4J7c6i4mAA*{~)3sSK%?B-|HB z3syIqci_{R^xmfrG3i;fO*ZL5@1p%VdsN`!dO3fh?vybtWn4J(43G%=uA&R3Ayvsd&yMHluFNT5c4QCS%4R zQ1v{8OC~*sxw%v&XbA>6$CQr}?0xzWgP%ngWrKfs?}f6#U!|YZp3-%jY7R$0M-6k@ zjl&FnhHXgM2b^W|u1`P1-s5nmeTL=c3yNq`TqYC<{u4h)t60+@7etIW`5)TjrjWa z*Uww~##%u0WsQ6FGfOwFk}DryIcxd$CEfN5wtm0()P*06}lvhhMa-2KJM1hkw{5P z;pq~?*Ys3K02lP|T(Xo1R!qFt7B)MqrI;scGwE77dj>X2J47O%wBq)rtr`$hOuNAF zs+|i>{+wllK`%GM;r$6KqJ62GCJraB_Q5T7T0%z zl7$a40Yf}ju@iZ)nzI6)ZyvQWOJ)$J^Y(=O8B zZ{^yth!DXn7D8|EYB4j4iv^sjwd`G!CnV??x?rH&BwVt$auv55t#yGX7pn!Fr5(!2 zHk#eCCtV_ajUo@0XDSQ}FCJh(8s)Y>lPVA%bK2t2lgW~3(N&Ow7YV_>h{s2HQEyTt zj8+)e(mA8OQPDMZ9%FzKC2KBWqzXhsCz||x3NN``X}nY@b5R)t~X<^v3Th;i!Siq4Dp>-t337c-l6b`x1ZQY}-|5H~SxU8pXm4LVrct+g>* z2Fy|(cM0+MY;YOj7!Kr62vseeaLG-V!5TRLqdEfKks|Jfw_9-0eow$pk(I0(!{(C* z7z{eUzvzp^z$t^P#^Y4iV`91{i@RQr0-q@dC>~~{hBf5TRjFpz;DWV6t6K;b1&iHPA{;4KHRdprA(SH{ zd`&0@z2!uz?&#!927~0>X^WDl9jT?AAw0+i8g|^J#?U#00ShsCgVC8U=Gz6m-R&>g zoTgx-Es0V)XLn$EgQ@K?upKz36|DMFoiWfjZY*I=9TTafLxMpEGBQ}4Vm8>eWnvb6 zsFqFGOFJQnZf48*kgwwun%$hQ<56K)d|Yp?KU>Uck){jl#+sF^iG)MGVls^I(Wc(p zrS-0CCF?Lc;|3()i@7?6mNt+G5=GQwi<2bA2mzhc6gZ9$F_yJ-g5dc_)>kyT;~tg~ zgFD_(nyE7778?!OU7>Oux2rI$95h2+aOQ=oF`k4S`jjs0Xog&=PRZ>-Egmz)3SI-L z%UFF>oDXWtVSgdq!P>46(@6MHTBy1_$yy+V_;p;Y6^|!k=~@tR#RE>~j?ljUDYOi!P9|+HxzV^Tg&NTYpGl(;$tmrSxYd`9OAV<}@TA@TmNTNl zuyFYShEjp(qylxbE$9t*1Mz?^P1&r7O^nqz0_kMJ5nURXgc%9L;J{Z`G*BXE2$&n; za84wvjPrsVk@2^*hBh0mG+VWFqhmn4tk^B^)d0eo z0@Z2?#;ss5NEh`qf~MWMop`q8PM2LliKS~PZ@rr9s4%SDcYvW#_F^1uEIRp?+hC5y z!F(VBr^Ofyo9KcO$@wifjl1P_pw@zx(*ArVD3*!=9T^Y79?EZw>MEH+xW>nVRIpqK z>eD)1i1O@Mb6p?nXcKsnc6Oz5fm1sV^G`p7L8_tMyvf-O1mZ!xtyGW#PNd7*I(m%8 zVvI?&3KB|}JYkPEkI{j4OxJDeveA0JUdcA-guq03gv34Nh{4Ns6Va3bwN#1VPNiL8 zBsyMCHCb;wT}aiks&<&a>;OZ)h6cUauEW)?bFjD5L2QX8oUUaGv2KQN`B^-g;@l)x zOMA4UK(?I@Uxq4%1BAzsWb>TKV#-uQd^g`9Ye8=mc7m~ytckImT(*(0(T;)?krGLg zEPEvthUJGpAPnw!G3v>uyEb<#R0<@We8sJAR7FeBXy``V<`UT5tmp7X6fW1M!0@mG z45?(&*%feI+GH0CoS3P*LRl#pDF~fZHOPc=FcH=l8wC+BX-x?)<4ZQYSVU->Be^sZ zL~JRp4m(@%hJYAp!5M5=qO2a{ayuZKi6_7Z5;m#g3AJ*Lrl4wv#nJ(Whz|I@sl2~n zh?jI|*2OeJ8Gk*&5>9KDB!zY-QL*3xOqxk;%*|E!1Veku?v_zkv$-n;DbsZ}U?x%Y zX44fN+BHSJWklCV?T{TZ5(trbHWv^2=qM?cR2b$Sd4M6Ts|x;1qC&ZiHJ?3<7A=XU zzvwJiD><(%gi9$YTyUc?og<|!m<+TB;mQeXHP!HVP^T4cIK738fpl1%9)|&rRXM$% z3gwFUPQ!1l1QOi{74sDHrB;nhsxU0M4lwYR7H91=`EnNcL=I!5VXWkuv>lF`;8M7m z&SPOVQYghMlt&9vJp@xs++O!)k)qwvM3doe-T=3gIXDq-*-29<9ClUw6?3t3;o@f;sD8f_sdE4pn(Lj`vVjaa>4Mw@(_#9BMaSe%h4f$Fpi@iL9)iZN9?EI8!# zzaRP>w0-aP+qP>UQsM>(@)`=d3?`?T2eOuf1d~xb`&7e`!9WxlWVSJV&#!`kmDeu3o&#t?sP+X5}AO zUcHi7F|VvFe`EPQ%UzHQ@Ci%5S~|IO_0ok)`lb2BuP(l0v9U-kJ{r6!c>BW37S3IG z_WU2`|7rd$^Obok$P@VExm)HgofGDs0sS|C`S*+7$Ifeak5&x^i^Txz*=mH8=Rw7v zjNng*Io%0V)Fx0%M-Y>F<0~q1vjH(RizKIZH228wRg#-g17alH>4;U;pnK$>Qj#N> z0YO5^7^80gJ@U7x$U*J;Ts}}ydwYB2Z$3&6yNC=+3Gl8Fu{f1`rjfFTCGCEdt2s>8`IR*~xsdhnQa#5$tgFH(}Ds6mfNM9wt=dq=m( z9?}mK)gxlMl@%3O5NxjY!(2G|l!Hwd*CvWLB{B1a6cSxAHVYk2K76*+8xjj>3g zs9qB9)m~MSTQCD`h}u#?^`6cic|}Ez8epVATkRwCx2z&RZ1f;}FN3LY;$wL5xrVe)owyCEE(;HBW$mrA~zx!8gzQ7)bNJPtH=!qX7RJxs(Mn~!=6)- zBM4^h;2CRWcvhjMQ%nc7OF;bBSv7~QF3!7AO@-7E#p;@n-Gf$WU}Q( zbQ7l{HzF1=q(-wNtcg{T>k*5=T@#rRD#j?t^%#m+5W;@+J&)a%Sy%cedJJHP?Xi-3 zxPP+#kE+^Dh#5=AY~A65-ugW%awB2}t6wG&8rqQSA6JnB$ITMz5FPabY7hIzROAR^ zHrvEXLcR9gBfncku1CzKSj8C_J{_!|RFVU0G8<$EQ@zu&SNl~ea)2F4`#S6h@+(#3 z0K49w1N%zDTjpg)$>DgeQyI}sSE$JKhzZLTquCL)U#=pD5fd7VN5tXgv3}WM-wG(N zQLk^~)v7YubKkep{M)k^Jp)e+P#Z8utKCsH`hfVCs;Yrwm9=?Qo8JTKUmeB`sz!i6 z@05I{;U1^v7e~oW9dDPzhqk%q=PGj0h(>~TO2a1_&CgWipb-uBoRAvspla?{k^^_y zghYi@diZdv`KgK=MNIk(*s~tqqne+n$SsHo&KBCj@HtiUugX3$n9)E!%WzgzB zR@H4njA*=CY$?h2y)ey>ROH}n$>Im8&3ua52jo9gk%Q%xIVSO$;ggu=2S>?GwSprs ze1y?_Uqy}}Mq`&Kc!&Gen(wK|^@!0RHQ1bOIQH+V$YI2YBqXXmLIu8~BnMuSQ6CKY zGb0r2+egXah}GBh4+rxt6*-C+Fsp~P7KW4GC&&Mv16>Yne{%a}+pg{Ht*>ppdMmK? z*v;>5UcY(4<{2Ow;D(LN#^101cKzn{_WD_Ce_T7ccFCG;ZB_GW%~cw&=HaVfS$*xQ zu=@Cw@2y2^!%RgCu=W=HG8B4!hdf!rM$*}mF#hVtJi)Su$7w%g4)56*Fx6S`y z{sr^TnfvqH$+;KLQFBY+VXVusosB75&f^&Lj z?tj53tv&K=%R%$a&p+IB#@W5+?DoKz4}eE@|NQd5W4-52#IJ6}ef(Gto5-sce!z=P z;yv>1`@CoMEPEfV8QE@!12L4nw!oj=Gwx`P zweWr3vwMb#z|{kV3OF)RxVnM^;rhOC&6rkG+v%kL+t1CDVete>EwVECl z{sOG`_Y;MyE7%u~^>h-t5=k}h~ z7d^VoF}0E(h+YuS>OFN5afk^mBVpn$*{7 z?LH7K_w^GeAy+H-0sG3Mt!VEFlaQ;$?;}ThkDtV<7QN4UR_}3r)T<-=`p9D^2dI}H zVMKe6ogf+=HGQGI$4p|yRQ|?3>gV8CP`vCRAC(r6VYC>0`YO^|E?|0Q{ zlaQxCUv!X&OG2>eavlW@8Of6 ztNHh#qrHbsVpT)mXFaR8J?X2}27Q3K|J7TQzFIw5AMmf7mPdP=lc1~R??Xp>8a9;gt+wa`)S4#%vs{}bTFt*N8trK&v5p#tknK1zV*Ycf8P4c)`_hTZoL)k6>dZ#{YI5nD@}zu)`?*g?2=^AnpN-F)xnn>Mf6ykxVvnb|yVlijp# znl_)Y`S{JP&AE-=ZTxiOzKt(#d~D<9jdyKaw{g|R#T(U)#D=&*Z=AiM-_UM6dSh+< zkL$l)|Izx_!JC73u77C#ZR`KAe);SQ1R{v%7Gpo0+zIXKvt5>bQaJ9G^1~CozDzbXU z>ffy{t^DW8PglON^4XO;R^GSr#+8?^T(nYJxp2k1VqGz;Xje{KSzi9H<@=Yvx%|21 z6U*;ke$(l|FGrSr%eH0X@>7=|wY;+Q`=y^P-3Q)AymRRTOV@*#h!-tYmZD4k zC1T06^t7c%FRd>A_u|hNzqR;<#k&?ixcKJ9YZiNp)y3FiV3AxjFFt+oF^igo|5^A2 zc!}YQ3nv#owD6XNS1!DGp|%iT2rf_ymW5|5Ja%Di{{PJXa{fE>Uz)#r{=@TcoqyH* zCG++9#5_N5pGW7PIsdr%^}V|bbNB51Ga~P6GM{^o>E0_zm z*PyQrif7*e9uEELpm>%I`U>=wLGd|1f$oLw9Td;J82U2wTj-yle;O1`QRqw1 zmj*?{`Op`kFAj?O??GRHzAz}lCFt|e=Lf~(8W5B8xj|7Efj$d;c2GR_8|X98X9mS* zNzkXEPY;UExCr_b^r=Dd=}&<^34L-p8+6;C`2YM9^ik-egW|6>AiK=1gW|95fo_3r z85DnNg+2m(WKjI!FQA*Dn+L`3p9$Rr-83kE=XU5u=*B_uTVCkH(1!=bZ*tIwpbrg- zU;ioeLFj{n;+Gld1JDNs#m_$$dO!63LGe@n1-%b?-=O%3S3vKD-a9CM{66SC(0c~O zJ6{6b0NpSs-T_1JhTc6W-ttQ5UC_G*#rNL~y%T!pp!n|JL+^mzF(|&{HPG9kw-1VM zdmi*Q=xu}ITh4~w3cYnueA5r0w?J!9lf#pm0h*Fmov6x+8!uZ3PaC^kP0 zy#{*Cpjf{E`UmJA2F21_p;tq%9u%`r0nh$kJ1C}(L9c>dH7Ldl&?})=4vNv&Lf1go z42t1bLsvsr4~kE|5_$#nib3&-4Uoa@<%8m5{|CAXx@u5-%vsQt(3OMYX}^MA2EA-h z-1;PR1$4!rxbb0dFmm~zsQDIj8Fbm8xbiFLQs~k_amf$86ng2P2>k+FW_-zh@uolH z&?V3%gW?~43cVP5@u2wMFw}#3gW`Y9K`(+{w6VA}HwOmun|^!m|I6NYz`0Re|7+Cl zPS8&ix62L=vd^0XizN8tC4~YB9ylSNC&3>M&gDpO&x6@43GVzPlOe%vmUNl~>t0KxNbtM2 zlSvZXbY3Dsf*an8$4PMAF0mL1t`0||B)I&;NQ4BJ?idb};5UB`g-CFbG#Dho`5O*8 zhy>@tI};>0i+3{ZU3VqHXPRAhA;E^{ zSFIw!NBi%*GYLMpZKs_`@cyc?F%rCc#*RCZ;H}2Wl_Yq5(TWu$c;(CG%SrIhb<38K z;3e)3JCNWXVChm4{PmsfwOO}w}k>y)&O@fDx zUA&kC5Bz=6A`;xkTey$}_dL1PRwTIVhy@EsaQhzf=ab;~)_L2%J}A+%?}N3_w8M_(O%cJzNnZymjI^xV*xRr6%qN+DVBXBUlzAreD0r_R#`G|COgVEm=61{>#&?Vl z82@BE#ki00JH};AN=^xTxraw);pMDGda{AfyW9W5y zg6^Xm=zG$4r!S?CK(B?5hO$Ek4q1oxgXa?~hv4)W{A}>e!M_atY4G;JYX;9BJZbPy zcs3Cjv<)f-1%o>dE*xZWKIgmz<0(AMxdWa{T);V*a~LPjIf!HDC^ibpsa;oHB6uKw%&_;22O1hz52VSUk|z|K-T)k>w-vN9e*m;T6LRh8gf&<&B}|haMWbZRoc{zaBaf4l?mG^^mbX zax`dyCJ93k&;Sh*2L2A}piaUAUIjH!BjMU}K^0U!k36w}^ zz7Z5bk%UGWD1ZV9bti&6$deEWf*i<^P(1{)AWOo%Qy>E}B$Vv}(jfgK+!>@mii9E| zNP;8@`R{=QNRV)kHi&~b33qRT7>JQ@*Ox&QL`k^gnIHlpBwP^%VGt(a4&Q(f2$68R zKY$!bNw200@w9tCPTi;6M`2KM5QF4j|#^K45>aKM4m+U@cfnLiPc` z5Bwx#{u=mzkA&bQ;04|(ca ze7*q;z(B&M0MG+H2_O4C&;cC@AK3wDftG}S{22NjAtZbt1T;WH!u#d}HBgiAu6uwA zs7QDl07{@F;kv&91yGRi=I_9MU_TPxxHs4r>`TIH&jb5_eMor4r(kceHwiCHfW5$8 zB>WWu_5^#9@SHor8nA|hXRZTsASdBze*!WfBjG7W11XS_@WeZS1V~7D%=bVH#3VfG zb|3;G61KkpLLemJ;VXau2uOIy#efg^By6n(JisGi<3z9;tR`XgVXz0-gM_*F0T*yd znEnjx4t6JD(htVLI0<9-gWbSxB#czRu3%RZ2HRj4unP(OGO!A)BBAG8urt`1gs$~q zC$JL;QHk zA^&o)6f7kn_ja&7*q(&r8n7MMj)bd3U|X;)3CI2nwgKCaaAgQA0ZT}@>=m#z*qVe( z_X3N-ViImQ2o`}wB%JpaSO^x9aO6U;71)Y|oU_3Kuz-Yp_ksCfJ_(s_Fb~WlAw392 z!RQoR_x+<_1dNdIo4B;oVF0Sv$(;Ukv=I-pO%->*6cUNF3s zggbe_I^fLlno%mRJ~en-OXo&`69n@PCjJ8%=YiG+*O;6`vG2^Wrm8^8@D zTwns%gX>8+`UbcTTt~v80=O1jOG3^y0RBh9zK6kY!EZ^(+7aeox|)P=slN(bH3irI zpaEBcD@pk6D7XS#LBg*t1DAu#N%;9m;4*L-2{(KRE(Mp8@T0}x5^xC#|78Fd1M(bd z{kwYb8$g~zt$*tya1kKSq1L~Ff(rq84z>Q(Jh%Xm=TPfk{0N*6$aARmf8P%L8j$Bu z>;JM6{0jVv&j)9M zGf8;w!{7{X1_|%_GdLZbPQu^M1E+!0NVx7(a4I;JgumMZoB~cE;q?o_$>3xXUMmDA zfs;shl^dK0P9))FYH$KLfrOXr0gea9lkmcq!ExX?68`E9a4a~Mgy-x4@5dfP!qe&C zXmB(MPhAC$0!NYXq_e@1;7AglkcS?)6C^zDF3<*T5+36OM}Q+pc(@B34h|<_>mhI$ zIE;jiYrvu4P!d)j0Ed7>NLaoM91IR7VQ~RyffkMS%Lk{a=kou~|4$9*x_eK6XO)12 zcisqSfJVaG)wCaIKalX&0_}U+_awaWQrdU4??`yPjP@<|wtPQtU^rhP{HjD%<8X`j+QCE+PQ z&_1DkLc*i&qivvVAmI^D(>|tsOv1zWqkTmCh=hlnMEj8TAqnf3(mtSlK*H(@+P`T3 zB4KG8+WWNkNm%f}qvQ8Tn7fVkF6~_sX1%m`Xz!3P{VMHk+S?>dt)RU{dy9mL1++J5 zZ;~)}IqePF8zhV(wAX2`WB&ikXhGWO0VAJ}#D~9zZzFE@04%)x2g~&MkDPng&l&tx-vwWs5Icw(yZG z5;2E7Fc?%V&`=wNF6bQBPEF}=Y0tvVtUn4m8LJi1vY;eKbLOZFiC0WoZCoBzsVzEG z5hxoX0k<3mPSFHZ84IsH*iJ$bv81_R`<_MITn430yrRa$l|WPHK+=U|v|$T#BPpBF zl=XSlWu8K4EudZ@yxUnRmuiw`zT8esDvSoU>sgwa6-Ge`g?1cr#d4B@ZF}arY2J^q zOhPHo$bA*FEN3)pi#}f7C6|_pMMp7!g!#sJA}?^eGNo9l>`t^}kR_Is6l~Ko%O6vJ zjQN9EiO8zrDvY*(!E4ku`8Iw=?{C$Vd3D{F$q1C@tQp?flI7#sb`-M0lGuVJJ+tbx z5DK$C)s8@ZSk7m#b;rtClZutCN+%vb$&fiY2x-lyZZD?|*U3@0KM2la{%G0qzDXh}?&E zyqH=vM3WA4q#eSf!TP?aXK9GE&q1XxA?4ayQf10&vjt3kMZ+kt^K`MA#w3twylREY zmkyhic z&RzCadMRHR;R`#3kWj7KtNqhtJU#x`@l(s z(ZKwkrI~q5DJY@PJ^*sX*S>i@bDd%Bvj}aed^TURqB^Ixm{;>0(P}BLGn;FMh(#XI z%aj2-q6^AW>2#vKKV*roeWN|IoVNDKjSfZGoT^0~MPF8%&lLnksli%QTM%bU7b&~y zmP*|RXK~bA&bHS=R`}XC(le{hwU0suw^Z8?`QdBdaL@cUTKi-gM?ll$>-o)C$zHdm zBwDMEFHS~FhL%oPvI+7^k3(LvBQT8woWDNE4PX0)dgiv-`P=CrOC_gmr0vC|!PdUP zo~0qyJ_@;N;S--{a!jLu(|d}}jWgy^$+kLHwr9C&n9wNZ4B3X)ZJkYanGaIqSQsT>m)}*b@EE8rZ6HJ zQ7K!FHm%ucBdZbn#`L)c-1SpZzea-w4eDmt%a5v(s)(tmt>jXC&chEu3a?pBoB z?nw!0p#R^LkOJ32+XcDe%S>O-TxVEjB+5inC2t5kM!nGN&YKm|S}~UKVNn$Y8AmNt z3PgO`h{0Tw`=>kQKhnuzF_Ixy(?QW-x0o|>2N`@>v{~x%JHoSP?VwMR0mIGsk zj42ZSOc_%sSUsKrg@V1{^(o}~o;hO*6#)-`J+CwiQ7k9jRBG6bcX>>vQa+yzB(Y2y zD`(d&Mlf5tLs{qIg%NF%?hxa*VvnAj37(EShs-a7}tOb`}EVCG_QZ<@3`$Bw&r8=JF zMbtryS(QmjYE?w5l)A&Bgw~7Z00LCax zw%Mmnk7mi6$t?fZ&5Wv@c9_}pCyAfN#;Pz|XBp3qGjjluO^tNTa%hD3Ji$l!LSo49#~K|^dmU{DYr#qo z%dslPWt^0!Lr6RU=d{1!vW0?JR#W_+>0~%P_3u(N5)5O#!%LF6TC#$52XB08t--P& z{s-3@7<>)O>p0_DV+^~Ch+Ps2iCSe(BwkGT;<7AScld<*ajCiJOosC@UqUc0mh-lF zt&vJ<2~AL&Ze?JY^o%%Y)&yOHMBSl8iNb4+Q1j;%E(mI0Lq+__q7#PWZ4 zt-*F!e1?$rv3BJcmBng?Eqe03;HOzNCV7_=$pW;6mDsBuCPyS$hAD2FgvGDK9w9c_ zv}Js(=A@Mds-+C1&s8%SY(1GWCoFZ{EauefS_NC$(j6W?YjoY=#f38J4f7&dzeiIS zudc|%WwF{^&gTtbYqn|;+2pH35u-kmOKkZLZ^;TPky0sZDm1i3VZ|NMn_9(~v(OU4 z6c90~G-VNIYGOo?tQU*ckSA}8#Pjvl@wzeSD9RC2F{#blQx0z=qY`Lyys#qTF|s6UNm;7`dg{dGB+$FpRMFfnj} z@|(&0z3DDH-5{_8?6VjI#%0T9x=1dQi7}I#aeslak;h$%h{`M|%R;=G%sL(`Hs#r3 z)s*x`Y8k&Z8k360E35UMEuZNnzDaCJhYX&yNMtLOaM@d*-ooz7wQ zrG*YHKWK=$Jo-kwk%1{hU`o%1zF@Cg4H13b>oy7#NILEFM+^R_y2sSqSl5aCe}4Kp z8uI|=-prMZ9~dt%?q?hYz2Z%bT^S7e`=j^6XaGmUy8znJ74QzgEAZa`*(3Q8@5r_= zJHU&>_rv@E&0*)TaCrNnmxk^ex&-D32oLQ&_`~1}FkavVg9kz%_!aPG{%f4OIfrw6 z964vJflmhhF>uj9dmsjV+js7N8{V%!v)|n>>tEXUb>EBde!y{khCXiJ0``aOXV_=4 zQTD!Uz0n#pnfk}$RXDZ8O8;HrQq{i@s`@PJ4_foLnnukL|>w@473U z_9*e^84uEFkFI3V_l3ld5VbaaSf@QQDHrWwqRNkQ(H@?Zi}oj?X!>ga?N56{xz^HY z4-ti%JN1-yMw+2LMD*%Gf|}^ngG8_XNc?%`Uj32i)&CH+X6V)b5WRYUs4}fr4=iKR z3Q#LV4_ppIAJxlbS%4oAynu+`)+fdxe2ro#jl5JoYd zsPf}5f%!z!^N6D9!vN+HQjZdan;nMEV<#|548sUPJvsTn2+`OP`yvx@9I{sVo4BH08r zp-48-D65A?HwoGWScJSxf_fuWFbP#Kh{73E!5~yYC#pzrLrG4K^qnoUf-xJi|Z4vc%lf9*VOHfZ&B<)*b4gH2F+*FaY zZ-_PYYof}Jillu_H2oD(G+mLjuL!BXBnmgH$j_5V7d|JR7}jr9K-(Y;rR!p-V`}5pNaBkyOcjmaCv64T%IAu>1m?Y4CC}PF-}hrRi=&8Q-lmpc9G#pVw|2J$~PLP zCx~%+oG9GfI7vH$bJ8B4{Qe)L4V^H+V1>ZuJo$gR7ry^y&c_})(T^P^o-zQ&D0pMd z)ls5Eyy;QmN-AG1vEwx=XP|g}s^11T`9MIyD-3r6==iuutSgu-Khd}QDE1|AZLgp2 zp`1Gg0x%W1B@p0t8tKXdrk{vn-@|4(5zXYMr^Vay^HDJmCfLGGU}roZm5S1eq61lN z$?;%b_aIMGGO?4C z7kaI-DwZj*V6gDA&2o)jloVX9cs@6tl3QS!`QASEVO_@zlrh1^4RjG0qcH8}%s_?z zl?F<|vw0>1-Si3hr`fD$kOQvqFuNc0e!=JT)TepL>_++T?6e}e1oPXQai@(`(=Ace zt!-L;z7(3$xNW{8n=aj(6lb|?AiEjB~D8s^coHzKRcHM=iwY52oM zq1+{QdF^sxvV}UVTO zyHtsct;cWfSl6K$LQfYKW`>xj3x&xwXJ#n)uQWpn4*xTm;g4^jKaKg3{IMr1c=;uh zoZLyKtPb0nW_jBEx2#Si==yw%grt0)$rdaq1cGz|F;`cIJyDav#!uNY(QG=R;)_#G z)oRapXv@FG; zhaKr;Mx%Brml($=T+W;QlGtHKIw%W3;VM}#0@x`FK;Z^oF9O&m1?c@+E0sHYy$E2X zEWjKOfrYXFb36oQTmT9;T<3ZSOq2zf;~_9o7GREtz<>)tVR4!3A<$D6V2+1CM_GV5 z9s(^c0ENrYb3FtIWdY`R2sD%hnByT(;{s4VtX95N5I2e~ zVo%VPv07V2vrk-$$5MW?NRkrC0#bO~VULf^u>cBO0GlSB)Yrt}pge6ZaOHtOIBP}H zegjXaGs=^Cr!<7>92P&X;PB0@>;GX#?nhYw3Lg8jzmXkw^=O#uzBdUq1w>M;6;PuoZoPE zAGm$M-T!cZpl?Ip34PnLuVQbZ5YDEpz7>(G%2*B7%t~j>5pp56a(HGYoAc1vBnK*#rA)};kY$w`vp}4zhQls_ zTAI@*(`K%vSdJhDiLfj!6k{TG18sQal zj=H+e3nf#{W?d2wiQG}1D&2FZH$C%@whNHoYRFGr%SyAlmV;jxH-u@uBcBu*7N{s+5y2 z$tzN)S)W83IdjjfHamwqJyC=0EG7!nt}u)`ZBdueX{w2W!nDv7Y$?o6XG^WGMwG^2 zIwK2P8il4rD$e&T%A{Lsp`C#|cHNamB}iz`(e-BZQ%kkekfj`Ui$dtno>_J{>lNB5 zqJI>Ec=pU}#{R)6l*GmjP9X}>H+$x}*(ucN>sx3ii0)Ge$=S0gGj>1QjzgB%8bl!^ zXU{BWT7zPcAGQWjh|AeCzaQ72R67d!VAGaDJkFl^bWGcLI|7+uTLy)AoINw^Sn$G- z6*gNb#MDtg%1Zkvz%!S z+8^@6)*uR}0-~U zHalB8uP+4KUQ86&>p4BV+~`Fq)%Fm>NZ~AV_T#}R9vi#r7uqOfhAns$PVIVTHlz6! z+HQg+g;T13k7c&)f-EtMr*JCMGs{gaoW)3OT}8ie1&;a|JlsLm?q}7 zqfd-p0B`=w`hMtpyzl%`Hq8EaDeY#)+w5mY3M2cB3=RJc=AWw#tA`g1y*zZo&>=&H zp(QXA-nzjFnEP%A&ikA@`osNv@FM4Uj)$`oy!U_Kz^MZV42<`G)&CGoZ_|V+xfao0 zV?4=N&$y6%2|Le7Gvtgu`Umvq=y%aipcm<0`o4Y1K4~9|zB7yius3@r_Arglh|WraP@u z6(kjX9+_{9}`Y@Ocx16V)5daCktfL4mu_a9`9E0Shs>lC%Y%jCrm26)^6lk z)eT#qf)uT;sKTR_LH`z>%&&J9_z@G=7!2kY>_6Gm^t+yu%x~*f@cV8B>nE9uZ9#=8 zQcW}h5`SD?DVC}=L~Zt46OeJlpR95X@k}9#w5suie99C&$`q?7#hv~fGTFgfyA`bK zR&Wc>oQm0;IZ0uQqI@H_lnY6UCZmZLvZDe~(I!K<8I4V9uuI%Jhb^E>#WnHa-6&J+ zI!R%zmUS+qS*X;7g=ivSN~c90J6FwjREn0cTh=mi6%K2}7vL)Zi^qt9F@dRClxY>F zkSd;w6cD{UCXOW%#;`f<5!4-8kx`L1G^-x3I?CIRD#5-~3HF)ngdvH>>=|yrn&GB- z=2A&q$i?!4mMLXP=g^n|jc0kWm?kP!`9?zog<7mtrQ%_3Mb*^F)q$)_SjnJ>&R$jX z5?psWS3ygPuu@mmEL=gEVmY*8(F~Tn**|T~;6pQ?X3RpVKq6jzN0-0fJ~`VqT^3&L zR`3em54h6R)M3<~w3wttx5!!y)*~t;DajaQRg)>1Y)BQExY`r4i__cgL79R(sVfRQ z#95O(WD$QMBIhgk_F_|DkyX@np*~eF#Ue&D)(qGcN?$gj6xpk(Jo8q{6zeEc5GL!= zX$>#|OT1O#$x>$2ES2ce24zmi&u9`Zv8ZgYxpXKuR+6R{I3{~efx=Fi!Zt}kVNYpO zrZ{4f!UBWqrNxarjOmp(iE^%jC8;o$5WhASP{Xtq0cS49t7ePxjD71WP9dbQH=ahB z;?zkBuG5)w1j7hS)=@?kNXsm?w8T(ON_ggYTkv@~lG_riH z$(dK_#qMgu=k+zxTAe&26gSL5Ym=8AP*J8(Ql?N$F1s~@ycMn$Q(_Iz@5sdh0yI?P z+gnDyAyrK2MEZzO;BtfwO<{fBB#VkipYfDLkr+?Mk8=aMn%@v_=1pdq)GR@)9=lM( zO)4Eo1a-5Hp-gc!Ws0LFDGCL|YON~y)^J@StHeTPAy=Sw+5%#QT$cz))as(gftIV@ zprU^fWr~HADYlxV$Y<5fxT_*jx$S|LE8^lN5S3D(@`lScwV;^{1i1+~Z6$&Lj~$~- z5uK#S7VL7HQIyl8b(2sMiYQASFI>(=?ts{s6$#Q#sluo*IgME#^D)X4lh23(3OpVm zc=*b)Rz|FExnS@#zAB&fx%7e-s%rA(MtRsG_9{I-nK5q;m|0Iyrg)q(1<~^$*Iw7h zGtR6qlIO}&220hSbXKBPwO*OhXgrN(v?x`0cp8a@KKbw{pg^&DQYs1*lUuDIz%6;4 z$!HNViOLRbK$eq6YtEd$P%5VFD!;-P_XV;hS(vMf5B_VC2L+1vDO0>RNr6i1Hf6Jh z7E;EnP$vpjMa6W@sO5#kynLXR$jAfMU^X8qBIbFclqp6iQw&d1*tr5*Ua#=kH5#+9 zo_4$32D!`Pwb~+$oUiQDw9N8o%nXM&&%T2)#qE?SZkwc_@XS6wdH+978xjuaSwiX*yScHWcZw(LUx1<$L`C_6v=Zfoh zI@4vP=|`=d*Ol~IRA=;soz+GQwPg@pCN=FfMkeF&FoIdb@^>DhVnox!^3NJFe3J%# z2^kJ=cKt7g4CnJjA}mD0j29`#ohH6i#3YKmF<6M3;;Z8&p0?`uXG|V_RjASPw31ZR zpbZxtnY2IXvl-nyyS$icHFQYEZxqPobwfTTNt%_4UP2b&U7x&2IXHEZl7hv}{u!dj zwPuFk#Ve_NgqV5lpt4xa5Eyy#+sRL}YE1IpqLASNOyd7g$nZ_pD#&Wf0x666Jej=f z^<{mdn#?6_a!KB*td|R`t>&V|&LV>qnVN|hBDnFGpDPR4V=O0|TC zP&s&IM>LZ)%MV@|jR?eIzI;tEV2vp9*ws;Y-c(G5Tk5L9mn>@y)vDL0%T&DKNYWLn zngnuG7`F>hq?q-oHIhiZsE)x`c2`K`3MyfI0dFBID~0%>vcK6%v{H?*+mdS9RXqg? z!MhHv4u%ZpU{;5P45u(x=gjJ)n_geW%%Ecrp20FE-3fjg%hBs6P2C$ia@(MsD1>)=5?*`$(hWV^GaFM zD24Jp$gkGJ+x$9z$WtjhR4S+4Y%vPPBRN;Cn(&7d za=Y7#G)y9g*(@+8gbk@Bn39BY4q2rY)oFOCaF5wxh_25Nor+382;%<#5?X>b>KeIa zWZrOm=+2>qgHg_XoE-*Q{m=J{`_5p$1EUb1!+eXWVq8rBmTm*rfk8+>@u$6PV#O#8 zL|55qw1IxWVy*3`Q}IkDd_pE4rDpdHri1Lsw<=G^eyh&yk}{>!O}V{vV)-a-fDmy2 z6OoF;@|4#KeuZFm2{F<36XL&*=~ckx6U#6i?)sw+2dTI_6A}`xF0=a^4r0=+LGa&~ zb&;{tQMbLz#15E@@*id7Q1OqR(&EOxKeNlq!K4&@hW(bT?jmcan`V3Y#L^CZ4vbN7 zucoTc0Zcmn<=Ah*D_vyVM4!thw(qc;ek$(Rgp7pUxH3_rwW-i#N`-i{SyI>)9os}d zCLH&6?DrmPdKGc`#C9EO>>H!trcG6ieVBCP8tnJ(%q}u+GSuS}+hQ{A`{T4^Q}OF2 zWZcw$YWA7Q#spm@!hY|1L$BiQII#^T?&=@KWl?bpC&c~H;c|8{S(t!hf5v|A^l-0Y z?lQ3i6LXreGpTr$6JmBe+srO26O(df2>ZR`F1^aS)5O-8tim5v%An$uPRKg_403is z8JLL6Ucr7Z|3|NaE}K}43A)ivNx`L^5VTi|$9Kx5dto9jy`xtVmrg9|+$kw|x=#)J z2TUk$)VyWPe?emQ_gz0jPE}4-ohlE|JyDyPWP+9 z4y57kvsH(T17j2%>#53!?>-|JVls|=+^dYsCKhze#eOQ0oHm||v#Jr_eK==hA`V{E ztBA`d=69&EZ;XP2K2`uiM!G6L*YUIA?~!@XI3%s-G?5;#AM9xRm@!`Mldlq+I=Y8Cm_V^ z+@*|`oHMEtH0fk^bhtu)pvHEzi%1)b@p%BIku4XCF^e11WU^rVm{40hq;!yobd+Z z8b+QWqJKrdhkhhoM;`{yfOEnAUD8}Y~WNk5%e!vRa{WEusFP`oRj_t6B*NNy% z8WZA@wM97Tecebq&jO4Fk4NqGxRq*ITZohXp&M!E8=DFBs*R`-HTYz00Zw{vH`30x z6u$*QbZ)m7(aPFuQ|z zu5P5Av&ZUJs?;8bM(2>V_rOW->_RG9k&*(d(48cfv`3*NwDuaCBy^ zU8VK(~dalP2EU4?bWIFz)7f9V|`tTlit{kv~yf- zD!W2uw;An_bOlb@ZLS)tJ_!%45} zM%p=>3^uFV2!n_?;EK2dPI_$@QbDJ^+7PW);WFuz8d-ZOPI^rjQsE}EN#nFCv>u(> zB5QAtlXiJW;tM**Rjt;eHXrJALwns0C%w80S3&1QbSX4?oelMypuKL3lU~)0v~x-r z4Jwn{?sU50Y}y7Vy|No==Nz_pQNQ2kvv?6%dkIc@MK{vU*`zXAJx06PsW8aeTjQjc zcO&hbOlsHYo67oV3e%Enm<%C5+Hg5r;ylhU2;jC%v>A*G(p(SEJOy=)(@U z4ll$>FX=|wIVCJUo!xCl6k2HNTj8V^cOm6>j;lti)p;x`9}2gt1vqJ!=Q2L@5ZKsx zKm|Q)ta__iWsl(1@0i%q9?`(^D> zob-Zjq@5GdYqR^T7KcKkl(k22((}8KZZez9dc9U_ck1DuGmMk|x*KWdY*K6VW~D=; zx5HIp2q*njH`318WHET$8nx1k&89(|^t^7Qom0Z3a5(IGHDZARb8ylwA8z=(&T+M( zHfZQ-l>#oN132k9UAXc(XOq!nLo_C}Q3JX5D@>>r-Vo4vwGZqHMR~jaMIJdk#>%&TBmi}U5H)>?Ujy`p4yGHbAhpWz3?^-qBLuz zZNQ|jT|ql#W>R_QjPNU5Zq%*yntjqX4W~T03uWifxD-y8UxPg&N)N{gPwGP0IUNix zgH8u`9GhQy7*2R%7s5^>cbL2m#HIK6UD88w!Y=P_^3Fjqcuh(@)R9FgJp?B_z6;0B zAyFFCN|Qz5);OdGb;& zZ=Rh4X+<u&1y~EC&;Ar+DImpRLJ#ejS;rl@_Ar=m69J8piM7U zuI9zHBK{~GB(9g& zh`9uYpi!8X`({#@K9!Xk^k_|B`6f!nS{uZisVBXWhENNW@E<;GeixBWQMPm$YMHgP@iw*4biYs zX5ex&{7gnrFP1YEi773Nm*T2;_2;IrxNxo%=JwuW73;k0vLBVHI^I)rI6jX~tRc$BKHL6UO-cxhX8(nyVRR*G3BFR#_(7*oV~pFm$)-wag4aU?>S)8IuR%B7O3jr@i!`cu z>Sw30##|}P%}*FnL855N$c2tfqT)>#JfWF(Nu*)rIgO|}tg^y`M7iCb;40*mtlsBx zlwER-)0oj@)%t>p=e4Wi$)BCV>T{(qOE!VJQUza7Q1PLNPj2BkG(r3!yW_D!CyI+@ z#<)=^vwEbGY@sPMMU(-qPsZnSVYXwrBwnjKR4%cqX;H-!KRboZ*>ji$F=)c@ZNQ{S zz)xHikwvX^TV|SJN*|&P@Px{S3C0G>M`aSHQ6CEhj9Ps%QbGb%Un$v0BSm*D3>B97 zxhX6?XU}0~b)sQyDH|a{!syQ#9Hvyk8lGtlGZv)*xz`;*Vz~-ThMVvdVhKr2?D6Ge z@EF;z<65+qT++Zb7J{mHZSwyAqqL!9|0k>rc=Z2|H|YP9y@ZLz=VAAw1pjt8f+^-}makp8 zV^WQoS%y?i;A*nGx|lPbM2>ayFsMjJoYO4~~A zyow(*j>C(*>6R{`$k_{N-*_tCYz4PG*J>dWE~rwONJitZXH(*af?rL9G~+tAsu8LL zV=Ya-AP!~R(uP~%(})dDxjL#qWzneE!q3wV9mKYL7TW9)ZoeIwMqQBxXfP7$1+>d6g|sG(KKw`GW~h$fL5Q zg|Uhg3Hj@?Y=IYP2HgpxJL7jFIh|Ig^y$)eaXD1aq5d$Ul4izvjd~;|%Q!v0N;Mu0 zi|rK~x>}meWlDB+Z)S*f9hxCbG(LivA(m*I!sMDWGvv>7fB9!`h7=sDXEMVdoxy(^ z^CS8HB#FlPLVkCv3rhX&q9Hk6^EXN`f>EUCt10wJStYDe^0W#&A{1!NI(V*D$Bx5u{}0+A^&Mi4}VIgEmD%QE9d1b^J zSED9tAtsLUmD+^UJl+~t@l+nA0)F+B^O|ytD#{O=QfOK07un=BMJt^&Yl{kg(;gnL z=Rykjh>WSQn!^I=L?cl12d@mqqk&2p{u6r1^LQej81FhXOAg&ha{z<}kkMyHuN-am z&11jDUe7+By+3<(zoCC=-$#89^qm8v@b5Kh9$h~2$;g8vzZ%Jm>^rhx_>JM)hEE(0 z3=4)?Lw_B*cIdDn`_S0nmxGVO%z?#0_2AZ=cRBZPPUl28QVwU}#etg!jvDX`?AHHX z|5N>!_1D;R)^jkE;2|t4YbEnDm{0J0W{#;~E@Zq#Uj@DfkHegTWyYP1Qy4*p2xbNN z8~u9v5m4Bl<*UMsT)?iskJ*UGGcy9e&>wK8+yu7SIH zt;`s>bKuTiE7J$=7`S68?w(`JLMj(No9e9!s5*UBu;cbxBft<2MgiR_Dm_)p^QGx<($h&DQ2n6=3K_Ptk=pc z&ZV47d#%jmT*A4e*UAje#hiptPoBJ4_Gd}ONGMn)k zK-cFkffBPBg4(HZA!~1YYooAi9>_zNFy;f$iN7y61R%Wt? z*~7h7X0V6YL%mj}vj^FOQ`S10HrNB~0kUCD5W2=^tm%l`VGL=!`3}wXgT2Aty;kl6 zdx5=rt;_~{f<1e!%mQn`nqDh2fgH$tt;_&2AnUa<9Y}$cRP!vV0}>#aYB(8nRDvqR zZ!oCz*0jS`=uq<@qsplETA9PBFe<%P9$=Ii;n!#G;GYK%9$d+Jm{Z_v4L_4c z2Zs7@>G$`8zN`DJ?9bU3vem5jSZA>0%$J$RGWTFS$2f$sBmEJ2k-h}n3u0gxHvAv> zK__I)=r*RYu*p_LqZXsNV5*C_Wt9o0_er+&Rh}i9v!@b8uB;^v*$V=0H4gnO1!76z zmUrE+83bp1^xAv$J$`%MiGn?z`pZ>HS?Zg|zRGfHU%!n`Nb!cn%7)FDESlW$+lec*!z#76B4{(ektXwTeV@HvE}g?z4o%Ts!#d^YD3Lsh??rwrnr@Gi+n|k zEkD`t^S3?ohgU9m>bmE>S3Gsod&eDr@{v1#`f%f}kw-6h#d*wbqOZ{jG2XBi<|9s5 zRkpCeV-gGCO&Ei*X(?81)vC5^Q~E5Zikr6!Jr-+?Xn4ti*PQY4spaU2Z-tddobfx! zwWr?k%5{4!9(Jw1T2(!H4QyD1H;lTASbE)h7Di?B>OFQ*QY|PdqRMJRCrBk64wF>v z=Z8$%w4FC8vD`THMT1O`h5%~-E zojM%&NaehA<)weVT5;dgiWgr{+>TBN@P?D_thAQ0NhKapK4lP_YWz@M?dH3xsj4>Q zt%|E5C%2GLrT8gA702uEyhr%WJGos>y2yAh`u4EddB>i|WDmam_I3AMIq$OP*Ps)8 zykUP{A@Q5+E+6V}^U5l9wUv~|8r7;=pcN*{dPJ#CW{tYKy-{*cN*p_4O=R6xL6_^9 zKOA|d?C!(2yJ0l=`(y5BF=bcoa>%>~Z$&3~c*8nXEgP=OW3Eg%;r4RFDo4PSlD4W< zsZXIa__A)ZP7p-YDYMBp+3@z`Z`7S9U%;TH@nd-T-1KDcu|jd{+_w);L$*k0qU zN8vRn%s?9TkV@mXy7~T`)gbeUoMNP6GC5-Tuu)j3CY6<#tKqG+cq#!gcn`7fpMU0A z&;9C&RS(LHr)PIx7r5m9-(UH5`_@lCKJeR52eatJ9(cp%rq7j8dTJ)8FDK<$0(G7` zuF^#eUS1+piRImDn^W(w`HM=UY*J$3&Rdz~~# z?q~1j<*Z+OcYZf?Vtle;eav7-YUP?p6fqj*R+mAjlzAMrqOoa=g{l>OUXIkF30I{^ z4Bql%AKq{6`}dx*be9_~rVH=;IFi5mx)Tn)^ZJ#iYVLm9x&BIYVmG{DZde@8R*QO6 ziy{F@NF~c?M9G*ioRMo(%5Z@z(Q%bQmBq>p)hAW))e{H(`rYGPQOyrm-oL>vJN~|d%k?&%CY2~ck%U?UBAA3@S+t@7!P{wUuPeA9XL7nKj_3Rc*9~{ zT~vt{4S}2}E8_XhfpkjmkhU_d9G{;Plu8bptd?}uOp-d$@au=jZhiOo?+@GgHN^D2 z?oZmg-gx5B>Tgo-sef2~{GN}#$3!Pq;SHN)_Ly8J7p0S_gjoo$?~2`_I#;fe6*SJ8 zvg9r3RoQyYQscW6lM=7~pJksO-Z*2ISA++z9Xxxx2R>A8IDNrg+y3S9Bd)yvll*f$ zbYf?`VQ;vaFcxabiY}tis46BzQbjfKbR!T`mtqE^R>aqviiL1CZ6yZpfJZOc?fh5Q z)%Tk>Zru9Xvlk5Tzx(LDL(VwnHOJsJ=k9#ko9M(&c*71^7KT|Ah?CWD*d)$(g&nvI` zGVqThPS&9lV|c?z-EXZ1YhF=FYE!1UEp@;gl}l<3i_@oxSH!L`l974Qf-lb{8s3_1 zx&7L|yn4@*Z0jwfpX~6(wI7RjW=_21EcgyN!0){b?s+@n4F|01aqT8PO<1@c~`VV`Fa=UDJ zbmZdv(K|e~D>|_fZ&+Q+O0&9_gI^anglWAapA;G7nzTJ%3tAf@iNmWX=shV_-fJLC zdgQA+&pYJlzl;=LQqI40kAYA1^Dh1Tl!s0^eSeP5&%1j6bJ2+vc*EJ2sAdZLgAsuy zqzU`8dQ|UBBNkKCT#aWHYPlHdhL?=EvPRA1tT=z4)!YaD_272%AB?SVEI7?+Y&?4L zd9?3d-}kX+RyohR1f5upH(Zc#buzdwWrC=qD2~YLHNQnI_QoAZHJvpjbMlJRY1Svv zM$SCh@KrY+yJ684XJ28z_S{46|J*!!_RH$Em!EgrRiD4~4DYzJA1tF2%kYMUVbp1h zx{OX!O%xQSg{ELjVTNASYJD}LGzQZdS=iDjG$m3ou?W5RN5|f;Kcc(y%XCx9z3vbE zv0H{typVb2<>x*${NM$5eE4g0Vh6loiAa>w6iwz-SyOeo%lV26#3?PY= z$r+1yHU45QM5yAuQ&ugw^s!U!+e2^MW!>RpR^g>zeh~WB8|JQhaepLr;mzp8QoLb4 zv{pGUp5_Ze&a%;v)4{;Irl==g%EK^5$-G(2~iS6))#g#x)=RnehWVB%mb0aC6(UkRh)n%SSXf2>#p+G9F zluI?q)I$H}KQ}C$AGvbX&3iw2;Yk}#{MFG9+_Il^&DoxR=Du0~$cOL2g??MSVG0>- zCMCY-ip<~cbzkt%U5f{eOA>oGmOsAKw(m{n9`5|RK4QK9tQXOVZSaOO38g$E_f^cY zoDn+m`gnPlTv{p?9mN0=<{RUQyuj_slwzea@mz7vyhG1@o-2I#5nk%NXO8;rgFCK4 zZv11}aq%%HpJnm?^M*g76HD-h{lTn6WL0q$Mq9w(HENrD8$YA>w`$6~y6%HPY?S7# z89oBY^2AyZ{p}quzkCnvvLALT9rLL3v$KvV-?c5{zT=~R-D+q4ryp-P4xQL~QWaja zE$0?GR84~?sn)0@&a%aqEnE0V7KxZc9!(If1sZCj&_!(jcb7*mG(SDK5$Z%#dS@6`D(EW7ge^!ca8s(aVoz4XB2w?Zct;tf+sdotN@ zV)2R(U;E_!B~R@2!=sTQ!wbWGhkZQw_xrXUqMi5u*n9Ih*RAq=+}q4}c0$-gNNyNP zlAFm$@}7jnwq#3Q|KYgl|XWZ1@6zh3|7AMD((`y-3q z`r(^m{}_Dx;eY?el`nqf`x`g?(pTvXU*z=RePS6$j0`+}+gtxs`_F%R_iG%Lud!Er z^0%LP?-No{=PNnaQC%scNoL|1DTE z@hH{1R9V#(%C{?d5cD^j+@sw+0~ElRSU8h*uv{8XCMuo~w7%&;M$Lga1p>u@c1Hp&D!t;s zoC5J;K)Wr0Rz|&nIR(PTfObm)t&C~|a|%R{0qv#)S{bzl<`f7b16r*FS{aoF<`jq} z1KJGP*t0^w#rtCBz~qin&P0#Ro`tCT=1qiDgL0>x)QtB^n|qh!IH06pkHO7TMpv@!}7 z%n4A72DCp|eq`){jB*8Y0<@$7?e`_n$|zPaCqQW$(0)$>t&CCya{_dz0qu7s(8?%O zFegBz8qj`60O9HKo5(RSt^sfQ!Hzm-@C{Qpb zKph*g&2d`FB&tWkZA^1Hc?k<1s5jrIU@0<^OM z?KdRQ%BXrUCqP*n&^{=ERz}T(IRU!cfcEPWXk}D9n3HOs_5lgBGU^@7Nu^KwH3_sb zsvXQpxlen)1X>xj4(3Gc(|%O~t&BD>KscJ1JmcTEr)Av~Uomo0orZ$W*@|LOV+E;_EtJj1czYCK%}!kyC|CiKo?-`AZ&Xt~ zsg66ZgBF7Wz?JO7rZgZ;L4vJMi znycfa=t}V2h^=6#JL>6ZoUm3=eK_EWXWG%A)oU)ay!Gr6ScM?4iBQmLm82okyY-&5 z&o7@|*&FFE^eN>`>&quHXrvo^Ei_}K8>dJ5bq8{ka{#Yw;Yk@!guWgY6nEFjIqG|s zpw>wH5%}H@np{ul<+|ln z)@ojFDqpWOC=~J4qDLjBQrCrwq_YsA_;S@nrtns&#G@7S*_hr2mUHFv=-F58lALW*`2d^>c!!SM;CqIv_7C{ZsGCW&r(9V+N4%A$RsuUQQ#= z$i<7PVj@-TJ$`@k5BY)ZS~geiO-Vfg&7L30Tn#)+Iwh{ypQ(BRgHx*0W?c`-HMu%h zJ#JJB;QCUdP#90jdv7gE!hcS#KPxx6*v!Cy&6nlO5qN>ikOpI%s#w5!s&KYl0V0^3 zS!76qsm^k_Zr0Oky8H$HqUQy-$>pzS8f3y~G@Ii(oUkNBORGUMDPt*1(xS(PdgJVCc#qfrr3yhiXDo~b5kGXh+@Z)j);dLL!Ax88s_eCx=k4fmJ%Yxa?zb{ z*E|iQ2PNZeo;iJLpZ2K|Xj9oph;!0h%gdImrFzC#N(7rRyVnuYcN}=#WAU>TUlVPW zikUfmN}u*A627P>{P9o{0~-XoRGPONNlKTmuvW@lsByjuo6nm}G>0HK*m?jyA>P)f zy-fmbAzdKiRx;-y^a*3L9Lc$k<3cD;nYlu#l5aRTu=u`$1nbPPo;kg>PkXBb+Dr-U zbe)}g;xN+Ek*#upjEX+4YxH`X!Mw#mxLnOn%(lBa(8^k}f%R!I3AD17Y@mHwR06H6B^yYe7LheR>BR$#! z3AA$7R)BpYJ=!Nqpp~<>0_+;;(OxHkR?gZAuxF%4dqx7SoV68T$4HO%S_!mr)>eT1 zB0bt`mVXR7kh2H^>=xCs*# zfmY5U2(U|}NBcMlv~m_ffIT8T+Q&+um9q!}>=5bEK1KqqoJ9~|e@Kt^(GqCoEP?>L zLwdB2l0YkG5d_#9(xZK(1X>v%a2c>Oq(}RR<&`fvU&|PjFeRucyO5Xt)GunZSQ6$BIvTO%rj5jF` zh_W46aTk>O4#=4O4`fW~Qd*!W+kq8#K|!_yEA9fJ?|_VEJ&^H0k(&)+^TGM9H5l%F4 zZ{;{v(}QAL#1L`Dm%k_8eLgDN0U66Gr8p4jJ0RnIb=6%EmhHfbyMU4Hz)HJd3%t4C zp**M1cr{N_e;+9Ly$&3Zb%)xZ-dBA=^{)Mo?!R&Wx%Yi)w zy4~;W{_E~*cTaa&aQffY&Zl?YzVqul`5oKNuYQOx%RfT7p|q&p0f5x&6hRr)x1p8Q2k$(sB)^VQGQeTLFFGR&nctI zBjtwTlXFTC%0Hj;50}NSpR5k34hCm3>U3r^I4d=d3$6yVvv`}H#F=y`iaWdU9E|f$ z;*2^JNjSZ90mgYIaRwb?$rY1{DV%!}r`I9oj9<)TVLsO+PNzdmiEIOpz&L6Whv^Vw zG#jPhV-mN?37iQWPOD4UGhz5(t!?KR4l|+Pq*C-4(%29ADYd=4UGhis< zX{F-u-D*EKiPK|<#T9538Tc+fJB2fMJh|xPeISVTGm|(ShL}2)niD=!PW$Oe95^)9 zXrtmaShqp@-;+2LLkx*_#utPA`_vdtr!$##2v&64oH&Hrc1+?-It2AqIRfsrZDIdSvDW`3u@nmXP_RW5P5q5I%gEXt{5{41_}n`m8m$`Z3cD5=Kd7U&I6Mh$?oQ&s&FY*&G}D0X?o8lx7R-PpqM0llmkQmY30r=7&$`X{0H<`Dnn@gr>2>jjPq4#w)sr{`(_^W)qdnChstFu;Wa$yk z7Hq;v#HMl*XU23Eo7ZP6!gdu?ICHXIp==ON`@>0`3DcQ!;X-^0_k$^%(dy%J1kCsS zNt_YW84_I9HkERIZwjX`=72gb%=g_%oB`A6LV0@$PBygPnZjY=E=5l9eR~q8$8=~i z9wMf2-n z3za(DYxhs$dW-3;#X=jN!nI$W#DQrNX-1}3><)Be zaO$W1(iF~AYQ(Ek{{7FE`k9J|@_OaYukK`bp1$*#?fbXixBZIk_O^ff`fbJ5M}PGG z{_xHFHveYxrJL1F&*p*ld)j}~{)zS;?JsKe+MSKhZoG5jH#Q0z_{QVcA6);(^;ZL3 zKxX}>b%2z1cO8@>x_g=DB-Xr&}-Tl_?hj#yH_nEsfP=C00@7>Dlcai<)sz0WFv-)|v z+MQ4JD-&;2Dij~x`HRW-d2n0)$>02Ve;aF0@X9hb)t^k^^>b4q;9>q*LPc24Ij&B6nzH$6MP>UHeVYsuHgK^WT+nf{+O5F_DoD>gA-R7ovQ0g`} z#e-6}xhWo$x&gbo-W%ky)NO8xH=Vl8PVu1B4fr=Z#p{A`d-~&z%pdpai4kF#n`Cw- z(?UIpwyo9bWbL7s7EVmF^K#7nNwRr4=EKH(Ta;r$xg&T4%+4{P+;MJ>Ihi}^=jNDD z?l?EcgmTBZIp*d>B6{a#m{8_8H^YQ7$GI6MlsV4LFrmzGZiYFX zInK^7|7WsQKqfjn!-Pujb4&DJoZv$&7^?TTvJv<_QhV=2E6*#@L#6k*C3>jzKDR^< zmEPx;=%-8XvrF_)=^d2vXP4-q()-*JJyd$1TcZEeWGf>WqPMpr>8ZEFPmbe^77K>x z(xf8~S5LJ6HHkBWD9Sf%6}Sqd{m&^JD&S%Z{y?Pt#3asyAxN=G&{KRLSL~Y=tsgh_ zxrpn>`~&X+b3YACmi&!#OYgT$@EJi#-4dgSEL?TizI6&WyYvo~)aRDop_2OC(mPaA z2ma})NScA)OShqt`rOhxR8pT?dWTBtb4%}&CH3AX6MePPa8H%qp-(1rKdnHYOy+)C zdCLA9FXj8?+)qmF+G`+ z=hu|0T>uRU9nF#LGo!O)PGl?->nU%@St!-V%UFapTjoT@Qo>ncW<7C{<7%S-bnk%s z<5DL-8QH-f+3zWxlpBdcZlLLrOqJ_d@VlPC-x9Twp5qrOLjsC@`ko-#Oo1}OZ}D@q z1uJ)Hmz}D*jc70t3V1SwVx6~CjE+=`tmruoi2Pl=3`x{Q83JwtZP2;tR42k?nQXQe zNY<$$ZMI}ud?1@bs-+m2b+%(6MxTh)S!*@z#JfqW&2tpQja3~Pu$saaqc>hJG4?8u zFp?%u)W7wfxX_iSkM2d?3k=Ew8DnYrwgZV#hx|-Not$Z5hPV*l6Yt4^gK3ZZLSsLj z&>NTrVfVy(s#c$p5;8zNL5M-IlMwlfv0Jma(OdF@ZXJZ>bF-(9 z>V@V1IaaQAQ=YW2G+AbarHp0Bt9iJ|eL6xaer2%*L}Lp;bGPtQdb~A&aP2@*c8;dj zlz#3l*P;m0)9dPAdYnw9xkjNU(c61Y{YX9d&XG{zPz}t```4j%;ly3{=`Q?RHdh0B zzMbP{u9i#W3b}eugclgRfPkka@CY9J*MYKF@44Lfao`W+TSmmKDg`i^f84pI_k`__kRQdz?ea458IS1M`hP!}r1;0s|Lein!l;{#Qg=zjTuVOB=2!I!_iO~teOft z38vL3rF;n-t;R22|1aS!80%&;kDF>{2Z`5f{2iX-KEycj7HZf6KBIE=Xv$F;qc zlm1_oUL}HQ1!W;Xn3`hObbmlZpx*1N`o zKm7dXe{TzbDfZgY6e~mJX1Tb{WIBiS9@R61+RZc5IT^Y*vuh7>383_B_7t0J*xlfq zZLckq4kj3fzXmUz>7|xSk2^5JZ!h4Y@#7ADj49UGbKnw9v9sL1$SD@Ab)PT2TnHs? zMA#XQfD;FDVVYsWZeKZvmy--;$}%;wVHD7_o=Em6ckzi$rharb8>?3mWI7&7r~M(l zF4Yus&afq(YX+-{RJxq$W*gCHJ)I0P>0;JLhW%c|o->j$eZj$*EKbVb#ezP8FeNIO z#pUrINfTf1-#VCL!9gI`^rEhJ5QvPiy0WO7k=V>gRc0g~{c|@`a9Q9d6B!qI5XjWX zy$eLiD22bY2Z4Z3H+mDWHZz%^scz&bPztr%7JE+L9`-dMHfBXTy{U7i1`^%zl2!5GI;cJD+n%z-rzRrpj-i<;NdjBjfWA0-0jC zV1dYVDB!hf0FV* z#>kZ&&LF*%Jq>q&fhRq8LC@qxk#?nH3+g6$I4%#4U}?zuks zPTEiCGx><BXPfPG(t&i$7ah(;!bK+RO-rNc%w(zo z){I3Abu^uJw(ah&-sc?Hd;9;lR2z!Dm+tDf|9LCBsoMCRb))7(>OA=6D=&*z zVP*QVaN~cq6Q|tz?e}okC}-E)Oz=@YUfkM_W9?=fb0A#Q(x9CEKi4>?rJKY>y$^h6 zvgfTWFGH7EXQYvI7K&Un(xII7LMkA(yg1Q{V+Db7CJTw2)k5Kf8*p~xQGmDGP#%kP zV!eOIkxanpXe2Ow-J7?zeGCDvb)~&brcHOPEf2%De8qXT**q5oC2X?LVne`&(~*y3 zkuKlr*{6X2J^O}cqDU8dc7jYH%;IG~Em*R?XwneqI*e@JS67k83Z+ERfq>&T3Y51J zGlv<%d04aPA~AC}5yf$H7Q^k;Y`lSIgSJ?LEE6?-01`Z^)$JD$36Tt zs(DRpfu_<+*2cw)Xk(^aOJH<1;mkI9UBKwWGcB*R-;Y?o9m$}9cZM_mkS!a>=mM|v z_WHBq9rP#YKSRFX2aar&F1QFUQA6A%N1Mg6HNI%>W7!h$3o~SsWTXSI#cI((_>2wD zv6u^V>!>rz)-th7#cU$0y5qLl=y0?NbFE!MTjGxzS@q&&t!%!WTNz(fD_i1h%++!g zBPnmh5_jdidNx1kk;zuJ8R$IQP2qKZy}vs^e|On^e^!Fm2x0tfr8#qaae_ zt^DiDTG@0Nw=&&bQ7e&^Ovf8%N@eQ z-Vhh)WZRAyN;-=I$y>)-`Ts0yWq1X`W!1{7nwOomb~Mb_qLHM*hh&j3>IirW`WdZk z)urqDR@_>pSyNKSW>G(TSYnN|%|-{J&Th(Q<4mLrYa+h%cq_kXSu5)<<9XS;s(IO& zjd-I%l|@|*n#i{OQCB=d)Mm6YUUj=rz1_#vqIOR@0M24N#(8&$_OzL>D5hz;#Dy%z zaJwo_%os0R*2>uB+{&w(mvPqV&1cY9hcfwK80KxSVDT@2ciyhqn`qxQx8R+6`lb zk&gviQPMi2m2F`(YH1(47+b5wlzIG^ilK%y6VSzx5H6NI#w?#W#_dsKq%}TcJiqt; zf4ky6imeZ9y<)4epLc?(6t?-RH$ahFocHM%{HBCmhHJ#(P1Y$lN)ytWb;{@ ze=QYy&*riQyJ~JW8;FoLp&IeSA{DCV zsD4E%^x3NCN`*d4^&F|tdsNSs3O%oSmQ?7e>K>`k6V-XC&}XVnr9#iCPNYKbRy|WH z^e)vosnBPr?jAxjVLB0s)yfg3>??4j4VE2JeqMR6ROsiFUyus@tn%|xp`THHPAc@% z%FjxL{@sP<#tdb4n{lO*&uS3%OnPxOoFNOZ8W;9bTg}zNQnyHsU->MnS z)Jvgn(Try5rO-EPMlpIWeT(AFQlW2Fyh$qbO^P>4g}zbo2C2|DC|)lW`g+Cd22h7k0GsyhR4JWK;O#~! z+Jv4}8nfmpQlTczZBn5|&81R5};+LB8(YwD$z5(6f7^ zbYUs$jeCzDvM$Y8H+IYeC`qwQ%4Tmj%id6#uQ(&HTCVCXs^NHD4t=xgtx{Rvq#6$G z<*aX14Ttt}=o?hSp}idXdev}fFNeNPH5}Tc)j`|1scQ-{_3hT+ZGTj27}Xb)f1!Mll2@9QyOWg$%~Lg3f|CaSQvK`djQTe972w3dcL24( zlT)db1H8m~Lg-Wlfo%c0NO8Rq@Vq4(?z^Zw<~ z^E<=5e>wDYXO#CZooHUR{jy;TNO*`3pG(W1_a@D-?6Mqc)C|im%b^C%u zfW+tDNQM6OsGQ|k%KGk6Ig7|T!r59Tkg!FI)snNw!76TBKiK-gz=7q^?{9s72<;FY z!x9CbkPHXi31UiGUujWVq(aTg;m{_QvYM2`p}ibxR1SwWkxK>ZoZZ2?fSiL*2>Q!V z(y4B!8$(~0LhI_fRA^0IlM1b>t5TsAbww()tS(E1ifU0Rw4^Rcg%;ICsnCMDAQdX8 zg&{N>Zrk#ukPwNLnQEX=h3i#YuikpKROs(*{hn0ltF~Sx75d7pS4xGxV(S%Bp})KJ zyHcSq-+K7~x_rub*Ql($9Qs$Ivi5T5UyjP!%c1Wam9>{c-!Up{FNeN;RMuV&{fkjq z+u`Q}d^Sa=63wKeS@e})IW+b2)Xy8X>r&{isE0+<<*d(D4~wSDq0dndi>Aw=&sL9; zr=`9=OFc@SmO}4QkCLaQ(DUk1^0X9ssvag!%WvyMJuI3!C|{%5Eqk*f5p0wqap>p- z1W|um{q13IErotd{Vl1`Z>qm375b3+A*s-BsJ|f<`k?wjsnD;hzb+N}fcgQc(66b# zCKY$_5+;DfMK==S=yROsgV<_NkhDSe7^SP5O8KHjDrRzjCUZ&eN}q06DSC~xWM z|2;wRw~GC{_rAB+1$z6LKuky-s7)CQncG!_O$eeHM^%$jOV&k1EEu;Kt5YJG#Y z$#bUDCbYBH{rr{(Jyy4kQkUZMS>6b~LY@IVIZfzx5E z2o1ZR$D$qsM%yzkcZaSt{EddQ=C{S^i2YCu=%Tc*DanKVfd`RrC0f-dTSYb$ic!&k zK8Ir!Ys%JWlUP0*40=pBizYlyA%K9x7o*wuvCEi^7ThdeVq0-D9Xqz+VI!3j>mf0~ zlfeL3gBM5$XP77f&ezJM9W0l|lZlE4uGOA9-5a)nbk>7hxM*tT{U&2nFXmkaPZLdu zco=0G`GzwQYZw}d&|x`i&!c%!iU+#`4`i?&u%gw-dIC&55)30XJKpH#Ea`Tmfn!cx zwnjN@c&_LsTY+{pEX9MJfd?{J4@i5n$hRzMrod;zu|wV-Joe;G-A2O{x7u9sV%SS! zoCD8$J#i@>Y!5t;!FtecCF}(23!`kw=Q79jA%mmt@dVQ%@79$YIa?hZ&J=5Q5j%!R z@nCD>%k#!$P^nDD&mYdj*SVsxj^W1X&qm%5Oy11 zJ~WbtHqK7_4ihaY9;^>MkimN3^ClwAW*#r+g5`#()edpKdc8!O93j#d!tlI_je=82 zophoWkmAAGzyleq2L`gG2S-)8j!XKY9$~%5LY@kmx`l2DMN3$<8uXi+Ii~3`wj_C= z8F(Or^}u5eq%EXHFXZxBudbf38^Vd!p~Y@46QQQ98Y@%wVyj6vQN}8z4b%e zx>8FzTt;2ZRIa4;F-ymq4`-Gub(}j@4Lp#+df;fWJ|IC80~^x)HsP>~AA3_4BUob? ziW8Z-f#Dqmb1YJhi-(dtP!2qh!Fq7)!)u*Z#O5d({E3QyG-%v`R7$}X-a-<&e5cbg zW)k{v3b6}P+CVYzKnCjpILzEsB)dVhWoYXJDk$dkE(;+9#3tu!>y5#fwM<3xb}{DS zC3tXhZs36o)`MbK-}LY-m&Aw!!LvmwLWKke?MZnXnW7c97(llmxg=9`6H+|5d*Fc# z)`OKQHJv-TYv6$l*43)koI82OzylcyA*)pyICpaAzylfj1qU*A@GsXoICs(=cp!uI zK!(tW6c0KB4`i?&G%JxBR<%S*-jvyqse7Exj3s5qX)2#{r0DR)-xs&$50~xFbg{;j|u*BR^o`rSWMMQC0IFj@TfbJ z6|pc=sHUqmkf|>Bpf&J72J31y;m@5k2Oh9o(Wv*?53O$hu`A|wc2GZBpxAEHCgQDl zp@7-EMK8!e&0=P?`q%^ge}!VFqy6Zb3Y?_(|FfUx+Bp2T0t`=Fpm{Q(Xw*|AS)zHe zIzc0FD%4y-qXz2pnha;rR7QuJqUMw?)QaLjQ^?GDoDqW03t4ByWiNUIt&3McF*qGw zSDkKBXZ5kD6*uJ!e6B_oh+>)dr@~;{V!DU{k)oJgsK$(4B5I*R5lTbzu3QqCh@VU5!$jLwNR}h04|65_p-ky) zup04jrdF+8sbo_vZ(O42X7AQ}QiNBZKCTzlFQCAIBGM;>kQ_((q*MCi6^;ySV$NV9p-S@#1b1J z%OxbxtkeyKZrp56p^Q20$$Bq3K-rifOYs$#g)UjdBfdlhtG;feWwJGES&O42^6f? zd*kz{XAoqtJC_U3_$eNJKqeQyGv`JAK*zP2;j zjw79z9*GC5*+7vorq7Czu;0uYE0HuCbayD)1lF8hbQq#&G}z`qp}Efc>?R9ut_l`^ zBvq^+QL8_kG@2ucgrN8OS(?lGJGy2KO=VpI$doM+yD#ZUf-fU=9hNZSWs*k9&S=+M zib*`-r+4c;{XbP(VN|?WvGX50zqNDw_V>15w*AX0tMbXpyz+CSCSwC0Wf*ywINWn&%e{eSNI z4QuaTd!g#DRrl=w<^JjZ6Zigh?>T$^z1`h^-K}YEyHI3My+Cn%;cxvv{nA?C$#tDQWEufQe>G;t*>-Vm-IbqR_^)DDfZ^`g&peero`Q3*p zc1Ha^y1k;nTfKre9}Q+1+~uvor+5xKJktONg&lJ|pQ_cA*Np5-ym(+=`M6Q5B;xHP z(TGqf5BR`caS>T)HMW#NW}Fn_!YmulCDsBr+yDldMEYUkG?`y)JZCiOB(hA8x{YU# zMx8{83!`r1Sx;LFSb9Ax@lxo>rj2_>-74|=@lm^Rex>~bzglgajz*0{-v*;*<7DL3 zN~R`QjA{MLqZZ4?gG4%5{*vhZh5l0h!Kl9^UOL@h%0C?Smqd!;r2neXj3)K6 zN!4xT?~i76iPz7Z)s=rV8Z~UCTMlL_DZ%fnw`=tXtn4s+^o&Qj`kqm@O1yZ{sKL<5 z=N4F*{@j()!_%(%{Akoj^!~!AQ9o-mY9wAdJ!;g?mf7&*d4~Eqqai2J{$R+dPe(SR zOo5}@VRs3yRqNo*BD{QxShtLNL*m7YI$H-NDG0KD+e+!-&R#cM)5ku{`2>~zxUj|$L_v**8o)ZtlJ;i z7Ph{<^}@{`ZdNw8wRdT+*!aT@!}>q0-@5jZHK*p|nu_{|>KCimR4vsN%Ja%c1By%k zlR3R&T?O8vQ_3rruSXv~J}J~h zHnwOGTi`bfISc*y)4iTE;Li6XJ2Qx4Ig9zhJ?{?hd3X6zVhJzd7C5jDWNZQe-s}v$ z*||jC$k=9(JKYA(^v2${WM{^66*(IU#M3Qc>uJE&mSkJg(}2{CMjUo#JEsfeL_tPlH%V;p=BCS7%33moBt>Bx$qn>Xn~xHaqu zpaGEj1CRsK8owlTYzI$<9o~u#C+O z;>n%B7KmZRcO~1J5yLVzGh|M>z#@oY#djoI9DDM~*tC#4=?vTP+mbC!#ITI*319@Z zfiV!nif>6awlId_s+61!5d2B2=L~4$Z%TG%7{fA-<}I8wffWm|^3d`HL&Fe;K93wE zId9T$Yi9L{e640ra7iYYt>-&pJYtRNbCloBb&O=bcxswWL#?uHvuif>3ZH?#IA z=TO$dNqu;a4@x!)-=mEE71fj4@E*S|+1%`Vl(D4(jNK}*+8etMNVd9g?7~$rIV(!5 zCzW9nd`+@5vztK1g14UjpGEnQV*kDS&)fIyKVt7c_Flfn?_Iz9<=r>$wsvpZRqp)V z&I@(|JCEA_I8dTYZJ*ux+SZ?K-L+-k+T8r$=5KCtn>T5{pnaA04(($$KD_aw4QfNP z{;u`strP2twRfyF*HF!uHGibxHBV6ghx#|vKCs~6y{c!bZdE>{e3P=Ec&AdSG%B90 zP$=Ja>B|Q{w=ik}Ttfs@Pyf-9cK!IvvmVnM3}^6=!&Vk5=~B}cNH|4r*bP-D)mspW zZanuIkoYq+p3i0}n>FUHb{bAM{6$~A4wH<(0t`r+B}c3z*h6V1Q47)TYCQvcqJc@q zGok@WKG?uh?YyH);XZewg}VYUi3%nee+&&s+bUkJoH&mZZusi2^1WpAQBk zNn6?)Vyo$#BNcKp`M3)%`>K8blZ>yX8j#dED;+C2ZEUG-t)_yFDXlTp_h6Fon$Xab zYAF_~yJG?!5X43$Y=s+E^=+7Bd^OF0#KU#nb)ICaId|9IY&u&Js7+K4!6f5vwgZv^ z;R?Ihc7^r3*;GAM%)uW}RS&`>(<3(FiFRUKEZGcalOd}phGA(z)dO~oeSFwY-%Xhm ziBdKY&$bE)SFGX=z-x|F?}8k`rUz^ypY+(1wwB=La@imibU5KA`6^5@J=QASMl~NG zi@9p9mFL3VEUZnf`Vves-NQ`C!E-U(R;k*91Q{gTP1uvq!6egdV)YTpO2qAVB$L%j z!tM8YV3NN~ChuN_2|kQ+f@7j?GEfbPrFplIlMp%M;(h2V;v5 zCeqn9oAlS}M9uH?TN|+ZpMXiGyR>GFI?Bxus6(__PlWdqNtongFv;{B6bC!YIWO() z=9~3Qyk0HjV3Lo*BvW^M#@`hyA{}g}2%?hnbRwY$O!DtA$@Co5@V8?w($#SUTFr96 z!_qaFrw+?&q zL6~HEgyM|~DU_LX)E-V0Le+Kv*6>pO6HGEaLTRp)rHl4bBvLN2L06T57u~2n0F6*| zdW23jwd#F3jeUGrPhT_j8`V24njTcX;uZv#65hfTM9Hk32mf|gg zE62xP|H?|cLdV_7dZy_MB(ud#4Q?UDn=bmwHx8~GAF6$u-a@xn5645SFK6pm*`&J- zd;7YJzVc57SB^J#-)1ylVC=4l&*2idlD#fC;Q3qe+KayOHG?b1$8g^!<<0Tkj9_Q- zseGc8NOk=1eg45M8Wi-WZajwim&O}u4xHdus5Y2nz7w{$;7MNb;PBEZtGRTbO%`b@ z9|}3ih&P{xClkd3!%N5GrEit6+JgCHyb*5{y;)DFMRBmh_YW@}kCOhSOSKIZUm0FH zKC1hdc4=F=Q+DA&Zvh;G&U*?`*y@)C4Ls%VQsIBas|Q9Wp2dBmHeO)U*+8bscXHk= zVYR}iJSo0-@t58^xD=g=wxwc`icehpr5_(&Iu&nCUpmD$6ZLX~OpBFJ)&ZZDqxg@D zzx1QSOQ#}kshqLm-!J~sj|?xJ3LH9+;hNQA&h8IKxCkhqz%Lex4`2MH|2n*MD(148 ze7x0a<)bb-)2eoTbrBxIAG-KUKRCQ}YWPR&Av+DW1mpx4n+w+4)!Kwr%%XV5@Y1Pp z4b_Pfo@ZQ)7<0#JK9Wwum%e>V@f`)Gc$;FsxsL(`z`xu3)jjv#BX>W!`AGv}J8Q39W7oDcZ`b5Ck5j*2-BsVH`k3l@s#}1m)%-wDbHn(k9KKOxQu#XHDs);t zk&3m53_L*8PuhQnqOuxyB(lI>k3ZiBUb|Cud)(2kGHtF!`v}&mfaC3iAla(tNGGXZ|*n1B;#|*fP~Hnfk-4wcg2o9;;-`pe1eMl$v({;0p;v% z7brRvvCd^Dd5H`8red)fDTb1Kq2;G4aXe8fPMr={kjDoi#AU74{Ka~Pi(6@16@D4! z(Jju$~5-_r5&%kub^ju&1Y-_r4tlF7GhyyRr^EgLUId3;O9OJ*V8#n6Blhv0vgZ9E9U#9g?v-#bSxt#yct`b=JBAb6pVS`^T2*_#eBD~n2%jC zUwjcCGG3eRzZop;-#g^-E$xrkLcZ}_pzmLc%6b#&KsTOni3xwQ)q&sdq6_)P(*kLH zk%fHY8A6|r4}}BHVwtLi>^5tJ@sV&Y9A3ybp0SzDAl^bYqFgPoxke%9phIc82v={J zg?v-_#!|k}LcXbd!`;q0c_u+bL4x3<%SjQgW&{`VO+Am6@&y+1P0m-KHbgF41MFKd zpLZeO)O@wnKmUsPXgJ9oA0mS!*B_?ZRxW2RB`RDb!L^(XXpUDu5hfWQ7XuQXD_00r zN`a;)BgR6NAO|NZ>N7CO_+S{2gsQD%9q-g#c-R&Js-G#i_^7_dpdoZ)<#EIK2pEu{ zQ_iWcg0C9ym$_Fx4!TM|K6?(Ynz{$|W1!pAkI!j?t4i4f5Id(5*+xtVR?;aPt~{w9 z4VxaH$cCn;?p%E(eAW1LHN0x-&ec~y^<4ec-Xk@Lf(d`k7EE=o53ejVsf1 z-CJ~*7h9XkOLop~|J8P2>$6)e@TUK=O+@=1ZDiw%8_(F-Uw{3&b?sAYE%1i_ zG7X}Bk2<3IqUssoM7~!k4T|?EZXdIy&W`=%=YRZp3;etVE%7M0nf+W5)4^*?ePDu-V3p=FDSz#?-Uh2imGRuNkLc*LpkGR#Qc>p1QWDT5@eR zRZSf~Mx(gBF~`pM!*#(;!LXj49~lrHy{Os7ldc68VAz~y(;q)-!QG5Mz8BbnVUu_B z$VGQE{;XeM0fx=F8=dfoMaCwBWM1GJVb~mFm~iDHW71{`BMh5k3>6-}z}Un>vH8t7 z1)FV5XA!PgWK3G%>R{L$V`kxDi;TgM^7NUrs1AnBF=i6>7a1E*au+ls44Y%jDC{jV z21m`DJA+|!j2VR8MaJOiV2&{uHpiG=*jZ!@j@CKGVAvdEI$?W}F*sW17=vMRjA6po zB4cp0&M^kV<`_eT%>~9VcudbWHU*n)3^NJZMaJMUJ;xXfn`6uqJ)HpdtyXcifR189yh7&gb4PEap029N1E#$ebS zW2m57WDL$Q=NN-wbBrN^a)Gfa&6e55reL#;p~l^f4=5h5{ zyS1Si^?-M5+`j%l>vyhiul;{(XEpE8xYZxu_>5XmeOL8-)m6$rR^F=kz{K1C#s}2T zyaca1Kd1b83;etVB(y-uyr#J0WPi>CF;Oj^pFW~fxH&Tksw1-2jk}1GnaG=?MX}U&?!Zt zdG<^tY@t0kd7F2CFm9ebV+mbo4^FA(*@JQO?CDF$LVNHypJxxo&9jGrDd91*riGGa zksnh_wdVLSg`49Csu!NLAb&B#%{SZJ6l}INvtBq@WDHIw=NN-wbByVPCoVDuchnqX zFl>%7Ot@~5F*tLbV+@APF@_3f78o;)e+HZXsF;GyHfFL2*Df+9ZJsf~usOy|!ZnMG z!7pvIo6!Wr<`^>wPgrD3+R!(_usO!`rK3f0Jtbr|`_`rf&t{tgIN|06H)oMHMy6o1 ztywI>;UZ&j=g+=33k;iM%p}~j$Qa!DbBw{TImQga*+s_Ssc4Qd7&gb4PPlQAF?e>E zV+@APF@_2^EHE}D%`)5A6l}IJvqiXmkukXE=NN-wbBvjUCoeJvXCQNo!LT{TjKVK0 zG6oN`ImTev9Ak#w_x~+LM6oCC{MWX3^PSqW>n~l~RJWC10KdHGpYwZu?d|t)XRaaE zX8Si4nqJ)6j$`d+9CILC)Y71w{Xf?@r=^?3MZFKVw(NOp%gfMZ)){FeorNM7jdUm{ z*ajC6TV9-K#j%1wIg^D%&T64>!VNgP@hHICZ77dLI z*SgYPCex<7)|Q76biU#|+iaeT1~N&q&|*WthSQOcW05Z3>e;7&|2_MLW}-+JdUm{c zw(aZM+6gS{W=pQ1CtvpQ)*jYX1G7{hx$|1!nJ3wrBn4=Zn4%nCH|IM zNQUtrAvf#w%Hj*(_U~uU@A;N=2s~geqYgn=)gb|r4TZ6CIb6ur^y&ciCxi^;@xJ~;b8#^c=OF9HD6kJ9f(ppuA z1jq?TGDJc|xW&zi5PySX;zkG9&u?ZU<+7b2G9+EbszmZQ+NdD$LNQSf z@rR^?^xHEw7jE;=)-Vy2+#%0ap5OB<=@2+Cxr{oby{Zmj!z`6x99|z2&j(9!gtO}r zepZLLd|0JcLr}9v6hv#JgLUI%Gv>7W1fRiBV-A@v8t$O>B+_u$Ghxb#y9uw~X_ed| z4eI=!dvS+M={H_J9g@+nszajApy2J4jqy-9=L?}F3d=ep{R9A-vaDujFcdZjKBLD# za5_B2#hdk_(M}r(9TSZ^NweSOb9=~w&TY>)30ue;^8|59a)(r2bAHdYq(k7BvdgGL z46Evpkb_Nu%G9C6 zaMX|s7}`_-6YUwFg9uR`ugyhD?hp=SS=5pafnPW-qYhbF+8d6FZFeDK@-o2^Utm+A zSh7IR>JX3BKo$+j2HLQYNRuk?UYjo8)(2dC5_Jo49nxf*&IS`DN@jb8_Q!0_7*0B+ zr@isL^Lyly4uN0FE~5^?Ruv&hhfoQJ+U-EMZuAu#sh}g93e0*Bp_)DqUC!z}hs8|W z-Y{^wh^-R0G(&E|u0sOHoS~XvjUnz(huAamAQ|+8h(J6lIYOTHiSv8TB^`44zK3L1 z_8vkzTm%nZ9z8@B_k`+t8n=b%Sr2tv+~GV%bhfsq;qizmvoYkR(urfYlgcHlj^jqW znrjP=O1R0>_6!AfVL6>{mWfF2kemLX_x^v4f>Z3D+xx-ZUAy1gy>sU~JDu%sZMU`_ z+G=b*xLMOapsj4&zag%FWxcrer8PlwpC+fiSDjIPUX@aQR>>Vr4yYJbM?SKr?XDJS>%XUD9DzX#@)?_;%LyB681Dd`A zGWu1H`m_`W)Uq8|@fc9ac3{P0Kq=dS6^{W$-vJp~@hct!=g-M@V6|Ov{%+Y0thNiz z-_>_OhIsy}yWknJ9awP}+$r0E6?Z|m?|_WSWz}8Kk?p{WyPz%GffaW_tM7me(p7gs zQ?>&u?t(_&fwJ+CG-soQ0v8EZ3`ujV$cm&!%jCGWwC9jNym zFzWRsqT=!9YeXXyu8DQhEHGVP?$Bq*9XqmlPbi&?5p1QDAmk|eFPHJZ`+QBd1Dq|M zH68l9CR^U;_aA#$Yuk+l5Hj5)JeZk?;Z}QG;5|i$t+w2O-uwTy;@1>=zrBlXe_$)A z{r<*ptsiOLr{Ts@Wba;y}nFkv*Fm{l#JvTQTJkg>*bPKOl;F@+`lNDyn7j=4h%Db#Qd3A*e7 z%1(i5(P^snqjrAgoOXtbql`7gumnj*JeN^BudGZO>lS=zAsc0)k#?|)8dIdz;+s{s z_S$16HdAkRig?7C5ld+U+HQN(M$Tk&9Fhr(&Qu81*>r^>L!h0LO|5b;p3lu`X85DP zg%uu`Q8TaX^T4H96?2+wjGFoGIn4}LmM^T^xQv>4Wi{qYv)1J_Ssyj?U2~clUbk{# zHO6Js%$NBJmeXWy)XdMA)6DQ{lnZMtE~93?%-5xyCaa@nzH?49!)r_~tem)vn)xzc zZE~8djGDPSyP2m}j$Bv|arrd!<-UgGG+7=sb7xL7!-?~SRSuU?Gp{UhzBDUCPLtxO zncH)k8P1z8d@8?;nt5e;^QBo{aGETQnz=Qnnc=+o!V;0osF_!mH(#1%HNE%$YZZ0H z{%`MZ?Y(Rd+kMw=aOXWc+U-|vKYZ)=w{F?|`%PB+b!~Fvn;S3KcT6nYHJeVk6=yb|^B>!BX=Q&cL<6 zx%~lA>^Ra9@i1hlvw>K{+&xaWDFeY$LZnzOy7TRtr(yJ${C_V zaNu3K;1iPJpgTdtVEtSvcRjh45VIU4{P9o{bMi=+O7nIjN$K(x)=IexHO^OI^Ldkr z<`4w0dQHMXAzdKiRx;-y^a*3L9Lc$k<3cD;nYlu#l5aS;xS6gX!8&uS2OYLK>ag1` zk?S(t$)9S0H@#zmmA6XvX5yUSObP9Dot=8(Fw)YIt#W~kiaxGu^m?1Yyv0GdT+L3* z?`|UDaQSot80(!Ath`0Cu^9&iW87h&MP&-1qAn>On)5EY!RA8slnO#XCj)Rbn)rY4RGHrU#Jcdp_A!mjNUZ>{ODK!MEq@p35|h>D)J4WXP&1&lav z4Gbd4^PM4Ctb? zuPK7KR}Jnur8JxYs~MSCIFoj;TpCX%DxMG|4l5<@;dj4-NVpQM>XWS^8w$m!Xh5IC zv5GZiYqUu$pA7~*CY(hR9;XmM4p2ih8$Wh|T}A~riY*e%zyyO^(GqX>S(!mL<&;_-r_K$lHU*p1i5sXqf&Vdv5~e zNOqM8=e}o^G2XZCs?PS7R%Pr~beZ)dz+5HX2Xl8bwRR<9RS2JNAr9@ih z(`?^N7=tF-gajf!qB}uF^84~0HCC#hj&BXIXL*d?%*W}ghqVw0vX1teCS4o>Z5;t&6K#E&V`1lLtBWX2^>heCPPFydvB)`XbrFqN zKONi}%9lD8wVx&(unQ5zw__!VwbZvO4&W1G+0_=yeQ`bXB|dv63;0R6Rt z)kjvJx$=4!uK?_%rw6BxY$;&ypYCP{?|9dKw^=}NZ2d^)z^9IhO>*y%flGo7Z> z!_(2E&(CzTe|NlNxoe;8PY_K-?km;PA;5!zPP^DAJH|8b;##KzAnEIYBp2Ib$CA$a zH|4Ys|z7zc*(tMpY2=VX`d^z zKD$_^9mARSxpS%j883Os%8`r6#^u3+vt7U3tjUIAG_{i4R5hdbkW~||ep&`1X)u*A zNZz5LY6C`!DI*$bhf`^uQ^cySv5A-fLMnqkl*yp&LWFTFvSby~uK{_5iEyCD#o3(V zy}~G*3^qD7WyqI2vamBzuP5Cq|lMX?LAd`%!~6r>)ah12~pv zYN|fENlU#L)sDtOT?x|CG^uyH$!;N7f}>ScLo(^6UeS$=V>st|rUH;>d8WMFBCv$t z{)1y{bwj4$uv6>eK)aR8zl;s8! zVaCg48fiG6Esbywua4nvi*8gpddbg7bgK-C`VifSQcK1h_v^0eW3TQvbqNn!aKJEM zYHaV!TyMFAJdk0zgs_Z3!2%Ye0tqdVPWqtHP#OoL1gC^D44zO&?TnE#>4XoWuCbwP zI4YG==5UWzkDIOkbu7Ra-#l#V97My%dfl3l44(%K!QOGXmiKs=Zlhs#EK5bq5nFbzt~ za4c`$N*h3qWu-3O95`N47I$Fv*$BFLVc=NOc_Ubxji8IS29D*;kDxjmK^HF!9Luq} zo+=P!xgHl;4~|9M)h6t(Bdq>g&;J{%&so|3?wuEGycPWMqbvk$2HX3rb&U_YCma z!`F{osONRiuA6L`3B3!Qy#6X6prb4O1%NL6!lZ%TsH1?cT|eji!vdshT5CE`4qgCS z42IGMV`#N_LC}!h2rQGV|aDFObTblvF5 zJq!QC)?iHjr_fMF7N6Cwrq`-~6a3%eUir><1w+yMie8w^FN}jb_Am%XF_@+RyDA&1 zVom%b*9}!haKdV-3>V?fafcYO%}8eu4-AZf$iYLZ?B}~u=S1~3DLzC8t4WfXKsIXC zw%jxISwl7;R@+J$!Eu;=NG>GGUd@sbXp2oj@D~R`Hg84)NBk`$R(%z{ z+VN8nUzl!6=~2-~a;R7Bb$L+tE#no3yikGSkqFPEawVoal*%Wj1~MV8zs4Fuvuz<# zEZqnCT~*HM`fFaZ{`&Qs4@`{j>8nRBRBE}ObZC6YSETW|SR~IgzO(DErKnH537&%k zC?zlf9jMK`8R7um<7kE`FvyF{u!l5Z6u1L4=QB88+Z+xD0fO$=r2t;@M#6N;I7Stt zR&GMEdp5(4*Uz5BL2y8*c!FtzMpum=$I?TM$cyPfC?Cv(Ayw{H{Gou5&3n-vDN^wU znN_P}DBvIT16Zj=w)zbd&C1B2>I;}%@z^_XWQJDPff>F|zj@s(##P4 ziZnwvi?p?F*w(;1+tM$O`Q4UpEd{l9zXn#gd8@-7U#mk16u!vn!a}2xj-RyDEL4)& zhB+Fegwi5QMwAGpB%*PQ`v$#YtWwTE_iS|)FB|V3x3Gc9l#8Wygc_W5G4Kkhb`mHy z{6sZdtF(D8<0DD3o{EH(RHuEMhr)!CY_N2uA1`u7y9!s0!O$BFA)`U6jb*%EM^4;t8O-Ymu7X)c~LIZk}$=-$}aNirTDb{-Nv3V@D>~+f8?G zPPd}qaR7oQn%6Z)0l3F;g@=%fH`?BKjEs-CWWH5Nn*M0MNlNilUKEv9_gK-Rp^#COq6B^nHH~}r zH3+Yk>ZL4(YUAK|RHb3K%ZkUk)?lO))tBLFmJ8=#469O+ zE)j`z#7<|_#Fzk*6?1HVlo|S*t?*XYfv>Ti|DUz;>XobFm3LfOJ!tK|BQ-6B%yEZ4A^V0^^8PJ9YyJM7XPs|)XU_ajAG}FfFwr|O*N-Ts5u2Z1*-6SobFS-MC%}vO}4CwSd(Q=4u7P#+t?d11IoI-s5P2%dx z?hN9tHRW!iC}s$!Z>^Ujw4lB5z1HtHU2_WXzMHsh40qgwU@VT|(unRI3*76zX#IZu zt1p0PkHTlNr#n%ja^qn-r5ZZ%nBVvV7R=WOPGLTH6S7VJ`jLx+yBW;+4a@TJoOsOf!xqp-M5jRS zyLn;(UGSJL!o_DmXC8CuSWZ0V@QoJ4;omuh`23qU>>kraz|IWe%wsM^XhAc7!}{Gn zatiRln;zR_Z@CHgnIT-T^{_V>_sM$5%;rteUmb|{yuXR9w?Ab8)!uME&@(|Lr^h#6 zW&vHWbr;b>Goa_#`ck+PTW@^Ug4_BUr*I#*d2GR5uyq%~Ml-l`w!ReR#MWzh3ugU- z%bCuO>Vr35YMcJ`BNwqsGnfmuz7*)h)+-;dfL4FY>3H6E^CcG01zUF!=`;g6v-PDA zC$?VhSrC=yIEDE9n=iK8x{Kha8N!*ZFGXlUlTRnVf6pnv`)y< z|6vQR@F}NoAGrB|1$V(?x_C7`gFELjm%^NQ%=l#b6SPy958k|PoBnk-@A76a7d+-t zpc9YDjIHqu{(?iGd*C@vq0jhuFEY{a4%X-~PVs&USeF{;e-;eQ4|L zTQ|4zTgcYl=0`W*yZK$4^-bUAb2k2A<2N^cXk)yQ+<586+WH@_|MdDB){E;8tv_|` zlWQMX`}b@5T4e3|>OZai?&{lDZ?5K7k<~qL3&DjytM9(>t0%u$Isz|R7r+~a$Q+uZo>KQ7W?2u0^uMrG`D|8RzmO8GnEuAT1h7wJ%hB9+w0Y}@HRy-0^3 z6cHWdVo^KY-!0O?2!#&~z9`x0{&s;52mQdFYT2kmr!zDS27B%TRm`7`7EXNz5VW%T#wd5aH?Q-=+It(GGaWk&u?R4EmIus$u{3tLI?R1?* zIs_pIf3B<8<0*HwMLH0DikG@$y+8-_lt6Wvk!3rbxJU=^A!>4zI763RqyzZi zRHJluTry|qpmJl>Ju}YfMLHP4=|;O)KQqp$MLGz;hNd-+DBz)eUXktaH!rIibK2HwS`eZ2`b&p3&sK!7?st<)-VJk zQy?Zhxj+X&J1dKHD1s3sDODg4)RLGq(R}kq$u+T2Y4?yY6lOe`n|@ zq2W!Q8J90C(!mHqwp4KSnY|yMU!;S8u?OP1yeIf)>Ce z=}Tn7i@inY?awT>Mu6_NE6r}tj&6P>M@SeL5!4sOVBAhOzmfww6oKh*NywcUmHCw% z&>;v+MTK#$Xm4+RC7;rfZE(Yt-Lq`ZujGIZMqnaZW|%XhJin3yItYPrSx%1ao^Sha z7Df-Eh+;tZE?O=ay-zKS9z-B84DMuR&*;iuFVf)%3~>WRTAU3C+WzDs9fm-3L>RQv z_EGu789Hjj)x<@7puPG12w3}21R_UWBV_k{+w=PoKnEt5@UfC%FGI6EzaIf~AWju8 z4y%cr9pC(Z1R-b|fzV1gSU-brem?@}Cw0VBt0_Z@fCXomm;u$TS z-?sod6l_V}g=D^YWz+gHIAXIRa0Pz^xH@ZRO^RtU$eQdwBJ7 z6PRsL>Q+4Dc_|{q*+4xjp466h)10Jq@ z3DoG9i_uhBt5)+GQ{wW)s5hix_Z%ix40P%lIfM?5W8f-ZnuP|v{y;{(xeVOMRU3L` zEc!Xh8>wZRWwX&R^jvIM4+Tzq9hEx98)A#W*;F;!NS$N|ePHI;oD%Xm3rV%QZiS=< zZoY65lKRY5H)la^LsDVd(y()qq42Ys{`ewtjH>(=p#MNzI9ou)jR1*%@RlK4G{wRMFcqyTncAm7PDkrw4N zxU^vEO~Btao6ULiwvwTinD+(89p}Nm*Q_j@fG*6+D*6W5nwW;jY#15CSiP1+>B6QqI>Dejol?%4~ZDku3|Hx%gkIu*od@wrHsuKj!BkLvXZR!xk0d4jO5wkFh)Vg!%jt~kvv{Q!2L9Wqrmi8*L4NVW=Sy$ zx6-}{%=gd#EAF-d4-`$VQ zg2FG)lUVs&Uw%)5(WsTWcJ6+nG#r$@X4=;n8Dd9GC*d>}LW_I@)G=X?Yr_`NthC0> zGQ}oK_v}fIvp%|CNeNMdkt>lg?0Z+(?FHK7a#ug;nyDg+5x}+NBhOSzuxQZ{_F1RcCYSy-_Et|w{N3cKflFn z{`RK0@n;*=^}kymtbJ+i^=nsG-@1AqXyWnwF;907Uv;{7XN4fqk?IPUQ9DSE;&ROvDmA1LT{fgftJkj!sR%M6UiIC7 z{p058jw8ADDWAzjPDWL>G}6+HqBCi6(2w__fYOq%X67VP)4Y8z$zXCCs(YzA0}7UR zqJNUlBjeI~c#%fK;;?UjPDMuwxi)ufqW6AJBbV189< z;dm{hvJwTer9>@OmOB+8RJX<%$X$0N7dl^`(~@RZJ4Lj55~y>blJ3`nUR7b_3=d0z zlbBKB(^WP~#E`JkY{^Kw*YskcsoXUua$)KMxtT~v?(2H1)EQN@6K~)~9P%p>q*o6O zC_O;=I}qY;XS-CC$N5r*j`RZo6Ucp?dAjOIuKg*0HqL53&qs;8#$v{elcHFzP~E$=&lu(~Kt>~2A_J;OagaIeTXG*YzsZr<3*1J$Sj=?Ta)}rGYM=E| z(UXRpy^-J}xpWC6em6y}u6Ppxg9s~n2UDv939Y4o*w>hkIud( zH0Lua1gV(=HKL_6anx5gJBmq$6W+dAD{5vjre(SjOPdYzHIBqy;H>iTG>s2K+DRoP zX9gi~4&SN7mF9qq^akM|f1_&B2I!rJ`0T`uQj*0`G_O27r3gTg2u{p zGMFK1-D;vGC@{$WyTLbEqtzJKnxaCBdR&lv-W#ga*9fYNbTG?RVdmJ8*b6*HM}}Jz z-z6z;yke3%pZ5tOX)<&-602t`e!_$eI)yb@JQZ#ENBwk+>4Yq?511n-VxbERH_qi5 zo=T1Cu?Xx>Q+z7HRY=oYDd*!*N|%XXI;?v2ct+(qq#tBKvT4p|sni4J(2>{+{8TMj z-;3ZOzxZ!T7Nnhc)8~sHp_-kp_itWbg)$j7mtG40VCn=bHmZVlQwP z$w+Y6VN(JqAs3HTk})$#MbShQB2`X~H#+_S(IfFd43lz2Eo?wyAf>|Aa{qj@??~(g z?xKhhi5|sQ^KhN+JD5O4WHX*-sRiQPbK~J=N?v z5_^FGrN_BUAc6KEwT7fd4V-5a)Tu(tYwUigykGJ z&8{P{7i?_ka?cx$pxw$)uf^$Di&OknCE;tu=zcX4K--xNLXOH|e*+#wGNp`&$>Y4W zk=yKb4j*2+Pp2>N7VUEWhQAkSaJdBTL!%7UXJVBb{$@CdS27Ll#OF6U=t-w<#0Fd% zq4fr3kidRE`Nf{*MCt|R(+qPRC|s*_@isq{{GHxVBp8LL;306%T|zi6ix5&IO^aqU z-n-!q3S@!>GyBQU_1aFPUf?SRAOtHOZDu%>E&5(#$cS3=N zY5E|=i$oQw((bY;*-!VtQv1n|^%_p3USK+s2W#J&W=PTMOlA}^hGoHTM1c-}@=tqpCsHr49JrYdYq+nZ6**-D8C)cJ5>6;s$MjL9 zZWrv+bPe>mX~MdWl}3ZyAY0X~dHvH~&4JW27p2ZNs;rj@YsQW7jeLma`yHU9sR4O| zkC=Ij0Pui`+Zwg^`=X=?T4yl)3XyvRvdap7#d&Y&MYv zYyMR0i8-r22gjGA5*Y@;m~}FG?h9ky3x~47D3%Hp*_Joy&$NR$!NrZVH=wne=zu7e zD&}$3>pRZe^9$n`7Kr%31DfJhSiGH81L1JKKj@$=PMmOQxzRm7DO2N86>O0*;9$Cj zrKIBo&Tvtv)k`HbsM@CMCuyZPqE2GbzG`;r5bt+Xr)1J~@SYM>B73Q2hE^pq7s_=> zW{A-ASEL!b@e}8SiQImvZhU#n@3wr~OZ{7AI3MRqWHd%07g-(E$11_F>gR&O31<#5 zxZTxZ^+fIDj+tCv8zQ-Caa2jQ{2GX}0FB&B8P1{*XVa>f4G$YW3eN^|`LfuTJLZ@x zonWPSu83qgvIOU_pi0%6p;9hOz(c7@CWv%XX!U$BTI-C`LsClkV{tT^9CI8Ja2EDq zb=?a4P;b6u67~Tqk-5;NOS3xaJgak&EqytQncLOSc8WEh#QEj1qotL|?9XGs-kz*P z_Jmd6FMI@>%LK-Dt`B*-w zpTL}7z7qb7E?_@*UyY`EXUXVNTY1CE7O~UW{OP?v-n_B#_Kg>=|IqpiR^G6BW$k@ezwyc!*1lo&i>p7m zM_n;@|IhCC9{&B_w;i^2nVrvUf8D_+w%)t_frHBaNB6(s-}-cay0)b#!p_@mM{jMI{gC>BXc9lp%wKAr5DoUdl;kaUS#Ojm)QUmg6v&D{0SUnbE^^*c zLG6p`kDXm)4*5CaBQip+q$R;~{?wG+umiKtN(dU|7IL%ID&a~NWS z6oXfLYzdEYCB!hfQmt9>Wm3bAmsLq5p~bue96-vkBhiZ&D&BlDp6NpIZ~>SI$j8N(rhoGGS`ZVFDGafEm0Z9fnFPC&xT?1Kit94kLw1AB~7QA!xB$1 zts2a$xl&RKj^aIUBPb3?rk=qVv&9@yMK4}_=7MrfA_Y>*r{cA?UXXiTVO&V} z$igV@!#L_%F`mk$(sDTwQ(0Z=#K!j7+Irs|3`RUc$rWZqHA0S6*Vp#SGz)Aw~fR4AKt;xCAz(Uk4=kkmF*eL-oVG?&~y(H#AqPfAR}xj9e`4; zzQ1~eqex6>bt@IG*cBRF21ijA3Q)PYXFN($>9HgleS!ihz~G9mSaaO!Wu#O-QS*(w z`Et~rpSJcbcQCX$DpbHcB`#j3Nd|?4jN&7t63>84!y*;qVwqCIGfsfQ3P+N`L_NC5 z@gi5vwP1WS4)p|mXkv9=F-b+VTD21nsUFc#g=;a8Opp@AP+2UcYW;YYv*)U<(|0hm zlwr^xPwJgqIN1yn^-i8f$hd$v`x@l;Mv)Q)`*5tw@YN%}iz~X9M>{eOR}*QIZp8g+ zHznhE!XQ2EFw_;aq7lkx+JkGErpZfODA!}#(LlPM9CvJUv0u530rPg%DCa5juvjrj zhJot}l1Qe4e(;(K@_QmM$hvIwM@iG$ID(O}CspqnzEZMGf;7u=D>CTy7%mpW{E1ej zEsk4+w?RiEDeaovX7p$;#~Rrv!x;MbOo6(C-?@Wp>(aDRhpXCU^~4+ zDVQwE{Z3XL#wt)JXfTbV*r3)YJdIk~EK9LCUQ}7UIfkMVLm*k)6AZChy95HVbheCL zYswjB*fYi%S@U=D+KB1e#$oqucQ7#VVjp2ktwx(j8KWT)Ej4nL9yR1T`8rfmYl=d# zBUtkkr6UoP6sE?0Q4LjP1sT-B$zG}2%Og@M(O{}sc+mF6ik)&n?^Lgu#a6m3fB++1 z1<_m?E@0Qm-R}bH)k`l}vuq#VMvJyuNi#k6jA~ddfCY1^&}NE_YOI6=v1E=dw#Fk| z8C63brh0@WX-_^YntdK?7eb_w4l^NELefY;fVi@ ziBbZPT2v{VjfZ56RC2n2R)%H1nrb$PQl8TUB*`3+X@5c}X4GOxEs^cAp|{!r8S5p9 zhF0eNEF?s^c!DZ~njP=8fzi*UBRS2?Hkf?LA2jSc#f|OL)ydv|^$24Mecw=x=V^i` zaJ@KSnO-SSqzf=ck%PQf(3_1)Q)6k;dlVcA)od$)clm)H$Q9G=z>rmQ5D5Na%O-7I zrKv_co*XjFc(27(5iBXvnBkKH?=UI!Q?|J%-x&rk7w5oswqq6xWFbjYU}wVnoAoYD zMZ5uh(8?FVRw!EOdc8;0Mj{u`Yan(dtb!LESi9Pk_4d$%mV8Lp+opMl@?@i!D01wz zc)T_mr`2$**4KxsR)Sx4R(^F-C zA`pw3MnBloI(m!frMmuPmkNjBY_rsBH;fulrwiD%9NTS;*8eH1Kwv2ID4^?$em#TwPBj0I$Mvc z7%Y?{DMJx6<$6I0N5UPP9WyG=R3j>c1!G6`P~BI;Yh;0pkM%emXoQWrFiK%%9b7{a zESL#@I@Ku#>sg+?*76CRkj&QPGGj)4xk|$Rw7Gp}Cx=0GSRbYv-b69f;c784D|ZS= zCKrv5sdCKAhuYbsn)53STsz{G$e=GK5Qss$1TsV_6eQOvwaQlbShj+NO(xH3jnp{Q zja_T^%t5xC8CJ$|R%N4-Y=6R8{gie7e{JO(SFZN1?p^tzE6Cvo56cH1K4>3o?SJq7 z%l3W%RP%es?$OSBcaqy5-5zdzYU|aT|FYTH+}e2C#*Ot~S*O;1V(tFb?_S*jO}PF! z-MRUU$z6ymM^r1E^!Funh-0lj2`lZ681W#DE;H1SKsY!|p}sWYGrCDgx|VE}Re!w? z{0-L{vEjOGFC088o;ZDfb$+G*J-?(-6hZ?mPoH6F;W2P28AZqWUNo-fm||r}=4&YM zaP<=K$a0`iRYCX}D$1G14a?JJSUj5GkB9Xz2u&5rEKq#f+a*9Gl2*<2y+O1TS8`Oa z(URkN<8j0C)iW%k7LwrP;2@pT34^OfVzkExvwEjqEMVbmj`3imw^gb!yum(hSe`n= z;zgKXGo!XWcrp`j7?FIGRs+&7RyIPxJdu$4SetAz#WaaNe){rNGb~)j6CwtNnKxKwXG1wgUxe`ULTFjwQVARx5C^O{<$ z!Q_Z9vmFVnmP1f(hX!k{nQyaW=c+qjg0^+@Dv)H|pqtDY6pr&k+lY4aXtrDR$Lbxu z#&E67pc8CV8WJvgauqnCQjR-)HrhhDc?HO`ZqOZgKsiaBZgq>|@gOOE?Be*g&l%mEPfqK8+GnARv*$!eYy-pdO)lNuK)Lly__yOC{mTG9H@ z;7o#p!_v^XsJr^S`K`KjWxITH-+}^0S5dPmITV)%v3%Rx?p29)G1<@gW5EGqAkVT{7uWnamN##+o3jygaZit9IX0W!0HQ3Lh4p%BkN`Ry1=)Jh zoM0qOwk9EgNIxA>xItoskJSt&b>qC|Skzr@cK*fr)MnQ$C}4C$L899*{d@@#IUI@m ztC8p^-ZgpC3>mynif8>`^8ikg5`E`TZrdz4i<$H{o9T)=Rt676sZ@>?1@Nv0JoqR+ za~MYIYBI@@fjnH~LJa|0etU-#JDGoxZ}k`W5Af_b9eZ3|6+md(~OEwW$dNkYW5dIabCa8V0n;IvscL8!Bd zo5hr;12(B}U2_a&!Ddgt2}rVRHm`Y!JSU{3Y%&8G{(w2I(UqZT29OY{8f?%PsI|Zi zp~6tlIF@vd&7M9Ayd9d-{z)lx4Fn?s{=7>aI3x zj|`VjAF-f-(RFdDi(@ELo2{OXXCupIaW2}5OKdnp;YLM_ zdrgxrjIYYLay!t@&i0>gzkB=5+v@f!wx6~2>8%fL{lM06 zi{EcJ}xp0)q!{SWT{!2WQb-+%G` z%HD_f-m&+sd#~Q3_MWo)@s01>=xl^H?qC1H`iFMkzx#c=o!#*6{X1XS`Owaf?tJsw zpRc`p?agcI+AG$cwfboQ`^lf#2w3$VuZ2>>{xGieJO0|B74(NkhVAv=?xb4zwNNNG z;=<)@uB!@lKG~P;)k`ehk9nv)%($yW()h2WyA0LvqKqFW38sTcAV%pz^@4v|*-5VX!y}=>f zw>hNyR)=)2pU?%_gxMc=Vyea@&30Cj>{XA~C$*)oEvK8*mcADBO5M)57FNL3WL1Ai zuGy;vZ%nE(JD{6XWp+R}smkntZc>%m0o|l3vje(GRb~ftld8<%iDnennq@ziDF?&j zTspwp{L9wUC-bqK?r9F`4js}RIHcQmNVn&ZZgSti@+ZC3ubs8G?b<9Ma(vy5;N9e{o3nQ!~1>*d9kJeO~ZYg>HbgS7lzgwazS~yS2_Nqr0`v zETfyQGojEjx?BBSM(4D@KRFqfV56yKO+VYNREmM2*%tdY``wt%?{d27{4S@P&hK(M z%ppG1Asyn74t7WfIix#rNO!{_oo7l{jHGIUmha^o;QVDg7S2Qg{hUL(>Ao(ww7u!R zZaLj_U$>lYYH!QwruMd+ZfbAK>1O-7rTv}S+w%6N_7>zzLP=HP3W&Q7_FIYenMlFu zK4&@Ibf2@FZnn=^D))6$Jy=TjT8DIxIi&j*hjcd`(w#b_`{pU#(*57$W)=tgzt1~t z?{g06KI@R~GY;wg(IMSGIHddgz17M}ZsijzS029NzjA!#s}KMA@MDL+b@(%f|KYH9 zNFHt;{NBM29%u(IKiCHq0e@r_+xVKb(OPKj=~soT_wAee{{64o`?I~D+JSjzj)u71<8)o&lu+SJ}fU+iA=#m+@vY)`+KKUJE_ z*t+P8&5OR+n2wbTGpJ0PI%`p%zvzq4O=Y;S4{gTbIE85M7pEM~6Fjc|m}`z7bzd;^GY-eeB+q!+l%r;(qgmB#4#iqFkq<>8Iu%s# zpw9Urv+F5Aekzd)Vz@sTyYh9eIbQCX<7=lJ)f_}HYMqS;k%BLxfrup#rV4xM*eIn@ zRU!cm{grrQzxkg6(L>a)9*EoQV(_cq@03Sd0Q-s%7?j*d7ptpkoG%E&FjD1>k$`OeegF&*?jqn19-fB>O zOb%+^oOi?`5sl zm>YHLonW#n$H)C7*Vvn_gDyB`>!1sc|2#z%LOt*h>!i6-6Nji0D@BAA_bqu z7Ykmt7!fHV#cq7wHOFjU;zGvkp&>|*I|!OiC3ruVGeS(Z1LqXRQ)!ek(K32o+A9@Iz=EDOBZ@Abt<<~gr$&GGdU4$OHb#LC;J+vT&*98aAJcd=b=PwW5v+{(eX z?0)~&1FJuGc4D6V|fUKy50> zs<(2h2%M#&ODf4&!+wt?Vlb4t_^uu`8ETh?x}x~<(Qu^N(3*HDNspt)S}4YV(Dq`H z#ku5hiYY?(Tq0(WVjGDrhBbX;C*EdgT1sRBCrph^c21C%3J+A(9G61)F%jWXltgKL z1Z7lee9Tcu(Bvb@K|pPf8{o$6ZdF1XLZYn40?tY=T3w$OX1Z~!Fp~?5TOPpZ(AS{n z6=rhdYtAjq(yR_SYjx+?k@bIlI{HaXC>Qf+r8>?NK`)sW2%6(g#$GnU8~41v z6g!%i@iml^chv1w!^Z&@ToBpF!!=AeVX%Nc%Js3jzY~r?c`g<&#Z#lA8WQ?OqJs@j z>SP-qGyY0bEgSKMtmJ5c=P|F(nWkG^x7L@@&6io4{-#w>Vf!+J*KO;|`L`&e5PVkC zue+&sYPr-WzQVSP;QR9`Y|lsZ+gGtCrndG10#H@EB)1DzH2y@BZ*A@fFt(55<_@Bv z3+w;k5SJME@=4h@6ccER&NECa-Ve8sue|2)Uk)ER_@{%>{ulNKd!O4g zcR#ah?EJ${cl*=Z+ScE0wKhMs+1U8xMs5A$>(#ZtT2og4VzspLQNVuj&!dk)8=$VA zo8-gG&9Zy+u@lz-E;YCA(Z_DM25_kidkS9S8o(t_!HXvVTx8H*Zi?%pkG*IDz(qXsQFwhxKx^Hy?fM0oMR7H6!-X$F92uaLGDw%{72a)`9yc09>R{U$ze1 z=NiBzPr(aZ1GwZVc>V-{i`??do`UDO25`w!@LbmbE_n)`GXdbjxm@-XJli#ZOP+#f zxdw2_Q}E0Q02hs3_7ptBHGoT=f~QXaxOl05xvwH0ee7uy050D8TyDnvqmO;HYXB~8 zbXZKeeMmM5^D|BqMRvvT#e|ZYeaz1JmHJ>}J|NmE2@~S|ekr7! z`j#=ym-n?8W5H-LniiA6b{0ir&2c`2M)kd*~srF>03{dI;Q^ z%b!3uFibi1E|W7~vDT8q1!Gw$63vUECFdPK=19)<(^qW353aldo)3?9M{@X6>NfQGYSVVrvznB?-cggvDY(~fR+WRmMTQ|Uk_!pB zpap&Jrya@RPqEw3?f%UU{rc-1HL0AU%b?rCQ5Y#77Xy)KG!CG@_v6kS6@QA{hHjrm z8}#@7>#sV54ljdl_ln6B6HaDQ!Hng}-upk+9m(NOq1({y(`bkO5ogN-e|mDeN&E2H zoBZI9J3>jE-q-+3;swhgZukA(u;}&1n5Yz2PCZM85x4u*Tq+v$`GR3@)*8m2_;rT> zUv@Wub_W>DrGjCO;eu)9^mrLSy9AEdLorLv z3n~uf94(Q9pK(Z{m`d}ZKxQK6jIt}T;ex1T8Q=d(;8B84qEA91$dQcmw*CdaPV0WOuyaN)FH zu;je=KRFz?`vb+6#lyj%7|2+1e(F0N%6Y*uId*+Za2X~H?jOon{@^`t;T+0& z{t`K;eT9ewxo{{Kj^WpeDRQ92(C z3jSd2;MJv{Q@5qCKHSPkwbvb zx*I^dadX~8KJUwjey>&kFStrt&;Q?XHFNcOs~^4cu`6%C(!D|*e(CVn4!`{{efZpi zj~@KkLFa(n5%<5e|7-i-zMtNI?%qfDer&I^$FKev$ov2N?)!G%wyW$iyD!|`-ud{> zZ|uBz=LOq;x&0H{#`Z(ot833)OM_a1|86V0_59V&=3i|7_@=%|Z?0_o`o?!`WHz3+ z{;~D9uXop}RdVf1YrnSk?RR@$@PEm7)Q0kGkeuT@UBysk1ShWi*^JS7M#+rv_hyWk zw;w2Fg<6~=yBRFel?P>iutqc^l*w_m+K}`%s{yen;EfsMZ%i4HY(CDyP}d_MsUED1 zlO=-igm9*XR0ntwE#vW_C*RAds0Un^IDz`;OrrAy?-^rl#^}6$z>E=|F*;8#o-%H? zW{l1=fTxTbnHi(=wB9M>+O6quo_RZET>s&jMCS>(Gl~Cli_vk8?3D4~du}m0E}SrB zys|fAY#N?$Cs>V%zG#(^B2*!wWGW$WD<>+~pm?v(fop0|To=XIOyb*TjLwsDr;OY3 zjL~`i?38geK4WyA;yPvAeB+Fsnj^Ynq^;9ZZxS9$P*nA zB(BXQ-gk@9ar)|%aqHILIL}F)F}`Ic(RteFlyU!+Ge&V}gxOrrn=|BSv(iKQYQLbc z47gdUgTp~zs~XdCc_z#ZjVa?cH)Bi@jIZ^(s;O{t~&vTVO6dr65Ui&WR} zj7lQ7m1M^FA7_jL=9R>zUM!}3nVzTH_LzFLm#l~4=D4r>7>NwmL*Ay0b2H7^`QnT* zHVg`cfZ+m*XtlvW?GI}aykFu_w5bc>ni9~$xs)7;;F+n!jqkq2n51jo+#sNc1vy&H zX9M8EMa}f&auGg;bt;izuw+t5F5>adpuT3t=sdM{%DB5dV|1RGI%V9f&KN;-jiz@E zomMHXlJAoK5uxPz4I~4>15Iiqq9_Tjp6FmTX3Dtt-)}KG&TO4Bu8n7m&XZ54jH^E| zV+;hmL!lu>B9&OL3bJa8WK*wWzE-B_k$fIcI%1CGP8C$dVyBGzk4za6volQ9s;xkb zP{}aNRJ0uF=^`xw$wNswmJAA%P{{=_A2gee!!OMwdaE$n#B$A?+!RJ}f3#F+lDT}n z7$|q-fgAxtluBZSOuR7DoXtmGumM{uvOQgP3h3Z-=X`Bt!Op%;pYyeq+&cUE^Jl+C zNrbrd^=I$fKrPwxkDimNDpU5uomr=-a=ufhVJ|FnM@XFN1qLWvALx~GG+P-J1g}qO zm3^3C)@OFKR-83FzbBt-_~2h>4U?M6`Pq;Vg+M9ErF&i7><0s}Jh;>)&zAEw(-WySJ4$eD zR;OLvkIx!jn1{K6{>NFvh=DaKxH!r*!u}4?^NIB;)<})mDykVGUW$6tddD;52hvP2 z)*`nUTk%kl(~lF355>GE5vY_MNeQVVVoN%VZ-2%Jmnkidg~tAD zw~;M-iBd8e8b(DLHAh-o9*v1kMJNg$Jtj>V*WY=IF^gg`PelMF9GDzV29tds$94*8 zgh5k-#;_!dGEI^J7!_uWpPMmyQmJ-QE`e(rB1YC59LMnZ7$>=&5gdU#o#eVFmdOqB zNpSzklyQfdG9rUf5nRGTg!q01+_u~G){stH%+@;TU>1r?rY|4!=)wLNjm)-cJH#zU zIv;PM7~5<&hb=h7r8GZ_XUw4la%CE!Fx^D)VGK=-pxNed;|EEwk6k@N`Vo3u>UZln z1Lc$LN&(3h`>-&M!jXEn9>&-*ilo6kb0ZYofNX6JcV_~7BHOg?hp0edxaui{`}s(9 zq;WBdj1j?vD4>j|=1T_4*_oeS`?(pT?ny`-f_Rchz8~cvJ`wljDYRHJJb?<(tX!dz zQ1zmx)|esP{Dm1~rxQ#hOdp&m4L}KpN->6dkU)wr;9fec7KlKgD<-psnUiOVvp%yC zqyz3gW)K?BHv?IItO$8eqmpK;l};?LXNvw%H`MlZ1%7DERC)cEXA-qmU{p=_2!BQ> zjML?`M-BRYh(A{{_y97h6=aVJrjS4iv)NdG-i)!Wpb@;TLXzK?AQD8t7pafLb}9*> zJtz@se;X{1PR)XSx! zt(mH>HEuCV1~qPiOY73f<60n+N_dM^o&y(qsWW%G^`RMKwNhcnqSvG1p_0sm86+GsXhA#yC||x4^g&_N@U&P3>RJeT0h z!?t8d%s56Xl^EE`&8B1BZ=L_IZ@zBj;0?RhKTrNVIRa0Pz>_2JWBLGq$ky|N{ zE&`)3E!memF9p&?VB{nP(nW0Igx*Ei;rZo1T*NQCO@Vx3(v_zH0&=OO7zQY7@nuPY z#7WAkpmA=dJt&;tr7KdUl4EnhHo*^qBc?AD<#uxTS=Kp8I0{C*X61LuJr|f!Z-;*ppq75bseNYzJC2C zJTbnfuU_tTYqtF3i)!f*Uy;V=B69sa<2!rLm*o?l_*xq0-~fgQ_TxHGoB1*TR+9eX zn2C%-IC+s7awsV%gqX~YB_Z0E6%7YCk zdhdPdp-S&fx&;KJbGh7mxo|-c1QnI0AVmQMI|!np{@*!wFYIJzvI!sP_dWm1eV(7= z=A4-`@4W52<-D3`K-saEf>lS(;`jSo)r2+aDR$;o1PP96JvPazz#7^?qI7Q;~*>7!8w>kHQL>G5~}o#OY& z^U3BfTWVz+NlK|xeX*rZr6R}(1GJ5wS6i!1&Kh!FyE$0SnA|#3t*z9Ws!=5Q=5 z*~hZK%HEUxUiKT=uVk;vekOZP_9NN%Wsl1qk*$@jlHD!4U3RnVCfT*JD`l6Ul>%Rt zohCa;wjgWDDzdyRC5y_Ak_Bb6vKg6OW|AEy)5}z{N!bCiy=A+}c9eZtwxtZ0@nvl3 zKcyc@-1)!TOP`lMD}7q}gmi;+o%8|eJ<{(;Z;^godY$wt>7~*Oq-RS{ zm!2#=PTE525em|@G$uV-8j|{?E~!ImmL4uWSgMvzO81xUDcxDRt#nJNSjt7K7XBuA zU-Em&uO)9tUY0yB`H|!)$zzg-Bo9dLmfR-!rsR6bRgz24x`s0)r$~;IG$mzp&Ouyq zv?M6;N~R?i$>9>cL@Ak&>?_$_vZG`hiA*AtjN$*p|BC+^|1JI&{u=%>bSA8N#Mkj_@hk9)@N@9f@ssc_UdM}g8js>f;(pwXJ8%;^KS7HV_&B~dzAL^x z{v}+3^Knf45Ag@$KZ@TG|4RG|@r&Xgi=P%hE`C`2p!i<#cf>b~Zxnw`e3|$H@mIyC zicb)?#Z_@$oD|QC!(yL!Mr;!s#9tAs#gpRw#e0f(7H=!wQY;p8MgJE4P4vF#_o82m z-VnVkdS3J+(Nm(wL=TA`5Zx`hP4rFC^`fgpmx#_2ohdp6o!Zb8l|@-mTy(T3DDsM? zMHbQFBE3i{nh@ADVP_81wO%yz$P#Vz9LWyCI$No_7v7B z!GD?mJpV`hr}&TYAL2j2zngy>|C{{l`B$O+E9ddg6YH_wuLt z7XIOUJzvS6;P1=doxdZ08@`M$%41uSMV<4orCtooW$$$>bxQ^jZU{XlIQ2Sc@Cb5cPLNGBY5Myy?MLxw` zC*kpV822CC54eBizQg?$_ZQq3xj#lHVLZ-#nEN32Uha3eH*;^~evNw>_X6%$xuKW}+&nkQo#%$RKJE9%9`Mh=KLURb{2lPOz;}Va0lp1<3-~7RSHL%buK`~L{sQ<4 z@aMpnfG+}H06qu&3Gm0jXMsNi{s8z4@M+*vz$by<13m%#F7PqnqreTohkYk&^| zR|8i8?*-liyc>8I@DAYZz}tYg0>2Hs8Td`$H-KLU-Uz%NcpdOs;Magx1Fr&J3A_w= z3GiazMZgPz7XZ%(o&!7^cn0uv;Ay~9fj!_Uz>|R|0Z#;;06ZRe9Iy>+0h_=Eunw#N zE5H)42rK||z$`EYj00o9DDW8IQNSaC5nvb?1O|XU;4IJ!bOT+$Y2Xyl0ki=vKr_$; zGy;zR9tJ!VcnI(-Ks`_k)Bw>TI;aFGfdo(voCHn)$AJd`_Xq9^+zYrTa1Y>az#V}* z0JjHj3)~90C2$L%3@8B#fdU{O$OCdnV*dvI3;0jq$H2b>KLmaNgffo357&PN{vHVB z8v89=zYBzNjlB)mP_D5z;rf@rSAo9(z5;w1_%q-Oz~_Nbma(70^^bwi0)GU28VDs9 z`yO0B0elSj2oOpuwjQn@0#< z&j6kZ>;a+VVkg4&3BUzl7uW$dflyAdI$T$PWgwJOEDzT?U>cYLCV?>^lu+zgxIP+q z6!1u32p9nRfKWCuFI+>Z#HQhT3J9eVv%|F&2;~to!nFYir4l;~t`7lz1$Zz}2ZU0I zY2aE7R00X095@L)2nb~p8;5HsmDql8y)SSd;NCzeq1di)y$f(>;7-6DfL{h~1Kb+; zCE%7oD4`e**J7XuC;;++T;RsiItJGmNfwk`*1zETpTLiSe+Pa9{44N7;0M6>f$stT z4Ez)Dk3cBVtlz@*yTEsVzX84tgtE+f9j;#k{sIW4nDruDzW{t52&I_y6S)2{@JGNO z0-phXANW1s6TruT-vvGjgc8kq7_Qd?*8$f6p)9jj!}SBe`+@fX?*-liybB2BmvtLl z-wOOT@MhpQfl!KBUx(|PfHwfI1zrRE8t`i1mB7n^mjN#YUJSelcp>lt;CVnO%dB(Y z`fT7=foB5G0G*lv5@ix4 z5(N@@5;+oC5@`}C5=jyX5-}1{633F5Cvh~1qevV{B0?fWB1j@Y!cW3S!a>4L!bZYM z!c4+Q!a(8(5{HpEl*AzfW-bJ_9L+m ziM>heMPg49yOY?B#I7WEA+a-w9ZBp!VtW$Xk=T~RHYC18Vk;6`lGuWTl!Sx?PC`sV zL_$D@7KlTdeB^L7ge<^!K_JZtL&cFnwUED`lO<%w$U?~5cgn1?BV-37U;iN4ezHAeJ0VYh3z&VN0PWnUXlhQ|#kH1QK7usF$4dmfpDZN;FuJjD#-!DiT(vmcTy!)f10jWni zg?#(Nq&lfWdZ2V4>2A^;q+3g+Qh}6>q7!~7`IF>b$(xc_B`-;SB6$YwPPB) zhvFy2kBZlcSBdWu-zxrw_&V{G;)})Siq8d#3gY?91|ZU4v0PCDY036m{=!P zhz}I+Bi>ECgLrGPR4fp)MIWP>jDHfnD|%D(s^}%rPejj%o)A4ES|hp-#b~%ibd%^B z(dD8GMQ4jn6P+mPh-#vOC?z^p6cNpdTq3*3C^|%>5y?ddi1rfgBHB*0l?WH{L@eRo zQIy9&2!A8|rSKKu3&LlG-xq!t?dMo6yhnJu@LR$ggjWkM6`n6VOV|@0kD@?SggIeC zc#JS4oE18SR^buCgM}*LLBjondkA+Dep$GMP$c9C{w4Sb#f|)(;BCR{f}abX6Z}x{ zq~KA(I>9QzU4mN$-w<3UxKePj;9S8Of|CUcf`*_Z$OvMBqXYqgM=&KY3l0p2TlhEeui;Pe*k|k{x1CO_*?ODK9A4h{hjw0 z6x;GQykGKO;l03nmiK+$cX{i1t9keEZs&cAcLVQg-le?rd1vu@yyJN-UWJ$AC3wg1 zLcCd?lV{}}!8@3z;vK}>kGBVJC*GHNTku3Y4)B}A?2azMPG}& zh6b1m4KNy+A^!s1X!Iq?>nniJ40E9YM&FnGJ2b#tXn?ur!(YwfN@|9cr0)p zcnt7p;8DOMfe~OB7y|l%b3h+(7U%`KfiplSa2n_U+JQEp6=(*UfJUGJcm(ip;96(32XHswuE1S@I|6qAZV%iJ zxGnI@z-@qE0&WG|5-0^qfMTE!C;;+-JRldy0kTPQ{tf&W@E^dx1OEp62>4gvhrkbj z?*rch{u%fO;O~LI1HKD<2lyM{uYqp?-vs^&_)Fj$z}JAU0)GK~1^6=XXTTSMF94qh zJ_r0U@LAxGfIkHO0Qd~>Y2f#PPXV6fz$UN`tO2XQGOz?J z0t>(#Fbm88)4(J!28;ra1s(%D8VGd@=Sa8?14F0k;6kfKnh16aj@m0gw;m0>^-CAVzZR-@tzYKL-8-_;=t( zzz=~R0HJ;udk?Pv1pEUK>W8u4!u7kr-vHkRz6FGuVGL@9u{Yo^uK`~Lz5;w12(`i3 zOK|-n@CD%Wz@Gwt415;&Lm<=!V^9x_eINevJs{KoV~@i%)B|H1;QA5Z!@%{xb-=a2 zHNXdf4**vI?+4xoya#v}@J`?z!0!N|9vJ&JT;Bq`83?t(7}Nt}P!EjV1b@E~cs=ks z;I+VOfL8;r0$vHc0(cqlQs5=Pi-8vc&j+3dJQsKl@T3a9|e zfs??4fD^!R-~qt>f%^gX0qzal3%Dn6cOcXoV>`q3PQV?3+XJ@)ZUfvJ_$A<$z%583 zuY)Ay1n$}7{{JZJH5Qr)oZ;*{_V2M5$L>Tc;^xP+V>0%yWiQF@LY{vN`S@E&-$fq$ z)yP+OO7}vZ_;Zrmk)M5(L?yxTH}Q4IM=s!Id?)c=#LpoA_IMN{;6O1)^a}D#&lM$* z=ef1;cgU~2M%X}J<6eS~kxzKL;56j%sRTIk=hpErMc$g3zZ3Gsp5c9ycLJI@o#1iN zyy*Sh^U$p4q1n`d$(UsrEdo2W|rOW9jcs0snWIPG7($<{eDB0Q!7zDgAMNW?Hr?FVP=4H=)n<}SLs?=svt}xYqzqhDzN~KDbm}=Xs{c>+ElT+49g;J?s z?pIVfg;J#`2BwPcez{*#`bP-u%8 zSG(LVcQI8?u25*&zByz6_uyYdkt6g9g*xGLr9J)kyKwKZnN8(Lp(V6hquXz%zO6#f zzkqr-l|mtJ1%uZ9Qp7)>DyLKsdVAa(TK2Z*Es;YxS+7*+^R0R))@PmYIjWpasnD5c z^0U5vxt~(yv`U56kgm+k^vnHZnVcrmovZezvhc@LIgL`GZj~&aWx4cOikt@7r!qS; zQDgsee?*Z}p!ZcoQgK(jU+x~N9J(KoFU(b!mCC!9$;qe8S+}|Wes@vjG)e+(8q1aX zbG`6Rs+?L$=nBr1YuWd>W2u}{PbdkEdn#&NmM-T~PuAt)ca~bG1~zT$d_`>Sj&XkTq8O z)<-R-|$Ynhy~UiZzk`sJEbIh{hUuo}v<%Zgir zDyLPT5CfJ(w9|jTI#o`i(96R3pWj!m{+MP~}kV ztWEm5&1LzoOqEk8beidQXlA+dm?r&6u396i2p_6wLzV@>G5~s+ivhepxynLzPo0w5T46 z<(B2=qp5Nzu9?zV54o1*gQJ$p5qk7oxh-CH_BVrq_xt5E`b^xk&|e=2-lNK)6Bp4Y zc~>&h_k9I_rphT58f~O&?gsni{zR2SeJhPAR9)r-2>wWwBT&GQc-1l2U;hax&If_= zsYXdwr~UP*;6=*))W|+XX142ICienW4%tWe^6};A^87M6c_QDfE=y-h_li&x3bj68 zishE2Go^b)sO1W^E}n>n`tzUQCzQ^lN~yQEJ6&q3pw8rtORPgV3O!Y2o_2)$Yb*W@ zR5|oir7a&!_je-r*Hh$_dWBjM3#K#urI&vlRZgc+6WM0o(EqmlYpHTtg<6izpAGic zy8LS>Z>!Yk<_ZySosy@}+kTCDH`GJa#KVDXe_h4DdYPQMUbN5k=Vks?R5^u0rCKl- zz5RU>{*|QvA7Ed>;~@D~(0 z)PYkGe!nl)A)Oiq_*R2|Da&Yw}` zvo0_t$-T5pPGfLKv&*`KOPeSKijai{++xv;aamV+X%n?kPE|x(zm|0!DNPjW zz%h8b_|nQ9jTIp|T2B{WTDc=RCQlb%TDc=RCQlc?W=XmzQJbP9w3%kES?l{A_(@bb zCXX0Dks`fz{^JYt;c;pmt=Vw~#X zpj=Py5#v-3N5|w5<5Ula(IdvG9uC?qPVW)pR1ZhXZ#Ij~LHW<(NETJV%vd@`&*)MUK%U#xu+0Xgy+_>Zxg%JYt;cscD!z zVw~!!X_!1>oa(7*m^@;f>Zy@(v>q`YqrNYbM~qWFIt`OYj8i>24U;# zsvMI?jEAUlOdc^Fq{=aQ#CU)z$K(;?eu^BUM~wTZa!ejEKD$hg)+5HfR5>P(823=+ zm^@)eVE}im3a!h``aOspM>asKW^}?l7 zo=A?#uNS_(3eGfVjz zNN(wbC$f%doIv;n^?pc>cAP-?IvM|u!&0$CW!|M@x1x(7{zH*EG@BxIjBuRy?v}14 zIv24N-1&rmb~y3lSB^aa`8-JUxCu^-_tuI!SPOoQpxb;mVY zZ&w;vLhIjhUlni(x%1!lb88V%UGy@DOk5u5676@X~Th|-pdo8{+{Ga)J zvPtL~cP9*G(%RCr%3M*qqgGSYk#6M{ zl3Clh*VBxGxRNuIbyIq{oQ__d49FJjsmPpu-l5oD0e_VYFx@VqgMmVhpjVYOE2@U1vDvXJ za+%p#r`8-HVoqD$D<3%;A{ktWrhSp?m*@07_8KM761wjw2PKoa1uaqwX2SDRnoO`T z9Wj?8?Xb=jnm3yL8b`L=@RhxB`>Z#ijCf;t`u1zo8w}NceLisz7F_v-ZhkTy>nbdzQbQ}xqa$_< zxZKha@y8lMe9wk&FSrz<`1EY=*LY6KzBYju)^Qj8@kIRGh zn6WFn7!eL7%21tI=tU0f%#wY`|9(xPQR(zdHAzWJ1k|(Mg>J@^Fr-RtUw|+u!qrkc z6-c+0KAUzbH>ppl($Pr$GdJ;N$7FCk70v15R`0mI7^~R*p=!pLOzTpWiaV63w(G5Q zJ~ggWR-08tF5bzZV5?fYI%(raWH<-y zC16?)FF`9fmh5+sGp&c0j4hQ@p$R{wJ{${sy?y;O`~#|-8m+!<#hmT_d1?GFR5_JW zsqGf5=KjGyoU&v~g(e1-n(DOM)4#+Nr!3i0p@~7Ix@>ls`TXhl4l=cZlhjxQZ#MRH6l-SMS^tQ+NMSGwa%2U(FE z(@J-I=^*Q%bsI|uS<(DD(@J-I9VK0u=l|C(lcS&iUqh8+od182D#tkgzj~?MfcgKW z%ezonW1RnAy1a{A>CUhs9=~|W*gf-#c>EHIdCc?w7g6OH=l?IH$}!IWUqF>(oc}+c zD#tkge;!qiasHn&&!c3X|EJ9JD4FN~Df2u^=J|ihJdcuj{+}|>19J59|7TL(mwEpG z45}RC{6BRk1M~dN+3h`G4yA7&Ko>zaqYG zeGJ-PNxve#Z+#3}K}^3QzHfaD>gUj}i0@k;!#w{_UH775od3^L(vOs*pa0KM}A(q(@j$1wl@(o*{fJ>&fU(q(_>evI?~ zOPBp2ImY>a%CbK+Va2d=Ufj1booVH~_$JDGFwg(rNR?xp|6jVCR7s$Guc4p+zn*$O z#`%BBvcv)N|JN>Q_R*X;)BOM6C{xo+^Z$RP-i&Gf|Dz@EuyJ0We*XW%WpecM|4Utp zLG%AhU5Y{T|4UtpLG%AhU5Y{T|4UtpLG%BAq9iiY{Qpvi0^N;q{(q@Mf#ewH|Cc%x zNRDy-f2l*UQI3B8|F`7&|1f*|PyK(LYzygcq>rHZd=;r(x|`%9$+IZ7-pP^>ikv6F zUq^BAF2pk^>fLtYKcSd*H;6kZV%>gX7K%rAm*^}MZB8fJ0>zShM0lmJiXy-7F8CXY z>;7%QDJW_?ArPXN><{uULJ`*u{OwUZ^{03@qG;(}-u@^S`p>v`qe$mbuAaLk=N--l z6s5e%ad37Y``g%$$G$yw%2;@e7!$JJU_Z#dh@E8{*xO@&#-74%#JZRl+aJSNKl^my z0^~##*KqU7|4-9ox3X-hwa9;7ATR^r~#TYFg$AFU{acPOj7_ za@;c9KblB)Hugr|DKsMssj8dss~>)+gmQ#@fc8kkV4eX*%r|B8P6 zEz0*`jAVf?U2ue|8>UDW_|gSOD2fGBBny1$LLwx`6v+aAonjx7qmN{Pzebf~ie!Pm zN|j@ZWPvZ;VSz@*m?Bx=l&+B)MFXLaWPvZ;VS(<)6v+brImJFwjy947PU&hfkKEyu zu9li{N}L@GLPI*hE8Bd4sDDIoHBF*a`Yp2l%bOWJ9j8UCj)lwP=`*KckWPK zIp&c&sw>Ala+g}t>mD$2NAdWS%p-Ty9SqDncM?lKo056wPMj*oIC2-G$}x`IMX7R( zBX`GA=%Q$jJ-HE|Ga!1{X!8~#|N3oB2=MHtp2J^@rb;kzt$Q^aZ2J_Ax>W&TO zojY#odoYgNxu|lCJ9lQNa*QK)PKq4!$eooc$2f9lp~^9i+?lCzj3aj@svP6UoslBP zJaTsgRgQ7w?(k)D^donNQRNs%?hd8OF^=3FLX~42x%$&^UwjHI&AyxW|8LqPAzbR6qD_FC_y2F)JhFNJKW#+3&HMl9 zBjRn||G#qGCA37iJ z5&T?q;@w_o&VQZwEEL;+H_`7ztI;`g9??!{w*Nli$tbG-c7iw2>2Ajh?1HWNuk&w5 zC$O3MTc9)5Zs9f1DQP(OC3Nmtg?k8BzdpA2*F6pzxOm5B_PMIgl4Y}Dj zov^19*_=1to@ir76TK5iod(gYDGC8cFzO9zT{^2L7?>!9&F-2$@EBEKKyeq)y7_(Iri$W_CK{%$k$V zRJ#=n)#6TP-YR!xCuZke>U_A|sZH71ScvF#NS$sWWDi^B0#n_BB0S&G7>mhd)}dUO z>9{SyX31urZq{m%t}#3p#{xvJP3l-(jdE(fjY4Il(uVax ztw|9`mqO}F8}k#r7OA6FRbqtACXeMiC8gDx@2XAZs4=DR#ak#4w%)GEDN1wlS!F#J z$L5G$lhny*>mAKpa3XH!ngwO3xdoeH3tC2d+oWkDl&l1YK%@mzc=cnxs zMJg8}%IQ*BUM$TQoA!>$X!UvgUd%)Es-%wBD)$)OC8xPsC=(Nbf;lluq#F}W?}Rd) zjZUP|rr((iVa~Pvb<9olDx^-XVdg)(O4Z8_jzob9TE)J<*-7P8%nJ z#ZV^MsbempS0;7j)`j_$E^L^eb{BPyiEKolHkgu4k2+n~+olzroUT(1Ih7N}xi&UK z^h%^o+cND@&NXc9Xj@&0Tfz=?SnYB*r*cNGC29%;Jn?)~)v|<2a;Z7C~j zH5F0!r&FP6ou{6enh!d42FyY9a-@zY6Ywl(@`_S;q7ukQ0@1W3VX3O8@|l#T6-(-~ za(ASgRM}=b2FyY6fA$2;bK*wOv*wb!J&8nXY!~!J;;dCq0)mpRWbGhfVHHV^GFgl7V%u4jqq>g{0 zFdGO=*_-7N$Obg<>|UyKW#22+Qx8s+Tbdh z3nit#qp?lfX5*oTE25jJ8biil9WxQV1gT?<&AHs!`GTwJHKO{3SP1%bxkxxnH0F~U zInhb0^9v?pD4j6RU`C=BCv~Frus`ByDYFi{&a_|%=j;w^$mWU7XQ!*-lvAO7xB)b#ywDs$CEJzIz{g-M-&rJ`O) zo4q+@M4zmgoF$i8G1bnvk|^6&>X87y_lTn&5}CuWZpIvOlgeM z(>j$`9-4Q}2kVNOTCYiH8WU+}-H>yZD@47cFJqHL&r9lLI#d2eGD}ParV<)MH?DT+ zy_Qy{o<(s$%GQX-Xs9lv4YU5V$BP|A^gN_aqGO!0gz8ykWhzm3X%oRv%Mr{@XQ!04 zrUJF#b(gcF^D4|ml;0+Zo}1Kh>U{OijMIoJD(6JPXkPGF4ee4UuBl{`b3vuvRc+Xe zIir2ToyQI&dM;8&nGVS{mQ2nu<(-;0PL%@*+tfshFz9ExzOoL5I*@lvDObMjnaE?~ zL~n-FnN3dxY?iFnmZ|AIQ-QtjQhN0YuM9>dX`h z-YRO9P|NJIMAG@ljLVe|TJ0UH%N2}HEf^h{8NVW}Db1;{{fXW*sgo$y)zxsbHWRJ} z+#bC)9Mz^=g_@(7st1gEMXBv9l^fdGR<>rr_9J>zq>e#tMP(;dFc>=OsZ_40&*xN% zbgt;@_#BFfup^wGQK_r8Xw2ro_9c1_QYY@4nzmT;?$%7BVK9VZv)Y*jqJx4BC3Dlw zNMpv;QdAUvVmfN8WBU+2JE@~9+GkCzu(G386KPPZNvC&t8q=;?$P!C+w2`cGDpbzJQ#EhYJfF@e8**hZ>c{pZ zdR9_rqU<%f>@|Bg+4d$J`qaGMP}S&z&2l=_GN6Xil?>^u`C`-+sbhN(JqxMREh^m3 zsI@&8s%h%Z^wf-g!bmu3m0WCIWeRG=l zU!RIh>oewXXC_g%StA;ExTPv-3zpcNRvE^2C3+@O$LXK2X;9^BB20mKN2H!C*Uh@= zi9o*FDFo(ikwC!L^)Ki{wT1!Pndliw9Y@t;4P`w6mByX-c*>brme6)R2}dQI&XhY% zV^wdC>6?*~W5JK@MDz@#&TOczQ>#7RQhmA_Q3k4M(@d^X%UYa8f4gGMr}ANSa3Yi< z+!-CVBhfp8)M-zk^D}DtVl{?x^g?T*I_C>^BmP)S>qz>dvDvo6?@t9rE0Ee zjiQckRX*F8n^$356TO2;opg!lF4($_*hHYMw6`k-d$8<|E@)V`bi zExF&1eTnGlNgYLW!rOMF3%;Ta?n;)txlDCJx1f%BD~gF`8rzcSX-OTmvE!M} zR-LG2>?Grcx>DgS*fSn;r4;lA%g&Bb?y${OQ4-A5u`P(6hSV7$R30WHdTLT$xdmNp{*RghXO8s@WW<_tx4}j!{kzW=6B$#kRc@j# zd)HylMf;B#>kD?Mfe07Nf*mRpbP;Pts2(7=)Ck2KQ0LJ_pvh$GjdHC! z-^$jrv1}pR=#pkZw2*9qjPS8?@{$TG#>7zf1ppwGmPYEJu>(E4o{83y35dRuNLCsd zWDpsua)r1uDX)*N@bBKDv%?w`~xl*~1&c2?t?_0*b#+E+8U z>!wVqtSASXa$~w))tB0}yjkb2c}+IoRJx@NwPqUu!%#8d;MGx_5)^av2(lQSQ^!VW zP*=_;Ll#5Yn4|yei{U5+(nN0jB&ZI=-}%oMzfYdu7u07{sUrjUeSxJ8%@LE4=V$}I zIj!1ptuCvG8asxzA)&3R9IZsnY0&wTC1ZX(?rgLwUNqREnfmNunrzA1jAx;dP3EkH zWXG5{s1q9G83*K!(i9p4EEA0w_bos%7oq!MmX-!DIwcP@PXCQ_*ID3^q(g zK>Q@yD~de4F;LfHjA1qT>*Aj%ZwrR zPjvkby+a*MSQ6cEO4n?fRsMRxTeqsM9eYZrY3Lm#g}$H&C|e3uG3+;V1NI8xo~s!B z&Z4dyRoEIc?YggOE6Ej=!mxD>di9Mh{9&6~_z_Cn=T+Abv{7G2={S=ODCCe&%EytT zRaWE;`s$8P1JC{ETi^ddm^w9C@zRE=+f)z0x_WiY1AJXD*se5g{Tolb0IZsIh^#6BYi7!R|{}tE{ROk2q=l^#`w|6i&{unmOP}JuZZ(U92 z_>VB|1bv?o#*aXLgfS}mzS0N-aCDA;x|ysu=A&^GWO!L$=|2-Oo?-!V_EAq{p7cW~ z?mjtK^96PW2rYC(Xse}CA(qi(JymTqTu$4Ib0$UBURL|HUGrQs*r>)5^+`j@Hai|J zggf{0K9$!RTWL)?*t8hkGoe7mGaaq_ zYHq(_av>U0rWW+&h4GBPRMY!rbmQadbgC7|nTM;E$X7?z5*l^WL*YZCZX;x=&#myO z{!bRZ5k|7<3Lj0k9Od(+9gqJ^5u~x>zcT7Z$n}h2nrmpB%M}cD!_%smcA;Y|s7yrD z7LTU2GgH=b$dSs~G(nZlS4n*K-hsM46AfoGicoVdqi=dcnowglZRLKUoY%X{2fT{GW_^{g+z6Po9q);-#UqH+9{=;88b}pUEfCHh%hr zz?3~V?Qxpw<$?l@bVtSwhS<2(76_-(<1T-$pqy2EYZXH@|Ct*p+Qwqt?+==6vq4Q`t~g^*#lrrqRqam9l$xftUeU6sm2+slp;c>lkJt0A zSy!MInz7899Fx`|8$a^ZQK>_tZd$U`k)v)SLscJ+#yfMBRyyFAb2+>bPuodkigLH#q6p1pN=;(i zT~xRtm9F0BaW#wfj?rI8g*+CEV>~}w>}mM|F(|+lJ+3 zy~vWiA$wHzb=hgMq|7SYS0<3YBYj$WyYxJ1N$Qr$rCUngll)Y2pX4%08$|-tOLoBj zfxp7~ko6+I9=`$a;R)P|?}v-nPl{g<-y|-H4;J%8KM`Fg%84|hG2xGdUlXQKb(1YvG=nT|R&^%|&H`)Vr`j0IkpL54W zt${lI=WCHVmpGe)bd)+8+j2i|4Ajxm=p?qT57eRiK91G3fjV^GNBOhrKpncY-shK< zfjV?)y@kE3;2g=AM`<2&_t|fX2R%kXldV_3e}8G94$bFT{k*U^P>1I8tbVjv7^p+@ zc~;;1On#saO@+Pss(5ao4$UWBUHEHupbkxuTV0T42I|m!pVjkQqz3BHWP{c7+oT8T z&=k4V`ZJS*vJTB-?mq2s(Wok?{z5p&GIWY0^-Nb(8LZyr&BQ<*y4=QpB0f-uCbvEC z_p4$9b!c+i1HUmw2kOw|wg=Xwj~%E(liMD+d18K`4oz-b^$Y$n19fO}+p4ElA3acq zCg-fWvU=2@Y(S%O_bE3B20cbjlMPm#e$|l!b!f7|suOOE4Ah~?2CFKUga_)-WP?>l zdO`zrXg=vG(}Tf*I&`1(uqOipb?83n{+IX%>d=(zRnm8S19fP=>8d@Fb31b;Q4fmd zL#(}7!Md3F*KZ!hx@hCCG#_N`nO)We8-Jxq`nBQ1S?6v1m8Pt%)!oQCXXCFlpJ44C zXRyAy@mHF}Uh~h#S!XWx@@bSGsk2zmF8)lT{fKfm)(;ndruixlzw-d=nZ=)JKET6Y zJCF7K#h+=?`r+#1tS2`bPIK=yuino(eWNm6>;156JJu76Khu1whvk>DzPtD{&DVH% z+YPKo7k{St9P2+wu^w6cndW<}f9X8d`o*7V(scbJH?Y<%{!Ejm>u=kiwPx{Wn)hD+ zRRL@D;?Fc`x;}9h>;A=`Y0`AP>k-zy8`G3VdCilzu}Z;aKKnA+)3VEDQQ3jg zze*oP;{YM)-jY8_)=JKhcqKdGzsB#uPr&W?m&C7%zb$Tv4;PC>&x>vl<&nRS37-*O zA&dzp1Rn_=6P%AC|L?{BBYzG5biRkbBkyhA-Mr&@4&K(>*SNQGo7^L~V$KVk8#x8` zvFr)#BkVEkd@PLZ#rh*_%^)XpY!!0Szcol=^KbtfQb2{gZseDv)qE2QIXcQkE?3MG zIzlU#PblaLr@#?X|!yDzvyGUz#KnyO&U?cZbd1}f3i z0Bg>9jCIODC7Oc2rkY`$Jg5~{(^Lm*j_sZ_P=}_DSTpT9ai9)O#j)nl@0>7Dhwc+i zt~!1|A}9!&LZlDs)r+wtrb6Gzfs6_K^A35hZ){h4iKLw4> zJ=-5C8}uAHFXEANRji*3RHDg5kC-B?9}QHZ$wZH^j$!>^pb||cdiee}>*;|?G@0n( z6E0vqHBgBr6FofpEbDs%m1r{2!-x1-j}KI$$wUwD@Hf_D1C?kp(fYsL#M&@Wi6%p= zf9*8Z!vmBEJ&j+v{>ejF4-HhJ$r0=CUc*{DP>Ci-tiRw+)`J6;XmZ5*%nw)(3{;}Y z5$iKgvsMjMqRA2Ky$`eQ8>mE+Bi3tnW8E`Qi6%#^-{l0>T?3Wqa>UrKtUCrO(d3AS zUVoEy`yeG9O^$eIt(tY~KqZN;Em* zp`HD#uMbqB$q^56f6cmSpb|}vSogk-b>l!KnjEq2wU1dh3{;}Y5$o>WpLP8pB`wXz zU3Vdyb=^QEnvc8gf`75D9jHX}ao3%Wv#uGaMDuai9rZEmYXg;NKJL1}RjjKAD$#u0 zb@tm?R}EC6`?!RWb>%=MnnJkty?t4i4QSg5nsm7Pq9>$-o};1pv}+%|pLOX#C7MsW z_O5TUE*Ypq^J&*!vVnEsKqZ>5v^K?Ooj*{C<}0nW{F`;|KqZ>5w07SMS!WMaqA7xF zMfb4I8mL551lRoL+pIIl_5ZuF&SlBYm+gV#{_!Lo$yWGj=ybn3#D|EU6&X=%zkLK( z3Pk+ld4J#?%l!$rz| z4a_y0Zc8$2HG~@eaNq30;y$yDyHw~s%w=7MQR3{xdy}Lwk|Z`cv@vnCf4`9&B{C0s z=YtmC`Jmm6wR+v+oIwGVd<%2diX+s24BRm9++QF^iTQ)9nOL-D;`6blzW|OBO$b?Y z;G#7Lem>SLOPdkm6lHtk$QUKc5931{vvf1TDDjQTy#vs*4oA;AVCZKpUo|~Sbfk1| zfAqvj^u+y#e&Qz!MeC!)RLb`DTTI9OhJMo0#eAd0VnR7+U-U4tM%j1hhYhSzmgVOW zqCCZW`;f+7!BtMAld7z~mLN*=iiX>-Y!}p) zc2gBMgle&X4q3C?qBXmHLDq~An+#dA>!LNgenHlZ5cLdMv&*72yL>^`D4pKKyecX0 z2J$nlnk`nhWlbuy5j9oP`{Y*2&#}ZJ1<(Z^A}{z2+`fLy`7LTWGlVX z(8f?&=@FvBm3up)XOXS+jzd3-u9Y4>rd+zW19~FaO7AfA6PL8oBSdA&_O@S4$L)uH z5~YSS-rJTm2Bq<~LmRW$N{w86h5Fw)Z7u3|XUmX=r08HOdHa49mT((6h)IWvii| zMN^}Uuw#~-{|~aB#;{vuKb0IKAtW69Mf_GYx9`FC6n`jwN_-7Ai|x(&h<%RqJ?Z1( zir6InvgjSrI?)B97@F%B3SSZ4DeMXT!u|L@5E1AhbmQhu6$FkixZ zgLfbAOkRXH!DDfskVbR%36CtsT2?EID@Yn2h}afCmc8}c~R+z^=g?-Af!d&*r?30HT=CDsS1w!>oV6NgWMD4~xU)JDZZ)0x{E6l~-!rmHIn1j8Ey*aG# z81^gdSHlXkv0q}p+>{tAy&(1m_Qs|g`_O4Du4JoX%C?JElf7H-OAL|lbm1qtc_m?? z@HF8kxp^gFf$&t}C%JhgVLobjKgrE23G;-f2tUcqD+zPafj*z)=9Ppw!jpub3-N*VgF;)=fvF>Gkniwkxb6NMWK23}j zggLCcS)V4x3c_QoyI6M(`+{uNovb@QNx2p7%({bh$Ks8x`9dbpG&xGCRLtCImV&Lm zloO-HySib8MI0?hJFKvfqv2?V6&7&R9QClme2$8v8djLcQF4^S3UfIMj$&A04u{|n z!wQdaRQA3UdzP9K>2Q!`5~t{AF{c&<*&}(F*2;X5Z)LBcL04m`9s{ib1sx#Ane zH*OMM$wA{v50V}j|7XhejVI1;_W$LZ{eKjm`qK=t+5caX7@Ph7PZMLa|G%`HZ1(?`R2rN8|0RjB z+5cZ!PB#1hOR~mh|9@%L*zEr=O^wa||B`|-y8lnE|KE;PW68YITclh`4Bvq7B5sO) zDB4xn68uyk=by)WmuKUCgFD7KcI<(%E!a_PHHy7gqn&{t>W`=F+Vjd-SaM%4izO6b zT<(zq!6>W3DQjU?UPmiHQHbvUoeB6l!#08bMZL&GPoIhWQI517(L_FJ+`F^Db;Fvt zch60lsQfe)d84c_Uo>&)LY0-%l1G~O>#c#m7}n77o{Kc}@J|ipj6p;qpWUUH22d*cPpF8Aq{*jMe=q3ux9SvbM}4yu~8Pr zFPgb@#oo$>j*%w5Y$o~Y_QM*wYj2t~bV+F+>15l+~U4XAL zm`Ia~);<%3qa2|ai@9xN8B26lq z`%FZw_wWaVk7y#a-W#?gP26xAgNe&haqpgqG?CJJkFfH5(L_q?y<)kB)_dK0($I(Q z7}n77o{=<^+IkPaV13b0n$~+obD{NK4b{-ndqoRL({8(xIrO~VyW;DH+Rn|~oq{MIcDCiZU?KuzNzeJ1kv8exh2h$cdfd2NO?@rLIZOzhto zf6Csyuk;le-YCbiEt*KFF;`4Ws4=gyl7?O_8P?G8-od1ylp1q{1^J7H($ttMnhQ1N z!9b1l{-x z_%F~)aFW|Q_p-+3T<+|A!BzDd^HmM85cKJCk#Lx3%qKN+qLWtV7fi-bI$@qcN46`} znr-g;!NHSnX}*D7e8>GiNUS^W^6g%D?bYATR8PH1DcI)qhBDBL!;Pc$us`ByDYFi{ z&a_|%=j;w^$mWU7XQ!*-lvZtPO2Sn!)1rZZ;Hm+cXY? z+2#u;;w5h+T#1=`jPJhrm2dp!9c94}5C6k4-`?*>4{G0C{`nav?Kfq<{M?%-LbrdM zq*fWmM8e=DwfP44L_^_Jx!j?&UxRyauOC;X3IwLKrj4mkYI(VxVHvv~_XHm+zW>)R zt-NFYJn?6h&wH?+tiDI~^5KFH?D*$@d}Tk9I*(zj)f$zpTG%fQCJNSu8DEVgJ)xjS zl}Y;xCJ(6#wW@VOCwJuK+MdSGKm4gLer5kB(y7CLHuv{;U%Mmzm&?Sr&Hj1U>h%A( ze|*)uOC+^}VO&qDOD0Vn-0Zom#%wH{QTi>`Xjhl1uSViEC8;5EvW(4`z;kRLKk)2F z7e1T3?9CI?Cp>leC6^v{=ie{;>G!MOJm`PIs!J|@zZ&%MxeViyW-XAz$ymx#GT@p} z!)r{fYAVaBP`ZiPO`V3LQOL>-N`uT3=xKcGr@nLDceh^<5o#WI_M8*{`RqNPK5ypH z#cg+=@|#v**&;Xvpaev}2iUI9mw%BU((O*4XW3e>jl~xUukZ z%_L{tZQ@;zy?yGv=iYzdmp>JYo%}A_2PXe;_hTo#^M^0E z!ZI=#u*KC$LKi5zh-$ZHPBi3-rY@o_cMD$JM1V>bJ&nJe{PuT0sR|^gY9D#WpY2Zk zbHCs3hoAh(Kf}-6`1CoCKUw$~Nrf54q`9o>rcK^8MMRgZ8l6SlB)7LRc+wrsR&*^7 zUa+_2PHjCRcZFI7GHa3GTk5$3!uwp>j2G;TbZt=#$x$rm4e@3F#+#317Q^gCO`pD70%(h$4ZB5=nQY6DT zoyn-(PLF)q?9C+-+Ev^aG{l{_E8_FoGHYsmyuOT8{4S-V6YgnD9op62_u|W@#l}z1 zRvomve4jIU;M8SjU0nU?<+mTFT{?lJ0t{nU!v`XlU8yWn@9TvylzWmTN`!$RgPX7p7Bq=|`cv+g(+HIR4uQM$iA@ zod=SXk6|oL=B)N$N^NjBv`Viuw2H3=Yx1f}r%tHr%V}p#zve8JWwo}h)YJHh-%K2z z{;T+w&;4xfkiY%(d-tvV<)x2wAADx!-l?zM^8WmzKOiYD!#LBn2kObJ%ud=9YJDfJ z!gXGAGgHeZ6Y`QJ;xXte-L&2pNPAd~FW;Yd;F$-G_w9IQ_QqfS)U*G~`73|j{2=KT zPv3X3n7H@_lJYQ&6K#Xt9I9m%WqYEAYZAdw(-zD+vUWwaAumM@HQd?OdgZ1&vrI^>FaL(5aE7rW9g?cenKhf;YhN|kOJu@o zUvA3uI#b1lzuuE+Q%<1aje#gBcbsL=d z!)dtS`_i)kV;95Nm$s8ub5>)`RCS&bVRZRd*F4gC&|dZxjn$N0rTx<@4T3D&eyDJ$`c} zor}0|JQuWJZ3~VEqxP=BmT?8-X?2lM_B576_o1kCBv(VT@3Bq{q0j)&8dN{O7(_5RCuO zdY|II4p#o^ng9Is`<&{(-~N4WEXU7Lu|u zj1lT@^fZ3oJI?;(x6V|)Vpl)#Na1fgQ_GK?cg2q`y#Mfpm$&@n$C~hmNy@@7MkuAx zWBk`kM9S`?kNy5l^=nuE`S>Zn`|jPhjQ>Hf<43Ar{q11edsXivDKoi+fKPe~L%-Tj@be)HCcUOp=D$Sc=8{GP8~a@H3~%ET~6C@IlnJQf|l9!tOS zPD1UFB<_jMUF~ds;nMW2H#~IZEAO;C`G+P+85zb1hiN^=%kIm5H*@HW=lZ{0mhZl0 z?2q?c|Bh3W!9TqxKzzb~cJS`UNy@-5MmPfNG2VLXm(M%yyT7>gL)-qkrMSHFt1n?6 zzGBO*N9_LC<3D`y>GW|blF~Dbeeg$CJ;vsp+kX9y$8Y-L9qGo}#m60Y+ovu$Eh+s_ z_?Wj{L;j`p=hKfNsdE^{2!~5O#$WnFaO*><+aK6*#2Np%UHPuZu0NvmiKA_|J@N3R z3qMO-w*unv*$iWZ`UyS8pF954=Z`=0`A05);A-U(HozXA5%W<^5fdSO^Bx7^_6RGzu^~uxG(bLIlohUnxxKP7$Y2U^cY|N7t{H| zpPz~!@!P)!uK2)HZ#(I$f4V(=#~GJqubumtso?$xNuACxM)>5v$C%nx{d)ec(yz)t zy5Qt@Ha@Dm^RLpcJ#p<5rp5O?ckXldJYgm&9m5#mAfd=>*?#Jb&{V;-Xc*>{dnp!@P6M@le;HRnRpUZ@;`q3o8v9;J^y2%D&P@g zkBk+@4i-Hm%82$8enuD<&H~n(|3iQ=BJw7MXK%p@xwzhybFBq^8Ny_D)jFJyWfl>3 z#snB4G~2Tv*dpwU2{1lrwr4`HMc5S+V06%Iw?VK)*b@_AY|w1afMAQTBPPJepxK@d z!4_dZOn`Agv(-VcMc54!l$K$u9bNfx5n(S(P#T7<27;|eqf51$m?;2udS#D4Rj9gT z)^s}DB%9`ZE?dfJutcPm^tM*L1f^!!sv+1SY_JJR#jsUDutnHj6W|2}+GI)ywg{VR zf>JPS6%cF@w$=nCXV}Ui*dlDK2};JWl|isY*j5vilwk`|J{6@DDnYF5QR@sikK4AGQ$?4f+`CSBc3d0tneke*d z7D1iNu!SfRijp-&P$w~LAu58RWH}Mki40qa+MkOETWEqhfnf_##SE2KKcb}J5Y&+jTZlT2iwNfi z1a$<%7NTV1BEqQwK^@Mpg{aH8h;U{=P=_&WA&M}fB#;o)p$uDya*HUX6A9`NhAl+l z#6^VD0)jf2VGB_x5hbyLpblc#LR36lM5ta!PzN$>AxambT%Qk!~isxtY<$r7mlUl{-WcuxFVadzyP zv9#!EQ4*AHiVJ=&i1KXlFT4cYFC@Jv;Sa?(MwjTk@Dgwjfv5n&;Twzs;JyMVfN&g( z5+`940QVY*3Lp$m7zM!n2ciO-Yyxmk0u(?vxJHS4Fg*b7QxFwEm?mHp0QWA43T(0u zfcqIl1vc3Sz&#F70Aa1L***a7dk_`aWD|gUAw&f>*#zMJ2q=KCuG(x9fO{rH1vc3P z;64gbflW36xVHidAOx4qHUYTbLR4UrO#tq}5Ea;D6M*|Npa25X%{BqJS3^`_lT85b z-+%%LSCN~&CIt6%KmmlCMU=Jr=-S;EUIOm(5EZam)RkPr=!wgNPL&)trtOXnUTJt5 z4QtI~2;-y?q$ntYxVu?`Q9z5VfXdMJII2(xbb~=h$XjzaY}v5fWN#Aoq_YJcrRZh?%5Vku zK=ptcS%J9IQz4VM!&P>+l*=`vvr-6WyS9cDPZqsvnaZ-ZtBQHc^5sSvrUz8W3T(0u zD3KM|WFJs43LspUZ?+G}krmiv6UdMi*i;kH`~RZ_j|dhnod4T=ckbo6=Ijfz)tNue z6sLbby(W1^lA8MERBZCUC(oPs(L`kYsc};L9kFlh@iAQVb&*~8RiOodU+15TDA_m& zrc)?av5N>7p`-8UE~2D84G_34BHWq6Xt2`L0Df$Ji|{Vt3KOFaz`xT0JI!)S0yPXmOkZIoMM7!6MDX@HQ0jdBw_ z`Xm2~sFQjcAf#)fTpGh@aAHpbguHH)`(+pnPUvZXkN}Qy6%C`o@jVR?GR0ADtzk4+ z>}i0IQjT)L4Wq$vJq-|Y&r$BeVKms<(*WUy9p%~_MuROq4G@yfQSPiqpT1s19oy3Y zAv+!AdK*TAV|p4Oq_Lyi*}!OUbWa0>e0P*P6BrGS>S^%0-Nal(9of?WY$zmC}&UzXBDH5fG(mA>}i0&brInkfYBPz`~NY~g96DP#%~7ydE@7e z9{9h|1DB?#GbfYY)ho9PH}_^>=hk{Dmn^Pk6I)L|Rwh@UdhFI(y_$>`R!h}HvYIWX z!S59^?XI0$qm6oIHQ7uS>oxGZ?K=QzxteTpepT<3li<%=*}5LRLNi+dZM9S?^u$2q zI?{kzvIsPwc@3%U!L2eygW{!P9cTzpJ9`qF$!aZID$;C}3;}3dy@bSe7vP&nmg^b7 zLzOr73j9gbf!$DkxG?P zl}fEtN&!3*t!0vQFYnyi`>cuPyVfkW5*kNbxng%=#V&a6-Jv7!v^f(Aca&B|HIVGC z83H+9zLncbt6a;bi_v1&x+1o+4G*HMZBRNu8bop(n7*WUuv4pp5D&Sd5t*BC|;#s^@vS~U-gQDVC(|Hss zTL+UV)1*AQA~U16uT0Z)%!yG4j{T{SxL@7!8y)bm-! zqf*g)ElGFL09V&+boH%^2xri=i5C$L%^7-x8F?!`@YcI|ui8{ClP%L0%qOGGBxC=+ z%W47rK!>WT)G1$RMdLS@?Kz!4;Ib^W+|F7)tx;nwcS;*}kTR=J*4B2s48y@rE!T~5 zYNbv`#}!Vn*otz)$TWK6xLY4nMwSbr)f^d9xTT^P5IxlUqkBj!8ieD7!k(QNhWjuw z6`nv>ZV8s!k!sCebV_sb&I#!9%QuMvKe(Ok}C z-PM*~-o#Q)TfnEtH+()^2Zp!NpfQ1w2|8tcWB}IOJ2C{?HY@E{R*hLMt?5@Xoruf3 zvZe@mY?+p}97z*0S-nzowN3f86`#41&W5t;fF&9)#CnY??wk&2a=^UiTi6yh0 zs_a~=p@ka8x1ahExQs(Co@v13TwNn9z~)l zQzg>gCHso2qAgT|u8`5L+~-wAy9w&nU~VZUbA}y-^ioWnjx1F=UY$X!aX3;nXE_ko z7jO#!ikDTT(kiZx*BYsqrD|_v)jH5ujl_3ouyS%BUm*`d# zbxo1X*weLeR;3~gQbX4ps^}7C*-BSsc2(;BFs?8}S`nictc#3TTCT3tu$64QP*j9n zE;|t*O<9GZZC)xaDdWRp7iU$&c74yWRvG{92ea*9?Y;M6c~2cq+M;`T3H^)pe)}pZ z9h+IVr5$hN1h-*80MrIW)=gCqbKeG*99{QbpwODrt$`Y03Yv1@2-}B-?f?@4{r;aq zbgp3Gv4yJ_Yzy=AU!G6RpFFW^JU)Kg#L?qFn|p5Vfw}6OX7;Js&g{0?@tLpATsmW) z{@wJ|)9$I6N%_PdChniSbFwrokvt}OyTmOyaO#s&ohifk3F2RfKPtA3{dVlGvF4ag z^rGmqK+GFI{~z_hdE?#_cZjxFN4?yt>kE%ghSi{F5Wu}dgGr(E8h}vJ9ANI z%utSIW7QZIX>&@I46Xbs*~zvZmc^ddOS-J;u+a5BBk8b4g$48Zj9Sk18A+QR2C(dV z!$8vN`@*Q_e%TJ)(;HL&)%OKR=tf_d`d;4`AX%*Ug{ki%g4bJP>O1`*1}T}(8sL15 z<9$<4_Ja~6`<^wXzTJmG4zC_;>}UO+u#R{LeLyS|+_D42&+Q=eLxTBwLapU=gy81B ze?fBJ<6nXg_5BMHy3xM`AAYqz;X0<*pMnpbJ?_=fQwpTyIK6|*f}_~gXg(XQ32rry zdrv+Y9?zhXwe?3OQmNp!ezY0&Wc>iEj_vA48%QvpRpqh`Q^@X3WzSvXk6qA@(U98j z@tUy<`!fV2bfeddU4+>2>!Z@x#r-%8DW4s}JNq(Yc7N7eiqs>ysHN5IdMlg}HNe=D z7xb+G3Fb4lQq48?F(h0A6{j>cyZlHG?y^`dP9o~bc&J-+~ zb0tSjs&kl1iIyQ;a_I4bDPL6R+G?xA>Whc!ctq=}7(#|%ZSdwsCgpB5Vd=Iw+8Jw{ zh?#?hdfJMY6D95vpLUpmN2fsO?o&NXD-M|w2o@1uAQ+|{cbLG^jjp7}T&e9o)xAeu zgm(>wsUhRU5m-7{b5$rUq>9a+@_;-QMBl5HFGLHoErsIzpkuVeR1R z2TM1)9X!4++kJ{tlH9)C%lJA%0mZ@E>God2%XN@sqrW8RdHN}iJ<1|{;4?%E9h9T% zxMZ-(8tja+hfSR!K>5OGh_aA9i>@q>m+Q2<&#^I$IEZcp!e=By8o|{Ll5BL34|Q>! zZucqJ9;ZS04rPdLYB;Cil8w%dXFc4yM_GhVR)#3cT~|Sp!76L8?D24fuUm#_!R@jk zWZX85R+i7h)w|Cz@9}Vi4h~XZt*?o#>kJBK0SusSny|-{fV>p*=bgp&J z!wtqgPJ{3f#%oiSd*V4-Sw5#x?moq^r^gY#<9Kb2*ykSC@9A-bPd;9ovYX!H=j`cm zgs(-) z$sH5dPMkRY;J8bCiuiLAe;IptECfyhE);GT{*U0LxybBuv()UN)6dUbCV0ojo%uff z256MnCm;eSN*z9t1b484MTDyGqxU@QNoVhHW%nF)C@p0BA)8xlfEB`bNVZ}3z}Q*< zG&+pc2w~GTdL0a-5qBrSXq2W72Kp#~84!vFKLcb)O~65<*c2=ow(I;6sfANzAeX{y{}(NdtlpB?j{7 z;Db=A0b1U=;6shEJ%~_{0b0&_TkW2uL_pMu+YdsiehMv$cT>nc458ak-~ay~eE%=c zyoHb(edE4Z_3LXUnRC_b#=6yl!DCUxEk2!EYSnw&CgZXLmpTpMh$*RT<_IbW&DN74 zJzaH9UJNgn$y|htrp*a+MP<)rQtD-B0Z?N(PUIy)I7i#D0ltw>j@jjA7a6Pl_`-pLzm1tmdc7`7P*wt?lmk0kBZSk!C? zd3-IE8rRgyN%Ly7Yl&Mido&czFPDm4waiRVX@+eYf~~vNjg!touu@VPD-n5CSILDE zX(drM*E%&Ej}(gjAa1WV*D7IxN-=Cx5Nr`LLkKF#uuVd+wZsVAon6i26|W&zQOmkP zzjiGW4$JDRNwrkgPOEZVBlwUjVR8{vf?=B&9p+IoKnN<%u#H2oMaa}3s2IaG2Ei5~ zqk^EK4BIFKTclSu&u7@455X4cmCaR#?J5LYq*pf2W7wVt!4_fpKu|jvwmTr$A|#Ix z)VU1Xb0OFwoTn1hc82Zt(c5a2(yjy*Vc13>*dnBJ5LB398-`$uP=uABLJZpw1Y3kd zQi2LHY=aPN5l%!2ie%W55Nr_+J_#zounj=4ML6dqD1u>2K(IwfGa)EH!`2VM7U6J- zpnMEl-{=z{wC6Cr3|lV*TZDsMg7PqIJrHaWvNQi$kzQI8h}i z7sJ*C!4{zeC_y{`<>Zr~ zForD#!4@G=grKYpTPp-xgwzd!vM_8d5Nr{WEC|ZXur))lMM!%fC=(SU73^ zfq881)3g7WHO^cy{f+7UB_ER_uU%mBwEh)WKsyRW`sG+=iW-AtTR#4Yk}$~ z_Em>im%^yd&1ywSGC)bH-91B{&{JI?gjoB+sJ@}s5!TCV)bT(Y1?_7POJ-J^^=FtU zsRm_g5ePe*76!49W`%7$#ze_E$WX@tkt?)Fh($Ila?eR7O1i-swYA@s5DRZs)cV6r zl&1@T8*Ks7z~B`KA=cxpG~V;f4I=A zbv;b0xtRwjdjXU;27u~E_f?13JHV*UP2)gGBta=k4}9&YzOO;-PheEv(CY|$nlAIsyn|LKehkhZQy;WFhQ=GSuNfBoneAHbSh(^&tyBrDTmd ztlyOonAy%$WYxp^We`!$q@7=Y>r^;L)1-|ds?2zx@57aV}<2liEm*hB7<>InNz zl&2qn>Id{yhuEj?lj;b2Ta-5=fa?4ARfpIM@002X`)8E*sX=hsucx{|2(jPZC)E-5 z=xfxQfHuq=2eD0Owb^TqL)f{OsRba6nd2Zf@2s#va~ymULx!3MBAGc3Vq?#WTsOxd z?DW^DxqeqdZ1!1Ed(Cl(`~NY~qG0yT6IJk^H-6sefek$XN|2lSCCCw?+315R$P(mo z)u5`u2u@BH%|v-7fT2eSew$Z29#bTNISCYEl;qcCmosQQq@#iDjQ6Zf}-0sT1^Ng$WvCM)z?zmv!S-HUNxlx>W)d8 z_2zX^ld9kf+KugeL2cI(^|g#4;J_X2QnKmvn1aSyrBO1cx+;G>X;0}~22y9y)=f=I zwVgB$iCue_An$j5&$0d;N|0|D5U;%i`39D}V~pOXK#wpLO*wFcjhx9hXtj$jL4H|> zGSCyk)k5JSLfG202|@9CObD+zz$4s94+`+>x8AftH-Fi-!Pp!Z+)D#cv>#L?U1c0` z{d*CwzLWFnBLO=An+-1k>}wR3YMEv5z$hGy#8moVF=Pzt6peHuRrLi!om6$jZgb+E zYFi)3X|qmJU+c7cVQ*iamy&}oc&$XE>S<<8Ig>x6%cdInR3i~mb;_NDT3al7n$r4; zB~{O7BJx_XXsN2Jz9id~nirz%sHsUs%; zIQiwt_fD23O_PUBJUj8Fi5n&g6NZUH#(zKlh4FWd=f=+&KS=yr@k3*8A4`vI8{1F# z>Dh11erRszoM-NM(RHGv=ycJ7@Ry>e=TDsb+uRd#w~5~=&Wg_xA29aJ*yk6%yKu+C zMGM5j$>7BSeD=7RmuJ2)bIVL?#yPWP`o-D5&i`Zn$@!1Y?*fWL{hT89ibPwmM$KJy z)`Jnk87p`r?LvXG6`A^p-jrJ}_@(Nh7&<*B0*N$`Mm@tq}+oIiwP=So|?q(hrI6 zU`a<>>sj#&u%!PZejb+e_u}VZNq;B)6HDp|yNrfbyo1}il|laC zQis?BOKKOpVM%Rb9F`OlyI@JJVka!AMeKkjHH+=Aq$aVAC3R)v-E_`KtR>fC#aOcA z;aCg2YV&zm($9!K2TS^C(Pv>vKPCDMEa`)yPs5UaQuHZU(g#Ej!jj%E`Xo#0j0MPu zJLU1Tq7h%!VNY=Sb<8w|!IB!s`cvFUu7)uSELZ)Q8J6^%F%wH_B{4@h?r*faq$h8| z@@t%C2@ez=2upf^@BmoS{e}C(lI|zm50>;z!Z*Q^E(jN3N#}+0u%vUsIatzJ;Vdla zjBth}&0Fo|wQjZ9)LR-YLZ2>itTp|;nSQ^HB>nD8zh6g^erKlNuOmsHoay)LNYZc5 z^!s%r=@T>kejQ2r_)NcFN0NSP2D)FrIWrAwf^W=7SkjS8-QavbYL6r(=ljuRWV8Nj zraufva*fRQ=dF>X;raf&HIg(m-=DWeuHZ?@+u6<De%A!oUj4p{Sa@S1ZHykD)7=}wQX2HG|&{X%p(q}FPbilvS_QqEvL zyBoV|_tJGkQfE+nhEAPvQ)c?6^xexslmu;qM?|r6RvhFtz0`7!;DgGI-hs>ba9!-V$D0ey*#^pxwI`eie%Pg^xG^Qm5FRS zqGi9?m^F0TQG>hO%9Jv7GVU%(l~}->m8bootixb8r!0mw{ffJYtr+EN{#wVVmdTxA zwYk~VnXRR$v6s#X6}w`3Tg*#n(ix(mj^_(_t8I7Ka*A%-r>|E_I%g~9$Cng^+ES_6 zSc`ge%4FMI3dO?823Bv_>}%_&9(2c`*u_bmVY_}n5+KT; z0mgeH`KH=WaC?1|Z>GUYce~(A0@014ipU~5V$hxY`g{OkNoW!tCU{oxrG@YIZ{neH z1in0R<3w%3KCxx|<+y)gFJ*iB=#F>LH8(WCRee%8R;PG*_jJM9?x%Dm)a$;Ggw7fCLHCB0B` zAuQ=G$u3yZ3nUk?q%CJ6P%@SC9Wqc#hfLiD=Zt!GY<3KmR5UAsB^AyJS<+F@L4(|k zTVP3Vp1B#8^n)`WgeCpJ%m;d;m@Psi4aRn}?DCi66)S1tbe8Bo5qsoj9oerFvlFmf zKOtgI=SC~=aS?kuH=6Wb(Y>$=+#_Ny_(yZSTf|=Qk0!lK#9r`YdJArdR;>*((g+&U ztspm#U$`t>W_2A&daCeLSkfio5-jN{!c$;LPZpjGOL~&nPDtu%t(b*o2hPTn`hmnMR{Y4-v8HL!(I#5*>7s*eeunv1Z#DPr%izJN&_t z!PXTHME!X+9l&E`s8%M4vO6Ee40(>{3!fG~&Gx`Z(q9RG1xxx%;V)rHehSBVm7hEI+FC0Vm7g3H0cB42VfPr zzkioDn(KZ2yR^}ypXlGEjVAqg|1NDb>AhlhwjND-Pd^>cY9)fju07TCcnxW9#bh&c zOdz;lz}_Z~CcRJ4U)qf1`UwHMUK!2x;{tZQGMe;W!K*zms#*64*!9Y2u6GOA^@_E& zW=fZ%?L;e_Dkf9epqtZK^UL$>B4jk_sq^e2WHjm0Ji7=PO?t{ay9gP*x}2Py?2Q7< z?dfD9p131dZTbr)TQkII*2K*d?4o%z=?5p+Me}IV4@~qI%_9}KY2qeWUEe?Pepu2Q zCvIfpfWO^r;Z^t31`{G~7sJ|2kjT$f#VNl~0kdjD0W zo<`Rkz3`F(vH}R@a$pq5BP)PVr3glW9HRh2$tsj;TQCZ&AuE7TE*D0DEV2Td>;oBO z1vc3S(u@KKmBcpN2U5riY_bWG$O>$-2@;F~2$j_~+XQiB1vc3PF=Pcc*#uEW0fgYP z*(Nw2S%FP9!78!>n{0yf7zGfRZng<_ASJ7p4K!{NQrJ5kZC*v>*1d$a$$i#qAfJ9bclYJn7tiUGw z0Kq8mFDlCjV*)?20-J0CAF=|QYyvN%074$t=04kZ;Uykq1vYgU()<50QB@$hbNpQJ zpErKq=z)K64_um}Y;+RimD`1zyR6x{b+05wy1Y`Yz9A(VRK3`_HQK0WR+G(Sv0ek& zncL}_BAk3)`d6G{alKN?++rz)YOQ8=x?<6qze`xkwh*_l1PvHj8HZaJng*?}iN&N6 zHj~v_wp66^SD7+bj6x+k1$^s}w7U&#=^U?qAuJjA_r@gTO{=Y*lQ#RvwTGYLoY5SV}uhOVr z9XERd$X1m75!33818?)Qg$E_w?lruJM2$f>PAuG$H22xTW(HF=bos8VGrx%63u+GWc5TSiyJz7~kr+yShh#{+iK)GVhkeZ?&;_|?gz zESfZgjZEQ6;2cmHwDoM%T2QC-D+(DNETv32T<>W_{pL!{)z*gmG83`V&3hYmokOMd zTeKmY!VfGTs4I}j%Y4vQp)Yk1hy3J1ZApjsb--IcMb{-AUex5^hYZLl4enc z3Z4G%p{z{$|B4XB-(cd&UNh&g%9+~NBb*?tcA0R6uuI-E5Z8}ngzeb>XX*d4S0mr+ zn*XoV$>~ZN+K8o@R9P%-bJthuWVI!~JDA82y6{rKYYnv;`@XPP zBMhmPw%TQltMs{qC7*F%@@C4RPZ=!6Fjn^ZOUVM!(xmlCLK)I|eTqQLr)YT1iHbb0 z_t>qN#_WkC;!a+~1 z0ig_K)PRP*k^ld1lK)Td|Mh~a1PfnUxNO0?Fg^c``RnHW^9Rp;f9}S))w!*+zncB% zY;N|nnLo|k4{Gq8KK=LUho|2>ZJCxxzAm{|5|kW1_2a1zP9>&Joc!(NU6bWW#l(vf zpPsm2;_M00_?O480J;1N;%|$u7l*`0jQwQn=CQ=sNuuA0?gn2QD1|QxKO@{F)Cmwv)V!_*Z zz#Am{J($De4h3-*d8l!6D$HM89ljOd*q zN6^EnE$0Dq&5U~@7VsUd+uQGK84s9iX3|NJCaX#B?2l$C511PYJa)nzbP#yB?_euD zV6Kn&Lji*!g!#z;+vd}Fz+4}(`y&ogZ*fPgJ@Dzv!+nI?*}-twTtm- zZYVJMNT)jz33$9Lc!>whb+C}%7H~T~L2tjmPT>J_9SpbFyaAuz66jm%WF9bgTm*uc z%j(AL&c50w@qoDscm zc{~r8>)I~T8}S)!R!iT(77@Uh(d+TK_2y7t@^L(1?znJ-LwXPZZJ2>|lC3;o?zk|A z?4;S{G6nllXbTURJ1z{k0dx9{Sb+7g(~liKF1VI*hfD^$!AOu!R_kMi3s!Q+MJN=p zVIh+~5@x|i^ME&u3yUSBH~Jm6ev^+v0PF2uw>J{kRgj1CAZD(PPf}( zH9mqzDA!0}6zgrIU;pYzKAZ>4HIfU2J%`Ign)+eju;E|@x1%j~uL}28`xF#O+|)d59R@L1CYZPjF^K?m!luO4&niG1CUwo z^twDY2i6B4$OGn@IAV?X!#1bIs%OBFL{;+%(b)27cl#cZXe#a^9&DogPn0V z=C$H(cVF#k9x&I=7`?Fw`6F0=NJ@CXTsu23yUA=KOriejZHfoHA(GfIQjg;vQ$KJ{ z4hPG)rgBGIxY6RX5bSp9j0qkv*HjK?$c6>{dUt2vy$%Cucy*O7<^gki zTTi$!g7kU)>{8~;G4b}}hs1jsx3}}9bhfyfNH(+aB05VEjsIdQj8yaKGi4Me?{Cf4`&@w)}$6(ER9>Gy;K1ZFTh z`mTJ@@UGQyC&h@K2#^75$kiW#7xI9)@y<@zG0cW}JQ3C-cJY9@gNV@kiHI4)`-`S+ z7w~l>H|+TIL8lD|qmcEdZ9DnU+>rpLCSwE#9mg7BTbB>b^<{U&U^E4NX3W=X_O=cm zn(NHIV9-x^tfm0FC)n2JL2paBKH5GM(1AKl_ZbNk$a<35t^ayDdcYx1GFuIum^ zJ*3%W53|ddZ4Ev&*J2Ti-)^vDq=WUeZFN30*J388-xqLVgprM1+iHAhuEj#`FpgO) zK@&TxZ>#d5xfXLf17^%d1Ot6vtMH%|+~H}rI58_&wuabE^R_Y{nmfdZh{fVHl8#`% z-6cLW*WO_3>NQ$JU<1l}YLO4kwYSIU)q~9(C_BWW3w&s<#q1t;z+o`E`b&~+c|J7P zVtzN+bCWjA-5+B)9<-br)j+i*tDbOzpvY>z#)sxwjL@4wP$MJGzNcpS&|Hg|Og7RJ zCjEi_7|ZaXxfa7iWF!&};~{owu`SJq-mtc?8=V2K-yi6EONtN8wYSj;&ZN99zuwOF zT9OaV9p6?x9tKNTXa9U>TY?YG^%e&h9~Ph9VCq{e&WGOMEg`c3^9L-xepHO{p}FH5 z4`bed%c8gTM@f_i4Jz`jzpj@`jUGq94c6aAb^*NYd_FYS*UVNc7Q~HVvOg28@}arD zrgsN}h9G?az*_7)KJ*4(bDK%rgP8*C^u28dADZiHA&)og#(jD-yGPr0E+2Y>ueoeC ztJUkb_2-i9d}yw(nannq!DqI4+3n7@2p^j3Yq&iaFq;gaSA8wahvs^VQ}6S6Z6*ue z*F407mU6wtO?p6NF&Xv!m>T3mbG^mk^jOSJlf&GXP4c0+-eQaR3?99QFtX=8+X8%O zuDAHXToCaCSMNtAf)CBLw=?L%OcuAfe*?A6&xhvPJ7^5MgMoK2~~*6_7*C!LShYMfYN z0I%srFsI4KMxxX4;fIi1&vF4lPLtK)V`KjrE}q3WcQE^$A+VjbftSX5f#eJ)514Bm zd&p_9fo{V4r!@{9Ft>XHHa+lXm*>?(COZ$9+q(w4$>7BG0W-TwINdh9zqn(`Z155e zi^uNnZ=0~;g0vuuA^dB9xBRe17(EQ@uALssK?y5Nw z{2nWMfi4IXChBi4RXyC(fFf9{(Pw?^hVtj*p2S7r#%O63fQ^HuflZC2$AG z3V2@hu;?wKEyCXm9~4qTm+(-*F9dgUlhK7g=Kki5|NnoY2eybcmFZ+sseDzY1<8}rW$-_o@ZJLnt^s${L8e%0%0fxme4_6Jg(gnPj!A9E2iy3UA zE-z-Vi8_218r%otd}1~n>-F5H&5IdqpcXG?ux?FW%wY30crk;`Q|HADHcyQgGuS*; zKFo##xn5sacrk6wpCutU~fB*7c05aPuQ_JSZUX0R8KyqLjW5a7iOwkW}i8EjEMFJ`bs zeY}{#1KZ1s8EjI|QQ{S_aT;t0!EWBaUQF-*4We@e3twA!*FtpR)cKd@zc~L^u=hW1 z?%BCd&s{R-o;w_T190!`&RNUseltIs`N&Ll=FFL?=_jXen$AuuCI6KCC&&XhU$Qjy z;?x(Wu9*r?El&Pn@-vf{PI@Mfn0R{Px_>A~_;+w^Ju|_N& z`_|Zd$C6`G(JP{_h~6eTS9GHAIpIUXtAqjJv4Y8XP(Ve1{Zr#o*W>_)lI*gM)|QF<#K%=ppzjFKBT1 z5IoAWn2--~|n~t>AuM&=4oOkI#t)W2X4z3|Y_$KEVqb;zS?k z1r2ecdwD@aoai22&=4oOn-?_1iSFVB4RNA7c|k**=wrN~Ax?A$FKDns2yW*A$u=Zy z_O>{JkMe?sc*I9|K|?&^HeS#WkN7YzXoyFAh!-@(BW~pd4e^LuctJxv;$~jZ5RdpE zFKCEIe1H$M;Xa!6h?{spLp~G%x(V@HTJ~aKl1;K|cS_`LEBvdpwOd+qGGvnS5H zF!RvNH8b01PMCgv`ah=MGEGh&CwW%#S;=J*zvSqt-%WjT>djO5)De?UPd+e7O}Zuz zpZL|pCnk1*7XS|$|M~b` zymGi8M$RIy9Bylb1xC&m9yxL;H##tKcJaso*#aA~0~k3wdF60HjGQj7oQ)FHc~q4eCO1sWjZX~>{u5YvzbPeV3>n1>IGmBWn%tQ?G24j078vGU5{#sXH3g;x$2#Kw zA9sm=AtuKj8`~y&O5_m!OxOW?^^a`c26+IdRZ26F^d2`SSgiW>HIvM_YIbAYYQf;K zDB>2MPA#?Sy={|m*?~))hH%7`R5o*49PNxXPQ=W?LOpH8%ZXB(v$HCkMps=vtr!LM zj43sca5j+8PLXZBUX7*Q>Z(O&Cu3yMCUd$K=}NUx_2X_rQ?gqD!W*6O8d zXSJEFWnsDHQ!d z++J<2Rl?A6%B5Dax|#>|bMv*`5=sF?ow!4=XwH=!HL1>FDkWNmaLJ*^3#NQgp=+zH z4y!L7s^bx@t6~Tlf;DJSy^_jwSWHSk@q ztiGC5OJ(h}D%Uj{Lg|Fb1+7c(DFD!58Qb7!EgXn=nu@Fq(;B<_@ET^bgsh&}YSvK+ zr&RJ}%vT9%@u0$O+kzExalI?&S_}Fzgvsuzbq0GSe}lR43@9UIm9Zx(iM6)NnsS-& zqAC!mxsA$9+d<+|(u!9#Hc%?v?NG~IC6j)Wl;N&sfHEtrGI2~_)u}xZtF21t>d^=& zP}^0x^~v4ciS}J`+A!%oQRjak#HquhSsH5TqGKgCu_9=wG1TDF@#7MyreeY01p)_oGO<) zl=f?I5AOBjs#Jl%l-9H{6-q5Hw=?jP2E~w5fjl~fXthRVs}}Z4gNcH*Va8V@Nlz%~ zQDxHLO<@nI3$?0sLMM0RLt&br~Y!B9x>XJ!Q&u2<5t1%l3XOw=6HQLo>>Z_4> zO-X9VoGfEACh#1*H90Zl6d;O@AtlXPAcvE&6ex3yYeEgLF}13xEUQB4Ca6}_X*e2% ztlXe9$UFh)Ch-p`t`|qClWAQ*_ZrGEdoG=ghMe9&JC@0YvxT5PqQx|7jon`MhZDJg z8w)SjOmg@U#m0~+>LgmocEO^w07}ot!ZI=#u*KC$LKi5zh-$ZHPBi3-rY@o_cMD$J zM1ZG8wzyrnEJ-vPZgVng(TD1RFnSEp?Y$vm63C$hSb=CZ1rHhI?+5nZxsbQW=w z+}_IINq00`(X~8y!QPfTwe>(TzB#&Zk1&BQC$PFC5_mZt^{g4=POOkMG!p)-naGzT z3VEsO$%g}(RERL=b+P4QdUJH)_Dh;N9;iYGkn$R7wHduJo4HodmjbSEJWrNnm6Y77 z(#jq2q{^CCCRT0PNYnCo?fmrl8Rh_Jl1E4w_d*O$}Intsh$D$8nZT?x9? zuf~sK*!E`H_CP(EmDx#qLapz_Rk+S;Zf0uPWI|rDL_7w4rJL6K0%?zT^oz;t%9yvk z+}#csvq!VKB-#eMIaJFk%JxJJ*Cc|WrY)FtWbKM-Ltcs+YPhql^~y~J5WGjepai1} zcbgA%If~T<;o0lW(S_SDY3fLz3LQTXUSEb*W$*ZL1dv3>4}?dCp(PE9ABO{ZO#H~_ zy_HnLW$}Cb=14jR%KG8CpapA#I$6P}y=$;#TmgAnT_lvy^47c5iR;5r>JVBLV9iZ^{*@h0z4$@z|HZ;Bg4wI4juw9u{Ns(Ey?fyD zG0H_hx^wkG!p$92kk8dA2R*ujFbbGQaS;56p+|82!H?)5Jg`0J5uJ^ZhE{9+3t1y4 z4I1#8pWPdv2Y76+mWfuA3Fg6;{#$w~AZ_E5UVA;ZMJqJ;)iJ4b&)-&S$trl~ZRCCv%5)7fdnLl%!G6qhOuAtGZ=8XTnt9`Trq9!(L8=JJYg zoRDd?8rfQ=qxTMeFl#{5!P{4tRVgPu4Bt`{E+RPB=)ep)3>E(;hapOw*a3Wl^UB+v z3HbFHey_@>eX#dr&|afX2~_^qjXJeXBb5&ubyByb90FCRlq;^TUm09lk>>N!l)X+w z8*Q%xY|r$WdPQZ>r3}6I2KQyub(BP@AZ_K1A@Cd>=18yQ%}Yz2aW=YeKXX^GIbxhOg z7c4WBgC2F)t_l|sd}-50oqW)!`(K~TP-jEVC|bX~e0|1I157HtXg~P)T*qB`g7F5* z?=a69(oj&VIu&i?>)_Y)ONOt$mbY{3x(gKWM-}~7_E&qgnqjx+Zv&@QAaD;woGt^D zjYspV(R?;a>%N|Mv>B~O>345cjE+5hI7;^B-aU#l_*j&FJ(u}sE*sDF{|#pIXq;{x z^A|SIfVR>d(4XPy`S)KKYqV-59nA*L@SSS8mXpT9HhSUFO1twKt$(dtQnWoKwW98h zIWm5Y(x1`g()&Kc8_b15N#|(mwN8)OMy$1R1;vU1@2-$lzhbSBCaolt~)f=C)ij8IPK%QTCJ68=x4$QjyiLj%tafnw6mni z=5z%NJm;7+ua%|oN-Y-jI53qpB`t&b-sy1d^SatttVI3w=!!A~Ud~!6#Vka%y;KZY zmGwp}q-^KXo>JA}NvstTW+k2hWnQ(mM8@t&=2pt!u^EFs9O_zJYikvKK2Q$1e8#vd znKeOgfId*w`V0**8x-2mbTM&l^4PMh^%Cmj@{iJsG|AOtA3X%(HCB?WShX zWQ4E+r{gcu+(XkN%({b5eGpFC|Gg)>d@|Zh?zPp_f)0A!=PQj|r=w4r2F_P;o7SVv zxRtf823snqG_6D{yF|1@-L;CM;cR83vYdQIYS*bqSjd#NaeYK5Xvv4lr5?noo5igcaf)RH_J$h72|c2}+N%WWZzu@KnzNRleG8d;y! zDr zdfh9Il)+dxrM2~9qg6LRMv`9B|9zey-*EZ@!Xa2EfRjFYhqc%3H*NC0ZWRzVN%2y# zo-ESG9N^q0S%`syoz30bps{j4FwY@X|eh!ZOR+&SF^~VHipxTIF`C6i&OsyUo1(T7AtGz_{b)->aFxQ)9 zX*J%8*vOQ@iYMeLdlSpKboGuRuUGDK6eP8==!#xfRIFsJ-hf4xz-s=mr|#`I0t2D-~XR0yjiet{(=^K0q_i{{C~rIc^;cTcJAf5$LBseckx_kPC7R=``@#ln!Ro| zJFA~PZ01ihkImdX)0y$koIE3({{HmGr>~rjPV2y%fWMJ^LGm6+1-t~XW$G_e|Bt;l zfp;Y>>x56vzTJBp1Ox`@zRh91q;Ks@vsS7qdn&1U{yva%5b5hCWq%No7`jf-& zclvVb|Ge-2ectzZ-u-z$zWc9t9|92p;oZHR-`)9%op0Z%f%gEOwf*PYzqI|H?a{Wl z{o-xk)^Be8_|{vu)Gd1JDVu+|`7@i}v)S5=Y+l{?;>O28On`?r#B?%ne^7^U$N7Y}l7-1p7nAe6>LKF%6$-1pAm z2pXl)Y)6YU+_>+V$5AMaM7r^6%Z>Z)c^rw-aLBPM5jXB%%;N}@hFa2K6m{d?F^|Jh zicWWiOxun7f97!*N>NG!JUZpZeb*cgr%{Sf;sIyu#(l>;jzTHC&@|gkH}2aPaG2PR zbrx`MpU06Xg+_*=x=`M4o5v9-g)oLZs=M?3)_EL+QczqNCl<=_);SzTqa;1(CisQ+ z^4L6%LP;tzWGe}GzHgbwktj);`8=~=Z;#I72$Uq`idl)f(|hwg4o68`YIGB(8~4aO z4#dYqMx&>y7fU0ZSI+ndLc zC{AazVSb_gbr*2dfUOQScY2+99FF2-j<2*A{BwIAhoRsWnl;Ld-08LEaVU!8nNqaA z;7^Fr zuFm676vN8VK`7|X_jL<6v}0Sg`z@aR;yeyPF{F`;Hj?i2UOSJ2Q4EeYG<%^Ry#Fi? zBWV<&&3>n_Fpfyg<0urNxP+O`xbscU<46=C8Kaq4@Grswj;M~Lmg7z@F^>aF4S3(? zQw!yd&*2Eb598uhHQ-J!HirZL06~m2SwNSq=S-?S2u9jTr-(EbA zBT$%@gu2~x^Lx=e4o6|iidTe%`gq}492n%FFp-R^fd#!>n!`~P3L`RSMi=tkeZfZZ z#yA!XUSOn99n=eR^bmS~Z$3E^g<&oFZY#2VQJ^Ie!QGtq{%#(JAvBeZB!cc?``+KqD2-y)Vw= zV1&jshby?)fNoK*kU}1L>!HRtU;p@57Y;l7G$SYJDbBh&6ceN z;;909ElpRg0mFCfaGkDl;3Vf{9lZDDH^nDKpX{Ktk24crGr1h$!Q#%pYPzDYfxc2- zb3Z8RbeaF`(Md1PYLdWBH_XoNZG3gS##gYtMo&D@lXP@)!B>H-Zgm40r)U_6FSWW@ zuW5{|5IW9>Dokw15BTJ;Tt=Ism>3|vU^E3?M@w0VE(u?O)sak_isCxqY_D&P{!GU9uEfKu{{*qxvp)Mat)*pkxD)%w8yHDsPG+F zyOymh9aYEj5k(*dlw6CEj#Q56PBTiNj7;Z8RdkddHFB-hj^>k-qJx$WPD6^5mOeQR zd7)7TTrGXF7u)GBjAc=3v8CVbEUeH^0BXnbEC_~zE9@Sf8v}`?|lEY zcp7pt={U#YZZh6pmh-K4L10b!rl3TKDAd!wyD2Ma9 z9jNuWjuFN1a5YvP>9jT)MG_*9iM2XCh6AhZ-p`7@3b>=So!Cn zmmb%ytoig;iEC?nyS}Zh`**jz#Mqmie_FgVc7C+BEnnkXK8T@{-}on%H%NFP-26jF z$JGhn>!*A-y~OC7@x5_(b%h9bn-hlcA5VTG?^>DX3yzHmPxO@MhL;$BOYZ5NuhESO z$9o={{J#F3D>J?8xH4h-!c(T}UP1)Uoq#)^P1rr}JEy<@>Si1v zS3mO5UB{Y>JPpXl$(E2qD|d1Z!o9^W^o#rt;myhMbY*W$UWTvPA9 z7oTw4Z!hP#7#{M@W5q>r_sBz_NRJe!&VBp-3B{e@O(?n-v>v+iSawm|Ir0!c(j&#G zZ{Pgh3B|3Oj5HR40zUNok~4~PuKnEVp1OA5CnrSLzi(AtUOUcD zh~Dt*_knS{hp?10qDwscxpXI<{R98-mI>Xp7p_Y8dB?d4-5ZYmzTHhPQ7vb5&p7sT zX-*vb2mY=y`Th6lRcSu|IP0qZ4KMqHGn#XL{am6Gzy5(Qe#hkZ-~869MDIM7CPZ(z z_4{_$y=*Sbh|b*lxfCaE{R4kJ8E$^yuODBP;x`><=DhlSyIvMzXAEav{al6h6keAb-){?(gMbg#cm=KpuDscTo>c==109{?}? zz5eiT4o3%HI2i0dzTeyX{9b4GFLqlypWAV^KfB%7`jahl^N%)b8=u)Q)<3!Ozk)fXH3`N20IdM4muY;gXF<-s=}cqZUsq_+xz{V4$tiC%OS z?0F_|!Bwz3CE#ILda*ITAAIx9lz@jA-(JQo=kMe`_~vcT1UziOu0mkTGl2^|H}l|| zH>U(Vj99(&cdKY%!!v;ku7Y*X1TMG=d{Y7*M&uV=1#6xOTxewU2OoK(X95@71|E6P zGl7e31CKl~CEy{H{sp&zN8aF>zy(*q>pc^=;3^nT33!;yU33+UJQKL!Dj0euaKTkD zm=f^dTrRo_`ko0~a251C6S&|i=uQcENc5tspyQdq1y@0PO2EUBK`-;`6-Gplw59|+ zoGH23kUbAR()3KgL-^VAV+K9=h~t@nhcIC06PVopKlHtKZT){-{*_DLe`p?1`&+xe zy7R7WYm4681|OW8Pwj8%ix0gev46Tk$MEJO+IoUxhB7kj%SaGX^K>r~n*O_^hUjsb zbFd@_&YjhnK|&ImN+{PWhpdL0&y6CXuBD64Ai?ni8L`YrsBD#L3?1S47)Wz0Hvpau zz#5Vp6(-*~$Ph!pUJ1dvi8?b(N<5H_RuYmi6voUTF7a?eYIaJ9P%VY3eGL_fBxei~ z%rp;VnnyT|Ya#H?OeRp35y8^X#3Zj#B0tRwq>a~=4B!wFk#a+^W3WjlkpwJB<)L6v zQ>$x6Ht7Me(# zFBkK@79?AZQa7Xd!@+QhH9EIp8{;mhjfEqfTpcJaM|1HUHZC*+0n;o+S;t2(|NFdnD7TUP-IsYF-l}W)rlrAH3~U1|=`bEO{%c7jlDmJt&ko zC6j@wyu^!3R2C>I4#Fq-5{CAvK-kDINr9ADJ3A6kGYX~aP7kh?^H>jo7-#K=Za@YPUO_FK zbWCm(FE_HFmHohNsIrSP%#b-w26JN}U-d^D)VN|#{%tH#SwK#f$_a98M~G6UprtC9 zm0<~;sAx4tMp35Y*vU+=tG8-`=&Zfx_ETA6d_k>@>y8B}9f^l?{irFR=^WNeX3Zrk zV`~w<*TBP-B9^ERLSm-Kv?7APCM#LczZPr@0{8lzXu4d;gI4z5+fZc}Wtd?LDu)A( zp6RAD!D>_w4edmInfqtCVLA|u$5Sne%@I7*LW(F_7lTqlY)9>;Q40_pDcTC?CV(${ z_id=Mi*o;L2iEPN6|~zvrWJxOvw4tb;Kn<3!;>|Wp#8Pk@f~upc6elPW zbP3`}OIf{sI$Q)Qd)IBKvWxO%!7K3(5Un}mO{M}ep~7$@3DtUpAnY7}a8!u~c^%J*y8 zV97Y6<-a;l%kINDw^Libu&ENCMuiM4B*Ot-8ewCt0TH6KY<9_&sYZ;)RJNXKMKUa` zM8TucNwjS1`JT~5I872HTtpbEf(_1S`SaNZF&vT#uzgG)ORB-d9YA*Wc)p2#&Z zRmv`D%Uq7oLW0}{1H2Sgx2d2#$Ti!<03Y`Xp+Qs@coJhwV+4s{+2hn>{N;IC#%^aV zUzEq-g=!hD+6i6*R}JNME?>Y6bwie}h^ zK-HCE48wwFJ;q<0r)Bgu*7Ai-dxJ_mo^enG3Q9?-8^}mjF40)JOq`C2(d8ATa3E<>bPS*y(T>BVajCJSQ@}HfjW%rf6+e*t9<(ETUN2n=g0ina1nM*0W!jx*ACB0uD zmTy&Bo$R2~O_M#eP~}viFP3AJ6rjRACK(;0Rg1$zy-Jli)BAn)Slzzw5%tcST>7BC zYD)IJ6=hu5R+P}F=A?R4R*|lp3(B+rrFnJP3_gq*I2AKuD3#2UM58%E26Z)25PCU3Kk7+D0%%)C+Gk7*Me)8ox@Ka z#P&Y4`~2kcNUHSuZune#WYT4Pgq?xoTuouUP#s(E3E^N;PZs$!9Aqs3RCC5#)WZ_*cv}}mZH7S309MBWPL}G}8Fv>+ zkZ9%V0=jgyJP?-sA-g?b>JeL1{kdT}C1k4sGY%ESF`Y9qupZF_xrCNPC&RF!lF23` z!Ii7zzSP5a;+jk-hs^p>^7mLFpD_DAH04p$fN0w{yv87cKDDfGI*Ql(VYzV0i zlccE>j436e>QDC&yaDyQVay~ouwc(P@q8l81(Q6pdX=z0@$enF^Qz?bo%MplQWxd8 zz~L&~ z&hcEQ6*b|sU9OC?i~=P=5I5Z5avePdGh#^W4MZoDjx&j9Dwm6|TqQ3C zeTj5lm3-AEA)a?eCPxY{YG)*iq{B@oo{M0b9nIu}LP|oHSqaMsY*)4goEb6IKo8H= z{iazCqFg1f^_hlEwIWcf?GK2W-*C8aC@V_ANI0}|mE5gAd`ITID!F}^I6#$L)Y3DL z$Ha_K%OPnrl?wuoBMj2qvITy&7LPRzlT;-_Nta4gw_8jlgUUEZmXpPz)l$HbMyZ`` zH63E$qzZ*Zwh#;?R>?R2pa)2b%l|# zxooJAkXN;l4}AV54__9~sgSSQ2-tgtfP4{(0 z;fDlUAQhtvUOwe=Ap^WKhQq^QqC%)-Am|U5z-vubrmIw1k#L4JBqk#%d{|h$ANjza zij(>O-D~pNmG8RplFJ{u9KZC_m!QM<9zN~h#}4k?|M&Zmz5lsq?*8^}V&|uK3gFeg z`?vmb>!Gc0+We23nT?;@c_y=W!BR`6ymT!rg2dM$HhbCU7$rjv$pfIO}BC4%D!Vjb0eMgB8rA zvZ8vNnw2EEaxPv=;v6SI9f+s4suMaoa1S5lp`oOE>$6kj=~v++%i27D}qO5M;v9pFukmm|203&~!z9uRqz&E-;> zsbYP;U^~u32gh-c*$eO2eX*4@JBd+pvxO`7{O^dN!uez?p6VI#Y|SWVz%EC4FrxCt zKn)O;wifNCD&+**YehfOHmd~x6)12ie2V*T4Z{%AJ&bAb!Mz_pD*}$YG-x+lTL21~Tybx{o z>n#}=0ShvlSU$UQW?$a&-E&$mF-x`Mz=RDbdSGQXGs_Q_Bq3QVT0%<lME&&S7%nHq%9|vY7%XiNjy@VJAbY4B9^Xdzt6O{cyo!f-N)PQHQ_6apK^Rm2*7f;5sz+En?Jw#qg#QkJ2pSL_H!G; z#&g#H=*ma0yz@%^3Uy`s@~>Zh@8#y@S6)86^jnvH_)`B8cj>8zpW66$8*e)NvBNhU zMh~BP@acp9aBzH(I(Y8>pX~qS<})^afB&)l-2TztU+jH&=a+ZBcXPb^T{~aeh4#L6 z?=^cb+5N)qFRXuP{n2%4{chjqd_V1bn@{n*XzlUc(q?$4xyp6-(1-QcjmTTTk;W@eQ6Op6XfRDJK#}x|$=ponmKH={1CW zy)?=jR-CDZdrfX^L9w9h?*~z5oPaLDo+Tj964y^8Dse0qV{=@?@z*f}5gk$p_w`X@ z>~9uAS+FJ5*7^n7FE$e!ANMTru@ecc8$!TFV}Fz$nVb8dDC?5kJLH(tyD1 zI^WFdsvU6lB%G#{Vl$i&vz>UXZIMbNmr6+3H-4XIiSIp;u&N0|%7z-%UQB5C zlP$bltUE&+k2T0jAKY3g46zi9Grh)O=YbQ6q6w4HYyxSG)j}qSQz1p{7(*(NbJEo| zfwPiicH>lw@0-v1t7mSYzyqA^p)dr)@o&6*vk#X}@?hdCdxn>tNYFMK&2S-f&<;tp zdQP_-$BZ<)MX(-GQv1zZ6)eI+&0vQ&pZWpM693_5bPsR#;ou|>lA}1Q-Eks$O2+bX z46g(*R?V_TOB)%ZTo(uD&&!0GtU;UK;aTF_Jxjd(L;_E<0xRbQkx7-PNXK6mqMQ|RW5jwB0(w4^Q5i409a&-!PcC7yF4;i0d6y5~HeeIntZ zH<(`O?9ZFPy+ z2tdOXD#GYi9x|m)0V?OI6iewzGgbE&dq_hoB~v9R(558la{7doGKe($sX?ccs;S*U z8Hy1_zJu2#8)<-@?UB+XbR}ej$sn_hd6qyuOCTo_1vp%@FkOhIvhh%WCel(pjB`+X zU?WB-I*wJTC@3CMN98L&;926mClXSjUDdmk6A6sRJwK`71d%935h&MYDJoX3A(Gg{ z@}g))~;W{G;cn{71XyrK>V zd;2q1t4r*8me@U!$mHV|74MtXzTY3WinPf@$3g;!`Cum^a$Uvf)rO&Hznn^Z!%umZ z_)jMiY1*&k^1Xfvvx_0JBZc`;L`EcplVP)4hg!DT?6s0y(GP9U?8Ae^%sxCw%tpvy z9eD`pdDhQ`!n8hA!qGTJGT_Ka7#~$?BxK2@!l;0S>)9*6eNqk&693Dy#BZHQB+zIs znGHsZRR?j>Wq+@T$AeCf%vRuFxoY!Sxq;O&4y|>b{%+3_?>do)=W?P@NWtY{9am+` zw1cfgPi)tF6|4R>FK%G`jp`oW9(rcIy{TB*LR%JkgBt)uaYasx_#fGsduj-l}&n30#fUm2#6Lg+2vmeZS~g z!h0S+Kg%N?D9Dt~LDf=~!XrXR#Ynx-7SYPEoH8<&NoY#k5s@^%_bks6&-5(ujVBU( zu^$*#1%)P31l}$UB79flN^}v%C~}}MV%st+mJ^{#X6;j+C4R@V#3%RnKDPGUwYRMu zyyoCjd-2_m>~cFF-g(9LPi_-i|9O|cZ1dT5F%Ke z&*kB9V%X>*XsVVt3blABt;Uglmx{s-Jz@+;sH{pk7(^6eEJ%$(sy~KsLA$Csolq=R4fkVeu4e|= zn&#rL_HCye8r?Qu;Hy$WQ(KJ@*-km}41)%T1S8lcC+p*MF9GIEc#k>q_n1_$*cD^? zI46f?U687Nt(U0ftb8iQ2Lf=kfOe?-NG)cs3c9R<$)ylvB(`O>t&rs6u=Ac%4z_}q z%YzYHtw)r6D49*LDX>6oTaLxCR0d@ za7znSS{X}G#Boz3B2kXIIu-)WQ8UV=6IP&{)Z$j&#bIN7%E18Vji6d-Se5xoeHb>g zB|58A@Gz2yXChH4uQQ|y-f~Ark)u(qX62PuEU59-LMxq3HY441EuHUyZDWE>6*b-B8pr;ek?v1#D2vcC}t>q#=%EB{5M3 zhbEL@F4*iutgPl>c!(eAHq#;r1kPAR28oRWRi)4yg6o9xRn`QT-s}?7OhvR_Y-Dhz zTMuu0$Tzi3-w~mKV;E(4+_i>|*zbhRkzxk>aM6$kLO54Ln7$^Z`%bO~8z5Y69Eo>g z?QGO;mxL5jYs;z{ZPjAqMpbBJTV*N@3pFIssc4`*u#v7Jl$xdz>-8$>D(m8~b*cu< zA#0($mCcOFG(4oCvX;*|gb|Ag%|r~c_-qfDRkqtsCypGVRd$BsOtV|*DYdR)XnnX3 zg-hj3z~2&;Y&^!+MP7zSJ^5-$$P_Y?T23aak+$4SjKeMp+rNE=L%^Z9;i%M1w48DS zAyCDLG#dc~X*DuIh160sOQr*@FwPwHwUjz;3Zm8ukaT~LAM|>Ij-w*vn1>pX5sd{5 z9o*JWMoiFq9q-u@ze6TTay=>tep7$2)Sp z4iU-TSO*tpTa_L|RLCGjGJ}c|9{Ul2M-xXyw3+lLo7_m@5Pz*rRMTL2&1C9?A(NqO zq!vXrT9wM|2o|q`X|bjVxqxa#YZl9Q%59e#+Gi*P(HLPjNU|Rl(gdfsbFEs^q%E_U zEY}A`9#Q?~AZlS{=qSy!AzbtKMzK-|M@Tj~=Eb@mKv8js#gffJv}=e+c1YDr{MCL` zlpD!mG7ujGV`JGA>MjazJgtcWCPRfJ4!TJ{@C;BQ*puKaOjQI#u5v6Q;Ng6a&^XPI zkNS*}2y~P6sw_ENg3RYf0W_S*@iEcPw+ms1kCq#f8R=j(=PGDF3RniS$0ZP{Z@?tw zfQ!O!64h<>%Dkul*;m0k|hz97oa1Y&(sa1I}FE-vL0^+B~zsbCDUZv z{*H_|ybb`b8-H-hA=fUoIap!B4p!tN5yc=n z;byc~qXv?n55^@Z6X}`qxN2sPa=KQ_CyRY4(ASN&fTyr@L&$Z(fTq>A_=3M>WkGyI zPAX*vS4+vvaNO;3UCpxFc{}d(-Fo;pU@R8+%En`-rRe9OSS!fHxv()LJ43lcXHtc> zKiqDnFvKt7$!>38Hp|k0Ici2aPKGSjvT7+|i^-l}P&7Z3Rs@kRWrk=vjZ$OF7?+v? zbQLW71K+9*1)ng>iIRKmXU|(q7kiJ0v-_pfKV5|vP>0bQr6B!D zO5|*RLoe22cC#hxZB+4(g2jMVMyvVQxNa99fn#7SaugifQU$B)ImV2E`9T3(szs<) zS;oiJK(-NQ;{H4r(~Veg$Xtzqb zo65%$SquvqR|jl|tH%P}UbfV3C`P^Gw#~zzIpvVEwFK&92w1VYaywm3M+X6&tXJSs zoaFn`h?3(LYgLk}c9c?D$x)^{YLVSW3@aoAIc8Kx^-&9n>OHjBfD7SquPM|Mdg5wd z3DpaHL3CKv$rfyrRb6`6KR)Hq(!x$!tU7cThQ)3MOia~;lgn>5Fg>0T?`k59>CxRgX8`Z5mP8LKJbih|Z#1CN<6@om_ZYVg? z7WA;+JmoO9olrFys|89rrkAxCuGp1yC|owna?Okd5d#@p^XR|6KUu6>UM!Z!U2ZE;6Y{hD&l#9dewKE)eO#~PCd+CtXL#6&m ztx<8JU>6(|tLOY`h@k`gh|p!6Kk9OATdgGJP@Im#avV&j^n8lZ<#H(*lblh^X_7%F z)(DRBU`(y12cl9I7^^k{ksq7&j*G+Ab51$*l`IeZ3PO(|u*5739e=xocbi5PPGL@i z##6zH-7jelB_1_1BRZ#W{s>XzEU4C%>T&-_t)SV2I`X%IR!u7J)%S~1==y$#MMUM$cFh&CDh`~l$h;T zLM{$_*;5X!K4y^JjO5p&b_*JJm8Mg~lVi>T#cKqjrDS`M3DZqEWgbPV)pVAo>y;Lr z7XsaQlPiS9zFNQ=i7Z^uHL$%mJGxkWo6qOvE$H)4hrRypu4C$Uu~xqO z-n(Fge(7Czce-s|Z4{k$S#Q^D1NTMdi~vV3_voPYJ>AKHT&)d0I;k#yHx-qz`v(O?{wAfg#UdL zWlaB1(bPwi>?i3u?V153!T+0&hkN?1*wNd4y&a*BvzQwb+lZN`$H^C)5K%)dE z&<4%stQq2f-jisCB#n?aGsAwO)UL5)3Xipgcn^}X1PZqsDX7*OS4SGt45jk|nKHtO z5@`1zky|mtxCl2(s?bXtR?D(NxvqY_S19L`qX3xs$@y{eTDoHM(dM;Qk_@Pfk~fB8 zgjaIl8Wz^Y_~5|Oqm>xUSK5VoCkq1es=3CBW;iK2FvCaN$JvP){_~FS$V210Ff&B{ zQJSHLqu$HR@brNC*T(#AOYIYyIyfkNZ*pqB^@Xb3vJ_K{@^?@EO!(dtuH6!tg z6+=QuTT6wwTJw5Jpa_LSuhCuVmaVQ-MM{cMW`@Wxg}2h2mI$;X8B8}SsdQgvEy|`V z>Fe!!jfjH{JT7EfWDQAyqrlgg(palX*MzK|X(UtiFv|yqNQn(y18=~s)cKL1=)mfx z^MBvZukHQW_S3+BzW(!%Q3H>~juqfK)o**A&&$oKRa~dj>sqzxAh)7g9evWfESW!$ zUkl&q;T7?z>pMI0KDnaTsn(hk$7AVgf8Os*CQ3_(#E2Lt%7Q-RrLG}$!@1Ic!BLh+ zMnP~;BOQcBoghZ3Ryh(o@e0d_0i_ ztzS4B4GjWdv(yCk8eoWMOHw=KfT2M)(WC=mEt3tK zTs+^;SNO=lk#k7A8$>0tGZ@!{`hX)K5W<;C4b%}Qt~6Mi6@7_mdug`7wBL5=xG*v8 zcf8o=#VcHxY2#lD)Alf%oLGjJSKd$i*t4CqhkMM+CS|9c-q+d{%w}XjDdk!RbStAt zbel=frrM6}M z15+DCjbvYEJd8_fVIiiBM>L{E%Q9CUM@Ujkl*&vptJg3z-!l=jb!}9_Qlq|TWQX-c zuGtFKkZWushgYj986mF|K_`NxOIiiEH?bhI%q0I6)?Dcm)t@x1;HG@) zjn#q9bObmV&zxQIKHG)efKFQHnS})JYxJNH3q(#Wa+RjKtkXNob$;?OVLmC|N)|Sq zH!QWVTPW!LphMW-f6H2G{bd{b@4T{g`4=z0^>Xa;C2${qeCY$12A5uU>F!HghktbV zi-$jQXdT|OAK6FtJ`W-S{?y+0?7d;{m3wQuKfC+LZe#bJ-KXq4zVmB4AKV%2P}^VJ z{=MxV*nV`|+zxM}+t1nh$o9_Gk8icNvRn6VedE^p<{xkVi_PEIeA6bgd9=B=@#&3! zwej{1aRb|U*81ZIe|GTxgYP*|4qkEaO$Y1yzq9`nSAOQo@fCLcWtSfJ{hjZ3eE-Gw zgT9A+_xYZ)_P>2Ex%9WbOKbmU?IZi|+<(jdTh{Mcn7Z%3#rN8UKYslW7o>q#ZmqxY z{&in%g#a+{+!4LW5Y%VB0*K$Y?t9(};`DqBK7{r?Zzlc3UfzjG-DD8DAbt1UTkEg7 zwC;P&Ebqll0N-n$y|sQFK<=ACZupmu@BZg(t)t+RGW&Ek^oH*>Gp)!g6bpvjFEIG- zdj`-7NV_l-KMCIBQ)a2=S4!0#Le(eF%A1>sFDY+fR^IFka;CibS$U<|r@8XxX3R3T z#w&QH9YyEjZi%*D8MlHVdEUVNk%@Z%`6KB}u?9Wqd zYNjw@_Qxd(OU@LQm_g1cOqeMwKKnGMu*9rrvDwEj*K%QKzUcz9iO)(Aor%vB7MrCS zSt(U_bVpxwri3TNr%H&-vI@`AoGC0k!-i&mo?=5Yg#~AST%xexOkw;Caz*ke8fx|Uq#xz+Udh*s>Wv0 zbG3-gY7w1%TvChZtQL_OG8FvFAPg&g?P>^$vOQz=lNUfFMjd5PoI5Mt(w}=!FA2EZ$bNvnHI}4 z)DkULW?Ix{kTY7;@7!9aK{ryGeFDdG9J>ZGC{AYq&P6=aYch01rdrQ*Kok@SwElGdV%%-X}x>28d9($aM=v8s|%5Gm?#|1~<~` zW?NwkI*DaM%qY$|xG_=n+6`5|^=7Jek9$rZ z(7dfweNnCY$$543*m&a&O+R)sO}htqr|)9kPMW^W9}_(`9=xIHx7DMYK=1S= z#M?>J7dFs)a-NSoHeP>2(~sOt)9w}D(`P_$D^1_tZ>Js`;~ScO_-2}RkEl5yFqMRUjOv^ zPp`jY>ysNlw(;nCaQ(Ty$9*65z0dccFSqrK&ChQB!shp1{%#QeKYaPFOJBJ3@k>8) z>ETPSxrAT3y#2o2M>k)v^}(%g-KuQow(r@ocY-_5-F|%gqlce9{OQAY9NLG$!{;76 ze(=$Q_Z>WVkUO~NV0-_Q`ybr@*8R%Zww zMcnI8a7I=r-16BJTfR5%+sj-1+tK8z<>yhW(+S_nT~O z&-}u;58~(E8p%YpH1KHW z__fnmDLvnjhnI8De=~MIWDyF1dnrrq`{VGkYkVG_)tz3m^Y9|=6z}1k+~wEuyb!z} zl>E!yMml>nFWfLSCSfM(`E*s{qKVGP)niPrFP#%D3`D|SB6arO(2XN&t~*XoL<)Oy zgQZX?0mDEmOT!|7IR2x2CrSp(nH1kKK=}2Vt>S#%tdn)9&0ACskw6&jYIb<*A&MG$ zjQ}C!d1_b-iy4%rQ)sqIjIzpr$iY;Y57SgV)~d8K{Xuk84hHLu(6HVi$Ek6((XsHR zgcnkyT)78=f(zg|iY^3GE!J4&*4nh_;Ih32%dff1h)ecT&B#ct@8)AshkXOTCTVz~a|A zq}1ra(Ue?h&^Y!4I)Z_pfO$gm_%E<*sES1i}R;^R(SY_De_!@X92+6PkBab$&Uu*PA zffc}mu8*V|M5WS@nCrWtOj z4HPQWrCc>T%9rvnW)wsraC=x9_| zD}xmcZ&Gw%cyF#A-#0P5w-$X~n(Kub9<^wAOH65rah%*P_!=3ylYWji%z8ip2| z@R%}kzD$hVC8OR+!+F9cCIilMY+{nlt+sWTo_26c+xmd+2Xt+aXuzXrSS(0l7*7+y z)S%(0@#{fcO>&U|f#5uNK=78^I$SYEMpM4lu3oDRF&@8W_bEoS@<^4Xaw^8hJ0m{N z2eXvgG?-AH0|BcVBdri+!ln~TlmlcrU1zUDWr!`&fljWiqhOGIlK@fk!EdQa(I@x+ zeJ@%o_}11gfB4M4{>Lu;@TE6hDqMmt?H+#W@I!}hKdc_U`tX?tpF8--!MhKJ2dRS> z9jt+u|K7j<*#7JGsr@_lKC}1Xz3<*@?uGa6+Wo@r$98{s_f5NnU1)cA=Tkc$+Ijm< zb?4PP&)ojp_D8ngy*=DcZNF%HZR-vdby)*YLl+5GV4cW*W~!<%<)d|~5b z8$Z19rj5b|1TG7F3PdA#`+9Z#)!_BPHQ(oaAMw50H}s_d?N40!#NqQNl3yo=w#Kg{D0 z5Xrh1bkgp6JNVK(4#psiQQM4LF9&}=j{}jh(Y`F}C3n7mH-{r>3_@a@+;yKQIr!Uo z9ECx!3gTCb?)3g<0SAq3ZIE)~zBrFVQ1Cz~6ACokxW8V&5tdSjxOWZ@zA%r2Q5r9C z@t}L>=HT&p9E8%C$j4d3o$p`G;RqU~(QHSHEb#mMJdQ$XB+`vnTkiD!avn#bG#mo` zd&G_Vi+LP@(ojnpjNCg92h$zj8|{z4QHoA?hD_U?-gL+J6o;V{r8JU!&W)Sy_};+b zGzwzD#skjSjhpUlp5iE!!V68a-E`yrV6GfEz+qxL)>*)PdLBoj6dDOy&^Cwxxx zB~S`s40+^!KJH|veg)igCwB$h^u*994nip?E{qckdYP_c-@susO45UFf?sHV({=k5 zaMShfQyhhoRAk6j-0yiFOxO2MaU@ETW_?PK={3(t=NkXoe75AG?2h)9kQyh+x zxYXz--0x=|{Kj0pVIUkNItocGw;w;4?iZY<2csmS=#sopzrQ}89)yyx+UG2{KRx)h zIUKM`5*o6B=t8^y)p;CP@S>gZur+kI(~r;N2$UeLeki_>-pA%~I7$%JYROti?^ot= z7)s!I@cP;U?xXWK6eY0Qpv6jVe*fP*4nhf}p3Zg`?Dv=Fa0m_7Hj|^Sv(PX6_jw%9 z3#92?TXE<6-{x^7iqjcwm|tjDzqEj(25fcc9v>WhWFCj1I3ZZ0jC)=A;1}m{D2n5m zQnbF{cYk3XhoCqXVHkD6zx@0>4n}d*(3Q6P-Kc|~o5R5rio+?VsW14K|2mHYdVy@M z#4YIgXWclA?&YM!LV7PuC{{kGP@hv#uPijnb9I_5qJdGKH6a1e%KAmD4= zTClgDn8%^u$&_+*5DL26!G{)bXvel}_sP$L|2U6BPz-70q7C=B>EMI&I2a5RqYceo z=m$S=7Kc#;iqdjP78k}FKQfQQQIzVJ;)9I49RGSAhoLA5UUlXeH|~cQa6~JXOfC5F ze>IN-uRG!n7vUDhSMOWEVR}8^T`2Dl&EpV|AUEXrg>l3W&f{PdMG9?)b=>9nfdw3# z71aEKe|hgb4nk3=qsQz8|1v!Z^n@SBNE$_Gv)?H!ly`a(=oCkx2*oAL^g?}1PXe9d zNE9I%1H3)yF30pF&>W7aj-=K?d8a3VPH_Z^;C-7M@RR~m-B0@{yc5Nr@DrT3S2CV`tngAlg=m6zM|tDv|Sns)k1%SBOPeW4wFoXG|hZCYPN{(FvMRSM5>8W zMI6hes8dRn2P5};l5ac~P8fJd^{5g`Q98@^YOzAR#_93Opr0?awRqeNLPE`-E(*9Q zJHvJ~G+2c};)KD-DuiWT%0)*OA{U25prPxv2s%y-lYG8shqzd;-Bv~cS<;SzLf5Gj z2XuJQE*JSwtP`jyl!3*JP+zK$GMy(`Bxl$Kyg9fEVj7E~26*QeY-lh!DH(SAls6v7 zPZ)Se^$5qBB{&pH$U30%d92)c@}=!#iHSPXz*0#R5*Pz2K|xPGUqX1LuoGk2=)4h#GJ@A&(Tm+ALB z=Q-g$Rp<11pEpu6`XWs{(D62#v?-?bLrgjh8S;oz8_P*y;1n3hKwZunJ34IyMcYAx zrPmHL2mP>#&{O#o#AM108cfIGrn|_*EG3H+1`dHi!PU112l14?rK3X)qLFujjkdPy zk9Tb~N5W?E*B!O6Hstf=0?Y0qVsC|mFv{9Yj0b03b_bFzM&W9%QiaNuM7eDTy9S_L z9}XNFGFoblQoPVE6|CM+)>{_u?JgSH0#$prPeyWG#zaOogjuR4eYj3GbXr#eu_X)ULu^npQO5NX3!s}XA33zkr+ z@3i>fhR;$(8x0I@hI84di}W}1&P2pb8kYTe+CLyzq!QECXf{qWIZN77$DM3GOLUSM zeF5^t`x(p@v1_d;%CEN;$V9aaj1GX(}R+O^!v(mjKLu;+Y4!>V`ENxI^I-99u|Z?qdB zuaD9-97w-pbd)hE4Bj9xkkPK~W^jhUKt{Wk!`f1irk%DVV=+ek!7ez2&^2orOM)3# zQ#QMkiN;FWYGpvc7Abx3dVzrq)Ma0HkfPn?NWF@i>T#r?ud~r~4x?(8CdfSJjh8E- zh}YTZ6zef53{Dpq$Ut4TfN$EGkltmkrn|;i)n)LO&7}&e?-4eaEtrb7ym8V~H%5(- zrW6LJ2@GVQE?bk7*H=iCy!C()yph6#NQf-PV==gu$Pf_R%i-+6WQ^w0W{(sGdVvAc zjD_R=4q9;7Nz=d(E7~2_sLh{D6kPRK7DqD4U_DBDBdE`@>`d3w#h%BO^_aaCJRENN zOjy3>qP-Ap^EPx2q*e8~2*gu08@B!u{GuT#Fpz<|oU$7ldcq&KIU1C{#l-EV!2tIe zw0NSGwl*_;*5NheXm7v`rKB*>2@GVQE;s6S-5^t@QU-I<)+V!1-kxd=3el9$p>r86 zj$kaE0^g9u>Pd?f286&s2I?}EHU?<2)CRGas2lhAKz>x6*Y6F4ym+=u*)dzrl#OP! z+Fo7~16*Jr19f|^8ecTT1O_rLFmLaz%ZrAnz(B@b&h5R4b`uh0mNoM`Jr=46c$k^P`O)74d6gNiNNNR~^@;?M!ALb4&ZtgqL$nksjJOI$YxilIaLnhUA<@)P`12&6!*^{jK+-tHZheg6p!3 zgNppn0;=R^P)~A|BQr?Gi9~T|237GhNWz)N23LFK9BF_VWa5U5n?W7P4cqq&B1A{R zat=7sLnB9r8``#HI%_jX`Y8rzwqdQ=t*y5(tDU^>X5(YcM)b=v&Og$_H?B2X!k%uG zj&PMz&e2J7cqXXkIIedal3UGPBwvX&fDH{*)!t zS@ljCX9}R%`n6`4w%$-k@=me**s_e%i1d(LYqo^F{})fp^N-#OH@NsFL^2yyPlJyi>+*y*R``RlIlRBv&=|_9bIu4yGvz zYU8~#E4j9Pd#6~BP|oH*Jw!MUS89V%o$ssH>JEKf*z0s89ozU2`6M~lI zoDIm>Lj&&68g~iXbV=M{F?Ttq3Nm(GV7jfWO}FGr)$cO;vN${y(BXZJkxXYaXM&sq zjoMHPD&v=}b0t@{!LlXetRX)<1yso|TM5Zkjx1X;4i$>S-D_QWj^wJwmMt0Q2Vfq( z4%EgkTW3qIZQsk*Ivq#y>!gtbBo^sCjq6!GZzn8cpRJu zT)A-00=%G^e-LB=ykOoxf85+(=Ds_3-CS+XG>0D-3v(LHCp4ds zYOL0LT=Q|M#wyLnG#^`|yd{l~YCgIK45B&)j#!_q#m${!I~);Ho&}CTOEsRE3D3OP z8CxMw&rmazROG3d&`e0G@#IW!CMeZ-VkR&XkZP=%@z40B8mnh~Gd`)tDj*r^T^qoy zx^BiZ<5>fy(&bQ~R`gUDf7R|tcE$3N)0*cr&q+0&(mbnqR;ux&=J%T4OEsR*JfnF= zs<8%WwEj-2v0C$6&2Oa|t29q*o?h$bt-226cztRO*dOzljNMe<>loD21xKVKdSjc_ zcr-7L$yUZQ8n?zR6?t0Y(zv7=PidSQr&QxfjYH#*YCHj=+U!z|H5!}7Ce>K2v1+VR zja3?p#%TW8nsDnl8UTW8`Vas#wxWzZCGpHRy5RaRKIZz*hbr3u~ewt z8_@oe)m{?c?VnX1raVll@r?3N<)KoIr7P#jRI2gJ^iQULBGq_$`p45hmTEjT z{m}G7QjI64ADn(rs__JnNdJ*kW6ksf(+@~BR!`qQeZN#=)%1PS_pMI4Ehp0S52t^) z2E3KB){HW(OiMMMR;H9Gsm4>vq%tYhcv8tI8L7q-%5#K%&jyi)i5a(FF0r23p3`~L@RM+1LAlr{<@F*EC27a-(0LG61{ zXaU&4NHpIqM#o-Z5&fKZ98QPAu-P_eh@V07`FpqXCOSHZC)35W*@hMPzXygnaa92PurLsh);viq$w8` zpJB{d$L)YB7gBZ^ZR_H-ETeX~ep6iuUcq}U?hGeM42Jz?V?-RkDb|bFRDVLG7qOZC zgjg?PGyMs%Uc{#Q6C%BcP4y?n^&-ek^(V&lBFIhkCq#M?o9a)9^dfMemKZ98T;d}R zu@1zhx)LHC2w=CduEe+wglOe@lY^ui~(ofh_XF;T&u&K_1NIzjyodv1- z2_jvDP4yGTbrFEt%O?5>B3*<{^%K_XB9Kh3++G)-E7C>SR6jwai?FGFf=Cx(Q~d;~ zx(Fh@gH3f2M0y9C>LRSyJJ?tkL8NzpqqvT?WE<;rbwqjxo9ZHn^bR)FMG)y7Y^sYO zRqtS(j=?5+2O=E<9lD9$fk?+-Q@sO`jsdVEES7*-e8w^-(lOXn?_j--!Nz(AA{_&u zi?FfYfk?+-Q@sPJItC(rf=zV{*6R~&tYaY3Cjfc}8|xUX*C*Im$3Ub{0OSca)-e$2 z6KtwuuwI{FV;uvLJ^|1>*jUG4T%Um4RL4N1Pq3+ufk>ZVQyqi#`UD&67>M)45udhE}e3F!`LL|YQWa3$?lR9N5$!@%85DR?PXlQ;_YSJSlT$ZgN#5r z9^jQLE4`|=&WYd_nza(sU)7S|#Z;;lEds%MphS-(U=r3rkQV%#>%e^qOY4Ba79zTv z2}_HwhH`c7uh=fG*Xcp=4aIiRK4N8&m69XhXRXF$sp##JNjMz0T2h9B!DS1@YOZn( zTo0~P_gieY98Z!=*Bq`?TN!=NrgOjp24PBZ3*_+BV{Az0D3D&aH{%SqT||??ax7e; zGqz4K>!^D=xDmqI9$m%c(|4Ls)}8g)QoW8uvE9|C3qzaWm6CZmexR^_7I5Q!jY}4= zmMCEFEk>@l1=)T!)8{O>KS;?4(v+WT1gI{rVl?Hjn>)G8_3-Bco~v^j8OnS1u|iwo_fiONp4`GzhKi7RP|B>3=D1gHwq{FkKM(ngoZfw}id72{ zC{IvU@9zCiZei^j9NsQda&(L#24OvooFx!!QrOS#kt!O@-o#%Djtfl#ELGR+V58q*4oT_Ns2JU9N7%+Y*tAfYqLXL zx|bo0fSPC_y7^d7Y(wD8;k#@3#H|p_J%Tex` zr6rl+nhyMs+y5VOvHV?sZJOLR2t=l~WyJQpC<{>y%Hi2y(f}K188KB!e1jooGu+2koG*94V)Bn2g zlB0M6FHwl^*1O{iFL|wO0y1*ONg?nW*#u++cuFDgYM#JL6satQz>%^EZ1WHtA)CN9 z55W$efQ*3a?G^sI@RGx26Ogg_lQIWhC7ZxD&w;~a6WHcCa41he#y1DsJqHevO<8W3`2K(1G(S1_)wws%Ip!v3 z@18BsF3tRK<^wa)nWLufn7(-0IlVCT-KndlY?Hs9ym7KMshjxY#O)Jt&GVW&G?#*K z!5Q_Z)Gqbn(p?M0LVw}e`3DznQ9Y=7mnsHs=YK%?dgYYjyNZ8adfSp?X>rltY&Bcp z$bgu5{RGgk0KxF1al6C>kWm4D+bS4)RUbsqP*MMQBdCqyvR8|Z3OG!`*aVRN5%JhZ zVeTurDeN3T^la&c_9fzgVPa*dDPmjU#R^PQx^0CQE9Xo>r%eF$4(Xk<_e-xS#5e)8 zMx+;Af0#IhuFav*d9{!66z0&>%Gw;-D!zGxIiy)!>rjb>_ca|5oD|*E(Hy+3@H#R4 z!rFLAWWRU3G)rsaC9&xG@zNZ$HeM1Tgz?hMuZ`DM;mjM1m-@=J4wYDVpW_9K$4hH9>f9=*dV|4M z8P`TbV&Q!aHcW`W=vEnQ;8CywmMnSj;ze0yT^m)2ID1D`Wm+3miAC3ss>&?0het@3h4(qCr1%-8`qtWLNQ565jXT#yLt@blM&mnc z9V!uGwL`zT)}dR4c5l$3iu=z7^Y|<eNsz63rIyRFtbq20l($X!rJLam95@&u~F5COHfD@rx$t2}(cAGJ}L=pOQ zui%e*qOi-H0qKiimMVHFh#ImyEGq$)EC5X_Cmu``8kqN7n4fM||FbJy*_W%5B_P?HSy5zcJ@GrQ6-2i-{Rp z0cu#_VGGAbbJe6LXDoVC=Ag3@jJn0)*~_jx83=cqu%lP&#a)$vF-RGiB<9TYgT17s z-S4!UWUYjR*)~fJ^E_-`0<5ubYE|-D$Yf=^-V&ViMp;+595s2(u(zTMhC-o!kS;>* zgg-?Mb3ANL0&JSLcf%l`2@x?H{9f2&PbN|&#%hUmlAUDMS~Ud7tf7o|+8z%z%<`~V zDX=AnZs4e=2L5hK@3rgnc*f|V^LPlM^M0nI%SF<$kb`WJNdTMSVKWk7^DSG_Mj<92 z7B1vXd95dcB)$EhtI6v1J|eEQV4=pi}gBm7z*rWv5W+jp{RTz`c+^`kcZlq=-+YYz6st@IB21Al{*I-@K zOylu7fMs}CMgnY4=dQakeRjaGrE0B7l&Vgc=_f3qWV+PP<>FYU;2sbI+RQeo;ki8Q zxe{Q*WS5FCg;XDJXpOYR9Wm7Fd4_`7I2%H$O}O41bOt58k2ZpNo8Vy+5?~2;lWn$H z$P#Z-u8aw;_6N?WPp3~KJs*&!NKj3OrG@F;txAjAO51bkLNSo)YQY416A$|)39u~~TvjX5jtCQo=)9!WAEC9CnAz9R=N&}O z94&|Zja01JEmc7$oW;YQB>`4O5*TV2=V9X#U}a>CrG_ycHYNd9MjBXZ80BH35@2Oy zjHQMV9yYRd9*<>%D-PSM5hq1Q24*^5=$2?aMA9)zPlW@ab|{25{1|DjHdqTaq)85&tDJNq)X>hu+9klsNFPEC zZ9J?^0<4UCxYW?f!&)W4%D7uf4J|yZgtVPVNDWXWn8SI zhDILNC;?W+B|vIu;9(`C!jzL}gBre(hkfJLcYEaI>*V(T8s(#k`QxUo6LIj513&-& z>VdbmhZ!!=>dL-yd+*YU0%pTXV;FxVn;akKc{jfn4Wgrgov z=l~`gMplt*5)YTmb)T8-xbj%g?ZY%i1z(DvQ#Ua|H+h^!h|o4X0N z+_kP4yZ#c}WRi#%FW3t1YB877HSrSBuoTQ$$Pte;+QEv?Sr3$1iwEkLDsUVsL#2W~ zRUrcYc-L(+oBFx5Eo%%&ig4tb4kD{A$q&<<<6WLmZf^=_iH@gR?|58DAU`)UolK?G zP<-~)m?nMSivt#A?V81Fq`S!q=p+g)gYLOz zuZyhT-?>ie2om6JJl9QwK4wmtnjt*bGWR^uiqV*~1zV;ShnWp$+t5lT6volCIo#dv zC_<)`W#a(^_YGS0FlGbCWR#~GBMB?s_UYYlqw0qTh_$M7h0=yrrW0-DfsRu)s7nS+ z7C2U8S$%MKfT;sgXTo<-0%|fmMqB2hd@&HUOli;Tfj05cn3<3h4 zpy^yRZ)}+2=K9+WN;&T|TffSP<~lc!8$D!*^>h8D*Eo@Jd3@uCn2Z2`jbiCHeu&w( zQq1NYYU_vCM#3MPKg3EIrjr>9+~t%$_VY2cQPunFnmQQgLWnnXO}Y?fI%FJ&dsYu+ zV0H0o*2(JqR)5@Rb=J#`axl4wBwbx5>H1 z_t*GmY_-w?F9rNMKmLZg-nqG_-F`avMpf^xa}z*TXEW#4m&^KI!<$PQ(Lj|>IaZ9( zngO$?2)xv2yS*N?pKI5GHV=~Af9GamnV>DU(gnfyetX4{3zcI@N7|Y4<%veUZS|&+ zC>GCE!1u0Uyk06sD^wP)1=7hnlp<3lx4mv%(YwvrNPa-M97uFGWR0|q(xUfQo4$c_ z%bB>qxew(3``_N~ucl`>kpJ&M{=dCf@DAkv8@-TtAphUU?aTxD|3*6FK>oimKRJ;9 zZ|@R%AphTppB%{lH_{mg^8bx48VBysN)sDGkm1Fj~+5edR#O#}AW3$NY+{|M$U!J*UraEJvId=MQAphfy z(|e}RnntJRrhYZ`rKxvMm8UFI$4vfZ@_Un?n7n8*HVIG8O#Eu%OB4S*!A_VaUZeSg z=5COi{Q^x`v!a<&Kcc=(eU-WZvd5yD&sLJfoG9){^+8yjQ7uC-8R}H(CRZ3pN*l7^>qToSSTthRPSxsjv=RkI z2kzWP`c$&3F7hR`p_HMP>zdILl=IrNOsp6owO(trS8eGj7n7|z*-Wsi!*DI6P6>4k zT2Tfgd%+-;vIGNKU)ACDu(l>iC4o@i6gt2yOGb@SLw?j5r9J zGwCg7E%8V>5U6+K?PkD=X3&xm88{hMTePV4LWywGQ_NTYpwVphV20BU?%2K`Hm>mSEP8zp^5e$vBeXL1r zcjA~Gf+8qnwpRP%G=HiS#_b>kA-W;TXcglfKh_FZr!)G(_Hi`Oisdmhm_QJuk}cI+ zVzlGN?O+5ZTxpA?Dz@7-Rx3mjNpr&x9k$ae z+O5TLJYaXsxE+Z=NU?3KH^g{HkK19ep77b5k(Ai(sBt?O)}w7Y?T?G?UOQ$-5->>w zdiC;nF9UwB8MD*rVVyqiG}Xl^vsAAh?L9(=qYNp#%()_77k(29e{&@erW zq0XM)AWlN0I%wPu2ifVe#>)B`uryXH216-JF*2Un6x6ynhK6D=qRW_#jlQ_=7RK!m zuvjEL-NCwfGCyty!w4F3_igJ|yty$ugaA{ZnGG8P;(BMt?LbCVtY)-R>t@Q#m>rl; zkS=BLw#M5F=D_s09SkyGv@_;`OI+{NNXAzfgg5Kaa4VSfip-SxF*8C(5rSV%m@-8<8fz1oUm3& zkD1{F0-MdL%sMk_%nSpL$dI z6=HL2#ngtJtNswAv`Il91X$A7)(dPjuerh^x>L^Y7Cl|bQ?iC`Z!q|lmlKfm1LT3p^8$5 zs5pr~plo;eVYJ%Grj(Zm z4L9OOk3XKpQaU?Yag&{9xmQTVQJbG;t=^6aA!wr7_QlfX+_F#wQEYd-gx(5XzT=fr zFYPNEwKjz2ySZj3hCO^7BFoEY}3D*BzqRGIkQh42;*ig^0JPRBajo=sX96q9jNO}Iz2Qg@@4-;BJZP8grHb7^r^2zQ$3k^i zVk(GlhPIPV4)D&PuImy}6Ix{$A{A#c*;1mOCSvwJj$(d?)=&tVJ;(D^XgkhQ#GMS; zw2^WtXkmPPJj7r`#owy5jZJ;TRm$YyX3$mY#gw-SCF%|~TSHhfVscxZxpX!}=iLSf zsWRSTIv>{7n|WQ);kFeUkn$%2scOLFwKwejOgE5r=(7pEp-$){?P@OCF<8-*HxnhT zY?<-Kn?|9E0%LO9i6EOzl^xj{iH4o7T$a=&>xp(gjfO&9ol%F@t60yVx>P8U@!JPz zw8eU@Z3|*eH;Y*)>bi!upY6+LKjMR-DcfGK5wdZb*$o}LJ2a` zr}b>z5NOA}P26P9)uDKk)b^8&hN0b$*Igx(R_pMXwK>&$gc5Ke6D<{WIhrYBc@TK) zH{q2yRW_#*T5qmhb)|H+RISy}(J9p}!f-%2(#a;6uC{}gh#Kn|RM9b3hEuFhWE*-S^>&34wz8CEN9gD{<{B_|63pXrWybxP}7iQ)koxgql z>iNRFVgAUuXXoyk`|w}NroKP*!Ku>J>58YOW+oq0`X=v~eA{Gv@}!BsOnhtNT@%R(Xkv%vam_uN z_iHZJMgU;K}F!4 zqtz_6x<*SBrCLxR;xf~$&grmGW++tdm~x7ngc9KKU`t@yl-rxu>ntb~2xp8$EakW0 zd2f>RxIN(%s~z}sSW5AzP@5nG+_+TLWmmr z{${^hHH6qgAkl17HkeiZo*!A%svp!4lnVL$CU>-Hv(Pm-nz3lJY%1(Z6LdJPi=|rK z1QLp+cBo<1^4Ju?!2Mp;lk5M^pXz*N{oi3diK~9I?(e7$A#l~N`I&=`3iSvx=NaLH zQTaUKgQtZ?>#BLT$zcn)AfLJCPqeIcAC)mgY-D6$fylUns=;Zu*2OSYl}UgaRqGL; zo)DmLKVxvG+xcFCi0AaJdZXR3nmut}9>ApIg|^n2E7daAK|80qT7Vks55C?1DL^4j zjLqVaewekJYDCKzDdsRX)OY02YBPeTTn4I9)Asz`m_c#zYKgMfmUK6E`z%HjGpsnF z6y1;cVzmecaGI z0bYCUl(k=LbCi?^Wx1Yt@Za zsG1if=WAHwwrs*|=(!T1N(E|{>-thatrki+3v@P6(k6T*Eem-X4ZK##R*`|b=w&N( z)=B5eP&RAH=u9!C;7W*D?`ws9!BAT3%%_7+1ZG@=R5#i4r=t2M=m(g$=K& z`C&rV&#GStl|%Y$E#He(A-Y!%RvoQ=tYuB*?X9f72U)UFtZb>r;xwIXK#F_$5^&$* z32Pg+xV;2)F=A$O-O>*wGX_r(3hMmDSi6MevM#q4T-R59g6~7PQ4DoKDnQ6Jh(uh` zK-1TD6k>?k*`b^ncNe^$4f0_uR`>d=rwdioje}M{L^9>R-%zTw`)m%lcuvG9ER4sYelDA(d-;UH#n|onf)66eXe(E1 zDt;uCFtz-le21|y!B#`tU|o11m(^N4R!5Gs>X|}-$^v+UYf$T|%0dZ4xL$YJ!{(yh z>ejbXIWt4}ot18csPtX2N)_%J!^v2miVz;fANjRv{lWCV3rOIhkv4Y=P&HJKqGraD zg2)_|GYkfF0dpixT35heNq58X45mCzmVX6ALfP_c#v4oSzM@p`^xf1WjNN+S@G=~XCq1p;n11Wnj zkVfNyBe6+v|vfe~;fkZ;sS9`>BXoBDNQb|``2D3C+D8P3{iq0F|b-@3-+S8y`v|Fzz)Mu*x_-9*0tl1v33xkUWj%3YvN-i z_2?NNN`UV< z!$JQ+XeSoSh~=c7VA2viT%scwJW z3GQFU>~I}QqK;mGS!egBF*}Svbwn)7X2s*7`oowVsz-HD&t!Cp&xBOZj@yChhev1w z+Y`6r_hWX54)k>>)r&;LcE209Lr@%Z7z~W~Y)ep{Fp&5iA@Y5yF7^-ABV%^JF%it{&NJe9s(N_L z4%sY$z3RSEJ1|)`NldT0YvjNWMM)AyqWNxdeAo_|3xA3*`JG^KYHk&fPzE-rTO)yJl;%J7(^f$;`}8e||bXt(^MglyCCS zlh;j}C!U&k=Y(GK3y=fgRP_Vu^VP?xzN@OMj!=F@nN=c9`4xPQ|5 z+&QVnWB>lqcm4MJ=YR3Lzv|xo?3wf)kPZR>J7aJ7VGl<<`GHd*5$|*U@_x1mLn&8!zU}B_r7<=%~#X95|fu zLBnSX*Zih<7ebRJ&!AJ#*6MLhZiJ*U{5| z`MV?NJ@Y)U4EaQX@q5p|;f>e5`wO?;_lA2;VPc`D9;{#Xr+X5?J%fX<{gL@s_!;z` zIUZPsfT95W+|F9yRQ>r^UG;`}m+n))c+P(by=RsO4oa6$ z6o4&vUYEY~8@pchPsFjmc>j)%f9ak1Z>sKm{SiwuKmF|6KK@?S{q&w09$1Emq5%B5 zV-5<<;m5tYp1$Gx6Ni1RaL3Gf%k`4$b9X%T`j4Oeu6I63@0sR-WymNBz*9SK`rt9Q zOvfPN>~rz{Y%ue6>6h$!%Sji)-LZD8;;Jg^KoMFH69^Dg$i`y zMtYBm2bLkJC;)%1{h`^;kUZ_%SGW)VX!X>cpZ)cpPAVuSA3OEB^FE9Bv)9vmlsvEu zQAGjx&d>d3<*LMg+;IH$A2{urVVhm~rsKBEcc+j1<$qoIy-z=|=%V*1cwiZ_iUM%x zoRCcM-RdUz2JEJIjP0RHutmOu9- zcJK!dJ^2@3_{b@Dz3F#<@0Psp_`p}ZU)S7rmFw0HJ-mlJefK*)ncwke$KB6Y zj6hRq@jGhEA3S^FW17c5{rKE}eC}Ju)UW$rht< zHCL%Z#?{f|C7eQ@v$ zGd-;Hz%pbE1;%eYdr9-;-HSKmAACHsaDRLF$jzqT{^Ih_d?@t|_oe<{ea%G=Ydo+F zAwvQ9szbinI6rl}{(ZT-zV-aW{-XIvcm3;6JpGkVe(1t@=F#(BLl3JwunZ|f0r>lG z`o~4zR=n_$kN^Iv>vIn&W}dq0Kj+Rk;--JS>Yn18zWm=4ucwC<9$1E$p#c0<<5fRD z-TJjp>p%9jBaXX9k3Dty+YZy@j}M=5*QbL{1w+-hv#S@Le2q4+OXAXgxdCkB~{?ZfMtjp3czo8mG65u5Z7IG18of#l}8(WeRutC(smFHgvdEy z8M1}~@J%~USqlE+phy33^SQnq-3i;nw;Z8?=U1-K8NbH7zjY1v{%8K_<1`Q?=NQY7Hxz&man63;=UlwBHgUzQ;>UZg zx!~`2e)x(bckH<3N9Q9SicGNF{{OXVuVT_L@q>w+<^hdK{m4>vX=3rZMeV}f3%U9G z=WBD1%(dqhX0Mw)edfU#^YkyKeN&H5T`;vUd4t-kdQ5e`YF>Gsa);u&mtdOUS;lMc*2>=!($s)!v!cU7E;|V`JdWeDpx9ms zx>Y`cQryLh!(Du%cWkfrm!w9I-bLZQ=;Fm{0Hb498Evazkt9Z=UnsuFC^tL?5JJH? z$VfN5wSv+rq0vk*<>AqwrZYiJGP3GQu4#SRn#}MhP~QruZ(H+IueNM&h8<=|azhI0 zk+JWS>{-H0E)R99eJ5iJxV2jQYFjqyJ7Tr(Wb6zj*SELtazh-{!_7V!8%4?WjOLdD z3n0b~6IcLcY#k+6B?wb(rU5C=MkOqOD94AJHpY%l8$r=_&|vAc1IM*eszsE=FAWbAMy*Ei~qT2K$? zk1{sAlIs!q<0+sj&L3rLfhAY9Z+{f0v=;nvH^+x-x;ck|OUhA)Kb zqcwj#3Dm*)ql}%n^g%fL}x3+ zR|-gKw@KFtm(55&$a?%aPdME&w=%)qh84P%&wBb5KjSN`^bMYpfoxN`tPY|ptzfv2 z+ntPgoYsKDWRM~z&NrRY8`cc-oXfp!hjM$LGZI}6{y*t*GM?`$m(UMk4Cd74H}pdw9nAK38&M#bY$^1UeUfU}9odMkQ>c<5rwdPY z8_^YOs;T$wzaM78Z5m;AQ+9nfYe~fsGFizxO^tw!w$;-V$kSK$v<8+S6Nq))zTL(= z=5*Wo*zVowvX1r)8c?~15q%Jm(liF#{!Y9QiQzhvB;|0RUjW0v58v7x=J-cnTe-dO zUlRQg`akK1a-Pjx77CH>_uiM%m&yNLB)~m&YwcvIAS7W~%cX(6v~CBH$fla!VRMwU zH9JroXlEC5D~(jVhsRt@rr8Q~L!NdG0TJbmC}O}da=#ycy7caTyy35Rc4yk2ZwX8gIh&h}-yr3_f{NT| zbkFrNIa0d`ve@u)`kS3su6q|fZ6^>l;j^xAWfSR4mT8wbNq#Oh!izb3&fB$m5V88M zigTZ?{%Ne;Jlx*THhi*z3in#1m`b%$0E&PKrc_`!@}tr{B|i3m|;k=FYP|qm8k(Vm+i1$`=FfY;6BC2v{gx23sL~Z9^|vv7#%HU<#zI zGLcBhT;3h^x{^gK(5q&wmO+tfh6XFtN+8eFiGI0QH8xOB63bh#PQTx&X2PU_34++V zpxq=bg8??ww_4CZ_&nQm?sBT~)777Tia+2R72gIF2Zn~ZP@q5Dzpb2F3+*sSd&g0(zFL(fN9 zKv`$jL*;tjfuZZB9JsewYl5_6`J^r1NudG4XU=s!2EGo2ufrb1Q?_u!HR1I8)6G=qRK!nVK0x6-1Am3W5K@$`iNhAvLR3o z4T7|n40@bM*zPFkeVLA~8bA@oYc=|N{bJZ%BI_n58W`>634BMS`PbFxk2OIWs!$l=i4Y)zWpA@> z+YKgncVsRvZ`I4haR=8Z(0Js;{EcrDcp)@5_4h{sK>O1k43m{7c{ z2hk@e)IfcWW;CD!N@@lJSqS#K1FvCp#W;4zOrG@?<{0HBZ*t`S){C6*awX?Q4)c{a zdx?w}nMfd)wwJoXi<-3rJXe~BOH71m&Z4M08M#%z+ z(ea`w_u_lpjx8oKIjWSxeL4LKn#h=M+m?$~AQd$ZT*YuZT`u)#YpoV5hBunXF$g#+iRTFowV1S3~lIU%yDxXbr$^eMswNFv;c`oB2l7j zk(?py@SD=@PC4Oj7s19a6T%b04oSyq38;dOKKhR4zW+Z$QB*9Qv-riu!xvif-=9BW z?!wu7XHS{AZ2GZjam%~g}0oDoQHnuE?tFzOcT*{YP~x8Hi%zjMDYf7Qm192gR> zhZi5cN3o=sS>-sxaa@)We7Z_exP`PeMULBBL;UaCB{SSPyhzlGlgl!~SjT&@>&SbO z*}x!{bXJr8bXTN<4?&Y0yNf9P_f4B|6t8y|pEA6V<9O!Yk)BwV5fQt}as7Ga*4KF_ zIHrR;x!>m-H{&Z_IWArvUcm8P*~?e69F`tvyUO>)Z%&$9^VDz*d!OQd_lJ^so-jP0 z5x4X)7^o)3GrfQC3`y1Tv&J&WE?i`*cnx!f^5fL(HDevpek8m7Y2FV-xt`H_s9>8lhs@S{{H9I%yZj%LVYG|-gK zF4hO){V072$2VKujIa3U75Gtaogc}#*}lqm#E-V*$@@_<&GAefC7I_5!|pmil5sPB zmFJirZON4Pqr^`+rsv)xnd#19XPqC(xLCi+RJazn^{sZ^kIw!j$MKw=WR6D<+w1&D z`d#}~imQIKWjAu%&Uz33`x(g$cMe;kUYwG1Q-8b{LqI*vyV8yv^u z_C9|m<=p*W<;Z<)DV5=;IflW$hW5 z%flMSckInWM(n{V-;ot_OP>6S=^{Cvu3t;$dBU*D@f>^ekP*AE%5!YR+>$B3V%o3d zm^!{Hnd#19g=0Fl%a9TJu*!6G#oUr3zhYV$j-&0>k~tndEQ?o68PO1{6jxWwEh%!` z%=7&3nq-DMhb2)jPRWU-81F?_FKM%GtcNbX6>;$FG=Y6giG({AM$b;x81y ziph?11S_VD=!#X2t1IS~9QhTKJc;9Y`tg!E9z85_9LH8n8POT59QhSfBEu6nhU5>V zGUWFEYSp(C^OsNjEBLi?|6UJVk{_11D|448l-nCtCb}}m-F zMxl}e7nXpe?D{Kn+|^G0;?j%Pw$9q%wx_U$i!Z2cxukOiwhna=zrJy$MnfI==!y=w zXjZ6l_p^3kya8xktJ>iHd$T2E9WHkpZ0*7pcW3&gyRfClF@gj&Y#2Vf|@L!k0EwxjyMs+qED@wi+o%81b@3vmK=8BOGCt#*N8T4iB$@&w4I5Q3{pq z#ev0z^uzf`x=mVr`an8q35OzaG=e}jtekh7TRD9$XfjBj z&J%g-_!-e$-z*>rTR+!#$+@S>O)}X5NpgZ4IDEOcM@62xOHQa4-yj+99UDbo2(OzQ z(#sI?Bvjta7i!!LU%iJ3sH6+cT8Zhe-ZI_A-3`?u5UdARMv(+e!a4}j!Vt=_UJXEP zH%b7a(k_)omjv!U_ZCaSW=oCust#Az{svZ|Ivsb_b;G42S}H|Lz7QHLd+lzY53W`- zc--8}wy1EyP^8LLi>{Yi(N(OwtNXol=yl$_0oIo-AT?dRIiUKUE|QKID_9z&xyZn+ zAjXtiaKDo`*fX_aG#;oFTV~vD@(sN1K-C)e4+fcbJQ#8sI=v1RD$wV zOgEfcTR;8Q@d8tFU0e5VoY%Ei+>RGbxy~4gi`XV6yqAglT+Ct371swRZW9ZkueaVFt$yLY@_hUd<{X3^T~KUEMRha>?`q>5l_c!Bthax z-^G$?%kEUx)qwUt$gjX=3T}PgQ|#vBZnH(#4EK`uzRgu4qwbX3W(dNjW;pGv?8d1& z)8Ac2F<;kVYUTs6mfhR8b`8-aNb%&YKp^d;vlYU^$$`xz?U?}Lj4|&1|6EAnP~5Ci z{9>-SaLWAi^LNd^XFjuZ@6xq%ADH`(xqr;VbH~m;GyCP)D`#V~CoUCd{xtKAnX6|K zGur9rr|+76&va%Q1^EEJr;1EnH^olrr=}(!nEc3Oee%r7g^8a|+%VCdu&9n#{$b)! z&7+#nXfDvWG)JnRP=7($QJ$^1S-nRcP#>c-EACvlO7&aSmsFQ4zoxuOdB{R%;q1lh z7i)`eSe#$@$-*b+|2lu?{JZBmRn0k_Syu*ECLEq#scdQHd(h!RrYiz7H*B;oIbcd=G_MEWkZV1? zMjq)jbxbSor_6d?8b%SeQpH}C->mX4ZbtVrn|*LIPghRmIoTz$LYK4}*}Mnx1r2D{ z(~KAkWfw?uSB&J0O;?g_)=dpZ+H2DmihlOcH_0w>mQW&-4%eEwDE|Mk_wMnot9705 zy6v^seIvKSWdt@Xhk8v_2VmuIOBK$yp7`hGJ_zXBQV|uQE@~?MC1i!=4re3*=u*Nbyl*^IvoAHtbg|Y zto8lQaY}}}2+8svFn-#L!?>5NQ&z#gexWwOumRRI9Ou$6O6lkV5NtMdg zM7C(8sSc!1L{V?${cayifvvHcL2H_F@RUUvAtat0TH;9y38`l;WMKQF5hzk?Qp!*q{oV8=)l@>D%JKwWtn@DxB|@;2c%8Od=n{=)TW(LG?lR zZwy4+_C~p0>j}@Cmk~zd8KEVfzL4-!IYz*0m_WwpMztHW4A;erSwU2X)wEow7gUGqzG8JP3=iJ-Gt1RwoCZ zJ4#HP)xQia@xMY#{KZ0|-@*x~k|i6ytQCtw(bk&ds6m+ZUE7Kb6Acd`7$jHlMNP5apv|XLQ5<%cE+{fWh@eN#v4q}EY~MldIU!- zB9o4AwN%YZ(DgPik7RS&G+5fHLCuMIaI|0*LgGcCB_g3EzWPKW5>KRxFdb0|dz3L0 zJ)ia4Wt6H>Stt^T@!B|1O*Wha#Bc6|me>w0v9*v8Mht~iY!HaT&AE{}pW@P)EU^1^ zTbV>YnNOB#X*7dH;gNJ@ksmuALSm8MH;lw0-F&>%udwwlY-9;XBwz^wIg}wN-zhW0 z0=U1mM?$vfYQzwqV!QuiQI8N39}O+>k^Q%?+;_$s$~?1U9GF`5Y_o+SWwe=v<1*N7 zRV>dgHmIyx%8?_tWYbejAM51ToRnMxX^r_((Xxf6uS(J+X2)wes)$eWdEnnU(6ZG= zVi@)7(B2nMBs4LrRT6mLqbHexq2(nb7c=W<#n4n1t^v=DNkK+pITKp{&CnA6HMGQk z+kg8)hHJSP#fa5Nkw|1jVp#KeyQ~VC2JMa*w);w)v;cO&6zJoE7!jF z+O>lpK6uvtJNKEr-`|_=J$3h+cE4)pyLMi*{cGFu)~C0$t6#YK?N^_-`KHb2mA78` znvM5u=mBujb;Vn&E2YIz&$N2?&yc z6bn8(%_wMuAiY?BNK7PJx~{eG(KO#{kiI<}5l&iMPGfqiPGBK)9$c zmg|N(iMx@sOfqGx-*4AN*sPjD)bVC7i=(NPV&$|NxTwVwUGQnNFyjKM9l|pzFqem? zQRbw=A$*%Ic4i4OjY7-rO!jdeaG4F35I#(Sxtujva-h6<)Z^ejNP)|{Jy=@DG$?1* ze?#~X1ukc)|57K-`S_4sd_M-pV#~Dz@tjmOO*SXj~_Ybnu0PA2=xNe|G=>*{AmYcJD2F`0ginzjODcJAc0Otvjjhk8Qtx8`=7Ut#8;m zy87X(4_Gg1yF1DT1+O|i{ao^3DR^aZMu_+ zU7W<3iD|JhUT54q64Mz}M8%s=oU8rT0BUmz+~lpC3ZkD3D;-Zkd^KOpRr}L8*&0ub zVj-UtEis*A=$3f%E9PqdeE_v}L`q0VHkqglrb#_r8)t^8s;JR&mQh$>nBHa@DYVHA zT0Ka-`GmRJZwaE-&P1x67Gb5)NkXn<+0vBGRi`eCOFa&+`cSNa7p5a4*#>N{&DDN$ z0JSzX>G8R!JZLmT&htG*w!}oLIX1XTp#tX#4S0aTrr0TpfbGFt?HdB94RL8G-rS$7 zeSHA6A@2Xgn|pJ$|L)=w>|ux-Kk?@7T z+TizT;?1qO+HVM;HpJDKc=PI9?Lz_7hPeI`Z*I=j{@Va*L)>zSH?Pdq-VC5N#HE&a zb7QXdIDpy^*H_|A5CRCSyblIY8{(=;yty`4`+o&c8{*bUytz78``U|-?_px-h&NZ} zY99!oHpG>Yc>LPA+Sde78zMT5c>KUz?W+T*4RH}99=~R;_EiDYhPd<*k6%4kI}M;V z#QlwU{HnRy$;GGgFjqF>@pP_s96)V|n;G$VGFR&dP#fYRMm!$R)s6zF4RPrr9{Y2( z!vJbS+@FZYqq*9_#mC7o!92v{;asg3Ky8T25Ak>qSZ#>=4)NHVt91jg4W-tdtL+C+ z8{+OnJnqlc_5!F4aYZ5?_vUJy0BS?rh=|9|Tx~ai+7P!N;&FGb)()UH#6^a9Y|qtN z7kj*fxw@F0|DU~5U%B?$gD)PuX8#}eU$yu7y~*z9cKw~t?hLm-v+aSm|NhlaU3E79 zdegr0$t&i@|JpFtKe66g`?ED|_2aAR%Ey5C`9BXll;6-aKFq9oxwotjJd_JfAjG74 z@uc^_L-d?Lhy~zs@4O#)hzd<0#6%tB3uHqRxa20t%n5{0df81th9+>yO+d^Egjm-v zdl=$#0wEqZF89v)frrwe34~arE}rclcnAwkAjAR@guu(f6Cm?kOdMqWx*zYUq(aA) zc#$3EI%%G)Q&58VBC?0tOiS-X{;H*UXo``KH)tMAzS_~x^&^f!KZ{ZH3f;G6%; zEB^$bv!4fXeOtT#e*P(2Ukhy9&?HlpD+8uH*0_dN&hV*buifivR5_U(p?;&T>+xp3 zonp*$j7;HW(}$qA>)J#RyN{mVy$t`ZBlP1sQ~E z&0$k}@c?8rKCfEKjdQW_2gttqAYQ-yZ5u_iYfOuosOZ%&Qlyd?O-$g7Ym#`A?TJmk zk{)5Bj2DIdpd^CgE++fPqxI`=*CK?W%z5nYxrbb6bo2* z4u*oF(bPG(yHcH8Y$h@9IIq0@$wQdsoI?T*NcJw=#bh7hlfw_<_usBX2$Pa?YH`=6 z42pX+wSZC&;;*<}ix8$J=dioWTP!H<;wSn?Xbb4UuYUQw7Wd62$Pkt-LD`*yyQ|GE zu7#!5N7{ogtj_7qM{5X+nR93a{pj~VcRpH`+dZTi+z#halD(@^7k}GF>>+sNb}d5K zkepMCyFO)5+@q<*Y_yhd*CK=u(|PQcosxp$E;ge$zbzp2L0rCFix5^E=dnBO3jYWj zWM~=psA`dX5SMP(BE$p8d9_$}4+)C9_@V3(+9L5FDb8#0^x2Us#4E~q?3R5#&ciL6 z;a~jDu!u8y_Mr=kKZpyrTO-8XM^Ju2aTnL)5n2OzkSyJ9jSv?o=doMf*Pn-5-n3qP z!hgip08S(;D;ulNSlN5s*3&n>AN=RbKPO+{bsNXMSwPY^+_!qU{zFTFw$BSHpE@fb zX^7j>SrpeW_l0wOi19d&V|`cP><~BVX9dE(+o@_s>vV8Z;iB|QGl7eP&@RtvGpW;I zi_ahe;{JN=x~nzkac*x#D2)Tb3!+=kbl+>#>SQW5E8)xtrSyo<#z(f=r!guAmoi-z zwuxMui)yOKPmES&k`PDixMc9qJx8s^4dO;r5+yd~a9I_bprwM3Xr!0LQwfJ_!_q*c z3NqyiOeGmD;~=m`56`q@x>=r7V`QQpb78K@s5WE^M=i^@{T>nx7@>4l^+jM{=T>0g z5WVZ-={?{$ME^yO!!Q%Y{h)&_1p%KeeKmJzf!qk-kB`Indzipdxb0iRZZ>N^mcuRv zof{aKr?N#~O~xIyDIwz0j!0tzYlL^{K?KiIJvFl6_g} zSWahHikAlj2+%!?&^_$VGGBmcY?k7p-qLD=Spp0=4&wPEP&}0cRR_cF8#~9{*|7Wf zD#)pFnGb&fhh6f_VHe!-bJlOeJQ$w6oP}Kvmxj@4U)slMAPqf`;bq>`=WQwPS_(mD zfS`F#naS3gnqD7T1E6D;KjGngHm|ysFyJIAeSG%U6W`K1>OEI?`)_Bitf0W#h^06y zpy1QGPhytOemUc~;Il+>k-6YD@wq&>A-Sc>G*t7}|G|P@S7)`GgHF<4fV$3<-JIZ!OT_Bx2!uQwbxm@lSZy|=dTkvM)q9_9 zSfU&|0)YrQ2=5W{P)Hzp49bv3q1=vAd{5}+V75I1k0f{L>fvWd$oL~#Xq+*VU zQq#iKNnTF0I3|-DH4`c!62Zcd&#FFKx?wYQE z;Jxmy6HwyZ-L;y4@}*e&Mk0G;snhhJlqgB?4N7;=M7I(x9##7gS)vDag1PscFu5^r zid-AFyPXa$wT(%tGDM*pCY1H18+oF{GZs##V^N+E{Ue`u5b?+s<(N*ZND<+fqN3XI z7V2A`iu72{o%$)cn#KYx^RueY;{R_wv?8zk(aP@Ib|2W4cVD!-x$}oR?_STZKX2{L zYtCAB?b)mUu=@VhA6$LyYI*f&_3FwW?R?9QzP_{e;kEZ{ls2BTadrK7ch=T_ZvFM^ z-j=X+{p#Ob{kf~Jzp7k){^s9r{_^IxZPqp;SH5uNH?O?;igqP-Wp(5I8~^t#yTG;1 zHT2r%hI#Nir{nyCxc|id6L*lkKi>P1y;tw$_MW!;XS?s){_X9b+Wx-n?zX)B((S#i z&usn0)=vW3D=Sz3>qWN}&tK=)4i{USSQ0^z1j6@<6?y4h`e6GmTud8c{pvDq>rPw} zNko;QL}r$8SMS6nkOZvpZMVOS+q@G8QhY&Pjy05J+?6|V2og`FX-QcQi+HecCk{s9 zC|4c1+OoctJ8-cS5>J4z(?ekixBtaEaVUsdoi7rUzKr{)J8?-Qj?|lMXE_+d{y*M{ zOCa$Wa2eN_Wqn_`3m2tZ6T@D{eSRJ<6+V3-I}fC~_s8={m~gz@R%*FKyKLpFM7QcI zXt7dD6H4vRqwfzOALv_WUgIqFLU{aJ8=+#q$-7a zak-85|Ng#p81%_g;)`+`3(Bbxqv3iyQWD~FUQLM|AD;-(hNu;i(YWEVtXgfSVs2xs zY`pBg_1Nkm$WM1WxZhmWB91$ZW@}BtHy}p<0o92*sX}N=Y+=)>Y#?+4kK}obZ2D4F zLpDynPFcTLZFda1sUW~xLr(u3xxsLzEBfE#YGI`$1C%xF-iS<)?b*mh24 zd?Qw->Uf*%)NF`SlxD+avq#eNYZ72H5l|_C4 zxR}ul-azw|msEVGREvAnI3F3}P_*s2?Rrz_jHZ2=n>LZXmzkqIv63@K`~j(FlACh z;Y2hsVA2DA{f`zBd4bILiq%YPVksgb#jDVyrR(yzUBmKqifS7ygrXG?#KT)3E+n#w zi$`-KHQv)ewmpR#=tf6QlX7QN?RG)E=tiN7$3&HLc7A9fK~z&^hb>Y`3+l^WUm(S9 z2QCz1spPc5@kBP?NM&nDc9JAFer+Lv4>+Mcs%Pt^fg5qne4KCT5weh>TWBU#H+WG8 z9~R3`d;absqX3La{eGU3hz^xuQUkR`)Dv8;>z5PVNnYtXu%DJ1%0w(D3L8&bl!287 z(^8{TZL{NcnXuw|WvUE{Fw}F0ilE19B5e=bXv%~6bsVhX&P6NvCH{RrZAR~!>HmIC zJc@LNZ5JAPg*07GLQ;h5Yeu(@{0>ir+1|Dx$d)z-=BWd zng9MD79~W>9X6w62Vm7T`Od__#&SK;EJso$(@7y2rkgaAHZBQ}wEKaDrqi!Jb4{OH z{Qj<0WHC8KAu~x3#a^jyG4ce>C3GU6ra8T6S7?TeXOa#_qLohDnjhe2==A0xxOF80!DoSznYR2!nCgT;4({bDvfR4} zIK_cvaAFNtM~N~4OeoER58tsg#t|fuw2OKDuI{qPQh=?angpZ|}gt zpu#ah=(Ee)y8S=B6Bk1exSZomZdu=-+=+`K2;}O;DZh;S*rHdQjsrdKEsHcQQ7EPN z+GE9muDZTRn*&cYJdX`wp6MrrQE3F$?1Ci@vAWpM(rBE(8*o}0 zRkUatxpK0cOvr*1akEv*>JRe@LG;Oq(9p0#2`VMIjxw~OEj`afnuN6bR|{6-F;N3s z9;z2pI!(ouMYY%GM?8?;krlkJ-BTum>^7%_Y$e%=j&Y$8EB4wEx7>kvly36-Z#a=Kr-LpfCdzoRgN)#;RJI+YVYv~t zK}BG%T$JO2%MW6bv${yS0DfoZke1xBF_slP#%+KZCYwvNjI65oIwQ$h({)P(jhLI% zq71h?k=zECtx$DK>kH!!**DE{%*IWuQPGf0v*Z|iOhMz4H`QxXPE7Y29W`DvYH18=y7^ey zb4yY@refsIcP%7L(9?}f+fg$qlfmEYi`m~$El?W@vanYL+HDLVgY0++;hzc}ZgmQwpadS~dr@$s-N}-RzmL7NIUNjk_ zs#U31?KMYe1JA^JQYsd4`UU!GX=nZCRutgEzxwg3PX`|Uzr6WfoAzdU^FH9||BF}N zbnSPqz2n+zuZh=Q0HOnY=-}-KuQ@0kJRd{}_`v?#_FuKn?SB=B7V!SPAK07h6|b0A zUUuavAUeQ%HojxS*m&v2lh!}6`qQi50-^w-tNSY-TmK3WG2my{zI9Dod-3`k*IVl^ zS-ZCPJP>K%w|2jO*WYD8p25%V{I{L&+ZpcUcAgC)5B%o#f7tf6sqL=>5eR;L>wCBQ zTbZqAf=C3vdi5<=ovXygOZPWUHU%%-;Mdl!=V?mG7p7*3aC5#f@%`z1eq8)9*TwP` z*0m;vmyvp{9Oo}ho_k-pR}2dMS9^t^&}Z&3L7~ss%Lj!%eJ>Xj`m{ZI0i`W4IyzH# zG{Q1NU&LC=%izvOcApYd>+|<2L7~sv6N5sZyC(#NUf(MPg+6DG4+?$u9v2k)tUWd; z^k46lgF?S*PYwz_+>=1}zY%a>e4^Em>OP*~yG~Q>c^<3hmqx+C#{NG9g|6;@HaPU} zgF;vKKNAqT|G)SDE-3Vi`=1U9{iprE4GR6o{Z9pjeqsM_f*l-I^4~ zq?^rFtIHeV^E6cqZt^$!Mx{?_^j zf{#!f3J^o2WmQ0NPG zv;{Ob^u~RS#0N&R&ROMtySKC&uDxaLdxJv1d+mFILceS6EkU7gUi|GW90PoN4@a7=JhU#(ieDr+PzznE=()7Gsu_Cn}4ZrxgAFND5s z>(&~3A@m!zPS)7mh0uq#elcjZ{_WN;oIp9z&xit1Pgi_NY#D>)OEU1JduyY2A@nD= zZ*BB0guZ+G)<*9_=)1OWZS*dLzH|H5M(;xCPi)`X=v@f?@$Fk1y$hi~w*6N@zw3`~ ze=;caKX3o#&e~g6iXce;&U<#gdFSO@Z{Hei5&Li7A06!Ne{}XnzxBkce|+^vuD<$e zZg1nDckt3{o4e23`P-Fut-fS+=jzio|7`P}o5!0Rh!XJ0D?fAP8#jJo<6AafvGKxf zYHefngR9@S{<-yE+rMw`i+g{%_ujpq*#6aBX)kd_zH+$tmhI`*KW%^e?gw|jZ(rDl zR{nhJqkHX{UBJetXBGkL-?83UhgQ2Fk3eGkX!ifLFRs0J?JYYm*!~A_;F(`@B-IGn zWYK}$pzUZogX&eMmMo0bD4D`JvBpzGi=!2!4>{7)e&M<6*|kG7R&#~}%h22!eriUV zUy(j-@#L1&agddjRb*wH#7qoM@CX|bqj4^Z^`@8yC%M8TMOp)cNA?zXa$zK1KPQeR z6GJtH#;vJeu2c$!WpY~Dit^)lnT*xa8k{6DUP~E+1m}B;+ppv}huHDXxL;JTNINU3 z1+u7&2!?Gp2jG@o1G!#%CM~ED*4)W2K28XUTxbb8bUoe}TH@QbpStpJA#72P zc-qWi*t8E`%IY4P(-O%U1H|AB=0YQoIh=Tv8IaL2Xj4iEKSGs^Q(( zsF-x>eWY8m3YK|rBecW|Lrc71E`i1)cH3^N$yN!8wCd5iSW`+Qnjg0-oD_G=rs0%Q zaUu__C$22s)KJ*T)hCQH$j4c}NO^JENERDep2&B>m2DKEF#WvA2*g32cyvjCHG#H?jt$EDLmU3dJh&>gzzjHz+rz*%3sQzXUU5^r9V!Sv`+1Cz_S ztkmL1hB1*-kSdH=x>gpNMl6S@_*hgKnM60fC$z*{LQ8!2#Y_Ibnwu)2cRj8@?I~cF z@$@qnm4(Dz#!XL)F8KLM)CVuPg43$zD{;sb6r+o^Io3s? z9C$`crEu_6Ds&Kd(x6KXFHS_wUI}uYgOd#v6I^1kp-R$R1@h%Fm4+3mZ=eh?-l~`L zB{0r_CDJIJst+ZkAPi)OKNu}oh1fg3C$z-RFIq4}dwdLh_OmHk`Ct8)OZ( zNLMt%!H8JeX~EUB*J!kiaiY%EC&Sj0f9<^S@ubBc?lOJ3Z((_e-u;I^KTqP%7JsJ4Q&%Z{JGe(h-!xp%jXSN#gUR)L{kV}`ELp4V;2vCU2rY3^h6t75FH%rD){HiKmVe>z_)<>ATzhA?@6wP2?=spZd1wVnQl|Ty+!9v znjwO!&ha6fz~}g|KAdw$zte79A!23E3k(t7{Hz!V|BT$ePP`!lJATDy^?yXiuOt9m z;7NI&pXzCY-C3l&Sy}hy_?3X5D4GD`XL?*F%bh4Cwx~|DWQio4F1M62uVtwiHU`-^ zQ!+ZrkZ`Gu)vMk;dt5@JaoVSSMrb;@X)!uLq;{20kjV+g>E4Z!X)?gPT!Hdrhq#fq zGl*fupklL}?iPzjBceBoGgY!uCJV)KZm9R8os?l@34c!Kwlm25Bh8$=LAuCz9QcXU zYtop`0$11PN*;mBiFgaEcVawpG_6C^Xtb45@;54!mCKbht0~4!R3P0!`iSpQ44+41 zZ75F{M^39m_A)v#t=c0!+rKf*F$pI=_E8*^KXaOWME~-KKmDR>)#jlE9(Do*zTuOe zHY0dP!rWO)-?MMv>8Ku$jZ@=2L>mF$>8`py9P(fubK#Eq&OX5DdOQrBeX8EDVHqdR zzgoAcxwBuMH;7K#s?Q$!XIs;l%Cxxnf&;&a4%mGylb`16<^N4|QHzuRGY`rk9 z*mABA&sDNQ9Fr*$YgKc=`*+syL4=R`VCaJQEwcsS^qK(1{GDq8+r*sNl#xvq2(zRo zI=qmfqEkOn#VN9^HN1*Ni{6-T6h|6a!40ihY~>ll$Rr@Lk)lQ&a_D7?(2ZmxqYk{j z>suq0?O1fTPdBh2Yr>-Hvtj2P_vgdTUOf!4>AJLG2mSwX*xj1!ABRDA{u1(~8*=Av z1K~{dMlY6au>GV;6cZjii1X;gIckc~Fbkz6zt3=ee{AGfE1bvIpJh zs(Rvvasw{$@j+ir`QkXmSO4Pb^y(8f|HI}BuDs>S?#8Rv|7!hpYoA+t!`fG_{=_N<6#O&zagW{2HU0%J z&{lgFhgv?_VAd_8?rGkg6XaotRPkWS0)>9vzP9QFF7)mYIENv!$hVGrpu{6kVmEM! z4{sQ~!w`w(tz!q2lmaE$flFEn-g8(Fo^akc?tQ&X+Qkg*7`6+sB!h!ny~>@?*wL}x>|jp9hN}My(+B}bi2$J4XS0g zHG3G(Ib)2WxO9udn?t{ouPfMq2bDHYDs5aG-uR9q*g2KHEB5nYh{)uCPUnP9`>~=E zBE&eLW1P@wJyvu=gb)XG+9!1M$Ba(!AmQ$D3$U3@u3BI=XHTx}LAH?Vty2_l z3FC39RHfSGN~GFkV^$rat>Y#rX*Rhw0++Nrxq97l3-8u(mT6d$1bVxIVlM>oJ z3~QK74^Dc79Iy6i<5&g7%_i4+;NtF>T*pbK&mbv%S~o4H=OrvB2iGSxQfM@-Nvl;w zMg}uYOl89KK-X=6!e*1}*9R^vd9ui>yW zndT~NGS!TZSu;1Ri5^W3kQ6iMDxlI|e^TjJUL3DH$m9y0j>j@zQR;ZfELyIk^tB3| z7Dm3=mw~JLz=S&k3uIg35?mry_hL@j0d#67bnbtw=!A%C?;O7pu$hhIR|IBr#z-E( zLpN&4bh*x#lSMScmE}lRAy_xnrV}lqVhfzBDxPmSO6&N3P~vPPzdUe>r$(|gO$vsg zrUYJ#H8GFNbXcnqOK2)ODN&lA?B=K4tnM~!%QKE&0ZN(;t!m(s?igB}X-FIh$P>+u zhn3if7NUrsuasdOsh7KHUQouPu@2WNlhLe~z5JwEmB58P^3XbM%`)$0Ap-rY$JH4d zQ1wb+HYY<%&vK}S<3l4~VGKfJbbnN?xGKY#8K`JPO0{C#RGhJ!%le?w%1Ncm7YES~ zG_+2KjG9*(%U5B_Dd$AFJSF6+;ak9$9+ed_B}%+76ok0TAt>R1JW0-R1+ba*WjQdL zGy3wxs!$3k%DIUZRchlz+SW9Kj!g?=mMjmuSt37l-RdN*h-&M&3`(5!Whropr}}b; z%;Uzf3`&~yWifC`cl6~DDYikgN+-=K1TO56`|@4Q8Y10;dMwV^fU1`Qv$@rm)1zdh z9E%N07&#vD-gF}2iImt@p}J<*W2I&l_LxpxAuSSAS~#gRfAPgtpuW8PFc#)1cn6%@ zrPZHaxzleS#PfUO%-{Yz|NmQl`lo&OPy6Q2y!+SZ{{FL=0BbK^ySDoA)t^4gtN*O1 z16Rzm;tR}T1R&^?8@QFQ&w*VL;j0GEqpv^Wh|MK48^uaZKelIahGG@98?dg zL7~cl5)@iFIPw3#m_YfUe4_Qj00bZ1`Djq+M|M7P0u`IK!1p?ZZk=-|y5W1LTsvkn z`^Kj?PF#ZM3!#6zapDqmG4xX#CoVx3L;q&u#3krr=wEM~xCGHznaV4wFdR>1&dN~M zvj5cT6IP#a@&zu0UR%8u6ne0F5EQz-WC-4 z){VCYh5o?C51c?RoL&BE78Lrj^_QJMFI;1vy!zyz&?l`vX?Fg9=Gs$NuD$x;a|eU{PwiWKpV({e{_*ZB zc0Rl#g2?-Yt>4_rT>Yi1FWvl^&BT=-zw)AucWnIY_3vMQ76^{=)YUhx9<2Pk%Q`o& zzTvVoeOb?=`vMQHZE707wRTwO_jLh+tA;I3v52ZfO?5QP7Kdb^E%IHggQU%hN6Xcg zS2!FdGiJ$UQqyupE0}R6X*!awNP0Uy0TGC4M(TA2xE=4hXzTiDFfiSLFrmm?MJ`w) zW;qz)R_8Vcr%lGw8ty4rBH3VCY@^p|(k;##3@358)sOZa+#dC-@f39EClh3qixhhT z4B~huGi3yC*6auqZKp6_j*`=OicRP6w#(Ajt0P8kbg7O8o*IqJEHLoW46*a9QylUI z7_!FMSeNMVHA%OLB!|{$my0Bk5|h_@Nlk`pT@cLq5GAy-XI9~IY|xzMV;$X&Rjfo2 z9ohZ>@8gg_T$c{^~ zv`{5F-FAIs9wy|jT8KG90J32gJlH^F7pbAKDV(DyMy*{dDK~(w* z@8ESus&q}fzRcm~DGo-Y-0&w>aSuGTcW;sKRGsR-Lo|~rWc0r1^5?v8FY}#JDuM9!{fWFLfsiaa* z>fjNNwslWVr=ne-={kdCX=J$M^|b4GBvWYAJz*+ldUR#^(qr}T6bGX!$rV#D2F;pR zRb%BWOGgGm+=PwdI8sSL6}qZLc}B(#rHD#S%0i`+aN#&e<5cV<lCErAd!2>J#ip&ZZTbwy{e_B#6IvLC{HY|=OYch zI~Lt;y6sofOkt8~6}$i(;oH6; z^7)jKp)%zjYqhzE!ujzrrS;)RwUX!+3)gcOhJa*mb1-@(q*`fd2yc;Y8jq$6?S9>& z=t?6-`zkB;TU8LA-H)Q)VN_1~TzWE+d!4);qmcR#>6@c`7HY&Hr#5O^sTy0ion}8% zxK34cH>-|vcmt$6nt<-K92b4-=?|{X*NfG|0!SIg$C|~Yj7^eRznOJYeY1oxqN+Ik zbhV$2)e)rHRkKC)FfNVEqAsNKn3KgSO1c>3VhJ+`HHsE8$eGo_u+5?+JmGRM z&31twyJ3|1-o!7mOJg%~`!lqCuE{qV(@whYf|ucuM$@>I74=BD+NPm$TD3(MdzD&jh;{`Hv)r~?%tdTfxz6D&$*!_~*&YzyFw?Ir zXSdn-j@zH1?dBQU(9436_ec>{oJlG>mVC-H@HDUGn;p8Fl6|EssfSI68g_cUYM!ge z3@}R>_NVn$E6-{DBz zu`N6asTrczYyjV3epI;bOuN-O-eZfyB2j1RuDQHot^L()4thfO`)Q{)wId+O5L9aM z)3#yc=yZnEs$`3@BL%G9fRbhBP>$B{dZul1GMWX6Wp$`B&W$4VLOP$3qitD-W!f(w z1QT_k>ldEJ*MH+Shq|MuqCMWqd$P@DL3Wl-rAc2o1VX zp@|kvwCG1kj4ATK-lZO|c4Cz7!)|#Z;(9qV9;Z?N`mj%vh03(46}3t;XAk}5gh1Ob zxXq!av6Gykz}oav5JiZ_tsZoeuy4ptf9>UCSRaHh)RwoZUY$J}_l9Kdihdz(W!XHQz?$y8A5 zD$i14N3saHSYZj-ne-vssd@yG76ys7cUV!U*{n+!J9@(B2Ou!M4>5j$K>d;5Jg+=+>KEDZuRTgq$3?l z1JBOXz_uY#OiNZPVvtyULNe`Kf(GjxK5iGm4vK+dEkwFbbR`N{3TRnvT(moa+*{5p zvH64d#DPlXAXg9vXnd+ir7^OyKLm z_60Fjj-~8zn@JNTD;Fzt&9v5S*LafZ$5X0I`RE~=NQ_$(cnXYt^wxx}xv_~o6=5oy zrN@|^@`Xg4aY|iRgusfW53-EKwB^>6_1Vl&zpFo7xy?bWr>%(DkBoYGsPBo0J?!*V zwd}-866+4j6Qtg2)kPNWn};Cw{G_SsDj}0XJ{zB+`p6R{3brduE+0$iHPhGI!X#Ox zkn1uo;6=^u8)mL*lm=t>&iA-zYVBulb12EKRR%k*NglYXcx@0SXB3a%adr^vW|;y$ zHB*q&AFxCt`CKDU(}}cj9Tbgj_;AnAt0}u z8k-#L;4H{6kvE4OnPcf8EKEs9ZdKZ4wMCLVk}KyRLd~k=VM87!Ay)U&ZgpUy(WpXO z3J$U^*!FZ7i#xEBZQ&)FsJcDrIxh6dVtzI$c=$=ly^ zB?A8Q<)426U*H?OWB-iQwWvC9mI67^N3&r!%ZVPMGhN!S zi=XA6_$41kVaMUpFgk55_&5!ui*usiH59*`6CF(aU%H&=2?!hq&h>_FoAwcuIT}ZE zCXv_m3PeO1Hg35Ea44v{6xwDxyha%cTEF+%rqKj<*|0a1%g}H{fRwKjL?PnQbW_vX zPB&h5fZwG8%)%+K&js_y(9GC5y3P4kaahkXO8ZhM!`f zqbz%5T3sv2a9O+RS{anlEx88+MN>53GC!+275Rd#uttBJ2FH9uJkngxaJsW_63C3loRRa05ozw~yRQLqx3;t}`2S-Taw7r3NNRS( zIkT_hTo!FRs1a>w*+IF@mo(0z$8=*n(yT<;8Y^Aj?zb?XV)=XS>*Jw4RVa3pqcG*xlTLS>BQSZ)N~3ZzM?bi*r8 z^SzA0OIr7&|KC`B>&o7E`wg2<0{{8)&p*E}0L~q+x^?ad;VX8r zN7mADe|Ee(3L~Z zgNqOF%bs-e8PMqtwC(`Rh_8C?#pjN@G<^Bo@g+WYpz!%i0wPGwrdshSzF8r%=y(F( zpd81IH~LtHVVH?3Cm2o4B;p10o|goWePrBB%P|SWN|#izkFt}onBtT5#EoI8ola0C zE~88)rFsz=&Z3KFa^pOV7PL`|>`=Ncp(02|>x!KzLLO21BOGtKJPuZ<087HG>Wg#7 z^wzl}#F)FR*(RB~d;0?}3xj)$o;`bnRj@RBoPF;263rWr=G>8-O+t^=x#O8E;_{<@9h@vN`E;l9;qI zGu-~nwzu`^t^Ui&^w0z@xe2t; z1TMJ=nsWjnB9UEo6R4pHTyhgMLKC>;CaBK|gqU0|y9vHNG=WQQf?8+-m)r!eoD&EU zw(hc<;Qr7AF1ZO_5t_gyH^Iy21VSji>?Wv&CUD73pv(z`Sm6#YnLuSuAVl=M!w}Kv zF1`+U;GuG80wJ!?f)J2H6S&lOj|U!-<^)276Abfy5#$Sqp$S}a69}ORTyhhX<^)3Y z?8|NfJ~V+#ZUQbeflF=zc1|FK(#vjwVrT-F+ysR=fe_CeVO}DF%md7vK!_`_Fth0S~5#O!6o2XW(j8)C_b9^ zib~6A6k6#{Q6);bZmGf$_hbaHoCaXo2;g4dEAGeo}fK{Bt! z8r@Ra&yrHXMaoi{PjQCaCoLxhl0YDn8~2`?Xfkwh=xD^G+HFG-Gdk0z<&hN6M8^Xe z5*3WgYN-}@nBUJdt5Zv^95vd}q#q-PQ}IR~?Fl_1ke$@L z>a+I>>G*+JU;pM5F#XT^`Rp)K=us0xbh;@6E_6g*(TRvNfCwSqq*_g*!>fM8zjy^$ z>gzL`=d=3y88(5>Jq)pMFWxJHKPt}gA@*ixzgL9VU!V0}@t8ikF9%m&9;c7y(H)&B zd&Ecg0J+4@Nx5-XRQOq0_xR`zLkZ;EhsYSE9_1jeMW`SYK@rsrlkE<&LzGBTWw{vV zkLpl{8wzkTjokA?B(RAeaIRrHiPRB>W|(O!S(SSOME9#|JR|9}FiZgJ$8@U^%TlCX zD0O4gSY6UGJr&0YDuY0sa^|Qo!E#Jwu@b$yh%f0PdT42u`hi9hT{AWK7!w{V9jhDvZ`7W(rqVe}Iftkm^sYJH7 z-v1wCmsT_tn;kCB9CmayoyfF-9c7BQ^BSd>7^$o)g4np>lp1cVtaPhBRc0&Q!M)r6 zH~M3t!4E+I2G)Ze7R!_Up3gKZlCEP0)E)|hB5JCMrsAu@D8>vD_lAWaU2$YWZ;XzJmz(^mfHxomN?R7d(9XiYi(}ZlJxs+%}6FJNz zUAtRtcXPJQQb~*^CU6EM7{QxtPi*p)^avYeyeRAkVPGy8gwT4J#bR`G9BY~*l!6kP zm2E);-W)ZNia0V7<7qR}fZFX&lq&{e&{;4Dq4h9Q?4}qV7&=#ZYRa_}Sg+P2RI!(g zmkOjg(IX(DTDgffr5qXL8yE`)A+#RWRX44StlF3Y5kp`ejq^Pa3k^@^>UK763Vn(v z5`#LG?zpWW4B86@A+#PQtubx1WUpkH1668pAl?=kP4?D`*tT4XT4kx+mbHQiMuLE(wnWz1oMX8P_jJMZqKg4|{I{?n++OizeCg zK6~%e2!bF(Z)m~x5IU7Pt#Ugxr7Ec*l}ai}VG~kAQkBY3NhPTypiQ5EAT*N<(oBM& z45GFQh|`s)I6f5~Pn`9E!-w}?pLi9#PU!nrl0zqzefHjI57K&H^L_g}RMuMmU#(he z{pYoYlkjD9P@5yzK4P=K)FDAnPUAv0Kl)KHrq70JsW zQ0E9Xka~x%m}01;g(L)?I zMln?|DSsZ%GT6|JH6Z^eT5Bljdaw)!dX<`mkg{b5#flm;L<|vv8 z+f5}Cvm%0;4Qhj89%M8KUtA;dzU%TF!3I({DSLVu1kxPA2E|l2DT8_$1l2i$4T`I7 zQXcg(2wpHputCw)O)92d20>+k;PQ1?H>sm~83fOtBiNui>n5dBFN5ITIf4xevTjmV z^)d*)ZH{1r>Zo^YP`dQuBY5{+f8HFy22$_Xpjzo=5QuXG8%W*c)aYdplotp#IE%W$ zp~=f2D9sUUAoY$7&J11#L2-^?1F5(6Jnnthg*k#v4&rTaV(cTyuHm}z}tDtPD$NS)=TU}ou-(=3NT~|Sqpl+$h`{qkG`Q=&>$(D(0_C(l z-tW0|Q|@}&8%&xT*JaQiD3t2)zUk8Ktr+_T^J8GZ5@>B^z;C>CYv&tq&~3mQ%$Akw zRR@mq@^82_ocS%$O(xp+$FIL&{@SYd^%sw_FHvHTn7HFBG(`uWta9U%efL)i z`v#MH>-zHnoLS6y-KF829dkCA(3{uq1+C6v&TB8->KQR-gBiSb{o6p3vzYUmOEeQbDJDTIRW<>rcLJg%Jq^1$B8+wyfmCe%*n~BB9b+f)QT*l6ZMv+wg!=O zXOyIfEL_hb%D`@9z}kMF12LyKe{I$Kii=0`mx?)om6!vPynlt30RVR42KMjzN@4#B zEg%5wg&Wvk{*}W16tStZoW(|9`7Fg=+V7L2Uk9HrFrG)pdR3>57C2vJb37!cJSo=Pwl^a|G9f#*n9up^Y>O_Y+;)Ha&aFS*dgE4R%j5g7Z{oWPWD0l?H1i#^m4c-|-XC51 zKkk1P<`2@_9Ot_Oio^nea3~au1QUsxG%lrGz3M40vXoJ!B-pI#Qcohpk&D9nui(}d z5aovhLorlnDy?3omCRJ|Q8W-3D%~u|4US7HRjg8}oKQs4Li|)@&0J$x(2Qa^4dZE1 zbyptp4d+4w*-WI)HsK-}O{FEKRl~$$i{rYsIIuW8q=uSsaFVVX{swlMml0|}!i8QR z@76W6ULCuT`jpj|jwXVMaIZVaxCu|WQFp?1Fqjmz)`TEay{1c)_g`G1!ohf=L}Hq| z7{nL18Ysz*3{mJ@4H2PX;oANhzgTJE$^CBTbxZ%6YJb>K64+ zcW-mh!(TNF1!Ez~DlzWr&Zo8tPTCs=L(JX$sD!$`opp{P(Ylf*iJ{0s-&$t_lM0m!ixru|fs}6$g5?+a*DDszx2lca_1a z)d4~8Sbe!@5Q;+0RF5yNhypexaiI8`lr-9HPOKFqt-j_lZ(kJ^fuL~2hR1EU@tvml z(Qw45Lle>M7LE|(HB@g~6%`IbpcW2YL?6o{~yTHEdOr{z?kAt-`k%3LR946ar2`>7e+F3%2(OtCC_+A|O(^I^&`{tqll1El=9R6xZ&QJ5<(fEZ(nKl>{J# z`dGDDc1voIwPC(zt4gm(4J51Gq+Jx=tE-a2iC9!k)eG+GOQ*W5seG2Mn`+ClNrf|P zx1^(0NufkEQRDE5+xAX}=0G2k9ExdUP_d9Inrjhne^pX25DnCm%E+B$_LS+3xWPoX z`QpSP1|_nYa%0+Bl@y3aLYY>NSnXth^A)wOP|f93c$6+B(d4-7zRTwIxFm(b(R^+o zyDNsB!Zi#hQMj*ZhOKsMn(lTO&%drnip9Vf4U`k@t;QCBN#r${9NAL3HEJ}(Rwu`~ z-p=#+RY{RVFp?f*(6tZ@qFlOH#BnvBOePCR191;!JpX-FQaA?1?NO6j!_;DX891w{ zeZ7_;&=$64PoB@MN(zA}W#`&Cw-!#TlU$m?vt*9Pla@h|lzXh<`Lh)0*1d~yY#;>1 zB9_Yb-R^lh(LkkRxoWEdo~9_qhTUOhZ$93H{0veG_bkd@Y67(@LF0lXcSVfS&&W(g6T)fmVr!q%tHFLFv<|nokr= zI^UaEm@){4h6ynaN;z^2ng<1HPg&Z~h5TT^VycdVf`kRPw_aPwtjPu_GdM^Iw2{t+ z@sSX3r$FsERka{WpgT}C4N3_VEJRA4Hj^n(DwG`1aLE#KDTGyBHZoWYwySX=)*wQO zT*3sN6xN7BE}SdX$i6>AHI*J(lM}gS8zw3Gv?|(kri_--xh9`!a7=M6X0QvO1r&); z7MY6RhC+{wOlw#kj$0xTFT`7Vv{UFb!*!VtvIu?JvgjJGP-JpwG}j=*$QtcB$m5o%NMq177cvKUHb1p{Y>NH8iyI|H_?p!L(9ShUp?hG@oU)SrxN zsN3M4I0xz9>^Q<#!_juO-yXnLqL^u_8f4`%Ao2Za%@`_WL$x>- zN0OW|G2Gp(x3Ulldj49ff!G1HijJUA66(}b>E1Y=DCZEf(N=0)A!@}-dR{&qNG)*Y z8Bc;iyJ(~e6uLHh`|v_&p%f?!W@hYf7m}nF2qtR8#7cpY1KI+?FqQ2fsI3SfWoF^j zE(|)#ux_=I>24-nfRVMB^YVpIu85-|$y%L|6c6S4lkLMY7`bPVq)LZvl9ayBTzm#>Ost8aHh(I`?VRr19ENGFZJ;Zv4C~yNAP`Kbn7hKb%Yj<GHxY76-7-$Ogm4lHcl`LH z-#>cC5r4FQlSUv^GQ|m;^J_%%G|P!}UdXFR zxl4<-K@8Ud@GpGxb_h)RBFp74X4BnM1+FD|u~*Y^mCxiG<+OXu=Zi0RBTK<@&fBjh znNm9ii|u~0r{)GdcS!L?7QEp#UZvMA@%>E6U^t^Q=oXUhagFbr7QCS~UaC6D^kqWq z>6r;K9Hwkxi6kBm1bny8l^7oT4agAUlois)Jfw_3`DaP5^|ZF6Kqg!hfOvgAKGc}|NLbN&=^lT%YOP1V7n+u^81B#^!bPyDMF7bFIo&pp7 z`Y28dot)N-Mv#2o=u`17HZ)E(2C>H`IZ;p%6

ZQ`|zQ3!$t%83oiL;zv_NLLG@j znhJy4C1II}fcZ}>&@4-(aVbnR3a4c*nV0GkMS@9twWXs3;u7k8<@_Uu6H7nLjjM^J z3u$rG6|)&p?W--_Jw@_8Z2>5eD26pW6plwHWibQFO;3WH9iswKsnbn&u}P6mc1O5T zQm|9l%!7=_Evtx&WH&F@(`$3$2Nr=qXE7(MCzw>bJ79>CSGK9c$^fK zjJvaY{sO3(5m=_fv|+B7E{H^2>wSFdpR9QqQ zjLSvFJs$JDdLdLCWJ`l^l&Hu2W@3zW)T$MY_f4c+l?i5K_O*gPS`FstZs2s#Pg@MA zs;H8sp`6d8OgB*PVxSY^!pU;6*^?w5F7*A~wtvv8_j3(UuxT>v!B{y)G*ZcCH3CLt zNt$X=U3~)MM$_zObSb-JTTXQFzILI~RLBpF!e*Y5n~^|VCd+A#Ct_GN6pwLmCo5Hw zq?9hkqda+pf$It`<^ua zV8ON;N=F)6pp3w5l#QZrx-l-c`CK5>FS89P5SE}0ks);6K2;hv$cvvYl-Z0`=lZ?% zvDwQOLOFjh+aZ%>4#Vh>Z4~kaa2XC11kWlXP}#1GP3VMLqeDukb4pTDK@izmHfvYK zTrZP$haS&s7D5TO*eivbwE>o^%P;}kesyAZE7_toXo?`Z@S}XzqN81lJROcOx>(K^ zl*y193nDXduO<3kx)6$_V%D%g=F)s^Qi7mlCBcuYRFTO-nxajhWMi0$u*I^Skip7& zwPTiRbSI5ta-*DE6Y6>Q{Cfn~zhy^EN|Wps-OJj(;{~9A5kpbN%vGC| zFpj6AH8ca0*&5R-krWoeV}4ucauyP67ET+v0jiD~_=H5+45%cXc8@@Pch7}FJ>X$l zQN!AJl#{wSxtQ${g{%~b6)3e{v*kw1Z&OA+oDZC~Y7;W2x4}p-)f0zYazHK_m*YU5 zH!p;?gKRF0#{HF2xSxqb8WW}MaK9GL6JuUR!2FpT30z_{9wkrPngk|&q@)c>My^{^ zQTHOaFE#f{sHRE&G1kgRf5oTHV}`Oz)>FS z6w}pQzlz{uI*pL5k;}SQLOgF=2#u#I3To?t4&N<>&?+Ve;`MwzIhhE0n3NEIhD&0K zT@cmqsbS``MO0K^NdiUgJ8f}oQQ+t2)(EW+-3HlFDNUruIA;${juIL7!jo@rc})=P zi-o})v&CfASHClAA1ULbUVc=y88+2s-K$|faaB|}9F8PlvQU;>c6FN83soBdmux$V zY+4pubH9Y6YvUImwz4M{je>Rq29-6}7F5?_mk(XhdL$kPYt2~{F1ov;)4p7m`$Sf+ zW+qmq-YYc=ORq2+nMYSe#Ui0Zg4g+s+x|``yqswxA_7*u$c~cKIk#{7uC0m+MZsQH zwcZ-IS71)dt=xbt>Lc8&b})y6s&z>7>ByypXPa7u9i@^+e2~~kt0T{8!uoM^Td@e76v9745O4*buY4< zu1)KFM=PwML=db9YJ;UVK0ud>zOX@X-E?_KE$+fqVfU+#Bto zd$)nC0q@)G@7}X}1ab$wXUE+6mYsc&LExR+&F%R1HpnCJwk>Td3{DCBy6??C#TW4Y zGsr3M2JZ{J&-VQNWityvQ*ul7-NC42+w!*%CRnl%RcO$LjnNZ?P?IQi z2muD`J$Ayx>Y8TvHJ0G(bu^?G)G;xxG*Q&5S1YkVb{H~~TBe`} zcq<=_d*2655r=0;7P>7?$`on}*64;}89S0?8>V0zi9AB7!K%cx3BSJ*%vj#{LQ~k` z30vSjaih|v(gj1U4%C_$h_`b_C=#ir@K92WbTloSPe)T_?+-xJq{BmuL7j>uHJRXD z+fJn8>`>LmZ3|p(?)E}GGc>>i9X2hb!gsvi4^8iJcw!C8&IB4l2N5HETX_dU?`zc@V2VLI8&RoNn1n(%ms6qGJ2nbsI>0wv}qnL;S3 z#Ij|i&LzF?hNjPTcaNBT|~-U7X$ast5v0u#i&Wm0K*1QkXB4ZlHTuw zrr+xDuv%3w*P03`)B{a{8uW!pFqKo~T3UiC2_fB#Hi|GUf#uV>_g&ERTO1xFOy&Jm zf2vlG5CN7qF@Lh#5d0&ESI2dPNeR`m6zGuwEmrov6PkYWjE5fcrdXX+6UkVrXw$l( zj#FfU4EF1YR%TPf1Oit>sc2?GdcPN%-tF*k@zglrNAy^MWgAGkl_{7w=>E{$#DKH1 z%45~^C_T>NI%#>|0ZkJQPuxFl+tRQtB?2+F&S!ObKn$C){HV#By-Kg3;h55qh;+)L zy>Ex6affF-EK@iZlpB+HmyZ?Ocxjju{Crid=kn=9-Hb~)HaBiiCCc)?4VuOro_0-w z+n542Efk|0pEQsW2^M~6$m-&#P_Fe&r92)qx?#3&d*2F8qYh8A8js+FnAU@3M3pkZ zda($X!r@>gh(;y3ZmFGsBEkmUQN!N1K+}lBQyLF*ZM@#kDp|Iao)puh5);Ph37qft zo7t!xl*~LdiH4dZ+WR0h4LdwUph)t#ghvWs72IT-BMYts(`6V`K*9{POy{gBst;m= z&4>S!qJPPL*ZL=ZO<0Mv=^^Bwt?Fm5@B`wz(1fr3;TI9R67CWpP-Zw$hpu;nc z$0$)K^{L`W%}Qz!=MsvknQfIQ#$v;`Emdo^h?3C~l;M3NG=&_VK^qKVf>g9hL&0t$ zTxu}A4u>aN`lJy8>DZtaISwY#h@8Ze-Zwzgz^om$ovyTkiHaEQ;pillXL_OXB-HPq zN+L|gnoym}l-s>(Ot-wRho*NqJl%dw8CAm4sB0OZ;9v$MRf7m4*)b)jB-M7Y#p!btCLV%cGp(eH}FQ zJ3JCN+pdh&;n0~aSklS$h98fbdW;YlY(eYr_6L)55qAss1o^??TK zC>+WslmTU1hMq{4c`{lT z$qbufffBv1f~I#mJShtGa89({;|ZnBr_&Lu8l*vOU!phcrCVjYDVO5jS3uKeIXqC`nDpC3 zK3YVWMtBINfUS1IQnTlmCBhF`MMjA5J+2=rbiCgMO`qxTu=!#(l`#jD9P4v*Rqlk^ z7MSR(mf08(HL&Lg`KfB0W3@rS`*LXd4Gs^Ct3)$wxBYp$OM|Q+daYS32~C`~s$dE+ zj78gRtxz;MxE%Htlb4C;A*mx)LXmg`2Na=U8cz%5i@ z*WU?EpW*Q6{z#jN!<@=Tth5qobsGty#l!Bi1MKr8YNvB4(_a)Hu)Zrmus7gh+NL^BMJr?3{ zSR7$lE?@8`!}4HUrUsQltU1U;l4b7{n%?g4gz&_~42JSNn?`V@yp z=<=4vR?NPrjSQ`CqM=f@&%@Oy4`HBmdWCO>Lz8q98m9x^`=IHQ9i9y`BzeCbntr{* zvq4TH?~9=6lN_E6vJiP+2u+{p@N96E(K~^rU+3^_km<)ehNe$&cs9t*;~hcM$2&Y5 zTnzNu(DZ8^o(=Bzd56&SaSqQ0cksLeX!=-(XM@Z&-aa(_8i!|tOK)Banm)$i+2FF7 zw+Bscb9gqmD&{qz=~ahkgS%ed4m3S+cs9td;%!6I;~CH8FJ5_D(DcaR+2H<^w+T(J zI6NC<9q}5_^w8ni;KGu(0Zk7ao((P)d39*I@9=DJna8U^(>;f0gG)5tIyBv#@m&7$ zinj($cO0G#uAX>RXu9q2Y;bqPt3cB&hi8LJAYK`o`W&7OE^>G!XzF!%Hn@-BtwK|e z!?VGi3GWM_>-RZ48(e_!Rt^t8>%l!A@EpGHuyYvoeeJ=Q4nFSvqW9C@Hy%9T9USln zPx5|v|Fho0{`>YjzW44&_rA3Ei+d03@q16&{VbT-cXp$`&+L3@=i@uC+{x`car-l# zKiz(>=L6gA?Z}ni@jm(JwyiI1eSGVcTe+<#`r5w8)nC5)>Z^sTPd*|~{uG=v=$^!n z|Mki{j(_R+RYyO4bp5z>Odmh)=nDfV5nJE1jc=<@1QXBFqz}a!Rv?l!Wjfa7)K(C# z7RL?`M&`635>@ne1SHm?K{?4_Bzp3g1!Hlj1xNO%Xdsc0Eo)Y@mLPI*v0@T5SFor| zuNf0tLKe*Q=ZweZg0W&`!a}%G>10x@RnSziS2C#qR!Z_6b`;cm0*_}aMpyD%3&!@- zwnGlowN<*s?@wF&zP2W``2DGi-`5s?7Qa7b@%!42(&G0gFMeO!(qH`k^^4!vcET3F zKWXv%+5+X`_s1?2W1xDlIkxCA%0|#YuLVwXg={9ui^DO_+JRB6L}6LA-ESu6jC-R6 zV?Cz{(Y{r&#P+asoN@R27mUfSmZAJg7An@P z7=}R&6>M+tIHI9M0P&}xOuJwV$GJh$m@}R{W5JlE8vST8gZjhW2{Tm9BuBSeeMBv1 zo9YA&f@4goFe>`CI<=lWEG7J$ zxiB~HUo05qekus^Is!CyF*uZp;52SsPbzSN^v9F&yd4d<8<<&8Cxv;7`+s+XQLZIx z!)U(KFvG(hqClO9B}732?ygPC3>ifH2GiF_-3FsW$H1?A?gnE-jFU;VSqvC?s}xX* z@hlQoStHxLnc z+8BjnTpH*6X;>O|<}Fqh4T?5t4zsppfz{-`tpqaB3T8zb`F60?YRAW-8CAtporr-0 zku#)Q`!^V?A-+!MYceZmwNkcCB$7cY8?q$62@@nSP_qJF%+`w4g>H5~yGJ#acS-XY=11e^pDBhe@=rGo*Xhs5I{peuZwU`J-s=7%k?Bg?Qtjx9tRy?&u(QkYmB*>kAdFI_P5 zMXDL6hK4ru`>AFnVI#Ht$Z{|rM9Yu?iaK}OhB@f6XvM$y{=R=$FmmB+xoLDV-~wI40vXn2nd(rQKiT9h zC8uUc(@sVej7P_d&$0KB1>@zf}Nb=A5VtpBv5)~*MkJ)09>otv9vqL6g3tJs58U#8R21&VP zw4=N=1}P`OdEHt+KB7#q5s)=qRpaCs>iDIFR~>%)f)TBcN)pV(CiH-0l}M9?s6>@1 zVVFWt$Pk7TowVKw$|4q@E7kYw3&teHP&9^ugwK^&C6|cnhUT+9$v(qt1vtN<%oKzi}f;OUS0TZ{dMF(*B${UQA zK7q6_)n8gL#?Uz0>{%VM7%zv5@RfPp(6s%Vo z(h$ughC(iyt-u_PSK3_zZ_#ljoR0-YC3s*V190GL79D+$TYSby4q*^*e1}VBr8sH% z6()^}FvS~MkYb29Y*6(guLbHi+)-FG7z&`xaf56yS{UM#G;gWJg3_p{M4B)o1%}TIP$}p3-LW9~WZ6OENAygx>i2uHC znlpyRoe~Gm3E-)$NKL>7bR^$z7D9EW>U2F7u2`@H`U=<&#-^i0CB+?)6QYss_@{u$6!XmLlisI{B*w10D&qVr+AuHX4sDpiAxok9SOV8>?nFV(pX)+eQ(PC%;_3{Tu7cD+}KD>?GZ20@Ex!_1B zNOP?&$i!akat2k1ghFHhjh7hzWSooVC*`6wjK+geyHlSF_I+w0cXnTNH%=V~|A1GYEp9vO`eyCL?1~G8Mp`7ieL_Llt7Y$$e{DN^P8I^?Y zm+cf`5rh=WwZgCsuDKz(M2L+yC2($hsM&fsyXZMQH^!X(YFsOzn8@p7rq;s~BSlYV zn|!JTluk4o2?fE`5>*m!^i`f4!=uY@1I~5e{rLq{tJLK~WvPeN1-@W};bElMXcqhi zhC>i3#iE*sC;Z^}46*2Q_C9ulF`oqE_wEQBlC35>`AD0`6OBwXqu6p%Hzl)Dj!Ar& zFBTWU>ae_E?Di-nmq2QDENrTJX`&~PmKv)=oknPsLU^K?j$lKgHe?sUYX80kW4jS3 z4C*z3K)~1;HyZ3HkR1?M25YHJhOwkf1yU3uFu7p|`!`x_^8>q2H)*8-k8yk0HaqE> zRAd;P3s;0fp3cUGA+g_Vj2BQ(z?S%Ij@#7edcKfqX@d;Y^0VEDQfYKXU>mpfVgq>C4>)~814xCwOJY8{Wfn2Oshrv?Y zirRDNZ<3~QE#s8KC3o6=pkl!G!l!>3fFtCL0whk^JF}+_D^|bSR{_!UoH8o2|5E|` z@D7(F@{!ix3P%$+Evz?6P2y25mCXgm?;)Az1Ti3>2|UIkZ!lZ zTJl+L=gLd;Rr0-A#wo{xp3zsWzCRd%5>OxztUz%Hq@D39-IbG>j!3l$*C9GZzDEYwqS(5&X}>O5k_yE6l$+~#Xs{_A$d1LBz{PH!jcavV|micA$EWiAB@7^};A6@memh*9a zm7Hu~sXb>f@xse@k$2w?+;CPw(c!HqYOM-nkxGidN(3U_o`l|3)3Icw0tILG4xIq#nz20p_kRogdU8oF%U_;*iL zsd z4ep8+2ssB$0!X4wT1@v^Ur6Mx<L?P}FLWQYbI>{w9OnhYn+1S`PuEsHjhe`~aHMujsA`Lch2 z(fcOh$`Rm-9`pQqU!}M)Z`{&!6S&4~nCt*ibjxg3#|xK#dbQnYul3tdAmNanTmCk9 zLtQr?R&O$*f270jP$1$UIkTG&abs||N&w^RNoHK5;Q&a@QXMvfES?#q*_wEbY2rh0 z5v-^^^7?;?1~?IhnaRNoVMxA&jDci-;s>^^Jf`t~1h-@Wx_ z-xom5zqfn72pE?CyZ_nKCtme<)&IhIJO}&Uot=C4cQ-kjwdgh0dxnd1i|=|{ulmUE zJl*;Gd+1F}X61&DGm^ajj_KDqc<*``@6IO2!WMYHJa@D%#<}Al{H-rIe?R@Q_+9c;hG%>KUr(g<&#KGvnhgH6sAu4+-OBOuP25@xpsL$m|u-8T;p z9CSbbR_E`>v72CATkX03_UYHSFz#=1j&Vbb`wqIF`&H-f$KHJtjB5o5?mwMA#=&^! z!`8UB$+^h|#zp$(i!-C|Iq-hwx17H}`wKS#xvt1(Pj7QTu0;3^4r(qyo)O_M{P42~ z{}Z=4ct84zOXIy`dey{sBi=}!;qU&&jkvj0ydlOz2i?17e)F!^-2~%V*0}G&c(BPS>J2f@ddjzd#zFdy=Wc*; z&xLV+lj@5bVx0Aq5B|A>@hvyQxc;qAr@Icum7a2gQ{oGZH+sqo4RY2~zUiqB$nR-i zT9MD5?l>S)7?{E2zJ&}KJZ2d?5pTYgMH@I=YU=5DL2@& zS%6*XDKA7i>nUH+age@}zbsN`!~f8??KyeNarnw_9%=`^y|?W~w?Dbn01w}>Jjws+ z{M%dm>a_d6Q>)jN-FsgmKh1X&n@KF+V{rOh8cmMM!O^fB!~@l0VnC*5{|47W6CgQV zC(NQr8)+nMI?tG5N*E}qW~*8h#$>ABRJo2#qnKR|HVrbRG$lQmAWWKPO0K##zX z92CYS=XW$v=Ti7U3Woc1BROL87~oCU=zM*|Op-R0$0BUrw4@-TNhxh8hqx$<)@?dD zYa=*ogUChXRH2^bVXYiwnsSJC+8WcvSzADMq#^PE0~U$!;BITNV1|^jS&~r2%JStI zt=I;%!HA|1!Whuvb)_s*M7^uFxk4k=Jd^HG@ z$$^d+3o4lyB}@U)GDdvRRAG9Oso6_f{^^Uf9CPKBc{JHFGr2`t#?t|r&;nAs6Xgw1 zI-*Gj3MKLkEyGD6`*))vFmRu&m{uyhzLL zt@}rnmK(Q9%SOMfkA@t`xGfg(fKY%3MI&&AmQf4h%_Q0#glRIAF}i^Q(vrrCtq`4b zzGQZ>ay`MOQ?WAKPA=KpIT3)_I_o&nIt%}Pi zHfCE8$eGiJ^My!@Y4jAoDh-m?)W;xcH@JJr(ct8%JSMP8 zFsjR~(6TLm=prpIAI5&PX_>xNaT)BN^Ff9agF>GISGZ(E)Fop+9&wE^LU|@ewfV@n z8P*~(oVOcxJpr-?DQcF1VwGM*E2%iqE(CMSTK=(%w7h&W{ZXgoTNRg4k;;d(ZnkJf zvW3JT2l99c*jZXef>@^u%JO8|$xM^N>p@E`=(&JD&2`OQ%5U_Q6j`H@s1ffjYx$vz zw7i@Y@KLAbTNao5O}d%x=0k;U5d-Hycnc&JN}d%lps}dM>@*(Aj#JUGUAMxcQ9Vy{ z*|Y@HXsH;4d-ZfvltZEAxctKxX?gh;#iLHkw<<1U%>adrOrdSIvqdJ%;tHmW&l)c$ zheQ@bW5H@Vq1XaXM23L@7U~zrd0Jtjpr#wEj)Lt{tq4VzXC)tW&i@~JV9!bG==ZOr z4nDMhy8G(wf7;T#p9T*v_22Y1Wgko*PoIYE+++d?J&GrgU{ZODhJ%P$r3r0VKYbDXb_L{d}yPZ+{ci)|~#%=lTyNP^0dspB&;V@YI`RG&sYO*iI)hY^_{OkIQCLq6RdBjs30AAe=-e{a!1> zbKqt)B;zR!)LKlXh$6V5DWop_N%nqedYg37CwUZ4O~G=@E%Fj9&2R;?Q-G8tCnP|k z`o3Af&zhQ|eOL>LQI+7xU^--U{X%=B(Xm>Ft2g3RJU+pD?Hrp;4>hm=kYqtVz+w{N zK?2Ujg3AS7@)Q5~;PkfYMW5tRJdYhQw`{z`jH^w!Cm1j<5IIf=!F{=Lrk8+JlM@Zp z?rHK=I@a$+yG#-v^DQh|?1mzNM#W@h3=*jt4AnafiwR7YKnox%-X%ZDU%=DbUU1PT zc@$52N4IR!i&+D@#i(i{4Jk+@qqZZZe7k-|;6lxCysd^cb7(a(tzaNSH0_YuNaoE6 z2NKT38gjE8QFVWVoIe*lqJg1 zav|NO^PLcLX7`Tz=`4x%`(_IyA}JAi3so|V*@#sJ<0zp(=_c01o1<)>Aj4b-p-D28 z;&_s{^e1_*e|p>VFZv{p;_3A0mQAPeCeHGavWhp`@_3Y0N;pCnXIly5)#)@cq=+aE zsxJ&<-Da|h)N)v)!4DMC>if7ws+kw4m--gfUr zpX5 z$5e=qv0|;xP82Q5+Z`rdEMO>HLa|GKl3)Gk^tNxi=#xB(r;sE1Rs}B17?<-}nx2Ss zA%%mYszw6TB|oDtfon{xrqg`Z-{Hr6JPk)vN(JqgDv1ataVA;FRO{#%3=Mi>vO^_v zIgUhe=F)-dBX90bZ+qSapJeUq%%f=Mz`kWuMQj8TV~K<~5o=^n$kC7n>g$}fa{w|L zWYFxOfCNFt9GM%H+91I}rD+rlCXoX*0@YZUPbJ3`+KJRVtdIqXe~y_=lRx>dK362Y@>A#HGM#JP5}(WN7Y zeuL{)XQu#AE}z@;th>lx)mu*B^rY2st_42bxq_^>>y8RnuRb}~2A%sfH|_!^z`rlJ z3wX12Fn53BH1@@lcZYU>;fnV zTBRC|0$F&(cLqU0U^!dm(NJG#4Fi0fk*ZAwC%H_k3v&1+J6KUFLhQsw3?v^AbJtK% zvq6uh;@Nn^ZnZ|~s$OmD{bG60=SPvdT2vz;<}aB!VDaib679>RkZInW4eXTqA=?_*#%d z+uDfijqC{=3J5u}FEt206^!YlK9eaiJoLz~6Swjiiibee%RCoqUK;f zWMU`>^1KM)Xa}SWF?cr5CGKJ-`ANDgT}zabjL`?BIy!nIXbvN#f(lZ}C97#JfL!i6 zvGeMVV?TAe<2d#M8Sf@(q&If#o8G>qje{3 z(k1hW+`HVZI~VXT@ZDxdt~Rxf)xX=>Z*<=Ck#^qEaLh5CGd(ma;GnvUIi^aTbgvdA z%ru)Ww?mrDwm|*Ej0#6GOd(*%S?ZCe7onOXBvFs&d7bSP+Eyvts3)^Qso1(km7*X| zX<92}tti8`CJJ`fFx?RgfiOV@LI^k&Ny)?DKyD{UbsTLZ(|56vk_Ps>(&**V{y4Ah zc1ob?PKUp2R86y!RI$*IyGwxO>*Mip)MEnsCqJyn5 zU0{rk(6j_A+l&q+t}~8=83O@l16#RXuo3Q28MW5wjs`6$mFX}Ei!R({>-m1oB!i_Y zWkIcI2=64q?H)IXDMpe|KjQ=uYUCEJFmX{s(H0^^`5KW zc=fiEzd!j5s0R4Z$%7~NpXevtN#f-6fBi@ZQ63K78WA|2p{e z!H*xj?x1r(A6z>)*#F%AFM2-beVjMB71%oQ{iW~MeDCvpC#VGo@4tE9*njT+WA^@R z?2$R-L2o- z`ry`mTRHE~dSC067wZ(KTbS>jN+rp#Or_!vFYkWON`7uV!5j5lIh%O-^unC@>ww&| zi6hHXygBicfYkrzKjm`C{H0b^8(CNFH5bM5KKEQjce*H+w_WEH&vH>L?*h&#zQIMY zyeT!Oc!rB&d5>gHaq6O2Ub3H4Jk3S1eAH`B@%7K%y4QQN1%TxyoiRjX6Jl)>;@T#} z$|l6s^N5!<=Dc8Q6Jlc%Vto_h`XHHI4cH#YM4f#WNhfe|Ax<>D2cRE{bJ)oy+(;7sZ;^eP3`*Kdw6zc}`TP})q1Ny&Q6ze+ugo|Qbr@!H%Sl8+Q>7rQI z>94FB&^p1-Jbvpg-#yEUbmF+@$7he1wKseGqh~*UX!dwnlQZfcI{WblXOEY)I-~x; z*^l2pd%Uc_8TAi5VGHuz6I#~%?D-ew&)4YxX8wFx%QO1FocUyk|zf;(jyo|8|q-e=&Owttt2qZ}R*P=Fitu{ChWf{=4(%YfAp@n>_#Y{P~(X z|L>bT|J3~Xnwmd(ljon9KVQ?^PtTvPssE=Aw_~1+=My`7zq$9Wz5ZSb)b;y+cR#lK z`rX=YV)tnKaO)2}pV<0g&p&Uy7}W5)!}mqsFMIFwJ?JyMFZC9E-|D;S{jB%LuYT(4 zd#(Mf3W`}`_uj6{+)aOwDakm_wI~$ zGCNP%{`~g;vHkjO6~Og-{9E^)d0E}#IcL}qbQ{5o&Ldv#RUHfa_M5dC@Piztu1m!p zP~iG(gbuYEhv#J*QGe$-)ZsNhdgVFPD?LWoEhfJ3Txz!wPR^xv zf1a_&11<>t|Ns5x(!e>E8+5ze=saR~JhaavcDv{BJYu&?49+258Q3~8yni0CJ6>7m z5xc#ocOJ3ZdAjEjyY+0IN9@*f=R9J!p4;a{!I)dmo}b)^`lHvjo_DiFP`5e`HlW?# zfOc;K+T9IkcQ&Bi+JM&gO4)uNKQLpM7%v|4f9`%|A-+3POng+jjBkFgYN4=)+x1UG7rti0HME%zD zsMo&EgBwx5`5fw%O5?L{`Ie2Szvn#aRWAmf_J)n9UwI>8 ze_{V2uuK2Uz0dBwZLhfZHM_sD`|@36=U;Yya;Lv@*YpxYR?wE7xZwu;$;2orGI)~U=FxKzs|EgpfB zLY^f$E`fKQE6;6xr#y_Ay{4!SnmW$#HKsLj$$QSZ^4vCbni**+IWJ+wR==KNOr+{n`_(||{bLtkd zZa$6N098i~d_tluxXO#BIk%9fxrMA-Q${W^MW!ZEa@m42B0-vNAy07&SvM{Pt|fV~ zSJQEo&*U5BbjvN|>)k@uV_3hM1h5LgW-(M zpj$|;DfaPhA#47BN>wMBzD$TcJu^Xu!;~$!kv!HdWZmqWSh|oFM_n6X#|rCjV-g*#t#M_|Zc6D=H^X5rAl^TB z0a;l&om;)sP$)!!Z6j0FwH%ql*PQssEo9x+x|2b#!4WOJr`wHcf!0_T65oMa$olIf zSxo5)nJlg-y{?qWvXooMu3O0Z$LWZv+!#(84BEp~J=ra;3E6Um)ZqHZS$jdBd*HUR z;5JvJoRezWNK~>`AG5NlY|`CRd;ir1A6gF|R=`#)XAwce$lu4&2pSD|fWxszWv$h9Zl zKZ$SO1qSWQ7Uax^PxXEVWs!b|Ht+(LM?n^9*L(KnjLRQ<#z;3Ot$v}*i>wZhlcJJw z51ZR_#^vubV{`9y$yjtwzhS6S{~G@Y%SUB4f0B346=#2 zqlV`P7OZQH>NRdew3wvK^C?208#T9yd)~8PT{eXoUMvac8cKOk9<*qU;%(AB!1H|H zf_2#dW~?~L4u%tx);d^j&=$(&r0Z2Z?_97hf3O+rgn`YmnMQMT6INK6YPyHuo_Eak zyM~t_#k>k5;2tZ@n{qcLxQmHs^ETIZ{!j6&%*mR{sM1iuhb^|#G;6%jP`Sude3$RRkB)X@q2rT-J26KCob29x2RN8LnO(_F6iU zEeKViVOj1GnCBG>)-{9B>W#8(+l^wHsoT8?+jq}OJui3i|3jXKJYJ9Ip{oyFeE~T2 z|FIME)g9Vx5WW4DJv=gEt#eW3Kf8g*3+>FY{0+bsTn z?7e%mJ#((x_|X)tXO(K~@gS9?(z{BfQmG^}6e>w2sU($D zQaw}}j~(B^uQ_HS?Ld+7W2xA$+)gZ*LQyb|x)w73y({7SrQbK5pngWkZV zC$YKNwEG=*f125^#Je`(Z^Zj;ufn@>Fx|#0T(m>26bjJ_)B#6D`JlVMiT>~_@m}5~ z+z#)zz7p@+igBZTzvY#9*VY1ZI?a{!FzNPN`I0MbVZOJFe9bHIu8qGN@vK+kT^lb{ zRP{PsNs|+iugomz?(dJxSK?h8FE`>1Uxjz&ID6T@hG7jrW~FCMHj~T24 zobOT=#R_}&X|Ke)wq7~hVjhy47RQh&#gALb{=ua8O1x|1??ycJRd~p???;C+qUj{3 zTZ(V8lW`pGZ;QIG#Je_LW{c%|;Ogb1Quu%uO?&@<+j%A4wf=3Xq6FwPHPeoQazMw6 z)?R9SK?h8f2%>FxlX+9@^h0O&ys8V zjo*A2UOE&7GB3K|R>n3-1rVX{^+)|MyooRj^=v%u#L}2ElCEFu;Sq=7c^gd|EL%?8 z%P3G-N#ylCybm0R=Z^Kr6nKkLI$?h)3H$ba^?`U|-kFCTdn`Dz&kNJ_{`yB9hBqJ? z+NXuVJSm6PhK#lS1L;@267Smg?Z)x)p~LWQ9547`csGuh#$k9jj+f?vcsGrg4<3kj z(|92d!@F_3eB~?gu6>VgJg=g!!n>9J&Rb#;!RL*iZElh0fh>Bp% zE1dDmO{mCXqaXJvLE`<_+y&qGbq5>pdb{8oKYFkMuel4p_R@d@7MIuE1s^%sfY;mw z)xie5<}TRm3`p3THTCJ3A~yBGu-)lZykJ#%r=@4zmbHe^aZ9dNVIz3$v!FcKfY;mw z>w^t=&0Vm%G~fWx*F6iC2OIF3yI^r?zybTbr?1(7;?e*yAXI%_zq7K6;ZK_AjT^CqSu1cO(hIFji!r~ zok&=Y`UTG{u%@(_nYS@uey{-tP+{N3fb7x$YSUWGf*RCfqqP#lzA}tNJsGwX8dheF zo~CJ=-e3wzqpouZSN3fTNDnsPHFv@6U;|!r7bKSkxC3sjb6C4>&?KmDDRbkI6Wc6f zuEwkJgiT46n2-s+TFUb4?t=JW1733%L zW=5?R6fuMtZou}?LduIiE6y1qZkbV76_G!&${5eBXFX8rz;Iiowc%T3vT)VK>UrsC zS(IgSp{le6A8s%V;(KXec;p(8{)5-MoE4!z15C3Yc>hP1D7I4oZ$JC^WF}wgc^D8t#8j zljBn5%FByzS5^bNoIP8M(!(a*%iP@XC!YHgl^!l1`|^KAk+<1S=B=7fcC14L@vdXN zMR{7brpAqxs8(w}L8b09Px4uU!4Rhb;sA|C)x%<{-j_e=u|D_gXU`XNa-t#Z^Jm_a z-fSpaS~60972+*Ytdgo%RbFK3oab>pVcL;G9-a*u66Rq*)nB(+Py00=D8n9dep-MC!#KDC7~;9JbtclnCF zqvfCE%!Zo;_O3;8qH#tD4M~^P&ct0v5yr^Rgl8aaVO6Qmp8M;D~g$bXNo2Q~sxY=^_;q$v=nI5AfRU-qF-`n%mgmkiUiIiB215z^UW#RXSYXddBsb=9cS&_G#-MdGBhuM9| zz}xcwAAk0p&pY~#J15@_a`S)hgAW4E|Hb2f{y2H`&mMUXe(?P7pMUu5ZykNdnf~yv zKa@}Z@+ouw&)moF{ZIF5cfa@UmmGgKs2BL3zwx!N=UfcX9=sl;|EN*E;rKoXmpDCs zs?aNUjU@&tw>NVJji(G|yNx-ot@=FHYl1{ekw{HzB2v&(Bkdc-nwf8^Kv~65;IvHH zERm_;j7ZLn*Dc4w>l{e1Y}y=}v?XP_Mc!?H#&8s{5Z2{lQh052Bb?KXM zeQhf8Q+TNa=wun8t!(YB`*T~NS`G!)+F15c*O zj1T$|UaiwNqkH%GS8Y&C zI}et08EG}bH2PG1SXgyvQ zP|2%sjO6H`Qt>=f#bsG5AzgpkN^pL_sHorIl{%W$o87F@S*A@_;%cEiQFy;24ERRU zk<<(Crr5BfKk~K>x;m=njLl5tW~2||8k887#mXg4cUI;YcC9>Paf^mH|LII&1FG4b z*5heY>JpIAFO{)zQcyWC>D4sTK9#-=anYm+3ZfIN!U6yK1nUOP}k+8{Vf(Px$ ziYmS-U$ojz)a6{wbokU^S3PonZa(b(s|wWWbKe_GmcOGDZ!m`Ov}1$&7QJ4ktvo;9Sy|vm{2~WqX!| zLotnfdn(MSl{y>H3^WV$MqFHU7L*>eJzWcF+31OUXFuNk?5lp&22c@Lg79JLQ-fBg zikg$WS#R2gG*y0I4}bh^8#?hIc9V@TTVsa3Y;|*=Y%coR8hG$} zvj*JoHWo4Hjg>z0)C=%Zs-nPB+R$`^$vqd6=WV@~^O4Fd_3=8Hu)=0E7_FR(^(+s& z)iC1a#O*7oAnwN@-}#F#*+Arzek>x|8lEfh1U2RnG_B-_sh3eZAB6RdG;qDbX4I$A zNFAAWBJgubB`u~#HsMlUZNyw-Aj=MTjQ8L%Y3UW`UkL3WCiO|YQA#Fnw=HYAH#bju zZ`+`hG1}ogyhyUXK9VS#oM()<5(Y^F3s43p2JD(va8oOuM$mBX*0FMJ;DgO(#K%%K z_x)CipdMXmLnzVNnOhkON)Z>i(OzqsH_5F^H35SWw;w-!`o5QJP^560)!g~oLLH~y z(F3xkR;{UI_eRF3(#;7Yr!vlotdc@YjQJAJFH)H zy6Y(MWs=|Pi{rohwhi>N2pgKu=hJqh-IW=A6;AulLWX$u%ri=2q5Hh==i><1p{LKq z=bLAq`h4thiWe!m(Zfp^Dw1lMaPf>I2aawIWv0J=ZeA?sJR=V)7reHiDvO%{G zg62C+HwG^C23}@Su?lKvTTeAuDv1^Ll^xN>9pYrcaEn0b!*`a+?%e z6G9K7nh|+1ofN^Kt$kf*FRq0t+8CF^sr8!TOTKIECe zi6^i%_FQR_N$&b-mmg3mJ(*VNOxYw+XWDKD^u)lGE~`*tH7M|9E1L|Z@dUaEW@?85 zcxJW|W(%ejSbKf(p#QcF*nEsFNR7{j>}GAb*}9W$I^{H*Lg}qJ#U$(oGh;dS80xTf*PqT2gYO+J#C6TvXG}MuQxZ7b~?+Dc|+*TX@%7) z&Mo?DWwAClrRmAV1>YWUqUmVMvI148SuWn^l(_SAZ`)7{=04M3pi(Vq&;bfhg~gx^ zOGs@~)ghGiEnIfnvv?}{PqVVk=x}e0p{k@pLwb}s-O)rHjtn}CiB1-eX^0sM#=?p& zZX`lD`Ho+;LEf1Cm@-z9X3iRrGFn5l-{M?h2nFNCB-QDz#`n6`oDzwrgJ3Nbc%XA# zlrWlhx6iMZonjrMF{IX&O*3OXvqf38Kz1(r2Gbwv{)Dyc3GI&OlD5|spzz+EPyaD% zd;k9>cOTz*zz4745_dT(nJbC;>j~9|4+35C7rA z-}&%^r~miq?>?m;{J#(W`v>CvU%LN??)UC}{N5kEXWqMi_fLTee2-85?1_K!S;s$k zJUjmUqrY^tJbLraU%T@zK7y0)_;epL-_!rz8+h*xym$lZZ#V&&I{W~a#6&T9PABkU zVko#aT&(yicV)tY)3zA<;v!x)$D8zmBf-bm&fCj@wKQ7(+SuoWRKH{kIdqnRn5{0) zJ%H^(V!?@!@uPAyq4S9#wlSEO=769n>X;YqvL!CeDS`9m8hK zT#}~+v()q=U2sIyuR39^gNGwFm`d8j9WvH@HfApRu_tymBopTuh*HQ&b$^?re(@HP zvoVMrVKPB?(oVleLEw{Cv0`2F+B$=Gg3U}XhRbG$g21Whrq7)E<`%Nc_#wZ-9lIIH zS*@JyfdkCR z`OVHE7i=Mi?0h~|3u`(H`?=Bs9=uJ9?NB3h z<6|0IB(g2_FDa!$A6=w={uXkS#SmIskIXKumMIg|$4H+~ zBPCe%joE6*@G`Wp+B8%2{EK!Q{H+outFB7<>YV2Hx$4x<+d?LGqIOFwZO9o;v$U_P zE;;OgjpD|s8)Dw_I&OZ5~6TQ?phNJ#4w;U ziC7g$89El)?hYbWS+$V*pq?%}rb`2dkY%jBHrP~0Wx1#}6MBDxuYS%JvK?b=4^Igv z=~CewugbcPliG6C&U-v7ti_ne!?h_9bho%z3JbA4>?>xKKqUO z*_V*L5Q4k1?1@Sb5O=y@0+mrV51=NEW65B;#1S`5s<7gS3s;}%5-YVqzBd@E++wy* z0HJ=?7ScnlRc_5vRB1Mq#n80E_JT#YIX@Xrb7w>2@{mQ07K)bog>8%-OBqmh3L&fLC~Pl(rpJZ_Sf_-)Kiexbjg>$V_xZPE3!kyS<>eSg&T_ zrb5g07?2s8sTb~N;;X-A3n}HQ1pL$sw})&rcI7$s+!~S)rYs=8&Wd)gJ_}ouBCpob zg%TKkr*BqFx3?zCp1RuKVXL39h0OhmXWA{o?toBxeXwHlR*5(RaRK3mjCX2dHBh^u zQqu(UqHs2BF?4iZ2qj(yU$cFGy!yr#a)B@|)nR+#szI>WAj~)Fk{GMboZUEy7JE}% zFGxI}@L;IhhR`X(rO;72tEp%((|tZS^~n~pTQ;>Gm?E8?XOZO)ZV+ZeW+@sf&4@|0 z#5QQtnFZ{0re9DrsY!wBh!bU9w)!n?pEW>zyoH?gSi!>Wq>UOY(#Pd6rpC*fq_tQ` zf?F=+!d5`BL7B1O+p|vjPPg9%uCTa2w4-uAkzReYg~YplPI6M;sws4x1%qU?RujX~ z*fF24qx$4YX z(*>q>0BPTBIvF55N{y>ly+MN>x44X&FFAT5kkb(GKJU}xsApS9Z-(|;O);hM6y`d% z(PLs>cPFSyD*lwqz+mFmmbkP8)wx)f;wH&C%NzDgkagrm`<_Mh;TF=jN6fI#Z-l{W zGp4Mn2BDI>X!UKWvFf+FEQNEl*@4=r0bLYbW6++3*)$9d6>#_{*LwVP3mG-709)2+ zg{N#~L(xQUHtzB|Vd~jD>H^=Gp32C~Uaq?&;JGS;t}d?@qGqIG&tI+g4wKA-6Ip%w zB7_lz8+hRX-|=3fiz;Xp;M2AC2CN`S%9^^i`rO}IHecXvJYOfJMoNRaVyjwz4elze z_DU=prBw#3XARmsxO`sU>~T=qs9c-e=aGkdiVp=xgahDMF zxhN2$wXKX}{UVEd_W*&94gxQSFZ09Vy*1=9FH&Q=7xn ztfD2dV%v;H&3&F^Ea;+ATKvLN##sag{BqI@_TT01{C{-k!S8zTq5D5`|M%W!@BRF} z@46@7{a1JY@ZJ6uufPvH4jzB@qrdPdee?zAfBAfM{>5iMa`w@)uXy-3AO7}-@aa#U ze%mSW;2+;TI{D)#UvqML{QbvcP#y4p1q}bc?#^GllU<%L9#r68@16PIJ8!=C2L6M2 z18RS}rp(2jn#hz$O=X#%g#o{09ev=C^k5#Y^Yt|LsW@nw)s%<8j^+kZNEO~fzScNm z71~1L?X}om*5r)ag#C2Z3}JcNSIr4sOBWEWi7sHJ3#Y2HuB|U_r0P_kZy`7FkTaS7 zK-k2HIV$T^;&~yDR&v>4jODCNi_1K>Hk8?SZZ7hn{-!NtC~ml3WjW17W1!{rJP`SX zR9kngbvxrn?N)crXL?QRbbR&ZIv;9n3u!E2mZhL27cyGB5tt5~cbF8-a||wkk9jB( z-69Z1hD4Azm-+xD-T|atn7Y+np&`!^t%@|K9M2Bh-SH?C=jH@Qym4BJ^A&Wvwnn&MTkn>3&zepz@+%BpIM< zRiDlH;XWPEj5(tBUCZ(LFPp-af8-$ zl`aQTvf;U@ea*e~Wn0K`*VJ63?LZYjaawhbN2ha(%`KuA#bQyNF#I7qn0IyOoLueYS0!u24^J| zy7{U>)wYna4H3OMk^}`xz)7AeRP5%BxJNt7dBl!4{TU<#Ix~+n;F-9ISXJ$9A<;Hk zgG(&yOHPbKgu|PHs&Ng*Mu~<>rC>CXJh+n~>KcD>BiE>^ZXumnD`5pxfvE1-nezE~ z-mn}+H3HYOy1`KNC+HHf+385ZE^ed;Rl8eA39Tx6#1!oTqzh7ap@L&>kl8^&bOS2R z_0hVtgi*E{9jGk&|3pt2e)x6p0&RBd(*z;b!ty=xCFN)C2C(&Vl(unohv`}fP zxS7;bRko1b<$%w2WA+1Rron#@2)n|N$V4T*Z=&UuPp=`~M7csrE$Ym1gS#PCY z+{k&U@>@tcQ=BHET1YQ15;z%DORy4_+6V@*DgnMqx*;m)?o@;@`Qk>JN|oC}j!Cl} zc_30{CN+)rC`1r`TKB@b=r?Sw%IxNl$OlRvDuGx0MkY=5tGAF#&LwQPYCvLX#X8Q6 zT55=e97};O5M#FO$$}m)R&1Dv`o)dpm+A+%kid6AahlPwlY&6axsx<}ZN)YXw{Mkh z=wKT)B|SH23z2hiBmbrPRa;2aw>If?v7Y%7t!nv5AhB9EEPC^zp7h$&g7T}y%$kzJ zz`3}QVp9FeEhMDVU_2Lm8)qX|E+a8nnX64=afD7JVLjE;81o$s>9pjF8(AjR4{RaF z>lzi?@@%Tj%yu`djU$wpH`nv=)LS=L2O1DTr8Sq~xT9aRZz7shn_Ecb_OXjM$0Mr5 z7Ls$Lsgd=By1p7(ZUXmLth8Q2tk_u2N3GUI7~0Zo&gjj4ad9IZq1xC&+6>fLz7t;5QQhDs$%EsHHg0Nth{hD zD*)}+JV)~k1kw*eDl1weExovrR8PgXkg-kREN2NJVz#`oi1>y<4KWNEA#{E0_=T53 zu0=(1>R}f*@)D}p7LtzZRbo+293ipMf}Um z`*U}mAAoGX_0{)1^-CY$IV5}V=G3O-H*ND^f731#_ZuF+^}(%@4}bRSJ;1zBvdiRq zmuxy{pMrXthO^|YuLAab2t4!xUgk(V;0;?f8VbPz&t+|9(w)2+fWb}=6Ris(+(Cp( zYTkUHdSOox79iJmGg^g3I1b}*vGLyeN}%sK(5F`oEexHerh2GL7F~AQiN|Q&bBIlk z1FPyxtY+;=K2AXF>x~Dv7y7nOG|HE;SNLyz0O&!1o^BNC-BwLR+U0VHu6PZ>*5c5B zCzx%_C{;7dRS6;-hnv;ypDRn(UOtLJ>UCUXZ}owuFaPp8PunEQyCv#ID*{YnFK+t~ z-EvnhHU!DL^|EqnV<-rwNLN|kJilK&kqvmp3$KV%$Bu)vSbSicz?rXP_beiqtl(~vWB}WErSS4^Z zOJ~DYLjYF`c%;oam98(qu63IGx7PP|r%m5dfu1Jlv_@R=p+6{kVYG1cahDrT7^IQe z@f59rc-YLI4v{3(oX+I-&*j*&bQXg5IJ$hPXBT&-2dobN4o$(N?||ppPWl6wPjCJH zUODOEeIDZN+qU0Qw)TMc9J-l>4g)M%kDzMV#ql6tt83uZ7yIUXJehXuE=J1ow9L&H~YJCNqso`FtaUUb51^+w&wo_%uPZ6{=J z(ZHVVx)Che<8~iubuc{)T0u=Vkg>AqiAB_GcIsA*!!(ot8`xe`z4hnjUDk~jP`O>~ zwW!&lVY<;;Ojht}1Wm$*ROM}-a(OKChWMy4Tu^GwolMrZR{2ir1_ktO7yAS9>u;^^ z<#mGudbaCEF*E$u%x-n)yd6esBWn{&UZT^c7Gxwjj-(836g0?on)A2Tv$t;eZ~bPV zZ2M(7fRy0Y%3l7mfFb)KplZ8r9FT*2YgMnDLe~<5w{QE-o#V#8!#en$=zp4T0E}b( z#W)^=yoIn*SfpQ!NT$ygWvaH`ha+F|Nf62FYf=r`}6xBy8rmzPu%<7d+U45-LE^U?tSK6@9y8f zd-v|2IQi+j^4%}I`@8SrZ~QRu4@}=6-njqdN1yzGqd$7|lc09sS3i05_-`L^kH6>f z>M{NJ*Mb^?fBezcJ`zsv98;&rqt7}2Cr7{T{QE#wLHGQN&VJ_X&z^nD$^UUSID2~X zXHLHBWcKjqPxOaB`0(3LzWL#YPrmBmv(sNb{*2QfI{nU57f==a`0@X6=O2AC>*>*7 zI7|v$U>-k3GjY&SQUx3sb3wPb>6{ZAaOKE3-)8dOe9-pV6cvDI-So*n-bom_-RtCl z%vcqcB0e)f8PB=HtR%e4t;0qQw$<)(wOAQ~{^W1%BrtMCI|DUTq{dvwI&*vNw%D*9 z3&c{A!mwRg6_1|$wVi~pF>OPI{k*HRf>kEQ)82HNAatK0N;NI;ET!tBaGBf1 zlXNE`O(o7+tm{&HxJnG9w5HxNspf5}0|JGu9Gls>g!nPXMNb-+5*RrTM5*R>VPjG> z7zUntNT;Wx28N{6@jxolh;7W(b*3Nx*iM4ml#-l@Owe)0Xq*si zT4C7Gn@XzSJ%=6`nrDdxVfYz=RE^C4xXC7NE{c~lf z11Yd*f*Tt>;#ygKK^apYi@^1TkEVrzJ0jW7vs&3*o8+q%#|2&1DmH ztk!bbJo)|?5(5;IYZFzdt(YO+V0$Y{;6waR3WPP}{df;~Zl)02Z)|)XoTh!a#)kMjQIT<3^%ov1O zpB+Goxuh()mb0r06`<4l3yG$s#3;tcv$#y)wxp#k9@m$pIW$3%HYf{%;-!psHc)i; zyFuv3(Nkn4G%IH{pW+}hyEmJRkimEbtCa`~)A^Lcco#+bJxAD}>xVlP3xkihSLhRn zgZwf$|BIQQ`G1Q z==0%d(h2f$EsUaje{Ls{Wx$EQq~MOTt`m#NSVn6It#wB@+2n#zou=nTZ%{0UHhJ=I zUPySh#UM@Gis?bO@>Hu9P5Qh)$z&_n$F26fJ)`Cz+Q0Bm{^Cv|P3Vo^U({P$&WXO; zuLW$2LRv$o=+ek09ND!1bVVH*K__3ZlbCs^fKLMm~Bfiw~m1UU+xK53gZ3AIB=S*)aJ=%RRT81kb(vXe+a zwrxx4<=K$y#aw-wSxuy&sCc|eAwXG(I0Q`L*qrJ5kG}J=12JZ_qRK$An)PlS&&O-P z74FdE<_N}!#@Zs)JdP%D$~PP2@=^uHDB~5vgMiIhT`M-APK*-`CkvvKGHVJsLFjn| zzk$=**gX5_PJ;H!;h0hymAvFTE*^Jasc9=ihH*KmwW6qIDugNGSPXIeS9TJlq)0MD zXUp-ZG3qsm&}?*D@pRrK1d;|Juog%&pNNw=N#6UDJBj8D6*RhyuJd+3oDRG=O(Q;A zj2yN%wpZC8DCG&1u>}*IecKC(rhx@4iV^k7v_OQwutHS8E(dPr)44EbDnlmcAR=|> zGN=FBPNG2*betC{ZxAMCm?A!*u$Ha2{JFF7H-iN}0O1gIyAwjk`c9$_Zo;DRiWZw7 zHmlwpQ7X%hW+1F1kqH;;H=uGzEsNIDQjb4t*ByATrL;^#QOWL@;ES582((2>S`;9X z!Vy#?`Gyui{zf0{RG<(YZ>IcI2{2f71+DN5aNj-hIDyM>zG4E9>m&pa#>%JUPU7}t z8^>R9DS>X%p*rzsw%xU)3T@zce-Z1bKbta&ORF}usODTa>kR_u-cAR)iN~A=h<8A+ z?o>-fFl1{B*_=RngB_KGP-8pOhSMSko%Ow9r=#qQ=c{!QEH@~kc7|0l$ooha@8`J> zkyMok!3-7=B9Nlf|K^2+yw(`pU+@ktN)A#iB`1t0l&+N-$p;OjlgeZrrV&@ZZ|x*j zU>O{b0e?YUI$eTELKuXuIK7nJC<`)Tjdg>W>KL<3m5Cg6^_F8gBdq2~irZ{3D5QeOi@ND_89Y$Nge=Lr6V<9t19MLP&`u&rddq%Q z+khAX8e@lnI%CF>FFOc}^@jX7sW!N`@!K`VKmOM{i4c?q7zE3>G{Qj#gfR;_!Gg@O zTxvnfIY|C*5gm=i@D?jSuy+#v5UMl5lowQCL<#A@ZG%r0Og80>mJ)uw1C_0b60N(n z=;+0Vc2ZJPDVo}>n=agOD$iEnss&5Zcxkp1#wj<$c-gZ{2Y2r6rXcDJw650|HED&} zXoN~OV5`xzisqLtCE8I;I7*pq>J2p~Y}aSv-(-5?a0DQUY~TVL_ErdmNidKn?iS!X_Ig-xw3~qPB4i zW0^kqeLD#iBTXaD$77As7q$7Uw#@xSZ`xLt)heeLv%yW|q?3Q*^5Q_D>=gft!3PE;6v@jdg1sIZ>Zn0UXq!xwhs95&faP;uO zP6y8+va*3}B#!{EkE>#4e}cm`&2wOBGoNN`4^VSetDY3y^LG+$ww~tm0$hF)(!`oK zTAR8zTt$cu!RyqFIwIN#YDtEL%-QbqaX|RnakbOIHmeLl`&y^ll7>Z_8D#L#V``Jg z(M7jD(5;5)EY>4LGmpM&CqdI>se+=J!bD#9AlPo)i1R5%CHlB#QngxNSZ?ghcLI+; z|Hw{)^kFo{hVjsjRlv0(I-|G&Hj!i6&Fr-;fTz)V*hp7K^1Y92&;QEN>dq%l^2d*M zXZcT@<6l0-pNfw@`bd2Ax#vH1{{814J6F%&cl5Wz5fIEzxBRx|4TrY!4Ka1 z_In?`_v~nO_m}Vf(B1F6>)eG;K7R67PrmQut&{Fa{GkJxja>3*N@*v2k(4#NY_u_cXmHqKe&E;_k0KJm5`mp zA$z5r@@psjN56Clb;u@a*Bys!qUJjt2W+Cg`Gv&oH&Mf#!~vVA-@21HU=yY7BtE5^ zs6V*Varh?cfWy_-ztC}O_uJ#Yx05*F$oJx-eZbbPx6|<{-P(QYPRFNoYxg}n9S3af zzGEkGz}D`?M(u#D-3NC%4%piLj-A8-Tf6^fCvkx5c(?63z^(iE$96gn*gpQQ7ZSJM zKF)R$x8FXVzF0*M*gkqY9S3Y5|J)0STW3%Fd)YolJF5=ZK5pK{PfK2YqOSXzAN}l3 z#UT#d9n^L2^gq}^9pKsBc`MNaJi9NxkDp4#16kkRv-|rm_hbKod3Jwb_ZSCwc7NrC#O*!1Ke&@Pz_a^#JBb54yWg^t zNHVqA6|osVSW9Sq>{7Wg1b2#k*3_KXfQ87kNpe(IWTX-Fsr=|?3A;0nF4bp&^I&;+A=@`{=Lkqn*D`7>iJtAfeZ(C*~ z%jc2WvPjc6M~Or@HO{~H%R7nTbZ#Qfn&EhJgv=Aj?#jqUVfoUW&D%AxHo+z?jo^(W zdh|s*i9rKUTWnT?Y$_%whuKSP9<9VSG{&KHvI&bCE#JI5=j4 zK8_Vax6ch~le`g~ef3U4ts&i%Cry=MWON`#2VY7Ya{vFuI~@ny|DRn- z9I{dT_|B^L?*F&R0J}^3_wN7SyZ?Xh{{OxE|M%|y-@E?@L1^#Y|G#(t|K9!od-wnE z-T%LL|BoKvz`eZxH}CwdJ8%5>8-MbRZ+YXlfmr^}c=Au5{P`!p2SfmHPu~CdmmdEJ zhynP|9uFTwk554?|DS&JJ0As)elv*U|L5m_>HNFSOAyEZW#@O!e*ElDo_)*NZ#yH- zKI7p(efZ}e{+@@~L+;`GPk-t3M^3-zbandSQ~LDRKKSJaKlI=`AAIzI_~3Kz|J42O zzyGoO>izfK``LRxaPM318TY>A?$6)-!Mopn_rrIeo&56251oAHi31{#KYsjIkN?}_ z@VIe&@91wFefLp%^ueQZVDd}^Y*)ZFGi>DN?qyF{)$%@LWwHmEc|V z?61F&hyWuB*RLBbWs0rfwr(UTHjeX}Gv;b=VJHJ!%hd&{r$@`3L}4TX<`1R`+39GY zg8DdZVr`-Bhi%7PPb7WNj`F-m^H_9m7ia?KvyGmQyX_%o$KG5kr+^A7GSH+Ik>jC< zv96N3#w-FgaPD2~bmUWQ-2t>$Vr3ZOP%S!3J~Rh73i2bSGwsDi0uF= z74V#@78{MQlTDY5XUk3GLrvap3?zMntG&W#jeQUh4GK=l$KSq_NGrO@%!ZmqlSHp@ zRvn*pIj1-4YnDD;^yj#Mt;uRhsq+1uO9h^RTm_6KYNH{x5D7Q{L33+tF~R!WMsr$y zl2prq+AKFEdG;@MI^xk}jhX~t?X4!Gs@tcs@zlXb;fNw3Hy2T($a^}DIwW&i?j)kT zZG~(Aijy@XsU`ARQmvv%0T^fqD-s=eo#7Pe*BfPY{6a^>wTJPBG$+-p*)nS#9dGpu zKuZNeeF_hmi8P||umTBMboXSZV=8wVs434$IG*77AVV4`XIXj%V&7N9xW(d`-b8EZ zT;}iZ!o%R8zbXe4jmI%$<^g)9;V{J}WL4{-T}l^KKr9;cnFb)WPw(E_>6irL+J;HG zbBvSmdXQHOd+kq+4i#D5u_m{tJ;qEOy^)&7yIWv*l;*rFwku+xmVkVeB*>_6yCakX zg;F?UE;na5h(0gW0D8E)t%D7oiwS$7mvpq&yxKHz>1Ne4xR$qRrv+Ki9!*CSC}?P& z{N&E6{s_>ZMnX(%;{@xq!3v%rth(gg(DEfn=VcfWpt9Z+icWUHcW_^CbWP5I8|{tE zu0ZH_hsl*`ClChGIz$c5ZUxMAB(%%*QT9RyH?9ztC}%`sXye@Tl&(``rZW*3V;WAI zNek)5(;*|J4|Wb^xX;<}Vp4GsPg6B~)f+U2{;b3q8tR~+a=RE*E3#wsYsvcl*X(p? zn1?nrd}1q^KCG)<+99M)X*O4jr3A_qfyi_(fsIJv)zh7e3+|B|flYJ{Q6`Z9{C1)= zDmD;Hl4T4c?h9d|&H*dCn6}afyMQuSSr8z;P=JY1P4vQHs+tw#2$3!*IFB?J%w*!X z^I1@zoA;jWtWw~ZfH%`DPD#k`!-@(mTr;aXNif;a$yjW&vZv@(iV!Ee@Nihp)VWWD z%>*acnC!7sNGw)Zd!aNGRx%imfa^$oAyEtUWEZXniy9+lip7G^5)&}!;}*05KL0@( zj7ZVuxFGC+%Jaj8pA{!B){?fFw-5oOJ7iuBfz0k=0-~!aW-wh@vsv03@nd<`LinhVFL?Rpm!w=?Rq zjcCqNcunbJbJI8K^2sh%0495jsyCimL8lB1I>sl%%3LTgl;ulPo8Y59pDY8SS-|S? zzr4IXeVMo5(zo~UCw96CFDA?i2ZcQsDp56jK-&hvN~^?{;-sHZ4z5;!2{V8={n0L^ z3I^f5F1-$+RGSY`+GZSx@cTYlRf7=Y$AAw-B^ehK>GZI>?&Y;AeC=BGOxP)JNK(JG zGNJ)9Q8*a{N&-&AJZX#(OOMMwK3g<$U2b*lfqL|PJE&{vJ!kyxhiidYXWUK)ZUp8? zs0T(YTJ+3Xn}jkPQM6v?!*rpgs>AhE7q!%JbaZs(3y(usz1~m4`F;t$^XFIEVFXsk z)Jz!;UdmzGCIp|?uC+tGB{|}tC@R);+`$oI=uo`1#F zb~sjt3ZWD3)6AcL`PFtXh7(;-62RNn`(;D6|1z}N>$(0S6n-Y>q^4&nVxxX;Xa{^r$oD2Bs~5E%O< z_0BJ@wu3ONIRv9~pIZL>eOKBcO$;N33Cr!*eL4T4tL+F3Yw(eo?{jjVf8o`34GgQd z+NQlRMY02YvbTEueL)m3^s&r)ZKs2zvgN? z1mxr86Q$ZOL3jQc`|ZF^*%jFRA|dB*Tx|zqC^4RQxNH6UarTQ>+BHBsbUY=iYwdpKO1nCNfh@5}$nE#X z*-u|>2f731Wl7tA-m{;&+79T2BB`70m+3wGhgaI+bqpbL%btgO{dV?~SKEP&TVug2 zt`!}oq0gE3_}EcYumo&C|P z?H~+B#bD*_uj6O`J#>vjBXWb^^{$u9t^1AN(DIppSm!O?$ZrMu1XowkW{gzUIwCNG{$4xKDeI z=USj37#|oNCFJ}h@IV=7AuUOX17%gZEqA$|ind8E)XVkV%Sa-mR&E67>^yVLf7fbyl*E=_n`SGYOT zgz*J$^`HhPB?2;Q82GxW5B%q}|K>-xuW&irS_IzbR<-SW@9^j!{piUz6(4)DormAH zJbJyvHMg3FXtVhr=RCYU_0BuZ!?)RdwgdFjF#mqlfbAdz`PXsiT$y$_FzZt}?Ep2! zyG^@fLi@@X2|?ZKkY9>gL5soSS=C5;UG}*-5%_@wil9s0GE_h5X_q<6=UFaE&yZrA z_hpRXh-X7ebyZ5_5vHqy+Gex51IlfKZ1}Prn{r#>TNX;AiHNssb5N1x^9->;Xy>yS zrYo{&jX}Y|+f6(0=wRA?^JjhR@#W{Q0jAyU2h+{#^DU1JAj1hou=I$xz(p> z?p+c^Zs*+>FT7#_+--0Zh9G1F6L159YA<_oE>|@xo>GieI0`@ z++^*L)bl9iz(<;#r2PNS-kZlcQr7w7eNErp!>J$w)6jr0#7QcZq=F(=C6%L6mC8}6 z9HOz3%27$>s9co<1*fBUaEix*3*HLIx(F+%sDKyB>ar;6;(>}R2(IF>o+$WxDruNb zrF*(+Y=!-Ooj>v#hR^eU-u2w|)aUd5vH}Ubf3tN3O=2qcUZ+ zq}yNxie~+7Bq!xtEtn6$X$jmFqkE(6AcJ%`IzgK}rM-S4n{ZL&KB&y{kq+w(+URJx zQO#B(!3+qEMzw8+%A;+BTb)fA?di-gBzzn7L8c?OB>+ zO>o>B9OyJ3QNh~+cq;%CtY0?CXR=&VRF11g@Ndr?!q0p9EM4YG%6ZhH{NMHC_0V(o z;ST)2O;5a_!UGoS)oB=;YjF)uIdfb390z<fD-T1G>byI`1IewL7Ph} zPuDjR${;$E?8`pyz%zlI#THDWk7Z$0^ys{y|I#qj%KZCmlM@=3Q!&$tTxK~|H)&* z*zR}kK7ILf%fY1|ECm<8xcKUY`xb=xKhM8?es%7qxz*X*W&<;i%t$j!)32L;9?%3t zr@lV*s>!cU<|dw;c-w?&{6EHDKK5+@v-O|C+H2NDBT)TwJJB@de~Qi zO^nkd$MP+s6*v~O0;nPB28k$n$dnEzNKr;nN^?~Q6^$Ix`R3L->w=Mlz1>KI%#bZ7 zNStgI95G2~<}o2|H-%`Bp*G)+J6c|MM{3sNnz;-FBa>@wP{+wYlyXP2Wvut3vZUqK z%{1m3m00gl%Z&gBT+NHeLjv&R><61YrUql zxd3vBx&_3tkQ@@E^Ua9nst!INHwq#s zC&%hq9dJ=ZcQIGSBf@goz{nC6cC>`77>%)_i>h<(GTpLS%jJ9|TIH(IoMtXt=$+R< zm5S(Y5rZY(pUcZeKMbWiHHvTM7=z@uqzZIUvRcz+4v4^1ni;aFxysQ!G_xjw`V`S! z!C)!-5iFPqTj~UFDM=cb_u1vLl zmO)j@a5Zb!#?f5W!H26lPXf1AQn-M}&ZVO5Y{e>BB3Ky7Ie7^O;-%RdSxQ+0G;Avw zAVVT*D{$?u_P!jfpS3knsbe3|Gv3M6KNaWif&wABqjs+TFjI)MKxTP#5!y;_%5~8U?7&Rw<*w~-BwZeK$OxT}w1!xxhgZ6 zHo9*i2xJF~MRJ`?0znN>$x_WGJ;_j=4cmdPEs!yVWfA1Pt6SJi0LnL`j49Iq8R~M5 zR@YZe;c}_ja#k=#?b32cWU6bg0$h~E;_sOB;FX}tzh}~eb3uK7kE92$0QLOck{%>L zWy)fqlatTYAruXYp5y?^<*${A}SJ3oo1h z$^7N>_PHO=T{H*JJ}`UX?7o@%XZFvWF@4|kYk-p9Js?BCo~gU1YE#EeesQui`Milc zCh`-9kKZ<)9$y5|JO7929XmA<%#2^P{k!Wb$nh2`yK;>HAE0AIH%fSU8`~ke7$Y^r zfK`T4RmRk{n#DH7^p5GnUa0}L9inL$1AHM3W>-XXn}g=CyWllfYh1)?*Le0v!CsDlN9_SP~-Qm>>4yMQ5A})j6lov^C9boasHU z5Bp9Huzb*vkgJ7M4)NmgwBSqeo^sbiMpJq4{$sRSa+zpY%9iL>uE_L`?!&%A18fJe zkqI`|AfxR}AeD)_f_A=|ua+~OsLj?w@ z>~O5$uT_FBdr9aP%zjJIRPuHp)M-hhE|lpV*@u0b2G~xfOe9N?J(;l9T+XhW@z>1p zuDMZh@*tt7O*G|5;L}^chTX1FVh=)+#10anK~4zqTC zANG6=ux5zNno>5xY^{=VNpL~&VA~MhkcC(SRMC1%-VT<*z+{N>bT=!*1CPzt_D~g*EP&qrw`lF0ITClhFNR(VcQyD zbtL^|*5p2{hU|TME?StiRv)&dfvt`!6=tp3hiz(r)p32otTp6iVbAeQn*G>pYWAp^yJp(p7U28SmrlQY*ROYda2LC4cIvh%aq5K0dx3^OJn_iHl@s3a zC&q6WKX-g-?Dnw=sB9*>^Y;PIS;UFPbC%jai2eVv1;`goz9P7}!x=JsRc$n`wDYN! zL^!i;ki#+&kXoWTeu+G7OEjB0z+#0Cd-9EN7eCl~+c&ypu@jkYn2T=~$llCIR< zK40F`@^Nglh&cpx)E{}m)@UQ+ZF{uiw?-QoA+-lwH1Vyy#cQ z){@6=jW#kDip3m`vk{_G&NUect&Bv%lE-X`HY7LSA8&l593fq#yOHBqUiLJ~UG4cxfIFS%U2VUoEYP9EVjW#mgifzW(^m6rpzZz=@p}L@s#V3#68f|zk zDC=9Rob_ShT13qHOYurZ5Y^m{+8S+Sy!k=&3C+a2cpxOB?NG3#))OF)+!}3UF4WT< zS3Q=FcnN{@QgW11N41hiY>hTDdbub;*M%lQ`{`D}7sU(em{#)etsq!(gAtc)@UQ6w=Dyer~u349?VytMjPqV?a}79MjIJ#+oR2GjW#ku z{0UDLlbRl9mOu*vC%cR45i+|q+Q{zQZ2LN;nkSKoh2oBcoGFf+YG!nx)dNx1T^6bZ zL5K#c9oauJv!=I1GmVTlx6sZ9B2=s43r8!yP_8AbpLCZFv~J!}2WKNW!@3BP^vl@D zNpb6cAG=GbZD##drKED zoxS+@+#BYsvp?B=)9jVA-kB$6ZkRb2`2OEMU76mq>+8GLcA2OC8)WQrP5yrJy2<$D z{KTgx#EIj_?-{>f+_*cn_@2eU;`qYN3+aWU=D#%mx%t}s$)K7mw7qZv%Xe$Dd^W5o zYVws^ykD81kxAeP`p96U8jf?te4BNO4R!NVV+BP%5vjZ;J$ZwZ*+$rFyIk5R*U4+1x`xH)nx7LBGuM8$)r~B7byH7a3kYxdpPGo;6}z>sZM3XJn2PSMKK&k%W1Wa4dplp+~~LiiHuWh ztX^dq;Nrr0F!f=T!VUsAGVU_I8Z8Ml&{j;4gxpAF2koHAh)bHf%$2HX_7=;JuA*W2;2y} z?crW}5V#R`EQ&jGPJ)$6?O39Nis7i5-AfJvH^OdvxU&udH^Oe)*fh$15V#R`+ruFT zfg53$C1OIPno7|knes$Sf!s*X+71FY!ftyw>p|c~*i~Xa0;DuqH896aBV#Y^zP+Y84bDd5N z4XTwl$-P^njXdi1XlHDVHnR74-K@tKPlucBE*lVC9&zNx`t+^QM%ry#G>YE38=#TJ zz1(QZKt!aS?fSiWuP@Gx^eVhH-00S`t=C9j{||heUwqZfW0QXbfBAp;&l~c+BNgeD zHz&t;He6{~5(!o$H#SoQgh!=U6s@_w0LC-&AAR{bmE6f7v{Ws0vhArzDX^`Q-iXS8 zQfi};USB*62;0?#N?-pD?i&ET!$##`3Voxv8)b$}Pe1qHMs?4Qm=#&ZO@%*PO-iO1 zTZ3EqsOE|0&ZSuM6 zskjSr81Ysa-bZH%d#-?)i*ShJKn4qcMaVh!+GtC}<_(gWY^0Ru&_pKK7Rup-$>#GX zAaf43S5R%jAIhUE@?T=_2xY8ak{j3KIkvs`(ePOBH}hs=-8?$h{|~5N>TrhJO#W*S zo~@pz+b+_Y=IO}dKQd1JolN6<%jkD0c{Vd$v*JLy< zQmO1vH8O38)dq@hI$QU4yP~UD%p~Fv=Piejwvp!i@lq{>TF{`!72nHZ1uK+~TkD5D zTjQ3ZD^8{`77<0nC^v!?x>gi?zCd7~Ig)N+g;uFpuES<0_@ItLfo!2xMAAr?mJrHU zu=22_W^R=B?lTFcPOTc3p>WWhb!eNd>yKW`R7EbGsVARa4o_OuGZYnBF{yX>Y$mvW z-(@F`>nW>c!dIFNQRvIDW;wB*0bQl+D#=PYnabojF`KMpmC?2FMi#R72lXbhxX`n) zFz9f^?9c0s``edoaj=Ztv4ly?T}_)gkM^;(aJPeybB`7IGb;;3#%eoqbXh zj#^-_L476@ ztQ;~<=pEMA81;?o882Ga98D72re|X<(JNgzGP86zn{Rf=SeHdohW^gaux(EnPuH%f znQN36edWd@hqBR+E)@e^!F9RqjpKbF?0u*Q3#%E~a(y+HeX`_*b0n0nnyGH9#_!8} z>xo9bolZeEFA7q2bz>Q;)+bu2scsacE^QBiU{(7;ucxctkVtli@ zbOe!u2`~>TQzn3hIP&$%)@9PRIF_q#jP(d9)_QY*&FQCS#x;bWud~@y$54-AVx>0= zDm@3x2MxjL>y?fw8R`*C%=KnKjmp}sA#Q!W#tmz?jzD&?Hw|i1)@}`xg;>Lw5>PrVL#Tf$!^;9en7Htlc`I<sK8S_l@4z`Xg$Hx!>jy zbwuO~YZt6PqK26K%^p!ln7*)f{(4;+0`)hmOGoVf{)M&ID(wlTiH7j~;r7(J!t03n zuK^+J!3wV->VKWh<}17oMSuzrv>vSR8Z-gcE8S#;*C7^=1G?6O6<&i}zku0NLstfsDMMF--oScgI#+ld zA_ZxncRg6)HOLe^-O_^1dEiI5avsv=!#TJNpS^ZyK9t=I#ZN{a=}N&fGck(&~!*t)UhYX(Hu_Kbu>jVtKhD`D3k28{``g(+RbA`utM|IOLMapq(W`^^%Mj z@_|4t7?c7LkHZ1CB2qS0G8PJ1#%(rNsMU4|_Qc8($c9eJ3>t4myv=OV*QpR*xmL3Y z%@k#cAmOryH|JKib!i{}<^c}zn#mE#nTz3SMYLx)p~V%TPL7W@Z5Dem5us6_=P!_! zYO%gb*})SPSP3m8gOaCii6WwdvZi^vKpHt0FG~(LX^HWHngw5pFen{z*8FX6DlEFO z4q8?DNG%+8fI}|PsT3M=IY&B!;aZYFVM9Quhori-6ml2pSgzYBCkkjRpI$}kDMBD5 zq+@X#qfy-8O3Pxm88kXu-MBgFDPv`aoHJL*xG}!6t$X_T-5WSS9&4)_%SXLjvu>_A z{MI-`CFwQ{IXH`qmr_7gm2jYTIKFCbQ*s!$#4HvGx2vc(Z-eLx zg<4Hch^JOk87v0IR)x;C{aB-0Z#Gozu*Ly=r5>#B!?!Bo`%lBGZnN6BKxa;D;{&snk9jC@{ldMZS| zEC$1E+Q#SeklU59Vs@*5$Kf_6cueu!irXFQkRG0mN%>$Q5NdI06^E%89pDhjNsYWa z9yHhOL|euj4$6iFYc+_=8%jQB%|It@6iAcnFHx&815Q*B*pzow;t5X^q$J41T9HsK zRU~YIc&bS;>1?7}YVbvP1t3yL*$tiarL2RMXrRF3gcXG=uLERV<0 zcs5scm#eO-i}fW;*_zkOva(Ra@zrY7gx4u+xyFFhE6FmS4~eN*D2FuCDT!d?q(2ab zji>=eAY^6R7^Cq=4sZy?GM1=R6$FP|wGv$tE;rE<4fIdjjg~c?G>8_zw~5ufWN@|a z^5clZ%z{)dc+Jv=i#VMx#q*s`-WK*D&PqBX8*Pbzvz0|xw$)b{yWs$bV9Q;#GZxlD z+EPh_vz89ltIep}UBUCl@~2hTVlN76!hLY_bbCL6Yn(Mq(6;fTiwl|X6O2}H=_m6dE(C@|#! zm11hnm`9>x>NxeOyAE)mD7hs%d{Cfc%RvnweCkg5D@fIa`ZJI};P0f$Rt_N(LO@D>#*RIRbgPRF1OcLIZpY zQO`~D0S-P_3q|56FL_a)H7TTAfo9N_b4NY8g*3AL1NgUp(TXeMShQZQgu*rNabEq!Vo? z>glMDq{7j>sxM~GJ;1@+kU@ec3SxrAP&QmM;SN_RZWgU&f}-Lz7s6Ggh>t`o#oVey zQniBBXp8XXl*^GcF+rCtTPZ@UWUbI-Im&|-vV&%%LZPzKOqGD9t_#iA@(Ca~Lyjm| z%w2MTLo8Jeksc@>;kwR}!wGZ)Oqm$?@{V=ff{PjfBOP zEQolsn>A*;U~_Q#Xm=@3*%(Kw5XJ+6gdoZtC(J}v9eEOx3_-{e%d?Ft;!ZU>_B_v( z;-cB(k@AG4)SzNMYp}}WD@@u!HoaJ+)^JI=yfepjR2&xn{Q!r$=rlA8aXUz1W+=-j zXnd{)Be^i*YJpi9je<#8rOj<33$OZ8ZI9CswxFI!wU7Xb2#7`_4;%P+Rxr3AyxI!; zySxQ~5@K$}S#I!IG>~;cJ4{)dzQKH=qIYn>Go{hQ9`LZYLYk~8U!5>QyX2izA-EcM@ELBz|JeTzc z3+bBAm8hY0CY{RJ3%O{eL~-%F%Nb*laM1~}xun|Ol@^`NBwDFP6{L5nNa4Io-4~O` z9N-{@j8QII564W!LWQp;Oe6>4LayLyMmjBm@i;2pltmI> z^H~aOMdWC)EW`o@PlL>rMENu{z;Bjqa9y%E0F zY7ufNmhrSj+2)DCE7^LgSPWqaF_OkuHcL@zouR1>W3pl*!3oG&B0%tR)5|7erbwD# z%TO)m6p}`_RFEmmh-Dp8dNs?^j&j07QLU!c#(_6ZD&!0s(l`o)QMrZ?g5W8j~}-6^H*b z$38x```X?9<=-w}z3f=}>C#)5ki{P^Ubtvj_~ycS3n$IrJzt%F{@m?znYqQ;f16Fr zj?Y{-Lr?#1`kHCyuK(F}#V-5QkEY%@_2S8IO}=LG$yEnTG6v4z&gLv0BOR{d22?7rhPEl3>Es-KH_OFS#+uidx5WoJ)fd+8 z`{Rulp1IL_qp>gF~=IXu9AaO1?7ciq%&chBickK;1ay` z0kGq9H{bW8_y62+>BT2yPbcsPKl}x^ef;b%Uvc>RrPEIS(wi<~d-gssV(66RRI8k_ z8*Owh91zk?PfN5hZ82P}C2Ntq3+F2-58jQX|Ngs$qo}JdJLe4FE!Q4>-q)l5{jTm*__X^^|JBF+?iYRa+)uMTTOYWb zO<`r6H$@73Q9`-}N81}V@sz19xtwe~+o;BCCkA3I1@PEDR6aRE1+k^YSI+8vN zfKSBVz!dh-Q;pJys z;a+^#^o6#!KY-ogD&5ESUfc)P;czqnp8UcQ_a61b$?I3{F8t-O+Vs)A%L`+5N_g3s z#HU_@gqJ|)8T!CFyo(0FrK@52v>QKr_%ZLl>$c2l_R+9!^-T~7 zRsVL?<>$VW`cTZU@O!e>`%|b4BN-k6wKIPv7{jPuzXg#ivLYu)Wjzz&c!R2EgC`j`>#UzQP) z_E`MGAGq-08-H@#i9Z)(-|7$_H#|VFy;J+ZIx;s5fbEvcj`>L0aL>====% z{i|-i^6}$;`;GrfUR33wU-05KSlv$U1M6^m8314Oc+Pfbpz~t!lpkb2a*q3|-N(K81HQBG^qpS(&EZGg za@M(Q@1#Dk4hNS3@V6{4IeY*2|Ldlkjj#B5toQnVx&NNmzr*)=!?%xr@B5B8=kDWP z$o5wHz&gBH2EZ>oKC$oZ_WhrD>%aUwaMbgp!+vvQ_(w?XXIH-X7vFsR6>s_PX|}hg z53Iv4WdQvBWc}LGeV&ItZ#ZxI@I>>K55DYt_fe<+;2ptt-(UOwIo~;z?VZ>M*5QgW z0N#JgP3K?wk%wO%`t{fY*Lfei<%zFeb^B#6x%81gRjzu)C%^mUm$JPR`oKEUbq;{v z^_vC$)XwBX#s`c~9e>{=ALq|}L-h^FgWsw-E<5+Z-~2Sk_KxoZ>+mcY0J9(bm)HN| zv0JW`t~gHE-Tcp6_9PxNI3MZWLA=WLwX2$6xrXh%un(-mpJM<_ot!*+?tlJx4f3;} z?caOJ_#+=JopRxCfBfDT$Tz(GtScY-`bXK`aeZJN&Km>ZqtASTt<3$_e(R%`uO9KK zYqQ7R{jSvAk37Hh(67fn`t?WOy~_4p&l2_4WtQYGvYA;`hJ=Ikpe1!_{H{e9Y2U;x9AzUHhRIEKSaR?|}y!zqsgM z>hdGMx$v0Zz4ZGm$Q7o%gXr+67y$p_J5L%WzA3$Z=4*fGPQ2%_r7u4Ai?@?Uknj1! zYw-9Fj}N?+1=+$BU>yz=17P9bKbQLB1z)VL9LK!$;raYE)lZ*t{e_P>KXUxo6SutY zbC-US1^L1hU>&X!1K`Kc^}J=5=sMjXx&HHb`HSDXE%DB?pL*RHrxz^0d)MmK_Fu6e zW0(T0!!Ke0{9DVPzlW8wKivPJJvX`^d@^*C<+G=K=LQzPaq=k3-l;oY#)6z-3a}0j zhXHUs7P@Zk_y~V$`|h+zyFh;j}njH6KOyBzEAPL?fyV{|37978{7Sg+zU zke$>M#;{Iyqo39GOeYnNN3ANqM?YUP(>*KpHn+}kPMK1A!XuvU`M&4Y_7oLvyB}755A*9_xL0Y#lMl4wqMqoAb&5(* z#`d|n78P!bRpoc-Wm_>+JMHg3wReVUL@uoAcrDv(L~bjBwxDpF|A_LtaKu(jM;4@0 zd#9_I&adit&pRZh^9skAyOrNrj}E3UQZb!d)$!tZNKEGxj=N6o|9(a@(>=Y@6s8+y z`K+EWj&-K%vwYirpHj>%I}l^s+sR`o|9_zfTl2^E|zGvchv{ch~V6zs^&=yKhTY+1+pb-Nnl9@Aqpa zdus0_h3wGouH$N8o$S!=zAaN_cfa-5e^7pZdzfaXdwMGh(+%@}VpYdo!#Y!~^IqBA zZ~f)-`oA}8=6FJHk9v34acl8(&kuI@ZJSZy_H&~Dd#z@MdwM6TT5(ZNu+4BQZYx8j z?C-b!)Z734*j5}zR?ky=C#c5c!m5s|l+DKEwoH}%{nnrSQu%%K16wg2Swc_k9j{_K zzpCSI=8%{w`}?gAv;E)ObufLQis{^{j?11yVyf)#xBj@I{64g(ndzS1ajFqIyQ<^% zNaqMu_V-&K==b{%-mw+OkrBGmdx64nXn)sn5w*_o>HU4%!P(#6zjvHM^?UbkMN&OD z`}%)lCyp()XTLr5B>2nlpEoCZ$0-6HZ*`3883@}a!5mfz+e#77w5b@7j*BKm1Vhg? zPv2h0ebWJDn*;iMC+Qi8n+bSq+$Ys49@~xrrNCdW>DR0SJ-;NM$#PAx0pwg1#X_On z5*nHH|4@{|&e~&9{=JsrxwK-`|F3>Y@E_-$q;&`aW0TRSXn;J^hUcBMKKNDVo2M&z zdTO+|2B&BY_G@1M$92u6jsG;%pl9tt6xQq3qX$y}hxFMDG)E4!K_9Uwq>E2iIH0?+ zl{N~QXwXhm`sxs+OUzcINs(pRR1>Y`HJU_UC19uFT9Hjft!9UhsJ2|46vN21P{z;a zAZv z!WJ>1Kt?Fs!S`81LDG~#E5I*3Oz(?{aFp=n!$=j=q>1+Qqi>>#re|eP%!=uV*xSY_uV@Kvqg}MsqPQ9HLtPqR#0mo@n=TFUs@LwaRU{PaYH^`Lx15#txe~y&vrfeJ z`m!C$X+yDYxk9IMX2J}7$n9K^2@74mVoMl{aZ98KCt|54!CUsFO>LW7;>lno=Pm^t zK*UaIYPHnZu&R++dGvwq*s)LRj_DY#x+nG)LcS*BX1Zg4>xunWA6s;M%iL^YuOC$Z zMl)M|a8=Ljr)%F@cTBCdebTe0J7$JWX61OW=?o>~`C`ww!J{iPbgFku ze}+DPT+cW3otmLW`~S%qs(0^JDOhfD*af3-qe$T2>Gfh0B0!=Ne6a@a68&w5&r}@e%tJ*%0h6cJEhqKL0ip34FA|1FH=#+Ki)bjHFR^UUmHGv`kq zId$FS+2h{;|GZOw)^huoXD42{^7=GD{HHArL6G_oTP$%=w&Qg-#7wXvQ%+otLt@eE=EanN z+PzNP2Qa(w60mn*rZmKMV#@z_8goIntC@n}230`YG=qb(M3!Q5ZGRV)Ne1sw-D*7r z`SU4vzLhoyt+X>IQ)s^qs9%RCWJ$Xtxj-1p$05I%Hd9Jn9V*(d3t(Fci7?>c&T|Q- z(exE@iI2KK{7OQ|6e@k&L>bsZmOSraJHR$=3=i)z=t6=s5cZ~x_i&{s(TG&iIi;x=pK0o? z?ggP?@e-pk9K$hBIN}OX (ncB-jO-jqm~f)S#dc8T6_8MX3)yjfF=1tAIYAh^Jy z*DBbe0*Qt4);4P9N_bJQnPIsRvb20n)RPJ-O}+3;Q@?$yQLWD8<&S%U5z2+S!_TRv zcJ5G9;~MoYwCC;n9Ft=ZdM4pAg*q9IFd*)UXp#8pfTu+ zHF2E5=&r*RvdBtP&p*@DZ`-1&P3qG$?sj{ken*^jKZlySP484wJBb42w!}jwmZBiI z>nrn#sJpmHQ)4n=_t7Bep6JA@Vv1~1e!OESnVT@`>^5p8UpQE(hfHagJKrDGbI&yO z6j+KUXwFX@P{i= z+LBK5A*58HI9JtNmx{3e*+S`mI|vwK@kFN`vUJC<>zBpG!^c z->DM_PWVB3;!4ECqe0g0U?Yuc#O2+jshtfhZ%$Z&CV&?r%tgt-mfIkFJ?jhR3JB*y zx~>|~P6!aKe74`zGtV^j+0%77E7GI`pi81BeW}~_aBjpU7ueLybg%V`+ zc|_TqDWFWX8zf69TLFj_STcek8^Qadde<{eed$(BtNV|Q`@44 zucl%aEA5F};}N1vF%`3a)3w@ZpaNcJqfspxp+d|fl$^ZJUoF^@&5qT>n+OpXU1B@X z@Oa_=wm9`nQ(v-GQ>)(=ow%36Lbx}`JeQh!$KDoQqKgh&;u)7%PIuY?KIU>z(f(U( zXZUT=(IP!omoFu^Y?6qIj%*0G6v9nDQEjkI$pBttV`Ybjs|C%STxEWIe2g9Y&e-nz zcYkd6n|8l?H?sS%T1&EG%&vH3U6zj_{-KWy&NxjW~soU6`x=Jw3~0Ynj8H~YHT z*sO7OcIHPjpPadTCO`A?nPaAZIsKLC_fEH`1Jf^>9@};Qu8-|{)2>(VLUtWC_2|@{ zQ&&z^r#w@8CjT&b@8orpubYfb8YgEbel+pPvG0tJkDDjnGjZVrGx3s%x$$2Ct$|C% zU(vs+7(aas8r)f&HW{2)H39iABW$+8HrNWqn-Scl5bj;s5Vh4G19igPI_l{vZBMjSP6z5N=?QR>g zgX}Pj#7ZuYI!o*P?Zb8m4BNcLculQCHvj2iI~xpJ9qu@%)?JzZ#IT(ehAm>aTvBIX zo&U^`oyh{j=5Sb|NBZrCVLLMno1#RZsMZghzkb*bf?;DV>+c5D?Q-3)oe4H0DT(%s z^v8#X?UXf!b5$@*jrXCAc2>j;o2>#viwU*e+lK7`9*_kgq9g5m>5!cb*qKuWv^!$= z`XM_j0-GTwD|klQxi@SFn%U%~$nePXUNCG2>NOV0K-I5)j`N1>ELIpoY6(tKXDFYq z58GK_$kxCcQFWg6d3o3l$aq-m8KFJWzl~u#GYnbKd?zr{AJt(y2!`NNvFK83`^`6p z>|g|jpfC`092qZ#VLKZPnfMZuh^pJAJZuNr#aIf|@+19GRNI;CT`!HQ^{nP|!*&+f zgrHcWsMf5U7dP5j?BK(r)lq52TWWsRVLJ#mS@IbH^{DME!*(Xv1pAw@Hm|mWhwY5u zgQqXw%8ab1<{>-84x1o?b220I92&N>z(zY!2_|H9y~bfXFwKz|@6OU{yM4oUW*7+1 zg@J~d+U~_8cGeP#wnzHcFl-0GMoZIG35`6*nZtG_*a)Ls4IOE}y~B1!*l2FW>2;-n*|#ye@q&T2Q?&5?TAJHl^e*bXt< zA#a1skDMd+4BOevc9XkO&U)4Lo;Yl0HQS96BXK8@7Q5GqZu)h{w_8097{uXu-$C4%MA)2880)#c8J-Ac-xLF zrGDPShV86on>C&-W=HnHsbM>d*=AvYAY)ODw>)eInQi8dn~_G=)A3HXUT2w83MrB=%i{h zB?{>w2c&0GA3vRV`xUPQ4nsrfNdFoKBnZ4BZRLOjK?#p6=8Ckl(e*Q}Dr5Er@|A+E zwimXW=zVNOpzs1TB$spYF20;x33bhNyRFjk~O;ODvz!>0v^?SUf&VW zHm*mSX2+(T?f>Mo)8k953|N&TpgODuSUd|}fPI%;W!`O`Cm2y}RzvernKgey$3PQe zHE%U9>+ZA*jUW{vhq>)sz~#xeJ;s75;_sGuU#XV$ggB6f#FWN~$RWGX1Fs_pi`%TB z0@{STW=!ZbWRQ9Tg#ty+YsBiM3R*xqlw{l&FjwU~P?hiGd{wO5tXmM+&sIV{xB@bt zSgTbV$F@qa} z@IY0YhVE?$S=<=KHyplrAjolJV2`pE{4H0TgSa6FM53JMY;Yuw!NE*6XNvO8eXIq8 zD-Bzqfkd(h5v6<{t2ce<0Xr^RfVXsQ6lwP|HhU}Pj6hPg-jr%M%uoKyHa{u~f|Ynx#0@OgM?Sqhod3ENQ2wcRF zwbf_;&4?eW|G&{<-7*e`CcTNkUc{I!lX0=omFq;NY-`JYj;4%_4w5k2(%p#3VhX3xOcEP69jJ1wc4kCxIRA0wAyr2~I$VVQ)Y{hv2|YcL5Lp*GXW9y8sA<>m;zlT>u2d0RbHm1mwsrNM@T)wgl)xm6eNj#zAHvF6A$K42C4g zlFsz-KI|7Xz`}J74!VlTGD$>=8LpYhCX7fqR5V*GoZDqa6P8LoA7BWpJI?f+eb~>h zuqf`#ISE!OwPT46Du&g5Agi7Z3%_YUWzi+{9S{tJWRK zaE0kP`mmqV0Bb9dvd>sxD~^Pv;S!{FKH&A^lu>Ax!zQHZk=V40OL3Hw0*!$6Veixc zE4Bz!3~_YS8S92!6)($6bgEkKIK_-L9qRfbW|Rj>&K>V0lbG%K4iatR~ z6^b<~>WBv=yb?w^jxPjovYRdDvRTS+Nw_Tl_T_!p+cm(_c}|FfZ`ocpXB1ho*|-h+pDXEng;$U@2VUe<^Gj22k!IVhRl*?rhgYk<{} zfsz4fHkDa+n+8}N`6n5WWK)6tlm=KG*(Vv0VpD8IW32f&I7!SRFYh8IV|0f&DiPusSkMG9ay{ z0(-LtSRMH$8IV*{fxSrstd8_I3`nV|zA znJ^%UrUHAt23Q?gM;MSoQ-Qrs1FVh|Aq+^MslZ;V0aiz5Nd~0PRA4`>0aiy|Nd_d( zRA4`(0ai!S3Y%2Z&l)&Q&HBLo8yWh$`$tN~WX#X1AhWGb-l(g3UDvX}u$G8NdXG{EY(0A)am zOa=Bz4X`>c^%RGH$HdRacHgu6>fHqp{Xf0@&E@xl*#C2umzN$`x?!oa@@u9_= z7B5(&7LQwaY~d3NZ&(N~tjzyn{@d7aCXu$Q^p}Ucv5mV%7t=+13}Q~0*lxq z==i4wB}zy>z;%fTkU44jl3`Pjuv$tcBp1gssLxSE36U$(s7a{8GraB+Y277KgA#>| zyH)n((qul$S>66zq-v^*1qQaEX&U1_hL|Z9!w_$#kemOp?h+5`F7c!F5*Aaj9SD|S zf5PUHga(2DnSxd`T{SgA_Li;W342@ZM%QgXk8tTOf$J{e9F)ir1_|w=5qAq{zsIO* zK2()!bQo`1TBNm%{U*V}`FEais1NN9Znbxb6~%4NCACxzb>BzM3mnGDotc!RfY2CM(0s zXvc4-!@*>VaCxl;LY~d*E|D9Q;F5JC7Kf?^s0&7S&BofZwThfJx(N=KOPHk-O_dW7 z2MyUw1?!j;51^vxY#6b5=Z7IVg0!a-u86z0W&KcL}@h63BW93+XHQ z<6ch$vDC9sza<=Nw7lIKPaz3Y#qASwzJ}Per#h91qjZ;8AG@sfI@ZT7YwlY3dbVqI zMKTD3mh#C0jhTZcUZ$gL%wzI9b3wX=x`g=jH0*y9dK7?18Rw)Hzcr4{|gS++&YVpQP zm6n)}Iqg=;J*B(Ele$a%c~Bxi`n(lyscb;K6DXaa>G<7h=?7t z358fX<(;`kcZsWYmssx)x}BoR-DI9>hz2E@+4;X$LKgYIND0q@KPt~ zapvP_!jOwY8KG<|W}@jL7L$sS81v*|d8VMdgrK{``mW}KnsA~UNv8uwgUj5Onr+I+ z*WAHC#8-Bv&}4^+WphD1vd=^@oL>AlE%r*_^Ovk_(-S*+X@Lg&sI`MB?6>& zXc3Q=2|CUvJ@qVHj4&Z7UM%P>Am}Zy-cSUUr$j7jw(!P8UBKHo4|S6Xq2>y7!2ykz z!;o0Xa(1FopAHWmTStkI?h--WB?5yI-YgMxwdySx=6yap%@Ae`iH2H|uj9+6nhkHo z?rLT7OhbxKtRJp)lvqDp=_s*#z{=x94M6{|Xy-5{SC`;YCG2ZPT1F#yb+JlSBNf84 zoxH{R|FQSxagS_uo$!-9`__%J2#9j~@&G~;JNts7R8^`{TPjsaWz}m)EvZVXlB(2} z1-U?9z3d7&;D&%WBDeys2qGZkAg?kwFK&z|i;hE!9#mA3Kf z=i~EF_b2E4e&>76@BGeI=l4Cm=vRNoeE(kwz4pQVKNtGD%THdM0leqZ%fKl>HzJub? zE00=7-w=B3;olv8=HR*MzeI5m8Uy45L{ ztVW^4kuH@Q)*;;*iKs`XSbW^eL}DF|bv?(clhsq%mqE^sOB^d4UVa9VfjTuiZ822nke();Cpe-O{}Xm-LGcFHv1bi3z* z@UhcVUxMz_c*_~5=|aOB)Z>#@Uv>wxQ2{Pd)lADu4m%bd2h3WycJj%EM6waXdwNo4 zNVZBB;9R=Q_wiIdYK@0+r9jRoHA~tJKq8uG?*D-=QTNpNWIU;BNGdTFRa@`o@N%}* zHt`}?wQ0)IGp#|5E%M2eZ}cUqO^ECAxq2lnGnty&*G5B}LwaxwuR~fKXY(RD)Xg~A zwhuqyOYjX#m7T0Xz_Xbf$+S}GbWtGV(VB&1vy{|}^PHKiGj$Z&y}Fc0*iKxZ7Tc99 zUYeNDxZ({cTVYDIa;ci`TTXRg(rBwQ8SiZQ5|uiwXK7DUy-0-C+c|KsFk_0T7>d~h zmv>r<+nW?fPhyKl{`MnT&P7_ay6dsYVY`sAc^oYe%{U*=H)ER9kB)|#>5fYIb|f19 zF~1Bh$u@P(phQasG~~mCDAES4M!;PZTPn*GVU7V^KU&F8tNZ@KGRe|?Ck;{gNOCk| zJlTY};$Yw)a+B5NSw4;-kX)NoBk`Je;!o$v(s(wB%5?;61M!?ZY2hRd4kd`Bh^MCA zUM8wHg~qgz#QW9#|LvnHCdgW;iji&Ah#O@o;=Fg)mpjeVR0#uoD|Zrb~WA=djy-y zWa@S?ixpxhBPdg+M5VZ9K5xT(1U-6_FF|0S7DHYuZlW5z6Co0p*rP*}K62=7T zhPJPjC(Bu4 z8Z#n7^YFDkC}^ZobVOof^{$;k(O6%Om&z3kQS*2diI6dBSk=5~*@d)|-(5=33=Qa) z0D(q3(=O+-y=I0*ba1wjC84}E9<&4ypGI{@5VY&>aFV%Uf~@2ViLq7Sd;OZM8-_-h zPFuvvLdP>}>ZI6>nwX7hhi`oRb|QS1%$>X11$g^XspHSyEWtnTO6GF)q>9ZSg_|{7 z6cugE;@mhxN7I5aB1h<~UW7&yOlwH!!H%!$Jm-L4#-I2V%;d9q-Leg$o^2-UO)Axe zlSrqEC?j#IOlwZ6R#&BTr__svAML9;&u{Ll`t;2bKIL0BRYt%A3U!8qSRqT;5s4x5 zEtsxoYMf^9EUeK=y`n~yX_fRfo#*xTi}+JN;XDhqpYX|>CH%3UaGp%rm-ugf#S_uI zKJ62IMoq>ziLBUiy(;zFGEU-VikP7)iz=iVO=89U`xX+3f!No%?5LI(I*w7)*$n6@ zY^_$Rqs9=|9WT=zREDOL6o`}e`egw21Z%=;P)R75;4Pzitz)+1)dt1PvQiOZ3&UDj zZ6LCiGD4sC;YQc`!teQoPmKoGmGfAEZP7Ep&XwXFy_!&leUy>%TnBb$BfK{(*Hm%m zCzcYrfXnG|v&$6w6@zL<=!jEncB9n{)}Ca^2v+K6L(J^wH< zVKZd5(?zFQtzbI69M=+c)QDQWGUxI!q6+8Ca#yJ`>7ugj&zp%+dgSC`g0IIE!m)K{ z4EE;<-idN`DqqtwBvIh=(aOL>lD5AP$m3UzpV!H z1VfmzWj64#j;H%(D;Li&TuS$hC<0M&F&l$Z$q~vb$6a4Sw3M6S|F*$` zgME&-agE~Aavh!%R9FVUUAeOl{x zFv)Nv!6*(rua^dEaouee2B;TqvN9VpWz zGD3D_N2X%E?izc4<(I)Fp=MpT1fc>~oyceyaV^Cu_X~_W^DG!`X7GLi(mN@-c;Fvc zCW_@;4{D6_a=qQ5lvXXzX|&-+8V!;yCe1E6!9+J3Z;%ma@1uShBx#SwWpPBZTr}Fz zvPcpz_CgJ4%G%Sh0adgf*S1~05YrBx>q`)LyvR(Wow%ieF-YQ)O}?9oMJlowqo;hUd zV+|RNbTzR`N$Fa(m1&C3xE-%FL#8hQDY1l}Z0L=S&P^GF65D#F(dr9%$?S|fB*0cF zC)-t~UEKM^*KEhaXE#g+fI~HP{A=sKFJ|Ct>%Yfx+0^m-)_w=fhUwJtdl$oZVtp$A zZ69hDu+&Sr7!>11;3jm|$&V96e{6Lns@9#hIS6hJB*25=_Kon9eO2o-_HX#AGKNYc zM1iBCrTm0#JJ_hi%TT?Lt(UPl9+Ub^( zb4HfyVfkXKqyjZktLH{YMK@(^<|;@2%$&HP1s5&lA6zBkc2eu6>eH%*BWBux>wP7u zRi=cJFzk9tq#)ihBNKMqgu+LDJ>mnVAkfy;5(+1h&^0nDg14SZO|zbiDkFE;kD-<) zrhq+@htKpSVp7&%l7$&k8)-EO0#~uA!i1{l67JL(cBZqm+2sT-VilX)`27C=c9_~a z`s>}#f`9(~@84VCc3S{2xqbbd!{wEL$!!C!gInKcdH&swp~=H`PXeqzfN~mehg|fU z`h^@W8(fN7Fh*?7bAA-dW~0$nju;E9H;mz&!Of9vr#DpQ z8E$-HeNMV`o=GVdg!#ql3?*aGmRw1G_FVH^?d?*$WTWvphvS7$Ef17B*G(k5m5G|W zid-Fwd>kua^$|QoOspcsnQIwtkTa@8mbh(-7e?Z0y}W+a$LxyT(=GA_!iB0pa7k zQE%}W`n>9Z;^pPX_kG>mFJJrA&?W>J8~f$W9z#!F#I3Z!t-^($ZR(es61S~!H*Es& z0>At>!Th%7-_O1)JGWwB>LoB7M;9z?=Lyv=BwzDsu~ul>zRZ>N{D&onTVPtU$KBAb zOSsFV3wY?xam9gZE%RMOFE{t58;Rh~vBf>a2{-pSTej@;^|)blo6k+R;I;ba3})bc ziJQ{Jwpw~M9!Do4Gffm{9yqbQR@28`da5Qdx2IcI%MFO?Wmw*e-u7T7)MmA-9I7%f zu8KrIpZ5guDw7{)Ge)k{njm>&Qekq67VCP|oJqqI1kHA4PED9K4Hrs@Ru8rzAtrJY zcQ~-&K81GKfP7WUs#~(DcSPB4Kgh>>_a=-oVA$M#`O$q(S`3Fz32h>J$A-hiMZ@6+ zx1k@I;cydx|2l`mn>qB2ZolTRCAS46CuE-k3!v&5>T+E-LmQzjE0*y z>X-Td+^x&qOTT@IKmDuIdrrOxzU@Eu_$|lJI(oxV_)tCg=s^#BxwrOr_g=U6%-vt! zg?8S#^EKNq2!A$Aggz50Z~gg}yWy5`aNogxn9H5YTcH3k%NI}EXB$K^_wU;WVwn1RDbl>hA&b;ygVxu2y zY_PR+`{a#S&x6Yk9Ks@@g)b*vw z*#?2?>h%|ZBCpMh3=pfnEb`0GYuINS1g{USeyvb`20G|6pVL^TP%~ z^TPEBDCQ~iVgkg?FN>Kki#G|OfAQn4k9|u93ZuWsk{d+UpKTB>50H5eA`1|Se-W|` zN;}&i;vOJ#A4C=)nExVV8(B)egjT|!F3Z9 zwHWFH&=M?*S{>>)AS>u!H$Zudp*{d#!Lqy?hWZUC3_u4Sg3=ZpH~@{ovb2Zqz-vSO z2HXeI^}!s@d<+gifAC;q`$PQ(TnO%UeO}BpP)q+-#qzI17J`uC^a$={w>$6q`i z9)13(fB3**_u$VCg#AzLf5+Y*@3nUSaJRPe@tyMa$F`a9Z->dyheH?;euV#Cc;E3Z z;4(cp+koNiYy;Z1TQiEi@V+A-!3J8-kl+k&K@c1+5d_z|$q`Br1P4BX4Gv~DIadjS zVBbfu!D-DVCn-S??D+^bIG@?%kR=F$T_3?$=rHBhOj<9zZ^uXQ6*@Etf?(T6@D(~7 z34$Q(BiQ5+WP?M8AP7P}f(^88asUwo!IqC;gJX?N&LaXKxcJVoaEADTkGYZ zW`#&AErKc)N}yYwZEzBCYc8`FUZ461Hqg4s`9=@~6Cc4>=o}*mg0YWalkub;C!n!ENO`dxP?Y*M~lWuh6-}tuHOj z_y6&&mv3Eu=VjsYqc45vl6&cCr+;wz($n0@=T3h3gg*YKUk&%e zPYit|Gz&d*>l2`mhy7#XJKVuNw?-WF8#xnO=#v%1n^C3!NTOUXZ@|28F3My6+3hC! zqUU2&=J}ZDLSI8MgrWlX88Kfbhvf#Hzg>OT7W&47S`@n6tSOL&z)It3FQC5j(JBjk z-?-2xR;pZKU=nr(fuR~-!i|9X&c~}R^hFl>DllqVItX&ndl*ycGJ{z_edl9P7y6#H z&__+wg2)SKec&hs#*~anKz*wRpUl;TK762wP*E%P>t&VClSO+PP+x2%U-6kh-?v>U z6}4WCcij>y_b{m8RGTaLF77Y6)$US!#zGxz*)~cSg^pdB@t8?82Eo*=TDSNO3w7mz zq#}LKaBK8nO5puYF_^mZk-~Fh6ra9OhfdKB-$M102IzSx0c`~9EVY#d8sP&+h*|CWJAm6F+fzV~vyyEK@>c&H>$IcoWQ5;A-V~|uen7TEu__~F|++{I(0#8F*UfOlRX_lZOIDMQ4(t%<V>+oH|$by#j5IHP0=*uo*qoy+GzY$3w4ZIS4wm5zIWnRso5%A>i^tU8hc#?4C=On3C70>S(_dtg>s}_|ifhoQEp3Dv4B= z8BY)%sVKqJt##wmg}TYuAWAG>7?6a{H#*t`38rqX8=oxH^_w%RC-vo?IvBICJEnt; zck#8#9xv1}E!>1hE}rj_P`}q17qmd=;%n%R7V6vyH62)ZOKy~#NNX%>!RCti+Gu>Z z7y@jzUMmcHX1~{~bLMo+1heeAx`PF}LJvI6RV{iBRjC_PS@Fh!DjQq#iv5MUnTpt8 z${EWwjHGF13+(A;KkxFRF1;Uc z{@r!@vC|iwMo&I<^6Hc4f|~$uK4y=%j^1(fyrV}QzW>lXyzAg&2QNB^f;$1P-hb}i z7x&)0$L?+IzGL@!yN}v=|BktH7q}hpqU~t-Q{h*KpBwsO=*=NEv;~T|BY%ttTJxSO zGHnB1kfDVe`WRspK!_8pHM6UO3?nS`5wuXPwMt!<9d}w~iz0(9mt)!0A(iPY^a0{) zz<7lzc8MKpfFcDV0^(a8wV3BE^a(@W1v5EB@~j{>TTVIHY#Ga}PUcK|p${$yBsw6= zHL{?$FivR6!SH2P2O8!(7W$mL&C)hiqFI@B(0X}1yj^|wEc7*sOj+yNBG)MQl_p?n z4``3f>IBSu`$FHi>Kc-#*aL_hQDhZHxPbW9`pdTg_q*rH43ijntOFvnUJ;%Qn-&vL z-|95Nv=;g(8P@Anu17bk20>FAA8gqiTiYZs&4s>tZPu^KEN;z^es{_X11cE4^y-F< zX)N^VP=Oblgj<9gcCB1MN&)-Jy1x2CUv)|w;4EZT!^Tr_09R%8cJb8~_8mGhG(jhD zf$Vk)gy|x|_6f1IT@h1V=<7LDzJvh!G7YPueBRN6)^~Na%kT?*qsfSAQj(*1B~DTY ziXCj@5nJ0AF_neBqCOjdqp3=()Ptu`OQM3-cg;V_3w`yz&Le<>5hJzE5OFjt*rqbJ zw$Wm^g}x4qc$G%qX;o2HXA8I&Z2cTt+i5ZELLX7=i8Dn*_`cj1hJrk01NN7-@wBwi zhZ8MchLMS97AvMWD%67Q++yi9|7HC8j&Vq(T~zCMd;vnlYQXs@wKhH%7y3#g)~xBs zctA-|RUcc8fb&OcZG5H|`bKW21(+6xGl8kAW2n>&IKHIT`U|ztN4oZ8B%_GalRB(k zAE{qaLI{B{NoK6dTNeXC>8%QDNdtZiF=qvOp6Vj$yDp$i{&8o2h z=cD9W`{4_HP`__6dRWqlw-0DujI|Mz64mcjHZT1;-p|3%V1~UQ^G#s#(6JTExaPga1Yrp(LpI$7( zn$RTk9Ii-k3&zM`^=TId6e zZS`hpAQpO1h0$0f=z48!@5(%Tp>JRpyA_EhAWzdP6=*01{0bObvu|#puK<`ng<8Wl zYrI*~J2)J0zYtrqZ+4+irmWJaSLz6)SX2nIKMvUcVr%1lW}(lk4JwkTk6B{W%nQRo z(D8X)U)slat9Rfp{%RLn*Oyx8yX8ymf%PSQeA7y$243gT%O;#x#uO3k)ae3ziG{u{ z+2~bGK`808fY%4CVh8NctM-`h|93*K+d6&B;ScXdz<>Vz?~!i-z@&HggG_oG+z`6; zotqUVy_+$1rfl;PvoKdW)(9vXqzh@4GVh%kLFKclI`3K)=jh zYV&JPN_M@3mKp;PAkihxqU2${%ERN`U`=1xK4`Jb6=c zrxH9fb6Phc=dM)}Ret_5YvyXT>cP{(Sa4Azjnx>HXOfT%*pvWFyQd60j%&#_Xx8W7 zeIxD0;wM>(r&6)pvpemQ1R3C`yC+d_h3@Lu!!upmExE=l0~vHJ+p!01*N6kw_iF@S zVXIdau~d`?m8(|)hq}yiOpZy$U5o+OZ_Jq697#C`Q!5VG{&o|Ed?0eCf1^NMNc&fdHYIND_XxueZJ5B88R`^UH4y~z#U#WcS%dicT7kI=Z`7Y<&KHRnBK z9`HT~BK2&*NjP1){N0_QFgl=LSl*F-X_J;|a7^C8Q!Y2wBUO^FLwTH|%X za&4+=XM6o@p;|u&FyCGUE6smeaSrgP z6JYIngq&lsSoQ!pYR{*HPR?;G5E+}Th|l}PSC~&Vo5|gDNgHcX>U21cGs<{KPLLGJ zE0cr}PsfP-te$Jo{3KUU%tpzngU$u2dOMTMbW-aKGo;Kw4cRWv@NROF0k0%YyoLtl zl-5Mxhn1?3iFWfB*06w_dh&^1z7{{=uE6h9A5Cf#XkI{?6ljwqADhM@QAe-#siI{MG@w|D5d~ z-Fx?5YWJPHk)5}1f8C|OzvP^LG4z_y>8XB4Hq0*$@6BWQ!)M?Tq`C}fYU_hpykkd` zkwil4k!+U|6Rs-vdX}iWnG$Y~;>1}Be5*(5P*jL#dP9rtbXB5fx{Xws^$d(OX(Bxa z9Fen3W)QtnEt_ZE>u~NUD^SA!S%{tfK1~HXl`)l<6Q-w+v?m5au_|T57gB(yi}z$!V?WvG!R`D!}{UW_AXf=gzE6y!FqbhIIOtZULmRGs#)rmC$T z{qBG3moactQl~RjylK{=ros?rp~}fA(D9)PIm>~k>L4XksZHzU5_Fd39q?<{>EXg~ zS}3)_!*W_FS)YQJ#EL^1#j=TBlt8-$&r_~AtcDawm>P_#4bmuE!`07&NBlz=yy1YE zbh9xXDNWk>=?t67>eL=-1;*yMZZekcL0(013i0Y$HJ(=6SqpaiN;_K{dChVw5i6Bi zQv=IsgXuIs8ssUlrGjUEuhdOmbr`g!T2-McAyRR*9gZINWenvO)XGM?kY|Ehi~ZDi zk}EZ)avM(Sl{y}2Qwd_4ciMO-8*iSqA~~+zOd~Th)`g~(frQr@#aXX!MF?k7jMQ+b zL^xgvJX8){={em#-B8g^sZ7$E(5}ARLfRz%*#!udS`rwl4g%As5I?P60XD|s$;|T%%pDiW61$Sx(%8| zS>df&j0UgoNl8HR;ih3$NGha;l!xSS255r&Q0&wT_*w#e z$MPMMVBr#YxpJjH?Ed0I7-+P@*A2pHFho?P2DDz8;{7Cy)rS(3sDej=yZA(kwP!i) zER~of;7PMK=2emIB9zo^DM>yf7a40dMYTyw7U~gpFmxK(D|WXbc#v4ClIpB5uA^pa z1;h5+AHt9+OKpy}xCqO2P_S){6&kU8q(7xSJKHSOa;#bAN5rr_CC(ajf{-)Ah}iTn z6-Tp?c)sn`9gjqbN(%4v5{UwY322_HSFbe04mD}ntS7Wo2!skqtDlzlf9D|#)T{?_ z1zgG-&7R4bY$r3a`Wjtz)5YG95NQ}^*eX^_l#DYw&Oq@JC8#5F!pc(^!7^wgTaz1& z)=X5pJ-s^M<82%_Ys!^dey$8XG)9#P0Uk4nnMuo5Np4Nb1(KT>38g?um1N#3r-}#| zlj8#CHP76MGGb`{%Dlu4YJ>} zn1Utiqn_E7%fneET1b!$IR@UiGp=YR&(`xT2^NF~MH|D`>eaFxW4+$2er%m(QwT)aBT_677T(wUBC zh$?HD(j-R}%h}v$tQ5wRDc1J%Sq23M+XE0N%1~Q|LBY}_h=NV!JR%CIna&@taG%DJ>RVJb~jaZDfljY#I^e$}OYnLg6oC;M_#A*yvYf|*f*BI{u9h>k?7%hw6$ z4Daw#)rqE=N;(-$a&ozb%R}(MDvcxycCO$?*jWMR#k`}9uNbpotAm&&#%1yyx?^dp zx23|LdkBN0Id;8Emxe>R1OiZDK*ph(9Ax)kqlQ&#Q9hkv?3S+fjI$P*5*mmyEjkGV z(UV0TfwgMVFwF{t)o3g$WTD|qA7YA|y#nr66{%*vL6-Fj)#sSCV8uP{%NWWsgENjI z<>@i0Df7o6Gt{6l6OdM+qc?F3pK{%X$Ho(G_Dm`i>K$*=&C!rIY4>K1ZKSGEYM>-D zdd?grI-nyBbJLX7E?$9+=}fOPTA3Y+X1));G_SbD{+~XCp)(w6#k4_M^teKG68)4^ z?80ant9dwBo>t=3Zq{SRST7|AyO}QBb_0dnbycMGy_2ttpRu^$wCK3C|Th# zJt6gN`^p%x^L+|-+^S#=T1wwuy=ivxzK1ZhM0bdYgI+HVE_0X45ut}Sh>U?GI#jL8s;-bw@G#;9`z!u)bgNLzUDpH>oGp?G6N(BR( zwVj^l+7>zGgn?RSXK}YEb5~qpz)Ktksg~LTfs^!FMpC}RMDRvS(RHI;Q72JqoH6s|m>Vyb zGs++jF0Nh~gYF7L{nBh$nocZQR9A0-9DZ^B{r|~Zovq6+y!3xAz2Njqr_Vq6hm+~? z-ye^U{_e;<{M$qO;I9ux`(NBQ_P($;*!}#jw)452-u46AO8CEr<@U0sI*nlZ2hzY)B0bm2AciIHc z*%-hbHo-S<3}6GQtRN=%rUifvl-_9*JbPmRci05EjR9n_{NO^++h<$HU@BqP4KJ*fDNt~-|6Z2>c#->unC^IF@QU4 zf@drMY@qbcn&2oL-=elYzIFMdm)~%?zpsVfu>T{MvzPZSef-i}02jcwTsk@Z#Od4i z%loQSki-^4`;S|2Dj}`@!8; z?|#Sbvv&Sr=fm3{-Ff|v6x!RNcH%ov*xA~C!*+i=yS*3wi|~iTuMbNAY=FPmwj4f7 z0{WX_M)EZ9jVC(Dlynz-4Qneym+F{PUQa~D(y*B;R&v?s$^Y^trVVfGpc7Pv$Al)q zDI8biJSR4_{@CuZJ*`=im~oNFqyaaAF9D5I@OfKeWA(0`LD5)Wj+e?63{mrV6p4^A z5Ff62)3OU`C;!!#Xv~~grf)}6tSJ^VBTgQ~;ZA+(#&Q{yD61ZlmU_cfya^XiKDdxb z48*?9WkXfDG(>W=9j_JeJ2f3`ABj!V?5b} zxZ+^oAaaw{--k%yLzp}LzdP4vs{Ck#1papzuIunh_iWNr0D-|Y@;K8SH59u`|*mBGw-HDQV z($2xP5>1?b=i|2%;jPGZo(-3;MGn(V01>SZxVIr_LS;R)aqZZ9H^3%HHrLZ&=O!s8L5 zAmv=5l+}eoT=dj&ugRiYf4Z!BC#zQTXsgmAsIG!d?ml&sQTFip64tkV&g8EEfSE$?)d>S1l!Y2zcRWqD-=#oWZAt6)e}M(v&n7 zp`Iz2?Pf-(BrEm0acKDxBa4>GIi#zgNmK4MX1yFTkTXim=qDxxq=M^Y3ia@=R}%Ms z)0Y_bqg7Yw)^Nni@|e~yO`@!eqZB%jb%wD8svVOgJUTO!{U7ipbhW7P)xtn^DP|BU zjZ#v(Z%l?MYLMxA#hKceB-t3aF*F!&Ki8MgYB>X`Oi7+Y-MBKN_-JM*a5xria`C#{ zD~zbQVVY&Z4lgC@o*JKwCv^=;CB~v^>)jk)&bHbnUgWAaO<8)THK?&gK6!G@FQctd z$y&1AHmE|$uBvj&YM3Cdz5!K?(j?xqY88xaYa=0IpF|cN?uLkMzbAgyPdFdS=qG%} zPdFbl=_h>pW)h#eS;C**Ea6Z5g!6H&ei@%!R4}325!}R-VjZRwv`J>$q+IP5nh`qD z=|vgGqqT;VNLlrI@p$G-4CD!hFlEbZ;AI_8_sv!=o?*C@?io=8e6F*ubRp{#PO z_>EXh04j$nMz&QWZj_~n+fLD#;byAc7$&JhwAvgb`^kKwXCFV+mk`A+)b+r(-@cgy z7jdMRk+N%PhRKe`MUf%mNJL{CaJg>G9sQ0kK_@4LbgW&Ccf}sT<}#VOUCd&I7|ICB z)G1LZu9?r@}^j69G!F`4S zh%cd()mqvCZ@o{u?trQHg=t;UX-LIgGe6U#6xNJ*Si+bUqr0E+CAxI7Mj^HQz=-sd zN}Mw?4DQL(!7O4n3e}X+wM4Ixi*RN!v>X6LaB<9TLtvI3*W;d0=Zu2VgCsadM6ytp zAE%SUK5ACwS@j_1mm#KFSpotvL{W{m8&RpAWs$60(%62a?ZyY9P-2BB4BRlf_nzps z90E%blR_e04cwIj_twxFM066>iqS)uWTiXJ!x@gmxCjXg9=P7MSi7{wgb6;r*)|JU zBgaydO1Duv)#r!t!C;u3w#>BLDkyjcgTl*dv^t53Qsld0No3UqJH&HPoMscYP|;zW zz+IWGVRg1!?^O4m;Fr;e;XOSmGbCH33ve!7=KJ{EF5_WbDUdTt&61#Y@qVJY|A)Rr zwFz-uK3A`#WhPTo``T!Tb4U-4;dMxh<7{3;hq@Uj+xFpq^CkF(rOHm$AmG`|jbvJ> zbh;>z@o3FLvRO*%#d*$5)|omA?M9Xo3EKg^q}Z-x@zTVE#uaZs*$Pvtl}puh-*T!0 zlSW&e$#^H^OH}H#o&{GCRWA~u^>$8!$Qe^i#Zb&9xV+O++}@-B?m)7|qq}^GaxT)U z)m@KG4%>x{&Esf+XvV=u@n%eO`q9x)Gu=@s-;PAXZ}la(B-_+Ag96u?`*wMl5W(d} zT8%(@#g@u4MVMnKEuoeCw7S3VOVG6%$28cOH0h^0Kz4|YiY{mM-L59F0=NKGNGFU@ zt(A)&GQI?XMddnR87t&%mlN;-mDb85i#5?$lr}_phD&V>EEvaTHT>Pa1U{Y6MUz6+ zk|==tZduHjC6f(#)VGs7H=fBY$;@OJFAHY7zgkMraHE-(^HEh$vnhfpbdnjlVR4D> zL}HbSuBF8mZE*22x^v)nU9|1N6P0P@(rh|q31b3v#JrGe$CBC1xCL>Ru61>T$Ymhe zF9P_#7bgG%+b~_Rr;{`#YK^XwozP~h9~IS}EN6*n%!mlh!|(GY^2($kz)WUVbcvFb z(n~Qq*I^ncD&aKZ!EnwXdq!Mrq1o^YdC&T3Kx%vX(MJejiCpjRx11^H_mx(X&J*mEs+} znoz(f$Vhpv13R-3-W!%{s<`vEMHz`nNf>!!TXm<2I9F=S1~rS1vnk%Plqdwx2BbQS zQSq!wYx|#AN)*QxG^IF%n4)MgZo9PD)4OoB0z95jGx3s9gwnBmTq^GVh%ezWmMsV* zU(qKbn?(n`7#QZnvVlz*2LkkO;|$ubCQ)~Q?)|ziVKZd5(?zFQtzbI69M=+c)QDQW zGUxI!q6+8Cau={9q>IY7KfWb~bhgk=MnsyCz-}xN<*G8AZi+0LQW+szq>EJ%T!txS zlH&HW{W1oFvdhmR(_XTdPht&~8=``y6va4}EH?;YFq@|2sZxlbO8D)*M9=1_n(FG4 zdaqUjOn{`71;I0zMI%vg^Gu8)*-`~drVy;S_Y1y+(uk%}wNH>7*@nT*C8ApmF0{3aH-@zoqnO`m$dR{M`92mOWEDTU_GHvK?xib+{ z;?5WR`2YL2PF{8x+HHsKe*`uy|6b(QORD!hZ60y-s$A%7gD*3;KI&hIIGTsk*bayi z8ng`yM5_0O3J63?#uMuiM|W=$;XDuZ*(6kIk$;1e--`qOH~4_^FCI#Ge*Hh^rCJ0- zos0AT=Y$d_=E@#6lyDw7dyClIn-LUT1pGEb36sfadLGnxVJKk+D?@_X?YdW+S{F6D zaXh6t?A6&-bP}yqVA;X}Nw0!CgR*$r;ZPQk2^7)Q-P2`wKb?TwYv}IMzUbB~%E4G0k;~=4gi>ib0 zs+WBIeNUY`_A9B-oqfk?#j$U~oEi%9ct z3rgZ~&-swY%&mQQC4e0W+^&Zv61jPK7kVh+feYYRtkcYD3CELwFEduE2v&kLQYV~| z-ZSAwcaR$v;p%OBXsd9|(Z#6~8z@3oNnnDVCP;&%r>s(JRf<<KOr zs-#V@t3sj{L`|{WHEqx*CWK>S*VJs3n_Z>Mq^o-x zJv8=j(nB|$yH^60FW|d)WSsQ^ARw~eRy%+JW6`M}WV=Y;+AHVd&F%VOJe5h^%nv06 zl>}szi+UM#n9SgvRulxt6k#5#jL~Kv$7rz07A7!5^z)Cuc=aFhI6TG_o_3#Hj#6stEnZvwsyF-eaS2DbI==l8I_0XZOH* z6ntY*B`S|CI!Y%PGOrb7Tezwm_X%eHs7%9pM-4?!R^aW&6E- zX8-E`?%r>1KQ{dPdq1-`+~f9Qdym`wo88~p{hr;?F2DP%-OD?FarrMV|K#OYT^?L6 zU%qzv@X|*v{mi9TUwZzf@3=%F=NZ(&-PMPEO0xUpo5pqqiQt?5KCd99=!yJ^anXw;#Up&^T-yKH=a? z2Y-C-lfo*=#%)a+l)P?TI^2+6^ZFsNY+{R7Kf>+7S?Z#;1~d<|rI|1!&$hj2Ue!6)yAb0F#0mq{z6WuXr&>-WB8`VB4f z{$>4sZJGG+U162SH1z9B-S4?b_pd!2^pk7h(2H)=ZFPncV0e^VNq+X6ZZULyspN$h zDS7cy$-TEw@}i}Z7hI&|h0Db6yeRSBWfMIA#u9Eq`m|=^7c5OYTZ&(7;_qCVc)HAW zfr+2LH1TAa<(!FUOB0WmNvkHFF6-wl({C{GWLZCVnYf{e$4hT_9xQu7cNFrrww8KF zf%LBVyS;2Tb18mtyIIS2GnTn7Xt&X_-G<97=i1F&w%cHtwAyaQvfcD$`VH+iT-Hxp zCO&Mttvy^D8i3J%*>vxHuz0`e-nT6ES1&5{z01URUzGTsC8S>o0x7oEReyEa2fJpBpIaK^ zrZn}t?;8u@r$kSV@oT%d70}1E4*cCg*Pp;oU_7@F0Js!Wzwn@-n^{ek1W$~ zu)>>`^?SoI@!{*YW`#E{t?CB9-=t(PtnAHG^^W_$V4Y%f`gUu?FQ zEzNfSGS>xWd+E|_-@VLo&TKDPn(e!mNvmeNe_6l#mgzT`?Yo!tyS_|(`1-Ax?Yov{ zd+~#1`)0fEXvf)_hwz6_L!Z3-z~zsH{-5plY`~?c=$Qtr-t0nJ=-6@{I<(4y4(oAAY8p117`rAzVz-(uesE_1YLT<>0h6IMFm^_EErmhl_GWmDnLt^W?bSEj;$x%PV^6HlkY4=jE^{}Rfg9^t=O z{GK_-G2;LJ-xv6x5-N>q6pKlcH4-ScwE9F(__G1?eP&V4`Im+Ka{lBdzkkwiA9sX< zYuMd7QWf-yHR1Z_fX+`3!oL$R-*5XJ;=E7!hJ3=!5+3g-oZo5q5|6!E!eefh@Mu5b z1}n~Q=x=}Z%@V%KZ&nx=x>ZTT5mK{@cBksBRDb&${pt@LsSB}PaM@fc8Req1$`P_1 zS>%JM7HJ!R*t$pa`6eUktGq7TP{4dJIFftpzuE(-M+(@Y@lYC8;7+kZ6dA3LRouZU zuhsTwkq@Dmu0v5sr-an1Q*eWDl|OUa^z%W5P8m>xT85f0!?sml)7SNl#xhfCG#HD3 z6N?ZESZnoh+U-)ne9R&r+LOyp2f6t#hcb>^*Nrbh^CkRzMz4ihkTf;nYOzIi@P?*VpS$&2tj0Vh?510>Gr{WVC>FMC z8>Ndv$F9tH%mmNUu5>;;Shhm2Y*?^te#e7NK-nn|>^jyWM!cwytIx}aKE1HSt@C{< zV7@=~^WBO`F7$_fzFWOxx%E8(^SvryzE=j!_X7sZG??5L_XmPcK+XRX*a)bR6( zTC1yaiX~2aMHPjh)q!g3F9YU#AYi_~@blg39qz5)2$=7K0rP#p?|ylxLaUNUb(!%5 z;gQNJQ{dwl>L3I12E%btQ=2en3y8h8h8F_n>jcdAJU`#9o?G2|O~8Dw4w&zI{d`Sq zWDO=`M8`N?D->K|8YiDQskGFqe`<2_$ z_H%arFK}nz3tJCd{=>`fxct(~&jTC+kGk~0rT1U@0l*@FU%Kn`?@xdC^e0cRpSDgD zrzagE@2Ck(e$UD}`=wx||Lfd(EL@$ao&(H7%7)-^n=b} zTGGEGsp*z|1jU6z7|~CDeUOh@P8iQ5UCCH0 zVQLlKrQxSx;-@3#`^kv;emux$bvIkRiZfH5@-<7raCkU^6EBXKZ}7Zi3p<=;igA&N zL<+@Nq!maF1sC*vuxIlG+NOZt7}bZ`(}8rhG2AK1BSy@3_#mG-Q_xMP3SC!cs%ADv zBEv%h@hiw@EtqYUT(8z@=}gUbK$jkVejsN@%y-rxUn(5#_8b`#p0dRn zYYz_|Xyu6c2Ep07q`egbd!De!E^Duv$Xe$euYkxA^Pv&*5rcfrY`mAw8v?mx zE>?;qyTgM5ar21zJ~_zeh)oA0?v%&Vj#4LNQK#V!^$+3^7|X5Zpv4}J`7rH0T{m=No(&S~}k^z&%SbAJ;ka&fJVI z6ENDG_2EfN&uH1)qh)iAmd!c1Cz-HsDm9(81uD6jqPyua4c%dsA2DBUP;A_3P8Q?7 znxo{X)Z8|kZ^&L_v~2Z3sj+Y;G1UxOlMcJ3UA0YRhOCjRM$9+pkCsTaSMjB*$-Kwi z=+qm*;bWYLe-G>}neX)x^Zjd(FO;g8spa~Hn9thidL6;x7a}y6ZLutwZ}91I>3qK) zlsgkBM5$e(_L8w)YZTm_d7lyP95LS=gM4mJHxu#1?fF{EUo=`5k*z0*95(UET6F?LL}w`t_SS#iAL&~B&xyuouf_{%_PU%CS8C>*&OLvX5M^a3zYlFt{vMRXFbLZ}Emi>26bXBP2vVETDN^!X| zRjrmf1k%$`Y;@Tbc85G?$~)xKle%)Y<@3tDN?E{~%qaK&L|0!~sSMeI8!Ks3Q_;$K ziVDSsPE{XMcp{#us6LrZHLX&;rtPax2Oe3?y=t*hjdayhh8bJIm@H^|iLNx>k~LJe zx-wg}L{*lMPB+?yW_;_UEj9fZT9(Kvs)Y~fJ3 z&{oVb&J1{a!r?d${%?lEy~EyK5phnWTqqpAHK9aG2a*Nj+uQ)SV66Keir0HZLwc0> zFYa$uD%JbMI~a9YcQ00K85Ig$$(+q8eWhS5l(5GqgOQ#sY3%6j%F>3evsTXU_x{$h zP8&+N)AGr%B{ropx%E?ZXRX+bZ)gXmLK|8hX(iusheLM1xl)Ui{bgmemJTRoPLI|w z8Jx7GYAI(%WsJAfI2?hm%aBh z;*t-M`+7KBFkmhiB2(KyIn>x=1ZPV1k$N^=iZ+@x0Qa1|dhczq)I-$StCNXIrBbFn zF*@r`=*skSl`gWu5b5gWnu0W%Hg=WNIpY3QFXO4yVb7zaaFSG_frQ5#|yp+bvkIeRo=lxnGzf;Uo(XN8m+B(^vE$J5c~16{PdXS0qLZ6>t~JpzvtZO(k7h5V;~oW6R^ zxCVxz$6x~<9R}t;oW4bIV^DZKldaIvhu&jIJCexOskxsn?((bm!Ht-tvkv;{Y_nFN z43wIM!rTr;Lpzt~P~!x={e_}wCRH@Zbit123C3jEj`GOUa8N!Io~-!|F{f)rRxUXey;wC=F&H;0 z!Wx}kQ`}(l7Ms>8wW!|fo6;4NmSj1mw1VV>~6=}8M(pc+^9%RMZFu1np}KCZPJ&R^qV80czP<*oi1!F1oj`5 z>n^oQg_1g*n$fk&mT0nBtwyWLlDrbo`4Vn}t*z~LT6U>(I#8=pT=Yt^TCWC5?qJc_ z+}JZ)q8k%#qir&0^~m%EyWSo)ce~XQ2O0X(=|QH}KMn?&I$ATvN!4S|v+Rq3t)Ac3 zSC8{GkZxpSe}o%%ngpWq+(B%N&y@?!v%pY6IX-s?OW8u%=}Ii-E;wSAnW85>y!wW| z(xo4L=Y0#J{wh9)fc?fOrCV7_wpY>)tc0KT>U1NFM6@^iJHn{c`yOG^&2q!rQG0Bj zhJjkPmZ=&|smhGBp-)fybQxc(V=4L~)WIgz>HVJ{?fSG+UH!&lD3h1P!;|skbk0`E zOa)86#71-6=WGXh`kc0`Z)r-ldedXbg=b8yZrU{6vbAgV+)TWc$}3~xRIR7#Rw^cY zTcgP7N1Gp|FP(n>7r2PS7!MI_q40L0O>jT|S$>z#<6XsLaj)ZQIX{$?C8YRT@o}Pi zMQb^E>IA?R>jsvZ`3+`(@esNK9WT5`Xb{{*BFfBl%lsD27Bbc?v&nbNy>kctpSuHF zMcZoZr1#h}-?Jv2P`4-Z_G~ql%h#MUhOoCfRrc$m(L~NO<>*mIP^D5%moj2j>Q)7H zQ^quf*RB>jp-7iH%4lY25fOf)UmP?t9Oda(k;=9z<G8CD>e1$k^Nba%u?J|p^7|BM659=9D#}GggKQ~TBL4& zWoklK%>^eqGCy?^i@{|MM#C{_%__~P!y^|tf9^fGtxT1-kuHxqhousU_M%#y+C0;C zX7kCe*{ZcpHqwSn*lliU4f?3utg`p4BbWCkGbXpKM3tkZ%JFK>qA{bdm)CpZN&i&T*0FUxrmVF-aydg&Iq7XdsxUcKSg`7^XWX-uMkLe8XBriw zJmhKk^-+~yHl-|8XZ;0*yk{%Ox;E*ma>#De)#{#NBOMLvT>9GZR>B1q$WTqTKovzd z*={TBFilOyb3IMfubwHHLM>l#!XHhheF2*`UD3pRZeKU&2suW-uEm?|@SGfdx1-zg zv_I6Pmr^l(X(}O`&L#3yO)qEYSpBkuQ&z8}1%GjM*pCGc)>-LNPfzX;j7H>3Xs(O+@s0XE~`@`c-a$+Z){$!&fuL z9LLBd4c_f$w?(Nkj;8L|vOcWitD&C+)7v)V`!&Wx?$WOV3*0ZNInL5IjB1W!JdlrE z)Z)!?;Rt#bU$q$B7Ur*Dj0e(Xy7Cm23Z8l6@7~N*aSjjo^g)QD@7Y?Bz+8^phFY6KQxVsL5 zPIKD~UB=ArIfVHsMRe^I~Gl)lnHxA>&TBB zQ5V123kQe{)ya+lI8D_XmgI-@eKGwDY5 zXCDC~O%+K8h%N=IpDj*mXCtbb$=EXoTs3*PCvTLE39Z7aF38djd$?4Ks6hvQDV-{x>CV()*Rh8}@wa3IedesgE2&y5)snY0P zJI2S$k*{j;hO|(x9en@aO$gK6mslCJ`z?xI?-a9~ci{a;^=0(b=?9%?dE>!o-{<5D zE>Vj`+R{Wf*=qTw!sU8Y74iFuDwE4$b7vfiq`u*^)>BD;$ZM~`;UnXG-+-ysG5XKK zs|UsiV+&24;vh-;GI#d$E&2fXM(HQ6+}9~%vbhuxSbOlc9W$nsDYg6jlDAP|-xzE5 z^cGpolgWo$maHaiv-LK(>h<(w$k51AXCcev6@{ZVwcjhX-1QlSCzUpAG|%XDUPZbT z@5znPmOiE~Mk}?jz8rO!Z6SR)DYfXD=90Tr51G`uuHIj+xKbXqBhk>vYqD-~vTbfi zozlq-c28ooRrzq~)I2M7%$O9qD0R%(7!7af$UkBiUGRj@|M@OD#>l@=7oGo5@iz6= z1stKiOTOjDj47pR=|0zkCt`kQxaOlCb!5t%FQ`$eyoFRF7}5128lBmt&L@i=dD0!W z`uG2fx~b!GC*4hJC6=v($_aC8V>=rR^y2=m(`MGlq}9BlRjlS)F1OmC+(3Peta%$D zS;%afSLz7Z(CLc7q^I>(|6-#$FcvQ+n zGGLhwN#)crnvra3o#}SL8_#89olv{vw@IBd)3JI!Z%?RWRpV&K zjM0})4}b6Jp8&%jb-?u)jcM7s-GZlV{?B*2aU5J|5e~foEE|s&X5ne^ZR&f2vZ;P| zG=0R<#H3WFrPh;9&dM}0YL(%{{KwR}3jpZ7oJ*`kqc3ZJzyE9HllxqCla(84F;~(U z>`(`TXno#R-P5X?ye_Y0V<0=(nrY_QkeCew_!J(Vx$Qqm3bYDeCQx_cIn(H(0>oE=?r zvN)+v74;)_OzP6<@Bh?$=ut$JkenxeU3{+SpQ5vce;0NsCSRNX3ctzw3$M=o6SvBF ziBo33$S$&e&&o4@%gi#KXQavJ$Rzp|q7M&vazIkj1Lfw|HWJ)(Y36DEu z>&@7#33ae*>*)j4M!}JxPUVeZr>`D0W}E7!yfd}*qso@;XMh7^7+vlv zIDKpfmbnTdV>^)WPDI+3Tr91zPKD!Hdn{oo^(>B=SjJawN>$2qa>idx72U1$2$S+# zwuixiF^n#E6@cnlq-7X-n9G2;93zNl~6K0USr%UlJ4u^m|ED)55? zV|+SX?q1*<+ks`S0`J%kEOQlj25n&UPpQjY1@5sOSmr8ljqSiPSAi287-JT2xvRi2 zwgbyt1@^HWSmr9QfdgaA2rYLNSjTo?nXAAuwgbyr1@!y>(ZrR6>_vH19mq*7wQ?mlYiP;Z zvlX+>;GMFDCi50~t?HljZ;mPwUBYfq(~HJ9eK5- zobpX{b(4vJvFAyt9lZ%%PBx`(k6hGvi&~=n!)RZq%T`5m>aZ?uQnyv5ZZD9~X?kkg zY)tB%C{hOlR^{zZw579F?0T(kWM2l$(9-=w@vV(cN!^WF(pxVVL-v_sKyHw_VqRm+ zBrQ#-Gudn=Hyx;I;@P&3dOJzPrt@K&HKKR9vw^0=R$q9*{oGrd?jM3nO8bnJXi?{; zxSvv6QZ|`W<=k3duc3*gO9fdi?5}9GzHGWpid3A#0CZJyG>dnaYJs^XTwwBD3Uq~x>Zk;|JKbKXsrLoaXBrz*`k`3>TPpZt1?(L4GJdQbE(srOuLbBkKTgj#hiChh0!smQ>aZgZBs@^Ib*0z zW~U;WM8RmRr=zoKsi!5c=rZ={tkho*Oln5HsJR`3YOV)TMbR;*Wu2}Ty)pZADlwI- z%GLI^(&Nrmg6+IEU~nWSqcK;>GBGhbX{VMWj@U#NZ?3}&>7xAuXkVzyR@ABXVam^z z9Jc9vGTKb0B2rDsmzOJ*QL{;|izv&Pj3;1InZqOdGKe{w`x2^*W2iDL-AR{K=jioJ z)plEJ)3{s7Y`0WT*wxjVyjGDnr-E61rEZw@mzSO$$73R#eV*=%aqX#dj<#Kz3zn%B zwhdFZN}X_Gbxk>?*-ptX(=;vBVB8dqMV&_H=$kU;)?T_V!X*`2HeJCNbJ-?}J z4JNAYSf$=I7N{2L>)EH}x`f&jHl)3y?^v2!64g$Gblp(xTU*6h{QGGsD zUHb(V1RAF>j8{;F=$bE09xw5IE=;k`c*P4-w8aJ%5 zXkVzyUe?c1d#&hC_nG+Cj`4A5=~+fx#@t(*?lW*nV|)YI&yq-7(il_y`&kmgC5GiZ|7^qm>{V2PoTV6zdng> z8`+x$-_OUH2`Kj`s6tPpTI**HhIliFC~S^vEU7iFsp80TlWhsFLWI zck9R{4adAOW&*PPf=ln#QW=L zZ>USZd1P;J%o}4GAl5&YD&-ieluwTQ7!Pmj0u9 z#qHHi#+V+Un(hj!FuLh}WaPpYZ@Tk>-565{aq835AlO|uvNt%`jWIcprM^iGZt%7H z^%lBjj7fql^-*e2);0TA))>mNlsZ29lkE;SbjU`fwXQ_`*gR(xne`Sp^DUqcAo-=_XObUD9+!MevRm?1$^DYgOFk?4l;kGKb&@M3ACl~p^d;v?+LEdy zFG)&fC1HtQ;*^*rI>|{ArDUUIz2s=gk&^dH4wMKaEb+g^uZjOG{)6~8;%CJ_5&uy9 zUGbyhhs0kI-z&aT{2B2l#W#qr5nnF8Nc?{BR`J>5rnoH5isRxl#WP~B*e;$DpC;Cd z<>CqPvEo(Y!^Q6rOT;`eL-a4v-$XBqekc01=;xxRMBf*ENAwNRgQ72q?iPJcbgSs& zqK}F`EV@+m0nr7b^F&=yT~rjMMN!e|qG^#^WEB}ir-;-dnP`LP7|~IpLq+cvi9~GS z>%v!rFAAR%J}rDg_?Ylv;Vz16c)Rds;q}6+gcl393%3Yc)SO64c$P3IbO}wuQ-vzw z3Bt9)6~cpsVj)}by5JSTi-P9_PYa$9JSKQpuuE`{;C8{yg6joW2`(0F7i?wXYX0Ra z{>A+5{4M+zzr;`R&*BI9F20F>DqqDvfxni&f`2ey%xCjn=e@#vk@p<$Y2FjO$9NC( zcJc1v-Ojt2cRfXNyqLG0w}sc@m3S%MS-c?6#WV3v<*9fl@YeEH@DApQd2H_M+*i0S za-ZWq&3%IV824dnzULn9?cAHW*K@DpUd-Lj-9oMZC~;HVv$#R7i)-SZ%2jbs;I8Ga z;2z8sbJ?8NIj?YDq-Kbo<~+f9jPo#O7v~<%?VOuA*K@AoT+G?d*}`dYN}LquEKZQ) z;+Qz6a#WlXIBPj8I0tjY95(xP_AAu>lIPe@v!7r;#(tQ+i+vCKcJ|He>)BVaFJ^CN zZ(+CCC3cE^7CXpxu}$n#*(&x4?6vF_?1R~2HklN0Etmjxyvz}l*#(J2wi**m{ zcGk_T>seQ^E@o|KZDF-oC02@c7Awedu}rK}St`~EthKBatb#0>P7c;jrw=i4G5;Mg-iy36Pm?q|_OcnD4=33?o=D|!c zlg)UY@e1Qb#&e9P8BZ`CV?4~*#khxYJL6`?^^B_+7c;gqwlG?Z55hoqtihnC;}QrVbBl?fzF^A&>#wePNQki z01ANmkss8De4t+B1@#~gs2jOKUC0IML{3l#a)8>A9n^+wpjKoBwIBqEP(9Lv>W~g}6WRoN8afU1RCFrnDd-f?lhMhbC!v!-PednzYLOOHgEXLO zqy|+X6{r#^K@~^=Do1kANi+#6Lo!e)l7enT8$nM%CxA|%3DD!w@t_;f2GHZsaiHtb zdeC)f9q6&>SkSd-E$A`m7|=Cn4d~J6XwcPYHRvj|3Unn}33?Pd3Umcp0eU1l67&dk z1nA-DaL~ihVW5YiLqQKghk(8py%+RgbTH_9(0f1+LI;7q8@(I!UFcn)2ciQ(4?qWi zN{|FpjKrWKBmxy8A*cWeK>3Id%0oO*F5-f65C@cv*q|)L0%am5C<8G-NkoDogg^;I z&`SK9_&4b5#Ot8{BK`&XPvW1T{~-PW`gh{*psx|Hfxb$-3i>zVZ=kObuYmrQ_$%mN zh`)gTnfNp4pNKz!zD&Ff`V#RH=pTtcg1$(+2>J)&51_v%eh>OP;&-6GC4LL~0`UUq z^ThL@zaf4D`W*2b=&y-igZ_&673eRCUxNOE_yy>*#IvB!5YK@AocKBD)5O!DKO=qy z`cvYkpg$pg0{Ua($DmIUPk}y3JPG!1%24}pG-_!{Vg#Dky@5D$QUmG~;?E@BtxSBS4*{W9p6i7$bEiTEPu7m52p z?u^Prz6?gG7wxD)hF;ttR|h|ht3j<_B4cH*<3 zpCxVsy^Z(`=x2yegMOO074%l(7SLOWPl0}lxEb_j;*+4CBt8N93F70RA16Kr`Z3}r z(3^-GL2o2(0KI{@9`t(Rqo5xpJ_7m?;yTdlh-*QwC9VOzhPWE^YU0D7A11B>y^6RJ z^h)9i&?|_`K`$pR1HFv66!cQ!63|PC4}pG&xES2&ifoz9*` zr)Ql>r)Qo)r)Qi_r>95gG!mxMaEMMrGjuu=q|@LuolXbnG~lOGzmHCRUOM%9=+xt; zQ@4vwT~0c6I_T74r&GI)PHk2?wOZ)ZVy08GiB3&ZbUJ0EQ=@@S4SG7&>*!RsiB312 zMyIEpN~fouLZ_#kOs6NGM5ia6NT(-i=~SzsQ;nKV)haqwDd|+Hpi_mMPUVwyIw_-5 znUqeY8|if833PhG1f5PCPp8LkpwkV<(dlvP>2&=%I$d`xogTZEPS+kor^l?J(=|ua z>Cvm{boDAaUA2-468(=>ZZtm5Aw7ETU79kWPgHIu-Egl+UA69+ytJ z96IH&>6Fc)Qx=m>nG8B*kW2>Rk$mAElw@ooB(F+dk~}YYM)IU&kK_@_1CskBcSvrL z+(_*UxJ0r;a=xS^sYo)C&61GBBe6&}Ni-6vWSwNC2MA-+X?qxfp^CE^|8^Ti!;MV#SX&AWuRgLgi!!>jNzyv@82&%?9uHt{q( zDQ_KbCGQa40X!~`;J(UziTgaY>)=W59_}OD2e|if@8I6Ty^(u0_Y&?7?)lsfx5CYE zH*-T=57)xo#MN-6+;!ZQ+(WnraJgK9^D5^h&hwmSI8So+a30}2z`2ie2j>>fjhw4F zmvDA)&gXPE6;6h;nG@o8I2O((j)o)UtmCZY9Ktz(!{rd{SJ^MIpJzYAev-Y1{RsO3 zYS+UZ>|5A3vae=e!rs9?pWR_s*ctX_c8Kj^TiBb}8n%?Zj=hq72>Spwmrbx7UEQ0we^Cjl<%x9QSGWReaVLrgTk9i047Uqr2tC^QDcQDUqc9<1rhPjy;VtSYs z<|d|wDP^u>u4EpAc8A%X)0Tmiv{{Vy>7Fy()T1^t|X9(UYP*qDMpzi0%{J zA-Y9$qv&eUC88ao^FqIL>hlmakaYcmiRpCp*=Y`J* zpA_zS7XGp0y;y6L4#xn zbefz24Uj=lKRFHRBLkpb(huq(eV}gA3+f_0pia^a>L6X9cG3xIBORbt(hh1NZJ=h- z3Th%Ppi`t7)JU2@4dfK4o-~5$NCW65QV)6>sRKQg+yr_Gc^c@+~)PO2UHK>AAfyzlG=p?BCm638#DLDzck(7a+KuSR;$c>=KlP7>~ zASXbNBaa7NPi_ERM;-@yEV&+ZEx8W#81h)qHRM{*qse1HSCeZ%SCL19t|V839!0JK zT|uq{J(4^M^ayeV=;7p%pofu1fF4R74tfZA80dS+LqQKF4*`7-`Cib2$b&)OO}+>8 zUF1Qa2a@jwJ%D@{sDwNaR7@TKDk3GILQ)JWAVr{jQV7Z;1)yA#56U5Vplp&0$|5|D0{tU;8T3W;66hb$A3=YQUIhIe z`UB{1(eFWDK)(Zh9{m>dH|Pb>=g{+@zec|S{S|r+^q1(@pua%B0(}x^DK);WE z2>Lzr1JK9O_d)ld?}2_7Jr4R9+5`F>^j*+zqsKtMg}wv&P4sQhN71)HA3@&){RVmz z^kMV}=x+24(66J1K_5c9LBEE+4*DQ^2=oE;HPEl32SInC2SC4qz6$zfvs1Nd966%AzhlKhd? z$S;skALQprs1Ncka!?<4lAniqxr4k5^mF8$ptqBEfPR+z9O!N2?Vz6_KMVS4@;1<0 z$KZpdTPF0=$nBtg@&eFpj&M5px>Z75cC`L zIRyO%-HxE&pwA-cH|RD5{RVvoLBBztM$m82tqA%Jx&=YML7zg-n-=Gg6=r`zM z1pNkGgrMJ`4H3b_D$fU4Wq9p!Xr@H>i)G z-=J*>`VHEOpx>bL5%e2$9@iMzi#O`vl@qI$ z+@Hxo#f$De^K3djYwf6q5N|!cZ=sz$?M)}yIg-_@=+CakdF#f8Shlx3r%u+QX0#(uYR&bA8`L`>B1?Pv$t=jIp0XEB2OlmPxxg z{RKJ=)<$-AZGVb(R{o~5jB&OpV`t~C_gmUi2JNHoF**&LIkKlK`$pQ+W8d_Y9B1J& z_H?i|YH1%y+B?s4bm}#a?Bj~QK|=7bk372HkF)vtjhcsp(Ux|R_RZyj^tO>bJgTp! z@8U6U-bKMUYo@V_u-1C%C6NN!Ia`=c?SXx{IJ_Qh>)O6up(eTHv5tJ& zNAnAGYB_6Po(?Z1r99m<7z?%c%TpfhuJ4W_*P5)%t-@OLSG1i)6f9DLEOZo|e=7|m3 z&y&<6`?|TTB zlAgk#soX|;s=9t;PgnM}w5NLwnqw@0$DWSb?_toKJc#yja_h)GuIOuq2hA~7&%aUg zgF$o2+erJi@mxrsAKAmTef3a%3&&Y>zfgTIc_I=#bsi5R=<(0)%hP!=bZuWX>@RYprqx*7lxN*|&{|xdZ zLU=64O36F_ymJRey#p8W`wIF@m&=Yuc|)BcJJ#ChC?Uy z2U7YNm_g0XCX%UWv(TV_rGBh8qK)L7-<0%^`D{E-{eSrP|Fnm*SK}Bu^sxL!#y28t zE82+GhJK_^ARqc^uX}&uN)25uHZlJ#hx%I$Tth9Hh{kE(!MALwm2J?5=|84U7JlP* zxT$k43jQ>atTZxI4jG;Kzjq9dQm&Tm^LWN;qp~qJtvCA&p~;{*ps8-ysOYK^T~#ri zv|CbLPt})B*g`sweE-jnvu`j()D~YS=Itg7 z%3|5#tJp2Yk~-LkBvi7Bo;o(J80b0m(wM#5n(2BcYr3>Fu2qH&-qq*QO{iI_XVax* zg1U`VxJV~wAyUYc)Y(G{>i7VB>9u5~5RE76v(UJxyyHa@>L8t+k-nUchL;-9I6-LH zqM>S`XgCHc`0a^?V{FL$e=Hilb>VG+NETfv;_V}by&2@@#Xsm@^k^_Qa8dd94taB; z+?&yl{?j+Yo0j_b|7LWPDfW5Dm)fZ`>l?a`H6Zt;{dR{T>2gNX%8;~NZpJ+MT6D@e zRrDFOw(9;zH-|FOba^ViOw^;bm?IWXs+0+sU4>j)E)Azb4n;Z{u9(xUWFXxM`3p*G zAsd+qC^~vgJfW1O3;B>IyW1 zLeXvTcKTm>%A5DdMcw&$xzwN@rD3QX6kg9{EA(hc&x+EHB(imCpzPw1vYI|%aQ=yY zGC7@f5MySWwE|_J)GQR{c6s`7V~H+5^0OjkYCq5aQ%)*mO6t!SJeYH8+TYT4^c71* zt*!-YNvGP_vY11q!i?M}i?-y!d_fs;>o+!=@%=uSGa8iZ>|IB;sfz`BXEwa%b}OynDu!>Lzl*%EqYG*7o@uHB(#bttmUk4Ix=B zsf|ZQJD3wLo$A=s{(oW}BTX=8m>kBvj5PNUYQ21ivyNK%zJsly*05jAvQVqhKPCB` zrB{PjGMDMzbo%C1oT5WYKz)a4dq_7q`j(# zx_{(8?w@>kPIFb<(x_&2U4vyxSJqo&hN5GrMB@C#9F)t3vM+7$yhZIvhwZubGx=Pq zW;D3cVN)tr$VP{9Y+20SvMxqW0#sXG1!upP7mK zY-P(-yj^z0t3&phi*;bdy!ITOX$5`3s4-Otcf00*J7FBMM_jwy20y&0!M%IPq;{1# z>+jg=_DIGXwCMA-;Jm|zh%1)c;N^=N+`G?>2e#qWKo0jI^NPAerQpH^M*QozUOmWI!0Z}ZtaCDMNe?J zr(C?K!Ffa7(grVDZi63O)Zn~ev$TgFSj?b&?`T8yl$n`Gu+nuTBdKnxGLua>I>Yzy z!sRx&b5VnPM;obh1P^yCYH;u1Kp6~KtHqwx6w6!8UXQiXZRdwC`TfgnaQmVL=LLtQ zJ-lF1gY(AZg43tZ$HF$F*OE@?BYM;D7)rcvQG@fM&C&+@iyE949F~4;-L|N~dBI_6 zgIj6--}x*aA^E4|6^a1xTgk5^Pg4wl?@7Kbd6=RAd{OcR$?X&e;A4`HNUowt02fN$ zC)q-=0BVwgBt_8xB9ef_MezXil9MGWiU@F=WQ}A6#RPblL?~fXRDi#W{~~^o;sX3a z{8RB06dB-~;;)N$QEY&_#J7oWrsx3Iimwn~Oz{D>iO&(YC_+F^oDiQy?cer^9byx; zgZo6WLVN`hn;%YA^TKL|+!&L+$4N zwCEF}>#6RQMpZ6XH(ct-_mx*9b2a?i6knc7;`8Ru~nAgt>PWZJCMiYA?|D3 zm$@%+pXENqeVqF!_d#kF=T7de+?%-9a4+TVlR7c-CeAgSOF28Kvm?5kDksZ{a>5)h z$I8)hv>X{{J!ci?P|krI9tW{sW53LPf&DD|DQd^bqwELS_p|S0-^#v;eGU6k_D=Rz zc9&gcXW3D9nC)d-**dnCEn}}|uVNp{K9J30Bi3uImsu}RXH!1KdYttr>p|B2)HxNm zvTkBs!@88UleLvPv$D#{vZAao%geH|bSy1P##+x>#X6LAAdAOB%-5JNGhbjn%Y2IY zIP+2FgUtJxcQS8f-o(6yc`0)zb1SpUtTMCAC^O9TGObJGZARyV z&SDhcnHU9l21Ws%j!}RSi~IE_(&0gM9lV-%ndHNd_XqX0b^1?a{o zKo>>T$Xu>GKDU1R%ViceOqX6|N0e`2%D8Nk^ z1$Y`p0iKFcfTv&-;K>*TcoIedo`_L^T8skJU=*MVqX3l{1*pI%KsiPMPGS_G45I+0 z7zMZyqX199D8LDf0z4k005@P1;BgoQxE`Yb*I^Xku^0uo7NY=21Wsr7zIcni=bYK z0Sb`#_W%V*ygony68{>Y0EvGNP=Lfg1}H${?*kMd@!9|dNW3~g0TO>3pa6-#4p4x^ zUj`^Z;?DyVAn~UG3XphtfC4058lV7)KMqiU#ESzIAn}I*3Xu5y00l_=Zh!(Lemg(` z5-$u;fW-3y6d>`N0Sb_KZh!(Lemy_|62BUt0Eu4?P=LfQ1}H${*#QcWcxHeCBz{go zkEo5N#M1*5An~&S3Xu5e00l_=WPk!Bemp<{5>E|KfW(sn6d>^0Ys zV-(KFyU&koGhcF88YZwLiAVvW`fKh;7#VEjC7zMZsqX2hd6yPq5 z0^EgBfV(gXa2G}a?!qX*T^I$p3!?ycVHDsli~`(+QGmNJ3UC)j0q(*mz+D&xxC^5I zcVQIZE{p=)g;9WC!6?8lV-(<*FbeRC7zKDgMgiVO06>7a7o!00#wfrqU=-lzF$(Z5 zi~_t9qX6%~D8SEQ6yRqu3h*|J0{jd{0e%{z0B^-8z*{g1@KYECcr!);egdNaKaNp= zAHyiXn=lISMglfM6E|QK;PnJ-geE>pz(#1|BNzpE9RVAmiEA+m@EQU(LK9bG6yQ}D z1$ZS!0bYSofR|$w;AI#EcnL-Ueu#jL(8R?U1$Yrg0e%pp06&0HfEQvE;7*JJ+<{Sm z@5d;>?HC1k0Y(A752FD47zMZuqX4&J6yW*9PBsIP=Mg(-*+RUZmUD^iw46g+K+D<0 z`)KJBeOfxiHd@-mR$5xb`7{!(2>`$b008R%0IZ!u|6Tr3w2AifL;wJ40RX500HFG0`gauo0F?j$Q~&@_ z4gkPO007DW04N0j;6?xdZUg|}2><|`007|e007(o0Knq_0C*e#0M`Qma2)^uj|BkW zS^xkZ0|3Bd006iK0DwmW0B|(`09OG3a3uf$R{{X=sFk#zD*ym^Bme-9007|ON6^0? z1^~dr004L>000jG0N{H80C+F}0N(=uz=Hq)_-+6Iz6$_=2Lb@_0000=001Zk0H6o} zfIM;j!6XpP(hB<(zVh-Rbm;-n+<^Z0AIe;f(4xkou05zBcsKy*X73Kgc zFb8lFa{y(S11QBDz>Sy#cmn1CPGAn;@t6a+0doM4!yLf%m;-n$<^Zn69Kd5R2XGDM z03MAwfU7YFa24hNuEZR`qc8_>1?B)Ai8+8rU=HBnm;-ni<^UdwIe>>?4&Zw+2k>Cb z0elbU03L)nfbYf}z;|H|;DML}cmU=AN-zgdj5&ZJ%mEZ)4xj*Y0Qr~$$io~!F6IDo zFb9y0Ie;w80c2qgAQN){nV18}#2i2d<^YnI1BfsOkQi_PDL&l^@H+VVfCGsBHQ)fE ze-1c+=pO?PAo}}&1BhN5Z~!Si7XI$F0SA!!?_=P4j}d4hl9O84mg15#Q_Hp{b9fXM86+!0MYLT96^qT<(5Ir~G0HR+HIDqI^0}dei<$wc-elg$xqGtyjK=jOj14tdG2XGJO0PevYz&)4) zxCe6p_h1g-9?SvUgE@eEFb8lC<^b-&9Ki2l4&Y;$1Na@x0sJ=R0DcQ|0KbJffZxO% zz(+9$@Da=b{08O#?#3LzuVW72Lzn~jHOv8g5OV+@z#PD@Vh-Rg%mMrg<^X;fa{#}H zIe_|W0VGiI|F#qq% zmi`M;72eA@H)%^ycTl+ufZI^t1$=g!$;0G`V@IuT1+=)4W+c5|50?Yw?ALan| zF$ZuP<^XQR9KbD@19&dx0G@+6fM;V4U=MQuyO;ym!W_UR<^VP@2e6JgfHlkktYQve z1#*3PHg5`QZW2l;Y(L zIDl7T4&W7-19&;+0A7YUfR|zp;3b#?_#w;zyclx;FTxza4`L4Bg_r}l6LSD}U=HB> zF$Zuv<^W!RIe_oO9Kb&20B*w^z^#}Acs}L;o`*Sr=VA`vIhX@@Hs%2KFbA-UIe;C^ z0c>LqU<-2qo0tRGz#PCj<^a|(2e5)UfMv`9EMX2{5pw_wm;;!{9Kamr0A?`V0~p5~z!=&#*#G||!GD?cb@Ivo&9?K7SMS_`aqqwdnZBCdu(op+ ztr-Kcvoz>!G&q4mI)B63nQ!cQdUJ~y{Zpe`q4$Hov6FmosP6EluO(qW^dHodC2GqY zZEGJZSwgy_GTyXmV|E*EE8)P~M%nckVcS^*ho-cWfwyL0z?(R=!q*y}|Xv%h<| zlxhWC!h(CaTI!xE}irIi^*A1>gAS*J07aH zOsVNiOqWu6V!4^LvKX^>!WwBOq#JGXI(_NX9=P0O}9Q#-${uO0`* zMA!bQ&eQHz!m<>CZg?unUXZNT`TJ=uHH z`kOn|ssA-+2m9ijj!7!>e=*c4G)j7}{DPsbU{|CA`H*{hqo*V7>gwH&KeHiG=oF2L zX@|F~h^XQgi^<|NSrz*o>LQL_Ri;X3C-Yg8yRfmYjc7OeT^gsZsmrSZX_;T)YTERc zSUByBs!IAm+v2VJr-E%&t~}${sgu)b+lCpL)|zjo9j56G)5fB^svj-X;Y(j&C;K@4 zm=2bLz7m4vF=(6^wRE2z?ZH5B5_REbGh+#hii+B%(c;b1wAg9|*=yamB>sYjc69hd4z`|9L-hl2k+uKfomv>-lX zfmpZKWsd|5qMTX;Swx4a1?fi%js$a&NK1#=dH3%>VRK)b{D*c7P$M4&JfdBEgXVMDrMReqqFXWu1t^f^I2oPxdRwv$3Atm^skKd;s!4~`Wi;n!x@u|8Y*(6Wxzbd2vejrh3Qj}79&Otd z8!8QTG+1%T>@}Oysn%(A?UBG9_|g{$hvW2ADjd@9{}aUBgydF)M@Y=sdM$Va*ktvpM4Jd80x(BHftrd zV!FmWobh!=k#R6}9!rKi5PcEFk&rUI{Qm^{CMM}kqK8){?KO8b>yE~3acA0U%e99$ zZmy}weg2lsnk`S4dVzMV;MS{sQ>AD*rHd50MqgDIOSo%6W7+1m2KrNAcJ~NoT{F#$ zspfClXO!J|J6wpD!@-`jm5oi-WLtO6#ep-OaK#;jm0(qbZj)?Y0H2>Z04>4fG9Q_Ms8XHe=ej%+*j<9PQae&!28* zvxRQ5QFH0lW|P}$w%Y9pPpxb&&u9XDJ(&I42xdLfh(cAe7xc+;Em=0HCepJZg~h2X z#cQ@%Lp9KDnSJe)&o+2%zk+Uv-$3% zrfi?8=#z3?C*_#!ntOWVw5lMl1Z%cd!_=~Byp_CSBJEQI`X_J!naT(ae}sH9YPI1$W# zaRjrLL_RlD@!3?|t zOm?cXaaYgao^{AFnOb(rK3%9s)I}?ws{!dvc#iD@z4Ev!&+r zOmQNUP${ZWd0Q1s#1#1))ytG%_U;kPW_$jMy(q7$139UsR<7h`4J}!Fwqn*9yi?ZD zWZojLRsEBmyotIA3NZVH5zJQ7KC7-5XgFF%*=(XZ;VEX+c703f(ds<1daC9L#XGaz z%8cAkH844t{rm`K)d8nlF70S_mZ07)Q}i5(nzfy)S@p(fs@yL5OU;_zluvpB4Jv|7 zg4w%9Fk70bx~le6Eo7WADH6q5M_z3yr+gD#s`diLo+qVt^d@vU*_65+=*z(DogYXTB@|CK*-HEnz){0%PrDB2<%-*r|nH_8DO?%Ae z$vaYRjis2X* zK2r?H4N_OkYmAwsr3rN=o6Y2=1655t+xF!H{S(0K?IV~?`;3)nQRhzT4N_ZDHkni9 z+*)6+p^2nR1z9cZuV}TtY`SBXQ!QZv%zkzRv#zAIRG%msT$7=?AsNg$^X(~T&tOUF zj0Wv=+cKr5xYI#n*j1*&`0-%&wh_#lyOx<$A<(MLbYcdN(d3IqD*qpQUmoX5RpsBE z^qSs~O_1$9c!JP|PAZk8^5!R4S*o&CDoG`kR0LyEsmfAGDyd4UQc+-@Q5JDS0TBTO zl~qv@kx@itTo4g)VRd)Z0T*Nx(NTn7CDqT@G~KVO9uJxM4EmOSZPMN?yK}vmBTnWNy3_EMqUlJF4TaReL@Mn? zY9-c@S7*YBBeB=%5bI@IU6!sj=%zCQGA_E~%ZM_ncwD|9)KTl^^keRFr6PAKxH@F7 z8i~DDhuB_S0J`CTKgw5p0#FU)pibt$ zbi0u#M$&94P-{Ef5!o5XYb{ddJSkt40Sd)fGibHufrz^396u6!wGOeMs16Il<(tuP znqvxGhRxZaW|DI_odpzez&R&hsl-!Z7pgkOSB}Jfbn9CVBS1N+!H$vGkLVC<0Nf@u zIBq2N!#czoK%z+vjva};N{85T$t?q1q}ndlI04J}s6;!!6L6tWs>K7*KFgI^7K=G^ zs8e-}+ec!r)FIXY(o1TvZ6x+XI>Z{lW=Rc>8HxR%92y>#nHq>)RY;rV}Je0FU9oSDNX{#E_u|LV`#;^1F~apCj3 z<2!u%r~KA~tS>GMf*Zw!4b0|IT-d;T8qph=)*Hu#4OsC;muP#ymWJxAy+ZpZU3mXP zfnhwa)m1C8X~jHuhcJiLL0`ZN9mol8T6n!yEyWr|u1K`KV%^QRB|Lz_k{w+3tR!IP zO2WrS;PP@L)Rsc~9KmdNTSa;q06M9L>lbiz)y#%5De!Ed(-#da~2Ba2E5&8;*zvJ#KW$u@yBxx8SEUI-P_f| z!UiVOelt7A|4Oqn;uki!j#&?f@Z#9s2-s$5cgJ-Yv9m!L;q?~`Yws3@f&UlKFx{Tb z3j<4ich_C>iZ&M+u26MYO8bg%uh;2kuvo0XH``>+lWPKLz76lQVPdHYA1yNhrKs50 zYMKfJ@uI>C%W$jg@-t8%)a#_vYMDK*oMFPWGtBh5sd~5WjL}7*T`WnM8sd^tzT9#~ z40Bu$wR2L!r_V5lT~`hB+}gl1G|YF=<2wvYxBX_Az($7oPha1RE|BYzxt?_a_~KaE zzTW%|jcubl)92oXZhi5ofxqkxD&<)e>)q2Tbjm9Kn#R~v%%F*GCx?0>g+hwre2G{ZL|V(9 zI4dsO%KLti=|SWb5oGHDu+4jkc9{yonQ)YX>nnf~6H9Ek6bq=C9Yri{hazPb4Agv3 zsE!umFvmERRM?Z~$I{7QIOHZP%PvgtsabY@)%lI)(BAP^+-lcVZ*z8c0F8Fvii$5F zQBfoS(6v6)Cfu1Ol9iIAl_)Ufq}o)u1r?KcQmI9=QE|U{8~gM1HmC2NA^)%swf?^m zcQq8S&UH#wp+F!=KE-cbUg*s)Ed<5H!yKvl7-1mekDOg&m`$7`m%aTFN1%?Z)k}KH(p%q(| zXsQLQz2R&!G|R=a_o`L={nKYo-8%V)$yZOX<`0=3R%`jkRKgeN zXP2)(%bb3C;*gowjO_qzHM`JDaWGy)ThS_(Aq%ZMkj_S8eW--NakNP$D_I}IIr&Xy z39!E6rCz%2Tb8OG9vL@Ii2>ZZe9E3)2tk1cJDx}{5|Bbf)sF_1c3oiIl_Z0t(h0od zPgc;rFVHQPH`8u?RZP8fTNj|-OD|lze7!xrK;pRMm+%Tvm)SNi0u^7R)^`?@$uJDX zfv&F-jaFT8%Hyw!n`yVc6sKOg=hX}6E}y)o7jZh)V@hf&PhTo4K{uD{+e?9Huvh~9 zHOylLl)7N=bd*}!w`nid*Z*=yNS0k-wz zXtmYymU^N!?5KcfQc4$8KoIp3bkkm}&!*k17whS^E*ZX;UO0C7x_f%jOM<~{v)xJb zqH+qRq;#mu<|7Ip4#|Sxu6B!VQOLXfl^-P--m>z>fq0O{gU#wU!-Sg@N zw96;#=|$N?IqX6|-?A44C&O693>Atv$P$|`c4?bO=_eJ;-w0==W?(bzbgmcn^>kZf zSL|gh)b*mhrx%`HuGymiTOX&8ZWHLCM9;;0x;Y2XBml+QZ04gSF$X8}%BC|)=XzmZ zPxrifp{^ILJ-x8IqtTR4%5Y?WjRmnnte*nBLc?7Ia_M?6#hocC?(170ImFB2z8AUI=>Y`g@_S7tKArFp&1Q znRe^f0ljov=Zx)TEY$U)v8NXXQr0%pZhdmIUb^Si3t*S8zNZ%kvbZ+WZlA|u%S5}q zj|I1<7X~trHtWSckHz!pg*q0sJ-slHl(Shc_IWIrQZda@;ZV*S!>g%oG%V`3Zq+WT zdwO9Yp=7gO?DJSWpI)eAQQ6ZA134C(^GV~fYD z_4n^zxKXXH|M~^j!s7g|=5L?Bc)mH0%pX1X}llT-IhU2R@8 z{mOK^>Ef}UOr1GJPMxgQFZ}W3EtBt@WG7FVJapo*iLXsuF(FOFCXP2hZNA@pqxo#} z>&>pQpKchKIKiASZI6XZ9EFKoLJqY;nR_3jgZ9Q!F>ZJR&PP%(^ z(tS&#OOy+e(5U+2X|h1pL{V8cUCfM5x{^*hS|?pmCtX1&-5WHzc&XM1v7(m@g*YkW z?XL&rn&NcQRdv#pb<(YV=PmVH{eD~0t$vp+>GXb=*K7TVRn_e3N)W3`La)Nt*w(uF zGWmI(bf42n_i3GUH|eDNluo)&>ZJRGPP!X4IwTWGNI5PfOAOsi2JrQ7y(W@6=@L5W z;yUT@5gnGT$b37NmDG5N-%sGV)VlGV*WCTqbebFAnr@rUc8}3XceGBrqjb_8sgv%A z5nXJ{@r~)E!?bqWVyEbf=0XXiO2vXgJGpK=W-rr8_W_-Bm+GW@zfQVKbke;~C*8$5 z=`PYqccD(Y_iA+NtB+2aB64`utH8NZWIfE9>3cfqzN?c?+ty)QwyW(Uwx-ke5nIz~ z`-rXSw0*?Zba!k0&G@23CWDqTG*OjeWHhvHKAS(LlkNtcbl2;oyG|$FwL0mp(Mfl; zPP&ikr2B|Q=glCV+jcg~0| zD#1xI)C%XAWTID$Fl=_+cbO7((g`~0+B)f4I_Y?wbj{<N$fE2 z25g6cH%(i=kUI063Bw8)cmk$J0rN-!1JBiV7>MoZQNUzaf&DxLr*0VkzQo3l5>> z=myBDwcFx^uIRV-$PI{tiXOAf~sK!_VW-_MhX~sdAh%cpln!y{W=80^Z&LnZfxnIrDGRA zws_>io7Fb}Z=OG5?)Pvv!%w01!^)7#9>Vs3jVRYEzzJQxEHI zYTdK59R>=!)dt6_?Ky_+J*2z6wT}aL7%2C)Yku%bwY3vYS6h2fcWc^v9=*vYI}9Y< zl?FS8>I^&ofNpiPGWRXs(`|hdoechVveX6X_w%TDJvr!yuQ`I@~M0KisPq#XwcjAor>|FUB z2Flm92FIwao;sYM-__mfbIYFZFi_gAGB{doa^&m2qr1s9zRp0oyT;%swW*P>yH|Hp z>wTSpI(W6gk!pJ*U-xa@?LEs3jZNSq)YeA6?jGH(?R^5TPqf=%AgQ%9c*Rhi;Z*vT zZgp0Doq;lYsz*6o`^e1r-CGBEH}Z7`%H-`ZQ2S2R?6BRMeRJP5Gtk-&0|8sr*S&oA zRJlvHW_zD1&+&BzGKa;%p{hC~U-u2&>TK-m3{?AT4GvLT9r?ODb+@{KuQN~yurfGU zZF1!6zOK8;HNMWk%}`@-klNJ9*QvFix5}hmHH-DW&cHoPZE&F4-pJRfMWJ=J_bgv$ zYy!VbZEfW1)LPLxTig2tevYp*5dSX?4j8I4^mS^DX`Sk<`Z@!57xZ9BZRVB3X71QJ zfVw)S9llkijT9+V{nny3l+5^-bvN^@p)?XUHxS=dHCx!N*_ZZBGXt#|spX=2)cN5I z7#=jfs9UqWPsrzZR0CmHaWJQsDuDk7^+D*cw>WR!1K73%Xm~z@r+7v{eSP zYLg?6dYkSh*LYL|_3K9U|Hh6UoA1rsH}RDE%d>vY%MDH)mgTs>ry@&9VYr?w;2D3m zPau4VEVc1sOH}-Iwe)JTA4xmlk~>cM$_aRDoplz)+Yl_pKm_Q;DT-%cN^evYZ>`gL zk;>w2Rue}pwXvp(&PL^dHmtm}VU3=(HF*9}H02)}PrnbK7{@2t1+VjV) z>kvU}L_F6*)izWB6II?mmrNQm@2(s;1ZsNWN(Q;?Ah;;Cj~STL!?Aa zj;4byl<=WGC8gGjCfcba)~8#-zE>_$%X#CXualoQ92$9mb}Rr3a~z1P*A(Oz5(`$ z5zjEJJ(wPxGW2fmP-_nwuvx?2Z3u%SswK+{jXW=?2#wWdS)FSj=-#O4kdZ#9at+lO z8qynB8#XTaX<+Hx$h&FVgBQrJt>0Y?n^}u*sWmQEs;yz&!QqNIq$pLzR=uFC`j2Bb zF79G?3`zG|)M!}Ba|@H-EdIKY;`>FtBJWoMbP6#yPhG+iA43Yj1NbA zrGVm#*aDq)q|Av$Ap?l4XWxs8R$C3fPzZ%=y+%xwQni||R-z$qIp77#Y$XYYf&zo3 zp}H+uCla}Irj){JK^wUO$UPrSB)y4Pw!&68j8m!wGFR#FT~ElXtD4xT>qEmnX7HM! zVZR+4-(g_78aC|VaW5YJELEXr(S2x-(kY%qw$ga2S-L3E3_B!45 zL{DG4UJp%X3*&uu1bV2!KAWfw*tG7LEnDqmkYF$4i;4DXHbQ4HSbz&qu+UWvD;q+( zIZq@Np)oc>D9k>)dABFVd% zUOk!#@ExJ;VhfoR?JdI$(}g``x0di`ifFD1(>1*&s&;*7q7#GvH!{(etL3)AXv$i! zqHNh2wBbZ6V1?T?(%Q2Vj8gW+P_jT3Z9>RaaZ~%vM4kUTO;mrXz(yvz!Jb#0#4keG z?F|%B*JH6#P!ArXNxgd)?b_NZpNAbfJ$7(d+H=FDGAGqowvmjlBmj0LRQ4nGUNdS- z1Zg43#Jv#*g~6pL>0#kOV4v+U!2k^;(Iq)YQ3YR1#2i96A=k?;Cy_%}P=u|jNL?Pe zS`g#?9?9}9ywD&4UxAKRBzGHPN+}}9XHhK6cB)N>7wuF$RL!psL2%gh)%t(09=myL z>6)e3;-3~TTl6jbdf|cvX#OYjXU|*bzCZWIxnnKgu(T{MpS^vSnVp~c^h|DMeEQmH zTz##7`ILY1H+-(yTSx!-rVpHY-D!;H%D?{b3um3_eJ?psvoTb`25J~-f*-i)o6+U_FGw5%imz~k zPfq>Y=Xo~uzJve!iXZLn zYdhvlp{+ay@tWWVORxR(h|k?OCnExSc%roa@<7@n33S3 zZ1vU)ShGenDKuWS7C8sh_Q)!52_w4^+D1Z^KH=$Bi+EZS%rEV_=))(xwir^7PL)wXb;1s=0A;HTjob6SPE)~1&=KZrn&4l){Jn?&@~@T~X6}4l zvG>tqPf%X*U%e|@zdOtF&>!rtcOQ?DgW^bV!rDZ56~zc^@j}gkR=qOeaoHIcWo>i) zC|&QM6bIXVd=5%zf_;yE^+U&Dz@^LYYF{-y@rB=HK1cuhBX@uHl{d3H%<*f>Um*vD zkzfNgp)|o4?h1YP;}5nTKPmfbIWT+Ly~JGLLkBz_gwOlL1FgTmlUc8O7}k!D?7{S>tAiATSi`pv(*;;@syxoi5| z{u@4b_SvC_u6X3q%ip;2wkuvo4)Ps*ir-Hyn8M*AD*q)V8ZGKjm{% zSMucGw2@!~P?ehCQ<2`Ug^y1Ddbz)=|JS7lekyt{zv~-Mo%G&2{_BD-KM^{}L=Il3 zX{;ZtQWN~S^S}S-A5Z%DPv$>6dv*JhSHtf<_yI<--2QMfa^`7o3Q4~v2f2}81L;nh z;KyG3rO$riQ)iNQUGT_5U+{cWm_PMDzB#k=_M_hY$V?pl#{DzofEo!lkk_OMe&#n9 zLr(|)wzB<`SKlTH7d&l0<&&?!@94)yVHgV%GUdEqt z!Cxw_4@`gj>4Tcj{M-8+$<)p7{V6#hM}iHcA8CT$YrXA5SDrZeyDxq956|3L|MEiY zqd&fgJN|Eh+un7+)v;4{9Ze3>Bf$nTjWoeWJ#<{|!qB6U2bhOG6Z@~o9lx7g{`~b% zZM*4}()m|hdGMe9g&d?tf(;}JX@W~|61k})ocfnn&s{Kf=AX9j{Ld4{f0;`^@~aBzXc};#7MA_ES}L~Dn0P-Uw-hu#u@EDoq5d8OP+j7@=vGU`X2{8%!mCK+yMN? z`)?r!$&p|Ki8`9b-%2;%bSHO7XyVru&lu~% z%ddA>AHMUBop(;SKKS9;?_TliFCU7&EA}usz(#@%q{e81FR#7h+G~Dun)9tcd+3WN z$G(2$6KCGJaC!9qefzAN{`i-fKOJ-%If#w~8%T!H1V4K1-`+4i`TNCdz6t)weD~i@ zc=8F~qvyQm($D_;p`RFgTX_0jau68_HjwwC39ev^w|w^9H=Xys6RYq2p>?4#_Jfy) zo__4GtIl>Fbi>z={qo`DAUqOmAZN_YdZI#SGm0QQ^)~2 z5^NwjL=$}F&3Ay`wKdLsMqCE(|JPR^68p+IKeRl+-s;m@Ib#6Onb_RhjmbM z&-H$y@|N<~nfZU&#y$PSDSv;|bl`>O`#*-O8y4S4uz^GkP4I1RJ?Z^ZcP0Mpf9J>l zw!Db%+?l=uCuTl-OZC&`V}Ad|2X7!%VDF*E22wLL!9V}nt*@m{z3isc1;4scd9w3| zPU$CCMd`dS2rxDjszRH($)lD{D;?Gbm%*?&OZioGwFxl+57;onE71$$h(NvQNMlC zv}=r10ltSC8@Nr<1n>Is;}3hUy6)CreC(@lE?C}j<+fK;AN=+KxBcWj(?7BOm^$L! zqzdsp6l@@xtqIq;*Z%X?!z?q{ zSaj@@V>4IHoHx^&KVl{`_o!uQ?mKfIoNLVe%iOH`ub+<}JdTWCKi)Gfj{nf~5tCqg zt@#Rb-MrI0Gx>(elP8{22EWQNrO33fAM+F?g2!3TsT+Sy`M+$bvEVXHOL8z5^^uG!?1 z`A`vS`Wz`b&d(g8aYTqfq!r5{4y8_|z_c@GQ&jLyvRBE&kwP$7sp2*W%67^v(e#+c zfwdCtGF0_6oUjaH4C1Xq9SKA`F?S46qoE9BcLAyQ z61I+t;O?Ru6;|;#iV+mSm{_{c1CaT38ixuI4uNbs2*g+}9IZto(Y&bG5E$;&bIFJp z?j+h>oQm*1e)@>jHaZX;jD_t`-Wp-5Sw2DaZFHDRhhxqV;OPWdwu}nRimOdcJg9M` zf(3hr?FVfkzt`$ip;SECawWo^h)6B-8rG7dN4WDBuYI_pZr z{WKKCdz44&bfaFT$J&BS!=B+{p~TF2t8L_kSc?nSA*T<)Tvk%Dvw#}c3YDt4a@3X% zVRq35AWY51nVYK|X*E|QNZZ}0f+12T6T$;RtcF*@WK^y?GQonc5~$_FXipwr{Z^@9 zu@4|nuL(6WL{VUvSezDH6wQVMnOe2xY50J6y_O9``KgDsHktsQ1pSaTp9MP+$XEyGLu<(Z#nWjwPnoJ3hk6Gb z!XjuNQVZb6dvzFsIk6PaJA(c=5zMqPPtz%2l$fa4W)IXjGNE7{Ds<94KPMB6wbm&4 zJ^f@R=u?4Mc@Y-No%8?XJrQkIaQcyHs6n@yscu5cIK;jFKjCb5l2Q8l4+-dh66Jeje`@^ z`wOJP(};(Q`XZrLqc3r+=!}X`j*HNCx#jf2WtY3hnZB~h5k#|f1*X`3)8nJ8c-rYp ziG6=Q4-g^)G$Ve12-#z{Fu|E`(>Q7v?p6cFZP;FCodVec+>T%_mu}@+B?&6}5N9jx zaaj2_7MhwIaTo{`j$fp4_)xc~VksoU1l!N52;g}Sp%kK-SlnJIRr+?gCZSF|L&+Z6 zjJ(`zH|Y5WW_GB5UyDni{o_8Z39#19WK{nWa0)bSm71=61*?*tHdL{< z19eFZR>7cwoP$i*R^5i50vBT=;0rBJPap~Vniz}=NR%TNAAQuBmayVZ#Ony89nE|=F!l?LqZ_L@*e>VrwPZ$4wF@z?kA`hIN(B#! z7Oi+7OZwTeKoh>i_(2+nm}!K;GFelB?r}nl3Pp7k(6FmqMQKWAqiGW51R?1Uf!t(N z;}DukS}4ZRY_L#g+C;AwXmeb~(eQC_G0T8RDaMyFO^&3~6ZgzKHD=M4>Cr^8uZ4!t zCepXGf2>7O(Ef3^_K&r|1lm7FwbXXloo<^AbhtqXbb*OhEW5rmujQ_hxlNCHZ*SwHBt!Ioc+jg24l0$fl0Zx*SIImFZ^Ci++_Sg{4ub05_{yNgdlK3kwIbLoT=7BYM)$9Wt| zfhebCC0GZuSxRu)swmFQU8!*d@r;lK>ufiWEYsegSGF>Jxt)(^#BM$35Bi8+DlTF! z^>$$H-I`W%4E6ToNjfJse1U#1+pU0NFIZxFo_M9&OyzyF&2_RBht)Qh*V^#sJQ*I31>JR9n-tqbAl0nd zG0dTQlwv&LMnk38wwEAQJU-=(4}yiiewW#?@3JRFk*M-gJ>tQW+O!uCH<=JN;y_ zmaE{sO2%Jzve`b>iP>!}p-U00GoK1Ix)sPH+7qU)k6gQbO=ULCj8->M5SlOf<*JP* z+F6Ic5TI>PIaP-HeXKOqnsT)gs*_sbZxo}zGqZs7I zTnL^?I0DwHH;I(Uw8^h=2v(em0)RCM(?7YaD#Tuar|AmoJ^j z_fw^mwSxG(fGtH(X zj5I)iBiwKVjuMduHU7*h2PH!Nx--;IrAle_PO3qrLO`GNDRxi9L&uz1Z-#;*JeJ2y z+Cl^rtZEhWa-C>6O1u@Sg=5`ZmFO_IKjf_i0-2t|wB2yV7H|c|p4QqR-E!QkNMx%P zX*hylp$oP-DT3OHP8*+>Ds@j@y;bH*Z3LTIU4qkLud`X8u!6*TT_ozSx7==@uK@FL zM?Vtj^!$X|Pqa`*fX$CekuaQcJJ*ik0kC%E)N~8jP%)_vl+DYDYr^XRyNPEO8G>fjb zdQP$x6C?n(;}Bx42jcE?@t60-qvqK87>JPhtnGa|jp@zFtZv-i4C=1kzwE#y}8#E#NJ=tVc z3W*U4P&X4yFyfX?ztlKT0T*i#5CwsD4d%&)I}5l9T-F=&6SYW9X&0LkBgYkYNu0dh zGI8n{G4{xW=>hdM{~4yZ>A3MH$L|`ye7rU8TYTBVFBWcDIB(&!C1okPbjnf<}+jkCLEQ?sv{ z`PE95Ca<1UCZm(bO#ETut7^=En;M(4E?r~3)qMWqC8`<+PJT~~Av`wy z<>?Ej%hNlj7p8tXb@S9YQ@KTM@s!0sOk$JUCjL0_wb2r5{^gie_geev$;7zHv_0H} zBUxOiD^(Fmx60wTyv9>PDn8ug{E_oVt`e&dr{PwuFGv!MbXBObGQW;^!Sayhp$Waz z6P9}|_v$4!TfS}iwq9bB}x#u(#)B*8?NW zP24zfqh4am#K$K-u9tXr;$ssZ(@Q)val^z7dWokeuAjJGFY(mGbraX^Cf;he53u~u z@J@U74xsqOKh4yeE#r>)wvO_rVFwUNi_Kq-4aEd zjjW&0e_i;iUddDQN6jC#_|>sj>$Sn%g11s)X3?~0(n~zOIKDWppLlF>OfT`&!ZQoc z=p~+1QBj`OOFXgg_l3XfB{r)yS)bBNY+Cr+!ryiiZ{_L^m_K3ugx$nj56Z;iZcn%6 zG@4yBFPc{+M?yg=g7vwiUkJ-pMV9;P=A7BGY+2SzY_gnWIcaq^ZzJ^fF@mYJUMmE z)HQmEC#J5Rx>_%>dFrE6AJt22n)=ApM|OXVEj#$(sSocKOhqxCQ)Ak>T)k1uNhx;S z%%4)PT+Y)=JUMgj%(;4rCuYu>IY%$CdFJeyv-J|2X5KpU*4-ats}9b*W#%or1;L(EJbSZV z;>p=h&wg4j@x<&+vp4A_HqU-)_EUO^O|zez{p9We+-gu}KQa4>-Gb3d9Zs`#CDscF zD4NT4*A3v5`QOd|u9tYye24iCy~GpdFPp!tm)LCnlKD$|iB0A&n!mXFV{Fwy^X=x_ zR|Rjm$$pvTN0uMyC7!k1XSq)=@r>pBmhbB&p0<3?^1a|wE#I+x zXO(!%m3Y!(wOI8MPgq`UdG#vsmYd;;`6K3!So)svli-#*Pgzd3oUHe2%v&C`JgAp= z&hmie0lmZ)%l(%7$9B)rt@d>Dhs__R?#K0?i(fkbKl@eJOXq*OP_OV}XXh{)^3wT# z?bmqe{J-{VymbCwV<<13|JP29m(Krdrp8O>|Fs|ErSt!u)%B(G|C*Wc()oX_2Yu=M zzh-K@bpBuSF?pkKd_Q|Nn~K3&0nO?ySLy zlPbjE%nk$3l6M$*X}on&`?ChG(ljt2b%%i`$$B(6LDRs1)E!;@ZJ zwrd&~c!g~Q?_ul0?`I9RX&M-Ku5AQIL5~KaFz_bd$TNFA8oW%?z`*1E z{R;q3oZ4Hh*G3_S1OzeVWJ8Z2lU7^nka z_POPhsB0Fw>(nI4&BUG~CPMm6|BzMt;O8TlCkGifEB0p_+A#wxL z)v#f2Qi$BWA;Q25ZP*6lJ{udhfw1I8hP}HKxH|RLmNwo`mc6!W!Z1Z@=nvLf_U_jA zQivQ}?^0g0LgWzOacyd%0SR@4(K;azXwiua4G+rTfTJj)IR^7PXviUC)8Q^#P6llI zY@*e^(;jFf6E3OZarsN`4wYtby5L($x-hz*!|3Ix>_8Y-E0wLLmN`mr^Fa<(-*ooN z!FWlbBCwcsgp+D5@*crA@D^Fkr1imwjJmEBB7fcPLgWS}-hMOD4Y30L$D63p#k-@l znBjW4ir=$A{qGmYS~pPS#VAA$+1(>}qYanJl}u%s5sIBE%tjNXP#ka6~7+mi^6?Xw-Oupz0Zq`b9U4o=vUDCr6IfGXb4(bbk(6)U{5BE~2Ic86S8 zyc+fQor!EbM%vt|lCOv?Qz@uV1niP8V?*jawK$ApCAPxAy5M|9U03b!Y;lkq+Tr=# z@%?)%v~CZ)*;2V7Ai)24J2Y_5w9!(z2QzNCjJ*h}sElDSo+=tCa~-s-)7MpbbrN;Yz8%pkAnfwI!reNk*^&0EhP7 z>`+ST)IwfYPARk)7^o{YrcaaD`=lgYrwl{dwf6^I!p8-DA@3#@?>2u*rx5gq>&03YV z`FUvPwr5eScTbz*p$;9whIp$RBD1bRb6tzax*7iKT5mH1PI}?Ijoah4ZR%}!R0iRW zzh!5s9O!az1m&Stq(VK{D04Z)9%^@xG*=WsS5n(eiq&$n(CG?{sJhlS9{cPUf1^J! zCBW&ldv+G`cnRi*4jAoM+J(AD4Feb4eOf&awGfVEI#S36M0hz4;E*TuYw1{c;5j>Xh}}v0VF~Ab#2W?T_2u^x`WK{B9aQo0~Y*+qSxiH1^{=yo1^>bI$`6i@P69{0MFOk3|HCF4EcwByw2D) z!BD_D@9~00?YD~UJsd2&^7|Z$2xt4aleDMO z5pPCt6r%~rokb9j*umYXSl}3%2+9)5Qi#uGcZt$UMJ>4JNQgy$i3w*ZK!MwAnl5>A z@q(?12cuCIqF3}S^P{e-hWFOWfE*g$dHHc8w_5wn@a)euybVlg1LL4ojNuORA|2&N z(F2JQ#46t$&Q)-z$4Dc7uUBQynY(AGe7n#8!zJJaTx1~7Fnm`uBf_$J6tp!|6yZui_>RJTrfT?9`MqS`UTX**SCA|HISsUb>fY_t-f=^#n%9I$HrTC zqXP6TzNKQCqr#z_H-=YJ-Dp@`f4?=fjxDdupW_EquC*6y{}&ct7YKMZ4M_HrFcS(d zi%2!>lN?ciXe@_nn5!!my>+izXP9vSF26SwqJZ)~J0x6UiDbE?^xTsb5)SCujO>Q zeX2S`2ia{7U8D-|I^2U7%1pj9+>UFNy=|^h*R{phyL<69FkSbX;qC9m_n%`N{|gsi zm&3LB;u~zKb*7LevaIwKkuIe@kXnwIPc}UXTOdG{mrHb{M@hB@6jRY|_i^!cHaH2- zE#t{hl|Y>m@Nllb{lsx2=t)~=^{rcEHp%;P+Mc`7Jl05( z=O~`kWQ*Qrpkz;1NovG z-FbERmU_vg&Y88>T_havm;GU~-RNa=3S1{L`?Q8RHsD&jhG}i9o8urOFxx`g)Tquj zk?CyfRG4j}|7|ODwv7U0b@j|#-=-y2{9`lx!tRDuI3xR}CqL8`SJ9Bt#lUi`86t2b z>aQihI;b?siXcbXl^(m|FNgY0kcal!AR#u(X6ZTzb#pB3DOHjMSF?^IRYs~J?m|08 ziEP{%aW=A5Fz7+tU^hyvxHDp^7|$U{PVq{-!ivFWfQdP)-hfmn_d_Lp`C6l{s|I;q zbMV@sL0-VAm(aDS9I@3%-j{QBkZQYEiHBkV%vS9+2|Lscwn-W(6bqOi6Ac^WCYR8l zXCs3&ke$3w2B}C?6bS%ytq-*ccczJCRgfqvQDDl+RM=B)LB%AVRBF*|RNUAgjVO4! zYIy5*m3s>a?N>%mXymQ-dVAjM$pB)F)7y8{Wr36|N?0r^$cWztzuMNV+ zTF6LMZImFg8ZX}MN7hK)C_p@18TH44*9L(1IYK^c@*nfw~He5jlV>dy?r=i(p-NA{9#6q8^*mR?G$!p|S6gDvYZp6C@b-2z-n};yuEa zNeYg1I}yc$&XT`?uP8lSUe45dF|eg%LgyxMhvY1IofBj$I9439PPLPBlHY?Wrz#)Zvr=IqshP%;aD)F?r0=x0fzo;+8y1(~I{let5CHc733T*`Fo|3J1 zlZ77bq$l65anK|YXHpR&DuD$D=ZF^istFK%f2dpXMhaXVqGP?5;&MCr@olRdkl<-} zOW`Dy<9unUL1g-fza3<~*=D!tk~ye=2l8G<2Fp`7XdJn2Jlh3b!Lr-h@+bkGDTpqp z)ACUT6`iox>QvHJR{=<3ZQIm^8b`L%>p;mW<)CA}n!~H|#``|V1$#>^91bLeCgOLH z8Gi)~%?TPurY8nN*$$i)3z0h80i=qQXF>sIm1#Nch__M3_-rl^RBw5wGa3is>IZp9 z1q}&zyuBz|tW%AOmkddr0_>(j?jqC{tqPsVbouFPG>!xaQ<=I@EE3*y$J%aMyF$5> ztf8@PUlP0l${nnsaJ}G+VzU}pjRWf;30DGiCPDyTmF0@mtA?Et9d$VBLLyjY{CqG= z6#QXldO`zcb6`nN70$|jCX+8j$~n@X;G%rjn$3pe7}#RNJ%sK;KsIcJr#0Lnhl&Il z%ync}6wOMF4A9Hfm~NxrM!k3>?am2cvmiu)ay=O27B$==M_8c>nS{M8#ACjS)QZ$= z$y%)e_)`I>m*7Qbi}$+n)vizw7vHD#JRBBfIZ3y}@r2D*;2@tG+UMacti(k{=3#=X zCh}r8h1$7Q7(z!V2w*YARsaMpjy6jPkV>&myOm&QdsJ!HT1ZkwAnQxXC^uQr+CUK( zFSJF$Q-PvvuI=_IE+#8+K-y-Pd5Xg$4tK@cXo1y$<$+ZWht=K#Td62rciJF2M1>PX z&>bi^Aa}yc$BTJ5R19Yz7ZEbOQR6^7{TARz5qR7e&5CG%Y}vC+z{+y88lZ5I>71LQ zJF*JRM~$x{Pw9sUTeOba0QAGW%rL7QR1IjAWu+d*g0_Tp1H;Uvv^Mku!qGVOm>ylx>UWoK*x!8--S&yjVN7-E<7r|0Nw>?+x>02@P8%JM6&}f``g3 z+}884F;0$Kxp7?M$OwfbE@V+W5$;Bu%?w-@+H^pQI^#jP>bBYoiFjA)NSSbYX+gy26C!F3>pC#Ab&IA6L&7>Io(0 zi_rPHJDaPiw|%XGTnMX1s!{TS ziEfQIX>-Z(oGG(#s3ywISE9ZUnuyqNe=nZzAZTt#jaJ1B|^zyD;+0$tYT?z4HIlQ8ZDdnnNYq(r*F-X5#p3~qz9nboL z!!Nb4y7U1e$g$3hlIDWGmaF5<)XE@7D50{07c)*e0;hzQwHKEu*c8$F0W@N$3hBCx z2fXTrY$a@?Nrl3xRtB_{+g%k}FCeHO&gyuAmeroyOAsFe(Y}vtb7Tp2(a8wfi)1{G zuEIA9O5fFD6UBs8MaVVZqP1Zs9Ux|96Y6XYWSwp#DS4xzR=yGrI0%{R(S=OG<97S) zj)Ym;=sBze(M%R-#MKUqB#iViXRPmZM>`^-_+r6A&5EV7vWmZ47iZ$5HUL>E!p)8g zjZ17y5ZiDSXf-orwUt6hTZ~e3J}|4G-hDg5wi$Sp!;_2E9bl;5toIsrG)nNkkTc!w zQCX_Tp+QgG;p|}!T(NNzR~d!P(?-?^Y~HP!Qu<-@CN+3tpw&Wfyz8~&w5;y$*dpez z0)8T%cfy`vCGS#!P%}wV>}CtFc@=KX0MOo7G_CXl?U|3#IGT(j?5tI6l+TO1aP`t7 zTvIYlB58AUD7;p*fkitXj0CHMJad)Cp&y#hRA1%L56$;WoZr-nP=!Y1bS=}Y* zhZx)q;HV#B(2~`((ho6cUhRi|h{353X>I6-7#uras`+E_0ETKjcv%l|y_z=dL7`1_Q`*-!zJ2xZN`Twz0t29j5UQk3VL5%hb%|{gWS= z6eeFgZJN4w{>Jg_`0=J&Ca#~VPwkw(YxY%(S1qgfJbs%gX?kR$H}S*C#i=XIXO2Cg zHo0y7u(@B&-8}bZ^UuwnG)K*^n0v57KQeXi)D@<;%;qLv zyV*u)^pT))qta1IqKda`ud)Q|dqK6Y@$W3N~aVbM|WVtG*C{hR{X0$^u1i9T%(i2J} zN(l>!O-B~wRs7hfx6o8lP#em5+d(qM`zv1RfMX4RT`e>M6=4Sp*Gp;26GaMD%AIiY zIU-hV3i(2(o#^LN9d}-ddBG^OsF|w~t&TJNbrp@nfL4^o!Ku)?K_6P^6tg)X5)VRI z)EC-!G97j%^HqV#6l?*7Re)p!jA1g~M<~eZ4~UL- z*RP~QY10!Lhk;s> z;oZnA$!To_lKFsQ57`w~NXYS;3&n$WH6kR0`$2~g2Y7EL=@Al$`fzbvL;i(m*;lc) zMLSR9B-Rt6DVB7g1-~^f6lp4*&q-a{<|nK55O#P}Yr_Bsrg3KpgY9JE}!B|Fy*%AeMq$Wz_NtzO>8L?oM)q){y zzwHoc_3P#WtvbY^k`Um%a0EvJg0B=TI#CzmP9&O8!O4&z!4`3Yhhmnq#{grU)Y?!t zdSt^1+U*{};ciFah%91Q7L2FURH{;N<9&`orFMq`p+mF{B4mK0Ww;IA@VC+q9*}{r z>sdVIf8s1zn$r4?5e*S3v~eCt*lGk=kX65pry{{##w8@nd4GV2hbTU(CcPY}1t=i{ z99jS$s(R5LDx|wo2~C5xl`0V}q$n7VGvR8a+e`IuC9amZ7TYQMKrJY3gyYp(KMb_7 zY?$LDjU&_Ui9RS<^g#;kNXz|v(I??R(Od4}c_rYi7v%_H0~ry64qNqD1~`7Cwc+Ui z0giANn6?t@lAdG=mbyWxmUEDvMxGLz;b;#7@oG9FTi&E`81QkZVGcy&FtGdS8|GNu zzmj~f<18wi3&<5=b>HAlu<;&}=^!;M%_FWb9Jk6YyxC!~`GMAk0gg9o90t}kX=!TT zd>o?Rk7}`I$NBo7`l0Hl_sLgd{$we`dl?U0D_aFAz$R=U)Gp^7HGkahfW00% z){MC7FasYg6ILDk5?f(Tms+6%hrICL$si5m6Bl5CKvDGb{T* z*a!B>_PO5szt8XP=h5#yWagW)X5RUxeRIZ=NbsoOdj3&@-tv_KGyfvNev3~mex7sv z;swh*QEmC2W#6&}@E4t+w0H;zEIciWpjTKYqSHj1MPR|du$liXyd7RjJOs~y$Mat0 z-O0Oxdj>Bfx`rnMD_{$cpngPMMU|+NC_eWg?zP+o=TVf=x&PRDP+Dx2rd89jv|~O` zhH*Ge&8Ar!YgbGXlip^{YsHl`ZSd*~nzYEvPO_D==Y$mO()ZC*RZe49LtBltR)%)y z<7h{`9;ufsRbx(VRF_LZi_O(5l}*ijPqK9JRMqTF?WrpH^oQM-*N>T|;i$CyeMPrx zQfh+Afw*7NYkMVH*_8JarAkrhcbanpiHS~@L!#;QNfCU}@bvR#S>J_Wc zP|!=Q5gjA5dW~sQrBsvI6V1iD*)$ZvXd_RzG*z2^FldX_aivP-O4((SaKoU{I5GuU z&{tDL+!2$GdU!R4yxBsIDO`zw#_H+SblpIw>}clPp0M8&QWYDmP`Tzb#gn}*cV!wQ z5OJ0?&JL6Bh{euQLd)pv1GfwLc``R`K4UAMYZrPJdne;{38w403cpqy4@cS^ucDH$ zsGC8n$%0O?2W7^XlxYACe1 zcg_-Rdlg<~Pu?xYUGh>%C%S){MuL&5?ID+}K1fCbW~Igw_ce1pjVoOB=Nl@eAsA#@ zPEFm^VMI5wF_iWo6I7>rgLW`%F`89$QErge@*+_dJ-@=5t97MvSxO`N*fd6< zpVq54TbMz(ku$3tYKPjN&}+;(nY0rk?^ul?=?8M%Qeqz~XMu#{L>-H9HiKkR;SvM)+T`-N|?K`41tD&V& z$Xot;+NWs@{Lw}{)zkSIWwxeA$7oYQX+f#i@E>89bCup)LuYjc<6^f_WeytKiLS34 zNe9d+TchJ_B*L~(*GHFQ15Y)Xl}wd9E`=gcw<*xl zO{>?G_H^lnPOh+|YWk4VYxYS~j+COxn{GrX?KY(&DbaKdv7*saFzQp{K{MwHG(+v^ zK;P?B>u%2=#bhn@D(8@C8n!@sP(}_sv!z~MlhCLXVWy#onA0x19#w8tuF@-0;eo## zFp<->9;LObXwh|hqM|4(Bym@QDcbz%L1|EtN-MHP!|&B}3c-$3)%Af@nznQumbTbc zv-S(YYDHU+x{5(xENc{3TM}cyk+J47g`zLr6?ehv7q=u#O_$inFg?ArVY5hGbla)$ zDzuiQHf&A1e44UOsZWT*3KVm)R?q6pWoT#39AuK(Zi-a(^*}%qY74wHAM$am8Df>GpusE zHTAA5?Y7AbV#eLd+X`Ku;Mdb!HIXq-*XljQH%Vp;dK^UDxJe9R@4V>6j$eSq2*Eo9rKpxfl+2Q=;&}SB$kEKqUCAr8u}%* zCz4g_RW7GF>+Y%xW|`V+R5X->zK)T|qqX=z?-%E-y47trrM~BN6za~9E#0Hjx~5#_ zD_QeiBiaP^MXJu8UShKZ>kXIM)dcEk8fXl*W$36eDhW%Y8n>%wXv-`5e8*6!mx67( zTjm^uWzLwSX5+7NkJ4wN!@h*JC`o9MD*{?wHWa&gYkg3Y`c+Z0&18|sm9#?T(7_X? zX((-U*MmN8wOek4RC!ma?+9lcQKwWp@aIG5_>`w$%Xu;>yNUSOG=`#`R_MERL(bsW$~~G=$|6g5^4&trYa(8n#%Luq z`H;~R4-{;UY{RVWWeRq0DPSpkGHz?Bs6|IT>8LXrmxhQ{!>5^4_{>bA;v11?+w~jmKeBwnnp8P56EMwhA9^ggcyasql^?wAu(ezOQp7y zT9uERwPL0gw8r3V(-`%JGvn79vN@BYMi-)kg4R%`)j4UkDDPPe0c**wGFOIjN~5GvmO8`ou3Hs|clu^eSJKNy(Ka?+ zX_a)V`&^2gKOa`L&@tqGwq15dim84iS90p|)^^P}C`laVn7Ct>R|hT$?~~J#XPt_$ z!w@!>tKy;}hdl49-PR7>FAv1Es3oMVW*g~FR4ew?T;SJ zo$`275H2yb-NA+oq^4fX<)G3XO2U>`gHB zl(;FZ9l<%B+^*Cn%FSFNGjn}uI%lyS&N&tRtWMNQiQaC%TBb7^)owqSr#~on`{DFy z2K!=0%7it}ro7Ncm&JOLlsi?(Hu9BJMrxt6KD1NlN|(h!cT*%|V<_$In$=gP<%znX z(@Mt`vaT)evZ!;8w13b`Br*+)OjVQBn3zs9UF}jt2Q8_(*pw)}wX{oJM;4MypN#a| zQmxu#^CKo!WeOFgET(sf4xHwy84k#rjIY&l$RdW8yqvVQ)wY&6=B-=e z1!R`y1b>(oP`XJ)XHH}fr_R{jUUum_q6rGs8+wTraN_jtIs#tp(E90Vb{ahs3a#tDA>NKPu z?Q~nFo{83XZA?b5v|4kt(U*=f#cs3UEif^}l3kZgt&)heivl0vByJ?&Sg>jF_rSRL z)y4PV^ZE-fvS;StC#5JNO6l33w0u z5?Uv?5?;A{{<35FV9~EdUlNUlM~Zx+O~T&`zbbss(hI`Ku5W|Ogl0?}n%s`gOwU~- zt6z2lH8MTxrr1CWC8xR4)$?f2B;%DvvQ4$gBrVc1bg0#A$Q!K^gV-*0R6-0QueOd6 zu1;d-Z=u3f>u3Y5XLV{lzjY-1#k3srTY17q*cclc@u%Hm_jQ(qx9c~nw7G%4xmu3@ z^;X3frr%=&7O{OAV*_@`bUz4$F|8wn-!nEWYutPpEjqy*lLson3 z24+Fi*_>hnmP`B}v|Kh|BTSc-|GBo#>eAB&Y@JnObpzAb!1R-EVAXUw9pA{R=|j^r zg7dGMj#=hY?0&4AoKoWM*3~i7A9lB{rU%iL8=2)zRqei5y+2zH4MFRj;}}1 zhN;;iHMvu{e#_%f`eH_7Qy&zY1RtN4!)8W{+m#@L);Qa~l(p2)#@X52ij5il3F{*UjS@p^MChmKutYk`b^u4@0u2p$e z%0{YdGquvjlr|w%Xw+TgX}(cnvZbm`tJbxrNuUCDWVUpfsi*?Yeoy5{Gtq2I({tA1 zMM*keplOY*FRhAGvBkSGRl%i>eMFPQIjcSbs9Q)y|!vERAuS7 zNkjf{8bjxFJ6$Ft@;07OC;VCsI-Te>l!|q&#;!9qy5)GP>d88?b)AlyE^jNe6}dfN zw8*;+OR!K3)1_*avDI3MwAr6Zc52~#&y|(b%uN%scpgE3)8=2)|G$uLu7T+n+Q25q z^oW>Z16vlWi?SQovX~x$*~oU{>QdMSmb_ON0XMMZy}HTq|KO7Ml)yH!jZ!cQbFq^YV@$enve*-AcWi3RBxj$bFuB8}|}Y%RP&G9Oo6z zot!H;8IFuYkoS;Rf=`eP%H~G@rj@aQLjbE&n+;5|Ehta_$iWfC1{m(yX*o8qjk~(! zy#Y?VZkomhIPoLX7#ldV(4Ov1hB}Np8_+v+Rf{ef)4OF}xr(u)1;CP`WoP28cHX1$ zXDz;jLOD(1|8|Q1ckdK$t28QPUo;}(1hvwD& zjoqi_rfKWiz?gl)YK#qx*>5-fKW(b&zhe5oy6dum>Hp@poBkg?&DBP>g{P`^UnJhs z=E??^`Q>b`HnPmWx~;TOo-^kMS zYOdzr;1F@Ax!S;D_y29WZ}`a11{S;j(QWz-EOxIhEB&`@?9abLz4x>sZeV$Q^(epw zmd983f;O-`zB)F{JX27)qUj+tT+F+977hO8LX06A(pROrkA&h9us|3ln`wf z{zh04%9mbV`ovOxNi29?@E?Nn1Udn4@dt~SE~*z^Lv#HL7o_}0`RDS_fWLs(LJvB_ z{}G-6o!kF9HK5eo7r9^HR=7Jj_jAtWoWY^UYe@rm0yMy0D5>55hI}PDhH&P=`2PFC z*?ab;`ZkZF&7Q`zh^b#^uqSw=_MjdutqvDVJrN~9?D`~KTi%t* z`UbI-y_)i7y8XhM1ZLZn_JrA}=|IS5Dc5YxfI+Vd2hlm~b)tsF0^iX%RWY$ZYzgR1 zwMGd!WGWTR?m*UE9;}HPL_VTlZw9kj$hTUxlh6ab)iT}67b>~%@w0jzy}TtIOqn9+ zi(A~aOm+4c7p{o8_3C!)D(c}Vzt%F7*0~s0G2%xD{q3%1e@%X;K6R(P4>_kr#}o?{ z-Ep%jJjj7+IM{A`OmxAli?%|sc~l%bt3uysHcd`fD%lJSg65hnTAoM6F|#WC{Q!fXSr{)7L%@dR2)64!qhh8!N6Hjk``O>JE{>X25sa9E(NHdHjwiFm zOseh-%#({FXH{5rX16}su(ZSRcEphDM&>HW_E{B~w!zU(c3k>E)}IQRz0LF-xd?7U zDh#`NPq0KgJbj(1)Trh9{f5q`TccuIBB678+=+x(x*lOCwj!;s)0WK=wmM?zK)W{x zdrM|?#&@mOw|p;ic3*pKrw?pF3FvlZyO&RO>ULwchwc}|z4cCM_ADfh`bM_bBiq2{ zS*4zUHQ!<^p<*p$V4{ducFXnabuqh{zc#1ZO-SkM^rVYqQ{jRxUQ0G?#j-IO4z8(+ zS#C%iFN~7Q%L>Zf_oC&(CcE^0f+3ZAox4a!bo3~=NnGajs#vaqDql>A%hTUkqoyPy`iwS!@Qw%_F%+rwRrGsEjHqjE^RRx@=UMS z8uYWq^=eUj&}`JUz2&e(gYJx}V(_{>7UNpBpKTo4181Xl?7mpp)AQ(^U9)2lW87;S z>ul7>0dGXLZDE+4$rKLR`;}ZfSl3k}Ylr07s6q6wEgGd(-#f@w8|>=`yNn#|{%|~FPcSwxdHr2o z&+qH39Rc2y;k{;~Hq(7`Gnx+B>;DVr%d=6F5xb{ZXxlw*Pp9aJn2MhD zGAx>nsyFHa&OCaF1zn-(8G?quT2t~(sS0PK_PSM@#~ZYi?Y2@dnq+#0by5v1p{TDD zbul(*6f#A+kS}+=`gYg38&AB>sq6BVU^YMQxV5IQx2?vkT~{n5yB21h{IF-Hr*w>H zrQLJ%%MELsj{0MUc$a3@a_Vd?^cMuaV^@oE>XxQA61G$dwPK+V@zwfW_u9ID-Q3%p zNAHEaK_%-=FQ)T_>0=_hIv3WX<&lcTAIOMiF*Xxsd>W!$Iwm6f{w8>(2){2=@tQ2!t z)@w1CVmA9xq4ivWZ`11|XEmeiRJ+c+)s7ArIZ8eA+OPb2yxDK2*B8$R~EOSIe&uxSN`q%ck!M4z2QUfgV2k1^RMA0s28cPP!~`~bMNK$xdi7O z93y9e{4{9-zXaETGLR8}CccJ}nEu(aY+W2n5V$(?xniu_?G__;cdpwQcv@>M=V@NM z)kZ0kK(AY7&>d2G*K9&d@2XZjfyz|k;eNpGw76R#M>CL%M%F2|HZvQwI;g~xhMLFS zb{mts9jwj9a1yrnjUYLL5}cs>t%T7P84-@afR5Dss!6;RMgpnUgZBQ-tw;M!PE4NqX*Z*$gc)NZI<&2)0jvd&(2*h6}QZ%yWA zKkGZrL{TjVA4_;}6CRi|Zn|4SH$@6AeXkO0xYtD8#&(2V{b0?HZ`D=s+CUR64$!@1 zxmKc2NLR}zHjwqzN)}4Z6N<=1eH>-Lg?Pn=GvJ{0rAWwEaK=Jao72Gr*IJ(2D%HxK z^Dv$Czh&C(Y?jX8ghMfuuxS^eMZ$w(i1u0=14A*}52nhFHHF(&DzIiFp60FBP(smJ zwI!V;n2y=nE`78a_FD{|-J0Tz+V#wZP-IqZsPC+zrG`P+-0i2!`VgI8C%5Z$9Bmk> zeSLhYv}j$=)YDt+*`&o*Ya7-z-b1rd3oVDyV`x=fj5%Zq8e65c1)Xhlg&>N`62d0* z&Fek%vX&#$%5^-xOun3JuC@E$y1Tx~-~HP83^F`Yg-nL7=mNWPiP_#&8kEs0Pay6o z==-{O#<5-jcd@+Qze@$qExLmnG$6kmW;>It7+Q>Z?JRD#ps!~m?_*1h1|8hWlX^oZ z5G}UiA(OY{wOBiA&E>Z#@msG^zIklYd3WU&7ue=1qz5_15G>NgR3%0Cy=xlEY;JcI zSm&8lVYS7BwBFF`)LemDx^4-tH@DiQLhIh8f-P^?s>`GMpG>k7wCD`!(7I!kYc{H< z?yZGOhHlxB(S=IgMs&S_M(do7T5l)g=)iPkka9=zU7aVkmP=;Ku64{twYQtSI&Ctv z3Ynz06shNH>r|W8J{vU|>4g2^q%m8H_4}5fC$nC4?X;n&uaAtDLZD}FI^ua>*s5n5 zVeg!y%6coyJFT-R=!(ULyNg_X2Mvw3wU@Nl*OcpR8s2(~Lpv?ADd?Gj-(&3=byIGfj zU^VHgdRxj+cCLx~Hkn&*6xgYsO~Ip&FvV=ch^)z&Ih!gKlJljo-l%wiPT;e>-q0(p zb~l^}^|AxEEpKzgl51yhZrKT&^+z|~*lyUI%(m#pX5Hozoo&5!38m{9S3A#`Euq2Z zaQZ^EKrG4Rdtqa#wcG3kkMd%fM5>iaB(j85BUMWzVyR|LMa+IB?dh{0*wT$!+Kp7- zRPHn7vNdg8?*nU3n~fTd#9VgdS*W0Q8#8u8+p*sA>dsT&RIXfDABi9jJ5{SG-K{#) z^L20OTaPPKBWmXr(HRlz1W?z&8e&=JVdMfTUFv#m^_VeLDbG24 zNY^Ds)ocp%p5Bsvo3FnfF{1fDsFTkS%dalKu>8#Oua+NM{>k#a%imwVefg`)H!pu~ z`BTduTmJC!2bM2d9xtD_++MCM=azRa$Cmx*&c1Q^)MeH3iOa_f*FKUPwwAOI8C@k`b ztRkK0B#~Tnyy!^L5uyV{dx;j%D#TxeFCsqxPYHh^d|3En;a$RS3%@4(lJG|1r_uVv zM}(IN-z%I5&qwzYs=~Z5DU1sPLMM8O@HC-XC=nhj+$ua&xSvoUB$r-UdT#00OOGu* zxb(xNZ!g`t^o6DCmOi$0#nSthCQIicUxtOHolD^*=aPO&vvk7J_N7CX_FCczUPGrI zo)$bNctCKM;5NaR1vdz;5qwy130e;s2x@|?;4JjMp;d62Kp{9zuvu_`U`aqOzKq^G zd~)%T#d{a;K<^&jw0Pa(Rg0IS_YdE>*jX$srjTC+_o8v}q(upO5Am?YeHQr(e?xv2 ze!KAa!b1x`ME(|TS-5fG+J%pzHxe&gIB%h`kY6}wA-G^)IAcMDUQFD&aL|Hify;lD z{~Z4*boSyth;RUb-i*P3lLnqWj4LkvEhljwuAdmMN?|I(SyvKMC z@b2Q>#``kw2DH-iVcsRY32(rw@v^+Lcz&LhcN$N@JC3)RcK~mRM^Z0S&r(lPk5Kng zcTiuWZlbP3J{&KnE~4H^b&xN|6cwf1lo9!Klu$=ehf(_=-;RGnCq;hCeH{6C{2}*t z?k(IKk*~*(axdjx$UTqS;O4pKaD!Ys_YAIzdpvh5_aLr_%jLYvd5-fG@&|bzx+U@r z&dr?bIaj0GA{TQ;oF1owZjHn_UXGb_3c5XV4Ce^Wew;;gi{vHL#0fa6b4U_=8hi?l zY8ZVITnk54jIITrfTIdVp8(guQ5mCaz{laJgwe;r)o{dMbT#-G92GJ87`O_K3K(4l z{tb@u82uZ#5{_~hT?zgbjojLrdP!_f&Coej={BQZv2fivOgc#O^jaX8w6Q5?kJ=s1jGAPPsvViW;kI64NS zFbKiX(HMn55RQ(*C

JRr|NZy}u6{=4-@Wk5FMP*N@2~P%NB`&1PaXZ4 zqjw)u$DeZb*WXn8@0ETy?OGn)OiTq%C!^|ozDS@1%Up4Z_OjNZXSP-t$|7!1n{+1| zBi2n+ohSs~R@Kh{TxmH@z+0!KMuK_J3F!u~(eo#mn2+YrF6r5A7N- zjY&q!M_Gld#m>6O8Pp$SITXbWq!s8xLO^`%llOtL3}UaGQwCAnx13MXt{wYJwY45i z2re*@UUR_uMF!q4v0|kyr}QeTRTIVjxNk0v_@+H#2pKVzXEBK;e$fHT>+y10*n~)H zT`fyoE?ET`8P=l6{?wl@jrh6Jh@agf6mmd2Z4b6Kwc&a`_SP$NP>-wATAd|?dNIHP zR&RK{aMn8d%cT+Dy+@Q7?fWGZn6(x*Gn`F&$iUUOzP(;LiQG-iG+{@ms;B*hACIkB zdqsSjvA4lfL~9Us5ZTGQhB@k{Ew8!F0yUi1x5i{!?O{L($z@TJy7f;=BlgPzAS`%k z8+#=@p3~QoZ6}oMp;b{ZR82)KBZ{Tz#ux*7)p=i=DMD>9@0=q?bnZ1n?h+%2Oetp9a7Q}d3$Mgk}2-~1Df}GxvIX8zm4#X+-x7(X9$>5Uqfy?V2T3p}!t>shTY}{D~(` z8w%ABT0s&r5~IwpUx#Iayw=G%ke2 zC|=BoWg&d>y*;ZEh`Xf`cg_&Y?X-^tw9+gLW07fu%S3a66~O)%s#SY#(5@l%LXkE3 z$2H^!rYCK5nplkHzUi0pDmltGPPV=g|GLctxQYp(#!)UTv~v0k*%| zW1^{a&_Qg~S1~bJHFY^0v^}Zyw!K0xUjnfgQR4~lJ!;c1W^IpIRom045pX?kGJxtN z-F7Jv)fIBYPN3CDJNlV@%S#}Bx-{aa_K1$M*mhUK6dU%}ZrD;3B;_`kA9XUZgKyY| zOesx*>f@4k@BO6_Utb#Wr}v2VSmkj$ccQ+D6j{3AmYqd{2%MYH%f(!&0a-G==rqx; zcV(}h%a=gxHGRt_o%wl<)kMNta9}??qhW|#ZyKSeF?N=OHHi`1gJKb*tuY)j_xFmhJTyVT5pf^4 z!pI7H&8j_B5t3;vjc73Ktfg$zA7%5=fXJw;`_0lah@aTEyo8M(2XA1;D{o;Rrhm^K z$K{ua<{mK~$~J5E6JY^FSIA9kJ}rE(EjITc%gp*jV=|o%*5bNZ#2W4^#X?2nR_!Y7uXL=iWvM#KY2v&)OqOyyASz zF8`mt@^e>S_@x)V^#$_<=>C7X|99_y)BP{K|1NM^|L4Fr{V%@v$M62$-Jibu{=4ek zhj)JW&QIR?x;xUHKYIJOZ~xfsliU35PX}l9f8^Fz+#23``^{gw`NKEA>?V2hlfW7N z@4vCV(Y^6;*Zui8&=zQc(M&?QrXq}yP>UeBfR738D zkPasT5KMmZ$9I^1dqig)-GppK71Fhk*;B6RhJ?wii>5XV{JgI9S9815%qm|EKl$t( z26A%6s|vADwCP@#pSz=Fd(@p(840CKw`~;>L5EDtNg-4DD)`AC+hNcWd){9OKY8a4 zlfjxDkK|bIb9ruB(qd4h#7wIXi>}}gqQRVmjQVB)w*2DCd*CMzcNn!d7b32=uYyRP=E<|OfjT#;to?yT>3iOQG5Pd;mh5$%CNwi*#VB^kEv@e907E!#^w z(N-Ltv>^;{)-ylaGzM7Z%it$3?l4L~O2Y(H>uND-yfkY-Q&Z7BkkWz&C`aa`g%EgR zJJSZtR9=RkJlJ8zRZ=BKIccIDF;J%i*4i7P{3|WbE0XrV6bY@=s5LwoAo6s5wc1SKY7Ov(^L@+Qn?2BepXHT z1F*fAO0W#K=4?%Kx}6%^AjfIb=o+?G*}zXebBEbrHiIYJ!qKF#%b??Nj@L;>A!uPa zj#M%v24*06RE)S`C54}S#txH&%XZ+?{l>7J!x2V|Cs?>%8E(sgbTgVS6)A{Wy^2t> zR!QI|pT5I{y%n+s?{1PWljZ>1`l}|ouv15k>t@&SQ=gTGn`t^|%&|%gKlvj&%pjPI z=j|R*kd(cWDSWhIm;iFL71(u&8AzmMzOn4I#k!4DBKXOt?J%p5=i0oYCz9I}hO>El zgJ)i5Oz}nv60-C^>=RfLTkVP=00(VdNjc3aRoYt7floHeky zRZ~~Z(QxLBsLBd{^7b7Db5N0MTiq(uU$z&6P0d7zS*Ob-2I**dF) z@RLv3VKUZDhKOymT8b}YbscqRCkLwkk=I%BQ!aVd;Re`3^Zhw5#W1G zOwRiO)f-x%62d}72By}d4K}WL@RLv4VY*(@pytS|J3-U2xa^8q-%G3PTErB)UKdS% zMt0V7l>((NDhv3@C+;xHOjR4}F^Bh+p@D+$0d>^Egvb}O^^lY527)Y`b-m||eWc>T zPd;IXsjBJBM0_`jwQAql#xALj6r7j~G$bv9$Pz(+$H zjru;~D*miNU^8KCDjWcV*s7TDllwc23dLOw%W=4~sKV>DYc4k$>3fT* zNW_w)s=kS?8;!A`Z^McKKe@NVP;fI+jjlkMjU?)${RT`!hK-EOt)(_=KIr>x9R%-o z7J3yOesXt*=^%9mZuBP!B6h&Frc_4 zhjbd<$e{-#N=!WlA%Ycah;}guk%H1)p;Zj>+904RUkX3DeQKx4s@zCR5gBAtjOwe7 z-|kUXD;ifhw}w=>Rvk5Q-`EEFbUR-N11N4`mTOyUJ6s7wFHJUhS6jrD zm*6Kib{KL}9khM1XK+;0ANrvl!rWj8HI_Um>o5>bV$?}s z4jbb|FxZ3@4SsU<6tj(hbi2DDB{l?!7R5T(*EJY?X2M&w>8jrtPXd91YCN>0Dk}V> zvcq8fjM>(pHjXvA?wVL(@yrfJQL?HEi>gQssY#&Oa3VQ%sG`76j&>Mdn+qLKVxl3h zNTEOCGo!mkhOxuL;iM@rFw<^k#E4(Pgk6#0Cs%fuB|q>Jtv;OyQ>6v6%Of%2Ot!un zSLe1L&?{*LvVibn>_c8ff=}MN!>o`ExP2ibWP5A*A!3aiAXSDi7pm!zCJb-2UJS8| zRkju?B7E|m9cImt!@z=C>uiS9f~|n}tE@9;@Qs{}y``)2 z3o;qn%S-^LDy=b5;oy^EhpG4LRd|c6q@m$Apq4>t13@HFVpeaV0v_`E6r=RPxYZP? z$`C%ucbHV^jeGM}1|tGmjpDJ*V9;u8mJ2fbu@N@I1cJwP%FRaw<-jD674W*!%eM3EZO50y`ZVfp0y8SEdu%4%=9z z1D~vR7-QK9dV@L(;k<4vs7PO<6N44Tj%iJJN2Tp~J*~}piz$m#aQGzLVbBs;5?=(L zEO(e3gmVLz`_Zbkg2dTI>L_U18Pux7F={T?Qc#P#1yrTi6QS~j@JX=4tfH`wyVF#x zk@{lO8zNwDOU*_K(Ka_G55B>Ttyr?`gkH&&FMv<{9mdfceKl#QsMEmHDl=i7p&BEL zVXbK*Er!>Iegak9VaFF+mCuJyyd9<%fO3pHjVvi88`hh;iAQ4<#e77bHs}BWr!i5C z#)cvtXqC@{PZm22TH+h|pMp=^9fn~ve@cOrOfX1hF>mA`V!2eO#I0xnNFc&Y3i~NE;g~qMWwoCR5K~LT@SJwpD4tC-WUfE!db!yFA@$LMlbJ zp%j!Eu$W+kf?+Eo?UCMHdYzT73rGcpPplngSTDfG1S0M$kUSXji9pV)&T8hwE#ATX zNS_bAGZxFQ!7&FRCMRHrjsF+8hoPfF!h!>S($AZ zgN1coV_Qi#t5b_CnwzvT&!+;0Yk`R)Lb?bmFnluGVOWssm8N-pJV=Ecd|%4tMJgLj z+;giMBJ}4uyEN)^ezF<2DiD0~r8`WC)9aPTyZ8UASHmlJw>SSO_~&CkAM1gS^}vU( z2mZ8vV(iYjeWM7DyI($rJg-x=aP7QvZY7TQo}Q}x13!gZB66OmGI34~XK+61BXFkf ztit{Dl@j-OsP8&-U$S=US^4gu?z>-p5&SmYfaCF|o8)Ij?q770WEFqz(Ik?pGm z{CF{fSMv!IE3_16JKG<|mVNo|1mTDA{;zpZZ=vU2Jxsh){CJbY#PF`K-s+)76lA#r z__^8zwNajqog&dv4iEg;G)fTb7${d7mb#7;f64~>8Ug* zUkPf<7vrsz8bhQGzAwe3MkV2R!_}6fw_dOwq*kqvwkEb3gi{!sbjbz8DqzjWeuLI0 zeOK@v@lDJnW?^Kmj(4S2Y1^2p;rAdK9z2Vn!Zx_G;v3UwM6X9z;@|tWPWyP zX>T%DekfCAqXoU;R2dOlgX#7-O-~hEqQ@g?<8=H-wAnTYCzv;3sV$ijC3DPMKmXrY z*0Y{NJPMYagw=2~MTW*|Fd*up$79UWfLel9h`59m3w{W`R@Avi%XUG@>|7${8p!p? zTaVI#M1%6;X4q4he9>=pFXxB5rUO4begD7m@|BzKxN3uc{vdyzI4AR62zcN0sLWo^ z1KcY{9J%dcbDAo&Rlq9;>bVD+AG=5!Cy_M^reS1S5!lxO%eN+wKY01YS(-T0r;;7J zCFJRFd+mQ6=yDj~UWv6Ech9=co6_0=VrvN)>{gF&P`TlCm8W(nBU~n6MGs&v?@fYD z=-3T+ayBm_r+=_x6qdX1vyNq~VZIZROKHcHHM@66Oi-Sj9JD){cDThSaiA8X%)O5Yk9HDWLkDPe3 z#2#9U?hw3pMTspS6AN9df{0-2*l)3&W|MK}aBb0qbVj<|iuAPUz|G!=oLDz4;VU11 z!#j`4Y*Jokz6;aDcTWe^eCEg2*(jTa%c*WTGk-n}=evRO?CPb~J{^qbI0f>0r+Bf9 z{@3olrDZ6~PHWHl!|P;b0D-gj>{nj1&##?WKkb8aWNu#L7&}p`bH{DE8HwRI;82}y zIAaZ`5PVV$*Q>4%Qbj--X*@TeCCFN6ZTBj0=fBPEAH@FJ6ex88$DSs;T>!X&LJ$JA zpmrO5RV?ReRC=+@uJ!3s;PvkPZ18$ca+}W{-)psA8D$_??lw1egYXTa-s|e$?S|1o z)P1Uk^i~A*aJDvCNff7xl$3WJ`ekAw6I0Z7jzT0!W3C@b(v@iXz|>e0`Jgt9kyLkIgXM22Ti1rzT#}=Y)24)-c>BjL>YU#C)hX z_IgC@G%$~yfcysh zu{j_`?97vs(6dfgb@2Wz$c=#m=dal>K7Gf2Wz(V!dg%3m{ZcY&=Y{I?=cv6KUNlFw z8qJ+T;<@uA$V2g{$IVVk`LlXMRR`gKS+KU$ZxVH3Yi);pY^D}WyFfr6fSbJ0qq#^e zRn@Gm;*!v$qd|h))z=N}u@^NTvtyJ1iUVz4We7)KkN8Gy7WCmqwOTyt4InD#WHcRY zTSKNU!aS4)DbI`ZQIOjdv)j%9bS@R#vDzb@9M`qS_MLK+gaD|9cFs! zxUU3z@FC&A!w-cucio*lGdu6X(6dhYmK1v5^cClI#b5tGJHLM?yZwLNPH+9*t@!4DxVgUZyEnq?|NeS#?YFOa$G>&#Uj4UM z?aFUdtfOB$GOqj^0Dt43_q=a%Y*|8?te!F%CU3qQ@SgW+rvyr*2$e|#{hRqphQCA^Bx0iX#&sr5iq3*Jm*I+JS9*fzw>!N0=hJT z=llo;r3pOeM?jqtC=pzq_ah)n6L`*#pkJE6bAAN9QvxNjkDvD=AW9Q>&X1s5n!s~@ z1f5d?C2sn>9|2yPz;k{C*eQV$sp-%6Gv#~U_eG}!O607U$?bmgE!g+G?+Z&4D3SYl z83JEWnm~zU)yok0{8IuYjsQI0wB7f-@AFC%C{dH<&3oWBcys^sqd$A)#^=BAW%qyo zesb@3@A-HC?OpTEzrORuxBvNV>H6Qg^^b1+%&p{Nj5(f#H1QPTV~?u%2pcM4}riV z_-3GL*@$!K^_I)G6vQ-;`nH&3ZP0^;$LT({r8-;efzFDmD9uccai~aJF$H(i#(onmO$T}_}EoH(|-Xbt1Nhl(gFuD3$#u7XJmeOk=% zjqkJuZQSe)0=k9+>E3=5B6KHwumP6_ger>yEo(r&+Tad2-1^z49QcmYTCX)OFI1y3 zh^VHTn4thpbJtjQWU1Y8bt|>e1r~)518G={HZ2lzHj`!qjxtpHT)n@UuQ{uiqeIi7 z>V`^z`Z9#@faLhxneYbfMH?~bQXBPv!?l0;lmlxqotZW4SyJ25W)T`>bvt*RemDd+ z>=x2W71wFaG)j92&N)x$Ig~c(rOf&3c7lo^GXc_56mD9>H7SuJ$mbPS5rhW>hL0Ae zmTGChI72a`W(ORue(f_H`nnr>g4Y5v=fh40l$WxBfUgql<_N}N<1W| zPH*HjB$5xKHlmx04!7y|Hlqk9`mI{%hAU-5&z1ZYm?NIEu8as`hLp2giCf69SYXDODAv)EWIMV#$)gmt?%0rC^6 zQq@g#)u|_9otv9g=%J8x$Qaf;2s_l^kiAVi&;42UU_$eQ zP1Ko?49OE-3_A3X!=tAh*lyU8>upr>#KnBwq@zYr0}9EQyg>3om$eP1ffKClTooRU z;_en7neHqXYi>*|aenKGJQI+0$8jbMmD__c1rCUh#@GWfVuV$f>5T|_#1O+wA0BYH z@y@S2I-NI<9?sYr*|ew2R>aX&&Q{X2JJW{3V&Y;_vhg)8=}rWr-7t#yq1(kbtympd zsQ@>zQHD$$2}|NOww$P&jD!sm96H}1XHkersbw3F4{d`n$MEXCae1cAp60>g$*OUS@saltqzT)urOSs;%2mIAT`F9Qfwya z7TyB0hYsWNMzv}<^1$eON^j+wIgW4TZ5$W<0}97K`ji7*bJ-2QWbt}1=JBxXa4sIxyGZ_-%?Vd`EC-k&}>dZ`hj!d5Xb9 z)vbjOyZHv39F_-*IO@$%rpI7OEo-MUkM6fJbZsvJ-_-=58>0~qyv0I}nJ@!6kfJc6 z29|xm;m$Kt7TIeJ@MsYSGZQyds9LY)@sKk(Mr@@`Su4;b)DVg>#S;&Chf)b>6Ts8@ z#M^WikgypM47KJ4Xs6vCEf-6!>9bSC@azX2Ac`-@D^Z(y&Q|IAN#}sW(XT!2haOE} zVHMq2okqBx4;rDN2?da9WM^WdJ`UN$wV1xlp=tb3M)U5Vy6vrd1U_GDm_s82zhRD- z2;X-#8Xu?dq*=_2ZYO-;XuLRM_n>TZJLT$hCioKxAFvLmjJQVXDCtb> z0Z)9WE%RMFvVlDF!wEK?<*pZx(E4V>b%&Fcx9}y2+f?Tni$Fp(gVj#TV`5@I;EQk_ zvZJj<5NmgsE8^i&?oRTwADW(NNDUm+MQJxfgh17Hu^rB_;apHa`l4#FLagv@$6?}! zThx!T#j5x8>byTFt(iriRY}cSQ!s!lhd>% zmu6Tl6zdF>`Z?%_Zu@}{Epxlut4fDjhfkym_Yl0kL7!xS4Ab?gV0-PsiMN++@)-E>1-I-IhO zzI2!W|Cu}ezxM#;zXE9fZGhTe1a$tEK;drzwEgBl)lUa{eqREV{3bxdPXX$EBcR*I z0>!=o(CX`5R;BOxz9v6=xA&fW;kDg6a~buaGO9xl`mVSg#Pq}w6cYC$g#%lDpq z!Ja`0Eu)9xs5075mtoM}Gbo`bRYpnbG7MUK1|{^V%IH#Eh5@=~P(tmhjLOwz7&P|`N@!=5 z(agFGgT|gg3018!YFZDl1$fKN)PR>dQA%!SgZ1lTBC=;tLZhpUHrHhs)b|WZD1Mbu z`nn8*+MYoP{jf5+VV7Y5?-`U(BP*jqb{PiHo@LILkM9|jNkc1L;;zp(AN9Q_e{9d7OcGLw zY?;gS!8`X1O5FAN=4HS42lu}1u6^U%uRGTo$CW!s5}@d$ z?@@iz*IlelbqN~Wv(N(Y_Z=KIYD%u|u6H3@==V~+h9t}a+j0^Cu!A`x*;}^2vY|v2 zYoTTzPF>dbd4n`#Fcc&v*s>$i>03T_+P$ysvd)OBvlKWs zPHKWX)+J?%jmMHzZ$^=`?TFB@k4p80-DlCM9bWm?kHRjKo>!M8q^0y^0fKw{xJ~FC zc3=_4IbGIGsKBSBEgy+%do1b7u*HRVYo6<9UqyjZavyHG3T=Z5>0p%o`H#XbdtQ!7 zHU@fc;Ia9tkj8P%l1IL@l+Wq1PPgl<625Kc%!cn)F|`Q`tP5=lZ(xts1xwaVNo8T!ljSyja!&I^;`^z;kb@4?vX-2c&gb zBEf_Dag*VY1RsYnA}2g-vt5HO`{s|rE_+_1jDSO4#$aA`2x-AK^x9}WIX9MdM}fF% zk@bj}i))WD20h7us(dHrhgvt7*K&HKPI8zwSr5drKljnsWzTKKB=NegdclxLa8sSQ z;$l!^`sc+mJX_SlVaRV0-NlEaDiqg~s=G?)relO+pR^sEo-Sjz>hd6#eek2N%bwqi z>C7fllF}ZrHpe~HA}6l32+xUSm;s4A#}f90K(@2~*rV}i?7$Q00pJS$T?ls!J8}C5*(yOZ-`z#p`;NRMB~0;v-JeTvTyt-?6T)I%DP@QlLzz&f}xoN zs+7sAHAp-;XOwj_wpZZ8dRku+DAZqDouTHDTSBA|9h*@~1oEVmW&jE@Y=C2a!$)D4 zJ-0;$s9@65Qd!iBu>|)fg@GzH=$tOYskZEOz4g)?8$GEIs>-1nHd&`D6ZspT; zYuYyE+LQxb_Gfqb|NW!yyz;`|2lf99Q2+1$0rmgB5!C;C1abj>_THbnH@)|+yZ`C# z&)xmt-7miT*>`^L&QINW|DExjhqwQa+dp~xYi~=p-*M}=Z~e_%C%3p;pLz4&-Tbke zUwxCm`A2U2+Z%u5##h{+Z@m5buV4S+>tB9-aQ$u9{>`-?xK>;vu6@$+uO9#4aemx8 z{`jl^>gxAh-CV`4-mm(l`p8=I{Ifv-+8n=Y9C#{@{h0lr7KsC{_%f(Z~o7; z7&(6FTIGDLi8_iNd~-VrKJZ)LaHaD1YsW7gUm7&~=f85L@;_Z@r+L1ezjvXXL(r+6 zU%1dt<9s`R_d+{|pi?`)c%dESd^%j-a=ZTayF?HuaPoZ0zX7uq=lo!a@a z3+=o?%=w!a+BpQB+W8+Ygd9GcQ^?=AklJA+IO~ERz0l4f=+w@STnITg68!Lm)DE5c zjM`tn(9R*~l-ggr(9WS#pV|4L3+)_&PVM~F3+)^_^_iU?ywJ`e=+w@Cf1#a2r#`du z0~gvk1fAOX{)-~P;UL(X;Vy~r*{6*g?0{~=$W1Gy3o!c z=+w@CbD^EX@Ns76J1?|z2s*X%-(G0vFnpZZ`HL6YIRu^B`SuI#9EOiGJKu4kokP&6 zoo~C)&SCgCv-1}&v~vhLweu|(+Bpm#XLi2zLOX|`Q#*hDqL_2|ME7Qxi(<|pWM^b} zQCpBVh&kVUQCnUII&I50U1;Ype4O>!H(qGx5Oiwi&s}KeFnpZZ`Lh?=IRu^B`M`yC z4#UTpoo~3%&LQa3&Id2Fa~M9(?EIMv?Hqzm?Y#d&JBMz4X6Ng7`Txg9x31j%+MAzx z{1xDr|5|^(CO%p0su6sBdh~pAeJ)vzpnaaCaf#HJQwiY`0x73|Un244e97Sw*+b_Q zBzaZ2^L1r|{(!0}yh`9eI`kER-3E~6d=r7)T}Qq};as>0c0d9AT;W`tHCb+gF}>K* z{K=@+kf26SKmv%4XM8hZ~i%-PMZ$& zN5A%MC+=w-p!QLjVzh6b;C4y3YyF{gyE1X%B{0C=TnG4B$?rcP-+ME4;lstw=_r52 zF_90iV>X~SF3>g4p-1DHqQmebmgo!FW);pJP3*_T$d3mBWsre|BdrM@cyGNhP+O5i zuhEicOYX5qqy3((MvG8{C$WO8?5zq^t=A3NtPdNQgDZ=KD#&JNtZQ92Xx5t}+VkW3 ziqWwEW_q&T5CR?5@i!bJeQ_ z2Y~YapWH=_KY3PWrLntu_Fu1Q_v!7PUOTt;7cltrFAinOKlhJ5d+&Y07r>@*%5l%< zfaG2-{3-EMygu7W>!1GWxrX=3@1W$^%-FT=^vYS~0npZ62fPw(cIx&Y#$^Cfhjuky z&Rqu7F`v`6Pgv7#42N{a2t|@%MJRZ&R<&YNe4DG1{5_!wZfO&f@)ooW^Zrge; zIQ^WqdiW!zw;sB_Io~`M!Ov`oHZ-F@a1)pzYei!qPRydOM0ilxQff(S-GI3f6xh(L zCv%+$yHpu6c-M3gZN4Twu}@du`O)*08N6h)X@02DW;e^8j*wUVz42HxXrR7!cYfr~SKZ<7d?wHW`0*?M`N}W8@XNQq?zVFK;jRC0>!)vh;MN!4`kb4; zfAbe^e$!3;CUoQfz41#ozU_u{qj}@_`u}|WFI`_=|GewBfnvb-TuUlHR{5H1__dEa z{#VD}e_R}s$De%l*RKBct6y=Iz53~u->OKJ7mt4T=%CFiEs#STYXmetf&Vq@S^wu1xGopq`KA)R9qbWGl3Q?#w4iUEt&9YB}N+TYh zBE~(UX0+r6OO1#~(6(AsC**pyH{41viCT*#;|tvsR5?zx;|6rqI(j%JxDDGji*B%5 z;s^(~)kZZMY?-l9beAj*f@h~0&D&}2KxSEhQ5K-@8_~tby|K$}dQ@T7r|V^J)}9Ku z+Mwg8a8^~7p6L#Q80{##_5MAgL)aX{55m=mXp!^Qy6Y)uXPPN1O0ihJz#?*hbhmK} zV{R~e1P+G@TcKRzJM;MSBD^p;&Y_>#+T$ z&GP&1fRe6dA@wUZ^alU1| zn|Nsiwnvm0?eEzms-#Mea?(VzXvE8rWE+gC_|wAQn}#|9l!5UhzAwt~6{)V7GDMQMav8sY2_YcY1X=_;8xThB>W z-MY#rB0g)1Fwt19r=pM4i-8a41L*2!l}5Z+8u4I{SORe&VU0N`KgK7avKH9ocsOX+ zmTTRt&Fea}5htCD<5$SX|FhDFe_9&xD|y~D^?%KXgVP~6xDUS z&L%E%*)|pd6W#7DC{O?Mxdmb4@Fa zQ1=MGQ>@!4zw9*@t=W3i>ScW_1 zfX*(yvFwO#@W?1&m1J1ktT#TPR@c5<9RyuT7qk2CERA?r8gVw-jVd(mZz!K^^dNgR zrHR#QAmK|}-~&Y);d<`XA*I?ekoc3%Y?MHhw{hlCy6$W`zNFMUGoqPSvwlskIyF$} zY??<3>o=yV;N)^vqrEFXwQqR|#NXZ{W~$m)19`;0GBi*xp4Cwc6Cz*C)cBw8`|373cuUAHavMwf4%|J3D*twf6T5kawekRf$XR z>dUq?qD8?(bsb7ow3y}3^K?e*U75Vs1FvOQUNHNBbBWZ*sk2p`N0uK7&HE zCZsoG?Ru^Qpb9BVYIO2di!Jx57J#G-N!ZH%Gu#IZG=;A-RzaE;fC`J**D2HQ$Jm&qm&hO|h=0rNal zPBt*U5X|_C@cakCZtIefcU=Etf}77#O!1vc&gceO;e0o!_pDCo+ZbmX3@t4`Z{q0_FQ1s%J$=;%-v8pAYxeXr0N(%S7tr~e z=TDjY;Ka-4;MsT0K4M6DmP3E`N7_aWy??Wex7)lxAn>`uxcA{0O;Enf+P^2jWip_$z z*@eq`KU4Qb{GNBttPEFs&DgQ;gtP8*ztxCr$PYLzo9h*zsSZ*X)Dk4ph!6s>xl_X!W>VAX(G&S zwjKghcDF>dnr_wfh_|S6 zyw-vWaH_;3J|h8TG+&S^SUeFVYnfii8UfDv=?D10F=eXjm$6hhHb6Kfg(XT!s#=k8 zgR2L8Vl`)!!er5(#dfC!Lr@$dStn4k6<`=E8-p7T&XQg>Ntz)HKv8FLK%tQ)m0ZkKDe_I?TBl~?28%)t(au=uuTH3=_2QU@1B`25Q&I%F zf;2Q=&o44NQBkc&kz_Y!7D^K7fPKKWI4oNGeXxiO1g=%FnP}gV@tRU3n@$_Dz<$~u zq?q^siDd0o4Q0~3-Qj^5>Uum0ipeC}wwHxDxnAvVe9Q9PSg+SEq`M+&VeM`d+nGs3 z8@iKjYf&R%_Tqyiwatgs6AthJZmHeA!4{hhy=6mOq*%4IsJBs#WrA5G+^8T4zfbTP zinLpaVF5eewd>htyue1h`Ea?dmGmTl4ra@|j>J7-UoDX=wJO|Bq*$-AGE#2yCZ=d8 z&la}%Fmd;OA55NXFjXEZTJV6DVp)HNiG$Nau_opKW^TPxR8fd2;^CfmceI-bLaiDh zQiD!Blao@ZjNHuoU|nQ!swfe(pD0C=5S9pGJ1M$M6xf0o;|!wQYo@xzZ9dF2_xsR> zShd*@FdKl2Xg<5o_sn{|(&>9uv6zHqEUx4zPO~|j*_}rnqg*ajDlN8Pc_FJpMG3(Z z>XRLcYcgSg>ugb`(Ur3uY$skKm;sV$69j=9It_Rsw)rq!+wX(Uq7)YolYt`I&>&A# zs+ZH=o>i*&JK-QX!Tn;jSg7wAfL zrA)wp4kKI%<+^=Z+TBwuAK(O_8x&ghiN#bfEqjo9SO?`R9h5LCB@0Ho8XI7f+)gaX zrf{y7?>GaNQWGgv*)|>~?mfT<1Y_!Qw89BRw3VQdP89(Ax4fPd9^<-9umL_pq^zPg zEbVsu>7ZiRdCS|V7h7T4qnP;uC5S|iA**(?rHE3SG;4wiTRT(5g36N)gSSx2wAHM` zZ1Z9Mif3Z?-aI#;L~|YNngu8qM_fzBqNi58g`O zn+bb+0oH38e7n>xdWc3FP+$ZG;LFHqN+m+5@orj#cMFi3^7-I6k<{BXnawtxN;y&L zcWco^ww*5fo4%@%3br$1jM&+-J3Urloce&o8W_&;gKa)c{%XGu9==y>0Tu+Z8p?E9 zu-B@U>MYdB=38~b$&*np0M)6c5!;FHmJCma;6zGk(jKVPDf#<>SUs7Jhw^$>O;I>F zsGKFW1U$sm-Kg7i+4j}_S>4G!O`T2pk|F>+fR zW`DWg2Z6orY9y+FZ@EL2 zJ+(lYPm~h*Zq&&VX331%!I)Jp22`Ef8Os}S(ikA(fG@X_O}o0CgJAJ<;9mK{BYUI% zZ)q*9J`v>qKQ*&>{^ap9Puepw^TzdJ!>vn{vpFaJArQOpv&0ss`W@8K0#hX{2w(_0Lii304 z+RM4+XD;2h^wCvx@qFOm!~bm_dZ48;SMtKO5Z6p30EbzzVk=H`koJ1vb_5!z5zFA4 zdc=-vwY4i;4W^2kh7ST9K|U*ocCGv%+O7*iV347Uw7;38IyNCntk3bTe05VJRslJ0 z+n1D`fxlR?2K{`CwRoE9fODmAi~;mAv`&GIy~Q)n;p~yk}VaAIKz{eQCs+ptHFA!1%p;Nt^$Drh8WxyLntGQ z8<7xFX{%qSN)?`F2ybnnx2XZr`{laVLeMl>sup{Q77L_3{a`fLO@_o~C1`Y@eih^( zE0x;(V5otr!4g^}X|$zFy=pWRg9s|9Hp6h1V#>j?uUoGI*g`Vyow(5TDk2!kj2Km_ zbemSwVj<~;B1J?m(txanGHMYmTgi}IrQ;8ipSW0>D2Z8H-U_gZ81>YS)+Ou>cz?qja(@A?`HDjFn1S zvu<(B7&GqDrxx8tx>`s}4RTN~;P{(ZPM6_yB;KU;E}wPaV6KF;v7!>wOX=FAn}|Z2 zb^xoxJX#KQiuo3r?KL zElbg;OqP;l-5d1Wk&TgZTNAriNIq)=&r@3nvnd@xQTO-ULMomwPFVgrvC z?`3`A96jY0WT1t5hH!I4y7hkN044$K%$WWr52qv>c{ z()~gy;m6?y+Jqnv*-2uJG^KJ)MTW_qQ*1JAtkG*uxTPp4X7i=I-vds0WXdXlk9kOs z4;q|@@eXXcREWnZr>rn!T9u}T(G4>qQE&KyeZN1Ya-!2rR0mKw6fuhZj*!i#25}pA zvULn6=G~+n8sfVZ++V=Us5cc0sO z!I(kOHBmHkNPw^AxV z4x{Jmo0VFzURgm!8ElYkDB<{wTmflzNQ+iFjV{cjYc0&|H|z``RMK6iV&GCu$>*&a zTC|sCBk{0qVU)kvN3mwuD0Q<=N)WVYIf2>StedBebz~`%hVvZZ#A^j&(2=TXMr}4z zIXESdf}BxdFwdv+p13se+Rev^wbQwDHw1Odur7gWk{qkmDttcbOIf*ko6cvzDxJ*O z#Vn1@)m;rOfU$AfFNH8N=x+xjR6CUI=n)~4^2mIh#H4JR#X?#(Lr;EtQzP#2bDct} zzz%F9NG7FjI!=kYABqD?v0TGj^63gK!juOm}DoH3_0%MA89|$5H#^1s|Rs+u;a$5;sTe55z1`05F}OgWks zcT%`i2DLk(eyr}VpoRE6?`m+8TtGA=Ar!S*9&viUPPAo}3^Rr|`XYy#O^{cpq&jst zJmqGU;*giBq?4w<=i|G9rof>NO4j)W;feF5eAeT!is}a zsEQ;q@n$;cRf2G=>FbHMoo}?j;OdtlF6~`wyBcOLn{m)S+f8(1oZwHEJHcSSRSV!y z7GeB;!own1w#35W=9=n0MdPWvYR|%)2tHb&PS2O{qr6q567X7L<*ZGecr@Jb2y~As zOHhOsxd2+X2WbKeRjW~+k}@?T+wsG4rkCQ zj>KS|N<;>OjMwlMuzn%mG0Z}>9MKnEwCNQpw7}`8YL&PMoA#!ZoDz%_97&+Wgdr;; zR?Rd}1EYdylAd+zG0-w5;-=(nV>QhP_PJV@ks<+sX&O#oAc;n`fIMmGZR@{imn z_gP3?Dy3=7Qv@$~s|t>uOK23-lGtFm4;3d9gVjN*=QTrB!8`kp^0dl zn06PuC}e`72FENWAW#uq8>nV3k&wGqJTI54aJEMoWKR_&$_nc%ZcS}GAb`Y6ztxnb zULmA}Oh$!RrrBe7aLCxr`O-Mnp|bv3lSEb^*Q-<-&^A^Bc15eh7_N8*k{-kcwNADg zGox)!lm-Wlv2ed{hgXJP@gT)jj8Ptg7ITSoT?>_Kp)Qa2d6O;#GL?)*gZjgiWYFBQ z<+d?81L?|e7HyNYh>tG&a;Bw08awEKOrn#oK=BUK2y`_{_NN&^!M!jsH5|LiP!bd+ z(Xfi)Hpzf>XFAa7G_&B79#E04KbWt^2jIR{IiRoPU9XA^HRz|Jy(BpNKpK0Ah!n}| z3KZ{(eM$}X45FKEd7~i)N0#=w8evJWf+4duV4YMt%#cB+REBDaTtv}gQZIo@VFTWi zfof4^>BNou|3^>UICBR!|BQv+oym0!y={~63KV$N?Ne-z01N^;sdHs2FQ}prA7pKaH=EoXdRC8~+ z{H?1d55|q~MaCf*Y_FdIyg6-$IK_?Zjopw{*I^HYfm%^_{eQO{3zTG@h}bpC2%J*bSM2-xibcFq`8g*Z#L$0Y-Dd9 zdOjb6D^g!iZ;S%)CZN2|g6?zK4sQ9vblkRleyXqw*jSmii4Y9(9^9#a7C=q(>0&(CF}?qlbp?8EHP> zjv=^v3~EW>PHgB7@>sc(%_PXQk_ozXPuM ztVe-28}s?;BYSi3eC`{EFcU-o8Ajl=k=uOuQMes!arYQJm%yFVhwhyESh+I>>7}`T z8t`UgK7a1W-aPbtJ_gUFzJBV)C;)HbDI+)f5cByMyp|jL|LLghyJO0}1B!J0^FJ@wy)Rbr}r_#^-w|B$#pV)&TUzSimnE@?+4zK03IKW6Vka z;K(#Xz$OqyLAKf;`8I^3+Qql@md6JwLIAduuL6akkk{*hU}z(N4~tC`1_Flwd-_`k zhTFe?{~c_|a3l}{o;)NdR6(wFu$sA7gyJxrDfeW$kxEfHzHZm-e59Wb1gj=EWUh(< zfWicfY#_n#cE=Q5B(Mh`AOYyBGk1Zqmp}lDoWxhM0-}TewF?yN7elpVobIRVooK(C z9@r^8RMO0B*$jx0ZrsN+3BuCb1~n+rL=ghTE8tF!jb*uO)9e0oyBR}1? zpt|O)hTPt8cfZXoLwys5i{FiY|2x2yZMA;VGtby{j4fLBK~ukP`(e|r=Wk4nTM&O6bL}B!{cY1G zPBZvy_Id%@LEwabKySrLooeOXXn+HCb8u~?^QeGqT;_cofDh$CY0M?AO3 z8Wx&jWUH*G2Hn_O2%*VBB%jnBVz1E6R?8M9?AcRdI|&8A#W1=r0p*&Zh$@z2xq1=Y z_c9uM&7W&A`CuVhvoc6%4;~s3@NeVMH(t@1_2%X)df4RdF$UhFw#@VXuY5(vStmXp zjL&VK%7eFxf2X(gAshjp4XeQeFwV_Ez4cZG5B*kdJi;S=4}-zr-^AZeGqlY$`+0be zgjl%N#~LXj-)@CrBok_M>SPs7ut7^jy9xl@13o;GHI@<4th(P91Xp53Z^^5+3(DTA zEFfRlK?(7W`4N9R|0cvV}c}AxX^MVlARK>`o#y z;u>q?(Kqn_z;2J33{NcJJMLKUEgZfuvE$15AMa?+-@YR<|GxPv=Fi@ zlQS1g;WICo^36PF>hS45O@DrBV)~M4VcIu6F?IXYl~c{X@Ah=7_039)a3I=DBgnv@ z3w7QKr^R3>7-g)8gLwU_M+Li$nknQkBi`?=xtLL*9_Q;7(^OuU%6KW@HS?U@g0m*& zCvDwQ0n z)SGK3yBal5T*!uPt%_;B1^|vJ>y;4U)O_(sj4D*<6e>{|2OD0USv%%w)BdU5;R9Jh zU>_U^tbW=3;lTXA`@@~?4+oY;xIf%}^fUrY1uJ0HW>*fpMehE7iyN{;Io?*o&1Bb% z>rn*HCgNsHNL2&LU?N1*tuP-DSBu)ABXfYQt>BOL1 zn_O|7LdBxhEQ3Q?TNvPerLOr)YLEj%WRA@d?K&kX zYLnC<2TIlkQMwwgc^E|Y1O{QbSHenZ?~GfQ3oeKlxZQ0dY6({wH6KDQW zz&$roEC~PxqTIqyU5!u>l&6OS>Y!IFl?YjFQesr~(N3Tk_gA7K6haf0#5qU>nRDwv z!$FF+P{X47Y>uPQk|pOVxRwYzhEddzXcHAwFck8YOcI{^vODYo?yx)XqL(}DZgzbL zcr*Z`Q42{q*dy^izEI%+k|pjmN)!{&n}V$8A^{Yaz53KokDVsKK!z(-zSgYEswxT< z^ngoju3Z=7D(r73%28R5)+%;V&XQP3M0Eu9M|BA*N3CjAlARz=@%@gp{Lx+0NnqDs z(L1&ZhGQjmkkOC3&iW%g(c`33-kd)TYU%+SbHtO6qLEIhh9gjwVVaOg&}q8gT#34` zMN|n+T zmTz%&)#Iy_645-@uNMajV#;j=$rLJp=^3>ni~uix(QW*|QxCUs7jP=<9aLypBt5w( zk}X4Vsq0B%Y(!GDAl*$g&`{f}<6J0{BK4UUx~2}?MZ2c1alP{NTfJ(xRgHK~I(@4Yxy_!lVEn?Cw&JQ`+u*f9gU_DZ*M-Z~!_k?F=B!(l z7}5x-E47pY&iIOOp%;@`evUhV#H@JxG^ z`@_LA?Z3D`99Y8P#_CGiXlvJ>5#IA+u7W zkxh0KzLV;E9K0@bK2#?O)1%;ZCmv&XFSqbZS7Y@1_>-5o8ch_jgC3`@R`I5&l)Hsi zrY7|&aM;4K04%MUmB50so`xi8^6RdKlIh`Ih1Fx_fSr&Hr$J$4obbv{D-VS`DTA#g z_$p7Jgf;2Tp&_{fZW0t*-h8yy;Wr=)a=9d|`u(tl0DyQDCg6s}HzNcpP2cLaAr-@R zDN!;R8QlE?HNl9__GuQ*0Pq2_m?5x=jrXH^9@M+cU+!wuV@W8Goa7uBw)^h>2ScDyeh-dm^E7GuKI?6<-CF2_cUO&AeG} zICQ2SrC=DK4Ac?D7i+t5(5QgnT*Jp|Duj z>qSLu0e7FH;K2E6znTdG*aEo`MF_|Vg|cjrrC4uHw*{nuS#|Km%;{75P=m<&y-oy4 zu~8g?2N}V{dWwlfQNFJCTKQJTj3$~QRj~ER+lLylEL~+9)g<_Kwgk)6S||_OxnNTl zs2Jv@@eG*_SCVQQnR(Sv!<&N~a4)DH4^`k0U3793;>Tba3feHqDF)f0JyCE!(Tglq zT#cL%FL8j;B;yZ7(FE0pq+&5eDQR%8iJ)RWRiV2GL}F2mUi`&SBV>m%0E@SSB4!Bk zSruL&0#%O@Y?+ADb(}%GV9}X~4mIb7t7uB@db&D*u&!85sTybp*h0x`VmYBxOJf;0 ztS9xVA&7h(Ui!4#My8E*s|Z_6CmD(E%5on7=hX7uELJJz1yI-x*J4hgmSN-JsnSp* z8MUIVlFs^DVQ-q!sYoQ&AscxG26tUCFjNAlui4IdO@00ct_DVxygg8*9fdf?ki!`S zU|tsS9OyJrgSycO-(g@PZ?if*@2)#TskYqDm{_X>_b8KqiV0XvwJNZu=8JTiW~r$n ztgk2)3D!Gxf!juk3IG5oP3Z?rsS9U(0Vv<-B#8kgQGt3mT&$FRWxnhwNzB6T%>{QT zDN=zbR_vPnj@tBxl{8+j=hKu()78PC!`WO*t1^TX#^}1+L;?|@27pxw5Z0gtF5r+5 z(Y89(LANV{I||`;$?_Hpj19xcyn87sghr)W81)aZKmf-iyBXCpN;KT&yEK)PLC?`b zf(&PaG-s_0KOab6vr_%NNU#hp`XC)KEOjglAUY*Xz|GJ?DZcohu2%?$ zwY1Q{u~L%8cu?Le_I=GnI0P_U;vOHk9h75_Q3=!T{n*YvqHPTE1QzSTQL>u#!)a^juw^ zInV7tFXGEr(R4Dz$^urhoubH8i<~!A5geEx3$!Qb6_IKc9}wp(tv-2zoVa0PV>iF{ z^8@zt``AOFrT@e()02z5h4Pwq&^HeN1;B!*%5tyASuC zPv|jSQM)_w(C91brQdIMRLB~AMSY&DF~*8|_%$D6Mg3j3jWKp=FL5>g*RR1p?>@&E zYw%aP8e^=%-Ip^o`ibMJdoS^^UA4c?eU33!?ZXw^7_0VoyKRiIYCpx*_y=3Hzhdas z=&N?h)fi*d{+*%5=&SbO>@&uy{e5m5kLjxYF1L*_R_()G?PI!Xf68OOY9Ag;jInBW z+)$0NYX8|#W8_u)!k=A@u-?e4GE>r91(5hCdx=CI275sn6b-R8Cc(5d25NXbU^Dp- zx@v#f*sFH;!9Ce9z)JFHVnqSRxdtbi{<5c^s6=amzfr^oN~`1rcZMMtoBraaMsU#5 zlZ`AcN2?+%3N<`XZq;+i6gaNrw2Y_`j;)}_d zVBW5TYIL}h1yoKr*2+L>f65bTE_`pO5wFo~K>;||YR6yb_Ia?c4VJ*R*bghLhfgAr zXuKZ;Kn?L(7r-jmErVnhXZa!nZ;OkNj_xtRP4BkUfGCh#hy54_F=x1$yBxgY-ZsXR!oURO`fa(bagoCg3rt3HVzxr@I&cMb?jHvB2M&qlRCAF%I#5 z>Y6(E5YOGbGH@a9MXN+9iihK!nqn_9IuT~81Rvy(a}%f{$^o-n;w6R3jUGP$MgS>=l>ti z|39Ape?0&H|8xF-t*1%e$7o zu+&~U3?KoDiz^H7Stu_|&R;v9o%`L~m2>FqPiHRzh=AXjdD+Z~(|1k3X!@9`TR~?3 zX_KFrtWT~2!;irqyYFc;;^CRyfU7y5jfkknn`}jtbk`@qaXyA-?OLkg?-|)>x)_DZ z`9RO;fvY9pY;)?Ue?9r`bN>^0+wXk(*SClF3#gQ5+|WR8g>oe{M8$8vg6OXrF`|z zKJ(fi-FDI?)(!V}_RPHVgljH@-_-fR8`*tN-838<9opA5y!i56nZ+fMt{edoM}-S?DD!&1}i=j(}N(E#wRaJv@=c_}MZPij~q zBc|G_Qi9V0m%wYJV5Xo7vs)AeD&FjofoIu-(dGW zdDHM1`0TFXHN3y`uScxD@jmW^PvMhq{>b;0)a(ntZD0A^t3Ubn-+ulN|HSTl(xzc{ zbgp05F!PqXfAp5~XGGlk&~ut+e`|8-hxpPfBhUTj_s{#&f4rM7zkuEM#7)DVQim5{ zRW>colWaJa4-R%xZLXFZGy=J(Bc5&yn!`XdL*fb0PzG;n!;!%Ylu$J7wI*A&?)&QEYA@@u`lq*Hm zMuZzEnAbIY?VG7fZ%)toS9Up9@A~fZ=;Fj``I=9D^q5N~#FzcM|5dMM_x;nR;W3K3 zT*E&bR6qaW&tG}A_tf_UN-sF}sB`}C$1i>C!|A&(qMq`###5o^v-@^z8n)#|w-_2k za~UaQCRJEwRXzYveCeXZ=9nfgc5694rse2N*fsqAXX$5r^cM7kCtoi+=W*p9oPGq- zcuE{QV(|MXG)}ni^iQz+);0|T6nm6I`cWvRcp$@&g;tkKsZFJlk6VR8siX=aBM>Ok zRY!LXSMcc-!z;md(sg#pQvTApqf$jst#q-8E>FPdFxok8;Nz%l$y61K4KV(Bmd>R-xcKGbNvn%w=d$Q6G%`k22wcj=d(yLiveuinn?o7*(3^4@SR=+hdOKMZN6PDWM1 z45>M^)-eDnLBG)Q0<7h@(acM(#~0slO7e!czVu6f{&(dk@ZryY=ZEOkFJ5;6dp~*B z;y&cr7e0&KH@j&VZn6@_-@BCqjfBKqq%@NmJ_P=ixjG#q*hqkBrvT|H)vKWf zoUs}dq?dy^!Y}_&w7B|F_=t zgeTqel@z;gV$*PoN~<>KuaxNu3rHaaGjC@F(hrEj>XNGA?Lr$APeunt)jOPouDD-( z->d&i+;P|aAGq>{J*52~_nh(L?_5%SJ9)v=)o1+s>oIoyoK3@RxC4 zECxWPGcG74p_vjO$l(%}msHw4gLXZB+bi!p{25>Q&N;@Q90qXN`h`cnx+fg@ADz#< zSiSSB_k8lr?E2Z8h7BT>(L`_9=<%>mbLeQ2vCOE@?UmYGCl3)?J(P*0)RMS0Dz3fd zieDC?!oczv{p83M#Uf2K2 z+dnjj{ItI7NNXp%{-RC8w2|```ML!dVVjCBw98P2_r+yNNpu*1HBv~$bgUXM)C4(< zarDhL_qLy5X9?%OOqOzA|7z;0SDo>?Tkp?5O{25^oq@vT%weeosn8SI@;`t#i7T~y7#lMnJM0W{JY_EUcK+vlOM}G^&;o> zr$RqJaq*X@AHKKGv+ILR!($+2x}o@{_Sd_QIwA9cV}G1|#?F^esk1-bJm=)okJzFA zazVZLqxbA$*ZZ4>J8YtusE2zHOos-2p-7bje!vk~ZTp+vsxPW@7=?7x>2RP+xQ0JO zvrqXi@7?Ps-*G(q+&A_<=zGK4`mp}O#tYBA`J)%T=tsY1*PTtnW1v^MhW~{;G4|=d zwnLZS^R&=Q-}S3+z9k@B@!tCnf8u?wy1AGB=`&1ry|-z241`M8@a31bcc*t<^p0=c z_=)43wLkpn9NJj?@EzIKE7u#bpPc+p$FS?&O~YdVQM!ibzI4I+U-G2R3FVz9ocYqf zyzP(2{pU@GUw@Na`|t;T^zzUT59_e&olV1I;7+=R|5^Ce=YIH#+H>FXkyC%)xZy{0 zf4J%Xd#}I#)@R&w_-~)N{+!o+on5y#4Ud5^=^73`?fl$3UP1`R#jX=>jeY+sFJ4@X zopb)VXZ$&}XX4jq-SKU9-P$xf2Arg8xRpOO6noL&%9~oveN*#$nfSRM`>#)8`#$~6 z@4v_U3h_3bT{kxkmjaD)!Jol`X4(dMIYL*9Y`m-iN*$%;z@=~{6))#V8fkCM6_rc5 ztoiwSZ_<8o)yg%@#ZS2G8hhc#pWFB6KO2XmUzvC-m@AA;!((7Ux*p&1ALqU&dP@0c z)4#v(Eei4Kx##@x{*|j=c)Ic0bMN}gMOVG)PIkS$X?P4&N7t~~e(RI3{L-CI`iAM- z)4K6%?XNs_-`$s;`jlH={FnQ_c6IH~-(uJGO~c^+;HWofT*E&<#lb%MhP^j@;ofH< z6JIiZ@t%wC$=~(mBRuv(>fL|3onXyRK~-9s?20HT>2O&&}<<59hye{;%-wzEh~oFi*N$ zw}0~;TDqS2iRalDZ{+{s$=fD&T)iW@V|MNKwO6iDtG`;kXqB1!?d&(^j+(n<_N{Z= zLSp_o^ZNXc=dW8mYULX%SFIp`A>iZ7XD=t0XTa%zvb4N-`{IR*<;BAmzPWJLY;tyK z=HF&6naR&QZTdsg<>@1*egx|M8&ivbE#TtG+~i>sAN=nh3ry1geH;I`ESxe6@N1`b za!E#E1y*PSI>KZ#*D6L2xM#e97(WEgIuN~f{-hVo0zBH$l^+fV6owDXKy9=*A313j zfCi6l{sHtWYyRZuEI`B@UG-pWk@-`C;NiwF;ey-Fhlc?zwV1NZrO_fwChOpaS8_YG z>q7-JISlBi$`2pV1IwQ_1BySY<_}ChZkkUF`)gG159qJdu)juCeTe?T#tgjF0f0{x zzE%@0Yd$^f>`~hvc6NNLO}UqJXuPLL9XlRVai;&Ex5pQGh|Zq<&Q@=XsQhr9?S~Ic z(}u&o$n5vwS^5|-eoHy9M;G;Sw2>53IIi%?2l@8Eo4o;nxOmJ~Ka8k!a6c^W+8PBT zn%F-I7I$uib41kxC!@t@ZiRD1<%bJr(0gF2dSh@e9=$a}Ml|n^kj1060t$#@I~2Ir zTPUT=Xdj`rGr$U_8laVwxBnm8b!3tIslQT0Iqo!<)Rh{_Ka(7=Og7&Zesw-wM4 z%?|@QyXDoR^41(!!?5Ai><_krJ)-&j!Jhr@R#%UxdT>|Ies3!#BbsnyGW+eVZa$P6 zf1<_BZ|w|@^OPuuG35xw8v*^|+&&K^vG`C zGWV{nZXHqi;YN)=vVE$ad*@cSj%a>=x6ZwLt6N7@J-A!vZrbYB5ly(=I(Oq%K*@Tq zHV`Sb&n1eP7~hc(9nhHvzI`JqKU_e4!3QrTyxy5(w)*Q)F~5DzojH1|zeY5_zrSX7 zZuQrQst5Pi%&x8e8qtK?Uo%H-^%p_rnsuv2mjJ^nXJ+%lq5Esc8^*BvHUb}eK zVs7zxfD`cLg|{x8xj-!(JO8KoJLca!-=8PvkD2?!+?N3UUvDlmw{!M)vtOLOYPK_* zo;`ZzH#1+DdE<;VlbShl`u^$9Ofa?FJPyKT0GgFsO=~L*`;gk1Gerod4No_I; z&Jcb!am(iZee!Gnx3=+*Kh0(Yo;^cPP3_ESLL;Vh^$wc>5S%H6C$_zllgK18s_5yH zr;jRn+T>}Yik>=o>QIy;(@0XtH88c8D<`UEcAKw@H!j{gs^|@i_lzof{o+qX6@ACz zk4F`~Zt+K>ioSjEhog#KyZD2lXg<^J#5#z9)3k}l9Maeptfgly38RW0zf>Dl^th$! zsG`R%@uQ0FUV7oEqQ@*%Mit$)^ny`EcP>4DRMBTHJ#SReqnFMYic#bSOtbbI|J+siLqR*N=aa7S`XZMUMx@-1Xqlz9qd+bm&t8Z{EE17g1;I-u` zZ2Mwv=E|8jjVgM@%r&EmzG3F-(M7KsRrKFPc&s;tfMdL`xG)lBQ=rm$Xf;wn+`QXxTMiu?Q(yvDqegD$^ql&(7=~ttQzIW-D zql&&~=@+Al-n4YzsG{#)`uV7$?^^oVsG{#&`l%~Q_cM$H7{m)LCc_|EOWfXDvscf) zZB)^#X5Tuh=w-9Rx8TqYTsk{^3l1&%`q|-IaA?s>W`}RVp+zsA9liy$V+?3L9>vNH zZIHqMC+N1Rc;&?_Hw+_iNYQ^@xqej9b5`Cls_5A(*NrNA*2>$5qAe|BMiD@3U<)aW zcZ&6GkG0%f-U`+sMU~~PU>#CaULH;v^dUtX%a@JntF(OSP_)?;T7#}&Dk(mZYz_!} z+heV~bLFm4MQ>dB%BZ3@to+-kqSvqd>!_mdSo!j(qSvk5ITWq&q>0IlRcsezHkoa0 zkHEqy3-YL$iiQ`&QAL9b!{sA=NM8dB)o0Dnlao8?a)UIc>b1<|ILyUkvp{W7I$E|V@>)eV0g<2Fk^oO#&eUSqm+Y|86#E-&m1mr}3;b853v!o&Z1*c2 zQ{A8#Y}j_&I%)6T2XdJwZvK~`YrNb#35L9p$li*^+otU7l?A{^19bOxeS3j%U~aE$ zZ5n)Va~pQSr4PJS>$d5PK_p{#ilk%ZAkpNgK8~sym5~T7(zHva+G}pGKK;)+mlf;0 z5G@1N@V#g^W7Zi3iJ--jt~+Tg4wtA#UD{)6p}J3@cb&Dt|Md?Xxd({%pbZADLoo08 zHahANl6?8R;RpoOACgE_=3=UzP31c+s1c|7bgVBcg?hryMoOW$9`^d!2vrz(i^&%D zh)KS3fsWVTXp>N#E%Z!mPh9aDh(jg3c~-_$(W`-LX{2I*L*wLv=uNYgf%O(n5nZ@e>50C3&H-tgw5 z1Cp;HR9vtyvSKlC&^My>Tj@nO1&-i?9#7|>xJYHD>G ze`I{Dhh|8QfQLH78~eA2WFN}$NBCkxkw66gTQ7D)7%+JQE!Wykw^%@!T3oh-4y&_J zq*<0i22;*xy|zi^W$-2eBaei=ucpZ&S`NoM0l6NwRk0s4yl|oz$#R7nlT+GtCTg@x zc*vLZ*>n*x>&n2N>6f!MVyc*sf!Pep$UzQ?!GQOImUHx;9EJii!9%~;TaRvvfbkks zy*@yXylWiJbkNfD(&CX5xry&hOn+?tGxL{&8h>;lvHYdwYnFS;cOVg)JKLOPKe`M-{wX@c8YsattdG*VyZ(Tiem0CS^YO zfs_yPj6Zwy*=x_*qtEwhe5~!! z>{{NP@oMa~SC)_20Ez0#VrCYBQg*BSjk3Rwm7RPq;*S?EUR*EU6SG5hs;4sriYp|D zUh9l%|Ky=_D^*@f`k6!LR%*Gve&o=(=~+Gu^&NeVbvdcFqq!w*@3h^Lwsu-`(&c08 z0|y^)NSB(74jk@wINyk zwg_@KE=-(<-#q;0%F1fV)f(eFM4lwzwhR90@Soh0{^Q|4x+VRG!+&r~`uB%_KPO#o zvr0Q9x1_?3FehDZ2YNd_x1`;ju3J)L$8fL?>te2sYvz!XTa#kUS~QyL&tHGOThiyP zKhG`cbJw5imh?I6&v8ro?Dc26C4JWVv)qzCbN!iaNuRO)47a3DUw^t=(xTLK%S%bsZPjh5jT@Kl%S)|q*WH$?+_;}#UaGw9c!HK2 zVv-?DQOU-7DYgmD@fRep`QYY*4uGYkuiJc`ThiBVzSb@2Yc^lwmK58>+>%B%BW_8< zn_-7^>8kiwwtvO;|Ihe-Xl0#Vf93l6+OMp=Yc073`Tnc#cYXhp?}x7Z@s*#y^8H|U z|Lnux2U~-89O8#ZhwBG_aqzz!eDXj%xNrZT_J48z9sAAwSM49{{g=Jp-us!o$M@2E zd%J(W`{~{0ZhrTfJAb?L`#WFU`N)pB6WIQLw|{Q?d$;r3&)oWltzX#s$Ft6M8yU-`q8 zp9iSdzTj!fxkVdfU5G-vbW&{ddNG)nR|-ow2{_r5nBnulRhgaM;h@4?@2CU$)@`FAaS!cWBOi zN$7j!eV`;DotHUOXP;>KUg2Oh++YnZzAy9j9QMwgv3H%O>JIzariOfm(+KV2MsytI zY&#^Ulv8(bI>=PFK?@S!PBwVXKK=Rluvco=g-%CEvM9mt5O#a;C)Sp zy?$rxs>80_8GFNFm))?Vi_d<2b*Domhy84a27QWCMa@kW!NrHmKG|Ux?~Gk?)afk_ z`#EEx=BQKEAvvW^qN7f4cD}kro!;Ugd)N)Kv$Cr?>hvau-BG7EJEcD4s?=L;8G`v9 zcGxR-#{MR!L(2~P*$zd04>?toTvc(a4F?D-4!dw?>}7|YzccodV@Zn+`}vksaI}VV zsLs(E-qD(ZLvl)Mip~s~cfMMjAvs5@Sm*m&w5s5=k8!@dt&u`cC!v@bsl20Axf^xQ zXGqp5m3CF?tj6LS4#Ty;=vuc6(qD~bLx~k&L?20?=Z@4q|H#%zZdWZdd zwRq4`i`O|+=cvUS9JP3@Lvl(jUhk;IYn-nZ)#7!ITAUa8$oE>OeG!NG_G)ova(#`X z7U3Ip&(#8RN)5RwHE`>y09cEN!yddd_OQc_-WhwyVGrCHd(crz#9=?Hl&}wV)B<*B z&R2_oqZT*9y_};Ki0%Ks=EGLDKfdv^;GggOd`ANAB=D}`v1$91ea~d|$U{icd+}Dw zq^*+9a4~b38jI=P!V%sh_YI7;(y2CWr#3ijyY8Lmx*I;vr_4hL((`@3ycV$FT06IX zi#}gVn`|@ml@4%Bwp~y!a-CY7Q*Ex(?zHpD@66{bU>7Ei#DQo!9=4sKrrC~2veF%> z02Ts%zeAs|^MK!>&liFQLP3D!TxZTiqJbef{;Ec|V^&Pb$J3Qs$Selh=t%9?8ca`Y zUM&~;TuLe5vCmi2lmur%-uVhCe17;d6SCXK@ooD0d#M6@^! z71M1pKwYK7Ed|RFT(|{?iDTCqdMzF)MWe2KzGh7a9$P2sanBxjk5^ZZJgDoX z4ZP6Vf%otY-B3or(|YE$ZU1RuaLo6Y-=@Ja^T8Y`X1{54>_LXkMvuMPFuV=A(__N6 zO-sgvUCo{9dLV?_4;RkW^%y<{LEno|Hp>`dG||Y%%hy7aKH9x1c2q&gAVRn@8Gw`- zlj0rgdL?CD<7@3f`WnhSiTnTv}4^` zjh*T0kr#hvfZL0sx7|)J?{ChZJ3U0xzRUfko1IZ}px9D5yZ8oL^2p3v*m1ho$FqgX zg|qhJo^_^Ez-M0^fZn!E4@}HPQf5OPD9U@8M2%lnIjtJn7q-ZN0Ttwud)1S)VDGW&Xr>MR*lfG zmVrAH&}`vlSAxuJ#(D2^X3ohs)&#H%19;JD<~+IFsiB0!feT-Fq_XHZ0y8?AYloA( zFe(RfqXQ4?OJczQ=m{E(*jAFi=$UN0hcd4oTkq; zwmO^TX0bp)FM^j-i!<=8H)V742JWxyw^nwy+kZH{qMU6j?Jr(x2O0Gv5R8HRKu8si zz~F(2A6YyEn+tI1LU!>Kz-Bvx_2vP^QkYMW+EC}O$1W4e%|-o8vNwcvkzy|%0WAvk zTUn-G{4`_v_Tdb!w>=DISbP8fk`-~~%G-he{@Z|m{v*IIf3o+DJqvi?4}rIRfBUbt zds|=MGJwaw4&DK@f&YJV?Q3fd;McDJKYs~49uRM{WqkB;kmW>Sk33|OIr5Oh<=$ko zFi!U~6_w>v(YDDmi!LFLJ`S>>0015`&RjMCkTnGW@Q^^}$U{z;TMGc-L99m}QpUIe0J6Gx1aQe+0J6V$1aQe+0J6jY03MPO zUGBNkqmP4ZG9Ce3vJQZ(Gadn4vJQakG#&w5vJQYOHUNN!{85*!1Da<5m)r$y&j2pD z3tBS(9x`oRb{8}~1GwZaP(1^<YM1MZ{s&NKE9!Eymn(}{eQdivGtz-(f#rD zD{Fte_OI3^;GMyslg26#>J9fhqjVhOj>bkLm11n7a$Zv|rlL$rjH5T)_G_su!_ zx#YkID6Li(?J6ZHn^7i%)`Tk(V%bB<=zlG4F^wP`AX2oaO|1j%PmnzC}!L^1Jj)=Ek`R0~XKy9w%*MbB>0t z4!Xf;Gf#8_JgwHQh?*u4d=#8oeqA4w`Pj(XeA%3%R$%*~qMn7v__#lm_+XF=Rmx>VPj@4kurO>h z4O-*K0c-h+4o6#St_+9VYc|f-It&A9FbH0C6X%$o$e_Iq=VN`OiBM85>pn|iRKj^!3CPyF!`&gA?UmLHl?MoIWt zbxf@P)SN@;jr*}kz7~s2q(IS{R%@|Q4yi>O<6Lz@1htx#DL|;wry_fUIR|fy2f^B; z9YQKKJUJx7Y@Hb=gh8T36^%qFjWd2L1OlMesf~}$Ik*8{NY@9=RIM{f=BiTGXy;I2 zlF9n1jslLP15Jn|>dDclvGd9~Mp9 zJXRP*)~Gp0E=c!txt=-jrwfLJ5rbjZ4{J%?paT`5KPeX5eZK&QQrX59G3THPdOsod zat6XetpX}qZ6?_r)&q@LJWGiFNyuL>*2W`w#7PkY zDQ`Rdk-^%LVohdIio_|w!s~Jw#YUZEp~_2TvnljNzr})N?Qv-5GxIu9Q6f~P>OnkR zhT}{uFeUr#a8rwkV}fXRYCX+A8t2jqABWcOopU5RdZC;jb2Tbii!gkTVI`0wmkT4q zami?7R5;(~DN@kt)Y|TxBT*r`8n6!|lQ5!qCYK=0n4GEwlc7YIWjhfusPLo-Wy0t# z46P}vN09`dg5mnG93yCvm{te=P!0BnN61hfl$)JQz2E8-6%#9KY-?Txk*^XHn1m;F zleL&uD49#a!KPxSr>TI+AVyTkVK)# zf5zbm<~z7w#5+Pf74FluLT;27ppcqLj#M+=3kR$K-q5sGy13n&b3|gO)d#91$T}Dt zbhV-|C4mX5je;I$c&ucZ0}ewn8WLH3z~KmoBYBK0CMGhK>URPq)X;}&HY#=|T%;GN zM0+WoHsWfuyv03lE#Nzfoc2dY5i}UwyJuchI5d>7NKl(vLa9VGwKk(fHG~}p1tL(5 zDsVWS=r_t^yozspYR(b#>#D9w;YJ?yH$+6_s^vTp$&A$!Qw$nyxoPCFU_8@=4Bsc_ z9D#145^Wk$sX9nDb2M2};szcMM2!YqiS}!?Mspkz8F4af?CAHcB>~(()ff&m(3jQI zAuHy`YIv@B&MuncHO(>TiLrTkQuGnHbNst`ps>=&ASYI{DBgnNT=wVDr#aMdJ=1!hp4 zRH|G~^pC2sPGCfb4Y7xI@*1VJH|z5{4Bqljt8}iRs=4H_B@R;)|2R``mXWwYQ>Gu} zlNb}Rq6n3@=2K``ry8RMh)IwsA_B@;u|_JU!bUkGw`D71MzwS(-ERvmTp;$g=5=Ue zvf1GK`DS|{6>ChYR40&dSJYYoe~!WLjG}Gt7Y0st1{v` zS*c-F(MX@nnW)^(HOL}SPs5GI>Zj)%t*n)STD%df1P6&`Z>%yTO;AvC+$q43VXDVV z39%+pajL)i{y9fImV|@3GG88t@iq>JSTyJ7kRXHL-6`IO!wfaS2yM`0(T({K3QK-M z&P4`#jnok}5vf;8 z%*#nt43`fL_6;P%Fk%{iFn$Uic26jSZD8?|6J$a5vg zB=ho6N|RYMVkC`PPgcY_y8WYbj%=44)o@-&CyNbs)ND?843Y|?99b0#ax;)drP!F0 zvOE#-9XK4xD9GGgGWb9z0;L&)!LZnnRtqf@p43Rt@xeIK?&cxW*nHodgJepO#g9`_ zxLoWtBU!xN7lll|-V+oZ9z`*6SVXD9fH&C9<2gsF+ni)gvQt7W#-v~&foiEv74=I9 zHf)=vwubYF033sO$VY%(jC5)z^JM%w0~Hw?QKDMNljBOYEhNLqY`u=E0eCn*!846ccRCD5`qfIlSIuHfnJxy(75^X}7Ta39XjTkz{V(TrbR(%=V>E~j{b*WH z=_+63A~gfCXrA}8AuN{cnN2F0V5a!a>*pMuh*CDA5;5x0u|z)7Y&EkgCaO%g)FSir zMBs_3j*W+{2DOjOIrLOM2HZWyK~x`wVWra*(tf1fBgO_2PbJ2MK$JtG{azj_uRdqa zp(!n1527%PnSpRGi)XApMU_wncyV#67|u=Gz@er$D5H^`e>-2xwMl)LiY3KCtyBRn zaH&Ws2{Uw8MWOg8IJR>AT1vJw0W5+_^D2VTb{Yr1dn{KM`;oK~3WuV_J~qaofacfy zFe;gHp6n45YybIkj&wuq8>H4SQ=_R_G*CZL%nTHx90B_ssa%};D|+6q_P~;Ee|63w zSuxe$>%)eWE zzHR3}uRXMr+Wyn+?$$qUX`6q4MZdChI6gc)_|yTm|L^ws&DO@>ZG3oRef^{B{-Ru0WBVC8CHvW~0es-;By5tJEzI3oxbE!&xk)m6Va;x}Jr1XN}`4 zj`%#d4!N%EwD_(c93O(JUh(j=y*LTEu4?Y^b&a zcaHZ#9S>ev@#2@X+|k9U1x&Qu^BNZH2l?K65SPe#peyT#N*-{^EWs#7K zMkuD7)drBinT=_Mh-(R)`zTR=?CrMDfodaqm&KS2+97!>8wl0ps40}fxP}(1!HkX~ z;4}));Mqb(FT1Yt>Zw+hXo!7Ant<(` zQJ2A9q{{iz4FlC;I?}NcbQN;lt_9NwCd2R@Oo-PH9(x3UwC$W!45kTKi}GT0!pfCQ zPOXH-D25Asp*W1xWh!97L@mM>#_s$3_WF1FT*!V~F|%`zO>I0tXqS4Yb&H(?jh;A- z&IYE_c$MfUd8nAqG6ObM_p4n|?ME0js+q;AHLj$q-4Sm>_X5g{GVx zFFaFpGw9B|OrDvY$HoAUZFv1`p;qD`4iXF`P>1X7LP$_coyMSv!F*LQ`OL7;tRo@S zLS5rIYj)u7V-~2;9_4|E3^J8rFb=c52|`B(aVtQ!EVC|W`1+*E;?c4$M^SdzGF(@9 z?kMjb?>bd_aP@L+>d&oGKi;v$3UtYXYm@6L&oL~=+n_$%uz2v}ab2ISC{=T)DPYaD z1?sU4iw9>F*YzwImd5cWsLWondT`5dUD?8#YXeke+aeGC53Z}ay)9aNkUFzP$Lls8 z(DX}n^0?I&T{&KxHTwCN`9v0J!qz!!H~@?LgZ9;{M)V zefLYdFW%{F|Hk$!w;tX6tW5bT3NW1ebN!x^ohvJ=$}8BF zm7VR?jg5!5eMjlwxMKP9p{OQ}^;&gYZ?S2pAHkJoL+Le)E=weagHbeIZuM_z$i1R4 z;`6*1uWRMWB+2FE_<{}gt?KXl!hOGPf4=n5ix$|oMu&i)>n}dpzOu68VBE1W9;GV& zG^Q02QjQF1S!a4rq*$2$yEC#w(^8^u#vp|2v;jhGPgMf7rrHntak&i;JKwrhm< zoNO&fasNmYp-4W2=xRR@fpycMqnd1nwR~I}_Szt85!ZpzD!Bb@7jBIMwtSniZ=*Z- z7xw4jFI#aS#^byCNjc)sX`*ZL3B8-b$sp}7%Y}gndys3@5M}-N@nQ?54mG31dm|9-8f|3~W?FECo|o$RhGg z7bb3uA7`1dT##D;-sk_ZVSoP9D_nzo;AC|{IX90wOj;Y11Jw#!8l+D)NucsXuz2K>#1 z&2pR@Hp2gJYJYwMbB*)<HcMitM0>*7GugpAR zv}dHx|IrWHpMUJTsNWYi7}xJT9@`jinUOqfjU9|PW~8NpoXtpIfo+ig;ld@|8ITVg zTMNp$>18wRfIMeLT8eizBmI^=&42!PKkHhkN5>-@?=3Tuht0Ty_tcEE6z6P4`t|qN zIKTW{*EsJ#9@;o>nUOqf<{g}iGtyG9vl;1EKVpOZwZC-@_T|R|8|*DJl83_p2kb2~ z(o&?e8R?e~Y^1*ua*gz!B1eznIZM!V{>sv z@^HLxqvz){(o%>vwqJb9?DO~CO3~i`KWpVzSFT`(-Gk477XZ)S%kO?_=MQ$Cxt-p6 zZ1Zn%;1tb*hgaWmM zDrgs~&mxe^1ALpojDTi**Ue^tqc2vcqM2xmqR38DC5H*A$V+&f?=>ycYH5wArefu3 zi@ea*EMDPW)Qm;C<)@UlX$B0`=Y==bC+yzI(Jj0S=*|SJU)~HyeIB^}f*IiF+WJ0d zsv{vhjpnnBu$k1*Ca;M>0vB1K!RL!@Q5i{jF{bC)Y{V@Pw{*(^Yl=8hfSx@6rk;>+ z$Sg^5mXw176ZmTe;S3}-(7ZfWN+6tHykLz3nHmj3e6=sYaVCXjN- z96bS3a10GlFi31prp6M?YYGT>M#TG*YzVwn%Ji{TfUx{cJ&aEZ7pnW>jqwG|xMMxB zgJGRB2}7Vio_n)DJltNm1?ZOUTj>uJ1I>8O&1QJGKDY?p;uU~fy5&bH7poN-2IlA4 zj#`1y?BRyNExe297MD)ToAJc#^{kt6_i%iF5s+J#qR53Op^J3O0pCXM5ip#e>Bt=n zXAdX*-)b}NRPN^wXVhL(KjUV9csR1Ws3D60-O_za{b8@EpMJ9$9z5tSg6DEgjV{tH zzk|PxTA@I#o_15MJOqRKR-3W7_FmqM`Eb5Kt?bRgm7D$H;oRY(hAaYfOSilsx9Jby z>anu2zVgkLz3NSk z_^Qn4J?QEAL4G})ubvm=_w3=Y4Qm#*bRI@`xp-}c<J}fbmI;@g0Ff5UAnY7~fQ(RT3dB*X&g25tA(|rA9f{!|4t=F-q6?c0rc4 zI40+a(VfS$?;y=iFQ`Yy!>dX4Y9w)$mq@OajArtpj8Phu>RLzwyipb?vzeq?g>o9M z)oaoGB!Vbx#X`rOcCye9g7?_OFguB))zme?WsGmT=^z5>yZXn&St5zy>gBr9xg3Yk zKg)6G;r`&facEBobN|9`)A)M=z(UO48^g{{^6(@MJMir8!ePgZf)a3ty$a<(rd)9#;7PVf-I!jRH&THL-mN%$|>V$n+=JAbhSXk zW|=8tx>A;dS0^EtX(a5XgJJjXXCDt{(;v2axt^u&h8=?avmAC_=I8TLV4MxW{cV{0 zX4lBK3G01p_C1gW;dIUm^vNBV!En&dG;{8JI3@FXKB`YsgP5A9@NomP2Am8+wUpID zh8C`c8D+{6QJA3HcRVnO8dTz2#afojYbKr7CaGbj!*|drqlENAq(zRVk>(gBNHc1b zqf{?3)FU!=HQmUC6GJUBiC()lP)AKp>X&=tMn$S$8~0-F^1Ikg2Ltmxt>eBuFh3xz zUal9m%Yhj=Z(w@3w6rG_FFxJpuQR$EmCw?Mhb;OZ7_stG6t#nbWNIsEpT@y7b`4-NJe+8 zAe6>M@RCxa3q@lvy_yTTv^I9rfwkEM`~AI@Ppuq&^Z?!a@~*nGvGs{fZ2foEbl(>E z^SA%!o$}_x$I?qKa)#(RA8=v2C{LjRDd;3Xm$PIo!E@F(gCzcP?b~s zq*91AI#{4vWoz)T73Yf`|5$9$a=%~Av5-In>u9~%C3^W@tvH^*%B?;mA{Nsys$9S@BRs*$T!m=*BU+P3MS8ya*UhybV#hjF~2cL zRMEz$(uUMjl~YUITE-YlhtQ@rWzuF&uEwhYe_@)dn3bunhApV5h)9Yrl}_vOhuQif`4r5cQhAT|tF(1*F>r6Vr24amT=Dx!viX2*erFj=AGLpNjsV@~p3h{1}73d5l zzyY?^&E?v&<>i7RM`K}AWr+0NfKH};RbqIAE-1J9Tlcf zkWTi`kupxkLosox;F`Z1j36jA>Kg^T(HMbHv<#F(mz|@s0(etR{o4C}C-OtQ+Bj_25Xd+OfU@ zp+-$M!Q)o45-puxEWh7(5C{AJ?*;q(_4gnC)InwCdsn}?_lLXY&d$~+H^b}ye(lMQ zQhD}-Vf7E+Db4o$7tP1eqJ!&QG+BUig-cq&6Tu7%g5dK=KEUt^Lx|BrJY__>ID(5J^WvNwB~il&V;esIgtLm=PQC ziPUPFl~c3wo50Y*mKd|}T|6-dE^lI1RjH-%`4mkk0%~S;BSZ$u=V>(C%R~sa2cqD! ztxB>;rF&g59n=}TYom0Z z0n?DlLYQ9A(P0ffccE#fjS4Y@3A5kAb&YH?6vN<1(6TbRN;dJP+^&J}>j|cxJUwl^ z7fAZ2mKcvSZs>QBl?MBiOR~}_vd_2pzRY9sERRl0x&^jm=bahG6K1_SGC^XCA;>K* zo55nZ(WyeFFz8Im7&EL%Y7pG&(`37Q%1XZ#7?1ZY5p>`#R_W=bsdT(ME$V|dABeVS zh@Ir7Y8vcQ&oLg+bQ_Zr6}^}b1;Y|*W}>>09abmFC^@Q|Ie{7vqe->h;DeDLQ^wlL2*yNv-+i{AQ9Y7+(ri@rAoo}%OM34PufVw{>@);S?t z>LNVY;z(_|m4PY&vBk&P*_k2%JJ+Ugr9?yanWCIZ!f#w+Y?0=yT(l_F0G zx_DZopYs95-Cg}7@VQmr{>#l5tbcazOIydgrM1NBA07U{!2>Jid7AyL{cW}Nt?_?{ z+pPpP|K#@DAtd~i-EDYwpCAL^F5;5hC)z<#mxFA)0n1#zLr=zLzSKTvrj6DpwL-E9 zOT$5~zi_CPKqx-Vm?F(_?YL2H5k?zrB%0Z9@6-ga`plJ;&5tjUbm;CquOu(;yfRe` zNp09FOyL19@Eq0V2mJIrN0qo)D)&0|a%qywGZ^2|DQFz4g)yu~DnbR4iLw$-V1@A@ zb$ao*x&sz9`VvVackk+vyu8&TfpJABS}9EjloT)O`BFYqfX=(%kB!94I31C2Ib--M zR72-0IVPg1x}O~i)pALu!y27KL*+*E)ar5NkAb8&mPq<8J|846?|d*RPb(nD7vITq zxj=<3>t&NmqUTr-+^Y6DJQjr-X0lAO6)-2Gx?W5txky{I0#MqRnz3+6$(75etjEf4 z0qgO~C6Y#;qV?k)xVe$grMXiiRjSsaf{f4(6cWKP)k&k``S+tBLt3y(XL6ZvWHhm& zNj(WNT4kUx#|BthiD?<0XAyD|W2&ch`ftFo)ytPi8oGPWyh(6#e@Qc9Jb}v7L6GLv zB0J3^VyO$Io9Er$#WA)pu{vr5hEPijXf#L^Iuu&DVz);OModEpRgh6Ilno6^rzQQ1 zK&KyCBI)2=tkdwN>2!h$^T`BX2(()~(ttC1BvfdgXFXzAgdYL_p+uAc35HQ5Jm|KI z1G?>3!7&P!uN0aFZt7@>nMO_>r>y)pV5k3biKGKh(N^hBtjCnPv{fE(agC>wEP;&$ zA|1-KBbL}Y@7^Cb=^mFhGJ3liK5=mos?|}*#yu1SyIi+-XTt?8oz-yJd-VG@Hc`lrIE|DLXU@4pKltHu|Dy}85 z6cK7N86rqQxgJ?VE5Uj{I0V^2Z%TUgm0&me(j}6P+{HT=^wM@NF=|+Z+aW>Ax9Ai& z#Sze{5Ifg;=y0q|)w*dVN|0KiH%hTtjnipKidHQM!~MEZ&*pNmP9Hh7_E>$9{r>-+ zm7iI;^8Ht?9)9sKb@0W5SMUGAetGXV_G-I-v-`f?7w!Dy&WpA`xc!o?@7vng{M6>N zHhyv=y8hei#MVd;Dpm) z4-v$cM-Fn;;Khv;07E!nJj4Ne^1#F$FdkxjT@IKtF{e7mF@VKR>QjRH@iIP%!m*Yg z?)92-XH-sU?N+rw^f|6n(&TV27!=sr*l>;IY`o_n=!&S*dJhr9T-SKp{3&N{b^#dN z0plU=*OLTBKYpX#6(ErdmX>p9EEqS{Ogc{%hHVNZ^e!zG8q>6aPRUx6jZD)MB(0OK zv7CKGryM^B>a!C8#X@u`moE>|A`)xn1KpU0B}gS=3jSO(5l*Hu<-Q#9t5l|gx~}hu zck=zl@f$!rc5WUi+NuaiQV2)MQZn!7n5-RDb4=rvbS4QN^f9Ssd}QeZ%rCjF=k$X~ z(4DV$y3<4SGuKt!R=3WKEb;htHWtuq4@p{FV|l{JzSe25hd61=<2ku8vgoZVMvz%$ z=lC@a7!T3To+L1xV+>%i?V5+YDXy`cwQDG7eZ*x!@g#zO$K zCkagVI0UfRY1}-dGI5RNf}CnWyZ6D^^N^Utb(QCtQWVtpROGP@fO?*?Jhlj^%(mN4 zdLG-IV;EHRiicP3f;_h3F=S%_&GwLj!?o_*$YFa8)L~mE4>>fJN0@TV_F3x`0M_Z% zj)Xl#;B#H$?XA7%cffdvqxU3%=^j4-u-MkgLs|>hST3+mSMA;h8unCWe0&wC z@2SZ6ct5D;Da!b0zyB9kzOr)o5udvD>5cbnyk#S@@wD}?t^d;cN7lRRsr7r-zOnY3 z+n?Tk$JQISp11k+onPAd$WC`BwR6w*H@1Is^Vc>%xjEi_&+c1xBfC%A`P%B&SAPwh z2aH#l)mN^rtbFClSN5N`_w~JB+xz6+c#qk8<=)EfS9U*pc+bH%d~ZDX&4W*0`RsOS z8`|F9`qQn?ZGCV{z4D$bZ@Ch=^0dRR9sbf84C9p(F}aEk+bZbZx9H$Zx|xr(@$evxc`j z9kx<0*3?E}fo|vBb2_?EDsZBlqg2u?cd2P=fo|)uIbBQ6#^k8g&WKGAPCuVWF3=rp zyQJH4Nw?{eZo?(rx=XsXIo(n@zj{u$RDQ=U=|1R^?gK9A-tUs`eJ<(V>yqw!T+)5F zOS;Eh(!Iweom;(sm&@`_=5%dq(o|9vt3aje!$M}nFN}|Er{9*+IsLYj&gr+MbWXo5 zrE}YFpKvLckGrJ%5tnoyc1iakmvrAhr&FTfmb7l<>SH=OnD&Q*g?`(7lS?{hK3ZCy zGtVrgbLNkwbk4l6lrHbG9XXfs`#zU+-|Ld@V=n1_*d^UZUDExKOS+G^r29dabU)yd z?#Ery{g_L-A9YFhDVKDgoYO5`Z#eVdQaWe7v6SweF6+JSlI|TY>E7;=?opR?Z*xia zR+n^-%;`GZkdKOZnqz8GxiZKvt`pZHF6qK9=|V2)f-dP$mvjM_bcjnj*d-k_r(3G0 z&iZ93owI&fO6ROgmeRRhmpJx$>H5I!`qUYh%_u4LI!Q%Kjk29?J2zaIR}bmA{Fc&D zF6pu^=`t?q(k|)9Io;C!aMrs^_rGtl@Bd%4^0O;f-go7>hwnc4^#fu5>-%rn`|G{O z_MW%<6T8=TKC%7PZDZ?CwqCmVZ#Ey;_{9xw{m<6fwGXe|>r+;LZ}s_r^(p?5kCR(I zaImPX_`cucW%`kad;05<`lNF5agfdAQO)@^IsRegBx4V_)%o z?~{ey3!cbv{Dv$)csY2^+!K09^mXLnTC;Z?1JED50@UpTuA!ejIu3eq3;?@(=uB*Ix(eK5NpmqEPP*uRL>bqT6b^5N~i!%Y}+SlLc+Q(hjc3ZVPJB&RHP5JnB zHXOV0?{N)hesko-nE;H&*Up=*_}=aMhU)gsUcB@_^5A#iB?%hnve(?`vUfdM*gd!@ z^o}tAXJ$=LT*EnQO;IoI2%!HWH~R0`b)~oOzeP`hBM;6BUXrPS{tMsezsH^|>>hj@ z`o|#v=l=V_kb9?VIOpEpdGTWC9tUl$u?PBf*R`Hw!##L0sK+R%a%RKd;kwFGHoS|D z*kpcML{r#U(F{}(f@Up9$gPsxPRTGlE+_gBa1*wAtbQB-Rbh7P-tM}pg;4EBJt~#e z6)4iPL=@4-Y&2Qyn^AdWmAd6&0jBhNIE$sUk}@({#|WrzHfKEQy1rZH3=fVBK!IUU z8(2oK_}=Eaw%ZTBMbB?9-VX9HWW%wA{Z`j-oH@gTBSY`_8mMMw=N@r=%YD|)1-v*m z06X{Uxu_N2)ODq|7xkjg!I1~Iha(SO2|!K!H`HYEWMTJ!r+@qafHSjmW7lxbw{sr+ zA-cy`ZLP7D({f$wId;y2KZL#iUt9gBmHme|pAY`|&d+~W5_r#Zk0*AnnfJeb)yv3U zuA2+FX6!5^{eh{7detyx#cXyOAkznkPH|qY8811OK)IfCAbL$eFZC7(T@8P`izS``94!F-=u-FtbNLQeV6DOg}eHZ0CwMpk$V5&n? z0|j)3nsz!l6b1;Eq-(k0am|@DvvysE*=zuS0H|h2qNW=C!91zj6W>)qlAV!aRtI*b z9XJ?#X)|LWT8g% z`gfexC2KVMOt(NY(NtuREvHK9Oxfgn`F5(Q=ZS%oXZj6RXf)9_-!8J>Y zM6hWLZWuzTJrH7HAyJ4++|^LKPEjSCz3*+a)Igm+$TtU)RN9$^#D;Qy(@h2Rr=;|& zvxGdh1U+djNDTAdHy)2|)9`~=R=u!#HVrW7sPjxi;BGe!0Qa|^=E}q3;3+pHi_f!f zZ%Ul}O+YG_WlSQVth>ROfEU03$+^ZP$_EsXC#ICo;jydHZgX5x`yAYu#4_Vbnx{Cm zQ5s~}a4TTlc}6<40GpiLmJ7gR?pm}!I?Gge!=5|@X81|h zPCs@d_1MCW)Yg`HHn6rbvB?(IXSQ0lzj*0D=@0w}1Y_Xs5>kaDFfikOWHDaO+!`w# zuzaw&E}BQqXe}_BXRzMfs4s>21gTwVcUaS!PP(tlL2`33aEq{9w+=}8vZnWdEsJxf z_d7_p{I-Yf9HTEewq`j-snyGM3tw1XUNnaw-;O!NYv#f;%S#WtxAW!%XBTRB=C>=i zsA8|i9&$afA3nW=v~BGYtAEjA&>o=|eRuvs5rQCFch0r#L%a|2@`(LuR@T#@2AoVV zg&olrQoEX^)JO%%P;6L4Vk|ABu7Lz{Dv$7}0LOya zbR&?juzju{P(u@ZoZ{t>hBJe78|{vyp^i|BsZFpZl&u8Q;IU~V9d>Ek?WThvYVZI3 zD?hh#D2-yVG8AhrKr_dmKH+xyRZAKrVz?w{{|a2MP8k2~M9gKq!v z_Pe&PZGC0y`qry9e`oWpn=jk=@`kl>&-%YxH`iad_RDMf+H-ur=u>@9U;VGql?*5b(jv;fr7nwR_w=I6r0v7AX0OJn5M`^*M2tMr6?Uz%iSOv z=~GZ4Q)Ggj7*+^0Q10YKvtobPMX_Cj$zcp%8*t48n-%NW2)y2~q-r!cgr?R6DPqhh z+CnlVyV&oV75mZU&O1kwX_IG1MH82kbtn@XDdBho>RFL`53SP@URRJ(Hv}?*fkz_T z$qQ%2{*a4eaW!1Xfjp{yDA~bU=@BBMgcytG22x55SiM{{Ekxl;A!zl0-o9W~>_=P_ zYthYNzmrd7sDx-J(?r`}qjKHK)U2oBj8y=WNhVKcXgHV#LU{hH*dKIJYz#!^<8U2M z$bv2v@Uevqqn&=KXT~z>L@CG3cC5oxQ^PtM;ZB}6EA|Im6x(QRr)#odlO*_{>Z z!lB2D4-a>;Gb`4G|Be?|9PVU$R;&xx8ZTZu+{xCgSQjobUL0Y#lg(MNE}USz__J^) z8?$0v_@{Vrec?{lXT`ekGx6d}!kw(mign>g;w9<@cjB8B>%v*&$b)YPcd|Mw)`eq; z7Z(ujWMx*Y3;z!#k%m0@N!>czyGIJzOi!U%U3>jrFA9ZX4lf1*l%L1RT!}{`?Zfzt zBc^7No}Wd6C6ym^Bgxuef~m5om$g7D)l)&{v;kGhgxIc39NsgpLkmW*ycQ`@K|~>H z%^H%!qtRv#Ei17Y6CPSfA`ln*0+T=wUp(jF^5r68gu_x+uOSB1*Xy)0DE3+?Y4X7) z5@_Xdsldkq{qn&-%u$uX@m!q33l%dE3x?xe+9D=XJ&r3;sRA}DG9GWG%|N9n69+1^ zrmP+jLCzG=QpFq_sL=u0v3guzh^6W+s4iMf!YD8z8g80uw$<* z4mV0&)Eec%mZ-e_^f?F8$Loz!n$`V6z8C1!vdwlW4vu$fgH*EbFT;j5Z1-4yr%vwu zvcrLqxeSqs2(V=m3W1B!8qsJ_W2FsFZ&2MxPfL?>N1=uG`kUq)FvL{TNi$#};wadb z%keSeZ(&3+{n5S?s{(gdRz0howX8hI5e zAw~-yBrAMZp|;*Lt0O+ug~_x7x=&6S32tm<=mJ8StUp7HBXyW-(=ZWIf(a17aOEH8 z9Fq}Mu!`wc-%N;MDna1`#KMMRJClr9;a;PqYt1qXHqvEcADeTG^st_q=3y&14z$U< z(L~c2(PoX2m>Z*^d3!$0G4uZGF_DkrB=*z8t$6ogggCPu2-Kr@y`<@0!wG(ymLT!DPgbvS4VL)Z$EH~TuQ^V1<0 zPmc>#kcGUc21h#645W*80vwd`y=cy%g{FEXDrs#*6Jk&{0dtr<%v$|+6T`~|GSsM$ znOri5q2%gM&N)j~P;d0S#MsEZpi;O3tU24r~x!loARqL0^7@vkx zt$ZtlapQ)}D2ZOPrI59(8t;)zD4t|DoR-((RWckYrBpUI%IEo-nnt+EM9gHMC|yrz zuxWG$Dki2GYwPFds6;hX5MqqK$13?mx~_*SQW#_@n209J1+&$x&Q{cMs?lT1dk@b! zDx{Jz8&H>&^j>IEmPW+}7L<$a-Z(gIfNVEKkyd)JnQ?{bB@w9!KXBo#sg@NDkaib3>BFKHl1dn9>kFo zt}yJGT&;o`+jHv^7mCbul8RbMiH_syH41BB1)~KMBW1#9A*)A?(j6!oE@trEqj?=X zRO5PVt6Z+aSrv()U2vjRY%0z1Gyt+C^(S?vR2A7G1xef5oTFHan>_^f=Nd>qA0PD# z(`dIY%fV@u6+jB`Br_aPN`ptn(7w~>+{E9C28wD5L=D%XMSa}v6zc*bK?gi?PxDRlV0$TCAp^haVvIV!OR6RjnilCe+BQ6eOowI<3E@Es-O7*O)TuH_J=_EO*gn6L? z@)b%MscGyv9EnshQ-olcV*Mnkld%YGB=c-@0+qmYST042wMMyz=oEu*I}4frpS?GM zc3e#h1Mj|9y+;%1Zkk^1qZmRi?rYid))^LSu{K+>WXZDJ{V=j5%d#cQwk*lg>(@Yc zJ?Qsn2wNt@gyhVSLlPzs=FB0Jg$x0jWjJ%f_lOf9iLdbGLAj$k*ZoMks zd+XYjYWgu#u5+qxUH{+m|KI<8|Mo4I)Ns(ugzKb7EjeI?C{dCrk4eKE(}69cn8ge| zl`f*g%JCASppS^Mf%HJ(THdA&kpS=Sa?zFff)H0nTEXF3WmakV$;$C^1CTd$FPj@{ zaeYeCd5O(V8%;Q2vtr*jOsU1VjTwx?fzRuOi7@sgxt^flNOmKmL$=vZGl~dqf{`IASGC6*-R6yV5z*??uQ5ebO8a!-D0|cw+9he zoL!k2g>1)dRkCP70^5snU=1>MCSC3L{QhEf#$cfnAzn3+8co$+4`n1I8de}Wi;k+i zW*B3k7m=BS6bSI%Ut7|ky9H*1R&1Vb4|UJ6nRlB#~4`XmK1TV!CBfM=viR z(o@c=j=HscAz2kbTxh1>jS31~C8r)DfU+1mllDm4<}Cj3qYDUVj1+OWJ^RY}_6iM~JTb zEYomJsZ4QAti-0!1|9p*USE$C%3VW}OKGC8_Vh%+5BjHp^oJ*9_Z84h4P9+WkDtRi);vutuj{j1?A7Zhhwh z!h{-@Iqc_?R_J6L8>f0;N7|Z{GbW#CP+>k*R^lA1kY(z?TtIaEBqf8Q@e($!RK!t% z&Lk33J4%XlX*ARo)rO>UBn62wfAU)k2)*42g`QcU+(ZR!D!SUulcfQqcrA7~cKAu! z0G^^QgRA-N|G0o?J5Jm|>kyb-g)nZVI;BaoI&~a;N-|~>JTc}Xw3BXOB7Rt3K&V<& z0&8YZaVFDhGofXgYN=7G1;~`Ej2mNgl106yoUvWyWT8+Syn*ceR|^^n*Hl73*)R#B zC^RQk-I6S!){7HL!poIXX;2}(mE#f?&mVo)5~3jb?bM(FUe*SK$jJM+YX}vul20PB zi}JeHhkLF~5{b&e&n_TjhjGD53r9U2 z2|;Jwu`e~cqQQ(XB$#5wa-&(SU`YijjiZDs76+=>15;)%w}eQKJsQ$YHkYuO4A}ln z$8^AmRa@5crc!C5T7u5OB?r|GmOZaq$k6vjh=@*w6b&gnct{RQc1Jh(iq~t1W+G4E zC9!}O5B~n#|9=nOI{CiC9|8Zo_`Dc_7bEau1fDtq-{L>~_{^>E+oJ7Fu5s@Uw_9=R zO99ucn_RGh00(#AR5z2u9WHsd`K#W<__IcJWTDK=CHzKby+EyD zu{|K!j;8i(6dzFSftATa!6>TY!g#1lmRDz|)y5cvQ2->f7z%abkLR9>udR9w&O$4| zc`1{D3LjBhoL+Qiu7ymG?sY>H*UC~(ZKJ>!2`^OIvP2FHA_xgx7NXHbvB2a80!iT0 z()5xxHG~wfo~4<7trX*U{W7fOIHp?5l_@!sPlxDwj@YxV182QAx({EQDc+l{?M)s! zo}1!juPffGS99a-0IAupR+M8AKs%dgp9kfblLSFRp5@MCiQu{RfMB$V`&3WzYjgK; zH`STj=p8ySBlE0t9nNNFu~awC^}K^u>pwsd2ZFnFjH=oaa@kq*DEenPja-Ses}rRj6LJ(|)N# z(0Z5Q^-CnlhtV_yhegxUb1dBF%n6iP(=m0{buic8(jLaMx&A%U_9m}y8^<5r7Mhs3 zKG$D)@VO36+#tI8=0$8%1S@)aJpz4IXN$ewm<=0FRYb==IaTS=Gc9O$g()s$h+0<> ziHO^tak7ZNXn}~5xrg~{kNsk1p=;o2pq4E_j>m9lwH*t&w9kp;IA?+s)=8;f%xA|< zy@J}^{0tjF(+YsQylzdRWkBOA-N|I=edO};kstYo=RfJLqgsQHz)&hxxNJGQX0J=-dn1jOmN5y^8^|1z?=lVLKwi+O2{M-snnuP;kK0qW+X$je%1g%R zlq(XseqSc({wO<{68Lt!fDi~Gr8PQ1bEvpDsB(4i51A%?v#$GUa}aT zE?iNNYHJiX7^f@bLjzOaLAGi_ z(>xt!a`eDWd%9W45|zE>1Ax%ugp7(C4UKD0M)jr^mx;6+jbN2RB4f~Y2bz>hjHZ%# z`sWrj1jx3rmK`gt%tS-Mlh6p(>O41ffbWxu_l=+jq7hKar;~gCask0-lm4(4j>#~> z*t#C%%tE~gX;F=ILQ-Zh+Sj6%S3&EQ)4#BQV1s0Rl8RECFc?jndfSnEt{RNXEQ`0s zjLGX*66L$Kkm2|FC4^q|}u}_rQzF;lhmy ztwgn9Q;pR^F-OQ@#RB^mf+|p{iHUX({bI|H%eV!X8e9oD z9Hn@#I5Z&0!3Ryx*7}rX!qOhRpg|1Ucsj#VaAc3FM0rfAptQT6ZDL{0=#oH`;03Kc zj7m`Qq_~7Y2WWUfC_^!dIjxBK4QpIWl_)}O*DrgQ-NuDG)%jw!ho9^%AWCq1>>+7^ z2toXao0(PuPt8LOMVEul&;@VpXu>sBGNQtxKfZt{Iy~%K1_xes`X~w4`q{o$nUhnE-_U^pMa*v8;Kx ze7st}0_VM zbHGi;6+du2G1+A^!wDMs0+Me7@1HO~?&hVDJt;Fu#gTi;N_FRbIIecSU@BQo(IG$83souf*3erp(W(A20@KFBH=hx zq{!*4I_xKpzHZ(PB#wtGu8H>1N?O=d4>NUq1PR*r($JxS<0hx5*c3=v&Vum=>Ehh5Wo}`fy%_&+gY$9E&)#c4} zs%e-<3m-LPqUTZ;)5a~j(FeY;rZIt}Y(F!knw2<1gFJ_HX;AOxyZQXt-(GO#rTB?U ziF(aeshXYZ2B|@!*3Nld#{B_mHsrwlZ|?m0k>p9QguwXI+e3x0YdyB1pzh-3ZzOqe`&#$GfsE9ApTb^ zRe(=#n2R-y7XqvtsUT`6`n*@3rzV*z1hy`<-kr{$zU`wGtsJUs@kq8215 zi3xCKswWs;C^YI=c=GEDh|bXOXgx<8WEj}UY71hf#W{nXoQHon`r!i+7 zeQ z53?nTQS}Ccb_6(G;9Ehz*EfS~&Bppi_M+!BqG}0zLSZx$6gr48rAcVacAz0NOqLKt zGwKeW$Qr8PE<898Iy)_sX~k$Iwb(&=DuWB^h@|>eZ#?kfWLR!!WG!V3D#f#fnMSA- zUW8L&JV6FFoXe2FyS!$XI;nay=++!mkwPf#fFNv%-#L8Y|G%dH|6;0R8~FbZ=Ccah z!2cgvKy2Xu|21=nP5l4AWIB>w)qw z{QqD0|G)76f8qcC!vFt;|NjgB{}=xMFZ}&WQ3wP)L z+a&ef!)Tde;cm2>q`rGNoui$ukw;C<4@JC{B?>ArVQX?4O$|CjAyXD@aBZAZ)fSXN zxMA1RtGxIyoTClbK+DDBlH4vq2(D;iT4h1ho+%8IwFZdW(|Na8p6ap0n__1oi4TK0 z+F%W|nU%hv zqaCe*c9TSX4~KKK!!^>gZIY<(p*u(Gu7P%wM12njbF_ms&?dbSXCbAc>Gl!r(OHSwV{N9(MCc9Z;n5A8WxdkwT3#J-6S`*XDYHPCL7 z|LgIa>4XQiB`i|9jY(qwTDL zc7u|u;zMJO)>s2AOb%+WkVvM-34>@R-4PC@8o&)ZM7u@52q%!pw917hN46;Op*}~e zuYndk^9FJ*Muf7N8xuBc)onEclA-XjSr??fY6O~~qK?3q^WsBoj#gU(Eyz!TDa8nZ z&|U%>jVyf-D5Tx*wQ9I0ij9V?=SG=~%(thm_^>@k+g=0h2IWx2hw2=yx(3<}DwT>4 zzhRE{8`eNupmh#LGuXJx&;nKS`xO{>hOAaIW7>sds%`Q4n2u7k14P!EqiwB$7G2+e zLVWo3bF^Q-23ig2HD!X5a|qQSgoIcv&oa+W*I*xOgN#fKlA zqy6aJAvPPO6@1v7qiwE%c7x)Z;zMPQR#^jWErP@`Rwb!iZd$}zl`&Fhq)J3gc)H)F zDvhS2moswDp#z|0jXB!JI%ub37(%Ro6j`^Xd+{i0gXAVnkR2hPY7L2u+;n)5DWaMj zfO#v=(aLL}jf6_KULH)1lHMEBod(F8?y?QXbLoPhy5&5EShbOt#iveRd|02Ot*?Q0 zgTk2NLurmyS_7@Eba=E+-8;s2@eX{5ErUL2sg!t;vB8G z2HFj(TZ#{BbF{TJ&~B2g@S!kAE3AQbgMyXfLw=5yUjyw1r76XS)j8Vg8ff{Ekd)Pq zS8G81o~sX=P+3jkmf0)$QWVf4iMmufGqOu8m|)x-Ew={R4Ps8khwL0Jdv}D>gLsDkUpdhCe{(#Ma*dXx{$#U$*yA{?#>WyWHKU+X75i%f0xoJcaCP)X_Q8j|Gkd zIT$(&6)QuhT3?A~x{d5=HFf{|DP&i9JeOsVm@O(B%}&%A%9dW9u17ZgWZB_l(*Mv? z$fj11o;RqG6H}#jpCtGyHH^akN{d^1n_i8C^gs9%vd{|BbB&ygtYUAPgmaWxhm&@PER8V(nx@Lz}YNTN8fpaZ)!EL?0M)r%JLUy(BIf%!R`U*`o z5tg$=zf5~;%61i#|3yzByJ}m`>qQ(hF}gl-Kw7h?XxHRtCHqI7LUwi9pBquFR`*fO z7KR9Bh72V=BiYFp?kTsex7UnRhi+KXu-)eDa+{Q$s`;#AJbBmcR9AKF+-)#)aqLx{ ziK!|@78#q*+Sf0*o$56cWteCXGs89>I(~q;h}d3_YI1edCNJMkbydO78>ZW?m`IP+ zFo7NjjWYI(RK1q~)sNo1kGf`{MB8AvsEFI+p&3QvB41pKYJSz^c=vB7y4v=fBI+cg zXf=Nt*Q<7kw0r9jUDa3f-d+*(v;{&BIJVVFtd9TrwA&6wG}f6`YF@KC3T%BnLaVy& zo!#EMYgX`i5j9Vr?AX_;|m|7Rt>RoPi!kZ^g61p}?KpMsFq)$k_ zp@jAuA=O<^Ub$6ye8cfx8p!J({nvkTh0*ip)fMmPc0sMqp7U@pwyJTXPgNWp+%C|} zdX_Y|IzHav?L=2+(mAK1xQ-PqbutVUO*N(Uj7x5H9WW1`6xuZTT~4K(W%vS>~F=B6rRTrt_FZLGoC(J?u9bkV94IuDK^%U8ol4 z|K`?9Z@%=d`+xfW<-O0|lh6LiS$wv8`kB)&I{C98rvHC9E*$;z5p(!Uhp!&|&cSO3 zd;8zM|H|H9-TS)TpV}pM{?DD!&cXJd-cD})>{j#bd|_n@~fCE>`7AB z4}eiIZfIE2LI+`J8MkQyur~lu>5La75$X^?H?EwRsk@ zLTXi;&*`cdmc$ySB8^ZIO>44dud>%`@R4~~W`DtcqJFmjp3PCh}YXTyM zSB>a7Th)6G)$x!zMfC$2iPzg7rlHmGeEnXuMR4bB18MMX$K^Ux7MaAJKUryU^LSq;YyFoiqP@0Y21DNp zC?3PRk>BOY@^HPwMmo7Vz}6pMA+@>yoj*5H{zq;Of$(EhksE>u2vCkh zuGw}NB3zGbe${GPxfOY?4##<^sEmfOMH=;{Z=!^1t!L{}nbk??XK(LedUZQ}9(qAT zq#LeZ@8eAkEqUvigj9OfSo@i$kX;>)bIi4xIH<504H1wsg;5Q9y@9=cD-1t{>?)7v zpd4n17t4yvjtwao746}gWLGDopMDD2RXcs&0Z+NZL3?5{>=fLSj{L@YWK*l-=BJ)Q z_S!BEnWUpBxA9@Pe)%kbMcGN0PFuj3DtIOZbW*#c6&JzFfDP*tdZAC`QHCYCkIJKG;u=Vn4 zG3eXKu8Pb5@KeaH&cE~aK=kW~TBOBlTT_}7VZA|ya@VF*;wfZTC321sZ5nrIp-vNa zv&6TX##&Pfy0*{szv_0f*B(R8Czw|)gZz5sD%d>XvJkGfWx7tbe*sQ8uYOtRj%^(z zZ(z>P&QdD-Sh7;FIVWc^TWUO~6fv(Z=|BI*0%T?Vu zA5BNB!srt-;CQ|3nBIDeHgxSQSgl7Fud)2```HCp9eD8zDp*n6Y8BL6Snu9WJ0jbN|IKZwdAzs zKoy|`5`iT_CM+PoCuM?7gb_9!Am+4QN~x7lB%5WA)%mH0WS364~GynwO&OrL@>vRtoBRY|!)BxtK9(z@8i@_7~RkIiXmkjN%w z-BI09&lnG)fjvyJatY3(p(_;SGR3lJskpXCz9IV5%b&PQB&+M@J4z(a%W{$Ys4POR z(i_H1zf@u6iP!G;ZxBhbJmNv(!GacLO0E_UCNh?lGUJ|4>vmlVM9VcBVL(lVrewNA zLqeNut*WfAB@FwimtVh2B&%D%ca%uP=cFYBpVFP?fPwl2E>(dD~6Pt>xlVRv`O$5bc0VjpEwdAXR95j2ljv;y|`T(AU-Vy)^E$g0a+|`qsqILB776391W^ z<;fsY3RS zMm;R|N^Bcz)@sQvF5>(RB0*YBH-&R}8%_s#gfweKj?IbaK#JIOpH2!r*M}@!hW!*+ z-irhcHr#cQSYIR`=zQws_%4yWgLlEn=eG-XGwMJX)v$?Qfx1n>sT8TPO{I-a1s5OE7fCG?j~p_A9n_`pUoRYf+$>DujJwZm2KA=S`}p7tf5&W+tI9| zl1>*rcfi$36JuntyvtP4Y8}UDg(J6NP$?f-T z{npk`?Q?toX73mFzJJf%L-xMtrN8&mUwA2gsq)f??*FU%KX?E8?%SY>z`O4KKllFn zz0cfh-AmouKl?w<{?gee4!_{wA0PbHgSQS|JxCtx?*DyI58&fR%+Uu9|K+iC{6|3z zfnPlOfzx-N{PUCl`Q$rKw6}SA>|)}p_WA9do%05$lk8VPl3=kxH?gQ#UA=PNJ^iiI z-&(iv&gpNS{^rF#f4mOn)89D#jjOh7C)8r2YDeJuyNs&7wQ?J@zq`MC#YBY_`W;W> z8=%^zGUjTl-fRcEAKLxUx{ddDKe+qBbsO*Peqi?l>o(rqegE$J*KNGB`=z^IdNmk# zb#V86yYIU~d{=0~$rcYOA^U?Pneeb%B_l~~j z=zG>}ynFQBN8i0}vd?VbI-{k^Nk zcVXT;{THYI;;QjLYFY}7$#s?h4j}_#HDcuCcyv5kxAF1u@OZdx++Vlx?s4z9w{GK|W9!(unn!mD<+yv?y+VA~)Hv8b z+CN&i@&5kd{^7cf_x2C=57uqGyAM9g#tQKNt?PAtsPQh*7Y6d`OV{qPa(rd!?rgks z+&FG5NAAwX$1?cyqgxk`E#go6@d_9>aPaOTkx}7^be)ag( zbsHZXf9&{b>AACW`^O(WzFKFl z)Q>%Fu-=K^U5)cy0z5eS?9pe}ZM+X$Qh$2g#(PIUb@Wr~Hr_q@$)lfKxAD%=-#YqR z%f^knBzSOk@9f^XjrY&MXWhnoXQyYU>o(p!J2^X9xAD%|@!9c}O}wjvXGdpio4LdN z{RxiRj1)aF_fv(V!!JMl@^u>@9zHmHux{gn!}lJ(ciqPOhv$dq z>o(pyeE;F2ee+!%+&%o#!!KPI^UmS>4&Qerle;hx=XM{~ymWl`6bA|38`<$>fXk|KnC;3U~ zH=l3adt>X+;9H-0_x6JgQfJ;pDX3v2Kh( zJP`PO7KUDb?G@Dr@!>{b3??4<-A7+}utECDR{-7z8zhjNx8ER-iNpJi*`w z*+AzACehFUU34i4kloz0oZG=+`tIH=5^Jvp}+DvXsjESK=>-3ek*!jVu)g9wpaBXFpo!pPZnxdTmQHaTb6vq077i9cQ< zT47{{+t5>)3`kj_wm7}$&S;1QRqfWWdKs4k+?wXabR9T$^+*Nr=@9NaP4Ys?ma2Rz zV$1F%Z%o@BCqxrBA71uyB!P#PuDV0D9;qbJsn?zSFyBYQS6-hDp+A^$1jchlS~Wo` zf>{rr==$rg?|zH-@Ed1(_-+37gAHca-4kU^Jxu=93>T?VlAxm3F(l)O#tiZSG=juMfOZZ{iDaN%kr9aO*O^K?!$@;+lx zyvf&MSm{5r+7)cK(->UT{6W>?uu63-gmE9s=w+r>kFzus=kO8SwZ@r>T}x_SUdv)l ztm$S=UCDMV8Ox-c9xRI&X+>$IorcOgeY5Rm>cmOy8!M zXtKoj3W!M4lXA|fQe(V>ww9Il`M(dVJjq_-?vb6hd^G!<~Tc zbTED`#hi5=DCVbo4_}`t=AY2EH`xw8H^t1~q?mu;`^-%@sw;{%bVIP!vccBc{(P-) zKG^Rqc64j+QNiXYnVj#OX0R>Y=%~Iu+po^nsz=9!*RD3xv-D2j%gpTC<^KAKe$Uap z{oRdQUO@2e7OUpzN!)6nPSn|^J2U5T!Oseli+%CijL&&%tFw*q{Kvkf^{;*gJ8e~) z^=|%+H3&d#4*1_3`PJ@wW(z)t+kk8?J3F4;xcg=qxlRlHOUM+pi2?CBvNpa5j3hes z6mw$7ee2S$u$7pPrDxs-@N)aob$u&h1c5UH#)nlcH<~JD3@3*bXsF2I5Qi`hBsQ=W z1xdGwYO*=#aCHRf$wiB>ut|`2%yNdVWQIJ6TD=BWEUsm3W?cu?W_JGn;Fh}e(l_4! zkN01{_xtx=JNx%%5%BL1PyX$RfBdhHy`$eb8Xf-jp?mOK2hRR)?DzNn`JM%$18TygZeM)Fz`nyd&aAO9Z>sj;@Z@p_{2A=aI{DO@cc+QjXoj?}=CRZ@5ax?-}pid4M&VN+SttIi+7JwfVZ-n)Ief zOoNBiVF8Nmf;Uq8ZefBu+TsoM{$L5%q(BKRGdZL*XOxW;Es)GYMwj}dX(l#}z>?Iu zP|)~p23`Q3{+$INp7M$vvqAGtLKa5pLAz}A8yJ#Jwj@Mo43 zp)pc0r(?MUjd?|j?XgfQS%HVwtY#bL!s>_(ZQZPbv}Oyyv#(rI%;>FRHyZRUGZb^Z z+*q=qy3`LP)v9TMkfIsNuX1Uv&lZ<}?^{xorD`0JNJwZbR_tC@3TC0^_8>ekqN<$@ zN0opcB$1rNECEj*ECKVT?qzdhEv`>VIxn$VQ2z?jNgzp zQ*MT7O^^>KoEoI_;Nm+=BGp(B+{Dr1&|;XvB#Z`s1A&&QZ6j}c<=*DicwIhg%aKK z#w9gZDOHM-R9NuUK11Zg+{o-Xwx#e~!BFt!;1rgCo}uIi%_u8$#c^sBkMivXSH@dy zw610(BX-kjUM5Oml~_)>lmBr6h$H!KLg&M3(}W7Vp=7YmB&I7=uH8n|stGFdaef*G z3DgOGLGh%w1T5qf#vZqO*-9(N*C;fhWG#f7;vg`%BulkY&P@yAR5IYDS{?pO4(Q|d z13WWG8g(7(nu6ZQCi69xA5H9<3!!R5=ekvT?34(i7;)Hg=Dhz3P8`c{qdA_susZ;G zm`0)}qhzO-?5LrIyS1tc=d5wJ-0Lqm9W5D!d&z9u$!Oia8=IsN>BMjvI;dBa8M%XI z%85*`kYtLOYA)b*m@7avoo73Fn-4poZHt9mv)h>V^IkFLssx=g8$&n@1lLiP^XBLa zmVl`O#W6UOm!WWs8=!VM(#B1*GBx@*GJzeJjvJ9OFj#rX>E6edfQhu?i=w8alHEqkktw)zL^rno&!1c*cUN+T5CbL zFLg74Q?=5xr1zL4yd0dJ_b&nMZ0s}<&F-gc8BLZ;kXX?Da_IG~LQ$zvS+hwo9KjXf zrEYBh_e;S3Btvyz6>H{vxi-LRpazhiU`MrL-At!zdQ~OSQKvyAkfm|l`Q9r)BC3}r z4&EzwD~>Q`m|}h;k0IJos$-BIR4OD~B?XOKbvbjketZdNno2Prvt4&2_%_j)uw~zn zIzt2>h#WFx95bzj6jnlp%RcU24URsr$6ecND1|zmBzr z)hu?U#`{-;)5cmbxFA(*ofpz`va*XSLGx;Wr}3v!eO2kl1QPyl;hUl zTLQ9~Q8iGD)kG6fhMKG>Y?!8Ff8Zp!7$mE&H$<6{RF)cDQT%61z;dAia?9a`K)0ho zH34wl1$adUTC5sj$VEfB~HOSbRbhYCzr`-P1 zu41szi4d=vNR6gyuZJ=c5`lLzI*X2~yk;0YihRlJkhgHVC$pL{b@{#c`a7>6W!L9Y-%O0n<~?s*bv~d?8sCMvRc@ccX$rSIMb| z2$gnEXVM;N+nlvz_3)!hz@pSC_;7E^R580)D&u`Ro9ty_w4Vx%$)wgDGy*PaLJGY! zkN1%!;8@_wQ!J_nr2tj*G|}|4+1fzMFc4Ri(&Xz{ zl1UkO+iIt{BIqLrD+Vy_IX_I5(wOSW)zWfs?!RXVm{WrmlNOmKmL$=vZRthKQ7RZp zkPSnJRw~^SidwCX zroE+ky!UUHfcb`{<+#kGony;fGG&jFQJzx<5N~$VP;R6#DhH~X84#fzyhxp zBg2laXW}w1^XWcO^(Pg>tf!_+ahzRQaw10zhA|J@6f;S}@YSAWPxU^#PIFx_FDKHZfv6{{vI8rfhGPKJ$(Nd9AouSllU z?i=r#dAq&Y+J2B|_^J-u*4R@LM0e_?;H*y$6-|h{86lWj{nn&ZYu2F{>7UcSHys%` zIsyLYRa+i)hfk!8NhVjkzh`dzH$-7%Zg4}L`E9PmJ>2BV2_iaxzY=KsxcBs(Y|Q}f zj9$pBCOMx$Js!O^0k7%&!0PAt4txAzSY;N z6R?9)sX`D|N$)_8k5tQ-gXTCYU}*2MDqhqEqAh0mPB~3Iv#*s$jcCuW(xqXiC|;;$ zmluez%Jz~4a5vVeM_sCuZ;whCJ0cByNTOUpg2fumRGQ_>(%8g%mjj$BNNJuLkJ>%l zsnkJvptXFh=3Sq8cE0D0FPP2s@Az8aa?J?5Q$ZM{SE*0~t4sMMO{#0)zG-BIMGW;l_(-aPG2nyw=&mfzVkQD^&34qHKf;+2};f(RD%!_Vzn$$ zh%jYAHZq0)-hoxp!h+D56cXBOl06`s3A$j{N*YIH?XvB*T?mv^OJ)YuWQS@c3uu6m zcFpUiD)UMC0L`)uPEdSUN}-mJ9-?-kL>c+9oZzfdjsTUzJw2P~R+s^NbHq`O99mI% zObr@vSRGd&P+!0%2Q4_Er3!w3+yIX6^^|PNm3s1~NC(Go+8|U2u>QE7-Z`W*fV=f{ zHS_&k4Ihg#S`hcT)(|*0zA}q>n6=O?Z)mI0(tY!lnNN5k1&&T&v{Nl84}s921eAeh zO&30wEq`nS1Owl;pHSE}H1-zX)67krKVuA2CYwslIJwb_5}HEJm{6UJI<|Xxn(tra zx~hIrnX>prE|N5%VQ|@6p=;GZ(9mqmpCN{cAqfk%8WJ`!t^q zsdQslRY!2K0MhX57jWH8=hBm|Hpw{`jozp+8tdHxxDya#w-#tvSL2|JQe4Yt`>N|> z0|-(ab zayHxCTC-fbU{Ht}PAs_6xXf#Gvd|`}QPU0Wi?q+V#bVXyF>Hu*(wCQYa}pC_eh`&~ zdW~bcLBG%hhCeLZnKr}5#>Frx6qjL!^Jv={k<6^G&U7Akds|@Wb;CEes0)icuUS09 zgt+a66ZNCL(acN!j?K{@gsIJ>(zlvg0~P8T3$6{>XyAYwl$aAW#K-%D4aF%`fT{!9 zx=12{*Le2u7>-K&QY4HWPf4|9$&dMp+h8w8w7_?zi3#dKT1XPQRO(o+>?wpnPl>T_ zaUm$QaS4Uuz<|t%wGgF(>-n-GH`{SOyPm^(X4&<{+-%Jk^DArnQLq7eGS2u>c;P(y z<65-t<9q2xO=p6CEE4g~)&IJxfvI$jled-q2`+A#EZ1>@dG|QCPwpseLk*0@x_M_r z^lJT)S6+X0>HojG^?z@jy}JK>;Gh35e%?&Iu{&E@FTeAA`$27e-`Bg{%3piM^(VG9 zk3{dNwr2og-MQGN3Ntfx!(t1=H!Zdsc!tauw+;Mb=IuA|znU+Gk59&HyHn7ywKli# zd?9{vF3)$zb7Q=WyuURrzF>-u1T({up}_JeB8Cu2r7F%?^9U2p$;Oy0tC(F?a!h`% z8#nK37jjs9cVlcmoGhdtMLRxRU|A6#bIwTogVm+^xxJfupUJ`O`2 z8gKG-WjsBs+uph#bmpYjf0#y8e)!>?D0d#FUPSlkS;i*UK!J| zI%Ox3Rfwuax+9INmtuL4>x>1onWnUXc-!#+&@0U5%nb*EO=b-+%-{ZXZ|ux23%>8A z?M)`!I!0@)^d#wbx-kNyyooE^+nV^eDsSL}Jx6e>VtfuX`dSz^=<(y$_et~l_D+5S z3w+IhKW=~XfZnrhdEuLE%FR~z&I~CqxEQ(h-cS!3Y>5*|PM4)-9)UMA&@z{KJ%VwP?v>ICjs$oF zcSibTQYq$pAbpAx+HFs!I9-@#Gk_COFJxiW2hJ@Vsx;xjIEKgyUKwW*aJvB5S<8G4 zIu|3Pkt;TPEDc}SNxU!SVn5{$3z@R+LzD|;Rjgtz;e8THxW;#)uX z#`bIt`%Go~!3LK0xvgQT&&L|JLD1sOYtjZmDL1W2bG`b!<@LFuKFemdZhDbdA76EH zU=rT3>O5{XA74Lao4wnL}k>+m6~qF zhyxR7dPD&i)@l`g=C!3>B(cjB0l`gLA`lHzDYaLT@QbW~;x=^A#ik8auU+^>inQoP zQnFlMo=UuryTw=&n0_zo*m_s44|4gq3Dv=CnZ~booigwG^8CNETiH7KKJeFz&x;Xw zF#^v%0^dQtu@BZe^-sTl`@sgu7S>tsZn$W={#*lRKOYO;CO1gH*lu)ao3-B{W59ep ze0-Dn;^m<30c@}ssWv36uZt%kP9<1rP{D;+IHEEm;h1RUdN%y z0u~RmQm!|yP_l_%FbwHp%S3UH9!|glHc#02=(Xe4H-=`iiPt^0NAh@GH#x!{B1MGh zh1^tN#>ot4%i|6lafQpv%6w70`OtUw)mti+fAqNh_<4QPxo~|a@dtEJf8ywK+iw1* z$#Uxr!&A<#w_bf|rsw69b{kx9X?_<}a*hWqm6yXoXk_s`E5n{NuF*rmtVMbwBsH6}r37y|J3g+Xe^3X(nIb=a#+n^> zRz`dE3_CwNKF;f-gX3(kxzS_MO((az*m*?Fn<>+4PadD=*|;9_3<15_;GFlQVVoZn zXS?ucIO(kp>ur<$rU6(P{^enK{xJ2H_Qu}q?D#zrc(jQiag0?-DwmrUu~ub_)ETJ~ z5fh&7x2Z~_>FDK*+;ix_U1u#_>;5zI`B+N-v(JtjJa64}cD!L(1mgVzGMSI-*)0q4 zLgQvPO7nc#uPs&Z>WT?2;+Gc!JRG)@sswlQ$?nB)qSk@Sf6q)an3Ru8)*xFFQm`~r zet@PG0PCZJG}KE)dZjy=484zBUOqZUUSFgyTaG#zThmKR2ic?n>}8O;1e^hMmzFn2 zxKP*6A%4L9+hZ}aMKs@4vICwl^4SYU(RH!QjvGlE?GB27uQm&FZ*9Qd6&H#E_b zl8vNV(I{L7B9$7eL(AcG-IilU&Q+jP&%x{XMHyy0V!okd6p=~NxokQFu`TPZ&$F(B z^Z&{HAJ}5Ier{{)=kC9Ge|Z0E?w{TJ<$HheUT_b)_pY;FJNwILA3vkd-gElvr$2W3 z$ELS)&h~$K|1a(b``G@w_I_>eFYkSPkKTLF?yv9u*zPBHtGgfA`Hh{w zvGZwA()_{p?`;3%_IGT5Z2OOG{kxZb@uly7srOR;rGvWW{v`HpjvVtoSw#VS-#yJLW~o! zlcr14Vo#$oO;+9en-G!H!~lV4mcgU#A#X8`)&wVFM^;4YL<)&?qS9BJ@Tk~Ap$#%b zWb<7Tkp}IwsiPKcb=VHB4h@5Dqq%Y|CJ8->X4$+KGFog`D;Sy`j%+dt4ffL^f{zryRuqrMpj@E#`gYrDGANSK zDk<7E$^x9Ca>-nFRH-Bm`4AD!tIcsSR>Fe;j@6{3UmuPVAfbiFXC{GK$3?szk&1DC zsP3OwBA4;1Riz%2OLduX>us}GkW9PV<>JHih$mZy95Z!rMy=1R|5{iE(-{j+iSvt* zNj;DiEL9h6wV&kc>2|x515vX|(e4e?3MKDqf&SMr6-8Ek{zaVZZj>noAp9W z8pQoEr8IqwB^tJ#9OW8yk#mO$XZGSevBdgaAp-Tpz*V74w})aeh#@CNKOJRA z@K|j(twd6QlPI2pOR0{$|Fb8Eev#vO6IBZ!HAn^=MNbh>uI|U=W(>=e6(Xy8m`V2dT9HK6^~cW2pvN5??@yY!Vq-826v-V`M{SY~8U~WD z^0`#JB$?$Ar}YefD-j}siW#T~G|8%hYa&6c5D){}t)PfmK*kY}6Cz!ADonmQR@Z+n zMEF&juE7IE%Vqd(gD=$TuGOX5NW!BtAmIa2PY5b*+cM|uJT64IcnzFnW*hAa$WXx1 zwm#&FDhTe4V6}RS^VM7~-W+=gQ(*Sr79t#BqN3zju#uXkbS8wOL+Msi_IU>@I7LJ5 z7+Kg?!PsQB!ZAqqiL3w*?K0~$VumK`y_!4#NB(e3se8R@4rARZ859+*8o3K(e#t&^Ho@3F~sH&N=$N{iQqQN zs+SYwz(UM4p{5~&Oy#kZ+ucM$MBN+o(lXc`#v@H$9Hd3iK4T>Zd$ixGrV}Kj74*yi zo`6~H)1O5UN_w1X-~%CryFTs;78;>NgSDdslNm-UNCV>Ch}d;D51~zC{Tf||-1ZPF zG(FRxMn<3UlXaJmPI^Mj(W^rY@e{I7PV7`hx5FazSXPi=${59yF@+LZ)lpoef-=GR zb{v#d&JYiF2FX95 zgP#r&6*GlY0}`C#Avvsqr5ULx(RvBZwX1|~pk&pp5)A@vmzjgmCDIj~8cZ7Pe#sDw zUIfelTqEPQgLFDBa&|(dlgWIsgpM;qe)E$jWkgagk}^O@5QV9*Un=#=4p@9;Osx~t zEW8#~lM|<9={;)gyFx_Sf&!48gY4*9v61b8qzpMER>}8!Ev;llymA?|9M+_*W@7Kn zVdE;vn%A222XRR%VU3Ik6A2BWIspyWa#Ttlrjk_E*D|%9{P?g4g`sg1vuY!X1e4Q5 z){-qIJ@I->49?}FEIO#!TCJu^C1&qKA)@5BVAhRIG6WT|dKfwxb?dcc3msP^s+EOm zY({}ax+F2u-k*dBSs4jgyd%>Ex>+i8Af!<3_}J#;1c5dX zYm~Nx+;GS=gQ1Ma$*e+13}t&w!iVd!Sk|q9-s$d#o}Dh1DFjN6Dx(N%6*w;+iPN&p z!(O3L8NwC8_C+~UaCmQmt%vhJo%f@qIGS;CtwhO`g#hk#`2;+YlaiuEkveJno>Py| zk_BzNB~&FP;^NS*c0gtVRY-WAlg)O(iQ2Lo2Z!_{X`Se2WfeA(rNpKZB69Ts8SRp0 zni}$C221GJB%Um3UWVrE3eF~|Ax_}!W}((Rd|rsiuyh)%Z*u*#i&Pa^Vq@t+SJn7T zi4`<4+RyvtNkbV7a`NWihlq3#Ny_6y+G<)(y-4KX8q*UXvzBO^2nmbyskX{P zd+#|xD1#B4LQ3Pj8hH6)MeDXxF{p(x^}NJrx>gdAaz*i_l)6(sK``kai&PqZK>8&l zUx|zgBiGXN203JucA9MT`F>O9fxFr|sYfc$IAX^$@NqB2R2kz_xyaU)cf zr1CA;%*84VB@^?QfF|5-&@I^vTe1rlC$!|O)NU(%+uwiS1VPs1Y|Sh{1O(}XqqHeF zoodKND?M&Cow}SN1tM2!%WY=+=T8uLvR$^K<=_DYOq57WiHjXDEZddNb(=;#DmP)g zbf4jAe-ArBAcGhOX7?0}az2t9fc;WS;30?|)3j5HMoNg&kvtNl0@->)h=_4QtUqIb%=~~tx2RSk0!LVdD4T7!sYXpn+kN0@1#=1 zBB`F4l(hn#?pLaMKBi&SdQd@Qqwc}gungLZStv9Xc(gYjM75egC!)t{ldF8(T!^~cFQgahF~~;_}uUVXzqji!P6gd=O@5hzY(e$gADp(nh`vo zGlF>9i`o;2g36U)ASU`HJ%%)Fu#`%>zI+LtA(-)q;XK`?A+FIWHivS8k3;3WS(9C~ zW8ragn863JNYXBKccUS~E9mJa+R`bT81_BWZKW);JDMno4q1w%hE%|t*|O_98o&Aa z5MhrQup)pPB55K}To{$u9$7Pr6D(hgLwaR0oCFXyJ)EYz;OBwnVcZ8Y>Jg_pQCINSa z&M0Sb6c$ywTXdNJ|GjHhhgT1;y!6VKT>in!*roq^=?RD5fB5)=oBRK`-`ro{v-ftu zQ~k4derD&H+yD3W!PXCKed*>8ZGQE}?`~Aqe|!CTYwx|a^&buY){kGZvmxDo+1ME0 zlO5))QjhcdsX8O~e7l^u#l|QKf=;f^&}Rp_uTiyv5a^(c$rATFisSMsUy2)hfOnGbgo-((;+C%AQ?J_>OzQOL81P1-hnD zR*|m~FdcW9BI9PkoJoZ3|4}cO!=PvPcyE@lK1C#FeQZYPDk71kP zl?pQ$T6&vmLAAQwFiXY0lc`0pgsirYT~HDVN^;I!(yh0_TpI`$2A)5Q#hx6mCz+sU z5oD+7w#p6)a`^Q?fUBJLs%{c*Fu;;Vps=Z7+vhH9xnbYx@u!3CTC!hJ8nnPq;)$j; zJ#ub|VjpU$7LHMl2o+Lt4mUZvfHpDx*qN#URUe*P&B?Ky+{8eo?UPDdcTO*H;xv|58a+Ll$CGwR@?@K-#~M(I^FZD+ z3L5%UV+fY{Is=}V%NCJOYpr|&$Qho<8GOFvtnmI5$gxi3n4d2>kRxQU>r+wT5B~6`6=iGI>&ACL+pJ5SLQWq39buR657q)zyolci> zt)!L9_)y8Ru~J$myN-sE1|!+MfQuaCP?Bdw3HrjE}f|wQ1#Zi)toq&74p@9 zO1Doc-MsUW{c|~&(-|CANht#4v`*wSK3{THNPqz3G*9FhpD#IB+$>=E2!z@ZBUXV1 zLmo!T7(w`4i*Xq&!j7BmG+oKYt5et8IFVDo7&&vtdRNI=U>zGk&D7WF=T+n_5 z9)K38pR_>j++{8wqqjcU%@zO&n6S%wR7%wGkqwgU;cyDln^?Xc7-a(Kfh4T~nyO`J zf6xVT^bJ$H$>%mgds;AtPf7?d=f30`#Wl5U#`R!9y4y0v=J ztq-2Nu%%WuKhBbPQ1K&qg6-)+zg7(z9SJ1dq!^>!G=|NACGmng^4O@_arEOCP1S&^ zzwq2@PG*7?k|KdhKX_8<7u-2m={z&RT+HbzckNA||F_n^WbN?7d;hfka`2D;!+&1Y zxw$_LJDc9Vx`HX+*K9tx9kzj_+27Fcoxb0NKT4-kKZe(_(H7AiMMJt8@7caTbn{G{({?fmt;;g9l{`-l z7Ka95T`waU@d3Nj9rrpUrUs*cHFC{pn+DM$m<)BuvJVF+$0!vpXgIKyb9s-IvT%+p z6=6MD8%e|H`A*hOs}7>CzQ(<|H}y8J8LzK0XfMj!U}t2%TrI=jG?H)@WB*9q)s*0r z_XubBFOc5|3x7lz`>ES^x&Qc z?wBc0Dw0Q7+3V?o)@!v;6C5to)7OP&vE)h}&(gBeBfs{z($~8-mtYCIlWg@d2&1Ej zOeM68e_gmP#I5UeoNd(#svV=c8E{Ka7FSHa_<+;$uA?ArhniJ7x`ODo$xv=3F&3Li zh-FR=wYcUjr**WZQ5{V~g<%{dRm30)n^qkRJdk#FcRKKtdTWH?-;pEKpS7)3`JTr;pf6TmA}uG>aMUJ*EO z2rJ7aEf9)^)dB{GbrdC96{MHpx*ilJOFer`-1&4JN>I}=F*k9qaAx&$&keT?+gn%q z7nIoQtV43Qd51+a=2^gnON}K%d*SK2p%9QbA%ciuGQrou;H?eKwCUMIT&>0Bj&57c zUZ7>$1wPX!tq1y;Eo80qb)(!ZnfVdo5)_(j%1t7fu@qJtw<-dJap%fAi=Fh~8EK|g z=|kWgrqBORTYKBu)t6uS*DI6DpSkQ^`uj`v;inEw5b599|I7Wx-beOoyC2+r(axXj zRJPx@UD|rjR(|vMHZvQ)vyoi?&2<5Qzra5a9Y45j7}DOg7@be~T&)f@OJ&I&N`AAN zCE5eNE)FLkP_o|}qiW1*G&#b*2DQeeZbWjV37rz6xkfv0()p+s5sPG^-lmO-&8G&U zlPwO!do(}Jw9+7zrsfr645!P=sg1pdj$armSb_B#xOF9!t+k{{jnCD*Uayj6?35Ff z>amuFmx(~_5Aj|`3MA=V3SJN@@L{#-Y zb)1RmTq_F=$<7KL1EpMu_IVIlqNS3VVMH*9%BUxeOGPaj)ndIUf9EiYhmI?uf)!Y= z6`DysPT_W{8W%HYH`ih5gh`vtkqT}X8_^I0E?e|_wudK~^C(b51uL*#EA`{W^a!NZ zro^&i))BJCeN0kB;?^cZNB-%@)&(#Y`s9*)wRbpPw(*~tb!3u#JtAt$KIRxOLV>wi? zLLkQ~p%~{;Pz)8U5a6*&fW~2Pg@7=`mF$b<@3V7<0N?812zfCFaJ^}!0P z*H(yVIFEvKs9*)wRe~7KqaYP3SRpjxVlU)*=r|cFSmElEi@gcqq2olTV1>|xRW|DH z{C4P}<9MiGg&PD`*|$HBf>@|vg-Z~wt*}*p9tA?EUi!u71uou^6M^tGG2= zKjzZ=Fa7?d*Ii0os$BZUOOL;Fc=(>f-#+~1!>>EcA3o*qOAfaW-g)rt2QNN&7RU?m zmi^;>asLT>f42ALJ$J9OhweRd_iuMUy8Gk1Kd{^1t?gdly?f{5J0IA2{mysp7(11n zFW8;{TiXwA-?#mgt&eQ|@z(ckef!pnx8$v-Z9Q)DPd0yl^L3lA-7Ig4 zn@`w0+<4E%J2t*);{_mNz$e$=y#CO7eEl(?61V+eM=#rY{`1$)yj=+aIdjjtbqV|( zJ9_C77`_08V@EGp0>c)-Fzjfu1cokvq1e%Q2@F{PL$IS@2@GBUgRvujF$|*5hCwuT zMX<&%T zi~&M`DVLTooNb68Z~WeoJY|4spy#N#gyC#M1h~DU!i<4}fhp&gFr00OFbxb*nlX?d z=<6uAn1Nbgh!ily?2Lh!&Q6&n3=0gA0){Bg7;qSva(W5F0z;&LA*N;w7z|7~xrAYX zAyU9OCT0vMjDFQo95CE-rXz-_1vW|HX`}Dj=>gb)zV9fu_56Fzp4&l3eeR3ck+4MT z0?VVoSmT$#7ML3aMoM%EY=MnYV0dtgVdMg{Ab~}*OJECZfdppEEP*XB020{ljU})J zy-osy{puyK1zk-7EB&e^umwJc1g81?C9nnEO#(;ryqRetFz^^(Ir}FJgKqhXC1eZ6 zI|*DBy%*`b12oR!TScZ)q!An>cbRmK593fK`Q-gb834TFe66ns+bEkM5m{4R1 ze!&~U~1F8y=Mt*foh4SsIxRU2>Gc;!ZZgWT9z|Eu+%U;mzUbG@*B@A~fAd)FVg z_L;S}gGy}v%56{o>pyne|9md=g@89M0-QdDzV`owfFD}~IDN8o?R!JOk1hht^~3jsfFD@^Ku({%So`h} z@XreXw(B^3a|4-E`&S{?KP}>#YmZNaT>p3jIekcR?c*WWKP=*!tNCAs zTz`K9o~z@>Lat9Q;+m_-M?k~KNxiRvmA=k$j zam`iaPeQK0yaCUd%YP5KKDLN!t|IRb0Uuoim{a@5A>c0-0p=vVF9dvK5n#?t|0o1} zcoAUE7T+5JKC}Qp&Kb!chJZg`1emjg_k@5CE&|MT_aB6SKU)Nt>(h6KfDbGJ%yr!F zhk!p_1eoi%-wOeMvIsEOaqkMv?7!cD=ep>f;YaVk0nc^D?}o+v@uFhpM*r`GfcGr` z5Oclv+achO76ImZ@3%s=@4W%f75YX>{Z1Gn@s^48iHD@Wm z98&$xjXAw<4XJ*65!IY!{ZdHvTQ}zP{$fb=jzv^+miP-H)og|iD=FH^h zL#p4nF{k(E!cTtv#$53~8y51m1%=>q7WXqD;MW!b<_z)8A>daR0p@Jvr$fN6ECS3~ z!ka?CFE0Yj_4iMOfVVCJ%=PL|hJasM1eoi)p9lfJxCk)UL;o!V{K6u@T*v)*2zbi^ zz}$(UwKs-ictn6j1D{%5g<*W-9&8mZ%2#KlX`T3 zWuuJGbr@ePW~@rAE}_XDa8c0hF~MH!vxtP53tFGSZoHF3FD`WJR(Ldr-o!&wOGs3QHjV#o4&}* z%0Omi#PhfgQgY`CY)eHlJslONWsOB;Ru^5Ti!=*y)remM9~wXr(KR@&Ysa zZp8%OMetTVUK&>l9G1!Cn{`B^kd)Ic#$-+|#p5wQPWKhR+KIN~dQ>TNtGT@19Dt^N z?!!&J$T;UbBNJB`M&wg!=26ttaJG0sO&!mpGRmMuxurHtwv{X4YC4Xc(bSCBC=XJ& zo*wvJ-WN1ITTB#|P9!9+CQX|<@^Dix8rJyfIi*}k2`nqbnbM=G zsV~Z^xeTlIM?$Hf`m&jfiDd~Z3bWBRzTK*sIIMV#mdVvqp3!tjS`MIY1k${CU9zL8 zctZ^$l#(vXj^j_8`Z*6b^&*FepPr3{azTiv`Ls}cG&OZ*azR$jCpwjArmHHIp@gcP zWZTMR;_?|*%{F>+o${k3hsM&7murmzLiP$~lvD$(C)G-JOX}pZU6x{JO@000rd~9a z;HT%$R8mo*WkqHmO}-k=bS|i=W0{HwB~@@7Cn)q`F)y&V203Rmb##;;Mgx;=1|S&KsyLeZOS6oBr`S}7|s29%k)e_y#>)`q>0ne%J$Fq_r3V(HCrVk2yt013$aI4Q(u&?76dLQ*Ue%n>8MJ}NlZ#{6-}IB z)l9kCjrb8{6tuxfHHR9YPATik3~cJc025NN*1*l8ZdXY|Gpl~)!%e;DO*wvgJ)LKi zbRv`C1m@Ay)Z&HtYBneFH1O7VHv!{{QW67AR?lc^u39K2!ToJfiqf#I-3$aXLO@Er zuMLJB!#2le&w?_FfKlAcSKt3|Q!m;iI1YD zhA!+8hVLbfa>-A3giI}3DHNa z9ML>0TUo#bPexz9vVaS=fqPdLaKSck&rHB7=f5x52JT*2zy)`~(^nR7!CmmQ z&<4)$-7b1E`m&V;TyPgWb!7n;+yzgW30Q&jqPyV9D+{>bE_l*RzzSpgVlxm-pZ}k{ z`GK`d%Hc;23kM%MkoG^h|H}QR?ET2zlXrh;_lZ{rApU;>?gf0qW$Mz~cfMxl-tBMP zzJKejTiMMIY)&>lzVXn;6W3qAPOQCq@hgCT?@u-_{d<@A#qz$`0*}TPFprgSi@y?0WPHrWczhsE)A77sqczNw&Gy0trIDd(bar|sh-UFb#6*BFeyMmv)LZ^BB zEKm*#%2^?w-nq-U^}dXw%g4_IMcx14mtJf-zVqB9Gq?RYZ2;}~{;3{N^$ShTccLfE zXAUZT-$|uc$klh}tY_z{^y!VrX{C>@96xO)=!qAb0PH-1<~BBZhUOXO@s|NX)1kb= z+mky#3!Vs?AIdoBlc%2a$qFwE&Ryctdz|LNIj4QnKYq%z1%M&0@YHl}J!korCxh~) zzU4w6eotEbR^Rd@P|no1tgsF~cfD@)E!y!Dr#%Wvy3j@JNsS&J%1@XU0{qtsNv-aj zZ2nv$=l1w7gq}9CdHkh7&vf7v#&FS&=HdyG447zDwAoTh7RRv>$jVcu*lE5f*8}=kqxW z=FgG-|D;E>UR!evr#H~SMIi%c^cAx^wT~Y=ZFFF1W-i$qsc5w#RZEm@fHdi&Y(0Zo zN~F>3^;1^SVrn&ZqV%Bdxtm~qteVHqIO*gS@|>P~{+RD%qJMntq=GA?H9dEM^A$9X zzZ{e|bw(>BF+F#Aw>qPHK{-=rv_h)VbC+|gGXlfp9#GWOQLK=Y^xQ=)9VT-TG1CUn zj_;o80aah&V%Kx)dDuHX{iM<>>_?y5*)Om3>DZKMr4P0?)~>Hl);51)^TnH2um1Se z7hS!4;Qp1pU)<~M zJ!SW|cE`KV-1#H$W&q#*$o4mHvm3v(_34fN##7gSdwl|O_!YJ`)}OWZzO}Di`g{ab ze0u2@{^Na^+j>yiIQyUmf}D=7y~kr@CQ-3DGhQ^rn2~Y)Q&$kI?<^%e9oTpe-$?38 ztKP{l`I;M7?V)^{aC<4?>6o&6ynH?rWX7@DARW)NhLUHWCfr&|c>3_iJsCksNnWL! z@I6B}^L&;&O}M#?aPH9bJ&Bwua;&e9Gl|Kt-cBgqX~K;qgfMmc+zLVW@NFYM(m0`# zOmc~A7fhU|2-n`Tl<@ZD3Lwl#P0h3WZ4i}N(n=l28J{Nn{iTF+X47hRRNwbIN;1>* z?MZetI8FFYFtYykKi=qCQlre)3#9c01MjqpyeCtMTkOz{M;l3|)7Jc0_f!L3TGAoN zBeB8trG#@Pb7!MoTS_=*GV#iAV&$7dqnpc)0%ugxPt|zse_u4=zbz#^eE{;FJ1hKO zO9|(Eq0sHMN-mc$8;x2taLqZ#u=cMPP53{T5}tk~anGF<{>xIrId5$wb49t?Zg~Y? z%eE5WI=<7r{LdFn_?e}Ib0hE03jb*-;oN-UG|DxP(X(>0HHrBOZ&glL`5%|LhB-br zNR9h;ykeB|Dp&E1?&%rya3wk?r<7u5+v>F2ie!!bO!kz#gJso)P9Jr?=g!srhoyvb zvz_WTv}}Xv*>17k8Oxl1THxBJmlDpou69ET;{6Px7Yea>N$fP*r)s?Rw@V4>)h;a@E!oO29CT^m?xLmVUpUhk;+N%Az|CzcYLeGvRpW=@S{r!=f+*8q2>5ks$zpS1@KVCL zkGPM7qSkuhpsUGVx@^Yjzc z+WVIg&LwKP$ICX1idP$1ZoDxV7FxpGNcp4b{{LNTH`cC}uDtQ`zg@0edh?~7!`i_w z9XxqI*n7|3vv*&y^KP)`|MIQ(Z&91yvhj%xcKw?`#Q%-YuCI@tcJrRAYx~Ci(%Rbo z-v0LXgL|u7CwAMb<2&Ciw%-4Spa1%2%znFPui)XigQ-W)xq0{1wRPjGXA0M+3U^l7 z4n2`L{|@lZ+j~1xrN^(F{a%!tX>QLA=zaPrz1yqork?1Xe}#Bwz1vfzFZ;vk@1sSz z+vfFNyZN-K-UmL{zFVv8#-8Z?+$+dC>)e_udD*jp zH}^#6t*?&vpC}Dq2HsiY##Eh~n*R14 zJh#TD-F(XFalgX;@QK9mrRbd{PPN(4?Dw~yTjAX|pM0tn_pYt5yL@{qCS4~O8ay{@ z*R@$q zY5%#xw$q8k6aRTfiBoN?X9ROUMXMFqK%$99>G@f3AgPwlODUAoK+!?r{#)GN4uXyY9_w)X6fyTM$9iZ_` zPHEg1^JFzGJEp`j-W5Iv278`u*}LjXSH{W4NNm*AozGm9-*8^%eK!xLI&YgRSBUO9(Rtfkc}JzQx$+rzO_koaf8G|IKL2lS zw%0Cw-TpfG&li8b*aBZ{fzL+^yyo#YFYOvq^;^jGRc3)ZhyJ|e-qE0InALXu-mkbD z+%fi?yYF>IwxRW_1G{e6ZLu+iD~ zS8&a<@2_wS>fQ%|Aj>wq+bXm|Wblk&G`0-T6!+dX*-oX{gF!!R(tGdR1yg`yn4q<% zT<3MYQ?QYx&l^swZB1F)rq>=B)9=*Vj@8q~rq-WHU2xwIH2tdPX(P?9 zx^@pxm~OB4vMJuMMlJCF1o-!?!JLr>n1KOIn{f6&)vhs~7P>(C+}dVZ*1&Ez+vXH? zd+6uqKC3#04K8b|&a4f_NeIFq3P&J<1n`#Tw2Y}4zT{q~r6D*mWlF2!pgCkcgK3$q zY{tAU8OkWDV+kc`Md9oDp3bH#lFqrw=DpLZIqjyYja>VL=#-Etw^2uM9NfjARkL+w zFfF5&B1TLjFH>%u>PY!Q8h&7qBbw<^B3^coQa?plwM;SAE_WfKFsy_8W(2BE5_Er@ z%j0c>ZWoF&VbTHGV`;AJ4*MRLVwrniHr3;rQ$@yfB!bb_G|sMi*tOM}i9OsGU>M(S zi=SJhOk4D}Zkqn#R?B(#tJC^fZmrkW!{%AtCP6Lcl!S&kr(%Jz%q@matG(UMr`o6A zIG2@U^z^G|TKOXwe=mFRWiQ+O+O3<1Q@{N6Ph4MR<$GtRa8AFBd?Ebu|7SnDiesO8 znNzks|G+Tq*Drw2Jwpf>Lck$`NcGte`mFQBfxbI9&uJANsZ)f(9j5?(rc*2#aJk+b zAZlAA6W5a&zOLCES0j`W)Cfv}suoklZjnzmoWUb@isMcf$~20JUcniTMBSyL53t%$ z=%jK6=hQPw%S{&R<3z=*6mT?QGljBsJ>a`$IyYd3Mqt9l9Fyp)8A=@_I`PRk=c*Y7 z_~>&u#c9>SIQ$yt=D~CvzQzUXqw^ohzp!x#|Nn9vuJBBG)_UjRiNpB!o#shq=GLV@$oHSDHU2g7c9|6h9J``50%^XgAuedX28RqE=_ zmG@lvzAM%h<;quGxpeu{mw)B*YcD@|`TC`QzV!A>-*%~f3Awbkch})N55M!Ub4VTT z9K7e?`wpywuR6H2|Ni|q?0fssz2yF5_ddAy#=V#BesuRuyRX=lcAviU$(^_CeBF++ z^Nj6JZ~w~n8@6A&?QARCU%7qn_WsrduP1v}Ed6ZZbd#z$QNVinTV+)B_Sxf(wV2Iau-k6(Y7G3xWWP7ev#%6&#~{MZB+D@r7j>Z7TEhXygfJ4~l3 zXCn7`-6ct3U}MF^Z5dR|}X+Ult;Q0n1kl`9x5uc9j?zOE(%= z3x!8wEGhdPBbPGbJ#Ap}6Z!C!A;PI8L0WcdjK?5F;Uj|X_a?3cMZF0)5E?gGlk=PC zAgw~``a42|-7zyQg~=r=^&(pe^k$?I7pm=y9_ww?i!F`JPP%H$8;(?r zIy`_jjrD6(H_>T0*+G{}(BO85tkRY}YFL~k$sjHoWpMYmw_FE$h2kRQ%LWf10a%rQWjBDAHfzyFA zb~~|Nu~P9+ia)?k5D_|C_5!S&j&;%y&d2+N)wkM&I1v!W!zX^E-yf+Z&hvN35YZ5{ zMBc;3bZe-TRH-4=Jhm5?-AYkx)CD*#MPROvk%Z3g{#}SLa;Pk}0k=1g?RBotR4^$E%= z1z@__jGrJ1l0(S-G#oI24<0=yg;FCD8V;kp4U7GCqb$} z`-6BVs!NK{ggrx+5sof+)aLU;RardaWV1ti6iF5aI?az`L$s$;sx2HQHnxaPYn?H$CJ+5CWIQdxnwxq7^Y#|VLxB$Lu`qm;0r6p0-Mp%WEKaTsp6D#vFu zeo`Gph#DLrJ;be#)V7&wIN%6?Vbf|)(!(P7e3c)=33%MFB|q08V%azhUfJ5oNgTCv z$bePyv@HpfioW*OAtGuhPPYOp=%_wPp`At_E%%5F<@Nm$H{>84cs5vea&4R1{HhSa z$-~shvzT5v)oJM+iZU}jOM^Q`qv6I2(NU2}BV|z(#KG>fLj*lS_-=vJjNv%rmgszu z@ushyD`<)CS5?PMrUMbnwq?cN`pFXno;R5YSWGF=IO$|+g{+rXAflCuc`Z9SBvBt_ zn!O&FJ@#K0A}9{?9bg#Xnh0R(nQFzzm6aUcY8Lv@T!}9G_NYiBsUAYDI+mb94dW=?MCZ8U?YWUI)?2$ab?64LIi3F6}CNK_39|r&Sny2BRXKBC_8Av z71pWMn(ctla@yE!9{lhLg7%F_kc>llG}+EMN~cFh3T%{iNj=5DH87fCLzI%zxO;d{ z*d+6dTq~r42T5(5R!1p3D`nyio@kSqT1<+jOfWejSfW&qxAJyW8gw^47E;Zx5?>Hf zMaHII^UN9zk=bS;*Vf&**CUHTuVC3#TW$$_eNc-hV~)7~s)N1Hti{)UbN$jw5B_?$ zwM*=7?f%ZryVw8o!L^<5U4PMzx%0fkSFHd2<>=1A`mb)k@9?ABuit~#{$=;yw%u)R z`!QR8w)JCMFFnj{CHH@M>#og@ZocW#U7N4i%w2lp=F``obMUH-zu72n{KDRE?0w7O z(=H|VYL`B^_M7|v>FPT-?z{S(S3A3}yX9@e^j*XIFTdgNO_v{g+1q&a{tNe?z4w{@ zzdI-$yyZL(z_Xfj;6^dI)K;i2S_IkH z9Zt%rek)J;21r!`LnRBWo7$sRc2FGK(Sp?%H!*7bWGJd!a-dacMss#PFhj60 zU(1k%2`A;V3A!`May_%5juXxHq}$InVzLnvo1My}i%8p_4iRiX*R-D1rbkkmXsV7T zR{0igRNB5RC-Xj$ZuHATE(i+F_9tctE>WrFk`0=(1v_5r=n4~S;1fAXc|s>&?fMK@ zp^No_)iwruZw(Phmx#vgQKoExtAZ*v8HXsPfGQ3Y<)mga4>qGy@lm7SNXZ*-3K8{M z!_Rk`bYc(@!2+tTwWzpY!m3lvSax3;p$6Cr9i(`TTL0b{GbHwtj*>X4r!XT>=4`wnM&0YA{S*Fv6cYR!b8dG0Q79v zs#Vyag4tC+Jp-_li4{lFnk9LeSkf@^Syc|eYNE`i4FVq9xU7(460(Oh^d&P*tS1oQ zW+c(bOmZ^q;4cDI0F1EZA6DNRvA#WEd(69SpodQymRE6owcjond?E_K>NEC$1+ zW~^5oHn3hNV@jx&bm_5>D99Tx4G|6^jRuKgz0lJVg&x`R5HnJ!kxr}cIQ?G7wHy65 z2)7had1F6BShZxg+1AQwNe*hQw%YD1g$flX2{M_q3TnM8XX9$bi6YGUlS71AE2-UJ z!b9cK7{|I<57PS0K@KcEiZsl+9s+|xX%*Jw4Jt%5z-mM9^mP+YVFsa(I0ZbJk7i1YOgfvKbOlsHqkVf!YD#`o8|U*}pl*Z?k*(D{l4wYB$5bOw zg~?aBw2*8mu1KnSc|ap^$&ECIB?J@^t_xU=C~@(2Y*g#30^j9LCsOM*W4P6e#Hvzt zJaO|vbcF)*8=4xA5|NtG;1wNhnuJA52t z3wb4rdKCkAP`egnT4isHBMvm-f-DmwyIm!o><)=h5aZYB&<>NVrbqZuBAQHjbb~d( z%mdq*25i9=1g@0SjZ&YF5Qdf?R6|h-gl90spNv?uhmm-ET!9Ncp@EbtUaQdORN05o zzE<&NY5nyfLI4l(m~Tdk{!lD%Uc1p#%B7wlf}2*&ENDg3Y{`nHSxWQdsgk8k)mIy% zZmx$`^J+d4#8QeJn;>|u$q|B;$T&r%RPNF-eJgxMWF~yoXZ4&o&=MUarM7Zflq?tc zC|Kwi<1VemiVczMR{3;yJ2O+ocm+M(L|Zy#6JWpHbXzIQ?2aZ%qC=Jd0xLquyF&rt=Y)EYz;OBwnVcZ8Y>Jg_pQCINSa&M0Sb6c$ywo8fB#rb=Xc<9d$k zb-@KWeaOt$@vf8Yl7ZP8x@<5|Iay}KPL0yHUmKQD?t*vpk%A!hsG8fNm~PP{QXM+DwaA(dJOK8kxH^SNK9&0 zbkz0~q7@{^i4Mg0`v)^sY|;_UyaryJR3W~ES%i>CY4vi7YveJMma1O2(`|Z0p%?A$ z{9ss*^Y7?wguaKvT!PKjux7H$<;|oy^q6vG1Wm@MKhBR>E|$Rrk%$u60NV=Jh#ZzK zMzlz*(Zu6WwqWrQX4ol30vK3Tlgq?Pnp%VG1k@tcb!-3c*1qjL>xzHh?}Tj?uUL)B z(DlaPd5@g9wE}@*ok1er(ZDuQAVvIYEX9p$?Xq+DYhi!12?ufpc94dQq^n9&UP{`0 z&S=AVH8P4PBVdq7dAeMR`g_trX$@JeRK0)@+ZY3=V>NxIc`mJVQ0}(dO7KOvZ$okM^si)gJrctvxP^#TZXQ zyVnjuCE%4R64s;08h+BuQ6(nclKTw~$8uRPu7(ifMC_#L(zMvqs7w<)--NIKI6R9P zOw-JcY|-@wP7!KlaBBo6>pV-y#E2)-9ZiK|z!30&x&SYFLOO7VxTv3ZqoLpyhX^>-Uv9>}OYwPa7*BL_j%~Uam z!i3s?Uf3Vme3wL|K|5{gs6|^Hwu7rf1H@UOxpFNg5L6T`mg2*nC~k*u+E{RLtv)Dl zddtOOASxV+aNTX#9<+^|!uR!SAL zO|VGl<*tD^0q4FP;mb)kpwOO zWM4+QRFZO55~2$>GuY9BbB+cLtfORaySyx&sSw7FHHRc^QTxnd|A_>fkK*98gP}^9 z2;LUqrlnkuS8G-;*U2VqHQrhdCSA>(!8Ebfe}XD`D@cqpbP7zUL94tvnF)tUYOr1A)!h=<6%~+g^X1!ILrPa3KzBmmo<-{IfP_bxB z7NtZIZDg!|u~;=^$WWkNN*X+@ozJ!k9z)dXZhG`nFX=x)bRq(sl2T^e=%hp<4+1Zp zV5c8$uvRV+aJI}=OA2vfUdgTP@8(&(bQ>wPEehpO`F#$8 zGPef^_o|<7i^VkW9u%e~Hu*qfHW8CyzcHKbSiWV)D=nUg7Mq0vY4wy$8MNbSI4s4m z$!W^sM5l;`d7tP$;NY=LEAFS;>(yW+5{;*+LV;j53J#CHK_gy$tLj=0iBe2R^YLo9 zfwt^)tYRZ`1=N_QkRa(3L@82d7$j@U7wS}{4nrF(f>=Uh1&wIqGv`MCPdc}Xdi=wjduN*Na}@nKi1|-GmD*ydq-7-i z3r>JK_IpT z5PL1h-9L{t_jF#8Y4_nAmHX*{$7#g8--M&ECqzX!7i4m?KE1C(OJRN6NxK~zv2{kG zI|j*KYP5p(V6hlG;BmTLHy5i!BOT2-s||4-@zxw9>?_yG1}@~VHJX-$m|C}pes3aO zrP_8&qnVD;B;pQ%l7nRzQV)riP%ICMp!vd(BL<-m41&7Gi>*gM)4?cv!NJ{Sd6d2M z^;$Jq@~TH!1oYCd+(W~2G0Lh)BIpg0y)g5w$&o7V=NvZ5KATghp$U0gGs3n{p;YBq zp}S`i%I>sFRQ`S@U_*f)sAIz*>Ifhi8oi$l`$7!YP9wg0qk_8QO>crJ78#@@G{H`` zu^0@|RY%+7b)#U$Bb)63WD;hZiHJqwn2i+eX|)U%sZ{ir{MMQegqxDtrWl36eiL4Z z*?pBZk*e@DsTyfZ;Z{3lvQqg*3v9iZeT1dCUiG>QDI3qV@u+P)#~w6&$OKG{)ehqS z{hF_6Coa$)zdAE*oqBMB)O-b;>Yq8~o;rB)CzBtazIpO3+AnA?T{?K>J<~5*{U*o^ z=$+LsJp}FqY-t`}wk`f-@#BlV`EQK^-T!Ey_&Q*ED{LVbG4K+$*&t2jbTC}6-s8ND@$ z7CMx;(vR|U+Vb^=gK(6}j8Lw!ez!9V<_nF8$3>S@DOVY>S`Be8?FMm`3yr>vg2UV< z;IM~5U`I7rx0Ea{%qp>xwJK35NLTe#A>1IfeZJV>B+A(gGl{&V#5-f1w854EZy8eW zb(g$eI|uV@fe9sXt^|unHnx!M%aFoc4weG2mv_{O0M*Dl%~8TifQ>XR1?7U(5|nmF zD@@YL&-Krn9i5cf;%5^e@|6meYm&R{Mg+)*Tik@$h{qAUg?VBTpM#b%GyNTErx&SF zVa|}FoVf;LtvIR{F@c2iRV!U}p;AbM8hOeudgF%BVt>!hg>h~u>^NJDIM4C@c_WTg|!o#r__uqf7+Bfo{ft=U`S!V_e)%S@T%RkEUyaM7ph+ zd?y|Bg^F#{Vm|`U0Zw$QVH2Zg9NB`-Sn_8$m(-}Ta1~*ybUex<2~&lEny~;o`#(Kh zITMlvZ8c)n1Xa_CD4s`gv(Y3LN>T@G=aV43-p%HGjE8OM7oO-p5yC1dxRGrlu)k$% zqNY%j;49W38MM-28xm-{9JMgG6bGuAmcsofvSM9KW$coRr%XgRQr7FCG6AvHG9yv; zoEOao@*vh!5E}J`clDo$7INWQ&Q$S<-a=cbo9kT7X-f!XI)aB>0k}p7bV8w?ZI%rS zm-dc!9i0$MmI7!sDAsK8bhK=1yScc&nV{TE)9hur3OMrA#o=}gZ}vnuL^{w+wc>`F zF=*j+61dJNYsOk_eVDw$0>5G)XnV5I=UB%C24 zUgzfq?$I#jGiX*!yBjQ7%Y<0Q(kc|uNGFDwvsA=dMx(849JfFzb^`9psGDqT(Lxsz z2Gmyxx)KSd+G(KqV$)J%4ek~f&nHQuZZO6ZGll*W0#_-yQU!Z07cGJiQBYI3#Ta?c znuJTW2#9=-R;ykCZD(1-+-CoYQoK}bWebwv57=S2lx86}?COO1O0AHz_#6ffYlcNT zZ8Pd8`|WV*t8l`dm-O|Lm^F5UO1KjSd$NhB6Em^}U53F+P!$fvJ3)5kRec%7Iu7zC z^I;R%9Y+h{1k&c9V4#*Yh1+EvFhY>xo{F$3Yt6p$;@%TZH>N9FYH7UcFB`0hdNUfd zfk;Zo?Z@JT#m?87bf{`;csYZ6`To9)wiL6XAkc@x!~zQIO7Tjlj&fL@jrgL`gb#<& zkY9%t6Q#;hz4wI6SSk|6QmNXIO#X%`Dz;6TD%VMSghVJxFma0>lL*!!alXX#?foZU zOG;NN<^@-)V8L3Aaw17N9d&RXfLHZwELqn_7!ZNuZ|e-R{U8Guj2B@lV+7#=ULlh- z_yUYoG!h9sY~VAEraqns`!OL7qsIE|OZze)Ayc)ON_4GNFF+9!oG?p8bG_uZ(-9Ew z0wqIW0-VLl#w@G3t^b6caz`9#2Z*#O6@@^tUAH7*Pr~9ixb3_xL4@lmTfS**B5k|& zj@}c_j*9}J(ybcRPFsUEM>s`0!n44j z-I&uAcUARCS_-zTd6dcVAhw7!y3^?3=OFC+#Ws8GWVqB~Mg zOhyJzfRhk=z5-#TTC^aA5*2g9Y&F;koyAiWUG;`2RqXkq6ejuE{^7i{8UYoA&2T52 z6)j8+gdX8gz9H~7+>wbm4JcXk6e`(FGG`WNY<(FO*q1HjOyx9Q$iiT(#%uxsqR0^| zY0v1>89DgK8^@x7v^epx{u5<=-QaFi4Ys&HZM38YU$;#t`$)hwg#V$y9vs%dA_&sC;A-Iq}`IKpVf zlqm%*`6?>4u^5U+nk{E7>~?rWZ_$nB#e~mf3)VF^^q=69euTt5h~3~eCo62+4>7qW zQwK+CxoEA#dK>M66oWuWbY}Xf{uAKb7Ouxy1yRQsidIbTt^{$fF7JzaQ#dcB3YI9- zBoZh%UZ4I(|A}0{>;doQ2+zZCAqiVOTpk?M86DM*GnB_V(R$TZ4hS)j*fI0w{u4}# zZP<85*NW=X#)94)DYc4Cp37!pk$A?#c3^vukA!WuR(&?sdl@)0gu#~7JB3)>7C_n& z#%i}C4sd9xbBdm%zd)5;IiVg)>Ug%)7ePaC*zW-&U>f9nh-tAw(X5v*Svv7R%VE#A zT4^0`OqeLduI-;UJ1HOQ@g%(Ns4rr ztjP|-`Z8@H=wS^G&Oy79bdyGXTtV{ICqL0|c(^ImY6TjMu3nEX30{PWWToiSvG!K5 zR%ryXg^D*6;M-MOWiX8ibsTcQtg}NE^u9=-QfRgUc?;_i!e}y8LbHW(jn2E7OvgU4 zuMaAeaASEq;`Oz?Hdof5FZ$9{u5Jn^!|hr&gm%23nm6R-JB)p@*nc8euQ=+ks~n>6 zJVDY%V~J?cc|#F*r_)r2&x2qBkEjE&dG47H^q=rWrF20r*fA$oG=pm1)uvZ6S=mUt zZ4T2ej*9DOCV+tJ5#8GU8_MYbGpVKti$)Ck0L8lEac>ewFk3rCma$TajulgEBZ3N4 z5}*2ZUxov#@=a?n15-W}_FGwxx8tuigUzP19<(IDFA$Kdr@ys^oCNR$gocHF?yIdiFo`^kV)(!1S3KF*S{QZ~@A705cm zLU|t@G0k1uN7YIj-NiuMSPXR73>vS6e6BcIch>M)%Wf;!T^+x}74x&!rfKn8tNlls z7z|ayj!MWH@KT0G#fZcxmc>OdQ?8ck2$Z)Lo#u|{%d;zA?LT2ILRO0>Z_Hs(g2as^ zNJ6tld^($;O9beAS(i(?16&0bjg`g!=szKGI^YCsF=b&Sn;G|(OGc5dOL?y@ieoW* z%?72&V2*J3GAj@EpRn<18&48(kidqAauqxj<89f9P$=`cmcJTe4HiR%fG|_XGYCeB%jZotR!7vZexl%>Mbu9$Z=BCFxL-#bmEAcX(J5ZGP(3v zw&-L*AT85k4Do0ykF_O6m)3J!0S@{_?KOjzhrOaT&FEP&9%@!YBAX0g`HGz(suGs) zm5LoC;S_QpQcs*17_YI><^mvJ3(3^|Mzm>7NIIx(7uuAYLN)z_6nDo1mMLYBmNZ;{91Rz`$PRVIN-oCRc1 z;XWN@^JZDEqm57A)0a^pu#7+j5@mhBZ;yA7Kq)}m>(-9N1PN#S0h zN?AAC2I@>jNk{^jxGBDYR%m26O*^txGOv8o-QNY`Qs2x1($q`Yu?U!)T7quIRs8CPC zelG3gS{WpS1#GNH;5`{|PlhXuW}GoL7_o2(*k8;bge3Q zQ{Z%d$`SX+Q79nQd$e>J(84IkxX2Em%~L*S3#?{A6RRbVVju*$4wR+{5L1V`1gPn$ zRmHNc3MO7+Mf3ljcPfMZ(jKYIQN*&*M#)oZaQ-NWfs^#8-#JueZX0GzNm;z86HpUu z0KsmR2+p!CkY5zkdp(@RpNWSQb})b0%egpnByFd{9l~Wy9c+jPO*2lm{&ZaiQkHT-ksQ z^H61bHI_MqI29Kh3|QbqNE}PYELb9r4^x?wrND^<6JrW>eF_$XY$X{*Dm4>n zk1|o(p4XX*AZIWM^BwTU{PgorWyD@hGVDbOq-upoyCq1Iq%I02Py%ufQJFO=RZIF* z4g&iO4M){HJ}mF=08#l#e3Z~7wylyZc9 zsj?jk^{3b!#UvvI=FL_jtT%xD+MuTvV3M{ZqKilYx<*SS&M4|**%a&|<8feRpM2h_ z4BoSOnacu4Y(hRn`Mm@Mfvh(n5a4^O5{1e@Qbmmm+2g((X$kOgvynF$nvPOP@H_pw zT*qtiA=MDV<3gMVR(99(P-S~%Wp=ydi#hBG3etf(8j(nxfg@CAh-2EyOdfNUnvSq& zY0znq&>iOUNZFg|6f+HHHCuJUMt#Y}q`hWfWuK7a|EIKvX_n5PJ9O$j;ExwR0}X(} zz%P@7rsTrF*6|Z>j@tDy9z|hbxd7#mpebxd~vXC24rjl!}Y1 zh6POxsmBYNu{8#LSM|#6*tHn+jyt2|r?v)A4FU386b8m%%m@K!Lt~t*^>C!xWZms1 zs3Dr>-3e~pZLCAqO1AE9GBgTyp>rbXu2bTH7X~I-S0v3l+5*#tMLU98S_rt6HMc?Q z2~#MX*JBmY!K|0ze7xXk3jwepYUU*OIvFk4P(4V(i_^SK0xABOw+iO`7Cg*K+3~8S z_nHnOlt5wNf0F%BE)1-~y7tTuhZg95-us~%fI|v&KV6r1M`7T9kpDdm;5jJ_jKL%7 zakDm@FQP=glV>EC#$W5GfhO0sIy2Vop2E{oXyjgTzrZ z=gZmC8GlSNgOuVtB06neYYMZ8gj34q{7p{=r1l$&a3oK=jEQt1TO-kR!mtslv>I^% z&hiFOxK6Ct#=4}S*K|2%xX^v6>~?R>XxCI2)1JBA(0bhe@Yk8z_o1TB{niVdhKOErt~9VK$Q)6)^s;KGs<<@TI7 z$lgHZ{|>phs?YA3{|$Y!0)fv^WaKMO@|jdI335JCGWe~$`?o*zzLYWc@hCv{Z_9WF zc<=diO1`b6_a_eu<305^sPv!8$gS)BXK?KbXsgWpsWRkpVEZls&vN%3U?4LF!I)g= zeCSJ_4b>cAchMSUk&@M#s`z2ZY9<|awB}w9Bya?&l?=3}zJb&ZJdhcb%i&QdoJ%Cu z3!IJK2(-9XQO`&m$Z5~DDT6;);DgqdCk={_Z-6Xvs#OW`mImY$ZDhq}s^ntLI2`ds zf>N|Y(H?MI86w6DWbQRxe#tkxFX_GHJK8lBKDTG1jeb~$f|}7reuwVJAW+LdF?tNo zAovHpfd8Hh0$6s2p|`X=Ut`3aQwT7b_`d5)qy&m9>e5h(xHyW(`^fFz+6x! z3Qj765h>lNx%0UIUn_bL${w&##WGp77jiZG0cQ}9oIV;ZGd512kg&8(Voh{BWN((U zegX5P8lrg}qVukHINc$cK*DDXl;Id_2v9z|WHQ%mwQxNVZu;H0AF0J^LXqn_fAq znLo|EZDwNns_EmWJ~3sT{Nbc;;=zf=gjRc{_6We@ANOH4x!H-(tCzEyiQA8I6Zwq9 z^44e}TS%AF*-lV#v+UrV)W0itjn9PJ7B*=%28~8NR%75fHxWB$XG~Zo5M46s47}=`6St&a_)LSg?_e zrhyT%3`Dj((N+Tz-KIKDVkE~|1<}YCHgiBOA~)~0vE{POY`u|8Wk5P^eJ$B4SP@eR z=Rw3zHOE(@i4GdF8--wkwL9$r!7quJy^R|yL0;I*0%0f1!ag>(u$}MlHRt$2q}(P0 zL~h6;Zyj6Y(+h(78fVB@&`Ky?uEudwHbv!aOr{*DTkCltE}_9@k=9wFt~}PzvxUvf zQ>y&v*rK-1+SbBS1C)S_2)VW^6ZYAuWDEp-V-$^;Q-OR$Z!Sk5P&X?`P`21POPgsR z?bWA)hTSr@wEjT5pUD}vwaBMDIa{jZmq}rEKhCqPM1az*nBOhw^j$<{N>SlUIS54h}cj2iBZJ9CiDYnJiL4GNn{S*F~y4~;Ez zh}G64iGb1^A|PS0jM-_Amomnqa3UUZc~M)*15)Xme8+6@kQHw83?Q;+wI3W?J0!N7+cgft96>k zj{5^vdm50|v)X?dTiQsgMNNuMU(<6PlGlamA<67@*n+uyxNHvFQaU1PbTfH#R;t;( z6`~%9RtRSER2h!kwKtCqXJEC)RLdJkb0H~h52jniA|1eM-g=v4oLmZxW9>w#iWdE$ zb|^$tH)$ZIXSMG?Do_myWYH>?ybK;IinX{~(SX6=zSC+=Ya*0j>)}{E?F@7}Og38Z z=-d&IOy!eop`xee$${!Ji0WDGd&d^F&1%V#E9z^2dn-*K1p}0XDiYBnlD0YDpliXSMGcTiR%=#T1>sM(IEjNVwx-jAAVujMbuShKMIR zVLhnL)u1DK7f)w-$yRjx?966Nh9h_FyT^tzuv$}k?27=Zn^7R9XSFvTRc(UOoi%4! zN`nj9L_CnNC>pR|t2M4+aYI5M51~TR2Nt8i4|rh*oTbk)k>l#6^&HZ>UVw;;xF_!<3@DyKVr*D>uVH zWY21^A6w)Wt8G-u<`PIRL4D>9RKx8c4YX1fGI%V=l&nw~GR1EU-;3UPr*pzSu1KW4>0h=|OWiu-YJy*0b8{ z#+EkPYPVil)0-i9N;H)-J3zjon-yYiOIRR?lAcQlsKHbx^bv0?h?c>n1PBXe2mu+6 z+_l$^4QF7rLM0e5gNrU2(Yzz%OXbWhi^z+aqy=s2C9wm?%)yog)OU@uhI34t{(iHw z6W8p%X5T)O-p_rvh{D#_I#QktwPH1#apv@1Yb$HCnePrqesi=7OgMGX{IgY6~jFz4lm(IRkD(U*QJSoKufip2l9%MmN(@M`@Mb$^%#Ur zuPjqGldH#;Ib;M-ag54sdVt8@2=Ml?MQ#}ZRGbtQHr+r}Zv?n%Y*E`r02TY=z?@t_ zT5klna%^d%M}Qq`O%*$6^7{Xknv`bs?3E{1-n9J0@|jD2Ug|79z9=sIaiKZ?`}z9Z z@8+trkIhzQemzr~esr3j`sGx9^5>K6#7`%(+DEh*&BNgF{eSjdL{5QQh19%o)>MpO zyN_D?E{drJpkizr2S8Lc02S|)aR5Ym093qy_qq$hssZeA7lc#;*yAn;_5i4unCx{I z1XKgq<1X;42C&CnK=c5pV7k{`;8P7?kGsIz1E6AfUE5;-o*n=-Q#BPY^4(vj`z~^; z2B6{)XB+@7)c{n?F2(`i>;X_Q<5V+K90!0yH2@X!nQ;KvRRh>#Bebaou*XJdRSjT| zjnL8qpkfKKx2MAUF2Yp<*yAoZMKypu?t+th08}i=_PPt+pc=p)cfspb1K8s(cwG;G zios>CyWq8|0qk)XoTM7S9(RGc2S5eWz3u{&Y5;rM1@ij;RhpV+_3f)Cu6%BVSpLE? zvUKl~W%1t^g+nub(%}-92~O?5}3epZW95WizjtzH8b!^65Oa4i`CJLAIrDm_5fnmcZ)%g1v4Cc)h;$emOMmaTw#mveHZ*FY8pb|>9<*1vtHo~S z2K}ibx?yagTf-+xn^_>~WS~M7K@DR|+PN>9-DH3~*&|d$HjFKAt4Cx88dMR~Ft!|p zN2Gx$*&|fMD~v5_v_~jICQ_Rz84FOailBzEvGhHny!l2T}sZ(KCQN*+x~wF^ny5tBsxxYP5|i!y!_eF&PVJwu*p-v9Ua5 zqfsD3wow%U3cH_x>^~5qv>A~Ff<~!09T{8TFdGd6d9sbFI0_hB-c}n80Xedbs@U%y zTaLm;gFuvQqbfF*#}+l(MwP)2sm*|l1vGn4H?9Zv)3;GD-S-a~tYRa3_l?N|3VQ%x zhyfTCTe{B@nDV9%VClVK#t$wS8_Ui&3|89o$^t=mtJudHTi`J7^#FOY_o~>m8C%{~ z?{xz?viGXknHXEnR_~S9{}YoR)ht{Ce!TE`K?5&n;Q6e9OW5uqbJ@^2i|^dfs)=IT zeQY?hS2Q76DJRoeE>+AWD>;x8dGyoQYn{|f4F$ zS8_%Z-PUNW~2u!%fKiKnNN}voR41LShrK28qb#bsxR$hStODRS=&*A(6%;t zg2^~ZtOhEIyDg2J$7=`|K((u|A>I;$O)p}zq@p}W)M;<4YML@w?t`95_=<9@=i_n-260KWaE(?Mn`xEl0e zIVD`a8dSwdI?kJ}=b*{nFa;U+SAS{frFGBtJ59|yYG}&2ignPi`b$HGcVGg~LD`C7 zgC;<-7gvMIFN6cS8Z@qN_)A0>b5W6CH0RB%w^1HhXKfp(HOF=$^?)0eJQZKD<AD-TZ|+ie1Cw=ipszdx9GHT~hvxr-J3P=>8rD0BDvifnRd*bX zCp?Z$09bA#`vR>@eeSW(|2!!K7 zM)1~9rvrh5USGQ8usPXG%zH1Kz802K5&{HcOIdwMLOsGccb9zASF!ZfU)kBT#a;VXKz z3=}JeXXfi$XH>hU@%AS8yRI0NM2T9)|W?(8p}A za9Iv5m8+EvvXr3eF-aefw=EbA?(p$+ia8gyUMx5x7XN{-VVPWuujmCm4ad-yu* zjIL_lyIwHa>b7_QW^g@|i#8+GP$8S=LM9^As9G!SkXU2#wtT6=Tgwy^3a-0?UJtkc zi|Le_KC5_iZ@Zvb-m=S*VzbGHTpA5Rje5O7Z|XH2ywJN`_b}P%)y`7t$!S@hVMlREL*e+P6KRtVu3%Bkvd;NPR zuVw?h->PwC*68%O(;{k!mcv-Co`|gb3BrTrz%663n7`#rMJz@Ay3~r-4|u=EL%W?+ zNGc{8@sgb@784sqy4B7Zu}D?Y7knPU8>bDP?bYw>5BDK2Ua$1o#WZ3o&M zk?}?fG-CDpg_KV)`AqDXrD5f!%Uc8|cMt7t5iD!fq<^d4B9OPmhP`C4owk0oniD&@ z_n*~m?;)Odw3=gsVOs>lUt)Vym2TL6=GK?k*8Pbsqu~pC&Wb(fzvpF-$Ywe6WiL;< zb{W|Aw+A__gAed!j~NkzT$pp{>fMmSF}FJtDs-~z>k>*JUK4DN7(9@>AjMG+;|U~4 zn~Mu{kOME9S)>h#0w@C(<@lJ0v|3T4IY2rC=8)OZND=w1!^(9l*5zL=U%qTwdT{ByCEen87tdTgdf{se!otDxpPSFlFU@^?j-J!b z{>yA&=8rS)nz2p)YWi){=BbCK&YwDQ^8U%QCSNu2^@-}lA==MtbJ}IV_*wXnU64R` zsBf(#Qd}ZM3*kbYuh5Q0y(E-PURz#l)H@`S5VM7tppW8yUrS%-!2R3UwbSoTy+7}G z?b^F9FWpvPAG`74Fa8Hiwe%8t+~ubzD7b~vKGkX5qT6`L)T;c{@;kIL&7gR{K*&7Cq95QA8I~! z-P%twWEUj70mh(U9hPn*r6_7H#~_?AI2!4y0o8j$bS_Ux<)YJ17-Dg(=*0VsKOme` zdHGKRH?Wc5(nF`7`k_l3$IT=!EdK1ujPJ+xKVSJqvI`Q~0ApX+)oSHhd7H~2LZ!4( z@Yu42yn)F@626K9Hdoyi46b!*es-V=-HU$n*}puzR23e`{pEd^{pY18zWhzcF5Ygez4lp7Oi&pCTxQ-V#S(~_uDIGMJx|@FUqC2yPJoo_j%0teEeXSd&{NH=0_&Ocg1&L>XvFL_V9MZ^? zoqBL*d$mq-j+$Ncq;q~3&JaY3jawWZo|l~MOkWjWxa6Ai&phqAk|}-shshW5FPS*? ztoy#u_|jqL2=-&$$l(W*U64=)7^BXly-w(CJm0oJHMbKYvQ3{MXfQidX3~`*LZB`p zYRQzCmc7sTk;<2Ez4zR=`S1sBoB8g)-Li4+q>n3;gu`C{G)?SmvYRD$u3CP0*vjRB&zFJ>TSO1jO(IwGMPr$ za?)nxjaF~j3fYNpG?|EoEVS5Xe9#Sl`pc2UuXxW(KJxv){qcX_a`u;g?f>HO!W{>F zIr7)dZ%toEc0nQ*VC?oI^)jU+O68czL_r4H)wE`EXvalk`d~X^WIPhMRXRnpB->|v zpUID@kKL6u3#b23$?=f>>SB)dB^OI~5;cG7^|0K!p&yE^i4t z7;<10Ke+#bs|t@d|2q4?>gQkaNB6nMFCyQ(`{$9jTdQxl;HCCvjO=1P#wxO-`l`73 z_wWDPM)jC0%AJh``ZnLbFF*ACjknfkPw#xS7)hV*dkfh`dyG}2L-iS7de>pjd*Acq zHUIS+Y_4_h`@i?&$AA6A8yAjv@~DTThwpns0*r2k9%B{BPkqMLcbs_iYlAxb2|xLg zbkQHa{Mxhk?K?*EgZ5%VPrT|&Uvqqe>>@qJDl(b+j4%Ikb!PRik3RXA?|ksrpROO4 z=jJ|g3%Bu@=iiG{Pu!I|^(AB%?lD$Tl(oy8qO#`Tx@V z!dp+!o^jQA2Yuj6pCr3bkFkmrr9R^)AOGTmUwiZ0-lzY-pN=Ok{^TkCU%dW;sY9$E zFuv}jmmKuk8~;Rh^*zQaGLrg?54+@_CF2wCy7g6OmTtWF#$UjfyMB0#t?u}twRPcf z`>rs5hU{+i7^_G<>N7t7eC+T6Q;|ADh#^h4XnUiMetThG1eoew^6>4m!2ze|_6>D;^HDB0EZ z7^}!E>NCFOn%c_}7oUi{?B#b({VQ_apQpNyUHJNgZ+>UiymaiLKfenMn~bk31JSvHqWzyz1oJ*C+4%?Mun-dXKS+0;he(Gp+m6 zzWPh-_kZL1wUhpO*fk5Q|60D{e}m=Ems&UcCVu}-WcT>KD#kBk+GqUH*WG>pUE<9R z;f8k~x^~m2Pk;S8PS<|@5%Yy??UpMx@MCDQdt8sPie#TY<5#}nw(qT+@u7)RGM~8b z!Yfa|X>+0Yz*~NJ_$%6%MprI5{DJS1-PiOOtH|!@Gd}#go-Z8xqi;`BmS0}>#no^A z^7yI7*FXM?xBT>K-M=4p^+SsvCcCfhF;c;TUcwPBxw z-tg7d#u?xI*ust9Sn;1rc8~2bR*{&~XZ+tE`nvh9;5{E(E&nOYJhXoN-LVEo|773C z(uGf6`CSN~B)iA-7^}#}=`;QkdF03b>4akDwy%Bfii>Z!z&m5y(0}X7Yrb~Yn~wO; z#~=OXnPhjZ$5=(yO`q{;)VDtT%YC**jc^!m987U|Ii&5`Hz3Fp?Sq8zDWP(k;};L(LKg0 z@@e{v$<%jF{Qj@6{?fs>iLL8jA1k}=ojdf?rnAmJ^BnD+b7%i7LUxbpF;h~ zee~~Met7E>KZ}_^jDG&Eqa0-Sh#q4V87zIqH`48Y$z9=(%{DHB-hTb3FI{~18Tb5X z<kD4E87bgBR@x6&VCa##M&VOzGy7|hyW&WS$em?illF;Il1bbj@F$bHZFjzb4P+FZ&;jDb0l9*B z>qb?dctfMn+MnkA!XF2w0fm~kRtoQ{*{q;2ZYlKIr!Xogwk9g}DNqFkx`PLzpxAnl z)8_%5FnPLmf7_VKsteR8Vr4=)IfB@*RUsyM&vF#;R^7%8^?KhZ;_Z6Y>%F6hm3sD!B3A0z zJ&ITv6zm#Btkkn}1Tnl_b$Y#H6tPlu_EE%2ZCFPUZ}n5zkSwE!m8!!>5i3pflu^V= zt2=oFF-R!bo+I;yQN&8sy?zw2(&}C}idd=V*N!4q>iMKm#7aGzM-eOaY#K$Zv}fZe zVr8dbBZ&3NPC-WzD?7z7idfkx$S7iEr@*6#m3kia0@5p0rys>xITUOtUVdBu-+%cX zr=ZxX{658Nwo@3kYP?Tztb$^zn)?*TC@8k-woh@Cf?}&e`xLKKP;52rKE=xw6kAQS zPjQ5TVy8Y1Q&8;G$06G(w!Mt{QeLK@*r|_~Dkyg9R&(t0m{Cyd)ajIhVy8|gj-0e^nZWN5qW$;nEZ`j~)&5OEu|r|nClnMr zrTj@jv7;&4KPo78G)4PA+bOoaHF^ru{!T%$vn`J)D0Y~K_SXuEovnRTL9xR;w7*nP z?C5;$&lMCqHTF{l#ZHYqqM+ERu^(^m{G9~%|I_4#_LQw^lBWz@@7t{nm%n9t-)?P+ z{OubLo`fb&LAE-8{I%xl{`X39%~k#Hx3;%Oe?|ZMt*z{Re|i7=txk6 zHMF%?d(`K=Y!cF*0x6qxcK_?G9VnB(ssFXIO=tGMRyL`#F#UwatNHHy{N?k^ym5YE z?niUC&Ye3)&K)=V#O%G(_fEfC^WEv@v{S2{K6L8Qsn1Sbraf^grM*LYf%epi{KV@f zR<#dLAyYGx|33LaaQg3`JaXc96JMTqJJ?NlAJ|E7tsb`WD5xuV*-CoFurjm!gXIq| z?^_NoAHDSZrLQc#W2vz8hNXiRA6~qD@w~L9by;WZbGqE@z0bN(o5C2Gxiqo|dzHQiCv$}ZPzj-pofx#rwa)XGlRyk!*ij`8Tt zqdG@v*ZW2hD?8_`QO;=E4g^H0jPzZjlDRMfiBV~-mC?MFJzpNhTiNlYQLQv?Z>3(( zYmT2hb$au`gFB36!q$x*~guRMJO@s4thr;Q@su4lde)KSDr)zPDfm8whZ zf0Ty}$_8paygqsAem1mWCFiLToDt=)qxsM%YUQw_`QRvOrSdc%7)7nrp5|XhQ7er} zbMq)_r8Q~ZKSF;>*3%<6Z(F7IX81RaqTZpA_l=_7p^^8FqE^Dzyk`{k4vTvC2#qM! zH#vecw8Nrq97VmuqHY*Py+eQ3kD^wNdzyERqTZqH>qb#4olJA>DC!-3bImAfrMflm z9MLz*j+_|5dB;|&<{hJ`cl6}jM^W$S$ty=u@94?5jiTPslb4O6-qDknj-uYtlb4L5 z-qDj6kD}hulm9%5TG^GFi}p_qnU!;S&4qIPzsad9G>dPWI~@Fa;q!t9UeLe`8h9=> zaQ;;H6>?VR#RqHmwkY#>8O%m#SXQTs7_?qir-}%$9=(cCwBe-*R7Bqm%j(=&>sE&K z9Ash-0RjpLfnc^8s4uuRyBgg+2~lps0VWq3!2(K_Ff_T4D2N{1?!~j_VsV{d%22xk zVQxpz8g}9w>`RoLQW`6^3qCA!z}eLtL$z}erV}&y*3)P+6Lsb54!V`9RRcbHeLXH& zX~>-BV+M|G=2*o;zFY3IQ~?D!d)r}$V~^S zf#=n`FYlG`XlnPiSoxTKsQ*9dhiV3jAyp8bt{7NF}mqZNyl#gfAjwKZ(bTE-sq1SxBz>WP7> zkATVk>Vx)6*~(>0pDGEON(LCbBDdI`GuGLV+vfM{sfw-PcITmN3$cN^Z5}S?AS({S z+FZ9+e6Tmii(DP$tW2R6t5pmnzb=6K3LBQH77XPhD*ty-n{->dd+eV1-_SQJ z5cmv5MlSA|&!mdUR572D!Efc=FA325QpVWFqX5~n z^i)P}UGG1G62qXaGV`ZK8#%xK*&J;^=^P+o=x75YcCHZN`GDDh)+v+^uQy6qtX?rw zDags^+o1;JEix%SEQ0zxfXRW3HlRuh;e#oEz}j>%REZ=V0W@H7aFxh9m(DgfMBY|2 z2SXN*IUMigN|q?zFjZ0oc7v*v1C~g!+NPl@(J|yLOpZ^Of)HBUu#8pm)(ARYE}_z2 z|D&M(|LiSO=yN&ke?j&O8hAki|0gs6iu)baTN!k;dwVa{_9==~hVZaY`Np1x7`vH=nZ)>m9XNe-tc&${WoZ$c+^_jV9!cmf^4pdG<@Hb6Ip=z0N@vH^(jOHRZg8nPx&#S#oh(bxuU zBLiuo;nO>$ge%N9$PKR5r0pBA4HuRd%679q6gOtQE_}S=esa^n+Zq)2J5qK$xwxMS zbKU(CwlN(K*||12{VCgcdVYGU{FTBvh8Oqy2RX-q7Wdm>yIW_b&u(!)Bc{jpTgcLy zw`Z^{(ZC(ixK9$RY>tms((5LV-AJb6O@BSoUXK^eL0UNQ?Wb&k4C&(?#4O~B8NZ&z zaX$m$7D!0fb9ucXTPoRIb$22Yj!0~@ruRjhkRekHN`XYql=nH;VJj5#gf=P}P{@6w z$rcK5EMgnnkFm=-SZ+G7bx{BRm9nkN_5W2QL+^gE+;&?x?x%vR5#DL*N2^(o%R{u9 z3YA1HYy&@w_5TeZl=Oh=|EB`J5@s%mHdnqH zfD1&W;+Fy?%EISEMH@n=xP+6jW~_J^CFFO^nFmtB|A4{KCBy~mi$*rm$dr5$YZiiA z35TnNxAaa!4C|2VRXgu#BSfom^nup@KVWbwHTU%Le~kM7M%ix;bVgx-p#9D$A|x5r ziwQReH)_VZ$zE>Pg+!wfOh`?Koz7>>wyX`=*swKYK%oG`fvo@A8@YhT!Sf{oA}F_s zb5!|Mt`m{sfn3>v`K_LCs@-6CBV7+mY(0R8wz9YHp)#OtpAVJ1U0MG)oGCDf86q&%dZA;`S9X<7mW+|ESx(3^ZCTw*XNko z`)Bhrf19~v=BVipPTQvLm~u}(I9Zz1PFysxroBpgoaR%23HY$xm(NUq8xpe<*B+I! zSX+^Jp+U#&!D7(qY9$nN-yIj~sHqB9>>dFmnSrEh#+Ibm;8Ig7F5f*Ih(mz5caAM? z>u#Ky@^R-Zb`Jx>UVVCY;vHiP>n{QJGY`j|p8(a@RMeo$cV9ZtHlu=h}DvH@`L5;1!lH&%jtrMn7b$^LQ4*s_NBhl(0`YOu4u_bQt4;4l8 z^4;Zu9{+i4N!$EGMIAlhA4@=(>>n46Eo`)ZDB}#*R8-%~bQfhbK*<-5jb`8uqrkziq{QMPfHOaUbN;ghCs5XA2G~1es>X36&FBPc+}6;@N^bpQmflx=+YCoVkXM z4WJ#m)(lpJt_Glyy?Ni*Xm)zDX-!4x$;#&0vPjVFZyH-K8qNHic6$2sxfwNUqJisfoigq|iJsAxF8S17j_CcoWq(Uv<&^%M6S^iD z>B-L^omR7!8X(l$Ue8_(dKQnMo4vAeQ6&M6t;@2c|8 z|Iv?)^Vlw?uz<-m6|1=c&aDo$JJLxR<8OW@|NQN7VstaJ%>O zC#NWvvgaJzg%!pJhM)15oC7f1hM!#_^@g7Z*JVf#es+IqQ?8i6@B_(6w+%lkmZ}4! zLx!JS@%DzF|CpEY{_sa*<6Z0OWxU&l9~BGU0p4fc$h)uodc)86(lXBPzhrEj$96Xq z7BIP{VzE5Hxo!B_RiizZ`R+Spr1$^p*hr7;o+u;THvFhqM-Pw=hM!$A_J*JPN;1a# zemyqES9EpC;YY9fYhyVgBk#=Fh$RV4H4E4Q#36paYZy|`@8A1X8TI>KwLiSdRT%L5V-$F&*HkQj2Y840{jPX> ze*aFBjQ2WjY`kmTH5uL_q*ck`Tf<0%Q&z8{@6H=?Y>IJxy|oYEWigi zxB309NPB*Ng-=F$<@g#svU{|QberF+xQSwbbl~^9V(j_-W&b8)y!=h$YExeSzfzOZ ztey?3{l97XiRCkw{=C##e0)({_~Sxz{`d3sx!=uIXCIrb%=~($H2vr_KlRJ0{N&Fk z*@>S{WVMfIGn$9NYyQn#p&4$)g%yiYv-gciNdK>`N9sm_g_qq!N)d2Rm3o1PTDrWm@dkmo51EAvkM$MtX?sMmT7nM{4 zP%!}+2S8CZ02R~8aRBgr8yNpUYp=VYpc=p)cR^k?fIaR4t_MKHa%Qi)fK?4(kGmkJ z8o(ZRLAD1#1=GFm0!B4}J??@`4}gjl;MyJoNcRA!IQ>&|q_q2~*1n5UssZe2y34+c z-l!UYiY3N48c6m4s8|o~br+nW8o(ZR!Re|2>~R;I)&rnoiM!WbaH?tmd)x)IY5;rO z1&JO26-@WK3*xE)>~R-RJpd|p1@<=aZ(;69O-OU!!~(tW8tvp_bn&jm^QOKvb^TO# z^=GT!T)l4Lu~mNcq}92V@6G>l<))STie=@vm4lZ5cj>l;&n^G!@)gUe<&CAkE`395 zoX~5ZTzcoyo0nMaJrh?>q$iJ?czp5bCI1q-bi~A0rXt|FkKaswcJkuMQ|IoUyJq3y zrOCx#PW@tzo5SX&X1_gq<7|2M_1e7l+S$dK@6X&k)10x*ykz>}>077Io+hRb*W9=N zErxCXfhMyPYj`k4v|+v3f}^&4)h^)m6k7_{8iFoV&_&!nH(g4wP$SAAdd(HRCk#Ak zu2>@-Pc&q2!mQ+t>W$@e$mP%{LNJxhJFDKX83`o9>AL1Bji&buR;E#g5j$3{;HD{O zAV!dRhqE5E&^alZEsAb?#MvMW-jpMud9?S0nJ#P+F3jiAJJaO2-J+-N)}<{v zZ%c~!(`MM3_hFGn(wGUEnt`~*=yjKUkOy(;jJ)<&{U=%=rK6pw!)8fePMH1eimsMM z8v@HgK|`fvMuU!Axs}dTOd;)Yy(f$=A>Z-U+~v4I4AuyUau|xPa?+s7!2ch6ZyqN} zR+bBAX6;?o-7u_zDAQBS08LKizKk%_u}5S?WJX3tWMl*kN#q_Gxi68+phMN|y45n|n#0(Z%5vW?bY+5H8( ztA+@pk%3@WF&f2*?8nf;D3}^fyQOKmSx&3jaT8BYdL=9lJ8yH(P^= zG0_u;Mu`wZzCA6&sDccoxN<_Hhht%!nsDF+T)!(6J>Ow_RGYOyCk$&m)(LinUThdI zXQW9XMVG_zaep$b6AeL8j9?7&{I_c!-gqi!j6*qws58DWMm8gzW=D&#Q#=3};pxcR zY!2isVvO9cusz~~0H$Z6YPmZ}4zqDkr(_JtSX~KbZzY&98%k-SG;&{g>PpbF z8$K$BW1k`PH47>n46x~Dw^D(mZdzx=a4<;u&{!_%olf|eFqNgg5s3vNqjuHxW*h40 zZ|^qL-&s&`NJ4nNGo;7G7Uxxjke6<>5~7-kGfgF50f@=Q2%N5_*j;BgI6|o2^R@#YidDWw>FL5w&V5L!{f}XcCjEBNdKRdd0+mtCWE5 zRF~>!Y>IGZ8kgERuHKC@@ouIAifdDHJyoR=#Yihw6|vBuu9li&c;tSXO%cZZS*g;1 zpm8`+sW;<#Jrt`bi9VjW46>M za3zwGjZiDvzzRY%C||K0;$x*XmIFRe0gMk@fiaO8N80hB5)8C5fr+=($}^?D0`d8Y z+_kSoGm^sn-ChpvSBSDI78?bz9jIi&A*p0E@{K^WsEE^Paac^d>_mvyV{o4w711iH zHv$9J?}O9cD#edWSh+BT(1?QyWPuc*8i>`$aesk}(PEcQ#PTCGS!jr>HiZPm!>v%KFo;F{ za(ODMi5kRAjRr*cIUa?B?Ltp26y$Uk@))*<7_0<31Ezy>-7Zuecf~LnO-Ye76zdt) z$jDz3*<7*HF4lF=8*C3|>`jOhis@84{gM#j`~|3i`&tD`Yvs5mRKclEbQIHM%;)J` z^N`v=(I}9e@bcK;1x3|0n();^P>D}NYEjLyft(_Z(zNXULED2sz^>Gozl$NUDNLqA zMvN8MMoq|#nVRe~;0!fjiuqI!X5Bls2bLb{0|NFULlx_e+qv(yJyngnqPr6j=Fak1)ENIb74i>Myq8H8&@Q_7g&r{j?Oe_Znj zO~Vt2uC_YUVKmmyD||Q4#5p-B24%%NK%=VOK*gTYrrcl~nCq*@?jHV&{KM+0u*gyH zI6C$9xKYqN zV&#<1)xG&)pjj-WxPlMv#RQ-G^|nW9G#&fO65>S(GF~1lWP%ArAu3cP!;^{@^)=)| zZjuX;bYbeh5X zeLOA?TbK`nDY6##wu)thgSA+3g|IxLBRMdV5?rQTRp?yU*UQLSE0pSS!Q`ObuZIzd z!|D<#!AWQZvphnb%D5OD@?nx1gbX<`nvAB6aBw1GEpi+qb+%lcR3&wgZ0jqi?J>z2 zqX9O?%J7KNOK_MZt7SA@l=b?kuW_0#r%K5YO95?&m3yvvcy+&?jWPY8Hp%!)V_zXz z&?>!Jt5EV2pitV+Z)y9#6BsII7idfALiNi<2Bru)a8Uj+gE6c&keyMYtV9$CE;b z?Y2wJKqRPCsZJ9P1iH-(u0)Gm9EtMaPB09)y|#yxs^rCNqY`W}iHf&_1js~~t>K|& z78xdlD53d_p-Lt`U`FoOUGoUiSuUE?1xn2m`ZzV^6b#3aCRQIZ-3bSl-xLxJs94hH z-mpD5HH4D|wW70RtTL!Vpr{+gT%tC^ZmjF(%!Ts~VY4KuBQ==xpT14HXZX&U#^m2?w{bcI5t%jPPSFbdZ4 z@u0sH?cy!Bx-H8|NFHUy*1^bJmd#Y`Z9cvK`5AqX|$Mud@9&<%+ zsm{?%Q_i7%9~4Y=vbkU&8Xfd|Mr6_sTG58f=_FqnrsJwKj;MMqAO?^sH}FxT(ohP7 zXo(Vs(L%}}=XCe8Y>xud(Is_4P=?>zKyld1<>UE2SZr(QtdZ5HB`rm@^S;tVcfZyS zimUb@k`WYd5{&?n&|!X*Zc4=rUZ8pg)5~Wmag;7M8AqmDqfBAmTO%R&?bby)rnzKYB+tLtUz`|bo_{ib z|M$Ay?z;Fh7x@eSc;P26u;>5o{EwX{cm8_khj)wC6B z;C#TBZ|-e;a$|qvx$D2N{?helt^L~CXziJ+A6)INK7Hk3pb79~&$~S}&*R z0Q%h2^bD1*OkCu9m8G$4?4d4&uX@^D`q@&jHkNgXWMsN%f> zh<4Y4{mBJju>w`BRhej@Gm?@)cr@lAR@0LiwJrt)p%5&?q2fSnw+NLIX17_e|7!tQ zJei8f`D&mKO{KgS+(kp_q7d?XJ6s0b2$m|ml9Wes2kL^ywAm#K_Vo+EhBF33hH}kT zEzwK#ylOhm!8oB$v%M-Fp`&z^&%-es3iu6CnB8i@{=@>XiO^IofwwgANP?ysPz$S8 z+oeIH#mB*mgk*5a(0&b`WaAlD7G}@1V1Il8*yc#8$*R)f>wUeg;Ddg%(~8lkHX8IN z*`Pcs*Ts%cfRc>i6K3CT!T#6+upJ{2O?xM5ps5EU{S=16kSx~ZLcW$znSnelm0^D@ zG19{lE6l#lg8k9?>I}Qx926;xgH0hEZ;5`04dH`AHx%~KX{Dk`(^i8>vLP`SX%LVw zdxi!3KNo;yc+xN`h6Y|~jQq_)w39%~buCn?WMrm|gOmLzI1=bhddVv2gm1NAU$+2k zmP9&(ez`W1rDCTX;|EPq4#t%>Ij-edeH>_w_>@1~2yzI3eYyp^fb7UJDkH+|X%_4P z8Y9c7i3qcAv0xX_5?Mw#M3_C*f?Ys0WElYvVfGXYb^-a1WfVVz*^@2U1@u0aQS=aI zPqJVa(DPWPP)(RU(Slt-x?`EDGGX=v3w8m0j%A9!gxM_?>;hsO%hYNKv&UPo3&?IP zQ&=U;9%sQWpr5f!F_bX7Xu&QZn6XR&lQ6qr!7d<(u}mqFFgtI-E}(s}%x+3ywqwCA zAaJp}#2!guwr#;Kpk}emeokR_&VpS)w_=$sg2HUef?Ys>Vwrt+!fexmT|jVR8SMyR zwqe08ARMu~ga(8#Ten~rP=HvbphlRjS+EPJJSHYW*!T60fmNTwlfJcw*|X^)WR}*goK&Pf?YsbVHuSK;p+Vs>;e)AyGy7Y2v@($ zf?YuCV3~pt;p%r@KmN!q)$P?EJr* z|B~V{#K(Hzu^xC7Jpj)C+ckxs&GY}3*dIB6)8%pD=TlD?h~7x}d5J025^`Q*mzZV0 z#0Ib%pI)%UQgS2V=aXfIW>`mspXb(}nGXNOo}TN?i&Tg2|*1 zFAnHXuK{+kFuvrNIdJxP3WHJx4dUZam@Kl%%%frU~vB5ZPqCt;QYTOvg@Xe!^nTiak$JxaWBXX$3Y2)<+I1WORF3m%tEZEdp+5lUm@vnb z2R>#<1pD1iy+$%hX&OWqJO9sYIv94~{J%@)urts9TOyfm+OP}XXxM$jm$PNpL&q>W zTHXCk8c6fc|2z8gdo1Sjff4uE`G4Qg^Z%l8>-fVPo+#u`g^QE!6!$eEY?AK=0%#;2 z!o%IlUJc4cs501^K=GY$fi67y^Zy8~CFoEml*#6D{^6ujl@i6|pc=*4Ru|DajkFdl za(YrJG^4c&)`0^JwUZf*p-`!RIp(`uNT-Z~KM{!XF)oZ}yAiR#qsU^Xte8zV_5XJ` z*RxkX<+`v2l>e*epSttGo#~Ey`+@D^_7l&&@0@Y&j;&8_efQQ~n_t{~-6nhS=Pwp6 zKHk{Z@D0ebM!9SKGD2t>};9k>D|x$9mwg9$2mi*y+l>=~HAtqRtZ&ZXJokcFCyr zn@|F5SQcRw}TcXCo z7p= z&}a%7v{@Y&feiAXaiUe-Gr~B!Yt-uV`EZ>x;Evxa1>(1M$K9k%mj+cMl?Ji1FfWT4XIn}I5VPf2Hc6p&WBUafIGTR*6usW$nsegtu*Axgb-3C=ZPKcOU{5h zF~6#F;a+??TsAI$ZGh0T|%QN|KW8OqjA#nN&KGR$`p<9k@q%?qc^TZ1xP|<0%Cu+PTE6 zcm~|@ga>drs@t4WctIzLG)HwNBi-4{!WnQUGITy%<_x$K8QNfZSTD#4G!1rx!wQTi zoYC=Tz@3=NisYcHK@)_;pf*eukS61VOP>LEB17lGMe62Sd19c zl2|$C`H<(87azR%vWxIV&xQ9~H~wi7=|NneFaL?Mkx2}0^{J-wiN{e8|E92aC7=qoS?0{kCzz1a29v;CbT1IiJQD6%q>1^(}T`^fd- zv;Cb<3@nwS5-5wKDbxQuzt8o-v;CcOJek7bTw114()8y)?)t#l{?6G{M3M}jO{OW_ z^nZTF^}e(HoztKI3sM&6Qjit@)4t|<@7ey&={F@3w3tp~hzR(9^=q$pJ@kUL7v8$& zKIQs5o6qthmO)Z{5(4fIXzzQ%ne3tC_aQ!?01nwrCRiLcqks6>G1oty?eDyu0F{7z zmQr#gYx>{uBG*5h?eEM!f#<}Oz@}0H@PDuNT-R66_IGAqR$>$ZR}!?@d+*gBbbak? ze`of=EJ2D2Owp+6zx^AozdzgG+5K5b6cjF#g3RoD@AlaBcW3)MvkzyJ1dquIYbxTt z=g)r9^_8>zo!JM;u$U$!oPsU?dt85ew!bs`pj=K8d4*4yqyIf0dyVUF&h~fCJ_x8& zK~ou?Q!M`~%IjlT49mBAH5=&3_>CKOTQ3yW=j}=aX;=O(ZfjVrJia-b}du z`fPt^_96*b=F(6K&YAu{`k0OoAt^ z-tVZczc}09nY}$E5t6g~-{Sh*+5XP#g%d0*P*_e%oBmJupzF`i_IGBl zf~KWp1|~3T^gQ8`>(9>2UT63~1V`j@8HF^P|3K+$x17lyaL&CXCL%P%vWq`TwTtPtNvtX0OC&AR)^#G;R9-;g7mLd$#|H>=jc$&>|-# z%)$H6gYR{H=4^jw_9{s_1;J=SH0SPz9?ZM`_-ub?_GS};0wq!yZg&4eKjU)!(b@ja z?4?p^2}?sFDw+Ooe2eQ3&-Qm-{%nfQFcg}}m}}=l-;25a;B0?q_VO4dU`a^GS^fX2 zuepBzY=39=f>{$1Br?OAqvs(b>-xPjv)4&alL-_<1vEzi_XqBoKJH9*zcYJj7|Vik zVamF1d1&~Tu1}xs@628-A!cY6k#pwQeMtOYu1}rq@62A2O(xP{^hws$6YqBY?%Dp% z?B!)R$1&L)oG|(KzSs51v;CdfE3+&_aU6zQSO3mGbN$ZQ{?6=WF_}dq5|XT|cm5|_ zzkRm9GkX~(;rb>ANx;Q|1Vgz5WhbE_m^F` zSknfCw?C*<>P0!#O9Hh9k&@d?A&546lc*~3gTjR8kuj+H&RDP?UjVk31uJm7gZFzP zii=3jNXx@=E>ew=NGpeM98X5cV1!5vhM=OJw_rcE0Boxg4aM^j0Bk1Ri%jEiq*G@?6~&-3s6th9fl{j62KE0W z-A~l$6@|*T=BGM-hIuJY+&#C{C#HEZB~pjy0h$Tx^yM zOqgAWZQcOonKp5p73-(-^>Yay&c^pTdHH>!0Z33Mv!^eD%6{8}-Y z2RlGpKA~Cwop6^0`%4SJ1_lz{fjc8RlYg;~&o{rLr8mpHCVm<24@w=Dp>l)!}F zf_>`(uuB{)CCq#l?9VL#yToZt!felieaix{OC0PZ%Dt87o@Ne@vbb4hY-yf(06Y7O#<+AmUyk4G| z*f-wy0l*zw4Nb0gPpg6efmfqlR2#K+e{h$%N&4`+e81c?%Dw!c53KxQ5NF62i-iNB z5OBX?wO+d4q^n#AXN1c|Nf0v?C4iz)BONTs2~O-`(Q+cH?)B-$Wl#fe2Cr3H>R{L_ zn+*3k12I1;mjdB1`27gzG?$>Z8Lie(#~Rhs#3s>8gEKn%%6QNg{9q4^9^LazVnd+6 zUBQ7wKI^aeGpRz?*Bi49Xgp0^_9a9al`DLes{zeQx49D%8@`0>%}LMng$KJ-R>6mWY}f&PB#@vm0(C)8llSy;%b<8b3Mk z%uKLyqild=G<(xZ4#(iI4+b|c@KH&LZ_wbdC_y^)wYHUYAu|puqIS=`t_$bx(J|q_ z-tN=K#9=<0EX(COkYtSkh8yPkv`#?d#JH;a;J*Jd%S#3m>?X@C8Y*k0On@H^;W2|k z?cBdXej))5TS1kE5h_Rm6-&j`0vyaH>;6nk8kG}N1QCTan(gQzESM_9_XM^(5T_(U z@>7~f!GpNLftLvWq*e>}!knNN*-2AFL7}YKg~OIP`pPBHEB9Set7XS9H=7P}{gs8; zv&>w7b;iBS4ek8fW5*)n0}7 zYFe+9@2I`OB(Jp$4b1kS8Ar$L+#SWH!@pey=GxMgzrJ?C?Scv185SB^(Oh(_ndZJr z;IF))HnfrkBDPnrOSQqE)4ywP@9QJ(PV28yyQyleyMkeVIJ}o{s)L?3-qT88)&jnJ zMqm$s1Gqh{Zvh^sGe_{W)s_22F{cvyEYkM@ot;18I?|Aw<{EDVkdB6@pCp*v>jbh}on8d{z2R8qCF9}lsua)A$u z&|a8%^mi0e0U?pg`c&$YO;jL?q|bMmj9-okS#T;Gq%=~#VFH(<@ZPkG;(Zt#tdr9k zxlSOCTxR&XC<=Rql1zltp-7_$wuIt1JcvOHiTuROrknNu+q{c#z1Ds4;ft@hNL;*h z;h!%&bm3(eUU(q@&H;G)`R_SjIse?9&+k0E^NJl}=Na2y-G1lxecLPB-g6%~_wucO zJg1&}*48_=vs*v7rEl$R{?+CKo9)fu#+NoewDFT0wT(O0zqtO^_15~GYhPXqtPR&5 zSpB=zcdSlU<17EX^3Ii)t=zM+;(3qf}V4622%axZ@SiQf066919r=Q-PiBj!gAf}jss@PzqY!yxw+-J;&*OT zaX0_JFMPj+wo<$MfZn{jxK93pgS%~V zbM)N!$ujEPzH~^wlD+%j1@nSGc3zM9!EFb7N32d8wpR}BG#hee_BuB$9yY||d3@;h z_%+fVfV7_rY`LUB-xW+#k|g`S+Z; zC%@z1 z+pb*~&=1a;?~ui_{;Ep{-+tJZ(;5u!T~{w1+Vz-+k~cbKBya-VOmap7hfA!Bgk9 z#R+gd%u5GPIc$qF%+ef92xTjsXKxd0pjSef(qxovR*TyM+m2amfJCb<7_Hu#Wb<*m9o-@RGbykpa~@xF~8-6(E6f5W~0sr4UQAFsb){mE;e1{#5rHGb_Wt6y4u`|5Nx zv-;GPzh3#pmG4^-R=l1sdp_*>evjt4)3f3Ji2Duhx*KtCy8ajh=ZYPGLp$!h)%~~| zKWPMf&Kc~mynFWm9L(W=miywBpz{h>hvohtUn(0~u^c-92WIlK6<%zSr$YB6kF2{?1{IK#wZSU;@|&MrPI zMBm*XTMTPg0?xJhIK#+dSUruw*~K{~t;k|3o0WF=4U1vrO2C92z(-@pxoW^;eN04Apz|gT6{`pG4nQ+G$+7`ntSj zS7+bB8775bRGLS3_nTI5|8OPX%+=$;H7tg&oyOqoyR?X8Q%FXVq1}DmV)*KnfHPN* zGt@1Hzdwz^*>@aHay$oTvWeY&&0_ewD*hA&_7J9G6oL&;+J>(dyV9gwCl1yrZf z7)ZmS#qgyoerK*8XDC<<|MwJzm~*i5QUVbuHbsK#pjr%nb;a+@)#D5=u^9gHGzMn} zkm)Rk(-}Sm@9yUz|$G z>Kv?DDUGB#k;p2s{ha0cr>D7|7==kLtz@7iNyPRg%k@uAb9D|cA(c!E1cN2AvHh&& z`q|T5o#O_}0cVO*1TwZSTCSft#r4E1PX;J0An9y6or>*)s!1@V|M6+A&OGFqG*9su z&+xH*P#Fna|L8PVXM&NbEKi{jlAvPyp!yNG{^4n^&di}@1PE(0NfwIjg9=CB`Uj`E zI#Ys416dOyhjCJDA5<{{*WW+Q)tM4GIg@;9 zgK9*uwto7G-+A$mruyzas4fI%pSt3AUg)D{v3*c22m<-t(*kkMeHoFFs8mu&=3@Jx zA`rNK@)Xw-FUVM&=4nCVad4?Y)gN&Eozq;MS;F9KR!-201a6N&WglqQZ(s2{Lp$28 z+x9`F-rloZzjehqwH~#)bsrSo?cM77%@fMB8BP`j62@pWwhyZ4K&-!UTCC2rL=+x| z*&Hk=x9x*UIS|ArPQV`xBDN0--vGg{pGM%!KpBP5jF_Zy*lqiuxD60|{KTuZ;{>2q z4G?_nGy-Ry5*&ia1jF!&+x9^{8X)+!6Ys2!6M*tEK=9Gi2%OnWNeM24ik!sUwhv0k z0KrGDI9qv~091qlf)AfY;2a4YLP&`mkK=7d}&Q=~L07YDY;De_TIL8ps zB}xK;9hy$wwhs!m0Kp?C#0ZZQfVwL{@PX3^oXN~*Fp?KDxQO1i4{ECb!TV3V2suh{ z+de3j0#@(4;tchuRct?Mh4JtyVVrpUhe!#6Sd5mFv3*e81X}Rk(_EcnHkYAs7{*B` zYd-%!ef6oX^#|AY*50>v@9JN!zI64e7uy%tF8t36-t+G~pWFGs&b`}T*`94b{oL!% z-M#geEoAf4oBF1E;}sjbD=%HS#q%c5UGA*=*e$v=Uq2grdo?g5@4;JK*>MqT4MdY01u`KxyVsyps|((V#l zRp;-?y+&o1F`TxFIiD2F4p6Up&3Hi5mpGzo;dtykRj(4)IF=}BpI;z!h>b%!8JFt2c1H@+TEK#byFtL4uqZW(he!qDY1x)7XTcUb>VWu06 zJ_PWYqi>0__J#S5jz0KW4=z#XzA%qt^g)2i9DPgFzAsF5=IA>iz-^DdD`q^PilEN}Y1apEVMKG4PM5SRcwpvb7r08AbK^RoKTz zhVvoC9vOFEyRJ*r*e^_cd}O=;FqtD`iL(2JnQk;Po)7rUk+DQQ{=$4mN5*$t>%k=o z^cUuFjEuVgl{qq&DB53`>gd_J^CM%;`Cbnz0^{tL%*{r)KiIL!MexK+r?`gn@@ za@k2Zq^As@+N`#@u@n*Fet2kr-QF#4T=uKt{93SUBje=#Q@nXKY7&A*Eg=f9FyW2z zGy?d{JY7O9U}3(ac^U>h<{G$!e89pyjyw$kDl<=)kQ7*$>dZVnu>ryC0K5MG)voOy z*nIlxtN#N(kUSRJV?FR#5Byi?f&acbd#+jd`KqV5Z|*wdDEu_5+HX+!84KR1@NAWA%b1R>&I_ex6)>&9IIZetv_BwQmpzD8200)E+1pK3afn9uq#N zHqdOsqb&`L#(bw9MBeN9)M}S!8opX0rOVm)WL&T6Xs|E0f=E!vgk+k8MP7x*;4EGc z!=n)!hzFFk&zFkUIyJnSYK*%;H?L9iH_CqhC{Q!Hgpnz9n&Jpkqd=r?7^oTyQw_*{ zC9Xv=VHipEWFs4l5B(9fuM%BUHTFWtVqybk(?MzA%lotESSlYw_szZkT5uc&{!@;_ zWhM%<4A&u)aClStO&Wh+55TN=w#%)L4LehTxAt}&pMR$Cu%FX2s0U#q6=Mfbqo2^i!M)^y6}eH+bTI5* zQJ+2A9CqJVbl=>^kp+jH|3<@Z39*rzvz#rv9y*56(Pyr2(m)yl4I5ir=Ct?O&R=E0 zhy%(z=7M4Qf}P0~b=WWke#{~9b$u0}_GABwa!nf{Y<_#~qhsy1;10jL;mQgK4=svj z9j68)dGbI}K{bOvifu~-Ss$7)+VMDov^9Ya1GoGOswul-{i$n3{rcZvL05}r>#T3C zsZD^kn&8)!hl1emf5+J-;t!Y%H?05fWk*vipiagT>yNXM=_t$iGnr8|reUQ7noEHi z{64}|lekZR)Qdb=*oWas6k>xYR;h8ZVW2;34T(Tx(p3$Ast)!rRO*5Nq~Oy%HfR(h zYvyZB;h^WV5jGM;k9*|Gjl(?UQTS)!$s@S3bUy^nApV z09E@qaQ-I$cmLJ<*2?Ad=I#h7l~kEN3aCy?h>l) zi$pNCBUl1;cL{;_MIsp45iEhayMz|}A`uvN1WTaqE+HGgNCZPWf+bLQmr$5rB!Yn* z!4d-VyGy9X&;Mcd{;PdEf+bLQmk^j=B!ZqD!4jyuOK8zA5<%CFU&E0}<|J8;a!4exA zZth-$`>*PD1WW92xVd{2?!Q{MBUobR!_D2baQ{`!j$nzs5W6>ZKg0c3Yjy-npx)eV z4)H(QUsdf0me?+FbGI_wfAuAH1WRmQxVgUv-G4QI ztq-PC)?cUvJ3)TbieTVeW+|Ez0^pEGk`ANIgc78O`Xmk~vhADuCj9=ZFSa9C;@6v- z`%>`S%7ut?&rGS>wdiZ_f{TQc`Z=qtF9zh!WVz(;*SGmz>fRX?w|Gi z=|%G5GcSDg!pASX@xuNE`9k2r_W93(8iCh%UgK#36~AYkZ=T11TEO4$d~D~ZckbVj z++W$*bA5hiWBZS`e|h@{w(D-)jcmi)Pg^;E?#t&s1nLQ<=lFBaJ?HVfYwJ^64{W`1 ztFU#~)-9V~*nH3C4{!E16PuSd{$b-28*kb;*ibeC8|T(Pv;HgVuU^;J(e-Dn{oUHf z*50`GvNaiK3vRD|cJ)_QU%jfYA}fEh@?p=rPP~%1V@V)LkkJedcl2(hWx(kI(Pl;3iG0U`cDFxe z#qYWGXlazq^So0&_nsEm-f2zo+kF8MDnH?kUb}|!aK9&!&WO8~9n2*sbhkMwc8D=m zAaI3n+SL}ctJ-$7lIXYbWKClPR8AHfuoF*N&`!4OFb1x|Dx0C$q+Z7}!{&*0ywtLT zV6spVOFD@p^gi2H#etKTw(QW>pj3gl3R0<-Qf;0sHJo+{yJL^;rnN6o{IoA(r+pE% zzc{-6(1wbf_C?tK;%Ft2?GXSeKR%K6-+&LSUL$jr9#J$BNP(vYlQfgX9KGhwE@&q% zXeZcqlF(3-W07v5{obIF8jDU|JKM<=H>oLHk=A=grioHU&S`hy_La1I{tmp_VrEs# zj2PvJVXrX3niIX*B#UIi!D^=*F=9$$8|^eES+QNsq0^zm%>7$7oHnj;B5MrjjMS$2 zsxfldtzR?Cj1zrYsim_dtoO29%VGB7#mw$q%3Mp=xy}p51@gWRh$*N(dvCaB`XxP|j)h6$=m3%KSh@a3zsIsFcog9jD!^ z7PR{w+pZ>#;d(2hXLB8)mu~c&Der#mH9K6a^{eRuEH!&AqNz?DyzZY`(C!VkT_sVJ zYNdJ-;tNAk?sc8hkLv;3PSV?Wxv1B2jpU?0q%h~A=RUun-HvU?)^LQYjhZbf+o}(8 zIo8n**Fy{1J!soOFkdgpLwVR_YfNTDsZPAtFKG7@mK{3ml6ps|NQhWyWP0PYGhf_) zv!LCVZM*ImZ}xLsu~8+uoy@2-am44g^8lS|_evYDttnU!Dor}D$SSm2c23N$U$X65 zBH14I%0zjbY}PZ`PS?r%tOf0MEjvU{8tGO6>TpzXNKdM$?!>!fO>0Os3rkXwPqay_ zIBGLRCog52rD>pf(8P6{;%~tBI5Y&&@4pms-#+Y1_3Y(}t7;vr4Pj zLF+JDb@CEcx4`<8PmyC3s?gL_?lifq!%kkzEN8P;Fq}f+L9M2b%H0Y$NWp>T`YYS6 zs4JCPy4ovFG`5C8(23Oh5u27MB(P?J&nAe5f~N~g*@5?6wiya#m|CewSCXSKA|M%O zV;^s^LT$)swnnU8li_r~fbv{S5yxTA%P_RYKdl+Rt(=STGyEPMC= zJ>~PivOYiYkm~;IDW8Al-0D|dFLr%lW$pi8Yp)S&&s_cW)t_12UzJz=t6M9dS^1Tf zSFbcyu$5=HZ*hIW{erbWzxc_EZv*cGUUD&Z@o^VEcj3VcKXjqv{(bk`FOU~*J^v5q zKXLv|=MT;+=Y!|B-QVjj?|gRWojb4DY3^X)?0~Oqe{}l|+uymJ-QL^YIQNI={?FRG z&wbyy>bZN)J!R`lTaRr0_||xf+j{nvYx9$vZ}WIPYd}j--Mnk_7SBgLZ`kQJywyRgD_cDd{Qat(Hf!?CA0Uk@0a)7^|aX zifE-O4r-4*YtFYLEWCs!f=wWJy45ZfJNlsS3x9RE6)+iIStWuK#Ak5m}O!i<#kgn&lce*>K)yJ$Es)=UA-du%GHN z14$8>LNl$5n@tC+JF}o2zo1>(wyViJRuQ#?(5Xw!)R4m+ysm#;(C%NX=7J;EiE^{Z z_m~#b>7^10-hoH1Uvxct{+!`*Ee2-w;5N9x&L4w_U59Un|HYSlSoxZk*--A~(gSTUs*4O)fi zPOd%~V9u;^Ki#rR4PdcGl*NpOvJy{IDd*jX`yEyq%su+5Y@4YFRze|79dt%=QLTyv zM;hF=n3??~keY{ftKEiNhbYm7_6QdRq{fw+C))Z6n>9n$209_uJ0hazMpVr?U)*8C zAs7kOi>aKHoJbUwp-#Ln`C}Unmh^h1ha$DTo(%Z}~#2L*&ts5Gw0NwU{< zt_JS!T+r^NR`;=uX>%aT86~B`6G$o1&X;LYRys5hbaZ(Trr4p&Y&JdW~%dORW*jkML>B;5*QCd_1XqKG&aGc8me>S*nvz z%6xiUpv#pLZ_e$9DMp>rgCeQ)G&vKzMTIAMef8B~RHBT=Yo1LvaVe%XR{_uFt_AJh zY1=7!aZ1S(y`!K;eJDy!JojKhyPjSDA9kJl;fVCYSWK^C zyEOoZ7lQ&iP!U*e7C^aSP3wSvUNUQVO^a)XC|^HA`NpR-Mtr8JUEFu=1m)JS(KuR# zXDTJkslq#@#_@)MKgyE)c<_vpd|94$xI~1n#XHDIW0XuZ*dLBU-8dIvM~OJteUU9i z{iQ6F!X9ygqlsDVvJUAo{4^ySNg`e>NxsnBGv9qu*@nOu8sQZ*`jY?bVH znNT(*RAUh3zdX=Ti5p^<8$^UF56ZPda4&lKvL6Pe%2vhanP;L~O$XI_v)b%==D_>z zy!+;E#$0gVg-;H=d#~rl*XvVXXny1v9JXMUnP^8PCB8v}!=g06@X*?3A?l@&HF|nw zb$k&uK>nswq5@Uj@CADO`1oW>w#z_2HbC0nTlfmRv!jG`$vXY z5SNm{(cUz>H|`=F8%vO#_T{8ns6&*;uW1YhwOoha8!-=UgihxArDgWsnpgiDF9WlLFx!n}U`~p@Imhfb zNCxKG(v`oycERm}8Hp`Okp~0JtWJO7eMdepM_fM|WRsz?s*D4)hB6Vg;ui=gok|l5 z!(E0`Ok^UUljJnUvL+gU$s3^5y`&8$*OtCZwZWj%ziV%=(FS$!wRV4Sr}bB<-Bh*K zU7?^a}i?`b8l{sP}UBd`Y|18{p<-vTtpoin(ERM!2Md!0m$Q0j2V$jF#b z(R%?1YxNC35mtJXJjiPG0&h)F|DM5_GxWbB{;>~%Rh$wo}1UfIZ>KFP+-3;8H# z*;w2chywQ;&ayc|;^J_&RSZrAU$hUEv$4Vms$u08Gr{8HKIH2v#d`H}lp8(jSvDrB zWxNn?Cuprz9FB#{DUrY2$&IGQ=F`Is^{+~AM|37l^hrs!NKA-~d$b}!g@RAG916lwN zpa1^zFFOBhpa}5b&Z~BEJG(#^;GNsA*cP^*3Dg08>D<+G+_`TB8Ueqs^|CEy>#0B~ z;BA{P-6S`k2=oHpyfNNDHy#I61KzkkSiiEq1GEEva;>xW{IyM>An@a>jn&xdD$o&l z-AZjGwBiD40zc#_d3^4F0-6Fp=zfX&d9JT6sx0824OhyZ-Cmyz@lm>pPb+)@ZKhNh z!Lc}2XtxWoAWR|sWVRB>*Fw4)^L)ZT8<;*3A6xO~Nvhn>tC4IGE+(-=dcaE>AFfX| zz9)8*WT)%%Yb5LWb^oky`oKe_ItU??;h+)E*0pYk@cXk>qLJ@bLXCQ|l@6+jVSIv9 z8PW4`|Ey>FjJ;@(B_nku3dg3MTzVJ@(qRdzbjyQYt>a~VfvMq@+nr7f^L)%d>zY0# zA~jWvWK4+k+I22K7f?Bo(3Nzg-<=XlI7>!Ly@*!Fhn=eDBmP;(^ci*2KFtrc#IA&Q zLy#;C3tlFYPqp&lv8+%rXo}`&1IhJ9l;@-VS=;ncTKPCZ*L0}e5{mhvS1$4KATpv` zaz*vWwNX97WCP#;yhOY2`8EHnW%}U$0m2DBBa0dem+!`8td}oiEIjrNIW9=Fg8po# zs`a{YGUWNNf7Ud8_>7LLeR>2nD4?4Y>q^v`tDCl>ZLYt33Q zTIECDsv1y*Jot4v23{2BI5FJTOIn+chf!MdwLOpcXLZxZr{S$AF{vXWpBxjlQgq^v zc_&J`&=f1>Y_-~{7+r=-PInLg#9&lj#y-BHDtuP~89PadIFsalDUKE0;~ zGkQ~J3ch45Ho!dZ_s?pkPd91w*nA%f71&Xj5(pw>1cS+j&PT$5kuYtxkYb^g4Yq=K z-}65ItZMquQ5h=adX!Xx)j^4gdU28P$?YN{WQ&1b6h!9>3=)-8P49ai_RlJ&Pa}_y zDw9m7C$vJtu2#>p@hs;ZMaTx?Wr%7zSecB%d8l8}VxITjgyi{HlNU64S?DsTI6nyFS+q zk!(0p>rDshs9Kj&u&!{03`^8`40OF1ZOfi_`e%95rx6yWY(*W58KhjzDJ@NJ1!77P zDvY&Jveqp11Cw@L>{hx;$n!4$?8T-}8za%slnG7D;@}6v%P_?cn{`xQKdkw0u(szN z{@II6A6f0kq~540dW%3DC0JsqO1!LS#b90SwaPhPq($)|9cgl-kmr~Evlp5^{w_Qn zOfoUGIpLs2YdF!XzBr{uvy3Mz|jleV(`bXF1a+(3JbJX(LQc1)(JRk#=UQC8_uq4Om zHk)TXzu=!qrcWbI3*(}9I`-BJgT|m+AJSzB)G7opi0!DsG&e05s=-*ahl-y6?Vn{$ zpE8SSLbE<9Ch|etJ0vk7pU+h^DyI62J_K$NlToFh1&ly8?RmgI6HT8Gh5O1{cp})J^M*7!C$09~#Rgz0(OF6Q;7%HzKh>WYn&De$GEj zn?A+#0Bnq^Wz*mnv5`SevyD=#9cH=&*%U}{GlVtOaUa$a-hR>Z7XOShec)nE8xY?9 zpi-$9BZp6P`!LvgghIS<3Y3Ws@1W4f41rqPU3UjI=x~ z=OWb@iL`PE$MIy83`U5=U`Tm>);~*{KCMbL6wkNlyb1wvBtwIIlOW$Bz$D67FsYdB zd;3}k%`{2A=y{WW#+p8vbT2ZE!;wy%2~`w>%Ag8W%>_!SayuvpB;8Nc@m@`i<_E0j zXZ$n9^a-KWajpO-qf>TBrApyO%Fo2rWDdhhB!d_*4DQ{ucA%(WG0z+QGurfF0|Cf8 z0YBI`$CF|%lg6f9NWscpxYUUhWuhO9w(uesYUI?S=l`+y=3$O(XMJ#Q)6+9O3mBX` zKoV#eFyUG~Ra&czG1RV7Es{#5r3#EFsicx>QAxE*Ww@qq++gg4Ercau31ITEWFZNg z4aNos5{Cqn1d=>ih(D7jKt4V~coIUwm*hK=hCA2Nv|73u?3?GI?>}?9>;0YgyvsSK zPM!06|0;Z0n#=6KTy*e%!I$h@W(TgJgYSbci*uPB*ntk-1z$3AnH~6i4!#$@q~|g_ zFz_6F4}3|@Wp?1qIrwh)lAO!zz;bi&SKv!xF0%vM&B1rUm-t*}2QHg~?}RV0xy%mi zGY8)RUw+YCW(U5QgKvi~zi=+I1H;R~UxqK=G?&?dljYzq!Ixh!m)U_u<>1@k%Qwzt zcHliZ_*VGx^XD=<@Shxf3w-&8xy%miCI{aPU!rrF9r#G*GRRzJ2L_RYzX)Fz<}y2Q zejI!ge3_ri?7-4-@E73A++1b{UX6oqgfFvmnH`ug4!!}t%*2ZoA+uZ1t6xy%ln5(j@CzI-&7*?|S(;LpLA=X04IcpDDh316n>GCO=J zJ9r0t`TDuc4&S5>z6QR0elD{EpTfac&AqeM!(D|91-#`qB!(!jUg^4~8@VUl*kRLVTlKbm?yT8J z%zCd<^b*4C&*YA?dh_I(Ze*WyV~72atJFhQB6{2B&cN4mY)VaWrqtIMb{rC`CkHol z19{;hbF2605g4dI`JEkhTh_`Aul?d|7;t2n2wcIU>y%6e_S2#S0a2ZPV#gi!N3OC@ zVM{x%KDV8|Pai7tg%>X1g&m(e*#mamekWzEs`A<|&L-1|sBn5nd>PaL_!=F3%%eEID{1WBXYKDV8|S2!{cx^e!b z8$0ZUT&3R2FFhg%bk*FLUw!U>s~fqCql?tzc=)uvUA9`*s&KFU;_TXUz{<>4!bvNt*-rIJ2mG1eEx|)ci20*%AeUS$CuUTw$t~Du*|-2@p(_immPLl z)@q)w{o;Bi+{fd~pLycQ9ky)N+FSd@^$N`6DF+<+xlbIq!-mOKj?8T7$kpez)AtHT zW?>T=jr5k?Sbgq#*4GW|hV%;;pYtRfcG!Vft6;zOi|fm3&h?tx@w1=U zafkQ+tLzAGX~)&)w$t~DaDZR9c=*JQJM1oe(W*PwesTRN)wvzhz>c5w#Ev_>+h1kJ zEnijFPE>BE?-h20=I{SU`>%am{}24*kAL>|zACwd=SR=}+~NK%yHMAAV&&-B73Z5l z8JM?xrWOW+_LDP}pSjDH)BH%xE?Y;7=XVIit3NkR*mt4oqdkFW~k=1UT*cZDg8J@U)tA`8tZ3L!W)<^V}B&@?lePtLZ8)91P@TDPdpkhLEVJMSbp50IHc3+ zph8RS=7K>!^oyp12xi}$QTih-YiL?r@P|W-3pz=on&rvu4t~zN4vr*!_0c7?$Uad2 zZ-HL-T|IAr4u_uiTLX0Y& zESYgk6Qz1u_Htfd&;>0`*E=*~>poe3#d#QtVIP@MR*{c1Y$DcRJ<3c9XrX6;8~OII z)bG^MPN9qzr(;=4cNxkLD|tCYbocoTh2pJ8LsYC_G#>LxfXs@v)0}3o?ecKmb&!Xk z{@?9u|3~KG4qt|zmWT7{rtO7x&;K~hfiEdAKeziaum6XFgDF?m z|HHy5BiMD6eIApUu}~jPq#_2pZgu)dAiPRW%lN}EJ1DVO_Z6pIy+QYUp`;A$Oftbi z6?LqrsUFpPRGs-#!Wu&fLA5Gzuhy&@!?7-BwInRtnW~2gEIT8{qJ_@@cZrFOYQt=t zZFHDvb<4DycO9f1sQ-69PrG^jza4_qw3&&d?vDM z>;J92$$^afTS8$o_GE6`TyH{KfHJEZFhd>G`RKQ6aDy? zZper4KOn&`Uxhz^_7g7doV@vM-Lp@pZd|pKvbuvTkl0j7gR`k=8q%6*$TEw6gR;nI zqLXg6Oky!%VzD5LW0h%yI%7LpvoT{C@a?VY#sW#jO4_zqMz=dd3}s1*1DJ{8faeJu z?xq{F*8J}zWKP3H!Bu`yF?xL0yjDugM|H!;U%hZUhx!P2=n=9~dL#D==n5fzNC_>AE)6@pM0 zo|j5+x^NDG|KLi`vp?hc&k%dT8{UBVvr+dAZz#%gqA&F(vY{wJyb_2Ts6^yJao z-u&5fpR9h4`6Dv^Oax>vYsM$*`3{4cN^|OVG9pwqjVx$?Za_fayLcYzwQzgxC{X;t zR9hnWgbf^T(xwI`^p1muN_uX5U~rPINTjNW&LAlbRZ9bbsg_qE1NE>d$=JgX!)2SLN{y^nn*7#2dEM88 ze7N;eK75P@qbu^E`i$}cZ>ovvX$?Hp8kOLl!=f^g!*)#bFxt}b}IzaEVYw=RhmX-H4s#25sJo2r)KX zN>RloSps#sx6Oz5{KgM}e7NyaK75P@qbu@(eOCG4Hk-E6Ne^HtZB0bD>s8JAbk}?+ zjtwgvRUwj0S%rEN%+ZiZjRe~w>4^?zo^~D602jri>Ate{(tCdOM?pRuU6&85Ut(WX z`S8r%ACRCs^UVe*5>rnPNEJ}-O7vo>yz>4)WJOUj^t6VgIp30*I#F*Fi%eWZoo3uN z$ziceHfvV9B6ST{s!>I$UZd;e*7@*{4naN~zLXCiqrvEke4w6HKCpaKozy7=l_;9% z%wiL8tCPFt1EQAEc$$x=)UX<$db2(v>j#;@-P$z1bFF+P#X<4EAiq=c-`TIX~ z8@%Il`|Z8^zj*)K@0)W# zEhoK`{R8FZhi-nyO?&^p+u?P47?%nv;H-7NO^ag$7laD@f^utFl z9Pvk=dia}%KYsAu!^^|k;n~4E5B}q;a06mDl@=FSiXGE~a~&(46T0nu(x32+z5+2- zjPJC=TC>+H%~&?JM*|Bv{U=L_D$$)q!6ZZzTeT7nPljAd#`1N_BVnyk=?gU9J^i|68)}G)gF-zu ztz^Lc`z}|=meaW4&>8E0%_jB&O4 zX=Ne7;xyCbP*Dx(d;qxlbrULpY7JFMN+xv{p@bNlK&j%S$N8t(r9^gCns)hg&v!^G zCxVx0W9)>*Dq5zg()9S89sL+kB0%naMe zC@0nVE>ET#^$~RYYnBq2+%C1Z=x0fF4Oy_Cq8bcX0q-JCIQQq5t~ z0$XKDf&jlV+U~(`Ab&af~Pr>!sv`yCH zX@={FXzW@Ce-d1e+JDHVY*^{#J7DifZMO5V3f{peq+3XZSrLVkrWE?p zP?hwtQU!WwLDM!8GY~<@)Vd7=HaVDf1 zLwxKL?V7tEEG4FCtzVp^z!d~UC?~jDLW<31VJPXDW(Q7#QW$Y5sdmP>40T*vN<^bJ zo*VF1UJ!a>2cym0Xxt2?oMc8!tu_-%AyH{GFslIFS|-#k%oE|VjqUIGfO}-~n_5MG z+L*R-nIa`%5d3)+lQ< zBO0MwzlAO(hB+m+&B`b&VF-axO3*c1Vzwa;Cp8YO*%Ke^bvJl5Ucm!Afbol>HL4;8V~*DfV$TDmK> z`(!T#XdpUaf$8-0EW$97Jk?o$7ULlYrYC6B!fw5ODN&uI;b@vSiUB#9DK}aDWB;eJVN9ZC^E{3_DuWQ3vtC8;J8LQbKdJ)Qt zk#O+KONlbrA1M~oeK0)t9LwvYUQtr;5;3c`6p>4^u0<%-o+Pr$@w=B2LU~lEO~`hm zn(F#`r|%Bjuob{4H1IK6Z8WtA9}K&ZGVyQx{8EBfd`)dq))cPB*$&?s(vbz`>t?-Y zx2aY#kdfLzDq5Om9sj#!kQ8%SRH_$JYM0YPNaKSNpb2zF|E<0De%hP?>&~tOzTxZ@y$|HmL4^gSw?3RP~8A+g@Z1O zX;EuvNaG>g!cOR=M1H`+-6HO`!GzN@29pHS3I%Ucz%%_C+wQn_wOjYpNuRY&es(F5 z#;ul}%+`G6;GZugCN)qm6iv^hUX1Hrkso64Fc@1TMt5sWHrKFArrYbb z`)GWl_*jC1B#lo;0aAjU9&1cz8AX#DaMB-PHV89FOVLd*Cy~OAx$4(19Y z%`uZflbe<4yhYkr)k1Nso@pyLeq$+Nm%18D&oYsMS6s&(bU4}On8=tk!%>d4+bo`$ zbeK>axQGAGV~H#+mrV2Z999b>#*56crFU+gwpgm96pVKHEbG)K8B3|z zp2rwAF|#nbl+t|x&?3wSAEwcWjHDBn3v4EhTU4rCYK+AQLE02PX{9v?OOJbbv)9j1 zHWt=cfcbIOG$FoLcFiyX^*W5wSjx3E zC)YDao;m~vX=l{I{l^jtn;&8|j-PU5v6lr4oKCavBT`!e6+o-G8OJal%vm#?I{3}U z5_wlk2fZ+p79%puA=OZ@T(FR=5UNCG%F`n4Ns}xOd&n(rDNzBX&;4|9RBd8Ip`jKE zju5EBtc|gqPIG1rOQT9DP66^b_QrQEC3rdMn9!KYGkF^Y^?@fLmmq{un@&+lEe@Kk zl&Y6Awg$2L_)>z+r$)Hf0jCB>Switcxk4h92F!~2mVi^17pkC8a=;C;?Co#AeN^3p z_uhVZG&uT+8z(nfH-7)d_aA@jaq%W{{2M2qd-Gr1eEWfY@TX3R+yD9Y_uX#ZKDqT1 zw+4Xe@9&>X_ujtuFZMrof4cv{qrJnwdHAJ=cMpE*{@=X+rT6dN`>A`@y-&US!MoGD zueyWs_-U*UbO%vXAqfNDzd zYC(ms*oe1!1^KKqlEVhBVI{fJ=_)E%46R&}d20pq_u#7jqAO2dkGFTR2@v_{9nCbl z1>jeLzz~nV_)4d&`GmQ)IQb?Uu5klge_1}<*yYu82xt08ZOm#Vo)LpO=huwZS_PP| z+6cJ%A@|HpMyf`P%VH>!8n1cQTA`iGjex6P!p?fU@8Q(QE4p?J3Tj#Onuc%N2)Ozw z;f%e$h~P%R)xFrW7@Kw6GHv+fN^CiXfUR};MUdvtJtfsQ`98N6OV?Lg+laT8kJpkG z_c!6K9n`!2tfjFLZ_TdP){Y!*#9ND`rU^D}N{upIva}HqSTwaVqSzbp)&dDosYbPy z8K`xtVpo`Aq^#gs8}Zh1@!G5T?nb<|RJ`_PwX+d#cS?iFM!b)XXE5Ae+u!jX&cCTZ@-#@!ql# zZ!O>Adg$tr;X7Dq!jCMArX?IaA2q zH*W-78(oC!D<*CPTpM0`9$wLnpzaULeU__RgVnlld*8GXaBY0i=VejDF5akuB7$ar zq_5I=?fr#~fNK-_pqAkN(94M#Wz-jUU)iiK+{ z!*AFKxE9!SrCaR|dSHhu(&@-4k5=<;@9Q=JB5Q#imHRz4>ICDk*mozj0loT8w)f{Z z0h-F-u$1n?0D3(e#|!WLq@5Mm zf$DS{1zxjIUB{gq;<5#xA(OP+$kpXYGP}J1g*vU2u1y zzz)PY&pHk7EEL#*^JW(o-0Rt7UU=u}&I;^6b-RrMw|7=x2TtE@6u7leU{Qi z9bdk)0z0q}T)##C!aLrwvjWeQkKu)PBnt(0;JM$0Lw*|z%yw4b8M|P*vjWf91@S_G z9S*HLYZpX2EAUKRu;l;yw!J&{t*<=3x&LifzsUdbUp}T505mYUKcs=#VV=Bxs6Kr*jJK3a|qW%(dTK=RLT-GW+UI=v?aMmD#VSfmuWN zV;UI1gaf_24r~S(^_n`cbGyH?GO#EF=bMPs`_71`%sd13jH$*1M0fwd+XK>5$>wURWjLvZxm;z=r-Er-=2S?_ zEk~Qo(@3Xw8ExhjjTaH}a>~?Q0aiv2r{{Ef-aN2g&PDn$8aI}W`-923m=hr0ERbOw{hARN|iW3il=BfQu{-qwA znLaOLX+F>R&MXT~Dvu_0ZCZ3EPSMJAGI%#G*h-_TJYrul+Z)+s9+8rv_%JhxA3fh| zJ|B%{4eJP$qfP+TX}Z!V zO~|~tZQ_BhgT%WWTxRBp_ciVPXZwP;W#XmR6YnXhH2>8~a)3GoTG0DACC4Hkqn_sv z-O)Tf76sE68oA#5|F@3am*)h0eNMdcNd`etBztA7H((+e*w1&eW_eH##nQlva=2GS z8qe1{UD9I5tj@K1oM>X!D~@#m!qF&TTwW`3nU);Qrg~&XK@6_lm4h6(A7!Yl*~vrLk$uZkSFrGUX#df(XzxIj`t%j= z4!Z$Q;}3oMoK<91xrYx~t~}=6`u6S(OL+O*0v`_TNJbTK`v+6{yj$>H|3+x*bA~+$x06b zblN8|WlGK3us({PfNe?_Eg9Ssk9uz{SUD8!<#~G;lpBX;-CQHct%TU<^tdx2ltA&{yKuV#_vW zu?80$gK8tDb70B1~FE4&33)1Zq*pw`YZp;}Z4PE^pMz`9_?K z$KAXFr`SoQLQ7+arxC7OYKnz{X~Ag$50qihC|RHU`#ZO>WDKb7pw*`=t-FI(|8*%* zV(M(CnL|5CJw=Qs?NWtK4MMNNfq;UEN;Jy!Mjc!Sd|!7zVc8F_RY)0aebREMW~~;} zGHNxvxT9y245MXA*cjC(c2tr_?(uMEiNVei%gd6W+#Mg(3sF{4NsYpixR?kLF2%uN*%T$YXFB6a^i zEZP7Gu(ibUlA4_JL~wQ7ovJen*Hqr?WTc4nr*ypp$!3!4x(5Em<2{+}^(+oKtW8I=L%LM`%h$-2| z#+I$~J<{Q2eqiF$Jd1pixwFLMXy0KJJzcgD)$2^Q-R{y1(sD9=!Bo8xf?5>8RcQ?c zmsw>f?l}>lSGfO?ohAP7oh6nYtnJ{B`l5|p<|zE$oxSunv-p!|mi^el=gTY3GTR+W zsj+9;5Z+c&xqPcqLS&-N7PG81$pvH$F-n~gowVHJ$D`;D63dZ#mo}bT++LL_0jEn7 zus9hhYP)TjA*l}AfN=$_I8nvyWU-Qs`_5oOXN_G2`nwAB9t*%m-5ygS$253a!rcyr z`9TGc4pydlSf_{vji%6`BF?Jxt^(9DK(H7gCTt-$$mJBQ)zrt8L88Iaz$vJCq$I=~ zIK*v@3fKrb{@tYnoRP)?pF-UN;c+5Kqn^>AWG!Fm;1~%_LRX*$vOJ>|<>vCTvb=-D zuk75$k|IJ*d2}uZc6)Mtv@F?rQ>zc^a0l$kT4+8$NXUM->-V%QA1cS}vW*=i9*6A? z5|0PB9Tt3#r}7;nmN%knI}D)KW9vHQGMTn9zdfGj%y#11L&=`?aSEy7Xg*MzvV^oU zZCZ=(FK_?L@b(gK+_{a8R!f64n=M9Ar8-;36uu)6sl|xyk8Nf?WObeg% zU5hq$kyzdy-$mj(z~K8z25EVRMddQCc9HnQmz5BK=|WT+!X?wldrl4(1+Fn7GHPNq z+hrpdYc+%j^-}e@$5Z*NJRTxy8k$c0RJ|uVLsJ%eDXm8YwPHn^k&ex0)MBbb8h_%G zmmVZyq_0Uv!H)`ERIzB3aR_#Z#DHQ8?TTzqLNkhk(Qwp#ZFT3kS~8-@Y&@-NYAH-5 z)R3aI9?W+JfzZTzNg%~2H))Y$1-$vAefBk_Wg9$45Nb&rTO1o8WP=M^6F~4b1(Z=~ zW5VWy31;$zd@B&EcOQ=o)9r0#&H4ZS^xoSK?&x=Z^Yp`~Uve+J{V$Fl-j;9u(k&j` z@+WV;|K=Nyf8y?cI?mqs;Tx|%`u_bpdvCw%9DeuVpE~%qgV*kV!~L(g_urmO+OPay z7YE1SmcYruL&AUojm+3WpkeIf9GhZIX_9FNe9ow*T5OD@c_k@`N|9$D662CoiVAR~ zW?e&qx>CdGwzV3bF8We`T&?k?i7oO$vs`7*#ENb@Y~RNlENewgJXzfWyYW|_=m1!K zVsO(CW!;oOsEV!)kRm;T6~8TxJ*NtIk^l*f%TXHgaMH(Wsm4SYV<|78e2U0mV*)2i zWOke>0eUtx)9y9WZZVyL&T2EZsg#{A16U?Rt3t^5$~N5o&)<2X#P*$g7{yJ|k}|%J zmaP_xQ=T^*Q=L*yw;k5zr)-hXNjM^4EkU+0#Jis8;N;i_Rm~U$Kpv49=h4czP!3&G z=ZmSK))!d=P<^lkr_5I$h8?sEi$QM8$89~S8S!vv&w7C;;su5-vkc2|I-u%c+$M6? zijeLrA5RtFw zSRwEqMiA7~lx(XSijn2`QpN1|vR#%;d@*v{B@7#@TPKB7wCFY;~G3<`B73YL>^Phyew;+WAQc&pLV)(5rPH<}2mG2&xoh zN~`2NS?BA6scT>pI1AW7Z9|)Y!9{UF%bb+mx_;*oB*g>X~I36xFDgW+Nk%Cz#t@O^oD;86>kTXtjf zq@i{bp2@hgNmgU=8Q3{+N8NflHJR3phou0GnjzQ8MdjdKyQV63tfn7)u3N!yN^=QVs2(ZYVXRc2Hw|>#S3* zO_LxXgRVXDnj_wgR$cM3kW`GJjDe{@Z#vx?HjUFEKJr@=kKhKgoRQEaP6n$t z$_~UQc2LTy0x0uZ2E$;I8dUUQeJBC$x|EXg3v@a|t4X_1P7SNohefzOWA#)tKphVg z8)YVeShixdj7Ht*l>^dj_!$Ep@@fq^>saw@*ej}WO(^xl8X&7%vBS+@d6^EV3@Vb0 zT4J3ajb=S0q}-k)_yPjqiAz*<-^!Y52cP(~kWb0?NWe*x5!H)X<4mk}+ z&DEGZeDBM2$asDU)(x?cvbh``I8(0_aWaS_9WfL^CAz6nAxk?%_aWBKm+hHeA#BTZ zLkAj3dd~98R$)3COhSC>Du7%P7t9XQJ)@eIOZU3gpasYk&5)(mhRI)kqQji1GxR68 zOiZP830A5jJss0_U++}~-)zZ73SlU)g-Bozv#OS<8G_vJmb;Zft1>F0;9Gws<9BK{ zUIe3iuhO8Kl|FdeI@8IC$cePhngkY)X4-Tu9zNxX4s|f~V$DP!w^XfC7ImKJr>aWF zLp_J*yLex(%wp9jHRbHXI$ACDeK`lSbv~Wa0ohoKq7=T8c0#TT4_k%7u;=&(8A$rJWO7uLlJFqiI>loS%m}y+9e0WNL%x3vs_8^tcmT~&5Btm zu*O5ZF^~j>>5^F5Cx>t>s2wVkkEH~%i3}>m0R?liCKX7HJu{iv&dWrcTO9}{kF`}Z%2y0{ zij?A9p)_(czHSJ4MtE4~$f#4CI@xjp-V~KgL@QlitPGe+*WnYy>VR4yW`rj(hMmQJ z)RIdrH*8px+9hazb;>$=*)mFkZJTV{iPFvmT|nAzPqN)cwLEp&*^)t7y~r$OWr7V$ z(8D~6WZGRJheK7W@lL13@$N|F zR(;WZVh1sT8bNxNE=!}yRMk69({usjCohUr+vAL?PC`U=P>Z?h!=bN@14))gY6|d1 zvm)TI&83=IGysKc zUzqTqc+!+3WH~#ZH?Lj7yli|BX0@@ZB^rtqm}V>tt=^1bQ8pbI#6T%C5lY}%hRJgY z`>={IGblw23-xl@BuWlfAJbW_LrRH-SGv6#rIn_oS%n_5{#gT~NP{djCeE~_&M>>S zN}RO+HG3~JM7k*uB`?WA^^B-fLyOPxRkHz&>H;8&v<6+Rqg2{h&u|}>RH_nm{bs6y zlB5jkr#7gH?`Q;N4N^nEBJ6R1AFs|+gOq#LEz`A_uUS)-B1%GJn(B%(?*7RqIt=*q zBvno4T7yO~Mwm`8LkGI%D0Ze;oD51iClFd(zB<;ahxG|8wy=&Obc|_}XzPwT(^Gj+ zB}eACcB)ljD%Q~OrsbegKa;xzAF_De3+Rb4APsGG(z^4(CpxqWWSJDOn(AeUT!9>m zF*QpZ66@h$v71G7j5JGLMy&%K+ECX*qNWBsVpk*sFld$w0UK4U^f=BlW2!HBZcd&d zt&Vk8F+|cF3|Jg%ao#LsxHTO<=ZOy0JQRX&;9fDFkzzxS3I^K1;O?webmC%@BTHd- z*7gOWAw2A;V!7N6;b{s?5j6phT&h_~jY8OKX(>%Gf<~5}X>`6NLR%~-zwe0-As}2% zL!F6($K?hJ7RNqY$uwsqYax?X1W5dJ-N99w4U~r};piE0A{Grmt6KFkiC2{rvF4=d z=EzH|7CPdj3}ZA*oIL|OJN>@p)Tj>NNTb7MSrEF0b^&xL(M-Eh~r6}VepE5g8mdNyZ!nL~u)lJ%AbsFU9JSyi$fY^~l=}e}; zMLc!=+WyPu=l@fCKe~7S2kuMv{@~ub?g@8)@9uZqE#CQ0cmC2H;`Fyp|KjPJZvXo2 zufL7n`pB(!-pZc*o0G3Pc?5U?FK&L`@jpHO^5f6G@ryU&8=rCX!6WbJ(+>acp>y~r z4&HY#IQYc<_w0AUZo!Yf@;!koCFM)`=%Cs=_~o-}iHjbBomp*AWcr0!yCW#_0iXmbU4k=NGe5fivi&FLg+#42ZXA3Z*)(+d;soE5a8W z{d9YfwKPP&tS(^x@fJqNLqZ>J7ulZQ*7A)m-W91*HISM^DQ6}_$*Y5q9#0^62o5*{ z(z>~Ae`yON;Gt>+Sz;CjHrg1!}Li~&P(BdyfSCD)fPg$3+C+QJC&qzVmTY*e2#=}8t-z#CDD z&$r8?Hu!q0vIv%tEiNKjUd+mu`~vnLZs9VWDJLp;cb4;>YgW=+hg3=>OReVpNy4bP zMzNrK`JqXNV?(~=7O=mtg%M~tASyBxHONSwfIyHsDN+rXLGH|1247~|j z%*vOg1?)fA!U$5$qBWn3F`MlfJ~C-KQO}CIZMmO-+7f2qHqvbknbeC&EMKw<*bi=D zgdIru<;&s%_V1rvOFs4x$)%iuGw2o!uq|O|X-#e`5`}P6@E%gnxt2Zz-}`eUH-x~L zz${?@-WIUXkRBD7nMu?oamozz!VE@IGqu`r>iu5BFvk68!dJvZvH&c-fc^O`V0#$| z$_>~_+=tqIKHu?_m^7M3KVO6_54=QKj>UH%aexFM9jFED&usxa5kdKowvS~yLY!k| zhRH^mjA&a@K9`Q=#2FFY&ZvC#`wub+%C4@gogUh4I_`tjqbHBD;bdsG z1+-B~#rdL5qr98SIB;}7J#)?aq(4!C5#G3f{i!Wr z;r^hLno&+=k}V3k{J={BEjG+%8Mo9*rv?b%B?^Rbz2E|EKYs!HlUu;LRFTXkl}Quf zMN5JEP*nvRE`FQpO%#;txr`jy&}dK@PQWn!h6U_OHa29M_==1%?`Q0sHPPU=__rnqKVGQ(dK!&gcT$M|tX637^;xbe`Awo@ba;;`jluGx zuz>yXEno>60>y63ajC$i9NsU@Fi|2(BH`srMlM(58(=*%)0upE29_WB1?-P)0UIbY z!fSh=MzrZ5B|eodX)?wP@hF{*Gt;i=b9%KF8=!~Nsq-t1fQXEbS zHN$K4x_piF3TD_#}0&Zg*LElW&!(mwt%%!PM&sC$rQY81a>enL$22pJB>7j31cl&tt7pU zkwFR`32d8Q!2azmV0!{JlpQnfmf9IQ6&5ME-EQ?kH6{)2LIgfyW^unWOh@SkST@58 z*uS*}Y?h^=UUMW{4C#l}PSN4=mNfAyV+w|Nx7$mKdYdjxs___T3oT%OcnjEG$MH#J zs^+F{FafYkepVN0rkW3j&9pPrCv;Ot?1%}=U{&*I0sA-4uH~YAn3?1!0g1D66E@hu zw}Tj`S{XTjs^E>cVHHX&D6$0AoQMEp!ubOBZ)^dZ%cO;>GwJuS=A>%z^;{;yLL|}> zQ^`z1V;8`t7O;PP3)pToX!RLzrEucZ;;5K3Y<=YB_z6>VweB z@gbe?zkvN~TfpMo!7yM_e$elmU8U@ay-BIgDlXs2kd8MiXiX{T%p{4Jf-HM} z0sBK+zQYXG%c`R^y+yfc?QOU`PEz z7HyBXwuVtjAsS-PEF+B=uu_uxRI}UgQ~nSf$_C|VfD!)80`>>CfUQ-%e1hWnvB_uq zYQ)w^l{H#vz1(v%vRvf+l1Y05rO*yQ@_z0D_WQSh&638n)xpI=B7|&N&)H>|M>P>_ zZtF#!h;STrOG7u^RVift{{QB^5AWUo(ET61|29wo5WWBD_kMd(1Mq9^weG#?-e=wY z-MjC<`?YudyDh-x|9f|S?hbsXd*_?(?45q-^gBI=;-u|82@4Nl3+dilR_`2J7 zZvCrU@4xkRw}M;5t*` zw~xQ&xPQzYf9j3@1$Ghs&W#su7&r1aJ`FGoe(2~skN)hDbaZz3JBRN({My4eAHM$J z4-WnTICC&KC>$K`|H}Tm_WgZ){}X}g8-LDUym|EITURjcr_=eBb>@+A{^Ie`n@_G3 zSS1fV`m*yEZ)|E|l^pa)JAd(LQwyu)pGV61i-((9SiKN)q@KTcu&IUB8=Obd`HTCT zT7XuqR6BohZ$k?(x+)-?U%Yig3-Btr<`EEhVUxfrdFBy+e(@EX1lBxFoL_wTCV@3i zB9NNY?Hv6r&H$_(@g?vo__uLMZ7@(T65fIonJ(oJiQhg z56&;bO)ac>`cocU1V=DP(af3w0hYfBYb^-wKe+IgSZlVucYZP1gtZnQM-MK>ORP1s zAD&;ho3K_x=kUKhxEL+5Ry}g~`{x(VCahJX9R9}#7xofs)%J(KeSTqW!df-T;lF!u zVIIL?v`eo#{_t1NFNT}2R*iD_uOD0tmRPGUo?{uCu-1(7D-SOEORQBFAO3^$i{2)z zHKY8(gA09$wd&%-51e0guf|#@$3FbPgNx1*Yt6;)J-^U4VXYbEy$>$FY>Bnz;vYM| zXm7$=Gs=%YxcJf~)|!j|+WEzsuTEeXT{Ft}o?m>)rpDKL{?{K|eDSjJwU_`{>L#qU zp1S|7)+VgAo`2_qi{=t*EhfJC{Gzc5Ypo;S^5CMrz*?Ky4!`dF zLf(Y6){(D&a8X-gt;NJU&M%}*SZf{mng8o?nzUVXbwf{osOKVy%VC zmz`e}H({-Hq<((EY!X=0wtRj;ZxUEji9NrdHVLeSDS3WDZW36F#V< z8@Xv3(wb<1N9$7852G3$i#?nMSBHwxcy0GnN5gpsH>xkW{RIV%IFA z+nphXvLwX;%tUd(^8^le(~Vhc{&x~GstgT|!e#@(#HMAL0PDr850tcMcQeAgt(nkVv<0wprdk6!PYu&J)T<*vtuvM7tKSCy&BXJl%+9&0cair}=7MoYXl zqTuQ>1ZE#?YElD!CBT`I?9fgsC9kZv$#y1HOUkOJCJs304%K^Fy4Yy7;<>4jmrVUL zSD1QjSqqTb<95Y_NS{^a1%+1oxFjMeX3o5pJur7jn7iX;DsY&rw}Umui7Bi~_A6P#Cd zI~frwn?@G2Kkw^QTn^$;uZ7$50bFT{rrHvd#e`terq-+)9S04S^kPuXi=>fl%B5Vj zA+_p^m8-T?Zt<+6CR$Sum6il|ow^b6$q0&zP|EL*nqy6hYtvFuh2lVNR0e6a5a+nG zA!d4R*X2Nb=3a`=pS~(SSJyG1xG;_jEnHUrh-~>vCb+cttR^_x&iCZNDLKJFl<}yR zKsk}|pAw%KOXdkJ>0x$ioI}!CA@bcCW*8Bm=d3_nX@HB}mQ>YjP}_Sx6o6XNkJl3K zy~@<9i!+79$XZ=wThyz{)X!?vr4*1hN&@Z<&Nv4K13@XQ!pn8XM^A7#i;d!G0b_C| zPQ|uEjFCpJpNqP&Mfg1^>vnYTwH0Pnyk*Qyoq5UBKXsLeQTU*nyuq!FeTDr9znUd=qnQc32TA6)c^LW6l|{Lu{y99Z|vP ziq9-eoqoyGKY5j@S69evvs9^(^-7a}HJSREt&od4DN?l6tU)u8#%V1XXLZ_r$^b%6 zE4+k8L)vf~Hm?U^B*azM(`jUuHgP(~B}3HU({M#BEldqICLa$V@43p5vSf{| zDx+8Gmhlu*kOWRs9iTP6s>Y(x;|{GL7TTCa2LvOZ{K^}UU~m( z_YU8C_>G6gv2%W_$_;a_4xBk@0@813F zJO3S23jD|S-+Es@`Fkhde)z`2PXyHffAZk#L4ClVx$(_6tQ%hhSOfpV(fe-xrMs0o zvpes)_dgGM2X8pI59$TJd;e=s`O`B{UGNuf|CQU}?b7W}2P}Xey!D;8{JU?t`&W;@ z_2`?9hDUEY`lQ3(xc_HP|Iz99oW^&=`#%Wo5+M6`_dfD64L`Kq=ER|~_t&<2^5g&1 z=N(lK4$f+xP#u#3Em52#P<-6sS3alT{L!2LcH2jP{K|Ji#Jc-O7!UfA{- zeC9)e0tE_5xDb+y@0D!JmIDDKTb5;89wpl{P(N9cWm%F3$&;J}2;9R^#xj&CKuDOv z7-mYD%Ty?o*Ov0y(iTdG*OssCYhS0=uk>x(x}0O5lh_WSq~Di&f4}p)_vpX&8ungG zdsy1b{fEm!Kj{9>vd|B>|6p0@o$lYap*5}xZrKS(TV|>jtFZ-r&H|gw&F->LebZbP zs&00cg({n^Wud^WrqG-&*29fH_--+&TU4yU({pM$2`8~EG~$db3k^HNwyXN#(=(5mT_CK;L^bPwTUKaZL{SPe*eck>Cr_jaYHRg;h3niW8ve2kA zYRkHCyney`H~DJNwMfD>&)D=R%siKgVuSFAKe4{n@qv?Gj;itwEisV&L=b0&dKyb$}dPy{xQ;^Kv9y zs8+{mEZb_^c{ykO$ye-G8+#^fT_iTo(Fi_y1fL`YHEcEDQal`_Gq!{;K=WmW6)8{in-9f5rX% zWuYH;|H-n@kGa3MEcB!9?=B1dy8EA(h5m;7pO%Gw&Hay4Xii@gv#j-JE(;y5-?Xd% z-*Eqn4P6-Cf(O?u3q=pET~^l7`m^j-*~mz(MRB>^3uCGDFqfZ;ue&ea{nWD17wvv> zSy=-IkGG)rkYZaSNkfRTQr3(4b}Jh>D|sj!N0DZlz%8{|jdvNP9>L1s z0>?~RER>VUShzpH2#Zj${t#S0{Uakc7a4gzZSy{59tjNY$I` zl(k6$<6K^BB&X-wXv}92wR}gVg?N+^AWtT2W|CY)uX(LR4&03iC&ZL09~)U8um88# z8jge4@B4SZu~Xl6xo_DFtbcQ@;c`3PdS7|q|k?zg}_v%qu2$prJRzMMnv z!{H2kD06S+i(!onOZ%#NyTdfabh*hHG<#NRUbv!;JOs+=G)Usb1Z`ZJu<2vlio!>+ zL>^p^kE> zFzAW5nqHj>4Xe>$w$#^6x$0qyNvJDu-AFssD)OASB{9{LR(|^;E6;x}cb~HIy|q{( zA&Aj6JjbRuUe#*Uh#h9FvvMD?`oPm4HRU=DcUmGP^*T*U_t%0%zBBaaO8h9sLYnCb zG6nslmEXF^%Ja$U_bDsiTdSpLDp}LiQI+KaC0{$0)K!CPoHYT$Jz~69_NkFjyx%b8 zQ6n4odO;C_UO65f47?iO)58gGCRH_<6RRc1?Tf5DpQnDGvhuwZmr2gxMxvjFK#{^x zuFk}=dMb8S#0YnNY|w0ZaJ)X0>QLO54WUq4jg6=PMJUlxq)KO*2q=W1z$eG@ZHuft zpQ3(Wvhux`n64qIX3C0#nlzl=@^X;iW5KnvK-am+Jf2PG8h%d=8ZtP<3Zr~VlVXV+ zufZM@9iSraiAK8lv=%Vo6DuaiTNYXQKW**5`UkT9duo`H7%UDj;Pn)6umQ0V>H=ijZM25e%p4WUo4p1S zYs0;`(HS%&9g8tf#^pCJu=1&^DeglN!*{Rkv4m3{P=5^?eOM9{BZ5y2M6yxc=k5?@#tVv3EPDA4u*!VQ(8$4EVzCdv}|=*?>oQbyu
  • ?HWZq=G?^#;4u;Zm*6 z&)8ifn|yH3$gRh0xSn$8a`kQUZe20NN}D&jRz+#{m97+Y4X*+iV5OdosVio;WmuKI zVk%WdQ@WOG@bC@SMF8pAkVZ>sR*aq|nXdjLH(WmO$h04|TLa0cXi{I&OjpmA-mo9u zy-DXM(yp#;(vD4T_n&G1`qyWF{WV*Uuh_pnLyxc8dVJZ2oY3RfZ9TqZKbq0wE4ChA zv>)EB9;a`}`LZqW7i@A{k1t&f2HZuL>(^#mo!{qiebH9z^EU1b#lB!G_Bk7JLa|@7 z75l9HXhyNm+lqZ=Rs_D;YI?j79D*UpTY*tH2tgh=FuynA`kbxUr)_duvCrCyeQLJV zQ{Gdzw+1(*EQF;TpzW`c}?L>$<$f`uhm;-_c|1XAlJw3zW<1gI-~DD zX7~MvZODnf|LCJPTp{3`AF>|_W8D9^RAW=waxZMs|Shj(wf`Gvph zHrsS>w#jYNy~P&zP4=(P5cpQx<=$vR&UU#s+YQ}v*A2bN*8B}NvLv|aA?_M>^1 zyT!K5>+I(F*Qx3zwW{qPK}UuJ9lVjFTo>zCS1zsP&&#@ogtx1tn^Q=p@NpH5vZIeFNZuQwqwR&oj^BkM} zS@(?mX4|CCw8>AK6m>n@Ht9_^<_wcQ%QoqaHspj!pJ|))2K&*BNpITO_@09S-*)Uj zc0c3(#O{^5w_W}+-}zkE(b^xbeQ@1(@Xmvq#^3wzzkUA(u>QZ=S#o-uF4x7i=dV3| z4PM(^|Kp91x?koN-68kE=J&ULX{)yN#H|Oo|7<(9JJ?M+zJ2(t&6ga$3)};c8s92# z_$}w}Ip616-~9L?b9l|c4-bCZ^#`sy*I&5)3$F6|*7}Dxb~dYye`riN9{`6h0RHxrn)C4gFs`W;#p$_UKVQN#D`?zQA!)x{j zw6%CiVcXSeS_7qYih4#i=b)hAz+3HB7ea4$-o7mKHs@_NG(zS1P71GU=|NpGhWYNC z0DHr|p$!!YOV>4l?Kfnz-Y;0%9CY`syO(8M2)%vxvaB_w!89{;l`$fvSSMYo&9%BA zYzWJ$^)~0*Y^d57IaM;V>1Y_ssv~v&v)nEBmU~$XSbLTYT_|f~qp>Vh*|5I}T|5Tv zaNc3Jx^N7<)A`P2q3>|MV_E3ioo~0H3x~@Mdp9f#9qo;5=)&IG+S*#S)f@Nh@2zVK zdrRBU>{b^-)eZY&-`YZ1x3{+KRu}fxyPWS@)&e_QJ2up;s%j1|Ypj453N@N0<{jpV z8&6yodfmo#%R)mNAsf0-t#>=$y?y;p;IAQ&>E* z#0}AIH62g3o7JwINDYUDNT*kt>#cQd{qi7O2vyfF55k4e#`@(zv=FMSUk*eIq4o9p zR4vKM=L@~Cpyk>)xKTDiv2$uUKj8epvUa_5?apPP?_YcWve5Ufz0Za&RI9meU#3u7 z2-Vl^OI2zM-SEj9PqqbE2)%ye`emU{+IZ6L*TLP%(?81^;{$7#d!rZ2$AwX|PiC7f zQ1&WQoa?yF3!4}0Ru@7&o1SH%PuP6Ive5IJ=a+?EyLs)h&}%lYnL-y2-H$o#`&AZ0 zKkBqY!eZ!0oc8@Hi=iKO+S9|u&<{H8OH&q4IzO~#U&XQ*`oT53)x`pQ$oZkYw>r+> z<&=2#f!$jRr-yIYeZ#WQ*YCc5S?KF_U$-puwY#qclHWWl{eSZRrm`;Xg7-V^ptu-% zr}J{CB~_MaiXGBWBv1*k8m`a9f!zn}KEO8CLg?Y{;j++!-GgPJ`@8!#bm4^KEzY+r z3%%8O>#_pu?OvWmEVRJx?&Vp;!kJ`!!+wV@o<+zTvMuYvR%;uzWuelBWJ4Fucs{ab z$E?L6<6~=f(5x+V?pt6-eq-&~hqr+Gf9T;RsQ!1`K^xTm+uQ&B{dey7 z_aplc-23j{`}S_!i|;*b_s@4fy!)Kp%_ zXg#+65Z6`CzjJ=t`C@0;`S`Uzbv+rIlLyh&PgQ{ zfqnIk(!p3(4$z?5ufzr;MK9B1^mN?xhY&I)CE`q9eZX_36qyQTCHrixTx7_?sF2B$ zxL4^kI`v+b$g%lyj8ZC?FA$W>pMAi}6o)HQ984+F;LiDWpq9>vr7+>qE1700fkClY zNf63GnYLOG*5Gq(B%FQlKdwyizgMRChbaYAD;8Q>vtGg_G1eFwa@ekqrjI2%D7> z5y9%=5sOIRX|99_mSHA~A&Dvmf=FQ4Wh?68O;d{HYxQn?Wo3#lPbre6NUNbjcnj2l z^(QL%3T}{5U*DgXV?8Au^0Y(s8e8VNb!yMMG6lRc1vI6IdlN=gtN9Rv6rzw?z&ViN zTJ#T)D4C4-S)bbVHL7LP(6u#>t-dRk-2tQ zG{KF*B_RbW-EV$iWr{mjrm+1Zs+ohZ+ODXrn#3VV&DU>b`go<|!z5%>6sr|02p72? zf!&+nDd1;g{SF@ zmNGsj(5qC`5@E#J*+`)cK8;P5Se&HusbLL?H(LRAQ=C$S;t49+DB@VBRu;W*N$$YC zQZ_Us13o{L3K(X*i`8*05TLg5Q;Oh_m3=A|3dQ(lwBQYEMMUdl>nUTzbcl{o#H9$t z5^}qmePC#13Up50~L^3S}(TSz4K*xH3gyO5ro9QlLc@(q)NF;GTAK$i)zf z;lYPcx}2dExB$L?N>};dmync`h5Q`iyCSMD3-X3Lifg;L!TS60Yw)EnziwNk?n ztO!DvqTXWI=Nq!lb(_cX6yO+XJR@6yhs&k_-#OK(&Y%WWYRN#=;06)7DAxipNHut0 zUe(oP6-w37F0o(L~f2{sx;s@>14`CMm|Z0J>P?HyA+R-kzM$`o&#Qnam{lxlm1 zWW6+!^+diAPIQW8ij`Bjd=P;<{w|q@}w>d~Q zTisx;uNZ{u7smDfu5_dwhi#DK|JsAc?tgs$se8Y-N9}%RSKD>&ymaSb+r#aj*}8e_ z!R|ZUfz4mv%x=7A%xA})^HWhsDmEFFjF`P_HKr#5YOVw7&k!(_ zYzfjRCRU=sPIk~{#iOmUIQu}HZ!Im(G%@6yRMpK0H!LH=R<{G#kO7NLMagWSrxd-q zB6gxhdZhKqUV3ozl}GL|pNo>@`kgCjZKQWuE?-V7xP_I)#PDv_p3jXu=j#MI-3U%G zMOdb~CfF6kiIR`$LfK*>Qc#0&iew^CUhjwHbBNDNXZ234h7~(mHOV4A9|c9wQpz*3 zaws$6ie^86dIN)&+&kKw%II+Y_Kz*&3Va(!8-TBG+y}q4G~bhHTIYmpw^SqRg=|O9 z5BjlDK@*!TB-tTjIuk3PRYK>7Ca%OirqDiG2h5%_X7=BgW_CKE<{U26l`iQq!Lqa= zaA_kYb>b`q&S|Q?h;CRxzL@J)tz0fV%C$Y?CwEQK?fTr3##1;}`}M>URImrePbOS3we@N=$N3;IKqW}?Dl#3Kk_U;f6@-0t3&=M!bm<+Da$6au$NHIn{X zxliRIWlAo2#kAD&F-$CB#v4(i&IK!7T0C-$#TncA>r0Dc`?5bG4@*@%6&%q$DlH;f z8cv4zREZ$OSTfwl@lY$Sw7f!@KyQA?rDsizEV+K;N^lhx6OI?s(YVSt!(BBxf5>l# zu$l2nd%c*)$~HUt&=N}dEHUa}WkT}cVmlykr0EMbqe8~7m3ZMCQKf{soD6!~gJQ~u z4~996R@79UZ?M1sRiA|Wg)ZA_l00?um6x75w!k>bd~IpT><&2Pm`lYwbi?H(^N)>>#IMuj4SYMT)J`EYYx}1FU@zR*H-9>_N5yDvq_Zs%F@iv zh%(VsI>1$AuZfL{6^}pB!?FV7hdu2~o}o3dmMf>Y0oTSlIkbLh1Xxa@%$JvDc_PXn z`F=9yFX_#C+=v?6;5oORdt%8pmNDa0PF<2_uhr~;Ru{h%>^M$3wv7=189}i|d z7Vp(nZ@^3w5gg;#I#;dK<9xCUKCqW?7>y59q)6ZV$V+{|2Mi&}^=nu1c(|W0HM(V% z$Y>cd*5}TiF`O}kIwdSn9r|gre=eDe z_4~EH5{X3zuwD(e;}NN$_{CbDOSYokP=~?;Uc)j{GSt4*of^gA`rOi7&oIggiq6pSXEi#iCsSR&vfOm{ispTvAV}9{cYb?&#*q>Zl z9NUliMoRW&duEol*pkpvd8R0Xeei}x5K+GzV1_BEl%|kY8K-)enqxK>0h>=;NzsZh zlGQNNkQ&uyRkAW?yRohHsW3&Qd}g!gq4g$(I6d&ri)o0;Zg7$*g|0ImsG%V z;+7v1Z|dt&}ze<+9RBM&9v0e_e9fP&J%O%%Xw;wB$in%mvG z$^0iX=dy?l)(YKhl4VUqNClLBiAeiGo|?j9-lm)^GI)wYGx3~hj@SQ>aop}WeEH$y z4nA-|?|*i`u=kgHH}Cz-?wfWm?0j|S`t9G|R=55QWc5GN{SNo_o4>c2-1w7?(ZdB{`_F9!RF8DOEMS)TR`>1Yq|y9oGsb@`{ox`{L$7} zma*E)Zy+r#%k&Lp9J$9lE;?4(D_VNZZ0kF}FJ&BU+Dup2{P}U2%10Z3>39&WunDv@ z(~~bm{b(I<8vFbTdp}EaI?LzR0Kc)%udo5MG{0k?mycYw1y|VnS(?wB8#)22u^X1v_!YKcmgaPZ!(SQ!eq)DU zVJ~KBekUCMl4V9In`OG@}r6FK7cK8)`Se9mW_YOb5dg3n)#HucrS<4DLC`&V&bHO%XHFm)jHb<6bb@whf zzdyrYYK>(9YOk<^va~Gr)b~z9_NAl&PU8r( z!e+_RoKCync>TZb{9DJ~3%8vc&jtUu|Ih!IE%5vY9EIFc_WBoRo%gQ)+;ElI1=rE& zm6jn&hSHR;y6%cV2)*fstN32GUIF*THM=JG-P5i*x594ORZqC$s&lH48{%>$6wCyJ zP{8Q*K&}m6lqrKpTFDBY@R9?flC0~+Aw9vbu=RJnb-l+5p4Ujf_ns(C|D7CwF@c$tA_rxM*7hI+Y ze(|)ijVAx8sFF2icBX7CLy>_D;PLDgXJdxbXBAU2`rz`w$vA)!aOa#K3PFf3=mU^C zZ`P&p009H3&U=x-xTSQ%C^QGHi}^ZTrF*eL7~|mbd00}20@z%Sr$K%^+n0ng7ox8k z>tHH!lkfHnyP-Kj#%)WY7eV0Ab$lf)Vy!U`ybSUl+A7iU57T%jomP{fi~RXKlgG|S zbn_xxNO;SgQ5#DRnL#7fv-oU1>n()JN@+MU18AV{&+#Q>lrU)n9mV)wx(GMI@>MsD z<>@uY5=rA80e&D$XQ+PDO>55^9ihM{D=%Jio?Bt)Egn%fT-9x2VH6o#=nN+f z{xqC)g)Qo{opgoQ<5^C6@|`;#qCbwS-o=fA^V{-cHYYuJMg%Yt!5p$O@#B;37yv{I z-0|4M#^ZTz&;#duO9^`-uF=Fy*uSv()$Ac zCB1LeckJ9N^7K6Pr{sfY2m*Wvynzf5wNLutIC%a9{m=))cj<>idejUI&*v2*Y59UC zb8!%=mrBv5S4sC{alvc_F2;#^R4Nszd+di1uU=rv-I$^a?X0O8^*%`PkGH#m7&KwD zO?7iEB+>6`Q5ouV!(2nC27;D_LRK*4tO2d%d=7c zPuA_cUSvGqJ@fU~V%oy6O=U(sU#rngG9$C3ie#x!R*R#2*GeN{3u&}S+^mE54QS+^ zOg>`;rgX8J5LBW_$_e1d=YvuqjafoYxhVC0O)`jEJuy(KdxN8kiGn~=evS^SvT8My zSh)$6B1VTd`xf~2xhTe%V5y|^@#Q9;W7UD%jo1GVbG*@UXde98L3sbod;ho>+r4$? z?{*T~Z`k_RE!O>R_u6KD{(1Z}{=B7SeAE$r=-9Sp+#Zk8Rn8o35vRMGi$xsE z7Hv#^esO6DA9s|T>+h{|D;!zcBA#5ZEIv5d8q4^kFOPpdwRBgK&hK)7Ope4%e_!0% zTII-dw!ar9?@wgBzB88b$&Y^O{P`ykCO#*OdzDo96@_t+#RyG=34Q&vFpoQ;=K64R zm5&GRQ6FxOMG9Oy`T6Rl`|t@zv9Y8xQG10WR$J0DqV~f6oJ4K-6JvROUs+n-b4TP@ z-kGSq!V#@4?}@0rP|iuzzKD(Ggd9uD`M9I#+z41Zx55#xE$2+sUMT4#YCqv6V@W;F zU0TvBj)<|OGf{hm6JlG^=@#eWNHdAr*WNak@%;MIGCt}kG9R^9I5@UNw4?SyGmd4u z`me@6AAjA_5a$DXL!D(UVPlD(}zCD)np|>wB=QT%98Os?u?)>#tPTFlb=g!d= z4}wV~eeh$(!an#fmlpPMM^7FLJ2U@X;iTRcb|#W8H0vaiKH%TRl0NV*XKts4>lH`W zk0qUnq$?cs+mhOmbfJusNO~~o`a|#1)_l~_ljbAo3g`cqdwx>PVsVF$?YH~V@y|VR zX$g0Zo;bGRk0R4}R!PJwh{wXsq1q!$9;RHWRDE)@5(%ooaH1K82|CAAI3|mikBf7e zCOE}eyyY|rpU%WsNY@KbotA-V<#jWCdU4CW%3_x@xwQcfBuL6lrYot z|8F_=-nsqB&9L)Z{~62r`+59?E%1WI(e>lK)RzfhFLn7(xE4F#+?mFB%lh<*Vi5A4 zwU@d=NEmNMu9D7l_6gw%Ve+gK!jsW`es^?XARQB&ILyC`6TCr zWg{KfMg=w9#@mq-+KMRE_?|Y-7=Xa@TO#Z1Vf_nMi;FN|J#@bDuOe-FO z|4j^Vc?)@*UGS4TxdGM=j?ae9+(HgY7c^7_RL z-;b*rhm;!N%O(hcS^*=2T7sYk$hIKG$aW-+VauGLPE;SC-1Lr~G&#AkoU1tPy%ufY zA2-@O+%db?AJIoc@hQ#U=UM^Q-7t>(^3Ac!RwA4SJV@C8v+{9qA z*}pGqgi zqR*G=f8YDvz4!0EV2|IsZtp?6KiK^uxF28ussKEG z*Rk_IcJ2hX|Npn0>vkTz{e$fmNi4zphaMI34I9-mvbbRwLcXBm-7;?VzV*S15wp z)KFI+T9cgTe5B6<5B+T>DQF0U3KSR5=3_yE2Nw@y@m8i)gpu z;`jOy#QA?7w&nxBKUwK?^1$`T>4TG1K&KC;*J^s3k}r-l>JS&k(}8p#g2x+Hv6;_7 zzAl$;R3U#AZp9KYB|mUJe`=7d$D3(U7?&jo+BZ;HhK+}qR0)(_7&bYRBSv&cs?k2N z)pERSN)aZ5W}l^U@qBGmf?=!@%3FN0n99I)u|9&aMxR8|MQ#uh9Jf!z^`6#af7%qJ z1|yMFJI8Cuf`!HdwMYV|GPRUmqLKs>4SEKJPSy+uHP&_AgaS57eW6~g6U}HKTkYh8 zV#bKEnJN-wlagE;2o2pcNUE}*fgG=$QZ&8koIiqkDkXn6fx`6^sEOh4*8EJ&$_rrz z!?MEyxI*0;VlHyhe{ii{^;m?S>G)$b5~v%aE)S|`6w|m`&Q$X>2`0E)OJIX7t}zCqSFdGV=cXSi_=1Sv9|-EHM6ENoesPs&aDKlZ3E>pW zRp)Ws3=Rej`ttcsdj6U(SK%ZsfcpHtfWoA;@_c*LEDrMf{5x%wO8*dc+J40!0A=1d)x*-%soMxsy0Q=n$_z zlC1OiJm0;OIA6%?gM;;6qi4+HT#s8z|7cwKP!#r->Vw8?DMD|^<(z2_g?x}-;H74J zzB$JXE{J$Tp<2}{#piI&|CqrALf(+y==2iSJno0HIKLMSl_;V$T9PiAm{Z@_1OYo_`6_J03_nY}?j z7=1&`Fqj#bH{^WBW7p`*y#YiEpo~_|&W=5Rdix2cK(LV453_7u8O^i?LC&|$;9%72 z_0xDUb*eMoI)j5kUf7=*)C;}&_HLiWL0-ro9u`i;Am?o-aiDKu(7D5EYd&T;-!g+k zeK6{0+Tr?qy)oylC#@0)29UNTO8pt>fK}c+g92>HWV;iaN4;r=XTS>wV2w2L^R*70 zZ=As)FziPLxYg{=vKP9LMYAGG*kp)_fXZ+D&0d117UXmmeP8R4E%*RzC$eQ6Q z)|a>bv=osCK{2N5XxyyoB~t-^pph~2_s39B9W*n z(nB?&_qo2Mmt!c4>Pn!+wlu#iXUpL?j6N>)zctc z)51iaY%vP1xK5ibUlsM`x|BtvYsDm9)_a~{R}ETHEtCx_g-X+OerGBbol@yw)DR05 zeH6|ONVUuGWlHZ3I=(<4%$6!l5h@L0VchTd!-;V{I7WJfVL#$+M+)7ZUu;VvT)%?x-F4F%u{K!$#r4j5Y5ghd z`>uzt-Qi@M|I_)g!|xs5dH7=2_YTX4k6-(PgMU8w;=wBq_=Ai4|GxjV{nzcw`^f&< z-fy`Q&L{5Oy4T!$(%#PQ@9n;0x3?SKec;Y_ciy*i<4%0%Vb1Nf2W|iP_J_Bh<2>5V zY(Hx22V0-mdcjs<>#^>CaDUGIGWRpw=UwmL{MXGdZ@zj{*n~IXjc=^BN_GIP?#aSB z)K`+ZK|w4;RNyZD6cVfH6%rO9giDikEeH$wmNfv0)+AaxqgD9XT95ESMK}G=8)(U8XM|mgL)P1l}DtB#sZnr!1;Q76mPcoU6FU;=@& zW@v~(KFyZn#;PT8z%mCZ7BG9IV47P{e3Pzum<2BN1p|%yT*oSOr<5+ zl8byd)lyy45NZDUZdrzPI-haEtoFW5IjI9qqWo^s6@TN47hI>8CjSL`9KAr z5uWqZW0>RDCO&|iyeV+%1L*X@>4h~iec(UkHP=oboZ8`WT``f$pB{{cpz2516Cq&KFIZ@}Kt1M^Bpyg^K=q)aOS7Bd(lCK&6p4 zI|wGBK(*D0cZgAu#yZ2OS`rE8i>FONr^J2Kq$#i|QH@D~0ar`WjeIM?d6J$k&D9__ z7_AJ^xF?!!1cfkGNDmy}ojBAAwLN|T#JpFp825hi@QL1$Am6Vfd`zMofnwo+rifTX z=jce7jIsV)MN5>!bi|ADRdwJbClp~WUFHTos3emlmMeENqQWYE)mt9qNI6q{@|EeY*`ea;+h(kKKxIGhzt)!%4oG15aj@hL?y9%vao zwp3@NL??pxhY>7UDr0m6;_#?pd0GtCb3I&17v4JQU$ReltTp! z7OZ?sAH+blm3U9EDkdE+(9UT|tS}jFh6m?KuFY7Zw17o=CB?{lqZG+k3l|#~>!tIp zp_s)24Z>;j!1S!Yr$}k7#i`+#Kd%NUAzvS%X@(?%kaN1{750F-snt}EMiNB0n`8Pq6RPA; zunwR)HHOr6%HoQ?0N++2=ZzB{IM~;#_T`+$$pZqeV;(fnV1*bPPjf(9s9gYa7RIm`gn(jlqF1IqnVt@(3c&`r@SxrxfgL!sZD-X|9I-^oT zujkN#)Xc}Ow;0QaiPRx_>9EMj@v zexU>AhiMOFm4XdZ8}-_qtn;6y6yadT>eaKfKcUDYP*8wt(t_+`OAMU~W+c2bsv(hJ zn68Xa*VdE*Y5=L5DY1qjjZ9n|;i6F1%Pja)VAItYKMZk&9upb$IoI_Q3T#AF2Dp%+ zTRhe9Cb)Wrj|5A3B9f}+B})zQBvYewUMZPwhO=G&AkQQ`b(|ggm}*3!i!$yjE7btk!O?6A zrySp&P=u`EutBAJpuSQaWU^KwY$xApgtTOyMoSbf4st@hV1|Nln{Xy=3UFLR>MGM{ zmqt`K?M-R*0Ti-2e7F$@IS3(6ta@s?kVR$ZlcyAVARUD}MhQ1-q{Op=R%Kh>S|0Hi z#lBY2yg{MNjz&UH$U3IA*+LBzH)8`KB1crs+ofR$&w6AkHxvn}I>NKyg7J_=7bGeSoMgdSdhsX(QF~(G2sF> z1gTNwMi|A*bf#aziV4USnhf?(F<$N0M|vxk%@+J1y2AmA_o9tq2(D((1lvi((aL~N zRW){6x-eA4LP*o!u6EHN%t<4bkJqu}sIOrpoXJISU#n{H)hd@QV9qa3g-Q-Ran_=e zZLoJOGJy5snCA^T+|cj!^obbBXW@8HRZy7~GAzHMgxvR#R3Ric z^gy;Btw(}w2uuiq$ylo8xN|}g?3POrk{hyYm%<{xB0{%IqBmZOrKO;+1M9G_qi4X3 ztc|j+Crv4o$}l1nn@}DXdOY7p>0FNVYd)Xf#Pa!k#83O{Ca;F*i0rs^N)gK-X)~uc zEX0ue{-hUy^YtD{k0?2V)Ie2%N*R=3DSJvX>zvL)gBV9O_26h|#$=uKM0iNDylNB; z${mUDc&j*{E6|o$=!Y@qFHL#)NV47l+p6I%1BzUr41(*;d>|?0Yr@D3%SpN*dmC~q zS8F*wI-v;YU4tkQb*3cn9o-5VnN~87`M?PwmTIaI;4eAw?m|hVhdG};rI1r$8B(gl zI!DwR!AUdIx=Ek;ZGvz#+O86<&{p96&)kZ9fT06auTdkoCNXf5UC>XEP)EdPqVfDjR6 zZd#c&km3E&Se37-bPTO^Wz6pjf}5m4$R z{ZWpL<6Vx!|9*Jep?MfN{Mmy)Kls?e3l6x0Yxn!NDdCO*F6W!e0_??a0Hrn8<{{icNwEmv;8`u73J-PPDwHL2FeeGJ; z|8afA^%|Gt^0{2jZ#v)PY&xIpJaBvm2G^Y5_g%qIJ20XT1SMq}xR!{Z_mG3ho78cm~P3uk9doIw& z_;e@JE@Oj>D$w?H7{A_)d>64-B$7s-}wd0hZ?Gw-h&Bw?c_y4dOj> z2s|>}RYX3c6+89$P1LaluZQ9j3UFHj(4g2a=eJ8Q1zL$PC_utvj34N7{32^+oGkdM!wkZuDOH$dc=0@xJ@ zg{z}nItFIuwFEWd+IpriXQmxM(YY8s7OGe*7ur^GT*A86E-GSjzMLZ9QG&mTAKxUPeqI+kea5#ekinPuB~j#ul$LPYC^=QJ4i^)(lsJmE0W zxlrWjIN1JC@Ki*MhkJ?nz55N2wK0hX%kF(Ec@(GAzn zU%37lAC?Pvu_)x?tr#H;T1jDkAh}%+f}V7&FQ{}xO1h9UlFek>NXO&zM>}JE_n;>p zQ{*kG$fb3GRIq$X&4ihGe;ZS{E?jpklWx+ryqKi0zN+Q(SW}ok1le%@%Z1P}9vr%W z3K}J|U0~X2jgR5lTpw&W{~SV(C6W?Gx}&7Q!M#AsRJ<`bqtBe*g@VTv{eqqs1#OU_ zI$WgM)KB>*(82k;P~ezC605Ar#i=r$N-In+8J|Dz8B_cQgd9^8Fapfp;{{N>uvE4% z?bKl%7~9UzL;hn5$;#&opi+^RYvU1lkf7K(y~jL02KkODYVnf7wyV{&1`1Xe^^80( z)tJY-A@7u88-$HkDG|pEGoyE9p2JeOAT&% z@o+n-3s)cG35(#FGNGkak`-V=adDYQGbY)P{84EN41>O`M}}J9jQzag0yrWA&~| zvW+$yR!d-@&vS4)et6;0$9Sw*s}>Zj+~y6cp-1?_{L5t1>Adi$WBj0LD49`7rD$Db zQmuGmK4iEZ|LelfALAP}vtA%mk)hh}WQNgUaXxr&IKB-%65#D#Nb_Mrj9?1iZ4Wqs zm)Uve-(GV$K7HX4#~etKDK zcvzY5=Pj4xz0l7ci)56G1BoQUb#*xGj;vZ_UIh>>9B;euuw#*W@os_yg;+(R8P25A zWMtl(Hym%i@X%v?Kc40EG`Pk#Ify1koSK^Nv`xqBFFfQJuc|UZ6xg`clzU7EYZd1s z-lpSK7k>5_FS1QG8BLNP1Q*0XtlONAMjMWoUU={^o-bB~a;kvyT3pe>gelG+t#3G9 zc;P|Ecy$Pl$td2;q}rvfk~5Y06ow7QCC>vd-Q#U==bva z_3Q9`jDCN=>AxWFp7S|(IrrSU_be{br734z^XFa9!?v2+QV+91g5gwJ&8Bdm_v2gL zyyZE#)m$j1bGF(^=ZKtB&lKbJ+?a;VYn}sJ&7(0SpRLJK20XBI(E^$uw`cQ;Xa837 zuG>iENN|&zPU(e`pmoQO3iIW)eV{oo9DAmx57oJTOO@-2S~FZS+Htev%m3iQ0_R!Y z;!p6&X4L8=W0f>gR|W9MJSP1@%CmQ?d8eD{0dEgpDbXCtyNWihzVoPO&sK9A-0&q# zaF3#@MGBn9iSe=7^9;|@R`Z0~t&t4wv@`i)BSTjmcTAZ1pl5NbdB29*K*wE$#nRa< z;YN>JmTR7ct>$_|(BpPdC~$6zWr%e4xK;UN&-_;NtdN!B2`t;J%Q~lXnZ)?WFP!6< z+iDJ+_??EEt%1axh6cP5u;T{uEYIwq`M|(WA8P87gC+^Fj%oyscR|I)v92yY+B37o zo#Jami)gwzkx!)gri_d`R4yFwOm8*s6KJZ<6Jk^2Go4O_u8qsSxHO3W&rRJvb?_?( z-+0hG`1FIz2flUSod+@pF4_Ol{r4O=YyV63KXd<6_Wf?(7xuk+Uu|Dx-_r6omfyDA zUXCt5Z10_W-vwUuvwP11UjHB6bLF0A?m2Ji4@+NMdfk$~^wg!L#jh`3yVzQcE{(qOS*hWdDoOEpfh{N~XW_i7C7t8$N#qvJI2ZjIH^F zx!kL=WtAXSi!Jk_k@ra(55DbXFDi-o!WSY1ELP+4@XLV!$@f{Qz1Y7^??J^tPPhSnzz1w9#ULA2*(0 zHs-zotq<(gCW+>jx>|z5G(k~JtU#s5cb);sm!S25&Dtc1<24K=Yp!7yS+m|QmBvq> z1Cm>y^?}{mBuOMny=+ouOID4lle8m@A4~=$AAr^ewri6lRpGc^Kby?wKvw-|H(D9L z_8E}8dv*N*`!#cj=Q2*csq-}Ws&@Js-5VEdVeT!f>mv&`oJ2a;Q1xc4o=sQknM$5a zjjyc5xocL}2R007$KYGZwVhHST>>8hTAY_q%sy^_=U%zG{(udeITUNCe7uFoy=D~n zfn)?FJ+`hE=PqAef53{(9J2CEKASDpV_k45ATqo>erTPad+zG`19oiY5N)_3=cH0p zkJOVos<+2Km*(f5y}JH@C7U^vcQS4q$#M;%r7(_#=*QjJ%{i;<57@GqLzP@Q+cd>? zHA8g4HICIe)-@blQ_h*I>knA7nL`c&T-Ep%Y9b$Jxrc>gxIf_H5=*ypooR zHMve8wAe{wmxtB|R_*@fM}nlbQCI2_slFwvU_p&f99Vuhv_7zGn27c8eT^)~B{eFii(e)@aJCRK!@fV{VHQfu6HPp`^(IQ&nqD-W77Sl-3&W z$UrSTtCspcc_#}7sW zgfpOLZ&9cfkV?WJ5GkKeBw4&(IZllL#X;zjEs8i2rRy@+;W`Fh%jVtG_|4-0V-D(X zQDER%+37d&S|d((OqS!uKS%+^?0-SMEs8!7ZT3K2cN*8Da!$(X<2GPG@r%_ipxD&z zf+RGWTm^5WvA9&{=>E8}F3kShsyl2y+&-d`##ewxLQCqYcrA_~G|1Dt=e1d+nP{!Qki5UhZhqztsH-znElvlYpZ=lk_ljfv0m4Zn;Aqi zk6YlgA6T`v+T%qun=cj@OU%zT@wXfy-QCn@vWHH%h>q50OZc^rFU$ts% zwKppL6s?-Tb2ZW89SZ4;%RfJR6{Lgqn;QT|N|aPn2t{;c%rSE2_^vcqahF5d7KM|m zD7+)Ko1J8(VdZPsHpi#gzk}*q6meCd+X9O5=`@ z2L{J?v(N#z2uQj>iW0}S>G7EZmpQZWsxU+t?G_0!9XHZ6n@;FFMv45k#@&d>*DMaq4JhQC7htOiY-wkx?bg4x%_yK!Fh4^@oRs( zNijIhFr9|%RBZ!E1K$xQic(|x31;oFo{RTTz=ndqnL{j^M+wESR4vV`bi3O+_R#Bt zXCJl7Z}lLfMD-#=sacL`q_P?9_}7iuhp%3=)xM9(gyQNMlTq;~h+l1u>+{m=BRtRC z5+~n|o3VN!T>uyHB|DcSj=h@!;yiM-w8b8{y%Z!a)xo1Bo23(IZhQh3XQx-Wt@cH> zUuP6D+3KV9O0OUq_QkFCOfTC2cgcMgb9<5zWt7_3a1PqDTkS#JETkLnMic3Z zkVrK<=y6wRGxtCYXur9=sO?NFhqfEIhB5I%tgVmB4<>l#$7_Wl#c;`4U5fy|SnaBl zqku1Q)gJ%m0PmXLgYsJtxZXFw{du0xsnH(7%b9VJ7H4N>zPd^eJ28YfX_nc=nJ=zV z!}h}y^hvXnEzW#u@7~*|&YXJD)Z)`-&svJjpSQQL?w)ggZ@+9BoCu+M6ic~Aq(#(qF zt>rC5li4EhbB?NHQ*%N|hVjOW-9!WUj5K{Eui`iHZZWl@%1z#1ss#+U4D~x?Fj#B3 zl-sRgnF42tl{y)c0|<`S`+4r<4@lF>Q8I|>x|nl)ejQfwr9N4$#(1w0i^WPh+!raM z9m?QJQCITZN>EM31uv4Wc49TJYBUA6gJOv&8f$q_2ACW;kW`&gO9n3>hgt$!1pP{w zCDG87ZWA3>y7_yyWQf-JVj@)xM^mK)4V(tEOe#uu34~8)5+L;!nN(wrYqVjHw2}^X zdDlaSqPo;I#B#S%r_zueK@_#BSk(q_*}^()CmQ4G_Q{ zU%OZmZ(+5*n(IeB2CZYFH?5GO>6ca%Ot)QBYnHV}Md5;3-S0F=Ct9}rh!X4OhiK_=LO9N!hmYkBu_ah-OGh4#i|$1sNp=xBHE+#Yym07 zjiT9173{p^sfyJ|txFb?Apx*}pmti+7p{_VZu0U0EJ-QSKR z)QDFQV==P@0#VF-IvunHAD&dKWXox#4bIi2Qe?&3*7|MmMHnigL_Oem6HL))1VkM~ z0*FbE*DexlFyEruex`Bgq))Na-J@i{+-AzprEM33o5hk83WP!w3d1Pb6_Xh?Q*WWH z6Qr76YQ-T#Yyd73LZ@%Ecwi?T*{N01sX$vOmSUNT;PbihQlf+^sYCe|PBmjVM#nQW z9?!aBc6>MAyCp*uBq&T4DJ#qhU4(4OCY`fub-rIHajAHr-p_f0nw8PvhPz@nI1r>3 z)PQSV-|wUoaU+$(q`DUi`a@(O&k~R!x{(6W^8^mH(zQI@H;Pdzsi$h4SZRDs&b{>k z8ARRG6$g@?BW#O_)D<|_krEvw*7P#9mY5N1eNrMj;CrGTUrFUrwH9_LLnch38VE=F z5r0tODd5VIqg|-az`0(rmj*d5B8TXF*6E-f#L7@qD}#24aVLm9;Vl_lPgEChvz|A( z1gUz!8jV=6Yl(Hm4Pz3-Mr!>$+)>3Gv*NG>=VQrcGS1~v=^9)m%SND1rk{fI1WRw)lf@TIamw#j6_Ct3TA4hrKXau42NtSCli$%W##i;hA{jPQsE1= zQWY)W1u19sdtjeD=?BlOcT0wPuN?$2oe_!CV?D7{fJ+`eo-I?nEot4PRqrSv77J?a zd|;)F;!pb;)EHsD6(r;51sVGc($-5gN}O$h=?dnUr#hT7*drS zmMO-ZN*T>_rMN$r>*J}W-cLkUO2Ham@Mlw@R=OPyA$UVCvT@!3ktB7q8z@D}$v96F z6w-7WhfeBc1}>QIKR<~5luWkiv{edJ+>Q&SlZ8$?M!-_e-)#EAF(-(mu_6eJh@@7q zLO2I|U5XP`z7Kq+_)uC#%0x}-A%U{xtp@#(Lb|7cYh>imNgb4?zrH0yvw<@ZULivL z7ThLG9y}96iDtgfmb5TQs^OTPAWawIxztK9EefKZ!vlf5l?u3I9b9LDYz0ya@dpwj zxXR3u807_mTpSzQCl}6tKnBSd>`)Tcslj| z5|l3cA@D3)$XkUpVR}d)>WUk*nGl=c^94F=R*D{{TPGVKFW#slrd0}o{V^T(2vE=@ z1BW;YDTq}un=;UtV5lGw(3lL1zj;6g(Tt!Xi`AKC!VjV*^BtCC$I{5px6 zL8p(DRE=3tx_E=Ds!^q5t9FKrQk-VNDPK>nM$lljuHvX(4d6%@FKdTDwL7L}S1B=C zG+JiDIL3DO-~#bm0Y-6PqMKD~jte~*txv%zFzh2(?XhJ=D3 zc1*NOF79hrO_zuWxj?K=`anItR3lU6+JQ(s+Sl=DU*qs#JDLL#`GUu)V-=m~ zMI+TiNG>j0AOQj<%PmXHkh$?Q?efiAGN3d>>w#7u?^{8CEJZ>f%GW~Ws@X2-3LgUQ z0SupvVY(svL(l6(0)3^S2?T!C~a|%b`g=cRdgG9hrf;=TzI_WqS z2N-6|^%`OryvitbH6T~=T(*JvU@H|3`C7deQH4%fBH(Rn7*9}mV36>0k_^Vf_LHXf zAExI$I>R@cM4N3}skvNHYAO-BZ7`f4<{L?nTMpN{J`-ei_6Bu7i9*}e2iUArG?3-! z;^8N~bQ=&Hl^u1r*@gkt(WL?IM}O+$FFJfqTd~_7ALNC>y^yc$jd;TlTdSGp>lZ}f=n(c zJ7p@mZF%FrdKGn3KB4AsOJ)KJaH?gqHLMkP(_)1Hafdh#o)tl;j;8e!U1>WN-KJc{ zUq)NE5d`SaHf6>KS-wxAm4p4`LFPO5g8XYI%p*?r4Wvb6B1pT3G{kz?DJ4KEOFfH1 z0q_}{z#I@X8xu4!SA!mOzT;3nq-7jfAqj!Cy152}3Y^)?5|MB^QF7q6n+&jM#>up! z;Ziuxr5bXlBd9`_kgFPlb(jEfl?P7jz#%BoYiI?x2ojp?uFUwb>w`Sv_QnMRJsgx7 zpI~*JGCd4zmq`4tUJrwba@@u}F-_}6y{%Njiz}UMfDFJ10cJrc78Nm&U{o?>C%&B? zZdITA3t7tt1Q?jEZOV)fjMGD?b|8=J3C&--9->px(huDurb=G z>{TE~Gs2N}m45K%FXckMHq%6Ez^h*71cg?;TCf7@1+>yypqP&{Rq6;EPk9H~1B#pz zJ)&eZZ>*{5bT2?RVnN_g81*Nq{#uqPR&^3)_|17@RFtNzlu~)8b!9_eR)2?{<4kJ^GXycxV+9q>dhxhuXHThU%w+ znvI&70^_%N4nLwHHg4GriSVdQIT}b@Uho$SqA4>E+8!p!T%M8eqMY)JRxPL#IIPnS zmM8dpzhzdDXf;oU@gvn7eUCF$drOXHN-J2cL7`PX{jD|A+mT0AK&!@_ozh-rw%+?D@?ed+9%xti@k0 zHWq%qV9eh$ug%>(r_TOlwmS2p8D;w4rlqOx1M(C9U3P_k4!rD7Qf_=@f>OaJuMT|K z6~2iDn4l2wZUpd7EWiY1op&PuG!%d%eI!Em3Q|e*(tQ{Wj2(3e|f71oqAmjX^jNZ8QJe9|wLqc%t{ z)221K*o^!2D%KJkD-)C;-^~=9H?aVxcnXe8EWjzAf~O1xh>IzyR?UWeO}9w+qi&Z* za8)DnLY?s!3Zh?$2BbFAFcU)RRBuF2o>+iWJOxjhSb$SJ1y39bfDyS6SFZYOq+cw1 zKx!|90#DXnPm|0Sa9ys@Ji)nK6S3&Xsh)y!Cl=roPr(x=7T^?5!OBp82{xBgJq71X zEWjzAg2NLFaEhnk&`^L0n4an>c>KfyoZ=}sdnmvJ=e(6uEWqQ20$3#^BzFBqAqbm{ z7r_HqTeJc}C`0iTwb(cH1c3*bQb&6izq*2Dr#@Hx60 z0nQu>Fu{A(sh)zzPAtGFo`N$b7T^?5!RbQ*CU}oH)l+cV!~&e+DR|7p0-WM0c=S+! z39h0~?Gy~Y{|`^SZtCDm51w)0x&z4mf83Al`}V&2@}HJpxP0c`Yxh25&y9P~r8}0Y zi@#rd{^I_H>lRL*f7kre=Wd%5X78E3c;=sH#OdEoKWF-pfXONTYj0ezFhf0YO_}-F zp_7JJY*sHHl|H_3{Oxl^4}ub;6`{-IQKeeqO;s~}5uQNAuIdssL$-arMxC)L79UVI zo&jL}`<0oG?i#k*0Z*`HL6Q{PUYVdQT6^Q^n_c|KuCY$k#YQ|C3-n2=7|9YE=dC~# z-!UM&99Jw3&M1vU!OsW(P>F+LP=J=}&2%{9F|u~LhY&QHCeT(W zT@42FS=Os2dvYR_>oKj32$1HUKt^@@`d(Q(R*s3_vdc+Vs9%gC74}x{~XHYV}lAOZAhg70y>%tRiLe zu~xX;>hT>3+0mLeW7LfRfE#Q~|F~C&@<7&aEyAp5$#yp*5yihKeDrzbs zcqIspnu(rU5mle#V=1t7{QzvBN$=S;*v^_19P>w638XMk(E;SVNq__>l$bO!A&s_L zWHauCvJFY;C*?90wkWsBNE^O^G@$D@?OK{qmGKotH0&7H9k_C~Er5~;^%w$6P@#@m z^Z|c!3oGJ%-AQzEZhON!Kmi8l#zQ9!&pX!O9J@_G(8>gb9@-nwChWWauVI6JA5Ixo zrGgYKO3N}qzdr@VA|=Ap@6i$9wOO>CP{bE<8^8#x0ocKEx?$I_#~1;LYL_aB8cTr) z4%bQcS>Q<12xhHB(kLX^h!{77t{qj=9$RW`tOBgTF7U2hV{NkwM5UVAPI&BgrBau; zT#HbwG?U^QS>BA9wKOYX5GiG5$^sJahaDAxdVbnW z1br@Di5Df-_SkAOmIt}qC7_l^0N6k+-@a?Goz*fhu9kVK73#y$P_v#5RCp(q2lc9| zC7+yA8sLI7nX%J#tX1Q}?p1P zaeULRk&f8{Cb$a(TfkueHrN8*xNESTw*de67BIoZoV0OhAPpGgH|$!P%`ISpE4{|X z;{nQG@x1=zS4*2)z&49#g8RPK#@PUIa9F-(*NEF6mM6I3YHU1ivzxEmHRv|ooZv!d zFZgChW~r$qY010z=fy8AzIIVv49{J(K6uSR?I3(`@xa#(yybv(;DXur9oWDBTl?R>-`yYI|Hysc+jrx>OZR1FpSSOf z9b3JKYRAf z&u2b5bNNhZ=G^JuOy4qn)%4#^pEvcpsn6~1T4C`^yKDD9t^Q{O9>NhA)*^|O18IV?EZv9F&v&3BD5w7>gHHD9%?_H$yPl4CguxSk5Lrs!$~rX5ci5h6>ACIb?ObJyE8pa#>pVr%}h%m>w#{M1qQ$4G`cK4WnM1 z4Cz`VkRusB#QU6zi1axk4)sS6Rc}eHHnj+(x<=5@NN}p*!BL)*g<2$unO@E=6U~+ZvP>wt&}(GkLS*_gBMQ*~NmWtS z%hzMD+b{bnSeqxih7s>3GgV${;Nc!x@t0$T_`>E;fym$jVj!4YG^9Mzb$y+vX_u@P znyK{^F4odelPo7YV7bf?BaGeG8(jR_CdIDn4K6H#>_2tje-|CR3vtbKlvd*mZ@%*^g~f_*-_`D_4oIN6!SYw$W{+olF5#fRQ6r z4(Cw`1ZM|86tOfvDx!z7YP8w0t!M%5(!R8pqfFW7VtPeZDBP}>3RO=e5yuKJiY$!E z)uBu}7C?%9xmn@`GDpZ+wBi9l@h!MzWV2GVB9lFrucnKIz??Obib;Z!%l%d;kV|nf zG(e;c1c~V&$$u}HCR&=MgK&PbPKHy}xlv&_l<}Yq3bBEQ8I7=9FT!D-O1qa!#4BzA z4_6YjkW2~9a)`zkM)kEoeu7jk6VF77Ubhuj;)oCxIp0CmUFy>t=|&4{uSP zOuQSZcnccgjfa#VR)XxQQN_qu&%yI9ldjt>ObFu`jW5)$mAcpu%e4X?rWL0QFgH(&O~J4`L;7^Lb?Gjcrx1@P%nX*jT1^$k8M z>6%Y3qEHs|7GSxPb!|)9G^fOK^kBw`g?UXVS9Yyrnx2KIoo zF^`lzlkVv1XH7vTLCrWK1+fkTG6XeJ%Tc$cTc4#DYcDIj-!@)_iE7sf58@x1x6YemR-8dlj-&21VU)NLsvpL{aE@ z>QS)qCX3*x5oL_7W-^r`S%d=J43g|M0&XNtLs3{mramyDAc7X@2ngxnqg+!eavb6M z)BO%G`nf*Hw8@qV#S9}5DN9;f8Bt_mJj({+up{d|lk@;?^(b3T5iw8STlSd>3u-wt zH71romO>+nbhKG*_~jDj&je~Q&Fl3LokXgZCUs5&5x5}Fe^FzashX=zT|J^mgcD}7 zPX+W;rcE$7SxN^x9W4irGC>`2{Gq%U?<*C74>p$1yq_XMnr+p@G6VxtpsbSRW@SE! zdeRAy={?LAd04`vjHEX9j>?_JFi`24fxKRv@}yEm3XKqr1WlM} z)ui+^2$u(;s6|juf`V!A9x1^_uAC*fatFefm=Q&!D%+uOOX{=TWIDnSAy8|`D`7NT zH$aUQELM$Jfg@S36lL~YxJdzOwiqU|ImoA5P%o0>OfOZf`MqIF$_P1-;$G<` zn7!9*QZNc$srf=ZzdxySN>{_gzLyWfRpwGU#C(Ixx37qVRWfDrwd75f~zAD?p7KLc=w3H+ls;n$3?RL73zRbB*Tu}OU7tl45UoSlRJ~Jk0!&&mQ>|XDoh|d-Ms5`Uf6>&w z)jiSq&re_U5Pli`|BdyF>KpMv%+t%H>6HoY_fCF`cFEy(LsQJMDuZw~Z)6b3ZMR#B zV3ZpzS!t!(rs#aZ&aYTqSN7I(YOg>39IsN%LV22u>bqKYsf;pgKC#zGEvb8;s~Bhc7&V zkHB#pz_dWnAdYEBTb98l-qG8D2Lyh5AP2!Ehc|C3O~FvWKL!W?y7|*F{T{O0`nTh6 z`~Wck+-foQ$UMedaD=wd8vZztaU{=}kak;WD@Xk{luZ=O`&ntKq}~7Dqk5(H|DS3) zYJ1R~;Q+`AIOu@?8H8Cqgb|iuum6OBTv03>i!|${M%3sN&>G0U8|tQ{cB2wviHwwi zk3?${&$>;;j6di=t|M?SNjK$=SAh!(0(bLz)?et^m8jG4yDG!icm)SJyQ-~twqiQI zR=?g_JK`{{G=)bA3OG@816rfQucgu~-LA!w#a^|#%LD_1t`7p6I~%cKU~_MJ5~V#= zyA1^Y-|jYp0LJ5Xj|rX#w%a{M&mMoFePaY}*VHCh|Az+;O9Me<)oz0OkWH08e4-s? z&~&7ydU_POE1;HPRyE)Y8HtCWn=6tzQj!xB+l?+J15~)6D#f$cF6piDRY}64QIEn@u zmKllY23ujAI+r4gNH11xxQTuvuec#!G=wCGBN@Rq{1)BJ!lG_?bIoF;-RD_w4Vo`z z*4A24_wc2Iq3)QrS~Zn{mA&M!P*uw9ff)c1&EQfNfUfDb6%Mr{KfDBg?9)2G%+ zeRpOaiX5+a+o;mur*^M7*q84&F{4+wzmReaQ|36pu!1tz1DOH?IffE729{~}Yj-eF z_Zt~6`0t0%$OPfQ3A1M?WNj_fpeW$#q8cETMyc!aQS*pvEBUmo_`=zuoMAzM(Z2oQ z!}#-PwP>Y?WJ^rOP?=NfYv3-sXqiH-U9bZ&pG-z0DKX*8bdNZAw^@n91P9XFWW+Ta zMw-D4NY$KEWxr&uVO<4W?SsmQnb_{u`k?EBFn(eqI#}y3IWoO6!CUId-!jM6`e2y1 zInqMmeyOgl%n)l{yl&Bh{4a4JQ+3hHIKf8o{8|h3l}E>=BZM#w%2?f)m9D zZ+p7Dg%eiL7inmz5F=|?$)8RX3;jU24r*#Ez+sH!va}k;vSz-*C}}^|up4rWt`W7G zEr*JMdN>iciljq#!ckuvyqrKZg6zunY<9h+fU^YfIWOOT2sk`Raw&i`JNW)TZ|b_K zgWoO??qCJ~e-b*SUS( zskx=8Jp+DA7y7(oE-M$m@@I!T>!tk{ug`;4OQTlXAFkl>&$ShwxB>-MkSHi{m5Hi! zZq;@3t4dz0h(aeBcEVv4>&Q)zU>$-$62*Qrm@alX2zQ;e4!}M0@OtrB;h^J%JF~OS z?n*e{811HTDTm1tkiT6q(5w+^N>s6d$*VCXui!?ylrY(Tt_R_vD3K6yk!(QeQEj0I zgnRVib>_(K&Jj|AOqX7!-jnk zznlodoS9ZEIa;lC+7%cDxCaie=Z_UGa=dT{I_vbVgc~3AP2n1?oRY7yq-hG#)f$7R z+A1jCK*judtXi=%2p=LcZK+;ZjVgs+JQR`ZtF0msZvOB(b*ymXK_!Dmopo|I!bQeM zeN(uSz*GW6JnyZOeu803nM&1fb{sg<>?nnJUiRCDSi)ENU=Aucgpw&G>oO4Todlm2l&uz9}5ljzKNEQCyQSt3$4?WlR^Law$+sw3k>F(rT)bFGw(z zYe|-|+9e@aSjBPx_g9D4@neM>_fZ@)>a1sXCEWO^ZwhDX1mmQ9X;)9KHhff}X7*~O zTw@Kqr7;MowxwVO7S5NLcvK7!m<0x}WhDUaza3uB94p+o11xCtgU)(-SHg{t`lfJp zP;zB|FGBH+R?>{6TE(K1RqAa6N0SY<4^n7>+9rroi}leomE`R-sfK{I-gC#{_0+M# zjqg>U(e0h}IY_+X6MH15*e%Kq7 zVnTea3X%i^+}k?qiCy8259lT?sMiN-XQa4L3~sM>lQ}vL38u2DDt%Vb^C2c8`Z|Gv zxYnn_8ZUtNOr{zhET=DZ*0EjTj*rzQF4{12e3QbQd>*$u9q_P3recaO${@fwrG+Z8 z5V9)ZzB5~h`fN7Y&h<4&0l1&)C#fK&$+OY01UC!38< zqN9sw1WQAGLWc?#C>I)x+7JR!oKr{-Hrjh`?5sz3g?oZ+N-(1+>w=`!dK4{U-2{`B z$chq?&2S)RS12)%itzy)4)h4D8g_jCnqF)HyFI`~c7=O_9Vi~84H7ukP<|4TU{+9R z2Z<*9buI+ zr9`(DEk#7d>?Gtis+J(Ln2hoY-|(dnDPAw;UE}AMWH|^n$23T>aRp%RZtK`_v2Y_XfV##bk?7-E8G+88}(o& z(nspaQe4+*#OoMQq6-S6niY_89b+nWwq8i^iq)_att5C`?_foct$u|2^j+beVBb&@ zR<4Abq2^k(9&FVsS|1b+?EnXhJZ2R03R_DyBT~PhfdVLEUrTifFQEwGb)z@{Q-%Bo&TD1}(lXVuo~ zy0@oPfqOzVm~HsEH4N)^@mOUrV=w5e!@I!^onR-hK~7K2^|J!T`-FgF(B4pR;Lev0 ztb(j#tlt4u3!6B}B=Kf6qN%9rCb9$bcS&bGvMbyZ>;$}!_sLnZ4}hu$d>oajG%bMJIlLY|b~45XbkIm#5A0tXd|rSLjNwPf z38WA!k`gWU{3W4_Db<9GMhv~5li)nxWQ0)6kqJIxyOpG7`&9+SbylWhgWeC;PP?-n z+|>x2U{6BYhPN9RF$pR8^#F{+y&$iyl28M;h>qoo7Mg>iaW#iI2vUn8Ia;Y;18noq zdT8I;Q6E_2Gdmc7&bohBvYlZ6SR=x)93xs8AJeAlFpV)riznn$(zA# zs!3Ms79{n=Fc$0##zZ`{?mI@d@xBk*bk@DQlI;Y0j47fP$hogdIO9Y7pn4mvYSJpm zs27p(4Bo(F8Mpv>g@o#dStA~1z})eJy_`R^4jm)g_?|sz(^+5Jm24;2hq#{VO9fYL zBd-_OwTjfRa|K;dd@y9{xuVb40)e7NPip1dl8%sVE3)b}GXOV#Xnpk<*~aH(&}QKO zkIq~&b?{3EUwu$H2p#w%xC8k61J63(*?-^uTlZhJ|Kj~m+IR21Pw#vAzQ5h~N+MI(zY+#SegN0Tj3= z`1ghPFRU*R3un&%X#VE;=g#Nm&zk%9x%bYk&t>P%nElc0duA`2&CEV#<_9zHp1EWO zn|b8)ozpi=_ooxnkC?h^>RnUcn40pcpAXDkJU>_o+meiL3)99+@4jkk`g1$tj1vuUKD#5%xQcJ$+`1#qIMER2 z(>s!ktLu>DlRJtwzPoLTcFT@9<3vN8f7+2`e5V?ce0)dI#?L^TqJ3gVoN=Nd&c}Ad z89zpC;(T;RoN=Nd&PR4689yNmNj~hGyJ&v;1PgS$Gb71hXzrr9-I5GILOY4&8@Jm- zu|Bxdi224vqH$CmQ0s zW=E23cY;^#v=fX|j3o2#v=fYz4CM0d)O~2&=x%oZf9=$l2z7uTXym&{PaiSs4 zi+02r-w8HxHg?1rCmP~Be@C40onRB^xjW*F6Af|JNALgdojQ2kLE^xF9(dgWbpPG^ zU%o%G@2-6>*ymaP>hdMahxdMdZ*%Wydp^EL*|Try<|Te#=KdE6PcQZG|YljmOPwH!+zAwm>_o03#)EDlx2M! zlS?=35#!2p{&@QJ^302Gv+sR|@YPGdecs+{Uii&#+UN&D${eNk-fe$luALqG1hUw#U&-Ffxm1^6F+`sdou-gNONv@g8!>@Tt#`jBz5 zZE|SPTj}+nvTTGi0i>Pk_#73_x_#R5dlPEUrfWi2N>U@nkGtdcqZn)uJU!g8HP~kuRh25wP z8C!(Y!pk-i5OFt{DkKsC$L}X}jShhnP=&r>qLOH`e$$U_F1PEy^;7@vr@nVH^l#7q z%$Zl*`|UgA#JinOKJ7=Zd*YebT>Rs2U&L-yhm12}4iQVOR8fu!Z8;nEU<@c53M$$b zSjpE4gTVlGfzp*+P1|IAUJ1YGgDNuliNWgnLD(XyWNVfQn(?)>h@h5lnZ$UmI-8u(MbpuYF&^FJScmGh_@Uhd@I z{}{kn9x^U+RU*;vx#a}ZsMw`KLvKQ$_;OWi8>G>%raTGGu|k=4vZIY8=3cb_%%|P- z2jfYX-Fn-b`8G^F>lJsr?6r}l`zmYieDe*D`*(Ij8Zz!y>7bB~a0C^phBL6@fC&6% zM(JZplC2d|^-R7H@8=RtzqYB;pPB2`-f(*G;N$PP@oztI?su-e?&e2-?`s>rbDq5N z=`Vio?ROIFhB#!LZx$ff!({#CdZhz&p~tauyT&+iHVG&FLd;I~{T;WRO#8Tz#OYW5 z=H4GJ-*w+zAAbI&pWnFn{f}OIIQ7S~o#);3?0Y}5oOoh|-4KS1t9+|ff+4uv_Mkym zF)`gA4{5b#f{s#nkmLhSt3qg9O++^tufOJ}{;~J!*WDR<=QTh4&E2hsfB(X_U2uTixM;|Dg0!Y1 z#$SJ<{ehK_*7cXoa6kV-GVq%he5CL`-w)^X(z)N&Km3_#>hg^n1%@5<-i z_`6?y>_tENGms_&U$yfBK8x{YT{&F9<%2{rR8tf5+L4(va~489+yjzgao&*KqHgz-=8o z`sT_b&(XiO?78&FH}?Oq$x63e@BcNsQ5-U!AlK)J@vHvz>RZ2j^JjnZt@mG4f9dyM zU;6sjZohEfTW|Qv1>x5|?>XncE68rJL&g&%_8c+pe)2F;`(^VD_n!9C7e4%gKVN*` zi+**(Gr#iq>Y3-AZF+9Jqrh%3L&g)N=o~Tr?s~hY-gecq?)lKdXJ7s9Yq58H`C7ca z`ohP3(S6QoAHM3Azh^fJBZ+sPm2h7QX!#8W! zT+dwj&li2>gZHo-`61&8@@|e8-}&x$ycbI1#j@v2ij~_x`yfIb!_dORtR9-|+1DcSNIK{qd!r`^p8Uh41-~#HW8PXlpM?a#nJ`C;uldt` z_x{%x{`khZFMs6wv)K<|)`z||SAKZ)(Kr9}@U?-r{mp6Y20mmwK~BpN zxB2e7^tGQ$hLY=-UIX79#=T#->D9Ns`!VPL={9yFJ7hdT8p{#m??${A--Q3dzxwze z_g?w4%zwV6_^?Fcyd$4n{N|p^U-CLR$8KbXj3-DQIb!TL{_giHPke{ovf5WY@~M9( zCq6#^&Azq%uDd>TYx7S}xcEADBRyn1K?ckb<7fTxWmA8++H&rVz2t`2hv>(>w*FG= z-#_w%+k&rq{%=3>g4;gKZls2cC&)WFV*I6V{`rbK9`}Uj&c2^K|25@1&UXI1aO*1e zRk>4--C3Hy0WeMu8BdU8a>V%I|9I95W`6(8*F7fOY~K2?eV3zjJs8GA_jVt5*89>= zJmbymMq+^097oTux@-w6O|6fe)d-0y> z`R7gl;-UCn{~w-zH6!rChi#-l0N2y6q^3`8iN8y>i`VjQTkUv)H9U;hnj{8z5Z^U{ z@(shSjte@TV87WeUhC8Z9gkl>4&x&mW?L}_c?3Y}0L2hN_~cl0v8I}XaMi=ZB)tQY zv2f$y;I|#)iU-*wP7*_T;dZh;$N*v=6gY9@e!pAIWI&`1pN&{XGiMN45W|&Ex`rGK zsHmPQVetgR)%02zf6xKMC9A^2(VSS*bSEa}+9k=Y)#D{6qp_fPMy;baeXbmUQKqX! zn(0U>ElY+}knDCMa>Q=*vRNhU6+%)UwxOt&LSd}Zpkg4V?BoH&!>$h^_xl^kK~UwD zv(qaREWJ||x$oV!Hu6KQq?4uvIZjC@u-)4!>CsuN2RdPL>&j>KS=HK!XU60kZ&78*-ao`FLWdlh!B~y*lVIFNTb<5!iLsb1-}~R z(=;P=dj43~_uva4SCh#jdYg*5-wYu}_cb}aS4Hj%`C86Dh45TJ?R=nQh&923eMd}#GB5(o~0rMK0?0G*Lr zc03~{OO93xHGOOe&Sm(Hl+TkO_Jc_tQ3WT#gY3b92X6(9BT}jvTRT#$VZC%c2@>!5 zkEkIN%cV$&=XEyJ^|jQRPIdhPD4YwCUczjHjGuPG)!mF;;n^w+9^-OpkQz}GtH~IX zu+d#vLGPgJ11o56U;{n8qBA|o3GS5Xq4)nuJ)C5t*d_(YR<(n_kPUo5fWe`)*%5VB zI!5gV#_1t6f8mI4pxE20U3IO+*nwiFj(hW6oUE{YsE1e9A|NnQK9X_y4Bb>(UL#>< zAH4ZXM(sY7jx-1^;iv3M3f4o}cp{x4n+O7SpbBTWNivY(B-+rhblQt_>bWEniDFSc z39CvK#08fGtL|$PiX!BWAbmqF>@s`JpzA>G_Ricj6_|SK^fWpB`=#rb8cU(YKQ4ZH z@rp%y@r;GaELvo_X8y zXXdFHWBQimmn;{S&physsXO<6e*FMIdd*}ATs-h*-y=0K1K64kSLdb6OtsDV&A2Ta)!z|0B9D!xj3*voo zt>H@`WGjm`14J|s%#{OV%%kV^`28a~5bU2`vBBX~ z+GFXF|HIz9z`3$kb)wzrPQQ2Wqk?jMaW=L*+=N}Jybo76vQE6MDKvGE4ne>Qu z3aV2sgG8ZbVyvTWH<}W_n>17Fcf<3ykr%!^YY1ve36=|4V=QUM@S<5Q1?n*lVu&_U zYS~RaAIrOtLQ%AOa!hmh=nQHyRmMZDt~>0PYy=bmtJISnEK|W4a-={wl)ylRw?-x_ z@wJnN`+Z+EMyVEUa`oauH-eZ}VdsH@j61i>7<2k{Ajn%?ym3G(7m^9BK`u zOpoGp33Do#ibdLZ*@(fY#?m5QQp~*Eu%f*J$(Ud_Gzp^Eq-Idzezh&eQhl1ON`pq6 zQsZdPY_Sp7A@$yfumWQ?Ke z21s#jr+Ui#&HvuVW(}K`kPKFv9T-Z9#xO^h+F7X6MoFwqR$@7n?&raUWh>icW>ELu zav1k@&oHRFf4VDyCrAQw8^?1x)+^ZKh@Da*oj5&i^ahj~Ybi)J&W9V(Rw;_6l0|Fg zj)#9bYp7&YqejKRBCE)RlFo=j=(@!eWK#gex0H~c46A*i)x_mlOq1`!vxb!@8*J9$ z0U4{OGbWOMr6VrTS7M-&Dq*=pqe~>MuYN?tL6Puw*yV|M} z>0%w0is&eWD?KjY22i!6AhU*Rub4F~3KU0?gNiA_HHRybWIzSs6&cWLbj$z zi4vlBiBN`Va|J9P9ow^K-1@?-A=3@UL%B*HBopZLiVSOuI+5s`LxfCY;jZ1&$2nUc z*;r&Y=dAC{8qyXns5~|rhB9LWPl(B}FqWYljMKFs9m=GM_ArL`b&H$zxcja1hFDRy z!n}a>W3ir@99i9z9x++1%C$1}n%3bOQa8XD17a5JbFeXMnC&u2PVAK%)db(c+UerB zk#9R;rP>_v-Dc7Nnan79oUYNch8KGChV6P5Hz;Z_3JtV8YV>=c2Yv7SP@&r zb_tI*#d?CJ^17)EFpdBY5W{leh?Z05CP&U1rVQCI%lTxt6Nxc`9U!B<2orN-26{wF2!+`!x8a{R}aLBp%L?GLa<8>NwlA3!Nkl4q)n3KckreS7A}dDbpuLzSW*JOo~OU%jVmv z-W)Z}QlyZrCaR5IS&}GI;=>1BF&(PRc+})CnBN{tF(zm0!{$0 zL*bc`?-gbZ!-Yn}V1bEcomN0IQg%HH>k!#VH@Nl?ju0aZ8iWkE!p=-?_XYEYA+nQU zl>mYl228f4Fd&6KO^PIko9(uu1RHtN3Grf(o*BT_t7i>YjlA@I?xZ2s&qeuY*vysU zO|1Z;@W_D4=ks<|?L$CNiW|)gol0uxSToDn`1Gt{uba&D6|&EghEjH*j%D&_*o=$1 zndtVwG7jdVd6nQo(V0~JPnk9B)P}JV(d?$whCpOCxc+^!h7D@a5rczTtUO{ud9gfJ z`*u>J1aef(WTGm;3(2&pfm$we%lXi(q17ULP=ZXgG$$#wNQALdjbbgE_XvVl=euTvaO~f*LR*vco`?y$$gfg98bC{MDK>`J2A}xGwIhSH! z_W(}T>mV!dkm%{jMz_`k7tiY!rCVBDt#++c5MnW_QFK)~1db0+r233mhxIU5$3ViI zdRwx`eOb+A`kVu*GjzM5T34%?IONvTeXxO;X=7);c`yPs!x^_gNrFJ+UtMpI1}hohjHLwKE+%gvf&U{-A=8~@hD?^yd9FRW)!`&GIQX_C_k z1%aybeOhP)@orEF6wDCWj+yni&@#y3cs`RK{=1WgAac|g4k|%Bm&`@dpn_`xcgcZ) zS9OarcJ15OesInA2H%qoKC{Q~er%Vz z@cEq&A80$Ni!a%J?}f(p^Y-uGddJ}xx5ECn`=7n`gXp9G6iZwE_&2qKHLBGzRj%hT1(0U{-`yHevDcOSw`T*GQ%1qy-)|@|vAq#PC5N&L21jf&NQa zoQeS(44WWlZMvO`LpiNg?WW>4fkvy9*JR`<({CqAaU#^U8V-f3cn=A*TUM5eQ8_*i z2eMYPhdS9pNOPo1X0>OCIFlM#!%lvb9u<=d7}kE_6oZ)=YP?d<-*94f2>tjbNc8ktj$zlTH(AnSlm{DD9GJ z?@|dQz8SHkW7Y+q$;`^H#t&ml?yfv)%zG^ zR`fp44zMGh$goWUCD7o2g-M|pBLz@}&OlKOBuPTj0w_F}C?`#4 zvJxt#iQWwuAZX`%PBD0o&Ocfq;Qms*Po{EczeQVV0pnBshQ5Ho_xe)|s+h^tc~Ebh zC*pEI1tIH+u8M%lE)JCqcB_4yNP#IBE8B}0UUQ0}%#dU{7YyePgkpXA5(yS*MS-nTlF-Ufq{T1#rtg!+)z{2!VBgRAvSPSj6bX*8_B{WD%LNl+p zLTgAH1yBfT7$Ufe-0Af$xxG@}$mCtURHB9h*>%|kU2I=xlR_km1sh3GPL9D!k<1z8 z=qQ{(ief<+#H$(|a)Ou=7Yn2KQORX`nL#Q~#-mUvLnNR9$dn3_xN@<$-WI|MPDwQ_ z6l`VK@TH{Xayep@LaJoOmD+ZGamw<2*Qp#hJt3KVt3L>Ee3+0MeG2K+2Z=I`&;%LL zU8)<7;O!DjAxDW(xjB+x$jwRVK@%R~g;BJX7?eYBvjC3+&1QwtdS!&GjHOEjkg-q7 z*(zQ~3XGLUZ_>s3`%W>i{ZbV-M7CoWfP=bXLud^|p z$;(U2`>vj1pz4$cQXTs(VMJkbOLcV0_s%3fkEvkk{sp9dWRA+ z>PT0SUI=Czgpp6Rn?;acml`FZ49<7Dbd2s@!ayczm zY~J;gv2Q4TFMD!rDV3Hf*! zNl{%`N!N%Ds1R7Bgme`u2hnjkM<3xmP!STVw}Mf-%bQ>x0EK2jZ8(e$B(n{N==B}9 zh1G#~i`FHZZ=h6}MeS;8!1l7EQF{?X@;VGiHxbR3@l2Ob^kq<|%ylCqjWlEkOQYZ zs@EcnTpDvKMjEC=Xr@_DLtMa8q)WMMy;0~i*&$VHl?okBE-u*QLG2U+-W`sjNRAM!xzgaHJGtE$Y6_emj91V zF%+taVs4;`(PFmU>eTv_Vzf(vPzs3(MpCZF;J(dOf`X=6M;!`R*j`TOWGyGdzY4~hVZ3)~PeSTq;qg^kJcm zmD3EWC$ua!?n0N47BaM(4s96{IA>JQ9<_jB_peSd*mARpwChBX7+EM<9=SoNSimiw z$+#7Wupy@nm4-wi-c*ke#2lx4BI@t~R~re%Fsad9$!lF zU0yq2cOLcs9Qft#&s_T(H0 z2JsQ6&C!Fl$wWYzqlw5tJukTr_FE_Q92+fFZWygz|GDuv1Wm71fYP>+2uSTES8^KC znqH-FnPJ=1E<=to&L$xfisKP*c0@G5TPxDc6vnZhknLsT_@IlW+L`E;lto;DKrwTK zZMCo1nMAJ^3uc41(FyZViKkpXQuG$1HGOmtqXjCAQ4`0TR2W;qT+hw%&ZxRHi6vYJ z$v-2!@azg>UnA$Z-3ntj+3v(}<`s>e*a~AXDC={}aQtH1X8O&c5vk{`OiVKK{N-*e zS^}?2k%XFHAS+mlw7}cNzIOXzzzHqa%##rw=BUBtgo^Zg;S5n5ySR>B83*%fcQ~Z9 zPAgvO6iZo9mOPa&wEE3-n+-M6y*yZ})FN<8F2wV!fVHhmq?19Fl&6YD(?_3#<3InH zoY?xSch`?rxB`1_VZhOUm0`duT)q8Qav7%D>MC_j)0;Ph13$3^#o!=3Ev9^CIItQl z7n^)6?C8*-23{Y#sczb#@kT8yqj0#x*v?pHQe!+EJc(ewovEhY<7Fa>3RSHb>8PM` zdNS4OTi75k(A|!aq^MjBl#Fc-Vnry#nPL;;(^{+)wYtNB@>=8#M5n<`J1#TbL=I zw#wDpv;6h!Mdzy5*3P=cdF$0>Lzjp8WC42P71Qe{M-Se4u2)Bc;mG8+>Y46(g{6Y1 z5$T(vngH=BXpz^-k>R-Bw(1$ZpO40rgqrD7)t+_xMV|*CKa~-pNxK{#>0&I`4wF%J zG{_0LSR$|Fuuz>yv_Ng4To-2Bu`0zIS~?=tYn`CkOD3EWI6dffs1&Y9c1LW}=^}Vh z>TzW_d-OBhbrstL?mF53f9Ki<)-FDD_?1H!d4qR^WQHLIy-H2sTd;tibh1 z_ns=z3xbUk1S{l8JX#_3;qpW0hmUFv_g(VF9_C85UjxUeB+fKK3+RPutF5n z(F$1?J;>nN!zTz<;Ci$|dPXk@9y&p=0@w3>@A&Ywm!2S4;ce#mzJPi7+DlFltZ>x5 z%Hh%SQ?<$d|EX*8+Qo+tzk2x63tzeL;)5?8jQ78|@9ceHZ?OCKyS<&i+v#k7e!I2x zxh>oOS--XUH=Fg1&ur9uf9=!PKeeu|eG)Xk_0L0(fb^+4yUL7yzN7v_kAO6*0Kh6! z?+U~7@*(%3M?gB(6#<;=d1#des)Cw z=d1#drgl{T=c@vc&K3apx~)DRdIY4sT@k=Js{o|O1prnstn*#{KJ*AkqYD76viMzL zfwX+l^Ux!yl>wY<$j3vEpeqAdVS(mF0?8?WRaXD!tO8_Z0OzcN`&I^U&MJ7p6u>Im zpB0wKUTom|Rt9j+DtP|N0M1zj&zk~RW&d~1D)`=&0i3f6p1U%Db5=oO3Sfo7<$SlF z4?O~Iri0076#(a~0+1SEMF8ilg6I^$3QW&i1(B5joU;nTQvfUY_W7Pb?17zo$(LT+ zDeXWzJK#P4JGLL)Hn#8Ee#+M8w?2IF;}_q4@s$@}bTNMMp2N=^zW4AAhwVe+@Lmun z@UaVTzVQ7QN*ACDI|rXQc*nt`2gbpD2TuW!10UXhP5TQ1#Sc#ebDd!&1vN4nQ~q433{Zv;*l=kk?yiby6<`V2D^TVEgMXiX*)TkjAzSj4OzUwb-^3ifj6>!Z)AJk z$acMv?VLztspo##Bi)xg(*2W1x-Xv4(Os*MZ?GU#uGK}x#s1=5{GH5{E-z$8h+IdT zN=FtI*}(@3o_684Jkq__Bi(yE(!JXw-EVrNdzVMLcY37z4UcsH-Xq;RJktGNQ#y93 zehH6sxJSCQM>@C8TIUf25l|Dw7VOmjR*!UV@ksZJ9_ilfk?u_% z>3(5Kmv>v87Q>Bn4C~aXu~=BlV{vfuyvylMo_9Ij$@4C!J9*yabSKZdoX+d>{=7%I z{M-p$jY*k?kx3#YvO6B&g=2JE1YGYb*brwa#-L2uCK`THM%l<1B93;k zs)iM5l4*m8TfHcIneOG)7R+|WL6pgb2Sdm%&}Jxvj@1~|f+N`;%ScSF9`tyrY4Yfk z8-$z3`KkATid-w$)VX=znU#RbK}e~)4D=yX-gTV6A;sb?H1h0x#cprd<(>QJ9mMZVxejyAgLtdbgq=5 zqhLH{r?ZforS(>+ZU@s1x>b^cwNjap6A`VXlxn=Hn9*qYvS6Y4hFM@Dpb`w$k%CDo zX(vLhK8)_+`B~OgcTuZVV-)TSL1ga|l8CLavp7$0;S>XR^Vt6dr^HDQ#5k z%Z28!g+?#i5_+ZT1_!w`H0(4HkbGt`3=Eo+fdCX1oYe1?YkH+WNkbEi#2_dPC8m!K z-0qd;F*9+y*Qo2~8*uGy@fl2<<#sE)^gN%%?DsksEb+0{(j5C&{9wf1F@E!LtCe*O&YHb$^?- zSLEiLl~q@*`R|~>ovcotZc0>df>e2v$C(OXo^NN8`RR$P{|86l&@J=rs5yf&hMFoz zl$cP@a`jOkN~U6EGiK)D&|s`2svO5(M!Gmia06i8&Lr>VD12BeRl`>hV~|T&mIUge zks-A$UuJ8Lj2c?su-j>})~MF1m-Eq3kqD9wPEZUeW?}FxkQbDSq%u*rOH@JiqnJy9 zGQFP4ZcZP4cK`p|H`;3#-hV+l`0T;8gKyaX+5PA5y=^bK`=`6+&gXYtzVo#0*IxY5 zi_bZH_aP6W0)B1lyZqGVr#AbW>l@yyC^7-DfosIt|nPF}<;Y4qkaNxvbt=GP$0X)xo*4ipJbe3QF znBkeuBYN+~M0@r%6?FN63u~*~$?@Ff&C{HIO#%H~0sXCT0mXBFk1d#c<~14glK}m! zaLdATKMRv{?OGLd2J*L_?|tzz))ByDE1;{1$*vH%@A2Pt+&v@3T$ASJPqh6#iq=<0I zZW`Hcwi}SyL_RqQL=(HUbd-#dF|Lw~rx7?f6g>BLI_lM~u`}JRP>kGjKi6+)fNSI@ z$^!$D?Gz13F1I;Q0ce zT5f^|h`2q>#@N7EhHVRu_HtM%iAaJWDHS6tQ+>(R^kgueb*xB{_KfGmHSB8)=wo6w zoe~mNMj@MzB$`4Hp~LCE(g{Z(k}X&3MYq|&K|Kkf6m8(hB{_)>3njB(7O;FHj}ybi z$^bKiz1eKq*J#k+WIaG}#Bf+0)>A~<3EE1m4HCT%G4lx2;Q!EOc*()xf3=feqHPKbm`vvC`VBvo4*VxIfCX@1Ut&L%@z zyM@u+K5Nvo6$c)RMJvhCwNTU?Ci^*q4ccZR;h_zr=cK>Ym zmk)pZuyOc;!*9IsUoO1k!Xp; zZ2Z;6|LOk^yTa}>cfP#y`#V3o|IK^<^`g7e-@$jjb^E_>zjyD^J$3y_YoFdu_`kaU zkNY3p|KIo9`{?H9H$SlXW1HsY^EdDD|D4~s_|)k>>ZD2mK>qCcwrbh`pY3PYeZEU_ zzsZ3qwr=%hQ7~vRlUgv}&HbJI9k0Y2``i25bK){6Lo209S*}qaO_nLr3!L}1;ceJE zad126m3Vg>+J?LnU)jFmo%lK1&+$sUvmMwDcqQK6e)jgWy%KM2U*5j#mDs=iJ=@>o zm3VXeS=-O@O1!cC%XzBY_S3;Xe|B|quIGA=W3>kz+>vPDNm+-toQ0_n5Yq5pVl`*!RO;VQl$c?R!k!%Q*XeukyXhE9Xt$BfdxG#LGnI-{0Ne z-JMe}(~9r#{=@s{_PmVv@P&sjoHK4>U9w-w^h6TFdR_OX>1XS(a9Hq4>_6lVIj_W< zhwLHim3ZSYf0*}5>^saI=H|p!lVYe*6*U^vFfAEYc5y)3{^#v~_Da0D{gv&ncqQK0 z{_^&hy%PJjzqI|OxdNAA-u|cUf0{F1rtFQw2M!;Y6EB-k_P3+kQSZc&?TB~c@OIcM z@!oc5JG3^ZUgjyj{jL42x#*S=@B8=tbH>X=w|V&B;e&JHr8?REpWFY_EAiI$SGT`9 zCtfzC__PxpXra5DobJ()RAdUKX z$PsOGvC!W||6ln3!YlEi|9AYqHjVNZ+Ru&@W0pp zUa!PH|9kxJnOo>GtMk9x|L!^CWwZRg?{&V{c_rTSz1H_yuwV;#UbM|QFB^~ihn2(1 z+WY~R&D@8~A>)gV zyZ^sV7L0-TnV{!nnKtzur&o z?*Ffs#@+q@b&hfy`~UTCU5l9Xf4u*^egEG3_Fl2~%-#RC`)j+M-RRC=?OX$I@9)|Ez3ta+hqk`3^`5PlZM|S? z)BjHY{r;zJerEIM!F&5}*?7msi#JlfZ}9PJ?_7ImfW-BI3;S<-&<|o>yf=QlIF?uo|tta3r$#;9RvHS)N+O-izj{ z_1N@g)$Z|g9eHGNqNBYBW;&7vRXJBh8r=b}nPUNUZefqt4^SJ%Oh;ZDKc0?YXi-NS zH3H};=)1Hc`_N*2`;Ep-GhP{;YG$K8(~Q@qH*03YTv5c=2Xk*&Gqrl{|3s}D)p?(N zvBa8%d`;{tqpmCDkn7Ag9*3YaTyN{g=4$oW`0>;liZ5OP*!t1AHaxOF)yCEv=GySs z^k!{r{rFs<9$B0Sbn8dv0&T0dOf$4=4*NB!-)+>F3v~BgbAftn{CEP5h7t=s?!I$2 zmPxCze{3uZ#;)Ty9m{s#GuM$vmZv(}{mr?KJT|>qN4xKy>&PRE6CLgT##~1>H^>z0 zqjX#DV0wzB%GW*LEsTEmOLHB0Z2Y)7iY(rb*!`!uKs~a*PM}|&3)ExNGX(mTxj;R# zmfr5zC0JGM;0dn-M@b>P!{2pnnGnjibB~2w?WY5=DqbF z!hZI_R8`{{m~jzIO2!FLH-pKK!{u`ob43{LBU7;BOCp;sD+M?Ea7J zf8XAxL3Dm%_mjJi?1p##Gsp;VW&7jXSGT`s>kqbGvi05m-}gKIdpAF_+1>oMjSp#1hyp~m(X}8qkj~|}0{qiMYOtms4 z+ZEH!ShbUaZY-j0hxE`$bz)H{Pg{wsR3UPailjDs;aWw^39#C%mCJ6)M#&6+{Lqx` zmplj|%q0S4u{ap!!_5jF>+uAJXC#=1Q&Q0Cn^31|wFjMCsuFQ=1QW~z)Ra9;*QF|nJ4P{1 zR0tdj1VXGnik4$aD+;mvu{&k^1rI{#_J}YSBWr25$|)&32d>_wu>_{HT1q^W%%D9+ ztilx|Y{_x{csynM4<3XNh3IOqLS*z_MUIMUQf06#+vkdPE@HZMt}ApHqTPXlb%N%P zM^mZSg6&z zodgl)kGoU0pYtFDu3)9*9_b*Bj9%--J4Jh_TaH@ct8TXfoBfsuSLAp{AmR>x+?lfd z?;cb}^qUeI30B8rsG`|1GK};y19(`cDaYl8p&(u#_c%k2sRZz{_LS{sFE#r*GQvhl zJX@9VOnZQi76-<7Bw!o1p~h-lI8c*enXgD37AHqWDKA7?mR4_ZiAaj9Ltsp3P1*i8 z4{V`&x*5aACMs~u2sgCYI2aF%%dBDxRb8yr+SOr)$}=u!0k+L4+rRO^Ru4f?#K?4< zDpaXbQKc(6sammCjS`U6l|u!~wkQQ+jCdcogFR*YUp=txFvBig=^UwdGyYO&hTPDk;km;|B7a!#ekOwl1NcbH`&8k{C! zPHW`g^n`7F%J#o_U|W=G4DODd3J$fZy%OJQw4t1su33FMZ@aZj0GEcHDA^|l7JqC^ z*?!6c+fj{+$l18Wu<=@qOz6Y0qP0ogP3wec@EMaV@R*y!+aU`$Z*9unP5)^G_xUhxVc?YPhGG+Vc9@qvuNw+^P#ASP& zM;gt+*s4K^j2sgwVH{z(UJ;#aHM@AW4OTS|PTBsM2ey4_jCL#C(NHq&c$NylS(!)h zdU6Pd9k8A@d!)t|oSK5Mz!Dyqvi(yJY*#25&L7`DW&58zuw5Yw5r6!`Dcg^GV7o%n zA^x~LW&0-{*sf5nm_L@LZ2#B;+Z9p?@yDep+duNac7=>U{Bd#0_79g|oL?mg5PvLA z+5UkCwks6D<&T9a+yCf+?FwlZCj0-Vto_c~Me*<>htIq469->7p!R=e?;rQ@-G96D z)g6BOE!*F*^{D@^{i)42ZhT>b@%=Q|*|$K0JMZV}y~p*7YwP+m*tNCYoprzefgRt` z3QMNh%bt@{rRB>e-@5(kcmCRy>F1B%D&yMXTf3{@d2C!<+nr(DonSm#Vfi(KcydCw z9O4AquOQRUH+e?5dtAFgidS~Fj#gO8-6+MStHiAdx;I6qpTBV5P2zg?adiUb1*a$pfo^d|?SexJso#Na$T4AX=!+HJd?&U}~CK!L}*C(Gpec3b8dymx{ z%yHt>E3AH3G{>n^|HQo$j6eA#<b+SXp%H6;{$Sh%={N>M;{+Kl-BS=O24U`1E6W zLErmFD=eR95U+nvvwSYzpJ06D=O&-8d)cj!-uV6-PF&qRe$fs3zOr+$%KqU-eRoD3 zMk2L-ZJ1*;wrg>!|23A$H(dh+?2&HHY0=iTGV0+{`y71r@9f|&~J6@M}n+m)Na zJpK5=1s(1kt+4vPEgkMnz+8Lty-`SHo;OUt*yc#}t5I)32<g`o> zwazfkGOsOn+wBRw@uQPZ_XlnT@+Or(`?x#-dEG?4wMur^8OSpx>ZOXGt{?_~IKk_@ z*)!gwV`+l-x{2DqN_N;8-s>jnr8uVxir#Z4IQyDsoXibomA|=4cG(%uh1_(@ zjbOT}Xg@pw+xb4vV4r?moPfPyRk6bMaRzoZzn`uu?B`FAHZjjg?>!bLNN?CGtgs`U zA)Rd%ma2ZbsxYV0v>?wIzw=mFTve>FyPa#ktAmXE%N0J6U-fS%pEd7JzkAHzAjKPoqj5r;fujqrr&Gyog`KY@qLIbk}1)OWivRRf(rH!t=%Zar8T~$qvwOaG{IQ; z-urGGCc)Bs_qebCX8UM`{q>4qrlVweI+VY#cr%!%A9D+yu(e7hxfz%n)>}*EGaU%U zU!BOO_~Khx*8n2&kBJhR6G>J(u zjXoH+>`JSv=^&o22FilO;OJR}X;v5!&#qdyDuabWGbdBPVy&j7Gs2`nPj7;!1CRE! zTRH3EDr?e8ZGo4i%}Lo5PzOf06;LR~sJACvCX0~?g;wvi8}fKo9p-7bNcA0=ke-xu zsSLUez@Ry3G$w^i=CxNQWeAtlURecrt6Q%DfB@)?)c__qPQI&vsy9PWw&#YjNFg|p z2m+Eb3nCV;jQYx$U_w0Byo`hVz0p33H^N3FC+71!dL=|$>5(K2MTfUvh;7&nhfudl zCQVEu$;)BeDd~`rX&4zR4p~sH%Xh*V9o3;M6b`!J9aKN!5>DPwx`fus50tSHE80wp z7^|HjTp&`2PLETbDnd;k9n@$!Hjep;#Hm!m%kKw@tJc<_-507+A3AoCdyv9((VemqtBkR zR#?`aRqONYIcw?qa`LR#%~^{J@Tq$(ot&(Cb5j5AY15_cq{+f;1sqgwgIN)L^SF~! z91tvV#G6*-6Q;L03xP=}dds?mVqtKlWwk>Ey$!|LWTq_`VR5$_(jDeb=F^3SFsg$>JptDEs>R?r&f^fuXjkk#oSJ_^H z3Wyu8xaXLg`1Y&V^;MeQu5X9_Rr>Y{?{5FK`F3=vSI54_UL8-sx14Xe-ne(UiRhUu zr)15D(#c1ArEC{b$R-^z232ufjWSpY9pu8d?bTfd6E!8Gj>_FSoDD_r%Z)UcOF0cD zQ5_p|d~M8&rJzP4T7=4{L13=i$ZAb;(2PVt(TG^y35jv{O1+o3Y(%PLsf8M4kM;j1 zk3Osaw+CM5A6`57{CwR z#p@q*3K>Z-h!BF)kX%d*GWhiOo{T44E!7I=Qiy|^h(q&CyNrti4R6?TQ59s+ta8B}3$T$+5Jf@*9E8|Ca<@h_Wlk*#S&!zmX4 zA3SYJu;{AJq>l{1IO!uYRBWa(;l&k3DOq5`1-zW$c-AZmqpX5V@dRghg5?qeO=vYD z8!S(;W^EO2G!;7yX{9XS!}S8ys-W8_U#vGRG@ZgFP?eKb%tBpA#siEU;J{`yqauB@ zc5>>Z>BG9J2jSRrAn-4k*Sn2-FW<}P{pUUx>5ZG}bDvA_eEtgboNySNIKAt#`~&w~ zee%u&*VIcB@t?ivj4fW8d4iPUO7M)`Pzta5r^Wj5S?oG z6qqp-+!(4MHkwP-9Qs7i@Kn$Q7g2GcmMcS^ke39WNMdBlvvl7Czif2%8-R2lm`=?9t4LQ1 zff_$Bh=^`-V>;B}6ui+pSwDzVO_W0-m?GH8bOEVNv2m8blf$2m87Dy%dF5MZ)aA1=g;2{Mw@$Ko$uRO zyKgXja#(Po2!r>|7;TVDDvHXkjx+-8NE{B+!(OX^)M`U{OywamYa|)9#IbTCQg>Qt znM)?=Qj#xw_J`O17Vw9QkNLybNcguHZPfD`Z3r%u?u)5xA!~;dU`?DWmN0u&e;6i8 z5lR6S>+Ec!j8~(f7FCXz?R>q4b`paj+>=`hVhbIk;M|@+ykZRe;c%HhEWUQRqx|8# zRz#QtOhR%QiyM=65X`;V0UJD7(cZX9O=gg$Gm0U2#7tsN!$#Y2F;a~TRmVbmIuvdx zS~uSgX3EJ@ttF+9R6$@eCCsxwRJVaYTzJeM7LOI5AmQJ_AEfj02fEyBu&k&?sX_~; zgIcH506Ffa`%CZh1FMR`nhvy-bt$X}$_Vb&c-F(9)wDIQ~{f&El@9^c*wqV&-cF36VFr0+!FiZtih%^PGquFYKr|e|ISlC?A6*S_Qm?P zi`d`#EKfZ3tJP(4U-WQ}=lL{RU{lmYM#k@X>_=bL&*CtiEKoTV%dlK(+RyKJ>W96m zoyusj*y*B-esISVPyMR84Ev%bu>vaQ&_Yp~+~#;U`yNmIuve8+?2G4Ni`cbSdg7^H zm6u^(w4{8R$aDE*LCj9DNALC24}0}Rr`Q*djTW&p@Abq}zgk&_eNn@?Ql82c3p`3s zuz%-EJ@vz0eef~t!Nud1MeMQ{@)urF$uV8wizEO5Ex1pDiK zp88?0il^8YN8Ls2Z~d$%p88c`8TLgD=lOC52XarRC)g|Zc?`GrF>o{sjKe5GekIH5-loeSv zH|gh5Z)1mkm0Z@(;uytIESKRhj^ZZ$yymMO<(6I6&*FGO#oMVKdo^{+bJ6=2 zd0v)1;nc69%jB`RW;%UC2;>1?{(0(W4{WUu)_-cv_kSP$9=QEq+Yj%p@BIGuPi`6h z$fj@or`CUZ^}Y8kTdap}`PJPoZkE8yf}7V=-xG8Ic02FLBg}cd`%7WtoE?;N#eR|( zA~s|8DW(}d>oh*gbVBiw)I;PdM&nsi4AjPH#K1BfOBcpjbxdFtI9gZj5Yk%v{X0%% zVfwsYU=WQ6(`3fkW;QC>Em(rokN^^(0rMCy9N4ls&I|{t(1=e$g6bBShE9;2+z1AG zaVM@N1L;)1kZVMHK_IgC-*F;~oS(?(Q2}PyGA#BfJUdj;a$T{`I^#w{JQJgug~)hv zc_5Z9IHpre;MJO@=b2!v(v27;Jxw$Pm;)mFy*p53<~hA4OO+`;WOP8vs7PK&3^zdGJ#F8<{;pHOLh`XF{R_Wc zclf53D>tPs4I(LSo-xsi63rw;M@LU?kaUR5PwHV#m-KR)!c(VCXh$1|96Q zw2VIJG{<(h8p8^m5lBTYyMzEWdVv@(+CXFq1mgsCYyy}I2#<4v#8_14S4`Ep~HDb>GEJK``{fYvhy0taH%V~ zT9}5ios!ELJi*!wah7AE{g4prwF1ecG13hv8)6f2C}$CFCYr(OX*Z?NET1YFd_4%P z>;t|F{}$~3Uj;Uwo4xjRm5 z!E>`Vg6&XzuqByom$cF}Q|$K?%R0l_5URu03sOQIN760TZT5?)s1WIzgRIsNg&|*W z)k@AVGvdkuu(me=vHi%)u54bD*PpP1xwCE*J?`ZdD{@}9jg&F%s#vzK66JQO$#!@l zC@HncZKH)7MH!{#s)6xJT7dXOtY-2hOAi?q7j8q5Y$4vs zg3>HvlyAr(kR1sK?OX0Np-G|h5}H&LJB*cv3s@@zPMktgh>=h3wJlg0#*IRwu@QwO zo7a+s3WzusY8cE zs{{^&h3Or&1)-stT6Dm~30BUzB-1o2X{MG)OH8X?=(P9*&ozr;8;9Yv0*W>S`i5;|W>;@@ zbA@i!p=4Wji)bwggf_g>gq8`Om(Yr(Rzfbudz9UFb66t8LX57TB{XcnsgZ^~?x49C zO@nDF04`rDRF%Y37*3J{B|FqB4K5d~f{bqwAhiC=0-M(!Sbw5s&)n*>=Xu3^yON;s zY^u$ZcD6v3wXTAHfqs?pN6DC3W%*(S(3*2-9H=4kmF>uqF;w|vsIlXhhvZ-8gWbj3! zz@p5cS|63mp|g%QQ!drY{(tPf2fX80c{V;*y1fmZrDfUOq3x_0S~)SzT@jVnzDCUdl^19 z|9>vOAA6rWe*L~WUFV$ld5F%?VyDO`?lx#aHQSg=>I^@{)ewRcSh(WgSvy%)2AiO* zS&zAV^|7(vR7p*m$F{Y+DuttmrBtl4mn;uF+ba*td{PyRXrWqdq6J0`SB_g`WDD6e zkuJKr>`OrD&ZfH2vSH+WYT}P4Y6&Q#q>&y=30>Zxl^g_`*6xZ4{(tk8@F#X3v~z0f zTbpm%fB)XoSImt^t*6&+3BBY5=(ZRA{O|kyMFp}L+)!?AZ(OW*$xd4nIj4Y?u_j_B z0l7gkUrnk7=D01YSd^!8s^GIF(lq`7j`W@kf=5g=$ z@exlMwYu4j+SZtBl#yg9qPKeyw_>UwQQ`V+)Ha_vNkcII+d7uI-z$nf=kBx{Z305 z*kcK_wugaYyZMc6lS|gx7^YV;B)fy*e!Utu^F1JGRz2>-A{OsdrJ^!vP?LH!5hAPY zhEa~^ShIuJX0pW(vP`m$mxk3T55~6do;a~_v)j95{Y!Sui}vk$I-ixWWVu#TsiNtX^3K5NN=zjWHscT_qw5w?qg*wc9a13sbau__^Psg|d2{Qe zEO;xwL<{0X#;l>?YKI?Sxq7tQG{T%jAD62h@1_PGxZjnE7J1T$H#6m&7&V#UuscZ@ z1vSCV^3{77<;u1mekU6Dw{8`VbHsq;B-5y#JF8|6u@e%t{=9HSX^ zs`8@jc^2aPE*%>Wimgf`R_S+D+adZ&1}(;-eFp(!8#vBdz2fFpY|Ql19WcbR6}>FT zjS^Ny;-Lmst`v^nDNi<9?QmF*dzENpK))D67^J;FQFws_XNJv;1lemRy}SejgeVAoSJe|s;D!uO3Z9(9;=D< zj@}eQnWz)#q{SYe8P^69IAFLAObIu?v5mN7>{!gW%J(`0-WcY2KI0d#%rt-OTSCIr zi6%YncU*2tV}utaWeGz@SYc#0N|7Q_jKwIeJn^Ra7#Q0^U`qJ(&8@Yu*d@D|!+?c- zuhy%kJg;kb5y_03+_B5uSSv@j#u+DF=gL|=QZzzrC(0vjLak4P(V*#DW4;v;#$Lfz z1}k^n123*H^zjqB583z{_|O0T{BH^Tf1?DRxO?#68EN{{@3FFs_SB6lr7T98jvdQB zyM!3dj4O2+w)ex&FCk%bJhSu?`n<=n&z>*cGYjaqK>O|h5dpi^k-MT}&QuZ*5C)*S z1iulpU$wrgjpd`ikK~Rh%|3MaPXU!Z5LUSNEb@EL8K}?x`_UhdbQKOwj{bDPW8del zVPxj%=-;j{X$8LTf=^r|&UEN*MiV`c=b}eBBws%7Z--$Rnq-`f^6>cqxOQ<>iL>$C z08t8gVZtIBokw^+E&fch<3`xHXU*Xzl5b-acEhyz)p81KiMMrAvpR7kR3KL0Cev)jm z8cB{!FX>LH++fIzy17vzKklYnEfc}Y89a^gm0naY*m$}?#{eZZC=;hYqXU;`r0CaP zDJt?s>K8|kehm5cGGhrK`hCLi;6a;m>CtDNdY6@DB<>a&5C_iK)DERgx}s^z;HhRj z@vT9>)s|J!?6wA7@OT&Ket=dq``GnmUNH1H+U{i@7JTp01LqQw+=t`d`6?{e777?6ou zU9Th6^HsD`RgAGYaSYw#5IrhHMnkUB%_w2jI@5I~j9-p2u8ShExvI_idOC-r^R-vX z9h4vd_l(%8`D|+T;6>t0f2xi^R@SJ|v@x$P`!Z8FU2#rZF;WO;)U=+;Wy$C`C3&@` zEBBgwDL*b`8Z^O?0-ed@d4xG#E!)XP0=04j*~QDT8yLfYKZ=|Y{$GC3oUOs0oLgC@ zxhyi|j0c9?Z)vk(GfdrpViXqsZ44#9_8P3U78TPl+pKm()ufv_ za@=d$H<|xGZgy%ytXmaPt8G`)H7+}HqE;a}Avhk;v3orh0qI^VcJ^B&<|LTdKo!EK$#!put1Ah7c z&Clak4}`

    qsnZ=c$)ZIc5RgzvrL>t4gY+5hMawR&qz34~#|tJVNmj#ERE_h003amDg^gAa@Rg zb2QdrtzF0R%RU34c0fCTCEY45L{mx+-8EKg1-b499!b^Nfue>UO!$geS&%c+#>IAA zIZGJ@d^nc4ZBShZ1JG=dPSy^ARu`@I=w7~a<<>Ion3>e;7KGlk+Ph)0o3oCKrLH-f zwfln+l}W=3S}7Wp7IH8VZ=unv=Zk z@YD{w@jK6TP=J$qw_0qe^=>vJhVy2WkJ?Ky5@bNdh`ZoI14!BsUch&Y9{U) zZe*DEsZ5kXCsgo=EE1x)AY7xp9&N++idA9@;JhD9s>jYOl)Kwem6F;@iO3!6YES`UXjU=c9XEFQq@}?- z6|)X1s)byDMq5bQ8{vKn5Rr<2u({kxQ+?>M0eOAZRd7dX$Jk)RJXJ# z8p<6@r^lfCseeJJNu40^Utj3xc_wJPy<&4`mYQS7!plw2oz^Nku;)`E>MEtJVW5AQ zbN2J+63hv`#L|Pr34Oi}sF)KZ4s2!237x^d;(!XqjaC?V2~z@c=Q5A<<(#InLe;P> z8Iao$ENI zGuVMwl0KoGFLcmxLOb6Bi7H(j-_AF?&~bb_-@kZ}IH8^IKfjPTzMU_Akk}sIocF*b zO2@EZZ`S>2g>7sYb{dpSL_kH{A!b7pTA6#MdR{{nc~xDv4>}?!L)d(akeamW;VXRT zz&yQm=X@G@*er zz!_4tEF>UM2$0iae9IFQ-oX6E*x-s)hzpj#KK1QZ?Tqz^jbDbT97fdR-ZPwMQ|$WLLE`xGx9bOL*AqNP*Z;>09Wy{hxT@qH z+ktV=xLD?9iB$Ic%OSqiU<1Oy#Q@#d>(1~ldyr5z>j}6N#X`kzc1bqK+>|EuWVV`O zV@8J4h1^O&cibXE?)|`FawkQTrke#8ws}rif}>Mkf;PD=XIO$1m0Y$rAP-OWcUv-6 z4=Tnr#dqZ~UC%*hjVTD*6{YOq4wp>2OC#*sn-`G}okcxt$_qQ5WNZZV z3}4OHS%0#e>{5H&t$CVG+q_>}!y9)F5~D1pGMI_RbIcO~?=2u<&KDjMVC-r&uYHe; zMqJebjU3opKY5T4lxDSok|9c?!2om~+ZKG&C!FbSH<&CKcdoS-mfQ&ph&`JhBq-2C zvgiYnM+B1R4x?%O66>HO;Ma`MFkGmC;pEe9GGAghsS62o>x1ToCOj9qutYciF`@aG%@KsX0*f>ZtN=0JVt_wXkJI3f4x&F;DH{2uOP z04KQbjsd`)131C|e~J_Bm^Hwh4B&)h*JA+CCj)rPRzRHtI3XJHwyj`zGJv;i1?0&9 z-m(=C=KxMP;or6u;3ork%T|D$4B#zW!S6T+a6+)`ZCk-7P6qInt>Cwx4B#zW!EZYU za6bfK;DoU1+qQzi$pGH674%OA@RqHhcMjl$c=Ow~ z0`z16Z`lgECj)rPR)CxXI3WiAwymIZGJv;i1@OrL-m(?6&jFl}m+-c&06H1KTeX7o z`v0?Ey!J4<`wO>z5BTNFKmSh5z-Mnhe{g5d$NJT0k59M>z4<}&xMK4D72LxgeSE_9 zdS2dnf+f72a?le_%c}~y-k-y^hxJ9Ecenxw;Cjeh1C(CBM(TjRC~NyQ;Q;S-`u)9z zE6c2*#OYdj(9Rb9@ve+2zw9!kyj4wT%Xpcko56gemtFV!+d`TI|( z@VuG?;)E;HRd)C?-}18keSwNAKL-HRCVw<3R)J@}W_I0QTeE`)?GNNq>33abScWz2 zv+FJL)NrtHM#YvrTcE0&jU{77tUxG*=D82gZqP+R3qWX&N5c>f+LipmF-3L}1FP<>&lMRwRp&O90gDluE z)2(S(>3F*abvjTyV&R>W^+4OQ4}IF<@p_5`{aHClkZyYtYl|-7sveK#7)p6Hod{2R z@*px5w9^bhlrva&-AO;r{Ixl4qH(o??1cBmx`<+lImlB`8#g+xyGsw4-M-rRv+q8? zdw%PDa&`)b+D&MuDpNt zI@xr~wBuoAip`WrcZP_&wa3S#5g*Inp1q5SsE&poeI_8QN=_Cm)A zIZtoCGOS@B+{YoPtM>{d7Ravj=-T;9Ebzi+(ho7kkb93mqqT2#@{}4my%&_kfO%fBf3p zZ7cU;ueqwCKY4cdLM5~|eZU`I&@<8JMhLZ=^&`3+YBuO0F3iNZZE*gur)IvG%n#91a4f=i0bkg-1MEgVBqHmyDyz~0na z@Xn+8tl6mch)Ek_v#ym@$X_C0`FtwFXSadM{RVqN7rHmIlwYWP{{|aAyLF+j6NZyQ zXB*d`C243IXj9SD==#Yu66l8ahYM>Q(w~Ug%R;)0?!CbQ3`MQg3xp zZN&8zV#zWH>u5XrNzx=?vS%HPp8 zex=Ir*;ii7>Io@sN00LTvkGqdclWn_``HbkcYixgI(BDgRy`CNiT;4omyFz9>SmN7 z&T=NJuo*dvMWclT&mO&6U*1mbXTRY>&s&YwTv)}cdVc+dp110rexII0{C|7x;qLC= zyG5LBzt~Ss|JLjOtn~c7y$kevfeZAl-dv9h`1P;kRX*W5b{@bx;mUCS{u8b|R|ot~ z*uSodJiLG3xaExLJmZVj-niF=kbR$oE5jQVBO^RC zGCP2zPM%@&Cr=B(V*8^iAf6PXbuSZ0s~&CBPW|D-8xyIUcgKUC$qG>SDIH5eXM~9Y zWKPgUMXmF}JemNS3#XIPa}4jG<=WH{(Bb0gdW_dA`N>?<&|Vr$gC18U(K>lDFF`ru zQRy}Nq3><;E1$n}ZkvO%$0wX2Z@vQ`X`Ap>0mm=-)Aodfz!Plq3v|T3W}BZk(D+3) z%nxiYf^Si*TCHolA8KQpuBy$N?I%sVP`g^-M3zLx=}e831@__d-cZTy^k%ElA*XnM zwc??AZFum+Vv0du=jN)*ILv$_t7fbP>Xz^~5Hm z=^;jSR3%YHy#Pv{3~9WsK=w}@Q?;{!C8)z=I@axpc|KbJs&+zDD^3bnftku_%yO%h ziO+WGT=zD;vcd7=nu>w^(--{zH?IGSYY#tt_vgTWzWnp$8Thw<2EMy_{_F4Bij!)2UWcIrhA z_(Km^=BGbu6uG;6wVRBwkYl$^!$rl8^-g!fa+%bX;-CdC$=P7I@SQDM#Q`_##OIG_ z@z!3W3NGA?a#Pk_jDiB>5$WkVLX%OR^tn>`8d*RNku!hTB!wvT`7Z@1k!Z3?rcmgf;-cZxjxNQ0duxgY4CTnZnI})(n5m| zCZar=*1H+5A64d5WmMJLVBxZ~lizVVOFp_sj)zi43QH7mS_^DVX13n~=N*6_FPn#; z3{*g2D1(43_mTh5XsL>o=nm;u$dc2ynNEe(bEMXfAX0LO6jmn$tt@f5uy>@M`%ig} zPt51Gyd}(uJphepS%tIMB;b|ydL%W~r*^AnM@NG2$t)1G%(CK97#rnCw&M)+Y2e#2 zq2Y?=mX@|HB%6f`Z_!(FNSBwNa0#D!w04Kd_6t)J7s4L64Xcf{NA+$T>1Ob)8P3H6$_ev^;5NbZ@VjwIM5M;2*IDH(Xd)^dK79X0wOylYu^6(=Y& z!%l?Ma_uT(kILB$G4Ek$+T+h+y8_=^E->PD+d4AnGv`cS{H{li_lReY{+AGswet(G z=_wX`_G?E+cPEjo(KnYIwJ8LG3xFjOX~8HA`a%lMpVDKOABvYp7tl7(`s^u<`t%n2 zWsk*MkC!~N^|(+Um^IG?{$bJ@Z*Y2Y+SViC2nBuN_^9^xbR-Z@Gmp5_>1gtnJ9q3* z?~gqD3(R5v&T`6~>K$+L1>I3R4;-IZ?jMt59@Av7Tz0I}r;T{ne2!^jdHlSbHcma8 z)6&z?qMOGNnNp-jHO}p8d`D!`onik`*I4wJf>P005TO)${zm}3?n_3xuXVX$9u?}1 zb~M!H1!NrZT&>q@EF^D~nU|}$f+NDTJL~qtbfMSvX&$rscVx-QO;o%V2?Td1)F#rM zEOBS35!hO1bIYnP`{DN8&pPUdFWKINaN@#_08ZsIe&CM)Hn?F}zaRcRZTp|A9|rBV zZG8qh;gYue)J}Lu)hz;SvTL+GFvZ27Sa-WK(JhVY?Tps!I!coc-AvBYo6o-!_LZKJ zeRRmQgA(f-c<)i(tLgq|!FKQ1cE;^B$!5Nwvp67u5|GJ42g_=y7(wbZFR>F6x7%LV zCqiNX;B+JuFp&8b7F%!s4?1B6bi(ER|7*8?XzSMFH~;45;>KUxm|g$V>;APryf!%a z#6kD!$FH{ce{&hur=qEhDyO4 z;RyT;D|NK<2^ODtN{kU~Q0na>(#sK)KjqYsjKNJmPeG_sPb(7;30-g;5$u2~)U@4i zSLAgN2r+^UO1-U3EU0egD8DFG3;mwFnB`JkdDe`21Hvq2OF5V*Mis%YtI#?KYB7Qh zO1+&zQ>kLMDhiN21CexF`Erm6HO>ogC}eE9*+xdqpe+$KalQ@$K1Q%XskfsJokuV{ zqNA`+MM1MU6LqgWMZB7ytRR$SYB=%dfrT&7c?mYx(E%GH*r3$gEk>gxgwI2$)V0P^mjR9sNR!t`BUd7d z1kxX~wK~+%hxA%~p&BFDpwvw`N!QUqB}T9T>F6eOq382SKJgS2BiMk1bQ2oUbr8@o zf(^(?H(?fC2LTl$*nqEe6TZ=P5R_vC8?cyeLNU4yf>MlN1LD$6=ttKd05Q0nas*h1GqfX4_nDE0OR1flC7z+wa&lzOob z_?~#`9WjCpI6yDZuRDEw;tu2ROYZztQR@EKL+GJ z&5k$?{mF?v6H<8Mskg)kHn@Phy}_l*`ImK1JXMGhY;X#Ev0h|PJcY&xHh3p_u|B4M z5Apx|M-dJ;nR_>wAai*ktW- z!6tAw25`Y9a5e^T!6s-Q0ob6>i(Py@{Y*V%Oe}KmE>)0bH;NzIbB*7i@y&5r7Q}y=W6?8w0pt6R1Z3Ht5?IyZw6nY2^sO zCcCeh1J^vO7E!9uZHjgdiTq|uva5kRo*+S`hm>)z820e2jh?^zdi?3e#sCzoS+C@k z`KXFiGJw$17-FT8;;4L0CKNc+3ssv$rfGkGtg-!j{Aqb(0HRUV!bM>|npB$_1-FnA z-fLn02pgtFEnOnQFqoG{E!c$C(txxvfPPS=^@7{3Vnx*LrJLN0pPQka#um*;2az3g ztw;3Rc}rD$>zJT^1Ync>?*=}X=kI?Xe_Grazy?mD>i`fo2C#uw>pB2x8w1$D4R;*? z{1JdnP9QdTOnv@4w&ni+6+e9;qn{^>PjXl7#~u@2vHwL|w-Kn&9=V}5gc@OA9MaQBo0h;c ziD|nKLS$Rb^vSW-Y%~lXI>R#+&7C+Ng4WJ==sI0(4qizq4Cl=K1wS3Gl(cj~Jl%5; z%{|FJ$v#t!!`rIaER4FXv0GHU&d@eHsxYshX-RCVO0_+8U9#Bi>Un}N(X*qaymw_^ZEjB27kkk2UF;kut%0a5OC{vP2OndWn0j-;FJ>X~biwtN+ z^u1~{KqS&xy&N?2Q!T}IM#R9*^eU-Bo(n~W@!7te%UK;?L?G-ec&p*=*3EZlS0_GB z4t#3p&5NAVD{as((x4jDy=oM!8K8`nvsEh#cw^aNmDL?2Qflz5Fn3#3IuGe}lN-W| z@tM{@R*ZjEHCFQ-(p5ulH2HR|HrJb?Utz43lAfb`BcK}Pd)0tuJt6eF{!ql2b~yE; zJ|oq8PH#{QAgkI$Bz?{}p(B%nGduOFkN;WKSj~4h$0B3Mmz)V-oA>$-i=EaR&S+FH zs7C2tH5Mvb;&i-aWoJ>kXVyk0OR@7P7|O%2D@=wk$wU-c^hq^y2E2S>MThI=JKvq^ zY-{8Tpc>@8YBW?BLsGIOGz7R()?u?*GQc=>LoE>3@R*{~>z`f-j zxD8Hw-f`bs%(K9qT&DqW=ROxrz`@J?|L*QLY#r=e`E2l?7k^&Vz>6AqP&9BixciFb z3%U12+ZTHQv+`1PNjG*%7=ps7B#j$f#N2z=emsuBrUjh-lovog{*rVq2SaP-O!^oz zt}rrQ6jqk;N3Ye-eS^F@pDv}2ztTP#j$k>LE_19zE;wtJT4ufParN<72G7P3c=nC> zV?gA@lhd)V2ounsO=Y+5!|nqATg7#xpSd{ zVQelFzsP?+K_MFsH^K^ap2;X3mLyn`y|bWYx;(t&+iaCOtUD= zeY1o&TxC>Cm(fT_L(HO{y8{tnCym339!Va)*3;CZs)JW?Pq}wrzU+AKZEaudz2ABr z4?TsIaoG}9VoWUmw9+}^nCQ!f_8I6LN1p^s`$=bb6h|X-)O$d5lj-m|gHqhrc8~Xo zWt_AZ?in_kE@aP)0M$XUMKK(RjqDv0Ept?%GM`iiPLUYQ@e);*A%&|Jx^_VtJ%1|1 zykYkw)49`a(lwSX6BrcX3o%050-!!jkH>{+6zNL6F@Pg_rd0bKtByH*Sj*0fBTal1 z)|AjB9H=@fwu>W5>oe{mzh+xsRvol;5De#K&j!QUVCj7R0{DW0;hf5qxWP^AvK8Lm z;Oh7&{|0y6NBK9noIjcsk54N1%?SYT%9H{l%decAeU1eQIUcj%)W^(us57i6j-{t@ zyhidyb5WXr=lp8EJ}#tjp<~NLS1!uVqXPSB1;atm+_N3__?*9i=lH!v!--k&_;_}% zZ~0PL=Lb$%-O?RNoeYL^GRq4P49ByHYL^YsM{|KNolBZG&54v*Q+&Ma%@>?Wig&_F zrS7CF`Lb!ckYtpR={&CwChZU=7NZhcAEq0Kw_sEaaV!dBIBEF;94KI|U^wy=uAn|G zL0I0XM*S?O@M&`9PDx&I#1LUC9&W3(K}}gy(td{WnJIjyuUl+Mnh~TPIYM=UcDsI! z&!B3%S02_P-#Tmj0g-K%E6upwzCginP8uwZ-gIICwKVd|q`6$iwfw*XL5zUjE-Dz# zDLwXRy|^^ZMYPQ`R+*H|$;A4Yof9 z{Z){yhH##lW|!ZJSmo$jpzLh7SqB)%uv1mUxV%lwj0?dRvY&{j@0XDOM!e}A~2j~2Ot7f#u-XHCrITg$uwXj z{{;~EA6&D2@8L%u=IrTP-hxe{f%%rVP?A);1ARdzv)%t^-}h^O18nlT^K5eR_~-$% ziF83WDU)48aVuumO>s&|<@)$<4Bs!holOuLeou&TH7Yy@g`uC*PdgO`|dU$pnj6(FUlrGZfy44rbINfYOjai3c0Ct2N|_90IY>*&bt76%rHlu>ahw@^1oMT}S~uebY{+;bPgu*xuG5!}ZL z{Mt78svTgH&ppp3t2dVqlud{W>L;Y@nq99UQXFY38Nb>e=IFF`NX~$v@YFi&NlJH=QU}gyHgN`p^r$5maL^?{l<-ujJDfF{w0Hqh$ZR{) z?u^K>I5yfdY)J79ktVPrL$7U+B=y!t$wtls55P&T=+IlffYV0Tgn0h$fZO*5UQ zBG-qf*aTDH1>Mf)o1zIR*__B#>C9julOU3$>tKu;78DTqlt!5%;|wcWu||tkj>Xot z$vc;G{HxBh$?8q*17(wonor7J&GN`lKt_GBq^0$U&8fZ9)?IifHz_wsr(U0R8gnFt z)+v#y&uOcf=Q!D|4=}#YT26pPzB8-}veYcAWKCJyCXIgqZ1T`~Hd(!dexPh}QS(V@ zq$=vDWPy0C^#+zRWIU&-*eU%4FFVR;(XeEzJArg$UUP;mDLwSj1}RxmlkB8Wz<*%0 zkX|$&s#;wDgo?7TzD=la1vdGd^K7zu82KRCguSr OGng3uuG8t}?aNwAh>VfqxC z6s5(u59w{Vm>Nz?t&B5k_8^yPNv=9FD#KaDwan3^SI$C3FrTm(Mr-woq^;dgVEoS{=1Va}Jk zLmKR0mpjnx%;2E$!-;HsvNY;Yf%i`Jle@J zeG~BpM!y7EDg(;#eNGzFdMV``1oSU~d%K1I8>o|{Kb3gd}Jr(1NKz@AK7 zmObm3_S)*d(_HTVFKyl3y7o2uufFuP;2)onpMQGw?nBG_mVbV@eX&&B>)p2$P9??L z;CbBgP-&BkqNDs9ynwuSCVKo(b%R%|M+KaI3iNjXUA4QGq$-XS8UJLu^CDr;&?6bY~3GKZv(fo zBR;)LcR%MSE-;`5+MtP_@A|gpU~np*-U~e#Y@(ybo@wjH2ZK#Vla;~X6ho zTbj&;bMbU*Ws}3%P?@sQ%^2qxr z*uK~ZU~BfnQ@QgV_4(SeqCPYI?^ys#9*R+5cpdXZw_A3K7qHvqa!3Ju z;nYj)Mmo?83RmGr>s7DmBIX#0DprTIs*;@r54zgjHarj!B(ma5?C0ZqWOjzKTp)>h zBcoaEX?`k=0eeYqQG|kiHP1*3f z=fjn5G#btUQI7{ENN=*71wI24Spe9AVpg2B7oLDs2!fWY+9O%8nQQBTwY1W?9kch$zrlpAo6FiAWlFqRKwX!GKQqZM>1$uW(I#6!&Rnh! za-0;E@>0JI8?`#2LjK&JdC;^i%X*JQGg?7H0w%4=%UKn`d_JSi(4ihh0pA7=To#)R zsw=^RwyK%MnlBe1*y@BBlQD8SduL*l=dSDH9XhQyi)}O8@yYxkeW%Ms6xS*CdIguI zhIlax3&r(39hX%Hqs_t2`?gA3f4#N+gWF%d^Tyrx?25Y&@BIGG_ucrwwc54U9encO zKODUG+PiPQ?Lawr{nbCYL0tWTt-rqdl{a2`^|7mu?*HYEw*SE!v+YmayuSZ+`_^{q z*8BI7{jDn>y8by=-hZ9B^`R@>E9jNWmw)E+H{JNi<&G*Q?mtFsV zE`99Mw_WmR-TkFWjqcI#60+IL-g)$Yf4zx(Dl-5hQG*b>i;58RmDAl5pP*#5yS zWUY+PW`cL^o!eAhM@+HRChU|!3PzAG8@81}!DD$TwCG7R$Ab!6szL)5yZ-W6!a(pS zpobMln`TfFUwACO=xJ+d5f9Bd(#os`1Jq04HjHp=`&&{R-t)-MplR#FnG33at2~@WL zRT>wjGFPNT0`m~X%!9X8m66%XRC%j7&$AjPW_so{fK*N7aw8SV<|kKf#u6>JB=)ir zRU7(+jA3<){H$D>iQu($8AXPj;ixeqD^tEEqStQ65;>QKO$r~jJeRc{cf7##eBK}D z@igEtXcIov{lv^Un;19YtooLQ#+10>cA>?5SxR- zVZE9avW{VkJ`W2eycv=;L6TEg#tb5@8nooR7gVyCw*9}y-@vA`;iwvAnhMM^#>l42 zQKd=LnOT;!7ahmSv)PHk6iPk%K#U~{70C7ixMEx47?B%kGGc^)qbA~@Yw7_9kB||; zmvm`@Ua7?r`M$^I`~C5BfpSw+nun-4b|ZrsX@bOBDh}lqtW#RJRc-G(k0fY{z*41( z(JSb<Tqs6f>@DkM{YLPfez$YqTDEhjSE)i;6c_MkK9&jp-lUg zqEwkxi%o~Cs5BF~WxmYry)u>{SwYL;W*tW9Y18v0tL~IkSu#eY5mRljXibQ+sMia) z`N6M1yEdRxB*wj>*9y>7uc&r1@Vr(ZSF;1YQA6OV*+~1MZRERS%Fdx-{0X#I!Z|b3 zo&;#!AzBIP@NhJDxI2e zVUJQZEf-{Qwa`{Zes6D-NZ_u@BSjW&&xg%=IyY@8hE%btsj?Yq={z|8?n-GWR3h#2 zzlbFeNmVDUX@4=GL%r?Kv*~m#gM>YBaMJ9NDW4g!=~2p6E7<;rV+lCtt0p1PR(9%_ z^W`jDflMbOTA{7x5Ra(O!*s>q3iDxa=by!@N_ntm=fLeD+f-V)c{6HSBx>eULBmh%3$rvd8}N0aK#Tay$j-NZJeB~1CYTP}Q3SQDe!h~yrgVVJ zI&5{m5NBz)+=(WltLIHh*!e^vp=X3*y)eeBeI$hZ)-2!#MW?B{xdo+C6Ry(G0Z9a$ z%0aukvBV^7glaZTm8I!$(Vf^d-pzIfk{Yy`@(A1tWV;Avf|wJP;nugu65{{_De($K zODW27S2xr7j!(~~xW;8#c1_51eGr3&PNkvA{^!RMJ$w}Q21Y~AyJTl54y&}^q31Qb zS+%KIRj-M|Y;JKc5mdWLLRh{+7l55p+TG*BFrYFG$PU2{9Ix~gK z-xhzv>UEWo$mHai+O+zxG%Ho-0aU8wgK?`^j2Km~4#uQhkS5zd8%vl@KB%*~u*4ay zxl#){v|-iMu~o@s3)Mm~HvzmzDF})b=*mlCiS@tZ&1_D=3EK2Z0UJ`LA3bjS4 zlB?QfnauKTG>|81P~dS0-G13ohoYL5StASLSx1wq4{JfTZ-{WIqtE;`UWMG*e8$tCvik&UsaDXbfSVsY2O74QkUZs8E~{lvkg)2%FjYC$WTPGlm3J zu=HSN78t~tD|81+(_GkAtW=F*ya^-t_K?7O`|;RA0q(XjT^S2Ak@p+uV0rLo9{`;-Xz=7QaOGLa~X0h0oO)FQuV zaYddAvi8K%J9V{zn7MLKX%y{Qp~}tJT|SoJr$vFH5U|x%?S9X17ko<8aHbdus>W3o znPIP}fe<$wt?hqpB9ZU%W~c2nhBygLn`n(m&9q(zg|eYCC6pr95?pLxnX}A6JCn+@rBHLbzR2MFw#d1Zer|Z4_55*Gnj33#75fEg0Sg90nUmT>#uIs|eq;8h`q0SpM zy|219wEK&3v!b$@f+*)xO|xPJkX~8Xblw!|dB2U7FY0ce!9<9(>fKxwnMV~w-mfMVZ}5KCDJV^--RRVUY_rSt(?}N; z3cZS-3jyU+N}J^;5QW{$9Z7)Y4m+#De!b$1x{IJp!cCaad4(VlHE^uXLH(jz?^B~=6p#u=_y=qno1b7WWH{wZ<>SVRK*R2f# z21AP|sZGtU%4Hf{XI!ro>K=$Y==Dlt$g7$+;uQwvjdQvrN@RY)rqeUn3jtvb-}s$mLmyDE=`9yyBIClY&bFpSPq{XS=U76w69R6 zSFVlHUU7s~>hjHGsVfxCXf_=Og@xDj#;tMB)BK4$)#?k|;lU(~WkLt(&wMs>^Ecve zG(ih-^KvSzrTrFSR`MAh!P$P+;p&*5t8rC*I?urHNJVdcQ!G)O_04&qEqSIZt0}W6 zwyNFyA_(#thBI1UW+_B35nZ;bZ9}odY|z59ZnXz?7dB^LrEYdGQUWn6c4y^!Jz|-l zSZg$}ULLysZ(@m-pXamn?!ptQV--&;h+5&Y8LVlKhnX70z*ROQ3Mgkb=@=siT!`^!?ee_ap_q{tG+kv;A-ugHYIQH}8!*`)uTif;e8|Oy-y$g{d5*0meHk822~fzuOSw{u15YS04R-_gNTM zU00txy!!=9jBmQ9$19sK)Lp4-+l8E@11*i_co#2OYlB>EwYAQ_m&91?C|LK zuda>rO?Mw%;w;?5d1(`3z69sW5$^d)y|je6_`ve_y9R56eZ$?)UxH2F1G~EkhhGBr z_-Xa|NOzYQ!<)?xEa*Zv=7LUhiO;`%h7VxXB09R zq_Dmw%%8D*JTjdNajCyA`TC>ZpI95=!*?HE(ctVI<&T#b&kUJ-Ur@X!-+cFVON_?`roB!0B@>LN3{2@$x3_;wQCuuA}o&U&nuXiLt1jg>m({2l=8n zCKylY>*wM_uuV)#jddXx}8tn>hAo9o%i0n ze&ZwH27Z2nyz#Q_N3VbE`nO&GvYT()`q7&|v;Elh>dxzTlH;p!PE#-WO$)mw0~I|VzX#f{^cx0R zWm45Xgk~6yYFl{{YK?K3m$?GXF}h=+oKSVJo8J?o+Tg(X&56YNm&eK3{03Kv!O=IU zaP6zaD}NbF{QvMOF}V!h;9&5jw=Qpe@VR>2Ka{k%B`yZFbh_jDP)lU89XLO$sFjpo zrj2w1wsoUDX6*((Dq;5gQtazOPe%n6rP6-Sn^%iwd4i=z!yZ%3S9u9VQcQ0!DbAUC z+L~)y@ijO-39z!G!d$05aNT|>>*T5x(LkZ;Kq19B;6{OrK|`>z>~QPjF{;UM5k^u7 z9C4%^8_vxXcosQf((IrPu0Had6;6WXBUE=pAN;FW!mkFA(SliT zx0B0~F*7Zg?yN)25oVflT@3EJ2BKSm2x9$k6H0p2?)mZUZgI*!Gn~qE-+jU+D`fG4Giz->9=pvohw@$vnkY zQeKDDXA*c}CkmJTZ7k78-I~5&WS-Jy72x0^Fu80WMjef`u}ZOrF`nMzF}O3z?EcGG zqHC9DgNkETT}97<^KN+rPrR;_$zvvuTU1JgRTW2RyX)+|JCVr3y|5?W)aWn@T4Km2is<{E`HZo+6?>9nrzW1B4gv}P^ z?zk{SZFM%bMFYoh%;_`JN&z8wM2FjhYPQ{jY-@6GBbKo2`Ec5BDt^i7r{#7vMU-=6 zI44D@8?Qrw$FBWB-E6O77PnSkC*!?TgaGreb$4c%)!4;zQKz1h7Y$P zI$y~JR+S;k2p&!+z`f3i{4p~Q+i1GvG@HT|EBOZ6AGoj$o=hRUHkgQT#;#UIb)i-! zAwR2YxdD?K`(;2<+Ef#IU!*a!QDpM~g7}0P$ znWGx%Mb?_M7rBybqY~D4TU{Pc3$Qikm4-^e9N`H33@dfC-9(~Z?6~=v(6_h&=VbEe z0!?S_rqCWKyf}vza)FPOh(U98^x$fYN*t@zVog;SQi`<()4D`?$ONsF$JLfpMF)~> zWvI+(L2}yVKZ+%4VIWWEGuEmLsfF0)izT~J@iP!HfKx*jfuQn4Uzkm$KfDzC4A8Y| zg&xv_;t%a5b!%ax}#5 z4Dvl&F?+Z=?u|W_ax?SYWE8D5@uIHUTuScI{n8>`P@A@4%`7I(bxdJs5ot8wXEUgx z?Z-Z!bOi}A&4p~5H7Hd|hvOh*A}HpH_#|Bq$Z;d53 z*r8_PdTikT-;X6W@c;jnSYiYJ|M$ldFZ%!gL%DXk2%NFaH}L=eR4lQwR)`6pMXD_1Z7@#XKl{B@V#by>ZPUEbOIt-T-L`#<--YERh9>^*ep&o2Gi zrElE%$jgtY-r29BWy( zla_UuNFP`9?&l?dA6^Uas`uFLBMIQwodkTC0DkS+fFDi(e{PcH7~t0>fM1g&odEpW z1n{eq#OJi{ee5l}pPK-FRU)2f_%#XOhmt>^qTyF3fIlb6at!dR62Pxal1>1AC;|N0 zN#b(@UZpnK{hS2wD-!Vp@GFy2U%pPM_mOk#es&`MvUS8!=)`jeq_F#nMEs@CPyFSt z-`jc9n|5|zl87hoEKU+Y?Y=Cr$!8_GPO-^L6Pw&hvK+I?OXA^Y_hyo`GJNfRR${Xo zN%{$!-Ad|rJxP2{o8?z&Hg<0&HoNxBw~rjkcW)%69;{XB>TA2*>xuZ)=O=zGQTl!& ze!9{R5~W{Ba-E{|tBKMtCs~dueLqq9UXrw`^ec(dFD2-)@rl+)7`4=eN6; zD1GOdZ^uf%lvH~=`Sa=3-c72#mE=05+BJ+*27fF3Tm59gV;m)5YrT*z!rLN9n zJAanc_fHb>Q|kMvq`rTgWI0yfKTYcUM@iC&`u<6R><^zGvOiAh`v-}5Qr|yH{`~vN zpW|S6JAat``I9IA{0B+(K9T(Sl@2^RDExPlEXNf7yGhb--=FmHq%ZzflC&}e?fg!X{+nl{|8`=_-$>F= z*z&iM#9u!x@i!Age=JFS?v8wNPKBVI-$=wi`uxOyJ+b_+CE`gt`B?JjUp@Kfk0y5f zmE_N-j*2_KmgM^7=gjr1Nv>ad&RoBe82lqiu2Y)eFDF@k@$4+Ww0!@6ZOhrZ`M=)y z{A=HTAnpI|mB%l??9vBzzi9iXz@IPb=iSbemO$>J*lJ2#we9s52JqTd(<815g6e49Q<+gx785knJ)*$Ox)W#76&M6T7&vG$ z#rYiZ^q^m?AT`eLrTGx3ns`>#|Me_Yug(JvMJNJqwsNESK&kqoNNCEMuPv+$3o&DL zA-WPJ^f~G@vZmrRE$1i06pp*0?LtgiD4-DEqZV|ICOf4CVX%TksIJrj?l?fz#b;Ih zU(QnX>f~7CY69R8;i&S1r0NUfmZ7{XaEy%@;F}A)jX=B+GcA+oDcmx+z>D2Xsttt_ z4$0=C&UrRf5p@qvP0%@N;i(cat@B=P0%J>6@vw)fYxV_JYui z0##$rs`|&zQuXS_`W_MH1EuPV8mnIz_E4becRZ`=A3H6?9srtgE#TSH`6R7&F&#L-|&rMwj& z)jxEWs#mu#_b4GBC{#SV*nS8^0QlP7>JGdArTf=*zUj)p zzhYduarytfY+k;(_dR>o-mOdDyWPL_ty@2I=}otkTRS)Z%}w>@r5oP{ZvXeLf5-KA zUcYkf-(Gv{+WyX;Zw(K=^WaMku3oaQeiw)UaIpXHo;gO?mM+=C&h0`3jo7Gwm1gsz z9a7a9;V&Q*Qk&zDrTY!DjI@1{98`n3d?@w2ps@&TyJ)aeqogSH6PG98py#XNLeVVV zV=!zn7C@Bf3Ch^Hd=8f}i!)Pk9dkbE>4UDcTIl=g z0O|Ja?AR$1kXj~ysKcQl}kp z(rK?0`mzKlOsPz(p%1jSmqJ=<39oBWTUm55zccEB00b>L5Q%94yb3(*boowT&4LAA z<@z8#d%L_+=Jsy@-R7Rr@B06Zp=9e&RCoFo>=Sk|XwEW|K}gO>Pc8&PzHflw{EPAg zrE)cOerPMAHJ?UfY8Hge(#+*n4YB<-aiKY45>YP07n-z`#uvl!)MWBXWFeNj80GlM z+{7)n@0AE2J8bgvj$w>^xhVK`MkxB>O8s{Hq)-X*Q|8E!CUZ;2^L7?xLaiOPD7gX{ z?0R|)c3Rjxiy{8-a9*4c-3mSpWW(<_o$dr*E!2n$EzCMqao(^RM6VzgGF+Bw_giJy zg0dQ4cD+1o)+uX9vORQiNDQV{8>_X8ZO7t=B5Qf8g}$?Ni3W{3mubb!CY|Bn_2uNb$yTqZLh$5pmFA%nU+8BQs*CoT9+iqCdhCAV9x zw=I=yjt2&LD0y~;k1*2nM@|&^n1HQdyNz9X`w|+|>Q9`OeJnLbW#MhC-t^m@k`3rv zyBgC!mIXyYC(D1~r8Fo3J$}I-%W_UnXf$MQF^r0h+SKf^tFmgWZLB4iEF&7Vv1ThH z$YiGJb^1k^2|X$w6dERv46t??3me?fkeQQ!^ zrxg;WGJxv18R+^9Wks=^@zUwc1gqEUM25|{6TMep3FnY&>ou}m=?-K@L-}aJ1F^wMd^NVmoIej@r=#U*Dy%L&t#x|Kk3|to*Y-BZNMeGoq7?uLD9D!H! zxKW;v#BjCHuTBb0m&NiulBs{FHZ_}xS|7fjL_A{b~mJYvQdzm?JR|`)jG{cijC!UrGuv< z`cQ9z(8n5~GNMPGSnP2#VPIgR8f^xZ$RT>GI;|&TgqqPHmCu%3KLv_}*is*6=7(j^>!yhFwj~>UCE#3-$m~TMg3+>Vv6#IOE%P zb7o9O0o5Lk>u%}3w*Pf;p?EQ0ivm|*GnJ0BXcX$&fHj@8tV%4Gui5x`(asY2BHLP^ zhcz$_4LLEiNpjq&MvZ!7MWs9YNuf};?+gbV80)j$8e=VR%WU?WGWco8yg}(>YmS>WcBs=`q@saH7DQgHlLTkf?FjI8*mI1g=uD9mntb2m&3GV5FJ)=@N~0dLBL{ z6!P$hJnTy#AV5$2$?@(^j)vl+#j zfsh_Z-fYT?g&CN_q0Q7J+3C2m`dsH-y5|>HkZ%27Nuga7pHOp?03F#wrYsEMLdv1V z&?0SPfeS>L9Q!s^!{pwf!PmpZbV^K3Z{RC~>TtGF=(ds+YL9A4BW1JB7@`-o?xGYp zsy3^Y3WDB$`ZzlgwKVvQTNJtgxzPTPbGyKj-_!eE@i1y zUK0J1(5R&*a03e+I&5}}DBfgiGZj-R&|$??0W*8ssG8aYuQVxowa}eHJn|J3#+bQ_ z{t%F^g2&Jt3+HGZLAzlAkhP(JNx8zHt+~z_o_sj+%vq7*j4l{gSvDLX3q2cR>p$QG_P#HD-Uffp8dFOT$ksdB~~^HVy5TI~^NJtKki>>5uChci*+ zLM|#BL$FwL0bR;{t@PWHLiwCdq2;;m>Wwz0AuX`Kn~$|HgQR98Rn`TV2>{86HXX}{ zeRdL#blNkcUM=7UM0DR|^0A~)mJlsvh}#9M>!kb{*%qJ-I(C}rE>VNi7+x^BK_%y5 zHg-6&8CGPbDn6q-#p#smudbH2e<~@o7x8VkYJ2h%U9jjDc1{SFOp} zVb202L4_<)=|IE70bqd%$q+9yZM{s7g5v7-%2@jUzk288Tetr4&A+%gz43=Pyz3vo zZe96L*FJjfod>^gAYJ|N)$0BS_lYZS1t$TSy>H)p^Q8}5y1o02yRX^#-+a={qppAVdyf77|fLxXJrQ^twEGM7WQNWPiIkGE{uot zxYx?z?HS+;UIRn$yfCEN_yVyjom3X6I6wwLkR*>SeVDwsPCAuL5@ z+z!s>!$mp|4YP}yT{qBqE2@g*p-D~09V{wRBsRopqu8&mwuejiFf{7z;bb&ucV`(8 z4NIFA!&Sal`T0Sv?%MAs)k!^B5Jbri4}}oiWK#upk|qX)=o% z%n&uHgESOZfD@CfIKWamHc$vEKLIO#ekLkV&aISZu1}2f>3JG+Jg>{hhvu*x z@tC6xJiLjG7Xy8DxW4)??_rRMT#=q<1|V#jS7oFjSyPO>G(WLB7z=4 zIJbx9AVup1EU3wKquXRESOWwo9OMgRs#~w{M9!SLQr>L0IQEb(H>iqc)&syRSo2Dh z*Ilv2)*s%(Ah?qv)gNg!GONNlA&b;2EpyHw3O6U9rm3=ho<&rT_9@)y&4fdvH;+0t)trNq&!mF;@;_`Rg!@$>bHlCreX-X|+RlKV= z87ErU#fGXy(15@o(=%zjSBcEqTCMFAtBbshK&`0+NJG0LjS&`TrO+xF{HV#z`@~H5 z(|D0yW1Rejdl=|~S8@@znU(=bSx3;@njh724Wc3NDj`?LqAql%Mm;^6+}3SZR?>2V)GJ$cBan7NSmMj;fLq>0uu?l_51e)JA4sXE}D!olODbt?zYKY;o<0dl)zk z5I_+iJYiOM8Kek1#E2S*9Z6GyqR}_QVwS=quF)Ntw+97GEf3R^Xps)#N=^5};f#y` zTO=VMSU){iwT|GCu3*Bw|Bt;lfpXlc^2V#Hs`svLn6L&&QcWcxBu-1VWy=|s$d)Ww zmb}T5C5Pb1mMqDeWNWh>5(rSmmJG{~8OX3s0>d&)hJo-w*mnZMGAskbz78;KGX%oU z{GaSfx>Mar+5HuP@BFDg$EUig-o3wf-@Es1cUjFUc}eQ!U68)8IPUv=dn<*S6LgizUO>1UEs&I)cFIX1r7ea&x)~FH* zNGT@Oa>X{HSO%3VketHmA-U9Z25KVHo&m#ek7LN{{TKnG{hE}BLe-d)=0#H*6u_nY zFq{s{W*po=hBdyECDypKJdE<3kZX1dX{0e$E2N%CciR^2I6|>mcH=sDqRD1xH?!*7 zo>-R9WsYPhKigXBQ1v1mF+SCNvaTOmV%n#K02PTGZTMXZ~{Y8B-5vxi%+R1RE!97D#V zwWtU(G&ahXi4j5roSO2fJjQCI7LUaZkgjq-gn3BMtZ9{2qD;8Bfk90gROW-_7*%0K ztlrO8Dp626GFgk1V1hN8{;E{O%5|;IWV&Ft(=~8v>lMV_Uz~uU!btgelIRuM5ptk} z&6rX}icUBe>%m?IF+Db0u){8+7uT|k?8j>?+5D=a*(Wse#i4mdg)ITc3pvbdF=7Y>tA*CBMcbv<8K>qDHhjawJ5FqoGQU z8@OYnRB2F^etQih;f&)}wVVePJIS0u3yqN&ie~C%wi?L@LQ<@a>@ExQxq-CG4cMZE zv*{9%EwKdS*SGE;_MD)bq?XH6yA>&>6J<~jwjb?`*cuga0GLX*3kmUxZnero2I1E_ zN(}3(t!y(PhUk<|g<*P75)1=xL^w)ATY?ei+ik_c4RbX`HrfSB;iN{!Y=AP_Sbxi$ z-208=av4J%*h@ zFaGv8hO`y!lXAJ-;VFg2h8-&wZ-IQrq8(9iz2kOrJmb||8fju{bhlb*Cep;1%4im! zNtP^@?LxX_AyL$3Auh`okX|amMYQPlcioHJaSQ~7N5_SJ1!Zz=31p`&R^2XS*9f&Q zC9_tNmd= z2QEAv&C3_TbEjr+-A656cN{~?ACZIlxYvqgC4?{MJdk6y%#TVHxRa_N;0!|*3AJF; zu(XzNk$PzqN;ZuuIZmb$gGN79mPD}=0yW(AlrI$uls57*y)*`{eOtWEn!atAT(Oj) zRc*_hJa|Un|G&KJEek6T*#CFCL*Sb~fBioofem}(5qs5Q?K$qQc?z>^|6p_FQO9hn zy_%sMz2qD?5xeM|qc+%l$laPbP}G6Z)4w`_F@eh*lG_*FrYW9LsQTSR91I@h(WU}kKPrq6%eu9 zddFxSy$bw|qFRm)eijAaPdaTopb{MT!DT@&CjY6m)ltwYAZZQ^&FBUHyL&L&@!!>K zb>OLk+GJ#5Bo2X$?}M zk~ah3X|(F(hXKYZo-H7!cHlVh z3AVZ%7=p>SZKKh?^ZTg-)1Ch_dY&<{CldkOX>B5a(I3#-zz7rv98PI%jHqTixI*+Y zY-Sh%Z#HqWNmDoxFQ65&l>j%6#jvc`9T?$H+uC?hiXsYpK2jYrKHX2|aRgG1`3TQv zh_R7S`bH_5PimwCrTDJsXc5Y4*%oi(eN>KB`2p2vA1f)skf`*#Q4T+bcvK|m&tz?a zt^;ec;ch%^vdE8i%~LCE$E;24|JK?BtItV`@I2-!Ah5-HOoM>`NOoh&9MtWwfND&Z z;{b?a*j7g#ZL0jyAXh;s48aMYzu_8;!eIFf!SQJDr&B$xxq}WM&~rPTKwIxrs4)0` z8-<$LTme$2yUarg6b8*tsd!$x-Nc6t^_W){ov4KIdOXf&jv*;W^0@>?SyTm88(|F` zrA}M%e4-I$7?iaKd8)z;M_CsW;j$YhC7ZL{Y}Lkr2O8!idBh%y-Pkb;Z$?`wJ5jck zelKEGNI6l#Ti^;0Yk^BXXQYgcF^6c0IB@lK&ir%;Y*jQSr*IVS5k*foN~=45j@ zneY=t=6t)}6Llh(iQDKz(B0FX>ku4lnol{`<5;%Kmq7$-8F+t{n~2<&Vlv!v6vyYY zzGgIFvDuHNtYd8EwCB28YC^4cK0QoJF`rYpSSy;l5G3cXbWuMR8>EN%FBB?XJdRTiwG>u`gQ7 zWxRX=_^}dZs20aZ#T;Jo%8gMjl^YK`JPO%EjG&5TtzOWsIkesZ+|IuoaQn-dx!r3L z33O{v>l);>i^D-{V3shtIdlf2E?wr@pz1`*g%qY5F3Ucx4K?7lQ&iU50i2-om&^=j zs9V<3X40ws+DrdEN?WULO7qw_IY62f8bcBL#x9;qAV*3}OO(EZF zw@(T%T^ozw!dp((6vP`v+KqzMU@6ugxh8K8TSeQ133@~kDd2k>1$c}*@t`|{=cN2t zYk~-B7lnAGC=}r=DZ`Cwx-K(vX#gSyY}D~;X|!1(i#cq-+Uq)ibHN2*7~(U-Ic2(z zwSG7ejT==9xB{t~U{=NzPic6uDA}#08pDtVd^9l!AJ9ysXsv62>*IsLkIl^W|;zlr%VguB} zVx&woHDVkwZ7E^9uhNKWblRsWrl_ zdOTW5k0-TlVvI--05X#u*&5Qj)(5`vWW!9yMv@px#*=80 zRE)B3l~h+Mn0_dxdo(FY6`VKMGP74=14DMjokMo)4~2aWJob7Gz?s;bi)V&&YMYY^ zw_`XpaB`x>V{mm4ry#V#MJhIgr;3_Z0#~kC%4v3+S>9TIBH%i)IiZ=kp2FrxwgPdD z7&S*4ZB4UPeYaM9L}UOsz|hq~BV<4Dg%SoJY^h?`}4SD0!Av40~2gUrwzGusM&K8P2B7sg+Z3u}T1E;SiJoMIljF zYjo&{HjeU18dX&*n#so^ks(K+_PPkzO!VaZRR^~VjGfIh1-0N1m6E^JDSy-CoLW!V zK^YXjbUKM-9|mE3G9hX-9VS4g9#Mj5Tr3Gvxvix!3W{3m6~J|(C+E$~^%QzyCWJz~ z)`v-?%D8zV)dwM0T+&g{K8%UnsBJmEj)^r9GJu{4faOF_&YhX%33?(jZ9%e|1tXK- zz-zRcu=8A}GGr>lQ6k<9C&pT?&9z%hlhC^BJYY4^leL*yZPgPgInYNX%QmByFF>Vs z)M>?30xw6a30#ac)LKV#in`Uwm9f@(8StCv$s=dxcQ-v55nk9LVXfCUoidR(TOb}S zBU6gTr|b~yr3YcEDi{S0BOstB9N;$5lcO_pn^RA+YJTm|^M z&=tZ3)jPXV(Fffw-r4PQw!c=GX>Hz`m|!h7d!U!KUI1_=8g%x|a898?P>*YAg&|_v zF4z-rvZ~T?!*uzs9xYTWpfqX>L_Wh9Z}Yyso(C)^8g$moEKkrNZXm`(T21MTU82Hq ze!84!3j@EdM8^?Qj?kC|u5ddZUGAB!^&DU|nbD7!nbqB9^c*jrU})JphxXw!bG!E$ zy)_ZSJh#Qc`Trpc#f2kJJbdfn`>iw%z5UR^gUti)J8;(iC++(JIQ4Jr`S(5W?&mK5 zU>RF_@#6Ov$z3-t{0cO|?)HmD;DyVPUgPxkNIwR`vF`((;IroGg+r<|rG<81Y=4EaKUog(Sn# zjG$0@X@D2aJ8_;k54K0T6kz;SIr#kb7iLC!&PF;wdj4^wi)%tesCpdfR^yHoVhs!L zsnK>el!)hsB~U?^?W(buVhl=Q2)mnk*dF6zfbNc01fRcvX2y8d261Fz|0c%$!Kkl^ z&3Iv4jx=h$L^A>$F$Gvwm2@VbRZ_WD)E_y}$UqFQSkmrypiphu%3i9%FqMQ@+1!dTiY>61N3AM>a6!6%8ad5$T z@yS+gHN(_-p_*?N3cg$|4YIVdWq$4skp6Ij_s8$LC%h-73J4BdK#x)<=@E>ToN)=b zaO?S@Ucoy^Qyfc94u-6OD{)PvR?c)?9ZlG9@nk*P7Ux8dzE1{de{hd_bP~?Cv`GCwlasRDkrm zx6CZjb2hM(^eCZtW{2SsMnOUv!2(Y@SqqOU1=vgYdX34%A(?A{z%Af*e0M$C7UM*Z zzV(>^`<^7#pp&vklk>wh7Z>@!_3D zX;Y83g&1J_`tO3zZ4J2+qZ#RRBU zEJb9Z1|m|GT1N*?gm$gaAxlax@vMzYwkrD^F9DknHfW zoQdNr=JGtRA)9jDa%Z#sB$!zE8<+s>-$iEzd)`JQ0DCtJKgVmzCfK`e=e8B=#KK?y z@&M`IylrNr=WK)nq<6FMb3`m}BAp(sx0mL`!vFRA0*o)5U8T<2fVW!sIo@dQ9QjEJ zlIXp?z=KSKYU3?`!tv_uR4PrF-z*KLGv!iRG^^KV$jgrOz!rY3Y%R zA6@J(K4jOscQtpd0K$KOFJXgPRv*f&HLwqcY ziYvWy^`Er2i@*HHYY+QxecsLQ{Nb02Yd8Gsi+_Bp_vnB7(xt)%J|R5EA%05u^51{* z;FLO$VFZk4T&%HMMf0)1j&kH}5UJy1?6T)*Gy{Cl# z*K*`ZYei)L&m$G|7yXw$=OJfA9y_`{?mb)BNKOdPaYmjJ{_Jx1;}7~)^NcsHU-6yq z{QUO%XWUQU@MHP>U;Qq8-mPap_DyAB1Dg<@<4`*#{FX;$p83w}U-j$%zLtAdJo|tf z-*su>r*i4S{m=Q<%}@JXCsPqNE}amb;|w|_{PQn;?}fJ{LSMc5DXB+Y^r2UL>D=f3 z`~O>Cc#bpLl<;uFN_~2Pz3z)=-2PJp^}826=U<=ps`AHP_vOqdzx(J% z-f{Td4;ME6azc2H)6bM}^f{k+-0u(m#dF{N!^b=?irmcp>vj9S{6E*{qi?$3@*|eN z-ub1lamj@69EX)D;ZOhSejj@44_a`=fP^rq*6^;hUYec*@duXwWmcR&3qdE`~^-y1pfPp_om>kdBZv&q53FMa*}uNF4q6T)-s=BI@D zt1kW0cOHS2kw-Oe=|BH6{VmUb!@}#|^fB|dwX;QKWf2(q*o5#L9zIjTuQ!K3KF_#n z<*aL}5BhlfD}&#^^-S}9_Svs^<_q8R=1Xon24sv(2+y%?o)Uh=w{IPuwQuojfBWF; z-+SGwGf%tdtNZycd@S?+CGuSBDawWC3mef1;W^R=ObK7{i?8o{$8Wgq<8O)GcE=^+ zxrcsx;nzOc`7bHH|7q`h-7l7p3LB9L;W^xMriB0HCr5sJ`KLbq)o&I*`@qXTcsui! zeP6%mIk)r;zy0Xp%bxgbN!SQa2+y&3oDzQGKi&Edzt}zc>&JGk{@-)4hdlS3@0;&> z&_3;>@{gXvydQtvnZgDp| zEgyT~chA4zox;Yk3E?^1VWxy%^XjFCesuQ_&U^4L&wB5F-~Xl0ddvmH4_@@RYajPt z54-K58xLBcg^i0Rgy->9nZ#_ZUVi&`zI65{Us=BL1Mj)v>~Eou_|)gVd|2W?{Mk=^ z{n8)2=RxNQ8=(o|IowgEhz}Z<92)-!8JCxY_7AUmj1>C!H(mO^+}El zq*nr3>`NT8+aqVMw7!UZw@85M@_CG$FdB62U;s=HIedMMKGgk>4 z7fuMz;bJl+{F_g{>);FiEk)k^{m8q2*naB=pQgU`5%9U+eCU%~(%XLa{>K7C|L6(f zIebZ`gg<)pFTb!bc;rRRvoCyc^JmrTf8)LI=VyNRk)ta<&pq;2Pk%^9*tlRqcn-gj zDdGD+?EBZO-24x(%0A{7AN}y{ulwis9E6wm6vh|beDI}v9}hhk>?+p5Fa7!_*LR)&;g|ixQxE^(s{f&$ENq-NAv}jy#}sk+%|E~T zO7&-F-g3t;Z@B+(|M~La=eb|_)bl=12xq)t*QICYgpG42gy-E##Q^y)9K?t1d)KOGV_)+U7K@YI+Re!|-xSpV=RpZVO< z#*4ms$NOJ!eJ6X_!{7O%-@Wq_OA1YW>`9cc@yH3`Ib1TPgs(XB8@HX=x$)Un1y{cL zgmw~v2ZJ~RHg*S|ol|KMEt z;+KE@Jo%8YarT7p94-)3!r!~&tq<_Ncy(&;|NQ2PrR#pvzWt5+8t?mgmi{om>*77p z7v3mroHZdlhoi%k@TZ*{pYq$$-+$~i55DP!(KoMr_H$l%!&yhv#v@+-s*B0O1K!98 z8;_U}p2MwSN|?UoH@`memDi4*F0K9Z10Syl?|7Jh?JpkshT9fje);PzyDc{k&i~7c z)WV_RzBlZCFZkxqUw@XsX-EJBE3V!dtT=}u*xre1OK3C-!gWs0NPt4&lVhfiuFc_c zb?S@(^AeaTSaDm&#~`9<`)mPQ{XK(LCuR$nUTmITuwpDACVR3Y4|YCS@l?Q1H-IY! zLiPaEQwDJ5kCA-8&@ttFoI}QGmW0ZD*Ehzou@rGzezy^(v}UrTq^r@>4&Z|Oc37_* zYeut;3rUa?NE#)g{X~v2fJYlVpbK!k)kvU8h``a7%Vx?RNCWNWY4k!X;{D$QE8a)Sak{~ZC(Yga zFjx_Z#uN86*N2Q^Lv~I*cA+z3#WET>#^dxjVU((+oIr`KbU6-Pm@(QC213vQk<-b) zQFS4_pbT1mrGV+0gvEwhHGN?(St4RhoMp;Yj!kRAfbggmHXgN)F;ojpqi(WIkQQk- zVt$%od63JjnambiT}H6#Tx`a<9&{bd^=FV9X9sirx<~Aq$9;8sH_I)HOptisRErFB z@?8JJr@fhD%XjKUY>v(7guqOTn8@Zzzz3;O6iY2l8sxdHPH(dkEMB%^nQk?I`f~ zl}7KF-e)c(bFNqI6Q*2`%8Akl7m)@es-;ms7t=FwY_?Q7LDvISUfwt>P~~eMylZWa z2|Bkb-%|#w#2-VI=gpjRQu2&>ZNU~*KG`)RP?*VDI7#?*pJlQ-1rU%>1Dsev;+e_% zbZ1TsWP5!Lx^TLB7>?iblFAo)3=wvZwF`nZl5B0*thk`gmH?7Vr^};bo^RQF->*lD z`swGoutzNG4rBDO3gkPb3(FNP7sF9BVOmNylk9a}-(^x(qn{ zmk0d=cN}o{|7^d#@2C6vd;fc{x#!1wy1Re4yS@Bh%f{08mbAtHTvT^`dsk!Oo1pnU ze_egup=FRwer=AMT91gH-I^wthT?`y87BHCjjPn65Qz_7|#u0hlgM^H-H@;g5dwJa2ll ztOe!-z#NVMJDYF)>T7#*1K81<KN!1B-hX4&Sg6J@|8M51XK})p!ku=8R62D7OJr~&3uum|3i$g} z5^jWrkr@0QhqNM@cI)A&!?v(tzD$6&bdxP>Lr^ruXUZ8r-@PZ^)s|c-Y6{iys~q4# zrdv{27&rWYe+J+W_#Hy_<#B&Iv&oZ zAhzL)46lrrM<*k|d{$Mltbfk>$l$0|r3)5V# z-SOl~&HzbwqGdRGDk%>W$yzj(M)GEx?H7pL0M(NzR;qTD)UY{f8WJpo^L#eJtJn!r z{^WL2-kJ#TPf5zT@eWBj<>-wHf-B*Y$@NMhE{$}G5$P0CPO`F4%ygt-8!>RkiB$U; zPX&4Cn&Wg@>~m}}Q6Q*JQm1tE#JT+O?WDZb8R1V#%B>xfGR$zW)WA!FdP`|liDWJl zo~#z5yITxQDp6Ue8ZRYE^=1>x42!WQJBqgI+Q1hRR>&HTiliK>!$$Z-DSvD`DQ|uE z{gaaNj@ji*LhW<}xt#R4ibWN}#VFT1)m%pdGl; zie@Pz73%n^WSl7F+qRSP)*M!UQc~V2yPUQdxeay+0^cnWmZkRSA=^FGTu$bo7S}Cd zqh>dT7(JSeH9^)N-5b?F`9sf%LD3f6WF6bnsS~CA;q9cnH3`vVcukmXtj)%6CkR=%h~$yjbk*4$ws2DYdp z|Af`=gWE}YYl@^lDJk!iU8ay$y`qzuNKUAal~N9>3VHri76VV`jYt_a>XBl)LeM3J zOVFTBV@HB5k*z@e{wSP0pfg1 z$x6^Er3?y-5?U!CcDh2BZXjI8@CCk?CI(pEi;Zd|ZJB1cZ4gQ9#JT+5m6byaYs-m+ zBhryQhu?Jgio+`_x2|X_4_N-%^0mu}LvK6OJaopv_Z;jVJoCVZ4h%q6z>n_tj{M{P zqx(LwZ@llky`SED_1;H=41rJGbMfvk?%voPUi!|`^OqhA@&sNFDg*pz*Nb-%3;(n5 z($hIruN+!lIkfbW(`owWHh-4DpC#~zl|Zq!1YAnsrd|(?h;E@DrC21U55|t{YpqH? z*{&Aq980!Jgk(1OxG8O45w-ZlfMlc}>pOTdQ?9^T#<4q&pCrUc*@23cEH1R;bcQVB z5ZCom+A7-_cO72hs(B+h!m3JkYmxTim+V?pQ9>H2a*M?wo}4fdNf8%Mn>CS4~Go9jP4Ukt#IOnoVJ- z*RZTiQM!?bP!tp}v+PyEP)m-SN>^z*mC3hD1?wc54+v=bLqC_|x=taC%6P?K3a*AL znk$7u`H@=#?r~XLtke^Q0qA?KsL9@-YorRiAPX(q9-TzT3#%^!^Oj(HNzzX)iGjcUSb)%tp> z(lJc6VTeUPi$=<&s#sx~j;<4_R!ffKcmrKkdR1Y>(F~b$_^i!o;?^v)#cS>#&}6lw zSPh8?A!6xR1#Q<$1`R0%52Ed1xJ9P00xe5Q%IGwtRdd{{$|)-Zd3Zv%47G&(eG{RXj$fV>A*pKR>h7bj8WX4h34*nX16YP9ch4$JN1@bP@!@m(@2%|Q8nC) zW31Eh@>o`lDY~GWt64SKsqjUEk$5x1WZUKBR-9fyl4)b1xR%LeLztlUV&1TAii1kE zB~?2ijPnX8sBePwdswe0t@0zr%!u8wkoHH7Zjx8^tt8!mB-CuQ#XRnk0^6nw(ezM2 zdnDWyRgejr5m*zJ={6DP?Nl9G?ObXgvL5ygHx(mZM7ndTfADJ2IEw#IV#t?*{vw+0Z9}Ib@i^^ zh-;;2NRz{Ixh9neG&|JA0*{z&#V~;%GM+Uca}|^*#)iZ&NzzT31eN4dCoRKrK(bRy z`^^z&4a&W!+c!X-RZvkO#gZ?)@|I}*#e>WD}TToQ_#Gi(|z3rO}wELI;zx@9EM z<6(@Ark#2}ia^<7wP{qwy$+dUqr4dFkl3o6OnIExa;iL0=oCoGleP|1H6TgQH6%1j z+QYU>ilK6~R;x#}UJZ}8+TL z#~+9%t;0es^Z2PctZo5Sy!(51F|=+ z=PJ67<)xF%2Op3ejSZpb@dHdDn@~38sc{^I`aTLO2R3sJtf_`YD^_A;y_8v{U3=J9 ziIig~a-mb{>nHEN4g@sk@EzRV10s0;#~0teaO4wEJmBeth5^2cCUEJa8U}6L{bL z=kKrW5AFNSzT5Ww?LKv11Y`;Pg_PXHfphP zv(E@k2_lOaQ?G%#Dxkv1D3kF)a9bNa^w>!YIy#m>_#2;y(b-G~>k*n}Do&?YN`$L& zE;44aFgQ#K367N(N7EJ&vYxh*B-{z55`H*VGt$FQHm7@4J=SS*C=yltfeazNZe~fD zwg`cu0+iZ`6Cx^C$!s1?ID(CqqoP;n)}z!oCb1eZk}5=I-?t|%GUHw$5-~v5%36s= z3^?v(#cXImX6g|ER^@uL&#FSwN@J?O^yX;`m#P!KB61OLsDlgd6qV*+9m#{@lu48} z#E#O6i7}Kfx6OkOoVIYRgosj+LA&EKsRn6dASl{59b&o=8l`hoQ;8E$t)Jxcd@hm6(Xyt1+9HgG9lB`RqF+dh zMnQj}5o%Z&M5_uFhiJBfzw8I&KUosh0$a7(X; zWyLLlwEH=mDnn7IJmM7ATrNyo^oG9FX~nAqA;>fiE*fApWuz3QPddF;&yl0iNTVg7 z2_9Sc+_Xg()eG{N=JS-~WosN`IMm330v23c1(H*&+$6&*XE~Ta|S}N)FvRZ@D zD{_p?X}ks7omM#-#~S&hLWM+F6q9I1ZE4G2*=&Kp?O~zVjpnNHbl-4LbYyfrw_(9f zDdEMt6qoUa&X^2g`-o`^jSls(F~*Y~NFE`wmR_=a5a3I=h{wcws4hf_SiUO^S%3HA zr!7=A#kcwi6IDg8ua-c0W>AlyOFP*_7|(}gP^gPRnr%o`?0unW3q|!!N6@l^wAPLA zO(uksu|6CtD7#6q;c^!8~Bxn@I2DQtzpW6;wbNgu4xG2y0%5Y6tSC? z9Kwl?ml+Qf7Olc2neAvl9)M~M5Rq`bt8uN+#s4}9_7>DkA^t45( zFEXiu2FyxM^1j%pOHOIS6eNnsUSR<+eiqn#A{?S4n3^r*i?PI+X*ZnQe) z8;t@KN=Jr$cgRA@fGigHY!9zvwNY7YYDJ<%ntKjFOX{w*3|-AK;XyRsswNb{tfMCH z5Kg{BRC;t?QfidljDlh{rM}cgrYvYyXFJJC1W}->Z4g6336E=4V~{S4ff_457w(u% zIYB%2;-;uFgQ7ahB$Fkw(Hyrirj|=$V--wztZ#FuJ_Lolt8#i!Rj~chDUVE(E~Y)( zi$;}fHjyvWktA9vdHw7t+pN1z#!RF=RWCbzY2jDX7L*)Gijop7AQc8~4C5&%jX7)| zhe$4?^u~Q7RL~5uLO9SKce4eKh0@u2J0T~_n%$3fO)l&59s~jN80%XQTOyhx9f=vx zzT&h6DUf0+jd3}+hvft2K5>0X#WcB)fz(NH@j8d2CUaD4{p<%L2Rb{axtP*UshR-aO1=;l03 zHIjzaO;Dw}(F?gbHA)sKzsQD7qQnp7LSa~t!|@o)_M-bQn}SLXjST9glrTaGxl%PK zKNv|XHQ$XGHMED6c&5SNVekkD9r&Nk7HP7QP2{R>I_C6yz?%k&x7c2>R2qZT93@yE z>q8-`uV(FoH%wb#SIftEN+#REa}uJy%Jk+sjYgY!Ms5KBD%08k6G9l2I)wmQ(~dcN;cjfLcKQr1~Ln z-Gj3Eak5IIRHYI(N=l?shr{`1)~CnCx`jlDJ#Uz{@Orf*qBCtU*VJ4cOSh1oRdGv5 zspXLzH)3fwRS*QM9fuA~Z4c%2q!a=-M_gX0}D%#Eac@h*mHp@ z==*P-wlEOM@3aa%m&VW(N=QzlCFY_+&ggU51lIw{p*rn!AL=lT1Ji8`r6&v&i)bq4 zF{ICkUYkhkN+BiviqRq4WIFU|Nal+MQ8 zel3+6(W1=s`UIaS+q-U@wy1}bBR6AaHOYyhL~`6=ljF9;B2F@2&2;5Zzm;xwa-$@w z9k?vW|95oZ`3pzvBl|)2|LDpGR*Hvybm)qMcN~1y!PNtAJn*>vZ`{AO?>+k>dvDwO zggrmo^YlIU-~IIEUo8Li@}8xaEJYVTuz1C;&+IaG?E*x0;;Xyv?NLFk$%UPL!PqRN zaB|`ab=?L0jsbpiB#z(y#blG;@!}%dx&!z`0G~PX$j{8@#CjXo>|eJ5tMe~kSeqlW z{LHNGwH771)%QtW9|Twe@y?!%er7Dw+DF~>KHw2dnK{zOZ~s7bFH^<>{DLVnM^^cn z`JFgrOu#3YGIJ!EpPA2=Dbovv2~3$e($3G!>fWc!R!1&*y>w-JT;;{mfXVQwHcr zXS2gQ`T)5(XYRGbCs(lAzuw*iv!gGUvw%6NRF$>f0$47%U}2v6RNKFzZelrQG4JlW z0ayn5J4f35nOUC7*%~)PvI2n}AthEaphK zKQpr}#zFil>iIq_8eLJXU1}uv1n{|ct^beX3@|1)=(N? zx$2u>=E(ZLy-UJrfw9(QfF&>%bEJ2g8OuEwiz@)jz*x+Y&S++qr!p2bz%MWsb0iO% zncs=V;)#GyU@YdykTWx%lZ?gXfLUNH=EwyzGqWwm;t7CNU@YcHx-v7Xdp8za1El2j z%K|K*+dG@%?C3k~y|14VU^!VofkQ>S)`v-?%D8zV)kg*zmvj`g z4`U)XY6o{jm{=1b1KR#+c$4L+R6+#&0+pB}fyd1JPE?5s;1j6C99b%6=5vxt2!L6j z5_9BZn3>rYmEZxZKqcl#d@wVs`BWk}|37T$gA2QV3+nhidimDn3zj~3#5}V5@QV&Z zEALrh4}JfTc<{#uuQ~Y81Ft<0+keykOZVNpkJ$T#z1H4^J=cNr|BZ`}-}O(s@P#ig zym%*$|0mGc{j(GPb9&z{e*g5^{khejCGh`{1myMe1A`J6#E{@Cg>8 zu-U~&TG@%E_+kybNtQ!;I+YBk#D-nK!y}_6hxBa6j)iN`%zW;)5Us2q-5k<6G850t z>fRTktpUG*0Ic=L06h2c-NRo3miOh|!zF;Clj+@h|IkroPqo&g0MLDW?vDVL_vN`i z4EWs_=YAOQxzEo15MXwnocmYSj{#PJqW(7&3a|>);*Wp!57gqoq3Ogtdp~KG7!*^e_{F>#prC01{_I+d@zV|(QA9LUf zE1i}9-laemu)A@iChd0Ws@vx z=nfmA3qmv5O@`A6B3>&BBQ=bz4lF@p$~4(X=M-|B9*eCla@haW;~11`c$^sWd|k+k z2*kA&6^EJ*;(HxB-JolB*lks!ty)aTtVP)fh??fpz#q&En=tacu{x%b zT7Aq{Dd3bvR=rGeD~1CX--W?~;VMq%y{;vPlQmq7;Gu@3lc6>anw8W@bMaIb_-V~Bv#7gr#+Qi31xSz{4a_v>w>6OZC$7nmU!0W7# z*&01{V9#+3u@Hsol}<+%N_|8@eXp9s5m76p-FBBTdI=}au^B@c_Sm&ZL*NQ(Im3`i~y zRY^RilfbWX*rk18M0@RCylw+eDskK|gs}k*QJD<1=2N^9OQuN}PZ=%?bzLRxr?Om` zcHCMq-^r2q(4x~$%PTilONC~$+3s>gy`pJEt7mSF&su)oaSTzfNS4MxzKNmQF{5e) z8PpL%X^9G`jsn9&DQ!c!mgV=sSg;h+RUx$R%h7OmSWJ?C}^q-IZD%x>kp3BZxsE z9~iYkU$#UG_-X6b$W0>S1jI?*RxQDH2#;2yAfD9q1%)3r$GME2$x8Bg%UJCC>2VB= zUMM3cFt@;S^;VV6VuIdH_u>g6)Z(&vJl1gZa=g*3<=C}cq|h>fJ9}QBII?fzP)T(M zCdINIt>hB544SMJ)0B+1+U?a0DNtFd!MVVPv(QOXovn6q;U{-tu#$1KP$?${JsbGp zW8r?&X;RrP+^|A9Du+iIxzNKzu#TjmHO?wC4j=cTQWcj8Z0u8Gma4`ZgEs52g(BZF ziiMDxil98Y+QPF~-s!3sS(nO|KO(k$c+RLHbRxanerOl~BE(w<;0IEVa-^wgk~wX0=-eKKRvQs@uffqD>En zCt)CtV?ZsL65?QDwZkwy_OygS3F45?yd@)=baND3~%# zb+^c2g*}d8#HR7gI6=}`r_F!_y5XYe@GVD8r|RW89}`MB25|~ivj?q-h=}y4cqG~n zjgtf>7zHOgpxd=#iVt`5iq%fEh(XcO^JQ$cp5sKWEGv0d&kQ7s9aCE{?D^VV7_2n# zbxtJn%!ol6E-q)RDuS1kybIfYh%1e9bg3_dKoZ(kbgk_$Lx~27TYNexuynT5Yjzl! z_wxW)Nu-DRpy-yv4bHEzPm&bI$kQ6OXfWY7o zEsbESUGYIaj4G~Y<9NPK!pW{_H=IVPR_RLlbX#bf9hKI){gzt<9^;u+yvI}KaG;Xv zFxPEyg4N$DhnJrghDc>-QyJ00L|e{wnoii$y_%XK5JR;PIGyZjtSMFriWu56V0hVa z3|byZc3P6;)>K*!!;OUIxGX_rTG0v#XX}GZt#P&F zfJ{fcD5p`bkxzuIgx=(fPs zurjvE#cKvHgZQRm-VNodjSy6-=88m}8%9c{2uNX}jq6qpT5B=mBtoS|9ozHM(OwtC z^Wz0J(smTxEsbMrPb<-qoUZA^;%dVafig6Pxtwn29B1e`Tjs-xaU4Uw%om}?5H2}2 zl(eEDqlx8`4K>s=4LKtgK=hEV#wBYkL2D}a|FQS(agJkmov2-}?yjotPRKifB;BdZ zB&j$hzwIO>k!4AK$+9I`wmb}8NtR?;vSd9hKeA*tkTFZ13%kJGk zM0<^g{eN?t2gpD~gQzggBjM$dDjbbrA=pY~{3`fxdNz_32R&tCUDYZHm8`F*U zI7cU1kUNCnnqbB|iasG4*{Y1{K!C8RmJ^*D-W0*)22(0?B|6V{u^zL=!|o4(HT=qv z2NUQq<}gtjB-Fqr&R|%U$CW{Ey$-1 z>xEc+$e?s8HHmxOqEkXL7|VjPyf=zWcR<&NAjXHUnT4V-cGl)$r?Ytf-wAy_bnso5 zKDYC&;2-~Ben17%SKg{X8lV=?`Ol_n#e_4UKINuj!l_LaNCWtuEb3kcdGB4e5Ae}x zb1~rn9zUCk3E%BZv|fSq%Txe-10$O&kou??{KZrt%_QRxSjde9?0YS@AZ2PuB`UL^ zsCS%kl1AK%)-pmt$c*AxF6Ok-oTj!sP1RabKUw zK+y765(pMdMzgQyJ7gC&!LM#IYSgK-F##UAS-w#_5!>z(j zRgld(Kh$P!nbav*EE(~t(1&YOkwlBu4B@+jk|2mvrI?AwbN&k0FNO{(kj|a?(LxX3 zH40y>KbcQGOnz1BVE~V~O?tTa^xLsPJzM7)(!Zw|SP0>8BBk8^gww0xaA}oD$eA}>B>nWi4N%`D@hEU@3RO5 z&NzA@?@#R(Lthl&e%|~Q%k3{4zF7B4pK6!*s#Lom`}4yEsz{_SRu{(R?Jv>EEm1# z1cF9Ra)6*9vuCbPdbyfJNR~26YXe^7>M@OlUDlGP1lygOC*>qEPE4cIm=nXqS-h4+ zV>ru-#<+P>uu4d7eDhlt`}4?H?9iaX`C@nG6{+JcHtJ{+OnL$WS_y^@6!V|<=QkIM z`93jxv98EI6*Im`F$4G&E>=ItKW3Yc8{_4HRy8}}0K_TM%hc|nJ_ zTdbR>r(^RT@ACGt!v&8ke%6>=*@4@PFL@Jq;})BmDr&d>ZA)EVT#RnnNHsilX-}3D z(3OU65Bw<(I{Fe?n^YRQ=P@8j$_`OKE)^9zN=nUclR2r<%yeW^4XE117Mf-<*<^-IFIq;=~zoa2%-%LW<6*xTl`P2vunF;0t!OKO=HeJI*m9OwGkJ z1KTOg%;RI5)Up}LROZ-9qLWz3aQb zz3YYlukg3s>mx&I7kKDwf#jcZztZZVJV})*njo|5Z?mV;zVb}o|HL-~vWNYXo#mLC zhSnWZ_x}v=dvq_3Z~gxDyTB>NKMPMCf1Uk52N2(J3+5U9Z*CVj5^de+ zWxRd^FR#DGp3eT=_T=ElPN%^y#O-M~_|Z4+0uLPjz29l+4&Hw%0e<^EQvx#UE{_L4 zaw?a8;a75b@WZDQ>Q|iap3wbIozBX+>2NpL|1JF~y*MYA?kVwEvl30M&&rmD1GPI@ z$b2C9^$6ZA^C{i&hjUwKwgyklus_nfXL!qa%I5fkSgq|=|JK6vNp;lqMl-Tf9RQ~Y z8DIOvx?y21fA(Q8a}NPomz^GqSMw9ivuYD;(p*m>>tkWk>-H-2f80_+pXj&eGxT$} zOn^_)yXQ0X^S5-xC&X$#Lmzy@LjA)x`rOP`yM0OR4aT+HXj~D_pHJ+Y7SrjU_AEJ% zLu>s}7YKgW&8mKp;OmN2yGZbL?I$l1eElF?y-4u&*dgK~!Pk|qa*^QiHBm$_5_~;T zPdU3d!T49A+g76hFU}4ADepEK#UxPR00=9ak$=-Ja1i$2@7nDK&+h_PZU5vOvk{cu zczSA~MtNz{$7aPmSr*3m(t0I@4cox}TW+0Nd=j5!w^FHfIp2RX1!ffmyG$_nP6w`% z)lMf8zf6J1I=@o7gl`bnySDKiL__(9`_NMiK3LTi&Yij)Yi-*2z!|J4^h#X~Ow z>1YlTozbv0t7nOskS668nvFFzWt4?U1rbYASSg_C#8GY3ALfQ0i6lfDoZ?5=1gQgTavfBMWb@-2wSvalEl^t) z*D!ZR^U8W8#Ff8)Y915|cYs$=Bi5+3`!ZJ3S|&(NO{(>=+ljKGQpE;Dd(4m1()h@s z7%Ok%l>vxTkRyo^!cEah9wCQ>#}}hnqf?I}ZbMXuGw}vbm#bwOZ`E+K2u38J>s5mG zzv(s)%BW7rc9oBTbii1$V+m}J$?>){#3wZboguoxRI@08yTzlH%e2N?o$KiZN&-bR z>~t2CCe6keQ0fM5M(fpNYC6v1`E0-F!;A3A+dSAwXfU&c0gO!h@~8w7aKkP)nT#iS z+MRau`E;?C(^5H*)BkA74k>%2RwHDC>517<*@#InRx-+cy_t3@Y`oeQszj$_%Z(dT zraq|T;Yr&Oo8<~YH`hxBU8ZmIFr-q2TDgjj1WW)wOSPl#l-8)TCI>>AEpsY-# zGZw8KWuj(@pH?`xoFwbjf|D$&>8gO&^JQb0%*7H#wp-{?cD`BM_YqSqcLXDWKcfvnjq<3+Gm2Y6;{JUXgGbKQKV zPZDEpY*^_ITAo5#w;`dRQkr6n8@eP+yjg`EPYca8-Mr-8=D~6uAd8~S$yBn^ za1%mPD2UEMd8Uo(qz1Bomj$%7UR5sq+}k`1sCHR~Mk!F|iqx$|@P#_cK*A^;1u-aA zfhkasUSq3l9aPU!;X0ed;BLXmCNZei#gfvP@v`lBTpo*dQf=X6-O{yqo4A3(IjY~~ zM%-vX^eXhY$gj=A_H7SeMYE^03{pB<=738K$7mZZIt3XY6tRj_80!62qSWQEvRHMF z@|v40$_YN-PLG6v|b-Q#=rc4UmAC8XQ{WZ|CT92Ym@3c*uFt>HQo zo15ukI^C^E+$^1JXz)=q>s2H~PDVgBX0|M5nf@4=(lLRNh;%He+9WBqd)c@xOt>3` zwlxFcR+gmINO$76?0RI(_7iUpFQeANM@rv|=PMdee08REb5PGCVk06}Dk*Y9P2-fE zWI>@Skg47t9}z0itjD|6Gz(AD__&MPSxci56;SBQv~q0=kAqSXZ9~MX;tdDXn8Xcr z*jLqNeK;Lg$u)Vn9i*i6dMUC}i$&17*6n6GRjQd2D^;RR)r-A;3Km_E+M6a4WniL+ z5!H;7r9@(w?sWuv%2#`Z+@KY;Y>_L?ltyzrQMhPo%vFgSB%!G+GXm+PBo%{!|Mq$z zs&M4CTJ$(ElB!3l)kM}FI`*jK&_tFPI|?!A5a4f7lfYG^lEmvrWC4XM4ULJmS#g?- z)Oxe1HOOl6&=wI;26E74qOp`!l2nSg!RS(%oWiJX50&ZEhU@U!G=x5L2M^7$Go>XD zPi5PKq}403W3AjRq55*=`pjcqit@3Eb)zRl>xx zj4Kwipb(Uz4J5jwg0$Q4QC?9vI5VsEOp6-nc+0I;Mp8o_=1_wnbHnmLiHqZ69?K1h z^9%NE{pxKVy6JIDE2|c;y*ANAxVnX!G|cneehsb6WDQgSC926bo`jFc9V)9a=>{O;R4_^(TM z#1uu9iVW?(4&^#rB$;Z}N*M;}6bp%>W+mM`ol#1H0U{Pby|wdd*lqp9Z5{-iA4(a7 z9mBdllg1EZjHI%Qn<)@WNOs3~G9Cwemre?xPMJHZac&XPtuNd*4}1r0F~w<#^`=6uJ&Qo`O4mggzLNGBq=o?x?>t9B zxY>k`Cb=PqZKLApM1!2x3el8TP@GI;R8DeTAzw^4VugYSVn1OfalUy-+@6Ms7Gu$B zUm~z%&uw8H5b{AaExry?@e@ds$2nGS<5agGG>*DzrN^boUQ3TbhDmG0ERqMVN~uPD zDuE+JQ8f4{KZ;Pzj(EeER3^@3rmOs5ChInuyUWM;UAK7{x5!4FD|b7YZbR3ixkP~l zS|_H&YK6o>wVOs4st>ckd#Xy~azbp|CZ#n=HHu={RI)N0cY1Lr(}ZASRzGSJ2|6jZ zO+gm3p#GxhMM^oX0FyLUv|?%(MXb=VR^l>UITIO7jDmk zn?^=M!OF0%Aogc4&!Cldr)gNFRCz*9$9OGSRuW2oP*sj%40R>tgn0yAqe+9;E7h3ZG#es1Ily-b4w^)BQR zh;gn~CwiPLfa~R8$TT{Y91mMVwCt7GK|0ZkgF7fF{h6;OIoi2lH}Nq|OS0Xb)j5R2 z3hR1t`F7l#qL4YcMS1xum(XTCjhFibnvlj?G^)q)wMK1bK=rKCM#}Xg1E^tDs#8j- z8dn5;QMr{P@g6l3Z8JmJsctgmQEnqSYAQE!L&gFHa-1gCE?SJJl-5rVmp;Gr{|~=2 zbolTUYxlF@A7A@JGr<=aLwg8oAf)cpK!k6i-H^1ihb<3seE7b1` zxV%NP*+p2+MlVy{Dk|ePJS(Nqsw1H~3zfzr6n4e}VJf_er<(C;teLhXu6W;mUo51@ zsBEIf%=)P+UY1RGrrTc5$$9Q^UDN5FFv}&}VX>ZaUC*Hh$5N@BR~1l_$Q>Ui`qSe= zrBaiWu?gz=s_}No9JF)<1c99AWqLaFMbyP;{;GwwdDk?2u?d>|SsN((#af$(!TK#d z1E2DXeCTG7)J~hCB391@>;9r&CcClf9F$xfF09YuX<@-gS9OLuKRtEzyvZA;ahJn3 z@FZO{UHNBrMz#(BjFGM{q~Vq$W$e~;-P>(Dh`}>$RRIhFM%SW@KsT$ub$|=22THo% z;p4oJK2S#wG})+lh+Xax;@H(uzDDDioKDWf@-S+|;%FtGmYRc%LaFkY$D-POE8Zv; znf28q_&D9E!Afr_A7Wo85l zxtHR~EYZl7GLke*fO!3Qd_os8=VwIssd$;Y6>ozoEq=AOwXU9@uz;^e62 zQtA>5K9(Gr!8YqVbs`EdzEsY|V8Nku;+~sK!GdL6Fe_3_owPio8HKy4NiGKy(P$+dGQ7n=>= zr>v*%mi3!KVk#Cw_T>+;4G@gHX%jp3Q(0HkFH>CYt=>UgknixKhwoI%g+Kl|N_np> zC*XDjLTjEvcW<lT=kl8$uwdHnKSU7(KyDKq|J{cw;YvazMMBf>E39QQi;q7 zJ~oCL4IaT7ZJq?*pJqP802f0CD!RD;M?$|4y8e^bscT=j_G8zGtABI#hprY5|J&ie zIz$dWb@1H>Z`%L({y*E#UisrI@4Aw>{QH;Zmrp?c|F>Uy&E9YA{o}n??*9AT@$Soa zeq+bpdCB(U+k@@rZ~f|4Z|m9NUkZ!iLjZm8Kg|4fJ6m;W`*k(l0zF#jEl6xG4t0hVPES5mL(&lp;x9`BMl^G zvzl5IF0=Sp!nx3%@~f+Ir7wc3$Z5^cd)s+rLQN(BMAH4iE| zlUN@|;9802x`+zudouG^En#2f1GWscat=Nqu-+isjntyCMAYjft63TX?i^`I#PY+g zP;XA^3vM4=!anE&R>etu(xE4;pbv>!WyrX!n5!h}VI$Mtew6tY|mLT-Az~OHFVTLM_pjnPw4Y=C54BzS0M5fd{p% zT_{zhQwbcv|4#GOtOakEaBaV;I^z#BO;f5j5^73Vu19LaF3SMg|e%D{zbB}Eo0dQurR zT{NDWv~?R2iB_F5$aX^jOLlV!d(#K3;@^{kncrB#-tYk%!0CdSzkCV%av!j4oNaqF zK5D03E<&ioydfpDawMCsPN-7JCI(U})m3dO1_4QU*%J0;K44pNx;A6tV#mnJah4si zQC{j|MY7yQs{O=x>OxaBse0f`Gk|^R685F%7v>C7wUwEF!xHuze82`M&dSWcehK^a zK41gXVrAwpS;D@=2W)`4tIYhxOV}6tfDKSWm6^Y23Hze+3u6XJ56R45xP*P757+?3 zQJMJ*mas4I0UMweDl>on688B%U<1@WW#-RY!amOjY=9D`%>21a*yo;Ko)Bb-%V1SL z*8z@z)3_$qh9XfQ##2z5+MM+3ju}Nu(*lavXe+_YpRB(WQcm{R00S2!^BBMQA-T$=Iv4?4o)@CTEafd2W)^Mq|E&K685?e*Z{RgnfbLP z>@^>-0ZNN9^Q%kPt3F@@6cJ_ShfCN)AFu%mhBEVmCG3F@*Z{RRnECz^cK`e@YC)=P zF!L)**egC@1Jvzg=9ibSmwmtnD8b3hFD+p&`G5^jW|NukEn)Y3zy_$M$;@|`u)98B z0~F6><~vK+9Urg(9%PyM_7Zm62W)`XPG-Kfgx&H3>;Jp*;{Jal^#0Iw3)KAko@=kY z`b$^Q!(Tjn@ZiG-vHd6an^*qg%3H7OT^?Wh#3gR;ulBmTpWS`$?$_`9;?9HHzr0=F z`h%@$_>Cul zISO#QQ0K1zJ;gy!KJFJA0%C&rdB6Q&eiL-|;KS0kk6XuSXJ-Y_L^JE*Oh*B(D&qXc zf*UY&AD5L=ZdOkaM*(gw=KSTLmxb{7xVxP8vO#!a^+>E{7GbWAqC|YGHJC;z%T!xr ztvfL|heKyXR+0(eV{R(*mw_G^!sFvwbK2t_!V}=0qt0IndRhpNkDJbEPj?3Y90j=i z0O9$D#c~1R@p1n-?d)vfSr0%x3UFx>=U>0z1`OTDwds_bQ{f44hcf3c0lh4Q$Hy(| zw3iLSBNf}@dKP@90i{CFPPJ3*cJv-W44h6)$EhLGbuc2|607|Vq0C;$-V+VkfCZeIOrY1_xW^OT#7@!&@R?xg1Y>p-Uq^Wx)HdfMp* z^Ag~?s?47adR&+n9~ais9`7(O0dBDR{8^x3^^+{$}{a4Wo;Qnnk2|?4xGd8r^>+~ zJ;+Bv=pmnOwxdjO97RB+)yZgTsYUo0_xdNzj0Eccd1$>jvTHrGi1YEhK@~t$qv&yH z6NGPjO6#OjbZDkaPwi}fR)YQc+b@Q`aE08NzjmpjF97M*oi0R+se+9#mFz%k%)C^j z-|*T~&B%*b5=()aiPLi3ra@TYx#HwgMK@-3{)$)807r&RDth;EXWc9E-058qeDN_s z8yEqu^G**WG3hRa`j(sim$KRj(8G8(y2z-!(fz*P;EY6rA)B_&@>0Tplvb%k6k6UW z)!>fn{S#4%VJJq83iqGy7%#SFPOnh4Gx~5k%g0?LSFRe}j6AV~cr{^|-5iVOJVqNH zC!51zsys@KdN|T_t6D|_l!!aaA6pHgs`5&|Ic3yXk4JpuJ3bvc&_hsdEZ25z%!(OPcg7?5&kk)8(GL+?B3g7Ot$HEfjv>}%phwJ zIcnNQg>0**9rur$`9aBzpID75NHtCXIls^CPdq-+bNT)(Q6bUXtlp1f>87DkIrw;% z7@crjEOi3>IPI#QgVnJI6;m0h)I=sQmh{j@7Hdt9d4eeQ>|rULEb^5`B`-04)ea1O zvHd-7{+i|X2da$(5T*-LyXaS?+6A!-4+Gg)kJwp6i>aUpRA+fwe1>#y)i=<)&DBQk zKBnFo4e+I`HUbngmP##p*tj3orj=2;53>UdrjFxe6_L1H3{;cNWtvQ>)yObbFIK3e zg;p_f--?i;K>CnG={r?|Y`a(3%Pr`B(14+?_y$NSR z>|{WhMR&@%nAj`0a&`itaw{=FEw0V7f(<;A+e=v12P}$Gu{fJz_0rJd^Nu^zD=6OK zb1}ADL8VJRQ5fr20!OW3*(*g!!L%o4Wd12#|) z1igeMeR$Ld3WA`PuvH(hfr23FOIX4OY@i^B+7h1O3b{xgvEWp1`2{GEnzVqumL=an0avti~4{K;7Y{I z3rpC%4@dm~{zA+gU&10jU;{V@F>`DQoAUu1zz>L-qf1!W2W$X;9%i0j!oI}^Yybxy zW{xaj-|WLt-|X8hJW*;|nUn_2IJcH&iqtTdHCQTH=%<~jTd}&mX0Jp7ANAZ4_Dw!u z133RMb9f2+jXq!lc=|B&w=7}b=mR!@Qx7wL^Ah$AK41el=P>g(En#2p12%v+4m1D8 zC2ZD*GkKuciZ}X(4d5Na%-^tt&G_Ip5bW!huxTH#0sL5)d3Fh#@&OybS%sNrmas`5 zumL<%n0a~$oA3b}zypPur!eZ!;0r|au-u9m39Z9Mm1=w1GICGxAO}yxZovtabN~6cnMy#WFWxp;-Z(}g@GBk;3arLU0WOU};syNdhR<0t0MeJ|anZz5Q?3=srE{4a$aTex zhmUkwl*QJ?nm_huS&%Y(T?LA)?oB{uV)qWUwc3=hqcm()i;8{xs0^FG| z)@SN%@3|hBfdFp8erDiWU~2=IO3Vzpr9|Nqg@;e(fxyT$NF?|qT~+F!mJ zW&q^%jxPNhL0<0ww*7p2{WL%07_`XZ?6@F~Y|t@nkTG916yRHziEc%EEb`_1c}h3=h5wX7GE6f2ihQ?KT!dR2|ZQV?5H`7w!PaJEryVV=jMnY4HR z9*z@%QA;IzzrY@0r8i8}>ZA933ZxezO`YpWrPk8%NO!ksPpE z?qrf+(@{NPoUr=+I}K8(gqn~#v`5MiRGc*H9!TB|D|Q+^QO!iVU^ZN-%1y*cGOtu8 z5J*s4>K?~Mw@2$u#^tI~^W=Dx;@MOyLr~p8b!f&~K6BVFhQ9FTHRdM^S$~ff4)Tt3 z{(0dJnF3gVjc`_(_gzo|e6iUB+w%0CO_z92dRj0` z_Llt#UUOnSIDFCF{q9YvM0yccv(d>{KpNNtWbw|Aj^nD-E5~Qa6m%@tsA*w>U|FVK z29=v3!7yp_zMXt0)1;Zr#H_)%-liv_5jUCQ$(V1kPSlCjWB33$(O|?Vr=+YfZ03^_ zxm%(<%AsfNSOPJMlwHC`Obk@YOp;{##A;93V;IE3Ell3XScJoYAfiR(AzqQz%59tu z9h6Gec~G>yb88`P26-n1J#Bw`WB$6O#smNT0CF5y;};^cN#m2xgvJMWM%{dD4REXb zvTOV%4ZEmgYx>+{>!L8mIa&ViRgPv84lC3xoIrh2lyng*`x({ zH`-*S7bSdH(b+#&Lc`?p+rIKy{~i+rY>(5s@2>MdSChK==gD7W&DP zCh$h(bA_mh<)-y!#?ukDWrJGTrD(er%~G)3{vs;TVQeg$L9$IMDa0E?0+*?N9i@p} z(>X?KQ`94-!~W2zv277`T?hx|R;?^HcBjWkD+Sd-0cm3<4In;GvlESqu|y?ir)PHFsa-&fC-VwQ42Az@+jP+9d#2Z7jSN6n0%$^y!qC+*P+%(6u z%Y$sj9)k^NaY_JJW8NLvlC!)Xuaym3+?b_qESZdEU(a{QE^IDLQCcZXahfvgsZ_zm zG^(U0dX;iAp*2x;#6)94&9nJwH(SAy8LFJO;CMnCoXiHjtP4u{mE%~n*Uk@R_@s<9 zie=q~i@NDkF&9GziV5mK#}eKt{^cMrd-+)h6i;3(@pEV|zADHg=-ID$*w+?hRnOf8v+;|LVi<&+I zImL=%gBp*Kq+Lvb0??9f)v!vw(siO{VK8V8QVA||-`1vJkqwL@lcHBEjHN^$vr?1p zaf{3vGfsk3&u+M7%FP}ZGqvorqNhnjL#*D^(75AP*}#-iA!BkZsurYNmO_doNlPT` z>+}sRh7PRFKDh7awuI2m{`P;|{^4zHJGC9&`oFin_xiuP{yo>l>#w`^xoeMKd*8M8 zwO3yK^wnR!I=@=K`qINcJN)^>M-EGe&pY_TgP%U|4&Z~s{r|lGWBca*>-TrBeDuoq zU6HRuFaQ1J-?;oQE_W`!=F(qZ`n5~%y2M_(vG=LH|8wshd*t4WcmM0|2Y27Pi|syp z=VLoRx#RABE_@XFe?z}={-=oWkDuS>Yb~DH8A$9ryt}peV_qz? zo&-KTc+Zo+z7u}YSwyk*FMxpP!+-FDq5pCw(Rz~11<|j4B=pB;5+&C>0+vLb&>x*i zl-NM@lcCTbo=LP`N@3B{PkbQs2WJv(n7t3b2Mzr{XA-TS?iM}0`*We+Ka=R`xxOK2 zLFBQakDW!7+0fIIkAF7&!ZV3B^z`J<{;d_YD27&mH_ivwnRzG(oQwJk@ z@_)&O&n8;mu@^-D;s?XeJF}+^+&%e@e-wW1nM51J^~wGZg`aaK(FVzT@*y|;b!QT7 zkh~`!a>CC(lW2qFJqdqH_*rKXZIXv)|7iI7nM9l9;o0vGUpq@4HcZ}!tiL^Tl~U`; z^%qOKWreSv#awEGJUsD*YvIE)i8jc?6Q8;fJ~)$T!;qf%-TmLf@Z~d!HVDxZKf;7Bok_Goh@SY)2g7@35^WHoC*J-C;oUQdHVDxZ)?b8o z&Jv;xllP&)U!SS9$qhpE1pm*&+h;MC+$2QkzYK4kNwi6bPQEW3K9gwE((e3LICLh_ zhNXS{BX11-&ohZOEbZfuL7~4tlW4=zKK|A}2>p*Ui8d_l<8M6*{oPqhyI~$aq&{(` zZB1-g+Q(m&34P%#<`Ns0_VEY5DfIa>i8d_lN7UDZK6@t72K(?4?bkw|JCkUGh=1hZ zw?cn=Ceen;`^dp0^fzY`Z4jcz{`AK}e|;v=1|fRvPv0E+%$Y;XnGF(0@IPx!49F`tTq9 zcIZzQ{{JrxJs!ILwrhWPZFcpuSG~i}9NGt;J}~w_wco$;i7WEuKe;Si`ol}#y!Wxa z_U`ZQvOB-MLv4R_y9&M%l)}Fr#sKj%{PW18&MrtXa5+e>zoP&N{LW7Y_{gL7ih}_8 z`;G!6w6Ybm&Pd)kUQWhiR%j zOr|s?38fh?HFL7j^Z<(H$9PLQ3Xp2h4+s4f2LVuz0%Rrh!-2ZuAOPx7fCPqqIOwf7 z2#~)pNbS_~v->^rsIuZ9K&rza6;=IkAg?$GkQFgV5mi4NNGlEkBv1@eVAT%?-4zD` zk|G`j$aHvq%D_h+6;~VtKn+qH)ei^4ih}^D7K7A7^}_+b;vhg)#-jkK7X5JWk5(K6 z$h3GAAbsNbiI^UFw6o$M0P0bIOpAUv_~sP{0Z@Zv&ho>-!z&J+v3y*QJo-&54xX`m zSdTpV(29c~39$mC4LaWy<(D-MF>gSuc2xD^KhP%qX^`jJOlD-HrUUtg?)=p&Cd zR~!UzJH1$6xknylR~!WJz`Izdp#8012^B)W9t!`n@HdBF8~)7hb9erD=Y!k7xbwE1 z%C$Fbf8yHV)!)1N6IZ>f`ODJP=WM+*^!e~(hktbK{oC(2{Ml>6!*4k(AHL||;|ITR z@NL(A`=EaCvi-l@|CRlBUjP36*8T$^d*EZ&`#UcV{rc7eSHAN~=St-A7l2Q|_inW= zCoXSY`sk$}yrf@x{iVx$zq9vad-mR2_O9)IZ1<;jC%f47y(K@Nxt98HYB}+wq`vhL-(kH#`D_c z!xcr*BZtyR80FEpSuSUDHf6-oey(aEC%Tc%}&osEwspjCBG|IEE<bRC7D9aw6h7J+eb(b$2`i;p?D@{s^*aO(6UR`sLcKY*E2wH|9|ka8oAK7 zqe(%FB?+;Un#op$l{pBn!xf=3qMMRbnih*=-RNnVtKYh!s3YRkbFEQ!rnhygV`(~P zJ3W`9XOcorqhmNRQDOtnCK6YT6-5Jc&6I>pXGC%`6b(3)8w<&vm}$iAWL3LC}vP%3|mm7U`b`?@N=s^Xg(%V z9XT&W1hg)U(zqaX6B!(*m`S(jOvkxVnZPQL(SrB?=5!oZEb2{CO2N%rR;ttDG*^N- zBp0i4W;YH`rRbnQLAE#}cK+~`0s-}kB2J|}wUKTWAq%6VJeLixuPIlq+Te*WI}971I?DRc7ft@C1hG}anwpPTrnXhGCUPc@DeZQB z?}-%!*W+9?OAb;XMZthj$=HNzmbsCc&1+<15RvK$F-Ln^beg#G+ou$nfzgXf8VEHd zIm4l%38_*Z()AjSLUxkpQbs9dq|g>!4}EY&!Gb8*t|QMba(meoMQt)_xMPp#()GxU zl1T)U*s`5~@Nnd}ZUq$#PaCr2mBWX5zG)ClidT&Vyzb)-_9CbH0_(kVr$WRJVblxXvAMsT@Q z-Ho>CW;Z<^5^7$XVXjx?xjuz7_8wnRl%)&`dmS4cOgn{qkAvG0g^Nk5k=CT9sO4Ig zX6da$FEQReo!xRfhwv?_8fj?70XBkJu>D zo;5_V21T&=#2QTqsB7c(TBB^_1-LV1TC!3{Dcs!q254IXQNX2kxe~Jz>3%!gWlbRi z(hi_5siRG!L~`9u!I2YMgP|-=i!Hk-wDTz|=X&o|= z8MW;e;taVfpIR|hluLEYbG$^N3wA-0DaAqYCCanP)3Pj#+@hJqJgLXH78ky}qA2k3 zT#M@_YRP6LDo)Y}guyOh8utcU-pC&W9HmZ)V{Ml-9^OlC_ zolz#DAuTl#9oJ~9k~B!R7K^$f8@DM*NcXLRl*%ZpE;1UHtBMI393*Kak2`H(=q6@A zMWMqQo)q#!cmg#nu(f1%*;5Lv0%JuLwaX}`45X4;CioP}4WvXD8r7n&LsF+lSO|+@bTCOlF#3N31q7_;kml`E=t*qP}+S<;k2Bvbo78yf`s5~~? zbUfvDngUbqRwB4K<>F~BN%WaGG-*z`-QQht6=Pa0r{fN0MsdQ+_Ba`j*AlUbCU@&? z4UO2PArT)&daW{i`PAx0Q+7+t*NfexYvXA=87)JilAyX1sg;W9d3rpFmxXF}Y-(Hh zYH}lbl#^0|-Xq%0PHNnqw!3IXOh-lu+iB&DT(U+q%8*bgk5^Mwn5DpOnv|?@3Fgu= zrM9W=kgEe!9*O51Nh&5Ac@q+%Zhh{WO+?gA&A3Uj2zdrc=d(pB=LyMi=x0|H zuE8P6s63rQo$erAPQY$)6!TP~GM-UhJc_GRhwAZZu~6UozEcW5QAg;km8%S56Ugp( zqpE=@?N%=}E3^tuwaoIMsB$!tg0?=eqHrecq?L>paK<%feMc(h`bpJbS|hoH4~r8n z*^j}ZsaMKo=zCTa)<_pQJ)7?|kw&A>i}AE-7di9^mMb48)$ zh7-*YSU%lDRg*F+MO!V7Yf`%+6}$>xqs*l28u{wDapmV$6uqpPf#b^J&-l2~@-!6d zb!#bZ(r0pQorwxLG?&p_Ppt2Laz()dlk7sNDxFF|39!$z1<6S9WCcm!P8J_UYBRjR zhri`0K6^a+D z6S>)q)Ji4YDb;Ps%){EHUkFU`^QRP~Y8A^?-J?jGO-DKeN1D@GS#p^et&P{|GPXm&Zd|e;d?JpT(Q%V$LR7^Zlq#f=KwM!i%%TH&*!X1|CP47=snyiw}(;3i#H4*zsTk(ibyVkIUygS?ik zj=C*nI2aY`SrW}lji?(@CiIBYt6tAJ{5aq!AXlsWf3FA~K78pDJJ-Xn2sWmF5UoLG zuk0cqYmwROLi6&k1t`ZrB+7=|C$SCLjuxSX_nX;k3{M4<#1x0DjkSS7z{NFxbzcGY zcR6=2sO}7~zt7BGcjGQ%jc%34{?calil)-Zdj{ikREax2Rz)O38ZB_N&QDq~A$j7U z2*+1(jdGQwnj(5=`Q-l7fhfb+6s@*JPnlM1*s?VS8OMb#gd{UUJbDbljyvP)10`pI z;_wzx$Qnf?Rf%%Pl^EAOL2ED$HKGumtHx*+?y@vjAxi$jftEvGLKrFCqNYBGWalPuwsDEznha{1o(!&DH#7tnx1mqTWnoVpPs~P z5thGb(N!IgGHQAnVR&;9wj7B;(d?pdY$uwDrXeU2O=lNPS7CwoNKg4if*T`RvQ?Z_MfCpb-Wy)Lq+&w8 z)JlzHrlLe~YI0JC%xIJ!q9TtI>OgC`F(z;KiN2hU)fsY9D#qFhoe)7JTg^vQfI{DFTbL8r(WP`|1nb#d@q1uws z?U+VKkb1l>cT5@l?dF~F*RiGOY_b4}O_{#F(k#H88{J*&k zhNl*W)o3O!fy=X#My-5XAX<#mjayYd=_Clg#n7|XL@#1BeI>S=RcEyhtG{(X(6J}k zPfsBiji=L3De+Sw$Pe8-3TpH2&h~ZjSw}KYcfq|SWV|bNG-2eqL* zmTNNj^3#|ghLKQg1V+(#r8z`W^jKy%x^j$Z)0mO!$z#nbOi?nCqpfCaCdbpGYDJqR zjtw~v`d=`xo`ewe2gGJ^nt0hfwKz?@n*e!2W7$MnZ4c!-Bq^-foHf0!=%o_)po55$ zh@zGl*v;FeO1$pJ=D4ON8J%U$sC%dOi9ZvzBxvS>wR8NpyV)9oO~i z%=I^3f9|zUfyn|9SO4UH#ds-+r}oHGTD2ho3n7wZnfI`t+f-^}ih! z4{siP{@}Mj9)NE4{4}aA|a@dg;MSp}pVT`{3Sp>I?NZ=Y*d;H`IrLSf|`sIUF`i=Tnza-fx*lH&=w zNK~~xMvn$}Eyw#-%T}(+Zn=!`B*HePT(iopFWY;U?MiKPRLD^igc8IFS26@+tzGy{ zt9IaiY-)Y7*KX?rq3tp9db@91wW~2>jG^; zc6+qeF7!joc10a84f`|O18=CKF#{{+TD$PSS+$cnZaS+JXi}P%a+93AK90~Yt=hFU zaaQD}+OVDX`lCj3yZ--2x8Cjp4{X=NH;RNR&J+z{`chvo1Zi4ZYu8@ERfGoH zCgoaf%(p4KTEc~JsoJio(K&0%7 zQ5o7~sYI={`^FWVz)ia7h%JK0_`=vKp}nr1J;)cm z)_S{dTn)F_W~3Z4@rDFF6fK<1ueIY=&0wsW<86Gzwnt3_ymSz2%^p}b!^bUrWD8U9 z(ya{!1+h?CYgSk?2OfR84KqK~r)ZujbJA*b1FZC%gkRWqeg71)+DWSJqU^M%}c zU6I$Xn4>4&q$+tsL>&(>q%d9Avheq>+TpEfrO;(tRN%xu<%{bQ@RAi=u7-1!yw(z7r=V!^h+Z3O`1h9WFir2VBcy1^vXGyQjJuWlr&sNS z60O$Fw%gWtY&@IQt#!QjuiCZAipE!j+>loVTPU+SyMs$EU2i#@3-30@cT2BcxH zwF|9`8CE1m-fT$pfcM$RU^#Bxn1%k=6&}o4!x_ks1SC&Ea9&-v7NK8TwnL}Ws;PI8 zN~4PvW-TVaZluCjm(z?+QJ!g|I;jCCkFtQa){Xb8R&c6BxjcfkOC^w(Nuz4R^nw7hP%^Yu0gcRytHmHLch4eGu*BW*JMOix}-@F-F2nhTG_q4(I}V`Tf(JD zb2eOnJ;Y{Opc==Z%C5~z_(Q99G?p7`mTY*rx`3O&0b#A( z7QAeSXjQLn2=K606i4N$lGoPSg;z!hQP^CwDN-e~fQn(u%t(W)2%@oxy}ze^BvMb*))=wYmr{ zYPCX_=ER~Y0{2dMoxAXNEW6FwjsuFP2s|i;z+l5-Ykd!Y&9d9vkgcJZGF+|D>xa&ius(| z>uF=jl6&f1rfOx z==Ev2wuFOC*c%vQ;{Rjs-Qyj}s(SG}PR=vu9A*$^n&-^Wh|nB5?+1^8N~Kb%)GL)r zRg#k7Sd~gDl}b{T%A=ADGuUW1APyoI6=Xm}1$=;dRRkX>m!BX40|SGo*9W4!yvp@~ zT)>~)-%jfE&{OC1No8np{QSEAn#uX@wbtHyt+m(Qwf9&GrSsin<4B@Q^wn7gEO162SEXZq{?#@vlb4#DtmWZ?HXsKg zH*~P|-EosY?7KC6#m})wEFSk|7r$SbOTqpdt*TZ{2yxPIV`=v$_45?RRg#db_p#g6*5P{&DLMx8A%p*(&<} z_tw+5yqkZz`Kz0+*c3O@n>TEHY2$qxuir2?3L8&Z|F`u|uK(it53GOhdUSm!@V9~A z3A{GY4IqIhuKlb3Cf|pAZ}HvfW4+Jwo%8vt&zGpei<5?&kArf^2 z%@Iah?$kOp9CiADf8mD6fu9cn3iszz@lj&NmM~}co5@y!7IL*1pd;Y-2)vHv6YDz@C zHpt*Y9y1*M@TF~BmE>j+VUQ$=_jRVqIfvO}AFtq8zJ`-CtpvUjSnAZWj)lPXaYJD= znDiS6JXB?|*s1hePQG?zR&JQ0GEz}p&>J+Gn{}M`V7%x;ymDRCjark`3pu3Tu9arT zR&RT!UarH$2+MHhFxOXFSw(b4tUh_61?37-u5wB2hYBtk1 z;S!QBfm(MqOmS!tzhAHPuP|qoz?|T<)*(p}3(cL<#f}6`)5k5A%cDxUg zYFh|I)O5;haN|DIaSop6*A{G9aEE=Eoem3GE&~p2MvY@O#2(>D#>kSn5`vdVgs>#% zvh1~$lr^fzbU3wIWeI8yr+mg4OZh~bUav&!YBnPp6g4Efc+VLP_+T5?tPMr31?Thu z)yy|J-WfT16E=>^&a`GOqZlI?qUcG#~ScQ52-z)15l+`Lsfbt?n$3p{8?}=KZFP>vah;15Fy(P|gp;NyX{1e$^zs@HK7&u2 zsFoYfSm4f`<9vie>apFrKRnJ4NhCYuGP@p@lmvt{$Gp@wp8gM+oI zg&m6vZn0scrt;;{h(;P}4yq2hnnSvCHrImMVf#I1OelJl?UzL7ocZi!mxhr%)UMOj z3N@`@2#XzaxL>o`W-3~{X=Y(cspXmy_^j+;ODxz@;0S3nQ!TbK8{|h-@M*;X^ZFO$ zs8&Ux%e5I&ELqT`#EhM6Z?MTJ8pHD(HPfdsUmVsor(gC}ZCq0&X-=-(FnO?uvMW^NVq?N0Q4_$U6X)k6E zpO7~){ZT&KEI@5gxfyjHJMGxy7zMnL=&Ep$<3+wbIQA{Y^I98Ml-pgbImqxBrj09^ zne(fGH*Vu7bvi4z@+3%wKWXXh=CNqx%Qh}o!I`n3Vb-`&@0AeRxmEMLbAcoKgP{Zy z0>*&Hi&Wn1mmI_F{i#KF$!B#4$T&c6_nBobiAMo63<1&?=g_o?{tT!$h&}{P1*#MFM=N33(43&7yCU42_^_TAOckHO<1uO-TQ_dm-ux2};rX1$NBW-X`-8Q+*0{BUwSfOG z{J*jI{(tY@aF5tKyZis{KCt^syFa-5!rl1pUhpS^dJqkMA^3aVxc8#>4(~hs_xKzB z=lLJw`-jb!Z+_oqYV%RSCvSWaWc`2LhOv)c^f!IsLW3h=CYl9 zlCbqXTOQDjtN&XJ2DQw@{FqCey)Ph^4(?YM^gt=K(@K?#OT!gAHd`95*dKGL zrD2G@U@Q+q?zhET8eSZocrC@*H+HC{xL3KH+dn$OEels(<`Q?wCGMiT@F!@KTN?g+ z(k1Q_E^$ZGwltj5ZF%xmK{h+tLfGzz@ zQ*sAm7Q!w4n&Y((%E6QT)4%5U?Q@ppxM$dWmwxrBAK{jL^?8qtTlUq*`%IU(qkb%< zchrxiID6ZHFZ8}!msG&@{hj{CKRy3or`+n!~^mPN0GI~eW`R45|){~z?uaMaGq{mAq)F^elWBLxIFE~LE@QQBG63vGEkb21R`wgN|y$go;$b0SB|{bHW{-!JB*xI5BU*qhpPy9 z2ha`2W7l~efJ1ZCIsT6DJ7j+k3NU}XYK*}5pZY`0?mxB+c_b7~rmtpp(Q+u;2VbO; z43_WG3en3C%ILWS$f*^dB3KnyGud4Be5{;~a1Yz;7${PX>YXX16pws;>1N>9>Co&U5QV@b2hD# zXscao(lh18IZ2~U5+pFoH>zWFjG>tAARo>mKRRD6Ms!P0l+u_2-WyZr!G#Jk2J+z% zB$Aj>MmAqmwQ9I$fv0krN_~=ID=|9Lue4_A^K3ZUG>hjFN~j5O;Nh8+r>JR9f?au= zMc3zx{OHoN7mNJVdv*nzyJp@d`R#g}`5JocCI2e!>V!r3TNx*=e8~SD@*5}QB>`P@ zl@}-#o*TolyeRXv=dwY~H$6)X>T0Pv!j4b&-;UuWld-EV^PzKfOHe8~1-vXN!ozd% zJ}y~Ep{W-lU4boGz5cjT%M$ta?BVAH>WG+{GYuZ3Ld7MJC%QElV^&z6GEiZZB|6b* z3y&+>6#U%gqP5U)vzvEKUt+b?;3dg*aO;EPKk|J4D=h1&vmc%J7uS?9q{ z$f5Hm=Hmh|spPB{%yV412cH+o3-RQ|5! zIGl_nJE!t@PVp7|wNv@KvSpB{4{4u9=2+ePlV>B5M$}Wuxbg_$MCzti;Jjzw^8zOx zxQfS}!XOfM=K{mP1)e#Tjx&5XvA>aU7`brsX>>xaY$_5;pSf^TAndup^Ma^TVWPnN zAA2ef=OCRdYbbT*!efqjIJGGnMJ{~TsXW}d;860+g-0LpaLz;&jQNeH@^EFUkx=5y zg&U4|IAFdKgXvJzP7Ljw?M3r-2^&Po?8Z55sAohrUzk zxY9$Ag!aq@uWiF&=gfq`9C%LU;YvrrvBa5|9I&ZK#aLPG$S0NK88r_je=peXDjsd}|#)B#6P<^W*GCH+G&JnnZBvE8|Dj9dSl!_pO zQ^~k9rBry^dxxjs`QY061M6>@SNhw(51a(tz8(zxS>Ww~aUdJG(f{jfp8*;8Uc6RV zyV?Kw{q+95-XGc5c5XSU^tZnEz}{PY72i|6|Kfe$c6DAUaQ8;L_TSd4w_ddM%*}6X z{^912?-`yC`hI)wcJDdQ*FpZj$fjrGzKwe~+P)w1|A3zX+5G+?_?h54gD(yif;WSD zg74b7xWlfnt6!8#P$(%bpn_D))<7bsLn2+3LQ-^=)oQUM5`*|LC>YX7Ma?8;?f<^r zqR7-D7Rw1%hK*H2Ln8zgB|UnmbS5O1rkY46j7zzx2HxWYe{ba$@7-8mwH(8OM|0AP z2z1QU3Ax<0vZHC7taPMEqs3#XLoU~uXgFF+!e}BLJM%SLsu;@_^@B=?NEP&v(&WQ< zc~IvfHKbO|Bru{Yg|Y-asT+Fi#`oJT!VN|%<3^_@L@S{DSu{79qQ;ltkA+P^RY^cs((>-Md9!on>bX;hclQ3=u=Fx{QpnDimrKKw-$Rn8;ViDkSykN+kx$qht=Fr7%LO zO>5t-&7-W);!`X0cwp6zKWo9Fsa5b)bU4Z7k}Z5>8532BQ(|>msv~%-ZRO>5Tg`<^ z2bxs7>6>1K|kBtVeMIz0rS%Z^2$q^zSji7v?8mqPi z%Z#yYITagFGi)QVa*O!NEn;>HagaVJkBrKcV56;+W)+)5IyoRoJ{=w$(j|&3W11PE zTUlPp~YX>JgNog5Ght+E~+tHJT6Qs8cr#oI@Y8FN;9>jOtfdhij);o+h1I{ z#TWL19(je;=3zk>QYodwwuEG(*{IMNO%GXAstyaBNtf~sL>NmXZiIU%N}!~uZG3R$ z79Uu-g__OwcIff6WmO_bq;O zCTn514Gw0dEX|6$Of^tCUn+~V^qxA>ghB3+Ay z&Do(5mdtpgaWJUVFg(i3%|mHa52dHG(v-}03)OKC@qcOM7PdtxEATk#c|4st9QMe5 z4h6LVbK|0skBM1P$|Rx(NF;eU66!Oq!-lj2$p21Ts<4o5qSZ(j&rhuWp{zCf^e82A z2gy{aU5TJgD_V*mO>9K!n}4%%i_fmy;;$Era}%W1EF}m$H4zhfPV2!#qG+lX=MwlIhqJ%tz%Oa5+m*HYA6VGRBbO_Zep~0#R%vBrM>%UU% z#Cl;hhIR5Ii61hJM7T3B2h%b%PVtm-c*yIAhD1rVL2BntyW1M$HlQkiZmU0_k0#Kzxuzs?e4Aqa~ zg-XL5ApN@8ELSk-R(;kJMA)FI5tS7Q(bSx|74E(VTL_?{bZ2tNPO1qRX%MxXrZ$au zLQ~?QWORLG40=*j|I<0;531XzLSwVncGWdW}76l50yte{wDudx8|LVzX| zX%S-*MK@7|c(lhgV#{wyPv|Pn;=iu&pGAj#EZ6=Gea5*uP1U$MW=e`H0icxyNQL!+2rf|%@i@&;`9U00laz#l zTFyNqo{7XTUe32lW|q-4HzV+Zg#e0CO5}_9VK!%KwE-JHFwH`90Hr8qP-zUpW3ClS zH%rM>pVhAQ6g+=r0j}{B+`h5^*LVt^w-8_jqStx~p1ZOD*LVt^vk+hf+dg}Z1$g#C zfCzICDe4$hV-SRlS{hg83h6RcrHk^UR~yuj8JTW0$*8RBXVdZ+)EMXDgd9Im5K<>d zQHe||#qc<7wGIg^84}q#rH4T_(p#}*RYdN-C%v)&Va-GZHP-^k%F`MFP}nx@sx(FH0r;Phl1;);b9rw$700X$?o8=*g;DMEDv&Iwpyp!hvc<=nsE0$p_K(#;YGpnw-I;WbADw3uIW>-yYG3{ zLVy(>4PWaiI9OSLYdi(dTv>o?ItBCn|1F+B^X&iF{%w1|y?1W+y}K;<5@&ThHG7*yam2-n@|pIsBd*xIgfswYRK2$N!7|+kKz(eV_ODy(}Q`i2FCb zw7cPBo)&I+{4ct-rL=37Wwk2>O0motJ#<#>={{@z#^zb}tCtr%q!`2eBKX#~!T-pKLp1<2@_`laR zx8twd&oWFyClV4L#wd{geasN?oM;`e33!rfHzxF92_!u>#b_~GV$LFkvS>!gcm_j~ znMOGij!zJl0`X5s7h^@H4r6AiPNUgQSTind+v0ir!nY&d3M^ZfwgAiX^YO2_X8GWI znN2_xsI?qvhLF(#sSVnfHZM3N*@F(SZ*4vNlg#&lWs`|wd88UT)mYaHq(<3(yYe1NWUiVykv&tOY3uS=Htw} z7N>4DKsn~K2vUkf`2^o77=1B27#bY{K@Y`DgvT+_>Zzr0j|G`^`-jQUh18{h-R*{- zyH)N9{FE&aC^h8p0jpG<=L`=uS}72+I+BuoscGRCDf zAUZHkkH73%^b?FzmMISJ8;MlBlmfLO;4mxu4HJr47(1x7FZlqgg`+FE zX7v#5=jnrjF~-^INRvl!zoLa9oJXw&DyxZpwNp6&Nh5f;UuTDN?f2Sh;PErAxt*^4 z$JAhjsg*Bz=Hkq!w&+@%Bkj*KaA{OH7!bnLgk=h*^P_lUAd4zn_!7I@4L`m7sMnSDJFhsNC6!E7IH<;vDwIo9Qkqm!3n95U4iyHINE?NrVnM9Q zu-d8DRymh7F5V49UuZw&TJ#gNe}zv6t&4X7mJ97CU9-GG`&al#(7yO$z-poWglkq0 z(f$>V!!HCc-f63W$B(<_cDnW-Q-c)_r{#;Yxj1v}FSr)xNc&ef*ETLr0h@&eVapF# zkCgue4O-#UyLE8_SS~au@0#Tm8nnXcZu_DISS>UNb2I><$0B}Qu*R&F3wzo5ZB@yY0wG>lE%d$V6)Jm?DCVbt835-$Ca&%1Hf{jL9lC< zS7^`*$8zn9CSbMDpp0u)57D3%4xoSr_2*MI*C5C>x6?JqdE0%JLoWHEF&Ag9K`(GE z&XESKaFS$P)B&4?2Hn2=Oy%ktw8A-4>tYYETxihqT(i7FgH||CvGf1E%@dq$y=wh2 z-nTvCw(-{mnvVd;#spo?#5HIg-<#&0E#=-muXd zf@XK#wmcitF$&kSF@0yVF-1d>Z~{!*iP@MY#R8TqmxpP&Kd`WLUe#c;ua+R=d~X=* zCzBasNa|x^n970lM{|u)m7X{pnIMneeEK_jUF@{Jh&C$FD}B%;dFF{UabwFF*m zX3*G_*Ah*$QBzDKp4ZWFt%(@TGMRCejcL(!I~&srE@xv}!RD@+w~2nc-sZ$?Oslvn z*V&l9Lw>`Kq6RJ1m}|&8X`0#(mPj34dr32$c%<0q9ckGNwsHFr1bC&k02~?uoeQD90&az(>FT3+(eln z{hq`^I6g5aoSvx~=W15GdA@WW&#*;-504_Hc&wYu;*=m#a(OZqM`0?5=~AOrSF}i0 zag}s&-gRJj7h9K}KR3Kr9D1+q$4-~aeB`*{onT5Q7>6BJ&lT-IVNqSM1TT9Bs@|KM zM{Kd`&JUXP)fVf!fc%#;l!d{6FUe3gXY+7%J(Wt#Ih?5LY<-drWiqvHF`h4131nR6 zQ@yIt7lfkJ9uX*(LsJD9w=DhPCm02nNGG9!!H4h$)Y7m5%v*9MJDC`GMle%UqKd<0 zy)lu&w=O;(s`2myiqkWeDGnp~HmLZT&Y-z8U+*?%;IYPdH0VObj!RvicOB^Z%Y2t^ z2fF@}R}Q>q^|o0>V}(jTmC!=q4oWdu#!5ye-Q!E~d^SWiATuAV<{ zq`IN;sIOP1Ojj+@blDO{4?q83Td44*G7BpyY!su!N+HgrrU?!d>q8$#Q zWO*!MSzUynLj!dvPU@{;)YM8=z1C^>sIJ0cAXi{j5=y184q2{NoO#!Q6>hJ$JUgDQ zeq z_drC`GdZir=qzNxZO9_alp&z}NJcwdvBFO=bl7Pyx2 zJ{0o$0xR?YHWF{id44LenH0rIV5Sj1GOI%ZlTiq?ug^-!gNd2!T4}Q#*PG$EOcSc2 z-}%TJF)DkFMhuRJ8JOxcP%A8iOK?4FAb1oMcn72F7%>T9%Sb)Nf%4^4aRAZjR52VA zq(tkWG>XK5Jr46REm;STjUoN6N8pIHuhEF1Sg6cXNVOa9*R)iK>F{mp#1YG8^0foB zXIfAj>oMX1KPeR{u9eY<&>=HR^9mboV0Ee{MI}bR`=w!EmA*%iRmSP^H5xG_9%nl_ zvcefrd1_RptUM^PC+QfJQ{-Bm$SeIukKx);bCv^jfZ2*J$0vm>iXYJVzOG7qrjTNE z&nF*&N_MRrSvG7|G;UIwQ5AY-M5RuASVv*J*K{qpPVD*!oG=33N1!*mRx2jTPpPULB|8QwL^==_ z)kXq*Qj7s35|xRo(QE>Xv0@~HK_fL+;Tl+ak~Vm_g=;BL+9N~G@TR4EKJmyKGU{4+ zvs@um&$mKSPfxH~M_@W+h^t~Jc(Y6&%i>yD=+c9{+D7}36pzO19k?D5B$3W1nuIZI zClwJNgusy9{|FqiYZYbWv|&x1j2UiN6Dqk7&t*mxEp)<=Wd~u_fU^BjjKre2)(>%T zSDMO}oYxEGs?kSkN}4X9$y%ld>7I{2^43i9nhjZPip@eHlCzQ&SDSEMAx2lV6NU^P zz>K1e8d)Se=ohqbsV6sQ93+HjP!ln(Wcfj(7={>>8Ci_};z!_!UAw>xXIM(*Bc*7O z&*>GIvPxOzq!CjI+)%WnSWXBfNlFcehebLk%=n~CA0Br4Lbo0%n_S<}6P7-G1diCX za{dSxDdi2F;IUY}S70kSst76soit()1rKXIDh^GdR%R6E+XtE^=c3tWr9Otby;L6y z_059}Q$JAj+4km>J*?+_o{fJ7dHP?sp>5p0al`uGuK(ux53S#^9$H@ud^+$8fx805 zz*E+~y7nQ@m-j!p_Qo}14PJY!|4aVg*?-&qY@gh}W$$11KCt)tz0ThA_io(%{O)^p ze|YzBH?kWD{z>o`gD(k|K%IcE?RaoA*B6^S;fu9$WIC8DP9V z->pk)imvUwXYV~n;H9P0xBL(IA8Td;p`V{gMP*tfU7w|=A=ORK!@1@;1$2`{T?y1v`mZMg*pc9mVlEqHCW zx!ZIL_U|@!8*ahAU3pi&JQ&M5xLeQaYAGMv@PPql|gS8~-7QDBe*iN_w z?{3Gp<8Hyh?bvqAEqG@;x*c^3-rkOEN8Ez9w!_$Cq``?^V1H z`tfC8pSS64UUmb^z}|-UK{wtVf#*nQPASL=mW33|`5J2@@Xo+HFZXX5cs=lrz&qT6 z1A(^&-s%>-7Wlcq&$$Kr13w%1S+`(c;Aa9qb9v;JbujRjz*{a8URIucWBn=XPjL%g zU%zerHn-rw`s3Fh?-sna{z&#a$u3-+zwyngfLky|zz>o=|6^dR9S1?xBc zH~4RG3*PYW`@au@idUC_*Zf=lZ-t;sz<&RR|65Vz60px7 z@CP0o<|Sai&+ofDaxz`3_2?0}3c!H8Dx9V|Jm_}d(Sb+11#bmz4BY4zycxJ5@Zbb4 z6JsOrsKA5!o@L{&V-w?g#h2viIkE|IgmNd&50q@2R{0 zzWc|!@7#Ua?)UAcy#3u9f?o>$kKj)R%^>c@f=}_@y7O;45A6IBsM~kPPJCx~`*YjB zyZvL^-ECz1NuIB7eRb=;t+#I7y~S^x-`d>#%;vi{e{{3Gnb~|CxOebJ8$Yvg=LWm+ z%#FbMU#`Dv{Z;GA`U}?21pX=Tp}2u}yB-*FEpq1-Zg#FzGiCc&N%^ zu~X@{3`aNc1-U{G!mG5|nW}>vXDEfD<$!q)ZJf%97Mx>xx?DE~1GkkycUi`0 zlRdGh=P}6|IB=dHw{gW931_VOl)`3xS;Z=2C+?+-VZlhXK2tQf+-|g`p46BY9k`+m zTb7txwqYor$7^+VG@0?mPA=bZPR4Bu;qs#aZ4$$ZP$r}coA_?k*;jK~cEL z*dSS>-beYY14r91C`V;P6CLrwr~)fig>=B4x`3fp6&;z<6zNX2ZZ}`g7aXwXFN8xa zLgw_W(V6m`u2i!#rl;^h=(jiksEDK17 z)?}`POGwq}xu0gk)JAD2!a1Q(fH7Jt6`jjo+veKhCYg4R)%dDWHkpp;R4DKHBV3Ma z4HQPoRJvvl?TXI3G~SerYZmI2mfWVGk~GTK%>FUszG&krnx1RawN|x5P1_?3$vDQ? zV+S<|*~-DimeuW5_+EQhtvapN^P_fmp-ibQ*G9EbmuwNNg^3P&cP`jwNBz8Jl$#Zp zlRB(9VVslU?c2EiB-b5Oi}j9z_6^pOogUe1d-$xb3Wy1TcUu{b6mYTO^cJ31+Vpxu zxd|1Ua8r?Ky+q1RgY>@j2*>61iP=CK6RO*0dBds7p1-znjZQNIp<~!AR~2f~rFcho zy-&AsY%XJH{bp~RVI)*1xnt4FYZo|JqsB~6f{Y?oAK_Cat2yX-{%SGIu)=35m3pb5 z=aIUl6LqJ2&$G#~eT3@IpvFuTdZ;#(oQ}X7waGC!!Egp!6SdZa&vu5egY64!a#T(? zID{$Zq*=R5j8*4nUeCL199q|kd6})%F;E0!T!Ng^c|8lZ8FMf|2vQOU?5vzKu&Q%i zwg)d`l>1ntJE-IYtY5Axg=4ZC0Z56HucM z$7VI7TXaquYLjasIV_t2`Fu62(Qmd?r~C2znoX{*3C$LxnDwbjwh#z%>Yinj!-c{? z&rvOvC}(R_z2mer?|-++p;7_q7D{{p?FczW5*m(Ph&C(>u|!L*<4ww%z-5?qcGiyb zARQWadlsp+Y8hrA!2`lU%^P2kgFp&j%BpsYW!N0$Ct|@lW%)%XpjxhPb|)6x$uXUJ zxm z!|N8jAM$%F7K(c>9RGdM=jQR>!|7Dq`_f~-N0X66-2468z8%l^c|PLx-{Sk4@8iCo z_tij6zpVEO-v8$9?ElIBukOEMU-TRM>HQn_{%-I6dvDkq?BV|W-cvxOzz25Uv3qg% zMZ1aJz2FyuzaM;E&!WuoKzY-u~?NZ*9M3yS<(9zjyobTmQ24 z;jN$9x^s)&I=AKD{GXe@w)x6UdGooOkJ zlV@qRqGp(Kz6AB0rtAH=1&%eU$aFZhT4f1p4ySy^=?c8RxWEx(sKjGNL1>hiiG(+r z$3o=~**I~A59@isRB@2LVT@W%59EEt!i>lYOt6&-Dp%^PI;MESS+CGuCCN;sXW=C) zH|vc{1~i*E#q<1v-Q7A&jIa!64s(5_l~u%JtLZy!$4rPs9YJ%1(Uv>4P7Oz$>p{T( zI?s16KRQ`(&-l0&Y;J_*56$W9=`0e&NCO}@MRbAJnyt|%N|vDfA$F1@3L*RDYhwmGF194 zXK3U3odu4n7iwKhQrZI%FVM40{aDcd8#b;WYc+D9F&1Wk)czIiSY-GiFcEGigQ*Y& zp+>;!0{~((mHw%pOa@=6k%b8|Rx?yqF2Ez)xggwbljFdS091glm!zhtD-v_eFph9^i-c67iBHS4 zAa!xnxlQxD&1MU;FxBYxRjnYXrQyVU=paYZci1op$tu|b-|tSU!W8PTl`EX=(M$`N(m+cVF?VRUt zY+S<>m63|-g5IFf+^pktW}Z(kCZk*zb)(iK^+FD*w`--D(^7`G6J zsOgm1;KqHZh60oN39!>~vVjav)C@J!%{? z$2T3}NXE#Lxe|hxNQAH?XQ1TyoQ><1XkE=_M1!J+WEbx_&CF{b*RjpoP~=*0P9IRs ze3Rpyk+A`|rscT8K zF{pSyEq3x9xHEJvKpuPDD|O`-+#c&Ap6E`od|Pxb8Q)`*t5F3Al}jzGW0i!QWFFfX z++^ceVmyLJP#y8 zq)bfZ1{IJ@lt<~F6X*G-g%&VEi!vz#6#o&>f?&vy^Gk)#CP$hYTkJ4dRMJgoS{X79 zlk|MW#^uz4S;^%ZWe9>exD7jZuby9C;EFT7uT!N-13~&KG3D7~i^j;tDO9%IYGE0u ziks6BiaWK>`x+ZpA9vv_sBFSc3lcbvQJq^WuYG=21ch7HS8EvOx~-($vvqF32|Np#Mc_or+ejO3wqovv1>X$3=A z?3g2M*laTut=%-Upe%AN*OWS{^ZYEcAV+~Cv(Zeo*vf2>A63CuB!_gK-&~NRS`~#Z z*JenuWI>Y>Gj_7Q!6v6@AcGM{&GaeE7l(Dt>2JMnvB@>XB7)-W9tY|ziQ}2>bU?PD zQH`1iH)>Q4D#QJb*+HF(;`y4*R?>4ho-hjea%05daNEh&^OrVG?6s&a+~awpE0hXm z`IrhAN4PfA>({#IwA`Mway9Xx!DuAy`BS?aLf**qNBL~C0JT+Mkj_)K9UI0d;@Iq} zaFOFhzCAehIY_m+7UgyqYYs9zhH2wUX6F3v;j#BO6s1mQbnX!u`I+-qSYzV6U`y*Tz5kp1u3j;5YU^wf_sgeQ*x(9`CEXb#H3xD_bAh zdehcmE5G%`&41qf;O4!X{mmQ@+||EltTEuNeYFG$g@!ID-0vJZa~9-9n$ZBe;b=VW;L6`uLryTj=9$;w4uwFxbF%gZlN~?^I&!5U9GSyfsO=FbO}@)Y*Y1Y zsn9D`31@)ieTDaxZlN#t{*YVf%e=2}3%%rhxm)N(@5|gmf6#l$E%XPx7u`Z%>it2t z&o?GY*!Hiqzqk@oI=%a&Kx6sD~b8exJ4We$Lo5A!E6a~5N znj$}LXNOE-G-^6eU3R_^Jm(hr_24tyLcbP#x?AY~34V`T=vRYJJAy7%!hZ~2_5(|y z|2ufgqW+)nUQh59Ti*4@fd9Du@A?QlLPp@y=n`_d3>hEgg)xhcgygVxK&nQz*NAEO zK{`oIN`-<+b{mO=teT}r2wGlE)=?oQ25O3&R1qe1Qbm{*YR{fr#Bfyt-ztVOuXFS) zyAqhQU`vA-m>HyJ=Jk#R&Y88G^UQkQV0rn%s3TOFx4N=?;kQ$UEIKFlA616zB*7nM zQJ5&WU=;$Qo?MzK#d6rWf=W)(&0;nb?s5@hdQM-FxqJM zh3P<`^7KF+K&@N@Y_W={%ld!wuG?kEvX{${t)Q;g%pqlw5>OYSjpF-l2BWZDtJP&gkwp8(h8&Q%hMShR}pvXYoAoV1Hw(Fx0xzDy79rzD2>$ja-4pd3)sKe=8tp9}4fhPz8>X8jvqS=ri zQgY%mmujYF-msA4wn!oyv2!l~$-!-;Fo2w@oBjN=(x$kx*4KpW(SglZ~g zi4n9aGVIIIRD^9!QVOX21&BPHnzJYq&BZIkM50^yMt&fS&Na2zxJ&g3U72;8DPi1= zheF9_2#eJEsn*D>Tho}O88I@^>4dW|7Ej{>8^a>yakbwN%Bp^z$+#*+7M*t;*x{wY zrOYDO8-dc7P-!?TNo}D;Pg<31C*G`rqJUI5WSFBG+l(KUvF@mqN)N)!HM7I`e@Q!B z#Zkfo6U~m3kPK2pm?|ph zn9x8BQth9U`CfIR(dQE70%ywIF?L+5UMAn_z z&AUFY{}(KHp6`FMXX}Mq&)E8BaBIJ~^Q7&+-~O$g&+p$G{LFrP_wD-2JoN?EZbho40;{>s6ay+r4q~1Dmhie);y+XABi9JpcaGi&c$d+}Of?PmYy{lD&ish{ybc|rH>fd7+r z$Gh3x8-t$-zH_G-d~vWam&?U=bo;k_RTr@9kn1C`(g=k84d2;!*Dwr9dpG_yo>kaGa_@4@1!Oa z+l`iKkS^h%!;aC3%=v8=T~IkKokPGCP{rV|ObH~g?}l16BOXf)$PkNj#dr@+vr#4H zW9=52P>mF!R=tVhB_xV>MJ3))nmo*h!6kd$95OkQRd9Wr8u;H}w`hku>|q`nW z1y4-j_yl3VTX&xEpSN2y8jVT;s+aUAmMFHttHUW#N=yd5Zsf2%;6y&gA2zKDHXK@O zPdRFljVI{C!6a5p<=RXLu~3F8hC{Ku&>l)rq9bxDgaM9K&2QQ*Vv(UvaZj2fSRkR~VM@9n4ailt< zLld|mGzsz$WN=vLS8h>WxkbrtQ47mVr%>l-^-;AwsKu;0P8C2ZggBy1rYc3^$#SQh zDG-?AmFyOLsV2|@221IPILr?S)Hq~_cKQIKpf7V(D#7dqFn9C;MK1wR#6c-%;g7(WO9P> z`2rlRMytv}JZwcmMVg$1{Ntk*nRuwz%Er=#P%~_znpLr2hV6*aY=L9(W_SRF2%0<4 zWySw?TM=21eZZJ?a#aWoHAYNx(6NDT5*?*XPwRPv$)FwS06eT6;j$G+NwmOBQe(yUb(;s4Og0mt2zbdY zPKs4B#*BKf92$oIKla`{>Uk@z6MVnVe)lavK?MrvRMwyb@%WXmdO zN0w|!mMvM57uiKY3dox@R558_x^sHE&&kX=O^0QsGtlQuIKx2t4D_U@nVw8H4QbY} z6=7?_+F_sTB~d(Uqx^(7zgEAno!+l zU(>6$s#a$Z;w)uuG*_V(;pmo-Ers0oZ4pEm@DV2lCEOTv3QQ())l4F0t0Fw5APlH1 zn478*wBn${f4D&~E?bC+6`@n})bkBt;;EAbxZ@>f*qkqgxYt6-k~D5*!eYO&Mf8Q5 zK8!07-Z$oPU+qA&0}R9mEs8f44qB1SE4s>h}rR9so+ z7J}ESHmAHR&CSAKMx5$fM6K-=R!eeZ_-c2_(ukiR7)XT-j&ypG5JIJiM2<1DsK>;~ zzumSY%oZRAcG)D!mc`bWwgLBRe$C-yY8np1sfD6Nd)UG&IF;U6-GX3&9z|%QI-6cZ4qPKldTo2OLS+>b(^rlmas95yB)GmaJ7J7 zM;0L4f&B@ZD{c|PJ~a!pTOy8sG()TmZ}b2Fkr)|;!J<&=C>0yGsNH^ z1JPTkXqFqXKgIE-Kc3I*2|QIR^YWN(GV^S@q6)M1`qBIgHQoE8|RAr-}>L=B0tzvdsvAwxZFA#PLRF!~-UJtRHnaZcQn9>@ALfW$F z2vL5w7+{&XD5$0C$j-*SZZ;SyER^Cz-*@}58%-3lF`Le-_0BN8T?Ka4Ke~}^^(p!B zw&e*oZuM}UE%0LqM_G{`IAs#*##LR?#R?;`mh0f6!n?$Ix_!GuIHpp|!V6@x&%wQ3 zT?_n*R-B|Eq^=}oz(?JX;49??e*T^<0%NIKGG*WdkV6gQu$)o#zKc{@UrKz>wS22O zC5@oIa?*%$o(%{#EkBd3&&^{ z=b0xJ)f(Ms!X75ML0H9E-xxW=l2O8SEr`xmH%3gii0Pza;B+wwMhm`XQz=oHL;=f} zc}c^O0$5|y+JbEs0ejHib{%BW89})QEc(sR^TrL#E%BV%!=Y)|Ce=kJfJ8d^!n7IuN)gp@4`VKdg5vgl;#OmN(pH(`!{Xp2br*|Il;M0&DT z+p*JTt7Q|B`WS}QS;^2lrGCqjhzX(WKfgu9a>MZD7Fv=5S&qCOc)?5AVJ00HgjUb0 zai!j?%*>Ek>9@9{gvAuR=n5E`u+4A|kCCeG#`3}}K_DYmU05+~TIq#;yXOWc`1<~z z+xwBd%OAUZ;?lWXv)>2N{U16r&c5*U<!Dj;w)YCC*0uh@kDji#X(3uP%y(koHmYCqc$Sk`{)l3yl<)=Ze z3qN|Y#z?Ra!A4SrW(|{whg1)DIjfQoRc(;=L9WiG=i|Yw%R~kQtc*vG*O=L$wSdd@ ztXRiJNvmZy$2x9WB^d8SCAs8kh%rKKm`~=dMeb|hM~~JRs|FUzX@qG$*BCY%Gq&4Z zP2%3D>&+QsW|vltiC5#3rpGI}uZABzTw^NziQH}#`hpun%t{`v8Z+1E%;kojO^}c; zb$F;>=2VoM1-bXZj~=WsJrEWNqqUjOlnr&9HkaDS_ZO@oz=TPf^)3~YCb5{>VaLsx z@T2={OdOEia2%A{iO*Nfme(%Z5L1>@zSl8&C9i|`&G`r&sS&YQWAbZ^I+^Ky3Xl3t zYAPbKEUJEeAj>iz*-f$S;XYdHjwbzizu@LN@T0jkCM8QC=Dj`QW7}GKy_HpW`U$Pb zLRrxVJY~bIh)=3h(MzB;=Jpy>1A3CcBRyH^w%_cqDh`W{I-U*~Cx}A2Kq6sGHj^U4 zFLDO_=&dzoV9zK0R)8P_Bk9!2EkWn4RuQ3EtbrOsbCICUp@XBTxMFf`_|d&JCeyQ- ztpuSA-kK&)C8jc)RCshD7i&c&1qOi6RH{>F=s--a1z&yr8WT}gn+*LqxiTm~QL_|R zL{ce0vaAPvvO25E!@jw!%FH;ib2@zWb!*JDY0Z=p6PSe*3*&IkkI8Y{S$bfLrkY^q zj?IK(1U6(%3(9Hm)d$v?5=A;BQ%temh_?0Rf=~yr*D)}w>1lSQA3{dAWjnKGIMs7a z`08ud7$pH_Nw!k!BgUu<465jb^njaV-C{vBOty%ZTj&Zz3@hU(r@~k7Ut?m@9f-=% zPPm4d5UFPO+{(~xM`OY2c#XQz!ka;TYI?nlSz}gf%tA$(tWq-pj{(KFv-Z;SXOkiy zFssH;Zdn}6Pl}UCsXb%uoC04h*O=A3N>)V>>WW1yl$B>X-mY2&I&;Ovc-UC2>~JoJ z1B8irW{t_#7S!&i$nrfo9? zt}y}^mEff%bcgQs$TIF#B9rZws{tUFgZ+LvDA8>)C&5?qHKyr|J$Wkk$XcV}p{2N} zgro9+b+MAUM4WC!!#HdB7{)14PK2*!Ys`>aFqKtlSeO^4CU4CKP^;SQ4D6w^sB&OS zA(!W18*ZTCz|INq)pU&+D-*5T@~hqIoF9OFiq`EDod8L+5;An)6<%s#VkhZmrWoaT z_$pmvW(fv3zXsH@KdF@U0yzs1vRfrvXlNNsr(9mO(LjX3R?cH`9DJ3mF)-I8xrt~t zipx<)Lv_rAmhNEC%DgVYS>V{immnNBrtT!ovG7&A#x%ypcr@=1R})bp5oFwRVFsa> zY7iyEZk2DKUTLN-z-&^6atwSmSz`vU<7LZ2z?VnE?y}ej>dItRCVJ9H&A_hJ8LBRY zFR5j9rx=70hL5izg70{b0*pj2D1m=RJL*{DmmAA$;hK))lAS&w=kwz@1)p3H=6EV2xK(faygYO3z?kV23es}c1&pa&YP5S5 zIRWbr0bdQ)n7FYF3yFgpk`#?ummw3}QmB;y%kYG+h_D|ny68X(Og6|p2w(Nr7;9ki z@?0b(D6%!C0gh)l-{kbF6lA~ex`QzWGVBe>pyiM4=AvFPRDaH-Tn7=T9*8I`FLGZAUs-F6 z%Molm9S>Tftud2v9Iy!7WZNJ$ zxG`BaU>|c`AJgD*Ptc`B8BC>pS_k8T$$dF|)m>vWVu9CUri^(Q)*+0xGm*WO1!vQ7 z*(~9#u;3c)Mi7>X5X!w5zWV!XOi%BV<+Pq)3$!1U#Kw{xglYo;z2#?0YbMTQc2&lU z{kY=hD)80UtTA1hs-q?euAVWK83Uskq~G?#GCl^zlA*U(PuoE-*EWqtJfH} zM_H3TUTZCg(P-6QKw3EKb5klRalt}z78F)2HU{WO>M6N0eD%IHraB#u40&0a_xqJn zHbI*K;WIMs_Lc%L0g_@ZH4{v-%EgJW#+Yl2M^SAGZ_F~D2A5(3LK~OoEe1l&ng!@- z8Xnsl1=D0zsF_5r1YdR5820+7S8I&1#wbcZ;!C|2%!nO5nkzikuoi`8sg~ixekOHQ zK#-7FMY*mjISjsPuQ4R6SqUOwRfa5A`&tzShviL?a7$2Rp|WDvm_)TDsy>2)9135x z))?TJQEKx`Zh%D%4x25oSz}TxRD^ytsh0>buA9gJB4-65NWoddmA=N{R5)&S)J*Gv zp@_?^7(BaNBD69x%ZwG34mv4AmkAEN?T(Ps4|#p9T2z?a}n;?cf z?t*mzg0Ivy#<0!GWY~?pT6^S2ecLyb1&0?k)im1t0PygUwc*ez*XuS8ZV;}NHO6s7 zor@sHLyuSpbMs%c5(&yN?K!DQh}Y!xu!7-oLMby2Qb04s70T0{8DU; z+J)HTTeaA3PKDf;!B^rM<1>iSYpT^!p-57c+Uo|5Zl?m3;^ERlyWG+Q_Yq_%VMs&C z-49<0YYeT_)EQeyd2^Z69I+C?t1<1J%zG64+p`=mPZDvNvA^Uu%5%_}%Rtj7UDr@u7Dy_if zF$C8al;P&S1ioU{7-d#O1#`&MJM%743C)SBw(LZgs$yK^*mgfj$=FT?TALwqUkqQ- zYYc5^jy{D-VzY!osM1$kbU!Rv5?@29v_j1a@`|E0yFLvTxi5mRs5NHbv-W795JYDy3EjGq%=7yC>J|39LjwmeAU=s2*l&(O~B$o&`Z7~(fkMlX6ckH zmNFBZP}qW@sG(%)Jt6l6@D;hnxD&NR%UrkKNPi{jEcPg+&V>`Kil9u(p*=OJD^ZYpH+)rFV{YKW zoVy3Us;)6N@V?Cb9r%h^V{YKZmizpZ+y7$ktMp&hmW=6cOCuW(RbbY)1wLS4fxT+4hL!gYP<+ z9F+I}Joh8}Kf3>+{pSAX+)~!EFaNL0Z@tuCZ_AJ8`=1SzBAj*x9k?z?4nHvBrqY{C zXdK~Z!dOp@dZW3V+jFRhSNjC;;6kv{=^yTZXqR!+)Ns={MO1|GvP#QqDe$r~1Rl*a zJj;;USZ4*YiEe>Mzq=7dzdRczQ8F$n2h zRZsuk4j6ePR_%l90HRdxfwTPH#B~@~PN%aa2D>p5N44=hHn9q{llZx>w$U%Ndy6Gq z&I&Hyu8kaK2|O?dCA=b;V|_BiJ4Qj6RYUXDemn-l;(K0iVEhgIY}dvjRHYVlD41$B z8!rok{=%Pdb0xOTQar#keY_&ulC4(Y#rEY~yu1Te5WUWatqw|~LET5VRky7(ej|*TZB_UFr&p^gVouhp zmAo$1PK##I_v}K@Sr+5zRA?>$nNg;tmD0pWqAl?7AH50$Y$U^0Obok93?|PuKB@^N z+o||GWHp>cT%&l*aO4Cdx4^RpcEFO9ASm7-M?J0Pj|156B2sUd`Fgdx${M8>*- zB^)eeBMxaU;8+|DitRXnhASZ;OIbiAEx)wnTTVZYjiQ#~9ur6YqX1c5DFTV;Dd4D)5IPtZ-M!*-vODqB+pTvj0)XyeW{4`HrP-v#hSRvMmi3q89~yD=Jw>>43PWs z9k70XfEt|#!1-sHk_A$q`$O%1pTzIKzu(UAsqQkaoAd%q-;JD;+dE*bO(WHbXIHyY z)rZ-M+p~ESf`>NR)A5o!?&|%Xg%3$r-?j05uL5D%HqkmdX~|mbOA;xMVv;Rm*(6Bk zUZF_oQ4{9D1@dk}-uv+#u%hx1W<+kaRg+dhWsp=#!0nqKiZj?5_d1*~LI}(23ANqJ zIoZvtBHr_6Vp*beq$ZXGuPpU+m!s$fxc%*u?P^d^n{wz?3&Y(kmwOe66>O+4&X;~$ z?e?>VOM|11nFGfLaGtS81K8~K!E!X=ot+{do$jiN#yxaOG?~O`EElE&IMgOfVL-H2 zlrvdoVOgn=WuK*5?yCcUDuZS2))$IJF9xwZ!v)VbU;!E;^I{LeMX1=C*4V+wX?6Q- zk@Rg5*$wh9-U%$q!)P4Mr54?!z)3%(_Gd{%3&Nar#7dXJt6il*gYF1+OUJFt9kAfV zfZ<|VkxnMSI$r3Yz zn=3MExrW!aU6NE#w#`Cyo3eL$eDcFP;IgBQs_i-7?1W`V>@J|XTcaJ;fV|#x5b1Tf z$*8g!SGip`-g?gtxbWwtLf~sNrz411&ttIB3gS9s329-42E|079YVc?adrdeFR@Vy)5HhCXoweROPw;q}*@8-EbN0*45m1?|>t4CNgk@MpJH* zrW=@oCqQ;?#iawNwF?>;)H_2p1NlTH2DA zt-?|p)AJQG0O@=-Rn7g-4(Lx*)HK{as*K2_T0^RW7h5BmF?vO2VM|))p;ZR8>4pfxa>`oL{&6p~JD}OVzDRHOArJX=;Av;PBklX{> z(7Ke&`wUalDQ!?_n;rwTF>sFF=u#`JvQw)4`*#ApgyR{1&~Ex%N^0PeXIY^#eL<&w zt*Z1nXUK$=)}j;YJAu+p;PrQKyH_LbfP>Ot5m%$hYFsE*P)K!^mFN2`Q*DralXU5E zK_ILiRBrE9)qH&iv?5@|O_ULrw#E|GUs6NEA;T2$vAb&5oCDd%0@bmaRy% zHq=M;mS{-zR|RyjOeig&nXpU>ap`K5PbFpQ|j5eqqR}bA#iN@(96j z#%$uQH@6GJ-NJF$+yV8Vf|aGXsa4v=$~g1*YF!4xc|xyc>Qza-N+g5fD)3Zgx~03t9fZKU#^IWUu@RFv2PW)E{xQ6 zhouq(I1jzL0380t4k)BE8SX6m<>ee@6}wetFm{>DyBs7FhO{I#whp_9DHnHdHUFyV zD0D_$W=s#eq-&+Jy{vL@G;mrZ-qi}M8=<4x7zq~*vAY{NxBZ<&GEzl0Wl7eqifDIL z#V7^gU9;yRVVNGyOh-nkiCD8+yBX);8+O2vZ|W^0X!=v9o0@j9O-W2)+Qfh@C}stt z0vW(r9|0+nc5~eMYzGWDx!H?UYG^{OIn6~;TO)^ayeUtF9@THD7&=5n5L8y*S-I!m zu>w*4nNM<|EZ7q)Uu?xTD4Mc)}ZAPr`qO9sB2AtI3bld9DeAzd!#$q^{bu^De0D0HtPNzAXr-)-XENppQ1j^%*$SY_?n?;#rS~#uK?Jdg#U0A&To|biL5T2wI;E zO>(|sr&A9tIGurCXdvWd-W0*9FOHZa_u_N5GC)Wi+=GgMg4(EB++qfdRS6w*ld;{k zG`mBcqIVY=xE`L&TM2Y~ zutP-p(#8U-%jt$F8mXfdC{#uUS*2jHW2)U^CJiQ#vIaDb4!&|LqcH42XstNlYthKAk@l)PQ?hBPQL1RAaH=r% zN~2gTb90{D`?hV%7xFA61fkb*N~1b(bOe`8NIGlNqS{p^iB`o5)S~X!8zmzDc{>E4 z{05E+HK*uMSOVzj8_g;WjU9a;0IvTiBYKgI#>2VV*@YNz^Ij<`bc$ikT-CI_ z&l!o63_rQrB4V*Phi0rUgqaS^zMA11TCjqoaojRA!&Lh7aWX4R{3)~dKkg9qM#cbR zA;DJ<)G4=IMO=Rw3`DJMRM3W%i72=Zv6@9QI=Qt)fGnGat9cy8th_*@4NZ&Zo?kWQ z#%hqtQ59Tkhtnd8>-KGGix}%|l!2f7qCG zPQH7K2&xkn_^~Gj9*(-rAyFTAxH|xw?u_Xxy(Kpauna0tf!;agw}{~i@(s8Xb3rkP zr>lZ2H3vPcgLs}j8_Oa%geyHYPH9B0%Ox?J_jiZQ>a=9%hk?_xkdPL zdqQFZb{c6-z2XPTa8Qf&z$>}B--lsoXm|R}7)FOs?gzIB4_EX>3NzUxGr3Tys#?|R zp=^JtW<7GYoX=8xGD7rV7To&gEn;9;SW+)k!4!ZjyQ04tGcp>-=6Q((HMgbd{U8Owjl9DTSkX9$Z-1z2mCS*t6bQ!83N(4Rx(wU|c+2yN#VN5xw8tEh%1m2C`4CK@ zEpOf{Gj&9n_}EZT6};kdlUZY)6qLiQ-#n*B^w8AMN+8Ua&XPfg@yaNalA%;^s%h6M z4sAgZ7WL6YKl;M04Ba$*O$JWQ7UOUa7GGAM#4Ho1jY%MIhRwjF8 z!x6yk()#{}<0lJrXluk2^W#hz!EkHBx;>IgmpudZXByj8`(b2euyFd}ErMCNAPu6t z=phwW1w+f#dL*_49$2eFVWp1-tZbt-EcSpxZNpNczMz7GJscW)zfvmNitwm1`z3)Agf5D*)-9` zt!-2S*QgT}#%4Vn+?BFm^x%Wq6mEeuW9k@-t+7@!IC(w~Ta0kLL)0Uz3^`t0rz6%2 zM@lo3%8utDRHI+2R!TnEL9?n)!yPxj8|}bp7EtF0CQM;r(5C0^5D7T4T}bJ6N{oBK zphoac#|Tj-_Y2#0RGXfG8PqDM)hh8a>+$}$h~QHvteDKgSqb(uhz%CZNW}iv?-0sJ zoj3c%xlo}IqEXctwKqp+eby62&F|qAXSE_aOg8M?-f3+`?8+ro))N<{+b!7_tBE%! zbl@($MA&-Ci@MWhq29od=wNG>aODosGZLolRIRDQmkSM@f;y26F28LgDYsk~LZ<9M z@`CjCHjaTSbA_3XxoxE)l|={xnpGKqEH`wzk?6Xq&Pgz&mosK@^2`=d%rF9EP+9bwnT=IS^Gc&tUd`i@ zU+%6(tWhN6{uHV;{VI93w?!Z|VX9(H4G)|NLU*VZ=LBQ5R!@sfwWjyGW0)0ioMbrn z^nchQ;F*lKOag4rsbSGqanVUoe8MhkMY&X&bi^tM3Ys?4UPCzLb_myNhE^F~TIGf+ zh>cjCR*H77fs!+jVh@>;7Mg$^H#bd>zG#cM!Oi$K0FZ;4)?yeS-6jauQIt;Cftywr zEf>{b+*&dbK`;^zoQE@RakMiT3Smn#V~Gu%XpD>{;6LaXi)BHI7i2qF3|*?=Dh1&5 z*~9bOnE_0Z!*B>(;W@R-MWK@wI1I*m9i}|c$I5b{IDXsjRL%OTliS7MtiTCNrDajT zheED`ONC)dbCm$C4g^o)ia0@twn}9PQN{EBc++xrMYmEP#|maSIqbTTPf&efv_!_kR)gr;Lxeflg;&mmO1+QvGOGoS zZAPOpiq)$Q0#8zg0q#n&z%QeU(2K+paXQ+{m{#WP8P_Y#Mcgi;Dq3g_@!2{-BV8>{ z%{e`-dLR}hmF5EDX2L_$Y3y!zO zE1SBD<`}q?jx-tRrcDyu;kOnuv&;GR(pm}yvPB{MU`23m zcX8fhh!$|P1^%$5Io{_j2``m2i;Ipvu&vdkRRq~_fi5d}ic}Xm*5&1Fsn{BC7_DVe zaDy}`#4A7@n*Yca5zPpq(6K?>rsBI@2cqSr-D$_=_FOH&@=7hC$)H3n(fZ=#w|9u^ zhb#Zs+W-Fx_S`+N`@g*S=NDgj{{Ni&XFqe6oSmHhlhgB)?*jh)-*?O&{nU|g_-lt> zeek;n-+b^n``^9)VE+HkYq^(m_U%8q{SR-y`_}(D4=TQg7hL z_*a(dUVRpjS_JQNaec4GYfE)!{dC&3Y`dSP*3NT*2W4KyZpSG?Gc)d4p1-*7%URA`q)-r%1-oSPA-JUz_ zEEbDln-$$fvXZ8Erm(w15$~i;ORp|>Qg7f$`d60fUR?mGYel?)Tj{l>eyk#%14(N| zyn!?7wIzMLB95=lcJglE$of~1*S|Ui^45xf11H#P%X_WjpX_?<29C4WmUD;V9|KWq z#lL~S?X^XHV#P0goZ=s?>jGXcd3`_LYu9C~_+X|x+%@#`lc z<44{PWUPnG;QD^>Z_e7g8@{hMWL7}l##X<+5B;^}y>`efft>Y_xq;vPwdLF~WU_TP zfhGF}?)ujj^@)efoffV2$YNa=(Ciz80bIK-+aYs){E@wP<-TX{^g}0qa`KN)4$ix| z?>U|yfBMk}kKTFsFAu-!;HNL6%l(Ugc2PM0w`YHF_TjV7J7D)elK+kTH|9TMU*CJ> z^aoDwJNcoL*6~NqyL(^vM%({4`uzAKx&Qu+zWdwH|Mm_1?HhQ_8|Xh$-OaXbz=;hm z=p?Vm4x3;+6QjYAM zypPTS?jsE#XRRwYV3@vkyFSL^I)8**cN5T+8?e$}Thu4k737XXlI^gmuj{jZ#X&-u ze#49GMkU~D95WfO<{-5j2I$hvfV1HRi1h^V+~q8|c70wo)N4S-df4liLhBR( z>7>{do$km3+^=a@9#QI~=A;dwZrI|mD_naj|9C?ktRnZkW+nN!KR$b-91faZac zCBr0A33iZ8ljWGzngL+us?8vWHxbQRux{q+WT;%bLZ7%3;MeVhuiAA2RS$agpd}cg z=n={wA{0HS_nVVp7!2lOthi_ujRd%aFY#;FXSdrwf8_7&8oe^`hG0$u4QKmXHSe?p~)#RStF{kRC3Dd8$+b>sMr_rDnzH(Qg zmH`t;L3b1orOHtj2$pK}z|n%5D77TTr}J(DAG85cE|CatO@(miV=9DH)~YamLU@C* z@;(*k3*4@k-uCl_^GDcqH-Xjeum5~ut>P!=t3CGCd-l$M_MATdtg~M}`@XY>&&0FO zJ^l65XHKsG)8D-(FN6L4hfd6s`;Pzc_(zVv?bteoj{f}U$BzEVk#|%&${+rR!zT~J z!>>3zIe6jVI|0{U_291kpWFZ5{nb9b|5^E8&VOJ2;k=mt+}y9{p2=P1wA{Uy|Ni#N zw}0sNhi;p<@4NMfw|?Z-w*kJt_w3>i4ljNJ_#vbh#Kq;uEmNQHgUiaE^9g_O7JRw! zukcX2ibaYM#s-1+t+rox>`+Q;sXsARm^>Sqg{nsTr77g@Z@DOMg4ptp-UN|sYXzw+ zHz`VlIHTebzf2InS&cbf#_g=Ig4A%?)Kz`rj#cuVzj~H@odHW=mWqEfxW6{0b`j*}V@$Ox%t}o-BEh537@ha|(iP~~F zn6X&jB&MA#TV(a-bm`RVWk#+Br79qiy=%*5?A`#;y|IifJGdKDsaUmjM3>FspdklF zB+UuQR~FMQ+_GJ-vF`oM!kuQ2e<9G{%QWfvo}Wk@Qo2$?p!y7mn&*H z?G;*!BE6W5=yoJos}cxsSBD^X2FuQuhRn?&UB6x2%D4ey>)C!28SqxdST(r;G%InT zE{)>KwA}OoBT;WwhD&`urZrlHMsiDRDaz61jS&}HM35|-g9g=(qj3kF0PaPSN$CMK zW*U5-o{WYD$~v6}XN&3nSKJu!m#8EyD9k z5-%eKQVARf?}}=>YV#sstWg+mSR(9cfsUI3$}Ky$S2sp1Z;Z%xh>l$-DFsnPE6F5H zW^CFND#9Xlg>hE^-@%bU`86H{4By)N&b!|A5kI|?Q3LE{AlX<#^yl3PInhx-Le^_0 z0t!w5T6UYJ9kH&*j0Am#cVmRRMYzjy0Hyuucnb2(n360j0*$#nkiriTAEw=+*A_f; zfvtS<^0RM@xckP4ckU2UbzrFTy4XPj5DaGxh60K97Xb{9kh-kPs}NEusafNc%b(pM z2A&$b8r0$tkK~kk#51#DT`B`s8nVX?W7T)MN&uMbtI#`kJvi_{LI|fJSSd0s3RFB~ zd?BJL+SFz$1Bi2jalhI1Cs>g<*p2oZAa?8j4G_Ea-zhIZXow&8hCw2y zP~vOzS_69DO#$Xj0iA6luqF%G-4Zfl5votaoS~vfwZYVoR?k(YG7tEO=@L}{$B9SZ zbYsLf-Wc%>8-zQlsrgT?sR=o})j_jhambDbx8(HAl*wAc( zI?yfO(I;Zd@MaQaw)qjhQgwUtTQdZ|MKBLUp)Ad2&2YKyU`3EMrxa&(4(k~cDlYa6 zI$R*L3K95WZW-5Vy%ppAly-l6+$*DyBEm8>(XeR4I zu{WN?TIfh-*hvH(P+I}@xkDwo!Ky&wov{FjSO|M0&qJ`O&?mlcH3+V0$)jqpLPouY zM1bW>X~_+)sY=yr_sO^41}gC-A61DvUHaapxBo^e0XPxf8kI1cW<#XNM4%f3$M9Bm zZMsr6>O)>%CB&q*kQT~PAc;xBSOpDCT;Qs*B0DiLPdjv7&Dfc>Xt4?RNvgyne*skD zi{DHo?hJ5wn>yu_QVDVO7O6zdq&Wz}gvKZn0C|&L#4I~}J(Z}I%LO(Hkug)rdX2JT z%*UzQE9h+=n%bRdeNu_qRtJgtT2F~&%>-FH1Q4ovZI$TW1ytgTKB^LTx-7m;Z~u){ zV)a(3gjDQ63R0okB-ZKEYC#HB#@^Zt*H;NLtHgB;4xL7<4f+jQpvQwDQVnnt^(5Vp zRIxBFPU@4)Ge1d{kiP<`#23DqO57Rx@iukJC#Mp_w@4)@2-3!*s;Y|}D;Nzx`m0P? zLa(z*5Z7CAwuoAis;gC9B*|;{$>-^yPwsuQKKT@_=bv1k zyj9i|)d6R#4k|!wY=CHWjaJ4Y{d!ABZCcdJX#_IGdRgCt*rHOwA;4C;VoD6@H&*qY zplEf&(|lNoT5YAK$U4pP~ijlj;-Uty<3M5hjM>k`38XXB;6N z$S&$6{(5?%Na_$R44_P_Lb|s zcJF)dK_KkQ?zuOe0y>3pH%dTyK0kE9=QxUd;QjYnv#Hkw5$)kL1)sg|Yrsz@2{=Fh z*pH^mNgsTD@6HQu>uPr|`0bjhVJqg|HxFxC1JZs7YOsFR8=Vl{If1WHb`(xQ6#?x1 zTaj~+ki?I|^>fh857uN6(aoOw0q|g_Kbd;qDKPkZ)9!Q%NGHGtn>VrfpKj1!uH~%7 zM2YYE8>yqnwnko*PT#u@Oa16~-LwyAh?DN-b;5x72tr|yy#mF`0PX>*+5Y-HzyIDX z$?N*LPij~t!02T%5&Z74K0{(8n%tHYRPnqB2H$2;H2qsB&uZmYP`I^ zIIJ`iV^w4CuS~QAd0=b@Mt>=eM#~;DEGi7cPX?h|bQsvJ5%+#zU7uOFu2FwI)WC3Z z`>)gV4}2i^X#R>`kM$3|6O8q@GLG$zv5v0a(CdtK_~Xa=y*CLwTleryBDOZazkxT{ zy|1Hr_ueFn&E^4j=;_D%46m#8TAl6lX#!~J6FM3#YN8u7?u^o21KwO`?_A*5sMqT3 z>r@1cN}vM?7&oug!}V14HtHeBQ}l*UQlt%usO0dnCD`gF{;t1O@m>~uilS}yA7AQ5B7AKExQrOjc;301O=C4NL8 zG_+iiHMBkioN1-Gie*i!?Eo&)w!@C%v{9p~i)-oOy6HdSqQw-zcjPo|Eg^b=zrJtC$|1} zXKsH?yEl*f=y$KXvI2o0{e`{0iS>cC+Oo(u`;oRSt)0>CCYHSxl<=)QO2C5)H!t2g9X2jDW z9SLp)GhX;X3M4S zflRDg*(Hm9k%k7ek{Li7aLR468mS?CHjJ0WS~Za*ZO-%jXb3VG;Lt z7mlAle(w0$<7bYaK7Q)>$%}_Bj0^dKzIgBgzkn|8ySV4#?u)xFjxP4jUpasI{H603 z&tEuy{`|S~XV0HGfBO8X^C!=rIDh>7vGa$|v-8QhcmB}1aV~>Mg$K{^bLjlO^Lx(k zKELby=zQ<&m9v-6UOId6?1i)EkDoYx{P?lshmW)4$+36*(6MnWAJfMV9^=Q*@qNel z9N&F>*YVNu9>`Ph^3h92FCM*c^!(9tN6#KTbM*AlQ%6r8J#qB-(PKvsA7w`q5aaOB zk#Qs+(MJy+;YZNXeMk2k-F;^7O2&mTT_`0U{`hfg0q1)?FI zIDGu@vBQTCv%?9Lh8gNF|?kSEbQc<8`5kPqmC2M_QA=-|GCdk*eCxa;8P zU~m7G{g?M&+JABXh5hIEpWA#oV<2iH z%TMxN{-L~)m-BS~!91Ra^7rNM$={v7D}R*V%e?{^AYaP8n0q1jeD1m2vmn0X>D*Jf zCv#8a9?w0NdpMWnCOI$nP|nE7IXd^?*>h*lo;`E+^x0EqPo6z-_W0RjAl4)U*(ANQ zht7;M`HVh$@C-kL&h9(A=j`sYyUvcz_D)|pefjjI(-%))IDP)~xzlGsRLaw*Pn|w_ z`o!tur;nXJe43q3PQBBIPK{IflsNS;qv**=PsYUeCG1$%cm}% zynN#F@yo|9AHK{kCzsykLzl*-d`VwE2y$LPm-k)Xb9wjWU6)6fdl#=%cXR)B zQ~plw4>#q@xj)#Hzn%O2P5EDPzqcuWEBCvb@;7t8vnhWg_wuIv_1tf7%3sUwMSoAQ@(FKxC-7{Ds^{Hsy=CU)hvDpZn!a z`E$8n+LS+=`^8Q9Gr3>bl`n3}pU(aKru?bg&uz;8ocq~L`9khzHsw#|etJ{>MDC|H z<$uck=S}(JxfeF&f6V>lru+}NpV*ZDU+zC`%I9-GzA69T+<*N4+57GQH>%@*NvqRI zx(39UW{7Q0G`H);7_fTp(yD;5SAEsBDk}-wa6m)}CUgibgcbsX&@nZ12mwOq5Fi8y zy#)vmAnPF13>S_?gx4ob|26?v3r63 z3cCmB9oXGKZ^!NedK-2p&|9%z0lfvg1L)1z?LaqUw*kEgyA|k-*eyVBz-|V5J+>L> zb=XZnuf=WzdJT31(5tcQf&LP^4(L_bwLpJ?T?6z=>}sG_V7~-l z(4S#f0KF8u9Oxz3&w*ZyT?X_b>}NnP#4ZK;Q|uC;7ho3y{Rwsv(DShifu4u`6zI9w z1whZieggDt?0ld<#?AwJ7IrSsGqH1ko`IbW^mOdUKu^QY0(vTTCeTx`Gk~6qoeuOQ z>@=W1!cGNxB6bSUP1wmmPryzB`a|qTK##{x1bQ5{3Fxud2|$m*eh9RI9S`&e*l|FQ z#*PJg6m|^IBe4e1DeMP8kHC%wdN_6z(2dxUKsR7hK-XhO06h#l9B3We2(*T609wV? z1EsOUfL5?N&@xs7TEeP8ix>@*!YV)uSQ%&@D*?@6MW9)X0-C`JK+{+rXbQ^#O=4M~ zB$ff1z|uhDSPEziO9G8zB+v+!02;>PKtoszD1k+RuEQcg*J5Fyhhib1YcK-nA=o;g z2V-l22C+kd2Cy|i{n#Nueb~W3aV!Ybiv@ssFh5W?<^$@&aG*}i3)F#mfZ8!PP#fj~ zYQ>yDEtmtS8M6a5VK$&f%nEcqwhPc5v7LeLfbBF*=L6jy+Y#t?*gT+p*!O_;Vmkos z!L|q5jco_C3+n?a$9jRvupXdNtQ%-2)&*38$$@rYGN58i3KYXSfr>B*P$AXo~gAO_=u9e~03V5=|~A8daN#s^!8!T4a4 z7>p01Fh1A>2IGT`V=zA07zX2mjbbo9*a!yWgAHRaKG+ZjQedam(-RS}5{r}E_prFsudsVNzXC1mflXgYqcgy#Xl~9cTg3hYs zX~|H>vFI*w06RkTv}jPcN$@860q?&ld1GClKp5TIDGt^ zi#f#ZEi;IZtsdk)Tbo+cSj%X$@87jqBbE2ZLwX_~olxVnGM%nR=>bo+95t8pxiNE! zMn!z|qr<}}SSxK0Isc(ev4~z=@~b0f6mh!&eKS1(0Eo#O!pog*#lxTvv)(~;8tgkJB{Pw2{3iy`lc#gv}Rcm+2O4550mxG$hf zm_q4PiK;JC2bKTOvBi#*GV0kD0xDWoa<9fAjFQ9u9PQ3_60}b%xn=rw{_gB#bTkhE zp4zgeOB)9>njZ8|O(pFlc%Rm^`PnYNz7j@L-Qx`E+tD}3(YK?&jRf)kfxaD#rmEW* z)cm~V{_SrB7=2g#vwy|yB$%Jp_aAS?iv0a5W;9&gU{I-RSefI*J(W#^Vf5&TCo6JsNnQ zKsK$TtfOO%w4Jo~(<-uOm-tm=bQ>xHKGw2^3maZeFG|`;7P)mV@@CKnTVHx5qvNvW zGwH}Zqdc{=;bC;#Vr}YZCz<}Vj@ww9c=ZKa-$%>nyYCAv>)YROGx~02>uux|oz`~? zTj$jiY+dmrqvu{Pwyft~4Hu*57P}MHPBQ;#J-4!TUQNN)_e?XI_E%cgbYa8EnWdus zHfjO1IZMIT7g`t{7vA5pj(ap5Tw8A=%jvX=)3(mrjf`%)|B}Hy#4T&Muwm!)qNKl# z9I0FPB5$<;u=SmvVRYO@JClywqm#(iZH$gv*?JpU|E6`^hOP7J3${Mb&*(c})Uv+) z4J)JXR<_x3G0yJ;BzuBN;unSGBC?UJVPQ=T^4fM&W~LJ-4!TUQNN)dwLj6 zdv9!6(}fK)XO@ck+o+q+<}3wUm+!~uDF128I_}Xhac#YgathNbPTM+fH!`|OY7BNR zXj#Lh4P%oXwQ-7dTE%VH5%2j+#*S|Lnr8GB7-rI!dtWR^_whl|aRT(l^tXLC_MO*v zbYHs9)3>T`UhliT_oBT2! z{2+R>Tt#n^_rZjs=S0_tjuR20Nzo3%w}tlz&k+`d7Gb~OE5Vb@g9+gcypJd57`|IQ zus7xuEiwgjWrNC9Rl7prYQU5YWjO)wg5R=y0gE-4O4<{_U^){DRy;8--(OiihlkdA z+!{jVr@fvySxR#FggfnpIfaW%`b1jkjiqb0s+Q90gTXR~j{~0PB_f+#R7p^-C}UtqcxMNs){vT#wM08m|yx=Rd1@M zGuY#mge4zI*_9migqO2?o=C>+%qOi`!jUr=Lv)eDCpeAea|Env6&kP z`Hp~YF@HX9 zxFfAIq`8B!7?d-Vi+0LV@&!GXP{tXoW-=UULi8LPjhP{n3M;KCYto0J+B7C>vBu$B z!iq}-Tw0Wt%jC%?eHnY%q2sU#?qbD-t@>yt?4m4LOP;cuOfC-JDpp)5P>pEIxG`!m z8|Z>bo8YhscVWc^3-!D!UWld~_ENQ2!owWC!&!0u7^M$aOzM!!SgJWKxR%2v6tjFj zPnxh*z2>Z@NR`s|s+Yqjc%9|5J8FrbBWzAl6?aBstoXTnkFx3+gPy2LU9IG;?tHkQ z$2A;2JIkij*;E9jFXO?oPaUUyRt}p2*z~ktU#3DeZKjsWWDH@0iNkgfD5s|_5pPbD zPuFm7F7DUVxmG4TgykzHHSs)AjnIKuHX0*+c8)mVMQpQVHGwikR7z&IFK2eeD>R2s zI?d*;nu_KiI#y>;r6}CNwK5~i7WdjhxW9;sXyol_!l2^j^`2yvb5|=>TeOl_lXOO> zHq^K_CftGL)B9@{Lj?C163(QNip3mPML2JFU)Q#tej zizej{D~pDBDpq!Lu2sm6m@bu0;AOQj8;gZB)pEAUv^Rn{UC87now;Bsm?Cs^z3wb< z#qG@U1?`p;VF{`71R0`G<>D%rF9lbjbA+AYMAQ}yRU*1d#-61(d`XtgXfIdld21r4 zb(OPtAi!P7s%)#uKp~F`+DFl}i>vhn$z3S~rvjh0RkD}|9pSFq!bL1! zjtpukWu%r@d96V+5#=r_g5y}eY$*~`I%4WrJYvtetP!r=3NM)E^BQthDyolGZJDGS z&vU1|;ANICnvSWI`ih$J_~W)}#)fk)SGW($=hmt6sX{DUQ8`2UoYiaQ@|_5L>XfbG z%!ZVChcR3>*W#KKhfnYlbhA2+t9-t&%ak){Q8FV-n5*9btT=Z;Yb(^1(K->#>Qki< zcfAk}vErO&i_@ETqwENYDy~VF)SPBlv*K*JT;8j7dUTmFU+=;wL^`+_)C-cv9}Gew^+Xe@ZbaZ}TlA!7M*wR+lTOSv4` za9)#CY2uvA5PZ!xo2t9)hN?yxGaKuHe8$0@1bS9nOsg|$RHZ~BMMn#<1j$|Y1lO?Q z!l_^^!IZ17k=BG(spQV(GAqtutCtbM zv^A9osGP-+x@h4@6OMp5Wim6-D(b@K@;QdYqI>j!O zJTJLca=bVweq1~x?&_e>shS0%zla{`xLbIj@FmeQ;fa0M^c~+9>RZ{jL+{(YcXymE z*`fDrNl4`FrFu+uIT+wq>&%r&zJzd?ecHh>0T6d;fi|P!n>{#6O zmyWl)9_hNY!-T!sb-4Hobjo``_Oa|YvWtZ=*?O55Jz0gP5jN-4cJnfIzwej4TWqTfZ1A4xiplC zCTld^RZQrTMEKA%3~3%kjR4YzRX`{kfrk6RZe{2En(QoFPUm!VqWT!wHxdl@mpq1Bs{ zS+B2X774S- zURQ;S+{^tX@Oi>&OWCOMQYBp>p3uaJCTEmw*PZukYo!{U@Pw4HQVmye^%Gvg$}<>Z zxZCdbkt!$3{I1M$H+tbuSpDKDCysk<^;}JjJ4)dcw_k+k0-wWctSSjooedQTJVem? zrd#H-fzNIt)9ymnS+T@(es{q|aD(%OKL$QGMMp}STFIpGs?Z~wayp|%wa#u z&Ke6@!hxd795fiYmmxf^58Aa5matZ@v*eSJbRZot>$%s_0qt6Jszk18%4bvs9Yt&F zdI$G1HupU%ysm|Pe2(S2(*rQHPM^gT%$N#EUoeT=(bIw^yZsH!6mvD94|_sdU#1k- z+wzedcZM!yXK1GE^2GGIxG54hYLsy!cR3SW$?`=@>VU3Fd8!0$v+J|=7)kK&J(4Gc$w7ag1M+&udAoI zd#>PV5a$UKxqKjI&6#xCa5CVkb9aLZ%Z5@8`n~a(E=a^Qkx;?HbshEsHh0bF3)nMZ zJ*jltQVw;Ka{GgF?uwb#DlLJWjf#3J1!bCRwSorAM_HYDjaBE;nq5|{g${9Vm;|q} ze6c`@@Fvu{T+tRY#Jo874qU+A`MbTEdMc(<#q(vg(q^x^xK|;xu$Q6I=Bad$ET>7G z%^kC4xPIm`Xx5dXLxGCX=&U)?hO|0e;_euNV?dlM6G^C(m0TIOWNJo3lHl&7`>^5y zHj`2xaV3mt+J%ZKQcVvC=CgcmOQozXD+^|)6J@Tg+M4c4*|!ufM=XYl^SHGkUp%f& zW^+wbfZf2E?J

    G+0crq&enx7@OAGAF}Ogv`Rl!@H@$RIidFI98K*G0H3pF@y6q5 z#%H}2uhL$Oa`^=8s~~4KW1&>aY82(FHWZ1fugNmBth_K`ql~1t=*xRZTyM{Ey=uXS z(9OlIsSu&sUsQD&=;hof>^8M=5K*l9OYQf{)o|b)<>%>2y(# zQmOQ+lWOi$EWs~;&k?R<)S4*nb=OQN;kk$8KAI5j%JMmbRuArpssja7#VKW|G%Xls zuzZFTPWhF!m_DT|)!oR7Ir<4FSUzpoPJ}CFLS@eTleLnTyPgT2WBJr^ZzgD`(R!vS zmURU`S3kkDe^yd?6Z)vvO=_$@m)U4<@{?51&Gv#LkEhji#vD<4(=_GhPAe+44_#0le$-< z=<5qmWVHar|F4s8C%Y3x)GtKQ@<&JqP~7`TovS4uNY0m7JHG0;q9fQLLlNY0@qFw) ztc2|;dR#Om8W8?Lc%twC!Mmu5cK_L?=82usHH(aVZ#B1l)?sLLoYN_`sqcY69pCGP znFNkB3ETk$1_^trye?!_1ik4mAnfep+C<6fp+yU{Y-;s(qdy*U_9w z;6RhW?Lc6P_E%GSr!GZAqgkEDY1-Of9vGNG;K)Riz&;SD<$K96lfW&Xi!skUY*W1; zP|x?iY$k!tgVF;6jeIZRXA;;vDBU117%*3o`E<~Q_TdV8PEzCUG&2co9+WN+$h$Yp zAaJ;OP~;$xE>`>=lOOGcDQz&U))iZvHp9)9A_IXMzIQM)Y1ll{hd)*d`FR7^_ zHzwo_M!4(XOahxnxs&ZfJ~v?|fz6{V0fD^k(@X*fn=GXR1jg(pOFkPa#m%-VdJ$77 z`#Fp=2pno2Wibe>D7{K!kWy#SGZl1Z!B(bQ6xci{7zotzy?~ra!{$K|u^PHuwyLd$ zd;p8VmMAzYEv`00&4VHYfi}Jy<(V{WHa!6dH1eH*nL*%SlcgLE@(g^(J!X=(dw#rFbqCV5ROLJ8#YzB8CfUekOof;=1FWAB;dH7zg{ z$m2awxNUiZlTG$f0C}Ypk*pZahMe8%(-&P)Rk5Y<9h_{kk37hWg(J3r&ZnzYGm*Sm znL_2Dw$2VblYUM1kpp?WZy{!q*EFBAATLU(9Fcg*TO{o7YmLN^qt+;)%%m)(VlyHM!FyEKB()7E>iv(%JC3QatYrRt`Ynh3HkT`#T$Bw$ho2? zifI?|&W-69iGRlK^jjU{qcOjEAXm%`j1hr2oe0z^{ivgmbcg-X;i1%IC^lZOq%t}s z+0dbuhEYqJmRs5~`lcVZSyJI_-Z)}&5Bf%(V*9gnY5$glnw}9d>h$ z*34*088oMA5pB4kVYES)u4-8uHddxzm()#GtwX4M-tdsF;3%pxD#Mt%9hxN9*p88mxU_JT)i z3zaqIfnXdjrPQjBk!&bYyUc_-sO5IIOf1k(#|)0a38gXZEJTJK#lcv)LfI@s<}t5B zL$4i47t~d6aWFnH=E>z6t5I7Ap|%cexvl0wcO$sOF`VMG!;sj7;dH0s%#XUV<~$k$Nk9(ea64m zVy{I^7LC$cDnw%gWaEJ8k=wuJmbO|7@|i-)G&JZo`7~OMKVQ=glB4lBIqDhAgsfgW zJx-VhqXsWp8jNTuScMv6mV%WnH}Vk26G%-3FQ2Kq5ydzO32CJpa zcyMgOWTnl><4a}7mDz!CV}C{))X9@AYcstRguGGhcu_-*(j(E-m`ztR#j-BlkR83~ zsG`KRwz9)iADO>`;YmB3| zn5AH}<+ir56gZvHoMkM84;pRgooB^q8PyT=GWVFeI+2W4?Q31=jSoc(J2Wn5t}!;< z;>dr}VjIaq;*C+%BC`|>x7^|uOTlEMGUl_?%(2m&&OBPCN6WhW@Uls3R7GjVl(vE< zI20SumX}#mMY1t6J#s@Wx3tw#&_=2bv=j`Z#+apGu;s@7^-{p~I@`&|5pE1I+MrGz zXjz--r9i874GrUCc-oXN5Sn5spSEcAYYEj5ZZ;BhmUInf-RKxuVeR+;y6C~_i(ava z*Bjn?EtsJ`G}kq5Cw)e)F@UsTtY>-4+HAvmA`zR{T*!}2jJhV!fd#b(&&J~TaDKv2 zA0-?Xl`-z76T|h$XwVsNtUxU?b9q_IEp9QF+el+UHkPB7m{n+L%Pnm+mvQ?tQ@&z$ z`^%|FSzYj@29;)9Pe=6;a;)G_`<4|F7F=BjyGvFytjnf{bxF&O{p-2hG^}mpn+P|S zGTNX^->+qDrsr}SDI?HDFPXmRefdjSwKSKT9N#w5L*yF!A#Iqsyid#8Y-29Bk?bMf z*cY|P%;mjXZgGpb+(w!Pvat_piJ8lLwcOHHbGeOF3usvPo*q_3%Z>f(x!g3YZRAr3 zH}+z*L6_d&vNqFmxs8+vbMgQG{zHqoU(R9=z;PP>`2R;)KHm8M&sjd+`2YEEo`&~v zeXH^RL(nYm(F4IImXA07znkUbjsO2S%g1+8=6^^0KO0T+U5)?W8D&d(1-Ut(`?88+n`T)4-T{% z|GyIqg^oA=e>$*#F8=?YC0>Q>31)4AFaCcF`k43ljiA-||A$$5d`I(Ij{koErWEhl zhgReN+0Y#gZ~Q+SkH~xWtmXKBHsnf^<%|FS19YE;H~xQbmXA07pWWp7g-c$3&|KA3eF>CSvLTFaS8~^_|w%PA!{QouVRq)3DU%_6* zw;caJ46V*u{C|uUH(T-lb?7@KZ~Q+Stfu6R|GxlY>bE@Lz{c921mAD@NS6JchVQq0 zyv2T>nJNB%I?R8|BO~lDPMazI|CE{H|Jj@bTORIUB}H0_|9@P3`Hb=ZM~c6I`2Vy2 z5&!@84DtVWOIG%tEe^IE{~ziwb^S%M1H}JRO#DB}2k`C0|KANWg+Km(4NTB)KmOkj z`tisAAIa)BTk-#FYYac=H6Xk=p!u0)7Vi@#hJ+ z82CKAc>>tLRU2=ffOFV>=FJnp#{cu@31CCN`SS#@@&69qJOOOnotrmLz>nZM_~ZYx z(|mmK|Ie_~`CE?vzmL`KTaW*r2Uq*8$NwKY%klp=&Bg!E;v@BMM-{`migV1|C{ z@&D(6dgkc29RL3mTrF=D#dkaY|5fNaH*fqud*{y||8Is)aeqhS|96C|b@9gkv)L)X z{rLYCtf9?X{J(~k#~c6u0gT$L#s7cAHalza|GR)Z$E?NwkF)Z4JV&%OWuzn=ep z?*0EZ^Z(Di|KE20|GD@7+sgkx_x^ty`TytM|Nr~>|L5NS&%OU=ip=rn|DSvRzs>yr zbMODZjr{*}@Bg=1Ct&XV|F-J{%)S5LcK-jl_y6BQ{{Ol6|2$8%{}22b9^B8p|IdYq ze@1`^=HCBrBme*0`~UwW|Nq?k|83^~pL_rRALsuM3*Hg*J=b?_-w*pDeFyZ-@BN_n z!QP+t)_dK(OM5$dUh282=cFF8N7b`i_b1(tbYI$ic(=cMuv^~sYS(RDr*~z$^j!<( zf0aKjze0Yre2sie-Y0ugc9-mIlsV8MQ%Jv-J|n$adYm*Q-CsJd^S#ddJAcwy?R0kT z*NI79klY|SQ4*J|mh95;QO9pOF7DXSfp@Ivkc$5(zD0bhI4#zS_rN~G9>ac)9f=)` zjbJ^Z*G0b)oh2%WOrkx7UkRTQUL`z6xK21J+yPz2zxt;LV@}zksHI4$v{}lKOVg-7^L8rMF>Il%r;MuzrOD$6g#*Eo zi<>@Ev6f*|@x|*X4rSO-sR@UrXtrA%QAbJZbJ5)7qlz^Qo8M}4rDDaf)9*^g1C-qz z;p9A39Kx{C-f}Kdi5D{{Z<^WURjHj^wu2eAx-Mdi(rI(nOIlUcY=GisP*nsOHZxxZ z9Yug)^WpWRH|j7@b#*dU^Mo?o;<*Yx!h_{Y*qP-xJ$3La|@U%d<>hnX33$- zuytHV*TNZX&YO5-}DMsjxs7y_jd!=56jVzfpc6BLdP3gQ^kEWDv zYShE96{-bWz#J*XD|Tljk%w6X@ov z;4Yld#L|IUP4BTsxFru1MutsK^3;G=7#KF*&^3jgVN>x%2P$+7o6i@-^EFE}7^}td zJ~Coynr~W$&2KDZ>PB5CWzahv#(a(D)~Hix7`C+4rAmbYCXLe&kEntw9k-a2Ld~#+ z4W$C9tf>uJWlm)bsWaTNP6`#nX6K83Q79R1n^dOoeLNOf8kk7{Z38OE{2W!+im(sj97zDw`%^QkTsh?(97P8s#gPtysmd z8N#SYLMH5@ELlsQvYSjUuC)CbHs0!5ij@porf7}1BNn5wl1jVX%50dMSY9#7u<7Go zTL||T^SCE(PZI`J(}F+2utm+3y+GQDkST198%;@?o0eZO&ahSRj5`ue(+*=Hpb2F& zj-~~F45URu-c-P3461B)(o?pWxJkklqYN9a)N0}y6JE$v{lS_t?QSyX5s+4MyYaF* zoOb0=nQYo@^m6-cm|=^V{G>M@3fWQKS%WX*h&MGl#IWW3W|NXCI#As=BA~RXC~l!7 z#UL~qS6Axhd`L@Y$`wsc<>3}KQVcL`{&+5gGGrxfVU5d_b@?q#R=^_je}=vJ$gxW{hbCVN&aW7v2PE-RKYY`jO@6-yX4-t;Ak z{TMdh;6lZ|3>$Blv|=BIjkj2u0^QBa7HMl*!V}b{?Wu^@We#cmO&x*`tI8I6`0Cav z6bzd|sk5mFN?*ow-1|5Y;y2(dc z$gtV@LMRpAXV{c{p~{Lq7&bafk&e1QWwd4@9$U_2ZnCW18Mb7sn85WmO*K_1II6m; zuW6%P0J`zj0#oe9u<7+4U$$35wF4Ds)HLmYJ27k(lQWR9SM4eb ztuxmHDO=NE&1cwBskl~)drh@$yzD6`bG|04-;rU9cu{RUokfpU8azT9N?C4!LB%|V zjqruSK^yYQ)2Mc?T3d`z+==u(Xte6ll#N8uVaxkVq^n4B3+X9#VA$wNHfC12DQ_B8 zGz{dTWYdDbJ;TOZC0el^!&VH}bVYyIYc!D-qZL_9lbhGaun|6!y-1c#cALGDH2btk zJNFWL88($h>9!;yVROtzSJfW1yJ_w1Vc4p4&S-ad^cj;e?eP<-YSU)a&9Eg=YIQP_ zjFO2A?NF6zOVbkF#jx4L49WHQ9oUVJkX(xIRnflO*YL zA{WWlbfr>;jW;8ZqLX2(C)7n$Hjj?tI+Mj>qg72aM8dG8b3|HO^bwu_O`%%NCQH){ z>0sD+tKTZb3|lUutmJ4*!e9z#Qs#73*W~753|l@OuBZ*RP!i=0v0AHHPZOJnVdIU* zPzV{ex=n2?1#O{xDrhd3W3B{uOqu-uA;H7{{{8=HUFj}O*8=$`@`vRY%h$_2@}+XI z>;>8NvJ+$x*(%vQ>ATW%}LCBjQ!!dDy$yz1X>^qM!xq7kwpqQgo&0Xwe~}5mC4B zHQ{Z-(}ZcEMz}!miQr+y6!_CNzADX%DR8JOHMK70^_a{7lUqYXbcrT6a|%4_P*ml2 z=3+T}tZGx0>cw)2Yh_d5QdcXXbU|AT)Ao|OV9J(a+?tV!De$QciGWKR^O7b{KIzNY z%Z_F~aH_MFa9WeglPZhWZdMr+QEpX1#T0nefjFUeXelCUQ5OtOk|LYD!zpm93A}30 z)kupaTg}wt78B8Y9pG2T$_BS7j0&;qo!MLmTV>9&d`ab zWn~IH>!dMO@lbR$LisG^P%)cs+W)7(wdM`dQ%r$xO@9p*7;&cPrttvh zT3f5xa_NZL;*IDGb-zc`bQ3(qde`|fN#|!_@I>o`E^N)F zb97PLv=dB$e{G|x&T^fk-M)I!u&omViYaig)izx&@6|dzx==ltu~f(=?{o@0Y+s^) z6E0Nqr)u&mHOg9`Y1^9u7n`tDsX`uAxd_#3#;QJ^Z(2E~z{gg_v^t|kRYEDn>1ZLA zAe)x8DR8n0!bLlS5t}ki)+#PTKH22iPl1;mbSG5CDjhOqs>x(9lXEpaK$-$K+r}3a zt(XEo8x=YK*R^34Q>>%SS0q|71)g@sPMKAXM6^W4%mz(HyN&q=t~PIJUBwjm+C<%D zvgsT~ms_VSrYxvVbkqC;XPY-yfr6>-j@%k55sl&5a8665)TL~tsrbBN3f%3CwOGv) z#&o!ra+K{^O|8krm;!$rwWT%F$Mm2D(`XlG{J}}6!M!bxooi6 zKEMIzt?R3p0uP+GT(4pZTyWkhyoxFC!Gi=(ab3j}IN`iCa}`tIh4YreRZM{!&YKZZ zF$I1&Z}wQl6gc9%^=K7S;ED5=nN>`IE6!UzRxt&>IB#M;#S}Q>ytQ5xQ{avBR(e-V zfjiDynN=|b{y1;JRmBuIJhPs}%)?jkkEDBG0h# z)`V2#7&hL7Q;ICZ##_Nrkzv?)3o|Ox3>$B~MMa8X<1M48NHT1^)e#jW!^T_mP?2ER zc1bT_(}bv@a2Sl43tujP69PO>{>QCYY2I%!ZU?7XbgF8Ne)oPhsn^JyAu#f@o*quY@sS4-))e_=h)2;^;1x{z0=_YtGw(y5U+n;2H6b1_x^K zSPZWw)+)_}!H^*B0}~VeQaoQQCWxWYu_rG+dcRWxJFUNjIB?#d&KsKg?5X>|f9JkG zeW0=D^&j8&-jV0vjUouvW&D|RC7P_~ZFcR@P$m=~G!7YRu2hanu2ordBZ;xHi`E7; z;{&YV`O(+U`0S!bPqlq7b7{86@a+1NPdWb8uXecal|8P~3wPpvuS6^$BVA*ydG~9K=87HQEQwWCDP#_Q4JUjfqcb-Qkz+avbM6RUJnnB zJG?q-(CCa>S;1diDBkm>;OX}V@0)rcbj}m`GrFHIX7^t8!ZX)BIsbNp@>RT%2f@)% zI;*Z!hszV>s4=beRbe)QQw{eWjn%m4bt>Cdji z8(9z>8rM?eC34ViijRBpCgZ^H$Ux4P^;AnrS3PMo>0D!UzUK4mqA6Bz?wTdo5HE^>1Ce z@b3A;S7z;(kDR;ay-!bl`Va44_}WnH&P(DjoOf`;9Vrrw-@fP&uyk|dh_G1-v_>2rT?Vxi`OO} z{AuHtu8V*DJG_wu!EKa-WChP3+;HzBCo5k3bpA!}O4jM$T>A3E`&}3K#m39dz5zSo z&r5H>8zcyBquwJc_?`tlvVDJXP;S?r#19XA6RUKc_2sMA-uhLC<%*pGzp`J21}_1E z3%Xi%85NoEqdIjNQ{1SfCJ3j#?jLjv<}H)9K{AU+aWezoiB^fOQXja>0W`S|q*{qf_ABR|F)aS+@_rASs|@>BDHA8YnEEwD^^fBn&g zkxPUtFTQv5)}xl_*5S{IIg1O-#&Av^mO@oFMK(=@tt=@Za@Al^*#4|t+;Hd=Q{tYw^rhf2ncSY$RjKG z+2d}#F>p-yl%1X*dvynG=*Rb7-S^b*j{j)szMD?i<)&kn+=e&8Ah?a1j;!D#E|6XR z+)q3YDBJG1`zM)xj;>u&MQ69W$ z^GC%G9(n1CXYs~b5WLo)*NgBmS&vyJ;F^TrPH6XZ+!i=oOlIJct$kpiD&9B*1Sj-*pMFADu38LvPb{YNWX3DFabPH@CeYCoUBVPfr%F_PnL0SV zH62ZyvD;nQd#`!R`q`bf><6ArW0Ad14@?n?ng_O$~nA6_-BswhHw@PVbe z>pgE`$)59m@|gLKAv50af#5a@F|vX`BwsfkU9es!$=&nRWuGkg;DYackh_U zKJB@C{^=j%4IBgy4NS)JiE1D>fLAlFoUv33lyu>Qp%j~}4rpWkk&Lzw2;z8%nsy3* zZ(n=q0{=zY!jE^A-n7@U9Us)+@J-<{{WGr+`w^#mv&bp*vVGio6~*a3)>K|R^z_rt zPA2wWcB$*$-9DXq&UI``_KH66c#r1oZ;)UQ2=<4AllkP@LPG6JleuV^^gC(u#IVN| z^!qF^^!#6|P9}+QjV3(73SM*nhD#56`)I`_#~*Xcz>y@yx)WhSvMz(|sJi z=W ze)tXc+;f-A^HMkTil4t}pVQC3>aP8$o1X9=fj3MbxQ#M@tl-@*e$n;f%F*SUpE!CC zt@EXyTzcZt<&x`uch%=#-M{-dcB2Jv7(s9wmHk-38=ku34;y~ky}I`Yo4wfDT_5_} znY-;qJ+SFb<>JPvw?BL9BXjxx{tFK%=0vvN1L#$#Xwh8$zq$N>hH}wPSxUa3#}djo zgLC=+7{f5k)&JWfyjD0@|F7)k9GVc$)&FZg;U(CNPRbp`hV%U{C@)j!nyi?6_>KWu_3`+{XeZz z$qjrf!^)y^*lOlNh7RZTRbxm$m;aAhym^Caz7Kt;v}+>;-0aXrB2*z{cGxYPi6@-P z|2LQakMX7F^8axxgRQq~HW~tEYdz)CP$qgV|KGrX3f2|g)BJ)Pz|)b`JFA7d zGpAJuuIP6|C`JI=hf6xF`X)o5-TZf_PP9jU>Ll?Hv+Z*vP+{?`l*87N!H5=wO8lh zJ~t80<^P+@|2HsTg_{jKU(SyOXldW4mF1l||1TEqD(F5&x{LTi^kMGbbPpUOZdf~` z&b24(Bjnk^O4cLy)-Z{x(uVkEb}jq@VF-^Rg}#cNOlc`91jN@&s8d<*3@)3RxsWWST717`2!`vE8+pzk8V<6r)+xlZ@QeL^hXQHZ(9e zIXEydw03Y}aC~53+2F(o=NSm|t8flw2+Wqy&kkF>4MmVNbf$)iMiy^C%#momlt8S^ z@IfyWOGJsel!KXmf&Z*cMQe->jO-jm#>p)6zpdZ!|MPEai&3f)rPjhw4~`-=M+OH* zh6V;E(9aV@F%f0vz+sFID3pbfqz!tj%1qrs`8~L%G*05?tWq1Vj?;17uzF&IvINIh zjCxe6fY}Ku7s+^*D3z&cK^#RG+0n?*=;+|&N@A@&qRcUE=!Xi``iffJqtVn1{`d;} zWF%;{luMy>WVoI}DJ5K*k))>P$XW-6^khO`qqII>RW<6eYDVq(blha})~E?jcB1SV z$czQFMJi&{C>L*Fy0DyOG(!E!G)=^#+qm=%8?Z*PVPWj>hh<@Z8?%dlqO@p-qyJyp zVLJ;2GhsP)n9PycwNyEcrV9Gw|A77dLjh(I=F6cp8Q#k5Mvz+nMYBVT#UN^7n`Squ zCt~KwyxJD@EJ+il35QD+C({*;m&BDxH@QMzRvYsg5@Ku&|@vwH2QELKAbPb^688ru2EZP16?t?D&DjsIFSkrdsFnN zMjf$L^ge?tLhCeW6==cim`g`ycWkm@X3XwKVPQMF&a9c;$hOSxTi?#w-48itl+V#< zMNgw0JpDgqNM*FGup39qTj9!uXb!+kVSc+d5{(h%G_x%ZAd~oqH*(nD_v|c@VfMXI z=C9M+_*T~q;{N%!ZSJh7!GCgunLV3C`SjNkC}{?x{Fbtb3Q;1My?qpPWcA@F$!@>@ zR2+UEBAERbKBmZU>hGV?hE9Z;tAoFg*$N5UI0*myCO?dn`!B9G1A`+ot~SO9l?(Xn zCY10+heB{7(Lozk&Mz4r%8&R*jH);l(b1HjE-T~g0{QQ3^2)KWV_?#@gz_fhL}{>8 zb-9ApB5ltGicXX*N>v*h)+mc*T|hM$b)bBjC4<8|GB&xQ=u=v3xv_ybUGqliKr~y( z4^pEljWMM1DdR1zHq50nn|!%pf=#|A>~F*7W^J_@{!dpMW)E$0hTB=Lwy_CkqL~o# zVv*$kfen~#yqF;K$TSu++cBd^ge>OML~YvS7tiEeGpwwgt}Rn(Bp_Q(r2FD!;>Qej3(RM=FQ~`te6Gh1aORkW6P!k$awd7sHr6a>b=l_LQcf2g< z`=alYzW4jy?0co}g}$fz9_xFk@7}&U`ZlAg09W>1+IKu&(&w?)AgBV~;y{2AG z?}5D&y+f!5!Ct)!dw1=f*W24G?G^QW)$?i3hbT+IYp628vptXZJlu1C&z(KD^jzO_ zRnKKT7xtXna|WtcaD30vJsW$fJycJ+C)TsBC)nfZvGo{G6@yhhV?6^s`}Oqq?9sDx z&kjA^J(3$+EWuk0S_Uf#V=_u}pa-SfM*>y~$myT0!Fyz8T` zce`Hidb#U)luO~!t_QpBMl};|?7F(^@~(@!&hI*_>y)k&x*A5}c zo$_1c*UPVxUnaj$ey;os`APEQQ7wjz@~WJYr{yvEI(ZOfX0*u-a+Q1)s?abX-%s8z z-$TB$d>=5`DC5Is*|oAOWtYk>K-C;hlbt9# zR(7Opy{sb3%aSO&9lnkI#r#kI>$N(Q1yxa&OJJJ z?%biXyHnCBkbEKeMDo7mP01^$CdJc|$0QF)?v>mj*(|wMa;4-_$pw8L$MH1k3`a0aJiM zz$jn{kO1@mt^-^HcnF{k&7jOAs`s7>>K8@>~DZy0e%Vi1>om^p91~`5R6|27AFJamx0B}z~W?JaWYsqWUz3^ z-hy9&t;xX3WME~oSK<3V0)myvz{+GV!Z$Af{vPmoK-el|U}Z9}FWD3DJy@9x>`V42 zd;|6+1Iv!g_rhneFB#aE>`wRw>`Qh#e7+6v7QoGbHv!%Vcs(FkoD3{Z z1{No~3cd%slU)v7fM9Dfur=B7 z@Xc|64Zt4&9u0UD;1u8yfQJKa1Y8ez7+@U`Hb@!RovZ@ilmSbC6kq``510eY0Hy&` zfJwjvU>q<82o@>}!)F3;9pGBPHGo0D0H6;L2lN8E0UdyLK(Jz&6+W8*4S;$;9iSFa z4R{dXfq(}9t^%9@90wc&90eQ!90FVcxEyd9AZ)9${owPyfcpUM4Y(IzKj5B#ivbq_ zE(F{IaCg83fV%;L{mQ_8Wjnz)^8x1peh+X5!0iG10DA#@0J{O@fHFWSU?*S)pcoJX z6afktlzs#FHQ?U>zXbdO@UMWd8c08d&#)RuKY`C51AYYfXTT2u-v@jT@Lj;S0N(_B z1Mqdg*8pDydYrr=#GpVM+1>iv$oa*eT65!x zSlbG;k!hu)28St|+%+E!q8=fI}J+g4z9=fI|oZ7VRl zb70d3P=Ibd0pKjpflce%R$!Kg;IOt8nB^g;g92@&N}TN>sI{%YEDu4oZ3Sj|2xw5C zjm2fQhoI870<$~><+c@=6Z2v4PfD%U| z1=>ja+22OaM*cMB8#kc@(`_oCZ9P+BD+-`Q)NLxzMsmqk6hH~9kpgYxG3{?731uq^ zpv2Z~Dlp4)042a~Q-N8Y11M2;n+nYG96$-RkpgXGhMnCx@ZZ1x--I%Iw|RxLJjN&k zc$*5$@))B`;YfitlJw5*7;oH!GK#mUz$^~|$~@kt0<$~>C__0?ppA6KvpobTlX;s8 z%<>SRjOT4CFv~-LGNU5}+Sov5dk9bl^)?ll2zlTA#u~zbX0i5XLF%*WfjVu5t#%&u}0NPlFm`I#< zmbe+B`i(>WklUpiTQ;oK>s_I>Dc#z@P8#fvC)??j+4sWww$Cloq5*S-IGisMW%IpY1@ux$cbmLP4X+ zuJlBT{-Ry9Sslw(@-3rHxVgRHsdWn}OQEVL>#7lP!PJe}ZOv}V7i*iM`lW?bF|O9M zvz@3=W6LD`VqaNVH)x?bhzQn6GJSsG)|T!6*`bozP%Z%-%lhQi)0hf4*kOucM+bM-3L0Fw#v2 z%BF;<9xAn*!DKfkEc^2kXRV_TI=ZU3u9e&CL8feL8mqOs#hp=Xw)oVhimj3<=q&P( zxLWDTL^6Fj)2$1`QHgv(TF!Jl1;4M;vB^!Qq|)tIG^DAF(x-1{^p4Gry3A7QHrq~> zn%y9SZXG`BJH{CYde*PnG|FY|x&CTu#gukWiLDf?pY{KIVk-`rgVvq`b66s-H3feC z$MV~fg+|p@g57!DVGAu**6E`Eh3I{$L?qi|S2o#$b%(s8C`7|4Tg_WA!vs`GdrT~f z7d;KNU1}569dXlwU0jCY^6+-{X1-WxW)%ges^tmSL}9r@tf@Mi9&f3=6!3RkGNq)H zsOEK1Yd)y1NMrJnux;~sS{a+!+AdXX@}@4N%S7^OgF~~h)Lc*$)ZPTl*TX7$e|YQF zB+SMTPu3QP$;Xc2;+}v5B$$}Vj3&XBkxAj?2{6ih!{mN2#(ZSrWf)=p?8GxLzWmXN zM__dM0~7bd*z#K@Zh(>HJ0>oMapmQS42&udO}Jo8`IZSKj3_^0Vk3+v-)}+yqsd<$ ze;&q?KQsO$j3j?#{9zbJe*gI0i@gfaXlOmBn{ z{QFG{VEq2eQ_sWb{b#10oO*QphViRmr256M1xBS`E!Y8L(%XVEj7Z-m2*G%ClVA&s zMi&ZBfU)Qs1pC2A^bz`H7#Hy@{S1skf0TX%#-Kky-wz|uZ=r91@#j0}i(&M6na;r2 z^APQVk>^`zC5$^if!+wC&iA7QFy{Q_vFBmL`7>ir!g%vX#vX>z=J$`?4P(u37`qxq znqNHDhH>VZv28HQ+%;x`G3LrKA&fBJIJN=CmkY*5V08KO{AXco`IG!dVPyHk{0CrM z`Q7|mU{v|l{2ly@`E7ogpW$!ghxjhOiNA%fSC%*m8lH1jS5jN%0z9Ul$4MKWgu5qifteGop4w$V4B=AshoXz_Ia4c;+feeVHS%= zW*>$*EbgDZ8)mS$VfJd6zvANAHq2g;ncW6+SGZ9g5`>)CMdc zrw+mLJJi7gc@UQ0rrt1+2M*)`SUyJWkL9D(epr5sn#J;))C`v2pr*0>IyE(rlURO@ zni$A&EWb(#29n0|E7Ta4k5GIpzf4hBeu)BDev#r~`2}ipAV;wLJowi@{u9g3fqx9- zD_DLO{2j}O!QZfa2>cbx&w!V){51FrmY)KD9>_mo`5<^{AYUBF7qI*U_#>7NfakIN zIQRpW9|OO~@}uB)SbhZjb|8O)<%hwq2l6>AKLnl~$X{XkLGVi~?+3rY@;>l$EI$B# zhUNRgPqBO-_z9Nxf@iRNFZeN*_kgFdyc_%o%e%l2vAh%f0L%A)?_+rfcnZtg!IN0t z2EK>ot>C*@-U6P$@@DWjmN$X#VEJzF?SXs@%XfiC2l883-Uz;l(%Y z@DP?)fX`rgIrub|Zw8;j@-pyAEO&qhvFw3QV99_7uv`Wo$MRC}F)ZH%K8ocf;3EV1 zVJx?U4-Mo8vAh`EkL5++J}fT;AHebg@O~`M2k*o3Ja8|TUGUz4ya&q;xEsqhxC_e` zxD(4Jcn_8ha0ixka66VYa2u9YaO*(cf@KBVjAa?zgk=f58_Obi7nTKZ<3PR>%RG1o zmN{?(mP_DzEVJP4SZ2U=Sf;_X19=UWDeyKdli;mbCcxEL#=%>#jDf4L+y<^3$Sbg1 z1earZE_gGRTft>mo&$DZc{b=_83D{dE@K%6mk#8cund7qundCjSO&nwSo*<5So*+) zSbD()SbD(u19={nZqOab4wf#^#?lE|SUNxxOFL*_X#;gEt)Pab1yr##gUUdbu{43w zKo+qyf&!KXkjGLFa#-rX5|&z!#Zm(@SgJu9%Pk-^kV!1h0*Qf)V|fOMVR<@;VtE?a zhUKYX5la;~7fU7BilqXaGmvLvDF=~(3}Yz+AuOdJh@}JsuoMG7mLlN8QV6_QE&vaf zrvNvW^T37W$-s%_X5c`Q_ZM&&mVX9^V)-Yq0n3-bAy~c$4#x5Ya1fS%1aH9dd2k?> ze*gzy`FpS*mcIl0WBFS!i{)>?43@tJ(^x(Srm%b#Ok(*fFoESS!8n$`00Jz34rnZY z2F9@bDd1!I69BM$22fc381S%s8jNE3BQS#H4|)H>@&~+sV)=dEKd^j?_X?Iz^8SwH z_jrH9^1HmhV)+E`Wh@`({RPYK@cxYDw|RfU@-f~^SU$>o5zBA!UcmC3ygy+14c_xu zex3J6EWgJ4J(geP{SM2o@P3QsBfQ^W`DNa(vHTM6IV``(dlpG*gt8$C{zX}_{3m6> z@*k9WAWc}lLKz3rfaTvQJ(ho?bXfkC(qj2CrNQzqlp4!FQ(Lh76Lr==o{8m4)ENVL zI+ia|r(yX5bt;yBq*PcwPbsnd1Es+7_mmvV-%&Cwe@jURQiA1gDDgmwu>3V8#PT_6 z0n2BpQ?UFMHIL;lsgtq%1+^K=pHnAc`7`Rofjj}rpHjyU2vl3CkZ)8wc`eEWb}3HIPSQ`4n{omQPZLWBEM__Y-guu;YK92>R`F0_eBT z@u1&6$ANzPyb<)q;F|H5lRupNHe4}&X7VF2*WN9YZ-;sIE`h7X;$&hn0@sR$$2J__I20af~jbA$6g?@+B_&G2)o@x9{m>2IP z=yN!Dd}^ErJr2JY{2b=OdsOfxmoNFZ3zAee4RDyRJP}gn8>C(4Sx!JAF(Hy$MH+y#eN` z8{z*M=63ig^dx+f{{{Yo(2sB@|6MRQ-46bR{5pRL`Vazq8(#xG2&eGp_=iFN!5H-l z^#b%BJWV}LeHHo+K1RKdx(#{`uA(lby3lWsqRyc_&}(oeC8bV+K7)g)DT)U@2EPYC z2j7SOf-iwjfe%4%!A;;=@Mh>MXo5V5K~I4L=)kE!2#y0sfCGSl_fOtSykGO4;eD6) zb>8RTgF5)5cN~gF;Ez%tqIYaS=!4Y#^o~Oix}Um_-f=KO_fa39cN~P!2dMYcJKliM z`>FTQI}SwXebl}5jsp<7mwGR~V}FG1q3))4?1#|Z)LrzBS%mJQ?xc6jAap179(u<# zLhqsOpm$6mbO&`iy<-xg+o{{=9TN!MM%_y97)R(<>K1y30HK?yo1jk!p_{08(>ulx zdN=hh8s-jx=v~x}^bQK48>x5FI{-rOpl+ad@DRFzx}M%KiqQ4c+vy!62)&)Uj_zHA z&~?MeAyi_lxBtLR<_p{uAX>0TS5 zE2%5!UJIeisW;QTCPHtfE~9%5gf63Y(7ifBJE$Jrt04pr$e?>wgcxd>?o|-FlzJ20 zDJqwFLg*4|JKZZHw4J(`?iCQan7WAWQHUEmqMsbwdh_Fp%&GodkKV^RD; zIOpx1g^(4_dV6OgWP$VE-Wdp)DHGj09U&uSpnInwWPr2X-l+)b;e5BJLP&>@5+N;| z`}PzFY2fU)Cr3yP=f6D}LR;VrxF<#EOgQ)LNf0`NI-Tx`5jq{te|sW?PJ=Vxo)Do^ z;T*WPfRGB#f_tYRq=fU}-aJBbI0NpTjF1e@fqR<~l2Q`7cM?JpI1lcfh>(~P(Y+H8 z62ZA}?|6h3;4HXz973nSd2sKI2+hNpaBmKwli^&rcPv7i;cU2f3_>STC(yl32%P|D z!o7_M9Zwxc_l`#BI5->b9fi;v;e5DvBtpl+nQ-q2gpPr8;ojj0ZGyAm-eCxBq>iS0 znE#H3GvXfRzoV!l=^p04Bj9YfhxzYtI3Mm|{yPkMD0`Uy4uw9-9_GId&`a6F{C5cS zQ}!_b9Rxj;J4xoFO{|5Az>P3`_Sg{|P9X?qU9egBIPx{0Gyf(ml+76b0xW=05=a zls(LUJm{(HVg4JXMra1}-w5#8N`3!N6?qaApQeCfL=@n@gE$Q zpQ0JWf8Z(T$z%}!LErxOXa?~g_#X6TGKl}cccDL%LHq}v0FToQ;y>^m=*wgf|AB9V z$7lxeA9xJH(%^>~*--13(2Js*GCiH4Di2uOXp+A#B{0F`UzDhHQ|G-zF zPm@9X2fhNmnhfGU_>SSrG=umLd>MK+8N`2Z9`Qw*LHq~406tGMi2uOnpTKk!-T+hh>`!S@so(G220@DTKGGKl}cXP}3ZLHq}2B@faJ;y>_7gb@FM2cdtH zLHq|k0X>`y;y?JVdN~=yf8b-#&&eSE10Mk&rWwS4;KR_z$sqoNvz-sp z4B|iVLFngX5dXpVCHK(`;y-X7^mQ_b|G)>Jx06Bq2i^z$oDAYWa4+<9GKl}cd%-<4 zgZK~J1HGLL;y*Zxx{GEI|A9N9r;|ba2i^m2ry0b5;11~JWDx&>+Yv(i2X2ENPX_TH zxE0(&Gl>7-dz+hS2Js)b8TvgL#DC!3(BsJ<{sZrVK2HYmAGi^`lV%YAfpPX_THcpLP7GKl}c zTcQ7xLHq~bAH9WU5dVRzpzo7G{0FWCSI`XNKX3)~e=>;wz~#^b${_xObI8kR2Js(! z^VFjm#DAa%{htitKfr)xnnC;rmZ1-nLHq|U1#hAm#DCxt=>KF8|AFn$1Ii%&gEP{L zXa?~gxCnYd8N`3!Lg)u&5dVP-peK|;{0Ghl=g|z}KhOmonnC;rI?xZwApV1Iz*;ne z_z$$8FO)(22WPqsnnC;r8qgmKv&KPGhaOP|@gJx{Unqn44^%*zW)T0uw`e7rLHq|w zphz=_|3CryLYEQ$!Fh9zUdH^FgZ|KE%zsPJBf5Omp`7Z^M^fKnZ zB=m?bWByBkIK7PdFAlw;%b5S7&>y;t`EMKah%RIPTZBH*Wz2u)La*pD=D)4bFS?BR z?`-H1UB>(u0bzO>^IsTxMVB%Eg`i(_8S`HddPbKq{{^6LbQ$xXA9_cZG5>j?UvwGs zp9gwImofjjp>K2<^PdZPN0%}GIiY`a8S|e5dVH2K|Jk9>XBqRK4SIc+G5=Yhzh@cq zpBZ|5mNEaCpwDL+^PdrVeU>r*8KB>18S|eWdVZEM|LK61UdH^Vh2EcK%ztX=_gTjL zw*`8BmNEaG1${otnE%d12=m_=(EqcH`R{b-0b0iVcN+8oEo1&W6?%b|G5;x{|7RKV zp8|S-mNEayp$}*o^PdcQftE4byEv{~`&iST{tA+s~HeCP#uapu=EKY?#czcurv znNQDrWaeJz2Y3g3SK6DoaHcVnpNY>zX57#daMp}`W`1UF=J1&VX2#(g|Gz+Az%QnM zIQ^aJuTFm!zVE+p`p)ThLvO(4)0a$l;M@N6^tsdiY1_0G`U6DpUH{S32TxDKsm0%? zUYL3gdIY{V^^K{|PkmzQzNz<2-8l8usovBDQ?;q=)Z&zX$~vWocz_~k0!q}`IX6sCOq-65M$)hI^ znw*>*o%qYd?Y5b7!>2V$T{n(XbZyM{2mBx}|XOFqZjALhv zNybhbJ9_M(vB|Mf{$KdNr`I|;ghpz8^`mY{10x|*Q35Of7WmlFh?cKsed+9T*vg5E^Xc7iS@=mLVyC#XwM zhoBZgO@itK)d;E(R3@lMP=TNvK}!T>2}%=`A}B#noS-N{+XyhD5`sho2?-+nJUUOVZ6@eM zf{rKXID+N~I+mb~1QC87J%U_2oS+Q^9YPS{=g~KiYx@&4OVBhygsDd-$+dBU1O$x{ zM3{OMkZZgl8X-(Q@=tQ@Zv_37pqB~yGeIvB^a4SIsYiZKuKkXn-w;HYdgNE+8sY4b zpOb4pA?O)`eoPQy?-9b@BTte4eUc!;-Xo8bYmX81C_&#O=ozJ3+S+bTdKk zB8af^$UDfj8wk3Nplb=0yzAUy=BAxI8EVhG|x5F3K%5NsQQ z#UVI%2(}KvIYV&v5JZL`JOrU32n>O52s}gJ8Un`<*oMF|1g0S{41s<}C?1e=Cn;}9G@1V;_Qkwb9A5F9=PhYi7@L$F~84jF=jhv1+gc*77(55f2l2!?#AdM58PqyKw<|Nq|mdvG#125bN`a8CO--t+Lhh9C02 z&3gns4F81hGky)<4}2GT-aiLD*&l#D;&(!yD+9fmRgeMa0w1t|EkM53t`Od5;Jfjg z;JfBG!?(Ci_aIL4fv?@GJZ@e=Fb1H}lWp%lMo7 zzg)S?g!d7&z^6Vu0^L^VZ%Fu*mY{*T_PE3XyAMAwc=aH7`MANkF;CH77xKf~*9Y z2{IC-pPhMlWOKjy*Zm~FB9jS;1%mPfk*PREo_Ii};^39x_W+rcgO|xQG8G3ek!vpy^gKaiDh_^2 zuKk)IG8G5EBG-OF&`$~a2|-U2L?+C)dbS99%=LT}{wi2qF`3a5=d~`V8oiYoyNr z(q{naGl29Na6b73=`)~Bu8}?iNKXNze}D@4UWp*mKR}LL%Mg?%C_zx1peR9$1Z^eg zY=XiB1qmXI2E61N;W6MO*X#rl_5vny%}CG|g3co7OoGlJ=yZZkBS=AzoFExNB7%ej zEfBPsATlO_6UntV5=2HKa4flYG(krZbRB+nj&bDpb3JA z%L4GpHHsiGM7)0y^bdkwA?WV}{gt4X3Hl2`FA_u;llMn*?RkO-ck&2#@(6eG2zT;+ zNq+Hjf(Uo=o*~x=ck+Hnu02K2lLS3M5aCYVW8~VS1QEvMeVttUDnVZ+=t~5BfuPS4 zM7Wdp8FKAY1QEvMJwUDzcI178T>CIVA0+60g5F2ay#&3NpgReA4?%YjMA(scGr4vX zK{pcgPJ*r{=zIrNKl@jEJ1`Zc}a3DK@j0d-ZpaWT!PLaC_)fnOkRjw3liifh;S#*O|H2J zBHYQdk!u!$2#fLz+hP||+>zZXWPE*l@Eo*sSSbx~FSGvYs312EN*f0*iMpXRn+k^V)2 zvTD>}dZNN&shUVuv&A&bC#RH2SEqmZcTyc?(o5l1tF;ugIMS_Z*I6&EkqL19{DJZ{ zeibhj>&aprT76zWyV72yI$EXoznJPsr4XSUJZq*pifOX3OwenzNy7Dj*5$J~-N8b^ zQE2O`*+$OQh?*K|Z?mOV2lhVIQBG0UFQv8SfL+m&Xid&et!%VgTpDdcQ%%{VK5McS z5*c;rh@)tZ7_DuGGnP|{TsEcIs|d9<(o(JAsv1P)5X|GEkG1W(lrW@&>HSa@u9Maa z-8xEjbTs3`sgCTUa~x8x@2j+4GRf*^{TMDOmr*7jF8Rpt_c>(MUOUwhhs@~gQyu-E z%-46!E=iGg?YRF&0;E;B&tMbpZ7wsVSb{8BvxC=UdWlx#3m9U_R8wqkg?!0o!Y5hvZcQ|0fmvaFqNbv(h^m7Dm#3z*xuwQhv26&J zgRae?1%srk4XT2PsJ5*u8J!lD#2K=0?u;AV5!+_3E}Qk%!wP3hR_&T=6`xvH(G;3BOV}kVX&m-o&M$8j&7oo`y;<&J znUxUTI_!r`jq#vb`l0CmWIyDPl6*wO5za#GP_epd67XVxFwJ+f+9S zzNkSP_gmE=&*qRMR9k9PtI>Qsu%MDP4fzG<0?Q17=+#5oxK!{X0V7y|8Sl2!C&At1=a0P+ zMvS-k8>z3r*xJKk3|0Xg%zKEJ;q5>A;Anhw3aZ}se?I1Hisw#(vo{%Bad$kEbR=WV zgrQk*7ON}PadWC<*%b`TC++Eq(A;$`bq$SlDBN@z{Oz*1Xp@I+!lc6Lky;x$9}~f9 zBP`Tba{9$$Atw)3vo=j6kO?Z{LbJkEv&F)pm^>3!CL_(fH18>?TRtX?)rMK9t?A|V zS}vHkRpQZ3Cf1GW;L6$LcG(qff68Wzi!2UhSyOjpszM(V!fHb-)M`y-cU6@4SG)dr z)*H5Xlp=>gr>v#SiCn2@n%9eq6}`7A5!;}(L98~<xJEEGGjwu{af~hP8@--EWHq zBw}O05|W0NBCV>_yOdB$YmTnj#{{t201LHkf4dQP=Zdb7r{Htdon}>|tg=a4E{Rs+ zt`(G-tXZY0CzcXIsMe3w`dO$g8KXgcG27I4L&l&Z*Ku^!Rn@%E1pd8s1{v?_)ezt%rr$iawokxR>Hpt07WV*L?9YaC`{RaUYH+lq!?8Pr7QP3^R!Rai1~yYXVvRZNM^6&qCR!fIVC z)TUb@r{5j6CKiB_CtQYV9o4E?HYUY|%za7|<++ zQ}LxlO9P!IR+pj~iB&_=VhTRDHmughLT$la_ALgonng|1mhx-jzQv?EoD)}la`93^ z)K$s@GDoS}Rs-WudlFl~pVLl(Ca6s}%lueIqBl4l5Mhpfu+Y zSrpz3d=e~Jt!0xwoAlJ9S_3>=L2GvAJg)M}?jW*y_13Z@Vvuz5VV~45iwHaER4UNU zEUK+hlPQyR3YE%GJza)Rf*GqdvrwB7B{J>Ke9orGFD-T?hMdk_ZYb0-XWr8`=S7Jn zPui&L7&{t;+sBx&S`!PkNm;9?ZMG#1WmaZRE!AE2KqZ~<`(;Icx#n6*d(=`zB3qX_ z!mw_QSgnzTT1_n0RLE0q|57nLFN~-?i&~q(k_k5Kif}wsQi)CO#(b(05F6D##(>ot zSg4ID^i@SYD{@-W3SZe`7D}YTlB4Kr*VUfxlG$SL$Xn%3Fl0!=AySXk>RG6@yA-li z$SN$=3PO1_oNi^p-KtUMsw{cqp`=8dQ^it>iY-?#Kx=hat&WA-fWwxH7t3a;!z&8P z+;VAMCX}fQ-9$GK(TSaYr7;oc6q*rF9CimSR;y*9*60qKb{S(~eo z)+N%7@cg{9(^za7eAZeh7*n~bXjZcYtKGsvZ8R0Jxtb2WQ&@`o%!WWGWz9ycevPx@ z(>r1fomCh~2kJSkCI&n2Sy=5^EY!AACYMO5Hs{@ygv3=7r5equ!zQ-M{VrW=QJSx4 z+y0s;t(5yi$*iDNV;Im-pXr=$xf@)@TnA`C8eq6y@L6nuH(^;8MREXLA&Xyu-6(vo6*gasAor=|-%0jJA-BM~RW`!&x zlZDlxV502Gbfcnny`YH5l@@!;Y6u!~MTNQutyN*QDi&&E{-j<}(^X|HX)-5w7&?|z z(PI!houzEs9cVZjE=yOTlq4&vw2x6@waWDmvY%Tt8FkW#KCG(BJm!M7Widp$*`h*O zsJVTSgkN8k8FO;6zhOf|qyno|uu!Y17xE!jN8U^(Rq}RO>dmRMW|uM@>)7FJx+>Au zB2}3)s*sfJK1Pn!%2}x8aM-(#kzutm7HT=1FYaTcSgn+WS`KGs`xpsUD`BCQ!#U7C zMvT>pS*Yc3wy}>9VYMO_YB?Mj>|=yjt&oLU4)0%m%mP-su)fEO%d0N5{+}F4kIY;* zebVGZ6F$Ln^o3&wQg?$hM!yX&@2kH{(%W4E-quZ*#-YZi_Ha0aYURsPkI&~bXuWQo z&8}0o!{T-`EH((EK~=+S#JCnUs=5hXN$S+8TiPXc%kFcOBgQ}@Vaykz!9d4stml*N zQp>K>x5DCl&TLHNV_CJztkYYe&ThB>zpvGZ;^IIjg#KSAOuLPSMocWN+n3aBr%w-m zYf0IC={8(1wroDV%&}Ci#l((G%$RA$C2qM>mu}hBxC}8a!{m|0jsCRDtI34Lj(l8V zM`d;FL0lHpEnA9sp$&#yJ)CVf-Trzw5;O<{eoZ5o6XNeiitxK)Stel$bRu3|P2?>@ zUu#2ZH%qL!iqNku#5##q!c?;OLgk7+oK`t4MnzatRF_~?J4sbPxu>dL`5H>&wK=tV zv)iM4T~)PjA62!n)k+3c7LOq(iyG^aKtZpHO5AHzwW-oFnN0QcV#6SkIMqsJ*)U&` z<-Ci9mbs%3C{!9nHkK?z=ksY))echC-`i7FOIJ+Z+ep3O>-JGq>+~IG zK@@cc>l&Y5R&XrUZH}UJjjC2#92JSO9j>XPDZSljSqjXjI|g+|Z}zzD-i{^SG3gh@ z@@%{y!B4fFRP}fFRMnCdRqZmETppLk8?d~ds@mS&M^$aIx4mMAJ0cF$ZAMEou8m}4 zxjxzKP9SQD&!Mmty|PYTnvp3C-qunptum(4iKNRRQY=Lg++8J`$oI$ToYkyr;^}gb2UBu>-WGb#;S?2Pq8Xm1(6wt3#)mpK>RMBQK)}q>) zw;9snTGF5O2WQq|wtQ&kIBRJGfzkLVq0t3L9&s_K2k)%v{9rfru4#d6UVbV!|s zgg((JtyR@clS{8th@%!&q80F(WKCg1FR2GRK6}C;cW5%sWLsPerGg^48x4zQQq|wy zLsctPE2`QPv}k>1yVLY~;%cdLUp>{PKsMnoI|89h!)c5JZFyza(1+y;euV`h^ymySFPPof)ru8W?eay;M)*`446moEcI>Meh&E!f z3WYMeA>X#=a<#Zb7L6Czs%niwF>fgq#Rapf9WyB0p=P0$j?MdnHeo#;uItLGQZO18 zmy*61u4)6R>Tm6-s#mTS?OGjtU*mL{9IvaY-d8gaqfqE8 zMi1Lp3;zH6df>*u@Ni=w2Orq_ULThGYjo%hDw(blg`T!l*6VF}n(bPtW6~AG zLAOu4VAN+k;YKHwXg9oKxEZieGsEe-T(MM56*7?PH#g)WrM+R2Wp(V1=Ek?)-xwGg zZVcq`bnUZ#xaM)iuls(;#Xz*fx85ly(H^|QDX6oZLiXjY+8Fp>>VL$5*JNX$Od=5} z_p~u^p}HUoTZ~p+Wl5qDNz+?HLZ5E z5^;#5B9%Pr+T0Z@(qexuWoVkywYbex>4uehgWRbtcIEP7v6X5r*wadlTC9jD#A>Sp z?zL#!t@(6tDX0w>rGcVP%PMpR-#YAeJC>Lr>UM95k8&Ax_F1=+ueBfdzdoFC9S>J@ zqjIU0tS;u0&1C+y>PU@RvRcC-Y;5Pz@XI2h5C>VI-!3MSsc0jQ!r+9kOTc?6*82(v zTv3b`5Ov7V{|+wgcHdat&i`9;WQ7Ip{6;)lEETiyXnrx8&qh&MyXuZMqxEPNUqm+c z?O9% zs@K+q!nQf%tcuDlr>5nR7)=RT((2ddyl(SecRZ7?Wwj`+3jrx~$7zerNrY%0R-YUsyJMxsVQ7|@_f_hQMZ4l?mwy4jl zR7YAUU+6!5z<+Am?H;UC>z(Q@yBQ>{G+K9Of9lhyBu#?F)$`V`7+%+t@ z0&w@8tt=^*<-vv}?(2*WM!})b$D37gP3z1y8_{&AWY7C5aO?XL zR-0m>mQ%Ds7OPFNP|GQ-AcNH=Sg7R`R*=SO<1Ey23M)uqwJ{cIIfWG@vDzpLwVc8V z5?JlF^?e82!V2P8?IH`c>6kHEPAav5mO7vBCR;*Zv5~aajU~A?*K#IWM(Bezq@;Og z3*qL3*4Lk-U&QR!w}6AzIS%uB zHWtkGE#NS%pW`rBUw^7|5i`@bfP>aK4%2!z7EJdo;GlJm!wP_n1yg+sIB1>YuvlPY z!DQb84qE3ptSi`9FwwVwgVs3?OAs~|jQ1_zpmmPJYK4sjg1!YDw9au@*s!sH?pwe? z>l}wQ5E~1|`WA4|I>%wT#Kr=C-vSP+r8y2up!FxF7co@d0uEZ|IINb~SOEGKaL_u( zVPVC_0$$$&4qE3ptijk=Fgkb+*vFyIaagXgv0$Wc0SB#f99DKLELgs%Zvh9ba~u|d zY%I92Zvh9ba~#%-Y%I8-Zvh9ba~zhIY%DmxZvlsZ^*Ij9#r4+)7cHMRcn;VvLgzTF zEZJDl?OVV>>l}wgC>skpeG52fo#U`xWn)3RZvh9ba~zhoY%FN?E#RPaj>9ULjRnoV z1st@_aab_3v7phnfP>b3yBF@F<$B)&4lDM3yFUi4{{jDmk*QMz_e1f&zyH<1|7zfW zHSoV0SWyEo8t5FjLGhx?Tci7SGO|WAP`zDT%qGybJGl()q+8<@4f&esiaCrTNbi3? zI*7w`XRYX&T8+DbYbKrAekr)#OIJPjc6djwp&? z^+w0N-LEKD;(gLH*x%dK48p)#jbq6M2JQ7kB`sa&<>Gb(Ef3-NACmC0AbasProu%Ijj7vc?> zUhUtkmNhF{_d-))_vFJ>Tfw0&E~R1ao+`_*1a#|Yqi~lw8*LQ60*vn4Dft@xaIJnP zTx+B7>%QM{nB=e3@6wHAt-ctI$6^0_jXK|MCcyp%V+0mgbvCq#esyQ-2Lt4ixSt~P z_qz89afra0y{{M%cj~&7#a~Y+7c!|ONiI}Q+tQkrqH58rk~*c)mQZZgJH-CI?|u2A zPS&j2Gipn!0h1-#)INK(S{I7M22sG#R+~gJW7nYxFG+mLs59Voh9x2KW_Wl`F(->y zVS;Ku3q+LfHA6H9s{LQyrLXLsC&Ob%}y_P(BjiJ;zh<>t|SyBuZL`(&#Blf93_ z0(Y(6w?@DWe28Cz0dNgNU{7q7s`R2_S@EFaF?(a@!CqT(&s6Z{4LU zSOLwY)*6%72;NyE9A}LHn>c)GVX)VV-n4G^UG#(XPn27k2?YIWUz{knO7E`IO%W{Q zKQ{u(MPl(D51h*8s~Vq9S|}Lh`m!crO-h}qire1~JLL70EZbPp#5;0FvzF89_qxYL z@*24!w^PCyLVchJ{Ih;AYFXjp*qw;9RTQZep-b_W( za>$#CP&ruK`*8KJs?_lXQrS+YQo7&7^#4n;eh@@S>9j3JS( zIG21%ZLO_yZZ3AaU3p&Ut0;3C7x_jN1MH0jxYtCx*!n`Wz3OVX>rQA{z!9>7PMOvB= zTcatTwi<~i{F1U!8`lg(dUVO|SaK!;+RSEqB(vm_8{VYZ_ADk0tDd-4&s+0I#{YEB<1*;NmgVvmaP2{w!^`jg+#tQD0P+=C z^j>}F-q}&PkJaeZ#npXxHPRa{JItpQ<9%`UUd;MV)iwTK-U zxT2x|{A{??d$kNiyEVwlrvdo^@hOJL5x>hJg6pr*R;Ca{wYtiZozv0Xtl*NLVAplu z+SASzPwt-j#UAILdvmW_2kZYsMot@Q7UrOult;YC=DL_xQ1bbLfZY zQ^#uj_ffx~jsij6)uUe-<-xC`zwO%X$?@dYt?q*aXN=6nTFy`*)eV;O!f4zlRa+XW zR%|g<>ssM)XrfBQYAHLDRhL;~mabiMV&Tf^hWDa(XaFSqRnn; zBGt;owrsz*$zsgp)&5kz<7n%vMq?z>SV4n(ybV|=yp8zI+X#umX$?&9C==$&Epa;G z&FGreM5L+}g-t45x!hC+TP~MNX-*3FWZuf8v24G$sh*22HO*F2$!QN7Yo4U0#{M>x z@HS#QZ=<5t#Is2uj4;yLtA1lX7tnY#^XX(RD00s)1)}O`MGun_Yx7m}p3Gb6E@At< zO{sXo5r&7{blQbP*{aSJ17#5 zOlh~-{qfv<*Qt=k_hjD6^BUNGZ<9T!wRwXt<^{*=`7{j`aPMq@+<_l-`kX|YFvqQ-CxT( zQ)a6_mnpHo4Jo{h#htfd$z(i!m1-$fQ3V~Ex~;4WncU52NLH}>MWJLortdXS@Oqj&}HYD&i&fR$%L9IURw7V>&vPZ8prIZz2 zE}+mZwgP2yAn9b9tU*EJrL(5@92X{6! zhZsnJ`@G#~KoY-kRFU3+T4m8P)$-ufoLN<%AQLwk2NG>7Q0EuvKum{nKSj{?CB zErkv3+1bz>!n4*eVh;_ya-3P;v_V6o8IyZwLvx6i*;50qG==T=);IZ88d?Dx+O@Nx zIRxFT_W7*7vcA6&H?#ycv~y=eb8u*`E^<$`wsM?V-?TwPOD5n#UOws=nZ0#3F*`r= z{LK4jsxt>pe|Ne!Eu8x6)W@dIn>u3h$CGzYCMQpt_}au(6Q&8l_+#T&je7+z39b`_ z1e@rm>Dy@?O^-b~cIB9d|04fdevrSB`Vn;-rKQHex4;#^&3l3OQQn24w~qdN)WJI% zdk_Af{P6JI$Lo2UikX<-95=@@jZn^0NhNjB71QHEn%urCXv^ZF+TnC8F3wALr9-J$ zDw9pTS;KdqHO*e->ODFWmz~OYFK|$So?7fqt4-`U7v9iT_N5PQXiYkjD^?DY6O0_H z=U`cXzsAJ!>3p{Y{VdhiCE{sas#I!svg&SBvvxJ_nvHn;;NdN?{PC;Zl1#aBAvk`} zu(DaI-&xt?hYXvVF*3GlIxlw$FM?Ks_0kf=yk&iEu}31S38}F<_p$XEbp(ZU!j5nhZV~5 z$FEkQGU3XZr(nZARljoTB{*Q%UM#KO*ombaPY9bSgPo^mtg<1_-+*( zQVVQ*eueVC z7l2bwHGIz3`&smqLNt+VluxO{(3w;4i{m;pW3m4VIZ;a17K^3&VmuQqrjz_1IiOn6 zzpb{ft;T)DhQ9@M1I+gCi*YxE=P)?u@={PC+byI8ey&QIMkY<8CF zcQ!k9$FSL1{&aP-Q@3-hnO(z-~B6HBqvpA7pU{1e>%yBrLVEZx538)-VG6`55QLy`MuX-Od$Kh?x9EbA* zn#}QiBS!#>lL^){l0(5u%yF<_{0Oo*WMEM6t|T|d;oO1kM=)=MrNQo;1T4-T7?id~ zcjj;?!5oK^2FlD_-@FmP;tYd5o5!KZ409|j60;49^9=???!q=44m?<5ju|#Ji!%=f zMeXVeLwQNd%;A0WMgWUL7xru(hoq$$<}g?!W*Zg5)wCtYF=9X2(KLmCD}?Z!475|Ki-*#Jw!Y{TO0hCyks&NeF@m0XgYCYVEzIf!jo zocS;?XTUZbGMYlRIT)3L*oMU+5WC;@s`t=y9Nuel$vN6&4(b~@0$7~=u%3||-g_@G zZ-513wqbFM!=T{Z*oH$^(K2%&EDWzySRC#!C~OU~;gIk%!yEvM#J!ot!4HEXcj?U> zGJ7sD`wy#<#bFSGqIT=e9Flm#-n<_y4fkdiXG9E2d-dMD(y7ZOQD}mhMdqO1%;Ic` zfjNWT%pn^n?9DT%9Mqdx95%80ZLf+B6i%!4!gO=qSan3|c) zPJD4<0sTe}_IlH@#)eH`bz6Q`GGnnHIA2_??#WxJ+AjYvlE9 zd95t}p^S-``$hJ>jgFpYt+QJE@v0E*Q1e09m z#=F;|KAZH^qgq46V$qtNIge|lZ+jA1_n`p({PF5ta@#jCb!5$nJKJ=E%gG4D8l0PK z{f%i8$dLQ)!Ji-4QfvbJ#(Q(aM! z0xriZ5F1%mqyU+B&!PD9DAqN6&cWW&3VZon&UN@V?Byd%Z#oWr-h3l# zd*_%UvUg!;{Zd>`ff(4ka{{)0y;8`+8^4G?-}MRBb{@wRRy?tQ%UKZvJ9nMEt#2tF z9p8cNedocfE#1iEk)^whjvO`#4=nA^{MNS-kB)DjL$!JRL9A^&ipi}U9XTw~1}64r zgX^1!Y`f;I=<`~@+QJEDX}4Mw2`0F#<#w;db(0z4;plSQgD!8a-dYSt$1JjDm(g*Y z%NZHOnm(zoYYrZHE(@Y6yX>4jRoTkfKO7x1yH(j%fq=`BmKcdg$8~MQ0}t~Yva#o5 zZQ~{;y`my%E(dK88(CH)9&;{v0vUP9Q>=|Vj!7XScNrbWxSYi?Fmkuiaoy^~qs>JR zBTFwnfwiS4Gf8CWE~6u#%b6VmOV=13*R>b-mDVZ9-tJde+dIc3kiEN&HXK&$1ABKF zZPv9Dvvw!36>^`>xr^nOkR6^fmN}WA}|&;T`}N^(@t< zX27*T$h(;*8~w)U){z%SI-HyW;BZb~|KFGYR|Eh5sDajUV=M_BhX--lp0K{FW;eoH z9Ttj)mh^HRmMobb;j#^Yb;-L<3ku6MSkhUrBn~hCS(mhWSWhljVR;L%JPxneS(mqS zSWYij1~tgxFjCg#>^iJ&pI$D*qK@Bs5El<5>!M!W|GqNE395kTatYZ3tC+(ZcGmXv z2f@~I5tRYE28Va)>#x&Rbd8mp7W-YJFlbl~@2pvuynENk!;(V&Dx$J-E2c;K)T@& zJG_4I?TW6kGKjF>H5LaA%i+uv)+O)WHO_@4A-dskN(AeY*6JEtVR@))a7cN`y1ZSx z#yNu;z#1j5YL$EDOFpjPlcofeI*!M!0Ypjev>UWI@vIkbN9;WS-1muCdrCf>XYV~s7rZ+A|HR6EP9Bmb|9#PQ? zbEZ`m73}Tlca7w7ZqSSzb`h~2rT#zm-aO2atGpZSeV-XHW;2_|ZeznBk4n2Bj9n#F zNmZq#Bvom%jVRSBl}cNwRFVvso`C@Zgq@pwfqaB;lRyZY%@Xhe*;o>`0MEyh`z1FU z2}wvu*aF;qa8I>b-RhERq@I|^;d$JDl-twi{hjwc?{ZF^I_G`I)mS}H;|UHB&E1bh zjg!n-I-~93B`!{Qww`_c4~;f3GO|88+?Fy@{_^;Z!Vk_wX&k5kz$PjIkK{1JdRp!tPImTYBJjv_~72t#%ap~ zDufAGCSDGqnQo%ov)->bPJ7)rEfta&wU{(=YA3AeSSv4GvEQeR)7E>jp4J=GDy%vX zYi)EQ-`(+kpE^#PrbJOx)C^kANfKJEH>??`j?+GQoK}#^as_E6LDs-VanNW^WIa4i z`=oJNl1~;2^`vgV{nVgkFpBk}>o7mWXq=xoPMc4N1~(9>S|6mxi-lO-N_)KDCydil zsbZYY(n6@viWdzf+Ra^YoF6|AlAR+R(@*F}Tob4k}S0ThygcgsoI9{oZ((zyY$}avO&Y?p-s^Qjtw6 z9UA0nOG`+;C+g*>1$LYk-h1qb_G!RPifpaJ~p$ljTK46t=)@`1)>EJVqMTF+(c)lNQdyV?1lf2rd>tKHFF{lN3M=l^!Bm z6VeSaj@POgVZnPlt9Cc?yvBr6aWo3QE1I(v<44Bp!qjf^QxE5l@B zv0OFW<06$ZQ{-t&7t`2;_twWT(yCD|ba6b@r)tq!sMhJvf$_qFqGTiCSe!n&jz?;^OjvOf|B z3hTlQjDxhU^(ZaGBSJzqEL|L@T^^@JMS{!8d^G{5%X+lXZZT77hXLTZdrSMY=AJ#! zVmF;m^b-9@xe<#d`)CjKnv&z?7Pt z)kAn)6wBeBUbo@G8uwm2&W1oxG$U2yZpL7fGOo0*Gzue0@E7ev97H`$%$K_`mt+We z5HWISvp97yrbik0?s0Aky2vI)xYpKbzS?I(Msh0D@t_9}#vv+Vsuu-y9l1;<63xb| z;`Eqj8Q1T``sY|^(XvXCRn%3*tLIIvb z65QB_I2cz@MX%X!65({bhVV%)R-3*`u^HFyI5#xMHkt)7-XJiPs}|A&YeXG)&_gZj zBCld+7AO+>P8hz^-P z-s20k{hkg2)6A6IHa%Il3CD*AZjXk=3_&xqWlv)gZjD3C%v3!MLT-+a=*%q9Hga0` ze5Ur)81HFjB4?YPrY}j4>n>Q|AC?2%&CIntu9ToUPC7Hc@sRXjPO0sa9!x1Sa|#bh zM^lP2-tEjpzT+Ij)e-4_D5tm-CkqLo+~u*ZnygQs$t;1dG7iyAibAnl?lkqN*l1;B zZ2F6*CGg23;rAEPvkoyI6MhsYU}>C2Vn$ZzW);o?^!tW(3!WKX`FGN{?6&o zoPOtN@$^ln{y%Ug;8#xNPu+TIW9LtIUcDpiJZt-#+rPhkc^ln+#@63%y>shDTeokm zZ2rOK+c(+G>o2|j@ zJ}s)h^iQwf?_>F<3zr_hd6(I8b8*RzL+eWUh<@`a*9I?LJBv-H6=1~Xjh7y`35|wl zg3VE@n=f56CX6)*9Wk@~%nO$uJ5w0zTKI^5>*;p{FFj_aFjjyOn>SuMGbW6c%~7kH zFP$C}#v132m@$PpHB%Vts{4q3>*;p}FYU|}#tJZE^TtctW5QV39JRXn($<(T))zO*(bjMcM_m@zf8I#U>XpWAvm z7reAGQy442h|L=>EsqIfWpmW(!lk8|ELu(UNW`^kE(9+v&ScRFFzWoqOAALVy6ocH zEf?=G#~VaO5P4kI5OKuJ(%PFZTztt)X{HE#@F?95jlx;=;woOct$1a>U}kE!JnUXazW8Q5|E^E<{|oSewbBRRc#XZa(EB!HddF z7VWGoc(FQ@*4p`zc$G2n>|(}+i}FnItlB#gZ|xc=cu|_kq7~qXMRANpyO?p|qA-(1 ztM-mqTz`Boc#)sUq7~qXMQ)5myO?p|VtFQuRt=1^*biQ0XR>GoIAZaIM=U~ilh}oe zrI{>RHE_h@zM@|+lSM1Q5sPa78(6&~m&PlrpWu|ASt{}* zN8|R}pWtZU^s2HaIPN&Ds_f)(F_W$RQlXXovh1^m*Cd!-4b$orCe?VA&MAz5;-bS{ z!2Q~*ywL2(Kv7;mIHQ29iG7vTpnZNfWR{nnS`B&-%4T8>&D-g_&vSAg=G7EoFNUf}x>zm36y|AbA@FUst7LUpr|685>N>r}nkqHx zphP%AaRCc9+hsd!d;cFz9*6u`m^{d` ze#rPfZ)LVVf>J7+GWs1HBxN~My8TAO&x_#y_6J}TZQP)vpW!6$hJ5BKaOxzFA(?VM z&=&a&t=EcpUN~P)F}{Y2^>m0-8FD2HF`bT*Jvr65>S)8EtzOilhp~n(q|`$3T*o;; zU7Z-O<-D^7AJGQMkWw>x6$Q!H zItU1Ub3_|cbaRr|FSGlTe1GvIPW!M$8{dC5+T1m=kVF5t<7BIE6TlKZh9|}!p|~;Z zT$3wJ;0!mI)3M3Od}ZqWX49dcmh^TF0Mzwb&9sJ*L%G23GaPE}a0Q^Qc6IH7yta;4 zb62|H{JD>+g~Q3vxrD3Y4OYDf&LCZq@@v^l!6_G|mM>6;uiEhvA_4qO%n_;@3Psa+ zrWyz5>Y|#;`<)eB=4yJQ)pYd=uJh-;`KYH^PskMEizBUAy94JnI<4l;6X{wLlB-ge zH&QL7*$&S)&6}bF$205yB^ExtaOMl(mVfn3;LO_TuY&LY&C|D?K6UEeEsGNHlG8|0DNZS ztsDG?Yhz*k3+r!NudWBy*Vewe_Rh8D+J&{9)jwbT?bYsTc=eiElbUT*@x(E_{0NJBy!MeBI(*i{}=8Gwo& z!JV5{vO&c2jZR+_`EtCFMVx_#-|5OVJe9DMHBSF)uc-PbC$C9dr2dv&%Lspsp>fvbh^S_zdrYu_1BAUVDr zZZx21NeyZ444aCnIv&S+dCA}RFmN`L@pQoT2W@5ffC7SD4u(%RZgd4{%{d0$0AC~x#%5I&@R-ac6{kF zEP%7NVNbCYWZ(|j^#>hdl!59&t*f-VaU>|MylP01Y0;?R#N7y&3VAwJG#yJ~Fq6;{ zOs`&rm|{f6S=twK=RC`wJD|v9D@xPvu4G}lS}qMlq+62%L=fRILM2^DFr-I(PP8mH zwjLZ(q@(yXM@x=nLoF`Ti7%&z6p0S+BO--LJz3L9k#`2eG9~sKWFspM;tU^*aId976>3E%9SRL?#Y%%OgGJy!6?W^5ln&Xz$Aw{fK9B5FeL&C*I z-%G)4k5UJ4*wYS#{k=dht3|vDj`6t$zVW&Pib8mhD0LB5kt0MpROu6XrK^@Ym^WWj z39Kv_Y$I$m;|^o{eFqeb?n-tB-mZWXc~22*BYM7%%MnK-oeJYxzVCHFO)85<@U>ST zP`D~uGS&5~0nXuyc!@BJb7(3S>~>RBsv6G*#bSk`dcKOX5*ktva#;;|Xg916WM1SG zib%UrB*0JuM;I{(m5JrjPNJr?18dZf0*@O?x&br2OxhF31#6l!PdFv93UwR-N{Lj< zDFbo}dN7G^z4L$~R&5iU<}0Rry(&Xdc(|dJSfTDOdpf?l7OUjYXuqws3~lY^Aw@LQ z>}Y~)6mncs2&6kIQ*Z~1jc89U#g(3`Zlsftx8|lo&ZSQcDI$hnZ$}DnCrf(eN=0@k zLNA+WbmGP!)D}8ipamP0ze;FyG<#}lZ64(#CaW6e7#7h zi}@?e&bx;cAwJr3RO&`K6G-Rc0WMuYGDy8$%qcY`P(f09IiPs4c!4Y|Z4D`qTod)^ zJX*=-N>s8MrDQkc@0ZK&Y$F}5!%!OHSvMQZOM$K58dAU{6Y>U?Rvv=dB%0L3uo6e0 zrnin z(To00!4a=C!*#)4cZDA%&|HVli2vvVlm0l0|2}M|2%* z93io+OU{Z=7B}LaaLm3XEk#Ze@%b?_vk!V4k0^xBg zB$Qg)-lt#&{!FF91v4qZ&BGdnRg<-{+b3{=emW``{yI6RN5nSR@zEAvJf!FfDHQ4y zqji6;md-l$ZaL1UYFfWcJ1Wr@-NUYm?Mom`O>$IFnC+u6 znegGcNHWxu6B<>CIo*$AELHPucj5A%&*ljSdzg z@kpzw5}ZMzLR(_|sX;LcR{hDeqLEeA3ofW>+b=($Xo1-@=F8=x&02ufy6AvU73v|5 zO@rMps#4*bUCcX(f+=O~yF-e0%BA9BKv%;9#nY>CSyb@0E41u~SW*|FLWA$7no2C| zlUBMzik6(qHDfVi5K6m6R1a1h^;R#|iPW{0=yldyAt&eUH9Q$#Vf(rR3Mi3>%Y}GZ z>g7TuUM=={hfhxyXp$^B@>GOUDpI?b>`=_<$dIBLD+~(Zpr>PyGL(qFf;chs>d*dlG-ROtOs2D%Z3@K8r93RL>f(RCn zbFNgh=!lo2sF+1b&MT%}ATmg0t`tx+EYErK?VsH++D1|X$eY>P@EbY z1eCG^LW5m6P(buyDedmt|S-$M#4TfpR4tmJ4bSOCNxIUuk_h)SnZo>~ca zq2*9g<6KdrimpFvNP(uLMkA;(&S=9Gfn0JZl<~Q7D&kdR2Dnz*m?Le|=@`sTj!NxlzE|EO1#!0S8P4a8ZBA<7v~s9Vw1gdgR)ZI>2GR=PSXQM^afO~&9+q` z>U8Jz03$FRf4mte^qqwSm-EEAAPu)XeBr7$+oPebpX?>w=OkAauXlQ}Hm(MptY#Dy z4Xi4%fl7Zsd)WZt%_2#>&170FCXB1p9o^@J+C$vPm+iI zs$y-YS1%3Eule@BjZHc4=Z7AN*n86R4f_FO{1fL15;sndrTgjZO}l;s4*8WE{JvWu zg4k#a*N;AOs+x4f(9l zAz%H$8>!uf1MaXh1(H@JG#-55&qw@X-{s1yV1qS}#|5LvWyz4Q5R79~2vU*exj-?N z;MuT{XSylcA215Afog>o*7wEAPVm<`zyF(}1Mf56|DU~b-NKnaIAffFPXF`i|9QG_ z`f;Z|f9e&d;GOU9ynE-aoyTtf(RP2^vGvzmzq-Y2ZG!v$#^yPY|L<2eG8^mbpIEQ2 zJJ$Yw?SHPN*H%{_Ts2moz4Es!?_6P5u3P^6^4r08{pF?KU%Gqg*2QIz0r1xsE`#6i zw!UC3{r=+jt^fAu|NnO!fyHnAcjW3(o*w=YK$*?OyPmSTy0Ng4wOR3dj$dyq zxH5kv$j837k7fi=Z@gu*Flknl#AT|I_>!r>kaXzy93c$xl{@)jw z&u9KG;q@SkT^tKKySVGg034ci{Qq@AQf1A7Wjw!LsM;!>=++cztB! zT_)D6J|7Mav4F=vcRH5edhzi{#``Xs)z$7RzLN+Xaul>dvA3f8u>WJCe90^hcwb~K z>_6lvV1sr1>hieS?tA=)O_aC%4&giG$ZvyUKSuXmd(S?K>Hqg#`x?SK6YIDkjmMJv zu6_L&*4x9Lk#w8h>3sR@vF?4rPfV=h_X+nQM|LRo9&+!8Dnk_Ty!Wd_*fo0TW-Wux9KJKe&9~xIfqyd z8!UU9y!Y)P6YKgrO{}-SmALT`Yj--9*FLb_TYQ;`@@KzbK8s%`ZaBoc(FW_deUDqy zy~sl*%7gcr&(PzDvoo>mXOw%f_Z?wHc8Kd|VOdrX$N2Jp`3Ng;o_NY3)>Eg;=(gA6 zhyEgIV%@a^o*(+lza*Y?i1lO}EPFkE=(E3NV&N}0u|EIq@U=rM5bbP=kXlwJ$7DQf z#>{)qzAOB=LzHW6I6A)8I)-wMSp@msYn~i_%puBSZBUMT%s9%2zV&!hqHjGuynTqW zV}oLE0U!Fk>rIs3yFR>ih_Y>iVsDcldUwP`d3S^W`4bN2xUK0Z&=n+l;ulPmU%B6W zKJg#JATOJV1vF(-gxPH$CqMMd=6V)9!|NlIO`9}yS6C{2@O2+CQ7(Ik)kC?~Y~-@% z<%5_05}-zEop1&5{W3XoCG<^Lr-QKkh$U8T0-B zImpZ{^sd-oX($q%&D)Rdi|-$&QEqeyz`TW{DxHh;AF0k9u%%f`1iJ_54;dDj1V{hjNT_3PHYymoOVviy_f4=>l2 zZ(REGr8g`^7k{+)fkk%lnuR~Ge_L7op8el!|FZl%+dn<}het=?(Gjp40c`KW=F-7S z{=!4k1Ch&RAE!U{8)g+`@Qm#}e+UF#Miw5l0h;)ld=!;Fb?!MP(6966^PHEI58soj z)4?FW{p#rc*CznX10M4o^8WX~3FQx8{$v}lfPJvN|1aKQ!hH3^x7~I4^7jH87~k=^ zXnZ~TiFrRhE;|7GCCn#Y`3;kgGj9XWPrUZYWbqK}4m&V=p+2$vIuquyxvu@h z>Rq?yHW$wiXU6>P_H+9umS1hQUw$>2J?uPZ)4AvPP>e4$KEBlgFdu)LxrF`rwO@$s z{lbyyJTM*3HBne&%yc$?1b{yE+W&1nw-&Lz=N_5#Cv4zc_OBnGdgV(@pjW;mw&y$I z>!UV66Io-A`1<4*WfLe92G37^Y16j{Z7xB>X?o#9lZj-<2rrs13ZGitH3?t#JZ#T# zB+&b8_;A_}_e1|~YWbnRco(+!oH>B(mzke@Y18DW@@LrIxjBIB-xfa^{JIHrQ`on6 z%h8y;`*ydz>3#Bf9h2mvE9Ue0Px0;DbVT@@x4Z2l)I&coS4p3IQ4KsF`qBUP?cH!h z_#3xhVZry%@BW2Jcx%ig{6IOj_lzTJc!!-9>^<(GCj?BO@7@QV5B}(TzP+a(@$rt^ z-S+nQkU<|3J~Sv`drvzW`(Lx;!~X5%q3$o6aLR|Uy{FCrWFL zk$LNMj-a5uH9YuU#RPiE%fR!&*Po8Hv>9Nuke%;&xc;G8>Z+mm`VD{B+hwP2>fxULVS zx3l}dZEmeS(0C4b-v964K&wMAumPdifgK-`@fGC#uh};d&##h|L!N9G$M%+X|NCER z@^r8`eyjQZ|M&&}!kPT(4}kmrYdg?(aqImctG{F8;`;wuUs!wA>PuFBYxzq{KU%tB z@wE#-1WnDKy$g@uSh@=mQVYWJ3+-gFn#tt2?;;(wmZ$o@t{?<`y{0Flz!jVdNacRK z4b|H2kXCGaldRv}jL)u@d6FWnd)HihAi%=1&5j%7KdxC<@b-yi=x&^5k(xtkIl4{K z+0tl_s#jYamuq;_OtP)#`X06^v2+q@DQB~xOei6!t%^bv!QJa#TRXN(;{pZZ9(Typ z<7`INe>@tQOdbChr*ju4;xZ^*lXH7{XHl!5J(PD1O2tYWqXu0~=8cT2))3hs(b3Lk zI%qvyr3t!`C-Yh|nh>o_(boW`?lM)q^gnEv@*W>4r_-*T)$w*=*`G-Ug|NdJsRye> z*Ts8KHWW^FxTx$lT1kY+Ay6^nHCo15Lf3jN0S$F3JWJJbExCD&RUmkOP|2pc45(lh z5uIeHEBM0!XUhnPEsvPsU{P@8o4$06mDBhh{n$gX-e|+F$NG+G)6P4g0e`ev>4O8t z^}NTR+Hoj{d1}#m$>ZQ$L0xI*l^UFN<)mCZ>paWmy4iG=4U4rxzaOEKytTgDZi`H( zRB-PVsm9}Yy&~f}#xYqA4rf~xqYV-uNJvc%76w(^xko?dP%NAE2{gA@$Ab|V-;l>0 zhrE#@9_?{ZwkAk*77c6tm=97Lq!z5YQNvMD{ct}sk61Y5Yv;Le3g^9@cElg?LZp+E zJZZisV^vtJQYG$g30`YU|M+OSG&;sAsVpCI$@1lkyMxe2yx5 z-Qc!$x#?D%Wf#&Y;1t{?h=8{n-@E1XfmjPmEjxCt#%0&e>VvynJ)~lUJK;pAGFQdh z<(9w5B~qZ~n^2_WJSmB8-Nj zwX=$}+iW)?NTsA_T+wp1i^V)_P{|wVEDCBw^As&HIYFZZsY5oI*{tsN6I_Nvm`J-B zXgV`&mkwq9dWdj2VmUuwZ_%nt3lE$QvQI9T!h@#b=ht? z!iau7T}XH#0%XyQ7rX;VryDV{@9QZ&xLqL&Xdz5!zFg{T9&f1w94jl}awLT}`-WoC zz;3n{8nl9bs*ZW90k)O)MN&n&0`56G>k{nF)1tq|)T>%5jA(neY#$m{+DwHV!plcPwS1^oX5U))25;iNliMYe$ocS1*>?mNKa3v`Eg?Mlw0C z?2$_q82Becg-PexB$89AVmlm4NCiwRgObWt4cxLmVs)`*!-CT~vUcqzj3uW-7=}3Y zXa&vG`UIMU+DWwGZnr&<22R=M$pEFvL@5^wz-K|my$DB_QBW~jiX@tRqi1E6UK<(9 zB5%inH4xghn;2Ct3b%tksu;oBbg;t~qoKjTn}i+SQisPhp|9WxSjr|+F(-F6swx~D zSK`%NTBgH9T41?3LXuF25_Mo#j$*ZDwir`s~~z^LOI>a2NF4V65mU& zZYh%yBb)LLg3aw zg42vdrS{S*heFw$ehJQ}s)={nUC5i03njt_a%qvpaJ^_00*QPH2{S&t(&%IwDK45$ z@$O=(tegcIo9R5TG_1&qVZPjoWvoJF=TBd?mtH;;>gMSx2+TLLO{56jO$Ss2jk~-R zo#SX%*k8vhO5BlXX&_yd&#n2JEzy$?_Tefcodv0*NSy`wu`*)1Ok4D;z(R8o2+G29jjGU9>oSgXmYO0uwb)6&M`zWFWWr_1EBy0uL+>)IZ0 zVZijDoAO6{$vD;$m2N^1dA^Ygy2>TluVW5pne)WrI2S()QslHNd{wB(AejQ98?mxQ zQF}Knj`*}W7BX>gVLG1^JA=Ey98vDd1GN!NrLy3?CJy;<))lDsf>4dJO8)PsdYj0cq_m$9cZRstG4}xaDu>bad^!S~lU;kVGwnyJG3dZX;rTvVo+G5b3#F7yIvnn5XTxSAuF4QN|ak3OTOh)w&>K78P)0JwU8y9*Sji zIL_@kzIp}wAX69R?uoa;-BPkbsUA6yiL0GJA($sy)yTkK^F!rUfQb=?kmLy^kaZ&M z#@T$ui1bpVQI+%#8mf`Z@u|}l+)YM~tiZO986gl0O_nM(B18AOVuBnPE-c(++#oq17%y08 zN8Na$na-e;udjOc=-rW6t2Xr}ih-0euuw_wve!| z)Tp3*WWZq!KbND!Fwsc%vTBt<&z1*N2otbOyc|L^-9))(tuMPPft^@|CY!ZF(VZK3 z8g8uDk2JM(Nz2i-vP$_AkuXRVhv#!(%i+cYu@+WsmOAr`HSuOx>L`?>fX1GD>ykIVhwCeE^ z9s+r@9HB5+O^{V-@0RNiHDGg$*k#$}wKuHe;R1*Slc-CGCF`M7$75t@(jg;E&rO%Z zm3U4<+{H@SS7;%DR06@zrYTVb=~pvoIVVYIwcfywg#jlB#Lqa?fX#_gm(OYhHojsx z90OQQVqREw3#1ZBs4hQ;Mp3=x?|`*=KU|Nb1y*nIkr&Lh@{wduhB*O6<7o_4XnD2JV*u!Y%j$@U^nsUBe7QEHtbHU zyX@LoKegFtHCB-8ksuu!#)P;h(?v~LS{4>tR5?JTC$!Mf=_w@Rtwn90=*J$e?Z5}OI9;`yj-eSEB=+s( z6Sz|*)mSalk5)YhmaFp#H$$W_G1}vy(jcgu)x>q&TLX*m69YS1cIq?Bi9bmeYFLWu4=`~&Tn6D6X3Vt%}A$4W%!jldKc&Uv56CcMX zPkQXOP)SUrs$84IifveC}u$hWSC^5QIH+g83^P%Qd8Tz@PtDF>^{a%tZ65YV?4aczIKhC zkqBkf4&@54;anv~=~9@N^n$~Y?CT{1qe+mMupFe?x?;ZnuPg-?c8ra;uYM5x^61Z_ zBQVDZfPyW`s9;MZRlt1}I77yIWH>5#oV8dvCu)IoDGfF{QrGSAp@3T~8 z3xf0<)3UZqE!cupv0yLmN)x=>g|#RRQIV)GG>`_N)*#11juz2^V9%fzi!swP!<`i1 zC{2)=z3A4Xd|BhkjAjNLGlR+OELPSqXi|;m0{gFI*9w?mKdXW*(-^(c8kX zJXWp2u{F7&b)FjtIGrGO+TAyR4+UN?s8`IEl04iP=2(++pk_#h&$`=9hRAm_G{Y(c zeIDk$sTi6F7vw%jY@T;P8J`v}y5b(O+;cM#L}i0g$ftC?dYQ(JT$-tOlSaCNCzuX0 zU%Dhybda#+3FXKDI!l`t`D2zc_-5)nWc*?iNVZ&vN#6^N}P5C)6gCP=h*ME z3#yq_8fFo&8}`#9?sL?*Vw$`O+cgbmlnKnN5IC6$?BPmI1age~ZSy5+(WcJ>u@Np> z)XvrASX1bRl=CS(8YRK2Wv|B*WL?jD#4K|4`4SURNKCXUY8saMqJpsfj-2TrE-aWY zmm6Lz754jz(Q3%wgT=VU`unt}pmx#>-3&$LG8<%q7?BH#f$(5JWngDtVUakOJfBtLl0;$2~U(?K!8f@B};+Zt<8%|RD(6sHJY4+18j&{Q?$oy1yv_Y!! zWU`?W5{|Y5Y$`IyLlo4E1rrb`Hc|43Nu@|A(=vfgpR*DviS4%4N{C{zY__URd@?p= z9hf?tr-}nK}n4YM~V^ z_mV=jRn*Zas50b{{9+~v75sYDnW-zFN)vaUYY93C`T*o==AiRct=CKqlAvme|9l9O zlU1HX{apoQ9Va0b*Np}fR?sfyLZh-)jE1{XKh~7wRz#kI$h zZ|<*irn$cRk#PP0a`H`9EXJsA0!=y_9F1eyzDQPzB!eVD`Kd%1oH=wBrEEtDA!U5J z>pzmoHz%`f0)H4~*YkJrk!6a*XRs^KmTGUvy+ zVwkE&w1I)0lW}((ZPMq!?B~>SICu4~EM@)Gc3I|HXuV(v=WE?$MF8dF43W!_p1uL9 zT6Lgg{#>+@FKApMmh@v)B`USu0yr6+SEZbr&o8|b+!y%T@{gClxcsi= z{&HgNJ!|)@(clKbPgcJI>i)fWHNATC%73nWdF3}(?p~o*ZaVX|Gw(Tb&l&p6vrhly z^j9|W8;*@7aOdE?>wD|j)4zTC#i!G!Z$9;(r@nmZH&5MtiaK@E&X0G#yz|>T_w3L+ z&)TB4e*)M(`g!(BYH8`Ll2k)_?F(yPn78TY*FHaQ(?4DN(|Mcz z$=aXH+w^N|Uz@k-SJ%EeZ_}@=eP!OJf425#^EUnR+LsTSwq-%g*0~1SRzjV6cVbPr z`qI^x&fD~!)q4(_HfbssF7imZoF`+AdXcm!cIC$_Kc2Vgk5)z=I}`PXDv*~wNMjoq3D4J!6eqGY(7Tm~LZ(dEw0m z<2tkHn-<Iid)0yFo z*3ooOQZ zt^B-Ab6dH2n`XDN^ESO>>yCMw7PpG?Hoa@>F0d*)H=)Am|8Rg>tuk7?VSt^vOe&2N zf@E=$ozzZh-lo(JHE+}8PIBI+iJio}P2)T9d7H*|V)Hg7cgT605g9QxzI^rN^EQ3i z>dWSBdTI62yiG5zUYxh--s*n+zZ(}WEu7KLY@Yu0)6l6;oyzU}^^UT$zCGA}%+_yj z1vWpk`GSpaZnQT}gZJ_$f+fpst6y6!t^DK4%a?z+{7cKvT>8jTdhw4I?^yUQfVr9< z_3|&Q38~voE$nJa9Z&Olo|61|wcv^Px``TJmJ^J#+pIgg^#t$9){H!*HMFyGW`Jmw zHc#cEoop2p?5bPl4fWRjZ?bYl>onT^Qn#X2%9UEB(-$s35A=85>@P{EY1s)gLB)+W z^98urlwdC`I17ztQpu8VMGX>EJOmB|&)wf)NhVFY{9MqF8}y?jJp)W@x1tDz5m8w8 zWZ{&ft0z3|Y9@~5*mNS&<-B#7r&DwH(`*<*yHo?Ucxu|-=H(#h>gKx^cFi)g$v8zc znrSo;6$7oXlJ$p5Mz)D(g#wvytw+z;%K?)YAht8a zmLOM~=h+VJM<4|ACW8$M?OwUq z!o*%S8$xpfszX7gcs-WvlQl%f;99yvc*qz+GG|qBfTt^<8Zh3MS(N z{h20H_qdBx&Z8%jr553dX?Y?NcE=qY0XG9$!Q+?u>7tgR)3J^+cYot1;|2YgCX<9J zPAK07ml3fd3@#{?0}{yw+b}|lnh;7!8MzWr^b+34JLm4lVlp06O~7PyF>646aN|Af zt+lgRE~7GmBI|X26l-VXSZZ!wh9;w3cA0%Z3nw{XH~YBWfyo@mJ8@`O zy9^!l`mi1noYAXv*$H|z%}7IiaZSn9Vl9sC%jE$Z26f@0DJt$yF=8yjK|~_ZL^=t% z?VMZI@xndo<@2B?(-A|_W(uTiVcG+Rt7Hm9IshfGa6l7rUTM^$4%Ag|p_#VZ6`SJ_ zh;GpR2W2Y9EupVF6 zEx^Ed%eR1DO-CGb5fn+_k+!!%ID3Ab3FHvg5$#C{L#<-q=ybhH)kPQH zip_Dv_Mq3}j(9WZ$#g`?$S0Xpf#gd8o~bDi;)!NG=`!atzaK@28q4s_gq$MkJ#Fq$ zom@nzm!EmiUyj2aBv$L!L*T4$J6edA%cwsf2OCAAH7^_pKKB(Ja7ck!i06bcaeiWlUbXw2Q;xa030Xsk*a zcyQpWI=YTdF~;hmGmDf8B3I=NgzP0q%HJ)P+j&nN+^3sYE{o&e2)Z&IUrjaXQnrGW zkZw!_2leTa02dTjoA$Y?JTwUSvff0YE%b_s`9->-<4+t4XD{Di@&bleZ+qJTDh_`2B;NM@c}0KO@g?s3XTeYxV&g{{#oDXP;ZMxS;iZi+1_? zgI*ujL*g@fl`cO6^lCc3;5F)@ZqKa;DqdWycBpu^CGia7tusxHs!D0tGVO|)i(yxi2}Jm8U+ost95=UI7RSF1bY(jJ!+LW1xJgdE4Gm4Cpxgp) z?9W|z>%wX0sed~4o>ST>C#d@O)}6x6joaVae*boD8`}Ej)~mK+TkD%&-hACA1HSj) zzj4n-bYpe>bL-0b&e|8(etC^qyKeQZOLIMMc zcfh%kT7;fHtm8(z_O_8u<|Mw7PTUh$V%C0jq?0+}k94y3c4QUAzxlTC61=hYW`tIG0%-n_LVXq{X4PdToBcSOwA<%AllXR}SUm?qjZ zHrsnP-#F67oWxgBwr?U&`sQ2GAS|K41x^NED#3|lqEvDmyJzZgdA#1uH;)uNC*+ZW zH{Uu^@SLQR3f_FfNWpVL3>Ccjrb8Xg$Zs*tAwPSlqxp!hq$98tI%XBF;OgfePWXxR zAFE$D)Yp96AL(oLbBFqxk91OBtABE+ulXPj9P%^s>T8-q{^|3~>1^?Aq8M!=Ej=0* z3AkEejfgeJ(G+FhekX94JLadolCE9W?abSP=K5lO@FP{gusM~@PdTXx#A_~4=0_MR z;zHP5U(8Q6b$t;UDRb7{fhqb}`rDCr%t?GDg@D{Q$DGgUT>6_R7#0Ww$dkcOTx(eR zhmo7j33Q~}rN0}w*_@=4%3b=#35R(6MRDoxM?N}d|3lMQ`fFfOH<`;Tim$4;s3BP# zSGqZ5U_DTFpw>>KS>aaeBTna}I<_PUxk zl1}PywKh`2oDc`0k{i83%u9Cs6=GE$y+X`MKh?!on;^6~Ok+q?`U6^y#w*=+Pi|C2 z>m~X_V^GWB`)gfT`Q*qT=6t6cH{EU535mz8Y5V+0XLI&Esk3c-1Tp9P+a$!YbENh; zA%<#SJ~wiOIhntbEBIa3W&HN7`}hABKC`g>#*L?}yb1jBbNcg&$mNE4E%vofTYPvo z!sfe=9&k>(Ky-qgB=bVrN$&IRw?D!5(e(T7C)mQ9c7bTp%uTi)f$P0f7y*~khS!Jq z{o6UCJGkg7hzubKH4N^3eLZR@)Ow&2^#ZK=dhh15aXQ=H0-R z%XQP0U-QJp-4l?`-iw{jmHok=OIJR@?)cB4D{C^_7LI$e_h;+Levq@$&XZXLiPWl1 zDIvjf&}E=P9BRXza<6K5q9K2`rM+v zILqfd2`+EQ@8{!>kI_EQGc(VWotd5aJlg(lk?EBh@O9ZU%Ouv-iS+xaYT6C@c|+(cSHiZZH~)hicg> z{8Bqm-G^CB(5>DiUjC{P~K?ELFFfJ{O%_<#fxK9O+Wp z9LiX<6?3}k0c$GV=l%aq(cx&*S^pn76dIa5AKt+~7v8o%8{V>S33mz`qkkQ(jl4Bd z5&T6^8s0Hn;QyJQ8};Q3eJ$7q_qnvd&xosev=Fpr^3TTfcnZ3@mUJ9ECD4z)``4u7YI)XJCP=VClda zSl}vHf-*3`>B)tzf+Gjczyepn5d&vnfvezflz{<^E_4+fHgE%yK# zJ#YOdQ3eJ$fV!|t-}BagV&Du6F!!E+5pdr6#RF$xL02K?tv>{1V1U!O3wuoRy!8hU zoPhy0*ZQe~g9gsP0=Iz!2hP9(w}Ast1_n4Az0hr7|A8~Gz*Vr{z!_NJDp-UvFujV?p8D!i$9ajecoVG;-gFQ}BR*9)5MW3D5gq z#XpSq4anp7ZOjFJ19I?Gf0H}LxtSv#`W`g%{$FG&&F8|CXfcpn8uccebSAYDJ;$@y&8zN_hvo2sVCuK&6@5A6%1e_GuLg!+%o=fv!=S?pQ(-p6H6JC#0az9iQz4jQ zr@03EPH{$a9e`gUHMIt&)M>6~^_|jg&2<2d1=w7JFtJW^J+tq`=5DUNr-ldNVTeqf z)yWMk`Wb!ahHb6`Ff7!j&g`TjiGkAxy>Gq;5yBv>3$>{;*mMIQJ>3C#7gAHF!*n|h z%HMaoGmj|_!nRPD3c$2FLxHdFv}PF!24G!CO!;AoosQ}4JH;6ta{#`D)Rd3)BhzArKrcFX}77iv?UPAaft zQhi@NXUn`t)agN37-~~Ao38t#r#k=-Lu!hG>2^A%tM7Do>zD(uFBGQSFs)9PTl zmX0~V*QUahi%rq_QBoY><4R(Rgei800!QB|&KL@){uvljQ%<(#?0u)S+fXpTW;q-R z95AuYP+;pju{no=J!ldJ*^iG***m#`PA2-!4LcMJu<2f#vUO5{LxFYvokw<^wQDHo zrAiotm!URAu<2Spdb$Jb1*fL0Fx}2jVD3BJ-G+hz_8bdS7MNCNC@}S%)+|H809$N@ zDKnd*@uQ?Tz>ZR9{SUzUp|J}Ee}k~^Z=VPDdEh_hflHH9<<8l^D`9ZA0S3kS$BO=r z+^S~{gEPR6D?0l(!2TqXA7Jwp$q#xo5S<@;|McAH%a)5u7K25j2xTLIP}6TV`-^oN zUazuC^NyMoQgKOCq{mp6SX?ptY)9q0ug^|a{sT8%x`|hksUkc$$KG)1WTiK7mZJ0H zoDm&okEFBPO_)6+?RKnw$Y48aiFogEvx+u#NR=kM=~aP9As%(DA_9aDhI5qWG<0;{5UiH1jXX?xkIGRJXsM5U(Msg!n=-e9CuS#1~`V*lt-3n7pL)=Qm^khO0LrJ0ooB7Ud9e6{bbKWzQ zhy~F}O^HG!7RzC|*`jyPI0jL(L5_yvr^Bwzw8I|Bzd;Vn&&LAVghZ<{Vgdo#&3NlLwGPQKJbJzdwvG*g7r2|Z-XCF&v4MFyQ z6f$>Lv@vZgUMR}rvFMkbqiBv}pXZ_gviA9hd_&oL;fT_xJ4m&uf=g0@KJDY@5Y zs-#T*qOC&Uv2?9wlj-P{>9(X+_|W$qG{#kVV<`ng=32}J2bI@5nzpi1o+~w0wUi~h z%oj=!lq)V(TcRq9FXYPDnh~8bvoe`8D+|V2kP{8+iE1DaWmdmKwEed zU)_m7Hr1XgcIG^v0i1;;%jJfmI_{~E)`YE5q%+o) zX%|h4a}hG6w1wqHO71qhJzaDnYe#2 zU7k6d&dUvqCWNs(Bxdi-;`cVW*7Tg2>1fSd3Vf8wHJX#oI=S9;536%aYMya;kc@xG z@mj4=O80tSi*mZ;A)7N4mZVLEa6MOO+g65S4TUqmGGx&@J@HJfmQTwQcIk&7uWR~h zlrGx~3JYO08&a~OA{BA!rP%;&gk1V%vAYqei+yDqPDH|AH{*O8b>W_7Jr!rOOTt!?aN4=*U3&GGZ19iHYa}&v~a--=8G;?~V z*b6l`QOnhmy~ZZC=JG~aN|`V^C?g$KS)}epBVi|{F^5MM(Bh?3D~%hPIj3Cp;bRlK z!#0nvlrnn~3Poy_O_mb3V-kZcr!K)D06BZe;P>Nn!!N~^R4S*8%S&*r8mkr}iD)67 z4Y@1vRHl%{n-yJH=OCpSo6p`@sq1gMq*L_H3Z0p%!nS_O2}fKG)Sena6)2{np^Hnp_~qCZAN)gS#viG z0li8*+d^RWch;cmZ%OZ`#Qxq+nN+^BW3~(}1v)A{dqI-^umSR#)Rys9S($9 z?+k0a4T}*kY1EO5tYELJQr33DYQweCraRlx$&*caAQv&)a}_J>2%W(KZW@NFwQ{U` zuX}gCX6Tu-m&Kw|g+ih^F%)!0@j}O0*;J+s1CHe7r9`S7i3Xf5Z8=)=DbkflATDnj zy$+_}aHi|{%4|j@4%y_6lq4Kdw<&osYIS+Zw#7$D6fOlBshgvc+$tQtpYZ_xtL6WRuK$tu-Ue}cXYA;CBj;ithrKsEUX7KPu($tVCgQ2#NPwepjJBA+O3_mh_!`Sm<=Zsm$77JhE95-I) zuH!P?<$@v3U-&oki~LjhW4vd1n|TeMfp-A*0(OsJo8TJZ*9B35RCwXATIdoUIr^v3 zZ*v|P=EHaa-qBBuygBm2kqwjlk+V4Khtoql$M>H&b>w)?8G^rI=U`TBF?b1l9b7nZ z^Z4emyZKK75*)#OjeGyNeqwC=*@@!Blar53-To}vG467L$feB*G2<1H zG0H(V%T&9~REV0X5_bB4LqN*6$nR0srJSFA`6TIqis6zT3aTmn#78@$*ET; zwFPmT5DThX|8+2195|s=R zrN-OFql&c6S9Mg(3a_G+B-^=WL}2b}SX8RGQY`h@;U`eiXP3Kbd0kXom3XpE2Dc?r z@klbCh*l!FfnV%u=)6f$yJg7wQtn{OYL=$T@XZu;>a>Zl)1DVAi%vsM5s-OGxbTUt zhBj6b(}9YlQmgA4`Fh(d&6T6Npv2;J7bR_1%TTE6Y&CZo&w!ur(jXG8rb?x&lubb@ zM#ES*Znw53mPT77k*&MM3?B7_?9G-PTY)qPtt;!UQ!!cHt}#n-qn5U~!*zKrW=%RA zbSkXYMB8oLZ)v9e+|^wTjUz}Z-K3|Skej^>nQ~~$$vW<-6xuDRRj*7If>lbSH!7qB z{xe+-l^+lL$`)@_8>&WaDv{A+f)OGkI+}?{%PJ<35KF7pxHFlla9?IM2vNZW0%2D}?9*0EIlD2L(?mt2L@Jlmg9?X+5<3$*l{jM57o%a9 zx9O_e0l{iep+-6*4N_V>;d1$dQ3(|a%2hIVCdafa1su0HZSYKdlV}J;T@6v5p#vqS zH4`o5q&X)HxD@p@S|VquVen=e>4Zs?aEV}iu&^3)Aq{IgmCmPY<}l$75`}!UDIydS zMc8jq#XUJ!tKo{KyeYpnpl|!ZldJ}v_Gp-psX?eottV(pSImSaBiF|5Nhcvw(*A#WjU&E!oM zx|;+Q%1D!vN-|*%xayXy#IKBd6A}%PQc?bSGIO_pXyoQfuyBe;#DN!H0=DW(nTrG&v+ zb{Wg!f;;FWWp2_Za(je|u7Z2I4Wv}+H`?V1kycx8H1j&IR!0Ss4j6yb z6|`W7b~Wr(7(hzaE|*N1v@}KgqbZLu8AvnPmP(z{dgNM-IV`q?^m4!8w62C#6%C16 z3Xei%%O+@ND`GadAPHYsluTx7M6{tW*-chgQ(aF0Khhu+7Ee2oug0C?Dj|1Pe1%Ft zr8G&>v@P8zdprSm-4K!FS~W{!^u}%)Nn_YfP+Bslij?$9(xtbR92JvXACQQO8q%OC zNnG}P*phHsMlbDZL|rkJD5ADU%Rxz@Y|`V!df8_yOO>vcG@y-p#N~8B?^g!XmeF)q zqmpP)IT(vAOOw)Ig)%TvGt;OAfw%;) z8>iF?d9+aui52;r1w7EzFcw0|M8aA~>neU%)LwSf%jT>BhT^P8OqysiV$Pa9H6LRZ zTgD#kYJ~I(tE$#Y${|A%rHFXoVU37ar)m`}RU#ozn4~p>OC0ev)CCT=s{w+| zN^ON9(QKt0c0xsGX|2^}NpL>fO~YHwx(p3nmeRG9foz^gxx6(|DbmyhQnjYfR;znM zvSKVl#r*s)b~V~&rjk-CL;+Ra7Son%gi0567{WR!jFLt9Td`_VD%VTONk;*@y{plv zyZv&xPE@cIB5tE4A~NPx`Cy`u!c8%Iwk6G_86zHyq@@;qcigoW8ClpIfbR&Gx+2v! zJc^)=Y_?nx+UD~yt+FVQsboZ@wmY4`*jYv)B_c#bC2h*%cFHDBD8gY!D`jZ9$V#Mb z)}7%*O$U{e(avay6lJNpsTuWv zZZUEJZ(L(Ew9S@8(PPoNi*|cXob(qJa&^TnrU(zI$m=weEFtH9zN?W5_@pGBjKocZ z+aM!yp@brqh%+!cav>2el&dbIok|dey1If*b~VzHEEP24Rc*bFXDPYUp0yZ~hMGTM ziTNBPQ7bmA7F86+?sZ}7x*Ex(+oRE><0XyPp{?4AdY{=<5BU^1yGP=SMKU@?)8GpG zn~V!w)76N@VqPmAaTjD3C7I4vbuF7)*{tMq(qOvc2{Cde7;ktj)oKIV%xb_Xnlo5w z%1N~`?GhUrCYOVhh~?%$x*C?*V1UFFZh+o zhnO)r%5e9sT5u9dslA}bIq(J28QG(>wwbcK2~{*(DrWT7gt^f0h0HBVVo6ln>8#nD zl$09<+O7_ClL&hpRcW@}Vw$mLIw^6KTI#Z+T`A@@ZcnMHR>PU4wJo-pMRJTSc)&nb zBNaH=)#&xK40kaGvXTqw44I@T6~>KaLZXRyY=)$$FyZW`f#+hHh(2oc$`mT8OrO}rni`2Wl%`Ae2=A7zhNfAKWX+DcK9g|CQaZRDSW9YCevQQK%u(57 z)32*rOs!I_;^*Ai)lf784Ut=^%|;*#YOonkyR2duY&w=o#7c=m(Pc2!s&%d2pRRD( zDp$DFvV>hBFI%a6U6PY(J&6+82q?uwsi3#FEnc%LoKJbfiHz_#lm;PdDsaCu*N9Qg zh(e>RYmAJlT`wt9s$e^7^~nsSSXHc}Qd)!XSvFTzldfE~NXkt^IbF)DnkIcLA=O$_ z&T=Z52&F>Ws==4hx1*H_cGRtE>fRa=h7ris5t-aA7cpsd&6Oxe%vxDBP&Ze#NmWDb z5j7gZo4dKvQc0h}XKH73I!&q74BJvUnN$roYBT{$Bign{s+p3{YPPg+-Z|Y|;rX1Y z5(=qB1}a8K!eKp~Noi&bx}>HDw<46O~#l~t5K#5 zetA=yGE4jk>~2=W>NIDVOh95%k09&yJY%s7UhQf~YNVP8;2~|+l$NF3YDcszR~M})^jC2X_{C0z}(y`|Ss$)>~RO%)6ZuQWu~V;W7ST`gu?`bw%2OSl|XQ?!6j zT+}T`p@!oke+0gkT&YAbDmIg?h~HaMdFyVaglxN&@^V~hYRUDD(Wkm;5COd;ES5FG zexEi(G-@(q!YPjj%ehD}L`P)0bVzTtI&Cq7Vf>P=hNY2cr_5v{EY(=uMLC_I!v;kw zO)_nfsO`zdtz<;5gNwDKUC@1-6P8S^9+EYKp+HvR&ekoKc#O`ZH6^VE56E10XDZ*Y zlQ77+DS~w;qJ%}KN=JkFrdrhq1}#iI-41&^5z1i6z<|kOLo$FHvPOyu$QvBA{y&v_ z+R)@rAOnC0asd2x;*N>ZgnEKAzHR)o`HtK54bd%$U2KIc)+H5|ChKlI0;uR=b-+5XhnnM=8yZ;V4j zoXcku*!SCizyt5sN?s%tOEi#8R2r1Py(O_&B$KFnK3S7yqsMG>n%!1TeNM&R@I3_O zGa7wfyH3lg&8gTM-vSP|UaO688cm$)oQl2QF9@FxcR3xj+s3KPsVMI$qn4(;0f&pB zNltlAMQM+s&1iAbZi|btb4qh6_TC%UIvAtNVW)g{PH|4f-g~Vc!tc|Q0hiOsDJZei zmvUzRf-mdUqaZE0)$L|*;^eu~6Zf82a2iOLH{dtnZcctq#ol(qxCyP!>G2wjoZOs> zz3oP82sjvz1=o8x**O(^+l?=vw~&6X*Qw`Z=2Yx$H>BQ0Yh4s+@^RqYoE@=d=^NtS zc4K#W9k|i2_Ys`boQkrZc4PE7{SK2!Z+CH$b1L>WI1=|-ymlC`+ssMKso2}#C@Tp! zBxo(|}@!KfYst6Z8%MEVhYT^w+7dnQMcZ~(74a0bssoKGnMu>H!tNj;1)OZ z&Pj9RQmX0I(~^3p8@D?s11CDCVsA4w(Qc<%Pw7kyCo-pEZ!^_16yeaBj6NR+ZlvwC z(MUDD&6LvG;4X+S;IeT-b1L>WQ=P?c@>ENuHQ?a+1x(Sob&G?LV6DK&QVsA4g z8Jpee_E{V*&RKIR_SU}1q=WN5omFq;oH?grZ!>k0dWtlg^lpN4#+-`1&D3o15Ei%H zuQzc{pR2+3*1ym0bCPyTAmBN1uJrb}RP1%bA}_x!Cg3vq=zx*o_~umHqrn+Huo7vP-p}#Qso2}#+=O3G;AR5HIn11jy$#Ny_nC-* z*^Uz&&zy?A4bEiN*(is_Z^1eAoQk~-&JN!hHV0ww_&C&@ioFfa%$N)&3+W7y9QT}x zy$z1EnXNcQdyH<5Ypw>@TaC2WK{yP!)<$dQN>9~0!s!`8XEzvKb{&VDQ?a+fIn7qB z#pZT+NRD$(#onCyDaPrcZ6+hham=aM+u%qoWJITR0fOMz=Tz)%a9X$1OE6Bj_0F-) zso2}#+&&_p)mfeJMNG`8*xTT!fZgOYFg}-;W0_O2x4}8wCO=M)K7);8ol~*5!5RDp zt3BYMtbUGZPQ~5^N4Y7#*QM9FJRI{}4X(E~JX!%Tdqn}f;x53#wxSldmR@%fd&Z*d2L_3@`*qi}3?d8A^PJ4eB zD|;K9&WYo0x6Yu`aqu}6dmEhI?woYQ7y zeAa-+XX2dNx&MFs(AS41zdY%fczfc83ElXskNswBYD^@2Qh2s-+33Tg#nD4X zzBdvZ854X>5D;*NHxE1cf8~FHujl=dcLh&_J&#ReCxEBGIdE0~Q*Md7nDc#3oHGs$ z{~P|8X~<>%3EOg!hn7hwrI{)PYVvYY>B%d@$y8QRst+(Z0+}d`Z_xY(~TX+2YmK$Tg{>2Nc&G(=2WRjVN9Oclk(dL&?dAKoO zi?kxjkiVkvdc%Q`-S4ou^F@c=8oGuL{D#Wf{?~;sODnf+{MyymBwGMqWlJlE2C4(8S zR0BDaJC&4o4Y!_H{jHNEU%YDUq~t#ylSs1%=xPsdJo4F#4-<;wq0e1<1Tzge#i3zO z(dKXa^%{A#K_n$Uo7Ya&&2^JU5jEuevPu%yYwB^_m?6E9u3`5T+kdh4pD#W)I_>$H z@5lEX@y1oZfBfjk*-M8`E1biLo|0guAy+swY{{vUWTuLg^I}b@kn)v+ilnNQGZCpeowsyzoWJ|M=w_Si z-FII6ZTF?aOc=J7uGBXXiy`kYU3=-6cYLpeboixRrt`bE_ zJdT`9jK>3X(wETDb~@rLwOi>**YMrThJWyz_Ld(_e&Wpyw=chseCC^v4L{>J==l-Z z0mWCp_S2J?X~@|P4J%7^c|ujCXi?0XFNxdLK(P>1INSQH%wx}Ktzl=mL7CxZB$??N ze)Z0s7k%69eEHKi-X**5`s908er@8+C#7pF*;jX%&rhv+mzjoK+|aPK|F0fh!$&W6 zQNM|tcMl^!l(<##6YXEGeJOI_Q;i!ooKV`2zx_d)nT8zO(C`3Enq9*ufBO3N;miJX zmu~0D$6jz&Z0q*dPTcbRP5vi8{l<;Q-c`IS#Y{u)Y-qURGn+hldB_iUH>rwGqb``0 zE?Oln_}~W&dlPR`=_*~Siz7}?*YLfihur6Va?KSE>x2LN%LRYKPkrmAbFTjJ&sRUV z;!ln{-zxr|nTDL$(C`3EoL$3T+>p9}bn{*udGYm!OV^(9=*x;zG~TK zcQekAC$sW*Tx>cMLnNId@Wwdo&rDq3HAD zfnowz;4yeYr0C2r1fI%itfU=&C1YK&<;A%1@FD6M)&5s+=Ul%AY&hcaOOHL#VC5eB z-Q{m`y&h&7a#KUY8K=B$tdvV;Eorox@tT~$U7~u;Aup#XyU`nRJ52S6BA<-XuC6OK z{8@JJPhUOkfCrZsPk4b`^tZoX{^Hgr#GdQ*kKa?d?ggkn4LPTwVXw}RaN0?0q3AZK z&2e=}pJ7zGU;}=lXJQVrjhD?eM^_9fAsLpSLt^&{;^qe&}*x9FjpMZ z{NOhYbdXm|i7%&y_bj{g4B>0j6Gq+Y{sy84&kw$pKO)^_R7&-J}~ z!O;hN?OfQUAxAVcEGhfyk)$_i$QzZeaLFCh@^9(hAjgFR*JiZ z|L4Bf%Lm=Gf2ob%|G84+kB94i_40kf_VA9f=6er(Ysm|zFw>Cx85$md*RpH)5ar4T zzmY!ZA0B$?!9V}I^XWH_IsUavuUc&2(z#}z**Tfe3I|$PqlyW#0!4z z=7$Q~Ml;Voz*D`}T)xD*_&Zm;zU`)SnQ6$u3=P-fOhcm5H)VQ7w#+aQD(6uLLQ09+ zUr>_D1TFPtp; zawbE=nplzaGNPD0Q4(9)WV&svC4BxmY4kMVi^Qh%+r%*y;g%CMc07LjRi{6AqjdD^ zN6CKs^V2t-dD)`}*;3Dz|F-74N4$1x^SD1S(*k5TOxfxZxvrM7sC@2Rg=*2+RH|Y1 z*(G=Y9;6SNN`?U5s7CUBc3zQN_WILn?_Rh5X=7&phc)5LxLz6giTyUb_sN?+0fc9K zVI4C)j0_LJMcH-59r$TCrcMlOy^MEt==5*OkNVncqHi2h*z9}brNXtJc5NDCruoRQ zyIAw2Dw1rBGF2T?xUJ((>Ek3x+6ocM;Z3Sja$PW6g!gkqY>thezv|Lo#9B-5QbitB zEWYTRHy=G=dVfXooXggJ|C(#>Ir|l6nuiQm3`%n)U??}+Ep%3 z+y?JFSsm)4w(3Zh*`Bw>@tLn0zI4KuKl#&x-%P5``bu?D_VokLx%Y-AuRQ10L+w-F zWu`G?cmU$ct}6s>@*Y2ZkK(XTA3b9Fh5f+Ojz4EQdUNSt2c%xz@3?oie}|a{$nXF> zm0iQ<{_GDezw(*4|G4IarzF~Ez2{ERSMNMaS-j`sTN;lXuaYv;Tx56vQp&F32W|P% z$T6o>y$j{4Q@Z+%aETJf9Ohrctm>=gdfk%pF;<{-lZ&`Wj=Z;D^DfAgLL6c72H zk*dTi-}%$zDV8G^9eTm3s_V8Lui>#suXU!2$tAV@YlBD0K2vJEoqKW0j zz918c)~I}yedoOrI(*UPul@d8rjl(7=a9EAx#Zo{3aaw-mmfd>snV~mxbZ1w>O5rF zRfZGFX4|dLT6~(g&m>Fc&G6i-A}A9%d;zOWtzzVDD(-jX3f&xQzcqBsVUJI^4|)6T zhyM2yKRM^lci%hz@-we!ym?>q7sqVZrkSa8k>LTzCcB0=n~ziop8Lv8{;voRC~ZAu zJ#X{Ihc;eJKJESStrPzfD(ufpor4TF+;U4(qYJ>Jxh6}Z=4~ZYwW7?Hv9yiwqi%?h z*ov}ZQR%H&*@^cLju8ChvFnyy!Cxl4=BJDA{&M+uM@b^5Fqb}gf8(|rui1JnGj%pH zJOE{7H^-0c&|^>V@40^Dmq+}WdhmkG3vaA_?&!~)@n-q#N5AmTb(_D+Otq2W0hlSf zhA-kh;^L1s-aFjwR}5e9)C&hc_lF-5>ToP&N+}N<`Qo9>R0|m%fPk`V*#E;*SNO{} zd`kMmd$%9{jUT_h?cAHLIRCXz9OS+{eD=3aJUqlqHId-~_$IrCEzjI^>Mwt_XsKa( z{T;s^`Td{%vgYO6%Fq1fl_P^AXPwYGc9fZFAj1QYOLh%^<|~4CPQB%C@7)#s#@*)| z@PAG}clmRil?PZ)`}Q;W`*wce9cHSI3=eQ6x@-8lQR<(*3;uEF&(2+ahVz}ZVf&rV zyKZTm@sB^8We+z;v+N}GfMsdiisaZ_u-<6O_(~jy3a;F||=+bz@PCJSL6H|+tvbm5K zo_{t~voTkpVb>errN(T=Vv1(NDR@jpZy=z}wm%2uby`V8%Cvl)=X!C%Wr9bjr1Gjg zjW?aN0qR=gb~@2?wDE?GwzTZ}VkInfB*Ug;JtA`{o%%$>j-xcBC=Ih)5ixlZB&ALI zrH*VwX78lcvU^cl(6%BSpkN+MaPDO?8Ypp6OdGSsu-WbMdJ{-CkcYBTMG~GPZ3QT; zLPv(mWv4jQGRtC7e>NM|`^#oYv#8DL8j4yeV+(oo8i(F#NE9Se)d^zwpJeE{dAuy? z-Fo~OdAVt)7s$)TtkYw%$Ras$n-V)}cB#7IZpmlyvR>{i+LeJgSy1V-bXwY|Gw{%E zMCq1?>Lz!~UyH=43Or25ka%N;mw$O-h=2OUp(FS5pbrqd+?3 zbtR(aY@^#$jcLCW9@%Ylnz;pb#ZFgJ)WbPPw$o*ZoRxNDG`Qav$Wb{*j%YJ=0?Ar) z5otCb&Kbo~pEYaIdX=gSlUB4tw5C>;_`Dhl4~Al7DM}kvxVO-<6ZsNlx7%x!F7bX# zUNm1r?%kUHI5lMFLN#QQLF}Ovo`Bb(qjH(5!yCxk(&E_~a#5Qy>XYS!((4Y_G>lXq zF4B0sDIt__PfD){C*<{5NGCIs&6y4PSMzzfcOU;_<>iGMl=TLQ+n4lPq^_zTE;*^R zy%wTfvv?U7HS^_`S>e;eQyKgo{zTvJI0u?JbT%*3bkJEM-K2p@CZdEn7HUXp)s~!&YvXm1)~v4Osi2{x*0semM~v ztZ#eWa?)rbvkjM{R;a}z2Fej>=fw$YI$ctD)b*szR#azmI@!z-z-7%A+EE!kTU!pv2whxV zb4!Zhh?;QJvw6Kd^LYk30tN2s|*db!5xP=8;VkYbJaX(!lWMc8eq6xvo(D=^r9pkTzKR>>G{4sd*;DPb2<6Fizk8c{^IKE+g-T2z^ z=6G>DIlgAxH%^Y5$5)T5$Hn6-#+QsQ9$z#r7#|wjIksc$706()eeAK3jUyXI){U$k zX^s>}k|S$Id?VzDd1Up7dPF?3Vr0q4;*muof{`J?PQebrD}v_*+Xat7HiHKQTLoJL zn+2N$8wDE#>jY~BO+itR6s!^W1f;+$SS?Ts#DW!qC4$9*MFN3fXn5!Fj^S5^pC8^n z{Mhie;RlAd4sU@cBQ_0hgm)m;4X=gq5{tvh;Wfj)VRG0!yn0wYEFNAlykvOs@S6^0)9e^EdG~@;C6;@z=up6GeWKzlQJQlYBFOHDApa z^H=bf@E7wJ@df-L-cH^Q-YdN4dE0r9@wV|E;BDn?fea0scpG^ec!kSHSaNJ9rFi0}p_$U<=p`Hi3;` z16T*vf+i?}Bv=D{kln!yR*!8Pdthwq*p{))W1Ge{j%^rQH@0@HIaVA?j;$HL6EZijA2=Ah-6Rs6D zg+*afxJKv`l0vg^wNO2|b8^SzE0fPpZl8Q?a@*tslUpaZOm3dsG`Vqd!{oZjwUf=s z;$(7i&7^OVoHS3ao>WhYCs#}^nOr=%Xi_jaG_iAH$HXfW&rfWhcx+L2XAwRFoyP7ZKhFZbbM6@CAf7fEy5g9(*3*_27Dh*MaL0 zZUh?{X4?H zasP(!UGBRGcXD?ke24oE!oPC=itugj+X&y{zJ>5j?wbhz!u<=vH@I&g+`-*}@OAF% z2>;CeGr~V{|Ag>0?rRAD$o(V2SGlhu`~&w72w&m8g79VT%N@iBj6i6`tOzZb1)&); zBQ#+qghtGW(0~~b>M=b+9i~I5#k2@<3`e*cTaEBE>@@d$=JyVPr^<@ zcp`QpLJg)tsK(R?RhSB)5>p~nUmk8mZn65(;!aR`sajzzcvTY>Nx>==YcV@D(W6!s~E%dzDMkHU^ZxC~o{a4EJF z;Sy{K!XvRG5gvgZf$(tbaD<0phao%^I~3t3u}>oW1lH{*Hel=gKOg|AA7BNjet-p_ z`hjy%R6oE3Q2hWSK=lI*0M!rB15`gi2T=V0EkN}HaDeIutOlrlz-a*04>%Q|`T?f^ zR6pQkfa(XF1W^5e69K9ppaH0UfEu9s0V;s%2PgrmAD{rJet;aH`T;V4>IX;xsvpSQ zf$9f{0jeLc3ZVJ{CjeAGKm<_zfa3wGAFvXj`T@rQR6pQYfa(XV0H}VzF#y#MI2xe( z0iOb>e!y~o>IWPJQ2l^q0M!pz3Q+xkB>>eAI1-@x0Y?B-Kj3hH>IWPKQ2l^I0jeMH zN$^Q{YkT9aIrBdJ_;{f6{XfXPatJG9r(=h~6~OPn55R>W1KZFf_bu+@lfQ;5sB0(l zlitb26Mu)RqVG;zKXL8^J+X4)!11TxD*X%N?Q!q;iR1gh)%W9LSB%xh0%NMN1BLGh z9}#{@xL)WKN`;4vzBT&v=-s22jOIsIjqX44&d8G^_l{gYa{9=rBS*n?_^$;I38n>U zfm3k2U}X3&!(SV|X83HlYQ~4TkVoJ_{vG^jeh4xU9s^gqKjYmASGPHyiMO0Ljy;0i z%l!uTN^X(s=YEp&PtJ?*-oXu=OE^Z(v7C{iKM(C3wGWM&I8VLraL$+B|L4Acd^|m{ zjK@7~oC~JeXr+5b)C8aYB#*laicGO0?}x#vfOV|4eDB&yR$I2Gw&E}zR|XAB*+(-& z(E|CYJnk`Pa)E?>*riUM{nETd-TT=H$PR|;(6*RW?-HT|DQk6AUsiiRlK=@@-V<2$ zS-d4?%PV3zT86rQM;EzjyP-DgnFcE|!!s?` zGj;Y+uVWy-Q;O0f^;E>rLKHIQM;lw{S}GNsuJC)l57DN~9SiSJz`$(AX`ip(fef-O^& zeY9Jd;%u2B?8CXs)cZ9H#Mm;0S#`EdQ8vRN_UBp36k$cy>|G?xmMO@J%qUZcEz?=- zqut81hAq>X?8CXs)O(%{1lcm3!K$-mI%@@dGfKGN^xdsW_85lFWCa8B37)|g!p{oM zj5P*MXZ_=2AI{>!0P7zwD>B1Be%3U@KHAMcKGr`T_Tk+A={>p%ysUp{R-N?^!xo#` z-Ku1dDTap?bk8S9vxRW6g1h;LV*Nw14`=a@n-y{HUBtz@(7}q#a3RU2Zf76u=0Ydy zLL2*VP8aUMr2!nQ3kg=8b)lX0jCFUbd&I2(HdfFwpCG{&!psWp=0Yp$9~1j<78hDr z5#!!P%&ZFytjG)(n%LC!?4#XWXk=ZeV;|1#!ab%z2G)gIR-JXBp7jjAyH&{^Zx|gb zxOzT8EnA4wSi#*~h_n7Vm3=si3sT^@XVkk}mF)3CJCPMs%_pc~3!!8McXOed^^bymIExEao%{cy zq5Fm=Z=ZC-_5H0A_VK@te{tMA_Qu%tV>;n$!mEX+j=l`P`fEmB9Jy>n4%g~af>rQ~ z_XWep@qfubhyN+wBQPew66|5PgK#MLG01^~x!>m|x%+YM=R`PT(DcIp(bEcy>xzMU zm&PMehu`Qmwwk$Uk-#(gNDn>M@?>3X4nkUA(x1%vG~tSuh}kLzbtWfiL-KB!E5$^V zrHsm0Pi5$7IWoJY53{9A!jgy^2%^EoD-qlDln0tf-1Hl3>17!0G7|kl8!>Fss(bycI~7>XlWa zZ9zCKe^* zl3c-@)<;}kV?t7oc|20c&|HURk4I)V_hGi;G=*}dl$?xI>$-|8E)R?9;j{yuOjME# zqg2Q6V#ox;ku)m?dU_=?`{h2&npD+h(kTj-Ws!~qOf>YS%piw5oDP*s)lq#VtL z*zGOADmw<5y`>+so~DEGW=%?+E!`H|Ep|$mtBORLNYv^}!26N*qFH67Q&Orfrl*fa zW^eAptc8fF4R}3RHso4Cvpxx#3q%ROG?vP%GBIx?qxF?CrL51Kl*3;3DP;DhKFkh4 z%0f>sM`kzmVRisY7JB+9WcG`Fm>qzSg`Qr9%-*;(>P`doQ~Pg_hEJbA{H9*<8;hk--p=&Xjo{- zj?*!FT_0u#AYq{)H%`aw#y-prK*2&oW}J@MYnLXewziTgM@g@_%A`XHU%t124M4y` zLtdPY*=zbRI{^I(4OwwIX0PtU>;U8|G~~qTn7yhWv;9-A(2x7@;=NCK(<0d4xEnJ&-7t-0IC%lGT?N~ zUe<@%0f<&;$bZu@d+Aam8F59DF-I(1j~eSaXTID!)C@qgLPPeOj@eK5VRitL6&iBi zbj+^n!|VVQD>P)j>6o4F!|VVAD>UT2>6o4B!|VX`Dl}xh>6pEw53>W1t8~`?*wDj6 z!Yc%aVpqa{{I~v18>iZxz}S}`%2_@D#l=De#@0w?3n4lH!32sPJOI4|k{^I)V)lsl z18{cC7Cm^+9i>jTP_%6nfpyvZ?nNP4V|hp`)Cq38x3B5F*S0%akwU&2%U7Y=*~@0h z2(;GO?TbZGq}^SC?3GY)&-kGW7PMQfl0_li^aOG^B383SpCvf(AdRdO92lEwb$aq=4&f{xKz076cJ{KF!GTdI z*;#@CYsCMRJ$V32)lmwwTbJmoCwGbt z`{7h&s@ds>m(@7S2WVXj)eqI0|H^(i$SZ%ApxhsPp!yHJN#D;v=XG4Hg|n&d+i2Gd zRQ_-7c4Dz?uicJRms0IjFeYR!T#<`cS?qD?N?*gS(z{J7VcI`65om+gKU3;h&}!WhO9&55C)Gl~~FB{syOVL}zI7K@FVmXKK8Z9|4ANFWnr z+DpV&=3Ux++mtCfjMcg;K(y<5nY*-77mm>hyPc^e^=(r^K{ZvrqEY2`1t@9WQ?Y9M zdx66Ds0?bBG!_rlvYiR877fG4eQWYjTDAtsZ@%_^c`Q=18s!lFyM zR!OL`zu>j6GHcZ>omgJZH*oPPJd!8{DtWWG?1F(KrRAE`xvHUS^!1YO6djH>o%8<` zr#6IXCx1D4+hl9fHhJX4n-dRBTsM)OIBjD8@!!KU{+ErPH7*&);NAK=$IcsbjV*_# z`+q9DQCJe{;o1IIN52ct2Si4dqa!1~hWGq0f)N3a75qc+sNfbsU0@L$HvIbV4~MUY zH{nkjp5VX8zYpGRKb?O9pTm0!-c~=G=in{H-h%hfKab_HgW!GgZ-dW*FuV&R;Qoqx z5BEZ@hr5FFcg`c6n_xEozwx({=NjR1mnMykR3hpRWEy@4)re)?=AOMbp*QNO|% z|9JlKY?kAujK`3VmQ3YTEL&}ab-fK#2><#u4EQVgEBh7Z@{i*mw<{O(I^$UWvAYac z!kLU)8_0U#r(7m(w;OsL$2*mGYQMrf-YL9O`W41_C-YA3R~Yb4;+@p5Fqd~C@5Eis zn6H354NtSnaHbyhXJQqX)2PE;nRcet>kI*;g$2AcFWs;3 zFfYYR^()MWR4~bYg?YRLFVU|s#*6df{R#tKj2G)yn9GauqPr?^zDnUmc#&O(qlQ*4 zmyLMpX-IfqXIj~w3LF;RD!jE{VZQJd;Vu0N^Mp4DZ|GMT6MkO!`F@3g@Ot6({R(r1 z*9ouN<+=I10m*bW?lL^zfyz+;Sw8v|9s!UFq+ekHfP5kS3J(KFD$=hoA3)}jeua4e z(v$Qni~-1D(yuUp^hjI!73KoShO(;y=c^O|8B=x{c3N%4e7Kq*9L*-9FV%d#O&?CP z^Zg3*u^g7`SD1%ovG+&C`Lcmw8SMR0aXw*yrLp%%#rcG}SPDz+^4xsFkl*V4LDA|< zd5Yn%GiwW*y*V;b>G9kc=T6R@yPPqfu#j^H=Z=1bM>)51ZtqukgtM8mxnE%c=gXWg z_bWWixs7vMzruXZmpEVQSD44SmGl01JD=+?&Mlnx$J_aY0q17U&HZMB3m1_$?P?nH zS>|lwY}#dbzR4{g^5*SY_RKpI<_RZ-yOurk3S+_v;Y7dL0K#$Mu6576mbt<);n=?Q z|9@ahfo*~N*8ls~|Jh`rQaw?v7K`_-|3C1>zV&}-hBNl9|7SR3-}?UpXY5=5L;8#N z2he@%{~6BMxBj2ujD73>8O|8qxBj2ujeYC?8P3?Z{-60i`TxcGzjOY7@la!E(l@ag za_~FGZXe?b*Ni?fDjxZa;6=fS!J+;D_J{Zk6+M&S6mDqy1g5bb8U` z5GQu5YiMXhz~S-E{D17d4YcFxSsuFoX3w5IGkenTlaQpznZqUoCeBFmpByMewq;qe zB}=v?OAf)vk}S#kv-M-yN^%a9rgK6F2MSyYByIUw(6q29Yn62umlB$C`D^J)bGdXC z0+*XWpn)bda49QLO7CZTpEEOi&&;+?COKg-K5KdQ+53IJ@6q?Y@As$od!K{-G&IjA z;VzCA1RxUm{L3->+s?QB+{^l_@4*A&aZ0E2)!cghGuM_Nzy1!$3u!2$Pe7iHyEz~4 zg(cFz^SjIMFMd!^{8_xm&m1lB-n@f%FAWp*3Et-;0E6>!?ky2s`;z7NOX?f9>uaC6 zy2P2igL5|xvGsjooz6$PyTo|-&CBnr2OA@O;+ZQ;q+fXl=}sD^?B~tY8EXFd7eO3sJ4#J2nP)%T}2MtJzl;XSPg9~`D( z@4mMc&%LpB2+)1tf4qPB{a2kgoyj5vhMr4HGmyT21MtdI3dgwV9&ZQ-gzxamL zxBQ?cJDm#Z))UVFDvEmy@8CGa-L~gJt_<&7j0a0}|NUdj?>{eYjPd$27f&_h#p4tg z<(|j5VMAVA!u!)dU48%g#vot&%>EMOU1QDuaf)kqCm`?6t8@MZyya~3flrrL-_LH0 z^!3kNSR%b=wn=f5?*!?6W}9=ne>vNH;J^OQ<@cxl$;Nn(pV?dDy=S&b!M=Wi_paII zT%60<<^zBDi_7mnddJ2%U;E7NDGS&=PC@2=g7fak+Vd@7Ioo{Tzf_jrpI|md`ouFk zOQiSAHYwQQPmr#2Rh^GaQQd|WBt9YIBSix{tVm)i0UJ6+A(UUBS|hV(S790SKr_ErZYMD zZe73q^b*Zov(4dg3Yz=p(X0o_bK$HezyJLEOE~}e4pjn*Wyz>jMFH(t)v#|2=lAByJO3iWYDWA|#uDRydQi8X z#d!Vp+fOy*!ExNqk@HfePfkdJ)T7?QFr!JfX^}~7%nEV?Zceh3Nud`}cd~G9*pRC! z@z;KH3G&x|XXA!^?d`WMLEbgnTs%%@WxULggrD~)XkLtXv{#lh^|{W(&bXE&iaTT7 z5kgLz&o$&dv(33(zM2>RV`ho;KfY&Uq_4mI)+N%rW}E#q_oh$m@;oQujQ9BVVu|;j*)hc}x)Z#2&5q}`>uPrV zg;y?d{=%PZigUUDfA!WcY+ZZm=&z0vZ~`#9@@H3kz`5oge)4b(IQ&KjA3qoZj=OK% z|Ly%gV44&5etoYE_}-YE|FA=C|H^g^D8I;`r*Gf9plRH}af;MH$0_ptoS)|B>DzBu zBS=x|af;MHn;^)n5v0frbetmH&-s~Xp1%G1HG&kS9;e6*vlqH?#?YuUaEWQR;Du z)I^&gc-Kf9M4KRZ^%_Bnlc(bpXFr=D_`7QaDM~#~ahkIU zf>*5(q$oAbIn5>rUb#k);`HY@#rex72wt&9kfPM%6vr!@Ao#L1f)pnrY0fLo-~B&* z`{io{DNaGs97b${;ALwBDULYO95rl$;7ivCQXF8UIY-z8!I!KNq&S^Ob3U*Mf-hbp zNRh2A&5nN)1Yfj9kRr>}qs@f#^zE0f5u`X7c(h4Dt{#5v*6a45t*dXjdUWOA0510H zm;Wzt5g9)G*x^4s#4dgE()%tAFTM2OHxAx)zyKcg*|pamee&piN6o8$b@j(D{`JM5 zxcH6WPXEvCyZf&OJOEE!sDhjPKLal7K!6qgM|XwYSAkQ4w_SeQ<<}qn_SX9j-vcU3 z@j3kM-4CS@e=PbptOX9gcKfIPhC%*Y54U*h$z7OxQ}#4p)pZ;zx5ug!Ns&Vv%}z|p zr*6;Dx32HLg}eW`Bm$l0GCIG!;;koENKQxXT(6O>XqHT9!^7K=D@YP{2FYt4g5)&C z+xg`cZ@qejj_hyEI>lGHf@*HmeRCOr)HpU{oRKkInAuINqMh&NO`AeX0CS? z*_%is+M{Z?GM3s2b*80W`H=F?*g#pa`XhtWePv?&-szWl^sSdauspbQItl4@iZE=9AK43L zj{5}B??-3wz3d_7on~IT9$NBvMmFkRi|!FYTPvTwflA-{(ub6Hnuh3nVi@|?mpr7r zGoElRBTVtum8B<~q1C#Mn_4@RE44mFRmhgrW+!J_>hgn;pl4d@-13UI4p&Id_z=VC z^2~S43d;+!Fb;y#i7x0{mmY%TOdFqDUh&q!3dtGoGxV_($MLvFF?#GLt?)FV2z~3~ zLy(*qF*hi0{~_g_>21~R3yTibB$$wI1RY&GJ$ix3@WK{->%v0{J=6SN;`Zgrq{T!` zRc^|@bvpAreQWO_<(+9nK<4C^ao4YVc3fd}t9BYBeQWn2<((PA$mWn2`mGVm#&W;y zHz@Utq3%4SyfZ^sj4g&%rxr_YR3j>H7KQJ^kR~if8=t{NkQ^NO5OOx=aiy+@ab6McDlsH|%Std*khCiVLd& z+YB15CabX_-fPFFc{S*_J*2oZ4kFS_+ZqK5C$`7B7@eeQm_e^VqOUZ&bTPMa$@#o6bo{Isco zSNUU~&z}3{C(|6|-!E1EI==Qr$|}6BJTS}&NR|8KNzz=<>Z%z`YO`SL~?^`Cq6b$1O`{3xqT$)FM63|wGzLjz8F?ERYoGqscstFAaQ>3&K4Wb-cL!@ioFcsLR9};w zf#(C-8`IT6@1dpai!t_o)?;ak*N68XOIP98%QkrtSc8V1x>$NnSsQu2_QZ1*=>+;6 zmkhOecn^aR3b39XH(%(W2o;M(P~d$B#d>otMHWY~MZD4HaY^u}CY-ls1;GnkRh|pw znpHv?c+a(7@SwP`@pd=wBdBQ7L8aa4@F*8`+8w){XON^eY}K8iB=Myl31n!n%~dw?!IC0B01kZ5P%Z`@&Ul|4d#Tq7b#2MIO}Zp%tlN2FVOqY z5}JLW_hs`^7oTZ2TvF`1I=?8u4ITHC_K>WLwFtMSN~uetM7@yUFWCFw8wsl3plGEr zZW`3s;%8<%O1ip3HUVW1x?n6$4BCwu$vaSvj9smO&!QVQv5HW%sb1ah4T~raF+6MZ zeO@R@L#DF8He(xEK05He<^KOQTk_Vmr;q;n=fV|NJnz^kpOq2qiuhDTiN`Q_t4+KyZGV*wRNE z1^U#pcxnJCs5H+H0)6V)>I%S%7KQrMvy~NqG(o5-g7nTm?t1Fka%upNH0tfCXR*`( zQUnQZ(gJT<0Z0=An<7SV698YI8o(pAfv-yq;1S!v-%AbP5!=Amt^lNo0e-|b@HMFc zJmM~RV`>18xC=@v0BM4tA8{9;sR2CVE-0o3@QAyhumX@`aCx*b*iSv1PYvJ^cR?;S zfJfW~*%g2kg+A&oKvDyE#9aWd0HpBkM;nI!)U(hEK$h;87jRsd3*Vje$Y z8+cu60FSf_4)=d;s}8R4@BhG|aCLUI3L^QxcK`6wpI-XlLsx!c`*mA? zzWwQ=$GP{m?b^(pMjR=I~uRf3)+{7hZC7x5VfW(~!h_+ZXD@xjPIu?YgJ0V2@7&(|oxLC5OZMt}UwWZ@@ZE>U z7yrY>@4skVdD|80%F8eR!R4OccU~^Iw1w(qI)^7Sx8YsJ zQfs_vt&yTXZlu=u>b1t0)SZ^Wbl8X-ELuti^OKwwXz|EuI9^`KbbHjGg1K_ed&S>R zt?`bvMhgG<-nB;3nKYd2wAdX(b>Ewe2bCEd4<-aw;c!w!ta{d;b`aL8NxT0fwZ=bA zt?|ASjS8yi9!c_g6Ah7Wp$uqcic#m1s!<60}2joYQ%QM|6huhxnrjHjk3ji3=(!yDY7pPlzB z!v&F>V1tQwm0Z`6LZiOcXe{dOq_331qR{aRzR@7VW+rC)F)<+rM1`G_6F$>vwRCRh z-=^01;97%3#9^mhE@ZMeL9~Yht2O}6TbPa)a&FLA^z%HA7km;Ix&05XHR>Zi&no#v zt(&on9A~w%n(0Db*AjBl(3T=Hf>qViCMLA?s~7)zOFeJK_-y~aMwP;A-?`ElXLHyD z*CsR$Qw5|mEP6(UnPGL>wP@Lg@Ubf=ok7ovI?BubGPTBEq}KS%)t7$0+A4*$f8Y8! zQh3ySrD3M<^G~id21Cgz^7$al%={S3m8RL4rEJV4O|w&jkj#QGGY)|aIsMv69Vs+U z{5-|X`K~pp6m9kWYmF3M`+ug^`1zGanh|cXuA@Ez8F(0)>@BL@MVypct!78hSyRYC z*iO<=JGLy6O)bWxm#+KVOwqb0bFCcn|U+(nsM$QV=V3|eUvSW*{bV`a=tY(aCR`S4G;+P3;j%ul+i+1!vcLEh^II@(@qcjJhd zPHG@LOX9NAwh_qfqM8|3ATQ5dIaw{GShv4oRR^#uxLe8&W)MkMBX0<%+9HzI3ne|z zi?DC_95dA@n_cwqmw8~nfux%s)^((?_V=dNIPvol(l9u%xQzlCG3b6I@+By-z~UEj z(5{dvF&vIgWvPkrM{ir#kwW9GsWsNNYo^#uynJ0p3XPYoHB$881oYku>cJE%Q~HByXl+VcJX-cE7r%Ix5M7k(1_@c8q%1c<~K)!e#G{WjyzC(K3MP+#uG{ z=}SgStw$7GTY{E_-8Z=QxnS{S;PyFjSPzH_zAx|>7_U{z=b#)wa^Gl4eB@}NU+y)T z?2Up%4T}~#LuV~Hnp7ybOq%eGmU6Q<(xSffg5$N4N<(eQ?W#tXt0NwDW5TN^#s4TOnDObYm?EAIg@*rDey1AsTS-MeEGmt)g8KC!Ikh1z;9hvtF$eQjN*XMSxqD$R4! z&4js8!icJ?3m$BEO^FF0F+|{MJ(rZ{I0yy=g#@PV%3&@m!IzAXiuie&33;);QP;6=UziyKI>g`)|s^TEC2G`P;@!0o42YWuwk#Yx(}jZ5mU0%<*hR@x1Cv}4p>r?TmpgjfX{&I3Ix9p> z_ogM0<31ZuDrVihF_BwpIFYAfR4&S$Zk-NkD()E}i1{~zx`jp~+ZTqziqg$aYB{Kf zDUAfZQT8{yeY1RY;E9Jj@7;R&&datg{`|$S-~ZS9#S8!H!p+?u-utoL*X(`e)lXl0 z+g1C@C$6ZMKYIBa4u9jYd+EbFFS|q^{QHC2?rV15d-R#35Rk!q>-pQojU;y7yZxSx z1Ro21&RgKt&i;~7Vdq$_XZc#$2}&wkMc5Ey8k2d&%8m31wa|UW?Uf5LxJiN*1n!v1 zb7)ql^EsdDxHH&6%Dg^h<9-1G*}JNALiXz1&^3EQ-MZci%e+Boe8;TPy>^3abx&O$ zJw%Vft#^LTAM~6G?zBT@KuZQKZMGbmob}$)ZX$8{=fC^$KIZsw>B_Tgz6v1(233$x-&G zDb(dG!EhNkoDyUYE)MIW%>|Im;r#V(y*?5;txnG@bKQCyb5EyjyYd6iV^A|Di%hCP zyA0zQUKxCKhrJ)Z3qx}N%J&o+h7$08d{kQ$j8eiN zqluAqQ6ZAui0N5lp@AO9Vr=JJ%3g#KiGf%b8HU^KW{WI11t-&it*TC$Ay!Ekj=N6U z1T`5a;5o2b6n8QGbV9C6zx_N0Wv*qKMQu=SMkOeAs$SoUgR$T6p{mGu^OnlNMHuHN zlqen7%0dtKId-CG?%W-V`3^NIjqIQ=NzJj^))tjHhGa~sT;{G!i0S|t zR83m#kgC=3N5-6Mj>>E9B3mw3x)gRJ(uJ6*8e#MTaeRe)?;YKH`*Uhdg9gj_~Qz{Spv*s)(Vu( z6dN@Oy<32Ng+L@Hfv+kQy4D(-mUG`IJteSOmqE!iaU8DanMqQ==xkRoNLo@ z#wNWg-M6)ZoD3nXNDW5-aczIEN2 zcdHRde!wylF*MtOIXZ>Gz0(X{T^}TQ3M!T|WRIQ{cxq9p=IJ?I6Guju>Y?LOHER?z zD19us6@o-TR;@Y2RKuj;l=>Y7pXQiOvy(vuoNY!0&zWTLv2xw=4Q(X2iii7FC3I(J zGUn}l?0F0-GbxUS^Tl`>uK~sfR%Ht_Ny)a6`gmqDR#$R;i(>+7fgiU@j4^4(Ld;oN znV3h6Hf2m#UA4R-Y>b(}d{fg?dr-3+A{MP4r{%EK(&8j}{APpgP4y z4hzW$nVEDmPTI39Oqh$HZOBEX&RyphO^+0vM5x6J#VPZeajF$|-g_4Yq9@PhBXGz$ zn-9z+47@xKH5@%xv`dp|R_4ZGO3Qs)zj8+@ptZEsO7s}vCkOJz~RTuY_(E~7855U zRLwjG;sQ)WwZcLbPipA#qBTWjUPX&;wLV<*C#~VAImib*7mG@PnR@+lc2+JBlX)k& zF4q?rg@c@LjcM0xk6W|&R6AUJ^BoM>xNhgt*vrBwNhaqG31ecZ0Dya{AK>GcUnCdEA}S%Jqg5wYwvtrcOWIGWLkA`B3dNvs9naR8vLog*!tN!7+|}OAUT{Sya~s&TGo8fuZfG9Qm(ZoR6lDW z!)d($&QdD$aj%9W3d-jhvJK{MX)gI$oX=)z&3PBD7qziV+gvp=B{`I?yI~XyDnB13 z3~7)JNj^O~Y=8PL3@jDo`EI6I($uh%2|8Y;nqTxrZBocroOS~nk6bI{tQ>dT?@o-w z?B$y6c1Oj<+8mNy*+X+C!bb#6bXeZhQ9Q@!dHs54B!x0>RBB$pj6lqs;GAlQ3;*m6 zhRl>K71fCYx9bB#@<$Nobj)&{$Y@Z5j3QicaT4wkIhs6X5k~6Z+PG!FY}|k$uwTu4 zmPG4u=pkLYQ=*b_P(*vMM_;e^y0*;B`G#8!{LpUAPxr+|bZcw-;GA{$--PV`{8TB{r{i8^c|PJ@zU!L{_@~g4uAIO*DsC_)C1&T>*9y5 zp%+nbA7KCD{{FA;|Iq$q|LgXz9R1LRk6e7;_FFEz|H5qh#)azTCoa5n@1tA4eE7$E zKe6|=J!KP=QG=uJTxpQx zh~jFqV3y0NjYg@}rLtwT*Pcxqi^gaeg``)i%`~GMPNnU2a5ov%hj^@to$PFGw6g6= zz1D$^d`q|7GD?}Et&R&~0p+`vcXb^&M+ROpUv%dKqt-3Z^B6>TH|T)M(r#zUlSM|I z2NA96kuf0;e`1Y_$p&sOLpv^y0;kIL>rq7+)DkixyY5kwTcrBit=MsU(kIyM2DeN;6k|H z>8ZtdLYq}9sf9CAmd5Q0f6-rSG&B^)W&iRtDx9ZQvuM3NHXY~evM6LfdOV8#P&vVi3)?= zm##HPpVJm(x!fj{QDR}u0aL~jEvgFGH0Yql9K2TS%Qe5RVwbg*21ONBIkU*~`C`-7 zyRC&%$GI|L4>N{gMGZC1gOQu!=IBh{ee+s_EEY!v2;o6@%vq7nagke5p%CWnxqvlw zbEFuqt_BK*@2|UqDr-}y+YjI%npI+Vmedi)S1SXU?6oIw!Yy!bR%u3UAG3Bpw63G9 zHT1>2SgZs-G3aPrzO1y{3M^ZAidWUZ&>@dT$+RM`5nj^csv+K*f=iA?Hk^ zJa1u9l7&M8%8!UvHD4%uP||=7xOE+yuU;H6x>JN+r16 zayqs#mnt)oSWKfp+WxV%Mtvtkl-CAuIru}+HSjq^kpWSG#jv4UI?WQE$#f`x{fR{tWC-UY-cK^ z7%2?Q+C0->j9K3(*rN`b&#AF5LwVOBcTe0raoKFvw0&sMrt&6SoHfKo#wW;OR)95m zIGWO$P@dEr$SU zs>4#$cgcj`E}v*nVM#G&W<5rz5STaykBuQBt5LfdpiEHjLOH0r=%}=}_j_v%cOFaj zxY);WL8ht;1st_k%!=Ba5}`YGLphhrDq{gH@!0mo6AiY+b^V^hiPhLr2L7-^1J9qf z#30Vljf`#L@~k_SkT$pXQ)>+eHCQ>J_*Pw-H3l7+qY8NkMs*3-utvqgC|kB#7;H{* zyX%`JV;Gx|ta zYp7c6g@WGjYr2!=N6k!?C`{o(R|Uz)nNq964Te;WEM%y7@8*d{qZ5ifDPogA)4VK? zm@uMHv!-K9vuc7$7)>G8FgK)wtADoEkOu)&mXJvXNkH59MWHnEy;y+=O2%WS3>LFP zx6fBvIeq`*CmM9SBlz$LB8!beq&6v>K+5wlpleNi;ZLdQe1v9edcV(I3|1Oc&65pX zr@V0`kT{k0nVDa=ao50R3QrXVYNnQ;`NukDTXcu#g!MIh7Yf~q5?528%eR;8>DTeJ%m zCN12__p|&IyZpOr4a{`_QW?$Q9jyTmht;lSmcSdXz=hgLiyJk)0XRf16=U@1yVt0M z^4K1rT?TOM#GILsaX*<#

    Yb&A&%mrH9@8IQ#3nq9<2 zRPGav7Fr600PW_)84LQSo)o}d2wzA&oV@%c*T%c-1}oObB9B|(aSLqF0&6e#8;&qU zn^dOhKxmF)z-@~n4h^PdzQr>olA&Z>MH#NCV@H3u){s!gHDJv`IHV=HK^x9#&Bmm| zu>=nJd08sBRKYFaVu#!M)wRYr4_kAVb8<|-Seu$5iY`oN7AX@rY?o$5r`{oF^DrSY z*de~w(C7+3o|ZgR6K7McJuG8oOn0cTQ9`OMM20~iP<~`Vni?Fw3hZfL$gKp@Q20pb z<{}lrd)<}`Ry}p6G7jU4J+O05*0%K?Ie?RrLLUA*(3~mj_}jYwf6uxDKd1ZulWFtu z{{Qj*|MC9+%-f5{`~S!L|Hu3PN4o!K*OS`kbpOB3yh1+S|3BXUzsURlXV=#Lc>lk_ zChD=s;}-ZE+5&4YPqF_$dB^a0|G!Cx9|ED?jj~+d7^)pvLeD(cTzxnbH9RB3tyAHqU5Iua!rB7V?;HB@qG`Up2^u)nu4nBPF z{)2D6Jip9de%z7G%mjBDs=VW z%15vK^p)>8`ryTv?|*v#SN4Bkf4^ogVT&QI@r$BuCU zz3`H~Pwahg?|b(qAZOqcyPw(p@HV^sy6vs4-*^u368-6?-TBPv-#q^R7q11ra+lk= z-nHyVNamVW8Pegf*6Vdn#U~wodPBMYwxQe~ujD!)(jlxaLaNG_0l9Ca)lSuW^~X1q z`xhI^y?;ZwA3c$yxk;6-wed!q#+&i5c?#dfk8LRT(Un}+rYf>zSFBb`NNQEPIX_+A zXhS(|LpgOrIb}n+;f8YZhH~Guq1<3Yxo_N1?k#IM&6oAaB)q<-wTN0`pC*Cc|HOuJ z|7AnDKiE+2_cxUL_=a-7x0X|6Zz7FokE-FySZXJyFO=+VD)+k^mbdng?z#1z_{X_& z|L=zN{?1yihj&F@AJzKusHzU9&C}!Bc5Xwt?1pm4hH~(Ra?plyH#d}f!-jI1m0V{* z_N11h5GK}PEVj}upBk@rwl|d9+DvZye{CrD*Bi?H&kg1NYD2mIv7y{wZYcK`8_IoV zEjMe#IM$Rr#@G1S6tgF%`eQHJP%hk1F4$1c-%xJ4k~{Ze+tp8PDEG+?<^Jo2a(}d; z+#jywI_LJsWL;jTrpRKoJoPJWyxy+QXQ%q@%Aam1_a__5ovhc+t@mWTcCOrdz1CGg z;YyqK=yv4q$b zNXupqO{_##@dhX}ksKgjDZ>f^cukgnuR=OUfNS7LE~LlUyw$~lE;XXN`kYCyxY@-Q zbafI85ylu2#!Sg_g;rIMX>3&ovZ|xr&MO43vz@XbA&jkL>1ACDy0@wel${^|Y)Vi<$vJ%hfqeVyvfGPozgKVWTKrP9M8yW|3 z4Kc#;Zn>3IYVCaN!AS895cm()-XZV5`JM6u8yP;e(fgdW@%p*bwDG*MI<3n;${1jz($sRoSzLH#$NViufrfcQLx!AJyk@0LDy}syOO0G1+m?s zQ-f5IvPs<5vEIB%u-T%O)O&u(@-zOz@B3?Rohc!+wg+$jVKC1pEEN~KU9F>;}H!NAc+KBH)eb#9=8z>8Fq4Gtt#DT zs3;R8TUL&Jl$c_A{#^WkA zrir}Nh6S2Na!JEg`#r+CUn9p|rya8GMm(vQZAJCZX>%I@}msf>3IN z!QB~;m1X|_!>u3OI=Xe`QtTfMN{IWBe1H^8#mQnG$5(__rPGtJ=O++W^X(^YJi%z;8{dcp3s-sL8|&RJdlPy? zF1PJ`+s|?T<{fXnl|6vpDSySXPd;Dkq6KrmIlD^6k7{1-+t`%t%*BSApNOsnwXAY| zJul<%$vO@0iwv}!!__(k_g8DUQ*(9`DQkftjEpcq!KGDMD^;l?U*lYX>9%VlkodP) zFiXu$Co`((tGvepRhyZLwsG?_9|SIeoaYkRFJPCTlSecI)#-T$VYogbgiV5)DV4rC zwAKsTQ;XGXmnk;gPJUtMjC`@yiN~>CB0$bXjb-5Cz|R|^R;#-_(#AZtR%hF6xg$^; z_etRi;1cljF7f%A{~zEIc>YnjM5kA(N?jG{IP*nI?3Wn?_uH#=R0@}<)kG5RqLBsi zB~amFROi5u)Fp7Pj#QWmR;1f{i*C_bF(zN2OTh00E&-kA637>_ON<|pOH}*KZq}Hx zy-A+sOJF@q!vcOkm#84PH?0^(!^IkQv!)|~#+z*@LvhoIPh_l6CCCw7DOl(f^I9U? zZ1p-_P}*}lx$|a;SHB&&#LYV{@p)RUJ;)_;kIE&OUQm=pMv^PBLpu~DIuk}(@8&n3 zCF;&hCC2@Ei3N%0nz}I@_S-`Y69t&`J7~}if(ccn5oc73U!Y68q6S>z4d=PU=WV;4 zexH)3AJsku9JkOmHSK~l1K@&wz;uGFlz<#e;P~UzECDb2Mlq&pS$07b7O@`W=5xJE zb1XG1(^avDlzpAD`${&edzF4)EQ3@WRCi;S*nJIfiOe0BD1LqhqzAb~_EEWn;LJI; zTNax*2P;-SFT+e*SRX!Z?h@5pSICX48Y(6G(IYF6eG!JW8lkIA)aDy}ZP`?OjXN$;cyV0< zc~maZwnDGd3Hx-pZdm)L_p zJdiGa^5U(FNBi#n#S8I;!@YmH_xjym+|BR&%8t1GsqJKY?_oT`w_bIdxwyl9{Z&od z{qge?hB|Lpv8fqm0N9xpgP9qaL(?|HMVh>%$7ogS1y&#KWkWS-WXB^XE7}DHojGw> znN}bLWSN5bfHi$IxJ?6iU-ebm*6xpO3{SRAInaW8&-spN;6yNF&xtGuz~Kh`$f*`t zbU^xSrkHCt8``9(3Gl#Oc-Z82bKNLgyZ>V2TJO^+DIV0g-2heIyb2!lpKn~{*fZUM zAK6-PjCKkj?|VBWbX&72vXzB0#t3%2`nalay_ycsG@eH)rOpg>Zd0JD(z2@eZ(Nlk zyPj);gVVvPb&rDdP+c z`)3>1H}PW4AJ~Am%npu;VhA!iW(Iy%37BF&sz^x&p5v2wkJJs!HxxmD(Gpt=82r|i z+jUUeSH1<*_M;ouw&p;3&>2?#)W~<7b(<(L$Yqo!qn3(z7UFoVT#ytN&l}7TrJFRb zSec|%%qi+^VhLyY@ISIKoHZfau@vASOKu`pVSE> z(+do4a=Qkq0dBmt`@`2Yzs#z7$OmR?L^gQKIGrs2!EQX}=Zw&^i9)40P9U13I~6C) zWJ-!kiMm@K(UW?S>Y7Mr1{t?;Q02;Ee`w<>@3L5O^Q4vI+f`82%7T7yL~Yi2@Tku%ODa%a#`O0~^=&a0@zp3VK{? zO4*L=9NJ4vv+Yh8ns501zSi<=kOT8}c?oCPwf}TuI42gQsT3A< z1CSn?qg%)rg{t^sSZ1oK;z;$<6t81J*msDwY2?66fPs2eGr>Q;uJ-g+Hv)VQT)EK) z8IM)wln*@Egviaa`yCIb#X-Me(EP~F<{}2k5lv}=xKL^8nc{SiH;M~~XqdCEOHOXT zX$@~{_kEuYycAU$x4#}#xw7#0Zd~PE7Vev5JyuJFfClODM=fsJ>A*bWWFc(S?;4_B z>C#=LHBA=dV0`=QKvgRX|Nf1uy2ru`(^*b0f(&*UjdG_@680zFAm2)=c9y4|GBs@Z zai?CINUQ}c{O^IW) z4H=I8o&_xYYd}3K3;&+$Mkgr;rmwQSdMJ!#U353La5rBD^^`khjihqXfKvLZlEj*% zR+`m(Ij{n@6HNSx3(hi+1Is1bs;IJhQFNpnQJ68dIy4v_noV42<}zwduw#)8ykK(s zjca&YyZ_r~1209D#_bZQa%JJ)y>XRyS$NQ*oVEdBe96k<6A|Pcp6FN}(;&&4v@D^4 znNEpf>52gJ))#5tH-B9lF+&+YB*N z@~)wFIbh*MP}|DFzjNc-9&X{M&nS<36&#$7vXnPz<7j<^POFwstPEmdLJArKa_0MW zDp#F@^tZHnyRd|_wD5npF`N?%hbr;_MtoJ3!)_5L3|j?zO*<^p#GE#Rd8x+&?>1V= zNCabK9#peh1$_H;XOxg_(=B5J;rC}87M;G?_TVABmPr15*q_!$b3v%!y@U-}ctE?& z)}mpTjT&aRLn_y3mK(frI|r&KNSP-)8vgIn^Jsa2da0@zpq<0*RgcvSX=h zT|~5V3f%N(Lag3~mvELA^sO7iIk6zQ0ur-=9OPZIQ5$z^5j7Yjlj+bN7&B6gltNOQ z7?q~qF`C@1CvQW`dSI|0eE0dx#3v?n^4Rd{Q?uieHeq{0Mx*s9L?-MwsYSXd&Da`0 z2h)7BAT=Sa*l6b(k##(^@PX@Cg`RIV%YDJ<6%~+=mJOOi36KvN5~=~ZiJaZ`uy}U+ z<{ISI?z=vFkSQu2-+lwAcx8gOH?Da7Dp6JdEoX2X-~~FcdUkWF$O~hb$W;~4_;M^= zhy@S)}Ye{cIswl06?!7KN^3;gi-v$b_QdFH~BWcxk4+iBYJ{Lb2l zWV-|*_oE}7^YcjIESDAAPL~Z^WGR}Inz@+GRl89Veaz4NHNm0B)l-WcRECd;ezEmj+G~bkk zEJ6sBm=nkS*w1ywDw+$JguI{kojq85k$mtzK!8UDhU^2oxtj_&|GD+UA_6~<=DVCH z#$36hj5RSEVEH!bVKuup)>*j|0O|zOmviMw6%nWT7f$mn7a+GZQ{<6>=Zd2mlP$2L zyrXHYfW6ti*)52wqE`T|q|Z7xbVw?+?M~&UVPylqoS)`I+O}smB~`OZotr2#CA)Oq zaLU|9ez<&e;D@(^XLgr<_^r|QaSH2tWPS+!jr7Ab)AU<_35Sb`wwyCgR=Ia}EiaP) zJr7_BY7z}?Q@O|Oa-UnbD-?1|$G-3MS4L*+tg#qoy{R4Z8spNTijT7k+#zZ;#h5Iz zc_tS66Rt14@aeCHsaOEG6Zo)DD9!R_Zb;CbS zRiBOWT^KeC=+L(EIkH-#NJSNTfIMvi$XB}9X4BvD(Sh5&Yy8a4((S%o**;ETOpnOz z&{FAsZWngsh%1m*KNIBLsSB7vaHpp(jJY{%_XXI?M|9R}(u0l+E$#OBvtCT5E#Qe(Gb^a@j>M_=Z>P$ZXL2a}t6wfLuJN1#xn2g}0lvaE zl-=iwvP{K#?q@Ke%gXZURzjvbGsETMtQz3C#rARv{v0m03lQ|cN#3|oya`T4O!G#r zE$DXNSa`Oa!x6bqvx1m!nIi~xZeq})6ujWkrpV^#WX9#1anv4VXB{_0=G}e+V$0wd z!WL0*D(^jxVBjGX-Aw z$dV)QLUqXx_#8Ljk{NKx3btdc|u8Xhke+(gUFNvoF& z^g`fpEv$~$Y(SAvtYX{%qpCSE&Z9eDxz-0Ac0FH%^vR%sPbO5i?CKor2vWN| zTv%C1$W)YEaG6^1%?ZPyF{{{JAzP90!XgtB2%j+!X=n`=zQEY|fyfyPK1LdrysknP z#r(9_DicCeDO!P?EovC&+nsm{c)qeqF6S1q!x1rCH*|Yi8Q1#G)QMS=lY~a#L^TmfX1qj>@F;h+vDRP|-ppG03Er|OOAUw#)^ST^ zX$tFrF+LW;PEnt;F4LIfSH5qhAyl0{P5W(x0A${PJk-evGw>2bZAb|}A=L=C3Ui_| zs{sbxE8n)(5IK1;tPK~nW<_4Oh3UZ8l_iUK1bBK}(VZm7w5$dLT+i+Q$#WXzp6XXU z53Ua!5LM8Txuz45G&vXb;v&amLZ_1pN0t-p?VM-;9;6Jfasmf82pLxTwV~XU+dfzE z12misYUrp(a17?tyT!GJ0LNq7My4$0Sg>bRCZl#_YBU?@M${PwLVv-!mQX9|((cFB z8Xz{&2b^+Fzta)P9tXM?nPOTwn%9xB0T}CxRj`k45*7z-=hhls1V@~(0OCe9vhM@B z^C_T93mY@Q{ynPmuv-ddmOrD*C4Fzrf+uvQ;N~bqX3GkQ916#HyV-onKQT-3ET>cwQ4aZ`i_v?~TBF4ko6SJ3t7CG6OdE3( z(7)G{+_2Q`7vzT0SLU+psKenfxbQtE8X0ObYjniOQlrYWXTexQAYzSB&B9c)dzq=x z6e?4t6VkhDRziUZ)dEFGrrWZk#z=)qfD3N~0^U4fl+b*ah%$*<&L<2X>?i9wn5Lz; z6A}=j!@WFL$c1?*k2#5&2>mM3;!#2s7S62SR+L?Ptdr}7eAZF0w0C^3n zWDp`xRb^7axn^lL;{b^)4QSi?Cdk~yBpqAe#GIdZ+BvjWvI9t@Abm83!wkw=#CXWXFr-DL7J2xO zPBfZhpHhMXpd+7;d6p#s^YuX1+~P177Tlm|_VD_`52kUjzvfC5>Xk_tXhtmcl!=D6 zLx-2LrM_368w1(`A&7Ci1?TN7TY+{yxUPeUi;+)ASSU2|hG`lZOPdRf7t(Q3@wHF^ zaqTf%98&>$8Ch%8$`urlpwGG7r~qP7QNU-$Vo4%drJV04(VVT*9lb=|6qUm@E1`f- z@`+yM#@T*J^D9l4sUlQtFj`ACr=^%>9BPg<(U5nQZE0NxjvI+&axw|%etCHkduEKH zoQcJY$Oua4Jdo9zjB-RVzOZKV6RPtBrVv2=7Vu?anrCIg0t~P*Ff=0{NsWeFE>?ru zL`S{-HDjPqQS0-J>Bhqj+V0V)(w4w$WY_QaEZaiOG8YX|D_^6fhO~2OjjG(6;DBKW zH-+ASZH?=!kp--eLqJ{Wwd?;sd+#3ZN>OPC&FpZXzI}<0$$$${+fCJ~}hp1VKQ-D}0|9#?KMP zK?aafhS8aCRg%+vPEJmz>agjSdCvZ0_kK?Q-pi`>zN=QP^MgKUi=drAROFcwlozUj5u}bib!lYwI4W+M&1#w2 zo4?g6zDj*iPW9^{gYx8sPF&^uQq~g*70eQ7kegSyV2~BS8wo<(^PW!?ZzD!0+Y(j@ zr!=hHf@)@F;Gy%;Y_j47iC=IfJNJN+>rBsoAlbYW13m ztZ&>urxXnHJ{Do}sW%(J+xkD7!>K)ZyQ9<=i&==do5cjs>$($JM;gX)z) z6fY_B9wC9yqeQNq38t&5V$5t8Kx||V^6`FhcfN{}ARMz{;5=h?!a=M}FiE5us5T8D z5XctTkQ^-68(~T{Jsf}Vp}AH@p-xl;PKej@ys7%Ba6d)x3Ga|9gP1)7N=C7=F+{3L zP}-ZXaHOuMH6saH6Q;TyCBqD}39iSJA$(j%L5s~|Nz${T5+KqQ{ZO0B(7WJmnhRE| zXh#myI=H`JaY~Ibbcy0%Oc2=4AQ>2=;FYrTwK+u(_bTaDxUa;=lHZh>N`edY1fm^+ z>1>}vIgK6Q9Vu4{@>~5mMYmn8c5=DQI9Bq39Zp2>s9iH>q*T4z3P7?Kgp&qL&0h{u zhZp7)og6i$;t{_w%(mfLu9`0o&_Rj}unC3o>1`w)8MKh3gjE^$PtPg1Xrwjb%y5{^ zsa4;o8uABxbh(&F_sdF&8IBT-Ix%h)Jz{=y-j$GK;x)$Xj@gzc4oBNkWoQKpWZ_oQ z6GbE_gB7%{V$@WkK0JK!Tt;Kq991hQ?&%C+)SuM+Fc7MagJD|bg5H>~l}EDeriYJ^ zd-EL`*${EVgBfBcH}GqrHdP~3zLn!MF}SU3Wu=xEpg~-1Al>xAyb~o!A@NcXEdKFq zL>c0qC|ve*#>HUYGcY|YPYiPr(BwFxBus5{zP}=?Trt`57$_d*JsQ&?5MP3fk%4g_ zHI9~b(pw;^c)d8#eE#i^%*Rm?L)jz@NnV*qm;uxW(X7lHc)Zmznow~dSL%fb)$k1? z(BAQaq5_Bd!FV#oWx)HuTL}y$nNG8*V2#eDp^}H!@AoKxW4DjTk)BGF{H zUG?H<-IH&kiB=_%Wdf~|H^z8KJ|+c5CDOIng=37W@_`gzkqV*-4W)KXRaz+mG+`D< z)H_b6&}Jr=ua$-2!R@n;2@(tY%`}s%p^ZSTR?0I7c=49Q!&Dd>*F`iD9f_f8w~^8^ z!xm?53lagWJf$cCy*I}$w!u@h1?M*aeMV;}YLou=Y{JD$-&TCpkCm~ex zreJS77ApsRX)F~`azk#E>99o->W)(6NZNnaoB{z6S|-8vj8a?!$0(6XNv85F9OOi? z(isgXy;en{K~HW3?Jd5EkVt@MTTvemqRNzTDnQrAaD3$LrAda!_Av0JpDjm~oQ`R` zm*+Afc&4Qj0XR+OydE|glxm3tmBYMBKg+;EEz}k`MX&IN9Nu2odU!}wX@MC9dY)({ zk*gxzZkr}xsVE($4xba)7O>ek>(W z*=V&7*Mno4iN~~^c@uTgZ*~Yj*^c3{wA_xAj*H&I812BT8^O&$0frrHp!`}d0>zjZFI8$FIz8d?to2ni|={zn7~ zthxYja1$}R0C>_b3>*Ylt-AkKK-?6FNO+3w3Yr=_3midO2G_f7aTIQy%j^8(7aIm= z8rNLKMOGe;-(27cxCaROZWwq%o(b(D=-^n2lzeFUj4%^)Ee6F>Y%W`kgL^Q7swjM? z3#ty2H+>dvVsNTC%%@;P_veZO@S)(-^%e>04wQWS8I`MKJ4i+5gQgcW;Z_SWI4CPs z6eMp(V!hM61d^yh4z-~mOq=jx?U675N4JQb3-9^R!CCl^w5Pk)&D)zB8%_cRZ^{u@ zIiCNia6AV$`!mOOdNDVg5+Nl{$S=&|#S?r;LIZ!wMxK-oh{}r!nP1wH+4x zv4-3M6dr!Hqbg0V_Z0c*JO&kfh<2!pw z97et43L%!VVmF>dp5RA=m-_dgN*ENj>eUK7KThUHnbJbvKab&x*D^%!r6lc z)ADiW~}#uwH5^V?aVek!wn}Z z!cDEUS8aNH^T&DprpNbGti9jzTIwLsd-YoSuVK>@1Z?*DfPkA_ z6hX9$70k!jxZY1v0$XFk#s(oOZ(bHE2f;X-7_OY_-}eq#JoPZ zRPtp5{(iYIisOk4)Ry6p5|ex#=ByU)`XQQZ15M5b^>#slppa1Tm2#tDJvd;fDqT-| z<27j%#BznRbkMB%aepLkXU_wIZNboQ-T!~5-~a#K{EW!K{r^1hfpl>H|D3rD2lxN! zImNB}|6BL}x9G~TlfFB?*DJy|KGa*CvV;V|7*Jc-`Xg< zj_%z5!R-%%KW_bet4{zVa2;Jq;Brt5WPQPnrRo7*aB{Wk8fj$$mxJ;g))A?bsv)zJ z#p|wl9F!edbu@WRA`?*P>S_RMi_XjrdDkAoFIP&jsIETVU!TBr9fw;9T;SH@$xGmZ zp#bE);Q?;C&NZX*Nb@6fPa}$jQLC2Mq*MWpv^#Q7TT1paD2peBQ+AnPm4kxM2Rd#C5p_3VNs` ztY0gYu#`9416OQ%oVRhSH6J=Sz@7A`ht_IiMUOb}xpP}>!mBP$oUAtY0=K(v^>E5y z=@ReptFxzI-S}26+F)xDT6xiSB_q;W^{>AGv2{au-Esw3-Q3Ly4EZ9#8)i<9m@pjd zWcXY>CJ#i2Nv9Ew9;$+b@P=0G7yEL0Xi$S_J#_PBN`gtG7U{J70|P4H<{4t9yHSZ{ zGfhzB_cSd;tKD>{9}{z_E}u7oNivU*V|gmnA#kXtXs2O88lP2TY~Pn+45E z3cfgtLYrx%J>iY{b+!v`%r9Q?2^jpn#wV=d-n6ZIy;i8z%3H#8GY0WqDH!+Xlpz_$ zwJ2F?@#=WUAYlvjZtS^YB^==<@P#Gj3z8uKNAyDK4Gg?(FO^3-)L^g7zZmS;hmn&gT8YWW3HmcRM zY$-QR6wLytzKY~&0;FGr&ipOd7es8@IvBcjTVI*(16%*$AG>$MNx3yUxAm)bu)*t1 z`=XOt2-ajfu7Bku5Nd^^U33z=xoXF|`kj@;7@TQy1+F<@lLMuw_LeG$T4TK`2xDc; z(AB%z)o&+BP-ju0vkF)i%cZyw8xc*gRY+is7oKNG6AZ!L8*lzby)Ydz$vVbsEQ4@_ zzlp`083wzYSB|9u7KQ6r60_7Qfv{# zws{A1FNODdEqX}nAVI+27Sp}392w*NL~MW{$`Fp_n9jIUs%K;(OW>3e=;5c;n4MZ7 zj}5)Yfv_`B@~?3vp6Q~Kz(uglbX)QN9@lTUF8tDk(#iik`T3Ll@jo8_%rScO)uVSD z#SXu8_}0Uh9DL#6jR%qazutetzJKp8_9lC0yPpN!0A9HBM?3fL+_C-ppd-L-TOZ%* zZ9UihJ8s4OJ)7^}Y;Qh&x3skb29GME+$s!VHTX^lu^^y-}!&$V71mqlevT^#{`ukNx>lR3>cVe z#`Yl_Yq6W}m9hWoSwiFP}I4Q#IPBEpnT8xJRMIGu8L`)u>v3<}6TL|yz zyd3q4;7e7nf+}IDtO-ZNCe$Q4l;jCfgn))xEM}wu+siYymu;|Z42@g4YR9>mf``?w_%}-wsBXlgK}!!}(wVXC*kJ3Rc?U7kW^6SZY#sFAASUXJt!jg^ChUwYYlE$WW)Q@LnXzST zuyxSWftWOBY@0UNI_Tj*Oz0U~di`nSrCy_5qbu3EQi0OxS|Vza(Pl`G_h=kkb1>Bi znT^RwBwa|v5MuJG8QWLcVC$g$12JjL*fwmibn3ScOM=f{F{Rp?|)*S-unWm1$cP!vnP?`j~{bK ze|#h!ZSKB#_iX3gJO1rYZogvd8(X~lU)*oq8}2;=eAT^U)3foBjgsrHU6-AH>hHPT z&vHt03xDeg{2%@VxXFbb4)nWmU2?G5UB7F*uEaDyIRSEfK#qgmuWdP3hj2W6JUIrU z?z;DxPP)?B7BxTAzENi6a$`Iz(~O=RS^9vXJJ?Fw)@Odjrc4fjjF(!|Xun;M^-**MsQuiqcu&}_hBYCPEssxo=nVvG(>32eLH^{z2| zW($aBjd$0J=d-&H?$k> zkr9w*EjJFXPHm6us^#VpbMXWn6rr&#=i24wks%OeEjJEQvu%rd;^k(!8vJazd1PSe z10J!1dOEiCnJ+ioWP34<4jL`mUT&_M#>v)V8XbIA+m>_fG)~-$X>{4m1C~Bu=ng*UZR;~P8>{|*-u2DZ_5c5$ zDV3(X$W#_VCvaUww!jKL6lL2fuajx`WEWrTwq%e_;R7 z{a5V2aPJ#?AKiP)UULuH{a?GE+q*B?b@2ma@5O> zKAr4hOokT%{UXoQazeeTgV-%i>=9-L_kpYOj6Z@#>io%X&L{}J%xfV9hf8s!=N;-j z)<_!3J{4|vi=C>E(n?~O$$|>mNP6dA<`k_8fp}ZGSdeL6Y78pLR3Tk8V(AFtsd8A! zP-_q$>yTUtIppRPoKXVBSmTB9DAo@_`CiW#1I3;Txn4e_V1aZ83sr@pf5g`L-Q8K4 zPl6-kYB(Qb6NyNnN_eP26~p4SND*{%BS8zFVoCMq*nF{2Bli{}qFp744fvwa?AMZk zcnm~}dApS&QKBRjF5#I`LIWLjGbwGmJ*Qx+F(QPvx=DPTQ9WraZ)V6$C4vvU0yM%Z z9y2T$uutPd)V98$h~ky7+*i4lFBixcYkWCWzzTS|5NIg`>QC{pR;}gnnC-+yV@|>F zDMrt!0l3v{7JNiC3JH~n+06J`X+@9dfmVa^)b*w(C+*A&Q4A-P>^XN*dtI^3R@b|=VO9r9*8MV=hvG>3$xdeU2g*?JSn=o~0q zj{3)3D3%U2pq1P9 z`8cv_(gRhRL1WyP)N-noX;V_cqyjYsZmYi10GFwBPt-jb{AgYhoyerfbh8?WG{>YT z*6B7fSklw(_pofdT!UlSK=pBIGsxlh42{8k+?yqrCM|Gv$P5L7X0+=V|PNI+njXsj4sMbV4-+PS9prE=w z%qC)$WW*COu)+wxW3E$d7;e!eKBNyqOoWn>k(3fk3*jLX$0JN3&6OfPN7D8=N zg1z~uK?z1BBOQKhE+fz@6w^V?tdFE9UC;HJz%JstnItn1A&vuFpbxRtHk}$D7MjxsHEPVwmu=@Msj|QL_=|3t6?T z^-ALgUgG@0SQ|Dv9bjB|*ECwx@MveQl@F73B8QQkL9~J-dht=Ro5;3fR4bDRK?>VO z{aUf@>xwb$E5%~mD=Sd)d4JZeP~VNh#1Xw<7jDO~e4lJszI-sXdV zeWgx5=ZR>cST9?MCnG(+k|5|H){G)?sN2`_i8euuk`(1$tR$XZG7vC8Grm@&Sd!T) zsP03;O&pC&kzlYrj<*;_=V*VfPq{D5&C1in8bUprwzoIPf%s|_mcdjU#{V<#i4ax$X5&2+G z3BZMXwxY&-^|4fK6nUdVm&36>o57ELvxi$Yd!W|1FhGfdR%Z2achu~9;d-5qhP{nW zDp0M1PCbwpgVglqYvvRKl1%v<4JMfi*A!5hT&(vp;fR!oW$`2((b`P{8gj}&&M|wx zHmB&Llu^Q3jbb=U8&NjMQ?WuNgc~6_BIUVapCFQ{bU;A8`NNGlMQ=ogVklq4W5a?R zl;UbVlJPW}S))`D)mXNVkE88 zXdh@O9$~xkAVa8{`?+%&-4>Pav*-XN`(jBddt#!TXd^-0FOXi)Ff|7G!k(&E)(GeX zo>O#)oI!>LeySd)6Ucaoj`L_e#Pp>6AdlgsDBzffi}}K2cyBR-9RiAY$egbsw)$Er zGNLN1(Iy9p3K~IubX-TrzW&faGhS-*uNN{34G{k(uv)DM!su$GrxGc^aS6>vptcY6 z5y%G{qfk@PV&viH<`kN*&Bv3&VFWWu#iS?IGD`gd4mZZhrqt_zw-GCoqs5C-K(0V5wb3LZj6I+D!hxVVxda&0kc2Gik#FJ7Tw(*veA3bYBn!^Wb1 zo|fqrp$v*a1g@cK6Q@}=S}_o5QD?ubf)%>%%`##j!Ca_bibReeK`&{8LD-|h1e=1DK%tcM)+rwz zVr4i`&F{_Yf|uJtt}+huK_nPf@k~lmI1kg#f&>J<3*tkGdNTo8J5CDG58pkP!DkpW z9Oz{mXujZ+FfkfQs}Yqqs7^6!L?BQN-8YnbgFpt^{5Nw7t_$pp_i%w?xl(F_?mR)Q zlr_Aqu@7#=*m7=|=nf+7xM&Vfm<2_w91Zt|{uYrC`G!#p*1OS=)TA+u?3H3{jq-ZB z;sEJ3`Q3RRqcW?sF#~HRV##FNAME;YRSvZ>d>~jKdD(`B!r@HKYB7Op?!jCJn`VNQ zwww#mlE@V52$?Cg)4F7~W2!)5{2PtR1QV`#znO9~p6Lz^sYrUmt;!G;$)v1? zJxRY#i(?EFWCz9W%ZYe39zzcPVlJbh*BbAsP zi1_@OemEYikHxw#nd`OrUM_?-K_9DBIO;7xUVZjau zbf%x)`ms4h!ZeU#kE-PeABIK@uNQ702;K0CT3i=vBU0~=g6&WZboDyQ%qik#w_Yb2 z4Yfk(5Ue*q%LmpI8Ws8&C6Qr}GoWL=?m#H^)9%k$@&8+!Z*d*Het%>8A@IkopIcAh zDSiSVv+`+IGAqyZz;vmAf|bC!BK-_BzXsju`$oI*c-iU|318Mf*~+YJm&Z|E=&@K8 zH7X*_*VLuVgryX&8`R)kK3KM>u1@T_+04r8_*&^uPflhf$fce2eqEVaIVh87$9-d82Uw4lcFgop5eh4ji}Xbz9=<VU%JQYXWf2M z3WMgEDXc>IOtcizPTBS_A)Wb#$K&k?mlB~!Oj6?`UI*JC&@S`Tui4A2 zoDJPd-+y3w0oeM3ZxA*v!M)mmX;<3`H5tU4-4av9dMJrC10gcU@vR{vDxP6Z;{61) z{*-sg*1eXsKHmHxFtfSqn?Jzb>uvv{laqIguao2I8Q#Hh`|4sN4z4^_r(UkCgZw1b z8M(gTF?fboIzlJ{!_5*DuXv=@qv852%bQJY4*TF6*3ZC(nPQCZhhpkb{8PeD0?6^ej_d%aDA=H%D7oz*+xdbhi# z?1nXjoAtwBtjAv7sZoYwMq*jr4f0TXmzt%qtUkakSxLR>kT_jA=mORJA>zK%^j z>Bl57n@-A-R8=F1vjE$Tsg+?`L5Cs9D3)tU?@;RaeUV|msBpt>jW}!d<-zz&2F)xP zD&iv{U1BTPX%wyviC86m7LM7gV`B~7dJ*c=W6KZU(BC-U8)ch*2z^KTp@Tc|Ret#R zHTzTKe~&3()w$^Oo8YSQ8n?4vPTx+qgCkZ$ftCADrdNWZL~ls>nK;5$M^Fp`C1g~w zn;lSS7lu=(*>E(iocTH+WgjrPnF<0-2tnmDTJz$h+3$fwQPS!UJU6KD8ya`0dQ*?TIBz~ zz(u<*Jb3cIP98Y^=i~d1zIik{{Knz%;2#hA`(NMJ_rA8*-Tlh0w)5p3W&2Cp($?Q? z3GOet`OUxG^b>qAUVfC@;yBz%*zfs<9oQ-C>E z;GCzRI8(p@)AOE!f@1~Fc?$9~1sp7|m(E#%+)M!nXUFFo7k>HCtYZbvH4yyrqZ!8v zI5==ve@Aio(ez9K2d9bWJq4&^1|Ff>&ab5W2g|}Z|FF+^%eDblA zx17A{Tk=+TYs>v<_q*I}x8LpB{N(1_H`z^SjL~a7if!$Zja?{#kfRS~MoacLMN` z(6SvEqM*b;EsZ0x%ygTg9*=lI@L91Rsg^qf*6)4~JS8le%vcarv5z*hrAAg$S<{mZ zz=0-}4z)-H9}RI9Be7I9xWUk-GAg)y5B zJS|!@e5oNe6LDYMSC{*vMmg2285y!yETLSYK4|8_<5W|rTq;jX7EQsT5hYG%%iX}( z11D&qKsS44j5hqyb~s8>89rZy`~7iEg`<7H`9Mf}oUWES@E8Y) z4TD&&U+N)}`={V()}je{ieWsidK9i9X4OuY;(ETiXOwJ28P?aXX2&8((kR3c*q?U4 z9iC<^n&em|T7HqMAW~MAg0&bs^oF}E6&Sapq>@8&o@zYH@!6z_xPKC!rY)L6xY_O( zLd6v6jTM6VcBt4S`D_XuW(yLRs757Esf?-Zs7GYnZ-b}kOgEun#+j-Lm7`3brU&7C zsg&}|KA&Gl%H?w0pZB+QT8iZ3y!);2G-c6L%3UHcgwh}?2#@&=>_)09lEs0Qc~G|G${;(!Rd z-vUn)7LCkEQX|n7dl3|gHv?3mYlXlj8{=$2iy4g?6ElLk7Yd9sVfUNiY22a#HSbxi zn5Z;4jqHam4SUQ^ayWa>;5sSv#;plq88wu3qcm*4Cc-*Tq1-a#`b|pw0=cPo@lv2Hl5FF(yET_{6-2G$l^d%OJ)W91Qtz^>O z9FEiz9wY*r`N1gLD<_~j-A?<{9Na|efP`i6Cq$#`omwV<*7faHDyJpCbyCNH7wUL{+Svdu~<4;PtX7KGSlRZlUdSFlmV zGa?jpFiy8Ry8B1q>5DBICea8Znk>=aR#wPnBUBPiRy#$xjd>_+7%)+;3w!FdV5g9G zzaF0cphe@LGOPP_@HAr4I4Ha7p2E|xMdP5Rs(S)YLl%vLf~oFD;AzmJaZnA_{aSb$ zuxK2VI(5GWp874CRGGol3{vWhF^*4gVGw8=VayJe;r+>WvW_D2uYJUz2$9F#G2KL}5Mz@l+bqttyFo_Z`A2L(vo55UtG&1mdb z7j@qcPfslx2c<*Z_rcQ_S~P615yJSAzZ9>j$yl9MAcWG;vn?72S1xV|o<7T>ad1cA7UAjlSTqh!)!hO-eWpd@ z;CSENhNs_c(dacaMXG+9%4CXVy%q^;Xb>ktkX9R2%)E)1WzitI1BfY-ZXTXK!=iC; z;BL`;*G!J2rb}#%#rRN7q5P@fpdho&c7cQj9Tgg=1=e4Yhc!&sbaxA$K7B@G|47r# z!PBQ%G!D+-+$=o3V9_`@wQ@7?^u(fZaC+r#!qa1m#z9D^`&IDt$f9v@pyhs*_5J^B z*AKZayz=C?PM&t$KKjk0y+h{Ug9pz8H~*j5d+x5Z^UDCXpFK>QsGqmw$ z*M9*7;OF6crgvX(ZEz9X<=Wrd*xtT(Z_CMwyr?a~SOL!hL(2_X7HAsxb{0-eoE4dt6z!lUKn)*{cgitS_zXwsh?Jtlzx{Y-@Vk^iE6D zyB^on(FAc>=dL1_{?bL^7zrSQ#$LCM)xZv7f=(ul9y_5Idm6`8xw85=c(b7%4 zd-i)^Tf^I@m#n9F&*M*V$I0vUv8Ol-4PIY*YR6Kq>7D)lsT(P|T#4@C7frw4Qu3vb zE4l5YZuUaS>!TRg>$`1f`pVz1e!t4Jt?#AjMN8ka$MxNEvX@xs`&iVMzFyCnv%ex{ z>3Q#SZR>gG^ma?nYn-hZX3znVT!6t2?;=JC>`U$smE6wJ+ z^oO=JynXt7>nUF4?7L3(gpWPNxwErXzDV?$Xa$dE>tah~3rkbIYblxIX1_mpBPEyX z2Lfk*o~7hf&c5Sh6S`1xg|n~KceXgCTb8~V$$`G#x1{g3lMV2;ny4Z=V)p52sMgH2-@x?*gIzE5;?B&JD!B@>f z#l_;ZRz*v<;6tv`w&8J3>cEKUx-kQaJhvpB8Q zbhbEM3Rs%nk+7}lZPV|vG`(hVa`0Wg&~&jlt<`b1I9+^}rQ^ld+Sc*;)2A;lP7aO% z7Ah_lr?o0ty1k$<`~7LV8qV_nH@??(G~Ij7*3W@IZv8Bt07wk{?kkCb4i=X6WjmJ= zl5UiRzM{YHMJH!fv&>ot#x}z{@SoM0xDJ-IRS8Mgzdx1}lh%5HMQ~L^)SJwvU!llT zkr)Vj!yzkmdu4`_*Cc9;<|~}F{imFWCi_rK9BUaNTIh&XJ(B40#q_CE13@u2pBTs_ zYd(={o|Y?0p_12g8GowPtRdY>p9JUY3|gqSz1d2$GnR8fXx!|LM5PdnwsBv&(&GxT zW{Ak*20swf-Euw~&$Lib$;e)V``OUviGk0!k{IZ~y3WlHS2iPd@J4)U{LqPlD=Jw$ zzR!4y{O>UZo|42s&{^1LwKHDncA7?2g7vl^#G8eb9z`*AC7xr$B{@Z)OazNTcqNcV z`9fFFZaOiLR!f|UN7{K6Ka((|rJhlEG?xv5qI+k0$xj$9NiCt-QL_yie-DJyd@`0c z@KP$6RF#sClgEJ)oN3D}sIpX!jmsK$$`%{IZ|Kedh=fMcDIDU<6sD~Qd(!U4VG)sec1s2Y`Mf^HF55{ zxY(Ymw8DY^IjiKdV*5X52PhZ+Z{?d62z(r|bRo3Fn;y^x-JAWRrQ=+mB_-#1`Hw3; z)2&IXS6E2`BVG9$Wct&LHM$u{PzQox4QKWMt`yy{_GsU}G-MbKLRNi&m8;EJn;KW; zkr?73SdjIo&j81f=15It%TmdxrFcRERep`3WaMvpwW*HKkfDTgpw?g}kg029m=y{n zkv>HcF&?kBAYV$8i=?jS|-XLuVH9n18(dqLUlnRh!}Yr|DC)Zp?-2ZJ`6E>!S5KwiICnP=tjZEnNQMjW*X7 zbnd5(eoqDr)V?fR)^Nq4tUc`-hqAp~0aiCzb_0f?pyk>t9j_>}nS}4`R3qcv_-Wi9 z4OW^>f7$@0bx$j~cm%8EYx!obHb{Uv7l6sl`@HmctYXFNV(AWhU4k#RZNU zmI{7Q-A2x(#X>ifKZ~KDhJ0k~O^Z?h<~t+3k`Cr|3~yD#q*PC^bp4d~q8XA#d^TV6 z*3g0DS@r*4?s~uL!k=CE)eDbYc*ORgO-NT!(*h_{)b6f=qyWHov_2;mxalgZ@x#RBV9Q?0? zKRS5t!2<`SgS+$$EMZsD7ow~KPB%oYrTVJK5<<)mSC>AvQI`{cjdg>IgFeGZjGk<|0rP#3f^ zUP@;;eu?W5e)Jl?ihwVZOM-SPB=8qn<3(Xx}whPT3Jzy7_IlA92G<|g6 z9J+pDH%|V+F4T4MHJi|r|9$e+1yoad9c4Jg!AERcXN2)`?Y@(HPM&KQdiTlq+J)YA z@*KO+J5Qc%7kbCZv+P1IoqW$63M$`ou`H@kG{v_WeC@S-_X z9B>t^Uan<|nNcCmHL>E-Y;EVZOLn2zZQL$2vyIt>rnifBq3CwOE;O~Bw+l^f=j=ig z+t~%QrMD}oLfn)FUCd0H)#%c5b^V#^&+J0~)b*!!p?~7~6T8qqcKxwk=pVWM$S(8` zU4Lj7`We?}>_R{7`m|l>AGrR&F7)?Zzi$`%Dc7g$LO<#Hq+RGIT%WKD{XN(3*@b@G z^>MqwN z0jLf5@%?un-vMd`e#-qr?l-x4_kVCd{rDGLAKrS&)-&8++!D6la`IF=#H`z-FX(MEBL|fx9vX<)EfNVy?5<(_mUv< z;48Zy*?q^Z2C4`=XXnc&cU>SZ+~fN4@jqYq7O1jWmB6#pYo50JLZ2}*- z^07Rufh=Dpl0_ z1m1nOP2gR3*#zEsr%m7;ci048x-_5nwbST&*mdPRW!;Rs9&#}a#Nx!CEfhvJ)yt=4)dwz=49t%sI27kj<+!}_T!pIqyqZ@==% zwI2GDS3bGcL*H@m4!aKh&VzR@ph${fsB}Lct?89gDpur{r{JG9|H&@&>zj-7KY1e(AYx|Md1xFLYfCeb@H8>_XqUy|CBy3cO=`;RQ-wH#>A%W=0a$ zFM>`J^0Ka%w-=|i>!A;9FJ3_Fq4#euPHWdg@7rFS)~<(+x5tZzwRTve?U7xmxoz5o z4!4JPp@Z##U1)#1zksgw*wkLiE;PB9oI~XVXtY}gO`Wi8oijK^SUxB_4jqT=LW9Ra zyU@V#;y`D;0{-L0fzEoU?|5;bvmWX_UL5GGhr-9O-C;q;kX`86@!~*dy#haQyg1NV z5A_^-77DE0v;OqKVj)`p;(XV^Vj)_;A1G`t&QsPy^P7t|%-Xl1+vRpGhPC#^`7fLQ zVi)?&&2KKCYhRpSI{uPf=zl%_uXdq-cf7dXSU<7hUp`*!2iCvhUh8`8g6mpy zdF#PjU9OGK-HPG;9j1eb%+{6*uJG+x-02+qXJf=+#527mL~DZk5&Z{E%fYMuO;F$;yZ z4#ES>fw8oCI{Ca!++W(n{rMbMOqPeavSij1u}pp-iuGl=581>$U{mg2*~I1IsHEj^b_)Fv)v6PL7!OW4H4XSnsxFJu!Jw22GY z#QA49rqpb;%{q(eMncC@7}8&Q&PN}xiTmw2Zmm7P&L$32!?M<)wfrVFagWS#DXEg| z2<#{tHAz#~$El_9Zs%>{a&ugn#8XNqt(Rj_q(T(Y<bXcPAfHgW%Uj$7-OrcKaqpkw)~=H=o4Dv4w{}1I;0(t` zTS&RyYY9oNoFzmmqb&K`@q8b#9yi}dtjEpw5$kdDeZ+d){JE^h&7aG995R1?dNomN zRT34G9SDU1kFZP6W$$jAxVvoP?zD-!!zS*cP2BA^aWAlmd!9|)b8X_DJ;$xJ=lgBq z?wjG(+rfMvu^zYBkAtmMf5b_ZaRx;PBTZSpvOj#$T)(xrQ=7OK+Qi*s6SuJEwffyP z%m3djx{d;SzrOu3@W-v6TTkHTp8&|?diIq(E(e8zZeLq?XQ^m{)faR{H)Ze2ZpseI z0j(}-;FQ5q7U=3cuC+t9lBm`fGFYxMW>GPkYH+HR(+H9@Ijb_<&E;|VzMXkos}#T4 zJgyKNxM3UfW>$@N({bI0GF(BxI-24wi4;|8N_ajWkgIYcca~2yQ{DKy_3X{$aglP< z*UqPq)1i-*QQv7j8)3StF$l+*LW%UB4H2JOk;jOM`nj~fR|<+5*u*m}uXZNa5mbOG zd=M6T@j+EH+Z8q%5>jQGRT8YBgFLQB+S6xQj`#Y;hLfVc=jM1T+9$6_iM0M{$vNi5 zZ7f5(y8ZW)4@YsHCdndHqZ3Loh0-_QvpCh268>7~ z^sLv6)1i>j8z+;elA+)fnJf9?RHz~{l+=n#e%dS~HH}I7cmttOvX2Pke8hyX0jMs| z66r`4*2i?$TN<6lhqjW1t)W}V!VgTp2iW?9ZxA+|6r;YWWMRWp8-kU*d@&TXW^Khw z2ac>t9(GcI1Mqc}D{SGNRwlSEIoUyRx7EqXvvPFHj{78)=V@x}$eI*IU^q(!Mdu%1 zR}9WHW+}N>vW=}C-`9KO%{rjp@4sPf5GoN)4$`O9+_=neUM>JZpjQjdcB7*K+ru(u zMvo6um2p>Z`ENRDSdp2k5o^I57HFv`8uh{Xc&8RQjR(_QQ>++%?}+eY1l(;2r#MsX zR=eCOLWM_^S_ z-;@Kca=7q!q{BHVBDTumR@BV^W_|Tb^GWi(6^Z~)oEOeuukpL!O23;I=a9IW1Ai-T zLSNWty=zy#2@N__m6>{b+{tBx3?CMPVKe5ZbohHu&tHCk5! z6sLE`1!S11<%0PtTFxfQM~3I8cp&;q=DrnOX~?lNy-7<=xiz|M$E8o$JCIFTCjF zXHEjgpE|zx=tDwv60Dpjv9-ws|= zW>J~E63BQdkl~;|)$N8vi-U9))r$FQN-uFyed&bphQ)7`$-N7C4tiUC3-Y+hD}cN+ zFg^!;v24qGtZbLgPhJk>_<$S-y|ir0xw^2@!~2su5OvqRE+;LxY>S$IK;P&~AiOls z^n|i>0mF9C+RL^s^N%HEQUfw96Lrwm>-HAI7N}%3IWiTtlSL$T`7-W?Cfb=)fxLU} zab0rI-pjVUS}>`B^N9KQI-pK?RP)3E$WF~Fm#;@;+8I8 z*bcg?+1BNX3t~Wq<$?}6s;zHy@&qne1oA8wbkL2>w!9m-U|}&t2hH4Ui(KV`c_7bn z!La>CaJJ=L?SeTV$8tdj&Ess#xy}W%K$zu%4!X|S7PjPq86e7XK?lw1Y>Rqg7hG<( zL{HL|E@0RWn&8>i<%$cUK!)XlqWuPV>)Vgq&;_|k3dpnU(?Rn*+wvZ_PkxdFax9Z^ z(6Gj~oF$VWR_bY(UZ8P3mCS8)-twrxneRgAY-<-SHf+K9%slR zN)57&GK^<}0R;~8RDwgYF$u}Yv{Xc3q2phFM1Dh)Q6|xaJO_OPzXf^R1Of6a6Lrw` z#J0SvP4s0zj%A__+LqXsvt*(#1)?kyJ=g2R#gsj?>0s;5qxh zy#KPj-`}Hlzp(r8&ewJ(AY1?6ZNGTyqg#ag%kIYJKWx4pWb^;qjh6x@Pr}c`hts2- zO<)k5`$uel*+20#N0#=FP8wh{(?d&}W6u40+uF=0+Q|`yHa!4RUJ9hV&i1F`^&2cF z2O8RRe<5-Dtw?lmq`^=3fW$K}#))l-kFQ8h4l{)5E|BB{k{+=w>FPDf$$EM8apKbt&mXADWTjEtd;^1gQp1Odf2#|Ey zwxp#`A15aq!sJ09&T`2IY>T_bB^?}aJbXC041`%OdB1I8PwbM*ClF2!L72${mNsDM z_u1BF;gSxHH?+z9K#Jv(W80ruD{R%lVMlLrACPFdeaMU4BOd!c}$$@Q2%P#5Q#6y@2fjG-0`?kehzJb+6M zfH2D?4co$=*d>=wB%Bo+#^d1NLz`$oie)^C?GMZq3tig&F^D!%7ZT-fMWTa)5PqTniI%xawk2L= zt`3esgoz9!S>`I*mUNA|IyeRa<|+YUmbnVHg*~ykE}v64IVzdO|GPfuI(*aKbGP0M z{`gM&nHHu$G>b(#-Ei`OyZ-ZMDdIc=B28ArA_Z2q>2q-9Z3VzMxhtOGYcRz}iHZ_{ zq|VUGv*mVtkY($=7)+s&c&9T6SBDseh0`3gI-=ddhxyILBCQRGu*R`Ix_$X$*P^;Q zy8Ze!%?g4UYCV~4fG}Q`Zpz$~7?Ko#!{HkSqC=+-e9xRsqXKb&YI3250F<(m~ zt(s9U)Z%DI#o_E(PP*xkB$~}-QY@BF3F8DNsY4m>gv)_IFH*`3%3?Sn1jJ@U>ak*J zJStVg6<@Amn!{vi0JVCjbwbvXH3-!D?<7Q7u5`V@C@(rs{?&W5gJ_=bK#$hjH0aZHjVD_jv#t;7J+I)x02F|{K*Gx4 z!m^QOi}09ALPfnxO48VnP9wQkg-k?+GgQ)&(OiM9M_NMs=A(QWzwa#8EGD70VKm`< zAX_U+0~$qowPCr*#)TLxlR_7XckpOF(>AF=|x2aBn(DKwl|D%hD?r% zzECQa%t`0whbub({;T&xCkn1ehk1MhYrX2Oj_!Su-l&f$V7*8?{iZC;)1t1#(p#^$ zZ=c)wBcXt`(^~0vqhYXgR%KEc+6)jz9nGEw!er+RF_B_R=HeB(mh}x)J|CrT-t8cW zBw|c~4vR$m)T|am<$ww`t0b2R`9~<3KZOXsil^$ioPq{gxhh>f^#=Td9cc|Gnzh{?joeosP`@zdszefa5n**P(o~T+=<;9{y>SmJHH3$on?k@4Ucb1lH28S_eR+9_)?adF+`;y zJK-*)1Cr+EiXA&Ek50m|PPxjrHd^(KctJHNQ$Jyc(pJ3$uu)biq*+c^yD_DeEBo!- zzcz*lWG?HE$zHYCATn8y$D~LjoE6h4-kS|aU{MJap#7>p>B$9S8@ie7di`<&!8SU` zSjU&`N8jQ8KZ%kJ?*FH6-~T6o?wTd~hJdK614Z76MRaE!>Bt*m#9U>49*;%(8AVHq6C0n#FSM89$s+=l4h~%DJ9yXClc2;@<^EU zkC|N9d-TEm|FrG<{{-5#Yj)__Da-c>Z-bqh1i`kF*C z+_a!}H%?a7WDM{6RFTn1t|Xee+@UNj2UunB#5;0#Ja)V7l2=Q#_apU6sYGY%c@g3B z`=yfLEyI{nMrGVzXnTBK1&FmnWMF@cYbONLU2S({f7;Hl->j_m`-}Ko#6ie2QsAN-~Wfd+uE037tp`{_Oa1~ zm5ep=y)sU{dJ|UDF6mUW|6tx_GKK%GbH5T_4^}&_hpO6dc}46f6Qgf-1eXC7y2=?_jLW=4C)8i zfBOU8|6Ku(`Z542S}n#|w=8B`N;A_Dx>X<_Xw<^l6-Rdn^_h4=z6|GSm@R{n0~#VcXE{{9b_-@knIGQWJ*(ogL=0k2q6 zmi8?^Xjl8caq-f{7ufauKeh1c`8(|ne(8DN{H)z2{7ufH^L*!$-C5x6bM-lR?xfj! zW^c2*0UVx|X1jLJfv3)Vf9Ab*X90HRjOiaAxz|7Whh3ck*Nip2FW3RfOeYA1nk~d? z6Lb@;c7Xt(8I?9e78DbAC#x7%q#G^ywEV<~Tpt0&gHocNDWw_8m9HY0MzvkuGAP+4 zqH0|W52n23atlOFf`FcpnHZ6t7?BzvsvSt~c3nxvUvvXRsII_yf83I~UNKGprqQi* z+W-`7at-06w@i$9^TdcZ4G`%L!3QFck}jcYS*^5;M%`0XU8OSHD-4$lW$Szy33yR8 zx7gi6_#<44+@DOg0`cSD!XOC&$wXSCVi6c87wPKPo64%MG1;?Zk*{ zCPutsfS@q9MRP?&sC!!xiCRy?a@!-t(F~)eV2_cB`}7JN5LlR8f+t2m1B9o^v>Q6W z=JFmd4nY2{RiIJGpW^Ups!WDMuCNE>T?&Sk6cLKmTn-^BbRF}jEg3JG9WY;jW_~s?;=d+FJTO2&*-ogYAT4RVn`q0bmr5{I zy%5F{bS9JKBLzy8s`(172IN0`)&LQRwdtT4RQ)^*BT^XJ-$?m^6wvY`LJTtekQVoK zz#1dg$)|jNV#J*jBR;o<&`3t5{H{ir#qeSi<*Jy+FT*+)mAsKEkizIv-ojfElUdsG zSAp>n&WXpE+d?oA0G5)FFPHA9)u>M+@sQ35u@u~u)Z99aRY0zrmQW!#6P_4BPK+S7 z5QNHAXugr6<%}I=iVG#8)d{GnRt)wft7f>)Cld_PsF1bkI|c|a(haI5jn7v6bg&XB zyX6R{XSoy^HhslFO2)kPY%Ecb!PesICPut=V#Hr=Awn)iP39q}4Th81LNJ&0x>MOg zq#YsM>pH3D7%y1CV{x3E-u7?>39*mB9zBxvSJ^;CrF;ONXyI}S_p(M7@kg0Z3qj3V zZRV?kF+AfVzA`c5%UcLNpc>_t%2zsO9gNGFwkKD0r!p?fo3>mkMXmd=GF$WNr3HCn zgfu|7u~1%47uP{iW^=W$UCKC(>TM=b!$XFT)0^I+Kaa&Fzek^DCPvT`Bd9(i*vW!m zp_}5RWU10tg+@JE&-meX(sGMYjHd`iC~9>imbY4qe>E}U4Fg2mD7JMs(B4~Qe!-e<&u~knAa%h0vR{i46#5zM2Wp>6F~UYTigqvgEAAxmvmq zZb6HG+l&7{YwBO8_FT30@LJdIyVqR#f^;x@nUvlC53vs)@-sR43I}Q8p zuFQUQRAshTh{yBJ^_qlFaerhnF2Z93Dt8LFm>bi4WsQKn^6pBPMT$!-(ZBZ>2CRfMrubvt8!HDQ_qp zGKi?rFhDw;?NYVfEunYtFfou|g533efw+!(Fs?3W-JrrNaXU`e(6bti6g?G)R2yc! z;0rXBHWq9hKSTK7VSFIL1Y9RcdLKuE&_IF-iUsbQAPIgP39x|#6L8(#x_<``qXP*h zNV%URUw)i9Kn4;_P-k#=%LE=g3=brjAUA)K#QAaN0NRpZ{F;QjTQl(B;fn_nBz#dn zpAx+ysue?s5i0=0g#jkX+8;*`f&&RAD3G|j1q=@!4h$s7VqG;AtaDnXVJhxoRTDh^KtXOd5>W<- z)%^uI!?s(1sFnhgWcVLH=`*1@AzD3F2M@aj5=_8#cPlF%JPZsZm>|AxcWW;8^Zy;UO|2YU ze3kPw`wvh4JYi?R&I~(yJ2Gqn*L8e%G?e1d%dFj@)PZ})K1UN2E$OE$OyKc$PFa|g z!32?ERu$$Gsk&Uta7mJqbLrte=)EU6e$j&A@-aQsql*?CKQinn4o^mg{b3`++};tr zWl|ZxYk#H0&=sEP1bn4NGbAY)*t4NTgh;E)2LNvf7t6tbbmCpsGI9R~DK>~QL3n{+ zfgq9nzG6%(5)chEtBL*TaK53siL%$_kx&K0niR;^$}J6Ti2G9+gKNYCTr-s~1DRlD zgQWuQdd!0Du-IOvTBF#@RkIUgda;DDlojoV6}mt_0C?>LmfoZH!oM4v&+LbPcO1KW z_!-mN?AYXK5^vLsu0E>k*`y4HyvLvrPtTwqotpgz@*9J}Ppp+^zq8>Wf-~Bv=Ix~~ zHuCwNHf$$$_W~i0lh}Q%CMZX_E1T5`c2{$I-M}&PE|eA!$pi}<8>x^=p@3c}s(iA_ z`+cMjwRGCU`;eki4Q}WgwG+;}0O`7mM`&*(;8MacjIsV4C;B(kLPtirImGfcgYB{l zD}qJTE*9R3?N{n#8pbk6$b`M+WSO;p+szk5E0|5OS%ufa1~FbpX8+M`!?Rm?p4BtF zUMtTD)OFXk`5h`6?O;m1e;P85K^e0@l5zCy@dR3V9;LgFwu#5Lm8aX|3HHufJ7x<~ ze3&(K$!(>=Lb4n)&)aFqbVWaU&$u zgJp{>8LZFr)mtV@?XMFl`#nBasMulmzED6*11G%MG&4{q)b7f_hBsc9iveC%Nf)C8 zJO%|(WjLjESvu=s;|iS%YzXlbTu=Gpz6PjD0X`XU@jg0@`PmMa_Jj3SAVRz30z2j@ zulMM-0ru|y7fyY2YR@P3T)hX`GrRW5wZm)p+QRB*R_N-nbB5ID7v7`M1oQ^ZT4XaK7D{az1bFKj+># zmzz6x_JP^In=Q=(GrzE7`)f1a>0eF%{d8+O;P|!ULyjvP=TH6C9{hxV&OdbV>}BW7 z?R+kAyN45ONKQ~O0g_SGQMl$`y;J8Ox@Z*KJ=7RL;iw2rqLB!<4Zm;{+%*IjF;c=< zNYbcn_ywcjz!03{Xh^afOfvj7{QMDc*Kk(@%FbO=xDbiR+wd2Tf)Cdw!NPKcVh~*u zw&CZEf)7{3mKioI@LEJYGV{SvaNp3gi#n|Fw4%Y`?a>3H;KRj#b%udRp2tE*7S2Bk zK71r#Fr7hE0#c96yl)hISnFs^P!V0wabjC|a1?x4>qJR6B$|R?b{p;;1s~SBkifwq zkuoC8Hrz7`KCE>-VMqN50*r>X;qDP|U|8#DUBe`j<5_7N?ivLj);g9%H3qU1to3a; zFbY1bbs!O&$+QI5Ea=N%jF3Jz zKCE>@NVF$DOruBO&mILI);dv=q!1DjP4n8fkKkzMArQSf1{Q#er;VVT9SZQ-9W3O=lLiYg!i zFXPIQnSc5y_)%KN@`4e8SOVW3{b{4%!&)bCtRUIxwc#Uv{;8wj!&--FHUpA0q$7^{ z&ql!g!)il^!*+2VMG%g7+EYitht)=;1(6gnkrua~{ghGgVYMMRMI=$kt`4#dKY0{< zSZydB;YpEZnIrDvDWl-SYC{XMWZ)XcAMx`ije-xWjiyHg2$y8x2z<{d_^{d#3y44YI-|)IKAxfkqRSX?jxRp`x;dQ4&jG~7r6nuE$bQD4(GD~Vl?0s<*e0bq-9zk@Rro|(!a$yvFc;QGj z%tug}SC4Fw^P}Ly_Rb@?8rE<@JK`#xqu@u`yKb;58m6ctu5xY^d{}M72u6rh!(SDaX$ot*PJJOvnFeTs^aT*`c}Fg{g%eXyI5l`r)PA=Z^0K zy!+yWgnIEr!%~=}S8(}4?nWiS$p=8Bjs((i7oyXMP%$>J3c%_4mQois22G1j=l%Cj{t#YBSfy#3uTg}O?Rk;yWl0?=r-BFO+-27{ndrQ-nH_RZH$nEbPm>4wORy)2q-;z1t746V+}iSDHL$^=J`jBLsyM8?y!5@8aI;!Eu3y9gHXHGi4~hH-K7f$DyT~$ zLuM~XIcYuw?DU~Akgk_N<%=wZMX zVnp&DcUem}Ed|VUAj#{)gHk@9^CuNhi$iL@;_-Web+BNM`?bcQ?pWgv?;u;_rhEan zUX)9Aiv&6-8pcMUlpq^~7ZOYHPM2p>zIuZzq}4!*Z5_yw9<}X@_2$_gw=>qb$M9E3 zIf4Kb(G}gXEC|Qk;XEb>tOJRfR?BD|P^-3TZN((%Vh9Xv*!8EYVTR4v<9@nvs6E!W z$M9FaY60~Y%uMS52Ecg>uq!E`nF9wxiS~hp$hQJgTBJBG1e$s=T*TR+>PhE%`fQKe z8fV<2-v4aPGg0S2!VD%7o(^C46mP=G0wQaG5i&iZ;ORUqmyg9Jpe3vKvR}{2fA&d+eQ_-iEs%!*1u6R?S$^m zMunl{uu|!b`{uoe>MO1(dmCIFY=fR*gXs}94%NmI?ifz3opCpOK|Gx4L=*)8+Agxi z`5UtAIuJ`2SoOa7L^Mx~D*Q7y~7G%6IeLL)FI$H*8fl(M^p*m9d07hUeq` z)w0^j13-PFA%v|2xj}`Bja23USCI>L3c7%Wvt_AT<+BMMZF>sdf)4gH<;uN>%8wOp z*z|ftjYFleggb_N%qVt=01vZ)A!Wj!ben9K&RC70uF%yq0_H4EFXh^>7iHv1E?ccb z9dV=8+jdKP4;3FP+^~K2h#H3qV+l8`5?eOr@?^9SS2Xn`=0{pFne4EJ(hzW~-OQ>& zSKi3^d9)QT*KxZoUxQV@o@{bj6*qCf0vvDXlmTDVJSd+D-q3mOXa}QsmdPI#w znX!Z$R*5a)x=j(%L|?6Zpz20DUJS|lToFz+sZy&8fs7Hex}u~gKB48>sFU#|u5X|{ zYgP6hN!b_YG8#vyAg;f7UWOSohQFIKs9pbH48EY1R2 z<$#zw&8bz$Uu5aVR#HaKkFGC7e`qB@9)r`Z||WOJyy73chDnh9Ey%5+^|Y)2}d@}2Ak1}!H$|#8Vs23RAXT) zR0Y|1$f8ZL5r?T3bRcOS@bE-KwA@*D-yqz34_*FP;fCFTEu6b?=(4ed8&-)e;kc57 zmI5Rcv5K}AGZS7j%mWsoHJbv*nno=FVgaUGsVEUvIj|8&{pm)lx2(@=9J+L@aferX zYh3%AyB6kjJOkib$*p)%_yFAGcuNCZARsogj7w`)vtS_3*CH_3l*7f1YVVBtyuF7m zdF-SNyP<(_##q7)ulDwe72I0D*RgU9ro_Wi)(y&GsgVU^h@LVwD(P-AU2UjbQ!sr< zjLMWl9g653WcT*!|6MTMo|?UQc7Eornb*u*I`gdQA5Y)0=QDe5-lOh0YwZVXx7lw6 z&RcV=etPw$Rc-Z|D?ePheWkwg!jU!&|J89VSfIu`Pa-}I{z%^kDYfo+s=!e&bcql-7Ds5Oi47S(Z>Shj#?f;~JjTFk!#)r713iJF+8z!?Q|h8f0$^l1Mu;BZ0%S z3QG~yUQ2vLyr3F74vCJ$NZ_zhDIx_UoUCFYhdB~B zZ2vH3u!>AVsO*T31P<$sW}qamt15>&Vk3dWdc&iF1Pc)!*Bmbz2^`iNMo?KOq@#9_ zS9BzB*cwAJr?WVYGJ@msk-%Yv;~|w)8A2xr$7Lgd!wMG(^EfHlfgYOU(viSn#W9R9 zDe!hQ8s)eIoYjw|aKnO%D62@2M#JK%Bh>V$3!WOWT`DQsvoq469u))v^C%}m3>(25 z#)!vr2Zz-Zmqd<0BM7cI^pU_}HAR&O#Ugk}(;V7J;INuTU^_+()(nAms3U>HYKoA$ z{V9v!dDfwf1P-gINa1#LkQQQahddHEtfss|GhtC9S-~NV1P-r0jw6a;e`iL+j>t&h zu$po_8sP~X6)A@}5;&};1SwOb-5dbL9l|Ju8(x2n(KwzV4MQHe5g!%*p^CPP(s2!9 z9sDRk1H%KtRD?t9*pZOr;6?(66;4(RA*4x0NO7Va2j06rVoJi>? z1rZbkIp~qVVTH2;`EbgxzqnHlY9w%2;cx<>BP^+tkRv=2IIM7ty=TxYg~^(O90?p& zI6}cOoa8x!cMv0i!wSdKL>P)lvi(ILAEj`^%P6TV#bOXlOCt|(M@5dH3WYFONMI00 zXq2G-VTH3plwgt;S($fWBZ0$`>Jf#LB^nR$4s;}NSmErDFjSHdgQ6VBNZ_!-!6K(p z3WtVe2RsrutZ<@6888y&?2Q;22^?0qh{50-rf7Ch$wec9!wScWxDHXghS83TM*@cx z4%4wPW5|)P?zmtia9H6YVnkO36cJ>{g`*U1cr|1g3sWRYg=k;z{;y0u;8?q8{(||F zoZoZ)z4IF9rE_0fJ7x8UtN*xq{c3d8y*jt@jg@z;T(uBhICK8z^Zzn`%Y4SU;hbI( zSDwB6zssLne&cdw8C`zb(vO!uvUKy3x#U|~T>Q@Bdlp~1s4PBz;o*fZF1&T2Iroma zE9Tg_b7p@z`){+an=Q;jv!~AdXy!vRH_XIlyfgFD-FiKzW)?EAN zghl%Sk1O*Uum)@~&f5N*s~0Q?kLyyo)xp%E=bKr$e&PCYiKiE?TexmZisN!#xOU;% zBacU+98xbaxsZ%WXgzNYN#UIS(e#hTC7zr9;q(v3C7zxB!SoNtC7zl7{`B|9C7z!C z-t_m5EXi>maQeH`-`!@Mfwvb|}BtA45bIbQH-#;$#?DBof_l-+DvwZLJ zz2g#3FW<9#&-U{jPv+(CEPrR4aZ+d#1r{bS%Uvn#VJ;}Xx$F3&EHOYEFonq3-~cy4xac5z(d+1Z8Jg)QQAHI0Vj1+A&U znJ!hzWT>Heo?rg<^0&7++lQBUEmZ3^;?-g&jUtcxL(T<-5lvo?iai^4GR>|G4wK{MF^JZZXarZmc4J^)`cF6VjYE1a(w zm)PmtbZ(AMeAsz-T;e(B%bhPDmw49sGUv<2C7yA<)cMkJiKm@cJFnhWy5r5W^N{n< zHe<@_lFe`@n`0|ftP{#MhLvuqvQ!zDcyXz`R34XjVX3rK8kcx}skl@em)N;fSSpN5 zJhzlz%8yGtyOdkXjY~YUlwHb>OFX@lS;}lL;Bglvy_DW!oG#i~rdB(omz&{Y9YfN_ z&;rgcoVIY<)E4z|?_aa_UGKlQiOFPBsH6`8MO@UB8?QH!a*WF7e#LjSJh(`MB4w z*@YVxZg}$k|K$Du$@~A4_y1b`yg&c|p1l9Z^6ApnWITEQHy=G4Pu~9@H5*Uf{~t3M zPu~BxoyL>*|3@jtllT8ed(bEE|BsoCC-47{95tT2|36wap1l7*q8WdH`+sO!o!aAE z`|8?T7J&KR&EL6JUb}E@W%XOD?^tcFqN^vb+_&;~D=%INuRMMEhs*C@zIvIrWBz}- z^r59!EGhG^n@`XCmi8?^xcKqK8y7EKe8Iy1&d)l(?0nP0rxsql5Z`mxo;U2t?g==H z&hwp1cK^V)&(-JPxszt^nZ0ecJ4?(yb>{naWx$ura5HC3KQR5_>1%9Zp8WY!pMicO z%0f-bi7_h1$yyrCH!~iMN<}EK?FCveM3h6-O3hDsY8`J;nDtJK@Jx(w12dLmU!iJe z@mndM-c6x8o(f^^T(0Jaix!5sXrNVS!A-2w@TdH_(&du^E}IlE=r&uhGyB6>$!;)j z7ci~{>|hMPcfA-CO`+iH0>MVcE0%=ylpjNw*+DnvNf19BOw1H-yP{7gwou(p3{9t{fmz0^rTSwVW$gr7XL_F+`@Ris3H7K^YIZ zs)=q_K4EpR& zf*ADKEvO}bDWefWlEjg8vJuVm0ush$(&vK`d8!dGN}jMc3d26(sV5DdV*<_GGD}tOx)0L*( zrQMw>CJoXk6YVY?2Py?&Wy_aLfOzr5V_Y#n*!|WsU0*lVR75^ighWQ^w!}Jtg^2*v zq&2cw@?|TrxQ+*AwTThx#0X`8P;sSGt`}NZf(&{~g;F!Zo{4c&tPU$%Zi_R|N?Vz{pSHmbU({0t-YjtHMoDWLjjWd3=oE8L`R>Gs`zJ=+H$X^qS8N6d zAl)>4RZk$Xj&mNonXGVDsN<&-Y0_hABmm)ZG$b%pFTkF0g~uieqPA6K~E@MNkuIvMPUCA!|K zoATP>i4iZK81b?Jf=g-!z$8O-K+^*W)6U0+gNCQ1lybpny-6YUIdXVSPR z)Jo%~D<@@H37Gz$fm9P9ell@7i6>1&N_Px*(GM~8e3OGH%a=$i>5_A6ADjTOwGU2!2rn(% zHMKI8nR2FAQp?_{jN=WfubckD95_2Y|N6DhFFEXA@6ONFuGyEF(+mo=b$7d|yF)-s z4K_%;|L0l^4|Wr1$;+Wo3&}z)MqdDFx6#_BL?e=M6fn6<0U+A@@NshbX~ zNU4y>lyfF7)FcV$(;7va)(xf;gwio0Xsynzw>+j@Z!FX>3tASBXix(5c@sV$8FYu^ z^{y0PN&@SXTBWInpMX;)?b?(j#RA)jNM%}4U814D=|oQ0VaniqqJ#wWf-LbW=57=! zO%K7PQ--Ie$ab179^p-?1Zx?!EeNHl-#!7Sth-B7hNqc$mH~M+=ZS>CHd73HW4XTe zcOI){Fy-=sQI_H;f1ufE!FU08)fo!#OM*vWEhJAVir^+Y2p_el?DtQ+DPwm_%Lo?W zLJ6AF42wm>UCr(7YI$=fEh9y*f-BK@D5bUAu>uv9)Cdx$8-W}Scr|aHGbM>`Hqwj& zl^l*I;FRr_FT>lifhyqykbq3w^ijETJK^%~FlA7>P>G|SMoO3KyqF78DZh{@mScc@ zT?+aQvChYJQlfc@!Gw}y>WMaGSeMQ2l3^k)gU*Velr#O2Ts{`TN#CK_> zT~529CROs+1IT(SVi=gA8(>~S#dRQ=s`=%JWCFJPf5Q`P%1B_>ri_5qgv`eD3{w$9 zMl~Ym8-`5oFl7)0d2(cwYfJ7H3|l~=)(jd7tzZfyAaTRhDYO~P=Z|vvaI55)v1w9}Lc z?N&z9JrHkbC0{fV>$;7S27--BO|=`9<&sHB%c5Cds;)w%slR&SO&Pme=Q2#F#uTg_ z%ck49WVZN-Aw>{%>-f2}hp0Blw)j-X5 z|F3_dO__aR+N~Wk1d)+OW{0R zD64oQ9mxx7+~19K?Mk-xl>P3BH)Xr0Wr`+w@=z>O>adx5Oq5$~vRm6p%a}$h>5s;- zq@ej*6eam;c}O8mjmt#b9s>bVIOsL0YRh2k9do)@|Nog&v8g==*M7HlLW#oEGy3l%%-zvTQcXJPKAbGg|c&t_)+V>(!PVuMwnxY)rmvFohnkt(zAQ$oP}>(p zxJZX~A z1*qgwWZDGV_S=A1&}|=a5#3b=bv>PE$q>}e=5tL!Fr=7)2gVU#x-Y;4O!rkF*vwc! zJfB0Ds*LAz3M|6wDJ!SCg!QZvgJKoT?~Wn)Dmjh-j)?`>$Sa}_WV&6e6lkSh3-O{SaH+FsrTLXARepQlI%@Thm)q}fE!k9hz= zwrm$}@)($A;{k?dWHYzk<@{dr97eT^)Tk^3JUt>6MXUA!(-+YN6Pj*EeCDU&J_czRu}rG;?>xN>3vaDTm>=GLQCk6B~=U<1*+ z>#jCdZmVP}4OP>oqS#$T?YGb@HjV%wd5O zACtZaS zkwr~G4W(R7OP1Y8Fw)%hC1^}6z%DOAePRK2c?oKL0cso`D^xOG-mEu~nkVUvuQ%ga z7BLhbZx?$HSWr2J+u!@zHEg%7pgOStySxOIi3Ql@B`Eg=n1JbSFF|Qy0d{!_ihThl zIArYGWdRC(0kU$NRn$E0L+ET5U`Qr{6zc2iK{FB7Qbi)k*qtuIQpO!=0{cR=?uitu z>#2s>F?}(;2C8b@h%vg|X|`Bq8JVgYhQ*cgZR{MY3iF9nU9M%gBuUA+bZPileq(-O z0jxwAYDo4K859`Koecme)``O`nlKAG5v*VYRI3{WT+F1s;|P$OSb%j|^0*>CB2qCL z-fG$>7k%+`CJ$C>ooun7CAwA!iqK`#g{Q_5Alny!q^tl2HKSFm(1{XgT8E7FlRJRxn3DV9epuJc2(j;cq!30q(a8*i%_Vg9+9tZwd;NL-nPh|(>mHBPt*YY zUcc97xT66=?vjg(!!Qx3;ZOcPH^|lF&#AIAJ>!(w#gGH~p3h@~M_tD>DEH zcX4B5BNN{rZs$dNV~uEqS_0x+;VcFMm6ndxJfi+vo6rmNM5xYog)ws)7({=FEeC{^MEKC z*pF8k6AM>>Fq3e5ax{P9+h2p+FOqJ*R>a|0jrJ3GGE~O1ScXkPs?G`&nkd2vjBS^U zoD|t`Z7}Uxm?rafJ4=+-lSb2E<6()Z8=kr!?MjViJVpD)-2QrxZkt{2{(tV&`=|EY zx`$o+)!G}@kkucrzH&9Na_`FFm36yt@fFK^mp)@x)O_0FM;EQdm4){$7z>X1cg%Cn z|8f466P^3X+@H?{XYZTcoINn}wV4;&-2*>6U73Em<71AbW7TH-IQ%G^^sMy^dS9qp zl#<@8r9e3tcj2rs4IhIbiJn)cqg4=dNckSNq?3&QE{%)t9_^^W$$#w6R04`o4H!8~Tl3*$np? z`!mrtYN|yb0-KFktQlZL5%=aj9&ZhbL?Wn{@uq9BT#!Li1IGWg@r%_@-uk7pFa5{2 zzx(I^Dx7{z>)z|8$bB!p@2+$1_}dGBfU-&U87q;Bo#6nG9@rERZ#|2NG#7$ow#M3_ z5?PmQ;At`@`Dl;o9Wa((@TyZ*7rya5tK5N`V36Gr9fm`S|*Ge*FH^ zpLXDqOP|UVo^s%tYtH@8UuVvAKlh^VedHo#6Yn$5o4H&Rt7NM|0z%`U&Q`)uIT(s| zDYk^vqne4-eKj`#cEekYU-OxL4~O6S?k|0O^`n~aEf?SVrGt0AHNAB2*US66?<)lF z{A*=1)Mu;-xkSls;1RbK!B^A$bN{*Y#Yu zT}Xyl7~xerQbQnwoZt%)S#nD&8qa#gL!YZI?)&Fgf8xT}HH)80ebRf#CYxVUdMLd_vzUil})72*j=__24>%~jXbLGT}v?5 zb4iIzCCiLamE#Cr!HaOkPRy-ssoS^y;&ln;BR_a8B0YQ$^w)2@(tY+-uX@Jqp?^N@ z&9^$=w&zoyRyN^2W6-JPUU~!8vDp!c3<<$fBMm8`S#m`Z;UIOP@gf&1*%CYPq`y4 zSB1?dJ!&NfWV=ib^R;t$Rmu1|w34uDF-#aR_Wo@DmBH{g&jsJ{&FFWsZ~OB3cizn2 zadY--*WGrV{Po+?sIqx+pK&fKMDKAw%vG zTT}6>UtSdZ+iUi|<%^D2UO4leiu>VfZ$HOty@R-K^8=dB1OZiC=a8Dhca-&xR3MsN zGFh$-7`vhSZx>&A^V`3Mex9vOzcUrT^?&cXO?vn>|F`dlXFdGst6r~cUeIT3V$lFp z%I9JpNXk%DP!ACywan$y{JPG!z&2r3T{xm%BQ^IEgyN2@JZuU zfAg~6|K&w%l{0_*fnUGoH*fojvUz@=G0D>uSt1L?b%-n_TKNbVq^hF9RG|v)WjaD2 z-|@I0x5CA@wBqasKfCwp=lu133vd4Rv#Gxh-~Lqkwxz@O-Ffqam;S?7S6>r+ud?~V zK4U7wt7QyKhU+CMA_p@$A)7=h_D3aOO3qj=7ne14D^hWJvs)(pf@j_RHa7a4Z=FJN z@mDU~yYx5vHs5jTH4ik-^FO`wf)9N60%h~OK4UwK%T4-py3U)fM#xw62kKg|O&8&2 z+8yAndS32u9zTwU=&i;3^I!U&_4eiWKLmd8@>_rQIrX8Ff8_k%=FcCx{WbKB_ug^% z)PGbqgMG$aAw-Ko7jKo}d`%B(Vx^3*5m!88mee>DSA8X)RiM+YR&7h-^I{bK9j<$` zb;nI==$$aQe#teTeQMr$_m6LU<6nNT{Li0PHUoXenoCc?l2QPHT)E{=B_e6GNhORb z;w|ee*3I0zf2QH|c#yw|k?qv%8+to)e$ze9 zJD;6>rF!vy{%iR3lW%$Giub;y`Q9(QGYY-`!rMNjYGv7vThhF?!=~tJ0?C@+xg#p z_rn*wA{o8=r`oO8y@%WP`b+QfocYCT)brlSe)6NwS2j2Lj3 zZ@%&C87TZ$bc#x!QhBwqd2XL^!WZ@PDbXvUS}}weu>t_;HG=K>wGPe;HM^9G5?@!% zmL+bPbnD&!cj0Us1%GtvbBxzt{Pg$KY7e~l@CD2Jzb0Su>lDy=y0W>y&v=3`%7Mh+ z)#yW@^L3|R>nq*(uD>{Vg&Hw zR!;By%ME+Jdew{m@1ie!`nKPH`2LeW+kN9bYV=2+eTlO9{66CeLf!_9f9}8Ky`jb> zS6p`I?cckq@b$lV>x(~{UtfCD`>S_<@N3kbYd@xJK5xKy{6MY&<6j(j<-AllZSnF~ z|M}Xz+?x;T?>GziHWYi&-4DGw@}iG~f3Ivlx6gQj@T39b_uT)w*S+I|7ksX?^!a=5 z|Jb{W9@x74Ci(JzYA+e5r7r)}CzQ?S^chbObTeRl`iCno``O3d-8$F*(cni`F)o?bcg>-b7#%J{fzV3 z82GW3b3b{;tA2Xd&+PN@zCPm#0$&D<APS0@9gFK-=JoGa@p$tJ!rZ9@!mVX^sXyjOWg2p zt4}d*Qa1Mv7?1xVdBFGvJDNBX`j5Te`vS7yI{WSa@UxFpzN}tdx!3*5|9f`e@58@U zHqYrZp5V>cfbrS4{?kdzzrT%q@5AN$U*x>%&<$_A_Nwo9Qv2q*Z+oZwZ=2Wm?*B_u zZ=PB^wDQ2RzWA+$kn`W>JTvc~KHL7sC&JIwR~))LkH6X>vm z9RJ7G6F^B9S2$L^0wWt!g;avbxB4{!@K9EFD_yhIt0zF(H4l0<3&3W)z~+0k33v^* zl)TqV%cfr;?Ei`iLJ#U2xVGvWxD1U2db9~5>@xbl3l~gxz8EirfP@y#lSqJ3GeH=E zvy2rWOL>1ZlsD+EYUN$7wq04zY835y848etJ4h+3ejO0U%DwjQpPFahii5ef-Da{KCr4qVH z6RC<-6y&U@+6@Vc5>ZPDupLHmYMmo0rA%Cb!vUcrL#&J{hKfYSUXxFsZ=1yVkC?>p z=f%fq_K#%}z;2lY6EV102Q0=^u^7*l@kjwQ;(hbM#y1HHR)vnLgLZ`oxQMOSEKh^V zD9L;$kQV8(oQ7$m!f}}DuURK(5+8q)Z4xg$o=FUM+~9b-)1{KF5<@8} z%R{JPjBOHke8@J5^BysY;g-IS)9fG1B)Yq05^*Ky6SMY*ZFk+HOL?6}&8(j4SEw1^ zB(S_#Br`S8oo(_aouCyyTP>(2s&c5wWxeqRme-&_mXS;O6Eul;J>NEo;PFgixR>c?sJk7W{_-7*P50n@QA zTVjAZ&#Fc{=3=XnNe@o2!D$liXAo7@k&1mJiY7zuj?d$5S?hQt={7uq-;fnXEyPaH zB(7PoO~QXXlNj#H{WvYkiJ8Q%C41-UdZyhJCI+iO^hR{mG%a9%_ zR{>f38EC1v~mU0Q$a3_`*pYol|)IBLP$hF>9Kc-^ylq0@f~kX{y;a36I+wr zI?Up&mZb*6Vkig3@VZASV8NJYr}A4UQE3qbQBOr1A=m$7@67{UN9(%rb545pZA5q( zP;o9EVc=44+N5oY2$Qtwp0sJ&vwvojd2=GvN0AJb(0aXHGxo`^od9=Z{Dj5Zb8^naRA!WhYD>#OYfeN#yT)9(r(~j8w*fj=%Q{WgT4UWFNh@EYrvt~PL z;+c3AoKXa$Y4IUWQ=RLf|NrXt%HH{{M}@8f|M>sw5AZ*_`ab_7;__eWUD~^Q$r1O# zoX!&$^FKPpySifsp3`{C!|$KsmEFZ1Z%^@-@?w5~Uw8j`fdA1YBkGU!fAdcBUl;Ho z{zw0@Zhu%bfj+4J5jH#9>tgsMKA9QSQ9$R;whKg;P8;#I7vamZalAGoi;hVv58eOW z1oaNmxl|@6;j>g^THw5H)oc0*?_BmQnxe+a#HVr|uBugzinHE`n3Vey2``V&Iq2+M ztY;M%kPw)5s?;dbuAZw@yM{(xmj64v^ketiN1T5=biYlm9-ZQ5`j!p5`2WaZcba+O zB5r@m@ryhi1ZuR}rr!Rq8hdlw!0+#S#^BZD1KulQ%a1JoiH?C7IPrc^f82;GH!ALdUEMVn{*ln7fres8;wo@ExrUEJw)ce+~NSb0n@9-#`Dj{FwhPD@%w^qFd*(f&J7uhV37yi2FUqpBA;rO$vCP@ zDdntJCxi}r?(8}6_5(YY$%(yKEZMG3^$GpZz5n9@-Ol7YHK~y5MAA)qcs3ZgVD`{P z-9Wc;F-rGK8b*4JezF-WI-=fgSaCxi#skVp2?4Hm)hcfi)+}8dk}Nfrud(Pw=>2eHZv!d7FBbiuRz(;2p)-!TNB9k4YdT}clmK}x> z+8?lT01(0TkT>on~#Jug{)?hb#Ro(PH88bl_YQJUDew0iTR&i3qRlXV0UkF ziS(SuoPX-!%JxFz_OZsJQ#j~;L*V(PB_3Yg ze-nDwkCndVamSy3@QlmWdvyN$kM*9rzkNeTr*KSO=>7Ve4ENIQ8ah_^rl%c$zWJ$_ zt@Cl`pM0znyI&`AbP7M`h0Y7NhL=`~9BcfYcOQTL?guYh=~d^G$4b9p+@Hd6dZBbN z?k{H(E)@8Tine&O` zR(!(!{k?N^3TN)Gw<3(KzjT(;onv{wb_74)_+TQJ50SxOj~|P?U~<|%I)!8SLgYmz zr%UFQ-9A=&^JC!W^_Q*p%=y?-qi!9Y!r%O~jksGW$&^@KIuHR)bkrhr|^USCVCz&OTYBlW4$kbR2R>AlA~U-tIP*7=0ieE7H(FIbbdk4_Qh@by-luSu7Dl!kNs^Wd8FyeB-E$mREEU`=|`vB(S7q^+Y< zgiI_%USv(Wq+U4PKSwxj)aShUvh|)he|D)+H;+yciLuao-*kUTk#M^I;VX|t{_xi> zTjZ0@KXIv1H;zsb)$vUr*jojZ5z+`e<$&aFGQ z?A*L_)6R`MH|$)$bKTAqWCYN5uH9+wRCaPZ*X$5G=+4zUSM6N6bH&cq&dT<^+xKkW zy?xjAo!fV8-@bj@_O08uY~Q?n)4{a|&4bE8?%>-VqQpYB`x+Wxis&Hc)LZvUEnVjtbV zdjG2ZEBCM1-`Zc$x!+dt(DDtL0yEqH}BfKbMubP+c$69ymj*y zkQw2o%^Npw*t~x8y3OgPwW)1hyV=~VZ00tv*(5g6&8s)B+Pre}ip{Ofm5qBh?%B9| z^YD(t+YfI$ zy!G&w!3S}uQ=Q~TsgS+ z;GToK5AHg+^Wcty+YfF#xb@%`@NVp;gBuTSIJo}cx`XL~b)en%0&EjmTMNA`^hk&= z4gDX8zYux^#Gem69O8|kZ-@AEp@%_yN$A@k{%q(9h(8@Vg!tmn0mK_Z`w(9g+JpGQ z&@RNE4DCRCL1-J|PlUE0J}mluZxDUtUm<$P z=OK=f&p~vN&q8#Ne}QNt_dv9ee}*_hJ_B)xd>Y~a`4mJG`6q}5@=1t&0ZQ;@eq z{C?zZ5TA^^6=D*(4I+X3A;dWH2M}@O_aS1)TOdY}--8%I-V6~%-URU+ax28|Lw*-x z82KHDXOZ8A_ypvQ5U)ml3*zIE--P%$(`?Z~e{d>HZyh~I|19O4zo zO%M-|Uxv7c{1U_+0r6ju=R>>) zc^<^iAlF0u6mlNoCy^hA_zC34Al{AqD8zq6egxvjk>^7E2jn>r{~oyx;@=_v7vjf| zABOl*Mh5R7Ik08&6_+exQ@o$j;;)jqa#1A4q#19}7i0?-{h<74mh<}5)5dRu+ zAifW=A-)%}ApQk1f_Miqg!t#k0OETP6XKsC2E=zGeTaX8=n(%H(IDQAs1W}M=|OxK z(uMd=qyzCChywBLhz#*`7kQ&6_M0kk5fm9*hf^ZOj9jQQk15$?gI)sJzYX}4J zwMYq~iChWMKpqLPkNh8q8uAE;J>=mKyU4dg>>v+=s36}4QAVzS*hUT^UW*(+d=|0~ z@fpY-#HS;>5G7;>qKIrmY$96_8^|U^0oj09N7fmS0I+wz6`Or_9cjgwJ$=e@d;{DHO4Kzz#Dry+j-+NU5sdF`JdCf7a*ky!f##KhX& z5aVnA2r;(y@na1AZKwe8L!msx4~B9OKM=}7e19kd@y-wp@vlQ^h<_PML40qBg7_C9 z65<`9Yasr4=xGq&6M8DdcZYre;-7?`0`d0H_e1=n(32s)E0l!z&JY3d9iarow};{o z-x`WR{KF6q@%KX*#J7Z^5PvTef%xVS3h_;$a}aM0eILZ%4TT}TG4v#ezZE(Q@i#+H zg!mhwCqTRqC!&_-mo>h4`zXBZxPLz6auKLXU;`>d+a8uL@lS@s*+P zhWIO?$3T2V=(`}kJoKFqZwh?}#9t0Q8saa79tH6iL-TR+t4Q(qzrkJ<_7CzJ1ojW| zY6SKV@+t)O5ArJr>>uP62<#u^_&1ojW|G6ePy@=^r$5Aq8L z>>uRk5!gRq-wXQ(c?kmh2l-h9_7Czi2<#u^rxDmc$WI}#e~=d=uz!#jA+Udt7b38K zke@_g{~#|wVE-ULfx!Mjo{zx(0efrMKgf?Guz!#rLty_PKZ?NqL4E{*{ewIgf&GI# z2Z8+qcI2>skRL{1|A1_5uz!$eBd~vv83OwU2@u#nVE+#L2k{ZuKga}u{eyT2>>tEM zVE-UC0{aK-_F?}ZBLwyjGDKkiAOqx4pfc-?_wAYg7ytXTz|MYprLgkvtGoZO`^Mep zf@i45tUYf_-ucw(Ggn`<^Jeg@FYFv`e|Gz}{hRlmz5lT77p^rnzqe>CtUOsf!%D;!+5cnZrxH~!IyVpK_@Cxue_0)sC{ST~t@7nKezI*c*w$Qytf+zVmoT8Q1 zzIP>him!vOMvyLW1{i5{jOwObk&Gwgl3JE6wUbvX?$`@r5!H^`qtswxOR*%n<;@vG zJ=~D}AeWei)lws6cJcx6lI+EaWLX;2EsqeJVaB6|61mldj8az$=c0OY;yP2dO2&$b zJV%N_iX=^cEOZPySY*!fXX|Rg5L(6_m3vAvX@BNv!I|lnh$w1g<{L#l>;JmnOq{A{A4&es<3A^+`Nz z=ryk+GHguN@-cIU7DYN96=LB`Ha{9FLsWM19eisOGFX-^>3u8Qv3&_slv=s2kzsC@ z>X+KpR--kmi#;r@3?(#AAa9=Az)&jd=Z1__rKW=*-0S+fRh)3PBE{`qtX3b)=tfxq zd9wW7_drHTCTX(bU|zB`K9|nU{A{5akLUrBEe}wpSCiW_kk^VeQ!#Y&)`hC6iIsER zm^-j)^;U^$leR?{Ck0`Uz%`7H5kkgKvoVo(H~x9fh=moj%T5BlQEhdn88sK_XkI5` zH}kZnn7xQ3cRQt?RqFT~!kmHIGCn5xywl7CzSOb&vK25rQkZ2sRxlkVxFns>3t2&R zH_oF-clC(HG@GJ_eZN%7L>ROmonSIbuqY;sQ-z+{5n9~>M(YY26_4g7uu6Y47){DT zj?dboG+OO?&M3z)lR}40w(XAkVtD-q+b{d*+?lG z28pjwETc*>rjs3-{i2GrA%h7(3^CQ1lz4HNK-rd$rqxsq)cHX*dE6V-(-l5ntw=Ui zBvw8@XL!{HRxaSUmC#zU6SP?^M-7@0+fl^AOszNksVYX|zQ{7G51%ufR%t+{JEeXO zq%>r(VkwQu$)QL0sp-s%W5e<&p9{03uKLJZ7mQk@HyoQu%8n!_dYf*lg+i1r8@Z;4 z7SheCAl3{uL*`I??e%kpZTEOIBIyGV01;NisG&@}EbGyXR^x_bAE%6_n-?V2Wj4QO z&ba)VgUBQ24AbQ7UWdu>6`qzEJ49kMFurn&JG4-e#AJ*{I-8tj()Ab>b zpEGo;ic3wki*qLJP^l=zXm;Do<(o2=_my(ZRwYn=4CFL*H$Dw-D@JD;pfod^5Xc~r z^*dQ7X&Y6%R8z!}O-XhZYvZ^yRI;oRnVZo3N{5r>S)a0Y&bWe zQcWq`jL}Up6K@cPT-6I`r!qkc?ljuZrpQX6Ep}46sfcTTIcN0ZJ>RBA)@%^Qk~msb zxtXTxg*=s^yArMD24RM5%V?r8LY_ZoGzx=2Z3Hu3X)9bgSL@e7LQA$;P79;JMh7Fw zP2&_lj%)75C+3X0k&GqEHeX9Nu;gH>m8f*pEMUygpt?h?#nCxCmQ;kCPprRg&fpTY z5=gb11o1BQl2sXv#R$`gc|(UOnGGE+M-sB1W+Z{AhrT*zR1&dXvxtt|0Sc;NslF!8 zoNTOO_j#q+#iB-1?j`J!G02c>|1@WmqfV97Gay6lB;K0@Gt$yAKFxq!*-22{x7rX3 zStpu|Gz??o$&kS|^>(7qjC=7RYQ$QzDIN&vR@#-~q=DvWN~#R&V+;%Q7P|7BIiule z8Hb+~8?iv~MGz1m3NvP+%TWgtW?am%xS}@=PtkgP^+j`4cq`wjx=q!!-DowM*QQB9 z6Szo_?1SURT*RA3Q-gYep?Q4c-nk92?Py+lYVct~Y}BZD69|e-3dS%<%F&)*mzo)g zt*WGzU!N~fB?Y9s!Q9#?-xp&xmevs?8XtnKDlz=KmM!k+e z6RldfppI}eHHZ%V4BJ9izcOdgqbgf)3v@xDSiCi(Vp%R)QYBf94ih=klR6bVYq2Rs zz}J6t&PWk>SxM_!PaP!M453sFTxl|bi05!4tw)QqR->NRGebMSvRG*2WhaUg8QPR< zDSco>^j5Si_osTD0^OO33v|mbPG`kTOx!T$Hlk579B@N>Fb$L@&(Sj*WGwB5sg4y_ z#H@=Yh8d-To6WYeHeaAhjx$u1wmzn;d@-My%*+%f^g)SWpRxn3k?PQ7M4=e0+@O&U zLK`J}T#BN+m86_*HE4^Ru1!(2-x>E}Nnqc&&TBg~i?UZy$D z`g~F?GDJ+S@IG?af)UM)>(vNmWK(S>W)ySzXs_RljMU)>9SmCoFE>q5U{0=ctMdu2 zq$XBxr-=5X`{Z1bFi#bE8mmEK=<-1 z)6IU;?xkbdhHZnQM!CAnaIRbfRYjP&iE3^#=}q)>F5^cAa>6R4yL}z+^mwr}%0!cP zo{C4Ez+qK%ZO&lZ`T&hY##B`&oHkb|QdpkI7)qou4aal1VkW5o*0VJ_F~s?mz%(0+cK+J_XjXVpECRf+|%m7fx|Nw3;bt4y3wo8j`5yqhI60B`BNZzXmX^|b#t3NoG6&OV~ znQY>%T6&sEB*$dTPkX(rnN{^pxvGVUbf!Ma`h^Y+o`Z;o_V6*ZN{t35mia*=T_Q2F zZcd|NK1+>2b7ZGgjN+wqf2g%5#@2`Ds=UYmS5bQ)(^)(rPCAOE8BN;HRFr}tmXh5} z743F-f*-oO^ShnRPs)`EUZO|gRw5m6Y`QV2DFK^eXH&CIpzf5Ub6ic8E9%;OKgOmf z9UmVwNG2QDG-mc`yxmFId45vM!!~dcvC!Q_p^y?g`w*c zyB<^R3t}LM2uLe)459y(HN)geZmyAXDu6u+ zgg(IvV5e|pdPsqm7gPXC9>?fhq#zM~(Sk&$hyXqQyZ`x#2u|T&b+JSQ4^jc_l0G-E z5ieN*Y}rHXm~~+Vum@KB0$=!svS1g+P9?!F9~e9JP(z>+IR2m&z?6zJn$VL(1!G1@ zmo6vjPF|1UCCVDmiZUj~{qA%?xCyZhV*Y`uhf;#C*dF0T+brg#%+yPFvzDK3gY5vIi&h@`Z$IEp;UIXi0rx=f#^XWpUl8Yb z5g{Pn(H}Wxrv*G&J8jwQenaK~;-bUvTU7}Ri{bcjikOR*B;wd;a%xf~qgawBx%#s|uHy5@d1dP8Woj zDqXh)+w?g4^fZp1AovkE|J~=Gcs!zCz^tCek@Z_OqOs(~qKW_8kLXi)(_U;uFZ_T1 zi-z%i)CmLB{&!vYQvLcKa<`Y%vTw&O36^ zafqKyB?q7&PB{`+Y?Ki54Gy<+5wU3Xajc{VG+jyxafP*_4?X`DVMXwB`6xC5X~hZE z!6;2*DrGh{jW-ySKbxsVb-mXuHak=!pOph^c(&9gyIqEL2aKF5F?3rVl=#U66-3*~ zoGYB;y;1t|#^Ld$gK@ZX@YI#u%DYxq-nH|V(3QvM^1vVb4O<@8t z@ZH~8xp(hld$;WV=-vJztC4&JwV+va;VU$)uby&HJFZ*078;}sj$nsDp^aM<5?>Q;1PZ|f!NADH_ItUv5v zfB%DfkKTVR^4y*0gP4NHt-kP(SiAmkS>8lr$byM zylhRSqrS*@r|uZdP!)dclZ`>#*qpxw7qTGLnUfXCu~3YGBPcCwbf*G48Z-VZ<#fji zVUvC((J#2f&it*CpoGx|sPk>+Vi{DmT1DEj3N>n|a+QG~Rw+3!FjNSq6x`j(LRCVO zqZ%D2JsL%FtwGAg_%>G>czGs2ZK{a?uag4L|}cJy3+Y+t)~L?|pL{y~L2!tyqzblO-*xnsK?%p2~49&*7fj5;75S zkd{m?Aj!R#uff;Hf-Gy3lvxw7RGW?mVLuzKhqb6E8?C?*STojYf|{^(KMIl*&uz4u z&6wRNQ(2*;Bn-EWCAuW%n7nV7$~8>E@nIa3Gg`yM*Glv2K&3pJXtz^rcBrzI%GB>U zd~TR2xe`e0*~kj1q9ItMl&sUc^HWzr?9rCj9!TOiW0yw-ra2l1yBU?^QHP)@@B@|Qf`o?SK*AZ#!dcHPxOKH6s8@h(3r%M4KUN2g}I6EXx$kS2WAlM-Vw%;;8n*u{-vIGJFl)k48x2HkkPqdV0oO0LaM z28C!V)y`-}MIf@Hiq=D$)50X&W~gFDh-yMFBIJDrPnV(&irhX|m8Ke*hF+dz^0N-s zvWJ;=vc-Y2enr=rX;!#dY|9#hD@n%>rRIzjt(KWeVPsWlBG)7Av>_x?Enl$mLYJ+~ zNGDgo(^Hoq8RQ9b1{E2NS*%Es_$ZkVvu#_G8W^a&MMXID~X8Y_Pg0p=+Scq z8Ef@Osmj)El+CrG1H<%MWd;;^EY?$zO!eug60h(QtNBSwpBQfF*XK6E zxn`~!_ym)oS8jqfPQ&=+Im1!1Sw4_kB| zS3x%Vl@v8dlygC@i__Ft)mpI#vnI|NnqP2ZT0IAj#v93YrIw#~W!E<`6XoJ;khC&g zF-1vXjZs#&=Zr4x2LWew!_&M0D#WUBcfgg&9Gx*6d2nt-axJou#+!bOSe<(^G-6b< zmX6W^xJ`;GMxg_?YiBCi@u*~$E6u1J4ohs>#AYdXRi%nD zI&e}zhqJI`1XHIgrl(W6!Hb(JWHjR7B&!f)@+Hfc#lcjLaUGZ7$1zh*HN{a>%Ti^q&m52SReW_PgEPnJ_91fQ$t5;7^Xq!6z9o=5z8=Q zQCyv$I18LQjkbBU-7v-~xcMy87O-r~p&d(4`V(A9f&9S?^CfWHI+cCN5bB!V?}&aToOPo{n37`#IpIbd zPn|QEX%iQjsy7zVM6~QwM)?Gfy5^uVwB1h6={v0<*G+e=X?=5V&N#(7I9COpPMy}6 zQ$a~dB3{vIN``F28uD0=f$dNyVa9o>MTo;`xT|j8F~9PXn4Yn%3Dq{VY@wRcgMz0| z2CZ=>)-(r&jNJ9xqqJ`^VSnfPIfLf~-LMjvE|9gp+?w^MiK!8VB-P0jK&g8wnewu6 zl$N#8_S@$S3KT5Gy$-DljaH@H&sBhjULufXOc+&jLo!m0blO-u*;I+`H_RD%#s_(R z$@-M9;$YmGF4`=hYOfy|hlz@j?aQ1WLAzeEoZo)!oROhYM5Gr?IlLDa@IJvu%nCW1 ziAEB4oS8SRpi^N`+)}Xv>{w5BMlfcq!$V zD;B=}7|3X3TZ5(<5o?mBk2thsd3ud$hgmgSD~@}W8G|}_ywqYNIZItR>v^<{0 zfUjAhiAx!W@|r#(3kF-B8F zy)p}@DZenNml{>HU>Xjw{?l^?@KA9IiW`@>__SZIRPyPO+iG`*3E-z0AG#&ILFWR; zwS9MevFppFtg+kGe6gWeU7|W3)I}^QjdD}1#TZlI7E~=qVkTBep=<8k25rS=YMSd~ zQnKr$N<%gsE%+MSEw^<$=~vi6VHz!g)9sb5(XHo;{Re(23}8|mpRQiyiRm|Lf^cB}#xv_z-i1K$!Y=845(n@XhJjHMEz zjZL_1Z{*rut7=bTLxP{gM}C>l&gz{Wtw&oz*olpkWi&sniK%+Tm^%|SGIUfS>M=H7 zr5#S3iVjBCv2dm|twnN)Zhy#_T-wvc_E=oKXKsQnPuOHcQzm@*Az zmL9T2wdSSbPA};N8j2onti0sXPiOzx-;SJ4~g)}%&Fx~jv*DtL9k3c%}WVoyxE z8Me)OxG+f9WZvKSHE^2W`Hxl!RW+iRrYL@LQf{T=Ebz6R^4)~#V%7wdJ6GE3Bc+=_qK9(+51u&NEjc3xnNkX*YK+@0M^)r_5cAqncGw(Rv#3pVT;L^#Z$x&0 zk<^;M63T?W4McLk9>jpq$ivn?vi91;KLZid-9z-?DgdF#iv zs$1W+`N_@S-hA#Rv-#+af7tlVjUU=5Y&>H9W9zS9pRLpD4+D`@uL}ih{u;G*xcY(B z*Q`3LPg~tt`J0tj9QVrVUmbt`@6Z23Ef5VoGqiT`92FoZ{!$vi_MPurS=+!v&s={n z3cj2xe0A``Pu{Sy`rec5Jb-j;XZ4N~Da-XqAmu$LN?X2~g|t6E(atgn+WE6+=;`aL z57KJOHP7Z|2sHHc(B)E&jU-OG7Hm0t<>Ff2ebTi&fCR7QPfog)<+@07Gfz9|T9zrt zMxJ`owZt!eEo-5ZuH^xwV>4^3C)!!g{5QXtHRMD)%Oq%L@>5m^OY0rER&#}FP&&-xqjxt&KFO#vrK|^?mf}Y z#Rur>zn^GlnFQ^8;Y2$RG8O*YiFTGr(9Y*iw6i=FF52y1Pqed4f_6T4qMhZbaAD`O zC)!yiK|A-HH0LbeIdd~VaMGN!OgT34l#{L{cJVo9^;qMcxqMcNs)@tANKRC7Bf>rB8wl5xp6buF2_|FFUho(^`lh39d}5&FqtG;sO>O%e(Dif zx1T@LWP83c>sHJ_%rwADDobUeqN;Iuvm?osoWU2ud!p^U@-cOk{jDP8KiPF zIim7%YEW9zu(o}_hKh}a)5#X5N0NhaK_6xl1ucc?qs%md%Q%@6$tqUu2teVHlFngl z#U#Q)#E!I8My~Q|%S@Gcsi&)I)GiFsnd!Hx37$A(w(InC?9S@#QOc4)jX!%y!}cpL z&`_SFCuNN{{hmE?qgtxipT*>AwLlV;sF%`8B|TmdlWE;OvJ;BckL3M9G@?kxlxuMV zyICn0di7eB$=S(9lfiYbOM&XrXHs0T#MLcc1g9ZWA#ILROBz-`cE1L8JnFUlWX^Xw zEjn9tDP1B|yr^a;wO&F@HWI0*pbncc%Rh?YmK@1uQQ4Rdu}sDYVyRJ^>P2~Fl907z zF5Zt0YiJd7`^p)GuXEXfm3R3RMGCcYZnUhSbb*F)Tx_XPmz*_PVZ7*(oKnLhVXMN| z7_-x>O04LM7HL_a@nWKwsJ18~Lq;Q$`k6-G ztfdM5G z>qkLas$_r*gl5teRHzCiMsJP=nL>mog{C(hjJp{r)t^wtG=E0)dhK$LG##(j=?8Ui z60@WM`T6@bFshX#QNEK9#(82qC{%jUtnHCQQ6OE5ml?|P%!C^NueGDR>ycTt8DVvP zsA+>tOpTX{RhdA?!vR$Sb6vT8cKt( zNvWj-HpH5pwlN({#N=2@7K`4fJi#hnGt$Y3lIYL;qjG1;6gxPP3O6#Nv2SMd6h@@_ z)kvRg*8FZg=~}GK@ubqO)*-^YqpqOTKm;4m-=BFa(62U9snb|On+I>F%+*2+%ALSYza({Z)V75zdI zq+AiBO{#uoGNqY@SgUttlTsi^?5wb)VL$hE4Vhw8n|Q5AwhY`KGO0=>nVl6qE!*oF zObV~cL#0?M6sCzTmp=-+L^xh|^k#D)RLS8$Ysy0ljgllttTHgCO{N%gvn7%$Sm+sz zP5FTZJW$e7S{S70UT0YYb-#wN=aWMLZGltA_@LPFWi(clyofv!8Lj}1I|#3a@*0Dc z#G_%k2GWTX&{n5Zlvy`!PG*g4N^mi`?xqwY4(59-OqS&mxF4vZRB%TbR?|y05Fx9W z)RKn1zrSBYNyPY}6IMMXP(VrqH`Zv#QkxOkQgvvtOugtxQnx%Zr}R;`Qy{&XI~~S2 zrX1}PXc{;ZP;9!R48&a0rgNk_mIm3XuHk384pr%>`CkGlhO5=3tZJh|`#UiEK&zNxY&) zrbNMyB?`k4=5|yjo@LJX?xa~OG)JCbfK?@xahLDO(Ej}ziuq~7D3G8IcE8scws^K6 z+NPW)4BqwJjMghQ!H7@u-M(@ZCK`bNToy>qlUO&F%BeOgxkNaS9f$M+$u63ya5> z!|ItfUu27Xwpq!J@-pwpG`rLnJJS6c=%OT~aemSgMhy!%j|~Mq$(1VIzU7{YL(+|&NF6H|Yb2KQ88@)mxrDvU>?=~1N9JER*nPR4*%g`1D z&UO3cDwD5B_?gm#tO|LnAa$$UR4#UV%_R-%Z@6CrB{aYZaK%!(!|AZ!jPZqDvS+%I zD1vkk)ozfQlF_g(QWSR-@jW)u2bJ68W&?N=5~D6i;ecVB95yHUew)e4`BA$Jasu^6 zXHo_`;f6A4Rq~2g4Aiu-q+#QE7idUJS#0FYCgbLS6ryxXr))akqS@k9Mzd&=b_*cY zRY9z&M|1|&qdi{8>vqsdzmYbgbR^L)rAJ4Vdf5Pp zW(M^ZaPSFJ{elK^t*AWTi-PSi>GVPG`C+*y*2?&qfy?yxPAN}L43p={p|ZTEU48TY z8gSk;H4c}11%DLoXz76tya3RmoiVG{BpS<2Y1G5CXp3`?Cg5(!mpsArOK47IR7Q$s zgkph>w}x(ppZ5J?XFAB&hE-2JGXsu1-O;E#>@^J0;W|@tsU5a1*jM9N(awV|C9}N8 z7Dh5#sQ64#D1!KR%aUc_OVanTnh;@(qsEj?dwou;*60jQkmXu}Y$|%c*^m@U(RiAO zs`+r6NRM^yj9%k-a8uh=COuZ0<9cU#Ke_&A7qr9WYbce{AaBUH+(Kg_*>CBCY7LD` zj1UBFv7QhNwL~h#1Yd}zE=R)!dzk3BX9t2)Dzt zRB8&UiX7!>h7;qI!bgbwSTfC8hgUPrdQG-6R-q&?M1>1tglTxJf5y#KbED==besxr zHYj7HE%n9rC%)be^tc?a#qoZ*2~ra90yZp$`?X4C&{M`JxIg7tJU{VU>4|UnN1BoI zP?51^z1^#2TcuG~;;FIOud#fV8{|x0W$Jc?bLa+nX2w!h0P=iP8IuANJ;-CUqyZW4 ztl=x^l~=8VJ`#F$Xb{4YFClM3o(J-^KXUEEYp+@}4_~;;?>>6xqageLXysK0uigL2 z?XT>8?C_6v;=5nne#hZ=9@yJIaaiB3Za-@4qX&NnvgePs;+tRDe8=jSHh*Hjw)@V_ z>gJ<1?%Mm+jn`}pSKqe#g1y8>Z2vp)iU42gLr@_TRPoyw%F;BUe7W z|E`O?P9Q?vN39*P<62U6W?TyRVTmmzp%qn*9Or@WPn4AtopiV*%9diFMc^E-wWG24 z!Dr{h^0e2TajKD@<#ap2tB%lBN_^T$w46Zl!dbRTH4>AU8?U2t;@bNcM1Q6Tr9s0U z#ZtBea`@Rsv!miQi*^H-Wu?K8Z?FYt(znGqakY0JF;XiQtrV?iYLS>5X^#CKiX{xw z%=9NiZbq^#p67#JG(pdaThCe$$63j82she^wlFTzkXuHyF7j+@swYcPz3&fG;poV> zbK-)y{$mTGIITJju-gPnKFIn|8g}v`ZnZj@S^?Z7exIUea-Pu2jHu2n?tE-PWI?vD zcpJ^&PBh%s4?mfG$rjAbAv0_Er^<4uG=1{zsU`_xMc)J1w7=^ zg;bqw^+&7>99kT*!X{@)VoqEeFNl|)FlJ6fzOW!(e%_clarKWEM42e{=u(ty#wW#b z+08^UhUjU-QBbeeXWE$1_=c1mH1X7;H#YxvLA?BwJ9CR0|F|GteoC$d(OVENKf~CZ zxbngU@$wUmEgI(|3u2vX&1||&_nD+o7tAm^Xw0MxGmWJ|J##tIksK=%lhRbg z*iOi%!8pZOZZx;Jt}KXJPz?K$-^P2E?V?3Z(yCrEHmQjv@UD>Wc|9$rXPaCuO3sO) zML(9cwl0F4lzBl#O`+tCg;_LP?JB*39BCzmf*J2OC`KN7)S^39e}7@IV>L00^9luZ zAT_*ZqpOfsfo*!#(p1(YudWi z>sE&|9?fEfW;ZfT^%mC{`nLs9m{dkaE}cThlVTYJ8AQTtNh#xXCFlb;?s9Tub(G8$ zBvD+<04w)(M_p=2Mm;&Os+~s42hl$oI*9>8Nl9<(Y&Yj{>C~heVeP_`bCZ|X&>hW=AR#X-PdGk>VVkSmvShr|o*+Dd+&^g+(6XWP86QqaWo^5+}r#RzT zFU{Y#JRT%KKdv5?Q)608i%oP!far^&VJ2JDtQGF?u_8Z8gOq{3N2ZesmZ>c)e*3~8 zg(cFKR4X^}LYCDF;6j^WJTKQ63Cjxt)ocYI6f;t1oy87d^K}bilGUt|FZi5+wX;(X zgh#NQc7e$ej9|}Z^@*WWQ9&&|_;5XL76t#wn`IiMv0yuxK%QN3^=bk`@S)BsPmR z+bx2xc1QsvXH4?L1~u>;B|NMx8fW#+MXR=QKBXk^On+7+w3My|W=(P%s33{q!XzyW z8ws@I6~L;pm;iQfSrDtvK&8M2s*@OvF{V^CGL+mYOw)EP=culs_(rbV0uQ{=#W>!( zZ9zoGwQ$S!8jVrJM2QykLA0f4Aj{~8V+dz_U*qh4WmF_4=HHwWq z&1)l(7E;kBc(X|MF;ki4CS}DLhSR08zL;`$KeZrgjks49Yh>8fse!DUBgyMogJC&t z;~@QMl}nT>=(L(5CX3Or|HTDSDB$@lO@rJlttxAzKr-f7yE7dJ=BQf-IZqQN=e2^O zQc5ju^4-{d*BG^iOeN|Hxo|(Ir3WS-A!*dKJ*i7u5?bRj{chQ7rAzk__ZGyFG0#A5sJMXs>-v7tm zn+LdVRrljFBhS7(62iV@VF-kW^CVfaExs*8wq;wkC0mwcS%$>1C0nv2%lirh9?%&m zw1EO$2+%@HfzmcDTiFR^3;RwXKv@a}%D$Afl<&1YPf7e{-m_olTf*;`dH>93p6)$I zSLfV&rF+ij^ipz=E{2*FOXSm;W+h3$sX!F54;*>pV+X3%2W}&gK&$?E6yyqp(0%{` zMRY6K0vlxf?PA><;!!?7j292wYUBEYhSfo@K0r$0Ot(*lH8tCbYWb*@9fFlP?g7-y z-A1C~59x0EU|n7P;6X!=>~7F8%-a+z=}6Vo!&w>~z~J)|X3P0%D;Ej3r7)n?r4MX% z=Rj4q_*NblA@W`spOhcl_bLTlRt106Wg{aNxj^cg}19s3;Ta5oklr zVim*8076=DM9SvvDB=(ERx1X3)q!poL-4^p?!4@bs$L-oxF&(pzT;fhP<3l7NqxIoDtSmrhp#5dPE2_JRL$37UVdXSQ5b?NEq+S zXum6?VYna>9Xc}py;X=1;B=&hB|~H{XplWdVC%U^rlUrzYC*~j=t$ctakU;3qIxoF znRG<8iWQQeLlpRuNDk#HxIP#Fb6sYT9sPd@FcU|@og#|08H4O61q!r{l^H?nvqQ3% z5a=E|thb6Ns~01BSMhM(WJv2V1soNY;Rn#=MqEVB;ThC1gA^}ii8)YDNkWHv&IA|r#ZSa+qNHjZtmkpz#( zN+{hd8mf__0xXS=O<-!MNjV*j!#Q7?NEgdxlJ0ZfGBd!8qSowXiMqSqA8?U^yNFcK z@leaoP=Dp9p`H#TV(2U#3o+qP@-8*h!Z8hXgeFRsl_`M4D_*KcIh4bA`!K5x*6DV% zV2l2|uY*Tb%+7nmiDrffgiajXfhnga&kNb z=Kj0T1QI%?p$^v*;Lu%`GGa9ih)7B{NYkqxX4L@}Hu!3q9F(hGykf@~Z#i8w^zI-Z zM&NGS3u6XSX4#QKgz*BgtuSz{~a{d61dw1HHJ zdbqGe#nLKI#He`Ml976rk>EySfND~?7)AS@u~m!CQ2*;uLp>eHoD?H*k>jYKc$c#3 zadNo8C~RtX|#~7!?8;1 zkO?G2aQvWW>wW~rMx-4q(MCIxkre9g|)6P(T?x>-jjJpnGL%9*>)m`Kj1`PFa zS#=~cV6&=+vFac$!6Ipo*~tYDTNZ;ZEl-WG2=YN)5*7P)X5Y(QcOK>v0Z8tP+vTLkfXN>m4dnl&$PS~<>Q zU@=P{GJymdbO^Av<*Lb6v|>aWWD)n^iWOAQT(OXhddrD=+wf^1_!sWgCj9@;TiCv2 z{ibF2;`5H{h5zn;E_?*`@3jw)xlxJ5UA+rO^PRXaCF|8JQEW-^Qldrq%_=fXGJOwM z$td22=?ix&=H+EYWkEoFc+jt^^*A_cRHni`9)Cr}Rs$K?b-G2tIk0Ph-9LL&R#d@xz*_M(BF zZ4~Q(x6rQ=5(%@Jrp2-EM)KdF&Ot^C);N(6BwImqz5$)vHGYs8=fm8{|?b1uH+M z5}u|pKJVC$8nn(^31E888nwD(C4h-+pw&a#rB>UG!U%!DFq%jFh!2L{p6R!)J28`w zc>Kr1a)vnjv7`GuF0xb%@%TKTzeCAVeReX`?KA+lN|470P|B{}0v4%g6nGttx6~L@ zVC=A!VkAF(!KB4zx)TE7HeDQ)C-j1>slg>tCc&9IiXf49c@leopQ8#tygO1E#SIV>o??O zomQzinl&DO#jJY^a53=1v89aK=G9WU(6QR!&m;a|XBv9^nc2iHR7aBwKKkE-3Fc1s zJ?`%I|2u@3dFqPk>FLgW7rKRZp*a>f(s82Cl#-Kq;C98wx)lrd88yv{VxIXO&@dO2 z(cO(Ns@UC1dvrVF0Z>czwhq8(F8F^q+rU2JNNQ$+C19f`uF%!Pv+2$lClO-w@sn?}gzZOZ8UC+q?+{*H#5cjE?+OaxaHX?)E z5ayKGi`2+MgiB{rh}M)tH9km%Yo2U1TTLdYN~43cqb;vD6PT$k2{yWQFx%{P_OBkz zHqRV@*=Ba1JGR-z^FN(!Mytyroy_!2Yi=Y888= zh+1iZf)#@Um%n1Pk2q`LMhM^N(utquL1aDX0C0L*BX2$9P$>BQJPtMUbOpM)z}f^D zjG#DZe&{mq*W*bHDK<~i83E~Jdzf9uO0}esl$v_XP6k?-w_B3bC_JcLu;X#0K!W2> zK2gs`Lm+f^kP5bn16}6rp`MWFmW`BCb|Hz_Y>^;q$&(RKWV!Cmf`h?Y+X%BD;U*f4 zffO4M<5eouV{)KT1>4V=mif`G1IPP4ecwHDyl3PW=h?;{o8zHpJKiC#bck~(pkNJ{ zsKL{_Bd7oW($;k>vI`c+N^P{JT!D@B7;97;txU5owRiLh1oUwppBRa`KwAgZhp|!J zk3(&pN`{j45}JX_M!${LQ(f(3In1K%W+Kd`34_SR$$20<;Z(GZ?{OFO=GVC$pz|H~I%uyEqfPOR^|aOaWR@7QLyKC|WD{MaVB z@yU(K`cKv~Yu{VTul{g#fAx};_R9A16PNGd`U@8h@&pn~Ut1Cvzp>a~+y-47^G|)R zwz|YUX1lZieeCjhmJx?!qRuc(yiwz_B0u$Nd$7qmdn2`Ln5CA=H}Ihwp+Kx>q1fyP z3vDc*`vamJ(-;ZqbcR_Q33M=h&jun-jYNKQW|5<1Am0QZuUfm+Zc5E4NI#|69)Fe% zg~TC9o{G8ST~$TJl<19B{P|8SN`egFy($nzj6{88W>Hg{z};A&)PO;77}Dx=gFU-D zjH1=18q9-8;dC7tas#T?4JJAu9?vxPEFjM_lK0`6<=t`1y6bZ%{ana)S4(*#Zr2-f zsp-kN2jP4`Q9Zg89~yQRW%Aamy&fN3&;!s&3j5W@#t(FYrQB_MC?C-0eA-ktnJ<&!kwZx4dJ z5VRP|Ez4%CUj<7?AeaKxT}>{^nIPFEYS0Ss6BS4s`^md!mUeDGnLbwAE%uVxYI!Kt zEw})f63AGG=oRzj)-X<0C8Uvy$Lh&aGerkzOx;sP>Wuv4T{Ek5;3sGulp&x{0?&br z$<(mK<=lYBnbykSWS}V)gONtW4m7w_zongi;9e2P0VZ35-g$W}qFba`wgzmUYDOZt zT<35{ImBdRo=7oV4@3~l9VB|FH(voEfN@su=5()B=KVMZGX4uNZdd7DHtBDc1uKs7 zJ*2@}WslS_VPnXcl(%n2nwg4skjQ|*{sP6-_X-pB7NCFrpQty7NPX{mAad+c@0eNS z9X!g+b9uQ?jnRz+2a3hJNdoCJ-9fy`Q*f@J(->Vsieye4Mm1wE4@8YE{Fa$T-O0k+ zoUAA{HVUd{$3WVMVVay70JSl2A?oSnL?wo1%Qjn6G28?eejSiDw(z&lEbUwte)ehR zE@MXZCP-X0f9Nj?@`h_}gYy=fJ`h zD(@$2mRazSLM0w2t#m2Sn%TnXR3c8-=&J1|>6+TJc$|ng(>bO|HX;}^;QZFWgOF$@S)_O4l@uVI z$IG#HP2feMVsb_~*Z^z^UQMi&5F}|Ra*1ysm;u-K9y?KQ0eZ{-M7=pg>U%jLa%|ym zo>}A_ExgO2v=<~Zr4*zS_S!x_;2Y{E>_AsX{i#yh5(i0-FA@nSz+OiNqQ(~frkO?E z$-?Kz`vMl81=7YA{>GW5o!i2v50Q7Xh^z&KOcjUCdJol9UJrHSAWtrc0IrNsz25ap zy;LgYr%SN9Cymq@S@;`fR_DON%fo==2PMxcVy2~Wxr%@l2g5F{MFV&fg*~N6787}; zGmw$d9G(Gk#uoni^S}8Xy*&qU=KdOsMTWsl!_26pS4qbs{kTMUI)j>B7!1AjGFRl> zO-9~r$~8?&z+S?}qdjl}8bfsxH~JybK=gVtVwcN`Tdo1d9T;xyi9o&aUhK6qt9STb zjNs@f(`e?61S`v7y4!6flYqCz2Ep#(1m#XB-YSSsLP{|}#jqz#24w+y&CF5{S=i>d zQGJgGBFBF9>X}8}!LMMh+0v{4Y79a+pqsF)NW6~_MiDLwb=h?LWT6uEmDN<6hO2vN zAZol9`^T9@P4C6#IM6iqI3REA$Nw<1ygT{v9A|>Sk5fR}*pF|XS=zb%c>1(&p3k`I zUUH<)$dCViW_1qyc#dzZ}_A=U<+t z0v1Ml5n%xHB5a3%YIEEY_ zlU&$I#W;TB|J6ywVTxbi*IUew`$i!whkm^U)c^~b+>npTiD6}!B)zFxDhaClbV?dK zJcY4ZQm{RGwUE}Z3;ufBrFvR>Q0!Y^p8}E<<7vJ&L=&-u(xl-W9y{qDQX#gK^XLS_ zmV*IC7Au|z8a~wzwXt}u?)Rlm!fMG*2eZ|tc(RBUywiT-8kl&vkNmLI)Ytt@H1lrWTpel+x}W1j$|D zN5&I!8=S0F^R=d0YF2CwOtA!xoPA0+Ul1CQGtcqjNRN3okcTg7bG&XHRzbX4YPCRk z)JT`#z!77yrVd6&iS^?qPZ~oxq;_e)lbU{9N*hUZjt@Js6Q4V>iWSh$liA)OQ{$s(M zY-AHb+tVOvsiU;3UVk^!r-K$=YZd%J7*Ut0V1RqJgt;H&ki@S|?2~ zVj$Wl&7t*PA8o02Hbz&pct|TywTQpd6&PQ#-Ka(VYR~lRfWA%{qRrq*w&iaY`A#@6 z)9F2SaO=$yh&)nCt^ApY-z(<%Mh4;ppwavPr3;0H6HnUt@0}-Z|JU~8w|=oT*!!@iYHj7aE7j$1FPpCaa8;mhK-$v3FO?R*x~MFC1vEeU=Y|^} z;sUJtyK|gS%=1D2{2%dexbcd)6`12w|11hzKDPpM9I?!zz-40v=IHdeAHk(_D{#z@ z;K5@B<~S+ZJ!SBdt-u_|Fz0`zz2V0Dj}@5X z%xa!640gx&*>sj7$KZ1MDt-vuqf_u%az%f69OU4S! z(dltNf_u)bz%f69i^mGgF}>~{vjP{56`11%YM%H0^S^A}aN|AZR$z`JrCAg>F}DJ9 z9OccTz|L5KIgWOZ`w?ub>f8z(^CMW9TY+PK1j}Ou=7@?o-bXO1|Hm%8XJO~jJD=Qn>CV>nZQHNd z?rw*+FW&mn*3DZ_+=?&WvUR`BuW!C?^QnuETSGRv%}Y1Fv+?GQXKY9t4_*J!`a9Nd zT*|CpyMA)Pb&OR>n3y zJ&{oC z!(t2YFc9_Gxj>?84a=s0lVz_rD-3HnYU8F!i(W9=LW)6*O;Zt-P8C&uSj)$o^&wwk zc^#BIjE1|!U|ZyXS(KO72*zqzI zn^qEmD4P<)8Bemd_=ZUfx)|uOo?$&vY9>p$emWB4`ZW)r##VVO)J)?{u&PGUBG;8& z-AN0|?s`;~8-#r|y4y+fyb!BpqC}w{>0=Q#)nwR+sm8*IKDK!Cqy^c8amC#k#ETIe z^#zkdiZe<9P;;k`#gp}HtgUgV*!LSnYT@YzEmA?g-I3FrE#YlI6Fn5nnCfjBZnj$} zgAa?QkRB%CBwr@i0+SZLw3_DOAen7AM z=V1$h)&&5HGU-lR0B4oafCD&a~(4FP)l9I1JGv|f;ovD`)AI+VZz2En1XUv23!d z*p`5Tvvxn1&@%m8P||yDJWOlxAUGmSAQ8Q{`q@c~MjDn1(ACPagvWEZ%fFl#RDOj^{Cgq&bQ)ff}&cj!zmi-6GSjEy$4p-LW(G>je-fCC_e zSAo7VX`$uw9AM;6x2iqATTi8$9M?#R(O?kkij8uy3L6Hf3FGn9n8m+6Xn{t(-FjA+ z=t8IEX$D!Zj57ITA47u$yhl}wQNoG^Ndl9X3zHUw4BZc<(>mq}Q0cl7JS`8#UvW-yYs#wC}mVD<4U;0go+p)C{Vt(f<-ko5NRiS z>-a$nitJ@oDbxjJgUTu;lX6+BM{H0H2$X2bA>}3wIO2PiCPgjYGHJneBXvT+Gk5|J z6}v-Cg=^J2QUs(%6!Q6++K+ZgZ$m@_0uBA^L5or?;Votmnw7;`mrLN`LeI^6gJ3Qw z^^^H@FIj9BJgHEcTRLdLrbApP(UT=YmObrG)N9w(UOFEs%ThO2^t(F@+L1Z4QlVYV ziHvx-OZsI}ZO~*KBvBN@sSf8W2a_-tPU2F69OU(`G*mc|c4fvbQVqNWYOQ$c1snm9 zG#$cZgL;E6;CQm-bK6n4!PSbbNKUkS>o1?kh*g?IFkX*=#fGeiy$UISvX|~`T1se? zrx}mx^$aWOolvK@@Zw1e8kEZElwsfCRJl>~Wa>FP>rKkO(6AVeqRC>_rwuI?tumCn z`s#xgDR-O^kfN;yRD+L~6VZT>Zz>F9$u%j)ghC!YUPA5EkcLVJEfToM2Kg8x#OSgm zO3gmt&Pk<|su{|5crcBs78CRlKEAg0(@BenhsW~Xph=daokqMC2oQutw+eC0SGRjQ z>7x@w)Zg@XOD6P}lNMpXxt+8Vp@vmM+!-sRrV*{}?r?68Rw=kO+0QaM5;A&XYZ0Eb z@J9x)J6Q3FAka7%tH#B87%LP*>7faTO-ejfi1lNGRJcO77VSw3ylON38f!O7c&O_K z$wf%0Z*{S3C(!hJQh>QW*2((tzNZW?zTlvR)}f6ymk`AkuCU%pH%lcwgu7f3Nz~R7 zk^mTT{V6clw3f(83p5(^6vL#*S$L+PuvRh^h5hkn&&!||Z>SoQ0wGtibhT(g%A|$c z4%dP#%hYR4!xO0ldOYDpN?t(yZ4MKP)sDy}i3DpoKezZt;}$6p&k=1|I@QIEc1S9D zrHI(hHhP)}=26ceRZWD`rS7ns>zmLwCM~*c8cgl1uNC!$J5@<#L8wd%$D^`p=JJ9k zAXW&Bb64mr41IjkqLU$c0#iUGzKB_^s+CZ$WmQy9u@hj#Z~#LATSI_W3wWGc{Q9It zI~0%x{&Z6S#S7^e0cf3zMO2pQjwc)|>vkCj3`}MiFEv`O%A`fZ=KFot&IJXwnp8*& zO$ObSR0zv}44jHb1G9{j3u`oGiqNwrEo{rIBi>;cF^4&uic6_r)x*P$bSC2QCJRg+ zkh-UhzD@Ma#ZMfxP%)LZ(zaPiS1Cd1Wayw6?_`BOLD+DvYBqyIHlrhbDQJ{RHSn0Jm%ZI$iKxKI z+@OR-(tUFAs)H7EmV&!gB=5G?7VvEZi~y>4oN>Y&O_yEH_)=@KG)%_xR|-TfmcB0$hzxP#b(aIV%9Ql?3R ziY=JX8q{TPoaX~YVVIK!eYK3ji<9LeRf_exQWAwJPs5A$(Xt;d7=B%&%PA&P#-bf6 zN7qb#7-=wz@0`d`*nUa%!|<@H4Wd3XH$+q_5iP1%2-G=pqXVmnwA%wi47L_uJ86+? zBnP}c02NHCaV*QQa$M5~#-M@5N*zzWie#h1PMtNF60>&jIC6NurUX-VI9zOHdkosv z>>lecBr&5bBY>PF#}_8hIiOn`PQU`=q@!gUe%if%YOyxa^iv!EFmA!6^kO@&qu#dKO2^W= zkSWWYTor3VJR)?Wbi>m8?hsx<8uI2FCo=ecFKh)PQqP;Mg7S?>q^5cMAmdDFxP7In z#=!|R%d1#iuDRHQ7NQ~w8h8z2D65qz#iY15gXms2sMSqUZ6z1!hWqg#nP=e5^>K@I zw&ln16lb?U`j{uv(1L){wppPPRk2qOn`KkRC3m)rBz6Dd@}vcCh6}BDkF@BL*77%` zDwy_rN?s@rn|ah|*D`3n;BSage{bpF&435hH+=-iHWlkwuHU9LhN#+h3>0h&*LWT9 zBW6p6(QI}6vg>aqGFVDNNf?cGDHgLbIL1c3eo%|2Y@1?7tM&AJkAw4xBu8)?6E{yY zgRo}9Zhtona|L%V9w`bfU&zOzJfZ~%DvJ6A6p0lJNDKPj==|Tk@VbQ)uRM|7`Q^@E z?J(Ov+5YqG=++OmUa%G1{PyPaHy^$6^^Iq55bIxAf95*2_Jy^*wNtB~Sv|e_2=HeA zgq4Rbe|WjKe3|QguBPh&(7T{2bnm6NFX>AcExu_{S=<6$AODZAUvw>TCFl#62NW|` z);X=I3@VIHk>qrh=3OrpGP>%JaL3CWh(c@##d3qmmT95q(eniO22ZqD#P10S)t<0l z7m8M-AzsSP$qhDh*)mf;N;Ef;N&{trr6c*VjnMBueJs5Fa^JxtiYe&%ik#FMTw z2{%#m1)Fg;6egNlh7th`*Ex3|sEZItc; zii}RN6QVpdkP#VZrfm;p=sF^`cqLNp7m~$Pp_jYLpyp~Et6i|SHlL*eNCULRh^9%cr8+ZGe8M^JmS{+AvLM>iLU>T;mc6vxy zmeMF1tq_4)g>te(NPNAkf=Bk)aku|6L$|SE#jrs&uwp!&kJoG!WFX@RUX*EHS*(IO z$DCS`Kv`W-&=)+i+_>9M&(LirAtgBv&+vG|!w%JOghWl9$W^1pFdr665~v*xHdJaR zt(n5UJnr^W=jS%qE!2^WW$G&324yI!XnRoTHOu9E2h?6ENfE3@ zZa+Cgw|uDI$mN85-p&Xu7-{8G0Y-KE25cuKn}ArLRBxr4jcz5`p@n^E-0df3=r)&4 zRoQUAu9M&+Lk9Pz3VJf#DS+}qeYlpfJAi^K)K*Ql&nUuvX58(^XXsW-=fQ`RkSFBH zS)D_{XLVG3Yu#c{VYP>{^SG$QN^<8Ei58JUMwMA+xY z-OfNMG7ou(u%8}xI|FmbJTxJ~J~!@m28xh**gk~))VSLjs6OUl_Yn4z<8D82erAMS zUnSoUDUuEI0i{zYbp1>^9l<~eZLCFt!mlBW!Sq(aB$$vU?6c!;XJ8DOhZRKFXU5&m zKnb!t2lI!ppBQ&L1MSD|94sHgetg{R3=|)`bC7um`>}DiGcbAV&OzHD?9=0JXQ1qu zhkZlXkB+;YfqG+i4z>+pKQiui2BwYOIS4g`{qVTk85lC=;ocDTsd2Y6uwcv+QY!3+ z#@)`qc(FT2n53{D9CtedwZ%LUmBM~t-0ch$74w8C3j5@^+Zh-scIOCG6!xzfcRK@< z#5^1o!v15%-Oj)ku{%dVnXv!pakn!tL+s8GBqi+s?zr0-C?MvErxNymXWZ=!R1Wh5 zDGB@kXWZ>Soj+7&o?I(o|54*^XCQ=_Cw)ZNC&t~rZU$}V$h8pm{o`(D;D^|qBTz`# z_l>)qfdFEjV0~fVJMMM{l848~Vs>%y6Yi>l%S^_qJW z+6kN#VKD_b;f8Q^DAuF(U>Z}sr+ldvPlFQdf*8}$Or{|Xz`+#n@x&9Hbg~eyoichz zd|$H{JWI$0`QTwb=l`wrxj8~Y5A(SL z7N_4(lH?ibqnaeM7V)_Djzj_@LgKT1@J>mSQ`w22!3Dc0LL5me5bcQmbP*QXO^+ua zab<-+C3m`!ZdCRRdv(GaX?i-vb~!Gw)CH$d;=@kG(3O+JVO-`rotBU)MT$gB&PBn| zMJ!R$ilKhLP#KbeLM0a$DlLy*XNrZIMl+#Iy6LqCsXEQ3f<8Q|d;7kVJlbxNv(XA* z;MPYj>e@%gE_$EEdA4K6=AypeN*A4Dhk2Nb9+F86jQBUeC~$}(Ah8z|at9SK?v%>x zJ-=6uCY%fPLl2COBpkX_f^%WTTO<>?2+Vqi^;Fm&vx}Mt3z@W?$%pZk^!`@* z;T&<}hxy^{Jb=HU)zVSm4zwP3TntB*D9*DO{!iQvMZHHd2&hAfY2hg>B&P~xQu29H zm7!0SdR7qyi~?yhgJh{Rh%t!`%nNoq3TARVS0U34Iw@AdY?zXWtd^$Hb~?~#gK%4F z7(f-dabqWf1@*8X_$x;1sxzw_vYTQ|SHb-y)qsIPZ-#uZtp188*N~}EEb^o0@^o_-5 zZ$5SNQt0(d+3nk)r;YLp7JdPx&*T`mzO)?Ldg9_E7ytQ$f9WktzrTLn&L`I&dE&!M zS1wyP{g8AgGs?>EeOZCDjC1%PAq7+VpuovFX+pWz> zvk0Kyk^3U3S%QI2yY2g z(@g|K$(FlFyv5aAlUO3guDGRQ4}=l*-5?Y%XsPr7OpRr)pGfAaVjV<>WZg_BVCTrS z&rGxmoiwqme5zqYe32AGiQS4JoUBm(lP613-`^HBTGN_E6BpsN`;J>M-eeQS{9(!} z3c*U%L@Id@RB7}pVjw^4hrJmp-;4K$@h~p0+&a0BoS10#+&W$ABk-?6QrA+ibKfY6){?cOs*vQ+S{hY{{h8S4?QhVqH^dE!Apw1|V>>fi`NvYPXTY z33zStJXn@ZRE)Y?ZS-;;MG=ysPP(~a&`1>0c_}{><+3NF*g2T?yC(S+EQmo!b+H6h z17W+N0mp`40=4?E>IP*n;IzgK^-3j1 z2f|b-M`_Wjn;{ED*eBJXNx(9bqHDpV0JxaVj*&?v=?=mVsai$@fz_bK9mQcWDF%m> zh(fJRf>)V-z>f~f1d8bCLBY#}V{K9o7s*IIYw&f7(}#_$0+KB3sItC2(W)sRWxH(= zfd)c!D+Ma6#bPLxG^1G=j)k)6j4YU1n2f^M(rpK$8)p#M>PI|w$l&r~xf=4N{CT>W za{I7!B3`QdN*N??4{9WooV2hku2EK*FrVV7oMz@2mNlhtv>#?!sSq0mXq^#J&1(zT z;{7KrW~bJMt{(^IF%?GQK=OkxLk@hAo*HQq4Ks};1Vyej0=Y)SQ^2sCtwdM_nKaOw z@dRBdOME5Xq69x6@FgOBz|Y^R1Or?tt{SvWWO}jdK)(3st z#}SmT5{qQC*2*OQFQI6utfwW^XhX&ueiqg^Bg_S-*O)k+v=QOy7- z{>bQ!f)%B+MGxIml0rkvsii)s&e2*;OzuO9RR@|h81hO%;gV6o5JAxZHk%A(YJ&z` zt>s!FEWo$DdTTX0xsOuL9Yrfdx7F;kX%dh7XpWQzL6Wq3?Tl(tL_ExR=~O+4txW=) z8K%`qA@ZQ)8+5T4r(}FI9vd{f`8b>+v&jJ^#6pOM=29NN>*|RNBa!n2^IoB)TZrOq zgCO=sqvkP(W+$9&M?mnh7H9l;F~#>>51+JXHf>EQRN8?$9jC+Hp%wIG3{a@F7ibK0 zDX3B;q68REQleZBIk=AwlJ8Whn2C|hfUcO?fko%Y@UY>pdVQ#xsQW>TF{+~qvvx3B zrF?if8;5JHD(o%DT2GgU%?O@q7`&1#c~Fxm0*=3gSq+oW1IIELMz-=a2o%awTA`Iv z27x$IE2|mGpBLOQt%g~_Dr)z_Y#v^E@T5i1k0q5*MK5brUxD!}X%kbjOh(3{m>H^| z@nK#P=}Opc&KOSO3vFTn1HklfZq+1}^~0O`_V@ z6M<0Lig_5@43um=&r#6^PAHjZ&%ZL+Dkb{aR-g&`%ja80(4AuaidNEjPq(eq2bLX; zW=k5H$~CDTy*}B9CI)7_MG#pG#DVw2KB7(Hy-=qdu_9VUO{R6XKNJ>gkzP!t*83B! z5?PSWLQ=kHH(bdV%OEkq?lwuL7A*%@C7Nx45;(oEzf{b?8<&n-%u!$ovc@|G)}uE9 zlnrX3CBg^;8)(;4guxo^$+Uy9vZZG7rI?4RC@d;Z_MxmNWz?#*ZZZ?)BaK=JPM12( zT9jeBF_k6qv_FvN!cnkkH_e5KS+Q=tp;1EHlWD%*v34ecBv3afNC11nS`KH_ zh;12h4VpySGXq($0#Y}@%eD_@^F27E1*2*#3xinqa!m}ScsiZRH3D(}!hcPMs*{#H zAio%^`}BOVIViAtG*Hcg*8xS24K-2f^)(ss_GFe>oU9&9GoPrEAvIw{K}jD598-im zMZ)e-Mxa0Jcs+KqMn~Nsze?-5)+RCg+EC4uw}#8>Qvm|Vs$E_34+i56bb z%9@|YL9OMe<{sd+P$Q!-R4|}$HX5tb8M(>Xnq*O~Ng^X-##4N;mW~)XIMa>BYc)xr zjIN(gfseZ&GdYc82FBX)VTj)N?nEoxY}DYBctizcnk5#kcmbWWIZV@7ItYp_HR&Yo z57I=Ah%;;8-Eje(ZRLfaA`Nrtz2L6ryz^21|Hq+MFP!+niLada#EEyEc-@J=KJh0fo_=Bga_=8| zB5~q(Ph5TC(i8VMvAFZYoqq!y0q@><{m#pG{&eRVJHs7w=W#m>U=BF7bJ@;CJ4@R? z+WzYHr?%g-{ZHT>|IfC6fBW&<*7mjA?Dk`};qA+}FW!c>e!TUytxs>gck2yXuLSu8 z&)j;##;Z17xbX)YH*C~4uHWD`0vn!<-Hm%~tgQcZ{Tu6_U4Q@jo7ew#{YC44xc;Q| z`g&nKy&hc0)*rfl@AcKSpRIj!?Y6ZKti5IJ@77+tcH`QU*BWcZHGVC$hOb?@cAvGi z)t|5a$Li-+Ke+nV)xTf;^VMgAoQ3A9vMQ`ntKQXzt=@NaedQM`-&*wFm5t?J0?xxPfE!q&ex}N6hxXLcc6?GA=N4Or~+Jb%seHZ#7^ik*? z(5ttqTi0zRx31YjwyxN^=ay^pKR5q<^ItY^-F)Nb-)!Er`K-++ZrYpq&D18jiEch* z^ODVFz{B|Ujn8bnZ{tnSUqa7=eh=zG8kB`-=uyx~=z-Ao(tj^~Z|O@*A6xq8rPnO| z)zb5q_Lh1e|3Y4hE&ZRRM=m{RX=m}*i{HQ99~S@l_TTUR?YD0Pc3jI#ON-S%xFz^2 z*F9YKaG?L)t`n{k4)i?Kwd2}xp!+ATZP&H~uj;tATw4y@g;ye6wrCwc^0VWf%B&2EP6Km$<->1HXBT3vxjY{Mv1< zCD)Pzzw|QKqHEECpZB>ITni5T^nIaUL%(+5hyNP_{~Y+f`vI@}l>^@ufc^{mF9*Jr zhkgnD(t)qP1o{Q^3kSX;1pOTPxdU%{F7z|#XAb<6FGD|te(J#Id=vT!^b-d@>$T8- zLjUQ&r+**%G4x{x?p+7{2>Ou&pZZSdhtLllc>49w51=17@QGiAz7Kuhft{~G--EvA zz~%$Nr1D({+8=_x1AWJV)ivnb(6=3Ed>#50^eqQ!&w%~|`VR*duY$e_eba&YhX8ul zZ#XdbQ|Rl^*BzMoG$3pFcL(wX=xfl|92kEs^i}Aq4vgr~zd`@zz|c>juRvdMAo+9X z%g~n{c#RAC67(ep{?31ZIrEDS^!zvUuh73b5P1#s1?US7Jo$9!^U&vShx^dypwBt* z%8x;}LAN<@_chRGq0c(-@&)KK&}SU@U>o`u=wBT8fH?GN=+h3o*NdP}L7#HqMf=bv zp-(#S#5bW&K%a2n_T|vWp^rOo?fcNjppQ9lWe+R~A9Wzm_#@Cq9JqK2`Y`n2Gw^M{ z+JQafgk+=Sn=NB zzz^LD-2&a>zzLgBgx&(Z#er{p4D@E`%?^D1 z2cb7XZ*t&kUkbeudZPnheKGU~=nW2h)h_f;&_6lw6%U49553-jFZ&VnI_Px{{L3c; zVxreN@WnrbUIV?xfj9jOdNuTF2ma|3p?`$_(Sd*Lh5iBh2M0d)MbORA%?^C_Pocku z{@#Jl*oFQM`a1`n&Om<){jCFUxG(f7=v59JTnzmU^fwOdz65$D^hyV|ehs|>dW8cU z9_Z!J%N=N+hW;A*YX=$+hh7G~%z;G|`YY(K9C+R9z@FnT9r(B)^it@h4wSwKy##uR z14SG93+OK#m<~dJ4*j_U6D8=y(2E^Nm!TIyFLGe;70?Tz7dnu9G;|YmlLH^!gkAu> zz=6b9pg)8D>#j>(mpbs}zji&?^prgg zI8c9#>)x(=JFsx8>t3#VIWV)}y2N#f1G$3hp00a3F!4**#jcATNI%4Nk?W!}@NM2F zLw^eWsRK_%p+ABC#DS0eI`qfTA3N}>heOYYp6|db{~BzHpXb0`9C|MFTnAopALx&u zKXTxuEc6`cIS#x}4th59YzJQS73fCjMhBj_7xahFA3AX3$!A1HT@Go(etHfnWVL^c3hR4*aqRorX?3@V19QPllfCz<>D~^d#s> z4*Zl3-2mO-z>j|x+|v^s_z^es1n3D4eE&0`$3u^I;CqVD5E?r0T@Qc;(7=JWdA;tnPy=c> z@FhQm>QLQ*FTN70K{W^7^lr$8YzO|C3*@d>9r(w)kOf%|eC~4~6EYq6%w@=c33y=aS4peraB2;u> z-hc{F!GU52x*odTfjkf8p}YfA?}e^|u5%!J8FVdl?I`~LK@hwExb`-8Ub2I2e{fsg z`sUW}ZT@)k_ctH3@w$z``p4I8koDhM+XRe$zLmdT`JLrYF00E6u9|BZxGlVN%M!Er z&Bg4(cNb32_%^lqDA&ceU%GVj?Z4ms+ub8@mmY!A-l>%mD7U&WPkQV5Glmc9<2vC` zEo1L$Acp{Q=E!%QSgn59lIkI497Iw#qtf#m49tlJ}?Am+HlNmd+sB@KL zMPYcV?DlB13wtL=`hZ)XBc*m`eJ1r-t9y?CG9CkD%#mMv{<0oN)>JO-T?OQgY;=yI zD>KWx-A1*&hXXkyvza4hc4j$KX7jMoV*+L~M+uLaMV-rR&aO86_vz}SbS&Q%kfUuEU94o{4!8lI2 z#r=V(ky|v#fKn;JMav8_O)+AbnLs+=)=H*?aBd@0AVDyb4=n_(COz|go!c#@Yobrw z;(jB2z^xxEyZe+|+!x3gxdmqFj3$VHwYw8%tL|h2SMx@#Z{zW*uiRyZdbW?H5S;8( zdzUj(Fu_YwM3of2(q%IVdTI}G4!5`ukT-G*rZ#h&=FIYraEp5Zkt4V8^WJm-Nw*`K zRkC7q(gVBoGE%KYjgZ1gVGZe)L=g=VeAhp-$UC~lB|y%|En3M)q1vcmDW%;FwmfC5 z=RuZgA_$Uv|iKT#7R#FtMvqe9X zU_`}q50gGjnfboX?H2fyT}|BLqLDt}*5wuzcQ>U(3b5s7s|Y?A0!GJXzk?ZpV%i_8 zRkV6EqPwM*&dsdPWRtPIwX$%T>mCayF5S6#=P5fEZ+~sO1K9sQv-PU2M{Ry)Qv;{Z zzqfJ8`d_Z2YwtL5<63Ivm^uU4M5a^3QGz?nF^e3|QJ*J;;1wzk06-m911x|CS_ z+@i7Y^M$8x^`Y0#{wlY%0zG~9tM9(@?h&|q1pZ%)fPMPj%L~_@K5n=O0zmuo$1u{8&*LXZrGvL{Oxy<9SPhWCQ^`>_hN2xb`-BIfK zPM*H!L_PE@^``%FnFqB`UwlsWywjZ=F)Gv79i^W4LJrTd#3+#nFp~?-{YL> zO)o-6jo$QiN2!OOJbhxK-t?!JYdq6`xy%FGr+3b&9yWcCM~vR|bw{a(*{8S9Dex?l zJVM~~bw>#V=DT%Hfzu{+)B~8l?kIu4AUDq`aN0FbUX zh})-^&na-)zm5_(ece$4fgifgDRA1qjuJS1-BAL8A42C8IPG6Y37o#}D1pEam(D40 z+P{txIDOqw0+Exa7soR%GHqtpob{K>5Fgk{VDs)K?9&V9yrpRmKI)dHuRH3N;FC|9 z+|l&ncnvcBm&*{ZeZ#qw^Al6fcZ70hU3Y|Xe&Xa4C(2Db*)_=YUoJy%`w8b%ZpJCg zKYiU%%K1+|exls8LtcYS|K&0SvxnzY?ksycbWKG3)7KrPobTjdqTICeUV}{k_bzSf{T$N;$9HJEy$qXM2>q>FbV?2d3Wja|tA7wA-_;J3=5qjN<>7*H;&I zZrb*4er&@6zux_G_XylQ0(XzV-6L@K2;4macaOmTA4cFQ%F4A*Q!ZURVgq;98wL|1 z`!d?^%4iraNCc>1I{v*?h!8_iSr$u%$X?JOdyK%=bCFC(jab!!lo`;GwpHS4Jtjo; zWYjY0h-wupBteI$IOuaISHbnc0HUCoL3Z^2A;3Hm33rMp)@BT{pA;z2HdbZ?tq&(|a*1*P(P{|lyBY@nkiz)}d3u+8ml|Z@mYu;?y^YrZXe|9 z&o{`^Z+v&DK@J_$AV<}DF^hYXQQ0etF`E%;9yTN&Hpp(k?G$Ra>s1&vve8O4q{i90 z;R6Mcyipa2S(FvF`l)u5#<<%DIe)%Eo_=$^D-AL;Jf=YoTj3;+%cZboD}#PY&4z=F zG@iz$EIHUsM7`m7vDfuAEYb>=(-dy-9W_^PXF3fxn8~tsFj{DGp1~cai|fue$p4qU zcY$_g&Fcd9dGGV){nVy+v%8z-cKW0$l}EJ-R8mz*C6!bvsU%e;O{n)fRjFrEP4~5V zoPY|A3XGyCqNAXQy1-csIt}Bq9UWzspopWZ#|NSU(;|yu(9!vl?0e3+Ip_9H-9Fvm zTI{=4*1l)w`~QF6W+V-Q$-1O@taUZ8&8F$l~(f&%?| z*$9k1f&%?IFHmgom@&}z2ntX=FHo287zElLK>@1g1xgqmgFxLQC_weRK()hT5GZ>D z1&SS>7pP!(d4<7OAIp0L1*o1ED0X-Z0%?z+0M+vXbrFw2@U?pc1*jG&p?C~}`5uAo zXpuTzR)N1DAz1@S4p%KHMw(`$IT5->e+DH<&M%;5f)uH?cnpHC*(2~kY1pX(t%k~? z(%=U;KFNm-zgz01stQkrlUmnm%#&^h!O za(ls-2RH^HRFQIz#~}FKdjw>@;B>^+rkK-nof_X#K~l=43KDRd&51{2bZLh7G^0nZ zup*Tqk3k?_jKT4RA&XRdygVT3)yKjf!F0R?HiR-&0m->qNNf`$njx{tAR0_o3mck< zZIhC?8eN$oMG8Y6gMi;7C{S+lUH%Znn zpg^SG%MI0g^)a_cAZHB7VwcYXw=5|ff^`!#ljnMr_XuwWEyZ3pV6<>ZH7Vrf2Isx{ zc)UkYARMnq4a>{p&R%^y+9Mb+jZo*@9z4#V<-pY|v!zqE^S(pWR!%#dTD3JjRkcVW z*q0m3_v&MIkDx$^;L8p3d-d^fkDx&C;LDBvd-XB1M^GT9@a4w)z4~~tM^GTbuSgBe z%R||=@&BchzjE^M2Of?d{O1RM`a%ExuipO?AOhe&g8Y1~yT5SvJMVr4$Upb(chKAa z^7ik$jetx5k53=n`WcW*@5^ugqnlrM^X(uvUIwB7{_geg`WJ)bK;E^_yZYlG|J-L@ z`LQd?m3x5n%m3jv)r*3VB*i3Y9`sG07$u9TAYccMJ?BCnazQ@i`GD_-fw`==y6Y9! z6Q)dD&AX}Bp`#g$lTdkC1GT=9i-M5(pc$-#E$y_wS`z!)ok4|ZAzTA znMr?Yk?m;M6!o0b+>9c|?YNKhBob78uUr&_yqv5&;h_o&Rav+lWLlTwg$_0Z1~PdW zbOr;iRPj+Pn%4WlJmNO+MM22R$u7&XUQUNOTJ@S8b{dI@IO}kN%b>}!p z+^m$)9EB- zp}&+ZN$1*g+SkBKM(o<$VBBWCjypE`ljV>Ni5bRi-m@e7Lr0JuH@(KZZY8X{gu?lp z)oVszfSjwPT8U&Wel_k1m1(79VM>GBly+qQ!VzS<8V(HD^9aPEHAtH+6Iq)b452d8 z5$#y6(5^JGFskbHc`#+~-jV&mGft8GK+yYsmn0Nh80Fo=bqlxJ^(dWCqwZMGc?iPh z=y)yBGeFu@%^bq)v|VLpLWU~b=9L}UKYs+-FmB|_(M(d3F<&EcW#NfjC+s)7P*`qR z*t9cHt+y6|#{feIDA(+c!aVrl(hWSGfVj;7*zxNxRq z-2hVdt{vGQID%{s?=l@{>WEOgx^QNl)UaWvN6bo5XBJ_e?C33wRt0{6P~7Irc4WW* z2(qo7xT<)oCP_7`(khCULw<-1M*4vE)=hR4EUKj{%vrQqBe=~wcVz$U5oDo$z%Gr3 zg?d#iL;4!ov>I`WwBtd2K+-cpYd|%)7Sqj`owk^M858%fSXs^1#V^Gc~Qlz`^s z5&;{dYBlbAW5p9~tWJnwts!Ru3l`YhcVz#^BgobW+)mXTE%kKPYt@#VtS|e#PH|bq zoTDHl8I1-|qk?lD5Z?10**|>**#c%wZu6x(vVZCbvIT6F+~#dNvfpDNJlv7}@DXGSBrxJYw!y2ccJ13PH&qnLS;TGb@5p}Z5o8NkKDf=j9ogT1 z1la=S3~qCGNA~v}LAHR6g4^8Lk^Q|#kS$<%;5N5+WWVJIvITMyZSVirul!FZ_dnwEe+MG~ilu(lYsFFvtoX~9&imgM`;NAlTPi5X3B%=WIoUYWByaAAsuo%c z1N=K$gI-iDwSbIlYjzf~r0u?6Kv;IQPIq-fwNzWg5di(W-&VwYS*%!Uw{5!EM0eMN z6%n@EqL>_9ySMlwZ2?AbTj%r*oJ^k!+S#^J{6!3U0Ghr3ZBH_=Y>5Pk>{`m}Y7}ov znSSburBp#qD;(H1<}0^i_hIEqk;U}V#IF1zu`8hLUyc|R zmBV-)XxkP0f=pCfK+jhF{xk_v5^3R9C%pf7QGD-PK^}~4W#V1ixvJ?>;%>!qt0JE$ znfJfe$%W)?`&`}r^+o;Ok$d{L7nUIh44;1EAS==VZ7G#vX{%eRJB)ig2Oy(ZinV9{ zZHG;k7O#FJLwM`j%!;0ujda$7C zph{e&j)t#rT&LZUh^iqf^=Xnk0` zwA0>lVClTSSc+3uMum=yBACKswMgpmHHB1gSaBUbHF~M8H8T=fGqpxnwXA$t!G_fc z%ii{Z-5OeK5rGQx+a)L&+I+V|K~<1q1%aSy6?A=D5k1*_#cq>dUwOIjUdL_nRrHh9 zHt&0Fg*=fDun8AYS6c!H+GC$Z>o-Vm?3qWV6@Z!BZO2<6sucyUw7wAex4k$X8b!BX z-OK4b30**7k+|-5HM}NJF!=ovfhuxx@I%Y6w$lAJ*HZ^#}k4BR(go+&)VyR78TljMFlGz@Od+W zXAj^^j;m&c7aM6S;p;)Gy%-QG#?Zb2uOVm2jjdp6vX96qhpb<5O)Ey#N0a_+Y4;M* zi+ns(-PSCswJkK$a;~Nfn(R@6x9vI*yl*l$ziTUa-!i{aWVybqZNA!gUhrNZN-q$O zeM-0AAmP~U<6t>~s`R^6hiq}|5c$PUz0b-7bh%7CUC+hWdZpIba(Lld&yIe53XWY{>c^pL^Kd{9DhPS|@>CssB z>NR@&bz2?~STe`IpkDs2b+I@v`U@ZCc8MOp|AK3Q2i8xTtMNDoT(`+r3%tJ`&GmJH z_;?LCZ3VHwlkCx)o)<*#@#=zKfw$UEf}i#{1N^pvU*HkWY5#`4;(qyiq>^;saA z;c`>O8}3TpW95QhffxKwf}i$S2K=`2@KPT-FZ%aX9*oBl;Imz#1>P}_?$^N*{aV0k zyF?2-r5w%b6D`r_iy7_j|KZ8Q?|As-4}SCkdH-Yg*?a%yUV87|-S58p_B%g*r+oWw z-kzNPhtu`x^;>`N*1K_{EL(DnCri_ zd35tC7-;R<5&p^-)Aa03;kj)?2H(c^XYy-xo1UNnd9FOpCer^#K zu?kq5cVCR-5iZXcoSt2V=LIYSAG*7F1u%Q(`?PCEI6_}A`$P-z`Kx;o>w&WQ@+}%L z$Vd23U!d7*%OZAxWb>{q7f8Eyggf=?`~2MXy@;(K*?idr-y{63FXLOlT3~J73HWXW z^LW0m6U+iO0&DY*3tmS!e_!la*Juv`n+D8kKK0LOQ| zOS^W2NBafG7v7bM7%KeDZ@=i~5&rIP+|2@Z3S;xz0LQJ29N`Lo!SNXxDPW;+H(v}m zp@7p7-tiZl4vYvz3>DVqivYW=;y%J_{({}>M6!UL0*K@n0%lv0Ji?d$g4rh)$wNaz z5nG0``GPGP(Dfr+@LwBg;LjLqMXVIz=JNrUt$-Zik$=JE#R5{mY~gP{4=~;8fg}9& zFPOeS4-~Lk7@N-p9Jc~;ge(6A$7ckjfW5-qd=B8W6_6vm`(K;3tz(4+JQ?P@p)Hb_ z!PJBfS1E*u73b3Ro?GfP5BUwiS>gVgW9gePRJQG%FObY$%)0+@b+pKO%(S zwUOQnNCBILzxfQnWxH07h$y(=@`ANmz=Gj#9$s)fBG}-KITo;C7@G%x<5oJ3h(oyG z_>6QEuwGc3`+(DSxgHUkaKY(y%e8<7V|)Mq{F8rr@^Jd#hwlH^`-6Kwc<=P?*WCHb zcW&PnPJiU|;Vtjx-@W-IH^S@x+x0KHmR$YGt8cyXe(?3*{68Oh%VzrUFpag-gjE$ZRlcH_s6%(ABwJc-}>WU+}nMA&x@V3;PT;j?(+!%ll;pLX}J6m|4`#rnQ?|UPVhnw+--o4?rAiwe{$lFEUzb-&NFYK4& zy}d>HnVa3`zdsu9*=Bq&sHa6Ts}&v8(=EJ*|8DpBD+NG~4nW>2a(2D|d2z(Jd?ase z;oU#meg5pxAm6=V4`%*mk^F3*#LVB^B0c^1_VX@wG~V;gaEtfR)0KLo$g|=F-lr=2 z@^QVfMR@b^_Vbo|G|qQzm@Q89DbDLfPWu-)5AF+>gT20mdF`KVKd=AX(O}=P8EnB; zo`StrFX`o9(qTRC>6MVu+Y3{oRfl*ua`H^4#)$Qkh65a^p;rSPAoZSPEw~9OnUx0i~ zeO?N3t3E&S@sDgj|KkUb2Knv{wFUXi#(A^I6Y>Sf7i^rD;@zsxkNjeH`}s?M?P$E` zo6Z*R>(pm~E8PX&XVmAVIJfHaBmd#I?>^sfgMV)F-?eFPalTG{7Ks18zQlwk;`H_F~9ox^J`?E(Qed~tUB7L3u zEbxMUf%HOsUW#$2KL6Epi}7Fm@X;9GvcV74XMs!VYcs!BpO-@1V*A)v?>>)5`0h<> zJBqIp`U1Dw3y3ce`b#6@c0&J8KWB^gpL#DI(dS=(!RfwvfcHj`OoyKY-ktFOqn(oa z>GvFs_k4qG@jj!Zt{3qOxWN02lDc$UcXI!e|8YC6|HqFVjq@FwuiD~#o!l3=m0#d| zo!noFbSL-!`$xA(|6zD6(mPkDC+#a=ee&=hJp5x1ormbdTMvHv!Jm4teDIa0@PqqT z&+q@UE5CgI2kvM0iTj^@@Bg~>5AOZNYr}gVxYxV)g?In$-M?~ra`zkVvUk7a&M#hj z%bmY^=UeW~?!5i>f4cqGZ-4vkuetIQx8HsG>!%+*{jO8>)*riN-)h{tdGn`ke&5aD z=2zUjcjc>Z{OpbIzmeX+Z+zDEk6rn;>pyh;>#ldNf5Ek%yY?g3zW(YjT>Y`D-+Xm? z<;zcg?c{G?ZfpRZ_^!*p_38cwbWOW@E|ySsF4S6i0O=U0kpn8|@crDf!x9y*`lV?H zW2iV~7{tKt{ph|0+*j(zg06b}amCwVLTJ z(*}j|Ap=80x=)rCa&xw<;te9vhB&$$W6q6t?pt8}L1dtGw`(b}*}!-tM@YD$>Eb9$ zMqDXZ`NeqXRaIBG_4h7Xz=<(nX4rD%+hV&vVZ=oThb(zv>J8E$?4H6-J8iB{Os?1W zEgDz}_NswC@61Qhbc}XTcU9{lnXC5dQFW>fVKbR=kQO6Xlzod@XSNz6)S`*4YNHxE zC25wJ^;^{(g$H>L>5&~4wHDw~K-`HgTHv~3jzpKiyR6`nmM9L@lv!&=cV5#cYb9YC zYMYKkgT3>s`xZ#Ko^kWFOI8eDn!1v-4lsViLQAEsFUPJZvU$MPs4m{Pw%)gZ498fu z1KjJPCAuOevQZwdy~flUtow^eYvjo7>ZIqG9C16iXrVTIS*TQ&_*4^|2|jhp;T&sZ zQ?1n{nsHS|%x2P(dMa|gebIu7N?rhJYL(<{SoYHZ)AOD{YV*YIHqacJLt}!)rZvX7 z{jq(Ea!zDDum)5m5_nz}Gu+B8uis@#Xp=BRhM#*lrL)Vna^pKLS_FJ1wRMOfz+wq$ zXHi?##ZejZTnX3WAx05PqdUdJ+`svuixzN$>ZySakGO^7tMmdJl0#it>pmFMa5!BI zI{8vBb4GUk5ARwCN!CCX?$FjyJ|FaAYT>sHl1^26Y?Ae$Q(yVY~}iX zl?X1=W~nr(QslCu#Eq%w8{}3CuaZSIm+~2>JXzF14ootwk1e#(5W_Cv z$&Hn&LhFd4?);%$3*ILneLhMmv(RYf6^@sxl}V>k=BTw=8?Z}$)k3ClZ8%c>JMx}~ zWt7>VnOSI68IjGlHlMF^rRA-%x|c2I9z*t9HJDcw61#JI-$F~0TBl9BQO$9qu-0v@ zhpKN5hNW0(DK0+aY%8Z2UK$~H@O=xJF~$%x5Gw&gSasI*s>oFDPb-7@5P?AGWD<2G z-KSH>xpQmZLW&x*F!LrQMHs>rwNL4&CR$3xmcoU`mum}Zgg7m4m?2kx-@e7XJ2ZIG zH%4OzlE_(u=@>{;9|(S(ftT%)M@Hg2?{~Nkb@I>mEqJ@4&-g{pu~@fdxO#n{WsgkR zV0tiX3c;kYWM^KbXA=JH&)v6}Mo9;WqiPGCLypiv7`;xJLqTiK*1}?uhVzt68*Y?_ z*vU7PuW47#1+FxO(keRl+X_2rEj^B#wH+SHjY_8*2$eae3mRTiM`mR0SqycfMd$Si zT%O0A+(qp`=(2gEht5fv3B*C>;i(i>nl0+;Ke=cD53%8zQ+T;DqTqN$yW>s}FKtH- zk~Pl_#5IA2NxNM4ufBiZf|-nZ9y70KQ(9?4HMEqlGp-sNb3XG!VH)9?V{tNF#f1uYb(_;E>xOAU&Rfkq1@bWQ9^emiUy*!>MSpr zX(gx(bedKYqCW93*&c}VN^3k^E@f6Z_4YivqFj!RR$20l7QZB0RbHXaaY#D_^PFAtBl-n3RDw*mGB%R2_uFb~W zfG}=-V9%o&n(I*z`ACpW%OeL=f2e5$&LJX-^A$y#yGG2Kfji)^s~_FBKu0|oH9-Z! zp&E}CAde4eY70~&N*T5waLWk0D94HdIDp*v$i77*p+Tv{aa#60e4X(FaTOaCGYfsX zCw0xJ?~N8{qAsed$@OpAx2RV~i#0}dj8d=VVJ&3Aq)ZquP%|h^2oUHG4@y;qS#Z>?eYzA*MiThT*)Rm)v7rL z>q)D*pY*L69){RL&{tv5Fx!4SEZ6GJ$$lmB>vXgrjd-XI8NQp(x|CXuKHKLanB>|+F<{wj-o*}Cj449KvQ&zPPy?|8=wx|cICH|e-p2xBVZUYLbiG5V=)aTs^A=qm?kn6~-)z*8024h@*KrBuF>QC=m_^=^b zq)~6FV`RZ)NXWQxpdu<$8Szy$9oC1-v7>c~$hf`V26?+Qo;h#?b^>IMc|&f%a3(pX zIX31kF2Lc_0j{)XadZn8z;U z#_hN7TbM+pGp)5-RdA~9iq?3gs4BzZZ87F-hQdp{E@7cnVrtpR5AR#(4J+=bn9&bB z7lxAdBCzP|Y)bXE0dC@jy-dP0`UvVzckO6l)U zSGOp~=_N*K;+uH&KJ?X?kzqL+cP4ea^SY|UKD*A$Cq zvtFaZjYNJ%>#=DPE!&prIIc9V{_-yW-&O47-s<$v-1sr@%WwVs)(8|C0g$B#e=SQ< z0ekV~QBDV0ine(=UX+Tk`h2pKw-pJudU2}20%qkGr3&1e!4DI$TxxROHn=QJAY49t z5!tqSHcjB3`J2g71ZZ!GeQL54y%_kL%u-YC7!yd1X1PnwK$Bc_O+_!q(URy8BAGCbDdAi9&WC z5vxtfbfOfl;Yy5gRhb7i3EK==NB>-s)RQR}rV zMFp1YOA|cgdBOW+vJ`!qgyYR+DSE-${WNANLhE(-jn+DE3`AFT7WHTVa|_GrwicpF z=D9bDg-2|w9k~mR5{7C^{v@&#$9u}%m^Am&hX>1hFx?$)Ii2V zkUwj_ODej}yH@1Y8006rwz4F{^lQ>o%sWJ8_&)edt5-uXKR!7rG z;!ScZ>i3y$qr*=hv1?_SaYst>w(A%9|GwlzI(hi&ga7*A1NZ;S{rBJd^?U2Pzjimf z^Q(7~+rM%?uGB19-_(P$>-HB~Jmo z15m)V{AEu8R2aZZo`P~=055q89_;`W@S16 z@QxjT0*1<$Jq2$s4B#bC!Fgc-FLes;fSdbUPQK;j*6+Oeqc@Qof9d2~ZoK>Y_g_E1 z`iHOmw)^hY&$;*Ww|?TDdiP^@zxvwu-uan3-0h#da{o4c|Kq10efV`({=kD@dk~-E zZ+!VL@RtvPl>ZugdiA`W)L_UTkv`jEdNU;U>y^pOFtBVETT3shKvv6bqw5Sfpq^K| zoWs?D-6Y|rZJexWcBNGbOb)m@GV2+-8X+Ac$e;==H_m{cWVRmqe9u^n+fxn{_Bn_} zykCEUp@YS#AJA(=oI(Ar9B5Q~-4pR~H?0y{ZW(I5UyXp%9O+Q!es=)L0)pV6qEEkK zs4JmWR>K;s_|-mA3DZSqlDE5Ss9tr>9L8&LYfulUv(#H%)pQRr+;%>6Wj792&O4nd zf{rHjlBo35q%8GvjjAi`xDK4ITy_aBYYQx%da+{(=iP;Y#gL6x(#g=Rdc9>er<(MM+MU>vSJ8HRRlmO?!?T&bz%IEwWa zUOf||-0GD$tTrJ+9FnF(y-(80xjluISq+}^INTU$JmJ)snW~}{Z6cSrEE`e{!xF5< zs){zw(iPF43EV`@Q+hoW2hWwMzu$d|A?>&ANYzkmIHQLKyoT=-BVYvP184Plrav8qvGHXMr@*ztDdt1~F zu`mVY&WX-?Kz1erqm?wy7fB^$8r=p`(&e6P&><|P4Ug!v1cz{P%B+~G%8>0cUlz_R zy3>^-GRhMU^Jkx6=+sOKQaVk4?wWngFqb(d)$8H1-mK_jYA*XOt4u<1 zsZ-ARv=;cBX|I;G`iiKiwvadH*4onhUdqm;>KuY_i=_Q2(VU!xGcQfsO~WJm>NHjK zp>v4Ae1f5!xYLaCnvT)uBdXT!@SSCOF6P6uWmQ$(=^+DgG@aBde01)XxXQFTG7(#? zB=uP)L?FDN5l?~LG0W?MVJ9*{O+h)8fH*T}oi)Z~Jw3@6A@Gb`AJ$L2{#T#DFj57p z={fDmT1IfC4Ej;-QC_b{n6P8mDm%wa56-~CJ4Yf8OSNFRGFA-<^||U|OrmzL)>AVD zvjRDo3w3#njAt|d4A)$}iM7*K$bmQ2l{c6kjKl5l84O*o>Wp*6Z|Tie+eVjY6ZVr@ zN~_~2o8XWlo2A~WELGX_qyZcVjZmA{Cr!>6IVBh?$>l{TyYo>;EUBb{=yYr~bdoyr zqP{*>X$ngncRa=xigkeD%8x(A5NXrN%!X|{go(PjFje5ipCwhg*QaSVW5{X{Vn{{- z7u549Q|bn3Z_Fo-nzWWP2f}*OcBZv~Kje~kXQib{Pg_f69QV)Eg*aW0yL5XHPkPbV z;AaOIu73O(42?EHSUyzqLe9=v!AE1Xg%JHxv+HJ^D9&=9u-tq!bE_&OFX-(IVUObUb0B?rIerV7Q4r#jqR?G#Q20 zBQ0yHVTZOb$ch68$4F(|MygY{YXc{^xsGP%fsN07tJI^_7Tgd^saJ&pPMAs^26t3C zDJ7P|Q4EPxN66VC1uo`(U^Qt1xMonM);Yj%?TX)&sIqCzlrR{a9OGnkWGjkI=O4b;eY>it}~}I}5rK zDz*iVnRHo_TCFYq0K@ICeS)C_&DWG#N=?)YvB8vS=Vgi-I=nHRcp4Mq_B5&(kVT8* z#`)MO$37|4eqWwWW2$Y~1ha$+(vu z%EhgpdWxaXEb8MWKA*BBB~oSGH~oZH^m63f`pOfh7{R`?PSr%H$RyG80H#fF~4CzO+ilG(FONrB?|O zs~PK{)LELBTX6Y|n-Ls8F?3RtbX=8}^!Nb7mAg+dG$DzuvQSWD|69$a0es zhA7Eepp!XpB29W#OgOKQXdb{7Zp;i@G#|BzX#jy2*^=%klb%|ePQ<#P#MvAk3uk(l zwYm%Ld)_k6 z8-uvPWvhm=G}khSieS0E$~Vikyl;@X3D$!pk8uLiYHfTLDMw%!J;eY8%Cgz;#1KRj zK;s51pt*}i-hdFtOC5pctUM95+M=!*=bZ@XOTJnG0g17n+mS{!?W(ol_(bov18OBy zZ5UI0mj-juZgaCm-qq6~+celEP7w!d^5naoVkpn3F)5SP$;@*D#$83tIouVR1B4z?O{$MF@)3lhw{5@6a>Z9QjGPRyDL4!0j=q;hbsf@pzhm zxTI0g#Eq;+O%5o4aCr64HIMX$ZkYlgahae2ZXZCQh)#s3N zU*=ka0hP&oeXgvdGUQ_sIjIvSn)M)9LfCbC66I`}hO#qFl_!qSrNridvABfpKBphP z^9hCyhzXmp3#>;G1VS!m!n&fig>J~9d}1mTCZ)V(i~! z1;S2Can03Ak`wCu6s%JUB9;`A9^O{GkrC31oj6~EU*o8f2y!tYEInnh!! z?X4t69!ef>YULtnk}ppgcg=V@4tR#w=%hIqalTE^ZLvXXir>t~rq{(rWkMa>H5$&= zJ$Q0n7YQmORkbxl`Eje~Q~h|@s-}S)aj~tA#ac3?T4}kAYt-3{>`>a6WY*}9-VBY_K?mxc&x%a;B-lMyJ?JjlaAKy`K zfBg0bZr?fm`dhz#E4=mKrhMaPPk!&!x7`?ltbna+KXz^UaCPOo3*7bJ;H!6k@yUoF zsqD;AtE6P@PSokh^^u1(vt`R@jnE3yaOF0vq-JIbL*iVDK)h-S;u&X-396+-0y>i; zuUR(b7F}bohB?Q`3ar9Q+c?8!x;(WoF_1#a_Hb#+9juI#pE)_X@@-FWl*?<^V-v70 z=6Nhl>%8m?iLt`gNpS>039T;G5jat`tC?{=?BXSM;DrHVH0G$0*kWF^yeE8~JslS}LEb#6k+!Ho}h^qnhXH@mOvol}aQ~jfzb#Z7dbTbYr(%Z3@#tkg2{g#e1FB;qCAKdrwa8edZG!)vgS@?U+7-jF%LW4efXpK`TrNu?o_b zgXV-Dk}L!+zKDN5n`KK~)UkO_OCd*H52syLjLG@HYEUJ_gP=xv&5X4Y$YXFeBoszc z(w@}p1-!kE3G!fGZUP)ve)tIv5co8$+nowJLIX@)A!cVXLPoq!#o%=Bsv(FeM@Jw! zn4l=Nv3z!nTFtQV4ETc6|Dsy81m7UjfSqAq)uE=Ydiw8lCoN8>>+jEw^r_df6B zg z5rB&<1?(c8Xe^d+cix9ELK-wv9`T37**NU80p2G^L~qn5Xb=~FfaBH;FfBj&1P4Nq zWo@?L9FW5zVdak3t2v`(I1C5~7*G@~@2GRKzGSmta<0iaJFa0pyo4Y~Sv)}+oQ&!5dfE&RaNPTA4?cd3++KbBn%q{NEy4LZxvfHtN);OV zfp*y2vt^!yHrw&6(8_FS(l?m{9C!Z)n3oSc84noLuJ3~68%7?N8+L!%uh%+Gx4Gy* zE@5(21k|ACI@6LK)}3=1E92P+#ksYj>9b`&n;S@$&v7}fP-GWRTh(@RELvQ%Iyu77 ztb=v>z9%@K4BQB9P?uchXJCtK_*!JTl?p4D+?2AVep{w|3B)(fb^p9JA99u@Hv~S3jkA_B0CAHt77QsR>8%a?aDTb^%_k=hf9xra z5!PuS`lJI7bf-((bdR1#>oSR9D_0!#qHfGC6GrIyE&m*ycUFEIoeE{d_F56`trTTa zNBUT;)t@%8Q{nlH`$%eeoaBcD1tP2 zkL+Lv)A&S%!wm$fQ;rb&2j4hCg@5qmkXs~Nnze( z4_U?%HsBZSrnrc)P#Y!T;@}&1e;sJaFFRxjBb91x;I7AqCh#XL;Yy`i9m`$&@E{Eq zd?@CarL~OqSPps&d-$Sp7o=~x`?n5QRvL{81Pi#+J+$<`#^y2dC#}3-T{p}e-W^(y`CjphbwTUjFbNQa2_sLf^UEyR5MqT!&}nXnpTu1Z`*_v z5vw5L-S~jz>92x;{q6&nH57t?Ep#xO9v&E8V_8GX5DKkXeeoPipldRc8{<-w&nBE< z#fR$c^rt{B?Qc6|St|o^iBeoSJi|U=x&1~hTbb!|EM*pFLK$-w3s%%8vS}Pz)lPpH z4D8KAmM{vz&_eVF?Snyi!gBkKau!d?LsQ`yznF-Y(8XudL0gZSi}vB8%jq8kN6lYy zZ!*mG__kLCP@~#tK$1Bk53f*9zffl4OWm{%H^UoW3O41>KV*m? z5aPH4?{ErU>tP+KKozE^H=h#^j|qFtp{d1|-s|8V@mvqD{|dNYKD85xZHAtEya$wN>xW1)FUGgRK4Am;^k$Rl<=1DsaDj`N%0=VA0@*K3HCVS6c+ zl(KGlRnc6jHl(ggIFAiME+`Zz^5#$@ORNcOQXT<~mvOIbcDdCgLl;KPRHj3kR4TI` zo6P)9r{Of~U33agMRux9hvJy;GTrq6*W}^hY2^A}xO%U1@t_V?cYg41um_Eo2+ zciy%A+qJLy6pZJMpFi1r3dj~GdFLmd%oC~w!`ZZ?^^Ikj%7=DNCI&C0LvpeT;00Y1 z=(1MtjgVdfPHSU2g^*TaQFVUgMdLN+f|M*E;R}R`gow}EOG3o5SXl_h897u~#niw% zR;rSz$1Dy7_S)Nk;{D)L9D^nb5&hgHAt%C4_C7qheFLHg&}xRLIp^goQHYdhh!5Z z=ow{;8X0bR>punEK zjbrIpp+S}zr4iPaUb@(3%vd;aKBMgVwA5QRz!MY0Cs`yB=iv&SEeS9>q(dmZc~CL% z`Gj#Oc<6PEc#!CHz~aqRmqX)BNOXzOF*i&&)m~3p*`fSg`<|1NYhUJJ z%>3nmPqmeX<6}lVwBp_QZ{VCec!Hxli(Qt;TJE^Du8HoVXR))Sv{Y6t(Mnc4ouo6V z5@^m?6V3-YGo{v$pUwmyku$^B%he>0wI1HWBNq%LW)7BJ1%zEYXB}p^HpQXRbHa2C za`mN$@3S}l1sIQ~Z*o|bgr>mq4{kHnu5Np%Vc=sL@$#tN&!Lr2u4;BK5##2EjdP9+DE{!6e&ucV zfZ%@f6Dv(C6K!0xJaQOA?l}NOI4V&XtPEXDcpI+DUu%udf)Go0H!c{&cEoI z2lQ#@367pH@keUYT=Z%I!f}CQ1zr#j`YqaG7-6vL)U^ssOQwZo=ghQ=P{8W}gdWdF z1E(dbK~+*$@!Iu)&IC%$Ws~{3r_inPS>9hw?tq?ZGRgkGhAVFP8G$ZCWV z+wC-Vl1mk;500lO3?uh5_PE* zDOAI7(u&SFEtHJJYWt$EF>Nw;sRKQB^UJ_F(2lYQD3D4KnpN2v5?9M0AylsBQxS)mNu#Y!*Ip`)6y=Pt zv{5XzNU_^(QH(PhKDW=Ef~)!Oe6l`hqt3Z$1@H9O<;2GXv?;Af-6ThGqvY4p%EYjT zYeS22LOCz-uHdaDsp0X$j77$}KI9jv-7*a;SSJywR6C0@(VxyB;P2C9gSJFjl1e1K zCa*Ku!Fb&I5WsQ&DUK#x7L~5ptbtq%37#4`qK~^ZZd{f_3FQ78Rh0pQ!8WX6=Ub1T zMjaZ#6wP#`b%lolSHak6bzFtkW(}#AYM5t?K$5HMOijgw-t`x_vjPvxI6pbG(%ktb zpg-=s^@;vqS}`Hu0$stflV7o9irc}X2tdt)E(*z+vGa8#*~TyzeO5@0W^O&hrh zqhxE*gRMXSD*(%~sWJR)AYf#K8%?gd=HX z!ILVqAg4w-F=Ajkn-hH@fKvVh~E4n>B|OQ*tpB zN{zaWKoFzUS-<1dVw11HYn205S*xb3n9;#QyIyCS~EhKKw9oxcvQ%wTSQ z>*whR+`^yC3+`4M;5n+n_p7i*$d(KHfG@YOq0H zcPA>p>=Moz{JE7hJwd70R!wWEQJx%ur*PqHJu|6@j*P*ytt`Mp34XZ0-1{gH+w=*J zHc{uLv7oRNaQ5+*D&@`~V^U+15=S@Yu9tU8UD{uo1B5td)PS!9Y|;hJKjJLmXBv2c z><;*3M6YCvHf9nVgB0QsI9Z)(z;ZM(EHfQv5@S+Mc)0Cf1Lm)Tp;m+U#m@$H#gdO#m4^I@A!V_^Ga(4J4ag zECsK<=IoV83exNEl`wj<4}DKqtVfRDj8{G(!`rz56|q+GwugKEmKw*0Qmz&kX{x zJx$cM>q)cenrg?c#`+*vBasQP>ridgl`RaVaH0oj0s|`eeya&7@N~^@|Bjap7Vl?_ z_eS(ZTbi+k+Ho^+4Cu5;TY#rIScQNG@K)OKsHU56au`s+}$*?yz<}1$GzU;jpaoU6*Rr(LToFWKfPPmWp>O^N~;^qj26$ zY2Qb?t%l`O#Dwj_rIxX;o~~-CrDzabe@_|w2Mx8*ed{Z49cb};--|`-h4S9_b~tYM zsQexT0d*vPeEGM&;WIweK7HReDrei_-&SBP!CU)rxeU*t22?MX!T%$lo=M3C?*)_6 zCKDM(;=YZo7>)pg-PQY?u^Qx9Msrj)z=KGJ2d3OMQ>DVz7~q@L$94k^?*_U94o&Te zF&sBd0Uo$YrN3JM{rv&}w5#@RLdk#z_ZeXZ+-XNd5NM0;s_*R=iwKgOi*VHhr5)Dd zxH%}>An7)?*1Z~VSnKv{`bf6TbUi1bavImbYZavT<0v&d|37=*0VY{h<=@p+9j0fx z2Lwb>lrZATSWdml4|Sz-&Q-61V)=5;`LT$sq1TA+`Zr=&6jV@g*DU7jVqVrPVpw$z z7yvQzztvssXwz}N5#4X?k8i%B>YQ`WJ@=fO@9)rZv5`*?TEj}VxaT%$e-?y9{tqD$ zhVLEgZxa%w*vSfscA?!T!LD_!+XfLzpf!&v68jB_{4kdc!&N%3S9>AbQG;SL9AkT} zXu>RHQZN#(OKsQ%7w4+x35G=dejp@ryM;vL-f5H*3W=I`l0qU|GXcVHWE2wepx?{a zyLqOOlh?xMPeURF_cJwS=#*MpZb*C^kon{<#!LBX$GWBX3 zskch`J%_}jIv^yn$3x|k>)_nvOTg#X~Po?iO};wNMv>kiSS=@ zNN^`9B#JQ(>obK?q>f-70nofhNTWC0A|$f;1{>)nSuE1Z_jBbo(Sgh0E{`yTltHZ@ zQdKuhgS(2wumh5ubkp5Fl5aLrjXj6NS-%1ykv<+0e|!n;FAx%7=lA4HoCr4-tg=I= zmrd93jvhsO26$Pq&xuo-X7y2s@H}NWE=4;z294sAXIOa#AdgcB135;1R0F`RmF5YC z#F-xeA(7fGB%r_MkN_zDCuQQ4fe^A|#2VS0*%yu)!4 z3ZJZys0)Z!?LtghP%@1|TN-dsvA*AlQ>m04OOQUQ2Z^+cXpESJStvv%GeMdhIcllg zF1h6}ogQ?&6AOv=-bsOwc+74gaW5_KPHf_IvepvWe3bBvSRG+{Ew3F$2TG{S>RXKx zaI53m5|e~NAn8^_sKZ9s>~oEtqUH>%N%!NUuoa6}rzg@80{3 zr>x=s&-{Ak;6E??D)5is!~dB-fIwQ@Kp@=*9@Or@BAXCMw*#(+w?ZJ@2Oi8CM({o$ zS=Q+Hfwr%`NpMm7@}X?Vgo_Bo{#&uHbIlIDv!qr#2U_Q=U}8lupKaZ~f) zdL)t3Ls(VkQnDKb&xx%X>LvMjXJn_@T4LC*(TpzEtUOsBR=RjH*C?fIfba`|TTMx4 z0VoCm@PRKq52eYmp@Wap3w6+1)d2$O)AZHCBo3cx`i|_wyG~jhCjVE);Xbgtwu-~! z8E`jadf!X&_Xh>$HcA| zuCWF6nAa&sBndWJaE<8zKLiB@I~mj zL8RnKjV(4Y=tVuJhDOX<{AjVrp>E0a(sc7^ziA+@+Kz#AX5e~BDh)QU+6}mk6ACOz zrEW)-td3g~QfMIyZl0Iyq+G)yeFfZ8+7pELTGb~2+-I!j*M>jXcVr*Vbkf2u@xLp$ z#I|`|3OR5jQqFhUNklg?1-@HU!>Md)02`C1AqXNJtMho5!sLGLgafl5?xegD#`FDL zr<=oyDfg%j@l71V$XK=&V#R8kJgRZUGMn#tLaUn%*|8Xa((XVcVfORGban_XQ)UZc zInMX0JO>rJoz&j)x=gAL0`s4^)!Za7uf%;v_Tg?PEimI-1?Igz&)n}sbyJ}Iapv^D zP?VWEsjZI)+nt+jUh9I81P5ztGp7mfm>?e7wgL_)<$=MtOz zx<$tLb#cdk+j7ba3Or7+xqJFk+3RVc)(V{HxUSD8CD-yn{Ly!7x?!lap(fAb{KjX1 z^^f9{+N}|>63B5ksm_`KHZplcKtX@b-UbS7o5@4c9Fp%7C0V88$)hyO!EUD%?{Y`^ zq1DNVnFw8iqw#t%6OEp5Z^L;+Ux_B0d9>OP;a)3Z_J;)k?NR|ro{}8+m@dUaM6QCh zI~A=Ob4)!;x;j~hMqE#znM~J_L@2KJz?}zAq$DK?pk?+r`-VW(r}+PYS!3qVC5vC4 z`!V?Nx4*wx;LmIUkTS5kw&HVrCl_OW)6UCeVZBAlz{Hl>oc4h=zxB?@eg$kAv9?Yb zSQ*rMUaG3pYPE_gw+6kz8z}UtPIzOtuRH>1Ey@I223Q+ih|lZie`zBk}%`MNE3u(0KFi^ z8~iZ}u9ecEe45_VR?DR7ptFA}vs#%1-qXh*=~5D7%zC0x=!G-&c#TOkupV706zxi# zCv*KYA8dD;5|a*c`0l9<_853uBwd;;lGX%7>BsJ+;5gBg zf%ov;$*gH(Vl&wl{xeQ&;;~Qyl(40*M>AoTK8oZV3sZB6Rx=?I>7$ieA>NNsAvQuI zp!tt!Xf!s|Pk3U3D76sA07OBCR@q{IkZQrllt$(lF|d@Q0^5u?*i1+tK!v(TS$%#8 zfYjPh{b-Mh4#AB|s{;}JaD)bFuoV1^I?BVfEPX=DD8rG)>|~qsykvN-LpFfF ziep(LNR5?f@B^HHwpv#0V|_P~yB`4bHTwOoqBce1c}Mn8J`23?=rc*{mVQ61LJh<`PXz^kQ%UtaB(EV4(s~-!cSi*iq3r)`8(Y?lDfPK8e8x zua+j`&9iF0{dm#|#o$&Gz0m*4nA^u;LSn|9a%!-7rV8iE5?D^-efMtybx%UKW~?fPi;pO{kzrSXL}?$q@jr3x!N>X!8lIG;CBe zi6JHTE!-&7wLD#jlbK%IrPFYRDmU9P&H#{!Rw{p#g_u1~2`5ztA-c4p&ZK9qomolx zp0P5s{ITWdEccclc<9@QKX&N4nQISehu}jq2S0Z3IR|?OAGq}GrR$c|rR0IT4}A2% zvwinF&^_>g#c$2V7GJlhEG8Cyzwi;?hZdf-&{;TZ{+si!otNk1bHAJW@Z2-!I&)_Q zz8QF}uN9DezYWCwzw>{1<{SQJ`rH2d&)z!wnpx@aa}Fbi&)$C72@+@BfA&Z-VT}j` zGDqzoMR7*RNZD*hO62V%mnV`+Dp8NJVtAAtSy6D18;-?SZk{qKtXLQ@XwQ=cyW|Yi zcBkD1Pc+(f#;l1EGvvCdI9=@(o3$z9O;g5d2-i!ltrru904~0C?Y=bj=v)V2#0_nO zZ5WDZ7v->~(No5MnKH6yfsFK!R;7rh3xi0*jxjyFo2*m{<0vXOg{nZ0nK(`3y(!~A zPZ>keaNVqiiwLW$IV?wq4TmYwN@mQnT(o7zBCX0OneU9N8zo*jW$g5D0NMfyspSM6 zA=TuWfoee))$Kai=S}EXtpeSkNxIQ(p~S zJ7whAUNaM{gbR*2h*+ggIBHi0eTa&peJS1Q09zZH#Vw44*-&HTlF>rjNlh8K4aU+S7qUcX92xOemJ7A4cCJ=Z8)fi* z3I>qaQX?+Z#|X|(Rh>O&$_PTVt7&2flLxxcO;iR$gGn{?Qm`Qv6U7#91Dpf{gCd=& zuP$8=lI-2zcce*CX(O0|>;zKnp~;+TTTM62)FWAg^dPHELc=&;j6zjXURO0VEs&(^ zc@vV{{7AODO`b2c8cejSyUH+L>2%Uq*BkX+hoHh!kDIM;FqUnr-w20t5j7rm!c;PZ za!G~;^S&BFMT=%`RFXl`f`U5LboOCWM!fEj1UReqEZQw5_>l(@xoQz3q5vfw4u)Mp zgABvvzzgoFN3Kb1$4S_NzU*P85t57nTY| zD@lz#Arf*ZK133BAr~8s$7Qvn#=WVki(j2GBE@orfq96u!c~>(^QuCWBYJQgj?{Cl zh*wUDbu7l`Wi+{7$C=+x838tkkVVAam~!P{nkzF#BtmyOR9MgCQW}@Zuyzom07n0G z%<=tngE2^INSuf_8iwJGyIG_>rWLhDi^f>dOd^%w(ma&mWMUe}3)f5;JC2EG#5e zpm?mAq)fR+gIAiM)56#5IIm9`+o7z|u1jeQPG-s-T5CA*7J!wFb;AH4vk8@&TuUw+ zu$IiNGcLb>%Gfl_EUgRDAPKGw6lzMz3*lFQ| z;UGccn%*5-Ap4AC2dP4rN`%64X=>G_A8s&aC=R$9PZb+Nuz+@HEI2OaY7o^V!MTZ- zCYfq!?D08x8deLxnKITTR^U@^7^GcRGYM>Y4m!M zGHl3DN7FefCwbEW`OuR$7$s+1NQo6B?%_CR4|8lyqjL3DJ{(RAgJA0u2B?G{(i=|4 zoCCfMMiC{M5Su`uY%pspG!hfcItTE+LE3CY>akRTte}vs^{1npf1~Rt5z1J~$ZE9| zABl{ZufSRZWV})GSOII)D662gphPPr+SBL=7#k%bX*o-Ao-EZ}k#BH?0+Xfsv3fIK zPggudkC_>T8dNcA>N$%`8;prmZip~RU9oJ*Yk&+&{ahT$B+5D&QS{tMiDNyv4D;Qo z=N$a{lrcxMB`b!b;HIAvgvVoC1eq8{9h(5YEXeAm>iCpt5ojL84~E0*FZ&yOT*<^;oZj z#B-%Wx;t%-`P-+%BitV})3nQI1tRL%rqz%#oDpp`Sfe6wVE?@oNn>6np2$uai_-#Q zs@@RV89fCREC?j@Fp+Yy+0MtUx*&FhURUf#Od{=Y!KrN)HpbN$?64E`DNW25B8Xck zlvuk$rYn8B9Op(5md}y4X=JM8w3Ay1PfLtM4A0I{7*T>$nH2%BrV~*VT!tDLnPg*H zFIo|qAe?fij0ZR3I4oxJ9Ry=5{iI9gi1-leCCcF%h+4YY&rr<-)+*^mec(*{s=3~@ zM9}SaawOMape6wM4;57_2H?GFv~9@YZVkK?0{IXU$vz}c<9OkJri`IcG%i(yG%0JO zoR0_T@gQ2P#UxxSk27l5whC>VYL|2Nv|IIkZQ2^4WLim&(=8f6MYZA9xNLKffKg6= z&=nLZVRKj`>2-=KKds}mk8600hji_FLC(o7UBJh&Y!E8P2UfG;5QA~8Lq%edZnu*u zOkI2t0L{$2e*1;qU)L{Ft9nhcs0&Qa0_*H-pN#hlJ!7ESbdSrM5f^~4<=dp?S)+}K zb>Qt&t2%I}Er1uYZE}=y22dh7Ow{t}Y{(h%Ik^KKqGyM=#N;_?+Q|{q0xhG|r>gY? zSa#-3u}{>a30RNCibH}Qjk}R_kAkQ^r`Dqia=kl#hZ8B8>vAN{rFV znuKZ8+W@=XEO3I}CsA0LHsi|oCi(x;v)D{v(f?omSNeH>-0z$H+Kbd*gZqDF)?QRr*`~5F(foR~8x!J9+ zKt)2E&&Yh@(?1-Y@g3ZO2_WcidbqXDH1mZWnKoZ)oHG5_j!c`MaZj0U-jQkZL)R(O zf9}Y%wIOCcza!J;3uM!hJ{Jx=cFwo8$-`S$X-9)cHbV?d%lYgM%0)t3E9YCj!c`Md#|hKTilUp^Mlwa)54BSo1Y9$ndWz7+S*-xb2~C^?XJGSj!av(t6}@^BtMC@rR%7$h3_={B%dAZT#UUJ2GwK z54Y{ew2eRfcqf0@+ITBFl@s5_AAYnW(>DI_!yTEn@rNJm$h38pX8vbKrfoWv@9)U8 zjT3!uN2YC@=({^IZR14$y(7~$PV}7}nYMAFZ|~$pTN`hAr*dN3IMKIuWZK4wzPTgQ zHcoWwj!fG$?KgI0+NNo5*^z0Rrv3VkOxrZ=*LGyurfI*rQ`2s3heJD+6Wyk1zp^9K zHck8G9htUi+Ar| z!1R~>51zea_7k%&pVeoJvuFE$zEocN#nMen4?b{*?_*1^Trv;2hw6vUKlsaopE`K; zL3`ytS6+MI69-;?KtE7CaQ5QQ7eBUm)uOtXUwpvAPZvJ2@R9|2<+7Fb%0ribyZpK3 zYnLxwZZ6~(&Yr(x{-*h>=iT|H@2W$uK78Hb%MW)CKWydq2df9qoqx#Oujf89^V6A+ z9R9DlSI@a~^@H}L-0AA}!a!?w_S~wKY9l43JR;jI2dj|OVADErUZ2!cNY z0Kfn#tcnlybY)a5DVwA&Gt10gN$F*JucXv6HPsdMDh6I0X>7&IfK=@^vRMIm8Jy3ic*qkJ7Wo~6|gH#ne97~~Gz2X?O%52W{GJn7M`|XwV%=t4nNYesH zN5EDApmwQR;k)Tgy82J`pSoAl!~Vm2C0+I}@0IkR|KMIp5BLx4m2|;n*qG%PMbSzucY^xyU$)pPo3Lntz8v3W$u){N-zzESR*=U%;6(pSyBYOkbM z&s{wwC3l+voIij5UP;fJKX0$3_n*K22I+3Im4%f*jHBY+zZz!-^wN_R(#=l*scFQXps)ni+i=}MU!OJGazNgU+{0LYd+1$PFWGtYF@K*UZJ_EBU*XS z%6s-o`tFr?@0Ik%l^gd;`mU9C?UnSMEAQMZ={r{5u~*W!ue^P)q;Fe!+dAp)o^{H~ z#x!(SCp^3|ordln+>cp)%v9H1`+>h-{`K6?4ldnJ9;@}u@j`pD%+PDyuN4;%Bw z1pn{8GdIi}hF7%ZA1||qu0HtHgUHgAOG^hbi|<{yYeAd8e*S{Fao`31&-mwOALDZX z@n7!mirf{K9w0Bdq;>zrN6#Dy)dr2GGPKQJzcFB{tP_tl>S-H1WW>5-Inl)E41q*5*b}9j5}nvm&V)ev>6H$ZnGj-?GTb8dT7mYYkO61JG#wVn!8tH zN4E-t_OOq)2ML!er99V9r&MZ0grNbgLgt_aJB4^88mh-NkWV|sq=k6Av{wc9O1FDO zc4!X>w1;)PJ=#@(#nhKe#-LsbF%_{Yblf5rP3QX(QIGV>tvpq92x#oV{C4VX-UZ%< zF0^?U_gtMCU{(rm^vaA^u=_b>w4X2!f%Y(uw+GZ6YjuS$3&~g}UyBD33!=jLmRTa$ z5nT=SN;OCih4UmA+O8t^Q1IkcWc&6A!B_aMFbV@MNZrUzIMXI`!wqxl*Z#x@p-hJOI-QE=5y2}hDfi1P;wrrJ}!Ah+Q zVG@xhxK4$t7Sq5>@LEk6%KdUJn~g9PGAuQ{(#4U@gU~&{IhUncPEo9<@J^>tL~0zh zd8csAmXm&4J#I@V0-jwo`^|)n0bulT!_<*Vyxfe&#YRS~M;#U9Mjej_it5nY*=X~j z#9rz4rtmhlOaNOd$8CuWTIoKaWn~#Ej>U`~>7cEuXqIZ=$se6?qrDW;=|l~R7xjxH z_ZVpI@y)rpRJzR(Lvv_~HKy9JOX#N9U$f<;&y|ncvdd1A?B!ELQHNvsP?G?!%IvNM zk0KGu#g4IZJ*XEoc|ax>lD6yC;`abQd7xGn3H(ldv3yix`b`A)KH zQEh^5*WyWf5Nv?=dQ_%c!|WmF@VSd4n@6#GesgZV)G#eisWEb{ZA!>+nA*yg5U{0q z+?G;Gt(L(fsaz?f7NK&3NJH%)&7&?_ffIGBoC4RrZBOlb>6yC;`lWw0oUX6XEjNPsp_vl5pDS0#9?rCKwcHd6e6*E_` zYc6pn$SrzAE7dZcu@?zOnka~dt~mq`TZx!a6Fay#;!=s+NQ);yfVKFG!}l1c?(xmJ zX`CNXOpDfVm#s0mRI0owyaW=pNZ^*tAGu{N%UOg62P;%d%*53$Mya&{RqEOiW(-F_ zQck@NB_r9cXrCM%CguqzL3`52+q3MkoWz+$tkND=WQlNsl^7JmhNT466I-s`BG^7V zOpmhOwjq9xo_w1o*i5&((ROIhNv}^GuLd~k=~7XSL{fZ3;Nyb~VTY3{F^qXkx+hjs z)nGqcA?XTu>b#v>-lG%UrW%{+c1LI0p&HQtY5f1@8aTO@D4c|jG zoD6D2(@Zkm8A2LmEAc2(j(cR6xM=-1{0gw~HMzWQaf+9KQDw3iONi8x8y7_S$ zCn8M0;&IX-RL`cMXlV`LXCEkDYxMi@)2&hNM5{?4IOH^ODUux2v)P{4Ov>e8#UhAs zuh|2ciIZcqTucq*t-wjPz#!d(Q*tkXdTpTy00jcC8v!cmpI`$0dGY@fAt*ipVBHd^ ztLdt;gtx`k1Y0;!C(KqCB+qWyAj@DCE8;E89e`K8cHK)SPZ)uu5@qBxWgV4Usw*|Q zSa+mHD?KBr*08wlb^L!AV0Ld8cI75# z34PLxwvJ^4qitq#l*Tr=qfS<^)wtqv3L%Aa@j{6!op9Jyi$gV^k5ZlplhnXzxZ zY%ttyga5yI&HN|h;h!1*Kbefd+XZHCkn{Me3wcGoFC$efd^D}MYqVxCY{t^lf>iVp z3LQ%)*b@h?tea4oE9!Mb>=2e33N?-~%_P}q^Wdnd*rJD-23h07Lx?hCRlEY*13EP7 zfRs5PYkLg_;6ld=h>GYYCD-9txL7;pl~8k!fjOx<2uy(gUz-Hx1pj{@?sn1wGrCn^ z-s|(s{Z3Sm13gYa?;f!e{{Ou+!tIX#zj-AGhC%?xKLNvAxGXtr2f$%2{o#0w$IjZr;8=9qfqi#NIG%+ z|1cDzhsk6+X(h2p=vYbYG;#Wv$C2rQ2hfkIAufC@BD1BTDiWPzO`@xj$3hj+=2R=n zJ9O5=`^QME-++&X+qrx(8Y@M+**(p^W)8pRaDC;@m8(`V%Rg9t z-g5HLHx51RQ1IZ*2QNK%-qI(Ql%+Edy!QZk;Lzfm7e|Y}h1V`L=6^B&via=X59j`Q z4i4NJSPeV^APD?}|NPnim{n)*=X;-z@+||we}+F~)d3J&)gug^ij}P#6HWC-bt4|f zn}viPYz!(kMK|IpWLVcA)kLjgbqaU!)c3;|{OsIY=k6cB+Ue>8=xT5EzM5E6;jf>f&G zE&v#|Q9oF(h>?nu3&{nWSF6>e?)AhRu6IjQ(5Sm^n0fpY=HBdk^LIXq{POeHe(m+y z&tLJTf4=&rpMUb^7Zn~}eFm~>tO=IDu>~BhXS#HvQq85aQcsMu+jUvw%am-waW&CR z7yBIAcc&N@uej_RcRe{>U-^%RUz~Ya>Fv4Gn3tr-SO3Jj{w*IkoB7IPAA+pv>w-(k zy4=)sI})y=ArYYe7;40A)HEm?@=#6e2E${;BU?ddD)_1^mtXnoAARVuXY-|Jz2P_9 zH_m_74b3|~b=`LxKYrh963_k38OW-(COBrpwT#+Bbc)#Zy~Cy8Igjz3n>hDv^euzyUw?kSlz0SjMwR*4;=^C?CiIQSs=6k) z7cS6Rx2l_CXJmu?)=jbZV~%xqyK3o~7o>i9%Cj$gaJz1t-TL)` zvp#Y4dw%rQPx!CA;3ccE>n=c6>8U(IikH_^vO;Z>(ktw@Qw=zT;M$9W)VzH|C z!`VC(QsYyQohN*X#Kw2W^rP3Fb>rey|5m;BjEip3FZk8!BR=xBLzfuWzYbZI)&z^L zQi;l?I7o{IMF1KdLNL8<+8Wf-t}5DL4ae#+59ZvlH+8p{UReItKVJ0WN8R~#;oo{+ z>Q*1{#n1zYLrqeCg+RUxciRYl8Q2RI+Z0A6TyWweZsYK2D;~X?_Gdoj*MH z!rN>Aqnwj{+FL%o#30D3uqJpE4JGXE*cI|1pEimJDC#PyLAwR1c!5$o!f4d;lvcb{ z&Q6hP=x0Cj1CROn^DnveU#}tA*T2qZG{5kg`6s^m8uIy(YgYd0$YYRIeoZje8$b~l zinYQ8#U{($!LXQW(oP*ym14c#4Mt=*r6Y-&GM+-xiMcz!@pU(YzU~9x>RfWg`~Ut> zO}efSz4`aITzT=oJ^qug`3RE6$!1=%*nU zQulu^LjLx>!w(EEKm3~eUH(7FDzhdSR9+&ijHaQ30-77=i+Vhfa7qArFB2I=26Qk6 z+2K&LQFEraH4lk>&-&^0Z@A{8OyeUD`t*%Ixz8C7eD2%F=B>YZnex@lrz2-0tMr=S zoY9DP<58|LNJ>NpiAa?o*KIaM3O7Q@XaV!$CCeUXEt{W4-hKY_U5z)M4`2RJ^C9+C zH@vEP`*$z9$+-QBmwx8zV;}nIPd->hR;e|?3|}-u!z2qz!@V);ook zSQI*Rt*16yTCP9Ey?R>ew$DBJLg|86ROOrg^}Fr)FXPplpO!}dyX-&xwRapn^$Ex- zxh8lYr)}$gT)h9g?|#P{)1$ZEaQ9~~f9=Dr2*!SM!&B1_`C0MWME~-ueV;*AiFF&3 z*;u?W)}3fGnde%eoLy+un;<=Kal{!|q$;32HdTs=wZXLU9&+oizWB#n zn(w*M-x;@B_*>s|)Bn5XyqV`vkc+J1Yl6cWFnC(MQoG!tya5EI#&E|)t9>AxAs7!z zaaJML=nrjpijVfCb8r0jI~TLhf5oRRGyQkBKXLUJqQ66}2i`gR$d7*KzhCsBFCweh znqbXHdTP6m;$~+M&w02cj=DvymM%kDD8r_ZVx3@(Ldp%=)5v?{HzKb)<0aa6Uikg$ z*IrI_9`t74Lq~TkopZ(mzlgo%3E|%yMpn@^!3YA`oFIhNW=iBCnFlCrZIdNyO`T@U z4yGd`w<+;RK0!~RzdKera<|PR zt4~-HT*;OjMIPXbLWP*b_lp+Ig#nggKd2Y`dP>B2DwC=b!+{i?&MW>dTz^oGpj+>J zd2aQ+i@$j8zux!PANazX4!rbvKYdd03unIKACcAZn&5pLUaot^;kj z{@Ob~bL-9H=sW*?F2KIx74Q6V@|mmuimX00#+^FRFPPtgO& z>f_b~$M=5txGwnn7k~RbU;EuJU-Q`)J>t6B+kXJrA8#yw^X{+TX@7P3i=X-s$br84 z*fqg$uW#fVF|G%9U>fOgp)iF>;Lsu@Sn(bTF!*9}We_Ptz)tIe@65g7^xN>x&F{IQ z`>Ow>zxwjqzyA5h{<`)058d^WC9p&4UqMz!Yl6E_q^;DdL0C>?v`Rgjhcz0>b-~pq zES<`RQ~{q`3ix12^`oecTIq=lz z&(C-ovN~K7Tr61)g)}iUn~TVm3Y^y3QY6PQ4Aj>Ft%z4Xzqd*M@mjI0jU1owxdOfO=osXmdn#`RiJ zfukf87YMk3SG%QFA8zqtBi2(B)5se>E#0~F2R=IYzT2x0dWCc8&(wRJMdmZ|HbfeuY1Rj4uAa`_Vzd4Nxbeow;-#?n&5q$2Cp~XLzAz1`6E|_ zS6_bN)4%?)Z@u%2FZj~=Z@>A)hu)O>(hHvcq~YN>vf5h{Y*zb?7QpE3#MDULBl3k* zm}lUTSmeBMzDM@kDTz(n1k$Fb-R3*q`kajMtjbH?o=D{GxBTkWFHjG9)ZJ$kUXVTT zt%ZeN9{6#0O)#oJh`=*M$-u%fF>JV~tx?q2bVCB!FGSf=j%zoEqA{#Yr?K42dM_wm za^4qy@m=u^pLqSz%kIAIrqbgF(s}0;KlkXTzu^hjAgi4Ew*7!JE7vEIP&>-|K9ApxhDMfr&as5pU1!brROz{dM`cv z#_PXz(YKJ*_L|_)pefe#j>4qSVIPUbn;_3@+eA}b#0dceP1CgMS%cu~PHo!Jllpg~ zk7J)Q|DxgJy+^;cp8xKXuKWk%Ux&B7>-NlBzr6h6k0GnAHNkXy%wf4~BpVqCraSHw zuz^wTX^nP~%#AFp2AoAI6sm<9I<@f?5B|1r@m+^4^2TGIzyDi*{E+gev-VS{Uwrby zm(lmbJ_>=^W^+yOK2B%Xz2Z5A*P9o<^D{rra_5hvcc-~qe5>#8KH)`&9{boge#*Ub z{x)Q_u_kyQ$4%>kUvm4`d!JGt{fWDNre~I`pZWPeKJmi4F8Oli=I_7g-M{}Gr#|mBkNfP$%f}XPeBSGUyVceN z?_)!7UGPu7{qBq5-@o`L-@ee}&>!4I|LCc|J@f2a&UnVB;V*vnv+w`nKTg*FzS-Z- zfdBsX_csgtpR&M9d<%h@x6IERHF?hUD49?(o>WLQ$#QCl6u>To9KquNL_Ml95~GR_s!1uE);oD;b>k-YL18zI;N>Ssd^gbnKxlJOep`E$S9y(PoSwML^7;1Z+_##Va=Mf82ym1=wvfv!|#wk#uMnqmqmr zRv8cE%>cqQH7v4J(i91gk2(Twkz`C`4JHAhyh$>o3{Ef<9?W~_z!ej%R}9=#T|6us zf*e)dz(;)k8Q-E6CL~xjO@9(7c-my#vgs(xlANZvqT%wUANSA9-j{R;R%8vyPUx5= zYcS?2W<2Saf!uRFfn=kUBU6IOhgBU_z*TXPlste4%d;#UHT@0{F)uM80X70(1G>!k zEdRNnyobhhe$tcGwf~QHG#Y7^k;SnJ3g`^353=4tx^Gx910n_Y)<61C>Qh{41 z)tQHBDk^e;|2L_|sVGU%1ZSzD<9IGA==NOPH{m(OL|IP*FslZ@PA4VPo_~^W@`KNZ z`_G-5@f{`^8dV|A9mUZ|>;HuuD}Ur)7NAd|FMaCe;G zaFaLY3hO_(I7OKT$GeX-qwxe!8GmNg7rAXq0R={&k#Dgu!tew&cL~!2i##kSqlp%@r+?#pGDv|A3~y;;8}x&FTc66Y|3CxpO_K>F;0NvpQ!&_FHFT=&Hp zoaHbBH3`+S4GF|kC5_5NsabD9+3HH{~=J?nG7U_ToaFp9#0ci1f)HQ2$1Xe z&jBav`UNWz`1x>WGL4 zlVJjv4LE9p^%$!1is%xq>CkZqVEU_p_fIN+s=ykYYtfd(d8nkbmOOvWT5HYD@pRM( z`A49tr%D>Dcn;5ktS5#fbG$h|=eUdfSw8t?BI>UJSM_TG@rbKHn*co~|IGg3pX1*h z|KwkzyHH_RROWS8bX^%IMFQpUC?BO097uPd$T%Uhw3V=J0Nx+?`NRqrYEjmKagSA4 znx(-54u&_}}aZA)O8#o)N!i6?rn+_0^(12E=z%ZJvW3C*N0n#~|1OaW}paCh4m;EK+ zE@vA?GG-eJ%Ht7}pLFSlEptgKl=K8l3A|;Z{rQeWn4HRJ21!MesB9!q2RNW(dpL$O zI++a9Y~Wpha%R~Hfvabv>sTh~Qh{qH8r>(R#|RJtY#8A2Xo8raX#Wx@c@cyplZZss zz;`BvoPjA(2G$~ps42S&1H~aNL8=ssyEaS{FvkXN zniO`nl^{tQrNfjfF-$b1StRU;I4^Q6#}I)#CVj~LZIMe#$k9SJ z=+8g}Pmx(!F=)Ud7!C@~Zsz9J;_wuQQ^cr3aSrGll3|An+`QH$xt7MTgl3?YN7A5w zgus0sCK|W~{5a!t{f_}fo*t(_Qo^ti7IofaR6?=^kq!K8QuzbPq@iJ9oTmJ<6YGM! z@QSWmAz36`+mSqg;S^zPO%`~RV{}o}B-kV*kTE>)(uuMUbYy}v9Ye(}U|b4p!eKH7 zwj5}Sb@0IIq=Z?N^9O-#7BJo6P1^C-fktzFiV3Y?Ak~IBA&~HD8$3<3;s1%oof*{}}NM<+{1M$I`u~5?gARr6$ybl09@A(2X z*^al?w&PJpQ^9sTO{u!zd8((Vah4;JesZFmg!(<;mGgpTxG_EOVPLh{1((nTO6UC# z0h(N(I3@|JlFoqVU?In`xR^>11Qs=EoCLWa69kwz@Q%L%XqQwy>}pBZ6z#y<*Q%y5 z24=@KA%q#S3j5ChM019UdA7g42t3cEP?3C}SlQI)p6y0GSqz244l71f znVgJ?o(r&B#;=Gq$Y4eA~8H1 zrcvE>NrMST2}olkIqL=4Q6JiJok?}+KCxJDDZ~CT3}ek9EUP7=9GWgMB+P$fd>ezV(=Qr zgYImi2#MJ+OOZSWF7PlQ>xSY*u$X~j9!=4*gqngVX`F*GJR11tNhuEj{k_GqoEUao zLu9}pg29B~;FiZQjLtHiOn`A9@clL6l&7glO=U5Y7nI3Q0>^71DM1^KV4A4yMF`KK zl8%G=O<>;(X~&IC`bShTIZTh@7Hf(U7@aIFtm86knJDeZqF|`3zYXY5WkfBC@+!_p z85dGH)&wpIDs_m}D2t@R1fxo}p99~VDq-NU4o7IBfDtk-2=-xOlkfOsKLse43Ma=>AhZN=gW%E%h% z=GS&|tjSJ}M1Y+f3G{4!4A39;OvVzx1?QN~hQQeA|A)ZLWc~l(8Fc3G?niTc;0FQL|9w9_``ua6_Z=TT^DV%> z{ofO>1iSTQ>&QMf)sO6BJA3y>IZwP2?CJvr_ObbXvK0Wk|3HC#?4=*s$M*Q{&sd&# zB{(J6rvm%fFx`s+;51>M3hZN#c`pipQwN~HKFoS#AG_y!Q2?Ay>{Ef0+yvkhW1k9~ z89YBG7oX;HD$8o@3ngE=N>{Ef0+yvnCWS8EI@&MoMfHsCIF`~`&8g0 zHvu@c*{1?0xe37O4p3kpXMQKU3BW1PJ{360O#n`d_Nl;0ZUS(s1Qggu|9-NY0GvMU zQ-PD*1mKivp9-AhCIF{dK!JS-J=slA-M0cKxd|$33hZNqJ93g0D6c87k7MQ|`?!<8 z`)h|pjbf7}24z~zBj;9GO~x$73A z^Iuq;UHI(6%NK-&iw;!|-LSBDm^*yc>;)@VA3AI1;|IULa_HdOd>=gc6#qE~+krRD zKI7myOSjFPzVz;;XUu&qaMsdb&hfu(=>o8u@WI)4A9$|+DT~yB#~*n3@>dRjdfA%0 zZb1P13GGAQT)1fE=7mMybLU?@ugzaPzp|WNK5Z^P|Ap;07moh|fo-xQja;#owc)Tz z45DD;5y9!E3+3x$sR$+W9YHUL;)KgcST>YAc-tCBeZZm#u4YhiDa6Odo)dME>^Q5B z;we5<%%w+7sRt51yX-hL^OSXtzRBdZj#cT$T-+WCxI9F>8aRXtYZ1h2#WbZ=F`b|j z>h*kYSm$W5%_4vwYF5WRA)o1&3NY@(OO1Gor!{0?B7&5(v>3!jc;C0zIT}fg>I#)` z2W+&~!5(Q}S(Uz$JaO-72D!Fkr232nboA)NLRT=of`9z z_OL(~Dl!GBMgqc$U5{RP@Ol|TRAbDn!*_zoVZWP>4?v|MtISI-jD;z?HyY$e^){VS zN^_rDF9W2KRjM{(xA+JOGPl=BwN9YrS}|EeRL|>G3XvK;4ib)2np<5j17*b!1R3cz zo7B)&xsr~Ql6fA7`c5!jjA}g0jp~G6ie_r_r>~a*(zDV4!LdXnWFu0mw)uP-Bg#%z z1$RbioyTpyNREkQG)yn$*E#sKVCt=ECxnGsI!LdMV(z$LfcBFbUXn}^(P*MmaB7hu zADCK~EJ=D$%+$jJJwqZ~jkj|(FHS7u==+Qz|tG+Y`g8q+D-joRXsYaIJ{LE?YjdKrovL9Nm_st*K`UPd*Tn?fD_RX)lY-f5gKZsnNnpJYu1PU6+AqYF8r0GI&8DwC58$qRT&qt;D{F+pyOnxO{9=a1c5tJKP{RX9+bK{-?i&yAmwPOkjoKcZQQQ& znGvDHP{J`>K_+`3`V&IS9J>{~nh714R$MI%G~8?DB20vDx@i_X=&6}QHpNw&`JADM z@mjIYq~rY%egIuBgK%mUBAUzu`+3>0g*=;yBr{wa;!=aIP4-}2sjIC{N}!^PH?7x0 z8y3}KjWe}=sH_MHCD<^%45|%#VJ*xSxpK1G@R|uDiKKj=SubPgw6%UM6U|0DZE&Gm zB1*%kGA8ofGPv+=bLpIim6A-OHl)GZt{2UG{CJ=GzxUI+t&A>AYhEjph_J&ko?t3; zD`k{QSvg$C5=1sQ9%2g4hbl<#&_(OEiVTBlsT_@KJ+@sCc_u$7g~5#&s}z-nV^JR! z@HL;GWBc)X)67o3XmF#whOV@o~=PbhG@vSrD>dpDv?B{-=>17luNisxt;P{ zQ5%U;M$vL&Efwu_^FoB+bp2p&T1HrrYR#~Y6oZ`G&5A+?O65X=YK};NU>WMR@*LSP z$JuIV@vCbbbu%e5MRq9HoNy^aH%s|sXIQT_LLD=icY?zN=o_R;s}oA64$ZD}=xx>n zH=O!qv*mVa+U;>>509sLHaLv73Y1%y@Cel$S^ebfeb==z3L`~J>R}>0jg{wtVa4=#FEsn?bq;U~0I7<7RgPuUQJ95Qen8OCE=a=c>h(Hk$4l`e?1hbrK)>>%> zpzOv%%~l5Mx$Pze5JBLen!p6Urd47^Iq?2<4#uqn?U9W}DMi&o6=uY=O*m$ChgBmw z?ocMh#Mr{1RSc&BD^m`X9Z~{OLg*FT|DEd`-F~xB zQOqiyFj_SfT(*$(MiWfH6o6bt4LT4MtfI+QgE4zPW1XV`vvC$~6ymv|1iE}$ggJrY za%f}>0N!e0r9&5TT$4x?qW+((bJQ@zvbe0mwiPpBI~}xP7*aiv?-U!IaF$Dq`dKYi zsAGC-=B{;)VlGwf@m*+y;BG9~oLsIDVV+VnJd+#3BGHAyakWgc?b^)x%%_o$rI-j= z;R|&UU@_;4Ia5s-VL9(+CD4+nsS*{hQ#Z4nT)wpScgdL<2o1+^wu ziwz{n6&8K357#j%VZlCx0Dv)p82*1br9?qEiN9qQux! z9CenF#YI~m=F@d_3NG33!){C1kM@X-a-p)cn?@#>1xs4&3&I3yJ3U$b6{|uutm1PXDX$-UI8PXPiCkAON%hYaTwoQdIk-3>aMWA z0*t=;nPwHg{pdMzxrGW8OxkmhqrPnPTo_yvVWD!VL@iLG;J0(xn$0#&x0fZ7 zP$kFDn~jCc_vrQz&cZC)#!JZlyVm-D+i%%AdF=4l_YS~+eDdcL-2ub_@jHb|_* z6Rn)4gA4O~&BFzLq^satxLpdw3N)9O^;?vQ4aa5CvvP1=oy@c3$QkEzU9{%jrpqI= zAo7^{k~giTbikEr)dZrBVLyX6+?#sfKcQ?smC8WS`q4u*wEC(YiY~qV*7nU9BkAh# zafuIJ=5wikkUke9mGCm3yF`N%4B}_O{VpK_oYA+y#rGbVALagFqTJ(up+wpJ?)Ru< ziSlXq!6ImgRBV+Ox5~&EbERBG>q3SJ1V=buclG|QYO0jeQwD~Q#Ps-~)d>rINTFM} zF`tw>R%U*i)<$#5Mr+oRMU3%$pfZI*Rq@lsO2%7Mta78e169 z;r>-MU-$><;W1*9mwEWJyQ<#5h61sTr90+lpzob;vOIB_t|lxgAUe z0fq*Bzl2x%o~A24NR^R;SqbAnN($*UGgGO~mT-;Yybh7k8WRvvVZ4fPt3r(24W)g< zM-P4Qg)1gpRll6xz8PaAJ(Le#va05he~>;GBN%;|&#l!xpEX_Ir>uh=>BjP+kA?d* zS+S=Y*qP-o^}&0V)`w6#B zd)wX%b|2r}OMGME`p!4+d@FZpb45OFbplM@4o&{W#wI^1`R(0|q0u*5U3;o4|9oRpcgw%tjG^22=9ASiu=aV6 zfd~7ujjf%JmPv$Co#nT=BvZdOz0IZ0wT0i1B>dY8RMX`|%%cZ1OHYjv>r9S|y(9%J*+<>K;FiA=U@{cn4Tp`|%%a zZ0+g&_+s^|n=v&0$uD`dj$-9PY48_3Foi{zzmA|{Ose6WNjD*KGpWJyP zu(lqqzq7Hmrys5ttCz*ef0XaMA*2%y*Wccl&iQbSk-Pckm)r?LI}n)t|I5`0T=%X! z_tijqrt&N2ca{;!t;BDA{3K&!Adc=VfZ_Gx^P3wRzQ>zmUnz zapyS6Wihg#>^t*_-d5th9}m44Cbc^=U~)a%{QAZw@0x95Bp8kEOrLtyzqYZdduE#$ zsWf1=@qo4UZ1bNtw)XV1&BfY!ang0;)vO^lovFv1;RXFbAyN{ohC z<7Dfad;I&y*wHs0{^eoq;OqDQe80K(oqIdGV&Xf10>^8%|Mm7h0DPQ3Z$4g`H@3Ew z&ttZ>jt;l?_MpS#IA0F;ECyF^sgAdexBsa7{PpiIy}zXDn_s*#LN+gdip^t(M>k_g zhJTdJI*5EVouiP5`E%iK>#rUZG`_gueDf<-laS=AKSuKKW{eM?2af2g`5uN$jp(<$ zG2a)j#uqyE;AU=fRr9!=dJvKvy?_1tzimwNWvfw068>1H?%#~@(RNPq?q&Dt?%WSK zs&8HY{@}(;U$N3Jy#Bp7-?AUa>)#8R4qhJqmi>+S-dt%R-`gMS+1;BlJ{ZsWer!R# zx?gughP}@Xf4}|5Epwkm*O~ z%*PIQ;(UdkGmTdAt2yq3?6_YIe|H~PtNgx6>IEz1#@5j}$D_Ty#}04C_<}vB7_IJC zQw+Ja`|IED+qf5BwvsQr@BN!GzI)Fp-sADC)VFM5NL!U6DUF0yaIyUI+T*M53YpfP z^B|@dKiR;9FNI9+n(+5-#`s)5XL|n@=IU>{wa1sVkS|4S%=hMM5c0jp<70etpYy%T zzshbDsBj{FO}C9*=%F>{0A*H)i^x)$2p1_jr5^ zA)|Aq=N^A0$F;|Q$+}O!~YA4kgOoarUA z;f8!yA>UWLGzMSs0^hwjx-TCG-}UVMvR@4OzI=OQzBgAw$oHPvJH{EtIp4cx?<@Ot zJ=5G=kIk1ZH|F`W6@OvGCgLcGob$YA_P&zodiH+tR@kF2e&7e&eIxcot8U2jp4mHw z;>S7D^V$1Kj_cX`1!lYU*oV5@(XQ z>|)i&X10*d-Pe?_bj=XpU700tasCKVcV`KFlmNIU^;D|B-B+ic+0xAUd^#6i&bsun zR8TG5o|%DSrOBcpEU>vv@EB@&f;pbgo!kr*PEy_FZOl<0K1+b8`mMgj3shkUTRudz zr^|HE9WP~9bt>pwWi;EH2kxv@C}LTuuETb#sOM3TQH3b7Fl+;j)LgZs0m)Pd8%j3e zDx|(yY3B8#gUeE&G;=p}yRbBKjM4tc-0srS%rTC@AB`Ku^}~b{DEVL6>DyRY&MU-=3yME|*F-<}y4VE=9;QZuvpO9+WMV$n}Ai^Zb^cpJ7Wg z&*I`OoRD&e?epfO0V=`Y=9zwDE|pYJ9^Nmp>4|}}^8#X)iOq77!bbgBQm))L@GuwnIY4PTxH@pEuprhqfN}PINYx3z=N% zc=N?LoogY}g$Qb*GM#RUqd6#zIZN92)7|M3)I|n5`^`QOlX+;z^XSqnGpbqR0@iF6 za>L;$okm4;FvpA8A=$~WqZF$S1F21o`Lw{L=Dv>=JVqu7o~H3e&~oO5R0GnLezVVu zHixRPO>A~2I(&5CcweTiI-%pewYME-llsUUFMYq`UE)gNKV5K+=n(B$a*p+e7}Nmp zbju9=V4YvQW@J5WZtQ(a?cF{BUF{Pe&sfhO4^Fge77dIm3X+M44l}(ygDV)wu}d`u z8EsOa(t@>UH_&lM()-jykM#na1;t8d6QHyPE5+fY zF$3yRx+8Hw&#i+`8%t)LhKY9Jql2-2d{W!0Z2iQ} z*56!v(J67`>w&Hxa%2DcuUvn}b^G|{(Vrjv*wI%Uz4qt}4nK7GlZRh*I58BlN2f2gY{a?D)+W)3~bL%JeU$uYj+7AHjzhB?`w!PUgw}h+V| z-`@SM8$W;ji?98Y;~(06!!EV^oW%PQ-*@`X#G4b1?Ux^)B|c~8k9K}=XLUN?>Fj*& z_MdJ4==R%B-@E;qlkYuw9MJolD?h2%vIjj#7t?8dHp$P)8a3fl4Aid3(g;g(MMf|( z-HIqLS<>cp&+toY?gcfwmLK9S(Q?o7wX_BTZ~ zl;$u!A$wGxuXH@Q=edlIvaRt%LF?5jx*U+XEU)r8@B)u-MhJV7f|GtvXcElABDLHg z%~IC5uFcu5Ip;}=PEExUL3%B9>uVx}mE_t;%T#I;MqvpQ%lYNuT<05jYdjj1JC&K%l#(;YrzJassQu#zp&)jVKn7Wfm9$o`ouvlJWiQzql~{D*6=7|t34;_n zrzYyrKaCJlkZ-q&kV2-?9ZHr`xERQ7#b~5ePN8*%oEsex)b|x|v4|k6}f*L%P`# z=&{bMHk5Y$E<%)wrln=bVadbIaSxOwXX^xzk*fMUZ9}>QgihJ933f*VYG)iFij!tP zU2hSEf?F@QCODYxjH!y@4Q)A8ait|y>_U+D8@<$FB0^*c6ECs`R%voIFiJCQmF#sp zVkb+20@uqNT^Gq^-jWcVNxXM05!Hk3MzUcOOQ2bq)ZLuc3W)R=9pzLZU+PeL1(phZ zR>qeQ<3uJPya~0Dip?VJ$(%HSUSg&XV?jzH2@xIQU5uTX6oK)Owg0o{2*~Yr8tg)v zq&ODxaLEoZe6F=KZf62JRXJ&kHNBDP3dmlh@zqPtx{YR*N-h!2tM(Dutb*#qWYR0< zxza$K<;wFQ1vfAj@wUG;vXQK^{iKDZi)yo>V^u+!w0j=VxXRU&b&jHX9-6k9qFI_t zd(S&ZqzuHVQ>r&B%!`vwvZd6DqHfmeY71>PTTP()m+!E!mlBTNuttc>QZj>1x(>l9 z77rUaBu$T&l}x2Ks0p&8j2b;-id(FcA1D4tgs{+@SYud$Yjsi{PRs(mFkH-LM7jiP z9#zB$-Kds*X5n_WPa=fbE0n6*7`04R>PbmKZWUnMubRTJ4;6HOl&f)Rq0^{X-p>Dd zjwocjCE3WkaC<;5g%da@fJN{9V!ti5{kndJhXK* zlQSthohi2q7-8ckk+OYiFr5%{5uF+1j@_Cw+kX_XlI(G2*=^XWHh_ zyp_f+vzO)zLD3}2Ls>nFG}pwwk;^pjBwB6uz%p1`LQQBwiNPGR2p|SLNn^A@57`A` zsat;?+2|LC<&Hf@aTPCS3POQPBJCcEn*v;zsZ({_gcIN55;+g;`vUyGA@;PLpqL@D3Q||O;<0ML{Lns*DEXv zST2_L>%%eQ)_we{|Z6_?IUOk(c!h!LCty`uO+L7fX3M-^Z(V{|&Q zv>f+Ad2cH5mup0?!Y+zK2o!P!Dei+@bzV#-lO$L)^ipdvSd3Mu&>ot}bh(f^y%r&q z4jWjGEi}iqOcLV;Fjs`Db5UoK#ktzgkOL!L?h9u2Jdwy+*L;31V4Z zV1vGa&x&jbtdDbr>$){L2zcxCVC#Hx$LUm+UZTtZDr0#XUNW6DRB1L#V>p8@P}N3? z0~K1N6-qey)#%wvfhB0dqB3bUK;Exh0-~OlI%zs}iEQFhb8c6?>uMrWkL{(T zF*5J=m#O5W*-}f(aw9*b=FM^11`2gHIiy`H%PtXqFh2R*=t)?3&Ixrwn#@R35R95q z9ugV800U(Wf`P0&I7gc2d2}qRCohT+SsB(PPhtC3vylQzg<2{5xTTKu9L!Opz$?MS z3e}@)mT(fySfah`z%|%LoMLS@1R|DocFBy|bUr1tM!MK2I*U=h11m0sT#FPOMYCnG zL@*o}dCl{B#$p(xF{SP~n$elIOD!WUbm3(!BUse6e+>5NKY8U|{qOxBL~nfVz%WcS zrv@*ja^q4SZ5R8zh2-UB0fkwbH-uS^)w)a5JM|(21&u1WmKD1}CaJSyk^v$roH$df zcx@mf0!lP7%Ihvi1bSMJ-rsnVnQECDJP4HH#8Y9**2hg(DwOJDpxBmdx9yrkFN|`R zJgr3t33z%Hu8eFZzbG^YRw*FitUW6`BNLjaOFPebN!a%Kf_IvY5O!Uw_qfqqC{!6K zu!!nh%ju=gqAM&hAP84(V7jN4UAOL?M%tsjdfTv);6otaw*t#;)rl;a&Gn=OmB|v; zn^wRl(uC?JJEV2`(#S^J1QMeqAE&Y+S1*^w9Lga^cSO)7O{iuvisf}%0h=26>gjXV z2(hF1V0+Os^Mjc?#A}`2s4e(3C|5c43Qa%1;FwH}?2;}v-j8Mmkp)Hk^lqlsYPDsO zFD_Djvu`089c++FbgA9!=qf^H>S|Wq`6(K6w0gtOodJ5zCBtuVd zY~0CMw%taHT&;)<5Y!ANW>K9X`=ba!&F7?x2gz1((J3YUQ7TJzQEQTI==u8GG8w3z zuA!yAg9?d%yGHbwQfo;!$coBSb3M(-S=P^(Dp?s#^2tuN#bwe+upA{hhPn2pDE{By zI$0e4tG&nn31rl!MI@cnlL8t<%#V$*CRJ^3TCA#auyd<_N4Y6ON1u461^*i%4(Er9ol0#&ov8)L|dA!I^ zM=1*C4Lpr*?2qB2Un0*LXQ~f9dUqwP*VE^0$GH^$$UHjt57MLKZnQ7l54gmU!k2lM z-t{NwChH|cfJb{;9~7s*XJris+cUVb=8;q=<$LMMTJX9AH9{uFxSg>&bUM%w0xK9Z z#2hr+3yR5+X@6d-;~n|o@4A$SbV;{cvm)V4GPi?3_gbY+ll7e3V3szsVv$DXc_CNn zE~qY?s&pu+GXl3=biGip%vPG$C)!9@r1DbvQC+?>pwUidnEbl`{K z_kVTk-CH-_bK`4n$Tw2g|L*!vU;nD>ufP6^(+{1#`}8YMUwiubCx3bJV<%sJ!k^r{ z_NUkW-L4XAXB_;_!FL`k z4v2$K+5gS`ckWL?HNTU+U)%fEJ$vtq_6~P{W%rwQC%ee*ZsI>CzCNKPGC)`0Jv(2s zBk!cP|8D!Ix4&xp^`I8vhXAdOexS1#ht>b=WtO#If=|NbRDnwoZPPE08mWw4X;wTL ztIRl!rHjKE+MyD^37x$t#2}#f5QweN_OPjrCgZvV;*}*Sz?cCdB|E5|%m#?=^Aul6 zC4K`sdtr!a)yOv6wWujyr&3zgs7>0-@+gB9yQW-+I~4{T_%CFr7Zk=J<^>@p3ol@7 zA#gscNwpzfm4_mz0F+ngx*IIX@j_Gv-DJ}+r#zB)FLd@fA*RA&*_5dfnL;Hsgxu2D z@A^2?p7dd&(rH(RkcXyfjZV^%)e!Ui5F^Goss457?6cQ)8m_0z=u!$TF;lXcP76gk zqb4zI?k`LbjlJZuuTn04H@I?Dvb4!)8wQup{2wxLua44=7x40r&{h1qjIrUm`Wk97vZlI`Z`5c3%!#ux+r?qXXdjj>&27^&RKjZBRaOI@nZl$Cm#oedTUto9~S z;+LSaPY*GS0yYu!tOl38R5Ndl2L&ciHb-4quj-3TvQ9fg!ZpZwsfdJ_PYW^Vye-vK zSd$Pp=NNNyrd4foJPo?kD947QKXTzG!5ZHV;aMi>wLT8^6ViuaJxOlZLsa2A95IEN^P5t6xBw5*ZcaF3$ zRY@vIK@;2K#D9d&ZiJZ1Sme`9RFj6KYKw%cMqkY;>X5@Z$WjNEJ7tP=Tdi7i#7q1F zbap+&6zeE7k!)N|(Ne+Xdz9>`l?4dgbu>!SGP<3-*OqHUDkLK8TK_z|;Iy(t5GeKDpI<2$yZr&TrtyW>M zv^zN@Il#JRu|J)b{LEsZfw`GUyazhF7GfBxnXJ+*G%#gM9s$R!l`$}EC7PZHR<}YE z1guq+wAl=(5OW-229;scAA1-|Rz<29v?@K5>nsQ(ZwjqsDU%b8lx>1rMLmp6{J+rI zQHT*{BVuSH{-CDAeG`$)lvi~-{=~$p7P~0)v63d`%<{Zb5kkyih;gS2vW%+OqENJF z#o}PZ<(dxGfmC~_DP*V8o3v6jdjY`75hCKnbUCBYEI~B)?pMuU3AqIkn0@Um_^Zr2h z#9m|Oan)R?IC_6*|o8yOc3LRGfQer zZdqq5Q>7g!Ty{#?dM=gt3FvHljfru==H1ZQR*2zv2J6Z$VS(^U&%iyn-DhgXjH0Op zuY#Jf9h7UgGt=SN@DlHaR!@W&*J@zkeo@{D3{YFJ5pZGw9uaM?lXbuWSkgd-i($6c zu8yXu#D9QRkB1o9>^p3MtHPcO(|wPk>vbK>8nj-dq#?}}ahOU&6A;CgkP!2x5HmKU z4$+VeepYG}6t;jn_SCa0xjMMIQB4L^Yse|ZG@&u3pZNFC>Wv`=szVb5T8`|Ag{D*% zsxzr6sJoey3~jh1j6oIL(pT!8z)B+_<_#exZQI30V}?Nu$IkC6k07p46CCI zmo1`l*(+p=sis`aDn7W(FT(K9m)xw*ml}*S8N(u-_z`I3hnN@w#fkq1TFpa@CZXBN zoYXRCN32hCIjLF7d&vf01!bZdITQqjUC5dB=j9re_;=817Gh%P5r>#*h=Cyv&d|vr zQ%TNAKASJL=2(rgds>Br+n`>l)bI8S`8)x^OyY;2l^0?PIRxbmwCMSzAWMtgWW8#@ z)diWdO8(fX)etY}7^LKZdyOH+4KXp~i$jdF#%!)XocKX#Wrvs;Lc$^n0Kwhmf|Ioli_Li&TRMiHcBMf*8x%&%USK8O1+B&*CWhW_h#7^L z7}{2e?}Ju)h>0OJ6=JkCx6S3A68{ETsUaqY`cjA)hL{+dw~7A?S}7qWh89ucd!dya zVq)k5CB6q*Ng*bNPHf`4q19mhJe!NYCcX<=^+QYy4V%PwLaWz@m>6m@iGK~PdLbr; zqD$gCpw;U_Objv9#J5AM$3jdDos+~nq19_cObp?X#J54K*Mu0CqQNIl04L#Q-{b^# zCZO$toSo7JDAg&B&^#%+Ib`4q%oyD6S&1PghF(YFUqUM(#KZ_%CcYI~@gXM0VRzzN zpj9`-#5jpdyd(Vn|D41tw{CppjnBFMgV#U*^hZvalmB`$KiR+bj%%NK{B6gtKKg|t z+-qL!CcSj75B7J9lTbzxnh| z8JPOK$38R8+P|@>=vv8xQb8{Y@`WAHzf%hNfG&<9#kVn^DC*EhZxSPFe3#?EnLOPBe^tAM@GH)4b)H@0`TZ+y{t55~Cjw6UEFzVW{S zQ=xCfxPY;-si*gii@DH|Z+v0M2Xt`^>4c5>M809%DF7Scu!$jVzOhL8{lmt-g8+Lk zfB9A%sq&5OT`G#M+{pox;Yc;+O&QcOUNj{WM)*2Gbb=;gRg%@YET=ooPOhROy;-G< z^47*CKQ>ZFce21v0obWvij!hR5AtSnkZn2#N8^So<2{J#wrnV1mZUYuYqOS%jyJXg zuAf<^4$9y4)(@%P$pB;FE-glV?v0IIa0?ii3f&?`sqT$UJ-u5%UwH2=(a0^*As_JQ zF-nbZ%qMaS`wj$bgu|3;J1psgyE2%>;F;-&Y}FbdLQ?nYorO7qI$}wq`fRQScQY@p z5Bp8BR2`WeqQmy*fi<~6|9r`~beI}rJ_w66c{^>4jSpZ@jfzc?+Q{MpIZp5WL1 z_}V|a_Ql8VKmN+&!qM*?z3nJ-_*;i}4sRd)+QFL-zF_~C_ushx(!HPG^Y>n~`!l=l z?q?_7oiGxgwezDp+RmqL|IoIyeFB<(^gry`jolqa**$)-K4cbUqmD}LcB#zKqoIDG zuWvG&N3++5EZHfT0Rz>7UQT{nBUL9;PpV(i(;y4iybnvD@QW6!Rwo88}_*%*;E z_Uw4w?A`{=#t6Q#XGiO1cdxD@d9$~9e2zUkTsNE8pxGE9JofBh-R#Z=&BloGv1j}1 zX16zJHbx+jJ=2cu;(uaRsdzM%?`;iTrjS+EV&vw?${*Mis zjS-Y&&$idi{@n)6#)w<8XItxL|8|3BV}v%@)f4MxH>lMbC;G{*9$z>6;j2qq#tD$J zt2eEi-Js-aoER#*dgHp;zuACoj4&&^dc(TezuutP7?D_Z6|9@xp!{r{U@f~^uAAMU z&}^J~vg~THZgzuWvT;Jb?8;v^yFuaDI8kACHD5QoL50{j#a`LfY~Abzm0sh-mf6*G z-Ryt6`amyE_?caK>t;777#ruXoL#x=Wk(taY;+)SQiTJkPGob+a2(myHupXII9$*$pbl#yR3=SCe(K8&r#p6P9OJ<8`we zl!c9Rde5##>t;77?;7V2o?YqdW;ZD38YlkGuC#Tt8x(GhbI8uF)OE8P6laZdmd>t* z>t;77#2V+K0lQMx&2CU}HO?_PyOP(`P$)Id0eATQzq|d$t&_%) zwD$q%0u9Pvox{+SaZa3K`pLuvW@mbgPeQ*e+H}J7~!4do= z$s&bLmhKW7i9+2}mKl0T=i#e{kVu9^+xBoWzC8*o9BM3OO!sejlgYABugXBKkWZlu zCbV+)tPnVDy${Q^SyL1mNQ)x*Ou^DgGgDJZJCDPeAS-ni{mmqnk?_%h;{iGG*M*K3 z%8ADqu8+*|p!-vuLr=IM)^Uk*L_x4;$vGZYPW-;H8|K13RkPxml@mwcOzwd~9XP~f zD5pVn>s4XC;9E|Kaz%Lo2lM8Pxt#%$X$vWnlTeux9$FZnEmWbxuzD+#Dh|`=qUaRL z>@BsNGnF}N6$gWH-kEVsP7nH3?$&~(anl?ax3Q%__uG@~a?$WgQrbv817rPFZ@;!3=PTsu>k4nUbO)Qde`r7Rx*Rd?eqX+3#cg1tH7joB z{(U!hiwr;BW${*LDrpfRISuiyx|CQN(w72 zW_eT#NYl!+MoYNv8;$+|8;@_d{q~~YSq>#0PL;^i3>~_7TB`>EH;`yzBOMOp{LwGb z;fOQ1&h!6Y5a$0s4l@4(kn=wW+5Rrb^EW|;{|Myv4?$M{0Oa$(4rKC+Acvm=+50V! zw_gVt`xTI@U)(fH|C#%&eqwdBrzp%}ob1CmS%+5#)1FuzMhs#^7USeFZo=RoVh|$; z87Cuh69)ScgBbD3IQf;EFxZP2#0YK1iC1pIU^ikABcC%)4D#w2;S;Mw#2`j?Xq>Fj zO&IJ%3}OU9^F^G{veKSVDcoPQx`CHgLBRfuPc@qZnh(U~mvN&Pr zO&H7~1~Ib5<3y}CVK9vt#7K3E6Wrc}ffq4|kpvef4!#KkH)0SYFFsDld=myv#2`kd zTAVQW)hBmP+_57DF*4ZVM9eo~U_}gKJ3jsPEqd#xwyvL?lBb`3^1CPBbMmH>>dEuAfBf3}uYLcu zw_Ibcz3}*h$3J}h*5mH+OOO8I=*N$~^5}I(uQ>eM!=E|)=ZDJmFFxEp_=VH=9(=>W zc>BqN!ok7*f7<_+eP{pG`=@)qvHcf2-Mw!=^+ARJx%1M!Pv8CB-S63b({+2dy8FDu z`xD=vcuRswym04(J3qYh*6qi(Z(V=K)`z!#>JYy1^B=232nb&Nvo3tV<8-0hfstu7 zH6VyNC(&+o?#kFmvIXAAYGbR*H{B?o=SPTKD%I`7Xm(oFI{hNkSdP2>u`M;~V}wO&5YDh>#|Us6v-Yk> zh^!{EaE;2jm1#bQ($=KQk)ol?=*V1Tc%)Y*#a5j`?YXrRl@jUlgaV=2pl;|~yIOB_ zs%dW$)XDw=#qmLoX_m>CRNI7GY%kg9D^tU3!NQS6pPHa?z2Ai!YNv8!( zQVStHPa3Gvktu?1Iz+8qu`<^xYXqO=vt~IDXPjh!ktnA(r&Um(vB%(@w9%05abZNd z%v5Wz=s{F1kDghUP?>S168_#03p^9H0W|D}vjvIGMw!AQKM>D#mg~HZ`{Wv?{kXVqTR+ z7l(v$fw1j4()F9oCPFDGAWRfWaPTRQ5YQzC;iDwvIJ#`kh4N%P>a|IaWqE!g=bdb} z%M2FQ&XW-$Nmf8T6G$sspoq~d@3h>(xRP%QgBi_AL(w*-@;Gz+O>0DV*)$Yz zh?&*XO-vqCC#=GYIofEHMM{{et(=iXF}ymR;^V!jG+1|0CuSo?Yq4Z8gP;vIkMfc} z;=7`RNm`pk(khkgBg9l^_TL`a@B^b`cJn20Qd(9ku2jyEBGM%LoYyrwjIF5C@^YMo zVSzgQk_a(_ibX6nE9+!?vaFB|$Ze1o?7A7q>!?G%iZnf~((Bd`bbAsZTnYlENql!& zFJjrnxL6(4vCQaJji1vE)2%yGY0S27rP>SY_~jA8(E^oCW^vQ?8%&wOTQkST5Tb*Q zEoe|d;bMtmFm<*_Gl#!&jz|ZML5rT0Ce`JVp%%h~PKl-hHN{aIr>X*OGY~aaq?vUM zi4Zmq+xE2HtBH&p%oR}AFi__GWZO;U?D?3`r*dJGt`%kyvj5K_gjvS2mgiees*rL5 z9?xj%7)0TB9wF~XEBk*QGV8RvCagemkoW|=n@Lo_cD7?S-Y;* zuVwW7v}3~u|RF*{5S{75+#D@$@^1TW9OijUAfM1_`m@5L3tI0Y9=-uN|c}s zTP$aK1g2(oe>p;k?gR<&u2xd}O4`@UEg+vc71KhyF(y?G@=c5$*mBRc)#KGU!s?*) zRA+zr3gt?QePRq0L&if)8f{1tExr27);=EU<%XT57K)H!rC`mPzV!8!pY&5bR zCyyk4Jwou*kf_iiS%MZpRsf=$sX>6usw!E^lj+4$Q1xmjpYdSZ+TM;3UEdjsL$p^_ z=FnJPHn|ETPK&*|j!?;r0nYVP%SKyC+LU$h6k^IES)Xn8#}eGBE@6<0=r=~lFolQ{ ztl3MuW|E<}ni&ZD=Y7>_wlzjfj$27)qBfQs2A)@I%~ddwG`M;TRz$JP=ZC1q9LyqC zO%0T$9D7T8DKsRKmir?qGtQ{_5z+OU4P3T#mCO5GP=4CZ^R5+-BH1in}uq^pjoOD9p$r*3_LnG111kJ_~som&ii5ez0xAIXAWOqRU# zIEf9iV|30~2m28#dckJ|x;2{$seHQOHZ3aOPI(ifX*!+^s*1QhlTir8UUUwAGD1|G zw8eBynvr3npri@BF)d_#9hss|)uxw(Fyl)(sG;VV{qJ8_uk4olvmPiJo;8v7JZJfh zb}8t{LsC!oxTKq%78)sP!3uZ>8b2PHDD@0}0tu|B=@tXTGiN$W_d(^i5?h@Pn*p72 zGr3x?4(aq!IYJc4w1jlBb*cpxFJU3LFxi2V#oB^}%6VmE(i4W5=|X=h?7T7}MK;sroI5PcmRhP1nIN!kv8y-cMJkZto&%&x3q1zo zvsKRxG&{*x`vZ;60O?eJ_ZK1rUdE{r#`b42Gw-sd?c=HJ-0FE^F++}log+xKK{qRw-6E0l5OIhxm&bcPPzL6ddNV-XawS77JOrf^QF*K`23gZOTt(50 zRiKjfzN3jNu%-xtBJ^lUc7pD}Axr z!J$IH)_X;prKSyy+!rE5zE-j;*@jk>Wz9{gK|}I%WJ%Z@TXUw1N|2NsswXt&3qSFy z2$5?e?aH!9)ogzt7>hxckv*h6%}xe*S19Gp)L3)rPD|;g4nF4`v9WOR?(-uA5Ta#? zG`R#?$5j=^Qbt{Iip|p0ssS}sK0Qb##X2#81Kc}^O1pK_BvsVNhG1~Cjy1B$VFq!g zdRb5{u!YGr+hSew3${KVdwbDdshbA!zLH;_E;`tvt>i(?GJB3sb5dWOCxQMj zC{r9P)Ptx9XBX=0I>k=C8li)ImN(Y)4A94ww0>5wCD@gaOxdee8#Ki1pGD7+Ec4~o zvMlF)x7F;ptSZsGuxzCyi=K+@RCz2a{E;B=<9#N6lZjH?>9Z>GM-Dr%Rtkg7mKozyB zkge2SG}<|*?P-mkJr%XZSex1e%r~cPteDidf~No{{)WW&tP3C2OS ze6x#*7UwW^vsv&E8M*sc=ZG>}0~MPm2+ez@oCay(++aLv;+Tt!K& z=Udi@u0fz+7m_1%4okYI5m@!cc-AP+Gv-8glOmiJXcJWM=0xWDH-K*-=<4sFyZ$0F z(PgxmKA4e98>XA3R^W5UK**1)8J(7sLw_-)gjT&a)%*VSsLm%h!2_p|Z<@vI2A;_JmDj`q0 zvnnDOL_6d4D+rrZETO^8(x4*pq|T6jB`=co+>}`o)Zqss8>B?G#t1dzGP)V`X5;{G zkzGoex)Yel<0&;eMNp~Q(P7~r+Pd4`r~s5^hKqip~FwzTzo0RG^g$G>uG7X;_yM0Or+4Ce7CpNLH$ zhIH%IA(+RXe0)v7e70ghk3adQHGwz*m>37tS0A`O{^T2D6L_RbWsg7khS&sR9FT9) z1Hqa=ocK?S^YKjxEMpUR#IIlxo4_M}1^${qobb~leg*T`1Rn7#n8haWh+n~UO(0JE z?Ge8MFE)Wk{0iLI1Rn7#aMlE3%#AAfSZCJ-k893x10b@=u1Cr7afJks2e$Dh<=6NnM0-J}P!HGw!0 z@JIX#)Yt?b@hcd{Ch&+~fwCqL=L*0heg$%D0+09=NU;e#;#V+O6Nu61qrC&M&i~)} z=UXRlJ=)%T3;2&u{(Q0rp8XyGIg|2JIg^ic>v17jIjk>wnKZ&B1)o2joXMb$*-N`Z zfCw}uH>BF6dqJi$%&fXvh~T2O`^A*4hs~M1uid-Tx}M>j$wC^+gNMH~G5=Od4xCP% zaTW4b-nJGrN+LDSQQ-WrQz!EZ)2#LD$V2B$qD8e_p5!uZNxvniChXL1se&%{ zQv<9cOVWHg2)csYQuX>Q%Q`{BP3xc*WOa$^s>2CxRuVC#-tw{yG+6W}GZHP;P!!Z! z54-T`ORys0qlbz4!Ac6p`j;oR<7oI?y~VmP)*mDTa%m~#7z=m&u0QEt(z@y1NMSCrCEr9txoRG0=AIh04bF6hMAvOPM42boPNMl(<(#oZHr| z8CHfyu#uUNw`Lw)9N7ZZc0>^Ym!jlqo7Xim_0Zl|N@s6d3~xd?Ub1iz!)LfUWZHvS ztEA*$I5*Io0jp({bfL8XF)6%-4;^HT1g@lLK~q(3RrR_)DV0R7Z!T=^HaEaEe6tkF z@X^DJ<=xd_?R}sSd5kgj$h4eSLefsd6fc$WO{qXd8))JyEy zV4_R+EPvsiWi3?lU`>IL`NJv*WV7i9Za#;!giZNdIS=pHRDp&v8C`|ueWT@D@B-s* zA$m8+3}=FkJ-jNEAo`*T-Rf85QoY=E7xQWn3Z|T2g@oBsu$_uk*M>&hhjhsl*%l=( z%|_E4bhs&Gv}_bwHl+E;pn|k#FS5aOR2jMsy4msjIDJS23^2Wo|!AB0>bMUnX+ClDM_vj0@e(LBePTzU>p~Ig%K0D^NK791r zqaVBWu4`XTq!F1weJ+hql%iAIx&LQlC0Y zqOif|=mi9!WmsjPH8`x8r}I$1ElsNVVp=7Ka|;4#0^?8r><9sF0#|A!rqgZLr$!4+ zF#*o{cr!_u0`K8+p@8Le+=j?mX15d}nw7~^fO{g-;}BPo-S)I0*I~Px9I6weYO^*v z7+^tnk{a*r#70CnKFtQ(V~UkjPp(x5buTxW2i+3RdGMg);nFyVX%L)58-p5cJ@;=T z8#xzOTGd=*R-(0$MX6Mf$$=X*yq~YPdtQNRp-Gwwa4nNMO~+=Hh!9!KRynMVDO4ph z>s2ICmI827UdC%g7W2klv+dcgZ5G@9K{qxlI6}1AMUsQqa>0itv+6{17t)+7+T?_q zXtnHI=9*1G!!pto{jBJkj~N5;xv^Pg)(FGvEPH93EO|Y%XwDS8Rp~Re9K)7y1Y7T;KMn% zir5oTI!I%B4hRO3+mBuQgRR>eINno#5*grp%xcugcBZmw~^FbA(qh1&L$&(Qx zm6W7{aj0hHJ4bx)ea<9mMKIVxARtj$|=_^m#N)9i;eix*oglY8}TP=gb`%s zHA(EtX>veHqCs&~dt9v76ndr_C{f4~Jvyb!tWhPm|0Xu#uOo!t)r7uiw?^LB5{pw0 z+>1u4gzithB?k+>GV0hV7OmIjsl9&@A!3Y@D9H!h&Kj8Lpfca4WpC?!Ya4M8zyB0s(<{`h-Ju}x6&oM-0h|@Ep_AkIbw{GKR9Q#`4%I}7x>u5=jv@tblu#H zv5lc3Rxms)Wa>Dp=BqUlBS6j~+-lV=V_X?pvqH|vvsk%1>UWo&!}BMPfrze9n=#%; zl<*M;u@^lD?~*N(;kk6)%M7^6C?gbgWTvLckzbbyl!y9LhfX_%YHIfzVk5pjLUf(F zQyscI(Jf|VqgNZ}%hs@!T%u{2C4%NKUkj4x2qN=-;;SP>j1Am}BSft|?o9|(s%&7JM#on8M zxw2N}!kwOWckc;M5gdTV2Ex&0stO?_l}e?Os#N9)xO$HK1O=QB z5pe>=S=7TMsGK950gzEf51@jg7q8>J_gDX`*ZY5!RG;py?ww9H4IG|_oaf2uovJl_ z@3+=Btg3IVAca@`@ekwnfl@L|9Lx-k!p?2lZ9lZp)+ku zk^SK=D{>gYrO}{S@|W$ji1I^1k9>>0GDUMvu|j|R?-|7kH3BdF(aIF}tW0tDyd4j1 ziOwGx=0{ho@u1%xFlZnMhX+E8t`ZVnl2{xZpGd&X_Jl4GbPH_=EF|naFqaVtEBQjC zpUV~7L?qjlh%_3e{3-#p5?whG*T<~VWq3B*Yg}^2k`*Z25pD$vciXkX=(7tf{5=mJ zE5ChB=KZn4GJk#;e3d?b&)!aXEdx^c&G-0Q@O=Ni{mk8ac7OQN=Jua}yZ~-YU~{te zFKhRLz56?TFZU_F@4WKHs}ElN)eU+lu>KNf=YQpeSBTBb=A$?M>dIp-|Mk`**7@}( z`u=IYaUma}!0*E_+ z$ityd=OH-5i3G>FB6XFAdQqc2V$5JrY!P}HY}&?UDPr__qR47ZTNwqZZ=4@nU4>#i zmjTWYhhU=DX$%otN||l098K3F{nia6)n-GP0g=p$&4Pe7@kh=*mZZlt4TT`BSq-Gs zpan|9^EA7@*gR?v3u)x@xpcLz6>{BPx4_l<9YX82 zqYyZB7VF55ou6(ZJM~~W+bwipUduMEn%-=ND`p8P%i!oMMhn5VE)j!36YfSH8=GU* zh@|^%|G-a0c<>|uZfFtALi>J@4nfo*I}1A2wpw|Zu0PiY6 z0`K)|5r0Ll8h$L4Y7IDgQi&!)im5-QvNDCbGDUe#K`Lml90-Gxm=atfhy5_7=OWc& z$5M+WUJWDl5=s!ctd>eWW@BZFc>s=FLB>1)M^@v3axESjw_(auCsw}IjQZ_PwCOKY zT2wPRK+8_B4FviUkAA{jE0oA(8e&5UrlZY9Av4hHp$3^M_`8`x7qZHYDgtFIveb;= zmp(VAfEkk=a9VgkB{LxigL#}S4hiDNs8a4^(PxG-DIBrpa)lT)a}Nh0@^oADoOC zDLFENjSR>hviqEsDHNAtdAs`joMMGhboFs}9>F0C7D!2x2f556Ajbnhv^FTmaWurV zjA)%=nM6`2TYYTM@iW(&Yiquzoi~Gj{rVh%c@M7OQS&GfxdO%inagO?;X$BJ2vl<< z`&&j2_s!!mWGg+AbiPJL@ky=0XC$6)4{8Q14Wvo6 zo44V55pMRvk%XXb&Uam_P*`)VR**4I9zz=W0!ThlC3-f`2}z^D)T>BaXw+i5U}(W$ zuCB@&*A7PNflEI^wZU3h?sP<89kNLX*ndy zJO4PBu>!?E%qip%LJZVi(yFAJ$x2VDl=xz@I*jz=Af`HsS4VuVVfu|`UBCMDl_}VE{JP7d%caXtyu7vdw|ful{rcW??|FOa zy`A0vzWd?b-vXHgtzC52zw-|}AK7{9&MS8YJKWB9?R>+fk6n7(rJuPpzVyA9!k4~n z`?K5kY`L#v7>ZmqAaZM^F6-@act{Ojxgf7l}MJzMKfdG7kgUt3RiUd?^_ zyc)@~OHI4!4+VnBU?31G1{1+}Am9%Ly+waEz6iSfSy$ILe!?X`{hr^(PkzhR`b}`l zE8SZjdKIa=@v6sfttY{yA9pW-H*@W#p_a^I$uvscji30ot@SXt{$uX-<2493Ug>K7 zqwd9png;@&g8&;p{%Fu}!23rIWp}hvPuuu0w?RMb{(MG*e$>6?6}P_SN8C2Q+`Z*k zn}67C)63jTC))H1w@p9fUR>0sU^3}#)63m9{h&)eFD$k3GWX{%oc#H}xx)Tx31OjN z$Sdq`T=KuXJ@UVHarnGTe*7)%jW4(mf6l#lMlbx8d&^(k`j)?Rd*RRBTaNX@=UwT4 z=3Y9{3!iga_F4DhqFx9EgI-|%!iD=YF1gzaf9`Vq^io_;+3TOV8b9cgpTO(0E*O96 zUOWS@&$zdI>ejb>+QsXW?k&ggdeD{r3HQ5Mc54kP-WB0~0TJ}L#{r9=IoIvIS zF8zCNP5*uunfJK297E=hUDgk{mrfw_J{Ot$-HVHniF=WGuZzrmF1d@$dt7AhJ;^nE zYGiuAZP~ls8_z)Iepmf>xwo7^=02DHk8VwWuZzq*?k&fVdAG~@Zuim&WZvZ>^G^5T zVq{`oWd6uS<_}$R7nyrpWd7hJ*YK&K>29}Wzwh371~Tt-)qjV3%L!!u(4~L-t?B>3 zMdtV1TaF>~`!4IZxtC5L^9~o8x4IV>BNO!^^L7`Rx47gkGQZ~{^X8LW!>5MVx4A8Q zlY8SC$h_56|1S5I6Ue;9rT^Vq)4$n8=6Bp%jv@0Vm-TPEmrfvamy66B-HVHnIW;`~ zu8YiXx#TV~zvGJhP50*$t@&-YHNW9rJOhn4x~=(j_m*R=`7M|Auep~_wB|S6*8Hk_ zadB%-je5V~w&qt{aVjZN%Y?C-C_d_u?1`7vGSH7+tgc_@3nVfZPx z*{^bcKD*hkcFzBQ#~QbG{im;AzV`FiqF3L2wQ%JNSF9_Sz|Q^3-s|@QyZ7$0I}h&k zE`0&)$ggkz>~?VL?yWS)89JCUp{>bWSn1a*? zaASn`r#VMTP}_L_zENdH8IY?~<+yZ4r0P}UY=GT1pIWXRFnHciQhc!7tiY8zM^>{L zm@jrziZW%pKu{F21zPvVOUpg&kOy+Vj#gWVVL5ArOU;DZ40Oqk zFf^MmY4f9G2~OA4STi#a2-%oYK%!$x?^{~p@iFg!31yIQ8G{7IjEW_d2HAHrfdttZ z#;B0V)hl{1PnSf31y5-0Y1VDsdzY4VM(Y&R$i<`WLUxh~)p7`>u_LkAH2uS5&7UUw zdIwbkNCUiD;cw_u5=guWTKAr%B_0hr2eiM|)>C{hob_9Av=$E5iJq7aW<&n*Bo+V< z(v>I$qVQ&&)Qu_O;`qSQl8%p22dz;$B?Q@WB+O))fP@xXYQ9)yq+$Uz%tWqbmeD|} zOcZ)b1Gpp(ggGvG|I)%1yQFs|eh>^-7Owewb{ zSd?1rk*-cNjy3?~`@o_|eM2?H} zstv1aPcb0ZamjZtE%ywgtu#w*J}HvnZYNBYx;RDCMiuQ)28OFuO2m*b9txTR*j5w9 zGzBC&F8QvdC7y7}ARROl!EwE1+E75}gESN9RpeGowQAKdYL&?}MrpJ#u|o-ain^`) zqork?(Yh6ivg%U=NOWBCo~0!obIBEouNqU>#qsW?C7p1|70RpvmxO>Y$0gsnw6Mi4 z>D>aZQpr`FKHJd-fc%F`Yva0PLmrMhV&C7hVE%+&AhdhJwT2 z&`@9vUL++h437G-?V?to>4KECY-9S}K%(Q4?^s&m371@hg)~)cS zzdrphK%(Q4zqho+V=lQu^-*K`3>U|@EiLJUORi976u4v(2ywx`rS(~yhf zZ!azBgiEfFWEHq%5D0Tz@{LOiTkMkFpw}vST-9m7(FTD0TT5%>y5tHO)8_mC*R6qi zd)KeM;o5g!{lHcA%Krp403Un#<(Hqh_l`Yc_b+zqyW2bCo$Hr=e0gw|0a#jdGEG_4_P2RCP{SF}NDbHP7 zCA6@#sKplPjc$~tPjqwv&0Zk@v9vC8d+bj49T~y3Re}vmZ`R!Qo{DT((gpI)YAi~K7Blp=NQlmL5HQ~9iJSm>Eqm%tq^QjTGHv3Y13~9@*EdjA>^>MyyGtT zZ7zZ}mTp^UOw5Y`{=#6HSrr+Y|0^nUC;IOnVhc5V7M+R`g6@m*( zZ_W-~&>OY4rjG$q9T!|7z_7H`(}t=~zZuALTyTXT!_x9jy5OVTmaPz6SX$EQmTA*( z0`eReTp`4;w7lak_>C@tD})!8mg8~3Zvdhk7vv3y5u+GX$#j!ZERe5YQEkW%k_tHV zIaE7QLrUkEwh2x&Q%j3l?1J9tMQQpdM;8F^3IT?tbvbmwM>;Zq3)0n=(3feIB;+DK zO7Md!ThT#D#Dp&Sb0dhUB0N#0p#drR5!WiK{MxD{SwVmg8}WD?pUv5-Ze-T3Xa%m+(dpO4G}Z zE&$#YHuy{Ha_AC!jtt-uE0m^MdNXwB65hyxHQfbL9YbAVL%+1t(}t=~cYr)+R$5_G zzqGuQE_lgp*$SKar6rwinKs=9@*EdjVN1WXyyGsoNjdzI+KvlVM;8K*E{X@maQ+Z}uC+#A-1ug>6WBP>6Fk3Z+mCDRrWw!XVHY zXQQyjcW*mCcMig9NJ8q!@oFHMpL9!5Q-_Vxgo+M2xD-TcRN;o2(PN{2OjXz?!GaaOnWVA`tS^%tf7Wy-FK}!VSwW=+OYbF2VARI?!C zsuWG)ZR)0NbwCAX&g@6&=r~J`x<~@5@N#vKwR+@WxLji3S<`{vy|g~9I{CPNMD-o4 zU`!X}cgbY(EVo->9dt4C*{TzwC%^XbrKO|L@hTHvAxG+WwN4i}(F>k($4BeGOy=a!@*dMb=dx}CQPK>D2psN%R5bXX8Hb9n`QqW`!06lwbU%1OeVh6 z(Z-oWXBP61e>`Wk<@Xtr@O-!URT?+u!n-x21?>Nc&UEZ3QZc$Mvov;H{)uNF#|=cB zTjzdS>~~DSpxHM~$6F5_Ds%|ft@HXXJzN1+x0(Mx83+a(P0n;YHlDc|gIaj2dLw%i z;`kee$PS~a25XeV8KkY_(S((Ru_P_ax4pcMRO?|nG;Fq8$zC}agsnOS_wDeFTm`4Q z$XG3B$6UXz=Ma+WC*rYCw-AwXZHa>-Dgi>o@m>WI1C^l~Pz!~?oHA6&ciOWV)deDK<9uf{HaWY64nawvZN^K}Hij=pvPIdda zFIFG#J9{1po?7-&C^V8oqk$a40;NK-kEdsUcT4Hyq=MR!910DQIy9tsx}~JWekE;M zrGhxd(>+sT>>-7whH}Vk;OUB4HlQSqrbxhZB3s~o1P-bpaV$C4(SXjRv3@xe=}~oP zl;cx?mZ(y^KB6bkFbn=oqpfZ^MAypc+CU96u^g%oDQG4mG?S5GqiR~v^Bi20L$s-e zDM!|rD$Ha7-qE_k0UeNNkU?luOAmuekVRCql_JCP>|Uh_?hQq2m5ex6IJlhR_>`Iu zjkp@@79l$ria$L7{tFh)1KE2{^5FzBHMt;= zK_@YdC@crqTsdq8)Ue%CGWJ;@CupHuDiA3#G*Kaif%R;ZsV&@;;FU&;oOP>_ zN&y-C8NHG*hqKWjjnzSRw~0D*MiNjnU^ZGs2W5*kol%KRi7j>l)hR^jmHAeXUbf-`&a467NklZXhL7u2bC5YU?J4Jh?0v=W z;RG^$QPUn8>=YWph@H@d+@uvQo6(+{j+|l1Xe;6GO<;{0QPe0JiBxn!$SH+t*hH*S zIBb#SfXo;}KH3Tok49wqJdnLFaXqX+zNl$0jR$yjyQ1Q)ueD}T;cPgXvEZUk7^w7F>yF)wbYm# z^yBS(9%Ea9ES4ZuqCm%M^+qs6!9CgNi9};x63S>lj~Wy&5ehnn@~T&dKRVJOUkwd2 zN;}I&1JxK_w@0FoFe`F1+YF_HEtr)Be92P5giq6v^Q8L@ZjuE96ZHjg&LZ77@LnG`i87S-YS&snIZ3H4}+_63>9G z(4@hl9b#yVhr@|ShuzeX4pz9+B$H*rs=;GDlEGLqXIGPx5XcKZNXxvKs+6n&X;ie~ zK`^1233(C?H*-BEF^TI)7YYPvWEAO>RlWtQQD#E5GPOWP*r$m~L#Db7W{9`&f>~%Z zJvv#JG$(OtglZ;1)?e`z)fGI{5 zgSa%7#-u>i47Z>-G!iOonHb4QziyRd4Z$`Pu>by7AFN^6-QSUN;PFoq)` zv8*$_d{$?nOf2nhsnkKE?GHpE!>X>g)XEsi!M$=eQ))I70$+$H#?3Cv4#H)%YFLqd z;O&zZtGBgAKGz#{d!l!E*f=&kL{vEH6i;tUP(mve478#)b6uosw4_Ni-!~OM%x8>P zFQ6Z=lb&T)xmI4K!5=VFa6s+$flMYzP0I0HRZ1m8g@)cwLOC5#i7}*SQt`+^D~U%YdCp8Q zO0_SgYC$d?4Fw?z##@85zmi27Nv6(8xfDk2TfB*66$Y`YXrJj4qfyJF!*=#ahX6~E zt#A&a`{}ls9d|+o2e-*=KB12MlK|Tsa{?>0zzglEgL1e&l0~>z5~EF!09q{JVq{2z z;!<)shQ>0<5UYXeTOk^a==*tb%n!(Ny9t!4rV9||HOVW_JJKOGNrrHUqNzv>F6P@J zCix2#9W+Rh4vgp#oR4ZOY!PaXIw-{C^Xxs z=W2>#T4iOxS3NphIyPO<2`QTybM49`QBub+Mz#21C0Hwa9Ym zV1P%LsA>@npzL_CqXgwnqD3>`67S?jrXD|88||hJSgKhgqDr^i=t9kcMdcAO4ow0i*(fGen#0?+h~tH1 zo(byvHKjk)nkI&pT6iALjZbA9+Is7e4wgo8oXv8be4`^oCgm(#PnJ<^Qg8AR3$KOL z;1JVtdfs3TSe)yG@HQjy-89C-HZv(SQ>nB+AC}W#fXLvY9ST*9iX!6sjTWCTph^WU zmnAZ5Hx<$Y!)EPB2Qx{)BqeEPNP|*BeZs^Ga&$21wy+319&x>4#7>MEsa_c#48tR$ zXb`nQ78FG)3%M~)QX{p36K%R%>RF>0Gb-!-n1BWF{gOD!*`Q8ewmGsUdX^~lJUVQ> z;7Er?rXP^0cG)OZ`E)UBYarsweQu}UOC<>jo|3;2yJVseRt5O}RInI|JnTpVV@v2q3M)ulLOMucLNv?3u;A{Qc| zEJs@MKHQkFxy%TKsw6&aD>6z z>nD$NNT&ox|qBfK9f}S#{0|M4aeLRZCk!~kl1k%+$Wh z2~65*;av`aA(78zyj|fVoc;gP)*rXF@urRP#v|A7S+}qM_Vwq3TmbL9W?lQXtABiT za`mZKK6Pb!C2{$0F8}1^)ZYKv`^7zW&$oNmZfW-sJ9qCicfS472QR(o($ls-z5PSm z$*upk_3Ew6=D%+K@}{u4x&Aoc2YfH|-B^2Y?FV;oCr*&xd@i$o)+0GkCg`CA{TCEa z_&S>}6@gp!7D&3E@V+;=Kg=3ELLTS?nwXTD96s>I71lnxh+)_pHQgU}2NF+6c3*6y z6cxd`-kr$W+ZQtQMhN#&+fY&+s4{NB0;P~8jqo_-+V3r7c&f>5PT;cHLLD8cX`?JC z74Lw%_O^u#y^rqpSqqfH1LbCzao)_Bh}rXQI@aF0kfHbd=RP+ehbbs4+jK`025rdn z?xfb+JYen?jPlV+z49z^uL7=C6U!&6OG(+v@88I>^%kAzXTJ4t&P zetIFpQ%ydf;e!hqdJhclpU?15>CNXbw|Lbcbjq&l@}y%_>Y7orhIzO@%$7YZzP?Dm zc+zwCetn>cd4-dR95JR*t(-sQc%NFx@RWibqQ6BT2av{T0j#mX+ ziv|lTrl7*2Sf!|xrwwbLSjg~{6_zJ?1k*5%DkCXwK$l0}Snk@#7cx9$h37N;*g}S< ztdIo@7LcNmOO*>csNLPPac`49x{%>1D{PLkqwZ(|Dv(%{s@X}w-Ua5`M;2M(sXjT^ z3a>5FFLufbHN6b#!L@VgnrSN&yJes1k`FIr=zTJ|e?G$xEo69Vw3cX)Phg_u&9RIX zvPh}#O_sa%frSiDS>gE%-@lOIDWl0V3dM`OhRAJ788$M5g4byNcu^mF4{`6G+s9WI z=@&iK$LDUm*WR~~;i-}4++EY!dlxc1)yEhlw_6hFx{A%I~sF`?`3V@ff5PChkS8tVqx7biDs!ZZb1^u1lwx5 z+U7vml9{xa7*3#x+d7Ha8#O_>PUpl=BJLdG>GnW139D##Dq6>A>fi!6sTV!#u+$1? zL>+$|U2NWf&qLe>5OqLR@s)|?>6v`NOE*DYz`A{g^DT)*!iE5%ObkFj-GY$)XbsX>vMSwdF z?T%R~f+4a}Dx2B~|4W?dfAfRM4Q=Mk&RBB04jGLnBDZqL30WJOdLe~Z>JduI_1ZkC zbNx_kSTL)sZPmM?mZ|63;GBzn+YU*q@k#T>%^Nx~sF`$3?D8QjePh&SqIi|A)WUX- zVv!(Itze^eC?VWPj)B9KsfyT+j0%;S)t6LrVhPb3t(cCb!Zuv!Hb($qoMFEPyUDL zD8xu)Bob@)bRx-Ua0DtrHArl=bnK>J+z17U8{LUCguaA4BRLHx1ANm!f)OF4CR!#A z=lk@{fXT*_W*?iR>NN|_nE@zjRy#bDX{Usepm3BLscYB>9UJ8q3P;TDsDp*WJuMO` z-(1#>LDQX+|1X#t&b<2a4d2D)$6Iz@4Sh}e+$uxe8L0)1XAS%+Eoo1v0OnGV^<%!Y zJ)S2ZcBZ#o*en_e$8Wi;1*P54lw{aCcB7N9Og<_O1_Twgjj)x;+K4?SW0g1}o6HzY zCP2-up*Ksten*=pM>$zv5Ukx_@ILW`9`FjVA^pZR+b1Gl94pk z*N{&8CU_s%%r=ZjsVqTQtT)Lexg<^Ep|PB@2;|1F(^OLg!b1AU#v4)>RL9E*B{eo| z<wnVmkZ5x>lyW( z@#D-G#+m7*R>*WaTgaSB__8X4qy4)RQ+WE>312Wg*B?w;RLxGY{iZ0(kSE$bTTW_C z5TbF$l(Sy00k3NWyb)lBO0}Ckkj`l~I(;b#U!o4PFIU2svlPF*ZBq#3dx<+6voqru zOv6r>vQh2^JQ#CyQUfn#kD6ftZeWx0Ad)3RY_xVGmBhzrCD=)x7Tb>c&a!d3rB#L93e(NF6%Ms&=Nar-r3iCq5}< zd8h;4edI$tICj$OMKmsvX$13HJe`;BbT+KXkwlyBVKjCF zmy_8vRJs{0gMzm#C{${Po56CekM*)z7|y|oyhIPQZY7(OQgAp|CE}S0oK?wKZVb+N zF0s0qH{IFcZ%v!?9X`k&YK>cD#KvP5KM95l#GN;nP`O0{Hx=rU!yNoK8jJ1O7fwSZhrhYL}-UFymW`ep_WCMSp*H z@-lvS=JGD~)o`;F(i5M(d|ct_-q~(umBAX&GeNGJXEf}6%W18rCpM>?$YC9YrSMTI7E zBSTMO+!z6;0@V>q-L_-)=ytRZw@^)u7aHkUBLoX}gKI?$J%kORj4DiGmZ_wTz|BO+ z3YH+L0*+B0U zR~uK}edQZ28+-TeJ%0DccK&XMxb(~0|FO+){nF!@FM zRw=3GDtY=D=K1jLIGX;&8=UXYfA`Y*?!R=+1L~!NhNTKejjX zK2!prdp@q4j)w1jtn>Z8XDqGflcz8ASir`?3U%*XJ&z|lKVQ=gN5^|%=llNOSX$F3 zOkdz=dSZH9p=_S3={&@DzK+iHc=sQ2&i6Z~OY8W!>BKudu24}@Gr)Hl0>DWQ>*z~w}P+R_3^g}4l$NF<4PqV4#S5ZgR zU&U_)qc^&F$KG^wqN{^DyQ`eXen`pL^mwk0v-#+?uXJ?$nb$l!?0vWVj{WJ-1KG|& zI4FiAl)uUl{y|EL#xZsPXYp3CMx?^rurX3gg;FveM}uMJA#Ij~Y&PAz`g4vtul~Z) zknK+g9wjdwBx4CkvLLKGOi!YeQt;EH6(&cifSn#T+SzQPI}xmu+U0Q&cz2`kODZ{= zZhq{Gj*>t2_e(4JF8>D#%kkSeP`3nOaIBy_lI&zBYQCII{KcNZdRxX>FRrIx;av-g=Q(;# z`%CNj(w;ce*)O z$Jumq$6Fm8?~E?3zVZ(-+O#7 z^<{iJYwuZm={%I+yXRs<3SZ~;l|^YAzJj`(Uv;?g6z4t%a z7x?b2=j?CY>HxKOhQf0cQ7AAH%LDSetpE2L7fBDDI>m^n-h0kSL}($C!)Vf3;otl2XRiI<#q{2TRII>CIiZka(+>T+0&D-dh(3He(>_z6 zAxuUkr~U7J4eYzQnBJ>DCgfx~okl6fp>I9S_w>c|Uj6e>0WVM)Ig@edzuWft7t?ze z#T=1O<2(=ZGwWTqeNS6V?+vIc3V}2EEHvx?_q_ind^Z-+hmzj&k8mE%u^`tRmje2~ z=Ldeo_g#zWz4n{tL9!&2L(-x{pSsic)W!5(`$a{Lq!Ez+(P{sC9`Rn^cP^&)?!!1Z z3$h1E3X=u&4}9TqzNajv_qsoV%jp7<$>(W@{%v93lNZx_{f8yfSyq%V%CX-EUZVS+ zw3yy&e|a%W5v+vMj{QCGf}i&tET;F`UpkG53WN|S;n3@!_I<}Gc#A^`DsP5$wl7fMfft+&7m zI3%YfXx4krM14UXZm-%+h2^aJ>Ki_iXweyO`c^Ve$Mwzi{*ROmpL&f@Yysrqkr-9 zeT&yVaj*5}VGiYaL?LI^Q~!D2H!PO#wLVrN6&N8v+CZoM&*}LdwV2*(eVjt2b14yI zIh)aUeUDsB@3lUfP0J9Omr~B$aX<7T-y;^&d#x`erwB?BIKuJX`ycs>zUzzWz1GLE zoLpc~gmDJH`ycsw-?fF-=Y?N@Qv`{lB<84pSNmTV_x`CID`3ern3 zKIXMPm}fHC6o%y;?eF{dYrZRs>AlvM6(E#La|G|q9QXa*zVGs4dT;Y77NnxeDJ<{w z{(Ya?^X)CB_l})N4uV(~Ng-_U$aD_gXIr z*3B>{F(T#AzmE4^T1@Y?-dvUtLEd*kn&J0jPw{OpruSNJCQGm~O=k+u%J05A{@k~< znBHr>`IJnF=^UGx^?&>CeVfky|MJ@F)~@~Z)yU-!?$vhJFa6ARZ1ck#*7_yz^H=|K zXJuB#>lRzf(5Z;^!>0-aKA|qEXbf7j)QKG_Npuz*IEkwksQ4HY1$l&3Pz$4e4Qp@|TntvyTqrtJQ^~$;&Z-fPPgeayH3;Qcjj6RSoQJY^-SMzO`J#r4RLWu+YPS-Oj6@zbY0HfAJapD@0ati6uaBj~ zSmaTEj4w+d7b1n5l|a|TiUk`i$;1dKV=*ZltADiiSLdPZU7bCgP`VsUVS<5;(Jz*Ce&W^3{kVDbqw)~gp zp?qo$`>?4KijV1wDl>&uqBN%Tgba)2IC)eyQ^Blw7Rrc-C%b_*ICV<%!^W`PlyeQE z%<06qgxJOEP}AE9B-hn4AP4wSEw8oDpNFz{rT?(9%5dt!H2~YIjR!9q;yX0-Tj=i2x)WNBMK-q3qocJ*-f^ zsBt+1B^i`W^MPJ~#~Nd)X)+XlmMx>PaRVEJ>O>4l^s@YfNRI1d9#Kn5j!+7-41+xm zLA5rAA73p0#d#=uca0AxlrOC4Ev87~I2xeD3e9J*U^%5kdF-sBw}{@r*|IchPW<(d zUnl+jOh+wcI=UGPfT~o5YOE<+Z6cbB!N(WNe|{dyU$NElZLH*|iyD_x;v|=XGg%Yw ziFGK#)#0>~?w+;AAlnsCjX^T2HbkT&lY&j4Ogm+cxXzgC=j#x~l~DpujpF=ql>f{* z|G&5P^0jN`mA}6%?>@MLZ9lLT-gxu+6TpvO;m;2|;tqBP)a>4Wd}`}f?=L)z*YCrN zTfZkPHJA#iLL$xuh&(tl+-5V(8NH6A!e*-}C0b=PL?Qu79ZFfDG65BO%tqN7ktw@M zb7Ky#H!HfGm8BGq=khfFP)Xjr)-j)O;|a9Zd&V=M?zpKw;~5#A&)*C@JrV&Crh8r< z0e!-p(-6t=5!JRL-Bfk@Tap_5ppM<|cbNZM* zD{6pJfs2X^D3~d0T(4A+#Hwh}1}Ik;1|bztsX7&$SE{ZA6^RZwm8vTlBB-2InHovA zbAAX(*g`kOw2XR@>ep~H+)0#dywfTBMYSt78dPJ9w-9~)E@ed<7^=23{M|L+61j6+ zBJeQY01tB)<`Q_H3KAUMFbAaqZE$rx!UxYP0E~>1a4~{l(VX3&+W8dSNz`D3k`=3h zjH;l5m>d*CIe|SRC(m+B(Q%!;?K;1a2NI1(^aBpB2yTD(iB!%#XoiBv-v2CAymaw8}h>oTk$pz+&u ziSQGEOHk*z#KSmRF}$#71=6djg(zAHu{PIa{TvoGnKR2Z!&FIRa8n*Nq6V7ng_Slw z&`M^tI>9ih%~7EASt?_~wM;Fc+ZZpC;8ZlIQ?;~9tUV97ME1xfzCx?zTe-xA#gCcB zsH=oV1|0}?1WOT^It(hVt~g6%$O$LcQ;>fG2ZpR%0%z~XmaS*mdU6OVK5L<#k|q?^ z07+(V&n52u;@f~rkmtF?!#K1wys*#`(hFv}psb>7r(Cbpl@v_lp|gU~kj!SlC>Ljw zY#G#}!$(Z4N{u7+vfdd=$(Fx08Z+sVzZ^6|x(!KbNDwGFD=zC2cfVx_T!J`q3Gc(z zFVi;tRxUBRs98b`vL&UE=xLZ8<|SsFwW1nWTCTQmf>phaD}YPj=efkgI1V_ZFDwp-CpFU)OCb9US7>FJ5EJ2= zXT<@L7TyD;qJ=~;-j8M~k!CT5WjkULRz#u~RO=BkV<3KWsL6(IQ@8~3zEc>QTiPXF z^doz_pIeKp-Lbaw(VZXN5ib49rMF+Y^AdS!XZr)&&F$-34{rVJR(1XUts9%~-~5qH zY4e*l9^7~VI3=)q{e8Yy`Pj94*Z%MI$hCjn`=zxzcK3Gg+)aTK124J;U;WFgFTVPu z^&hf_GRNY zp0&j!9cjywm6~?bP)la91WHkLH{O`qVxIcc<>_ZsTK8mr%#GiD<`xrnwCGSw3m7q* zB3UYx9hS3acjEC=P}|>mh`E4H8dVI|TYUud(1_*ECO(b%&I5($!2= z5UsOW9GAbeacO zdk+C!lKl~&JNF-=u_Wmnjh%NNq5-E`9aAy-rh(C1m9{45+oaEbXp>74J2vSH974Dx z(INM=eq%U-#?7e|6-_s6zXi4plddTU^t8$QZXR~>k}T$(?7Oid**t@i zFX-CFx%EQb!Tz|XY`od+hh>Qub#v%cN|lW_x&5##`=fr?c#GQ)%aWe%hmE(ofG*2o z-VYmh9U9M)3y~wG_x|b7PnRS;-WPlSc<2*L5-;kDc)+`(-1~>iUtD|lSNJ*3MT!%7bw_Xt#gMJ3nQu(L-`7ubi^cG2Tg>U?eYbBCkEk}OYK=ibjA zn%R;pj+xnO56x^z(i3L(8qg`9z37g4*zh~_p(T-7wXSWP>tK&TX z_piNW?fS1>XRrPHwO_hMUHzA(syltbo+(dPulw6)^O_yoA2H1Y<}y;y&KlXqu1|VudP4Q_YR-x zy8>8W{D+^GH`f_$>^7s13MbDObZA9mF%ZAEZ-psl>@DgOhR8YFBWx5Y&ZO zuJ7lwiK1yuGJYa!w`+7H#ZOB!wx3&qZ3rcjR8u7FsuomZ+Mu7v^NsGPUWY2hRInC^ zK{2hzmQrI|<)_b|vHgoB*ybuNDcoZE1fg3>p)VCQ6>1T*)MvA!vXRvbC8kIACf%ON z@YCXq?Vm5f7N0cp;jm$L0!5aH8o~I0lt_OkjhDk>P%D+IZAueC$~#CK^3&(d*#6lP zY@x86C@8IAA>SUWy=Yl5M&U*$q#Hpk0y38uNYWomwdig{w)yFEXKX*a1Y0c$&PZYP z0t|uIH!Go_q_=~8iAj#qG4Lphj&-_3530dtEQa&b@13#z%o1!{<0x;3QOO_91d?I^ z<(flK1}hLTh&<7)z(ilH@f23=Ljqv?oEh6s?{}+Gc>t+7fv8Q=!$PX1cnyrrrZ~P} z<2{MS12xjhn%p>3kHA==S)s!M!C=+Qs1htrk_~>U%-BA-1Y3fKqk(1(jUpfuADsBq1htS>HvuR=^<*MSPbH);*9Ml zmS9_mr8BK6hnLg23M{tBFkEb>X|S*E2FH?6j9cANg66v85e2$Tn6dr%5^RHfu|u~S zgqesU7!~u2gCKay%JlJyq3GFwfK$P4AcSf40AS0{*nVsYwq;DM5`~U5(wcD$DR;GD z1SC?Ql&aY;TA0>+cllrMY?5R3hisgysun=pGNx5kUhpJPgmFy1mVwjWu7ZD*7$v{RF60X(!;$zm(4qeUUtz(aW} zqxa~3HQQoPcu*~OfZgV2Y(KmN+fhx|ItWRtI+>E%iEs?0o*rVOx@t$c!XUxIy5`sX za!uhtm*r+`KePl}qM=BkJXBMwX_8HaNvouT8a-7J5rei~wmUrrqd1+k2Z=5}rDtqE zxCC2_hJvOF-dq81dL=tj5wEm$V#ElcSXl+-imKIcwmX7{QCJfADK%sJfhE|EQIL=% zS(0mJDjyJ8klnzLYzrfT93)Epkp-SCDM)<)H%7pDvop5uKRg_qzq~C%P!494*(aN@wR60D(e9fAG zEmT8KDuGz%Kl&^~K@W%LtOK$4 zk0-z6!$lmPvkb)E$3bkil&Xe}xJOg3Y0y;Nh5CGjoVA7)?5<1>#qAc2ORb_?=5hvn z#?1q<-{l}yFFz$(kG(^x7tR{)5&lin(88FE|eIWK59K`aVz6{S9 z2V%d@MXU?)l>@bUTch@+UzzG8@tooT z#Z`bm@aidg>TJQ2f-g^geezNvsh}oy3$GO3EBvz*kyB0jq$WZ7ASSB;xWqL%i5ZVRp8iLbhyB+sd+|Dr3jCd@?Q8Ogsv&3g{ z1&hV+6Tfer5R>+61IwpcHKt$*MDP?|tk=6EWvc4z8fxfNIMvd4yX&YQ33*Ayvh|U61^mP zi7Qwr+9%q_6)X@vFM6XJjk6gN{Z;f=u9Bxj&xxKJ*4#MBqGv_VauqBV?G^1Eg2#0u z3G_YadtAX{=wayLA$VM4i^OM&&*Tagiq8xN>zf?s8GBsJUl7z)@gWEGmkM zT)`p{DI&Rog`%7&#}zCPWkp%8U`Uh}rH9~DiS#j+5{_4xM9^Iej~)@9~sU2 z!||Ri5Nw+|L98`;rLI;R+5pyb79#Gmo5GeVU85bN*DpnqvZTxvER>WaC9YtBgpn{@ z!H|TO&|JY&5=uf1U4L8wB}K^_-@$Rio&b6fdT?mPI4}gbx&MPJcnW#|dSGbsSjqQ8 z_rDiRmHRE7a8}IKrqh~pn~H^0+d++d=S40TSE$hG5F#LA*mcIu zYvj+$-uIeE9U*&I1>BoVms?XUnF!hM2%omqL?kdE8MMe=B3tnnsx-O36ZY&FQPuYW=CJ*3=s{BE=-jwJcF9 zl=9&BDp;ktaMPMpvq2__7Ex-{!7tBQ!zWDW8X$Nrk4TsUJdYbAYt91-Dm9{YfPp3M z_ihFP8r=#3ineCqdFuXQZTklG1M;#pwPt+H_A}Ip5~#$!#5ZG(9Ay^i=gO2z4N%D- z+om-Km;mHtQl(`7vC^6g0WeEc8YF=ATWwJGdLz~77vP+JZ4CaCq=;@`e_w97R>*^# z;D7tCqG9+Se@oVhT8pS92Mw&!fk*sWl^WI{Mm5Mzrs^coZ~0AY>SRi-)AuE%nV_Fg ztLp@;o6YN&kfUQh4E1}4`F`BJLUtStx{F{Ce>$f|WZ zmGSgc(w{Yz`#Ll>EZDVK*Jmk=HZ@BgeJJa$*E(Knu+0!oztvUEyW+5mOhieqI@LwJ zDi;Ed7@8@4Q>}s5MREj)4&#jB0SA)}hUsc5UqManH5c})uvzL`MD%+<=-_!`iK1V4 zAv7nPs}J1oDBynF`_%YO!qxrM|Ca8@<9^yw?zi}5UB9Qk4Nka(A)s3VC#Zprv%e$i zfPMc$M^qct$eTH0E#@|cO?dSjigS!`4~+n^~w?sD`zy0WEGF~banzGX+G zQ?#?}jcDS=L`91>*GH>PO(;#!Hnc{N5uIBVh-vthi*e^P8#4U}vTHg^r+MHa=qjw!VM^yb=IwFr7 zol805KVHYaO%C{m0sY~i(oEBZ%mIF<{TKV4+5o?q-*p`-tFLaUl+jAa;)I>OV%DZN z8*^IH;zQdJ1V&p@Xxei86S1*)f_T6;}P8QbV1fll2Jhf(Fouk?cn(PUmy{^^;T_!Vb3|qMkfND^5;CJ)X zT($3aAInVgne`n!zk`?ZyZ`z5q^at4Q%`#$l;a$wVN(%h%) z0e{r>|GEUt3-}Wd_m96VIkp~v08`%msl7&Iv&=JtU-oTe?HMtWoeBTL@PTdVRDUWw z_$O7!P{V(NAw8Apmu~QvLa9|4%%}$yFr05|fTDllFs}k{rGSPd=i7dJrRQ`6I~HqJ zYi?FDnx5ZRCDvQay=Ee4Ygo;_YQ~AT9L5s$mOnSuP@q=S*Q!yMHPtKoLoN%Lz!w^! zTB=&8d2$g=p;s*9jZ`dHArg_U0WH&rrByEIYZw8jVk_XWma!H$I1OYekRu77iZaNPn|wBJNet$CuVP*y?8b}dy3+9 z#bb)?;4)iKagzL1`H$o`%QwjblIJ83O0JPqMP~VlvX^B)lzmRtmHA}v7E;0u!XpKL z5Zpbrb&8mJdg`0dVbGaSerDgyBQrP7v}e3CtEXR_{@(Pq>E^V1`UL6o(ubtiN$Y|u z1qH!bf_FfBpl?H;5PwEomO7=!iH;Im#Yc<&EV@s0m57npCC7*#;J!`qUI7NKzQ;_jaM>E3iYAAVylslH)?cYxLY*~i1X|W*rFTa7S)H(S!uFx&= z>$yTdB)^U;^n>zixk5i6zlMdznPAfuEp+UaRMkdUG4CizgZw@6_i=@uC_j@cbgldh zuF!YOPv;64*74A_;xk0d#5#`OP1b;f z&sDUvGEp+0Y1gS#sW^J27mNx+he>&xSdVdgLTET-w^fUyd;^LvDz=Fq`2YQ0JlA@C zMe!%D(3cg@fJVKYW5d4fFt%9nvEAi%#lk0JJoGlj!Y5-q^j5|2lQGtSFDO3CwFzER zER0*@9p+WVaNHV4&>=WlaQs4LjfJih9LE)Ul;Bvd(02-s;R-!c@Gh>{Ufjr(wcWS+e-!w8DOA3-tbA^(UPjQ9jC7 zSEx~RHdiPjI*TjRAR5jxFjF|4PkUXA#hPLYR_7=!ndAZK~Np zUCHZQq4!E&;|je;@+w#8-I7(H(6!9#ctz@vWg9?_S=8;Y)Jx_WySLnIY ztGGhXkq(`EtN{t>(7DG#|b$l1B)7xNu-URFNI#}aZ#e2nNu>NL*&j=~7sxN@`c^<6liOJtiX29k_YMEKZ zMO#l05CpcC2M5er9>gx=X<{zgdi=lu@BueRvo&kEta2t9OI;I6VP2-`LS3C$-q6)X zOn4}yZR9AXzLp0~$#}kxi?$xe+W;OEAzTbNmbU>s7*)6!aLm8}9uzohd2p|AG2mUi z4LHb6a5QfN4ssK$92mfZF6dx4!BM;oILJ-#PTmF_>ik2{)N43O|P;2<}Fn7083xd}w90o-pH9_%I% z@;2ZgH-Uh+0SCDWpn(BA?w1|xCYa)Fz(H<;N!|t=)F$Y!|Br>PoDg}0-w*}_djuVU zVs^{y8pX%KIse`Ai0r>*luSI+nvqR^Zu)fT!_t7{8Ob@~XT=`|C-QefS3>aA^;5dZ zMKF5w71YA0y{}{~vcw|~W=jZYI=iol`}jM4TC<>O@l`isOAzQ=5A^XE zY&h$C>>qZx|0K9yI%Z22Xzq7d9uX2aYhK)8^II}NUw^FN5jugh zzWrU1*pdR8`mVtv>;Y#@`@6=*g%a_IgTPtOh-)N)s=jORI2YrrYI)Z%Ea@8O_w4~y zd{EEc4)}Jrx18740bGN}QQY{moe|d<4X?+##<@Uq-!*ugKylW*lxv&=^z~hX$8ig1 zeFwTm9BAsh29GUm&YJdjjo3noc0ujDIvVWWRLNU||!U+&eMurvHKW`^Vkm@88;I-v&%w zGgY0kPaQM4S5h6^+}|sHpntD_{~P@xv*;*rvjDt6_{Y0}g_i(FLeD_=L7#!DkP|v? z>ba>y%iDi)A~^4>&+DkNWHm2Q&4IFI*$Ik z&;HF73Yh=7LILkTS14fp=L!X!|6HMf@t-R+CE{FJNl|h^s;|)Pg-It`Xw^DoxtObr z%(I2x5`K#-^qazOa)o|H_!X|uFAKlS75XLNm$*Xj65hoXx*v5s8Nq{(`w{N@P8qk=B}eeCqK9TgO5ln|h2Z^v6>_ z<_i7M)Q`AAADw!XEA)p`KjaGi!PF19LLZrWWC0o@+vTb+)*?ED)f)0AMjxZe&Xk?W z6?(dC;W-{UR;pg6=PFev({Y7rWm>LKjZDK8s+Os_LRB)=0yJvzhN4kNHkt_)T0Y$C z8tJdl9nc+Ip|?W|Pl(a6Qg4G6o)C|R-U=-|As!F?0<`cldOUPHwD5p-JoFZ5;c51G z=*`f=gYWUs&qE8($We2&)b<41_FkzKteAS817nfFFo>SFaQ}Zi^a|k>3#~O)>ZQU< zS^mGrCS;o=&kN6;eC(fGULMN+_SXQQQ0qttZ1ZHFx{1t~bncdo)S4m*Uqi3Y1RRFC z(jW7*b9uksgvWg~RkfLFIlZp&`$%s+txkiO! zy@uE3aX4y0Roj&?S_+nOC)-VQsAxo8u2pk(6EJ5yh1#I#eJ;Zz=GuVD>Fi6RlLvQS z7tlg3#T=@CJKT)Nb&93kOrydzT2z_C+{%_MrZd=sEwk5ENs?}xCe)@nnN&1oG#IQZ z4YstKF=;^j3A*k-LszI7-5c)<+@Bqk-9xXFg$f`nTGQV-?<1%}sn2K0a)ynq-Lf{l{jG8rKG52qOG+XSc zZ`BR;2Hl%dk1>I06KmF(N;u+iN3mL-#`LvDiU?bx!JH|OiIOi4%ybPGX*<1YP3|tDt8;pw{)5qNtfN}9^H<{8Wy>oP@eqN~A7426Xw&PJ!#jbwbZ&*=%6^l+m~yDZ$s!hX?# z6ZY5tCr&gcX0M!Gr?^KEmj6+HiTnuJZL$qBkI&?$|2F;M>32zQm%dN(L&-+*bK(z( zj}?7Y3C%qHD0;0zMFlvje zwxOim5+K`5DTP))aiXO*Gl7gVMYJ3Ks5j)m@VG4& z&^xpmq}DZ>&1MZ*Wn!RM{FE(>O!>{z!z#p3wB4dD9(AB=$g3N=aMqFbm63|w0+y}uvt1RRg)@qdv<@en)VPCn+b6ht1vKEqBpq_ZIuMa z6HUsQ)~SDee%#~ML%Y}rkX3BOe<_MbbqzLzNAB>@@$)T!9 z4O26&9MWX0Jxbf5Gq{b=)e2$Kph|g~gp(*|TUdqi;`;pRP|MS{nY{6`uSP^#RTDjW z&U`@qv8ss86RfHu*XFG#rPpUm&Zs&RF}pIoX2k>McFBw?URIMnw2}$Ishlkc$J(n~ zrijUvs98$RFo8M2Tr3e38*(Ejwv7 zRN%5FZtyxmKey%FfMYOQ^qGR`rZZlvM#ibR=nXYt%to5Rh7#Q|`e}ojqLdD8rs{9F zv`98rsrfn;%tjLdzYAO43I=^0U)PZ?1Fd8gYb`f?3`%q1P)#oxk88nh60BBtT&M=~ z6NJCZB-*iD*ids&7Nte&Z+Fc+|DPF(BI`x5@PMd{CmomLH< zA*}jp4`E16F`4K^oGovSu;)nA@4I&(*7D3$AEunMyO|8boasOR-FE zV%5EStj*OhAKvMHF3czi{#Qsu5SnN~6Dj<0qms=Z1uUnQu3 zztO2T@Z|=N0}9t27EWbsc?nBSrwoR4xF=%_7u%L@NSy{f*`U@HYJPtvK^ir+bcd(9 z{R`I_7S7^mAS$Y9N;Z35TS04&`R#O*Qe$;fg;1#&TPabeGnA{4*G5*`DVoHa7FQ{k zOIth568DyC4+|G`C2L_O*r380(&Q%HB|7giTAZ<_%3vq7b`zZM=)!t`*ieS~soTGB ztzqFvq~Nt!NS7tRgz2zbr6LLjv=jF`jAnu;WR*lBQ!*N&d0PQpO}GQ$NWMU}LhY2F zbWyd@+Zy}#{sUUBIV@Z+m#AS#4J(+Nl(*vS`Lo_46@*I$M>yt27YYYpASUQC`9z-j-K)y&*zOt&La%6gDERAH85mP~_>mh}9%oTIPtLwQ~x@F4v zBW|+iXh&_8QJ&d1EnIzAIGv`WF?Zs4J4|N0Ig2NoOXRg-1A+Q#y{?VwB#3lW8DL6{ z7{X88euXR6SZyb)ExK*CPO+Tm|eNl%z3M{Gu=Tn`6SI(*KlT2 zVO1Ty79#?29Bq0v&N>`0l&Q24GuQ&fXtv-fbhJtzTFqH&jIHIcBm9cBFgGj@>#rFn zI@SVO2>YUjdb(8(8q?iMM2{%b;B3{NYBU^eZKp%3OFC@)nS!oH zyLxerzSYJ&U6(!=r(NT3@Go6|)Uf_)Wxu&%w;HNFri`Kf9-c62iq=p)qN`@?xROC~ zot!Qi@)R2|PhG?MD-P?AVlok{Kg-0L4!@BIgI%&pqh4|AeTci_QTANH98TEPRuU_L z6_?2zEN0`)c#}a%Z>a4`Ei+Cp)L&s(e+TvScFFo<&;P$PA%BPTYT~8BinyF-h27{*UYMElLUND($~VCesV6*GJVIbJ_f=SzS&Q<2oJF$~D4FDWb1w%Z$xnvesPIcBhqEp9qEB96vYxqJz)PmeyQv zFwA!+5AH))j-MNqajC;lKA)TZD7xrU9bW+>pAYsL&fE0iSo#jo$d#WrAxzQtZC>^w zP(SeR;_C$mUIcnG;Ui0mzOJ`;FF~c(Xx{XCkhhet;u+8Sx|iwci*ax>!DcKKtzNrX z7477jb=s|GqITR#r`~$x(*WIA51X{>eFU87G);65E{Dox*E%B|PQ+{x)E{>Rv_`OQ zbQT%RNrp`%QVM2EI2DYg7?l?F0WNP3(=r6~RLA1LD*22HY(aCm9yBOA@VpNd=DK~) z`*3oS&ouR*4fHBxvF9z}OG`{rSqhT3$vXyfVi3~0UMLSDUu*kz3@j=y8<%_0V0$6R zH@qFE8t)YUOlQ|>kvAP?RB3h2YcqtShIXM4_1kl)e6OVk!-6#s(l^s~ovli0z{!G^ ze(RmR=MDJ0&E|R#)1ZUE<>oqFqpHT*UAR}tHWP7oj_{`f>m0Qz(k(RgHl0hCvmsqw zFXAe6G;X5pjaQmY5Foq}E~iUsCY$qOW{#b`U-X6Z|5GQvGBJD0tWWWR;u?iT{s;NT z<%sN8vMn+txC5|hX3g|N)0OF?q~DPeQkmq-l7wVZyj|=Uy(qd?WEK8V_z9s=@T6c) za2oUkbOH44sfWRf|CN(>Pv$1&K=8ld2cKUnAddE~C5%U+C{P1Rf?t5(JGRaG;F z89NTWMhpA&wYs}uBF(g3?*bbuAdLE^AG=q@ej=E@p7_OA&!7J4*BkGshnAiIZW}o@<&S3xaPs zEp$`mr>|6>f8jjHeS!GWKOys1JapHiuCK>JaP-fUW_*6lKroNZX4c}pr>0Y3@8|#e z>b_af&i_kN`L;>KT}^-K6?f8FI$5l{bQ-%GcA^`k&mRJ@voOx?wjy58^xk!3jsN94F%)Hy!k&X-n1-C}ZV3 z9jN*;wtyuQXjR-%#1qIUeG7uK^q%xPRqqM=R6DL+`SOYzrT*~}ta@4e~g z`+#5&zoBojzg8!jr~@`Ru!b$5R>$qAHm@|BTkTHC5;a>wNz#W`Tv&3USFHWRGrix4 z9-Z9!70ZkNx!L>F>}Jucs%uW%EM5O=UBYR34xa~c8Gv9D5);SR`p+>K()vs&_l+La#XI zhPlr_KTl__6(-z2Kj9B6AItsVk=l9wS3dLo3v}z$_dkTsgE$I6uqvFa`6`r~MsyZC zs#oGIRZeZknyzdik@vz87pkhmYCBPc7X&|X_8;|EOF#Vs^ASJz4*ptlclfl`yRW|F z+efT=gnle_{>NPSJcy3~1gGtpoI6^Jb%>I|Zb{dPwzleP^inym#$)N3OHHdM30u*6 zmq6I6pra?8Sp4Y&zklV3_dK&n@m~GUe|EvQAD)5pJJokTsyX$czu@yA?g0?Y<0OO? zeCfvp|8e@!KM-HH{n@*oD*xBlq)!WP@uRz+X%s?V`~8JiK8VkQcn1BdFh#0Wk27K| zI_+L#Ba^qL^Z|FN9o3h*o_MLOYMUeJcsCr?qpaY^h40;d!{Z9o1MC0m+)MP|IP>bg z=X~M0Q$D=srxRi3(ksrn37-dX3V>i9=P9h<&-jl$BE0SJ!YQW+jy>-9o31kq$tEA_i?9R7vtJoVW8E77NJ{p(>T z88_{?>jr!t#0vm|c`)#?g6}*5-|qcYEH0?r`TJi^{5|k{$?lit-v3zXf^P(5$NZ|a z6Q2ih0Dxc~RC}!8|5$na?Q1TpO?~kEI}N{IeaAiLW@?uQ6MsKUB>v8(r&klF;PZgr z9|-2bt;Y&}k3G73@9OHaAF-kjU%lPf6F(t6kKFu?bzhzIx~I0^{XKjh@b&}2Jec%Y z!ACr*`{cvs4_JeD$hxe{|AI zPlQ%H{pZ$U`%0(hm{YEcUh?W;_&ngd2ZDJJ^Ra@{d+xjFdYIn%wc^FcUhkQ{!+YX2 zhi~_vkPwjP{-OGpz^Cwez+(>t^El9B1%L79)?+1;5B{jI6FcX!^FOxzNZtEC{p*fR z`g*u=+BMBzoq*2+e)@hLtDP22+HQ(qv<^>!i>Cg76Gl9>wjNc}p<*_#3iwjEI|w#; zS;3ancYkr$229$G+# zdrmd!x1a>LZk{aB%C?bIIy}vM&6y?fDp zyDwUK+qT>FjcHGB?)UgS;FAY}c~Ath78~{yFR}jV=Xc-y4ATCInh7(W|9a-je?%YH ze4V#Z`0q=%HEI$$sNxh z_9gtQSL5@5{~ZYC!2rk#{%L#n<1c*dFQTXJy8Z{>c)-2qihX~`y#K0Ss_;{ zfzJcJ_P${5$GWWG*X};=x(iNNf9rdmeYEt%%U^l?mNid*-TBC6FW)Vg-gWu)=^#E2 zc-Vnp9>jpG;7DUK{&svG?Q7GwGGF>c&+y&Cx@UITUO4}#jlVWufX`ht5X^&;kQMy+FNnXMd(+K{i60AI z(8-m}kMwF}a+mS5C#SrfCp0&e@wp2Jf_cO>Vg>)~GdARpn;-uj{KxMl=|^7Qt^L^F zx4!TA6JC~`{wdozr=7PApSxfnn8zt6E13N0rP5nIdgO0k>u$O2&$QtKC*Sa{_rKcS zbLMZp_sNf(@UD$p@VQL`!90#}Si$el#!q?ev^`&V{Pe`PzxI_Izxl{9-~4v3^Nl%g zqjBX6XheX|^#+1@9FnkteXCYPKfa-{>3zRFZ?Xc-TUxMAFf_*{zCQ_ zSKxEqfnXlnr>x+QU#h<2obTN6%$M-HzTUX=?{sb__VAOUkH383$Id_Q{{K8Y-#`Cf zJwZ>*R%Ri^Rf-ekJLM~7*UONZ?K5XgKRSJ$bT451|EZ)Reo|Z)OGKX(oh-al=oIV} zc%UaCX6litjgyZ~7AF1^h#31*oKuOX04*jlA$ZUDn_CM6iTfWItu<-lNHkGtM+4Pv zrlCtmz%~d~Q|AjRLQ4{=sK=w!+bX`gHeTK=n}dP2lgH_!3mtTE#`0KvxmTETc9$xjx}#WaQr`G9*>;}a_+rA<@$cN zJC3u;e_VFpvo}$kJ7w6^$8uJ6fP3@UuK@l0WT0)}-p6p(wyb-@#?gMxW1Ay8cT(S+ zem%d7vpEay&12J}HTRyr8Wm8pa{SfC1HF4u#0Nfm7sa^~hmCtAXT=BjF^}DW%G_F@ z47l-x;0VsjmT+Sp`v&CPyMfAqpS**!%KiL=$4)|VZq2YN59h4v06*cey8!%THPANj zlfyV`Ti#Dbx%K&MN@V9&_08!w?F!E3EcgkJEsECM34JwvKT(Xo!aCw7qwM;8b{fDC zbUe^J7=mP+H80V3dF&PNSxgt_jsxZm=3vvD%{#!4d29((=8k3SVnQJ0tZa$8;IUai z&K(0(4*XccS>=9y%wzkYIQOn$Rf;*QI>3*4Y$*UgJ{o8n__2tyw&neJG!Ouvorvt* z%Dy>$KNfN}XTgto>`S!fj_RxF`!O{BfPKV|)uZD%kL?BE$L|E12Yx)sS@RNom&XRd zS{^It;@pwIyuKepf9Gu80e;M5AD}XK1X~vq(CeI)Em0Rd%16$<1E}o#G4vW|mHYWI zk3ECp+~LEje3i4R1N@lBJ_7LL!+^HFA49Kj*0#JKkKUl*b6@FqSUB^>!9bXS<9o}_iyR& zxlJ{|^VmjP`qDp-P1L2RQ5Ju~`ewZim;aYA1iS)Z-}7bDRHLIq9Ra*9@Gi9*3&UHW$1z{dJOZ{@8yOf4THv6 z)40R5tx?Z)UHPIPyy`6#)lKly)=|+0)|*2o@W|` zK(K9as==vMq3mUBJ;G$RaJn5hEP9Oq1aBFlo*dL^|1k!iVH?~-?DHO7{08q4j~+UH zv{=HK_v7bO_Zxo67d=bz9{sO(ZXOeurJVas_&NWFp7Xq|yhm!STHAl#wWJ#-nxulz@IheqjB`2*;VKh<780t>k9IaAzpFOX&SVDdaY+y9SjK7xFpjlr_txt5}c36`( zqh&8)<@B0oQ1t%w`b~2e^#{~Tp~-_Aq-zNuT+-(>|CR$Pj~V<@KDYk@u|@&B1oRINw)T)RuUU%nWknv`u_%R^=3iH?p`qlmlFayELpxXNDC zth3VbY$K=2P{><$KU=+x8lpOPwA1lxti`Zam(w-0lq1%(TUGj4+86Qbss=+B&guLm zv%eD21vCC!X1(2>qq62oR80KJICtT|{f?UC zGkm_KA#h3eQ~z7KACFb-Qtr3JG!k^f|26YjaAI8}4xG;R+4=s3jtC?A{=%FY}~8PnXTQXBjNYz-EmDurHy&(IaB;CJEC!&Euh5%O}c1D0#w;X zt1T|8A=2)75R+wn%SZZbaGPorwb`tLTJOqq2z9G&cXcc6bu~4-&hD^Q!)Z<1S@IMp zvl;fU*I6hNw{fsvbl`~n_Oo%U_^G{9-<|#O?B`}%v(DLL z6@LcYfS*ug6?%n4{xkXQfFm#{Un_f2woCRI87(_YwqoX~nXk;u&m?9}oqiqM8@O@0 zF>Rk-DgA@=Thhy=X{lB!lI)h;D!D-7ldJ+fg7=HB5*Nj1iWQ{ev%ehr{l8ZY zD53MFHY|A;K?9H8MG*Yu&CzPDZe#}-`rUG9?I`-G<9xWyG z_&VsE$))Q=J!*(;8OfT_Gj8a|%b`b4`k?PGhaNpofbL%oJ-TZNeS4WQj+%`vBiT3# z7Cg-hy?Z(IXpIP#t3Tr+^l~*~9IZdWHcr*Hi~7{NmqRb= zQztBkUeu?KTMoUbPaVA+dQqP`YMFK)t&-(DLAR*g-?5Axx<&1N*mCGa?Ji#qy{K(O z%b^#wjbJ(SqPCe>4n5j7(Cf>gN81K^X_>YeE#z`vK<#MTK!01t4(({$K+i6R9&Haq<(2isCGVO#ZW}ljYmxExA>Gxa>*U zow5rhlQNI&=$Snd&CLIq`QYTnnc&Q-=|4|@Yx*P8vFZ0npPxKU`d#T(X;P|`ydrr> z@@WYn{-O98jN|m90)YtD7Lw^WWr*nN4P~p=HffDjO*`PGO(j>M5VFH+ zW!F(qG7fbb%S-CR9ICKUlWf5*m$g|=>k~=951iK<^SVe3@UYh~UzKpvfo{|$X9hV) zMpG+!jA=JRn!99absN83*Ld%9FR)wVgzF@wPo)wYt+lr|L!cQ~yipGtbGS)_VREsSQk8_0c2~)v34{oZ zx~TLPBU2}`IUC)_Gg1blDnFg`35uBOPn%n(2_TLRyNdVtG=-<_KE} zLr_^xw?lbrn+(#%ObEn5jJOG1y5b3_Q9>Q{db(9lxGfID5HY#75Q;Y%@q{l`v+8=W zj)%hZ4UZ$%iK!TkHjyfO0~$SC^{6dzc$QTWvN}?+Y$G3HsubY=EHad>S7~7(L&0Zo zwVRDh1MOJJdLf2LjckrURhjohnrW+1o7LJ-8ul4NNL_7>1RAv8omZ=z6>E-BrHoqf z?u8sFW!sI!TX>RkAl|Ibrt!LwNCn9N#u8HzuBrY4?JaNb9lPWU@ws|HVk;ErmvA6SHr7p2NG>Z z#g%ke0##Qfq73={d2F)5=5VQF=}^$DS88kyM=+hF?8&$h)>n(&ls4(x8&A{mrmLySX3IpS02f#lj*{D8aaECUtzgvTQ^rgzV95dE z-h9@WDC8ZR9HiTRHUfrlNFtEzn zcEs`!%I2`TdZa#G%Jxi#j1KM5b~5Io5lhPzYEcfKi=^RjxBq#g?r zO-!XX1>4#L;4^B}jiyo})@nOd8D}~P9xF$4k<5Y$eZokjy#5vucH~V~xR$S1)XrcE zb78?I@MG|;OX)jI1H76Q-NhQEZ7_v+UWYhIN6FJE8FMIXF(;Z7YE@Rst%$p7@5DTe zR?ldmcd|K@x{?nuV?;<-cV~4>wPzIq`CgXRi)tXwTxyTNAH?!XNyzt@_v+iE(!6GSRV zqXgxS8GF?9>1>XUJJKKuDmU)V>P_8@&S%7IE#PrPR$aCy0)RE9mp26?`2aQyOq@5N0M%8DpJ<*`37VfJHuDFcwV~JtDNSIocg6V6vO)<%GX(HHNx4 zYWG^4Mj})QGu1ksqrizBnzVwG0`Wp!wP0rw1pNraNhW4Y*P|Z7Tel$;qw4w5js`2> z8GV&9*joh(Vq1`Gk{x5+8I4ghL=5wzvisN^_Cx{1!wfm>4tosF(VB|MVCqD&!L+** zH!AZ?I?~PIno7xt$WCE%H0oLv7T41arP|4qyyh?xN79}io~n3qXw{pKM>`%%IIOKx z;>#9tR7w#z6;WpmscuY%#tIZ+(3I3=PqU(F$I+(W>T$Hf`B+RViZ0}Eq#a?k#jlMN zNDHY%qiR6f7xdIAqtUNVD4p7n25EJfpb?wg!se*9((WFf(ZH&>4lOkB5|VNx!up7x zXfsi5A=zz*oxui98%+W&o1@%x_Rye?DC(P@I91j8m`p*VDzsXbn8ya9sJZnSUDJu> zvl(G+;O1mm<#v+ZB9nGmN<_n|G}@{$GM{dh!bvb2uo@b=Oi&&3XjE}_L?cUBm#KPU zO&53ZHshl4$9et<7gFp336da(px4HA#;N+hJ7Ho5#yvoc{EZD08Y553HlwIqYw&N zwHi-QUGEh1ZF5ph#q+VWI#C8!vD_Gp)vY)~`}|HSKXVP6gL}AQXrUZwog>!J#SEm* z+*G0oAL)(NwMDO5pVcC0Anv6buoA>CphL(M!|DiIn|{<(L=jBMI9pYNNf*OutiVidRMXB!6OKrjhDP7wL?`g2jdZCR@WoQ zRD1f~g&dkr6bn&EG#JH5d#!|IV}89SLFse#XciE4b?sPCmuXv!brH_yaM{#&vl}g2 zs#;RrH5hf~JWOanL;zDLT4V^nO`Ug8-cY0Dnr3G)q{9()Aoga*>#T%xIa3%+gK!tD zPFPZSG;Sx|X_{)(dP+;e1f9orL~|*TbyoC@vTIk>+rCb!M1}!bX06PG!JtBRk-8xg zHqmiUTg@89J@SL#R5pj9-qDAn%8VsM#IPuB_DAbFtmUiMaZ6kofK%BBY$DZeWjsIG zVsq#zEYY%;0&yi`C~8$If5w27os_;o#x&|~HXOk5fVzRk%|_q*-Xh_x6})jSWgEIY!gaz*jdmw z;QZ8%g&c6gm&sNfVS;FDED_WlB9WrQQZJ!d4er%s^zmrYYBMuBn|R^VqYB0vb%Wah z*xam95-ls+39J&NK}a;*U}+J-5D_j#Gq!L#FZnR518y7Qupy=KN6J2?MyITyYCcpm z$0LA8BV)tqTrgD3)Iz>?Ui>aLhZ3f3W;j!C0=qFI+DjY!xtPtUj#Bxi(d5rtGVZP^ z@3a}T;=>0y@?EXFNc%%3XIEut_kyIZNCa?m)1=JwY)G-*byO;rKs(#%VUl;VIXaXY zxA}?$YS)pCm_12r8^LHbg`t@wYKf*Cahy&#f>~!eB3#JPj`<^CJk2JNY=ov%W~~#~ z2GbdR#^?1r+;l3Et@udHUFy`KO{|Va6RQ|2AszVE*Z1X0P(Nfi|_jLT-wj-eP0qW;O*1A-x0q|X zbwt9{h<7^ejy2TiX029(D`2MdW?MHdJ&Cod>Ztq7nTR?PNECDS02M3yTPA-2Yd4ZA zJd^j?88iV_ZU&+~!+zQGRb>;66M-UyDOIs%Ow-j_at^o65Gwm=XTX!A7>o)P%SwM; zx`NeF3F^Tgl2&_-iq}IrWv%AU)lG;Gt4E2tsudw&V=ibwsv!z;_V526KXL!W>_v)K z6+QVrc}w=ZtTwZErZoM`bWyrTnv?uik_NB;&lmk%lo0L~;)2HoLFiG)H}(A~_vEfg zJIMSt|6DwGl8_)U37@Cpe4coZf6sgI+Gj%6+2@d}?0 zAs-hDj$tj}@i?Cka~~HA-o;wL#r^o# z#~05X#ah6FF^~@lAQubX$y&gJT96N0AQuacWG&#sE5L(Rkc$OJuom!;%7=fDiv{mk z=m*?c4f${@aIxTU)&d@k27DL|xma)*YXJ|U13uJ;Tr60@TEK(-fDbbP7Yk-t3wSUu z@*z{?Vu6CSfCrNzA9BR;?<6jsld~4^;6&iVqR7Pp8EXL#ZpMTA_W9zu8P)8qxLGCx?Q?Wu~89I1Qbri1_h#k6(=iJDOM_0C?tvru=B82zDNF~ ze7F2j`7ZfAfQN7gxOuQmewBQyd>*_v=*TN_Qod0hlLzEZ`35;6hvg^BSIJk(SI8yu z3E4i`UfCXSM`5?@QR!9S2Ex4b5@|}F4-ozO0rclFS$h0kyIq4WTPY|2}qoh4H85GOHP)o zlB|@hkVqsG;(g-1;yqw*WViTH@h@U~&AcwT&oxFfEJN%2N;OdJq9 z#T&$k7#5!_UL{^BULgj7?nV1VdqsP|3y9sKM@73t_lS0ic8Io%wu!D1Z57RnE)jJ^ z6%i@gD2j;!BByAB2ob@elSQjUD@7|r648WkpKz~mkMK#bEAyyum+&6pPT>yWcHuVR zRl=>pdEq7ChDAk43O5R4!hp~z+#p1Ru<&HzD&b1u3ZXjb1f*c2ASMV1oPrGk1iTSBS+Gj5Qm{fG5lle)puNx@ zzyrA(><8_F?tykfJHY#qZO~QFR%jl&1nNK)h=evmF(?2zp$*`*$UU;1vK_MRvTd@f zWLstPvP)zgSw%+5Hp*hMfXpe|AVXxZ>}1(0*-F_8nM5`*vk&Y=?U{LUX7|jaGrMN) znb|qB18`Gro4IOcD|l~m$xLUaGDFU6oQcf@W}Gt{W{}x^vwLUv%sx50d-l=UU9{YW{XXj@xne70sOLBJOY-~0F-nMK2cV6JxlV?}WuAE&lE18{8>{IMj z>`^?a*sXX}u?xI=*{Rr}*sj$Io$FRLsL_NFACl{Nbe9FIY@68 z95G056TD-P-YPhJkZu3uXuD=LL#E`ZA zCXf!&>jaWPdaXb_NUsry2I*%7!a@2OfnbncC4dI$rv+1k^izV#LHbF-#2~#A`uiZg z0(yOrejIvjkZy%u9i$(FUKym9LoW}~%b=GA>7~%VLHZHsZ-ey1(2Ij~9(rMr&Oy%) z(k;+m2kD2P=LYEqpl1i^`=PyqbTjmqL3#=F=RtZg^ru035%kO;y#V^-Al(H0VUYHq z-w)C*v}cfZpx+JBHuUr$Z9)G%NSn}a2WcJp%^Z?2!&G4Sz6ML4K!ZD znhdtV7#ka~i6+_x90&`Tj7cUKF!>pSF~%61cXf4VXSb$Bu?h&1*W5Gm|k2;T{j z#NGjsz}^n=7VK>hZ^qsV@zL0yLwpqW7Ko3;-VE_3>`f3O*c%~+u{S^rVXuc6!2S%P zANx~?KJ0Z6z1V9Zda&0(#IZktcq8`55Z&0TA>M%f5yX?&A40qydlkfMu|I%#4faZi zS7W~q@ha>U5Vx?GL%b6EJ&2pw??Su+`yGf6$6f~Ua_prLABMdI;$_&2Azq5T2;wE! z3n5;Ny#V4x*z+Mi1bZID2VuVr@qyT#5Fda&7vlY~=RiDxJsaY2>{$?xVRt}0iv1SE zBiJ(`ZeZIG*Rf|nJcKH}+(R zKgXT~@xQPqLi`za8^r&_W)OdhO(FgVHi7u>*cjrEu@S_7!-f!lgbg765bHzySF8u| z2Ur*4zhE7R-^bbzzlXITeiv&({0`QD_-(8X@mp98;y1A>#DB&r5WkLH$3grD?6D9(gJmFo8cRd` zBxXYV1eSvMaV!b(W0(Q)qnHly@392L4`Uj{4`C|AyRbOK4`K?$4`4Bf@5f|_@57=H z{}z)VJ_EZ5;?uE*KzthZV2HnoJqY4&U=M_NJN5vGPsQ#J@hRB-AU+v80r5%LafnaE zjzPQ)I|^}%9f3H(4nrJc8xTj>I>aG%2;u-+gV@JbA$Bnr#14i*Y-3J{EzAM2fvrHS zJAVPO=KLRsRp)<0tT=xTvE=+Oh?etb5R1tj~;^Uk@f|zms5MtW-uMkb=4lDNfxo(7bm&* zI*5Pgx)$Q!x~_rvUf0zS-{ZOp;$OM8ApWK65fJ~vbtS}ixi%rb({%;Jceox7@$Ih5 zA->J^P>64JJq+TXyDo$H7T2W^-|V^s;u~ETLwtj4F;4yl6X*ZN{u+b+0XtLZAM8CC z^bhuK4EhKAD-8Mvdlv@%0}%QLdnX3{1NO7fKj5om=pXED81xVJRt)+FdkY5rgS{Dp z{=wdaLH~eVF!T@h1`PTKdp!pIgZ&u>{e!&@gZ{x@i$VWjufd>yz}^}92m50T`Um?X z4EhIq6$br-{Q(C30}%QLdnE?_19sfdKiDfU=pXFm81xVJyBPEj_B$B#5B4$)`UiU{ z2K@u}>Civei!taQ>_rB35K`UiVH2K|FQ4}<=}ej9`S0lRzXAMCjp^b7WE4EhOs z76$!;-GM>>V84Yy|6tF=E?!x2yyVQD`9A#rrw8ui|M&6#`}qGa$^YMg`2Xt>|Nk?@ z|Nj*6|JNb@{~E;q{{->>KSuoj)rkN95#s-Ui1`1j5dZ%J#Q$H3`2Q;q|9?5+|G$U$ z|L-FH|2v5PzZCKRmmvQCV#NPng!ump5&wSy;{VS>{QtKR|GyLQ|K}q9{~W~spN;tc z9f<$`7UKWUMErjn@&9Ka{{M8u|9=zl|KC9T|1{$NwS}r{9i@Xj2_`eVFe=p+y z9>o7oA^v|O;{R^M|8GG2|0Lr7*CGCYE#m*zApUeqlo_>M*M#R@&9$i{|_PlzlQj~3-Nyp@qZ`c|BeO!2TyRH0{6-PgZTe{BmVz$ z#Q*;b@&BJ8{{Nqd|Nj*6|DPcK|L=(Z{}}QAe?$ELM~MIb5b^(iMg0Ez67hcn@&5$k{~F@|D&qff#Qzn<|6_>%e;@Jx?;-yGUBv&tjrjk! z5dZ%s;{X4Q`2W`t|Nk1||Nn&e|5p+J{|e&&Uq<}@9})lmBI5sFK>Yvni2r{U@&A87 z{QonE|9=|s|4${QoZy|9=~)|9^@2|6d^f|1QM;---DD zI}rbWJL3OuL;U~G5&wS+;{R_({Qpge|GyFO|2H81|9Zs#{}l26*CGD@TEzcfgZTfS zApZYq#Q*;Y@&7+W{Qp&m|NjBv|F1;+|MwC9e+Ahjrjkw5dXge@&DhtkN^Mw!}#xQ z3C#cRtgRfKu77nk1^#m1pZj{?f29YWR^4{ZQ@%bk>$tEVQ`s^u&okifnDRApZpznN z&!iVUTh(CxUxR7KnrC)L^<)-hYwY=ot0i^9H)BL0k>L2GTq{*==>uW9b*ojhdUIX7 zB=5uR2|VUfg?4KIsw@Dp+ZNebM^>+2ZMWun{IEgK$_?5F)B7Z#xM+0-6`*CF!~cIT zt*Xx#3`W2ec1)|9FNifgCs55=YZkUbeWk)OzG*PZjQW9;sWQ2Qzuo27+SDVQcUo1Q zN}np6v`)3;LQ8L!@|{AzXB2cs&QC$g;#NmtzzM5_z?tQo$Q5QSCGRD}#GoD~(-qd^ z32~fK3iUdk+^8<)gqD8FY~a*VX;tB^!~DkE*doXO5Jda(<&yXRY|a+#^EB@F$ng)O zW5?(ZpBw$Z1_eA0@O9!&36FoZIsTjd^4XbWU`+<|p?fCrf2Z9aoSS&uv-8Mo_p0S8 z9R7`VA=jt?*_*;)9G@#({4U$KdSI5w!qP0BKnTbE9&k?F=i4>E@hO~B!anb*5f|jR zoDw|=G7hWtO#4*rRKYZ(xSUDngFaKmXXjt8qn{*C;%Ro)^hQ{Qq)zt1`Tk%btu(@7 zx;2p`g&e7PK3)zg`L0(QrPC+de4;fj#>(JmD%tJVxm^CA=aRSdI(rUi#n|Jfz)%7c8^yYnz>=D{ilg7KHh zCGN9l_kt_kJ(j{n6|8=x+L_7jHVzm!Sv`kyMCS1gQu#@bzt@c@nn}#sE}Du5`&yK&v;Zb z+UdxHT%pW^Gy958pHk?l9AM6Sg*4iHnU_E|rShbi=mw2E*)zO+Hw@BDSP^{0GvsL0 zNVki*aYLPW#=(5Kk1aDI%HU z8?#VT=Y-NJx|Ism&wCgu72mLUvc^S>Qd`mdvNn}WzgaYWsi9^jd?PL#k*BQIXQOyI zpbi*1K}V{dagXd9Rbvv7g9Go_)6dg+Zc6%7fv#E~M3>C}KfiU9|6g`ESDZJ(41o0$ zkrRgwee(Ecj=$=7_xR1nk2#-k?2E@<o)#l<9!>?-^gv?4*&Z9th?4fvi`F5;Ccmo6W?U{(8rUR-d>ku3qdYft>$;?)nXv=Gp{bmA@N&OOnB^b^gTp zzn)zH=1gD)CAo?h)8nqslQ+`wfowE9T+fqtCu%;a6RK6iM!O=7rk(;;h7x>cNX9B5 z1Ejk0^|IcA6!I%xQf>yyu>{cz#$(YONTK8Lbj&c8hY}*uYI6NtF6?F!mgLFjX}w;J z=)O+Bg=;fKEk)be77>kv#$oJMC?PP6Jc@dH38~zv&4wJ~$$03T0@4P0&0#sILa4%6NtldLdRe#%)yRObzlzbY^p67g|JG8C3FdiY4j7J1GDLI=5{ z+Lo_7@D+Q^C`fyvxyUe86Ij00GyA<)AjX*e}HNvJG@LEj1n)fOM zNmdnq+M=ja>5^SSS$c1553lptepr)mLlZq#<^sKNpX}N`RDd!F366GA#W3WZy83|A1 z#D~hkRPSnVbr3f)JFb;;BxgvE%*l8se;4Yt@E)UedBsyH%XH zrGO`pPYa=v(Nw2nwOCD6(`1I8rB@dDjKt|QqXom7HzRgBDb_#JrI0KnidB}VWxPg| zuf~X$J{$Cds^dqCYqYCw$u0G3T#Jws^HH1!38|{gM2+_gGoGsTiGHBzj|FOB$HfbY zW+qhvyX6@#lskq%Y8_c_;+1qK;1Bm@JRuqJK#Phbd}aU2?=K`O70rl5YNc)@6(a}H z4sG)4C}#$mvf(kULY)ds7&V`sB-NF#zyvyCv0JQ7WUeIBu@*?j*HZ* zkl-D(6)EVI+ccpd0iB#wWlahd8{JNw7R!8lZ00~pOOH?<%xVpuuXuXnwl0S+S_tIm zfEXH1EB@Maln4|IGr<*7RA|~32J^Hl9EeaAFPP(S`_w`pQ67yuAZMOQa5%_^SL%1i z*>ZC{1(ZC<`6D2KORAk0>n%=L}TgxhO zpB6XWn&G@-(NCl$m8X+Xi0b#aEJ&_nkOfU6hq$*mjZ2M@h9`-lk&`D=dF7`I38Gbx zwxw7-7&c33w=juGV+C?QxfWz@QncGxmUT}#W81PJ-2IFKS$ZJ!-q$r&0zr zq?7)VJ3h+MZ9X_wt21GonRU~G^OH!zHwzT|rCE?QtgucrE7_Vt`czMGtfZ1A?}^c6 z738{MtLc@$fi4%633wPY%U0w;!#&Pw6Aw4_&^XYdh*q5G))JnEU^Qv0>c4SuiI}h7 zO1b4`%Ls>49)E@(QiW1r%4dRje3A$i#aW^d&t=NV^2)0h5~5s@8)V8CDB_tu7ZS#J5AuUJTU zf<3c9t3oj_=y4$~;F0h`*{>BRmKpRm=)`#9lk&01xLsdaBtPUqZor04F~NSY=4G3+ zae&k5$tUYJ^te%rw}v7Srov2mP^g(Pe==6>CZtHB(d~O1b+(H)q6N&k zkQlYHWS|nQgPXHrd4jD4S{<-)323zumq<)yaY%vOT|_;EJ3qUSn0Rv~zUz+jy|Apz z$cz}~Q>jo@EBaD-k5?&y3`;XL-wF5_$2%4hgPdQb5|J($ZF(noFFP8>T2wh>R)RC$ z!e3-q8qUilCV5&BlwLCA&dO<}4W=n;pnDVban+rpyW<*N%YvzA zF_G}ffKqwhTapW=)vWhQkshz5Ns$6c{L(RL=5N=jG`PE%kai$YNi;`|VZCA%jD%Sb%Tq5CuO~E`9`zkBg6`l7vY*c4T*okJ z#WzV5DTeWZjmF5$jm2!NmTK{(PT$D7>*YmOG)_$m!77oEq7#YcEk6kAe5%wBYhV)U zS!!S^abWK!)u%;f<BnZ!RQ~qv~|rP^d;m z6uZ6DbXX^1ia#P&C_X;LgPyWS8x+EQCQ^5Na3PTh8G{sWg>ym=_w!PTO?lO6jqps3 zbSP=WbYHRFC0nG$3M)^AUB$+g5e*)qB^u*Wx83(vD5*9JqFrJRk z)&690i5TH&6gpHHmkW`enH7rNvN%x*r}CMcoQ_4cRwpJKQZJLSly+mntbBDL!AO}T?N5xd$#$w^a=})z8d0Nh ztCk-!8b1`x5Tp3PvQQX18Vd=3qS2_RU^)}WZFjLkH>;x8!lf?T&^jY;fS)k<&{y$h z)xL8DN%*9$pyu5Kml(#xc0MM?$Fh`^G_Ka^szrMJzBu)2!FYk!e8IM3u|6@~GRV~lcFlC5Veq1oPfZm?&#5yR1k?qHfzwv=qU6M;cSHHYC@wlom}Oh%|Do1LaqoAjf@VAR8> zCxyo>1nO<6k4GfBT^y6?wnW65bfe{qOoE)mX$5l-%Y@}FX_Be_BDXSA3u&qNgb58> zM7`FpE7AUFz$Fs_pU~~MMr;nSokCjIF$^t8~sog7oABMTwdR z7a~GTsTjSQHxWv*paQdF;c+HqM75|i%+`|L@>okrlDm-Qa6umrhdpmD6Y)2OJ;GBm zz~FE!`c}zS8)8DAj7nf;0)&C?7`|pPpN^P>)#H6atfM8EcyQYEJD#zSkinb-Ug1ne zK4D65kt8`ara4^ka&p-t`v6(#j6&SNs%w77kC6nE%GLe4!3;v7evO>ed$bj(sl}4k z7OLf9S8Jqu?pS5W_nm*XkYLN^NC|SX$&9m1feCcDOeN%{imIIC%zQ1L0|U9AkYY95 zu~-5bhGO%gzmWGDlYv|!T3W1c3Eh-$%7B+#G09h9rJ2DNsW{_YTR1S#!mVVHu9UT+ z8N|bhnA#mQ5^WC!vV|*^xY%p597jwQ+E`l{<*(tvd^1p})Z4Q%Z%vC_XEJV5eIg~L zN>m`~4%8yJM9{hN%3|-v_;bE+tQ2mjIWbf=JO1%VYd6zr z$6p*ibn%L?^0^h~#g2ak+5CRrF>%Bl*E&|fIr9~8uKbPQ9QhN#x$*mfbK>s>=fXSS zbovCGZm%4=Wo>?@{)+XVI^O|K&;QB#QxCs!<#XVa_!UR4b>8W$IBx-`!#@sAv^*6v z!729-f;03u$OG__jShxm>(2S<>^Fkb*iTqr2e||8xAE;mUj=yuo^a?L;57FA!0GGv z9)HP^_}aCLD;>IcVck6~I&0|}+wT3+`Wj4iZW>xAnu!j{QnFCulRC*IY_DjK{}ybg zH!Mk+5=^7lDNm<8LM3eN9G5NG?$RaOU9x1miRvShmlFWK%vupP)5t7vpD zHWBiP21ibbj;;TWUbSSqM=aTHbJ0#_4W*UVMq_E5h}Y9RX}j*)Qx@%V)d5|sb94d3 zeN;@0?DnmFdC7KPTC&|gF52zeFDI95cm0y>u3NI*wM(|UX3=inarCSu+ugBf$0gaJ z#!cgDlgx8{hHl&ZwsC05c56$vTV1l9Ysq%llI@&JwsS1mZe_W4>%V~Q_Mg`-U$Wi9 zmTdRXg?;;u%i9+1_Kky$CEKkp+U+~u+)K8*;qd2HuD$n)vh%+d`u4rw*^>8hWXX1i z7wsBDkl-AoKNngOt=F`feO^2IJ4?2E$&&3}uxPh$Kc8B%-HnTO`_3=hOSXH)lI@~W?Gv3p zTe97MEZOeIOSb#rlI^~~Xtyu__}Y@~{%Of}UtO}@SLWaU(<^_ta^mwRUVdVH;!!6Y z$KQYa4v?eo!N=nm~$F4j2FGt@3va~(=sPo7Njy&s#dgLL8zX&n`T8FRS_`$}X zZ%j8Hvw^MuwLjt@Hw$EA)tSH1!a+2K$4H1BfWy6G@>`Gz`#+rlED zCDJLCrc@(*nuDVHZld-YkxZHq<&2RK)8W$$6xDVSJ*U(vL+de?qBU6ypQfRxx|^uI z#v`e#e3W6?I53!nqKVx^?ca+>4T4T^2~p?5r%5QP>?Uge8b{U`BC2FGJrzDpK+)K4 zqV`&(ltK`iMllo>K79)mm3I@hm&&BmX~{?_30(}Iz8Q)}cN4W22bBz7Arq#Wl*6YV z14X6XMD5=aDp4_MiW$QM-SlWED()s~4+%ahYUx;vPch-skAkAYZld-;j5BHQ=upvk z;MYe&QGOTEa|*apq#l)Yn$8g6(>Fm;ZZ}bTG-b3juc~nnkKxl1D9Y|8YL6x&6K7c} zn$o54=`a*!b`!OSgc@fBIjN{oHhek+Md{r{?IB@`OghO%4e$#5bP$SCyNTLEB1y$S zP_Qv65k4J&qU3I(_K?sF4V0ltDH;d~KNKZ)6Sap#T&9_HhNVpq!#*f_%Wk6fkl>>R zO+;Be31$f|6uo&D(R1p?>J*{HM42U(@M#Yeeavp6_K--h87(PfOdgCJ9Ev`AH&Od6 zkxWH1I-3zC5W}aS=%aQMwMUb##SKN56$J$1jZpNFyNTMPiIlndXG?-ahflkq=uNwc z+M_9%GW1M5p%8rd^bJrnvYV(qnpio;X~45`G<^Cb6bgLg->t6D|mLhf?b%1CRHXOYCN@d`VsI7_$@wrc-{L7SHdft+U*KqBrg)YL87RN(dCq@)C&6E1;-*H&J_Rf^UP2lp&=Iurql$6un_LQG09}hRLW# z47^4TpS~Q5p4?5;9-E{n%Q~plno`539|lFQ-%ZpWn;BY7#S$dPfD!RfD0ZuCo2WgSGBF8!KS_%Qm|rh~qFcL(+M_8c>KQ^*7#4)YL!js* zb`!Nnlc1<6MkNdy?EM}LMRz-)=(R^vQjro-flegAGV~xQy1Cm`?a`z$g3iPU-2^@S zKqz{}Zld;R0yTjNJ;f(v5W^3Eq7UCi)MJk(iDZ&V1(f>(J$!#Cdiidm_Gn^6BO%FD z+63W$KPdXJ-9+utWXLg5jhl%S=;0Gk^r5?n+M@}4wqwSlNs|QIi{nuAvfV`O(Ug{D zLWsv17C7e^6uopeQF}DWV6#f4cp?RQ_$U;;WH(WJG({OT1wJlHb6{FK0!1(0P1GJu znx4*poi0Jf!lw^I(TjEywMP@HDkLbro=JceYy*luWEWA~9um>C$>@R-kAa=ZIuw2I zE}|ZLAjWx6?o?Gt8tiiqLD2{8B8uCiDJIaSOfX>S*gCxim!=2a;Reyut%^ zyMjIFSUoKXOd?8w#m@z=aQ|C;c5mJD3WovTezbG80&IVvynp2V~M8uRmse_0UHSJ^xVV&?DBqyY?oK&;I7Mwbj30eZlHu zSFdz^$Mt5{)HwrB0M;EJbG*oLt7B{Bdn<2tzIbIN?7DT+x$`~yKqko#?(mS{zW@3) z>w&#|?6VJbN>O4l!(^4H!`MU68@9(f$5YV+_~cO_99QotXrGrrCE;W=CNYNTxN1*9 zyQzGVV>ltMWa5skJq6F1Sfad0By@w-9FGXP9=qw-c>s9rV_r|iq!>exQ9ZQR^+NVJ zkWPZVKf|gr<+yTBLHjhO@FFQhwU`liZ0;#&w=|tdDuy2A*tp}0Jp_F|d)MeJC(68_ z8&QY8N7tNlM3XjYK~>l!MLue;>z%Xzlq5Nx%#d_6;YjQ$Xzv<2BXJ2`a!;(H3(cM24pig6MzatVjBr=Wd-k|aVGOofcG zj@X`p_OVFv3aAsDOl2sCyr-ajEK+iYPf|(3)E&`11?^)oWwIuhlrtRVkoFX`2V9C( zSx(_&9O)4E3OM@(GYMV>n+lbu9=X@`&e@{!fUqX`I0L3BVNXGOz{Ppi;G+t!i4J~G zL3_YS36o{bluVHhZcjmbz=^y7XpW??oP*s{&>nD-X%eau)3vCB*;CLSaHO6{WnyYl zk2~l+1?_PYWd%jo1n|7rLG3AMcOw}~vT96Dn6iW1Q_vnahLkqxct%we2eDV&*yR#> ziV-y}Nk}*Cb-i=;u?iDcQ<*eJXpUR<6tu@pQjX}JY1)(=dyYojK2d?M z3ez!;qh-~xw>#_s7bj9eO6Jo%?}+SqkM@8|h;dHPby7DS;XMWIfs@IIf*J?ZSaO5{ zuJo>(2HP7ljgBRC!i?vpuQ#uL0)22AqG#&mu1?^#)AOV>r&5Rm#`1TaEhbbkbxs=Gq z<1vSKPeFT_Y7!UKk|ry24$q!~_Arenqq3-(LQHkwdkWgal;fymhM+`Rbe!5#&>p5r zoKD1fj@Bf{je82(!;}{cNzmjp%{knA1)Sagrfe#4S;%C-iND=Bp*L&~xReH_s5EC1 zF~<#i3feD7N(qG$DV^0FC-)Sz2b_`2sH~}H*ren7Jq6DRIMx78)Kxm|xNc8Dd%$T@ znwAJ&A_&K|dkWeE4m^e6z)n*pB*!%{|DWR_D@UKcev#`H;4k<60X^`H)$R3puDWMk zhdoKF;LP3+$MUq(8_FtZdzy&eIajZ{a-C zllUUOKoE%Un4V-fWT9NuYf@?K?~a*HY}gKIiP5ls-swp) zyf+*zGh{yyw~S&!?2$w>pSJh{U$k2J5b3j+gr9A(JwEB@rkvqDNknGFe#Dz=n6XGg z_Q%)~J06^D@)X}4P@xF!Wr*^XPnzdm8n*h?atrk5Jjo%*Ky&ZRW$@NPdXlFPwhzq* z;vFE3-T|U({|daz4n*&-Y zSkEiWW>hC+4YjB?t2fegoDC&RN?mH)&2JryyY2e++I-wSx8S(2)xVb=cb=WbT{hq^ zccg51thiGha$2P6`lLH_SJdD%ri%kBF|(3^fzYW2VP?dGR1M#GK|AP*csAYRJek&w zo%R@{WNd&;dyQY!NE(8?kbOAE?(#JG+yt~BJuHsHDS9poKEo*GhK+eL+Ig0|yR!z~ zb|yP6%Ci@k{}v_8z1Mw1b!UIK(xU2%-_M1o-54n4*sUv8dV4Lw1Kb#&MfmjaH0Rj=_VL} z*WPiNmm5Lw1qL&z3aO-=k`r;QV;SRgt(vQMBXmxu8<|`MKPlGz=btXOItB?q3#~x4 zSqn>i8lS59k!~n?wNWzhSd;C?s%1v$6sTOhBgG3`RH5nvQOwi6a6IB4X(t2IXdrsh zPv9vnf3nZ;(|mHtbh-0epD**H?NzwUPaOvtv=?rf$A8)BX1wt+`9uztG&~Ju`03lV9#LqL!YofU5J|VEen8=AjLQYczQxS)e{9vWnh706hB7 zWtAe)2Cc4~c(j>3ND(I+B0H$0{j|Z7tx&aMMk8{frz)A0mWhEEf;v+%;z?T1gc8F> zIbwE(7D%`elf_PB=2Hfyt_|aX!H6k}LxEaugFV0XdH%n{zrPY$dGpHQ>fx(5zPItL z4P|3v{jT-rtt;yfI`q~<$wQlK-&lLiT5s*t>W^1n2r9{My1wjsm5T;n3;Z>9J0@a> zoF8#EoyQ&TcRcgNT_>J?LIU;EUwZs;$FDy24UjibK6c5`FCBgL(ZbPdkKA>ncEo-7 zCx_qSh^_npT=KKKJ!&{IUditEyOotkt*X9b&dv6$mB~>ZB{q!FxoECe7i^C>cUX&@ zAAkI+dd;oN3oi!2&XMQ|+n=jAKXucpdg=hS-M!82J3n#ewoFMRdi{8FP|l@^7}+cD ze_M_u2X5;WxUD6H(QSd$e#Le(TO1axn+8^Y;7Fb6o`dPxaTf4+?2juQ#syVxf2iqD z&h-A0)}QTtheocdH_sinr0BVu7)K%qd~g62i=qOK*v+eIvlJy5%j0 zTPmx6@7c+rqv&f*qLcn)2DV+zS*g(j%zyvrbKQj8x~%YCgUWl(Ssqsixpi6fXWi<0 zG;-^*qUXBRbqjLqvMLsCbp_6J=%~=3+np+Ssh~)tP_g*^J9L#j)1gZW@71Bcb8_&m zGG{t;N%dztbd@{Pp-YOM+o7xcnGRi2#iB!3=`%(ThT~+>n3QHQp(04>+|1sgxuP}f z+&G`lZiGC*@$oah*jMz%b6A_>!~RwE5nz0_mK2_|A;-tgIB-d!y&UNA+FwyPK6F9V z+h1fjKDu;QFZ%0jd^kRECO($D@pI$D@xe3kv7`z#kNz#1N0;@Jjbu9BkLJ;3<#(P( z@47qGEMme;NKP!}IH5D7CiW;jH`JWHyWP5+@SaftQT~72| zw>k%RyLCAg3%5EuU?2EE+g=bEv+M?N2V*{oj9FF%GUlVmyC4r&Yiu;E{A4tXCdX4g zwZC`QI>@`r3h(7zU&#Ivacy`()!Sb+uC-9`ENlJQ;92Y4?XTr}?OcDYoe7?0ufFit zTK$o8s^Hjc7l~1O9IfzTp$U>PPT2bX{<_FFpZAxSozMH1J`QXFAG^HFUM}|7J^YZ! zoLelf{HzBqCgz*l|4{dz<%#^mIiiL|dK0bf{fALCZyQ1;RW*;se1uHL%7x+N;T zzBQCVJrNERIf#3`X;$`^-6T00Pg+!pq!QUmEZGs%tbkbkxjnGiFPZrfQBVxAGULW6 zrjxOS*{Y-Y?DzihFXli0^zl1$dha6%;k%6H6vzG}DQ;^*3V_4Y&OcKXiR ziEX8V9N#G=ggmDd$z(_Jk=ZU6i#2C`&(t)sp=N&)jwSljiCo{|Y03VUVsjf`)|~%* zxx93bUa@`I+|p~#TI$^L=-FPZQx_ZI0L98dcXH$@d0BNR6swtX6gLFf)5pE6Rm0C% zx)|{L+vuEI_de@aSs$DKeD+^1ZQ}`>+_$3=bU@6>h{g;i)>xCzSXRU3oM%`GtD|k7p{VQ zzQJ0nB4oIfkW+ap=GBM!N!VA;HV)in(2(YEZcg(_OLy7k_Cst&9@Hs(G!|CxX5(%pH(_Jih@-fOw(O(Ll-Gb^RoI#ZCc9bcKw8cJR8#+!V35F3`F z9hD|WrF^f9EZt$b*>}Xi}w}nZq(e|&O~)-JFnh;fGq;BEwP?4y>X&a zC~=a;_@rj89I(nIaB{=bF6Z05fy&B>veX`h`>0><`J#CL2!P8?^uOko#{O+-ORw0z z|J>4hEjR4)SV&04QSZV>jmyW}0}X z)v$;xd1jJ4Gn4OY4qW}*Kbf0@x0dd*8@7+mjl9SFxW1Jh*630vJkCd}zFaF@XyJXS zXN(&yQfljyNRH$xgiyk%a*-Kxsw7px~>vMbWwLBi+yHI5BJ(kCP`xP#a z55VVkKJdd!+j;f&AzK7s2lrncH7aP9i1Ne>5(ybQ$O7J@kW3BN9)}~|9Yss_AhJA)$gy?UEg(8v2SB#=Qo{} z;~S2`%GZGS?th+q=VM%!r5PaDeIcLN7)*G~fWN*B;n_3+#expaB`Ws)PdIo$QSFRuoFMv`I4C0`*uU6*ngh%RM@SgRlLA6?gqY zyWr{r8*qVLaMghgxWF#hf(9Ibzg}n;JmSCxTwoVmd0+!BunRV!0S8cepT$ewcOrk^Z+^8s@Cjp8jdS;$&xZbwa?{hB)eQj z0(k4-z|=Fi?IZJ}iqF2xagY}!=QRp<9Cg88%TafLcQiYVx^rK^|B43S4u*h9ay}Go zsX^wcUCteM7b-=8-;aAe;0imQT(mqmICzfFhB3pd=$eSy0 zCF~iF?Zb-|wdpv>OT_aU%lBAO1M}G3acl>EEywl&UTy9)wtuxN>ba{YaL$=!c88Qd zp&iHd`L3tnyxTm7(2m2H9|Yu)w;U4l)Tu;hl=rhmw#$ZlQq-Gg2SbH9)oOY?8B4B@ z&wD-1C}zNr$W+|dG$&C@s-)VLFvW#>L@LZCdYv&b5;@O!iku=fzA`PQPZF|_n32F7m_B093_t$)tn7zS?ti_gg9y@0;Z_LwQ{V%Sf9=~_j;nxIT zlGo@{Y>U&1UQkOYXtblkpriPr!Ej71O~`=iXViSk*QlTO@GHfe(`YM#PjK8rMRcjx zI~lJuhoeO8RQ+U8kNbnonGuOrMmbdr6ZIi=ijRzZOn2hxhfRMY*3GMec#6-Q@_Kxf zAm#at%9Bea`ti?i9SpxY|97vvb>+lsPH4yf>-ZlYmyi7f)cxa+{_yC_j#5Xyf8>Qn z9&`9xhwnTb+4$PV9UK1jFRed){nVk)9lHI{^=qG6d*a$vs~=q*uU_H0%hhvThW#DZ z1ZM=^j= zheUQbi!xO|YbtDGSof-ZqGNeHO{SIUHwty3TJ{_IHmK1C)CQMOi_a5NcGBY^C?IB5 zqSZ+-pv1^=Fcrw_*>*@T_}clY5a~>PeSI6$Zv$!rOQ_X^Y)@}$gSuX*5Mo7aHOfMS z;?1Ea#9Drqz{`_>9#r&D8`rl%Z8xCSzl7RKca#@r$$}5}qyutwkZgr>Tt*KXiZyPg z{MGDqtn=C+*@zJOHmLgs)cTfCn-NAMrbo@(8apvK_qc`!m=w`&1e9thHfpz2Ps|%7 zxLlV=>)W6v98l|BLTx<}_YM_4SSXG=0bF>eO$K3^g7kZc;qioGcz|Z@-0Hvw?QpApcY?3 ztx&Ya?Lo3E_1JRLXOyCDnhSP4LA^L8rm={UP_ucF6@qSYtZ#!lb3pB>CDhgfHF8Qc z$wVU-8un9ToNjQ8ms!yl#r32DQ#qkKkEmNT~WYs80vfx;HH`t{dfQWt0HhfQnG>QT9<5X$BS6 z)9smndvjLKC^Yg$+gnNm{Hfxok!^TGg^V$q6@~?%7Syf-YHwIVZ6#asVd zUG#XYYDk`>6)r+rR<-E1GKE$olqxe-;4)Cx4yZl3gj$ibr1EqU3Q_$YmrYo@K^8QP z9OB;MG%htp8lEJIMoykgfm%@W4ye6;3AM9Qrl*E`YLy(;qaA*x6r;7ehPOhTZbl6w zH_rHqnF$*tS%1&~E(3M+ zfZA)8P%F$@ai6c+=;2vS3{*X#kz`8l9w+2|NspDwm%6f*Bs(l&f#CpZ>;bh`FQJz3 zrNgOAXPk<6r-eZvZ&W9~de2*~dMtm%@}wlm9Rw*a`~GxW-v;&ffZD5;P-{g*NnmR! zlIWH)MX$#!cRfQ>jZC1iT_Oqm9IrulY$f)hfn( zhN!Y7ep2*gXOX(T4eI>?wVO+*4frNLa*|Gt^-3XHqNHLeQ}*j2OOEwsQ>H%2SoxGY z-fRubu)Ymy0RpvGETL8e$LsJ`h4x28yyWR|^>J!Usfk9;2WqH+@4G#H0*@+&yIlmU z1E?Jc)INL(wHa>KPFKt{^leZN5~w|~|I?X+ z93IlQL9Ixj_V^NN5Ab1~z76U~0=37MP!J(Uu|8)HW*vpT6<;Y`QSFSw$*bjbrf`7Yn4V)2JcW!l~a<3M|Lq-vtm7%g8 zU1dY1cHNuo2HQ%sl`0O3^(kR>Yqj>4)~~kJOd=q+v`jK#ac+@iI}=ro1?h6%>T9#K z435ZE#B#E~Im|cpqRJALUL{lKj3{kS$g}aRvo^RRgPJ^%N($Hyl!wBo(Gd7vZZtIU zddvjp$7ZbwpQu|!3Ey&ame*Ip2R&vsPKNsBULB;T>b9rtyuXyiBLYj?~Jjc2fqY5<%_rWH?c$2boHKB5aK{ zzc7hV8KEO^LV1{)R;t5}FIpF7Y%QM5NuFrO*XaaPL$R{i z2JE7{`=QHDTkc*lY${5-DV7DM7tX2N%=E-PooFQHskX*he@Url#QIjQl!?YiF-F1L z1zlj1(~?xpNCv5NLCx7bIG&GZ%E>{UBJ;v#OfBa|y=Ik)8_8U<*6&a4Hn`5(fU{F0 zl_m1|2BWBYspg-s?Wnu#kGd6tVun?4R6K|`rD3YSRUg%p@m!?Qv~--)3b;9)lqO9) zIiiF~wk4DX(;nH>b1`tjelwTT#%iV*BO2A|NaH&-d$PUN2c5M+($uC*N@Puzg;up9 z4B3K_ED&_ETOIetwMLn2R3hnFO~AK8e5X2%_Jo${AIE#LC&l_3d@JjB7rc>Sy=T@N zMADlor{pBQnM;kp+0qI?O8Ywh!m!m1FQ52?@nk@yx5hc?!I>T_t-Mr?^y?;_>Sihv93;Gx z;@zS@y`=|wF+~WrCz)xT%w?jzyiXg^{RybIg6p+ezzPiUVycpp#>}QhGlbdenrd}6 ztWX9Uw~x(@x1Y5k&<`Xc%q-IO(;cD8)&1UltC1-3Y+W4mxLjizYzF&1-Ha8sA{nr?}wG= zoVCFpF3F(&LNaJ+eJMxK`GDZ_nc_&xi{qXm`rBfLRr^AK8E;i-VO9x(^RF`^R0znk z23s+OMwl82iBX*cY4jSno^RDlcr&~iv(h=rO2=42r@D<^gVSxkIP`mGZSY3?p=zd6 zQgN%CY^P?aQL!)z`nhJ7Dgd`vMV%PXJu&K2x7tb~Xpw$_7`RO~V3M_5Ml4Tf7L_#9 zQ@lkJc%e}c$htHwZ01|~FiGYJQIF>f>C8a0&kxSepS1zcm+(=176~*-UQPBh{!B`0 zrZb~Prjd;XGUH?;3qDIpm%!(%qHeZ|BdXWSnOu&^vV1=n3QdG0FH%0!XO`Uo&)AE{ z6?uYh&SGR|+7W3@ZjeQ#5N+5~d|{)rHgIW@Otz$Eq!7{u;E7r-UuLO5H8UCy8`OmK zjuK>2OsD)hv1KHap3pEzvz#R+rGPgX7}py)p~wZzTvWg{cezh%B@a=i)y+gHJ;)gq zif#8}CB9@Nn>JspzV042P<((*MjCE%S{^8-r!`IIf<-1qXCgH}l~sD4C_d~N?S6X8 zT_R^*OKRl#QYMwuhlQzB#cN_9GArQ?IqjY1$qM1?jMFTbA2MSZlo>Kbt&xywYAhw& zY*_1`mkl*Br47faY^)e)vFRkuHDpi2@RGhjIA=t5H2iXL+{vGBluyc z*bm{|G9L9tbBSuaJj7>3KI+cYV&JJuo=v4_GbDI>{w znK%>S#nCL#o&d6!xCzLo!)7>JM;zO@+lJ1>-Q|6TIgAPQ zNw!o;7BshSI7rt^Zhd=BuPo=(aek!H=S?PF)$=Y&iOZd)UA+hDXc$eQH$2O*-2t#Iq&U z%%IjJn;7uQrqv*Y0ucKjPPKpC5+;PJImFgLGZY( zkjO+EV}g*k#K>e&Xz;NST`#M?DrI)7!wOkVM?9L+Of)K!G?=frX?NI9Z(7qt0=)Do zG_(#$m%$dvu=RuEwtLv1<9M*DdFja%S2{_5I`1vfAgNGLRtHTPyhYOwm+oTul$@ZlMuM*SC@dH=g1~LIVtE zyxhr3Os4~$c(uxEyqQdEWPV!r;Yl}D@b@<>?6_JkrZe?%I+hq)4b7fh_Aqx38$j)~ zLYoav!NN<1$S}zjtNOU3S;KU^J$45x;6;BrouZBQmXP$#0>yr57Gw=8tP{;jwx*Ci z)l(cRsiet!Vsu$mMg&_;Z<1VIWSXLu6uAaiaq=0<<_qVi&e}lH-jLGIQhaVIO}Hto zRdoM|X)t*LC!%GL20>B1Z8b|Z)U8peUZ0p{ww3F5K$;8pL-2kgO(@`6XKKl*l-N+iaV|?$5_Jw0LvcPf8b|dR)hNSxW;#He zvTcwfm1Zg>Mo9Lov0w~}SY4Vr``LcCj#)@+Hb4ccPD)B)0mH`X83wc$cCfTfwM5IL(!v(0fb)=~H#J+0De zPo~C$0iB3~Br;+}kA!;BW>v-*T-T!%H6n`5>ez^ya1M3cVcxV%rdzWVOC>T4yie3M z*(Jo8HmGOq9=IJf7y4o6O)F<0aQ~~8Ba<^CZn5RMn95`&a3U>E6O2j8(@1?>FGO+# z5q86vI;tW*R*c~}4Bc2ns!qWDY73u4nOUrE%&Img^m&=U+7&6@NF%U=b*oxvRBaZE zHco^ork^ljHQs~y{v_!NW$>;>p*^}G#_~lz6CR8E1EHKl%!wmRD81Pw%xr0)BeoM~ z7!XwMDv_qHWhhI)b*+My(6Fj#Rfr4sDiR_Mu~byb=oS&`VZ|ykO*S%2*LL!#WA>T@ zBNHzSTD=4k$&scoHOo_ZAf1!qvFwdH-GTR% zJHZA1628FR!=9;xFBbuSK!3{w@RPdKZ8}wsbkUxx*qOeQ5zripSyr)-FBI8)jMj(6 zLtvCuyjkt^9z4|&>5P(A!R|KGj^j4n)L`7RJIOLDjStf0luGKxq(!KM12KhVA~B8a z9UQPuqN&YBR7EtSJQU9o<2qVPQ@!GZMsqC80#JVZE$2-K4qxBYy?x8myl-i&2AQr` zr+F8oTDl;4(go>|PWa89CGGGs12k-)AKku}M(pAETzx=L6v$W_NbAmfH+oa?ne%Q; zL}2LB>_$}?GC`WoL?_bI`lG_6b%@L$7h+mNU1GXDfv#b4mW#xj*$4M-R8y%TI#HR( zv@-!@c2p~4Bq}3Ro^*S|P9IkJp*=8%R#kHZRn7ytNE%_Xb*7RpLrA@wH~_ctkkB%u zmXS{%D6K(PD3!9lyc_3D_dNVJZ{Ixe@CW%r1JKnizhhZgXS`^Sa~}Tl1XvfXJF)cV zrMGc_TeXYVwK?tIqUtVXwDKy9mM(0IG=Ou>Jh!mjapoWNM`NEsbI`?_`GBiVJe7`L zy6Vi7R6kC;LI=040!~CLRUVBvL#|g$Ri&Yo9wqf!{ZJ$iN%4W*jI;K{nhq26%p&TU zvX1ws%^?d32h)z+kF;x|Jd-4@ry&$wj#ZM0CQcS}^7U+SS zuj=y+>XYrAwa50qQ3g-^>q}lypv9d&<~j6H8L`GuS!60kr?wc)fy0{gguv%Nk0cx< z9MAUSOcrr6J;b4SdQitJ6TozCNY%=Wz{k46^&KjUIbu}T2)qSOV~{kF#Ypf>S2RHR z3<}B7%B<%7KO0dwJU3}Z<729aOc@>nbqj5ZSEsa#I7ObAGTFhX8KpZ-ykpBTF3Es1 zXbPFjh|Xo?O9>elRJM$E#3-%HG0KxQqpEXR;Ms{@y*$SNm1-T4h;n05m=4N4Y9jI6 zylmYBWuu8sD_@y`vf-&|&GLy_+6Xsdc7Z{AAhNOobGn1WCP*x1#GM(FFtei$*9Tp# z-0kX*T-Mc#i>5%)1&*zitJUXHSJUnlbu~857qNk%2!oDdTm|vEx^=g!KYUqNFJ^KniD->2R%$39 zAAau4YPxqtU7gpRayOFXg>)FaU(huOjg>LtqORuo1~X(!ELQ2LEi$i24m26h;(Qxi z{DsjxJTc1+YzSxEh~ss&e7CDVbXiv~<}DH_3L#h)%Q4TXuI8_(t26aVP4C1AsFiF+ zOCuV~<(OXog04pLE>|6e?J{CIN**F-HN&J^`XJS$f%n1;^bU+qjOHvxBdXWc&AVOw zSC@75V)9c86RAu!k5t*`R99b>trt9KgDfiOmg&bhoSj^0Ka8+>jM9u9^`!5l~t@hYSdH zRkk{h4$B&$8A}IZw>gp@A)k060)mdKaWLkWkE9iZ<6+8t)5v$qFiqTM0fWzLJ&+5kAuKwUN_YRkk{(GXhS;WFz91BlLud!cJ)xzo@I@%z#SP+iX9T)%jl3u`AJH zTTYhZk_{56i4(b99;Kmbg+=De;@8~m>Oa4vtM5w|RTOI&PcSUaKZm;7VXmsHaq!Xx zVNqFt!I6G}ExDs%DSy$vI)laZfmwqG&1{s+L@3z-ae6JcK5=xdX-rA1-=@p6GSM?z z^JVd|yIuWfmvl80S-9n8^JSdlI5eAk4t4dEx~gRn2fbCUCo5)&XJWvI#t3#h!YbEDwJtWx1}oGE%!h$9duDwXQpYi5zpijOkbmhEJj;!2hC7o z-DqW|?OD4kLX{{ZkZA$D!kqB`J#XduTXtT&@zm<`uIh#V^Y%G_kK_e+_(%doD_!0F z*o7QG0YZkn^ROT{d-L)E?%*#zx(pD3bCIK&qn(7fpP-7d?Jzy-7X__aK`8-A6*yOfTO58+>`0s?76p&U3p(uj zR79`oKoP%reEa#HB7PgVIS(+qu1*nS7hT~!=M^yszzcj_&N5SZv-eq81@8jzoLUBx zrrwq3tH~WgE>A0-LA?WW@6rpoQbmX>5$rH}(1)c&Az~-R8i+A$sRuLz!nfcS+0Ro1 z33wX74H$s=AQy7Ya?u%dZ3$0i98PqcgCsS87!a$Wn^L~6a^?()#WHn;fOxf)YNyk^ zNi$V|lJ#1%kdS+}Q;lV8tWLlpdmFmbY(qWl3*H zdROwmpMUnr-TnNJ9>xo+COWp4hX)|It8KU+#mlD*c=|I;BvkPGDWmX8i59y{4bu|3ADUuH5<;w?a36?&j#@y`Z+15RVdJeE53T>Zb#(3DuZ33s z)9T^MFM)@g|ER~~tu^j&Pg(z){d|4s4od1!(O70Y9gQoCaR{fg_wM2Y0;z}LNIdSe zRdpb@W*G(@H;Z}6G+}|Srr?=5DwXV5Q*L*Sc=vb&B*lQFkNTE$-adq+9-mF&nmxmN zD&Qn4CG>1nO*9lo?@55wOl#u_mMS5RojA-~a*PEn-dTo* zCQ3|WN-<>!)|jf1fTFKA!7IrjPZXi7l1%^=Wnd>B%W?AY&_e^7{t@43PTXiiLmQ95 z&1!Kr7*1+gj-(U4;ZRjIj&`&##q0Elt*1nta482gdT`q44=<;fI~~iXZ}j5rVJK5A zB>UDxsC9US9D?lI(=1Y~)e$Ni5z(AA3-@`%l=xms(EyzODVz^IQ*Z*%J9dvvfX17C zf8`s^`Ll*l0CL9uu>oYg4#@gT-?HvE`=J25js0W&RN`NJHWC9+I;zJSkmwnX5BipP z&Ts@^dF&qdPDk#~eM`E>a0H-!1e3N4gn5SJ&wLAedL3H477hiVjFgW%9vaZef9e~} zso@B~APE|+o;LbVmb2#hG8~H+w4ngZlK@Wp6wV(%Q*Z)MOLmVHfW|W%f8-m@J%%Fy zF=YR^1!Q@KL9!sYq_XoZu-D5Zcuvvmh`?WxrXE^?+ zZ(&bwI2Lc-LjhPY<>Q8j26Xc8`$ltWI0BGjf<}MMX`}zgaw0%qhGX$!HWYyMvU~j4 z>3IL1Z#4I4Y5>m4{_z`uEKgJayKh++XlejD%l`2jP9?tovym8p=~6we1Bsrd{x{ze z&uMA^^2_eAcsg>w>s!)2ni_x+6HM9~5awy>@Aww>w3>RK<3=a|lcs!J_0WJ$exGkN zr5LeUERs_o!rmyWRdV4`g{N`EK8` zE>Ot;*IDXu8A$XLPzj=k?zG|HCWavvTXHTaVoQnVZGEU)&RJeE7zjZ|q(F z;p<;~?QPdyw)+da<()s?nQeb)yRr4@EpuyQ^ZPenz48BTl-B?2I=uG2HGK7du1;1r z!9%X=v=WV!g z{3wv*nTY@g8oni6WF}sFDlfos#%CcU6{c9BBBKvtTB^ z5{UB5M1aEz-=dygYZlK-q~o-Q1$1wKBMskJ?wSesw7~%mDty0LJ$-{0&q{!qNS%TS za7^(Gfl-f>0E=fP0vu!b#&WNjNB~KmnFw&0;9JuDW1~^jiE$_VA#!h=M zz;S|aISXnV1)@B)4RBQ8Th!Cf;>FVn=@{~`fbI=&px_(JUA2vzHaNgBf$!I>r*H7$ zNrifRcskAjjuCugxmQzO10>BCRR7s9d`r4tQ;g&Ask{J(2cLyJ<@nV=o~M}sju3py zJFl4sry~~N_`tWE12=~R>Ej1=iI<+UwU{RZ)g4K8+iV=GZX}RBExjM;U?Z{5Y(qbxK}7i7s%VKI1$BjzpfL)kSP9 z@e4_hf_O@T2Y>EE8Uh%Yi+rjtNo)K$V)J_t(j(7T?0#pf2XvuMCektQffvRy6!{pS z(Cy%QnTKo)FT+r_X?FWLmZZvZc^rk|Y8A0NtU*0^ECZFYI3fq~=Gk`akk7RefYFt# z4$HF`Ney(DR{@!J89g{mL0BS*$HfGUOTW}&1J!PpK*T@$A zv5(E0?!_{^;P#gQSI*acdv*0{SF1N&tY#OI9zD})7T~P;Vo%Qi39K$wvx`WNf@;}) zE52vT&PNBG>7!ypY+ar9&9MF)M zNY2drqrgmhP51P2{q`51umTQOLjk7WXSqVA)0Zxgsf&EBpJ~kv>S8^grFXrM0o!eW z9B~(G?uD#?K~>lDSq?`dP#Ww&q74X!QjrMweOb8Q-38K8e3Als9FO;F>Vv8w$iMBW z{)K9&JM=`J!~{A4)yN757xY#ar=#&=uGKcISk9W(5{g*v>5%ZiuE#k-tm6D(ZDdl+ zte(|`bSoPzr_4gfFv{s_MyM)9z>K965En1B@=3ji7Kewj881SkLk2}Gyq+3F^CM(L z9y)+~Dt3qg{w+V$V@a>+o*J&+e&LB48h{i~VZ{nHDaP`n2%G9_%`}(7*j}~OCA-sj zn5Ks9tYu&}o`XMIHJpnCqLvG&6^U%vMbd+%PO)*fE{`08)0{@~W{ZvES>uip~3 zUbXp8oA2BFiOt71%bTy*_{WX+Z2Z{9*KKeckF0D_nj{^+i|%iR3T&G+8?@tbeHSz5JM$<>Ee{%++RyP=&=?)=uy5ARHOsO?wZ z`0bt0_NTYsv;7m>U%y@5`!{=U+GF=#cH>j$U)5i=!L6wq(`P@fL#ELm5^iqTz6 zyRZlEQGSc2_KJRs#`j3SMPqvfzeS^a#7R-j>L~4sDfcl;%ZyF?zI=6?$D7~bx9B%- ze!JhI-?aH{ev5wN=C}GSdbIg1r$u`cB}3*7Q4*Clh7CAzfj(vHy;~ph`_v~kzsc`Y z-?#M_2JW^1D>sB>n*g|tdYgOStl2;Zv5TGr~MZF*v6;)7X9BFpFAz9sTfuf z3Nurk@I$0uT}5TlCj9e#>vsU)}gkzeRs#<2U>k{jVGE^jq|oH{Rj5 z=r3*j`kkT$wK?vqn41}jO`+EAEDY?{PjCH#-=hC+>p%J}`cqp!@3-jxz4dc`i~ifK z|JQHPpWONnevAIZ*3X_6U8*p@v-Q!FqPiqt)A7g^q&^6`>(8*oRFT)d``Wv%|K`d| z&o533Kc5O+s?cv;d5hnoZ&`WDZ_%e#zRPdX?^^lJ)1p2)=xQo8Unqnc#<<%Bi zM{|p*u+~azyx*eSTG?+=cCF;MD6_`-ElRJkev49T%xO^s=VcM_HKDaprG#gg#iVHK zE9)CRi?03S`l{cepIZBj-=ZI1J6)^vWmwlfw)P3XPyL&wFe`{LQZfJ4+u=T>1JZd_MAt z$NjeF8&;n5TlC45ufOrzD^D(1n$PX8r&yQTwST_y*3+U(H&q|s_(#7*|9;~iZoP7K z?Q`4Y=bE(B&RY7u@C}={{T6+4^Vo0EuiyOUlcLMt!=Syq-=dK{+;7pty`0~oui3-= z77g!Z!8lj0eymRbXiK%=Q=2P(i+*zBpZpg6#Kvd5`~R1&NGrFV0B_-M25Z@u%Jx7{HY})II*x8|DB4cpqM^o89A2)dK^#Qs27ApArKD zxZ)wG%>e{($h_(ys0Idb#Y0dD4B(1~K$rsv@X)Is0zNQ+D;|RK96$gU+N-vNr8$5A zF0EJVRrvT*TwnlK>Z$qoQ*2-WSL)07_)|<^09Wcm`uJ1y96$h9;;WtmRA2yCJOstS z0Iqll$T@%jA=+0x1ckr=u6PKDzyPjz2=XU$fU!o(gu+2mJ8q1z(WscC;%XR0ZD(p` zs%WO%0K{7Zf@=_0Jp_1Q09QN&xxfIfcnGjLfB^RWs)ryO7{C<|K_)POD;@%L4j{lo zuX+fOzyPjz2wp!25FmEqYGWH7f9iE70Q?6tTrC^#wd)^P`O=l!E9+mj_PuK_xY1g@ zas7=q$J<|h?frWny~eNH-hJ2VQ(Hf`Tip4zo$Rfz+x(gBw{O0BPv7|QjnMkP-ioii z;Q9w{e&X_%fB*k^a%~Ho66~yn2!+ZByZwll$t1lBC1gslj&%diAqr0EGF7o zL}`seR6-l}BL@k*m77rt9qlIjETg$4B3{a*;ju+92{=Qf>uHkOFO`e-Br`#VB*$4~ zW=bz!+w6Yj8HNhl(;F~PMh4e^LfwAcM6~|2 z5g$e)wJxNH_K_PF)C77!{7XEHlJM`+`1U#UfrC6DFDEdnI?UaMXQWKf}AG4a8jzcoiO>YMoGJwY2jxb#*%xS-WNBD* z71zz`-7qrE$|X`ET^K>seL}|zgPv>IHjL`-UaH?ABG!!yF z21zSPVIRet1RfwJWlB!O z71GSCIxE1*G90Rv$Qh9kMXE6Cu?N!$qh;N6#7Rg-PaLU}#89W=L)X$URq!z%l4lr7 z!&))J)@m*vrdsB>%;)v!2qy7ivB4K%lNVa?e0<&?bkZec98;4=)a$;M(`-HJ3yg+v!h z*<_6jTR72-RUk!bwuh8bK?gZRaY6y!v6lx%uK&~-hVdX>gYz*<)16{Q#z#na(ANsZ zRFUD~XtajYdX!NTiA1)*)C4i9R3ru&AY7<|kU}Pr z)`m3*vPg^>wX+(9LW6|Pi9$S)Vl(X-)ShI?c6@&_E{tey(o~3=E%PKkm@Z)0{OvOg zLpE3IvXyv}(O9PR6G6Y1RbWc&Ge*h60Q&`)kbh4!E)w^9M)r%p_-w3 z1FMS_4Z#`Yw6-QDs^MB2Vmk1C$!K*lJ-a`t6pa~??@81GhSe{Nv^3>2YNQ^YNO^8qIj#O^TF?&BQ83glq$I0m@mLiuQ+GtUZY-Biyay!$C9eOjGHR zjZfJ)g+marUlF=ZizhU>)NEUQwaHZ$FkE}lT?|Ao0clf~GvahTl^b@)Fg)uT_Cy{) z<5JpA8Tm3{Psc7EMnc>OOUk;5*s`hf(L@S{xuRN`)-%;|zmDmC6+hYDb6^nVhtW1q_d(z)67071fzFEDWb@IF?iE zbuL5{3kAz*sWYlBjp624OmbtWUaq!M&VcG>H8(f5LB8Q$DI@GRf!AM|ms$+fAuMq~ zx%mYQTfcmU!6J=H6D}6QMI!>o3v|BSN@0=45EU4KP7`!NQd=+$8jOU>jfs>SvT{M~ zCMJw3*NWYGbtEHhzi08&0vD^5hJ|=KpRTL>SP@lGi7FEmt@U!{iLr=b&pN|kvT2UD zss>lfI-1;CTSKNca>5iD*1MQw^F1+I;zATPny`=v(x1=lc!85EDI!srS$2br zut<%WVSFJAsnI$cgZFW~HY?^^mD;ew_1KavE%FC%9Gqb=;0`Of1%b(nHrXgcTp^h- zAXpVK1tBvUQqU!hL7?s+9%>F#jXK$(gti%rPk@6pUK9sWGm;+myR~5>(Min=ob1}X zN$j_(D#K4?w8Eiez1KEd^uk!IN6#?y6x=pRRW5d_QWB?CVE~Qv9v@F3GK04A;Tlw{ zp)jHJ)R5fIs|^>>*{9NBqDl>tz`-k%O;A%R2V8deLKW_%v$0Mp3{(3{y#P1<=nR8e z2oDh#5pWwst<?Ey_h15pY+79F;|KQ07=1i&zOsOqzwHnZ#;k zE|j-RxYFtKZ7CNF28P!j+3|RbB-0FEh@o(` zWDm(1*=|n8L_eLgl@321iu-vaN28L3Pb&3EHKT&P@WNQE?tr(H=TJY4gj!)DMkgTi zG1kq`a0Myyd7+LL64MF7Iw?|Mav8yj*i^`@IB1_KV&zD=VHSH41ZARPwa!|pC`zdr zL?!4N>huc@me{A|f@ly;wKtN;0^_3cXrUi&eBx{@T4Z@#tBMoW9VtpbCPceXR4&*> zLl^C=3yTOieb}1aaab+GIk2yjs#V~4K44)3Q5_+wl1nM zI2F}PcGGIKM7&j?>|}p`+G*OuLbg4n%A-0vwDs1)Sgic{N&NrYR`$MV_g`+lYV~c; zvZ?rj_@zAn5;J|-oy1H54o{c!Y}^~(AM^6?TofghxG*tOfHOod3Npwk+Pr*#6S|AT zjDr9Sl$dGMt~G72uM-L#7k8@IpuV7F^Qce7Y$?_}nD>%+#q?4F%qYD`T2JPTRx(x`VLE{zqRkBXZ2W;Er4IR6 zr_)gt4Vz+9KCg-pX6tGVjX+MMtmaa9#~n;3GlQ(x)1$84%w-SiIIlwzKO=QT9p=z- zTalZYh=d{In0>(82?_BPDK&38h?D|}nZEQ+Vx|C->FV_E!o*Agj>E4;kAf=VSuEeu z#7v(fO&_P8+aBT>yi9<4-b4Dyz zv#8!QHB*K}ZRzS+M0&*sbg43LNL4T~OV z1szUga#S*0v7vS}$4!VR+Q1Ekd*4RmRCnvCt?%a(T!^V1T6MDKLhocjal+Z((Nyq5^%vWH)= zcX&Wvf)tdzoR@%z69Wr5#N|mI9Q{n9h{pDkQUtAws*#=HtqR7`y7f+H#K1c?} zu4r0T{~(@GT|PQs(oB{t0zol&3Rpq7el>H@VR)HKrXw=LPO8Q9jGGc!Ezu#|l$NHH zalK!V;uSuF7sk@e=7yAM`J)G!Hyvo+H+65n*wegkX{-j>re2-qMef(U3v}s%P3j3n z-m|0~bA_})&cklsOZWHiocd;ts2^E zCYAGIPoES9g58|J*ks%%S_R3<6!Lnu(Bg*?vLa(SC|)Xq`=Jsf%lz!1C`KwYTh0;V zc%uLOCVg{n?>G0}vM21l;KuuIeD94n z-uTk%|Ks`(T$isueC^M!{p7XowearW?EcK|co*LJ_|7lvJh7A6S=;`V?Qh*Cw)eK) zz4g=PUyz*{zijTf)KfBpYlf5ZAq*ZyGbht}F_FI)Z7)t^{3 zR$smHAppSVCw)}geQaZ6$NOB8Q7@iaFo)9n-?y^)$QxH)dsN+hY;$9GXJ>cqvbu}e z_*RYY{L822wbv_;9#u~2@2*~2f03H~tp3i69!ehxOTjM!xC1_Y#4oM8DEv;{*B+IZ zHhhsC`waMc_0gl!(uOaRO`p}j_7XXL#4QEC2;dI*^bxzX?xOHJbss%qmI7a-+dTum z_54TENAyzR_YoAIPPg*|Qmrk|V;#7&;#t-C1vPV*i;dfigsi`u>b{L!VtFE=%>U0Qch_!;o8UJ88CQk(&wPl~Tv zDtz$)cZ8>pzH({Z%S}ys8Sv<$vYr9oe&H;A1TPaFU9?qaz*9@>E($-Zn_OD=GDUy% zD6zEBiyC;==&k2N>7)2k;foJA10Gvi_cGmj^eDO%_@Zu|0pEV%G<^gu6~6d@GvJY> zbuZJcM~@Dd0$G$i@J3NeEnZOdi08U{oTvAyU?P3oz>s{asK{7&6Rk6yeC_`9M|9nI)^;Z;}3Z!Y8T%8Ur19N;4ajAaF8R_dHDdB9~a;J2MBcy zz>oU5x^uXX4Hfj^9qwXOY4<=xWUoWI1M{HZG_^=>bf9onH9f?* zJm4*aN>ZH#Uh5WY;r*G-4hNVa?si`aXEPvi=^~_xwrr7}8|#SWAy zemG8bh7%I_s?{oWLBfio31o^TO-@OKNYSa7-Vz^pJv>x9LZKL8oWwAmEM^LWyiz{o z=rZ1yRHcu~bOe$3s?arfrDkFWhoeazF!ra%IkVQTM&fqajVj4zv}{&KF|>Ek8tE$G zFR{&?rdtZ|c#xG*k+h0CkQNIMNsQyV9u1=-npO-2fz~A@tP^C|E z0GwZ2JwyAv}OMaXdtZM5$dm09h{p z%m;CH8QGn3Lh3Lx!%*Z7N;MDNQL?P%D0rMn=c=uOU6&4r)gfb6CBk(EgF{mn`mI>A zkk%tzOMqe^Tro0JqX#M2fd@(zYSvI+TicY^^wazQ7p)L0x7eHi=H}|&V>f>8#us0= zuKn7zhjs@$zp}Hx{pPLrZ^bv?y77^X?E3eweQ52gSKqqwVNhZ5^W;Oflebn@m6vlX zE4w?Zo12gA?1lnlBR5_%_YXbsv09#Gd3V+Lj{p1>pYgs!^-C`Ep~d5(C--kBJdCeA z!?+VF5eBd2|mg2sea@x#(CnJ6yv z|Nar*ARoCM_dv$ZKyC*~rG5(X{ML0j(rpjpOP@FYHhd#}_3fC4^mS)Qw}PZ%KSg@~ z`o0|RmWTAqCiCz21;86!z`Gd=kjnkDz`N-o{gR*ezF+(kzVU``LmuA4v;N%(k_!G5 z@7>6O(ei%X@DRSR>wSOm%Y5T}?|yc|Nc0a-{1X#^-;^`+mWSZ=?_1 zKJ<{DGg50oB2Q0|`ZZE(9=hkv$NqU=>l@=sZolT%%I+z~UC)pPNlgFZANIP=NII_x7SvV0l69cI>hvF&#p)G7D}O*F1DvAM?Jq%a>tX zJX{B+{?#7FbEbYb)BzE_nM{Ysum-R51qf2QDQYS*cp6odZY!+RWiy?ss#ur0i}A$N zF9kVYk2dFKbo2UUAQw;Z!J2un(2+Z#0;-VeY??}CMi$;EE4+Zpr4l-cL|ZeK)a_EF zg0uvQn$FHZdMOZ>GkMIHqczIw$hF`1jdB0>s}?$PJLIakXRnuBXoBxpx^UHJBY`rG;U-}?sn$n95pAkVFtTR|9yPeIoZ@i(~FZb}? zvjzpA(LKd`ZVg(B(_4ey{*iy>eSh?(Z=8?Zeq><=tOw!2ImLO;8nhItw+6lagTLo} z|K*2$BYo)h%RHp_tU&=dVNa2su0cyNdTY?z|MdCu@3;BJ_>$Y9#Wg4Z7w##<(=})* zL=W2^ey#WYf#3CwaR2sKc)fVezF!N$nQ2$7l&C?QReQQd_n|3-%Pz__VL2=y-EhK% zyH1U$6&APhOK)?$MdfKNP_T77jdMQS44SM^#{x9$Q z-MDXz`?vQOI&vq-b>m%(Cky3LklrHn_IG;wwzt3YGrmDSa{D0<>*V^CklB#ch#cYSnsv%yBMAc#p9Q8_n&% zKEoheu@Van@Mv9^tL%UzNw;8&wHzZvoMAhP#xrbIC##r4gyJro(p>(^r$N;#boMGrDdT5Nn-4P$1M2FFlH0?%(1Z3ta zYGk60(Q>_kQQ|0SX`%6`Xjc+6ARLUu&@4Aln3g%zijA-#>xeUqCK9xzmBxc+4BPKy z5f`nB*sN4ROS+ZGFD573=FTwmcq3nD?Y5lGwv$vJB&scyXA*(UD)ZY#Pk(PZ>u>`oMq?w6~Pmt^Y5;|6sY<65$W+yC$_b?v}fM-lbM!f|?O0`fb zlI*Z0>pVS#4|-{d;=`D!g4 z&q-4{Zj?o-9IN;DXNX&q>0+Lm40DXkDAV4;eAs=_SwE;;B;G|w8l+hyV9A^yL zMf+%4z;lq*>Z%R`<>7)WhDdrw(?}Fi%ytBqAp?!`cvn!G<&-07)9Qqsmho1aX*pAB zpQvlHONcXVP|w=EiBL5cFzmef3`3)nXqi?yi4H8NLUkhsNo0|7CR3h9>PcND253SO zbiEQ$LsKmgAC=vRmWd=1L%E5jB}b8&bdy6EvuB&5wxkOcCCioK`y-^98`x!m#isRQ zMP{)@rjqN&XBeve6yWE|b8Q08blQ`K&T^RoE)dBi($dKZT<^yU@dk<}xe!bAdyQ75 zh~-DcSSORyCR}2giw^}E^-M8{dMp&+Sv z*i4sX&PckE^rRav^|`Q`!ZOr8((75YQ!`jTV@>i?sW4m^i?!HY42B!Gi*j881ffk# zt{X&!07u7kQK2TPfn?K(Y=eq)Tf9MHp+Pd7hdWhWmnx-Uq6Is4oz5m|YAFYzrHfrN zJx-M=qFb4o)P8G{At#azcr4qcCZA(Ri)?EvpE<)YvnNBF%SIqHW(!HWU8oUKtweN~ z28^0RmzE0j03??#AwtM8xT2&WY|#RtbLv3WnqwqwGki|BJB1jq->ftlb;b#UruJ<_ zK#01PG4s4a;R-!*7W!fRkI!Kkknui|GYRXEYs-`^(xK?RHd84M=V-zyS$=11Xjk!RGgG@;=W6xgV~^vEm1n!V&tM; zUBvLFGYn3(BTDr_sW;O4_1t)53{05JcAfEPR*8v@(h~*|9-)hNBs8!KDK0#dqJ|bt zjuslkmCYDjqqAATW#N7y zOxmqhRA7Z z##EgpODRRxIt?{cEH`0=gJNtpTI4uz28GpnX^?6>fmesav8CjN5yg#~ywR2Sd!lP~ zTRo-QF&q}1IK}1y2K=lWM!Z&%NQn_@ovdzmV>MDOlci}q;-E-=kh6xP6d4=M;wBfO zxh|Ve&EjmaA?UeGmB7>TG)XW?PE)N)ZBo%q*tQZN*|fHAjweo~B~05cO>_uSl-vaf zs~qh85d&!%NIgNQ4Ykq z@~yHi5oJBqv7=_V+)Rx+#%N^dy@}N}y4kj#iwQogUP*pleY-iTEqJWI$KzB8*w%@jh7b(=v*CE4m!I{i@F zR0dH#7L`jWxoE3Hc~+)-?Wx9Q>>1sOgeUz$WK@J^?IgBODyUu+xNcDt`b3e`_(ksC zwVyk~0H`F{X}zDK28nDgs|s8TP<=8?wkFJ+R+vq+!%DXd39^iZxI))TXUIXJr_)GR zaJmRz&-B8zQnjdJF^kN11{GT9OtKSaU+x1UU$T=2Ov;sshGx3=typh8!%&a$;VM>E zD`*N1_lBI%$##aein>wNY}bkUAU}1B&B)N;LdB@upNi2@Dz6#hv>hYpb~{;UrY352 zRMv<2)*we{%rqAn_xGjBl8f_6#HuDzqAIRa8iSXDJdD`I8e#U6y`j_;aX7@9x;B84ZVuAi zx`~lhHebGwIb*T&0* zWD1{kvi0_$o61jANidu0sLKefly9D#&Ef)v?Xz9A8IDaTV^+wqSwQO2$m@8f zLg%X}*ifWth*V2xh2&L8R6Ah_XM0=NaD`)js-f2+Rv{>{q9 z`!-7J?^>tVeq*h$`fIB>Q2AN@JaLR|ddUMqsuovJBLU53xG2WVm`X_{LKDWLsU^C) zoJzHdis+^if`){kv|Us(?L@d#PE<%a&G($ltQ>8E{bnkf=voXoVCbY{R&xsYMe9R% zP*R7A#xmpSXk1yeFHan2PY?unYG_Qt(U=X`HX}y3!;f5IM0K3OFg0;o!lWrObq$F$ zB6WId34Rb{P7nlmYRGQI+;SK2!dYnEZqakH40OcNIPY6_kKvfh#%Ys&4e5%+`O^(P1dJT)Zsc;Gd!*)zPS zf-sI!LeEy!L_=}(p2Pr?+r|?tRYG{ZH1dPsbteb{JT){lQDPcXiYY^|##D_|9HTeE zE6E{G6rrq=O_*Joft`3P$N53<=m~-VPYn%iJO($b#o1susbx8mPV|ODRn<7!(ZUq3 z(<8Q?5_Q6*96tzNdx9WSEhPKa1Z={21)R#D_B4wWYjuRmMnp7c&BA>iF(tm2QnXM8 zAH$F~K^hach;`$+8k5r}fUtRl3={}5Fta|=WsT^nabfwb`V+@reTu;UQ-e55`9bhi zCkO(#U`hz(A_(2dmK@h-CCy2kya_^HTj-w|C0(jvBafb1O06IYsz{MtrtBW55krM;~ z9B6|0vG_r7c!B`OQZ=b8${azavc|AQ=v7D+lFTFwG8#2>!_9{a#s08E$FU&(DSi;V z<^(|iua{5&j}t!#!Y2p7K0y#5Q`6OELw@4;-~>Sc&y%aoM*PI_t493Fxv*0eap3KfxzC2n!G5QHhCelZ2+F5nqrw!IMe1Co8Fqme`ewEMz9T9AG3z(vaSagNO!w8qv; zEFU9??9ediyjZlHDa=z)^xz=<;BoABK4t@&d?BYr$Y`}*DKHoogQh)okVw?p!zM*T zO`0idb!V1NQn*>QXtziawrHYAy-Yw%YpUr`CYPvpkh+*q9UIOezJh(`O$Wg~fZ6sd zJk9f%Z39f#tJAzIX14ttX~zRH+ulEU&quaBv&^>XNFse{)VSNx<9xr@LGWH|B9;f` zC|j?`B{Wh)M;RgR$jx|WXk=sc%!5ab&+J?*mtqbhBk+Db9HIx4?5KD!YDY{Lbt#!W zoV1c(ZCpWA^o&vP5O>FN=jV?mHJsGrdQY+ZysPozu55?q&jc97h<}5 z`{5J*lsByg@eEzQW-hEV-bkM3Pl?WLGPpdJj7HxAX+OHA3J8%QFeIOsL8k$BM;8U1 z8p_-=^jQiXzeAORw8x6|P}j7(W5t7F+{deOk8*ko#r<)o54iHcDL8d$m}x{h7$=f( zOL4R9d}|hM7x-Z*!V^g?$qk@FH0v-$z2quWGxES;rt}ayFhE{ZX`G0$?I zn3fwf4RHOJ!L>uFq&LAEU_|IoBMj744`^dV@=_vFq{JpMw%J^=V)x;8k?mu>aWvyE z%yiy#pp4)4!rS}a%Jbc+)vMjZU!aQ@?fDY$v!QxH!nMJK3zFyI;yq}9*NBT%)e9tg zmNfO=J?NRH`pT~WMLg6`y9-TUsM7b)v|Qpe4K1=Wda@qW79?RP4ys?c3mqDOzGysi z3pLW3am5KXBLwd%E(?f&oG|GfKMyN~T2?0j%FV5*m%VI1Q)AfJ9{^Vw6ow@Z}x4wVn?>GMNR&VX$TkskW@(8@|<`3N*+?>Rg2R)2G{hpqkm-WTnB&yD}J^2;}V`o^1YkT+g@{m<8qum9}z zqpcsj&h0?k|Fr#^oBw_LUu}2S9$tPR0j(=*p;CsfDk+w)8g5h?MdIODOzRfdHYvv@ z9i`i~WPOri@}pstxc-iL4X!-UqXHAnPs5FrJMMD*c0$fgsBAM%pc0lE%kjM0Lh&)< z?EZ(78rk~5B{OBSfE3bDiR3a!%u*|(nmr=MiRsL-9Vx*c3WJ=w^Z&E=CQy!BRr+`> zuDz-|2?P=nveA&GYGzn1F3Nx023v9f?76`Ad7xvHP>x2i6TL1kB%iMrLCn{KUKGe-E5Ql5=BS!aR;8G3ys zn8?7Wv(k@uTmCE|1QPWm*C1>8KDltx2$86(I*}=h{J;RwzP(H%nM!hnvYMpJAPmUW zf%^TRSBdkH1hx9;F+vpy*~JTNEFy-8j0@=Ql=&j(kHv_*TQm~YcokeU@hVD@U-{Mu z!LqUu_Z9@0q(M*|h|2ZgqUlU|!#!{#p~vKtCR1pNAm%j5&aRCSbP{FJxS34{K;TNd z6Yqr;_#4R2fDqCf}OAe2nlDeaIPBeLTZd!cjd2+|4Z( z(Bwc1C5mzi6QjLoKY^=cYkD|F@JgdyCbF!n6^QoCP&iEaLTs~%h-If9Lz_+pcz{xk z&^opF^v#;bJv<#wMJX;yma2TZ+3V?2B2m;eG}B3y%5AAig?#~E%2+bTh(x*2&X*By zTWTev$ucLDvTRo84K7A-AYD?Us`#C#M}-^N)dOQhKU$6Z_(--cKy`&C>jclro?d_m zrlNyDysHHjUrOkwlT2{w!iBjh$h03^45~9`v)p5~R4@+n%a?<8dnB!!Xn5piFyX3;9@t6_AJFJd&v!F&Yq|XSu#6g3VIIQmObEyE7V)>=> zHxa1hP3BwKU<}dQ8V?VgF0Nj5=VEY^Zzm$f7T43bV6W5hFREiimUeMS+UtqbNTEd+ z%bZ#h1_DScUE(@Sv7X|Z132!g>XO^>y)h!w@_Rf9w$=jIiCjWM2~(ANQ^n#sZ`N_G zq-8y6XQl)*W&ia4F#=pP5nC}cSS4kpJ0+JW~4L;&GXEtYs zZcV3qB|lsZGrc_8AK-nt41%sTf^M;F$qjqeU^k_-34~wVv?`bgr`nxtlC7mZ?R?WU z;IydZZC0FgR|FnNYFu)_KuoF>Tzu!qD%PXdsGdRB8nOokFGnL480}TN?o21t^t%!+ zEfMWxe4d`G1Wmtt6QOiKntK*#7PLG?+N*Xm7~={!ODZ2kYDzrKg=o^Bh$dUDnQ`l~ zNF?kk;6a|PdIYgZSD8cv^2eH{mqMzkilo2^S`?y5y`as$dR&IHh8tm;rs_ts;v&?L znF@Ge*_(AXv_ZUBZ4+572#1ZFpPjyCgh&h$a->x)V_i?BjizO9n&8`+hNApYNFh}c5F<3>Iuv1N-Zh;>}RF?vl zUg;dJkeJ5Les+wQJkWUh=bH%04bHBUHBC+G7+35FWSEb2GF&eZs6ja$$cYS`6ch6? z{Mz-p{P!3^^>L*JIsIK6VnwGJBMMy0hx%wF1uLNd7D4nymTjW&}WDy&Npq`-Ew zj@vd7sU|`;*bLVqa4ux9gLDnU2xr2TCF)%wLJ9_gNUD?*NJnvuz-nEWM6-R|XOP`a zGL_<@h7bveb)x4c=tPsE2~CRP@t%A7Gh;-s33-anPCr&4JcuvM3^2ALhg>eU=Z-P; zOth`A2oIKL1#Ig1V}xH1VH5}i?tlzy2-Vu z8?IP14RY^cjbgLJrODvTAI6Bb)^35BfvMGGh5#u^L&a*j4no+~kRjkH0ph$qx|OSz zRbxK1iI8}?gabw8gul@-w4xeU;~l15c605tKg>#g-rF|)F*eYe+gvPx_myd})JkPw ztzt-Z;8r4nsS22^_zLVDedEwIe&(B{z_pQ08X zsvf)i-0AzuyJugu)8GI4x4*W)Ut8cW+XDK={JhLwzBaYDmqK=xGm$suKzWBic{Z*} z96$8`Auo@W4F@R41g%NEBESDvWS+cODSl~y*UHv8K~(Er5t94jEC zU-t}9lI1Zrg8L^f>7fEZ%60wCn%M~5J$~rUW=n6aS#PsuHbQhKF6p5)ldm&DdDdKD zBOrI;^8R=_)KaeNZnmI}u-l2t**X_o*8xRYbHScQI8Nji+k^c^bHR0O%O23gdm8t( zbuQR{U29b7)A#m>f1*lnKcXEUGAh?KH+y_f)3)sBGYwGEXf~K!?CbcUMw=x))MvnI z;lhbl4Dxk#V3_4xHlh(GHtbmEGB%sqMq-SK%h~2!HBgl0TsCq;O3ux;u6* z-LeNXaRD!uqUmO)C)a##7Rv(h#$Vom%EdiY9j*GRT1 zy}a);Q>#ac^drl-h7?~JDyXVq0k@@)aZ4y z2o8zq0C)j5`rZVcqNenueuSsT69rXS$!%!DhqIXD!CM}AxxPm z9_|(^b&b`G28~K}3gSn<&DFqf5l>l)2>o0N4Tg(~%=MyZQz<}+jErIgN(VCjsA{@` zRnC=<<@|MbEsq5iV|w|7Ychn|t0n`ID}34$l>|B}@^mUWz;UrM>dOAMCW{o>rAQ1! zKQ@JOyCcg{q}1r7I%dV~gQNE zXM$_10?h6-%H-xC9s?prRLkAO%QZJdDK(^w`3%lLv`DzBs~BO*5itckYI@%Z*kpS( z%5YBvWcG;Xs#t|0l0;n3_IX!uhb9Z6P{8GE0P(6ksWMxY9C%+(XH$@a`Kb3!o_N+xxwnp8b;36fe;&AmgD zg?-KnAL9dbmSY61+9}o0l1CxbejKi~3Y}utPXRs9CKYN{Ok;ZG#A`BY&zj5(6e8J< zNYwkuD(+!w)vy>*c4)GYHxtRnP*he2NS8O%zRtPhB9qH@T^==^=ta{wBI@2U?Gr(h zEu3&ohU{6BMKE`!-|{fkay4ENyvYjN;VQ+QnrtBH30EM?bY%>q`%NwAL`zhqm&y+O z3=9=Aw3*8FifB3!1Wk6*3D{(nJ(|lQs!MQqU(*Wh?6W9-mmXiz9MJZy#O z6|$LJ)rUZmKhi;1x7zQBVM>qZy++#;Ziga`pfP>&3D;!so~_G>sFvmUL6VO`DP2mE za9N;n|4vPoYAbyL0>0;J(#`^dbbaBN4~OFl(Dub#-d?_*r3*|_?4V%!pFaVcY_Fb} zIOP)vTu8aJihw9^O!s?J>Wknr!I=Y_h#_%t%@1h$NMPJv^_$Ia!Y~acrmQANBi7{tT;oFc<|ZUy4cf;##<8 z7Sx;<9&jX?^DsrXGYq?Opve}erlwtcvs?W?!(!e4KQeXG)cz0df5!eJ`~G|1`}aL# z-xVMe;D^_qy@syMt$uuUV>Pn6v~t_Z#tObNxBSuNXD_46b4wpz+E~Ju7C?@`jm4$K z$ii(4&s~TvtjvFM{`vFq`IWg(&OLW7I=ABZwBv;i+OZ!f2Hr56oPE^HmuFr+Bg{N@ z`s>qwH=Ub4W9oKL_->!W>xWm_Imh(U;^N}c^bF7%ZRn|rS`sUCtk+f=ybhF2H2CA7#4_c8>K}C;uRdsjxsP3Ml@FlC`(&dIACX;fg7tI90E0ip{D|}($jEfGhySKo_ zc(2*0H{x1h&?kF@QrC8eFP?t;;dR$h_-NRKN*GR}p?0~fnR264R-3!RSJ&M499~DZ zz(p}>vN?$kR@Htt8L3xVyTaG@AA0xUb$AOrmns8wJ+lzidzDH~H5lZbgdu5|is_zb8Jfm=U{m>RTi>uLQ8pmZW7!2~^N_e|~_=Ce{-0zoc zfyeYtk}DCo5|u!b;GjZo*FZl8KI(Vp7P!tdW1&P@ZmXqO1l6%BwY%}7QFrkcINOi& ziqdOxH9pPyO`3eytbpwR!`f->eTx)g~6&eb$?s+aEr>e%2Tcp4HZj zpG=fWB~4FZg|wnzm71|#dicTG&(A+Pb9nvC5gdTQ@Siz15mtCT2U7XyZM4`}C5ap_o8yIzC_u4Pa%q*ShKfM09&DxKhHzqOJY>nxd z!9F`^n7sBd&%5Pw-*9C8^v&|e&YPi2W^o`Vs{JgUj)%q0!@y@|7EZnH@cL<6%1^L4 ziW4|FoNZ7!Q;T#TX5L*q?YoE9AG-yv)wx6(%}Jp`Arp%mwcNwZd?WZ{w!nc3zn)_2 z6{;pCj0jmUcklc85&YCm__*)!V75^os1aGnb(An6s=F`K78XtwkE}m>1V1|SJ%co`aPI;R-foPvi_)T1l6`gW`$e@+%pQNx!l97Hms?$a%BCKEd-jG z>1hOuYoaXIKqPc@_n99IpOtg}?a2DcTL>_!jE56tQ?I3kTBV)I?Ec&v5xo7#`bk>| z2qM#uMB8l+-gJ0r*bh- zj_x{y78Xyd9A01E0&k@FR6L%ppuHlY7Wka>uv0f!vMp_aCv~%s?UE$hPgEn-aJ9Yr zlOIf80KT{dp6bR-EEvxkbW`TLjbL^672nwSg)Q(B6OGqvg|-r-dp!XKddj=kec{yp z;r00~@Z)CQd2rwW2nLG_71N*w!pi|B;BL? zM8Swh=yHSY?(TU24ve4O0;lnGjP2$$((KB`S`uTzyO$1(Ke9eEf*+lE$V8S3hsPQafRX^Y&^^F51f5ueQFB_G-dBzs}aTT$)7yxQu?f+wz{{N@u3WnS zWCZx&>I+sgs|P?{fPY^Zu5c^ofb0P8UVhGUV);yvBj6oN&s>Txodz-m+_HG>Vg%e8 z`0m1+7kUfm!bu=&z(3Eo=dYMw1-S!WH&>rKJhuok2;A(bI{Xd?cq{O#+48J=b_!$@ zc;$>V15N)P

    H0FsM z$TPbRneq?^^2~z9Jote;vu|{9<+=1go>|bC=iczj zgID)+II}%&LK8p8=gdO(NDgw+aAY5!bFYKm})X<+<=co>|bC=lla9Cv+d@9%$OECzzUc-hn)`ps{J^ z9LO{438p+}AILKc8uQ%aK%QAoFy%SxK%QC9nCHv`d1gJql;`dT^2~z9Ja;{iXVw!; zdG2-~&n#%nbC-kM$84jX(2xhYk6Fkb$w6~3nApdC-1(q)xfN*qE_XT*azamV#zECg z%)ZRDn(ufZ&n#$c+8qw$neEh5p4%VDGYcB?+-_^>9SiirhZc_9bmVzQ>?4sQYllC3 z_$7zk!w)>XbLdNlu0PaU{KevX7oU76ap(>^H}AZ5=PD2>ICJ@Xi&rcjUHI+7hj!SV zyKaAX`-bgDZ0EMm-ue;99C$5=0bB@T0Pop+(&pux_ulx;#*G_K-_SRp^*^uQwEq0{ z#(H#peeH8=FJ1H29<+97^~8dj07_9`3=qh3f?M`X#-rI@-BX}D@pB{6b0fI9 z5$puP!K&UNah>8gU$6Uq#;B9B)NHv#*d#QvTTd%ix71Cwg*!&(Mi6r&@Cl+LYNY^i z0vFE=%0#H2M=({ZC=7&lN>MBm784CQB#36wIOEfEBR(}Z;*$#tiPObUH#g$D6NFErd4j8_Mzy@2Xl1onJe7oNQ{a58L0-BO|+p!*4F1ntj&#Bogh4#7*$)MK_+yn8>vAiZ3H7rHnI%C#ga^3 zsThu+Dj-AD($nTfTsJr3sS||j4pYq*pSJlDtt-iam^DK6LMp;EqA9$ps$zp@&9>8} z(N$-Huo${uiQ+!b=crm%@Ct=+G1EdIvZjirT#t*qXk@+7(Dv7_oEve)+=$C3hsX#nz@$Xboq8PVL|-$ELx*1+nBjXpT=>-V(* zqHvstj&#I=n)RHBgohEHa&8xz8*%B}h@;SQb@9SZwHWPLxD*+ga5NPk#I&NL8mV?H zhsd$AGZ?1o;Y!p)ebl&cUcd$O0?wbjgl;o&Ie?l}ELSJFe76F(2;a(Q<$TO3hoT)N z&5PZ3y4>QIe>6AZhZBVA)Z&A<+A0E3R6-7PgH+5^M${8snSoVaqiqC^%aQ7kKjQ&& zBkn&p;(k*^6GcO&pG_5OlwX!$zm`*#K<;+a-H_O>uu%nMYc7%DLUH>eb0a=HH{wH6 z1SfP@jnOL-UGgGuK}0IH2JXj!i+1gZP;BBZ)Q#p#Yy-W+L+3_}=0*%B2*s(F(hweP zV{~5?nk;NKFnf?F+F{epQtevJ2r14mp+eC5^Zf?Zx6v4#PTtiZAquYxI6(C?M zl_eW(4=X+b8DfF6QxP0w8CX7z1zBi=eU;w=*dxNMpTr_0GKJ?gY#xfEig@=+Y3NMVqOhqw%sjwx!Q ziuM{yubLZi{oIIGP7tTAQM@_r=W`&Y{XAVG60smc=ix+H$!d*}-J)cspI5vlA4N0$ zU^Hy{y;d7X?{Lk;syPt%M3)#G$3Mhplp@CG@eUaBgHUS|u{;oiMIzeZ<-oy6RT z_}qxt1VMKUG>CEoF4u*l??j|xH|iXkPJHG-Oea2bAf^)^5&?%E z60tlg4ne{&AjA!!D(6%9a9E`41R8hTG8>Ozqn>}eC++S3GmGz;uK$-V{HqASp+jFj z^r}PsL&-yD?0jSAbvsw@q<8MN{r&AXZ$D}~zkTl3EnDy0dfb-0brHxD_`c1jY+ku} z>BjFiKD_bFjmieRv9NyA;(OMgw{EXT*4NfPyY`YbckO{|J4@2)msYP|?X4zO?*Q@! zymsZP6?WyW%imqTVfhiux#hE$ezf%VrE3>|{*Q&*sno!@t#dR#OhgOjAd@bE`+8Uj z)^fdSG};&iZgZIMEx%e9GAvbb$V1cw;Wtf+A2Ge|z;8K4t1fDENz>^xVaZskZ#TP& zXo3TiVp%zK?=gZ^lexg~OQBpekq{A5kdisSrNxsCGi2#xs>6k&MlqctGjiB(|)NF6RYWV%dEHC zNTsR8b)_39sZ25K)n&E^wPQZI^ScSc%OE|d%NF_epqLfCxNON{1I+hZo+OL1DpY%| zPB-LQ0kiPf3BsiYa$Phzj2W2?k(Nc#>zP(e?#ZK?pL1e%Io1vkG*UE{CJQ^RRV<+l z6zeqeoockBhT>MeTB9O7qbHF{DkxhO&5B!g%2hUBKjCP^T()m;9XZl&mJ2qSsF{&^ z4e|#ArYh(|uGzo_P>5<2m8}O(5h&I5LcI`=iYZx%HL7T;mFR|KtRU4%C(W9q({gjM zjEU~Nae}Z%kR^u|k82XCt|S%6HRY zT-NIu$sClsf>dfnI$h8L zUbL#=aMm`IYQYPmHncnmsBtEkZub2!GjIng*A`-VF{bG#TOH;Hda^t0b^VCjgo}-C zV{ziGxSFhCokS=GUJ)La^JKMMHl0*QN!qm~F&hi7#?7TMM&h)Rjb#AV+Ws#+`A%nz)T3U2&a|Aqrj4~{HqBJMYt(?Kfpv7F8M4}Tef8!kqUhu+F|*C#E-H$y zX1Ogl$nf#BmCY)d0Zmj9IIWo$+26t@h*FD#{Tx%upy@VfBa+$E2_^vxu7%lrUWi2N zoZC(HIw4{0*Aqlhl`S#H;+V!%xMOkx)#X!vv8TW-=s9R@*g*GQzDGDpFRlsuzg9))=j zc5gr7U>e1U_rE`+P1Ru#~Z)P7!g!u9Vw@2+=Q> z8L4NF^kUH{BpLAi8&J|z$z(HCD5z+E;ad|#DnUjWaH%AMBqT;mKc5mnIIAlMnDzQOVADo?KFkLAWKwC6Q)vCJQl^Rq=F7&79CdVE0Yxt zmteCrI9I8LDXiTn(Y?A2H@IXe~~xs5W*?FmOrXy$u`D$?VlK!!dA zlJeyB9@?$4CXisT^hmTa8j%V!u$8672_l*)=kR>4*r}s*Ny=H7SjX&VK#xA zPB|!q5-ALJ*3OQ4~iZZ0CJq!w#ZHe%%?nMwzZxa=^F4i~PQg|Q}^O=;F52A>nr_f|KFR^ft)X+{b?Q(&?{*Va=B*%QJ z#iz_xv=YjNVKm$;W0`0H%GToqWTX;qMHfl~p~o*xA~GCQ(E4@*E*D~CT@aLVC0x+T zD1qb~EosO&B*|pc*|0U>`s-JZIoJ^pO0m1N&gILT)2<9c92UlOg&}IimN4uK6{}<^ zF*Pw#mMc@lDW}(1IWk4il5a(@kV>@E)mW#?5%EeUsRPv|iYzF@YGA@>BHS@E&dLQ7 z#BiwQ@kkz4S=TM4(2>XyJX5GxDbiNqVw$!xWLq2sJ}zubHe}d-FR!3zJmfHrkxwy3 zDAgv~<+@{$RF!WI;g&6>$g*L>%<5An96_{K>T){X%R~$W&7&d7j&y~3$Dm_Kp!Vn> zW)l5KG1TvGy=a2)f$&GlIO?Q8bO`SZy>_C&@gDD0eG03T%z8K_!^djMz-(;Zaf0ZK z@}?ZljzV4~YY)=(NZMpbsmApfm%)3HYFf)ia9q?lXZ?n`5&t?txDuk%Djcvv#D`=3 zB34f421CIR)3PiLJE3~RZ-l&2-mb5dCJ3iXk|D*YAYH+#mn#sR8yZT5vrfRr(^xbrkZFVLVzI zkrm8N%R$Dcu@V?Na--J3SbydOQLR=>ENSE&gu!!mGl&n-LF4u^RsK`urnpQ9qi1{R;^ zh)UCQ8ws{fJAGNBhlMax4@P#Arp<7t?}&V4GUsC}0!W8uMEFXl9CkWXf^>TwIo*MA zVF;_SS`q5lPC~)UHnaJp$!nZ?qM((@xXEe>j3v556SarE0%jED0%erq#L!^^u8!6z zxSJFpL+(eMg>Ozcz!qw*II#2rm#ucIA(?7|M(pCrRFStvS!fWis7@+AGU$)a64yie{cIu+mGB9w$Iu6 z@zy)GcDG94%>OSo-@Ezb%_}yKP9p$SkR|XF`|bAs*FOK`G{Il)_Al+8%DVR-&-`82 zz_=kwN-gPjwH6oxQ(o3#dD#huwOU2zfu05waR>-5IjEHYiN!d&PW5Z?Hs8 z(Ys*hv8KAMpwd7$9yUglhPhTXP9!i3&c!M^*A%O=iu0{3tujhb&*LqOjK#Sih&NV( z38Fu07273(Vx0tCXThkCC)G$UsY!mRT@>7EJ)nmT3_%oT?Xwd^H=Ihvpe}7C3(X;$ z%!WJJq=R%iXxJ~-Ycj|-=9_G}l#Y;#(Fp>)IGyfrv04)^^_8JK* zs6=fz9qk&C)rq4nIbzB0C%xgw%=DU&X~Y#?8NfDclu5eM?8g}cZ#8jTZaCZ96OJ|= z!yLcwcu*AbMlu-D>W#S9$3`j>0s4h#9|)ff3Yf_!*Iqb5v_z#A&$wKt+0tuxYv|X? zy_NxH37MvBU@|ijvwDE_%Oz*!$5TXt1aFEbEGZW1kgbuE$u_##R1a-8E1i&vzzW}v z(_KjwwkP40QbRU-5vF5jB4*T4*)kDpNJS@XRfz^p zmy*jDO%Pg80SVv1ZB7v4m*RR$<}kTVhC##u?AS3%wU!s9wx5XA(Us%`p*lq}0~Pfc zmKSS@?3XFXsrj<#G+hYj_tYCf zE7uy;nGmIbH5eBF?ZBmHO*j-aUPO8xS*idHtqf1QQM4Myo0TYKq(w$3MTJHyixoX0 zvOHOKm1L%tV0kr3g$E-{$$>0<`Y6%`!a)>~ga<>VZgE8d^%2)uxM{*6C#qo*8)aIS zRNyk%A_%rLN0}theTv6*T*%SMaue~STFTk{6S+dXrI(0#@jWi=r zuA}rTMx|bhMnYjJ-XS1sXh=ohHc4T7638fJ2C?=))p@&+&hz*{@2j9`n3i2p`l%4* z!;Mf(#~Nf$aMmW9B~p!OXDk=vGB}wwVW4THv)TYu5|Bgd>SZYvR$2+QDGW(+8yn9n zOSIN4=e-`*jud*h)6W*DQL$1>T5wqkb+90sg_wb;#EapEKdCCJ+>)DNMO0185umi& zwM1SEW$Kj31-0CWhdL;pE@#3PzrH&`q>``^DPkEWpX>sEma=?0_#BqeMHy)x~Zc@6@7T;t5ScC8b2H z1e6A{y%aO5AZ4$e(xPP{CS#o68JRB7U($4wM~!Hwzd}xoiYMz?KF2z4J{h5F5iik@ zh5BP72(9Scr9AMDB`@KGYMa*b5;nW6_}=rY`DG`r<2U)5Bu z7!OBBVy2vOMJC9a$<;SZtRld)kj1iQs!}mKP<@m&z`A>wa3wMA1p{^zGCZcNWP5}D z(oZJ{ybvj{!xRGoYr4`J=$KIPBL$%4s>e$TMYN%Y+2M=%Y6)74Oc50A)(VVHWh*hs z<&ap}qcWM+$PW+YVwq_9gGi>}4+nf>d9orZK`N6WG%cQ?+VxB>H|W=ZIAA-K_EkPX zmMAF>cJ5tLjTV!eAD(c8XqnCr=?c;B?1hTNy1~oJFb^3HAE*V0Zm8A9DA8on$t7uw zkoutxBp1{Ss?k82m&5%u8}T%%k!i&%f>2c}PNant2tJiun1u7C4$#pn;Al5uloPBW zmeL|R^c3DNWU~zqGjgpm0+QZVlX+-mGTumD!8fyIH&D$%!wlR6BIklaOv4M|B-ZxI zg(eD8@HWFk7FujftO6>_3Be;xB3Fv3j^qiMb}7w9;kKt&z$S?f^?)c$Kbv#WjXO>d ztqj?zG~sNo({v~?R*|JD1@V~aWYPuM^KE@psM~!f-%YMef<#h_7Slny+l=tC->$%I zJ1pr9I~R|&h%7y%s~|CQP&dn1m|vPK|D|SpL`qRDCxR`etlw`UO-R5q!>F68D)E>aGI?WRvgspL z4Xczb_rzGQTJL1A9GY)sq#zCVyk0XYhrDJn#I_QmUg)n)!b*}6f=X16Y0_8AI{KYuANbOH%SNeb_)CDHJzQfAh?7HfnsPA zE6aj8EcRip@!-j8XtIoXrEH9n(gu)P)exde3Y})5@1?UMVxVXnNA#3gYNAW@1R)Dr zSx@Q1R64E=+qhTm)D0aUVqHz8+H%_+!CtK5my>mL8m*LM0py|XBpfG9ms@cVKh%VD ztIO~V*q|bYXpxi(1vIENTK@90ryOAt4izhcB!oFi3j;kvT}x~Eju7wok)T&jMQSBV z0PTWb`r!m2g~4WC6ZUdss}Xh@akrN5T2(&h*UfH0_a$|hYd546QCF7#X@V$vr4Cx@ zw^Wd_m?;ThehzzNHP#)_ux;k^X#xZ^^fc8Fpv6fzW{xzIE1MGzAh((9Lkto#gg6@5 z$$UDSB8wS6QyhRsSyU;L(L_9rl+fjA_b3!3tKkxeC6S`7mq)c&uW3fuXsnWA!KO$o z8t+m#l+;aU;~5hUp+11MLR~90f;#A@Zqt(xs@zF-Tr1k|p+`uGmZs_PBwS4;Xrc>QPa}HO^3tfu?~3?z--;Ok!P9HpIF;t{`_CK+fAMHNqq( zQT-|fv?)EgSqgb(T8eu{A>fx+CLEcdg0qxjwX@AGSF1qLgqf=i9S#WgjrgKSbwi^H zkciC`{nbxR5$T4}b{RBN2$wJcM*|GVe6~iFk)$TzY%xMrq+zS4<_%+cvhOOTmBc7h z?!~l*NwN93HDtS1uT>6GnA~i!DZSd)oJ8N{L;X#CqW|~ag`L{QEvwbV_x@e4D*s{J zt!v=1=k9LoX|guD1p!ow%hWSK+raICR1hjh~T`8h<+POQl?M+(~$X|iZd zOY^J#o-Jc3BdY>5@6JDYrS1VvF2pi>Rc8@F&c|C`hgZyf$0z46NnoBN4ZfQ?!8OXF|?-X_`!W65eV9| z;I@i{5fN-(-K0^oGAhFSQK8my3Mxp&!TWtE8YV5bOM1!PQLo@dH86|7RUlNfXQEYa zYeBaIs{X#`K6KW|y+2heFouPr_tnbT3hC^<37W8InX!hZlx$L(E4ZZ?<;SILEo;|P zB(1iioNzRu0EutcK3c=_=~&97b=PjUF1uO^~r7%{LQSaS%Zj4TmtW zD{Qgh%0N|0%x&(7lFjyV4txxxdT|pSCL-G=Jg~jCoP=q&+T)TSnSxw^$X3EF)(U7C z+zOJDiGdPH94$)tC>kF0$=Wf4uiAV8D^|Ph!Qd!Ko|0bD-m8O-_?TdKZLcFfzPmV2 zuJO~>5wYn1%8ock_Vg2VMBB02^Rl7z%3nHsGP zJdhs%#5_(>I1mS~zBd5gd3SX@07e%toTD+FzJ3?^U)k^GnMLnS;AZ5rjXhN%3ryq6 z;|b&5(D6So`c1e&$2--T>R(P&_jH~AhCLI*p?wtu;l_Z6Vs5%C7i(~h49mq%T#+r> zS1KTKt2>f)n+(xo0Bqh)p=f)-juR2-oe>xS#D7JK$)hm2Z+gTgz$D(SR%k z#{x>~aXlv*AGJi=5=U04fiU%4b7*7}K*zQN9Rr72NvB{%PjN)td-bXQ-=PI-VP$FQ z6N}F~{P9E2*{N@%TZy5TQ50p(_bDDUU`(U1fsZLe{0ZF;1r~lqFsg> z|J&E7=%|u#&}@S2$8ECDiy2p?q;7?3wzLvBk46Q}s^IkbM6$2I%?3kNnz{v!IVO@E zD08H=!1M7~*np*>y!ZbTP+g!>T^&XP-X;fG5O3lWY>gK|+NTlOPm4Uni>|N38B?ds zo`DE>Hc{{Ma$Cc4ss~(b5rMe@AyiT51Qhka~i6K)6DT1@`^S`?)8)i>e$D~-#2xE*C zu22OS>l?{vS80wN#_+E@W?Zo|_VE9*_Z{GlT-E)r+}F#}Y}|0;VK9sQW;7}zQ*242 z(Ws21kw(>kkY+|@G@~}^fQf05VoY(U20{%j4j2f{fdq^xNeH2YmY5nsGaW*Q{O?G! z*g>9dblf15z)Y_Le0q zOu>VSV6NPl`_UO!W$^yMA+<8Et(8LJeHMc!yQn?HS#yQ#zI_?duz2h#Z`hqqc+{UM8+s<%+F>A9m&&;+ln|v4l*uG^rR@4r(9OL0e53Qf2LJg zU=%&9zKjSq1UZR?s<~WA^tDrB9O2RZ`ZAB83>Xf+1WJ~=y(ZbWkx{xLvrdO%Zw9Tc zKGIA!qW)C9(6qw9{MDIPWs%V#wK76Vr|KzRG0Alb7?0&pIN7Nu_i1JB9%O=T4|}3D z)yv>$aL8J6DazjK5UpA{lBqu*wcAFv&naiAgpZgZDv*W-~e> zt;O1YRYnAREYqrE@NJkSXRXj_OB~V~(m}dyMcnQR-LP=pND`7Yb|P@Z{KXkqWrx-M zlW{bIG>H_N8zeJLHb;_a$eg)PD}(h>X#^6@Yzbbmv3rJ|VkUsL>+mjG-)_yKg)%<$ z3aMbsu%KqmJtg^%E%i!yV zG!gVOKGz`a3pzqIyk8LaZDsK0FC@GP$+fBnZW;I?f3eRg!iQWV%wKUdibdHM4w4aT zBAJ9$_EYuz|H8T3=Z;*v{hRI4)-Setn?K!bZ~Snh4s-d5Yu{Qct$uyAxbl^i$1mTp zoLl<*QVRS#h%bI(F|zQnh2Z>0=Sir15C3_}_@X62U{-gE#@VxXV(=!%u+Fzy&Kw#J z9-w9g2F2h9+eu)0&L(D`6b zj6_kXU-wY<++bkuH1l$@7;B}vv1}k!rU&`9C+09Z&i z^J1fbQgc=%Z(-o`)#}BTWa$l}K_NvLLU8^JK{Mt=m0-Ijh7^0F6Y<$;8S7;&(O%C* zm1`+T%L;G0rFpi>XLqc9S1ux9wg4o-9muFZoKT6QlHEZzlO5FjluN?w1Nae!NbQnb zHq;ZFQZ7CWq9(_#|VORW(Z7J-RZN* z92&_o^%Ts<#3%tN>uY*D+5C|3`x5YO<;Yioc}#*uS#TRc@W2@Y6IOScZd8UB9aF_2 z?u*0Mc&s$!9ZrdFRcunoUu-y0EK4`-)lSF|g0p7`V!civT?$8>3gslba+;6WoG!&~ zr?Px4muE3gvg)!imTDk_?1W1NCf`l4Za>XNOX*N9L}QkGm><;a9V%)kShy;0_U-Um z9(PF`a^6c#LJS8>VyKje1*#{t#It4yOjz9s^AaBmSO!S1%HsB}KjV`JonE>Uv^dEj zBx80!QoF(^DT;i+2!aQgN6@WFqqfaC@Wui%mgbbGgLT<@je;{2@_B*@_+p=Ri7}@g zJ{dBC;Qkr{9>+GZa9#LA_)hQ zG!h@k4xiULEFjHPuha}=8bw}dB*IDrH-cb$hQNf?9gH53jDxY+nSixX?e{CaK{aUa zabXOT+6+|{hz6BOz{!d|X#~O63;`Lo)kD58q(jVf>-F-$DblQu&GzVKiyhXjDcRR* zc!pF+iqJdW1RWI9_BPfFHJA!5hTZv`*mK0n=l~D)?apAmfhl~McKGQ_`bsau1~ZLr zs^-gaUQ!`W>X9~Q2uxVrNrs4Cks8Rh=qTZFWw9Qa<&;RQ;*W81c!bxA7Lpfxo>AZJ zHiBSdhQNf?okpnAa`;fZmCw3tNY2qAlbJ@j9m@sSft9ot8qOY0Tgyo)sBWf-#b zbNzy?)NCn8sFJJ1yDoc=&4xR+e4^AXRqR!NhJkrKj38K@AuwTe$3}%cnJg|4NTpc8 z?SY8DU~>wYHiMN0e5{<1Y7Qs)OYj0uxquTz)UY%U;VBC$}n*$)Pse2XkG zM%n?;>HzcmE20x-5ijl;d+{FL$H41tLk z42OGv^C{zn83Gg6kq`Ic+LZrq?o0DWl1DCBc-{7YZNF)|wT;g|aO;1!KD>1O)(u+|{qFzJ7kPT3@xV{AT5YD}S-Vtems_<@sMPziwGxwk^m@zgfB+=Kp&= zr~~`rZx?T0ynZo$VLDRi;D^9TnFo zVl6e{ssc}S3-xX!T@zCw9A3(_+8yivHdExn;XYxJnlKfKjjBf?`7|m7?0gxK!i{Xw zO^9Bh&o|_h+)8SSpVJiOLcQ*8R;e^KsCm7)!3agt34e`^1V(v;l97HB$@HiMUgu_t z8$Z>cqEWgfTRYyqk8d^m4n{6}%W1@44dQiM(djJ}+xc#@E96~SP4TOm$6|Zfs1)JW zVyDs=L_Gayr%NRAb-y=Q#^Gu==?(M~1j`N`eNFL8nnz!~&>o7mOag~b-=$I~QN+6# zrep&+CWc$0yO(PZM^U-LH)e|4FV{R)auK+{3t!mOLQQ+SmPyBHVq^(WFq1_Nw-=%| zKaobF{J@21ih0c=g(XRktho3XVU2a%kx+Bst=URtm=Xy-0IR1xjy{e#aYu^LpkAqY z^mTjnB0VCT1P%>1N^JsX$-HPA#Y${6-XB;SbTC(}g!2JS@p4VE(C|`ee0q zyfgGBSg%C;EZJ-=IFL(Z#N&;ni&70f_^&Yzk`e=&$HfmhVR+2%@NtAK3ar%i}na8U%hm0lHZ;Hh-9`I$kYN*Il^$`_L7c22b z7jf9jLORhZ$&heEtDO0_Yl^Wf$we9&FH8Z^i416Nmn_&*>&0z zE}vGbq^1~YI!ndDP)yomoFys5_?V-YB(T}?40O1@r? zBHfuf$`uq>a*$5TLz_>L6Ujt4+rnx!XF6lG1hlp~_bg4(lBcs3I%O}Xiz&uSIQf7- zD>-v8<&ffo`377p>Be33P(jNxs9Sf=6uH6B;mh-uni8*DTtdFyMoMC<*T@u`a8Dx~ zf?3>zv_otYW$kNheM?iM>ljM~n^HJLXO*sq)@lScf@doJQI{*ARt5%598J{V6%fti zE!t-cxG*g3u9ERM=4tw}Y?h!)eNQzi`<-;ynJ7e>j(jD@x&^K4@M?NgArdRPvv5LZ zcgQh3X7zg`ExH*IFjr&H#W4YHf%(Gi0ZiO9?H#Yxs>UB39zuWP`z4 zj&Ufhkw0foQBJEv&$Z zLftjAxAXMO;p)pYMbgeToViFvhFg?k7$dy!VRlotq=>2=R!gXA)Fct7K>!>mWaG8QcOy>T*zfAA?7&Rc_ z1v`dFAk}g!nu1AI1b^Nl2=P7#pQls#6hZTF@KCf~b?ytAM^vJ$R+|sfAvBo?d}2As z=g7>k$9c$TGnm6_)?$ltG{b^d`;H4&98-+JH^NEQ;&cf-OyQ>_8WzGnOqb$;l-(=G zLvFEDuXUK3O@q2z(G)GMfXm4@Wi(E~oG9%Q!pJZ+ALX|QaOb|;^`+{*0GjsOXbtMh zcaAAmW7a02#674F_8Y}wDUHyitOSyLyVnc$<%~cLq`ni*5@z3V=}|L9ZXj5~P9g6J zcU?$dCK|a~UlH4(HbP_~jX{)WP$%k3HiH?h0j#(*#eSa57QNL{3~hMoRH%@!cT#MI zqKfWJ(eEp!q;f0B#6#jxE92@1G(`nR>H(*t2}*V(=zptjLLhb`c)Ja1kb5;e}kR zk_nYdSl3!0#r(1Hc%i8&ih&-_7OPR0q>PF#5>K~Bj%tN1gsT;f?F)lgd%%+{%w(+1 za#qgM6e~>MAqHWt13p~H+7d)w;BYY>;u^VxOe58f4-xZ1hh@6j$g%nZO_6t&dot

    eve`)ykOxN+U{y| z?h%>``~3({SuN3AE`anYn0R5@bu52OQ_M%0t|<3X78?FVz&Mz`El{pTGHE&8mE5Hv z#lpD@m&zBkwz{H?$6PKF3XxG87VekmA-pi_hVh9tSzp587+^j4^w!5F zY?Y*&7c;&vR}914O4egT0IMk`?Li+Zc~S+>5T;iTMJqYSz~*wNQ;8&G(BWyUkNS#u zDls_rIm()1A{g?KwP=s-blflm<4$^n4zh?{5JTNuhmQ2DO=}Vj75rMiIv>y!~+cvwunX!ho;c@<0JjRpZfW1pPhowv{L6+l4 zH%k}MRJ4Wq>M?|LXJIB5vf>Ki#}*G~X&#wQrsS`rn3kM?&1VEVqlbRw-n4F`KB1{qx5u*i)jC=S&G91EDQbcUgh!PxTyL>cT6}1W3 zQs*1^QJ{-NggPTuS{8g{FEUN5rK^3%d0bNrdYyxj3~6cl z_IOF=dPPg#r#Nwj&m=_|KA8-m&_ogDwL8{UU#TflX>Z({kn#%b_u zT5W499Zr%ZB!_y3xv16vR+f$_y4x;CEawljSy!i!qr9V$J?ggD^Bta4gkdcZahFpO zCX8s6GhfgY{q5es;f_Wq!EHlIF>BT0=P8+{BFTie&s8cOt5Y00=+Lp2v-~1W(I;lx zK{<*AN8XGKa~TEmwOGnZAXK+c51MX2VX;V!MvO0L4PfrJV~Tm)-H>Vp%*|FqAy<~9 z$aFI*RbrNGtZyGsLJPIzGLB|ME92t(G)0)S0k2tboT_CBhdmLl;rVPNA~Eq%v{I~A zMk6sujL5#zt$oMsJ7$WI{-vL!Y<@?YaK|DjO%?N;R3LrXUa?n%S&Q5ZUUJL!UYFK9 z&i}c({{PhC+vbk^=*ZWOeEP`kN8WMdbw^%wWPD@@*#NIO5_J_9Lx&8X>7jI9tN87dS$8OV*DR6iD;_drxFKqo}>+4&e+4}I-yC84iOSb-E z>xo;^*5kG!TUTu%TbFFzZ)*|WE%@)v&u)HX^WB?o+`MV?`pqY;|IPZ#)^Aw97VaQC zVVzm`tYhmt>u0Smul*d}J@~@fKd=4$+RbY(hfIP`S!=Ep;O>HV&9?TCwX@e&Ag|!J zSN~)6Usm6{`nRipwfdaZKU-}8Cc1g)=KVLp#{WR} z#LsPfbmKi6Z`ye2#@AiTGmC-6D;JM0Ua+{e@VkW{EPQ3*lMA;kynW%d3olrB=0bm=vXET}EEwBZDNf*C%8~7slqAq^n>EI4YyIu-D2R^5ZZ}EZ8g3s#Wo9+ak0iV&u*N)&_ zqkq@Mm%k5u8hlz8Uyuc#0-w^w=K}Ca@JU^qEPzjdPw3*)z6}HP-*oYbUj-irAJ@hD zJHfw#f7M0d1K?xeW4ic+FMxjm|DubzkAQy$|E!A%Klmr`Pr4XUz(>JHb&>FckARQp z;$vP6J`6sri;ic24}lNqB60<|9o(*q4|^2&Ao!pzp8pka8@NpuAMjT20q_A`JaRqk zf8MW)t6u{D2>wwQ7ajxt0sO-;@q@p)5WEk(PZ#feCwMP-uP%P}zrf#vzt_d5Js8{y zZq>ynjlg@rdvsC$DR?({w=R-D2JZsz(na@`;GN)|y6CzCyaT*L7m=gj@4(;b;>AAj zcJOvxJof_dHt;rGyzfo0dwr`eZodWG0&da8jn6>R*th87(jAaj=FPge@J#Tx;BSwK zxBZp^H-npX@z)Ia8}K)}_{(#^o4}iN@n@HSH-b0n;*akS-T>a9i$BQ24eZzJ;&MZn`0clYzXpG;i{E$ycrAFXE`H^F@EY(MUA*H`IC{KV7e9X|cole+E`DYQyb`=p z7eDn)@Cxt>UHpU{{1x~sUHs6`!OOwRb@79r1}_6I)5Z7R2wnC3)b@6R?fER%m>EbPK1TO?H)Ww^F;053Xy7;=^f#-wg>*8x30td&x z)Wui69oz_R)Ww&-6+90-PZwX(2hRo1)x{TG1)c+*ql?dfA9yx+wl3Zn1vh{jbn&?Y zcouk;F5d7Ga6PzQ7oYJe@E71Obn&SLFaZ-?y!J*g24h`((ly}E!Jq5m@WtSn;F-GE zyAeDCJVO^d4}oL<({-`=zu;-$X}VbbFnB6>sxI<(g6qI_x>#(3KLdZJi}_E2r+}yE z;$yD@*Me(xF`ENV22a+-*jFKk(vx&C^iSZ4;EB2z5WxtHbdmTH7=ocLdOivUV4#av z`#~S{brF9e=z*Rt+E?M!rmKtSHJ}4Jx_HG`fC3ah^G`}>ji#I-hX>Msw7jJkk z_+Rk9x_JFH;CJA6x;Xwc_$~OYE)!z_zC!lF0vm5KL$V6 z#qbvR5%`fVQWt?Af**9GY0N(=N(#5lW0KN&nsf+jd4fqE5hAwV#;J?9t>*DeY z!PmjpkBJ}n(Pu#o)O7KC3=n~+i{E}WsDi34e*I;j0xG)r={JKiDC^?KJ#g+_(#3z~ zfdB+u{P0D92fQwR@E3psoGyOgQcwg%UA%Px6hJ{2-*pvu0(gQhzU@ws2YFq*0;&Oknt_3i~Ng000eaL2~USB zCrTF||7hR`eqFrgy?_LyE@qzue88uR$-f2!Aas#_1MmW`E{1*rJiw!ifu{pEaOmu=Na22>p7u{j-81NWfd{hxU8a!GTT|w|D@F-oh=fESuBX!aG58z60r7rG1 z2QCa-)rVJhilJTzTf<7{`A&E|K|~pYm-eidCGIR`NO@1v?qI* znh8Vcv>`=WzR~7|cC}uH`EpRyVc#!#iix{GYT^+yx0m+M*k|IR(tb&ZnvK5D&Pzg1 zkWNb5ci`PcwFC#J?Q`zA@u+GYpSG{72<=(kx~tX9EGH%HvtlP%fc1|cYC@E$Zk`uP zg|5_rp9O*c50<6gnHiL?P&2z+Q7_19Hb1-iuiF1Qq1;n*cm3PGiOHbDyMAMD3Yima z7kY(Gp*?k|Vw~aG19FyB@Gjs}Y{f$T7|dFgs~!70%!gLs)Y46Vsao$uz9 z69ygq!7{?wU>Zg2l)0LAZRJ>;O*j(ih$HFt^ioLeXni+0l<}ku87bDIyD6AFu+j|j z^l4{=A^M81<$~l2L8`A5e2^3h?w^Z1RjI>c=7Fu?L2aqt0ExSkMNsZ3l}fb8@PLRi++RUcr#fYfZ9cH}A6xRO?yVSg*)D!Y_ERkyKqc*%ay zYwgCmtTi3=ie)YmhWDCRX7 zd9dg(@}5zhtgDeX$b zLmgEvwW&Bb6;VSSn=Uonc#jVX6xui-C{#D6+t?AO)dkh}O$kUhcjZbCHo0idlkNuO z_^>3TQ?(T0amIF=BlpoVIr8rA=C~1a+KruYd6iXvt{auFx&c)5BM%M|NLDvJcH zwK|1&TWO_K+@*8yiblaMxcpdQh*=@yLxg6I4jlosD$`}zPkP;zurCX9ab*qV#I+%6 z(bbfIi<7lgfw^w+`UafV8ovU5S~&qbgF0ksRvQDH&Z^&S-+8X}M(D2S5 zJ(IPG%j^3OY>HASpA@@9qnh)z-TbiWNz%DcK?v?MxjlGFd{8FqVuPP%n{KwNa4tDT zAuHv(dY6tc=u8{=vFV?>NI5w!PcIBH7w*7M8HSMMJk0sxd=TTTjSiCF5?r?L4J%$E z<8k<<^3iHtV7Tt^XdQO)YLhyR)YNt_S_~Ymwo+MFE|?HKodBeSEP4v|ZZ_r{ut=!y z=19q#vC>S*UdTG*X`Dvtc3&-P&G@a+QO1#rVAVpplOh{oPYbdpTaOyd%&8U~hM~It zzjSUicjTrc8{4ngM&Ri^yZL2!O1-i%*w|cu^*RD?^~d2|erDyimB+%9^1{;3m!7e7 zEdy1&$LGWrtyU{-%qyr|e;8F>fK- z9HTV5X}`bjDdh7Fen9oyj#knN`K24utH+pOPseWsn8`i?8*KtQQzJEM*xB7js&#LpHIX!e8^={Nu#zK&(P)usBHcuF z%xD!%ADnSO3)B-Ds)-~Njd2Y6Qo}H8*w;P{)kM;X@;C}Tstq+{*yC{x)kJcM>Nuj+ zW6-dt6B??CG!n3(($JaOPy>dY-F-tv_BK=#IVHGpSVaRXnKFz}#LWnaD;_9{Zq2wX5 zgs#d&)(RLVUg%8is;)Nd>~6cNlPG`8q_E(|9u*C&)zIVRmkZ$0hCOQRN+%K^naKmu7+<9o^HGMK?o&(?85+vt$3Ty2 zj6Bk?$KzteL?#Al{AlRO1wCD9*wdavBs0kzs^dpNcWT$>GVJbzkTj8C0fyuwp))lk zal_8;J|y>^=b1TB;l@|0XkgKuhS6vtY2s}5Uj9E*n-*u@FGK#n`%I}-Apf6&H~20n=W9yswh=KXH?|MpJd?=k*A z8)Cz4r`%+x$sk%qUw!gPEGQp32EpAy=q$V@JDHNetThrC_Lmoc1Pb0dmE< z+x;v-L~Ol;BgwTJY`N3wL;|SORkCL?xlxMhlKxRIT(S?{WS6C2#y=!Pw0#3KQe>RD zVVhP)G!Z?zJCZZ_E_PHlw8=Uvx_Z{JG&wSjQON&iqTwB0jP6T+bjHW1iD}7xF?#Zp zO6@xCfpFai2t0{`uYUii{C_qCb2?5*#2M)j-XV(F>>dnqs|d`|BLO&S}!yK;(z7_9P89N#pW(uLSf`y_!Ch72Z{bb9A63KG8 z=We-&j?5r55O*=Q?+EvTA$J*5Vh$RsHF}sWm}ZU^V0v}~{C~5e!$5@mf7{c$Es+1u zMC&@dKy>bR#CXOBqKQ0d`vu}%D~)?70CfQB3Gg4A{|`1k)OE@MFo{+j?5=}Mrj9al znvpvJ(H{*m!C*PoOz-+@eJkr>VV=cy>GV$-bKR(Ox6C+O0j1y~>>`rgRir@&J~5Xg z!CZG_izh~2G3SDrphNwUCxwkF=@9Ak)}wZq_{r-kQA4ht7I`~P6%d#@DHvjTgNOZU z(bWO)?8(-2095(^4l|XX(MhV~PcrVzbPW65%DH>~zdzFToP+rPPMWD6e+}mk%Kv9W zAXW6KN5m8pZ&S9VUF}@0-8y}0>Woz=yRwnC z>=ToOwKVd>Bq4mg9X?vy?bYIlyO?XBawS!Z_jav#B;*a*a6!sOs7@%%;~6qkbte!! zV+Nuv=GeBtV8?l@%M_@W@ZE3Zr&N4{k$eMc2;%FPbaN9eK+3 z@3x=3^_#6HZvJX>xbcgPK74oHUHj>pvijrI*2)i88q42bmf>6Y8hjH}UHsN!dEpxi z!u;3gxw)@F^;7@3_C^PQ$@$G>w?5p=x7XfiHx0nVM()AerPtnQn*lJ9h1x`(U?Tu9 z(*R8D>KXxHH4Wg9KLI)gVB(;|%TYZ|~Ie}XHf08E_x9QG%$m0hm~d7y)qU6o85Iio^Z{mzV}{ z$e-Y1(*O?n6I?U}U}8J^us^|trU4xCCwP!)0EheuE|>x^u@QOLpWuAc01o*RoM#%q zA%B8%rvOasNgehlIL9=AL;VEm`Tv9GVsl6S^2pKczu%^|KEKu3{O0D9H_zJm>y1aQ z|I7Mg*50~CufBgZwetOyYgZnye9Q8Km)^6)z~o|mu(CK>JbPiXaMAp`=KXVDgl6Wh zTO02#3-A)f+)gdjZWCSy>a@A+@Y#}rmr&N<&qRm@S;$!$ep@mnB??}#%h85!+~4hW z3!8nUn%jX14`D9S>`-tHd$u6MrJMnKaMi4J{ikU-1UTnxhv zV>$i`ojkTcPgg-td^uyaH?mPWC)Eg_SZXTO3~whII2A~TDv7)c$Gn+lG@a=fFZIa> zNW%CL(BCff$D|_>e_|MqTZfrM7p45xWEp0CR}u_UY+Bm9Z1s9&JEWCHYlcgv@0Tm( z@lmY?Lqr2^9r2YGK{bMvy2K;~Q72!iSpB5n@VFdRU)DD8R4FuDHtgs4n>w)JJRG`G z8;*%r8-`uoeZ$%NaHKH4T*U&b*TmZn!&qjwbYOuWrWM%4iw(m!-tWG^dmoL+QnoMy84P9ESa!7?}^zN@n66hha}AmrNM%K!0jv#txHVRJ#p% zd}KaYt3eZtpN7k|Co(UEuBKi3VG@QKxr%Pv`%t7XzC^_WtJlO^4#UNsMP_+?u~xMv z-f$Sc;eTS|ya;+y!_LIp4a1)HYn&HCe`@12@q)v!zvCO{gR~kn@p{9spFNH90_aL@ zoF-mo771eW<};VxRNiojc9P$xWs?z7OwmdkI z_N%v>Ft`8a)_=fk{=F??>wcSG*?jYgWAjOy!Oe4E*80EOc-u;- zweq|5owXmYy?_2EYtLT!*xF;)9u9N#e|YuztHl+t{Hf(vE<9wpzWgYdQ~&czuY+0i zJxlk2S;ub%qxlcOjE`q8etq$6i`Om27B5`*?!v7LA zG>ntS%{^;YaN|^L-kE}N4z_D&n2d8MZd^IjFizLD@FPvbFi+d+%VrwJ3EEcgKhrSI z%(is%Ov5-O+xks24dYyFi<%GPBy7t+ocS=$zBZ?!GETcT_l21c<9uriS7;iBiPn}Y znucNi(T%UpG>mhyt$at*Figg_^e9ckFoEm(zfCnv&@XO%U(+zm^tR%fX&7f}+?w@P zoTuLya5>UZjklY9E@OB4Tqr46P*;iR2}w6n@Ci8MP53e2R&VA5FAX7!XB_vJ`$Z{Z z>oYBSNEbYK(&h2-)rK!`6Ig$-?XV)7cg{47Q_pVvbf#gPI(FkrGYxMXiA%Qjh#s}< z7!imeO^RuG`EtFJk%XM1D0-7QiXf$8c0(I(jML$*eqiRqI1k?1uVxy?3Gh~5J<~AG zd^dO9Ov5g2NjHG@(?o8n8&z)&_;QkMlW?IfvVc?Fn`FcF6 zRK(nf&13^URvbkw_F5|>xf=Z?tuOJ2xwJo!c6tICf84^x!pVRqK8Uz;!H^$j#g0)` zqSu!@E(^CX(R@TGVpNVGDMy1u#XvSiQgoZFjH=O*2hxm7WDyMtv0@$LR*N$Y+*Nl` zl-H5$)$(pm_x~+(@0dIC#v}3VUvIy5o8J1_)+@HC%^z*vwCUaW?#2r? z9s~L3pS$i_``X&|Yqr%du0C^hcjfad*R4E!`P0i!T7JmV$Cvs`7ef;4HaLIr_C*=K z2>3_HKY!o(Tj#m?O=$Y?|FGjKSYQNDy`IPO?m}z2~mY4oc))=|(g{ zIVGf(Qd$WgTNhE9#$3)(F~$)7P|@YjJEKFVj~!R0W-A8FmV)iH=<3zT4(lI?Ew@lC zT6{UkQV~Uxcr#daG-z+26p;P#EITew&6W+AbytQ|3{SXIl?IV+rd$17w}aKwXrmcP zl^tk}6g#n|7q=1V8apmc&6W(9wI+ied%cjQ3%!!Zk@6E#)9Lhj3+_e?8-+rh0g-SL ziKbs@yV%@#oDpjR zEqh!(%8IjAG*~R43OnYfW_bf<#Xu_{6kPQTpAfouToml>M!Y0&b)VngcZhtqWfjPD zup00&?3kOHTnq|uq>8U+7du2+aHRdoPNvl_Cq=T04P$bc zj0?R{FE@bU@VKej#~u6(!_3PhcKq0>*~c0%8}*_MGH2^{6B(tVWa3z*o2`aCS*5_b z(Q+7xIe0G6s=G^l7-iQ?&0b@`tckZv>^L_yn=@e6#6u-^oSmA@8Zc|(JrX<4OwDEt zm^JbIh#jY=W(}~)n0ac%j#E>!2IyhTyc%N1$*EZbbTB(6-U+c|c52oDuZx){KkPU$ zHEV#p#mvhdb{wCYHNexdW8xVPJ7%V44Un;zd9A~aV^gyRm{-iutgz$g)T{xHl^qjw zDeO2hHEVz@#mt);c1%yr8em7+G4YIs9fzl84G^K2c{9U~LsPQ`*iOtmjbX>ZsaXTm zCOalx!m#7O)T{v#6Em+~*fBLVYk-Z!%mWs7?4O!7z&^5L;>ij-CZ}c%@OGFYL1>;#LD@&8+$+v#5BF0kdXSeUn*K zyxV|TGpoMIEGph*U7O0$ zAZ}|_B2>Fuqlz|m^2n*#cNj1msK;Ck+fK_&hUnM0e7l&SgSk*Eno;@|I%JWmzCw=` zv(`AnPOhAq{W}9@%`7G-uBq9#8!&5TF*(7fX5VJOteM5+#5pzlRs&|uEG8$8so7f$ zm^G2Nf}Pl>X5V7Ktces9?8G)T`{sjRyzd|+kktWI)u z+7tC8J;`#an&H?9HZ}XV2F#jSqfV?-vo{+s8yrz7rHb1vIcGgAc-q~N)5}MEMX%NB zcPC(CmrliMBhYpw(qJd()a>6FF>9Q;l${_`vu`qBR;;DFy&5KXb0x{&adiiwl)zAN zs-|#6#?|noIN2p|;q)L}V<)>)vu`wD)>CW2HM63gSf*xQZ@{dH{1@!x z5mU3TGho(4x(jx4bZYjm4VX2vqMkf_YWB4T%$mqj!A>rpntjc|839bKs3#Abntina zvnH}ou#<;Q&A!TjSrf@7*vVy6v#&H@*361}@{p<7R~RsBB98<+*_oRCD+6Xtq=aB6 z51yKR`N41MO|7UWmrl*T%z#-F=^WU}B~!C6HDK06rUZ6!v0<|&5798I&8ET zPR+i=fLSvO^~r;#W?yW;tchF=?Bs%}*%uiwYi6N7Ie%*Qg$B%;$p6Yt&YPNjfdR87 zGAgi>bEjsXfAHJ+9WrdIhkW57<;!&I_42?e(yWio_ULAd9oDTW+1F}#hEzz3(Cp-# zsoB3YVAe!#26pnmso5J1m`#R=UXdEew&*C~ab>X{ndOv7tm2Pxa(IN-iWZU=d!A9> z?Vj@gEm-F^hpVq!x(NQ|-aq%&z}?osGpdsZs9Y|S{JfcMv4dlG50{HdnWJ>tLP5?q z+Pu)N*2|E}0!5wJ$y_cf`N!cg_2x@okl!#WWe^^}uXc6XQU+Cyb&lyjrCL7A&rb7WPhxD?5TF)=9>9NIE6o2^;NL?#f5 zx}!pj6H!McTNa}30oF|ShNKV?eGLrt8Y2u-i>?wEEl%#gDlpeAUf-BEGYA~qN$+Xx zIIQQ+pq{U)d@YxvD6W2+eVf1aL?Za)e!Sx^+nVWVQd2MGLR zrQ8`d)%dzpzM>-xIx3E0gP#W2K73**0{?y5ADs}FryGV&E2bLUHw=}8w-2wB#I4<8 z(H8Hvnz2#a(~m{DPAZETAYK>C*0}mRt`~3Y5?=r|A zrWRcd!|3FG>X>uG(RnjxCZ`pJ`vx8QCmD1m=7IYK-N{+L?xFbG2M7$sexoh%c{qad zg`y-(TQ|Ia*{Q%VRp$;T$KRoiiw>*JeyV^>#6*wKuD4L6mmY>Qm2%%(bYX{XM{v%G3%ixwm?C;RT(5%q zPCt-iY|_6ctB??vA`vkrQMLsiLXEsM@8ZD4gxIso-wdflE-#K&m+>ys`-@1LDt;*)RH@7z)5A*q* zz1CU%#Oh@$*Dim2d1vXVFjJpx@mULZELi7nfSLaB>n@o*;>g^*a5*zKx3)UJwDh>u zHF&4P@F9(!ICDC$J9r~~ZC<|Nw%=0f&)+@n6l<`Pl0{$lh{;hE}TfcU9`LnY*4E z$m8?d9$cNvD#o8(GX42f!$==7dDx!Mzhvh6=m2L+`551v7VNHKemF;0I$|P|EsR9d`Jasz{IP@R!V76V{NPyxutYd~X`@cU-FC{o-d0sh$^;E*rz|A zYZ&3hlLzgo#Z@yW%_r94fjjKe$O28r~CndV@%xry_mdV+>Uo9R>z?~L2I-&UlF zn>gworF=Akk~7^&+;if7shOuj`)x&@Jk0GqZ#XanrhB`;_gVcuK;U@;Jo|17 z{0Zj_>M6j$F?}2+c5bow$Jt|Wh$x(0Ag^$__ z&y|OG(w*!jnyjbK?25ZiyjVH?xbvj2Ov%%4@Dx+S9CQn}*zS@1m+NnLocYj$0fJAu6FG2a7Zt3YU!r`Qmyx@(~ERd$1qf{*1CTp?qE*}s*qY}gRqh!F36;X`#C6!2{8XU$^w81s1v1YT^@pgH8 z)JJ5UyQOm5MHsqfhpI(ahxy**tm!a6oHz4=`|!FO+wa@msH5mf^IT0W?{XriyvU=_(P9hc=~ucfK3WrFk&ZuAXdNAS6-~kIRf=Y=VubMTe26=Ko(`See_tY0I|p@%75u(#k8A zUEtG;GW?&fIwLj%=1XS;jQ`Uhp2{yj?&;h`3#SM=hi%WDc$#nzverQ_YbB9F+SLvC zr+-%pe%FX68n!6m?t4XdpJpS?oIlm&{gPZrr-lK4M-rGupC)|$tW~N7{Jd1Gx?KUn z7lJlNSsDJm*MoA_)G(v|JAsrLKiMr>Z5>*452CCO>IO?RTOLG4?tYjJSR+JJDOw|y zqQBDPFvbCwn0?wk^QSSE;JjG{|gKo7P?35T= zGg>E#iLjUQz&GJm=M@P251n5x8WOc=g$N;`U`598o}l!PtL2r0YI!fE{F&62 zY4NaX8I^Ldo&{gZ$50#=uA$*Xrm;^g6Yf;0WoN|bpqS0%B&QYOajI)&Y}H}6;HBko z-qGmTy&VtLavxXA%LmmmdM3AJ_K<3s^4T&&#U7O^Qdmqm;v&;!MQ)#3_KbKrhPkrQ zp5XIEhV7c$LX>E6kjRekD2fy_k$xgK;D~h0eSBM9I;fWSUVu2G+VWxb%S5FG`{W>> ziUbSENFqKW$$~f?FNP=f7z9=9l&H|4j5mR==MdM6XC^3%|GlWEm4XU z*z$3089cSHe9hAf7tNny^t1o5n(}NwGBfz0QTbE@Z%!zyS@}?)qcZvH1xG8a=G; z!cP*JTCKxj@gT=amKfWqrch>|?t&27qi~4IcxlB3`NZlrgf7>u6`!-gHmo(uE6G*( zj7Fde2{@V3D|vqLpjzI0-hT#l7ijdbx(m`yv2r||lYAoWkNd-Nc9_K6`_!_VKwOxm zm17iUAUEwUo*BmZ_@EJJF#}(|9T_P$Cm-oVJ?Yl*wY+dpEj!QPwj4REX`HVa=0{DB zK=CEksyITjPl@>Vo5m5AeyYk*PCU&FFnQE-k$HBMN|h__xT`Jt3YC(RNEd<}g^0&@ z%JT=+@)^7&K@V+7;-&kEB--P|W{s&eS`mV8(nxZjwoJIaTv=cTL84tQBC(7>`20k~ zmBEA6KIWEKEFa3@OaU^yTxwEsNy*9osLQ zYajl)cV0YvoA+AWuLjm{zWXS$A%C>Fjp=ch=|>K2+(jKFi45n`iX)olkT~Y?BvSZ3 zQRFTcXdf3WurbEf^1IVf!B>sP6!*Xrkpz!Yz}taZ&FdD1x#aPSq36Qr*}VIj#$0D^ ze?{95t-tayPI}~t2J1l^E%LRvl(n&xZJ(MZd}%(MbU=~-R+OEAMt~Xmiqa_23;R6j za;IvwT0&Nbkc|dQ$1jMU311t%@PL}e&)}N29a>FOd^?ejM6yG=!;LxwoTDZ((tb7V zv5+jm(b+(|Q1b>0eL^l`1y2vfNi2q;a?%31V#SPucG1l7vz#Z-%`KdBKutT(;F`t` zt)@x4Em0fPinVe#?#+3#km7`AYx}fmFOGG%lnueKGS2l2ZMTfbF*d^o8ZePXkQt1s zc4sLG8E0J7@x2EeVb|;j)bts=0ZSj^25c%`lCmQ%nG5h4yDe4gh#73Z^^vCu>$piHkGbravr}onQ&K!wX_affI+?M`7Vh|(F3-)ae(Qjmwx7A1urzvTo3Lcc zQmP`OFxyG;RbrvK)Enm*~d=9#k^rIAD1i1ndz&euTW5})I%G{V-oR@@iZr}rRGuhfl` zMTv}2)|j8o4muJ%*0&XLiX3EInCMASE>F3vbOXn?=>)84=75^U&fLvddPE=AEYFK3 zc`+#$QQL^G2jf`@a`4FeZN_@Bf=J;wUGOIwHfu(*r+=aFNpnOtXb|lMa1i5j-TaS1?STT^wT!<%-v;S;L+qE9WeNV9lqXZ z$7_QohvY;Cvt!B3ezQCxHlX2w43L%OcGYD27TYc+lLLCuP{l0k+MSfcE0%E93A zHGPwM|9>66|KFB2Kfgh&-LiVY@)-Pd@d@)Ega7kx{ygnI*Ns-;k?|!L`Ib(xZEib* zcN>vI+ifHeBsr1-sboAI8RgnV$u*2h`>id#LrXM63;;I$6D@G>lem`yzya_B< zZr7YcEQk?}CXQlEBSGfksX#76(faOgOlV6NAH6stbgsP8tqkkjl~+2QuEbHPy3=r{Rb?fvkZ3=aAxXKqgx zKn|@Z^QAH@Cb?HT+Gg4~O5Rf6zBMbN9vIH#w{s6BR+CBp;4uazca% zjxtu;C?oARWAX&zwv<1WFOucVU{H`lc{ZK!2E$#v=0|K9n-t|(Hr(r0=#;zRq5V`c z=Lv)e!{6lpvG*pxuB>-`;L*9~?i<)Rc(>2~bR<&Yi-h86WlIk%gD0p5B?X+F`BXRm5O0G$CQC)hb4N(z0|$Znk8O z5W(2hk&Tulp!Dv2G6#J!eOjNqc6ZARbZNU~%IB;;hqTnNB$3FR1E-s2xN*1KD8NXU zZwQS_b+*XrR7@2nTm!1eVoBs(ENUo;i zMWSj|meO(|DOrN*^^QoXBvaqLPd*I#Bz;<+ymt46AWkl8KEa2*cFZp%dDv9cLfcU4 zz7uXdzl)p%tLkw_ZmC3Z8jofrTJSxilqwxk$0@00%Sl}e)HyhlMm0_Cw2d~4?c687 z{3k)5jGxvgui@k=h`X%$1Rv`1(q~mHiJf2=+LV~e^2WV-IuY_&$xbAMoZ7M0rz8z^ zkTUF)pHpRyJhU&Coh0L89k*>&QGprT?cVyGTiR|6-r-fUb9@&dbZ+JlF$pZ}|8j5Oxwi zJ7Dge3EL1pdw}uzkKYGGo`la1czkCfUlu-l0QB`Af9;D+xdTSunW&e)pnb3hY#;EW z_X24r;j;sD-wvM7<)s_W_2|{i*39ENZ^^6 zSGeo2RVk$GIP5o41Z1TU2p5-TdR~~#YP1(Bqk#v5{3}Ne9DVrsZ9v$`Y_tP(;F+)u zp}PmBp#S)-K;((L?f@%zCh}$Ox<}>A{^Pg2*pxd!4W5a5g}d&7J_y|P%|P0TyY7HM zcqVPTyRL6uAM8=&(s}%*l{zbT-2u7qOr0}#-J?+D^VP?1JT==r+5yUN7qidp5BI4` zxleUS&}Co!VwdfJm-y;*-veY3%%pz+=yNiY?f|rSrq70O+yg3cpVDXj$8P}Yoy?>= zAS<4!_lluuk1~_P$6s~Y76(T=;4NO<7JC3O`j1}^M4k*yJ76)MiG116v`4AO{^QrZ z*pxfqGoFch#n7}z#h$zO-+S=3EAKhD|AD(de^G!re&E`Tt3PzLeDsS)-*n}7?&Yt1_m%e?{@h_R_qTHD zmSll1`JaF$uPx-l`|r*D@|WD|FAMm+Y617rH(hf<^dIa~j{oV!*v}0U-lJ~>a!NqX z9*g0f<-B~1Fdyka($@eBUr!+X>@Yr#EMn>)*Sxj#IA_mK=lea%PTu+KtrXHomD zWmgSgK9W}YfF|B!#ksRSrw;o3y+`d6qrZBeW#rSVPMvw^R-@0YWFyy{x$z?rD713q zJ=SSED|FtGy+<67w;FKvSTpP_Z_UA)Cv6QJZ1*C~JB!-h*6W#wrw-Ow=>tsPqfFP% z`kXpg{D?j4&OPuipN?>L=3nQ#bNGk>^1k+K4<77+3bwO6GYZVqO<##2>pr4@$kk9N z_=`q9GcptI4M!=|@D0B`tsyg6Z%*K@)SLEvB%m~rE}}b&d@>aFACW*#4akA%W?j>( zE)MyYUqu+6GU1A=jm1)L?zB893d^?9OvGZF*jdh6%|Y)G0feo>Vh>=|orSH11r9{5 z#@!l)kyH;9?If`3vQCTetjft;Fdy}E-{m)-4#l8Zf8;3!(u)0@H8y2l|I09 zwqJ;(E{#s=vl$vmu)u-p4?Z!~4aU{@R%1HMSf=8P$ey^fKBr+Zd{hTAR&J`uMWPr_ z%54jE8DosY#;DdoCyiO9mhaJ>!l0JU!tso&*Supq2@GP4I*Vbw!x;_JW!G|9&%$D( zo5qh&AaXUZa^r3@MscUvr?t4pl^RXFWYq>!qFNEkZLqk)(0Eb@FLx{p2r zOmq)%xbmJ0p(*sfIcu9t^JoKp}(tm`|=dwE!V7|1yZ3%Qg| ziqV4WN>y5z57A7T#f{YC1*I)?%Kh=A6Azn&Va|8AYb`9mBEbOCR?ZswT$i2od7%(x zgF1~E^EomZcH@rK8i7jCI-_{pygRP9Flgsh<5p)q_gZMx2Hv9<&}S8=dz>EZJchq~ zg$Z^V*A5Q6N8fNJui(qGxzdvu1TGHTsbOha0{75mzFh(}avNeMSiVeD=K3<(xmRC4 zF1U|)AZg`4dz>8XY}ph4xqJ6h2k$w0-NF4oc>g{3{>i=n`Cjwx|95wM_nABY;LgYI z6mS3P?H6v>Z~gYIAHLPTb$IiWH?5nmyYZpelp@MAO!R z|G4KrvW4Gz4%06DKD>qBdUDV)MQMvB?I_yF7yGUD%(Tz7-#2XGcP>75;P;^|{LaM( z)6@0EvPnyVyI||{dT($}zpvlI?_7NB!0+p}@H_9%PXl)3p08cZuXN5|vUH}&td>lb zkT7hFOwY$#bql|9{_@ma1owPp3%_&yJCZt5v#Iv+xlILD$8h9x{ZZb+?_7VhyKHx&CmG-WhG6T_ebLF{CEm&;dd@Rcv5YMGb3cOnQIT)gn*sX@7as_70&es z)6zMtp4khYS&p0lpsAkY_kk_^&c)jf{PJ7)o$FuP7%hiwZ{&t8VKPrA%K7p1{ag5* z8=rUJ_izipbN)hxk*`cKX3*0F=b)1E2x&}clBo&KVOWx^UI_x!uJXmT!wp31NC{DTus&h>WC6X$fy zqqZbbw8Hg!!8uLdwMCP2(YFJ?4_?gg9RA?b4+Ku|{|R-#`e!bkxrAT>a~5!=VNrE19`119h! z9`bt`U5PM+B$p5@R~vKhp&9d2L+Egm>Z^wE8)9QVF_%7g)zvnk;Y1^X>ab0!nV`{O zz2qpv7FQlKa3*SvgvHacr@+rn*_y6~e31o5>9u7tk$B$5In60RoE}0po;~v|C-_i< zH1Gxh5^>cZrssA~fT0RV1%luL{G#b&7a+#iZrXhj>gAbz^6-G9E03>z;w`J_{_Z#B z_91<`C+-^E(C6k>F&=-ZalwYQG;N!5;Y%n@55@@?1AX_W9e*Rm6M==KS@A`AW>o z=ll(d0Bo`>8AnbREXo|yiriN$0#@j`PYK+Wj8{9k7c0mLiFD)S{;(O&lDG{w0iP`+i?BL}k=Mp=v`zSb<@;!czMnduuQ z_kEmg1^xH2PrPXr(%*R{ce$6iubkX34(aOuh#~#*tL@*XV19)PAdEq-LHA_hemPFy zmp*`3*q$2$m=SfjIv2-rF+rD8{b5*6J1r+Jbeata6mD@WR+xj*Z*?05%VEG|wH7uf z)3JMsj{3?hsgotL>l6{BQ2U_S6&y4H|MU#rf7V4BQRCT})hNcATTWSNXQ_2E*is_DlaX-h1Dia$`HT}}FeWs`V~FL`>kz9aG`2;kBR z|L^{fAAH~-IXH~(tM?D@{pEYwy(@QrBKHe-^}9!R{>mNW&eg*=-u}tk-*o%Nt)IU2 z2XEcJ`7okxi=joH~#t!`}*I!?jHQT>tAv0Z(ZwM`^u|7ch$T4`lG*d6dZk3 zZgA!2uY^~=`tVbSpL*#~K|XTD!w1BW0+rQMDPABkZXu&8w}{ed)2<{I%2s7KDkoYQ z!|Q5@9@{f*$Sv(T&-*DqnapRCwG)=Z?>c2lhH-hE@Svb*o*e;9CAShsGzRln=o>~W zt5Gw*>oX&AVR*;W$P$+zSxj3_XM8dmdFzM7hmX!QjR#$)3gz)iDo;mm=^paW1IH)tn;&n~GE!3F(zsi+8x>dp zKNxA>+x$>2g^s8x^Cx}7ariPrE25~av}zP--$TThu{d{N~vcDzKOPKkGj zAZ#K{v9CY_kSyhvL*F(nz12lsFFCdaQp%d3sI@_0EH~@-*H7LK=ci0@w9~6i;r_hQ z!XuPy5+PfL`ehgkiZ2@9j3v(T{_2x*r2i81Q?c7JIt*AIFR<5Q;fR(G`2 z8nNH5X6~TH79eGqmPWzE7ZQ0f#-(AL^oFyj1b2>o*Xy)!10#sI4*H*E*UiuUqq8=R zMrBiO%S4xk8bsEKD!eUs;W;A2qgPt=K4U!(>gwDBP^U2oGj zO2AKZ4`?UstHU-6P(U>UguXl@o7og18?@VTkO)Too4*L2lQf1xP$18|9`;bqE9`W)3>3HmwPDsCvv$3f<6c zX*ypjt%#(f^$YuVohd3*`hp$wA|=*!)ZngDQl~)d{X*d{^|*fn(3$3;k5d=$m)WcZEkIwb|Up-|?jU?W+YQ;s{&FapWZU$%}m<&T4C2fg<5hY^#iEDRz zwd{B>vm;PsGpIKSp;2dicRjrzch>h*1e0wfsoHU?9*mIXyxc)hX*#3EZH8pCM#LaN zrZ=m2D?OgFlLn>9AnFEKix((-zUEMGJX5qM)Yzp^l)|uB?01V8>jw!}sgR=uB?je% zoR08;Fzm1$@wmg09buriXIUeYTA@zY*Ldb?XH4BJ%}kqE(xF=A+R^}m3Ind-VFPEW z61@iE3IR6DhQmpE9GM+rK1r7yGr@*NvLHt5On>%_sbseXEZQC}TdqgARif|J?D??8 zdNApe2HiEL2GaEV%SL+KvmIV1RdO+ponZqFL1ofcPJRbJc*e9*8mny|?x|F7;$R6m zs%JVt8XIB8h(?xTR+t1TpEWdCJeE56GVGCcZ#czJUYNI~HKw_Ldd9RC6|g!RRC?ut zuAmiVDngjl%;UqnE*QZe-|I4hpA@r7avU^i)=J5AIkm=vg@yCWb*BH<8B=If)R|xj zrE%s~7Q>{yXqOvHU8c$vub;OaG%k#aqPA#x$7r3ldL0L#ap)bACT;}&5Hrafzz^~O-^FD3-Jt|@yO z`J@udE|pblz8&aYkode*NT^3rv$@$j{{khu%J0=bNIy5K;8TGi?rMa z&&(IPLBvY(eAeR|%dAM^!%iu7EWM_o?wBqy{ID$%t@0vVM?xOVyrVwPDDGI7_&V(|>X++i~YyuflhF<|oN0Cz~&FZ&IkP4ql4ZaU@aG!Qf zeDIggm=YLiv9N6wJM#(GLkDd>b(xV`S`dBN++4GsJLQQlV|B3{*v;6X697SwHF&AWjm3O}m{)rRIphmO*+obVN{!O|l@@oM|&l!=Q%h5>xgJrXf&54Q7se ztQK^D-wILFqSd(NrE5Il(@w%c3VvhkK|{ulN9BcX&pKWWaXR5>jx0TQ7IaLdU9&|W zfsYrKHUlVu^OoZGhYcDH*P}l7YiCR==B(Q&$zVQ)XsjFs4vNP*hhw0eE8Olz4Fluw zV%4<5>{xckrZ)|!#7JC$B0&M{HAQpODN_s#!a7(pX&yzPZDZ1Mf=kL(MaT!DcnlaX0+%*2iDP?HsWDU5nVv16F})@O*ye%8cbbF2!mUn;#BFmaSB2`! z1QVS!g!@TEQ+e)q(3?=rsOc--Qpf~y+B>&~s-J19vgKqv?P$L2Hb!`THpkJXW}qAn z>2-3v$R{#7Fy(&54UeCC$$#+UXH3O1G#yIWn9deBuQ;=cS5we}y^!cQqtyn^PVGfE z0Dno|6iUlrLX~(J*4!ZyjTi0ichs*EEP$HGA-rV{4r`u?SBCSBMfl#ApC{QwG!7yxHkGjMHHo z4h&x11U8jn@GLc-*ZF z0|0Vgnjst~Q$z(io=qmx`L7C+^X27yi5@+_8X-FCI? zQp~6)Q*q2JbhT7Ahaq&Vk~HH=q_~()J<6?g3f^JtbqwWV?U-U{v!v!N$suEfc6bYX@vw)v@sI$&AIUP>w1544v`I3M%Wr4t< zFoVq4OImZaEY}M4>8w~N4)QgPjuUx-+ z?H^wIbJvni>E5FWmaAtJAC4)vrAIm7||H`j(@{(VMRPn=60q%C}!>-~J<4 z-g)?&hd+1gZy)~AL+$WAx&M&+RPK-GzB%{)ga2~ynOpkpy z7^jI;Z%E zR8;(y38GbXzz|(cs}`4&Pbt9sp~SIo4Iv4&mJ5>Yj$qtsD{@0AmTSEt+gpNhvjMI; zuKmI(MF5^}RRmv)iqIgDLT;JIy>wXahznQ58;m_L3+6Q6R=f7mcbrmqS`kTffit2I zC;Gi%8fXrJCsmqJ8@|-7RZEyQ6jW`(9pz6c9M_kY)wW&$bD$4e0N_0WmT8$o%A8o1 zMHgl2u+k?I?&kNMQSd|CZ1pj_NykBb>PP`0M?<4jOvbH9@Ay4UCZd{^A9%U3>F6{^O` zq7fDs%Mn~b1#jq9Gpl5m;p)}DeM(`eBf)AEXpWI_)URblxRN$_7maDSUY)ku%d9yr zCQY-MUH{5c3dL`8I%VT%F~x&I#u*k-DrfB_6k(-WJDU)NhHSPgxXB#-#WM;z>(DS& zR&lh&7dYDL+L1{T`plYHJu*SXY7dQ(*_7jQol^?gThauh`!%McGjtCX)ohL;#$qa# z1C@69IL@YGo2ZoRqn|sakOZbyoh&d(#v+c$>w3t};~vS^v6gB@p-79NSSIN{V_)&0 zD{k(A(}rZrm9b(f4~#}hm1d=iKOu>R(Yla{Nf_rpGABq>w|G9@@cpru0B4c zX!IjuHls;Y8x(9at##(A8ZTfXjaP}X`lu9!6;&(d8R+2Sr-n1NQb|PvvJG$VSfe=#_u33>xepv4MhK#!UH=GjCCf~A99lR|?%5kgODpD9f;)c9iswMMjwdB^K z644ybQ*?5w6J6?%^*F6LExg#8pnPLE$3nR04cUb^<_BuMoK;JeYFQC)-aMtCgrza# zt1>uI?6-;y3)fjTG{kZ`T2cn0&=qG~4*KMD=pFvAXB3ob&gQ*Z6P?Yw!E%x&NC#@f z9$blP!%@i;`|hB&K*n9}`a4f4NJLI52Jgq<)m*<>!m-+PF^uyH9w1^1Zv;groRo25 zNmj4@^-~J8Pe-uXj{$(lvQh~;aLbFJ>WC7OK}60OljUT-O(l4eUcGusQS&+^Etb-5 zFrWrf)km7M?rh=f@|@|`!$BAi`GSa>c%a|-wo{6-%+3Q|XcJ^R)2IQ$R#>mZniSS1 zTTpjU(0sknR}H_EUVGaq18l|vkjp*{Q>0(^&*UuIT`e5K|(6DglcNghWQYOl>G2o|>+ zXumfICt?VTSO4xAg}{yC;snbGXEe;pV(ej_3UHv<>HxMXSkD@Fr^Bh+t?4(GgpV=1VEWv2@@nCX;I1_V=3DVSrfeebD^e9$Tm@w8np5Fnf^tEWO7 zSf4Q+XFQv+S**GAJjyDx1|R+T6AF9o=9?9_TW?NlP!{8f5oT$B+!#Z+#wT;Z5et<9 z(HXLueoZ{3$hxF%VL`7|Na_Q+VM@qEku{pOP|czUONqP@xPWi;7vlBPk;G1`(158F zN2x6&9f#sg+MGppJf?=#B%9c99o*SR67q1G+<4uojImkjSp%8|uXb^#wP3@kYJtbQ z0KsfzwB~bx@THPR2l7C_{_c~YwMVd#vYCYV%eY#@D0wWV*4!QS^AVHaP!b`LNNY1W zc9`r`L_&MrCBj1BB`~9MV7g=%7>;mrj?{X-%&OIX4KMkXe3d);7pD{hEGF3!57k?i z-Dk$usAAZ>2$I2FqveAW^j>LNG|7IKhY#OoFb2x&7~st)X%|H~;wpP4zb6$rHm;RjyfN=p+b|C^Omc2>c+5) zu@xjys=d*ahj0AGDMgnTr~P=Qz*TeACej`0FsQ)6`+p}hqy0|vIzHQ{T@DTTo| z8l(W1npuxqP?HD+Az2#Agj6oiOEZT_S*%JbO1(`S8DPP7LIiNwnhTR{5l{u}So7mb z&r4Od4`xHFN}6KP4@R|e*en~}>K){iA{%#?JYrdfj#t1Fcz#IDz}P!j`147-BQ`R& z7Q(1r)asqv|M`?c!!T!RvTzNx{b^xOaOl!6$T z<3%6Vq-whyN4<$;Ri~v=sx#G6zd7ooGh+-kRX!i8FBk3Ph^10cU~Wbl4pjEAu(6BsP)hxyrCuD`0y!3 zdDIyX@<{=4Fm&9G(QrgrSl_N-?oz3zL5#=MzQDE^#=cITQXts?o6SqpYJf1k{-nhz zBhTq{kU?jdw)$Qhg)2@lfq6Z-@{Ti#YA{)P0$VejJSuf+ugMKW7Lcb1^n|HR3t=$4Gx z*cc=wzExSGwVU!UbAeC^$hn^ zuUDp_5RA!E7>7*@;{ED`IQ+p=3IG?xBD(0fDQ}lT*o50?1iMCW5j<2RD}hsw(=`?^ zw8a}|3(I+(CA2gs4Ou7UD~_to7&T}FY-v0#CT2b5oAatz#N$>c_x2MR&KxCcj7Jo$ za-B;mWqTC!H3A%AWd#;3cFA$U!x!z5iXhqbQ%7`Wbakq56NqaC+En2QaLTF;jYWha z0)#ay?4VMI(s7B^&};wvRK~1eLOl+d)nZd~%S?c0tkn+Yqw&HqM|A>Vs|Ao+C1*r> z<)55VOk5E$x{9S0<~0rO3B!KTC!2*xnvS1I!L4XupM-DzVC-2eC@|h zl}hL-ILtO0Elp`Qm*eSZX?k8ma9P3Sy>bhgjB%cx2(|+=ry>UCNUkZ35@n zgC0KS0+O+NoI+wHrdWt(^~A1T`TkY@|2q!MgZtll@4wyq7V!4}V|V_`oyG0nxt-nm zPq(Hw|HIAn#&6#kUH|vjlWV_uEx!8yUJZ}_pQGT)Z(Q*XfBn$S{aVgG_*FoD@y~NF zbdFqCe6UXi=SwXv@!SjGglA6$F104wb1#6?pgk41)bel7z0f{UV2@hWms{WCxfjH} z6}aRsXzi`QC3k^vqQD-tyDz&7_`Mal9)>-SdRlDpvRP88S!xZ%MiEAYV+1zNKSqxPD7C8aCmG*%rRuGCTl zp>-!Tx^k)B32LyI4;T%)-1nEyy-?d*flDpc`rHfEy%pHwRrk~1ojvzL^(SsgH-Gu)nR}DlzjE}yU47rJe|qB=@Bg7Y|K>*h$`4)t zJJ&yW<<0kgC->dAzwz$BzZ)HVNAA|)pE`Wq#T);9{__0cHLydzari(2>8YqN*0rWw zYSFbxu~_J$sM#&{6Flu}i%vRSl>LfUs@o4Hc~orjQ#xI64Yx*vVySu68yA|C(Q7%J zi}Tzpp)0&o?Ip(xw3kYY(U4WiL8yr&yPi~byZl54(wxH`N#T1@i$v_Dt5SZAjKd*2 znJ8jUo`QQ>eB5BzDRa3sZ;x(1(V-q!yOm%nMx(;G+ZB6dZ|F%-Az#juVxY~XhF(%i z`2e-6;A90y%0f4t!>uaW7a<~}M7~vkVA|+UtWpys{RSw5#GDo$FZud#F=X*J7u6jB zoNcXVnOqq^(V^jX+05+Dm5NJbC{-jE1D}SWu|m|q<#8CZL7&FE;wTXxl%(>KPQYvS zgq_sqP0EV&*sm+hY+|AnovB#WQJP=asya=MEndnWXXT~~s+_9d5eeE}&=)ev$ z2SYL&b#0J53({k!b1to;MT?%ZQ5C#S2zvE4m3jpFz}fwT@c5}!c&_}TCpwg2e%MS0 zN$TOt(vpIzTD~==HD4OqoT?f1RP9F<2Bl#AK@TMLbi36KNX7zZ=+vw*H+ifB5L8uF zT&j-H7vs8KaB$1fBTXS?G&Ha+fHCEq+*Z~0|28ON-Zv?OTO2^_>_ZO+#?)UH_Ii!|$Ev`LR>~g4K`^5v zwp^hWK5q1eL4}@rW@lh|X&`i3CT+F!3f8TV%UyJM)5~;_FkfPYL9dy0+~(M>av-li zjOR^*N>tCRPPA^)_v<4V=%7-{VhG+~6An{@lQ@}4Ttr*5rVUGI9YF%g(pV52+#90Jcv?|K`mG7OWJA=416Fb50+hZ-UDaWR1@k_ zRu~|~DaVuTIyouzilFAPs1|FB0Ha1e`t)jlM<0HogU2cq5mYFDTx%sV#!20N3!13x zusy)WJ+=liauQdYjv?j&7e(Tz=WwLdk_f{K4R9zOg5*5#3Wirrb_rf$aGj!89?0-< zeVnF5le5fJTFT^*(bnHl<=%bAKKS;1=9e$Uw=cDWRC4A6txAF#z;l7=`_M$78am+_vGI2a( zMLibgLcN)RlxscY*OQhHo`0DRvc=EKfdv=s2o)3c!D-vVOa`>jaj*$PF+~R z#0PK%uj0`-O&GPR!>Zl@w~R1mb}(;bFT8~^K@@OjUN(gQI@Xv`yktl#q$#n*3>R6r zHa;AF)e{{cTvV1)AT+aKH&rWbkS*U`ChW3P#67rWME1f-%e7!p0kFGDZ_r4Zi)CdY z_40+9-GrIA1CoPuDH@L{FTxffO0sz;<&Kx6!@C){h3N!beLm2~{JIW*<0T#1%L#Zq zCJlRY+HN-qwc7ztG|H+`ssVE(s5X!a#!zo@D&Bc89u5~|6m!i!r_z>A%w@sOdlFe? z!Wvn2!JCJWWBoxgrQu_m0r<;xY(5)uVALN`%6b*TL+kDnCBj-uxZZvdfZr*#45@jw8)u=hL&4q6*O&pY0 zqQw?h5j(vmWfxe!I;>B4e+(UyEnBpG7rgnJIpi!B2CsZ{0YO*Z@k9skILexiJWwX0 zro|0-Dq#(C&{GK;JdjEeka{J;Ql~wk#^HlOFKUp30(jEP>O>Q6h|NW?tZU^UcB(Sc zDZ)*d#=7NdNP%Q+V+tNGMkHAPx#JWTDZ@NHY5mssfx-7RG+5^ayeQ9l9)wnxAzmot zmr0oi1t7#`KsMTTWtb7F-{6@}@}Pw&{W(4qyJ(Xd1xZy=;Hdzh5XM+0Vf7i0bQq;f zFjQR*pZ*Z&TIt}W&XDl=2n;XX?m&@KP*tVds&h`hvfx4C{IF96MHSN4cr*(Hu){F4 zR?L(0p^pNLcm*l9OkZeEO|)N{Ly0n&f(Oj_#0gjFoW~7~>nzdckmI-pSe0v?^+$78 ztS36`;glX7&x=NxWfp3uHD%&3=4RFI97{@z+0-tS?TMS%vd6-s9iCZT{e_owXs1CU z8WO?{+J+*zRGuZt;aueSGDwhi)Amy5NCWd(Pe~ti-Rh9Us}^F*w%_hb2xH|J-F&xC z$!L_4CBNtE79`J@2t4n$V%ZT%)F1l>c=gjf_a5`+AA3m$sTO3jidAmgOWXI$_%sU& zL~S@g9UlU9-=^AfQg?>sGN^R(fN-EFOH~jON zmC%fptv@F^`snKZ|Mr1*aPLpvec$c>{g!;=-(G+2+Ut*g^vZ{F{{Z~+W&M2I`}p-6 zhaWxucKhHU_Z1fuUPIPPPPRCl0P`ZQu^f)h_2PV{7fHxeYg2|g`8_qM+S0)jSisQ? z>!TS+fE^l?GIgjZHdO^AnNA|`_$dR7GQAju4CO>tfYunCXaJr|JqF)n^@3eg7TW6X zIOK^G2_6^06RwZW1(^V}Y>$(@Iln}+rpy#Ld6-&7-nS@!W|zdWfO{E-p2#Sk$e`PZ zO?AAMRQL5FAK4{tC2PT{C$a!zb^!r;5srRASz2YVAcw>1=4~dI=-^rLUT9mW`nKe1@Tuq}7!)R2uaBbczf-w%9@e zG!G=Mn)>LaroMYoQ?D10l1QViFos5Ruc@ZKEU(5PP=F?4luldam&VwNY6~IS)#JS6*uBI~O(edZA}UZ5lFZ zu#L`Zsj2z?B@G}nKt=EHrXFykHffBdPBb+-%m%MU8z@L+?Fw_b2RjKU(Gb40n)6(aUztUi3Hsc&D@)RncdT5h*Gg3Jlvzt_%K4e`rMYHC8!;CU^5KC8XO6>EH`IUkd2P6#_EHYn)>ENO}$>#TTwf*(UJs|eoZy? zWsTL;ykp=B>p%`_Q+Bx#4(Cn*-q6&jACD}&H1+h(q}2{<4N&DP7z|BFm})hU+&^hq zIwLn*GDob&>gT`prKY}dQB#-J1`v`1110UF5eIumFjGf)v1^&Ot=PAk;Rh8yI9mv6tT!GuvYh^sh@kPsjpwu z)Wx-?M)i6}q*Rirzowe{vZh56VO)Y;u(6W#z<-S*H|_P&4PK46i{unMVBwvXzbM5` zwG6J0WHzeDLzSBPQY5NYWeKVSNXlxge*RltYU*njHTC*#5!E!c(>B^HC{yv8*}_1V zwp+x9y>`qmBzf3W)B-p#srybSZfI&y&@;iRdfbs)Dp8!qqgja-e9tJQN{7^ON^041 z0%|#^b5TrA>i_3Hd2sjjH-F&j`@k<>{&|hJz;{eO@y=Dj)IT=OT`v0gD+{KUSEZ&m z)j+Nyn+m4xk;b*EH@Z(E)5-UHBuQ**YCH|_YDMr=yaRas zKxRet^4iBQDX^*=I-FWjV85c&@v0E@(AzmCd3jGnaoMi3kKuSlTMgX#?Y8?cxMRN@5w=~INCQ5*W&2q7`MMqku*)+@oA0Ti&hrrVi91x zK+p-~#frA;xIJ?kt#C+IfH2?!b8v!u0YFJUu&Rpwfq@yj;E%}auV?@C3cF9(J^9xIj!rm3jB7Y}d)brYNNEkI zd6}p=pgMD}AAnNkK+RWEm>D1!um)3iJe>^(&GN)-*|8@k)yA`0Kbn9tYY~TciVZTF zl=NklcQ_TXQ@hcr)`)7et~6zZqqV}M-cF>2KMnjsXA)pdb`=?DoNSo)21*OF%KnBFMS?CHP&PkCQU#| z8Bo#9YFSo=jpRBgBU?*Gp!8p9)&W_+*|X@gP2XnBC67lw-Be5g-3?@9WjctwKN5W6 zZL7%pL^rq3GWGH%`r`S>+Yn0|qQeFt7+|Odz5jA>j^Cr`I1xDm(_@gVW|e2)l^!P= zCF8xE_getni_^@?6~BbjOr=u2(Ahy|ZXXw{5t~dPW(>0psDXik5GrG2rqifl^faKY zh*i{tc-79{awaa)uL?d{s*<1%OIpLCPSH&(cAp(SY|$>=vtbo3^?R8f*{WMKjLNeX z-=oX3Nthy04_=T|QnL9;NvsqWb5~%k9i4q;I&k*Y{{O2Ee)!;i?cVtAkKg&VJ8!$K z-1?rIpShX4@xklqwZC-rUtfLuk$UBiAO5SuH|5&k!{76t=imSFcinV9`cd(#Zyp>x zC{(GggJ%JhNF%L;n}iKjJ=sH-OC^9owb*<^LD9o;U?E2l)^<7P2<{%m5i%_8k7M#T{Kr#gAZFzun2+TM>HRA>_18Wze z2=7Kqy^Bn2jL)JGY%8KLkGmjcg$UhQpM_LKfZe!G76#h8KAfcL!fiT47O~KmJ(e0-Qw=FDOJ2`dS|%CNVK8T@>>~Bnub6lJ?&--mFM7{Ao_LQh z<5Ph{`yf*@AJel@1w}L^f_bLL;(^gDW=q#gBaqS;al7iV>vvDr_hRoss=#~R{=|Fs z$XU8bz4bfLUBCOH%%}h}-uA={oiggZstZY}u1=czq-GRr`fS-$ol%0p4ihLNIk4?6 zt6kU|>z7^^nX&%eF^TG<;WD6I#h7{+Wd`fX;9GhF24=kVi5YIk@a*x}?(0@Fu4bUl zp{|rrs_Vw%fo;oa3+v4QYCyXy5*MkrexbDMcgKb)gcF%j+_qQO=Dn%4{`O~!86{xG zTb`Ja`Ak%Y>%KVUky1V1fB{@Yj14MzC{D?+r4$!+BGnYavvz28AV{m zo1d70BGR&^CR(dMsx)gOX;|z?g5DQAhPBIss@EalN#Ath4szj;w0;qCks0gX0W+eB z!|009$LJJOWn(?7eM@f?fEjOkVulLPdPp@8W+bz8O5M?bG;3Y4t|5(dyacJmTEnz_ z4z_FK3(eSZUPjix`=ZQP&2w*jVg{Q8`9{8x_Nf8NNlnNDmFW0IK+MHfYACCuX4jf^JnMRPhsB9jc?um`+LqwU7*X#VZ#= z5)?oiVpSMd!;940ah`+Mzx$%hSj`)M;E5S+Zb5njC|{kCkZ9(!24%@<1+VZ$so>RX z1YNAk#R6-Yh4dmbcAPih_3w@e&L^>!pTz?eyeXS#8n?FC+tM4WdE*UF%)sGx4?sV( z^2HHAV8-j8m?0*GrrzZ7m<8|+Q${hgLdRy6L2<0%K^LM%{fw(a^&U2| zFH&!PcHZ^7FUpM7EdIJDW;FXvqXb@~$f+L{?DAywB6O6L!euxmwF#9;HIPYMMXZ)| z(SQr@IE$bA?wIN6`eNCnCBa>=^?ALwj{C8tH&(OwS3WUgk3u*XwepU$IK2Md7iGq3 z7XOMTX6$h*a*-M9)7q}zJ^kEiOK(88zim0@4lhmcyRrY`D+12{hyGQ@*=+7Sw4!`&Cn-9M>_n&fqH}{=6HTTTHzdQKr+m9M@ z|9t!3zx>z#PFnyz`smfe&E8)+H&5r{@A@Sq%&A+5inaC70z{w2|6uO8c=U}|bDJd< z*Vi?G=9A0c`s!_Rs_RD_D>+|}=ia%MXoH|nKlo?4cWfnEU#_eqz4_ng-oBM+eQ~fN zy7El!ZCi=fr(!_#i~sc>=ia)NXnlfQ5&gT@=iah~sB~_P1BiYJ@pH;nq7CZ((#^k{ z>ue=jpZI{JFZ}lBbMjWA_32|p^r>&pNn455w_GctA69bhEuL4}pxq~b{Bv*KCa1K) z^S2@*V;<7!SlW#<#NJSq79z+h4z2T@mq;DbnO?of0E<25^eCj zFErkgYi=dl(BfbCz_;ZZTZuOL!|Q)2$8IIs;18ew^#?g-E76AG>+`?#H*@qhC)%Lh zC;!Y}deb&J#SKpM`9JICsI5dBoapn9@8-y@L>rvw^MiksBeoK4aH7w*ek_M?CEDOb zpQqoN!?qG_aH7w@=MU%VTZuL}(dXatuX5;Cq7B{j`P=_E_mQnc8=UBKzxsyUhqn@K zaH7xs>g#gfu+51!2>Rq7z5m8-asa&bE5Sd(i1)eA{zC3UTZuL}(dT|Dm;3syL>rvw zb3ZuAece`~4NmmAKlA;$4{jye;6$JMfp5#zwi0b{qR;(lIal3Ew80-f7yMeTvXy8< zH+}Ad-ZvKKr30m)}aX!6QC< znB?BSm1u)UeC7+^mwUK{s94+J5uf?P-;;aaR-z3a@fqUdxv$+yw80}j^A(@Xy>~0o z29NmkzkYk}Yqk7jliV{W`TvK1;^0np<9Dvz2ETmy=gTed``H2@@A1Kld5?P>4nF-bZ!Pa} zmB703c|+}d_SG}{)ZpIy{9unN;wKDt%6ok3IqWJ0>*-Gq)(4c6#I0m70cp)3aSLQM zyRilGy8K|c%Hvv{Ca%8NE$^|o`t(bc_qa*%SDE)%giBBbsOp=k@1ACdr@mQ4$}5wl zQDabQn{{s*R{Bc8@M%yiO9X|b^lFD&G(^TLn511&m+q_2d+g)S&IVc8N4?BrW~zpZ z6_C7WjVQ0L7M^8GJw=;XsDMQivOOcGaI-R-K$!@)&E8NjNAQA~2xB|Trb|94WGMhV zHnH7O!A?v+&3k;;i+PWGxa;LbUiEzBeKC2DzemyWYV#h?Iq&aX-ea+Z6ss3Gdk5kk z)`qU!uQnMGbwrTX5{#9HWW+*YNl4~e3?X6UE+=Z|Rp&i|*E-`q*0U$I$zn`^GO961 zaI6huuCOQ-=CGowE@vxb5Q6f)0we$!F;s**92AEg$*lE=o&-?&I*2zAbgF`nQ-ltO z^=y~)`V-Sv>GdD`#4{^r|IRBQ$+uN0r0h8CH&P^-@C3rerJ0@=X0saYh018)0n|O! zkpoA6`en=7G_y)C-8juu&O7@v`@Ab!jjQ{lC!c)3$2Hae&)$~@I8v4Sb|vXfdPfBW zT$mY~wP`x3?Ce#Qq$>MfRSC?5ER}svWgYfyx@8{_*+f(XL^iqTMG-_m1q9qcN3ROk zr@+IFi>RomuPRA1GnGuGYHS7Hg+H2^>EHRz?|jSI>YVQkAI3zySukuEOEE`W=}0sl zr)o8p&|7d!c{d77-_BgEiFAjwUBgeuaQo*SI|fw-?H-RE6U^~0A{KNqiJwW5G#hXt zkkgqfC9G=I=qrdiTnIMErdF@*?EP4y^G7k84Nst6wQ(`6Lvx9!l6kqgp~PWr*4|8L zJH8n6)V)X3Q8``Ad-8T@Ipu61W=D>B&asptI>u7b8;(^PHH|%E#H<#drXn!>*rqe# zw{3iNV>kTX3oMB6sr3s#-M?k{iFmJKSojUOX!h0w;SeHBC^8c+=IU8@P+2?wul?r+ zqSBz=<25rq4ia$}1Zw@tkegVh4VDtzR%=W?8(t3Bf^^7e3z(X?lgVwn*W;kS;9O?% z+bmabN8XgSF1Kq z-Nz!McngC%1qY?ZEm~O6WfzcbdjI^t40zYr^yP{JWtTC3_}~5?Gch=PZDJte!Mt^w z1sjLnyA$zZR(GHmF@NvkMXa=kPYgsnY&WcD+HF+k^hy7sdHik^-lB2;cXv&A|5py7Jfs<qzd%$Co1wq$15pe? zc5{of&&#evBh`AI*@Ap)0`N@&?7ru}KFtiXuHEAR=%n0)tyFcY^EkD*96)GH-Dxg6 zV}UvuPpZ;!kEsOZ%ZtWlCb!pd2eliEhI*n#SSppMw;jgtVllPovo$Mzb7k3Gw$==G zYiqHLT4*LeIbG4hg;po6Ez`bu)Zp}`3xNjKM$EoIkr@=zF)CRM7GWk01=|nQi)>H8 zEO6L8^=OKUbd!dtw9JJ|jY_k#LJ7m3FnmrKQVe zb1jIH)#U`GBEms~N7qZlwKv23GU|X9Lnz&J6l$0o4K)Za(ZS4ZH%3*cOb`O?x{4*g zoUKxn2`*!)OeNum&Gnov;j)_D-i|JyGHV)Yrwy^^4Fxb)Gv=ZK*Z*wOneJw4>YHQ1 zv7e0rKcBdIq98d&GA}pH`Q{D-;(!5|9s8N|IO)42FUSv^_|3#4^Uu!THh<=PVE%}? zzs-Gb?$)`}fiD7=NbZ+hCVffzfONYwHU6vduZ~|eo*O@Dd_URCvaiUloUBb^lOIsL zs`!TDdPPP4iu`NxYZVs7`{bp`*CyUO0n0M76J+n1I(W)9RiCwbF!=NuJOT&wuLP^q7lKPwb^oBSD}(Cg%16AHan{-{vsHS$MKvu9BWMkCr<859SQYMgoHvp6L=r$_P07TZKC7( zVwEU5N(~MHc}jk{P-s$qnNTPtzf>qRA-_Z@G%mkbC^RO&NGLQazfdUjRQZR6LL>6k zKGbV+S7T7F$b9Wd24m@zpJRf_D<{7#6#9|L@ARRg-v5+eD*vKT=q2*|ghFqSKPD9V zG5I%yLa T_|+B{L6jlNWE^Kd|D{<(#dZMgg3v3FcP{lxi%Jzgl?N$8w*B4FPL1LkYr$Myi!D| zJl1d&%Q<(Q6NYn@xp-f%Bgfe56l-I_NCH<(eycBaWCu@7Y!(U)O+IgR zGZVtwg-r?HdQ`tzBMsY_OACe8=TbtU^juOXv^GZxg;wVheP}k}KoW2>XOCvmKC91y za{P7n;8~qe=m%%DLZKg+)d+>Ye^$*n!Jn5*zRs zU||Zs#s8hTa{M?&c@+zeXgFs_v(;z;!IeY>(&{^4yj3b2Bi4{Hz54TTO{}i>Ub0iDEMV$495MIdkQ>9s?0l7mNvf0u%anKiDqWyHe9QRTB3PfZa`eeaeNX5(FDyLIz)OTVYuL-rsjA%sfZtKlLbZ|_Mfe5J!Nn4f38XGwa9Y9;z3Piwm))u27XS9)o zbvSCum-TuCi9#ihx?{J~Et6kw@|RJnD&T$gGgtII24M)Sr(A9Vs`yGwVti-N9WG)f zRVG$UCk>XAIctx5aSUZ%5ehDd$g?y$q0pHtx*h`&QWuONssX|UPaI=j1Ek%A!;gCL zkeT%8P>tG9Z5v@2X7c|R$hKJs18t9i2&oHMYsE@MjYTryr|Je@meN*Bz9bbdno+c= z%O>i~=?V*B%UI1`Aq+G<1|p;`c%9B>BahfYPg|b?X>A~Z##|-l)mz9#fi91$9V_ah z21hvKX$oPW?lBM{bwQaSm9e;~jue$xq?q>m{Y$isvbwA|S<7Y9PUcYd64qRTAgCsU z0oY?8Lh3@XR*H7iwm{I6r*bYw(&zGHmZrWypoHGhZfL?x_RB;Z@n|tY3{*V^BBU;$ zd0RYcwX~TN;IOAuD5YceK;DivQWc_J)LQcqE3B@>(7G#K5~2fTkAVoO3l1IJLK8|H z0sXEf>5LR&6;Hx!3slvnM4$ubQiZhHVGV%}Pf!Sh&uHrA-8-BhVmCR+_0`)>2ouJq3rwM>RT)a4RQ-!BUTbhzw>6^<1{yvA7eJ zn4Kz{A%itrQBwI%wCYwPI>=_wFfWi`)p`z#X-22@I&L5mFbN&RX503biRc6M8mp9M@a5 zwsy428|?Hj^p^hCOP75C%u~7>JO%kVuiBwiI$Z!}*-aoK2Q%88V?Yl?<4b zu&4bIqd|?3Wj8_6P9Y4A=rIr>b-_!xT&Tb8^MP&BUsqu$^Bi{}X{q_W-dK65nkaw> zQqUUVlqx5L!QnjyBBUb-~ub@LI;8T?*@q4hpW;?Rq5PGRF`QL?J&DrF0t9m{irYt0aWM zhk6V|NL{c|6h+vexUZzP>O9OtC`jAk)3s{F0vO5Gf>B(9N0Lp#O4Cgt3^w%{2$#B$ z%@OrRPE8@Mfe5J!>v|^q%#{s2 z1|p;`tn0DZsj*kbv}0!glIxiKdxrr(2R<`VpV)u=hvT0dr^YwQUX-oK>_Bz?ocXT- z@0tIQ;={6ir9Y5fDGf>fJa)#|{mg#B1G8IZ{yy{Y%!M#qx#Iv0Q}Nhw<6iZwWc00`A&)3YpVf_f z4`U0DN-5nh+sL-v(5YX~elXbste zA~mzCHDnL0Swmp`J!{Cmv{t9Uh}|vpPFS;sz`}gi(5tzrXQrWb70*puG=2P<_=W?I z6|pSUF8DGQYc5nB?udNswVbS!pi$9Ya|nUr2OUDuT60H%B6hl?qPb>Qfkk(>4 zn&Eb-iu#(X3gp&vRRukF+}Om3P^=LeUToJoCDy(&g%;*>GnI}zPmo}J9}y~kr;kYNeGe5{ zbkIX3t~G@U)J#vI67YP)9~lsMO7WYqH( z!={ygUW*Zdh55`1YPo%%^l$4bp4<0H|FqV#2xPz0Yovc!>(>Mp9rPOM8zL3I>p&;{ z>ss$2u>PLQNnd}n&Ji!m3^BG0c+(of3iBJAo6SuoTUYViE zD>`UwnUq~by~w+g`v2pXYep0}Fs=DWjkzI2FEMYa?f$kJXJnavdd(67#SdCy`l&TP z5GZ1&A51^l&Ho3O#%4N-TgDG&{_xI!qBXFMSUrlpvUTyX%#|(S&vK03pyK2bVpCNN z%OzwSnoCH;<*~g=86e_P=NK9@O<O_3l8WwefbIrFX z0EZA^ddX&~nomTSH_sE%Sf1*RGHZFm*zA^r@mHgn=g}iMsvEa#Q}#9>4a_sidkRP> zN_X<+M%59HgBhTZJ7Q>6vX&n@ zv}*n>gIvV7<6%KwU~+N3bwlggDB92c$5>`?u-!E?CmVKyfxXvp3~Kh62UnG;I$1?j4)IW!^RS`?>4qklB}KFP}BcJU4T}%+mBT(_5!EO+7wUo_g=( zgOk+cjN(5OVFe(+Rqmen!^FoXOyj>C-#%`XJullPJ6igz^fc**Bu_{xk^_LR07)i) zz`ddu(*g?Ms{t8xpktwG$VL)+N6>^BGLb;RQOO(fAq{0!*==Euhpv?LF`q_fEE{6A zUM}yu9{YRqo@Zvx-1JxDg||fxiEmqZ=98i0{*wLF7ryg$r57)r5_ zT3s>WR45lT;dx~>0vVb(h1kq6t&M5Ya2<)-6Pljj6mi-ozM%i_ zz^_rdi4;bX_6VfbnOaf2s4Eo0!F)7D`?X~v0`(Xlu;rJx{q`8=(Hdh~zPb7p=^5vK z?bE53GuQa=8$W+^=HyrKRZmwiZAp^}8`TWh^c_pJ?Jp-AJ`@flnFHFjN+D}Qs7O8H z3lYq_qdmd*k_7$HyB#8?4dCQTu?WTic1Gh@=*a|JgVHd@I_4Jq{m-_VSQRF|H5*UUq|oWA{p zL$CPR(($Kln!o3!tN;6&*Uo?W?j!M4S5L>nU!v{_CYAVp=}W%sQ#a3i`Fi|;zm5?< z|Lo$^2c2=s&DWjx^!xwtWqj4y72K&J?N~L8MATZd$LDZ5ROCe&&i#;H!=vV+2F2 zSvU}_C%g?JP%JkQrM6Lu)WQ~xIp;~nl^9loEU|LY+Go7+;Y~_^_C?PFi5G7fyZ41Z zpZK}UTkl4mt3ltm8#wvqD!yv(3bqM<&AKP}(3J-le|GytnTJ%HUOV@y|I#0R-q)nR zs;M5nZH1fmvF7a+;Zb~sA5`jVVYvTX3x6<2DpR8e8sS- z80Z_%vh9y&p0?j7w%FhEo{t=bF4Y}&SEDhvi$Kb2ho{l3#E9q)A zQHL^u#&Q`gSa-TCKG;I5lOWvccnQ$h&LC(d-S^{f{r+>O{kVL<11BE%#Z52fF46>` z{eSY@j`!D~!UIoixZt*Z@KvlUSj5$!o+_4ZP8^>8W%ksMd?$O>i(mdj?Vuz6`04?_ zKldA@kIz{T+5ejx@l~`dSj3H=p5S}VAnx1O`1&DdxW5D)xYsV;r@za;COzQhzrOEpPk-}s_^PQZ zSj4rPp5W82{r;u@_4ECo`TEz7IjjAJzrE|DTW)|H&(Y=iZUn|5*1;%ZAwuz6y5*i@18z6Z}IM zdZBT(>a)+uKW&){Z~fJVYbEFY@ySo#@u=qh*&{8TyYW@1D_F$so1Wmr?44(Q?<&bn z>5DJF$n!$#{l~|Ot;j2+?4n!t2~@67n(@_>x`IVqs_6+fJ$e2)r=RfZ|IpA~cORbo z-hbTv7k%RMXB})hxb&X?@|~PJ8DBlID_F!`nV#U^Z@X*9ftQvWH$42ktClui7CigC zCw}6-=bm`=wSAzU9&~er36K-If<;_`=?T8*!z=B(Qk$Q-;r>VCTd#e|{KxCBhc0;J z)}MUojvMxi#WtAm)#JN@Mci!Z2|no&<%6CN?|9?>b8~IycdpZ2dc?lcEjzyV)Z<_L zbME0EuDpS-9@iBt;=)Q#@OKY+t?v>W-*L##W;X_ZwcX%oedC0K=g%6O z##fK+3Knq>r6>4?Cl2~n)$jS!mdn2Toavcu@11*rr0!o{e4Prl>JMG|^R4*mFJGwJja|VaZjJN= zFW*=>@F)9CCocYK?Zj)obSVw{?<)VqtxTMgHXU_=?x^46tA?&%5tl)Fg6GeRJodi% zzhpm`H9UO8N!6R4vc7cWrO-ESxZ{9V5B~U-4}1_`)prGpxH-}je4_fNAACIe?!U$F z)!y)bqgQ^ubNAWU*X}><(yKq`?Y!~Z)PeY_t}9r?<&mD?%WnJS%+a6!?WOHolt=x~ zU;XbJe`@aRaO&lW|29+B8$b2MNAOi`SFngXAw9ug!d`vS|FO^9bHhz19k=P2dpoE6 z==NVf{F3CfpWW*G;Qcp!;6Z#<(-kb@sz*=orrKYjW4?dQ_#+RaC*E=IMTcy^&A3(h z`oj-iweg?_3_E_fFTSeo3KnsLqbFGT&7)7m!L47u>-j6+PuycX?8x7~amPGRyXRdX z^8MLsPCOG|1-pVpT+HYRZjArog$w3X*Cb}J=Wc%Nqs_}+dZoJmk^6q-k54_k<%+Ae zd=Fn$bp?yKZ_yJB#hnKpbvc^F0d*3ZD|Kz$;&$;g7(77ReRoN9R z;u=Ly@ZrZ8e~U(MRlRiRfxm!`a{YQobVqc{O9yvWEQUu9*!JuSzPj9FEc|vvPcVG3 z{nYp$C%Iz3SSNa{zQf8@fyPTY3fjQpg?)Ghex(OtnJu2S>_pYA{BW0yY( z{^d)*{`8kmdla$1D3jcIXLiR`&y(-|=F@i{&Gc?dUBM!5Q}hIX+xzv-K?nZslKcO3 zsp>|Lyk3Z0U z`1_B3aY}OESO7SDY~q!P$0lx^Xiaz~4xbof?)l$6zA}F5xIuCfvzC7acx-;h{N3{_ z^QX=m=J%Oiyq_>FF=(sCzpFo89jW z_$}s2mV3`*uIE>wk+|~&$~#ve?i_)*vwOIaemdL5kt6;7hXQds1mb=m5cmBa&Ig5U zR4ELnQ3|u$vr%p$rWrsOZu*Y`aeokq`@KNiYXWh<6Nr1YhjS&H@ebw5w?h`ffkf-v zetcZP#Bb}Zh~X~3P%4Ug%+)C6Kq{CwQ_U1P{p7?60&&L*#2q6LXB3Dt2*l|H;&eTn z7Yf8uF^9E)n4*P@2}3w~QG7`t?u!C(_X))Pr$F2n1mZp~5O=RY+<)|NXfTT>@I*Q8 zEulGIk$^e+oAe39c?IG;0&#AEIF~@2Qy|XK#gWBeI1{p&2;7AxYj6?maP^WDh@%AJ z5(05?fw-7JTvQ(=#hKUYiayd|+D(5-C9o&+8K<9hSN(YUh&>hH`RZshvy!vb-A`;L^i!hZihVCe zdJ;#GlVTFjy~$CaA}3jT^B1x|_xbFeNDe6SsMQl$k|*x8wn9v9x<1Ll1Cyhbs$iKa ze#-uwyiYLSjjJbclqwgK$xf_N__+alil6_aSM&vVxc;G>yJS&rv^i2b>3TrkrOt45aT zZiA+Xucdvaz1vZvIkJQ1Bd2xPpQqm?nBxam4ctLf#CO*|#r~jacBUuMy?$Z?j$fBbHa$ zpV%H_otrO#8IN=<$6W??g_yL1Z*uI7M`9Rtj`A6jS`JCMn9PJ? zNwWR>BgP+hfBqlABoAHHuq1cu-$i_+?vvcDe;=vNZvU=aWSOcyA(-h=t7?|%F2nn{ zm?VdNrbCAJk$k)T`vx=1cL5g6cVQJ|`R>-gi};w{=etY)K9Xm*e?Rd3EYE{}Cz$7< zt16BMNW~;h?DO2Me;>)T+rPi-T9)a)#|mb;VO7a8-K~EYkzldUwBNsvpMr@ zBV-pvkBoP_rSa$YM3S4Jfm#19aZEiW=7RW}B)f~EksR4Y(Ib)zxyG(KDPgw%57@M4akd-vr2KY_EzYAYwKp zL<1Gk1VkMC6GEWeB_QJb<9eH*B$~iFo1iF~z&e|t&?O+^r09B^ATOH0I-4LTn!q}n zAloG%Lg;#%AS0Ts%Qf1Y=Ver0_$vo zEnNa4meT8Og3Y1{tg{JD5lvv7O>lCTfC!=MZGy090_$voP|pOyuNtiHLkwqZC%Xhh z9PnQdk#=l!_LnoZ6QT(iFxadMIg?t*7E+l!h`M8~<@A|65^b5yxniu6Uuv}6HVQW> zg%H3+6A+PgW;B6b{=XZ?W*U>L6Z620JS+Kke)Se;U?sPzWD_4<8VAJeXO5nSagwBH zhv!6Ll2;DPi6WvKVw0_i={LJ6O1c?7c1<59VuN&8j+0&Yby-5Y<`5gXKiN$eLlqNL zl}&TAr_Dd^hZDBct{DUCHL6-;1Z)*9;g}67rIJm4(k&ZFP|0XLPct&uWMFS z#iGSEQMpV!xAx~Mm59dK)^$OdVk1Md&FLy&Ev*xqo@+4s%X+1CQfsu}9`hX*txh~c zr``EV%AYS#=?rbq*v-D6vaYxJQq7R5Y1PEDUK*=0iOm?AwM?oQrR!CyXY}1uR5R_` zx`|4FTCK`>Of=$7Kqcm%Hns9pB2Y$Md8lo3o7Rnz8m<^B%gsd@* zzG|}C^-XnwImDYThNxu~ZfhnQO(VK&W>S8D&PAgpidzFjhlG(PlWd9MQ!lcNC^o&| z)=f;lp9swoQ!y_~DK#ohx9LnemNt9p*V36X>muxzG`4F=WNJ(Tu694>H${u3A~)0U5~$IzP~KFdtiO+l zC^0?WUM6BvtMz(z1R9#*&~NGZ5+z4)IbjVs3Z)Lwq;V+AWJ=1%aCIEF))(oHBX20w ztiipGybRQgV)_!~p%*bEUapf~($c~gEqF0sFkvdV45X)P{@!JO!3*#Zoj|XlBh8VTqW!O=lwS{LJdnY~-y*05R^mUXiCV8V4h9 zNG!4cv@;hbJB}XR`_9LD+FLNJcB^bKLu;nQjw+kF9^YkD5;t74edO9OqhkI%A}HAA z?zOWkHG6b+YH=~Btm!>)tf_ITtCoZ-6fniJdaCNn;W4TZj@vL8r`(d!w`w3eGz7i8@=H20rj z(ET?ph*)Z|f)_-r4ZHY%IIXUTmGkg{t^56(<+5w+j{MZz)#Au0G532oTWZ%S@nEJu z2C$Y8uw1IqgL!pT!$Wg#BT=PQ>6ki)4m)+bDH)@zVPinuC@>G51~N%4rnITT`6`I$ z3A5T9ufV}J6b&i&df4$Vd(~B?&yJQrLS3;q+L?B+*r>NOK`mP8)DkruSDU;^t;TOu zS>oYDykv7B&?1^nc{18IRmrP@7LQGdSdlmdYqa*oaC6ZlD6y`ZZ8{T)Q_PyyA<2x% zw#bw+ne>O!&q`01x}_T>zmt4z{=4%x%{S(;`S;KLhPm;-eJ(Mln^VmGX!iEmGiH6W z3p0P1d2Hs|nf%NNGy6>cZ2I2m3#P-f2MFoT8^J%&UJdPkwpwlF8VlW^zKY zLvfqpG=)pCLH?@z>+-ARS^07Dxryf|?wL4m0-rd1{LkZ0kKZ_69XE~d&*TI6lI$YM zMm7%1PDGfcNGudd}!`r8X(Ar=5~ibfDg@mISBCcp}7wM0lp)pAz-+^ zxjgn=l%BjNsF8CVRJCUkppgp(ynN^ZqkHy@R_nO;B!J8K&;v%loDV%<^h@~A14h4; z56x9Ja3LRhz~~q8p$Ck9@t!R?@BpTsN3wRnk{{+n4_I=A4?PgRt9tCu;f-g^nfKheCPp7ws~yBC8Fh#tRApUiw`|un;M?^gv9Oc*8kiI);sI}fhSUdch>*hxOr#&uO1kH-&y~2z2u$s zKR7T3zq9`5ddWNMf38N}X6ygsCCxF}g!C!tmC}=?nO%&{|)>Bpxpn1ndC_`5aez>A|}Fx+A$mI2TtcL$ekqH?NGo3=6JZ3{%3F}T zlZiOP7>?i&82~!G1-a7_g0zq*?hK;{(B>`39jOS5*Mhn+7YYL{o$Ta|dpH6xFb@j* zF{j5B@M6gEeD&$M#(-cK6hxgU90cy_Eyy*76NgMbhslk)fzR+3z>nB~TltK^4Gp`G z2%8Cq)9!}(>eF$zpgm59KM0#bJ_xvlw;*>n+D)2aSJ(piZNSaE1-Tp3A*VZRbD1&5 z!9UGgklREH1lzpUfZgc>KE+#*>u{*qiU&OAFyRJn;w{K^IFC6Lu$sK?01SMBw;7C3=1pc58xPiAI*WpM6L(LAa+h+ql&RdY{aHQR3!NWGh zX9jNMb2zS2&5*)1chLaCj|R|kMS1dI$X#T4&au6)!_oJ<1NT_ zxPS#F!zR+~3jo*i7UVjd86im`=nYz2z_q*uxw>HjAMv9>pUnzf&0CP`aAvdHD&B%zheI)!$q8AQ!5Fxbw;fe0h`V2f|1lIvv0zyal;_6A0izh$Q`_2e_EG;DFzR9qypVZFLgBMSSMxGWFY0G6-87 zcGrn~^$nbkF`01>VzHRq7;qtPL9Y4TL6^nuuv=jh@L}G9T=PTBTnhGtLJkYSym7hb z>8+Y;ez(Qvv)Vmgs~=e5Eyy)LVMfg^2j+HIfo;46xiMw;kS4c1goi@F1-u2h)(Bht z%#A`19tZ;G^A;TN8`xxmA#V`%0O#=*$czsd;(v61IKdgcBcmq!zPCxIGeX1*WpZIJL(U3(V!bRi?<-x;Rr9{ z@M88bh5%>s7UViy2)2@;0A>pXfHTD(oSSOX4pi07Q z@w!l46)q;|P`-=>gSD=DIVywVaM9LG*Ag~D?MHN(02;RV+J;D^Y^}tzQDY}!$(xrT z%HC?0fb82(WzNnzsmy{eB{Ry7BR~fepsJi8Xp61b_5SwmDudHXx(4R_-blEVhLEaK zT@TbkUZ39Rj`%YzXWZgSSOP8Bk#l%UK<4eIGS_+qrX^Rwi+I6p%4y>HR3WZ{>T%LL z#LE!1#!<2AWBELi#*(C)YE=m{OgSqmTpNwp=w#4<)of_drk zD&wj|gTVrkFSnTq{17jLA=rcA${QZiq*1TZ-1HPPurAUH&2Cdi&i+I_)7CW{8_RKcccYQS)q zXhqCmnzkpE?od2y)h3k*)#j*nrf#n>UKW4b zsZ8ZuuMyKy#_j5K(3_>LF|Z7W@(Gdzht2*?#!wX@v@oX5wVP-sZFc31XvYw#mT+S` zRLImSDZQ&1347xnMrFyjp~}`PFip%}V=J~L_H z@R`eiG1*IFI~2-EY;pte3h)?kqvSs6x23m^pE`5W%mLG{Og}b#&(rC1h8Nc$YpIXVSY^2ur(FNR;hRuE>2?Ncy=Rzg!~`%273DUkZV| zxlXRlX@~r80&#y8h; zwOY8TQYSh5WSJgrRC^V@_uNP7MKRWsH`4$5{6^yL5-9IZfw(&+#>PCmNAy4Tw_d#? z`-7A~T<^W*Ze)D)>@m8(@4e?f8rSQuM&o+@RnlbjM=LHkop(3W zRJG-&IsP}(d+&QRuJ_*e=y9;$UyU3WE|}UOX1pBPj^2CequbGs-;wfq@1>7!Z{H6_ z_AmYZV5D9y7U);K{vcV${I+7_#mooSfBZ<1Nz!KukQClgDKpy5|R@aYFszlAiuXjvvPh#2qIP zcdS6%F#>VM9&TiO^y7CVj_S#?k(ppQ7IWtvF^nj9QZ0&zDB z#C=*I?k0h_PYJ|*QXuXV0&zDA#C^PnLvulMF;PLQ87faVV@R4~-)WgZoKzrAA`sV~ zXOHC9pJ$K6jWe^T!-dSGy8K4Z>-zKYk^STn`~BZ8*)ExTW$xj*E9a7PhPf%JP5OSx zZzK=(GyQpHH_f~@km-+{K62^}CezY&MAO+Gkz*^o?skIJtalIics@f*g= z%J*)%p+*1Vg++$CGVb8KbJ@>2=aEvH4Pd+tJKfs&s7OskW&{hm-?ozT88~0e|JoJv4~s z%RMxhG=uN%;UJnX_mFbHU-@zm4Wc*i*}DhM67IKW+YHIF$#a`<2%0B95NI5T9FmX8 z0GcPYDrg*tGn`NM0GcPYDrg*tN}klJL(n{_RX`*6)GgrS<2``p&4D@)!yZ1_186rN zdLSxYqR>u0^gs+d_|OAmj-3xZ5N9@?$lVlSA9}z?AU^bfkDSEgBV2XAn@6&K!0spVp}C@g z6Zp^rwmF^;J6zoFzc-zmx?CQbcx)mu{=MyPIx$rU^RpYgiW0Nydlrl zojtp)%~W;R=KU536HHi$jJsj6b`t7HrD%y7BVL%)+H2KLy1Yc%qp`G86E{|yf~)Gi zuC<3m`C4vI#=gE4-PU6HFir1Nk#?*aMj~ph+2eCK9V&Hv$!Rf>ssu%tj3DlTJ?UDz zoYR>F^XWaN#5B6uZ?vtHS+XzLYgx1iIsFc0N*jy0+8J}p>#4;%7?Q0)HEluT(q;`C zb4{|9Lz#n}QHm;B0$y8nxPEyXJ#dqfZFFi|y>IVYQA^fguXYK+mje1SY@<^cQi0v> zXfAIxCE6{b;!7ur%>W!1%qLZ+n8#kC@ifW~$PK`UuUa7}}c9#Y$COnu8teJWGfNR-?zOrKX)@LI)h4ivL0 zr&DdzcVZr|3AM+JXhiRB>&=4eV|5p)*7H;?wXNFMVA574vc^WvLI==RwgOSH*pfNS zyglVs1`TCfMptqo)}*z7`a**1+4Z4E#(64CRcypS={Dvu_#r%9G7)8>(hS#Wb&*h& z$}T*mQ{^mK+EYdh8WWKdTou3ba7UP4#FSY+jEcda4b~Q;A!oFagmpM-%9r(e1c^c= zkGf;G(=C%sw&*1Z$?b1-)Nt+ZAlfCONNF;>5VQm?Px4e)0^}z(636tMl%K) z0-(wki>hiH&Caw1@uZuDB7qmPG)X%5CSthLK(^6^ZAFF;>&IDZ#Y#nuMKa;1>IPqy z(pF2pBo!|*xdNKHY@%KPDGOoCSj}F+dMvt|+Cyjk|Mx#fw?+HjCE``u(a+}f zhoU#V>(st?iFk2q^lN4PO87&HyX@+DSETP4Q2iUlx| ztp%gF29G40gq5b7g82-3S8Cf9rapGO_j(&>*%;8`T8BOsSu$1Q-ded%plA_}Mk1v| z3u{2SN)UttR>Ae{+W)7v75X(PGo&&WH`S4%5{neme!qW-woz7>6(?)CY}!dV<4ag` z34$QualC6yOjwlvm7zgx%P}ouM;bJ5i$|@Nwz|RO#3&U?>6kr`x1)_zg{T*`)_lYY zt1B_I?n;*g_XxX=G}*oe9Xhy$CX_e=`dv-Z87agno`l&JsH#ngKnKpH3Td;$8Uh`j zpx}BqBTa^>iXCZ88eg&v*E~yLr4`L)5ql7Bmz^q`4yky&af*4qFx$qoOTmUuBe*L5 zu;4D&dLvDm<-@4h)A3MPEuS_wv{~9jP&xt)(qyHX3T7>Jb=y;LSbS8Y(+Ib6g897J z7o_@)&e^L~6QWb;HAZc@1Jw*kRRC?0E>p=84KI~YO%Tufqe!+McM9%s_#4gL6kr>j z+Lr9w+v%*;J*rTf(zhELn>UW@ty){V+KAYd9*A_e&CD})suURZs11Vo>>4qJZ4^@z zJEkR4B&aQg+|F=5XEJA#+pjO%y-`DQJywN|h7bvRxx4-q)brRAOF4PNXX) ztYk|U^Im_;txu%WAZnp>uu+#WhcqoXgQ-F(!S!$=CdO37MvRCj%>`G*?>yW+B31#j zOb@te?3}UrE9Z}#yKm0HeDk-@{9*>5etG(=={e>dKjq{nCXZFzt~g2leR*Qy^@$6{ z|2Tg6xI}i9>?rA%q%O%k1v+C2U3d{{9Gc(0D=3rb&=^8lu{;vuRQHDp${GBv50 zn%EVou(s{$E(^wXR;w*^Ql3DvRA~|3Ivt@idbpLOKwrenBn-m6MUqCMd8&E#^h$@R z?6A$0tRc8EUWd_gw{RDlX=@(Vz-e#YrB%aAc8$A$6p*5RNm-~_avmIP!{!=^n^R6h ze5K8@VVYhS%%<1DF65FX%F=PiG=AEJRS~4rHaqC#(h_Pb+w-(BstLuAdO|~%BPpiQ zt$w3vrW#?}3;d1d?(8il+_m36%DDNSwm-|i9%4@ z%&au~_O1zLvug*tKt$<8I?w=}S- zb+K_%6vIkp>QDS@T0X#A!|dZNIl{U5zPIT%nIGtV_9%b zyTy%&T`9(eYD{Hp+z8*H;&&nLwvm{ftN2QlWy2`BD7fkUxDl}*#WcFoZ**auR{q>C zCkr;=J6ENZGQ)=T<-A}vLws4pj!|Z%#MH?4%))m*c8gRI`(UM&V!w6Sf24Ir2js}Z zU^v-q>6fu#8+Z08%D`n1heT!s)&6A zrqR)Uqc^Y9ijBM6Ul6h5P+B>aVZ%o1DT3Jy@ntunDp|ssdfiXf(lyejPS-;jI~=M- z1CT1^P*C(GC&om)SkX|Q?yQ}BUFPb z(lzEEPu{d3!kJmY3nIP(b@3wh;Wlk%DkxW}#%=^f^jcj3bh}J3P22S5$Ddj)nxa@P z)l#X@)7qxdTJCC*Dl+Gk@-Cphib&wize0Iw@5%sZ5cTv-_L1ti0auA~mYopsJCs zRaML$t5T!VXfy`Uz(CT`TAE_by_MwwYIUrn29?x?!&y@h#Gwod`TV{%s;7f~e@5Ff z>ue#dx&Zn@PEcLXbs5$&sbZ9_SNnoEL|EBW0@P}m6I0QMI{}s0I+{w4(qV(0g@&U^ zrg2ZI4F^F|6KmH!%{Z>|#bRoVK=K6bUMw{eMYqw8v>E|*7)q722|Kpf$)$2;TQz_z zDwSYUX;7hr4QXyVoz+;q$TDJd%2v%7cu3PvKV5pk)>XBf@X|#I`3BsW< zr}?*xIT7Rbu$bF**1`JdTM&4=QD9s#9xX=L>B4T&renAbMq4-ZB220%)`fS582dMl z*E7ZW!Vgqy*mxcqe&uwb9#!hK>0*?O>InaGvR!d#>m6KODv>Hrhx8S|dUCnwi)r>c z{EUT0x{kR+godcv?IBfaQH?u7N?({P!f>vW1qmt?4VvTLj1s98AhW|{&IRK--0vzy zgYIPm)zVbzp6qfIG1_YuBv>L+1dVM1d6ET?` z7Jh>h@wX@lhY(?cs6Q6&I_$A?n15a%sturikAY}M2s88bzNB%OD+pP^tvb|4g3V5h zpp%|#j!-!~hDNFhX2ud3KU zys!?e@5f%Dfe8Lv2BMhBF*{M)IkRJjyrXxnPv4>de3Jk>c$Mq1d?voz0I2!r#+{Ct zg6?SmG*H^O-LHxHTNb2(QVl}mDuz=`W@yaQuIK~xjw$DdFsHYf+57otGKuOPF~3b! zNER(+7g1caw5>5B%w&UG#`N|E84GKRsk$qv&lF&L)@v~qi4s_<<&bi@750`Po6%Aw zK~E#^jbdt;!b>Kzu=%H&Z8{ToQ}fr4ImaHA&E7im{A>fbNcw@PFG}5WGgI59T~ooS zg}G0%7I_!0GWH&S%GOnf=jteH1-}ZAb0SO}yMI z0oNOmh#?4CtlEkdZKzsN7ZOb9lmy}J5D9nITYy8VgejItO+4Q#Vf7+)QrRqEUY`UtdnH^kE%T&M%wr1K{Y1oJBxA~$nu+C*MsLA~G_Gqc#U?Bmu8_B4NdA0$esAOKF^BXh9V8=z0@n{k>(DeiqSSrc;4FQ8O3!$z? z9Bo6KCdv@qEcnBjoUai{)RY|(Oko*KG*ZG$A^8ucdbE6zY|Ly@o7CZay&dtRF_N|g zt8O1gnZu2k(_U5;QPhDtqdDn!`Xx-|XaRClFru_kDln_dx{baLZ3w5Bq`lRUzic%6 zGN~4-_5x4zN_g9Kr50C1@vzybOn6}`K-4lC$ZCiZ$|wTGmZH8Qvn^w^<|e+Rlv2P# z${T>l@KQ+YXPz0!L$yGVPP?kDrHT%6Xn!Tq-3JtVcON2et0rUTly*B}OWV^~kkIvCK zI&*_Zcp-q{21pwD)?6y%d+kz?MMwCjWs?sc#X5+w3-|*e*Fc?C>fEmIR z9ti|9#AYyNFbNJr7;O_K17?i#UP4G75CYlf=$!6rb+=C4iwVj1THj}XeY@Ac*4lfo zwf5e%_g-tdB2iaHd-++k%uPq}SRHtA*oO;+a@;KrcBg9ut9A&Y*%vJYl&~v!tLaci z?Xd2_&HxFE>d-SFx5ep@Jx{mR2+Bd>@v1(A8)Mw}v$5qAWVG6B)f-a}qLwRtsj@xE zp7puZU#<~kYu0V?2<1q>VE;4Lh^CcAQ6ywReaf&AR%tlrE@y}>EGEkZh7QW0c2$Q&ooNo*E36UTZ0J`z z2-3304ntN|Q*o;(B2*g}pm#gd0=Y%5-Qg6$1@(oq4G>(a2a; zs}vm9HN9w73Y{^cg{+w#t`T)e2Y1hwt0mOx_>3_%Z7~~WFL_pM4l%hIgeEFoJeXP&4kZ?~?ilh^yR7%Mn%PkI#5ICq`d$s(-FIPMnJ-Nci8f9 zO41-9oW3Y@;bvP8gxpeC=D~1`FA)KFdR5`3Y(zsXX^KX$ONf?g(~Xg4S(u{@@^w^) zIW=m{58u9?1Wiy8kf;Zh`ie2~sO48!GM8J?)mWkVrQ59bLzHVQ?f%4k`BlR)gF=ug zIxOc4gdJ2ELdhw-Ih#(AtYVi%Z5-3XReOq>$Ah&}Hauq=q%bUwyI5y55s6U&H=$`j zh!e-D2+g1)tcJvLoGTx%moY8D5gSVtiFPr@Hwz3*b z9|xyv9}Mg+)`ugk&=seR>ZI%wa5o;uz-VT2j~Cn65&sUF(|3p|1(zyfvEk%UowDa?`TVgn5aZ zjmiAu|ZuK<*6<@h-tOR~x8%R=0upVYQH}n|ycCkp+ij1cJiO-?o+^MvYkwX+J)59&fmwIPjKJ z3k6l_iUqVwDwErU*~<6&#jZU#&aM#_zU zWR%5&dFH#<2ok(AB<<>8L{7`dd^(0%1S#5(Is~yh;zY=9*5ztZ2sHDEStAw-L&F0h z@3or)Fg+HFHhA6Yw1*9;ID-c{vNQ~aV6W5emyiDL8j(}8J%*nu?pQ=s8irhC#W+fK zHby;jC}BdcPG~5Be2rHuV?@No9i)u9{c})zD7LS|Nn2Z|39{NyYu{YR&7+&Ka=^I(|>vTv6)VW z${e1`2k$-OcHVRFO?$?{_~5k%=cm89|Ev4ocJc}Pi(O}5*nesIx6^M+fA0Axt)~Ch z-XENN@5!B$^7+}VpSktcTWwHZ@ZkKH&c8ABC8>HUyZdRW|F-vvL*)2B9{<4cvmgF- zW9xri8dg}$*^0O{M$J{u^_F>F9aiQ|IJ8(+2Isb+3KC%0T{EXr@%cZwL)3d=6=CNewMGbiiDU|+TxA?lXy8(wU1C}x(hr(cZGap2tQ<9!oMRRT z$B$njwC6Isy3XR+$m2n1ab31haF&4)&yVJC>N#?rPvh$`ahx zTInI)_Dj|Ll+Jo9F4vC1LE(@o&6^-?+TqdORYO|MQczS2){?C(=POFn6e9rNpP?R7 zGFEca=(=`6@#nO2>|cHgkHnUVmppvQSF&BQT<&P7AN8R@rBelO^1IHoFom1Vfj^9( z@KQwBFE4u|*`9XcL0_7YcEt;@S9HQY2gVqDhs&0M$X^p!p$z{9-mnAs-2@n@RbLOfm{n_v=!98SMQ0TE>)ImO#P9rUtV1ts=ev&KLx<88vm1;d*^` zg{?}XpkkJX=B~CzOon|Ea!sU3x=R~Z?Wp6EXdfBK(;+%{)TzdTcCS%j`u>CkSB*w?7&K=6N*x3vmYm?8j@U8^JCChJ^nBSc2D&jW zb;c&PYAE?O)nNHmyI;Y2Ahvk$ocFb$6qVC!(+#_nl$TZ8?;<|UneA1Xv>bf4iibRw zUxLDJb8L`XmhEjV<}7;e zgKLCGwru&pP(R z(fIPPf;I+jjYLcV9yF#a&Noy-VP*^uU5z*niow~&m>Wol;ES+q>ll*f3k*_i1m#Id z33OT{gzV5$)33VhsbD04GgfdcWR_~}7GLVv18qiYQF%V8!&NUR@;w!tC(KEIezJDJ zNM4His^(D>vTBTpaVQriN_JkV&4pp1E-)4YB1Ih zy6(8Y$f8~z8PR>J1c5tkGG$fk3?Yej6+>uWI+M?-ssm!{oX zHeV4VVgd!bS057>9i++?+rG-<;l$?aU2LLTBe$%2b(OJP!Krp@g5DGx=Kd$HwVKmmLJ12GlZy?PUc2NXOCMW`tWikEGLa@b&iVjVOIoMM%@(wu21M~isvM0wzNy~ z3^L8s)p+#u8mKxyCFH)Tbw{9+jR@Q#X-r9&@RgAWA{EpvRg2YfdGG2nQ3oVcY4L(J z8uT1&IOl3yAr@OgufQw5IPA7<44lRCvR90EuNFQX%+L35c)1iD2koPQ>gt7#If)lz zXVvvcvZ?saip0$hoqqDt5QFv%WtGlQA4jWBGY^F`6=Y;ZBq?Z_5h>_nyjoeQtRIc% zzYH*u*JW+cqE<+DFrEocrOGwk-b{{~ z{`~xF*NBB=PW%X#s#24nmmXfE%G@lBBk%+byrwSY>Cq~nbu%cHQ{^iJ-6ff#oa=*sVs!cOQZvRTFIk?V5^K?9@X)#L~%c{yNj7a%LROyZ4 zZVP00Mb$d%!C2-^YZ;I($wO45i&J6LRhDBUGV&uCydUruS#ajqUmB)v4uhV~ovqdg zVnrbZaE`5FM7~}cRe3vWsYN&fC#TS&xX71qU(fd>Pg76dyhiY~sKwKUzze($wYun# zCh3LQ)Em;mp|U}*VJ=%$bl9hIr|V^CFfMoUrECzf7BP>Lg zB!%Y#$VWSi8f{qZp09C8AaoRC(k~o;?7AHoR4aPTp-}PtIm(!-WQ4i4z{GvE2_8bB z9^qJZmIGeOWx8PV^Fj`TgUPf!q=p6#ru!BKuKW!2j-^E~$5RBZ4CnO_jdK=6_gCf2 z)!J&=Q(F;H9*o-TQlBn)p-RG+lE8fKv`mxsJ~3IcPP zQdbR9YuOxySe;->(OSTG(vl8Dj>IzSF%Ks(2A3V8JINwqudn1QIVabUS5W|(daic`P#Ijvf1HaW=bP| zVaq*?=wW*=z8vx>i}#DUeAzefSZdF(-FfO8*G96M zS3A|Z2DY{Cu&0c;L3UA^7OQA4CxsYC5oK;P7V^5hich~}jabfDcs`|+81u0~NcDOH zeHelp0T%E2W|1q+>uhcaE>NJ(@#kLD|6_OGvGbh0_%EJ+!};+1$@5c?3-DXdW@jw8 z)&GIh?>POOQ|aP9{>k^8{DYIy$;Tgm==l4OpE<4`KX&v-M?ZZ1HsJ7IAAZ@zdw|Se zT)zXzqz``n;Oh_i;O_tN{`>d8Wq$(d{(V&X*VF$Zy-JJem+gIU@4NOsZ?6by4*nrf zlW-6@MZvY zm4Rc@Y@6}t%|Wl)8<|JHy+%k3xmfimo|)r%tLM!c**X`KxpLDs+dUfPWf(|nL|}1n z@SoQR@K|1FjBY(1m`$^k!)R6+BMm+fM`L7UDJI{7YA~~`Y3f1#3PF!4qEr(K;|eu# zYa+8+;ToOA>ysuQ`^!$DNC(woS%JLN`h>v~#6ZSWnURnheM0vUi@7mZM<7mnAP zG>>aLW2vX1(u%Qr#&jsvFOplbkT{*IiyCG|vV6DU9IxZLm!A(7UX3RreLS3dHcoOB%==lGYL#ccNkcB@=gN#Bpxo(|B|ZS?V2SNED~Jb z6ZCAQq?m=gz9`Z)NfuxTDv1$0oA)ME*1Ni!@8ydky9$a>6}&Kj+n^o@FREicTt(U{ zpY7>gz5z>qsnJKh%-5}DzPl+N!fSYx7nw*Zgnl~>vTqEn~M3)gK)Nodu7#$Vmb`RGGw2e-3LX?Bu2Fv!x zhS=;Il7z=DqKf`V9M6D7IPPY@>tb zM$~-NX!~=L_n>MnTp1L&?y8xo_3pnh!Mos!xD#qFSbKsse!JIQnDwbv)f%DMApTqnJbyNxk~(^X+oy)86XB2^}fmL~`e{q`sV@AEkPHbyX2XVP>j=x80U8Y8k^ zlS9>P^-*kEL@+^dhDaZxMuW&6)$x#A_5?YaRr-ZJc->cEFil8p6^YfDnt&&yp*Vwt zp|Psk9kNu4NVY4K`g5q}6b{z!N*V*B)e~DvpBiR4wc%^zqAgZg0fI%@?MLmtg)VDc zUctpd`oFAY^m9z3%D2s-fDXW}*~-;GLSkl&Mqb=1qTLeH2j2*3@LknB`tvoS+wb}_ zs5!e`9hGg}6MqTd2C2%F% zaGGH2DwcRV=Vk*U6*tOe~Kbw$?K0C`&Y*c00dNSEx-bLQSsPDoDc< zh;t#BfXWXv34xQ2X=nEpYea1hHJAQurgP39c8Qvy_XxhINLr|XZ$T5i5Vt_YD9htG z^*L9FqNaIMc#(}8hM~f<0cVx6{V6=Q-K?tylPU&YK=6cJgpR*yji^pZNNdIUac$Dc zA&67xI*e?Wd%e{Z#RuSab-?Hawl*G`M?bYjmp7}n+AvDNco`9fb6)~8lSr3>MX@m&ip63jiw-C{u9E7oe*Mj^ zVJI^g1MhqaBuHBl8_P;Jisq87!gP(Rv2|g@)#&-e>pQ{TRbS~L+bEDtb>YE7#3G3L zWIQQN=Bug?F4{wlGA|GY4ITh+h18#{+fj0iCL(yys?hQnLM))|QH?Ih-PqMewRW2% zi!KDQW0mZrUbaRQJN;-?(_adRD-QS`Qk6NI=)~+}@Y35rWwwr@B+bVX2Ams)MiKX=R|uOJdPk(Bnu3ZwFW@ zCam)A$E*?gUfB&W@Bpta^n&to7FkkzS!!)HC98HvUB5Vu>vhLC$)Z3+n^zKu2bNg((_x^NS)UMY`&_q zjR{)dC3eQ(LLNM^t>kg@Xgv=$NPB7$RuRfYAgC+26fR0(e_nPVg|3gmySAkn(5p%l zb+DtKS+|4a#^fY|!FNWoGL#Asia~>7zp|Ln`l?-!c%7IJmzK@@oxP(q0(YmKz#Lb# zIQ9nNaz)mpf;1X1;B`Y=>NP7`bIO>mKn%E(kGn!ZqkgbNCf=|ZG@P&uGGIIX{ID8y z32Fja(hTN%vx=J!1Su8kx-DUniQ2+0Aarm|S zZv%gL^n26;H}}Bj&z{X)rfXTIo=+myo3pQOq-(jz4)?&cWz>VxwLFhxtq)G+_k0;_ zq*GaEu-#0ha$}9L3qX8hCcn)DxEHABX7XDLkHctqkxS(=l^CFCM)9!gF8glpGUuS% zAM~>b1XEyVhScF43=;8W5sbKC=FZ%SJFZX1UBI9}?RKA=00{vEHzYuMSPH@iDgFWz z1`=>CccIAxGnPRVhS7vP5Rg(025+x>d@xsY1p+p6HTSkBmuArlt~i>nLoYZ{Qu%gi z$@sUGLdjwCqc(VbSt_s~NsEYUXpU6%;;`8+wieNtr;yfS4Ch^{wU{tljc!#cfb;+{iwP>5C{fa7(?P*z1AExWPixfxk7sc`Q4Q})=-9^r5rX2L1CR~M+CVWAK<7Dp2h^hUMga< zYLCKlyQI%tZ=*KTg-6{yjcukhzx2cBB&7YTOk?;3v#rw@b+gAlY#Muz;xBL-!@#`z z(7d;~$8~{-NBy~d+qcxvb8k-#Z|H`re59fZC=dkK@~v25)CTv0r!lo?HnEOBs}$#p zX`#KKXEI7u1JhU#xn8g7_H#-uDln4)K43MAjD`(xzY=cr$wu3{?HOW^a*Miw=RH-9 zg&EqdE>w(8miP9e=@*`zeimP6pB|+idmi@syj^=U`}Aetf1s}ue&PyiQ3CRuuPV z#JzBv``4i_)DH;~e&`H4lZL}C?>W5=){`4WOu8)~7B4lyA=W@J8ArPf4)_KV>Ga|k z?1y@{*k|2P12r2T_nP^xQZOjU)T_GMs`SHPFsv1P!CaJxuoem^CfV);BokO5M#HrY ze_lo!L8CD0mTuFgG%efnlG&`~lO@2tX!=F|zf*0;+<9l}^pj5h>&Z`?eDz8Hgg$x6 z@$Z4N{VzQJ>|^x!AocQ{Kiqle9<;Z&``)8pKKk~f&pR@XKIQOF4}a?L>kh|<{NcxD zK9qS^=1VhvCYL!m`1OOg?S9+t8&k~QoAzG6_rcxr?qjL{4^#;Jn$+;%A0F5TxA*^I z|L692~^w^hcln(fN;`zvaAh{^Z#g zoYl{ev;EWepZ@dn7oGji**n6)-rKJ>sGq)HPVMf#!Whq+6LvX~STkR(X;Yuyn3?v@ ze*Wy|lM?Tq{p{J#-rg@CCEFR< zYlAJsn9(^=7ZUCQsTX{{j>d~#Jgwdv-DNZRc>nF z+1}aSHRGFlZuk5}=P$Zud=v5cN1uQ672_KXmVW^5_#`IY-}~*o-%iRoz4u#t zzm=4DZ|^tvelscY?%oIXK5*R`H#KnYH}-zxnz6-IY%^%_HMhk!8zaTo=(xiap2Cw7 zXHr-SOGTQ> zIa<#XBHAc|w0@QRMmHbtf?rbNqutbQDk<^d?#}K`QsPYNFH?V+l=vX^7pcEUO1z)? zAF2P5lsKLG^VFXwCEiQ@_tbw+O1zu;v(%qm4d9J~lKRuspI$Sb$#afYoI!Eyo3jz! z9c~QZVS1LHB_+0x?!-OV?3 zU3!onTr-|TvJHd)KQHW2}~ktjg=0tgDdb%0{D-8(pON{)%pWEtepTiVGJlYin9BTq=J%5l=Q6*S`MspXWaf7> zznhep$ox*`cajq0nGa^JC(IiM1VVR_1!bypb5m{AT8Q!n~0f&U_&A z0kANC9r)j0%kQf5DxL1Mx?1=?(@-ZM&$6{S!5-ZDm|Gu{lz9Kv%Wu6rDRKJN%Wl0a zDe>N|m)^R&sJlt$-CHlY^^)sp@FwD0FTQo{v6ZgGxK5YpXj*l}vt4De;jw#}cBXx; z@=e6MnJ5!oYjG1X$nhRtGrn=Lc<=`Y*A82`iFp6u_YbZe_D16L!S5YhJM4|bdk4RJ zaP6=+67L@T&cU_AR&H8mADk=a%5`ttM7)14pUX*!)92E;l$3byyl`GfO1yh6o{Nw6 z|C=8R9_|0#Za9c1ugc{h=+XZFUSB-g|9j7|j7R(b=XA!S{r_`2AUeI{7|9@`K zc(nh2uAe;G|KIbINBjTh`pKjH{{y}tT<>dc+8#gJ|35d#Jlg-?3o?)P|IZCFm;3*- z<8R%$o%)I$aMu4ZM?ZV?%+YHP|K#vH55vPx%)CGI=8Txxx%G})!&{$r{sC|vUphZL z`{A?28Fc#l2k$r-9(>yV2ln5xf0+K^{Ze|7{)D~v?S1i{u=|(0Z{Hp4eroDBQeTnM zQkk81?aWWV{?tDGC~&{v^G z2H|8feAVNR`wO=>?E0_t2EE7EcQao(><8Ra>{bPn83G9mA-|0ek!%17Fo5({KDk5# zNaVPum~DK>WQIus!+gy)KE&p2)CU^^fpbqixs^|{Ktg~(^2Am?$qb4Bf#lU&`6L^6 z0*w1-Y~zC`8+ROx`{UdAV9ELi1O4;Lt$dR84+i=tyOqzT!##N1F%W1!eJh`2c8CEx zyuFPNn#?ay;1_>yE1zTm5(NSB?{4LjEI^_lKz`a*KFP)%1>^pyTlpj#FBFW|r)=eu z%r8*j7k_6PA0*j$Az-{7+sY?dKO>-@KY1&kWc`d_J3G6teaKqr(O*C6fg5{(d+Jr& z><~{De?ZEer>L!blG!s3;zn*OpJej|4(1DTE1zWX2M6(o*vcna{J}x|!ME~BHt*nI z-odu)xs^Hp$@4dX8wcm$#{8F@mCs&!`pc(p0ekn4JNdxL*PVDLpL+be$KQM$ z9cPa|bo9@U21n@OA02+z;q35LnLo{ZU*?S&II_c3tq z3V+Fs!Z$7Gp74VgQPk5fzESvnrhFxwdiq5-VmF1~Q~aZERD9E9Hx&P<8-;H&xF`J9 zg>W#~4=K{aVl$fo_Br^&kN+X+ z>C+pVf1j(~9k-Jkv761m?mFt};~TM?n%~17-H6@nwQKBap3ba$JoWG)+w{Vl@N4W7Pp5A(w)?(D+*5q7 z`ozR=-tdGKY8bkm%YBl-0u6M z?WsF&xDorX=msXLt;v75rLu}|J<->CSeligE1^HP_( z6Wu8MVIhXP6W)m36n?Gv6L*5O;tyYX;hP?QPw_qL$vdqZ8@}me_wXmL_*LqTf1~o7 z3S28s-D%#4-4uR}ed3OHW5YK+{GQ?$;i++B!#92G9{%WWy@a~s-l+Vh0@un@cRu?@ z?56N*>=SqDH#PjeUHd)7FGl>cZfZET>0XajSb)Q?|X`0G`x0W!#7tD_wdIb|2pc9bEEQ`46c>G$p7!8&USA7(yed1^|`ky zxBl+=U!4EK`L~|G@mxRuRPeh0Z)N^d=EpN%l^JB%%*XD&dDq{4)$Xm-2haZe?B~wD z8k^lX@bUD$ZfAF+BDJe)i zkdzdp9Y{(FvJPC6-tc1Y;=$e(>ConuljS_DH0ai3S`@VNz1?;xH-c2QnW>O8Og_-$+XO>zQ9qO8RS=UrS2*tC?R-O8P6AUr9>( z?=$~CDd{g~emN=W`!nxPO8UOc`;wBrH}l@4q`#E;rKF_4n7JE@Z;FJ!kokqATz@`u z9koIIg8`@{CEd-k6?F>3!@=I4@<{%q#%#Cekq{JYG*OUm_UGKuHdpU(Vr zQm#Lh`KhF&KbiT-q@@2g^KX-qz9;jZq@+KQ`H7^Y|0eTql9K-G%)d@b`s0}&PfGgk z%)674{#fS6l9K*t=0}r~{z&FWl9K*#=7*D#zAN*tq@+KT`Jtqw@65b2Dd`VpelRKN z4`hBIDe3oTzCS7H_hr5>Dd{^h??_7e_RQOplK!jAze-B_y_xS#O8PyS?@3Df-I?!B zO8Q-y?@CJgw#?g-l746AJFiGv&croCh9AVm7N`YY?QP74pvqf6DQWMZmz1<~&`C-f z9YjeXm@WU&rsMqi4zoe)&^hzw?+QfXt^k zA2MRcnkdeMsv(p!TPao(lw81?g{V5JM4nd%g*{#Y>avZ7t4Q>#wQ0ZzZq(lB{oX4t zQbNCSoo@Ca7XtA6zHUBKy3plp=dr>_nowRii^Mr1O|oG~!|kwvxXr1q#JOTDRv-+j zERCi*NUX~`K{jZxcGY0ttOee9405V9lwNKQiuyQJloslKZ`}91N?c-EGf=cnua~)3 z)LgAIsT8Wd9^~ysYuXTBNP{=Oi1wZ-0$pAM8XT(@HAS)d6n5oaZD1Egt3-lAwrrJG z-8OIdShl((+9ZdoiFFC17^L4`)@M;1HKJ}bS@xb$ujElNzj9D=$%6uNuGL-!4tj z-;IHLh(OduJeY@tZXU~rbA$ z!vJ{~o-zTYg`1K;uSn#I2W8cVtr@7&Q)wxN5RZCoiSg^xy6#w=RVT5$=Xy&1st5c1 zX4|R=B}dMwb1hcJEY|B}3zL#Pk62Rf^faMH73@*W3=+%P@Dv(|y6_Z>&0D+`X*0gC zYNGW~pLeNxId9`~JYI!EP+`bzgevAyX!%xf=n0o}VAGt0)f#?(=qE9r`= zz^Ip1dDe!TzQs~VwUjS~@p9N9E+(~P!+FQC`#BPPf1|mkVp8ht2-HlJ^qJ`*6>E^;OF#g;u(Gq}G}*$axjfYuSl0 znGQjbU2;TQkwUh5TrcoAZ<<`v{VnsK`3xZM!ZTsE2ErdG{YrMBQ8gYJT&3TJRTZVk zRijj91-U^9by8cBT;i5J*E3&r)q^c*Y_+Lbho6AyxILo%MkCABn*LCO%-m3lh=Ix! zT7{mVJBj6NcqRozU3exPF2z#LpsV44wT9McUYkzPz6B44#cDvn9lkv&4VfIqTAjqA zw$5SqouRtVdsnuqAIZ6AxScfOAkuuN|q-J)*xA9DEQ;#hI-dC z$*TtI>XHxoP&PN?p|YEu%R-|PlPp<83^>SBoQUKq2rM;RIKH`BR3gvZJbUduLjYYa zJhNRG6?~WK+fBot=Jd9Yj|Y0o_xhY5%!zh$IzoKjlA;1vPOQuQ_8UBd19=yo2}L%j z^U6+{J|x!rpzT+^4=O}{AvB>XgBZoRt=4;uQK`vEl`+aTE314b z=!Kk8a1fWh*? zU$B?@$ogx-`dTphpfV)S!|pF?;5;9Lxuky{#{S?kB+nz@Jg5xGbEDXWLYL*bqUJ>< zyPFOA0&sKTp-u0)Ks~p9-iN6G}OoM-^){vJrA3@*+`PQAM$^s`#q2O_d)J=|J43BdQg6uPgJ0bx@bCZ#Lu5R`>G2^{72K&M|O3W)DhwTBOUSi zOw4u=pDNO)jW)UCk|712;Bbu-{Q2bH?L>;_DSOC)}2Zs#QfBwA>%v z={!qa_}v$KsmGp&F@5AFK?`L?pu+*>Jvd(=8FQem)LsKZ@JU%1n_{s-`%aP`-V@t`s{I4)EU3{*Q>w! zuy$XvyZ3JooLhmwdlX>FJLpFsM_k?QM(&03&*i;N-gNnw7do!>*-&!5627PSCBETa zl-#}iV<+-D*MA06y6auE?($!v{w$hY)TqAffa_@<1%m$i!VLkYx`&3FUKR6w9J1zw zK2ep&=>o&F=mEkZ)NuMZ;W=_ISsl*H zrh{;EvHyQ^=ZAJ~{m89<0N($nLEXL22Q|YE&)#$PrZf5M^z^4szvxs0x%+TKJ)0~4u9?NYY*!nTi|bIz9Hjf9s@-azU3eQnF0S`|J%TA z{!dT;$MkoAax~E1f8P79y?77X{jaQ1qgCSNV7weADV2qpFz|Md0tk$J^kWR#(a!Y^B1~ zzyHC52Ks$I6OyKb-zzG*f^U2tf*WOK?jyuS^; zX*Qh0NovhBq`My1+Xmk>8$&l7tIRTG^ICd$3;e#f@f=$z@(io1cdf3o4ZdkM`AS(- zIG!)vmEPV4-!vP=q*+5Sq>6B@e6$U|X*Q}3inD1oy>fTp!fo))V8v)Ot?LZO+;y2? z8+_C1SjUk~O9RX7Yvo(p;G0$_o1&IC>~dAThWp##n^vbOQq`7frhT{Tn%m%;RwpWw z?i9iODDGOiw++5&btTSGtAeJBcgMZ41%BU~X(nG$cKY3Mf893t=D3$smUC+0BEwyy{mgCf&2bkMlcg2DQoS24YFps<-8ZN( zv|f}{KCfN3-Ps1;9Cw4S>D5Bb7Vo-@y$!xO?iIacY86$}?wXCY4Zay|s+Ll#@HI)k zYxC7@@XctGR|V0m8Qb8S zR!2K|vn*JAA%87h-v-~bI@-1^Q?KdL-AG&B2H&(gh81~MGxSpJT6$><9NkR225uFx zl4OAO(KTG#2H&(gM#?*-GOgwB`g?I3eADWLimWiA>Ew&o($#J7O{=4oYMC=6UA)_M z$~O3>)oG4il0d-X?}iI`8+_C1WCd&~SzeUyTAj2FzG-ze0nCu5Y?*i0Hid2QO{+6a zn=6||M!dVK6t}@Qtu8OAYNf#0R{6T;g>CRntE;hPt7zl}?yk-ATj0p1)rp+R$h2Z> zcL$E!2H&(gQ=(!)tL1Tg(_U^McpS|wv(@y{R^oLKs0AvAt?BqY4{NTwmCzX>=JpRz}JC2_|E**c|(eEF9 z&(Y@{DMuf3_(4!P;ByYe!5>fl!ozV%>o@Y;jZ{rB&G)BbS()u3L$ zFQ&gf-A(7x>Aj!bd+T1fhwbg`{?zVUcAL9U>d#X@k@~XKXQw`W=TCrIiGJ{#cK7_< z$0p^8O}QEoG%%&X4TU+9Z@a{p=A_w-tV5%*8Wk4PoDjDQ>9@jfN?l+WQ(`gNS~Z7u z%`IugroyzcWv4Nw#yv&x@~viRIj;qhRl(9<4ZmsU0%KBTsTEgnxL_(sw#gG6o8ug% zg!HDe2S+v;Mt(1@29s_s{Z;U@pL>D9nPr#MtQMXpRE2L9=VZhqWQoPb_-aXaAk-e# zR(U-eigfxb;b;H;1qKOPt-{Kpvmq|ny^ ziqH_zr?UyuB1Ynz?XUP=A^qj>v#Se?J9dkq2`+Y7F}5lPjD~`R9=Lkyd%nI9=dG#L zWnt5jb8{&DPvB>l7nuBvC*Z!P`lZ#BQ}jR-m8fGN*ubyKB`Z+NsA^LJT;Rz<`pe*F z7Z;d-Y|XkY9-;gRvr=hn*>;>6qxbm|VUM6yoo9)1XIUCWRWtpM;b-R;7&s@?o9sM- z2FA!nS(o#mTu=$<)e0Q-6&g%dF4J6dZq|G({iX1;vkOev^JT*r=*GCz8JpOuq2$|C zgXLH4eg*3lLd1jTysrhNsGR;1_}S?NMlTp+q%bPm|C7Bp0dpj)&O|e_l$A?mRe>@4 z(p`{%AxToPX=&Sl6stl~ND76PP}2x06p9E*q0k|rAM}ulB9Rjh)8!vG!x4kFkI7WrF_hy8CW!_LYf*As?g|m1A@Ue zv>r$Isd5;os0BpEjTsmwG3K_){1$V_9rpb;1XGohTN!5vr5A> z_#x?g7c}cyG&m2nT12@u70cK_!a1|mMWuWuHP*{0tB_53)L>Boj}2MB@14-BW6=b{ zA*3uNv8X>og0%5UIbD~7@p?Fi6zNfwB9jU}3hU*uDkOdHfM$k8Q)-9N{)9{gdq}oQ zPQ+=QrMuN(5re0QQZG%b6{^WJwJ}SZzPCfOwnY<(K~PRa8XcKL4Kk~=GZY0IkkqRs z+YmLNffSL^z+}V#>iZFBrdu?sKnzpKNFy1_?CQ*@8;oE@kSfQ;>MWs^60kXym42&K znzoV_P0OM&+Bt^m3s5$U5!AS^cj9$3L>O44LQ8SM*hNcmHJHYO6dm!+pjp$Rk-#<{ zr&1`AFzd+)J!nN*42*OHG8+htkx?zyXYmk~&qsOEcL2>A7EN|H)JRovMQH%hn((f_ zhDgIfs~|&qA}qlv#h$C zruwNk#$=!l7bG$Xo~TiSQiq2~u@m#X37XaH`DB6QM_CjfP5AUM)y)H^l*ltwmm^fP zo=WtLdNi-{Swb>>`_N3XXxI*j`!OYWz-B^yIBMpwaw0vRWF#R`EaoS@U8UJ??slhq zL-D;4npG_t57*Otk3h4EMdRTbneSm}_IiuP!>usiL(r^j(WJ6WU^gG8DO6{7X|sri z8?otNw<6_t(@IkgU}Y^qBQn_=#(ZypX0NkoJly!QXdbX=65(k-J|X=RtUJhvQnwS1 zrW)!vlkH8JRJ#?2^r?Y|>){AZ`W}R4ueE4A-1M?&?zd>Vb$y~orYh=hbWE%=HcFVE z&Ps+_5k{FR2u&5bM!;zK1A5Xog=Qs-#={LSi$=7^aeqt8H-TnFi^juEE8iHJ6)YML zx2Ak1G|SuLxWE478$q+2MdRU`l5Ysj1dC=$Mhjt$lH-k@z%T{8-d zAVisL^XxdvG$XzNG|O5v9&RdGG`vOA3pL8MAn<}PA+0RviEb;@5>Yt@#o1CiJ?I+2 zDvt)jolv#!>qE1QMN@)mA)_iHNSDaVVlzkNTmD`-5;F5L&XUmqRz=n55U<2*s6~^u zXgu6pvS?BkjfXo(z8*B=EE*5@h(*jO}_MU$wM znsK_(tLS*%U+6}#;1rg|Mkid3iD5C#RIyMho6Qv_6Vlg&W~4>q;cksZLs&E(Zpru> z&(V{luq0v)5QO9d@@f7ELJv4;3y|h;(_Q zQKFN%RH@AnkXCK0^$?|yjkuJ_GqjK-b3PfGy~?8Ta8tyh`C*GDEXpG|s;4EUBAbI$ z0o>WkP4W_8Z`SMi^jt(5R2vqEt*U+X^vWW98OPy3OvAs z8eQd#P=|_eGR&p9NwpoQgc2n!ZTci=cArJ#;r53`6SinPq`>;B&@5!pc*q6yRW`4` zb1As=!KE9m8++IDH`j0e`qBrt{>P2aZvEQUBU|LPFJJq=K^=h5=HG38eDnUz7i@fC z;{zL&jTfx{_4=dhFJAjHi1|0ye&FhVx%#fFnXAuP{iD^7tj0j~zMuAueBmqKxbjO^ z9suIp^wpd1x*2!zTF_`sM`gFL1OjhCw^!Z+`?J81)hb}L6gokIyt;qG-K)+r2&rS(ky-Y z0f6Uz_r!O)>2WH+y!5I2R>3tI_uL=P((5n%MsyY2WpYn?Itys&Hv?d99$3t8jx2rh z)vKAX7qa8oMN7XsoFuv5GC+coYV-&9g+hM+(r+DVHQChi!f==(WsQmy z2RU)^LOH6{<)HC?rCfg2WpY36#Dv4k&puo;F0tQUGt1w9xMp0Eo?bJ{ zPxmPHqqi%TpK-Y0T#i3qaLZ3U9Fx^r*={>CN+?D#+vO{T^Hsh1&tC@?`2%3Jxg@@m zsspFGjBI|)*{?jZzr9}__3zL3tDihnuuH_Z6}XivOg6oTK@e*t-C~jTkFezc&K%kq^GsM{O=C6?vjPG)&1V#+3J?;_}RMr2ZyW1 zCH=xxv;0TSs^NO$%y6oi*}}BX42!WAeg3NP{hgy$x5Rf^HHiP@s_}i@QL9__x7F(V zrlVH3q^H&D`yU=9bM*N0eZyH3ZpWXm3EzMJLF@T-`4*=Zl$0P&LG=tXGUhWQ7h9|L zTI=!EJ?$NpaWW*ShOMVp_snmrq7ktkUfolk)~`HP1NcFBGYYV-dEuSGxZ;??L${L107uUFC}9dERAL{6xTrbg@h4dKe}q0n6t z-)RlPr#6HuuQGEYLegTj9;+>u&1n`_ zsl&s6^Xt*IKU^cP{@GP-^)FXPt5Tg^Rb>f=K_RxKRsBw@6;~~~~UvfR((Us;U zvb4W-kOHH<^Z_v1e{?_E282a}b-uTk`#fZwH=-A=Lv03~BF>f8u(`BRiyr5M$$j21D69*1vZ>)Uh4Q zLu?Y5E(J!jcdS2lJ=&djtdp6GUdle!4oC}*wPSt3H5_Nh@(?No(_Q8wdEI&Oh%O&e2yCFEP^2K@v22{GRI} zkC=~#$ZX@_HHV`7UDrb$GanBjU9gT`4Mwxg=ij;>?au4yu{#I%&1JFV`%Tw_Es%wWD7bnM21DBB{Tr@_JYwD+g6EBc(4i=Q-Stq% z%-ciM9jx;p7|k~CPrDxNj^=%;nxmJ9e(fM&!LiKyQ?B7S=ItQ~08AS@oc6DsU#H?i zH66Vq3jp&*0Uq1Df5kPP1xwmP;((Vxe&YZE@NDz`gll-m%-chzf%X2syY%x*H-G9T zf8&4N_{kf|>)*Kk(d*3Cf7|-d7Jluk*WQ2aRhxga`JT=2#$Ro`eFFve{olI2xAuj# z{k4~0{ruH8Tz&EC=T^%#ru#gib+RGaNzfrqJcyc%9Ic7>T@vvuK| z@Zy6IW_6qGU${W)A@5h1$u?UTt_m++5Mfrc+5VXev>vjDg_&ftb>X=1;u8^ORhz8~ zhl3YahA^wxY+X1Nyrd}$v)9{fT{sZDWHJl0vdz|or@+fgzc729&DO;mzn6SyVfKK{ z_K(hg6Y`QWEzDkPvvuJ&@ZtgxX7}4{|Ih_m4~f^ptYowO0~c&PUF(;u{cVg3Z>2W57$c zxiHJxY=7tc7n1FQ`-F61#@lRNxEH*53WQn4X8TzeXg#F03$wJ%)`j!IOLqxjma^G? z#syjr$??LBv)Q`vMR>`d7iLMDtqZS%7w3sEOW15(cow{5;S004&DMn%!AqjPFk@}D zF5CrP-u#7G%x3H2wcks+zc6ELwx2xzwbjdw0AWVkY+X1Hy!Zix8D+EmRTpSI+$#`f zq|MfaYr)Ia17SwkY=7AWS`W7qtoMK4a(roPy8bh(F9HAf-p@rP@V3J2HCE-;cmCkA zmt}aqK2KCkwklTk2R*r>myKRc?rCiWl+A=-D7dI{tA`r0cI8&Dm0msEHd$Qh)q`m* zDqTKzk3K;)UQd?(u!cF>GhjY=U)4H51242((AGC42BSgitMi(w z^FVRKFjh?0egaGOBI}{d>`DbNNvvvep?G3Ej zuMigW`%wt<`$K3Ld}U!#=32eNwk4D67*jpeX>fgY2oEB~I>ih_F~6+Ex)l{^hTBQx z;>FNqCL}bPd~YX@r-FXaq>I>7dh!mfYa|y_+he>Ua4bbgf#zTjGLRZ+h*+ONLr_YO z=-d!vyOI8=z|`5=qzy^n%6K?Fie_H;kX>oF-Pe>hC@yam>a8g9f~i|w34F6xugjVJ zD{t>Ed%0ug*|x3j4})>LJa2sg!2wG*3ynSSWEi{Gs?7G{eY|k-{V()#;4L=xejg~w z63Ua%Aw=&hx64cqP*kG}05FdB?8a0BOrD#wumi!c%5n36^kB#w&yFNY6 z)O#{`!~uO;%eG7{OkP5qz~-;KZDsbu^D|Xn_Hv*4LdJabOvTPTQ_=5A>-G@sU1Z%l zXtw+0ovDY%r_7*~hLC>KzJFm~}L+r2g~rg39vLfzuDruHTiY6>o-CqpWiiB!T7ybfxd zHTSYXd6Fkm8cQ`*mSPAl8B?KHHz8+@>PYAWa@rm)iWMl8Zlw%&?MA}Z)SdeO_bgSG zZa#A3TQ?p8`Tq}Y{r%SD+BdJ6o8QCzy7!D-L&oclIIeTsZJkwr+5b7A$`vcfSaBHT(Syo*Z@2*y=)a+_YB~YRj_3P@X!hTvd!?C4S;oD5y*@G z{rr%|!;fux2H+vf%?*GJ&j37R7rFtkZUgX=mg=EEkQ)GNo&j943a)wvaLFoIwE=iY z!1WT>aiapCX8@P1f-9Z@T(SyQYych-k}q2Y%bo#TvI>?w1Grojy!R0sfQLlwOIE>q zANCC3a#ir&hdcwgWEH%@2H=6|Wvk#p&j2o21ydVRle0GOTuc!+1Z0Wh)wc**4V5Cd@oVCWgZrK(^a|Nq$1)@WmP_2%-& z&VG^q-X~9n1VB4j;;S(IK z`R!m&arj<4SRnB4Tsv6I9|r$D%N-nxDn)e=VLQ!fGv5z1!!axv4)iz@t726g)`k-( zO2lP#7@jxGzDQUnW`bqbO;?M1BbMnA!O>o$q=lqDxz`;N)dm)drS@PJ4ls(0_@PKx z-`Oh!a>+m|CylghPMN}NirCR|!JR}2O@+*Q*Uv@VB|2DB2kzjvj%K*E-MoFW?8Rs= zZ@XF8ecH=*^8irbsqMongWF+#E6w1Qi*t;=LmS7plVcRJcA-1w7+GP=)A5vJfl@C! z-o*kjm_HZ_of(J;)CO##P?3x-8n284VIqyRci2!*NldhGaTJo`^iCrdH@k_8_kC}M zOmozaiyF*!p=x>u!f7dkk`qmAW_h|1u9FHk5wj!Eu4Tl7OuKxH>Kocl($A8*AXC#^ zP}v*p>|l*4oe7MCCe}%lxSQ05`P6~sy+xZvEz6sgmoN7L%T!=BKT7n1;cgP6iB{SK+J8y@0<;h=sHYwExf zH#gtER9NX=xmmt>?~T9Q{P2w*zwwqEjT`ihm*05W%FkZ^v+JL_{;}(CzW(~_x2|t) zeRk_-wjSH+Y-Kk)o6zQSHvVMemp6U_^!}4JUbC^h{+0FrYyDm8{q^)Zvi_8{-&_0m z+JkGaTf^7xUAuDi(^r4`Y8&(j2wZ*I>ep6(XZ2sKHdlq!r~AI?`-1Niz7P38Lv`N^ ze9Kq91nLStdWF35%9a0bE3oxkkcaTEuT8H>*S4>%fsTWpx%q21-?aI2E8P`gCA{*C zLG0vibhycOCuH_x}B4O5jziE1(R! z@564&d<0HbR`q?M^Bax^JN|Kce)`z)mq#4qt5`~udn#t;gHXte0=Y5xW3&L z*ORaLzIQw1KXNhhcfD|R1vG&0%^dR6Ns_*IK4*0Ww3qN5IA6J4E64Ya=dG@QrWn4r zIUgPMl=$B6AbYDTWGAm2_+(ygs(?EzY#x=zM-`+K)QZe#H53{swl&YtX5Rn!YzX z(|-7HZ0l)M_PxpB`j9KGr&j*HBab&YsD7S0-nFNEY$+~*Jv4gnpkRPq2 z<1-y$^qdbDNPXl8qw9R-h%kl@>yGo$F<}fG*ucMV#K~~hDmz)m&DVE?(RRokVe}lX zx*M*in#lXQ&U{-A`GWa&oJlpEuN;$?;m|kErf)m)(wwgxk(chUR-KQI$*bkeOmRNE zPGXEcQ$ba-=HPT^%!~sw;e5DYnrR1S-1*88m??)f>wI(!Cg&(1 z=6rZ(wVtw*q(e@-AxBPD4fVwxa_VB_tRt60@&$50&qV*!A^*U|$UkDm z|2LOHp#R_27p@T-KfC_Cs|UWXUwQrVZ-YPH;pfdyd6RGbfk&lV_mHb+JhKE&DkDim z#v(i(i7@4_6vD$8ZU4Jpp~KS}VW24@V&aX6nH0EAiOvsebW5)k@{<_ok1b~m6SSx| zMYz?B(Y01pi-cnY83#P4q7J@~MoC}VCK zF%v=zP77xhUH2c=5|wzAPb6-CdFi)PVr0*f`hO0ga1Hjx!1aP=Uz@8+;?B3KWR(% z-Ny(*de8q#6kXQe`Rm>P0(5fa9G#qcr@f1G0;PmY(g{rw#b$FLAv`Myt=+UR(DG2m z*53)8$h~PI&J^)vAMH_U8}%oZHms6CLonbbgSWsHbJ~=dN*1g?JSrsO0zs4_u60s* z4CrL#Je{2M2;5aVxhy-OFh*0qzIlI@#yw{sNs4yJlXKm|&=o86*016sF3;G@n=02p{e>$ckDI5`_vY zG5&^PL`rFj!V?7{?ph~%9|k&EI!`AjpV{szom`fk;7K!^gGN%L)66uDE=SZlNq=sU zP9nIZ$ikQ!wF3JQ+6I{3$Vl@+qv;?FaFNHd2yG%wvn@pAz^Hd^77G3Ma_xU$J zW&e@$baL{E_ioZj^0MrNN(S>ZmCqv0Mjlq6xE=~*HFSYaa1fvwiCB_~rpKu=3I_+B zW^TYWcU8St2=k?^W<+{AT1-zvijl{;9FwBBc-E~>KK-d50y=s4Je{0;NW80b(zv9( zi&1rf=95JT%jKF-N!Hm^f>ajhgb0P|p<+hINQeNB1j$r(oM_@>v#ON>&{T-UN?1xl zcX0?QDF&b8i>X{8#nY~J@<~6?$wTMq%bk~l&zt&kqG1q!a{0hJ3@Od}1R$;A?Qq+?y{$<@zX zy=Qgm`$HdejgTw0c2ompDiSYKXUePDgnOVw(}b#dA` z0&bR{Qzm^Cy!n3mj~}&u|LNbK;fKIZo`?I}v->T)d$;lWyp+Rs@ZR=dosZLJA^hE6 zwSNEpuerwgl3B^ZiQL9{#Y=r{2j@|D+WAPYSQtP1P3!mXe8x4>duO7B^rg3vu6QZg zeav}vKE@Rb-EV!#`u!QrHO3driZ_=w9E=;*D)mzS+krUuhMo`6!uF|GTfaa3H?9%h zniVW5zU;OXH@%b+e_V=oK^K7WoIYfm7QA2lu>JdEXMsFuRy+&i$%m``TeI8=L9TCmsB-=|MwfzIx8VJY|7`#MyR$%^66CF! za02AowuhSOUO}!|@P2Y?|9;pt$d}Br7RV!8&{Z!L*d35Zg9Yd7{HlfVpMJvn{m^T_oP%`XA#{!%*@w{kMGNl-zTq10_AF!JJ$49rC=uh}J$4A4gVR2Q z-edp%(VuXQ^Ch$Ni4}0gOZj;R=ZQn;9HjOkG_$w)cl?HHr1#EJ7SdyfkcWCF4$^sm z>HJk{qkHpzvb6r_zj2N61vBpCA>^UXiv#iTL+BhaTG$@^OY8ST?h)RaB`ql)IfSlx zskHyN6z6;AxtCQbS_5wpp<{j!l*p; zEQ}{(hu{#3pAh86wudGFk7INxNc*IG{U2K(E1x_I^r0zJ)Xexkmbm8GSiVw4^6_BEOX5@-t8G>;XQU}dZ;4e;5~9^ zo-0@TxO~+IEV;hsw_W3W$&5U)0yj3{n}jYSIm87=ov43Y}_bv|UXD2YPAP9rd$AF`f*IMQ)GU;Uc|U zsNt&q>U`?fh5H9*v~}(G9aomUG)KCS5iYd3g*A`wdYki5yls)q9euR2?8v@FJny-1 zzGN@Q@Qy(Mbw#8~`*i;R>rVU=cM%!F0tg01Sa|&rN#&6STkj?GY7}mn_+Fi7vluZ} zcJzFDkA`Fn#bcxTUM7HE*!;vmUKbR>k5lWX*L<< zi-Hp9jYT1=hjC?EVAE;1jwTbOw$SP6!9qiky0JDLZp2#dZr$5cw=N>D&M4dbo(cMT zBqE7c6fV_GaDhJ$^QGNEb};Nnyd3OC3R)sq*GXNGNNFgYzerBpLq4;-4zlRl@m+6z z9v-SVOEl>ymEGEIX`PCDYC#3s@lM{3y&7Ax-YT&u5-)k468@^EsRc2^MqvZijGgM%4q8RFYP zK>!b{>+%c{{;o7cFFVBpU=5!r-EsRx_(?Lr#{pQSwK~IUOPg0uI&6W3KCyNOT3nr3 zc1Gwl2RIy46DBGZ>MCUDRY{*3_@oMVqG8zFDK;j30U`Iqi$8y+b(o|BEeuzgsUYdh zL`&}I!MZAB^K42TQ(C>h(~p{6lry@MMmIjmwvp*LfyStOpoA%m-$2x%M5;k9r*joD z8e*zO#B|Y72ZOAs1G}@{|96*ue(B~<-Q;il&l^8^BYFKB*FSol+4^r=AKJpNef8S= zuf1yXZ#Lhv8Q%D-jkj;0>wmib*7d!$FRbmaz5MFuufE~xi&sClYOdbv`;4#Wd+wD_ zU(v5TW93&?)Rm_$e|%Y5-U6&I|3{cT!|sLnW4F?oQc}nZGA?$LC9}zl3MY!UZTs^> z1BFYmaw3k7$}FmP#%YW#aQ+U=4@U#M9_TVA(VkLzOqf00X8V2@Y%9GAr{<~^sL)5J z83j)vAh~k47?&lPA2VisSZEPYt`HBIeqr|gHrw~PU>hA#0jMn{<@9t&CU}Km5?V70 zqlTQ1r$H}>43aE{X(&bo0ote8Y#(#MRteTetvUsVR5T#nQS}n#CM!OV0DO)H+murNn`HgJ1h!7Q0@`n^b ziA<+?4UJXt-FPNK*34WB}9!3sJcXw^llmv>CT8| zVtE6GYg(<82SRv?&GtPm*rp0;YD}m?quUJ#2HVhj9B5_L>S~?3%(tgmKaH?O85-kt zLzvyP*}mHa+jKfchFAkfbLxbUYCJIvH3MT)FB)}ZES1K)RD>?~G%m;hv^Q+F?{dL5 zuY=dgS_@8%dM$$%qP1dPru|7wHMm5!*zGr})2z}k4Sq-pv+Fk7ce-GU^H8frlv`7= zj1455Gh1C$%4bqzy^OL7*_1~O78UT=kOk{@%Vzry7iO1Yaluv9Bb8E!W? zR?1_{h^L0?F4gLbL`SLiMq=FPjCT3pC?w2$Hrq#UQ4z7*nQDxjFAWu(u4s*u3rt7A z**VEgB%TNh^=gh}hb6wkcheDbst(eTGR0SoRwdVu0OP%4vwgD*w((eVII1Aya;4OQ z_|_;EtII;O5$;CAkrI+grnI)1O!EVlmW0`g&Gt<$*y`*wj^SdWoJnU{bvF%KS;jFY z19i9{kxB4GjT)3XJVc5efOgqtyYGVSFbh`XD2w8w37;OOy7_D=De?>jE+439J(cJg z^=Mw@vxEe!Y{_Q(Mi*?^P9CW;C3wJQLVY-D=CE=iJ)UGFAyF*mC%s*z*>CQ4r+h;Z z4j!@DKH`F{NSC7ZknGR)#lj#V4#FikERLp$Lahrn;aW_~cLhz7%4{(y96W5Zeb@!t zSeG4yC3U1RCd>|YRW$-N)GA!54f#Yr#Pu2Artk}sY%OUB2M^h79|B1N=j11D%ej&w z^?Fi6tS0o3SsHO-9svh}toPcIB=pmGeH6!JzMLV2gE!c0-{696KMC$nB4~LCRpNTd zC~{089ZZAPig_woX;4`i-OWz|jUb5uAv|caeb5ElaG#orNN_^+;A*ANmVj=Eg819KAauiAHH4H%rq?Vd{RNe62Q;pT_03>I zNco3y)*liMCN|rN3$|TgO6?HLP2@p6WcK(Vq$9C5DbV9^Gn)6yfl-sm(>+XW_=SV9 z&35d9t=UM2>8u`#wuW4~Hc&N_YmT@&t-w7fIVto)*$NVpKu^sIKx^7;O&4s#lLm(A zQzb3bjqbRR84k(=Ef^iwWtcCM={S?mw2K40(vnT#U}Uo$xnOImu~rm~s+l2&A<^A9 zn$mF{ZwGh%dY@{h^YA1}_KSH^NdmM(o9)mA+h&~rWi3-xG&|s!9*!FmGBK#{?$T^0 z-Wr4}s942^HMH0%1LGaoYzKhtISDCV5(R~WzRkApf^DHPsHwsTPBs`WlgGgmNE|CF z5h&4)*6UJCl(#)t13H@!s%XG*Kn;@8~alE9-RE#c$ zB52=e;>8rLgWlokD%OqXTgXs^hCDaKVSbO4Nl=@n*iE0XZe zA6)iw8hACtpdW~Nq1`R4B<#gee-XR$5HqpO=2RAohs@N)c4wv1K0#50lc}}VFh_f9 z%u}}es@Ac>2v$tN`bJIbgC5nBc14%1KU)O6EABWS`0Ir~BIMsz(LMPgo=0qA6r@Y>uqP#zspeT-C_8r*4_HZ^q6K+3vEJRBErb9k9anM-Exo zUL*^RJ@{Q2HuUf~yV%(6ob)Hi+E3=FpS1SJ580mVvTC@c&eH~bQ9JY_=$TnFel)^K zQ?bp~;)7%)I|`JNp>DjEOpZ|$fkSk=j%IRVjVt#07Y^3%^|Kwb5Kg7KQmaZAC!rLU zOEksSG$o^AD^<%WrCz_-AZwiLhp2E*kCK#%q;$#1wwNHTWDP2d^q_PGE5`EZ4vS2& zth=BgY)##=LwXjokJ#rfd$}xdAv0WPb%F0ntMia#yU6MmB&&dR{UoHWEdb&iyK>E5 z)rZl0tG51WyF1og#xm{;8X^=5hR-|@8{_7lR8ngVtTrg^O&fL0ZzQ{NY&?jK(#`0| z945#}$t?7D{1-MvrnWO3F^P6z+!+S6Vl&aNfhuk!Y(P|no)E|+CWebaPK5NJQem+i zPYu$gahFxQmHtjwC^hh?KfE)+L7*uS6^kJ*nGE9*SJ8WC>OC1e;Yf17Y|GSo%JGuL z21dGKz5l;p>C;O$AHMMqH>TIWd406?^{w8uuU%_{di#xyFK^V>zqnpq`-`>LUHuPN zORHa4&HMhq$6xu~E8NO&udvI%wM+rxC;8`*gZo!yIkUd)A)2%8Aw+Y2^x%<$(j0*Y zuG=1>Ic^Y$jttz#h_*che%v4^&JlRvy6quG>Mv)mxa&JlRvx}6A5`|%0spJ3fVMwGgpa5U9W$C+$z%78*famdR33fIFC znskGJpCj4 z(eelMq#Fe3IRXz{w>?A(-5^NK5qRM0rEZNI1e_xS_n}HJm1x``NX`*>h-!MNG2;e7 zVvfK=0MtwE7&i#wjttz#O1+eZaf5)JBbbuWLRh2Zc%vsUOabqArbrYMD%Gh%(W!Q2 z*h;o}b{u7z5ihY)fZ&|S>mvuTIf4k5orViWG2Q3}6X9;Q1Lv|Sv5`_@tQ2U6lpKi; z8kL%!!%QzFQ`{h6<_J6#V)0TB#SH>_j=)2e6)(k3+#sOl2s{L=y+oniARy-mJd{S+ z_7JdkgMgSL@W9ne!4WqI@Hqkx5%X;iVQ@DHqRu)Xq6Mm;rAAb2T0_d+HdP6iGtnXz z@Asx^2P70%s%jFI!aZ}_L&V$-f{25_eXR0w!zqs(yk?HTL&WcLoxYD8yn2qnL+s;n z{aue7ylRfXgG1I!xr_5XZr1z%ifyKXvV+Yr9u}>1urSPgYyLZ~5NsyLIIkucTJ~cIB~^mo0x{ zIkWUd!0P;;);_$pBxjahdTVL_Yd$tQL#4w0sD+QC@F*M2vtj`iBJPK?B24yZ9QcQ}@1M`@x!Y7fL@2Fj zsUf0R!bmfsSnEy(nARFbdkPvGsYR`v2bJf8Me!EMG;8RwdOw9kK*~yMoXmx}I@yBaRcg|VO8r}-2~O2>0vi|E zEK#XZs&M{Tu(JQkxe%Yd-5+7yVvWYr;aFu#7RF7oPmcHzUxcDsp_s|_Mir>Z%bZb- zxF5?BqV>Nr{M!B9wWSnT9nahbmsI?I6bgY~SZ2`AktcxOiq=UgMnngy+BD^EUDbLg zzHw|x*kUW~zrtq!;-??74~By%oT?*&m{p_jl$}diwT&=(Q{Llt?GS2}n#( zg|S>nM`W>9O@UYDH?c_J3eL+KaZ4hyo!kwAdqbwuhtv08JRk1G+ufX44uhT;ba2R;$zEYt z>+Oyw`JNi9@j@g&C21_zg|cZsgWfU|+JF^D%7|m9m3B5ZO5I^{s-?MC+-nO{-`_qQ zZ#T&`LFI&AEnn8BIB(L;flw*M^nuw$0s%Z%EDHsH(PZ!_dP^-ZlZar(>%(cg2U52u zLzm+j`!AZ0_pL{$SO&w#eT34|aX4456pPtuS`kPsH31D0q+zwn zpf$7G|29U1js07PBjVi1pC7;zIK{WS<5I1v7aB%o)JuT-x#eD5$8-AJo9ag z2(|X_osamU?P5V!y6FlLg>rsmTp#2(Imb0)J-=BlAPAPr1i}-wV`Q0L|5jrp=hLNZ zkxj7^j*zSAJFObvNoo)7wGBhxzh^$&i?`b}522xGObCNmDP7S)Q{+yv*+NF`B#)&k zjs7SRK{^BfF6VK$u9Ju&=^rM2v`cFvn5V7!yRqZ z^f=ypdUH$E_n&__o;r@Sf;blsHC22#NfmlYxlJj}aB*CUvgM2zm%^eJZba}v=vI$a zK`SA>5X~^Hm^vhiXC2Yj_IZ4M&pRA1W5jn6vfAjXs8mda=u$saW=1)wpw^>IY|03& z8Ym*oYUC8`G5u<@$wy1AoD|LH>&fKFllJI#eRRC~{&VN!J#$+O4HKY}e0JnFbYfx_ zxzc!`Agb9j67dSF_LDl_4i9T>Y@E9#D^*pCc2l(4GD=gUW8CR@u;vKq`_DO?n8Aq@ zjuX*{f>l%&#vz61Fl}BF#KI8deoJE%=hC z{in}|xp}#vi=)FJ)&1{39K?ecAG~>=6CXJ`2wdHN+TkD`+~nQ{fg1bYHy`94FV&>a z_aPje$>SNX+Wu4LgFXLp-G|2pb25LF{in>wdiLcy3XhI8F9y=szj-LaOHJ-RI?@pl z-Z&JY2hYU$@A}6Eb40ia-qyE3N&hY7`iHKe>!0(jTvgXTyz*~1mshdnkKFj3CGGsz zd6(ZVSc8m)0q!*3A=CT!6#90$JiYyu+siX+c*KJlF!y zw&9o#-nBw_vnNs`lB^U`Q_wf5-kt<9RRl_tlX?X0D#r2)?mm$vFRkGbmB=@uc*@T; zlG(nQFO-Y~v#5a{F$%iUPDqWD%-o#^Jye4wxk*)XmhzWGo-`YtX11Vy5so zBU6=1g3rNu(=?W!b=Qdu;V!HF91Yq4Hd|;~Xn~G2Se!>FRc4F@h8ZQfP$ALNGcdRv zp6Mi{d{pWGM<7tY(bm=ihxIyR;5V_?XJ#1H9ParUj~1ios*K z2rUvBkuFB0LV)GEX%_Sw)xe|vG&|@kyA{pMV$F%9W+n=O6ABQhvHYC7P-K_2{Zmmg ztCcEAs>4W)sF9?@%?bkwX@Um3#~ZXp5vkDc6zDvUmtZ8%R%n7Pffza!Z6^|g)-LG> zZl)T@w1io(mp%Kg6B(SkG{em6GL@XjLW`;i5nU?QRFJ{CKxB~^liM8y{OMpvgDBu_ zCa1M=wLgvbv|+lh7PO8+)pr|+K~}*kd*)pyGUU?QHqw5RFXm&lW@aKld`8NZTFLyP z)@OJkE(d#2o-d*mHdP*%5eB8DsnjZnMU3KEGeBYWqSA^fLc~~p?p-G`n7gcp3^JNc zwm^Mbe~m5VM1QWp(-O1jxh#SSEe1-a@o+?Am`Ir~WrlRgz*1B*8pDYCunYRb7RvD< zYk*bu^t(`Gmt~k#jgd4I0ZqWO84ANQWDU(h^Jd}4_cEem!Zaq1VP%wJvsK+3`9t}d zm>Ul@g&ov0Xa~ees@Vk2Slj>8?n04W*7ncj0#Xx%Z>G>7U&sr|W`2;LF6t9W^onA) zT^Ea!6rT>~+j`PJ#;d__xEhlSC4Y?+=IB>^<}Q z|39~M{b?H?Tz&cS&wcOR`)-#2sFa#Mtd!~@h3b5F-tj^Mc478Km#>h8C5t_zQdw7i zyd*Q(-}jJywfKIs7l58BrJkF5Y2iP&P(OCEJ)A}5IPAc^o(Jt)&E?*(W#47~j#o-W zU=+5lK`*Q;AeKNCr*UAE8EM^8meoq3g4hrI&FX2REe-nzNK;ckh;#FE<_0t=%oGQ0yTo@N(?Pt0A7K zIDE9FjRolF^~LXyB@O#BxP5Ec)ET$-Put>s_kyAURy}Z=wp^3zAX9q)cvv=JhkNG? zoxVL*mJaw3vp!wW$_IP|*xmkY;eETE z!C*m*WZYcsw5JZXo41Z;oPEqsmM=H=*zI-$ExKdvWxKIXQG1CzZXox2JNFkH!}CfX zPtwNmZIl_o>@rPvEHeThitF$$@BP;Nk6o5_yraD3%s_;3 zk1Gf{JslY%@i53IO9aQlI35s?dk)MO82asVWfe;iLAN1C@JA?cYZ@~@Rfe>A82{=94QCUyI0D0 zdxZJ6l)vvhDI<5Wl<)G`*KH|3^*kxVce9i)D_q7rAvrm>rTmohqzv87QogKk(G!y` za9heZ&y(^=-|Ss<`XP6D8p~}d-*{8y>iv&a?pZ$T8RI*(lnY#^ghwF>&QFSPs~IEw zN`|I}RTv#48Hze;rvuG4ZtHd7A^iXHmd4e%Ni$_gRmGp7^fO}Az$dkg|>}%ezX-mzDNFKOy-@x23#wo|J>%sr^FtzF%1(2ZbAhJR{Qe zWWF6SZcF*vJpTWcr7d&)o2$L$ul%Ea%l+Saa-ev!@ObfL4>fo$JQ;y4N8rLt zf`!GCy#(q3V=pdR3y7Cs%Hm7{57mPfm2o|zL&3S;V_S-Hy9TPPd-0}%DIZNE(EG~m zi0nWf4=lE)Dk>j!!7eW1dcp7l5=Q(%Fyg{If+?11H=C1wtr`*35*eVYNI)1@xhmNm zclxoNR-D)Oq#eVQFJ3&kGmaV1aN=(paK6KYnjs3J+I5VAI0(cLkx;FdMkmR}2(_NTjo(Kw2bC-AmpM}7z|*RSueCg zf2J7)MK@yo2{jH@Cyik>2Hs~t5;Z!Cw!#xJL8pVpARpdgL8)TEq^~LM%3#<7Mrq|# zv<#4@pc?(beIZb`#r~6AgE0&W-&ZN8Y7xWQy>fL*CIku6NsyW|EbfF@q>FVV%n-Zr z(Wn{=)w_g}9gEmBF2wvuSPO;&l^oqkBzD+vJ~Gro#cU%&XiOLiLCj~f(M22LtLxQ40P+Y^BY9~!=d6i?eSRIkr@h}u_6A4hL9Ofqk-$m+mngFG9 z(gECEc8fK2>kwR>Wo+|%=JMs%H(YQAE;K*%yVCqT( zq7|Kg0u2!fhR%%tkB1F#88pcg^$5(C{6czUCP~n6qo$U|W4ushh*kj?J95Fe_&JD& zbPd@PJ7OXp&+u3?Mwnj_p9qBV73h}Qf6Jxi6Pn~&W1){Td*|HJhMxBh->a_yVf z%*}6X4mZBO(O>`D_3ql&){LwF^{T%5)zzl&D?aVYm#-)*Us{ouzql+d{S_cT`{&`u zU>_*X@1@fF<_%X;cfJ-gO{_tad8^DuR6aL}H_G|!NO2T_67Y&|?&MA5F@MEv^ z48TJJS~mc8Jp=GiuG$TNSJ(i&RHXON+1Cw#9nSzRSp_fm4B(Pg@G={KmxB10tb!l# z4B(Pg@KVnJE?EUHu>p9g$A8Hxc(G>ym#l(q&j2o21uwDzc<7yexyAS&e(Z&w0bH^Q zZg~c9$tt+l2H@d{y=)cS;~BsutKbEm0bH^Qo^J#2!1S_J@I21|E?EW7ovYx+f5yuKU-%wf5U zw<0DYxwT)IQy9fmvA`ujZ(FGz=`^}pQLOvxF^Y_5$|Yo6!mAS+?L^8Q_*cj+s?PMKJ&`Ewz&|qFz8^b*f$Cwba;>anv~0x5T4Rob(7qR&nd)mUCUPjOt6q|N1GESS!`D5 zl1M-qR_iu}A{vuLWLgdcb6d}wQDbScQ?M52{0XU$+TYj%Ut z6o-x||B5!Jz@t$f)TzSq5kqK|F&+^#4)kp(vqo=M#N!xUM!7WJLuqpJFPt%|lUf4w zR2oH4q@yySOhm4MW~5v@?e;8UpG?nM_5~X?ZXr&BlZ*)1s9J*viYlzuiJiGu;u=!zV!(ys2=3G^fLd-9;xoMfUS9C&l|aDL}J~5Wy4Wi;+SEnOaoB7mO4pvUWyqDEgt>u zb_=#UFTlf0+U>)GK|Y9CN%M^?tyOw*k}LP&k{RTsk}zr^hhDwiqCTT4Y01t^Gm*jo zd9qTq*EKa;pH=G#TW^)8N*_$l7==$R53Nrv=kNl2Q+CTGJU>osI1N#6yep1XQ3!vP6BSC528? z^+LGN^)*l(SC|r&jxH*RZfV6iHwN}HtkI5k_|$fbP@P1)D2|j2*|#YcD&SD2ldF!# z&IGKxg~5ms)lEz#&N(V{4Z|bLRqlB}FI*Mdyo^~eLZqee7 zklQV6%A2WnYXH?o6h){LjPf*U=mC_&5m}pn3fe7a3-SB&Rnm({qhY5)cj<-OE&2k6BZh4YU<2q@vfWw6ZD2zI0UJc> zrM!eq{3JJ-j_Tv1Z{KQRPh;Fl&qc9X5lKC#&bf0VFAS@su9OGoM&+Sk&+9E+?!v{@ zodqtmYobtW+n&j`RbWm@NFksMc)ns4sG2sF#WFN4R78zMP8-{O1Ot_aGgfaB*yXef zl5aC!E<+Z}(`0}S(+p))OT`G!bUKa0J4^ed83IW5I%iB^IGhV~Egw>Hpis5EE~1t@ zw3{`&E75LsaQboEeT>y&Lv)n7J5d^$F+I$2IRwfPE<#LXsZg>?Z8gG;S+|`%#%;H7 z1(X@rk^vG<1Qm~AA|0BH*h*$Kp$BnHeIDmP0X?oW%O1b~b_>S{b-{d?@&!&$Z`NX?m{Xrj#)D3tf?iG$_?8slypu{6nZ{LCyiPo+vLi5Vd8KoP%Wfl zDB}*`jIW#Oqa5J#b?2S+$?4&yjdiO4v!3wNg9ew6^0Y)_++bv zr3p08*XcpUq;+!Is6ZNLiX11q*@Q`Pcr>-E-3k$j4z;p#lBwBM4YJC;GMff$1#`&I z=H^0C4+5T`nv0*Wi5P-foq>(xxh zlZQqUF8SNzkT|hI)|DJ2Qw^`%O&e8kOsHcSP${DO>C>$P6O)rxvdt?Ipv%Y ziEJZPrzvDczF>%1#;(>p#!ttjR4TcKpcKY^A6b3Cb|0zCTj4;8D`d%)Bw;via+MD5 zC2_u(sW=H_vs1}-Y z4$U54+isx(2(SXC&HG*pFM{Fb(`v8PaLZ+lL)%W0S8?fUD;Dt+v}6%x$@=l=8*1JM*gR^n^Sq&M17^Fv(G`!(^18({8GW zG;E|1Tf>t)8;lrsw(^CoKAf!V>!o5c>p;?c-lJS5^#=oVoG%zyD#%%{4Gw*2hb>g$ z<6qyFior}gkhK}yE)A{Wcr>mQ`z@S;T8*q%l|XfoOaKxz>g=#_e7N0$c3mWikVY^@ z%Z@zpvrOGXmAH|&nkX?CT6n%Ufacl0JUITL?G_C;pVPHgbqMy`ia+!xRV?7TBq6$@ z#z%UgE@s(iTF8~hD=*z1M?TU^@ED1^lpo-IGSkhau}wAd^CSaPLEEnqgaEr9&cQ1` z*%`-B0^f5Y7BtzFFas_t#$&7WSs;V9*$Abfk~CZYj?dk3DC*k9@zQ;mw-s<{OQWV)~uP z%a359I!k?W6pWgd92miXLYwdjeyfF3=woqEQK?!h@6b`bf#?*}sM$p!DwtKX8ic|; z&kG{sajPHQ?t|{|@vPHhb!bF%C9LK)a&9|cEkLl^uJ~ihcY39&In`io<@}-TCaNpx zGn>jKgQ1FYYOXgZ<;XGyv2Y2u@}SsMj~kAHO@Nj&+slt-QvE!%#s^;ro9I@8BQ^sGZyg?RZ#wm6 ztDlMMH@rT;UIClOW_;~twYg0S-3lkJI^V$~yU!+ySSFv%RRFkHzL$e@pt@ov4{arA z{Ial#_7nUf<0gxx3cQJuv~S#GT%)o~Gi3|dgpU-u-<*h0z%-LS)_2ZVIujlP7cSqP z_vhV<4qL9+5le_>o*Z=TAXn75F={ih;8Z=6E4XPc+dhwu(Mq9@rW>2-gDGM4g{v6CGf>0$2R4%)&)2?>5IK zx)r%@8|&DLGrkCHqMw_*w~S5vRA*gV{*hD{MOImF%O{)Nau3jjcg{l;k^5?iF zor_25If(JX0Br^|1Yh)I7t=~Po{Sv-+!&knHLDiYn{myd1gBCfk@;jg0L&r>!QF= zz1}zg8CTY50G8EUzEJs<3>!Yc8|!{y!~e4xYd_7Dxw$;n=EwQP$uhZ3aLu0&8@||W z2V}2Y&=M`IgX`z1W)SA|>8#LoDW!&;%)Y!XcxnGQ82$qG>0L%f7{#_f9E zWLUD5%i93FY@uwK;^xpdEm7KNZ*DtRSC-ZfFqsZ)h?~3neh0n+{6ArL2M9OV|3GY|Ftqzo1nTEgcKv~(>Kz}B|<^nJw(0+nntYv457Y4hu zQ$5Se(}~`wfsp);kQnD@quIH%5R@RsF3C}@=#g!rz*a6E|8JgU8qIP<@|E*dI4Kdc zhA~AQD_g7#`XEoWI^x7c(U>yaw`i=Y2*c`7Cgyg&-#SMkOxwVi*|0c6!P`VXM-AxK zIq+_|Sp0u+bLczq|G$3d9f!`o?ks!erZX=)gPy+O^z%-`r#^S;X{RzLKYjAbllNKu zi`B{MB`5yugmL23@wXpWjxViTv(h~F-D595h8_LN(dQqn9Ql_cPe1bD<-c2g-12=7 zf9$Y-_|m2KEtyNF0pnl7kMzt#M~u5O>jx;93Q|k*Q0e-W&)pY#(hn~q9;5xkGe6nA z`svrd#C_!((e4W#`IEoD^0kj%l{TKZ#TYU;mN#RrqIQy!>J0PkJUVDaS!$-ooqVz0 zq1th|-vqcU&NkzJ`{HT*Kc9QkAH4O;uV4G8##K-H&*;7@qeqQa<)=RQYj6Lf@BBd8 zc)}KAv#oj!44^=i1b399Shh7WMZeI7h%i45a}8~1iBX|YYUZ-rj2}&PwsPm{+JCQ!h??bkUn6Er{;0 z*-4&{rx9wFd~$}3Qo^IQ8GqtY*L=YwfAqF@zVBPfOJCOf;)|~SoG35qmv%s|Rj9fP~gHOnr8FySP*Y4Ee*^b2aA7A}c z;`+aH;Ex|a{2Rfi@BD8cn}76qA9~!q|I|YN4u9siPfHtDZZWP-iBfK)HH=mo;|xC_ zNyc)7d~s|@^;T!l;0g`7Qp-_9c}L=7-%S60b}ISN*YEI-$KEse*XL#4`Ko`TA2vLN zhL?QDBQJTIw2^Kx9+b_gYZCd25us@VDbDR~cZ%{OqE(XpY}zBR5(m&>eM@R@tKunt z@{JdLb9&QPs(AkL!F_MoXiFROEyg*x*3+?>l^qK{ zc!5=MEej9C0g|SYRiyf8rp_U_K5HvRahvf!T=%}{q4)XZZ(iG*Tz|)pKlI7)p}&Fp z|MIjCeE&6v?)aIz7+;q*k}bvusK~p`_-TV@QV+T#{q`@ukuA7y(QbOnhyS|rsekRS z{nN)Mcm2XEAHFVa%(fWwln~@8pOZz9QzTZ$T-q7ocrDW^bZe7ojijV;KCfH4BJJoH z`^`7}<1?Ro#cv$DYj%i2ue$^CXOEa3d+j%$zNVD^=^tNi@Y2R~i*d8lA`)yU+P+^B z$Fyw)pq#Ja`qsoWc;Lm()(W(2Z!rDO))#mzzjPB$3>sr3SjEt5ME zKlEEiZhYzC&y&IFk8)?evHq2NUjCS;fA7N|@xR|X_FwP3`m2Go5p6M+JQfLbH{YBF z?zkpaby+fM*##9LmxnVUfKw^7^8g7b+MkCc6(5yq3q1{i<-nW6R%gK2e@NQZMrC;R7~#} z$ccLyFMPzmef&8;_@nn<{Q>2jzIf&%$v<6ho za(Lg9)n3aq%5}5SF37cMpTHXfbXoz=yF;GMo0@dZ@5eHfPYVN6N&70{kdBqm zwb?df^NGpZ*Uo(Xy`OZM7k~U)#OvSvJ703Y_vG(}-mCP>$hWS$(#B+q@c~LcZ!+Od7KkCO=D!LFg(9c;DY<{M78%;rHLS@S%sl_{tl;dZr^i=JeXP(2eJR z_k-nEzD?@>wIFSbwiu5`ZITGZoKS%>Z>=t`!0`_HoPsyn!?uWM6+06 zW@ckvi(JGd^M#3s3#C$0nCetWqc9;!Reoo>`7fX!AiVQ4@40tj?Ts%yQTyjqxZ{K0 z_0cQ;?|nW{y|LXsdPix)-C`_cV7u8z2Ll_-u30{l)eBl2xg8_tF%>P}9+0D1uYhzZ zX-DGvH||?`%PX&X_=%^S{MP$MZP;6kW7n(Gbra#(OgS0!5@-zk2*V8FhFxqj=m*no zeLR_F`0^CqX8f-1GuL1D{zrc9-5)vo&40h@+3&jdQ-Av-%db85mG@qC&1--1ys!U6 z+OW15dxc?{oww&2k5xpo-^S@!;Z@#mA+vNCwU8dc_s6c<4w0R;;&+oP%lFIscldGR zyPv+p@4f_ojs4J5g{OV${XY^P^=j#bZ#gV&47V89DJPz)g`@`(n{&K7r6|)79lPRJ zD~M9y8cl07Yqa=?u4&s6KVaj*?|tJpfBahG@9sYSwWoiUC4YR)y&pMIp1*~h+k`Njln#jnqG!HTZCv-9(=*J#xtK#w(&D`nq z9WQ+7+aEg-Uh$WYxaV{I{6Adr=r{cE#IL{LhPM;2w4rY?K0q?$Hsi;d$P+*M4*DZ^ zd&eJVZhHB9`#<{2?)BH4d^)+IykO&_kGNkbZD?DJ50JXK&GI;YY6Sxi;+x_$#0DqQAW6KkxhkX`{Er zn5m>;eU4}6kRNef&7V}NO&f%l3PDOXM{YgmrN9ktR*>4x>URD!Up=>0{L7cV5qbK9 zUiY``v+voW-}BLj%;~>(|KsF8{P>V6Z9Ha+@d0x4wk7`Vb)oizZ{6*^ziSP@eZxnK z55Dv+@2|u3hd#@fpZyQV?r{G@(#92AjD6Fadq_GgX6?xgQj@U)Wov>q9;)J$>w^Ln z9e-RJyV;VrvxB_;2XA}vsn36b{PG8X`>n6~z3A@uf51~;^9|%r-aNOjxnU!^L6bHf zy~X$dschR4Klk)^UVfKPz2Q@j`t}sN>`xz2z+ZLw6J8BH!TsPnKlK~+->WyJjYn-u z3|5*EoM-1oWWuw6FXy}hHNwSOQgNzM)+kLKQmO^e(B3&$y#D2X`-+cW{TE;9eS7s2 z-{}6yef&G$legQq=A+EBI_A0X{$TjCECeth=_zUSDZu721*tIt~2 z-}4V2d5V2g;eK!U=H-9#&5vC6);CKV-7UrkNT%6l{JjVK;NRVUd06>B-f-9FzUy;e zd)8b0$A9g4KSDotFY*45`P-Mi=&RC(ye;wKIVw96-|2oUPg1L2n1ANp&-|A!U-jY# z{^x&Ee|R_a@BaDMu(!t8lm159=xi}QzzfPY^pH>8GrsC=FZ-izy6 z&XUVR%kV;J5V`P^Pj1|6vzsf7BQIHNaY{p!L<8;m4Q1w#)6V|gY(MEHvtvTlQiCB(c2>5Ce7S`c11Oy%PPQOVl(foch9m;Sx#x~JFaD*8v$z{$ym1d8 z%_G1!_??BN`FRkJ^#k}f1{?ne^mrch_}dHjxcRKTRRb?vKY+u-+PFLDsQ`L<(ZW65 zvP{|f0em3fvtGLMSzowtXTNxs?e%xAAHZ{>ZCtV`4H)_h7M5nmv<~1#0fWBV&Y+*a z=1jy^2UGNFzc1IB@kps(jA`vf++II`^96`=*Bx=5_sbIJ0G^fc#$A9ko2K>Lg{9fY zv<~2M8Eo7c^tfqS&sn&~TbR}Xd@$C=oj^~UruFQFd-@sEI)GaSeAYYeeAcTM?(7$u z7PJ=*d;I{;9Btzco6>-x|JK6N?3mU8JUd{}XLknutTjs1J%O9pjgi_KlT?2eUW92u zd(o-a58w|1;+)wL=b67OaSq_@8E>2h(rlX6#=_Flq98cni}y zfLqAgI0<^%G_9vE+|$pP)&V?4;IppoeAcHe+}STSt-a{n>j&^JX&Watr2#{K>cZ0O znAQQDO~J--(8~iKXdL;?H3I4Slhh&&Nr?w&O||U(Eo-a#rV;)u#^w3}98u$q70~JC zjD5<&o$fPZ58$C1Y#akUZqC@N7VhyDGuB(&W#w)h1wBj!YEjW-SfX*NygaSKbckLeu1DK>t_6L)&N^1svL z0i0UnXFOr2$MnC`;{lvfoB99uS~_#+lmTx1SFHZi*~hK6PkiD;}3$5*afdDyXc zo%-_OmmVuEJ^$$2md+f$+tCLfx%SBA%dcL(?9BJiM5n)b+B@{Di@o^oX?OT;{|zU? z9$pvu&nF)~2Cxc`A6^eDh&{rQQnsn;Ix1rH45X>a6`$gp5`{psLL;bKs?@>5+4{6A z)G3pzOy-n1olQ8VAI<})WmgTC5wf z8}@ttJPMq4VWQuan3e*~roi!nt=@B-MXslh8lxA!Ydt^9u#jXrR85)-qF;=uDIw|#>3Jcfrvw6!f z>xibiVYX+Ep@yK8$Ll37nOig5t5AH7PVtT);MsbJIC-^`#!zVKhT=riA={2^n8ki(+P7I$kxDH|D!~RaQEQ$K(hTwS zS~*IF%}g&SXdzpMVzdh~QWJcVbg01)35J>^<=_f<`~mZ!(LouZ)2ZnlO|Yh{#87)= zSpJP&85*5RTqyS9oLcvkvCNX4R=HJ0Fo|fWHX#_rj5IIVWgKd(AHYL@>kG{hVOIt^ zAlpz5U|;0Np+of(-S<4bQ0joZbJ8SS!NU6%Z;3=DyKZ)1+3CU+eIi@=77m9{6C~Fb z|wElL204stcCO_=DN*!5D(iE84H>29z7g8w<`m|;xJl}G?&K%C@DI4 zbLe%4^ZBqW)WPemX3Vl>rG-sRWPQXL0aBYLtO{&cDkV0OAsV(*gLs9j~+FIUfou++)WH|QiCogTS)l2JP0ppyU zn$fL_B)iQ9Jt1W{BPkQPICC_j0^!ND=2HLPRIhIPPi#sqYZ;^cH0SOwq%?! zN;$A>w>#x|IUD$u?ApZDrYOnwZPZ~KjbtP`dt^AXtA;#_& zf!DxV9Q5b7H+MXb)JS@d2uq*2S%x4@8@c|lFe&h}Q3+S*dZ#Rl`7T(S>k#JAN}(P* z^GcPL#_MESn&)Pa;m>HKTQ+%1;q^(EtM^$IEmT9pprAH3&h~Q=x2DcWWXQz0qfSMD z0-?3{`pOUYc4bH^9Uo844&mThy*DWguwJ|AQ2CZe4k9)-nF)tgr)EDGtS7^P5ukNe z8`N>xhYKa47R`vn*2a*~nZkT^U}TI;cc=)?S~oV;rir*g3@}0FZFJV$qle{}?aDCK zD;8o6RaTZ}Y$?kTbw}^@GknvlcX(cg`yKEIpKsG_xZa0ADBEOIYM5im6iAirSgl^Q zA2eJ2p6z94K_#_w1GT91s0F6vCw66s5}ZT^)ucj9VuI+~V4Lu(?c88u4h_6b_gp^9 zOp;t7f!Db_i5B_<(&k)9QZ?3Xw#Y184`!uC-W60x+ zhHdVB9$9|jEo6XGpYNzzEXXB1)kYr3iVqr98i1Cd79x?XZbda@o|(bxU`LwSAb%Ic zPIyjk?4T2qk1eCo3J1ej=!cRrZ;c#@C=a@8qNa`~U9&N!30$+ge7bi|E>Cu42+1N} zt`CURL!GU5jf$_DDl-g~hMFuHqn{%=6?R7FAe#Nc~K3Pr;Pt*{sUyETgJ0!Iz zwPdYHmFrcU;=8&w<`e+gfv<~tm6KpM9vNnVff}Fyj8JA4{{qHb70;%w&=w zzD9bEhBTap-Qj`Dif)ASJ#%uYv@648GACTG!Pt!s+3(_s2(pU@0s&J_g(w+{B03Fv zI2y-faUJWH*?O2~_1=h5ntEMVH!(|`!60qK8i1an(oD8KYvy5|TO%n&G^%Jaoiv1r z=GP=_j|_*^T^YvB>V&|`e(H@u3`;60m4b~4VH@%_LdtlsnV3*dsM}F?T_35wSdr?5 zFeo(CkZQ23Wnw|YA}9@%5F^k6mCw1;>R5xQeQ;YQA<1aMUSc{CLGln5{htkkyXU3#ovH>mxYtW6gHa}q$Ca#*2W|ERv ztqFVdaC&`L2F}Z1AllvpkTup|;AAFsDalBVne8;H<-xpGv1SQbn#BF$dYwYcHkb?v zT%PrMlr*)|e!Hy+IOQ0Ulf{P$g1dwuD8<=YGVD)%8Z(oI5GpNy>gs!BIP}HcZL>L_ zPAmUdb3tFo-uo`IJdZ6G+80^2&-8!t#;AX;-pr{ z;4M7}seVIaN+SvdG2XH&`Hl5L$kMQC;&a%kO(LtpDVmFqvw_8Pu?XIOtCb0F4%%#~ zF<6`OJe#odT7S%t?9k_?KX;1y$K6lSq&HhXKPap8Vz-kTN?^#HM9b~)gdDZ zfc49xJ)dIvK{v|~1ftrB3gc)}Vv}AuGvf=8qUv2LTMRlCh|P(ZBWE~q*w)rnw$``C z4(GsDH_xCm5D7Y?xvC#EkdoT%s`HwzinG>)%iwDhJvRFCNSr5JM@n!Co$Qfe_5Pdh z|0fR(4xM??=?9&B->P{0%PUVfcIPA4EI(}Nli<&{)z4LftL}9C@D*#nsU14BbdL)? zsPAE!U$ccJqTxduYQ*m;24Gs4x?pQTzdbUmvy#hQn!r2(8`i?iN`u|)=)q7(XnhY(;w9=Mqw<}fO zR)xB;oT~t38@PmXq*h>@9EkcJ@1trAkxU=rY>+tKl@+&~&+0H41F#co9;;}>V|t}= z1ffxt^pQ48_>Rzi*={BbZjrFS9d2Ni?hj0(v)9efDNmUwR=`?+qJh z;oXK*Yw~3KcBSguss=AM^xGYX)+Lr!f%A_~2|~5;eN>H+{m3Ni$tW90!zeZkI&b>1 z5YOylIjh^OQ=7-_Op_E1aj>cCGdHXHk_)K%=ibiIs>swu3{#NXk*age+iF(V2uyaI zNGtHPCc2rH7+F$|+fUU-kTP{rMjK%UtJln;E1k7~H0+ zPv5NSyIn}tdtdAozDsgk3&+IUm8x&6SxrEs=<$r=;x zxR6V{2~^R!Ij{2x0E6NZyce)(SS;&o)U0;=EmfboS=D#FkgE4STO&$~AW)_wZgvmK zxo?$$&#J%?OVut^5+0O`u1PTfF7?z7KxGBVAK*)GmVTaXmxI^cz6DCfV_Sxrb`M@& zI(OTZm!2?sI;k3@Zi@M^RQK?Zt?hRL!7?++(#SI0;gW)vPAf!DOeG2Uv-wJ3DvXY6*pU17UoQ*zwc9APFjs9AF+Hwyc)Hia8=Bn6I~0Hmvd1!VPRRp0qSs^0t7 zEYkp;lK_dF^6g61x77lIHbBAj+*GpxprtbgIWYR9k!|mzY5<+44Gb}%v0%{8wqygt zYZzORVcMD%5LYbp>Iyt5j(RwmNY#6K`C~(q*>j4*(6gKYq#qUa|MX zPx<-ipP$E4`TqhxwvQkHqz@$M1ctta&!roaXt;?mwLfst&)}kLj^QHlPM~t3vf0DF z0D6-KJ1kMNS*6)-7NWWbA;Dam(7^fGZZ*z_r960B9aLJag5X>p{6&`*aR03cNrl%MWx=@by$ zXZ-;2S)ixor#*h7w~b1sE#09&Bp%-P$x7AX%?hj_z=v)~_7Hap0|X*!iDrk11=zf>6xr+v~JT=Zr-tJ5<` zl905jt1%0aAWBf?@OE`n?+)htm}?I5HS;_umee9G3}MSz1^^CLYe~J^2v}h#@_q1> zQ_OW!pFpFC(58(%2s>NoJiIyd9sIu|hdzAh^0ji3FvoKq#_2*tc zhQm%kIhY%U19l+Hcn~*5<0&^S569ynnv*(+kmE;ulTIc96T@t~3oUpc*ctjh2ZDo4 zvdzQ&(TzuKn(}X5vb27Hrnj9b7k?#9`2e6kzk;T`kEd)Cko%Wt$^h#9g178eS2ait zF9(q9P>kD@6L=nRIX&SpP(-c-u-!`L=={6^^)LQxU-UuXB?o~ga1i}ps;do-IzNT| z{-{8di&=i`N^p3to*Lqui_zG&J!3kJ;&IxZ5VUOyam^~}S~H)cS{)DqQ^a#rz%6*o z-W>X-n=9H-HVyGv0DJ!cv+K4q#KNznAs)mHu+KSsH}m>`$gg#?0GqB+5P$4i+RvQ7 zi@#L6E5en^1-H;@e?%ayoGT2`T0cJ5O-duSYGVB{NjrJG5~$Pm3=~ftS0V1Aw@`d! zs`YN**VC9ak|HiEAp%~JtKxvODGC(RsWbI+F*!~l$2dRKIv`i0ZB(Va8tBte^PEbh zAY^%5j0!_eDQmJ5LUO4J#JP0`_14gV-95wE=x*BGbAWgG0F&vqv%CC$cK84N>F1zt zM|+&Zw@mc^AF`x41ofD<9wQcgz}|eW$Y!^u%4XZ1ZkV8~Nd*49>1P5Ug`=$}jVtf7 zlWV_I)(P7D-XGjN9_;5pvtPwk!Bn z`@TB*DJ#d)oS*&<9=}v=b97ro+YUVNuPp`q^g(R%LGzcl%@@n%(x#Je-}`?}2>Mdp z2$Alz59tWgMbNqcq84C?Gp4y5n-m!>nC6FmaPjY9dSEI2q0#Ll;$V*B{h>|F0o1yC zZdjzy_IbK!#bzt5g_t%y4^9?BF>Ar>1Y|`C-42Y!=j*=EK7Y<=^t;$Oq(ule+>#f> z|KA)sSZ+4+|1UeVap=hXmS1)3Pme)I-+DB2yOjMIoj>>|<7F7~}gw{(TL&LsWb65`rDMyB)k72JM zlp6%T-jt@Tu|r~LHk#I(upi|zECXuewzawKXrpFjJkd2G@l_XIYpN)&V~C|jlUY|& z2kpJNF^7I{R|a1Qht1w3Qih2$j3!t?Zbd>>EeSas8u>lJEf!MR&CTiTx+t(F!Ht59 zk}Z~-^J=}AtwMe-qPzWE(MMFh6t|lb+s>-lwXs~Mksxe&(Ln7YY&F=M_P#XQm0<); zG0sN-2zIBcj3u9AM;(T)fQ;q7o;M5q>{L)dHfC#5TrVSYFC^#Zk&>bea!72zgE*wL zEJKX#ndJ?6OeE7HJrrep4Fs^s8q1K4*e9A=(@6H>?VPyRt_+JKw;u$*>()WLj_9|L zffSi&6uWRk4o7-^WP=KCj@?45Ub#umNu!8YqrtETBKGq0{AZFd&Pd(;09%eyi#c`Iy!%en_^nS2eR zF_qGAj;TziT#K`}GDxgeD=Asdx-F)e?g;7OphTAuMqB{mP}SJf)vVjDvo@2-(LwA@ zZJq9}k&2&A_rod|pp?otFgR#luhoKF-I?q78g3bUq@sjE(TT`zW5s>V9G|@L^DyZBxX=G;WtY4D@E#>gSu;8@jRcq!Xjks$eHq#>TXyTO` zAW^Pb>SVPweX5L1dOX6M#0>&WZtqQdUw!&5WN63vW|TrxgQDf4LIl-Vo>RkGX3TRK z70Poclbf_8yhg2ea;jxfViYo%B}1do8Cu1P+SP_C)SYOBK!b~9Sg$u5Id07oIRUj8 zpKoB%K#tlvx@S%vy~Uhl7MDRlW6fy{omS%H4OAQXT)Q_NpuKU`ZZs^W&oqpgToq9i zA1ollpYQ5H%VayXDdmz7ksVBWbyi`CJ9Yk5+aulDCoAuDB2tr)K7X$-2N_CU)- z=$x!zJx?q|u0)VrjAe(Jwlj*P0MV*sjIRxenKjV6iZko7!pv~_y)X!?#I6j2AX(0& zT(BfWwEMl(tWMn_OjoKpL3{OlwSvaAQWhLC#_L#Ubm5UQFM-tMAn&I>+?PQ#VXu!$ zD%3X8aY2PUHDcBR`J7Y0hQ{T#z}4GbFRW{ObMH@lU{?kgP5nx_H6kDzR(-xy1Ett4 zv0l%P21r2`&9d7b*&)CH0J-qUzW_6mqsaTyFgMfuvMc!kjh{;buJ}fS0e7BvU zJxuHivvsuZd8r9_416}9a}y(zP)?^94oacNH4zdg+z!<%V+hwnXsjsDAb8#EWqGhj z&(nFx7Sdv&T#FC&;>T1NSGV#wTq=2ZqDzO z`u*V)bw#Ows(1;c`PcI8%rM{WJH2@zu`bk;x#7G$%h#QCQmIcRNUgf_kO^|#81njj zjzF3OMNmt|3A;eD;%K;0U zph&s2>a5XmnD_>cMwYI&+>SroyHp>p?@kAqQ+s`)KPLd3^n6tEd%-~Km*!BcDz%Y3 znxuKZs)s#%P+ZqbBAr-llg9}jAa9Hyx=arWv|2-XqLuB9a`Ld7vpdSL5U#PR&5iu7 zSDTA=G9=Uu9H2;Lsq1qiK(T%}$L2RdO2Ze!fyHTgt@b zvc^Cfchb&O*QTXn&Fi6Bk{MXEh(rKAiYWl3mXR1sRTZJdR$uEX5%2CXi^JFN%CI=h z#CjbAg+m?5QF1kC5dy(^E3)B=zc<6RlrekUmEGfxo& znd*SVPAu;uMm0{zyglT3ri>}MK2vkJ?0OFB47)wL1SV)*!A8og3$t>sinU9WHI9Ta zr;$9T7ZeUabBG;Wl-mQ7P*fAs`#!Tb{eS7~t_)%x&wEjc?;{4K1Xxk=D{UmIPs^2z zNw#yNN;q|bX`@;htT%!l;~|*N*re>4jY6Hl3jN9`#_ej70*s7Nb4-yL7O@7iHEe7t zc!22hNwwWj>Z-99n&Zgwt_*@N;4Hl$Rs#1uNk zb+QcyVFQT@hM%n^THMLaI$|fwGh>ftnj=W%vvWG_*;5f-V@g7M)m7&R%875Ro z-KNaEUr#JPqQFI^UlEm(TGtdUQB_ax_xs^G928S%j?@QW)riY**i(4F$5dKfwq58d znFuSZj*GPeWlXJcbEZ8QB!M_;Gi{_98vY);IP%R~$dI8Y)4CwXjvk|a$H^ge2_y;D zbgSgcZYSdp842|@A!@8!R9@~2e6^G*;I$gx?>R&dRJJaG($;FRSDz~&JpeBS6fSdX z`bd%H0?pLes9g_*fxP!PdH5TvM_zacUHamolWVKrUH!||SFXmZ^y1_{mqFoSkHD*~bdDa`$6jKlZ_6FFxiSs~)@L=vR+k zcl5W9I!7OQ^sYy4IPyLq_=65T=m>ZC@Y-0Z@$)L;uvmZ_7UFl9Tj=`4?=2m^EgTfT zx;(a(_thhUcP#VG;ElH&QQqIaRmVxX?r1&mi}YuKNcnZ($bf9NN-hHZ(4oR zqNHzJedD5}f4KUGi;})!^$m-XUb}kjqNJ~1ef^@O*Q{Q%DCz4~U$-dfYgb=;=+NPR z{?)a%|CRiIHE=-<>{!9AuCKqf{9B8XK5O||JEWqEG75)!CKhucog{m2MJt!CT)HUf zB`cRKN_w}IyDdt3*Oj|2N_v--yDUn2=aoAzN_wZ2J1t6j$CW!SN_vNtJ1k0icIE7% zq-R#nEJ}KM<@BPYr&dlaN_ukT)1tDCzN)6u}wNMqZ;EfLursUXH6nD_nP6c4;=fz zqNLXyyKYB->7Y&Gp;_}Wr^U6WW^IoEtIR61C~1AQz9=cZN-s)Etx}7UlB?vRq_x%B zqNKzsu_!6NiZ4ovtzwIkqO0f*>8&10zrXbTMM=N6^u0w%zq|C^MM-a3x@l3;?<{?1 zQPOWOeS1;TZ!LXmQPOWNeREOL8<%cel=K@*-&mCN>q}o>l=N#$Ut5&)t4m*9l=Lf0 zUs;s&hNT-8CH?Z!msgLx`4D~R2Zxr<9qt_d+L8JSd=y(bcI?x~UVUtGFW<~oWADt^fY$*PG>%S>R(R1>D1#+(TDGM z>e8ckI%^-Dp8WUad!KyAk;fl9b@Hl{t&^9nes%TThkmfSaroVPh5B6 zIVY|-_V5$uj{o5JN0+WU{-WdB@%-7B9Y3`4*DEhOdi{!hI(k6!RJwfi4S z=MLp}|Fy_(xn<+<`b2M3Gkq-u6`2QoIRtL;ww_jJst1Y*OZoaBp1G+QQAMi6t-4z+ z#!aDQRErc6Wn}QsR<%`y9?Vk%mB1Tsc7!2W&DE%?Q=eX6M+%Z1}L@7;FbZk7gyem5 z6H?5}X_E{~vRIK(TO&lCgej3de)V=ATrco3gqJX?(o^(|Q*LG4u7ZX3yh7C4GGx0t z0Se4yaB9Wg?jx5|uv&BKP1Jm~hsgyjqjkY+qQiiIJECqRHF$z#s=Zm2JGr{u!Yq!k zQB!uOE?l4s9W*HUoy5+z49%Vm_;JMr1=5=%%9%l4&XqVQFVt%l zwk7H1PCTD0T)EQCgA&i>nRfgo+kLczuAHEuf@gb)6IIeg0q;7MkeYfUSQ=+WGTE*b zdVMV_uD*7=4?B=+;K9U)OHMZJMy;|)f#lc9Na7*VE{F4o@%3S6NH_lS3` zOQU<$M#Xdk3)sN~2Qwb^l4|uXJ@pb2S-tnJ5XUhkV@fbF#XtZ9As9k2OEa*5fdHl( zSQgX0RKrrdEDK)h_r5c7;%LU2r{KWu?-&0upULE0o%8Ow=f3;&?mef#xo)*ZDNmAq z$_6^4+8Z_+tF1wPDd;m4Bc*t@;3Gv0OiRxANZzbO2fV~;i-J#Wtd`^PUNYxQDdZV_ zq1*FVERM1&YLmOt?NG)P43Ts0NelO~)fU;MbheQtRb|WJpe(00<@B0TM9~YDNPg|Q z+e3~B8{M5+yxi9<$X8or>PwE4-q54M!E~9V6gNbQHEUg~w=9)|E}vPG=@vuUOg`!L zuphD7B5c-}90OIoHOyDt&QQZbMbp|)Lr1=$wS;s@MKzU7ntK(qxxjuTv}lH@lu}(z zbTW-xINj4@uX{r?}r}0EP$&_2)RjC3&uePnwnzdP5A|tn| zby;Peq~w&GS=G^_`dU@B6)NRPP>ia{(#RNfI((YEsgTHKz4?|lOj39IB14blhpRe< zOD(%H9P4=bpjAQ~!%3e2K$uBvqnUp)9SGu|ypR%3RNxG{oA~wI>uGa^00mUd} z9jQ&#!m#I5>QaRUNy5C8Nt)dHn!2Si$t0%L7S+H~uM{h&TdrbN7pys2#+=dARW^tGoy}7-Y|5mVXdX8AGqX-l*&J{g*#)v z%q<_VPpq~`jw<90cEjokxeV1{Uq1Axm%3_g-b501cS*#zWU*v&WXSte9K~vjc*{h} zOk`u8tWHt&1>;3+XvtrRm6G|eu^cJZE2BWkXmQ4}6`pd^LS=~*3Qm_vlZ-g^)v_$> zHQ2~@KU;vZ=Ip9+peD~IvO|;BvtU_m5g|LxVsiJAKUq?$GGyD@*iy(j6V5RucbchC zNf8NUY8i(wXg{*JGK6$QA$y(C^Q`(DbHbuhl}wv zso9otcjM!1&M4P82NiYHvv9ABlVfjdc4M7EFVyL|hx%|iqV;1yb%rTPHU0zZb3MF+&UQ2tzzJWI7lIQGU zvKz_4$*i_0N7IF>d?^+4*7g3#QZVN%rYwP>u5R)v)U~{=8SCcBTep%Tv*4e!h}aw^ zf7)T`)C`TVC!VQB8uC`2Z2HnET$!BOR#jz_j#47!V8q+P6uQJxzowQq zL-9z;?lV`i?j8xxOvvH0s?-4$cM`NEwXM;u2h#ntYE<>fjLoimG|IUg-CR0oGU*(4 zLnJ`5z?kcPygKQ+l!~zmsoMvCJ?zYDSeoKl|jW zRe6&u8fqz(y~t2SwY(PBI9ZW33jL71>o)|A4Vkw$9JpQTjALQ-F4)ZJ;tE$WOLE|8 zo0gW*tRa_W$=b#lYeSofhRgE4JO73>uOu+p1B?U@bY)51k2bFBjCgqHU*#l#nhcf=g6pkj>^Y#ZFk^ z%#u7=3#+qLGwIhwHC=-@+mD#_O0v1k;w2|@hoyR(B-ZLWed&avo+zt5JwMO4sv~MJ z$bFTvX_&M}y$MG-QD}DT9ox898-zQmo;sx|G<|KSt2W@>F=;_+>UcEuc1Y(5ChOT^ zfUMU71zk6jZaE`yx0fP&e+^42okF+p)YTTjk)CXLTXJZU=BljJ4Ut`aqf}lQ_M}Ej zEo-&s_byptf$~x*j3-MaN-OU;+m*7bqi%G)q)bpv*^e*z`hC5

    Hna1+t4Vnhlk* zarCBD9o9frsWp$Wp-WFRM7>)fNUrt5<8O zNck)BUO1;srL~P-vzD#5#;JI~(1?4T1%)Z=s-*nf$zslV;z z3(b;UM_&6^Q;|SRoz)Hc39nflD+H`PvL-Io^<8by<0wUjbq{;%Y74d3CrjAOnxd`X zbq#CIL1g6YSL)hyNAFmQja!t$&^F8L4u=eHUu~i4lUvI;tg`EexoSg|i>ljcwIy37 zw9JKrkdska1bIM_6pw-AIPet#HC+$9UGM=e-RE1>37gl8Ig+yW; ztC?Jmg+H#gkk!dHH;=bd?WvV5Rfv>S$c7ybCE3VfXxVHQi{0m;^6HU#VBzjwZLw5r zYT1xXrhe{YphaWkv6>xYz0K+BR@~{D zRpBRF_R8Iy)szmU!sEKEM58X_tuCnND7cD#y{es%d zWtaMHzqv4^;>u##sdEiv8JoMYc;)JKXnGx8LGLIpRmVzYK-DO>3r&BdF)%MVwLxo< z)CZHd_0g2WWD(v(7L*U>@v)IjD8~9>IGm37Vrp5ilFgZA`L23EcFb8b&SXc?D@Rr3 zq$c7J-MDI%FBJC6GDCTwo7CBws-;q%Y{Aatmgz0p_JWP z9(l+!p1Ml*_*w^{k~@{OTPhK%lpgvA!U4T`e>2O@l2Vd;#D5e=M0bnWqz>SRg^v@| z_{aI1c}4ODek;BieGTp4+_dnG1qJ(d)-OndseiX_+2572*tw_sSu6pcjqweniryUM zlqcs`R?A0jyf=G+U3>ATvhSvU<`}1_zV*rbJ5rWlVxxezaoZfVm?tK#7Si0D^TyEd<_t?YLP_P9oMpN){dYvTgHJlZ_B znEvU#?P5kw=WQk(*_`S~?zTB9OW$W?EuCv`Y$KPp&i)km`Rj|>xTYhYxxYc%c-{RS ziMP!$HBM|?d#r0?dogY4uGi2%%~xN{-Zg!?ZGWA%clmyM(Y84%T2Jh~?}TY*V>=OT z;nhE-e_q>UZ0F_sYqXup`|adxo1+@`#Lf$k(rs)hhqh5;qJQdrj4j==zdAKa7v?FH zJ?AKeNmFT~ZB%}Tv5imOUzwgX=cuwhF>x|!Za5>+wk@Bff69W4E!?`lO!wjgCZsuP za!*X0)b`%EpfXHIPk$6`?opxSph z^0`o@Ouj7E8xl%Vq^ndPc&I?vY0FHETqoE!tX&S1)01noy<1C+?cKJ&NZWfsu;XoW z6a$~wdqJ>oXeUfcPk1?P=M%rk*v>2V7ic^6_Xj(gr;7Ob@tLYO>ZMGonuKAn@gRVi z=`p{ijeT5(v9Xu$&(p>}U}l=54Ee;^2h29Bt_XmUD4i z!kE|PWWi26+I7df`h-Fs?~mQlY^z~XWy#|Z`DQYcNjhVmiKUa}+=e#7%p}gxHcG^d zZG7_n?DWhuM|tz}o^R(;-Q30wr)}fDg8s?-EMp6|?9WW~qG+Cy=MU_~L~4U%#Uf(e zy}y4W{qx+ei`h7xpq?z}(zJ~i%$|$ed_lWrDWbPoRW;`lDMG5!mmTBoQYsT!q6(Fq zMx89VZOWXb-E-V$Br*NJ`+7lz!nLRhO zS4dm>pSQ!$-!Zm#>;BzSohq0oSMa=@Dxl5#^;hBNw;3Dx%>Bu!PUX*&4Eb;9RQkc= z-e2VDpT86_ws-6P8>c#zw{4D`k@I#ceV@Pg%)8*{dl?(~%>6e^bt-qBoOFLfr_%TN zdw=i=`sWYd$JAbO{-2#gr&!W+(zDVt($msY(v#8?(&N%&(xcKN(!0aqj z+LRWhyQMKHCACX;Np(`0bh~tmbhC7mlrLqGd;w=AXC$X3rz9sOCnU!u$0SF|zW>9L zLy`lM70F)7kn9~ON_Iif`xd;_LV_{&xNr{$~CrKA-G{I7d!h zoZ+43o#LJ3o!}kk9pfG49pN439pW9}t?>5phP)=P$lJ|}@hG01w~MFa$#~m&TgYzD zO*}r2#XZM8%RR$A%{|3E$vwe6&OOFG%00q8OwLst;I44@a);a|x5(Yijd3Ziox6*x z|IoCf{)4EqF8Dg!!0-&Y`pD3_6WYiB5=)i;jtoijIg5iw=nnh*m^;ch(*qZoLxLa@=u&vJh^yc@%ZAg#iNTy77s5TT0F40 zvbc9~xY%4QF7958kzFSC#a)X!bP}CF$I&r#6dgf_(IIpIt)RVVh?=N~cB2@gkR9zp zIwV8e(H68BZ9;s+;+*50C8to4}9`+tAOa0d7+oCaQxQ@{k?4UFR?FotggM)3{62!0kYjIRfVZ~_>_abN() zfPNeWQaA$i;V{sPLqHD>0^K+ObYVZxi7B81`+#=r1=_F&XvJ=z1-pP|>;#&y18Br{ zpaI)}yRa2_9ku|kC419ozg~k)z^kzlcojAPuf)56dVC#Fhpz={@ijmVz8a{;R{>S{ zN}v+!feNex%CQzG!y4c+Rs(lp6>teFfzQMW;0`PYJ_E~uSKwvfcDxh#bi4$-96uBI zG`s`24L<|;RD1>SGQ1tQ6+a!g1z!$)3Vs^!$#@&^N%*P2f5MjmpNO{tpMbXjACI2` z+>Dnof@Ui%xfRDjX1U?!+0r)8Vc;F-PX5b_6 zfs6PNKq1})6ksWkk0n4J76Z9h1jKj|h_DdI!2;j{<^$Q72V`L`P4ricfq%gW_-D)k z{s}Jt|A^VZKVTN{f6!ln=g?n(zej%t{to>K_}}P{z~7=j0RId95AZkW9PmHU?}2C0 z?|{EX{|)>V`YrI6=)Zu!K)(V02l`Lo&(T@n&(N=dXV9;JKSjR;{sjF3_+#`Rz#pNX z1AmBq2K)g!1N?XNQ{ZXz6X5sJkAdGqKLUOi{Sf#a^aJ3x(Z2(~g-!#%iM|g!g}w*; z2Kp}W-_Un}Uq{~t{ww+x@N4Lsz^|fHz^|Zh08gTS1AZBO9rz{mufQ*&uK~Y+z6$(2 z`U>!K=p^v7=*z$p=u5!Qpf3VHjlKZ<6#6{yUi3NOC(&ntpFk&oA4i`79!H-BehhsI z_)&B(@FVDxz<)uX0Dc&K9QYx09QZ->G2k)uQQ!yAM}Y4~{{nm;`Y`al=tIEwpbrB7 z865+@8+`zH6ulq#F7!U&JJEZA??CUNiSNPh2EGj+1-=!(3wQ*-6ZjVV4&dGR?Z7wV zdw_T0w*lXT-wGsqYJqRSZvh_0cLQIK-wb>mz6I8V;5PtYjSmA~glK#-hmGRUy5G|Jb+&Td!{y}d<;2-n`1pXnbO7IVQJp%ur*CFr^dMyI~pgR%x2fYS? ze+a=p=+y}PgIjT-K{q4t4|+BN|Dc-?_y>*As((gk5BO(@ zZUzp}vw?kd6R?NIz%CjAJ7@@OqXDpm`oJdY0UM|btfLOFhT6a?Y5^;#2`r-qu!QQs zBB}uks0z%Z3NVMtz$_{OGpGnmqXIC6^1$6F2TY{8Q-#pzT;J=J zT~M!^B6^Nj0FVkD(u>}gT=n|Pw$1wiQ>kZH-+4?Yo!#&i2-Ks=)w8)a$$@&=#lF6q zeyvEi$~5X-k{XsYBbi}y^oE5_vDBnLg?{@+OJqwVb1_MNl&a`HdZ<{^9I>i=P~?2R&CL!)N*=Atedq} z)iRaTkxDdXEm^5C*;D0mQ!y&#Eq6oSRJ+(t?WL>)} ztJ9SRMw0By(lMjfl`T6TUUs3XU`m!%ol`)M%tEF?UuRGYs zW=%kyvxhoEf7TMKL^`r%^^Sqwz8uV|D!O>dLXy`8mrYTl^}$ynuyh)oq19OR<#ZMU zsZXS=h6}oa#gSjGIBN2kuIo)#<>6|$VejmSCE~$oK-~)X(_y_L?+EK#`kdA5E0L1& z%6!NXRkdw7Z!^!V5&>K~8SJ-3_owN>{^G0H^SnHrJ0m*X&FkJ~yp_H;Z!0L;a?dE*)&)~aU4|qMOzGCYjm_~6cfFDz>)*yU zd|gUkYiGmT*z`+iy49t+jVb95n|T|P{|)!C^(=po_c66@Mk!Bcui-P19s1gGc0_Jg zjoJ=fx}cFKWMNBM*BW|4%f*PLQR(Gt558WIa);8XFX>uwpSqDCHGivil|0v>QpK^U zTv3*kep9`isF}!pDrXBc+96eJ$^pRtaMOl@cNQYRyL z^)dR=NoYTR&;HbE`9E@|XpR9pcWl#nmwQH4q1E$x&6RLS<+REp z&P;qqZu~dY|EsuUUcWjvutxd~bJ3-+q?Yx$id|WhdozlTecaJ2)I&R&^oM4z%GHiH zTV&&(mvTK@C3;(Bp)Sv0MZhbgiGG;zfJsX zu~)oB^c&H=qF0bNzgLPxi{B$}xIe*r881)XT?@J2;l7i+l@4&XkvGqu!*}93d8hnH z^dt0sv_kR@X89jo@9}hrbHl<#o;@_vyX;w?*ulA;eKF~&yej+|pRAQl39~*qOIkiH zeeW#k={pVU)3cXW=)obo#AD$&WJ+-nvG)sC-k9=^J^qL+yHcNWV-R1+c zq}TM{`)5h7x!b&NhO}x;559Mn^qLX)o>|gs`um?}Nv~N8ynB}P^e|x^oh7}-m+!i` zv{Jt2Dt|Uhdd*dynI*l(^FN&>y~guDnI*l(^FN*;En9PyKbj@I<|=85o}MMWCN94}OL~oWzBfyHjd#8~OL~oWzH@PDg?>$3etVYm8ee{Emh>85 zesh-e8eg88CB4R%-9$e2sqpe;LceB76Kdk-hp` z$ew!{*`vLS?6s!Ij``P;OnvVVsQDk^TXN0q)fFzc zZMsmstJLrG+rqs;+8!Avyv4DiscSTy>b@t`G&ZXPmqMY;>`bldwJ_2`brFEPir89MPtyEgo zYht8#Iy@$Niw$>eursfy8HT}<%VZ#FhumIYsS(vj%%tv+P{s9Swwft9lKL^iEH|6TRk;C)?HZN6u=w6VF(zYJ)4F$W=qi<-8|9Y6bE=eK*&w zMVIyNUZbqbcJn?9?O&?5Zx(Hy4Bw3--=;`%_PD{yt=U}zwZS|vmWSS2T27XiaYGGv?4SJ4NdmCYmhKIeD;EGr`uApgm(rS@_=jX3Uw^9YCSyam*Qrv zJKvzkmAbc2&q(h@=Bv=Mttz*x%bwxDmDW2e8E0Bk-f12-Jv+m`oto&VT$Ohm-S~3Y zP>TEHhKy_M7!kcYdi!*Y^j>7XstoPXL8sl4`^%J}5i9E}ero7*v=n}MDAx_O%l2}w zXvnGaqd_iaaBETtbInHdUeVj9Wu*5a^Ttkt+|w-%My+ZmJV@6ynP@%`+v)7*{e{6! zUENVuwu%a;Qf(*|qn3W8NOBbh==quG)iBa~k$KWma5#f8x85{t8uIC4%bY4@Z9ZSZ zxlGE=R$b|wVVJT|MvF_|DCT9Zv3%eNPOewYNbg1FjamTh)Tzoi+IJ47m=8BuO!->UIjqc-2p)SyD&j~?CIr(mS_BJF#cj`7$9}gnk@Xom)J|2so$}S1>0T=DvUH2_o%W1{wkVgO??;c;?URdc zV4Yu13NEso)J#uTt0v1BnS7Bc$2U&sBkrKh=FbnEEngyLE!fNCuHw+Cj8=y=Rni3Q z(Q>ZgXf_QtuYK5Ur>yi8E9mW8W~BEbQ_gZi?^D#O_PU`WZyJJ8M>-|*`)bx=!4{O6 zyRL2^Q;5q+QckPhn5#D9dS$Ok4@shTCnLQVnR1NQYO`PM#&<@7l#LWrw0jg*YNxzj z$>;1voyXRRl=%bi4}uOW&jIxpH1*nR5Tnm7jQ3NU)f6$}v#JxSQ+#9zhG6PFbrmA-HflkOi8m2t8o~qxdSPUJD zKNHlnZS+#*S3m0QdpaY%({s+`dc%Q6E;Z0+{FR_g6By`!p|0tN2*6aFsLUl=T0 zyMWnWU>{^Zi~UsA&sp!Lzd4xskNxSH|NG(p`G24Xbm)ey7um0&)J~6agVAdU+dMX7 zn0?o*hKlKiCUewlbJ?A?AiFcO;q+b64Ko`~_l+lFFbCXbpUua9{mh2b&IlU)VRJkb zG&$L?o8^q@<*F+XO<1CCkIT1fmcBJl1eC`e3>(cPB{TcAGaF9dUwtu)(VMUuf=>3G zGaF9dAmbkLKT<@_)iAw_Pm(6TAJ>X&{yUpVE z`vOk(%V##69&j<2*BVJUO#uu06|)B1bf;PjZoh|exa_{`X6akA9`wcyaU(^=jRE$- znGL50T%1aTBG!<@?Pb4oX2aHNmSxm85H0%#sz3c-s8&3BP z34Bw~77jQa?AvEHoE~r%i_c4uS=D7{zhq{^=>bOq${8l-jsj8ki)S{R9&mQQD-@0R ztsaW~qL~e+2b_(R`tca75o>_`f|(7c2VBS>HIn(*VQ{ctIBUR7_f5i<2*w?5tIcrj zEPZQU@jIxH(`oS<;z9QQnGL50oGVV=J~_OWK!Uw8v*GlBGg$*cgV&#khuP1c*>HNm znH=G`IphuaDE4hL8%_^6@`U3KQ2vnJz<%D$hSTH5PuYzDyVLJ+v!6S&;q&%AJ?)QbgR+ro5Fq+wWXEvO6zk#f!j8r`Cwz6-T*>KwZK5HQ0a8Z6okiBPS!)f@kl(*e)i0U)80r}gE6CtiiE=Kn`Sm#Gj5D#QdGtt zwo&Zy%!bqB#^m#Pz2TVE=Vp&)He55`SZz*^-4P1A*~3`_ZrW0F%| zrU#tM<)I=8quCu~4`w!;9&qM_%N7igH_kqGe`dq!0T=a~+@b5B>%3xtC7hIY29@b%l(hAm{w#v(bJ%%d(gyzXGE z)gj+-1`FjuBha%j)^lOr>|4bvk0Pp`e#2wtsZzpN)#~Zli@dN}^Ke9WbZX_1v_0g~ z=O{_S*q+s6_qR$`9sw8qxOs|-Y+U@{zRbFG@`;OG^z?nNnoIR_sTDlA>N&D*Y@Ehm zV$kHO#Vcsy>RdQc$h6y;ay;#>=?00uHy%-v0h_C}>zPcbLz(h@mpT_n`YjtDF=ces z(<><2)wLB4vFAGC>N$>WGPY-JlIz^c0?|j$5(DOk${Fjsf0ih$u!$ZzdgnMC&sfh? z^s*){9WXyS%UIRy=$&3MLiFBt3vCa%^h(}LUUB=s!I5A{_L^%RaJ^BBQD63So(zUb+7IepQE+isrtJDTcdi=n>4Q?`5k5mUMrbCF_c2D!U#B)c~q z@mxL-iqt!1V?o8(o4Z*kdtQ(xB9s zhp9+iQ>!Jy)l{JyR5znZ17l5VBc`^pX`*kAdOVEh$+h}&D^jA5j?g*k=`hxJVT4L1 z9-E^c4r4u25h^CC==&?FB3dhi8s)5MP#>fyo!J(y=FRa|a;R#R+}46A@~ zpdt{XNq7252_kewMB78Uc#i5dj4ypPLYpg#M8_3Zu(r)neunYgY*L`6+8||0ij7Y9 zN}Wz7tE~u$N;)#;C?>;L<$5DkK=jcgb&g^%jP>0=Qp+oRqDN1=D@eZk>jzb){wP58 ze62_@;fV+A)|S^?RONl{b|Y_$6aztXX24ia=7Hs23M)LKjJ|QrQH+JLvZ=7(5><3q zT+)nbjKX62YaqI}g`yEF%=!U~a|fruxtjBcg`X^Zn4GfDEnK-EUEIq45&MJW^!;Pd z&(X(7Hp&vZmgLRvv3|f7vvqtc|B2+>{-=1a<~4X`-s8EyB9#IUacf*7_i^|a_~XLg z2|q9Xiui8vO=1dHL|)NTNiO#r#spW)xhZ?QA% z+l5ZyQv|;id{*!}K}ULpKVwtu&^~_ zt3<5LQg%f40EdU2&e%5f%!5gCYo?+kA2w4_l0thT+OUm$hC;kvH6{$LaXmGhGC=qu z;R&XqFBX1wRWv;&-*V=xK~LIb3HH6|VRhNr<171eUCWGbrR=$VSDIXb4IDvp+^sFHKu1snFOg2UJaa*k@% zfJ`T9iRB}f$S~V4c}W70?9><_)g%H;MK9-5Ohuo{_e^?V!$C+Aurn1UIog?uk~Hp2 zMM9jpu!7L(J{FP9TjBTI6ZNS!+Hnn?My}A&bo)G=sm2rF%^9q z>#a;h-^x0|RP+ezElfq=9GZK6?oH|Je(y z#h3Cq=!N8$|KI()4eu|~>4jhPIQFIe4vlFRd+mA|N9Xt=icV!ZPZn^HpX1x7_0tQ_ z@%7(&8AsQCvq+l-3A#&JIyd*mU#~$5*9}t*Pk-q}x4JOZ@H&PN?VVz(oy$HT^{9q4 z+#vZq{n-yP6{$w2(`rfcbyJbXoG$r}Mlag18EuJS)}Pu@tJrqv^y5g}9o`{##jIg_ zK;G1p2k{4;)IO4ldY5w!pEKkbRjPKmKT#^l3YkI9(+%zL8QSLMp1k2VsTEOcyjk(o z^3GT}->I3ho_doUNz|1GV_DqUmS;ygn?07RntVGFCc|Yn)2Y&X^-if!C;ds6C`c7@ z$!>>EwyP#}2UK!3TsoZ$xVygq+MbCP@(SAyYs+J9#+UNh z-5tbxd2?MJeDHA>rHrGZs-6qT{WYJy9g23+>d)OY-Uc4G)M$x=6x97>ydO z?4r>)NCdUA9gcFv>$C=gW8c`6ULJ?6GID}hvs_7NEIpUfTP>z*#qfi^3sSX&(O2nZ zA|q$arnJcny|7%JDfQ&8Mz7dv7+hwXCs(XfW4~uvuMdxwO~qcO+8Aq!kwnv5uIJ+2 zuwywRS49=whAmdDv%cpu(y0DXs^yz(N zbx4(`o%BL^=cJyKn*P6&KRw{O;p6WAf9tMviNgCOre2E^#TS0|YE2FJBW>fVJyS-m zmc6;(aA==S(baa~*GefQ7T)v)o?uH!JiimwX7l#7%X3?cA!< zAGl3}T-QyP<+2L3nw*PGC4E^#gZ$|-eZ8zuMzXO{S!+~|l-cEMKpV+biu$5UBQwOq zy+X~FP1Q@OidpNmcMIKi!tE|tEL!~_mdRKY;YuRc>XDKFov=<@A6LzZn9Ibhk`#UE zWV)fB|F^Qf#*)5*ylwxu#4rAycr5y*=sBV%F5W{<_PD8ge#?~ufZk}Hrb0OSZTpN z4%W7yzBK4-)0#kLoGBJH=H8$rx9FR7L$)ZZCriE9xNFQdHJaGCu{Gw+4>IE%RUO62 zaZgjKcM&Bs_u^@$=Z)G*lh{J<0kULe)j_9I%v8GGXf+pe8hzDz#v1JpP}PHEZ(i?iR?CS|R*r{oy+ zbhb{}!C23QQ{}CKmDmWp=6R^nGr zC=$w<;w?{NP_g%;>j%l~3!YwJlHM<$Hzw!zlDvq1eJVQCq$)*4SYawqU72H$?L`$T zSJV;iY12Nro~k@V#?)3y#1=Z3^5Im?WA>OWnqajqcX+*2GaqUx!)j|s(eKDTnPMr_ zRVp<;xs0(b>jhJh=%a&47U?xqii9KScB%SFr>fQ*Qfg<^D{CqOy?)n}*R<>dbA4>B zIT-7^FqjHN4;@UYcFJ1}r&IE1M>X~rNCqZ%*s>IJJ(6I`OpFOQ zf^CJP<&T9rHfzY(@QY$2Ir-3(Z!qqd^@1ry^wGieP@hxUP4v*g z^pKxZ609UA?lcAD)v7sJ9mkXEZpP#tR9mV{#P1u{k}7AxQpxqpPF2X{=s9JKyKtsE z*PK&It=vf4LN5CuJfkF7xnXj_iMn3Z8CiVVwlbKujN;?G)7DD&Vrs=$S;!~G>6Eo% zuat&P+ko*#?b#PReMX4{)3YYV6hr2o&toWibpEB9z2DLr)Uila<#ptp=2F6~7>sj; zPBoDjS3M7rF}0QJi7j+6{k;w)B#1sbm>%jw32~x_4yK3vP=a73HgV@a{!l_{B}&^u zF8iT8kPsm{{>}#y!bBxKO8;I55<)~DJxb?zBf0U*#mUp&{p+XlN|5OJyB$af5M^{o z%<(NVXq2+ki`YH{5g3 z2?os3t(W$;zvrN1ZUZi{3+!_naEV=D0|Vw*yHFFy<#~WxS23$S20he?ozUSaoV89&DNSE3LSI%v~C3b;+ZUZi{3v{bv zz&UoQO>uY9PX(RDCCb|Ixzu*Qyy?}N{qBG{YwaeQ0l6jKbX;l|Xy-QI61zY%w*i;f z1!^#0jxCIr+6Ah)4Y1pXH=}GAc>2c{X=~3ws z>0#+1=>bw3aIbVIZAy#M-O`wplG>%aq&lfgx?Q?Ox>>qO%9pYv=Okw(XC$X3rz9sO zCnU!u$0SF|$^FA5d*A`die#^3C}~QHlHHP+gp$}LyCgb^OtM|FMY374Ny3+~#OK6k z#b?B)#izt4Nu`0~;$z~Yq!!^}@gea6@rrn_cqnd)i{jnln3xjV#k<5hu}r*OyhXfO zyh+R#vqa}aXGLd3r^)HrlZ&#&?TcF$H!p5l=Y(g4XN0FoHN%s_6T;)dW5T1t zBP5T(A>jexig2%RC~OLg!rj7{kP_O3yM#KSOt@XRMYvhGNyrzn1m^^21!n}O1*Zfj z1t$c@1;+$O1xLuPh(m${f)&AD!BEf?6a~9U-2;kbGuS2231oupf-Qp0f=vRxfW<$@ zKg&PEKg~bIKgmDAKh8hKKgvJCKg>VGKfqt%@8u8qO@5KTn;+v-d^>*^U&oh`6Wd$( zoB5mgd_IeJj(q!ansrpU9T*28IZN_0YWoYa;)Dmo%MEIK4Q zAX*Xa6%9pAQBky86cbS*yJ(k4Cz6S_i?)b1i#Cb)A{O~3>g?i~#nX$Y7Eh9zl*bp3 zEgoGwvUqs$5UD+}vbc9~xY%4QF7958EmDj2#a)X!bP}CF$I&r#6dgf_(IIpIt)RVV zh?=N~cB2@gkR9zpIwV8e(H68BZ9;s+;+*50CEseD=A7c3g!d<|B!`A`7 zj;{s&E4~K!HGDPjtN5x3uLOPt>wzb+4)|rP1%3%@fM3LF;1{q8_<5`Zehw>upT%; zAI47weh6O%{2<;6JchRbKY*VCd>?)?@csBn!1v;R0=@@75%|ye3BY&b#{-Yz&A@lz z$4&TH;5+eSfbYPM2EH9X3V08GB=Bwc5x^sO6YwoqI-vx3Hx>imj77k^@FMU{SO|P0 z769LX`M|@N2Yfx|PKbf8!wC3V%mLns7l5z9Y~ZUg3-~JZSKuM^mkIw2d?or5@D=Ef zz?Y*x0AGgw2Y3*j1Kxpt4}2;5-Gu)QJb-=+dd?%TtWW zzB=JoCOipjqb~zn=u5yR`XaD_z5uMF&jV}da}$0RSVbo${0y*yK0V>5fMs+qu!KGd zETT^U3+Us(JUR}{p^r`YQD7E*WWs*|X3&Qx{17mWJ_t;qW5C_$1HdGDKk!EMKHv@L zy)^N4807G^_}%c|*WjbTtMR*lSK)U8uf*>F>harwT6_;sgWn2N<0C*7ehW~E?*=OH zn}KqC7f^=Z23*E(0`A0b1TNt>0H28u19#xp1FyiZ18&E60-ugw1H2r+8u&E)D&RJJ z2>4X|O5kPqwZN_T6~Ha{<-n)lmjR!Q4+5Wr?*Kj#zZCcc{1V{f@$JCP_{G4-;THiP zi(d$Q41NLd(f9!HQFuS_k$4682z(oG6MjBWik}CR;O7Fx_&Gok-UnR7w*rNDFHLkk zde4OaJR$f8B@p-r#S!?2tOdb8D2l*8D1yL0D2%{AgwWq8guqXP;3pJB;3q=x6AB>k z6Y?YQ6Io$`e~=G>e~=e}e~<@(e~=r2e~=4-e~=SFeoTiC&V4O_*!>!Drs-zr*Z@fJwZ#2gDRGTx{^bElVP zx|YT^O+OK~4C5Vzek`lU_1-KH93f+K zR=0}Ib0mRWcH{hI^BM2&o_*QVM+fFPz0h2#6LYSkFPq2MoV9y2=Q*TMT&WS2%d|=^ zW0m*s*_r2jLV2Z1R4Hjyn6au0chbyrNP)Pva^AIwv9{T+ojyjeZH|)+*_ASF4t?<) z#^y{$^c;s2Zq==nRxdk;7Z`7{o_*QVhY7aLah_qDn)z`9;BLZZBK zBT)s>_}`3GJs=wA*nCM`d&7Cx{+6+}nbElBAj3QxJhLm$qRpYB@xK_GGl|AIwsDeZ zyngkvbLck@(FuZis=my#0h72aao%NTA1d4D*tytTi4$`m8h_2$oOPmcj-7hNl^9V8 z(fBLIDldq}Id-I#SE57}MB^_Rt9n2*&awB3xHfX$wZCAjZFV$HmuHw~e^+)TOq)YT z<9{$VXA+Hb?BeRIgov6et{`fDzVUXV$zg$Y3pC7A3Zl6ZBwAtI{*1BKb;j)+yNXux z|J}h76$LkO+3Y(W+9&ykKYimKAPHcq=M%up5vUsn!&CwoI@?^Q+s>tG$woVyYm<6D zBqfDhp^>kb0A`NZfdnvfY;IgX0nEG~Fh>FyM<`nh`kQ4>Jw-L_j`W~2b&8$7g3gAS z1*fMmy48i51vgFrv*r^XP5|@wO#q`+>9u;|wsjN0q~c}8C{gqc4Yry&ILx=w8Cf7+ z&KXoYocg$YIU3USa~-|i+fY5|1TbAo%xcdshm`TQ%eHKCx7GDhZmbEl6?#Kds|r*- za>b5PU(-r!J9d9ekv7&1N>q!PDMN+PllVqAp3bmFNUCq>#?6s@p z1M0nMmCjFeKBba^{Jud-rHdu3S~Fjfk9v^#PPHn9oX+gHZsbwppkMD+G@Dwxtx;~& ze1W#Sm$uZRomMlhl`AU6PChZxSck^SgO0p{!RC#oL#jkuZ5L`PXL8hY84bE#@Ll{Yc)-F1NBak0;)84=c-x5VO3C zaOouSo>$smrXz2ElH^^i)?7|k-arx(X@`a6P~C9(dc`GcwG%ft3+Z-0?rJpqO95?P zcgZ48tzR2?>%UFk>U#Pg6de$deUcxo)S%OfU8bV#0E-%J`o9?u-V1|5PCjFU zprF+}*uhS+#L#Kh)*b9EwK_mi0iOFf$yHUfN5%e%Mp@ zLyB>zY>3t4&a%4_&AJk0mCVrYYx5*3ndKSN(w9yK`)!ZjU!n*5i_|2!ud=B1j>4l% zz^o7D(_xFVmk6W+Ey^$$7rT@pX$_klA z|NXOU&+#O;-fe91L^>U*Rztki9GC35NTOFT*;1wQgIWMjzf6QcY}C8CI-S*zWO}Za zQXNQ#cm2%#qBmR^iS%R129Y?GT8}h%uyHAu>FLN{H!jDetg1L_#{(+OAQ0?iZAwET zTPWBqW{<&4{xys}DwQ&X3e^XH6DpNER9#)F81go4)L7E{o7swA?^kCH<4X1avG?ZT zuB&C;@XDI!&CCG=Ik1`B*4=qvPMfq%(eH0hKw9FF7JTM=Gl5)lv- z4>PDBBA4QfpNjgSUQX}(f~X)b=s}SWaQf0_9pH5BwVEP+_+6Llk9F<6?%#bs_w(c# zljr{3^=_3_9k^eH3I=81&FL_&vkK9cv;jq58zEXs)3r8PsCfk`tu*qm)TqQAsThg> zFE_7xFfI@G-?K3Qp165f4(_}k9(?BDBM0x^+dg>N!SH|%Zn8go|6lh1*Zzlsd-~fa ze-qrxf9uH)p7eto_{oz89e*LXYyZLH*B!rTliJK~-UfUb_zU0{fj0o(wfFD4e-qp^ zf9viK1~`oIc>n0rN56CQ&frEkb>tpp_n5t>?gjV%55Ii)7l*$X-1Psh!|s*8yz*;T z-hAce74r%nL@qqw^z)~G9Kctoq-nsrM4AHJOQcC)aIrL> z2Y%`jX%2YvCDIJ=6PHNSz?&|SrhqqIB25B6eu*>zyx|gQ9C-c3(p&*}-6hgI@MD)q zGr*5tBFzG?xkQ=$rsCVg^Oj`{Kc|N?qXRwdvT*>Q@~3u zk*0wkxwt0PzxO77#9xW`O5jB25FG zOQcEQIhRNiz_Txr#)0p=SQ;+?&$>jK2lz{*Ie@!Fng!TPq#1y@M4ATZ3#I9J7Wnx~ zq#5A-mq^pV&s`!-0q?s+ngrf^i8KNH>?P7T@G}=mQ-uQX)0aqdzu?H(hShMM4ATlOQb14yF{7<)JvoZK)FO32jq*}F`2$t zGf@D#^NAREy!?XoIrdT~HhG}Gp1 zwqq}6s=vw+?Ivmwos>)m?CptcAvPizDl=3&?8J6C+Jh#BPKosls8Tn-WnuOq5zLCi zjHwAErx5~NNESQEo@Gwkoe@D3qSiKYDjM@-U&dI}c5L(ZL3G z*GgtnyoxwhF_|1#tumjJ8ii7>SsYX|Aa(QM3$qW3V0J7ugqeer(x8|tj3>-wG@5v} zkj%~CxP|zIbWxRUBuOAf&0a_{gSAqCy+}t zBu{Y10i(E(DYdDaw=K**Ac9#N=gOrLR0P$Gpw*J4gbF#;ep9D7W>gX_XUh1vT> zFbn2c1Y^)#VpPN7N`DLyE}Y7>iyYCR(k(Zx!gbq}E0dOlQ8ypFFnc9}S)!Z)wUmq+ z72HbZB)AHX2DxFbI>~$tl{@%?B>boBFR?e1E1I}`L6vNr&23|}_ z22~4kGqJ?1~FT4$A1sMzJfRCMC0c7PE*dk8ne^nYA)tv4jqX zrQng>e_=L4fY3UzKGe@`t*4vM?JV>}Q?m9_r@t!fb@lo^>L5 zsGCO%vk}60)`{7nZXPboMu^v0C-jE8d9W}WA?{|Kd?3`#{e{^GVK(bT%uqM?7G{Cf zaS2zGs-hq>28?N$3Tri3eOTjroFZ6^uexoVH{wtyNNQ-YU@+C}F3d&m>D{Zf-5iMhJLWCx-`hb8}%fLS)N2K`PWuU|}{wgvvUp zH>jH%3$qadQr3wrp>Eu*TPY zZVVS@BLr5g!&yz;@D^qx1W~M$nS#3EF3d)VmRRS)C3R!4FdHH2VV&D#^Yj0sHhyj6 z$_r2b<#ZOX@Qsf@f9wXFYyHE|9QFdT|^k5AhgLMup zQ9Kxj9;|U*S?3V4`lNKnU8B&0HBvIJb6$z!!7%h-jYN#=9Bra_;DsKnk(zOxQ&1ES z+|YwHk~Xe$V2a|wAoO4j284CaR#7}~LJ!sl!5UWAIa)>Wpnui| z(Mb)~IfX^>zzRKBgX3VG16vdi%+P~1s1MdT+ePuf2t8PX4`H2SU=$B}p$BU)Agps@ zjN*YFdaw=|!5XK))o02(?$SaJ*08$Hfia2)YUsflqz3DpEu(m#gdVKHaj?!YG>Qju z=)oG)2kV?zqj=B_Jy?SeVV%Qm6c41(gKt_gs5|ZwLl4%;2DQf7cJ)d5j=O}=gEg$K za}18+!E-|o)*v-l=foVvgHGtd8XO1f9Im5y@SM8u< ze)dbDQ}f81;k4h;yYn!*L~0RAdaGEfZ01(6RBH^PMJ&}CA&(3E8bjpPi~?)SwYQ3; zvSiN_RCzg;>i?sdVA1aS^T*x8+RV^J2c1yyHI-gP)%pyEFzr8vs@WIbIaTd@IMl&XJufWHJ|qKThlV za8Mf=%~{2*#04zXMSGlXFakms-uBkSof^-~$t2OPKiVl^+ zz_qm=B<8!7p6glH8cthk3WavYX{DRVa=b~9nOe5n5~pSDdcPudIX%x7uTLpCALpis z!p!BpFnRpvQyuTk^LWDO4pqlzPeV;qxZk#JZ05? z(kQ@7EUt=}FmAm&@QRiXhD+cVm3SaL)%w>}oF0meo5H?o;o1RC~6RE9LT2L9EBI%?}wtY;;M_u}Q!22@qX1dxJh9h?oc>TP`ECox?+uQ)L8h0aJcb?H9iF8!Z7?y}j7>7VjFA!m6kGZ^H?OsiaITt|x& zsVSAS>NqL3ax877_zIUd>p{T%*E#O6rd2Ppt!u@+C8RW;=v57ywJWa8c9VJBFB6`C zAfsTs<&;F(a!_kxUzhbsmyV}uWzHMM<72DPYMZLdPwZZ{D&%koj28btuX-@<4zKh! z(2d^!Ht*V`xBhzTXSVL#B7!V+uiSdn$>gNG*W1gV{^5Qy$U^s#&Ci^^@zmM83j71` zp`Ab7du_k~@R!H$27UwFpMH9L@@K%e?c9I+f1lhPWE9-p{J`$h zc8~VIy#0>t=LOj?9=iA1xd*p66fgZLu_bR_^(c<5)CI6tGy3t8f{)cgZrGj1!4mE@ z(M(E3D$-D6+@XLyijPyDqtrlCf*KP>X`8S2IJlVY#p4SS_VWh%!g z!sjdv)NQs0f_lG{w2wE})&SPl*jQ+|(*iSqX9`m`UjWq)l9R~IzMc8`?BUu`$($A`du1C2Q%h_ml_Y8b?TA$8g?gn1#}URerHsV7)R-w- zfDH9gMO<8K zHI73Kw(U}Es?zeSl?GE6^e!erIVE3UQIDn+hZ3@s?eRI7Xz8@DwT$4mK8$M4{gy6QyBW(w&*~AZ~39c5MwN)F^ZB zO>f4mL^_@fxOE^c+sbL_82pe2goUi3asRb79(bk^ef@YssFBhU+U+LtR9oT)P(jWi zNGY9Q4agW~t94{tDxkA;5|1a~hrNDnjk6U8lCvmMuH|{JqKTb>@3y2tmcggf+%#_# z!CErsbeL%{w{Q>rbXZ23&DdF<8u1cQaLJ&9+4)JJqT~tK0Wm zTjNTovBvas6l#!G3<}PWN=qblRW)J`N>5|7p~M-im{a{qL7z;k*NjrXjr z@$OKA@W+lWWonax(lMEiB6KGWp%xoc8L|tux;}xSczi0DqxQyI*4DUtZH=Eg)4*Gv zRo01uqPNqgG@4dYF}TrnJw(K$dbW?0g6m1FKo79}pIKYutaXX#Wt_DxQL~y_9Bdbi z6zs4Z$U7>S0p&PnJA^@_pl%gv4Xi{b+I!72ucB*^Yil&lG%A8M(9;cRI4*f9B`wCr zVh=Czbhbg$*}M!oLdELJQxD$r*4A*>))<5u7*iOf(w$L4KrP;8@ha2EHHsiamocGN zZprBw?Pq&AID;L$eQk}mt*!CaPy^0lp23*)fh72vI1`|Nzq(2`upHS|>J=$b!}CsY zh-+?q@2P8RJY{W-C$GCtynS7TM~6P~-6jayF-$_L)gCfHV~w~gmQux>;;0Z41goI^ zftAP@uJNGn2{jn8ULRMG7)IEceqK%Y@{~Qw&f2X!H^quQyu+ocb*dB_a;Gm@TjPh; z)_8HKvBn-;U0Xv5HP%?0-wb_yMH)8 z|DQg6_vyRG+vv`p?c9CkwZOHV$=0{-z&j7z{`mI0wm!M_-mUMx(wn~#IQ_#&jdc3- zjbtR?S3rvg$k-I)P2hn))9|qw*Q64Manoe4CUwAS*PN6PDQlV8ENj?@Z(3-uwQADu zVXot6Q)<`q8m*!=Na4*%QpFpxY7H`RFJ{|xUnTcr3k_Py^>N+Fpm-LnYf072iZx*> zW=VuhkHijLPH+~)o1_ndhyQn2Mhvdm`E)`n+Z>BC))36>clBo5}i95v0#mv+wqdmNs?p>?1V~ps#_+S{sV7IQtTT^@Q*F%jskKj2$ zDB+cU;CW*@QQTIwU#@#R27ye4E};hPfPA41A5=mOO{k~!+_3GWid3$}7!Wm{)zWAS z)e*Ur!A3!(zBs70lu~@}T?-Amp9#pw2cVW~Fh$;`2PEES>a{|$KM=b~B_+nkExt4% z8cure9bp-jj>Vb@j^Z`lCgXKyq*Is>!_^8Ed>d-au(+Gf);oAY$M(K0)EFdZgxJK) zii!F@iA*^Yi}QUs(cpWn5`uCihfE-bERF2_aL_UXZKhJOOsVfq2(n6+nxlbJ%tLL^ zuoGepOqDALSyV^UB)R*sMHy^8g~+*nOyb66tqSL3NvJbshT|HHv@%sePo*kgS%e{y zY43*HF@}ZY1elrOmQJ@&oM@v-&z#|~Fa=Ac;L$g#aK}syXr*i)5MdeAs3+=*(@pf) zt^mfTle+FW#qQLL)lyoz(j2#ip<8YkWe*6qN^Db)CmI+gqj-}?8{INBof^(Six;3w z+M;`-OtNcRGksRgZ@efhqlx-RhU2iB-)mf(jtB#q72=bOVD&gp9c2b4mLAMp16JF6 zzr4_(rC6M)j1jY%DaTc(gTjteql!W2Zdf$eEQ-F7iqo-@l=k*t8bNpQq0A- zzT->-WJ?otLO2A^DTU6g)CU8iZHwt}3T$A*D(=K46PU0>Y3HmAViX^8DZJ7wyBsTK zibJSZ_p)@xG?04NZE+Qoqf?1yD}!x(BrKzD(440&(NAbc0o@5FFTEhpivYh3I!4yJM0t5*a6M{Drp$06$O(Tzt=(OISbw;pQ zYQhX64_z|_7B!^?IjAqyj zOUMMv@z_SVpJt200+nV5q$k64rbHN7p$E}qhk$ze5akB_u2f4H)d^C9x9yhsdR z6R~j*;y}NcDD`5A!9ef!$6VDkdYu;RQ54IX-CV#2&XD5N-hAYuj5Q+4wjUpAj1uK? zF+Qs5aLe>-Fp+Qx*iN}_I^ng0;})84cv`K))bk*4Hifdr!Hg}XHPt8$CYc$J{B6ba#ErAg6!4=SqZt$z(Qc)Jba@wQl0vXxQ-@uZd~ru(y8 zwPc8lgtSHuEve&NyI^nqXsE%dcxC2KI+eJscr0a`PMo02sxh=0)=WiW6>dDpqQmOQ z*tE_x_)$LT%6@cs0vqdaB}K%s*bjEX)op28^0B5Fq478D_{qe#|fkBH|QG456c}~&m*z4 zl}O|R)}59da}ayyqoGE-(b7mhW;A1@sS-Y247}HxV>Q?>5H#LQ$vj_Wa|)=DyJ!6h zpBDR5Uc`E>dK1e|#R+fFZB6fWxk))z@sdWYC}s?BOxintvY4)zsxay_2jk!z(Hdv% z3DK&|+Fb?KlN~zdW`+eKj!Y@8)J_=Tag!;-Nh#mX;7Aj8Np8xyDY%`AmFrU~QO(J{ zK4y~8Nauth2mEGO1Wb(aLU#sPPQREY5(PFBb8AzKk!DaIM^$Q6%DPHnp2eGZKP@|Zzhj&X>+>yJYXFw;@MU_xpS;&`=_v`=&%(BPeChZ?zh#i?Zo zt=#Pfr&e`FNS>beA%~{wgW8^KF-Rl-=#Z^k7X8}hAq)!=1g2AN15-V7I7dJ=QvY>UU! zLRGRgH0#xC`GHU|aI4fMNty53c`Mb*VP$wD-15<`RH?MG=}}(lKsAYC>QvevaG2L3 z2xE|u8dfLO5>Od##<#<*5Xbajdi`#)-%HS<(QA~7R*tCpRXGh8hpN`eB(Tlvp@vzS z4GOtNzntmig+Vi0A$dxtP&D3G!4yj=IkVmGdBGPi54b1P&?Yr6qqVDzRGuja4GI~L z2ybGh%s83F{en}Lxl)mjX_&O}kD-Pv=?u{sQ^m2Yx_G9MZdy1!tS85Vu?e%Wf!U7L ztY8<>n(Q7g1{5ud+PGv=bgCr4SwA+ZB-=49$%#hCcWK@G%Vq`) zQLF@XX*t6aQaKqUU9&YA)CnpjrPKXvN-S%Pnclqp%0h(&1)nr>FrR`3Gan7;zi_8r zDVJ#+B8ME`V^g+*2h30~<{h2QR>2}zt-EMZG2@*HL_>|CGQ{W(NXI~~qjBY#Mipe+ z3P$7M=N4r!76b;{`Yfc=#h43PGaDz25m6qe`)1dT@u?(-^-I+e&67vX&?|{AIK9q5 zD>0^S2Mx!P6EvTqnmo(Ym7%7=rC#ubAA+R(=AVWdyv{T`1tIBI%P?5Ry&(lxO}-^i zw^=hz$@HFc$8qPEL+kFYO-%8 zY`#>S;Kh2fpmJ?l8HsACkSFpk7;JvVVEQ23v@;B;jdXDY`%VgLRGPC1ofo;Bfu?n= z8&f9JA;LBB`cM}pN2B@q|JlF~Y;0cJ+}ir#En}+${0#5|TaVcM$Iai~*Y=;ef7{+Y zdmr9=!`^g{+-Tfea5?Vvl4 z1^#^dpSM4{{kAKAdgZ4B#(nY1lLNl}PoMruz_#z6qNi6+{≪@<%7{IQia_XP;zG zPLKcQ_!o~~b?h7?$G2}j@90ZMe{l5H;B5nclsq~-{6uh<;1!4cAV0yQ4!#g%CwTM0 z9S7_|{9u3oWBc#lf9d9%02O!!@X(FF-}r;Gv;E#LedUmUi+}9>_$~hUU*brQ1&#`q9(p#F<&uaE#WKURmhHV@fosg{fgR+S zkYRYB*L}K0FX4bMhBy_6dYXn5K^ArWQ3dsvaGQ@^;Ap%%(1tc-_?S(0F=~h{<3e15Leb~E>kYc z6bORtQm+Ir;Wj?Kz^#5BUs~WOsZ<{!#kM;v4^$0Og>%oN84*V=a4m<0{h>e6C=pbK z!9LPm@^6y}?NzG^-6C3yUL=F-{$Rhhj6*_u?xZ_}{9z4h7ipdt$>;j>QzPQS_M^;N zN#M+qK@jw;44L)mQn|pBLjP9co){7Lgb*jYf;!SGHgC&Br93l{<#Hby5%-V~CkeL0 zDVSX=!%~lH&X(J6;9hN3r{kq^H}@XDMFO|Ce1zc$ORDH#3pW}i zs3Eh6sQLY%@4g|=2I+a9h|l{RfH*#oviTpOAhO1Y>y zqY{V{)8_JQ3jBK5%thF7OQzd2txV|vuc*i;qu_!c;BMf zCWqI+O1G&Y=A_A&MR$4f16~;78j7MARtpvxRfT(fd%2Ttd^E%bjb}Q+3$2>cLzPwo z(U;uY_`m{3`t`P|PThbKwB7~@PaZ7eJ{jVsgBfYVMTFO^Srw_c%PhRW--fupRd8>M@cExD+ndQL& zgo_cYPbyOqo%Gbv5H6W$`@YX3U=DxO!rp4!BNyF(Rc#SdgRg87)U5TJY#myvM=BaD z8I~#u=#=*wW0BXu3W|2mO?^;^Ym3mR0qGdU)_BMV8`4s#z#oLTCdk)hn=fh=xr(Aa zp|Xto;}C}{6KN(lOrI+=^^!0;*IRx!BJOv>>SB1)P&|obn?03tiB@;1+~-GwJuieI z18atqMWb3F8;Ijij3sOBXfP_QLj|<@ia?Kh(2%T+MvAax4G2dNGtz~@03pW3o(fZg z?r1rMFz~#Ey(C*h`4txs=@S3s#K$ia?KF>rJ+$?-aeS2 zIc-1-&8A$O)>se%eQvqqZQOH)tIJ({(x_A_)Nl~&!OqQl-xK0EUnfC&tPiJNok!`? zxvui!&^?By9ltK~imy0~-aWTEGtppl2%~4cw%n+?vry?qu3SarR+;oi zBefzI%Ts0v20SeyF5HUHTJR~zwEMWgpv`{Wke0VV!0#{IqnY4K$`l^!MI7qZN+Wc6 z5vnbUrQ5pbQivfnMa8vMkvzAv{^^Lg_k=h?!$7^Rpgu+LL7P-2OV!=@ctqUC!fJpp zq(U`%U`<)x2hG-U^F1{h>?sRa+ppUQYt88L#FwmMTh^Am8!wz|3;Gc18Wl*IjGOgZ zYrMR{03ICTD3EAEZV>4rw#-V=2hZXDS47iaH8f=lIodo6 zf!jBJKPci>Klq*SHhJ*u+sA&XthA{b%GFws&@F4rW9io6kf}sssowaV(88c77g^F) z>>!URUL4N6(ek-1y1~*mx&Q3jr(@9^z@9WAhNVHRKU9_vG8Y+)pYyP|FgUOza@6O> zBio>*#-vIkTo~++B@?n8 zy;z3{w1{YOb*U;Bd6-?o;dee`VUSaWN&|KUxn3GnN!``^OTdc^W|naHoljpF6zC~o z`+B|Jp9bGM>NO$aQ;wfE__T#VWmIj|q*;GlA^SFG*Bi@|;YA*%&w2RNg+YyLw1nU- zoL3LN^tG%YXZsQ`y1}O`4E7LZOt}-e>oWC`==IIz7b_PTOf40R-}&T)K~xS>OZ0+| zq&*y+4%LCSoD4R)!NS6zE@^VBfvJvcc@v~d8p{jRMFx}SJj^c)!oH8&X17#tcgxio zOF@k#V044Ig~4%fuVjpn5Ulwlji@tD)Q56@w#hCGj#|Y5CC`Mig!N5(;=#)Yn~OX= zZ!ohkh`UpAG}9+7X698#EMhKixGpkyezr+343_Y+?X+Q%DvPt8RUhlioA~GkQwxL4 zsx9WN4JHePrH6;#nOqnY>Qi^%4C;nB^O0#qb(S~s(G4aR28#+@MxbKws^6H%14Yx9 z_i+~)JU_w4Ll1@Kgyg$8XzKpBS#^5LoBrqquP+SpEv<-5Ct}T-_IkW!H_4^8xya!8 z8T&gH260^XXOvdO^l?CRVfU2f?NW4uv4uf$)$7WO44z*CzI|a3wMI=!sg%`PS4ALD zMWRix$-;Sq*A@%CMFiiG8k#)R6|@RjaK!mASvbE0eA~j{%9kD&Rr$QZCoQTh%$ha4 z+(4SGGUyp~q`rKNxya!8HRXv5gIJO2PtfM19IR7K2b@i#cI@*8pRh1!xSFiceaVJh zqcrS~z5BlRh{rGT@cfMZ_=Q0W^QMk~n38WaMnoU-mtP27Wbpj-^|*yWdex!l0)zSU zWBKaBprYtKs^7pw%7xhKfMBD|*vb6)e)!mh!Qj5$u*QwmFoo(#?;cdV0ES%h0RGo)}fodHSO0L%(Azl z{c`My>~>6fESugkE6K`kKUeVNe$Xg$LXiuG>A68L*ZNvopphrLUSQyP(Jch+ACJ#z zKo+{4uB-?na~Rl&2Y(Cd(jSHft?yVcAoT@H4uqMx^z2XfsrSO%S7o>4DGCbwDngnk zFxMr)>-7m*XpplyB-WUG*QpxS;x(s-sCY3NE8)EAbIPRg4R1l2*`LBbl%p2<^8Mg-NP=E;|RZG@dI$ox*G%O~CtQdTqykmqDx zs!|Fff|CKS5=^zc=_rNt>Nyh9o6}UHZ3fpb1wWbX=`0_S4|HDjVB~$Te#4!Qyc->0 zo$mUL8+p0&BkvYtY5u1r<~=FcH3U<2Aeb*hXH>td{hBNM$CuSbk>g-#)$RGJ@|g78 z?7|~^z2$n|bM9Sc<^-1iiOWnjk)02_TP`!?Y#hYbxO5$yxlpBiogJ}!S za=f3OAhmK~ma<`dnB(-^fD9OQ(s4vk2G{#MStygF-6{p{%$so3pAWx)>2SW&38;-b zi#nIk(U-Ar5ZUzdOR>c`wEM$gm%8W+_g%(JSs2UrVda}Ysug4IqJ`*IMSr~u zQ8HJ!aBNC#vMJ^@2cFe2lFv@83Su!@io0I2QG#h!OV<<4YJwv4`q$5ziDcTDR)UIK zJ8KiGNm z&hvH}JCED`^7ij-|HQVxjc-48>x)~zyY1M$(8LZv6FwhLI-#H-+prE34IdWDmeb5FD2%K6-RFP!DbaeBsI; zoqqK6?xPn3EP+or{FlQ&IDGTr^Y*{E|GWEd+@I{%4;zP%JNWX!?;ZTafxmah9<}$R z-G2-27u>!3g54(YyTBWP$=2gGzr6W-n?JGXZ{nMez4F%koXdBfv~le|f4KNR!JGcg z&D))-4fi#Y(O75b0I{}>he56Axy75cg3&qamab9_(mAp-zzx4Yojp47E!O|lHs zoR*hA`90(zQEmSH@!v;<{@wB4MTLI$__I-=pE>?aROtUX{y$NnpFaL{ROsIx|7}$0 zr;a}r75d5JPo6h_Rz7}|GROpWa zKN=PK8sIfip+5rrNL1*nfmcU`z6y9%ROl;#S4M@t0(eDK=*xkZhtQSZ7IvPr^P~{! z2#|$OJ%UHj;L@HrWtTrg9Fa$4RA}R<5fw@t5mBM|5grwa9br+S^`m-JD0+lOh1QO0 zr%%{;La=wF`x zWmM?LPd^?)S1-X|IQ~LZ=sz9*)0qS9;8H+;G-;18Q`C_@)Li=Fx%UryXG^fN68gEl zvn6;n^q#$YqB`*RduJ=_YO8;@ceb*whJJSMY-M$7ZDiQvD56~>%gEH%*`*dZY#p}F zd|e4`9yX&w$-{f6pOsb{hxblDE1|^Uz0=Q1D1LbF^s^F*9iB};_N>nfC5OOAI3&~s z*FcsUYm3-Ab6__C0%Z2ntq72P_U|5ih5Tea}P+B3C3X`8Y*9nb3Y z#IG)YN!@??{?nsEpSJ(BsL-eEKP4)(uwRG@&F$x+Leu-{h@0kb`Id;lM?4}T@L>-- z{kM%fzo{!u*i5TWY}T1)vSMJ!@-PnlO3J-l~MBr_=MFc+l;X#|y-}rWo+5*P0 zF^~U8f#(1x* zSE4jffch`UuDIZQP`Pw2UXIU1+sU=t-TJtv;>j{sb8@k2zhBD+*~#KA(Ze&iefayK zhD#d72HB%jU*aZUo^1{2fZZb4PL+{9hCneiU=D#QL6G_6@HayZKOOM@5iPLlCvuZn zQ_J+^23;J1F=+^jtwXmewzoshCsom!1C@%?Xi|ew60+;as0R_bwg$o?+8Z?WZih7B#;_Pa3~BAv z2}`s|4()dG-L`2Yis>qyZi-S?!s|q(W(w`DgKK21KlQdA9C|ezkxs#BW$;!-NlUzI zHw}i+6L7hZ%>+z09WCYMnIO-?jNEPNN9*Rit*n6ePl=DRXunOEgTgS^(ntBYILalma(z$@!t�gT`r+-1`2o48!$X zO1Z)eY>P`f6PSk)uc}lmq+FM)j@e_=wSaK~m2htpKhq#s$koS$SPjyXB+`A=6x{wa zmXeCe0oX4z;z1?^G|ez|WM}V8BP|!LOv3jo9GB{+R7_|hMtkT@U2bMdxm>p{bF*Bg zZGc<9vEZmtd*c*TH=L%c&x~#{u8pze2us##Y7oqlt2EsVP0cVU;AWeIR6A-DD_|2(#jFk%$PzYI(M$DA zh1>{pAE;Uhfw&-pjSI@-jsrXGv{$HzEf-67GXaktQ>}J!s2v22#t;5I^h)(}M%Z8)Mz(m{Ww|h_!3I) z{c~7`qE!mrZou#CC5I`jGUy>XW>*T-P(lhy990cVZgm1e(-gS*p->~>he{f#J`j6` zi!mjR9Kd!96ohWC0oP}Wp(+Z7XZT761YR6!h|E;6vx7{#*vhL=cL4XDq2kspG20;0 zIV+Zo@l38KOlK3oIn$^Jj+`$A8L&)qCQss2e<21&C}%9Gkj4(1xgEE3cw z4%LaNLVToVt3!^XDqYp6%$%(6=E;N6LPPNh z35>JXDc>fYY(3yTOjp#zgen!uk(LqF1bnT)&;^9#)q^L58q<0SOxTs8)a%-QzK!E- zu{Pv-j#$IT2|J%_II6=YC%rzo|J;R!7K@v3F)jt!McElyz-D5OP*}K-WDIpw0134u z)qGH`!nyQrm|0>Sjdob`HCng2wnin?V9j(tp{E1F(ve6HNjaM^J=gOoaGa-cwbP;P zjw9kC=jC_4HPm2w1qvQPIv7`Kqm-TyvQ-9SL2*DZOqwI?Vkgy1z?iN~_Wqx6x@x0b zE89`2fJL-bOffPA?3M}n{QEDwcFm`@*ZHQz<74@~||+z_j1vxN)i3FE_I6AU^Aqp)6_mje$@MOp0YX$uq? zu@JBcC|Z%wc~8WGt2b^T*_w&PBn0muXBvfKXV}LE<;o}%Ia$_?{=Pxn+Sma1U6MKzRJk;%)fq_=5LC3&4ni~ekkr~&J#n~bOA z3Y%`Xq>;}h)U8^m;pe+h6-i_y6>OMnn+tM8<&0d@R~ds5nix8YWs-DL?J(-b-Jym% zZIzlS2`V-kOe{XoG}G*hqXh2dP!smmY&z9K`IOTkg5WjVYSB8ZD*%d z&8^VgfgV43Sg65fXQUrDs)P-ZiGE)hrIHf_M`vVZlGk+%6$VhwnGC9}_VFhc8d|(r zL!A@>;-kvItov?K58@%wdLu7QW3zZOVAE|k+m32A;p4OQCOa!65fH<%bhg-O4Cpc& zW3YhM8>NFRB>^*OBO^D9R!t(Xqj6Y9)*$(Dx!>So`CKoZNDxwvw5vvTsIjS}-=%^i z8G2UohnR8rD~o5YW$~_y#$}3rW;$6c2RTY;Q6-0sy4nP**-Ffju&r>IYiTK9!6n)LfCcli!01p{ z>Jwhc(}QI+(-@h2tVmV$>@cowcEd8#aUPetPKA=?NvhPwYAr3_tCYMx)=km%l$7O| z4g?1qXn6N`&l-1< z2D9-e8^Arlhk-W$Q=kDnaqI5AXYSp$d(ZBNkG0K@ZN7i=rJLI3Gmkb7|K#wwhxx<% z9enEGLkF)uaE@Pk`k~WTpE{@2(?_3tapO-mZ`=CfLG|F#!597C-@kkRMZm_^{_!)9 zZ#%l@@ZE=ZZTGMpW6P=_N%v@?dtZU_u2jU z{{G&__TInu(xVR_z2RsYN2^-{!T<(NGGMOw6_(}-sS?QvcXm#qF9+86IHS+2+Q%LMra5Vx|w?1s43tmx@(x4~40bWc#(WuAqt zmxZ{M&o`u;SxI8B8`8Y3#)Z6Rt8pRe-fGWr*g^*>hVJ z%DKab<*vkW5pirp91{^ohq#sf>Xi|3uZW0yc|_a~hq#sVOEn^{65>{lxBZB?kS$1C zId8N>d;BPPVPTdb!3kbP627t=(75YI#7!gOCJ}KVdy}@(zfnYcA%oZIb^7N*dn@Ow z_eI3LHzMw5BjSE0A}*x*T3z3tifHf65ph2m5%&`jac_! zTR9&zBH}{QTTNEm4TvwnuvOGNyjEMjt#PD>_EzGwh&VMOPKk(gndy?3i!EQ_c%Acg6c$0iW72SIi6@RZBF|wpGY9%1VDMN_L(+^E3>bS?%NI+`C`9 zvH1PT$UZ*m=HZo%gEJow=B;_vY54`3hhj*!1DSDsNcHM5UrlavNKIyBixOcVJ!_bymlWM$&5#W%AYG$pH zHR1!Nn<-5TRJT$w{Ze6C0J*QUqUQy{Xt%E<;n`P^4JWWUhNXeAI z_tl988|uB?@jPk3YJYd;p1$)P^WX3Jql^4K*QQr*?#}(a{#Adsua;Xn7ON(NqL%PV zu#J<4SVf6Yv1lKZVsh_W;&)T$S>-~;9GG%YIpWAZ}-};*Q@3%iPvY(H? zxik0kJ`>WA9I^$0$f{zxF2YV_k}URwY>-l=HXi3X@qvr!$aIvics=CI&#xtFSncUz zLc05K?&(_|7TMEB-`rm6rJGkv?HZH@nOWCid7(kz-Fiuhk(}rjT&rG}ib}WPPk6P) zQ1`az*@U!mBrPVSpZv(&$2U`veSFl-t>p=+T_A&NE>tFgwCl-Ox12Rt80LbD7uE=s z5;~jH^netSR)=Q(tTE3fq?Intef#nE&40hK9@)c3-`rek#r><9l5ge+M#&VjSiKx1 zCd%pMWUc|F-OS7fjd+T6_*t(J8!(IUJx&oU##h}o&Kq&4ljx5u7X0db6i%8eom@d~wC`hNDn}R6A4ll4CD{ zNt#X#Q>iSGh@bg*i%D~3n=U5J7k_Z>?@K;^k-z62{M8#Tocnv9Ni$B5>~xYxI{z8)hn}!A&hJjn1Lqn)`mmany^$>zymL=PISh6LVhmj>&FUzte*|J5` z(9r4hJ& zY%qGsp!rNYPX^5||D3IzU-6bp+xeCkzi4ac%LYv=wM4%^wqQN0m5N78Xs6Etu|3yz zd+Sy)S?W^;?(1#5IP>ev2F+($dNOFf_n&Poeet(1ZRrayzT@0mdhLi>H5>5E?z(ch zWIH{t)#?P1iqsN!(3W+ttDs$j;9$Oqoq61tacS*Pd^-E&jVGYOkc2F>-)Zf)GW;-Gn+n(kc>U)*eMeA%Gs$8K9ylb)J_ zjIDmH9CxUY0D8J*I0F+R?;|!`G+oJ)gR=!v?Cu85XBv4jXs+I{HFEV|Ug33oid5=d zFTA)u=g1pJJzh-_exVpz#x*r6QA&NrvRqY0O>Vy8TKphE5GQbkgLALrGi^K>G&6Z? zWA^{Of{l%*9QoE4i>-|>8#ETr zg06vfZ?#Esqcjk))|pRVGH5>2&XYki^tX0K$ffOk%Ztmcoi7_S_fVlcjq{fcn$NWK zWYF}!d~2!y!OL2@&Hw-8E5}!^-amL-<(sa3&6VS8A9w4gZi$Egm1E$kRUIeXfYA!Us~tHrp|i0BnT4<+?^aW6 z;sF_Okk=HA@usC9ey|`KQKvB+;SYy8Db8EdpxkuQelS>(=L-|>l}~J_p+i}(EhTBR z5Ne&w5I6?yR&9Zt;~7Xs^ESOIZnV9Px<(J&Y$M-wS3G$#Lm$rI5>~3b;P?i4BrAQknuZaysp?m^G%WQs?MFhK~Cg zZG!}LA4aRBH12ehBk%G_I}fS$P{@*|=m#B*F-3cNUfbgCzkgx_K^k0wW}6aS!)@70 z*U5_A1muKK5Hdy!!wi%u>nk=K9f3LpzN@z}cqxc1H(N1M%A#1GGNqAL15(l++exM_ zSEzRP*&7g*FM47FW&){ujG8Q%(XwXr()Cz3yro2gY&NtU4ashaP(j0MHQ1wxz#=|u zH={Y*$P+rsj3jN1Qy0+TP`T=Y)GV1*K*C?Z)E-I}%{mMjSoyp#ZOiMA&t->P`I;v- z$ifuJ!HdCq_FS8j<1tn0Qz$x`*H#Kp-R##|#M+%EHCL~L zjU(0x4ZsKF!0_5^W{?}zxMVgHi%81BIK|!4GEB2 zHCiV#MXY;J64$iV+%v3rD0gd0HY2^GX=?M+C>ySbf#OlEARPe>dVHaKAUlKgS*pHL zYSGbumjw5$Hr^R!AIQZSyWP^A44AV>4@Ow{eXr#2;Yd54N*Y(4M z_xQ5b0CKTLfr;uUBlsR!?eZn<*Px@C*YgLB91x^zCd*jV!|_Jq&h8KCTfYplen14MHB&}u$|Rx{D54W@-5XG!+Zf946; zTc6m_staPi*f0d+*EDzRO9QHjfdocNwWn>YiFoW}mB0qwjgBxmSXEaE5WXH|kvFc- zK~04LrNFMGE%2Qho^Z){;keeM_7I2-2=lN^S1Yo)EHu(PpWSlnr=Qp$+KE8oRG#!5 zDv2_84)nP3qysj6#)+Ctu$jX1Y#-99(NPjdl z)MBH79E{gW@i3ha%#J2-c2jh?vRNAEMJaB5*%KQEOkWPCLVw6E`eo+EwE;ZULQ zl~x*SRb(_TU8TD2AJq#&=&0m~R9TcC4`(PSok?ktQ#E@zqy}9eoXi7Bm%_Aa51YKS zT$S^#pRm2=k9$dpIUL$HXl(S-5VdmXvimqneUR|=nY}Fyrc@9;@ z>_|sgzUi})84tPvCDp5w8otPbmgyx7VmD-_}TCT!yb*62n&}rVOpj8;evt z6k8irqr}Q1)`E>;s)pW$NeX6AQv4WYSmaCTC`vRXuiX} zsux)st4(0yN+4@^949us2+Sk13s24IXe1ilbnY)OTG*_$wmFdUdMT{pRwvUDrU^rf zMs$g;c*hePfaU=^fVCD;z|&=W>Zfaa1xV=GDL?f)^_4S_Wz(l>c1|9pbr8HuP8X*e zZ`LAFVS`UwWS9-@?t0Z#Y}*z1cDXccjC}Y^mESw|6B`Bvl}H^{h1Aik6G9{Hdqi(aH89U1!##wiwIvt)|MN?HJ zXZHC-p{sxEi48`|_GXGPK(MSf9tdI0YzFw!L)fqo!8*u*t$fi%7DO95!a8$)H6{nT z09sXmSj=WXK(@M)_>rzU0V>pNdac==0gFF0o+ZF|?c<-=pfAKldy*QA-mE$Cs1sqW zWYVi{NW{^YQl4O)vPy<9ixo!=E5uCFZem4a21HF95-%}fG=`LF+=}K4v{+j!lApIJ zf_%6d#HwRY3z=V-%y?`Iw#np(To{`dV-WK z5K+lX-lfEW*k6gYqGu^dQ<#raR#|`yemZ#=PBMSMiOf1$E#x&2LOj2+zw&=Qv7sw9 z)eY0+Yq{Tn+pAvRFJn};dj(j$H{0^0p_^1%rOQorls3!B1QUfuvavd~K4Im3bqxm5=buvI@9n=eDsw;z!n~qScBK@vmlzO|rZX#eE6=GgS@kbML9@bo6|TA<{r77>4t4$=^9PAjmv^8<-;NW5F8jW-rQO{g*lnIlR!T&j&b)p zp4h-|vbCMGrY#fKgSl#qsLWA35$^JnDrPAWI!xVqN_b2AXkIc@*MY$HFc5kMhtEl8 zfq@e8QU&$oB$vpR35rc|3c~8e!?3qx+;!M2c}8lF3r0}R`{BWk|NjG5?!Nc%4-Y=| z>PI-k&}H@!{(HOTJzxCd`p{M0b^Xh|t4Dimaz2~Y_X3{pd=1XKw}h~7hEaS=3wn{x z0ES+~$hyb&)(OMve{)@eJ@#HNW@J6tE#BJxVcl9vS`cU66K3b!UFR?3 zXT`u3PxG_BMw(RuM&T`&|AiFDwGEz#2=9|MQdWTu^Pqup!wICrxH_a-&eF^}Sd*P3 zrpUf#3gkrx$#__%=E`uE&EZnilYSO8Hy!J-wWP{!lNQF=CXhW}sH>18pna~@aI;*r z%;}VZlirkr>Y|5FVi9Mg8u;9WCgi1PR!<&%OMyIiu{fbXCa&(YgTHSC;#)n33j+@M za0J{Q`|2n0@)QN~9tQ~*2i!Kt=0g*2Po0Epn|Y}O$jd2^w+9B>E1Vz$gu9zl^^P}h zH8-jV1QS4t0~x_M44AX4NCVnl`l?L+4RFEJ0eTrG{|gO&t&t5ipe>d2g^|6e!+FtT zZDKIBOJf%I85Q%#Z8eN?X>NIK238twzs-!{*B;q)p7+>B-*4kNk@Y9yV=`&9hLJGm z`#oV;JVI-cwp^u|HrB*)zF6w8q$X&qo>G7;Z%Qd+rpE!E^?ZX+wx~~-7PlTSmy7J} zqi-Yo;KkK8vcKZvuO993y6zj<*egl)VDN_{viDW!c`CB^9U7j@RVV567_@QN?-LjJg%Pct*f(@BJDB5yuJS6$4S2gyFj5(2o{9Tx8i||X_qLxx5g+{8K~(wGN&tBLylcOF&As|dSM4kR4!~de&pY2YuYhy?ee!{i_DCar zb^`7@-=V!$3Z-6z-gG6uwZ8}OXFV0vP}9vRHf`yGf*4A>+2cc09` zhtJH!d*}PYy$#qSFYq!31bZ8>N0#Sh4DfdbTt031XpeN*%NXG8ZNPKxg2~2EBbjyfwQ*(&$$ciy$yKIT`)c|V2?NZd3S-ew*k+&3%+1)1DR_c?nTu*Z4Dv%gB;`M%-a2JG>z zcNqiB69e`*6nVbigU=K*y8_wWDc{ja?L1^3^2?|1I~ z(7nHS?{n{cIuMup!Mk5^*SLFh=hyFi|DE^V(eAu(``2!N_w9EB#lBAhY=9rU^(D7D zx88L4ONZZn_>RNY;Tr*i-?!Xc-(+rn6kred#vA#K&%SXNaQ^-EgZaT{9voJFrt<#E zw1QVE*MIW**IxIp*REf=_G8z+>Y8&6y88Qohwv+}e!RseIu>m46E#cea>y7*|7Ioi&QJ(6Nzr zHdxiEgc_H93hZn85+^x$XI(8gD69M%__)2r;6#qFk&0RFVk0gGL=UhFFI#vxLEnBQUyJ@yu|#ZYh`Lf4bLFS>m(;!~&?=_p$7D-B}f&l9zkW|e;hAGfxc+2wN| zw-|1V*&_#Xi)n5#_z;!3g;lkoNf6hE{@Mv)XH#ViQkJTKoOqd1vfHdG{+iul*exbz zVRezWAz#qe{eqZz4c{Kfou-ZIZDpw^tri|pm_Q5$cI98f$IKSfT-K!^BYUVw>2LvO zUV~)%nOHMLq*GF(W#o3&16RHBpF3l9^hWx&aB( zX|$LoQ(X>ajQ1)(4vJgaV2qGIW)d7-)$_g8d_$}s7Z!zd5F_ekRG&yx@O1rrtag%fwJlSrHf^kz`D%gaUAf+kQ^$xii z`76D&l|Enj=kW2L+G1=9)El*QWq}Q>H7TdgtS4oDI4m{8&{VOH4$f>yfvrmgq|0*qE1^b_h4?XhW{4fXy`?7q}*Fs#}ggu zHsg#3TcfL$0E)fb)_PiCEu*%@5L?V7$T5(1It2o)qoCD?I^~S)KwRA`A$?-PBdpr* zn(Q)84X^T3@G-u{tUSgid!)=UwPn;oss!)MhmCSGz&eBBsN84;LCPlWK|#OrlkhRN z#ZdUn1W8_bBu~*%&+Jv#TwCvRB~q6?y3w5TVy5W>BO*Av@)PiJV~ZK2Hpi)r+7h=% zj9_beN2_fXW;atMDO%KRIUb>^)ppq)RQYlExW2{6>bhu7X-+R1Moc>f%q3o94c-QO zQN`TYQ=krFs7|eICYApiKCW#s%MGjy*aRJ}aj#G}(*j*7BSLRci&=NX(*&$lX=dU$ z?a-_I7<`OwG5ZiQ|5NxF*5FJ=qjh`g)au;W&iR>D)J=^A>6isO4ZEQ3gzL_A zw(?Kl;}d4vEH@U^cs;7ISd&zlu$)O%%0U2w?=00N42##^L^ezh(5qMeF? z2>=sm`wop&u{pb-n8qjxSWqHhHxta(HHH)tX$>~)`)uV$;p4|!47LV^)b((^5KT}~ z&-LvtE1E07%kMim1dDEiUd$t2wX0H8`A6{a+qRe?gy4MJnNN_OHj5IcGb81uJh3;E zgzIFfJeY~CaxsB4uc-V8d|cgPm<0-mSiHpHXj%qy4Z=v{71AsQ)nMKUm(t8(*S^@v zT3lB7Vfgs1TMRj9)z_g^EfA1&*eO{gE?E@>21Sg-LFgE$5<(j^vdpzo`62lD(H7Go zU2fr+gKmFNTbK=AXJ-9swPAM_{sNR$R85YFrbum)_-y4L!pEO+(oUO2+>SJ(mAqD{ zTD$5~{WYqMnV_qC6gmvX4Rb-^ib(O5e*ho<$t|Yq7qX*_$7M(@Qc%pba(SNabK0b; z4}Bie>mcj1IpWGXVOIV=eEjKKOm$Nm)alrT8%@4UW}Y#(rt5L5#M`Vg3e*4^N6mUJ zW{721eh@zXv@OO;28?X7xLDU2KZdK6HyH|0kr^X&iO*D*oTk1l3f0&)D}N6@e#;gU zZP0dBc-2voH4PZnVI=mT0nbq?zvz`6iPgMz((5Gh5~}=N`1n(|n0Z~4pdrUJe9jZw z>sTlT8(|gAura6^sv6}&M#z+bT09R`egHl`+G0$s4(O{%zU5WhG!Tpd1otZ`&1Zx^ zulCvbz(>St-IiwBps0L5eEcW27-%9Vp!R3p3X3HR6gT*cv$YzmivENFrF@&7G~#FS z2=K@DE8hnnzj=!(fWCW!YH~0`2g@!LFCCEL-BvXrUyc-M3MkOmbD^1sE6uKaFMRxP zi>aD9Akc?!KC4R}+buU`x0HdTjRD5&S!82)N2o6;z`8NcD&GSizp%x4d#L_=H+=jl zTMWINX$As~7co^x1A@hqjF(Y;!|)NlFjmsKlLhqw2n$fz23wzN3|n_3(=ui8gsjfm~+7uGTp9xCw%+~TTH8zx>JzK-^dyv z(u~WZ&S}iL97sKVHf+*7-BatCFY0tNs(c50{H84iL|bp>)b>A0YhD4fW!UpLzWV_Wv8_5>)v0Dt! z4LE}aL$wIc>i%NxkE*4f$8B0`gQLhHh$({?Ha;hD-O9JZ$8S8bP3rgBu{1*60bra!VFaAfZM`7*E&z(NY>n zW4|9^Ub~vP8&?!8v+^zQ@kehl4Vy}EX1y8DyNkq$kp?>S_<2kr$RbvoL(G;58tYMu zR-LSTGkpApEoNoQRyh|HdqH8us?dw&vd9t(!Acg2`3+H{JU8JHiI2R>H^IjrwZ&Mq zO&8ion^CW^DqCf+p4uI6Ly!Gh!K9LGXI8ob1Q=GG?pMAMK7O#pB%;}*X@&yD29_@% zH0&|+)~pw3roN=dvAw42EouXX`?+2DKjGv1Tg>e8b+=p0y)9;s`rFFifRFEPF;cTm z`l#EEI59>rn{Bbn1(qX8bHjj73D;UKoOY>-OGafY{|9`0XNxf!5TsiuSTOEF?+Cn0db7u>s!no%v+VOdp-aEQdfiSd-v?T zfS2e02&*IqIIe=7!;%R!H98qNF>9r&d~ z+Sst=dL>BZ5@3Pm%C6R97PR@AlcXfPA=HQS5Su-+JMDnz^*tTP&{0cW3wgmF=jq1A zRtk=jL=%r5DW>d`i^SD9e6ybEL<$rt5yNZgJgDBA^dpo-<^fe)A~|vQ=qLIA$2ZUN z|Mz%{_Kv`9w%dgPhkrN%ZjX87;(*&@^13+SF3SIZPVsi0bMt(>{nzCG*S0tNQ04z$ zWcX`KJ8nQ2O2EL>3-kZIHjFsyN9bTY7=nE{O<&ZsW`vWianDE4B{QG9OIj-O#~k^Z zBO8(NO{SWYDJHx1N02yh6oP5@S!uE1XD}9(g|Zyrx(KE~vCO6=%$V ze2k&9$rzsk!i!D~tjC>1#^Fmw_V&?F^8b%-oaO)T@w)CE+1mm1!pO!x9FhHBJ(cb| zH0+)K|D2-yS+nX#M*e>Ts^ieM!r;O{%*lFV4#aGsC0xTSRl-(LVd-w%ja4i_mIHZ= zp@T#TEe*_R+t)1cW>&sT-3+WZzVOY{#I~k>lI3s5aR@s3k?SD!I!wIX8c7bBY*!@< zo2V~pWSy$dhiOAuDKImu;asaz8yn6d8|R#8B&yE0apbWGYQ0Gl*J40jGiV`#v@{^w zABrii$8q6pHu8!L>!QzGZi?ML`bqx(@xfXC{~mAG^9#hrAIm`8XVJY^{{MfY0Q|7# z|2Lo-)<9kf;7!rFj6+twK;_5h0@29Cr${Xf9)- zG0}__L_c=>Mj2r=>RN4Zcvzd65k_I28`K}c;|3;$wr+VHVTFnUh8eOnYal&FTq08~ zt+hmrx8lxfqtjqMerDax?$JTqZTbJiD?fVW9&_)b@BZ7n-*)$l?zZoK!h;)ke&x=0 z-Ff$&9ysa$Pq)AS{?FV#zHQ$Av|IoA)<3-Z`B&e1>o46J-Krk`9#9DQD~IkOdT(`j z_3A&k`4d-u^yb&yjBXM)Z{GM9H~#vK^u}jD`0*R}5B}A`H(h(nHRE7?<#!L72XCzW zyUMp$-dX892rHimDha;p0e=16*L&ArxcV2b{ikc+fAu%79Y6Tm`yT+hf}eH&?!8~Q z_l=kN_&zyN0_QBcoR~yIB=RPkRUx3mTAC}hWtcYEgI1%mXE&A$VaD6{k`qM8GEIc+ zXTt$udC|r&7hG+7>Tf6{IB=e)Xw6$QIF+K2d2gr98;r)VMxdh=#PiHD83SrT45@n! z!mIZUIQ3IST)4ti-stSToqBvQ5}P0yL6E~9yqwsqK9CV(?JX8VB%|UYV|*cvQovWn z*Y1AH&MI)WU2g%R>TFdsYqPl9(-^snM!DlJdq5I?=Jf>AYL5K1a{piNo;<0|$S47a zX$u500@yWI3$bV}>|!}9hW%=n?N*TzoJw)o-LKiT(L-^Jaz;Syw(s%<(08xZL0hst z>+;M*NBK!}=JWz-o!EC9I|S81rDYwVmxckVFHt^q#a@TD<=oSX=5R9_SvW1l(@DYJ zt?v*tMfK(_yFMTjZ91y=P%RKggYHO3(yB?f>)fa&%5+-e*xUbnhtTj~lLKlnIG?o` zGZn>bwrnUJzkvxb2?z-1Yo|9@b^^L~@Sk@G(cGZ8UoB0*yXYb5QXJR|7ZPk@WUOIq zj5j7JGH9qXnY{AXc8Km2fgM|>e07x*;LvPNF{xs)1592>eTxi{E^u!$IbG{wzfjI2vo$4`aY!wxfHQ^PLse!~y^WR?)M zvCQ(ISOq+cLHq zj#HyHo7mTj9YUB+$#qV4(}cz%W4?eo?U)`eyGS6>P0tTlWaQO1(Ae+}zvmR8_m+h| z5}o?2X$}}}3V5IT2!+;xdM>d@_(e2oP?MF7+c%F+5iGk7lVRz(?n36)wRkkni&;!r zj;uLh=5SLjuTF_+*RH&2hu{~yYY{WUQQalb;|07WNpIrXGnV6uM(8XTh}+bKfM*qO zGIz3<%7v6&53ms?mZ?ZEQ46F%Y}A^Qq7AnR_Wfy1osXO0K;{oWdxvOZT-w2wS$8IA zat{KnOSLM`O0{NF?~xukFEgz};rNPZKI=; z>&A$df>qI?idi_G$8=4YJ7JxKn(bMCJyQ`+vxmy{zqv!ud_4y*;Y5m94KOUJ_;3zr zYq$y<0M zZ<3jJ$ic>7M0KGEJr5FX*Pm`wi~!tTyCzlvCikX_Kv!$yc}{~IuCfJ0#pzNL zx#hIlD%&a0;oT@Sbog;Q#8RMIsxDb^hNI@fjxkSNH3{9(hHEFVYa1mYUB~By$i8-G zhsdmX-qW<%u-X~}4U9U<)~h;9&5~#irYMlr)6pzRP{&Gi(AyzWdP=h+pBq*Y#mFTZ z%s8t>Iqf!Bk+f#HQQwsM95QQxQrS5-NDRr)#g01)?FllC0;pb6h(+sbH&u7~t^ z!`0^GXrA2I2^j}K+@i2T*{gFio^M)nhL3ID2ZR~=ti{cnJ}O5^Lt;pxf91D#Rt17o z_0cL_M;OlE_|lvuQ&lOl(W>-3AaO3_&P*{Ih*G=p=XZz+NZe2+wy?}B#P1H5MxTJn zXk-D7cw8G)2~lD3g=Kcg{>^XRAwcd8YWTId7(wMyuGjsvP>QwRY-{FXJPGB7QzN_G zuvE>f@7p14Rp|BFf=5gQr?u)5bz7NeiegNmHOQ{OXRDAhiDs@v{LSy)A;v1hl5QBP zzz~yw%%VE&GQLUzL(91A4Vd|24bNAzc5?8YJA}1arzm4@fHpZ>Yqe+uB%;aPDTdh! z-H+C5q-?O-G$u==(b3yogA;r} z?l&=3Rd@uAn*5AIWz|KIu-Xax*LFiY&;-I(APcUc*OE}EcUJH?VfwJP442&4Sk{+g zwL16d5Dt^8$gT~Iaj{-C85gR@6n<|?*s7SqeZj4d=WQA3x`+<)8y0P?xXJHAP&M=1 z9Ew)T)JtRCj+92;=8Uxg$7l_Va2*+KK#Mj)o6!AT6Dq@5hLd}0434B7GmIm)oUat{ zsVic9vCQqf3%JR%z`$?4ZHJH-Rj=J&jv1on)Y$G65-}Ce5WoqVPbI-a;MSa(#IOZM zlAWS7md9zE#lkOq9YokkgZX<0;QGGca zTZ^a+^^DVzlv4G&U-L%+snG)<4cLaRkvl|t)9RH&o5-<&nl@6u&F7tJArA{~&W|`+ zUhwliAQO{3s&l<{gd535L z#TQTP@*ovM+Y}`sNgdUzMGGiKWVLKN>eMp$j$4rC?R?iAtx3Nw+x1OTt4=XRi6qnw zW>BInoE8OJ5J)x1L*Xv;${Ly^9x){0|NCd!Zu`j;v+N9A%;m^1vJ8e)4|QY znB4xXol%@LU60hY(QsI|V>Y8;GOsrxuMT0_s!?VHjKvVl6rAB*@9Yo^U;2&l%qdCC zu6N66(>M4jPOo@%1&*b8e+p^iSp=99@x#&XF&JuvXGYImDsY4B6}6$Sk$Fatv!MZI zUf6DpN93xePx|(06OAF^S{-Qx4h1}tfl`oGg0ujC9UhiNX6^Nim~S6_%#O)!hmM47y${0a{sk$&WUYT_w}9=4Ng&iP~{ex2u{i zjA(LZ)sp-F<)n?!j7BQb(h8nkbtkRaNbgjKsos|?a^QE%T;$j_BBryBSls-%Q^aTz z0E)a8rVnORv}tvX$l``3U~dtEMWh)u-ZcS(%b}4XG zo2i0?%@__?f5p$kfa;eifB3>Ff=>7;R&_@MZKY`tI}9hnG}W7fSsX9q{&eJlq$G3L zKx+4Y_Y{HHdIK3541$ZU!A1>9cN_wB`{Sxs02W8w3(Npt*9rXockK}UwK&kHT!Zd} zCDi4+Djnz5Dq%Z<0oXz(-FXWWv;YA~FxNkDf(X=l$Tyri)vgsosjORtRj?doU>K(Z z@;u$DS$%DgEQhLhU)mvBAP)nOO@S9;h5@|@D_l!L;7U|FLJ2KiVN-8v8ujTUsNFwZ zhqeb(lNG|z0@8(PgU-f49zgk?rdtGBg!{_5|xJouN^Sn{ZHB$PqhFO+ad;q=O9)z78A^k}Kt_r7)a{r^K( z?!M`+aQ7W|K6vL#uYScH`OYWb{R;E{py3SeGoptuYLEm_gvGiz4_{|U;VrHe&NB5 zd*68Hm+!4kcO$;_{?FY1z=I!u*7yI3pG?pV><2(L*(35x_T# zuZKy3d-J;aiJKjw2FR*8RkdAqv)If`r3rJ=q*DiSc~;OI=X67@$=JGeB&|;}QgrP!4+h8~<>Jurgdvi@0C4 zO&LKQKpIK7!@x#EE1kIp)ByD3wTO=8qIU2Ldn0~+Z^UloF-L;z8O_A&sS#{7bzEwB zHEJT72__*6t{OKpkWk%iX)XTh`*&^Zf%t~K5nsPU7$G{*jafSax&^d11+1oS@z@`G zQX5fxbKb`niXNkFpH1HIkM>6V$liz_-XR9rvQ^8oI!%n=$ZXZ52^fH6)1~x{Ih(E0 z(W<$^!ER7WUVY0Bp;ZU)st!^sIgrCoFJev8xyXP^4{Zr^AXGq3OqXdm;B)QDf7~1K zD|;jU!ww-&Ji1eY^LA*IYh!^3=cb^5Xc982?|Sj5cAN>PgDZ+z|Eh`+iw;%iP3 zh)l{G%{Inpk4N%eXR5a5`67(Q^X10S#Nmb|(j^CB$-!>+2Vrz+#JBC;#<%Vem(Ps6 z{l>i!AG0^&qj!idrLtuKSk_f`EvlKh0aA1LC~P7qXV+E6L@BfDFrI|l_r7Xx#9!GV zg!ORf)m$1=49j1kb5xuF{lX=S^<#1zio+2~tMXzoR_u@Zi+dw>J9efp97DNBLssn3 z#-d3FZPZ>*B9&+u4rF(bXu7h~ToMTA-f7hCfjEuYJrF;%dk!9?$7`!<(-^7aFtXTX z2c{eb52bEl79iPo#I`pOk@sEp!D$cffjI5KJrHl$S;Y;h3GPCmxAnYZ=EM*o%vIUZ zodpn8a`<4d&;r{Y2}bwUX?!&0HWHRmG71VFOKbiN?0v413EOX)IFD%_*t+UcL46WN zS9fbGa}R{Lvx+eaZo~!3btSZEwWs zxJ#w}rZ>-$+PoM*y{MLl4VPuK+K^xj)OJTxFIW8eRMf~DyN`cBqJ3#ZzO!l%#O~YC zBzkRx@K;$VJ9%%GD7~id&Ip&S7UYa&0xuN^O&OZGkKe28+SmheeQ(6I9pdt}6Sp7l zjo5wt?@^y|ySi%wwLHX}uBvR==)}Ry;ZX;2Q8JfPf{qDenx?MGXffdUzPsDqH1|O4 zwgvY=?7kk2vaVIbrsf3TTUm*%^(a|(1D#1za%N7}=^XdDB$uHE7G3?+ljqn6vD@%5 z0}oQ>R1tt(v0jtn+|g)L+H|}wu(()^VyW4s8;If9P4Z*Dbl1ja)dMAsx$MZ2A(Xnn zI~b^$DJgA{Pvu5|jOQ9$Hqhz1d;RnGMvV4;jz4jNFlRK!;{rEbLn4!xjhGm!HoK^a zYDcZ&16J-V8%aDz+0efIFZV{Au8^k^r|_#~oo_B$c8(8*iHEMt`E*W{R7Nkm8D)=T zPX;aGJL3xJJ`MK1U$G=)Y;TH{x{u*rScp`C|`X{o((x=l}6Zf9!MqzZ*xQkpZ~vp{{Q;<|9>~<|GT8C zKmO9s0eEWr4|Nhpu+zM{J?eHfK|MKDIT=}-c7jFLjoA18ay?Oh_Pu!T_Ko5TZ;Cl|< zdGIM9&;Oe%ex-W-$FKjT>-zNs4)K1GjKJkgzobN~f@pBG6*qk}^ zlDB{2(1R~LbLb_ZPdoJB9cK=`*T{amm}iq$?hsb;Tv27j(s2&${B0 zw>a&JPd{@gNVz4oqCHubvlJX?Zk88!#Y-Jp@y;B2N${&V6v59|qgEzo4!z{lB(5D?*3CFqS$|y;dO?4E z-C2KK@)oE4_0?zn#gHYXo9w1HpnB7kZu!sduN(52LoW$_wf;ge=v<38)H8=(^7c<0 zdP6yL=p~^~JM>2H%%PXO#m=EO#8ZcQ#>D6`n&A#sQcH0x>%1EJMI*}hojUZg;8$~K zz0o-5(8KRNb?9Yp|HPq(-+$`R%R--a=;05XI`p!)IDJFE=hUH-WlI(8a<<}>Sz>}> z9{t?oUEt7z$EOaxEcn$NiaxzYecP!+FMInZ4n06l9eP>l(+)j=_w3BEr-cpu&*BNp)T}n>9TV~eOu0^i^znq3(17`Q103CR_&v8bTRGM9roGnewvpuy1JezC}J{K8V)Jw_smYJLG*mEx4_dyy@-Qn#o01faH z+KnxC>mRzdE9 zf1NnMap$=Y{;{RJ`p`)x6Ac)CMam@C%P~?JM;OvGteMRWr=0@xQDZTcQ?5T~wOhU~ zMJ)_*Ub|#6sFL&CjO%Ygul*>WH3Sr1g6vxvmqZhtw;z>Mx$z#)A3y3nBASMl+svFs zgJMB81_(45WbMo`8*OFvSk)g(J?#-E;S_Tj>X!P+qi-pbRxf_x31!me>OMjH>>#@A zaC0GL(mun@mXYWwZSj3?aE@DSw_f-#4II11Gf)5s0^rTtwZ+yDC&=bgYjYa&Z{9{~ z6@g#^gg7#SaTtT3Di~6>m%b`%;FhZB84-2q@e17PwXp`)i8_M3V&owd-PGwQt@~V! z1682)GM{kcM|gwiOhi|RutCgb0Y=Ev*Ukr8Yn8T0sI7&F)>?zM-j{<2}7l?S$Hk*$Y#Uo^4)E15*DFFK)Ik`=2AP z9_=C6_-qNnb1(aLxpU#Fw)XVP{^p}SWZ<^lf3$~a+R62M=ptUc3ie~St*S{+P30)< z*UE8+3W+h(EW?>ri$Qt04Hr#U^5j6dXcc_%G#1S@S!ae0H69!7PBI&|FIdSwd;NQU zUuXNoFJ1q>d>Wg&*2+DhxI2qE99;j5n0uv(=%S`yYiuGA@`}XNmXX8VfLRNe#17W2 z%`oXa>W^!h1Vz0xw(!V95@l#VYKq<0%-$DTivWwg8u_?AXmzLD8da>7uvxE`6iczK z9DImx2-a&%a%cQ_!apXGM`*nkas-h^!5}tztoulU@FCA|V}%j@)NReF%Pj#aem#7?rQw+eCduAU|aov z>o;y6J^1lkpRr{G*w*jg`rK_@|5+73INJxz0WYuozw^Nd_=6kQzhuWI0J!~rrt)!H zmVlc-wq+XF{&dS7aO3?z@$a_|zw++4-hIbiVQ0(THvt2m@#osFUO}F5$+=(ek2uNq z=&*}5=Gv>!W3BO{s4vpOe6U(9U~}LHwYaH{wd$=-;6e2P#Xub>t- z$D#!z;%kKC*3Rm2AL&x7F3+ON#<)QmL=w^6kA`;$T+Pu)OYDu5d8$$MDpxQ8 z(`=B3^bWBaHKduc$D~!_v^5p|k4b_!92TP<7qtU;npzBmT|qIl5}2 ziYxQ(WZHL(g}_Fe0%_7qDmHDbv8IBB%Yy`gddiq@`dd2$%kgE`5jl*Vm2KSK3^5S~ zQZ`)C35g`oyHcGjfMlRGVn6oOyL%v@T^qgDYBgB}OsiSI@fe#6j2i|uXQG;&q=^s< zo@yZJCIvKj2VcEI2y3+jtb8LiDA8V%fF}caG+Xxd7}V@GY6}Z38Utrsi$d@Ej*GFg z2V%z(u@B-KPOWORs?dB)>h3zqgAC5Qy}3E7Cu?k0#e{J@rTX1IAO|75dpnkvj&OOz z!@b*hVTa(_jIZ-6IBAaz-)O|;u&UAy)WZ_qvZlyF3>81F^#-c_;MNXN4aZQ8MkgIQ znc6gKm!qY!&Kpf2U)7l93eONt6vcoT6MJ=6*}c!(&YrZf4}#fQwZ~hsV{qu~f!HxP zbkMS7Mr_6npuW(NbEP$@`g6^LJQ66R8? z#*%>J4coSod|u-lwRU#@EB8jcZ->~U2Y+yH#NXQ?8q}I|paG4jVqtVg@pvISte^q1 zK1VC5$cufp!<$G)?%xV_hVCN6s%%?IsSVG3Io1}Og1selv7TJ&|ntV4$?N8X%Z+55-G_-{w)erz% zJG1i2+C&u^nhK3m3-Gt}>b5A6NQk)P8+XhgojvBk)92U&;qUFM-ORbqlkZpwI-)Nd zcI5girD&N_1J8?AuZ<-|?ylXs1k*r65XkWk8+!DyJ9dRmH}9n-F04&CnZ+iXb@?VQ zZGcoB;7AJ_(Q>v4xW;BG5w%amcF(Z~qOo_MpU%#Eyd}T5Yh#ay`rr0O{I91^e)%=S zw?7E4egC;7@azA4-2;CtJg|G)_ZUld+%Ef!K0E%H&K|4q@7y~EzXLei`A|E2XVo5a zRK7j`zkLP2a({aF=k74KzWwl%ZoIeho7cbK>W_gRzRI63`=~FyfB1RtwjX{9d+-W- zJx`?%br}+@$<`|qX+oAxEP3wa@0rCBAd^3-0e<0v4#}dciSr>hSORkK*wO*tA(u~G zB`LZLTbRf+;yGtJ4oHG$*fzLlX~y7snu2Wv&^vE`&j4z5uDKi|wY(dWYe{1PR+w}( zZ!MN&(b0Ic%gnPeBKl*_U)iVvxQ)G{OP;iWoV3vb6EoNMWR>zwM4Z}aciYyctDm$5 z?p+UuDzJfGH&r^PjZiH0=5)v-Yu5m4`t7yD8Mqdy`%Y`H8LEtIkbw145ErtL)zUyG ztEHM}UeyW;wH|G3U)gvJ&3F?^Nz|Td>Oguz>B9*2HVFI&%WgV-;n54Co4@_-WVV^v zZ-0AB)8xm{+iJC|@t^yVH+%r}$-QUvNrO1IVfYc$Cx`@8Y@bV?Fcj}JlZMLTfHap! zZ6r4|pdRpapODS8Xd{ghX%s^gQ2$NLE8!^Y;9d`wa^~xRT(vo=hG@c$m4woiR)=jiXYz!^$DeRr_^i=IfC9d zp}yYZ?3R5&pOAHJ+Jd_>O^`vWMUHwyWyt|jZmAhje4Uvr7v*4}kJ?KiQIc+tB|Flv zr|FmOlb^c{`sCKL`s928+(%TOJg@nr)o<7lT&N|S5A=c7jRyIk^e*TVCIt#Yvx#YJ zq@GLw{_8HZrpFjTjCofdLJn)XSd;kw*?aFeM~*UYd^9;P$vCI=I)RF+>1ndkp$;T0Ff{7+`HrX#Y0%ncP0fr;_4*Y@%NASU&;fTMI zdTe9ugl^x!?fdz8{@F0k^VCyMJx^6v)l<(m-fNR>t%fjxKrDt}2x59nZhbBollh&- zWc=aQ5j7^ewVvQvQYpAy2@#HSI1DgAvhIY__(mK1?yBn$0n0r|>ke@>Dtg!96S6YE zeko{FJIA@}Ocl+Mq{=2w9+ncHbh#@~`0Nn;Y-t@Q+@0Gg1K z&x$oyRPg7LC1+ScTARd4Fh|rtvP7Biv!bUIOj5CE8bH3(>$yH!V)6h-Axz~fE?-w| z>9ufzWP)*t6QZWa5=nV#Z5U|>$wr{YL?}U`k_pND8voie!I;eK zG$!M@K1bA;?Am&Qxut#!X3F_;q$qe|MXtjaH(TR_gKUL$`DK7yfnjvNSqj+ET(sDW z%L5J^nqj!pt~jemG6KfI40q67@H0c^in{Keuei))MG;^_;&vF%^D-FET9YZhL(aPIu?=kK2X z$NB6$G(RzS-`w?c#W~;HtnJma_s_m#wm!Kwd*STz%y(vPoas)UK7-CYWBU8kAD({k zG&Oz9)I(Dro4R6(n>u;&7n8TxZr|nz;_v;d(cuaU2a8%Xl1}1osvt9j)~M#&3AB^# zwCR{v%5;cgxg`Psv^uu>3xfjyw`B!qCmFNna>cfz9ncH$D!3}}74fXE9`MrcLIp~q z$*z0VC_c(YKsm`!UvH7#c6opRbUm?}8faKNETgjCE->wSMJH=ev#G5b7q4s~O{lJf zz=g?lkdR!A4(AgJ?+b?k!d|+@mLp{?mG@|xl2|p$cC!vQM0X+(DHK`_EQ4uKyO$4X zbtF|P6y010c5&gP509rJ+NyEj%*GQSl}rO-qgc5msFWD?2+q96E(c-(tgR%8F5U|x z0RX7Bdd{flOkVVK7?}^)TZLdHQz+Mxom{(+zu;h!ebr6GdI6hpk-_Fe66WYuoGDkb)J2O9t{cOmp#}qxZv6gOwFvYp z1C&Cgq)nb;v;pkKH=C7srY5z@yf-X?O3DGI6|yr`(o1qBz3Cv@8K)~AUHrfZp@z-n zf^B=ise6S;BJIN(dYA1(dD?}xRZuBAraEO$iF5;~hnjKM&Z-)XcbwHeo~-FSTPlS( zChz83{W?ezkH>=qd)Fza8qAo6dH-UrZpaNu%}% zn`|)sc)s88wku(`6s6^`mhTt)0Jci)xKb6TL=HN2x)@cXORqKBNWuUx5@D-2o@q&X z6l!Z#o%W>}cQGeB9W5ytPrG2I&PKGEHy9jZB%#q+qRkbdDnWLAxzwQSa)4Ux4mGP2 z7@SO{s7Aa2u!H7}YQC&MQsKP6&R#je3@Hwd^U#MPr<6Km{sv1}@>Xv~R+w z1cZ3>>Ohhf_7 zL5fEODy7vMQLWNW)tjAcz*`KKoJg^j)#u)AaKw8$UPcH}_ed24V3iX!&C`QnUTN69 zYDME55lD(>-C?;lW87V_@cJY8R=xQ+I_aRhVYcf3#=jX#P579EkIzw!ct5I!#(}!(TwP)FO2i02Ts^n|L zk{DCTx^Zv>Ssv(a7!C|T8bm4#zK`u~L0fpP(GNC+`6y8MJ(2IDN;Zaed?im$2e5=W zS5Hay?0A4kH31qI8JhktgM-O5cui}E5+$!a&MA^Cfh4JBi%)gId1p_z=LkOMbwIU{ zzWDDO97wJRyrYyOnX;Wo^IFE9BC)6wTtQdszClpUM?$GV3G+MV{$Oy>)k3)x=tHh% zwSjoMsdzcqj#aX4hl1%$*&A}z8<|RgARNABBQJrC^66kw@C1b}?QW5gL5q$L`?vqzPnFibI(R=eNGDE&H`2v`07esuO`qYVnRb9ALy zcNcvPnv^)A5kW(cmVnF25}$&F5X|`^ScOcrCXA}YY^YXDWrYYvwj!=Pf|oiR+{2-| zi%BG^b(yI-;qFM4%EDJOf23>M~Y{Hgg!w;Sl7N?RcT$hup4# zkdPA#HbbjWC9UQi1#jAk+EKl(32?bX7h6md)oMaDQOFT$IPKJ%Vrgn4YD3w2s*2Pa zk&q`@$ye~OzsY2UZXM4M877+R5>3wO@^8tn;<%UA;PB2B=@eJObjSTB@tw!jg4QCQ+mO zCEmC6fWd)jg-{n((*9DkZuh5B3Mgt`N>UCN8TJ=Kac@Q-V|3C{kI6P8r-nru9+*-J zsSb;S_dH<=X|?@yKF!3P!JeXt0EJ5qI+#qY9bMXpTBPJf6=yiWE55Rq78t0NZw&fT zZ<*5jj%EvX`zo0T#<|OZ<&C8l5sE#u&ZPnoqEV>%1i{^5x&iQ{TaP2bE)6F_f(Hu` zLB2Hm8)NzKixL~u`uQf2br1ATm#HGfwwq%L?Qpf26QGbQm>D1*FThGQIu18lOgV1^Qvuu_5>c@^$Y{gmY7cZQm&t~ljas47RAW6?y{^=pOh)w;V36b-uH^0h zKp$J$2q>t6;Bg^S2#dbHie)`=CgG)^sMgABtcc`TZ-xuC$d=U8XA(vmU>=KOYLUr1 zSv~A@kc}=}E6Gt!Z`U9`8uwF!V$16jowexPPYn)7((S2+%R#l)RBO1$uB9Y58HKA# z6M&~Rc)d%+^jwU>^pNd)hH)LfU_F=$avGcj_@Z2j(u>Y=Cf5@szFe<*^7)!cj?YxP?+ovc*jB26{B z#fEIl8CvO1J{+ziSOMpqaLXyR^_;`yEtSi7v0LQ^0Xh|r$NM?Ri_O2#;Aj)cdJ}*t zW}%debBX|z5K72#8tE1M5vWkqtB4({=?zlGW?p7+G<|Tjn)1569XcJ-)83BG%19`K zHVfrMJSH>?jNicqNoTdR_*H|W-nSPc5vbw|lVK%^LcO5V<;)hOG>t@k?M&T+#-Vs> z5YG4(FEKbOeZAgys<_bY1#m4Zbx18mmZd@4X$L4TGSqEE3O+P%uj+G#BQg~{67z!_ zw0f}IlKoPwQAg;wGgDA%VkVFgJvEP7iR5*?<(T@k(MBm)?zFfF$kK6eE%4oz&h%46 z+Y=~NnSoCYWZiCWoajYea%pDW;3x=rJ6ddXqFgZVRg$oS<&aFGA7gXQkm$EJyqR(- z3aY}BwHd|W$oIo`J*lVNxq74J2F(gitJ z&V~fdQ5GT{a0&?#F;&S?8E48b00_FG7>s7BouFnj>gF@qUNoF_`w+W`*V=j+FCZ=- z0)WVi0g2buguhj%8(ch4&bb%=Xta^7ORhTCp(w4A6g!frDuFshOPyGxmnlRvG0nBZ z?Ln(u=BABRkV#95TI(ZXSPWtDd`-qc3YNs-RG{c8D+3a|P-#}}XoM+nOFuW-NRfI_ z@w)LM$X(4Ng?zuvCtIRJNXmAqiwafI>&SK}V0ib^M-2|S7i)@9#ErV+v3@!NU^`@Y zJ(b0IibzHDVh|f-LcTl`m9@nO4GxjT0a7SmY&1N4jVczJG|^PcjY2)A#sEy8gK$xj z>g*x8+Vmp^htOqf0B8+yw7X%(*8uKBM*y&NLg^PvN|o`~>Hr&~*GQLr6B}M4Q0@?c zVcAqO6{AZ?GFyk!*%vI#DIyD?`pq)!{)d<-wLM7Z9GDBd;~O)E85wZ*VmwN)>6$ea{+I_;6P z59}5?3EO*&HsYkmF8B%DU7D}v(?3{TK_|i! zcTKK7Z*^tmyDRTodB*bhZD$!7_`|*ap4pAv{>d{>Tl~dreeo7s5@7cgXTCGs_piRg z_VDD(hr9pDy;JnU?F+A7xOnych5fS^&j0tsU5}#v&D?0<{sDwP-F7v2C-AAM6J~<* zUwrh0VESKY+K-+R%r4K(4o?a8TtD&1YJJar!_$YIHu|Gp%K$@|3iKW2GTq=C4V*8z zrL0R+x-Z#6?J#Fgali))#n===5Z2ct{rhmv2Z^G%W{K!h>TV30B}_{!8uPkTU|_V5_G(psI;13GP1P?oomfOFQ5jlh;gG07aG=6f z!R+=lLo4&>SPRDbWC81H&d64l8X38QVX(W|WKb}*B~tA|X<%0XZm` zktax|VXxDHf+B^vIiq5yY#yg==4hjhE>#2wv2d&fPUoJ%9Yuq#2j9DJys_0U{H>LG3c^02XNZRrZ54f7~COL2q4JdWzBu|i2XU!hHA16s6R zDMQW!>998!q% zJ38J@Qw7tM*QeGKZhtI;R!c}dli}Qx+Nd8Epxq{n2Qrd*pf=klj5gYoqtvQ*{?GE>iFi2+VJNgB<(B)!UUk!tq#|g8N*?>zMkY(Q_^!A$nM6>P%A3 z`BMX=8nAnN6)`2}uu`;5(sjG8gY)!^GW(`Iz6sY(dz7)?j~cN6S-xk(+nJ7|ExTwU zhBsv?0|(n^G1BsYy4A|*j?<=8^VAJ5l|s=vj=%VIAg z_WFKr4)PB)x)d)JBRNc)I(>t~6;i>=NPw}2=Sj?GhtO14YhQUCKqQ{{OQk zK0UF=zuH{6WBCuu?xo7&yA~c;u+Qh_-ZuNK*|TQS({G;o`qXKYN!t~G@JRW&9J|81 zpk912ckKKH6Z`6_Plm#I;Baq_&Q6FvdvQJ*u<@^<%QAduNKb_bY5JJ&O%QS<0ju-sRSgUf{P+L`!Wg7Rz&!@n1_{S}*q#q2dM+?e6r!RQbR`QJ zkK73QI64^GO5gF)AzRUnrx2YAj$m`SQYDUML`}nr)mq5P068u{_X_u@BS+6qOzey1 zbvDaoGJ|5U(L|YMR}F=b1i0a}i^+V*A;N0afv1Q8?3cGv&}>J>=}u)4Em27_x>#wW ztq$Ih#^D6Y!%{7mBUiXK9127R(y8(iQ1qfmgGdL_4#&PPWynRC0W? zqJ`VN;vnHHv4pafdgJCaOSiMxx9Nrhbi?_0H?oCDUiLD1sSG))wRkByu!HOQx+lT) zRSpe@lxiA$>)K;tYWKTQ?L~U6-T;(`tPFDHCKhasg~7JnIQI(2<3T_Um}r?tFi*=< zgz(BhhY7G79SBTJtR`{rUKf0UfUiCgTvu;lWzYBn3$t{ms#+#r2sRiZ5AulywR&xQ zx_vYV4m@sl5U#P5>dM6Fh_@Y%R2yD@11}a*;C5b6n2K12p-P;tB{^?mD?P>^PMM|K zS>LVA?(qZfIq&gqV6A2_)Xs$4TsWUe>W)UK$Ri+=Kv0YRW~)W@p_qqN$e7Dh+e*Fh zw?<~^{zu*Lfo|9z?}kSS)_iEMpQ%S1wQL3QxRP3^UI{`_qY}rHUWN4|-dM0f!hx;S z``dZXHlyJME5W%&5G;U|Anoo)9et*sig~&Na!}}%m5A0yGI_U-HW?NmLN~iTQcuIk zmQFSPqGhY@j?d>wx?;sD}5^D!2?ycN*F7AWnDC_*S2Khm~R9svF~vTXr`4cAt744+w*^|0)c+ z$pqGuTVNCBsLFo2SF<-WaJ#O7Rl;U9VT(z4~YQC*L^$vrd+P#i!GafML##xVd zBknKgU2nz{QwB;t??F3y!HN0n)j&4ysHO@Xa4#Rs);%6+u;rvQ{$ORRZj95ND$zMz z8Gs9^zKn|zB;6SggKfIu2HiOG@opdq3Uvp>0l5B7rdfv0Mv$DHlQT_!vl59Y!CaQ@ z>q(6gdG}W8ncP8m#_9e?-Ee_P>5Ne~jvh`*Fw!r388HV3J<$lBqjhfqhrBH6Zo-3p zpyY5#_2eKJv**bz=hpEJ&Q{$$>~oLhl2KmZRYYz?lX{8lj^hD5Iw_rQn0*+{u*%V~ zT!-_g#hxc!%0meV(b%>lq=QnHkQ<~FBs$&<Z_qg|1 zsCjPIoM!3%N8NBfmH%)0Kx1?qVs8i3M)d6)SWbrvSLu9dlcsN2>v4Zmj6tMzvwmBk;g`>vJ5m{QTw5ASq|6hP4 z%+#{|A-+eeQy*$jX=#rmJAr!2?I+-~3=xYC$7gx6Fe`qScL?~hDFl)lgVjQ*n=eX< zY9x?5kSmZB8Hm@lLR@!x!-y*m3Nw}Q1Ee?fq+wQiA#YtN9e@FLURHE&9plJ9z)WdWGyH_SfrvkE2Rw78{kA0)%SP7TST|2#bHe3l}=5JFlrky z2Z1!~`Vaz1SbOOR0?BW)a$s#a0*3%^n+BZYe;P3!75I9TfDA2b${@+aT zCmY#5k8|j9n?`mCV5GQ0Lb57C-2+rB6YwEyzt%5jf`LRN?%D5m0{FA8kZ8sACmq@Q zv(8XI&ZnV)>^o4QO0E!hR4^c;nI2DCUCZ3#Qqmn)lC!` zuCe(v0Jwd^iaG?m+#z6xLH}d}(d+VV8HnwkqVxb99gc=872UZXz)SV=6s)_Wq5bYG z*UotNA4t?IaE56;=|I#32v5|rfoL&G3?vuP=O|Wifb=a?!Rura6L{WT^Ef$AyVPss zJ1Uk*I_PkQbv7lBE5o-*s;owoh=(ti_gDNK*h41~lYuzwItavZ!Ex@)R3 z_3X)8C-aj}w|&N@*p`9l-T&d&km*TIoxXGDl=Xd)fJ!v7vJg!O?uP6MiD)%pw|kUG zBi|r#P!NYeOJ&HNtK~6%EjSW;$Igk%`z++M^J{^T*xPqbB({=W&aYiG5__8oZS8%x z03#LE>A^#w0Fx%w)8$@<9QeW*mny^yj!X%M+jI*`EBxB?Mq+O@p>4V*psbAfXjnw^ z6{kI@YQ)QAlt4908WS}amyI}U#Bf|1zInlM5grWr?)Ks|Xvqr<%lr3-zkTdI09 zEq}-^4H_&IXrc}>6ar$;ABp`xCX8U=4Jp6&+>zK@Oc=qDV{7$hy-pVO4wf&aLoGt# z-Of0j&!O2!xF2Iup(>lw^O|2~U;X?U>j|b5SJ~MZ`2A3*{sc%aR#j1n)@f zCruc^!kY+w%`+1F2@~2{Nbcv?;E~vmo3O2grw{y^dnEQ_CXA3oDp?l|iqL5lD!?^e zgC+;c$HprVE>@d^lH{+3V=h7yiop=S<{F8;*@O|$Y!yU(xh2Vk)ZYA6%Qqm2;jK?#mugGOR+GGPRJh!J~TXS|4rE=Gc3+~>{4l@df0T}gT%)fGD= z*C~~1iNJ52BeDOqbI$rc3lACiHOENoM@@*e@ZNx5J1`Ra5ffr9+ zArKOR2jaA|3g;X3e9Ar$LB&sm2UwCw5=@p-i0bDFhKTZO_L0~RnGkECatgn8?nvwh zO^D63;c`9=O3^TB%nt=qP_a_(B~l$mU^017f>I0HTWVjWsWe!0_K(DVz=T)}j}rK` zb4Fs{Z$fNWVbg;;KxSYa|>s-+?C0CQAAeZM=XskIQqgi0E}cJ@f@`*uz< z-`5TL8&pls%JF_L0iN%R6}JSpBT~LJsK|aSlFonvtZ5yGBOn~k8i~Epgjfqt8u+y{ zM`GV=Lac?C2mIO@BeCx>AvWNXLQyg7b#^EkBdT5S8V_@M6G53t@$Pujp<$6)Stazi z0{r&$k=S>e5NqLq0l&6yB=%h<#7Z8ALrl8@a66n&q;Zbvm&A0bt}t}T$NM5qgyXA; z$^fYcYy8@2BeCx^A-2tM-f)rBJ6sg^Ag+d7X%=}=uHh^lbSWjC>V#bqGl+vXa{Suf zk=S>b5G(fCii&tqxB#)iY$jahRVnF-Rv0oFlG|F$Q-K3PB;0NzC4TMHk=Pqdi1nC% z^T4m2G7|fC6JqP}zF$b9_K+uqmK_C9`XOTPr*p1EG~Ks1@N$Qv?U)oR+6&-2;pCCn zx9yyDzt6(c27c|Nk=VDI5Q}9UWxdtPE2TEr1TgVhNg3Kr9zW&oqQU2n?KXhsX6UdKnYir_op_>tJ_Oo+Ac-hy8{ zZY1{2Cd4NE9F=SFurrj1K=v%|DM=V6M_8!R2`k|H)0;1-m6o8j%RYYX*pb*b?Rm=tN8-WK`tW4>Gn_d&bo1I}=h=}1K;64k1x%uUQRRLp{1NEA&HdAG_tCkNt zmE7^Y*RTgIwVDO@hTuSPt2?yu2UtU{zrOtIwA5y6ba7-_!gEe>Ivk^Wry*}DtLD<3 zavR8S0NJ1~)zS6Si}qH|8{L3ZLG=XiOL+L(jsJQ=zenO8|J|nd6rjP!DU{(e`ht=! zr_$ho_fYx2mTg={j{b9~qoL23lE&@hBN&hx(yRT!2=fAT+Q929om6F$)grv=l-@Dt$7GYM4FmQLs-Bjv(oo|Wz~5=&FdA>=3w00xd#z;+xQ4UPwpjzi zE5aQj%Jh5%L_N97N{<6nJ0D)lY#MpVT%*u0dz>UFlLn9yASn<~JWje-4~2P|?~<|r zp2{OFPrvfyFHI<+lJ%GKo?eE>Jh+;`aRA`v?XzIR;`P8qP@$QMI2~&GfCm(D%Y}tv zJDJzI>1MDdWvj_zHdszbv9?}JV*v0WFZBnFa^F-wkkNGzc`qxhPYxq*EoHOfuDcg` zuEQg5lUUj$I+UiGZXcGz_IHYo(fwiz4rcdvuu@W>)MF z6vJkotg}OI-tvjje%V$gcg}WQEPs2YUO14C5|H!90<6dI~|813*Xmp8PW7 zVhdSVDF(e+952^UaPb!rOTmQSS@GhOw;>dK9F`_PZQ4!_plBrNSU8a{^$s`?!QBDK3b>H7T+tfm)?%zH6ZT#i_`1kJP36iECj<&|8vqQCL5>?=dVN~!}OD&BxsD_ zpc*I-S}d{o0*fosfdg$&kq>TA=};uWJBiGI%0V&I<#ziIbo02|%^>?*p=68AIW&^d zGbO25Ba?W^Ngx4JvANOprdltj6}6QbeY_8KvXY+)TACW}|IeNH*ux)M(d~QKqSf0OmJ~?ljyK#=0ePs46v*^sf z&%AEN5Age5F@4_DH>O?;>I5E|Y)l?wyVaJntpLG)gCAl2+-ddLh?P5dkkSRgo{1Yg zy_?^=u++PK-=W_>jsIf%yQ`-?`?3?iz4o~`-gxrU+|Ll!_m2cysPSe9hECYinf>m4 zj^YPhANcHp^9zA%U;3jrU3JZ~fArJ;Jo-Ow{_!`3^>apoE!@K!g8y*dQ9nHUt^aZZ zdEeUiU-*yj{`^1v=HVNj_UC=?`0A%;9{PIu+wT?D&mIZ3a1(C`F1`%8;PUgmufOu; zl}l&mzI|ro+|-|b`Rn#uCzC&@f9=vYeL+}1Yb4mhUA!UqTKl1!=P&&8D`!#{3;$Ah z`&VPLGkeb=p!=TgTfE^JKe_N1!upvb!4_`g4Z-idmj3E%ul(85-|qfk?#!9y8UKCe zFK)Tuv>*NE#y?&5!3*Y2xkgw&Vf69 zsl+RkOMi6dd!sK9)=wV^ws0eF2>$ACe)68r6izz-yk{JH+}_)-XRe8Pj=r;f(@p5F z?!Nvn58nX`>-$E6E!@c)f^X}b_>-sO_dDM4se8}(^jGWi@4EJk@7{XRPou}2U)cA2 z<+EO4{j`x_3%Bxy;QxN5djE&c-+Lc@8RELf&e&e&Ty&&>V840#% zcHw*U{BM7M$_pO2_|hMjzfgD^{pq)#^F8I>X!d+fSU-6r*utg1A-KEu2g=Piyzt_W zz4fYhxt@3B6^8(D+gmS!4mP_VFaGMa&;PBke$q&=h5LC!aQ5uCeD6EgoV2iJTm0>P zAA9;G_rK>`iT8fQfAk~G*M2X5)YV{iJ8>k~!VSG4c;(wa`tY&ki(j2MGw^@$3qQ*o zbK(U*D6V@CmOq8uc;)Yxv%>lbBf%E#=ncWYyYe5u@Tb_-r+?CaY5(Z^9teHObKC{r z_QF4H{o;-9dc)d30UIAb5^Uj?-Vl7U>+07#uKL-1@BEg0^PR7~cFzgl@E`TwEPHU_ zS)ab?nZmp*tRFWLY~h~X5M22`U;WcBzDs`X4?z_)C3->9)`m+sFm!;oHr;|SN#`TSJ(HD(FFyF1e<0Zw2MzuTF)<(m&ipH}$Q^3U%2w~yN{`PJ8daL#{T7W~QUfAG=Jox=L+NU(*p z07J0vg%`a-Uw@5~`|c;FPYA#Nmk-={^eI0+_UJc%=DpOtr&U)@71mcqf-NKl7=lwD za?2mO?V+#Tz}>(;_v~jEsTcUZeLi!?e=SV^I99&nUB?OQ%Ok-SQUeUZAN%2R&v8Ed z>8Y8Y*ngHO{@-ZstY`fu@W7XzdB%gEe8JDZ_Tjv+zBCeSAvwSh{QT1%>YaDzL-tqC zeDl!jy>HOod4{h`X1?|GpH(lK-ut1Coh_^{js#ms4=@D(>=FK%&p!9N-}}$EygK@Y zSAU>(UEzuonFrTC`@{c8z4ejrz4I<%ePJZnLV|!H_}%AzHu0K2pZ1YE-#~tdhW`Ae z_x_gs!cD)wLH+d9rB81wzXt&_KN4IlM#FK9QmAAr$^_a;lqyq%r{rjeSs3a0`7A+z z(?x&J9{?~2!#&8WT#MA?3r@do*|#JwzbSP5``qu-`b#Ir+v#`TeW3B5v%>n^NU(+6 z14D55TOYmk6Tip4{Joc-bmcAcAAja|G~2h@Z~NVcQ_l@_-};%VOJxeb z^Ae@>{@b2*-LHQ2r*l4d_rWjxYW5~bSf3gRws2Ev2wn&x-=Dd;`}%L^e{)Uyqi_4~ z+2cR*orli-r0peN|Ml%}T#zpm)+a}TE!<%kg5&ouTzc1k-~Y0u@BSG38FtsxulUM$ zO6x7@4Zl3*t#AJPC7(zM>$Z_#3kPt9;2)m*?w|kYZ9hEe>T|_=@BGxuzI*eX4}Pw7 z_k#O@f&ArXU3yi0SpUyENlomzZO>(UF4{A(dfVz{s~4?KtlYM8*~&#L6U%ojU$Goq zo?N

    58S`(&XYDi&rcL7pE8QTDWQfU6`J~YyPTvbbfm7uDPq`aDd(Sh1r+S;xjg>h7t7Q{>eAd*i8EO5ch^G}_zZGP_O8n2{_1tMIO4TVX@R=MV;uPmSNyqSwnotfNf z`a0VSx91;Em;m{fR3a#fF>%O$=Pzw9*q(p<)R*MKyeM#ZlpXQ^+II2w{No4K1WBXO z050Ie@xS+V`)$wPo`1abKhKFl8u(veWWU#)ZA)(Jz2oLf63Mt6ir_>5G=I}A?>>Ip z*4^XY8)A8i!svh;A8OzFpiSPMf82Y+ay%r5Xe^c-@(X{kN!#;}doO~=;sKgV%84OA zT(XJV^N)Kk5=zJ@Psf`a_^Q|^&d;W3nLl}mp6B5o0z5kwv_Sr()^B*?%;RF>WLwsVyfA42(ADXdyKU>xxc7#`QC3I>(a3Q0?|GYQBev%s_g*v^CgMCtB!(Xj z_Z(cY;oI|%d#}hL5snNeNo2@hz2Amy&p+ZgRm&!$Xtw#?6dV~+TXaGN+f#`gO$afVN=V4Gyj|N6>NB=7^>}2)q;N8bP>eKe z{>U*kx?84hdBUZ#o8`+1ApwJj&3IlVYnzxlh|xYV(yV}lo?MWtM--v%ZlY=qQXr++ zDA`?n2_jV`2nL@Un)-ytOnv%RrXHWf=oldfz*i_1Ig(5rE$))3Njw7Hj3bPLE=J=m z9eirkB4l6_Q=?r^NUE3XL|1eb%Z%NDGwmK{$NUWpNk_GSD*KQCo+w1%+Q_Sqf6UbT zwlekj*G@bcB>?WP#ED0gsRz4cYQmw%Bq5Y7aedwyP+MRc|vGi%IS@ z#iV*E7wIMdUQ-!l#KCxs(mep~ThhuQjSmdH`nbnTecD#09$y;cvA8U9D8e04b9HpL zCJ>~D$B{N773qP9my;o=r%KMvCJ>aB0*dIN=mtx*+|2^0)jEi`Tbeyx(Bp8aujIMD zh9am2rkA0qk9o}0r*2_t&-hma1`c0v1dT;wN03)T%x*16;G!lu>>UO`q0!zKW6Gvwlekj z2Uiem6(SgpvqzFwL(Fb@H6c_~Jla?JGM?oF6-CPCBiN><4v`6a*W+Nl^@0=6W*BUc z?PXenaI?^1TY0fi*YKRZ=&E6vkyju4n5nmVgy$XqHpgU)iY60rNfX{!*HhH<_!!t9`Lm7YQMTBq5hTr3sWuVnGoPRwI#i*^W7)UOemS zhahUSEFSfksZZL2w1F2Y&>k!Er%;2S92u5?A#y}9nRJcRcMqd4_$4q_VR;C``!XQi_ z5Q`xgf*e6!4ei<%hD?CxElxc?aJB_e;G-p>Eey0xyc%sva-*7)r9Km5{X(@8b@Y&o z*YD4SR59tuVzTPR{qbI#Y>&M9nU9(Jgsn_HzJ)<$Ih+s~P|NCwGWBlF)p(Xv3T{_I zgyS3zBcQIfJK;2TmQUO*qIDf2V7UirU51KhDtg!96S8um*C}XJJIA@};JsWX9zjU( z1+p^n$B9!WN)vltv-;=NH?J;$+JD~VFD}DNUtPLr@tcd)g+DJ;78d4v^Q&`jo%77T zYxbg8=Xg2^wj1)IJM|&by@1_@-s(PCy~&hC&wdn0HB`@lX4qLY0FKpwIJwe_Kd@Hm%R$ zRU6cU2S9rVKzkO7qnU1R*dwEwuXgY}(2fhVV{PV6N|6Hp*0Fjek3jB1=7Z&c--eNH;dE(BtD+Id~RmXXu|6l7vmSbGUzM z2hRZQ4gJPKim&PR{@QQARB-xeOpn@UA?eq2Q`>$F90r>{GpzM(##>lSi_P3yDa z$SVg=+vv1~^xmDz2AFc>@y}&r{@V-m8TyxntYFjn9PVH0!Batd!}-rbvasp)9*-OC z;3=S;p&whw9X8$0m>-|KF{Bo0u(oi4Jbqh(zruBJjLOyZS>7#eI zB7iAB9=}c*O~{plCjfjHWb>-k&g%M4IOz`!3s9o`)fx&4zx3LWD5lk zO!sTdk&hjX=}}gYwoEs*y(5pG1saZg%upZDbqlo{OzZQQBOg8L^wImAJ|dMx^@5`o zZ%8gQjVTVF#%qDWKq$CF8N?pu0$5g0BvtpRr8>`wXfhkjWTbGUR!xS-@Egxv7PAZASit8$InT}AF(=Iae0Bid`@dlN-f3;xHg(<9^5jcw zzqDOpdo~d8H~%TDT{t@lLP4E2QRjSP0RGy+_gQ#mU0Azdqb(D~&o^t!!prNz+W8x8 znW%`qSz8t!V_&|s_FT}|nHQ_mpESKq-!^m)J6N%Dwgc38&W1WB>Z_XxD(`qu?Xz%@ z0@U$usAHlo`;$}0!aHn1{o$*r@D+fD39oL3BCW_6Q zQfK^3WuJv(o!S}%w0RzIZxc1@H?%o?{jzd!qpdkXiw8i9Cd$`uwD|a^n3W@&!kPoL zG+e7pRJ7k{=?QCn6(MZ)meIYgsr! z0-d&RblOC@d{dnsKj^ViV;`tc64QU&Wn;BR0_Svo|_!qSF79>$rtF0JXKVfHuP^&ICZfhBli_aTd=0w6!xg zOl<-?;BZr0s2EUKI|H;doZ?Kt32e0VgelI#2_qQR(>I3I1jvA?VI9Axw{i-ptnC|W z1G;Vkn_xqmjVaE;*&^ulX&aq3fmL9t)8j{gRsaraYkPqDyCcbRpoKagl z6|^{-i;paJ7XEXgHUI1R#@xemwb@_JR%U)a zQ=0zSbYbczQyRGYS8YGEDH9I@_N{*|{RdE&Mdhq~LhSBU(xv|Z>bC#|EUcgVEG%w2 zUjbbD51`JAMFlL(6J`_u^;s@)jYS1^*#|&f8=!y%vszh`%?tske`8UBT^<5ZC&!`!yF3J-o(@pJLTK&w z5P-Tn78S7YNovLiKz$yI3hc5EfI2=F71(7Tm>4NwVNbEkK5*?zEi16QLvZb-mKE6D zA-MJ>BLysY_HGZsi!Cd#%R}%Y%L?rB5DZ2NSP;6~L(sRZz%CC#Z=`^QI z{ur?TzkB6(EC0N5Bd7`}tvFWZm%qRKspZ!!=a(;9K4$4ZmJThwbLquPiKX+F<`%!d z`02&hEtVG1#S<4ES@_DrjSH78NDGdI`T6h9e`@|Ub8nyP&9QT5%}vaHYxbkF2WKyt z_02wQ<|i|^&Af4@K10r&GX1;hf1d13vXf^`PE6l8ec7}$?En=7zd!YKU6XX*F-iArlXTxQ z=(?)FCgT#tP<%4a4-NPK%eLz$ zmbH~PFS!?O^Y=|p&RsW~pZv-UY`cE--sRn@0l0dv&1H5|`V`&B0#`VvuTmzXM@~IH zP-bzLDwkp97E>vQ6d@0wGxa`?l?K9Kvl1-`K+deRGlb76HY-rKP>E3m#h%547>c3Q zcCx}?HG3ZRN1>)(v+Y0fOtx!PZIEau;VgN?a;!*`(E%g62O{F$q|^#haUyawQ>WA* z7AnUZU6PBG`3kKl!IbXs2kezvF2XQKwFLo_oqgn)EV^s;@$ya%0;gG6yO0(0F{v2D zvixR6+X5kffu;KS1d|W5R;>mg&2UQGZqj$?> zpn7A5tW#+rpsQ#S&AO-pDSl#`0={@Vi0i!2EVcxn3-@`0 zw4~+T+@iI=uKwzGA1e$EOrhi;>#|OBaECGn-LAjMFQuTCsv*|zB ztEX{bFftoxGp>NHC-QWb>!v*UdL3!DFkJRKJ0UfOIb>k6Gmkuz?OIt?TqrA!dY&q@ z^>9_|%ZXTnK{uVt>K;C>6RrXsY}N}E5@Hfsj>(bvGU;uW@i5I-`?9<3VtqbfveS=1 zlkHYuVsuL+D39Ei^$6B-sYJpbMMIm-Wf6Caydd`h!{#J zUa}wP6eTDG=CXZ9pviVCFcG={Y7KTWgL*7ol?QQzt}w!81tEi!U9$V)a#>_zg__Pb zBb``@N^ou3*=yOGs52if)nY`pkm+kR+i6Ff$y~ekDT`C(7*;Q(T~tJ_BsK=-&f8Vz%He+roF)FaSjyA_ytUZBB> z$2({~QSswUvE1bPj?DrylP0KOqUIv{(YR91`57dZXNYD2s*_%trLr7C;H0}<5(Hqf zy+@qM+`Cs4x6LV1a1bsgI@wCOC*bwET-@xt43#l=)?H0zYPyHfy*bG5gtLXJ6LL!7 z#Q(?Mo4`AcUUi~(-|F6~B;83vmQHu2l8{uKE6J9ZkVzs-vMk%OEbn%b=g}@%)?zJ| z?M}LitTb;ZD(p)f)sKcfPDcWI zGCkL)9oYn-%*j8kF`NYUR}VrHp+>woA8K`K4#I@@f>^bE=ikYa#c{ z?67XN@=MNY<<+3H=Ta*_#}AlpwepM4YUR~rSkI|eKCgV<&&<~MRx5w;S*^U9p6WT( z%ICExe`a>ew_5p&&T8e=)IHCsRz5Gg{LE~yZ?*D6U&ijd^(iStySD@Zqe&um=VAMgHQ^Ei!nmYVoiaJmK`}Xz_p-1yp^$*s|NL zX{y!o0ZNA*X88zMN#~)F!RZA+=$1w#TGqfSP)CxjQDy8{Z9%kELk|89Lb>ESAG^$Yt}+lOqTxEhAK!X8PQK{NLBVli z$(Hmn;?yp`JrH2o;^e*oE(&mGmKap+@?D8V7I19S)WdeVe0IAo7#z_Ens8CxJszmI zD<}1Wi#H87XySnFvCB_qHdz#6dE7|lFTXRO5k$*Xg1@DL-Q8ieBfcmdlmM5x#`%sItMjo_$i#3XnzF zq5#2g@cJ%0pasNS$PkjKGS6NGIbvSZdbN^SOI3$gOUEXQ@;a95LLgq8RpGKg(BQmd zdZtst)XLR{*n~&56qh|_luWx>Wt#JrYG=h+3#&~L|MEK*P3Rd5MB-xD2P0b+PpSGl z-?@2ByK!NmckkoZkt1mPVUVM8>x}e~zptxJ#-JK?B zm4ast(@=SKG+;+6y8wTC+3bXM*@ba|ml#7lDH_c5lfd34d8!(s31`Cs^pq7-Bhcy z0x{)EW=Wo@MJk!r?%Uir`Hripg=E%KXX-I+XW7iuugnnANpjg_Z)Pm{kXu=ZW04Jol3`QW|}z?l_Qy(Mb!gy^X|!wgXJ_oTaTl( z=Vxxff2g@x%s+Qc!*T4@qJS0i5#}O(mJCNI+)-33(2eDUyZ|Dojx}OuCpv$5p{WT7 z0CRzgLjy1i1`}ukO2Z4%P*De-JqijiywPZ3Qf_J#6#RaMMn$$)K~eWR(Ss=f4A@WA zJeR_U!)j6$d~$@Q`(_!0^kze{{l<`N8!4lCTo>$_spJ~YeQRDN$yA6um1^R7p(|uC zkA;HUT>~yc;6zgCbf09A!d0p58(uOAk7gP{(cPJIOj9i0P^8?BRQ$d zKo!ICOf=04svcUu0 z%hUqL5|J5i;d{GMEQaD4O0f`gzk?lsfW^A`)WLo=54I?R*L-SB%2-jakHNmjDs#mW z%nD$`xlAQ+LdfZe+`I3=E~=%{R0ImZn5#9AhJyOZYyqpZC~!K2Wt+-Y%2&IpQG}-R zV?(xb$vG`qI+`_+t27`X{R{-4wkx1+v+klQL%7a3k40P6E~*|3_Qm~wb?qZ-YkzZa z<>1E--*H&F`2HZG{|_$w(9wUr@MQ<>gKKLaxlr5x^!`s?czFMx@4s#TwHK|uzuxueQBy^zh$o zKY#0Yw!VMsv5RloBDWse{Qb>;xB28Iz4@|@KiT-vjrVM{HmKDSPqo(oTd8Ik+iqTxl2bXcm-aQzj*)+o@r;0gh_H0}eecm$L1- zH?0+^hMX%MclrIlUUIbNnTD$JxdtO91;nWphV+O_rbRwGs~1H-W0dCxA=#Wha@OCm z^{Be%_J9mT z+clHSGyrF$*Nm|vNcPYk7>tOC*0i{=x2@&(`FA19LV1p ziYVX^pKZZavX0wKZ`!kycq^GGRHjX^m=T8tHDWMN?XhleBN2+g=}9#w%rV_E@+lh3 zb>W0vno=Epjv3Xm-1N}2E#k$$Ei{XQydcJ_^7SbKvv~DO{^bJWwn;NP)8qa_eVLIB=P! zg5981$FoH#JI1tj1|9fqi)@S4+8nWqr4%-GvIw>LzK|minr<~&Mv#=5BNJ72tn@p{ zM8BKQklkTximH4D>^nn|Ft@%fo3*GIpn>d4eC4gC0GZ`kQYEInqSYk{TpO z&_cd#m6Q3wn5b6uxdYC+twBb2rrB!4nN2Be>x)AUINxnz5Izf7%5pLf=v8rCs1dGN z8PT;a#3d5_DmF^aQD+wlIUt-YC^+5I+mxE8;e?dRj$O6blT8fb)3rvws`xoeooLQB zal(;Qc;3%V=mD0l_Q&~YL(Ghvq%}d^G|$SDgr9E_j6a{syHAE3ARC;aH(jTKsa@D` zG(}D2*dDL2aL?kCHlIxuopPC`KxXGFLXJeSTddEDLdLi2wU*tKd8*r+)dAk5+FH$WUV#)HNvm7JGG;H(6!e{0C0R}otnLG;8PnM5;KyR=0DpkT$URvO*0GvwwC zdFY#U58wLq2}iM6af4N0mg+i&02xED&1u6Ln%6UQaaI&lcs@wiFn~d7tw-w<%>tEo0~~Sl-!qYsjJaB?neo#ac4k$+5L2Iq_)6 zA0P(*F)<~zPRfzfj?U0X5ig87EMKRLV#vxq{Ler8d4MID7ERCJG|$Z1#3%u+3+-Dsp-Nz>Qr0PV2HK)ghH~-bS2qIZGI%=VB{&pHe|M=xpJTp)@a;L z5(66#+{bhNB*mliF6*{91EIEFA9B!4rc!a(qA(;SXk4DEb(bv8Q(UguPqSsd&rexa z7dVb{wx2rTNR+HexzY7B!9&L#4MfUq!WRPv;qLkWWrHzjHhD&YIAxG5}(j}s& z8uZ93O9GI?l$ta%%Vt}eQ>`Z9CFVjNo>yDW&h&(%qVa}Brm79F12gHA3j;8s&xLur zj|E=vSwd*f^hv+Y^P9tvqcZ9TCKh$eNZ*{nHH4kk^f`j1>@1dM&r+BvDO)@swVs%^a`XcbkugnzO!>Bxhcq>+qJCt-E@wm(2Ce;#3(>+7vSUIt{|3WDNy2elp|$=>qMHjg}%16G@)8T0&!% z=ZrkUX*JKUjzPCqN qFztIG2aYTAp31PK4!JGX@=evxW*HUnXP)BZvQvi@3T&1k zGX8e+gu{a8GR6#({hVl{6{CtlC10tEG^>tteyysPrcfp8whVr~7ji%Ylr1P)t(_&z znj!(+X=##RseHN6hLpAhwn?8#VHL;*wvdpc(P*ohl}V~?q&egUb16R=DcMpkFBD38 z8OrBSk4OVJr^Z2uhmVYHEq;Wff*e+K(_?Q(hzhL*_uWSImQP8(aX`~!AYMJ ze#Wzj9^$sku%fC;LKCO$s_9bx9JNK4&yEqlmKm0u?1fi^GO+0!EI}dyJFPr1hHJrm zmJv#RFfy^uFK7%<0OU{}*9|}28p*T&bhR-#!^$wNg>%Dv z1{bJaxwo+&a^yP}p7I1-4c@e`(Zga*NrIUf$0gfly=Wj{9TeCko9oKigRcrXkYI@e z=d^Jb1nn|TI})Z*dC-DKOs|!KGc{D2fs%)nrPR&|N4kn9`%+(-<%$-ZAIf|kpwtdL z1B`j1ug#{m*u$n%uG8r4Juj4zZ1*P(!s}<;UV>6B%n(yrIg!g$r}a|FrgSl#7F4?q z0mvpAav1OgW5$S*sde&wvpr_2MyFEBLB^O9ASuZKlSUu)>6(?=|I;N$hZyMH5!h?? z71d;bish+IicM5yqFHvO+MELHRJt;#f-1FtV&QrcZh5TU@41CBZI-C6>CPlc;HX|Z z4`j^@0Cmj3d;_e1@jF5p9Y*m~&ckO`lByB)x!=xmy^K~)sg#hA{HaU!>eY&?1{|Z1 zgGL&Y6krC!IXTJ#HM{2vydX=tYyxbfb_P&qe7ofg6@)qZi?HTW#;B?mT&|{(iUP@_ zG*@D$T$&-7l*bL)1+b@M{Sh-mkfRTT9GKpxsG#)-qgb@vk)CQ#EV(U|d-Ik>3LdVd zrvy4FWYpH7AEN4FB8UWbRae`Ma+PkCA!jtN;_MVb(LhwKhz~}|aa$^whd{^y^vP<5 zWzi~O$ko7<`I6lz8ik6}5ANhUL_2V^Xj@1>*TN599ipoAuz7Vb$+Mb5lhK(+>D+_Fl|ydg6+z1^$lxtW#(+6k0TITyb4q~@fAm8d>W zfYUkLs|^E>-g(w5%Q%t2q3NtAtL0WE6|8^F^>EG5>G)%*FCl}nH~}?bR%@4-zFQbn z)hwAv=^&T|W^_YLSfsfVF8Vqhx0!|gW{uVOWUD+&2D7zK53>@G?T_-c0c0BqhHO`i zd2i=kA*xobp)mb~R!=Y^nVz-t`dn7(6S>yo=ytW96#M;RHzOemvl}k@IyFUQP0g86 zvmVXkl)w)8l$Me+L#*S}XjCv&xt;Ml8Jyq!%EkV_z489FOTTgH2QR(%5`XDSFFkPa zPcD8icni?ESibm*qt71w%F(|*`tqZ<9wm=19R5FtKYsW%huR@}_>v3%?}eWWvi?1G zp|SC^8}GmHx(izezjg3K2VZ$09K7-1!TnF}e`x<(_TBx;{*}GI-TV07_wBuF?`?bV z-o@QN-2KVjZ``$a%e$}G`P-de-TD5Vuh@C}4!m=;{rlTLvHi8%`Zm7(vaP=eUK#w~ zx8Awc-a6je-Ta-+9|>~*shj!D7jJwz!g;_KNdD2Zz%r8s6BziuUK$TO{b8F94qFN< znJhLZ5$U`oU6V&E9+_|1fq9n|OlD`R<>eyMWtRzcnjO5^4dN^deOAH*Zd_T-81jZ_ zWVvW$*$`Z#z_VQ}Keo|2UaS|Vla)$r{C-5b-wWxA#URxQ-RK0Xhi0*k7^Ri+Hea`d z*O-x=9tQmJhKn26uvu!F z-E!BVi&g0kIw-gHV_|u=FSyvuEn%&EyH9)KT|*^Iy3;t5E=;=9uqnpEs6rht*D1W> zbCo;f0>JwBpU6Fn?)xIr{p*Ny-y4zcdm_?(cSvV-@tHgGl_rNsZZO~2EA`z>hIB@q zHdQRoTGgsc>1Ct2N_R0L-BCy<^<1{4pas*ZN=2#F)>q0~w?euWHrJ#=UbZ|GsnXN> zs;X^#b2$9+xMh#}sy9*#)rJ%trmd70CM0W|MHZ%-s_Q=2n~)?ktBr~ybmXoYjP?Dc z)VeMx=x}0s?TS7li$iubH53+&42?#Xk4A<>BYR6UvNuN~d(*PJ>b+_SA%wa@j{JIX zred$=ZrylsNY|)LJk$$LxEaPSqtohmrMosV5$Vzq>EMvACP~3DPy^>WvWyQ6b2Vq; zdQ`elEfz{Kvan&LBn`__qeiML^;o|Y$~}`VRG+iU3)Lschl6zOahFl&o`SXtwY%D{ z8_GSCt`m_i%>Bm9DlJ)_+D)TXX(6;DuF72xbE`4a@tiU7JlR+Dd66XS(Mr2)gt;0S zL$BC*WGa%bP;}U7=T0*Z02@y&@iCev81so>4~uMp(B_o8T3(o=^lZ8?M=8UzRhv{> zPT;Mfwh7u;EpPp&mcyRuD^t)h~leq!xVe9xj8ng^{I&CW3@7CH`*(78~ZcO@d-D_yNk>uLMyoQ1}m@DV`CJScP5<{(&0SK@{{1a zs%&+KGZ$Z7eXU;$=}@_)_i4YQyO`q@#@*HaT~9@%gO+r(n{O$E*lMU4)v1?BdUZ9j zaXlhki0|z3-V?FBcZYQ2n$;emzFroP*2uTJtJBQdcP`Bf&7!z~Au8)yelNIVYpteu zeQh+d*F+=xl7;{OqibKgcFDQ4fAKpnf=AzV^v1(~dwBW6f4Tr3eB@xT|EYa$?^pMV zyT7;l&vswB^S+&p;70$#)^~1YHh(dA1Mu%R-na4c^?$p5IVj@%exBUF{=kkHJX61R z1u5fD5QJl(jGOWs&2mD?x2DM^hpALY&L{gSm+p=8S*DsN&*?$?)iAUG%nMGyP>Np@ z7>=6{gFP2U2X4|NiMEKQl<4#o66nC90Tc$r9M1I~4I+j<_W+ObwBudR+3VMzA7FX& z(b|>7EY*flT?Es?{kmz$-5`U~lq=4&WtZb7k^~~R=hHs>j(Gj(L{2XQ z*NwoDv?wRasE(j1gPzc~Yvcl#h!$?O6{wnkdD>{U{m5hJb~%S9asa7jTDD~shrWj^ z8cR9^-%NpxsuCPfRLZkDJs^$R!1sr($nAGaj(q(>ASW5Ln_p}R>3o3zF;w*^K=*`# zkCq$FlIsl^dQ>H_sZ}bt!$~vxIK16ST)uvAA}P^xM_?Jqk`7GEI5nTmsd){Z4ZF&) zZ)kibTPeX_9#p%W6S?K?l(c^$NuFv@8?O#UssJl)w=fu`+bZ@b$i1tw)L5Vf#xsAn|05WtP~yid1tc-C~tTCy|4NG&_=PUvY-5cGV%K)Sui z!!Gnwy@#`C23*mN0L<F27KfJ91je8 zeS3i=8039a4|0Qyt3_Ea1&#;Fxy=Oo+J+7lI)**hx;djA{b7vv2sIi2Vo9Lfkr(Z)%~73VZHYNB}BNe`idKKEgw z8C-9+>3YZL7f=$dH(*DpcF=!anjh1IGBW#f(=u0gg;8e7^lDIZ0A~FJOfXs3 z2V#M2^ie5^FdT`*=5QK z*Y|<3Hf*ASpJH1a=4{6)&7gX^#uw|!w0TZoR)b{qYfl7N7V8I+XcJwu5S-wG2CR%N zWssP6T)m7o%aaP)59Yuw8DvByXo8B2<&^pL+T($|#r)c%TlKO|50YZw!;=ToxrE1b zny@Pmy1r@m+79MxYLlzLh#XnoZS&E!cLs76^AYF5BZXyp3eDm z5KU^)oA;(>Uz?v)IK~s&XM4j(((LqI%5-gZ0w%^|($5eW@!B-NvKZVk9yN}P<<{VS za{rotQt22^_eK`0hWaYiSdMNWGtsy5bvas&o$WjWc1x?WcivK$XQsB z7>|NRuGfkMaZb83MlfY$QJ=dzS5u5F_WzrkSJn>wo!<`r_l2J?w7?fyV5J4#lfH3j z5fJv3K`7?ATKcGw-|4#p!v24daUsTBwG1_13H7{XnxCbBwx&TjWD{ z24mCUT>74|X?iN(0#$aNEdmY*H<2+A>}8HKWzpbj{BSH4jYo-n>#|#NfdiiEc?z)`KQ2ETUs_J;B!jzIzyqU>5V6)ydOBuT6XP^<=uv2q+ z)+i%XmejmnPidEG$C-H{k4FrFKB>Ab27$G_=nQVAw7486;Ykqkx%u7~-nh8X+@~_@ zS7Hpuv*+5VnhQT0nj2%Gv{0=$;pM02gov>cJWbQX70a`v<)QGKLER^69<)lduT--)1bUH(BsdQ z=jX2UIZ-n5+()jTz((JNpx|=C%bzv{8QVPDXYpfun zRFHaRX<}|B#aJXEJHvYZL#$#7$Se}soErbHrR(|DRj~v?WaVm8tppzPV82>H%dM-` zhS1DrP%k^}Tn#*i!bDv6v&Qm^7Aqi7jc(GjLdwcHDFCUr#(h1w`BYpT6?^hABFv^E zS5V}uP`;*1@RX`%WScY_SF1c?<6yh26vgQ=(VPy;O@v|5Xz~Axst1a=*#EzL&0D+l zHJ85R;tyYZ<p|LMkGY~0v*X#Km^!GJy9&y(5f&)eMy?!QEF`*qLIP12PZm-Sa-TzpyAF9p!w zd?c=IUyXbtclUUYbANALzj#yRYa_STDI#NB;ak^_Zi;+O$qSFRrhB99jh z`z4VZ_US#E6Xyb7xqjiMs8>gB(Az}CxWiYjAKVo6s>ls_o2VGq_`~b_fvDu70bd!p z0dGH;jB}SiyuNo+*pQ*&cgOG$P_Dfr5V@SYABrsUwplKQbHMQ0V}YpUEcc?wqVAXrVt5DyT|2+owJ(e; z?Q`0?yYBGg41M9+YyoFs>mG~@=fpb1@DO-1duBThqlrlWd@^py*wkG?%J(noGwULbw-tv20=v%|Q< zexJRB-dLb}=$99tFHPMG<6U0HkKB0lE{rjbUGBtqW^hG_@fQ{tU-IsIVZ5uyAGz_! zT^M8RMDE0RW?zRGe`$g7#eWkS8=yN8p4o^?gdbacKKLW|LU>m{KYZh5s|aK4o$f?<=1^QBT=w#V zFN%!tksB{v8HxLGwo`YG#50|&_ZJus-m&<+@Wy*!yldV14MO7Lh}V*;2uHuJmH{?wI|if@~hGFws{ zO0OF^PcYzV>uHRR6TAy?_KJIV0dMX1mY<)v7s$K3)gQU>#VZZD6KB8oG)SEj?XxlN z1n55eKcBb!oZSoK>i+Y|*WLJ{1;#huYRK(4F61W|?_PtQjdy#2^sjzv@%cADbT7Pj zwds``4=wN>-@>~U#|8Za@2v&g**Lcr2tPAkeEx+H8Rx5Ryl8>*j^$8{4eANbJC;Ld zs{3M*@!|je-o@vqe>^hMM{c}uWt48j*}BFYrHje&!=LIbKYuPV#)oe_xN6vAYhO zavy!aoPa*MSRsA*XTLFOyDs+s53hY_?b72H|Muei=&z3^ho3pLFZ}rhN-T!6hw|4s5zrNkx`gm{>@JpN3ji28rt^c3vg@E~4{(0j1L))Ur>|Mde zNiscwc->B=L*s;!qXw0@S##FQJDu>mgjh5 ztfca}L14v%Zju=Sv!urMrp%*U5NBKv3k`}x@y4XeO;@87pSb>_5J8MmuT&6`7z@2N z)oQ24y)@auj8-C%<||`mTr3O|Jp$84P)aftJPLvro*;;xFd>c)!r6PpC$2vjB8cO7 z5yK_n>|>rMu0IeWh>;s1j@v+#Hh4jZAV$5e#PBAF(gx2D5ybGkxDvz3;Owi4l3Sf*37(CC1relwLRr5yZ&P5a-M=N-rFS2x6S> z#_>al(hCS1lu8k7^h!x4xyqT*a{KEaAk;d#1sX= zW{4n$XG5GrqbLYAPI@7FHiYxdXz;}K^$!3U8$5CCi4Z{? z7larGAZK6NK5^~w5J8Mm5qzVLHyEf72;`oPVHL}MI7pKU@{ zf}9qCeU593tUfU9;F)>Vkb{tAFc~~q9lU4mxym4X5<2+f@=^F@K+TikHA-h zgU&n=Uoe~<5w?0)v*5ZtQtf^p61}V3+k>^A*@r|SFr8dH=zVG+QMR9zKyF;k%b8Xi zEMzAj2i8aFe$VDo(va0iTDxjVM*7$??>oHGE7fK!IO_xb98sRLOmSWYdOXnz9_JAY zTEXZKyR6A*g=Do^D$I3v?lL(U=7SVZ!E=F=nc5bcsq}I1J|GPw9og)*O>Trmdars} z^+iPATW`EzG1$KfU5|62akhn88SIOA+EWHQl*`>c*q`|%2{C5R(jLO-irEz9NZWO9sE@d8>#tkRqYGMMiwPO1oz{#Ac0 zr;EM`(CzwzMI0RQ*Gwi(7YJi#KowLvsFBOYK|lD zR83vD7Cb|WdfN1O`x^Srsd?c+umBP*+r!y8O|qvdbfh#DrvblTOOOH6i`C$%i*A$U zmYu{ zx@duEt<6fu$fR3nYel@+ooelzOHB&L)|fMshM{XYepd9oR+;g~=CN6z4CQzV@l6P7 z1P>-g%4u>^^;72c7-t!a3`ybs|2;wezr8=(7T4c%ZkyjP6cncg-qXKvb-8;&*5f2D zIQ#bHPLrNn%$%p}o|4e1`A}lq+$~LRjC-RceT+M)(@k`YYxd~7r!zge7ue4{=D6$W zh6UHXP4wNXru$DEb*kw5G_uKD2Dqn@MIoSXRXeWc*l1=-8H~pCX#d!scE$`wRY->8 z)vF@n6$|&DhVD3BM+r!zXZ2zmO&%9!(;=(Yj;Y?U-{f$eLpu`|R~K){GSF4b9W{9n zYqAb?+;7!MuM#A1nHX2Q`A(Y>8AjBHnJThGuiY#cTV=C-sg>0%P$}6)`X&AE1%DfIC5C%S5l>` z5{|KzDq&++vHMr$T+Q=>N6PJtq0^aeE$2&qeWX==q9ECX4wgD#CsUPps5i_vp>)M? zf=9TrfaSDGjrzC_=$6n$fz+6@Y#lLCpzYQQ$!O;w%c_Tp_?nxF7-Q}@ziAdaO%b2- z(`*bss{2#K&(oCq98$!4-WF8dq+(>4BnwnVuxF8Ttfi1a9#phr!1!^t-o7e{4c*2t zu65rVmqI3(I~((>b*Peb!HOwzNG^-@YPJUu4ZBmPjv=^Ku628*48_YcqlPtPnJ}d^ zpUcst!e*UPDhX0ns$+6p$(=B)pF<;UTo#H5go^kjHx)64r{dYZJ}WcgJ=eS7X^I%b zPi#5E#qd&E(#PNzq|M=K`wxIUaVhy_0iuReeCF89zAwcJ9_2e z|2X``!|ytr9aaxtapA8o{O1erzu;e}TzK^0FAsk1;5!bygVMpv_dm1$(fx1RclL|> zFWdX{-bePnWpBJ!*n7$DpYMKn_nUUjU2OM@cm8bWgFD}_qwnN*9@_ro_D^kp-L|&< zmhBgA{qfcZww~Hjx8Ah%z~(15e`520-IO=qxcU5zKiK&3jj!GiH{P&uY5n)se{}ty zuk-6~SbzT7C)Pd?fM5SuoPG8eXrJRP@Y*eAW8+aXcwKFlN3<$t>0{ivjWbhH2$x`pxU#8kO{$*1shx={K%_ zb5zoASpTM|q+h@OjZsOzZv7jgl78*_*GDCNYW?d%(lb{B+ppO^j!OEf?bk&meZ}@` zqLMzc{i>*>4{u*NA)W9e%(T>e1*gk-QW1+QuT1S=JEf?k=nff`G`~}fN{Z|dQAyvj zQ#c`YJ-;oqrqaAhDrB&Dn6FeTm_gqdl{B~UrBO+<8*hk8n%T%jB~5Q+qmsfKnW&_x zjdWDf8?6QA@qLk%&tA z+Krng!xfP!KFt#)i6uzOL#L}l`;!}gezVlGNdIKxQ&CC(c;nBalK#=gpGGD9#KtF2 zNENO{@+7BtU9CQrM2uaj*3o}F`e0PjA3yr(sH8u3^ixqufAr`lqmurIqYp$S{gI>p zbV4dkF@cvc$uy@k6`8a7l@>T$JG?ox&mz6>_lGx!_L-!AcX)GXpGo>Z4{r|bGfDsU z@aE7ylk~HPH;1<55|vR!Y0Gokrwg8Bt+c?-&+Pox&Bi*5^us&98I|-yJO4E*=?8ay zBP!`n@BDgH(x2M-wUAVw$gLvoNW>V^YD{Y)5i2clP&v33m9%{Do~WdygLg+IB@ezL zDrxcHT~SGigD;OtS~&Q!sHFJ8lTk^rgMW5#_`7S=+DF$8f8g-(OW$%>IehVM?!qTF zUcLDh7ykW)cU`Docx1D&J~;TZga3GNWA|GRx*I=n@T$#6*Z;-Q1N)!3^b7kR-2WH5 z+Wz3ua6hs4*}aeMef^%eySwwNJOA6xc<0SKN87)-{XN_Mc47O$jrVT-?$!@%J+Vb? zy=3!`Hh*OOFE)C6$-VX6U%ZsPv~%&}7vFzzeDTc}kB)xx=zG=^YoA^F==ujYK6T_D z;So2i;HJ27MII+SswdWl8j}FKUN<>|QHdzwnTAP^inX$&>5SGGnhDz3dsE0^)ziTj z1FT?cJ*NRDY`Y{mNVdv29nk?NEd|I)^-Ol$&^G=sqU|S(zBMX#Oq!ji zDQZ$5<_dVJG?{g4v}mfD*CDOi%?U0s zBLQU(M^aP<*#&-u`o0qEvo$C9npAr3ow}l*%QXk7rpk0O(yv$%`h4h9PooOiu z&XtGbx+)A94X?R*;9wY{D)}8_q9NT>56n!bC85WHva7i@#_l*)qcDcc|nj&6F+%mA%On-~Kc zhU6t7pRAVW4B=;0l`6AiEMxiOVxx8VyCEvAQ_d#cX`zr&%ARjcRT-|b-C3Q-%gWF% z3d5mV0O|>i>m5p=3>yaY`Iys@;PrUPE5TwPaeLFLKgAmEOu(=VS<7qbJU;8~H9{FC zsLxZAq{NnVwA!%iX3K|)REr~YNw70XQ}2p`-^3`pbs-gUbes5qFleIaOJmPqaLp>> z9zzIVp@dJyj0$0B(aL8vO8P4#?*hbo3vwhT#i z+#IGId~;Y2Ng;BAP@q@`a+4G>9+E0$61k?!lQ{_}%bG4W2C4-ok&O?8974v2w6Z$p zhMG-v&>l0!%?84OAM@FN$=mSl|6U#1Prpua73qAoSlLaX?qB&;c zv%sdfenn;nc(+N;O6|#v%lGFAAXyPyw8>U7lO|d#!SP)7IJh)DFakfqg*Rog=mFzlzQA zmmx=u=|O&@)%Wv_RB#Y&0Z1+IbQ{Q{Db>nVC42;yxx!4#^Un6Sg&b8b7-W5}hV{yg z&Y+U(asZc{V_Gkj!Ba?kA{Hm3Y|_q<{_ZD2jxtgjySmh-{88QzXTI+>bD8ciOX%G} zg`Nqe3WiYbSd@T`zYjTz`V{J?OXRH6s(GZY>C_}Un4)HKHt~2c&AXz>H|0`ltZn~f z$Pv7uGfi~Ly*V`U=9wg0P6poh>MZMx9h)+FM=?N>WSmje+yIswQmLd*V8JQ3!M@lr z+s&~$EJ_^=ZixL(+x5GJ<}@h=Q8_5N_v%mv*e8SmQ4oSR-Ul)zcj+PkceAaD202O^ z^sSW5W@dAsPV&26C_^2ilU`uEgJQ>%9?&Xls4yvICPKAtX=$TVPx2#aeidk&`;UY& z6qLkz+(@+%aWb%QtCezc1lO>slmJiKe5Y6xs#t4G(!l=Tg&guMqXCZS>MmB2`*f|w zP(@hguwE`uRcDY?I+W7);kkAY*w1(nybYwKE zIUFTia_4744vf;$X~8IVphhcC@^jBBC%eiZcooislqzNM4S<#KI?(ku{~+W*@sWh! zFm22feON?+a()b!jA6fTR#ikFx(-l4%|>^|$UDCra^!kR(4G%0kh5V?OgNn$Sx&YE zX~20rkekixGYZp7-HCy(esYF07DVl0QLmru z%u{BvFr5*WQy(~;=7`g4nJN+Zt%GE@Lv4NQ2}eIC5nV_x7pMMsAZX&4_Ofg%k;&GF z9f*+pWF5jK(xc4HPc4m)G%rc@bfuh~vT3uR8K_&S*^LQk7rSLo?z!37P<3QIktC?} zQiL?i3RsZ@VO~aRqi(C$%_cHhraqH9T8FJuWiJ6k?ZD`FQI_$HFPqoDnP1jnuDlgCmBddhv8_Rn)U|tPCdiFcVcSWFXqURF?y&FMsm4&qrkv`W+}>5M~B?HuR~=z6u)BsCkN6|A-SXCcP`sP{x?HlG2KCZY|m z)h#9*+k;_iFe_k1!Ag}$GVKTJuJu0(Il3iefY-WcE2%Lp817FyTDdCEgEd1P);WdC zCk-17)P>D%{a(n?$#%6~HYZqJ6{_J`2d0@~J=L9OaV#G!+-gH5mB!$ClGb)!d%^+F zv@Rnwt-dU1nXWYMVwlpi*i^Sj7RSzzhbt;HPQbFiyBTt{Se?Qs3{N*kfnlmvYnId; zCtGmAfhG<}@rv4XlgTs<0$X~>QL=+sZK4(2CNUk35s~8(ct3b+jRsm-tE8q)3MC~{ zC+fYe+zAH`DIj0aEIjR+sOEG{yU5uhUUlr){|-kS^t*%nkF;W0@}z$sfrTS6KL-n*T51bWEm`5q`>oFMO9 z4)zaV#63hv7K)e}P@hf(IbnsS?ql8zraB`I5|uWY9kdFgsa7U>8I(xpQb+ibLv}sJ z%2^G#+90G9-?z=W)}d7pFXYm2#qtMA(#duJ#F#TXKYYRwydh_C<80gI_~9I3n|P%^ z=osTtx;&+4K2d|qa!MZRRe5(Fa_IGbSpvLt*=W(Dan8*&R4)r~Uf>TdR>s9D{5pNDi>@lRWNH3-YU~br9Mo!!@TEkO14$+3`wcfCiiYmf=ZgM)Sv{;Xo8R( zRO_8QU#U|4IzuYMG{U=y;$f-u7 zW+pm7C0jB*S!#pjrW5SidU&svPm>j$1F#A`f&>s} z+AgcX>Lq1Wi00I?8|EC$zXT;;rELh0?tK_O$%Zk|ueM zXfF)wW0&uhY2&W6Ie`@pG6x0Yf5|%#M2YoAZcxTzE_ym`&|{ZRQdAqPE(Tx1i*HZ< zbVt3H#ohYu)VVbfc#DE(4bxD0b~Io|D!TxGd)e%Sby<7)#|6d^&kD#e68RSJB|pa+ zH;d9#Uc32w@ZvbjFKV|WQjLk~E~>L^fScxK@kaPFXl|e!bkA4FQ^#}3q^5|H)orrZ zLKtXX?D=j5z*NlR1ck6Ttlch5Y%TD=4`AL;MgybCropm0nH#-&#|dtLAlMS-8BEBq zcDbjR>BaX!xoI`4A9>STDM(PDVs)F|j?b1hXhAXp|QiS?)Z>Q@bpsc03@%6cU^Teh|P);z7&H79Pn!*y#MUmo_e&Eb$oR1Z1M z_Z-;w^ijUwVc4N1I8;-TM=+tZ^66pLu=pB~)1;(Un3laBP6&JpFBy{INqocw_M+;6 z<}LRBFJEKVE5`L*8-*zf)G_*1Xn3X=23$#Qw#%HpX671zcwaYnY#|A$B)a#Z2Vg&D;Zx-supSl_wK#b6)vxBl8f9kcd z0mO*KiUQy@O8_xKp3gTK`r}W1No)Wy;;qk)6o34wS1$p?h&4UmWbBVW^{UtaV&s#E zqK2gBNk#8?NPy&`}7sfS|&h;f`11;ERe0Ai)$kDjhQ3V@f!25`<@ z@RHa7&bbR-yaW*EC4d-r3sJh@i(>;g=Pvl7*Z|JC3m#emh;h4hzB}{BpL$Vj0O#BV zFN_V~oV(z`C4d+s`@Fm0f!F}fxeHzp8^Aet!Sj~@VibDbUGTiv0M4}w7W@Ci+7GW? z`kqVmi=VytT^FgNzdZW(qvGLzKm4Xc^ui}EeC>rd9sJ?JS0Ci|e|P`A`>DPEYwz8A z$GgA2`{eFxcYbB(9Xqew{)NE9|B{`!vvar%cr(dYlu zyiwZPV8m_gQM;rx>T|u5XNn_I@vv@lC3fVBGDSuiv|7$;3hK@1j!lV1LDq8JX+K{Y zFugLL>#4aRf~K?08|1QBG(xet5@Zr+3--(TB}0uBKASuW?au|ykkhRh_> z9+1r&#bvSi2*oC6W|s*9!3spJUzsN}Fx+%Ut)y%W@v49^Y~G!Zi>gs?D}3_?u`Cve zP%Kj}Opt635ym7^P7hSBQ1h6Kgwg<0py5shnGKXlXU;RN0BvDe>{}uf%jbiW{$ht2 zXWHx(so8>2r=8%4WZvxdNW3|m(+OA`mq$6%Z{EO{#lAU0v5wh;XcX4uNn)Oyln7iW z=>k6l3ve=BOEub6KzDUlC&5>Y8N=e1%K=TH=EcTy7C{{?QHORu8dLG0Rt-z&D z>Ht(hcgU7tPbMWl2n{GsM}DT*4bbM7#lA5@u~Ky3$>t4YS?rfaD0U)a3Q|XE1eWr& zB9zFHg4IvYXYEX--5<$QBPmRgH3HdTYvxoLI?rEY7Ceq?19(Sy7$@9+|g)m9Fp&S zcl?LXojLV8Rn@0{)m44!RMklq1ZMU`9oD@9>_D|Av$)_Wjy!cGt#%a0Fj?~EB7TXd zN<_eGXRLu>ty(2pb!K+64(nP0w!V7!Ff)6C4vVb-Thbnu(yp)$jO|pNSh&LW3BIgg z1qQG1T?Oy7s1Du;Q(_KhW;f}u&J|$geqQodMkOci4*Al-)|ii5{Z+J2wfSBy?sj)a z^_rGhs;-@lU}jz&*0BPtcA}(!l8tJ>ZFP&d9DpHV2^OWEi=P(*pn;3 z9^ukjX6DpkZ(ITP2>07EGlvdqJ$xhcxNr3fZkZXX!`^WC?%+|f?lLpG4trvS+8!a@ zCo{9@u-C6p+an||WM(II*y|48CO>|JRBX)bMjiIr6*}Pvi9DH^RfoN1g-$p^T0&-a zgARN33Y~C-?=;NpgbsVv3Y~C-Zx77udL8!o3Y~C-4Si;Ioeul>6*}PvyX4I5S{?Sv z6*}PvThGkw8Xfl73Y~C-4P|C_wGMm53Y~C-ZDMA2l@9y36*}PvA9k48aUJ&Z6>58g zZw1Wk@jC3UtKJA4Q3_z`x%nb|QN_OUC#9${CPnO&j7Ub+J85pGRo zW{=ZhAF~4N5jH@X+2uOyB`d%lVN-9*h^{efA6hdahR+yYZ@ANtG#p?1(cUNaVtbF< z{oc;&ckbLt?i}C#(e?*7eq$rMaUJ+M_$YV{7=Q?Ptohq(AKHDN<#vnRV%qt%<;&Zz z-X3g6w;#9lgRKv2y<$t;3T$1p`HjuDZ@yr&vgz8~-1ywa8_a)gzRO%U-=yUR_yY1~ z-Vnz{(5Kq*7XaGUoqZoe6I2Ky+7VlcVDvG-o0h_ z5th3x&plv6;RKT0)N;pl>RhYODtf9!BCp_ebzvcBxzBRn4M@@eb@6B>S{zBm1X<+z zT(_B8(A}^&EzVVyfyH5Qtg39bpcZshWkl;@`>M(&i_KzNRe9ZV(sFWDWuxUr?Z4-) z-EhDY*!Szxz+I*jh!vB8ZY?-S%F)qiF~`XkxDZ^py7D8yBUV?w09>%D@+Q~=d#fw& zg5A}XEx@v>@&?!eJF6-Kuno3XRW^eyu(hf(0ye?ss>&v?0X9}uUI*IGs>()S2IkXa z`%p&$2tdvN$9tqd(ct>c8W{<8@eZ{xw#~?^kXNm$j3BQ>Ub(8W33&zbidB`@k(VPc zUsc(NybO8Sna((@fyhgdm!1Kpw^$kUOhud2L( zJPmo;s>%Sl1G!^WWiv8H#;YnLh>EDEl{@7OKI+TkOj!tsMM1@-#oo}SuQLzuQ02|7 zmv5bUfQKq?Y`tvjWvf;LY`t{r%o98m*}V1pTfe_5GP3oOt(UB-Y})$0t>0T!d421} zTQ6Q!*|_z)Tfci|-aho9+_iPrY2fZ*?rcIv$Y@pNb?pO&vZ}HX86v|oBk-_lAOmD@ z2H2kz!)2mbuHpmIpRdK01)tm4pf;#gmBB`ABetrtd4t>_S5-zfq8rgwl}#JO2C=I0 z`bK0UvZ}IiBfJql)6Iu=U1%e8z4k_DFdXX$(O@!CulkGrP;{{aj45Mk zRb|AOG$vP7HW?Gf#Hz~c#<(%Qs{ywtUC(oinvKY~g@d zzHRyTs>&wIw=CaURe9ZVzvX`I?YXe;4f^5u#j3RcmIo{koK`;cO=|~S0xnry`C@SK z>dF^^i&j7YZY5C?E<-?YKn--77v%0d|;$B_ZWpSNWKD3{=!K1*V zR#$!`c;xEUz%1DL_5b4dHZeoL~Pxv@_1H{~z>>^XvbI zj+682|A+R*`St%pS>ycr|3TI`zy8->GS096=iP9A{r`}maen>(U}x-r^XvZyd*eTO z{jcZ$Tl?wS&ea=#VEP+P`2Wq%bBNhvwVWfj{+7>hlwg#@!!PbQ>mA%S&)IGqJ;K2g zm{a3O8AYJ}5zdaDJ*D3f;%WYyr!eVoNt~x1g8EtB(2ZNNs~0Fb4^0xb=VHP zgj8Zi@p5I{Dke!Om`lf!((q&j&-Gd@D#t`@+J)U!F>nHHIbybRPC~*+6qduWSN@+oHI_SWouEge}65Bobqx|1yD>0ZRsO^*B36S(BA1bUTdh-)al zDkt?Dg%ggGbgTex35WS!4$1O&y6IZh@J^ZK}H3i2N-@O_Sl*}Am!V_dhQ zoq*$9Jx8|N+m``y9It&%buuZ&Up|9(@j^xUT~$I zc2V+p2PaP03vy7qZArr2Xeyd>k6O)CJ88>%wDX-ZpDq_XbR7?6+r>6x7dqPA1U9T5 zEkr$I0w3oyu`p9fu9%+>Hhn(5UUFu7J%S${d3uGcd)6U`{Z|aR|M2vBggET84>>r) z{U?n$&wt|d>Vb7$I=#BiIzU&-|97^EpXIEfnacd_MZ5EkYz+iD*&f+zz)|X7MrVI-#EQrUlQa zRbyO2)j>CWu3l|4a-19ml7(Sbu@|Ue{zTaw3O3RKy5;YSM1)T`g33zMD{MN<|F`|( zwa2c%X>ITF-9OvC+43PvapxO5qaCn))z+K00-GP-EN?up@r;e#y%&Jj0k8S}<{a`> zEvn6AdXeeM^*3$5c>PJnj~Z_`e9Q1O!}i)sAI{AK4g2DU6Yo6M`5yRxpa=La8nmxy za8d6dgLeK@bg4nxiUt?;4l-!tPZ7%ux)+zMPdd9Z?9)5IpxeuziYzs_n8f!WgNu3x z8Fcff!b=UhR_jvtqTWFUUHqxgQiF@`eozY+^$s%VD4)VjYqu!};VgRls(M!h=Q}1> z&{`gM{#0OTqZS?RAR~)<2Q^A_O#d>2t`!|~pWXomU7BP1mKt1qyF8$Uu0_3r40`xe zw=Ok^t>&1nMZJRzV*IIFmKt1ff^aSB9b{0OAfC9?;G%~g)TN7h2N~2Rh?|!hT;$<{ z3@+*&WKiSlCoD6Ft>~ru^bRnHdA0Wx=~w!$!k@ZnX$u#5_<%0O7WEElp~lzVr3M$h z?;wMVdIuTQywtPQ;G*{(WN=aMAcLBhx|bSUk+88vy@L#DuQ9Hr1{dA^pcXFb9b`}& z7T8jQi?hH%1{d`XGN{cK&SeIjD~`i`dIuPEYO{r7slmlr;D8o77xfM@sLd9^Wd_}g z&*lJw`}7Vl=(cZZG;_jOUt9nA`s>$=>zl^A_ujB~`<`X@?b>&A-=?=$obk6-)v+NlHi%<$C%|8)Kj|Bv)Q!gO2Gcy?!Xpo`=GSd&Sn1I?I} zqJsT;zMrWE7G9DJ8%qFD@8T#qcIbK0wSQR(xUYtX1Ab{4pl6Y(j%DKw%GZq5NR^2f zD_t_IEHruTi%S6)8D<5*FDwOIoXL;XYRwkIC#VY33+5wIJYQR^;Xl)+CnpUDJmr1Q zWTXg0Ji&y!QKG}-%k9zPL%v{1{T%jvJ(!_FnYea?(NH0kbfR5|_!sJD080V)^84)}|ufQwri$M_-J2?}Dbmx~Q5GQTff$=Z*P21s&JH{EL1$}O7Iq9mk7Yq5quS_-(2ZJKO@ODCwh$aQ6f zs3zOWBH#~?28gRFEp)1ZSf(0^jPpz|u?YCDO9A)OmpEt+iDVFOsDnm4mJ|0qlmij41@^S-|na39;yy-{*7ZYt^ASdI+x+C{pHP5$1|00VSL5Hom^ z329M+-Po|XI7zL2cL^Zs-8YSvW22rVO0jljtP-PeU2HGb@H|(m%Cq_yo+8wo8#jcP_E&|?vG(ehes$-@Rp_;?qs3Ej!;v(QTmjdpa0+OL- zw3iEoIL7DmWdg0hVz%tH`;G=kb_Xe@77o>;)jARM)roz4_>HB2`{tDHVcCt=zJ4^o zq#DcA>!TjSXJQ#&T;`<3KK$BJztLZ3GY0^_+!hXjrZ=n z$#|EsVze86Y52(6C(U8=qqV$i|6}hXJO8rxioN!pcMt6T{q7&`_I7={d-iqCc+@p8 zEOR`bjXH@ij#o>)Qi^T|id7LSS4VxdBXF4^nR9oNgOWF zZP{Uj6oeduw}zf{LZm8TE}O!fEho*B;~=qq(Y!<~Pg4ya?v4yY%~GX?Gb}&s`6wY$ z^V4i3FXt*9CFJ2MS)1{rdI_@TwpAK#RF(WxhaGnlB9B>x9>!a9RgvPo!(b&vy5g0J z-(&oUULrQi%l=-@87!6LR=}E|J#3w8q#eUPE2SrxM;8e_ZE)$XGddu#wNc)L}` zj=H2M*Gy69+(g71RmDWtIUbBvs#q=*I`*_wAOp2gGZc!jMcOtLe-*Rdf*9tUq5;Y9#m!NB6W>BD6GFm7RBM&pIrKxSRpMNzOGk_QVUGSpBu)~1>S9=ONuR64@9cbysc5`*Mv+kktR}!qU0w%9^C{r z@_SIV=0)v|lv;LCp=jUN2vvOHQofZI@nmBxk=vnf$mT$VqTkZJTK9X zGr>rfr5f&erFP00W7vWq`HNE6 zQFc0uJgW`5t+|PYlBg!NV+$?6)COd4#sY0be3oE>r`}4qI6JkX|C@kJ*ZBwCHz5o9G}o ztdE_wbkxP9wMS6)mV^|>(s3W!LOGxD)q06|InQ!}8px430#7%D0-g<^`D7^SYdJLP ziVWjykGE>6@|#KH>*pm3qf{?P1)M==TJw?^UnL4ou~ZE)(QqlIwEXzELU5;1OIFAchQwC(g!D$#7`)ecegg~pPn z>vE&jnCQW1H`+j1%6P}T9nEaR8f-}eD(P>y*&=33Bmu7*7J=VTgUOFGn$Lqf^3+@jZc}^3QwOL+9Gbz z-Ijv1XM~2sqw4Tv2BrK%iohKMSH%3MqS51-pf;jG49bTs`RZor^ z&1RJFhj6Zl*CSmkrf~+tG>@9f6XIksBR5>+0L2q$CNB_P9L=<@;eJ?X8oP2dgB~is8a}iCQ^X z8l)Ltm}26!99bkOhqV-|j@np1632p!!Z<_)OQ-X!VRv4FttavhnGuLq0h2tmt)6U! zsIqrFzl1UaMn()cvyt8ijs7?&Aj5X|1w2*&UXgvZxSR72T?4PZtKwu?=!;Im*z6OJpBHF&k{Ly|6C-g{B>AL2#rVq=XOvS@qY6Gy zhXdIocF;_F(4G%iGS?+hIYH!TXN>o^xol%taT7r_DK@bdl}dUvml6k|Vb7X0yjE`o znFysOysJ6Y5 zRmzco8?U(YRch_s+I#e}o4Swsn!R6NZv^GA`NK+tr-~{6sH<9QoYEA@VNzm?E~$nw zneG_R#o3hCuB`X<67;|$G$PG{K#fo$P)`scPr5);aa(%e35~1;ccp~$WHN2@`_`L! zi8vmif{GWFs(EL#)y`@|QMHHY5+AX9Lg|hiwYjQlgRcZ_p7r@mldw5adJsb+*`_Sv z6}*o2M-8Gb21@N_%b6TtjwD&s%9djF_1ot*JebdC<>DD8TS#@(*swItwaWH#KF`?s zS~=CTm)n_~Lu#YS`YrQzv=X(3P|k&2BIRY$zH*6Uu%6nZt)dv9*a8u7R@#*^QFIaO zPtZ%msI<%KNs3ghld0yVDBe~TG!z*4y9L(m8u^~F-a9x3WknJOtNd8(*ZqY_i_@KxI3tlQSHd98xOs^aYCd{kJKa7U+wJIhtKM5Kp| zHW7HW)aRr}C=_d?-?n~CFOjI%S*F+PYKx(~++o~gzR+)ID`vtP=OgZjw(-}qh5Hq| zBTX4|`e;hjYW}{}XII?WSf=d3C1sdU(FW}Z_PMcrnBuh4EA5EW9^-_~c>$IlaaAql z)_7P~Bkp!_Y_H<6NTuowwkWR^RXaUSTJ5&ut#G&ztmQB|5u*L% zI4fg=fLP1NDm9zoZSxYCchq!iR@o{6EWfVFr;6hUFuq7;LwMSj~VIn@N z=1Ww%=D-{blMaMEhI`=m{~L^#t*yUd{i*AF#&;UK2Y&zm%JyB`fvxXt{lQjZ^Ou`{ zzIof`?#4SdIvbA#9|S76*8FMnv&|UtW#o5}py~Uj*O-!~HREN54;jXW>()NIHr-Db z3%@bEXyF&<|M_3r14}lW9FBdPo($J3*9Xn8P!za$I5h4M3!6=b-&h7{+qav4=;phj zv=s2@o9~9(j|OEO_e61gjbz+TdeH>Tfp~1er ze8ZhfoW`-Q#r~i#9cxrsvPLVT024?re)PIzS^f4qVXqv>Wpl|epRaV=+(-=2iw%DI zQow!RYuZY*Ug-;Ud>oJpl}x_YSOk39Qowyo6D z){%HZZ4?tM+3pR~-5@dC7nm_#3b^m%1)ggQA&zY~@*P@j4G4T+EQ-1maNoCv!vRN2 z0rw5OK|dR18-ji z4-H_tcv=0u;2j>oBrOHp=QM{0Fo{b6_dS!t1DLu;13Wx{ zi9Z_P;Q>s-Qowy~eRu#E{4~>U1 zG?xPIqyIzWp$zq-0UjC;WoRq~+&7;b8V_aQjs|#WJd~lf6mZ`Jb!a@4p?WmHL*t{7se#+kRl?ym7)l zQX?lRzrWfVd8LM$!0H{bS}*j4cs^Xucj^wFr<*A17S1^_PrTzR2P#CSNrgr(rHvNy z$&=bQre@9)tNHzyyY3F=oPn;AWo-^`j0^Q!U<@Y=2HbB(aSlyt^vjNwC%id z(nnWq@zoOZ=uOwscW5K;ac4%}5pJtJ{5IKDM;>~%k#~ffXwN?Kjta2S$m2(`kvtlA zn&s}e*6HE?#kj%qBk%v-{Q$dSJ}OHxrv)VRN_|~YS~17 zm`MfvbTaR!yOo|CwF|9aGU-mI3(bs^WV=NrnDUQN*6(8b#d>q#?H21bf2iS7I?iI1 z#7BW)P|M?Uc+N__>DtJ9R&#ba9C_1{;rJ0QbUyrg=!KC7yZLM>vF!H!Bk#5|w9E~) zEQ#=eJsJLLVRW!s`pW*kL~dGeL#@{CrtwOyUdLO!u(*A@+UQt(2Z5y?Ixp$D<1~Mg zn*tB#H>a)uYa}e4@Al!sCv_%~D||0FWIB4#p`q1|@X#v@9{k(co!#zq9+)NSBwbHe zVi+Bc`T1h7(cmS_r+Slv0CUodI^qQ_(yM|g7}Lvn&e_=|k1yOOQ(31oMqx~=KhVCv zW$UR#WWagY%E=R$PaYJ94cy!2N|_Fp^W>__0{hi{s(J-p8TE1C+<)!FyG-2G~u9S*0nGCM2PyEHqt zyShcZ8&5ehqD}RM`cf`6(mr%|qjkyaMI$zEbA0Y8(W=g&kZ6mIf{TuboJ|YYW{3tA z>qiZ{_T<_LL7v2P!Y zS4)Woo!vfn_RG#V`w^A^58GFdjMvrW4N zJRzi7N~S}%Po}X}r-Rx>OpLk5$$>}>$c~a4E2tNDXs%1gG9!#0`c`|5(VX3;Is4WI zT>XD?ee+*8-@ExDgL(Zr^FQu>Qj5Nqwx6~2+^uVNZ{4+6K4*E|Rzb@I5VAaK=Sw?p z+_`gy+Ij5uH@4rp^&KsK|4)Dgd;YdtTZLlB>Prt3o?w{8B;LpKh2gwFFBuEB8~thuRnws% z$GUpOywB#O`?k6(M3z%7z8R>=y|L1MgkFF`QJ+;!@=|xCviWopA4|R>&RP9c(Zv?R zvct`X*o3o_FSEDJ8n+EfEt{*-#n?t$7}q15TGc+BDcykV z)1stn0W4<4T3#FJ3zqBi5L_bV%MF~>6fQ8sZpG=x^OaDzl*c zL!MAw0*gukP42um{rt>~(E0GMw zBw7wTgGIYl^d@kxWKU9jg{a2~!`*s`*bzcepFH>V)k9GedI56vz*4(jCvx?OcEg9~ zB?zTHZblMH1@%T#S{Qttsop0V?MH8$upgoWVW%#Xm2`^nrxKfEsukMWG zvWX_`?=^VQxT_snfLMwqVPCS$3w~0beG1RXx@QKT;CQ*H~qNb z8+wVTFwi0_3vAUfNH*)Gsx6XG*+Nco=Y}blJuyzXoV}VWtU7~=;az%(2wQg#(v)iy zqAMjOml!mON;Q$!jsm$bZ_FET$(?f1YO|?1n}O6z;CLWZi8|Q|ni=(}nvYM>GDk=` zhl;nDGEF)fA#$7vRRam!@O%0Lj8xmLoS2HbGQ)ha+O;#oP)r%vLZo{jl>z}Zn%7P? zNO;D}$ewut71aX0W8-m1QWS606CZL-Ra=RXZmH+!iH?C5mFpMfKq)QEV>lxtv|Niw z)c~~>XuL)$(Q&dOVh-H!)Oou_((V!5v6!zclF3rZK?l24 zB4>B|iKY)N*vi>>ITWVr10iF0U|ymlxynq6!AlVviEHlOrKA*awy%CD|R;1 zjccjGDW~z0?X=1lyHdYJnYQ>RNbyrY()Iah)4BGv#c`%O)F`(=a~@g^S@hhs)`%igkk(?aTwE zc4T&>m33*o_=+Cu%#yRhV+Fe&qps6usjY@I5jdOQ9k${s9v>U}@Lh*)& zjZ%Yp$8u;nG3317_P8&0)S%pyxq6b0hO2__^g+)^ap0#L?u>PmaP&$j zOS+wL?O0bAtJ7&;&y}EK<-X#y+r_-c_zc|!f#cLLX78oRy4V^I!nnO?50>M(U=Ym@ z>X=;>TW*JT03cZK7)8x7(4#rWyyX?K?XdbYA&Ll%C?(A*#UhgXEp+6=$2ZPFC>awtmr*h+2n*cGF|SM!9}47obbYhL5kZL#N>{bsG_1 ze<+AC)mhBhsc1#yh_Ff)gX!oH!?|&@UUgHLT#0%^ZNl&-y+l|_(_F=iswqzKS9_6I z(`Rq?#dgpaV$)$KmhD7a0>_qWUd3>U?ug-zGnB#v58F*>@$1-la^k5k7rHD{e zAv*-tR0=UqFd<(uH&KggQO|KS-EO03tAqxkjix}k-62U0Cz~#nk)Zp~sVNsZE-+!2yWX<0RbSUGp~<|Os2k^R>!BK6lPfkyGuiTr zi8R$N#u!_9;4Y5PbS=u**raxzfXp>i?Ug60KaNng%Wz@E<883bLdnGz$Jvmr777nX zqQ&{EvC|V!libdhB>|A(Gq4={qm?i2U=5a zZo2~{AF$(8iYXPCq`#;X=O#k+{4nkcQ+Zq|;Vh1c;i7i%I#5kg*>JwnN|ZZomz*Z) zmTm3N^b$un<7)WZd5Iolukrnk*Du+ z_#(CXQO~tc&21dvsORs@OOyrepstwgkaD&g4|8g`m9OHwj5e5)q+@JvGKzKrrjqnY z9>eRj@BaqIc=y`g5BKih`_kTLv~~D<_wLzy+u)vP4@2G>uc-sr|xC|D8X$z=JQZvFAr%eP** zHQBmztH0ISdg@koE4Jm|@@(C>b$sj6tqZoyn?K+D-saagKfn3O%@1zAd-E+?M#R@> z`4OJK`J0>LO>wif`IODnW@Pi0P3PwIn^$aJw7Iis-1zavw>Q4L@!5@!ZQQ%@_Km;T zchsGX#!Y~(lSjo`*j8@7$BH!j<_aAO1f0{j5n2fhG41wIVk1MUWY z4*m$d6#O=LHh3E7fd;r8Fdzz^1YF<-a3#0|>;jYdr{)LDUp0Tu{BiBRhIg9ZY<`{j z)#ewQpQq(#9GQ7@)m$_u%pvp5X4HJG`Ev84%v334aWM_R~Jk*v0J<3~KmjmUB2Qse@}Z2GzBd#10OK5zP@>4T)+GmOUccm3__f3g1B z^;fRnwLV*a=K65GvtC)xuhZ+n^_#Q}nXA_?TfcC9!}ts14~+L2zhL~7@x#XVXpVaN z2h6(bMx*&w^DSW3fzmDJCxTfUN>4Q33}!7TJ;8hvm^Gnvli3Sq4Jdid9x$sz$zyhd zSq(}qGX`cHlrXas%&JgwnjK)qLdju9!K?x$yV(Y2WhmLqC&BEgP&#S85zIxoQ8A{iguLZLrl&&#f4Q2%>U2VP!%<@pW%6uHm za!@*Memt0Ep>(DB7?@?Cbj*AOm@!bg!u&WeOGD{#=F7n>1*OZ)zXoPWC|zcLESM#r z^jPzyU>1kcrRK+g84ab!m@fe{3QCulF9x$1lrGY;aLh<3J=**zFpEOzQRYX283Cn7 znlA*i2$U`~KLX6cP`bdp2WBBCX=DIqIFxqH7BCA!$zt9CvjCK~&0ApRhmtmEz|04w zP4fnrJqb!1W&mclLdk4K!0Z+%Y0d>^PlS@mybfkJLuuV?1hXeV$zWasvzwr_hWrA| zyqbjk68SeU^FZkr$j`yd4W)lWegg>1 zV0HpZ-$A|wX4gaM+sOT3b{&+yg?tmtu7%S5$bDdT4V1o#d;`p`hSGh=*TL*6D18I@ z8kil2($|r%fZ5}r^flzGV0I;xzKVPq%#K0nD_T~T*%eUw67oebdmNO$hOr zq4XK#(_nTHls<+0J(xWjN`H@h63iY2rB5QC0JBFz=@ZDu!R$gP{T=dAFna`)K8Ab% z%r1b^N0EAAu?4KjeQPuLM)bf5;ype+Z_K|BydKUIV6(|B%-pe*mVC|BydGUJa&@|BzQB zuL4uZf5_{MSNqV5;+96KQ~{&VLQ02Btdy z)e#O%b^fa%RWQ~0k3(26)%mZARKQf{KNcy2sm^~DikC|6qxG#Mip+Nd|;~cpAUHwnCkp@D{>2%>il;L@Siw~1KPz$rnCkp@19AdPbpE>@xfV=x{<{vj5=?adyB4_y zOmzOc2DutcbpE>U9@}KtJ{#`JE{Ac>E=>af-{AYT=^c^sP{Ac=(>Dyoe`A^H@ zem|H%{xjWg`X-n_{xf~kbRU>N{xjWY`UaRl{?k?gUjq}!f2OaQz6vIg|4d&seFaP) z|CzpG`ZAb6{xkiH=}TY&`Oowv(-*-6@}KF8rZ0dApKK z7ykq%kpE1dGyNl&K>joRqv^9?0{PGMS<^p&3FJT1KbSrPCXoM3pEi98Od$W6K4toQ zFoFDM`g_wS!36T3wpRHBm_YtBecbeSU;_Ei^mnF@feGY4)5lC71rx}BrjMFF0w$3E zOn+0Mv~`A=I>y#q`h z52bgR-VP>LLg}5RzXB7;f2Mbs?g10Xf2Oya-UcR+|4e^nx*JR&|C#PFy%kI#|C!!q z`d?rI`A^Hp_ZBdL{AYTr>CIpQ`A=K3{Uwb5UtfRi+Rn2#F4q2c{^xuTobQ4EhCMK8 z&mIqh(q6zCj#~@;j-9l52pNmlwPb?om>MSnYAfGp75O&n_Qaytz;AEnEAqn@lx9Ec zrPoIYkAp$BM>!mH_JaXOI9+tM8+AoN(L&ifT6lCvh85Cd)|yoq5`1`1!2u~*Zyy)_Z?SuZ-ni+k=HJA)BcFTzn=cp zziYP+_u$_eO<9Y*R!hUC+^jS^%|5(nTQm5e1`BTkY-$nHrLuN-0#^Q^*z|Zuasc~xGuJvZ2OGdC!Xomh11HPRO zC$w@xSfig0l{(syi=0$WQf!F|xJJ(2tC*7<_IUE&Q3Q+1z0#~@$ZJ6<_iy{n_p1~`gyuJ!zBN$ z>E^(Yd&$$P4bwv{GiFP>G{$dhu(p<;k;C0;)EN$X>9YG|)QvSMN8;Simm~^AhcD-H zL)4yTV;MHzkbR237hu>jK?6|zyrjA0ng-7TSe;nm3Y>Rdjy$CIC5%`XnQ2> zlkO-|t#g=r0rh97Y+r+LX^~^E!vziYKJ*Si{`CY&iw#!uH#6i@P2qO6w7w!utgPJ z7#7L1F|}K(kIO`I5DDj8Lys?8caL42!pUaq#Gp^In4=NvyGfp`bCHxIK%69*6T=!` zPkX{;pR&?Kubtq8C%Y`*p0z92Ql`BZ@7=Kbp53D5d)n9dOLtzkbJO-~w;#RrrmZWq zFWdCS=fPDQl=(&AYi79PZ_{=sp0fU-^|J9N#;Nft!(E2!*WR<1nlGEWliSQ(@>a{* zaZk~a2uo!vYpXT8Sg|pxw(M2a8qE6=ajVyx8ua`LE@VwMM)3lRERp}sC zNyHcO5uJXM-T$`AO`f9JJE__0;#yAmK51)2nNddZ@^x2(%_d?Y){!HVgPahFc-nq< z)SJz+E8ByPU+N7yoLrwgS+nEN?1Tnbyea!q4%(lyD|Wf=@&z+8?JUq}Z0s)$+FWdu z<)S&6R93e0&{u_1mPt`Fbl8B8Jlj(~zD1cw(BL%7Z6SG`_28?*!B>?CAE|}6S3-$F(p?M0ZOKp^ z%f)z>9SBj;{mK)A6mzbVKj_)iFk-@*+ZDam2jrBw5?lF zv=a=f)wyzg&bUolvjN?vYHcP^F({|}cHF8ZO|H~(HY(JWiA=RO97dIHo{Nk+iaU~b z9@Apz+U=CT*vYW|Y_>*Gi`U;QVVjg@54ugQ*>@}UPKU8|OU+2e78~=vm@SOe$F^Lp zU@y6?8LEH}y1jgTWqS{Go1|t3y3Md2Z1_^XL^$Xiv?Hk1OH{?8YpiD7v23w4RvE?X zOB4ciTQjq=odvf^z{jMyjo-r;ltQiFPIMflR3T*@g~?ndJVvoZJ>Uw71T8bUY`*Oc zvn!ig?lz0Lm|zE-+MUpPg)TmxSUv0Sv5K|_7UDu_suIA{L%K5RoSh1mS7)r9@+1x$pt)1F$A-gcmvi7@OTj41p>)#wNNYN4iXMc*?f& zGoBB{wKZ+xqE5R~-OeT33@LUBMMrMz6da+E8W!bLNzO5yN;l!A!$IC>C-OP!Vd|&F z3{udQx)VaH4`c`TDC+EDt&v0*eAY%d!%{e17;3jyG^%}@o#tEtNo*5oR?Awya(&MB zE)vn~L3SwGP!B;?#TuGo__k1u@xDYTQDfDb5RS<`?M?(bGL~vKrmn1P@1g7v*6cub z2ucpZRjLcsa3GL~OGz}SCD%YhO4iyGW354)ui~Qy9g`zrd1X5b><~KrNPXD|HfqJK z3L14dZQn6+(+Ul7mM?O zWZ$Wz8PeMDXGRXD64WkvsrwzpX2n&I{GKH3Ck|hYo}N{fH+u2#|9re_q!`evbn)>c z#1J38seMjW(n+y9@oN==W6P>Kvsj?Z*f(ot)dEv(!D zXS4UD)Arh`gOruJwIs-)yxpF&qushKON9MtLQ0Keah$1>LMkRC2DZaDK$pB(+^X4w z+#zCg+*S;0$MV8;?e5NQRLQE*VVieXWG`XOkEI0a>jiBQg0QXJpAT{eH@W4soq`)n z@v4uA)U9No>kByf_%Pn<^WKJN&ER6Tfznotjq^-aS-Bq{YUhck?bvDwLF|cQq@@h- zM!QVNVU}^)({!U2tj0(+6)zEdqNFIT6*iDh_uqqd>XVx_JCF;NNT%xYhr;$?pgEvy z-9a^^1jsZK?GH-Hct@LYMq*S;!Zl=8_rr&BVSVz1|HIy!Ks~PB^}#dmGV{(m8?-D5 z(2(H_3GKy+Y*}7HSR`wgwb_!jKyW2lmMqDZwb-)#wP`7|WiHEC4(Wl@Qc76^rD1Q_ z!j=#S5D0sqtYN2w^3lRc3+?wk?^wgkyfbsh&NmI|;m$qhJ@;7t`Tx`NfA;@YN_wUi zxblr2mh_rZ(lfn$e~hFv`~MM0(0}!dPF{NS@4)}O`0w#8@Xzw+UwQY6a&ht54$vL* zJ=GqQ0_WrQ>GF3iBKzD(f_t@nE@K~m1-i$WeB2f2{b7Da|3Ul72k08~uLzvog3cFq zG;!{l_3ovfssmd88!eHIdbI{5Jk}D)5+KV>(ZeCL-6E7R&Qb%d zO?W;<3-$fBM8d9$*_&xM%=HGJDU%>dX1cCHY@Wjs$T0vG`Ch{)0@J~WE)uS~BO37n zFAIh7OpU8ky)ainDku4KJV)UOIt}~wj(OJ3Uj588kU#&5d*kkzE?DQr-4k<*J|-K_ z$L-^Sad+H(z#S#_SfTbCcORf^LVxY!4r)JX+$~AlZ(z9Ad`_A7yZC^E;s)pE9a5rc zMQT$#p7I#GYL3YF-vgi*BBa1)%s7{GNL$0Gjn5RX1uscGc#dl`@zV0(C}wn)Z%H=V zRA-8^Sg5b&LKmGayxyw|k_p4S)xu?l+dTK&h?z`w%q-fdg9KD}$KA)@8+Xri1GnqA zYdy{+x5swq?zsDadkpMD@O7Yvo%0y@t8V{@d(sEUU*N9@{B^B&Q1eOSu2}`u@st_w zJvZOvz69wusNDw&ePVMT%fxJDw?U@Tj)e$|`un}!xwUwMi6a=DW@28i=5f%r^(HZ| zY0Zu}w`YcDw#UV0&gCeZFQ+WGM13BVI5*7qEndX88V z@BcE7t=)VsN}d_cX}U98D}EZMquk?YB@fsMHnlz-rdN}5t6@|8 zbN;$Fo~r9+G3`$HNt+sXmIJ5R+1b&rb!Ptur`?xa5H8#t+<4QCOV>a1+MBNaudBl8 zH-J#x(a8^-e8lA$h#>!{<0Xifc-7(a2furOU;K&-e+~eT{r6e7pMUMm3m4s2%NH(O zIk|Xz{JN8)eGFxfT<$#G1aWk6`UP)ae*M|s;?9$d+ULzopY@98Kk1Aoe4ppxK8Ci( zT%ERj2>|@UU=ETlSU;g}S&X~UPKGTDJ>~tm5?UnP>IUby`n}5RD--O(m-(6QN{ZX7_oq`l;}*Q{>~Hu_cV_tV=UeAm@#;Rt#fMw*sg}m8XY8CG zJ^Sk}pTzOJrSY>~{(SQs$J5)3byXe^F>J0{UeRAi1-z7pZH>l~89!t??W$7g;N#JV z3%or!o}RI@w7Y+QWDkywa~!YS-ow^>8^`}fP9#PPh{540n4j^oKbX4^eE zo}95We&_73DLskfc|#W9SU<<{^6f0w6U<%>>qL`8`*^UKi6CufE$72os)^0!bmfgD z-SF^cYfp}s&)8|7bNBD>?!gg0$MMqbx!W>ooImO`E6q8PV0dE~Plyqa2Njc z7QMSZa_>1~+WozqnZEq_SD)*p2euF-#yIhPv-m7_TBRR?X&iUp15v4rhWhJBYgKT&p(;(Q?J*y zeDBUqul}wlweKT*Z$JMDXM7);@Aok8mwX?X@1NQa?#}nGc0RzUA3G*vcp@r>!mz2`|x&ujkd{C|3(b>T*I?cG=LE8l$bF_%8~ z=)WEQ{fj>VK74_HpZy{KmrU8od7L(MVOn^U@@Yisih6p43$T&92Y!J6hS6SU3X1P6X|V@$#K zJ6P4J4c7YEa~$$yid@*Wdd>&vN>m7dWyieYt%MC)bQPw~EAz~*3BFB%-1e~C0Cm?2 z9=$80c2@@7LmW!=q(0^wH6e0f{!G@I*Y3&!zl{WDA8;UqOrzL_u{Me>k-?z4K%2*z zyWcfu;JY=%AJKy~_-?n3%`7@w=_6+bQnKQVIkK9xD~~B%qla4Sk32b3+<~?g>Z2BExDh|-c=mcXcdZpDjB2DZ~;sWFuUi3ncXEqnA ztE`gZRi6vv9yOZSYcH0%G>i5XmF(@TlW%($(8)(UMJLVkAr~J+I)TM!HBLx>AcXCj z0tt#bZIfaUrSckjj80J7qF79gVQS6yK?Gu3?DTw*N+-2B-4rBtBzGv1uIJ%k;KjiD z+TjUO<~uv<zFIkXwiUW0q5jO8@M8iG>b9ao}lhNcfH}uflfZ`X*xL{ zWAeeI6Jhg=<}P9c3sZHfP(?8gi>4D{5f_Dz(FqEI?}s_Nph2GH$)G8P6=u}dARF-q zYa@-#Rg!DNrjZw7=EX*TAgW52B6rux=lp%3lfV5mot(c}|De)I@T_zaBGz<-$hcP> zDZQDnUZp+#aXP_l-ZvSUEF!eC6q}8JtnmX@N-aTALOeDCA`a^wIq5c&B9^c|t|%Ro z<943Af`1Ei^0%I*lk+#rA5=PdR&y7XHMlUUOG=~;RD|onr0>%4W5x+a2raFfPUTuW z#^ia|8O-Y`?v86t%?XmQ0xMySb{!1t1m(D=b0*edMSXXje0mOa@}W=D$@yFA4L#PtSl(KICaSIe#nvL8X&tHFwcucMxD8$W#dUepU=L zGw#xl+Y95%2BXiTIknW_AQCGODdsD=0;(tx9gFu+*JyQ6Qk`^>*o%c8WuP3$e$(Aq zC+I%}I=T5YoqQ0V8R2JkW+XA9KQS$yoYPK##XQvxE&6f$E(~_GY_3s*UdOD#1J(;#um6wh-*f$|ue;aj>y_)L*M9uk=Yz_BjcXr!?d0k^u0DU2 zyZWlDAA0)VPJik2`%b^=^dFzT?({WR{^H8_Uili3{hqnm^gUih)o z_Ssd%#dp5Ig~f}H{!eOwPdPq#E!57 zKkoS8Q^CjYEkAzrn*Hei`KaTASA(zoQTdhoH?K#3@-fE;FnIKz%SR7h&K>=ilJFmv z4_}z@`3>69AD4vxpoEu%-&+#?{qp0-5dNc*@b8shd4TYLE(!l``RF0Se^?U!o$}!e z6F$FtIr@W=@Nbv!lJM`BzxrD{{p$Jk_vrUZ_Xew$B!ZW3nk&7F2C{s;h!%F zzpH%o5aFLI3IA02@P!FKGDrPvN%$vAcuDwY%3powPQUudMDo)m{3qT&_`6E@kM9J3 zWLEg868>X5!Otf)IQq#F{*ImCkG%UkOZboO1b>A5Pn2EdM@sl(y2_82UFGfNS03mp zKUQ{?A1)s~)K%V5GJD%j%pRE*f3)l>Z!O_vSNW0hSAS@yUwvdwdV2}~!S@gT!)0^* zKnZ_LbG@zn%J)C{D{n2o@_kSK$`6%|{+9A94>bA@mXE&o(MLZ}KKh;~u`HeE6M@d-%O&d%n4R_`>tSBa7Gfl<@z!6Z{cves>A~j`t7#U1b;h z_7YxpvF|KD{HTm%rxy`{l1M_4w5#{4sib zV=4Tr9u@v|r5?YseDshWzqVxd?{;GL$ny3zr5?YcgqM2!>hf3r?M}b?$g1~M<@GJHtx{AkT$+ z^q+Q45qF>QK1u#2cRmwHf`O!8-dWOv3g0Ku-)CLD^Ex2xm9KM;erac6FT6yYpW*gN z2I$=Rj59Xi)!)4{n{9vGC&gcU=hK0dPX$tbap$yckM_@f68?F2Od#>B^Zdfj5+CS1 zd!+iC+%bTpv(EGLJ4=OkJS!{M^pMUbyp|Uq$vw3FzF>&e(ug|Jj|{ zl$~dfM1S#}0g!Umd4A^UK_5??-1bTP=iO0(#Iw%x(>qIip!4jJ@NaTQ0g}!-&%1V( z^ib#7Bk3RLJpD6ufzI<&I}3Z^&U1dVuus5|bEkL42E6)D?#!m_JbR?311~L?Fa6G^ zdzSCAvNq4Jtoy_V0XkAi=O_Mm(b*#u2+$ErIzRr5=$t<;?-TU{=m;g9AA3f0&Tp>v z3Ge}Q_>#^$o)Mi#`pzD)Jb+HOr1PWCh|VL*+9PZyzQX}FXVdbJ?9ApdYx*Af7QH(x zka#vNzkO$k4@}E@gwIUwFhJ7TwEV+6OL}Np-XjDCsFg0&`nH{gz3{Yr{$gmKI2Y#* zb;bs~`dfEqQ%=i!gsOm-Cd-%pp{Hjg*lAimf77u~SPGy+lyrV@=L~i)tb<4B>=A(y z-@yT!v!V3^J2%c_hSnbOB=Mb2De?Pve*ecv+#@0+zJrw#zwiG?F2vh=#B_LfP$2Pa z`gqIEt@^<9u}3t9cjwbeN#DD3Yd#=pk9Z32&Zm}=zGvrFd_dA35fP^cA9msIT=?7z zH~+zn_ua^^{|Pw#|Ngbn)!(@IFRp&Z>AO#r3!i)CU01p%KYD^+{(;M{y?E`?cV4O= zfAhhY9)IG|8;@=ue#PO(9DM1H2mcrR+GibHa$Wi4;5JS*so8uZ*CU3@1(la{P|R!* zil71r5}`}D(YJ{WsDNc6@oh?O#8xeL7jR8j;tQlDFZeL-NCT;3_HY=K3L(>8QtwvN z)~zI%1Tr?}i>xCl1CNuY=h8!8{pkA~bkoqGy3;g5lC)VWQoWF9x%4k*Hgtme)PZD0lP!E{MUEq4znU>bSkA2P;m(Bb4kCA=`k0M!gf(3OCRD{tVPYS({aZg>Em?NC{|S zrkNv3pZNI{_p4&i-k@nb7^(Fvwxq(18-W9Rx2%bn6@?$0N#Hxd#@;yR+Tq$C0Y!bz z3o7da@bG>+4D0Mzn1-ycB0S@#{8XDnb9@boGEiNKvuxl*!>=+w8^pKCIGr}7*%HaL zRhQ9_F0`p@CYm;4QXa`-y&*eLXNUxA4HRaeWP#FMf8piIGEgawIoA%?&HEg}9AU>UfS3GeyMa+|+}eg272_ z&fi}6287K~V$KFy7*+5znjkH2kPitz8n)X3ig%Y&hok%gy;Z1-c(nAgDas}st70Ab z9EXGNyU)SKhfK4j)4nV&d%c0}vo&SCwm1&LgfSUa(|DfyZHtwP+jY4v8BrK>U?&q~ zy&3E{-MH&l(5Rn{dM?Mz3$!sJ`uJMC)lYeOj`L)X;CejKE$r?&4woq`)9wUP#YFz0AAq|UX&>384fpl`?l9n}RCt|z=svh7aYlF0UE(}fa9Vb)52k)(=k zw&UBwa1oADXG3Rf%<3FcLpJ$v?&T}2k`aB+8&x5-Uzr%3>D}7Iij4YHF~xC}=y3U? zy7<=jivwghDU%Zn$zYU6g)5Wdq?#^N9YO9OPG?LthO?U1FU)qa1BY+9&p}hA&6>sv z$`NqXe>LCs+Wk1F5=(b2MU5iim+TC}nkt9h##`PfYcxSi3|2JTQiaA!H>(cWYX{Sg zSCF{ZO6t#bs}82#;+Qk9z)jf0Q?{6_i(+$57gx~x90riBBywOF!)n*Bb?S4siK`QL zvf98Sn$6ZAcf`<=RUUF~^SwbDX)NxpIZ&K1^9PN=5~*|$F^6YVm+x%CHWVkhh$6-g z?eNC?9F%n>z+ly2+K$T5J!(+97(|U-+wbMr(Sg)w0H* z3TK*NFBe{_6H68|CAih?4Ba6+T!YFsW5AIT@Kv}>q!qc+uygHj;m!9s2uLC=EmW$Q zm@#)W!w?FX-7IEPNZdFhVW2A-r&mOG5H#D{I(TN&L{ zQ&+v&lxlN@cPrJXqThGNskxDjjzQAdISwbc?{nz3`R<@jw3nl3J(DWnjROpns}boAyxk7Rq<0 z-DI-C$S|5G+{Bj7b7z+!VRhDSOHb9EmNtJR9IpctQ(A|nhYb)i^kp*m;$BD{_2 zVJAf@eUgt__QG$Wp|WgnLCSh_d+fApE(H?{W!hAD;}%6J+HAv)my(Qma)LAGOQ&4; z;`E!RfS@op$_u;WpKFKHllvS!7HVgxVhx$^hVbVH=p?PnpIkO;Cae=?3QUh|) zqt=o<1>p-Bfyd`KoV4$AXoL3Zx>#V#b2(yNg@USW4UILq-P=fs)1HdMu3!uo5Py5= zSH`rzu5U1nb6TP049vOjfldL5Jt-L*-Izh^AR?^W4sWd$dD;bmy=LZUT$k@_=XG(S z-siCU+z0G@!mKM4b~cK9KgV2*n8MtIwG>%>b!$=}jczXrBNub|`VRBtr#-+ypEyks z$?c}l6`@*`KQ)?|lVi+F zD2h}-@hd10BFjLtTRxRZQ%$$ym1*W;l&y?&x;Xte4{%Uje(cv8YZ#^@!(D}>y{Veb zcA~8pBe73!h@4xAV-QAs+pj~W;Ed*!;<^RgQF*rSw*sn@&G->K=)z9J(1tjHO%YJ# zLg?###Ttpc&!onj-_YkcTzcny4oz!0J^mp%bsQ1QeER!m=WMu_Ghf}fjoUgK=7>gu3uABCPHH}o%Wv_l)+DI;)MLw}a zf*jY}hDNC%VN&6#0l9$J(zHwV+~sBoYV+M9c%SS@eleYCgISR4I)APmj$U=21H2L^ zQsVbpZAj`RTn`ETb=zPidX-^4-VR0_oY7GvPUPE&-JOS;wWgS5wGx4SZbcflhgpNM ze0i`OgqzVsWa1$-T7x1oVt=*Cb3AtIZySvGzVqZC~hWA69a@qxz?z) zHo#d=RZtP^9fTEQ_ah2hqvy{TS5HoFe&Ge|;zwS%sowmU8^3kq8*U^wTG#*I>u=JVPm&b2Ce*RcKe#z0VAARl7^awfp%fq)G zK7S}5zU1K755D$bdQiXk7Z-o%;+=~UAn`R%I9LLXJG&P=;V1U|xOL>d^x(FhH5qr) zsama+*h~g3$ZE|OTwNY9Ew%ThJmaPu+Nw}#vI>mb3L_k3~ z&0$lVVx2|21zv6414=f_qpDD?f;vYAPi&UCFgDc;B&xK#%+Mr?xfUu|x-3eG={;Zr zuyaV)04Kz>01muu8*c=Y>&2kQ05?sSj!m zsW-a!j?q0}%`+#Y*X)e?^_f!CQ!&ctqxwjMdpa^&N=+&0hb~KG-u4|&-&_KNN?!|W znbRZdLv$)Bf~fdpzpLmQSW_rkOe$a~O&pz*x4_FEQ359-g0JyaJZoEwqxMp_t*2F@ ztB0&D7!ty09w@Ipm1lOVlz3bMeFCNj4XT641DZ7_{gpEsi3(1cxe|yBUeU)EnocTW zqimcDKUD(9tw~OgP>-Kaqgr(V?!qQI7l)n|unD%5X9JvoX1>~tvaQ6cpL`G4ls4dg zVK{4#;lW@rs9NrbWJGS%5rTTL&Y|@LnJnd=hi-v~cnNei6qPbUFm4obE;juF>Vk9` ztzucsr0%3542mAVNUhPV?5iig*aV~F;%(h2P-U}}oAgTL$pmbO1Q(}(R996F}-DK`eLr_)t?x?!> zihDq_Jr9P7-_fE;>^c)&F1Qw~^h{mrj&t89IzeDHaoj1jaj29)bE5TzZL7AXNdhH> zELR9RGW(XY1O=>$p1@4!jViaHtg=y$SNDKg9K|)?Lp8@hJZ&~0q!GL7POxy)B$%-c zr=7&&9ZfG^@x&{E+SE%V3&*M|1h2-qD!8f;GAUFHH5f)f_4fAKpT~VVy(f{o2P9Su z-J6iJp@Q@DSd1hD)OSwJ!6sS|nJU{56ZRz{)17VOT)NkB25dfRR`T_@m2@*?t{MJW z`w1f&s!M7}x*0n|3%qTP%T~SceWgTY5!jRQd{hnek%dC5qC1(-u@Ojs!NPu>>Md0( zPe~~xOR5JSUIKd)s4DTi%LzS`lSZVnT7~n(huSz&pALcrQX9q7&16HD>bUR)_ki4* zK@5Xu*MaohY9WUBrVSM!#yT)EZ-FEcUr#n^NZgwpPd~B*%Hb#mp`f-KtBck;sfLbP z&|;>s`WR}1;Yju@7Fo-76Ds@o+wTEUy;E?7I2TQ0+#YT^GqloNO|_vhCPF@^g?XhY z2ytrPYt=6;f#L$n4OpOhWNPR(g5`M71;wr(^>jn(2CNhxmW}3i4uc&!~*4`yCDIi9|y1k$?R z@!MW)7FA=Af2Rp>TPaGIy(L=TYUA3smq0Q}0y#5-*+fZ&UbRuI zJEVvW;k8%qli8HFKutoGYsq*i@xncc{1a@MtOEn@Mi7B_e3?#je zcSd4>nKg>YEu)&&|qV8xW*fFpA`yuUe!muEFLw zoI9crwv<-4+L4u+I^A^UShf6-RS$5Ft`I_eG^#Q|RI zt-H*Ccgz(Jt4R=>g=?f1o0NJy`REc@H9SliOc5xYgP}C)`}mR@nQ5nL&~1D^U$(SD zlB5>n-kYnwvjjpJZlG(d+6gMvB$MFY?$<-o{U_53HB^@$TlUzUhs-DHs{lP6qZy^|K%2_fzy-QY2wJFCSfC+ixgKk z8~jwHO~%N*USo=&7`TSbJK=V89A7PgD*;~wvfon(vfe^PrHSg!B+xZb4#@HQM62d8 z6|05klX4(m`uq}DY^Lf=Yf@~C(;k~Tbb~Nk5@8K8ZKld|*URa3h`_LMZ#*I;a8V^m z3|i0=q93hULae4DNROPR^=hViW1VYBna>WjZmaA77sC<=8kO#*95#-iVS$4Jg84Y- z2SapFL5-nrkQ-lVyYW=ol)xMJ-Z5pWY!DS`E|3@kPU>7BVD;d+uD;~OSt=yC6Ds}Z zp#D8|oW87-nD`q{sUV$MT8J_&cY2FtjEKe zm3|5}=F1SD_>$Vs7_D7fAP*RN*Eh~s*z43)*1o=+K;Eh!+V4WdKkD{W(IE`u4 zXy{?t<*t8g37pXw5@$u6Ky4`5=$$&aCh8;wvN4$!$}YgZvr4Y|h|iZ($L05xz^P)4 zJHnhxqp3Y^OxM|X2+TpqtRqCagXqjg5$zlSyQODthh7ZKvQ=-yrNmJ8mn~$-YqD9bq)f7kMrP1lNGYa97{3-Lxv1gp zz{<+ear{*!Fc?)CsbPxTMpO{itt7o_1c5|$%gPj;sv`#Hr3CNx+4$Z}{;4H!LJd~y zNP?GBQ1uX8nYD;M$Hj9)=@4OGFnuR3Iuy)|r{xyl>c0V})Vq=Fd!AM=T-Zv~QEM%S zO~QonWV7ZvwlB<8iX;aDF6V|B=x{t1iiJ$@`mYqtAM^sU=LrxXRQKiZvxL)1?1iYp0Nsuy9D<5(Pym!d~X8JSOuNC z1op^I{;X$0Y)fGGn%4Ui0DZdm-Rs|g?oD71|GZrYeA?aw_9%U|3xQAFo4_7r02)F7v6E<>Sv#R z-l=i=%G38=`Lm0|D{sB>MOTJ5TURP4e{u4TlP@`OZ+zud`Q#$VbOPV_lpEKs|MJZ*yZ*-O%j@{{zjf_5 zuD$8nKe+m_*Vt<>x%zuozhe+T`EucbC)Gpu;I<2mwLu)se2|uULVZgTSyi5lSzWAs@LEPFL2c+uGJLah9|OF zy^Thw87B}ERZVPcc8ui0kx>Utds8$_D7zDYB9-oDfMt^@K5Yde zD>007@(Wu8C~)bdsS_A>ciJkCADIOzMo69OCwdGI8{tEuxR|lU27} zORG^|T>-1Un@`O8IGPPyqhB2mqM|f6t!2Q;$M4!A3w zon~FF?J`=Ui8NNZIt))XTLe!`lxa{n><}g^3z|@dLw!79T2>WA6ks!IU5xEXZCM-0 z2jzQobvzD22#fRaxXumxLoB68cv5Wz|5R&TRbrlYCIIzvEk`o7?xkWH$A-5S+F<^@r3PWKujC`J-W@#;W<(rMH2TJDDb?EQ+MbF8hhXE)9ORfUpro2mn>Fku z=!HKm5tSKC>JqFp7Q%sR^zyl728@JEnK{2=j0s*23ic_sy3 z53M&xle{h%@!@GXyQb;G&`UI1+`Gy8gp6LB4BfwnzI-d+HD~R#=HrY zlQlYF#(I!CYa9V?_%*Eq!z=Z|=avWvLVPAE#?WxwZP=+%;GX7<8#ENA7Tgb~Vb_KP zzXkf$AKXQZRv1g@*#w@mqfBUyG=0fv6X1LaJbY@gg=Io_)sk!YR8lWWB?2Yej7QY$ z2FAzDhLcQ%Hqo`|qROJRA(>V^eBDbl1SwA5vPCTE)?&ct5HC&i#XulxB(BfRb%Y`! z)af?aSrdb@6lQhwWZQQ~%c=#9d1Tq1TDsd{CV0Wxy~!#`*CQ*z2tQLNkk%rXM0QlN z%3T>ThXY%$uG)HiEY5?PPj;(PGYAM_zU+tnUb5*1Q`@-sv(TY?@%AVc(RRP>TCELj zE`S|9pjf9HakJ4nZDkYJm?0ELJHo2)UcG&d1Q@D2-EihA!jM>Z+p9<)_xdXv_H2GU z&WO3#4?=CU=9tSrvPI0-Jr}qetSDdKcvG$q+#VCnWK>V+^_xgFspphfW&Mu8pZr*f zs8_v3QfmxW3$jX%29*}qttep3Tj(r{uz7DXG=#tp!}`h5_BCR95zObnDMKH0yPI^8 zY%I^?M3)s^!D~oJp5g+Xi_WON?Ny_hl_>22@N`UJ-pUwW5GKGIho6v)1AMT_4$Rc5 z(GrWD*3pH#q+DGM7#ZoTGMJo5TumKa}h*G<0Sk18zrOb4k4X}1?R&-}^-s~+WDRGh8A|j@_0e<=$smR*`-E1Q+#t(d? z4<;PRS~fjxWb~O<*0TAF|7?p0^pyPDu=>%Jh`V&Hl=D=1Jr@$Lu^~btGL~Xk=^DgRJ86!dEl6j+r^jbY-+))Oz zu{VI{Eqa4kQWs;qe&NHL5n{@R{Vl=?bp@NPfU{zE4E!=Qs5On(Tv%Igl6n&rJ&*#R zuz|O5y>)4{Mc7&*+FX_98Bn&vqqPKyf4&3e)Iq*VNtX>nhm}9&|(1i+8yEil(m4GunZ4aHuA_-$@ zFYPfIqjGDEPLbt;=P&;AEkf}&G{G2tn^6sh9;32WtWd;UFXYBVqg@aa1q@g{q6s_4 zp9!{q&;wh*r~dC&g5K$j78YA?qmCb?>@-2!))HaGxfwSjWtgFL*J=!#4rJUYXX${? zkRgLiqe*MXwm|HL)9);jre>y=+C$=gX2L_OCIm(O#=qLim<_9RYV;T5#tN-^JzDWx z;Jeg_A)-$VMS@utN^L4F=p}Jux<&LS0x1!m+sO5Hs!&}G4UjQL&G=O^*Rpm@;yJC1 z_=bqycCAHqon`@}SF2lY`L$&FGZq#vA=o|6z=qi)a)RETgI)kSl_EhdpJ*K&f!*Vo{kyKTv2oGiwYq;;{}#Q4any9s?$prBw~ z=X)cFPbd}GbVHd~sCF?Olk9pp7%rU)zrK~>g-EARi;k%dp{x-s2Fu(=@d30_z;eA@ zqehi)3&0K4=dV4#MT}Tr*?P5k=dmv0gF z6u20yOk@@3iEPtUW337#h)`Zkv>I@2Nl1;~L`!NR76(69BAU*WwNwnHKs*efuQyG9 z(`&8RD6M2`I7HV`o6&Vpzva?*ZV|&|zE-9RvqU9P@G`NrXaj#>G|=ZX+`~2^;*yXK z4cDl5`MDC&lYOx7uXpmPz%5%s0Rg8fQmc6@kQyW36 zDbCGiXc9ORV4z;g8D=RNTdaZQJP@lIrf0p9|(2}?G|atHGh({8)3I$4qG?2Tf|Y34mSc~+opjx z<@!Akm zk)#^t;*d&!m?-)5wOdvr*Y9syJ)_#M@jh7E;2{UDpRxvzG1|>dldkeD&wg^8XVD|Lg)dm4DgI;AZRQr5itY<125>x2N)l&*XPc^8dR06PN$hW&d*H z^3kQAx%3qv8sN2;PLJPx{I$pVF?ReRN56LTjUbC4ee@BBzkT?vhyVCc0GR~-@ZdWE zl^6g0jcb9s{nifFIkIqzmSaRX2t!uddc_3SBQgjr3s-a!EZoL`1n0(n{l<2iWc$%* zEVXBoX5{27q;L(a!J2^!`9cs=*;!a!vjQsrrL=nV#;puLn6}_{5k{lLV5h0s$F00o zbYisUq{GTWsgr87;yk*K-uMS4f?kAFu;DEvudq1S@nwFXbzOJUY-NK$E2=h)ubv9&eAc_K*uRNy5O9c{FJ_v_$GHtKx~>bzN`! zGjInm4ntI#*0K7P?ObIKVZ1Rif&g3b8(43ze32XIyf>M7bU}GJ2p312P+)_%J#e?I z%mPF*xyxmbFyhQ=nEl?gK?+?~^t*Kz%X>^VcKf=_rP;+dl?Xu;okew3$?SfguP$Rk zlPa@CHPx*Os3DUN>g3uY8@d2p{fRANuxLW!IKxsi4_mdG!9+%f)8*-Ovw*=iC8oSi zkHO7!uAaVQi%?g{(hjVCE8;a@t8}5XOUy|S%Z8g4I8N$yyOIp@3QQ4~|F}fBL^H!v zq^P@LLm=1ec-8~KH)1?l$%BQkkl0PL1-nVxJKSD+*s5NI+f_uaQS-uO72Tjw3Dgd? zlQ}I2N(GMFXf|l_^I>-QM_U>F?y_3%EGA4-?@hD*T8So;g4OvdY7!2(o38YV^_B6epO2?e&{28U0a>a-a<} zb~+83frG?E87|5!F)@Rn*%T}Y0;jfSSniZ;N!+Zd2ort;>!*~VSRI= zV2FCEZQrA*FVu!6K&^@9P+f%eSCT&~WCQA=!xWj?Yt=(H$_O^2=F2c(AZdHq=K^I$R6V3@VAzaO?8OSS=Z)la+plvaqIk?IeH?})h zYhKl0b0)ftNl-^ADv;Kz{(uue$Q2B#A9ah$1h}VHdUTJ!@y%OSh_whkH64gq26C9J zH%*ge*d*@QOEX}nBSjo7I;urAV&dBNg26^0u;pCWa%V1hw&BY?3NvQdeBecmb`ELm zvPw>uiiP>v$#!pLBNd&S&l>zV9*q06C1?|Ah}o$q*~4H86={1DR6VP14w=i_bAb&9 zD`SA=)&Q9;2k5|J2K7QHSkJHf4N0~4LsCsfq<9+lqS|(pKBG~pM#rf1cb@-jcEY# z6GR|Of?Gd;N`$a%c3W&j%KaQfqgC}D=hpRs=u=IMosFVl##bhzL24V9{&0(^ScNvL ztR{lp>@H??D0B4jpt=zIvoXKw3mCNKm%vFmUlCWfW)ExK$vXb3&sN+;1z}7v?hHaE zUgnCROt8U-;k;T^Ff*~vUwZRh8CD_LOsmcesC2Yhxz<{w;8mooKo!`vE+`0wrguH4una>5@G61XdL z{HZ^J-2tJqSzj%xZL`@aTyp9$t4dc5QT4`4wuo$)TH_U%jz(G+gh$np9s{q3nZAY{ zh_mW;F;AQVa*I`Vd3%*_Wrk`>OL)T2t~z^R7(Qy!k=$hffA9vYFkxcY1+tM zM})g70XnDaxV;r!j2BvjS5=K`70}WD+#=>6cwOL~F$GTU*1Q#~MTV_68xV-o?k>j( z)9Gy(T+Dk~aq=%pM0F5_{)ovz@#<+UUt2{w9uF65C~`HbK4FLeoX@AZ!KsHozeU8r zPu?I}0s`{WOsf^l8=EXtWcm=?iqHBk8l}D?u$8G}9RIy7VoFxbz{4Y~wQdY_N(3ZW zbCZf4bqjhZ+p8`sQ1B)dF4t7xxwh6rB3$#3p<6_OC6d)}Oo0pzJyeN$+0iF$Wdwo;%Zp6r z*Q)Nsp`~aZFnk0N0pb?ChO!bMU1X5h#F&Hc-6FUe?;jgf}WNEuz&cmU0y3e9zlrP}ak14NiacwYA|zi*4MR{f8}LxKt9u^g+?*HIAC7ZW2Ig=&Fm$)eHicFd7;_0|@lQtGJYO_E__*#uckgb7#kz;>Y#Ex<%v znL;bx^zFe)>3Wy9J7i0?Vsg=5fcs20L&D}_PT9kTiVx8yKAD?P1~q3M-mIZgc>4WY z8B)h@C5a)fHy|*AO__$Ck795`7t{oy=jymtSt;#i0CK%u+TKK2g3EU-w@bPpqQ$V% z=Cpvl^~9&*ZfjKWT6JJ}nfNWsV2s26`8U`8|1a-0ulu6?|3&-%i}wE)?f);@|6jEK zzi9t|(f|Nq-tRxjHBU$p|_7mp3U9=Y1Ma@|f9l}lce4K0}t^NOntp6wW3G^5bfKR@3?f93E z-*{Xc6UTr1=r@nP>EO=MKRn`&UV8X@hu?Ad*&r+6#~ytA!FvzB=Wm<={Db?2%QbCh z7o4jhT4O_rbRfy!?M)d88zu5#Vz9EYy8eaYGt&H7$^R8|U zGJANg9)3po9y|NyU1hh3Jv>)Ww}{>SP%mvwAA5wh9hK1-yZfOYZk>-tySpo0`LOMK z?Cytp@a`>QcR$qQ$riD@AL^yjpLX|s=#jCNvAb94(e^SJ_~`EHk9qpvw=#f#WV@BG zJAI`S@k9mqrm-vFiYQKiS*mV z$@cyqcpf*$_6h_8A(B&3U1=mWmWBiOYILxSgitYNX($U>xF(M4TT&b_i!7HkiR!be zV`Xi1rD@Z3oyewrwcj-oI1HPbUaK(BKiUGAHVkX%gzWWTY)%-8f%D@6n}}1Y2_mZc zmN0J&LB224H9lqg7BsAq*feX8B)lf&sKCuvov=Rk=Iq)_g_(vm3K0L$Y7FF)|9gv| z#0`i9YAOF8dtU-4NmbuJJ@@R+>ub992zpfq_sGaWV!tpQ3e@8!T z&vbo%zu)`4?|ZM_@%uejt(<3VWE=)3`3e!vRj>B2h*QGd$r{*LMw^w)bZ;Gj$+1=^ z?F~Y$PBhnR)XN|M16)wyaI4-*)gI&Wsgk$rtVOB$PYe;LQ!Kk9bha2RhVnr>?Dsai zm7-aKb54$h$($`&Xt-dchkE7@4-rTyR_o>+VZj4ZWU1+f5cf3g_PRoN?D=3d549q- zZjS-mlI5L8h6pGRZcB*ei&7%jbGvgP2b)nXo+`$aODftSVgX8|d8`bV>yI5B_hv#U z&e`kHTnQ@)o~$$2inh90MeGn!U!hbeR%j~+$Kaf@INYaAy2Jq3)@67E<=bJb;25j$=q%QZv;6$CLRkH~+OE zqVBPj2)C_TPcY?B5}d!rV=XmNtYKg$8Jtsc_{erl6`Ti}<%M?*5g=A59WUWHlp^3H z+YJ<&avXL<{A#lrVLKfL$qM!io)b&T{M|!D8P7y}&Va@3H4`!g!n6ZjKL}9jP-r*} zvIfhZycf1--DVFpI~>dtC4UhO*lCN8jVJO%EnWo|tI(XBO{OAjHcV#H_NFa{=A*P@ zdS+=oI|aW#(&I5@*mv4mT=;3*}R_6!kuqLmcmAOugX z_u^jEBiDMaTvm()bJ>L0PIcH?2c$BUWAXOfaM(D9QI&4O?9z`3V}9huZW1? za&_$BBuYKt_n?yGJ8$qa=YkDITMByNsvezUzP{>)!JKj9BDp(OCxFv3dK(GlD!%-fbF$e`oj}^^Z91L9#bQN%a zfRf7>W{<8T*s7GGNvaZTJIr-|uv%1dJ)&u$Qzb5+t(rYOC%6#8W^QJthUZEW!C)IU zs}^^);Y?;MYOt(?AUN+&wLw~6saOpy4*SFeF8g`E01g>}o+QM29N=oF znk|lI@M_Igm4hKPZHZX@-3UX?eSK&ZR%>-!5JTfB$N?^^v*pZK5-(CX9TNQQL^kWQ zx$|8YMRq)MuNWebW~&sZdT0gqfTmVX*IBcdLttQJ5*6I4@+Psy`@1(z#5=Rt1;- z^wf5gcMG1yokN75xA3Z?2J**LopzXDEft)r`jeSbBdn$(u6W)bir_G+#+AkI4-r0a z11T1SHqCS}ZJ{gIn!l!)s1VKWmWx&$NS>0{=hxbJ7DRJ#f+kCp~b|11CLj(gP%VR5KkK5${j;t5{#OW_+OiZ| zKe2f7k;SR0g=DT?QDfctyG$k%_9aEmEUFz}!JK?4GEuJ%f!|bCo zSDWqye;o63{28m_;^CJj_MVT-y>QUwn{D7eQLwsTi&{Nax5Z+%JM6j}4=Hd>sAB81 z6dP)Z$=)V`X`kyc%-c)CWd{R8ZGRqWgH4c11e2OcOfAM_32ZbpRB$CVxReZOD>iTm z4wh8oImk=E2nl5Nlp=WE@3SUsQa3#K9=2pS3~k13js}&7I*f<_Y#>dEAcnUGwSyvM zp?Z~i+(u{PShkh0b1nwXv?)l_VbgTryfcA?GT=6(tYV|pgq<4b>Qb_%3&?inBLc7i zb=D~mm_gYCu4KsLR>Ea_Ot5IrMoQpWn=_llrEWy`=AB<&&sB1>Yf98=v3p9vWTTbl}Es3s-XU! zTqm#h6r~UZl3az^iRm29@vYJ;RK z0g*4ZD^1MPX*azU3kUaNl?>IcaAhnKWI`-MQ1Zlea+?I|gxjJ{?B1tuoq%g-wy92F zD;q0h8*z{>iRpGo8IACgt-MK{z{z$b=CBKvxC3QF)_}d`!D5svPeu|s))i;-jUd+~ zJO~eNG%v%5#QISbX7GvYYYrP0lBEiAW8V(iR+~N zUQj2@R(0Zj8rKQEZDTyRecR%cBfUzV0(bMqFr*!`YwsTV@uU)Jbt0~&C2Xyxn(m+) zP*u4m;Yy_zan?IlBG1&rE2B&?jq6E>2>qIbtI-$3!6W7zZPPS`|N1b*d zT(HPUlA?N0J_1WsUfpbr_j$cGQgNzgaB8XqqdC+{r5boHOS4KnFII}lKpkgGnI>p2 z?J@&SF%cxo5yHfE!v7xB3AI(7IG@gSvR&f|jugoPVpT$(x(M#4O(Dr{IJfC|BK7cm zJLyx*NlVgWjryo?EFDd_i@jt*kvfXi?JOYXw!ozmP+3MWgbw0NkeIkmsDA}@65Ogz z98c#u*{<;fu?l3jkq&jOR&ae^Ua(RfsaV)#Jn_dwE5!(53+eOwi-CI9=T2J8=}HBh zNDDi|cvffz!(ldC4x}?>kXaUHNJUbViR*-XHmDPFt2%ia&l$m<{Wgv9s5ixf6u%Lx zv!sM2kbOO23w7(8)CrW2mN`!{XsKk7giw&(inCy@2}E3hEH--@&*xoLq8)d*%pR^B zL4`0lYbuGMiRg9|Ax_Odfg+k3y> zk$sJFAytazlKU>*WAoZC*|V?SsHI}XXt|b5)j*y>@OP`-=0194U#!{4MpLa+sZj@i zymFtBd+zof29hhaRO=5C(CAiDK=r=j!CJXoJXlN>R_T=JT!dZUE}ssutC%+Uf*L!w##< z;j}ph49Ujo+0=joNA?Y`mL5ndbzN%KTP}EErdmgHLB5>?xfq*yD({x_hYDHxP_~f@ zdh4(k(u!8kWlFI|v$ifsXUITyDrs{%t=>yxQ6>qM2Ra02wd(31q+AaMGg1Z)v}jlk zmBWYXxwh;&WK}4ElIz)I(c=?&+7;-@eC1G}oh$R6P=~aHl~#(3BWPJD3)!SVVy$q# z>*Mwv9k{w#8d#JXv;#o(Olp&&A3ZwrXSp@=pdt37CL{FUnv|0^MAwN8ag(Ms_>-=& zBtWYM)B#`o2^x#067Z>>D{HiK4D8S>DjRErgPMA*QCPv}EgHoJp$GKQaSXu6k=lGNGqZ&UmZt&G=##6vYF*Lv@mul8L8G_CUm@3Vf$V zBOX5Ql@GN`-9j%Uq^uR?P@xIcn#~%x&QQ+z!W9=I=OkJUl=!fVtiy-W;EXArt5}s5 zPISoBAq75wr;7Pb9Rl6T1Y{o+9gzLT)7LH;ko{IS7Nrm% z`@a1KuJRgG|9%5^k7>^vxL8el-rp>VVVfmhErI|5$%}Z(b%M3aU6?3%s^J#X!IR0N zFM<*cW%E(cz?G7k0A0i0d=%u_bUIxq3P@beWRQHROz7s#1KYPA1@*fe1JvkIFjs2j zhJB1~9NcuT(dO0$oF|QmrKYMrv5mWczAf7LsDaP2Nr9j0z-O^~JiwAo`+|1331`Y( zPo%`T)7CUz#~r0~nn$I)t=-}H5F|v!LV|hN}ZEpn2t%IU(%JQes{*E379rIxRfBw{m zrgm3$FYW4roA}-sDH z<1DG~=VB7kfq6dbu=^0cA{Q;qa2+AyP$nTjKEWDis$5<6HL6)lzJCy?vJX@-6RQFd z8{mak1rr>#zQ2<%n)6wqU!Gw*hOD(6b?2Gq8K3T-G48|lq!H&6&S?bp=_su}t z^9}=T^u*eR{2W0?kG}8P@9{QlNI`JNQq?Zql)StMwVJpmAF)f`VpIXmJJ_KdOfDq1 zll@Bu<_ro?O>EBa?Qp+=EcliF0ifoJff{0OzLk>Lh!74ifi4ZFKnnBCt23LcH_Ekc zw3Vyp;yE?f=?~G|nG=?DuIP+pJITB?*)_vqa9fJm>*bp@ADcqWT(*N``xgV1 z8iVkORX%}1MX;<|ZIG61KgV3gv$<-m=PWxnQ+gi`KAUIT~9SUs=_I%N1ArR%JHNx`|A(|Ga@YgQ`U) zHfNnd$;rd~EB)sJH5!BbduxHL!1s!>kHb|W+l0g7#0>HVY-P+Ja<)mI-QTvcWxLl| zK@p3^*Y$eq5^R=i^*q;dOHm@*zW}Jz804E+xTf(sFVLC+af*?>VWoLJj<2I)76_a`O~YD@O_4a^xZ=!%KWS!Ynatk@X8 zvyf?-d-hfULy820Ch}IWcj#grf<#-2@wP{#yJknFzZa;{7HRO z4p9oyEY`xAeE)o)N@LJv6RUa>gIXCl3-J((!H#ADx0$^}vJ!Fix{^C6MWVf~*!DuK z;?G!eA;6&X230m-(4`Y=8_yv9miB(1r=+x%bfdtk_9zc7j_6>d9Kw1wC!6uR&fHKPImEDI-X2h=&Egdi;H??+*d78QNr)Fie^8YPA18G2H5`W z$82~1v1~VB&;EKNT=m5}Q3v8iApy?!N{rc;v^A(gsU|aScd?U6vtb3K%Ju-;&wk8y z*EZR%--XiM6#kP5xd-Tl_ z&V&lCJHXeJijnwUO|IEkiv2T+- zC)BNh`l|HL0On|;gl%GTHhEKZcLI*0GvBGw3S`b=p#+7{N(&A$*<{0FrBg0{wNfC2 zgqbO1`=&w@#m4QApk8`_r4xk@k(u7ge@oh%)!~a19JwW#KDQpSsx{WPztLi6^RN-j;i7!z{Lq6v)Pph zRoEwfk7P6`<)35+Yt2`8f0%kpRA%5nloE6YpA>d~L_ zUDtDb9LS=dVXTAbamJkh(u@m^)uVq8UDrd7)uRV#dd522q94T9^$=tA=s}tud~A92 zgY&u`Y^)wVNYjIkgV^2WfgbWA*4e!}Z!}kJY0GX?j{? z_2|dYbv@0odh{SoPh(t9t@prC4Kc2#)d}KVJ*lyJ^etgsPkO8#JxJ4&9IHp)eXQ$A zjMbwDX?kK~_2|2gbv^O1dh{So&(-7l9KB^jHQ2a5M-Le&LC2K^oUMBDk(l+eM8}oo z1dvvi7md}U?>^S`93HDj57P8pHCB(l`&id=kftXx7P4vgp^SxW+7pDwLN;yG z@>mDx+s(QIq_KMRAk6`xv3m4WTh}9w)uRV#dW5lh^zCL{4?k9q9;E5v#pVgWrhP z@f@GipsySS)ep=YVI`j9vl{djqoDfcFZy8E2XP^c^&Fqkptp>I>RYs*VQ4xOMwy`J z__PLn`6#Hq$%!Zypm0JA0jNoX-aG<&!b>knQmhp5!9l@ud`g4fGzzL`6C5EiLFBLy z<5@kXL0>iss%MkT`GXuzMsUAp^%@O&<0z<}O$x~IsemN1is)HAszKLALG^6%!!nJL zcmNYTt1s4|{ZY^p*hE4i5oUZe>RCOaL2noZ)w9WmGZI2eas-sMr$MhD1=X|3PepLa zFF|2Y)~*Je(ct9EyQRUCifMZEMh%j)0!?BM?-j!!TEq^wW&c@M?v*$B84!Df_*4IFu0*XuN?)|vnhZR zav(@DDCSwMYtWaBg6i2shQkoY&>|o7tkyK>u~ASxn|z`mM`%tGSIEnJCRy63*QBXZ4e6)meG$n)qw5&m2JPN9(M2J+vFdN|*(9}yB^vDS434=R> zu$&xGFo^f87By&Z6jVHE44bRNuj% zAx5Dw45m2GYDR-LMnUy#Vj&sismB#?cxyGSLF=QSdNv6G79&K_CxWu3G-z!URL`bJ zBtUR%5E4OIlNz+TIn?IWzx?=EnBgIc77)*BLW5RDLG^6HM4rK6!H-Fv)wl*NkAmvi z1WN)*`!O^O${N$4rBP5ln*?7F;e6o;2YQLCHE3}ZRL>?qpu2vm-!L1Uw!dNyG=$PG^u%HV}S(4be3g6i2s zfgU_alSl|OI$ncDM?v*$!X*sB2$qR}@s-n{FB%2avxyDj0Z0%-C>X9-4SIM4^n_EQ zutfSP1m!63%Ef5Vt42ZfY+~?$j|lQKxJzJ_)}U98g6i3%2oe(#gBS{+lm?BAg6i4C z`UV$l_#-5!*PsSfMnUy#^6`vJN(>wT<148_!=s>jHt}IH;`0ZX!AKX-pz@^_ctwOm{FE>D5I|66v-;2yy5?RfW&`i@I? zn3nEYdij#HbpGP^7vHnkSiEd;df_t*w=BpD=gog_{%_`+^Ut53nfvVAE9Sy;=g)qB z_C2$W>Gt#srsquenqFm!nD$QnaO%CDS?a&i*jQ0A9UcqR+qtoxWV&iBP~T~7 ztR|VR91GNUSsQCfrpQ>JzQfvBNir#8f%@)hV;#v99t+gh#>Og=NgfA0;jM(md7qC2 z-7;jlX)I7*9WqKYVT>hM#PqVUz!Rz?0Pc69X#&PgH;x7BtAi8$WF)})WWlsH7O1a| z2u`CA%nKr7>W>BLt0P2)!4Y2p=ULMYV}bhW@Us|%0eb@h)AeJ4`ay>jd=ML<32;x_ zbz_10w-7&1%fK~(5M%nYu|WMh2nm)!2r0z-QPWGuwSRpzN*v)QA~GGup>g_7cyS41 z6b;q|5Jof|9}CnI&IkEfhzcWg*t9wpsJE1W_)x^h!E(fO?O33ma0E=^K~6$>+H`Cz zP)|6>M?)cu!h9jqOU45Ago6->fslxTE2eA40`-Io(iDpYXh@2fUOX14CmaN067R#n zQjO{8SfHM8lmwbo5C-verXyp4dcp}lE(9y0;18I3;|Qm(4U(55h#aIy=7r<*x%Bfc zmI?|XAFS|@sXG>^Cma(-d~^hyj}MqSV}W|YDHIb4&~4Irs7zjo^T=x7Jc9_!i%QTSfHM8h(8>bLtF@@P3l;n z-Z!A(`vok-2MAMsEKpB41Yu|vr&tm<6~+SfgaZvK0780YUN+^%0`-K$ITC~_aVW)_ zvSWdI!eKZKQxFRJe^YuaP)|6KlYO8+CLqF;8Amw1Zz5PkP>3LkK`$7m&!HzA!HR(Z zLPLsRN{t2T2}dddSS_IuK4MCa1?masM|lCFxrh=n#m55mg!2<21qPS!GOQ^v7N{p2 zCs0A2Nf7 zQS=ouRJcyKFg%P`EzwAiZTWM~WV%2)Xpqo4CRjC#cF76S1LTwTrdw_+buz(u8j{OlYxmqr zCf|&M@vLR*RFI+I*M9fc@~yFNyW1c6(EH+_e7*m3p@(jI-~E5L=jQLUcfeSr>doZuRsFuDs!`AN|a~pZlZhzVz@LUbcGA)t6uWS6BSq z7xsPP;pjQiny3j*g+qCQbNV}VJDOujTPPX>De!x2MnrN&3GJlB-_<>A9tJ2 z*}Kbk_kDNW@W|f@zk2QAOYXncy6>_B|L642y-vO6+)utsS`#$Ee!69m8n&*D;Y-y{ zBpENt`EsmP3(_)z6A5%U*riOLDGnEPUR-^iL4~%Wphzp0vhjf(_)k7z&2o zir?wruD|IMr<^Ulj(qL=e9xX8M==}FRo0QflO|XIF6hp!P@wL{e@JDh#e!uH` zM-j`Re>nD>O!MgLes#&~U-=9B@!L36S_^7|4Wy$O3NC*2Ey{(b{KID$wtDZU{_^F& zJ^$QSJo;Hn<=QJQc*b9QUU}cQq%~3#4DMUbRM{BpvxY48PP!4IQz5!Q)GY0&Y_~f@ zoXgwIR>}fiTW=tTb8ou+SH~AZj&Gj%6ZYS3b@>i_areJ|^BU3r-r9A4dFpu|k=6p5 z;1b4L9UdPcdL0fs7AyKWxIz_Fd#gn8P9~PGw}Ozp(y*B6b-}MsHox<6ocV_5!Svgf zrha52+IQ4X`R@1s`u&f5{2Q-p{N`)Y8lef6Z3uy85k@IWRL;jZ&1r|H+Q{KbrVQH> zE>?)p*|xWmYZcZ7Um06G_W9Z{wyy5<{-*!hG`}XTz6T9nyU$ze&ziQ_5zxyWlkMCnT(Oa%RMOwo&!3MHz z3pjGxkL zA3x>6FG&Bkq8@+K4F9Rl)nC8y-t&L@if{iS^lj;Vph+W|U;`;KhJu%%{}X=qLk}Il z;cCl2IR4@#Q{KqmUh?7nU;XYoBNv2!bN|CPNNcbr*g#T@q2OkivtIl9<&}5uGFPmx zx$~RvKIMMG_suU=K1c%N6^eBqpb`qf?j-yVMVO*?-lKO(I`nqUJN zG=_q|dGVRhuaBRW_{yQnfA@o1fAVVTTW_?kG#qEoKltPGF8XKbOlj?fnqULjGKPX* z_~!D=-BYdGrVhOK+p6$W|KES|GI;K?SKl~u=->ZIeGu|VYgcH34Wzvo3a*@;{dn`1 zcYP>w=gdQ`2L1lCzH*=Qfj#hd?)}+^UwPYGfAunH?FE`(1Bot%g5Q6g%X2Wf`rjY; z#v|yd@m;6i^UE{e@Sj(_^wnqvwd4;HC=qIfHm zkh5tYPV{gQ+;X1I269NxG8F7vz2iajJMTR2oU48|pT7T!^L}|w4?!P3e)$)=0phSYA7p+NaE={n36cIzgA2EZer8{%}o=;rz z)=wSz!he7H`uTU<`(H2lr!QZzy7KG87ki{NrzY4y{)VC8OW7pwepFT_|HFg>6yhZ3NQNM#qYS~ zrLX_ZkEAt=CfGmZ_2%r!3d84$HPloMW-qN)fVoGOya%19td$;|LPsKaG`RJWr zRqqVk-dXtOeUe-L`r@OX>zLE7+QSE!gcTX*VC;?=N&iS5cYoQcRzjo z!Q1oagnxha)!%(>>HOPH8O;CpPW{Ey?$__$z3Z*JE?>EC<%P>1Sbpx#zu39FTBT=?O_mGi%t@6SJL?n84I&fYOA&3u1mb^4dnH%>pt^a0azfr#z>xpt}V zm`{N$LR0$<%;+Z0Qa3h{gh#*B0*tY*8)IM=xJ}0Bmv4%F8?fbym8ty(CXN%^a$>fU zRNo5p9Rm6c%qJ(-w~?wO)3*RUcA&?=^m1Z7Ps~#Ss`Jo#bsCs(PONG?xAcp#vHrn< zEuh#2=BN|fGR#m??3;m(0c8x#(I(EJ^le6&=K*~K z${3i}O|0*UlzA@DGoXxtY2L(obd5N* zMw#aT9Rtc3nB7gBk#94~>;w7+lrb>rn^@lyDYF;o8BoT+9B^ViI?C)BR1=_#fmz|i zs>V}BzZDkipFgk#6x+Z=a$;MClmR30dFur>FsYn4KiBvI>$f+7G3TxuV_+is$1~=f zbz=-nUbo2@{T?Oo%(K^xF)-oXCS&wFt-zSG*NrhSN!})7HsQ>(){QYRvEC+Q^wm`C zp9O3gbifAY;uG7lSqFS3&^PFS4NTD|*7w8?_za+D&;i4fCm5VqkFEngeZ4vj%;6_i zHNHR6&pKoM(+0MHVjGzDPi)Ju10Kx(&4c{^@7T@n`rWR#?4nkFwenXh`0_87|8m*C z^WmMZ-uZ$ZKiF~04)4;pmu_5gEPj3Qy2V2a4=h}}aPj;X=U+U3!QAKP+H-qmKQmjK zefG?qGsT%Rrav~Foj%3%kEVoa`HylpfY=AYo?L47=X=xXTwW}(R9vjVVE3O1r*(Nq z_NS_De^T_@{81-`^EexF1RL(I#T953gb;4A6`Q!r0f%ToDT{p&2nxjhYyz=NPRg3$ ze6>k&zN|YDaavf;9}jiGWhqud_L{5ixLTlUq!R~XL69gA`_l=;M)GEl-7Nd_tzL@u z1>N2j5-Zpn0mK@F^1cFA4~28ljJ58MP+}hhkOHwk+1n208Udx1Y0-gRtRxUEeaig( zQIOQ%-jnK~b~01;rlPS9W6#>;L>^cDcryr<5V$1CbQ!KX#6Acv1!5nbKy0ail8%PZ z6B_PJ*j)FykyM-`-K`*IRv1i)RRb*&X(60gBg8%kJOyGOnm}w)%=B=CP_kZd7>B^q z5GS`gkTcS4a;}_=r|4p_-Q;-0ZBK}O5R?kU{&)hh9dD1cI1z9mQOZ?!(_9v>^C1V< zwRbzJT7{T`C0=kh!3td(_!b1L0czszV z9=9pr-W59lqioYo$(Qqy)<8m*?*Xl|l1dir4 zqk$S5X1msUjcHI)N6mIpv>-8BU zi+5rl1P%kSUzBlbbSI1u}p3B(#$V-fqfCib2Q#2Q#F z5&M`X_R|xHHL&m@_EAmj-4lp4u)ZPo5l!q}6NojioFVpMP3)Z$h&8ZuF_{1FoXSk? zdiBc1J3qEVT>R0(HS^D$y?y3#)4zZ}Z`aRiX7!>)kUr(52@t_=8DzfuXiP549HRKT*KC~@LwR~Oj*inz$M_XEnTCIZ=ytWobs%{5w5mYq5Yp#CY zZ$d3K2V9NiA`AT>r`SWa5SET8jKxk zw?kPv$+e=cY^CC_)e%(jTAM_QM=Xlw>gPW0>O-SktzQt|NVxNKay8Z4CRbxwD`zXT zWuyUCbFzFm??aMec@tMdgg3_qy4etuC0V?evzXy*xY068W}LB5B|aW$m1}6E(m=D^ zpaZ$!aaSK4tFJ~SHDRfMlg^DcS;};k5MO2BOpYj)tA}cr;e;f85pPQLfe>0NY6D^t5vIcJx-Gu*%xZhiwEj zVD)nzcl9NsT&+KxwUGepY31te>Z|>9E`s=)@m__Gm6V>~r;CgPZQ^QQz#KMv1Vxn@ zK38tDwP2Gk1VUVcwsz{~8f?vyg))yNbMbCw;Oc#kyZXQgSD$dGX(M6S)5z6!X1iF8 zW+fWDL5LPw3>Cd7lUHk8*RqMLp?C}n_(Ekn)}@4mn)Wh&9^4XL%h@Wpn`Q$^4&+|M z9gTt{X|CS;xT`N7i`8gA<&jDuW)B3FVx-CRq@W}!o46Wo(yp{Q z=q>YXTMh6m6{XNxl}R*&urI~oP}^naXqe>8UT~)!VD+BIU479gSL@GRZ6u_7I=PzO zt~U@QX{U>!qKLKI=|bC`P<<+?7B+D;6s7#pMxcpHB%hPqY{5(0nGeidyK?(V zYXx38b@^M%e>1}@{C?ryQZA$w{e zPlajMvK_7Om>JcP=g;ejQEI#V8MGo7S6^(y|UUSI^ilW@M1ih@ zf+Onk7oarWtdeD)&+Ao*dW>|qE2VZ8auY!>?x}fNslwvt4I}O|jJVe_lQQjR(`jGB z3|ZT5DHw`*kZQZdc5PYO4mVOgYl}}=6;}awSc9puG%R>3L!(V=sf$L+ZILY$2#(1* z#Co@)xJW_p#l>Dk?sk%Cn`L&@Fyh)FB0+ajNsqj*>1jDwFOd5d$ec`SVTh1} zp$whG8eHC1!tj^n6aYmxiKvk%^di7^4gXQ8&42=c38h`Gu-Gd{kTd_oFyfIR!a&!a zF^otL5zb7L$TzrPC{%YQXjiTkMi{%-oX&+HTPYKiMITLg*Z^Oz&s{S_*l@o+jzS?$ zbxN@Wtuicb@sPE)i?XRgKAEwybW95H#du-yV#A1w3?nWaBFrVU><3vZE9F|=j^%u9 z!RxZ7T#C6`=misM15*l+)nATyxS4wlBR;Jm*j9s((?Qx@$K7bNC?{!$+go=!aV1%Z zL`AzVl=C_`bB+jGmWHnkMi5@ZIy^%}Q-TDqBN4-WSfN-<=HPZ+%_Z&erjH7tK8LMo zZTN^}%t>;yFEfm|(J*4oFrq(1On%yL$Eyq@UTGNdiXoy}5;`5a6!D8-VJHEqHpXwx zv9Lo5_3)fM!`N^+f@TP$5L$e#VZ>_;BVIj3l+;4S>gu7^LN8K6NSXB)?SiGsOPJlo z#Hgsl-Q@F~lDAWs4H-s=>xiTyNvM3OkmHMpm}p9bKSVaeV#n((St7+k&EGRq4C!V< z%jjUqf(-fnLXby;=CaOOeV!zttiCH;kqii$*|JMLQI`7GbP-O zbA@)zf;Hk+yFi`#6T^sy4I>^hjQH_j|9@uomZ_buSbP@v=j6{x51jPCNe}$VJaGNo znsp(?#a?!%X?ug%j_hlc3#n2xm)v*h9-G&G$)0`nMlBU9M$5His+KEdz~8NQyXEMS zeX(XE8%?!RrA8h6@ydNh!p`;q!A9b>wC4>R*xGj(D5%s@ttT01;Cz}UpwX?QK+*OU z57x@%;=vO``vztWz8d;#-`0oD27r311Y9|gJ8I2de`;WWN|Z|t;A#LnvTqYZfHaWG zrj|V@d8VTN|(kQLWh7flk2%mDb{G#)&=Pd8OTm0ZB8e+J~9?%l2Cb|;}GAe zr4JD`nd>&lwCg~r8u7L8iXVk3iWicdR=t#RWNgV0Ux`G(b|6!LLRD)mUX!|AwyH9| zEKc(*(Lk!Tyv#}%n@M=#_P(P77dA@+i-11|s$-edCPhDbbmqFonq|-sZ)}>jH@b7e zhG_Htk8FqrPIPY85I1682MqlaG{DCV7_hL?jH|iCliHmFnDs|&cP^KAWV^G)@@-m; zRTZaQl@*XRz0z|cjiv};k!sqC)Pw_kr7bk`zIOd7w>u;jsYEIsPUr>{d#v3SWUIZ+ zCDjUE&RD|5LlEnA+EbxUiH^4su0xPLKjaEgNjB6fsaBg zd%|`%C^~3&Hx$+m4cgty6DA|W&ek3Dr0veLS-Ue3fIJ}X{y%s?Gb%tgpgl3B`%l!8 znqXiV4jmH@hTsLnV$r(j0XCXUrDIKXFfdwxPal6DIr7-3U5XV4BcF5d?fO9eq;hNG zHon_*Yy}E8o=C(>687aJJ=MA@-ctPvZG?oQ3dp*=byhXe42;Tk?38;ro( z$3iYqc9lWhrB+TG$Ti`Q4fBK7v_EEdW6^YPI?M~nUa)-toZk~u&6Kwy zc&h1)v#1KVXw51C(!z8)-HH`TP-QT#0yCe=Fi)UmaI~9nxf@K$k5p|f@XrA!+l~a> zo{UtnAti?$t_70eFvU2%P$<|!3YB8KEh?ZRY-G}u8?Sg0u1>L?W5X`49`qeZ`@vhs z1jD=w6diOogZck?Q+H17zGl~d?dq-kW~H_K=yGl6&v%x<-hF=QhfC?j?=Hp{zP0e8 z`3L7C;Cz5I`=wca<_k0Q^u5yo(>*2>fd9ll$NJ8>REk^NZy-{3dvoU<>pO-P7znD} z-fViu`u3p(2I6nGH_zU&zHMlMfl%H32I6nG4!b?pw+<~ZAa#3_?j7q}h87se3cue# zqW7)iSdaA&4J|Mrb-#g(@RL|@aA<)6sYW83C$Yf1&Vz`Z#i}uL!jjGvosn!OnYSjp zW;hJ7j2SEfH)}pNg_^l+2ie|4g~$4r3@tDaTD`p~43G5>3@tDag}uF*4UhFN9$H`^ zV0(M>86N9jG_=4#ZhIpE+gpbyAM0N@w7@_P#qG_1c&z`tp#=sa#!;<5g7hZYzJ zF5ljSiO2dE3@u3b!22;Sb5*w#>p*NRS>l;`HXrGUVTC}%e2+0Z3sq9}mfPE#Ch=H* z|GEz*&!=xB4t?v$>tp@r3@tE_nsIy6Djw_a8(Lr>5Po|TE*|Ue9a>-@W`29~Fdpmg z8CqZ)&j1I|fz{w>qNDnhHdenyNZjP=9Ps!VNxeu_rNV$48$E zWT5M*9#QaGY#spLbnk*9-E!Glt`;3RPbJfhs5EiNVvn+cN?PMBPs3NmTFDOE;Oiwe=C+YCVGDFBm|U|~4!JF2yHl0ioXnOBC6y>Td=)#> zZQ7s&Y|Fx#Mm`kex&EeVTIf(y%^(;YuMl;$ z?rOL4GH=VZyQR9)taocfJr@jdkrLjDQ%|{{#%j%agw6#raUAY?Lm)QZ7f%+*W;ICJ z0~Nc~OZ9SD8)DDFdAl2G7Gn4zN}`EOuF*uYe#VxUb3v(XgM)n0&JZn@C7X($o2Z{& zFZ#)TIu4F=tiO*u&HCxV{Qvx^*wpT0yMDjx=*oYs99jO)}>D&)sE)Uqf7P0 zM;5CKKVK-%|8%}M_wZa{_Q$iinIFz%roTU(GJV&SnEEz=kN&ylR&^F^&KucN*xuFu zYi=zVHo!n;*{wH$uDLa@8Sp3DeYobC#;^ef zHVGy%Agvi-Wbb2}s~~0AfNidVq+tWLxe5}R0S1I_@8-`nx5f<{u+3Ev(+n^`tnJ-= zy5`oaH3R-+dr#Ng8Z~TyfupZm&t9&%^+kpaFtE5Zi2;W-1B`6G8CZRq#DJ>|8?en) zaHU}bwz&!-ngK?3AGf&*6vGB=a}|UQ8?en)AZrE~*?u)}B7KrpAQ?7bn>i3NY``{i zKs0Q?HgiDG3^1|*zReuq4I8k{9N-Kau+1D`4I8k{9AGp946INZ32m9AAD|5zu&o>z z%>PfD`m?Fs?p>vow}VW6mYumBw=I2b>9j>^;kx;c&HZN1I-8w&^YmAzpJ@t$2mcs9 z*ZNno=yj{va5m78qH=r0 z(z#ckF?rJG_7yG9|dH>h5J5EZ=OtbL6#xVg#O0jHK)r-FUi+a=k#w zOt4fZ5DG|Mu4T(vx~ydGcybM^y_|6?qs-H1BcAlRKd2ZMPz++D81}9j$5XDhlBTJ= zP$m^~(A=Puv?b(5qa9Dc3{u&;6MjfyM%9MLtMlQAVw~{YUWv(4P%fAzyCMj*Es|JU zCuwwTc)Xw(_(m~!BJOL`_Fg5JsxhfZhsOe3r)ttv4huM|pFo(pslkoGPVz{;b{ z(`Tri^tr8XuN+ew?rV`%R4c}_22H%{s1_lRF*<>lTUZzeDSN7^Qr7LvGh&Sr>QI(# znbA5KEfa-F%^UTcK1=M@#TYJEZ{El~Zcrb{MtvB#26R#bCw*?~f{dvT7bph2Q49lD zV~#RUzqLK-b6elAIi?s6Pz?V@F$`Q6Im$f!&d;RJZGG$Hm}1yLF?<`v$kk(&1}}=G zh#8OP3EF8#oMzeTE0qfsh_~1?rJxvN0z#Y{**f%l$D@j&e{Sm_t1-o}fnq=##V`Ft{+b`R2yMa~D|M&Kears;yu=nrci;3`w^i< z$2uLPhw&@_)rNT<*e;qFoJRS7Q%{*dM-aO`y7?pKdZBp2Ktm37i(aNR$VP+0QN&)=45h=q2$%%0aY3E(t5qBfSU_c!Sk)0=R~9#I*`yM3AF+ z3PenIQ`xqn%9Rq)$CfJa_OR<61w_M3id%gcI{mU$5-LFa-{!ktc#2G%!|T^q8S#sk z5Hrpp1YXo!=Mero=^R#A_B`j#VRpH9lg?pr#BTi2eZxHBkLnwsDD1}$&-D#n`p9GI z2{KNUV_rBvQiajLF})a>&)D9PWH4q*^aj~xkg7bgZz!eBcB)CpClJrO`GQJ=$Xhhl zP|D3wzGsmq744*j;|kpCj6zB+;>PKuj>IAF;|f3Sz5$G|Rn8J8u3$28PA@c8`0{wp zeaY;n%IEeaXWv`?@62SdT(i_b{9nD4%`zMR?`KCn;!0!ov(j(t5xq$`9)bOgvANzv z!@_cHgfJ(R8%CnE+sv`~W;BPi=@^&Q;@Odrq{EVjN3cgYZ z8z*eATN;pfVMLbQSZIXeC!mZSVKK2NT0-lQlx^_IRJ;U-%w}7Q^AT&%sxxXSVnJ$Y zM7k2bT#dQ&u0J3D|9Hp$Z>>{nx9{(Vw|^A;;rX9O)c~m6`dWX=^DDQm5M;b~KXzt0 z?5|DcE{Eq^*!Oa)ggnoml2+K-o;M|3s$dETP1itffb!Y~mk>=sN)(F?nkHuA z0sI~;PS%{=IrmZ1U%-niw_cI(eC1YZbc$c-2W_`JF7Neu}PhfIZ_QD#q+5~ zPt{2?3K@;g5W^C|nCH|bu|?!7@kh^ownQ!r*9K&b63`dliT=_#p#9yy5(Mfr$r!f<4{S#{ENu)=<4 z|1|Eqn{OVjuVS<}=WgZ}ieAOt%-uR$RlrQP_eGBN7r}9SK3iuI-wS=t^{hYMA8|c< z#a{tYkGP&i5HM1&v%I7@LHOOZgM2aBT@FRa6 zJ0@9^RFxiZlqQT$G_|QwazcxO>ozmFOuJx(&2}-a*czf#YKd&Cm>!-OR?frtVc6(V zy;wPULPLTsDve6ARlv1eB9vIF?!ct$!1BIHKaKg8_n^9dv-vU0PV~{M-?npX$^WYv z>yW^OC!okS_=hiobxfKBS8ceuJ-Lx%QfXtt$o`ynhFlrEZwLGz7US?!s}~rB-`z(Y z>|qGHw#bGPNd?(BOJaqnkwd^3L(3LhXon*$x2;yv#8I|YFW9u6Mu|rr>@C?g(FA=I zO{fMI_skJefTC)GGx8bYh!3J75=v8|>IlIEo1`Q$m6P!!1~!IL#K{DmWS;CWBw2F0 z>10R9WxN4oRM6!HyWe#%*x&W0)2Kh#zZC;l?-mAL(!^3FFH&w{aygi?)yM!rc*zMw zm6pmv*-*J4hpCFa@vdA_G2kgX<1z64$PE@m}Dt+O^k@3V*l@*ZluFu)0U1i__^Y+jG zb8G)@?cOim``&x4d#}B>fA`n!{=nVN-6!wfz4PmLK5*x4cQSWgbo&o(|J&Q|ye-^5 zy!Gi@KYi?wdte;A_F#Yi*Y@AP-`^+pzijWf_r8Abf7@gBUcCG9 z-5=XM-7W1N?tF6Rr*^)5=Pf&d?Z4Un`R(u7*0&?u+grc1_5EAU){|RzH~-(wf4S*x z;+tQ-@mm`|xG~xwH(t2@vGpHb|E6_*{pD+)0`#u`IUZa4?E3!R-roMkeaS5mgOA6p31Hs`)Y3yVL*xoF|kl_3gF9+$IS~s!|2NuLvh($ z5<^*IWDKtWZXb{J3qY=vq6TP-@1(PmPRgB(eg&{|Jk|i$}-tO1JlfGKUvX? z%q(E{v&UoA2LJ@nQo6*OHD(}C7VdVsW^N$N0Csl&;&|M+0JMcd6194s(4)<|PFgs3 z1@I|otN;Me>)!gt{X&Ok+c=%%2#TYLtXI#@0CqPwcRvn|$$>U)a~M3$+S8pD{J>> zA-gIxu3fZ&TSD3%bd;1bU|P*WvF*$PHa2$Oax#AFIRv!P0eUJXI*o2BjVE#@A+}~9 z+Z(&g$+&ufzzn9(T6MIYC#4=WRB>Ylv9qz8gvM{VpvBbfWU`oTXVTz06M@2-^bBGb z(0ar1_$dH+I7~#3rYd+j! z-B%os-*nMLi#M^X$YP)ZSdVoxgF$1jf{jJw&&B?JWR?0f})T#cYft~T(|&O zVEARttX5Pd6_QAVd< zXyIhPd!^JT;HR-Lf)vPXNhg^ z{Db3h<^rIJJx{UBBwlNi`9yBmxH1XsjE=|T1t8l;xgy!lhziFJ>Xg#Bs;lntIDG*q zG%DSipkYbdWg6**P@MHryF1qLICTMF@^Z69Hrzy!B#nM4F~VoMy|-f?kBJLFT}hN# zyJ6=V118ZOxxMZTV0Y)K<8kr=&>v#ShQMd2N~VC<;^LJy`eQfXKK-@e4oDYz+R^f<-plA4Yv((D<^0NT{ZD)?hiHkO@zGUKdTTf(( z9F-t7Wp-ZL-ocN@=mnsn)>3(*Q&X~-lV){r73oYbc6OdT9>4JdP%92{_E67e!Li&( z&1Pz|dfM3mp8UxRK%w z^ghYrgf{GHxooLD+v&ZX{o`@$0?;cY99|jKk`is1w&xO!nHKhT4vxps3jos@u)UEE zZWSE4>0X&MQ!{{_?N1+%BNqUSN!o3JVEJTy)NEz@`m9y&Z2#Hu7&!y%gQY}CbBZ;N zQxbzr8O70IW!C0)w?BG34qpIj*+Iu?J6WSX8j^#Q+MZoW>~4dp3%;O~Xb?#fPjofS z@D!(^>6a%|2)e!fo5$nO1)x^rJXVw$)YON4F5ParGo8M({Ts*Q-~~V|XbE!Q)j^nD zvnjT0j-3JQZ2!dZ7`gy3j6NLkTAJ4Toq=XIuS{J#+dqChKDhv}Nj=jkC6fw|qNsp1 zvDtH^o$U`CkB=_^RIA7FjZ{+2$SR4WDtX0#w*S}Tao_^b%5|tjUQ@bygG%Eq;;KDt zf8X)=H5UMOm=#oY(BUm1D}XEHmDy$E&i4C`$43`{+K9_AV1I`LD{a@vl3D7CmcH|N z{52NVzLpz*6NAPX~meMlD?zw!doXy{Ge%F}hegO}4zt~_gXz$I^k ziRBfOo=m=9ysX~MRQhV4$&6}Nf$ZmIbhW)*KOP@m0JIYLF62F`(HmJE@MSpcT>-oq z8o&I4l$0`x$+j*J6N!N^aNW_YWrOdR?c(wHWfyHU`${n{7Rq4LddT2PJ%5!{_IP~% z0?^551#Z+TCD^n{YCXI>JKJq$)NJx-B!*+eK(9+FG9T(`QKMl-lj-oFolMbnDj^TDJQ_}g>y21+2p80bP4dY? zxM%AgVkcX<6QeCfiJ(rnFuGJY+@$ONiOSyT>-Q9Pyy>6Zv&&xy_DCo9-GNor{j1(z z77R}Uu;&i{xtAKO5X0mT%vFv;C%?aHpwN}z*A?oS&Wki^DXv>nzgVZCEmyrb^XV#s zuCJ52Ef4*7r75OqzvcUZPWD@2@9DFGPd(9Z_Z8RVDNttLK5Z>0oLbgHE=iiCA&ia zzb{~zK{R@OHd;wBqJ`EVIOr;|Bwrq8@jTmZF<2=dA_skaWHI?ly``j!M~_}3J~l$7 zv}c0EGbw^o(#a7~AH`$Y0Yuo8NeF}$#j#W;(Ls9| zDHEcsY;hQoL@CC4?c{QkBI0))7~(s-r?2x3@tb?=H+${mvW5uF3p04^4RIBMbK-&g zT>WaNXDXAsQN9QU_$+~$8&@8N8u(EmUr4)yplc^U5G7FOnCTN$tRH{p=PEM?F3>o1*~QEaB*0vfkVeKO(9uqsl_M)ZaeZPK>z#yg=^f} zz5Lz($F8FR8vb7>Pz&TC}!wG_Gms(J->=I-hRiy%& zCKEogXN&op1|JTwyUwNAIdBPVwHxCERR-maBtcW+r zWTg=07W+uTPSi(2qQs}Q90>jcb92B;LGC%v>12wU5eV24tmmQ;2P&p8xk{8lx+C!XtfQ!jS zgQw^;mdFnU)*k7Fk%FF6JPV#(tVW;Pzn*CEUw>q2hWAgiz7{Vz&2p)wI{6x&2nrB9 zXmkZe5g3E)Kwh;3!-&8};zO-%7t)=F@B5+CT)4M9ahe}^i%<0f$~7df)cXRSoth!J zduXM&U^tc_`FJ=Cr)n*X#oM%D!hMV8kX)_l_OmG5?m_`(Ws(!G`LFK!B>&ZQUUMGF z%nZq$!%~PKlNyn$w2HKz$RUA7IOcTqbVarCkQB)mL`L)CZM|=-OmgBj-~ZD-$@l-l z(wcnDDe05EoKHEmfCM%479wf`xfzs)0lbXnbAfoA>va-&B^h9~0dYPe=eW&< zZ+_x7|JiGOzCZAdOY=QEP5XQ=xy=?4EI3^X(lY!&@Zy#l(WGKNo0Q-Z#r8^eIvq0- z(M-Hpl+O81xy^+M$`n+w(GbNfgCWb*u@r5WBo#eFSKxy_wJZGf6ljG=Y&- z9+iMGFO;|8IMPlMAX0f4kGn-YPzB!Ff0@tsJAUgrrC#BC zc$)C}UNUxgXqVZBf+tgXF*|~zAn3nq3ZN8elVD_=t$}zQC#_ptQJZ7z3wchA{afDW z^ZZtBX`Zh_C8tl$8v6=IahHvKAw{3tH%`{X-}LvEW_bVf z4ZapH8T$(7Co{&r@N|4)?C*G~PxT##*O8n}P6EdMdY|MaV_)H*;+*6hV_(R3V(i0j z_xZj}U6!x^{U2X@|Hi$az4z{W*?V7d_m}Vfqr1qR|9r>31K<9{?GN6zZ^O4faqEM( z?1R5O_~rxV;9&nl`=|TN-Y51xxM%OdyPw$o;I6$3?|fqCgFD{N>$bmS`)9WP*>-nJ z-8$O*#O4PtAfKD+TV8}HhnHtwze()xF=ziIuYYrnhp{%bc7=KVu_T)G85go?YI1wvihj7pJic<9jb-g=y@KnmV;2SxU&dyqu?u5~FJq}` z?82bp%h=2`c42h!Wh^<3T^MG38JnKQE{r(7j7?2r7X}|sW1C|FU~6Z#|5f)Fh84g3 z4D9bB)=a(TRz1C|Rl6W0SDM)knr>zC@73U2#~5hrO%EN-!jR-EjahcpzhVA!v~-Xi zCvUMYJ$GjJ?=K9j<`uU)xxjmx@8mCezf-+2ev5sP@lZbtgQ9(U)zB5<6D>{tZ8=qi z;^V~hJ6#xCJli3-#Cq37;im|MaNG*#J(8JBMU>Xr?FDas`_8B@$nlT z%JAo+h0)m4GSKnsA7cIX7Y1feV=vnO+J`dy_7}!#Ps>2ZuX~7n@!s0~g<;#%*o*dG z`%uOjkg+h5ds+rMjz7fu{a+Z=J&nC+KlV_D-~WYi-qSMPJdOe^=G%(j{=$&&mshJO zlVhsYKybFg;I!BaYLRJb3j@TbWuRl^A=an1FjjmTd(nRQp$uO&3&Y2!WuRmDA=Yny zVI=u9_M-jJLm3m(SQK17Eo03d2XC=|54c4n@Y4Mk++P@I?)$1U0lrl#vVUfz+Ggvx zA3UxpZUC(QLRz=me(lMLYqylHrduG0t5eryS+OQQ&K^V4EiQ~y|NL7-$0rXJGMh0*WRGSG40A=YnyVIcf8_M-jQJe1+Lzc4m_S_V2kdWiMgUl=Ao zjlF39YaYt*+g}(tKP>|tzxpB8Z+~GB{WSKX{jYu~!*73KT>Z3+w~k*0u+J_p7KYfr zybI?3KnM!PgAf#|24g{RZW{>C9u;10;+4})EDXxOris~;zROL#V!DZiLHyS=F?%>S z-9&tRIIZM`LH}piGe-~~zkC|IF#7*;yDyu@E=&ZtjJ-dNU6>Pa8T-UEcHwU6W$a6* zu?sg}rm>Uw|Mjn2yZz3+7i@hW_`~x*Km#BwF!3-faD|=P#T&>o(MZ?EqtB`E^RmMg z_BJO0wkvGkO@4oc9m)AcPgd9@ezdT_g+tr#$KnDgvpe2CYC5j|>h7B#p7flD1xDZy z;+M;xTjWkna$3Pw;}y6}j_`ETGvi&Zr5Q((awLUMgjQ1WvN6%J+mBx4P760$cw$r% zNybwxDVXY)w6tpk-SkMX`#~4))=oN6whcD1i|rn4;KfQdzcp z6(Up!$>dSCTf&wqEHvpluscx8_Km*X`L%3U7)&>3cXRK3dCn)aRcFMDjE+eRo=Ay2yQPxJlLCnwW9 z$cS6P=5Eg2%)Low6?fy$qE`~sRuwSgKIRtze2yH)xs%3k{ zUxBtBF##?Tgk#q(ut~ekDp{KHt z>5(TV3J4M9L$RK6;$V?wEe~`ZSl$8n+D&h4u5G<(>(!f|+5GpL|I=n|^JL?1H-7%! zhwi=iUh`h;-VTTf`2M@?yKmU|$9G?{q2BrUogcmP?mNYe%{zy;KXv=3ZhzbDH{br6 zTYquunOjfas^5Y@UBLf%@SX$xAbPO9|4aMt+qd^$zkh4*!+Y=F>+NBCU%LC7yFa-5 zwq0`f1v|eFVg|-Lxt$kp|KSF*{Uh7&+7`B7zV*qipWOOZ5GnA!b$k8w>$lcEJc$+9 z{OI#L6JHn&1UHq9!xqHUD{!J@rpg@CqqGJ(3^`~#ZAWs=e2cFX5m}EBaiphvcYpj0 zAyIO~Vmb(J1lU+j#7Qe%uCpDNbm|UMj-(11AzBTH#h7~cy%U7#NV$C3h)T^uHxP{s zqAs1az!Ei;5*a67bz&6Z#p)#2v)H?%GepxR2hlW3pd%(29~6VNM8GL>m9VP~!;OB~ z$i>8hDf4hKb~k#4sKtCDZF_QHmZ$TMNARMEhZoKmhQB8V{VWAC*wS;?)-mKE` zVm{zS(k2$jrc@Sg3_YZZx(z+H@zFDc*yT!ykn%>!Xxzh!MqH}LGO(14#rq|h8^kL} z9&P4$sV&|4x-&$j+vn&}uZOW-pe+nIC7SmJJmJ`y8_CpyA%!lQj*5l?;T`B4(O@yU z7)OM5noV(ePr{vW*46SsTa2)Dkd8+@g3Gsqgt{?4LzH4JnumoFHmvvHkQJ@=0?ARb zi}X_IVuezXPFKpLLoUm*cYf>)QE>ID9M6)uY)5ladCyH%gSgy4cqo+!7j1@DicXid zlELuKUz{WGCfos;{5qrs%T24E7@=?xH_G)U9(B?B$OoV%ntUrQ=KBpPjyEE-9hY&gFszZp zu$bxvsguHqOe=8dsDls(x6Tl{-Us6|5={*?Tx_)UXu7A?1goyPdQQmK()D~Lid8`& zb7}WS&Jjp60q0U9mk;$qPROkd^pJoAtz=P!dTKXO5UT`TaYG$r_pIlBIpnaO0NW)- zhf-*)hmsoQh#;XdE~Jvdj;yO58cfGBqssp4&f1ViqbSzTkeyM5R9hrTrfLOV3`J3?X%3E+1^CLZT^^@@Ksq)LQG z?wyU@R=w8hRywhfS#P>|1>!*kNsQNqB6$5M-8K6;lu{zHR@CgxFF9+Y)?)Jm(T;*y z2v6dwz{+Sjm&@U0Vd%+$Jkkg#dLbxC60!DyGelKGWUEt%B-))I2)Kt@6eyZhz_7vq z5+kNWg3Ps=V46ipz5UP65TYClDy0mUrSgs}nLOjfj+*gWo`nm+_L0zKL4>K4>y=G< z{}0X)Fdrxp1+|!n#sW=;6*OFP`f6enQmuwaitY4Zl&wQfi`_ZbZaLr7N0E`*FLHU+ zLOHrP^0;oA$fRRvKU*d(JJNJ&^-3zX@%pniN_N!N%W1YjrZBD1tD1Qh%cKM{5PH#Vv?^*VFST-|6T93O zd(Cp=WM~=te|Co8+kqGytyIctlPG!VdWelZikdXMQ zf>w-BhmVAsa&Q!j6cMJNys+@?6uwAr`c(vOC-j5(5P~3p%N*hm|A;4Bi zBQq57kvwd=C7KNkt63Gr=t0G$iFU#;g3TO9lX)?`(>g;?2#26`zM2n)G0UiWf~iGY zZiEb*P17i+EGQPvpp0O`!tML#h%j6*X)hU&>;T+KI@VA^0}ZhyD;gFT^O-6E#d48+ z61KOrGel-YAy_3_W%GrM7RV>v9EGIWV4I2JY?j02WLIo+L88(ow|@T&K{8>t877iY zg%$Odr;>T06t1J`mS8lTcD%u|!C0$7bSu)Wv)Q?o9=6#`sMxL7`Mm47m7>W<;_^Vx zm+5kl!WAtg1r5H*@X-3VoVAgLv-L=nhwX5aZep;cv(mt&vbNDiIwDcaFsYi;;nVeQ zZ1<?<#mewpKU#025w{-WNvbFt$e_-4X^juY!cZ`LD-3Na>xZo*6_!P{ zuGZTl7hIRt4hOj+s0AUiV%X^vOI1PwRrmW!OckiJCI|*pH}>kTY-(CJp(KbFOzD{# z&k83oP`V|O%RS&s37PKgR%F~glK6qPCPhO&X6)2AT=8{t7p+CF!Q2wm;Eu{e|OQcR~z)Ek4S zsKc4E-IN*~wuV<#4emm4sV1za&e}lGek|v*m0qz<(nyeC2GJzU>Wb3?A3g)u8Wxn& zsEwM^LWKacL}K@YO6AXcw?}G@>VjmW4xPRuxUQTn^MKM9JR$ z({lt#I)flb%SZQy#sVHZ_K zSx4TKDPH)XNZ0VHMv@@gqKH!q)irOCs%K~L)=s%5;vj*7?q>>3Z}B?`cIxA z+L*(cbyDFWp2p4o%wxA zPQ@1cCADdyNi*S<3ap`^yFYk_&=PSoUg^>?i=tCb1+w{gwBtCOQ%y=iO4aO;z?+>$ z#}(FJbcSe{L_?q8qbS$t2D$tQB8gbN zo^N?>)3u_7LRSe3Or${Gdio3@M^Hy;QjK^ykioQcD46MRMP0`wxR6$3&7vL^3`6eX zy1n-uX9%fNoZUa!wRiq_r?vg@ZDZ@dZRwkTu&Hi*bVFJHy>)5rcL4nQ zpLcxMtt~}i56uAhN<%=7byJ3W(UMnjDT$4Bi;-~18$~N>vsWj`Vm~x!6Ng2D&_)%V z1vzA)u25@xpypOA#_FgCDzm!r*uVwFerqu{74_~fjL>+;cO6U!tdQNmN=EqNeBgI{ z*Z#@`R>)3XhQQv+1Xjp3UxvW$gun{L3vT)q?5s@ShF`(<$^>rs6>LoitWcX_l`Q^c z#=z#v1a9~hY^+S+hF`(@gun{rCT{u_tgTGoW?#Y6U%xVen|%dOzhgpR1)(?m3Z8!Z z$^>rs6@1-T+4%4uY8;qu|h6H zlL_HS#PlHoZhA?Ya@XTT*R`Qws?h;6q zQA=Qo{bD~A?VmV#Ba&3W-K5Y_17GTv^@!hfV0qvI9Nf3O2UTzn!SW^cZp`w+@vG&} z=h%|}S2NaeR^HDaY17#a&yjUZn(QkbSGO%cRVX}}cRNb0Yj^zre_a)|ul*1Y4XPl*D9?pc$l$tL{h3vjS!FTh!$TI1p`+L>jopI$qC0nTK2 z%vq@b~ksMD(2V^T@heoZW(y4n|FnuJNu{ht2 zlLe?`CwnJ-lZbHnNZzQsl_Bh=I@w;ZCKd<$h;G)|KF&C9O-YqR4qNI1oJrTuw%~ABUmX%x4#kqk~Fl z(65A&P^}d2k*Ud_-C8O>l5snn!%+`wq8`mNZ825s$tkl{D;0-is%t8&?a?^lRYImg zrett$JsKx*f&wx}l@|DYED@AK#bMe1cN|jLRI*nOg}ZbU9prcdU^5MxR|m`p^|Cze z4LjrS&D4jpT6EqtQtlp-CHQ4v|uVnlB`@N{BHfnD*Nm(xpjTKsM483xET` z6&fuGA&1N`ONbpA=880Q_Pe45em8_@a=JJa3m~*{WL5)%PCjhf(8vim$rR#xUM<4* zDk87DIjvR}!Xq!us&+6GJ%+%4g6r@iEzf4$JWg6>MmMEpHOQM{Ml6XI4YxHBu5?Q+ zVe&?1wA>jIx|(+gCfIR`Vu#8_i@1{P2}R1W+9ABgmzy}-u%&2sTFaqDTAr;W`dDeX zc|%%Gw=2bmw2VB)9)sa;NXuA|pbR801hrDVndwR8Or&0d=4d(LNL4b1)nE#W z&{hGd+9O!dG?Rf+O>$UU^z@gpf=LjdT+@KnsChGg7VxbJJt_WRaFxo zK|FZN;mB$eof^x>i?lpjYwNMn@=f{WlvYy&vs($8AlyHR*3xp15zskf8Ks&`w-!}< zVK1f>DlS_Ndz3Dt-LkEABm&RzX$b_vW2|jXdyK#$Ezg!4d#tp4Q+}C=bmqEjI`E0Z5+E~3!wBKRcfnMpiX?@ep@YZhtw@%{D#TE3~VoC@{@ zB5CAGv2Mc{6y#RCk}Vo@w2b2*zt;{rIZDgbS}Meq>#A2aTj1}xQa|1uQYIMSMxaWE zr?vd*gKcE(_L{zS_ZRMb&#mym`re1OHg?~$qiy`+ZFNmwfA5WL0Q|$?);CHUkC7>W zO@JF)ZWomT$PvMbUa1v}3Qd`+K$UqfiErD4%EiHZDYh~aQZRL|C|F6NgXa2?VKtR$ z+jhv%i3IA|Yrpass_drL8)>9jFKH4LTMac49*=p%?OqGAfp#D2BNTYOlj< zYyN+q|5-r;kKZVJ%;)HJaCBBJLh0->RtL)OD0(r}R;Y5>CwKZYv1sYOOo9A=A}LzlIlY?!80G}HO= zyxk@|3K!GWa9tg0O~h4W@(|{S;i{*bJmqP^c!@QirwILyw`#ZffzIWI=9i zrc^KnZMvAu)Cm-(;dzS;lx-D8T#<=!bS5OENj;uZ`ca#OLp3Iu$Pb21A#27QtX5`$ z%7Txd%5HAAO-u+D!iueoE2J|HZE<*UG*@LEimywZV5b-?aGih^!9n#uuhDf<%%C0; zyE#tJ<{NSv#j>8g4n1}%1IKkYwED-BVR_U=n4%`IBQ8`cx1o&g&RJyOaken@EG-s< z!k!76OuQTFm6|!B?GW95UM)q$P#;8DM7m|xUi;<8PGtyvQ|q#HhA0?aTq<qQr8tcRYyn- zY1G%0H_E`8_E?|k2#^zBdH{+`>|t&iXOu3K+7_}IaB z9K`p3XaAe`!+XEJ_s+eO-Cx@s?|#kBFYSEY&MUS*wC!y_vGsFXovp9h{MpUc=2vd~ zAJml~V@b^+7WTRdoT%1=c#r9c zDk#XppY|qV?Ipwx^kF*HAIe0!tB10hmcdapstw_wk)rZtHP3_vBoPqW0^#we-HBLh z39%emPk0PGYG;*BP7w!OD#H$JSWQuKA&hr8gvMHOIt1cKD*UNC5!+fqY;T}C%t&@A z(u)+MQfW9)dQJe22cqE0RWVTv*O_KRs6@08P(o)S*1W&)Dv!gOXm%k)bn@xW(6*sQ zf&pc77?_3iSkPv_nhMg8l8Mc(R`>Ar8u7(}O(xM2c64;>&;0Qe54NwZ7d;{WjLJ0vtFeXuT4>mJq8rQB3Bs5LQL#G?&f`;cPcLgd&-~Q4rEl zs0A7d;{sEt0JhDESa}JtIlZU_@TSwF*@PCAL{P+t#p=S4%Qu2#DIRd5be+dQ}a&m`L-d+C*&q{=!Rd z4jrLVjpJn_742m_x>M@OoxosN>}aIS@n~^Gq!7Ug7V=O!%%7?gv9%?{R;^4H9u-}| zlT|yeRBMAAtc6Na6I91c_p+!(;8wB3*q}Hif7+Oced`ipEdvw+>lQ|ZZd5G?+(eX6 z>MRxQXH$VnE>)>HnLb7I36}4Gk)TY(R+kVf^Xe!~kQFUX3KcTlK-rQvKoN1+VU=RH=|XIGJf^Rs(U2sF6f~(a%6sq&}QiP*{#VpBb{=z$naL#4|u8A+wfX$Pu`qkJFJ z5xHSdVMgSNLKH3owr`n;EiWNa({6bHX5KeV#0pD@Rmwt>WD#uO!bz}o6B4Sm za=9?dt309ShW&U_>ecJ%u4Kq3{mY+ zDl6j<#%cwR0_ISeh%GE3c7;Mw{Aqb2mRmyX3dN83)6zsNyM)*k3i|M;#fjMb5@J^< zQp2AL6S27^#15LRKvOGN5pR&mN+HTE_K}30sE>q1iBD^}R8Ed%2LUeyfsw#Z#4<~W zU7?x@e_EJ`rI!%9Lgf|yl$(gnE>6igESFlUldplI?E(Z38eM@=1jZmckXJ3iFe0## z_)u%xg>(lfft`q@mJn;DxL`PzAo+MW45w->jK$luVZwcj=8#;i>GrcI-0ngF28hj1 z#AcQdTM7|mQX_Je7AWPL$RUA7IOcTqbVarCkQB)mL`L)CZM|;*vAKy@atX1q516X5@J_KzUEJ}6S2u9#IBHJ$)8davBVN$SI8hc%m4q_+Uc-I z*Yu!A&h^FOTE>d1gMJ=EhY1QKGd71r%ZvpXw9mo<$|%ExYGw13z+_W~33lk~ql#E7 z#|HUe)PRG{R52YATZM{>qgW4%A)q`x@(6{`E_W40QCim(^4Slw$^e3?ksp z-f%E7>H1mvBl)Z?t}vcHZ(BsJwZ*yDA3gTAxWW?R@w3INHU|dKLjzU%;D+)i>I%57 zT$zmYhq=lAJz+Oyi;*}OnLyy&RAeb9bn#Gz23LX9scaSj_j?`L0<6Vzgq2kBBUQ?B zkrO%DPd)neN3LZCi;gMRypxPggPrKMYr09Y$?=(yHPB*dXAqA?GTBnUp}XmJ-bit{ zCMjkG=6SopgS|y58%uBvu5I)>qZ&+}s155^ zvmiV#@%XW~ycJg2^DJ-9{kmWnei7;i%pn4%$LIc{-&i0$ zqL&&F;5!+~%1*S~&gQ)E$WPGsM?O&FqucLl|a)59WJ)N5LzxMUu(-*sS! z@9dtYCZ%V3;LZnP=?fWd&JaU?Ck=5GPvNiHW{P(_d(W=xFAcs32KX!ie_48VFa4$Y zE(~a0o$n?K(W;?L7PCI{M_i0XqS5fRwK!-dYA`a?Wy4|`wK>=uYz+*5WtGK2u)lAKH zyRVJKAm|X}a!UO{NbKc{BUdan`c_VHK+r_Gl!dGW863ef3AvhFj!Zbc<8IV})=G{XrMB&k&ABnJ5b9w+a6l{maQ&mjkpt(pJZHGB5U!y< zXoD90!SnH@Tbvp^D@1VU4{XqqKY0Fd>6R`Ho)yA6z~Hg`*98X8e_Oh-&pmi%t6vIS zetCtk4)uWyTJ%lpx0Y^k%CuI9^w1wjpe5h5esk%TE}7N};UD0&*3Vz-Z!F!|=Qgd` z>KChA?IS&?`D{Sfe|>2-=cctn6p8)dt)LZu@O)(P&ervQd4-*9|NH-iYuwtsH{Jc= zyI*mqefyc)FS%tM{M^B-_P=58!+XKq@yo8mw9HA{PlxZ$9EAUlk_--Gr@Rj#@@ZI*A-u;yS{L(ip&G+!M;qyJY(C^ma z3g3$7d@tst#d_WH8Q#9_Kkww1=K0D~#pelM@Z3CH;XCu3=k$!WnCYg^asNvv&%a%o z=@X~2&veQ?ZLIPMdd~E0I$X?g!)LeiqyBT3T$Bkg>UT7{PoZI&%gQV>qyRi9FAXe zTJuRxIj8Nz6~4;PNzQRj3;FuS{>*3I=s*ARSC{5{c=}eK?-|dH&?mJYRWQ^?6=0_7(O8&Us!k_JvG+V}IsTU*$hPU0j;!6Q^(SnO-vX6}AkX z9j22hbRkFI*q`|mf3kn(Pu{XL$CsQwHEZlE>^GcKJU8}*6n$=gq)(ndu{6W`r*HPP zc*)pT$ZUC5i)Y5Z@LK7Kv48YwpX5hBdmYKyk9lD1Z}LfAGWHdAQO-%uG4_RgC&vEU zukrc*?kkt(dw44Pd@mXM3VSi?`c!ob$Y7 z>gLoR)p2myCUdB%x=A>BQIYy~v=;B57JV&F8T-L1$xF{_@yytzlu=B!b$OUb41|H}j#4xA zi~VoRXa3Vam^|Ng9m(141@QeQ%#hqaTp<-|Rg(Tk<})Aq@Z|Zs%aS|*-|@u8yVmY~ z=-zwpHTQz|V)u6L{_@@LzuUh1hP$`#{Fgib;!gh#e&@?>|JLqjZ~xHkx8J62zv$M- zZvC5E-*Ah)_0oesI{2}J2M48tSL}a!|KII@`~Fk=M|*#{_aAnD0YnT)yYJc6ccZ)8 zpiaR1cI=(k@7&t{@b>$+d)wIdmu~&$)(>vIZHwG`!RGI8esFWVncIBv#vg9{2%z%( z&;S245ZqKY4%;Z|^jc0B$v1NrqC{);sNHL~WXl=VDjaH8n2Z|2WXhSXNWi%9_0&Wj1?QbGAIsLNFkvh zN+-y6KNXO(m=R~2g>sHa(mS6$L&Vy(bTQf<_KZ+4GwQ2xJzXphQn_%iQRtbGQHka{ z%0NkGV;h@ih)Bw&!vmRUA*pe}Ot2F#)kususGTDM>6lhB z&03Lj5IN@cMFvO8DwpgTt*mavyU_wo>cxRYuRn2yK$sjPrtpZCQWgNME<2 zbvI51$|ENqLOVE=CP`@f6=#TWpG|QiA)Jm66w?kXC8SeF2)S97jkFvL4k}3(0;Tsn zhu!%4GX$)_1IddKfU4D;Z+TU7^`kl31Ou3y~tnWRY%Ov@K}&tmk$w-;ah(hu0f` z{m`uH!$c`V<{}MD73Kery*H0@R_5+~$v5)zz*{X9=qb=UJe#nP3wE#(p`k+d9MJw4_j*@6Mm z=olm#cG=wvHV4sxk2Ur~F?Ust=1jFlz#bf6j4#w$8f&m6q$qDUi6=;l+bef@F<2Sc z-7Slw=wpJ(meHIK)@)Ii&Fga@-x%{~CTU9=6Wt`6EK=UaFr0|TEu0vydI!mRs92yp z&2+S0l4FC#@v~|*lY~_?nQNe7r^BX9qUV-{dy$*SQkV;+r8 z)@Kf6+IDLb8|9>^E$NSoPEtt6EmXtS9ZI1#Tg%h}k*e;-F-5%+H(9WO-D#myPAAtM zRE&v;qn57c*lNUs1zpBKZitJ`%wYcbn4%i7IJrhH*DI6*MFMBYs?-CwD*7Adj1Lc% zI!VTrNEW(M!J~iCn4-**MY8Q|Wa6%BlPjdkc%Q5!u_2x>;FTW57yF}{s~rp(Q_CM6 zQ?PkrM9QgDDILjLutbwByRnL78T7e;mkAWQM6Z!CHwxCYf7!l8VJxvNRRtoIYV7O*XY~lurUQA82v@k(Tk=MrA9swbfjy%SFn*DV-1-HcyQl1aYDWDg^ja-BAn+;e**v zk0}bnCgCJo-f%ML;1YdY4qJ?p#a3}SV`-_Kk=t0dP#8KY5wq^tm?E91w7Q~DY4pu* z0nbS;Ia&$f;wa%8(H*xmrj#HC zEdq-(b-Cj31amy@S$zeh5PG#l&z4}_lx#N(%^?o9O>92uA*n!rWNFEkj-@aAcqq})V4UYBlfsxVUpO21tL=x7r)9iOhMo^|E>uePBf$DHn zcBQkqAttyT7{f7sXEQitbfz%{o%6cD^L11$#ae)H;Z?Ut3GsR|m+84mJR`d-lygX@ z8zO_QjVVHSf~ON0)AKdT&UCsL3){#*B^RKrb`w#^HhZ}c&PoI?HT9nyQv_>ms|UBY z3pvt^<*kx8rPTb7=KV>T@DjzAt#1z#)f5->Aa58`1ZcdJGn?HYCn{{JvU1Lau^lVH zv@Bt>snZvz)W{bY;x@8wfPj>a>`8w={}?GJa{+QIzeNhi`Q2KAEu% zgPx_gjw$^8K-HWL1^o;tuI%un8Gh)GxFU{l)mODyZFMST?|6t5D8ys<&=y4|l#AG$ z9wFK^`8q7ova)$gOd@EjfcO2DRzDn!g6Sbti!If*C`is*BfyrRE*@nWnll@cbgyFA`yw5V=^!bkBzAw*Ug%F!^_&H15}v#>V1F+(Rhy^$rJ zVW>>IIsfi453r2;^Lz#rOY$U%e!XqxhT#Hbk^pIj=drXcQxh9WIb=y0w!GF?%y@)) z-!sbkGIVnY?w#t8Ml2dlHOn4n&lzo$^FwFU&vzKzmL>XVVw564{h2}1Zj}9E507@6 zt+LpzxJMv#U$mCeVY%8*Cc8^-8%yOC&23*XndDLw8*s>+Xtgz}y;`cvBnyPs{0j1emq=CT!@u=9<0V3df4bM-L`G99%Q+PKe>5A`A*SKHF5CY%y$Z{v<^ zV%QX=LCujZWU)G?d-0gU9;VBonw4Txgx9$n#G|R%+qNhQCbC5GG*jdQ zu9j%YWPB~YGaxqR=K0!yZIk|o@x zdXYBR9{7T(D4&nihk1`NlQ5Y@SJ1+C@#4r8o8MZZu&^zVVWd#FZz~l0IaYE6+H|7I znT)ZBk+6H6$z}xHv4%Ti%fT@ZV=q4p=Ord%ETtl&s@Um-2YI`Mveb{r5epmf`mijPTN+Prf@BeM zrD#|x1aVWCAjN3IRCAM7(xQHznLVN*cxHNt%6Vz~CQQjb+cQ6Lk9&a+AJGueqGac2iL==|>s~{B zm%Z{0&;71Dndmh2+n++vEemYQO12IeO{W zkPA6q1(XeqxR_MH>m)?nL9>>jbG1&8EY(5@d`Jc3E)Q;rCT)ov zb8J}g_7b*ZZlXQ#hfPtuAIy6yF}qV$HW(bbl7-pd48ttUAxMV?<%vDbgsoU^59wZ- z1VsV+J<8t5o6;m-$aO)TKRIlU)Ki(k{!KS~-ovbAx>1rGp^5y+L2lJHU+s}4IS!-v z2~8Yh=G|Id1io=tIZ0ClVe4IyD-O4O{;-mCWh5}11TzLv!0kAwI05!in9_9YsD)mK z104uGj#H+TZI2?hAJ=)cJuk2&6W{;(OQptg*_>n1&IZUyZjVdw8cFUWorn@eeI?q#v z4m|JG_05ptc^idUEz9fvd7f=|{_`Hbw1;<$&-s2r-T_mV`(Syjx0F@oFudU%O4QP} zT%YR{iYY#g%M4+&C?$3FGe(ZqI4KK}QJwD$oE(c0K6$f4>@)hDu(0?Th&`JpZ2b-T&okRB)v} zRIEr~hYgbis3;BIqIS0r66tWg&RamMQpf^QV%hQ}f#&0+#e zrGn{Pv(<1Ey@Z>OxG%jJRj@1ut<_$!>hjg-L{IJ&8+p6rWdr`wpcinI2AyIhRk!#% zT)Cf%d84Ipzt!rJOwdPx!U;Xsz>D9pM>dIfPym2M9^1S7<%V(BvTRyb(cT4YCVwSF3{MzCN78{E% zU-;(2M;F8eqv78TA2)QsjR5~K|IoZNkL$mq|5bfoZ$iI^egc(I3-X`HuOlPGru#45 z!@9e4ubBJc+#k-pYR);QpZ(L>joI+*;>=$F_T!(M*V7BPqsW3`VRmlsZHEb;Ock2t z-|+SOX3%SIo0-4jh2(l_;dTTuC_idDD4zW+BtO2MRG7~iW-rWqGBId}xlVV`?^T`gjnUm|$X|&knt)|?Y zH-1NOUHayAdfLFT$<&A)dN}a#RN*HD*cP5#4^5?=WWG&%^LlWq;FIYR6M_e(GN0^V zoB8o|zry@t){0Hub-%-WZf?!?yW~1GmHlKFJM4W^*-u6~pUZyb<(HG|-l^;-yVzmx znaX}L_VRr8H<9b^sq81a*kSLQ%KpOh)Z82#y>lx2$u4%-JEpRqoMz4){j?ga}VQ-qsesan>kG=7S~CJj74~3nbD zkdxEZj^N53<7HC^J~`j)&@VinA=i&i6@F5HZQ;rFmrkXfWWG&%)A~!M4ctDtG20Pb z8Tj?n2EMCs(GES-_;u5S-zC7d@Z|cnQ)w?W&)u|s&D4QUPEtF9>(1P~egx7lT<}$b zuusl1JM;@L*dW(mJXQEf0d@v{cq;8A^KIIj*RP(cvB?QyM{xaRB)NWQD)S3XaOC=n zrqWI_-x2(UQw5)#8zuyQ!Bpmx9c(kdY5l6H<}x|C>@e4*Z(4u;RQgH(K9~OH^(&_i ze$vNx=nV&7My@|^>fk2_uswKk{oqvEN#VC?7nh!z@y~o^2DurzT=#_TcXV&kfxu4P zv*y0GcF$UJ?Io)}SpBQHPtM(kegpk|^et!;J&s;sc<0)O4Q+$h@cj8F*FHD%mD!cq zH-NkT?DKy#|MvOTynFt6`hV5`q5h1X*E{vkU3%ZrU@5e8_2PFI|7`Jr#ol6I@kL92 zK3AT*VQy{qtFynk@a=^^S$OwCcY#`Xf#F+*Pa|JPeh+ywQePe})5|YjdTK1s+UM3j ztVlGLaqer1OzKYVUy8)$=B{SlZ8}JMK@5w_F%X7DS)5oX^7Uwani5hW zZ^$QAi)nU1Ny2EN*O}MNoL1ZP^vtH(rkgVxTTMGcHX#MGO*+w5 zO@k{1wN3pix2tXHTe($jQ_o6ntEsc%NaQ<}y0@GNCL6f`KRK*hS6XVD=2wJqQ>L5o zWGiV;T4wuox?gp$6TKQthOLLCrs>whQq%MfwE_@^t>-$uSHf_c`d+tGFuhmIkXPHQ z(U4Kw^ks(hxGCGu3za}ia5rVH({X20%?TAOeSGQf)HeOt(wEgX{pix)s%`p_r7vwY zooa-|m7e;hlG>()m9E;RcdT^OHobkNt+wfHE8K;(r^c}kQsBQXo z-MiH`eVguGYMY+Xy>qMS)D`9pGp|?M^z}2Z+iE&>HT=B(J8GMr)qh)U)4$RGhuWrp zt^d|m)2VJ)TIp{!?YV-zVn6ImRN|$iH`-+;ENk|<+3VCcy>|9mwN0;?y+&=*BeO@; zHhuBzi`6zgJbPGe)2nB%R@?N@?4hluLOwlA1VTPqN(H-#YBn?RTy-hk_R2lAX;PO} z+iOC%wZgDddyVV1SD2|yW4e@D0iwF(R@137;rk5Ri_z4k?=@^MMpK(UVAx)arZ#<# z;fq_cPTdo_Rx&d)b6-@qfBspkpFM$}swXh^0;Zo8!y`tf=~|bTB)Kn5%vo!O)$glq zI=}j#YMbg;x84n>3xKX}y>U)&imZNj%ehsohJ&r9Qy2L!=)bGv|C2MXo>}-jcn1#| zzG-;KFf>@_|7-r8^M&OrW?sE`)zYso4d<`d|D*oH`XKlU@XPR2|NF>0kQ;SB)IF+u zwJrei_Fp#_nL9Z9x!JeRzGCJFGmnC3f>*6ktN*h4hSm7$^HLmBA9`G^y5T9&BM8?#blp#OU-Jl-}SQ&kl8o+*0&YbygPJx>3$0eiZjk$ zEpklXMbV{(t4W8$vXGp7zu4+x9JIE2BMPcn0>?S?bk`cf(VIr_SPTVE_zumTh^+UbhowYKDi~d+AhXI>waU~M*Ko17M18&#p!LOM=2>f z-9{EpZrg}T;*06QN=$B17Vh4DpenoHd7umTY(G$yq`M#J!oAxMRAm?A2fA?AE0lLC z^>1S58uo$Pzzlk;dfJOUPkeGi`Rx7dd{S3yr1G|=Uh*APJ$hzWl*u*V3;pQHzf+ii^t&cP>23@F4>^ z|G9a${tJ3=w-MS#SCG4q%XIJ8-8A>-bNSgnnJv$J7IbpapX#Z5^krZgGe@#ciA+|1ox1e?w7Az zIdu}Sy6*NXjwIQPD`}%DZEtK80M(kJVL1SDC&*ZzsAI!u+9=!W5^ic$Z8_DfE@pMa zu6>rG>Efxo6?wqWnUI*wQ&e_uY76<_#FfM{MG>LbCo=d zr7C7TnffRzl}41w*rnwrmCfa>J&Q9-Rx)CzshZ!;9ML!>13o0+qv1M5)qEy4U)56s zz)bN|4fiRkW_Ip$4@|ZXSSfz0;jTp0tS)Y{lOJ%#e%e#y0YlettDZvy1qu8;A zD;`wynXqGVOQjmF2~^GM;wqioiHz;IrN{$@uHixk)$)w(m_H=|9*Ws$xU4|+m3DiX z4G;J!W~1S<0oDA@H5(4_QOrieWdW-BOqfkmQ4^T{G*tUn&FbQ2Gxnn$^Y4X7aP_*lfy*JYeV=io~my=SQn1yESMHR|8J3j-hJRJom$#azD5-Xs~x- zwgl2}Ip97CG#SjT2*jT%0eKY1(r|U4YI*i@tRmnCm+@lSFT3OMn%U&^#KjDc^|D=$ zzvQSg35PR*mm=UzFXNQjHB2?Xa~&X%(@N;xx0 zsuuA@SbIn|4t!WwHJ=H`Dr~8AUuz$!va!j}^JB-lLy-rlR6_+M)k7aU*0}y(aAx_G z;fLrov%!nL!vE}#7kdJ)O>f4P5rO`p~^=`)Qay_pV{0+=Mb&!epPS`c2OK3Y&AQ`i&RMODoPrD4kujIG0i- zXm_m>RmHjX68ut&q}T~Usk*Xfk(5d|B4jgSIvaNr(OSA3D1&m+e$Jm7np?g~tzL}h zY$chCJ4w$a*DG>#IX~+<=B`9snZYPtwBwaj=vaf7J-OznZ6m9$darG*xE%x*r1@US zTyFS+v5?%fw8Pe%XvLdpF`jc;BMEm!$U4nkI_6Ya7Ec+vQm-hz8Qb=34d$xZvz5YA zdwMqUQ|Z|nHX{G)o;|sR+E35Mr+T*a(mi`G!7tUbmAZQu@@y&NW_x77FC?1H3MSIS zVxAxNT^VOvFdZF)c-avv_q_dLssZW^DRx`03Pq{cDUXYiaxJFF)mmNY24%+#O0(U0 zxXcn33ph3}79y;prBrK+P}XoXV2ZSvir^2%xPH1*_s~*W=Ie3RCv-ivOuB+qM;IMq zI7gx{*t+fNv7uN#njH=u_89AG4db3TUa@**o~>5eM9kt=SN=#Dy5iaBW>oR)*FAT3 zfA`H3Yxafe>ss-jglB8m-tN6-YhCmupfg3`TU+7XZxf2gz*>HFR%thV=2QrN{HnZr z2m-iwYei6v7FzXoolFa0H)SbSB)U{bw3@IB*1rUIUkrvsObP}vrhem?nU95~f!A(| zrqlLX(wt9MiYYr;tyZ#^UJ{TA(1JH4rv%s0lBIu)Ptnd=JSaxewX(T*EY_^FipM%s zDVVaQb6MBWn=aMlN|}uo1I|%^iq&jvq@Cl3^)i>TnQ|lUXagTO)s+NPh7KI@HPU88 zam3TT*&`aPYr3JRbwuo^(h;@n?r#SMI^l>ruh~B#2mFx$ir0(15?5!=^E>m?>vv|- ztkjg=b8G8#3@9pI=0c8S9WzJiWJF?v9ZS47H1!i@M?_4TQq>I2Gg9@E7rBHJbV7}`H#-apc23V{g?C)>LvYg{W5wM{UuaHZ$u5qXORaG4zVMM?lIkix_fn5 z9j2R~`|{j}=lY;B;OgubWen)x264*33=YfnF47nwVPTR>T3Gj20maa*hF zb5CZWq1Rn|`pSuh)@Yxh$`dgI=(X3Le%^TvEx0Ay?a-ykhSyws`rvsD&A7SA(R^aE z;gM@kKli+bCfpPWR-BXh_~?uA)6Y>ZyDPO46`rbu1X475!liP=CO4?G+lS#)uqf0U zLdFCx&~Sz+J9fS#~Dhz0QF=B31ayZt_<`YGeB_c*c zRrC;k`q>IiIuf&mLfs<8HvAQ$9hB>F$xn^UB{G?}7K2Qlk&8v1EtN`L^hNmT%M}`X z$2_tO9l@#~j(RqGw~&vw!A&;N9Gx=d@gZIs*`W|(HG*UFH>kTd@@&Y(PFT} z@uiBDA9gE+KF?SL7tLiEOV&S1MoQrRtwN@Sz5qY{EQO}x>Sy{?rfTkxoYeP{#-UiS zB$=+IB)IDCiY3lvJ$x}(r`mS(`S|Gr3QbP*jo2DVxO@SkV$Ug zdIf%ZL3v81itKEJnvJ5DZAomY6&Cvay4B3LxL(f3dl-3`<$Cd=%vc!oa{RPGp@}q| zT(27oC9o0}>(VB7u{13BB&(wdiUHfp4%Te8C46$&lxw1w;iu;nno!qLuU4w|geBeZ zQB~v6(sH-cOxWXcGBJS&i2ii06BDW#&VxP+Kdo12;yK!sBr;YDCHT`_nyDr0LVA>T zLehFJYshX)ugSPM421 zd&5?@H6okA1y`pLg{G1XI0~L<)|E0fV=0RzFK3!Tx;o0qa@@j~Y9*&h=(wsSchQcn z;-_^AO+@nL#g4s8utdQW!!xBqlSt(fv`em&A)?+G2reVh8nmgL2VKEW&nYziM%mXF zf>kC|%M`6Xx|$O4F4st!Z4M^lWGQ17&t{!=f4Nabm+{lH3Jueus)dmyfZ0f+KNA+M zsld?XwuI%dH(RMC0~vdRben26qdkl+;iqR5nw;SB`RxH8Cu6i*8d-TVpI|&~GYziu zGuqRhXsBmx@uJDoV9-T;^B#qUinR&6FI%gW0@Vl^r>klb~}g*5nwLah{4} z9cC0p7x2xK3QYhkKzWb~;wq6uQKGsbpIC^68sKun2+MiEZA87HY%6=?Hg5_w;G1_V zG_DLEY?)Fu^RU78C^48EIV3kR;QFl5mo|1qqqmv{QI)}3tcA|wn|CQRL1&_twPbyx zrpraPhjJ-Y=S_rNVg)kqAF!h^BQ@LE0OcP#P(8kRr$S?oSc{b1Hz?MM>53yq6fhGv zXy;@zVeES-UonOUdd<2$C$&>3if@h-nk16~(b|OQV$E^mC{w7_5+QIYnv*7l7`WSt zv~jg;Eg+=}nSnwxRA@|&DsJM;SuR`TVnf;$WD1-un{SldtXLcr0~kI?#La~iT?nHH zz9}m-4G%-L+FhQg7fFAj&4qgXnB8X+ofT4I8QSg`#_1~7@ZcRxp&2MNiENeu+vZZZ zNO6gDr)rBdy1lNz=d&h3ET+A2UT(!$a1j;mL3Q|MU!lq7>zR0gED}S?>v4GLu9Wmp z9GLo({Y1Mqh|_)^eDn&Ae8nz0hi~>2nsz@|?T3U=o-Pr&dOd3tI=yIs$o4IcMw(3o zIML~9rs}>-(2UOFn-b)sK1>gt!8f}KjfQwT4J@y(V(qaltA`7eA^ zP-ry7f+63>H+h9dLtGc~pZF%H&}azHLY~4mn+lDFI4k6P_+~?)(GUxTd>7xWD>TJC zCt;R+J4$titrk{s(sCz8n_{NAy&!~()j*_Op{gdEY#t!r!8dCPjfQY2g{G>|bmCTb zHk`5bYSCb=oaXzPY&{WtO+>@pjJ1$2)kt&9o!F@96k;i9Awedt0 zqs+)R@y$CF8V&J83eD{bjfR*YaGCDrZ3>Ns_#Wh6@XcEl8V!Lu$k*}Byh5WPmO2&hqL zG761`pcv$z@XfSBqanZr`5L~NQfM?psv!S}ZzdHQ4bddXSMkk+LK7u;mp73v)q@%P zuJ7tUN&e0pE-(H05ln;g4Bda@XXoM}tJFP|Ri% zqi8khsYix=(#!NpCCbbW+FgYvrqHnSdFo6 z%oVF82Emv@6IEz51gX%q4le3MpaG#tAif3KYXADVg3%-ZW$|7Vq7)vxqd z7M9<=e01qkOXkH-FW$EB&kL^tRry|RxN`o3^Yi)v`bjj2dS*SR4trNZfR+tgcOwYIdC5z_u)C_o*)tzN%a%{v z2mr3PC=a`%n(OZK11&qo+J+ynJf^U0t7iG5FQ#2B8%=(L0<1`dm8hE4L}GxJO=@Gq z2lzq1ZK>vWp5JQN-2wyl0&dW61=ZXxZorf20a|v>?1o1XM|s%1YH_ywR>NldmG%wy zc-UpMsXBW5;=`Uy5YVzI*Rrt&DsyeCtp0S#u3@A9%BwbRuwEi}B6i5oWpHgJ6_s*c&;!@xDXoz*s8 z0a!u{I-#27ITobhrLM7Y17HO$sGyqFc^0JMZ4g)xsi;iZC*Gl&+r=$tG9N?Bna@f{vH;j;XSpsVFfIq1?5$aq7XYRHUvM@*v-TID z0zhNUzNTOO?CSeg#nqcvmsh^9@*wyg;9j|G`SInCFWIZd-V%;VHu(8{TXv8L;^u z%zp;l4^W%8&LjH2)IXpX^f&4kK~%s8PziOS2aqo#9|boBQpj_3U)B98hzg)}S3Nx+ z75;a6#g%U{qQBcp*b4Pju@n|!k)DyxGe#_)r(>Q{LN>+YCi{r1j!GhpUFp|ML5(RQ z^`Mn&M$6W4rdue6NXcxDCum!Tu(@rlFVEm!i=8!9vcB+@musfDY)p~%#VCp-g0h7I0Z4Df}ktw)BByVY3UD&~oYNq&zW{MAwDN<1o@6gD1nN~SfYWD*6 zn3oKBZEngcw-Uils?trn@L^?-xVwQJzd_>N`-R_mu}(h3gNTZ zi6B$9_&76L@YnN2|J>c0Delrtapx9=yU?l|gCW*M*z?9dZ^6Z6E?uM|?j{@R8GCgm z>5q8LmbiKOHJT}2t(jtdOi{P^T9Obi^-Ja<7r~sOgb#=1pkFGN5-ykBPl4bjDagBp z*wPFsK)({QBn%^-FSg*8>4*97IgM4QPKf!5U+HiSw=1l#f&Kt)4fA8 z#oIMgylqTT>EcOZ6vrASdnBD~XW3*g+;8UM#uyiJI^%`Bm#*jeMzYL2r>~i!r$+pSVOUveHoAv}pg(lXj_LGzpb0nNCjPA*UP|(8oky|xW|wkXVkTZm-2 zK4&)tOc__l+Rn%gELvt7gKE4*a&j#Rio>)Ep35y`in2XN1Z%9zX0(Z(w$p6|?&4v6 zBgWgZF`1;>5okr^G~Y;h79*M|Xw4L1%@meA#EVC~SB) z&}lY;T}w-%J(e(U>gSA+exIbO&2(F+nmx`8o2;|B1+Qibk7f$@m?Dx)#V|(3<5I!y z6V1kY#X&onvQcQ&i~ekMRLIxLwwyT1VbA`SW{Q8;O!4HH!W(Bzq%#s4hAd3aUG$52 zi#yIndU$tWOvHzDAl^^!c;cN%)n;FqTwbP$Q7uFrI2HU zuDM!BM^XeMRP*^!mUjjH`sZk-xI#0!TRVLX6k9ue4HR2DeN1pMLYVGyJ&UPrPBQrloAUcje8J^t`+Ob` z6$wWxmZ4?fS>DG1hgSdTq?lW_N*+*x+fcPi;bI(QsEtg{kEr-lnj)te7;oY+lA4{Mi z#!$;iu^I_tV+k}wENVGGRwF@lEP;mjM=eLnY9xq^CD0H}sU?b1jRf>q0u86vT27YL zNDv-Npdr#zON6Hy2|{BDG{k^vIcA=IMt09ea4dm_h*B+)q-rDxj3v+zpQ`2fT#W?& zu>=|p!L>xWs*!*iOYk%uiK~&oHiU5x}cj3v+z7q2BAUX29gmK~^%nAZ|1KmGXno(=n0 z0uAx^TH@~2NMIXFpdp%HOL&1A39MrYG~@ziq%zRN~oc7G?biTW-|IgdR{sM&VccTozWz@k3oep!%|x>dLCQ6ps><}_I! zrIw+Fj^rR%>5YU=J6Rw0CQ|#=6)2hv+ES>_EiedcBK6CiZoMTaB~r|=L8n}lS{>#8 zj~9iOU?eb{#4(I0;C398$S@N2UHvL0y0(iMDW!&55Caq zu;or&r~)&=LC)ZA^FMq`I8*QZJ!X-Dk zXfhnOv&V)<+wpum5DDjV)g;X&At;NfoQoCQRX8ZoWKv-f$p-99#lu!?5qw$iflSx>E4W4iIj;3shn$Js_kESc4 zqgP7HPH!~jbSCn6Gb*Uts=}df)&E%V(F|3lABt(w{=UtvfOM8Ssx6`T4m6 zuR3hFeVw`bMIQ75FCpY)Dn!`rkaL9G&k%y~~L+M0%K2{_e)A zF`u_rBFs{_;gBOq51?4_LV?2B1@$xI$DvaI}xRP?syO< zY%j;ae|WUq;SL==M3lOo0*YY3-pCH`lP_U6R1|gCm#8G9ayG#)NfDkxn-ovqddY zhr?cv4+kUKF=$c#RL~y}=iENkI&q%>Iyp2&C%cLzJcD#%j_jMCNHHes90ePx9?Nrb z+AE5=MrtpeB))#k6BkcIVx!DNv85_wo}**TV^vwZJ8PnW`h@kGj}ipD%=ZjbYqb zPFM=@zCE223z%R6c}wYh%$rT-y=m1tx$;S%lNU_W$z$L7!Wp!kq#r@s)QmDm(V2!t%Nrf zh-Z^AcTTlVbccaXu9~J3%fzSXXOvF%YdxVn)_il&E@#u37+>pyQ0;O^81K<0_FeXb z_F7Mz;7)HZ&3y=2G!? z#J#6ZL~AlJ@R{ngvm@4diio7Dl}LrEag?nC-a6@|FwB`HQ`BxBw8D8h9ioGtR95xe z^(dm8{~w-t*9^%1Ut4|u>P;(uzEWTQ(sE~cap~<#*2NDk-m>sF3suAS42}7}ogeDI zp?{VB3iM;>O~{`jH|ajF3(b9Ht~a+b`>xrSf=>4P=amZ^X?P>!jFuwKs!Qcvw4TPU zuNUG*3do||XRo2qvuauPtY61(Bmqn1PEQSWqE)lpT{xn=kpTRZJ3Tejm{!eir!Grl zBM$f|_t|TxORbvEMA^FPMhq}hE)dPCFKDfr*|`;9fVoBitLttD=E`PVNgGvZdt;-3 zVDv}Bav&8*ld(Qg$A;0gQMT75+|;Vta;jNf+*~I=>=ZX5iY#E*8tQASmSr3i0R~KO z4Ol}#?&&E6stUi56T4h-BMf9w{8K{`j%ryh;GZGDQt?j>SvRU#?&Y6Bz)$f{4LLTd z`JL;Z0l-J`PYszgs`>2lPd{J=FZitwDPMocdMK$*gle^b+u^X)h9Crz^Gc5vGej9Hv=AuiE2n1nVy1h!7CWc z8#e)dipgk55mC+WT$8yG@KH=gLn4T3J`*PM3Pnx8WHe-asAhF>lbQVBQ{1>gkp&D} zL&-(evTU0S33wSN#r!ADjfUVUiw!PN&=&#d0JdTRCFRT_xmFrgytsGoAuwq!5S$=By$>k?Nox-!rk1b(K*DoDfI=FOT$*?rD z_|)Q)i%%>*zIb-=vBgIh9|89kKD7AY;sc9k7ViUf3-4W&7sbWe;)%s9xXqASJicgO z#1^k#JhXUl@xY>Cac1GEg(pEBgvS@oELwP}5 zs4bjW$Sy<|sD<`ou}rH&)es*`RnHo%^#dUFmITj z(LbesQvZbhas65SWBNz+kLVxPKLqY%d_aFjf1m!8{$9PT7xgv$34K-{)l=Z6M!O!< zU#~x;Kd3*TH|S^3r_d+SC(y@1?T5$EN6|;nhtY@72hj)6Gw6NjDfC`c2Ddua&=Y7D zjiMBK9JQkudOdmwJ%}Da4d@K=6!IkU1oAj?cKK0okL1J44=q2q{J`>=<@=UTE#JE= zFN@2y=h*e0A|H3md-5Q2X3OgcS&9nmugEVma&}AO7LV#4(LJnt2;5Ei zfbNX$KHVu$jZ)T$x|;3;xT`X%qjbl2K6TBW+4=ovzdci)z`Sm54n2vUgm^c4H^jTp zyCB|)-U)Gpjvx-vAw(IKAr8<1#6H@G*h6~|B~*ggMY|9?Xa`~&Z9^1M5n>B%K@?B{ zB9HPAIh2FgM4J#BXaiy$twXG#HHcNT3bBG#AePZGL>6TsGAIMFgq9!{(IUhX=n04g zv;grA^bUx(qqjr64ZRKGt>~=~^JpGo4$VQ#qFIO;Gy^e>rXi-#6vQN&gqT1R5aVbZ zVhoKzjG|G95i|mkMrnv)Gz>9>h9CyfAjAL~fapj45Gj;`=tF%Fy{H$W2lYU7qi%>U z)CJLrIw3kx2gKv(afr8|w?MoZy&2+7=uHrBL~n%n3iK5aZ$NK=NTMV}J8Fk$Lv0YP zs1>3GwLmnZW{4)#1d%`qh&YNv#83?4G4vS3m!mI-Xhe+=UxvO6;!*S{#FwHkh4>Qm zB@nMiuZMUYdL6`T(Q6@IgI)vi2zmtKi_sTDJd7TOcr|)8#6##Ih%Z841o4IF3n9J$ zeF4O)(5oOmAALT=E72<jKZN)LsHRNj${}K5|h+jp%3h^J1e}MQE zY~(BYzC>kB~ot_$lO55dRSQLx_KX`~k$@ zM}8mTBgi8Ve-HURh`)>cF2vtKeh1=jBfkytlgK9_{uc6E5PuW-O^6R84@3M7z`FQLB#@fXowg!n%6eGuP^z8B&H=mQYn zgT4piyU}+;d>8sIi0?$-3Gp51J0QLteLKXrp>KnD20a7ut>{}Jz6E^?#QV|vA-)-X zGsIs&e*xl~&^JMRBl<>&_o4Se{CV`}A-(~91H{*(uZQ?L^mPzVqo*NmqMHym&<%*E z&{Gg!i@p}(YtYv~d^P%Ni0kM&#GgZd4&tlOS3!Iw`bvoRqW40)2far@ia`A!Z$Y4bkoys+ALPvl)DQ9t2-FYqCIspSc_RY# z19rerKgiD`P(R2U5U3yI^$64t@;U_S2RV&E{UDnN)DNdLIGt>|AS_J9`c?|;f zgS;An`a#was2}9#5U3yIRS47%@=65i2kgF~evo^Rv3^b>C!v1sM(&1q7jhTGJCQpf zj*t<=Au@z0BQnGRGJx1e`Vf0a52A!f5W7ehVh8C!Y$I)mA|gU;AuWgkB0%I39wLWu z5SvI7VgqSFtRr=ZHKYcyic}$1kP5^yQijMPEJOxjAeN94#3E9Jcmg>Av49jH-htc! z@pj~Ph_@lPLA({Y6=ELAL(CyLh*=~HF@t0vrjazn6q16NM3N8_NCILUi9?JbF^Ewl z3NeC2Akqj8F^q&^{{QTAW|m)LcrNlD@XODBz!P{ax|vt9)nE5qkiRRrotdFM-bq?$ zn?FE;e1HwXVGKprp17w?qW^HOy!duAhbrJ__W zG|MpiR~f>krtX>vtCH`uXXUK=uag{z!xD@bzP{Y-UJdRjLc}>Fsf++{%+SaKU|Ql(u@T z&Vb$H;@d1wh(d<#5!E(*>?j+FF=lHjOh>bBkV&i*Bj^cAZa;XQHypb2Fi6razyb`o z1=KGv<5s&ebda9(8fi19IO6HvtQPCqKS#uVDjiYFOtDAFi=8j;KOqPFkpN2myx1#o z^>I0f?Ftr3(%$YYzT1>Fp;IGX()7<_YEa^o?86EsfUFg>(WjL_&?de9@rag_* z4Pn7An1xKI8je~!Ul~ZGy=JEQD$a{OmJZn+oX6Zr6^D^vtjC0c?9hM7(@iNXTcgf= zJ!y0E-3*C$%RQmOG_Zick9bqKC60zor?ptMRjNhOSF?FVPv11CTez+`B%&F~6~Ner zJvNXMVa{3Mz%Ib6c9~a(t}OHY%?w=ThqL>8?y7dWvH!T!jk1dF+7D})ul87m$EU?V zp^0P6yj!b_z#9%L3$h{zTkncoak%C4hc8rpN@+TF)IzVrfewTo$0<|Fwnq`$j|;jg zXaFm3owtCip07Yxm%72mNvq=9dv37lL~=-YOQJnFh(x!FPbUNtlhKvgVnoN z{(I%lNr(4c{`f!3lIr|F3!;{RY}ZzK+zvNojTNYjfqZ|zx@rWI(h#~Gz+k=!E7h*za9#pA<}SvL)=c@f1PFl_BF2U zcW&Kmjxu)_yNWS3v;gk124+@|m%6Y$4a8R=V`>cZJY8GIhRqz}rfQC-@TLo8W7GR%M@N6i6hPd?o zjT=9C|K(5s4X0cC`(}Rf{>wBApy7Rb`U$|v`=6y*01e;O)d+9^3ZNl&e!o?)rdfb} zR>7)f0rpu1D^LIpo$j{^mNg5ouPRW^|6exqiJ7$zuF;Ms$2=ymAl5C(2{A2)ZO zfj^W zxX07icKso?(EcU;@~ywNyjc0q&D1ko;quRyY;(SMpRa^$+@jwc`9_E(?3B??EM z$$hTylZW<*VO+MricM6+q{4QBu0T4WIK+4%<<3--t)$;v@VlZ`d^{@_f@c`uXk=id118`t0S zinMg?g%=VYC5J!y{Oc?~+3pd~LeN4dKIrsQy(f4ltx z+lq@{4_~?MCH!QoM?4D!3!V74AMSkPb@yeS_%OA{X`gi8`q9~^tv+d2?(r3;p5*+Q z?^y9NezK|8c+=G?bmE_Ew={IjYtFll7Eb!<$-ll}BDz0{zHrsA_d5GeBTwM(?7Wbl zZ1jj{p-rI^(|NL&d<+(Y}Y@2y3;PJ4*Kg+*YK0|9`P)M zCUoNJ^^wfd?H->v@q|~7+j6sm?r`4wKAV2*s1q+g>-6g$JAD%4C$%2&EW{;r;%E0f zaEG@pPrgr`xYuafL{JLi(0FWe#Z!C9Ll54C)c-hI&LHhxm=5zj&oLMJ|U<#G4l-s%4MvOQPq z_0p4Ptz4SA`JL1D2rl0HoU>ke{^8|+;wP&;;#mkk=)}98f6z9&->#j$r%0Utz@Z1; zzx0dC*JO5RZu8n(c;Zh_{Q5e6veF}-h0=pgY$E5Zt-g2B9moZX-z~jKpMCdxV<(sn ztuCuDZ`oft0$#Rc)7FMV_@tu1g_x;b#-t5rSpRY|WGS7Ky`&Ccill!&jJC4DI zUfz?T_{ma_corHCI-y3&ogea( zN{@IJf($zG4cqMHa4376u&=OctNXUT?2

    |MaRKEt=H&%Vd)&rcS6#ItZj z)rs@JEIoE=;YZ!~&OdRdxoe6CJpJ%9OZGqO&T|*N_8&V=e0tXj{A8g=JPUOPotS>E zaC+qJBmR2kb!umPQQ)D%DVR*{k?@RM?ncouJ2bmBG3KQynos#tjGNBO5+$8NFg?8C{m z$dbJ-_*Jx!Tl2OX43KP(coy#tbmD!cp84$^pWBfeSA4nuk@Q~2-}5&|{Pe+0@rmEw zdHCo4E5L>Y+#d$p*jb#2>%<27V4=wL>@vE)5%eMV+tD8>!?2{Mm*B!lR zauIKz3kS8`I@mWs2vtO6gl$Ew@v%*{_*C_Qu`UtfASnMK~D0M=^pVc zj!$*seRoTKguZs|?S03s+IFq$;gPGY?!W(SVdd~ku6g;Sd+&Xz%TK0y#Irc;)rqtF z-}vmyNB;D)XKb5)`r*6ZKlxB{i|{QcY&&=b@xm7q%E-z5WU@y*3+GRr_+rogA0RJn zcf*QX?tS9Ar@pvrx8l*qu@t0jR*qg0oqG6ie)5nW@hlEc5-foBmVGIYoVe_CDK zV%2fdStoRyJHBwsSqH4;(z$tH`A_tSXK_5K6Q6_oUwHgV|1-}eUhjLud*%U`y}ftk zbTs?@i_W}v^LIB3{+^#axJNvTLpPncfx_E6w>j?7?;W!I**@s<+vmmZeQDnn$!F~O z%dea3e!Im_9@HbA#aWh4eAk+LAN!f@FY@(QUdbGN=z_@6)@S#YPCa+t$)1m+~vbAS@7Ccd;M3uiv`SEFIKXmv@{N#Z> z;#s(G>ck)2mVvJ*HWIJgzo`2CKqUY3-B*pSdi~sABtCfJk;8_DcjPDIJ>prMs_Dc( ze)tauE?IEtkLfp#TspYbc9rZlyS#bFZXe0Z&iK_^c&7_?o3S48EY6B_;s^G1UT4|= z;2-ciC{KBA*hW3zPn*Ae&+q>B(0vCk-uJOB?-bYl{@-nGbq>g!KWY2`kZXM|$fKSE zGUN{#-3;WqS4VaRS?fm(n}?nQdH{#a-wb5-SLf{va`cawFoXQ_13+&1xgc+R4#){V z1lkPbU03Js3^J&X7%=xg*MCy~0e$cEo!iIs&6#ugKk6W0;Ff?Lan4n65i|Jmv*8Pr z8sTWx>yiB#nRMeaDa_!@ZG>-qrSpZQP%YqOa#B5?4fxz8LZN5y$+O{OI}|6&?OL?V zQ5+u9By|Q~b|ZZ2i$E<*cEkRlhatle53Us{k_yb=lV-z5JB4y95N>(803YlxT#pnz^ej|J{##>fP6^mhT_moRJc|J?iGshdZ z5x(^mr52{TjLRc=B6P|{M4G`=TbWV6=tlTv^p{QGi)@5%Mt=!2*=)90iV!J5mWe{S zLe8ikyAi$_{iWQd+FHA-fxx7;id2>o7-mO$CGutn; z5xyDg(I)WG8{wNVzlgzXE8UeCr4#cAlq=cD&uBktBYZRZ%O>yzHo`aK`(@ZT9p$5v zE2h%%R?*j-Ip6!gnGc!qeH5!vMsUY7&QwYaP?d6g=6p(Sgm1?9C8L>Agsm3na*_dt zPpQ_-{*w7-K5WMLDAsaPmM^C(M6C#XrkYx3#`lrl2;YqME72OTP9T+Zr^qxyWp`(0 z|4VIzZ^ro81is`q^UZLs`gVOs(>J^!9VL|unxNm;aDl)PInqkcZUc# zLopFacWN3XU=k_V9f-YM=Mu%i3wse+M>!dDhlr%z7D==&lrsW@n|I&DsKqNWxk3df=ulBKsQo5N4`kxhxlt)pI1YlODmv zWnh7mF2qt)32nuj!59zY-h@=DmDA~{#<;T003wAGEHPJqaw(FdTwazX!+|F4LIU|z zO|AK})l{s~3Wa3GVyTc`uOn)zr$_qrG9oygZ&R4HW>F$sJ3>XlwmfdjDrH`ZMJzlK zibfq>v{?wSbDz*dGEGa;oTZT_R!WMJoy@yi{!}>MR%@M1QgsqJjmw(_)YBxifs=cM zWa<)nipR9gAoVTE)IvBG!K5VG z^~D=zB2@7D)07e@XF4@h%S*8`;tlxH!FEet+$$oBfkMnyw2&oXzt?6ezz#T5m#f*j z7$ZwV%ICFrxS+?&+8cSnTFPfsk3e^orneDl^&hN9BuE#TDy0zIQga8&Wn3+!^WbVs zlhvHbsOE2V)V5LxS(Gqvy3ixYc#FkIJQ{j^~_;-A)cP^a!L?b~W2@1I;63woqqiUX?Q(hm^Uh#uVf*ZgXVvbsM-W z)Boml1Tbjm${{}%&zNk{NZOZx>y<){DWPZ);mj3|rbJ%_V>;ZN3w!-ag5%9vyj+2U z7#0LwBcAuxJyxGeX2n7+-2vB!M2}T)!El>9R4)QHn~@x;wzB3#ma?a6v6fpFvg*Y3{L;~fk z9uiSWZ`0CF23=gnZWaW#D`Y|iw&Vmp?e0`YhHlWy$nlY+H<)bE@iJbn%l2|A+!S53 zCt~Y(+;s|L;$&K@I8_zx>(e9r)<6xU(8Ea0oA#KCyfaqxbp=}s@pvh>jf0DIxR8o# zraIC0{&cOH4wC@GjWABM8?4sta4tI*&85vrsg#7nnYcxx3dtCu5Ph&-h9bjVKkTn$ z+MTRB5+cjymLTPdtT~i{5I6DmV(9nMm6$4F?}+ z7^)k3*iFN~TU3)|+ zTgYILsb-@p%po4l+SDv`I+u^uVb~tBnCEV8UxBZz|(jwQ3b%w18 zS~cQPkpQVQA`xqz9vYdBs0jtES@hJS-gZI7rBup9#6dD>$jqT2*U?^~U1XL7{V+E0 z-gE?E%~{f(VwJGhE3Tp@R%*UfO(KGIXFVdxIKvQZiEifIwmBE+5hUwsh~`$w$A-;5 zQ{G(_V|3ipQE8XjWgAkfBWELtgy1)!(6xGmH|9%qXscFgMR?3*vZh-YqDC}V$klb= ze1<9e1rL`sg)I2cbT9NWsN`}rfF(1PcS}^Z)fB{9nxcvxb1-iTCxS>ZR%zG#-c}!~ zm*KI*sd768mToJGcZw}%!s9|JL4u2z>`a#`m$DgJ^`&tK+rLtea5aUdPmpajOsO|L zn5k1uAknmjq{^9j;8&WpyPG+86dQoQQNp zs`~U+hNCVlP;ny_i!YQE!h}L{nJmoWLAl_o+JZonA|7bO{cL)_v0fROY9}lOtERR$ zCwAphAX0DzR3T1saxffmAQshD_l7L~oGVBVPIpBUVIyRvP*2;jW{7vQ)*z$C3C`{< zNJ6@fXiYNHU{DipGeaNfjg{0eQ?yYPI#dMCp~V)Ht%n>MP4gg)JrN2AxhPqzSwlHD zH*l%`P)#ly7ouLi%QLLkY0awzHP>K?Fc*pY$b15a?G8yB(WiwhVvAr_YXh#b zPSP&QQJ0-UDG%3dG^{2{WPQLb7_W8<4VWAHSg#e#Mw7X^t5YsB_Kw?~_XldWqA9Bd zot|nW2ih2Tw*^x<)t=7xX&}3e+ibdXSs`%2mvNvo} z3gEzxcG`F;pQHRW#Mde|u|R8NyXjhm*__oGvbpM|V4#TxTV^+_c?7H64v_-m_k!&V zMhWIv#x}CG9^p~6maAAz6f0RNhENLJtT0VyLWOY&0qbTe(Q#OWSOY=KgJBtWe;z4DFULXY%W;qMpZ(s2~Npv z<_Ni(Vu$`XU8`W!kHpxFqy^J((U!D@am6OZvz`26K9Z`~G?|Wxezzse4n3?#z#1vq zDY z5u_)FVr0Zm%CeM3*?K|o*4h-iPABi~O>5PE?N90Yf9K2@ua6uzIMH{`zjkf^|5iI7 z!*uXhGEB3$xAX0`A?xX)tdpP7>NP1@N>pkYsg^IxK$F7)JCF_OqRiqx&N?BG*`)4m zSXXcsH;Xo;EBN2!h(Lw@tD@ky9=NT00!=D|Mp>tW@(nMp(e)4V)gxa7CQw1|)A_sm$V^l~K z%_(qpTVs7(HHot%?X8n#k6`hZ__RYIzVpcAduLW7%WAL4CQvENSJsL9?b7S< zy&6?!w_Gm;_5Vtn%mV)Xzo_=-u$kADu))smjaSTE%UqO~G=zLui&jD&k?%I-4$ipv zB#rpP>7q+2!Oj%=ojW_}rXq;h+t8@+k_uhsWW^i|WO+;6M&L+FL;@6_Z14;?NpSe> z4j8t@J$P4c$8#2^Ka&)=QqUWzgCEzywpMR7D|Qu0dvJD>M19viI_T`j4No38EV0W6 zRsy-dS=_XK~GZ zL;K>Aq-j8TdHu2bt?!I`t@y25;`QG}uZ#MxTjEqD(-X+fNs3b0d*@cArexNArliSV z%c$0+cD>irzZ#r&@QIm&^SdrQh#mRHg~!2(S!-a4&0F&2Qc0g;#)~bYqQvs4KvZ-H z817_%bf`v~Ti;z^yOSvjn!H42JW0fFZv`smhApKLu(OVLmzcppFP5x(q|_4L#aXIq z&JZB6a zvySaL_PsF(=>Dw*UU~P7UO)fc`7g|WZ2s-@ub%(w`KJLFz|Q=_`~&Ax^Y@#-&-|T$ z?_b}%_vXDg@9}xvdBu4@nMcq2@w_F#(Qm=L{)zV|UYhvh#GMnXfQR2NCnhHjolqtY zng~wp4_x~ePHa9gF#f^#%i~Xs-!*#4=vkvDjvhW*0T~DEsB;t^T{ODo=-|lTf#=?n zBlnKnF!H;Rvqye9vJB)RBuBy{t`XbFZX;Wb3;|!gSBIY(zHj)(;Y)|l8J+@J3hHob zI5O-WMuvAE-g~$W~ZBR2xbUMTdwX`_K=7KiN092Hjp7dmoa-+{#g+Ye0if6@Oo$hCN+|JMF1 z`+wDcO8>F_t^Qnptl!`N!~VVdcj%wj_hsKZU!9!HThZ70>fitV`=8bV^X5YR{e7hq z?_RV9x&gYufY=Y9>!IrnXg>_P4!X{O@CYzlyw-qA4un=gs|>iO09^xJW5C^D=xXR{ z11{Vex(d3=fZLr7T?t)jz-ik~W66sQ_~FwK za6~fTJ1hi@kqr3e3lQ)}GT`eK2-qYU@UN#pz%9vuFJB4)(H3*n38Spj=0-j3- zyk$8s(LCLNH{A#U2POkv|1JcKm<+h;0SNdp8St_jAz;g7z%vekfIE``e_H`l)z1z1 zn_oe|tI2>DZUd}4Pcq>790Z)340!f?5HN5u;2EDnz{kmeCkqg;b28vbM?t{V$$%$j zAYkrfz!eukz~jk)$KC-9LytG$5iSTgJ{j<^vms#oWWdf}AmIOGz~%r1Y@iINiC`u^ z%7E2hK)@8rfF&3L-cSZCGQg(v2m|J>gMd?%0T1b}Gm13e!7T{*MH=wH(;;9RX+ZH^ z2)IWYkUI?mCXxmW-v10RsdCoFxtDD}d{DO#>1(2>47IaK9tKLQ^;3 zzDGg8b<%*=DF~QP8qjjIXmIfSI00Cc1 z0}fsf0eedW4jcsmm&>o3J@ReUcz!uYh&+iQZcT595Q-y#@rU9P{L%=K3fPY>LOm9U4{&6b^IA^luOwK?R1Tn0ShJP4S18gRuc5b*Rg z;BmJDTj2u?c=T!rIDCEu@3`}K5HOZB;O#3Q;4f*w)oUPNGikt84?)0f(tuYz3oMF< z4S3n55b&Nf;BU`>fCZ%iFB%0#l#l^0d=LVLlmap2G4gJFu4}G3%bjI2UF0U(47W6@LcE) z=nezMPK9oVZZ{yeKXe;(n*rGbbSreL0fUc2e}MjAz`&WnR{9nL`d)xmL#qu)ybRq8 z-E6=EZh&rrZZhD0Ip{{{#;@R=hwlUd*H;5}rXXPcYQW~j5b%ICptd~(tY8gT-2(!S zum&uA1Oa1M1E%i=`+d}aNdW>ju?9TY3<0-T1IAv4fN87&g`*+h9c#eos}Qh|H6ZgQ z1e|0I7)(RJP}YEf7l5C(*?_(qAYdGa2w2a4 z4PSzQ1MS!F1@LobPXq4$BjDn_$JY>pfGzFU&;bE=S_AI5Hv~*-zlJ}6fLHC;a5o58 z)*5i@Mg)az-KxTFx@raU*evqb ztNM?b>m9#zc%PAX=Z%fMJa^0SXZvE~c3Gzy&qI&DHC9v+NN9=J6Nw2ye-m%hi;!jWi3jpuhy(1&;M9PD{v_mx5TdlR|?w zloVim{4za4q8g|@P*XGq9?sS+VWw*)2{e^rN-eFRl^u~tU9xdOdxRW4xffAN*WfT? zsd}xzJwjlrfk2?^Z^lGVh%>j;tkr3AF>WOw5G``(Nj-wFMAUk~obyN9Tnj79_FS50 zC{G#|fYFOk@%hlA)8WT>K0Q~z&{k|aSfOoih3J96T$t1%GwiNCwamB1!#j-4GK}%Vp9!rm&sh82QqLlzqbxJVa&Qghv zx9fCsb_K5THGi}2X_6>gE!s?S*w&}t>Mlj8Zn%m1%(-UVUV~Af0~mt6&8!-dJRJv> z$&qkcB+a;&V4&L)-Roatx+NFElgwW!@%$XL3lokxPPn$(U5D+3Gk zh)`CHo9V2FLd6)GNt?Z*6N|$YzEZ>z^^%4&1Zj4aT0}kFuU`Z!1#ECcp<5nPx?+M$ z9?+I)+?0)%GE&Zk#TkEsFau#g#sgOdUK{9LBU0=Rkai%LJEWqz0!2!!^ISO^aMB$l zmJE1dMa9KhydI6i<{$QIrPvf;B8e1f0a*RC&A7>D14)Loquqra<_gkfq(};FB|21? z=+~1m6l;mBfG3R8l$WQ8v=VK_igFZ>dt*&gsZ_@79iLZ@33QzsctbA(T=e6SaL!i9 zzzv7cifMr~i`IF&&D!cZVsfF8B1pbzZC8o8H|T98GKGBC-j$ntn32nPnC82!aNS4v zeJI|fL=xP{FQ~~>%!~GYsYgVsCKPtWVxn9igqAO5WigkK=Bu*B6iho^jRbCqHxu4= zuGRO6UMmL}EJVenRwOTHsl{BDK#VIhsV-M((z!@F6Kul1a3ZN{+R*ff3Ls$wPel`q z#pWs(6D>~3IZB$Hv?&FpD0<6g)aJ*7oC0%Wo9kte9=}o+UllwYV)^FwOsZ^BFT3U=(qIN9P&NCt{S^{n<=S^FR(Lyk4?n~%p1Q=3rb-}$l z8=v%rDw0!XKQTIose^I-3$v!u1`-}DVdd`DP=dY zO37}ilnZFP6f7(5c&u2=X46c}$H;jZyw3{P%yXXBAH31E%12^rOGpB4 zz>G@!sxDU3R9{5kIT1-9scgED4J1nrf-I5*OAo!Smys?O6NFl*VLsfQLSjMHk5@5I zq8kWSTy@Z;Ty?u=>XPg<-J%`2*m zTTM2oI~|c_UEnG+!c~2&H_WusZq-Ih zQMH`vWm=jyVR2`&2$<=?UDrx5D)DNaE_*Dt!TqNr+y%IX#e}>$XAukKl(UP&yt^o- z3T}snr@CdE6VFDwp?IwC!Cpj(Yp98u?8L2f&c-$aje-hG8QE3v6Y(OrNDG^34$mQ` zOe6*!t4CM^S|uFzx$MPgf(o0e5f|pL=bc=s>^75>99LLTG=by|p)#c3PcI>GfVMJK z$q70rO{1jK+#uOf9wvNFk_JVjYm6o0i5JT_UBbRz-LnR1mGRuKI>*bo$3 z6=Hb0bqg6Bd<8;YE7uT9Q=Vi&PR} zXC^DvoM@w4=MhhwYP1o%HQ})E`9vYZ)=D*bcu-T!pNJJB1rkjvWiWSnk}))>n#1*I z!q&Hyel@=&c==AX4i>r$iPazEqP++akCVLGqE5C`o83P7p&my^w-NwjDl} zz`H>U*5)lqr%46cz&0({v1OZLF@(k=N(L-4=){m7(T$plPF$2rjgrR{NIGIJ&Q=90 zt<9$4QkPQYx+BcWTr`RfyYvV&r6jZIN-S9{1kJgisnu2qJIc_}ZnxEzBv0CkCc3qd z)WL^;+>0ntbs_8T#NFLYr=STmYw9E@8F$g`m_~!mN3m!rgu*n*#BI>@9xj|$y`qmu zbYituNkeT_mX3u|sF|osQWtArQr@Y-WWZmojP0VA!JwqC3&tv2Y$LEIr{*F+Sef%h zvSAa)*<&Qu30J6ktDfctr`JJqFcpKHg5nj!!A2mL#XD9N$9xrgsKAt>F&@d}Q3696 z`PPJv3Q$7hlBEDbx?AJt)=`GT}BBLm0de0#0J-@s$1?Ld8+Zi#|3>dR@6}BcU*?%kSk# zyB*Icfd(3{z9n(Bf*yh3O*Evju?C+aX~aSV+YS%R<|Va3RRV3T+=)m+x9SF- zr$Z;{5t(k+;cEKHS~o_@C6Xk)$w)MAwK$6oQviiULNq4=p>!N%)V$M+P^_kwza7c@ zCtkg%j8Hsg(gm|fB-7e{u}BjOpiE&6MMZK-v~6U1dG>b$h$}`VSSI4nnHY~23Lrk# zQVlxXEERTVJoR`rY$0M*a&Ej=hC;+~G~Q{r{X{WgvSgALG1H8-!DQ$}uyhrtO1OM70=9u4X(*jHKX>HCs+umSowcQYNr0NWcwNMxy$22+VNP&$QVrNzb|e}m zPuEjN8eShC3&=>Vp7#4dvIfhJpEFM{#7dHC+Y+WT!DPsRdfRZqPL-LuS)@!zr4ui^ zTrE@6o(2gPLre4sA55j}jwoEQ73hKsPUqQlTLX#@#VS&Z6G?y2n^bF2U#gA{O%Gdd zyIpN{X;dovqv56_RmJUE3XPHhK2t>6DYj1Mq81d~NU@FL`h)nZHCZU7(ur;u!7#Yw zw9^i=7%l}UOE=+*w@_aAz8bn29oUXf+rZL z#&}yI=tDetbmY%^L@iA!MJ?XMcvg1$^0fr!$fBwswDykVXt6kA$%gIHT+CsW<62<)uTdQO7v?EB4CUh~xxcY}zR+`VhbiYS`_ouPr z4>@VC%h%d{@AQ{n9KU+&?_GEFANVO<)7y);njVwra@!KgXkF$6EZLrC)Bb)qVFKz4rMB ze#}qln%-uR<F;70}h?kXiCRB2oWSX{A!Mc-cGYAtb<*k~KvQ5|cv5Vfi@tK>p z+Wm%XN3FlQ{F8z4C;sE$$?Zcgy`NoK+^VvipVBqG(Ns8@Ribe|gEP@WJe39K6j)xV zI>&+a!O0Kz%)fEmoa;}rKX>C>-!Bjg zgZz}P>0PweGj_CsH)5n(BT_z7%+hTXNZQ(In^3aGsZNh4f+{6~MfIjQxp>g)_s+TQ z$i05&x$EN-wd<|pf7$!+cP1adXaNzpC%a%iKc#DWH_dQ~QEG*-ykW!DculwF0s;=T{X8 z7w!Him%#kZ3`vo!%>lCO8!^2pDnI$z>R-_}ZE?;m7xf)|#)WO@liiU0KREO0oqxaC zi`RVI{|Y~)YkEtYo_DMhcd;GSQ|`R*(HGxBYzrQ|`i86h*Y7FpckP1xPO#tk+WfI! z@Kd^`H?UR^FbuKOrIN!@u)=Y#n+8@E-b^4Zg(8Baft0p<(!z-qO((wB!Ts~MNiGYHbYdC7UG}heBjqlb%Vv`=;EFXld#()5bn=S3Zkx9B$X`Cpezg4a z?s$i-?|Gh2$}oj$Kg zG6%>;gW$^qX%W0On$(F~uO0vQRrxcc-Y>RVyv)73e`oJ@Yp>d4n9ljSbyc>i za7K-b(IOl}D;Z0J3r&A{TYj|W;+=MQ!uIa6M^RPK7G#nl&<84?&9XMX< z2u+r&MAIaZ<&(AqhNCSaFHC=V7v)wxa^Yof2xou)&7a=a%-&OcIDGV)oxOi7PJKRb z|J)UMeoEK$22(giHNB*X4popcD%%QZw9>G5xd<9-)6POL5rchH)Mctq&lOi>@7w3y z<3GCXqF0vf{KEcQod4F9?2>J6za{PX(6Rkhw|?>jKc#DWJ1DPHbC$&l50_E1pQ$hy z=4u3Uk%G9B|cOVi$S_GU3`BQi_Ua|wc{t`X)7WxeP9{c4cM;Bj$T;8tO_u6sA zgL}TW>X-bKuIbG+C6=(% zlvQl@2$sjVZcr4{$tY`y8jqv*?z?5utH#b4dD@{cgmSd;(! z#%tCd5PFTD(lxyc83z@zl}MWkH}e4`rxdi7IT7JQEh(M`r-Lx*3YQ919+#%=e@=Yn zz8|hQ{)J25KK`=gsXsJ7_Q5GX9Qvh-KCtG9pPjKU*SD6R(lxzHo4%pe=ZZ5^!XA{F5)vGL`r#UDG?5La8i4S-~>b4Aes!$Qv)l z8o6?%!-ctSu8!s+LL4iJHrMo!Mplm<=KQel@c-QFvEOaYW>SBZFW7BC1TDSyy;J2U zPkU|;eoEK$t~*>&Sc@0B?r9me^@&>^et+%npZ|Q$vIjpr>*ZgXUw8=YD|AioNYE6mQ>8*CO@?EoJS%Z= zF;TST{BWQ|RjE1=sbr#sQmfgS?#I4oPrmB93mUgBNaarIc!V#43x81i^*%p;bHQFa z1hLCkFXX3mO>e6|R{rS{g zpE})qZ+NSZSN|!q$BX4-cG#nJ&{nSu{AFV2Cl_w_{+axguIY`sI5kjjR&hlP6msbT zik5>)HEAhG*=)lXao3|bWff`KLr-_NcXr!rufM$7&)xget#^I@o(E1n_39JXK6`Eb zsH@FmPio`eyO*C5dt%{*STPZ?`l2n8Xwq3ASck4kf(Y6L_>wdTD zz2d|b`v;Pb-Lia%<=wTPr@qKu{R`~eEswtcOdc;{Pm+}a3EkSkA zbX}<8^;Rho&lkhZL`E}rMXT(~+meZj&pkc6Et9s}vh@0AH{MwL8Z7d%@Dto-xc>9_eQu1A~Obuiij>zgwk$d>B@6w;YiEV!3C)e+Fv-Z?ejy~T< z_t_s$y|Yc^i>3F}`Kf4+IIOwb&TuB5>;SnFjVL>rb}_=xB_Ev2<>EBik^KVMl!UM_ zy^ZzU@-DP>s<`Ny$v^yH?q?a_`@8SdP9Age@V))~E`L9jIE0^y^oTXlo`G|*WG9g6 znp-wYB7sMPxsE1<3t6|-Q}uS!nC9Rt2tPenTygl>&usPmyMLoSHV^sooh|I!|E{ym zv-6Yr?Hv1Ec{TE~z)yvH#8^bIgcvdIu#jQ8fvY*y1zxnkY!UWF`BE&BHL+5y+2Sog zPObOj=5M=QjsES)x5Epp&+T>Wxj$E*e)wedF+RKbvX^$e?6WPOfcYu5M+}~MQ-7oD z#HTO4uX5RA&-`>(t9eyz(e`)Vy7GK~=hB~jf6*gn{PwE5%FpssOph2Tkpa%)3CmfF zjf$kgG0fzvnItkEL>pejR?EiaNL|YWBgN@{{7Pyx!m`UhZ<}_1B>n7h|GD<~%(MG! zch2Iar*D4EU9_L#r$Rkq)#bK2LR!M*1I^s_cHH4y)Yb$NLMhw~PE*?MQi!zzbuX7P z-77BI`NKC}-+#FI`^5`>Ke0{Zg?D#baJg;HwU&A5lb$+y|IsKv73>i=v&CXtls)C7 zR&^DcrZx+k?V?TaWGLEIPbnTXZ*NO}Ivt!I(l>p)=#DmY_9c6c{_u-CCO&xe@U6Cd z^#0K&Uf$}W>+g7hsy@X}(LG|232Kr99M~0Owv3ISLGF$tf@)Sb$JCXOU$$C&Rd=>z zNn+D$-tnK@e8h$ODEqft%g0Z19U8dLKH+-v7cU(07xvYOkFPF1)64(wKX%Ue@xxz2 z8u-V*|NOfJ{^>0M(jQY_r9aNX17_j3%6YDr5VkIRXoETj*v$N||4I7ex7KUZQmEFK z8(lwz>bs^t&fxv$r9Wb@8NtA}u;KJaylb+)Ev5c=Qun~2MCU};ZLtQcC@=0!s$0ct{`eX0W_4LQ2k^VRfW1{brX8&K) zAFaJ!x1sb$bY}P8Wzj4IN7fO~CU3d-`B^9oY*-d#7T!y%oG>|Y#OtVJ-qXlutT9kENnu3AWCi{CjFxmza60guCC4R@3r zi7W|F;nvRkhss;ve=7=Ib}&R?S@#xzzIq+fK?c4!(1b?Kwwsh zBt1E&3)51eO-q05J^H$_2u&WcZs9p$>%K*^=uY2hC~k1!ar{3y@@8R9wxN-i%WL&Y zt&?cxwR|eCRZSVr*3e0>K@jtu1jC zi=hbWb44NrJ4)pmYTV`QYQ>maH5VC2GF*_kLf$TmVYe+(4S9W>TjLj3B&wj{N%tmg zMo|nrIv98Be*cfnk>|`iWn!d%q#k+$LfgBBhQ z#QgjA`vw&?Y<;^1Hy;TEU0#nU(r&yRv$mTts|UQV!5XBu_iruf#kv`H#TIhoEmr|= z(a~Tv?&TX9uTn}1e24Vb6^X61fV^=lW>t!$H>0HTI7YgO0O0J#O5pP@rzLIWJL05V<*V9N@ZFRbUw?k0^#QL*zRBi{mc#DdXNU&SgQr2KD<;^wIFl!IFiDyeySmL?fXX9n? zSu2vu`1lU^Y{(OCih(>^Tx85E_HZBK;t zn{zSToa8F$8oRDnTa2dO_nVq}eNnP-z)OXB(L;OxrJCCPk7{ZbTx<6Wkz%__^B#YS zz+oDWZJ?=faCM*?=7VUM_FFliaE*nerlZ2b)@0BFURiVs9HpSDD=D(QrZyW*jeS#7 zuP^Vz@YMgu-kZQXj$U=5cVFt>s>;4X($!QpD(;djd20xXEy3+%O@!Isnp&k^4rZ#{FHMq(TVr~)_6XLiv>=)QcGhQE-d7G)UbTW%ESX zP<`3lft2o4uEsk_xtlN6A-1m9@>MzX3aoTeuFj&lhJzQY8OAH-6KIg-8ktI?iH{Yq zhLyK%11~fal$B_y-fUWoJu%eDWkbE#i;SeiB3O}B@Gq-eUAj}bnivX_qxYna$4U}C zDI@fhCBPE|xG}ultMP6-Q3t)^^igq&A&GG&X=n&EKtPuPnM;8!qST33Kd0N;Sz-kksAyW1HU^P>T6F7HM(r57dzPHiq$ekV?ti| zGD;xeap?}-fe>B4>~tv&ybuDd%0hwYOlwo^1h2-Xbjukw3u#yyIPsC~@U*SPbfU)N z1nzuk!3@@__Qe2>RetH>iOE2(@@{Br$sUk+RS;hTFZ_N zypHP#ZC6fyTJ(voOC}p#p&*%Ak*Z*NFmf79&<2k%Mm5m@r>tv=*MM9k+nd+_f5F<} zTlZeL_1)`HUxTlF*1)nr>+xj)rq?j~zMNs(Qrd2%8)gUU+a(uf;l7e{_yOD>6j~#= z=ySb0wjP~6p&aksSB=%1_C&AquAXBheMVolYgP&e13E#uh&^w@iG_i`>;v&U3Z83O;28?QFYU5G zCJ{%Yg@+U4?mDQwTXDLeji*@4_(^c;I)SyF4(M}O%nN;^+v>!@A@3Y&)}H>d0Fk`r z)ClpqrI{(Ok6{c0&R>eP4qvo;N!0TOjL3L3oMx?{newdOnC3E)8V6;#JtMT}`XHUS zo~(pDhfK2+WEx3fGQwA?M?brC5O<*Qj2^|^JGAxlOgncj?%KfzwxJs>HRwidY@#J3Np4mmZs$+mJ z6VbP~{&d@Q&)7Zr-HCH65cnj8rn)`X2GdhR?b>Q2>#4lekvdeP;{J)5j&pq$l$@{2 zKdJZ(-&CU(0kdB_Hdrga^*dPUSGDNsX5ZNE$R0+Q^9(kg$)M=+Z8+SxTvhCTGi8R? z^JB34dE{e>NqbUACmGlud*G;Ri=?qa$rL&4Y#j=ZLvGmu{dhG~GnA=xm|-i2%9pd^g}HOvCfuFN-Vl&rss{ zLK@E`Bc11g+6V?+5KeB;?MCkTcp3kw>i!w)`pX{skL4@C>S^w~$sp%K zp(!J$mMLWNE?KjxSSiU`gEHK1da@2)@gt>-l%}3` zi!S}v`u{y^KeBfD2QKrM{^8R1T`FDt^u_PKNFII))TYM{{^HQ=*VeCW4Yppo`HP$3<~HYQwpMQ;OdY*srs7M^qhbPv$Gpog z%B7B8JmdA^^YMZR%Es)4Yaowa1bCIUUvwT`Xl5j8O6?e{7tUC{@I0)LbScN?^<%7F zFk|(C^RSAi2tq6^4ihW z(shvCIwQHzD7%tY#JIGr6{ws~dnA(tLGj!f+vl#p7HLg#ZKl&!^LlTnH$h*a(a27s zPOlp%1&7ckq1+94b)qFS;pjOtw$E9CZMl={20q7Bp(e!nEP|`euu&K$vmLiQO&eJq zMN`$n1T1=YgrjHA*gks&wv3;!&6b(2CTi^hX~x1t5AWBs5`pD3&W+O^RZ|9>Yf4>0 zIC|EM?Xy;3%PTA*#HD0{bg3HXQ)>yf3pz|^E8~_Blr^s^45l!HbR8WPjxNvGUS5GM zh6c%8rHPt!2jgAZ?T@l@eF#lMWE}Ft99Ce_d?|@1%M-x%(v0n;E1iL5v}&%_BL*F? zm^AXk1*f^%TD=>+aCC9T_Tma`%fmw58-OEedc1*fu+(gH;-XSzv5B2w;&yxNf=27T zai3Jc*dET<9qBxU6JR9+O0c4usNS73`tPO*oG77EIc1f*9Krr9AlCEXo?%C5DT@5SkHDRR(K1IX2^R1r^lb>sqo!4{Hr+ zEF5jk*lw-BwoPbcukSl?TLo>T`(4oMoJ$u{UaplH>KdEQhCFLRZ2}%Z!qMi8?dA$> ztDp=)XdBHEXiQbD4a`)z?FYW=)Mdo=)LNc*COuAPtRxJgY-7fDV+FQy-In-9MvaGL zp2+4|e<%=Sw_a6+QMGT4d6sg#BwU;pR9`q+pRrwEfo*@Jm?Oqz>TENv*tQ&V2SJ67 z%cDfLUDK;{*UMoY(Jn|ukiphwY}Zy`E9Zt80hRllZxFQvR_-L#HeTZzWxQ<r&t&-`YjT6o1~E(gOYircC}YjVoYpUQd*Fh$~PuyI+icDQyP{9 zMJ9yD-aKRbxfR$3ysV8T@p@3HP5rvm8o6bo4|aHv5(Bw83Z5c51^JfV5IEtn2WM>m zVFk8UFCeESK4G+zK~d@S6Re}z2yBl=m|P@4!A?4V;O<5rVcM8Rd zY*tuQOypvsClbJ9;~Cq(Ux6)8HFIMYnR<)?YGl?Ye1YaBJ;W>&^(u-F_(TbFbTZ!L zM!;`JGq#^zfo(7``|MQri_|z-OKDO#F@k;!$;47zs#VX`&?eh%iAvI%f*#7kjY|pW_EIihqvHjEvY|pV$7OnqpZa#bM zFtPJp;D2BFd}Rdw=N^H#?H<|l)7j(os{=lX%OxolSe2q>)SQtd+tm`+S45*Rfh?*D z8cvKvLTSaIv%~U)S_Dr$osC28oW2DIYUs`|Jt=amO#)E3|YO3J?v1L>H5WjmUJe$6&InS zO9$_!-~Rj~D_Rx$w$%F7b9mF!io_EGFY%=aymL(6kxQOuz3k*&Tj#j9IXO_l;=>?P z<*5g_pP@LteU<9g>Mc-3w|%dxhPT$H?~YcKV{r%u$Nr&s15U%>T{|d*oQkwQb)6~& z2#$yJZRft1ty3*-PmN?f-CU5$AWJubvDaNnl||k$t5AFxW|=P89VSLq9P9+^o4Ii( z)xT~kiJ|NJPdgV>!f~>5jjZ-M{ULg-I;Jdemag5Fuk+PS&e~!{gc2R)}w1`rNdQnTySaA1o@~2WHgJ zi&pC&-+1uEzMZCWgImSa2@ zw7uQWAD@m^^YZ1Zu}|k(?+1zbZG|H80kEJ}|&6 zrx||Z@sxP?)$=&nt@Zzxuc>R7AG!4TOAlTA+{FhEKYJJ+eCA+$;nNp_{lDG!_x^gX zzx!9ay`4|)INN`*-P!u{Eqn9NHqDJsY#8ewTi4e97{Hf*9)1_KWft+g^V;$^Ho8aDaD{zNRkU6&kci05! znF8lXE_d1lsdFoEhfR7U^iG=q zKDPpQ*aXl_fpg4oSMRU_*JlcxVt3rU z@Y!B@i zE4J}V8}HfL-u#2@pWS}fwzK);8=t)VJC}d-^4l-#mtS}J1(!Z~>6b3O=hEmBd+C)I zKY#Hf7k~KTTQ5FvF}d-|_0O+=Wc`QN-@5+5#Y=~uIQ+j3|Mj7Jm_K~k#%P1x{N|m1 z-1+dqrw@Mp;D0-K_@H)h&%w@x|8n6cE_};{)`f4l@PhqM?*G#Md-g~B?EWkFKEL;o zy&ncA4j$M`?p@yfv)%XYe#frA%j~{lJ-L2q?GroyV&{IqFR^nGT*fJ%S8Z||SNaqv z-d!28Tq~FFfsKd5%EDf%LvKa8-imbYjLsu_glln%HSRWPxnJanMY+O?bk!B<_!a56 z73nG~(v|0Q!br!59kVy`DLt!CFlXU(vW9_a*~vvZJMb&c&{UNy3mQ=`kgdOGMY?yd zNcUYcI`7Qk`{_BqGl%65SET#>73qF&MY`W!k?udQNcWKy={~$7-EXZ(_n{T(ese{- z|93^YkFH4fUsk011CSa2>XhvG&+e<)xSSnFKf5B`&&<+>*VRTC$u`?qhb@)B{)ff1 z@x0Z@p1T^^b5;gEF}^ZY?$MY;!8q`QAb zx;L&!ci)^&4~(Yl3gLv4Y7>IOyoE%6@X8hGUa=zG)fMT!W<|QYSERc#r&Do*sVLIK=4&V=*X!TGEt@^9$p* z`==|?ePTtrkFQAgu@&k5WJS6^p3~7HHEb(FS){oEbKWK`YYTvm)JVSEPH*Y#KcCIpL+Nk-cO!vKOyL z_M+9uUbq_B3+DCz{(9};V|&VWcm1#b+1(3%<$@MQ0MuG>9j(4ADukL|8aJ`P< zHf$YisY9S>H}pxnYBML+S~pgWt)tSfdqU}p(@1k2Q3cI)$ z@eA4AmyVd7x`*`(sI_vE;!kf6YYK`d;HCT7lRei@^sdLrYQ<#SAY&_&vJ5s=3As@r zNUP+v4jJc0{U&Hr^t5+t(Q1xq)-W$md-NdPyry0kb43G|s}9Fbv>K+gx;i|lUssdO ze7P$3iJFsv@hhB=;Ii3^RU>8sQ{X3??fz8=GG`*lPv4Ty~?#d#tg3 zXIBzdjJ)(6?sEAIMaL{4N5;Undr<|iyU|(r3=9?Y5R3l3Y)|bm=Y47cqNwlP7j0`| z3W8Iqr5m=v)@M>>D3^ySdZmPxkmPk!hnu4y(Qggt4&hQQy9A?Sv-0%2Tuv=9RRFue zm7xK9$qv@f*af^cLeO?uVGC$b$0>3|w$s@*J>&d^GXK;c(|X!;ql1WpWRdeu^y zAKTZhazdqQFs!mUP+=?@!oZC-OMcrEx_Zk`E=(U2uek_ zA}1E}Oi+4z(!G|-nl9ud>gKfQ4{L@+nG}s{DVNTwLCUryZqkCD_Po=qCaPJJk_~(^ zP)M(B1rV0BYDh_iG^CsA<|_uqPn&r()so18K-RKcUK!`LVh9h3DUad-Qx06rOc#|w zXK;-z7lissXD7fJ7jWqy6#wOmj@r?@^DVDiKhLr2b4OxySmwmQJF&^-7d!CIu}pgM z9pyO=f1Vt8U&M1!Cmk+&>gkp*po(m|J#6n}b=oy=rlWN9mD9>uyz zbTY~r6H(6*7%@bPakC4FT}tTY~46LCs``XI{Fj%-KMgW9;=vg`acO_y1%(4`9l zB`oP}yO1G@>X5BLRH5&b`#hGE>+P~xu+(%AbXl=x3PA!Jrg^4GIb7y?n&?t?DG0z3Ui^WJQ2*XV=_nr6U-PN~*041<>6ORvTi9&D2Kuc8qH!yD2rU5gc{VnjQh$=5u%88!bXtd=%pDQa|+*4IA;pr`k zkMX@>ri53js*}|Y|I-{Xj*pcZS}TjF&WS=(9FDpZ$CK3R~Ma7d2 z%5-8l=p@;1%M-$|G^*p9Wept|0=oaBGlbJgYx#WDB%BUtn#5@&pKQApRWC@C%8>>e z7lP&_jhKzDw)s7C8LZZ$v+3Nh2g0e>@eMC0<al(d-U4c9K%#wHKZaH9NDiL6(Y158jZ!pk}OZ9BIqL* z2jkUFnSPEsoL{@^O4Si>c@s=c=ZQ=O#cG2P4M6AU80G2dD&A$hL@@~=<@Dim<}#?z zpMXkXT~bL)wpW!KIbY{Y7A{e(SW4wONPS==1{pKZ4xcqg@P(014C0JuH91rYM}kz$ zI_! zQbzBXT;HiHF~sL<)jVA-;BZ%sHG72`E{tgtz4+i`waau|D=vZ;2>v6FnCTZ|k5zu5-?rk=pO=JD4SPhe$i|~aGuNKv^C|h`UWFvCXtQPqR3XOAF(#sF}eyWK0A?kAL z|7|W}z%oK)sB*Jpt`MaB3bo>At__MDOah6aXVK` z>FE-xgrVPuw!dSBa68Gy&;qlMpT$OEzXxm0;!w0&i9uUw>8WHE$|WV$Q>SF-qBcha zRy9afb)1}}QMVJq6LF%~D@2B*N7F=ak{BdLlY*1%R=A7%b3|>N>y;Uz3RN2}(}uFS za$J$>l|d?=E14L$^=0MUxJNfC)Zu65*U=UnvR)arv<%)#mUw3vRq-%o&v7Z6?K7LZpU;ek5j9#jNWSSX=)l`SZdR{`33O%P;n1;0hT9B1iFIFe& z-3F*`xBbpJBH7n_lWEl(wt~7nG)-*;k07$CS97sGR|l24ur^14#`l4;^ZRo|yim*3 zM9axBNfIvBeYruUxN5O0(G|W+bqo|Q(v`5|PdmGxnj;{grVJCMJ{gqzL#AISiWR3` zw!=CRld1^nVa0^*!|D{{cHS{Z#DaQQst+|O))dQA(;Ey*8ROm&qdI zu9lARwU`OTIIGmMrd+GmVuer_lvtUN+}@<4tj}e*0XpL3x{qWgH6t_Af}+Xsj8|!6gf5niT6bK@S4bi)7Lv`f z2hL+|fBM{rzndfco;pQnL9BF*4$HK7wo2y5DMuO3 zPqjfW%@2}#B01uRVj_^t-OtVuuHKe}LPaU2OXIT73~K2;R3t_OCk&{q_sP{N#aU=et`-OMCQ z8!ww9THRvag{L`zWff*1M+j^&; zp0=?GRfY8u?Dz9{O=%51T?vDBA1x2Z>ubS6glCqkO_wz90#FG`lS3!|)jTxu>-Iv}% zVC6}nn@+VLCmZ*ISd*=F<5W!NF}_34w&yqqsEgK0+v57I>yQTo?{ac2p7POVjc%h# z-cAQ*oM?|(9nZ?JI_#I?{Y(+sc+p&j)Mxv})Jk-hzy$PLwoisa1XL_hS-u?Qy49*y2 z3!@3E3OHR$PTCCbA%20KI%taKySC5khEL(xFhi{U;2cq*yF&$O)VW424LT(Zs>8C* zBAyg$nO&#o30}6@%uS^()Yoa95E*v}ZE8l#O{X5uoe0vJaJJdzFDdM&hgrIi&&U z;EIIh(g4PxsXB0kSiIp)4K6kjH@|T%gOo6AQf&Akt{W+q*!)Q8337`W6QcxX?!@W|ci;P@QA+fBs{SlKzyde~0{jK-TZZh0# zzv*V1bW3vljMpm2ZfjbeHnNmy50Y%qzzYs15QYbexY~0O**-&eNR*(UJ_*xhJqPnK zv!jM;F_U6PbQVoJPEUjTwzB!q9FfV&g&w%InIaRoSWyR)9=LI_vn`y9XB}iLD>m09 zEKY+vTMy3>DTTM8RIb&>8v!cwgGp18&7h36MqOmusAo&lcn)-{CzHh5t;rxczzn{a zEC+>>)$W1UZBsOvF?#Yi#a0`BhT^la5;esv6x#lixr{{4Gi$kO&Y);CXwX_+*QR4b z(J=;3m?FVj7+G(!PR%ej0q;=LDV^ zb~ax*M<7TmgR+=lriu)a!(Guqc%JP!9lF&EGRb~Q$dOW8l|pgz#vB2m0hbnH8g8gb z-bAbcK0%5iT&F`QZFQ=t-ZZO2g8@lW8|WMnLxPeM()kLmWD5Sr6}sd{*52ShW6MSo^uP%fE8@UtPZM z^3_X!dFdxEJ#vY>^lb2y|GgKx7ZZn{Km6d~I}eq^yAS^B!H*q0bdW!|eBrk*{M!rd z3&{TG_CK)yE&I~`U3>rE-jD7*xQFju-2KSzckf!e@XlvW4!T(js5lCT>s8>eLc4JX`s%kAN0|~ zt=GM7?Zo;%ND^N5cl6PNQ(*BWusHhYh2^kB=44nRgFd={3M{n*mO>xhI|Y_r0!yQh z?w$fmE`cS{M|Vzvp-W&W`sns4u*4Er0)2Gr6d1AuhM)On~$6VTVifBjoy6t6xb4Tqe=AUL#M!&_#~P_Z@&2y z*b;N23H0WJr@)q&8%5BY(^Fte%#Fs;o8c+2C3Zt$^yXwa3|V3~1lVnS3T%np5MZ~_ zDX=95K!5>)Q(#NXjR12GPk}A58v^X+p8{Kw6c7}>IXDHj#M}rlcmEXF5_2QK+}viI>EIm$XlT zE%A~#@DlqJ*b*-REf#NDr@)qYNgQ~Ic?xWam&AdWv`&F7@sc?365|xu5-*7ZFVUC7 z;3ZxH1254|fi3Y872@OCC4{ zw#3{pF!%kZz?PUB2IhX_DX=BxhJm^7TMmPkm>UA-uAc&1Vr~eSTR8=`#M}@tw|ok0 ziMb(QZs`=*5_3bq+_h6+OUw-cbBm|ImLvrTBn9CV*b*;+fR|KHfi3Y82zUv93T%m& zfbL#5xuvjp=43Ace@AatPJx~5CGiaKlJY6Alf5LK0batM0z27D;u+v2rBh%hdr3S4 zyo5OgcCwd%$rQa=JOy^Lm&7x`OXyQzCwoae1H6Pf1$MHR#52H4$Wve^dr3S4yri%k zmR{l|Y2YRKQ(#NHB%O|)|6j3QUc3D5m!V6)eCbUWKXdV&7hiq&^M~@`*1>ljkQe^& z!sNoU_W#5F8}@#C&)eJE{hr(&+jkj&QeEmNG zR)4zo$f}R;fNuSJ&O!WTPiovu?A?E5ZTscK-jZ#p$@s#lnKDDyWV$ZOg>~nfKk%)u zg5#-sAn+etF$UecuHD5N!+Y<|`k|}cdoL*nmFv(wC~Dk9&yoW#$RQaTG$;cvn}|rx zjgPJ<2RTa)v>>NKD5YAV%#&gyr~j8L%88#LCy`l@gXILKDByTK8_7}smlfr}XUSRI zHeBZwlH)1_E<|#Of3u<-=qx#lJE5}mNs9MP^dA6ik)wX@_b241Cv6G}ag%XB2?!BqqAYtNFi7RUst%g?L~>sB z#ueqf>MS{nwYF;#Qy21ex)>#_U+Db&igI3gmYl@^7x;=;7E0xElt_NT{@NAgyy7f5 zi$R_%6-cq3ql$PW=U~-1y?U0M#e7hcM4?`(mb2tc&Sb^w`I@ujEaroJi7oITf=UdK z^ZrkK-->eXK1?}Ep`2bYTRw`V! zoQvk4_y5OLZ!Et0EIEsDS`dm1Mv6*~jpTgOgDYJRck`?E;8)CV{E)>AV+MvlZ0N?w z){KbffB5)HJdK+#JxljRNm-`G5mlNeXTIgBE6L$*zT`>W7qiKt?vH)-N<595FFs56 zMM-6auh8HgBOlGj@0YtP%6ZXQau)Mek;zgRo+pb@-g>{>UQx~q&yus4x9VVbmn0S? zQHkWd{=F;8dBIt77W0-U$^<8}7+IOg`QKNR^Zc{qEat5oT`CC_Axm5&=efVKqMYZQ zC1){j)p1T@tGNO(%frvTdqp|VJwr}>F>jHCh!wMpT#jPpeSh?k73DnVEIEsLOQf?! zULj;Wx}SaDAN|&fa-MyboW;B)@ztzQD>23B0pfihea}jAE`Ues{Kj{zU4GAH=knKI zKDhL|mww>V)uc8 zJ+>$By=wQl0f)zV()^>ek(x zpW6KS&39}*uo>I<+{UkNe8=Hy5B}lc|33IP2i8FfQ~><&h3~uIU&vkljf)@Ous6~h zyP$&L_pcAvvGwPy{n6TcpM3IP|MMq*|H^Owd`AG<+PHsX{VgXy+9L2`G_Q~6fJc`B z7Q=FVGzZ*V23U-*^Bc^P0ad$0HAfCrZVpksz}=YZ)lz%j!)b3nKZaLjPsIbgC3aLjO> zIbggDaLjP+IbgI5aLjP_91tu6EZ$74TXVp08Q_?u%{jnd1~_Kv)*LWc1~_JEV-Dz_ z3;-(^3omxo^*O*>1~_JEZ4T%y101t-a}IEq0ghQ(odcX@fMb?^(;U!U1~_Kv#vIUD z1~_Kv2j+nGGQcrQ-#-V~%K*nL{l+=KS_U{~>HFpYa|s}Q%+mEaptTHe%+ksnU@QY1 zv$Q-1=*s}dEG^9e+A_c~OV{Rr<}$!BON(=Wx(sm4(!w0@re%O*mafhLjb(simgeVx z2bKYjS(=*z?q32(9kX<04tU4O00cQE;pQChElU8$ZqwIq%mLrL3~?9Plre0ghREG6%fzWI#N1%y8v7;J#&mV}@hrfci4PF~gPS0A(5AnBkZ?Kwbto zX1L-UAT0wNvjaT`)RqB`S(=&y#ASeEb|B|~Z#@}+AjedC%N!t_42Y+Wxkh0Qs4f8{ zj~OmM2k^@P$I=Wj2XM;($9xH&1Io()$6O;f2e8Wk$6O;j2b7ipj=9Dg<^X0H;FzUf zKL-?-0gf5&8|Hv_o(w>cV}^V5;^9lzUa|S%wXMNr`SRAK_g;GAr9JTE|E7zFhyUlH ze(=EqADrI*)eGYbU$g%k`w#BFYVSjPk6iwty=%MwdH3GkM|a-6^NF3c9cufBwzcic zTfemRn#~U%zI5}(W_sge8{fH60DS;{a9v%$xb~B4-uy`wd~5vkeP;d2m3?Idz6?fy zyScszT5fuFqR<<M7L<$Mcvb=qV6$wU$ADFqzK zruDH!cw>t(M-64#FUfY+YLgUoMOFDRSJ&BW*A7QKDDdPj@XX)nJw>@x3Pq4|&mE8p zWLN4{D@RSnP1>2X+jXbMa|;yQ4L4FwMD%nL9N4rbK?my4k^{a|;oLz2JIeyVbbOgupcMZCTs`U;n~>4B8b8$vdl z6)|vldx3(x(TS9UV=N{^vBALi!71K6r52GqWK-f$0jCHXNRr?S-~>3P1jhJE(Fu5= zRmhvPZc;cX6d*0|%-=AdsvMev)8k2(IW}@mc~Hz%EMsW5iX`q3-9`W4Zdj3W{a9w$ zE=`K9vSKM_K#jUxiX5n;K!!T3++xV7J0YtbLoY&CxR4P|b21QIkrA_H-{>DxPJgO$ zNCJk(yvr|o)D?Bg(`>#%@t|tB-(e`GxWI_JVMNNcu#q$(B@-QFy2DXE6AQgYLeTs~ z)5UDOmEgNF;iyHM?5D4ka*{hP^5gszoGbOR)5)S#?nZOQQ>L>Gq9S7=(&5BJqCSy2 zMb?}ulL?=2npQK1xC6{=;%fRzXJ8qv8aNnD3_94*9r@ugJ=KV3x|~$WbYZMoYOR%3 z;tdGtj{C|4jmJg=Sx`Bc4mx&P8TQ4qThnqk8WGPsS|5F1tQTaW5yoR%E%nC^i*}L)Tn&y+V78e#!76bl*$kE zTEm%Q{J78=3TmBlhT}exOk$$k5F|(*lUNSDVo79}6~@I@Fm(rg!489CJl`1cti&BG zU#&ImqLdB|LQg79F-6Bqrf-=Q32RkSO^)(OU!IJwXoU`_@M7y>E(hvz`2MtTjOTq3 zPg!!zenrJ|um~ZO))15|5ZMkjfW{38K@uXD%!HQLtC9o%N_%9~D)p*Nm+6qjjb`Qe zwbvt_)+F0aVQewov9KU4OM{Z(=FLtGBfFFp z;PT=$!QD_Io?NdOLx^U(Z=sr8P8aHaxfkQae@d<+%8FG4yoJCq|(o&IW|XK z;VZ3HtK(FOreuO?%eUj3sA?`$4`hFZLqhZd48z1`FRm|&s#Kq! zGG#(Ai#?v{RRZXWJ7FgNWZJb=XWHrxh}`151#V=hJ90C_-N;AEk<)UiD_}&@PqyQE zXF5(41bi~Bm3hgn=P?T7s|nv?B`n{$Vwg?K&hF@l*vz2Z?Ez?Ldg^)u%Z=W|7$|6|PACgelTbF|S$8w29}?x%0I4>KP?@&qLcbZviTD&TTlHyE&3DL-J)sNZE0mx+RnBXd zT(#08spfdmfw&uQhx~k}BsVn)c z()Xq|Y1Hb0i4|14eauF$k9Znl-P8sHt*tZ}Cq1r@Sfz%51NDY8a5YWvE1>v5F=6lx zj<`}+%OFitHG?;E@}w0s7w;dq8{ZJ|oDw-HW9N-x8&u&0#T{Y??ib9B%?yJ&l~|Nlj6#M7w+1B^WJany<+#FonPB|_4cD%AKkil^V>K6c7tAj_uAh9z*9dr zUi$$bH{k`?=?fdt3u7ZMTW@DP5p;RVDNhL2S(jkT?N|RDvZdn2?rP~=C zxJE2pa_)(0Jl|v4J}BuNHb%B@H|?%H49~tP*#UbUJ~-Npevbd<Lx{F^OW$_Sb{m7B zYxrmDxgBZfM?2R(IDGZWdcNlE8XamW#7W zPe-z$0fTBc&_J6$KCW7Hz}?o=ZbJpxeg$Q>A~n5li+=X0m6d(v+t(vyktdbiyh_)2 zr4Bj`Va-fj1yxCyg4f`fb_SGH)Vl;Iz7>R%l#%CCdGXeudcCnRXmpK{(Op5)nc*EI z(zN%Sl{LNV?Q4;yuX<9`jjNdC6nlthpgw7Rnm>KRg84Aelq%Lzj$R0uN*xbjpA#b9K{Rg zc{q4t6n!5Q5kLD;;XfRVTzvO^@7X%;N7_9wp8fpIWjZdtguQX)=*wt1*3%UWm~&5}xXtF?id zD!2W>cb&S7xSm?e^UkEl>5P?xiCapZFb|!n?`$3t|JO+0+HbF{@71F)()YG`NUqxw z-^i%(kjxX=JnIhyg6!6-sxYedtufD1j+cas(}L>X()TIz(3yJ9<{|!9BR#82mI=Ta z#KrmYHAj<3&)epq{zx%LjLX#7W?ZptIpz+63LTe6iEO*3SLv>o!#bi}kc#LnJs0Pp zGnJjqLzNdt%2qzS@}R!*XdEef+dL%ah8Y2s`G+hna`$S$%i3rXuLqUd)UQjeky|$U3CbcR26A&0JVkU0@-4j~aJPo>@$7k~ zlC$i2@1v2DU$<;QGrhQEdgHZ6gGkBSvZvJx$SEjCXS9<+QR(y(tfScoY>!5mTqMu| zLnpOFKys%3_+9Y{+4D?&XW29Uj!56c?=3O>aec2I^&@?6%brw{)(aS@l$J0(iID86 zJZw%$B<8b|4k;^2GDzg)OiS_xkNf6Rvget4&ax*o8&~-2R@U=1M_#1oZP}9%JJ|$+ zhy9WW_rlSnX~3+jwL=UY4-F*OXu^2F26c1jryq}@g}eAOm7QhJd#*%-de85!tn4d~ zdXciXWzUHnl7&&I6Xej2GnPf;*=))R5x7$*R%ElnqGBQ!1AUT+$D`@C?0Kf9v+Q{- zIwtYK*Dl-qaoqfO9l4RFw`I?BG&H+Cd!DJ|EPKA%k92(X2UZ@At(Ko+Hd=t%%OyNi+I$1jor8QO=QEPwI$rd}fH$ znzi?u^$fiG?D35NUwFU06BGe`#C%HyC2nWkKy*+-)dKykqTW(xG&+eidU_2^Qcwbh z5CA{1HPP_8Ww-lkz);E2By{`bZa2a6aW|>aev8+V{&1r5Zp~qP=+kasa$RW}U2QNe z`GiwR8(P&KmeSX1!xHC6L_1#quVqoc*Vo4UbtyG&>C+HY@7GC{h)~}w!yUuo$RRa)Sz7h)!M6t z;2P-s$hI&RGTdhA>38QtSnQhU2}qY95<1$>UYk@XhP4C-Hep^aCfn7C*c#i{GIpkJ zT~Es*Y^#=w8>Zi!hOc@}P$n zP&4XMsSbmI;m<%#4)8@M7&XLs%QG$TCRYRy!ZAn-|w#r$+T?|GBynZQWLxjqX@&U*kpsrXE{rW*C_o&DOewa(+egPUzti>_|= zjqQ%?VRSjO0eJidJi0ypBHjof@kG?L^28f(Ha|d{!PLjfm3*$%MGVymO+l`L4rENq zxn3l>WRJ|(iR-1OpKiv3k(3EK>~&fl)0{P^CwZ#kr*O^?xpAk!XhNaWS0~Bqy=$rX zB#|*QLl}3;6ERN_WwBuCbrI@j^F6;<=0eU5A*wW7u}yDu=~4f`!#`eQ)_!^I;`s0- z2Y+_(69;cO;1BM)@W~55bK&TMeBqV*f4l$w{dev^aFN)5?cQhietGX-AHH`_-;3|9 z@BYT_yLUUgncdx;53l|5&iC&0ciymbY5VuV$^XfAe*1Y_f4ueHt%tXmt(R>6+2&7d zzGai&yldl=8$WaS@W#=Gyz$EQzrFnH>+fHG=lTQduRUbeK70Aymphj;mv=9H_|o@Y z>R)>OrHdDT5A+#)9#C+F&#YHM3rXN%)1SnB&g&P%2FFTvwu}>yOERetN1TzD&KDC< zO4km*c8-|lq_iPxwpjB`u5JrxGn|enlguRsaR@OAGAdTJAXH#;Q<)p^g`C#b zdtq8+L3b;9sIY@t5l>I)VHi|*KGzc3tEVE ziO@`Jo;78DD59^^A{*h&%`^9DdIdzb=^yIBQozhQkj<@sHqMO!ij9zYFf1{oL0k8TrNz8;RHo` zpz~FslR}gr4Q+gGj?hM)QR~FxEqS0|sai>7)EWZ1X3;V#x9j5pT5aoT)Mr8Ss(a=L z)z8F)z>`9z3MS&txY8PyQ+}yb%fNo4%(J3Ebq4XWg^QbiJx8p*#dYhWbA*(ZJ4!mE zd$Jv`5NSVA=13lur)dJu0n1g|7MjEpQ>5J4nYWK=)ykM#CrhPKkW8skx>e%wYD^=t z{bZmHZ3)er)i9kLGp4`wnz;;7bxJrW{FbbCU6^9oiq{hQh?vOx@sSUgG|Lu-2$C$r z(AKTMQ3Xy-CbX`ousQ4uE8IZxy=uOnC{o>^T8-PK-FXAR7TA#8&Wt+w zp-`B#`&pyej1hH^Im?ik_GmlVD`8ZNatge%b?;m&nuuGlmhX5(a@fNMnj@w z>7BzlqM(q?uoU;5akZAgVsUMpN%X5AjbhVOwx(BHTFQ&9SQ*alHEtomy9W#l?4r!r zxWTl>BS{`;4AaR&tR3SUY@(y|CIhhNY`ri?EQ97UOv;(WeR{c%rhHs3##w;-->bZgWmffpxs8n>&~1h-}NsMpB4AG`-eP zRw!pub9-FFoCw`mhPWW#LSU{|A84Ek8~;Ch?;hm1cHRee_w;jm9+G9fELqaM8m(8E za2mve1RcvZpCCvABuEe>c<+b;KoAdt1PKrXK}mCcThH#XBKa|L{McCDT33>-b)3y6 zwkzJ0<7|18+Ks)=I$M#meyumc4t=aK7Jn z9^ZL<=X}4f9BC(x14l|tCSSJf>P!JrcJG8wHt&ds z6Hkg|qdFyxXh*Lq1nv=DD$ZPa-=ah$i)Ix=A4{>sq|AYYM|5S>%GP6qE;pnxW>3hD zQ^6w~3T+qXCFCh(@`{#{9b2=Dl@tSR$Py&%brd{}DY{0b4F#o=Q>xwD{rN=+hp-SX zYbp_)(3=fsAl3y2Dr(az1L1tOipFrEl^iHoS=oEAC}HD*Cd17hmx-7lso#L8MhqQ1 z)uvJ?Cu?e{U}1T;H%S!Ty+wMZ5Sg_%e}3M9@&IAeiK3ao%xJzWG^!wpXuSj4JV`5h zeu&cJyxS@|tpX!`XrYv$AhQBeFgsJPo2iw_0pvDhwZ}kGyWkMX@vvPT7)C$Rv3IOR z3B4yvttK(-l=Qe>hq6*E>cqLCqZ!SLs8lPw3r`)hlE#?rA6%4Bxtc)Gf=jgq$-!8O z4aDjMq`l46P%oDmmlBm0DcNRfG{V>S7bTP)mUioIx>avO;FYn%O~zs}!$DJarj#dM zl8;geTEGyMy1JOzN7^V^uPZUiRdl%?&on^lU$$Q^*k}SHXt`KmC%v6Tg0m2BZa2$|deqcXnqU~R zTAWa6tf-0B$SQP)B5%6ovQmjgO_CtuY-B9$9iEg(QrfhXfUT%et50hs76kRQ(5^Ay zWWE;VvSpTy&+v&}WVRM7t<1z@X%NvM7c-LQq%t6LAxO*I#3awMOO1p~HezusHXYa7 zy`9YkMx{-9rAfWp0%;k!8CPM56s(Ov5Q4*GtMytdPipNX5{t|B-uEp^l#N+s80(fR zon|^4>9;Ebx|jq{cbk&biYt_7LP=ey4zVXT#7^P;$)O%3M9Cy2Dv0ZDYEFyN0c%s2>BT!y4bQK>QP8O0?dlHWmp><(y%=>E5v2GrKa6l|XVjvocAwM@Uk} zz@jF$y?C_2I~WFSpR7>R0o*F1mRSNh$=rq#P4cX%fbZqQTV56rJkBAT$fCdW23^mHowTSJo*TYm->t0EvDJ zxY7`$TG|=K8!bzyS243@w~)2~9;dkI{;z|>gB0KZ@wI%iXxz32-oNEzW;-fQ5Ud(g zBo=b&ovc@JBMv?pmjuYMJH<)Yy@oGJq*A<5q`)4LA51E6qV9?Pa<+)2z=Tki#x699 zjzGd%L>VR5{>i+AQUnP{$Nerxxduor?76N3o+~zW9HhT)m&#tJP|75!@ks7MSH5vk zf?%i9NTbNlV8fZk5fG2pPfaurcSg2FnUbx7?J3FF25oLEjzmg=N21oOJ&cMLCd(1K z!IMRV>vY?qBS8soR`sY%ztEfvvg<#$z=(rPf889MkjLp6l>{$nd47_v4sw-Pa|F%W zxlD}dd6kjI)i>ou2~1aNcuE!r8rn~a4KV>iMZ0RMthH>BLJF{zfNL``1!uBb)sqs5 zRC_$2MmYk!;K)rX!#pZv1ThsuB3U@zc3RS`Zq%S&B)R_MixOz9Rxu+DsDu|xN9l6W zB5O-^n9uT+RE#Y3MJQ7$Po<%W?tW}hA}TXIwm3BS7SfoZCX$0*x@If=2trVQZ4S}xHHtVgp5}6KwdlCRsit7G(DbTOwy5fEzN70} z4ys0aBEP@u^Z)Iy{n*-#AG(h2|I$@;Z{tdT^H(-@wi7!)3jX~1{~UI=TZ-VI<#eug9m3E=zo@(W9y{MI!$!OkCtvtJ=Rwjm66KRe3M%@MHgc`-Qx|IgG zs+F4qui9Ya5~g$2TrF9_y;MENRj6S;moLW(DqFuU*v2%M3nmq*gAo zm(=3VAH`O33g!c7{dr;Efzt-MT@^@eKY%i3UMB(&Z)Tn1L6b)7qC0Dg_hqi|; z28}1cuPLEt*z zji9T2RcA7VR*e{A0@Pd8%HDVgvGF2AjOmhRS(8bTRjFn#lO9C%R?H<6gQ4C}OB9qo zy}^iI)XJ~fto#%+26b681~DEM^n%wk65UqM)x~BOV+PWyRyG)l%d$x_QE)Ax5`iSM z+KmnBi72CRE`e~ZqFdpjjBJ{xm%i9Vt^5W)_)RbE;Flc|opcKmIR!4~FpAF7c8jgA zYGtA`s8yP3NAe~rtXqRlUL^DyL(o!nLZ{n$CmBVhQW_Jcr^hnppZ~9JZmex??ELVy zx`o~N#p^$GP1#4UuJ8WR>wozFx&~iwfv>l~vu%OH=;kM-jW^CtfOP50+Hd8CC80Ov z8(h6#%?^k1pfIKSb9bJ%35-X@de4>sCcwop|kB6q<}<5UNMJ)YzV= zdBt4&@*A+pF3WF_9;;FV%XHwXSsPR%wH#(J$Vwj=C2$OB@Y5cqWlCe^iq{7lAQ_lDJo}H?F3VtlZ(-*u|~~cG22OL(|mb6iK22A5_qWu*bh@F zrB?<-m?UGa{a06)@-&2vUTis>%yRHH*t!i)68 z7-Z019i64(F@;2$6Orq;m3TEmSV?p3x88tFc3Fm*Gr+TA5|R4QtOn5z1>E17E7#ZvZUL#*R=v!9}2gIkVF7rVLmLL6iOZ z8?MRROLLnOcwdTUVX2PCYfzz5o!O&mo>?)<(pkh94x0E-%)rGoL`iP5R0r+txJFOx zPm5WzOV_*_sSi3}`u~kLV3S?iC<8A7qJ5J{5vtweKCpi_I`Ws{vNvf>${)cJ=pp3&dWQ0W&2mQKfC>* zt^cs~x3*pY?EgQrDQsT7@;_X8@ygc5-`x;5-nRa~tlwLQ*M0|x5BkI2|1UN-704%STt_hL}r=Elf3`_xvc*xoUB-7BIV^Jk|y&0^K)505=Isl z#hSK7i)Um#H<$IpVPv6Ev}e%$+8J5zo6Gv4FtU&+QZ9B=%QUy_{r3V{^41Rq7#^Zo z>-O{A(g5tNvy>>8<5NA)We6ymFA4)qA2bjT?A1$H-em8;XD;?{1Q1*CUw%sI${?2$ z?WRk?V`bpdxKKuQ5Hx2mHycsa|{b%bw4 z#+kU#Gct9nWs>aucg@8<2q3o6XL%3?JuF&AU+l*Ep51Lv`Vuxs(?(Omn&gZvi(?G1 zO@p@mrn%VfznHW<3QJ)VZ3c;xqo$H(t^^J2;e?5#SIG)Ycsby~4{QbAgMSU)esAx>+ zYt`k+i@pC1K$N`o-2sLM(w(OCQco9h9dq0fO=>WiXfd>J8V*nNv(gk?a5z0NLX1rVa%RzM6XRI2Hz?0=IoUzcc#W7+2;XDxziBS^IDpua zjcH0(!D|?&6L}@#VPcgmQ=qwsf zO6621A8AIL%@j_`I#@2iMGP3Xp9~;21i3PMe{U}K69L4A;6P^Y@6N>@1`r#9vY5TU zGZ*`E0I?y+h1vVtbFtqQKx_ymVD|pjTNoM5+c*BzjsN<_@W!{_xVG_u>;LBZ-@ZP%p1J-F*M95TPhR`@wc@pR z?*F^}f4KjNeFkI-{L0mzy?S)DarFax|8ei<_ddUOcklM@f7<_%hf7k7SmN85?-Yyl6#k8PXVAKKpE`c>dXaJJH0Zw8qLeq#L()_-pObL%gxe{k)O z)_x&?^We(w2KeafpFCv?%)?4FomBc|m9AABvh9_nNYa|2vMsyaUa@atQiN?3+Uev# zK+S94F)yKWX_GYK5xLE=6PAUtExlVXKo&7>(C?2bb)&6#dXsO(VD#FzElQMTDJ&0i zrPvcUhYtEwmCaNr5Xjb1o$4$`fj9OAUdXl8DRXTRvkM-EN{txYZf0PdmYdmLtla2y z5{O(EBdqIINL;q}>ouq0Vxq)2b=^-@Cx52obk{^AoP!Bk)oO%Oa%$ zULWF;t7%Cc8o}C-hY*1x(aQS|cFSxZWc<6O;(St>{bSc$dU8>Mgqvcm znu>uWO$1Q|TjE@sPe8Ge?g?nS=(VtVJsY#gY?HZiWlIYbW;Gme!INTLGw3gaS`?2s5m=xA+>bW`1wW!F5oT4PInW_J;#)q|R&P7Iy8 z4UF@=HffY*BSlG@Fd}DWwI$9NBYr79*s|?N?M&oNI4Zl$Jt1uL2c-Z62r3H@uEdW z9^$y|)d~b`lTE8d5^#r2Ylf!Gb06@7ge$stJJDqyqM$hom&g76l0o3$5^807)ox^@G4a;-NOr| zaj^|Wu-w=jav;iiMq}>w|F|e27+lBI$>>y3g)|^PXe+U3KAyp7JeJj9LF%;`bzmt) z(%k=zMTyqTD-2r&PniuVf`WiU4z3M*Od3=h6Ef+Nq2Q5Grz;HWTmSl`giAFBEE*fv zD-xnKDAyE0Fg2esD(#vZr)usfo+{JmBnNGL^GONO>y&uCJV~O+P|f2^Qtn7Cl^!uD zYGjEKn@;D8B`n7E)?Zna;M-k|6Jq%|NPU^=d3o?YRUle?Cdw4FQIBn+q|76;b}U}s z``e2W9K$B^#TM}Fl2o7PXH*J&Rm?;{nzepZ%c>J{sB5YE%p$jcVNs$!rMZSd#L^{w z22mtcOZUf-NJ8oqn#v&G1)(7E0;|Eb33MgDD8b^IH`YBWItEEQd5fm)ffHju5`}mn znk-g?;j~lboRXGCx9CNQDuTyTo%UEZ>oVzzc@@RL3}2B36q)HZ5V8wwu&R}TZ*}iv z?9#l4;A&*Xi@2h|mP2#WNQMehqTQTE<02mGS}+%@4JygCMcBMv%5<|pK+s;1R;lejUT9SWk9=n_Ju-=mTP4BiLR_lNcLBAJn<`G} zVkJIliB=OGbfqgNBeBRCe72ysM^K|+#L~7nnAOTPRU0a0(gm~|3@^JH+rw36e-Z1d z7Yrjg9VaQvOvii8;Rq@f`{@?ATh+?xoT6%3xUFCwBsP^DWT6!~E);4Vw+Utrx@YsX zj*wxHPN6>|s7@(^+cTun;*_G6QT7)UDmpnLWg#(Wc51Q%0!d}e0*`EEX#l2eHwF^g zj9@v}C}?Ds*^DhP2t6fXTD%>nXBM5v^zydOcr~$Z{v5#Dw zuu5^qrUnJJQiTjkDx(p=LxtvYjcQT>d>uAhO5ymF-2U?gM)33?oBzj2iBcmujkVKd zZQ|xr?m!oZ{WMw_c62NW<%)y)bWnp8R%$U#Aomr1R>Hhz?9Qz6lPSulQ7Z(6R&rfiBPZ4#xR*?v-p(X zW)~$ey;Lk{#c7irl)Bi!b%@b0XF*+_K}J>87OL@1L{CE|#%v82B@i?@%*sP58!g1( znix0L2t7&WsX>ntEC^&b6lS${qvzD5{jEg_sNxg=1(rU_mbqxMKssa}2S!`LL6mm0 z=p~G-T(&2AHGyt_YFG-cC=VZhJ^1(@d6Z!o56VjcRbEm*dy} z=3>h*OXIR)l4M;e;p5Q^r1MOIpxg{y8!?5d0@0GJ8eT1)s7ecNC2gX`D^-Ijq$8*} z9H-H;Hp_$f`SgsX3CD=!XGKdu${v%3wtse^lr0R$RhAvJB6(3KX$;9?kw!E%Fnhyf z6+)Cji51F)n#lAv|G}b!l{V7Zwo0@jFw-2D)3H{2GMX7wEK4Va!MKTev&InOQH{Cs zzb#4(OO1-jCt`GBoXgq~NFmr6KdFH<1rorRv}b_k*%=iEq;&Nc7bVPQx!Isga$A?W z$)P^dnjob`m&|30OuL0lTWQfF&|$JYG`AKe2sZ6~SI{d8Mhr+YCk!)WUn12$jW-5) zVwh8rO3wsw$;m{0ZDAgoRwzeQB$CG9454~zd6-RFHO95ulbNCb#()CIH^uOPY6RW- zwS`t)7-YrjN(ILjV3le)&3Xcw_1c09^>LPqBxoj!j|9GytZ#pMQKBBB$;tc5p-Z2#*|n>!RpPZrCJOjL+o`^5XVik#vu0_PM6aTY60LMPUMVM=rACoz5(O3S_q)wWB-)1BIdVWC zm70vedZeV+*MD)e##O6ELaNbLv|f-gaagRtlngh(K5kTG-R%$0`TsZGwRY{r-NUU1;Fque ze7yx0E%52du`}mp_ttNP@YY@IqHj%;ZI1<-&-kp#Yqi8h3T zZ*>xbumYBtWf$3&mN{iF%xvTrxG-_SnZ#RuDajm+L5>K}B!Ku;_Vdu$V7CeW`Ml4f z?tjtI?4coeCv>pS`*~u{usVaJUjC;G4%fD089>0mF$|x{^�R41T|WY;|=U^4W}G zKGsuaMTkPNgr7HKB`Iq(m8`T5>h)*=1g+QLX5n@=-ybo6KVD|WH8a6;Vjj=jmg7nC zsX3tGa-oyQM7ftpQQ2N~)a3irZAaBL1IzhX_1na4FPoNPUBKHp?q^J}J&bm5XSY9|uPcha^WCCSHm{%Q;vVKZC9{ZMe)Ly?8Vz^$O<;Blh-ra|T z<5eEw7yLghS;r#bq~H6e(|Wu{)-i{i0Q=BPpK$nj)YoIKk!kt=4m@~I8%6YCz30@% zK+L?_3!dYi?D=xPhQSUc@VK93WaVI|XhDFwagV;O>rFMs3L_&W0wPL`C?*EDsnWMS zx>{%)r0UR94|b|fQ;kNsA|q3@RwdPVYHHW?;Xyxr03o$;v}^Pskm{@4z3tYlgU*a^ z5pl{+Ru1yDN-kdIvYf}|70%LSDL1U-T6`A7ku!l(5%^X2QxR~-wm;ZE2T~D)NZWSt zHOS-34CM35uUuwgX9xSU&&3lW*~#iMm5M**G6vFKLw>S^m6kmlnKh7avQdIWCdNf=tz(0itZ7<~k#Zwu z8v<%glfb7tIG~=|vPD3V6UQ2&*r}Dwj>%N(>?DpGF zL`K53-Qw-I(@c)tpPOY1dp#^WZ03UbP<^zhJ|89_*(%FE zNfPOs$>}xtl1zJ<2yEH3AEJYGGH`y8yQkM%pO60n>s`v^0SY{|je=7KD8AB0vogzJ zhz8PQCG)BWrR=U%V>!5+;RIy>2$7{xu{mkjdOY^@iGTo6b-i4=U7BVpBq|ofn%No| z@UVBrTiqSTdW(Q17-4vxS6g(ORh$^@@#P zTTLhqst#<_KmR|!_A_fYM>qFxeC)=XuK(Ee__bfSR@wjg{THtOi>n{m`-8pD?7emO zOS^C0Iox^o_UE>r+xqFP?B=g+9&WB(`O=l|-1yav*~X3apI9e=kk|0%<^AL5w>Ow? zxhrqI=i&#;kL7wiZyUHw^m8l3<7w&mIbWOW^42#8)@HF+z7^sv^@oq&2T+jf<*j#L z{7iO5MvzBOmg7-)I87Cn-`8u$@0|n4TkpE~f#)iKZg8IU7mwcquns_FzA12JUd8f|@jNqHW^#I@!RnXa zjpKLv8eFHg-Wphg6VrLP;XqbD1n(Zd14xblqx{Ceqj_xhMSE-x*j+k)i?0o+`pv-FoQ%v6Z{I(5G^5O3|R<2M0qKKMa->snxKPWGUWZxe5Yc!_Tw-vpq(J?{qwU18559_cH`Hvpn< z&sPH@p0ei<@Ai%3>i{VMkoE#2E#(Ef72-X=c6<%M`P0X4V7SMoj}VXi!O-0YVE**6 z6Bz8t)5kJl-mMV90n+hRUmH;M?ZDccOdla)1wf_uPAa{1u>&~J^s&recPm6NfOWhJ zwDIkAGq5(R?KQ-ke)D(-fco}&B{1j;dkyj2Upd|eh`zmU1V%h%uOT7;jN>hU3GA}=DNK3 z=Yh33vDXlnG@#P!CzbyHF7|WL z?K}7X`F`cb+8w;?8Z+3b=%adckjX*T)C8}arkarx`yhQg>*Mmyp&SVJj3F&>E7Py@txF8wt(zb1Z3sN=A zvd#$2kZyEf!3s3N^Nl-QuLJVWl`|Gwl2S0EmWI=sHHkF=K2b#d`m)ZU6bN_OuX$^M zJ9ACspkXImx&t!f*ok5i*7-XFF>i?&YsBbUQRlLCwq<1xGE6FWN2_4NBEY4cLopEU zG8z2|u4cz#v!R}=ikOI(SSHb$@pY#ufE-Om)5r|@P6_4NhLi4Kh?6l6IMp5y05|U( z3W0E!Qw^NpN|Sgt$)*fV$#Hr|Z5_ynq{-1JToj0GY+W}{@eT1Ds<2l7!t6IagX#S)ZI)RAzfnDIL9NUmcI z^PuYE63$^e5bhbT!~z#GDpdvRquhZ;6C?>2N}X2H(a<|)MQQa-0bIEDRi$X9>UShg zA&on5BkKU%oOAf$K)C138$6=U01J*wP>p`NldGgVatv`;x;~ZS+|(lLUFskKUV`z% zeiEtYt6r4C{Qmt`=kRVI+;irQu08B~Q^b?;Y9G7P2U*nWMb=5eBLqj>W-W^3N<0O- zirk}$Qa9agRPCy7zX#6Yi-B;@nKxj3P|g*tSP4T+u0P6U_3kCjI9fTE(=L06*;zrFys6$tm7d81EFTS%+ba+o|~D}clZM6jsNq+*p789ZAt zGGJRd8zDoHI3aNpw`HUi1~Q^VJu)NbDyji6r&|8O*jNnWwKqgqQn3vc%|E`4hvk|s{!1vIES@BxaZ7Q%~sM)+|kS#GD&Gj zfiBnzk#O#i6nMp3H#22al_$MnOzfd8g3e^Y){7s4arMe?I)~LjxaZ7Ql_8s%dQ}3% zcJLsP-XxJo&_107R#q(|UY>Oeci8gSn#S<>pg~p2`H7JM`^i^+**Rna;KGS><|{Ce z5u@or9(W5~q$HgtbM92GVDKQ`2f5NrCN7QHQfn{;nHPpk15J1o1lGY<{;_jd350vj ze8pzeiK*nRggYBGo!KF_j6I;0m;ok4@MzF`Lz7swY;^u_WEO6CXPx+A>7fu;K+THpLoQ z3Cvfo96N`lK)C13S5%>76k=XfG+GI&Hq+v=BTmbClC2J;__XL`26xaquGA{1>|~6^ zRbEQ<{CVS^b65<7d(M1CpgM=*4RfS|gr{~6>|V_tAy@5uhtzquTyWKr7fpe*^{Uj& z3MqX8^5uc`_mzorSO|oB&V1!Ip-Hrxq&SSmYN{o92FMM32V@tfvpKFPOU2=|K~24} zI~L2#RK{>Pg@S$iD~@wW2EsjOzTzFY#N_01P8{p;G|RCWEzvBGvN)q-JcXs$4kw7h zolGHFs8+;iWq4RT@XNe5Jy9cYL;Hv~GLW2yqy_9?K+*5;v)N zAa+}95_~kL(k^9j@fHX6v9B2Z`TxCZKfQMI-i<%IF}wc9*C*Hh@S3&%`}>2dzjsv! zr~mHmzugsgerxB$+rPHm+WPVqxA}YE?*AQEe&R}U)1=FfR&%XfRx85aOLenAZkDG%yz$gt+z(bFX^wt=mgS zgM|hmZfkFaxM#gs1MR54RW5^(`08c!IS&#}f3Wf?!Ns@V>XETkrXI&yDe4i{>Q@C9 zLn%kSE$T6-)sh||t$tq+1W8{)S}o}j(&~2vL6Ed1q}7rhA+3H_a52(LN9tl&h8SwM zLR`vUJS<;2QWhG7ka{b`RecZ*x(f|LNWB%}c0Py(@#2hrfe3k^a@4RhTdM1#8v4MJSv-wJURA4G!}7aHK@n2^T_S5O76 zo8;j(gc$CKpQ3PNlFbtW6D>GB4d=(BdYIeyi|;L8I(lKDL5Ms3TOnBcf@siMXb?i` ztq_j^f@r`mGzcN}R*1&|K{RMCGzcN}R)|LfK{RMAGzcN}R)}{5K{VhN8ibH~E5z%9 zAR5$9+8{MfuxN9{*m9P`(&b)E&@&v3i$DKd>9w`JZ<9 z?N_!xwY|CZiLH&zm%;o0mp1-<ix;6okYLmYH3KD^%h z;%;aKLcG-vqQFjQ1wuF*f+(;(S0IGY%k^N~`{Gt;1unS@Hs=b27+9C!B40aiR>;0&8;x!g!D_w+kM;6k37H?ScpQLMw2o zU7$Sp_$KgcJ@UI;=t5iVxVhLdqdfRnNTjEA0VxkY8WL%>+bc+&X3IzyI0c`+^}LWs zPv3gekVsG4dff-^CUA5<8hg*m&)I!22?zAFz0`f+gadloPU=1wKMu6I4<0q`)7Quj z2lVtcvK|Lot?{E8JuT3^2c!9v8^%9;$&1b~v;vpB=$N4uxYR}G-h;thfiMp8OYVYx zXaz303yjbTTyhum<_d%mdbu|Q_a5k>6}aRs(B=w+7+9BkL*bwQH`l(pw*Nglf3*1$ z_~k!~pU-R^dvj)zx36EWWAAa!q4TKiPzYYdOcKJm<-X(3sBmeCL{5wxh^0h@vHEZ^dnIeuyBClE9pn*5-U#@>U zs3AuGQW|0im-#9~d=!cJIvL=r3izXN=!k&7@iDu@o~_*>2<9WKw7c6g-kEjF5Yf;D z%q$6Ft;`LelH(YIdLFW2X& z0ab(vrPGL-9M+(8c?c+)dq^|YE!H!b=oMU6F6AK)$tRt0MgSzjiBeKe)9GXoYObhX zb)TB+ZO86>0R*IbAqLaMHyT0h4*E-JcVSlOyI_PZvt#tuWjY+!ir z9|InmGfc7Q^3&GGEYqj7dxk3_9E+W6nNFozQSeR+!2vy8tYjMU0W%oYCI^^;54yJ} zCYI)Dw@|MX%%@%vCA>bym3$u25*%~e?o7>|HF4@Kkc_7{o>3Ligk~eVYR5ATx?hMI z3C^}TxQ|8iNSYmk%qtnFM=%pR)@h^|z{#DF@oq3W%&1>=V40uo9Z!7A{JaEK#NhXm zmt~nqBC*OcLj*YcqdLqj>FTX!*uCnKrJW8*f1S+Cu>lk9eB-q`mU#tfGGM)iAb&GA zFWc_OUBNMY=4YQG`&EiSKm&cp-!$JjA$-RC|2_HBWA&bkd-Q)RcfA1c(HBd+0RZ^4 z>;>uO{Exnl3w@T9T=3#PQhZ)s6nr|~`L8`q>OK4&Y_;K*L?6rS*Ex-Te z{r=TIx%$byKitE2|K)CS=U?q;+h5)O;qA9?{nS={^DCR5+kF3(qbtvEeDB5w*8dS; z|NHlA-Sb@sFRvesc6_FX^4eB_obo|f4-#i0Y#u(qM$bt^&mRqa9j*f%0_37U(P4>c z3_T|?{qV>HvH=6hT9DlIC$i5P(!$HfCLmIYg+;G)}KJWy79KdJiO44`Tz=$ zf~;)?$Yp;5b$Tia6MFITwIjptr@H_xK#Kblv?tSooEa-2F8n)3JzpKa@((V4Q*ols z$*>9WSmEWj9qEfkmbbqD;#=!gjU46?K?s+ta-;#p0FB<-*7pTg?5TM4?jETC2~a|= zZGG>>_ns$BaB4_DoB@z??vLdo1;_`SkZW7t6IlKVD?Np?@}~8}N8Nc>QfpgZ2#gbk z74pMJ@N~ra*N!>>0q{buZTahBjt$8H*DqIID zd@isGOY`+P%#gz)5s02o*WVp@NUsALNAlRsS_1x5=wQbJshbdEmktK-jBpAD?e$*2i&wevE5bazp0dFwM5 zKl~1~N}Y+JeMV6upvzu-*kzx7R=S6XH?fai0O|nFxV5cM1y*M@XRdSVYNSg zvBM|OoU`og3lnW(AJu?5bG!X$V0Bj8ZHP#W;ZYT!&h2&<7F>IEUb+2#oh=Mgg{@ue7I2|KrQokKP5U z`1@#xh(~V&!cR|rK=?{;MPT7)$NLLMZ{4OIoBWW~&!+=poSyq$IQqtA>8rgJ zfu%n-^{pSh1&BXA^&zVr5rM@&c{DHkKm5L`AAN(b!>O6?9FIj{9Ue}6Zw4dS@BI)i ziohfI>_Cu@-URUcp&Y_b5g6~$s8){N32^+?HiWY?FwSGc91wwVmd+r`(GCFf z4K>8MJ}}slp>6{x-%vvw*#kp8dCV?*4gA&?j<$S#K-EK>?E~v`VyM@THcu)Yg2*B8 zf%3_fUiKOIl^z~l0s8oM8saD)Sf6w3bOS*8b{gU+9~g3#ovs5s-%dlE=L6%Nw$n9$ zy$9+%!r>YDvM;uqdk>V*3S6q;-h0put-z&Dn)e>aa|J@U zBfw>MK{K=hm)r%7 z&?8Lo0B}T|kFc;F7z5nkx|E_rn1UyW4&}wAXXsc?ilotCbIx%1fflb5>wW$`yT)W|}3V-g)wk;^~ z>l7CznjH@`{_}a+FwFOf0tko#z0T=$rXXBurmUkeAV>rNE8UGyb z3XMs}HizRlTSZ*4tAki&i%lq{oH%Jt%46doo*9)>rkj20u%|RQ~#vzI>luBqpvxds zJjJV-VDZm>)&2Nq?)YPVl;!6@{Bwv+U}#J9=^a)Y9`et`Cwh-f$w^*>$2 zKeipq00IV%Vfayh4+9nbFv7D2x(J8t$#a80FAQ{*;!kba2Q zC%nE3r?chMsK_?*Xcf;^iy6CXPYN)X9wDq}a!oDWd+J5DVD$2nC{(q)`a#+5CaobK zA7afEH)4@es+uAUrdgYDLbnsm5&3jx;Iw8`Gge|aSkE(gDm9stROIQ(59l!$ba38eWLw_$tFYxhwZ)qt}62>mmBLzVC&= z{33TxFQoC6M)=r5y2Nb-3OuciqKWh4|Gh$&>Cz1%Rhy+5u{j{d-B^aSqqx_qL>Ul% z-&e$j=qdTi(=V4zy^|S>wo|1XT0TfA)kLh9=pYP|oHA|}iC570Bs#_hBfDOen>nq2 zn|5zcIwp}D!yOkD%Eelmk3&_WMNQQj=3#?MD;K1T$wdGIt64Q?MpqIug}AEl=bbRl`uX=EE&^5`Ou{I5 z7ijqCtUYH^S$Y(+Ja_giRqk>F$pi=1^ms6_y`|U-zkn?dfM>-&Tb#GiUw$m+mpXky zna`cRfOCkM&XhkrZ$4XMCKyWK{)Ttu!9uJxv8;6975YfJhSi7V83h^9>cL=^QlY{u zY37<%eu6S3=IJTZ6kN=55T~d%r>77v4aGWfwIsroEUA|AEHdr%VK!w@*id&G6gG*c zAx)hnv$%=46D>DUvE=P^ZNgCGq|Rj2nW&{p879cVVqSGHQ+~F0f9)YLQ;6YqX_g7C zBxVY6FLybX7S>dkEba8m&DY7yE+%F=V`yI|W&+dFYe>ulL(%BDJa9Pc*yDT`VRbAk zbk$*_Il5ifaxOA8HA^ac2t%6a2+@KD)`}atBloj7a`M=%5;4TrWaUEN5~nt!-PYiO-es-oAe}PpQm&Y-l}fgh zPT#IJGKk$ttK}K_)NYGdsW4V_YT#l<4UnQ`%$}r-k`+^gNU(mlDw$E=zCE2*tnq-X z3$0-W_zc~0KZD{jp2pqE?O9rifEY=6!Y?3(Apk(zMSyk{b|s0;Jb~%|Qpo<&VjS zbJL}=IcPw*Z(yf{Up||Iy3Dt5sca6)vh+)2bNKjCcTVPTs)1fpGKcz+{EUbkI$-en z9sgPpIYx_}`7`V5PvjsReZNB(?RMjfa3?LU+OL8!vomkiBgm!9)S7V{!6`hIRRUmESdnypDaRLbOdg{C-W*S zUpoj#4PO^f?GRqmz`86Bx&HhAx2}En+Relb>-s;v_NUjrbKkrAkN3X1_pQ73&QEXu z{x-VR-~96CyRUrj#{ar;b3G3}eP%x|u!zldZT*cdo0Q zp$N46oHFQl&rODoHX)fzR`ndUTxtxJB}9MpuY4d1KwnNDw)fYRb4wG)a9tMvHn$=rbLKRw4sc5wlO*ph9VCjfSIAPn>z=lu(Af6%Cp`#e?7WtJp8p$p=m3+j)$KORok^Yu=1i^OQERrmCrTmlwDv zRs%m|&vpS8gQJ;@BnRz+*iyrtSI6b=o?~)a!D{4kGMh{$>uEimU6S+!-D98&YLB{L zIx|#BF3C<^VsTdzsMeUJd%0p_!VUXs$#ct8IvKCx<8d~qdO^Rt_)6x9)xZy{XS?9M z{7>iUw9H5x^t+3>h@YT42D*TG)CD1YzCqOs`rXCu>L*qsepuO~9`1WzbxyAKy`87h zGBaw>?=B`qeS+#J=z;R19thzZ3aVbv?=E(0J+T^p^^l%diR0-{1ik+T0MXQt45NxAn{ zeC79l#m^_-de1q}dCv2k=ggTi=XoBo6X}ob zx_B#9)W)M|Tul;A6-kB)t#mojYLy6w54Q({#$ZIL(jTkf{-^XYRd&hr5)V^fxe}{& ziuB5rwqVe=LUF7{BPx9HXTv2?7P2cR15< z9m<(+U!^OOWe-d?)SN{ zpmV-D+4RUIoKBAZXq|lc<@}SQ6T8;?aoLGApp9BR&Oq31aI%F+!}ug4+gw8GhxEXdF#Y-7N?WtbLnLE>*^;-C!X%dw02pN zHl|2oNtLD+CfR7JP*yW!Xn;;IuQkw)C~23YNRfe%+sxtyn?;xPIW1bZ!dtN?G*NA; zrfpz(tYqe%FCG(ajm-ObvT4!JIGr3hmriECmwuvjlKYr+f?K$l2pJ`5QhMB0LzN(+ zau>HkabxYwe4ZHdHEXuiK}}qu=61nQaTFqXH>&JZ(N4gaC45?@OGXrWlc~mFCBo>k z_>GaM-!)%5dGL=1b2>RX_B@1BK`q7G3(W& zEv8ANJDHremPV5u&bM9@i>6D#P}@rQ9f}NvDdM3LUGHK&uq z=hDgSSKLpOPCl;ZiPc{Xkd82sZWV6hGg*V7o-I55saVXP^dyq`sLLLAW^#e7sYRBlSh|GcCNhed9A?M7 zb&~#{(UH>7%Axy)hyFQy`|!ELq2a?se-j@h{?W)7m*?+VX+o-yyeoM`a+xF}(Mgbz z$4BpwUNSi@Iv=?KIc@YWpfRz2+#!8nltk>vexj&o`N#=lzM=a>cZ>fxv8VJ6v3H_5 zzIDPpeme5Z0BdWv6D?zyKF&j;rr;QowZh49xK(X5v4M$ zRB^j!(#{2yuvY?Im)8z4HKO2rya`3%^Iz?G_CFDbid)a}Aaw}obdEh?W5Vo{uG$_s^}LD#Y2X)5flhr}~f2WY_tm=UW&_KkjW=a(SO8YURGKMch@Rksz~p zKABUM)nTK;QCG?_pO%ts5L&pZdJ7TM_$|$FC>D};ETL4Vs;4V#&6ICbEv`5hQktBx z*4S5t7Me=Zlg_jh9*v6bRNcu&Iw-Grw27S8U(xC4V!Uh(lp2gHFRH>8ES2(`+@yhX zQ@8o?fUiZHD;ifV<5QNRI=@@Vz2~$n0bf~ZNe}DzKAhF6*~IxWXbjqN7ptKuWe{&g zJm!)$9+j7HO^9TPxGC45-N*{Q4_&R*)cQ=tf~(~-q{~Tb++cU9JE~MMplGz!*T)bEv=!^P0v6GBzK@OyEjcY^Bg4fbNx5PjylXUvbga5eRvpc{ zwP{EF<-A6o4roNz@GaVUf4HsVJ{aZnXh)9e8FP!MwAf%y-?q65Xthx(c8soA-dq~G zk#AAv0t<4p!Q}S@6ig~)%vU>+f~TW(b@HrN?N7O?PK`R{LW8L2(|ilIt5#XtSu@#D zu?2hArPEh%LsVbZ*RgC)=b}x%u+tsVXz=t{1h!!Df-#e+vPF$u5w3c|K94yU^5d*g z>9u7Uk0IDepdJls538{;jBgP}YnV?R3Y$8KhFVqB#~N~L*IL&$a2rWE)7omCcGy%k zzu!Nm;#;^4>6qF=MJPX(G8j~Pm0epY=@>blYk1PRusKw(XBBB{*r^!%EI*FA17#X9 ztXZ?9wUuha&{kIx=~TUhl^TA=VQkqkQ>(Nw ztBz|+v4Am|VyZ^8(XEqOYt+?fb00%vdL@~S<&yqtp_-PQC_8$Ho@+V(7t8r!(4>O( z>*DHWnqk9KDyZu=bny;d%rv`7v{bTW)hlwCCLZs)rM%JR=|J0hRxzhP40iy|7&*fn-WnZTlDRi@J+tGG7ljKvaoToawK;$^KFCZ;u7I;>Gm8MNUJo2!_@Wvy3N)D?7IHd4#0?PC?8 zg-_M++H`cXRn}q+n=xt8E8+x2I}>VdEpn7Bi6Gg`n5rCCKjvFx+%ZQp=kY4pR?%qF zM%!&|y&Nlq621!A@KJU$?QX}aMxRN6Eah7?{VKbTa@BpbvSHN~^=Lg}XuFEWW=4hj zm^vHkc-49wx4QD;Q=|$pBU;Al^i3ikR&?xa#pKMpB6TuU3_6YOj+Tl$aWz}PB315d zcvP)eHM&-G%)dA_|IaiR^fB+tHDJH7gNIZL6A#}<%FG3*^CmPz-D9hoOr?&+RZq#~ zxTEZ3>xQ_7N|+i+O{5%4BRJopBIjb}X}7pym9=c1pj$P4q>#{L@jxJ~t7yV1doGu;qJgp;kN67Q?Vq|aB%>f# ztfzpvj9w+BKshrvxMCTPzfmHXtRv#nct{mlR zI$TK5v2H+XFsKVgom;Pn1PV@NHRE>uZXqMG50`4*U#^UO}Q z{b>iwbQ+z2!P%|0*|M{nO)(Cg!Q$)sI{LgTi1{V_D$43TI>J&iRZ0vVQdTLG(n*Di zzC<@vYPvH7o%A%-wm>Xisntfd!9MbJSDSLU6imxuB;5`tTTHR8c&Ozq+Vp{lqn)Yx z8$lI~W{aiayC+ZFEm`IBibxm@hUz9X#Axukg(;O$y{(({ICUW+;x)GmS;o*(>+9~} z+qgHj|D~Bom~-l?feL388D%6_Oc=YEnl$GliJVzq!xG)1#)zeYU2l{U-N)<0ZgnUs zYKrz@m9V$04)|GGVRgF#oP#`S$r!WoScLYeJRP}3^nK1gHZ9ynK6x$pkq5FBJYuuN zI#jh)sG*v&-%WWFsKQcbn69B=V6w)V+U|6eOR`_`@&C^p8ec1Y51cGI^OHBG|DSVo zF4+V9Xe0edGxt46UN#a!d4sw+xIU)U+2Vs;={x zU16iw#3X%5TRZAC1yOZap--;xMjJR=R~QOrV)w%zqe+{^=IpdfbUs-L>Dumq!BV9w z++(1D=2Jn9az!gxw%O_t%AQ?8C5-y=bQN2XHrP_eTFNwS>sHtZU3H?-LOWE+gtHkk zkT=aQf>(d&TxgYZm-LXnWuMQ5KVHCeU9e@Vc4gTE!yo^^!^dyk7s}3So$9ef2>lgwY!Y6r?%$&fowNqDotmmYl?2wP%QaLEVUx6 z2_)iKf1RQ{I@G5zHMqnZ{Tax`=8xAKRi+=5|FosEVl?;v#3WT(tNar|&LpRQ2IlUrQI_ z<+@u>-+$Rg7y9^~6~*tvZp-~){0Gc+03+PAtYzX=F3rTwamaKWfB(KXWGafB>9k<{WwrNR@iM9f6C+SCSZ?S_$6IsB=r9y6o0 zhLa3iGr5R9V47ca-Tu%yLp-P68}1uouOa$)J=f=Jh|2#b4RHa^_k$jm{R{4~YJ=q# z?f;AczRy7aaZqj~*epH6?)s0Xf1}+gb?7d0bU4Dngk7m`aNk16tSVRyu2AbsjNhV4 zstYDVmFXtbz9Q`|hl9Iscb&GY(zZ5L29>6e&^uGvEW0AEcB#T)rOn{TTC={gCXST@ zmR7!7G206YXR_wc>q=}ioUgb;o(@JDN-igxF}Vm&#p$B$9(;bmE>-=Z3-SLJ_v8Pa zJbCNnUK5q^pN}6lcG~E}qgvS&()Xls$z75|M%v&}a163m{Ipm%e2(Z95&6Fs=Wkap z|7U)xci_~Ji2Fb|G$fOX060}Dem^vlW*0%*2E==q+lNcc-K~Fc^ZxMnjKQ7`D#Pp> zcW3U?`&6I3BR^no_yhKK5%g_Fe3iKw8}768$UpkOKR;{UjP2DsU}{JvFedBE?E}7c z5esfdOb!@abZ)--4E^k-{_jKRyxBUYw?8K@=A;kg1r`!YPGIrFkMp^m+d!YwpSt?L z_b-_@zsq`y`~0r>fL~-Gq38sD-(US|bMuMxIox<=|M%YG=gsF4z5V)p&M=b>8O( z+&MzZ+?+y-`SD$SPVatl-kdJ!?c3*chQ%zvD^lQ;Ki8R?M`%2E{H@R9ox2R0@FA-{ zxVH~yJOYor@hl)fp1>j>cV%uC`@G%y^ZxH`2hLlHNA&idrEkfy1;p1Iq;Iz|Tx>Z5 zEo-;f43x2Md$zm1q|f7xP~RKQ+ZCs?@4udTSZ}XBr}7UhXk-~x#=FUkL1n@nsuUWf zsb;N2`pjMTt9dhbXm5``b2C=W z_g!$c%7O6#1?C1Um2)x)m&$9e=`*>0blyxJ&|AbY>E3AxLOzh-;P`?9k7@1`Vm%h8 zX)a&u+^)WGf&BL3K6l^V<%9Y|)9`@a)GY2k*hi=b$0roHo3SX)DLc3*UKQ$dch!&Q zExWyYlYQ=HEQ;?dc(9MPz@4xV&dC>C2(Ngx&(}BinKxg1_9nQ6P~dCkE<&x;#S7U$ zV%7u~*b)}MIoay-bJ<({->;uAZ?2B%jdRkPxrtan)I@>B0hTf65(q8h;&7keue>*J zewX#e`uy&+wjc|Mx+w6w(=szBpU_4w`dpvSFJoc(3$%q6{% zSv*P?^7(XuJWBiAoqk3C_l%)=^LS_v^m#n$gDs9^As>PS9)(W|bAJ|s`>@Xc^qK6g z+ZB_uzdrQ8|F0OT4NYA;<(YhK^72X3#7h%j;%@A}K7QT!spC^)_l{-8rbcfWJ#`e3 zT`vpB5a}(_jPwA`C*U)Z!$%$&sf`=}?gbezh1`qOki*4Kiq8`tH~jnI^M)0oCqPOpx{~Q69A(NMbSEQ5g|6#PTe}$- z#hu8iw7NrX|F-(}?_Br0w{_3#_0vNRyzaoSAMw>it_Q;s&yQ51+S#{|JryLJO>iOi zFeaBHV(}JbP*5#7>o#WyEJZuI1mp#h`CaI2E)t=yQj;FM>51XERu<;?l{>aica} z;%<%-*;tJy{F7G|SIWOMjRV);_qDG2UO5{VkM{ojQdF{T`Hg@6a_=p&r+|btn9JjG zTg==ESWF)a7|<-G_nEl+lm^s+nYmjW(L~9ca;7R6#uMH;{PSb(6@TaX_g?wO8Lu93 z-0?@z*FPg0TDk4gufAw_^_%O5$leM_cmdIsc*19yzBYN=_YFUIcGF+m-;hObJpD6I zZEUWWy>pWH#NdtlMt(^4j)jDqc#5b5jQJ=XC|YAgf~uEO`4ZKzsx0vkN))0xcPq*0 zOlFNBio4Q}e|J%mxi7unKfa#$ZS2eE9J^oq0OG#yq_0*qUOsE8NA~29uu|?ZMHKRC zwN_Jxa#qgAMQv4N?V&o=v~*%>?(7?nIh7`=!L)depZtR{=Dq&LhqVEvEpx;Dk1yT( z=H7h|zw_oEd)zbaTX*|k$lfuKa1zVeSWP@vAn9yZug``wzBH>qE4a5*WUXy`+`+}2 z2;e1iN#M9Heei4NTzzSH>0@{Qp7`9>uX&GN{L+z^oMOvdyq9*5^^-kkZyND(^LWRG zOlweS9Sx^BI&160v-!mBYr9)Vo>!lJ&KbyltH^HPpIa7hx$!U8-SgdjU;g~(k9f@4 zJ9;KHRHaE;Gx^d$YLBWu zHZWus&)b);@DHuDp3(T))tBFO{~x~l=Yt>8{3&_A`e#qxWE$Rn*>g%~?}(Yyv?_%o zne_Pv^7f50_f#$V**!NeIqUEnzIXMio8JCM=*N}R51;t-v3GBIeNShH>>Um#ivue# z2HKpohSge5bb2ipO(a;425Y%wst~Q1JZ*zA9M1(SZklIvnd}>{ZG38`VDlB@IBd>k%p6HQZIqKQV3roc(T-Nzn=Jxx)wLI}V?b8>m{L7sWkiEko z;cz~ZX`@9_a7Z4qcC;XrxUitmgx8r;LU2=f(>x2$- z{PVT!WqZE5@ejk|n@+#w7i8~HNO%F6TX@17)0f_K-+g-GGfPj$m)*0hdrRuEy?r-F z^bddf*puqN+vgdww*(SiK)M#5@b#}0&VR&l`tye-#@60V*$%$t`?sI5Oxt?+5X0XN zI_lv14P@^SNO%F+S$M+R)3-eJj{JMBZQ(nY-00juUZ?uj-G{vAwT*i22YcN581C#H zJY(|iDjy3^ZQtKNa?E#6v3`B%y?w-=|Jf-(^^C~+m#^G(%(C;3lI%Z}`nj`r&`fH( zNWj8VJ7d#>XWqN=^{YSUJ68GlHI_fiPq_Gm^H)8PdirP4b$32Je56@9#`>X+Aa zURjT8kI`Md_s>uM;)=7szjdDjp4flejjuU-`_H7di-T~UnsfQBV{gA&rQSTz;Qry? zUacMV^2-O~&n0gRwjcY$9k)JqDcM^LC(8mdq40!@XMb~D>)~(y_A`qfc>1`z=!1L@ z{L=qWu6^|tg;$fSoX>U1-hPnq0#csvgirWJ$U?;d{}+1nQqUckXHPdND8amUM|K<%-w_J0Hb~<$y z+1ncuUcl)qPdJ*~a{6J}qi;_Y-u=!)559MI+Hm<{%&Cut57Rtae7gKjj_mCP2`}Kx zlqY=p^snz~T>1NZm|OmK##={!_K*5w{&@RO*6QD|hVHxcs;dtEKH1w75?;U&Cr>!> zr5`m9X-X5#&pvQU;*AxjlpehzJt~`8T26>IT!yc>%Gujv#^l}Ap(Ic3*X+&LJpbn1 zr%yj!Kjg?~C*rR=9>4nSUw;1r&ja7yeDHJE@9XR>noVusDI!lTzCXMDhF`DofV&>J z=@(NMdiV1mKJ>!Dulyo@{S#y1MSG?ly(z@K(+){J__&WJcH?{5&z^ek@3y{>JCi); z8u$I}qZe5>KXL5?Pn}jd`{`TuecsWVoJnl(=^RfiIQipccX`e}>7cEj$$Z9q-uSuS zTyl0M^sB-X_g>&wbpmmbqc<^=*x?)$YPm z&mI5FvBOLEzI~0((HoyhZ152nPwd(I!MV>#zIV|UzxafyBlcc%)T$G{lKR5X5!;Gu zP8c&k!jQc&I8hdGaK#gT>TT@&+YifxGdKVGMb&Aq^lm)js*~3p5I^w2!~b~uzCWma zjqHs=!VAdj!4qD&in-&yF{kw(#-Fw(zVqta`R=g&(Z?QK)4Jd|8|!-NJhCT)gcoom z#1lUD(ZreCk9U5BUfe$M;mgF|4UaEca*FYc^RD@Pjp_;P8YkpKE!U zJD%%oB7b_y=h17!+Eph>etFvqN7+~Y{fQs{@zSTso&*wJz_|-gc&v8^^3UySE*-X3 z)7Nd=FZGoA;nP*t&Bnz~1ND_}9(OR=8-auu5QUK^d`5NE+omUF?&mO1C%w-#%eScM z1O3mZo@Ebu;d{$JHNh<(01{rn4RN0E%3oaioqLY?%~SV0H9?l%(D~2);@ILJugF~e zlk(laJ^$!~&Kw(dAN-}xM@U!B#z-I6*P@C)lGuU@hj=YH6K7KRu1?ipA^JR-C zDzaZr{bK5>srr<4YVpKglYg4LLwf4y`J;D=UKj6yP!mR3Q-;gFJ^aJr&xwoTRpOtI zDo2M!$BD*8n?#q2a>K#l!zZqk9xMG5*WG)PUyQyq{@lc46Yol{8b3!;lb9v@AV-h9 zFml_-dEzTZq9ezEcgN2k|Ma+GTr~Fh*fnF#F??))@mt(s#r*#}7V(4OL#Z-dh`T-R zZmwBRQ}rr4I~u#FaKTF#4T+k3^#~nzwxcy)GZD>TxYL`R1YQ04WBvd z4n`bNa|iR$1FMTDp?WbC<#I`y!=wjK)r>{FJGlBDLiKD5Z~L9CT)I)Wm{@0MX6AX7 zL15;0a&D-iLnG-@!ksqeJhVCBC~*+JU*>IyMmJxq3UA;xE`aye5|onWFc`?C~Z zU0|K_Yz`*U375TKGIK}#W}9g+>%%WHn{peLiKVmQ}?cm`LuONB|`_5|{&8tsn+a%rmS zC6WW{$971jm8_dPb%!}*B%|I2V;xvq^bNjxwCN%fMO%*P6arbRJx|U48qAMWbP``Z zOm(t;jBIA>?V2m4pW?BO?j`E>{`w2E(AaH2cbGsji(z~ zyc8`lUfkMc?E|?VdXg7SkdBwiRLWg)SS@sp3Umk0nzm3oYzam}>6A4ZPnna6ii;RX z{0M;+!b-;@X3mG(6tLI=wsv!1ZP6J#aiU`nQ{DoNXEDN)_nCr&&Eq(pIMH;rESQTb zd8$mPS;5%Z2RXbPht>+j>z1N97jy(m5oe_4wRvXd0NJr@oWj?RhZA(vLmK0(Bg>jB zwd7!(?!BY7!&}QX9A1yNnRA9MIq$%63T7QA+ul+q>v9#_g>KsAh}Q?!9@@x{bI!a3 z!Z>dds@po4!@|vQXC;^oJbOukHs*@3aj_{?)dZJzJhP>Ed z)?XK@r{lQKQjIe%Q^n@bRA1X zj?Z5(Ch(fYJo^NcAE`)K?Sd@h^mZ@{TP>$Mohs%Z%==*i>xDqW9?rI{<#@%ynEkHV zhqVIna|B-8){wVI1)42qi}2+s+h8LddYG?1XHpeGNjLAP?qZUjmJi2qE*lV2i5CV6 zMsyG_$8e4fnjECp>d427rh48wJK{f2Js_|i#JmA>)zjvl87yw>eCX&`7-@p99tmJk z2UCg~GdN>&I&<+|EYzPCs(Vr`y36?34rL9yO!3CxJ=M^qWBU)yJ8A+?n#_c-JXJt|0M|sx(Q>RvW?AlJFaRdOE$4x*u!SNdN9v6$ZnfqObK^AVGV}| zKb}R83DiCIoT*8NGifd`17|_G!OM{7A-+24NmerTk~!aTm;6b0us(Qw7Cp#U52qtt zZ^RYktQb#a&DPA|4W{TGzIrH7c5xAMj46^%*~p*~-v#eK=Brb2SHs^eIGlt#NLVeE zT|9F|ck$Ju-f|$<3}y0R%3-uRqRimcUv&G9>aA=dQLcte{wm?{o2r91yP{k9>OtD% zYS@jTYTa5%XUysN%y$xnLReXDjmmITZ$ySL^x~o z2W4cvgX#S!Th3ejn8_T*<&Q8!9<=%JjU2GVx z6&_tC!sKBJm2BP-%?9f2>dcpt9s8(Lh1!_KoHjeiN}(Nzb}*J0ycB8q+M$lCWRDcO zq_x$d3&zlgQpWIN4D*bKST5#`3y;+8MW;EK3v>r>07W&SdOTYzd?;DCYfsAp=ei^U}oO}?YE+La?o$o zm@4naI&TUqmnapAzQ-VJ8%r{&c!RBTOena&F-@je=LrgzdW!J^!xd`#rNWo=$ z#B|f3W-MmhY{QdifUv766Rk8Ud~LlE_f0Ey+jrR*?39#QEfYd%D!ZcI;vxv-{gsF^ z5yA3dT8Sy5EKyI}bzODSFkMR6jHeM|NF5QW7 zelC;!q4!-Tn>{IXne2!@-ptnXU2gRM>T+}Hjw$lKftZH~&Km2j6)u zlPz-wu(W?Ox6ep2TPw5ij$rdk2d0o!88rQ>9~s%B??gU_Ma_G?;z;doWDmUtE<6J* z&yBm{HeV_p$8=sUK4oYc=fGaZ+nSmy*iM%tR+neG7{wTsWw$NQ+#;g|6&Gnz7vS8J z9VI%l!r;P-g=s2hkJ5>DF;AyDzDQH;^*UX?WYJ$@eM&6pVAR=sieM-rXriJir`vDJ z<}6&6ky2NIn&)SE{h@P~_xWs3(zm?xVxomi*N^W(ulum&4X~yDpJqK=`Fq3vA?tt+ z{!Q-Dnl1I8Dof#vha8m__PFjp!*)Do`hN3s7)5`$yF1x+x?P8{F*dEud^?bB6^Kqi zm2U<0+#aH;jhFGPVLGccb(L9bQ>)On&6?ew?E11h&&3L_u>Pnc#n|f|Q=C+`BNki8 zn{}9pNLJ@u!TCoelU=*HYOa-3%1|S051TExS#9PHx?Da*gNvjW)z*Sc8dae6aL8hA zEj^{&(hMjAi0Y22S7| zHfP}K&tY@BdJqljdUtJ8V=(M8e9elADN>qN7cdiwYjj>@<$%t3zZYx`^mh&W|X?F_!9OTWQ z9drBBR?^!@W2tV=sO9p@nTuQ)o@%OF%sT6=&4_W)bmn23!qBS>x3S>d<6?00-d|G} z;sn9>jIPkE+}o!V=1 z+vG!&y~+Bdc5?B=_KBw^?wGh_A~dn@_B4{&jTo=*^>F9&L}tN0*KQ*^9FKW!K5RC@ae-*#Xjbq`#NmBmK7Y0;yN3lI|mU zRq~|d4#^dgb0oOrXvx&bUq&7qxq0LpBe4guzO12=X@aJn|rN z4RYS_y5Y|aXNLX5n&JIK?}(lkT`T&6s3wYtxK&HET4ds4_x@EhG$g)!x6UX;7w%TG z|2Fz}NZ@cVeAH3H$lEjhV`c`N0z*r|@JSQH$Xi0K55GYp?;QY!S8!F{6smmSNQb<8 z2pBeSMQ;d2v)-lKJM#94VED*WhmqHXLVh*f#e#^Uv({qdt$n%X z9PNMZ=o`jeK;9InzBVta`q@<95SssE_nL1P#QArj`2cagE>wAS*H!*0Nc?X?m7OI1 z8ear_LJ@ZY!|fC~=U)~CdBVWEOTvIS7!2zthJi<@Qf1h9ok_;h)l#Ee1A%=2clT%* zxP{_+shp*OU!d#SHC>;;l~btl0mHz{PXXW%igum?z{5)$>~K^6$SLrl6!O5$PXS;P zX!E;v;1asT2X*Nb3ax_*9l|&)Lg7y1unST!3q>E+EU*crO+uBO{jvcr6dDKhYZc^x z356dmhuM=BSOht&5}FHgFz*kB<(xWC68`*t5CC8jm^pC}Ge%*gCkTZDMv4h?I$o%< zlbluwq>md!`XphTD}}-Zj`KueoCcw}FwPT%F3~|<9xoK?2NfPCj6)|B?lg{-f)unu z(EuqJ1kxIz$_#y@LZNz4zj{FqDxvVBCVJLE;Xod+Zg`pfH6fSTm3SlVILUUm#D}*kO9@OQrLg7(^3gyB$ zmJ5YDjpG1Eer5kUm1FGDD?D3x$Ud>h~x?4$FkXkCwyikSSofAcw<*=7JoK z6jZw7@cRRm9w7|oP@%zq!5l7#Vu?^?Cs8aDhH{8dG()9_2}3zpC|uxBmI^~TNN6q$ zpzyEuNeu9V(9DZr|HedpK zjFd)B5WPGyHun7JGKn5sD|>ZvT((hm!BkFm%J^a~Ti+8?myew_x^{Sp=m@b#6q#5r z0i&e!0r8sgty7OmvdH(4r^kE~=84}A8rjN^!>3JpD!N*&=GJIX7h8|Fz2OG;FK3(xIUDu3 zzS4-)eSTXC9sQNiV%|x?Mio3)Y7JJlFlDzz>CmZ&guaxKdzzTdUCg`VRfQ#2jCYAx zINu=Xahh-8GN&xn2;D+Mx=c1xDwYhIgwJBv`rL|wkt`85c~z12J3SeX?t%SIa!=8~A#P&HMd3;HzC zv>L+6V6>gg(wdIHgfYns?#R=k$N4^L@p7HZ5?PGOZHb)Sqg7k9^03laEal5sK%q_- zUBS55K_vWnA?IVx)ir8urVd+cwyc_B!5s*ieQtHWp3F1?U7fp_4mhZ|HI`}nC-_Wa zIZ{Q3xm;-xvkLVv5lf!?c$Q3Z->pLld9c;;+0k^C%A#$p0iCe((po?!v7*po-kHRt zBEH1}GKu;57Gc~MCBhb4Zn~q0nA}OFs~Jp8tDJ1!;^xw9xr4NsV%1r_H7^r#aLzxI z*f>Amb5uS?sBFQO-O_ZYY{gjEC689aA#WTbGwyEPgIn4vPqpasv?R+RSGj1*9WDkn zx_Zb*tJ7h3tCYY(A@2Ow7b9AFH_@u-jb4wYX;lm_6DC%dC_4-4iawZA*s7&+E@o;f zTd^{Znfw-eM3uLxs}(}!%UdbYARFYS(6u(Tuw%Pvz<0&x^g>X)M@G#c{t4Fo0ITO zOh#j%Y@@X#RtmB)Z9pB&dSWD(C#_VKQzoYtk7+zUQ&odoWY6*nQFG2oRYTm4`&4;H z-RZ`XcC8!Fap|(uDK1rAwMKMQ0V3!s_+=0AeQ0worz7iZM}kFHN?U9-@JiWQRir4t zx)R9L;sJe9o-=U?BmIC+AD2@l!nDPUv#iR^m8quf zCNC=O@_i_L4TrCs&Q&a4Qi(b24SNAqIA|YgQRySjj)Ao>-dZykFla{JKr3h=k!w@+M$}UD*m7pf#Kf?mRqvK+_!bLD?&{^a3UC>qjLBG?DTNq& zD`CrMw4C|+=%U+hPKDL>l(m=)WW8KQiLrh77Dkua+t8{^jYuTRCBd^-8Bf4vqJ8yr zuw{*QVzD@C@)p=s%QCh%--3#zvSeIm(}&5TG0!FNi{#^Ylrk&*j%+0#jBwfcD0Mfc z(}zZ1g!3wIh-Nemo!MhI#yuUQ84VN@K6?nILgh-{hbn5Jl3P`bH(EaI7`}_P%R(31 zxkj)MjiN=X+@STx^ZsbiW;1E!omj65q^IsylyfL8o#I<8ASJ4tZ!!O5u%mAXE#{xvwGLb4LTwKjW^tX# zmFp_pb~j-v)a7ymopeOfMO(^Uw&@+gtcqwV#CpC(KHO;L)r={cV)446(RPxIpL5;G z*JyLZP%@Hq!NAb2aNCtf4&_@oES7ez6z!%NrsCtSH56irdaGD4#*;qI)1sEd*QALot#hmn;tf$>l1i~d$jPiY0Xj`dLE2y$uqFJKCx*APC zitEe9l%bxaJt)=*c+!fTMOjrKzu{Y$?AkiFDM@+rmLO?R5NNbqh?MY_GD1eMBTMj7rhDWXtGmdz?OfGn%MIG{qDx-UGJCd*`37^eDc?{1fMn@=vu~HxkI!(qzM6 z)3GE;WJ+GYA#5qM8!BhprdATPwHgR|LfqaE@QHJC(Qe04jygK&ZrP2y6<)tyMU?Y? zQ%vjE#?2-dn?~i8WGY?`fxCo}lPqO%lNKt8QXv8so9L60 z)5SuqX0#=v`t)>BpCNJ`r!N_yP>oF3@sg&Joz=^k7#VB^g9=MDmvXSxu$3%w;m3R- zHkR(>B2-kT(oC%3rK_qZb!Z7|Q1(jIlCa4sWw(*DyH%aG9JN7Ybjy`>1{DlI+*jQ?WRB>S0*urmhc&8bF~~EBs8I1A>|3h zxs{|{vtvrDL1oBt@}`@SpkCWl8-lb48z1BQ@K7B!nvr*@1e)eejAD z+mZ;lkwu-h#xbCmF22hlWv<)V^y7}#t-6UWt?}~?(x_7 zaj=BRRl?h8tX3xCmIlfNc}uy1zIF>II1_O>I}N2RZ1Qt&0TN-8k#jqVw1aj;Yi=)A zkD48<1J~shm2gsPY8!${?jwK4WM~^O3Tg6vIKqKOD;)DAwOsNH(u}iiB^y#1)mk(j zGE_?TNT((3Qj0w)ewW*0%5=lW3V(TC@Ei)!}4_($KO>K`NaYkWd4 z14Avvvi-Y&JI#y1)3QH|-Mq7z%tbqjYU|U5L`h+o&Nw}enkhwvY*{sG&ZTjGYJM44;Ly)F~~<1_yO$&CisXa7&L42b_A(pUJ-*}49g3~2U!VO5_GZcRxt{fO}J&hTk7 z_?nk{(2SYBfN)&!njnky>wnpF3 znwqUli~G=+4Y*4NMa124qJKISjtZhtAj@pUeNZfQVZk+f9CzV$kL01w_*7o5VtV5Mcd1Ns zwmh@*|NWcukQq>Y|4#M5r(%S4Ona1>hOHLlPFUNuv@TrlDyBmjTRp1}Ot->*w606* zsAO~ZU24>XBTDG(>1lV^rdKM1DWeLtMAIcVTQoad8fo%Qx9aP^R|3fVWz8{ z47dBMufUb5K+UL&Hgy#?nsic`xP`OWYz3WX{=fdv`_63j-Y9ft+cok@HFUizMH#Il|i}d1h~FbQ=8yY(wbH@ z^JHL!vdx|75$1-~Q%Rs9dpDzZsIzg5rCTBQZa@E~gVVM!o6tGA?_?$ik!P!#Y$2NJ zT8t%>$^~m&0N+ZfZdRbI#$n0_b<;YN!K=>rBdM~9O`-7yZSYqOsg9}QWE7UT*QoW@ zvH7{O^@q;i{}(aHmZ7QbQ`@FqoZ33IWoq-(rm2lnw@q!BT0eE=)VisQrq)ianQBj! zr!rHkry^6nDcjVlDRfFPwR~#H)Z(c{Q(Que$?cqXz>AYxC$~&)p4>FKaq_mw4U_99 zubf;rdC}zB$u*Ph$?{}oa`j|n(l=?FTs4VKDkhgtE}2|BxoA>4ImG!BY@2v-V(Y|~ ziOmz6CN@soHnCx1{lt|M>n1LmSUa(1qCHWb$V{xBh)nn4_?Gd_V~VlmV@t*sk1ZOLjtz}&AKf6OxT(u<^PrE8>ZX_?EYuv!|C`lL4LDk&;e zNS8~ONEb^NNu|;u$#%&$$%~S$k}Zqo8}SvPXg z$l8%LBkhs$NM>a9NMyt}VjEdCf{rLgmX9nMSv<06L^?7Awu5cpMX(iY;i5xs0vo|? zU;|hWt_17AMPMyh1KOa><$qWWBESc1U==`t0xSnhz+$inNWl=Y9ofc3lH7`H8Q;YD ziQG26VSN4gmE-HiFXB8#){M8u%j22x)#H(I-?(jj)i}yUd|W=hWPI`XqH*c?(Af5| zZDTKvZRLDQHjix@+c8nh*~G4TuMWI>duO4dQ{I z3h`5*0`UM)hPXc{L0k-q5cdNGi2DK-;yxe`ac_`=xEIJm+!JIV?g7#e7l9PSDZoIS z1WAY!fQC2@5)j8g9O5Wg4N(SS5T)SL5GCMLh$G+>hya`n5dl$%Vn9J01`&uN5QaDe z$UcJiKnUWyAPDgtKtOyO1R%Zz{1D#+K8SAsFU0M@1Mzj>hWJn5g7_M6Li`7CK>Rzf zLwpt3ApQ;D5Vrv<#8-d?;>*Ac@vp!H@h`v#@y`H*_!3wJ@lW6+h%bT@A-(`kfcQK( z9^xOtaS)#aD?7$E*0pb(!0dWc(r4&pOF3-M{7f%p_qL;M|3LHr+}g!o&afcP7* z0^$~MEW{^)9OAFRF%W+RrXfB7j)wS4a1_MH!E%Vd07pXH432>K7&si_qhJ}tpM%37 zJ_43Pd>9-G@gcAT;?KY#5I2E?AwCEWg7^S95aRvdQxNY12SEHO*dO9gz+#B^g8d+F z1p7j~2kZm!Zm>7RAA`Lh-UarAcqiBc;vHZS#M{9X#M{6m#9P4x#9P2P#GAnw#GAk< z#2*0}#2*4F#2)|�_8s;`af7cq2d{-T=f9zXygPUJpbNuLDC6zl*#FaXs=b#A}gv zAbtmV8{)T-w;*1Fyb1AYFKeiL~O;x~|gK)eF^JH*S8S0R2K z`5VN`kZlkzMP7lp4tW{k*O0$L{3`Mnh+jee4Dk}=C5RUze}edBILSBG)5%N65 z3z0uUya0I);un!WKs+D$J;WaJEW|G$TOpo@JOgnp@-)PAk*6S@gZvKS=aK(`csBA| zh@V4#1Mw_m3&b;#Cn2suehu-n$gd!tfjj~6G~|~MyU61ZJIF5}wvo*cTgYP&o5-UO z8_3Tg){#db){uuGR*{DwR*;`TEF+sBmXHS_7Lf-a7LfZP=8^j#W|5yl%pgC3m`3h} zm_jx}WRQCxCXu@#(#Ve?#*w=qu14;J7(?!W_-W*Jh^HdAK|C3`6=D>*1tNvq3^9V- z1Tl>K2qJ;}u#Y2AumK_kz7H`1ZiE;HH$Wu8_aKJA^$>&LI*0`LF2n#>577^(F`twXabi) zG=g;yG4M5rtH4(wo&>%E@kDS5#1p{95RV66hIkzK62z6@B8UcXAw(2h08tOV2vG;l zho}WTh#K$(h-z>iL={*IQ3=k4r~v0cTme20@mO#+L^=2z#ACo&5U0VJ5RV3HARYxi z3voI4Odml2`2j>f0`&uY2-FYoB2YiTgFyWNHv;to>n)DJibf%*X_AW%Qxcm(PP9EU*tfRzZ;4=^B5KLACbet;f< z`T;rw>IY~Ms2`v~pniZFf%*X|1nLJU5vU)aK%joW3Iys09E(8x067Bn!{uy;`T^4j z)DJisf%*YQAy7YHIRf%lw`T@%js2^|`0`&uyB2YizPz34+EJ2`t zI6rKtA8;@N^#cwXT9pb*oX%P28x)Aq9IuQ3l z+7S0ZS`Zf@O^8!S1L7o7hd6=MAdVwdh+{|v;wVyvC`C#TB}ftC2vUFu5EdeWNH3vmd^^bvUvNke=WNkM!EVIaPZBq6?q&=B855)ii|afq)Yt0Ddqi9vh~`833T zAg4n7J8}xdSCNw;ZbPCFUqL8{FC!6%e?`I&|ALSZ|BQqnzJvrJ{s|!az3wfWgUKxU;_hkG(gKcO1LxMDOOiFG*+LyE~Q2a^qZUwbfY~*^+I^ zt895CX(QXRB-^rVS+Z=0PQL(I%FY5|X!bx7mLz2I7=|@7Vdgy`VF}BF48t(MJeC(8 z%K(!%4|qVxJFj*lTi1M+g(-X_xpX%_B-dN^II-@BxFgPV59wnO!=y4 z7}IKvG)pmJjbTz6C_P|6u}~u2Ac+hH-h{yC&x-;UKe^5cKv!~kxGZ4l+xf*FgfXej z$skQ@1*z0f!h_>oxvP7t1sXCCk@Fz~G{DGdOrpRe+f_ali>vF*bxH3L?HYvmkp$He zv>-@Xor?GKQirC?92BXAki5~C<2W2N&n3=12`_jv0bgLh49~+0fIOkco5hc=OE$t2 z`09*c9`DlBg?X!t;@E)YaWtnu3Y!Sz{oy=Je53UMW`B&rviR~ zONbN++!d2##WcB~MmowVHR<}JkG``!KCyiu-rKmKaTc%MyQgr_+aGwo()Gb*h4Un+ z%mq#&7su>Lk~pVuE(SK++wRMtYA!(lN(KaemL?8ej{-dBS+N|xRV`LCi&y^>(K09s zFZHxyh@|L4&Vt2;Om&LUfx`3x;WWaKmQbW>Lx~R4G!pN5FyYR%jNos>?JRvtAzJ&a zBN(v4S~xx|mJ?`Q=$<8LDW(PLsYDL02EuW!-{BN2l~s~eCF&0~9VFNO_Ta^P|M9rJe{J{g z4}N_AFE_5Ozi0PTJD)jx?!hLrsF} zR*XAT=ymXj5jGnee=?!S=Txba6evHG@gV)0k%^0$c!SYBRw;)dScb!if!qcG^m^CD zY~-div~EUx1(iypU4ixSZc!^FvL+lzLd`fwTdcQ8$8m!jP|WUcO(`nFP)KLu^%`z* zM#3vwo~RY0d{)nDC8LTvp0!djQx~eO%Xk4~gOEd7$aZtB4A&MVBRpte2vT5nuTLpVC>Zp6%&B; zuDOmplz2B5Xz5mh3v+>#2@*N@C{U(IgUT$41Vaqxm&`uQ?S5h^qXQ2U7ZrgwjK?C%DtP2XFTp zlwwqgIvgd;if{XUQyHzWAJe*)*6|RY&M+T9WvLP9SlF=0_(;}7tpOD5r!inX?)=%5 zLgfp!K)fB%n`*g+tAj2G?^P>UD-dtyDC zOZg>q>o=wpMZM`826A#p5=7Wi!YQL78AZ5|2H~$#7P4CYSh-y-ff8c->VzWS!SQyf z5)_Luq8#>?*bH5$52$w8i{=8MTBIPtBp-{@9a-C)#u(qZtZc;69*5-81^D>Qq%p{=z#%05xzD&Absa6v_6)GBhJ;6}!d`%#A z#ht*dwO^m2s(F(`kbb1pB5FuBYi1!qtOl7xq#B6V!Gs&0fr2SLE+vth{z>>a$k{>i z!ye}i_Y<*Tr`G7j#agwhW<66FA%8@px=Mb)vbijxP zaWhLxjee;{=`}VILLnyF0R>eXiNgAH3+5|D0whT2$bno_(Zq_9=d6ei;ZwzYE?P5W znGV$Jf|sHyzRlEBMwT_IUbiu*`8vRXv7IZ(i30HOu7*8mGSdi(UA9G96j7`d)_!(M z!JGB6QfpVCdcG=!8LB0^*+BDcT&o!!Ag3gvf{J-fNbf88hV}%pl9GkCs zh$O}YQv!<(xU$!?`SVj$3DaW~ka#E+)C>7C<~OK%w2No4W`l`V^>iWBEMyTVmBwQ0 z)4US-Xd_$b){+WCq~&&9B)W-o6NH!N`UApSgNdOO2id;s(HbpnJ~*8~Py>sPuVQjT%jFYd0z?MK*8ankqUM`y4Jp=E zL2e&2)M_jFh}MvVI@|I0xK@^;JuNxoZFUb{Go|R13o1=xsIDh~^m3+TD2<`Jk`J*` zv=b*wre5+URDS@R1=9%id{N1zN`wFp!a+1s}PgKi;2iE z!DFoG?xdimr(b}3Zk1FsRfdWsJ5)5yYUsgCzxfSPJwwM6zn&xB3Yp_&Qf|55-O`|Q|LiI$`Xjjo{0!ihuYA@+ccG3-zrMuGp zhbLo|Q$pGPpb26>R2mFEL;C1+Bmye7@&US$ju+*MW(7hZ6Nf4t%r>n?L$y-ML~^?1 zA0j-+OMqm;h=*qeRXh&rv4H63EQ}{pRqf!7vpLtKXb5k{FfAk!y?QdArUKPO-Cu;k z)mtb)I;tsIC==Du1AIz>$weNCGPuqfUYv|+&0Ll)s!EltxB6(_R|?a`RGcH*J#9mu zQKT{Obrj^9P-;S|8S~ac?qH-%L9J-dL>gfVu7r`2w_N9}wQrqLuwi&mkLm3Jj!4yR zH<|XU9VDIt`!SvrCAEWjx^7L&wj^%<p{M%;0=jWg?>phV@i(Fb+T2c_4T$- z4rxZxgL@lm-#Mii^ir*$WF)J}M4lbySf=XD4%nfe?-%5AF-~N(A<}`&G)Y=e z6Za4>59(x-If#p6_`o7U6j5h(#3@Cm5;2Bq10$L&lS_HQ^&#Mou_R5y6(iP4h6rC2 z3Bn0Y${bB|KIAk$*R|-*U?|6XO^;j&6k>%wTxTmKGF)w10j7ehO&rfRw4xpfta5ap34>bOtrAbg!nH&*iFq+aFJ^~LlkR4?vWID;lXN#<+5XjOKe}E< z90nl4r_)0v>+=Y{wi!XWD&u2eq*(*811XwwOBpP-{psoZ|39o9ynpBSH~!)BT;2QA zy%G589f2FA@iXj*vu}xjx;BAct{2Wps7^EukPpy3XqPa)(#6WHfI`)EG3BL2fTV)1L z#e)560`+Q)nh3&SP_wR{^B^o;Hq}g#rw~aB)^waQ`;6SJ4Wx7@R&S@Ev!RYBX)V@1 zg#sd#j#MdO7|rDJc_Zd9QN*n43yS-!q8;qgU9TVCH!0ncD$I{kF5j{a_Q_UlzsI{j()iogkamj#5Es z+LGPMCb{|@eDczD?(txIF+GVY0K$ECh1-QHz$A)isk2@-$DRfBd{74EwT9ejBhM3o zOabMP{wb32;#j|)_ucX1oJJ4o0-QVZNXnU+Yk6vA#mg1I$0C%A6eD^g7~{gHX{+4z z^~Yw6{pz`Mnk;HTb;&2$>*ul~(^sKu=5jB>i{7AxRI_^;L2?p}2758V zj#eu(Z_v5h)y;7F?!4}Kic*z@>F8@q1orf_F1DP#2uM4QMjH`Lz%w8}JY@OU49IlY(L<*~C54G>!e2m4Z^-hQ7ARuN(^V*w z4N~PC3UmEz8Sw~WIuC=K)KsqQtb`z9cm2^vr}_VWZ0+b7d*A7L)%q2=_P=_q!LOX2 zflMjSInR`mEMg!Xbs*Ua2Fu;N6@v|_WyO60aIvcuAWt_BUJVc|(N0FC2e?5rBT0Q7 zJOxBEWTGo}=88eg9S?TOo2~(>OsFMIuZp_TlE9{hP-HGgF>#Z;#h zC6a}B1}PNX2IUnRO)8@oHD+-=)>Qy5uv>2`w%au8mE&sTzdBXXeB~sY>S9gSn*f$* zR)^lFcbF*!g-~>uRnIR?kCUm zLp@$(>DQk#5ARK6iUYicblFZj*(&<*a7PN4gJ{KKH4^hGO3t5bYH8-qO~2D<6!u2? zDs1LGj34zxl0!x{bIBn#1i7S#{sC}T&{M55VF(-n+c79mfGR_k%ZJp=;4~;EDJB^r zm3}HBlLp^ovVl|0?k?F?w-0?n3-^uW7|GOCrjk!1_4&& zsto4T$FyjceKuZ<0(puQ$5eP1B;^Nt?LqrMwt+ZH=!@A+ryCV`d^7=rmf39DD&_oR zioM>i*ilPr2otyIE5Pbb4|A1z3WlRZk zGES65d#M8qcj6I!8t<3{lnkp8;8F#AP~rjVEQUzH?r& zDQq!!xY!W_hU|lEq4^IBuyWSZk{qcDERqbL>YYK6)ddu|6eS6<)lMg@6m`~14DUP} zb)c(;2K1@kwT3|$4k>Nkggch(O`tUoIKYCjM6sPwnp6#TDkVWcKu6Mh5kOkt8wmt%~uzAijqT(j?Au4u7tJa998dh=hIkpraTLP+0ivb+}-IP#P5xo^5%B(8;fp=** zn2IE{8Zm_3n%)Z)_$*x&)LulXiq?$q7iSWk*X^4U?ml})=)8R2lyC#&1Dy2Hc@@7Y zp(`~b)C0``U6MmZH_YXYL;x?AdYxf2hSPCUXc>8IFwC_*1ytdtgj=B*VLO^jY6Sw1 z6Go8kx;;{e9~OG)aK2Chbpv~)VXA_g9k#_(ri4f8j8Mxb^0|0aEY&MDvR&^e`9jT8 zV}f*qD;1G`F;E_a&^B3W&!%Jhe?KQgTOcH%UqU;uC$B&v2u6up!A`|8TUIgSj?pnU zJWM7!q3%@T;djjl>x^m%^`wM_S_~5u+7zQ!7+H<`a;A%V~-x#dd_U6Qa3d1R_w$g7prBS-G>PDzP6Q4VJkwDI`1R{1MbDCWe?` z2^Jml=e^!g4(*k40xD$qtY#L|vuQkLW`xO(TZ&}NGDV2$sOE#5?tY zfR-YIB9_U}xX#e|8R2GfMi{PDlTtj!rkg^ZiHa6E z(;WN8Z2rTw43?~5gCOs2dO5CQ6sjMSs%|gRTvBVJ5ksY zg_f6*VWHknu!@-Q>m^XsOYM1^QnQf9nl)uM8^;gLkcL{UH^6j!ysi+4BoLK+GEkG@ zmQ*E*8kWX-oKlT~3Xt*H65oHrIbovNFQxl=yh@AFLa+fMW-#DY4f2ifJ$%?DAg}6y zC@YkoEu78hjF1BPtnqTIIwZXnEH-Qg;yynorm-$t#neun2&Zo_X@>aPyj)obkZtOzIG>1A?FKjb~wrdHC9t_PV3aELCPEiLs>7Bh*Fgx ztz&q&M`p?~vBBnv;cRgB&t?+)Y{n23h|RK9oE%iJIKk09pkYAVu@)>Q{bXk-k*SQ7 zJl|B@IU(8f2I3K4Lk^gOL{5*1z`?#j zS~1b3i1Z*E3q}KBLQE^X+UYiHjbI&#m8c4Iu5rF|M%eR&OJ)Sb{>W;cD0g!aG1{el z4HX(DVts^DvK743@CJLztdE8_&9W1U^6a-Rwy+A%fqk5FZE0`JPr0Q%RyE;<^AZrjCmZNpG z%miAZL4it@oxCiSdo8|H0p)7F23ASI`3M5d7WwQLms{LG?L_h>2rOx_J#U_`NV&e; z;rT{G@z5|#P;k2u>0;g4jbnQ->!K<~(rGP~){_lAgAU74f1Lmkal<;O@T>wgTY;>g zn2?e;@8f?zBWx0^AKW#>CM=S4rat7nxdPT3cH2SJKj^0o3)OrBE)Qyr&QuOMBW#3C zPa)O;s-nh+N>NQLru{~<&d1XsP7OEHQY_O+ayp%x4eI9SW`s&QO`%961$X-jZuv`a zqafwWa8c_@D$9YwDO6T%#uMSjY^ff+eMVRzF}6p`D48k74IHl|L*A}Ck&l3M0|}hW z7QnrwV+y@+baq$WJnv%}3*`+g@0BsX>5dgaL~6c^)}t`E75iez0;nhNaVyb46Wj_W zH^+^p{r>M+`}VbykDR2A|MB<-Kz{y1kbU1cdd1=I9lq_5IQXN3@d3L3`}=R-Kim7` zy>H)(?0#|g-|kAgPuuzV9d+l~+y4RNj(@uA6RwKuIa|N7_1G4&`KO!Ty-98S{l*72 z*!BNs{d?BwwZ8%aR{P;c@vRL`bj2SKgp|bW(Oy~lPFU49guu#GxlO9Wb#qF&(yNQ_zofo%yQL55YN(ijkaZ9I(xxs1Y*<27#HdWgM_=fRY+X zfoQ5C?+K+Up8TL+>#Lq@FTnImEv9A+bic|A@l3$tN8t(EumiTR?oJfIq`Omau@{MY zl8Ly#=tIhtuA8bc)ndL>OUQcJtuz|UC0)obm!rq?n^)rWi8l-xHPo(7v!E6oUz z?!v_wf>($nT&k7OuzOh3h9onhClKjXAi@Z2b=g5~PA)T|CcLOsctHqc=$5A3#w#%4 z6mw&>`zXC5DRLB*FBJ0167dhcYuENU)`-(+2C*Q{FYbLzDW))su z*vr*3Y<-RwKYAGO5?v2FSRPgp=O`?IP*qKHXMh78CdduERLHgUD%0r1;zXfVQlYFs zOZ?~!6Si+~z&7kvK@=*eb!-fMal^-3eP5*`56W!2!11wa-tWPOF$sjLMS;?J{eoQilVK|UGFr5b^5CDfEjs}sfDZNZO8nr@nDr394D zYbR`9>wqmvM&r>KO@UD@c}pq6rxd)&K(pQ?N~D&heO?J^@DV4HCH9^irs3(8stMV=(dOrRd1=houQ zlCL-f?k#_>o`#?lBQ+zEHl-R1oP^84n>y&GdJ08e;+`8WFLYX?K{~ket z9)$t^j6Hf#LU1}2Dn-D{xG7r92%kX2SAd8SFvgk@syGLc8DSG%*ebkyFj@_QnDX0r zp%Y%{D!jZf67WaJdEL&8kO?nj6<#sv67z7giYYVnMUa8$})@#;U2sP9WYVpqx7KB4J7Al0o4XO?X{fg%@y-Y&7HQ{5ismUOM6R(p7jN z5R!=0{PUMYe)J&VCAuDTusqx$5k_hr1BAX?S#MX;8>W@*XalW`)=D0N34o7E{!|DV zdVqF#V8ZqR2W-n)x~7I}(H7Bm)3LIc@b-b@DHZZS;fT;u>a83yDAN8Y7S{OD{S&tL zJ7Bwl|298*$%O4o9I#!%*V(rJU$*w1wUd8!@+wfXFLCrgL7lx5ka;fzs;+%KcluG|G<81x2>&dmh2CSaI z&m*JI_6CO>C0q`Si<$T2#oW9%HH~+Pz46E>I1}K&;#lCic~ihLBjl}XBN|9}`I{0h z2X@Fz!ql;BnRl0WZkyi1u*7)Dk0=`g7)1x>%M8NQb?6rBCALg$6acKxitBL&leRKCv(bRAJ1}Zz zXl6@u1q=6)(Q76H?8wGlG{86Y)Dp8-8M$XO=)mw@Bwz(w_>H4i16j6acVG?AWL;R7 z=UrK59#==FfW58R9T>=SN1bnoWrV+j%Btp=L*MXrVd%$mo?b0S*l6 zMN@LK2TN>hpxIvmB-onWftfv%@TGUsyaWBsOFRhKTxgr;M=!S_fXQ-TozEc5X?App zsa_jh2dr()?!Z`|vA(6*S1{hS(aV4!TeCYb-)DjrEVvcydTDeGNVGM(1IvCU@xl&T z!PHkqF9nip&F;Y1pGmr;*;lmxH@E&|?a6&c*kSo=i6D7 z-t*F1S(KKYjaPrJcA6&ROBc9Yc7TFtwu>2o(gOtvoDBz+W(!Z&oAGWhJg5v>Ch+hI zW1+N}4cC*Xw}T9^#+CIUpEQwkQHayqiXE1BF-G^wS0bC>iKoe!3jYd;(OsD4XxYkq z0&$Kgio?ETagO>aX?e;dLUuDbzdu;U#i(EwjC4Xe8(Q_8BIPVx0B-E3DtE_0y&cXQ z4;4-&f2Wc+sBoWvBVDwa3q(~ue^%|NmAsW;{W;YSAp<0GYMs?vG*_46N(f2@BJFk! zPt;GV9^YxRuVEsQt#~^u1>o4BqGrnuGZw2L=Jd@E3X)hd>|6{t@8NdWAAR&4hvVnj zD&SqOzn}t^++pvY3RqBb@`(iUK1Ew-K9Co-R~`t$t%?;8moy3Hee>X4%7K2X*Xjhj z*)zN*S*IE3tkVwpf*`8h)|q!ucB~()^l=#BJxx_JIT*`&QM1M;Ng=5d$*>`&v%^?I zRU|p%uLhU|(lIp_ge7*;Qm9XxWWQVMHez9hskF{OZ6j3R20=OCEXdyOI@ph{@pJ7M z`*%HO{n`pP*wt^JPEY3rbsE0{l31-^L@vrqdb2+@ z5e`%_xbyOVr?Gw_uIKYD&!Fzf;h7rTV+o7v@JNV@XW3JeP#b4TKEtNsK~E zBecNfd(DtZ;r$qfLQ*o2_Mi2mI+Oxc%{U_T$wLRK?({5z<#XJ84`2BzTPJQR~haWq9 z>@aosvV*@r_?3h2JJ1j4gXix5#s1Ikf6IP#-@CuR4Q+3^{KqC zA6oCNGn;?D`N_?1-mGlmn|oV-xAn_g-wSdA1h$^N_Xm6bX78K!a(l1Z{le~VtiNdO zZ`OY9N5NbQ{ou5PDI{xS5 ze|Fk<>-Y=DUvS!Z^Z1{R|H*0N4G`?{kLUfn3iI(l9RI_NaY2Zh+)!mB=@_35(w%zZ zw*Gzm_~T9+ZykT^_+w5RZyx{1@sBudyaAF}e00{oD={B`GS%hc2r;T05-+BCf5-86IBmRn{O!lz?zHj7@#DvjpHKEx9X$TF z<8Qmk*iy>nww?)adYa_|dVg-Rw~ya-{4S@BUB~0&vD3y|$D`ws)5e>}H;!*OZM<>( z&f|C99E>YEc>IpzcbqehB-x%GD28L>R!K=)F!GZy&w$=$%d*yN=#* z_39bI$Uc;o1$M=w3s8CPLGdhqDMbH<<^q7aM6hV`By z6+l7M`G}dF!(TuAb*GKD4}b0O*PJ$X9scU!uR3kKb@-XX&p2(odH5@bzv8s<#^Em? z{_^?AT{RkqzjXLZGseY2q*re@i>U#V>kS*J!MyA6#@6?3ec$<#yQ=Zl`?ubI&iJaJ zt{vAWU7vK?cpJnwe!^*Em+NO-KjXCVmg}coKkc;frt7C%KjpOXhU+I?KY2cKSB-}2 zCtN=yIarMdXCe^J6q4* zdbZQX+gs1tdY041uB~ToJ=1C9t*vKlJ;Q0^&8_>k?sM9BW9#W#Pe0#NS53;+)3%;= z&e+K2xq3P&WsP!QA7&Ed+&Q(qtL!RH8@qPpUD;{lEf5zhIc>bTTidNUZM?B7?uzGw zaa9L*tGm^6#*z?9(z+l<1D!CZfF#y)gR!&y@b<$_8*gvFZu@mk8@sk&yZu_HjkmTR z+J4As z3YUzryY+_|iYtw}os*ZGyu@ka?UNUuyx3`D*U5`cUUc60s-tIn>qA=~a@yFn^}($V zI&HkQ^?|JqIBmSS_5EAlf8O{idwA>Qg(ok(Xa6t$Sa8q&7n-uxH6HT$Jf3^@|E;mO zXa8SV829Y|3xjdb{;ymZjeGY0EuC@C{=c{~?%DshR>nR1|ANZ6Xa8T^Cim?B3rpjk z{eNLF?%Dsh6zDzs|Dw)V-Tt@Z{~uWU$lA$c;9dIByNEJ6R>!4f8@ts zwUtP4H-lT2g-%{MS_&`xSkULFIySF3PF-i09=~@SRu;uZco8WR|asGr{KYr0o>&& zcwhox1*Ug<3hrMSz+Ik#mrMYxu(z+>WdJXp09YY<_3lmlC>nQ zq!U?3bF!AOx+!Z}hDj%~4lm1E#_E==C7C3h$U2ylwS?6zSxZt&UM=gf(f)LGuCU@( z$;x>3>U?apH$|{QuE$lfH9A4CJKLeoljL7pA%~+A1Upj%D{#HGLfS?r2)3sPR^WPV zg_Mp?5V)oYR!D(=ZH2sbSAR-;Y_v5+umaarKK)*eKpAab-ucV2KHl8<*phsWSAQl~ zMjLaomaw`dYf0)xC$iS(WG!KJOV*Msj!tB)&BY}WL4+IWm-FWOaSqoX+l6A*# z4UgS;%XD?F^4PG#SMjS?=VLb>og!F)>na%$ogn!7DS|KCr{>3Qym^Y?%l4`Fu^YoF zf>pltuJBRv>Mxy--55*}tibi|W+YH<^e^wbtH1Ou`I2?@#}DO(H79EctDAd#St3Ly zvU<14da}MBDL2eHSx?@l7UhO9C+o@k0P@(4?sRpo@&#mtBoJ5c58M9VT>qbI`@g+i z2miVE^Dkfo-l>mYWG6%V=DiEakd`QltM!1>j4QrJL3$?BkV}&+J9dtyul?EwZ|R{W z8yX}ixtI^lpXtWx6o&a~lwZW!aW$5!rkIQr$ScEUQSjE!cKf59)uq?T+Co1LR-E~ndq zge`NKW1gbqR11JSq0BLhQ$8)(3}93r)O1BsT441)*wik%Ufue)QdiV_qCS_04EG`E zvQ#t4YFV_pM#vuzMQXlAtLHDmXx*2omYZaVJ}C+( zFchL|ikGK3Qo^Mw>65yAEsM2aL7*~WLm|j2M;K`l<*L1~UnvC87TW6s&XWG35$1A( zY=#YTP%-W><&)j@M<0EsJbt0Ab>3@U(mD&T40liKc)zsP`O4G@m$lB5p*d|8Fv+|0 z)G6x)TIXWwZhL=S%vALh>6=Rsz^1PVt&*M9`BIiDdt*DuQngIiEZhRO^vNCQ8VG}y z>6!%H$)TZE50xuvPx!1UHfn8-=}=14-yxND-sh`uD#eu}NbHW&%MDtV*pWPeirL2C zfkk^x+oV7-kEP{`mN!!}UNn?;CECiT zxZuzlFy5x$S-M9Ybp6psN7u(MuyxJ*Iu~@!l6`XbbPfL1sB5m!1W(qLXYT%SF}?Yh zNfVghShsJjZ+>uDp|1Hv{Gc)9@jME4;)J$T6>EaoG{9ebz~9}}tZ#mBmczXzs3Q2s zNovRiY2Byn0Ojo87G8ybz*{YVveH@#xTTbYrXtwjFY*?8f+6UWzqBz<@yvnj8iFBS z_`3;xS+MUvlmBffmB#u1gWP>W#qQnYUzAo)G3@S42H-rO9|8p3v8G(IfuD$`#6U@J z0BK4m}} zk-$5CuOHLYww%Jen4E|pj2G96A>K+Bl2p5aWC-}wG;);MWI+BYGN>>ykmxQT)^7HT%E0 zAKUwjy&HQE?EY`Nxt&k$WVb)HU2y%C>&;t#xb?QJwaxc$zHZ~Q8^*@|dVB3J0h>Gf zsg9nxGpXmY=K9&IpX6uDe^!xVh1Y&id*c~E4k#~}aDDRX=f(wF@TR$Xr5C`mC)c+= zdvxCfgDATG{nZami!iL>9k9YnsWf^z00v5TthxT3V_>&xcdXzs@TSqzfFw|hX3g~p z$J213l*V~xUhponu-LsgIsyDaF`6~k&p76PyEeTw*-P94N~7b+R026a?O4vm3*{{d zOPm2pqoZ>PKjm1$#k2CJgr)0oeRK#!fO<4*uAg));!78#S-D_kVRT?)uqW&%9AlWP zNP{nND*zjAe=>$(!~MA9b$w|JSJ*_WJhit+djJ?HVYBA?w~m1=Sf(rZBFLj%AQIHD zS#$lEW04nj-U@{$>!Y3Nu&=rPjbl-lhJ6M9g0~)ywt+NIpKQ(bqmHGmxQbWsHmHtV zHXM7L|5wLwW(#@+M+LC9x2A(Ex;}pOi{zriWEBU76&^CB(Ix;kIZXbwV_>%qlNFpA z-Za_(k|u}A$F6<^pAErjCeH%Kjn8%X-0LSivnqzVT)ta-y<-(6Pu1>w5+Nhx&~n5C!Uv zt+{@{v8YQ6WCacJ*25bEd!B5S{UOKFR$MD9I6hQw^ldnH=RfEe&TQGN5FYo|XWy`< zgDko}aP{--B9*<0FU1PJ;?j*C05(zC-|raMEtS23f5n?_m_X7*Wq;q*U*DFi>}8G= z;td1vpQ!BjJLbP&m!7LT;3MJHx*;kzmjSVKqdS>OplQC>v7Ch}dzlwT>4ttT;d>lQ zxU?g0_F>7#6WjiO?%KDmoe0PO{`e(FeUNBF~>x>j*Go*}(`>0OQ2Wg~sT_u9W-^mNB~uZ>|F z@7XQ9TdTMy-?kO6#<^u9d|uA}{rr8$IIoYPxfQUvie+_2D`3;c`0QV`e?RBv93#Dd z?6Hx){MLwWtm5oELpnXyt{%+|8{IRp$?x|$#`xm#*~!}84C9`?pH}gPoo6qMMi#89Nf%xmu*8{>Y}{(bQIWk~05 zpKpEX*libYQ0Fey@U)XUB?^)wl*zhi?#hX)%!wdb+XYgja@XBem zb>XM~zwzYvpE(BhjPc8XE}Q|I>cX42mbkmmaLp9pmAGs`fBXCP@4vg|7}l%D*THyQ zzDTTKB+ei%*d14@L3?+6`foV<_vfGI81J?5%WS+C^vl+@6)epe-V3VcN}Q8z^56f+ z#`#x|JH~l^e9gvrVHa<%;^jZXd1=R7iFC4K{z9{n{$<%Q()-6RwUJ)hF)Ns_Go-W4 zb0x;f=J~(B&Bpj=*BxVg@%X{{i^K}1?+oH>A6*I2#`b^xYy0=-+>Q}mA3rcR-#gb< zu$LFj_mw5_?2E*wfB*OF-~a6gmm!_^4FDI3`)#C`E)px)&@-gBE)rM5oLnS+$-eJ= z`oAu{iQfkEdE=J=n2RqG%k1wNwhO!aN@uMJxZn9-Y;gZ&&+!;OFn+NOZsA2@nbAH2 zw?I{1338$;|I_0~C%GqlUMSHQ9#0dgpa{v#^Bs@u(J+wVrU=3aOb+ z%nURP)P$Qf6&a$sKdTu5Jy8`hWhA0tr>B8v^Q@faG6Sm|ZTEelV62{PdNO_aCY1QwQ8ASJMulvhO=yKYod=;@-Wper<()d-qhF=POdhtq_U1 zNX6Z}kbjxriSV#_Gmt!$F3ADAu$ zJ=le_9k}w|sR}^6(9+V}N!@=M$REs#jQ{zCkFXmx?Ihzr)be%|rchVgznP;xub+CcoDU4q{37C>D1TlCapVeJ+FSQv>t7OCw{%U3BXn;ZL{1|0MZNh28T*xair9e zwQnQ8Q!V4gd`qJ;c?R|PT0_`pc5`tY=lvpG82HYlVbp>GA{9&&lyp-GSm7iHnzcty zky?Ub>f$s?WG+0^4jd7yFor?ILqaK0B{h495FLR#F|~bGXhy1oRxSsz^&S->rPd&# z^nu)F`?_J3sG+K=x)fA4d9KL#=dvU|_o z{iEHF?+$l^yZ7z!x-sgJ6g}K(ZerM|k zw%)vz+IsQk|GW8_%@1u3H@VGMYLhx(b{-uV|)EK*WbThUnkd}yY|-r=8~W5 z8E9BPD{ETyFOMgHS}TLT16%O^9|o}ecZg%(2L<>y%=xLH+=N^ zxN)f=j$v$u?9Errb$vZFu5XGP*9PXG5XeRvL^_b~WQ?r8frx4^Gce=Tayo)pEN;1X4B*i*3;;=%|g$JYvGPoDU}dNsCQIa zEw(6(?ggE(Z5XUCu$9|KV`ZA(w2X%p*3@s>}{qBNdRv;COmYFr;zE>4Qa zhanMF5A*Z0*S$V2Uux*Z;6gaonJ?h&(yxz&xrUG&Yg)Ihu+H`I*IjCeV;mX0fb8KB7zv)s#1og&ZYOprnP`y5W<6J|W zPnnh57IzaGf2}?CT^Rz4tR3z4`vtz}Ep#D)ty(hOw&bYPD;0*FSY9FJdYEBcx$EQN zCFUL!q#5XDZhMtgu8#{B8hQx=McGI$&d$S?p>f_uF~t3_H{h=|Vg;D5W3ndt#8958 z8wMJwM+KFuN^!~ElBF{3lCF<)mzeudWI*Zl;XLyiG|t-0Q9iHBqT$mR|8Qu!y+sVi zV`UchWc7g07pW>R&$ZE9JV%o*@%lJ(iMbbr6GE(?o@ZW##=OnkMDSj;lo8_|t1E;o zsuxQ2anFDZbKYztRcl5Y;OV+OAu@G)~#fi-DdL6L4H^ zc6vUB$m4#f>$ zB^+lVU&$R2mAu9khMo%B$gpvYfeC}I)ufQ#lJQQ#Rk}V-Tw)F*L^Rrwjd|veK;yX0 zTuhTOr5`0=9crdC{dANU4W)?Inysu@NJCjV!SmjsuUC(7t~XsD$1X945MR9PFUa%E z-*|l-z0}Zyc!Qx>Ff`xrYp;(Z7aRHr1Rt7gIzQjAczw)XYUo2SHA)U{<5;*pW-c}K zB51K4tlp+1^3XVJ4?sxvW#De6SGU|%v>CuXjFu!LZY9b`1hO#{>VCdz#tInT^|-Ro zIAqgE`6k+e6qIR(&2rY@L>y{%_3nTtd%kR3DwVQYsa=kj3tgWp1C4`|S=3vDj1I!D z^I@SWcGCSi-Dxy(h#F{+<-CXnt8uNGYc;Z!jEjfHv`vHe=?a(S>t-s{G!ecx5ENe& z&$sd#g4VNcAw&{>dVq@sjCQ4=F=f*jl-?3*i9#q!M4>`7nCpwVBqTKu z4QdfJILK98DQFz9X)-x65v;jEUJ0M%)+%kDMCeQ)gAK9J(4X#R;5eV-iisW&>qW(Oy~lPFT~`bmKRlr!0uW$k+My-LswgizN!zxl~3<$qBOB7<| zPEtkUY{3@kCn6TwN7$6Lp4?HgcFO+;}hTJjmoC3Nb{GnOoj)CW7yoJ6Rp272# z3Xr!JdL=xA<}d-0$wIGyXV(@IS-li(=;iS2(K$?jJv4jxj)CU{ zsD;pnW)Iyl@SISw5c1IMg-ZkP8nhLPfDoHI2A&f|6@uZ}SIt){Kv+}g8a#W!940_$ zQRq>4c4ZC|AQvSRhG);8!vqMp3B3fKedQb`Km<(a7@j?E4ig|GCG=u=_7!uO0B`uA z7d$k3?rEW%7pW0?(f-kU*79o~+}ee9KD+i;YaiTv&gM^UzG;)$@(v0I&)fgAOW%K~ zd+C)&qa$*o47vfnZC}{Gy7#HQUs`|v;eS2+@x5=}d(G~*tv`J5mdoFLxxM!Y=o9$M z8(+Wk8$0jX(RZRdTj19Jz1#No%eN0N$1iVR`dH}qLhlKU4&N9eLtnBkZCqLZWEdLlVZRH$mT}CeP`h$Dm0#|@lh#EAx0r4)go=u0KNL6H6LP1)yGMtz&JA9j^rf0Ad|6bnIhRnS@q0>oaS3h%&b#+^YGgPL--QJ z1JL+Zf9AuQHb4n)IOrsVw$Ve=Ff3^4PQBMcJ8jl3B64<^@KSWSvb_-)V%>)bAdkQB zA>y_)mRbX|&_U`YsT7{1b7CbujbH_hh^D$U+vi20Y9`XkzP#J;7dSa8+lo^r6n%(h z`MAi9EMkyzNV$`WRG|!&b2E`)ag3l`{F$szW|WG;w!)}|>5$5ov`i*%nB@rSs6wtF zYD^Jfs);mAr(*QpfA%2)Y+NfaM8}6{b%)@^-%@NWhpJI!K#2Wu+Dk-2QD0YPA)Kb6K^Qt z=eo8zn2K~eCWLdn_=K{#>JU6#QM)e>4Dm9bs{nhJ^C1F!Y#;X_0(?MzDlo)P`Vi%k z$RVSeiuDnQ-Zo(__B2aRDFuLhSpsa8a&jr6ib zNR=^+SaNM#DQ^B%V2HmA4DmmFi1b)yW92DPEOB(AJ>`gw-I6?7Gp44+wbB(e&1mh~ z)Ql95{xmSe$-yK5jgx~(a*}Asovx7>6!TF{>dQ=>(lNDWjNMAMG>+z5R<%b=CguLo zfAF~q0P(TF5WnR^6zaWcHD~Fi_*k_ISr`(!eWYgq8z@jIH@uzwhC1$4_;h77psUdF9x~21oRa?i2w&)G%y4LE<4Se7oI2U zPoFDL9y(&=8kCW(ngUL1>Hw}pO{`1Tm?@UcCoMkL34&S z6%u91W)uynRC$Jt71U&J+QP^f!PKYmXIBD4@PQ%9K13_+MRGYvD700M!OHz%y9qh< zM!Diq)564}kr*k38*^P|{gXw83UzjWK`sD&(RC6`>EYKt zD~NY#HP>GOh#;qloZ~NBnaJs! zxYt}yE~5uUZ8^tw&&0>p%6gKXj2l^N(r(u*r&L_hos-2soucyQjq5K1_~*KPbB&_s zIOkSo|6+w+E-*6}mvH^1b0$u$;d!MlS7yT3V(WSwXz^SF0yzCv*5b3upk9vwdUN9! zpp4|o^lodmb{z+F=1Lc!Qtrxh?og1U>lh$4SC9a{$(2b3*A@9XI_C#WJ%HbHWqxiN zzX+f)H+}(pp7*YEa@P34K<($+a&zyU=O(>(4T}@pw^gKZ9RkQt8^81Fj;xIQtiHeI z`ZYl0wDCKyGULib?lgXnE~7uknY%LjJ9K;FdKln8ZTzrv3U{oG|6+w+uC?PEzn9FJ zIIRWe_?K5^;->LC23kDVfB^pJm9_Y+GN{)d0rckT9Ke^oGQHcHtzAa|orThcS)vMs zUANzdEA135=a3X+(3OESqP#j1rAp~~4ko0bI$oL19SU-E{Y8M(TtNc3%~vKBTvz1l z56}4lOB}!@zcN3*+Gy7wI$7QTF8q6^)ZOLq(Y*dbKxuAf133OyrgSS!ZJz%Rt&P?$ z|H$RirN6uMgO`}2Papm0(d&-Bz`Wxq4nDOrL}(p z2;A{Q&xEawA{Y9|m1K)b(PVO<>n7+K)6xxh>0EZp{3p(d_@rl*1+IS=1lKu1q4bPj z;QDX?T<67&(zEgc*KY>Fbxvq0Ju5A6{YDU6=R}|Kvm(HyhJO9tm!2R2s{E|5K>KSe zpbZei%Fnn3+JCzOT6py^SANDW(EjQQXahvT@-t?E_E%Ow8z5MgpXC>5e|ZJ80pe-- zS#E*$|5^cUfRI~$mR+Fz&$6(8z6|7pOFi+D>QWp@>)aB2!M8_9xk`O9_NK3 z^RwiF#T9z71i4P~Gkk$|g-$9#-jL|oS1)G1Lfe$vGe0NhnV!9Bfop{pDYtQ*^F&0? zUb(=vLK~FZxXyVAqGzvI;98;K$t_&x1y|FvmoIRw(AnfRu5+Gv=-JB_xK`*;avRq< zA=>=xr2yAT4M~DTar3jp0__UzM1s8e@U!>=?FubJf;{c;v)BUd3hhCHgp2dD=mPBu zeL#Y|+VC@Mfp&#{9zh;o_!+uDyF&YpAaUvZ3|XLEq4`FTXB>JK0cg3<3Y|7??Sb>2 zZRi=iz_mhGjoY}+34o_(&;r*AoilFZIwwY+o?TnuTA@eAZCvNPywI~p7r0hvhH)C# zT_f%3S$KhKg{Bs_ah>y6!p~j;aH*jcnpgyRAK_=m3$!aVstEE7!p|OApk1LUMUbd} ze)i%8+7-G_1bN}$XD?cyU7?FakcSF>_V5Dj3QZ$|q!-NZ|6A)nwRZH3y?2Hl0e^Y= z=O6hBe8XUNbzawLHe3&~iSGT-wNxQyUaF%%a8=o=J8n~T4O;`1Bw!T9&Q^$dL6Chn zuh$Vo^B3a-JTWfHGA-3&3C7@S0f$QsXm7u;&e=KLi29y|Z07*?uB@{B`aOqI9D4vN zG%X5v++tu}HsmgqRaWdleV>I=Fa*NlfTO!MN-bU^Sc3A3k#do#zh>DTXN5_iNjfpMY8?|^*ALm@$bV~F&HQ&x$YxF#bBMc5JcRXZDjd*bcN-W)PT_&y6981SK zYQV~5Q0Qwif1DL-WK56bkFQA+*-Nz9qD$Lw3n@#DA#v=~dc)*|EEZDnNHtc7xcn5W z7OTXuX_xS{q!uP{a;3UV*nH~1+&iNkr#E z{f_JPq1yon>|v)fSI?UzRUt@BzSI_6~S8g@cMN!A|ih7^1R{-#ScOKyP+@04@ z)RHKP*={M{%OrR_4T?6jYaUifN~|%G6^gH>q(nq6hx zZhR@AGmqH{;Idtr&gmQ1oA++~GeGL$*RK5|h}M^HJbTUynD&zwssEZA&sqwSJkJrl za`5!&_0(=W^F(q_cBpal#xnq=CqL!j#+QKgIvsWUAX5%*d@(?NI_frdjz4*2VVvEx zuetF>%Mw4w(Y!K|JECrHTwX$dp6_{O^rsc-@f()_`uYC68TI@WXB-GOj^+%2O?iuf zb9~q<&+jC1^2XuGRtZp+Y~|4O(`#tmH~^I9YW5W6HQd+-^yYi!NzQAyv3Ih90g9*H z*9wBq#4aE;SHsVLUW0sNXU+>)-X|}oLA$Yiva|tezuo)7cek+K8zDgH$yOqB$Gd+5{&@Q5=@oc-1)g4k|D{&ot#6t=Ja44* z&0lr9k<$Htj#mwniJyJNKh;R-yst5MG(X))$#$qRqm$WDtfFa#IB;_;J}68fp#e{7 zZoX1AqQ{YfQZ6IFZF>JTH`i~_nk&VwFXR)6I|HMraKPfw{p6lHb)RK zNYwZ$o-07?yb7L)IC6nN2tvjoC|9da8J!cbX}_7AwDFd1O)NW;u6DKKLB_F*5UQY+ zw3%ldUgzO*e+oK*SXFX0clvzlk3II*!`VY~6Y!0%IAsFv`#`*^35cCF0e9J=D?9R@ zqAj%S$cxN14>;kPD(FWEDzyP;-W!5Pz}=zi0V)S9d}~jU8}SYvZ@zzBV5#qWKVr!F zen*mO=owJC%yCKXn(7L0DmqcpW`jqP5oV|sGi0S!O^{9_O5eBNF=|eQDHK08a%3c( z?p+m&D!V#Y+7GcDocrPbsCn|=g zYaUaoP&ASR?N;Zm+l$+dVe_ej{TQ0PaPDLO#(%c{VDFKuZq4Y~tr;vv6wEY8bijNM z1-b6sJ%}~H75Hw=NzcBP&)NDs1hd|S5fCBJ*JWU37D|0HC6&> z#H4kM*_ELRddQ@$R59A0Sb1OwVkrZOMZGprK>DqOSFYObaia#xq+c_<35=LdwKT4R zN|o&-vD$ZWK6T*#-v=lC{6=f-^1r|QgO^8_Gnc>o(w|-W#Y_MEQsWYOX&XcX{OHl+ zM}?yo9R4M!2k@PT?ZcNJUOM>0gZCYL!$I}nk^Rr?e|Z0U_uYMR|Ji$gy7vp9AHeJP zB70lAzrFh&&;yX&eg4j;c0RQ81n36v(w)QYk8l6P_FJ}v?H7gqXXrOV?+$fC$hc+jhSrAe17aPB{@s5qwMtoy`{Xeb0cl~SDOY2wHKE3v< z=Uq+K-+kV{JpKF6_X-?uzIy%c@ju9N?3xwceC@`0V-rioA-0)|MlY9#+fH9Jdb#%2 z_A4(Mz0CIZ%P$&@E%P0}Xf(P!I(pG)WO;PtqS2A%(bp~-4KI&|E*cFjkAC<9xi2q? zegS6ejNGqYH2RF(UwF~zGjf0KMWfHi{mU*Iy*$b7FTZH?8M%M)MWfHi{qjYl&&YlM zqS0sMzI)N=GjiX&K<>+vR4%}bo{{_dMWdJH9{RhBMlZ`f^p_WnUY2|4lNXI%mV4+= zE*gF3l0*`r|8~*nWx0p`=%Uffau5B^MWdJH9{R{dqwkbE;QKc(koz**ue$&^5D`1n4vSP&0aM6%xY&Z8hvJ$Q5TIqv&$|z7ei-8 zUv$!i&g{uYFXH6P=oeo!`pgc#5@_^AXGZ9ZzCQCJ+-F8#bgqNWX!qttxR*z7eP-eR zU;oRs{omehgFl}B`6sXfZ*yml%$*_seD9PqK`&%U^xc21rXeFYvU zPy0g)qpHwTS~(0Z6uZaq;<&2Uh-~Saz;Z3wGKSQ#K8h1DA+MvrANrE#pQdn*yJv|) zFpuk<6waxSyF|h4!X=#Srdw69>R?`cqH^8nB$i{jjyd5$@1IzS#7BmVk<`6?y5|(J zSt9AwoB^cQqM9{jFgQLqRwl6C&JV3O>;xLo|;CFSwb}X-z>EsJ`qnA^<{c2Cb`c0_L)e7kWZjC1J7|Q1n6&e-9@uZhbUCU<2QQDcd z(jz^~h=dVuSDMFqN=j149kb%p_);U&t0BBmHh5SYkgMfY&Zqv^V{g-EFP>YSZyBDl zI_IeGgR?r&#a8DF(~1`PMBMhB;clPPoG?q!*a8dGHA7Sf5BUXC#usg&gWu_JWj z5u)uD+hj_P=cdU9=>6KWtI(*;q@xCQ%*Lwy6x2=S3{>!c_ft0J?w0bL4g9%S%4jTl zZdt2-B;C)98BkVYtn~BINUUfm%}N%BGSOaiGVxMH^%|1oWHx#KfjA?9%@qq6O>lWt z>-5MWi?f+V)r=RdsoR;R(CoP8F!@ejY)^6tPNic7i%aIKZ1%WfMTLyTCMPh%apX0l zA7cm}g3V)mwLqNt)E|3nKlJgn+}dZ>cF~$LF8#oz-WGf5RhN#oe(LD=kACE6e3UwR=HVY6{>0(qhxx-V zKlsGK&+N7izW$)R`)liu9$eY~y$|iZ zWAF8sKfF=fdhX_*ZvO1%H*E4CN?>>Ux3_L`V({`tw(>CAXLk#1No-rhc znM7zpE6}NUSz;LuDi7nSNmzXe-;mZ2nmR9rxCRE8nPiAb+mMUA#8C|bdTQ*7PN z!0}YM)6YWDrl;Et5{e)BU9}mzn-|5R#FZ_P%;QL>(wRiU<#f^!Q8GO#A<1%$Y}>j# zGWWl3L4$F03QMX)U&5oYMqc9@U9CxLh5lgZVX)SV^c=z(^=eoG+W$%)!ZyNUW9(&O zldzT-iB6OD)MC9AjVHL8Rp}1nQZ>vBlXeZ?_d8y*iBcZIs*aWv2ySX7T5ckmki~qP z9l}kkA?27(1ef&&L&W!f*Pl7l5Jsam*)>YtVQf4p%iLgKv$0e;!<8dsJ601(7f-W| zesO)vN6uA+pV4!7gPAGK6aIfUz znXv~OWM%Ba9iquO)WQ2s)@mTNB$#BlX1-PJm5YsTHxrNKhm2L}SIJb_8!00Qi6>Qj z|Mz?vm6}*HD+#MxYKp{o5H0D@2rEPBM9q*5R?WCYk{Fd>h8}Nz%7@^^LORzS=9So> z$jFL=*nO_hR6*p?Fw?4*VK|(Z<3&y%R@TQpL^%@;3yE|JwX)&#xQZs)^mGuG6(L;E zOf`ztjXbC)Lb*eF%kR9(m0U|pf=gaA<_r)fUoun4Ml(Ahdwj;#`ld2AGitpVM#fHvG#m#@_GNg3~TsoW?lSTR&bC@e0d#w3eL<^JyT zeHxy`caq{L58BhlAhte+NR7xevsFlM3|dZwER^X|q1H?0;_Lf9#IPhHToy&UF}>b! zJ(<-rAnq9Mxr&sXY87y2)+9JO277-!>_fQqyoHlZP>-x?wHPFqCy_?N9#ZY(WIBu@ zHs{cpFpC@7cx%&#aB>U`btY-VCfmh&rDc(^LcC79g>Vcl<#TE`rV4P~_8ffkRX#+g zQ|z^xOsZTcQ#H-3F)V9}smvtBvZ9=wCTX3iM6{^Qqw6o+{Z7!_CBV;u0@v(Tk#2FA z&1zOn99BhD!Mj`e%ww{(*_;aIY!d<{9e?YSV&Dsop1Mg zQQpb64VO)Wnk7A>+fqyR6l_H#TUFr+(Q>vlpN23hIA(?((`_oLNl)&?*G?>nkVWY!n>%E~UI&i0K z7vUJT?m)oz{chr1bHe1BXq*U(DQ7V5qznRy6YUb*#;g2T ztt9$gwpT3Uoff*kbwVSD#0as}3d=SX^Mq^(pL&*CvuvX4QWk@wWj@tGn(co7@E84g zn7Kl^;gr(t8YGM|ImZ!ssy&LA3tgs>R)rFVwo%s3O-cID{|d6bbk$X+MGDaov*0C- zu{mLel7YALg%maDT2Us$>dB}GDf@2)N_$^l^8ugG+-Z@BA%;{}VtP(1;scr{oe0~A zHOX*YYIGG7$_bUDH~BM1qI^Bo&eqd9j!0U*nM-kTU{oh^KRu47Gra}u0l{!!fNtxB-+cWMu}Pb%M*x1*6S85bUK`ROih-g4qWa@IifW&QmKqm zW1(R=%1m=gf7|a9&80A0N7S5|X01p}p)%C47aK&Jltk}3~-&D^xi4#_gL^~Obu zbB-8L5-C-oX}sBqy3+RF`w$6M@8-wCxM(7BdNO3y9@~_&OeW3K zuDVHRn5yC6o70GEIXU6tNxs!riK154jkr0i(@quIeBlCuopJ*0!OlpO9JDdkoEC%) zrvo)d5@f$qYqPk{_Ogn`*38hyeTZQmCkqf$Y1K@(nQT>*dc;T@m^GA@NGe9OM#)60 zpBzT}#kKeP5dD%d9D0bD9Tq)a)4WQ1SW%)$(+EpwG2QZ5TR~cd+DMFV_{TA@YMEY! zX%Afy%62=kZkQWR3E7n@5jSFF+SxeIO$)I>T)@};!xh_QjHc98?Y!;@qXYsfPRjLo zxzxl30s(#4Z82#LT_tSu)cOxDM9#Jo@d93GaCm|e1~n?$lS|p!v=~hhIMx8i0?2gQ zmadSlY5UXA4X)Ayg>%KCRg88`&n&cIn#vLt7CeKo;H0a$c!5n!6Y&kdCpz2eR)!L* z_B`Np1QkS5~F%e%K<|NVQL>LYpzi6f-3< zDozrK$~Z&ChJ+%J66;Xw-{EuBi04~WK7v=HquhY&r{I(+4Rzb`Dy7O)AH?;tSW8${ zl+bn_^&w=#f?#-%EE#cEE@pE{gpQ|71r{gaXa;r7M7zc|`!q^0+b{MZB-z!67^6f!XnE#&K!PKprebV8X#X)ancHQ<#Q3LQH>axvII%ys{h z23$l!r)havdyI=+zVy;@u-HCUcn`;bqAi@PnplZ%wcaF6Y@ z5V2=M?I~kY(TSOM1taR^1#rfcsp4kYhhW*f(e8$uy>Sgu6rQW-c(^zv+IhKD7wglC z)Ivzbu0eEs^D{mKJ>m)@G*3C3fjiWej>z7 zB-z!KWSvVIaJH*8lza_>+X~vK#DSZtUQruzyiIA9op<>(2pCps;N#yF3VhGtII7U; zQP~(a>6-nK9!Am?rwr#O@e#WAuY8DjLm#p+E?tQbY?RSsT%)4p(wT9NHC0MXn&DEp zVC%_nTiJZI4}pahNM%Q!A(rauB&B8Grao-Ky(&%VN~ZZ}+$Fy)^nr?Qzi5)8@sF;yW3tE5Yy00XN-b+$;dz{awU|1%EXbNGsbUpRQ#{?F|v_I`FRw)=nXVmt5KLAHNt8xH+M=-SqM zx5Ar0ws{O<^egLsy8fp1X95&Y?vMUNZj0lJx5^U5&Z+8jYry^2WzwI>1|)S(%b_ zL9&1EUF_-cC*pw_2vB=}6$WB~8F-+TwH|*Wx?mtcErACu0xU2C4_E|fU!Pg!X51zmO7yIwupY2PRUU}*JFLn1HUVr(!f6*;x+9C}XqQRwWn8YSJmz{X8}4=qMv-U&ZI}(2u0`oX z-+{vfaLr%xX#{Z1|Beq4;Pv2|eA z+5L#lU&|p=*x<)Ks?jLoz4)I0P|HD)uFVu8eIXNWdpv1%$}mnro!+pfQr#NF7gKGO z8`jA@wD0>dg1rC#XMe2%y#N2p6NuG=L<&B{AnMVIrNPNwhwT*VHJx!$IagFnRQKRw z)~JL%yW{d6*5AJBLj>?KoM>VI7u+`l^%z(_Qvo~%*L;Wo9s^%Vf_My`@M#3_82G+? zHh{L`vROqr3W*Q6R1*I|4|>J zQmjvVluWgmgjo?fVaO7vwG=bKNNyxG;AYM3rmz|bWwg*g1cvym55aS|*p!>3+?(Qz zm9D~h+7hBNo@EPdaolZBJxEtO(V@`a`M-UL0FI15_aOo}GCp_$vAQFp*K_f1c;7#flniVv*ev85UV>&e%6Nw;QHWwhybpSw*_^5_#r!N0N2Nf ze=I;S-`f|{U;=~%?fDP^!h&AnLj(v5+BkvW;g&t? zqAwIYA?)#1W;CT+-To%$(=fUvv{uzrwAi8CBoRq6I*7N)(A5S)j}?yZX|<+08Asy% z-7j0H6=N`B5oxk90rz6*aWm~EdS(gD^NQs3lC@qMkx{f}DdF(Sr|dt{v^^)6L=vjOHIP)DAI;+ z5SdoAHSQ&y?$in+2^1=6#i?!>Svr}b)EcE_tT2Pi4Jal$o4@MQsH>cpH6(akBjj2= zYM1NWD2WPQSSVU{LT~6TGFt1nE(C3-eF!mYd79`=%ZjWNxeVW~bVz}%a-`sRJ;-uu zeG*NU2T{Gh`%E7~Xwj)y3~$s-UCkxDsv2%*OQUKoABlA$Se&kvvZ)ugoVzBwgJ$c zpGU1>yQe6kgOw_rI}|-s?t+e;9NaTEe%_~165NiMB~>q{*>YbOqQVFZ6Bsu@>~x+X zKrAcd^adP`NjpDu>0@i~txLixe_4>=0>lyd#)=CNxZ-aoEQAnpvp>i|Uqst?nMoWoWA(x9gQmvIsLWd`YBtRsXf7A^U0~b7M4!!){ z_jR%30>pKl#P|jXR@(9v(Ox}{_;-Qs-PZ+ml8i?@aEpR<5GWaCQrQ8nry6*=UTv4V zR8gPystSg;?2NMi^8NLqi^!+;|p1a($JZa={pY;_EK> z=cnYK-n_959K5?5S7S<;YI5;`)u642o{G0>PCdpZU$(fvB^t#?j+&f+|Yt*x=2$UcAC9M1DvoEAAlG({xgky``kh z!{1n2Ti<<3Ci{<^$Zc7)h%}6Gk|0fDxQWBjBFcsN2v&&T-6`IOV+F=bP@REMM6VX? zCfcMsCEGMg7R4(=0esnW!zw>XBy6Z=nJzI6Gh8mlRjy=osYJCZMwaikMT;92QcJQ3 z-MGzzOv6RDj(3|4l!=uy1Tvfy!>o zYYxQ*iPnhDuvaTgCLE=g5iMizwseI^D`p|nEK3UQD5+5i?=A7L^GCONs0#wkXS>}f zT&l_~H8Ua-MUY;QgiDQnnQd0)5zTP};=hwiOq2CUoivzcnQoe8x1~>t z`Bt)kqSg|~atFs4EAt1zEwOe3lI#Ex5Hx#6lRTTuF4Noi#%v2C;y77;0Q+Eps= zTuFBM9<5q^F;$~`yrN~fB_4J@dx{5&9;cd3qL8STiJU_-g93q#x{W?RXgABX=&(Q& zBM2f@`YMq^8*V>|l+xoK2%^F!%&0HN;i}XWN3E`dvST9bXmZA*$5%iMJ*}2gMQ=FN z(rPj_5tn(G-R6Oxpyh_w$VZWE)X(2C`!VZ04fvGJ9WV3nInXI!9r64|h=g1yAU`nyi?KybxIMQ@~aC1yAK zu4R)lIw(a=r&%CqsF=&MT)dv{bn&Z+G?7ZT;&6{5;?rcNh>qe6&vbOYI?3x2G)k7~ zL0+kINRqiS?vJW`w(1N8ld361s!uNQu(f@P2dD&3Xg1oSVs$iD6epzuj?kcWdvt)Z zQoF|vi{Ye$$Pj*YgcfoI5KZf_F{sxN({fTUWf3pt;+1Bk79B+>1Wr%USl-017&&!H zkFp^0A1s~hrP{+K9`;7Jc_D(oUn-ROJ?W zb%3ij!&bU*wJ~vJG;TEd!_n9gn~qTrduSB|bcQFWg{08R6}pqog7+48G{RHEF^_A` zG7lfU%|pIRj~cijX44g|I5G@RNI*(?#DT1xa??Pvs6tG5B`;9%t5bDSOFHNXAGQh4 z9l~N83(FlE)>Y0?;x_H%NM6t_$id7jRk~+Q=~Tnaq>BZ*>y6DN9yXqRiidQvpR}uv zfY@;;TW|`A1TkdwavO!c1`P}qMn=qT338pQjmiWYHQI7H-eE*t4Cf&?PjyqHdXB8g z1Bj`jjc`vGfXGhjO1Dx@;r4J`88V%QLEGfAjoLwO^FSA>&{&vclCV_i8Sy-B_GK|w z0^3owU=XM+4=X5B9tckHDr9q0v?wO~IX+cH8j-LjHi%3~D^p?z%ZgdXLRGk%WeOww zO4((KU6M(as9cGm^5aH+X&tuz_BIchuHoff+ODGGg3G{Q*L5;>9Stjy#L#rBW(OA{ zq9!r|bai0pBQZ8jmhxK6O?Oa3=*Sp_%SBNWN)=O9M<98guu8>z{E94&gpweWt&Ze* zX`X8??~4uk6b}WA>zAyyhq)Sv&W&JjSs$jEDM$*$TJgA8uSIGyxTR{9t8}iGB;=~f zhASp*rKU)tT2mX%Q8SCMhEjBzBoZHTqY=oeyW$pAN#V#*(a85Xx>0nqOY5-He)Ia` z9JPKmmR1W`k`_m8550KqKMj@P-817ScfdvH-=e`2sCA8ta!KDheSXNMe(b2J_ijPtZA8qs2akk1y8iely6qRN3YqF zvKd_L5P8}|6VR2GIciF#+Ane-cD`s%&E=!qPWCnrxM%e<-Zea0t7$MkRO8yPPb*M9 z<+R6!rI$!PGext&6jE1}ejKvtSftliIH%G~vvNirbrj1@cjL}@prFazSniau3h+&1aZlFU2XGI%zM7V(+=9c#vq@icKf|jAm8BqztKE#7xCwT2H0Ikw#K335?ei zMg)F!(oWNmKuszhE+?ZHlxw&tkQvbA(+L_PM<9bS5iKW1BF!>aDrq4#ETz3d(MmSb zeTrN*QM)9*jUdSURDHUHAR0k2>}u?E<+{bMuE3{Du`PaeDBYd~@T!zMMy2Z5JUF7) zAdm+)w-bg{?+Z#LRq0w4SPzH6llWDJkvetS0*Ne?Cie@92R@Tds|oC1oeyU<2kj76 z#2$k@Txf+4??bm{afaoqvC|dx&YeZ|7KYO$^-k8|bS1nK45tgh`RgFuUI$F;W^#RP z45r7#n8=_R%X(!B8AED#(5LCP2%E_%Sutwp)h?nU6J;EWLIuW5fV7MZht(y;?xR{= z;3^Tt)@rqB)gp5Am5P^l=&CgZ*^{1PB`d1Fq){6`d}|hGSiTxNU32f;SyXP%0`-QJ zL6pONT{0?(I4cXoY7Vh8sK-|2B+Jw*LWE=!#Y*L>trFZM-HFFqkcCIO!*GKYIuSbD zED${JnELKM+!#f{1ptIxE`%{+~d%uczC+lsL?i}m;2<^f5k^>z$Ji2BD zcf=$U+7;37C?)N-=AY$mt1Zmr}QjlUW@Bm67y>EeC9J}|d1oEd&Kc4qk9 zFy!~wx7M(=53Fr`Xyf|EBbPsT`Awh)z`>w}q zFVqU{Z2j8Sx2}C)tGfBO>%YGF3&78x+}v4z_qqazKB%8$2sESIxY96^w9>QFY9}+w z+g+1eKCPYfVm(_w?BE!%@+%LDozEkl*M&e&$<y;b=u1PO$>+wERngz>M!OL`QGr!K&#J&i?1Xxw z-OLlsU)26Qvp{5JxhJ>(@`L~xel+0!f(X7_o{%RaNd~-F_WVPtD9IiEClORid zIJmnAPp?Kuy15!F0$-%u$Q_1yz8WF%=4z~n{`P8wUVBr$Rs=n(UZMKU1ziz^zn~%h zRiR>Hu3oEx$+lw~irlHo9YgMiUQLCHFMjdr@Czulzf@#ezA4rs@YFSM)3%GXR;8h9 z<$H^D=e0M*x+3sJVg;wwJ19mHJFkDB=$H4<&TDRJ&5GRL)|#Ey-PD>DLCDaDl=bh0Vs|P_Rmh|*C+&;Lupeq7jBrgOzeX5#o!|lrt z6#eNV*8=_iO?j=z{cU+|AKsMLilArZwS9DRm#>Jzm)G{*s`8Q_wdAHc>^?eB`-4aO z19_lsU%Cm*maAo6ZgPxHyHb~#W>ygPSKO4wioh2O`}EavA&sjK6#epfW&4E>6#eP@ zK^xZ_l&_g06! zf4I4zD*|7n4pHp%4ST+zd!H?VGZqEDM#0e6>p07?^?DdNU5|fp``X{QDWerp-j>n+ z+D#d)2zpjV`|CGlv?2;$MtgsMA|rWP%Ce~fT>=lQeKQYUzV0oft#3P#(W<}~$taRo z{@`tW>&b$yivIS3Zhgneg02dBc0sqE2)Ht*E5mMm`-wDGrSD5)>m3BRy&px3u43mGFgK>-cqb~g zVn_sYL?PG3azcD~Ohk^oqT}>xpjxDHOtvSr`!8V5@<{YJnQANLW4DpU2K~H=9g}2- zDUIXDh>}HOyd-u&G(&Na$r{|mreQ_zqwz7?>13={E?LsW-pnWxNq87zZziQJ~$JHd_kJHEN-a@g>0ro z(5O6u#IV$`!4GPDzujzEwI0Dv60j(@`5vR(&BP5HtI;1gw)%OpZw;sE&9#LGC*0@H z`z1UdbOCsNLsjkPpQu!G!I`gKmTC{`^HFn0E&s1%5|=ZJM-13K=e?_#59Nz+LaWB@+q=0DJ3w|g;S$(q?v2dwnVs9VVI06 zlkBl|OxQV)u;^ycTy&5uw~A;I8G%x!4&F=X{Z16b-mKU!Wj=MVf!`+2ju#D6v{OwC z5)mvr1Q9Yzx3zoAZqv_Otu z@YtN6p*^>IY7@-&4KI*2^)6Ef_5XLjZ7sF-%WJ!hU1;aCJHN4ayz{OdedBFA(VeZs zFI|6R`?ofq2YmX@_A9mz)_*DVyP+Qr4MRlei?{yE*1z0(eIZ@sR!GZm zG)o{8P|i68V!p^}QPa{+g!6hriQb#}5R{ce#{FoAb~qEN*fHE{q(-r_k%-4@ZkDnd z@kz4@dmx}@PhUV(vzdt3X56k9MYLwWUnnQ79>SK!5sfXVn$^Rge%NsYTPyDRDezLd zmTbjUsN9#tL0%dpYLSFAn&!nOD98_j)eWJ?8;VlTR++7$&sD0Bi?pYvQ#ZzR%tdv3 zGKEJ@i>jogI343P2d9{%fwkJ^+Q0C*D&;&HlOzU2s}_zYL(U|p322PTR$J<8L#)?f z(B9NFK{?G`KWV;da2V;46UR=-v{EU#YNG1GRK99v8>|ExQixgv!>yv-i0?kf=L&?q z3H&IWs@BVmwkcD?Je+Kj@ixbdrLo(r?f$#S>USKsh9;t z=LHW`DCc{FP9ZvuG`c)IjUt^1v+n10R|!YUr=uMPbJA*J)Gk!ZB&fpQPSzr6Y9LFn zi#mn?SMn~p=O?IDm1#Qd(rmjG9Tx{&Oa~p!#ta!BkH&T(Z-iN4Tw;c<+_Wn@eiEFN ztu>&5MnJNq5!GIbFmi}8X_@6jwAzG}e8lXG6nDU76VP_TpGO{{)tC+H(vMPlhKh$H zWPKzKMp=q3V;R{%kTjlZP-Ic8>|FLC23g4LcZbqwz&e?pqtxpNEVp|WWz}pFqedyL zMLQ5WM8+Y%-$S(@qtz(^%iy{&HH0`M$GgKET5QtsYy|1FT3w<<6~kHuW48TFqG~T1 z?bel)T4NH8Ost-*P*U3?yNwts66sb>QX@FeuvAZXHvZV>s>|wm+==RZmVs--avX7o z0tk$TVdFIR&6z#j+lrv>UwOL3PTMA%`>#Z_-Wl$WvJ)6pt($@iBrTNp8|>K zlx9xqEz)geb7>T&^S!QOnVfV|OuMSo5?zp=qZ4Asa8$*p7RR|b!k21QG2a^KIz_bG zats*I_~wp3joQ2fY(kEBnG}Vm*#3a0GM!pk=11k8J}$Ae1=28b)3n^*@+)>!YogO} zBD#i)iiByREl>r}S0QgC&1Q_ETMfh|dNQMkRc2?+ry*oP)LwW1Le_fuKGqc_os;@H z+XP9yg^HO{`&klO3 zRWMWi^^f`xB~Tl?z%Xi#E!A=bP%M8kF}pn-N{|?83GE>kX*$-_m}cT@CtI*+HXV)< z8mdzn#=RbFDj?65>xNlG30jp2s9{g*L#hGxs7R{=w}X%|0@AQS99MD`1Ch2V zAA(>KVg-p0a!`XLM}oq*xe=%*-d4I&)xvbs5v!caRizrU_C6mXIY{tmwO8z`lzWWucab+dz56la`{C{Q)a0FMUt1g*yF8LC!di+FeBY&?7dQN~D| z=8cE}>fLvkSSuY)<+>zn=SM>Xd~qX62C2sy?c#Xtx(|Wqs^OB6FcY`E@}vxp$V^qG z1xBe&7^Ko`JGHSYxpKCLZvWEJm#lrqy}$WS`3Qd^xa<|p7*#{a-qgzx4V~>)(y5fd zlEr~kYgZAc#g2zXis>KvS@RM|4W#T;B$g)oZ9P9tIl^BGUXQS1n=9bm{+R@4Mq1DX#oSGo$Ir119I)Wr%|qNv%lkR<}}1YPHmoS|SML zR?ZOI&Z{t1FT=xj`zk;_`~ikVP~zx3?vjq#|lmx)HrJR18#WfoOqH z10^a&@MN4&YG|F#XDZ31Y}GY`@)8L+>R~cOGESv*u~bxTi&U4{V2mW>6zk2oGHyD~l%`_Fo2S%pxkQplQX8?t&Us-!67@zfRoKB6^X)3^(U{KCP`l((e~Av7wTYb9Of@uw)~^qeIiIbX z#wvjVjOt9IuksRdz?omCLx6(7VUm{|mXy<&E9Dc(V6I$eXq&H&YYSzARGl3?t1Usg z97LM*!x6V7gvwM}NtyD{%2LeFRI>%IyNFdmx}u;(iNwZv2`lQv8`UiCaN;!^SS!}0 zm|x>h=;g2>q;z6l81-7iSd7$!CJt@aVU?x8{dT{N0jp6quhyCCHJb3c$&#{RuB45Y zqDPMDWzxJVImWMc%{B1+KoQZYDJH;H;2dm=S}dV5u6Cu7V8Z6}!#bxupAS+Y=z>-q zjFDkOr8-%*A=g&5j^ zX(phAT}D_+o9bbejmk4Mox&|w*&X5R6fcpt2hCQ*Ws}JiX{FbSH^8DP>T?x6flSb1 z^84^=&Xc6kLZv$0e$&o?JKo&nAG7AtDY>j{$mHS-kxj+|33DE;P#HIdOEauXohnYv zw%d^pdeb)nppgm9taOTxGjnU4vvasK?-R zh9Y=9U(a|cz05#HEy{!hqd??`GXj!frEGaRpV!kM z&r%qUg(@~&88&G_2BAb%QAz61Vs@r7pBt7CZTN`FTQ>yK0i}~lZOUx&IMiTSsza@2 z1jpr}k~IU8fNCtTUQT9Xt{E8Au|03vy%w}cjSs$MXn0oWF{4*gern#w8E#w7&sL1r(z|F&82KKGWxUj zh|)R;xt6UI6ZUY)k;zgHJsSZ@QgKNY$>3N#RxoR6%I7Vy8u8ED5^AL*Q%6k3n1s-! z5gmA+R3Qr`K)#WaxbU2tmD?oB5SgLf)$vQ(5^x|U_ogZmyrHtB;~ zb!jaF=sF@!j84M_sW{rUZo^hIl`%V^QGv`rMy<501{;~yG}xAi@sC1y_&z7xmkY}xcWW3QDc!$awvL(AzrB04q%ANm@i|!to z+imJ<@sZ#UJAXdp7Pz3)lyT=e&9Z252omniHe0;jpjo3eUMbYrY$BTL+C%QzA?A+w zilss%koIQ_;YcB#iE%q;3Z-V-9pWvELvT@Z`*p*xF}LtTFg>@9WxA4A52qlP^R_Di z?dry6v3qBApj|nE!3WpfR{%MwP&QKnCp4`Myj2Cj-!|G=6_|s1J2y&I3Yi858f}btun@&?#j@KNUrw7d%No~qZdvmkXwfp?UP0RQhO$l<}x3*u3l_#&z%dSQcjZp zf7@P0>_&jB^K0K$bd?>b1WJKI>mRv&|F%9|O74IgAnPuGaKYe%JN;FG+xtDkji=@c zmGrWV$50C;jK$@&%xp87Ds%>Xym>uj49AmL6_E!?+xxfxlGR87AA~NMl<6a=rI6G_ zjR8m9D@$bRWt9QM6Pw2sTaM#tS)_`4qG~)Jw=Ba!kHteuLpE16kx$082oYZfd;M-I zq-;2o!cKo%O$XuTf=Y9_HO<#WJ2rWP&}m-ve!~qni*7Rw4->C8n1fHD_nQMQ0+$JNfOC2ZmD!gkY$JMpH6TMv1`$RNJrJQ-WFOw;T zNz#qi?S%{ziJ&!#(NHn1RaH;}(iA`fq>pU>$iob|QXSDVOCDP#T3JeKmcaJ8+=p0e zgvS=Mh8i_>Sn93Svb2t_2GTwU0a%uzPB^4dxe+QCL@8G`yzVV0>oZlYIiJMuOdOB;-i_O0^&rP0A%Bl3e2_F_ESI*34F4gor{!iC&ixXEe$t1{}XiXHf1W~lssxEsbt*N>- za1f_!YK>My+o|nd)fxjvW~t1|^@g080GA#^kz}M)sZdfG6_zD~22)&CuS-2rwc2UM z14d6dp|KUK;BX|YOk@~5V$FKfgxqCF$D}M51RVh)Zc_EDwzo z08DD-I2(QT-+ zZt{4^bjT1#oVKK0N26V1YZUwlg^ET+LZKt0BLbnK_eAdrg}y6#S19xy(K|w+Z;Rd* z3jH6^{|JS?C3;IJ^i9#5JhVboam3=y+6oa@C6A=Kj}FEw;}xOM@_1P&v@~843N4No zg+dGC{B#TwuMB8bew?3<2San?{B%4RnjPn-V`mA^UOES?5J zcc0%~r~HWv0?=&WA{LibwetX~I0_gv_7!9eI93!N?)2;F_5(**;eyDfCO zU=Y*!yk*|P8*3nxm?wln>j+I`C`bAkjfWp$Cc%6bd~+bbwIk{-XVbLiZExCltD` zXx}z;AT#IG^wf0g{r}isM`o+g$H0Gf{_M2CP7D0sw!kN5n+on-;l;;qeOIVb%hgoE zW$q>r-t2R-sHs*Bn$xueIC#%GsC7#esZ`)`l1jE?H?;@q#qB=7|63Q`o7!@&Sg;`- zj)1q>#n#658CzPQIQPzK%iu!*u7K7Ft-md?SoNtWOsRvha5gJ(fj4u?0Gm|#y`Eel zY)a;>Hmx`6Ps`i^99&lb!vCTz^>sRS-ploJ6RuI)d*Gy&%X;p?qn2cYFx52yO-vp( zSEO+)hoU9Z5-`e=EGlz1%o6{`ny;7EY$ ztNu^$fHh@c5B{QD6XwpFE>DXVjp{VGPM=QaOAU>!q@k-dO(@USgWxJNMO(uRDpfVC zF@1xzcn7_-DJ#Z0 zk!Z-=NH-jAESImza+M5`-RPnIhu|Vg`=SO28)tNGf^2YcWX%PfG zw@El}-SCb`Tgk55B*}Ov3G{AVa9MMmhNA{NrdDrokx(J!x>@j{-|7vUHLN6UhAU7Q zcUK0)+(2lRyJVYtIBO3LKUii0l}e`K?nZ8S6)vbGYu1QcX){_BVHt{gzzatyS})N# zHTV#;G7ghnulGq!R8X_shlW9;Jy!AsOopYHr{P4FO%BxRDdcp%93_)iwYWVLgUfJE zo6Ll3xHf7c_4-WSSYIMlahcp)O^}I_&Q?MLAbX9aZo>?&EStxKU4`SDz5zMOF!w*f zvEwJiO*t3yF5XMDIK)8SVKGms{r@uN4RPOhqnNkBMKf+>aR>LqEMMOLvT6fGK*%Vv z0Wiv25>@Gn?!Nn047T zHYrGhsfs1AT1i?p7#t|BN!gWjFo-IUysQ+l7J=*k(~Jc=abM zgyXR)A~R~t4e)fAt8vQA3#L(1q#R{H(q0c0wx{dzRD_D_eNlB<3EpI&m5?04!5g#& zUF8L(ydcb~ZFN^YgDu$+zupndrUI@aVrmPHzs3v7D`utC zo&br0oZuNBrAKO(bX`}qMy--cNR~9~GR1Tqq~lWas!mMtg5gF(t*e+#g@)G@OPfq4 zL%>P<7 zRE{cib}R+b^Hh0NC$DS^W{gU+t(320T8vfDkswRDQN7x#mIa)l6rtA3V|cWZl`%SN zvR&i!BfKDZ(Wua%CR(ddz=l9tM`1az_J%7Nt3yc!62*!IfpaCP#8zwziaXsABEqVS zt)Rq`5=s;BVWy0c0lABcrhG7OV^jvG*`@YNd~QvwEja(E8n~QjF@xlT86;?qdeTL0 zE^1&h(FWP@Bc@oqpt6Jq ztF~3lh(eQ2XG5|);09|s&e)%MK`p`Xzq(5&~Sc;6h9u;cJB9s8(Uq(J?h)!BGsnWQH9MTQ;3EN3&(EDzBzdqbCvY z)cF7~_BUP-kS$OeS`xw{0hJ>LvjwaM6E2z0T$gItq*I-3AYo~tU^McsI$CQBrv0ok zoGCa}Xs#f0TjSC+xNnF!bvkDaTo6u$OgU5;aHUdiDepM55AcGXtU4QKj8xQQ(6Q07 zKV_$MAT^swr9#3fqO9>{K}t41imIr#RcAlW3ld6)5((0NyG)_WB>h!aidCnJDw86W zNF^CVMygSm5t}Oo^KG1!@q#9sqL@(Nda0N4#naW8$?ozff`~EgN=EZJZIq&9nsgK| zc|+|QXYSwy$tVLdDMj@b4dc~>LsA2SYJG6lolR=}rF0Ro125L4t9XeQ96gm6w3Iwi zbKQqE!u3SaW2Gc@za@s~tTk`Zisw*pdpbcntR}lp!Fx`&(?$jCOj)aXNhm9U)B1d- z9zrBhUpgFNbs)#E*^f&#q{^;`vu(lg1TScA5K5ib;-~CRGb^#{izJ~mQnH*)jZqc{ zplvJpat;~p&2^&VE4-kwo}rT-sZMYV>;SKCu=lnlBn9Ob-Vfg7aMp% z%q9C8be}UeSol99Ec4r#L5LdA00>jzzeDaApg7C#As+&obW|xZ%R?HNY!Bi z^sGf9%U7(@q^^OohS^MXn%;6Ku6ZT1pjD=>I4T2uQ#QbZ=32t^B20Q1LKxFm%= zU}kN_V^OD#N~EG8iBqh=0M#99g~G37%vEYim$Pus8~Y2Kv<3k@sKZVa+{nY|X@c(#(Pmz-UMc4*6eg7c$~u?I7Bw@b0QgpANxufoS4VBFjN!8 zWY(uI71OzlhQQ+0QnBuaz2Fs@D<`E9o29}J$I*v*!3t&Z>3m8sB~@V>lNiC;s$>8m zl+o!)(&!E)NjpOtrK&XF9ir9+lYBUqUbux>zny#W?K~Pr5)Ja}bn6EI$R?T=ep3H+=*ab~Gf~XaejMfpN(yE43 zTaGInSwJDA#&!H6ceKh2W~+guSDTjl;$8}@rgL=32&kAEAja0^agkI9beh z762YzFsTXY_0AZcPUZ-T3K<=$M20o_3?5T}GDd82x*V)&;Fv$fcgFahykI2hx-sFm$M`IVGmYVlFC-FeFK0jH_M^OF$}3NhI!+xk91NvZ^zy z`e0h(!JS}*q}E6hdCJ9NY`y|$5#+H<)MxVLjJ1X(D9xw&{+~FWSCz3R;tf?J26BDc zV;RI@bvLSxk`Xr>HAuz6m@-*aA|D8P4g8EV+7a{^-1S@uOw@o3Kmqa{SDeplYz4(&{`RcP<7v%6~Cdz@+-hmbz3l_ zRYX~bo$zSG;Ej4d?ej|DqQmXfo1F$smI_FMS%Vv+tL7L#SIu`O01xdoffw~Tx!Pz` z6J*p*S6HSPVZz~MGMBX*^UHOcGhMaoc_*Jfw4M1nS77{pnx($2!QG%N8&nNYX^l90BoI{gz~&{fLV zs&zYV(NWe)P90w|r&1YjENu^^NzZbYsrbqXCc0eDbUu&9dBFsoHu~(9Xh@5Jm#FT(uBbIBNc%^WO!z|CsrM z=l(c%|J=vt$hm!Ie>?lt*)wO2v%AgwYUcKt(#(l7)6>s_R|3gt&GgvRlT%kug{PJ$ z-7z@ce~2CeuL-=O#gRXaJh0(Tz?y#){pXs$?EK^Z9SbPN zPZf`Dd~^V-)~Df;N=5059#)KqjD3Z=bB*vqhQ31G38Z)-eP5yOv{1Yd-dAXCu1+pp z4KIZC6e02!eT8~-?a_UOdUUO%uTYP!J*uzJ#^XY?)K_R@i;IrzE7Zduj_51Y!ygXs zE7T)C9M;c?HrDPV{pykTaH2!|3iWWJL;4E!aH7S&LOq=5;J!jVoM=m5p&m}Oxvx+U zC)(6kXya}Y9n@E-hZ7yxSEz>*9njB-Hm+#be)WL-4eL`o@JoUH`wI1NqW$^`^>Ctn z`wI1NqJ8=b^>CuS`wI1NqP_YG_3(#1`wDH`O`^UlN?8wo=(~cD_UPQcbCa}3=k|?7 z(jJ}L*Y~77I(L_T)|2+=+=aeEJvw*3uTYN&F~{BiKVjrsBMT2LoVTD`7@vP+{(^bK z{N&u@b1QSi-0bWVv!9$LXLp(T@yr!7&Y9i8cmGv@sekXOpHE#gb@J5ylfRt2Zqh%w z33>s#5eh+z6Th9fbs{!#xcD{km%uCkqsIR*ekVBPmyZ3{*xh4A@U8#%(R)X$qZ-kh zq6b80h>jb12UNb@KgXSS-1w=R#l1dux*H8{XPRB8$K&v{Bg1j$9otX5dq5Z|g7&y6 zz17|kKc=5}_s}pHZI~6&yY-HaxVE2o_aLDu4{Ed8O^B%@uIVGb&4Sxxr_E;EhIpMF zadkiO?tw#iohGlzhC6&6aaBL@?jfYpJDi9OkY~C&;>v#F-GfPIa(OY@<{(!s51hSc zzbm?j73BmcN_OyU4e8Vl_Y?1S8_I4(9Y!-{wRFVg{lvT7hNj&vgkhY_s`9da;@h~5 z&jPl@Nt1QeX{CL{w^@y2xZAGN>oL;UQGU6fcz3^(E}NdP5GEAsh#%cgyu05K@Td{B zd%YHYM_kfRyu05?y~AN5?38KMX^-kB-resW+<@CGM$)o6WS07gcL!F&&R`^~4D1=a1+ozKz?U7=|L0%dqM;hxZfjb{ic@+H5Xx7iZOJ59=qs zO|;P)%vPg`cCCsZ+DCkwX6ygPrTdfP$OXGvbjCJ)wUnhPrTdfbXK3yt#c#HYTFO&C*JLK9!Ad?+*Y4qwe|!0 ziFbRQM^EVuAY>TOj^+37Bfiag-{>?uoJMe4Wi`(4*H66L>+D9o%V;5u4s)mWefx=b zdmZ6q^m@i))vYSOPe1W)uQMA>93z50oFdmTzU zJw6-drdOlQp8dqT-NpiPywDC4<+gTe-=m*+x7(mLx1O?Doy@BE?)}8O-Nr?D8O-jZ z$klMMTOaXlz9dK-TvhRT4an-k>?8feyWPg^AW$9Y^5CljcGrI5-EM>8KAYQRLa^0o zVwZm6-EPBxyNq_P$3w18&5PG%kQ@zFI+V1aH)Q*u3#Aitwph&$#$;Rdy3u~(-Ckz^_pyA4&Wx>26QX|N-Cjr9F{j;Vz#OY?Gty7I z+v|)b7h!T(y;!I1=bqh1T+{7!9tQ|DUIT;hmgmm@$@sIcW`Wd z+j97EynSeKm>VOEx@6ZT@cd8a9~KIIdj7jYp-;^} zBozAO{C9*xpP2u)Q0R~5zaB~x{L)9c-|%yddBP* zg+d#%w+e;UXKxV-t}nRcQ*@b7=p~{{g+ebDeNrg2 zDY}G*hJ&u8*KG2kHZo3^E#_jku0IkzDHQrc(Gx6a7Fa^!uX6 zg+d<{J=TVTi^y>o$ZbQWEJ33uZI5kZtXo7KCmaa9S=4dDfzX>o9VZ+Jy-~zF;b2{F z5Z%N#%aC;8nQ-1vh(}VTYEZ{^`M}IBGghI{g&CelDLSy!`5B%^X)ttdhUZZl44s|f ziN^**XJ&ZfvBAc=UUVbxE_J8VUDkzCjvR``s)WhaWh~KsqWgtH?-l*KQ0Tvj?h^|A zy69e^(0fGxCKP(N=<7nEUlZLU6#7-s-9n*viM}QjdZ*~CLZM#~-6a%yhv-foT0rgL zSjh!$heu6>E6HZNd|>S6F@D~P420e^#)pi-&>P42kTDo~!x$ej21BnO>&#mNb^XE^ zA2K4bAR9;Xq=8LkeT{4`o9eQFcz^L0-dF>n`-wLTh3+ffBow-jnCDX)tn1!lo=`o2);!=gupLcb?^gok<@;cBU#H&)6upT5G{oL#Oxb?}s5D0Iuz zX+oi!r%n|L-86NIQ0PHZCkurhIOP)xJz&Z!6uSQuBNV#dl!rV2-+SbG7_|39S^T(opYi2t!Y^p znIaFPMnp074$3m^&e-o_ZV$;a!xV*LBXBJg8JOoZm5@m-W zRfg$GJrLJeR7{4+!it1j5-=cI6tAlyUM0@!7|DbR;Dr{*kTO`$8BW9Sx47StZwS`& zz@~xI^Wd#|PA`_QLM9L;y#|ZcA_GV9o?66XN5DJ%AW6{KI+4w(^;sgVuX{qgo*WO= zU=61^-2$(0zk?>h8ZI{VU6!0$ba^7AJ}N00DNRh04drMA#$7s>vY|9IP-oF4x6vNT zM^=Dgc@2w^0(j*J95Xdo&nZsB)4)#kC6D>11?zcK6X*0iajPXK7yS}~4oI@7TpzQO z;ANyYk^&3~S}ae zzR+R~#@+dVJ?JeM(ut=AgN+taYk?B!M5p6(ZOno@r5w{u3YzKczS(|1jP8eeoNf|aB* zrmvVYUOMZQ8r+E_jNnSx?>1%Q3Prl=QKZTd6BR{xeOI%L3h4TAPD9&&aKFiW1?zce z^CV8sH52IAVqU7RyF7j*7);?LMMo2gnm%ih#1s|@iRkch9K0u10p^Xer_=SrO`v0( zj>NUxZ_8tXHQm%ak<)bRv@|)on5-n-_JB5(j?hwrBP3<&wP-CZbyswznm=QTmg)sG zO$Tf)U6r?JJ89ftFCFEyGrq|EHdzGgcwqAcuu9@}Y_F2!1qtZ|!&~Bz`znM*YA0=| z52ULK6(v~I6!tOxD9ILrl6W%7tH>{y2ChRm-SFRXzx67?8t&IT9$1lF)8EkQLQbuK zVKNKYG`m)2yk6@%c_86yD-xabAg3PE=%rrwF(_Ei9oOZKlZ!4mtWl}W4z^ZKN|RSIX%!G#F zj^#BiWz)f8DVvE5rqXK}bnzFQn#Z0kSj~N!$8i0yuB{e)oUHaE4La%<*v@mA$zu&h+5X5$+xw?QNysAMaWt3JvpsqVS(*v2LI zYic+p*Nm9)tx8H^X)s->^!W5yw^|>lUMqmZt<@3yGr>08uc_`*Z){aPl~%5eG$Q8i zY%UuOlY#2BqW1F1ob{G_F4VWN-X2XAr{21#-T81-Y2+GkYs1sM-Oj7Whw6cfwL|?p_El>^c9~ zd}IEoxo74Yb4SfSGuxQmJoDI0c4p7%Z%&7&XQu9+@=lFResR(^IRf1cg`qts9-GKc zY!*KzZiqLJKQrDKKMK6*KY#3~(O->THmVlACc0K+9eEEFzvF(0W^kO`$I@{*jZ~$S z7Rq^3_kL)bQo(x0<&S;wmOp>*3!i`WytkPjedFvK=kD7p^YQ<>O!?}G+ONfqA)0~p zr70EKv>DyTf!n2h7rpCE=ijVvANSu+XJKHy@B7_PE{ma(ej}H$Lz@(L8;9 zX$l1(9Pu$&S4e4>w)y^d_Pfh_#MjUG$<)!>^N%%d{>;hK*Bx}%*Fq26`p7Q-pkqYS zzrHkBp|)Dn`t?a-q&z3fIUl*<`Nb!$*!!Zp{`%(0sa<{)w14@CKi+cY2_Jdv$A^)p z`5z^kr*)U6q8;T>ZbRJ;_=?DHU4G;DmLpX8@9cZmyZ*O7SNmAz$KszDA1XX>q<9y1 z^He~OXIU(=vJ9ZMuoP=GFYN=&g0g@)TxIcGm9@kwes3dhW#R~%Fq(KiuDcJ2m98P~ z*`Kjq_uM`1i>i~@7bl7H-h1xJoA$bJGPM7BMDvvOYDyJK@ZI9VH&pZUm;T`k``l-F z&baiI>+j!PQhMyq=RA5t{gA(X;mdJ+CUvabK#QJ$(%@rdzy08EIfd^mXZu(k{Zm3YXT8BoHR}czsFD<{dxBf-+?5{S*0>&4Qd_mnf_g5=_ zIri)Sz|M9znU7M>AT5ip2K%r!ITfCzy7xWC%4}4xhw3yOWM+%92D7BvDps)jXrigJ<2f{PzRZmZR9z*f+xYH!SBIF#qi3 zd;Rw4lSW35z5k#05KY&5HI)jPJ&2mp8(Q=HiOW8I-`6ESIO0FQJZrh><`W+N(JimM z{Hw9wToZZi)z81{|A@Oux776MDluz7rD9Y0ipHd63Zz_Sw^;R(Oc3?tm6!%EN9>q3 z??iY~mhjSVWOqJrf$y$AU+THz5C3-DJ>s`25BcuY{h7G#xDyk`DWd6IuO_UJ+MMxt zWJ5K7a>7&hpA`CD^zeJ%`uz)`eI8^zk7p)-y_@1MscTQae8^}s z%QRrGA`)k^l(!)bnUgNFO=*&AOL#OIGZZqcD$i5-{KtQOG*>kJ?5TH;c=a zZrJ{*`oY=suK)bazW2ZI`mIEhTCb)Y)>o_rl59C!Zl!nZ5#roE+R#EDqrHH%hWkfofin$to@N^zeen##B%UZ>2B zlQgYvXetSUXXB}Fnr(jMo#($9Ft_yOwp{&R%TN6Fy`R1Jr+eQ!^wT4Ml;Xd5{f=virgdFu z8ZE49l+8|N!_waS)YE&MaNzIuf9ilIe*VC&j<+}e;-YsBJ^Q7pQSDFPc=orFUlC2q z`qDJ8GHpTX8|!v+?1~G2^Ca||Js(ACf1oyMSmC>r3mg zx8AnfZl5{%xeRsfKM#5RiFc0sZQkv$NX1 z|GtI#WBO$K8DELq{h-`3^EUn5iSPJMyyc#ggHO4e#`d@ozTey~_^0>(#c}Ub`{bX$ z^7Shol^uBne&1)07)e+EeUE3@a{Oi2Bkrc5Rqzm-#O;FL*mZw|e(C;QZ+Wovr^j#m z#_}z%oOa27U*f-J?*(MHqpu5H?r!Q^1rM>S+b;O)$Gj5%>u2}=Hu3CvkDR>MRmVY} zE#I?ZyLjcs>mIrO_VXW)xtn;a;2}0++XYjxPww&FR}NU%^wuYiu*^MdKScY~<@bn} zc7N_aeu{kRuA9zsH?da1L+qZm3x4m#PbFU$J;y8@v&kHI&GGIJ#typaETv=e-S4Pw z5ykfXr@N_Z6+FZ?XS?9<=l8h%rvD6n?RQ_NC0~7P>fEP4`O4@&o2c1Hy;x`_VKU2;cg=B zf`#uS@&!Nr#r!qDK)!O?^6WK-R<8S-=y$(-T>iHMpZ)vC_Pg!P`xIBXna|Ia5cxcA)^6VW_wU99Z2ebp}Q;8X9zFY(=R+3pA4zVEpB znf?Fcf{_`;HwtqBJ$6j{1eoD_2cHhFR8*$ zyj6VWAMd>P-v|Bv*EdPs&0|^x50O}?UGPKKUAA!9eRIzr{-!yTz3PTf-SwRBnqO`) zU#$M=LGPwrtdVGH*SEjNc22vr2hmOcOdr1YYkD$ofAN?5(`S6+dq47yp7-hl|Mju= zDrf%JXNaa|eQ7=RU)rTru5`cso4cV0%lBoG^ETK1`L(Fy)c>%&u;=C*fBN6AZvMs< zL{q)Kv>qEJ?b4=3QR&L}7LI-Lz+mm36B_fuLoT`UsyqI;Vz~`iPldzhi=z$gfr=h?hKa`LojkAA|)iWSeR( z=iH^`tvTn^J%;Fx%Q>gmHp$D$TRJ;O>R!8UAaBtbTrLuj&7qaZhz%QYk1uF9Q!cF| zQL3fGr7BSblmrxEa8T^B9gzB)BPq219F>Y)qoQv=l4`V=8?p=^z!`)X-zYwW|hq)S9A=iX}P-*AwA5)}^RXbgb1%p9LClGH1VbF0Evw&b5fsre#Y$&CaR|_8J15 zz;@=-Q%lt<84%wvr;WBou_Yha1qvl$o5vMqFq-gas!Kstf%MtDw0gNzFjSV7?WT6F z!R^Uu6RA|INdk+?*|^jucc_&zW!hwQ2A44}3nS@N31pLus2c@$nlLM}p#o-Dh8vY7 z6!n@AmAq~PTNKHFZ&_btvSc+?^&ncSTvrv6Pp{SVGtXQ(w5j4^%@z5rv8LAqzT;vI z{C_dl42T3DM0n$>?1}_y&w>A!BEgy@cY#bcBausGTA5TT_scXgwNxsRYr7Akz_j7VL5&8lnzP%T$j@3+siiTyU0$`6U{gG0 zq}7Z(-!NNE0T-2w1wrD6GUWz&KrwZO0%<>eQC9*^q)ZJYq|e%=xo}Quf>Xh?IPR7q)bM(@fj z$qdp$Heqn)+<82Zh$`YCZ6#N>l;!bCS)GZ74F$Q%?p1=&& z(#o0&-ki13eV0rP-45L@6f6ZKhBpcY9|?U9`kYYk7UK!oYtTjp{tQ1OR3Z9H5 zTj@Hh9EXBXP$+l|3P1s&;8Eyw==6>s4N?R0L;iX3(<4g)H?2BK42l{H(7DjLLc#OU zInX&m!E?~r(Ah%4v(Q=4Swg|bLw7-Uf!KJIKwGSaP>ZL|1ekEe#fF_oHtXuXb}}}1 z#M}`bEe3%{=MJAcywiPyz;lPq9o8v$P?zipT?kz$6ubxY3Fs3-!Mj5jKoBPTclepwlUXdT?UysJWwrg2i)7b4wlYpzb~d`V90Lq2NX6D(EVq z;LXsdpic<}Z-TCXt`G`72)Z1)TqyWJuoS*bDEI*AQs`2l;QgUbLZ1{0-VfyIzC!Is~f+f%wpf3mo9|c_pT_+T*fNq6u6$*x-TcBHng5}W7(9J@@D(E)oHlg5SpgW*D zgo3rum!U5U1s@B21^S9m@Nv+c(48YAqWSF)tN+&|cYgna9deNKvvdrXLT%^we|D`o zc7Fe_@r|9||7%@i=lB2CsIl|=f6Xx2`Tf7f7(2iJ*T#&U-~a!@)njK(yz~2i=lB1b zh_v(je{DqC`Tf7v8au!L|Aq1SLsAuY|Nqbt-^jw%3&+eqIiH?~=4v48-;J}eIRlQz4HMs+2u?ua+r;Sj3*(NlH^$B$+XQm@oho`mbfIXs zk*|ZoT0fqZ^u#E2$Q&t-59Z?IZ>6;uojSK;!T0#aZ@*7jNr94=LCLJ(lDG1Ob&Bu# z7`(8O1jQe=6cNWZ2Q0NhK9{#7gN?eWW{Bmw+2wh~+qY{@3TA`HqywcwymoZ95L+DL zq4?=52~epTv?(gMP3yV3wzjLs>+$SL98_`QTtp1G-2}F4+iQl4Lp&>YudtxTgF&Oh zf*ZwMMcUew9xu#eD=|>;GH43m{}R}gt?V+3Lp(V@WhL6Gi4+F~H(^ap9p({jk2mP? zl?W&v1`P=aZphkep^HO2OAoJvL0u<;-Z))wQ#R=T)h6|LuYT^#N(j_**r^e*UvQiF z@zdk}S$7hX#UWm~pSBWgS(p@`Cb-pW@BMG}#U2meLn{HUxu6SA72Mo5qs!tDui#Hv zIUUr(#k^AlH)AW?uMu4Js#ULE2r@GV+5bl2dRkYX;~LGy#DXH>l}gP?JM&O|4mV2*!Zeii>XzCAf|?(_Sh_ zUn(TsWTCmO2ZO&iRAL$DYmR~(>5HK@@hxW=wU-4N6U!4(SBb_l4=D!8`xf_n&>gY1f(H=;#w zB^wxV2qx9o3JGfD1{NW>#x<@u1R+9v#Rh8P7Gq|?HLVMPLvSR3Rgx7{#;uY}f-4(x z(i(zUA+%!Qv;j>w3f6|7zlNY$IBkV!8JH9s20tg^r?Pbh?#z|Fm>1$JX1)jYgZV8t zs$dA}&r?=Rpe}Ajh6`@Y8oyj^N)Hl+@QM)>&#l%m!NqSd&{nJI!LHDH|34NUnOT7j z0sqfJ2zPQ`6XjG?DR&_d^FEzjuLk(T6X+n9n9yI5?6m1PNs8j{s9O@ga z#ml|0TC5nN&b$Ln`a+J3-tP>0OSxz=>n-c$c)+D`roprFYB-|OVTxi4A4`%9R)|-~ zppmVUGDRkq3KTLm2^>P@c_gE?OEnTVq40PtrY#%1&l^s`4PM)=G97c;6tdYg|M-5e zSvi6CPaY7=hFg#SX%Lk+arJKkPx3bfBazIeLIgaz3q`^$&DwuXM=~WphvJ^$4G#G0 z7znt27x1ojd*8^ZWia==Z$m->eZ7QNS)C?VtgE8Sm`;|Wg9UdIsbS&766sBa;aI|v zaws*+n(clg#~PC*m{Fh!%TgK5Br=FnYcl9yBNo>&GJ`59D|mu>wK`#;WR(VPP37bs zxl!)G?IccC^_61Uo--~jk(h#XlNG0WDPP6Zm>@y{71#7L&m6lj(A2irOJZB!$*OzI z+k^W~=%`g~J8C~@8jkhsB_G7I*!4B^e$l!1;q7qzC>z9*HD&`nx*^Lq7>aAF4CmaQ zWl~p1)g@U;u0>;Uy+W(jr(G%7MUyzlM_}y-PMZS(EGpc~;Btn08w}$8 zF~G?Jv53FK(J`zp&AFzZdFF+MriKeLmrbn+GCjO?#|0VX&LGpKdD@{trU$cCSCG+B zW}Vekz_3PzG^WjpVneCI(xz0x=kXLs6+siM&Q0M?CJQFd7AsR%klFTo?|&)SY@i5; zI)Fn9(1NUsI?DGGb$Sej_Y-wwDh0>iv|-dKGO2>o;Z@{3ls*x*;{{dSvm7*4>=w66 zhhrLr!7R%cqlt3BwcSz29i?1BL}tyZ67`ziC5;Eo&I&6tDnn_7v96+Fqo<}&Yn90g zrAt|(s5uodM)bC&a7>3b3dUSsrpXdZQG>&~v{d#O_=z*XQnaz=5%d(ZtBJ<_2j*iO6cnlJaPAgO#D^x^7x(O zlCe9+mPWrcdW7gU(P5x~5A^4(tCHi92sKRpt;Hesr3P<#opn`WXa$DY4HTk4yrsYp z`@zE`D-xmrJG25jxC>%KE3kvRAlg!3h~4rX-35`M71+UD5FT2A9oz+>wgSTGlXr9% z1cz2&2X{eWXa#m~7o6TwV2E>-9o+@~p%vJ{U2xja3hdx60LfRtY&b-rJGu)_8CroI z+yy7M6d2-!Ysan!*sFZwBuTB_3+Z(rwF>B!dTQ{O9D5Z=4l`6$J@##cRK*NgRSz!M z!Bw4gm8U(3gtMP74zZ&=c$4?6t3di0Fo=dY@f&6fT!;d$p%oZn&t8ZEAhQjyzz{s~ zJGu*;Ln|=EDTELUfTTG?T40BE0eclluCsP*_t;e4Az{QmWL57YjDTD`L$zwV!w7p7 zNYt~osvdg;JH&t>i_cJ1y$=I|6hA{%wcQ46^Zy%0W~Rl8QNsuN5x=wW|GO4AKi)i^ zJL0~`FWRvSt**l|4cM&P=!jb_-F6>)gM*+Aj)~j1n7a2(2j)WN1O}fGci$uCq(a$D z2^{ve&H`G;(jbq0F`mtE=QN5I0G!~LvIXw%y*N|1DVIBC=+G>4?C&4kaj;sc)bylG z)#dEkc(7PTrOUL!i}*}tsZvQ=%Cbg`Ce)UsyRJe4lvA((zc$aABSEkSauh}-h4|fR;IL$Cy6r7U{^FhDy zExjRj4_XCu9bvDF9NfVB*I(58|1&VQl6m}#BV()1Awna!TK@o7qOd~7-D2AC@ZL?9 zR8n6l=Se#2I%}ljk&!5xOHeTji4~0*r@R!&sf~`bPvhP0!~1ZBwdXa2&ZM)@%O)7t z1VG+pHJdZ0HE2;$QpnS!B10yVu(z6Ugo3y{DrF*iz#FAqD#a9eIpq$OO|(ubPuB>z z%3^tDIV|wX5a09-BHa-CQy}P#ao_(lBe9YBE9Z`!`SvtD`5JWA#BSrajU6xg3Haw7 z^>beAyb~u!Pu=|S5TIW)Zah!J((cb1w}U`+q|1Zrtr$}C%4-#`TrUlHv}Kc_^=FVU zT-yj^Sp|t9Rb2w9T4`I(XKao0_TQYu1at5yuJdgZC4$q-HjTTwQgmr&SjyTbb}VfeyIC8BK?jrn;?e z!F`1=>~LG9ZLBW?uyQ3>OS|hnC#v-|QhrITU{|EF(nemA*Bg|@YBivu%3dEGFOqD4 zaVZ*ABNfX6Q=hPEYC|tmcQ1)h+-GyUObF)sFqzuc*iojA$>@~N?DP0CRyfD{U~kEj zcD1^t%hZKB9*^1cxJ_2byL|z271jND<37*jc#X5AJnwt=ZJ!s-jg*;GhJN>HmQ zMjH^RQKoPO9WGfBVR1snHqx#n*)XK4oL3*WYHGZfsg+%(wqq2HQck;p`B0gf-a)3; z$#5d6w7X=iCzvpoLyTD!^hh@_HJT6kO&VPQCZsBB#;NpY8?cLIQ#Pp~VX8B5G3?aD z>UnL>j8L4Zk6kr2*2~n2E>l}6j}6g#F@~%DLldiQJ1SOVp;|O!l2I9N+L}Sj4n`@l zC8-Tetz*c%RGODrkWdzJ5Fta54&;<_rP+h1N+<@TU6OJ_#RO$!%d3xBHMOpnso^eD z>s_SRW-wq@>ceDe>yC=m;8vr}m4k^)SV^XGHCkS>g=HHCkfPCLFJ|3rLaw4UinzXN z^QZl!#ue8{!W1KQ#aXtL&O1#01j2c>cGc8qFH_6AOl?OzCXdzVH5xxurruGp+Q0@P zu2jh@OU3Ps9tjx3Wt$7xz^f4=VJijHv9hA7i8#GQicwUFco-=$*;pZjVU8Z)L?FJJ~qx6R=R$F&etj4vDu-w%M;~KR!=}3oUifSme;bJdS zmj-gasKEl#N(L-RxTI7Cs?m%x;L`+Px08^&-L7;smJ3!}0YtrO>XUkzTH0l5pU-Tf z>=uOnP{nHN4vW=ZRRAwGf;t~&cUtpS4_h`T{~vqr0_Qk-)rr>2_3rL0c}`xWCzWT% zQ<5$Dkqrs5{E{udWm{5a;@FZb$&w}6vSnK{lQ29I+MNV8HzW%SWC<)Jxy#<=wXm>D z$lWBc5V*U*LY7BJfXnhu2q7ExvT#4yuC6L~c}nW(nSmdv`Tg8ARdvq!pL4$ReV_E5 zbH0m~Mcki7__h&LVYs7nrmbOgH5(iYS`{j&1(`0ZSO}FUFR$ui0&^VxEph~`waTvAvfml$0zfyB6a8A^&-%Z!w$ zL{Q7tdJIw4GR3iEqC(E7f#;W@nv54(!PWkwIG-SNLv}A#2J7rjEyR(U1=J3R$Vp?21sCZiypW?Sh||>sE9y$*N$e zEB;0fDe2Dh|JM3*)(+pc``oSfgKxg_1uXD(-!05}7yORrt{-`5RdRV2{nESOmB-l^ z++2UZmweCJyIl_{m$Se3kWG2DF!56&?zTli`>5cGHbS!@XkSA2mbhk%25p;D; zWDE`5C}{(x->RBcQo)HH&gN*Hf$@wXM3H>qxL`GV>0Uc?_eVKdaE8;4cNDQ^>h;nv z8(>pXu0TXbksF$xpEQXZVk8ph@X%209QV3EaLxCu@v3nPY7hLQ9491_aq)H zgd^dV2Max}U&0ItA5P*(ppyyKMx4=sWdcu>v9XyGD^jG^!!q(Q0$L0Jn0Iq4Ff~fp z?dGt`l?XP28XVQ7^LSrPM=Yge_0?mmqlRS+yq6qivKd)qQc5XE@aa&q7#_-rJ`K0S z`NW_=)v{!WEEOsfXf$$pu;8ffbUaXQy}^mQZ{gOxEIaou?k>0_{C+Q)W%p)5^SUB> zINW@SBI!)N#ERys%iCxWav-(VZz9TsU>GpJRbp3Lb6LY&h0stjcpM?*T)QK+NVJj+S!md*A_m&gB&?q+rF!kU)wN^#fIoIwgd7H{JIlOv z>-Do`ezJaVE0x^tx8aL&FTCuZA37N#w*v%ZnR#WfG=0q5{W&w))35~3g`1LWIBT%8 zAUi;K*#xbNCQdt1=f|t26z5Opf!Y}MRq&VEP*u-Hl+#e?l8AEq*zd@?+YNRoh^{RE zxB6j;)pJ3ZMux(1jE}X7QYDs5l}sTvs&jqBNVW6bYAVRxeFnjpQVMA^h!$EfIpQY0 zBo;(Ou|}65IH4rjLMKz;G;!QX+>n{1mduQlVtgWMJvBfcPx=v}p9^ao7j7G>QfCVq zhLzE9%w74vqx$0f|7h)zwQI(;&8z?X>dUTt)hu2RHEbkF0ZRp9YLPd^M(#9jD)B zVa@lu*XvTR)-0EW*f>LD;Iy2HFTJ$9s9octg-QfMk!Uv)%fyyw9$TM=fe6qVdd>G) z*CHOj&F1nOzLV0D2!tRJTV?5G;LJ4SU~ts@jB5<%n?pySAQH~yy2Q0%%aZ=KJ*J8^DDdZ+B<&(xIB327x4S zrNo-=w_Pji?oNq^rUAejbn{&E{g!LiXZjGIH1ml@5hRnX=QB%IG^a4&4=$Bh^Zlml zHt~yrc=xwTG^Y@d3T~BH^ZkZvt(H0v-_~r!wGzhk7{~x^mDha#-L+T-$0>8pSs&^CF^ z_baZo@;ae|c9>imubdef=>5yC z^*-0g{T?0E8VIHoE;HO_bGcgFZ|Lj`W$tZPySA;ZuF%UCbWq!`Jm@~oV z;j)O<^hH1vxGZAL_p`1=dA%({n!fNvpHH|Ja%S`7;Sz_(UO0ULkOj6Ig74$5WnHk+ zc(}&F%MqYDeLjE(u6$VYeatnyr5%@t9$(z_J|GU<`LO2uPp-vzzVyMEK5uUGHQ)d3 zTFM1B_i&UdO`i)y&O+p)u0@{N9eFq`>P(*lM9nrzKjT`|xv9^?am!|Joe(BnG9uyB= zy#J^BKY7)-`ueMfSH9}-Gl&0X|C{$~`$y}qT>H}6#}0mU?+bfBv-hsO2lrmR`{mtF z?7nBWwF~Y1?an86zGFw)3GHlc|MFqx@OcM+u>CK$we8pWKIQv<-(bt$N^d>qTIAaH z)nC2(fz99F{Nc^7+stpiaO01!#IO9ul@A_1vhfoeZ#(!g0PvD8cvILo8s_s8BdpMP ztgcR4q5PN{CMw|wFDZ73R(Z@8OSnEVQb>ur`iGt={)cCZKbTPr?0P+6)Z?|*#1C6& zHDAve4e*A;&b1PRj+Bdy))1FDP~?_)MtM##p2*UqQ)R)6oK{k1DS_a75lE_7Bfppy z$tG-<`(3)5u120+^h{CkOp%{cj0$bIC5T$OoEsO3HkuxX({_o1$VS4Z^hB1)`29Ul zE<;i6J`x_|MWP$5b7mn#MG;GHhO^m3EK?~98Wrsn z!zk1o?ZKWYAkP%Xa|#cd|IwU6VxX9TC5%j`uBiTaUzO2_SQ|7FlG=zyJ6N2wx?DCT zjj(OjGX*oJXxBz?I6e->QMF&Iag}a0!Zt&2gwaFAkVpk+72#mAZ`DRS@i|2+%7IHF zQG*;cc)KEwWLcdEgML01P9+OOipXa9VqZ_`^~mN&JX8F%XNvzgr=XFJ)C~k%xp=Ub zCsXwl5erC^)F^k=s*1FFu`b%F2LdEhI$Xqu2a0)o3_Vc%<{VWmSx>ejBN!=#?FnBm zvS@G^&$f)90<{7$ogs>tmB~a@nc@p`ib^ttSrr7gCgtL=(5qG098*pw zd-=G8v zYzKJ)G9|u2mG&2-!~?}*lz5={4|7z-F;XMS5jfKu82MC@QyHb2F#Q?2!&H!QYb+0_ zQoC52M4`j)^Gxx*bBZ({0eg=ylpV8b6-_kol8ngdWNbkC6HKbv9>iM%D=Bxg)vbNc z6nma2cIOnqkVQ7CLLM4{TF_D4N|nbUXo96n;hIuztE7TqSZ9bv!?}a^dZzd`&lK-j zQ1}ZGC>uj_xj}?A+tN4_!Zoc!HOp9;Y%_5I;Yfr@bjsM_zr6Z^Yn?Udk}dAJU(aRa z8Q30_n-eQYvK_k`@SEWn-D+bIxMN8Ws4DFiY02Mdj?MdiW=_!tWge+`C0#&r35X~J zt*%6kQvPUZkj|(uA$D3qrBl?H33T6LN9KVd<5>nVrvPOo@lK}S=XFrQ!$^=~l8h=K zOfJ{6hUtDWDD@Oh73qooyxyE5lF-qyooZGx4X}L#t4*w3js<0*4#OJ4<2r`6yJ$(R z>$&GF_C6jc7JDBL6!Ts9u!k#Ixfa00B+1q~;2mBX#fy1yY*j)^qa1G)T2eU{$Yxpb zs~(x_0r!+jAgKfev7rx({YoZAVR`}-iX-hIs@2y0p3Nmenfo6+dWgf%`?R}%_+M6XdgC^ zRz+=Fl@y42Rf3~6f3VlqCvB*P2K*z1p|darJ?n;NidTE4c$H_0SAq}@oD1Vi|7R}4 z!zj@_Q*@pD|Lbetymsv^SO4a!edWtntivxJ_76UPpn)g;&fXvHJ+k+V-S69tfM@y4 z_NTWW^nKo^`8Kw`ZtGc_@7{d*#!qfU)<3!a=Cv;Z7EkC)o+3Wb@^JU$EqDY9p-_L= zBh;lg)u(^3;>`c@BX)TjT#~hd)v2r%NBeGM1(sy3V09{M#ZkW-S@4pqOOEs-;8+DM z4a{m{$;g)> zK$Q%np06h`FQ@Eow0-rGwkwo5)po^+yBpuWYOd|j{l4|c!!f)YZC|;h?Fwa1wOw%( z??&5K%(eBP!4Y_BcYFBmM%$M!X}dz1Q*Boq!@JS;WpiykXmI4=gx-y|M@!nSQ07$I z6{q!=Yb#G*x+H4_t5aDkPVe2wx_?R53RbsetvbhdBkTHkS*uu`%35(;??%>3mSnA9 zbt-Gc@%`np-a7s2`RMep1|NAiIlp|pe(Utba|9l^9>MMp)!iU?(IP_Kb3O8K*6s$u z3+D(ta6KBt`&zSIU{Yk1gc&kf9}D#&CNO-fkfroVRyV>)J=;xA>YN({FPJ0n!1c(( z`Meth&z~dkz}3seUv3cGH%H(l2f)Ljyc-11nr1Y51A?#w;b>=gSNLpZ8 zTm_*-vL5X!K}+rjdPXQr)N1uk0=%n+G%T2vdoi7y2qFAtWAx zQOMqzSNJrA?gc;=m6A)B9zVHlkRvdRI^~oXmJic##S)~9&B{t=2({2sx7EU$ES3&Y z5*#4xw!#G%kZD$*KzAL8g-*SwV~|nG8$oFhG1@{j7VU%*Ly?Tiyi_%HooL3oL~f$S zaI$bL3Au1Blkdq9m7k=8u^Sml44HLhWR^Lut%F{PfdJ+*5a+62&<5Eibnex-Fqp|=h{c4EkST{kw&P%Mj5 zOj_lNX19Rm^S!p#!Oe*k8phN#NKy@q&1_W5wgRTRmOnGqon8Q)Td`R$fF|g@6)aIr zX@E~Otr(ib<8mh3M-+eC%*T>OB_EWUr4ZAxGG@WNccF;>!wg0DPtg~J;;bg%X^OzP zY_N9Dzd0Li&JOd*+rx&VD7^A6E4XwV8x_e(U}$EQW^};y+h%NlL_l5|A*JqwQC=`R=)cGUH!hRxhr3~ z^8PF7!@oHESBK2um4lChJ^(M?|KWJ){XWwRCWJ^2R#?N70Bpu1L!TYsB;|GhT1;!YhJ?+gul0?h-&u zAD!`f)C;dL8WK8eez{tnm_9P&^@tZ#E1Z6vQa+u=-Xcr{S~49c4nJM1mjI(}qG#Co{Ga7i{~iiXeW;EF`P# zl-Z~S%yyAw2J~=94*itIXQE6sGp=KBrAkf5Gqz(FY`fv6$ns%=j!;!O%L){uB7G7H z^V!;B1wnvP~{M=sd*MZR4^rF5u(*-$|$HPo!E7P}>VnA4(h85+fF z>ck#EAxtNx7GPWOS(j&PoDT5-iZomCL~Y5Z)O0w5ICKG#9%Mk`oH1mqL0(Vybgt0| z!p%;vo9MDy98FkBv`y#`G7OHW>0rioaQT7aks+fgeV~Z3D1rvF-F}){(wvy~XT17ec!kjblg{;)BZHXsX1sdd zc)?Om9o;$U7&Bgm7hWs!n24!9XGmpZ)Gt&LL)0QeT12WL ziBO?i8|OeYbY^Tjm+v2sQd)R4s6ldllq;HSaZt|;hGqqq^o*s|*>cRzAy}i=wV5D9 zOy!y8vKP%)WVI`=pp}zn6_sS+g>29NGhVH!VIF|0-}ec zA!5qUAo5-yuE+$UrrZo7=K`XKOdn#}nn7%Nfw&@lhnhBL5SuO_dPvV9rf;4>e6tsb zD{^kAX=4Vl;R2$EoEl>KrWwRHd4aehQHGj6G=un%3y2<4d5GzQGl&m*fw&^ShM2x_ z#_NqN*NRjbYFY=p1Yh07gl|eP z*ozvZIUX|<8)i(nnc+uL!Wc=UXf+tSm!oA&>ZA#Rn$~7)YcAM&NWr0|)fwBW3$`Be zYN#nSV@tVU>mdb)npS3PD=yf2$bg}yENjO$#%&1s7~R9E#5K{~uku z_SUO^d-c&Pe{<#G!~b^ z^P*NPi1);R+a5(d0R#Fxih2SD+&V96#V+8~fKNcwrbkgvV6bm^6!iprwSG?2s@dq& zfKNcHHIJh1UaR-M^}MJRpii~BYf*1`?^|Y*u!jwhmkgoHSH`!z_fgLPJT!531K<(Q z06ZjgxB>9+41kBt;=Oi(?HRy5cEQ9mfP3tM@eF{6jrF~D!N@a!d+Y+sGk|;Sg5eB+ zhlh}R?Sg@40QcAhre^^6*aiI=01ppr_u2(L&j9YR3k=Ty?y(E>82}GYq4(Manr8s_ z*acnB0Pe91)ENK|Pwe;B1^b83TT6o`es^~4b9(h%H8%d(>C$)b2M0fT@b&W^fmaVV z^Xu=p_MM<>$(PpI^`G8+;r8o5x4=)$y9938PDj7}pPcsxJnaiO@9#J74!HhGr?a2a zUvTSRUwysP3(yyHI{sa;KwDz&f-Yjf>-CcE7U|~@adnqH{ z%|!iVkR7%}s50qBYI1%8ZfVBLN-Kp{vx-C|zDk^RRsfeVwP;+N@_ig>k?|Y{BJcg!g~cyQjUgoSKy z1!|)S@tn!fG|!IWvMqOWnSwtWONeZNZgk^ByVOf@haZ^BAmC00mr%BDBYv4u5~U_A z)Yzez#mN>QVX$~HMws73T?JL8kQ-)&C^w z=ctj%rfBFY=9%JaJyX14PEqy~ir5|wxLg=EM~dHwV6B$jlq#W~lunk7NTp)d>!Z;m zxBF$!6#vsR#h2z3p?I5<%B)(5XG)9;x{0zy-YQH4G1CaOs}pWmi0~=WWX0067n*yZ zSZMBn;`K9B%y1MzG_}|fqO_gQ4hT(8fu>0oPgXizG+@Qm39F6E6e)`bpYu%d=L?Fi z743;6M+c~AP3F05oxgERDd19hA*kgI5{`jDk1`=6@=yNSGsWD4%+S4zxd)kHC*C$9 zqZTO#n_7g^AT!&qj#Mp{)v0l`8B0lANK0GQ472NBpmI;~8qXB-PDzZ(LlrKR?&A~* zN@=RL74mng7CTY2IG03|AvlgtvZa27hPJ1386GHZdZzg183jvNR-%vF{&I3$P5DP1 z2u;*6T}2CWIB)4%3C;&dEY<8`%K2_99x~LX= zc`i}JxuRuc>XB@b^XJ9gc@H(#1I4_DniqdNCtd#p1h3hmB5*wDWbQmV6zLpuK z)w)XSBCc}5rraASkCLmlgZVzKM< zK(W|`*8(9`i(6(R-U%W}EiR0hsAw@rPh^5D(ydl`ZlW~FRF2sFv$-CWA=z>rAPMjEpteWuU~@$IyM32}x^q|L+k&nUd?06q_%0H30fh+86f912Nl&x#OHx?c#u zMKYK4YpqzlhsGm`p-_xRdlgJM_}01R9w-*OR}ase?YRsO6at7yk39xw_5T~sTswTv z-WA__z&Bs{`p0g8w>^4`nAJk=tl!%XKrYLgBNyKN?BTVWQ^xevcOvj`oO4kD*4ZnI zo~Amia}b=C0(!qC^PnTatiFjCjDZBoB;hCbHslBz2(PSwq``}nNjnj3QzIFpuws)f zW{e;b?W0*H5GM;a#*uWUo$U#1;;w75l7Td;z zej`ezvbm-RS3*ogt!M0XPco?(ov0*XDxK6uBR(7oCGfOXFeiG@=)_&sWX)7}Dj?DACrilPvXxT(ibTC*qB^K+^ru2vuB`9y_W#T>v(3rDiMNGuQ8J-#52`2mOOA zJ@#;fdr{z>UTpU?t$R&X1Tiy^x=tUR?gL#x)Ya|-7W_a{F#V>~44e-`7>w^HuO>MX zL?SEu>O_zEav@yk5gPH!u9R;| zG*gWhb@K*h@p@UbkL%iiMwFb|3=(`~%;N?X9@9(-!bu@fJ5ChDQ6$-`)asE0q=}^) z9gV2AdvLkZu`mF~nHGkcGckbnD6f6V!}m8up)qh8FWkO;(^1`7|D;>V+4}d;`n_#_ z=rRn$(SML(=;0XuqA+wWeSDe%a3&hW-t6DA<^Qm=6MPb5Ef7Xm*8gYPc_^r+29!aK zBsnQYgbss8EZmMrrkLn5H|j8X+!f(oJ95{tR*jXCXxR{Al0sTZh$v?%K3}s735AWq zEu~UDhRqnv@-ie8YyBSR1Wwmr{)S%a6`LVk3T08Xj-+Z>1u92VWNjQ^AlqI2f5cIJ zQUCvnHG1vZKfi`t{rRhJzPfYeZC74#_)CX9Q2YOH4zBP2hyC*2U+ukfF9h!O6L!9^ z^KCm{v;Aw^qwN>_zRUOet=|E+_`PWJ$2RjDzrXQypxXcD`t`LR0_@gm)8uvmTwA<$ zk~SKFS5&!QUMRn~GJbxQZFid37Qp?VYeybx(=V@}UdXt!r;{*^19`_lo`<4!*YZwp ztQ4m>kP`%QJXEo}mUH&H%E$JmF(B#{4*`|jSGKzr^>}wx1|euE@4q?494tU>4<+ob zvCM~zIQ`m%X+6}lUtaOI7&2!~%TM0`B>90h9!ij1OB(3imO0QhL72YY39cKb!L_QA z{_-O3g%%61we;3&yUjsg9y^_n*y+~*InLN$EYT5YMU}m4Ii~}@HGLgm?ga7~<`TUEZs`g#8 zf4t}nM59a5S(`>20B6m&qSW6tfO$whI1MiwZbjYy<%K~P#$jj?haO5eFHfFdNV*+| z)hPgc?s{q#uIeTAWdJNQX z{O+M^fNM2Qr(SmY!1DN7(MG^E{8Q%Q)DM(!rd$tw1zam}c6w}0Ujx)Qvx2NFAa$+A zovsGEr5jGXIFs=m-bIthdNPf%oWe)Dsg+3fvQHAH4{o^n6kvkUpqb0*d?z+lvF|tqg-Y;Fu z1|Ew3T(87uZ75FfUr6#$+vZx*`7pV@2on!yey%0W2b?f{36S9|1s;wyT+29(n0;{C z9a{g$+Qu6;Hr79K?c>)Tx%Q%~pSk*>tL>{du6**!w_KsGyx{Othu?Mh`or~upE`K> zAbzmE|A~EZe|PT}_TIBs+I!CKkMG*MU%R`v^NF2XJFnUP#qDq1X0~7G`|rN*_Pqhz z6!;5U@7l_4J!A7{H>J%R8(-Y`p$%*O&Fe2)`|Y*2I0xeMrNan!d91AQsJ7cqI~^0x8m*ZIJL;-1CXK-l=5kPpnZ?uqAf!p3jB)(3|6 z#&x+EebI`kAS{O=hm&WOC z9^)#NRJxYyw3j>1EkK{0n>OG4CSa(CfW|I~A9wbK!b{%WY@`8u*faZJaG8;C;u)7X zPd&5QJn@W6(mOq~$$K>M*|E6Eow(Jd{JC2<-*h6AlY~k!CmKT691(m;87<SF^7xW%F7n1Vo_ND0@#A_U053`Oz5SlDUw#ql`-T%|yF`D++23^H zY?q`LIQwlU{&LA;;jgcc`oQZ>F!7M-Ov_A~_OJ!+R%^xc_LB7${<{6@WcQ0Fo_0xj zb~@ets}onaBz`+T<)xWXm-Q&;T%>4_^`lHTcx-9I~Vg-aHT_34jJ44M^e z!pclKO1_tEPLiGBl6n_}&d#5m7}O>4;~5l*hR#V0M0dV$PuZjA*hA3HpPu;3CHhl; z?R@UUUoJ`S^w-YkPyFSQ#oS*zfAR)jDe8odTQcVzfy_14MpKoW=FXode5F^t%02yz zN-$L&u#j|f@Y)Et{(0vw7NbV$OYuyrOIvhIwzIk{XP1n8!Km5&{KBnni63v&To8tz zTe#IN`%|~>{^i20Zb|QS>+TmkTITHXxBC~1G2vGJB8LC`M5b0M#DsB@F|3R>NZ=BC zxi_|JC*E*L{CM67hL@MWZRW%qF4>=YW4nIh4VR>MdSjbDF{n!xbAxVECkA!-fOK}1 z+4!v!gSsSsJcGiK^OGV3ZT#k9)9&`+ZNVO1-n4Ig=EO5Dah`f+xg)JyNhWF_={7oo)nQhZ`eV+jC5?nis1k`_as{GRGPbtk4s_6d!Bl0EP8I89Q|5DAJlu{%>8x3!+6L7OQM$>7Yo>C9khNMVPZwc++(Da+s%Xhx zZdmGfhHZr}wuI3j6$fXzrVi@*t-Qs$EP@vni zXqY21Eh(G8ZyYDML^D#d!~J46M^JeqchpYHEsG6wjxFZ6)#TDTHyPH0F@nw$K{ZSc zZiK4sSXaw~g2d}tA`KGQY^5CUXGZd5G+N#-?qyFgh&Y`J1_~LOZgV6H3ZgOzh^U(? zhjMhEvg*lh>9~4aV<*H>8I;w7GAIqcAs^>xnAOn`6~2)}!bL5SuJqy}bR!!z35X3b z*WsP8CxtS7{ z)1@|%!lm}Gp%zPcCRHUuA&ghE!w9PfGe{#A3ze?to0^pG;##d{blGyIPbHTy9Mn%S zfG4q*+D@s+KuAiBs2~8(j!i31^qYQGAC+2WMDHNoV$3|k8?`V#2&Ck(9jw!)J*j9l zvso$*b4`B)uks+tP%p+>DVfu+7w|+H>u?3So}bi-M1`m0mUW1FezkRSGMB@t7YIt%{n8O|a#=Jq|WbF|d3Y!*qX+Bhu*((aZXaP``x-W8tGWjWYvW;!Xtb(uH5JgH>tl}3 zPcr#rf(OOm$q8pJ-&L~TyY|TX&eGKRkMVy`U7=32a2ca57EVQ_U{j)ErIN`@VImMK zr8!DxTOE1S55hz42(48HVUVpFln1?Wk!tfH2}zPD8khM}nTQX&u~3SSQ)aauxsJus zt#*}H(s)KOb7r%;yj&dqz$pd`?a_P+FGh)`ov4qMgqg6)R+)xla-A+!`&JWH%0gam zVMnnlfhAcg#P|V>VweFlW?Z3SgszD>4i!^rQ>zzZ5h+VE*R2VjY*(pFmdRA&O+G0t z-<09|&QlD-NU@O&mTW#OSNT$n=4hKD83Ms4akiC$pBzk=YBO+3|8H4LQnF|QfMooEhV~tXl>~MH$hMwANrI>2CNur5iY!d8%#y$8* zpD=8*Yo=L0ln7WxM;OP&qiU@a&L&|<38++JjHZK7lUKz-Dc)`mT2R4KYivHsG=d|r za!Z$Oes|8y)LLD{wI*JO_fxVmsvD`ukRaoFv?3W8)>23V4x8~B%Jzt(ahU9pNVIJ? z6;(IPkg8|k=A@CRw0LzQiY#Kuq#d$ramBn&i+U?3f)$U8mor+KCgV$HSg)SiK||?b ztc3dUnx6_AvCKeB1Rv2oML1PnRat;%dqA)6X@6j04bwbZfGnem_ql_rw_E?AIc%3aQuR$(G|1BO zJW^9fbkWq!BeRg{mL&+QMwMW~pc#xcL=43Qh|!ILQp;j;H<~Ht@ETmbE>-N9Dfjte zJw2#(nj)<&Vc5KJioty^`=fh$RO^FIF$m43luot7lSV7Y5OO}&h5fpcD?>sWu|gzN zi*#cN37N$8qY#v9!;Lf{n2lzr#>KdNB2yZa>v9?G*mAjF?d8)(4}#@%?s|1X>IB`j zYst}s>&B{rv}A{k?>WW5YYkQybjCG;=yU`0WY7i8DdMeYT&1}hE2YyFIc*m}=M(d& z9tcVtH;P(?Mm&^fd$Dmv2!rx=!@>t4t|?mnQLLIOjqI^_y_K)f6{^V>auU&JjGUcb z!m$0jrx+MME1K1$Ffj&EtEV<(h=*}7CXhllg%BkkG#LSpD1n})9Oa^UlpnY7kkm-k z%^(|;CC;xflVEQ!$W=y@c4|zS(U3ud-Q?vM{_qq7olaFXq{(D5g^Ec>BYHYiqQa0) zPr$u6V{BaM45*$3afRxUhQLrHLt&JMDNNX@IpLYKE&YS_AV#r4AR0Gz&VLlygI@ zC-v|O#|KMFFiq9EOr`BF34Lo+gFCtFm42>d#B<4VB3sTSNYHO*$rn3kc9T^}4~qh; zPKNP<4QhW#A}MB=ED~&1tgJOc)Or9*i-p)YaTLsC%+aXK^)kg01T~cijx>`xMq`tr zIT`6#MUhJlb4aCN<$5($OAgDaNwL%gdt5USUp|uE#lW`h8=vm2n^XV);?2)(_}=Mz z(ZO@Kerl_-`MGQFz4n@`zXEOo{I)BR!(Tld9{m2nTlarrzqt3;dpGx9x%uU7Cp{7Ruh4$Q0$;JfDho7k z=AZo3rJK2@Bz38AlLT^{BwP;#sIF6_oP6Bw%?yy>B$#^W&+1yjoo~72n*@;OWH4)`h}Ye4nKH z1@q>cfo)!K2y#lL2Zx*(D5DZqiQ3JOn1or@e&1nKvK2SqEA_ zd1VYYYmT~L_Iwh{7;aXV$KO3HW4K8H=1)c$!%hCY;hhER!s~Wj*BP8WF$b%1*#Yhh zG!#^@An;RN$1q=&OH0ANP4ns8Jy?;&5bXu?cdz~ zYw*ukzP@6Ce*_kI`&)03JB3p7T`%3c2Q!t+wlWd)&bBgdy#G}Gih?W9ljY(2oi0RZ z$mvg%K>1c6g_gU=oEkEN(PE)CuBn0`S86sKY=xWsLPiVs5|j?z|L8*xo%Bx;EJ2$! z+j-+tA#yM=gn2V1r)@reKB1yz!14&XwLa!~!%cnRFfhmBF?R|&W z*RH6MTrOpEs5o640DS@3!}rgHgL~i&hl{g7IDf&*rlB?`C!J?j@pk@nUXaz8w`kLb zs(Q9VO#uACB@Jr40H3+3Av+C{&Wwx|wSW$|yKVLs2|z2mOcieo zO~v407w+2Zaox!3WwPH%TAd2GjwPKGin+YX3{q@YphdO}&Mo4jymA9`*JaUB{n1C? zet0YEOabrsnln?tiV(bfGvsy(K<=CZE(lTQM@u0ze=p#8{A_o^rznJGA}yhB%DU!^ z%z%RfjCx++1k===@O=0ju4c15DzSVXQ{{3xX9;M`dE@SbDVZE0qYnPm49 z!`ZlX*~IM}#@l<^6~p9-#A7%RUD<~AD77y69c^<9h+a$_03WJ(MPA|t&G$3?f)>> z@9p;5ZDn}h;_LJ6oe5Z~ADC?y?tC#0CYXrR z3hshOCGacIGvV}ir=`tHM2n^VET6M4te5_pJ6~-9ev6(FGV9W^L~;K0`qD2?-}o&3 z!r6~B`a@N48X=y=wrEG|{LqP^xs@8c&~6S@XJg}_QDja5-_DSkgXTau^ZVHkhd{2w z_+j^xq{D6&v<~SRevUlw9`r<!%n*3P z03=-4ur{qn6C>(Iw-znjXhxwXCh<}U?;50@$twvlsnW0*Z9xNNeD|F)QCVpBi(y1* zSJM&JWT^rXZOTTt6{Vv}W)Q5xHMGlDg-#sO6rnp1DFPSqf>1{6L66Zp#bUG=wP=td z-Q;5Gq$XuqGB$EGXD(Fl3t$rq*5Jm>`@jL-W~@d3zx&o&YuCi9KX>)`%DWFge^@&B z!GqoXH|>3N@4nry-}&Voc>A5c&->`DAKLn=&4)LBcLQJlzO}ytBrd-myYH58ZEd}9 zTv}V(+gKe1}hz1-mHvFo>Z2izM@ z!THXDyRaYG%4zCykUj_A&-}Xc`BB_8$QRt=9FW0NkXxr9SDe}{K%SlAE{C_}0Q--b z+2{Z80I#(KZ}Sx1ic`M}X7bD7Z92ey^2Y4*k6goh_N}G^?-i%k+c<@{qMzCV-u&6| za$p+{q#rkCpFi&!*fVdv`P$mv0@$7t7^k>aoFgu9&7TG?$K?R}p}%xKf8?LKhV_D5 zjU{(pIg9JU*TomP^U{ZtD>Hb3+2*4t z89h>I*%M-%5{>1A?b(X){y%Uqe(xizFy66ZyyzC|7%+V5*!@!ju4rqw0Cs+hc)1bx z9cb_WJ?HZS|9cg{I|Af2w;0EW=aw`NXUYq#cW%NjZ}c@=-M;;g9K7H4BdhS5{|Ao6TcJOrIYop1M596n}KuvH1XKW zZ&e*5p4qu{G7Ic>NaTnDI>1?3{*m5P|X)Sr(Sw zVlCiZ5Gt3NbH{=A&QCg@-}2s7AnzEHFS%85%z0*h1X~&RNBRrAZs*6@uIA>;9gOc7 ztipK5cznUFvSY|IF2B9qU)im20qopn^->egrtfe5DF@!vUIp(C6TbLX32eF+CY*1& zJY)$hfG+l5mqxUM?VH-p=iA=D3gI0Fyzf@gG2oeizWqoNj;=^DSl~K+`EmKIIGe1$ zNtl6izkPW2t%3vYbOi3*eiT{RYjFW?{zl_+V6$oPu^(`NeZ70XzTj4VY1rL<2nnz1 z)wlq5ZhEb}F z^s8=xIsN| zer)sBW_k0zjn7mk!&iG( zBUk9T#x?7)>sfQi~_)a>RHY^B!*btk1(l@&r-SP^C*q9K! z-;$!;K9;W)QZ_^oMTHh?)viz*-JD8e$yRw8r4-EUnGLUp2&Y$efZy{j4pb!^5O zfeaJNBSb=pGr6+ajLXCDFgQ-k3Dur^?`gKtAQ;a9e9BU}GNdQS!x_AzT zF}=fP<1C(|kOkq-EeNevm{1FJSS?IM4yF4gGELXaR3GbGiFhZGp5&A1YF@;L3#bP_ zc|w>=bxdA}Vr4xM3uN&qRi_BJZ1X18HuOkL#nfImSY_?eT;l#S7KB5!HX^z)Ma)o_qrxWh-E5SDpHj&+q(b<#rO97O6hC`)v+*@P5O z#13C-U{W_Q3JTSQMD~O*(?ykv&FOeMhNn8wUNjZ}W%w4!SIkj04s$KZp9Qy~qNRm# zzO*1Tg<7@V33NLBKq4$@5;#qvM&R5rnu{*s zr#g0t0Y%(V97*K-v5Y!On}d<9CuG$w*OWQo&enpkSIG;lXkbznMlrshCS?qk2Zj>I zl3XQ)pfxqq(FLy2jxMaajh_%EpmLK8Sc6Cg?h1rBo&=dDRfFxehN)093pM@8`nWcZ z=N86EE(rBph%@R1MuQ6Z1U(5Bx&=02;*(eeHqb5=X@v4cYup>+*qre2t4|06q|j+c zv=-J80s+wu#f_YiB!YM?!9gvq6l`X)bcian7Em|-?Sc^8Uycxi9ulYXt#GUrONfDN zswdU6lR`42#8s@ML}OSr*jQL~=S2%bHN!QD0WE>~Tl?;X zM5U1r*04-4fe_J#F4B2$pf=>qakty1`du+Q7xp(26$&zLjSOS3 z&~g9W3qsx!2XO^VR}|C=cc5scYpFv;61ZxG<}%_WkSHpZava<^JcqiI34?hR9Jg^f zB;J5hMwN~ig5wI0ClbNrIA{4&qf8^;scY%BVOAS{WidUf3xO;gDCaYHBhU)=hNISK zP-n%g(v3~%f|X3jmeukrVJ1=!o=kEdS`adJ47CPToT>NyBNUTSEEaU;&B$AkNFx#J(Sas}1q>n75!xp@?w!nzRHt3;w<9_^%67(v88xMTD$W?S zRs}1i3xPgc@Pl)4VW=%8^8IgENUU&0ErXQ}yC{eWA(H5bQEqG$Ga{0dl5D!c4q23x zI(By9#?61XaASFB_xl|d>%!R#*@)9ErlG5(UrboVzCI`j9X(qrDC1sy5ml{)z|tg{ zMGK|Wq~3wZNCZrphEgpeUZGFHiw<7_-FAdpJ7 z>TNSpZj{G;aN+hOn@aWJtTN#jgqz99bU2tkQ-(hQ`vQXP|XH5POf$PHK-Q8A*`s!tR-qZKnF|0GEeb2n~( za6w2?cB=y!iBKw}MunEu8)oeYE>_0lXqin*b=uGJg`nDm7KH03-pB^~^|FMt>#6pD z$Ms|{He!M$sE4z?G-{Vd@IbETM_C$KgzI`YKpqz}*-5@PEDnb$vlwIYHQcY4g7tjC zTDWl?T@WV1bhE;6B_bP+cY8dc)?u+4Dh&M;8EL3pJMGVqdQZwD3wLb2azPkZT5+Y8 zqqV$*mD)O1sw#1zVstd6ZRL9<5s4vs4h}^o3wLZrPY7jmkjy2!8gFPlF;AofaCOX} zl1Z~#vzgX{IF8U+7F=s}GREZvAvUs-ej1M>jV3h^tMzEpCdouYZ=@3-W~iiTWu-z} zvQKt7JOA^95ZpaC4)Tf>Nkbt)O^Hw(=Pe$gDN7EGZQkF4B&ic9<`-+#`nN9#qbiKY zGIF>bf|(i?t_{JxbrsSdH~dV2l{J66K@=oCFbXfMdhqcR!g6+mw|Gdb(WPOpG!h$y zkgO;8T+a?MWLE{tbINFoc<5wxJQ;G~!B{Hz8~q?>=ELX^XKX(_P8j1lAxcbfsViY_6(li|h+=6!b9a&?l>7c0q_t zq^8)xG%zJG(vX1bli?^0D%1mQmJFnrL}fIBhh|JEE+)C%;{_qC1o1|`U1=mOT9%be z&1x574O}UyWikS%n`s5Nm2e*HeCA=b`^R&_q^(hHq$AWYC95hS8s|rWNRMYwTZ&b| zZMmes9?uG5HaT7l^36}45HkI7i|r-b<5~?*)a~M+2sd&XS(s$in3TlS@*sm`$yj!= z1>Ahs31L}k2f(_TRgz||P#?&tD3!x=ITn|iaJvYa?z9L>=K=TyRNs7dOsBy~RJj`V zvs{)Rf_;=N5Mi!V35)%VTIvOQAq^YUx&uXCY*rQJh zyCQYLBn>6_N+(1HMw4t{fpnK#7}b{iU_lbqW{kJvQ7o%DJ}@F{MDQ=jayyN=AccfoK&U# z9A0jaov=L|h8ZlDVY<~^I?znUJ2`S;n|0`fFgUK3G%QOb5=c@5oyCHDiUBR8qV!PW z2n@6glf|+j!}P-c`-_2-j=`WeN(mKPLb+dnOUW2JP83U2)sG?EaKezoMl}o?x}n&K z#P3;1#0HjbkQ0Qh!?{e$5Qx+y%f>4i3rz^NEb`$TYPK@bq_UXE_ZK_)bTkVI>JZKf ze7P5>)`E#irZmi_&9c_vn!`M71dMng*`&91FH9py!jWmbhx4 z?X`);P(3gggn?Mo-&JKJZFOQHYoIZWdMarSt2EYw#A?HYDm=(t&5!+yaNMEhgefBA z*JAw^*2`-M&VWt?#dxGxgNb~I&czZ+f5?`4@j+HX7RI^q;aUFw`roeY|JrsB{PUHs ze^eIuX8l&dNrL^>%`-Cs@w9%d@^(#v*`DRedu{unKm zk`yAKl3h%5L3Su5E5~@6liN_YeIu8_I$AvxrB#wgKpy;SUUF8#>io^K61-*FpO>)W zjoy+DMoOs2g?J%$L#-&~afBab224pM?Yse1!{J)JFDMy0NoqQCX)gb&i*A-g2Do-& z43}z#!KrmT%l`lDy?dDJT2>!e@2XREP8}X0kPslJ`|#*KZH^w+iwOkTmMz(mEm@Ln zxgSQBWJ#7RSuaa=_wD2|0XkIkPH0H@W`Jbq1`;y)LM8z|-f1B4g$x6EKnV17!Xtq& z5J-TAhS^oFd(XXf&%<@@X*$f@`sda?wbt5eZ!N98wb$cE%JQ+IdXvW;b7g89Ey`b44 zSXuK{sf0~D{GLPvlp#x`0kbZEmmRaid)aVB1jqooZz!pfgjmAT*8TgP8f*7SHpld3 zZKIsEi>W{N+`II%_PNXX3i~B4=cz%OHg!3~<6X|5PKWSvm-CTuo4{|b6F6T?(tTO^ z&_`;>D=z0U6&C37QE)fwvY*#EX*&O$`}dP@W~GiBtdmAxNOavW&-LL><`K64YMcy2 zl8-DV=+F`j8ajGmt=*K$i~jxliexFXCCQ*(aSz-|Mq>>V8W2!3BJz4mwv{z)M54AEk50Q{~cw(uDJu=z{l3 zGX0A#^~Jh}%L;{;78<;4-d8FeUYT|R-o2OALOi~R;l&Drb=Lk`pDRi}w5s4GWdJX& zKzKY9Vq!ABD!I$e4rPkAx* z=bk(N{)e~xt=r#v`>nSRPQUxqIQ_(1KX5C(^(i-h>gMX^XPKtK>PdlJq5)5zh$?x^Lsnr zwA0-F?d@;a7Pt4d{wWw@i+lIs-j38c*uJxvXYPWNYK*hYoDScg+gCpJUdW9gQ5469 zh1kE^AD(;n!G)}?>&QazvX?5a=myEXd;eV4{vTgQ7KEd1lM%0?gmLfQyO8xq*OP_N zdBeNfrBZqKE|6vIfA~%&S0cq)>2*U0{4h_LtNo?!YFSQ}17Dn4)L_+arCbm;K+pb# z$;;CjhAe9Sau^g{1zZ{`@7}o(`-dBdMMkB#FS}M$=gUdAF(N^Z#6k|Zc^r9Cbx3Qr zHk$FIQDTAE?F+Gguz}bahPDJmoLiOH&IEzvDp1BrGZ$sp3MYUb$VM!}a((CpwDRt) z3$Y*CK&*{TZJDfCUeHZG$#5k`B!yc(BXYafa`+FOR9Ys}LM=ZL~)Cy0O zm62EF^JUHMmT<&ewXt?#S)I&qsTvUb+=bZR-9T*KA#0tuOd#!CZ^<<#Kxh4h)P*so zO%{uuA5D8o8f8t4%ayZtT!{Ve8;G4Mq`OR~O94&;wx-4Hb`oT`#5*|Agz1*3&kM8L z%Xqh1R?gmjA@;xBskZCkTxBGsK3+{{{KCD8m3TMv=caBuvfrQDn7^1)^t>K5mct6g z3Aow_>$wz=K{c3FLgj3AA@+AR5Q}xAK}Qc@w^0j~8mvN%S^`0>X`jyoa9o`&aZOvIs<4zi2~LD;}- zQkvQ<`ZO$qH#)iy`)eDBoe{b{Gzo4%DyrKWT8ieEV;&;RRxcg%B{*6L5D3uK;Fh7B z%`e3M=MBWhwmFe7hs8Ve3hFr0$Zxs6IFrIm2&kG1<+Yx_T%{0BgI6!S5c{7t5Iabl zkeAFeZIQM^HVTc=2!-`&1lqR`gBr#x>ez$}p>l=-{}Wt@{naNYb>E%4SR+h>?szdS zFZ~WwnWyk_X|=f28q3ux$+2{=o3>`PKvK?T7h->91F@XlEJgH+RvQb@B4i-Uo$fr3 zJ2I1D3my+hr&nh1&J47~&bc!mi0$nE@{{YXUUU=?33j?!%wEN9;Lf}YS-*50Spk+km z+}Y$p)(5U5>k;*M+?jPD>ldGVi}1iE9?^15IWvJQYyTIXT#|O3f;aBWxKQ!mUq{79 z6sU1$;|p0ozqzbO7L_Sy`o(nr-3D*z3a=nUwKDMp4N<0;6I!Iz7;w`xEw`q;K9fwl zZI=Sa9y8q87hI_LbJtPv5oKG-+2}&WpWQ&kYgAZqXK%Yu@!wuY#YYrEDQ9oJQ1Rbv zpkjuCOxclYwV^!G8+mQf=oUD%q*;mRdEJT6do)c?GmIhx?(8iWD*o&1sQ8FtBjxPP z7b^a%4OARf7q+0|xZ_6)lA{K32Z2RisF#9qr5|B6lF2NwMny91ac9E|6@TVBDn6o+ zM>*3jRQ%};RJ=yL4Rdhj~s~F3})M@ zWxMwYta{(AFm$~t(fyz;D2ZWFZAx#UWrquN4jmY|a-TAqs?g_~!!)Hldb7~=`P83# z?u!>^-SZYzXUpyDR9!yVVPA1T)cIv{8e95o6vm+Y;d!_b-0YVr_>~Umb=T=}e9-~w z!lYgdK7h2CYwQ*eYaz8RPrDkFd17hgIm45iM_nzBJm-2=%^j(8Ve25svv2PBFvXuX zX9NMz3_#U~1}P+})hSb9ZDB3#Og6)0g7No3@svJTfn0bPZqM=6V(iUu#Cz%>MPteF z%|Wea=tW=+8F3*Zcw4Hvkt363sysLBWz`iM(sGI?l%(O6dc-xl`NEGY^3a_m@qE#% zBJ=uu$e{D${(L|Hu(i!zyz{ApE=BKh&!mf_k^Ck5{UbJxXSd%&uYBEJnnm{7e*aW? zYL7@S1TW)D_j?G-;DI%}KO;DrQ2Eer_i8K#nH8eRPNaS^&AURANo+}1AD*Z7(pvKK z7yYBK)4xmwu*Xlrc{6})VlKYz8KdYga5J8(YXr}j2Db-n>T)Y~kHft`_Y1r|)a zqZy`zuE_ghq37n1Emv@W+zA7}z)a!V(|3k#1R!NOnJqom>7j_rP}L#=X^L<_5EF%N zkZ8`&5y5I|i#b6{Lt8Q8f~#p`2-S>^Bt+1d;#fOuDDcckLufqj$H8VHQ3?T5f9|<2 za?ix`ZT_#1wy(1{KC^9}q+aQF9&(00|EDWw+;v-mtJeM}v=v{xU@vJBTRzw0LZipG zX??{`c^*rD;S9WCo7Shbb&!qu$ep#)Zug9VBM8x)FtFxW00{I#GsKm=e6JzY<#}gV z9k8>N484jDg55p~AbP{O0f%JK;#62})(k+jREft`-wrX!fC7& zO$i-zqh^_6siA%Pt80#_EhUkM*FjkwSsr4cIUOxrE9Y@g&0eNkWqE*c$QT+a$?4Bu zj*$#UN@?2b_bR<*LieVilK~?Z(olw&@juQW}yLRm8w|`3bR#Ho9#)*9Wz=& zG$3e zKvlU2#Ogh8^b8rA8B-P1C1!L_YohQ%$spEQmPq>e_tzZfp!4HCMvYf$sXC8FO|f2b zvRG`@4UFYYc;MlqdfIGDnd?rIXhMBen6J$@s@ld(wO)71c!WxUPfVINh7XsBISN^rYz|xuYf0g5 zamm3Mc+c^9LieUddq{()=WvNvIMu+_#BwcQx(VD_&TwYyJ?nX>{18H#rH-wxB;BG( zZ&*iqusIyl&BSU#Hd|g1|~uFqJ`ESqHw1(T!wYZn=O~M zXjW?(L#I&|PCj-$2H&byht2lf1F<%;)@x;h2-LF`4JAeFh{J@4p7I1fNZZoE53V^3 z8x?63ECX@!rAza={@xxYb*cHcsPYAT=A_ z%qkfUSec3)H5zqgIA)+}+t)QZIr)w?hut(-5{p|SR%oN1I0Xp<0TtM(Ht)1j3Zg4G zn)~qrYtlP^XU$Q>WdO&%V3l?#o5?C#jw1vf4($qwjOGojKb%P&2oa%x+xef@^O*D$ z8kr>LBir6+(W^JwTtBUH6kY0DVmA#2kX@atR+m1>)?>s2qGXTEXfR{?vYgiy%In4X z*etKmhK@GrIb}xvqNxPo?x(Cd<_e#x#G4d_r1ru8igI6te=PMQJorGBdY0S0WU_-#3lF@ zDBiD_TzPH{un{pxD^kgvO!Do&wjRSR%6w}w^VB8eCf#tcAWH$Pm<@P_wYzm>2uisL z(}%IXaP$=y94cy(Afu)06qyHk&Hl{rTa=t2LjlDNN<>DJQJF2RCgFUdUZQ-Rdb6^ihkfzwQTqkg{sjx`6D7pgvNW~CM5GK_A){Mbo_ ze#Kg)-JEJggW6m}?Tngm$IUf|*{>=DJBjKN_^dB+qEIx|W{;8mG@c!ALj@7DNeuYH6j?&e<0Paf7B@+S+@n$33pMvcWy3Wpdp_9Dq z%w2MbKt+_4RFFdMPL;M*(yeMLvm&SpnfW|AWJ}|kee+%GSyXd!qIYc~Sr7)Ld3CBW zt8y-x8#ReC+MLy;P;Exn;e~PYFRVFQLA?a^#(28Qyg+VDnvUkR3k@60U?)Jk3(42G zc$$=&(#=m?bCgYx7J%DqF(U1DD{#WNyBbMdtBhA2IGxYIwj)JFI??i*e|9~Od3%H# zUDpf;9SybzYNz<&i7G&< zma2K%2klpaA=NC6#;vs5n+DZZCM~@-6WhMTM9**hw~=Eb4=9YbrL% zv1GT{5?hdE#p(>XQa?1yJLDxt!;Td`$wtL6Z%Z84kjQ?dEf-L?$&fBLoB`VxyTxLa z@BY@BqvUscy=G&qGnIkZ%`v=9By)=y>8rZYV4M0t^Gji{0%weE8``zD?sZ==3_Grx^wo1{`5acr6MA}p=R zq}?0`_0=HU`GfT|7@<;cM!<~*+Hf@Trde(1j*>xz5@!o>Ixf>qzmm+Hs9vTn89+k_ zf6la9jUHd`R>Cn&C#h&PS+T%jld1OVREbK(_EP%vwN7+SRwItU91PRwLEoH<&Ps=i zbVdwuvMJ=PdcW=k1RX%T`kDhqyK{jrQ6W){TkTq%itQew=(sqp&^0K^LV z-Svq?DaCaROBDf|P@S%C_I2%>BK;pWuF;~56Gg;p=9qQjNfA9d_>uFGUKGn>+_%hwOeNx7-aL53|ono9{{ z%rdeMIwQ5La=$+15E*0SSz`cAp`&!YxMJRh5kBvYJ7#U(XOejw2zWO(w07No-wk_%So7Wryj0UZkC1lAr?Y!g<%fS>&`i(^qRCuq36Zr(Y(N>qgCj;U4J>LFr-|e@O0Q$y5s;I)%EXup$)+*Jrjt}$DblU!S}SbI`kk=e zoEKG*oyc}4LVZRMR~1^+)QOMcr8&Y28b2=||IT_0D*^{CyJTSU>e@(Vz#!vK}!lr>aew3<1qdhb@O42aTrH@buD=EIon`M)Z6^Mvg_- z1zSXPxm6d&S&6J=L8(e~+9rx+v3J_OoO#^taCle@mtI95^6?mKl(nR&t4#>>FvBb? zgUr68w{qtATaNb^Tf)|lZ0*1I;N-@={o4M|?tk3w7vA{6y$|et$KEI1co__P`{PgoqZytQp`S(2dR{zvnwe7du`na1PxcMD7-*K~f z^OJVJ^Tt;k5+}cW@|`DNcp`28z{#iW{QB{K+4=76FF*L{FvkEQ{(4aeO@$hQI4s#Nj^(QT^RD{R>Va?GoB`Mau5=sv(b2S!?%tBVP4Aivt z`t^2euJy{JHjC9=^7vn^Ika@q6+mZM&vnAj8_Gky7~p zz2NXNHI7}(sSb$@05wcUOm~zkNdf!|HEK)EWHt7rI=*b;(D6T5kHHLSa=6TDO~xCQ zzy%OIFc(?@lHw~EWz1>bjtjU!XyQUT?yWiSBwt`6$dcxBkpjnWaHimRbcsxYF?@d_ zE&PC?CexPG4v(vAt&ASzOQ3XQVOxG*n<${<9@m#f;4D@!8G@d6==~{O*7cQe{KoYd zqk2AB%JWzW%30)2M*`IoOF7Q^)e_&ZC3LdPG0@9s;*vYRvgR13-9Dx^@>QLn@@mhc zj4|0njXFs!`ewJFK%F5y?I_0Fxb@R(t@;gdDEAq5dT$90s)7mg3uSx{0lj4y!RteU zV$?Jr+&0v`%y-vU{^U8=Q;3xD2Pxf832Q;fJ0mxRYFv~)vSbC6^3 zU1Ee9_{Ijup>`=RA_gZ#L0PSGka1r(5OTDNJJyoEaeJ**+9a)PL@yG?#-cc*3ecR+ zo}?iiW|GycjlIw})+ zvc2;q>oIr&ts9BV3)5hvF4Q4QxO0Zg3((zcw9r;s$84tpGzZrbNB@33256v?Hn4Ei zt(X%lu8`R(1*f1PpA{I6j0m{rjzA~Hcoy47m#eGG4Q=Lw@099vQSZs4M#yHG$-1!6 za+a)Mk2M>X%IzSgkA8Bkl?b6u9*stzIbhju_Jhb;bkR;y3X6eJ5kLJER-#`IS`kt5<-F-S4z@Je+8Wk)>?H85?%HuwBhO2xVNMV&l3nV58OVHc#?t6 zQrc+tAhbswy}(gk)sr}4D`LIL(pkAjOQl8DF2_?@sQ2}5yRk4Z&=pdVcGsTWYsGP` zio-;i!saVFAt$iX$Or9OSl5T8uu)cfzN}ZLnIY`0=i$}sSrylU>Le4J_-xQ58wRMn z5wx*Vs!U|9pTt#g0RSovNYHNeyy)sWKkJEC%8}W&og8YH5n~w(W@am;sEv)M;DC$c zMxf8@-7vFwZ~K2+bFhwI>(ycs4O_0%unG^91VUViK^he~3uDpra6d|9k5f;-=yD7` zCKy>MOU$CLj?~hUQ76loFd`qojTKrB{ybITYHO6AzWsv37bts#GTK0s=m|w31j1oa zh+Cz`G;7<_eoakN7qaX97IbQ^$B?@!)A8$$HNq;)(yyQzh4q*IG^+OJSO*9F(eREx zNIaA~T{}3>U!h37aBH9wsz2jaJWfOnYDmn9ly*V?C8H|#TY_2!2l3O-U2D~tRU9Yw zgE9y8<91(c*QTn}@GEeKb*(m~EXQQKOmcmcKK+a}N1se+5wu#WF2|rTT|kVoQhilV zv8=)Nw3dy<%$j@11i znr3xfBij+wO_fo3`|_oopt4nqGJeV$F<0m&Ip~!;NlY>+*5PWp$IR27nNRZ-xAkw< zV^};HZF4~-@WEAaXPZ5tqCID+$WAn)C3cb}@!m=}C=F&P+YAJ*+EGV*tJ; zDsssO?;Gv_#IYu8cBX?&)kN zOmMqEgwTc2rfk}ws#9Y{pAD_C?j7x~wE}%#p$LS2MLOzt=4e;)4X7^gU7eOl#;LeE zcfp%0%f}+JxY?Jw_SXp`Z)Ly1P7KS=SS6+#}Cq; z8Yr!bu*5XI)j<7-9>l{`8KTho8_LI&l`zo@Rjyk0U>LYwfRMvBJuck5?aa&sS%yue8PU(Rq-98C+QZ98 zz9KYxnt^=#-U2D+%>Lz0yF^<_ZkI-3QXjx@A4X<2)Z@Tz!$sW{-(eK{9LqHnGid(} z7g~8su*J8SdBsBMV#1G>ja8+l;yya>#mz-pUvQ)}_sS)^Ti#wfAFm*!eu3v5VM>t8 z5=~gOT#j;InGyxd&&W87f@Ostd9`rsyVheYyq>62{>%cl+BKlTvX~SzsJEzc#VFt= z=`ya&6uqrh^ZoC?yEF6Rst<+bS9h3l}3EtaXUjut7 zUuaDpvhrr7Cp24r021nTHSYv0(H%4kCzdh=F-R9&{hzGIh)PhooKI|fZkWqzrvmOU zSUm_p6=@$#EJ(WD1v)k+?yS7GcGTY7=Ig-8RHb$ovL}lb%&aYK|1-d+QUm=X-wJ z3gLm=ONY1>1dEzlZSdhJ8;@-ZUxkDoMRk25QuN_pUXS5H0}!ZG(<-T23lR($70mWM2+j&LNSUf zVinrEJjhIHR(;hOm*uq0#?@-LWcUJx>=8wX z1Oef3mZ5^K_d)%x)Msk2a70UtYt#7XN=O{{LG1|F!u4Yw`cr z;{SJ**W&-L#s6Q6|GyUhe=Yuho+0w5J^n9Vd=#kvKaBsscRllK#Q($f2h=s<|IP)6 z@)3>yf7x2AYsCNGyw>U(@&EOOvvQ62KYKaz>%{*-6so=T{+**A+x?}}pV?85zV^^P zB(~qY#~to(zhU-T2Dg&hDq|ebPE(;N}M|vjxPHPd)n3@vk3$&+(TY4~{?M#_!ztp(h5) z)(>Oz{P&~3v*rj(o93(@dD0YYr2)NGKb0QW%~umc%Nk3?)ZwN4jIV%xdng2Bx5d7f4Er?5*P-*T{3d@xQLexJJ6! zC#*Rz)C~GWF+(er1`05<7Jj{DdE%@(w6lrHyKP#PL#8}kF-Plb+HT^8(on{PpgZiz zR-@Fc*XM;KwDB5Ne-OS;caLwv0(KCR_ci5RwXw zjM<{;lj?+WCzWm^u^D>zZ>%|LpyqI>j7C9ENgy~Gi50;t5z98Xoc2^Qw&sZ#M+>); zal6!-qcS5$gqulz1jnP1G{Nn;Ekke#FV!V?P{=I}?V%-?Hptzzo0cgzUl3!wYpe{% z!E0IW!VEPJXw!j$k(&)xAnkXK zLp7<|1C6Vm>t@RiskS?e!CN@NqE3XE_ zrB#TxK}DX#yY*JrAm%yKcUo;u4flWIl7pOf##P&%mYa5tlpqr|vefTe@QkMnM77x< zAE7h|1JHY?7aZMHItwTAd{VGRmuLDT6<`?Q{UJ)}9an~^K!%5NO%y<~!Os7*<^Ujy z<1DfZXh98Tt#T$;TY50*GX?-sOfV2@kjk>!O&hVdzs_Um+Ra{rafyZ#Q(~x81$i_T zp|lTR@PcYQX;L(jgw`O7A#VHTdJMxdhuoybIsHx>WC6?^3*?S9!-D2nv0{r=Gs~wf zP_&Xt+v_l2cdV%qzprRH_77rYS`bCQx4Ri-_jQLU*NZQa+&9Yqj}Z^T|4l zagDp?lW$zlV_8UHy9F9QbyrDT1FeRgtZ!L)r`;IWSe)vOktSESO3*4fS%;P6%~K^$ zX6wb0kx>SadVSTYp>jzrLy;6I;ff%JrD$X;RN6|05Y6*3bV?LK# zIy%Kn0i>aJ>%9e=E;=%zPRNCrlOD)}`|b5ahV+nxCiG&y2qseuWKVSlOKVgq`*NQx zjan$TtfeDX2LQk?l#XqlyL$Zf>j$#=N%Z)wYmUte3{NgM!p(Duk3aun3}y2|i5vIV z9Ge&B?XR7QvU%Cc;W|f6xkgI+U%Sju-#Dece95qJO8eTXl+BA!ZlLRx+B^yVvfxVD zyg=l}I`KilHa@c-eeznX&A)Gs*58}T=A|(=)~PF?gnrXp@PjX2YjutE|Ib@Q3q4nx+UKG6ZUoSZ}FADzBHOJ-!)JN=j|GzixL0gCH;nu-FJ(wSS*8T_f zziwaLKid0&y&E9F{oc|4e&ihe#lwGl_?5eVu=^vs@7%?Aeq-lbcHXx0XSaWH z`-`_fZ|lErea92O=pRXN@WDs^>9s$+wgRuMz(;rmnCFN4Z{7Obty!Kc*#Ab2`=J~;6<=~si!I|g#kClUMCI@4l?>$ydc{4d> z=6Ut8a*)mBAk6d1W97h`$$^>Y<;Ti_Hj@J}&vzduhrHSi=;8K5Qq1$xW94jSha|~7 zFFsbzW_CydJM27G&SrK<0y``oBL_v%P1YHZG0*3Zm9vrkpeV4P#bf1cWIreh>?eDy zoQ>=UMS=aKkCTIJWIreZ>?e7woQ>=UMS%UpkCn5L{h$c2pXjl2HnJZS0roS0telPf z7>WQt7Cu(aMt%%MfFBDUD`zu%Mu0uf9xG=vdq#jg`;V2gnLQ)Gp1sG)+0331V9(RX z$pNs6FGpN_s6RwuV9)MjJ>ayIiD5bzuNW94k-Hz43QzTmNPHuD=0 z@EfDY%Gu0sK)`Rj?Xhw;vqK2j;aeXoXEQs5fE~W&v2r%ELkQU6n;#(|KrWSd-|E%-*xj}-uRv4PdobLy}xmTJ^iE8pFVxx>2s&uTYtFo zIfsAi_P5>sD?7Ju{nTyc_8Ydz?OR(P-1qiBfBzFs_>(&)`N_XH`R1FiKl#0z%bV?6 z-*QX5_31Z1wELHCz5OsetR4QjgWouO|LuRZYae~XQFK&)xn{>dar}+P*>UsoQ|I)L zw!ZN(Up;T!lXiCQ=m0uM5ddOis`pe!=yA0xj`igD#oo|t z)4mJ3RQ?T#^oRp{3OZ&8{!Erc(PAQV-EXbW>8~5ZROz zK7=*4Znd8xn?&E@;|zkgHG@7w>rOVTGk0{jr)loUI}HYF_|mM=-4xUO$Geaj`O z7w~Pqm{dVS#09`N^_o`~>-P_T|C02Hi}i#1Ke#FB5A6TIC27vmAgIoG`a)^YRYUDx zQDEm=cfNH~()aDWZ&T9u?!0$XU4LT#CpIPh@%N6U-+uV^O-WaWs~gGI^Dou?PyF}G#d@O8 z`q1Hrz`^Bz2wk7}V8^H9%V|B4)IB}#)AaZL#Fzak2fUot6Ss|D*#Ct~(kJc(-@fzh zo05Lp&bMt!`uBGJ-X-a(+icfGt}EB+#N#=>b!9W!|MLAW-<0&r_P=aX(y!eA%1ueX zV*e{PCH?CCuiljOtMTA*`pWfbh>y4X|zTwszHYNS6Tc5Ql z>6>o7X;aeAy!DxD(kFlJ?4Rskl0I>VX`i+)NuT%#MyI1o(kFVtKR*29O-cXg@Q*en z{qW(3HzobU!#~`V^bZdI;F9!-pOJ?rhntcfoE%(|KJgKBPrI9vN~hAA^vTDZ;;=Z6 z|G#eQYqxIy&D(E0{npdZy7j%cx;KC2=H%p;PR!%qKmPLL*WdW&8+VSr@d!Ko@x#%< zFC4se|L6Ck{e!)~y$9{Se|Na^vpa+Bf3xjxZv!FE?8kY}XYX}7(i0(5Umzez62)db zy}XLp{bVp8=RKus4)q8WzDSIN1sV>iB8j?~ z<|VW2S8<37MY>gTd8MV(-b$ZV6f+Yv3{RjZE8x86vtItRAAza+OlZup-o$qBX0iW_ zmOBU~Z3P)k4>U;xXq!~ZOeS_C4jeqv9*%Q=9R(i(t~(9gU=(M*m3)){c3)KBbpT6x zo{a%e)ysgS%V1SE0Fr)MtXz!5NHD@faiK%b7uA+Cix)b3?|wbI$4q)AY?l`-+l!%& zfE(?gFGIPemY*7he&`MFryW%BrG*};Wwdv%(XaQ#Cfui8igYRxoR~`^VTCLpCP{6c zsrTkKn{;}huQ%FlacE1*5!&_J;~HPiRX9iGNo6yv-1DiQqtNST>IDkD{Sp-VM;s)d zd_{ZZWjkNnB#kY7HVR|Yirv5l&lxUL?@o`Wx~|CS|xQH zFSnwJxQU=kNQb^;YLxMi``4kOH>1zwSi^9^o|3&29syVJ(2bk!zky zdGUZtJ;L_Sb$DtogCpmd-w*XNrqLQgy34t6KEG$KM0Qr60ra+7z^$f!G3+L$g`%ui z#a-}P10jiP2YQ;tz+;~;#mf`vFaF6g5ex%4_WGz}(&72!YILakQetMVGEVXtyn@i` zl$-Y*BSi;dtVx|Zb$>w8P*PPJ{ggL)LYj3ZBtX#xQ6bJbl=s_$9k!3eW9oNxii8pbX@CmMad-9Q|l%FcELZ^gBL`3uHn7#du*Ay zaXz~XA~#sL>G^angqzkpb-@3iK)DUv)6;sJG76oK@KA39vzvaH_Gsaa#8@%Vy6p?j5L8G$o!1|h|LXME^H_`_kY&8 z%YOUoblV!G=MNOdK%H(GCaCkNgEP(X_E&Fx^46Db-G0Z`m!AIKX?p874}b8MzkLd7 z{LPaOoV?}uXOGnzKXpSm`sYW?;pZNF*8#Tw5BE!Z-@5ndyWh0?DLY@k^9ew}Gx>Rb z=Kw^kk9O{+Jq1HjP9g?SP)-~t=O7_;I^gsV<@Gp07V(l*O>ANm++8ym!ECI_*#s|W z*zU55ZAqSqhY%w(qYlF@as`=*8M|Qa3_J7g6l7_8eXR*_Q-@aP%6ot7<^vr}3m+8L za%{yN2te@w$Yq0MdlNr~8g;$3WVj}WXi7U8(RVu}M$;v$qbfZOAZ7B4(W*byVSO-A z-~iwq$FT0HeKhir{Eq&l44Rw&Zfk4%?3pa+YeHV_3cP3z87TtH-`E-vYDKZTUJ0Q~ zVaIcu;_V>L+2PVXdRYRAXqf%GHNV_uWR#4aK!*EbGt04`r zh%w3Uhr(UZBaWH-q}N!)Ojw~?ojMa&O?Hem@H!z$0jXnFkBgxeQNAOt!YFKa3yWbQ zmtXee-W45AiU&FX*^K5~imSLf5TI>UmPwC{q`S_>Zz zh(0=Sb80>ulHsz3WDensvO2?$@`Vu+%O=rPxjTzA_2N`svP^T(6Z{1qUeV#meV_x1 zFZhvawV^!G8+mQf=oUD%q*;mRd0hay{nNXcO` zaf&8h4T7nncj+!a(P~!N;L92_?b3I9O=mo-mKBuFR*pF_+ulS$nF^_jt5U6!H)kr} z??O%Ai^)4P2?0?Mz9bkYTiPSyRvPWSi#&WZw09vkwct9^F4X>WAWZllL=y1IC5*-@6m&1KMDr~)M5WSoo zn#S2-)Fb0=)^0E7gg2;G`u=i^w+kg>WP&suZzTO^-gsGp7nlm_isqxDIamn*oGL7L z#nCvcx(3UanY%%+rixR)sScS)T_x5GAVF3WFPs+6f-L~D-qheG!)jjk$valRYv$=P zVi#F4tS_9ld_{+&<^vsyrRyxG>X4|=>mG%D=(3WlrBudc)EALy_EokO+yd?8no_HyK=jz|yF=2V=o z;?mq{Bo^B99FSxNbeIEk>qC`Eilnv1ltE@e0y=n#tX$&wfh&^owC00xIe)-yU=Q^N zIwbs}hqc`j4WX5qG`Bit5PAHlGy`oo^9X!&wqviG&c`Tox4!B+6J*svT3$U7I{nmb zP>N6$1kk=!hbLk*gU6f_nxm?B$Ene)N?&7^-Cm{Z$5qz3vJQvh10BMEn)$5;##2i; zrkVbb4x_o@k`6RB;|b_&I!oH)Qkhe=yJ3A^jvBh6_$q5QX#k*$6H_~L1_^4`96$5h zDmd0;O>9Eni86hd$Bke$Zugy>?r4=OI_xM9bO;umMFX)yvrR=Vmi7xPYSUD!Gb2R4 zLp35NS|C~2E1K%vYXBwR>DCwkpcf4!ca=F+wJt_ibhz=4pWnVXPHx{#JqiQ(ns(uk z%^sdtO}{FTioxnjJFl-43*q|>Z;+|Y+_)<^af=5?<9*p}C5%F9;gV_;YR)EZwIW$9 zbIJu&FV_OqxHHvOv1?8(*O}#FCCmBo6&-31mSNfqkTINf^m;2b0Mc!jRH2HH)>*`t zRa?`wIzJpPA!(l9B?Wnw)b*$nA`&!}%X+TKP)BL8MIVp5_Nd0lBBzBsYzlX3mJhBl z8;feJ4D5C>YhT^EZ-ftYaGgb;SXH{bO~ehq?Dl*eV`yFK+r6#|kaQPDSFg^x5>MV8 zKuJxbmjK#qT7sjIy+GwxF6BJ_C`<4vq&GeXD< zx{WORBD|n-XT)>okY+|3;2B2w9gZKXy zSaZs9y`irtCmjuqq}&6C2-xAOV4F7Xh5+i;Yxl;JdRS=_gG{PUMnqJ?2KIt&)|Oc- zn}P0fapQ^q)-TVb+w_Ii0wurd_lAfx9tK{8m%g}3iKG2{E- zzdg`lEF)C`M<$WqFeY`KF-t?cV_Uufk za*h_pG-;WVoZDjt9HTnL#9B$V$E{d5B4j)6Sq3lHVqz38hgqr7?pFH50!wfex)T*q zTWguAz351$GqaPc>#+0JALt+fbVyPTqI)X}jM7etFo>7e9A4Jin8C_u=;4Yb29CyE{qhVRw?(iRXb! zofY!Bj?A{J-qId7vXzl|TOWwHI*( zROoJ=EIz`^Qk7H?lwC}n+*|jYd+OYC&ea1gggTs= zU^RfnBITgRS<>XCQ-`jeJSy$9dh&Sv-oBH^5uwD#?&J|UT<)*OD!L1 zHD-bV+E}Ps(w=%gBRNAgph%ahMoz4W#7ZM?JdzhHW8g6(YlE;SJSU{zJ~u;>TN;X3GlL8#FL zQCY)w{vjuIV1E7vR=^M-z#5RL95}n%VGzIop9aA_(sMeC-a1kasSN5Xdm9B@Z$jNg zNzS0+M%^M>5qqwJ1nUyL;i2*gH`jC@dF+wFV$KOjvQ$@`EV2ht2d`08}Iwp#r2nP~ZilS;pdMs1lar zm_QaQrc$`XvbbROo7VA$Z@2}}2@e?18-JyJzZaQsp!2hThgbEy20bx+gCDWnk7k1h zhv6dw0GuA9y%mN8UsMkB5X2*tWWoZ?k_i$kMJ`GiqKMs|&MCaujl(>WSBr`)g`J@$ z9F4>!cQnHqy-~th~I3k)}E!3f3dTBTQ-!`?HU7Ve@4SV^2=5No;z%w&2at2=e^7aL) zg?!!e^d!|5#@7!Ru#iW@2g7>f;d}IfwFxR2sOocVg3=3}_7l{K7#@s23BU^g!U5k{ zm1pxHnvEk>G&>CAaW$RP4&&WpgJpcxUj6Q|JWr_6a|hzULf)tH-v_)?`|>2o?E{ws zGC6QFTnAbAcUBFaRa>cD##?7QENjDG*0e@|eK|7VHkOK+cv=WSUQ0|Li4&NoR`nU>UYaBn8gzslhc3=5fCE>N2+qdHE?h+{^4mATf^aE`;2LQ)TXr+g8XPII9Mta$YQ@~T|zTy zSHeSOScu9a<_y7?(iJgM4Vzep?6tQtIe&`x>q#FxBCNm43n2MsK=u5UZ0jy>ph&vE z^jp^kBY<>-=Jm)WuMYFb-rE651Xk78P|v*!Hj5NgNe1+xoYNrW40t^rcShm$4CurN zdQZa~bJ++sZ2|TyAaW$iWkV9M8sb=w`bk@<#o?7L%;OWYhT)0GS-J*X+x4>p~uN6EbxXRWUvad6-qt-2>{mcEjrO+%W@uDM46!~;i@jHqZ zN5E!u`yg7Pf+o>!0wMSzOWYXsBEw(XLE1nKqlL>BPM-g_`RLrEa~I71YWB6W&zrerW_|h-)3K>1 z0I9K`E2rAeUYLoTc0p+V@AlUOg|7J`P8Zcws8Ap!lm<+$US>=8Kzev8jY&Uv{ldW< zHN>Phn}UQZJQzTVw~tdH><-Oerx`*4WM@{%e$qx;-BSq+04}zl1xV|5lk;2qm6%6>nsiD~lAT)v((xJx-3=e8iI-WT=TOr@yZ z8n)YAOnWb2WCe`cni&nG_^hRsIs$P_xJ+8{RuHZFINnoo2WmthR%T&i&B-(~Tq7C} z&?Tw82e3Qi403)`GrLY65h?FXy<0P8brJ}%BSm)rlB1q1!db{#jPnVSScy0?wsa|- zWdle-=LJl6_n2OLluTLJgkr|9K-#5}f?651cWeOO&hQjtZ)(*U{bU`WU%DxPAro|5_Ee7 za54c-S8L|fpG~)x6&<-8h{I~66ZU`wif1d1K)vZKMRjH)(F(%B21$mL0&R6Bu3T*| z17@dPK+a#Knc0MbUJoJ8kOWzb!FmpjIbEEIP1Pb%1MCVSjarfs%gK0E1fi?GdZE3f zq5(sHjb=1GnJ>o?x>+qZF+-K}I@3}xD07NA5}?c$zrluvb3%iNCY1o~fG%t=b|yG9 z|LXlgsS#0HbUl`8$zBEbF@yCch+l5AUvn3=z)3v3CBI?Rmg! z1E|R>H4mn5UG}og5To9#FWHF3AUdKw2gttykiSAR`5}UKkd46#TWv*_tqmyI3){23 zM{>DlHYJ6WtH25?kg|N}kpKn=-H1_xd(?&PnI417G&9&CREI`@z?NcxwGGBpgDrC! za8S3*OEq(NdPy3*X=rwoHQ=hW!a!Z7}n$xOq7s zt!|kYYhKdZTP7S8@lcL4&>klrcN!9Uk5ks`(Q+!FU?9Ppt;IY+iF7KCmW|)M3@}nP zdrLE;Aq%dlG}DeGTCv6)xR|IW?H~(>ttFaD5G1{zaNTdNyIh2l@(PvG=B0q6s@a>G zIrd4Alwlx9N68^~qUe#cNuyj0XVO(%Q38rC&JliVI^l1mY=&5Z-n;~GQZ;)+Gp8Mk zkhSDW8N%Yjk$8eeGh{r?KvF<&A<$$s9{`cyHB4V|w44UCaOLXe#ekWr+3T8_O}HMV z5>br6l-C@xwe%q>l4Veu39}Ji!8ogv*4LfDItTW$*~VYE+3E~EG+*1FqQqn_8YS%@ z2MFx9&GcYE`Y@GvNdAm4l*>C^gF3gc*;I!M=2p=>+}_&9tA2e&7L;_Y8sxnqU&v-n zhFn=>U2KMP!Y&XvSoHCFNHNvv%|`DDR>w!s3>$_(!yX=#D9_jOP?HJi?LyViY?7$2 zO6Fq@2jcVElF=&4I=M(`v#yS4Jv1+Cp0eHp+!@(pUmkxnI^h2>1Rn(|i_Ip&~ve~n9JqV^UB*|H+rN}KeZ^Z%LW zPA%WM@B;ATna?vbF!kEY+ZU-$>%TkYkkcB`j}YkbQao@VHX_m!)(A4Htkl0$tn^9T)v#b zF#QKo8qTAJd-M+9T2x-c!oo(bOtr(2o?Ag?SWR&mdTv#8CQ3#+?8Wn8i_t9Q*L^vC zay{ON#Vt5a1V9G6!xCHviobQ_ao^UM%uDfjD6t-mp#jXbVctlY{W4$MFwlme1=N~q z)XZ)oU*aGLwl$J=gUejEZrJi-Oi)-HauROVZwQvE4j$xvNo0$#RUBGs-GSKzLkDr+ zyV_Y*1#F$$rvgT7yvI%j3@LS|9xe=ssQRY?5$c)<>J#se1^Fw9P=_TrvXN$DwxWcE6xN6@O*%5_j4Fg)_Q=0 z3nubL4;*DT3XEAcS;L`ZBgv2?9ZR+hro1I5h5VdFbj5l7VH`%>8#Tb;7@i|PU7SV# zv~lT;D%W65@gU_x&Bl@vjZ_J&TrXicij0BMSQ}2d{~X}eKq>F zEAgqTX1hq=jtxR%VZPxO+lUuzb(J2e#b(lvu1;)o2FgyVSW>K3JJxh?q`2X?NUjaK zxE{?!kxa(W^5+v3PtJ1W2l7mR92cC!O5iv(3gh(?-DK;&E{9uRZ4!sw5DrY;( z1uVJkLaie;b@%-Lz|hi~z&tCfI z60-R3i{^z7FF@ez-#GUnaR1-G%$_y#{+TnUKQ*14`WYZ{1Rv^#aQ{^Y$fy&RL!PBk zM+oDOB&ZugJ6Vl5YuRQs;vhr|tKfvJMjVW2VTJ8vH3IbZsE(M`Wj6#mS8z;l`mr{F z6Mwv8{jwX-i2+RDe%AuPKQVv_PF1u3@O1!8a4dAJAAxsb0LSg9|--?;3C z^Ev<~@Jt-*NAQY?0UYB;@bZZP9MeZY-Ei)XoqyE%>!F?h$bIWF=X)D172 zkkyFe&hc(Z>V~s-vKn#1d`u?LIw7m0m_TGgR!1>`mO)lS&(4p@1ezygbrcf_Psr*h zCJ@@mYD8?n_5>aUE7OFmj`+lny1_UhtD|^UhMlZN%+K}&9>u8i6S6vrQElvGH3IbZ zsE(MGdj5a>)O}NX={@)EG3kC)Te{=qfxi`!{ zIh&dJ_{{UCo1nx0v!5$Z*?QjIscBLdnVMQ&nx3D(U}<4Z&{1L$6U#`%l+nwA+^QN| z22Lk8ng!7mC3(BY)qwR4W5OC4jNI7oYBqyBLpTBn!tHJ0ac6iSa%W+hz3%g=6V>n1 zdxoE9n)JqjPFJ3=^;`fmw9yaqU(-fHyddl?0NN>pmh< z8BBEOfFsXU!I5v*3~v9{b5wAz*akO$Xt6&&e^V7|y5 zB9S(MqQq&YrgOE-0hZ4>~T#T&aw zGv2kW-74Nb{W7;^3q>myV}z#zT)`w*g|fX2r$v&q(xxz;_qff7(-cp7MFs@ebuUut zQ#E68cElvu!5O+iGtN`DcBwe~qC%%}Doct*S_ONKDcYa))G*OHhQTRxGs= z2_n|C=Q(f>KqaiygQB6D_Cxv#YB;#D^n_>ZY<7gl{Vo-w?|GUrp1QR%Fq^y9SYyo( z>%(PG+dC%ITq6y9N+&Yk)o4N(-=0=F(yW6mhMFLY0t}3^W5!t5`C)p&rnl z#E+d!72A1VQon5vk3x9Z4*mSCB`}wvGqyG4wk9%J&DLVX-I9lEqF&zH_|(Fs4zd?_ zkexUR*^s$gRFPS?ku4t5ktW29M549OV4U>98QXcs*LZ}BDz>xVs(zpIHq8k4Z!M_n zuy5{G*L+CDZ4VZpqO2gX5-8b`!3ubTFPp)IIAIT*cM7(KJ|SX-EYbsea2m%h!BrKW z`3Cjd@*K?|&)Aw*LH14K%9?EEs`iY&h01!QZZCV}YS|I?G)P@M)J(IKjpp&P%|>$$ z<3ZIkaGpFqwQNO&X>xVG->VtyDO+DPRkd|GH{HM(e0k35FNF-jpsr{qvTq;& zdOJcA8I?>nz%q_2(I!eV2o@@GB<qcAB z04G>4a3px$+&Yu+RsA-*=*eqjtPfaF+y@Ffn;! zvZ~ZuY2Su3X;a8%0Rx4GVV?Ea>jv53C>z|#X2~dVC>AT3u$+*ilP=b4Opw)@U0X_l zI&^TcHPz0mTIY32pVk?1-f`@-&XD@?{o*siZ$2PC|F7wUom%HFp*huf;-}F%e<_v& zX&wFM?B7V=?0^6?4S3nhs_!W~%347OXX{|-<@xITb;EWmWT#N53V*n+86g5kqif6t zScNoH*W@h$2ZSfqdE9~-JTxk>=}@RaawdYU2{P4)=BcOzDvv8IP*z+W#Szr>EXr21 zCA#5qKyF`1@`h~%N5tua%MCw@fqN|rz&&P4RNSmRUF8(WK`Ulok#xmE@&?RU$zs}y z5_OX+>BDQjVkqi2n5rb_)D{e@4*k+gH&1QnR9*81u}{~GShUAZ*F0^t{Qr;IAAc#6 zXFwD5zd8PmG{N|Q`@>SII{6E#5DkmCKRh{t%dzA@Ho}f^52Qm3HH8fNzGR94&(r|z zLum-`=rn|(sRwI59TIP9p>21W?*(P&E8G`c{k0w+AECDw$| zK)FFQ3ou$Qn`&k=NFLBbG*M)VKE=|^7j4{;CwdZiBjofkzJe);MjRZdbsDi23np)c zuRAi9M4YQ_Bv}h%r_E)t46f46BSaKb^|Bi3Iau#2uxWpViv@iG?N5VxC=n@O!SkA` zAA=VzF!XNx|0z?-)ZUx-n)f`gr?O{ycWL+3E@9W~>W!-}S^4~mbNTzrS1cdDbjMO~ z@%BY*;m(Ed{Ez0Pxj)RMXMZ|--R#oLduE)|-7|uz@lUQwwvNu^n=&PU4ciU^5Lkfx;+L3$x?>@V!~sc9M>D ztCE{IpbsjefVxeZ=^w6Dw)u?kkY+bifR9P#Gp(6VzhTwSVW@jEyO})5VM;THe%-mv zVT6bB%IVDnU;&DwOwIpUGmEarP^+(P8^j1dWO_5MVgM64|0m5Dy57Nl+QWT`n=wEe z6pNgi|D)z*J+OgHB1Tu9ycq=yLAgkB{tucN9(BFQwF$~R%A1ic!m0VEjuPPnT!qar z;0j8RP0jz0X0Ah|a)RoT)Mf~<1U1N}=AYEevQN=WP+qdU83e3A5wfZICp5F#A*qwp zm)y090dAlQ+0^{+HFKL#XeTHzNo)pGIO^bkrx{L9ekZ6fdFAsr(e5OZ^S>RRGt}ES zc1-dFB_P)(NOUP}`T;O?@0|aQW?VMxzT&)sLGV&rWepx z_s;oWYop!;ZyrXbd9~Cc)n6 z0xUXv=Pxv~czQ9MxH@He)2U)mAK1?|V;I;wN9C3Zn+`y`qp}~Th6xfUsZBdz z*-_b#YG&D|vL{FoEN|KXtB%V4nPyfy7RUq|;>ul{=c&(0RoVZoncIYGWrCzrV)GR$ z993ieR5P63vY8;K4XEswcPE*ge`I`i)i9MkNwV7nc}Atpa{;i9%KnLFVB0Etg8VOP z^Bi@m*MWroc)ZiCHwC>XKViwq3pQT{Sao)XA8BUQCp`x>-l&?BFj5jkc z-;8zNi5Ei)f=Cy^1a})u@~Sx=0lgU>c;X`-==KvI;p-Ux#Ock@0Iw0G`gslY zdT8MlJdD>+Qro;n_&>Dp!UlMaAhpeFgquSPufPDW5v2Ng4Rw2r=XLoe+FhL!T!k^o z4Hn~9=jEIJE`kZ}#hAp;p#_1ji(mpj#w6EUXhGoZBACFfG0C+TS`c`;2qy4$OmZ29 z76k4tf(cw8YZJI9v> z^rn5M_8aA-IHdhXc^AgN2-BOkoxDbo>gP4|4h${4&O40PP*U5xMtB^w@Os4nuMwoS zd5tKXq=nbZ2Y8Jjwax2@YcO5Dd2V-gPO#$ECU6FfU!9k4p3_Az0o6%-h*}W5tczd* zzr(RD!E^cM*}XaVTb01MeAC)RFo|1Wf|vF2u7JxokuHJ>sIIllcB92%FIKR$FrV{F!t2&$Z^xoPB8a zs@XGV?w@JR?4Q1Sx;1^~)I)&0<`24c++65{z@d>rBl5UG#IF%HN6&c4r_X%<&E&8D z<*e6#(fs27xn|+1DPpp%F*@A~{#Dj!F;_U@!MB1^hUt#Z}WD?dH6aEbK!50Sp-yx+F| zN#lKYpL^?z?hudr(tn@7_iA)&&rWJ1a+|x<&Y7M6-C6T*zxnnT|NPP12U?%{>Mb{v zzO~qTxB0j`zVxd#$FGxHyXQzSZxduBcL^72w|w(j{)xcU-QxLJoFFr*6~l~Z@u|e)#f7cfap^{Iu)V*5k>o zoI$CypPuX6}2>Q_p(tF7&2-A2dAv@;kpotex@bO{WAO|44FcsY7^z z{MRnwT;tXm-z6uX^eK<|j32g7%fIVo5B)57w&TvmC)z)-Jp8o_(XGWDYiv~3YM0vA zUwg}O$D2R)r*pse$j7cOom+XFe(RU-drR{5=HK3PIuU;JUUX|=C$$l7vo5ud{_N^c zy@t)d?fZLwdfR1u`c0LK0iPA4dk*f ze8NBVxexvQi?S!4^3&taIEOob&-I3{`nTjiC%5K0geUNKbqW95dmo#ByXmE0I_IwA zKliT>e)=5d!Y57tny1d&yy^#2OSi6;lUuVL!V|c$x`coK?a$qMsqwkkkM6nRTl~E8 z{DmvtcHtk^?Z>U{e%N_(Xzn<4Yi8%fj_^)(seS&1FRi@f{`bZ{W4rpclaMNW`twfy z=2O#;zvO$bzPSD5J!@0w*7QzlBm7TYYM1cuzgqXz_j#6Hb^Y&M-?rV9ZJFw~-gf#a zf4JRz@jZ_`^6%)@)F8E?9;O~OUHI&0jQ@4~T^C-)Uv$FTzxb|qU+{^=*Sz{Y z_oD5~c2XPRCF)W;{Wc$c{m*Rg{8sH(asJnLKQ2A>!fU@3f6*P6T>BZ%fwRw1(DtQ+ z)P}m0y4232KJ~fkB_YB3s$>? z^N*hM{buO8U!=Zx-}?`=7Osf?qI&x~Z+iQ`?0Mx|{^7Z?vrbR8FYXYYz**EK{Ov!# zFMayWw?&`$&~cyq^<@v;dB)xE`PjS8JN3N$!Ygk4|UBWlL{T@@{ zr;QUIzT-3J?0fN9`-nfA8${6SlKYWtcC0{#&1{q2^CLXh^mj9l{fMY`TOEd+tBeA^zs#JHj6ez2gDc zz;QSK+v=OB+h2C!x9>8YkowPLyWSx@fg`3%_)oXIwNkp`Kd+kJ_>brO>Ad!5_C4(R z`A6!ersV}UpZ${qzk!qOT8Hoi{*^A_yM8fy_J6$W!S9@S#=g(~L-Wmd&M7~1T=Kc! zlK%w9KJvb6f4G%wD;>fUIAgkmuY1LTdmj9?>HN}*%+8R z;5xs3%~{EIwL^FUe@vI~$1)d`m!fZ?9{%z9KmE;$>o(>si}&sNsqMBG%$@dY;no{Z zOt$3?;R)OlN?PDcKL+_r86e2O37SEeui{>H+CeySaGgN1h#c z*7E88Gv2ozy&?2`{fn2MdgnPm!1uk^{LoW>JPvL1JE@KEadfFgzWmzI#ihTmhn#yp zVzJ0qRKE0zkW7C2)^p!0uHF5quU(I}xt-KTcrm)v-nssP*eg$oJ@UhEpLW?VpPX5E z{Chup_es8Ddh=D(qbL6AA1^}N>`rPUTpnF&XU#lt_Djw?&YAk-n@@S=yT5$hYfgCb zg~kIvoVxfO-#hvD;nkCqZKktWCh&T63E%VRjmyz@{dV6C??n>}*h6pqt?`a;obUaB zaN0l59rx`wpY)bwyV4;%fit5^`2BBb{NND=Nt}G*SHAwO|ML~Ho%-1KfBvNPyQe+y z+|!$m;)jlQ?M?d8r-T$nv@9rE!@lI>E5@C2@k zF5&l_<$lLEzsmjYiF;zVFWjVj;k3;I-?+)U?-Tok=eh5D&z@?sUF;B^zyr}Gd|>9X z+jiX*t~`46OTPG?=hC14w>Lcd=lsTd|8UiXbC3M=O`rWD+9r1_-cgQ)F10tGH+}of zr(S#MKmYKzrjxF_?Bo}J;=>_E*!Q9K^(Q1g9DCK{Xq(tcZG`KfOKt9_55D=aD?k6- zKYsQ%drwCm`_8Lg)I9$6tzGyDjtKg?d!j!_+l4`DL!AR%YVQB}^;6&dp7l!Q!7u*k z!JFAHeDK;oT_D|m>K{)2`g@UodDDx2)5-szWu{hdTGW9b&wQSlfoEpmFEj(MJ-1z4 zBqO=&7N^%HP+8+0p&Cmt7z~$Qd?2S(sqCTLGmr~CL9CdXX|*;%5$#TUL$Hbzyg|PK z$?8y&#`#FPr7!p^34ajw8Ld@Uu~_m*9Ay^M2h_CLfwbcTllY7~1O`$E55=kt<_&7P zhVAHYSj_W8Cj&SR;=lH(6%+P>yxe_xlH~UFu52MX$VhPGVu55yu9{U_sTa76ci-+v zL$(~8y{At@KGF&X;GP+k!_cha^?b+~_n1A7Oe9c8?D3*8?}a3A2^$ubQ9luNnKFFD z%i-?AkykKq*syfk9bqMeFCQ8%8?d})!B-R#a!!% zlSTppp5|c1n<$ZtKW0tTMYu*eqt1wd5j18Mt3$u^(%EauZL(9spxKvJJmS{t@y|}H z)annH+Wry-qq8FW%bGk>IOXhpJ1%z}VS@ot?4LG!h)Xky(cX;4t&MObj5^Czlh0Qm zqjZ!kHPW6$FdBn4(1fC1UZ<+B0qxU`Uma9Ef2Cq6UFP7rX+hN{YT9mpF0KZ3ao*0j zTXh)2ix`X$0=2`Dsv86`k5o4b+=L;0SoBBZ6{oY{;pMW(Gj$}46a|CDVW1-@>M@t` zSmnrz7zjSQ&A>q&1I6vG&%ZMH+$|3(_;5CC-s1qIxG3Tjw^1%ELAU%gei zW8gi#i85Ttj9B|eqhu`B(N&q;MSG4hr*l-p;qpsZIbx&q)@Zx{2Y?UI0YP5UN;Jdi zDvp6NNMKw?QYpphj@ZmXscw!%tFbayG@B$etY{zM=zIxDttEKtA`{#w!C zu)+%GBtXGtsylmmT_?<}t^Q>7!>g}ejjW!$^4QA9SFWA@^-5~x zCCiU5e`@)4(|q}2A{hy^bEftmwi+^7H(&9~v^z=&?EsOID_b>b-h!Qw= zVHMmWaO=D>@0dSs?t63ZpSyU@H+|RMg}E2Z{%H30*;mbmXHS}Wbmn6-*GzqGCb{>^ zd*8OVycf~-DrT4^XVwCFo68oKOo}y7-EewZb&nr68|WtD39)*t7|-Wgq+Sj(4p_hZ z$4(DaW+)HV^3{}v%<>7k6wi56xx5!>Z5(#j%Z!qaQ!ZmAo=xw`b$M9IoW~j|k+PvF zFkw?t(c>A6mk_o>H*t%*E@ymJw-2kD3VYmL9??+A=CdiGI>kCF;c&=PER(5_T$NKX zN|F4eEW$Qp(PWm=dtTb@A(ajJfSi)-aWcTMREYz{d6=9dpH`}AcQ9cPOSr$L=j<}H z=Zr2YEN0dr5dkYQ2?7`Wu1d3RbUPF~n=V+)4LdE8J~l7Ik~6*gu`Z7agox<0d)I`^srpM1uNKr0m9W)|TS3nZ$e|k`{wLo7}>o z(vqj!BbTY5p0bo<9Z7vkw855^vC!ZJYu1;xro-MmjD%vbM5@_z&3&%dBgItdoDi2> zyx$+=;hGEe%gv^#7_E80D*_i};f5t!@@Mr^zwGwNBmz=hNBKx4Bsk+r%U}5eqv%;iEufdrshCM7|k7UfHjMR*}{BYb5 zw3kZLPj!1FDmK{UY>HUIz&C4+JZT7Wg>usY)vKh*p3NJaN>Is^sxmUq_Il_%=DM7A z7#zv0FC&w@>_kvqveIlY$*d=nOvajtmKB$(wrPicmYiM-#PSFkF_^+mV;~ZtYB3jH zwu(h3>`94YRU*9!8H>pII7=6zT^3PqtyZkjPOrOeVDlEi?kf zT@7XRaN1VR(rh%YH)pV9vnitqOR`c5*b1Peir?snEL_{=;cvuRii~ArbjuWVTMflz zILj%C6sw~MGbH9rb#s!)LzQxA`m4PjX0J{!P%D>Qx%_%15p?*4WK9$e=_Ex%tUFdt zHgj+tLl9*77rh=?zb%oegoCMQ-C(U%WsARvTN^^%mn^z-MYBFGi-g@!bJ^zdy&kcR zn%-*>EEUliL>fkqr$N*<5=P8k!+b82V$PdwxRtLsu$8Xz3D^VrOu?VAoBgy=an?#L zs2KB`<1u|+%2~5~6fOiqHe=aH+E%ab^01K}kw_=8L=qCyW?#z5C3HG-As|ws*Im}7 zJo}HW^J18A`QwtwNyJC}1df*sML|)73?5KwkY=w+E!d5W*fPhDp{66Rx~$D~2qjy=GRd2Da%! zb9_~Rd}auh7a#2QFtu9FtV{MJ^$C9}60RzJ;fV3{eYI5ycSYOtFSJG5v=wk6I2c){8ik6N|clP-!(oO041)J#H&n zO~Y0sTfr!T2?`Blh3WPXgQl8Z@h32@kqE^_2TtShN=qMN#UvibGZC`JN{UXPtG=o3 zxkmv@;6 zcQs2BIod&&-9mch`Yw-(UGUcnsDvXNlZ};a&LV}Bih8-x@R5;x6J;uJL$C8x66sZA zw@109r*T7CL@TC>2=Xw+9_g2`;$kuk!I&!b(>n<)TAOR48*PL6ZhUs9Z>sG0^~(^|K%C z_MnhRqAnYJ4WW@ixrjX)x99SZpKL}Od6&|xDKa83#zaM_On3J|w20?`eWtSm%gflz zMl&g|uc?E5e7G49;tcPW(ynq%S2a`E(tmV$6fOEZWid8IJH`}RY*^r69WTM0E=33! zXRSH&QPdpD=k3U@X152CvIHHtT#k@A*2P2{v^!()q8Y_gv>N>_Ip8h1LZx8Zm`u;r zIz|SX(HD5C+T@}Zf`MelQ5A|AN~fga@r)&;2wuIEh~Q$wOd!54i*&=`uNA6Rmhf;^ zqM9Lc@vOC2hZKuY^fWBRMm$tubTn5kExe=GBf&=rJH?@90gA^#d0BxKszJ;VphKZ_ zu;I0kCSxE+(YE?(SM8!H7B$LYUn&R%IAFOZsaoFRa~dPye7TH8BZ#$x3W~cX=^~3a zba^CrZ#ff(tQD-*B$_^`EVgi+St^IX)*xqdrg8y`V09-WB$ifsJu)~Q5Btq<&PSjd zxD(2IIl&LFXDTwh5#UoL4r|myMFv4uj_>w}#o)Tjlnwdwz&_xOsMEmHC0-I+sXFOF zj3{OHw`i_eg)58qcY8!-M^dTBq_jIgl#p` z>)}k(rc4fsr+pdBi#k~k9&EYvCLVJoGZtUP)eO?*kYA$sxt_WTNuF{&innO85%lVU zR1xg_iF&HS(RjG*tp-Y5Jz}ra$dqg5;x3O+K5Df4t5T)z;Bglt#`3VI3@O68!zD(I z-kRSUPI9S)lk+YASGPy784TKtHj*pqJp}`6hvFVOLFQwYdalw)Dd9%ULs&yZy@4%v zRXd7Rqyox&O}IN`kK0{II*%J`Nt;Z${opE>WIg9~SX_AvPGGYqgEv4z+gsp~x9W@| zfK{VX3jx;*g?WxmyMVm`%9l1BK1VDJlut$BnzYB|aQxqzA1=iq9^rsVa4yzs?d z4~7W)bPautjw7(*!Ch>lT1!!WoyFY@!I&Aig>$V$xlH)xZdA|zkDvOxslBB=ckOxp z?&_|4cb&R=*~*t!EXy}9{dwu4#g8wZv{0Y_(){||YiGYZ3(s6N{qXe5K!?A`=gKqM zmx9|H`fp@enj{mvS2SXvY}xp%!_F-{FW}Xy6JGFrt>xUV+~2-L1>1MW&Eh19!9B2= z@3`q)#Pi&5s34!`m;mI(10WYB$sL{)WanO%<36o|e0D}N$TQn5737Y4Stf`U?SVYJ zz{dC|-?^e?cT&Z>=LeecuC<#g-o8t2<|au-?%_S;ES1OO?A+C|@~Db)^_`k=p4x7x zIQy=WnVlqMxrejA*3WpPooibbaTV!uS~Jpp?YfF|$F(gJ1j+W0b`QM9W9;1EG9Oei z&fl&X<4Nt>;EiPygxmHYc54TXhp1wk`Mml)dy-~^r?!=W=e@W#LE!GN=j{l(c_HD7 z#NGK;nGMwF!F$D2wRHDWzgEBhIP#Lm!E#<#p3<(WNc(QHT3DMP61a!-P;AHeq)}Bd z-Thi6UGnrHBZZBYf;r-HU` zvjQN8MnCsJ_7^f5pM|Yz$h&{>51sEnPXJFCfVX{{m0?6Ubr0SSwK#V6RJC~bW9H8H zKWc{eyf&}G+o2W@J>n7Z**$nYT{sq4M;o$t?RhIM9}1@pDjkeDE5ya#cJbc|Jl9qD-ZyDHxAYsb6RW>ma842cOs(R+CN zRLxkN9WnXteJaj}{-}9gPiq-z%NJkxu()CtD()WD@;$K7si`yJA)*R>g5(PINi*#SzaPYiRMb3z$pWKc-Yc` zwF!hu4I-JOM#%6`5FKGSXmztZE$9 z&_HaF^CZJYC8T#myfsTq^yBN27%;lsE=wM@F$J&WqV2(|;^vM#Xi1JoifqQd9#On5 zpD{)wrp8n|7}w$|@2A zX;|7i3_c3vUn)MYRb)=GK1i|*8`%uaAK_RaX@wP(ndaz`(>d{OoACOQ? zO!^2!F4~C%EHUX!G{k_V4u~8{xS)GO4$z#1Ywe4|E0|@2S1uT^ zsuaL5b5bedg>XJ+t~x|}*+=1U%aT%9j7>$NF})hbkRk|hegru|Muv*#>0^=&>d=AY zT}8JkRr0RQPEWGE9y`f19WHsQC=AGw`lkWu=-SKbO6$M2^sgix9hNn)J<>dc2HFUiC>G6bb|QeT-13Y>srX8}yLMVd!Fy<@kIkm|Rc!5Im43TvDLKrwnnz zAPPC$755YZ(GYG3x}j<}{`W{0yHXMrG=>M!P&IEY6I{~ffeO5fD#Zz8-CB!eyoPi$ zvmPiq{Tav-=j;9;CKmGf78)WqHtc>gXN^*(JhL8j8cZp@yN-ebc1;2J26gCQv0s~Q zm(<1ndUSe{x1VE|uxC7cvHvef*qb1?aZ)RI?Aw{T;Ky0PhrdO3q;Uc(cnF36-BvK% zeX+__$v*qN^T)|9`|DmMcu+whe`y6}8kr%K@%x-ow2Yt$Uke0xnqnJ;xQ$KgjVNhx zP%@q&qg33M%Qy;9t&mA+Kn)Neoutf^QD1K+%|*=$sx$On0;!xmm`*X7b^t1F zWm%ZIXzIbK9(aS zrl+PJT*8-@79U>HFMD?#zxvD7yH{^nZLEe@PhXu{ykqhDMQZW9#p4%#xp4QwEenl> z@WSZ}Q}Ykbe{lYq`HSX}`De{NGI!VB2lsw(?=^cb+KbFwKSRx&H*@^-FF}35Tc#V+ z;px-oZUn>F@i}caGP9qkWw@fB=bV)WBS}s?Q5iUQS-nYPySH}Qk>M&90wMgRP{HoQ zf*FAuptt-=jqT3Y*e;{7U0P#1Tw}YG#&$`K?GhT>#Wl8zb=nbJCfSVn1D;?tfy%KG zo*H-_v!QOgay>u^{zN*2%B2VhZE6m*Te?AGyEkiW_a=?)-l(zN-)n65hHkr5DHK-< z_GHA4X39m52o8*Q_U#(my-j1go*a$k+m$0?Y`eE~_>!q8QLi>dFG$eIxNB4}HZYFm zUEOx56cXuBF7Gd3C7-`scMPfr{%|}|0x4>-dM-ryD$?Ndm^)KryLFB2UaGO(85-N2uCd)|8rz+!vE55Fwp-KK z?vzfu@%!M-8r${s#@K!JA2ibI>6fwey82-JIBwL)x2K=S^6lySvF&>M$Jlna03+q} z9frz?e{{#;6oY8cP58`2A&tq)6-u#W_d!0snTN>N7JMBo1%T;yb30%oIeQbpknSpuP)6&?k zsj*!{W4pS>b~TOd6pihw8r#X8c7(*?u_}sJf{_ZZhzVP1VE%TSG`2HpY^T@Q?ktV% zy8F@iadh_?LUEu0P^jxtm#i7!WuHpDSk;)NAr))|8@wp*_x+Hw0lW zy)R^{u~fY7(ODx^ub(maO65EmD~GvsQ@<7yi)lKg$}e*^F6oJ6oQ3P%@=krYi3wtdP&b7)k*&%}F^6 zE$=6ru~<@Z6qu-Bcb4j?FEM~&`ij*nr#lldy{4dM5^;M9eqGS)HdV0*WAU|&6o*l9 zG-;^$y{1~e+LXLyuCUfbka#q2bd-v^n6cDI_;O6aUp59IU$JWQLJ`*MrV6I49Iv|e z2XcXe%i|6RWr9gFY1A`loWFQ`9=rsSGl0B0w2@&Fuq*3`&=Bo)+p7VcEA7b@tImA2 z=B8ORu@-f4H4_nxG~FQMxu!&8vACa4daXH-Xb5?NVF?R*Dumx1Xh8cNrKCNTDn$Lf z*Hx(Vfr2tv<}0@bK<>ghMz7$J+Tx0*J>S zNXzzUI#-f>k5aY9qEm;7k5ecz@AVp8e-_i%XI%t11#M7V#SvlY@meFD?>>G@&o_ls5hH|sz z4iRD=B?Xra@s&kA$+U7($mB5F{0JmcL9e7#ym@HN$AzttxRhcexU*go zgv)guhUysFm8-?eX=609-{ZlX0WXonMJiGbM{Bvky=?BxJ`5=XZOkdwkg;mD6wH;3 ztEki!x4s-?ZCS+a%K0KrKghvU^gzEbXa>|7llddF4PN##ll8qQx4~vy;3{+Zg7i=!UUrvEcLZ`&gFj7iZvo4Cs zTCE_-G*Dv;sE4ZC)@Z28(&-Y6cqtEuHaKe$(>v@LrGhzi0$)q`MRx;7S;)*n`|AmX zCu${`wfRNVpRhY30~luR+{REc(6(eShm@1WnniCmlNnFm87RoK73TfLPldy>3#N1IA8pT|J zdOk3KVR831hGMdAus9ngC(KC6WFf{TkW9{OK+?Q9Y%ax(I0F?70jZX(ujL#ZWvlXV zoJQT{m|O8@vn@f)Gg+$U!Ft@Sf7E+9Lm^9ULtw7Sy#&9 zkQalwg5=LS#CW~tiPnozzpL)sPmcBH+_sG&!+G@qYgHE_yd@o5DUwzySCPGiqBWO^ z0$fZMtN}rYDTy_2g9Nco7O7w^RV#1Hik6WaMBnZ52h?~9+Zu+=9@yojOg}-Ce5e(nA2&ZP}0bSTfY5tG1Bm|L0cgf^LUe3 zxj3jFW^dfafJ0@9HJb96(LKMp2B@&7-F3IjN+A&JZ zF-e~93zOb$4_akr(TjTVQ5<%j+f|Jadqa_n10eM0bhK#?dN2p57 z!nbUMUXr|C-+o)UEPx5rkCL5^B88dgGL66Bkwqh7Zd zbW%_;6p;9$JxN7l&H~skgrq6&%^0ix2qGKp^#q&^gd5FBAs)c-WK+_WDsn|9wxZ?I zezz?x3N6}8CNoOO;mOhEfq9t2w=qQVLakxUDB)yF&ZLSpE<}p3kMgDhpmdPW&@vG| zy-X3=cw~*@6>kfR!erU3py`ST2iXE`GSIb}14mtu6!4o1pnztiL=^TXLlkDC14XnM z4`uQ!>ZuQ6xNjRnxB(@7QN-XEt3uEdE->jz!X@kccsU(|8u+;E(?WP%i%WfSFq z$?32vO@Y-VoirQ?J5kz5RH|mD>;JO%u3@fR)q!Zg)bH-Y`;DQ^;TUK{t5hYG#0?O= zReG!REOBg6r7D$FDpg5UlBy)z2TTC_v>Mxh?Q;P0GGNU65g_4_49R4eB$LY|gk+N3 zWF|LX!X)1?xk!s5?)UT@HxrR4g{ETTkRGW{y}wVzs*v&cd$OHRcx!fVn}Fry#Z0o9qaFEx z)9rUua@J(9V2)BjAK0P8C0iM{dP?RD>#5ngcxHzy%av>(=2dH*WM zhghb>_)7to8EW#lyUgb8IksF+77Kww42nq*A|!o1kiRkFx){f$I_p;K7>$Z8I2yrg zem6yjRBtmKlvue|Qffi)vD$%H*_{$jf%Q_L6LQNZCjtpqdxVFLchAi&JmVa$DNm#L zS0aIE0bH;YcFt$o`VbFhrvx<+ zD#e#OsQ_&6@D<3JHN(xa*ezSKHmg~w5&1Bbwp?t6CAr?B?B0qG&b#oeo3vQ+jTjMU zt+6AxpU z0(ZZ)FK7u}?|sElLRV-)5Z~$Xje+xDSPE zNGO+zSv-zCKPFUqOvMVFJ#qn;MFe*Sc6&-Di0@MEjEL+h61Uf`MiIdl}TPe#VYOdLylxjo2^Uk|B`*qC( z9K2!31Wc%mbbcnlaq=1I6L~ZJ6H=kkH`7*a~%(?TiH+nMO85;thszQ?}O*h zUo;RWe!BN~*4-IBshT$%iJ;VG-$)?Fpuf>D#gV-n1WFco1Tj}Y;`>ZFAQ!V%xtu6~ zOxW#qq-{5zH zm&}@$Jc*$eHmiMNs70BurSM9E&sgg`;m?Hw4!fPt32D@u_c(0zgr5@=yHqHgXp3il z$?UV$<1}#o?@1spDAlcs46g)s+wMqQ$z&S1tDQiS864Z~)}2hyDV5u0hX<5d*;6gP zYzIlz(`cAz**T&~cIb-T#wX;xOrzKI>!61Zobkbx@i+xIM*9+@pFKt>gu|5(S@23pGC|TEe}>wNU=BEf1`q+{2IwMXOz`4e z43tmGoa#6NgL-mEjyR5{rQtYr{qK&Ueu+Q;6uCn>NtQD^ji!B%8S0mV?x+l|;USgo^Q`EWk)jjPr22neVs z;4UMmpMtq=D-mwmfu3kf4Ac(;FHI!oaFy_4iR3Gy+SyQg={pPi7Su?aYh0}x5rj3?L;qMkgX|CzT{E%U}p~JNPjn$2y!XA-L3GhH15fj zbGr>9nCzw?!jlTdP&-y@WNJx-t@nDfdA|lj2e<9F_OFaOz$Yp7`B};_sez8$vGcc- zz*HWtyusSirf9hK;FDsUHIgHCY`g>fCuG5%hyxtbwCu^s2Xex20$Sruz~t@b%qGC% zbZAy-?1cl|p~GaCNqJarRCew*@_TkaPVPw%cw>Pq9XgsV~!ce*1FT#2{d3!*YAV*uIMExB@6jk(k1v&N7#blAs5WzZfC;n z_RZ=5KcQcDf(8G$A8p2zCC7Mt+yOpmnLLp#*#f~69dF`ZA?FK4DxSQB;032IY2Q`C za;D`$_PWgyV@ahLYf=_>y}E49vL%WQW%0qqD~qMY$l~6@-(2{U3*Wi$$qV}z+86K(@P)1I zzuW%f?SHrZ$?bc#o7>jy=YqKZ-`o1;)&pD0)?2nbTT7e&ee<_AKfQV1rn>3ee96X- zH~x6zS2jMearZ`J1KN1b`aiAzKFBe!zs{{E)}3qry!Iz+-&y<6+S}KtwVzpg$?89> z{DzBKE!=YJf~j~c zx90flODB`Om`X1(Ex-WtZ94dHhH4Y_Jb)WC-l{hq?j0sl=jO8hGXq{ccN;B`2+H&p z9pcS;)8UcFRMe>y^q+_Cqo&*R5fFOg;W=y42pl6DfazA9WL^i?nY{c$(}EfBp3{H6 zkq_1MW}TVrIA#iZV@7X0CJ#a^6QZ|Tzby52ZV8n6W!mBu%97Qd#0>(cm!PV%ecNdBVU`19i$e?cGISM1#UbFX>&5iSw&^;}^%#{drvuU(g$;%K;vh!&h`Ud|n6F!kaqvJ?^MsNJ&xW-TGmNIURecxmg1nl>HRvE2c}}Vu48%M1T5EdnZ8$VACl=q`i#6sZyGZr z59*`8U+;2kc|D*v-lsPno0IqHV!u~!oG$j!L%r#}x@6z2gX@xgkG{MP^q-HG*ZsO2 z`i}#QNb^2jn)`bD_&K>(?{#%*uXpQ$yGQReMsNqZ*zeZ642iw3i|t){(=oB_>tcJS z-e^#4S9P(yLkHKzc8^Z-=f;t|TW@^(xW;$sVxU&En^0AMHfX^ z?=mEcJN2QcdegBfy+a?0qBj~G%I!Mt&N$pXeVlE*Q6FblhiQ$2QNbF&Wp}|OALoY> zsn^%ZmQ+<=CrYeI(MK!ky@p2H(Ph=to5skhtuxTjyBwSNmfk4pjmM-V>(UbR#%axHJ8?uShh0o~CV}xJTkGZto zWpK=`=zNv*ABX0X)tji3O=VpGq~3IFsu-P>x9E*ySfRE1|7#asG`INq#mkFZ3*TO- zUHIaK+rSAXz zXDY(uTaMMa6C=5#&?M@CZkkgoeu9*aA+$Q@-xXVWb8>#dkVo%KJ7g{tm#ghiJQnY8 z;hc|)AH!#{yl@(S@Ms~E8Th;Vyb->08p5;8@n@zYJTb?o#=Fmd{=5->_Ph~(dMd)> ztI%SZK~El=whC<{YiuW0(Q<1MJzoM>Tq>UIjHXC@@ihKyCsyI9Vej*go;SizOhq{A z!8$!KfBsYFjqsD_jqu}B5u&4$eEOt3G8LhH6k(bVc}Rly)Z+1M&+p|UqsegQKYHE> zKRgxTiFJKChaWp{gdaI?gdduUaMT&ObNY}Uo{I3q4n@UNYOmGuwo1Gp<%EdX8CeJO zA3Se_A2@G>@1KhB#4+UbAzz=0@WdA76*56lm6!mipI+xu*+z3zhS$y;;X_jqp4d@N z=kUR4JIaY=D3`i^IbX_Pp=tz+D!C{zGU69a;}1EpqevjLPS!1zTY;e09p`<8`Ut-H z2hJPe`=%m1v7>luu})H;T#W=)YKOb5cQj4-{Cm$E;d`bcJh1`9DADiFMjD}DnrUak z9xgg6!~4%0;eAsPp4d@N=kVUC2v2MPWI>6B32#1x^QkbGjO9)Y`Q1|y9$(ks{GYEX z0W}#`IJVvLdH8UqdrXGl{NJC7@Pv6i9pS-wBix^g@Psw2r+H7Lko45c-gZ(BsVDNs z&R^Bu|G#qX<8v3@dg0~Uzp{OK>&IKyw_dmTo11Ug_}z`#`u_s80G_+{*jjw`d#mE= zGgdyk5?lW7%RTVj{gb9QEdAkf$pNKDrQ?|L#0HzY0*D_aC)iTaG8+v^96} z@s}Wo1GVLPWLG>kgWz;sEmfFQBFZ+Hx~yPyvNrlUd7>xe@;(QMz{5mP6!8S3vC&+x zGZPv1sa@W$obtfv-cNF$)J6>iOysMAtHwKmD(VKs2OBm_7KmmNh1;!oycTydk!}R( zm08G-%slGD#mVfy1+4m4zxqh+V5i+#PJ(<&BkS$-r;Pd##(wrH?iT?a=a6>64!a|X zcjBW7;bx{&I{x{sv40zomNpX10TM06N=R95J5sDSAQz&gyee{b+}B71Y?Qa*QZ1Qg zv{ZD=JTGGh#qHk;NNR(U?Ly04u=K*R#i6*lfZXCdiGa$O)A24*uehpWo^!dXg2yV( zOmbv=^!{Z)%nXPbc=0$hu^S&G3j1#c6Qx;?$G0jNv7@Nx{JzDPM6nJuDIjXf=or>kX3S8Q$5OU_4;Ne zc~Z5Q{Uo5L8HZ>RO-Fn&)fsFfnMflcwg@Piv$X{&8sT$QOB_Ne#TzYT9hsTw9oY$X zKLO}yDqL)ONYcuM^#N#Mk8xyCUShk3XY<8*wAvR?i$T=IWziU4Z5-) z0O*G|tVwCNW=20Nm9*tozrF0@ly2nIi$~ zhk?8jL)MF;L^D!VU_tmJWV4Q`Wjhpx2~E%{5Zp5yu%0Q|gK4-W)L@XV#$~t0dUTHs zbYcmMvsa+oLW1PgP?U2+ArO=X;<<(?OwOM*Ou<;D!{ZbtJtgV=4iGmye%cDb6h zOYb@oHIKWKDBJBBCY37`s$xh)-I6Niv@wi{QyU)C1Y0*kQm7Mhs}hR(Ts1LOtxI7j zoKLHS5=;sp$90d(c|46+3q|*E5au-?ITA{G((Y3D7EciwqVrQgu3&VB7; zR_Bac^{iG0I+@mKSbd(FHsQF{c`_{LDOTsnA~|Aph7V#tC3|y>1z>5wO}VI@a0hz$ zcvS0*S*j-4nz1VIglvrkb^{0>;OKvDDS0L-@Ds6=pnQaN zlBLYDIIvwQAs+O$BpO26CE8H|mAwdSkF0`JSp|!lftYH{A3A$Jj&`)ksJn%f8b!K2 ztOvsTOF7jg2HhPl--0L}Rc)C@81q=Y4o^53jxb4(d4TGMZ9)t}V7$cDEs8hWX_*{10Q&2L@ zz&TF=Wtf3;o&s{ffB~fEJq2$u%)mKML2^82I9N(LjR#)4v&B zxpu2z2J)?tr6xDpM5h%KgH5qiEW!}UHS4~5LGapq9lx!NiJ<%21!luyv{-@bBxraZye_$z_dY9Pe6bvgQT8-0P#^$V;#3g0(=6bk_JglaKoLx zEVTQ-W$ux=3tzoZx$w;G|FA7=KY!~xTh*;+ZGLN$-rU;w`Ubag)B5+<-?jewwZC4w zw)V5Df4}+{BNIrwf!PLDhfPWN96a51LO?Sz?TJ;hbaBo_;- zEw))1E%AF%WaBna^MN4RMJ7aAp^}qrwWB^C#v!=?6sDwf$&*SKWl#}4-QOLc{p<{A znMmF%0zbQ3Y?aGMxR@%srGOs>)m_+NyBc?z8_r5O1B(1quyp@51GJx+0d2P=vH*qM&w^Iy)GI;46>Nybh!t~T z84Dgj3vS#@IzUDDOu)?b0$~UT6*1HOR}IiUHUnDD#zn%RYFN#ug-F>ZDD6PA;g>9B zA_+I~icI1tZRR`{(FV}oGC=#O8PM7@aygR=l#2x@J|nm$1fgL4?E$WPjo_M4HUR5y4RF2N7_PC!DboE- zfQvC5%&Hf+9?cFn=HQ?}0_m>A~-!%hT15Vs@-!wq`&Kb}e@QkMWO9Qm;m;tQ;FD}+! z1Zd-?pF91&dt|GNE|?asD)dwc7TwjSNOb1S{IyS1?Shnrv8 zykj%C`J9davGJ=L_ihw6E`eNtzYOyDlk1N43u`}I`=zx9*50!A+SPwt{k7HmS1+%= zYURf(zrXVNm3OSXdBwW?zn6b=`JrWM`E{m$HhtIhAydl~Hr>4RPfNeO^zc$;30az3 z{H?|7i}a#pab@AZE_`j_V4<||`i15B@6UgFzCG`n-vUe;`GXFAc9B`Q*x+V@updnPHm5J{=G{F=&_Dq&9*=g z-e^WM)Bkztz&_Fu>CjDgU=-tTT{^HG>u7PpnHEmPMmzqeO9#ksN7U)G=9Pw+9PRj< z&;hJTs+k}HN!nI~vUIiGqlKokCzOZr=)j`E@IO|c@| zLnLHnq97iRby>}b=|4aRdm4-cH>0^&+~&1d!ZtWUTe4EiN=Kz`9&du!PSR|BrWlHUG8C#-c&M*l&Veb$n7`FyF;0&py2|=nQV{)zQGyMv5@LCNf?u=C$5hbf6 zfd?X>wqr~)Mw6jv&=Jixd%a9J07u!PnO0TI^vz2LyT>LHa#{)zoINq)-??<~nqwU; z4yPZKJ09H*O#klE!K;Tm+MNyuUMq#E(WUXrmkwTatfRw$%3&&*8SVH>mkw?@))95s z@hls5jduL@rGrZ&9c^XKg^eZ`FntR;c%>$(L>Q8(7Sm2ya$Q@+MLBxjVi>X}{q{n@ z&9ebpttY{u3?7zQ(>I`liyBN8i*Qn?B-;x~yUJw}3Ct_XMUoLv3TfE|Ia1B}f?Y{& zDzfPpp@UawFv%93Vak4gI+<`Z+hVO$jZ`D;c&mb>TRBh}ns#LDEyWQ|I%U%@KnFK# zFr+{1bBSiO!+30BvmCOCB_i1qV%->-vcWkq)~YhmKqij1Wz*N8gB=YfgLb_kv+8g9 zeeQCTa8<%ch_}%>0Z9a^MA)5cLj`+}EO|Ym>1)uz%QYAY^W|+tBpqfwLBAVS(QvD& zCQvrV5HQ7tQcRc3x=^_sj3rG!4;{QrgE8PzHGS^V!A--H0_-r{@{#;#mMqg(E*-pd zq@$ItrqZJc#7tklbnud69UTsctl<7pcb@4>mkwThtm6bPx9N+Q4qkMu;{+GB=?j+* zUO3Wmtbf|{dFbE;nxqVPiA|q{4xX>U7;pibJ_8**ZvZp9f7kSB=-{~;i~;AZ=`rZw zIU0-s&#dWF(804c7z3_S(QUpjc^@T5%goti#z>EId1I!O9him% zJjv;0dg#)@(y@*cJX)p)p@T(@3j^*d(*w}Kf(B#27iD@MbTF^M7;sRS4lW(c9UH_1 z=alKapeXaPjuSjjruSUxUm58*)~#f^|5E?XV;v{>j7;}k>fdp!;{=D0>E28I+ebQ% z_4b(F4fT72buoMD4^toNcQqIT4i(cr)K@ha1KtwTRj99MFa}&9rhA}%M}slo=P=z3 z_1hYZ0Vjs(T~NQJ!5HvRnBEEXWevuF`@!@Ms4ra$i3R?N%a zyYRsaZ`}Sb+x6|WEp_wJO}ptq(}krkEhQI!w0Pg*t4x2r{N{!4F5EW%&s*E`)wyfn zf9LVHwY~73^T6{oj;AGXE)t;kSM_`>2DYoyy_|Yllc9VpDvDW%+)5Kr`b}ez$1vcBFFuiazHL- zt#Ua5Ub5xeZSVykEzlt+kqN0e7nI7(Oz$Yo)m>XxML(E-UKKQcz`)P- z4fwIiWUtl%4K3|fqiPF?Edi%G6w>A67I&isb-PR$YcW*H<8fd?R~B!D>s}$bLq=4o z*=R;;RIiJv0k$!kXb%uQF%l?im@N;MyCRPcr(#Z$hvtrM{e zj!G8s+2i%B8dYYdbYthOT-CBn-?IDc8#dSC0GSBuQgHOK=1#8C%#t|HxYbY|Bns>j z-Vx93@tHebKa3)U!c{HT^zbHcL2Xtd7;T+REhG+iF|0Y zB0s5G^{a0MbD|M3aP6DD$1M&k}JB+Y0T zxSh^Cr0HgVG;66Q0P^bX8b3h925!DH^P{IvyxIdaG^1hQraZGnK5jI+fYOtnKJltL zqREr4ykOd=ltC%~8$Kts?r?(NN85sBb zE44(-?NV&zTHS^#{xIeO**rCV+JeRrLjxJzely83#g+{I`M9I!@u5phgC|Rjd_%y- ztizpmF5dlyzO0$JcdyJF(c9CHTeF%tMs-oCq(`ucsW`ct^hE?JxvL_A+ndj3IyI}?TB}HwGn;_DLb&PQSG0%NcW zC_1#qSg{@zLzD4_t?BV76~sbEtmUNJvFrBb)2?o3H$*t_U8hu{nRse8|JTaHMb)e^7PbPx!|qf9yZIDggD`;Me^CQ91DZzZRH%}#m1*;KDs=P znxCIgdgdDpXa=Rxrub^6eYUCWatzg?rXM(*bP;0=$Sq>UhnXWZ?*pq{Otvrd)FyGu$aiJ#IVzETd zU)_ySl~@H1d&1@2nW-LMtr7EqmUr6*Sz%4o3K|>q* zVgA3R`LE1vJ$vo_rq_UfJpK1CTmtWE^;PX$*Kg0CuQzmLhk&#t-5Qg~-+6L}xY@|F z98F)1I2i~14LC{1UtJBn8ya`6yJ6VmQOkO2aXJm)^o#4LPga8(PJDlZfeDs%#vIgV zHI#OXb>eZ%P3(266rF9>>OxG}3$}U)mqPqL(jAk1&L&AyJnm?>D&T&9Wnvs`t#8vql#$2 z1@sfrq|;qMCv@@YM9#qKoF$R-S-*jf$}Wp+67TRXE!&9wPERb%krMU@bzp{3hY%|RjCvX#&E`=G$e}xS8yu9?u7&f z@)A{#9Y$d?MHF_kG1?246jwB3Q@ZY`s~RpO34hAwC1yOAX+sC*=3SkBXRyt8=g;>G z;jHEceWK<@^9T+c1cTGO=Hvo5A|v)nLoi}K4!>*ul(yhQ;^65)L)(M34OwG^uFAkw z*A>XR<_Eo5tE+0ZS|BH6u~>_c10?mZm|+W~btOAwiu7Za57?rgOqtlH3l=DoApZ)=)&wK#8Nxt`xfZ$GJd zV{~bZb{u|X|0!w5z}eYhB)KYT-xY2~G(H9#iIVpJXI{L+N}*jsPEm>{ve;oKVgnS$ zIvL@#x7X%)TiY5|<9KL~!J#fm6dZC}WuwigPl{Gz?w+NqDA6-7b^%2*bT~saDd6R5gCIK9Dt>k=x96;IbOuf}<3fOL z2D;H;+zv68c#9Kp#2(}du8=2EDbxd{Xd;^{c;bA3%`4%Ajdpb7Ha;Ez=Gcq5X1lv%o_=aJ3nn4Q~dHNpp6FN0LN*gUUmDYsfEnp zwPTth0?im|Mn?7Y)?r1R;n@Q(mPo!L%B|Ns92UzHIh;6bc8%UxwXO8x6>G*5NM^Za zBLJSx&~=j9E5}HC%Vx{ic6&~5z~zUfeDCZPBP$d`WIL269JbRGAv$n53Kt3yPlp1< z-0Etm-VLd#80Uz`m5vo=J z`q0Ncf|$KJJowhSc=vPqy}>qYn>P~GdVY#OrvBtpK=BQ{>VE1f{xQv-;WfkgX*P;Z ze%oLXt7)&Rg}F+YZ0$h19a1PoG6hWJVv&f*mm#9lOqElK%--4SGsfGIUa(B^d@?1; z!AzNobjzWL;Kl5aI~7m2kd|5sB|~XQ&INqdU|Mu0QYh?cs|w$8xoR#aX3r}jw-*yD zRSOHe>AbZxvvmn8Yj_y0Y_UygVk*_uLU;k;r)olQC%z(WY+cTnLO?BxE_hh6<;@#8@_V=nF9t4WPUM{@nseUo?(~)1F2wUVc^1m0Rwq#jO3!3 zg@Nq>0|t`fobShvE7!LSGhpD8#p&n9E7vy-GhpE9!s!fDu5TQxEix%z(2X$2v&X+z zuCE&=H8ugx>7o3U>uUx{ojGej<@)MzQWNqhor?fh43j#u0GADtI%6C6-Y^5_JOy)x893i5xc1fo0|wH`obwc1 zyJDDu^PPfgcN%8kT&JLN?T+I%Z$dJ;^RVRCZZ}Notd{&*&mgHYTk^`a?r~BRlI)#} z09C`J&MZL1AgL!Hz|JVCvB?LI1o#9MN7Xz|Y68>4qdFt0_4z-ad&&G;<`#c@F|zRM+yA)T+WMOG`*8{OQK! z^*>mDX#H7h_pYt4erEO3%C}eW<-c5}O@Dgf4%6kOKUhjH{Ai&vcia30fM(1eX#4V* zpa13l1Q<@9D|dG-EHg}Ub>Ska5LP7@N~A=#5YF2yQi`YwNL)(5v6d)vHcu|$<8r~A zJLbA*CjD-2gm***G}0=wLNQxO^BpmjO7`N(PB4{i*TZUnNV+|x9jfZaN=+fa@WD<* zXw>4PdG$B{Cm7J!**VvN{*;j&$+-r+=XSGN=BaRv;R+p4$*;sJMZ_UvaS5SoaTd$? z${~nlFBZ-1Ts&dJ%w0!C4wDXA2;)W0XO{hFHb52H_F$)Hf#dG9L*8M+QoLD>lT@e_ zWw8*act*Yy&VTZV2QH$7EnHUg6l^VjwW(GTSj-o-R8=7bIoiQyw&W)%GU|=W7x_-e z#p7f==gs$0ei6&k%{bMNyI!8Lm+)RV1G{K6L3r4tYbWdSd0B5L+pMekLf6HoMpJ~( z-+RPEwWxYaIl+>0m)a!RaYS)EfbcMk$gWf>w}yHMVY6J_6_2Onh(w7_BoMN* z)j)|*%8^VqP?IFwiy^h0V1>YawksM6`zx?bo@erW}D#}-So`_Nq(`78%j6iKEk7f|K zM@HLqIkOW`bt3+VfY#8uCzG;dcB+wJh?4zsuT4opm-5DXBkN)P)79lb zQZvI2xG1(;e3g-k!wQUm$DcNRJB3lmh9GbXyjK~W)15>HQ(a`$BT!8;OOZ<8gte88 zQGyWIK{eyS$Oy+XP-+^ZWI=4`rR6-au+Lc`{^BEUokE4buMinWVbI9uqX0__6YBFxch!z(aIEo^q8F%=Jm zq-dS7+v%!=TP>~~CL&92pCAQAUh%}(+KB=W3qKsaEEfjYi;1S$pUMRy!CD@|ua z-l=l&YMbt~d+|uD&c?#!(V98yUq9l((kmvbOw5eCUGW0sV>{*mmUXgR-Jb0DD_*o= z5yJ!;3C6^Wy`&4ssvM)BP9#-IO7?h>ayMMOts0V@840|Y4XQad7Pp3!9kBV9auu43 zxp1l-B&6nOg_Onr+qDeCu%@k+6LXC^M}OKm_PB1aA~uKiAahqxsE$B6EohugC90)% zu*0Kil}|OKqLiuleY8jgLjgH@vD+acexJ3(^&HIov=kG^^}Ty@bcl_)}5J<4bj7lsbAISbF~v4{cAR8nnXA zLWeJP3CQQA6gb}QHR)cm4xZ;_Rgl1?E73lu>!LlcrZPkqrAuK%A#znll~i5~L2j2P z3z=!Vpo&zx*XaoM(vBiX)kH5}!=iOM9TP+0(G@lKts@>-CF!0>kp6(RDb#kTSSp`NlXN^4m?JTzNsv54D9s3`d|sYI*TEfmvn(%dO}1h^AJMY4$qI3ML}7wuVzlDtsC z6%>)84@AJTb~)GxwW|&#ZT9&iX-f)g*N9%Fmf4}=DYe&Xd0Qo3ka9vq?2Pj8xg#EU z6|1sY8L!q_r3{q_=F2`S+8}ujHnSeFL2`L(y_zb;%f5@wblzdLgj$7`+1*HaI=DFq zjGGVYq{@XRRPakmG!sk7zD^I>5%b}YO9J}YVoAakb4#P?H|N;jm8ftlQz_ddjwsWp z-DPpxb7X_;1#3Hn7{+-zz}T`SMXr$oH=4?HtF#X8ZQ+Ic=avdfuU-7#zzgumVrwzD z_#$wx|K$tczwn6*oeRMWFWUam_II}5yG?J~w^u<`z|U=6*-C8Py!rQ=-`%{v$!-43 z<})__Y~!mN@7l<3yav1(_}=n10>#ut_letm#=xf4=nfrF$0cU!WEcPy_G} z=07uk$9!!5<#T^G_iN)Wn)AOk?$@V(d+JHRym;%vgh(ND^bP&|x2E)lhmVPx{hfc? zwBDm3zw>XM)_eF^GQfRhck#{h6Zjrw+Kk^Yx8$Z>y3`~zI|G6 z`)KdpwBEMS-rZ@vM_*abt5bTTqXL}&B}2WxJ+1eNdH<%N-rtzk`@|~wMMJ%RVal2s zot&3U!E8UVroL{d_t&QNKCuG7YN+?mPwRbRy?$j{?@^J=e|cK(6YKR$(|Vs+uV0+j z`-GkP!nEEeR`=%&_5RGX-X~V~=MDA#tfAhYp4R(>E<846b&pQYi>F|=omkzE8tVOt zX}wRZ?oS!&{YgW;KR&JZ30-((TJICO@KHm(KRm7X=;X|Q%uw%-80!6@X}wQq<-^l@ zpU}z=8tVN4L%rWWt@jCyzdo(^2_3#>sP{wDdY{nB2d8M|sJLG=1v7F&D<3e_`+d`T zpU}$p8tVO?X}wSA+5Luk-#4xI39Y<$TJIBj_U>uDk57)(Ip3ew`-Col>dX_*-N=dF z`_p=#Slw5r^**t>@0r&7_<9ASeAkrqI?DD7r(lLptk=5@^?v8H-Y3@UJErwMu>yZ? zTJIBU>h05dkBVgeE-n85m2;2IUHHhL%AX3V{5`N`0r&d1Z2bKOv$3>(ef@Q7-(E|B z2!Ph=vsb>d5?=lhxUav=^bON%mVS9Dz4&K~-Nl<0KCs{fxAf-xvjD0m;jh|9mlr_c z2*&jDJ7K0-PjS^U$;HBIi)~i8k*mj;8Zt(zLSOFN0j<5Qxali999Hh6C554-T%=9O z0v{bu>qc@m`NorsNp+#=z73#%6F~pvnbDsNml-BAwyN}PePkf9xamtXlNl~`bwpr7 zHR{{?uukBMGZPrD&~ZdyLKW+~=K5AZ;g(zDrZ3D)VS3$@(dB={!-N9a`97qv04D46 zGqZ4*gk>1@*pk}iz6GE*gC+gBnV0m9ja( zvbhnMYX8;x41V(T@S$;*-9R<=%l%gYTAF45#LR;kT9=OxWl}wMx_=8muUYnw&y0SI zqTL`fp_=<`{Y(0kJTfzxv6dZ~PzCmxI>Fk;z}38n1I{hI-5&9Z-Z<|Tb(*&F6c zvm9-5jwD>L$PKyIvi2asEUVp86UG_Q9q_3(i)zg{lVX1d5YjCBhh`=;X2B6K$yy1% zV+J|yBlW64V=!%DU{@<4IX!h}o|hyh7kitP|r?t zJgdVEyD=#&lI*__P;vsh@!-sqMzT^J(U=f9DD_`37!6~3VCMN5KNlfGlZtXiDzTooOYk-g8yQ>du{1Ch(c>6|Tx=rNR$>ERYrV{cnCq&>%B5~ zPgLN%PNi40mi?_lLao<*YB05G_v+19U|z=g?o}6iDQJg+AGH~zZZeC4y(W0wROsjm2)es$7^S|zi|kWiiWtHO@cdNFSD^gk_qI+5^$GR~m-6Xc*!ZhY&c#^WGxgBZ{_G$rDrfCQ`~K<7&N#gjipV z>2M(=?ZMh1%i8x2A)?(>qd@jZrwa|_lxP(cQ43&B%-Ui-6(Sz!I6ZC>4%ovzY_Vn- zf-?+JJ%k7sRJg9X{g~TEs4jSN&)p7$#)eZmvYb0%E$!FvLp@L%ie=0*R!}q&ZM#Z9z*s)$o~JO)hIz zp%l|)J+VNRBq)o7St=;8C>VyQ8;0O@2sjGa0tGCnmY^)pumb7x%H4<^s*7RS$-zFw zT&<(7ZpvcPH42#>Lf0q*5V}SgfVlZ^9M+oM8Ds@|(_hB5~go~|+jZK>KnBuTTh#ndvf)Q-dR7uhWHD^5PyCMVc>M7>#Pm;htg4|r^wkt z3+=Y8e3|NY?9NQXO2!j*xq)+ZFHuVM9M7c>X&8WD4k2RJT+|bidNxZ2j?unqEn=r`A>&5s-;5NBD=hySf0u$uDZt-GK13`-U*;gt!Qtyp)N%ylf`0bTOWr3h>~F% zlwk<+5Q1Q_Eri=IbX2$8M28*RDiPJ2|`8iI(VKwA|*U#6L4ROa$&*6axQ z{As7hwM864*y0G|Vug51incqVoVHYAwnod-ZR2hSW#w~ax|j)Db^9&P0MUR#=9|;kbPNE$))R{hYyrz zJwZV`79~bM?v79hgY2Ya!7C*}xHjGKXQ;gh=71w;01?QnyNi@D!Hah>dm;#maNP<> z9HwzSDg`w>s0rTH5t8M=ZmUv-@usDOu?;Vb(=B%$a@wOXBQ!lMw--rT!KGfbN3=m{ zBu9!z9r=#|=3~XVJB@a=gC)JbO1E8!M{U)Io?}t(xIe>ouvl%VCZ+=d5?53uNj&9*N#eX16RGh8V1P*Mn3YLpOG0; z=y6zEo;EUPG%{mTYXSWjw#{WP7Jyr_$Y;E5ZXF^oK+9NXiq&IDz6B31`{dYxF5D;~pD zB?Ru)G93n21UQ2sK6l94C^hRLx|_kvfwJ}gXYb8}CE3!uuwHMk?>&tK(g@9HrblDQMq30u|Geq0@B2=k<;#=je81l> zVAjl&;)9cNkE3RUX+^48kG<(2ZyaoG_1ymI@ni)x3)kMM{)}p9IM_G6$+C^LKGd7? zv}R|7+jXW-`j%Ivt39_|g!DF}ml0|fm&`inj}Ed%4OMzIirXc5Cwr zy~h``&M(c4$9p$VKNp$v4O^R^tB@~kZC<=o{sqc6?`Zj&kqirCm#l&~v6PnII0w?N8kLOSj5fAHDfwH`AM6cjIr}_?{cza{XUi|AFi3wSNNweBeL&8Gh@=I5>eH z0e%oA-LFJ3X*TUr;i( zd_9(n^`6qtunV_OF5y-g$l+G3&3e4eSFR^{e%vx}Q$#kXVyyG_w$DN_T zt#b0IM~jNpc&|-%%s5HyR8(KCsC~D*2!{vI45%Q>FZB|4*1ho6xr#5SKcGdWHs9w9 zx$yNXui&dU==UdUPjKJo3%>C6_$t2oeY_CdvGG1%(1ovWzKSor-7fd6;g(>h2~GD{NVxr$oC`2Arh7AH7%v%Y-R521 zm=^%|5BclLD`zCL;tU#~$6ac7^n@b!_a`FbThNI!c7 zd<7?8bBXD31!#_b_VEk1zv2>ZuYkkR&pvkH_Ny=B_VSP${p@Qm+nKf8b7_SPlbUO~Hs zJG*z$p*OG6p|8;+;m+<}__}cwU#~f+J^%i{b?sZPJ^Yrtzvb571po1+`}1A(*~!J_ z$ydB^jQN^5^5!SGBP&g0&D$oNb1p!5yLn6H_M;OOne%ai@2$eg{U zXQoPyZ6{@;5R$c}$qo{nSV(afmGRY+1Q27s=JMnvK+!MIN#+HX>inwa1zqr$sRQ0~ zs(A@n@bgD`^{M7-R>T)`s@X@n=LfB?ytnlvOzRmW5Eylcfe5M5cIR2!M4;N-iWN+S zL{8fTu{jCB#r^q3)CyF9msNhDr2g4YycaG9I_a~Y2z&&?;K8Szx#AP)dB6EF?Ba5* z2ySAw4&g)|GVBc+ysE{J>JPTm?nyx{+p(rCiY`Z2M!9<&&K^rMZb!w*_^~59q6oN8 zi(&x_M8LAUJ$*S@)9(4Bf9`X?@zZCw&*g=`_xknIE4)(PoboT5g^ zkdy}z`qL|jSDlak=@rzrE`DDteG*7q;D}s1f<{tFcQf6MCoo3I{bpyz09~M$S;b>q zeW4C~A*}mi1F!I!e3iWL5rcT%mKX0kD{YwRLF>?-l*DOWP>Yui#m)`B`1{Uh(Leq1 zZ+(xlHw**!Z_p5aX;uS+4_Y#TBhYy^c;(V-h(6mAMUUz^%77P^700g)u}(22f1I|( zddYI4`FJyCz=ZjrOE0%=zHMfaG)_vWCoCL)56Mr+QJtIX*&^=dttdfCh4$e^kwY%G zSZp=c%9`#B5W_Qn$iY4hDmxytB*@n1?WvOUWB?SJ&sV*(4CWUT`(1?L z@zL{O8#`W5fa55q49?TRDKhWmQ*9Mww1^fHX5LTmV5cSAHqX}M(TuQ{_IxAuy$*X` zoN@Cve#6ug&E+y`E&j)E@a zsNa-L1&^m2gC7TYtcMEr1n6hE)KM4|juPbkQZF#fD3S85!el-)$_j&7(S_6J)fL`+JhYt8POu7(jn1qivLo5i&r@ z%N01DKRS5Vp7Z~I?X~Z^@pgItZ$J2n`})n_36%f-(|h8BZ@c-CxBhSN5C7$jue|dg zo}dr^_j^Bn{oy;`aqINtPu%{)Z^du_)a_q+>-XLLAMcWP{`8mP$lzTq1btNZ7tUmf z7dzY=*lT*gY(^YVdGN^bz>~RPGU(a>k_7L(QwsCvM7U4Zt7Wa@5uO_{Y#Qs@aDZUL z4&Yq`u2Pk*x0Z=Vk`>lzCR@9p2}n`{`TeC`=>4C5-UcC|JCN!2%C=f38nfU-do4FM zW^Ldw=}vaz*yi%6=>hF|&u;k~xg|YW4 zrSAjfmf3QAi8lP}TVMbA>ldnp*H3jy8q$1b1zBRx=5ru=M$T6A!Ws_L%}$M6l;GoG zu?aI8J)Kg@P9FFzx!EQ|RDlN;*2(!sntCmn$ih;uM!kG7sP-ZBs8FXSlK?5B)oME4 z?rT|jsVATpngB12cuUCwV~X@-n7RZ}lxo1UYSV*#YGl%b&Y)D~{rEIYVV(`eXybCD zBq+(wfXb|J3FLqDxwjvZc!_xv#1gu&{U}bIm@1}@ZiBtCA@gGXQWHM*d@P8%Kgu{9 z3o*4e{W$x`)Qb-X5|57*H44=0_g1d>3v$!^n{bk*WXJ`^(#gqXhNkef>VvSAW!){%( z)Tvj`Y1Eb9`M%TTTvFEf5T5u*M0(QvD09AK53(laj8y(Rzh zY`A;vc^kYXmj{r8O#u? zKIeK?>8E3F7$LGe2`qBjY4iaOM77dKtfKD2nd%Aae5Y+lZfCvJ54Zo`3vDoZ@BrD4 ze00)jr}Hq>#1SYkk_X9{ae&7-v_if#=hOPMODy4Tj9Rhf#N)j@61;hgTeHX|{jF0w zJOtO;1f3i5Tza&Tie~F@KpSbTR!Tzd->0SYLK`%BL64KXGlNiE*$)L28e$n-^0o`3 z8R7W7%22jZ1Ub+tt+hRK)8_~J2I=v{n#woHEKK;#M&vP||AfvtBKO%eS&c8DTeErI z51hA}bhddELqpK(SkF@VzCN0BEXu7jtR%Jml6MojwbIjMTZ$EJK@(iGJt6KCT4x@^ zbTRVVjzY6x!U^ggXkc=q<)f+DMxe@y&D&A5*z4{_cxfzdJlERP)mnxDorLKsKDnJz z#Wqi*-X!;InFeoxbX1@iICJvlV0_B1hudbIxTb@L6wo_NMJ``)wHnNTveQm<)Jcjh z%7m0_Lnqz{t{WTKOKrIKN1yit%Z(Fu#ExKqe-Ml*E*K))2}dJ8Ef|6} z0DyQf8%9(?NvDm`5Suk&QuR{n53*`CkX&!LLAIS*OJ-c8^i>&#mex#8ALUb~*0f-e z&)Ts)vT63^a-zH6^t=thFx$}TZmF!?%AzrHG!a`QGZlSeuM>DO=J!-;!UoTzr}?Bu zTGAA+LRAO@bF-UTD@___wh_!5DC9KCwsr<(FFMep#bOodi7L5zPFifHSTCR6-1BS!7>><;TqVJxD`JvjEj3(L<*_DXi;d2r%yP7_ z7M)HLQm6g?2(1en6j=EirlREeEKsefiiJy%0aSBf)p@B85FtkTkJ!!HBc!zyO(&sF z^3wZq^>yrd8?anfxE_#Y+pmYt3}~5xJp)-Zw82Aq)M5cvLH;y!gqk`nlby0-kRbKt zLAJumX#=VNiGiSOa8*9*w9V$cN_WC4R|T*kfk)~t0kA1|=%*}k%H?9UcOys!>@8%dZu}`f#Ak>SU>HX7 zie=cnz;qO1ON<{?C11$n$_+Sc=jj5oeVOd;_K!dBhfYpoSgA|cVxUZ7f9e>a0lQ<4NPDIn^J|xrtl7pFv1zp7 zcj>9tMbg&=l((s*PUE?KF7C`q^Y8M!A^@1-`}|I_cbLE9_a zRey#+O;&&sR(%7OwH$PuU{q=>kX$7&P z;^NU*%7H9zJSG`8&58ZA9KX~Lcb;o8>RjCz&Wx`*Ew!R3CxAsaET!o%Xv;}G5HgG0 z+TCR}Sa7F}un>2}juJ@@(iTnylABTY17%u>8R`4gs%5jq zi1)*I@=_b_dGC&eu1%-rblTUGX@hi!GBix1ZooLbJx&nrK%gVvLA&}STzIFN0X8!k zZK(8G&->1}vAZ3IfQ1Zf!={Zg%J9)*<40CJDK4Q0`CN-pAND$_8@Mr#*GBHN6Pw%j zG)hq%)o=U}Qc=^b+D0rypr-{H^aPIP`?G=x-FddpH81N;<)*=#{(4C8KrnAUZLBeC zKhpBrFUft$lg7}TDOK>YHuJ;zyZxXw6-f8^PFulzwCv>@43;IqOh(;VyPnRKWw~{< z{-^@5z0-u-7mb&)F>CjytF30uhlM|0%CN*OI=BN$sAqYA<%4DAJ&Gn388i{yawNEe zs=-ImOJniy%ie8+ZiqnclJ6)>qa8Z|(O=JK#Dx8sOYlX&LrWHI1AN5cbLf;4kP4jv zYAa8LI&` z@YdHn{6`Oe@a_NQ;r1c^;AgLY_YL%&KmXvz-v0d$zW0It;A`&xqx(O2e|sOl_wVlg zxqD~Ve(yc)-mkd(lh?oM?(ci+XYZzWdv|{3&i`=dd+to$&hC8m$xod8z)5mrbAny} zx3_=#_K)6vdV75P!?*t7+x=TVa4Wfm-TdjBKYH`&&GF3-U;nc={^5=P_MPAKj`5n4 zg#NAIt8ZLI`LGBb1na&1*N%pwwlKTul-XE?7N`_}5b$URF(k`sr-ISY2(qLid|3>+ zgW+30b2Ln*+HMkVn~~F}sey`)%zTelu0nLUt>4$kSlnVl4@kuKkA~x;;pHdm(NH-W zPMDof<%qZq-7z}d@ARoaY~XDmvz<{EAT~t!^9IowYk4$e4~8i>i0KdsXI(?^tJT<9 z*f}^uGD$!?XOV`OO`P$nbhYlbI2bMtAOju_JH5^w$9OjL;GwCfAb8#qQJKt1QI~<~3NgNHo?-&u1 zaDM8QqO`;3&d&Brg3myiK-}$&l@8LUhx%IKNC+N|kA}bY*)#6=C2ENR=jq#&piFPG z=ai8`NK0mk_wW^K^m7U^zz+6cc=t~{Gn|UHf&=kob=_rhA4z-u3>%k-+)Lmchow77 zPz$6I!(+q;&C!r8G!||r^;{e*J(h^j6*}9OHs<5~&Q#X&s>LQ#kDX@6&bjjkj)u%q z=4P9z-3uul*{w1(-b-_~TLKT$%?2{@X}FE_M#PL`Q-9|(LuX1Qs~y!^%v4_Q<(9|? z9Y|G;bwCZPAvlB*${VgaBI9cj^mBrel(<5 z5^dFN+85=u4KJYatlB`uoC-2~AOtC+?_wwk)m3(!t2h3^(Qs7nncQuYs4Li*q02jM zw9<|JWD3WlDb`>LJEj5^aO2p=34r1t16)5%3B8m_7+Q2^X4<1+u_Sj^1il}8kQ7^s z4btC46TSr`^+&_iG2p;d@p%R_*bG1ts8NRHOKn*C{R}274G=Drbx2rq3tB|~k8}J)JR0^gKW$oVTP~{Es$4F;9lV2B*Vl%fY&-*BR-zP_RXEA zM?-`$2c{8jhGLI}g;`8xn^=fA*0q9rGdZA>n2s7)a0j1fju!km!tP%k~nJ&PlQhqk$ zPJZ_xV$znw0nx2vuipWR-b<(h$U+xkJ6@OzS>OA*-BGA%k~ZsO=iK-Yj)pZ-+a_u% z2I`Lu-365@akJT4PKQ#$tz53Fu+3%7TNB5b^I&r{tkB)qnibtaV+PnR&O3nQDJXM+ zaFlS7D0Q|P3`Bb74eLYa+&xZ!r0VXJKHo<>3s-QLVvabw{)Upql#c4|PH7a(S7-*Z zpkq@X{K;d)65HCYDwO~v)RmCIR*goKZzzEo*Fe3SrTPZbbCqpxd|ZwvfBa}zq)yxA zbwiDVwJa_svc}RizL_#pT-@)_nHi(LH&QY7m^p6!$kFifM>>aRyzx`d40{GRkC`la zxw~{@kp2b0aT-oqFHifEyys)R-J~?IYIlIJ9f#?j5XUKdmB_-cSxStXOY!VLFjSoVySp&9_Z-7 z@cuC$C+j9lcWHwt(Tu^vF*r<}`o!D_h7)rP6^f9TgN-WT9H7&>nB>>~>hT%Zw1Adc ztCjfD>J&qq3f!@P??k2pp4E0TK=noKsIi_OC;9F9(J&-9MJJgu?xRwroeO(&%kE~S z^d>c51Bju3EJeksEa%ucch*P4rLGL)A-@;al_(GLY`EUlxKeGmKplQM+VrcCBBRI! zj48+A`1YgWA{GE!U_119n-WoBEbdqB(CV!=eqYB71Qk(sH1GA-%+c^<_sq~8f_(#Q zME*MKx6!EKG}xEmBBM#uUX)wk-d9nEf?Kph#Ou$d+yXF*%oAbm5LAorRs^)~P7($~ z>dAtddS=lnR^Z}f!%mKGxf{=VH6UF+@zMb_jcsR>z&grW4uNFgWdP1LRlRVep6rP= zew?d!{^GHz!ANPCxQ{QO4S{T{jw_on!gc(~XtJjG)poP#dei-)zj~IC|Bs`gl~a>s zY?%mNpsvYBs7)e@niTJ}l8f}JvxkZd3vGr$e|XgJN}OjzXLl$l2T&0JjLY`U0q z_5|YVWUDT4vF#8MOySQu=dV07+``fh#TNU846AbB%tg8xcDHCaD7Oud!GLeD=?_xW zIvU>jKaYkcX{b@m8#5|1+9KGXCNTngfT#&~?8QO1$1P@~8jX+J)q5X0yjQ^m1wX@_ z4bz?Oh$TSF#zVL2h}wvTM0n%_vG-Xxq{C)QA9K!eLzaw#oB)UavI9=UOn@d7C);kp z$n6Hj`ANJgCVo(GY_Kbhk0kz?w`%LA3GXyjm?Y9*qCZ^xtS2qxE<;BNbQh`ZGmI&!C;85 zMq8Mwjw$Cx_nBc|PSw1qyNK*S8!2CuS$D5UIZQ&WlW+76IE-q8ai=*>)tmp~osV1_ zyyn~K3;*>oV!ARDOFeRjU z&C+shp(H!jIJzH_#;o#O4XINEns}>bK~RFdfsYg5?cf-(3bhK?e3ETIxygDDgJW@j zrcwiz-?doUU^%vHm`yKMk9&u=y`!O74^u^?G)S8#VgwOG1+k|yHOG;mv>XvjPPT@1 z%FoqjO?~!drRei9%|mlv6kNIJ#k+ROjLf3z8pX(4_l64QJ=z_|b8_mle% z@BN;82jqknj_1Ayv^=|@B ze%}9tPd|6(LXYO!$t9rAM=6u%G+?J!z(Pk)zXOBQCZnKdfDoCbE+UGjih-7yKgSg_zI?F!;=%cl)!7|?XO zc5(^8_OW5_mT6r@uJ1EXYY+(tqg=ZThWi-lg(>~49QSpg?%?ST_y@`;*G?{h>puE_ zKbO?I2r>}om4NU0)9PX`hUeLeK*c)p!f5q;l)sbOmhkJ{V~ofQ}*kmCFf6<&mvp` zlYflxo)!7-GrUe)^7_*S2yv0JF9Fp*hWNtE@XF$n_S4|J4d*HQ5)l7m8(vD;uTz)Y zKJ|g`i%687j={#j$f;r$A?TT~d3UmV?mwx-<>4TF?P!7E6>94>-a1}VB|8#!dQRgp{ zOOO^E8+M$~S70y*pZ;192FN&Gy9|lJG0YM7_g)C!ySu8_5gfQrKLbJy&X?#VI1ZjY zk&}QLNzaMr7eSu~c@2j5^B;TqZ5Iy|1SgkZKlp+VbOjjeRq%G__5Tte=zHg{UVr?~ zZ@>O8-ihAnzjJa_1DwA7tKWVIl>2`0t@%aN_C$ImRd;jmfAHDZ|_vZIHcmM6(zi{`r-3{(`?_R(2 z*Y5m|JL@}xJ9ke0r<31(lAV0Z$y>Mo-t8Z}UEii|f5okzy!D4}eeRZU>#J}6lN+DB z`NwX4C#VYe*o}X7<43Ojn;YN#b9AV1A(h~naNS>b`h9k7lRZom*ZsLI%kg;9Dxtdy z+cx6K7F>o9>xZAbuy6pOIy_BPQylJ#mEc9-lD65x#mrC3mNlrB7${}pkp^Es`0E!I z?r<}9mZ&i6&Y8_}yx*;+e6*kUo8eAG3=I^~>@;q?#VIb*Z5b4kCk2Nriyr9UnqoxC#AkU{V%eXhsbWX85QM=*^OAI-;>Aj0e4o7y!N7 z)meTUivqY_ts;26Cc7<$*i2h_4&bFzNn`?R>lK_nILH#Z-Keq6VdVQn!XFw{L1q|HeUf&C?cxsZ#kS zAno0z*|G%nUt6owC5fI94xJT_fYaJJZ1~>#?%zGc(0GBO*UC;RO9E7{PjIP)0VW6w zBB7dX0-B(haCm`Q}h3RuvJIf^k`3lWLy zKYX+>L=LNHe7~^P8oiuAl`XljsbnyyT7YQ6&T)ti2SVSt`wa(+acKeZFms%3w0g(3 zX6z>^2CiXQQ08j(U1|d)`-UsNQ+PLi!@=V6CAYT@!cv~7W@aD@5}>I>JH<%MHukCo zKw&vZ#!;Ac`Yq;;w@9a2UOzd;5Nom%Z{vQ0vWYCur*Obw{t}AC)HVn*8U?kVZp=+) z-2MFr3wAo1jI?-OfHQqV?V|(pDEG_a%fZ~t!x3#!^BuGG?7vJWYKy2#1CPR}Dy@UYO8N!=i+#LwJNoO5@7>Sz(! zIloc5d^@r>TD9fpYt=+Zh^=a=*%A!d>BMQO`VMjJj~^}OeIkw5PM6SE0Hy&7yy@vy zi9@wxt2NfJK?ZkbR&OgyH|GZnlI_ZB-0zrh20Kg1ooLH>JQB6pVzpc%8J|FHT+b)C z*1vm@CGtj~ZZt;oR%$b>OL8i-WyjgbUnZp$2eY0Vjf@#f(y?pbTO48x!G4OJjx*eJ z8bZzeWTF&dG;4Ls<|t6UtaPw-Cd7lZzV&;L7SU|Va8rE9S#uHKY`j(6iA`s58|mTZ?YWcE@TjyT(-St+N(cBQn#&SnzRF3bn2n{>UCpFYIs zFZO)1ne-W;1s7XrJQwFLY5?HN>-e#gp>DQ@YzNf|EOp;OQxlXX`G{tq+V+j+m>9)ZRLf!77)sx6g z0zn&2^R7HTig39l7KhH*4i$$QAj3HlZQ{n5SBj~)p|Qr7LIENEf-<7LMG|O`r7J6&V|DOp9yvZY(3SMlS+zJgJq=} z%hb-K@2?lSAhDyNx;MsYyVBZ#xb+7QG2C`C54x@q1qBJ#U*G!3U65z0ksOYi0aIde)#vQW2^nPsQT zP$vhG!7IQ9Z+__T9EJsV2QW59m~9>KAUm^!x@=@O1M;1+-sz?TqA+o3J1=g2_rYQk zk}ik)5>R&CBGX=PUgo7*)~y-r4G40_G}F_FWJ6Ni^?!P>P;Jde7!PW>g*WQ4TsO*Q z1P5yF9VMGZ5srf!YKY7we0}}?(L!nWOgvPgP2xI!8Ege)1iOu~-*w0NQXMkvy5#}f zKs4?gL3;~+np+KEIi^Q$5!7fVm#wo9yTYg) zCKV~nArfGEg8t1PJXjDmvkqbbNyNdFnXeFpi+TY7d@pfFYq!q8>gT4cLyclz|Jz3k zh^!|AQr~;QVwy?ye2D^0andaT#Su^LMi?{HW-EP&8F%@E1t<(OKp8#MUrjwm#Q3mL z!=xVD5RkFXv5mVomPD)jT^qgo-#|Bm>!&WR8Qd7$t{1L8hgVb?8le2mMsaOI&AqL< zmcY^NPPHV!RB;SJ<#@S*6@H29Tb_fuD3r>H-ju|ojX0j3^y=+?YSF>^=0^_}m{YBn zMs6xa;iVzK4S84lA+siStai$Phk+#{v8m6xs^jwj5}PKH}^ z8TSSyH05_Nz1iu`Ff8tHhZyjFvg=Lc-9!m$VL1$WpomLYJy3%-AMiV$6S*;pyBJZ~ zC;!#K0-En3u@BXl3FVoywLqze6mIv`5NijJW^bbTK#HjyAE4L&$9JOB z#tCdqgaEZnC1mg+pZH+EDRox9nY;GMR~{@nPCZRJ%}RE0u}V;=@T`RiSBjP_q(+m6 z&|ccPJW)2(or4CaQ>!l5M>!MJ6t_*_>=mZl#Co$HAbU&Ju^|a?JVM$|V&blO5yPnz zAo4upoOQk8plawYb%?5`@L(MPiUb{6gL1ocFD{TFb;mha6t*!_cik1o_D3yYM=VEM zjOxx;LS`>8BycC0)t9GTg}V+PEdYVXZl9Ucb(kh4Y!9Yxu^B?4I}&>e(GTYY-9>yP ziYzy_(wUst#Ua+&CQiy-c@3?e}sT`yI zac=NtW~-Oam(Q&{Fmix519Zt}KXC|Ngsbg2gU0#6>iIAHRRM^W_KCB(AXz#;n(81B zJOCSRWDfUX9D+Js2s!_$vvW!(pI3Pd93)j7zeuxlJ`iu9*!VTCbN-6Q{eWydDBOk5 z<n-@ilRhvNM}RLI%~Q8C zQo;@Aaf^*m*2uSV^%!|l_mj2n(~min-9ILZI<39YE}c-W0IHLStFuzjRzWyKR3jWrp%1 z+Fesbqy*%K79DXUqzGto1P)uaf`TyxJy=>|!%(XaPVG0UNM*LB`zSz;WntZ$509F%GjF);`hx=K9MU9CYZA(P6L5~5NiRtiC zWI^ch(Og?`$j{ENU^{c$ozjV6`?EN|JoM7wX1^Di|`PNr(ZXIs3 zUd|9dPdk8%q_hJoeU+ZCk58cI?YL-BnxFsw*X51(?JQn=@n3=$^NUz6T~HTx9^sYC z{w%nKP22K=95i!#2J@E%+07=d!#CLl9IMg14BP;^@;PiS6QeWj}5N`u|kzlLw zaOw4io3Iz5d!Qn-qseika)iC32Qc`OniWC--s=5CaS5>=SkkNl2a14&L{xE13<4_T z*)2>0s;`Tz`-Pb>&maB#h5qrgI~N4l;I!pg-NNhcBj3!6_R{|Oyl-E*e@5Rw!~Z=2 z_7^Qt{F)nHus~iUz^;OdsqIN=Z`&35c#|%`ZE*zHekcXdnVCREuHseC_ZXi56X{jz zq0dNc!Qj5Q5&3!2Cw>+kQ0RU^?rYGx7pSpcE-e1Ud+2xj;Ky(Hym;@`{dvy0$i(x5 zUg{f{&KH%WdgyDlMu9@MGH|64(%nrmU3a!{xZ1?J;3_yrPodJ8;QPm!maLc52mb1) z>?e-ajrV0p#yW%A2H8dJgvuV1eM>Wj0#JBs08|z38EDcg+MI+_kI`5_V&WFor0lUm z4AJ=FF-NY$d^MB>!0>TN{l5Wt^v`|n{QjT5VS@YrcYgbecmKzm|KIo4?k)1IFMs$G z5C7o9?|8@qJ-~nb;D;Xk`UmQRuf6|I@Bi=(^Zs|;H}9X``+x5J>3hHFu6ys>?tSy! zpSt^FcYn)W=+4jH`73w+>pQDE_?_D)fBoclountf>g2)gzjONsZdbQQw?A~_yKeo% zTYuozXK!)0K78{Z-~6GQzy79r^J{PX(;Gkhb9CeX@7?~q^}xkBlpt&(b_to&jv8pP z-sD=_nuSo&tQrdwZ`Mjc*fRI@BIDlD4;DonGE&R!s;1ba#%ifM?9_2t5novtT}N?c z>mh}#^|}4l;o3v&j~4@WfUZ|jD0NGBgc)w#8I1Z{O4|){Ys-x`^-kWwI&uBhMGUd8 zK!oO?h9034*JmQwO4V}ImjJ6??P2YdBm<>DD<|{rTsv6cQNj$y%bjN#)r%iq5)`>G~c+H>QlU)cS&DwhwJ4gq(sg} zMAf);xE2(#RSOQtGjW2XOuE|T_O9DsNj1Eq6cGlLeel}PF^f}yo*;$ zzzP^=tiPqXwbIZ~ncn`+i{oao6S)vN>Mq1l8hazkYvQ zNPWje2%%Q1f{-NF^m{Rxl#W!#NKs8$c`_ewb8z+BiF75878_%V za?mi3CwVZ)XPwb%87!E-#JN=0r%UjPbylNQL~reTf8<~RDDwE)6LPDc#B(ho$VJ#+ z0>Nz{LK=+Sy%4Xl2I??t!@mD5M+=mxvd*p}p*n5ZLqHfnZ^)`+VVb!lQjrgdbhA~5 za)>_sqX&zqg;o}9XTrL>&dRo=1C1hVTvA z;?QuYfCCLe@)z?Sg>L{7JZm*lnxR`Hu&@1%qeWNMW>O`>JDJ2CMZrT8uJd9rl*SDu7`n^6Ws%U^Iem$_I;u zy*7vy8Ga0fkSSHBl~P9#a_Enw+Yrs zb1dlqPEjct>_~Sa^b~-7?LRo&{tNyTmq%6+jloGd6;GNKJp&ibvx->^5WX!$c1bBq z*+xwJ=D#|`a2uBFXc)m7+h|7ZTD}A#oE_g@tU5G8ZFid7`dA;C#mk4k=VF-)GYH)D zl>20Y`!QO?qjI~Lg`H}u>$7wGI1A;*7-$6a{QjYQL$I3>zw-d{-Di@4U(S3-2P{N2 zTH)IT)8LCnSJrW7l|;+iKY3`>L|GCXQ7Lt}8Ld@SVHQy^Rf@8hYJilE4~h&UkVUtb z+Yb)~AHsAaAce|JWmHVSq%y;ui*qD0-mb7n0_sOjis85x5k}sKU&PJm9FwV%;9|DiAAC=n{0XP@oZgS zPsDV(1cey2vWWZrd(^=~)3BQA&K2Gz^jV4sU_-WArg_#*>wxJaLfGTmRRXH<)^`rY zO2VWVnN_U;o~BL`Z8I~PthWfcpEtyG;}Ko0pYeUbvmhJ9{lkrqFd^+#cjlnF&CNi$ z7`b6Axuu{kV0blWIJc(u@+wmJJWOx?twXDnoQdj*)vkgD5CzHMLK^OjdI~O9Ej40G zaS3h}0UJOtrEc_&7VZQY@@2jTWrY?MXPS#K5n62@kEjgD8t?dlY9>>IOt^braj*cA zt1^KQL9&1t58ez>omm-ytKqe?Zl;ymXpBEl{JN3u9*QvpX`OpQ%62VU8R|pIT)7qk z>Hv$7=E1p%Sv%|qr(E3fr5k_#5JTM7XyoZciLJ6`9K*1=<(DqS6riRNT~#_ip9D!d zYhdYqe6SE^Bh-C*l#?^i+dF48@ zc#y-0TYvv(F^(rYE$Hr~0gDiTn2DRXJG2V;5L6hfTtKS|4ijyziR>FU4i>mgxK_?h z{h?F(BHp152C`Fcq?;a+^KBQM@}@ zZ_GWH5`YIpRNz%3)W^~lp_7>UEK~S3D4|3)3QdQ;Rqs1WR#Ve;9W9y8a@@g^%8~@V}CVW*~1yx+Bbuv1vvE@m=045eRXKe z#i(F~!5SSdBv2^R?GuFTO_&BDhW%S#cC=v4V%5hwGdvN4VO|ON$d@U1gdhx|+54aj z5tM}UP)={19xV3z*`(K>ATxfu1?(8DF5nWU`70WnOe26hcO?^v-j+pgd{;bJ)L;XM zqN7gArQU?*ybhh<$$Xiv2quu?J+g^RnwWbL!reY>;rL1#05*KLam@8Fh~o{4kwYJW zw*|)mqHYybJEeenH-cGxXvJGLB#c-=QT zb$$PL9V`GNAxwLZQ+j5XKx#fqNKKG3DGLe)h)IV_n6{873%d!|_kQ`&Vp(e&ujj?O zip&d*GJV#ptJz#wd3$FsbtscT#6|0`xO>0xV3D_z(1Mk=lle;2h|9hha)h}McHEZh z?fQY}DE(efcZBrLVY9?v!QxMx-~Zpg*1z_4{P1r-An*O*yB|6Eu3P`|=C@z}%iu4+ zz@P8@vR~i3`49d?wyFQ?L{O7fvuvYl)<48;RE(-^tI zXgOx+%0y3%>9y)?au1$<-COapCT$}J!9dr*tHn1LneS`;=;yo=w?67X9D5NxyN z9{e^QOMtMQ;0Ao1J8B2)xh9KVVhrH%(yW=TIRjr$COi((bO}hqOnNL!nkB&q{wWCj z4_1Rb{`ljM%R%|+PZPyH@jm@&R@c=h(5F!J`sRE8;-~*JFeccW#sqrlCh-^5n2=wh zF`>4MKbm2#xkMBilVnORX1ejJF=0T3uWE(9x8H?GlqWvxGLbfBK#tqVqj}^LFzSgT z%xUV>5NwN7XJbXV^q73|hrpPiZx|D3@QZg$YUN9{PDZ7oQR_kh_my%CW;WFj;6?MQ zF=>&}dcMTB3hs$ch04*e!pWv#F?yp0+(PgrVhZb$TL z_f2Ec|HV8eU$S*F0j10y#_i?3NX>G;J255bkmuf;yM}{V8_zpUuA*@YwOJ>l8LX&l zr#G(bnZ7EN(6J1(t=aK1H7CMwq!?3*zVw)UqyS@rylG7EU&Lb~w_mEft4LpHMT| zG&q7ZEy?2k2 zUCrvlPIsT~)2I8Kb75e>UbeA&xnbE1HKQu2s#Mq{s3et2rMF5dJq#)9U6QKwtddF< z#}mdcd>3H+ii4eiLza0MjB!HDGYc@DafqKJ&I%#6V`Il2o*v#48*sjR>)hLSYB={) zkH;*Mb^EWg*4g{pd+Yn^lfM1!{rf>Lo0INu3M{uBULbXiXu4a=sC$g56nsHWpi>^vGf~iPCi-h6KY|3DgeN2FGEouVb<8)fb{RL z@jJnoh+kX%V1v{s=|#hlP^eL zyZ@g(_=yLv{I9Qk-7C$r|M=Ovp8m_H-}&^*p3a|ko}NGX;V0kpha@$^!Tqn{=j4P@$b6)<;%Z+`L&mu%kkxJdGynd zzW>pmc@#YQJ&&GU{IiR1yZB=l=E>hV`I?jBggp684}RkG|2X~T)Aygsr=NZD(Tn$9 zoSgqdP!+H{=g)5*{`|wg_3+O>Ts{24ho1(j0>1O?%g*L!owM^-*jIl0v!8wTp=V$9 zZ22bA;OCuu^g-)wk`y2NU!Qj_ot&NmQiLtxIsxIQ>9z^6oi#joU0ytS@Z`aAKvBIZMzvJn5yzwwz7U0uw zfBNlj47}ddGgsaEJDy(9fcJBE@Gzd8_D}oA3O_yVo%W6uesbD9?H((9eTtkS#|mGa z!l&@D!jDfor=4SkFHhU2?PG->okFM3vBDRp?>l|pvBKx4?>&9*vBD2eTc@pKh0jhu z@AUJI6+S(E&*^*KSadI2NT;8B`nhio>|tASJESJQMFG|Z{NwnvKuOe39xMFt^e0Y# z;#lFc(;q+m@neNgPk-$6$KH(ZB|e{i`1HeX4E$0Knu`xzeCSx=^NSB&eDGM|hZleI z;%^=+e0K2z7e8>U@ae_hxcD1yJdBqH`282(|9s$86{usf^r@VPMMR^Ytx?-=` zV}&1IF;~p7!k1T*tI4s#kFMw|`dHzMtMS$NSmE<4>WVs6_~F&)YILmd*%f(39xHr$ zMO+cjC-CKyf?wfp3|w;l)MaU`ikDN+EA4~_6Zo!^kDh$=Sm94S`K6OzI#&2oPJZ#^ z7mpQw<>VJme&JZ*XD2^@^7F?EKRx-mlb<_Q_{qu7p8V{w!q+E1bMiCC3SXUkis@{=cT?BuJLEu{04pE!AACx5B%!;>FB zd1EJksqop!kDdJ3u|A)keE8(UZ~RbR=JUzFI{8;`4E!?s)T^s+x%!r4g&$x2m8-vU ztnlU4H(!18vBHn8zUk_ljupPR`pZ{;`B>p|K$Z55#|l5Z`i84-I9B-V>g%t*{#fDD ztFOEIx;G~9rIYfPuKv;+1N%E#4wE59QH5Da*oHpX=`PN`^6V>*6+S=v6K8+oxo}cB zOCrs}AxjF(kY0G>gOSfJh8M$Qg-F1w*{;|TJ zaymF294q|FDSC=N7X}zQZ~vGdo_)pHR~##RcJ}3GUw*9cDL7ky**oX|ch3Lsod2I^ zhF-QMymS5s*r$uMTz@`_LeM+s|F=Gkch3KBSsL%0|KIX3F5Wr+-)$Q2od4glGTu4= zzjX(F=luWHr}57D|1Ce1ch3KBT^jG4|KGAS-Z}rj9@ql>ngJ&RZ{h^aT zb7G%B4}KM7R9-p%$LHU4`bDRod)}N==kI>_&maD^hwp#LJ^U?aKX&%*XMf~udUkXA zk<;%z{feufzWSc4SFen#-}U&Ho_x#eSOEYd;PD5a{>bA${n&c^zRO>^{M(mb_3X)$ zKd|Q&xC}14muF8V4}K6J3w*<)AA0mPk5-^c;PJ)(e({$sO29(!3nxEt@+VK`0BPV~ zUH|p#KYT3%2aoxCy;-8NRA-h!_+;jR#t*^Z7M_EYnBE5s;G?=4Spy0SSF3!98}B{` z1HZ1c*&&q~_VwXRZw*Wh0{t6xe^j6yr*hfxV%QCaxUpGzs*rx>uiRT>-_3X*8~cXh zP2-Q10Zb!M9{Wab1HilG&Mb0l1N7pk)qK0ta(GF>Rf$aG&^3au zQ-vMgCY?lvSyJ))fk3p#dJGsJt^3@YeVK%Ud+ewlYf41jUTPlFd-~)d)Gve-~HzazHkK0ObQnAalV@h^`Pc5)PD8Z-x&wCs9(D=c<1|l+I z(Fs$&59PK-0dVx#(l80IJh&aRS9M6ZP3@3A8)NUvo@*Tc1Kc;fRuJS_2BJ$FzKU0+ zEJnVEI5e><`6``Huy*Dy>~!D_HRH)g_crdKv2VTJ>_z~XyCVP_EXh_APVb4a$yB~z zi9uhrp-w3iGm&%`*emP3jeuBSGA2?4vWdr?)_`_1v$tzzeRgKfca2gHFwwvRC{(ZZ zV7t{lH1;i-mA6g$TDlUecFkLrD}UnQXVy?@_uFp(`{u1=rN@llG0S@WPlzN5*UG7@|@1yZY_g>w@ z#vj>h=$x3Su#7hoEhI?1H!XQ%Rq_D3dE|1oMp`rQ&LJ3BK~J9lYVV=(Pws7F-+)}r zUPCqzvoUE1^kgvz#JTDZn}O&Mpy33|#0ZdT+Kx5m%hTjL{p4K}jbF+c4kQa%l- z5x|BZx63GE!meuhB${e6*4sF3tuLKC+rI{B8%l*xkSo(%HsW|_Hh9~e&LIzGt3_+Z zj^P2tXR5y~-+f<&Prt9i9%Z1S)qEw{jOx2&lmlp)x+@A%Hml=-g0o)7viW4(YRx-L zdG`DVh^r;qr~?$8jv;#!LcK+6#jraCnMr(WLjq_K?=?Ju&cHeSg8hTKhsN)H@=qUp z=$OyX-`FzG7YMm7t?_C%aU*<()Eji{G1VlUNArN*jfwdvs+aI)Q9;IUe!eNN$7}rI zy$v(!V1yC3r{gefg0i9p(A+g-_~3iCQ~(~M zJ^}x~p;@kr(MsvrhPG&>R$EXV6MNV59~0B%cBWpHL^41on@;8LdOUzZTBW@RARiEk z| zhkl|kW1rvzbz7wG!d{6U{1^9oy1yRM&*ShXG`pECYZe*@T)>%~(Tth!y?IiOrrW4y z-0g~15_uDR%71lljlXbjjXw_}cmG32zxQg<^2dt?*$p+uL1W9Anb{(Ul?oDMG4~BN zr7?u>`RpM66zjGA|C2dmn!D!_PhYhO^H({jO8~H)hAnEl0kH49YfZQ@%FXiKPGu;V3+62s;8ER*OuMw#-ewAEO?QFW=#H3! zCLw9mGpXS+pt|v{fG4WVl)=ry5T_O=QYD*I#&lILf`h7)@YT(^AOH{EfNJfx2aw6i z0e>KPCjg;?5i8WQ1A!ZVGoDVIT~bj}0uUToQ9FaDiAeuUBGy=}e! z)oi~y@3G=;XEYlX6Q}HfkNIt+iXu&<0EP-s(Adbjmw<0XjT|=xpbaQrzB=FkYP$E} z9;4n$YghIB(MXX*G~cAL6S5(@5da*Rver;sa#6Lnu2DuB9OXf>_uw9*-dZDu#L`y3 z^6+k&`v7>^F{E7ru%xVM7uHgK9eNz43jNJd9<25r++);Rwcikv)^?PS$f2Jb*n&YO zO90N2AR-kS%uw63!MW3N2s}Mz7M6Pt3Yyfrb3|bRd^{TVw|!=vZ^eF~c?N6;ld0Fd z#v)bvV_v#V1ED)yMuxGOdlReEb_T(E!;vH%AsW?Y>_TzysWLCubMl7q0GneJ`c}j< z+jWpHqJy>i{#WC@2lp8D)?;-djbdn+u6-Y2=F|DGphj?oOoQN)V2;4-pUnxcHxrN4V>@rz$2NL0{;m6tf zHHW&hg#z}zg*-akagVXJII#7Cq3&$;Q7BgH>=TJAII#7Cq3&$;`U9TF>H6Ra9TS&- zVCw}#z10@FY$m00Z(Hww)!VPmd#t$I#!1#Ds*~+b7_zS8(u!J*AkH4Csa=q$Xc$OW0VDr?%6;=^4aeDyy$ zu=RqW?rin@2tMZcetou6R`FB z4s5+(s5@KTew&^J(&1a{<@5FaSIzzEyvK^W#RNI-cce6A69>))Su~8gM(%lo5EgZt z8ud^DR%YXT$#E}Vo$r6u*n4n~QEzdmge}|awy@5e7sehSnncsd)(B!-B}cnrX;#~< zV#|1x9^-+&_uw9*esUS<-v6q$_uw8pcb{C=n)kmt+k3F;RFf_wMzX|Cv#OLv*p^*2 zc+L4-t8&`$1W(FU!wYENxV;DA)yuiTuFk&g!55r-`Ps?$p8n4B;rXXL{3qaSzk4`- z_!(#a;^K!+zx46HKPhVP> z?|bwskN))bVJ`+GDRnWTO(@Iz+4DU5^LQ)+!{D>FNF^CXcJQ-D!_fSqP01;r%5uoQ`II zgtYB-(10W|B42HW)Nm&~{ZC)_D%%l5&V`U_j!k(dMN>BK_Yh?_on(ECN=3`)Rx@-6 z^}F@c{k>y8#zi~={O7xso>+lOp-nPa>&kGskXaajEP}UvK8CU#D?Ps5Ur6MGMhu35 zM!^nPJ3nbP_J;&B#$_4o1xKur2^8K>#8G_9GfP{Zo65WJqsekL|D2V$JD* zSZEb(s97&pXtqswL>I8a2xyJKBkAGi?=>n4Ms1C4vV5Zx62o_+JP(3c;_JA@DNDW- z&AzducU=8&aPgFQ0KN1#@fLx-iN2FXZMt581E?8qrMa}qby1l3sv>jae4yzZKv?zw zh;n)Q+xHsXvO67+yFrQ&868qF(y^1})E$pv031zO%ci&Lm4Hl~cOHD>UIRe^@-*zt zdY)V|7Dm`z8ioNfDHXz%FDseaHu7+c2R+@;ZF)PEYmgaKKcFlUWoD)6A@LTcO zk3ZKKHL5npYo>qkH(3~x_z5~&Hv19=tx7}M9s0U^SS;{F^pY3@M^3bMw6`J9?=Q-Hkbt@5Y`r>+QK!dk}3&+TonToLu4zUspE2%onV2~ggo zac7qIQh`mt1(F*#n3qRUdi8<52HFSNjUdf@bhI63T8bHo&4hz8)(I^)7h4F1dW@L6 zh;jAb?KJ@QcY=0!fL83Ibs_iLE0|75h1w3{;SBP6RXheXzuiRyJ^V*|4IS&*TX>~( z2Z`FoIApZ8bEFF~lIe%qE?H7Mhp2jQNnxiyu-8zH`GC-4s=t}kT%m2;qP1;i#e#Na z1)J?is;lPhsR)gc^L+(Fu1tvqx0b|BEh!z<>5YW|S~Vmjm-DQp!d-ElAd{hqF5-uK zkndbHD9P>4+cP)Z?A#=LIIL=#FzogvO_mz#BG;~>FWy4XX(=c0%iAPJZ2d|c19 z(Cn}PihNocWHW`h#gLOZb|xz1x*t5s_i^x5!Ocqyu>qw!txyzF!7#s!bo2kCH@j^Z&19Ar7jUqYK?Jndz3>FUqzHMlXm z@*p&?Eu^#BN<&I5i=kW9dS+~~Il%K3x&#Ch+t?eGMAYmPoXG1{au4lX<1RWk^Iq*6(Hbm|$`$l{{+iS3tEtP^NTQXs% z5$^T3TdvgCzQ;4jXdaE+qBjI|11%rD-tTaEQH?XTyx11t4%nCnEkq)?S=W??#>0IvOz)Gr;}wi#A7w2GzK}}qsQks(di5293NW^v!xnBUMNUKE=e1MFgDl{D?#d{ z(i~q(kACF!IEogafwxLf9yr@LtGKJLNMl!dkl7>M9xg7%Dka2i02fZim+(GvGVpmR zatdZ-7Fek`b})znP`kug#$rV1GjtjgYja_5>F0G1d9JT!B(me?h-Ty?t?m%fc1kRp zs>*eLG#Dx!YvOq2Bnd9}bR~JPf~NqTZ((Rux6(zfTS2y7qq7|0MvdcZ3O>-;Y~%;@ zCAar#yy?3oC`}6>w!>P}Pz*NE&KwUSbM zOrkEjqa8P1=jL#LfI23)pL-77;~WTj_Mh)C0=Z8Af(6!n>ha)te>0 z9d@eea5gbCM8Pgr&oyv>1w0!pTf-(;3wk?g46EmoJYfzZ1fL52k2reD&wAo<=I5Uk?X$(Y7Bm(-h$_5YdCJWoi#>w#?l0D3yU9uw`W)CS3{%B zWkdl`G?N}PrJwD~p^EmDi7EhctpH+eO{3B#+j)q(yVG1{FVm~ zPX5MQJ~ZF?=l|!Pz!fOrU>|(&!RhY==qulQ^5w5wJp0;b*=y{7kN)n{AK9PAKZXJD zH-ozT|Ka)%J@}<(|L|J6{_R&kb@kns?W;e2rCojYkP9I6wZqaRQYKji~RkUGwg`Z^JVl2(JvsZam@-Ui){CitRjYCEJADlsWpZEt370tRPo z7d0?K9MYxMLduAbDWGCbCjhfVv-BEXC0e zH~XindVm3eor4SVP2uCLI*+DC%_ZIKx*Ujge>;;9mej~%7+?J0UPH+iyHG<|S)RJ2 zKH5T7Vx<7Qw4-h)j+%;qLl7kqKIjG)?sE;QUZPAf*s$t~5?f3tjHouPjRDDnlyBJ> z&|*8ig&=N?>v!!n)r-An##o_cA`1LVjCG7gABP+B$Il) z)Rq9W6bD2aR+z72^km;HU=pFw@8%l28+R50&Ux~7HcytVda!6w%qA!Z6T)Laq1K8Y z?u+0|yb3t0*IO*}3Z(~nA#~azs8MSr_E5{a1WIl^kr?DtEI$8V_FmyD7v^(*4EWG7 zsepHdBpdv6)fs6?v5WOV!pJZc%%UNE{yYv$vzjn2WpIqF1cEiQI9e)-P__J;Ma4n%KOfKKayA9Tm&%9-X-d0H>d_MdeV&jp+d&-UR7R{OgBA;$8#{oe%?h1`d;^f5 zaXGfZNtlU20p}IHx6yXd)HYx^CuW`zs{}a26?2dUoq70#9Im@IvKVe~i*b>MKlEIK zE?cWiAkXbpyBU<&$tMSymsoN_Lt=Ng$_Ta@1>Nw6S6pGwl(_DvaTua*|!twoF`^I$R~ zie38XckDG<18Hvb@LksSv1Ymg%rH6!D4L2Ey#{By8TiT5>q-!i0DJm>zOG>ujO1=I z10MGjB$|h!$Fur&L}ROPr78+XXj0FVbC{Q;>u=v{kShkR%ifYsg&0zkJ|F`dRsCrQ zD#*-YLauCd>$>d*@E~3P*}X>I^;|;7HdC~XszuXX(v8fuTZL+RC7g=pq&1DKxj8eT zYizIKZ8p6z#V*TUyex~}1aCw)UGQRS?cf02NmHV5M+>?oBiDo1HB6|3(u|w=9a%}@ z)MXP)9ErZu_$gRT1t6Q_$GhBzl%PI;@7~6yMv-JGM&3Z*@ywb|QYsj+peJt{t-75G z9CgfOD=Tamod28W8mnrM2dtxal))@?9Y%BC^FAd8*w-EmxOmt$AW>Zpj9!LbJ$OUI zo=eoi@$;<1v3*uw!DJAv86jDwyRnT%-Gyon#sQ{Y{^WCwi10JC2IOh5m7q=nl}w@5 z%LH&WE8BrJ1cUXGaiva-hxd@Ke?rngRV+)kTh#DK{t9G~9QERE#Zjfkb%jg6U4rXI&(bCF=boRA-4bW%hE(RT( znsj{Bbc`ylbZRannghV0(a@pjVL8dhY>z(w%DqO?5GCuE%$#Vi+e++@M9I!-v7Cf; z7^5iY$r)<(T2O}0gMH7Hk>C`KPN?M;;)(UHw;GsJhs+Uv%1&G8j1<{T4-kFAWaOOh zk9NjVpd>)%+oebO*jWPxwlsh%BU@&J-LS+rTsd^+Vj%j2bopQIy$SF|&&^ zWNgZ@<)>^F$5%hQ*RV=d14&V}`hB5?H!{K+eL3Ri=YY8G>p+tAMW-x<`Xf{M`<8WH*nz-wz==Im(OlA2 zFtZ@j$N%EFMwnpn5*=^*LwRRpUAjz+TN<{KW-b3uj5{+h@T{QzfoeYFG4*NQzEM42&>b|^ELk8B|``FcfERsj!j z`mD1LL+pr{*eH%}!!q4j%j|MG&enMp+u5ek72a&fL9wBretNoZvoi!x(Yq4n;GThX z05$rcwUIQXyAI}hofaTSqK-qwX1iE${^@%g+;B>Q><}pf+94jKHXDeTqabJ1fhngu zB@B3Ia4r!DsB`g==Nj#$v7FC&oh!0M-p41+R$+{puukury29*;H|H=u1vgW+|Mh zcIXmlByhD+CwYHPG7U6Xs~&lIv$sK+Lm%~COU`O*)>seB&cLX4tmft^j<|F<%azb^ z<`bQJ_@jG`k;rtSU7XItn(i0KBH8ZRRqB)K$Uy175~LW(4MR{C8$Z}*1_94PP0h&= z>PXO_Qz*8JRjplwv2ttSYHk?T<|LBV3Qq=?@7dcJ7K=vC$`r}!86?NZI_h~Op&%wm zcEgT0UxH-wB3tqbcJ@4ufy{IUi>i!7cP%HWB##zeGVsV&jU^-z2E}BcNUzl+q8DGX zw}Eb0a3wIUpmaqvwa%NMIuc&gManYPWKnV}IT>tRa8yvwKXb2PI#jE-W-Fog@s36- z6{E{tmy+8J67wAjYI9_#QmGU}pY0Rj2CB>^SOYDWe6HmANQn{Mg#)MJ$*xNeh)9ZR zq>AxjnLayv-G-^+&39|itiYTK6yBo04sE-*d+?H8KtZiw@)^s&n0rN7|v?5wPJf6cEG1ak0OtJ0qB|N=+OgH;uNSOSS}L(3F#q zY(X5>2tE0GcRpx!?wdX~XS}K%yNx)TNalJp=R=fkA**&9g&JY%wLmiAmX>p!#YJ7; zDJ{-T)>CB+NY7D*%GtTXqKTrq$Qa3XByZe^q$5~P#aKfo)1zihjt=USE(Ui#Kqk%i zEeq$VB;ZAHTl>XyoQ%N7H(JTB7IGDchAqr^8TL$btoMCv zA>ATQ*Fm$8T!&cK9 zg%#FWcUC2%jwOQESdN~rnP!+y!+w2)4?lV51HT<6Thr#EDJyfmtzDb(OkISLYt*9_ zRd(Xm(C)@qC8f6*Q^VdonZ`?RZm+xn-;5y)Pw;L(+>qls*Sj5f23}za4&AJjCC-`> zzVeBUR8F?r>|ib~zTnOWZmS~(CvVmU)b9v|n9}_zR4!Lyl9#SuM8!OjJi^Pi>a7Eh z>WqesNm-8unWj5Eq}P-1!2lPx_LvEosK95S%RXyMH`9WXGJ+e)BqMVq3qG$8e0X$y z=ffli*DYGYNiMt^fJzr!SPzgoMAul(WL!fS8=dO4g`g+hij#=c<}i2xlWS>P?m(gG zBR3)MyO0O7&P2XVd2_Llc+?uSFyLv@WaNR^Pm2blh^mClUr?UZRT=b zbrW=2_0n3HQajc4$*?n@wTkX~AVBn1F>tjV-ws@86Xu}SzDhiF?X9qS;%-P{w;WNW zGtZGuO{;Z(S1>&IZEh7}LtCZDXZkvn%kZ#RG`f2XBeLV5YL};FvXG3eQNW}Mw?^z% zX^;C}lyFOl*o20)Hf~p?63Z52397yzmQ@JrOfY5SEdtd;Q1v`2^0LpsWrHNrP4Y4n z%#(k8_ZUc_;sFUXvC5JmBD4p9@u`?>InCJ-3C{$|2JaRAVACw1+aRi7f{in|2zRkN zgccRo*>Ii7B=FS`^lKK~nX=;uFD&YtQq*Qt5@t~tYEstJyl}7_-goyHh@m#pLZKSb zI~Shk2>^MSOGCZOl*JCDOm)Vx7aZaYiu!g};Ei3F_#Ez%qRet^8+KY8xL#|!o<@Yfh2?gjiL2pcA?$pezFADcSuHGFQ5&ykkrn94fe&ZD z>&}N^J6-cIgJDQIpj*6^FH8+?nfkC#>yf&}L#N+1%gCeB+d@b>#@I##S^&4~0-|ws zF(2$=1Kv##pNw-`B=l)x5)n;L8ER2T)N zAlC6J;IKnX?$dvA=RcW#c0YZ`VQ6wyn$yK8q(yw2P0pUTaph z{mw|*&~9i3ETqz0yM#I;FM{Hm(%0<@a|Cs=*ip4cX6DEE06}N}?VS(Zp{@`e65I(1 zOaKp|73e4(iPSn}mx8Ew>{iR zPk9M_#)yqd@$+g4_M{h%cCbcIQLe3PW01mn;$L z-2{}eg~-4%oQ2gjG6yak6d9KcBXDi$HW;tA00L)1H<~JL=vBPpDs;ZZQ@E)Bibc-> zpSq3`OprW2;%gGzQlDHNeH8isvEbcY=(JBvIFqE>+g;ZXBMq{5=mvbeQ+=|tZHypf z)II_hNP4@~%$-b>xzmgWF`?~r@S5&z?0CexlXku80fZfkh`UCd#5b7c+p`ceGpnGZ z6=BT|yT~5AWj*MG*T%X#eihE93@@lmr(EuM(%ND;ZucZ{OwN~ZKJ!Pwhb5~a>a1x{ zq&>$b?EoEC>qOAJRk|9D!(AJc%9BLSicoNqM`0uleXcHXQ31`x;^A-e`m^qygC;FD zp}irS3RfYQb6UCxZ|J#>9KQAsKVXa45@pKqivD}giI#j5|HPN(1z&6*oRTWG{NvW8 z|LAKje)!^3&nFN6(AoE#{=(^dPXZwL>-_g6_y^XHy)XV^>D`xK@ZdHwcg-X-Crz-+ zIEsS=33U5pv4qdNQr9AIb{U|p-1V(Y{X#2AlU4_6584panYYn)AA(wN2RfLBW944H z$wXOcs?#{%JtD32&BFF+C(jUG!jP?M8QQU>W8&- zGy}Wi*Q%+HOkNJKjDC@OhcxTQ%H1r?P3Q0|7UK=CD=O-xUgwl?GW z`oTzV6Gk6|k==#iIx%RjNVa&}Vtrl5H3(g$4HI{oU5@lI0dx<7YbzL>!qFFnaY(*; ztlZ7iH`bQAz(;Ax`(t079d5X96GjJwk=})oA72CdBKO({&+u5e8(wsy744V98BNEP zHrySa`Q9dsHV7lR3u6Pf1QcS1U5;C$X6Y}slOCQVkz&c1%=!iaF!)%+AR`$&*1cop zUVc=0Nf@iUFz!*xa;$sD%H32AJ_6k-6ULZK3CkTV4!%j3;N?~?-3bp@w>k$XqJH`<4CH`~fy=41(q@Yi@gN!I>h%J*%; zfWV@P@4~=#L#BT!4T_k3q-1z4FIh3+yIXzq!}08F${4k zw&=FpsqrHBddKZl(4pMTOF6V&yr`hwm!Kb5JQm*grg{VJ2!#f)x|Z$3tmlh-?;h#< zq1=rDdX+@7usjye$5L2?hZ&`}@x2SCAh?@?do*x-(!;Q3TOn;imQA+cK7yTChY6v# z4Fmmv|M64MdjW#ci+o3poPtBSmxD&VjqeDU0`G1L?t#VgNe|z-ni{3U2^aJ+CPXY1*+=UnUe&kmx zbSU?7n1;9U{k5-F>uw6}fgtcn4})wPK3%$?UfLavcOcm*9<0f?3*!o$=RfPgA3wPy zAN}6TAAIn~um9=AcU*tbYj^(7*5|D!AAb0ar(gB(vtRiG;Ku*mXaCL9k33yIaREO6 z)3e_ODh1TXKlJGPA9J6Sl)#(G5P!?!SUaVLPUNNys`9)(oUDx~vnPDjPSL)Y1)1Kd zqeQxG^^AxP`z}=QYs+T)7DV8LoCsq!0X5S>-XbZyPC8_3YTVR|O|zP$tgpK9$laz5 zdGMGn=WqHDNX-W7&S1Al5W#5F(jW`R?3N`b>%tq*@olp1W`-?tYXCh6 zbDaSICB{it6+8ev2}&HtB&t$9Q2W%Af+MaNfAJIY!L$3$YGSE?t+O?uS_Q55&Babe zLFsnUDjPV)n&p7jbpZe&zWkc=m+pL^lfmZ!axSl z_GXYQ+l4XBKxRG+wiGPg@*bUl(qo+O=q2vWA}+2;ZikOOjKBwKJtq4b+?f#upOQD8 zYpby_q0J#Lgvy4Ulm~0_vtaC}1ilU$1NeRn<#47gqF=2_b=x}Mi6HG^j0nd{H( zjROsu9k`kURo!}(khi#N`?VYe13GPSxM{f70v(T%wo%UctXN5F*joauBZjoln<=>1 zGlZ2xg#cY57IEb;DRuIl1RzdZpPe_YJcd}w=8tT5TqFk#RNl)|HJLuI8rFEwRpuf#r25ee3&b&b27uVZjqXFG7$S-MD< zvK>&$Y}H*uA?;*UlS^>hLM|vS)5sfpC*mV-GzHxjLL4&~Rd+#!8>!=<;rRSsIpCes>r3NO&O7X7u*FF9^DXqR&V zT$UGU(Rofp+c$U$bH{t=h#{ z2fZ7{8f~Jm3|1|e;%;3TQfyDbJEfAC46IEnxWA}Kg_$b=gQU>pT`RCQlcc>WZ#G+| zSruc`(`5rUYcD>8BJI8T82koxi80;oIsqmwb=A<(RzL2`O`ye^C3DQC)$PpWV6FwX z4jCE6WF^3p9j>pYn`|UJf zJU~BLuR|ypX$*8TWXwsKj==WLDqz=gmWL1G$&cPWh*)C)802;|>Mxj0oo&OQ%UgOQ zJ0!D+>aiO|;Rupjv2EP;S6om9AS1@89njrd2qveVWUbcCaR&_;)^}?G<43fqiQNHsNU@r)u39bE-|Pe7ESs=}Pz9S|LEE z{OxGFQ}J$X@mjBrx}DZaPo=WE=!F5e{8kJW>vRE#d~a#2(wheJ%V`68ezj2&>(Xd) zSfX@|yy-)rFOj4jBJ^^+v1e<^>$O3fMA;}TM-dL7gOQy?X%dn~BaTXoEFV62SwgN30DCC9+ zG02~6;IM6hW*|<->|D?^#1PWq9G2T87y9*WH-`t^RAHo^WI*e=yIN1^v`13Ce8M$m zuER_!_f~5N8r3&o$tZSUv-AoltW`*i5BJGOzxZZ-P@po#v^Niw`3|2BVa-#BK8o(J zS;=Cf#&T!L0w(Q7PuA+K-u5<8TUNSB zK~ZvAvy1TrwAVAs?I=E251)2774kf?NdhKdbGd2eQ_9xY6SWZJu}7Mf%`sU!p(xOW zXvMct?3L~`@9tV{(h7OYF1EO}MurP?s2Y=i)7qQOPEXpy4ft*9oVvr>aPbFQQ z9=LG!```4zZv+Q}^!F)EDrG%Z1AY5c(5@AHC~X#9J7|;d8ECTsEt$cs9s)dyhS;Xb zz@i;h1|OMv(I#0vp4hx0c4;l>bg`9`*yznBr%6uV0emMrs;Cp((ZVR8I8`>(Q+Fzl+nkj-T=7;bSVJ(Nt7M4T;b$yt8W%axgERS zb^z)i69r)NR6UK*5<0%YLCc`r$W@Up2zdzh@xv<0r{tSH0PuOEQ%_a19!?P*T4xX@ z+9^B_mRn98@IJW&aJZu;9y;Z1S#@*WrEqaTTRT$Q#sE8`k}Fwn4?|6t)S|N5vF!1w zjlDcmeE!vUJ|NSnn6@;C1)VgTz@N1lrF7JeK^o$)N3$y%B3NPTWGQlMuc_{Ox&*8| zY@GMT*ep`JJFkKe8}@ff64{BzOov(ly031?g}NO^pyG0-XG$S0i^Hte=tZB?=dPy1uh>PmXnnh*#vGqX^bo%e^d}y1LJEul2 z+)L*GqhA-sP*+FQ3e+MkdhI$x3aj7jAenY0BVWPSjGZceMrO)g3k{)tQ)FEwbjCiEK)z1mBH{JVP!O2;?nE zc{Wukb2ZAm)7P}e7nW)fSyG-%1T8*K-6H1NHO6#gA~fc(W{qA4Sf~{88bwOJ+h8g8 zu{P(Afd3CgvCX8BZ6jMpwv4dSyHL`wW-N@RJH6yD=R*W=83L}Y; zWh1^3+lX#t2{_Gu z?eLo6Rl_TW8z8GdVtCoGZ`d}h8(uOj9p(-%8lE>icX-Y)eV7DJ9@xs+!g-mqne!xP z6K5l51LrP~ePA7DEoTj96=wyf!6|SOoMjvz$HvidmT;sTE@u&E9%n9R4u{SmvA40e zvbTU7g`3$=vbd~8ta+@ttT`+?i^SZ<+{)a-d>PDFJjvX|+{oO(yoxsJJ(xrVul zxq{hX7MKaI)-u*GRxx~x6^sU>z(_EbF>DMSV+ljb;4&7`w=w21<}&6m=nN8lD}4+7W%_1t zj>0DTTKY!%2Krs}_4IZ0HS|^V74!zZKu^$@(YbUV-A32Zm(Zp3Mf7>}x%4@7I-Nw@ zM%zl;LVKCEnf4@Y6WEinfp!;dJ#8&*9c>M56>SBrK`YP_v}H6O%|_GFme8a$E^QHQ z9&Ijd4vkJDQMXaIQnyfF25UKZ97I zI_eUtl**+pqRykvrOu(!sU&2@vb?b@*C*4ZaFrfj961 zp1_ykKHP@u@Flnu=i-a-dH7s>4o=5O*fwk{wgr0`+sxj?-pJm-zKgw{y^g(>y@tJt zy@K6f7uX5*GPaLxW9!&U*itr^y@)-JJ(oR)O=pu>+gMv!Tfokg&8#O`n^+rJ8(4R- z*0a{J*0R>HRN!di%NZCNS3!HGVjivqYwpn1R@^~L*(Kih~sz=;>q|jh-3H!#8Lcgh$rG_ zK|BFp3ULvBCdA|L0K{W)Kg5N&58^Sn7vg-}192YihIlmYf_N0}g!pyb0r3dj4)Ji@ z2JtZ53UMxOfp{oxhIlY;f_M;agm@rsfOr6|hxj#I2XO?~LLA055IMLSA{$phWa3JQ z3|s+`j>{p^@Ffs&{0xW~emX=7ei}qFekw!~ehPv37x-d`pW`x!pW#x7pW+gTpWtGM zALAm3+i)Sok8lCR4{<)kf8jicAK+Yw@8RPR-^EXc_zpe>@ooGhh;QMe5Z}a4g!l%2 z0>ppfiy*#^9}jU0ejLQt@M9tV178U7Rs0x;f5#U4Is7n)oAJ32pT!S__$T}jh)?4OLwpK92;v{{10nt%KLFxy z@UKDq9X|=S@)UD$^Z@5KHE@eb?*h__?!L%bDx58^G@yAXefZH2fVdk5kVu(u)JjJ*Z% zdhAVzH(_r;yb=2+#P4FSLtKY#fp`P<8pQ8luR{Db_78~P!u}5NI_wpQ|AYM%;)7%%Mibby$JCt>?Md-VtU(6mDukfuE3svcs}+x#PhJnATGy#1MysJ z6U1|{-$HC*k3wu>k3ei-4@0bDzlK=F9)eiLeg(0FZG>3FegUz7{TyN*dk|s{djMh< z`x(SE_DhIK?0$#|>^_Kb?57Z8*iRrvv3ns#uzMheu^&SWVRu6eVn2eo47-a!>Sg$y z5Zm}25Le^3L!88KgZK^nR*0+cTOeMF{}AHE_z6#>m_@xlf!Y_fi6u%hanfOHz1Nemy{rCkCefUaa1}Mja!S=!37;GQh zg~9g0ofvE%+=0RN!EG39AAqoZa4QDe2e)9beQ+}d+XpvcuzheN2HOX$tcLA_>oM3q zxDJEugR3#vKDY{l?Sd;Y*eQE?F06N z!}h_)FxWo$Nf>M&d=!K2gD=8h`{2i8opw16I~UgLvDi5f7h)}l$6!r}3ozJ@_70`YLH4Dm3m2yre}fOsgDhj<8VV19_7VLphTVqS=!U>=AcV{VAsFc-v+Fek(hF$csCFgwKe zF&o79Fe}7&F$=`4m>J?bmooXqdE}(w;}#-Mf~4__`ezPe-q;W2E_mMi2v&l|JNe^ zuSWb|h4{Y`@qY#4|4R`6KLhdq(-Hqa4e|d|5dU9{_`eMCe<|YsV#NPNi2n-_{}&+s z&qMs5i}?RI;{PWj{(lnU|D%ZipNRPX35fq6kNE#_i2omp`2Rx0{}&+sKOgb`d5HfX zjrjjji2omn`2P`z{~wO{|6z##&qe(I5XAovM*ROE#QzUO{Qm&N|K}k7KZ5xGFyj9l z#Q#}{|1%N)XCVGhNBp0P_&<*LKZf`}1@Zr(4*v&pnTv^jfq#kk{}+h=e~$S7XNdoQ ziunI0i2r|#`2RM<|35Yvbi2px``2Pcl|KE@J|4$MB-+=i4eTe`6 z1o8iS5dZ%%;{SIe{{JJy|L;Ql|4zjJ??C+jHpKsLMg0F3#Q%SY`2Tvu|G$s;|Mw98 zzZvoWn-KrM5%K@;BL2S)@&D@)|Njo+|KCRZ|67Rv|1aYG*P(fUwTSm$i{=5YLA?KJ zG!O7i#Otp@^8i<(d4M%&9^eWz4{-VRd4Ol2f2>CQe-iQkZy^4^3i1C-5&ypg@&Ahv z|GyCN{|gYWUy0@cR-k!+^U*xOd1xMB`Sy8$N1@&p;{Q#={~L(^*Af4(A^u-Me7=n4 z0ZM2dpor!H3TPf6kLCe#XdWPg_HR zyAl6)A^z_~{NI82za8;^8{+>~#Q!ab|CXdXa? z<^hyw9zcQS0p#1~0XqEubj1HpL;U|##Q#r0{C_dx|5C*NC5YFH(L8_%%>xM0Jb(br z1Ms)c19bTRIO6{&BmO^z`2R_W|DTBX{|SiKFGBMG$D?_G@N5&wS&GZOp% zu^|$PbtU~UYz?^h|Jl!_?DjCtY7e$gW9)--JNVHH%0DF0z zC#Zqb*NJ*X?EwF0A{}iKPq~YYQyV6QJ&u1RZpfFC@g%IIOgw*VEfpD83M(ELCyw-0=mLVB3TAr)SRd#%gInOldLoe zv!;ibSvQ#xH|%|gnZXdyD}fJyC<8n7l;etkIv6f@@3?#FheBR&Kdj0FL3i9F78(_D zkj>OCEy&a1(0JGfQcFv%vAE3=SAo>~GLhH5$9@>*^BpRu+Lnr?eBQKOQjuy(>U7)^ z6k3dGd$VLKJ1X`TS6@mbWusN6OkR=MxDIh8@mMjM-ng=L}IJvM5&qqG5bH` zfZGiq-UfwgD3c8DXD^_-+LXca#Vd>C2K} zygCugB!h(U(`5&1!Ag)A8We*N)LvKr-Oe< z=4wee0*At$DGPRq6-SGX5T31S`7pl(#8a z@|)!J&_9Qgq%Ae`Fp(trJ;IL?CTvtt6Wn%?dBIYKJp3pBGAg>14s#(lBQ^{B`Wif)I zDT;|ymx<@Cl>cz(jf*UR;8u0tycC>&!!sn3`N$H=8xS|iT^jtuU{fN^L*Gn}gg zq&_u|pG=y-$qss#yQblqBLR`#p7JMRig<;~1LsF4!KUGT+yo~NY`{Klg0X=O*vCz9 zQpbS)+jUXPujf^K$%TK%*#V zxg|}#sL{yNf?ypSSfF1xz+hn23^;yZ0|uZ~{TOf@G@vk+D8|gi zl+-Q=aLa{qtHN*ba%1I2Gh7rnqIrc@-4Jmbe7DTLubbf5feqNlO|Woa1NLbXB-S3Y z)0x7mdSpQB zr&}B`psKxStHZlh&DfWBRN!7b)nNmw+KZ=}yR)huM(=p4Jyvzz+CyOw9^f$Uef=6b zWMBjK@oVVdfeqNFuc7nS9s~`LTODZ$-=0sJa$!M1R!AvQB7ZR&l_do>Ur4JFOH~4w zM_0_4&HK6u4jkBkecA*B|NlDWMG}>b-wgJXJcFqzFM=KalHnf>J2>xiF6Z#rkFl4r zK4Ybs*D+53JKzoUH|TBp@wCTiVb&1!OKOgCBKf!E7!@Ovhl->dz<>Mp2mk%PmA7Bo z{u=oIOASOPzebJP$2p{Z`vnipHE;h^R9?xM907`sKkMLq8y%6NY&08+minDVC(MdZ z4g-4x;K8%}nnA?1r5tvXvrsGBHS&fj+f>%UX)$B9FfVOT=E~Wo#VVEfXU_;Z!z6of zl0!T>(6mp(GSbvp?h~D4Q?2$BP9SX`bO>Z(ShtQ|nO#QnA$Ts9C!Y*XUbM*B1rQQBFV|Wq`cV^JmTT z+*e2G&Vo&Jl;Gqd)O>D9Q)L)yXo?!OB3AUv6GovSBLa!ngf?x`Z&Y}zynwhB46Bs+ zzU}EWUu5z)pkoYFYd&rjDKaU)zd8{$1o^zMEMBRa)oOi8=+%PMS0z_i>2!g$NE7PW zr&G~0SMu2qSiF&o1}2XMtw%WfRO^XkAQzuHnDKXU!r5obk?b8F9-3STgrBjqaDF7| zbaiKL?OXV+pQDye9s`sRUc3DXq*7im;m~xa`|Ddtycz|y84ME()Rs;z=oT-La?|d5 zVUM3-`scQlNE;T zLt?BH8#Zv7Z`3ac#SP$uji5&^DaUjm+uN+4dr;l&)Im6tM*>wu2N_`Yxo=g2_m$w} z5rjS9(f8>L_fFj=K6yB*-T@}OXP?5Ip?V{ehXGAQm>pnVw{K0;!t7k&VAI2Fuer0wsbV_vUs;k$a)(#b=BlIdYq*kkUiZzVg2*@Jf#KjX|$-{N=j zBJ$)xK>qYFyXSMmr~!8GI^E|6bi(X`gb|>#O~v|NtAYD|$f+=U0O-X;4;WxtsPA69 zYiA(%KZP78u|B6h1z`Wr{u=lyYG75O{WW5n#TC_Q+bs4t>h!sUR3zMWDchL~X7yOG zfNlD+dz;*>ktKziTtLj#MI~i_P$aHLwDAl-7E}m|8B0PdY{k@R`?!9=aeP-_v=CAW!NSHa4Lv?ebZN+F zBXx`1?zhLn&A2c$Y6%7#_KMtDkqQH{qAgL&%Es&FoGmCR0}Vh$_g0kY5A{{4>{`M% z{hb0HXQ$QZ$#5d-w3)$?hS_pmAF-#qldBRp?EPx=olF4hVF8;3M_GV<9mE>FsU>QC zu87@fo6Gi>Mh_Pt%-_?c>wwcp!R<4yX-k)t3DBSOrLtC*wx*M<$Y`MAN=rtYVTZU} zcDM9pb)2uNIdz>?zk9NU!~>4B2bP{puI<_KK71(#d#7K5@@!eMLj-HArilup;IZnw$nvR54OJzo8r<>&cI zwX8KBPP8&$lo93^6!wJA6wq1iPD#oucRBJE*|bL2= zd~tEqp$=H=mO@x68`p^4HKn|-O+FIv=wP$m`Rx%RQeJ+{v`E>*sb}Xg{YFauzdusW z632Rc!Uuh8&j*4t#IX*|{0~L2j`DIMSpc5A0FGin-;X5A1#k)z+DWir=Htt`aHDT$ z`Ts<u%rfuk{I&*^msj?i#Y7& zd_^IMSloiLt0fDhBAO~sofhalbsyIo4y0q+LMjvsM!|Z2yTtA@mfU`4Eoiixoi%-2 zn{`I5jf&6T=LP`c(U&i0V#`TONRJLtzdJNa`j~v|(ECHb1bNVZLb-_Ipv)t`O@EYr z4LwZf(Y~bpmi8@Lk|r5`dieBVHs^U7&RI|X2B$W37TL>DaSi~v_wQhzOJ%VQ)Th{U zS+BD0A^(hZ0n0|sV@B3d%r}|$Q%?i=_C2IWhYQSwjQ2n${mZe#hOS^NWsK54#_q(H zQ(mLoOSzC@ryM=J1{^xZ%ZU=jLSka9<*j6mN|&)_QU>A{ zqfc*WCbU+q8N0A^huUoSixpufPpYp3G8tFI8Q1vSu24Z!Fqk}Msm zns8N${vlfD4xLIqrZI-VG1CfHyj&D!^G;*cUE^{moIG){z%_&Op_>VjILT#XJ9nf+ zymZ56&k8L8iKdvWNTtnWwpI-ncom1NCdulY`dXvX(vHa)6P-JvW~Dn-l;x#SU#%E* zgcNdxJf1aFj55B;$`A5lIg==s=0+oB#$tGfVJxqg>!KNn+9wSLv_`8`R`HajE}^ex z%Z7xyme_BOtDAnWTF;z|b(ENl+)7>)$)=TFu&1XM<2B-qmMh|RRJD-bvk%lUMvAx0zIK2@`ysJTXj$zsQ+^^RH_0S*DQ67z5rbA)s_1#8hPsq0<|GZRDjMf23`uJ#nTQ9I zK}ALFORHLuGH0}7m0E4CnRq3qF~_&z_H$m0q{9s`3b9 zW42mQmDh>u_GC+6544JYJu8Fm5Sffwi(jvEOGV|F*CKNIKE|WCpaz}(ZVWFkA_^Hy44u{-Z6-FHDysHMHtU~PzX|xkEPgE|_gR}B# znXpdiGBxv8ojYtMqdu?C=Emgue59Unie$!;BVaDci&~+%X$POE1w1+5V-4vk zmd+j4M#!9NO(e{f%vgQG(co#DJQWwzhSdIKsPv|L~j%YAf3=*fHRVr%J zDSp!Bk0jliL?GqKG$qoc!Yx$DbRn)O2qutemv^i(NFu?prpPUlm@;vfwHel_?Fwxs zG?qxDtHwxOq%~^|_J*_;CwJV#pqEHuVlLke-hmC9&n&c8!5Mwr3eTNsc;u#dG8Rtc z;*oM#uEy``=ukw9TvwpXD_3ja6y{n>$Iq4`$_bv{>nQNRS(WO1O=+q+O7b-2tf@PU z@n%CJQI<>E31`&hR)if6MU~5sG;PLA&B67^BkrKN(KJ(j)VV{ROPlgWOIj@T#r$LX zhQ+98Smo80fLGCSMKP^aB@!$0+?J6`TZ8V%MrC1DL<0s(623qp(o{pPO2Ac4MPk`N zQ3(zY=6QoDSJ52CzUbT`Y6R+I4zVH=Ze&9fjabTV;FhKGXfhrx#`6WcT2rmm6e@qJ zOhzw3Vap%UTHP|S!Kly3bLx0JAE{(=4jDfXvA7>X6mQIDdakw@#4+dFqu3NoKmCn%*eb(1ozEz8FW`Lw|#F52zGF3=c4dnDK_s|iYJ93S>Ka-LK)M`)AplXJFyr?p%jDYkE zm0T)U2?~XRaLOZc3J-Bt2t?LJjK9iAwKoMtc-Dq4Nu%vG~-ER$3RYp#@^ z@33R&g_D&gY{8bnZPoc}V3#n@ZV-#IJZapoj>?VT060S?n6}D!$%2r=@90QHwY8iv z9yTSsL9x~ncNci&Ov)vc$ir4eL^tN+`FskgKAMVSf9u>4H;fhKExB6@hUiJ3xsY%a z>LsgFT@}?#;+!<$5vs-3tfyhF<0$M#qV;^m6E}jaN?CzBW*Sq0BRY9vSEMd)T4YwI zeIjJg=*44(I_2Juj^IR@tMKy+WBeAdq9pOiVx@dN%+(q|E~l(Q)bxgO0k_gBj8l(A zcbFYcPl*@f>Lo^THDWTTwR}aRQZ{Q$@n)%LC^lkJfhHaZ*r~tn+~H67D{7fFE7nLw zS##3LZ}`EH;nloMmA1OGV^O;>tZ>%Syrz)x6uiTr$p;hhxFMfXmi_jKxn!x8bQyIn zA+LqCvPdGV%jn!ypGU{lvu+3ck3ZP>2U#V}<`TSUns4V@{kgJ(Cybhm1*hGnRdY+C zpkJIbh;w4K&uJ&!3FLn=n2!HfO2hB}ln9A6Njnz6{-6Cdu)hZOaSdF0TKga(rO#!w zp?$kHt=}1U$*wc*{{=bi23P_Yhn%pd6ji>~Gosr2T7S>}16 zXf!vk6a|y+VUW=d%o?NXEJ(6BL6jn9=V$-cveOC#;v?29lka;oM=lce&V}a|S)?AW zTPe&*l%Ap^SGBjY&2rolQ$><>oX~XM<;S}1=|P0)l2bd-^qIHSf9P^`t@4zu3r09FT>e+_0m`I zS!`zwy}#&8|L`{0`Y7VV8}x=wf-2xoXHKuS9_wj=gG^W1ta14Pi`Zk5)v`uz&J+`y z!!bdVr>}`6rixXg5y*v^px0J46HBrPm0fEVcC6{`4nO~1 z>(Tf3Cd$ZoLK&#W8NbBU1J6aic z?RnvRIx6*uf@d#z>o!AFGwKjK#bKqY?haTJ8jDjEQP&jwu(1_(yJX6+Ei~>n z=tJI$X%u7uEt-v^b`RGAPKz%a?LmdISdlCH%eYBA`ts$gO6>!PAhU)sEy(oft@|#> zi1r7WPIl#e8f1De>zf{A+(|GKlWbOUSzoi%6gyffe`GWt*N)Z|X{9eCQgLdol_`u9==wq;+na7Gx#`I)C(noY%(d5rPN_-lJBzyj69PxQgU*=IeC9e zfQUz5zMSCyi_P^LaVEfrD)`P4(Yd&)mGtWGP@gO6@ zVA1cUN9c#pHqlzNlc+CIFQ%S^KZUoznT?yUJaz~;JH16YiTo0I4Oubt`p}vo1?hDl zzV9DTdkGdj%(O7;t`;XsafO_3P6+KaV>MM2L?dx!%AgLUOCp_sYj(>MqOpk6-^q}E zo$;10HVje1^Z&|v?Eb~~H8+#nR~~lN&$WM19z;Isyy|hD_8Cy{7}t=m<}7}1F>1D# z9Am{|BjeL+3pHoUJK+|WGUi;~SISHEqI5^_r;5b=WnbV2ZN)w3-Ev!W>B=8Ic+xYU zTW-DjH(SoW|I}lyGqz91tia-_HN8@>?NSxisVZH&^E%RwXZ&f!uNJHqzB2z$W4`sb z<{ta;je6x@Tcpe%DF@n~dGB|g_Gz8x4+vaiUbQpsQAng?siNJTP$i1BxT3|6=VAeU zprC8COd)PDYe&!jou@ty9? zi6724{QR;+m(KbdF@fF^i-`>(Nj2^*J2ORHq@dBKtql$MMBWiR9$)nEWw*WAJT83b zNAsTj%a&_Dy&u2L5V*8OVejY@f38^9y7Wvm+U>&G`H$RUZG|DbDv_uwKGl zTWNfE@qF)=JMXw$>3W8`{LD))e&z^cd-2ZF_%gB8<84%Dly>{Ep9a@|Jm*iLF>K;c z@Aog>V0!tSZTgRk=it}A_{!CGfw3*ySsG6!sw5Ov=Zwt6R!-a6xx zLW5H|hIPY5C%V@hkdDnUEi$&H-O_}$NKv7kQJQ#-jPtr{{Ru|ibsJuKlqY}ww#I_b zq~gWW-~Q@%w8!qq8{3kdrEz5fcf}F1%qZ>ZOHZi3{=kaYUCW*H6DMxPPXF8D=*ayy zWG=kRyW+03FMMQdi+7SHk;(YJdLeJ0QQAM(aNL};kKx?@?2kWt^6qOl6d%6!&?B^; z{_?Xgo;mF2r<~^Sv_-J96m5PjnBJe@c_PVbE0Qb9TDfrDY72048ooklY-Fkxel;wN zS5Plp{~=ZW()oY#hJGfA-}=I9Wy%$C{llB*Og?hv?a#j^z2Iq2TL=XwGBSh0X!b=j z{HR>*PPOb3uU}Mal?xi7qScYIVY4@cf^-eFmWI~VKbZG0W>q+Wkemne3_*?Ja zePx4olH>^s*N`Z2=Tq7c>kLNkgJ$ns5X{ZfjF-G&ewwNkh?64py=avAq$B zSH*4(SKkr*(7F0A76c#o#UG;g4sW`k{qUo@<8I2Np8L`3>@zQT`|3j#o;FAv3fw#6 zuhbmoR<)|M*T)L(h9fGm)f+B#F`p=l4M841YW8G`aXEiX*b#iq>b&ZSw>K>0esT>@ z{r2x4I_TH8tjcV;_TJ}SIr!T3q(8syX@i8IKyXP=GnTvP@t};K&Kcz8Qapw%zO&W#J@wNOGxN35i+>2b_1sgVzp13QH4gtMdHh1w=*ssz zZII{_2(HLg9%;g%joOs_P&#cei%U&i)Gab6O`)tJo{=QNvV6%{jqy+&-$VM-dk1M6 zTYr+%GX%H1_w|3s_zV8LcJ8rnA9~u2ci(aTQcoKs@C1S*mVzV}YBW=Bku5ytj@lwN zam^nVIQY60$V{4c`E#YT-BM0B)t#!SJb%ve2TGS&tmS_kb=p&(Jb0-@8+_&G&WQON zZ(K>qd~?314H9z#!B&|iQ83j#bvsCXsBKg&(V`_1G?;Z#v!~cqWuLh4%kMmTfU!NcvsdtB+ublm5A<=kDdh|7~xUTxv@aLI>uX{ZGwr;U_v#~w8lQgkRDs`m8#?Xw?qS^&> zet6mScO^zHwvB(Z^`whV%o!dCUCUXw@zN!fsx@J3pSZI$sZ>%nhV1ScrKyK?FC5j> zpL~Pt@AqEs`~LISe`oA4>4}%FUGqX|?x*KmC^EKB*jbuHDt3VHPVyP0{p(1z@q5{N z?XZoPdaI{Pg6|yjH^-Rch4a#Ojp^s7IQ_=bqZ^bHw}HFGtT>bLjUqR3G(K(;F{b z^R*KjpAIKYNmJEYNeEpPuT*J|NZsN}U23$;kYL5LuI9UHGv%{e{<*NU;BP0ta{Hwh zy>;fsHT8!V-#5Zk{@K$8X&iyz0di?{EKWEsB46>d2Vef_jX(I!tC!AI%6_})nX6u_ zt`~2n)T&RF-gKVU4r;pZoQ@b`NoNmVEDmn{GR+eT4AUxUmh=B~F*tBV9&E z+Ih!rdGqzZt-9>{*4tN{5;}eBAIB`GEk0KL^g|b1y>;Wiydh&7WJH`Utw(l@j44{`?FjBaadSuTW9PhjQH^xe|DG`(BF#5{?TDYqwXZ&~ z^oLg;vE}`_x7%e0dD2g@&#{BUk2 zdDM%aaPPV4YEK&^7X*R_NG;J3{JQaId+>(OuV=@f{o-3!U2!yN#o~|6H{HZs^sC?f zXw@&49qeg?q=7*20Qn?3f`2)BFz*g5FB-3&B)o(ZfBrZ%^~!s0TYBO_wuLX=Mtc@C z9!LfV1P_otq9gbRTTd-V7dUp#a@7UaEVsyUxFwh!E? z^Y%&@(UEp1@2#`1T5yx>zQ<0VFgF^7I3{V>{(XmaWnYMe^Pe~7EF*RU0U@!w53 zmi#labLf?!=Fq%-od55<)pKC7haGtqq&;ZTe1ANswW&+Ns7YDW*^}U~y{^GZU&$Us zT{n4Fk8~w^=*6u1HlUN}A-;MlMcDu(Ykttvu;!adlPTB8w&Es3uzQwahQuh@)kKOi zBBjIttgCNLyH5Q@SF?dWB16gmWUOy}+rMj+)V#8&PhjAI{??~J1yGus*S9s0Zl~Ko&m^T z-wgl(1(t33Onh@dqHJeRtqp`$2T_e4vl$$pi4zzV%Iu zu9N4X5HJ93?OV_EzOj-qxg4k>I>!J+wr^E~^^@S_xr8mCdI#XXecOVf>*P78Y6qaf zedmFjUbT_Q7SQx>NIlsE&NV%{_F~C>XFlA~5kr#=Abfgs1;S?}&3y~s)vK0H)`61g z(N)xgOZTm0r|3Fa>lWXGT=y-0dNZFlSq0*!N7r5_Y4t6B_g>STdD1gkAq+GXs@9MX{Fq+gIJLJ9+Xwf`~sQgVPiMkWnCHT3|5FA4L>q`-Ee03jNt<~|Da58HgHyR z&SrfZtR7IX4q?8|e1LfcGsF}z=_BTmdBg7y9Y$Hrc!6;z;{t}8v55XD{dd$c>Ja`k z{sXXLqs-p3vx-%egZc9M@9`grIG3YGjEmch=z4xs#lvSIsjf|GFj5ScvRs#NRb zsj$`OwKTnYwNu~yffr{_uqXO0Ol2=)FPkdtD`+FeP`<8lyFl)&NLJG|L4dJ7V0{2A zq5+$x?ooKtsaQ#+w8Z_I7&t;V*mVz?d@A|WQQXcTOrN#zDdba-U`wfCueOqPr?*ws zRekzQ_m^iX{yzSGzlCvdlH+^*7RK;*@pt28Je`#z-4T8~QCwA>Bv1uiwIC(oaY~nd(Qg)duNa(!EoHy;@^Ko^_RUrf^apDH*y? z{-DvRbg<&HGwPbHF!)SF$NMde(=j^MZ()p1p;P)TOrewMt zO^KeZESd8>=lOmMW1JT_FZ5fO!ud1j&r^lXX>hiBwQ5yIlN!0N;_g1NgfiqFa`#)9 zJmea3O;yHhl82l_&MCpORR)#w66d9U3*(#@IWJBXo~^kt(x;?P`z=fXd(}Vbw=kLX zG3n!}!n0KdW9ZDG?QfLX3e$%ILxFxJ(}w&*{(cKnhkQf6ehcG6-XU+lg|Q*ekY}p! zY~ySS>wVS~nVNM_PG-HwdT*-J%_f=kF6&(|TDf%ZM=X0SVL$)hb+TYT|KD|HVL$&b zR~Ad9T<(7Uzt=HmzkvNU@PD!frbY&{5%2x{KbT46Q9#_!|HIR2V?Y0&RvG*G|Fp{3 z&;Rq&9c4fNpVl|_^Z)5xV?Y1@_l=H-_5T=1u*mv4{Rr&4{bX8Pm2S@^vYA~G9$L6h z7q4{XMG}C#4CP8G8q7}QOOYs8&mRY=Yi?}PC&s9LCF;J1`ESUXFqg^C``sZ?C-mG%8RGx@L zW5H^Mkj4eFx93|rX8~Gff{qmt5r3UwX%f+#+-ZN?bvagdiLT?s!Jntg?wn8p|B| zQYm|z1*cw83ys!8I+ZYJ-s9YBZbcy?7Jq>wsirO`cROmn=x9J`YGnlD@-R4?$PgLVHiLDm-ZXh$zsH^@ zbg4sTz1116=PjdVYeB8?#uDya#0+w^n93rJq;ARQqIIqWoNQoEv_hs@BGxPjg3@?0 zZpt^5k!)Ab zo57c6@Q%(x(EpTooE(dS;R<-ofHzSrm?=j=FC;R1PSn6VGnvnUe_kMjFJmycspLz< z|Mxl^YnL9q90;2_bc@Jb_0{G56!E!25zsK>yGPA$I#Z5hOIXqJ4en4PmKSHFf`T%| zt*f&sd8(!?$+FT^b5sMm91yZ6XJd)9p?c7zP1#Z&nWGpOt;h>aN!_Gs>V=_LEUZy9 z%;8E#6LCnZ4rfWOb;T@BkPS*yD{G4tr(JCG8+E391DxY09_4GQ1)W;L@9!W{_~^g` zzA@20nDBrrjYETMH=cblao4+tI6ZBqp{_`}s|WlSB<>wx1NBUadv`Z9{x{|BoqcHB zeJDI|;{WEMF&qZ(gR!v)vV*ArMI5d76$U7-pm%`cO2Vm3L7Z@F$752nE1~iUE3UsKjTeUn^5p@?m*jiHSw%(NXf~ z@&s%;;zhDh$t??lp{Qs~&Y!SYbIRIZXQM$cF!}!?|YXGVBTn#d6XUI_1O431B0L4 zJPKjh-G{;7rM}HOxP2mF-tjwXkG#XY9vg>|c^z^xyJ2L)q&xmh{J!(lz70FDeZmNd zfed31eQ&y5Gwvrwc6G+2v)e@&bSrGm+hTp2by9l~sITZwE-}D%WMtwD-jVRzoI$fq zTQ=oterL9xh<9yef!^``rwOaq_wOAG+s703j_+t+Y>@5D$le{>>1N-63*F=oJXlAz*dd#{YVd;0z?c36M?PCc`ck-A4c2gruk@L*j z>d<+vcMvvSe{bJ59@Sn5oCn$1ah?G-StApXAI)kaVcT_^iQoTwY~L0h*FL7Jy=e;v z*rPpDd(S$VOCxOj=H0~aYZmshw3`R6Jf^*XuykyP2T>Oekoyc-x;;O}?4+Ga7`Wn4 z;`fyo^=;;1?fJk=>oiBFZV#Bf_J||frg?WtAFH`a^=PEzHC039T*llLg`jbl8wler zf3|Pq=C|h&#_bd}r?Nxz+GCE4n;}%rY9tJmtDh%~Y>WCf@`(1)gpt!i(2jvbuRZF> z$ejifvziAZVxBONji#ll$)gR$_!al)_*d&cWs{P)22k-Y{Iy|&IHyHEg})h@!I6&JzZ zf9t!xPHGTBnL!~i?*k%=>S$E;xxdPnPZ!rpU#-?zOB+lLeOPKz(tAUpSwz0*R? ztad^-X`Dvb+4!<=JCAE0M%X#ciz$PAJVkcy=**Ah0CKkVDaquPgd4uel)9zgl&b(;P03Ew(z+2AzkfF z8)OD?ruLq7>I)7elF)q!C1 zs=XGmU%>tv*k1#CRRg(|Nt|@n%8?#HgeUD9e)n5$UND!>jq$lW8JH{QPw=EX370!2 z;7M~U6T4L?=~H2Mxn2q-xs~zVD(nuEy`HeUTrY+E+{)N)74rIg!tQdt6!LN_qq|ke z?NecQxn2snxs{RKDdhIqX?7{sLm?M*n(%HFc6Wv)Gxt7jce!2)L8l4rR-w4RD&=;U z>!nbfTN&J~!tT_dy`HeUTrY*9+{$IURoFev-AiG2xn2r|xs_+{R$;e)^-|bfu9rgK zhiC0pVYh$vQrKOtmqOr&OLwcV+rN4#>@L?!A@IX9ccYLmll9e0cP%%wLQpQZGO$~P z{W(5g)?KcbLg4uR-74&M=$WgOFY7MXOCfN4-)^jlR$;e8_fpthu9rgK_|Dxb z>~`p03cJhoQV1O1u{(v*J{^CTay=AEf#chEtFYUldpx1EyIe1Y!0~OnRoLy&y%ctr z>!lDlzIC?>yB)fh!tQdt6avSe*o{H~w|gFOW`(%fhq+#U%#Ooc_dF+S*K)C*J0o^~m}{?jWv~=+`l+O%MAk2{gcOo;P@Ec# zI*aBY&uFXWeWSvZIIqd)GdjDpAeMXWLUbx=*WQR}r!9YFD!X=5bfz1-7kth-MNZtX z_xmV1%KuB2L(h1!YOkJ2)X$!-pB77bJy(xfay75tP?86N8N10@tE;Q3nl~0wG=f?~ zBh+$+sv@tlmC?zy`aNEpmFKD?{BX)3&lv3rc~%`Og>-&v#wyE%lU{G86-<;Zb&=98 zORH>dkJ)7}$hopyAmJ*xRqlqpp-q}wd~La==8F_Q!MKfEcZL;xo#!kf9)0=pOXs(b zAVNTU+M)5i{9*R2$o+-@;jSx2_h!xXzsZHVmq+u7o(IB8BpQyE%W#SGIh{4s$q2E^ zZT8RAgn*s;`T!rtVV}#+w2T{mGx+LPZ0}_Fw9l0@X4Kv*C*=uu5>$*hvkZintY21qwlzEAI?B(%x~e zF{u^uMzgZ2t;rRb9VP3i)DhOZa+*?ES&lo#4P3ECT=W;@JYI;~Du6Qp?E#(G!xxNN z0=|+U+~*Q+F?e*~qsVZ2{|73}J-3kLZzNVNVSyE9<<0lw$Wy!ml} zz1J+M&0^6{3kW@>1{N-@{U&3IepqOjg<76Y-U*&(c%>Q4V2`(Y$6E2%``x-AhEPqJUX$>uhKpYZu6^xUB%&CB_Q>wdHiJ36ak0#y4*Dl*Bl9m^!AiL z5mUq~Twcy2NNUZqzX`p3$KzG z=bE??ha@-d7%l5%;L53x`mF+`ysu4uLh$Io@0cXg5EG-5Mm`*QW8~$LKaD&#@{5ss zMt(SQ{m3^*R*ftlDUHNOmX6p*G$W^sa7T_EIecVf1RMT%`0e3WhBpuYcKBDr_YL1R z{N3SehgT1;7_JVdh9`zy!}{UVhXuonhQB_1z%Y&T8RuQjt03R!lbnY+_jB&#+|0R- zb2;ZiPJ@%>gg9P~i6iHTIis9;oP#+`_LuArz#4>?*iW%Hu^(jL&He%V+w3dZm$1)a z7uZpDfNf=~*fRFX>|@w-*&H^R^%3h$)?ZoAfD;KevhHQw!n%QVHR~I!^H^n8f^`CXKZFX!Pv;Shq0dVZN?hLMT`a`!&t^}F?5Vm8C=Ff##{!QL85;^ z-$H+Z{v`d^^bPde=?H zl+BbUC>tsFP}WnvO<6;^h|-{BD9b1=ijHzBg-cmTnM+|)NaPR5TgWevpCtd9yn%ch zc^&y`@+$JVg$HcQ`h6)8XB6q#-17?q3d5ibSmk~ zsq0^HNncD|pYs>e=i9Frl0KWdUL+-bI(2>gQqm{r`l0kj(#KQR$rDN2rmk5}kv`gf z-5`C4u7CCF&7^;AzdnQX!PNEduOYp^{rW1>dsEl9X-V%+UEliuvG?6^ZWQO=yXy5! zucjDG%OOs^z}ZH8A&oTB1EB^;z=aN>B|rj! zB%~MKk-T%cJ5JWhIPLfP{D;4vyQj=NJM+vlyYoEr2J#lfxBeV?6XH#0Bfp0Dvcr%! zAg(6B9V$C)epjT3{AvN`kzYc*;mgQt3-}Ajs}S$|0P@NLJ_`8-h3|afG34h9cpLIE z#6P7TvM;zRz7{0QO$??mpR@b~}x3FJ82vxS6~t*SawWvk4agM`o8Lh;L9G2aG7s@_S0k50eALCrWe^K)M=quC zxBq?*atXw*eipeH;^)4OTm9Z;Lt=G=%L--uG2jTZG?n3zOTRISa@tQV-*ScB| z=AUXpNbJ{u(0)f9LdBCc2#t8TaSDFpS?-w- zK5)kw5N@uW4&g=Dp9bN^kH;XaUK@q*G;ah#PuD?G7;R)9UAv}ro9fZt_XDRsA zH%>YQ!pHO{LwI{e2jPu>)GokdH4t9rR72Q!TLs~ne^)|y`jrX@W9Q2u3_U7?FldlM zh*xGH^!(){2%S$z7T{$kLTKH08bZq#-Up!}e*%OXesDa5>wa(?geSj!EQIPU$1K1f zuZ2+Z!qE^Ocl1#Z9^+gC;r^dI5<)iP2nyc*rs8l2pLZMv;ZH@YA^gs#4u$aZKRyJ) z8*V!o!pm131Y!Tp10ihRcL0R7Kkg4<>G%6Vn7e0R2#G)M1EK$#y&>Gt-wVQnPg_O7 zuUvEQ6oglOQ4Hag6%mA&zbu6C!VLllFL;&@;oQwU2+u8WA>4R82g2SJYzVtD7KE(? z6T*gs!Dq7hz|(%E`a8jt33-R%MW(b3-J&~0w~21%ujcQ?XYu~Td!6?@ zZyWD^-gkLl=H0^E%-h5}58NY|<(!F>qaF8DR> zt=t>AS934qcDY6FS=<2E#+~I#xof!xaYbB&^ET&I&NG}xICpdI;M~T!nR6ZIQqDP? zDu>`iI4;hq93|&?&T7tH92Wae?AO`PgS!OpXMdOdW%e!X&FoF=^Vm&xmVG+g%QmsK z;8L0++53STXa2@|i}f<=3D!fbAF{s2x|MY!>uT18tS-1|@GMqE=OTJyR98XVLZF7egX>P*==Wh8-?-gHrt<27|(9Aa8Ve~ zZj&le7|(7y>7|(8fy@0}acI&IppfH}@x^)VL@$A-beFuf{ z?AC8|P#Di{{X`Un@$A-3&!8}#-Fk^1h4JjxIva)Y?ACY+h4JjxSP+Hr>{jFTD2!*f zO8F>^XSYtTL18?*b^kmH3`{?U-6vnfUZhaMn@$93YnL^K_=E9>N_z`+8#2b%B-w&~O9lCJ=>(Fx`K3IXC zy?|%XK7}9o>vpuafVZGsh<|bs+F8ICplyh6*o3wqzGN%fgt+j2v;pxc=b&|n*Bpb^ zAU@(%vZZ z;vfAMJ!1j?8a*B24@{${E#MQ-7{ra+&?v;AzoHR{RRuJ>fU{@_V*Z_IkirlB_CYiN z@m&|9euzI2LVXZl^F9=Zxbh{`3$Y(XF^GNtKs^wV$aTn@|UZ zAN<8y)DH2(-$rc^e~d(}5ML@rEfANtp=OA^pF>R$+g77Sh>yJhH9&mm^QazT;db;? z3jg@U{m~5zcpr2<#1H%kT?g@3QFIpKk2cX$AiiNUdNRb>_4M#Lfk$S{WZjO zIr;{~m4BeGLtK0W{T0O7PoTeqIK2^l4dPRuL|=t?!(Y%>AU^bR^cN7LtI(fQ_{r@z zp)W)Hwad_#AinbN=!+1ae-8Qr#Ca5b9^$h;h5ig;>qpV&Af92M&q6H9q0dnGiPv92 zpN9CZThON)~06m_<_x=2E^f-vWsX&i~_~HzD48&)D z3|$Lx=4te3h|g@IM?q{l3ta>8tO-35;Fzh zLm}RF8+r)D_p{K0A-?-h=s^&F^%3+yh&Sud10cTcCUk#@FFXs~590jC(0w63t%>fl zfE(!E5bHOhdqKSSC(%_4_!H<9#Ecp$rtm#~okB(66Y_Wei$87cY1&RKAYdSf9(f5t zujIVM`P{1eRs}gVj+Xrn``#%n`wDh|edwwySub+mVSScWWoelI05t$NG5u3D(Tk$b zPJIsSVl@o4@E?qd@Lu5+!hrBlae#RU@)EjQe5l|>@fD0)1)metd0K%Ey-$EJUf|!y zzY-1dgW`Mnt9dW+KF6zZ@8w>>4RTkD-&qx$(ym%94vcCndL^P8^|zh=Udtj6W~>dU z^0p#Yu{f$-qt}-9XRw;0Xm)%12E8{LOcxbuNmcH0M_pAVIR7PAsReIC#<~Zyt1I1D zuV6^TT=k^F*ETh}mY_o#$oEyGswE}D*@UxWrWij>F?u^5ncUS0B{9NO4VxR)dfBb^ z)l*TACFf67j18sAtkn@75~mnHv4hbcF;#7eN~kMID`PdAqekQ{6<=PTi%RWvgG1`h z%FBs@svk2Hb};hprZn0+-C!c-Xi56!oY|ZpWv*nl=?)vne$&^{Icq?0RFP@v(soLt zW9lVDy;JM0_(!Up-Ii8n(X{e>Y&(5 zG4`-(P8*X;yS0WSNCrx|e859W5~Q)|w|NsiQ>o}u#B7qJDYK)UqD!d`8avu_!k5uB z+M1LuTCnH_)iZP2PRfMqQXV_$(kn5Hd^p> zqpC-V~N(76_ zGLfJpr1Qu?-BG7bnpHGoHn%$7(_im@Ca zYcaVy)xe6TbfMR+)|<+Nqae>Vb=|Z{AMKiY(qh@#%~Ys9UO+JxbeKtI(iofRwyYhO zrgIuszi!PK2)nXMMpSW4q-nqbC6re?KT0K7@-j3Z^!s_M?T&FxVXym^DpUe=W}&1e;KHg!_HL9OT%>ur@ozk`wSNs5sK z^=-BFjxrrE=QHsprjFGNnnE(+tpw{nBN2(pCAEB8UrSQcifg18iBi_v^d`+lizTa6 z*A#fUs7X6ADs`+UC*l>GOzVi`R4UN;9U7Tk$Y@WgL)oHBlPKgYHwGxZ6k;wPGfpCH_CH!Vn z*kq*oD0rP>j6_u=sVh}dvX;R}bR%8DX#!f)ST1RfHZ)eLG^tXk8sNArWz_&=jHG%w zGU-m(ivCbNqrvixR-;^sMHJ;~B4qKEx)q#oWz{mOsqEek#sJy$85L=hr{6QSG*WlH zt}td*Esv}2c9gs-d)2E6=P+fDh(W9Fr2#7rx;8)tO=YdyNSMt!PdFfnw!>bpDWbzL z4LIJHOJTmU(nYv2PlTF}JgCr6D_Okl4CqoTVo{llK388}3t<6Yv{07$a7WKv3CV&E zEA=$kd5Vz;8v`0!x@N)qjM@Waand9_-SqYY(2-{;jW#=8wi#_kSqqmtgJF#` z=(0(&Mk7&aSoM}5sClf58S>_a8BUI0!=@VWkha(=lWB!Z<;EfzzsBSB;C_!PT?*Kg z){HS7NlPT%EcGVq{Kye z4$e)GF{qa5I8IvW0M_0zJiGWU1%to?~W+)z}902Q)e|=JV17#faWWF}h`nva@c}`0*l<=}3w(WjU{Egstsv+2AUe zRNbgaV^DNDHp)1>yC_DN(`bx2Jk^5J-qe~x&Q_$>H|e_8KvSNn*;VeEKJU_~BTWzW z965JVj1Ftio(!vW$$n5`tk&WVuSJq=)xA!zyvdB-c3DA|lLn$Ql=ebR)in9F8Aq~g=|`xs%J?faIm~W@#Zd#^yc~_ijPLIi=t7k&yKtkH>1sj7Rg`TcNpCzCbC<1NbI;W2W&;UbGwHHwZ0>eSlh5bU zCci#jR+y92!#H|JOZ`1rpl>5p1Z;HF?hLAJp?pd3-qmZ<_Be4?UQiby+M;ksw zxHfj{N|_#JT6w%`RfS_E9Fxku(lQlqGT>_54hF5tcsymX zY9l2@+MSQp9a>!h+@gY861qw@8IX3!QoHD);yB*-sixL@L5c$1u@P{2gVnYnq&L@1Jxk5(%o?Li3?8YWl2?${ zgu18E>ytXCB~K)jihwVhrmQObJjIv}1@!WCV-SvH3h{QxV)fX=O`~0>mxM{LuUU>r zGsbF=^ik7a2p0~r213}3gj8YvkS{B*In@=Pvnfya6|%IE@Vh$ZT3eSj<-646n7U$z zMt?0Gb+-sx+ZJr)eFeY7m@k^MdUZpY3(8gUs8OoWO2YDDB|uHbsnaONmMz-!dDC%B z>r6BaggO&7dO{k!sVV9TZW3z+0v4-Xm9xpI6-T%i*um6S+{OIMen~Yo-bz?%-jpMg zQ?;;6D&oq;!i|)^U&+@4T?ej+_-YQN&ZdKb!xRkU^BKHRE2u3lZCtC2sVYQ1-p-L7 zmpZ95DfLdJKcceBlhSyAiX5jNgBlyLctDa5g+qo=ub~g+Lq@W!&a^Grs-|Wxx>|J) zp7q7dNooL0-8IPnUys~`th#g6`KyfJWdA)=7f+d|IO0pi_lfP`H2#C4O(M5Q1Wwmq zE_4b-f`8EXg9%Zk~cr7M4VHNrmjbcD^>M?O2(Tf*M21aN)U+9{b#LEvhD zZye3O-|PjmbKNEE9x|FRl3@3)4;X93deIsf&Cc0JF+0~;!X9mgB6!~K&4fS}WxXH7 z^kdlDkexs1d%qF!X6@cS8Hu>stwg_%k7VcY5B=f#xz>;!1*Ss*d)Q*LlYR@{Yqc^~ z(qQR^N3$~+oPDmJoog=9kGtyykOhXDtjS7U-wQW)qMv&JXZ>7biT;uSuUZrKT!JvS zoQ6mm>x|UTWiWULoijUE9|{0Z9`t~*rW=X6yNO2D4t5z>pk>uNMhaka_b1oSRhQ_m z^fTp7ut+Am?Y=A2>Dk8jzXtn19D8^w;i=nm!A7_m$c3y}xj9-tdzEo^uDpaj)d-s- z>5w_pCE7VpJEJE?vkMM8cXqC_r1y24jHWPC8_&5Km_3Qtci#Kixe}$FKWu!$*3&!8 zn4fUg0y$qEYmc@*hj+-IXXlDbv}emWW+1zkXtnJqH_gUuY;@zf`@cLpS6Iq!()SJF zsJZ708GOaM(U#pAJ2^X-U&5X)w25eeY~Yy1sK*QaLT@xXXP=MG&gGV{V_~bat1kuW zHdiWVC+d#eXm&7gX6Ld)cIa~wg|f@z>ZXlgo7=WTvxS{me!oA=&SiG61OCCZwS@!4 zgr^&HmQ9U%vxs$rBeiol`$cBw(o5K@LBB;`Zgqk!cQKi>mrcH%vTv9phwM9MJ#&q} z?#8Pox5a06`rKw$XD1$y%Ve%YzUA96M=X($^!OwGK!U*BS$jQNb@y!}8$d}|KbKl0 zfw3k~bQlxSqOpKsp}xgbbai(oL3qsE>|An(4ZzGFm@tX?yZvU<)Jzr2^>Eo$X^$4b zKU}?jF0n+vsT{Ob!0mTdvg;}3{h?}YG&_T_m;Br7=FVCWFwkEI)(;u#>890YGGIk> ztTa*p8+fj}*3HG2@EZ%BHeOEZt6qDm+btXM1|SJhz) zkgcH8m|tH0{-3Mfz;zz-%*h{h&_C#Z zB-gQaac3u=X@R+4bOx8sKe=wPe%_&*X6H^@qP`R;8eK8mSW25(c*D`{R!8=qyWhGW z%+AG@uzSrZERkwE69#8BlJ7OmmeK6IgDzP&7hTeNPa=<(YtfRm7sz+BmT02BgnfDI zFPfc;EK%QZlv{Wd>(yJ?U^QZDxS~t=rP6=)p2OK^X8l}vLH*$21;AB+nO4qPiB(9< zYUvm4quHr-H?%~5Fp+DTh=SFW>a=|>hrd1AdSKllIQsJHzklf4}o&CcCl zv3|}oWQPHpIb5h%$W$R4DSCSOmfdU^$>_b1e~&3*dfZ&QT7qL9ly5_ zwTKNDh*HCmJWtSdwni<|HEhJtV`HEULs$zryGWl-Ck&i4Smx{aOW3>ZWU*#$q&isBSOXDXdbG=BgIIU{oN>qwhpsIf^_kPboFh=phf7$?JDPpR z$eW!rEYV+Xr;4UZHD_>{F{iJs)usEbv4IhU*4E%h@lM`Ez1&Ot=P19xjN83*#9%pw-Lc(oILn%&(Hq{7Pz#& z>4-sPva9)viWBOzaw6vsERT(MUuCit?y_B6DR9JHL7*?d(MoRN}6ypm~3|2v6+YyRE`6M&lYP?Yev=2TC8ER@`uYc=sX^yefi#c zSs#p9@IDo5Ra%XFzg$%)t;ML>u8}4xIc2lb^VT)qouFU)!P<#N~@J6 zvOddl7qd)dIv1&EuyQVEmzM%+xgL|-r?nn?Gmw|69l=PjqjUzfZj&q|ADC^Ra#?}J z6$&K*qQw=P#`>K>g_-}picKq*S{pGL23^?IdWbr3Wh-D`vD8d|+E6xCi>8i4E_cdX z3Gn$v?UOcaWkWufYg-~-Js!iS@qSSo+~ra;j%vkMPbhq9Ii3y~S~DrXQj57Y{l4B{ zF4yhg3#ZE*Dr=;=u)FHW`@EHW54>;`l1Kj3ecGy&A8N+rtiLD8yRsZzDFuNg4~-ixo%Dx}g;z9yn=x^>B@dP98Pz0lQaZojR$g|87EWY{fKjaBo z4YJ+q@Gf^M@a?r*d?OE2y;l1*UemCS# zCdyD$A(tvZ3~FX3QiI+NiE`z3t~uz=&;YQG=Ahahje347d&&_L_`P^aje4}FwAEcc zPt;kRiA)!>rL?ixQu~xajmz0C=%;6@HfzWL&RNuy;N|o#d&&}_Ph_VZ0edHs#N1_h zIw-T}hThr>4^`qNiy1`retey3x%=E#NlXj#`tG_Pv^!lBG*vxUFW< z&Di8K`D9Mu-_#GL0^--i?-%bYx>dAJxJ8%|{6av2 z3V12ri@XG=Sr_L#$2kL3nLCa36e|kq!j+f|#)lavqIaSp*9-(NX1JqDrmPYJG*2GdZ$X z??IJsLRA{A+84?=m!CeM(<^*pRlEGGyh~M$u+QRQvCF-}CRVk}z49zoHR4e$?$s`; zdjDoOizM;2UZ)IH$R=BUX&4-~*C(}oQ`{+&;}&(yYA9x`szSf6a0GqMMrQ2<_r;ID z#{T`AT@yE8g4^KfG{8A=1MnshB-0j40QYLa0nI`x=K`@Q*32pc&9K&p6WXA<+0i<2 zU4u>o9K!~L?5=#>k!q-8xVoDUdcAg2r4dh%Wld7qRF@mBT-a>u)f=@=V|PCS`@{{{ z&5yt~aRYYqBd`t|FoDqB{Rk`*H()nE0`sr|6U^(yk!WK@yNIQ(QU5z=cKX`2UMJ3)Q=@dd!&zu7o(117jlnoa`@6E~nozpgi( z2Iz+knBWSG-TeqoowxzJ`4McGxBnc@NVVd+`n)?$qjRboF8x+oa5L} zvoB<8Sue0IXDL99yo;D8F&+mM?@mCspl73Nk%xhTB|m}rIE#Uk?4PX}d(vdB#q5lQ zOiF({9;$?^5v#f!%IR>Im&_+yW}mI)h-l21Qbk~?PGJ7bq1fkWh}HNsO^r*|i?mGo zx<)1k5jo*jB_%a=xvuZFyWPGy(7;`~imVWrKVvBN*)?Miwyf>dyK<$83>HJ)vNYUs zw(53mALJGL@zgP>igz+$B^>6D+zmE?u=%=36mxQ zGGskT5IGfe32Pwj))eAq7ijHiL$OcO5UbUwTPdAZRqS`dk%&82EO<#nQI2<2VY4D` zAY~eprICqt^>KY*J~kBl6b-SmM!4iqWe?%P%@#SQ(7JBOwO8$cG9k3$Xck^ zeI~mSv^F{v`y>sqnzYd>Q(@6iw5!n<^fEQB#3TW^$}87b`}&4V<8}9R=3+C4D+BY9 zq1Y#Ai1n5UC1Ef6%7vV-WDcZSA-gdMQYQmlaJR8ERw~queTf@$YOr8nK0FlrI1RC} zsH~f5H+zOyF4Io6>e61=T68xmIZe%>F?RI6x*7ASI+mE*9GDLc#Xh!X?7^qC)|%f# zCUx?jkL(x-aHc({m*tw(f=O>l`K?M-sOZR7$W%P71JfZm6uX^<*lOMsujC|77f3ME z#w2Y}%b^;Gh7vZ+R3Ka}Nh_BM*fEPzGsqAEN?_hL6nj4nu{CehiW921EJWm5eWF~`^~>OJs5d4p zTjU0#vs-L6<*k%5(*&!Ubtv{e8e+3Wox|XCN64a_)SH9(KCTHzRqcMGYEc?`U4OHq ztY~|9Bu@ZeW*LgTmxkDmqU(s~9ND%j?u#XZSkLZscfmDcj+&>DXqLKaa2z<(QU@(k zdtlx?6nhU1vBhY!?Q{2AEhFBMRD)d~squEY0duXAZKxdynVfWoinWYhE&(pmG!%O` z4Y74a%^mh&Dw9;|R4H79G*oZO33t5*(nBS-OhP9wltHDBdL|88YaELG5e>0Ty(y&4 z`mChaC{Gp&4!64AvynlSJLdvb4>CnfHl?f7ovpO2W1lwwu^9WV@t*(|e3?d}laV27 zZ+S9QU|v6L;+^B)kFF&=RZTkC?d5|iZwk}_@)BNkJDO7X-U3BtHPa$ONee4Obp#n?6+u$o#2Z|U|u^Edj}1%6TIaQ%xi{Xzez*v z1aGbb^Xj44Z_p4s!Rz3_ylN=+>omkp@aEV)uLNQ-_SeR5@)w+rTBcR{`@QzNS4hRM ziC-Q6E_Ce#@1gDU@?jHipP-4OUhLWDWkXqCnM~G-@3`#q(xI#`Paz?8Hf5smX z;SJoUz<+yw_E_Nm2Mb&l-*nXAy4h=-=p-Itynms2-K<9T?z4jb1NS~p@C9k5>t=U$ ziqA>bN`6qHD3SL#yUk23n_sp)8yxd`@A(7&-<{$c9>&`Z$M{rB&hZUytEO>|Zx@p# z)H11ZVcbv5>R=|14Y^BjWS*w*w!TH3qk0J@VN+r{soQPwDz5;%Al%m(r=|bAP443z0 ze)qH-Pj#YFkb%*O*h8_IWE4+lTE&{Pn^MJe3jcH{s&6(bR&%B^lk*wO(Z0q^BxEV8 zKAF_vHLrRGRCCgI=p4&~U9XeXVj@L0;=@NbXwyWu!h@TAFIl%~&A`Q7!9~|jFmHFC zipf80>FS`=-iKVxgrtB+2rT)Q}>EE!#1?N@TeiA5aZg#z&J& zk{;CmQWv07OXVZ4=8pBUdQ)4Ovzw*8UNv0D$aE>{?Die$NIf+ZlVugwXd8=>(ixDT zw2Ll4>asdxng(GbK&4H)*Hc^jQ;0iwrMB)VR)7H>vay zO%^2Z1vF-}-V+9uYOArfDcg1B$bwAaR2ylzfMM6c1imz}>BwQiRAx~U!-#VO(de(-xa;bVA``v*n~XD4u5t^^|pAuj4i(HL7w0 zPxR}xdds5@8!PHAkvBA?suIi$U6v*>$V`gYTSc(efnon2NS9h@z`&tYTS-vnc2}In z%IE(_jQv4ZcQMdV>C`GMU|%sS)8Ej0tI~3|Ypvzd1~s8-bo606*#^}+Gqq$j7*eL3 zif%`x?MHXn%S2q7v{}>AbYk+P(w%fB@V#VyFCxXkF46d>eH)w zt6Zy&oci6=y;Iju6{j{#?IZrV_zv;B_)M`(%o05=`kZK^2p6pt{!#d2;YWp4p^@j{ z9R_aj`w{m;+??=0!7l~h1NYFS1ZshR|1AH@{0sOY{t3Ll@gCuQlGg@z`K{u-$oV?B zweK{}42Qwq&c2m>HrvBq!+M)_AL}Em5=+n8kNFDo+srG#{qhPXhw&u1N&Y<0xII7r zOBOhhjiU_4nv|_s%jtVYn^|8m*ptT6@?wh&hKXUCX5)NX#72e@3bqzPmTHdZ84TTE zx6$Y?|BxqOhu9&y#C&#;9i&Ul0~cZh=n`|;ezu=3F^BDA`{)w0L1j&xE-?#SdFiD~ z%w%J1j4m+)OkNLsfa4wt+s$@Uf|Gr((L!4M?xfM|Z+nv6;wwx`eOUz=+*fP4rOtzFQrAy3U&#-5x zr#v z7ly_-Vm4F7To@YTh*{uj*oC2yGuTtLvXv~_+OBf7fGs~A!I^4LwV^e}5woXSQ!Tp0 ztf}TylP)oHsxj4|OU#(6Pu1bj7}w^h+Efh+9%nfw2m==8$v9#LQ_hsbR*WNN%9sn& zq|gBuL)c;+zuS>+`JL_Mxrbcdjl3ISlgANrcsKBFpi9i=eT4TBy2LEr^}Orp5;MW2 z%bV#EGk727eHdD9TnBj{;(Z7T&J!*pVdxZ`iCora2RZ%A963k4M!beDFuf6ym zD7 z&;EbU{(sSz);;_GMaI~(|6gQ`J^TN~CldDT{}&C7J^TMf#@Ms}Uu=v$`~QEQV%oF+ zUoyFqDiiuAbGno9|Kx804+4qw<#^}F5NjvUUz0b3N+3D zjWja5DUAyUa~f+WNcGOmM}Q^;&_pBIdxesvYbVJ6o)gZ82aoFvl1(E8oKjXIOG&am zsvxU++2@G06XcDj=0k(lfUeWX9H&}C4W0?o$j_V)4h>8zk$fcs%TyYvR4LPr$}P{$ z2dF1VBfESh1rsEkpFZygx@N=}n?}kxrEAf+TcXbWma zq^F-bk5i9?MuIw}W^tA9g={tLh@AB^=e?8y8d>WrJ(7hU$aE_vvdAqWW-aV7mj7G zoghO#H}4ugD3VPhM}DOTH9^{ZW!?#N4OS5viSv}M6$bYN8TFZY2hccJMQG&IQyLe| z)(MjAbMtn}jx^HjSF+;-nfG87u>oa+RfISNWkVanrN(v@O^*?>y1au8n5gN(cXbWma6b3kR-atJP8l?d!H7l+nqpAg*Ij^S_(5M%%(j$RA zP>(1f09KJxfrh~nc?00tPX@{co}EU814`M1<8gwr2&sA9pf#ZDGzue7 zt)YhI1XU8woYxKwOsh`9N(Nq-`vBPU^IvX(i<_H{9prV+ zx6ri{Ox5w#JC@uYfphS*V)Oyohn&LlOahx8N7 z%wbyQFgbbQiWQKGHG!vDDX;UN{&9I`)!6$w2MpsA*O#x21Flr1+yoh#!0cxqwa|bO znN@?w^)JlGR!Nm36Qw#c*^W#x)9Ful9n~NxO{lYAU1dw9%sX>lTg_&Pwo}T~j4iEd z>~f-1D4K~lY@MjW;Iel^wq~TR2=zO9Suf@@bo1truH48+a+RjR9a8k&vaptHxAkdT z*;DpT8ytP7qn+#FRwCdu*wl%3$g3h}NF%N5>(qm;57IFUn~oXGyQ@;@B!g-9%{%2v z^KOE*WcSRYNhjj648_H(|9@yEjV}T5uDQ79A}8Q=mK1=Xd6CmD=4Gmta;*+Hv=#F* zb5=#trZKfl{j!^^>h#X4KUdb~3ObK6YqED5y_RAcx7gi%hjf?Sjb}PmDo?92?Uofc zMIF&ZE1WL*6uq_~*>qJ%eO8x|Wn=QPEtGHA9JQj~?DA`saxYm+8Kxtlq*3c}IW?|s zze?6BibA#5EEo;6?qwQw9k`o|+nd%7m-!yLd#|LRyUZ)ztGhV^u1JfQ;Ys|}K)^Eh zu_K=U4>^t<${X2Sb+85x7UV%gh+G4d>gZ9Iw;eUOa!n$a%5>oBH+fv9m4Wy35`|Pb zAl-4}TD(;+0s-Y#u{bDB0Ixi&;_uwaf!jkU<-ErSwWgRaAL&$^vFn_1$)>FB7uq zjS54DwDn|0jYV#j*UJ%QgzQ(PI#Au$TMv^Vrz&b{XiHXyLYh%WD!oQ6=F63hmP*Fq zvL{;(Bdxn5)q}2snRiic)6s*OcUc^rBuw3X3%&f^GjD~tw8HGzvA_He&5mKUfi}1! zF;^K}4|vpY#1C6k8C)_r^47$64c^6*LNwU`e2V~G$+Ov^epZ>fPxF>5N_E!%hf`ahq~(K5RHma-w}Ac}QNQFFOV33DbO zl^QkDjvrJQs8kzCcQ4dvfzYvHyY^6W^#^h!*0~c^R-4l9U+poTDp}E%6k@? z%gmtbV6YF)|A&#Epj-#b%Q~9*2j&CJ8=2LGY5+$}{TAE_uz9L5wSH=E?%|x@aPH=O zn3LzMW@JYUw zFXTPP`wH(u=7Ee~GQP*Snvr4}d12N$Xo&mhs4Y5m^t=+Y`g#U^!DFhM$(+6xUY>d` z6#Yi@8@j{-(OaUo=o0fqZ;IYTsH~AH(IS(Pa-i46!=bL(ZmceQ0V+IEcp_cNdx`!a z`U73!RifXEeovQpO7ynqZMwu_(eFgRqf0Cj{Z{l_iny9EfhrXZEM|B1^wnfH(pY8; zw$LN=&?ROG-9ng8K2|bQ=n}fqXbtXPKUnU@RpjH zLGdy}J+sUh=yfPeVk>uD-A1<$Zg4I2)5W^k@Gir#Phg)wwPGwWi+wEnSh~ba_R;L4 z=@K*8N3xHk`Zu;2?8DhGBXjKI;|Xyg4y_z$6Q*V%Q)xHsgr4XcOUn$$oO)^MB}i`m z^s4ApI9tc9C!$wGuTX-=-r&LJm^dbixH@h$v!{MO^>ez!tf`l$UWUZumL7&sB9uVl zam`>%y*TwE9CPD{r(T$P0SX@HKe~NW;qTMW5QO9C1x`{Oc<1mC1x?*Oxn$0 zGF?mcx4bTDC1Qt4;PonVnncH4bdsME8Bmm7n(p0|F%a$|_uymh>F zbctEKQ+TJ)C1&z;JXix}Y?~Q84Nn7I^SBQ3lsx78Kt(DPT~i_S?Mla$wK-Cyg0tAQ z6qXr-yLvTUQqG}=(j{ddatK{g*1-qUC1oCT5M5Hnfd`%d%52fGzySxqCpIoX6`>zP zKSq~Wh~9+WM3-2AeiZ#EU1C0ZBMNJ?j18)J=nW{W%`%pli+%)!wOPgzbI|KiSes=m zF&o{C!rCljiCO4}(GSzL1{3`d3X8LhmCQgth<4HOkFs2Azji7rY@jMdj8b; zbV=u?=ID~1H+3Fe(sQTIrAYhrSYMwbjTx&yS#@Ig@(@mZsQ6I2q=$$P*+E(lTRVO~ znFg})s4;4>E#Cn1-sHVWm-N@XUsI$w8BJlPHlA}eFnbcOFTcu&H^ZBuOL`LTB)X*2 zylJ|mC-6?7OL`pdIJ%_A@Q$HNx|X+=B6Vc_zJ4iez->i)wq=S0mMxuZ6Wc_W)W|l{ zB{i@ObV>DWJzdgM*{9Ma-N4?kgS6If)$_Qml&j}D`9#p2#ge1P}>x}^Jy_oqv` zpLjpIr2C5ZrAxYxcpti?dyDsmq~j*{!QzAIk{%>Jh%V`Y;sYttZp{;Q63t8-!-;Gu z(^!5*1mgw93v@}JXFN}r^kF}kEtQIsxe zL=>S*8Wx4=l7>Vfx}-r-kS=LJ6rfA$7y0Ru`b0j8)F1S_T;aT_o3iGT!5Fc8;bfi9 zg3;Sp(m584-o}!i$AZz@SkiM@FnSwH`hM2?>2_-)Ya>N!FNbk&(}SlCmP)7}Y%afB zRx~e~r%QUd=yJNGmx(T;OM0p3Qo5vPpCsTNahqs^1d8w(z{yIT${f0?lV z{xk{q+m9yUzWdT7+-Dz}gnREzlW?!SXcDekwZoEC(&6?wnh`_Wo-;O^p3-vHBWwv< zbV-}SCSB5outAr!F09iftqE%s>A2VGLYYuTmsBd0(j}b{Qd^PnpOjA$Qd^Pnq~Hs{ zp6~x-zl-hp{=c|{#-8v0i>S|Uu=v$-~Sf{8hgI~OQn0h{|_(l-Sho_@dC07-~U-#5drdXWa=Yg zrSK<$ET6%>k-=q_I6CCx;7|YgpYsXM85gFGV2sFpQb?D*)OVZo-Kf0Vj>;|4L|E6d zTZg!juAV+=A3Yn9rB?Ln@^Rp1~Xp2Asdx@iYbMApsVK&4|M_As(j23+F&jg z6NRb_FIQU8ybt%}9V&OYFrC2XNy%x`MeC7wz=OnUIJJZL7Q=6X5Wh-FRA zLb<0`msAOrv**o%gpp>aB9m)Ql7fFS)HcS-a_xVwm3!Fkva&_5!h0Uv-N*Z+)#G%V zv*jeXS9tJ5mRi{$3m7D+crKx-hGa>dBHq_xDI+*d5H{v4CS%E{(Rw2uDP{|YGK;Ki z8E0kPf3uYXyJ;*Nt0sTZ;l(V5HfFK}{qmwQY|*b^Wqqnpkd@?xPC=nFlk!Z=>GbLY znoLj?lzEleY)YRufmErqCT3e~Wh;W9>Hoan`oG?S^AnuYE=(MOj_}L!f7ejn;VLI_ z%RQ_#YDtLY6Jb0<*ovK@+qGKqQF*AJ>I_`A9V{z@WkJ9ROfP6|I@LC} zq7$o-RzF!aW){5i!QbjyO~kRc@RHe%-BO76mv zw@aJ`mCaQ5DkViFo(VMb`D{PgY)bqgpD)_z%T?u=QsgZUcbxWAE*`qF{q9!qseW}~-Q))}` zl1UX^JeCdPtgQGi_RH?w<(JLwE}4(Tobs*`D~Af92$A<$l`D*8J?^x{t|FCWwf{XpRx}hOce&noV(EV)0nkkF)Z>EyCD^)z7`#{IawYDL5*ge9@We z7sDQ7rIRW&R-uRt5 zsQ<@gy^M%DyjQ?~dw%vVojjmx~w0MpfJn6+FW5rt7@1@QW*fXd`)j0|%Lb;O6=nhuNyt123^vym`P(B?^ z%d!ex#gge^fwZ-qv^7ZgE*JIc`J}E`sbOrHg9)E}rcjn=v78|t$$8}NQq)SwTONDV z-|-WQy4_q>mr@;TD`V0aG@W!JS#%jJ31c%aZTWH8j4SH4_h#x|x1L`0&Oz5VZoEXa z>G**Q*tBkm3s`Xq?micwdAAE#VT#f&%FW?Hz^pvE1m*-q-+isX zpji`7<@dfyuh0;%1cI;!c$wj#hD9+4tJavJn1e3tYW+WzPALWKE7t!jgFQ2W$!9Qi zC1t9+gZ2U$v|)YyjM6q^ahIk&##GhnE!Ta@U9bO_mD@e_a?ov-XM63iY$m3~LjG1G zBFp$&-esmAM{h` z$w0_|?x#9`1b2AJ2Ewtgv1Ow^b2^xF1giOP32S*R%U;M)7bU!V6t2~3)UA|Gt19+8 z;Yh@tD;B(@p(w|@s<2rRH;^)o$RvG?Wyu3Odp_>5 zIO9d$5<()|vMleiCCfHR9NCgB*|KF@-a`^nw&ab&R+dmGP)eaKztT1>rC-|qLZPKi z+R##H*vitdL)mvo;di}^W}e?nEWbR;mtQk~%zNj(d(ZjYbMHNp?mdeWlNut4+_Ap* zG$;+$*_Z=f&C*8i-N#%88Yge6cA^yy;Rq>KDT%Q%M&+6b!c7O7Nvh*?R*dO#sqXF> zLXm(yLUVgh9l#k39@7kGTxny5GLF@~r$8~VOv@bbY)?(%b^Hp#Yw<9jg-P&ZeA~s< zeL)d5iH&N>gL-pu9I+-Oe^KUhnY^|F>&4p)#{v&cg$XofZtLeUfhmzkr7xIS{#OA;?n)x5`tWo70Q;0INcg}Dsp`5EV%Q>=! z*m0F8GkaV65}cYP9KB$VTaWX5n@|KSA~pvcnngUiDVV$}KG+n;)S^u7Z459BCd{rG z!+8JiPttM4DB*@%AL@|jflc$eK1GK!Y@(eRDj`<))*!Iq1I?-#*b&QghLV!P-YOJ1 ze4rtkMIPFDGt`tU?yZc}J_jtCMV+Gd8B_^hiI$-0mQ;#yUryEWsw1HP@mUHC85jJz~^{$#EA%sgt)4Fbp0hmuto_ z@h~~ACS~>HZIJiSWItQ;vOZ!l%y6PMi2uK8?gMjkA6UC#ZE5XKR_|TCadl<>{VRXI zl37_@e)sat%WF%2vBa+Hm)601Kn`r_->c{K=Pds9Vqx*Th5Hstx(gPbr2AW)w3XPJ zhbIWkrf%bpHd5>9jm7mp-g@VJdH%`sPd;=oKp*=1+4(EA$dkGqU&qYmeKkYWS9gRW zJy+WA@G*>CjByqwYcfajfwsR#W~){Uwk<$^eixI%y0L^SCe__OVdA}b{^k+WNZUjO z0#!FAdMT4tF(n)AywPGWNs_nNs*pW2oQcQd-Mrb3ZKoQEBw36{3yDUyQ}59cbdu@q zW2TX|hjBLIjWW#&Od2&M$#mJm;n_x+Ph^pNHYgLWOjJ~g$~LYPJ%uXU;o7Nuh0lx3 zWY#fVX2g_8xQbH7L50mGQZ6WNhIThS2_$cbwpfqsv$aXFN3_$uT8h}FrDil0#FGu8 z67W#aO=_|%fewtAhJwj(NI)^WujLgp**yGEue9oDNcQDj;hZn2Fj=YXb@17Qahoia zxJs|Z)r&sXiA0H(=?RM5Y)yrJliwUU9L*)Ex_4k8_Q6kueBp-69g zDzs1T)Onomgv-TvqFS4r{X`tD)A^l{*^C;xWTjT_baTl#iS=q|8siNvu4>DraK&P; zcym!iO0v0arYKQve^QWZSgr;LvR8re?7cSXkxtH*^yScK9<|j4U8Ywi$H%*yl8S|GAG}I-; zQnD)%Y1G@RIm$M>nG04O*09|ym~)}+bSs$1R(wUiTaJldqLivnD)jy_P?mw`R&153 zW3t~Lm#r-<;8(g`OFq`Z9eGP7T()-XnSwuK+-_IuVziem5uq|ig=K$$o?v>>7^vUo zGZ?E`l;Jz2hNzetp;$FlvC18L#bEbm%aV%@(P>$B7Pc{_BzyT>Nk~fJZm~&VlWE50 zpEG9ab9S<|mK>9fLZn)8)#D@~<&};xR9QiBL34o zN~0s50l`7iZN)@%s-1M8Lb_sTFU#QImB(P2|{Sw z*Hj|)V1r3h!EBjKcTUT4G=KM)sWY63+G0_ zbgYHt;38-e?d_NcLzDGd5 zd!{G(lNPMTH;nGM=)$%u`HqLn6+C%ah8gP79(&p<{pyIRUv8qPA(i7SO;RjmIHNDx z&83){ooU5vrf4r_L+W`O-bFm^ZN(S!$;n=-UXR8l##NFhjmi9dW2S;P8w$i-(Xu^P z^Vp)KCs+$BIO0j03{__obJk2`sFEcL5n>w?=#Hyci9* zB-|~oO?4~=Lk%<8oJ+NDIo8Vr=fDZ)oPtV*5v%JfZZhsdn+Z)Yy>tYWh@ngq;bNFfwaQt5tLam^w9uE|% z#a+RHd#*5f#2bhW$7vquXv2XJg%1t1b(km`kZMzKPN6UNmAFE=Kc$%rdyG3hy)H zD4XFaYrR_!8>&@zD3>DKNVb$^iL{N_W-C}d&`1(WB7kA=ZRNBDrOS_)MvUfaB@{I) z37?_Zr95RRZbC7mRBNVrteQ4bq_@aNni)^fxZR4CLpj;+;oZ$D>S@>5(^k|6$4DEt zE|Du2Os<*~mZF)o5UL|=fXtN;p=%=jB5mkaq&n5LD%&(GHauJj_vG5GSkNDDoo1rF zqsb35O~G4_w*ri{n`@_}oSaJZLM<_C9@H-;t@SDyXm$)8BTaAffqJASM%b)B7b+0` zV%t5TQ|A~cO=fyEjK$y$afwby24k7&rM;n!)e~u@vL-HFuX?R%kI`4yuCs0?!H{?* znJt(2PSqWr;5mQ&m?;%O8II*VY^{fe%6QTgB+3aJi~7USBGZPetZNi_$<|CL+ipBp ziu$;CDo4mUZ!3nL_AI$;%+v$TC{kw75Z3hC#5B}#Im})7wr>(*UDg)z`Wq;G5}=S zq3kS5`69+zGVxqFWUj^wI97~ub{yHxNBqHRCEF+j!ktdSQ)^B>OXlufo%hd?bNA0J zKC<}X#kVis0;>Q%YvGZFk1YJ*!W|3At&eTJYwLwu$t}Z{Zu4XC>;8_-#O4WD72w}D z{&3^=4Qk`^^`EbQWc~NoZ(WbAU$*wh+K1QPzIF>__x<^2EEpDau)5E)R)4zsFRO1` z?ZR>{&s_QO%7ZI!U1_fbSGJdbxcpDcZ&_|G`BE~J z)!Ov)oBs~Oj{oui&f|akbp$F2Yd^i|5BAgAPjCAD{j~Pen|^OUt^M?--`!7ZKfUR9 zHqIGopf)u@@~vtzXAN18-!tYqwdu+AztGzBuJw0oZF=YWpKERU()B+ZEtct7kF_Q< zK3{7xm)7WL^(gKOg~>eHJB*2Y=Xr#JPly>vXRsU;D=yZkh*O~11| zt}Qh6pmXWl%j4QY)0=*4d0fR^%)2TONu(;*8O2zc1;e~OPis?;53#3*6!G);Srh$cs)~5c2 z@P5-NRVrac%ki^dY9&cn&vN`MnBMf7WkD+d=WTqzf!(mqf zS#t717Do3Rj)#tsR>I=4TH|qBKElUM6-g|ct2QneDz3q2afLtlCE zVF|yCi0erS$3^ZQ9)mO1@y2b524_j7+XR>7D8!QD9YsVU8g!#`f;D+@Du~!=Q)zlk zx6|Q>m2!dX#JT2#+R}7R#NfS7rS6hFNP-OR=G#GntYu3zN2;0%%OSUqM8hV^To{X- zi0MAv$!HLEbvSbDe<(Qh2q2Fsa_q>E&*L`3V9aM2YGyZD%(mk~%{;h|VS~mW* zy=dHYqUvZi6P0Y;9xcg^9@jCF^lqGh(O+jA@|mp_nS2Uft2+`^q?0bUI=v`;LiD!M z7BgpY$-V+g7gDvFFV%~4@SaBMn9M@8nRIs|SS{g*@>sUn?gdgOA_-5pTu&;|e87!r z2{juI{nlIW6#6FyR_Eo-1FLhKFX7ptx0I=`Ma;Z&>h%dnhBgsC4krN1nZ6ISQ2 z|LL#D-kbsfTACbN$qd|kPl-}R;KtSvrFx{TIc^I)23uouT93CiW}o1-M9E$mt5_9Q z?)XsNpUZOYc&b4bb1}I~XM1*Y$%EVA6BLeX5Jbv0nst5pDw9nU1n?3UP`c)NHr++@`$HIRa~Sk-OTYQU8xz(y&!8wIH{Ej#%d8) zZP!bk*p=xhOaW0RcCEZG;S?0bOvNl>qG^sxEU(Sh42OQ}ttT(-Uoo&Xuc{rgHODO4 zvu11T&i_B!nkkmR?)c@r@=REQ!+zqwB1_Oz`1)uM92ey}=}SHcw>BCFn`qcK!|{2x zDK+3f4}#8z)?mbN95zs4%RKx~>)?Nn2&NdZJNUmNUxlE+gBH+O$rTZ9*bFO)>_FCG z-fW9)u=U|T4ls`JOn_7xY=b}aZ^QmswzAKvB<4!{e}}RAY;G{RVI#595*vfz3>B~+ z&u@W(&e&2OvwmX zv(=JqxDtuhcZ0hzV_Gy?njxmrYwxG`!8vtv!47{?-1fZ&kPQ{*}Fz@XFfqzb@aoeEHG? zOLs3Nmd*no2VL+Spx3`&|581&`0tDNEV7FiEqs3AMGN@CIq=s0RXPj2vA=Kr`Sa-9 z!%)=Z&rNz5(xKa)TIGGbB_1uF12I`AK7_#c&no-LEe^hQwH`(XX-9t$8U()M(!*E} z?Ojjb@8G*OsJj_hjN(whH=m=2vEWcL$-|`eo)#$vl?ag~>;4Jt9btt4x35r*ogZz! zEcVkBTHIHtM)rpkn%`Hb#;!*wRM=OjMiyg*vil143-MU5%(zIOLt z_7$qJ^P?%$Y@bZEut@JKR3rNXg)TDt3f0*4bcHS^_Z6y<#Ymxx)L5alUb9{#ePtLw z-!8IrNSKU&IYOZ;UmYt{Yv)H(sLeW=XLjW)F}Ue4-S&*^GZ{1-31>0}^Wj_uh#9e3 zSN>}}M_O4P%+bo%#&e{#>*;f}^7Zi?X=O2*qm?hKb5u|FP?2{R@OZWwh>)@AR%GQ{ z>Ktk7{OEHunQ?jLo2o*!u|K5HZ>tK`*7XR5en(ZPHWp)req&#uomw~*;2MQigye*B zK`BmGs4l;+P>r1*O`#^+;Ux-R6r4K#tg@ecO6xK>+*7-7PtAsY(%9*Q{dw2O=V0D- z+5LIf*!A>z*X3rMz{7_NI&pu|Xp}!%G`gFv)00m4EZ3^JiEjcOi|NVBFW26G_$9$B zr=A1oI5&Kk*@9e}Y`R^?E4^%(5!z@ZH5m~yw$}|otaF(vTW@qTqb~)!?#g}bY2GQSb*q<+rEJpLShV0K*Q7n?Ke5kFsqg0|0h?b|@|CP|bLN#`NwE41`CO5!J@T{^w z?Rg#BpD&H*59VtnvOixMyPiH@D^d7K`qRnJ(*GUw;QoJm?hod+UbA(@=6##g#z!_{ z>mOc^uKmkec=f^6;L1O(_?Pcr_AULxk{A3v@aR9FcP)NwQCgf|c~<4 z769VR0A|R(r3HX61TaIYva>w}g_!}INS@QC0(ZKU&25^?AAU!jHvpNOBJ*iV(zU;^3K2%@6j?1+--8DzJCpojI zGc5Wd+{4T$>KK0oEu%`DD(V>L?6aW2q5r?`M{{d$So$RVW}nm?FxEoUvP;hwOzrjf9&fVqwfkJ7umbE8&RMin--@TXY7! zEq_}InCqp2l#3<{DY{f3>{y}8r--!KEu`Fz1YyK_a-oE`Tk@`m7pg98!4l)4*ZHc% z!g_0&;nYDl%Gs;~tnBmBYx`FXOx)c|^E)%_+p}llEWef}Zid{+N0~S^$@s6xzGdN3 z9jq1#UUOYMh|jthZla5`JYTtZyrj4xveEJ?!+4|2pw*zu^!L(Jf*uCZOd4Ey&(H+e z>?X^=S@=j3V0Kl*L=q;-lrn6p`<#?oAap~DS)C)+m+1EvZGK@7)zN==_TtuH3#(|7GJ zJM!n$aHe0+kgOZ`cZ2HI|GYzLdz65}!;zu!NWCMm5g%f*@8)m;=@tuli4W7IsuI~{ z+6lN6>-JK?7H%99rg?BJK52gbhr%;e00VKR%Y zyJ-|Jaio<&EJ}(O644Y*3bv%ZDtSZZ9_~YJekReWC}E5|;qr5}E>=79LY!{JL#9iy zf)qFVS_#c+D|0mDAc5W-T`PZ9I*r=}m^ZF%gz10U+FJ8H8 z`P<9n(!EO?psK%L|BS^KEPQ6+YTYa5zcTND9e$}lcU;uRw&qs&%gDL8mE{$^{>J5< z8EUzW{h3B?HPdUx&6n?fVB@yo?|(clwrA4Q@Qy3{o~^lg{<eb)e|!21Rhvs`p zJ=Q#~4Bk6m=SrTynlEXU=Y87kx*g>-6FzU2YZZ?6u1 ze}~eH^qKu@2S^PE8)s=}hN^jEr0Vy}^jr6#LO*r$0OmK|s2S#k{pSqk`;Z+12lIW5 zPuCdRA>%W3w!r|_*FHD+{q?&vgWK+3GXQsrQQxmh1ij#Gkp$Ce9_@^Ige9K}a$w2t z|H^s8-?wQ7`PBZ^1CWPy%;JFtk8yt*Yw)nc`E-Y;fdRk&3-=FxfAO*z;5jGYEgXh- zq;t_x+hnTthF1IGiw5u>et~9qPwYDe@J_MX>J%U2B{YUNHq}#s4NdiDhI;$#wq{`G z_3hA9kAaO|dG>J~=bSXgHMY-FaSb%}sqYPb{|BKN)}?(L)YswXF_^{G>8P2VY5<3u z_hYvVkbeB1H6y*UZyg{#W%JJ95i~}6#6F&ices!L?cD>sAJsnLJAGt;_mCxA+?m0B zW{mfcQJji%xS{{`(gDs7U#eNJm-a0aD_~)kI+SO$0)~70LB{~;hn}k$>BW8X0O=`v zdj?;SG1BoipQ_E_Hh4h_>g zZymt=o406&d13!52=nMyp5qQR#@G(+?x`m@LvZhXWB~537_Hz2_5c2D;oiBe?`%E1 z^^vW=+j{rb+qYh~b>~)htGM;Nt>D(RTUTv8W9uAP+wZ%ZpWpoG=HGAr#pXLU@7cU- zv$t8=ykRr6`P|Lj&5JkBg?0bFxA6s78Q_B(@7ehMjn{9S+_+^!+PHBeym8%zabtVq z2^$OR-(UaY`a|pgu>P0pf3W^r>wD|ZUw!B58&~gMy=_%qy?K>fb*)-fcUGUc3RZr! z@|BfOtUR#tS1bR|%5Sf{Y~}Wq>I%COTXC--E6-Ya(#q2EkC(r?{7G0t;IEheX!%Xc zFJHcN`31|hKI=_gBHTl&<}KQH~w(z}-4y!48tJC^E8xg}}|Te2-( zw)Ets74TE=AK>e--oXD0{usOkyb}BdPyh#L;01Q@Y;Yl1)&ETYU;0n$Kcv4;|0nw2 z(Z5RnLVW|)KTPOxy+ePw{wex3SUKSvi=SEim&Nxj{^{ad7k_i{MT^bF{35gHTXZg7 zvG~-*^@aal_~ycA7yfnOZx`-eFR$OU9$CMB-L!tm`g!Y%Yd=`~(%Q$?{&DU9to`BI z8`k=3x2{#zGO%s}x@KN`=Gysd`qdwC0y3gr8tb4!i&vb9oy+-#ESS3NwrF4GXHM%QxPuFeE|6=~z2gfz@??3qa z%u4b)WfEpG)? zP*uq{JqyS{R>@cXHmHD#O78t9D1)*}zVtF60ZAoaasreH>>1Lp9L9^QOO%_0XKo0RFX-98^MhSWD48>ZcxeS>%jBC^A5;YgEUC1WRL|Z zkUAi521$@qNnai?fH@#JkN^pl^z1;RO&^e#018kldELDr4&n#opTXKPF_nDI5+DJ2 zKI0T1w~7d%%bKXo&>7F?^6U;7-){PP@@{K|RY8gPwD ze&K#_HMm+OAHE1Ufm01I)mzk{>(?Ou(d)?~4K>FskHx9|2c^t5otmKLiF~P{}`k2wVxSRLOfU z1Sh}=m3-Hyz!l&Mm3-$z;Bs*J0eLIf1-mNw_K(6hif60j?;+ zRon)Z0{2z76~?_+6DOromgmTUD~)2EPM-MUToPxL=g$)%+J$NC?u zq&}?wk^V<2sk>kQL;Vl;$$$LWclAHe|3D>wc!mCZ`tPaacVDFcuKv3!`K>qWzoY+- zN`Awp|F-_yD*2U1^xx8dOC`Tl)_+s~%>z=W|GxhFD*3rj>%XD@hDv_=-{AMyf2rgr zeysnx{_86F5T^f{{%b1vudM#7`mY|4H|oEl|B6cf!{_y1(tk-M|L%GEFY3RjlJEPN z{tNmqsN~=H^`F;&UM1gqLjSP-VU>JORsWy*|5V98{ht1F`p+GZX8mXNpFJQ=`Y-Fh ztdhU~I{1D0X_fpvoBlua|Dlp^{*wMv`cEB@pVxm<|4Eg6{a5v$(0}59+|z$t|8bRk z%@gz=(|=4QU;QloL;8nQ@@2o!|GWO*hyMSI`nmP?(re%!kN-TbfyXuQzfA)#UGLil zo`(Hv4|y6oo#te_Do@t5o_=jO;b}O?uc|caJX_Alb&ju#l>&4QGg+NS6ofd!i*E2w z6Ul%_r{9wvZd0$n1|6mc5KMXi40`3{N&|YzLfE&AY96#;Dqv9_WWl|}JZJ#@U^yOo zYb){~pYFJk$PNB~`aSx9!6^=JGu+w_-CKwH7)rk6xMSbbMm2DY-FM0}jYiN?ea)XY zQ2Holpu`W|iBH2g?P_w+;Y^+KoK}kyc0bZ7&s-NG9S74!I<5*6MYB}Z$~P=@B^O6i z1q?Bt=Tv#6wZ05@fiLdzTaFRzzuJgH**L+U4EY2}jV;WxaAVI zz-JnRw9E(EghRje*1K-%TZhp}#QZFGt<#s9)~$I&)r3oCx$_&${w$Hz!}c@W)D1oF zhtB5*?zWfg%y8LzlVnOE#1-+*|whOrw+WnVKhd>o2%Gq350jobfShd zBRRJpakS+?z2vv7_hYBl^CShyM7Lyod=N9<6z4-Z#y`X91$M*xAZN{Yb3_o!2X zj0PL+mltGYx0~$81{q=AIBjzz!`?>WM456__|ge)q8UsilN8^MngjI|OS-GRMmcuo zZyI8iG~)hbF(0uuXuHpojnxt@)2>GeRVp2%fEdemKke>S^9mipy*Sf0(`AAP6W$c> zvM62Is89`C%iF3L!%iw~Y?AhJR%4vuVCZlo4!-}NG55i_t=l#q+3aonc%udH=RSlJA6Fjh@nMOn8CbzIXlz zLE6~E+JOvfbB$+9Dh&)-tb35s%gF|DMoYYO>LbdLb z6iYr{^`SjSJr&8?xV*25@}5*0^C{b{uG^cZDSsg4k9%0oD^RB)IB%>2?c>@>_=RlQ zjZhY}*%P=t7cS)~3+8gr(VoHMYWvG{kck=nQmO9HqJt-lb)bD*J4KmALR2XokgDmB zLG}wK(u7(~UJ3P9lL?DWw9#w>Aq^`MKOnA{uu4pE< zJFTo#!<1Y*(s0-Ol}6rs+AN$iLNEi@9oe3-IJ3@jILDU#ZnW_~ zJuK69<)R^(C5q-iN{F%^doqrB!r3_AEjd}7%7^Tw3>GZyIPA7&&grm9y*86fhD50p z{uDhl5z1SyT_{(QXMpdPLC`E5>xWRCo6bp-8|U zp|v1b86lW~>nwq0S`aLc5X=x8HcMRA^uVFp_m)NoW{CEhCE`j80x&`_LmbyEkyKg` z=tl_3bhDPJ7hI@0Bp_rf&@9$0JXdfR{aCtGxB4u`5RD2}0~b&2%n&Fw9f7#FsE6r` zmk&ZAtWJj~=krXyNE|dcCKzV=0qpI23**tIEtG-5;)R6E?l2TOj^p0W?}<-8v!b7*?vRkwhp^_k_VaXaFi>SiM--U5JaiPxO$$6udp3!7%g?OXwxCH{!-a(j4@qB zvU!Aj$5ZS|RHPy#`fO7=F@=Z0)=n*MpYQ;3o&b30C_+paj0G&ZV6G@6wynS#^_ z(avxllT3lEw_9@M6sn&avb}Nock>QeF#Bw+YEobbwy9X^?yxOo4rL-8#O!9RZLFO2 zIVHavbs{OMpB=KjK?7Q)WvwMS+#PiiY!72PB}~Cuu~sZ;a*0fmr0Xr#REqIoIg3(# zcF6X58qiugp>`az78*p(8VKghN;8-frM!!dcd95M*v%0+gxX9VGZ&)zHxJpSHK29T z1nNywwv++0h*5hgZ7#y9!7f6s6hb*`0rx2tCgO4kfh1HyX2>?B0WE`CGkG)}3)3}Y z*q1SvOHwlv3Gr+?K)X%O9u>}bs}#|Q2pHABX~;II0c}}sv!#@?3~O+vNZJ>#QVn0y zE%(T1ylWO+0XydiA*pUxsTryMjYGDK2DCz>TA__BZRYH3H)#ze#S(9~RLpXqS+%rN zp=QJrz*|%ynMA1m4MVmG4QPd^rNfZ%l7opcq~NVr99%MD3#3c7yuxs0F5mT%F>9{v zcEWZ0ydhg!1KJr*+o*ne$d=N8c7`J~s-GINjcY-x{alRdCx>ifTF`1g?4tV2kS(c! z?F{E)R6j9f8`YqM84moYK0Ra`nSS1~GsAfs)u)DR!y4GmaEM0r<3qL~4QyvP8Ke5K zA={t^wlf@eQGIgAHlTs+3@2k$KRRUVpMEg0GsDps)sGC>5*pafaNb4r!$Y<{4QyvP z#G?A4AzNGn+Zj%#sD5zB)~kW-4987WKQLsAX<$1;3M8uUAF}mKKbn{&Z4uQchHTv$ z*v@d?MD=|`wk{29XE;Qn`uLD7s)6kcCqq=QoekA@57}O;f$a=OKSbX(9Qku*8Tm2iIYb{F^15agUdNo*5dG_i zysn;w*D>cYME|-WFXwE$jy!K6`p+Hma?HZ(nDZ2(f9;T$eFk2~9lcQf=RjUOuxVHx zGn~Cp{cDD7ts2L&wq^}%XE@IoeE%=b z-#fQ)-pcFsSHQnK{_}si242$WTL!LEd(HW?dnBmQIn?1eJwxoTf$P)^TkR;nu%;3-8zadX^?n^86# zG@{O8-Ik`aW>)aIDdmiPva_LjF~Hk7vev9*-BqLFj5h54Sj)tq2@7;pC>x`$o=?be zw6RrgILkr|6Y*e>OSroEY+FdM)<)Rav$U$6kORv%%E3JJ^3j$kbujcnI_6T}JlJSR-;n{N3K03vL_*b)&rk8*?#>W`Kopik(_*0+mL#?_r z!~Z=Z9nz#*^pW0>r#MbOx>NQcx9Wip^^s2BN4kd}axg#a6b_v*+1w!7W$5{R;Cen_ zzVBl_aGU%knj5R#cC5Jx)O{&a@k9_qs-l+=i_S`$3Rf!CY{iYZ+MQ66>?$OE!Wu&! zgSlbae#B@p%2*Lkis>ZoPwxsPF5uz)=5!JsMyL5Si`5gcU_H_iv++u{O6)cR4lC)& z8#U9cOOQdODM^QSlzCrW`xQS^0tXgt;>x^2e2 zLYqs{u3V68llBJY=+x*|UXlvUTAWMOt+d5m2r-V{neRLgUp7bSrnws}wd=iP-pB`K z=tEn;h@{!M%eLKFI_oXt6d^NouGr{^yD=$Q=F|ReI)L*$<~YDV;?i6K)B!_T1i*7*h?vL9IZ8_hGgspOx9SplWw57V0l;SHav+l z59Q%a%@e!PB2ly?#YBS+SbDqVhBbig);bodQIEq4NBN??frpWtlfbzWleIBq7KtUi z&Mxh*m$+WIOq5JqqFzbR*{Eb|2BI1l`(WsBv9GRQIEO(8{>}A`PcA*UY+Cu=>Rqef z*nIBVmtn5Iw{BLKf3jp>{)gq)Ea%pqv7`qdS@~O-5=EZ-AWQ?aw)$5qH^Te_@6b2( zZvDB7pWXV<;vX&EvPdjmup};gapBJv?pPxiA`4H~eO>o|bT8G>x@WFFYv~vB-z`YH*ZT9RAnt>w8v))7Kgp=p>^!2hdpeIW3sy@^Ehp)r`)DMimpVn zJz8#sgm{h%w=BL`hOH?Ic$IO1ho1^Idn;aIj9o&ixdWwOvXP)GRKOMLT2pKQw@5WK zSx+~pw8eP+NQ9N@h4O5Nj=T9nGF=f%6|*B9Mq%tiJ1mqr#oU#PbS+;?BRXN!1CjF9 znza;SP@&c_q6Qx#h^~;m)3ex(vb7iG<5@>L(+$S3`A?2|n9GE0>N(P(Af5t#T&j;_`gmxSDqUAFQ}W1j?;Q1Ll@d|bDV2H3h{tlqWVe&=l#Pj&o9JXJM846jyUG+Brrqtu zzZ>@;&4eu#Y(#UWrq@=nI~qx6Cs=W{1hXT`Hzc|nvDk6k8!W-p*u$mU2zFBbppkF; zU5SuCAXV~Gkfqy^iq9TP`a43MY*OZ&L#)WU-yHR*8H@2zt#`3#>;JGxB-bRjPNB<#5*;?3N^$O*|j0!(ion z&_OxogQFf&Nec6_DH%^R(l)+QX?bWj9+Wf9Y_`DLda|9Z)x2yz>O}QF81*O_lZ=C+ z8*T^9BRGuPkgE>6ug8T0Haeb+csgZ~ceX9|Mo4$}sD}{B_*=zr#3Z#VE}u2s=~x@p zOc`3UTA~?E5s5;mlc~F+ZsWpe?UwQ$-rI|0k$5BQtGnHD7YjD>1_SP|2Be0QMN(OJ zlS7i#Okv>{BN?z*oU4#=+M{;6(N=FZEg_a=3Phc?%E62)V8+{6CMcAdPDnRiS2<%n zrL==aBXp6q8DI!Xz}s`>UG8Gti6%uQ+Ql0*T$u$bwJ!Cbv0PXFW(A4L2e_ zF-6IEG}+$<4ut(3Uq5&)~AJnc{q+DHIU8zEB~X zDws$l48tG5r$;?9WF`e8M2K3DMAMi7b(%V%RyR!^XmR zD`%|;97QfHj(P;@b&uO-vLjCGAgaKTH&J*y8TQCtHf@k`gic6t)?IAoLJMCW_3(O| zZd;JISi8cJm;gDW3TF-qNS@uN+5&oOt;sD}%wap8i{Hii*%u3Har z%>repg{|3QgsjR~Oqx@g%Im%^~rHq$nGBpmazc2iMog>1D> zjF0pfV=-4kjSJ5o_9)g1Y|LR0Gu3+0-b)djVkn!S@wK-)1-H;M5WLHVG%y3hgmkWv z45?=^dpn{h(KSnDq8GH;Yq&FQui6`^ShNLm?kE!o(FlSUR!2KZ!pkl}s7n=uFf#Qp z6AgQcFgbzbGQkDL!>GS*))7V=1Y<#0KQls=AStIWkTzM9M5vrf^+GNxi3B``Y_V20 ziI~tLszuR7_v!=~KR?Wdw!ae>eT8b-E1GMDw3tECseIT;=Q^=KJs@UW2BhgSW>c$w zI+BsfI?!(#zQF^vbhGBR3{%oDJ!;8Ec~3 z%@9(sS1T0Uvf09Wdhw*OOnWO_j|p2`4)D#9j7|{CRXW8cRZgcJ6|cc*CD{_0rhGoP z)zHn<0xknnP6~WSncp4tP>MJeN9bZXAEsHPoarb8YVtbGPSHZ(xmJ?0TU!<j+FnWNmIBore!mP+^~S79|W>>-7mg^UGDB~v7t zhbdC*X2M#NZM*?5Hp7J?@0pr?JhCNF07>z|FufS)lF03oLEGegz=}N_# zo2gnOwu-H6i%3bOWFgizMlxbz4Nv;8OgB|;mlX$6B||VC&|$jrZK8^h^bJBtwBd~mI4!ML*GQ2PI;tgCf>G(;_aH<6$v9PuW|m7VUJSOXc5}A zOeXKBAd;V{)c97+5>X6tCz*4F6v8Nw<+{x-dyLB`$1-|de-(Z&7JAhX9x_HqyE$Gh zk?xF@w6Y#|P%1bKH7?IqY5faFGAvn7v+iUGJZdia8-WnY5!Mi1gijz#9zGT73iSDl z#i?MKT)uMDqe<86qE&FG?M+z2vzWnRnKq?#2(seLgh{{B%a%y5t?RcLmoFUlkgzoB zNOzkaKUT^ZOdM-Ua;;RyV$8X$&Ri7@7U~@nZcjy9tIz8E0G^3rGqi_lHgjWn#zQd25;TTC|^6Wt|;KiCLlj0UT> zAy*UehPM~Cwn1iYZvJnlzj#01;{RF=j23&sEfbR6z-JOvi=vEJx|sJ%b;g^hl*_d! zEoBPbVltJsnUS^eB1xEY41A+cdd!Y6pTtVG2*Yts3r}YaRJ$D|90f5YIyx|XY3*er z8LX7x+C{?DDG02aWKEfZ&E&IttYL=@6U}U{lp%#my$~SewZ^E2_LyHiJ?bIF-9Z`e z6i}raXR)Tk+Y|CZD%xq|aky`Tfo{{}W85TsYy)o8LzLrAS2o=WwvZ%hPL??rBP2~d zIv(#T0f8vlqVZVQ9`L3d;O~b$q_`qxIA@TE1nWW#kG2&&WAbo$E3~IBBq4_>K2d9g zDqa(!9~-#1r)a4=Q;cX7Ovz#;>*}GFgr}6u7Cm+a&GssY3(Zr#NIEq?{^BIOeq+s& z%0evN-kN3JxaNRQ6g=urB&c1CY7gby{3)r<|;y~XL86gCR%oj zD8jP6(9#D7_y0R{Z_sT$d+pn6uUk8J_1@L^$~RYD3H|)vw|v9WKP~09Ua|S$Ccbgk z#tzKee>3>W##dm1(MA1l>CKDpU%YAI!G)aeXS%)lpU%H_UO)GSv$!1!CrQAQmW{cy z8y|hdh0GlrC!;^)QMUBplk2$F2ItD`$;bc?9Qq8-pVM7Q$9P7^ zgmBQ|@t|jL1)c7tsAbSb#}R`rpA11f*TF&0;6thz&*_(J{K+7cw+rRX;9RO%o_b%e z6ix=9oWVMt!P```oWmEYa7z8-DV@OsRkNr^UB|}B3YnvIObqbAq0ivDsu|B{9hXn~ zpp3z?nIXh_y65J286UN5a42uEY-Vt9)hzGuvhhMWgJm;=$E#*J6Uzo0OcOMJGkC#j z7WL@MX0lM}XxVrMc;L`yaG%wTXS8ezCtYLp&QQ*A`W?@B*&J3cf6@UZ4HnD{UWA$@ z9V{5Q`)%WCoWV&$v!F+x#z~in(KK4e(>TMeuVy^^(};|xafT4e>0yyaOe1&F0wo#X zG)A0Zp&D!_+GNSX;+lEo|l<>$fSruQgXOV`jq69?-^L6V=t*vO@HwPBFe9}4IpEE=f`L5~1f$M4`a{?_%t`h{y>UVHc23)jfCXRLm6^{-b? zuBKKmTlvAt2jGc8X2r1lv*r7jU$@LJTbAdS{$=U6mr6@l>rzY0;2~Ibr4H*1ZNlOP zf2i;1z54SPA71>E#oHD`i%(tn>cV?qUAy?gHmqd#H@dyEc!uyet6!A{hDT9~MI;h+ z&IGT7bLpDL$=eCSjhK=pPpM(^1&Nj$o@OEDu+fBV-aG1%v~d<8#dZT+&)7yxnG8yW zg|5QKig}OOTg7`hr(&l~)I*un{zMohL=90jE4pyZ|v9t}EDosg>-&ZI29AmuhRQcK?+^`HgW zUf|ObJfM&C%6uV6cnkg%yf0_ttw5`dNQ@cv6(X(9+gr zEp8VO1GfG<;~uFb9%Rv?zf>{0^Wj#(X>V}Q0mL0vVgakA058270#k3Wq4jr-dc>%j zr;Z29v6wXvuk_rhF4pn5Bh^rgB}~prhBUbwbWW`Ha%I#t!<$|^f;WiiYNj6anEixle z5q~)*7*X0;vS8I@3QboFs1@anDY+UcB?72ecKV|scfv$3yn5UNXUjDaiRKzDH_Wn$ zWDTusG3-t!Y|%u*=HSsPiOwsUZ-^~= z<2+(WbZtdDW(_U==ctFj?xU?Hm~GT7D+4N^ud(iZPM$I*s7J zMm=!KVz9IB2-eYs|31|Hs~&M@g<`cY&2#WmaWXzINMq zVQk#5H!rVao+6}(kPKs5p-?C!h1QT1lE99R&>jjcAw`5jYTmQEfuW(=xMv1)++Lct z=Nvd-j2mn`tjETT@n9InbHIa*v1t!u2Fzv{hV9JuWh$$?Ue>G1tmk?*o>Tcx*7Du& z-fz45mV1A{fnv@n_2i3ci2+Zd1gu$0natWdBqRktGW@diyJHsZCzew{T~q9DUHsNh z)DmQe%frcXw6bQUKd)wez`-z-M6N5T?ufFQ43DP0HScH8!GBpx^qme1?x_JLD&^0o zt1=r~tImYq?YcWO?BPtSn@%S?T^btqf2x+i@I_4G9Y~njHlg&HVy%QawxVT0qw#hD zbKnhmrcKDaIIZuf^G>|(0h|XmhtQ(8pz`&C5UHKhP=wNmd^||>1&#+X(MoO~)jHZt z%U}n^JI~wBJ z2)wIY9CLfT6$i zHWuK4KjU?+;5(2sU_*O4Z^%N#PGng}=M%O~v{Z)Aj~`sLBU|stA@22d*dW~u2J;12 zNUNUO^itP%NR^o`WoVS{n(z)iJiU}atCc*NS(~xmAuB76z*Bz4^_XS`5dp3Zc{^BB zc-bh>gMU~{U6vfiL_<#|66W}vr7jGQOQ9EE!{k@K3{n)*mML-+q}Es-g+$r?`9 zX@}oUyNyDI+vdzRXb8zBSetJu;BlrQDZXl*HfxF1uvPy*dOiOVe_&jJVN)n+)d$xl;tI;f)6<9b$uyW133HF@BtEK%`IY zb+bh1rGtKVRYi%!36*AI@S;D?)%MC7XpIPUcW!$+W*2MdaKE-{iN+0k%WQfT+c(v| z-ib+7$m!WSTDt__q%eB7gN4pq!H+*&O9VdK%cdYZA)XPNa+$^;d@60~t}+DUS6FOL zu|zLvfmo+o|M*gZ9u_v%$vIA>To4p99ye7!f+5^B6<_xVg;OkHRh5hU;5%yxzeQQq z77hxv$BCUy)@jXKoe79JTdlkX;Vd_!Y%L&O86kWB;$nGZUPY4>fzAX}QdMm`$cSCb z(%3cBo^WlUF_(Nv4EV;FF;=w-&vvD@KkNv21qr@1U9!PajHG14#aKUpioUiit2NZY zlcQg*CEPrK!$GtQG9?5{-N}pD4mMbJg;aAmfR>)9z|9insO04Tt&MV2Y#VemwDMcn zfY|kBYGPW0xF^DTZ<_4Nf!DOL6_O)X{VmVt3!2c^(30wj9?r@eyA^fwKo)elmrNU2 zW{fyx6OX15w_iJ}%uHNjN0^a{Bv@Bp&n?{W2fPB}2z0t-a%>%4DUCnbC7w8%|beYplTbVuovrIkSkt04+Nyw87*F z6C!t`TEa-Y$q4FTaeG*p(voTUVSBObtBaMfjz6n7GKX@rY6C*a%%!Ret z5^LKo2yLn@khrvS5X_)}Sw28JRo5FOx4yd`4t-|9Yc$1Jvl{GNbUq66X?rUTlPnu_ zNGfme_+r!{`wQdCYZV$l?jV%8gazy3aD zV?6ISM^jQ5R#^n%)L?5MV;zXXC9wNxfbGbrXP5x*dmHX8Y8`T?uXB^`#ur)N(W31{ z55}CdGE`@}gM4}nB7n9kX}AsO=u$!g0cByJa?aLfhb?5)P)v$Sf~X@o9T$);j06G` zw%*Es?$zN+nJCBAsM{V{xZ(y~61Gs>P8EF(V!j3~+)#$IMzKxdUfa0$cWSGIxMd9I zh(HUiFcuIHACpb7Dg>ccOlj-N0EUwe!A>hLIs9}jF><^8-Wnast$x!GW?{96D4i~3 zV+A6a5WZMfr-X+bfgU$;>3J>bgm5e6!_7qWAj~EVm z-CdVmuXbYBn}DFPxyJxx=9z|fb9cx~x8GA+HOwqzjj=nH+|uKqPYuh}GIP7W>s+aBUS1o981UBTAVdE*!%UVgldXDNA+ebrAsmJ!J^A zU2XF_L@j}>T2ZDncR4)Tn4{jT>gMX0Q7joJC6*h`!ll8iTS`oZ$9HN8bTPHNBeg$7 zgQ*gYR!ekgAP(JR8B-M3tE4-Zkq(xk^Zf9q&(Ht;{lED6^Z)VLPoMp_XREW{es=%< z&)xsy_lx`F{V%-ti}yZr@AZ45d%yYgAD{m4>F+s}Pu~U3|9|=JgKN+K*zxi0p9UnW zD?lrJA8;A|@uT91Jo>`JUp)MQ!`BYk!!JAd-w*!m!S6nh4!&~#p94c){JiLaoAtoO zWM}2R5y0jQQ4HGThHM|WH@+evIN?am?9w4w^1(u2GCw&~Y6&5u7KN-=fwSX>4c>RT z-c~IpVI1hturC_>%A2#%;i>(S!ZEEBs#fVjpuC;{kKV9A;^{78g3a( z)Z&TSkLf$VQtRML9_;gRCw3ul#EN=|LItWbA9n$2;BEqN^Tw2o2{y=9y+hX$T$ovG zGKBSk&{KM(7R>e5R>O0Dz>pw#yO<4$E;;Y&Hh1uHEip<9WML`zx*e=`(z4mqQpTHu z4`w;nkzk`RQ`w0VbJyffi&}z>7DJFxF=>E|5MJ43Anr7atm#a#U}7=rGmJ2DNb>#<)e>~!Rl!g!<}Hko zQ$+EUIoU23qEXd{A%z40o!{iC&xHmz13Alwk3HH!$jp5$h?Lp2x5Xi;RU zYRX#@EVOb%7kBH!JPYsIxFJM^Gu~Nkth2=iTDRJ+Ize|@Eg2mqSI!zD#zBuf{pDIm zi{RFBd#s?LowkMsrLlGx$wWVg$1(wSeo>R*P?V%u@3hXTVVfHf)xe!yZ%HlSKt_2x zL(r5d30MSQScC877QP-h6Io3Le`+4+H;W8ByG6^uGRsKXT>6H zO~*allO$tm1In-|(uL(P(n>nPaS-AtG=m*mocy+0q7iF~xe}PrPNG&M3|hcYJ+ogK z2mi~ZgtAaEZAKL=D)!XrXps+DEJS)P=L;;1;y7=~)UJj1(8E7`DKXUY0BtwSZai{& z*@o>BHc!q8aEispM(M2m7SeBKKA#-cHy=z{nsNt~TPBmHlL1mGY?q_Qb}+yfW=kCn z10Ii+YMpna%X`{qK$yH;_b#*HFmP0u?`B%qbC$~pY4>|}tC{9B-Nj>(8s8m-`Qe>^UF*obVjNi=kjf{vrumrE?4ed{%qbgK z<*eiyq(-$yZe<7b-TJ7_WR(m8VucC>y9KIlS;|P)wK8jXVIDSCmAvo{f2o#8HaOleERX>RZjYxn zG!#qAn5?>!t=wsfmE1uxw?makzqtFqT}lKEw!wl-ju_|0Bgkvd=X@EEJ4wu~YDg?{ zJa0z`-(H}Hf8*l($iyDLZZ7)pRuz}+h+Va-QLiyvqC0*Z!HQtd#Kv;GrFOPje+-z& zoVeJ4tcJ9gf~+k$<+up)jkJxRIR}kdRcyxuHikW9zrOfjLQ`*vxNo@>E0ZuA&Ahhb zxf-}&PP}=Nt)&4s2Dyn_G(Y@;x*3b+ay%X4>)v*YFUN?$EossxH-D^*f@}AFcSDLi%R2kzyi=t(p=S&uLGf?|!n@;i~Ycp%GiVPu~z*jxf!4h=>jr5=0i1#EVS`8Jdgr93CxaYJk(AReE@EIpp@rUhFu^<@$Ww zR@Gb}5P!#NJfu;4)}>qPL|{8DF6rZY^Lof_rMC+>8K5jLS83R1ykS!4vtc=I(%!mS zEQdqCnH7n`p=NEAEjitWg>rT_Z6+Dj$J1@5kFr)rDqCjHRiSK=^o`DRLEkNFiMh(J zTT8&ev~zX~zsavkG~=s0jWJnW?Z9?`5||1a4I9Ye<#5dUZKk^^LBeaf9k(r2A5+$Z z=#~k+W4&v?CnT9dT1G zu#W3HRK_I5wUvVn9MFvsZ=h&{YRC>&a_V&KDmYA%U5?K%+;=b&d%a{$M$)>Tgz;#xz z+3BOrb~1xgCmcJ*?fM;x(PYCEOq!Tut=W2*kEnDijTTq~7wjboNUJvGuImis?m#{I z=30l=7~_1xx?>s%9I=z&+f9d$v2+Yl4&6ELL1=3!STPI68>%G~J+`+*md$i(MotIA zywmOOXl=+4uny~9qunf8kSK6F=%C&uGD@!v#eN^b7|3mAR9H+3LK-hYMmVF6S+|+v z&`3}liR~SJWj(FN-nK}qt?+Q;{)x*4*?Q#t`zVqUC}8M{_jrWj(L zD6$3T&wFEV{1CQg!`9g=e3lUru5-goS*-hy>Go$=iA?7 z?tSUS3;)FlA4CNFt;<(_{qFnk{F^&JcIOj!zVXfnPX6P`|8Vk$PwbP{=U(|QPx&t` zu0Q9$gw`kh{onu6?Y(c^`#;_oZ?ONqy}we&9PCf_zG3f`TmRwKPu%D@xc6f>D)ygv zp^E|J*9zukl|*#mz?fQHpY11jk zQ&H#-z%_FTfx{FM8`CH^slj?OfI2hUJF9b|c|eWegB0b2+qG!I6X^juHh4~e#vr$5 z*D$*CDH*fU80z6?^`2dTJIZvhV5XFh!-hAq*?|C_y0G0Wr-}|W1c07B(w5fGhtfOdiu1?BMv+|WXAD-SGPQugUzOdC(K4F70)Zx z#YVL{y)bAPh(x(1A6qcE7~f9N{dp~s8N=0(TWqok#H7n^+7LDsG0P0Km9>K5f+7?8Bj0_JShZaT_B5m(1dTT&Gco8}fdyGl8tr`Xs@pTwoii z6Oa5-+hTCl)@M_6EZevSo?@|aJn2Q7%xc66xmRB$@(T>ShhWx>!6AC>44+sCgmT>48q$m@7OV0^M zTX11PauqRj@IHa5#gOc72utf5d_wO1WGyjAM=m*P;d*Dwu7!Ni9E8ROJZ_b$tXBRS z^~Vzvi*$tWj%%;W&w>bQHrG8lK(m>|usxVY2cgxR>>8aRl7-m9lf+dL2DCJIyU{vn4ifxrJ)#1Cy6MJf&}MYpe8N)?ILGw-ZBQds^*~QtMCq zKnxF|A=xv5Yyj&42-TRhORF%JR$^!sw+-g44Av^Couv}H*)?{M zs_uQZ)}cz%YDLb-1&4(oAH4xZlgb=$yLMmL8uO-YX6c}9_MxG7`$uXCMZhQa%HMAd-nannXn0Yw1R>s9B%jFnQx;mwaYVw>{$Xma7 zDS>9LvSeH1qPgOiE<9O<(4yi(va<`ROba^YNVA=MMoI@iR!i`^B`ge`b{?e)IHvVx z_$npYEu%$U#D~c?stt-h-!{hi$-lgm;77qCZP=^Age^-ER3C3h6|or2~Or(#bny9P-K_Epy_Q`bHyEM$Vegme|WZWF> z&@FQ7r)wRUJc8PK;`KB_s6x!munsnxj28T+X-9K|<(2>j)1Pw$dbd6W^XS~)@LhpY zTY%+-^Si=a^;l|Huzcu{lU~@6$71aF8Vl>-Z`L~64GVJkZ5d4Dsk0k+!=~+T?B+@b zCkkunHqE(XkKA-*7bjm_OCT!{iinTtOdphmX{UvdmYj=g6=kR8F78Sg*$o&<^YRn< zV&}ud{;EgDi()hyG&yL|0@xuC$^(>#HNm(`TbnuzOa>O;GQO%-0gH6)DjJU<&nx796+LO--nwEX01`pQL9l_9SGbj znr+Ue;^l(%4o@y5xYAITy~%boHsUVCPXQtWcrUUU9rEXEH&OfIn3hFz)nn01003^+ zLtCYbwVJ+M&N|$HS>xc4gi}*}F-}RzuU5L%#@%>moz~Z`ToG(LjWAF)w!r~#2%iS@ zpAc)1otD;a3@E@pW5v)sqiy8wx7AhwbU#>N5G3vT(>Vx0;r(GR(wnQyGMLI!C&kV) znw^PQ4{o(`$57J@BBjC`meC5W?S|qv zBXthbC@nHL0New zh&C%)XnyjKFP0z|a1#l(`L2eSU8=CD3|oaVGEyufS@p~n1&0bErE>4#`?#2HsX%skCHsEfOfF zq!zkO#(B$EUIln)%|IzuH=a{8(i^N(^fzQ?PxQzT7xd(ZhIJDklS=}>DJ|DcikyzEApDX)shE*Bc{(~ z;D|0+A>xnF1b)k!%_8RN65qqMU!;~Qy8aFzjLo#)|v zMw=b_tiCqq`{3>ki6%Hll`9fnE=Xi%cUSaytI&vynIg-giD9~I54vB+N^l`7j^~L? zElhwpHe{1zqL7O%Iu=uIPC1&~Miu}himm$e{XcinSKKl%RZS1a!P=hYrUg$Zfo`nF z?XVf}yM~TII&C^FX{6!zKk?%I|3C2?ffwdIsRu45`-}Jgx99!;FW0@x-u(UlPuCK^ z;otx3JD(Tt|1aMEU%danc>ixj3vGZ;twoD|@&5nf{r|=L|BLtk-yrY*mvIa)-v3{` z|3BsZ{~Z7CVBfoS`hVa4+u+xWpBFvwcJ#o<{EuVD0w7P@-1!`=&RfSfxgn7XhCEKB z0w{Ci=M-2@LF(1@LEhQq@bLZphD6wAGCPxnTI z0gKjS8k*RfGQ?idz^8b-?=007u^e$`guuDAlk63^G#;S>hSUa8npuC zcdH~oHy4nWm?t)50<>QT%7-{jC{bG{d{$$SJ|2LX1}ez|FP^P8nUi1i$1<^IOFzli zUx%U)bn#D)-D_+l2SzuEbw4rMh-t)5#D_0>90aP4U#Cpn_=~ecfWRS&n7;%$T{!C z55IfnIn(vIVGkGdMblqF$|NrP~uJHsZ0_w`7N8q9eZ&jzfbjS}-9J(O@^*#;9$qq?>w5 z(Dc_{`+eW^@$R{&{Dbd##8W z{+3+;4N^SENrc5%z$9$y6tU20*{VNVcxE(eYW+Upt{8Dq&5BS9cAlcpfyJsJX-a^U z4CF*LtI2%k&H+2+Q}b!3BC8bPUa`jON-HY25*I#=e zy?A*vP(@>K5cIy}R|?=IQ$W0^4SUv>C)+lSfsnOD0RTRTUY?x3^FqRG(jXiwtkje#@WfVgq2YEGMDQo^;45T0~u9eRkk!?uPGAS<60yP2D@UDQjrIfyr6 z+bAH-G3j*+@(cCuKe&+4R~EajqOgrE8G0gjJL#0@aSTq9Eemi-h!lp7;6Y1BJoWBR zT}p^^hQn39*+inY!I9N=&AXKz-T9$fqSJRRUpALQpOK8^ATUkQ-U_ATG1HE#mexi< zZEBR8o^+>iAz?H}PFN1IjP4|h)i{!Ao`84Zus?A5X1|g=k4joXxzc3%PMxb{wm7Sc z#G`iBF6Y!3?RB?{&4^ccCUyLQCSmOX4?4<9>E!M|I;)#*RTP!c{KD-39wDI%*tvNoj%(Y=1Z&Nwes?oyKywQj zfn~}`b<4F@XBqdm`4muhAcMH?6b9yP4P@mRu!%&fYFlME8&Mp6iD+Xgsb2*XWF*UD zo74w8Xco^B6bV}DhT^EPs0JQNBD~l3j>1}p+@M3x-K;IH+sq4qnTO&aPH*OqJ1c)U zkW+L#&L@+iD7k|lt|dg&#%0)AmImdg*>DLU>c;61uh0w-KXq((nBq>~Fv($lR0oxs z^ma>TMAeW7JC$k_3#d&_@MPH*f-b*JVyVfuMmRNF;l{x?)jFsuF?^7gw3G#IPS_NP z{7Sr~xZ*8RBrI{8G82>)tp@KM)ES~C0pRQS6+iSM2A49p$|TtUNtLa^B+e=T@D{T& zOnKU(M<3Sf10cGQ1v{NVwv`*YZUf@ppb8?65gJ&J^h`mYMt$0qR+M$5*H%HPUd$-B zNs`>Gla0c-v+J5N)Qpz1zOxo_np}B|*3YWs=wDq*v{0J0Q@156ahTYn7#9bkYn5&S zW^4wK82Dkeav=qT(+)2rjLo9&rf31O<>6V3f`Hzf&D3HU_t-?)VY43PTb+2gRT<;J zt|f9C8e1JeFQ|;LC{!yDB)m|!W*NqPYtSBanth@KU`l|vZ2t!@B}lfBO-ABmZa-$A?nPCX{?4Ag?0G*YaO#3 zod6Co6@&zfE!gI07jw+Of)}$ac610}E6PYnL}G#L*D>(Mbk0p$g*1(J#KbY1Tw}gz zNcp^CmHy7}MA`ykkg{ZrZgNoP>=;u<^z=dE$zyc_ZlxtrgXX2Dv${ zxS=wnFChsHJ&5sei4J$&o?L;vTRM-y*0l?7)`AcUm@_IzNacbgXXNO9-3~1WaBAzk zO<>bzXbcg-?b5zzIK4?}0A9*$x*cp$0A!TmxBlQotgE3kwC1*rO%uwJRALn=%7h95 z|BnX`nbd%%xNA85jxGCnTB{IUj~G>5wFOPr%ACO#sliAU%3{;OTu4p6i2B=E2KNYh z|1VrhfSh2lR4fqFD}yXmd9)l4Q@|n{&eKh)E4*2fShk^{ZnCe|5(0qJj)W=fXd|l! za&H?OX`-~(-c&D=%+T_F0YcH|qc-jx)={Mf*Bg^nk&yD31o_OlQ9}JGI$aD}FlDvJ zeFN?poZa^X)H?c~YaLv3X)LEx9#Fug{;EqbekQ7c?-EdcDsV9?S3TI--d&MfLcz@Eu^WUMi> zg&EswG_gU@IAYU*wo(Ginld_fc)OMuh-_<7g~>E1=x&Di@vdra6PHv0{}SC*yaXq? zUH}lz!&~25O90e~Qcm*(Nec~ywIUnZKbgoD0a{Z zJt>;)rU3menDny@j}3R1rKnFOYZ2;n!&@J(CDsfS=IO=&JB4JpO04M=Zq5MRBN>@- zzfEojZI@0UWCIt6|Du-2yR=gg%^8}JRiL5J3{5vIBH6&dwBV)dri4sN4Nq-zN1v%B z0BL4AwFZvKVe?^k$^i7UMcGg1U1UN~E^9a~T;X_fPxzQS-#b2CQa zY@_uOVA*Cop+*@qAvbXYj|{2U|6{d|MPob(Te&3w1`T}+*dd!A_v?RPGKoxU}SN*vsuq_Dy4XEf!nnW<` zlgaTPttFV9X?lo5bR~V}ZvogR?~X>+hU}^l4uERSDNKyT)oLzT$8|P_sS?9-JM%_L zGHY}_2^QUnN@TP;q2myWLJDNo3}ju~*u~v{exbu`V1PfiLsV(g#mjL703vh_z<6X0 zdcETs+7Jf~-wFW05x@ISYpa$5CDGeWn;xtHEG9rYNj!LKq{|emdO5M-@}4ynJ<;t; zxBq&r1LyeM!$u4f0O(;9*O3YVzRGb;x!A}`qq6mQEJkg6Y25ySS_c`iaFKVGbRvY1 z8g~Kk&0yOd2Y@re%osA7qdVJfmH=Jjq)r&r6SC<}6f;^5!rkYL;C!)zT#*5k1)J*8)(-&6h ztGN-8WE;scq2Hg))#*A2tA?dhj0aE_@aZO-c7>=%Y?702sap;xY?mmTVVjW}QDTD) zgaOs0CqttJY}$=Ns%cprUl4c3NmWbegac9!Evxj#0M*n40HI#moyxiiyXF?5bsgZm zg%qVn*2#zJz6v}@N_$0swZRhUQOOJYFWX{mDNV0zXhM|#N2 zc|&Z|6R*|ABPpJv$MvB|ce`Sxz}7UzTv*dKT65;u16p8hf?ps-xrWN19c(3#in?FV zcB8Ks)RYXMmIPs~OqtvGwoxtck=*d@i5&!m$t z$q@J(yN@met}u#t)t!=T31Ms1A#Z2k+}pbL?E8iSwT_IH>d@CU|y0={%y_w_pw_qea}n1v!6fv=Cd!m{{#2Md;jd-^z{Ec{g%_O zxcletegNeCnZTJ8Oor!vkj6 zm$xM`a8-q24QAY**#lp>!uD}Vw-L|4_1A-Q{+@Fd4|vBFW3uh7X{*hStX94mS;5ln zspDOW(|#A6#yK2p@mn_c^4#g^QsO3py91~!RlZrjxrWD?6`}{NOMN|+#s{X-F&ZQs zh+V^ME&AX94{{4V&T4zqFpnof5A|8yxN(af*XKNF0)6KU6%)KYb#YVX)fL1Kr>!Vl zZl(g~%uH4#npmaq(?P1xYr!XP+`i8g3Q2>)ZsW))sdT_73jf0d;E18tI$ks+1**Gw>dosoh}@=5&)wE7Y4Tx z`Yg3oJY7d_+#c|ho5TTl&YaGc!7N`q_|G3I6XD%N8i(jao=6~R99*mGLWecOj+;;H z$aYHr0oS$)0?TjQs4L362MwU?oazMt0U{bLqfKVDPI*eSmPV)o0C`4V7-M8N-vw($ zaz#MH4&AtAkG__956;u(&zx?Tu}z`DtbkzWP#!#`yDyKmaz-rt)pq=0jJ3$sVeTRioc4m`EK>WQbXY z8G7T+uXRKZz6$6#_n!r6isop$v>U$J^5^rO-LQOrxV3z^NU(D2yD}0L46C-R8*`Gr znLm3!(0A_7o&lrn#`-?*XS@gR1A5NaNYb6c*c^*^YZfs8iMKtOP)RhW00>luP`cqJ z4U;sNQ-El6V?9^a$a~L+39OL;$(mXp8E>arGD&r+6Keq`jYMZL-;nZf!{}X7K-xA) zHNLT`r%%hPG_$h@?>V;xH2ZC&n7#Ah-Iopi^{1GP9=r@}IiJ73M)a}|-UaksP?F`7 zh(W+TX@m>HI3H#61VbpM;{}vu6z)jXs4&aqK8=Jo=01IN{(dFUb3T9Hc6!-64}Qz# zaQ^z!%g!Er#knn@*>5AY?1L`{I?kueuMxHEgZ~WZJD)OdGqvo4-wgDePnox!TK3L^ zFFPM5;PQU`sbyymzVzG{(CoL5R`z#3_!2-tciy$1i%NT*JbT`QFTNbPx0|)re(;-s zrgOT}QOinpMl0_Q!^qgVFwMJehnaUb-XtF{S3^4;(4nv!K z%}7bA!>sI)Vn?@vrO{ua9ndGI8UBcU~Rw=Stl8;0qqF{!K%nZrp}P z_at`@rnmT8f9dG#$Irg~EIj-AlkEw8^5w_>^7zM&zwPXu_kZdBpS%Cy{x^bGnA?Z% zJou%9KX==_-MD{r?`Q6PAK(L!?!EW)-<|&C(T9)tqn8i=&EZcRe#c>S_`#z;w)e$H zf9IgJ|1-CL==Se>>5ty}O9u}?e86u#{nY8|w14^~cYhf$2YlkLb+>c(&YhpX^O-xp z`;KJlmF?~KiU7j{nz(pfCBjM_x{G-r}nZv^7u^EH)fxXLT9zS7`BJk3LiL< z_V)K*b|{iu5A>_0J|C!hboNKi{>a0^Y$gC`r`epbIFDu)1#x92ob3I=-Y;Ahwzp>N zGD+MIci>{budCse!go)92m~n{Jgi$>bAl)eo?!K8jyKczO7VTbkvM(jNV;=w^>u{- z(&9HhtQfp&0(houm@Er|_rk&CO2tP9^Z|X-!iNXLgW*jJ9~@8z)J+TT9}Er#58HR0 z%?IQG`LN>a2IcPFXZJpP)53T5{=wcqcxc6SbK~qg&%X2ELzkuPEY`n(w(ve61|e@fwBx#!a&r1-PXEkJ3m>2U>C-=b)55n;KXdw-n-)Gg{ejaT zxM|_T(?50kr*2yK;Pm@XzyGF%_fP-i>7RT!>8|U#)9*X|zMEEj=ky0pfAFS-PXU$d zpS}Hy&pbn~of^0AkM55iT5;VBJ-W}{XD&g$#zWeOEZ(4Z&?7Pms>*RZHHLhEE zpZouZHDBkO5AJ;E&W9crzHYsqoPF=v_ujPd@!9vBea~fK8{qNzDWe(!;NX@3{YZJm z9o;_p-jjzlUt9R-Fh>Ge3{&rl<1fLGGtNWw7hmFI=O$*;S{J`M{ zZd&-{@Rh??Zd&;G@M{mh_NIkzAAZf@*W9%5(cxDge)UZYA0B?y;aA& z!uyBsJAB{60laQd4&QtD-ph*Z4Z&_^TM#z{NHdiIU`@E$-05U3>3( zSaED><(L^!{90x6v8P|%lHWd157e6$J~~hil$#bl1Wf7Un-)GekPqaW7T!OQ4y1>j zaa{us!~^kR#S>3ZHdarQdaSu{W1GJ+=Z+6Pb?~X17QTJ(-3Jf9@%D8!A02$x!FS!X z=EH;UJowI+{`RdO`rHTqH^?6@8+h&Z=D+$qfX<+`Tx=0cya!J1VZD*`Tvp5cya!J!#a6! z{{Q@5NKAH{C|P}cVOQ-{R7AE1HWGUyy$@!J@A}+;P?F( zAOCH~jx_t0Nycv6W^qwIgIC;y z2T52E#J1s)4QbgLWYlD(F0UFxZ-%wPn8EYRbLn@#2|mxW8Tck>JD&?NJGbUh$gr8V z6Ou9*jpAKYc_U_aHKpfV;8zbGI8W^I91l+cq3!QF*Z(enGV`vv5Pg%RhP-Xfr27lW}D z5PwsQoHX1>k^V~MY$YX&0exggAB@+#J|#$&%o7}1g-wOf9LNvfGDc1Y+pLzwD+kFp zG(T3o3Xq`6k=l&`MbWk+w_*rpV{N5-ZQ3a#ap2>+@6YC@l}(02a#giDTR?dTc9XBH z6uLVelVz8<5uo5j(_ee-;A69o|JDoiQtuI*EEwx@a$t&A?vJk@yPK}s&f}~0?S|oe z1A6J(0*mzq4Q~rR>YMnIN4xC_0ia-kJc?NekUhG_Y|$opEf`6%J7_u)AQt10;T9RY z%bin9CNLoTQWfqJF!c&%;pgM?FBUmi*q7L!*Uyi0fnUA*`J4gg4kg{pmzR*94`E9? zK+6rB#+xH?Jz?(uD!dE|!yV{JUS>7iu^1_LNZMo)gF^d}L8xTYATdVDL<3#0NZDC9 z=&=3Py$rHVyURtt5{w`#*4whxcd3xzQzISQ_8QBLC|PVH#0TgZ9duHN9j`e;18SMl zPB1V4X=u1eR`{?pbPc&*E#gd4B|^FpZ0UK^UwiFi*~h=`+{t|6&Ld9d3A6S3Npw>u z(|+M(YMAF|M=8BnJs0ca9EA3FJnCeg5CDF~$q=LQM4qR%)ERpn&FgoSS3B0w<86W) z#fY@hOnf|R8InSHZ|-CsPn!F6ayD;b2=F=pLpuYToY%mze&}`1!S>#=*LlKX_|+pa`?f6A36B?{lC2b+xGtA-q!)~ zU;CfeKlz&u9Y=caGb9$ixA{C1@m~Mr7d^89&ox!;^-q4`GaK++Qv_fCJM z=RE|c&uqYR9)i0U20X*se!fY&uYdB+g#piyrTcu-QD6V$$uk@9Tys!g|K#yA8}M9n ze_#LP?F$2*A#wS655dth8}OWm;P9CZc+Nv`aACkRRQkM!VE>s7c+NwxcVWOYq?$k9 zRB->3w+;nC`h2?a6LP;lU*HLt=GPza{cr!KXH)g0>%5z&di~i{0a}~4n4$dox4%}8 zpb0CD2|4z(UKedFR+8%X{nZxW{k6Q%Ugq zN2;gPhg^M}bvMH?V^#5%lH$Zj09^l-65L11XQ|IO#PN(1seBY~jd4`?R}mw)kL;dO zAACjo%M04wDp}#!fJ$I9&*LR`r3Ckp;#ulromDhm1nz9Xv%9so@)wWQw|z=|tt+Kp z4lLgpi=3A_QC`rt6c$%Xa39$`OMPTn^2yRBMyfp+R=U!^`kc;vWc`%-t^z;4yiGU3 zmX`XYUv7M1G)W&HkNjEc+l+fF#i?cki;I^@k(2IY?aiK2-&N4Pm-%cbZK|LcPpW*l z(di}iSbeK!sgK|XNve{PGMHD3XtWK2$LdR;SYHdnu9SXx;;*zBc=PD(7#;89rP04q zg8S$fJ+VF*er&$j{T0vh1DjqDcoCPgUHVvkU-*>z9vk0qv2h4?7#TCeaY-_jjK}JG z=Tqu?Y&>FjO^uc#U-K%V5C$9far?gD$@R4!>%VM9Otgta#rhoJ741^uY9=iA(RVzh zKJ-d!UJeV!o39g(tcaB{S9RCFQiA*FOHZkCuaw|EdjBc)T_s?BnVf{lz_CJKU-IQ*LzBJ7>bv)p z`X2N5H>mIQDfK<(pEqkJ=cUcuq4lk$uvEHx%)Yx%sqeA%afABqJf*(J)<>X7V zWUjyV^tEbVg>T?Kdh(R|9$OzbsPFj6^#NYkH&VvFta1ZYhz)sLu*FyeOfHX)@9n45 z_t^UI%}wFN%RzsZ(qd02-N*djM~|LT-(%}zw8dqOP8rjYrQMu$+{fqh;Zy2+Y<$PA zv7V_uF-UUW9lEL(UFi|-qX$o^@3HaaMSBs^Zr;;qI^0fr)#Lto|5@t07T%ou=-#u` zcP$`0_t9HVsqeA*x7*H_D>4UhscT}z#A|nOwf{cyO;4^5dThQ7eM2T)HDHu1TlXumZs(DW`8soE(KN{b1AGvBAU>@erXk}9t3g~@%{|V+H3wP1Mft@ z6G;y@?JbpJ^QF{UZRl6L{>X7>ePsSB{)#X2cDISTfXHpLTrRPjg^XV`9fXWO_~6IC z{yduV+urv`H0KjugRXypxiZ7gqv+mbhPN^7@frTcu*5fgmH`v$(b#7`#Q_t~Qe6?t z5obmSoLf7|UgZw)|FQSp0d5p$|G0FgUFo{%rp7h~4Cc;SbtM$5_b#mpn6rBCT{Lq? zF*#8~z?2Z)gaqeJ0uGSGBmtbzOTcu~O=u3)bcayBvm+Vo)!j<#IE3H(%l&bm!#vMC z&&)IP%7N3lu_$6f(j^3iL_>oB^2I}HXgIY z)gF;yX*rQJyRfKF>K266SPX;{=Yko5pe2UqQN;+GXv(GDEt+F$xk>GH7sfoZY(F|Ij_l_a!DJwzM64MV-Zlquvh|xkT*W!S|zWIB5G(H@| zi+`lU)=oZ7T5c2<@cZ3o!NdLxDra&Z_^I~E;>c?$7 zK`I#b>zBrtTBJIGsH`^CETD?F&7M}<6grSl-2}?W?fNNmsW2L}N*qhBgECoEP; z)~I(R?Aa=)l29l})iwiO7A?~jc$grVX^6|q@?l=R>@bIlCN3}JRHppusH2K|{g!-E zXcwqMPF0V7t=;um{2Glw1mNQO2iX@gaK_)=f%=k#p`U+}kqZ}@A$*Zhr~ zwIkccHnFyVD1g;t>o^_`m%{;Z0c%(}mUcipu%K@}_{tyaTi!PxeDnA8b3vZ}@jKL-lWw08=8Ayp^@$Y0==LfAZB4b`@-#-@BG+$ zM8B97dx%p{xZp@qo)0 za+368oz^4WsXfx2(xTI4+%{{;Y>UX%S-jyYSxIt-3oW`@-t0AmO3{kN7K){#Me^6f zzD+&SJ=G)KlReTs(IegCExNAbysAgK(|eTrY>#x$^hoz~i!KpQ*yIsYwWzQ<@u~YVozbEz`+e1dCesL*-Lb07;w8V`@B2}YbXWCAcV&-sSG4H5 zj-L|J6>MQ|(4wr^;>Myy7qAudr1imB&?DU;J<=W2Bi#W#((T`()5je4M%pd6X;Zpb zS?MAB7evt`oxDdnS&wwfd!#$QN4oaB@9M8M!}juB>0a!S?u8!dp6`+Fxxsae+Ri)K zH}hXD{Yv4KOq+5f3wf0~ol@i-q8^%!1$Vkq(a57|wJC&Gf)OjZuFz1vN4i{(bj3EErs{BnBj8kI^D4Y* zZINVO&bvL*z0)Jz+da~~)g#@T3pjxF)+61kJ<`3>Bi+j_x~}Jq#XZs;+LqhRzhog@ zcR$(Q_q+P5*7HJlzgB9=ZIlZMn>HOU#H$Iv%M`Pa{Lt{{J<@&FBi%oHr2Di-x_|UY zx2;9jbzgP&NayL1&fB8vx?d`Y@BdcD^^CEH$F3erj~z2MH2U=D+R^H$dUT(WS4M6c znHsT;EEs-w__pEmhEE(`I`rw#eM4)8qC=t~WbpC9pAHrVW#Ae8#ewSwP8~1~9LRZt za|>rR$IUsM{So_4_7B(rHpXV452IJ3Y4jL$2zdti1yVya$iA#sSvRw$S$5XK{;eRJ z!1?_v`j77WwC}#YHGR=OQ6Iv5ocU8w|4+u8$9NGa2!AG~k6^7B=$qr+yo4O?!I_?z zKAehy9M8eYPD~$0#X$b3%85@*FJ`Ua%&8Ljiwnm;F?}eN1ab%)Cpa;^h>C$6qsH+~ zOfRHjAO|UNyc5$4sQN&TLgF|lroTfafgHv|FdRa`ASTC&aSRjF2U9V0SlTo(eGo+- z#2uDaEt@`&wSqa9;lyOa^>L1!m_C3?r;fRC+{E<$R16)a=1)w|r|Lw9VtSa0foy3) z!Vnb$dGfJunV24=U=Wfm&AxtOdVq?d!*IWvnC4J1kf#RwhKXr56+?&N@)Of26+?#| zzy^U(F?7tO!o)O-!qUQysS%x+?x&K_VTbUfF_(^;m^y`up~G;ZiK&yR7&;8Mcw(wS#n556 z`4dxhDuxa_uqLKz6byW_9njAvrm9p7WILemPE1v(7|6ql{(WMqOvOMp9J*ysp*>LC+6H{3#2J#X`@1B^- zP%v~{JV7^1Or@z9IxKy|#8irkp~KQ^C#I5A3>}vK$;4EGilM{OS58dDsTeveeZ|C7 zjEbSd(w9t3MX4A%Ed7ItsR$KAho#S-m zF?Ax9hz`qHCZgd=wHoZY!XG!Ar%^VL9Ezl!uC;!*WXE`Jctymoa=c zXFt}D!5{zq0V8k@(o_(UU(NlPi)kd4TG%xtoD}&rXQ;Yhw#Y9UiG7H0K{`S9ke)`` zoY^zB(@6d_TjbX)SwuH2 z$*Nph$VP1TT+JSg7D^RAz7+WlkR#>T7!R#AqhHCDm~r$3>oT5>#9Kq|!yR+GyW zT^h+~;@W7B*-446gYcxYOHDbk5NFEF#WZHu?puib|4%MNI-X*dNc69AhZl*Jv10({ zH?#o1QUF9(&z8Jg0Mz<71{O+r0wTV1*6S-JZK9Y@6)F}@yd()(8U=eHA<26S(WM!C z(qGFf9BO&Ntq2@7}%t&6J?Ewv;P2`%nPN)vp$ zBoi0c%?_g^V)I4SHKSOgb1rRU6#P|b>^ zE4ELOuNDZnct^)y5e<;sM>?DigbV+nEx0AT7>nnKJ($>#2}MHjA}C!{Z?E}9{!BJA z&f{WIaLtN0iAgZ9i;VNRB7(HFP$$cU6lfp=u2Al{qKLTtE-Pw|kG$Lte+8P_)dRLz zC?GD7&A#$@>SURxm&ioEQqqSPbuoSp*DVbv%9ur=t>%3SDK1sJEUMy3Fq&*(6iLQf{M?C)@&A6E{cHNkH^rO+MMinD(fEoaj z;TMO`9v&ZheCXt%MT7SZ<_7m0xNRUjFwD7$AN(#fX%(nRO0p zqW_8hQ~D3>yT32rH=lVsGr}AJ4gVW{@Mek?-ru-5ukp&|c*c;H2F3Q0tS(OkqH2XO z?yFSGDMd}r1DDKjz1J;p%0MX!o=~(l!bH6HU-{Jc)EjxIX!G6^fyieQmmDp7=EF0b zm+t!#7jGtE!)1>k5li#<0h5xSHybr>jZV&sIb)Odbjp=C0P~r>U;2FipV?13aP9uLoR|LH_?dV!0ULJVI3_NOlxj^_j~j$M zTv$z}{JM}vE&msI*5gSyb6>*ybv~^Un0%(Q}>GSKL=E{^3s!KJaJH9DUv% zpAPk_K3~2{fH&i?VRccWjHcwigjXOmX02MUHx%d&X-P1-{U_gasLZ}OE)K{rnyWRPKN-UjMxPq{`#B{pP6S#;&^b)IIlk z<;aVca9+IHVEq9YiYRPY5yd?5Xf7$?#;a*zI;GMj?0NoV$taB4#yz^K1dMJ&Z1!r6 zx|YU+Bku1HhCa})yYlj%&0GD*FHe7a*_wI(C~!XrYDYLQU zw%E37e6j8DFFv~U>Lq6!e2~g})#ulqvFYSJPP=5!qp$dx;E%Yp4{wHAV;oF+obpUf zf%%OQk4NF=O48Z7A<3&Z@&d8U0uT&_)iyI z`|Qzgyu5C7>asum`PS#&yZ~Cwqmg+e^13h0HQR9crZ)^u7$ zNQqT|*hahI+mZ7wI{2-d9=rC$Rm$tv|6-3D>lN?nOa7!i=m^D=TYsB89&ZLH`AMVOmMJuJ867`|8D(;vsao=Q__B&f53aVTeT`sCin;&`TSH5M!qpy3svFH0|4;^g1{;n+_1?&s(=1SOb zUY;1Qs-zWjgo|mFE|13%DeIJCVc1ZLOiDsYr%P+GSWSMlz1q@v?ffq`ShB*;g$D`u zTXDoY3y%9o?U$2Z-V-v(9+G@;z&;1z%}LmBj+;>gGbVE}RTlE|0%y<{3AuDSetJ?N z%WE_$mDXaiMuc@?Rn=5rciBib$1>-AId!7Z@z~^^EA*)o zuSFifnOL!Qz#snlCg+7Un;w4Xp7W-XSFtYt&F7a~xayO$@#YDz;bKLY_k=VGt5oN2sNA)r zs;H{iD|QbikGnlKSE1s^l6G*HFVLP9H-3>=@yOKij~?)#>Ngh}ShpVd$fPykE$w~% z$G^R3zw`cQ9p3c8hWRy9)u9qbOS+)YU<~q#Wn(a&ipX7#T27{miFuZ+NhZRSpl)Sr zC|=x)t2M8@`5ftz--~Yi+jHM{|JinQoU!`kO>YeM|61|%V|ddG8| z&_nKfQu3XJM*@HBfepK5!pVrtXE(d@T(fqPmrkb2lNM9h?=!d*n6%+EPpWfHUC9z_ zPdCr#>8GGJq5fvU=^tOA%Y63oA@PeZ8Ja8_U%cyQyz%eufj8Z-;dHj@PkNkWRL#rdzurE;xGU3 z#%C7j-+poUrRI${{u*!Mu;ExKmvM3ZE?!9NuX}_hPdphG^E14xp`7Day{587Y0y-h zQIAK^YWVDDq7L;XPrvZYeWw{uI`WWr>mOh9%U`^R4zJ&Q{{bIvUU3@UbhR3`@@sCJ zBPns)-L{yzkdcHu7Lmc16h(^eP&yQ;t8I2+u&R)@=kb}JzvpFL`(6l_?~(lRJ?`)Q z6%9%CRV9J|xsfy!zPr~aE)B?45 z$!UvN@w&7<6ze~$InO-h@hg6P?IRZy@Woccafj758JZ{Dz0@K5;E4yG$D0n=u#ekN zRSlDVZYWsvdORUtRT*!@(WT>S2>Ebac%(_b4tXXDx^-n6$g=8sz>T!UO_GL8Cosy|@ z9-J?Vi(LMaDqQut+GBkF+3PXs{zv(|{5J;9aSHo?q`HI|U$NrTnV%hSvu2&&gf}g) zVO$>zq!j`uUsi6!LXnU;8FBJea*4~@7+1=x`n1dFwsZBVWTCz1IoF)9W@4}BhHg6R zspmzS$DUh$iuJQEh7TIJT>hj+9=`Z?ylI9FS5-c%&sa%?#$!HSED(@~#)Gk#wi1l- zt66Ei5{uXc>8jq~;G zEy?epExr_e=k(1#S%B8=D?Af>V(+o%E(qXFBWyS;p5#jd(zH3~PwN%goY3voWYYmn zqmCB@8jm-U9rvkxsYxu?ZuqCSfAZ1o5#ilGUwqV&uZGY5N#yQ_4@+(iZ#wOQ%lEka z^d~RIn+DjhT4nIzN`a#?iD%t8k4BWi<5HW^8cqbtD!aC<_lQ*%fiPqVwzsCe<(E5; z+w=C1V!j{V{YE+PQQyN2(K~zW@y=nV-TA%qS4bbkn|j!=*e))Kjac1NR>}%uj8~lW zhfKo2cwUq#$ZC~Zt)X%jZANJpOSXK)+lRdU)~$t;HgCA$*2+yE|A>F-7r*?)oBapA zcMrDLZ~8v}sTXhRV8b-3gtZ#($By5y-x%-g)fWWTocZze+odyypM3P>Bae-RUgY0$ z{JdN6rWQ6#qxMy+;SX-RzWAc_vSapLQCxJX;stcYh4=jM%on$w^whe$ZbY&Et$0%d z8>Ugwsnzh4cm4c`)%%>ka#Xr>=+}QZc5|RHweYGS`vIHn+Usvt9C|L^RJX=BqR?PM zix>CS#ImFe6WIioaVJk`<;ijlSrO#pG1aB2R4Hz2`{TE_p8eDh*0`D{_bnIxeRT96 z$KG=M$|D~f62A2T??YYs+)=!#f(_Fs|I^a=xN5C*uI*~=MQfDL-D2;v{_--*cTWAW zI&$&4o3`EY{kLAln@ZR)jbcTuhQsIOr~X+$)?9Pz1HXLiu(L0DV9CrM&VB1>hWa<7 znyZk|pYf&wHcaEwZmZz~u05}Qd*#0U)*;W_GqFJU{bxeM%oDX4DQ*w`&+EsZ{v6(v z!-i>ms%ka-bMBWvS@!+$jlX;E$2VO0qqylT-$9#SDgE)bKOC{wf|qKq9)dSzuwfeS z;#&>B=fi$~>H9zU_2nt%^?Tj?o#XewZ}vQBKclg@|BE|*dfjh77$lzm`wn8T`ucy< zzp{U<@3y}5*bQSRjg5`oHkuwiXyk#B#>kPwn}*LBmJGcx%&954w{4 zm*ATm0V`{TZLauu5jm!3q1Nq8`OP(#A)_o7N-0atMY`0(#HbqNk%L*f=PG87Q8q|^ zQMj;cvSQ{aWrH1E?wVbgIYQYWIn1eh`eWuWWrO5^w}oBv3p0l(8zje7_0ZrTRfD1q z4RbDsT9zqSU1d`$5Z7gtcJj30Qa0G3p`&23<&-A1U1hMFRK{w+NN#X~vO#iW*uw4& zE~9La95~iPgX3n_2{z^&Z@@dIp*iM`TAbd7%4)C5YIa-2LF)0|RQh$q=`ReJ9Uh0% z9|9pJx`H=nYLJuAGM7>|NRB}4p~0gm8|-im-8DRlvcV45-~{g_{N9uzX|do9wGzA> zBMl*Q31x#FHtF8rk(3Q~xQ6Zx9zoe)$2tjE9JNNNq$~wfnSwiG%vQ)14%;|&!&*jMt9!A+<$2#fW;9{zE(lLa^U|n7G1YJ6P+z4J&x=mh^f_qWv z*Kx~XVF5TTTwVYr1hh)IC8hF_)3GoQrEIWcohXv#iY*h6<*d43*-(!v$$0^oizplH zSSRK{R;|X3Ila~yF1rkh1i73Db0KAe9qXifg9|7d>{urtVT{S(ReJRHl*5p)ncW>4 zeuuKbosGUQ&jV8f6f?egbLhDmk)-`%pI6F|E6ndSOx?L--xjx@)Z$CZ)gT?>w!! z+=ZEL#a@_uQcdfQ9?O+7hcOlk8Y1RuP2p5H$i;^Cpc=uBJ3I@!7LQ?4H`p<)yH<~3 z&Lh768yI&o#vUA7GZq@-j(#?J@8~6?D@TtW**0?L$b};(jw~7eaCpOTbJ#Py7*zfH z)6nW6=g@+|w+7b_o;he8JZNCcz|8}zz}^4(oL4yOIHz#5oV`J2zF)CxY$bah`W(6z zEuqWNVUX|dS|o=YgK$`nv#w#KSP~WsWEs4wKh`hkXY@S??)``QxXjN$9>PnQE15?# zwgI(z`oYc{>JRrVRtxxsyv`OFmuVbjvpyRz>#bmO!3}mzB9{vUu#`1XbZoUq00PL*09ky)H#nI{-ec-+3AJQ#DRB)q6@+%Hjrw_)f$*m(m4kI|@1 zmenQ8Bv9E~o&xf42p-(%D)TI*hOiRW={*9U zx*#um>bjU_JZrZpbwy8IIF9A4xJsiVHeKWSM0#D_?Xs42 z>15hekxbSUS$@4z4q~EUDVz;Dqk2I;N$~U$JV8w3C~E2&LsV7L1Gb@6)0No2~ zo=)&2e2uz3=hN4tjhsni;#zECo+FpEDJF$BpG`v~^ymmBtb1s+@3TuG*jc55O6J~io-jiR}doz%;)TqqXEO2cxu z7ahf#b%IBgk5H!>Z(QRLPxM>jo`t} zrI1Lk@y0WjRM`*-aiz+bHB!$SvUsFnayd2S@kU54Y?xg|bQo(^37%NSR!`>*o^lGy zW5#-Jbnyg6fmSgok|Z@I zbO37>2_7>zhk+!1wWMQQ;&nUaB2UpL!YcY?rY;TeB2|~qqxPBYQCUuha3PQ%aunVONx?s%2CzwygFf$XRx_YHrC7&JXVKHV=}vg=BmFG z<%?3;q(PMw#j&&j6A7)7NEt++0mqcdgn}rFHFE@yUMChuL|mTBNBA% zteGZw(jX=$qObdujZi&NbenAB^+{t?rZCmKMUyHgPbRU1#ctABeFC%}Yo-VuQ7Kx~ z32H%5tV-w)Cl#WGwVVK-t*fARP&jQDdddkGRw~xP=P0xfYbFVvibxWwxWqh(AYrf? zeT7V-qUG~-PG5*u@?;9eq%gqs)$FlJZ4zZ-%>==GfME#h$%xp$C?p>heo&<@)_0)6Ff8`!H|Dq%@DyuBghN+6l(^doZjQNkbhv! z0PIiip;^c_tm!9sXhdBhpJ2_E1P_gXD&%9VIZ5!)h?PP+k2(lS#>Ifbh(I3c*SW`>z&-T z2J#%%R1-Wj!ZDC%v8IaPp%H0;JcBirus^*AQ6NuaO$EV2BR&Gzgf-;^4~@_V;H6TwC-~YAD3S;a6@cbVfx3GW9Ud1+n zN&v5->(J9tBYGgR1-S`Xg&2_eAoJfk)~PHlYoGqj{lD&S^sD;!?0dfNfBVXP%Dz3A zFED?>tbn|Kql~8+Ki~cWfcfzDzy2>EkF|m|$Cq^hMt;xa-}E@6e^9_$fy~K5es{)9 zpZ~^%jQ$ZyIbsn+)er5rL7 zRBaoj9P)Z=%lVX24w(tc`GjIw^Zlfc1q8<-Jk|-w9DbnV8%|3vH{v1LEPSJUO zM>+d;=RcvxQO)*>$w#yiO^H%mn4UMk$BvI@)qxrIbTvf^uG=av$Wy(-PyNavx+CLWq;9 zFA}oDY4_!2s=jnE!M^;BQVvz_j&Lfm^$nK*p=TS;IWF{zQBbCn~uY{Hu z2bIquvk*e;RDJ2leBSQM!&H6gV1j-5E2SK=`)JE~2t5BY!>mn=v29~p$F_`Z9@{jw zaqQl)4P)!a){U(ly9T@!xDZtSTQyc2%ZdUY9JzO7!^rxPb)YKXH6v?448m$q9k4c%8;Omq z1XTj9BifPWBN9+8aLLGmk@+L@MmQsk;cdfPhqr+0ft!Xm4snN;3@sR%KQwQMGsGC& zHn??g%i!k0O@kW;?;YF#;uh8ot{uE)aLwR_pvK^;!5YYJ5CgRaJs_GvJGdOw9OMoz z8C)VgoA& zJOfrx5pnr|WPm%cWMIL-{DFA`oB;-B8)qwL3uiND6R4(mFJ}XIN3xEymU9hf4d+5o zDPa|-#>sJFoRy%ap%uhVEayl-jm0IL1)TYuc^nRh!QRH+3f`w|W^ZC|WZ%o)0BRnt z1Nj%OVXt9d$X*TJveei)P<3%7+rzf9wV?W;AbThgM z-H6_cZa~+AH!^EM9mX~2h2Xx%Do~FxhsMyAs0Y+#)PhJ23Caca85f}Q(RnBbWgy#- zt>E3wW)Q!z5xE!HfUE~~8`mP&AZw5dLH)*6pqfJti6JWy4`M~M$Z}B6k&7%r79jJH zc?bt#u(q+bvbKN>5u1kZ1#u+nhu00S9lmCG4XE@bY2F zFn4&#@Pgs_!}EqY!;GPAAm(Jt(B`2{LmP+g9ojIoerVm$+M#Pe4T=jvB+9Cx+E8vN zHneicGh`jo4lN&&ur{*p1r;jSgLh18L7d4N)`hIqtW~TUs8|_etz>yvR`BL&Ify>t zvX-zGu;#Pou{bOSsCc=xe+zgYwF$(e+}ppQe|`VD{5= zpvI-OU)#U@>$gzMHDCYz-@m>sBf#$O>q8$xUxD~n^ks+-qJM+<0QwTd`_au1??YdN zcrW?_#Cy=^A>NHX2k|cSS%`O{&p^BbeH!9l&`l8kj6MZ%1NvkepMZEf`gj{3gLoVI zXd54acq_WGjSoZo6Z%ja{|fPs=!0#10OB9e``dUQ#9PpN+jtMe_2}JgybI#*(L39C z2gKi@e}VX0^v@7)MmIpb3B4WSjp%IqBlVN33@%m|3QBZ@f!43 z5PyvR65`e9|3dr``U{9xp=%*tiCzcs3iRg?FGqg{aSeJc#LLj1LcA3H3B(_w{{!(7 z^csjiKz|JJV)SZ=7ok6bcp-Wf#0$_XA)b$30r5Qa@;0u4*hDXDyz(5{TbN ze*p1Z^x`&N1aUQbVH+=icn*4g8$kpZn7?PEO^9cqGZ4>2zYlR5Js08>x*Fmc=s6Hq zp=U!p9X$);Y3P{{PerF8o`Oz6JQ+O$Vgp?Tv5uY&v4)-ov5KAwv4Wlgv5cM!v4l1t z7STGy0$PKZN2?HXXa!;xEkn$pC5UOX2r-2gASTf~!~~jy7)P@ZV`v6q6iq{npecx9 zGzl?;CLjjUIK%)NgXl-25LcoRh?8g-;z?)-;tDhf@kBHL@dVTl(TA>t=tUV{}VaflYw1<{N;A(~JJL?dd4Xh3Zc^{5r1 z4z)nkqGpI1)C5tD8X>As14JdNhp0ex5ap;Aq72nQT#l+C9*?RZ9)~I+9*Zg<9)rpu zeh-yF{4Tm2q7*$Iq69q-q8L3Eq6j?(q7eNaL;?C;hx=^^g9seqlZA;4?P&-zUV;^ z_dySYxHozL#J$k{31qX``$0t6`$9z6`#@x|_lDTd-V0(Mdryc=_8t%!;ABRu?=RV7 z5WirLLj0UP0`W8UFvNG+LlEC#4?=vKJpl17aIPcx-vk;E_y!w=_;)q}aSNLT@pX1T z#Mjt;5MO08A-=+9Kztc!K=A(!{Q}}k=;shOqn|;15&b8`7tl{3K9Bwb;&bRWh|i** zKzs)M7~<3DM-VrmA3}Tz{Q%;V==%_#K;LWQR)~+I@3!$Bh>xLfxA85AkAfZ&`aKGI zNZ=#r-yv>9x3m%T5d8bY=xc3!72=-ge29CX&_3WC3+;oBq0m0)C<^U^j-b#!=r9WH zgASq4KIkCYvd;k8vJVGs*@um`?1Q2$`ygn`J}k6lpMJDupFXr@A12ze4+DkvLB2$x zeUL9uXdmQr6xs**42AYV{)s~SAfKYpKFB{1XdiF_hxS1}L7;t*j}d4eCpnZ_{5ojOeJp|eZoZq2+karPiALJbb+6Q?Xf%ZY(LZE#>E;nc&_MKFG@mv=8z(1lk9A34!(j&koQ&$cqTH5Ap)? z3Sl4QdE{k?&mn(<_$=}g#AlGr5T8a~gt!TL0pe50^AMjzo`d)V@+`#1k!K)2hCB`N zQDhUuN06r=ZbY7h_%QMW#D|c_A^sJ44B~^xqYxiJ9)Wm2vJv8a$iootMIM595As)t zcOwr%ybE~%;+@F-5br?lgZLNZUWk82?t!=gxf|l`$XyU`L+*rlD{=?KKOujC_($Z= z5dVN|fOrdXJH++KZ4iHt+zRn`$e$qo7WpH@n~^_2ya~Al;*H39h`&L84{;sxJBT+R zzlC@`ax=tVBR943|KG?M*=yi(QFdi4uvTMId1-NE}@*#U^)5bt>*XAcjZ| zN++-d($ff@nY}gv9R+8}#rjo7@{W|~UB{Kszk5vr@~cCFYsZ=dUnxMOW6hNVsdy{D z`1Tyd|K617Qh|^^XDNb+FRDw#TtQbnTPuQemI0?xxC|2%qW+ju7Xp_z{ANrO536c< z*DhyDjafn>xr*zPC@PL{*kG1;aCcmnGUtp|dBtgQ1+D3lB<9Gg4R*dssR=1vs)pON z%)SiV=fN$>iYKY6>H;OMQc(r@75%}IC?S{il%p7Sy_FJOy*(v5jmfn8)}45^bw?vW zZ1=1qx(kt%(>Rx{h6|G^;5t(Osg=|{C3?qN{BKHh;B~k=CUE}urbL(Wg@QTVO+18q zWA?Dhfm`fSJg@h9HK}|&8)yg>Azs8*@WnMMN3s$K)vLSgZa_*BkG{wcc(qF0Q{(A9 z@q9R}H5HcS0|Hra+0tbmZy;lGSu<{jDdN-$xdMMN}vuggk{PK46y61l<2DMDbZ=Hx!vb(ME|P0A$C!i<9o+*0v&%f%QifF zO7w4N3;tJ9qKgUd`|nDL-r=u+tX)irF6Bw3#8G0_l<2~`Qf1ai{9Mc@Z`dS4zguHi z8gN_X37f#@sEa`b0hi2_mlO?>U0!*zMzS0ddw5B$11z?RT4!(6bZM<7t+C1TSO$-% z^1;wj8Ccy8heRCEq^lBsI#~1ZMR-E20!8IZc|+VFh&dzmhSKb4L~_eSa#>F+kLWsB zd1u9&N@C@mJISPTOx=AePuRKgW?4(Ktd1Z!t(U^YOxv+$&o{I>pvX$NP>g4ZLK}+& z@B~X}lr0edJx4YpZpY+373jbHR7;fM=~5?v5VOmZoyQ|85zIQ-#R4B@b14gWI_H+Ssihce8JvHTb^dgbVmBUg-I!w(D}1HQIKK&HR*2bT<7J}}6+n!{t? z!FHl=fbYel$W@4hbqDJt@Xd6(@58>;eT$ekF&8ubz%a*WPDA@_{E;y7=|V@uTk^YN z3U#(R%3jo4x|t>=cArGwd1n{o!gMWp0&0;KkQi;wLQsEOmr?< z7G|nInedfpUD1H++PeAQ`w+D70VV&-<3$XTsk*3 zgEN)3Rp0M<2-;Sqakn!tQwCBF16F;n=dP8qxpD*n-&g9z? z-|o3rvq+?I^EEn?0}=@@{#MU@+`)^}xGx)>$+jiE*>m4^kVNB_ZE7Y1BoPk!jh_2N zcF;5~=%!}UZE=6^xnmvTdcWHnpGg66gs0xpbMJQY)HLn{$7hmlS+DopwVhEKMX zE#>8&``ng7J%V2TC_bAc7}V$Z^;rzkl}o6cqP;7owf z26X*}p0#OD9vauvBQt&=WgJL(zWWn@d-9Y()rDdSJlHL+7iv6#GVDs>)(qEOHk1oG zNy9cCvh!TIv_u+k=?qDMmmik!Y+!U(wnz85o+VL^ZbyN6I#=woGm}6YV$nR?vo^CV z8X6b$qcbM~iNvCLre}#eEE*c$M&mOpfFxqkJl(UTofZv^??J$3rC^vZb;2{GU&V!tz*k^<2API6l>m`<;zuEUn zpMZHL<8Oe0_&H-?bHy0AKX9at!5H8$5o862O(!KwdwH}UX1aTPHZy(R{ca!e+jrGm zkJjWzu`>>7o=7Np=#EO#$N|$`ea$=G=T&;?6pD9mkfAHxW9@-t1Y&Lz=cR#z1=<2Z+s{ zPCBf%imd~8_l=a$?HIV59xI`tp_Qy5jmN^BHSGE(5o!qi=P_%0*6`@2g|PDuJ3T@t z*H>G`&U1D52Og-Y;2=V4(coPAk{`d$Sll!d`tGoEvgqU)YwNqi&e`>O8~RPmPUy*V z^{nUNO%tK#PJWZdlWbeho&083O`$g$PY{|ei}$SQf~K)!rPIi$*4DJ`jk@XxSNf5k z5jq}qP0uzgP9F;ew`~G>RylhuNJ+vFitNcCRBH$Z}+>-_+X zd^Cg6RoP4_oGF3ms*@MBgcoD+91(0qcwvGf6ffpd{yOpUiuh-LCYu@OaWN^z z8GPC@uTsWBmVCLPFzs@@7D(pqi&#yCaL^-26@!kl(W46pg2J3MSR=&X76l zw28w!L7>qP#j65v6(YzJ`?NBVV`(s9!EHecZb&AoC0$V%lL`&;-ok>3uD5~{_1l9J zX`IP+-^#-}SKhx8ocIl`j$I5+?3}#cU~r;DhzaMK>@~hB6o&vfoa|tm`8Qv6qjBb*eH)|kkkR`#*7bB)el(ZYSF|MTRHb!h7UBZdyIyjRPXc4k@bsG~2MAErdw%KFjMnd%f*VL#ftPN4Y z;Wsrr(n?Wm*EYANr zvj=0|Y`R`Estt8zAZ@ove7a>yZ_zKW=A7|NQ6%$-TneqvEmm1``cks0603R)J0`kL ztZbyIC02HGKPH`=?X*|+oR?Uo|B01N;~vlKE1RxsARSOU-z5;U_&;!0U=~Hd=@e9~ zFN8yIzuF%EPi%l)7VN*W5QUg@&TD2C+ftw3#C1C)ws@Fdk6^wUZcT}t=^SRp!fKnz z1j?@!wN|Y~u*+L4ZV!txLZKKey?{^`2_*C>V@2wUMC!4n+Hg*!QW^a5YB*U*2bNa! zRiCf2Ozn_Xat2%+QI&PdfW}?XMbxU`vVtb9jp)+_dqLUTWf!9B?fCyij9VBWyWhZQ zeRO!_e?}&T?-^bR;{TTqJ~kK`_;}##fdfItx?|b*v%~1S=(*@3iJpU|Tj9(! zYAMBLjt5de*b4)V_uR3s-@4H$!IYahZu=O=AQFVyFwkhvz1#6! z9i1y8v6*AG4`rn1{_QZ7bnb`@&wLNa0hD<^ zwN1&Efv)WS1-1S5_+P#$5}FZix9nukgV44tjjJNT837Ojo$EU)0`w*_^{w<$!2x(jv3C!?-RB%~@fu7iN-{{^J$<1Iu2nf(*peOX)wOO1w zjZ$a18SZwGzMlIvi%1%=W3icKKqTB9y*>AG2bWIc!bogpV!J3$&%N706rISenQWihxgpI4tc!?Q6*-U0(sDFyL#^5PHvmVrIEzUQQL(%d+yv$ z!f3=Xfj#0#APw&Oj-Iic zBs6pQb}`nTt znL~jnxJpeui`r@7&{%N1L?wSCqjP&CG_#OUhnT2_p4Dm3AQ~TH zK(`lc@3y}C>#UtZ0Ooj3=y<0}=UqAR{eOrNWsLrCgA=9I!!1Qh|8oprOE)> zY=l#tadP8HzQM@-IW&D@YxjZRbDq|6lqJaa6d ziB*Dpo5-S!R!uS}g9l}3?ZTkO6}39#F)z=Y3i3^ath&h!Wr4PZ37-S#ptXDbT-@kQ z*)>5Io^fz}PP3v2X?+<$%M->zTDRi#bc@3sV5oIp4Yhrap~lFyZnO@)-J!Lrbh2;JP=n#$O+&3RV}_JU5KNZ@ zK5Yn;nfK=HY27SCtx(3r^=i`Rv^9i!Pd=|w$FyN-#+DEIqQQt>uknPcadlbkla}C6 zYrh(5+gw9U{&HZ|s$E*6MQhi8tA=`a&1$tz9&%uMQzKP(W^zua#%?uwyt53oykbfh z49Q8pJsTEa0X|n%nv7+Axn$SNadwdSj#w$3%w4ALFNF5uKLj4r$;}tG^m*%UnZEuHU5B>fCyx!J^iDn`X5+v73fk?GuGH zg0eB46@=YVfh}dXfMA;0W_7^j&PXPs6=5I~S4gqCWnAu!MQmOK~((!6elUA4u#7k*cojc1= z%d{b@3QvvuqQY7_fY*!ph}kM}h0Vc~iYrRVTuELr+)zuavJ{-v%CCmnG}lm*t9Z$s z4!6&2!=2V|)lly)U#$!ZT8!@XDUk$Z!uAwFu7&EGM7N=cj!%g3&p*HO<2cnTA zoT-r6o{lAKO1;maDGB}DAp;9q8VOxqV)Z%>FGJ>gsNLgi(|% z4pp*fX}GxN6j;2fn#`G)b*LLWMRGDK&RN7kqrWULC*)B0@mp)_dws+xnqIh+sF1CePi?vkQJ~vY8*Xm$7s6>082D#=4FZ;C#0~ zI(!*(9#g{Eb2!U>gZ&rw73>1rz&;fHC;AxrOY}^11u6ph1YbjLLoQ{mMlyX2M2&oh z^$Cm3dKgq9TsNF%Y#CM!A2Rgu&_hE%AFvOd4(hydhd6^T4c;<%(O_awK6t>u`vdn6 z{A8ffCm~)|9MNyoRB65Ggk@g}gIfFMpT0lCh+hml=@XXUW+U6;~|j_Uh$o zy)SGbWfAMEwCSqxKpgy_0!W-rqU(!5I&ajkv8oFGuu+lM+Tx%j0Ev#d zUyIHra|WUTqb{vb`qOcxRzaqlY0+t12|R>{azR^GleA~tax&c-NaxA8d~vT{>Bh|( zkmNC=X^`ozXwk*1nwrf}O+?Ejl_F(u)pw%fNttG74o@^2Fh%7Fb5Lp5XY3v$Ib9^y zYA)zE)O_-jPAO}?+JmKA9BdDSr>Vj`woG0-n6}}bH{7y zoIkG$D{V#}slC2~TXOyJk}{OkD1+6C-Kcj|awIyYx<%)dS?tP!xuy#^QW|+()?RgU2j%fb#{kZ zR!kaoMgxhC@d}(S{;F0MGnQ3Jf5oYhDS|FHiO$n%u9UGmO{sXY&o<*;tDxVj$Q{?Tsb%q~@3_TJ8DmRhutlmZjqLid~ya6iIZ9FIsfIs>$G0di3^` z!;r9Hiap#rHJ zd$-B-mSViDx0o%Jgu$&!kbUl0OF6t|$=2gWW2TmFM2&hsIl&`y-xi(2U^X~39;+q` zW>q1eGLxh-A8XMWYlVC+Z%hUnbzMyp%_Yfn%oZKkQS>T{BV+*ki9KPdRLOLKRyX9J zK!qn!^0`#jlvSgxcu908z~+1vBOb_T6XAN=9#`vBp3i~AA^ZBgNN z`p9FCJZz83J+(+VK%Vl9+go&ALnV<*`RsL_yV6M7>ts*QxTQtsh{<)e zYCxZ^B@(`L)@~zpgYhn;^OhriOGK6`l%1hO-KD6Kt;Sf}qATGZTf$+D;-x^X&~RDE z8zA#^1{^<+E}w4b)PbZ{W3%dV;8iS1um!f~0a=rikz&y1u_vP50n|sd%GqEV$x$MMlmH-q$*h_{_S5q7rn+ zqQsXiexIwM zT5v%?#l1zA21MJv1yp2Ia7RG>pPVX&CO6z$#k35cf12m7=|^VUN(idWX6mIb5n^e)RPDByQYt%BDe40v zU7{Al>d|UsOe6}R41w9{UUeAsU0H9GqIsCl%Xq`t4mTnTW2~8;Ev_AsWyaA~h~y?& z|8Pum?o6dNW1jflSxR{m-4e#~e5z2*lxocE3xfIsuoR_4dNk?vygjwb!ryY4sw*V}{r2AJLCN*7f1?@0R;ZzgZG=ztVLUA6jgozd$*VDU#Jb zpsj;U>CD297b@!e)HH|&NNqBXUHm?)wo*wciD#!I;?>rNeJ*~V1sb5>kl_vm2b{J( z)O7Lt%!&T8*r3+!A`{n9OYi{QfPpJbGk>n7o4rwfTP?C&Qff^QSJpV5e<-k zbopraf)^_`;3)TkGZhT7f>XsBke`o1NvumbE20-c0~8ztS8|wn>jTUezwZ>q1}He@EXRP86&s-7 zc)1({PJ#v~I3RztU2vje1CFu_PEc&XQFg&LXn+EtN81H2RBXUecERz`00lwRl!Q6k zx)&^iS9kY9S*3lIc3E?`<|+-Vv1`-|w=Dea+Sk_JyGE_OeDx2jUtGOxwE#{4JY=}R z@K!_0aI*fV`j6>fuMg{wTRFV)A1i|uY(>BP&E*d-x0cUYMwSjPy??2?WYRsR`-<)g zT@hpv`0e6Ni+dNdi>IMKLqCbW5sjfIB0oa*Bd8DqyFWi*LGW(GYFn!?{>ep<_G~ifd${u(_o&~oDbt+xaOX7#)cZDT4|9Bb?QtL4zKI@- zYYxwyf2UUO-IQsLYwvvTW$HbfGR<-Aop1cJ`t6%C%^6hgyy!UfrJFL%X%Banwd%KR z$~3P%L|&+V>!wWe+JmXBe#@pza|V?=w|z(b=1rOAH6rcn)Nk69X-*@$<7XlD8#il2 zb9{R(vT*8VdYp3_(H(c)uYSX(Omh_7@vp1uOEzVi(}?c4;tT56Z^|^M5#4do8S0BS zWt!87?nt`SuiKPqP9wS_`6~5An=;L5M0cEEQNMOmra6u1juUpNCz~?OX+#Gfy<0ur zlxa>QI{4_F>d|J6XpWn%dC>Br&Gb0tG@^sIy+u9Tlxa>QI(XZg)Pqf#=2&;|{rlAY zO_}C2qJwX{OWoU)X-*?L$X=`#Hf5UAhz`Eub?WY>OmiC1!PCxEcQ$33(})h9dcV58 zDbu`0q)n?^n=;L5M7RHhR`Z)RqB%8w&EfDVo9VI7X+*c*I#P3+GRQx_$Q#)wNBT z<}{+)?tP28x+&9~Ms(XZE>u@GWt!87Zu`=u>hfldXinu_bG!cJ&GgvjG@{$CSy9uQ zGRNkOzpde`kFI`rHD>so;U5h_{V(-@r}wP< zY~^oO&Rzb=^5x5}rF)k4E?ITo)4f%@I3YPYFe!aqL2PRVxL~SkgA~i(=uBff!>jg_saHgAYbPb&3>d>HoCmE zWOdw%w6fJnEJFnP>yxtEHi>5hV&4iByK-8s&>vR#rr%4oLrHgHT%A2>yaSK%wXm%q zlvn}db6hOx$vUu!F#_&0yXbg2)M;fyp`0t3GBL!yWhi!8j@Y3vEcH9g$T)D3T*TT> zdmxQ?xtTK#bU@gJN|u#K!Yfx7PQL*z~$HpCjCMQYhF>gD}gFT_tmMm?@C{ zLL6tXc8J)w2*obS5Zhxd+M3cp0MWf zPGX-Hiq*;x>-Spo`B)@i4MnJ+J6NE{;Vzf%xMOXPrylKv%8m6&Ddias3&3R>C{`mw zEEnzNs^M|LJ1LJFLN1nC9~Ywx=828+Ld@58v#hC+z+<6g!Ak5~fMOS9h_&|^A!HrV z;68$ls<0mWB-w9N9F}1`rhS!C`h<8(F;Q`lPi{jpx8%bSX&_4wMji^jpBH^>?8dnBgIoKr?qa* z^aHU#I+>|6PMfOa9BbQQJUnoi1_f)2!HqPY<>(@ncAJ9&UTvq+(Qv03 z2R$*73K4rpQ0&7ptS!?lNx1ou$3#V|BLm6K zq1fNcuy!!a#aY}U2(AQ6hTOhxx8C-Z!nSgg3pJyoP|#EMng&4W1c)6#vA>gHt%9(g z#NIv>`&${-DhS|7?Cn9ZzmXxf<8G`EeXc>gYBUAC8Inv_L4ak4trF=~i_S7jWP*uA zGDi9FA+cA0Vt>8$KJSjUk@wJiEG{(rHcQ7!(1mI_6AD?lq6g~)0zO|b5ldEWW82V2 z?CnCazmj3Cg20-@-VPM|OBvQO$yDAvOlJy1ciKuC!9{c|S!!8|Omh^=xZ+h`(-bNB z$E=$ch`ntn_MxrMo+(KQP3&z!u@B0yRzVVHVlNNH{z8Ve3IbpfdpRif=Q6BSaQ>9o z+k|3&Cc|0%0J9P?5#ktKax`k@(<|{d&^MleKM?7aG;3T zOGB~u%CJ_!F(G1a35vaE>s@jsIa-Om6cqbI8DbTjJt6k)hGGxP5Ub!k1(+QP(Cdd>Lx0QF z-qjA|yxu{^=C?-Za5-moKJ9!Y1Dm&93k5CFd}QKeCUMSGtrjW~f6P%TyXxuKU})p< zR6T4mc{xWv&O~gD#E25?LZo1&V1F z&rqJeIfnc%BnEOB%_ieCDcsydlLDDfHO!8#soRTJ>NQhFsPgd<)NCDjMui4o~Zp22pn72E2RlRa@?}Re$&RRGq?k^kGa0&`2;BX@)B*q7fN8qecE8lD(@#psWd%0>#S(J4j3$d~s$p)w zD_OkmddumIjRq9Br~~ug6{jp-Y+$uWwg=;Ox1|7E_j@`*x4ew6-Gw0(^4As_t?j=JI#S?MT~a zY1aH~tb(~Bv{&8)I4wHpcbBsJqSJnN8Ld__?Hs*+XPMXU6dZa!Iun}G8F8i`9T}DW zf2k++g-Ta|shOYR9P7s9vgd3Ed6}onO~_j;SE^LMIZf|8U9D6GAl0)dP>KpKWp%bq zpGrSHsoYT96VK+(tw7-u6f+qf z_}?JcA4N|~2Z`!Dh(It+QVlEM@%(?&)$m_%xG`=r+~iuhe1pz*2Ikht91nS8L31P! zAyV#9q%=(Pk#KaEeQ3!Ba{i~EPkK^ehkehaW@A{ZIU*!EZx|;@%OGmY&<=ZR7;^Mk zPpZ}o=SZs7DETaH+b)NzS;bAAw1bZDW2@Ol@vM=lCW5&qKd~c2NZBA$sixwPwxDAP&Iq7v8*Zy85%~^HnzkK{NlD zJ$^4dc$!}bAN+jPY7CuWaVOs7$5k1(|d6kWmY zsBC-0)Dye+bb*dDfer;*rCaaaNYx9|YZdo&fQmDK3I*GyvQrC@hecA++ow7d84lAjc8yUH#i^iyRI2UP3=MN+T@E4xVl zy)G2EJx!_V6zs{$F3QHLWA?B>-*f~~uvsg+Tt`lrl|2oq1{EZJlwGnXY7mT6b)aZE zQYqNcm0hIGN2=LFbW^h$duqTQ7pP(dTfwpmKC`Nz_At}J17!*}h-DXE92S{9RiIry#ZC)?e8-tGVKy-9Y7ZZdI_CExT0Hxs}^f z1RACjoq}y`*@fCTT623Yl=Las@0MMnjruBkE&%$bvz~&DaM|U1V$81Wd5xq;!Opns zay_A^vFCiCXF3!s*eaJ@o|&Pzv8V93GWi?mvI{k%jM_RCY{eeMQ$)KYe!%P~)lp93t$xQ75URSW;FWVR5 z+(7LiBppX}dsHg3sDs;+20EtW=uzF3l61@)*KnY3I<6_$+S&Reprr4K@n(AezqD|R zO8XWq52E%TtNE?wPLK!S?=%-_s+yN*mKPpexMl6$wXd#SvnH%%*IaAIgIfaMU;W(b zzpt{Z@m0&}F@`@Ft~Y$Z@J2(?u-$;_AJp&Hzgs`jXY}XlU$S!l%D=7bTWPH%R(7qt zVEGTr-&nqO`5%@?%cbSW@|jCNUi#|NHA`14rI(ybC+QvrcMHCv`>^g}U0s*fy>ju7 zi-#A#wD>^~Hz2yWYw-o>FVSzK??*32Y4kkwY~%stR^%GwG9-z(w2x@Nr~SP49~a)S zFj#OboT&b@`n&26sNbp%s9&mHQ~m6&bCHFYTrmAGp*}qQ-{=1Qb=JV?$ik})3&>ZV zvQr^nI}usf1xUWUp&uYOpNuS6z@slokB%JXk*~rQw19k3dbsI0Xq>$Xgd8{?G(aHx z3lh6Hq#$3BRDOP*$}dX~KR55;m!xvsBt3khKOieqm4rg@&!rsXCxA)UlDpm zil!@qfP7wh`006iKPT10jnc!->tXh07;=+T4>w5cQayb31Y}_yRL}L&&%G8`5_&DZ zf_z3S40@9EWMg5_Q^l%4Pn1ZWs0#E&Vo}f&q(?`(6g@>O3c5{txM@+$vv1Al$zoB^ z7fS46QP7hlsmIHbI{W^Ho+y>~IEj5uc~6ka`vQsNi1Kce%KLoj(UZ#iLdmjYWmz_R z0S-N0D(~|ocB#C_Nd_My{XECu7f1%LNhC)Y{CvsaRq4@_3_eye*dRUJ)Il5anWE2= z4Ax8RlEKGF2Cqmz&oOvSGI&`cIl|yo$>1gF(US}|NCxYqhnpKbdy5a%O9n4W?2^GN zlEJ9-^BjYhC4&)(vb>_7gX{C>gvUu}cP{#~}-68WvEs z^fP?7Mv)gF3uk~|Rno81cT-e*CbDq$*$c>@r61>hszo(o4IqD#sOHqbf~XVu z3obAi{$66Ad5c0G6>A;&-Q&-@6|m)oJR({9Tj}Q~S^Edcq~FLgX?9dX9+uqnYl(fX zn~>j2hW+ZvhW$=*%`c^&VRJx!D~Wt)Q<1+B8x8WHL^W+R$gd^W{X+V2V{<@$C6WC6 zsY!k*Iq+u^$rEh@c}RNnUmG7iC^_c=>Cvonejz>o=~F!ax#Xq$rRPub($A!aKbiON zza$s_SbDg5tJ>hg2PF0%Jw5iHO1`~MVi&tQa=-NRy-)u6CsOs_BmF$bLqC@4_=gh7 z5!L@AsTvMTkDgRV_etgWf%I_mYM7nHkb5PAzb~;%<+w*$fP7E-`H2O{4<&uym7{M% zB6j4kR1@Ek*yq&552Tv-wnTD7O?+RfiEl}do>UXxlPo(V%d*+Q8~LtO6W^5BrJDGT zWbiknpXV6-ZOP!fC6Xfy{+49$*QG~KGWd{W@LkfwO>!_xLta9L^+ogKASz?#!;WnuWe^vVVi6(rjWXyqi#@w=OxI;y%?pCdww*2t&?aTkNe903+tm)^E?;gW4hue(=wz3wtyS?AKNF5bI%-QqhION)+0y=K4WEt-O67r1@= zt%d(wcoX_V^b_b_bT?{4mysVJA4A@bT!>gycdNgt{s^K2@95WP-==+y)}%$%Z%}8| zJJqVSyVw4G?e&I#F}&80G@Pk_WMN%@yMC-cW98PB4=#`k3;GZ1ma=&z7V`Y!#u_PPwG-(7!`2=|25F zk+jEBjX@|9Y$am3fB@3V2s8DnxlVJPMCuiiQ6Z9uCM(IFFNY6j9Sc4PzC8Es`l1qA>PV&~)27FTY zNx4!#q5Fhf(vRytE|>IUx{t{vy;gUvT+(ZF*T^OPsP3b3N%!maOQcn@G{ERYCS02Y zhB;q*HoK+v46W3%ge^%=*GesGYtqxSkGHHXrEb@5muu^(+Ee9{zDWBbxumCPPmxP{ zvi4-Tq$g=ll1qA`_QbQaDYg1Efgbw>lFo#}^=K$I%*FCE!&|2#ld#>EN!V)1B;aN+#heUn!ID6|ayzm{o}ULiW$%+LqX2Sw7YC)`hIq|v^&<)SNZ$V*bU zTy&8JQZ8xEkdsKaG$3op%9WZiWaLUE41`>%X+v7BRNR2em6|f7B+?}7354Adu9G3d zExgOmI(DVK(w0lwT4~88cPn^n;89>aU&lD^v@t^Bu^dWAt+`ET6~Kdq5oleQ+kQS?dr9wz`cOW*DhVVWNoz8UaPI`UL)6%Yr(bi)|_j`wX@c?uWeg9W=*%IT77u+ zq1F3W53e3tJ-B*c^`_P9R`-K9^{ZB|T)lkt($z~=N2~4C+Uo9Aay7XcTs?2qxoTWJ zYjykTw$)=+b*n1F!yvoB{f5JaLxzKf1BROn*BSO3t~Oj{xYBUB;ZnmThLNFds2O$} zNbn*WG@NH}8jOasz=?xxhGPsmgG&Ff{vrMS`osD|`hzRRm9tj1uWVa6W<|H60^cZPleu3Wl&>5`>OmqttNrP|W& zC2}db6kIwFoMkXBowc-mY1`5Hbm*$O9WM9)IEqubD9 zP#vm59!4HQ?ne$IhmeEF0pup+I*_;FYUC>9O5}3nQsfe3gtUg^_PH?B5i$5zgti0 zllq|kJiSwI)Sso_uHU9VMz7PWz}KILz$uc$D~DDNt{hmoY2~_={VP|mT(xo~xSw+A z$|Wl!FhJKk?A?OEFG+HKlnv^uRy^RVV2kk#_A=8)!~=78oV@K(HEb2Z3^ai!*R&83=4 zG$T!0Q`79$keZ|>s5$S6u~dEK6MsMV*R!Pt7BveCXafBa#5j5%#29)n#3*_X#0dIB zh+*`wgg=n*`w&Cu_aFw*??Mcq-+|~yzYWoceoMkb5`Gh+7ySlA4|+F5H~Mvm7qd*{FsE-N_dTg zABAW`_d~Rz{}-YK{ZEKy^#4FKp&ya(KP3Ejh(`3^Anro1hWK*yUm>oe{{r!4=s!#N zVTe1?4?%n>`ay_iqyGf)Ec7afFG2rN!hewP0}x+~{yoGq(f32#f&LxDGtl=zJRQ9f z;%VsLO88!g+tI&)cq;lHh%Z9l4e=E83Wz78?}B&|dO5@s(aRv7fW8yrHuN13Ux@C5 zcs#ln;&JF63EwW^r4qhP!naEJ775=h;hQ9UBg7Y=Z-DrG^b&~2qOXVeJoI9S$DkKU z_&SJd=xZUaq7#S)bPQ3Cjv%g}LkR~Em(f1NCA0@ohYAoE(Jn+3?MT>$h@dTqT9k*V zK{<#EXcM9uWg)82#uSl1qjiXXLTeEJh%yi#L#q%UMJo^=LCX;TfYJ~jMoSQXk5UkS zhwg^>TeJxAH|T{Be~n%M@mJ_;ApR0PAL2u30pf#b9^x-h65`L%9K@fYS&09IW*|O* z5)glirXk*s;t+p=rXc*%W?-i4kE@lNzr5br==3GpEM z3W&F(E{M0GPKdXn4v4p)c8Fg?Z4hrptq{M8o&)g!YJvC_)C}>WEPhs)JeD~ zAu1svp;khTgbNa?B~*!s{#n95N%%*IRrE2474%VvW%Ln^5h!>#0hWHxvR}jxfe+jXGJ_Iq3J_wOSe*rOv{v2W!{Tajz`d<(U^Z|%z z^rsMU^nQpb^d}IL=#Qs}d;~oe;(wsfKghqM&_Bq(q0m3b)hP52@~kq@BIKgi#s&_BrgQRpA!eJJz~ zawQ7=gZwQD{e!$0h5kYQ28I4X-h)E_An!(@e~>Fs=ojQ&2=ohBXG6b$)6UQ@$U70} z7vvoX^b4{Nh5khLBG5m`9t8Ra?7u?)fK@v55Arqy`UiO{0{w%$1%duS-i$#1Aa6vV ze~>pI&@W(Z5B-9?9)W&AE=HhVkc$xL7vyya^b5%12K|Xl5a=Idj6nZ@Hv{M&5FZ}; z2k9fCe|pG|pnnA9UWi@f9*7;}hY;JyVTdi{2M~GW`w%(gdk~w*cOkOKeGu!&w;|S$ zZ$V^`LlCRTHz8J#Z$K;~cSEF+uR|;$cR{3(J0b2y?toZC4nn*TxgFvK$ZZf`gWL-7 zeB>601>|cG^T^E*N#v^#bI5lfW|0FBGsssU63CY!rjai}#E~yTOd?-^m_R-cF^+r= zVhp(nVifr-#0c^kh+*W@5JSj~5QE4K5Ch2d5dFxfAo`F`Li8e^fapO!4$+N#3?hbH zyS(x-l~45twfX|}&V@5Ir!M>){W|(Vw1d7HT}8f&{5$ZWA9;cH9`H7Gu{NqbapjFG zN%gN+UZmf){Ilijm*2je(;Zm20i5OkHF#V1uIkm7t$t8{cHl*fxAZdIBf9slw!tX^^TOM8s?~G#uUour@$WS$!(g$Y4{JUS-dj=i=Tv_%91Gs! z|5N`ukX>+_;oF9*SAHzxokG1+WtKs91c^ygS=hn%ErWzJ?6j^|OOX;2?V729lk>$= zKEg_r28mI^R!;Z>M$(vH`4r^f>h(b&9kx3Hu9lS?F8)H~ke{aj9f%ykoX0bY z6oqk^4i9qm80WOH9*2E^jS4{%Kfuj_b}*MSyK6?>PO%)EJ5=dcNwY7JOO=z{*xan7 z>i$C2!P!zAR0!lkR4O>lQ_wGs zoEE0oq_xM3R&kR_s^qPKWCeN3Yv-&{cHJAs#+?Q^@>Obdh!vOwZS7VF>+)41N7kE3 zHk#o~!Pm)5gbHWPVjh8QHAb~Ta74Hq4LVdUc!t)dbLn{^N86r>S5i0=j$4>g6AO1D z`9z?GPv~yTiFfT~n|*-yGPZt>(cQ2jvfydIFKZ%#uAvDe!)VsJBYKeaw|&h}vlg`t z%GqQxUvpdO9;Sbn$YHTmUDmjVU>uRC&+X+ioCU|@EshOWTYZLklC3W8Kn^apSN*>CIz- zVz3$TWaGh{yA`s#;-(0LyR#93G0_z-w)k$y!A-1#C|#h+W?#ui7r0P1lxF?$XeHKX zILzy^d5bY)y-aWspZYYBV?5}01}U%69gcnUVqIfoX)yB-7$xEDwXHPYixB!XF*O(e-g&}ipq z^*=})$#Rx9jgmBPaguSbH#12(TjBM{pjK&Yr85hkS zUqC4Ge!gGv;PLFxo{nT{ndUIfjhL|=cT{M$R%Ftm3V~p8DncbPWX;t1ABC2TDhGAc;&UOZsZr90GFo$)Wcj0u3vrmk!e%*?j{6b#)l%w zv6q~LJj}wwA_re?w(;CA>b zrBuW*hz8cd0FkFU;X%YV@TG`u2q}sj43Tg*-C>YQpdXF9x*az=7&;RDcDqpMb7;McdvMkluD3bPLg&U+XFa2SK(s1o3&(q%u@~g{u@aS1Es=Hm9WjTAO4H4V z!*6DM)j`WddSdMImqZS->%;jJnKe5EYbhIUSo8k6v(K1)jbXg(#Yhv?>%`0*SH!rG z7ddjBcqdZrlHQ!(TIPyT))8z~@%1rgE)(8Kvg(LV*0F}k=NPJQg^iuZ{o#U*;sTc3 z0Ap;idedF%uSe4!zcFe8324fuLe1^3joSYFg`$d7rHcgyqc}x)!&&>N5yjgUArPN% zR!10b)&;y=5kg*1&*f7|#Yl#my}YGZNENeA5Lc_4?j_1`OFOq-Wtv>1O|-F6$vw$6 zjqJiDqK?ogAC3l6nOr*F<8aKFns|tzC%NwB^CW1%E|=R;_q0vEzI8$Df_%ssZza3J z&|;2LMlRN^_BxhvBGxr|jT5TN5$on681qu=qm1U)qK*J=vO3)%!ezBu0u<*-dD}6z zX&LY_+?Jpk!XS!S+CAfXTtLENRbibHkCnpJ94ENi)d0y?Cy7ZB3o-SsJ>D;R16+#m zRocCvFcDQ?ooJX1T74m7Cuj;M%5D?Lhr&{0D&lg)2VueTa^heQq+Mh047 zG@4jOrbv?Nj#^->sbe5KrWxZbLo#1aHGQ=dZDNsQL=HzDuNxiiYR6mbTS>e-D&Rzs{kp1r z#7g!r@~2c+)@lt;Mslt)%cMG7A-`@B@NC*2P6xcECbeE_1+rdZ7-p=ilFM85ET2xY zM5UN?H1a(Ho8(PYG2@84vIBo(Jyo?g9h8~&r9!Je7Im=pVz1W0Jp$$#WI8PqU5fNF zwW_V#$XCk_J7#p-b5xs}1liTQMUL#y-AY7#X=A#`M;k`WoiOKfg=E)ZcE=bG#)RUe zd)NvWlcCj&$dMq5DSy~lEJq`yxL|J%CONRk5GJ>xbuoR~kP8YC&q-0_>DoY|fv1S)2a)6t zUuAgy()-X;)%*Vni}+VnBoH1p{djoTqZ-|0CO-O8B?B-U=Y@w=5ZeQWhgA@pWPZGJ zr2-U$haHSP{%VE@NAlscmn!>f#LS7(X$k)4=?S(D4?BB_4+QdjE2;SlWX2^qae)B{NY2XfSKV# zys|eP2aZgU@C@ZO5tR$IdK(M~r%!J!P7Nv7g?5t~Pj5(1f2Q~rziu{~Twug77fntR zi2A#3F`1^MVjSmUzuN?qe81V8MhkvC;IbL~y+y#~rwV7iVY~vOTHQJW31P^pdI58nH*V@rLrv&j{9u2aW&u$Seyw*%4La~$MI+hlNkqoT6D4Jr5^8j3TC^l zLubkDdCqw~Psv1o!Q)Lyj1&$U!TF~vi2^n+QOW$C_Y6(h|9|OuHsH9qbDlfhA$Bpt zPA*@^+Fsh4t!G_+kA>Tr;zwC3NyOsAaHM4o2M1u10Yat$$f;hxTM@(7&kO<6xa5_x z*=7U5Tc~29jJ2lYM3@Hs#_A<0cL$G#s%fjkov52zW$Vb-uri%w6R+F;##arT7 zp34$#wGY#l!D%J9i8@`-1AE?+TG8cFt)!4MLKezw? z-2VS_`~T1F|3A0?|J?rnbNm0#?f*Zw|Nq?n|8x8Q&+Y$}^Cqx|z_WzwChl2foX*XZ~{=eamq7DW7|I%*0g8hG~?f?H( zZ5VG+m}T`zs-o)Gs)c3sz3Lml?f?DYwm${p{6BB?r>mb|egCSk8d*KV@TlSIhL0HD zYKe7DIO1g7&$1Xmw`1!@ZTkI}|7tcT+MejmCg1#Aj4Tv0Q zKz@XL26?ag-3SkE9-Inpj~~?joAwRbg4V2E*4(SPQS&z%PUF*@vhcfw+ZXZ1=8j6<9Z6qg4~%SAqukMdS>HnsLy<{ zCZ#?tmn&r3<7lM-4&%3T^>TDHBhN3AWhaeHB^m5aJdNs@C~=V)vOLsU^@h?^C+@B_ z6T@1iJ4$b`t^vhW#_4pn@1dJ`+wEnBkP>ZK$>6S#gWG|LT zCKKQSfIAe67uvH?_SDCS=0%0B+sAi(}ZXITGiOv;K#7U?EB^#oN4(=jF#ZVWv$ zYMmlkI7wufdTo#%Qi)oK?aq?DUNkP?^Y#2>hU^!-A&#wdW~*3zJ)|oS3Vb!(;=Sys z+3kAC%!W$Aq1tla6X{cfm^)p?ON2iu&@A_{%D}bP}%Y@*yATNWXFkGy`6X@?k|IZ1sn2XYSf+~Q{64nxvN;Z(5=?} zm2`w?l?V0sfWgYM@_qr$qgz5C5~&3T5h~kf-Jbev9iJ$Q zYX!y$jLGv&cLD26cxrY8P_K)0_42qsuCUojI2as^K`w?FwQ5XMo6e*HTstJBJ#H*Z zB!aV@_T{2Dyi~1?3gx8G^9jj766?>38;Nw_*x@jmW=92|Cmb6z8na>d)HjG~gS}22 z8+(F2tmO`sJX~}}o<|fHV8O)}P!zX&lo<)aWOiIseF)N(CP|OK;!npwe(&@kUK-6d zc-22bwI!bDXWbMQCXyvOU+CepbR|(7I~><5!NM>d3s?K;$Y3^#sp^j+U85ZJl-)x% zLe*<{2=~vLr+$k_S9gc#4j-sAgM%pFW)mA~?_(le#Z9JK)mEZ9io1Igp+7qytG*=C zrM!u531fLaRj6i4HD<#g^#YMDkseKYJ#UY0^4%OCZ4G8>S#^&{7f+LEf+{CswNN(? zW~|^W-F+fmG&d$GvOLJt%4w|C%FK2l_1hpFHNnGie=-CvDq#W}4Mk_?AJu3Cl^(j!AcHsbRM-AOJs zOZR_8I+l;dqob;)){GAP`5GIZp;O&2($y2fa9C>#j}*R+5|US0z{%bTi&ol zR-y}W(43p$dK2WeFL!5(@dAk~oDk~$a3Y=Zg*nrK{hlxTtD&QX5EO_*qy z+R*lH66w;BcqAPllYv&KlxP zbUD%?>UbkD7|xVJ^>eWpZqFc1HhP7OFWF26f`i!*Zz-s@FoTupP(E8|c!Gi_M-*o3SoLm+F2VMQP_}|IZgN!Zmo{{b_lR`)$e__~7VvQ> zH<+;ac((1S#Oa`ruDL^_VJXawz~IQW@YHN^#Wqz)4C|#*yPNT(y6JGr)7>!6e^S(& z0Qb&7e`Is{up8V^smyv;{V9JoUr-wmrd->*`MD%R}wiE?9{ z@eF6>T?EDDds#BoNGF+WuQcjTG6`(97{3?k+B8SS;;~6%&|~twZnHi^ryhuO9NrJ* z>LY)f>i8;Na6N98?yVvnLj{t)E>($oI>A)ETc2Hasl;Vmo{IKU6D}M{#|Us+va_Ml zt8WzLr7)tzl%gTd_sG>0XNmc-dr{w zs0waAuwjIJ9+XGchk;QlGHfvYp3l>aWS+dXF}qaXh3+7kRw>4~8*$o;hk2Zg&Nfq{ zD2}Egqk@nelnONrEF;RZtwr^iNS7&j`$=y-Mv&oxcZ?-Bv;y(XfW)yvxY8=PYk^8I z5@}EyCbkPjdGYFyo>Y>}ao(ScdZ_-adsXifixG?CY*nCBB;H{1UC`TSs;?-Lg*x0M z({AHCzKet8;p4*e zvRYj7<=RB87x!gr-ApM{_LO6@^MU$R(7aruNqEB9gg*>kH9bAn@0-z!i)w3~EQNy@ z-J>vxd9iVR)@Q1ZOLWmnIX+AV0s*4esdT6fE67tsI){j0be%$2Wn~r(odAbYUOrLhD4|$3Mfg6%$#k|-k zZypWuX1|fjx%#01{9B*`u1VS3vL?LP(9^(%F(TeB1hV~dpvh6W>^KCjmQ+%$Va)3r z=FLqu6eu@Kb<7p=`XWH)B+mhQkK06>vtx4l+-qb~0e`<_w)SETY?L5;fHz!?5zHt) z!G=*HHjXvgLdhJjl>)T^ZAsY^;Po_yK^2Xz72 zwniZhZ15O|BpYtU8_ftkY3Aad){tfeNL%0mt=U#92eV^98)v}bz{V(GOxtRWF~(S! zLNeJ)aa__=!t7+zYfh$zX1d_X4_lsCA(-`KF_JKu9WOV6|4hkBx4C`i_IRat&N-Mc z=IL|J2@*tdm+|FRtD1Yq*H-QZK6&+4KAAnF`K0Gesn@I8HT&rDU7wI~qohBQ1R8pmGh_kfkZGKaLf0}vA`#<+QKIr^2t4m zd;-R^qvR7G2(QreS!u3kEdJ*E{*hw(I6D~2%HKKbewfKOhzl~0^AljgI^Cr7KF_zUiG+7ymW znqz`*6GQ+{M#=mfpSTAxt{Y*C7Q9ulno1UxfwfThY z(I(6uGLMK@(AF;1dt7EbZ{3A~xHZ0PbG^XWMqDWig0NKMNoS@Kr<*>by@_R5vrw7% zT|=x1`Uy#5sYDzEKalN{_gw;f;@rw7)@QR%j@Envu4!br<}hVuOkkZ9p)>3xo5kkz z6RaE;+J%(W;R=j;o_M*~@#Y;EUdY87(M-@~uKBP?Bq}gbt2YJq)Y7>^Je4JqvVHQm z1n`MtE1%5fLwz=lT}SPmG2mHqNY?A&O^t$m7;^FL2Irw;M$fPn^@A`Bcqo;P$@WS8zU9@nDysUTO8b57N3;_V z8U1+8eVS`Pyz{vFGwOZn!pe_VJ_cj{ou&Gn>Wd)iKf3z$l^4O-f0r(2mv=1v3Pi+z z=hADIUaEW8;GIV9(|uX@9Y@swf?F&0xnfeta=Q!y0)pP3WBFhhn0 zb#tn0BZgL<@r4^r$9l$wXRT)EWZYQKrc%eB53N$dQBcewMpi1u(_CxaI>a4RYtriC zaj&i8lt6`gntrsr1?7R11?5 zyWIs}*%v6A9nNaWn;_N)6@S(%P*^Er%w^hH zES~~f5XWAlnB$|0IrfVjbepw~bBT5@-SshrAj@!gq>+uZ$(GsE;)`a|HSzP_V71IV zUu-1>1stSe9XZHROw^N{wdPF6xSlibA|stfVB%;xOr?%96Ace5iE@~AW{vJ{%kXK% z9Ad~$B|5}tb4oa_m#p#%;~|KF#@Ujk4r}bky?$ed$A{jbu~cu2$3r_)vSunq533f> zBo-6$IUZB2<57_#T+R0SJ!_+z#=NY3luGm(>vY0dw5HMm2*=h;_Iqw?l@D_v1Fo1u zJSkZW=Yxbf;V?RhM!7>Y!j=5MTu4yuc8&0Qyk@QwOQwx=sujwkuNQR$172&vmv?n- zIM~w}hW&*}op(4n5O%9j&iJ~v0BbjA`q3(l{!%fA7<;U!K*xikj)0$U)vUg1K=ANv zalkZ)xHsTyM22LnZK~7pB2Tcqkzh(8+C&Hj%^XpBd}Bw8cGgq6PbucOPBF(PMGi|lv7Tsq#|5u>9mIn*q0&m`z zR>|iOIi6YnYw1|#_UD7J% z5Kne1(Xk-vXkqq3G)3gIc*ZDry!}btRUP&FUUR@w6GDEsx6Va^!_=soSvW;8$H|I0 zP7*ozqAL)ou~>^I=pwzIFXJwYXBgr0Rm$Y54;%Jq)|}5I!Yu)j`bU@;`Ku;#Z-n=~ zC3|GxH?Obz3ItIP;G@!rWlXMg6~q|{x{N2#P)9MF#z#3WoEsJslTk4bJ}ZyhB@p_t z?;0x<;ERTuq%mbmhs$)sy`J@_@|-mn zE|kZFuT9pSCm0lS=tT|%B^TH2MFm!^h&ntm@NHy_rAnrt+Yz&jQyH@2ek%2oH7jnBs=G-LHF~@f?bllYSfKXb~h(3LO8q=pz z1~vQI(u<+(qq~Ep- z^Ra%y8wm^uh9s@0Z5csLX6!1nWnRDQg8*IG5biXEJH2*1kffV=Hscf4!(=0$wu1Fc zy+@>~$?Vt#!;|e$5+n{(jgt6y^ZT6=1LsBmnFf8UgD7i7s#&KtbiOSsciQIv=W#7X z-Js*0A@;m!|A)U7`Brfno^EE-s9A(?B@A0kj~k7(S`u7@> z-O#vF$NY7AmuoPb(9VntlQ(yQ9y*;nL4aps+Cwj+LBv)0GuF|YJ5BR?=rcbTTGkUz zW5jKX#d^+y0IraQ?H(Rn)1`0>k5=>Eij@Z2X1)fF&0B-ETSE|2v-=j2}7&s+!uj_W6#+4$cT{MOGQRzMrKAtMrKAv zZj#=kjd>E&^=u2qW*%nn*dHFF4X zXgyzuG^^JMU-oh{EE6QGs?Avh(h^n5a&Fo&2LnXubvU8R^bEW;%hym=E0*((ilNHY zV!>8(z2MHkvB<(`icc(A&P-eFa8cl#MVjo=B(z?z%VpESa1`_Y6Db@B zf0loqy`S4_wZy}d{`%?TPB_CFbs}`;%BN+crocr#G>e%D<@V>5Mz%dwO&vEm$$({Jfoftn%#r?25q#s3&7L2X3mD>EmQks*|fd zQmI``gCr%UH4}7@Q!wg-ghbdiEWak_20(|4Mo4Pm#L2_$Lc0K5~E#94!Gwe`e7 z{@MHK6@v{>PwMO2Q`TW{cg0`>)Ds-d8wN`gd6KrOaMiTu)X0~IFgrkyE;A;Zz0*ZQ zM5a@dU5CL}t{7~9dg3$VTAKn^t3`9(3u~-+I z%9iszT42mkzc;DoawM>(X2*Je#JGh<+MG$9deuD{62Lb>>(hS4E=gl(4)+=kjug7) zWEy$M49-quSfh+uUV(1QZDVYy9Rv?Tu3c@bTJrVNv-j_;7;J!gqH0>E6ctgNXi*wn z$ZCbD>se%zDC!nNra@Mz5S7{)$U((*7<|QwL8Rw<9Y33EuxM_`F+>fl_t}opsY|}6 zdill>_6xmQHp>{|2_BYF2RzR28TkSOJhlyBz?zQ>T`z)1PM(}}^ln{a!+c3MZgWDj z=ab-_Ah5i-lN!88z7%`*{+F*9Y|yTgW=5dGf~uUBDy`;N2KnrQQ?xLzwIxz3Aiy&N z0zneovFPU4>Vvu&;*2Uo zuG0r!wqmeByG~4H&{=eAc}9}tCZ^-FwAHtQavtPt1*uXASx~!98Ow7{!C6NS-mqe@ zLAy?n0VPqBQf6Z214l~RlIGiWONO)6P-+iEnW2Ur2i6Q;T&oXWzhbaKyH1+6t1EDS zhGZS`JRA|xTcD>wm6}8Iq0&C&Y zbR7n-SuxlE^`yW-?yLbt<-np`A4yBKdFU0RYmB@4srrU<1^Xa8elRO#%@rq_x0mGZzc7i8PV)4BV~h zqB@#b8CPm?K?7Qc!Jl6-5S?tbI2q_2ki<9&b0dT+8dY)rvpX@Wm?z8?@`mS}>oz|L0Z=HfYzAwdFi} z|CK8S8(3za?49+q_rGYxU;`V~lf9FB_Wmm#Fj)UCeiQrg)pH@X|8E~{9lz)BkM<_u zAD{dA|7Q<8*FAe{>?;AXo@}r(U#(&5+H4=2Z)g+43%Dx$%==1guz)}Ao;uFg1&DtZ zyd+*s6}J4}diD_`Pw7we)-{vKWGkSC$C_aWYPBo{cu>7FojPrWnmZs>x!OfF$nGFj zRi}CH&a6E}3gOD9QYj2Z1ZF~rWKPK}I6+ONW*!Fq8dp0=#7$qCVfW6=*bMtc zsV955T6eNNCdCkNU|8SWwDcAD%sWVIa8`NTWb1UpaWIE6|QOviQ7tjNJc#cucMOu1kTn>?A- z^LNtJ9RtP}ll05BLn_Wtm+z8LgRC(9IXi2jwZZ}lCw|6jr-@dMueg|DgSb5DZew_t z_he)UTo@M<6))t(42TaYO=m>2nMvn+ipWWexiG}hwRXhi8dL-m5xDB=@$nP5>Ta;i zU;U}Q?v7Y^+(i6Szawt&<@&gZ_{W~IpDibeKPCVS|DPRK-DRTl$Id&9$u*2Fp3Cbt zj7U;oqMFF2rF43PFfImV)IwQe{l%T4;=b&8$K4G{L`+Zf6V#&`<@C%H?+Dc0%4lW~ z=_clO=O8&CI_v<~K&Y$~%9T4P(@+|@>DV9O2oH6UsyKnOnLA_~azI4Tuu+58Om+!e zbu$m_|F7P%wr)Lp^V2uqb>k0jyyN%}j_21ub$xp6zg~-ue*Y*u{Jq2B!S5af`@ggA z?|pL5-Te={-JRduakf9P-Aes>%HH}_Ab#=BGcU-yz&O8&ZRyG0y+8AUv@wAVj3rlJ z{y+1A7!%mU2DHH)`PDb?&%7XPOyG%Lyg%~-zcGOg>=f(t02dS3U|gT95$u^4*o_H1 zQ47>FFEFbi*uV()gq4uqn7|WOLTY0IPgn`bn80Vv3i-?n)tJC0M#v|u1H{Gzp0Ez! z8xwfKI)KFlHUNFHR@i4=KsP4vgogl$32dNPPu2?j%nNW#U=t(o22bj)z8ZMu1!!Xe zy}-(|ifhu|z+_JYs*@2jP$7eR5I)egT(MhJtufY`8ftnS0)KI10vlXOtV7^yVgj33 z!JqIDyk%noPk0F4yfJ|%JOp1I6WGKJAX?ui|LX7ZXI}WKjR|b<3Sk`rZ`zo^2KNE$ z5U9iiHn6sxJmFbT-k87>o&}|i2|VFhP>cy|;t2ADXF*|O0#9@ntnB~t*74g8Ke_iu z+w#R9@}K+5=XzkR9(ea(IIG7tm8ViC8yvE(J~pg*m7DvY^Nr~ShoQ$^<;Jf&KWiZu zUab91uX*eFhu>E`FMaBz55AW6$SYoedjWXk-U-6-o66;M{8ayCMrz39D(MTaf1CIa zy&DmB7a7b{A#YD-E-o2^bSIx73fdSGhnec2jFF=T`?9ZpTS}1~OXPGGS%TTEQjK{? z1bTA}8&VEIIBV+KSnaOK0sjYs>EJ9^YBN66Yes2j9F-ioqu+swJQNi&)2T{E>RrY* z;kBNB#Z3p-zhB=ydwMxnzX5ptJkig*b?541UYjl-P{%3&UT^ltA+QX*b@`vh^TG0Y z;A{P_Tw3hE(jK)e@Lmx7;iU%rE8j&t8M|Rz<)k$VyTS5l<7@Lv*qBi;TK@kFGU8;C z*;gJh_AgcO3e{g$^RksaU04P_d91CYR=tCTY*@`#vt3&%luf!&ta;gng>~*WXN{qR zkA1!dnd3(JWh;9Lo4eBv+tT}!yG8MC6gCP}&m0rR1T*j6mAn0+tc@j03*CeAuFUXEqB;Sv`9}&!`wo;XUMhf#o8Zx5WFA)&?5)HHGEH>+yO8HwuX!5s7PjW zdbQi^b)a_Fbvtyr57Q-x9yL3pW`?7=qNeA~Vz+zu<@?<FD0k&BMQcNFMygfeyR?p4)%L-uLXie)orV<(*&Kd3O8vw%@b;n$!nV%+`ki(DKK* zukW|S+>Nah77HYf3Mq`TVJ=@^zy(6gGLG4Bvi(xki$GrJJTc%9Tg%_>DxwUYTn{A{ z9Xt9MYU`wDF6Pw01RleS0ZXuHUMywhfh!{Qf>^Ln zgz=p6>9E--S@*Ri7tm}b7pQZ);97NB)j3WNo60GX7qabH z<BZ=G0czdCo7?sVaxeX}U6` z#x25Z6r=3aO5avl9K7u|q{)kkv>L3jGix{W8Z0O{g>u*RJo6-G4&oz6|PFs>~#vZdpcN7b9-s`k^HB z+91{oi7rBJ64WUuGiY`iL8S%PjJ4}YsEo86CZICd(3zCvo&tqEK-Eg?ektlx0~q-+7a6GfDW|w zf>jkFI$bp3!h%!R@7@2H-h) zts;mS=jwRYOeG!3VXXz0w+lZx)s`A9}+tKY6+wuAJ;Lfu!zysS!NV&tw} z-%C}71NAIbW`npNYuA%d8Fo2LKxH-v@UeDPm#a)N>z#F%m$%5)~@fRDno*LmMXJBfR456NvKS9IZQxhHi*Qrc2$?F zOfo5-b)Q&r0nL7*kv<+#nYZ7^K^?CLb!-s#<7(%*2X(Bz-|l0eir0ZEHVEvob`{Ha zP=os@sNu`MeCuR`z#Uh+pFL>AgR}9Y$5;P80;*h2wGAS5tlg;d8~WSt!w<%3gSZ@P zH!&Wk)_n+6v7F5t1m0M?Ne^eUfB!E&=(-JDE7xwy`L1*Be+{T_xq58iN4R#oUb=d; z?!V2DgEw?rNjvgF51cb8x@#py!_Gb=hS- zcWw8pwyIk{yOSceZ|(f{&X4W<&7E+E-g({jA8!Br_IGW6{qED-Pw#$o_x<~?*#CD2 z%E23NeeBi;Z+*ipYyU%A|LtIJ?<0HPwl~@1_TF&4e!X=4*0q0s?ZdmzUHj&1!8PLA ztB*c)@XoED-Tn05e?0mZ`_CVJ$I;gw=|}0Kqr;CM{=ngT54(r(;g=lzKFAID?fY-u z{lfiL>JzCCr@lG$>Qt~@ycykOcRzab4LAPi#>Z}a@Wy*~pT1$;c+-t9IR5S9AK!lZ z_*;*oWA^wB;FjW}*WZ8rxg|5ukgcsR+Yz^Kn;h9gCU&jkER%#=AZTdfA|dtTaXFoU z{C?ZgCOvd4Qc={llI6^}T!X32bxQ}q%sNdO3IcVJEO#$1r+|1Vmf;~T&ua@D1#yZJ z<+grxg~!RdtXNH+?T{m5z%HuEa=#XrvlbG?;Q}eiI25`J$qp5u+$8jbYTu{Qb;(WXm)=c*Hlq2x)j+}6*p z$|(>pp(AcaHe`>0=TNd-%376^28_u>a9gw6RZW2jIa%*NSe3i#hG*-iR^J5Y6+;+o8xkZ#YE!5r)Zf~WE>mz_++{7kIU8YD%p~8aUMku(Sq?=a$vW9e^qZs zg@EU_y zQ8N1I);GtRth1ctwnJOgrJiGqc)6LtTfv$9wA*M5Mk*qXWJ*Lv$!4U!dq2jjX^WoP z5~`yG(reEZFeMXsMoiD@^5Jya3Cv-yWA_$~#kuBw=c-&E8_{E(3^f|V!qK93Zr;8x zE@##tZcz2Qi#an=yV?XwhIdS@G`SXat>KX5CveZflwmUp6ZBGl=3v#1nyC%uOv`M~ z+k;xSd2U(8SKUVr{RN3P+$tdoilu&FRnC}q zMMwidjb|Za(i(DZbWc{zr3Gp-Ld+^#6)+G{96i^4&&TB~OPY#ilY_kOd?Hl`y<~H@ zKDa7pPFdPObxQJNx+ikyx_RptTfr@@v-bv*It-qt@_m6K_XmI+-T z0Vhm?a7_HH-_F8ja>f)qpBs(Rw+R{wR8#hTqGkZuiL~4qe z?BlKXuJA109V?{Pk{no{wWG$l1Hm`0${F;eH}FhpPS!x4R*=ItL2v79RZi}}rRQ8NKboCG{z8*R_BlK%T~62A?{|~2^y>e_c$AssJ^5# zNi|4sZSDNR#k=U|{_?pV_*@VCPuc_VN>LjqoiT@LosrrAcI_scJWZ#*I$kMiJx@U> zjmIhQ*@}!^9D8I%NvEY1TxY0RlGK7{ln$ZL+(<4cTdNamt;4jM+!Qzy5XyK6PAun+ zQeV2X|7W+J*}CR&m)iO7JKwQW z+u7g#&~|hC3se6%HB7x`>t{hF=YF_*(_QiKsoPx3u#lF);gEt1y^%*GKM^l5!Iw*k z{^9OTV!$i6tcw7P=K!M^a1rW4hZGhqU1uhdwOAy#)QbS~=K#ks;FVu77Xc=(O}Tqv z3@EZAR~9@GB^tp9n%BnbGEBNLaQ8+rpbv`!!Xmp?6N_kAZ}Z7R(*+DmNsA?SZx{p4 zbc3B)VTUv!kaRgBpyVvL2=M$U4^~aKEp9}1)ey|Du14_vX|v{}%S{}p zxhBK&fL;t}U3E0RsL8oR#oRqN2JG}j9&XKWV=$rm;9S&6p6)IJOx`1N_j)Tp6j}gh z4{R=Ju;h)9YVwBWBEa*C-Hid;?xbnBtx)7~1QOka`{=nXoxT9DSUK0^P7KJ`2zmzj z%BTwBB9qM#l|1-f1bBX?I5FUrw=Wj~o}Vf07_eGXJd2yDbRP{zV~?MyiKbixcz&j| zV!(k4E+zZXRE=1#(Ixw+v?kzrwQ0tH@`x1&D2q!NEwLCgZ6v>|FJgFplan&vB z1%QR~eb|True{#92=M&WvsMhRxcc0|t|H2J}P5#afm@>C4S zH3A&hm;qH)T9Ha4z2qnHMS$lw`nSb^)>Zc>7Xc<`${X&z6#(A2tzY$($lWvJntjDX z7*j<2HX4Z`HJc6A8@>E_bueN;0ke6_p3oLd5A@!AjNxknCRgG&+|%Qxa8^(34-KD~ zE4^6<2N{$e0nApod$qXckm^UG!OurgM;+*B$QX|lyl{ohogZc`22@%j%Za)~U(W7t4kw-1w}$K8`+z&@D9jK_2|XTeW-JZi70U%B(EgBSyjETpE@M53=Ze77|l zAjw<03;Hm7esvI5O`cLtV;nfx0vQ?&Rd27k(`V1C4IcwiW)M|jX@*D*duCAmM)C&f zB8KPp04@fs^1{4h3O4PICbEx`6Ma2B%%0anHU`9MZh*IJx1)R9ycdyJ@=od^hUYgG zCI*bgRLF@Sk};`IY=tHUvo#r>*F!o6ywWZEB8KPHhKd2NbVj}a@SO83caMw#uT0N& z5#V{Xsm6d;<~q9w@Vwd(yW-JPS3AsH1bBW>@EGvQca9eUCKre0_x~%m8e6xXy7@yl zUvXn}{1d>cKEL+!*G`YV?(kn9-Z^;B{>S!nd+*)-on2<3Y)GiQM`{&-P z&Z1jeN3Aatx3-QBkM{PSI^4u_^${P8t6geS{_{Wey?<}{`}>L)^+9sj0mJdFtyJrE zF~ii7;r_`6dGua%;b?{9s~tG^m)zd}`uO)(T*NS`NB7=%7A_fn%|nKJo4DpaV0hl8 z?P|VzOQ!GprRDGMe0FWVCugH2-@6a_?r!4R`+)C5N42YY?k*YrJ$L#0JJ7XxzTs@R zWXzW)14*9Z-4vp_uu`owVA%=Y_MdynnkgWZg1jQ{DA4olj~}Z z+e>!;$KP50{@eR&b9~iVkeuloI2=EqxO%j3HN_>jZ;R*Ax4n05hOa*BFMILz56Amp z6My9g6k}(ts~7cyCB^st@5|rcbm3G_uJB+n@|P4JS&TODcYZ)|>CJpK!+0^e|M4Zm z=Q|g5;yH%5&%7nWFMrsHhnx6IKVbO48~SP|#KR@McYoRP_y6(*7m-ZvYhcB6mn0uq zF*oqAen9duE9RByC*u|KxjRd~-_W>-@3~HW^;z$nW^Ul2{fK5>c{+$S^WK*)DSqAJ zB8um_^yI9&r1;39zJZ7L1Bx$M)UWKycv1h__LApc8eGKlTvxvFtdkh}{Y?@}UOe(!X1}rI`wV;$-*f%?hBIf$_mNF#0}t~Dd>`3_uI$-(6Pg)Irtf;{BBtkh_T;Rc z(15*7(p8?kh+#m3KuvuQ&m+@vfyt(8! z{tp*%JlB_Be%4%atUO#rcQg;uVtB4Q-*DDAH;FcIyMI9Okx6u=a>kR${lJp1f9)cs=lb$B zXVy98+`w!9QRTdnW2~I*S1vhrKW}Z0x6j_b;{%8|#{s$ai+~Ti{_HneN*En7N zZXT>n@>OT`q*89+{{N^_UfGXJZcqR6^7q?+cx{HSK6`rEjW0QF9&Yk_-~q+ear4Sf zjL%7Ldq+(4z~-xu7BV5f4u&ec0RHFp5tG+ zE?j@r&a02#dF_+eesFJkV{-E?JL2vuZ|&dwxohu9{m3=#+R4#>KKjw4zjpNW(Hjr{ zV3$7piNn8f*t~gsm_6J&_?d%mP5oZ#uk2x|x23*f=R-SRw|#Q!XKsD#LGK27t9MX7 z*x&!T{lByIk^RB`TlQ}pzyDTw?-#Z{wfDZgFWmk3?gw_?b>rXO{5v-XS6?+`c3Rsf zeO{@HIF+t98M?uRMQ^gG5r`J%8fu8xZV~Qea;FgAVA$aJ*H#kR6b^U>lhbm&&-Bs| z%x8_7DU3>4m~{}yAvqnM22@=jkN?|B0va-uTg_u+dRWa+d=t)q%;2syx9CcVn@~`` z?E8V88Fr1GcdjHVAYDOwg)m1XC16P2{Jmux<~(N#Y$lo z(xhBXcP3_Wlq)w+2|h3SW-CT*N<32Z?3mAil>|{{t-l06NZ#ceSgBVwL1ASsfJ-vN*-gl2qRuX2j zoN3i4iJ^sI)Akf@SiaM%Sb~_7GlM(oSQIVT<|Ex>k6yWwaKv=J3x{sT9nmcoi9nWI zdT3??xS|qxX4LFDQz%m{Qw!tp!JrdTCSQh0r2=B-5SEXK=zw*-+@#M+1>Wj~69&o+ z#_3Zo-2Fg$2Sl*yL&HWNYEbQ(A65o!LhrX1{Gtw#PIpw~qB=_WqJp&s<7(Jhk*X2e zd@%N>Sd*;e13#F^tXFMJ7&|XRaG}tZ8+xl%_Du{sa8?rCLJ!XI1W0|Cjk3dRXw5(x z^g^bOYfd_H#;|PGG1dsPBlkc&UWv?bZs-@gM1vbno77;@YH((AI1!D4(8#C00kQ>tN zk&f2zeABNpZmAlyrPDc-Z7S$OXqTA9Db&kgrD^Jgm4t?>LeHaA6C0|Fu;u492npQ) z*zYFQnNlhBY_e@4)4W$cBv*PRfj1BZ%O`TR3FH*mt_L@wQ$|McG?PJ^>1M|4z<6gi zzzuO#h1eQF#AL=<3V3c)ob?!KkP8WJ;pa=h3%wgk%~nXF$hd-Sy>BHUP~O1t%T&d1W$ibjvpdHZO*K$l3cJ z8%zAbN`mEad5+>_e%xU)^h8oS3qL2=(v-spoEF-}ngNeak;Obbc*RPB1ZF6P5Y?K@ zjeQA9iwi`VBEl)!H}nu`Rw{4_M4ZfEQ9kuwRuTnx;ApT6w@4}<)hXL-+KXw+Xd(<+ z04q!=$l+i#=;QKq=Z!0ge9jbV`H1K1Ln_aNIz^y4_SB}JK{+(?C1);~p{PjA*xh;k zN+OqQSMnU9I3=3FVBkjJ=Ol>>e3xnmQKdL6DR8ycvubm7=WACI>3k%Xl~WsSSw+do zbw=ncN2?hSekWb-xN<32K+W`MRIQTRxmdzq_5~S{|_H%ze zpz{G)^p!;>SDVkYW=B=viO$0Fp+C)z>;_j@RC@FJBG`Gys*Y)`V^}7500icQiH}2n z&@aspKpZnkZ$~SM3Gi1(%mUaSr#S^Zt%Ch_Oo(AeM{u66YPQP@ zI$uo}eLEFAkPw=hotX)CKUdZ{6jc{OIiODINwb+%1+_?Z%k_w=LVB?CbUX$9P@UJa z2u6hUKHdjrV2RVFMVVGRsyCJWnKLg+rKQoZ?hvagLdBIV+TvBcgL;#0PNyB2mS*|% z6o#q;d^8M8G&qCg`(o;MR#l8RX3=tr#==1j!ojhu>xLz`-@=d#lb-rh7@Sf-jM0)C z+*wsI=u9ar=(vk zwJHykaaBixg(%4LY!}4RYL?kNczhBL(|iBhO2RGMI4(Pw?+$2Dw5w9F*9Y!`1jtb@ zR$E+Fi5jzV&Z&F$&fi)|G%SB?jLHkI(F=uES^<6~h|-yxAiNaBj=c#F(XC>$qjiGq zf4Gvcd{LO0L5Up{P#kqsu?gus&!f6JpEgg0Li@DUl{2bsV_V<2k}z}1X$_OXEG(5z zdqYv}pq=5wffre)-!@5eP@OH9MtbOrd#km_*M(kpQCv9SiH+d&A&J!%Q)yhqsA?IS zFgn=}ioK!LFo<&Mfv&63us(8ND9d*8;)ITR!(6NBAUz5*-BF>MOS3A}DV#QCWAE!% ztdzlNIae|a-RWVqX|<6RD@e`O22SRb0~yPrI+QUdbz;_;rvCd%LL4Y{tqa%T3DZ_# zTdB7o!_AJAVz$@JEXqZ#z!{^~%;bWd&s$0GZQV?>ZJa6VM!9LH2_3GOxj+ltN`35y zkO0oX2sluT?d>mKNznP6?6Fh))DBK_K0fTvZRoUZ`ps4csh9#)$I@lKDB!tZ=eJf8 z^-iJLAIU{{Fv@6bH|^6T3dsl}&(dbmv&jKmuvE{}($4jF#}a+;9v0IwO0_c0fKW+Z zXEd`X8a}{0((5pVNmCO9qXXydF?8Kt)lmydL07MeZIVDb?WpbvB0>==Su8?LkB-V_ zkfZW-7%qy}wFm7eNCl{&)P+nQ>AQ8{6xu2IVUf&xJh;fGu_EW; z%nqtv)hO4x16i-bVN-Ot3`@38OP1*7%u%Z_syLB-d}~#QJ<3T!WiHz;5Z3F6VSiPuFSQHZ#z{{{t8`H)FMV=@qM<;#W;! z(zYvTyT}HWCMwBui^S#{5oPGR^4%ue%GK_S3fNJ3CDH9pz>%w1BIX^`Y!AAnYUEgI zzu|;kUKOlrLoiB6y@W69gIBKlN|scCF9dqM-qVK?%o}2MU|EC0pxGv+Qz;l%SX7WL zAF_|XeTK}%*}arltI%AYve1Z**Z97i>EVn=h>A+wT2)wx*pCco0`h~xjKp5Hot?(mEDz5@K?b3dQG9(ebkJG0|}sb7=Y$qA0IGv6M~P6B>re*m?fu|hAO`DW^gGGw))N1 zW}&}=CBsbEc*~ANbywM9~oQQHpZL}y23k9SFLX$2#tLyZw zaWn$i3YIN@G#*y`pIXgqp#c27tUBL>f@QDJPRGshg6|fp!(yN4X=8Q{f(Z8AYANGZ z(r!fc0`bG6YO4r-86|kz)}{l6?us%3}^%FI4`r>8nur zjpgKAhRWZ-_HnhjBQZI%%N`m7Q;A;>MnI{$eFt%<}(~R6|jq8?r;CiDU<=9F~^X5gi;B-~8Fti5F%U1T%K*(qE?b00> zWVz=u6v+3`xT9q)y)+DcR`ET{snk%x)WRViVbOvTM>;DP$Ay|^4rHi=5xr4v5zq^- z58|@eokrB3BkL*qvgttC-{YQHOJ)D7cIwFnuSzKUav&e8?D@xq?*5GLESqjki5wL# zwI(k0@w2e4L?Osh0p`Oy*3U@+rdK+>oNcjez1eD8k+?9{1B5|^4@)FW+6ioHVvVikPFsjMuJ|NKt`>ra#A9v^YniGe z{-@iu-5382t{*LX*}C{I-98A|v20EZ{^D}8TnzpM_ts)AcVT7@7*FeXw4mpWe8Yju zd@$$^m?4{GMOrnWMO<7I;r=sr?)i+!>lh5kv+~%hLggK%IQk zno$|WobXYfLTW7PjPpf)DlTjvVrz{IOSeI;0C6xV;+@{eQ15ny`CV}5v0i45Wz)fO z^W5a@?aSrn{yg<$)19un+~oehE;orcwM(b1 zC6Q(~9C+6J!JL2X^5lG)O=l`#0%z-)a;B6{pXM@|Wzp3{tw$pd0Q4izd+7$gJmg$4 zqt>-xFY9`_8-2c#UY?sDd#Xk79@{Z1DtL8jh)%9N>a-lE()Tr#qDj1!?F)A^15+Jj zaj>%jke73+#RoNdAsQg?P>-(-<>)RPH1D92;09Q^TPoJ>h~P?4gds_kV5C}V2o5gJ z+T?fVApZa9)`zxkz3-NE^MBm@_M7aD z|9azZ-KZY_r{ixqMy`MI`d`2P77*3%uU)Gg{p!*Eqx|779X@w>_uyj(_YS^d|6lLF zbN}^wKeIR8d(G}o?v8f9War0s{GC^9e|WpI{rRaMOxdYp0R7~Dl;^)-cUx@jf5X+m z^G|9StJ@`&amb)u9mJ`3JKcQ6GVG33Fa~yRWS8^RkfwPyBP!2-{@SoI64@@>afO+i zZ6S6a+zQtx4pI-`9?Nza;AGI6>ac)Zc`@vJo;2HA>%-Q@O1Usq1k1H^Dm~B1 zLuJbM%I$R7^kAW$9raH|Qe*lHLwWvY4EyeN*lq%QV{KTc>d&%fks{|cRftI6_1Ot9 z;2}W{ScL3M(H^$~TXCrT&{dv4j$xl$gY8DJ*JIePUjx?85OPS&3EIKNWFs1IQ}7s- zFG-UEui-vIj}%=BE7M#hS5}_C7Q_DKHDHa3L}x3VN!SybW2qmA65eU`D=qAF3iUCx z>x`(1BAUjy17MG0*sogyc7uc*%JYXY>|a^~c7x;_%JT;??7cN$H^`%*Jii~qe(lwn z8cv8dU!fL!R(9QNwg)V2RHqxTr4l}z6JobGSHdCM=LagKf;C|;hJAJo*bOprD9`W4 zu+OXkyFua&<@uc$_FZehZjfg~d44;Fedijm8|2hbo=?TF?^pwNgY+26^II|O;_BC8 zCm>kG+#k|<trtVb^$;x5;}y z!Vp#8L%g!bbuavW44C}7Jc7vx<%Gn@> z?XAIfgBMN8SrEf^*I>KBDVVyPDZt%88IrC!J_Bw0_lmxc0 z%*4zGj+C|~&A0283}>sM)E>5u5H%Sbkob_VZ<{E4_$nl_@ zbz@k2jlSK$GNYVzV%WwSY&S45DrZg%Ypnsh!Tqvw){bG{esxNLOEz1cMtyA z!8aZl2d~}#t^E(~Pl1>J-ri5|ebb(~cYF6YfrtNOd%FFW?Y-1br@kp=rfzTj=GJ#_ zmAA^fPu<>r{QXaUYb*7UCx4^xImFLw4_r3F=OW^mydX<5NQ%y-K5>a)uAGdCqSCsk zVRBUxQr~~6U^1u=A`lQEf>4}P>O+?bCasjT3_}9QKY_5R@4Hkm`6>*!c-92CMyY)2 zdoLACMpdC1zDjDUgy5;~Dek`YcIxr-CYKB;p@}3ynl(dntBuSi=3{oJL3Nrj_$2w|!QiR-DKzf>@(a1g5TBuMI^ zX{mpGsbEs!FhZ^&oTflj>R(ho*Moh$@YowC;SC{rBnanGKEXZ zv9MmHSyV%H{IdGe$yJ;nxf+7YB&Vc)_EN#5!s#?hn+7Z4aO!6+6-+9e1X2?*z^=^5 zsegW{U{c`}S**f1$e*F6e)>|uq{4|BYA|w*BJk8dyHqf#a3ZdOXIp}fN~xc^R4}P< zEY8#npg9Pc`pHWLlL{vhEUlR}h9Ofwaj9TZ;V@W5K<010Ca3=CrGiPt5vwW-mK%ht zr9N_r!sU`Fv1pa9;t-1{$e+DTEU9le*orw_kqHC4gl4XiloJeaK9M*{WK#d+Qo*F2 zij)H5JkC|w)Q?>%nAB5NHB7n6s|J+%@k<4hdI~XBL4qKh5K{m6Qo(b23Rg9OX4x8+ z`q4`TlX@!1f=RFtj}fUKxl}Nzrw~lBj3O(Loci#kf=N9kFp4E;(d4MqKe|*fsi!)@ z$)bRX43ql5E)`7bDGB<;L?~FSrhfP`g-ebd>IW|sOe!2ifrR%QN21l#4_qpkR5J~k~aPaxyk7~@R zp01six-#KIh5qoA#!qMIZiR48n3*2Jqgo!V*TWuiDsW}nX?5h>WNZ)SUcXw^TTWIm zN8lOHZS|_GZ`Y3>av0E=a!_Iz$O4by>h&Cl`c1NyDZykH${J})<1L)voG4IF#LG3yQgz`jq46&x*~hS zFzdPaFzqq+Nw?m}D3x4&k|s$s3K~VTUdk4nV#O(_dS=uaiQKS7dBVJg-R?;d+M4z7 z1&Q)>V~SE@q92aG@F52gvFDTVFsdv((;L=@u4jzG&e)K_8%lnjnL_!Qll3Pfpp&+i znU>wta=GdnXt5wOZnr)Hr_Dt=9HTCS=F_)B*ap!M0`OwAFB6*5ilKPB*A6=x!J#vEG8ASYqhB%#?$-Msau{2A5s@oHnDMi-^r$|GxIW^T zdI$J1>eY~=HH$K;nbTtWq>X1xkZa7qG%H(%=I#`(<47UkFjYP+3Ja$1x0~#+YC|RQ z_5yEmt*J-%)h-Lq`o<_3&3OAeA94t1+DtFxsvxgQu;_&?67ChceMOJj6fppAybHZ7 zWCtEY2PXzD(AnO?4eH$~SsLX#{n;Wj3EG&78)dSjI01yQaJSIzxwk={mRf&7wI_lI zgBSuRnauyP_17PAaN)ts?Kd>5)WNy|c=$(-y5v-7nM|uXVJtxk3hi+p<)-RM-vex7kpQ3{$5d-@D(b!^fN9p285T1x)5A2^gI%$$w|p5iweo;pnE!_oIV zBt5?EB`+;?Oxwdl;Oy!)Tx{#0tA;l)y7vtdgy{PE>(-2eGL+lby!tWa|i$gUFdD)s>>hlwNV1x1Co5-}p8jzHC?QxXW!Ch-{pl1uEb^7704>{C3 zxa1a;st}AS`9`yc@Dl=`NFx{{%GsbB473SGk9k2VpVV7IJL_R}@TQ+q!)84kiCsvM zT(1RjG!4xgSh1ixIv76Z_MjQH8hT%u&%~NG!$@S1(8+7Bf5^d7n(8=R?9TbDW+T78gOxc=GvN|yv&u95uAj&l1 zmU2c^PTvj|ebYh)JQs1O#aJUZ8G?TMw_d^_pT_HXwv6%tuSjruJ~I|LU1VB>iWT_I zysw6ORN>)t`DEOXvj|(vwuc^Jw{Z(=o|+Etv`np84Qe2YgoxKk5t4Zpx=j*Vl{RcS znk_WJ!>e8|(GOd{cP|y6qf#eTlb&b=3-;zAGs+lI!EZUT)7(xFk}zcuK*INpKqmud|s>)27?pbOycy zWhcQwde{vnS{xQZoC|3&^-8BbX+{KXcUEZ?god4U>wLZ(nIjzu2gb?B4BE&HF?}^3 zR7(1|VM9g-=M<1jRVTA1YgK!>ETWfNbM|&vm8yZhFh(Om_G+*jB;&*%{q{o+MvH1F zX&TD)Cw7I(I@222P3K5%)~J!MeJ=$n%-NRw;uKz$@-v#el2m zG(VJz-RvkT(+)j}iU`;)0!4&6#MB4z9593)pUYQr@PU`bA#KJ7~BxV zFR9@51xUSpCxEN;Bg#}f<&9yB7Bj{dVMODR`ECMf~k~{iC4&pGInan`=tT8$q z7VEmrSN>5;u)Pbf_?Ox`4sMkQ;3I7$pLsEbUm0-}gDJuEXI zqxr1V!le?f-gbe1cw6h+0XA}xaeopfkJY=s{E!1bM#O40+h%RO<-4PHWmJ_6m_p{F zZgBJ>AGnACRxXY-PUe0Dq6ifz-Rov^M87hSz$V$9NDxe*kvztgG|J<;?xA`0cC$BS zIs-~Icsg{%@#tJ)ql1sWghQ#v+ugRO1u&K#n`MDIofvMnoG#32V-(zC)x{3fZ`zam z$%tu}Pmi3m7M@3BK!a?d`7#DCJQiNK23r`zpx1rtblAh&ATI)3c1~t((VMnFV5CKTe;T44YUF~H z%^fm-8#uUMo{;tWJFe~8Th*1!{_x>+!{kMbDz2DjU$lkZ@P4>9!AGrSR>-FoU z>$i5_5Bvc>eC?aB1=p(AK;FN-H|+n}qfhOB=;&V@eaF$)9_dFJkU#L_hd*%m-oxG@ za`>eOzrXuc`>owC+do{<2}32-LP)F z>Bbiv|Mu~ZZ$Ewft;f+Z3o;da`uay-WCa?swH2pF1oqb+GO=qN$DW2Dkz!9tEC;C{ zi_7T*rxqPsze6?35h}!yiT?t7cr4zG~}NVmJ-h7^xXH>B1b^ zq>zYC4#z54vc`84oGht*c{EK!SVRlP_s7(ySDeHvAI(js{R zO$}G&uF5O4^>eFoSKS1q-o7d)4H%P&;I?MBtC|86a-vsK-@huy;ws-aq{)(W=sP&pMY${%tEfu06FEEy0@`c#T1ziYZ*I7<-+o3J$QqM6)AS-%;-%}5oJMA_agOQ4eBbgGBQL-7S4<5vLHEq#TTS66V zI=%K>0aG`Dw^~L2U-sTSPOhTrAMWXW9|$1?2wTX25<2O9VF`5K-uLzP0!h<-Z|~df zzPDE(Fd>OZCM;oBKtu&ZmI%m$J_v}4f{4f_vWbX1Z1NyrM8xpc?Y=z|dM4>^j6Tov zd((e?KQpJ!IaPJ4>Q>dMa}qfZ-fU(ov1&ex(XwD9P(@fmkql>n4K0OScF}HAM%mMCA2dmqgy@%$xqmq{=!XStloG_+~AXJ+r z=s^cp4STm$65%8gbHj#2(ViSUbu1Tjzv+WkGcfWz$wSR!6a#g+`zlBU5ky9rB=&e^I}v8uC>?6hqyO9mV4 zGg@!x4o!wid%IT+1JnR}Ln0P3d5d0)wJ~^Z>+6b*HJos=Y|W?IC4bIrD?|qyW#Z%> zU7|>!wkRCSLslFL2V;Y6OuK}l^Aw1n4NmzphDa-2Pg)0?Wm}If>S*M#V$9#Rg&UoM zzdg7_(q?*efvmySY9{TaRz4pqRs4fT;Iun>bPlh*QG;Sum(^=3;8Ekep*z!UmMHjy z@}%{}T0My7+aahqI76P;?>jSU3FToFMlr-! zu@wB}d8gJBqAtHkIRru=+Kpk@Z2>3VWy`$p$DY*>%@j#{zT3=^OeN{c81in!=(m^s z5$B+}NNF_23qzLNegEFq0{dFv|6U7pXB3d{JWIGuaJCjohEND~4W<>JII%lZg!4t< z#0-1PcCZbGh=O_WKFPmt|G#hlU%`nJv|6cj-~NB!{$Dz=Z~s5H3pLV%cPmpHjwb2C!|q_F zHY_LU95!pz;Q|Nr#+tpEYd4#N2g6evjw0#&0cWEQlE_s-QkK4V2Ft;WF`w5raKv92ylS7?u#BWD=v$72&6kY?35z)#2xSI$ zT2mViCF#OhX9{&Uy%yjPO;o{+*WlIO)P_U4`Z*EA5Xt9F3Eb4e@JMm+nrLdn!6cm* zX@H~r6dVe)?a2rh%niCMPHk99(pi(y7HX=)wv^LhZC353`R#HLNoP*l136zMM#KsU za}&5$&+m^VB%R$GY?^}sW5N(=hJu+KF~9u|B5rL{wZVInsSS%s zI#(4Zl8`fI^O5@L9r(rrY zs#5Wk(_wID=R;eA4lq+2_#|C}APO}i&<2T}BCxC2a1J_COl{zibj@tNU9wj|)+MxH zMq8Ek;4$pf1};h04*E(?BS_PNBVmWr6$sC_KMqNk3j5I}(XLt>^;XQ>$hznE8=IsH z4RiLG+Q1^|^uT$}S1tt1GZ09nYcUYC7K)m!^X-5^(pe+v zngwatp`_nNw0%x^etqd(x;pNufrv!0pfg$W#agBL<9vdoGho50Emgw9=9sMtBJ|nk zj}ID27aewSH#L0@NryM09#143vx3Z>z~>M#&8zS9dXf&ZAO)`#*tO0 z>9f1~Rob0m*j?~@%Lr!78;tYjm+7-ex}pA=Q`2XXbjd=qol99`&4A0Et_Of0_h5fa zcSyRSo+eY%ZIUi;bm%<Xp4=u$ zH`MoeYPvzvxl_>ul!}7ahC!%WtQ9STo?lbbb&_uAh49og(Ul9*& zcbb%py$uj zbb+KB>IN}2ohRu)^h}Q-)iOu1hP_OfpzPqq`_y!5g1-B6$Xsp)kjT@BO}Hh_#{ z7OTyCEDIOtw8HGMis=T274X4nbhBL`FUkORsO z`lU=wpGMLR^-VfvI>zu#95vKof-^N8B^eHVM>RDaA?ds(#FIc8j)cL1Me=RJH0ZQ_ z%ygI(>`vr;K%ghhSQG8mn%H+l&H7XnnKXPP7KyRi~!? zBwZm7vWq&brD!6WN}EiWZ@zv$lCBJFz*%%=;Y`QV^qA}E`TV>jT`d8H;V^h&9Z8l7 zsTKweI%G{vd%AK#8ssEV$(TA&HJS`X((`8oH%VuYRq9rQzXkb|2*{jUPR{#cn|6_O zR*$!xjd>AAq1FhLjJC$S9o)2&q%(yJ6*HMRH3FU$LM>P2812zePo)b*T42bIeKh2GUnCCx~}_PNuO zt~8`eZI2a-5{7((Xr~+zvon&ht&HR=Myp+`EGx~nj>mx+kc`rt4uQDP*y@g>=++w4 zau6k4t@W$x#Zs+A=o}WEQ(j4CWMMr>i{4ExT_zKzg7zeHQztU1I6>(@iaDGrOG~s7BQNCW~9p-E9`~NOW~Uv)ZTxf#@m?i>cM{gkzaVQKK|fmG+FhUNz=H z>|viBQiFAsoTm(3a%1TdW|7;_TUe_fIKQu2O*vB#F2O)mO+G7ck7DG21&O zS2c+{Q`Nksn#<+vQcFhb)tPgy(8{8@y)scnvN=!M?g2d<%%I(I*Pd80##*=cmU+!W<8GW~er?}GQVp== zk&S=a@)hGU7+AJTsZtWGx&ve4-?U)&gyY#9Mw$pYLP$l>9F7$e?Q)`&BLAEySIQE( z6nw8qrSiB;D+6C|5`|1ok|r7nB2kOiYehg=t{01>dGZ@CMSw~t#%G%F8^2{I#eKP8CH5HYBy_!(A)yvLy8G+cHRioWF+KNY39gIBE|Nm%Oj0Up( ztq?yTc7bSnglJOuZQ&t;+XY7cll&s@C0>=s;a<*N&AFbVWIxJ|u-<1O%)c_XG7n|U zFpi;LNnbhfqX`e~LBM90A2M?ko9;VSlwuz?-0h;Yik$XWA+=P3NUaEC5e0@3?zl7N zLpzOB!UE!~HxiOSO{&Hk1!Imtb$Pi{S4Xtdf|(;pF>6!oWy8HTC^0?T-i?9D6$78L zD-^N1H&R!str?93)3r^NR03CP2@HxR5Uaa|5!sMLosj!oRcU4B2%ya=pwfqqtj+x0 z=86s$ZOIy@n!!<)$yLq-TymxTk!Xs53Kpq7Zm?$!R&5fpgfUxY=5Qc!HIR77$P#DQ zJA6gZZVB0vVU;e~2}vNV8n=7Rl5D-^br=H?8RTu06{&L4fL5&f+{|G>k^)FNcw|Xf zr3nUI5lo6je1)y%thbYSsoRSAj9G694ciTxnB0?6sZ>V0ClZRPQ*gqg$MinXt;?ux zT{^O`J$LKi{$d5JcZ497u27B0WHpD$gQtxJmqCW9ioR$_snU3%bX%6L8y#sSGIJ=Y z4XFA-BWpw1GFV7GlB_}kspLjWww?80CVeg7YvS=nCzEki5IX@zbW|<(DM6(Vp(?#( zxM$Hwwk#X8WwTP@sM>rZyX}>v+wKLaE?|e>lE<)QQdacln~ESTa}_Ex2Lo+L zTRw1PZ5FU)L}m%#mbS%_F%h<89dkE<|DQ+WZ>T)5xdb_C!H6H%H5EFAHZ!vnNF;4} z@yHT)vE``F?~v=V4Lt8f34gib^P97&vMyzkH1%bZF_)>?bwOV&N@TUUnS+2N(v}Yx zS<+{0*=4Hu-FDn=vLgn!GTt$$EIzq26H8{~WsS91kD*?r1#?3POm^!Ms#_P0ENsuV zEFJ9De5}*9gA@<#Ob2sWTvCruBM)E&9~4*lR0$*GEM~NlDwio!=-^ zUch)%{9_EZm8{W=&E)iWr-!#b$K%(&CDDCB$CE6IkLoE zj0Y-Mv1Z1pj1y73YV)VclDtLU>e#IHpjTDKO}3KO?ewC04T0xo76D15@rXy3^cmx6 zNTsPnR2lKr-Hk#C_j~i{loGKSJGi@1iQDxu49zAhx)ku42F9~L*!DTb_ePcq#0cx-;X+?P!y+I0y|C`@*zx#`gX9k5j^u+0|Md3~}>W=0GolE%XyS>i6n<8R{4 zWWAbcNgKLI$)T(VAV&hc9x@>rsEn)PrI1e<2>A(1t&p1$0ZF9sjNt7_)$KFJQ%)Bh z=CZ6~K;W zf%9{=4l=g{B-&^=Xi!J+m|E7b7s_p2Ql4&xVi9x9?a$~j8!{sxwED4qvD;!v|7G<6#eXlij`X*lik3 zS}m1_T(LyU8*wRp^)iG?30bJvg7wZ!E{$Z%85BqKrpycvNFU> zNDC=S;h+qM5Uc~@OJ)PgTBxX~I#3A~bLKqCa1< zBVjdIV@TUxD+Q_s7X|@0X2OQ%C7q-XYBpn}@o>oQ0>;A_S=gS9M>A+VCGhwwZEwen z9kiXt!!Dns9d~B*kgFBNT_(&>D9G|2zsu&0fln|tsST()ePnGY<53eC+@@?HAV9Jz zkt#syj<=pqX%x+LD3>bvRgro=BqQ*)DTpU$SU}1YkTNm+aDg%&qJrg+6%Dl(1PIs2 zwaUE1>x|XP`G!@YumxkNoQRZb&Vni_jkWyQxV4a&VFDpX10l4Lg^)Y$6;(^rV1Ubx zk`B@M34Pm;Pi8VLP$D3~0~Lu;)efiqHdqCf{0@CST`my5g3?#nat_sie;>Iz3pC&u zSJveIpE>acO?W9T+#;7(%!$D@Dkjig*5laUe@ycbzr&X@e*bkrHH z#-u*DR*w*t)h)E*3p(&ZsnLoh(RLj!b_@mA=e`%gy|P>boC~W_b*$1fgS45N`pS?# zmXK@OQmr!DQ90x&ZdBVcGNs4R1j(c7_Gm>{TJ5VU%OK#sGarh!w3%`RmsQf`wgE2W zOZw(0HU0a&xHnJcRd;$d;g}@vL_o1 zw=RgwKH~vEE=xm}!GC7qlW7%--Ho>lZf>en?sD4Tfa_*e-fMxA%F1dfj-v*r%Iows zD%QFOq|=awKKFPtS1YSgPozTBR(e-^P*p4EE~{)Ns18TmSVa@|+8T)8k&QZGtGt;t z>eE4xMyd=WD@*>Ger2Vag{u*NCtrgLAoP&K7;a*@hGDevMpm61Z^TTtJKlij%@`Y| zy|?hm78-B=>leOp6eL{~20RSrcBzAd?%%ZF%_>0N-7nyIL;A)1E61H$L4M#ayv{YN zh#zulJh;~^gPI^wW3_=MOr_@Ph{J_yZD_{f!DS|pUmJYk0Vbcv^ClHUOMYl&%dM$< zvW8;9S*vOcxnc~@gw5{!N)YkQBlYPLY2V6RDP{Ms%py6vGF$`Ma2tq0nKN`EAQife z2)hjSj1DATw3j`VQO6yqI(7el1c(4Qd5(Ce_-xT9qBDgb3)_N^1Wo=9ex3Ir59hwm zt#aPuR6xAG66+mSf%z6Q$9RL0p}$T~O}sXN&|U?_cmG>|b&Z)y`Bsc^M80B-Gv(n2 z$?LDKj;+8L$IYWCfQ_xd7>E3$C{XDtFb31T-38^b71+yNQ0gi$hGDJP%L){`3XE~9 zuwsnE@!==p>#r`1t-u)P=A$T(A6tPjZYxGnAlFr3jO&)Y-38gP71+yNkQrNnz1#)q zt^#8)-P>J|8e4(A+y!V?ficGRioL7=(p6xL>#A{%XNG@|ufIAuwgO}L(T<`(Vr&J* z_{<(ffit=ajB%T{x4R%dwgP*(3)YRTz+UcxwOs|qxZ51(b>#3g(D{!d@srQh&IO7uCjWx@F9_i@`!m(F$g5!9qdP97TcC##UgA)2C4sh;XCKG<57x2F z2bogFj~NmAAL$_wiGLmK&w$D2{$;lyoUVrjjeXU~j|FDWj_?(TrPgSv^&Yo1rB_PR zUU^PpL9Hd1)g1~zO(dH&$Tj)4+U00z^IMWY6mX%Uv9BCi)MtFCps=Eb+hu6Rt&tQR zDl<`P)$9#N&|3_n+Dz0HgAj>bqXfRq$h2@v0!RZsR5bP#BTL((6O~*!a9_JZA6I*d zZPW`D3;9aDBrW?JT(+uI3 zXusMQj_fwS=V}kSZ>$*OZ3ea_0@UeR^rn&3S-_&lc$|^l5(Xl>7QJy~k-J#*7_T$( zTVNonYtiS8Eb22BJi%&%Dn-w**ObNQ<5xSsluv$9SNDZ3zG| zT?;yAWLN$LENF}u8rdy=AhK&g>qi#3iv^AGL?ge&2Sjx(=ud7~lR;w8I` zT;W~(6=E&+$7Bt2y4Qz`Wi0N8Td-x^9?mg|tc&3rv;sPSO z7StM9LaT|S8 z>*sy2k=z0SDd52dja?d9%D68!@GS-)1iaXwv5Uix&-T`n4L;bjYiTpI$*)hMlM?Y; z;yc7%@d2V=infYi(E-BegdO2B!To}1fnC7iKgKWdrM!1|-{aMI8t!x4Huoehi}MiY zOPnvT-)3LKHnGL5U$8D_$(SE9f5_}GPhq^mm}VFmBKo8Bi|EpcUr$^%v3BAR+V5yH zAaCD6-Qxl^3s?#LC{NXk`2jT1kMf3WEqx*fm6O#3oJ=$=^?@BPRd>pG<8i&)=s!n3+Z|I{(v%|81&nqgJSA{`_y9!~(MhknD@O zf^aYeMXRn@vtVruX8)nuVb>{xdB{4@h-*hLyr&sJt{&W(F%R3T><4#1%!B*=HDcB0 z`ipsJzrRK-I={b|%liE_B8y&sF_-rH%UXbI5q&#tDVT{)0OWI7*lu_2FRs7epd%LE zQ-7%xyN&=Yh*-`I?N#=JXE0n}zrRM*ey+c`fqs9DSag1WafAK-8j(e>zqsChe+^6a zv%vUb{kq?vBNpCMe<_rM1Az5Jzd=W2Ki8nFC;JULV$t~x%6h8bpd+&AH7M&>Fqp9P zV9pslrs-EBmL}_PF7! zXZzg_WkRKxrx`90M!asWMh27hEzs@E6Z_phV&Oe?yIeDPJivtd-994wxo&5!>392x zMdx=rQ{V6Q5m`|4o|0-%4`B#pEvZl-XLN=A4x1-9!n}7O)u5vm-cy5u@G%1txtHu! z_VcFItyF)Fs{LGlaWA6!Yt*9i`-^)q)nB8spys_92z=2@_CB@XBGfE7P<^P_tVITM zjZp&t49yxE#}YN79nX~;IU=Q{%lmx}KG#O!vFln#U)}GN5ex6B&t)q0K%diB^*d!m z_H&&=m-ahl#G>;%g)ZxN%7`p_okExN8#Do#P@_JVAh2c>Nn}FFf$A+VE788a7JTfT z26}YF!h32^&1b*$!ZDGWgnX?7jclx$4V$u1wK)bbX z3nQ}V*#hmRQ&>J7xfdF>7#V&q2Le3CyH7f4x0qO9|2TSi>V4>E-*3`x>sw?5v!IS7 zczzWMLRl|b8q9^bfJO3z{UJVL;XPTTN;}xCJVC!(M`Sv?Q4Pm^(_G6 z#a6nn#X!8+FbQtDS@dRN2<;>(4y4FbzTW=BzGfOEx~xl)VDTA0;1+B;3M>fpWD zrrb=K9C=@kPwZXC^ifCN0+E`@hmGAwf^)H#7kKN~TaEvRMn^Z60Ej<~=PKPW#mesR zA*0<^_Lx>-p7D||4f{cq$yh7@!pD(1xoWqMl8siYH{v#$h zmaIvowmZU}dK8I;o3hXUDA^ZH%h9&KB*U9A!fB}!Wwq3tZ+H_vs9ZN8NWh&!v~roL zg889fS!;)3jk{r1RW)t7NfR_W8;+*IP*q@dlT21#sVymL1`{+|G!C-r{G1TEf3NDAt zjzVth>X5>gUYRnEl<-GRR-GJj#hGGv$bq;UW7yH&8*-}ulS6KdR}>2kxo$jvu$uf2 zguLme66g*%Hn_vsv5$`_Zjj13rOWo4d?rhVXHu*v>$Zo3EDo#f_-Lj%; zDCLi3)5>DGZU6k4H$Y^XlFUFzwUJhYaD&7*X`->xtgJynStS+4>kfOqQLf>sSf=60 zIZPnrPRXMPdR@VKS|3eDoN1#;Usbq55t$(h2AhY-sY)X*ab(rOc;hqeG#Bls^hvGo zEc!d5n?((gQM5?>^u%csU*vo`@n`y%=pn`*8Fw+JnYXd_XWhiAb6D)B#B11JXIFSD zxqsvS3}gn7F%Dx3Sij|Hgs)6IFqs$MFv(_kgi8gl(~lP1E!Z%54f9Oq?*v}vLA1XL z4i^i#h!_*vC$@6FD!iTl4hv$R%>M!Z%e0?PJ}!EmALjg$KSlpO?*rbCI62-$ytU#r zL${+cMvA@yLtO?*66q)!XvVL>Yp^?nb_~r#DP*$4a!01#EOvr~O=(m6cZ~a!Z{(`VV%rtBDhNVIs#mVB8KqX6P0GBqC&O+n+K61XS$111wF;8MlO2!QSA$V~qNSCZN_jkIX(5`n z!^FRW(yC$4C?igX4GCfuy$ME2ZJP)3=v^60Mx)l`tRZd9Bo9fmHqJ*qt@6frB`0w> zQAn}6~fI91vdJYNNJZ7cJuVkIikTSrcQqczqw^1fdD`Id(ZI(F+ zV+zI5A(tV#3{iKPOt!o-Y>_#V zwIBiog5VA_ZekUCT1m3iw5yue`V=uK4(x}JcVuM=-fAONg+Em>fIL1bX-gi``B;gb z45utmatEYH-I7lkV7TscX>|5<)b3Vm74mqbP>(ugC?PKq1@=U*gdVPXn;CG2ilGOAP>LDoH&B7?S~kQA+{Y>+H>!qqv<;{c0xhYK|sj1Yl5?Zcy5>dat5_N+NL=fsI zBz)|nm|&F3hK|%-33@VgHf6^fC~AGNbUk9q$^sUZT&8y!Y*@o0L7h-R=L26n9+TO{ zcJ}Jwj+!CJXwYX=iL6}?$udUFTK4CH(Qq9~mXH>tvKrcMIFBLBbHR302J_VHj#87|D^c-famZT2qF$3KB2)WR@&L$^==Q>>CR}f~V?K>8 zD);2dnkK?|s;8ANUaK1Qe%NT!#bcw{>PqD5A<2QFF=Ok(g}kJ9{N^ zX}>}xZE6iFcgSEUR-0)_z@yK?xhR_Pn4}I!Vvn|ESRveGe{<{--{{pm?$d_Vgx-xd zV53d}w@W6s&y#88%C(lWK!9-aF?l(bSGnpw_GLX8(JJV_2pR!lbmT=(Aez+#CB6a( zxSI|c@^H3XX!~;plRbhK*z2hhN)w#P*d35M4%>CbycBioEn1b&;`f?UR%IUS644mi zf;28Vt5*V67wr+5Q{s!~6iUQ|)jOt4${w?KyfVAd8_db!AW}lWWwnp@)n18^Q4QIe z%5t@pE;{YOipd*JYJwH5&J!>NwQ+ed5sw?|1*0)T`*W{EAt+a43S~Cn_t$D@NP)F1 z@iZ2220AW}OYfBD6DgmnP;v${oY%W0u%aK#Ns)}I>c|$gfs(CiNXc>q6m2-1Aq100 zVSUb}uch+s>v9yVRkoRw!|ijV`X z=X{BDwAS{jVI%DGM2sP4)t~U?h-5&4;evB}CD53>qicmiN!SxnNdpBmWt64~RkKmF zn-O~)EFJlJjYg2;Tq+5n(xf4S4<@SNR#h5_ z$J_d}M@fICTLP|vwH7{S_RSq_}-DN?Wi9k;dJjoE)L=N0oprHVEXmGT|kCCFF`CMYtlXrsWouR_;Jj zDoas`#%;M+!ZxwGS0dxL>A@G2vtg9Q(W=B-%4!^jhRUncdu4dK=8Yh&cpD2V0=|iB zdL`13BOI(M6m__z^j6&_M<-s8Rx*vCmGJ2UhKkf(Z#A6`6=I{)x{EWG2sHE!2Nnq^ zt4f^`k%I$FomrECT%8tTit3trb;4?l2lR2}Nj(wqKq=nVwd^r>Jr&S8!Zo`qjR!OG zfW(V5Q8a~Q@sQk(25mNazE>hFGvO|ExfayA0`W4M^%s*7e@06n$*LWWI^EuYQmU6I zwQ}tQHE7L#RG~9?oNcEHsT&hEET%I0QBNHlOi2R%w%hKn!VB-Q`N*Zq& zPPVn66BLplc%G@&6wzu{S#(&E6^%>*Qst_0ghS6fs8>RZC9JRpYu5{MnZ47<=yFy# z(z1Az^>mOg59V1bDwDOMW?bsdW6i3MenL-% zBBf{{O^sUWbrp2>Xx!>i#iK|WS7zd{CnQJ8nOLk70ZDvp^qYDmyn$$>1>rH1G~Ox@ zx=t%5Y1wqRtx+m#;fkajv4$*)coHF$yxY1ZDmvJ!iC1M7Rm@rUn4qevmI)XLR9ltl zBY}1SBbr%#Bp#9Gq;#t0u*GM!>U92;U)~Oy+bXD-Y1QotZ6Z@?>Pr$N4moutRaFg1 zg%9^+6oPIQEX9mEG?=iO^KBQHu~kLPpD{OsIcFknH5FiQF4^)mdFS;?SlseXNbXQs zn?Wt80allJ@j@kqRnx`}NJxsQld!7=EIEktuj-XBW6G)}5e$M9DyUuGDq?C)K2y&| za0P127VAZK$k~Y3?W(k%9_*DcS&iV^ryABe-G!(uE%n8~>s>r!sEm|MjXY!s#C ztT$K9&@b$jfSPSfE!ipO6^4*cD-@rUr9VUg}LD`DVsn(G^?Spr>rC#!b98dL?XlC7_AtLW!iS?DNzm z?ieDG`eSaNDH&)f{CAp&$SI@Kw39eqtFk$SzMLds;Q8@%~ctZnexN;QOWXirAlWj5M6I*Z*w6r9PDS?-Gvp*)c? zL&;z=q=x-&r;6~#DrG7Am0k(0%bjv&)mAhVcep$zwcD<25axo^S#M@=UC~l@TAf)( zwF(tDKkJnsG!DB?qHhNB+Lp2IC^WrI3o2nHP|DWP)i3fS2RdCXw=&)o)%onhfK_`lK20o(C(y7{$%p} zN$2E#;$Mj`75l|YM9+w>7OfR66TT?CUWf{h7W_eQlb|3tj{g>j2~gt8c<=G<=HcKK zzz*&Yz&n7GIiGMIv8(Xcbek3p6+XzIFZ|?9`}c%%dAqIo z;DyngaK=?@Htp9f&)Y-G4SLnhmFFB})NPs^s{9~>zVf*xN!M)>4i!GgpfCL7O@g7VKIj-fC!D!dw~0Sg_F(zG>@}NsLo5DSzdfmRPWIxH zAJA>$4i!GgU`{w^sPI9zzB%FCrC-u*VhxY&d6h2q>?4f0MH%4_E&l*~G(22V*cg@B#hc?*Y_?&An&VdK(Hg<*z zA7n5myggL-;P@O6-Wn==kiner=1}2-<8w~<N_#lHh;rKA&^4-ra`zub?ZLAFwK99ki@aj`&{tl!@r!O2$C#&e*KRp#8>|Zy!D_i<1xYs_84RN|NhzaVrBE(KUjYC{OZj_duG`)iv*mb zHXin$7p%vyXl@{;h6U?T@$+YeYOxicFnQ( zv#vT8-g;n{@fa}xdyE-7FTC1UUa5Zl+{?TN99{Vt zymfJx@fcD6dW>V2|MkYNx<0wb+dT2*EP7XG$Nlyno^a#e7#IBJ?2D5B`2Mfptp{`& zD`ItTq^?w3Ga3n|Ynv*m1g_Q+7!*w)R(A^{vLT5&A@{qg(jMcNzIxHRcTYH6yz{0P zH-7!~Ti^agj<@-mWiRILIdILJw_PH?4c@w_%eaF@Te60!W^j~ca+Naymt1LoB$^_i zf<~^?=}$jC^gAbA|76^I(fwz?eC_tN*JJS3 z{kn|D2*1~3y!_60AG!BKaCR&{;PrY^|EFs&JoCD?@X{-N7hV_o#<|~Q!doZ1jA6Ya z1gUg|Y6PShc9=YP+E{QIWSFYxi-v#$kQYj~W$C)nkyiE?2kDmyzJ`AL?E0-=7k~G? z*QAH#UJot}TzKY5$G-o}-_QHvPI#-h%Xo~)c|FD#EO&o=*iON<=N+=_>nB`7yKRN^ zuD^e-ULqD3>cJGtf;**{SAG-XD`+wuc zD_(du@P{80&piDIyj9p`9B#Rd^}H|Z51PzMt=<#V=p#rj9|^}01CkAxwDOEilF%B7 zoUh0Dv;&|YD33D9Sbum>?Ad-$?F+w&UO7=)yzzr9_iuMF?pXnE6?7RpoyJ1lOnBl_ zRaBpk=Su#dR2_4{?KbWRY2ZS}sYe`|0B}^G7$3j=wR=x4Cpk z$q#%FJ$L)Em3M#nIl!3TWeht*b-9P=s2VAoJ)(@6YleEHZ3D;O6~E7D!y|2#M22}` zlZCS5;P+NN_cs3eU(cUffAEs+<@oZW4t;C?Q{~6USFL~c(r5Nd!drP=#wJ)30_m%9 z*c9}7{H<`Un5;$|T8&?00R8|`A1;s9w9$GgENS+dV*hndZ#edypz&9_W$^9rTc_V~ zqGh{X@cMptrC0uW%Z;bK18?Pa83&*`@czseysd&f1tI0CFBETNpm@cQce!%Tq|=pZ zMhrm+GG%&@0VpJ&mJ{F0iQnq{hg<7a(qnT8oq7W zyT5+m(#w~ealvh>U*Gkj=C9|#TNz!(V}xhyF-D%Yzwlh;PQ^>#JNOO#{)x^x>kfE_ zcFn1;Zn*Po7dvtfSo7#z#$$wH>@k+w_g8+Ex8Ic$nu|W*TypVq_a0mp-SkeooO4n^j}b+&$9Q#T@gt9;h4(Kz z{i*PGpWb=sc{|QIVtw}N%{!NGmgHZ2{WtK;IbFtp<<#vUUImwRSjZlt?D*ghet4|n9=lEb{3$!QS0%51{O?O{-E3HOr~A?Ku6Zjs zWrSzecNvcngt4dbACLGkY}{WR-PYtR4oF`AulFs=<1bkG{FlG?`>QM8x#vf>!82!f z8IKWivB#LNfAb;wM?0TA{^pJAzV^n$FD(1{;jjJb^)s2vE2887dg{9`z%yrc8IKWO zvB&rU*WtMf4>J6rvaPWH8qLeUe~9t;k1OAJ|I}x%Oq4HL$4bL9XLcEzs+oo+VlCH< z2)Ky_-bdlK$7A#B<-Tkx(XLByLSeEy%}tMvvWi7Ful`f@zi!n`d28-c@j~s!Q-8}{ zS6F%foiF`l#don2pMhsOUB+WXM(k-Ed|~Yymt1uL{;hj&dg-Yret+^OHy^ht{@70s zIOMBWt=Vh+NG)^QZLFXWjjzlh-AX8Y&8f%`RAl2xm;(IV+9AU!c9HK zn+f?>Zn*LXYqQsU_3vvnN1nDc@6xT@qE_v_wkGvq?e0Kkz)31B{oU4smo3V7r z_u-jFm+=@O6MGt8`#1ZNvvJl#k^S!HpL*X7j~)2&1(~xvm#bg7pCj0L-^B<#Q|~e! zBk@y@v1;>oHox-cOIF{PK!5b+nFqgp+9TV}`e7yWp6~LXqEDYe-v-YRUB+WXJM1yO z$MycPU)uUx<;BL@%J;r|OYxvEmv_P?QU8NyUv>1=>Mye38NAEb8_-)^?l9tRCJ5jz zRIC&nrh+;rD>-Cpl~S z(<6Rx-jROpP1k}+2qarvNnir54^0}`wO#$$v=>@ogyZ6OL>`qM{<(8)#Kqd0Tr?;RIBeP!o@=3CKKN8cO0 z2%Z6n(*WZ!!XWk-FJ80sjE6SAvf(Ss_Wv!uym0k#)AEZqod2KJ#wmA|Xxo2uDLexb zq5;NZJRR;azUn{Wv#z;%Qd7I|r!&wy53Him6-$nn+V$I9i0%OH$=AU%Aki6MJjO%b z9^<#3JO1dI)4gy0E%zq4{<6G-w%5J!^s_I>_dgMy0SU|i<1y0q^cX*I z$6-Ho@A%;SM^Cz^aNUVN`(5pT)JwOm>-^%}9mck0$?n^7YA= zCZC@C#pFYi-=EwLP6V!-ynJ$I^1R8;WOXt-8J~nFU6aPi6DQ@9Uz|L0@}NoaBuo5> z_yZ6%@b}{9fak!&;vb6d7Jo;4gZOIk#o{lC*Nf}oq8Jg!#6GcAe2Q2tmWWq~4;3#G z^TagKUq$bV{wR7;^pxmP(T_y;itZ5ID7sd3nP`h>gQzXSL>bXKQAp$zL822xGSL@A zM~Ie)L?Wi}@51+mZwg-#{zmw?@TbE2gm(#V5q@2GmGB~PVsW;R5Eg_!!@Pgn;!6SkP1^*$qUGOczHG)e87YNQ3Gz3LKQVsc!mEgh<^AG|6btz@J;?#_!sjx^Uvbf_*wosevohH zpUPMBkLNGvFXfB*Oy0-9JK}ZTi@Ya!KL@8Kck^!HUC+CMH^bY&Yw^lFlo#W9d1l^8 zJUQHju7}}d<3#MzQ%r@{RHsRxQ~4&`)2la?8|_^#<}bUyU0$mBfxXR$UcEBWgo*{ z#$LqcvUajQWWB|Dh4n1!G1fz@ds(-$zRCIui1E0Ybr!3}%Cgq6f-F1hRF;}`JZm{? zDND>^GCyX%$9$dnBJ)Y+&zbi#?`Gb@yq_f4!uSDWJL5*iHH>YH^BLUSjAYuIE1ku zgH8X0{yzN;`pfjE>5ujfnHc_m!C})rmit=Z|EDd$V=(D!FMF3i!JkDu3h^em6k<$p zW)ZuG{`8Lt)+}ODXvYL&7SSp6-o(T#qEYC-XrIoaofLYR_K#Wg35A}a{e2dFOrf9C z{x*v~qR?Hmzs{l^6#6>tFSF=F3SCV5fI=71-lx#nv_H?H_b61M{r4<-mqIA*PZUCE z?@(wR?d@6g7KLEin-mJs-k^|+_FuE;bqbkjf1E|HQAkhw1BFhdy-J}IXuqFDuTV%% z``s*hnL=Nny)=tnoJB8CXgTfqS@c^99ZGv{7X5}o2hyIMMbA)3OnZ73Jw+iF?a5j6 z>mGXN6WSBA=vNf_GwtzN^h*l8Mth7xf1v$>LcgUwN}=ayk5K3-+RrKUOWMyU^a$-? z3jKukQwrTn`w4~ap#69jJw&0KX+NUS4YUU-bS>?HS#&>zuB82tLRZl4qtGR^|D@1n z+7BpnChhwas?+YBMgK92?x9eb_B{%fXm?X6NxN$n-ASPs?YpyRJB0$YJ7&@C6mrmR zn?<)$2%>$5LI&C`6grXi?OAm5EV_w83fhgc=vx$8N&6;+j-!2pLMv!D%%ZPT=n&fV zv*>FS64S1mMc2-vYbZ29`)Uupy@U3ZS#&jp-lknOi>{nSS5WBpw96^<9PP4MbSZ_N zqHUwlle9}H^a$yA$^df!1e?M?l^-2H^(XPwRIG@WGw|Yo=$=DPNP6O)(24v;1LSs z!xTW_J_u3Zj35PK0Sbit6bSnIz)OLEhXQ^#1$?ePa8khIpn%)n2Q~`0tQ2rsDBv(t zz;5aTBL!?w9~k;TPXX(xeQ*i|ENlAUWD1y1qJZH<3amMS0$Lpflv)Z%H56E>rogc( z3LLGZz~KrC944p0G8qL9ky7B`)f8B|iUJ2oD6r&s3hcMC500aN;ENQXf1wB7eE(Pq z{OK49yfHsm)%SZRYatge36a`*9k^(Or(Fcc9;Q7NS@Y`j5a3}?yJA?wiIk*p& zQsCKxDDcdZJ~*%s7E|Eq1NvZ5AM8(or}pcENeVnErogX7eIV=u0R^7mQ{Y#;KHyT| zaSjE3$?gLd1s-Ge0fPd+pmP|&KlZXWf5GRSPWzO8FKu$ipxz zzW*xmh2o9kju;cC#ixUJ1Xl4Hu~K}T_(<^*u~1AGeFXOWuZf-)Jt2Bnbf4%>5O3f* zu;af#bS{WHP!uIa5s@3jA2>lI6&)j5CR!xo3U>-W6ut#^`_Bp=6FwxoS9m+m&$IH@ z@RYpcct`S<@Ps@%_apAR+}F6zbD!Wo%)O6$C--LVb==Fi7jVzzHn>G@k{jW=xkl~@ zTq*Y$?lSHoE|;^D^C9Og&MTZ}Igf$-1^04p=X{g%70$(+&78A1HBOeZjuYhAIj3^e zoZ~smIZHWW4wL;c`#tvS>=)TjvVYFLpM5v`7WVb*E7&va4eS=X%tqNUwwG;YpTw54 zzra46y_n5s(^xxL@33BFJ;!>S^%K?)Sld}QvaSLC66dqlvj|q6bp{J&IavmlmbHqt zf^`UMKNg$$3G;pC8_buPPct6{ITr6>-pafIyq~y`xe?@3z?f<9lETllGS@Jb%;T6x zGM6xgOgiHu#=DHy7|%1FU_8vYk8vmCX7JMDGR6gra~Tar5xlvGFx(&u!wKL4{4tDW zj71DCeJA}x`djo@=+Dw0qd!EySNKigSA-XX^OLiLHDOk`P8by0g{KPD;Phm@S6~*LB#;ZfAUIsGSil$1 z_&fOT@L%OW$A6sv6Aw8d-Z)+_q&hp`||3BFMWP>-IsoEb_Y* z$oEZvlKx^i^NF3;K#LeHZj)UwSt5XTG!r{i!cO(3gB^ z1^VJby7mvy7j~sT@uf#0-#5ATWytqUuKgM0`zAp0eUoc{2>HGVkbK_+NWO1!?GGT| zHvy9Gn*ho8O|E?w@_iE^`MwE|eBb2SgOKl=0Lk}FfaLoo*M1H1eUob+gM8lvNWO1! z?Zc4on*ho8O@QS4Cf7a)J?yvg1JI{-rH6K`T_|iL}-`bUab65Jf zFI|Rw-{jh5$oEZve-#59YK)!DRB;PjylJA>b z6CmF=0g~^VT&qLAZ*q->eBT5}zHb5~-#58NLB4NtjevaLAU@0&dO5yD`eUnFj4Dx-GN8bhczR9C+hkV}zNWO3K=oQHKO@QS4CXd!2 z-!}o0@0$S0_e~zX2))y92~M`ccmZjrQ4wI_oZ8)xB1d@q3`phEoggJ zdaEz3LjQIl{p~+PSA6Ljba_{Li!Xf{db2P6F?5eF{T?*)rB6dsUwR0d_|kua#=i8c z(8!m51sd*317G@O$n~WULw#TR5Y+Rf_d#7>`Z37yrFTNMFTD-2eCaY|`qBt8e93@1 zyOQopGNkQFZC?@~byre+iGyTcDngPkk&w76wN_T1UXgzMef{+RpYDNgz6X5U%0l1k zO9a&PrB_35@}*1A8-3{&&>ME8*Za~-pvJBw_|mOV-ItyT)qH6a;uq52{4-SbrN4qW zU-~1c;!B@}SYP^esO(E0h8SP^38>^t?}3WG^iGKOrOOcIOCzY@OG7B{OD2@tl}KNb zq3o_i_|oekd{@GJiH9<~QredoDCJ98=yki&Yj>sB_|og3f8$Hv4Smn9^xeJ`gI?`R z7onsty#hjarNpij_oZ8**sg^5(sQ8bt_1H&5ntMZ!n@KXUxJ`l?Mk7A^w<9vx_ei; z=u3YKy>eH2g)e;xdikz&moNP<=w-XoOMU5+(1l&;CBF2F(4D)|9lrD*pxb@v{m^Z` zWJ9<5k_Elkm+H`W?Mg55r6TmgUFikBL_*Ksm7eEIuZ3>em7eQM3FtYy(zAUj3_WXC zdZsVE5_-n2bl#U<0-f8H&ic~xpfh0qf933ozOtg9Mpl1kef1Qy5j%VTx!*qV-c9XH z8+zZ#&wTS%J4e`#tiMfKrLojRfF~mXCc>>+a+u4`Ww(`0&ScC%N?2-~>7>RqSL`;j z{7}xCR;$KObJ>BZl$|k+;bQ?dja*hXMLm_wVR!*x&IAj5Ka-9~Fh8w(f5*b@a+Vm1 zFfyP!sY!{$LD{@YbL|O;1Tm&KI#S}gt_T-3F{_QFXgOZO+G9F3uLGXfL9#I^TWvFy zv?jnsQ;O1FT~oR?uM1#J^7Vnk@#K^VT$>@#_j4;95(76htlQbw}v zK(nV@m%;RLX&KP~-JhB&6{Igla{and3@KddfG$fl^!g|UW67KfV!^3Gyn^TzyaKGl zLhvXbV+!TukZ3zy!7Mva?=urQdd!1>P~Y-g`{esiO6I?JoB*wb)R}&Ry;PtU40lj|{rHwK-0 zw0uyPb)(U?)^4(x>NZNHR2js`ibWFx&FQHOn%8=51GkAf4B|0?vCMtyby?}Sx{Pd) z1)DI~8sCvPIxHB2hKwIL{IgMzLoKaDDYBNRl}a@(8ZK~sDKeYMIUG-sNu#c%hE^X$ zEp(vCQ?JX4v*R*Mep)5lh>X#*GFdbfg3XmXi?{0&- zrHq*@R*cNpaSh^%|gE&{GlclNy)0T69mSLtOtCg_Ra(f``uU*L2 zGsUS`&bNnCM=GQ4Mx>z*d291s8Y@?QN?MI2?;s1QX;B$1x}BfVD>+H3U=9XO=UC1cq>!n z@7s*8e|F6Rzx;_~^8LR#7)br*%0FHD=pNXXH{bX+ z>9#||3hs|m@)TOf2_N4St5~Dzc(P=~R37Af5KLrJt{vq2q>$W5D2CIlDQMbgG@ex3 zd5*Ql-C-7;jEBi?xQxwO-8Mb$mYoQkcvTqu7lGLA*5>Qq*18QkBKdI^t=yJLb39)R%Mu*{=4aB_TS&`+#XfJbE;bUaqUkNlXR!>{9-)kbf=JW-reSMovzw>%$+**s z(BQ_eF~zbfo@wv)@e7Yo1|pH4mZpt-mog$Zy>gE06d7e_ed|Y#7 zvRIa_Zk9G8ol31+rHCpBg)JR0@k^!h^3cu4Y1ZlVnjm9ALJRR+!xh_AjTiG=J4@1{ zJcte5N&C89W{&FR{c(t%O1*r1O9kC57mn2$vu=mRV@NBDk6Y3CqW|=XO9fcBI+`@% z!*D&)Q-%r3%$sJd&yzy8--x-BNKT)^NR}i)YKif6y<9q~m-k0(dP?>3aZN9YShP|% z!Zlo`=~=4N!1x|(9_aTalQ4)qZiX_bV`GF)L}+C|>ROQz+ztnl!{l9(vD-tS)7STM z@u*(jAKU6F)yv1__mH?%Fz|>_MP05gxe6{%>-vG?GCw0bV>vB^R6|Qeh)`tr3aYLLIC)MkTlkLLLE)+BYtPph!*`m8o#IjY}1ymJnh_s9(v6LMjYL zRkG8G_do^~BpyPqpDY)S>gD}WAD>FPOdr>{oS`QTd_d7Kj#MLjgv$(&TIQe;BZWuP zayOadIYXB!{Z@*dL{uY&7ZYl{mrstc5IsQB8BW7O*XwfLi~qL{VJoNKeewnCKMaJY ze**Ww50E?VJZI^Pp$h@Rb{tza`WSpr&e8x;GrVNfL1I?SzaJoY#=+U{0|X*DC}-&a zWa$0&Wb-~cLFrBAmybrHND@2?t>tQpd*`cfzvs?H6b~TY*-9~!R4$#Y7D_V^II0kv zUc6YqK$?+;Iw_e|wSe+PCX7cM6`!Y2y_x*KrJATtT0%KmB$0So$jM+I0H<-ZUTfvb za+ewzwJAI4vwVia`EJE2RGFwX>D72*SSzBW9y2@exDsg;Y7-3YM|A-WgLG+kzQyaY zq3tnJ_r8bj_Ae{sX+@X z>O6hZhM3@JjmW@Bxh*PVc`}@(CsLHup$}%cQ?Ih>2dbZqXrI#~AujyVQ?!iuP{!$x4K_WXHpCN|7BMmXg zTi4Ur!L;jX@154GyxiNb>NvXH><>*a`h!1SpN^XvfyAG=ZoZ+g<%w8O)cU?0M_vI} zv1O(2;T^KX9NE8Sfd!X0@Q0$&NcL{hP-Jxjei)`|;0wVH)-yz`p-$J{K#Nh;} z;Xu1{m~vE};zD9VprgFh?iTHAKU=jjF`H^>)jpCcSNo|J!gj(pZFj?1p{`TqO5Nl} zq)E3-R~|Mn3m>9vxU8bin8i@1s0|oA!wtq{?&4T0)Mnx(eyTJqs~ZiMW=he?5@viH zX5B(fucO)2a(3r69oXI5jh(J%ckhy*Ac4=0&F+xoL3S4)pqZypK_aUiJUI+HyWD3- zU1zNHo2D{S%x{+^4PB+bn8Ph?i(onw4$tRGUfHIssI8&t*<~2?DQJs5pEBL^_+VkT zEN|?Hd4KiCk0-Qy4!8HW1K+Kn!o6=uE!(kmsbw}>rrz?%Uyt1ywFWJ3aEZ-1F8J&t zxp)EZ5ucYATlV8nz&SM@m%%H&C3&rze@VAT`us&c2Y-CH@m|8;!r>;2A}FZgz~N>F zS4QZlOAwq25>3;WxUML&QdVdC@Y{Rl3@6 zoMihByk1c-|?U0H*WO)Z3VB<*41YLKCcCh4v;`%X+AfOvdYzG zg8Do?4v<%3>H2mjY;ODNGeA9_9tTJ>v2;EA^!U90NCPCESh}ij{7CojVJ!6coW}<= zJwU#SrTOgY@maso0diN|F&yOGVtFVkx%x=r3fb=DX7qUfmcl}h&j3E29tTKyu{589 z^!PNWZ$2L9v(=nfizUTbHI(W0nt6r@X~RBKz$*1fh8fvW#TL`DL#CFl?|MBx1?uti zI6z{IrR&+J$6Nj*4Uj5h>8if59`E13Sm^Odj}K^ifD9W;^V!wo6MmxuWZSqy=gJ`2 zG}U8iY|sXIk>$+I=<)t@WD7ms1bjR_4v^enX+8((@dl{R)8ha+50}(5 zAO*tG_3YE*HUE(YNQ$s@Ro_^T_a`A+=<%w@2Q)oEE`_D}EcDpA3V}L2wF!{@;EpOy zR7-C>>Eq^V^Uk+;d_dC!BwJXTkFPfGd^4!SQ=0(k5$@0$!?9wU1lH>2 zm~?5X>IaX02dK?E?*a9BYQrv{HDT%cu2-8GsK-;A07(>CLSNq^Z?lymge)g+KfRRp4tT1wmf=K&Ua!CQkxN|&r_QKTRThFx2HCX z{r``yoPGAzeH*WYK6<<^{L^U1-veM{X8icZ%&`tB_uM&f48?Bh-WF{%>aD}O;A(!$ zYC3(etDxI$@OzMaB!18WaezT*zCawnQs>_fFwz{npd5r?ppBU$Ukx7rqc>*u4{Tnk zr_jbsA`lN z2HuKaE23 zTC_4u7GOJ;L|f&0k12??i#maompYl?HGQ!$GkSbuCV(;>{hnFY?qUbo-IH!dK@Bm$ z#>|ssNk?zY++d4OZ_I$9|H<5#iAR%>Lx&rh5Hpw9Vo9JnCV#0ns(@XSV$GUi?P6b< zb|#gHIL2d{SxmU;;ik>iY)0yg_?k;0VBDc6#u)2QYZWYFMcBw-n$BlqL{=+AWUz{^ z#Tqr*(Am_*xR?;ZO8RhAQie5h$gw$rW@o%hXZb`rwbXFqHQhUZG1?im7*iWOq|rHlNe+)q_yxoD4#yfDGJM7N!vBr-iLmCqux0neIR^x{}%T`Fx2 zh+&u=qC>8aFyL=^DxvpVIk^k+X;u?b1ICuKJg@1%^1ff+8F-fWjwTdDU61cYAHC7? z4zMNfSNmSB3s?2qWF2#Z9D#ESy5nv4--&oU=8T+gcp~Wr<-Oi0yqCZcM&VU+v&xPn zVuzl$4M&DdAwD&f300O&bwCq+Bv)a`ItofySJhpVf}ytfkI)ley%? z8-?l=j!6a8Dvzz19vhAd8NHB;iams}2uh)NywRygAF=($>2?@X-szTab0{BhcO6=j!c;jF#~J1#3X^%nc+1r1icTk`MoXFY`#QX_XHrhwU6Y93jtD>2PJvJqxXNG z=Y9SY6M*E&8%PF7gML#a|KYbhlK(gjK=Q;slA9N5%coQiN^;Yq_qDU$=ih%e0Ljf8 zNCrsHesd&G&8a?nX_9x}yWx?1?cO-K5o9;iCwb#C^3g-)hR5`;-{XBgnqHdkg?raM zzL)m+t_Rso_4)2CLm$m^-DCI{AN4-}^2k%MJ*KM{0&M2)s;PGb>}Zaw9=kt#)BN+{r8(YuFLZuo%jdY| zz0HH{@A?!MlhmUrdfdJMP8Qz#@E6Mg7_NA|_>#R|+`N!gOUeAK1Xm0LhC59dT%OV$ zI*}lH(`;FfPl1HqUn9^c>pm8JP!*tzWSeS)P2*Z}wWUT5h?@|;`x=N6w2EY0&} zJ8$uLKEcuhY@qu*pJ3@nGM!uce{Xm_`n!{*nclwhW{>F;EIq*fyU%p77;&_g&Mo~j zpYu5WPHSn7x9;4t-_iqY$@>(2OFyz3J#G*GvG@6DEC9oq*NacEbj>bLtzMx%G$W3k zA0_JG;BalC^*GKj^e_&?Neu291vlp6KE=7EAGv%qxAX^Jj zEIq)P1E1jmmVPAPxuyTcw#WD5>xcE|e!u&z7j`Bd-zQjlfDJ~U?{$`bB+t2x|N7k3 z`1Rjin&-=Q#vacnSbBgB8K379Ed5BPb4&l&d@BEInWdTDzBBTeKEcuh?4S5deM>)* z>AI-@5XNmGhTDKED~r&o=`3_WB^#n+EL8iPn zfy{Rekjah*ndw-NX|4z|$K|1iAOaNrHve3?THa8U^2s3QU@vgTd4~1MRc65;0P69c zZoP7~v|te6%qvUN8u7t~JPN)X{U+D_5!D3<4}QEyJL& zU=U!HEyz03G7R#;83b4)T82Sx!5~1-UN~k30Un&eG4}zsU=Uz^F~|zqGEX5BoI!x)v1J&f7YqXQ?1f`ykn$NUzcv|U^=+9x zc-?|QfOZ`>gV!z?1h``nWJPb8K6uT7L4aEqLDuS)VeoGj3y+JfUi&H zT)CQ9FbJ^nbiB7Ru3U{T7z9|gIo>-PSFXku3<9jm9PcfTD_0Sp!SZ)Qj`x1Y{QUpw zCsxjkw=QnJ1_)38JlzA|VLbqjO5XGMQON+q+0kp~`;JO_p+XM25dn^e-*abcIMABc zpL7hcUViY6g`fcY4uc(hRPxBhc<(sX(YFwyu&w5=KM(Ue6Dw*`azEwvh48xETPb9(X508FYlIdCAi8OQ?|zccSI7nl2z@KgyeYC6kvV#>T^0n~@jswQlC9 zq@>IcL2Za5jOWd9rD_*r7csb$)v`r*7QSSwy4kh}aAYl7sd8Zu{?*n@IBg~pJdKOf zWW1g#_4OiGjvyD4MY1|}vQu}tqmuKcFOEvi9zQA>KwXc`^1u@_>Z5-l-95bZ3TiO$MQpMx70ZSs=E>ZDr*=M}T{eo>2D zx_MAdOQiF|TrI)BZ8FuwujtjBjQ7`;*1 z?+h{<^v2a7Cz|Kq53pZ#@M|o<U{H;jP7RbPoDI7-ea3!> z6YMmO+l6UcnB+2DX_Uk26*Vf9hP7Uzr1c9!n>RF(uOb)Y5|QMGJ-lDNbm^iCM+$CV zE|si)x!WU)d8|C{72Qg#>|~aUa_BW3*b)fyJMnDE3-cR5L5|Os65pA&w7i>mkS*PK zmcUc_Z^KQR1Bied15bXK-$WvL=u7QV3nMe*HZK&5-6`!(Oc$tbPRou`HR_`6(Dr~Z zt3r3;s5c+~ujfSSvzS$$v`aBn?MFK^B3Tt_Tt@ZYOiCMuQx4NG39~9)DHSx+v@Xdm zM+|Z*pG%Dejp1xX9Zs=}xvbVrwWA5L$1V3#i+WA>&;Q@H(poux<=nrXd&}9cpS|bI zKb@JL{>RhfQ-6PIxb=5i?#XK>dnf+(gtPhAo7To(ZW!x-zOJu*Wv#vXXR8YIB}iKN z0w{j?&*l4{xehkx1MSTR*uy?L_|fJ2pD`y8WNSXa7WOg(&IcxNtl^(7-+wMJfn$CI zXXgY0fFAGJgvWCk9Q0H@_pmL z1djO;jOGLaDAw^F0l0kMa84k=3efS!Lb-h3ATWUd5toi$uDX1m8<;?VyADSa@b>>- zzV_!U=Ra~@JO6@npWXP?4gK5?ZoF{)k@fBMeh=vlz=NI!zcda#E+cFZ2sNmPi^v>C)WOa?Y(Q%>c6c1 z{AzRc8PLa}KJ=1v?>_hL!e}R5;^^P1697G~99OCB{vMorFguX( zao-`5J$9cay$g4r{>0yTpSdix-CF`~e`1L1;0ph|2S=?^+nqhQ?qTc>@4HXm{b~>H zBc}IxH$iPHdvNMuaQh?Q@3DL2pTFb*jl4U$k9_^r)V8<>Cmja2KZwnBa9_OY!AX<{ z_r*VO-ya&V{#_RC0y!23M$VQM?K2bVtt?uL*Ed#d~J=jY8-WB~W@=iAga zxd(Sxq@)|}U0(TMzDemz=uh8GZKHc|$%EnG z=I=-~}~@V|Sb$b-NAGsN~w_kdn@Skn&g_y@o4y$!%8v3=_v&~1kRB@XYS2QPof z1G@YnV*8dopywR|6hB^)x3ISg)py0X2o?14Xe$OHPLhp6pm?eRJ!z!V%i+!7xUc08c}@jmbK4evPxZLZ{3 zKDV-d$J*bl{rcL~HDT?p)vv97V)aL$e}DeOx&L|YgXj9^UU%-?+5dC)Kb@VP|IAtP z>@8>h^vo}w**?RZx%Kp4p8gnwoPPW1+Ub{`dhFE4PyO(zH=VkB>z}qB+Ir8Hyalg( zZfoV_@2r0B$sa#yoJ>LwpFDBm_fGueiQb9Voj3=nAfmv}ZcbKTwMlF~XX6VSzp!x` z`uYY1)*imH{>$s%zg{_?gf`E=@4OMz2a2pIs}~wJ>u}gi6|IEkx=m}M&fv6CbbFdr z9aOTc1q-$4ltKh2xBC0%lPj-UzLq(@knc2cn}8Ep% zAdwx~?#6_*;k-oCFp+_VP*HKF5T@IXI$hmh`cg7mG_X)4RH<~S9zC(h8os8@OVny* zZK#VweALj*6YDC9~s9F4o|SWY*wQ zJZjZC7@gIG0)i-xTVrQjxHE3ARp%vId9zpoMLeK6)>sb z7=2M5=nT4+S(J#P_OxU+^AaU9WUELa}rl1axi~ zd{xssvM?q>Sr;}!X)$J`Mr8+jM|e$vF4V}@u*vpDF&nLn;?B5K$itBKb5A|b% zWGS4Ra6+aUF79%xpiv2v zMnf@Xn3<#Sm7kcG;LXgemrO7=5$m9>9vcU_zB!#M6vB2p6|Hk>!mQdIw~VFTl^GVW0&E-E329gm8HRx^>6A77NHms?Gd5T^u{0illL zy=K2HiScP{*lJ|$y4{_@y|nB$@iqeez`R6D=5uVc&lm+&=nQ&pGt&^IQY!-UVG}o_ zgJ>F=juSOl?3GtvKYxHNkr3S&tTCqqf><&({ek! zs6tHTs;ZuD3UUpziE?TvVx6!Z4v~e7FsjBin~xQB6B{6H=p%~~)ofLU+j_4VA0-Ns zUepV4dqlx;b6BnnLRNS}H^8aK2}iY|pI(%pg?6qJ(#eilC{*K0rIRE~PHhb`qjA*C zgFY-qX6*<#TG+w+a;91-SMbCPhyH3& zBF8jqSg|ViEV-99`@=-X)XG|+QsR&rK@lx$I!r~Vx(++5zq%+v6!4T2N{p0D%;~B5 zm?H`0nO&R~Ta{R}<`f6ILT9U`#L$J5MTtbJ8Y(p4-XM~wD*55KFDP(|=oboRHLG#q zgi*)v5D2jxbyt6HQ6g^05*JHL8k~$f2%e}XXA??IQEHh_k3i~$V%g=Hq^8j>^bd;? z$dnFGOVcJBAI->_J~1V#JKP5M;?WjMb2DKgqpLqMFVP$r%-9g6XiPF?B$gGWLC$WCV*-bZwl&7h z4sGIEmhRSYD7h%%g!l@|+6rZs6%I?K<6}}C>Ge{EH98{cy3TOOqE4lXw^zP@Q9@@0 zvCXN{r~^-9?xYjS4cW3i87JUeU7+1QnRA$$z~~uzm7c3^Q;(MMDTXV2mkMzNQ^ZHz zL?;rO#MO?RZ^J@5+;6HnOU_cwMHO1!YI9CU&W!DGi$Sq$)l@N|*PI|dog&&9E;sJ! zab1bK(5;IS(kxM7iHL%S;c8Bn!kNabTdEK2p)+*4gK%ypHu4ErbNMm!fBh1%u1dj3 zX(*$GgqH8jn1Y=$QNG$sQz)v~JZ8wk1tA)Sqy35Lb4xY zb5>@YC%_SCJAqnmnQO>ZydTRN;FU6@RB7e2i^mb|mO`a2Hf>;VNbkorc80-5suOPu z36Qu_&qt+Xi<2|cNO{q@4ZE4l7BnN}*qqdqLW0aDqnUBR7SvWGsgFBxiid@2Det%| zzceS+u#<>8#Y>5Pv?AhDVW#G@J#ko%!Lw*vZOp_LVUUJC$!EvV?TZpxQ5DkZ1}@`S z8BU<8h?Tg|NFjL4ZKcF&xK{1*DWN=?8LOp538`x!(^{!JsPyr6gEZO}Tj=nGZry=1 zj@^WTMG`hfs&adEF<)r#U?{V(k=w*NQ3U)=84JnwNI-gRA=*H!YE_n|MvKbQIcRH9 z2ZPEfHY%A@0h}Q1lcpP0D_t@*L;FHWjLRz7O(~erz)3KhU6i1aN=#}vqADfpE-#Hj z`Lsc%N);m4gZa9aFV|>p>^8FXFtm7iG*WcYZ6~RGUY7u(5-%WQQ!XUj7_Ts}Hmq?GWq`bCK`N4S zLsqv|?1scK6Lv=3lsYR2gwh;#@@R4vX+|n|ImO3I#_D3CBq+mSoXk?D8#he1AJ1o| zjO=JdCNz*Tl8M#0jy}tk>Ma&o{h@gsf>>%-BBf#jXcw+0%BB@Vc$Ma(#U_J9`h+la z$(HI+Bg0sA=FJdzcSs@wHI)>OCPU~E;F~yVpVK$s_R(^I-BHKu-s1mNYLTx|^gJc6v z3ZofSlY34Ek#joV<8(=C=Ib=HnDz)*eVlFLdKfN;lW?CC<!c_)q{%f|*T;=~ zr##?_cpG|PQAdPQj49Cv>9CznuVS&eVkSBb3<%9c$x3I2&gx>iP*+iXK+35$-L*_OGR)dt zy$)uc?N%q>OOMJnkKtikt0qi$_3iV{txMw+Hyf1`lYYmg)T@k530d41B!?bdAw~*nkv>;^Mx!D$x*$oWEmwPzG#KiNOn8T zo|dVZx}!*Xy*w_UQI2iYYm_?BG(6Mkv=SI5p)0?=sDc}IL|yI5kxpH1rNWajrFXk& zdE$ohQ4PtmqiSo=Cm4fpM~fA8~X(Qldt~xhECS1QFKrU zix@WICAwc2_2qO&>I!u$rWsDHUQ$YRp+R3t9$tN-iN z>GH~JSN>rQdc`^kqWk^y#$Y48@r)DNiTKI)oRm+jpIkZhj#IT$FWq`<>*HJR+Ir*G z-6#L)Hm`v+%#=EVJ5A3ybutv77F za-BT+kLNyc^1+k;?xc7!vhsk(;Q1wQgiPx!*TqpXIEMwD8fr3y?>mQ1eL zLoarcEv6i;M7kHNgL1i*mP*6ANuO8@Y^I==Gc=!!Rl8(4%Q9mL>mih@F`%R4WjaD9 z<71pLRQED|UaZM#97+~Cu!yP#?L*u|l5x`8VQFK=Sc2NRo zJvmbnOxc*kqJ-AdDkgJ_I31{3ggrpU2=t3EMvC9oe zFiwDLn3XB4rwa)gr4eB|b$j8BH!Mn22X=8hWpFA}romiQznC{oTWDL1WR+@{ocgFS z?9|muQ`Ct+jT`8~=Bs@;1$S%?E zxrsv5og#J_dT?ICMxqU-h73!kW-kvOT|J-0qI5{gr@E*sAc^*rZGed-OAU;*l|>y* zEhgLjN<7`|M|fHfTZJ5!s$?>iNw@)$apbz`SgUH9e7L=}cwt(jGS$d5VyO^4j&fLt zlo@M~ExTGhLzGG+FBv7KULwPBOM+^PhWCrqOlil3SsN3wZ32f4!%d`Z1n&`CE$;l3_IQUZy!SZ9bhqL!Dl^s$p7h)K1UfwyidDjV?KvQDV4fl{eCh5_Ja5l;gAZ zh-aBLxCl=3voSj$@jQv9MpQj#gN49eOBAvw^tnZenv-z!I$2h8*^H(RnCltp<*7 z>WdQ9p|?hrPQkQ>lt+bOrx#V^cBYfZ8%94yM#FS9rNu&WMcBaSB}`W=g~r{8G>)`G zdZ>n^6VXaStyq?TH+yI<-F0}o#&DTP>f~abz|_)7Gbs$fq^CeRgug>f`Hq&Is%snGqiCMK3!gvNg}GoGKE@F zHrnN$)eu5am*p!&zLLcvW-BB*SsvrY`8IO;k$DL#Toi?9x@KnUY;jo7)YKI2rIMr* zFLfA;6Zn{{qj)kl)W@eku_z%GtL%_)p5LcTb>Zop%&_>0SxhRjG#ulu|X}6q*`GF{Mj5~G(-sIv*d7?9Q zx*jQDik6ik2G=QaVTfFm$WA8R@r(w`A$g7+CB$wj?usavEAX-rn~3EBt#J_)2}h8V z?^z7hL;;Z-ZLuwOXRek(x=4iOBV)W#ZXmrrUu)u0-7b$5v|S|@Rp3RNDvm{0HZ^UK zR?@f?E$I0cSFT<{^>Hjtv8G`vX@y2ke9A8osr0~fUCVQm>Nsh)ttb*0_@kmPb%{W{h7{shnsHeq@&6I2PJYOwmjj@BDfEIPc zQX{nFmW3fN=IB@iFHRCfw5%(=A>|Y%E||wFwOJPfNv<~EwI~tQ62)nCDzR9SQ^hKt zA2!$)+hbDWnKocjO@b92vsHCdx%JbF67VP=3o{+IOjWugw@joalr_zaX_XF*z=OzV zY}c9=OK3vaIJYPfHjR)N4*};o4nnwh(1}&FO{^gphN?=8Kr(H;9X8r*KfJPdpIcHt z$}*8|7H*hQjp-H&g;{>S&k4h@N(hh|w=7bY_^! zX@y<`cGAgemWcFay**AvNHjkaPAnWjD?cm5G6K~E6RNxxVhFcXh>_(;x0t}n6pP8k zpwVR`xX{h5Z=L(VYW)dQ&8PqIbPpUu56lN2JJl2Il$cj*mYeLV*}ARHO0#Av*9Oa+ z#bFc6T4fGbx&zW(yw5=aDsPYom__x-NWLVeBW$Q`w5F{>GEp4mQwVBVjuh$X!WuI- z4?7{SdN`3*dsuUb)Y<+-tkv7YQbrw_(ODBL=7D`wJ>CZH_sY*KO2qpag)PKO!z`_J zoLoCML*hxrsgD!IO4Ci|STGUJ%`zGi-dL;%+Oc#O%rGixC6`BrU?Z_1OEa)SAZI8n z1}xKMDsH#mwTa4jZS(x*%{g~gI;RZPI*gZUWiSDs!lqKziH4f6Vr8q99!YQtufpLO zH$HJ2cs@7dK-$r$nna2juAL}Qcp9^-R+MFnj%DOkXPAul5?q?Bby~G)b&XpLJ~IT& zS{f}G#zkCeSUEmdF^D3i(8IQs!jdtpNr!crvkLgiR~99vqhW7U#=?5wI1OUpT3s70VETCAPWh-XRyx&g zm`fF@QFxW{&i{v2ere_W&zxt^{WI9@r_cWV+4r9%&pdkO{xcb{zyD*WUvuiqU|&DE z^@XjgTgb`(bMgmHUOMsnC*FGEm7D)#^Ua$t-T2JLc;oig#1gQDL?JY+#@e-VvXm%>V|}riRLx3_bf=U~I#?!6 z4H-*LS9*YLd=4AGqu$tlh4#OAQEC19n9nD22vGTiw=pm?5(14Au{2swGuPBBVa5_hqr=ay!vE zY;*}&J>EzvEvht9xNJLxh5K!{3zKZCoGY`#vCl4YZKK?Z9(bcnMfiOX_GU z+E&OBl^s*{k%1GM%*2IBhf6n1E+VG!G-?b+63~Rm95!-?0cPLgz-i;NOivK2byeH{ z$XYE*NmDta-4&^@f@UI2nr}1)qkOdjSHMd30p zW(3wF)pXq++VQc0wn}(2t&EX2xAUqw?5mc5owl;|T(iauBMQ|EW8lP2vXw^?ovc~guE@<`yZLT{ELv=xj2WtxCAWHW;H%OaGUN_9a_a& zN1WE$CY932VHca3;Q}FqMqu#*oLJ#@UO9(-vvyJ*G~^Zs2AeT5 zOn{liB%w~Yl3|8)y94_66?52EECK6;(%=l!tWQcBT_xdc19cRdY~VJplXiIqb`qfW=c7kB8D}C94i==y-xovSPLE42DUe>VQKNs>-!&Wg<{Xl-s#$ z4tv)fWQmz-4Mr*${ZYlja-;hGM+P$rA1Q?TwixeG1u86-c^K(2C=oJoCXp}4t0c=b z$xa&wY+p8qec2MQAn#OGaw$6&QAM#BIYAUkC#-GPfmR;gpP&;+bObW(lrcDOKyy|4tVwfx#H zxAT%Y>`Rt_4aqgG&@9E1Z8~NWgIQH7S|dv5CpBJl5~4XpDxHL0?~GCK$nKoO-gyUJ zGaSNO;;oy-X=j+j)B7J8m5-C5a92##l!-CW-N=AuyMi5aCJ8%~$cvVluF_R-_AUt? z*&TD(JC=a0ms54HfuDk&taGO!-X%A`e;#cCxpmNYF5CpyzOU6_DQxNQ!5+Y+#uTD0%xBXYZ# z2$_W#F|a26{HVY&W=%%h0^P_+p?m@%tPEg#>m2shC16v{c*&f~OvW@i&d>_mtQ<4k zl98C$T5p)1IA9~9nx;D<3SeJ6hkfx9u(fVI+U*XCO{!6rXCtMRt<02BD^%%+s|0fvMzB0-f+(bJ*`XI)#5E$W$S`6$bdku`DQw=rAbuGWNTgUc6i|& z_JvEp28ch!?Yv+P`+_B41H@F~cAh_nef|=#0Rk0qJI|ZLK5q%w06~?wom=Lxw;UbV z=0bp|M%>PG=djOR0yaRT9&YD3bJ*uB0UIFt47c;_Iqb8SfDI5)h1+@79QIjDzy=6Y z!tFeB4*Sd{U;~6y;dY)ehkeG;aW#UZs^E6c&tcCm0UIDr2e)%>4ts71*Z{F7xSg|e z*t1K(1_m-?Fk{(ORPE$)nQ@pK!ckfTQ`l1OrUL^T+-QIjY(W@uk!p%Bu&p8A0|z$6RJDe{ zm9SY89b)Q&$fspgPB`tl5C*54?R>l)n~t*jMNt@5#>%vojMULoj!=rZ@i>!OD#*6i z^hIFc%Z~>J4xk{%XG_uVNLvc`=Z?Jio0Orz6qu~eyKW-1otQ|aEY95=Fqn>R!bhKKT?g!HLm^pb|VoLlVG zn1nMLXULhsGkBJWrt3LZA2|6@W0Wchu|YbGfg4PT7_3nffuEEocnKVG7x3J$9l=R< zoE|S_h+fmZX!nDix6EH^L+E(3Z!P;$gTEsUG00o@jljnCC2iTup1nUZ_mgCRy9Dk8 zcGn$>roKqoAcIXs4gAgvmwjU(e0R_qC{06|dZj$(HyIfpiH4Cw19CT}RJ%b@+?2(s z+!WlYDoDntN(l8DlgYuI8kb^OTN{vK8N6`-hnq>Mk`u!du6ps}tXH{YH)tA53qzt0 zuGKNKw1sCQQ7X=6)0{rS(4ppn#aFae4fiyB98*c@l37C z?Z9^{sBrHaQp2X}}*++6Q5bP13mls?1<4}e!8IQ|- zMQ%x6>*in5ZE!WiYtFm_9v^PJm+-f6xPhbbL+9IJRfyK?3^hcCd10!uPJGhBM{GYE z9)R|#MbjR+c(oR9r;wW-Zopx|MC_7#Q79;_7)w{ENH}`2KGTyUhc8r|k(rndMwxUQ zlZ=ZEyNWStv?3)E#Yn=PG3=0GII+bJds%ZZW$Fr59(G6ZHNOF~~; zdHiJl`OR~mJNE(TYm4*wC*hMT^E3J%Iy*Xxoqg7sKR)yGXWo1!f9Cn8|Lny3PIOM7 zr+@ME_G#wyt*8F-)W=S}{S<%d!q&B||F-o*;57e5@20@~7QyD`=QcmE+1-52=9!H@ z*!a-KXan1L*7_f>|NQz})+umm;HzuDviALJm9;xo|7P{qSFf%Lt9J!{cB|OXk3UzyOY|OY*!0`)NRbbHLyrwTp1Nw1-4iyMq+uyKjb_PP-1Zn zJV=SxEm##&<4mQ;5^aL1*rlTU|FiezQI2$1onR>|Yv0(6ZHzxxnzRirL@5?+fN{sJ zh)`@&?8X*~2yLMyBtyJEjs`7X5jTiU6_u{>Kf7eeBpt?7#_MkyK zD^*yIgA|YgYOvw{i+4u6XlKL=mlD>faAWllEL?9Rkvm{AsMQ&TdYc|o3FkD$66Og)y=CGw*X_=kse9pD zcSgKtXT-Z#h?3^zaGHTgqB9XP{fS2>1YcFTXi`(8p2(2XAxB4kth!gfXlKN6I5D6& z)o8}CZ>H_Uo-(GEWQxQ%r(i^-SCgBq8K^M@F)Ow{=hzK*KpeZ_4v5#To`bSwyBznt zblZm8j)Qw?aIaa4-8Pa0#aY~LR?vRJ@fz%2|K6PutBgUAU%j=C@42so*6Cr$_B1yD zOaaua0g@r}IT)(;Y8cGV@wRPFrar7$H&=x|K*zR-8-KUbk!d=5wiJx~q}rP|#+|4) z7BVr`%jR2gQIQL(gXjh!$ldOXzkO%Kx9yBrd3!wWseXwX7`Dflop#g*SyYl=1Y{$D z?rCTk)a9fBiZWcJ_2L!V$}WiWORIK4tg69w8QuT3dUA@F5jA2G!0#}0DT~coSy7sB zKCV!8Lf2)tTSsHc90bAMsz@!_0r7#=do0)o@ULW#u7Gv%z;|!4N!J zrPEe9q`TN>?F#tJ)l*F-l2#oma}AM}c-BCAu`0x)ZoWS18E9K--B{H< zBs(DP?X2T1D@2Yb4Mv(}g9d~NRUCmRDs2>V^)w6`C9?2nFbdED4x>}+)~ezo*#WVt ziP{D6%PXt0p_bDbyQ)_5gQk&=r@kEIAR+Lv!N|;sGmz7eu4l3X8NK!46#|)ZwGP*g z%e9K_Xwr=3g{kYY9Oz0{2T2h*W$LshIgQS+*Bd({yqyv53IVE{!-^IDtlC?=r#ym0-4D;+x^etl=es-Pww=^;EW>I0!s z0Pay{m{zetkYY*4Y9Z&&D)r}9w-(e$4~AP6ULeJ4oG$4|Gdn5hlbYMn4qa*z4;*1m56MDC z@`C1=EU0wNN9S@=l`6>;4nU+j?Q)Gha68^?GHZ18Iaeyhv!L&+Wfoq2^#dEoSu9i& zjq}4XUC#@&fJJapoYcEKQ%Lfomf!DzQhTY`u&BeL%Sf~W6&g{AV~hgbu0B0>0=Ptu-62z371rAZfgv_LM}nTToGHSXr_e z%{hV1gB^BVZk64UG(pf#xydMp4A}vfvI5;7c*DV{C9({l5a~o!u4E7D_0j+#2-M_l zw!(w_`=M@?K{A?4i(tZ~=cd1A!*-SZg6PCZ**T z<mGScQQNO8?vH7#__>1;>=iRLvpudRA-91EgLs7&?&|_OKct z?G{NVNl}g}5+Dj}MwN-EK(#sTbjL$gfINSoPGpGh9>jRu<0uo5vGJPK4|!KwABSIh zXu}9$bG26BF$4j6ODixbIWbHpdb%?#`x9y4QPZ|q4oggUi0P&5)CaZ1Fo&kga|-Uy zsp_bW;>DV%`<{s?erASr>(*2`aON^WH|Q$3x2LMH=uCTSHtc`8 z976>}szvi|W)vEt($^cMBKWAqc$z}T{jf2U`i*Iy%36mkU`(@mejd!qdYvO@t_d}W z4&jwYOf>KZQ5{7Q*VhY04Lg{&op{8N&8bigZFvN~CD&{?_wk1|2wg^VRcaa|>lq1Vgcc+qEL|GLc_wOfE9l{lbLzn9MoGHQ?Klu;Yp=F=P+w9u~bg zXOw4rGaJx+c9LW#^bkxyHdVd7kQ*j^K*^%2$?$UZ(sX+0FD^lIcv?jLz|5KiDsscKPS}kN|hzjR<46fQ%VUK+R&6c;Yf znxY(b$!sog0&z~7qii?Q4yt288Il5Bq4>$(7J+j@Z zcxEnBsEW!c>Qw`XsGFYDmbr1CW*BTHk=mg$VVQg~uQ9rgFpi|O9V5&zJmCq1mL2t? zVrH?0HOH>a9+c~47o;1Mhdv-^>sdTQtl4m4$wK{1&go}eKq@tFl^QdT8BUWv5n={! zG>L3Eo%M?u9wY0mLj(@dZZiu?BfBMHn2T`3Z=v1lj0L>7p^_$tskQV^Sko{}_^UJsN**lly#Eri%yk{M7aWDe0Jt^|F{ zIwVo9UF1iE&{H^P=!N~B-NNyXXfk3mgTo;I2+*CVBp^j&59*~E1~{au9uFARD!vpa zYr}BKdT0Y&WF?;BLAWPlszG6O+@V@6VcO_Yc!c%?Zf*hwxQ1wS4~^|frw*tl=NfI# z7qN;nDOux@wr0cmKYC~bl#y#3Ge#1)9%s9rMNZ|kU!=5B8Xe5gDe2C9Y61ujS>dpm zAJnb9sRyI{M1hmZ7$RXI4ji#+R@J&E32k<44TX7WZ5%HC;R74UdEc1L2X49Vl>%CwCcIdMg|U+6scE4J zsa+AGa*Zr)X2L_O#>p*{4qBCJ7pl`nK`OOSyeV2iUV=wVx=)98Z7!755_=$3OYI1( zME+1Cx@N+**SAsU{`jE{aX;TL%_~rnog!Vf5_C|mNOhGUu1ixiS23|6YxqQ9a_(V& z`wHO$Z$j{sdMx088_EH^qoKPt4(I;v+yf&hZY<&zJ+ynxqB^36v@z?+!vuAjgQ+?m z;O)f5%fgIq3E`nRk{wRKG!cT(e%2DGc^rvNTMLc)z$vpen@Wa8SH;J1azIwW1yFa? z9K^WdFoB@)Yerm$PS`N)a7?>go=lMmC^KnCA#UWS#?S|PL~a5W6Wee}k8iPu$@Vuv z2R1Kr@tCknLP@5&Az-aux8aMxH_F#Mjqj9CK*ob%AjcJ{JSAs3;D0164`J*{E>|76 zSss!w4FP;qItI8Wb+phu+<~9_U?w(+IxfhK`DC1QxeB&k?{@vqno#F{OWQ~GJ6GD3iBbpQ`B4#=k? zu#-+(P|%xnM@o9oWDlFBO4XQw#FUce2#~~3-C>EED2xZu92Jk04oniIL7#-8%t5DI z;+&8U=M!fjCtF;Wp_4;dSR zDvV+%6b>-bx>?;SCKYsZqF2|Q&b9w*LZ(e%OrPp10`NhAy z_>&hCaN~dB!f#!8_XYdH7wrGl{!i^s_g}kz@%(R}f6w{Xoqze>U+(?nUb6QUdlyo_ zm3nu|P92>4__?1rBfwH0JmVKW{okJgBT(3X{lVVpT`*8&z26YW;v(QVRmpPdV;h0% z-8d*t5g5v(2_p5sYy!gTJyRsJY};jlCdJerZUnCP*)S`=;cS+r zg^ERBsXy2VTuaD)zrV*2we9ZjjZAn%Lzs)^}8E^>z*Ui zRRXWdw5Fs!lG}gXLFzHSx~@3byg&!}J}iI7yAN&U`7;JU+UtPQTM zE4pE)etRQu-QjfG0{iR=*pa6`yb-wWa3lw4DJ)9hu+(pD1g<+AV@U*s6DBXFKC}_I z?r@xh5oK_QPMfLU+z4EEIKUYov4+Lzdg?be0@odG(aVGobrs`MzrGQ;{{9u5unCJd zST*&*jlgw>(@08}sIn;VsbAaVaO*e$H%s7XTqjL(lfKOQ49BpdLC{rJR8#-+M&P=` znLNqcmQbPb)UR#?t~*>sWw5d!6Xj~^S2hCI9Zsz(C7jj?MoazjM&P=`DLQGDs|Lr= zsbAU%Tz5Ey)&X6jY?77KFKz^`JDfn54GVYMEFseiT+_=un4C0*tP0rV{S*^R(;Kb5Pp%?Ox8F{yvL5xDNB7|sc0wW?y( z)X!`LuKOuNaRS3Dw#cRa$Bn>sKLtl>RYj(hayj+W8-eS7%7SsR2@Y3EsSj*&xb^o> zNQy>Knl178CVho*D3^^dhaHO zTYrtZ!V;`)h-L}fr0W4N0*DYnSie}>q z3HFRXxTycXcCK~q=C|H>^|fEPs$TxfOK-mT+57K3|C-eAf`5Kmf8N@9t9E(s^#^Zr z&z(!X;q=Wk^2moDAxWaB0WR0F050Y%cp6(Uov9l|aDB1-dst_RbB_#Ai3G{hK9&fo zIJ6mk;xWB`-Oy(&69L-CAQ4C^vEW{pWxSr#!HO(Nfn5_exBB4s2^?}^eP%BHpUkur zhMqVu8VNo&l~f8~xu&4Drs5n+%BnCI)M4nrV%uTbv5P1oB)M+zcN*^JcZ2p#5KbS?)kA|WtdL7A9Sz)47K`Ts2|tx8Cmyw zm1a1h#H!l!YpBVmG}KhDSMLrXWe`j(z(*|lTvjl;rwlb2fkV%rW`Oo)2CPwDh8hp7 z&~B4_cQC_p{cJtU&*XN~^rB=j)YS(={n%#M>njY;3$kwGWV!l0^6HuN)Vz8~R(qT* z(ReJ8G+-<25eAK&GSnF0%!m4LH|ZipssXobo|iE--_o)gB%|%Ni}@s=U2Ep;Qfo2P zl?Oxpy2nf)kF+r-ZJw#B3TTD*yc+7K<<&HnRc4|h%$0eA5qW`X51hVr%24Bwil^OL zr=B0`kW)lz^Md3OmMa-VhbDV0~;-h8D-(s@`Ita9_Q8T`#uW?eF)VOXQ|Jtq4uBBP!rRhs)ci>S11}jxMPB7 z)6!1!YHXTGX0cSx70FQtO)_iT3B^IYKFbh}*) zyDBTma=uvf1UhfokUOKI)^Y+79}KnexS?MEw%|d=2Wx_cs`T^9tDn|fUE)+(wL{IX z%niETlt61oSvzf61g%iiJ|4zquaMyq8dO#qjS_2>QDl@R8}p37Rsd;xtxsmHrB@3N zhFX8zP_KWD;U!6@L~v&Oyqc?pr!`koAkoIRZ62PY(m?O(rsr1}^t8EJ;Y&4`1GSb~ zIVm=>bw*B;P9v&&W}2XEZyZ))kWfI@D)psTmmdtZ_PC*5|9&L|yQC93WEP z)tSWXa)AiiBfImSAre92cEi(#T5Ne)-k&m=YPRjquolv?WjRv%eHn7FoY^B`xthQ( zI6YtPtN8~*tv+t3*T47;lQtP$0B4%drwQb#eJzrpEGz?B(&;p+nbxS;uVg(xbIMSY zP`zREV-{)m%auHmKmsL*Mo&>eX#%KNNgR3D0zIZPL|#dP760D{&fWOzE8ltH)u|6W ztq=ZB%lZFn1imghI)9&-X$L0Jvne^(2x5wWdLxX+je5Tpj6G1?I%olQ4k(u^oJJ6{ z1NrEZAZ7>p&*ks$z(RUD+u$w+?nKPgQI#suDUZM*Zqza(&0EipUDzKs24D!m_&tsc z23|jqS)klt5H$u1ij+6L{F6+~RD?1)V9}|}gap!%WVVOeeXFO0!E9~~A{U#QT+T4E zrD+Fr0duInY*4ZG#7rqht~w8Kwr^UJ?Bqbx$>B6}r#BAfxgsy>wooDzBV#Degcm(Q zS7*hR-30mMpe}-`7SCuZ8gnVw$U zU2{mbb=mbrjr9qLU%U80}HUBnahh5LcLpg52_*NNhkIxXmH+l7lN-@h>hOeZjvN zAElPd{4902L(DzB=_dDyPB)8HbjjWIfVu2-FF)L6zB*+Y?l<}lSVw{6aS-%t9dEIu zp8d>EX9->@58IvLLhE8dUdZ)2;jmYq9h0%Wd_kd>hBFXYH8OAwDnR*6CXK+E#jmay z)M}G)4+t1cdc6gY+I_0C1rga9RA-O-5;|?~@T3fk`8@hqN;J>Rj+s03AT}vy0*?5t z95Kw->sY5Trp8vsVktrv^r)Mift3|J&XX`Owmpe4EKN0nhz8ZuFc$}{2{G^Xi{iK; z^xyzhpcz&Y%9-|EeC*w2xjUF=qj=KSbEB;57*Yr6=a{U*&0U-GMxqVj*mjY(cythX zU)wo4w}`xNtfh8w*R#EF+YNU1(a1X`mQINd7wpynhaMRDPeODoRf2BRptgBI_xEyi zVaL*?MsXYTA$oHDzl7*O0t*FqvFT6>*>>@6(K6X-+ve(H;K50*0J)ipDZ}GDM~Sjj zk0+$(ROX-t1GImY2m%S(ooddK;(p^!P-KR3Zd?Ap#iK9y|MvgwIqKZ6o!kGh)T^)l zA6MSF|HfM%-e)iWzvq5!|ApuO{QOU!zjt2VJ3Rl)t&4kqedP^%KezV{ssEVz#nd-l z{#VfdC5WKX;4%pZ`9O{fxDiw>BMg^X!^x0q1OWuPu z)GSN%TGI)K6u{wKg7v}u7f~+uv!fi5x$(a*5hF~3ntp%4w$*NSo&-K1)Z&9e_dz3I9p+n;q&HV17G-nF)yisgaX+0<#Te95LM2i& z&1!SP0s@r=A2n+Qud8Y$Ns@&+T}dv!VP#b(%LLJ&;=~R*>q!}W7(ugH19VtyI8}lu zuq8~dM0}qdC>Q>Gg=ke+4CpP#BvkW8X}RCd^LU(WW>odDf3yE-CJ?N3>yUt=Gz{TPv?i!2q$x3 z=qSLQ;L<_fHB=Jg0%@xEeT}-1S&~Ez>(v;G`Jq`wM`#gs!9g@ur1LF8m`?$bRS_^e z)k|cpiq+6nkLY2o+ON%uQ_dDAjcS26#+=X$)fQY$%WmR%4YwU?Fz2Z(yZ295k5N+! zMlQlcFV@K-qR;x2s7S48XE2_YyAdZjHaxKXtT0h7Wmkyp3oI{9j}Z_R3c0~pEeO@x zz-w1}j#+6HBG(ylfJ8!+t7ryGqXoFI_wp5@IxFfnnFeGDWG~Z+RKJ{_)tiVW)Mx1t z-J2+4nldYXx}#mVv_i;vDCEXnpAWM^BvM(SkHl75isNFb)zwFX7E0lUr*=GR{~xUo zbj3r8xKy5%v+jhfvqL*eRy=6}g-N02~h=aX1A0uc;sx;axJ$6LdiE>CA z&{2s~Lu~o35DkZ776yAb&?r4~rMN>iXEVMVy;T=;>Hw#z)c7@jx`}iZZB_juYLJ3!XgFq|a31#sfgN<(?1XkT-3289qfRnT{9X%?KoqUH&t~?% zWrZkB@?MzFj9Qs8T&`C;a%+;Ad!=&P_C>v|I<#SsW}bz|=*=%ZMr=(lapmd?!E_Li zz&6R#4Ud_?(hRZtGeB3W1**$ZO}&oRidZ3`I!5=>9~>hr9M=bYzv$*G5jMkpr$rOl zjvXhH9x=t?xQgqXWuPi{(LY8Ir7W!%due>uiX;;n%&dCOqeRj!`Y2J8#!xvk8EZjk zU0TtCj_BDSJ@yKwBn5r0#ZrLrTw&%W1RATCiz3t+BsCOVS=wCpd~>CPYBfd0cgH+! zkFij7omSCr89`sDTWy*&^+L5vMt0kovghBpLJ%aJm%3E?VG&BdW z#1<5~Sf=x2DK`zO{8(#-Isx$-(o?Q~8TST?phVl z=Dv*Mm6}b99lP(V4fpzcS5`?KKG*BYjhtWrR*Zz>;iH^{0JpgL`B#7biFAxG z-0d_nPOnP{V~y!`b-aS7TaB&(DQRD?WAzc0hwDUd%$|S$a^)NqXL1W~84OY-oNtfWjJV+|iM#iVrorD|B!E>nAd5D5;u~ai>rvsuh?an(j0)5qBHuLC~qQ zrVM(Vw;OqDIJYv_R@8^XxTLe%IP4B&BbzQ3veMMgHUWV^o*TtuRhn}a(&8DkRaCC6 zC<8}@JQAa=VAw3spk0p-JA+=;E74sp z8Z-6YEZ5J4W7QPhb1SzRp@pV^BMm**sDykSBb_XZASM@A94;A492#&ot%PirN-ira z9dHrNckE$P$uwGeV9br!bK@M!^lOAWO}9Fnibdp@D~&ht7m^KuDIOWm%GF{FSLG3hQu(oG87NaGD{HohBPSuIh+c{(asQ$68XODoY+ zP(a;)$srvUrY@O|MXw>s#!$=1RIv9)D;=|}8X%;ZMoO8Yo*}D)q~uE(w8P0<=;7Q1 zEFl8joM0Nef3QMK8A^jH4vHvcC1aAcp)1##idi$rPOl&hdk&cagn&k5clY;Kh$KP* zolby4DhVZKJI>^c0UOI$y2kkBoClnzYL|Nnh6$4leW75YOp~o8bz8K~t*G{fF_%}st_|*Y;an(DB#v2H+j2nqPrV#jbFYCz1up4k z4PozBR@>dk43mi(PRi4w=IeO{Ff`eDJjTRx1nayxW#;rK5=s$bn?`;$8va5CiZkkBq6GksGd`v?@WdH*KrZ zfE?ARtG~TM3>nnO;LQk*NX7?DUxRR>^yE@C&6drMT&Vga*f9^-p2=SRZ!1I~OGR0C zBDF&+<$ALi<4HGhYj_rHHIf;u73COo^X1JJy7yhj2tZ65DeVqmkA-Kt(Zwr53(5I0 z9!YHl>iflFLU@HP>1lhb@EFQf%bXG*KQU7S9de8Fshys(#gL8rgMuEU2WClQkecI0 z=+$>D?*G5woOJHicij5Io8Nsif8$4P{DbQsz23g|v1`fIzqYedYYGp11e@Q(1{0f^~8`^Jk6<+V4? z9q!O_{_G~}$K5R+?O@S(b`JF21^RaAMZdMa2Y0Sv>)9F5g8)4{G^gKM&&h5j_pUrU z1*%^4`g4anG_2oR)#?oUaR;w`x@m@O38h@uV-wk{aE0*0Qw76e2<}^7fHF0w=8RU{~p)OH1ypf=-t@W(AOt^SWz-4B2#$;=T zJb@bJQHHb(q*&+Byw9|yg6j2Lk!=*?0_Fi8tgTgTcA536r24btg)QK*894;pW^ies z7+6Qp?R>s3M0N-5VXWgAvu=kQ$Xt@1(JUawOt_X41%O|Y7+H>HHrL7bj$h|}ml**a z3zr$>#zdpS=cEZEwEci>v2!$6Yy@VK7pt|f$X2klz|M&_nmN14{05g90(}dYK>=@- zo{Pd-IBQP&qTxF;qtz>8?b2jOf_6|mZKY!x3FgA~`cAq`0Q4+ehPS2!7rRlh?=gd- z7ujB2treRII>`^4%rxJes#3R??hO3a*3Y%(GK0ly0xlDzaben@k4tV_&`L047lH;= zwMzUfQ;|d&Yj72#IS+c`XnR$gU1q&>p#E%sVGDR{3VfbZX&i4b&52P+rUh`iuC;<_ zQna)H?~R*HwLWM}t#T0%4cWn@gjc2nxT>96+`w7q=Ua1`9?-FH8CY)U5T70ooZP6) zmotu{!{|sZ(CHo}=Q$;(f%3*$$!}4uvzy#+aG5U9w{RKNZ82_G9%P}W+ z8sNhtJq2udK4=55wVpMX=`3CoaGAbZo>$^RDbtSa+!!f%X+}brxH$rOIkSA(YqN-J zvKfq_l&w{5cA51Oi2Aebg)QK*)1IO7H7QqY3E3VU%{5o-$K{SbHFPJ)JG~iN_46Im zPYQ*tZCTASU?pxHKX94LmC~7^gBcC5qw0iEBvgjhARA2cgLIyA`aN6|G&=~2tpm#F z26V&iw#}Y6^Cv8|&TV__Gn*fHy%3@QY!leB@LyBGOwi>uk%oY`y;-cyi2;_*Py0sA zYEoSxDv85%l2B6LaksYRw2f5*=v!{Am~lMKVUk#CU}&Z(!<{tamHI+S)T%@RI(^`9 z-k*W1+G$~HeJB0b19}$zOBl#FesO9S@)F&Fcy+%vpF%MxtD1@!+Y-6ytx zt~LL4k6$NN%SpW%;GgSt{b1713@To>8%o{$bkH76@M&NXnuZI$Q`lbB=GVDi>`;H! zS=a&|+ZdY2a2ScTvaNXYxB+TB=Z(0Yu8uRgVcaD!m7I^aSB>HYUx}Zt_Y*d!6f2BQJFXm){I@12MnHT3=muS z?|R8W|Jknswk-U2KD4G#OpS4V0KwUEUGEFcifJ_>qRV8s8qT%xQo$h@W5{l8%W3{w z1NxTpFUM%CV`Ph+s}(iniIH9-m6THi_keFM<5@LzUZ zp3EFSBbJ5=*_QjB!QpuumKxb?mgFQ;vvLi+E@4AV*;>z<|GxhCb?O+e5!qsQWJ1uG zr=+k+b_E<7W_yaIq1n7lG-ptALh?;?YgL>5cfG8k{_IySYypqm>2OlIjiiYzOfv0R zTN+Z>xT+*!EuI6))4=5-Z=hH?SlQl|2mbrI;|Hcugdeerlg60pMC1ZT1iXTPF@>)% z{tyt{K|R?|5LJhS?%8P=Tlw#LaY6rC8`!e&Ul(GkAv0t678SA1WGE&>t|a9vaaPj7 zx&5o>KXm@>=ehG&_dc}u z_FKPr>n*qNTYEQu`Q}@1;y3qh{PK;r-oS6{UH|3lZ@W%j-@o>&*WPxGytaS!SFgV9 zDtUGP%CBAdx-0aRiz0)zpVlZ%=WltLHv+?i~-$YEu95;om>~w@L2}H%}ssVCOnc`|7NfR$>v0=WU8tD(eHf6U!}U6@#`B%8c^UUtjY_O1^&%Hlj>~7 zukXhgiq%z_vJGj8|Jjtk8Na>(kSR-LNkvle1^%l(mTGOruaBQZYZPGf=1h8t|C3aE zGk$#nCqeNrZIr9p;`u-PzkO}0xf#DcADTpop~{K`Xa@oQ_ur6eY{H|^y8aUkYvBFc zTIyRj;yY9CdCi5_9b9-EPv3fmh@$24uf*`Gb^t*pI zPJQ!c{JQs^;7J4cKl}OAH*LnRzx@-eX8`~Ef0{blj9-8KC&5z2^kA8Nh$rH>cjd8Ncp5Cm7BE{;hwP`o_)p z^||u|(HX#h^$Swpuo?e|_dVo019)#g_4S+a>)v;Q_6*?p7pA^$Gk)FsPOzT=JTy#w z?PmP?+;xHk4d7q&vDDi(@4qSaHJkD4-g|-)t$*~d{+dm_b))yLyU#;r zw1xLx*n8PV?VAYFfcD?}cTO6I5yd|6(Nd#?AP3 z?>)h&w!q&_%{SxMz4rvMTL0)HfBSDzv(5N*?>)hpdV?0bLbFWl5%qxY`AKE7hulu;J=h58TtuiQ&FYR|22JY)gv*g=ol3bxSx z+tXBkGk)EBC6mHMo5re3>woIcQoU9E|DT?_o?QM97dojw{nYyA{Is=akHFWTKe}|e zq1-$FY>+xMpgL$~q()(x?3md>*ll;vV%>7v^{h2?k+EAqOCiro0;HUMcy&rWQ#2hM z7v({@Wy#xV3<7!h(E_KJAMQ|7y(szFr8H~#`#Tg-p3Wf9D)pzZo#WVvoWPqcst!o^r=1JK(aEM@)tw5coV%WvBo) zOHuLe-I;_th%KPlP>Jrg5w9mmc`A_|#4BcdlX)mXB;MMlo_G1^pwJ40;l+nxXhdou zo2YWV>mnLIL#iV+k^Dk4Q?PruT!Y7hw8HA;!QA>p!!TQ1yz5iL5c-r1!yV4wpYLG^ zogIb?(v7DVhEO)MsHi-(-tlfO=OPRYMRSWDc5_4duEoYaG|xA^ySkN3-8 z`JcNQE;lNhSHq_qhG_AzWyhwK5Dn1&G%$dKHx1kMal?DgE_AVJe02E>BS3Rd>o%4<+QlP0 z;kFd`BKlZXt8N054b7q}7(R>{Pw8oT(s73zd{--lB}hf*QBTX&u`*3PaUgn=9;{TA z(R`}1QQ5IP0YQnV*S9o?cZyP{IhBSUokX!gGj}B2#LPPD-RVx~Zm&oXcO#Hu0Wo>8 ze;22+T2Qz%E0qFbtMzdC=pYbR`TxUnx4!k(t8f0oP4&iK-gxtk&%XZNYyb7yv)5jH z^@p!!ul)KI=kkBO{2iB(OJ9HS4=?sE{N06jf&Bk}w$Gpcyxeu2r>?Kwow;4ur@d-G#CGb@6#%6>VN zgR?un9#f@#$zo;~R$p4Tp=|e_dS4qNREizUOvr{&tMe^eCzWnFn50dUlLd6xGzNIP@L&KF0Zx#wOQp%E)E& z_u=UrG73-j+6w+X;l8pu4qCVxZmt=-D9s@l3zEzAY8q`weH88H=u*CwozhHFFQ8AR zEbred-&fXZO%$Oy7VGc`?bF%X$N|M~IGEjs%v2^ol7DZYibaZGBKFCY<@|g6ePw2( znn_o&+PzYx0kTEM`C>)P6k*kG&M;7EDrR%Dq{xs({|S|4{d?SfWo%ujRP`~`%&>Vr zH}T3G#lZM93c;0jYc?KnqiM5%&@J0mp6tm`{~r55S()R;iPxG5c0DRMk(E;-$gUb* zJ{bxfvOA}vn5~0`7KHX>$`Jov>AtdbP^@bcwBr|IblL#SD2_OC45OjK!XWL%+ytNVq^*%=Za`^(0jD1Iqb4CjOp%>V5h>@WPZ(#= zy;t9(FE3Y`doMk#Hi#--7$;~`rTkVcl5+M4&v%`=9BAVT;h7ek9{Djga#{!DM4RaN zxEV1e5|?_E-CxUpJ-O6iw~vqYZb!lGq~3{c2ZP&A!>t37!P`;0ar-&RhPV5jRy(>q z2%_6=&~M!Kf@E;}Za5pb_rCBRb!_d64|9zfZwXzqu4vt6HDb747fS37B%(=8i`>a* z=E8y6lTgJvAi83#3><7Iv+jhSwaD(%v(^h5;owz|*y`LPSFiEbp%~?6tlMuv_^2oL zWKcORjL|x;xLve8gA}f#rDucQSe<3V0}YH0>2(>>P2x(KtH#3(mcq_G;=Yn<4i$&x za1ewoa*oWIOuN2H91$2wQ&mtT`=U1 zh>zPbriHwE1a#N2GccEyTH0Kx zH?Tt2rePiOYzO1Q2A64-(0L8_+93-`J%Gs5ACw*O*-FNoj-fLIdzUZ*kHj;2wZNyBjXajPkHmfIH z-YembYvD>X$6~AvmE;CEKRUa}>!dd9_Ts4ep~kPcudz6@2N2Wms{YWNvylWkEVYwN zaW<+GKG&-NP7lvXH~X$q*rHx(lg8KF*9a=GdkU2hy(%%w*}VbZQH)7?@SVWV4ZSZ9}*oYbhEc*5IoaPQ0SYZT{_&d#79Hmm@3l2*{nWGC~e!IK0T_5d9T z))q&c+Y;?9YK9)G5xV!PdtY`u>%8!g&EaiRf(Zd?CV^c=a#782RNgD68FywOc`GaU zDw3HRGwbX+kCQr$#kANA#<#(T9Nl`6n3HeD}} zQD=)gtdoW=91cz+?R&-hs>)>&d~8KhEd!b@__a>IqGocopwDI@ujTb_tBkpPzRJPG z6RCpR_X_t_>17S@!9??H++aBqwdpZFF=uR>Ne2~$WJ7b7O+!Ojrx^C^v&cuE3TofW z-&aNHd9Bp}%qEm7OO>fH?l~iqFXStE(D*QIDIsiut>>g~q?0F6m1*D0-B(p5CT>?~ zcjajpZV>KZq-eFcMbv@?El;QHl!ZoO;wnLaV!Jcxf7hQ1TD+_5eN|8OYV%Q5;9a2) zR6Wh>n@3fFcZJ+n^;D0jA61oEeE+A<{mi*r@4HpG`L{QJ;HGfnZ*F|wjnei1>-u+H zC$9bVwQs%l6<7b_>N~F%ul(tiXRn}_{|IFG-@WwFOZP6l`r_|heAC5OT=?*X^v{3p-Y@L6_dXMx3Am~2K=jl9qaM9>e@|&#{OPk7uw8fo)T6Ihiv7SA zVt1ecP>)`-6#G+Kh~1&&Up@NrrP!a`LhKIJ`|8n`Eye!C*~|4Vi@$pGrAx6tzJ=Hs zlOJ^?$7PC@AdOa>`gk(JD2mdD4U_hVb9dT|y=K=N^j-C+xD@+iTZr9(UO+u6EXBTm z3$Z(}2&hN-rPv?cLhKHt0qRk1DfUOU5W53GfO?c&ihbV}Vt1(ZSC7!8*!P}Y!F{;H zUQazjmSTT+3$Z(}2B=5yQtS_HA$A9H0QCr3iv7VY#O}ZapdMwGVt-%@u{+fLt4DX2 zV!wY2u{)ITt4Ck56#Gxk-ZdZYum@L<(o3=bcnh&RY_!#*FJ6lMM_Y*9fnz{Dy0aAf zeOrj#fl5Fk7u-Jv31J-WRV`<}DQtPgjn!B>x7wiNsBEyV6n zPOlz4vlRQTEySwZSIw(OFI|fLmMz5YP$aJ&eg0DHJGT(KLqWWH^pd66Z{9-e4z=y- z(dRA2e$&~Xv4=ZUv#UohUWz^1LhKHQ73$H8widg?af^EN!ll@EY+>yV=N;ll z2XMl-9P5cwo1(<8h=9fw+>=8EM7=W^^#pTvr{LyUzmHN2Y9&=Xf?!=kTMiL_n z29IWsm^(!WJM<2j<(Zu(N?K18ckG{w;M2@a5!|)wa>Trjd_7e_6 z+=~wS` z1kmKcw2%gkIDABkxgev-=G_TX$L`?OC(lT%mXs_aNo`zKgFFL|?vxUCt{IE=cavTd z&$}}@;}@Pho6v8xtSY?EjK6Q!@PsUr8yaqM7^yk0@>(?xy+PxMnVS@^%>u-zh~|EY)VW9!>yOtYRX1AesPJjeqIKD{}g zcr8R9AmPkqNutyp3e9@U@cK`l(Vyr0Q0vY>iEITy&=J{U+Kdz4ZS;s_sCRrP=_SR= zppFvpIGLL5d=p8O4#yKoVCL03bkFo$}9-m>?&+8KM30$ zR)9}9Y?o8?sbPzJN`~zYH@?f&*4X|s`uVrop1DZ|cl^)UBtPY{)GYGYu$_1OyJVQf zgLq)qMW<+szG&(3-Kb;f1&F2Lfb*C;YEBEEe6=0qb0euZy6Zr98)PGB%`llq#12}g zvc1CW4q^!*()TfbD28<^@kUi50?echYB+}_mB(nGop%7^Bbm5?o@s^^z$J>4+ij9z z@aV_+|1UcC`g6Cw^5)Oo{M;M8>%V&abFK}qe)#IEue{~*M=yWjrLVd8Z!f;)!gucf z`+es8w*$6+A@!rFO8~IVpL?%7dd01CsRp8)J9p)B>cWNBUp~JJwe@lGMK3ZpW_Ml!^Sn9lM@zZ@4|e3Z0|!}(zET{Us(A1 zWbyM2@88BV8Tl`d+tu4HL^pdqvytK z$;N$P<7(DC)5zt7gu@FX@h^Rxk?Xuq_rCb(#S0^!Ga>D83w>y@y9HTG;!X32BGx?PGgSOh{+ixtx%Yy9+x}cWXOedGx}CozIz&cDOG;w(~g? z(wUYnC#3X$URZkf_qVq6_R;4qEPc*|w8Itr12R-nC%igrG@ns)> zoQ>-oZ6MY^dtu{qVtt1^gJT;{iS;w>UB>#0KVxC<=RI~&U+09ocX;$!3wxgv>pLVc z9NT*$*3Yza8S5`l7IuE_V|Sj9*!jw%&s^B~oLJu>BjVW3=fwJ%mM&xcGnXFlS^wwb zJm3*aZy$ZenwRb!?vP}$qn9pY{nj&!(Y!U=+Qw&&Zmq}q4hb8_CLYK7nZvlS?fR>i zKfiWs3vVCYTpPu!yCi=+cNEVokGQ(9ZvVB*pWpd78`t>|SNH#au(<#KiVI5W54QQN z|Mc%X??>S4;-gy&ewAlu;Og^fb_;oK1_HWZlRE9}wQwq_(GKCcJgQt?t2JU!ZXW{f zy4SsYmu4fUpLH(w9*a}clbk1K#{Tj;w=wVl6tI*$F0$6=&;{za;0D0n`0`bQm9X6z zE~G3Lo5gQ182jlj#Rsd`+G>f_pE+ylQk znDlxllLufR>(N`t=MbgrV)&D09RPz-V9BXj2X_%Q9yha!HG|k{?+)DSN4R^3%lD~n zoSBgg0yQPIs1=a*lV=@N{6fD(QCXfxVIB#j_M}wOsD|9W8_<-{$9i5x23#&?I5L}j zvlF)zM7Wd1^mdVAvYnVwh+J+`86h#|7RPPSAYstByXArE;?co@>eqITZZ0D48*8au z-1Y2i#4RH)e+skF>+d_q!&qAU(?j;hML2xPYAO6AMaMEA%uZ(n#o}?F)Yha=n-Pho8C6fPVT8@I4Xq!waY=LGZuL%qbva1Q_L#CK zjk}~X;%Ay_acz~d$YBe2nG#10d1AY}`o*IIXa9yvM>iJE{!K4Q?Xt1i_0A8?fVXto zGLtWSGOqY`W#M{c8QUQzaN(c3R6K9L?47xvT&M>1+Gi|#>l@1dkG(e!a2z}9MQe4f zT@|uVl9}n6be2h{sw7#mtjTIE-fdZ4WXohQvMkH8WXrZJZy__JCO}}iF1ftG4U@cV z1jq{kLV%EjkR1{rkdOru*05aOeHR{gF7P1SmkaNxmgye5%9eU6GvU6j{-?X^+s^sE z?`%5f_t{(*HSHaJ+MPP>!FFWKNb&RLy|XO8*`WaQ66XVJ!m`$@qe5P;B-K%+(M0#5 zo)_=e`V2;Q3UZ(_;JI8qQ;a2U`nDY>W@2mrj}*{BS@hLpW@vIsyX^;+C^cM2OA0LJ zE11skiDZ;difK?vH5$oTQWb{0Ub80^h;~IFnt`Gj&GrUNTDcmh9S#L{(~rOZZ$9$< z-^HDAw_}0Vz9xC&rC01IPP-jhY?O=+h*q_!fEfh#;rJ6VI6P*UX%|Y3mF0UpDnzW-nFC0QSI?sAohv1YwBsMPn-DjABz`~f~4S96>U#j}x|(e5)1 ze>+wvHbJ=Ai;kOtrQY9H>M4m%^!>wdL=7QcCNyYt7^9sX#ye^^wht-LKoy6{isJWk z5u!b+4fp-wE|P0z{ZZKuCs<0z`9c7SJQT&rrrhpYv?KgK3#@>j|Ayf&fBPz1)>m; zJ7I4m*-B@alyBHT&oOVJ$dpsMoupXjnr)#G8fNQ*T2K%>k7@=&oZk#D0h-~t-VDCL ziKTL|JPgJuuqpMmOC3cj`-Ye$7DLTK6Wlv#lNGbXDCac;eRRPLggB=e6LgQI87TBx z=7Hs-X53~66%^?X5JHZ|0!$00U_VX~@mf?y!bTt#mIaP$gd-`~YxJt;eW!8ijhkqO z+U%)a`RuSd^2gG3e^m4Sf+qNhN~@qEDaLPNEEi6rg^`~Dh=)i&Rj;8w8XI|8?VNu0 zj+X#&O1EQ1n-&MEiFd48u2rDTxbUd3f}km3Z#Gt>p)lps)k=VF z2nm`Bg>z`Sq{oU>f=2veSvxPuz2g^l&TGav-A-2Sh>E3#jG`g4q2Nf5IBy0Hnz3-b z84r3QC0!!H^N*R3h%0Ps{pEN2Z!wh)gEz{+#f2{OsSNQ?)uL&Z~TjqQ@YvsY*T{D z3z`A0Kb_7T6t9vk%WMPmpg|gm!UETfVaR2pYbVZdDLs&oU?}mwNT1aC;8ERN^qAVwfVYAR51(US|)52Tl=7z%w z&^u1Iqtz{$iiI13D9BZYX>l%6987=*_g!yBHXfrWP?5Ez#Ufol&Bv@ldqfk1P_~OG z?Fg1=2kLR$SLi0W^LqKwGY14YrQ2x&tj4$wz*LnH+h>DTbsUdxiZ>w8jMrRmh70_i zbIe1CvGK(z-E0`2^P7QydHmpet~bL4%FB7?Ih;3;ak|-HF;kiW=8c0_UvGvBh?4V~ zF+LwSrJH?0a!ND6ym2tP-V7JFw$3xp;k<#2)6Ks4HKiF~-Z&UuZ-xtmjPsf?e)Y*I z-E7o`Db2t@GX~e2;o{25dFG8@J8??)n3{otX7sN&!v&ngdFDBs#gTEk+21OrcmvGh z2fgdfaPe{Sykjo zFaMb5@^)`Ky#1`@`qtlWeRS&$Tjecu?i+Kzb>0C4XvnM1``u&x|J}uV&#P?j9Ulzl zfB$@HFEKtq&;R^1>hUGf{Qoh9dZJSA{JW=7k5An5KRu0ld>)Iu8wH;sD2J3l*(dVHjtfA2Kv@d0K2XQok)=lA@3t}cG(IrZu%jvrn!g?79{ z?wMxA_zXP%b5kUb2dn>l+KE%RmrbD^59^nnPwN%OmeWt0(mH&?fB$J3^@RW4G>v+~ ze?K;jdc2AAKMF>#7dst?&Xv+qx4$@zdZM>~ZwmFq4sHGm)2PSOYyR`os3*p<-Q==2hwE53Yqn_~mXQoiY6Z6t5)2Ju<`xDcsCp`c0Y19**|JXF@iJbrFH0lXo{@N63Xu_8tnMOU~ zonM_sJ>mHePotjj<%gzGPk85p)2Jsr|0~m|Cw%#VY19+m`Q>TU6Q2L2Y19+G{KfOB zz5a_TuA?pMT3V>WMsi=+^%KEdPxE z-}!xt-rE09-dDf1|39NoCPx2T`+vji-sAT}(5?Od*nl%_yqQ@2+}i(pC)P^0_W#I4 zoZQ;~-`f9A%u6UdvE#Y5|A!`4)wlNl(8T)X*8bmrYyUsCaJltQM?ElYeK#@od`tKL z_WS?mE!{Dj>+VZmxpeK4fAha>zIijW@vq=J{=)jttryolYwutCuC?{m z53H6~x3=HA^5K>0%9A}G_0&C2U;gBBbNN|IzX2)$-m&qX#ot=&E#9^8y9=X*=g)s} z{=WGa&Het|!L)8!e9N@o-um6G9=O#5PfQPR2cbooyW-;h{_KnD+(FQmd7-7lxdX(3}{awGwCPpPx+s&r3)%1uogkCO?n0=p`JD_Zt z{Y1?Y;fKhPL2(sRL+gef4(%ppUzp|&0=CSQY3))C6@!TZMzcMk42q$5 zT0zWyD55x#`3hU+g%%;_4qk4{bWwV8_FZA_;AOT<7ex$bU)<#mUTVv9aVvNB^;7QP z9$Ti1+l;esA94qTEi+VQiRg$$GMa`U^%6owYpRjJ@nCxx;#A)-Yqseo+sZ|A+)geL z%&|=&-RsBtnUp4k$+1g7+=1VgS!;5`26%&M>9Em^_V5BIkeDCT+S#n8sStx81dV_P z2_|MTJGG+Kmz#Q!7F&@N$quXPgiPF)naTu0iDX_%_QkH^_34HdV>F)%H)604OZlso zkx)SOY!Lj?PP`XL^O07xD5tqWIVM*N6Ec0a%%0C*>E&?5kJe+Uc(K#0_rhLpBwMT? z-9oby*RV2F9HeWdu(p$_3SBW*qk@K+tcI&OzA+&avt?QZv|ei<-g1KWwv+KtJ`^F5 zTDDe7H}ss2mfK_ltLG6UE^s@^RHfdCR85kPM6_O>9MKapUu4T{K}}GjJCQVX3-h+o ztW>O4a*>F)QKACvP>5l|Y$8>~hFG83=?$r1HJ6RXglL8qE5TZJLgou>nVf=zeZ5wp zmXC;*>FrQSpMWGv>;Oe55eRCO!-O8C0t71Tlx4EO8C8R=4w7VdWLn(`na{Fi_L*X; z81Lzo-C7Z;mCIyhSE|**J*npH8^o|ztESOLKN`y+(hjIop|=@R>a$&{9c^~2Sh>U!ZjhyRC8)H+p6aq ztd*LS`Al18sGUj5Wda$d?# zc*Y{jU4bzGGXxMl{21Ysi%gT_dSN5yfi+`&_9nU!P< zua*bHdJaoTyE!>kN%?w$tdQVSkS)tj{xTL8KL6QrpjF?D;%y_dWWE%aNGG=G+ z;K?^O7>5a{7*8w7F|wVi70yJZQ4=5nH4VXtr}ASI+`*G<<2n(hfR)f}OrgR-5+C5P zZhMfzmAou?%SsePj>rdavl>ovJ2AQ4$kf%USWm_VU8P;CkI4)kY~N@caE7D+G0d{dO=I z(7a+l3=bkZ%y5`A^a>TvRf6GBAxo#mWCjm5Z!``iaGxAfa@8@i9aXBynN&h)f#NUy zNQKk-V-(!M`cbGLy(k8y>S->XY4uDl7#PGG0KX`=*|(aJGgHdr3j8}XhxKCuK3 z7H>2TfqakxFh9o0c9NX|XN}Z;COP;^oKeCl2=hKQ{O2x$Vzw zziGR&4Q+jM>$6*L-1_dV7hn3urB7da!=+bUdLg*||MAUhn}y9Q8xL=MWaIT4*^SHV zf4ly{_1CSZ)}OQXU)FwU?R(eaYtLN$tJU|de$Q%n^{FdgUwQA!V1-_JlIM>-@Ah;( zuk>s#e|7m?%PsKAV0G!sOK)G&m;6ghi(goL%VKR2U7TC^+`^j{Dhts3H|IY)|Hk?6 zo_{g8>;GwIyB_$EUz8WFbVA8^AVH*yQawtRdDzqm&a&`frJwT;QLq9oihaY+=PbhT z(C}f-=D~skITJ&ryrhI!ABGxU*l2ekrGX4ENmq=T&)aF0!ia`Q9tu9p+B_~Eo_a|5 zFk|zeMbVp464|Q6N~u~Q)eI8>B%bn`<%~{-BVkKU@xgqmniq4P0DPFXd0gxgJgwzas}4A?xZ&Exl_ zIFoLhsUe>hQgp#o)1_D==pVG(`9^@$=!%{cnM^4jLT#R;&4cKsKgZQa;pm{7g1F%j zq2wM^Gu4JPQt4=h4V0BsoJpCrgw2z%c?x-E5DBN%V6&8^6X7bWr!X8-J4Jq^Ta20G z%1}hhq!_ax**tNZCthim6MAh##rs7gTaEDTIBazacn5L5{xyKJ!-f%;+g$hL^hQ_RWz6dx}&@v_yag3-2A&j|fQ6B~3* zBGtzM5)c|^@=S-+Y}>$$zi_3bq>xZK=@0E%ZIh3rTK-%SpxbhNLlreLq+7Dp!^~mU zf+IE$*s+6tGXeV7h=N6bhZ(879}@j7D~58Fh(C&W^=h?6r5SEl$zci4E8s)0BL{k- zRlaL=(RwEnpf%rsW4rZTiS?IIE@O6>w4SoMBwB4o8xhaT;X|@FCby z0~}d;FoL6q9Kg&R8ERuGF{I&@XrJoFYM3R&03HWUo57aiH#{$e55bNZ-21 zA=nWwTuJ0K0>jyMCQIbKL}O5klXO~-dJ|2Jl$*5@$AsG$A@Sk9-{t{3-i0fy&o@wp z@F>&kSCt`K>Uf8ZQ5Ft|LXknBLG&{|EN-Nmoh)Q}?tu@%j&|XSY82Q)J{gxwb*UO? z)!S-8s6y39kcn}nqIXcF%EJ)a3Y1!ghky^kj&0oVM?AO zSUd?~opOn&>Y04l9LCkKK~rShN;N8Erfqm|_z>(k7u@XNJwEsl>?jx9?9goAr4o%NTaLSJYbi(aHW|H)<_~%_vv*a zp7NIlMw3fo3S>0UY_Fvh#6~fdKsmD+P;4Hsi(I%;9~IK7sqhvN_USyIsTnLs+`P3sGuuELHqHseC!gy~$Dl!CA;Yc}~H&rwc@r!v#qypt#G2I0C zQAFg(Hn0m^xT1HYh_0E%9>H@88CE+b0TGMD@H!1^W>i|xp;|T)NT<{2Fo+XQV7A}(v>DRhF ziSr=v;frk^7pGAk7(Tpe^9)0HTV=~qh)5f|Xu2$RtagB57``PJg7wx&8dQ6gs#a+> zB%259)(z`Ov#hcC2wypq2|G{Al{Efh5>*K2?U zYLIS+t7M^RHGO@}FS1ODl>4aS*@q9ot{=2V3>6}!e9D65cpIVxFaV2D6t9PBtUp`P zrDQ}Vdj3EqiYuNy`0)8QPbg&?K@$l#xqLuP@I@SNB)UZv)dP`oIX~3YXfqYWNj z3fVl*wRun_UE_USvt$mdRwJ3Omxp;pj|XaDt3{7=C>9iUdqJ-;NJWPP* znO%~ULscv9?+^TgP&ua`3_q-53ywm1!aT4iy0epCe&Euj* zlFf6w&4Z+59**sHnq^-*9*aX|jz`dT${*a-nG_!7e7SHsRSMS?MzMLGWAlJRypRNT z@EBYzmy_OXo`h7M7?h)EHPSE%6RVbl-J)6EjccCg!-vndd0dq6@jMSc1gA&~S6sj{ zc&@;Q;FM_Lii_$tp69}c;1md~N29**F4QUq^726IswUh`q+3E0GX{8b7mt=3I+;u4 ziiSZbp1a_~XV^RuA6YA~2$g`VeWRH1`nsijIUB3QcSF)p@ZtiNsAs*NJri7DG+i$awsji_c|7auh8C85e_iEcRA+U@rlwo#UBo~PM7E-IpUF2jdU zwRv1rC-K|~AA&<1urDe3SbbPYQMy()tOnFfS1`?FHGI&lwJmaB2%xSVnW&fjlIITi z@X0ohi^3hA+u_6Suz6hUjXlqS51(Z7xTtL5c{Y5wZS%OOF5!6=e7I%vxTy5txeY!9 zM>wu(E_j{^A8y(_E>PAz&p5{afBM|!_pd%<>232*cedmHU!Cd)mJjc=agDD%W8TdW zHv6N^7>KuBeDW+@W5Rb9bg7F$?g$yw#b9+r?_xka8=l|A$bA+R>14qe+pHU*`e)V% zvl(V%7LPxJvAJ%)_9``)0pMfaN~UW8?+oQ@apVTz%-YMM{yZW^An*{H?>6|mLGkfTwt?@tdiIV~QO z@T;abR@qM_d+1e&PILyNtdCR_T#W?61w9%PDkEXP7Le;jzN4q$W-cnk{Yfm)jprE3 z5j^Qp(*Zo`_jeEPuoLk?Yu?Se;NlVy`*)d$ZbphTDgoGlF{O^B%jOf5fR7kpmjY;6 zWnGmXm3KC1@3%1T{D}8lG;hAyC|J!w69CkI~7qYP^?jy3@5!Tp`r^EXU z&iZdU@A|CK;f6hq8tfx{(x8KRwG)iW89gX8!*V~1F(Xr{3s?IQvsN(*y{khk4q$}+ zjBiv>M|mG?MXib$%J&+5zFQ0Au0r*WGgx5UZn_QZuN~fQ=iLuW^KRDG7ngVFS$X%k zPiSsu#IcGRH&*I;f-=c6K}`*iMXx@$Zq3AG2(Z&3$VRk2;w@F=s%Yt5Aj8Jmf24v4 zC}^{HR5s+KbPZOCGGU@U`iJyyLCsuIR@ti|2pb`irnhfz(2e|(YJ88 z!4cFhYkp?YoS>h|HaI-kt*Tbf>lb4A!PPJVsT8^2yee_LjFscM4$f3i=%$AoHcke} zwBn~3vp9$mp{qtkWAawB;R`brBWVVFe35Ahgpz1PVqvmfG=iKj4TbbhJae@gWvq;v zlRHJCWr9yC!C+g+cVhuZRmgF>>Gm|=JABU3G!M#ExyZST8*aRhb-1x-(W4M~BtbyA zDmUZR8Po7_#i%D};CL+Ms(iN1Q|op-hH_OC;}y_#vkPn=>N|IVE%_5wV&Ic=mbm#cLZpGT z_r2XV8AlXDg9^b^8XYv#S0y#*>u7usTsI&CbcNX`D+BQHbdaMlq^?I1e~2#gxJgMug&n#ZZvh=U9In6XHb}BQZpyiUH0Cd)q1@3pztOT9O|% z-CmM+55MDRNp7FKMMJqrIgb(GCh0EKYL}v^;BL+8q+)cY08^=6(tkxVS__ z&RUXx+b5zM1J79UKXULjaq{*!CEz0lfNk&NfAWTbTgB&|ycz9ircp5xdR5OdvRbd~ zhXx%r%uxhE^JbL|B57Y0=|u9$n=h$}Umfk&(rVSBO@Dn5Dk>ebg(^%iQdZbuF5T(- z+Fe+QBnEYaBBQlN3XS>jVJ^XuQ9@J9Y#EPVHFDKlsx7HiwjWmZ71LoIJ-g{3@4(61 zlkB{+Pu^Sv)5YZ-d{*AM_@;QYRDH}7np**56WXIrpq`kiZ1&09#AJBuc(V)8NxSz&@X7MemNb~6i`3P zhwwe46HWOEAK9+$lPz-;Xy;j^tcHj6Q9N1=LD5Q@CtRy z?^@qk`>$(1y{4~STK&T6kFUOJ^|qBiUAcedvga>7@Aasjz2(m@|H#6J=HE8|7xVFj z;==7)Uzq#c{Mw~|*=k>UXbWBb&gEw={mIgOOLs2*#o|vcHW#lhJiPr5@Xp}QZ^bdg zOP4wCrG*`nl={PzQHUwcOo-3clj*T{6ep+DiY1%fk|;^CX>JV3W$qO&m?syi8-w&R z_W}m!59EWbUM81f2E{lprI^au`j2*D#w%IrbV+9!NKyo+WX z7dKz~_-Sv~o;8u0Q9C;-Z*=17%TAr>ka?ODu`%>5gjjv$g^Is1eQZ?0 z)t8<++oAQ>oxS?<)5LSgI_d1ySDZTAp@?H=ufF8OU!8u=;>ivuZ5F0UR%eRi?|{$v z*0%hwCmrgPd78hx=r~TWXa1x^or=HSq09ev(xFaSCp*+LchaFwMVw^MH%~jX#S3w; zla+$WN@x@u6~>=mp3$Kz|IcZMI%J;Kp(yd_2t|mMf4NZc^&dNXh)4lU;Ua)_b8$EHDd zG_2C&kPK&Z=-eCc1(Wo5F6?w1GEeJJ-#DJ--2Zlw($TTmaPB{ydds2h*S$6Oqo>|- z$U5n*xgR_AmO~N8-kST7Q*Y%Py+My0vCp&cYlcyc(P{eVEu73294h{7)8M)dijyQo5g5mL}3;O$+XKhL8W$x~~=XcCho)0zER$W!|Tx-}*vS;Vh z(S;@1)D3WZsdkEP?K|zSSs54mZBB(O>_kb!+V(*UZ(wUu~~EykdC%&eK@_+huL(zb>hZ|7B5G`0E9E z{;%exxxWDH=l|UI5VRx%)B+bTsNB40nvK15-$VPZ4RG;v%ZUMdM+UeM>gFAo69aZ# z8{pzmnG*wEd}M%&*KHRa16Pj>aPb`NV(+2d_t1-68{p#h*zAjN_dRsCYXe-|B6niI z3y%zN@kZpLUGM_e23)WUp6}X#3wFWtjtp=i^rBsG#kBz!?1JYW8Q|iMshdljPO{*x zBLiH#33c;Eb2h@?eGl!pHo(P0OeY3hc5Q%*7pG1Pxbw&W7tdwgJYRKUz#Xm)xL_CD z?%IG0cENLw3~=%K_@Z6#Y}W={unV5$+JFmo!EHwdxDa~LE_kME11{JF&p0x`#nW^* zPtu)?15ZCPz{Q*Oi@k|;-$PGxZGel5AhWNe-1pE^T^n$rcVg~)=qX1AxVUM0#l_7O zCq3|F*9Kgu3+(;>9X9^oeE|7y1Tg=40PU{>aQ>PA;ja#`{i*=fuMF_~z6&7v6#<4H z577J40Jkp*5c{G4t1svjrSJSBg2(!s`yVux!4bo%8k`oK`V+&m1Ik~}ca$Vp6ARR1 z3tXU3xj~=W(fU!P+%*(tM74!-u23DnM{xgxwPOohU|PAsvT|a9a%_PMgsdw==g3%2 zEU2FJfpe*DaJHOSARk-c0=>%(YL^oWDkpv591F}12AC5Iq+<(QpoF~ z-rDSgr~4ng_t*j#C~g-EeRThWuR6BC%^|#t(_$xm@SVpNxR83W=oj}tSUk4i2|I$l z|3Tr{f+y?<^!^76#}>Fb19gFiH+$oK|AYBs3tUKbgRbYq0{++nH;0fePRpEFkUO@( zg;Y1le@-mO9$WB)9YNjyVCL9@C+rC5{s+^?7PvVBa&cHO`vAh;|L@Gbes25q+fUkh z)7Irn@4f_WesJ@Z8=u`st$%g>yTJ)SYxNte-@CfH@`jaXdfx7N;qv>I{YxKT3WGC% z!ot@VYV!}z59a59h{yC}J+OA^-q$WKtsY-~Sv}{qH8y^&=n7K40-;w@SiTrc32=2# z?CAcS5;MzESWZI&CZ%z*OmY}m1fXc9m4zy(-13Kudy0JT8~^)N>w#5AdLMmt6M|0k zmU(XtZ|{@dN}0BXLn+k<81-_#X_0$nv^P+w2tzfgpc&OUHLKTiCeG?7dRH9j9Y?!3 z(OcbXaD!1^807?1i}tuSoq%Z>3p5KEpOq1HfP*dVSB6AK2={|QvMcuFtO(kB*?Pd^ zNbmT~niIXTSU_ke{W(7&YAwGW6q9udlVfoj>r*D1swtT~L=8HzOvjhaR)~}u+l#Ts zdY2vP9lxk_q8HN{%kWB}QGxE{3l%vNjpT$qRSo+>k~&D&_4ZH-c&Q#UVhU`CEoL)8 zeF*g4X+5yyNbmS7{u8}uHxkz+l;%^=z7mYGt#n=FShWD-l>b83{{h9SRz`h7agnvmTgtq<6fE@QL1P1WRY6QA)_cWp9y^Dj8pnmsPC? zW1wVKNQvbVSiKOGse#nZb^^7CP#@Uw4D^DhOS8v~iM#A4dcC4Vd($Ixlo%$A-hQ_i zMn(gx(C((g-9lO6sgBp%302qvA4HOhrD~BZMghGG*1`Rb^qw;*!M|#g0p9@$7 zCd+9xA4r6<8e+f=so1axkiA_fL5X!`ACGp8Lb!)}jc{{Fq&lfR;9pzs_c+pf&bR?l zMKjdji~9ypk0v{$8WZFMTWlvYS_avR2fMjOAwuB1<&|`~!17WX5puXd#O zoN=SgMSA{bIMPm7m0S~)m_YS?OpfYSqAdq}Oj*csVlL!mLX=hK~C@`Bvn%O zwG7bvIqP8HM6Y-JEy_v$`pB|?h-8H(tND=tQ*^(La+aod6RA2{4h{RQSb<7!L4c@S-Rqai-Kv!% zL&;<}na+j*B1F<>*zNtGbznKtd(OC#55^6PhLu7)ozj}Jz;aPxu$N)>83{>bXsO-i z+bRO(%y?2ZgEc%9skZGr`W5S->qzf8`AdeWJqrL|6yhx@Ez#A3-HQ_yt8q)jB09tK2Ku9mCC zY7nz<-d8k`a$ZIwT%1IUCW=%jFdseq^VUJzk=}F0jeNcpvBIuUF3ZBTB3t$^=^JfDd4`@7eRwyRCzkBfaO08#z3Q*6ON1UF|p6eLtGz z8^eT!dJ(M^7JL5IexVl$L=zAI$xm2ZxtXnX`VFA>E!KhINbfo894!#iy?bo9&vp9!?^AEV5s#B)m*r$rWm#Ebg$w5jdK_^$5%h5wMPZ_=l{6h9kY_j8_8y z+n6Q7d-xvSkE1~{;*Hm3IfT(8Q87fy@X?yav@-_PC&}ut6-d{rO?!NPopqo)(tFN$ zMddOInRjpI==21Az?_|@JI^G!Q!$!i6 zEA1Hn|NV29p0f7i%P*RL|6|(3-(r3odH?`3J>wLZ$;IK_>;t(m>_Z#$_Dt+U|5*Sr zE)MaI0AgHR&#OdCdsQl2$9|$zk1!Fyk*HZTn}}Bp6R@o9jmW z!$%%qJt4qMJ{*P4#VEciz?{;~7ILlqKCA}}pVq<~RE4T~b7a>irwHBa3smXhK5yPM zAevv(Od-T`G*2tt`Y77Q!yQPD4y&9mJA`oX+_tY#!B!Rt?uRtKnjd7V`&_P5kZ~Tv z)j|-|0ngSF5$Y=C*IQ*nz1m90NN31|cGHi6nVxap2fq7?h$_W5cY7Rn1ttn$3$EN~e%kePDz$zhu7ddxv!_Ao` zh};Y}_AEL9iSAaw*zj@uyw#o?Zv-cOoCc0#kWxA=`#jRI{av!;>-ycby4f zP>%K0dNOS#5jCh3g^n&1 zq)r#_V&O<2E=NpGikS@|*3E>omAei zCxIEb+;($sI@l4P4e@?jsv7MX=VT2L1j z=ezT(K*)vvDAy>@0)VfRJ+Ii&>XuB!!VN(bNOH*@&ZjS zcdY3~U<)_Z(_g!BEdZ3g;9lADGRMlM13OF{uevDAE?s-2Z4GGpOC4KtJkq!+*epyeeyO@L|^*W)!_8L@pfXjNNhyK)l8#Z6In<=V@EMmu=!ajfx%;Biq3UcL4* zpvev%!m*}D1&@mo@*sF#YP$;rkKeJf>A^E`M((0Iy>#s*wl$#XxMOQhg2zQYd*j+Y zK#LtbzS&P8PGD5d2p$)2W0Y$I&}at_=2+tm!Q6*{B1~eUUY|TmVxG2nST*H7CJ9yyPZwAf}9v6>Elxrx^ zXa^7ESmO=Bj+IRho{6tjF6zch*I?Tk z(DYr$)|>>7i%RpxH3(?2gXhJwpE{f$JTBgUDA)FZMmu<}I@WkY@VI#Ap{#OsvFzg8sCw zA%Ge9nM)i|O_1|82%le&5|76<%^gLq0#_w#rbECigmF@Rz-@lzb zI&gT=61T9hQxsXPn~m^tS|JOAs#Ok;?I}SWfi;FN^2aTcKIvIUvWzMwdJoJlNQ;&S}wObIg0CKq8!y3jK#p#>$hHi zA~ie=L>V?yiM6w8y3Y3cW2CFkS$&RE(r2$e`^11ujf@QX77nV z-?8)#r=)LRdb?B7w=KQxgjCeKm59I>>r~jJ^4TIi9!+0b`jS)9KUn$$r=*`>`ur*B ztQqiYOQ!>6FV2_23lw?SNUOn!CUnMZ-FW`S^PNili>1GCO8VzZe|}0jtFwN0>32^_ zXEp0Dm;UmUbe7cLTlzhxq+eM2!U<_UkZgr@bu^53aw@p&KVFGr``O#4r zsnpxHZ#%VhRuBB~(jPk|{iCHna!UG#OMiGwD$bm(|7iJC>dYRvefxCyok{wf?UO0E zI5W4ty!2(Kw*JBLA6)vW`GxaGwEsW+<|GDYy5P?3lPS13lk|@5lPP$%w|;-=_fLax zR>pp1>0}*PoH=G)-MD%pbtdVHHeTeE^zMzjoszzAi#;T_>b72cj=6pUknd$E@F5eq=6~HNO7Y(x07rYZmF(m%a`>_1=Fk z-dYES5_6xN+y2z{we86E*4EE&b+%q|>0dAX>ZR|!w6poi&F|YxZf_CZ2sCiPD(;>l9=p7)SQ`bWmP`JHqSBo#=|C$ang3D%u^4K@bNQ2 zPvf*h9olxiLp|-&4t2;n*`Xfuv_l<=IPOr7{t~p^Khs>w`I;%sy{_z`3p4VTf`0;S?yzbOr4z0iLFVFX%`pY5fq`y2raOy9I zB2I?62TuJJRHL1IFerm+zkwW8rAfd9QzN5oB+~<#VcKeZ$Y=Q3-dOvbkJgi?K5{7H*hlNpQy)pa21Uz!E;Edl3t&a8%=Xa=K-L5o0~U5t z4wO|! z>OXe&$`79E5cWboOY-~5hvO6#?ubXMFs&;rJCio$d|=_$eNu!>px>}!q?~gb6fi6S2yUj z_pLt1^ZKQKT>S3&&w>A(^7Fux4x+2~zE0Y?lUY9JWyi!CIY9>o0y5|cNDvb9gcS)L z{oNEp#Hd0WC`$+WR6Wp7WRtC8h_@;sy&>lLVI!OIMRU}=?JGWDU?%E8L}oINnP?LY+b`d{zw;)jn#`8U;~vH6q#;3fiP$e z=HV>WMe~$#L|beES_G?A!u$}>GG4OMA|iZNQ>9Et45SBeRl}GhUiG!3M33H0bWxJZ z*63Zu$dWWo2Dn@(1ta(#1pdG&xuajcdO0a~@3|*n4x7?F_k^?A)IPL_qVtWz&pqX> zz$cMeJ{ezTe_Qz^dr>}tbCr6oQHIe`r5Q~Ndd(~}q%(X%dBu9z?@x%KjDaGtdU}uz z;EZ<^>q`JDlC0EwoB)N?-G-qC42orw$y_1G#+~})xBvPI@JV=$Om}?rAP)~EodK63K0X`VWsYmAt-R@1+bTF4e zbSfVVoZ*vzq-k&i(fXQ~U}Quo#$u^J7FTju4o;!9niQzhjFPS5#js+;Vu=)ABYofNvOqnwiM*6X!Vx!c{%@aeQ* zju5O_M2RHC8F`M1bKzJqOgZ*R^UJ^|^ems;xOMmYA?Vx6Cl|Kv8tB=aIV$UZB43v* z77iAK+*x@N7(o4DkB$2Xx*bQPFqVl>0#gsh5t_*<`Y2y3l1L7T!yFVa1Ts<(0FaiD zb?lSWdw@@I`Od5=J_O)3Gu~p;jDERNj0QWEt<<<1HB@F zdSiiM*g=krPUIlw36ET4?uoBp=)$wkd4 z;b=CMEt_HF5C{Y4#JRFzEdc<*L$O(x_%ez+z$hWnxO za7}0>;>C-JIA-h@=@!0sg0sin=cCa53^H*HFa-01kYb@B&^MW0o4EDR+6wtB5&<*_1 z5xt8yZ)c-qx~PbBB;fIc=Nex^k0zrp;e*5wEUO+Xlex4w@y^Hs~(c=d)wv%!v(pB68gYDB#(-?&2x#*|~0-z)5ze zJP}xYrgDAk>B@tyZAaOt3~gWKMYVgmYIH7xgar!#Nxm%A#HJ#fmn9LPe^xG+hnKrG z`Eph5nBB|vx0K6W<8rs7N}4W%-*gO1sU4-rDGr$rNT1C9n@wUE!OoqY(>ZS|kz^$q zNwu59gjDSi4PxXEM5_IGD-HRJ9VV9QU&W0;;HHyUN=1S(%J6zypmbGT4|`#NL*7AC z12abEFx1y!YO;Q{-E2$gs$o%8sHYJ|sLTtxS?!Y9tDKS#wZcp~8|Y+AhEK6^#v5=1 zJBHg$x087J@R|1XeC-+Y7h73xYJ?;U>y~w z_;_v*DmEDfqPpF>1*#d{bRt$GR~tbz7w1x5lC6-9dIO45qh=r0Dpq!c;TrC(#ga`N zN{DK{S@p)VLQ-ejwW1;^eQ(<;@ChZvM_V;!r2B~da9rMxkd6}ZsOcaP?fd`1h1}fs zTb7=&_@5Vlaq+c_>_QH_$^V;$4{iImJzHPidgoSa>*ZUU+rNA14?%Un-X;3dlQ+M< z`E#4!vl-od=Eh%d{L;q3c721}xO4sgUjNngA6(C`KX2_H);_WJ!)y1hU0wZ`)z7T{ z=&G~|ul(D}@2tFeMP0!ceqd$U^CizaJcj3Go{i53 zeg0$fhx6Y#fA`!!&;9!LTO4e=7hmUq=oZQ2)C2IMyl|zd`AXedJ58AZs+VgUuvCt9 zb2X4db)j0peF2E{<;9jb3Tv0jM;r#9sTyXhS*`a_x(N$iYLKeN;npY<8YMe)ci0%h zw7>7=2Hd6ev5v-Q#Fgn9uV)02_BGJBwo69?!*;zeVC0$<(M*Mv(2kr%*M8x|szKZ6 z83Tl^7ejcBixFhL5-5S%O=+HkOg7f(5{*;>k8p{(_n&YCV`w)s%5-t1uxpYyBbT`t z-iwEJ1vVsz?PxE?^cXJNi7b8Mn4=bmuvr@1TJfU^E8{PewTR+vsH`vDkM^}86GN0Z zD_R9n@qFZ%L&>tFPDY2IicmaBblRlW>*K7|Y88v!Jk{`9HHNO&%Y-knaQiVwRgVRm z@Q9S60ux7LTzsgG$bwI8RQy>ihoEsZOqG0oEtFXP#S;!3B}&a$NenO}yr%}Bp*P(O zVMsq+W%3el?wTt29^440rG@*BIb^yVZAu0K<#j=9G~p~7E{Fl79facz#FxsNV#1e{ z;(gp)dDan!p=Fa854C8bk`=?j`Y@46r!l<+iVF6#YFsRZ8I4b=X3cM2gi zns57k8Z;8RDLflVw@9MNr?GGyH;3JLyHd;7qO7v;^-jXLXgUZexJOj8&=4+`;HnjFh`mvbYRBTcxsCsD%uy*K zBVv%tm3g*+K^d(g(@?dG^?Q0WC`C&qV%76#z2xH~8_zr8*ezF#O4E3!Kyan38Pyk;{gA--t*7@{V|8s+y!Sa0V|culEe#>c&BFxIh2KV~)~L zLrB61CbF@Bp6S7Z1h(77@G3=ee7Bt+w4p+&Gz?T(bm7a#96}q^(<)R7iHaZ5*^E%b zv%8#DP)pSuS?6m_ZJ1U`CR(Uyo}OzC%QZ*$gac)GMTy2JUx|+rdL61H10g@gh0>{@ z97gNu23#pt1{9uHSUTq5eR{M2$9K7s>P01{WsMj`juxY%6da{fSry^4QbPh`0k`(h zF-MN}QIS5{Y!9k>JeoC_RKAR74L`wVn5@5q=whivTN+I(a}OVLa5a+(H(El9!3waC z!;nlYL?lyu12Bw3-lO!B0GKjw%g;j)0DG&puhc$LaPprA^}he!sNK)sz_Gn^ZRqC*(R zw6*#PM_7+^)mp!q3>zIoQqA&+4RRg65)cP|h-uVKH8ha$VT@V-(PNHCFRGx0cq}4i zTSkE4rN%JAQlLP95>jvEkDvXVd(K@3l@Y`R zE*Dg;mA2`U;+mvw+NNomwn>wq(@EPjX_Gd4n^YMU7tqU5+)xx1cflQZ5p+gH+;!Z) zvZ+^q&pTjxN_P+1u-JU$}^Ave1iDm|66~e7{R8O>p zIPFABi9uwTkLyE{D1+spsL>4AFMGwAkS-zYMCHu{6b{nW9+{>8{hP zLWaHbgn%PwS96hZWz;C9&p#kw)UcldZa35V*Xs^yO3m3k{zH`R^Vc;Ip=FZAnt zLdn#k!>$?Ae6o>)9Kc%d_rq+mt{A_-Hxbw_2;S{R1yW$4r!n=SDR+|r)T&=_uhYhz7?;tDHfFqMj$Hrx|4i`%* zs2CjvslEGQyOEJhK1E16UK?uRfl}_8^2%<&fsK6JYK{jzh~VQ)tu(A+-5TRp>Jbny z75Cy)j?PGByAa`b?ipT{mXGKNsJ4}kggcPH>Itx2I_V+T8;7VwnAEsVKMMDImBP4M zCc$U!$ze%PvE^jLjNoyh=u#QXj9YRQb=t5QLAa`~8qP>TxCR&QhF3loaKL0qDk_Ow zn`savl}VHev7wVy!$o%_+bInn6mvy|FilL|yeZ&F_87IqHal4zEhRaZ#yX9p8H#iA zFjq+XF5&jOQ8b1|%FJ?f#?gRM%~ZeC68i;AH{$g=u7fhbW1qERILCMNkdP6yM#RXk zKP%u!m~Gt~R}D~yQs{!O_pM=M7|u1@RWxMM0#hWg9-ZMMzRPd?Q@|0$*%ZqteYBX! zMF_#>8u=t$CA4~>T+#BX6iX9@vai}>b#t~PM)VP3qoYBl-5*xFTAfOE%lULU#kTW3 zTqiq8g73O5rO{G%_G;=Oxf&&FkFy+2Lb$uZds#l?yff4bx9TCSEy)oOI z!(_5hZ-KbpVX`3(`L=@;K+#xPgF-YFio#j6rcxlzID*nU$v{WQ6?{@~TJ=zqp~kXd zS>#CYhJz@H`A%6gT38PAsc>tYU!5&iA(Zs`V=LEENG2D>1z49dRX#`f;R;nMBT}JK z?~HY=WXkK`nCcje9W;eNBdrC8Dr`s&hiHZDC}XW<)Re@l^gpRlFrm(wdRoWsCb*o z;$be#Z@(tc;lvHTStgwZ5rN2P$|!>uY;A^;SZ!3!mU9`H!4pl&NYL{7PXi8H7spVc z#*{TlEfqVJQZ<(yCHr-}o-F#qn80eWb_*-VbM{&!;4qw`o8`ilWRY(XTF9=38d@Z4 zV{I@EvpHpuC(3@B8|N{3Gj`LN-RZY-z;B z(mA@C&%#QY5!_a@8Xf7;a0L`I?;sh1??*BRf-PzwVU&k!aIzEbBT>T*HA}1(&4=0* ztfFQLU8CQJT6MnzcbVncLElxXZnD}==R7zD&QcMN@!CQ;lgbr_I7!7C1K9JunpL-$|5zqa$Co%;3B{x5_`Dk!! zSJr>A{>-&MtUY_}T;MGzuDoxBTK?x{b9sAdyx8sXN&7Eb-<&4(T-x~3S?#o00)vK62bB+^{k_}JVk|Ma$HkunOs%a(yEkV$iCX-vm5{g%>NlJ5vWTn6c434$ zgc5gof)LSU+Rhp>tQ*Jv4yc(T=)-g8+&=+=NJdS?layJlrRq?s?gpoFeE$bE~`^f)=Qy&^z{cplXVs56-RX4hUMLn8N;JkDCnH z1VR5Wx3;@R(B9V^M;552&^|Vvm@`4p-_LE%3_%N&Q!tMCK+P0EZ=XBy{s|DYKs^QJ zm*rQ=2LvrrR$>1!b24NT1YJ9~w!22q-j^~*7O1PxK4whJnIPzzxy_j& zXo12C#xWhJnIh=5bEn`x0fH8&te_kl0F_e&y=HEew?ohZr4@R|`asnbL9d=$)g2JD zNNt7v$F#|iO%U{|xwYLjg7&`aIkG@;h4wLZV$K9XubkVQ8G;t5u3#MN0X0(uU48N( z)!F^w1PEH7yn=G93sg=K^s>2C-VQ+v)K};oQ-G=|f?hhesyiTPkpc_*kI9oEn;_^V zb8EY61nrG~J+eTBh4!(|#GDC&uA1AN8G;rFZGO`JW76?vzxd?wVe?!;?-cB}Nc5?3 ztPRYWBKpd?%{c+07l`bd-2Wf4^!BAguRT;c_=|&A9mEdYcHr3u;Qim&|Fr#~ozL$) zZs+j!N4Jgbd+)n(pR{lP*4ws(t>w+vZBiS*+<3`Gdj0$B&sk5ceRJ)SwTG^LVfFE= zXRmy0#auZJe8KN59{_^?0{__E^qO>fagkbZAhaYJPek&MxaDov<{!m=<124{%3H8( zWfb+7$KDejjofz2mri}aA$Av=3SQv45(s|So8J25kX`t<2R!-g_rLk^uleL7@Y9-+ z72P1-M7;d;?^6W3i%tbEa5V`8KjW#NdTfh$+^@g-`0duU=Y07I_|5l1FaP~Ze%e0w zXJjPgn@4L5s;!)=54;xl;n(G{pLH5jxluUw(X>|h)Y1V8fQH*W9zJ@(_l z*1KQ!vDo|GoRVL2Zs{|(-ZFe2mHy89%O1h*K5{C!#L$hgnGrKRGNm%C77F+4VVvoq4~@>hKF*~O1s{;3@yy_`-rLF1wLT~g0H@z^1{!*-TKxmy;GU@tUm1LFZlA2ufOjb`j?*by`3-o)9^)K zVRs)s6+9?q6*N`uCA40uScV&(KA=l+Ijr|%wV@$%VqYkeG1?v=GmEeKtho8P@7%`y z{vm&S)J;$SO!9@XR}AiX;|uTI>rl{B9&*(dyZf-I;NF<4bZwb+Q@Mo3vQQZ7UjOGWUiZ+Yvv0cMvsXV+|K77-`hULu^QW`Bsi|PiYUMT61~q?KT1InS ztC31}V6!fdBrT(4(D;xp=hT9k6ZAmv74O=*f;j#CUnoEElyATJkB@u8$F6wt=Bw{< z%JTPqe(3&a?|xu{z*E6eGui4FLV*>PPPR;f-70cFnC|@DfwOqL0~_;#CVF$qmKx z0;OZDn%9R;tyJrx;9CbFMzhU5g4c-Nsl{1{xBLW~<#auZ5Lb9;sAfO~((< zX5P(LKJpWPll#Z>KK0q$`#PA zAn7*LPf5`W6<=QiJS%14+=V9e*e*dep+1=<=@B&_iK(NhzboXs<`6vG(|D1n)CKOZZuk(p`_QRi zS*ed~zbXy!L`vw#O(oBcRHB_jn=KxtoiQ>*lt`{Djl5PM_~KLF^0p%{ec#}Q&Y<^{ zry7^P7GHnYY3n=o%1_Svy#4hL-kaSGO$GaS((F>r7M`l;p;)CUTCRy?G8xv8xNOfD zx4h1<-2qvF`!m5e4{p2kdtW`{^UCj@a$)cKYnPBU_60Ba>kDlA-amGKbHA6$?CwLR zf){W~1QvH+@%6Rqj$C}j$3OeymCyf#kzTBylYVD@>D2DOJ?HQ>=U@L-cK2vtF(qM~ zCQ#)@*z+RVm~T`IB+(v?Ew@jVoq9hUMVZjJ>&0hN`sQ0M{u{RRsNY9E!rQH%KRx_f z<5Sna2L2uUV)9AVAAI{G&t-Sdn+jgQ-4Iy(wND@Y<=;N`oWDN(@^j9=@|?Hb>w-%j zf9fso{kyk3_oipw^Cy4v!AsfQ2L~35IG1nK%Wgy}n&C7L9K~WGmh9mQ1=reItCW{g zLl|inlCx#=qc4BK*}r(!_4Q+~fAp6xe1h=6XMX2C_k17umB+*`x#9gExZeZc&F(&E zDtG};L16J|zkASMfAW%-tUvM65A#2M&t>m;;k%!=azRET-xRT4L8~Y!zTFIU`YjrsS^TSW<wdS@`0y3 z?wUg>=Rxcv-}l&m`NSvL-6K=M3%C#h!4JL+J@;3kU&`5+eW@>f@wtEe(oeqqqgx-Z zJ?;tj`qXn8uXriU?w&mrynrtt5d6w-7H`tNf4`fH<%^ysf9(FJoV$56bPD{r4|vaG zpK!rBiPy2aho^!Uxcvu$|7z(A_xvW}$v?XA6On&C<;9il(Dh%u*VU)17ybPIyyOSm z`QKo7&zcG@)E$k^j4-W=`3XTA5M8?NhD;EsBy?SmE4stF2pN)EmH0sLIjZ#N;>z>S zyzbU3<@1jG_4T)1^`Uc{*Pr_F_x|EVUw^@?^50{3&zuS_jVU(oNAzKx*JI6*tark3 zckD<+8cW-n&?_NI$t<+wk}x|7C86h^9a3)SJm_2RJM-ngPhJ18zj;vj$=9*1*cSP^ z7n~WtlHGm4RPX{`iNNA7|LXalfAi~Ka^tlRdBxBB>%(uq^55=#Yx+^wm2X8p`>3CO z=ZQIX_x@AC3-}-c!Q{;w)<4{GOZ}g;uYHcY-{JaKFMQx7H^1o9+IRo><=?#cj9r?CyQ1 zf){Wc1cI-O{%iJ+oBw#mS8luE-$vhh=2z8kyzZ8#e*C(xJ!SW+kJ(*1EzRzpF%`Uk zs~`~kn=d@~=)b=Am#eAgU1-xE?7Z*Y&;PJ=;Va+y-H$)zl|MS?J0JWscyu}yyugdo zK=4Ceddcq!7cR?hea@HP@Ws!a^6IDm=9Txq<#*Rz_@Z(xl2u;$Wp?-Uso(|Py9I*J zzFK_D4=;x5KY97{-gVnIzSxhx9bJ9L>XTRL?Mt`W?IWuXSoz_~^(#+5^r=Hv9qRq*MZogWt?=d_Ha{_W8L+Xv z?gVcG_Wf+%2lhR4@f5_3ORw1eSiY^pWMiUw+o|V>e&4sca@T*EarfrNR|8Ti5Wd)L3b{+fjx2h-pRE^McH#784oFBCD&ZhJ7~bDG*~R5Nz1 z#&KFZX~yGqy6Nfb4-Pm&nSu_-B1%^0g`68R+Y&cqDppf1B+Go$C2OsYVDz1~8oS4b zryOWCQbDS4&AfivJ$&3IVWsZFUYbqs! z!LwPml%^eC=y8zVDUDn@L)!Ow(!v~1T$m$>O04q&#y~?8skgI35R{xVQZXr#(Ap`y zRjlRddWr^NIB~{I!keF(>Zo)1PP#w}89d^R<9xYS4>!lKOUM~KnTdKnFLwzh8M7fn zUw*^F9Is!PTZ} z?f*ZBXM;g2Znx=Xd;=oOcx60d;U=F{iCC9xhdLwA$@7&`(dmk#(?1$;^g&#;Fi51k zEE=+q{#Y+js!L^uv4PgMLIM=!$pMygS%E&?nsRWME*J5HM#gmx8TVKE`GtiL;hr!tiiGx5DM$FQbuhv1L#}-vm;Tl|w zV03N3^v9Ti>f1|;a_sx#!W@4HIO2*>gNpftRdB#lBU0DuY)0sJ+emNV#W)x|L=h?_ z8Zn0YS`b^w#jEi&k_SQi-Huq%$Yci(WeFo485D;uq~_8fbr{r0Hhc7F`Sh8N`8iHo znB(372c$?{D;*7y@W8Kn)yOcNaMlxn?tygq4&o6dFlXWf?uvm6B>ASI=yoy)eh&g*na&I4Gn#jD>ZF89*SvRN3N3 z?N(dvq~K75#jIqw>$h0O?kNa;S`e*VUw~t_H7~&Np};C|UdRQT)D807X}fA1c{GJ{VrQ2N)$sxZEal2h#rDr%>;1E zW*Qn~vLL$r)aM2|7T^dDK8xsh_Eg6rYvOFyVq~h$K(IrEo9XnBZIKo+wAyH|W$;`- zuBdvQqa=#s1#)?|!l6#87UN9XmNIM{qtt}h0}m@2RJzx72;LmDTZM?48|AUgc7Q8x z0gev_?O5O-I>WAC?n_uQ9q&fVXuC;hHHi}vWS`0jdESvLAW<%YVua&h>b~Cw8YrA*5LJ&f3ka0O^E4{EUX(uc(}g*LcMSDG4vp*fNH@c&usPk zJd70F*vOA$y=q(`GY78=bS%K})`dA{pxhS z;3?cHjB?wzj@`;MZI4-nxAAYET2Py!qhG z<&CdwymjO08{$T4jv|zjgT;%dO>yFQ2;k)6I__`tqT-9J=(--vaN$ zUmg6HgC98fLJJgb-}64;J@QuhK;B75?Z$vEZ}!LeY*WqQbUAPMBXy6wrAy|d zd)l0IPYvjBq(A70?68-1=z{BZ#65afu9?!+-L7bwd1{FEF@8pN2s`XLo?R3O7Q55De0ZSuq;wCPlkS3mt}d{KUzCK7-!balc5(0evh<8O z>7G6(-K7B?JMJ-3IcJTrUdOLB`@|lcF1;h5%jWV@H4CCpTp5?ptXJPdxBQ7Y={`QC zV-;|))AT_}tP_<%QEgd!+Xc)eYJ2E{X~Z&JkRl?lwkpkz zrOQp4-9xtG2Wn4B=gmns+CL@ax+C19i4k}K?PM^GVD4B{DVkb-#(djMFg+N}E%Qa3 zbMgZsT>}}U_dd#7`Nyf+lhb{6PP)$o?WGhF=@vVLU!w#70(`Wm|CaM}(&Yj=tcsPS zzS&g^Lbu|0)*YAKXlm*DV0e*CT22>Ov+r?&50Y8zg_SZNnH;p%FY0-&KW4Kdztu+> zN!inirFR8%AX{C!t>Wp{Xjmy0s>8j#y!@nqu7*&Rv{mx*;8h`x`p90om3Pib7fd4N zr1IVt$Ro21HQ+155izQkRX&IAX~(i1(BW*>t>Au{?xRiI&ZPCdbU&Gs?#FY|{b)KI zHBBNYAIWHzA^X0C(8wOW(0pXksn(iSz(iidRElE9S@42)@6xyY@;T|Qo|EonbJD$Z zPP&)ONq1F1Ct0HGc5z+MB?`}uwfvsmTYmDKbQdozE#IAn>*V*4zbHj8R;uQ*V^5N> zCh0V@_4=KT)z*}bEwxR>(bK9=>U5i|Ipm(PU+qol>M}Q`xMB?)eDF>;omGl^=^9hI zlON}U>9_$SbooNM&X(|wjSlef9=$8GZTX~hJ9En0 z4(Lw0&0jVr-LdV>PuX|$;3FrQ`Ic_l zUs!qX@_GAD13CH%Yp1P#59IV)Uw!+|_f{`n{=nuZSIf(f0XhD@Sl|XbXIR?45;vL7VO0{sYhvNOBIo5M*BB4oD0n(c$vK@FURk4cmeGO~J zu--7sRvWmUbNP0z8zBkB&yjMImm_qnG_a7Zz)!0};6iPs#5`V(ss)E2k#;^#;=Nea zfjVdhwd|@NtyF8pKE3_Ez$&T*Lw>yjN@$5vll4=cs**`)H0aiowo134zJs(1Y#3tU zEi&M68O!R%6--FFlHKSvGL^=tW_x*hkZP3Z0w{W;(%FPxNQAdv6>t=jP_|`Pi)3%m zV1|s4OLmP6Jn~X*r!nNmJk>5aWZ3Azt*w8Ya%gZ<6FV)P7u8-Vtdv3{&&(v?GFcms zM_}Q|wxV!jY*CVr>To`;D=@e^aZ-iGHak@zOZX{R7==vzU zb>FFuo~(IT%!#K|SCg~Dx?iqzF%>Qp9ikBq#j}oXuvI5JF2zS%!PhQzNJsMtzUY)& z5jUF{Bw>e3c_6f6s6^qi4jxT5v!QMgfMsgWeKYP}Ma$144v!6H&? zGx#zHDx*hTDwk-rM@AfRb7(u042xBeRip`dVWgmEnl*N)U{+3^-JHrZw<-Zmll}@E{vX#c5Hcg<39$#Y29>B+yX3&nc2;FB<`eC5sg} z)NL46FFEG(Z9CMDgmSUu(33G`9Lh^*BIbbPL3MKF9RY_f#>ko~5Sk&7A-$kPXz;Zx zlGTu`!Ld-Hn_xsIBaL__yipD~RLO2v&G@$SjtUTE)6oRM)Nvbo5A~Di>M> z!6qrh5z+;%fP+MfX1yK9yi~1?49ycEK(Zv`B}*e@7D!I#GdiIZW9PDZPSN@HM- z$Hg>O)MPkhwmYK1Yy@#ZJz+%F2enwjVahE?u9G9Zi7*VG*2<0QC`ndFW)h`Jv0*m6 zqD*yYNIXBzbUa!vXp-;xl@u|yK;|xD-0et(L@HArm;Iz#tReKiY`~EsYF;Ze_Cmd; zqd8{JC8T<($wo5>X^0SA9!pIbf|FL7S@}}H5eEA2tKH(C`;qiDAQBRB_Uf2;~Z(BXqD~aYP%OTpgI~ZIFS%ij`RCZ zJNSyF=zLq#ajL0RYB}C6nF-sr$yf|7YL4hpbr<|Zh*gGg9We=V`Z{Gg*MWC}&q`Xv z?yA++$WC&7ki8$TRO$&cR*&XES-C7+F+w<3?pH>%gC5T-sTOHnQFtL_OEMF3{9+G} zHAN_GDFqv0GPFQCq4bC&bqU$2&#VeV%E)XqI`AN-73u?18df064(T4(LPgjQ_)lx%HypeS4+l+P^#7{AqR?64lP+qR5Lne>JrXGEFOyxJToXa#|+5h zr&L?*(h$m38lq|2TR~rG341h*m$5;=I4Y-Pqn#B~`6A>;JA-EA{0=?T2ANc+N6;mG z8=fw^S|T~Ds`ap+*G!C1i=4wUgBk%o*Ts2}z#+{U!g@qE*^tz*x(r?>QU+*zy#}r~K!7ix9v`*ZNt7IKfOt9^N)lli8WYQZ$$*>+D zm!)bG9yR=8C9{GBI->o&9vQ=+)(p;)nU+=?@v2-y1n?a#)b9^c2|tI+e4TWdwIGe5 z77>dM9@iTcwB3;`6b6yhY&;9T5P0!Kw2_Rk-A1oXTYXF3x;fAhszGeLqQrV_$W7Kq zyiI_}euquOqN8z=mwRqAlV^uf&&aIL!0tnYJ(5EW-Oh56NIeqm%V9Q;qt(1w85gNc zO(yh8Q7^mU^#@ILsD2dIiHw0t5aM-;4Idw)X(pjT25S{S%DrJ8rVTX~k}I+0F9sZ5 zlCFD+5k>ZrupdH_QhHQ1M_R3y^m4^M3?8Zse6-k5)B4i81CAlciz}wIJ~*=Cl~$PZ zy^aTRDh{!{*BInGj&Gs;npk46(fW-6M~e+>bV7uNvZU4dPAxHZ6QbW5D8q3C*7Zym zI=_d+L~%@RKOo@9DH@1zv0z9C;qSQQsV#93&WAEl(0Et3%562FnRZppGY20VaKxG^ z$!$u8W<;93RJ9MAaKG8}c*hkoAOU7CjbKs(sb!PoRuE&LhB>pHh3ztJAn?T=<$VOSQ-^jmMjq30AxlLnGrR}B`aLPQ!Lu8 zMO$!ppsxlQ(Nr@Oj>SeDSvFd2AEQvGSj5Op#BnP5K zN^m06jfyR`K`R4GCi42h7f;*K>+01M+7yPxVT?nfT)&Oh6)rMns#V`6l)RcKS1W#! z$nx;k%L9&~o9T{^IyglUa4&FTe)+XKl5ew3HoFO>i?SsF}{hp?HFe?RQ zN0~-Z>DN7SCCId=w!#)~kBZ^2VB``ZaKt0BV{i+^dR7YIx^~&e9R{o|a*bc>1}ztv zJcv(^wk$CnvyhTQ<+^-L$IIoh+KZ+}sA+MW4so$uY-M(TY|ssme7lf0i!=iARU#>n zV0q{hRy7kAL?s`~OOYlbFyoxO`hh?PKk_Zs4Ox;8@^s?yHqmG_aWTfUEExrMcKDQ( zYt@8IzqR_CyYK&Z-~aEv|KEN8zx)1w_x*p8h`RT`@cutLP~Uz3|BKxJXHM+}?*GBP zSzX}%Kf9ZN2_I+^Qgx@7a0 zo0n}K*?9X#dHwzC%-RpvE&|yCE?vF<$~#xG%imt6mu>-K7X2q3yI?~C2{o6F_zI+X zz(;I%^0g6opv;Rls<5qe95TlJYN#S4Y$pE7UC$5KEK1oRjP zV!C}Z7BBaxL0n3T&7|%c{r12di+lyq`BFiSVr@bd>ACgXKJDg{4jhAlss}#y)FYid zjk#QS)X&kCa4ytM8yrsxS>Gfvb4v+%bc4Kv?u;#(gEVJQsMlPyGTXVN-4{V^xFag*sP_trgRd)@W=m}vHo!A4KzCdw{ zx$QX)n+VV`fz1NdEatAPa3a`*fxZcBI6Ik_##lrIrMJdi77mNB;Sw>roreoiNb7VA ztdgNaG7~4~)^~f@oDcL&VACjcSjTl@J*$eO)r3b=PA}A=;6{wD+E%8bm0bsobq0lg zer`Q`VDr$)Fag*sP^e;VRd)@Wy|pre)h0Bt2Q+Ek-vl-bB!-<^-|b;@9?&y^%>sF1=hm|aHV>W*6M)SEnPcZxb=R;7pK!H#(8M0l z^aT>m&TY?e*gO#En80R%e3En5P&g56&IS4=uvs9@`&Ua~SBDz-EEGkaHJKI1y~l0{SMfS)gLY-1=@0n=^r)32YX~9yzz3 zJ+OJeWS9VK7N|opx2n5_&E8TN0c`F+u?IALfi#qJ+jAT?_X9d6uvs8+;X+* zAW!Ao_8f=JX+XyWHVY(xoV!HAonbS%|F5lFw6x#a`sejufgkSvcXto`zv_Xf4R_Dp zlxXpo4@&0FUk&x-Q|fVt9bJ_Mjc)t!1!qFB#6!+JY=d<4V!xqVZOQ5mWbpe)G913> zafe0MQ5w=v8aOui<%NeAncRmT3nUwsH2hBqaC}1ost@0xzQ|!9+0ylX(1yb&Z}Wt~ zmIk1C6IqWI2l^ls4M$Rua5&nCz?0U7py=LpZNk;k2M%aHV0+x*U{$o0t}&4^S@k9q z?XGQTqCXJ(Q$znF*%MN6C_19ma4ii87`U2t(S%gt`_JCe^~l{NYK{ z?5;cz9oL!(+GEH>w$zTs?mIL9WsL>PE~RV3U8-JBzSNkfQlJ4K0FjU29t2B#U3MfkB&zDqv>3tGUgEE@I@1Q z+`+^mX##wJZCN@&(=WPc^{L(6!;_i!^v3ccct1G`=bL$nRO-$%Zvurqb7}IcJ+mX& zCMP5O-!MC-DgmDCu0DlbY+}dMB7L!0HyNqplOh6sKFOr`6QC#X*@H2_epg_R!qfHd zgkT@0noSV*(i4hQm24pbRUwE{r;YTp;IV*oG+!@qloBbNe|}-~XM^1uh;7+2NsyE* z=J{ME9aV;jEL%oELf4EHF(a@RtweB75>O~3aOG$=C6+UTj*55Fj8wpL9*Gp_L_Y7h z$}p)5egV#5^MQTRbO82Cws+6klISO`9=mAy$O4(bPkyZ0gZ4NW3dfsmJBK3)crtD$ z*mx2t_|EV@d}M(H+7rP?7D!(`rC%VA_K7#P1(Kal1?-u+eaW`LiZrq1_H#E!m3>7h zw+Pi5R{QDvklW)Rn)c#JcecIt#wJ|1-`Sdad%D5YB-=T2X3Ck-302>g|758ndCnB; z$ERl!P~3TX{<&LEBo6(DTaQkd)VLs%2xqgQ)OAs@REOP@W)is>AY5t3pPaJPB1HwpOz1RC$Q?UAyL!JR@?$EYxD$ zuA0o}T?6lB`E=Ab`gx=vwzVpmLFBo%o=MY#*?8u^&&CB1^`D-N$tjNRd<;yalK&%T z;{q4N{~@z6a`M?YIm-Xn&BkyNN&$f<+#A!8Sk@!Qu_D_`r6R12}Ktk!iA*erUAcZ9 z_~Gt<{~zyxr#ZWg>806S{;v(1xZ`0aKDn!%;Ovr2obb}TKv>0Ok}ndAFr{B0mg2-` zmqiRb!L{R04$z4?w?Dp|G#F*VaMFwUokBMzT(>{I{JQ~@8-U?9wQjKN96zSS0R5d8 z#yecCCnEo>b4xS~#p2P)>Gp&xrCev2a+-*Xd@SFqxxFsZ3}??D@R5|m4^X>pcaWnI z-ReXv`_En}6$vd>twG5yW~*|)XIIcx$#GRAOVunSMe7E-f!i(wAf9M z$^22K;_{p-xdv~9{UjH6yq20ldf+VFoafv!YdTmdFI9K{W-{-d)mr|q4I-Lz-o;OF zW&UrNM~hCxKb@>6ljfnfckur=%_JB2gaWV6f8cAn^IQbZLf|U|_@DAN+EPb!wTZ)Q za+d#h-y!gxJ#^a0nV&2^`RnXgcWC!i+@Jn-XRkbv3>5x}0zA~!2VLOeYKU4_oEU$5 z*@4S#`pb!pfjxVS3>Jbv89wE2iW4u!^hdSZQjh;^No}z;Y2EZM-2q5HIq}s^JK*?o z0|7;U!Q}=@PWPV^E;nr?NkHxM?ZSv1HZ|8U3#o3C>r^dJsG*EQ14eJ==#*b6j{kh8 z#nE~l<lM)T=Wg^+I59a&oN(s#tY{tgiA*s= z^KGfeGjgw8f;jjns72yc*rBT=Vi~KMS3UL5-sm|oWfUM*X$u+bsCIsPq^K4WPp~C1 z>_MpSael$Vb6h?Xp-YLT&ek$`29t@lLz`%y*J6Hrj8!_IoL8g*_w1u1BsXy9aTiWz z9%wq4dB8XRnB%_j1>pK$H}g*5j+mTCr_1fXZ#L5}CwpeciGAb$j}EBQGx6fS@jHWe zvWFetX8w}CaWLXhC^gY=La>9VVkFTei(y_HQ$EwN62wuk_#=*3Ly!vGm-|EaeBACz zVg1hryFxWNTx&(c*d%`rEDuWU+W8Dku(}=-SS3LbT(?+f%8)AbeNz~BYXj}*5Uzmy z7raU`^?sFA;G7tD&#zT{m2cuMmO|(7jnA46z`hM`yV&x5mJZx>=)MPb57d_baNxfC zzq9|^{pNmn=Z^>Bott)c4}D~(wsXezw-1`z*K8Zx$$fv=_u=Ks_FcBGdgz%4{{0LyapyrZ#>Buuh47fL!|(|0;kMJm z%HS(Us7CpGUqL{KXAeQT11vhLWWKQ$Fj_=ZXj(|33G({eP1&|fEg9iht#2eFN>W5z zxyX0jx|SbCNQOogC@zR?J|B)*^;EnRsijpeMAsB%Ru6gS z+%1-*lT5~_VB zrl*pe(`+HUk}cIhjzlgW6-Y7HgQ6G*Oa82$@%A?YMmt`JwtKcQY|>cX;w`#c7)2~! zG{?;f8VjFa96HguEQH4?n)1!PZA1325$QyT%R0l1!RH~^V zULNz1fN}E`lQQkgM{Fofs}V8O?xt8yqsW4hmqEf)b2Vs;)g~v#?^1n80}oFk|gBpxKnAS`;-X!HIvFT zbsgjxMDv(kVmr}>5>v$)= zF~~5Kofn7`qCAT8%JnbYsLU1_c0<|rzw5u2D8eN$N3D7!6F6I z48sny7Uo4;Ea8OXbuvzcnzTZfyQNUIFq>7!n<%$CbX>_LC}bR_6uO}iU9H@wb(b8Z z(%mqq#isdVC!9cLlVknXnWY`O3#r+@*R^UYSOg?{Bzh1}5N2%Lgt}o(F4v%l50m(e zaqH(ZOC=^X>?T-vtVB~;%tJ*+7h2f?T822Zp?iK-jWIY zx9EmygLWasGX<}XM5;_MitFE>X%yNPB1gP3IWCiO%QN5@h7L)%B7~_tDfQE?QVFFP zGb_y?zWK7_j09~J#C)^O=ISOKt=6D+Pej;Wv(OuW@zD}S*iBXH1$8!x+szpxugmpf zIFTB}b0dM)3QUGB%^H`BkS)tc*hnUlDk1)2u z_F@CSZA0N!4a|xxoERtN;%qdQu9}SoC$=C5Hd2Hz&UziVSyb&td`RbX%gQD)xE(4X zy<#Gh?#*y>#!Mkws-n69a;q}wEQ4`Fns+5GA%lp30f&{#m0U6hCGdjd=4Z3`(=)~@ zVy7d;Muy6btv*Q03bnEo5Cw!Iw3o(0L@8WEc_A&uhO;ehxjSR5B+?vNPr6E(P$afL ztaIHeNt-+)HN3EruV#2qF*w6_LbJYEKh8)EqM-OV~iq3nb>odIC;!!t$%5+?mxiPd~P)`134I3|FVG~xS9PEN)!-DngrI6fZE z*2aDCj4@?K`&KRD*-+6Hil{hF+Dx@jYgLd8O^`WJ)%b!Ftqn4>Ufo=sF~YqD$TiHQ zb8xp&139QQSmR?-K0*|Pa@jHUWU~@Wqzt$@)42Be8Dp%J7g$23)N%@QvgK@LTtWR* zO->4S9HucL*9m)mfh3AE$Soa54rG$`5*1H}!$ce^w!^IDMB60DbyqP%Iay2rAQr$U zM!7TVoQ=p#V*c^Oj`HG#>98lk2mY*qCg@>b#k) zkD}?GS50gFaAu*8NTYtw#o2^e%NHYQ zDIEu%UL=;f}wNt>Tj7r5l zr`O9B=zJf*-l`C})a-Dyb9KOIL)|=UwId*JVoNa7DG9`+V6h0Nrd21AE9Qq7NG2SM zmco(Q?s)JiGsd)(AoAG)EZDfxY?!`PZJCLB$peE3t0+>4CXGhj1IP2(mUH0M0i&r7 z)krjw)hkq&w_~M_MxfO!+roD_~9Xah77uOR)%@tY{W+WPF zss!WFkzbZN1k!42cDT{e<78Dx&USzU+^loTmBF}}j2Bx-tqxr47ec+tVi!3Q4rw*#L&@R|dUJ5WAw&i>!- z|MdP>?f3VW_Mg4;>zz;TykcjxQ`|Xg`iVPCPhGoh z?fq-dTkEbpa_ye0|FU|+>T_1*)rYU1vhux^>sKyckyb8PIkf!G%kNo!Ca4Su|3$9+ z3kW@Ljb2$fOTq1Ozs@yvPY@hRvps%K=-SQ)c78f9>HBwnGB4?kJ3pS6^oE@u%}e^e zogdCidi~B1<|Tda&iCgfeb3Hq^OC-M=hk^i-?j5EGt%s+E4O(;9r2~=NYW~~y`$CH zdBMD-{LU5gl5#uGpO>`0^SpUU*`4RkOUmp#XI|3U&a>wwt?pbtFKK1xSu@f!GDhos z#;>qwMYFKn-kMlj_uP8KyrlQodicDgr))iJUedqXx?oT4;?2 zL!2Jyyk0Ett+KZ9(2dx-Y!%3=tu zj@3IBDh!AJFMDqS=Q?)QiPnCr>ejs%2rnU%m!!KXvQ;W2Yq70B(rrt&B-^qqTec-Z zFxo6HlDx=!l5RQ)Nuaw(!ZMTTga9FL2+OdA>>EkoB@jpgfq@w^Gc5BXzyJ>z2wULI zkzMz8-MX%}RPFA*8M^N8=U-jdr|F)ORnY9qW&i3`0 zwUBgs`})jUNP21e`pl}<8-kN#RiR6YbcYkGXM}sx+U%ZmYawZKv*VYvvDx-ZTHkE> zC9Q2beo4(u+b?N#)ACDdY&QLp>YEL}q}pcvgj7$AZQIg{g9bRQ4N7C{%-g#Bj?2I0 zm-I(2|E6Ekw_g4=zoc)u{HuORAG-W$zoh^E@~`+Mee>mCJ|P7)EkJ$fY)8H&zBEhxFnW??uY`7=gT1aYdT-R9(Nv)0R zI%^?mbK|#T*OwTQDG3edp?r`z8I+)gPPs|F8Vb%H=n0KYink;Fo^|Kd<|`2Yw@0u|EmaR*deOIi6EF6ruF)dp2%%nL=oJ{%8dk1M^d z;<&)K5`>R>aoxc6WS)mTgwBgc3XpEYV;m%`cS;{10Drz~Uw}L(0pgKTGO)?U^CKd! zYM@YzqdI5Q({vOUMh7m6d%qqX7`vl)Dn8KT{m9XQ%Z*2hpy;{IcbuZCQilrVmAYG3 z$DvX(nJ$$|1)eBOG%GDKd=g>c`iPOE7xqWe)kyS6VuWOfapkClAz@p|r*t@+7Sxd# zhD*^%5x60bRA4A?9t?_uESIooG#70hsaCO<%rpqIn5hzP@&7xz2gF)&(_90g*n*g$Ee$%xw@A(qCgqjFeGsaiH) zS2BE+p^_lWi08|Akqi<2I#&o&V!Dgbyn2+)9EdpIEQxjlVHi;6jCR^pI4lVFQx%mU1= z3$2lJ7eV+dQV@fz(w4BG;!c8&}dV(NSm)4R27 z&aQDe6?nqtfL|Ow=31RbI!5}7|M$8M)Qxxkf7;6DSFVY`=D&VL+57O`Q!Y1mKe+q! zo!<6uY`^HzYqq|y^}@~9ZT#-W(fX^`etYd;^*%s;JOAAOyyI`Wwz6tO_?4BN?bXfA zmu>IvH~d!(S6*Z*y78iZ`$fRL+cLT@@^s7Mq_MlJ_VK4bH1a+_f}RsudP@>pfb|>C zVBOg_`Eeh@N=QtT6F8*vaYHD(bu`9Al`^CdG4)W%J0On<&NMHGb;m>b!52>vzajwE z7vF-_>S~PDbmakF;>|c+2R@FGYP3@}fqgwuOT>XwXO3>BI<(w+G+5vNS02_6{7?X_ z_!+F*`-U3P?37ZB$Ed>;b=YOZh+LcGqob77J#@!NGd8wxMie>Mydc(X59Rw9@ALhu z0kB4I!Rqv|wxs6RUL_quSQu~T8bm@baz@D-=8`!&5iL?-m1~FjM}_r0U+}QL*9w3& zat7ZjFGt(QED=+K~KmE^>|5V(k2|r$}Wep7aZ419?GAej_$kP z9sq0j7Odk`^Kg>H7^vRmpklo;QEHAAgW)7_UzIGVCNQyl9#wl|2zpdlH>ZgAd}F=y z81t|`|6!fAwO>wk;1*wYSYAhL+<-}f>*46I9___C={7qirM^goL!24rZ={Z#X4qP+ zthPLOKl{|_=Y76GzVI0JK;Hc@$ju(6yDF6O23F@*gBS z{0YQaDD7g19=0ET*7Wn8z7gJeeB_PdDPg~~Z`R5~cU&+AR2Y|StW#r3!)98i2WA_U15Us=bz(m=GOFEsE8%K2=lg`IaB>K)&UN&jWcjPx}4O zI)0G{^6MXd)m!^xD$=QQsz%}Y)*+f#6;R1OmPjN@Z9_~p+7oS59q84j?%3BsdbjC| zGpA40(VIW(A$;@aeItC~@u7$CCUq1bNB_w%&QV7TM|7%=zVA*C@ArSnH{Si@7kYSa zQbz%D{GZ@GrH&TjoT{TYeTRqhdt~1@pL2ZR;k-#51*j2lg7fqdeDMsJs-y4zfQR%A zpY)CNj^h`2NN-X{0ZI(q5TvO(S~!|hb@afWco>hLF!&&)nwDFW=-!92fdMSF2+_R{v4H_BwE)z;4`rqR z0#vVB_A5vS2C(E;kO~Z7$*+K!0tmo#*{^^O3}DHxfSLjbP;YFxwZQIuC^-cXp!ipi zqs`)@!o3eA0s~lTS*v>=A_D^ma18Y`0>l(RfO=@leg*Ns0G9j;Vu1lH`4!wV1rUJg zvR}d7fdMS}6})r`AV3AXAomA;q~Ik}00EAw%dMMs??c}d7(jp{%i_b{y$^k3U;qIQ zpnd?ncnToE>3i9)01pgc$*%x}41;ZXkP%q&D*)m306+ky%YFsnzyOx~3b5HLP(XT* zN;%gL8JP}5N@WCBQfQ<>)wrmjTUwSe@BxSbPesh-)+@aCA#@5Lz%hEcWxDQt2nh@z zfG5ggpO||ef&&9ss#DIr4~3=x0^AHN_Z2*u|Ns1}i`(-rz%ldSx?^Sl?Y($WJmVql zImXO&%$%DTIDi_TI*0|(q*M9;x4QGA6M_H?b-U#M58-$S zJKrI?nB!!E7=p|wS=_CK;?)V!P#Sg>p%Decn5Gn#3*fvFsTD3>w}=J#Qo_)%se~ys zJ>d#*O**n;Q0oW{6&sy$sdykpqlK&;FZClL8p%gO!$h`2@FlfWDt6`Jh{NGttHadm zVJhG6iKGfEe)9jHbbaQS`GV_?nE@u#^5kw#THw2aYzUSkM?nQKfMe$4BuR@MGjEZ_ zCmk~%U&l-wilScqnYn)8^}b$jG;NS+v~Lu~eO66L&4|`%QBp6Q#nT66Zdk&4jcR;w z@%;aASg2Dvoe&xJ!01e5izpS7W4BySs=daLujUk-KG3ZyaWoutL0!>wALr0|Kd;w` zzKW;~sojiqMUZ~05rrfs-_Iq{YTTC}__XU@{Keq-`O{?{)W!`UbIVgV;dy@GL6&WA z6+M%)Jea^#Y{R6UCeAu$f& z*gygKWoyGmBPN*Pfz;@g(mZPuk{AvjN#l!$Aaa=|8mrT4BV}v7p&)@g(HR;}br7RJ zfN3dd8TE1v#lvBp*PC1-!zQIdq~A26NvEY-mKzx#Ofo$a>&J6xu}s-9gBy@OQlg)9 z9Y`L?|NlHs^1Rx(0p|PiBoDh)^5)3W9O;;!|Nn84jtl1hzjgAy5=_7AVSbNg{(s>A zkHh%6_5WOxCnE$%z*w#J2jsyhHD)IfHqXkU9Fcptp^&){&gh1!CoUXrbVMKE;bOH1 z;s8(>ciQFr0Ifm&rWpx|BAqsz922!TD>t6>WAtPkn=}NMOYozX5!TpNSQr=ZgF-_Q z=|Uq<>w^)`*^ZxL7bjgm@&Dggd;7}XaOYt24d9n2f1VtH+iC=WIzGCtj+bf(I&&RB z!a0S@AE%AgtqDkgraC8I$!TF-YTKOs`$kUDZ(Or1F}tHL0a92lWkvDQu7G#-wJ2(<)9 zTjMYz)58Ql0p-f&P$^T(N^BKo#7+SeNUZk7W*6LU*FO3Wm= zq0sMxf;>pG*2YFsyCx^aFzFEWK8E#U!{Nnmx0GDjM7b{C1yVqiA!u9;2_UQ_Rdj?L zhx9{}gF#ZTOva&bIE@*@MxmBfaJiXh(4>MzCi#3em+H22POAY55yhegI>dd-lGk-_ zo!dNqp(jgTnx6m@WNESl%`K*T+sje_d*3`+y4CyriIJ?k<^Xvj(){2se127HZU~9) zhz7~Fj^t3NP#cjbYe*m}E1EPLB!T6bNw?h9+GFA3=|*bOD2XAY&985x72zJXUIev@SI3CpeN!X4K9sE@9!jJ@>H^~!f20e z#73Nu0MDkXtYD6^lh`mLupze5c2ziAQ>ofOL-;U9nnM=UKml#Q(>CZlrcp&RSBTdn zkls;4Sg)-ysFmZj1$bi(xd1RE5+j-HxdL4)WQ#h(3rSij&1hu@(8AzkN0-Kc)^g4I zcxZM(sY4>>B-*tEbXbkIb&ANt>G8;AwDGuC;d(qDYlYy*iy-hXwED6|qa3=OM_E|X zC@0&cG*T<_jUJ=I?YLy=q6oXwH*)hRlep9+%2=F6n&TlgsU@;aoSH-{-Bt+)B{b`9 z$Bbl$6`9v^=BZKs=0&3n-Oi&-E@_m>W~T(5o9MHi@yjzIG!A(T3pQW9=7N!uYt7@m!g|ujt&pLzLR-?QubwTTKgGwf4 zt36;#iAmnELNYXWlye9JiVKoVi0&Pxn{2`|VrV$7h!LwK9-?eM=^)8kr|I;Qr`3hH zXq3;!``%8YoSiI5DT6w4Dw3UW@Tjj+I#()Yp|sVUGnWYoIg~PDuhB1022HJn;|yY_ zRgyyKs9|@BL482!S_dk(s{ARXTwXNF;m1oW2j9j)*=23xD6}g%?L!T2BV}6d3PK(f zJ%i?qa?EI^K}M)*SFvy+LO>Y^hJ{qxsT!y)M-VbZQdrX%*ZUyPE&dqQ!2DuRjZv8=y;VUdo^7s&KYGgmabRR`J!EKP(s?UQ|J(dWw|`6B;Z;vU4=Na zPB4a9s%1})a&gfpBe(G=v&+&LNz7$M4P%j1ST9B+lGs9&!o0amn37_*x(e*%ZMtqH zi{mEVh?0|G18Y=M(jXjn9K9m7+@5w?V+b?<|EI3(y>|O)8$Y=E)c>PTxxPCGt|yV} z*OLL_g%`*E`L#LEb-oJ_vpF@J1+bP+=>tSW&iAno5Z8OL>&eH-c7D5DPlEiUUY_&0 z_V9eO!Lr?6)d;nS%*ZfCtkeiWy%6gb4#MR=C}Y}B6-!Cg@C?wNYeU_1lwQAUxWJY7 z<#$;vO#x|+-QJ6%Q3#rTW#}-5h42?E6;6+}z*l$GkK%Gua!ph`P)CGoMOva;P@E(v zGb`bd7^wcNViUDptoh4A z?xkk+t|vjT3p};Cfh^Ivfrjz)(TcB4YW=kdVT@@cPtVHyCd^pTzS*UB%_PEu@ z^O=LXo6mu&yZMW|1(F>B%abXzFqYLmC`;dt$Q{T+9`fYQ%R(MtGA&Q; z=9a*D+!wTCKjq zlN42sVjJL(Q_roFuWsu1lpT2T+zec^1|O!-6sHABoGVsEvFeJ~)@j}|ANuK+9>y7- zGa%ib(lbtdH+^1Joa?Nbb**{*cTiXa6lA*ozq&K1_q^^*2jIH8@r?D4rMiL6_d@2R zYSU$URa%UYtm9^-5!=p=3R=l`|W1ONZ+FKzb0FHinFIRa0> z2mmjC&07JypsPVL%4hi80BWXkoiZ7%XUMu<=$FH2T}jY_Fq9I-Lns{;I@+Cr(N)ZT zoodM~gQJoYi#c;Gc=L@5=AEbKIah|~Y&hrWbnmM(T6%ua)k0jJid>i#06BYM?>)5y zFaKNldV8ts+nvWU=n96=5byy6tCW@4g zwWMsygb6*}RCtf^ezZyTb(Fx_0an3@P`uS77@TRyT)AExL_5MH6&ca2A;vq*l)lRF zL|=r4HqGZD`mj#6MTwP@;|@!7CVbx*j?HSJQYohnqw_Bu3t=?d&bUs?HOUDwp`fUE znCsGFtI%+F$$IDT{xcO*S~O-r=z&2YMn9lLMiT|$vkt}L_*?So;O(Gr=Erb3B!91~bpFOoeyPW5vnuT`x=IV3y1QB+q6 z@SeL_+ij%8n%zp5T`^xPQh3})mC=)~&je7pA%FoU(W4eXbUJnB3E-S4v%B_hdjYJ* z`-4g)T7;7f)?uhYXryF`q8^6IWqwlDxdv~?rI1dlx_SEwU}&)bF46xV%)$taoGXBt zoUG-w@}%1)STvb(>PaZm9iv!~7vHDQ>)0 z(nt(xPwc{BUXEbwh^d80H#Se)=Ge8sI&eC&=5bp3<^Xy8{r|yi#6n>&?04=;sOL4k zlqZWrt4wD*HJ*fCl$JWLgZ)|=3&Ss-;5NX;5W)7p8NVIwB%!YX1wZ=-a0U6q$2poYz zHF#uLSk5U-2)CVr+jZP7*5yt!qc}0)xH)C}8$Q@Z!#Un1J8o`V%1b#?bnE#_+D=AC zole<~#havVWtBvslA%!HxZ#^EY`Ib38a<&?qh1c;$77f<22jnd+uEc~rV3n4HFKFn z&aJV+aed16*L|Q3#I`nN`#B$MGrFU)R&7FLhH^`&GNn!?)bEOsB8p6uOt=LpSg#rD zt5Dk&j!oZe#d2cA(D{1OsR$%XS3v>{EP~a$a%p1q6K;`LA*NZ#C0tPxj;m9)pItUu z!#CTgQfb+6bCj3*ZaU?*DVJ~alU&Xii31TSwA0;`t{UjTih&8HPuYIP2iibvwJFNW}LG6ASn>$j)i`R5strY%Jx@$uoc}1V@!yU6741?^->g4qV0Yf<#i^S zf)T4)Z4>zfe`pplAZ3*)+h6v$V+)lQX#uZHXIt2j-`H}5WEfXj6RtjOAX54UU6+S_CFkCCq!>Dj9Puc#W54HhH zq6o**l?|D9Q?~E-fi@6Z$~W5pwVj3I{KNW_9KT{m;I*ye$jnSat?mcV1B<8V%~pBTyM#Msr;JQZV)O28 z;kh}G0(hu;SN%cCIZf#UxTMXuQwC5UH{ILLEkW-=I(=vRI5~t>>y~A7fwdSpZJG4m z+^w#W7xKq~At)3Id6t&B_pw+SO1Y8fBwj89rz;t^6{B2JU{>U#WU1Qi<{NR4Y@yxm zV;4_8OqN8es_7X3Ao zkJpu4A2#5AaojFUaI2JZ478l#!~QIj)2@4#$<}fBhR6k2(I2(QO^smlL@qQpRnl!Q zasigs+h641(;~N2X8;I7@N>O!^5D*y$)=*3P_nIdii)%ltXjf=PX!mwjdEGL+hYti zowLb{yFf;VL$er7bRt5EG(@O}Y9<*equm^%$p>W&g-w}Fx45xEn`WAbst!xC(UNH@ z`Ak>N$RQk2#8#Rv32BoraS_C{q==tb2cT`duFpgcyCHG`H1(qvImpxGb46}$iJd1! z?r}()dt;RgJ(eODJvZw(X7MO>#0%q*o^i+Ngw*09nashsk86abwkP>w+m%T|9jh1j zzd__lt&`;(+@;zjT`t;rE`7x2J0-OjZ}3QjR1nyNiy86=HnU}g1NUE@EIcfVA)_m} zt#P**#yU~)sIOMbxiLM4jvUdS0~`>!sORthx~*e$CUPLIFMy_gv?3RtdZy16xw+MK zZhMgnaMHVJl{*Q>ImZF+aoSN{cYtHZcbo$9$ewUZbi@G_AMh}8uKj<=V^tOS;S5X7 zUXjkZQpQoUab=XyC*d%9)Xj#$^=c?9ChHfEylEW4`T7V{&qBwFLG_$+i^-ByT(z7k zM9ImBzX7<{<-hLE1RH{==Od9o9TW4L+D~-+IIcE)8G-;@_XC(${UF$#Aqc>AKY%6H4}zT;f&g6i0~l@nAlN<`1OKMr{Q&k| zKL{?(5Cq`5AHWps2f@}1K>$neegO0CVuR*=4{pv71mLDF&$BncbAsQ$0>M!=Y#XH6lETfQFc#uI@}B{lpx1TJK=JJ37tdt$xvR_F1}~7&b0hRY z*`$}>rSyBX%8e9U-~1IIqXCxIsf-4Z>6d{?e?1w;Z7HMS(=zIr{FW%9Fmi5+!kCk! zO3~Z_pJi-xG;D%Q7B-d>)p10Pi>zda2SfFsN5)bZdtnh32LdWr2L&YDfND;^e&DdZ z_Q9m2!zI!&n~tn2g28a|oXC>Z-{6B)xP+Qv_M49jp2vq#_-?vHpBpXz85$W4rM6t(CL_;`qZj^t? z266Oe;UJFnh$bckC&CbC= zx06m5daY0~H}2ugK~0a@IfLpzXuicDy>7NQ68f5MgiCq8T(w=H4{8!Fey`XEUU zLE0n&iXTk~e;eHp==zEMe`D2I+5495_ia7}{PN__V>kk@yYu+&nL<2R4H6&uXcc1g zTvtQ*E7R35fE8n^5Cd3#ZvUe{D8dfT5;wB-c}Ml-9V}4b7z7y3b-~Y65wcqyhMNZy zl`xPZIf{wx0WKp>IBAK4QNDH*AKn4^rC?Q#}TX0fgZfLhVS|6qg zMy%W_9+mhyolnr^cA-G6%&9rM72#V4l+(!-BRx*XQ>_Ct69*Y2v4e{j5ZCHeB5O@T z0}CBDI);5fq|`#y!Lv{vGzhdpv0H1X?6{qvOSlWy95xyT^^KE)anPz2ocg%mh~@?* zHp5rbBRv!rlRd=e>EG+RC!}{Azhow);1WH6c7N1D8a?-D@s(M#Cx9JzDx^V#`nEq> z;5RL#%au_ia{dinbre$*2iXkXMh+Z=&44^Am^v7=F`GwIOd58zUJ;uhg`9QqA|g@O z>^4T39>=$iU`6iqYCYaON-EusCQZiKGS+~OGI3m}^$NXWrZy(iscdAFF43hH1+{Av z2v+z})nYqiqe>RCc}^i6Ke@%h@!#wEOh&)yhKvT-u{~-Tjhri^Uzz260^Dv+Wi*IP z-}W*Zy-7xwYv@8ks25E)cO6Bm#TpUsP0D6GoaB0)1kp0>Mmoh+u~C7DU`nA$(7Ht? z#EZ*lwR*(bd`FN~G|`ATl@r}lO)f7|Z+ zcDI4&|1WOeacQvinXPASzGCBZ8~f`c;PZdSY7da#c0cz&@3?SnWz~rAD=RzOtDBoI z+rAtm_upLm(PC%k%d7VBuU@vi&%O9PUg4TU7~Fn>0y=Od6b(fn=nz59mURbM^JlQ` z?gvOHctNbY9?I?adY`+W2!NHp1#5tGf{zC4r4LRKe<%Rf+!?Gp`vH;(UJ&b!hjQa3 z-sjdm0kCpsux{@M$ftNgtlI$Pr~mmsdRSLq698-W7OVkcZ5|EQFaN_7@t+64%AUb` zDM+l(1+iZ8P=1-3ex`k6eb#Zt!}|3P>#VIHQ9&m`d_BYME!I?99=3nHWBU1g-w0oL zoc0jj{V>AKAW=i-*8EzGchf`qx1{&^cm6f&{&C8~d-O2gjUX{bCwOm^A9NSv-0%?o z_3ORQ|5funuFpAUJeKONT2&K(zPHF zOD9NY)ua|6Pi{gT667>zKN>vU7rQ$6Nb@gfyK%oYiqz4AH~(MRxl6t^f#= z-YDKA=>g)SP9RPtePIgDsign@frs$-L+2rc&g4nH|Aog158+Lc9w2_|1mUfTy%&e+ zOeOsbpZ4(nw`1RU_m4>r?@f{(Alm8#?cN0 zPH^5N=?jrgCH*)5%tQL0p6MIu9mjDG=}nRzAa?5n>4~H-#5k4o-*}pb@i!jujqzE> zv9ppMAY|+W;)$d$gy>=W%qP9i&-$06=N;eUjpC_u$X*aj-~9c@!hrle59KfYq4)X8 zZ#ZW(;j@+Vz&YgZGgvS02QYYC5UY1)`t&b8Z;E&&0M?h@f;GUs@1w!`v8Q@iKmMiw zSYL7m>u!*kx(j0U&Uc^w$W)L1!iHb0yO-a-QeOGk%H`qZ%;h^b+-tve?Z>a(cTLz$ z?LK?wZ+3op=WRRg4!!cRYtO&>FIPW*^_^E=c{P9ax!|VY*RTACD=)v2yYdZt|G56w z8_(RJ*FU-b*6rl>U$3>+$hD_$KjYG0?mTntFL%GR`{~^u+U>7DWBZfaZ{6;!{>ADi zR^Pm8uf|uOw(@6}9=g=JL|%IO)?aLWV(X!;&KAA(?9DH2etPqVHv5~Y&1Y}?&Dt-m zJ#^`p){}dm-FwI0crUx&0hI>cc4t2~5>s>vTtmEIgI`;_{{L~39}e1lM$Wot*0;Tn z?tS!xw8JD=aiSYpL)24te^@(1y8E@eU+b6jDZ5YcOZqjtU*nhbt9QTJFX>n9ewAO+ zYrEI{l3v}tdP1t&g3c#nwQfyrQXnPF=YrZyQ@;Q{ zwEH2yq#xV;m|v`~+I^K@())Ms_e*;3?!A6V@7ul4FX^}Me)|b&s+XojaBZ1zh+LMV zZkbstZ&`WEjC7FVC7T;@@ysxpo=DAXT|U11@e|TmF~-PU!l4C& zrHpKUc!ukSxncSxt!`BPk{TO^Us8QT_e-j6Xnsl64fS~&{OanR{ZV>scWK!W^AaHs zjpo^@O%Of_<8hyav6xT7d+zZ`c=z42e)oGRH_P``g8?-ng*4SZtEslWZ|nQ~l78>j z_xdIMo~`fkOZuj*H~A%f38jZ*9qxtTk;6okdN$r#4qWG_de{G z^h0|m>&nT#=n(*XaPRsYScvrldnYUFVy^GsyS}n6=^tPH zW51+dy!u7IqnDoQD*JW#=CVb`YD^K3mLef|4 zzT$+m5bp@aL`iC_W3`5EqIzb!Z0&CC`X$}k+VV@fxwYw+bYpA7C+X%tZ~n7i(l2j* z*)QooZT^#A(tq6iN57>1u=x*uN&kNH@BNbg-R9rOomrh6*Zg78aq`} zjW{6zo=sI*!5n2Lv0+AF$62B6s&KZZQni7G@L`TLhiq)x20U$p&SM%?G;@V`O@gzQ zhOk~+W41hP3-HDoasgl<$I$%rI}v~i+mnypptHXJf?Nkc}bgq$F|#YTQoPS&bi z6CLM@u{lFVq$t6Ty93dRLaAXA>L$wu%}5y(=^T>A1Y(&y8nr7niAk~6YWDUUGIp}0 zAxp3$qan*_c!Y9JsaX>BMxH|F3|Wj%(-kJ=qb8*NF-#v>*fb^ zEl z=`MrR>`Xp_3`1;!&6X`*;9}Xl(ip0_w%Ly~AR+<^T&|{X!y#LmF3U>_RLV$Jp3;gX zrvS-9mY6ro;zF)iRHETF3YQ^Xn^eY?urL{j+>jr}4n;>)BS|*JXhXLN1T%Hp4H;NP zmn4{pY$$CdP|fb4GOZiRSnA|$X3i`l$a-WvY{~*Z!BS$UlStNR19$k2CYxF{m7+v_ zSWotoGTsA2#@vQOwyar}l;e6Klwv2=Sa8~cK;?58QJOPk@d0ah)6EK!Zy9JvMW95l zVmfACV91(6Djl-WYK_6FnG5H@ib>yoLzZ3Eib=IG5s^Dmxj5uPR)HG^|m|CkwG$bQ~`xOF6bpCuuI60)^^hRfdMkIt7M|x($bHSu18z4jUBO&)6;0 zDB~z^fk+9bHE))w#j+Evm;+38>I5DdbBA%MR^_F5DW4t>um;AKN}jD`n1M$EOJi-d-m{D2c`Ig&)^iC^;#D@Wk*$`-Zl{{fMTSmbeXvY+1UD)X<`B4SCoMadbAV6`PY%Y2NNXrf1!@%%c&U8uf^*Ug?ma zAWmK`vyDuV#^G8bmQH6}GlPD^J<^_nR+R{kebD2kx(k&cC*IOk6T} z0JuG+XH=TT*n7%jdC`bt97oU1H&*Og>1>Xqy^yfR60Ww-F!wP$642Cks(q`lx4Imz@rIcM5={B zG@V6qp@Z14j3*kyoXkT`JX}NZ1Emluk8CpmcVhT>l0$Ho%utcB&qyh+>mX9<_08kB zCwFhqR+pQq->=-wOP(H25%u=0%p(HN~nZR$cMH zpXRL$l%C>M=ESBLXL!zloGF5bflr@T73VsuW?gGu|GjJIistohN>q0S^`6(An-Us$ zIYJ&obrX$-&doro#_hT@u4PRjmW(iZTW=vYrwhppo*jf$v1j*_y%bJ!2M%-LEF?wM zidTp1K*+_c(Wq2xV)aQTtWG3Rrd!a^gffss*h%BaNi;fQIG17iZU<6n+b+WrO{8P& zM3Z_WKAK7#&}g#PZp%rZRSCVW19kJ-!EtQ5%#T->TbIMHx|v&PH;B6NR?%CQ;h+r8 zsE?U<`4gl#W{tbGdK*Z?bV2rj=ykVkDdQ8Fzw4YB&}jmunV?qsT?;U9`j&M%w0V9S zpPLqN-}(x`y68e5g`($%MAPPxh$ZUnrf6fKp-C~B9H>FoNYv#tlxKB9C5t^sOmKGj z;)z9vP(B<*VrVnRb~4EZr6)QRu9-d48KwuWV$9FV3W)J9Bu zL^|0-96Czze5_clYKg%>3Z>x|0cs^qwYTGKJq#U;KFFnO8kL@>N<$b3(?vnr0fe2*u2-&t8*Zao^Gk_XQzxI0Idysyqk$1Jen36hTKo_GM3)_TiZRqmRmNu45U zd25sZjqhopn4gm4CZX`Ud(p`bvSXfGiL)xVM6HO>IHns`E5mfVxh&zBb^>;aL#Ii^ z4y1l$kjX_#7cYJrwy{!_u$TxLAv&;w)i9zSPdH(h&N4)~j-*pbI!3_RxXp-oPnGjo z*=msy0qQk4MdTBx+K8Zutl~}%3i+N}tU?jh=Q87U{UrYX>sO?eYj3@F?dtz?^(9w+ z?Mi*`3wvFV1>m8};oTqGy?f`kcg&sj?ZNimrFUFHw?4N;Z2srX&gSYyZ{w@hU%meH zwVz#!t^W3EYUPgrn}>hy-+Ay^TL%B!m#yrBzrY63V2EY?4beWyBf??4Z+(C>^Ve);RM{H|6wsi5qwOwPIT&-Hde8;6uskr#>hqNy0#5B?j_(%qmXn&q5Gp+i5 z8%P~@aZ4w!`PQwm@!-=zdq<$XNx0IGMuVgzj}i`=u`^+eX!Q}erf^tQ9A)Go-ABU% zaS)05Ztun^+ch8jx|0!fQ(d}N;Ii*-1ch`JcZ(|J^!L_r!55gz6mH8f0u%s^bzcT_ZQJEx_W<->g!%p-`w7&)1_}g-@m92op^S*+jFND5!eB-Hg^@W`Ji&#e{VxDPi4ik#K#tcY6MQ z4QR*H05H|%+(sIuP`J~GwMuqGgu_ZxjAzM0Bi$HxRA*oebK{8aznwGl_p47{C!BfAwcY4ZP0quBGrX7oQ z`gRA!at+SGjEJH}TK%@IJDrIlPhyzF5C-HtY?zK#+&JHRS9 z5sJ5(1cNgTnJd?egJ?&Xq#`4lHN<#_@r~vBl-UJscvA*sck83_q^M~*=rD0OG?71Iu>#wue$teSixPqpsU$xb$%P3la8hz*^wG72zdc0haHl<890OpL|zvern& z)l$f{aK7JU4MHQ)U@Gi5G$uL(>UqEwH!!F@|yAVzsO6_R*Ihlfe4svA7( zHblWZj8ciDfD%8S!iJc5?l0PT5GCP4XQ-|d~AGV7onZ^|sS^zuo+ z&PaB0yLbfh%{^7Nsx%NBPUVrV|u0^kX)zv?~`U_Xze6@X* zzIw-%zhC|T!F|9_ufA@zvKm>vw(`Zz4{m<%W^E7>??tMP#-s_X@JwEB)?UU~3ebT*aWo7LZbB6WFubv!% z$9x24b2P3E$GMi6HXRTwA~dr3GqQX2?LO&#*eBf&`K0?npLB2YN%sRj>E7y-?k!Wg zZo4F9gcPMEvpO|NPQOSdeKIzOEokEmLWgc7`t9H{EwTbMRd9d|S zpL8GbN%vu&bRY6b_d%a@v$bep=BKUMTC|vMwiYd>o2^BQ>1J!uV!GK{w3u$T7A>ZG zm(TJ0IiGYt>yz$heA4~&j7}~O4Jtv@nxj!~GOm*GGxK!i9X{!P#3$X`pR>-d-sx)N zqzmj;ObXO|V~1t->=nM{kE}T(%au!%Q*Srw^)lZc53^^p@4Unx**Ezk`$m6cFZM@< z`y-3`Ba8SW3;QF(rZ07&y=Ch!eA4~7Pr5(zN%uc}(*3DVx<8rG(RsQhP9$JTEm-Av zvvo`At^eVZ?hk#^{ee%q-}g!Pdp_y@yHC3R=9BKf`lS1UPrBco(shT$ektCq_9&W+ zb5=sO&nT%&gipG-Pr8^-x_f-m-R+a^r9SCi;*;*1W^@ZTRc)ViEuVCbPddvdoxQs` zUSU^0wYvH@s~=x|DpVj zu5P_!t9#{Z_I__0ocFK16VwZM<$8YO5BAb~Prv-dm4Cea?rn7ak1xL(oZ;`d`kU7p zTlZXj>B^_BMR)(=>fZWmHvY}-hjzbf?YnlR-RExp__fbm8SK1h^9vi9?O)lXR$sXD zx7%Od{MwyQT-)0CnVmPSJ?F}Q+4;7sZwCN<{DjtxwS5>V|szeTlc?6RcU1A`#TgI_$NFbZy zAurU5+V%G~&>ZGD!yPIkL`tVS=tM>8 zkY(lsBs7k)RO^y7eIkID!PeK!+Ca06TuPfyMR`J|tI%ki zV$&ID<@;w(jPPSW&71knRVtl!Rnv8|ni&&ImfenoY7r&YH#1Vb zU+yOze&^Y;usc0AQHX`&P7IHhSZq&^;%JNcUK428J1@se2^re5W- z8Xe|h6Nszvz_XxkI-tDSVaMKW0=!~rk$4KZv+X~GeSTrN*XXe3OfdVOZ? z)iYFum`jE-l>|pmx^_I5M6_fc4?|Rp2Lo`J&O({EB$Ea@Xs$+PPjC+K#a2;{;eZa{ z&@wyhR`5wvLMExPE(%EmX@!Y&Wn#8Ao;iDhVTnRzf*c|_#VJWbUy2oQ9qX}nL9^vX zERvAB;ix*4_~!a6PM%1JSOpc*CP63Tkx?Yw>x^;{vusF+YNiUtSSiIKZk5jRn=hX| zL3cH@ho?vB#DFQ(Ns1nKtZp?@%Nt3d$a6h1U!x{OqOY2(Grb$Yg1fadYa@op)d(5S zqr$j_RuKn*P&w^V;qkagn-P>~wJRwiicJRYrSF(Mfp=iLD}+WalIhEtxH5^j#X?HZ z^05S;W@#W;QmO|FPNKO5&7SB*C!Ks*(fS23oS0~iEemv)ji*9gh^WU(bXKqTsMz>$ z7+t@A_C&_R)Qi%2m<&lkRwoUc5}9FM>R2p}W{2RMY`Fv538UNJI(tG+^WBufsCtPm zccO4XK=g(ZD>c+q!-?`mE8K?qc)px!qTAmxeZmEm2L%zK3S*kel`~3RXEicXjpAb3 zEeX}OpoE2%D@RIhb2~hH!X!irfw@#&tZErq$xx{T#FSl2!!)rg49ZEHKLV9U?`Gr>i$uplbZdl3W#0R_Z{h#<<+Mx38sXt47w1aS)0Jw{Lt3(#a6UJVhtB% z7cJo&bj?T>sfx}l*%>2Sbxi~YEsBCGl!~PSS#zu$M@duDS4t&wzKFJ25wuOgmqM9R zD;AJMY3krXi!{gOoMgt_NhG~-R~|Z<>NH}}8`7qoXwyV#8LVt2a%jgcPF^u+5iD!$ zFeWV%P6qRE1EZ;Un6`!~X0%bS)tv2+mVB%2G{N9Bbj^cqQzlK zvn*Zox^sfqaH5mRL5sZ0*-BYVlGhs`GyY&T9M(mAWrG&auyih#wb5Rh_gA}}f;5Gz z54V<%SekhggLQLyi^JBoauJc`9fCDrh*=Y2yW_~}QkE(zq5MvRE|LY8Emqf-NNXUA zdqRzrzHFBAA%84n6%!guElzpqhCn6C-ig6}x)v9+QA%sF2u5d%4VK*cT+vu)+am>C zuHq*J7%HHNiyb=)r<)n{5qH@rPmy5@e4}Zrr&28&lVA#{&`C)KEl$qG)~;O<~0Cti?P-rH_^gFK&xg&n(%7;nbs_f%U*bR#U#EY2BW zvRW$VmqM*dUUcU}nz9bn>g{&Xo->8bIaEW;?>y*(6*IQBjV(t|(PEC(lRO^JIec`| z-iW#Mte0~Z40>A(OLx4p!#8ua#3jji#LQ7fqG9bA3YY~}h!;C?mMcq6R-+~K%}OEN zwr7zW23nD{L1Tv0gSV_TJR0`X&5SwkL2Wt7)v&QdN1Nbs4w_Ap5#;JY3zJZ{vf8dk z(qb)Zft2b6TYzk*HMGAIFXwRh&Ax!+dBLU^XO0?zGNhDX^in3k#_O?0 zgKB7Kxb3pUBkpz-*M%c9_CX)@Sk;<@XClJvz{B=JSAfYAQu?~LDAb~Uv$@>PW&*Kz z)^5h8HxF7E@-C*9(qVLjXRCHg+U#=KP+hEOFV;;?%GI=4JY7`p)9Q$Y!HGyJbp&$= zk2P5xX~`M59BmB-rBu%B(p!U0o)+0;A>XPQbPaxC$UXM7$>VakF;*gT$wakN3-eV| z)YopC+gQpcNW3iYAtBu)N<^F(rx?A4clom#uWhgY&79q)lW* zqc_Z&#rZo1&ngfSx|G>S@i?8yn+XGt;i(E8tCz^KLyyO4C#Qiq?+sRCeE!fuA9OrM zQXF1z7X_^?<7;Lu=6b5F#ce(*1kXrW7s=Oi(ORAJ&c9!Y|JSU!am~tQE1~7TEnm3o zU3zip3ro($=N30F+7^Dh@acs^=bxV6G;f)Ea;`Ur&i-`v)YZ!yEojT#z>WXyLks&ftQftTu3zkYC z>@Bhst>qeKwyh6m+_aVtnlzXvn(O#ow0yJ*d-Mpf$%^QWXuTPqsavzueFB?vHnlJd zahHh|&4EDPY%h@kp`~Tx5cVh)_NWnHjqW6ugj*OC4Od+#3@E6#C0!fmYq$X=lND=( z3OItgq$i2OXM3ayd*leP(P*ohVKaEblWyvYsZM}#2idwK;mcz#4O-UeFm1;lPHQSI z2z!JId&CH^u23~b*#faxm)3YwLOo17OL__OkX0%{QpO=SnyH6+7MN>fpY| zZL53uVzSB$8NukN`<$6bmV#6hRbiuVA6B$V_!Ie(J50Bm8K0-y(mU*xh@qTH#w{@% z?uQL7Uj;s%fT>f4#>=R%%-aWgj1zA}%MleeG6L)v!9}!8tFZJ4uw%p!(Q;UY4UYgj z#$_HYhg8_m2(V*>2hnm+g$<4XJ4U1rEeBNCz}tsRtd0@eL(6^@);|L57-2fJ>{DTV zBfyRkjYG>`6?TL$pmBn4Xqi%BsS>BhZGHJu0kc1lTcRW@y>1!n)r+5@MV%8CrI! zupX20_+%fnY3(EVQnM8juCu9%T^WEIs)t%fhM$!tFR+PoQxAQLd%D%u!p~WRKhr6 zBD8#%3VYZHuwz7n(DI=w?4cvTjuHGp%ZI42hl~I_MxX~RAFRS2JOb<(F&nh}Q5E*1 zZ|{jePM8KQAEd$_Gy?1x(HLs}zsbATEVky>A%{ZYKfnJxf&XJq0H#VJx1>rM!~Wmi z?BAHMMa_n_&5Veq<6%0PMm?8!H5J$q`-~Ijwe5_BV{ncNuvIdcL9Ef%$zY0wM%w0a zmg8JJogs)yaBKfirNP^0E8IG|O=0k&qmpmu(tN&VZ$2OoosMo81*CWwgzdE~ukm>iM#zQp-|A(PUh2 z$>xghJOL=$VAnTuVSgE3E@#?k$I&&IjTv~2r!59}U%cG(TATSi@2#<1C?|5y0QVp?lggi-ZU5iN)%C#mti{eJC@uNm|$6YrvLUhMmf6A`QM>NugK z>c1Z&40hYEu`xoCxAhuRG7Tw;{OeQhzTUV@P=hz8yL_iF4xI&kOIzxsedP?*2{WNw zh;r!g^%W_ATC`jMk7cQDM2xuEbyCaS5~8hl{Nf15*^<4IOgob1NGB=qt+dq-zg%iv zS>8r;q*{w`p_t7az(RZ{>+-o{(RQ#19k~3QKPgpEfu>Sj2Vv_*nV^M=MM%otwd28& zUL4BMp)Gx4rf*klN#1p0oI}v|vn9j-Nn0A@V7Dz>dUH0S|38+s&o&f+dnUMhf~jZU zkk#6_O}pD(`TulUO#haKSSxrt9L&Wfad{o;^Ar9`l4y0~t!xf$#;yJ!-Hw-xwAp8< z5%1U#^TD7_(BUB}Dd-AjALbA0j3GTO8t9lCbuvORWU-i~7?p$H6`g*I*;8y`R-@>L zzOi06-t%9@o+R(#8UUOsQxw)C5& zGncT%pD&)es9AVu;n;vQS3MVK3aot*%e0zdL1@_EEQ^X$y0W-QZB zOn+kfz^R9)j+^@6Ag24ZTAJeZ)r{717lgn?gF>hV??R!7ygH&}_ z^^&K~|Mtgg>~5>XVg9EdZpVIXJm(X~GJR169wUF>0QiAk^(T*m97zeTNV=IsQ0LcH;NHyyB5N zu4Vc~6?lwKWCP%9H-9VqdHW5&yYzbN9``(c@i{;J>k;=~wbyfL_u4&9-|ehjf6DX= zD)1N|$p*m3i@Tor$wQi-e)7@h4!+^6O?$n6*B{(9HFNy6hhpt#4we4vznOks1s>xw z*#KCyiNC#L?reIen^OBeTz4OP%EJfk__gVCe)`$JE&g@i$W=En{hSIs#)q;2@XAfs zynNM<_TTivo(EoZ`U6wve0*w_d*~M*_|^WKIT4%7Vf`ZfAb#w%jNm^o_phq|9#o6 z(+@l-U;6xIxBTJQspo(96Q-Y1fyek@HUM6G&2Jyr#dOK%69iub=HvR1S zwcq~YL%UyO3cr%sdEGBA*i`ea{VqLsy8rs~*&luPT&B;dz+n^WgK-UlC}r(kLzmQI zF-@T6jOD@=-W!dvRw-I2nO!)E#s|QjtCueP+P)XuH}!+#?)}j{|2XuzM@~85NL~6d z5X^ykR#57V2zpY`?UPU54s z@}o>Yp#qPQ_HF?DoxOf_&$q+ZJ-_o^AK1iQF>y+C>gld@&iXU6U#IRqt+9M3(?41T z9^+Hr0QkZ+2i#h?`Kg!g|H5a#mjC)Qx1RI)^RIlI+hMdvfPdgtZeUpW7p`+dOk%8&Q`1JgfJ1s>yb-vIb| z{OZ<;x9k6L!V4Pwg2PXH;k3srxwG&1&RO3(|G7QC^;zTsrhkMA?6LR~0aGXtsbx`T zlnFq8@LH~kM>C=Xv6urwdG+hd$%^dvQ6E`(JbM2S!p}+Px(~qjaV|=C@06%r)(ax*)jNgZR^y>DP zkI!9x``+i4I;W^{>&Rq-oBQccCep`0 zvPpm6Bfgc}cEiIj?9KEeD)1QT?FPWV_{>+`Q#&93&GQa@?+?y9;qXUq_{())xoElu zZu#ZI@8y3UzliD6Dsao2YjBhfy5I#hMy)ksjGO94YbT>6jMi*`X&FqRbkgcc6ol*m z_|E%;-0igw*bjaBaQatgT=mqRUwFWWpZMp>&o4Z>w9_4Dw3&Wb1;(YW#G}cW`0DGo-5 zss?iXk(+lpJY@&gu2!RE+*-7!Ny?gM+hw+Vi~fVRrp@k2(JiXK7>BaGE}ZAx|89-OBYlGkw1b9E%cdmot=i z2npKmb!PmffYEHs*I1@oY7kN+nAEz8;Q#>>Q4BObiqq{~PwsNUj~4Ix`9b0T`NQX~ zNFVvDn?3KrZ=XH$gLhxoV){N6m`VvrZzWwPq!g?KyMaKu9c#u6;fkl`P9Y$6`1E4mn%=Vnoin@aT-48HCk#s-qo=bNorG0y~fY7 zE^W){A3loeJ3dXQHUe|< zGE_n^CmXeljfx}+;dsj{7~6Hp5@?y*Oc14rC{eQ(2hTW7UVBLBl`q`%`>X!g3%~Rr zKeg}Edz4;y`J?yH*hh$yuY4cVcdNi-WRn~K|6}UK_uX*Z9?xEWU1;T!d)`N#x#=6H zZQiuwMYlYA&9A?A*4IKz-=zYN;V3WwKKlOqj@8~+{LbC?f9SX$UbFwrSMN~& zi4B|1+;QcL;j@^&bD*(1!_*sfv``D<_M)i`gX9Vt-j5~FU|6aVp;Dkxx1t%rU3LwC zU;gaFH$B@v<0~gF`>s3uS^j^%dD|-|?{xijJD&ZwtH39o{&14%lPd5S9}5S-H(t6a zCsfGql7B{HB8SVSebwDOTYg0{Qh4&cKNRS$G^k$2^DyZd*=c0+$Y1g+=49Wn8D4=v1;#i!Zr^O< z8#cqMk}(w+;|%upKCK%z!z-0B6&T~#I*J1D>IEt=#vyxqPXWB58B>AnJO%J7XG{gQ z^Ax}|>pxb*2;MLHW3T)>ofLBORfidn)#_`%7#R}k66I5V~!`SxCVZUKBypkGI zfiX@sZ|~u?VKcnC8dHHWe8xvn0A6uH1;#kGZtp38S7Bo+u$`v>UYU)lz;>Pjc(tai zfzc26+j|P&72KE#Z09L}S9MTWHpaV&m&D^UIaC+uq$&YXSU^d>0$=Qs8ffR2Hl zEslX>T-&|<@R#w=HL;Z8--1Yy5Zr zd5(dY!Jv2=ZquW@DbTJo>Ld(pL&}mB@?TRN zLS2?_mk3n5-iUFAKq;&DY&TniMNzjA%kgh-OJi(e zwq;A(@WH3&_4%TPH?d#i8sKBMy1yk$Gx^5tuiP*KU z&}*R9%G!B*=v_DlvU=7DGeDV3(aL%{#I=(4XoN8334trw8m)AtsZYUdh-4fyWqg5T zJrxw}*^aJg?btfCn$D!Ra*h_43Bj~>`nH3Pl6BfJq9H0nSG?WpeV6)DYfOx@Mc#gf zi2a{5#BrEHiB#DtXzR6_S6h4L{)-IoRRxqFCYZ^ulplm_q|%jo4*m^=I=|Twe!Ds3 znhbAkca*W}u(U9bSohMdb{UVa3&wOQLT55(1i#H3#)!5Qw_Zb$*6S#63 z{-5f(VECsuj9Z0+E#I(BAZ=+u9d7n=FHG92{*Y@r(i?C76JH%6@*ZMDt)wpjIL zyf&(4Fm@}PBVEb|OHwqerE{H(zEcvdPJ5nBx(TLUhZ|g|rj*YLT)j~nhJuzv_#AVh zR6hvAv|2c>WA{m?^hTr3auApBr*IMeX`d?9h+ttcWMP?z;P4QB-iEb1R`_6 zhS?U2>#Ssg_NtNL@pM zq7jUzP`c)_=|o+bP@U1N)uMW>P8m9+oB4UunnTz8bj^f+VwcH_rUFyDPXB!R;_33d zIM-XQ&V}aQH~Yl$FBf*7y=3n1v%b&VUdC?lnoBlY;A28@MF}YgEmfAM7ZnP8|rL3bF z?F2%3TM=_pbx%U8fyuMP3KOn7x;|rT;4?A1>#(6V&~OJT*+P}@GdkSuYPian*3Abn zLT`?FI+hZV_Djt|$TD=y0+$aO=DX0NsBEa3>4Mf-6Dx&;t*L7zDkN?T60VrO=rq)- zVSS+J7<$-FJTUCh9St>F(S$E$6dZBJPL{m7Qbcbxz~flNQWag*V!~w2G^hCCTbcvF7YL!JG>h zI-Wo(7RQE9b$-WTLwz#J6MnK((tEvVoOIy9tvyGEj9988$Q)D33euX~$@R z${1k`DOs&JYT8UZXn+~!B`#!6X$m&2XdFIOWXsAipm>EySG0v|0Cb~*yO*+BQO`6scY$4+=)uIkYD3_~&NGXx+6k>7Kq#r)j$Q|b#iClX)#^p!0^r+{fCVe7i*JHkP z42Jmnqp6g|kd7oZbgLC`VcC2oduKDV)p}DKCdx{BPm!gWPsJrWRrYt2J7c@sQ!6p}pbnAMTjV7@6&>q1dW=I_q zt3?Z&LmMW$&t>I&9jhG+3CVyfg2f}1YB7LmrHJU&3tXcW7`hFjgAQSKVQU93Tf-%v zxgkYcT9~YnaC2r|A>Z_xs_jtP-;HVud=Ve|JWn4u)U-_zywEN*=rYSal8~!RlBJH#(zf8kX`GoIHe_4gs6eBpdL+Obvak?;o=x20jSlcM4Ue-lRym7Eg`;^`F6=6uy4ik1knO51Es4`nnlLscga8a9ly zeN41Onru{|tM$6PVOzPb(OCGDI|kE3XS@}s*-hv5;pVW%sSAb;>8>U&;0dBqi^It0 zV2$axxJD+$X-g50uRv*Z?pmYVETr7SF;3%K8e;iQz0mRM>VnfAulanE&@ct5oXPCg z;hq3jX{SO=gUZ{6(=qesVMAZMW59BnZaj%)v-(=Y$MDrOe9c)Bv9`{fgf|Q|J!1+B zTh{oRFAW=dS}sf1RM0dvb(VrkY8h)Hp5Y6rmetetc>?Aptm_d&h0KuTrdq>>Zh~kR zM7Envr>ihiG+It1>n)*dP$6xs ziJ?rI7YLGUnF^^!RiAc5-FX)=Ok#-}M@M^ib z8MdRXi@=mcTmGybf;tE$wRHGThNLX?tW2vWjABJ~AzO1%Dz@i5O zdz|TRX-Gx!wkcur=bJ63wIvjim9hKj% z&gCmvzpJIO5W#r5m8XkXZVQp`IqcC@ce}%>R?OfceMKoAPI302A>}T2NpFg>V^Y@; zV418ZZy(a?@-Fkgcy~K8o7EbsIC&$3i*zYr3>oOWFTr>dZn36`+dM`xf%*cr8cGh| z0P_zIRSg+1vo)2dr^QIx)k->Zs1d%j0=(dL*)vgtE8`GSp}fB>4p+{iY4Ngmrz%;n z^2WS5VfJ-vK8wBRlnTL47){j-rnDo`@WUH}O43na0z=>9*~5pbmclklB->eBs)RYB zfjPRl3`GapEi#-i1}RUcp@XTZgYe}rSUIy>rh^I67N>{R8DlPQISwlhS>X=FW7i}L zuy{$;7C%d79SHJGawsahS5FjguP!7W?4ElVzC zru=Bxg;ovCyvLX;6{MCn5oY|YG|R&py#|*pREB{LczQS;;OE1K6E7yhdMDj-5lwH# zDI}tlhOI>-epuWi=k3;gB+=IUY69gOZpp!;OMAS#dGwa_x)dL<=Adm^s<1GF)m{t6 zoesJLi=5O`1`n+tLY~c9@56u@?|f4LHQSE?4=C79izj(cmO&0LICs#?lZwy_PbKJT|Vvbj8=Ph^c9Z8A1wfZ1Dy z^OzUI7Hg7iQcX05>tp4VHJXd*U~!>vNAl!cMN=Rg>XSygLXxOo<$kSjNI- zi=9}!N}@SG>5m0*YgAXM8PIIoj#+Dl;*jN*62l&)0!fF_NTHnZML788XiF|!=dkE4 zc}QP;Ge(<@O@oEzeZxh*?kf?-l8>?ZY+D^_D&q&`L4@}*qCPZDv)U9t|RV~HE8%62>M3hVABObxQfVo<6kGBw*_RGl(k^{?Y0ssdmSz|KvG~H`Zx)|e z{PE)V7QeB0GpsjwA*@WW31%laZn3tQTReJ^UUV-W4vGFFi~B6@wz#}Fz3|tCKQ8=w z;lCChT)1!H>kD65xP0M@3!hn#7e2PoUJw>i3+zH*!Le}gf_`DYh4(MKXJLL}4UBg9 z?fjGTkIvsefA{>Y^ViN_Jb&K&r{+(a-!NaFFU%h^&&*Tv)_H9H!1>kr-RF0hN9JCh zdw%Yjxu4BFH20mkJLhhiyK?S=xzEj=F?aG@cdjy*nL85Ro;c?YokQminA>}9m$}8c z$=R1?e?R-w?BlZ!%-%bD+wAqTm(HF)d-m+c*%N1*v!&VOY;4vyYo9%6Ry+IQ**#}> znwwfG5C@!S}#7z|G)la3RG7U^lP~rjfrQ ze?)$b{1@^dav$<_Ttj3Sp4|;y?~Y^vHh5`;qq`^T?W+7h!D2 zlQWOPY7TeL+&XjZ%*8Y3!ORY)&1{&d&lF~knPFzA8S4x-bKuPC%b9xtr!Jj3f9mY1jZ-I1HK$5b$*I_sZ^}M((3E!S!&7@s?KCwz z`H#sLCVw;e#N>}Bzc={}xKM{b(8s4HC&3TE{h)uiBHa(Z5Bi5G()Zz$>L02|-vi$T z{X-P#yWl&Zf3PBb8{7x_A62CLz_&pEAVvBXxEJ&-igYiy2lO#Tx*OhW_05X(P59jV zCPn%NxC``+igXva6ZBC1*It&|j}e zw}M+h|3F2$18SLH__nx)FQ@^!Hb!uYemse?LXK z0bCFI`zq3P;9AiCup(Uxt^xfIDbh7?o%B}~>1uEl=zma=t^!wr{yvIyCAb3g*DBH# z;BwI4Tahk5mMLHjR9`xU673L(4SQ#_`mQ!Md||?^btjpK@aq26lo(k9rULa z>2z=!=uauqY2Xu}KdDHcfY#TaP^44ADWJbbkxl_81Nj6+IvIQ%$Qu;tW3Wn!e7qu^ z1Wp9)xvoep&;+uiNKMcHa!rvMpbq4!BGrKe639m? z(h-0K@)3%}f;f;_MT&zMkmHIJ15qHy6e$WAAV(F60TCcGiWGrdDMu733_?Jr6)6OQ zKn^QX5Cni6QltRz139QjKHvp%K#{zF0~svR#o51&09Hrbvf? zgMn;Sq=Ugnfvoc1LBIlJmH#XN1G3707%&4_bJ^*Bu|2_cT z4`h}9-VgQyvdVvZfjxn&^533d4x+rkX8PBAAJAGD*x>Ub_KG^f4hQRfUNT0 zF5taD-bZ?Z-!A?L{`EMt%Bal`8+Y#&lWR?GR04qRN`ELcjS7nv|mcSyA zRsLH93qV%+Zvo5$S>?ZZFb8Cn|K`9fkX8Ph1pvq@{~=%o$SVKMfN3DB{5K7zfUNT0 z6o3q*^4}zw0J6$|YmirftneT53i5X#EBuH219=(93jZO0NB#z6h5wM3k-q|2;XmYW z$V)(0_z&&^{s+hk{~`Z_`~}Dg{~<3SF9KQNKjhEIpCI2U(hJD*Kvwt<`4jRSkQM$z zo=5%&WQG5b=a4@DS>Zppr}!O^Cl%=r$nSxy@E?o`{Vk9c{zHC;JPUdX{~^CY{u}fZ z{zLv7c?R?p{zINYo(4UI|B$DVUxS{)e{hHLE6`K;5BU}H6zD1ZhdhP+67&@QL!Lx_ z0eTAmA-_PL06m5OkSCCzgPy{F$j_0Vfu6#D$j^|Uf}X;E$WM{~0zHNQke?urgPy{F z$m7Ukpr`O3@)+_c=qdb%Jc>L5dJ6v`k03t=J%#^}hmnUsPvJl0A>>D(r|=)FZuKDO zDf|cbQ9lGdh5wKrA`gI`!hgsQko!ST;XmYl54j7u6Z91RL+(WG06m5O;2!UG&{OyixgEI;^c4O>ZbQBXdJ6v`UqfyM zJ%#^}TajBpPvJl0X5_1&r|=*0RpchnQ}_?L3AqvU6#heQM7{!g3je{q;|-vv@E>vm zay{rN{D)kRTnBmz{~^~Q*MgqHf5^4SHK3>PA94+HHRvh)hg^kR33>|uAy*<-fS$sC z$Q8)tpr`O3+>>4gdJ6v`mm!yep2B~~rN||qr|=(g334&$Df|b&A6^7{3jZM&As2$4 z!hf&=`IkXY;XmX8=dv40;OxA)AqNKu_U6%3Fpr`O3+(UmF^c4O> zK8>6WdJ6v`XCr5Up2B~~S;(hAPvJl0Q^=X1r|=(gCb9|i6#hdtA)f?2h5wKwA@^R2p_z(Fw@-fg;_z(FQauVn%{D+)~oB(?JDAEbY2GCRZ57~em z4|)p!A;%-ffu6#Du%_Fwpr`O3axBsXJ%#^}4$=lah5z6Qq6K;i{~;};33>|uAx)$K zdJ6v`4Wtfw3jZN>L;^j9{}2hOfu6#DNENAop2B}f1rb3{;Xgz~1kh9X51vfQpr`O3 z{9;@JJ%#^}5>f;`mH&!J0rXV<%Og3^Q~588WI#{lzYLNFJ(d5`NDA~+{!1Y|=&Ah2 zBT3Lx`7eoZpr`U5ha3ZUdrBV(@S`R{1tD9}^+?z($q-D8vIcs{H3hTwtThe=fucHmdyRL`bkv5u z5r`dZRQb=2*uX}W|7?gAY*hKrir`?Q%72F=hbjL5`>Z){&B_@o`z@cdtXcZzQgHFv z#UtSf`qYK@%wIkKfw>#zT(ghNI>D154B7j5WMSqDFyr6%r}?SprcRlBaWXqOF;Sa9 zAjo$9W#u$78C<`VMnKK*CcsZ$P1BX6BEwZ6Dd@aq@)2m|?CWC|KhJzlV zHrXhHHdwYU#5YDkGpNon+gBY_`v!INmQ^Meh5&&_a>TtDc^!S~bT-l5=R%^Xe)`Y?u zSR&rWS3_tcmDiQ4^)TmQ{gT-yit^F0ufs;`Yd8^ab`M_-Yy5USEDf90;#jw6*XcA} z(d2^(k%Ls;;R*5PN{fi*JU&-M%*sc>9u+elVRXYDUuVVv8v{%AR)PtfBCB~3V;Hmn zzsHtpwpQ3?ZTgH67h@t^q?In_Dc_?=iO;k*aJbK$)UZXQhO3Qj_SWf^osd zbD?-z7s%&Rm8QekbvnB!FCPK>QtW2*Q4Q@TWuo2fyw4vZ9HCZ>6pU`OJ8N)rq%GtK zXcK}imW%5OQ8s6g@-jOd(FnEowzV?G$T%v>aoCr#GDaxK_tq<8j8vpqIR<-FR>lam z`rf=U#>h>Ym!reyI70cpZLN$k(v?DTVPIzp!H-bXZ`j%YO7I5k4dkNLu8xu9l$Rrl zHt->jPz!LV&2XWPk?|A`njQ{%gxY{34cff*pvTCQD#~G~jk1g*~!jY!cwDq)( zk(P*;{fahl=p$4!9BMOM%S{Isq=JE_w&)I2qjoG$+4HGzBMRMVIl@WPtvC_UXgwHR zc-gIJ1BX6BUB#g`!^Ja3793G_!CsWb zGeVigVK3WQJY!_2$;wXHqq2BLsK7Yv@pX%5j6^JXnS?zli)VzQjKiMZuz1GEI06@s zLzynPct$AIIPC0yUp)G)7ta{!KX{o?w1Gn(p~B-(o8jUaBXfx;+hH%t;u)dfTdvMS@5Z(b~@}*L` zcAy&l232}}k)(f7!012&I6np@=x9samQus&&2d}ruwIfLMkFhRV73{iRU*YK!DY1i zqBp|hjJ9nuR6I#B;f%Cd z?fL>QksS}=Hio^fs7>Foc^F2r``Q&#*jUWl$W)cnwefB;RZ0ud{Q7#p$VAtLW4Zuk z9Vsn>I&>u(TkQK@HMudeB)@BuGrXnAZL@;XzrD$gk+1pR-{kbKH#uy3nw-v}N8i%q zY8`jMo_D%}Z7hni)<_{~h?FIwxUOKuTeTJ$tp>e?0-nsF?>rEjr7989i?!^rXxEvI zF!1`av#z8yqU*{v)LH2AfsWv*1S3tS!IQ;pyqjsIVw7jSNI8P-Y&@NdNHA`u?JgI~ zp*9oO*71njHd-LIGW3DTd0#cTF_`*Yo1FeFO-{e9(Cq)1+`pin?W=D&-9ORfw5GQ- zxvqzk8oI7_y+w4#g=UehJEc&qS`F7SE{B+JdQ7H_m&ls9EG)wZiQt_CBWKDj)?CHo zb$d6MOX*@xy{FvK8QEF@7R=Pa_gN(A$>W^@)oH~rKHA|^O`^~cNER!EC`P+Jm(r#M zQ%&bB7i!g-yK3k-gb{O&Sm4kHCP%$$a$_*{yEZx9Tbi8i|D?%{k%jEv|1I~1gaAsp zzU{WL)L|I*mi7OvLdhERQ6_IJ>5jCT`CzyTQv%q;WPyrX;L`Vz-85TrFycDw9h;o1 zOu&pKu4qy$M>(A@5(?X5?NBb#3Gs1~&l*Hsv%`tSmPcQpTBdGETMcN7qRSfZ;0$VL z7Q~=&of)t3G#;;5+pcxG5}h8gsI4+|CH{Z!!LVdUa zt$Je)x5b8X0f(jArLPc#=`ylYu*L1)e zzCxQ$)#@R;2$OJFm{2M1P3cVOLV>AtnsF%>#jPd{)zPjBFpSIA_9UbZ3{7oD^WHLM zEZWSbCf;V9I-wcTlNDz)s<-B}YlA%JtGV(KDO#cP4Cxo@TkBALRR_9LG*dzXuQai0 zE{!|9*?^u4dmP?mLxb5Hg2ra{`%(sDqgGr+xoA=_rv?{iOy}G?5h=90U7gS-!cjYs%~gUVA5S;tu50f8^s1~<&3=%l-%uhWh)(!*gMi8r_q)OtCp7e7xw!F>76nq^x35N>{8=2PRTEos<;AV#X{4T`nS`r8UL1wF(obXgA3_d?~W) z=*9|LJ%_oIU(>h;`L9~Rgjg%UJI7PBu^(HGMToMI0F|tFv`jzjuW7V1w%a`#gt85C*V7>kw%W~>g*kT%@yHkPc5MBSUh2C znUVtHs=*tw=IgZv9*yG)&mXy8?`K^(lB^OagEmU2IuZ?BJq&I6V_+U~+w-Aa^TQ7%+ z@M}7t$!0MO6aBb_I4#DtBvU9FaKdOvFfK8VqUBZ;g|A0fFoUmFwT@0Cn!uCE5&{_2M-|5mhr#IiMwR2)2vo%%E?D(1v`ard5HCISyrom>K zj#$W32#{zknDsa{&Nhb9X+P`IM8y=kT86tV%wUekW7z@`Z8?+rpv4yDnMzit@$pt$ zBZ2GKW`bTh*(FsLOAMk*P4ZDe66RQBY8SW zM9cLo>8qr~_EtJf9Q-Op(G8K5uyz_ZW6iiZ$Z}FP>9dwupR?m{+w5Uu*;&R#!pnKH zt1f%BXe%Wen5Jlp?MwD_7aA0z$2YN=>oy{pA|t5ahd znp(QkVQQo?6P;){R)DtzM$DA*6--Pl1JlT4Ng8H<^7}<0W-4?eC1@6CqUb>xG?5TJq+g2U z(rhhOp&CRC4~zb`K3`+PjzFSVO=_gI_OQcRfl1MXrZt!(MQ_Bjl@5!V*L0vNfowKG z7qYo*scI`Y*mAWDvlRv7j7KUr95H>j8Hu|xL<8>RLS!>V)bvy(mkgrOPLqy$@UpIw z_Bl1SYFZ!H;;BSiPm{6iS`Q!dMSYnh#m6P9hjqHPHqFKP*K~jhD}=0(%rZrI%Fwoo zxtLJSkTlf@YVLtoW_Zq~Ab#pbF8%lTr^;t>g)NLs9h zyt#}sakoL&q#IgiuA{;5)rPJL`P%_=_GA-c1HLfbRvIo>K1raNhEa%SY*mBJ)CrY> z(4*N|WNIuC`hhX9m7h+xmJpgy(G$pUoi@J6OU-334ZvmMc4DV94Od$nx$z?45E z>Rk;tobh@nkV*J44_CLQOxd{2$C!h4bIDTlru_|?c91S(-l_Mlg$KM`-A6FFTIBz+ z_a<fXRc?iM+&zk>399RKhL?= z^Sy&58W;_ta!tnaT>T-NQ}Wj{zP>ctvI7X$ek-kfLoA| zz3=3sOu}imlT1X-iuZQq{r{yaA70sgCOFSO2Iuqp;JkeY?EY_n^YB%0zP$|2tCzs} z^QVFH|T)_q(@0O4Qp=hnT`>&l~pQyMmDYS}?~xg2ivQgmX} z=?U2u<&&fILS5dvcioJEhJBf*H0-Wa!{FK(0}Y!bPiffRcyg4%TlcP+G0>n@OH@cT z46dFr(6E{El!ncaY8YHKW1vB+mavR!7+g7HpkY_#`3?4X>)xpu0}TP}Picrre{!tD zTlXG6W1vB+me`bP7+f)9pkWWFme7o97+gMMpkZ&QmdJ!^7+f}Epkc?TmdJo=82lQn zg-{AOn?wioU@DCU&7!YG(Xj$CYRA0oajM4;MwN1=Sj>mb&hr~@@YcOcXAB~)crj^D zC$pUj9?a*GV%*aa`oIBFb3C$yeRiQ)>}Je9!e(inBD2u~Y6+VYmFzgh@NQ?VKv;4Z zS17r|G9N}OL43?Rydg&sJvq?Wt$UA~G0>n@OL#;z3@({5(6FUdOLRar3?4gUpkb@4 zmf(157(8aiK*I)DEy3y3Ft~WeK*OHb^BeN+*1d~n3xmSrjmjxm8Zkr_<=K9gOouSRGUO#&vXL2kyDi|&kP=^oqhPpjoM#*C&KPLe!+h$z?gYDaZ)e6pL%B|!*Nrho zuy#+a@+-Ta-F?gMt-IvzwL3rGxohW5JI?^#`>x);cl&R*->`klc5?el(|?%$5AeM= zGQ~}g-}>p+-)y~hYp@mFx*T}U``^afj6Gu*_zL*Z=3i~TYE#|}B98^0)S4R?10N%XCYzy2lqivCIZhpv8o^}VYv1{5!R9;_C)ZY{OCdU-G7X_NezA=p?$Y7S(7dEqI_ z`cKz?t`_=}^`EJQ-m`u$geHenI2axgsX@P$iWcIFZ*WX^n*LcW^aG}^sfE7Z^iOJ` z?=yW>E%d#nuc(FIVY*u_^gX68tA*Zf`bP)_0Ve_!8Rz^BZ!(?;_!i%Uth{>VRcfKH zTKPk@&_7)H1GUgUSb3#d=qp!V0il6brqrZsm0>QPXvg_%V4=NkzkK^o)k1%N`|WC> zzqkE1wb0+){u8y(mu>$qwa}Mtzf~>tCEI^IgLZ37wI7NEn~7LHAg6iHV&4J-`d>gO zOS9czQe+}yIa29R(&BsdZF;*7p^Q6DH3FVOqwDVQ>0YO?SOwI!q!yaruBj!E+penB zn%x%FY9+S?wOTXV6}4K^+hw&{Q`@{+t;uapt=7agt5$1#y9A*pHkbG5Kd2V^Uj3bF zp?By%pceWb{rlBIZ`Z#MLX$ObAnb|M+gU2yOm`ZKFABC=Tdz?IZEoGB7TVZ)wOVL> z3l7EI6S+!Ta47CZYXaFID?V_lqc5}RT7ID$8dr=@f&@;2ZvMjf>uRAtH$G7<^k+tw zTIjt-r&{QL8Xan(|6#O4s83Et0K0R0*v-DT%uTu-XN`I|d=#~0w)Iv|` zuT~3vy#6Y+&@1#;s)b&zKQ)8OeL9h#@`YeGl^O$=?Ed6J%R=6d{JUD{`;ecig}xX0 ziCX9#$USPI??Ha77J57KBel>!M}DXl`e(=w)I#5ld|xf}UC8&;Lf?seS1t4%$amC2 z{}lPQTIk!6Z$W6g<{6iQ!El0%=Le%kJGaoXOfNOT+lG81^d%;E+c+8eJ0^JBI2rn4 z6TEGl4E=4>d({?e-*kss=*0A%>G^-%Z7Zf5k=xgOAaUXIfU>~zFPmhhN22zw)t%Dt z)cEA*u(w>%ZPs{rxZXOA!Fl+sz4M$$q8^-qnKeA;R7$Spsrv)}2$kdTsYMJspJ%8n z7MDX&c5rF^9V(5d&YULgjRuys^RlH|X~eNsZ~ zJvNB*1OBA2mOgqDsGftM&ksm{8210lKIgD2f!L4jbMb~~DLdGRuPs%*r^TK(HF8mv zJUJd!{h~(#yN&yNwG|0k2ig05ZV@bQ<2l04B_pTZ<8C^|WOIXF+?#Q5dDbyzJUI~m zEAFt0^}d&mSsjHMh$vNyMN_d>gGjmuu`~Vvjs=oAH`mQ~$w6{3Y(_jP`y41b=yTw{ zn3~@gHN40BrSFUObM`s=gYu}bhBr^s`=W+-We@!OqV23cszgFRZ)b}w7WCZrMJiP7 zl6)DhCPrxv99pxZ(KEay*S2%^uqVOd@r0A?GLbIhYToZY8Z89E@v>k+K|Jw6Rs{E1 zXC=^Z1+uo1(>k^!9az{>x7WtCLBNjNeW_?N9N+|7DVMGB`Gh^+vkaS~M0?P2HB#12 zIPR{*adpS}r$wLj(d4|3Y8cyp={{;ZXCJj)=%ao)n<}B9S|;h2DfWTiNAdr{K8iUp z#Z%R>_fZ~gCKJ{$l}aW&!$_y=trs{UQ1yD^R=U^i$z_ZR;r?u66hQBH9}S&O`$C=5 zG6?uA4}9PZi?uaCT`|^{WwND;2m=1)8e*fJa9NvUN4?*$*Q?oKGi;;#7AA2fKnQU= zlNp~5hjDAYk_~jM;c&_~P&Ic|imsghUw!LJcID0$-6wYM0x|zaAj;pbfw+Dj0g?P_ zAa>tFLA1Wxw_mjVv~6sA&GdQGADf&IcdZq5Cy4!TJj?i7a@~xFS^%nj5+Q-&ju~uJuD%cb7b^Uwvd-~$) z-vP=8pX=9CI`C>)_OOFdvPnfMjZ7d1B9EpQ9`+b^KECsDwcz!gkL`R+EqHC`qdOl} z3)Ta}enc&Jb?3u7A3nXF+Ei1(oe%AN2x2?=z#x<1%bmhFMSE!?xcDj##32060SR}W zX!SGMq<>h5=Gdg4TQD2~D*f|n!5bh-GH4Df;V>lYUi&Gz#zPNlN!^VPBJ6qJ+-E=VCGHZcZ~BoISGsyzis@sTFo2a z9M8AZf(^!hGya=e@H*)E-&6};GybdbU)6&3#%~zEp%%Pq{1@ZD9CVYDOlSPM@#_bQ zPip#Wrn^jcsRipzpEiA3EqK-Rx2C^6pnMVy)2B?If{HtXY%k~Ol)HhT&y$e+h5AA> zLO{T}xLWWAh-nv73pRkTcTu(Abr2aZq87Xcg6M_Sg7qNMUPvui3C0&Z7!*%39SGMK zI8c0&Qx2o?7UL~y!JEdhajX`M7)QpDTJVN(XdJ2q8;k?vKrMLP*f;jog4e*_zn)sK z9)#2Fss*naWutshfhSf<$JjYg%y{#;dZp7uet;4QtKTVF16q_5Et*AYQcICI`187!Af+!KRsysC$(n~fA8%FiVOJx zAMksH+Hlw%kIKG9?=6Ov*RQ-@EqHz9bt|t^3tn6Kqm@5W3)Zi^cICBd!K*8;S$WNY zF-|Jr%55vR9VpJ%`~85YQYT_uuFsZ}wS|Geu=T>N7peuXZ~fNRZ>a^ZZM|UY1!}?i zt>L?f1|A&onVf}yKl>EZ_|L~xpef_WO z|2MC^du8{{yXl?(+eef z=OJe{zOeD^jq40|8J=#qa{ZI*1Mmj_uhu$ikJW!zU)Mis_0Cmc^`W{ubga$6o7 zEEYLeG?a*Tgmxe~kOM``$CYg)8t~w`W}M`6?QX0=U^uz|sHs|m3bpz8(9;+eO0jAt znvU6rr9q*V%L*cJ+&Uo40ohVBdrG#NyPs;3`;VNeU00#j9T`~pVk1+N_+qKfyW+s7 zkK5bu+8iPXkTz&jHj6cvxA+=fl-z&BRPEa3Nk5xl*nWiZL|RmJ)b$e!&ty*#Wq(;N zp;j?fb+wY2SgjBxQKshrzCSbNFp07$yV)7Z3!_Ac+<*8~tzL!NQVV#7MPuc-ecW+% zaH`NHigD2yGUIg& zbE2a}hBvoDK zY5R(~l$n*eQn@KKdsG*50mn{mqL9loeUBireY!1^`-&Ub^Y%f;nA&3J|82TG{ zv>vtFyv?4~A}|SCnHok5gJ2K0_o=?aP3|L8wck>qR)cF-a(`p0_TN;f)!?s{+&4_s zep7{74X$0u{q?EZe?8f^=P3=oUCI5msoHO-P^-aVE4i zrjnCqOx1oxg<1_>P|3;Dr)uw3p;m)yQ*v_4RPC2lsMX-Fl$?yGYX4D%S`Crv$;oJ{ z_Dd?%Y6wkFPKHypUp(1e_4S)TOg|0ttojp_DCSS7^Ys$nIeD(Ns;f^v(S3>X-8=nMrqOJeEhy%~ zP}EWcXB-?TYPMRPXHC^n^x2nAs=F(jkmIJpQHwi%NRjREOqV~IqQb59VdcB+Q_b+Q ziMU`zgO>T<*;XX^4hX~{2AYK?p9m0RDYRe^gThYu?dkWo&&AQPAoT3(CxXK9DfiX% zx|Wy%kfR{V-DYQ0?2EEk5+$)aI(c5Tt}tacm2dv-=i+N$MpLND;^)94@aIN#Xg#$EfCc)ugEKJZnVKuug(J&l-_V zWI7#@?3BXP@e0*UFPl6~VS4uLLw)>*c{82PK2GE~9g(bWQaIv9)f_LK+_X3%X;{WV zif}|ak)py4n|67OSIzM9$&HE?&l-_5ED7gUoQ+7FuN{xX2`*Iix(AIBb0pgDl-{+3 zLiO6{xky@$jz~8wkThrso}rcGbVR!H-3rO8NYx~-pA;05XN^c2maCBDF(cB6MxBmG zmrX~|$6unF@2Lr`@I7lp(y$JOd=HICC-R(*NSB5byI%T3)jY47PzukpMkEajYsmAg z5$Qyx(-G-0-&L4ia-C|Xmre2t)3Zh-4QuszGo6k|Cvu#QNEeM2ju+jdn&YLD+~SC& zVcicY!V&32iVC+!1gGC`RL$_3Nmj9<6C`W{;@8CPF_lz6qY+*QNDN2xz4~A z2c~N#0 zP4XLjiur9;cYXs8EjSQU9JR?^@YteWY<4)lBQ`2mJn=HjVY3|P7e&JeFm(;2VNjd8 z9hv&t0n5`yR|YI@xI#$95a|Jgp}tTR=hS5k-vCBY@aO72i*pb~ zifuN!pbS9lmMeNP-%$#4i+T;f{wS*WIGM^8Ilj#FBt@Izj%aS3TdshGY%p~mT%7uA zJg(eRbwCncJ34Uy!XU%@5UlddOa!}#ii{*O3i-pjnLg8H6qDGd9A|tMNXlLC80^q* zQ~46pI8ap++1f$6!*dL)lx_+V8-1~>6lbcO<6B(;{KpE&9reRB%caJT0OAV8?#+%^ z5tU4?keY`#LEU$)2x_Abo`Fo;7iI&`0&xp!u{*3bAbzaVmuk2+J{@q5xCR@|_TzOn z<|WFZdM`5clL-*3Tn@9n1Sj~i`2BaD7IhH9SO{`%A9tpj&vvLzWjwN{vco*dTfcs??yj7v4&nW$WML=yyNw@8r@2)o2pB%Jj8}?+1NVWjtIVD_E3M~OFmTK= zmj$trF9`(IJ- zn081olFMRqI^g39k13<3w%hgLPI0^&-1Byuv7uI_+a253JIPS4Gz#O?*2FS+Tcw8G#|IOx((VkOwfG>?Y7eW7Q@n(Ow(ic zv&`s>75CycEZ!@fkwG*?lr9ZI8Iny#+;xjfkfMwr`!c0)s}$(T6;D!jtMe_U6n*CZ zk3{&?v-OJaY7J|(lb2-+_Zek2bJ%yaOKBd*&2w?r(dK`}sRz7YTn+DYm6B`mL44Lc z`q+i%VwbN~d%QOitkVq5m;L@$YB9#i)c7aPq!$;WiqzqmbXk<7>0I1-R-ZloPIiB9 zNLD+3PXB*VYoeYh=LVzGOt)UJ`&v@S5iC_Jj+8IxZaYyS9`&YzSY|}re;=J0BAdj6 z*-$PDG8j5ir~Sk78I&OH9alEMriEOI@~5jMDSXD45QE-Y&1DJsY*vQ{EyYD^ZFD9V z@DnLpxzXwZPa64w<`aCO3^_dRo4GoE0vX<-`&1$>rNwy{403l|M0ap>3>7_ zY4G#;`aGxdoSTu=n=XGYyRxDa&oS4)7hmV3l0i>87*55aF`s)#TZVnwLZD3E)e8lt zX_*PQ#+)sm|ncqfnqL@u}t@=JO9|i&P5*IH@GMT_K-8 z0(6e)I>3897;DLmDCN2jt)v1(uVlfyv8sERApJl#T#k{IVSMZ!M9CN$8@J?=B`%Z# zLZ7v!>TS;a-0&R&)tTm?OHHzh@c>&$FY9RwS5H>kRv9_2s zZ0E902Te7tOa`4|3k`s^*aR+^8x=C%3eD7;9mdi~*Mp)&WJfhu)#)1#soK$p@6FtLMqoeym>bH?g$D6*w`5k`1<< z_J@u!wGZ=3y);phh-A=`43PA&D|vcpyyOfLL&{~XdU8RRr&5u7A-7`cr_D`WIhUyy zN4h{En#d*-p;-O_Woq&~nK~HHM=8H6QL~kEV*;fme3;6ElQy6SoZYJ3c&JauQBPW; z1)<*`46~ImnXV67y?}E#=($>G$~R_OmDr#)ZPj2O5gb6u=PGb_a9SmhE+59 zRyImx$5fp?#?)?inCr=5do10owu|W`;SkI@vLKf#S#UcKla)X)R^qz0o;<851IP_? zQ*-BP)r2tkc`|!T|sHs5nZrLZLBJGmg;>J)795DSST3mymt)1qgfn7S}GHG3{oFK!u25b1O%l1zm% z4=7WguiGMFvu3Con)h?D*w|ey_;T&Eblh#x<8oOdv`sR5oy~@i5D6^CcY~r3PttOg zY}L82985MUJ1#3-g*WF7S}$I`hW4c^w&{N_O>Rc5DSH8nRib1qZk z3#N`FDPJN&=VmcH^lVI>R>&f2@D>>b=0z{$CnyHh2x!7}nRnv;MaAmw=Q0SaB?`uf!uu2xo^t2*7Mb??RoPNyu>KOpJ3PSGNvu3ys=6L0CIbQo@kmF6Oh8ny$dv$k?Q3_w2 z<`~A1a21L}!XucaIC}`&YdxXM%iSkTUq={*@S<=W9~tsFxVW6p*>eGl%h1;(zIcB# z44@ScazA9nF06xtOGo!27<$kc2M}nC&J#e5W2=VBF~;ZuHO1Lm8dhK$6<#*9AqN{Y z&mj&jXgT~i%Q<1ut?=Sf%UKp5E*Kn;yakFw@^)Bk4E}sfv2Bnr3L%FGobcij-C%))%}^W?#vtMA;m^kqMj_$T5ORp{8A$j#cxJ%wVK39gH?m)hSh~78i1cYhc+Q55-~GuZCI=_RB0Mc%(C51x?i4iLufa=aneOqB*`@H}a%N|DYZNtH0n7V1C=SbexgGQ@_o|`1yk~JuUM@D8 z-Qui^8CtMI^2@~zI%E@?g@c(NW0nRq3kxBK%u<|(kq5yB1{&%h z_d|xd37Yxf(lKU!8Z7St1eW)PgYq6zpN1m5m^`$7_yHv+|Bc@vOu82)?=;W#85f{;UmV~}tVUOJ0#6cP@=i_0tM$b>5b z2@_Bp5)MPce)#h-ghP<94?+$R4no3Scm(#gzfO>V+cRdxc!Bdh;F#D`m|Ma zbyfEn-5Yen-B0hnZnwYvmhGo+C%3OK{mArD)5}d&)00d)TX%20W9zwF)Yf&zdySto z-e&9=eZW(|H#YCs+~2%;)3UjSd=7aNShGiwOE-VgGeEk*c()yFv9|C*|yld@wYt-6x`g`@C)ZeD>=zYL@;5Sz9Sl!zV?mlMcTRR`v zdGStZ2j4Mle?b@4U9$3>l@A_F2WNi){oBv}<-zekv;L;lb1WGhM`oGzW#}U_zp5rQ zy9|9~@VQ|b`bf_&E<+#bcJ#T?_>qa+2D%KrsC(lH%g~GcX~Vn(ePn8}as4v%;u>t@ z^fL6~eRJcwW$49w*~V4N(2K2VFXC(F=_wPX14GW25Y7=E-2y;wVj?=C|x_B6w{ zm!TKy((tWi=*7A;{L2zFdZc##c^Udh?R;$+`bh13c^UeMy}z^!eZ<~hT!ubk@6Rto zAF<$Pm!XgJhEJW=-=I6ktYFdH&p5Yct7{Q#_~bJ5k-GVtW#}Vy^YLZqBX#qkW#}Vy z^MPgPBX#qhW#}V);a$tnM{4qI%g{$^@-0hha?#wwC7PW_YV!5V(2J@Kw=F{-smWI^ zLmwFfe{UK3NWH&o8Tv@Qzjzt?NWJebLm#R47cN5|srMHwLm#R4=Ps%DMRRwSXm%W_ z_h&CdAF20efNj=~cyO$Q{`**-+k*I!*K`}MW$4BBx?x|2UTm)$j&q}}j>TqTxMdmo zNU!WILm%mt4Q2m-WaYyvyPw#7`7XDM?%ccc@tv3L+`M!B_D{Dzy#3 z+A~q6Q(NELdLM{uNp4*RqKe;ce6BHRyacTN-?jOy&FJPu$k&j!BTq+y$RjrH-gxWA zaKpFp5U}!plc8&Ps=>7Wh4t61x7MGuj;wuV?Y1>(&AGOw|CIg@^nxDOudIGz_2sMF zDyq9z_i^3JbT{j+U->DZrRsz2U#{m@PbF<_IU%LJvKtHZF;AH8<-*Qnny~ldQavMO z$$H+}<2=KvnKtyX{mT@Y0@|VmOrVVU{V~SgiPjqSp`(ySvAV?g_-@i2^0zDDoUb!( z8hY6NuPHJ%TguPpQbku4ExK%&i+9w$)qxOfk6T#UP7xu0z*?~JxIN-Fbg}(Q6&cFU z`qMRMsF%aV#<1HEY(MlU+Z2xhJjK3ME5p zpe+mVhVM%|%R-hUt1%%IOf)KE#^SHAvY~_RU!utH#dd2%*2z%colL|la>Chjb&3vm zm1+g8Y0o%e;|Y{Piz9c%(8l&3tH{_Zac>0`OGAP7m#f2IEn9MB3uRl-5{qU+VKUFV z9ZX*ISVtj6<}r#4$~be5YS8LRyIO8;hzr4N8c$Q@P}dxZN?gZRW?gBi;R!`!ip<4| zOp|VVeIAOVVw}?=B{H<&AkC>7B^JwBPcxT?6fnCM_pwAaZtWm=JWv?4PSMJZosM+nw0Q0;U!(F%3bVmjaB z@xI*~%a81xA)oBJJXKbad6Xh!E_=onJ|dNZh=^)cn(kRI9+jTk#b3*AB(B|;(d*zB*!dAE|!!xfpts9Vg%23A{%4Aq?t zx34Di?Me^l3d4+8c39%>Zdn$nay~t^!NU}pRJYcZ^W|uw(Ett~<1LSem4sHxEsLIJ zj>zOt(hg3U7bHP!8d})?Llv1q*jA#vl~Jr~^?D^dCV4%zEZ%IjGHxOlC7dOvyD^p; zM5X66G_n1MC^8<;&{bljmgwrHqV@_cGqIvyu<-?P(8-3=1ADYwr`q1(Fe4ip*#52} zlO=LAAw;7^bEif)hY~9|d8U9R)9Dm06+;%#Hxe>y2@a}$Lmk`SQDg!QRK`QyHtwV1 zC5NZv@mDM$W`U58j1q*s=9Ap1n#<$PT8p$n!uGcnnXXObD7x!SnA_k40NTydJr`}x zCOzRH(@%$6eMv44t27g6)(kal-=xTlJZ1*ZjZ5hilZ@9}{`e@A^pfd@--+j<);Qho zSkvX0b<_d74yxGxmLkJeLw$}ci3yh^k4rAXneUA4qChfHQDDgdKTLLp96{!p(!d~M z`$k135p;z|tz;b^_r{}GfuU&$%e3i;w>%UHBFGlvXfFez97I6h7O?$IMaDH64y&O= zkB)FcxM(Z-vh7T>>XM?Fn3D>5xIvl~aTwfu$(wvQ+>LAK)PWkAFaB3>#E^2weU zDsf4w%|+}sSJdUT%Mw#Cqv$A)Dl!|249R!XUTfJGACJ5}2O8^itX{#^i$^=La6M@Y zwUUm!n7~I|p`yqb6d9)2C);#EsO54apS$3u3jSo;Zi!)IwBF(AcEvdkS*^|Fpq^D^ z)~DPoZpo5o9gRdk(qfBvG{JcXju`Du<2AMmc6HNfaC()p*c`GVv!=+5vBuD!$@}~j z(3%4*&G*=G0+WF2gl3{`%Qstfrp)zfSlvFA(JL~78I}AVl$CoeYMgS##wACh3{I$6 z^0i9Ox}6!i7>NnH`FyOeDegcrz7{IBTzE_Y>KeI8LBLFJCA} zW5t-rDl$4nCRXGVqp(P0gMybS6zz>nG0_J>TSn$ws@`x3C86xI7o<)HJh2BdD~e3A zS8aJ({e%~1JINu@BZlr&Wf$D5mX46e|y-Il|&roE@N~Y3u)Kgg6=k8bra);{@U7|-jg?6Hqx6;jY)yYQE zXxJT6WS*|bu)(}H?afF52F(->~RCM`uqL);ld0h=@x84W>Y3^X=rC^8x% z#uzAUQdeX&go82Uv5BO}Xo&S<$YGP3BBLRYiy@0ms)~$;C@lsOn}~{xh7c@<3^oxI z84Z5F4QXsrQDig(PcfvhNm-H65E;dg#3sBVqao~xA%RUeMMgtR6GI%Eu!@X^fF*_) zHYq7G8lsUHqS%B{WHdN6Hbk(=&5DeMI3k8HHYqAH8iIrLMbvD;$FzZ<@7{@Te_%Uh`npLrnYNz3wQGEx5!-yt<`a>(AiuHk>5Zb{ z2Zk3JE?s};`cv2LTJ!4PsgJI{Z#AL&ny$U_1E6H-=h?df}0DKs!ZLq&ME{Wz;JLk>~Eq7R9?-a=L zjFP2_5OFZe`?juVxiMGw9+tR%? z_~P?lEwcq~?{bBXQg{_n=pdchUL7qH3{dRL4vMWJES;KSAKBEPW%2^}{MvcugP;9i z9mLklQ7NWaB1$77>IDwb?CoNw!j|kKTaPIO8dRe-wzNH*T=ZA)>fWV*&a@4xFwZd^ z{FE({2$qGq1J8Lp-BI0~&X}Y1v={h{zCOffnZdd^ zx)JMC#+>k3$bPwkx}D>i55vskracb)+N7c)5lOR-*1(?^BJohRDePSWvQ)aWhAak?EX-|L)NN$kJVsGSh};ib5?E_ zM#-o>pAWVqCnNX$V=hHEyeyNZc`PZ4T&qvm#=+ni=k^{0GMjcE^`?Z~hqII6p%_o3 z{X}{|1sb-nBV4hDnSeJ^ z*fW7Fr!7cDU`d$ep%$cJDO%mz0$EL4kcwcFFsrj#kcKrZXhBA$%9I(fiWrnIx8-Bv z;@j#|W%cW0Zf{eeqqHCuu`3}R*n%`Hsatyp$Y$DtR78h_*&Ne?G^~_`y$z7%v<0b% z90{{L)PgjG(5UViKvvThq#~Lm%<8Nbq#+K5vi@Jw{cOeb0^}F#&jg7JpZ|9(@VsYC zZkRe~eG!N|pyBb<{c=Zq$U&>iI`t8HTp$AbaSmEFv}Z-J7SGC4{4X82K*M9YvmCC% z&RgfFagr1+zXI3er6xD^sXuiiUY9HPxkc~t!|oM_-LE#RhikJdr6TICoR=vK~#w5>>{Zsl>Txy_9i!PJ7Z0Ynrr*opIUUiwryt#uu;T z&rn`AFWNh~dfMS<>Y3=7`p`O%`>b?2;*NV|Rv34xwq`GYj)5;wHQraJMOPg0KVwpu zcR&q|=$Gz*)4D#+L$b~GpzMGe?&J^L4u~lO!14~LxX(Xt9nj*!T}rHtV_lR}zCp6; z7z!fZ^D%_A(RcCzR;)x>4m%y_^-ue<%|fl^spl}~{qBH-FH}3z^bl-8j-bqN6z9Bk z_vx|RNmxYOlHy~LN;uuJ3D#b;UrxuopYP`#pYFYC$uO{J*x}Ic(>?0ai{o=x*M|q6I>d zL~okQ(8<`y8zfyBW}ItSN2O?As)S3ZH+H}KU9;>t!*%>)w@k{k#}Z)dBgYv$&GMWz zJqR$)d^#FxBm&M_bbu!sj4jv8oDS9-{y;YxkGTBqyx*BGh0+#hwk?p6yidqF+$v*q zC`E_<|8=Xn74YxE=l?|uyz=S#wUs}A`pPQiK%Na+f^KzvMYrz6K(Ms0tUpV)rqit< zE330V@PFs^4^+ME&w0)7e}uA?6H9S20=WPICRlZ$$g_0oI^8;G6|qk=M=BPUQ74tpi?7kvGgQSdcf)(>U^` zdD@P=d7gG4Z<(i^$RE$sF8y0|VEDpO)4J%+omXwt3oyynUXwBY!$i zJCJwG(@x}_^Rx?j7fkC`Z7A~Y`2`I5vw7Nr{P{d>MQ)#`apXPov;(Wx>Y->f1mOjhP-bMv*_Qi{A5MmKmQ3wJ}^()kUQsTyZ(a;)G_t#;&N^Pe2Z$LDDm@>eje(>e7M1&Shn zJ->`0|9hUcAfK40t;pZZ(>U_UdD@QrpLyDWd}^L{B7ZwiyO2-A^z_zs*Zcy8d}f}u zAfKJ5t@_U?yl~`m^Pg)4B=V=_dd!Du-Uzw-v$XDlS2l7wzv=jLnOzS|q_?<)f{_`AzAzz=T zEy%yj(+=bt^RyHB*Lm87d=sX1U?_U&A)Ws=$6&~}=4lJ^?RnaYd}p4 zGNgo{+x?x_Zof%?$?7-PyX&6yhiv?O;}aXN*nZfCuwma=+xg7;8%$x{_jDi7?db|@ z*FgHE%=ocOgSgf4v1C|Ot9x5crMVhBu3UkeU$H~vQ3f=`O7&5FS^{$ zfnUtmc78{*#EWMV?gUn@)*B&#tl7qcW;H-%!-1MG?63|8U96b#lrNUE6;d90b6c~7 zNwdTjl<=@3kCvpGH`d7pq*~6C_LwVtEsv$l)g0qy+J1+H@k*W0#*8Y`*9&l_Y;6 zMV71hkZ4BD6+CKAxV`?Km=7^_v>xf@<*l1EOWZh?ho6WIq z5L^kIiXT*^iboP$^tc*A*KL|5@R@{$w)+w&L3=#8Mx^dy9qF!nDMN=r{E4J8Vv7Dlcd7mgb6D_IaL;a<2zFVjvmur@| zY{m*LmC=53?6TE8!BkEz;DvmoP8a%8K`KUVg+Wd#`s^%SHrF1UVF1bee^6i2!M6IYjl5BH0{jjgjBqANT-YwaEg5OHH(>V`= zqEstRCn}bBBAxOMM7rl|vyy|ZJ>n|O5?4YA+81D(gndL)W{<<;^aMDO9JJDWk4`oR zX1mZ22WUDE-WMBQs#yZI-H7@;;7F~Z587v}Y%++y?H{|ExUUrw12x=IYDgI_NYujJ zPJod{cHmT?LsD^8_f*XiPth##2I!oNwdTkHB0=%Ok&W*P;hwq?sy|LMxFLjt5YJ|HculKTlu_ZiO*@4`0Pxg(+qgT zVlQI>;imgU6lYvDYLL$PU0k0_l9gvU6s z7%mPhqg=k`NyjQyd$sTBCWI7Luew|VFXycJxa|6GYnIrbNz~i5um#J6flCk{)fy3) zXsc928;R9Rv20@CU?LbF%#OKn$nYc05YwP9SxLHa)M7YX>iOy zn#<0I!!c2V1RN7JNWj^vhWp3WvvO#-NBz^xRzq_;Xw6c+c&IikmW3RfHd}jLx>h!a z2$qSK%hpKOp0IW}=fxLkmU#4xRgAzAXe{UWPgdi@e7sU+V_gvM&t~zq z9CS`XAM&=D1nw-yJXStu=hJe8ar;U(;J=0n_tT~DppyvW&2WK8$swD6uv^p2>f%`* zW>>cw8%D|rTA(YvP+yWl!4A{!QD`krm7~FNV;IcTcrJo&U7}gy(wRiC=ip=q+6MbI zEKw#SCM!L3%mt(7LN%Tf$P$sy`zS}q(!no=^IZ)RaK5WS;)a>6S^+$dddgi(*B7Yz zE%k=nOG(LK#vF=LV$zwZ`lJ%o2uW1vAtB8rf80z$LreS$SUvuSwiV?66toc(#J}=} zm9;mnHP&pv5C4ZiY`v8J;@yw!?(6PdZLHqCo87%+<;y$&wDXpo_KtJ=7uz4*o@|rb zk2QVO^k!2_mohoFe!lgQtv%os@Uh0P8sBVefk=KoUwz}|M^#5Z`k`w_%RkuZ4mh zv&ZkCJrc(bgiO}wi`U(wR=;8E%YwZ?6k?V(H;9fhML#U#b_7x-X(|w9+|_WcfqKjF ze#PbJazoVZjmznX%~}Zs*shT8beR}5&P!m9n9J24W}C%y&gPNh&WO}0515t|t2UeQ z0`D+q`cw_C)v;j(3jWqXj=fBSEVvn4pf!vN2{}DziOsN>HrFv6wfKBZpJ2~81l;V0 zg1-rKG`myLcrPfAViDdIbjC1BG&ui}AjAtltbe;+(S#8-VFXm}8dnmZWk( z!n-^n6kR+oIif?k9}jgHbD=p%m1JkLir14K8RkehK$Fv*k`tqTOJr@~fva4U^2IX4 z4U$aQ+$U`*&Yj^#iC83#4jHMJoe5@PhKYDat`oo~Tv^QG65AUFtY&|b>&T%}jE&kG zA+M!UrkGqC8g(sqAQ-Nv1=O8&)vW#&MY{{FirePqQM%bm)N&EGSa*e!F>elo8R}np zAQ)ikZ4pnh-M&}0mhqCgU#iBvg`7Q^%h{d0yTt_R96ywyQT1jEU3> zO)4JDCml%-cw9lmm@1LsadU2DuR8p;1FCmJ!Eh&-wPAkQY-!51P-)1u>vhg*&pYki zV409IqQ4}jMUrsA7H9Z3C>S8EW(VUT83C`iQpPOmk`N@;(`HpFZGgixo2(MWJ)B2HhE0mTir8qaUsN>v?V@00(dBLJn)%dNFGgxO2-K3;_CPLBUj)axn>erG!UI{#IaM z@l=7IP`5wa?>mZUyAu;b!=5uBS7CQt2X0NLV^xY~*=9WG=y}6jC|9l3QZ zbx_E%VGC7m#G4#$tIeqD?>rF1I@L_nB_^tVn>jV`)T}Yw({P)Gv}o~{d%;f8E2AmS zk$}~KIH6!7<%m>+gI+2|*uB;^UGG%MJl!OcagUX)k>Os%N+rgbP91i~brTefXZoXh z72|ATASDdOts$55d#se4sbE63LX<>_!>jgII1M|8@u^TS;BeI#H;((IQ%i4?D0P*PaXo34!9m{D2HQ#zbe7X_fnF;BKHC%>?9jy&CrN z#a=N@=WAnVoV8CuL0_lAfca7&>WX4?l*kYHeA3O+TqzMDo1;jxc;wM5BWznLh9J0oH&x(D4vAQ6dFg5YsT#2pI~Hp`%!guP?)i7-cRW`u{V-q@Hh zV-YXIb2iy&^K>KQRx%3Kh*mmRYIVtBE(RL_@-ryti4#t5KyZe}T!kHV1H;}p-7VXj zxJbn^*+FF7_GPTNwIo5R##^DFn{UM8*%n5Xh8+fXiY2+%A2jn^Q!JWCIOTSA%|pCy zC2(j}23-3vb$g|+oQd4xe>}WDOcLMpUzC8O3U; zhE=we!d2%VqPJb6eeLG7097L6Uk zXSa^7#m{*XL|W`SoCKZ|JEK(3SxT{OV#ES>#C+A3mEE>ZIh2ImstPm2dL4_D3+0(q z-Vt{aV9H`{(7r^+C4}r1A63sJJ1i{^q$WLMs(S_$M6>Qdg0{N6nS8X>%5&yni=}N= zYcOfG3ch5PN!4Y>+8H{)R7RN|A^n-4Jf;aJ?LwWroS|H;Or9JR9TsO%9J!)n%I&Z6 zq2?fCFP1>u`kCMm3JzV3VQ$z@b1n18OjsP{lwjwRHdn}NA2`5h+!q{0G0BJRP;e~> z1^c!(#Ra@cSBM$=^TV27_V?5MG=+J@JeA6J`V3Y}^YLa04&;U#pkS}xy{FIkSwG`UjUAn2Jr*M4EjT(FpKu`POcn`iYRoyrESJhrEHx&hbO`s3 zqFst;aTdAFhi$wl9gN3TDA=Xip3<<0`yGTkcdJoNzoOb z1GczzE1WujyCYsN124^uP9krKw(WAw-|o=vkW&aU9xTs+Sl+y=n3JI3F3hkkF{J>V zbudwzzvmfQt4`P0J7|vR0#lcRz+0=|(r;Dyp$|4VWCsd1X-mRut5y2uu@yXY_18lQ zf~{hN&1z5LnOq#Lmj+HUJ~G4lUx&l7+{jnF4RAm9bpm2-kZ%Q?y-?KGbY%o^7QIx9 zwlIh618=Dg=A63cz#JPks^>}B%-)=tbhJF-ZYAh#dlCtzCWU<5Xqa!O1+Fn5JqPvw zF(@dF7@~^uY(Gn;LzrM0@{%pt$c(*mD{KMYgzH(c(5^+9#zFmm8Vc4Lp)^Nv$%=!j z(|ADf?ah^hcoyxq8fd=}=kS~~pptSEb}IeLprC*?xNs{f4~H~aaL_Wwgt>v>Zw-WK zrC+4S?HDDxg=#Q#a3lW`6s&lufYp)+i&UA77MN%t>_K0)gk+NqMkaFJ(DYp zprG-fI(UW=f`PJQR4m2lkQn#WF>|vPOQU=cIHHcCIcMGD7x0k}PF7cc0(0aV#RA{0 z4s)SUwQiwC-MWzQ`bsXJM6tPo5DaF-AUJ8r60nRLzXJsel{_$5QH=6QHcw~LI5mtG zx>>Hl3-YiLvSz3em$F1%9@qf39&;ebrQPnPImU*9!D^@-F(=VZ$!qmE%B_ko-^t>U zM!SyLY8}`)ww?(EX)aQcvjjhCbzS|oR1{GT^MWeD6OjNON^#>R%@6@gy8#6^ejf@_ zS@0;`8pw=-fn+|)h+K^IIm%8K6_ImfxkWN|vMCoFIT;EXEeC?ZK(>K1bU2*I$dt?3 z4BAtq18q{H+%PtD4>Q#++3urk;$VLKdk2Dc&MMH%D8h}=0ge@mZZa;6WIkOj`z%B& zFlJq{Ba7L<`IhM*w{aH~%$K6kejsFyC7QN|6Zn^MlFgodoXvno;4x1%UZkzTDCsqK z;ds39&rmQcG#wSbAdPyy#JJ8^q9LLZrX#qO$@C-r0WRe&?qDV186M0z5Ga@?+n_4~ zms4q5*^KwR^vFN-c@wQzp9x{EMlpqXyIEFjjUm-777C{RFMHnsr`VzFd;0EXEBd^>8+(7m`X}p@o|9SBQNqssP~tl$ zIj>T^x6EsD-zN5L4ScmVu!^f-g08P?%XKn#^i7%!%p`418YBol}SYK*0Q{{=mTTW!L#_-yc-dHORo{W!L#^ z-yh&$qJ&vdLplTZ`(0<%d30-yL`PLXV6}qc1FM*U-^;G~d_JVhrwh#>7QkX>q!@@* z5}KSZ2je8En`vE351sBeoeSOSPm?NiX7y6Yhw8@+JY9CpXSSuu(J()%A2V=w*>!%~ z_v52rKGe*>z|&>d`Ly)oBd42`K{$l2v+CTWt$sA=etZPQ2Ns=yx67{i(ET{G`f!*B z)omEKw_NNBB+9CAN}~$Zs&=E^QO(cZjc0Zn+W+5kJ;N2wJD8vK+*i9{`8M^hqXtgx zKhsHj6|1qv?Il%RVGkz)4Tm66BC-l=J>{^PGFm(ykNZrzMojP4RU>Y@TfW#)G}-bh zliE%g>?Xok3mRLx(Xu7wTq=_FC&JKQ2QHC8?;Kya^~3QtUd04F{;Xcb>gi0(Q*u=! zNqDNM&u7d5Pcoa1I>LT$CPa!YO1?%-np3ufEJwK~8MnH)ks>J$i!GLNx#@SRNWDLn zFPLJPvLE__3(F-*MT)OX=E61fyuqC@2ufC++blF@6{!druLdPLmB5xyh>|9|O)85S z@)dQp$h9FaMaaQVq#DVVDJOErEEz9(x;`_U&CO98w|;ZhGDW@=$%Z3E$}38nc+lb< zUF7q4loUMsD?|EFY}t}+Zv9NObHPQplzO7GdoiIDJ~GqK(at&e6X1NYm~!AUtADW? z-&T{vc@spbR+39((!8v!fd}_aRZ|*`Bo$#o?vRAxOV3o5x6|%bDrzYo-m^*9;?Q)W zk@u~vg@Zr3=#+o4{xcn)t0IH$^rcl1`I4LIaSW>BtRBby=T%YWaPuuz zk59ta*-aIBAzn~ALSfvN!nIB^<8?&2wn(vBBl$_w|DGz&R63{?GxSrkqvxaf^tB25 zqd?Hs)nCY~k&3e#het@gAqdBnwURTOc6lTDdbQ{h^Aim>e8B-DAx3xJ)nC(9#N!#A zHk~eJ8~kKIkgP=9mT;rN$E8ZKOPI{*9Hhv|$Bm|>+oovN^mbgGQ)PAe$*i#u2zqsH zJ6;lId0Im2HksVI3ST4d#={t0bXpbdpQ|E+?)0Tq5sl2Vs^Y9Z$NuM45iT8!L9xV% z=Y&a9Qi+EWwotufFy>NuQPD>P^kz+#V5t zNYfQORe+H-9WZ?Aq|x9BP72alvE7%5R;%J*Eb6R_YmK5?xYM(upkb*Mc~#O1?`9Ap zXqaZGO5^34Nvu&dp;cr|RSako;xwshXmy^NQP$81in^#glg&!_)r#E9ho)(AVM1B9 z397zqT`f!o<0^Sqvm#Y=S`}@dt0IH$^kr31AVW*58GfhXtSFex{ont*DvB~XgNv`W z$D(|p(Gj$JWn68M8`1g9(uzhTEJl4XN4XR>J5sg(Jyo1B0gAd6hZYUKHF(2dZSbf;Y~aa( za|e6_i#c04H*%_+BRTB;C;HFn_x3LWZ-E;?1>W-S!=A*>$9{-?pZyN|m+X3Pt@o&2 zjP)ezTvh;j26(UM*WK-qS@(DM+_z8tPu75xeeB}i*>|==p{V85L8q`ebSkgWt7bji zK@gX<9zNUjPKDZ{Hfv3+^E(Q*9^jf>UY|p2ae3t|Xg<`IDlTh1CX^esR%lA4)>v84 z@T`MiYZ+|{Eon8tg)PB?2K^lbg)Nn1by$5GliWgRSnys(2f^03E^>w51XuM=xq}7m z$2tfKT5{A7dXvUr^O^OmvpNd4#!~cX%nqBCuo;c4Gdl&1q5USr2s- zY%L?iH9Earp=RCHQLt4PttO+wW7X@R65rENuvHhW&_d9s zfr13 zft^*)y1AoZYtM0dNQJ?zbbC##n>q@%_8gzn=d*jY&}NtQ%Z`GrJ;&&E`5aD<)n{kj z*io>x=a5#FT&K4xoKDvD9R*u^j!WfG5IVidt!3TNQLwe=Sk*ARsKcsu*;v2^&9qEwUJVdAbU}%?gE)+<|bEWcOl;<_KU9Pn#El!_?b!|t%)(+>=`3RrdPP%=p zt2+v|b~w`K)w<1gJz-^C(^0Us!>RNNqs~j1^m5kvj)JWnPUE#G35C*QFte`gDA?NJ z^w8arQR~&IJuG++u>CmBZ|!jKUZ4eD!qt-`>xzzotsRcgINVAz>6GhPzvw8~+Wt0$ z(x>zq+&U-g@{WS79nPg9NDE;!n+&X` zq|a?M>R1mYB&{}Jx+(ltL#7%m$k}Sy&kR7ZnoQutP9}&pA}(m?iu@d?ESH=V_U{H zk8K*;ICkIIhOwK*){k8}wiccPoHn*{tUi_>i;o>Q<{7h&X~&k1Nym6&2aPQnTR660 zj5F3V`tj)dqgzL}jBXy?G`ey0zR?Y%H;t|zy>xW#=vkwujjkN6kLE|?qsNVUMy;dT z(WRr(QQqi5ql-otjxHGGjP{ItJo5g?){!kEn@2W{Y#h07WW&f!BkM;l9a%ea*2rli zD@W=h`H}d@aqtAfI-(s}IwBq6jT|(xXk_8Yf)UP0&+x~?@57UcEyJ6KHx2QI4jNiC zv~Xy_5ND`o@Z-Vv2e%Gx8QeU$X>jAL^c-)|8 z&^o9cTskNnjy3!SUYgm zz-a?32kHa)f%w321D*lvfOcT%fOLR2aL~Y_frSGL1~>yf(0}6l@H}J-XESFLXCvo6 z&IZm+ob{YbIcqs*aZcl`WggECoj)!CAXgNzcQVx%E5N8o*A!h-H!|Cb&xc~kB zt^Hg2H}`Mq-`Ia&|AzjX`q%eg+P}8{tp3ybSN7NY^U&|&as8ftYrnRCX}`3e*MCs| zqW*>b3;H?zJ>X;TKG+JjfX!eN*a+?e8^BG_^TMTIEjSCD23CSP$b&dI4tRhSXu(n- z1w3#NSOgY=1%Lx@dwh(&k8Q=aV4JZ`*hcI=Yy)-^wjR3_TZ^5AorbN%>R29&W5;10 z%!+BTrI-}sVFzK0u!YzHjDz*CKW4wr-pby>-VE=R+&8>o_@?3Y!r9*3n&Kf#xXys6SC_fY*I&R1VJy>domJUhTo7fxK_pvvyZ(^@!U&>y~K8t-C zdnLQh&a>m}s;2wwxQA^aowBf>3U3&K~ys|f!9{($iJ;P(iB2Y!d}74Qnem%+;jUji>7+zd7& zd=b2e@CEP!!so&B2%iJbA$%4*i}1JLw+Md&euHom*o5#I@C?GI!P5wz0#6})57uYa1X+}!QBY&0(T+26WodL4sZv;+rjM!H-HTYZv(d>{5AMB!e4=3A-ombitrY2 z3&NYh%?N)9eu?lVa1+8C!Ho!S05>4K9$b&`I&dArYr(Y$uL0K}yc%4Ma6MR$@G5W> z!Yjd*2(JKFAiNx0j_?=Y7YKh2eva@ma2di&!KDZ<0hb{Bf8hTiyck@J@FH*#!gXLB z!VAHL2!95ChHx!di|_()0m7expCUXToR9E4a2~>Q!MO;}0p}n*8=Q^sEN~XWGr^e% z*MKz$r@$1#Gr$=LSA*3Ee*%7j@N{rG!qdQM2u}s4B0L40g79Q;GQuB&A0s>ooP_X1 za3aE0U=_lZU?sv8UH*LA*_Nb!V0J$ zEQ2z_5-1@of+E5KC?L#(Ji;8vAk{0M&peuVH?a4fClW zJP6&ujgSN+LKko$bOI+r2XG*?13N++upzVpD?$sfAT$FrLK83{Gy)?+127=e13f|= z&>_?UEkX^@AXEc2LIMy7RX~MM36uyGK!H#Wma3sPbz!3-! z2Ztj(3>=0~24o1OK#EWTBnZVoj8Ftb2!%k15C=Fy0T3YM13p3?;31p@lL!w5ha%(x zF2X~=AqXeH1j2*C!3ciCUKKMSuC144{#b7bQ zMPL!a{lI<*_XYbR{2uro!ta9bBHRb;gK%%KH^RNZUKIB42|pBm2Yx8r1AZvn9eya> z4Sp!x6@Dn(1%4=806!Ft!4HL_@I&DU{7^UyKNJqZ4~2v9L*W4YP{@HF3aK9o0sK&i z!4HLO_@S_m`aq;U6hi0o2z&Z_C?E`iLsC~e$0JRU;1)%nU>t578U<{!40iyu54;TTceZVk4?E{7YY9BBNQ2T%Z zfZ7MHno;{u&4PFUfZ7LO0JRTb1JpjC51{q|y#TcjU;)%Vpa-D#!TyV(_Q5{GQ2RhX zX{dd$PchU!*uOEv7enoXy@R3l!QRGD`@r1- zY9H(^47CsTCk(X@_9lkf2YUm1gQ^$ob?kM7uVJqt{3G^9gj=vJ2w%lsMfeBo4+wvc z{T|`(u-_qk1$zbI%h<~ZU&3BOxEb4w@I~xJgfCz(AbcKs9^rG?a|oZso<;au?6(Mi zgZ&2KCTtVJXRv1wK8-z%@G0ymgim5mB76dS0^#G>;|L$a9z(bh+lcT{>`{b|V2>bt z7<(AuL)b$IAH*I+_yG0*!uzrN5#ER0hwxtPUWE5x_aM9*yBpzM*j)(k#O_3R2X+U- z+p*gbZooDmybZez;jgh@Bm5QiD}=XVw<5d+yM?y@|4z^FDV#mom$1I`zp{S+W_iG{ zz_D#%f#>!5&W;B3$oOQYWguT7md<95#NfgWWl6*2mI`{F!7UqPO*F$^p25wd+02n< z-cF%t6?3@^ec@oq+;?dxfjLd*@z&E&N@{!4`B{j36JddcFkF3GqunZQN$`tCT;~Y~ z4TM{%Ris5cXH_l;#>>ID$t>3MN=~!8nF-`}+EP#*3CPtJzPK2l2$hr}W5n(VXjK}g zyOPRAWqd_VqOVvSWJ+eQn_NPxC+|uo^s))Ntx4$PRor3BE7IDqG_4_<1d(wDv^oV( zS?j`5kcWy6&68kQ;OMrnzzllV+#RJ`MU?!XR755t1*5RQbKaMn9|ipUx*>G|=4%fN z3@wnPZOsCWjX*=uuti9b$}HCrxV9?b>$I6zomUdYZO(ecmQjQ>(XgoD+G)Mx>rIwP zD?eZ3yRhx%}9EZh=;40Le^^4*t2SdFir-wrnF0*3T8Bca$esQG{Sm^+7wN= zp}#n}$Dt~P(jl=|nD>Ng*{<%b@KDjA-kp}3icorYW{5Sv_dL7RJMnCK$Kcu})epzN z^n}JVfpQ}sp}Oq!t>Nu&vr>e5V; zKs8VfP#5Dwh$GFXg=DE5C`Y#Y9g+_QvfGkMB|@of|1L(tfe=+X^iU#OO_ZtPpv1$G zd^ryPLruanZ^hBMq#_OY#1g7`cg_l#C&Z<^w$r@19xv22NsTN>gp%I4O)29^8Zt@M z=gNt#PIym~;MG+2DjqL+cY3%{dUHxyL>MgcgVIcl&_QpFns~b0v}*Biz*tY_@F*V7 zT0)ai3k)X{(it^jaoL2s8vH~vp76L#{JdD?Ys&LVxx$cBloh3PT+z+l9J=V#G+&(x zqiMduVxqM3H_gv_Kb6UFL(QTy*27E_Xc<1}8|vTCz(HqTiYM|^_ogOfibyz7%BP{p zJFW9eC{5+U+Y=~yv7e8khI4UTOg%~~s=rc9!vwOGbebAcwrP}LTXi8Nf)5-+Rp1U9 zHM{*IOuXgg`Ipi>0l)1;9~G8df_i$wQMRX2Q5&H!B&-3aw_tUt6NZU~JRp$ZLRH#D zigtdY*T+37i#244)+=g*(5EehQv|PUQr3uOfH3={bxBp{j!DZfJRDD*&Y7LLiHNn7 zuS#va*rYYz2oVZ?AW@HG)e)yt6gO5CF?Cn=|4{Q_(V^zi`+sZC@Bsrd>|U1jo85Q+ z-_IQ`oKo8sPV?&MrDfqn-431YsqN1I=3Y3>Na5Rs6CB9D#)XrJCzkMGv(0*w(&>)#l2x6@Fk#7tY%Wz! zYD@Tnyrf&Cn@k3)X3Fob*;v%_Dm#CD3p%eR3vR+!5STJ`g_~&Rs}+H-5=?0|3K*^} zP|IklPPru#ch>9PLb_;D6mm>T&65SY-jK})qEWlkQz%E8?#d*gsH(j3Uah-NZa4U6-kCJZR!Nas9SyRPY}%X}qEny3UcHt$3tU#!{G#Yo!L zNHje~TeFztdCZksP8zp}(#r0(EL72<=AE3FN>G}2x}P<_Zq2&YJl;&rn?;wXU$tll z?P!Xc@3udW_?ol>Wm$!15{VqO9bY0u^KLN`DCatPyTNC4-V-4IZMz9-68H-D=TZre z@{2QTXID=aEE=sL7jM+nChvsKYLfHy5ig;xWQuj6Je@97c=1f!7gQ5Fy+2o(3voi1 zhFSCgb&F!A3x`>G+sg^1V`7k_}55ssF z!bT#92j!V^49212>nCGnRd>hmRMBB)Kb4${Q=NTE#u}ej_j)azopQ7=+p1P9o7vfy zFgci{dKu&QVGLe4%)Sa{@ak~3Rq$83|8KoY(7OKpGSirZO045FHoHfSuJ-@45#QNq zOf2S68>v|}TOmu3316yV496#ge3v3P>E>mv(Tuw}kjjg#S$!qpULtqr-F6eMEmvbt)mW=fL*ePSX5Z#fZm ze$i1>M_d|9z*$I%Ty~$int*ZJy3=f`=uoo<*~@#hJsW%a?0t)R|H|GQy9%pd%KoSN z*Yzj*CH)w97F-5$;7Blpy#%f6Zx}v)SUnzrfScml9)%%lP4~(3+k!>9O%ix_u zZw>uw=%gXr*wbSdVY{$jWna$;arh&<_H7+Y4(&JS?*H%T&qwov`w#qm;QoO#2aX+> z;QWj8DChh!Y3~874_OcN{kreRgQt%kH99=l9q*j}0 zdQ<+KUlkSzVs?0!2Zp%fYDqjQsz}L-Ka_CoM#uAIB0}U}9f70^cRGW3L?H~7UAe3#pu^VF5`M9bpG^oV21!B{;47kWlU%9P+xa}5OlP*l zUB#rpAtBYyG_kTA?Fu=GdKK3-8Q+EAo6N=BW=ysgu!UG?kr{%@tRLzG&R&QQ)y2+Bs!tb zC-j=Q(O9YDm9SMAFP9WA zmWt|t#H2RK%YvG$5=a>|(wd|YOJ>puugxNZOT#^AC1)Z*D>)O1AJcUu4B%38RGPNc zqP29|DU1lYg5wknRxof%{-Amj_OYnJCzg|f#L$q8S%qW7p`s#sh?W^sQ$nu}`H?t}nd2Cd_oV9CyHa+M)ZP)@K| zbRG;OdYDS|{Fjz+yNq_Dq2f=Hf>_P&HW;`ezlNuC)+#V;U%)35rYl7`Kk8BnIn%Yv zK;lxyd0fI+;{VYS_G&%`9k}Jiq|s`P8tQ&ap=5%WiETEGOPebdYn8B~DXppbA+U>i7j!@0#bGi*lStFv%H^nNKuUJfm%SB;Ay0A%0sPzSTsbLh7T&1Hfvc-rdk93hm zR}5FnEnI7i2&(yMV^kl_Exeeq#6^tDaW^fYcLs_AlSrW>B#MYr!57sDhglvI^Kt>9 z)fQ6fiuRC2qe}Br;I(O1F*ps0H6{;H^TSw%qP)fzmk5+$n~BtTf=O#2of4NE26K|f znbs(E_Y%_@#X#ccyuJu)oDgMs5i+cb$6(MsF;D6++Y05hBga<;JYh@RsxM~L8jZYR zQMsgaF@#B;IG7QI;;CFSrj0bhFvN2$qb$kY26t2`GIB*p8P6bB1!C+Kj3t&cmN=A~;eOkgNgHoMaik6vL(XG3atwayP&OJxc4PmMeIxq>wt>A@-&=jZ>N}~=*0*2pUwTjPb$8j0bip%Of7lw9%EC&% z%^5ePasrLqS~RNjLbF0BviOBo6*uij6Di@|qqIahYJwpvy|n_)54yb8vdNX^YoHwz z*O9~xc}X%Mb>-rQYJ(rzi=O}020^@9@$)qng+-)RTCFlo(@=?PqLGwIDYOT(A%jt` zuS+5}-CndpF_HKHv0@_eJ}tqm)Z|%`=NDGO;*23&3-Uw;Uxgpcc}+Gww~&@roIFR| zgd4pJKVvNMAI1`&(h?4dtL)ZBqyb%#C)66EYPZ1FNXq#Qmsdi%%uYO2d4u&eVMl_1UFu#hLNa1mD__TyE!%ZvT`52zXJ>fvA zo~e3e<+#jXsN^hF+*#(yn7n4AJ)1W}M{-4qLKqE{8!^cc=7n6G z@uelq9Idoyju<$EvBbo*gwX3X zQ9*?b0j)yo3pL={(@{{E@=9gGSk{G;+-A_Gtkry`;Xg5!c$1b;kZz)Av*tsQY(1Ow zh#dN;ESfI)yk286Y>zaQb-93>@G2Cx5&E!8%|L=aN@OBIA0?_4DxU_=6`R$vCnF~X zgg>Du5IR3$Hq?VW4Npdt^o_Jm?{&s@du6&D-Ala8SmGsGLg+}R<2HEermy9=;kYW3 z&jJ^Q1&oHrb-yN?K|RNtLB!&L(Qs z43Sb@o8ktzO{KD4NX2$LjIo4_vBdONt!xoH#lob;Bh7{#B%U>JlTNEO6AhJlQlTc; z6zi%rzp7eG)%xjm3Bf>uUY9VDcn_6>C>0gDa?nt=8R9`-jaNt|ok|#Tn~Wr7#S-l4 z0=w86(%NxxZg6kL5_>V0SV&70VpWMpn=PtBX_r=S3A!5=i_R_=*y52uqrxqfaYAoY z*DNyE@IPsZOhX+db6kh0?6Y|)(xyLJ*NF(9E=6YY8AGyOGFHS!zSdh0^&U=3q>YsV z$*;sDzPcH=!hrIjaz+t0x(xNG4%*~voi2qs8II!qT;KF-A_fxl2{OSzV)_I*l{1uh zGJ^^RRZ@6!2Dv$-N@sCdU92%^>>*DXS}B_f3Xj5F>ZP@oNJ{K(Yn_l(vhq~2l(HC0 z)v|_kh;Ut@l&z`*aG@Q_nM65FX+fGUIRl9lV~Hevl97720RvM3Jza&}=w?J8z0Az~~cq$L=fV|@7a?f>%{ON1ynG7V8i6%-YurbfC{F@_|$pjLU$#?m>LB<5;xd{dk0S1ZAcN1_J6PV{F z=tl$?bgcP}=Dd6zKm>TYa$O*c=nZlr&sXxL5~a)-*GB}}oZO&|cx|?fR%hkoxoR{t z-%WrqCcr>t=ZxxHfqCu&eT)e((3oy&pcfHf5W<_uO`y3i6)a!JVoZR+Re){? z^e`qcueZ3CU$_DhU=Z7Uo}1vp<%|i;ZxdX20%HR6+yuuX0t~{v&vz3nV@zP4o1n>< zz&tlW0})`5=zKRpoiTxVZh{&jz~Ii-{N6HIeqj|6U|`KZzk%hKUsz#GfWdwGxm^P+ zzp%`h00X}|-4G}t0t{{m%&&8V~4&rOg=1Q^`(nC~XYF(xptO+fAc z_hIkRGq&&O^`l3OJTc-MejEB^?;qNK@YX@)z;6cv(0ly~&gj^g@Gbr!;7*{&Uc%z+ zkJzWM_Za#q^pbu|?>oJXUJmO5)&V`gYJUtc&wuGVZk`kTHo0$W;M*FQu7T`{eOS-2 ztH#>6U=g;uVCnX`MYH>h5sI@X_IAv$H406;3|pUTm!T+oBCBJDtwCtoW!U;$yA1K{ zi9H=N+%a5^K-Mn9*5}$~D9o-}(J{l;XgTdNY<;d>hJx&>Tn+yd|4@(_0Z0+vt zGHiXWU4~FCiXAg-y^`E6!`A28Whlv>$nKP(ur;AJ8P0gFO@=~Ac2%KMhJr2?f5vld zG890?&v(qQbtZ39LP6_u?J^W)SLHfpxZ}jyRxJdr&$Y`CPQclY8MY2t?Mm4CT)Pb6 z(3a_#Ve639F2mO6+GPlbwsgk~TZgQ68MZ#xE<-r9r8;KVI%KuWu=Tli8N#70*(t*r zAxGO}IODlC8S>%K*6fra-lgKtc&<%`xP;pOFYGy@XN(-ZVdS$BCydTEI+TP_@)i!n zIrnh(?yrOAz`@vw?BB8v>RaCXT(5w&rsoYP0Do4Eui=cr@PU)Io}K{?3&W1(u*bQU zjI5;P%PRS%GJ~s>!Gwk@AZmPFx~dX8-BCHeDD%pcymFkF_7?lOv**&{j5Dn?`fSQf zR8_H+^!0ka#n~g9m6>|-iI1r7CpWgK?HR$mR~@jXpW=ys!Lx5X?IAD`%&>7q4& zVtU{gOnb-O=DH$V_XSN!htX~l*Ml`*igd*jNqBET&DE3yDXtND?uneRs^w zv6o`^$Sc(Mqepkmao;uASkJ&T#{sI<$K{3^9`R=#sdU62%tU17hL$H+W|bmIqC#kE z1&>I8lWu`3tK>~nq@57WO_AdEpbLFp-!;Pn*05XJn=`JC646qShbL@F88lxvrF=#w zFHnKTb^=FK@8pGSVy@KSjyEj2z@eEyI8w*Y+5(rfd0fiy&NjA!a zx}|4wD2{hOM19|*YR9y7q=P-PuZ3y4Sk_+F}w49R;1RW=)psC4OBVD#~$a zK$=&$j8?UcA2bAoxt*$0KykcnG4;Ix@0#f$YgiQ1qrcE0Y~1ax6{5bV+;50f`KfTj zO6qOCvf6J>NOfFchR>Js8Wu-{o7ZjAp_zWF=k5a-#q`FvsP9|Xcg=TvO%KI)hf!2# zFL;bftH;xHa!uiE+2Pct1tqCLQ!x6SMrqdR3JbNue3LMHXO5zC_Ej{BUV9nE^SV`C z^E_Z`MN1Fp8~2(N4x(9u&S)G&-RvlN8$6X?>QAZ#xF#fWsJytsYYMAk7HMdn%Z5-Y zSO{gKS&_PI)<#`1V`|4ybWVyCx1Ya9egDGSHN!i?l67|WN7@n;Ao2Dg$K9cIOC{h)d5qD&->&6T7ke($zah&;~9yNR4B@g z4A<`98Z&lfs9=Y;;}ZN#q3j44e3p{LJnN`5CsQ;koui|eUhvm8OlO=0tvY0?PBBHJ z63rB2ViP%Co3o5cbMi%_(m4|h`HoMODZXe_qWQAN8Q5b@^WAQ3Hz!XtDxI?%L!Jjr zm0Eg09~1l0X`VZ*?dD{PMy0cFX2^8WRFPteMkTsNdzqNnG1MnCDxG}+Lyr4S6+R!8 z2I#dN0~@$$iqmVmIjfQ4cJ^|H3>Qu1TWYa?oPkjz!&*dZrSk?Tj^{1xTGtPl%6;A^ z>3tCxXJB+U+gfQ(_nc@{Is?s=XFS-3>5KzoI4Wf+raP>a7}%yw*XAsv(wuzJsC3Fk zs%B4Xx09R6cYG>C@!esq#K4ein(ua_(wscesB{9F-B+WU-V4F zPe&5tZ|~T=3pdFW-<@=SED}k1ZN2zMLpqIZvR-z$!s?2_&JR^})_PPJ5&N`xwW_8w z_}sQ2FJCw4Z9DD$INc}~0wIgrA}L2Q;Z(R;4_TVhj4GQel1jdU7fnkHPPey03WHXw zLLIM9g!467!CSG1Cxv9y5awkqLg>~~fZMD&gOX_Cctj=dCcY>tI_20Lo61o7cE+Bp zaR!}e{`5^an`86;d3|HBFZW*FtjWsErMP6}0V-E&RxwulH6UI_?PelvgrBq;uD?F-;L61k#Ou}ui z$eluGwkb{Xgdtttq_OhcPEA$UurH67gr`b9;DdPnX5_w9M8XKeZCr=!i0 zk4LJ*9}X9X-XF>i{&_Gtuyr8Hd4m(|-_rjh@H^ncHe)3FIkvrTQ=hr_$zDBcBTEg1 zzu})1XXr5KVt_NQ_XUeqS3MDQ7%Y)WgzM3y!+0exCe$udqG{pPl@VXmX!4s9q4B&- zCu~|BX{pT@t$2ty-=Pr+_;Qm%m#=ELVWTo7vn3rK3!yg6{g!&g8CvEHVqW+bx>Cfm zM7ofP3Un4St|`W?sbXE`EtN@8u>ga-=H#BTGSm$N4b8yhk60S|gqL6QDFZ&gIP36R zs?eaJ!6j{(tSlakM^$29x>)4##^;%Vnr2|FB=d@V)uI%Jiq^WiqOQwrvAVA6jFVY) zIh;-eO=eHh&5x8#bKXa)>Y9oLkk8iGjV_~J~_nQYp;Iayhz6cQ$`LZ7Z_OXXTKMmBWMN;tdE%3 z|IBTYvf_-xX$B0eikR5-bi?2=4N;6=PDihn9 zZWu^t1`I5A#u?bmbi+VQGhmSFI0KWIZWxGY1`MoNn7mt_+XKsrGlVn)1~w{8tVFtD zfYS^dT#HKJtVUxzxwHvg;PJW5oY)Lc5^ctW+F%b!@-@O8H<)#<922{bZWst?1`Mo8 zm{@dl!+=jSVBlSciKRw240tqyZ_FRmiZdo@1`KQ`m{?YIQwN9A3>duUnBOzn6=!g1 z1`JMq=l5`G#Tkdt3>X}FF|i|<`*;k!|L;4cXW)78A%t&#zO8|8Yv60Ifm4!G1$6oy zV9oDDvusJOQcfo#RBYvPu@ZqV8D@*GE0NB2`pv-dZ}!)+OcQ7cR6TqA+d0h&siW*t zBpXJBUed-8YKDO>B^AnL%aLpu*445lUn2gkkVhrfG5#&}+I5ciNlb{({sLGaqj=DN z^Oqt;70T`vWR6a~9g+!=dMZy9RJa;}U z;CIz^8u+UvRemnWcc<~mge{YgIb805PU0b?iHuWg${0m3kTzZksO1uGJUwAn2_mk# zs-Cw74c;uCEEXN`|2eHqJ!z|F0{L`BU7eT^232j|J5l8pO8D^1`{ZM%@|3onu3^ou zlg)0mO)^v4sFyRSLqw{u2U1md%q;I~W)I!|HEG07hHjs!X;g37_G0x*g>LVngY%@6 z4aKY-T-vB)ZTysqoN${|#?(Ybn|l`4Hf*^Sck)xpbX|)}Es|tDVwAwwCnb@p5yxHml0DXl zdYWz}PG;@&Ys;OUIwUy7p=Hck!eo#5#i9Y-vD`N1ZEt<`6%K7(1)G=ca7&4YCib?g zl%1IY1<{%aUXqSZXkAVHB%bj(RTFZVPgo?$ifOW`_n8vfh&waM&np^X@uc1|VV)>Q z8u3V7S8_Cjc`*!lqmoNZ${MbXl?oxDmM^3R2-q~+&%luRjx(?&EYDG$P@UIV3OU(i z&Q0d?ZmBw52~MPyq$8i#W^3|L#*!DLCJGXrR26bn<;4(LF;}yxqS+n7Eu<8WSp`X{ z*Qj$R+>WYI7AzT}QG=>Y1bUbsWk*BzWljUJcd1+EhPB?b@{Xd&!?HRh5!)ME%jwg)k)3863_W zdg%Ng4SPGae~Sry6WM|}I#H6@RTFOQWWwXYoncwDVD&jtnXot(O!F=Ja5^h9xV?^W zonPSB6pf5S7;I>rS*@GztB_4IKa#6Q+(}zrYYT?j8Sui{gD;TbaLhWTF;zx7wt_Xk zFGsr7v006F|L1j#$?|uWz2_{pvasuYO^dbdHEk9ma3M3_M!Vf8pQx?!2^B7LLvDzM zByoWx<46^Z`C8M|R7nbHqpA_e=rz#1tda`bjXZimuoH(TGO;{dG-a~Rh?We7>@Krd zW2|NEcvhV?S+dHgF`B6;DhY$v=uo8HnUc|(2nXFcTaoY821FW3#Ax=^JPno167o26 z6KS)`l##bzy76%VQoi_92`S&PtoeN}*sb#MzKN~lm!9C5j1IFX-*!vXZ${}pCqQi) z=G#!W8(qVxt;4JnT{IC1CxV7rT!WXA%7&7xI7Qxu&!*S$9Ii$*R7h6b8jHhRn4Hr< z9unDco3!nqvKTW2Ja(N}gNKt=ctORd_f&Xkd&rIJ6htDdwwo*Bx*=&Snqmo=Qck$y z7Sbcui%gA(7KTo<84|UoL@eUT69tlN1QjVu+}dHA#K(maIMJQvpDI$icBX;Ffx#QO8pWc!uu} zq!R&({PthsT85l1e2)scrv;_iX|ps&wd~>GLjFRx0sr zN5we&_Z;s`sOH`211&z@_AWrBm=6j~BCoIRE;=>vzRhGnFVKaR&0JWk%jg3pMMLdz z=#@?pee|+3CqV|0vaWFJabZOnlvxEt)jgpMsrb1`)oF zlbhQHbNRXn#snCctad}-U_^jHqVsD9yL{ab7!#Q1CO8NY_{QvTm#;ez5ny75%fL)= zZqvi%>keQ{U|ww>m#^EOF#!gqqTSTMI3mEr4tbuN;QNdT%ySbgVN772n_w{_z{C)H zo||A1V*>Ns1p6^2Fwae}FCxIeALaa7VJ~0zJ;ns)xe30@n7}+Y!9Iup6I1+o)`Gnm z6PV{F*o!fNd2WJ*hya5`=UWT*WK3Y5o8UW$0E3P-zjq;)uiFC=U~(0L!2$BzM-a=` z?ar70gOi7D2<*m~0E1J!ZV2p(2r#(|!r&aT8v?s9CNR%Uuz)dvd2WI+M1aBTM<&Nv z-88`{V*>Nq1eE>%Aw9SDj9oQm9{tDYMWf1*cSg=0kq*B)e9AC)=((X~LrVr98_W;x zIdIQFWMG(c3&+P{^{?-@fPaFEfeL#UI|q}ox3EuTAKLeP-|>Cl?|rq&1wlS{$3 zLjsZbwzU@@yIOJhJ0}nSh3j2?cj(PA_C>zCcTL=L%zlrE_ul(-at&M>z=RtLZa_#j zgLYfMYR=>db3HNtI;D*nlv$ z+tYhYmOS^|{hQ;T`rGyIl560U4-*b0b=9yln6SvwrDjm3mb!~gaU$*v7!&c3vlgja zi}kSD=?(;Ibizf!WJUbi;V-41x|jGM`S*7Ya>O@WRybnchd%kB?0g|{7r6#bGi{J+4zVr zaC$|5<>0`l!QE}Y3;+I5!!Ph3JVCC3Q#4H23w)@NQpf^(wifrMM6ner)2bFHGMv~>F;zg6o9&t}6e9Yx{X)ii? z(H;5B>A%dK_q|&ly8ch(1Kw44UZPuk-NU;yGUOUKrNV?gNv^}45|l!Awa_omSe<5z zGp=?Rot`ERon>e(1#aFfPlP-M8_oDfN1x*}U02wp{^`J(#%Is{v2(TQ_>a8&QxCiG zY9Z&MA6`zbfm0<+*iQ!H!Gbs;4H^n@LAn^J=QTPamopXhumzhHLs{O4Mf?SKSx6`R zAMJL|?j=4(z(q?YZiUZg)raC5+P-CqyhBv|#{JH9`VYv2?J6OQw05-(2@z%8zv zIa5prRE|Q-QB-(cZeued-EW1^3AyLnpFqiA0Spg zb^jjce`ue2{I?Pgxdu*c6k|&Qc8R#uVak-HUYW;GNNSprw1gilNL|peNyHM?r=%oL zQOwZ^f4cR|b*CM#eeC)V&U7EV{*h;HU-tYRcUf=F*Wc2e`*0cDL9T&Q7ED;ysMnJ^ zOT}vl$Mt@ZU*#$|a!Fa*=rBn=Iz_l<55>unOv29-biyAUQaIzJ8;-jwaNuVvM)c?W z{OAbx`$zn6<+1ngxtM%r*$ZcrYv5D_6V`@el8OTcxlo&f{+iobN$7%MtEUh)h($7^ zOerp<13oT~*Vxu8zPIu4+rNL{I^7JLMa0|L|VLJCFHde-S*B4L0_c|)MPww#%(9l<4 z;x~DjV#HgBNt-$x&sl337hM&pe?NQmDS@+H#bL1Y*t5qp*Prp?kq2D=r-M&=a^tJb zu?HgL8aO3TRUwe41@4d}YpELYA-~XQ2`g(7lUHsRrb6Wk&*Qb5#d(iFB$-y}{f^uJ zrP$#Yz5DnT&wPByTZ#v7S;*RVy!gtk$Nt##&iVg*>rQgb2uirg&(;ZtTSdg;d7CEW zjZ{KS3qM_VJ9CzT$eqh)0?}|K#m|VR6Mn|~!7lrsv@0jEG`6t$7Jj(5;5}^g+Vj_6 z8NHtls4S7sUNTwC! z)W7V%;CtV_`5pIDW5%cMdHRIHkuN0{-MMLW<$(hmIsLocL#`P@2{Z8PLf7&BKYjN| zV=%L1P4lgLewaA+5AQS=xQ18bw?1^>&yK(4Xu*Nxnn9E>17|68!uUsPHm+U!B2XkQ z`Q+lsr|;T#k0;)=UbrHV-Sfe>f0nw@M6MY?2{Z74LMMFG*0;gBr-x3RIC_`to(mS7 zwCkjKkJxu!jGTVU$d5$7YCcP@;h=;WxHh2^zUsHqCqKO61K_{Cu=x4Kx4&W?X8$C& z&s&B;i*;)Mrx!TLHT@`I1|Cc3gjfCejC0=k=ptS6Xa6}(=273U+Z#9T=eha$#tm0G zf4*|hTVWjolrRHFBy_@N_NBkL{jBE~+;?O8kn3L|Zarr0-}YW}MP~D53qQ?YULNly z*I+1N2L46pg!7dXRpVED_W14<<)>SEp13jk@XF@3FJH0R>i#fhy*d0kxrU7rX5co2 zPI&KUzyj_17q7n7e8i^UnVAm{W#e0P7qI?(+wj(1e^4U4Ofi|^TN6fHUC!M8=1^!ff1+H~u$cOloXP{It}SgiwRK-i@XPQAd} z^N-iPY+052Sr(AFR`R}j&S*AYfcwyv3a_V@L zFoSbmI^j2d`f}EB#_K=6=KE)it@&O0#RcEJZsg;=?vPt=d00t4p8E+owG1W9;PjGC z*qc81gbVKb^r{@3);60bagqtW~24`|~!i)F5;E?aX z=ofy*f9Wqe{+lm9Gr0H9|7*PWK$E7v-%m;lULdC$C}9RCO?1M?-DltJh=X?7{l^bI zcU_3v_upq;-!t<_>AaIdc@KZnmct(YpvR=<0nBt#YH+Ja^#_&b{u> zFOySclrV$cIi0ZK^^;yY{MD0|9`W?jf4unTty^E7IKB6^-7o*)UoT?&cAZ+RC#Om% zVFm|UbizmMAA0C8>!-bsTyyXdt{)wG*!%lmcgU`seV%$#vBy>gz+~i95hcvvkb+Lw z{`k)}{_PWQ;Oz?@x$Z9G@8zdIP`KQ)%<0hi&lNqaJ^Stbdx0(BDv<5_ z2@qkQVh>_J!EF7jd;Wkeg!}ue*x9j1$Icuh$G$iEM*l;j*NzrPWura4nUP0F){MAD zzB~N-@HNAQVd>C+haMT48glm8hV~hJZSZP%b|4w}Y~bO(Px~GmIAg%U77gsp`6FjN zC)X=v{k!K1)&s25S=PRVy|4CO$r1PeXO;s4DJ*;Mc*LJDR;pTY&d(24agkjd3psfe zSD0v6Yut*oS~doFl~UMHjZSZ#N8h6b!&yIHTl6}h_r|)MAG2$nv4|}d7Uy_`gYa7j zUA5*EWULi!b$Yil8k-jMD3#$#KrfWZL-BM`pDD;?fvQMv$@_AWw9{Xb3nKxsUlQVZ z>DnH>ZklQ^Q&M^BK2beaF4%%vjkw`5t2H)pCE*ri65)c{ZEB{gnu=wbYVUK~1SKJ8 zPvTO@HC(-hThw_ZzIf9vt?Sc8ZmEH%-iuTFkMM zMOu*+{StZ7lh@*QMbjdRsCgcqAFjV?s{Iq&1nsuCKw%Lo%NetwDmB-5DqBhI7R2fn zo!hGa|Lnbcyen0G_rG)7m+TuVDk@sgqoRj+a?hkiWs=EUlDT9uxy(c`WG0jQ{hj~{ z9IBujJT8qC+Mz0LZw6Gk9Mjzb z7b_%b5AJDET35G!X$lrSLwZ~(rdh&X9=VxV&d#)m1Q?|XAL=FKK`oDmN10@%uV~=P zUrxb7#SDqgc{DjE@Uke}qa7#?NLwt|Fs>TW^_yxiH#`cZPKj z&v0O+lN}3zkM~$Cr_?0U-wKgvr<<-3u~Kn5IHq?^LCn{+)lq-J9c?nSFTl53rBT%> zj=BoUVRR;=P_1D))p7Tcsl{h#U@oT?g)tJ79DX;(MDJGB_iP$RmZ$fvV)ff+UIXgM2edV8SPt4P|;7%_~!|*71Nk`la@C$}vQyV3qQXsg^R14!VAp^!Yt%fJqiTf-_l+ zX8XQEDkljP(m~|u*cje41uNnpS*sQ0rd>eg@dyuRT5e2^CF7B#)9cSW!}&3a)gnG( z+ST zm~^#*h06J6rzGY)SOX!cRHP_m{D>8Y!lgej<0#5mYQHR#CFc~K0e>`1LKid3AVam(!kPbQiwGLT)0*F7Da)!Z2{ z67i-*U&vsd&LGd(;+X9j(x;Twg0vBY$)+Hg%lIn@HIZX6^-#WA$# zA2{8jzQp@kH(B5=r+SQ#OOu<1rGiE)qoR%;j@9wirqA!KR^-*!yjV1WVAndQWQOPL= zrt|U6hcr+vGaXNc^3x)4K7u+Kp>(c&cno>HAK&v(Fs^QgBP#WVw-0S3{BGSo+yt3r2 zRebhrt6oUZLKpYfMk2;w+$c4@uC7f_hbkt@6c0X>?6~k!G~!HTvZ5%AqlI{=#EtrX zTCO#W(??g9_|&3^cpR=`tJFdhiGr6I6v}GChozkHUcAPIJ8r*+VAV)C-k-iI z*lb(*OKEq$T2HEy;7EqJc-%Ke`sFi^p)C(iLEh&LdJ?=E!IV)ifQ>?t04Iqa zWz_D_DZfgkygrY2*iB@nU0okfK_*m+*UAGWoN?i{9vO;fGGe|&Az6oF4+iSQXpomn z&b~7_jSk~CrXVe1jdH~vXms;=cO)ZZ0&N0!_1!{&?16U`wx++BPo$-8aq8r&&!2)} zXQVAx^Da-DP;h%C8IUUEIOQIAaDv9%30I}-D$D7PtvekYxT906bvz9zAcl%jy>tJr))KnW_?sbZ5M@+i*=8QSU(5m=7H)zGg(cuPBlPzY@tBL z-}3ahx}R>~9mPqH@@%wI!93oE++fF;*z#1m(|BC_?9^f)AXOtw$SX+oe$?qtjN}>@ zig*Gb6XTTbz}|ASi1El(Xra?I=O501wz>-!K?MMBFO#W=p*Gp%v1BtojM8EvKrjM3 z%zMK1hIeYw^nxkq4@W4xoC=^|liZxsAr@MWp2+)$gxBS^v)*3;WYKn>#KjWX9EPl-to7f>b;Sdn3Wn zBH8dqN7KPsnO?0_x4VGz_6C{e+KK8I?Qs!TlO*#I#O1?6m3}(tC`O20!k?NB&e}A2 zP+c8L^fss@msS&zfUQ{{N~DL1XITZU6{1YZ0jAF=m+3dA=W!!6?W!x?Z>Q@G7wh-4 zT~SJxh%W9VTU1^wx0Mm*VMN>sYB;!tXS#aB8PIO`6sguAZ_nguuiTafSRqx&dr5nx zoppf~nH3uB%~M2v8jpWF1?_A+?2-7K+F=z}#hrD>(5f30OCOa^CIz|S?i3hF3}cSi zne*t=z}%R_A_Fy=iX}o_k|+={XC9Me5s7!eoZ17G5rm5H!IS0nY@sEu?=s#Nz75mI4?XI==NnjT`yornbKN?nb!nb3ȕ@0cQ}+? z1)0g`JE0~glq;#+D00SCzc6|K|KgRaS9X7S_f|d1dPxTh%S|<}I7&S$`3{ z_y6d|%ho@+9$$NOt+@JEs|Cw9Eq(Lfn%`)CuIYWI=Ns=b(%@ZxV&(6^j~w@}ws+~O z%D;Adpa^r-35pq&WE241k3QEUZ=L!onFHa%b>5+;UY zRUuO5*%UKZq(gU?8FlZqz}_ifFM%f|yhzg-Tf2fI!>Uq5)NI-jjm3C(HA6W=a*zYl zH907UmbUlPC32m@-fMsz7qCOr?HxDO1(~?W%D#Xc2o;=Q6YpXwGU9M=1*@Wb#UDTu zu8g#_9e6#{wHte{2Buzq`O0y=hFIE^Rw+Vn&5~zs+_QU^O!@*2`xqZkEZrBa_Ell; z=cYq@jBhC}1j#!w1SxR2(CdbR)ktD=R@a^WL+S{Cq5awE&>rLKila?Uy>s!{IhAx}o@#c0nl9e`uJT|Rnk z+2TG-2cWiR2ev1Z$1z?&A8mWVW%d-XH<>(+@m_jqd+_A(s>zrFJI8oUy|kUV$-@Rr zO)j&?cyYb7sl~D6JZti>+p~KYPx=B5`xx)Km+p%;d5rc>PKFl5>oHz?AH4`{DR%|& z`pSuM5Ueth}V|`dy{xQ#{2iB z?ZNT-vdNeNJI8n(zqFmXczr1_HHp__yr5s&)Z%z`9EjHwlfHn%KE^x!rTg;Ji~UQc z2Yih8`$vDDzD%;}nlF-A*n9ExJRjqefTR0z$mCJmyAW8KM9ML~5jfh?f=Ial*qcPk zF+Lqw+8!J!KQkS{V|+=lv>iB7Cinm5m3Oaf_SSE(>;Um!>+gojO{4t^D2m~!oiyh5 z!=4m!I;@W1*S>1%QX zN7vn)pY$daT(s-x3MdDD;pqw}pdQLoR{=#UUb~ zwnV~%iDQ4y*K2j9oQ%fl;>n^th>UCATndktPxi)vda_vS)3L^|-VW9h0WznAiee%k zECh9mWuILLfB(=Yl?D4Z4fl(ijSNq1;;`&=b7*n1<4iY`+F=K(YaaIp=B5uV-I+u* zTsrChqlrT^-YJ%vAR1mgxkOGnvQ#N;w`0=uKm}j8rqGkvhvC3{1#BH&duWNsbNh7n z|CZV`>I~Kxk1t0@f>O4eZa6!b9XwHP2i)U|PjHJtH-QDLHcNP<;`VZZ0;gCps z@o_e;IIAY~S@*2-arx9#gFbM}yB~H>&F(?k1`rFcT9Z0;M^wVod1rTM$N!yIEf3<+ zFM!(^56cCwzz8F|4;e{OpR6iQXRV95PLg=cG4zhy=^m;wjYi|Bd{MV3XQ4F;xN|a> zB}8EqRoHMKLWaen zgOg9WN3roKMhUr4e!n|Q?VS)0C58#yKW=&4{)$}$8^?r%X3*bAR>2bCa<5$&W&N@* zkZ8HHL12F}7x(Iu8iHyL zOyXC*xUGVHLlp2nVgJ3xWL`g3aDbo7f`!RvZ~cULypySDa{rH*&Rg02-0oX;x!rSj z{%q%6JIc;Q+kdzHk?q0uCEJFr&u_hTE4uaU&2MhLdo#EB3hUonKWZIXUu`vRd;xsV z&u=_u{m<8LUsu;(x%R`gyVpi*uURv#-oJX&YHannmWM6xvE(f$&5xTuW*(bgYc`v{ zXnLDT0AKrm%lPZYg7IR*j|{(SxYBT`!LstDmA8Y=7;pTq^#tPS-u(Y^1d!7~v+?o^ z4G-!}q<&=XF$o`z2XGE&Nd`F`*hl|J5B1KWC{0FVD2fwNTH;G-d)rjgT^?4w`QL+x`TOo0(f2g3oLL{9(Q zK6<|nip)REX31C}5DBIuD02E|_t7utq4O@0!dXrvqEr-|9REK0c|FuO*I_gqV=$Jb zFwkLSAN`yjI`2(u`{<|i(0OkP zpus50h#^))PM_LGKdFb#dlQAm66pYsrP9dhSM8&}uZPaN1R7w`1i=u21ajK8kA6ZA zop(eEi;y&*1hvtT(--ffzo&=JdlMQ$IW&}(NC`Q8av!}%51n@j6eE(c0Kv1sC0@CY zeq0ZocSM?HqC}WQlVF%$v5)?)9y;$$OcKNmjd294k{B)>W<#V1 zeE7sZ`a62)yf+acDjAQ4(kU=ZFWE=$(nIIH2?rM)o{C497;^f>`{;-D(E0Pq;fWAI zpkWcj>xKL1hxE{SM~sG~IEC?CJb;|OU?2UU9y)(s#R!Wh)8SwYOr}4xkKU<=&U=#> zB#98sb1`soUbK(?wjMg~{=kzMJO$hW4ATqu(GTdLo_TL#B~%JTqf!8%=kKGxrH9VD zL>v!@X-o{ofJ?k!AAP?bI`4>5G#yXkaf$}B#Pj#j_vxYYE)fcfVk*HWaDYB!DU(ee|->Zkt`!I;|pg<&IVG23@+tIbpE`Od?cPq@;C#oug~5`e_aoqcf>fK3V@l7LP3Ym-ACV}ht4}9L4r+TP#nzR z$mwV8qqpmz?s;#bqVaGz#G(>NL!P;hzFQBScZmpC;V4mTCj|nKtrzLQu+ucWR(?jQ7f)^M%7Ukn4n1^@v(Rb>h^De1jo9ykG?|>J#b#rG?$2S6qB-UymAFw`P|C- zKdpaZ{g(_ktt;z}wSQat%Gx`PiM7(2Z_Tp$wbl2mHdbG~x@q|{%lmekmYyYOdB)Cb zRz7F`OY;ZKR~lYnrodW&@0vbp_>AdV6SuQvdZF?A#@{o(+3>pE+gAQ@x4et&uI_w& z=huxdG5l!zo7?Z-?r&q;&)j-s>qA>tZH2ea+x+XzyEm`fjBdWj`U52xv&X9LHrOnw z<#AzD)ZQeIya9EXRO5-mNWK-!73y8Fs;0eiIhaV37+#g7&EM0Aq+@_oYzRVDB+Tm( z=n4Xs53^N1Hs*?QsW={&!`K)f_|VF(RU<;ZeqCn9coReHT&|ICV@Mp&l-=WKKF-Ak zLuZ2l#j1*QhT3#%gpl{AxLz*lO@*6{R-sGF#inR;VW>Az3}j0_XSWv1I1)LfviY2S zLarl0If<7OO~Ta(G1U|T79Hk;ZC6R@s4&vd&e zD#|e*Gj0@Gny$K~tU{?gRVaRs_5dv-c!Mb|kr$BUF#taa3yi?0C^1K@!#r zG=lGnd7_x)TQy;gQtKaZ;&VvGk@yw3SrZ2-BnoZd)fvw`x*pO!e%nTIb8f zmJJzuWWMRN^$G^RMudaD;kf3p_=J561hYaaibVmfh^+%vekCBQgi~(+tZHVD9tfzn^OX9`@P^P0o9KCyIHUm z{CMo)P@5;y&EXw&RH0b3kYstpL|f0KSnTQ+&&p%ns3LJ6Df!onkpXj*Ai%ikHWn z%~NSbs{Y2na_N*Pr&9!#@&zc7h}q&%Iza_uLl#YkBLvu3CrXyE{-DdKf_UcKyvh+XXB5T_Gc-5w5bUixA+=R#wKadRriKbs-$~bh1tcj1$@JgRz$e zRYgP$zcnTBL6dSx;m!c}H&Y^567Ti$LBBs39m-^+6e+iZ$-akgI{3BEX@t|$;}yJD z^b0Kv>_by(HoHkhw1ZX1c3DZsg;qSw)@{*dgIT{)GTsYgtY6_=&8E|b^*v}92b=9s2xWYRMhrNymLI2@ReM&6yU}pZKaex| z&NvdPr5vqvB;4fGc9aTGZj&}v^1v3TW|?|fs7pk&WRoi{q~#y9Eo!4CFx-QTo@7+K|f!!nt~2)l!fWb37JJ>?puLjl@DHZ1SZh-$qG zE*H7}$z(f$lvO|1Q-~glH64j!TaL)Zaa4)+q9LLjvQDo!RmR5JSww9*!IJ47juw=D zGLgdi`Akrd#E^^Q#thZ0jtRurwVPDK2^p-wOteF>D43R1EMMjrdK4!bV|COL2A)Qv zmM!KHzTxs@P~+}b4S#rKCh~NH|H31%?^ZcmhLMU)(o;yuNj`}Aaugv|ed$<}>4ZGv zv_Foch@TmC9EF|dO$j(D&=GZv3_%( zP=+IKD3h~QIZW z!y4(zpi^!~+IFyTv@ry)I2}GmDA5dy(i*1O7-Vos4wmv!M)XDz!GzL>hyx;toUx*{a|<*=txW z8UYrK=jA#{qAG>uqN8x7my$7eI#}(s($V3N3l#-dfh@)A()tHAqUBD5(oce?IIbc7 zAQ5l#9eJp>%C;)i!0e5xr^=IJKVHjGE7xg6gDhmmp0M5Lw^1nu)SU^Bf?$dJkio@# zF&<7KWk2RB`fLbexn3jc!5SWRvGzbz28%%3a?6)iSUy$Esp)7@j1=;&o->P=GpyU# z(ui6X)Te5e$P_uO1cT|}*kMlzL?zBr;sZhR==XzVCt?o z8E#hT2qSfRWk;m~K9})0C1TuDvJOzQ1#`w6LZ#$rZ2!4N1O|Qc%>hANa;F;z?Ts%ReUCJpXJlw>CBUYp(;q}fOW z=iDxOkY`3jw~AItr>hwc3108e)z49#a1g6&7Mem9XU@tR~>_M z(Jk>kQYayT5++eqp-XkazJHWyZAzqim|L#96}7;Mp>d!yE_Zx#sNNnCDD57Qy{H4r zcb!tPU_~_|S@xA880xEZiv?R-Az4on3kEtx&RtBPxw06d6`8Ilcqg--(TId69CB0x z$!uW6=h6|bnD!1qLAq2@Nc50=j&ThfAsI;uL&R7Fldto56Y%8y|7%u0w6c5O?hU*2 z?m6H-{nnk!b}ra{6x^p*x9wX$-ns|erC$o_0o=d2w;8oQw0W-e3hQmw6C00h+_}-* zaIgOaR0p_b9a~>tdtmM6wfNe3s}HaK(ki>UX1UigvN+A(H{W5t-25EVH%-52N}0|v ze%*Mr(Qo*U;avvV@UoTf0pm;m`LBESCcm=syypR8W97n_#D-BK7b{ZLK`f2SZEo&0 znzm6bsLBBLm-JtEF3d8mPFD`nX^O{75=*vQv4!ljU^dk&Wx+>aTt84QgyOASud|SS zCd|q_#vqlF{iYG1*{!!&)I)TCWEpZo5|Q5^H0~{;Y6?7g}Wx! z6X}u^#!^X^$ag@oq4C_$9^SPbxNB;(%W%;sSKyQYD7BVu*5`f$>e~DxIJj&6>$cBy zZQ>;oktio~$y`(68$=~mPX*?F7G|;Qwl?{f{M5s!bVV6c(x}o*WBqb3k(%!kdcouq z%nK&lufjj$zit!m!nhY~sf`9#&!;L?7B7h zXVg?IH|UO9WWP5|?5 z19+v{Y=P>9(Pa>l)vZG!&+_GgEGKB6KKX;lv?Yg@HEwmD~0g<#H!ny75HZ) z9*xTdnWkz%EL$sKWN^veT>D1&XF7d}CMG0Ao zHwU9oY9Tv>*|0=2jY=6LD(PxXPWR*ah3o)k$Bj6T771_%ge5}3NLQv8vVE9kRVf=( zCGZJLJlLwXt3-Su+k;uD3o0v@g=it!m51$df?G5}`>*Z7?6@OFi&S*1iXz?bWyfQF zq1_J5rf`x{1)LxCdIdra;KRk^+=f}6X;*}3mB$FV(MCt9?qa(wn5_l)CXuB|*<7d> z35a=au&~cfm>r4|Gt731R2ofi@g8`wzL2fMY-3!g4-!>X9`?s{axfwnA8!L@c_quX zsZyeoYQ{@Rf~$8I+O5K@(n*EHd@__A=K3YJUqEvU*&58Yc~w$4tk0L|4oAs+d-3sB zU^YvR&~S(oa*pT}whq%=2hwGqhQ89gJpU<$Rvserr z2U1dK6azOn)pn4azha*~*A-zlU+L$hirV2hJP68DjgOeG3NXv1h{zDF#oHvUl&D-g zTspMR(~SJuJj`Y*bsAKC%Lj{9x{pi6%HnZWVU|qAt66rKh}5x8yIbXkiG_A^FxyNB zI9l%w5nhcHJ5{F}{X^}ZIY$|o z#p(&NF>I@ZI%I2QA=zAf4ShY#w!sJ|1MPg5Ah7^f4W;LEle2yDa+nQP*gjvE(PorV zdXZ59t1RsEWiZRi4IEF>;Q4=C?ScAu<@tJXXWM-p%pO^5?rb&gzZ|_&s(947p<6XHWf{mZa)a_`mfm*x9!{O;QjnP zTQ_eN!M*=^n~#IL|Jyf*;2r%%n=96btan*&FkfR9&35y;=@HXC;CueZ*B)QHf9>|Q z;aYU|cm+-khvc)M|Ej2bW6Y3`8V`NK~PcWu6BbG#{RUc70s{)P49)|;&*Yry(~jUR7( zW#czD-nb!d*f-YKA6dU={bu8e;i1J35e!$XT(r1__OD<#ZeOO|e^wjQrOO=$Xcv7d z`}lGbZ@jQn=1RR>zc=P+wpL&xd1B5hH^01OyDu%-?u$#dyMM`cU(njoWLWOtopDcY z*GDqg6mI@FKDT7M`}W%%d8J;rF4=BFYgZ^sgTbJj3bTbld(3s`?{H0-CHwcOCEI;+ z$#%cLWV=r++3xq2Ymuz>%lI^DXz>(wk z`Xztw^8I$jb`cB9HL))SO5;eQREo@NB^Fd_aEJEeaUfITe98ie*Hhg z|619)Vg27MR|E0%-~a6+@aEN5DY ze9du7C>|)Hc8of}XO*Klq-^PYQJN#4*iC>ZUxTqW%2d=5C?Dd0t3CO^Zy!-i2CAt( zS0;JVv43az7_ZOk^Z@+OI#71csNCd&d^hYB>wY5^1f)uY zs)eJ<$LQ99a;t?LLCRUIBj9aroClkDo~jf1gedmXu7*7yuH=Q79LLgmzE}?rds0bB zB?F^jCk=L-@`h8GE0`3Jwvwu`CEVSvNr?bHDIas7e9M92EBl8&DL43ro37fA#0|rV zW4PDRwSkw7ME8HoNIXVW?EhAg=si{>PK$*@}*LboXNs z$G96)W3|pW7~?`E{$z@YbgC_(3@U7bj!()JWxKt$+SxBzv{V5luu>>n&x| zfoJ9L$poIr=3{<4IxbRZGnE!&1s@~$hH}5jvm>W}l(L7bcC^Tyj0K$DX0FJw6@IJ= zRW>dA5^4=wE&?Zq4kGZ)mYc5JkHF^|p4wvT%SNE`$dEZVv}WvbfQAWM*UMvQvuf?Eu~M=NO~9_hEE9rwMcX+3|yI)xD-wfeR3)6-ZY*B z;#+%$r*;>DWdqUq-!c%7lQmxse0Z*Q>E!!>OuJVFlQ#Ir|AgwkKkdLI?zH#PXT{y5 zD(-(^+_`+dN&0(e3hKZZk23bw2DD6(c@Hb)U6MaRl71g9m9dl>In_yky1e$)SU|vx2+bpz(VvyIq;+aqq_4HZI$EC0Gma`Sn}Y<#qeozk<~Ozq(dj^RAg!|9JKG)%xls zt5(ZHmfy5=Edk3p=D#rC2`U1TU|qnYrt^*8GkwH#jfwrg{nq>GnZ~Sm?Am&X|)b4*f|`aKJJ0mqGb+ z($O1&0^o@-6B69#y!_b2%Z_~ zL1?3U96~D+LH8UrcfsDO$sxk+llnQOSp=Ir5AdiGu1K_B#bb6gCm=$-^SsBj<2VNK zH^(Nl6^3-jQ!3;Oh~P<<39?`tc^X(NsW72nK;i4oaFa@c6)*Kdsv5F>U$b!x;(Hp= zZr2M+w$u$YDX>v>e_Rc^lI3hGGzhdt#Z)jCLLEwgadD~8hF181J_bQ*HdMETf6Nh2B=wbc>*Y&nIL@<^so zvA0T0-N8CqQlJ)VJ6dfb-*-ehe8c+QV-t5AoA?clsGv4qfubTdG^y6T{b8jcwo~b{ zl&;!Pp&vysLh-cfXuU42UUY0io8jnV*wAJ;x=g2C!HhTFDOMedw`*_ltyaSYK49+Q zVi&B^@fRWt;*q1mU_;b;bqpePY$7=&oMblS9_u2uhPUrZ6w1*N z*gJC^Qg$83CbY5=^f7Fl(ro0je5MEL$`VYR%cmmg2;;J4=-ja4>5`lWix1+i=D;He zqO##THsL)s;n4`S=g9PGp>V~GG9#)~5l2<9_5mNakQ#W5l%t!7hxIv&PK9~a=Z{Tj zg+b_J*tqZ5HnhSyw3@b&&XT|1X%E>L>WTO$9+gHx6s-=r$y_DmkKl}#iZ+ApXRS_c zEKg_^1L#~e7o}`Nd%qM%JUv1T)rQ@Esu;HU@Db->yda;+juYi_j@r-)WF3caX}vmz z4W~vZg{bK2#2Zvu^0W}vUf|0(ll6~#A=K596TH-7DYP>d-G$xkv56~=O=v5-X&IC` zjkaAqKixfr5#s?77QK$r2qCx{sI3y0vt!lXVXHxp{TYvIHjY93P$MK4$qBx+=;KF~ zYDy&?ST-LeYDj=jdkg9IC>9EP;tn=YD43?%(lLl>wsZ_as~7;DEuo;e+o;7P9F0+b zD60;(u9QbX_n@n~0vXwX_1Ipz(Cr}Bk7>to4C3x%6I%94Cv1dw(2;_UU@}9cJ8GkY zArV`$G8$Hs86urb3Zul>FSfg=QL8a<9AcW69K*&mFF6K5Xh$x{h{`)$pil$N^Toap z63hNp9`g{XR;vr%T_rjUsLBzqm6Vm+C-46+T=~Gt?%TkZ_&4rsZohZ?Ia?ptx^(la zV6Fcbt&xrIZj3f|*MDt&XYGw^&szQ9>WeHNv5@9(m_^h7GzrGPGrr#N4~82I&jCO2 zAN*IkX?4{Ib`Moo2Nxc>q~nm6t|!2aUzYU=;~*d7C_kv-6|C05er2GH#JE!9@T6(U?| zkLvbD&}%nuG6G{Szg%7IENx8N@k_6i<$O(o6JkgS#VQEZOS((-ai$lNLCM_|rFPp@ z7v>Av3!W~5Dv?CVB`G%r!$pAal)z*bat{*zYQK7f((|3F)xKTHgWCkve9H(kX9mOgg zwN>c3bLpW_%KMxw+Rb;0Vm=jf$I$0(YDtgp( zY0yrDV*@XpuZgYTa42*NdshLAllZMIZ4rv!1fG=eB28y(?Fx`!5JZ1#Z+X(;ob^X z1&coY0W{&tNY`%eje)UA{8pAWwm5$03wNC8dP+G_>IOMm3&%KnHzoD_k}VMpgooLF zEZpo@JNaQ*$`Xx=vNxLa1{`{M>E2A^cNDN?-N~^WLJ9Pq+P7k zZ@Xf7#x(-(uSV55JaP@+A@O?xJTK_y;&O@eS36_bk0Yt}*jp;bvvjGb#0Tmy z+8*Zccq%IozyJ@X16(}X*jYPU=}nNXxo*O(51`eb`?=DJ685F!*RH5}er8xZz?Ng!anZdr9mtM-orDmMD={?0M;J0& zsY>ClGhPo>3VStRaS|}f(iWkBv2itjhjjRR&2g~i!h&*%usK5m2UYE3-gqjHI@*58 zAGCLi;JR0xj4ZGuFKy|pfT;?dQYPo8DUNOzTSRowAu1C)n)3q z!wu%a3NSXg?q!!Yw)nc|oS&9c>@ZLtre&rq&@sk4ln3cbGEpp*bJ75}5glj67LYv^ zw9hw`z4D|t;LxvFx;NAKMI!?jC_pJxs%l2AseX1u+R2$;tbF z)c7ka=2wH2{9iNOZmO8PCWG+-U(@!CGA!I)sVw~pJSqwS@G5;t;GxVHAb`ESIHSHX|5SrhhWeP6(R*j*q`_A*AE3 zZ&(QFxa-vmAsu(UYJt1D4>$CJMRpuI{&3|&NXH+>3n3kU7%hZ!{9(8d((#AELP*CS z`U@eQIj^@6(s82hLg?^wYv?TUhr@m7`HSq>b^M{d5Yq97)K z`a($O)YcY4I%lc65IX!c87d1-?SWm!wJGm;3+%Z3I;R$VZlgPkyAJHaJDpog3n86T zTU-d~gnD5iq~j0yg^-Rvs0$$-f5A8#S9N4sX>O{$fkd6~27D9*j&JbS+={S+F5Ylm?*yR5IyO#S_cK^@r_je!J z{gd4%=y8Hay-Cgs}KkfYe&R^|3 zwDXmnKiv8F&Ye5&+4<$2x9(iC)7vTUWOm{^6j(pt+PP@wg*(sOS=)YM`$yYVI_pH}gyVj!hGHcX|SrO~S)(foXTGuz8*m!*7(T#^T9suhb?%sI+#%&un zZ(P4I+^B9`u@T=OH!cAc2v2OBx3RUcvi{@s$JQTSe{lUiP>=A=_1nQJh#S_gT5qoB z*2Q&t{nB;!`bF#Kub;DSUi-<~57r)8`#PwW_{p`q*6vulb!~6$nzb%iD{&d9ortX= zYZtFwuy*d+`sx#_kFP!oDk(m&dhhDptM6aEZT05W>sN=XRZv$kzDj}>753E=tLLq5 zt*%&p45} zb@Tn^Pnz!ndo$c>-UGE9yXK<#GIP|7nGy5F<_pZ{g6Ba`m>vhKGafQMV7k|Ix9R<+ z+d!4b>rF#b)pUg^ZX!*WnCzw#rt?f&rWNCljgJ{0Ha=*)&v=jVPEgD77UK=ZtBg%! zZt?`ebOrdIr~jTFfu~2{KVSqlO%|hejavCGq#uR!UqSjikp4ELAA$5=Li#Tt{Vhm8 z4Cz0I^fw{>XOMmf(tir+Z$SD_ApLbne+|-q4C(&~=?5YGN09z1q`v~`2O#}rNPh{^ zUxf7ikp2RsKM(28LHa&Oe-_ez2NPhy-zX$1iApLPj z|1P9I2I;#Y{ZUAN1k%3)>AN8PVMu=n(jSEMosj-*NPhs*zXj>{L;8J?{!K`~7t(h? z`Zpl`>yUmAq;H4xyCMB9NdFq7Z-ewZA^oe6eg~v)h4imL`j;X7OOU<=(!U7lUx4)g zfb`9femkV!2I-q1y$9*HLi#O`z7f(lK>E#)eiNjB9@5uC`ZT1kgY>nKzQ$^@7_@op zjWhX%nY?-?ubRm#XL3A~qnRAerFEY4(MCi63?&SY*Tm6?=hGCPx3%w%RJUq6$V&*Wt@`MQ}*&!jYy;!LJyGC7lp znT*e*Fq5&F8f`Li?WpGjmUeKYBuNzY8WXVNv3&Y5(~q0eAmb?KXFFyCT>Z z@X}rP?nS%j@16th{eQCagPljfK7se|d~)ZmojbsOfqUT2zq?Ze`vyjLupMOQ;++e2 z&fQrD_x+E9eFPuceqj6F?YqIw0=I47ynQ{m>#uHKu^rzgw=V(v44l|LZ+mNd1>EyL zw)ODVgIo83T?g*mx_#sPjdM24pq{`FK!yLWuip>$7`$u!j`dsD_rP9*-Sy)7W$RI} z=OD6v@%jbp=YqWlpICc*?a{S|K-GbJ*X~|>|JrR}FT(4=ZUoh}E5M$F{vD#g+>!=UUdyPnaJ!KWcu+{DAph z^WEn6n{PAUY`)$+G*``6nB$<%!6jg?gcIiT%vv6v&l;e7)&S+R zVE>7q-d7o*eAWQvvj!-iH9+~S0m^3$P(Evb@>v6v&l;e7)&S+R1}L93K>4fz%4ZEw zK5KyTSp$^M8lZgE0Ohj=D4#Vz`K$rTXAMw3Yk=}u1C-AipnTQ<<+BDTpEW@FtO3er z4NyL7fbv-bl+PNVeAWQvvj!-iH9+~S0m^3$P(Evb@>v6v&l;e7)&S+R1}L93K>4fz z%4ZEwK5KyTS;IAu|GW{>Z-DgGkiH7iS3-IW=@F!dkRCv~59uDHyO8ccx((?Tq??d# zK)Md;8lGENz8un*LHc!&PD5IP zv7RUh1fCv&1tS2(;Hw~QgY?CaJ_+epLi!buz6jDUhxE%J{ZdGufb>fs{bER82GL7|0!Tj}($9nRd60fCq@M%nXG8j2NIwhG&xG_dAbk#`cOkt4>1{}F zL3$I?R!DC^dL7bhkY0tf1=40nn;>n3v;op9GrjU}kp5Rl|38p^0@D8i>3@dwKSBB@ zkp4$V{~t*IZ%F?b(*Fz6{{ZP9LHcn>{}9rD59z;y^ba8YeMo-~(tiu-#~}ST){RG2 zsFk}`)^1t95xl8>`P%0TpZ ze8Tj0!+TfmT2l3>oZ$#+x*hz zJHUOa&w9tCFV+s&^Y0m;lhZ%D*?76p@TWRoL?Z{j!uW;;JfZ{m4}6I7mm2Vwdcc8? zP`;%B-#Z*|I}UuV@wXcAm>zK8E1bX4fIm7Ma3Kf2BKfKYd|LxVSpD>?vjz?UA-fcx}-1MdA<4Y*4OIJiFG!y52jJ>Wo> z?l=NC@MYR>Xu$94SnwSfrH^UAdvpNrfrGwX1Mb!Vd;GKHFfiAs61Ab8l@Eq{gU(f*EgFbK`KB@t4*RyaSrr)LkAJ+kV2hPLqYQS4{ z0QZ4UfZw74dpdyUfQ7eefZnir4xEQuHQ*W@3+@94{YDLVvmS83!kaYUTROnOPgWn+ zfUpj5a7_fI0Y9$;xDU9-wHhGm0L}x>7uSH89&q5q@fyJE0SAUPssWrHaNta{8gQ!) z;5jgB{E7zX9`u2DNNB)~Iu_gqEZm?0c^$xY;22a5Q1pNU=Rwwht91bPfqChwl@&wk zOz!mb|376Tuzwzm=EKi}+codrMw14ZbO6u%nKl|VKxgTn`+)NqS2Wl7HysP^1J~() z)qu+n2VBkr?tPgCT&4#cnE76(0ckzpK$j#9c;axt?V2AY!@p?2KkEPokKrGW01ix* z|D*wWnX&UgO#j_tz;R$$e|Q9Npi7Tyz?Fv|wCmtHyRin0b%2Abbw(O6)B_G2!$1T2 zdcXnK=p6wZaE-18bo4A7n7vy^0P`VgXlp=I&%%MZwX6ZUw=@Sv>AOb&2fFm=Lcnc5 zaL|9H0lGJ@2Y|nreEeM%eHLG@+sTOVQg8xB-^@tl4BF@WoQ;yU|AM+VgF?}%MHsB?kwR9gyjhL86eze zAj{2Pc6-zw&rI4*ngyO`JI^!E`#N7$eO0Pbm87cg1z+|J@WoyRU*e14>+!X#pR=5^ znq2wB3bXv-WeUCnz5==(axA@J$+~!(<>ia#Eu238y!lO#cwhc*S%5?_w!E+<2GNjK zkV|%l<`;a3#F1Gk&`)C>htD7OVkx_1*lPt|w;I$Ih})OpL9k3;*qzC`ivr@$kT_89 z;+hhkGwM1KrD()c?@)$tFrDT(r3a#*mU<;6T`h*Y6DO1x&f4{X<~uQ*CX!iup2|gX zkz!7+Fic&~MP+|n@f39*T8`ODyTX1Y=wK_pVzHtj&;aLKT0bc21X zpwhW4+wOJ_c8Fe~$@#dO+1fdGzTHt!L$5g(EC zsIn&elM_C`FS})a*dOM%ip@Z)rYL+Un&HCXzSdIv`EohLia}P4l$}wBp?M<#gB6O{ zRz89ToaJ1@t+631Tjp9VZ$2B2)%YrrG}>~OHN1RGz=5!bez2D9Yt>>+tXArsDA@u! z9}_;1yUl=<(p*z+R5GMk4AlD7o)ET)0_Mz-0lptL%51#Mg&jVJN^dbzD4HlVwaceb{gx4n{i@KCIrq>jTAPN?IY{(Gkv$q^=CT*kGG2`SLuj&d5fx8%au;6(scXF&PXxBbdY+IEA#GtCmyUPO9E%F zlzM5Y6C@7Sb*7XkhH0WL$%zEf5ULYC%)f2d2a1W#YARR$~*ziFR1HW7h{f z2;%YA(j=jl_#$e{lvP_R6T`cHryWBgPE8LQM1_={PI^n}Ig(AgH)FL25S&B2iAE<- zV-c(xj0W{08EkQtB;zZ!h!lNL3`Ucwauvj`?c~`CEoq(!A1trh^}*FmmF*IZ^QuHDzs;m@ZTU4p()Z%3}i+wITfIE&?a6f8cVfH{s|ve(z`x5)s)t-Hf@fghtT46LQ?$&Pfc)n z#6Cyr)w;LtuCc)=O$%FY))CQJR@Dq^Fwqon%4jK8o9wSQBX%%+d#RM9ThlgYAY48e z0He(>w1b{PoHlx~Fh@=Ju>8nf9~_;Ar`@%;+*Jo%su@v+Q_ICv%F#$EdbC!K6PT-5 zHLyswxkVNVZI`nYOVt`#E5Z};oG0uHR?;oTl@&5g91X-;){@Ghe&Qfi72`(U8`wy=6~#^#GkQh>V^O$t44A zSK2v|rW{pKaoBt$n4y~)o(fQ1{h+sz3N*W#=oh<@dM&{z>O?z0FWvRQuG1N3ljc}n zOod3R(&%Ob4mz2S2t>UcWvVtVk&m*uc9YnmY)CX0^lSDGnr}P6jmg`MT5C9!3Rtm5 z59@k`UX5yHOL>|(Nb{1=r?Wjt;l1rzE0u0c_^{kHr%ln$w>7@ev`26}Vq~&>)o=LF zfJ&D#HBIPw9ZbfDrag9hH?>9aMk|={mc8YQi)+|(N~u{2_|jE0oAz|={(>I2_N^Wv z1LBV#%%=+=WF$xC@4Zs6MIpYr3mIHME|e2xFZZ>S?i)A1B#1pH-x94&dXg^W9?>lm*0omQIWd$xWsqi@-|U2hJkX9g5|7KDP+ zxTFv%cvA|J3vz`7@S=$-4aUYcBh4vp59jRq&?SRZMz7kHU{Q~_is?+7PFF+Kpj~UB z%{-|`!d-U^_12@Qt*|?xcbPEhQTed7NP7DzmFP4of>WuA$+R7hsD_`ckpU|!9V9hf zr@~AjuP6FRpF(Hq6Ww9{f-xTmA5NDkwBRI}rpqa)^$1Rd3xSR;NT4~NW-EJpaW(C5 zbhgT6mMmrBZEqxtN&SkHFVz?d6FZX4CZpbHD%x)#FRcEH`@7z1IV)l7?vP^izFQTU!rI9S9Jb_E+Q645n#FaxG;B$4mbBs7;G ziE=EK%@u8ld=7Kj<5HdL(M^H?u>@ILwHxXjtdlXtpQo$&BGn3!nG!p>jDl|-^TC!! zN{pk8*Yg?CNmsp{U?E=Zs#>p@S4pa(r>aQW?Q2u|mOGcxoOVZrNNXO{*YGujSeH$d z-7YR2h_=%SqtmH&l37lpVh1C>bST#5CA!lVg+`oWkDE3>wd;c+#vFW(D`ae4pr%w} zl^R;{g>6w+twu$Zc0x+T)qc2EFJ!m+9M>u_6|GK5PEUqFixsr1=7dP!!{)-aN=2(u zp_Huiu6pWVo2w^^g4~JHYSvq5)T5KPsQG`|^+Cl_ArA;vE+!*6su#w)at6c`?H26i z0#hpKl)@%zQo7g+(OV6M^(A^L#dO=E5`sBD+hR1xsY~_}U zI=ODkXY&;~5ELR@U6?W+!0RHdq zkI(|okhbRs>ZZ>T=C@`DL%Mgc&tuh1od^)K)Pcl9zMgApMNz9Z%0O7t=ECt4shiFa z8+0HKx-~<@(xK|_P`FcUiw$F-34P7%8BC~^jz@Ey)GOUH;P4=UE;c};45UU+@{;ek z4=96FLKCbv8U{%Aq{p{(QQHx?o;nB6{s=M@!X2c|6~$7%B@c2NTr9J7p7i)pI8X3& zD1!g#82sPSPsh}InBDIGJ3ibsNDKB_<_FG?=bdlojr^cV977*> z{F!i4H}Zx!_S-OhA>TNfl~fg^qgWG*d0|kyp;5H~)<=Wt3^P9}rfdh!{qaOyT{gQN z1tpwN)HUc*WQD6paf7d=!*rz{>(!hcBwj9O+^uND!-wd2$B`@x$<#v@b@hfFh(_^+ zI$qHsbP?b1b@|Fwvg37UtUc4Xc+hUGJ+aB!-4yH#O?&>nwnhA6hXf+)dy6R#t3D7@Zg0;G#guzM)qMIe!~1u;aX;MK%|NU8xZB4p z_p1|#9q+_v{uA9eb_&-kRRxTO#}6W44;(30bw$qiMq~c*gOO@Q5gQH$Fq*TuZ7u|{ z+A(xsx-(F7Elma)G+MGe=nh9m)S$obF`}lvUxBQeTD1%u1df6Sk-S`K>c*vRH-ZfO z*!h1sa4&0%T5fn}w>f~H7>c+M7j{xPY-XA5vpolZz`qN z@@U7`J1nK^4@1VBcXYkIi0yEM@u$G5(Cy>+>|y4x!(oJ}c)e>&2OK^G7yUg$%QFE- z!W#){>1vf@!uf>4#M?wW@&o+wNh+j~&#cIcTq zyAyX~4p71gr8c+%vk8~h{|Q3udkx-_3-i$rtatz^SIPCo0hgNOJm7Z2MG#|+%t=whNE zXVp}jM#RIKk0VHjRBgxEemLolCepkVDB;~u)K5^1yT>`ipj#p%(L%UDwpo6<&OWR< z=Ij8MMMNa!%821d|id7A0wXw@|lEFT0>&kdL8gDwC*^?=o-%rY33 zP-(0at%3KGwk{!PgdOu26fzZ#`eL?1sDL#=NtVI$|KsLeCgChj)}v}AZl1ULLO~OeCndcpQ~q5HC#VJfYN|E%pB~Vy0i!HN0S?#W|1{4 z9=Uqp&Y+!LQ@gXX-kU|&@RkdXTs3H9n<&E8z4g~l&|i%>8x5ZsG!em^i3}<=m^0V+e#>)$@pFPXa|Tt!tr^4v z!JK(`;Ok({Tzj~_PR^M#NGRTLAM&YVF+ zv2x^MP~>osEl*wK*dUuhMX`3|(4cV#{T`mWsAC4%4Bs!o1J^~MwBeiyOxa7bVPz!j(0ci8L zn?Kw9&gNG@%)pOqzIXF2Kri4GAZp0w5Ezh*vY-w2}OWu;UFcyymw;Z;dXF1DqiUnS{f8o0e zUtPFo;jV={7T&t>nuUK^c=5t>pf^FcffxlZfSv{20(GD|BtTi9J4iv7K{n_K&^gel z(8|&umVUAH{iUxjeRk<%OYd8{ed%>e|GM;&rROd^ed)%9r!MJB)ur50YALitEV-7f z@HKD@_QO}ecKAa0aqwyI8axmE5A;Lm8_?&Wk8k!i8=JyrW|P?@H(ekG;=#?+H`g}j zHh!`3y^XJKe0t-<8}HtD6NrNN(v9bBJbmMa4Rxcmam_|_gW9-s<1mPWc;?3DhGqSi z>pxuo`uaWV9|qb7Z&?4=^%t){d;OO6)_N60Wr(d)K>5JBesKM?^_8{Xt^IWETWgL*s;zj`~+ zJb3x)3s#@GdgH3PDz0W%nN?!dv3lX^Ijg4t?Z#iN{BY&#EBCB?c;#IyZ&>-)l^3r( zd*zmu)=G8dx|P@pwSuo$R}QY6wz9JPyXBuQe{1>k%O6|5WBJX?w=KVP`MJwaUG6Q{ zm-EZ1WqSFFW!v(3%V#ccEYAb|$M3>lhCcyfN8Ap-27WpG0{EHmjj#%f!xu*@Akty} zbDy4Ds+fo{(bXop%0yS1=n4~EZlcRfbg7BlCUTm{aTG27&r!65nFuuzVxq$)y2M0Q z6CENb+(iFmqL-TJB_?{YiC$!)7ntbzCVH-ko^uo}DJE)~s9~bIiDVN=CaRgJYNE1< zN+uFbR5X!bqJoL?CVHZYawfXoMAw<JPJNI#G@dCL+l<)I|?Q^{CyNm zaEM1i1BZANm~4nI9!on4KsNmSg0bJv8^ezu!=oT#L;F#%upu4=3mf85P_Q8$1qB=8 zQSh%J9tHUt;!%*VAsz+x8sbqfuOS`<>KfuvK&~Mk1>PFsQGl%>9tGGM;JHyCtsx!- z(i-AXAgv)D1<4xXQDCeg9tFl4;!yysAsz+58sbrKt05i*v>M`3K&v6XZ?_HSzA=XH z9m8K4!(SZ3Ul_xm9mAg)!=D<%pB%#&Eb_$MCDh@V}4YSB~L-9mB5}!~Z;nUp9tcI)?vY48Ld$ zzi&7V0FJZ;25_VzFn}ZJfB_uI0Sw?M z;QauO0^SecDB%47jso5f;3(kz0FDCQ58x=u{Q!=l+z+6+@LYW0=#;s5zlnZkq92>+ zM<)8diN0r|Z=2{o6Me%(_nPRdCi;qrzG$K^nCNpR`mBlWG0~?@^hpza!bBf8(cLDx z%S0bB(T7a*K@)wzMDI7zohEvpiS97bdrb6h6TQns?=;anO!Vu)`Tw=Km(MN0oByzZ zt=+v^Sy_Pp3BngYWvPRo|MY*i7NP5&B0TP7V(`w%_xlVu*`7ta?JVj;@@c$9`-fQ1 z`|*C!+r(He(ec!X4wH;1yq{|o{c=5@2B&%ahAhUK9mY#_vZyT4{-Ru{5;*OpLLkq6 zwhrt)q)kA%US{zBy+}Fc_q7TrW-v9P%O)w1HdtbkOScNk(=20APw5O)avR5_X;D_Jxm3wCBuR*y*!%K~{jYJ39t;ODUnlEO{ z0Br?2O&jXE1Oflq<|})&vg6pV76|fZ)XKJqcfVS>*bV#m4xhmAWRh$q?U<3M4X=Lh z*jDy1^=>2G!mA!i$mqp@9VZrK(aO%rcbk7k zt&B3YeQIT2JkM}Ut(agm7Ku~oq~a^o^%M1FEFMVNLzwesZdwm+*@PVQGL^Y+1=L`+~>!A5HtDPk3q#0grN;H#Oo z1NGu`2}CL>Wt|DEnxGQkPFKe$JLiaoVtC6}ZYo(hHrAKLy;^xPR@0wVEAQ7G!{Zo) zT#yo|&@0s=X%3-{LW4d*E0Y+~@6)^y@ro>qL|Z~zj5utLQXt;T#@&OvOh~u-tl)5| zI)29}-kOK5c}o6qCkX>)pWHz3M{DGr32Bze&&JkIHu3Vcd^%Px24rxn9ZYV%S{CK{ zLT4~B(qQr)OumkGp&pS3lPoV8{fta{SUYF18L6L0F`e{q5)IY`RBjaNKBSll${-Gf zFHp=X$wJlLq|hGXu!kysqZTOo8eC2lE3uq6qy_tFRZV-!=G=Go9v#^+8QbuW*2c3h zFl_98-7Y8`4X1og-d{1eo)XK}@g&Qgup`%d6hmu_r!PecEd=lRgBh^b+3Ih*YI!M8 z#V~{K6yja5z#8w!!d|UB87%A1s+ITacHtK~g_K-nDB4H|cq7lbF+mniaJwLoZqys$ z>}<7C%e2K@263=?JEyZ15e;(fW)cZI!$lBr2XBwxlnZ;c^5kySKc`lX?AK!1YCe^!r)8y=bp zcU|nX-)U&ad89(J9AEal+%ITa`BVT8dl?s<+-^nyyhnLKt}+PcO)4# z7#l*ytYtdhEmC5kPsY4{5Wq?dM2VDiGWRCme@tt4u)rp|eZ%k?g51#3jc~I)5i~^> z^KB6{`(t*S-D~f%)u>uYxN#&JS3_}!RO#W_U^LLx$v9J}6+?DCAj@p^WRag8e>{I5 zq}lDaP)|N{SdB9Wy~j}qD^fb&^V4mcRrdNwYcz-FbSIs!#JDym^@~-OZsw3;?_oFl zY< z(E3R$GU?#S`p=}@-W7R}>w$u5oUvPtK$fklbV!YXbBY@0vgVj9+w4U{qLD-sQm1Oo zd(}?OTg!!zlestfP(Q8R-eJ8Sq#8ERtxn(VR{mnUYTS6tRYW0` z$Oyr552d0Ys*cy&K3N#s;{w{BtQwO~*LzpvLA#X=T#wUsuSXwm1~b)owWjf8Bi;`1 zifm{ZIp@+)f0nlOdkzqPwUf4XwC1!9PHVS!MIPjOAh#e-e$=QMXMnqFINdaCA@IUB zp6#~dNj@)CLw=-!5yg0n$P0OnK{I^16ANWe;@)wJ1k>6b)H+LNT*-9M4f zC%!RZIB5Du?lygg;(eS?(P=p)G%<&t>WK|PbvaR6Le3Frs83e-OsiW%Qzxy)Vc_sVqzO_`crnB-mTRdkxH6~2Ki!=^q@!VR-@eJf5K2|;Cmjlt z4(?w?9^`sBK{YmZt6?~*#Z1=luvXRQWf21eea5raRFn*M^X+(0Y0G-4Tg&GC&66!P zj?*DISv4k~?Djt1^dQx6fNHGoR)Y=T!JZPWWBpdY$ME@VR<ktwxT^R|=}L>Mn%T3My-2SMk$Sv&m?| zN;2PXg$$xs7ISLaIO)CW*iU*WezIyz+C8jlU|^814y!R7y(QO7Fo1XDNMSv(U` zYyQ4VcG(K5I~5LgM79`|+#EulbfGcz)tJ2+4?4&P*JEY(ddv`I>16FZxxk**ZtsH( z57MpB!T0}9THZK!;HO|u|EIu?{x^gD{4WB#`Fmh5|21GI{}o^#|Kq?e{&}#6{~KTj z|NFuI{eJ_y_n*1h-;_451-sp^+&sK_?&c|*3mf-ud~@TTjSpU5gIv%B(3#Nc(r=c2u=M4nyO-Xx z^!lZjFFkMR$xFtPxWp~_m)uJiEQ=?yZDC1S1dk%@hOX~#nNJW zF|dd)UbJ}D;=1K`mLFNZYWZ)=9hNt)*VnIKXVZ{nYivwO_2=xAxh!53Sw4 z_8)66Sv#_J<62`aw-#L^*3h-TT6@&m(&_`N-(LOP>W5d~Zuu9>-&vk&XEp!$t3)zL>!et8=FPyWmIsb?GpX|Q2 zng6d{{D0^#uLU+2mlhW0>(Bn!moI`#aLJ^fe<>`&qDk)w!$r7g(oZ}W^nAgjANx66 zfD0zQYZK1Hd6RzVWAGEGnV zCr$d#3Y>rwCVk1ha2$@C^u zlWwqJ)_HFFs@WEYrJ$w#)&MpnZXTxXj zQU`n%eAX^K7k&)<7?b||Qus{xOq2fLR4@}h+N9ro27Cs5hDpD(0iO<^ZqoaD;Mm(~ zCjI6I;77raGU+!q;8WpKP5QM5;77ub+@)6d6!;XAe)VbaBj87v^h;lW55Na@>8IgM zcypJ29NvI8O#1n2;B|Q2r1xOpDBhY$Kbe46;Z>8~eJi{IubA|%|ACj`Ws`n*3kH8C z{lFJt2!>4hz7)I!FPZdRcfyPCqDkL=8*G6sCVlHc7>F60^mX3`V#)I+ebvk1Ie5;b z|K5lG0R6$FulyV6_t5W6`idVzzk_~f(idF?{TBMIN&jvI`U&(ClRl4zeg*w%mmY$C z3H@@Ho(laO`ngG;`$p(~=>A>$BIsw(&vxle&`+VCn)FB?`Z4rllRoW3(2t-Wne@iL z0cRh7Xwq&I`T_IUj|Ope#4}m`=NWGdrf-T z*PyRMUpFa!9rQKmYbJGOpszw-HL3mIp|3z+F{urPz6^cYq!;?Y;{Qt~J@3uX7ojhj z^xXTQFF;=~>0@bd%J=goeaw%c&q1Fv>7)D5XQ9uU^icumGtg&Dy7_hJ9_SvEu3rXy z8v3+J*JS8Z(5Fng{8s3b&?ij_zXSRN^a+zLUIG0#^xsG6UB7z`^l@$krG zpKR~mrGD^i^D&eD`dsLv&__-B3ompRbeBng_6+DF&__)ALmK)p^kI{JZv*-e^dXae zn}I$EebA&|KLmXM`hZElbQkn~==~=BdW zdizhIw?l6?>Dy?aZg;y$-|{8sZP43H`k%|tTcNk^(k19E&|6IUhSQ-pLvJ?etDgn^ zC-k2tz3pyr7X3{o{kKEV8=*Iv^#3KGH$ZPN>B}Dry&ihKNni3(@MQWrlm7k3q1QsM zHR<0SgkA%^#-z`ED)egT)h6A3JM=2(RVIDbccK4){==lt7@Uv4ZSefh%>8l>oaX;W z@WhWFfH&{i{FnLdO?C6C&5eyO&i`!Vwv7%rqkbwlmHxW*8`neYXRdu`?Ja9hS&Ofo zyZWQmcdq`;YG(EPm7lNt*Zf5*f4g%1%EimSUjESX3zv(_=<*zVH~f$D*UtSC+#K96 z1iJkzAO*T|{=Lxp(ifKgeW|_VU3%o=*A`#9c*A0Ffm?jERIn$@vLNe+<-A1C;MNFC}fQ(ncZ)?1{X?8nw0 zw^K&nOEN_SYv+hWL}4F zN4>Tzo0s~vx<5mA;#pN@a4ga>`aM~1*Bef3<;I;90ZqURJk$+QZ8}l)`U5>6Sy*ya z;-W8^iUCQ%c*WoMP_06F{<}LV{E;xh;Bj841<3+u(`7u?YPovFPRiZGJh395?=;<9 zs)aY9vv*SX`lXU95_C3u`7oBk^F}J6*m7hXjYgX#ob!tfBvN&_gB%T?zLSEm^Mjgz zKH6(yU>}Sf!T6BwclLUzK-q{W#gflVumLJKQl3A1EQ+UXTfek-4Yuq_C^txC`s> zB1p(#4;K+mGteTV^)hkQ8Yt&+a9x9hu9ZvlQg0`PEdez3Vl12N*`w*Szu8mzB9@Yw zD2Le7&TuTAsFgIMQ;kTg-x;P*`$55HfISg-w$BvGTF%wldyWDB`H8YPl@MA|1)fB;|-6fmL@ugDklDAKx zY9u0nnnOD&)J&bDus)N+(plA)?x5aUH{q3g?Y5dJ`EW9c=7S)BY_L&=cSfz+AnC4M z3bml-JKx1HCbxMVtg{s``s4LCk)Fo$HtA`%S6vC?YDV_4z3B^Nt zf?iCq2epiV+jAm^HUp>?ng75}ihQDGlPIp_t#B^J?iW?9@5u*q#XzphSp6PcbjIPE!{dw5hb!B<5x2gsgp}rc{B_}06?&9s+T~R3dzKoFc(cyhN}$SzLR2leYb@d zj#5aC1fT0PYyl1IDPwfJNpek}6rg~EVFoG5NUIS@IF(#%<)3#_FoAGgMBHj8kD>K$ zjP$2;jV!i18713F*qD0B6~+T{Mr=11@KFk%jg5;H+NTvj6m-LBuZYQ7q??YEYXV~^ zO2mPB6jzp$k>wk9QiK_;=0senqP^hrM$Z@}F%rUjATW7RbCT6e)LL?{K4K?DNYm>^!Rf6~_6z~U zoAL;fQ6gEkj7Hda%+sl6TV7R=U0G>0u#+O-iF#_N7wOkCjeI%fclR2&kx7zrB_6he z!-f^Lqy`YBAC4`Ldb_`B2L~iuE}vNRw@|ic=j*+6JC?CFwdJ3(^6Y#K`vORpZK@BqaH4wtQDuM7@v z*c!cT-dUs~ULLPuo*b*$!bnR?HT5jCGuNvA=~Fyurwm`OVFgQ}49hm_!7@#0?tIMO z&)A|iC5%UG)kY@=)XgM5C|Qt1rcS7^NyHi&EMav(~sgkyj}f zn)k>WZ6OhhIKQRhy=a*%r~UT4hZfQl%JS(jmv_};9S;wLORJo{nPG zS2IyISV|@1OhCoc83wDiL^2=J=kMJ~;m{okoC15!Ft0*a{St;28gil-@JT^K^XKKB zv+mTjWF{iaJ!dDy^g`3{%XU(rIOeMIN-2hjWv-d-r`tuL?HtHrdxWmFTn#1(!>a|u z_SnJ~XHM~jofJr+gtVi5+)*b3F;3?(J{tR(S- z-^f<0{Wf)H`F9zlw!o_1JYxoDKa3cjp&xlV}ArUJe$5L*;cuj13)UBPQ@M~Y%3 znpRRslxi;Qw64;hKE=)rZU$vv%d2+0>iVgaEndLvv2YB@cyo#n&6Q%AT-sZe-K-)P zya-hYrVU=QJa;EW$H4SNsEOH$o~m)K7$f<47uhBvjaVOX zI}3_dNU9F9!7xwUDZ-$;6?Z!a1PiP2yq?Z=Tw=0m^a!6n*N)n1?Y^zy)m?Gc&RBlF zlR|6y$!e~ZKm&zZ+ZV?2xRmL{ISMbf!HTDxN{f0@EE^#uwJ^HmiXTfOB+w}}y#5-6 z)*E^&CPxDaD;>^Mqu{imS1x24w49-reLMPp_s^|AWBCtDH_qSx$9dZRzg+)}THxu> z_R>JM<;a=yTQh`Z-Fu(Ov9c{E6C-pZ_7lmr%n(*|pcyiYy3){oh8;R5mTj3KgzT)c zE&og6g{EwaZCtixpx8CNY|BJk--jXJ;=&$8z9m%V@ITtQ| zl@F5%N^TQ10>L9eTzbgzElm1QC!^bIb&{m1nkaBox|{Ip1#h5nsOph?axGA3aeB!u zhZN2&J3XzG%~|3PJF9A&>G)8f6-$cwKr(*V@2&CKOtPH!d8gCq8&rLu(|6PM;&3Fc z%+C_2dH+Tt`WHD8XNau)7c~-{GmXSiXzPbF0_{%sK#TK)0!kU5v&z=XwZp70m}wDe zt3w#LyGXiOD%NsRMm*jsr4I$Oai9K>M_{%aZA4g2LE6=%=9CNF!+t_>Bm#81Q*bhP zFr!6KcQoMh2SiW7=-R0a?g`OtspoU#<))WobRUVeZFt?^jBgDM#R->H`V0 zrESY_1fDU!HAA=BzY%EriyVP71ROtR1fDcn`k)t&3z_{d&4|ZqBMdaO^hzGc*baO3 zQGv~Y(%&AFTtBkh7WfWq$}BMH;Wmk7)!E6r4~~nQKt>!Bl8$FAN6avs&|8GxV$sX?uao)TwAxHmF1Ho-K<#tirFWB*~Yz8*Z-T39kCpTWb@wAQWH=G-r>-Vj{d;Nv$jdkDpW7i&7yL;`GYd5WN zYnQA+t6y1t%j))OY4yt0(^r19@`06?t+ZDnE9Wo&e)%)YuU&rH^7YHk?QsXYegc+e@XTE0<1R{L$hE=6&;zoqNFYD6lKw&V`pP zbQj_a7lCciUzmR*P$GE!foII!J$S+fmAPP=C+#O#VhdX%*D=LApb zOljAi(yldb=X5%4PA5$&(cWaa=N)*){3X-W3 zrCoR2&gnvJF0>tO7sSbO4^L@#$+31eo6APT_Z~c5a&+&v&D>|%rJes+JEzU^c&x=q{hi>awYN zSUbDTg|S_K>$n-qF{K?grJem)JFup=XY{Uq?CKhAr&6~g9ym8~V8&6vQ^?GmZmunqx?z{zd_3O(&TUOzpz)f)Us}9b>ON-xItXjTf zDKFf&AkKetUYNTVBtQAzO)m*8iehXQ8Q`rM1XlNsMsd?if-^fXgOKbr4$wmfW(Ym9 zHG^#KG!6u2c3__`fq!NP_W2U{h7M$dd;u}a9wc8cdorreQw`aLZb?eu&P)*z!l)#- z1~kNubY=%; z7%|hd06uhJ78&;$cCYRIN%W?dxMy}?2Kno09B|F-zzknHr*XhJbYO<MK?rSP3q_4E`DHgZ^RZ zM@!`5%PhaOWENh#0L^p2{7?9I%emWI2j=F*C&cFFR+i_Nmabcdw`O>@9<8i}ix2 z?LAw=Ak-QN#x(f%S7)CrCgWtN&s#3uJ~;64s)3IyLm!v6W_b8K={~u={aqS(dcQIB z`tv9AcQP2}EnC~?4E#O3>+j+$?;wubPPDh5i$g!JpV80rx6dBZF~tmJ3xrGtQgAHCl(qmPf>e$0Wnm7S5a zvN9M+vpnsLTog1hZ-{$027v z_~_cbAFXfUy>7Qgv#o5XSPAAFId4i$G;6pVPJn}FG+FG#g$k$RE}KH-FRAB?;w3HR zl2TQcMeUNo9=*L+kn004e{kyH-yi?|$$XuRDSOKW+h-i-Yd|QwS_#J4^+gJ~PCDAE z(uHC)qZUI>2Oh>fPF>FPRs`v#k%!~!cb_rv^+zi+_ z3{OSlPL#3r!B>AVtCrn;8iXf_cIv5z=x$Ar4nf`Rjw#8 zzmOGMLZ7wC-E^|as*ZG1spm_@mSh8`h98cv-x&6RZ{0qFuaBJYb#<#^MP!OZ1ihtX z`Z1j87hG&91Jw4?Qn>|A?Up@4ut$^wlnZ-^zOD{h^lJ|c{{80i48ERnoUh>BxE4)y ztA#{}6wyVhSGJ<9 znnEgV)`JOJwCij6xUKSUdwt;RaE-e2 ze6qYX!@ItnJ^+>jhNv5t=*D1iwdZk|<$;f%zh?OF$7b+!ZNgJ{mJ`7b$FWz1mcshLY+qwEnvz#m&tyqsgn6!5v z7(V&kee1yA|2~;uFgAI29X$VA=iWSb;I;$t&EIVPzfET2fsKFK2(14U#OL>{{b20{ zYgdB!^MAXFuiU%xtQBne%V6*RVfb?(it0rmYUK^kc_21^XX)I<4=-wqXIbvF)Genk zya&XeKV|;y^TPZ($a??(lH0z;g_sB12IKa+?}vd&v~pc7#xm#L`d@0b)b{%Yk5xNL(@Ik~!)7HL~ zD(h(?x$PZhYn>unwrAuxrek*$l62kOAiR<;s^u0|W_-Q$rlM)r_W3TrA#g79%+pS$2D8hyFpE4SRqZDN@1MN?!;l3l!zmz64R#KO*U zR?jmzs^Sp&YDY^2;~i(XRL`iMZa1MPx33;%d*Kw>rhHu1R|$u6R!#D`yRuMmi+SD_ zjmKk{oTE`7%){ye8tBxhCUs3O~!53@aQ?`Qj2cK0N=FB@jNHAS{Fyw*!@Upmb8(R<%0w`O>X zOm5>-XFJ0~e{$PBb+$7+pC`9n!)(vk??!M=o$U-yy~%CI)Y;C|2-q;&)2C>JnY^`6 zo$U;d)5&dg>TGA~x3*!nr|owmAX8^MQzIOnI@=i@|C8I746}XI6pb*Gx7Mk%o#6#Q za{J<`vz@6C4h^$Cb&5uq;T1q~`=Y6{ov9HnoI2Z?8sUOrwvXKJMtH*1+0O9FBDsD3 z)Y;C|w&xAAJ!QWeVQcDaXKIAM8fN>5DZHKGwMBCK@l$6zleY(_&US_uLdorOhuI$3 z??!ms)Y;DD?PI6Tc80e%$?bE7*=|hH2s34S_AuM^DYBj6{Y`TFtYNlmQ)D~C{-fmf zV}{wTPLb^lduj&p|IeA)%-~u6=FoTA(?XT>0;n@bZV3Dfo{0 z=fYRaZvce^$I=^?tc$lTo@aTv<(!2VFPygjkN1DF9B)~Gz)^+eg)J#ik-eN(bK9Gt zYRoToB_ZEHXe)*K$waHEMT?SCR`taHXYb8pW!cuduzTk5zH6`)9D%S7+57>A=2nkY zWlTg(T~%FEch}I>6`Q8!c^>R)vs37S8IK1ec$gFv+5OFtNNxr z&RR+fsE|3+F2*a2YT%pY&|=#j=2U4-s1?)+uS9!uOj*m1YLnYYBW~9>T7h2vtM?di zzJe)PS?s9ppq|vpnNq!T1>0p(6Y+5D!LK8A3drsItGL-9ZhOMIn(~)r)uO|^w0y70 zvAo^68@y*RqR*4wcKusm_~-929O$hfA1EezeTH;GaHf2xSg&VX5>rBafCR-RkI6iQ z*YQ=G#c)_s;P4O-pnnWUg*FSf2KBMfV|&SiM!Wksc15)3kLs(2j{_xBd@_mvW_uqW zZj8gTU-BM*y}+odCxtGcd*#RB1z#vG(W3x48E zS>@Wsgu%Es+a2Q}SG!p)2FyUtl@sPsI~@&#vLhN5U89TIJFHOF9qDMAScm=<-Sdu` zlSOjY)ZEv}vAyvuo`2amY@vD3m#345WQ$~T$V#`{&!u{j54w#DH46p1v|7PA30+O) zg*%7krDuwTs;_oi(nfe`IHFzC^0+&A5aXuPLoBkpS0jV_)~Fgx9f-8ivG~oF%IDDg zagc0eem;1Z9hOfGwBy2Z%v8NF5aDbwS1{0@VD_9MX?~Yi-Y!>8D zBj9}qsp<|BGxnRBuVHZ`VFJ40wD zWUmXki(PG60VQ1^HDGEn?j}jPSduv4%mo7NER1Dafj{S{VCCWpd(~`Xg5KaeO1%>nj+Ipw%I36WOYy_x?5c`VXu>%*c^rnPj zu39SI3b5HKqJqq8sTn6#UPN>Y*niKm|B7jAsp6Z1q3~9^Gln$1f#K;#?=f&yMG`B} zA&4%E5vvhzs?fbIlKC2xE#&k<)e_kxqSh9!+BiCoYso1s&MhbmVWSdqlhLfCIkCbv z$f68HrfOrLDDsM?^)t^nnqCIGXuI#so9*5YC+{)f+K!fbDvGzFu~r~j7b+Z=HNa-= zM6&|)b3&VDI>ENfj|V;t7dEg!9DG5f@3TW$EBW5gKWOgqI$9caZ&Cz z-kR7aGDU8=WcM?uVxIqtFT=1X9nlYZNLw0W^r7@c;-?BV5aZW9`C4g3;S@KzoNq-*s4O;aPlGN_I}sjcqf0& z`(ao{_!&!aIT7KuKJ*Zo(A36+r%~7mt8&=8(s+O(JI0`^Zl_~01?}e1#*alB(CW*{ zq9A*gnoS6TXrkh*pwNkGvE41EiywZEVPT>IOsxo&Q=~*vD;kbXYfzcUL?lMI zCrnwx!W9R2OHtO|1jwACHwl6u9??v-WWC#`%hF60Lg%(!mual<3KMuB1QU)pn7aUd zRcPJp$$h$bk6~_+ZH1aFy`2FvcTT_@b}7)<=_B?%`D}pJ#hn;nUsKWV3WK*tD0_ z!a_JZL%{h=Z7z=3y3&A32~%_Oz+y7o4eTpIE-A4bF?TeLjE)=YW=Hk2|LHx3S>vpb z9BYqkSy2`_JWmiT%tBS}i_kgE=wr`x28_N#WaD-HU{n>p`pC2K}B z3h4r7eOBgKe`ksmxFNce7j4C0$?I8U(m7+xE%-9{0C(o{Dty{4311>z%| z1|vDk*L{j18ufix`&x@Kt3=b}AeVONfl7yV#tqDTD%hJk=jF}mEDG0>-B7-Bg&pGpp&98x`Shx!Uyk6@*9#7>-)BJ3nIP#*+MyIuM< zN^`RzLWWZ3=M!A%U&%OCREZI`Nw^kePfTy}Hr=D%W9V8c9A7uBi7JdUHP5@(Sz6aP z;~232I)ei_I&)h`KO&1Ox&r}IJ1&ata|)kv1gabqcDf&(8uN-A!4nLWKg?BQ>|c@P zuuf}zYbQQk7IN?0WEOk&ei;?j5v|AzHA+2EH5ch=s99{fMrUX^XKB0B6?oarQN1h}K40ru~-P9W|~_+qFI$JN3wVRJ(Y^*}8ZTJ&U)GHO?6fTikdb zUcPug4wY~2kx6(est6vBtW!_Y>4aH;Aoq!2oUkswS~S$;a1gF`fS>?SK<{`}t(UgA z3Ok*W=IF!=Rcw~|{6WD*7TIq0b^VG3B}$_o6H4SpR%W^9o8PGW!FvpaR6FM>w{r^w zsn7C;p4F^g87w_>6e3K5NTR8uwn=dRs=U3<+N-TEl2H(_IHq&exuvTr z1fSD`kJW{2QZ2F`1CLj>Lr=@6<&Njh?UXqXJ1~=6aZ^8#GccE&O?bT*#C5rE(7A1` z7gW2~HnWB@7ZrG5TvNZQcJ9%Z$Zi+%wZvMx>SuhTte>Cf|DSyG+r5y-f9RDZ$-~$* zWMd~;b;$VqW&(A!(kiPWrv}F4ixb7NK>wj;icK3Ly)vP|3KWvrHa?C)tSX=U;%m2m z@D^@0*A5R7sPM!)*RRCocCk-It>I@~*(H=dDHC#71Z`~MMu>Pe6hVuuCfhK$-ohMvu3v* z#!?N?b;*Dxnv7a#4ql!8sgc<_RzCUY+U@VVgxI2rqG!j_u<4O%RVBb69*}ue0c5llUul@oz8E_*qL++WWf;x)#(`_AvTkqu z^NiZY@}4BSTKVMVwc9tha2tY^0lc5&RxkTxAM6v3_RO+j8P;_r6(u2 zn$bsgI-Zx2W%P$hU**atPp;kmu3NYTieboM)nKC2X(T$=&M77CB!39AYT&_Mnq(QX z%!rmkyHh@SeC_soUsb)@YIx-}G(+N2LK!!6(kqldw`4ykRI9u^UjR2RwlM~GC}Y@xOV&7 zZ{aqF6z$k7g_3Edvf)dbQ@5!hg=YiI_LQumKXP4w?|=4pzNgXR7Mh%veI9kO7SCy=%9>?G|o(UyBcpZB=QVdHukp#tsD< zT%FCjFtQ5GJcgsdBFLi)!r_B!x4-oj5sTd}v#?^z+AVkX$@s<+=c=cSwYCPkD~W37 zizQ8+-3$W?tFx(`oUj=;;(?*JLo`#5%9}4zzf!0yX|h_cDpQVDcKV>Hl@2MzB#tq2HEb{t4>T(IZ>7LFmD}vh$bK$+H1G% zE!-MVq4y^3O({w+R}~*;s{uCl&@@d<(rvb2?Ns_3a5^-C_qMrq+uXwKa=D{iZFfQ= z=!61FN*7Lvs1KNOET;RFzGf+UZY6zM#}ur zoKe1AD{sncx8*I$5ARan`lh&cTin9!UFuukg1$F7>T%l54lgE!^IvzV%If?KZxJ+q=}azKO2gMz?T#m-^N>;kDcF7KNyG#CDEi zg%X<7qlK=KLp83#Kr#6+u$A5&w$>cd2iE<6pb=Z*l&AhiZkl z`TxUvfBS=%-~RM}e58RNe&Nr*Z6ol#`InwOb$7q*!SAK--K81r>%VAUMewf~vQy~1 zRr3I3&u>LV>IU?Or>DGf+$zj`@S7R_+j1U!i(*={~yA;r$H~T)Fg^gQqb6 zat%QAtIDk8Rp^J=+h>{NwwVNZ-FzE@K+t=oqf_lx9iUQZ>bK8z2}5tW_uqU@47R_SWC38RcJk`f14Bp*Znp9dhv347t@WgxoheXRc zOw9t0teQM~{$9rd~NFz^sCL7&S$Oweo z>=JTMJ`K4$RDu4iLvHmoLvHyCA@_|Axvx;V{k&(n)m!P@uR6=+vu!&?&mXJn)yEDW zrEMk&iXf^aUo1Z!hl;~ZP>x3idIR)gfs;D)GqY(y(;b*czw737`}Ut)XSM{J2H$3$ zBnQji&dCc>>DdLI_k8)9fepK6zO4JXQb+*3JyDRz9)CIDI=`5z;CD7x0wTg|>tKD>9A2KUbtabI;A zx%hb*V0S3`{wmAJuOJ5a28GwBLegI`qrF}8eI1R=2K2R`t|<*ppM!kua`Cn0Yd0h- zpj71ZqSh5f`??%Ijuo`qc`(h;1*Uh4a+<-t=S81A0_{F(YUed!p~sIG8!LdF2AtAY zn0TH_k)A_quUMR;LL`{uNWoGn@rRAGGiZi(Hk z)|u~ay#nQ3rtQ8P)3;xRyRagFD?Pa6|KIe%%QsK|?8Cov@0kO3nr=h8F6Am3gJ99?uq0C^3w`8BvVlG87wR#ks&)V;Wh+IM z8r7-Ej}^t;n);kUQ`cAc{Q(#ua{`VRe)KkJyDe$ANv`KdO1hqUTC zMt?590pAH;$Nx{w_}|D4w$)d}H1zq#^kV+j;aAN)a8O^eZ#{dr*a_#Rafgq?)zpb! z&)^J;Dr|1BuZPz)tlKcy()x@sEjnlmm;hnG^Ac|OW~n>5x`^7fVlI+U(LQbmFY!^e z-35%0Hpl(^V?juqNKZiFRbhG9&d@ulaNEj;$#)g9J%Kg*Z~YN%gpo`#Tr>tp&m z!Q=TidLCW;T*e$w3;$X%clJPk=**f4S-$E)3&GLM*v$r`Rbx&vtFBE~SQYVxypBHm z+ITtV=k>IOU%x)g7;hX68D_fiuon@dU{H;>msYV zCDj-OrG=;kRs1MNX1H4KZ#$2ITL&@s{Q1=f!UuopgJ(bW>_?t`>DhNY`_)f>`st58 z{libKr_(3@&y&CMQ;?Xa9_!G|$&;QZG zAA0!R4>u3L<@uj~@OK~l@PqGpuzm2$@BifeKX?E8?jP>|%6tFx#kaor?Jxe`z5o8+ z@B8Sl-uupbuRr(~AN-|{zW<}>BjlsUU-)a!fA<%D;0whU{{1h!{P2JO@K1i&e@K1! z%`gA27k~QYpL(%-`I}#IFaO=`TW!Rz_h5e1IC{bEoH0L|YkF2@@#BAR zvC!2@(dV>$X6WsKH$z9TNzfdH>RhcUUZI#PC9e%wtK(NkWO=brgB|}q z?4M~g><4C5wASK-{~LPX-n*)$b614+a?_*UDLI@x)9JV72Grj#cMc{yXG!2hXaFUf z>gI9@NtEk!4XSKFSI;7`+ku`>sI3EzYc0X%R~bvpd`~yEQh4w;-dSwYeE?x98qcXh zKVnAVV@u41jycF?H4Y0?bh8yeLxuX_^*amNOv`YwwVRHp=ib7abXFu1hPK*j+6r;8 z!lY9Nw0}_#zWrh$9ME>YIJHv~)CcFf$Q-ut7RN{pkl;pZ*SZpdr zT~4WbGRy0jwFak5Q)j+9AghM))`=Wa7N}}h)+qG&*IX=UWj=vTLc)5y!Va>ebRHaX z3nHl5mN2L@bTWgrVpz2LM7~(8MG7J2nw{*aYB?-oN($0ra6SjH%pqiMyFCOOTe9=8 z5B`gb1+^0D_>l0WZJq;bG)YjB!R)pIyi+}%wx=5rDey)ND(JztUMx^7$|!6FNv7iv z3c2YUCDzfB-AfvaE&I7paiIx{hwJ|Nuew;QvIaMxk*gu3$0NFKB4SH}vSmd&AJYH} zb3MYaFe7I9qj&eQ+{~P)E;?&Ru+_2Jg`0j_oy`((6ygi$W4>!fxdtOICsnET~$3{Ld~g;+)*YQ*+{ksHP{9uqhZF z)W#ZxzUX6njV`I7qf52{QSi~_HUQD)OyaDdEQYOn#)&md>4@g)x-r@TSPrVGGCMI$ zq51dz*0n`7j>6Om-qz0c*@`kY+yT~=*-_sy+>%T;#++;8Gm3At{QhscSPbZ)q(<+s zjwzcv6?30FsZz|&wkhK`V(an6j>J39s{99k>|)V5Xo!V@De77;uAtqTi=C4-*-I&R z*n@5W>%c9EnYt0{$N$U4q8TU#^8ugcx@gyHD*`WhiK|n;@B)`oH>YF|@x?Hm56FZ6 z@SO!RmivQgx3a#XMmtMEXJjSdn`s3RA*L~+6E>hrdD0^f%8NzKEcP(xuSaihktryZ zAl4L0S$Pgxz;TwSMa`R?f<IA9AgR8n7Mlq%J%*`$p} zh*KJ6hhZ+QECqY~KU^%rfWxcPW@cn2(VmkcE48+m>Yl=YrvVsZYnmhQd^V%|5B}$O z7A(^{=&BZkE$xA}d|95VVmgN~&rky0CltV_B9-Ggsvk@*7Qu|R$8H|>DlTkSZM6bR zlGPzY)Uxy^gqyTH&?{Z|=X7!J&s;2g$J;FfW+h=GBn7)u!iC8;-Q}&2p=1d4q?Vk= z23leHqhEKi@ak|LuhSu{lo&*nTleNLKXP2T7_E5-CH_{KPtS-z=TASr4yeji(JowR z$QaDfH8J3(nj7{y!?$xCKjp)D?N1%FT+|Eun=Y5IB9_n_D53GKkAeLOXO_ZsxR>-j zo}VUYb>s!5&G=I)-22NH3nTZ4fiO})D2u{+W7*g-Sc?kiDA(DUA(%`wpAkCZo`q-s z@SR1nSWJRiTB{yipR`%+ZH;52w=LgJl+4+nM|CqCu&Jy*`t|QDoC@=&!&Vr#f^QE6 zvLNtIK=uYCv4WeDZLpu9YPoi)$N%J=1qZq)O(2k=F^3YF$;#Y`OiYWcO_U)!HCYu< z3mUdI@#5Kdu{h@bfrnOTwq$x^muzN1yo?4?jix~%(SZ5Io*V%ujR8Rq*6%D>g6Cq* zi0l@e5<5;$XeTXNWM`+IL$(55+BLEoVV}=G_?;IEO_cGx0E{FmxmF3i+Z|h1U-<|_ zWID0k?irYwujozFviE=M#X`nva>}15i5aUDu$av4^&VY8G`0h)fC-qEdHiSq%8)L; z_Zz_CYyR~XU^jk$eUW+|vRdU#5k<9)k^~j+EDM^M^q7pB6~nU~fpX(wCPGu{!(Vx^ zNX{9&2d8re2)AHv1`590)lLAjGknL`N^V#YAOW6FTl?joxLA2 zw#yZeOB~<Q4d5jkk3KmlTCAXJFAmy!^%L zav!QgScyj`i^g3CC)9Nlm>=p+WGi!G zyAiIoDd?&c3*j06&H~w<*7M!yz@h5wG~L!mg+H28*8n8f%0w~Ixi zm$)^Y78Jb3WHK5vX##kB8X*SV&k_u$gYRwiqff%f!|%T=eZWd|k$RwABdestj0Xf# zF;!Maj70**#HR;`7iKPc(h}su%O%RhwCJM>oBWb$@$)=F&=HV_osWi@)(j8=qGnwY zxdZ7x(JvPHaV8PV*zYFe931XYDJ!k*Xu4j`SKEcGiBM|wh$5aRi--T&#UeZS1eh}= zkb#78e<*0AWqC43<)FYYE9Q$ST$9?;DA{}3JBy{>;=AR9&?X98;G4#0h!U41Gfr62 zIS+?&A2?aB;P%7Ix4llz<~o90yHk}tI}K)on=H2(XhN!s%?wa+rD|xDF>hwr<9A^m zXYAh5ovjb+>2X27Z6k85Kvbcw$e=D0orRY4`nJyN2k%zxKt!CL&!kPLdp2*FJiVbH z_E0DG($*`bJFHW1!2hvkoY`l;_HrMtX$B1K!RM1D9NAFB0pUR&3wy|A7Tt=6G$h5T zU~wk%`~T_1!pTmC+eCxLvQawI&rKRg1ZkmUqT8AgFpL~BqeR;4`tjesSm={5nKWT! zfKWOm;T*N6@{yHde9^ZP2+IbCMuPY-n$U}f&ma8o2b&Lm^d89DcOU%dgKxe6kM93@ zko*7Wi@*Eghd=zq&wuj8_dM60Up@W9Z}$gYe)k7I{rvl$ed*=5yxhFoDS)2*t0#Z? zL-OShJ^3T|e(Fi^WCfaVzwe{y{cgd7zx(KKJp7hNf8tU1;+H=n@BPT5U;OYVKKiRK zwhw>kZT|mZ_l3XqY5xB=Kl~H-zI5+9?)~cf+Wo7ydkQz^lz+`%ukTx;7=i53S?TDD zFp4$X-=3#y@su%BWGyH423k4iAhSpa`S3SfEKW?1+wfu?jT{mXVQ@|qqdq{Iahz;A zQp*l7CbM)oUdSnW`STPid0xY1w$}yfznn6%?3fj{WGrzMvX|QB!)6&Z&F{qUf|FC)3SJ@3zw5=qEw;=F7RF$OMf% z(Vt#S7z*RnSeY7{eO^ws!8k2bH1>+hCa5EtI?8mmDmQMp7e4r>?<~;Gx!9(Qu^rEX zVIGgkR4$Jk;SJ$>&De$rwem_BIlF%BTrAd0ESS4oOpECZwkd{cOSqZk>ob|rd)I7% zRN5$uEh$qEzw7#|D-jKKB#$~YB2F^n;sib6<7wi^OHR&7COY*kMxyq7t<;zGv4qcI zvM>}$Uu;0D{|4NQK3TAqA{cX4({TNCu2TWz3f1xPzkL4ZKA8P$g5j(D>iRaeOFZ%g z-Za(=pqwPpewhW0O14hhhFt*Ix|S4TYTtUEIg9&LhVKKEtWo7Dsb_DcyO>?f9YRHVhd z|Mp@b5B)kJ=tIBKQ^L`wbndT)@2ur{_u}qEH>n1C#F>a_Qgt( z(RXyolkCh_mx&Q8r=`X&cL-0V%&Gq1FI+5Gr#NLqUh?e9NP~3Ep_Sx-!mzbPTaw^) zbOk!6cUaon4}R*Mg@)&(7max)p3_*0XgVOh!&2O?MVuS8df5_%r}LHvz1xr3QhvyG zOWQKps%+P8d&@VUB3pqLn%< zsfb<%X^6N~S#3qJG_J&tXV2u?JUN=?WBx|n#E2E8xE|hv>7|ct&v#4 zWPF(f@6aLp_lKj{lbSY~#$IT5AOcl5sZ_8Y2a@BQFqs2~I{@DslCmqxTlcc)GNmhaLV4!Qyo zb-0f#2^nRkj4AZ`5_jlKeVkk@2G1RLe$0+cSi)c~0Gu$qOjhuzIveeT7Weo8?w0jR zh3{Da)w zJ_3RoK19f1lJ!r1*#%X)=J)7y>lS!YA5nc@0unEIYK3-wurTYe%2#g62Yp{d54ej( zj3;fq<7LQ#z~ne5P3wH3n-vyk(g=1?oy7u{=|Pwu>m8k#p1AC4`iT3WeH9nHc?oK zbmb;xSa|L}8)b-|Z*+G$5MKW9@|~8nZkxvpNkC1q8Tt7uy6odeM~E3oa}ErDy)2+dG*nQBK8-F_;t{e#P1iKHC1G*`5| z4TJMx)6VBvEo~jQE~1w4ba^_R@E|Xef>yoPU9Ms3wws@c19xC4z)*LjG-9Uj)*CFE zC-%s6l3isJl9$5W^B;ft6Q5}olmNGZkM}Fqf%9fA@cVd;Th3-6OR&%B*RkEljC*pB z#ms;4<{H(Oh+)#6g0_4cNnlZvW_19z=ma>~^k=B5K`m}-@e~jd!d`?I3pS5Vj>c0= z&Cdg39k;vXum(J!lB#9}ATyfERULU$Htt@imphl4)kH(-^&;I&0DI{Ijo7i7n6@_A z`mwuOo(e0MOz1^7Y}~& zat)Cd?Uf0w7uAeC9dxjvHdsm?qTOO|H)oIxAB!rGnO+i}{dX4&XAn4Nfe@3!I&b=9 z9TIbdi1yP6(?Qd-iq?f#?qn?S7SA7CEP}(Tm5vHF0n$>b6(+gSdPSN^t8#%(5BnV~ zZJ}XLARYViPhBjI!@fEQ#DZMN`?JYs4trb_C(um2p-+4t;nZk>TvZZ|{f9s>~|7>rG)RPCaksF}R8XOF4 zs_%?+?$B0~x|+2B^maLmpSEEg)i3|_1yymLL_9xB5su+(gPaZcbUt-L-4qD{X{=&J z$}{;iau)LJU1q=oT~PMi*9_bwBjGFkAv=ve1t=!TW8Z{QQ=~nU&#KD*aC&_Wz(iRp zzTbc{46uc?v-P76yLMG8r_Mfr6k+1giU&ipob8|e^LG}2d70s96?-krj3(C z1=+V^AqzM>t?JPqu(sX3{H{A&Ty{9O((SKtes>MRi}YTI!M;Q?p-siNCMSjxrry@g z3S`9HfM-u5n3|y{^uj+agCZ$O5!4wi?CGD=3H9#xgb?8 zL(#MF=<@DvL4&N=@-s#Y>yom7R9G_vtX~kPIE)s%b~)d12FO-Vy#L{^1q=Dlw9aR< z;Y4F8r>#cYj`|}N3Y7;KuGX^|e4MPok?3qOA2s!;KK-`0_5Z*8gTMH}N5AC@|MCmt zhd=#c_wt{;tX}-%7uoZld>%df`_H_mf9I+7fB1W8=|Zf22M9YY+Die*A%W z|1aO)-21V6Yhe5h{`18*{-d|N{a1IOs<^tt*5=ozo%rG#?}FeCUa#&zRdE{x?ghae zm@2OBKtJ*I$t=G3#I5yj$(A8ljBl70F226Xsbxa2f)F#x3+$s>gW{kP)V+IvZiB$OAh?6q zD?OUXhj<2L5Y~$mmTnbMQi8S2EBZJ;C^RFLlS0HoTjzGSL111G+`;SBNizehT!o0m zmBB2j0F5urQh6>^8$fX~wp@dqLD61$Xco6Y@S83O?%?$*fO_}{!6sNC1;HI80q>$0|Me>#zxd`m?~VXuEw@#HzYT)#xI2Q+wd3lGZ~lV| zf;)%@UVYAEVB8(S=R5|-yCe8qdt|=&M!z7qgL>iT+j;lJH;20;_?*W;yE}rT`+#SK^JO=XJ5qz$7=wE!ZzaY57SLW(-9)sQ85q!>Lu)QF-!*}lU z?FRVb8|m%{?jVDAo4HZEJAylO)7=Jva6xc~7{5!8?rjk8cSrC!j{$dg1b66{y3H7D zE(q=rd9MfACB1{l5E8$>)Fl3qSqhC+>gqbLHueJ^nKf{`lkhOZVB2zxWRD z%m3)%4?g_&9{s5Yzx2`TCx79|uY1OSq5bfme3-uc2X7Pqr$6@jr2oI;^KtXpy*&7( zAAG|xd@2b6lqR&Ti6$vfilO&5q@M?GNFl8w*x{71UURPSR53-B=&xqgL^|@b*9LNXpFAn3tVtLHVor14?+i!8lJMN^nIgKW=Je>B-(v`b)253= zX`1vI&Mb{#ST@BeRjn;3blo1ok%lmwDtt>oR|tbstE1p8vCVNxZi)E_v;^P~Fmw+T z6=znNOu9kZxEcw)V!h+>Jo1bzx#tZ&;NDG@vxmRzJ%$0^9flRyla65IJi%a>?B%jg zln5ReK1Hgw%+5G}ipl&c!uo@4AX{9YlUb&lOUVnU?P*w_W)N1bjLDicK?VLA1ClY4 zNl*@ng=SOqN89vO>juU02jBlGhGbfYv26FCX*RVMA-&%pz;^aw#zs-FM)t*o*0(7b z)&^IvM*DpBRl1q&H6k1J1DyJhJPsk_dKd(s8u4iPOBa;&ua2)ghdQTeVyj&Q4of@j zoZP_h{EMGrhzp;8dxAjhL{KZ_sd!wS^;A4B`E#5|^PR_6pxeXa5e2ydPrw0%rnWZP ztsd3K1t>)pH%2)wDSrUGcmfA2iGpu<)_>*tL^pDUMfGXhkvXp19EW?>rx=onGS`;- zU=ca<7C!XHnzMIEqpcRReuR&H)LIm)b+e(Mt~81Y#$&iM*>u*ZX3^(ik084c>(6aa zqsUO3&KjKl&~X1MI51~S={c_*v#M0W^GyZ9hkx!<4Dor+)xAGC<@H*hP7l)=7+?(? zlFMwlA9;yVd@kSf`L>0wAa9!&RZ%ce-%>$rvNdfhi>WLI9_au{hP`xRv+b$xBlT4_ zfJ(?=OhOarZzL6k-;9fgf8bLL$!yYZ22gh87ob^XR!)rYv@njIh^qFm=Ri-y2?s4T zIAk5KAhi}E&)YNuHcqgBJ=(e^g&(t+`%3Ia|8ep#y-vxx;eRA%loYi~E z{~3}o?RyMUlW*uHs4&GH&#iEz9UE=AHu!c}u4X5As)eV>*;Q%fUjY$NbhurG zN|uCUzeEFTx=lvcqQ+7<5}VvKP|U1vp}hC#Qw(uAffkG2^Zmkh&V+ZiPLjBzu4XysE|C zFWq=6HACHKHQ zCdd>p@+3J1`N~w>L5*`PR(2YAXEL=odM&u3I_5Lv6@}sZ$T_yhHWdP1?pgCjY(Drs zpJGTBKuV)r#=-#$N>COI6pVrp7vN!>!%)5)x2|j<{prl_sVlDne)VSL{A4D|{xl#+GR z`mi?GQnCGwbOXfGd+#wM<9xOrLQ*@=l9V0!6^ujDlwls!X`PZe(Gc%@9MxtRd{hAa&~5IgixRD~-ifPBG+S+?EG~7vtm9gLX!n zLiLGq%OlDG;zp%Qpe)#irT?lNO7X}!jo(Q-CrJG|x`E;Tm)?&L6!OKj<2? z&w!%iS3nIQIjBHU>Zs-{Xj4#bj>G-`<~@cO(hs2ezabFSUC<5X<^=*Xqa$)FRn{eP zNJAIx=WrujO~)u&PeB2G*9jxiPlVJ#;qg4}g&>s}Zr@MZ{1{;3EVFNc;lF&3!Bkm% zO3duaBo~02tC5)SsO0o^JI2ur?eVhG^?VM>iQaysM1Q^ml`XN$Uas9)qnx$!=xNJKs$X zM^9Oe?aDmcSEL@tTW7d=b6o^-hqoN-P^Y zR^-=^Jmjx9mN|0581Kny(}(B2zo~oo?AyOgtM&Fe)mLYXPtNe-h%d)@sZLr@g%EP^ zB+&cAfklQWWD*cM>;4p|x6riyXP*v4f^y1WPsU*qSxwTEQOmgp(b8rVCKTp;w*`}; zKS$t-RIglt3Y9eK?agVvp0o_5oKCtIvWPjgAp%ez01d=Dq8F1@OlAhUnVc-H*=tcoaP96KgZqdW4`|i` z0m6Uiel=OCCbu4eHYpKR zD5i(?_DnY_SYZ0f@oIILbJ`XbV}&(ObUa8S5IA42iIj?3zC3SLY9SHu%%nu<78B>+ z2dU~eJmGwP|CfC5@7(+71JJYoN1p!JQ}W4=JXfCm_=o@a$+v?nzxn99AOG3Mi|2p+ z-XD7OUp<;U{F4uV<%1u%_tCQ-f9bsVi7)*0<$u5Uj?bQQe}(1!Kl+Uyj!$2Uj#~<& zp?5Wzp^HsB>%4I#nkO#JS5b|nWMs(CxpN*W-VSc&e9wOBJq9!B% zodg8f*T)b)=Z&A-ui%)(n$-kQU|C%3Mn75zYT?9^&#IKS znff68<|?TSCDYp2g3oTEp1AQS?)~SVek?oIf^sR8_MHKU9<~f_oQbI;pCEV6cfmyC znK?>}h3Wz2+Mbw9ZKlI@%%FtIR&gM2eT1hQ)mR^O-5{n+F z-q9N%1BlT#Fg*Ko7&=xh)1AesVCy0(=P=hVw^CGSeT*tV6l!K#V#Z7+rx{mYEr5FS zZXKU8ext^qNy7CE2+8*Ge7Q8~^&B&nK)!yfY*zH~mFrk)eB7`GE=r-&@|&C4_r2f$ z9s{A)$XP`ZL}nnX4WU&qNjZL07TQjnw3H-!E(YpwW~hBdrRTM$$5zHFOgD8t(i(Ksm(SKK-l=vM8gEwflCV9KW4L%U@&2Fd9s!g0hWyKZ= z#`Y`I7p1KI8XDu@{{A@Z>eYBQKc+L4nytC9#A`>how@aClI1vy*{<}KPE2jI&f5Ks9bV2}uwyf&ZK`>bqQK5~nP zpLn4Su&9zTYE*rXH%^zyS9)R%77NOlPgKkrc)I}5Hygm zrK}FoOg{}`fH+#0r6}gEmo5F~_|W>%3-h7fQUGa84ADjeQnz~BRBH=c5@trsMp!3$ z)D|4m?$cZy+{$i>p!(DYBlKgZM}cdNzzA;U;dMDl?1+-&xa& zwq(GhP-Dm1+7ItNABToQ)-uA2yD3Yu#e~zkLTE6>iZ=nkRk0u>lL%EHZ4WyOsNN`L zd%KL=OlAP$7;yj!(4tAcO;K|RbFpoc%~-tOujo@CdM9Gu2&hjI3OQuJk>AmLxc9>^ zw4q%XeowG&+_SY|_hDx~kyu7Z zY;ev$P5MuQfhmZn3~!$(xhR_WlHX|&cUohOww!`f{ar%wOWvuD&x_yq*g60IjbFI& z=sWL!|Ls2me)tXb@Ar!De(`}N_~FUfHL^gj&fK_E`VN6!DGCJt`VLYyhO9wzJJWKzIx98AdPaqI_67|3MdNzlyRn@&_|P#_K0z%;TtP zK&*i}xR4C-rocVO8(~N^U(xE5Xh?r(n&5m_ChikL;3u1U+kRRU*?bUIx-zbI{5ZE$ zV;-yw71>NhPv_R9?6{>RK=Tr}DV&SKxCX3vGZX0Wh1z(PgaY%FwO&*7WZ(5Y^|$!$ z7rpP)*GMYC{O3l^Pv8CD?9h0P7S( z&FiMrB-8-U?AWiTtle=HC9t7wI(ce!+U*bR)TkMSIv)(=5@$1v;q*Sh4Gv`?2YjD3 zYG$rI7pY*FvRKb|1I?b+!{P}&Z}^pBIg-FW1uVLQJZ#h0fE4PnW-t5b@b&oo0T%I@ zM#o$4^Z8#RNBuK)>h>$08vS+2{e6vG?Q^I8YzBePd#4_Jz^6Z2-JO2Fy|3`4eEx&6 zo&|xJDp=l&ao?H5ScI8~I3{Sb+s@<(XD_?q$WGVH>Ipt-mknT^-2lXTp+j?Lr>G;bEjw0JE6z%=m}%|tfavn| z^a1x>-!If(`|jg&pZ@*d0CwLWCARE}-8cG5pGLmm(+v+*L&sRL+=BC$pZ~k*{%6xi z+fesaKDvBrY@YaG4-l;gOQN=BF#c3x&2+do8#Kt`3}`^7)D7%tmLMfHNMVPortr*4L%>PR4(2u^&jVlR z`uJ1U1!Ng6{(71lebDti|Nq^;c;n+YzW?OWFW>%CkL)|&`1n_Eebvq1^f)t%-Iqh1|~6?@T^0UXG?B_&c>5%#DXnLwhQ1eB+#aV=%Z%0Gy!#k zKp+wt~gY}8eq+VNx`a9hc-z`aIt?H9k$ zhHzJyXtgkn2$rIPrdQ4^34p9l2U|2Hj#n zslBEENb%mHi6$CKCQc3wWgjQcy~ZwcwALu_P9wN#|B zM@V><&X4aH-yA(}12W`tsUy$&p}j@M`bcH$a4ijq43(v!;w;wigzNW7nsLucf1XM* zP~(^|#XOE@Gsq*+me&U~LkU>}nT}|*S#}5QYTbSf06_V?4TI@I=t_c;y6y2?6##W; z>J^xh#Z8S&h^`kea=ArN9V9-RDXak@lNRqPjc7VpKQt&rg>A#a03+lCa>s3;_C2I~ z3+U7)k|{H#Y&sZX<)${P<2$9hU;VrdfFq$DENA=c^`sIb7*@9v*CIz*Z2`{B-E6Xu zoK0+0Fn8vzJShRGYASv{p9__?C@l z3lj~NoXC|#v{g=1(b1PYQGo2)jHfNYG=AKM-|@T+T$nDIc#LWkkI!&Y^(_r4R4ntT z30c=_X^w|I(&}k8ccyKp9*f0YZ#LmzV<-2~bO-niQP|wc^AVd)=HzbE!q&Q2p5{a* zuo=U%f+OVQ9HKcuHKIgm-AT;#Y^X6pQEXJ2 zJ2M%?pBBOfs7r$Y&*U0ZBp0`2bU5v(deCe%+p&;<1Rp89|EYIe$w@W8ZmUJ*= zg?3Z@j7bC7VzYUQ&m4BM_T0?|wIg$A>APH~rV5<6+N6LsILL!Yawo#ZksPK8NdbI8 zavUEX{reZ%Kir-th|eQ&McT#^QjA>Q@-6!JIHX`)71!o z_l$7a(0Uzi4WbBk%KSJ!-2d+9ZJ>v}z+P;sey?0C05mLUEJaz$B)CIYLx}VYjEpE* zHrnOc#3VYC-mV7dvbvBvOGT4M%lWWZ$8^rPZFhw1ND*3UWj{WxJgZ11#H!+CAX1wY zk$v&5(V_0|3#299Eb_(QL zxJk96RYQZ7KwmWQ+=3S2#uE9ka@^M9_rxR9a0z zRvIo!+fAp{L1sSdb;;A&(u}wIii;-{xeRovIbJVrJ;xw0xEM)=?4SiV0N>&iKBFb8 zXz!efH=WLWYfpvNU^-m|`q^mCx)UHDMbFs90A=;UnAd~dDi`Tiqh(0C$}6f~g!)Xz zPI-JD2CI}9PFC?bb-n*Cm=3Kp?j~RHP|mdZOxr7`bw_Y8V}mkP6hkUN*3P=#(d8Nc`s|gb5RmK}M1M;-2O3RMg z@Zej(`};M|LD=8_-@5g+Hy%B?djtIE3;(__0$&(`7e?TFzw_PSbk7mZPm?DfElJ&X zy&doJ$ahkZB9Ny$2OLAbOjR0wi308ZTID>Uz$t)2aRw^*ju&rQ*58!I@%o#Vsbl8O z{v>?=zYBjazWeso)OF`a=gtO51MZ*yc5@SNS_lLWU52a{=8K z6_d|R7EYS^@n&Do`7PUdi{k6m`+>vX_xhdX`<^}P_WA!TLZ{x_*|+}XnH7gN9{#q- zoD?+fgDS**1nR$1R2|4})Ffbj?G4#ULiWvMjLOX>oOHbg@QqUe3$IOPyVN=;dDUSs zXvkg#*|CvM2OfO0xlk&4v~PPO2KvA>x6G!mjrvcYVyiy1LJh6zqG2wMgmA;s*GbwjzXPRMuOEgMuXz+S1MZk`X?#U!fFQDpl-h%`-5zt#<1*6XUP&J<$EIWMSO~gJpv}GqF92`a?13jb4-pu0% z4GoQbBn9UyB)BfgyM86CYNgF49nF!U&0vliGu+uBIEW9RIWO7=E&pJA;%zM6PeMs{ z3mmB2`s*azP9OWxr7*0Q~k54PmP`|y<&OIgv`TAT(`1HBUBa72Xrwd)f#0;*ut$&h!tY|JZwGRs=a+3a?^d! zuai1){`@u4r#}C$|6%0Cwd|u!rtL#W@0EFXf@0AiEE`ChOjVF7BR{p(4g}8Lp61(H z2iUCi;T11_J7ygCZO*=|5AX# zVJOfb09#$q_4E7xM>psjKXv2IKi+Hf-68h+e7Aq(_IE#i^d`{bd$V|xc=Jmh{j*0u z`sjNef9oUWjyvn_=g{SdavO3 zV1EC`tv~zL-3MRy#=m{zC*Sx3Z+!B`ufFl*{=d8b)AxVy=GiUb)>l9N`#1m9&A)W> z`){iE?YDmJtv~Ws@K*oiuL2Fie|hic?){N_!M*;yyDxl%UHRidjbWH{JCmiq*a_6Y zb(qO8%Lh}>1Oq?AG zZgsVBv)fJs&fy-sCy({|8fphko9k$H{6B|&TtnsKR}Lk1!4}^Z{II=Dg^Joy6(+hB zUCcanL*;hlTijF&=CG4UEnBgcMYA5G>A8pT8Qt153XR3E zOrcS3!Qi;`nR<+{vD-EsNt@7@*5qutQ|Y0??6g@m7;GS89j3#k#qms!SML12gN?Oq z)f+oWdIrogi>AZP*JFsG?Ro)rF;TL!DX@WPmE+pIeNdqs`$}+njH3QP!)XTEPpg zF7y-O&?-+Anr?_}^wt)f;j5-g+GNt1gx;!KSkAzQc%ugNMpA{HV9!d>8VMC^c!3&W zK?IOkNF+yti>BUiFpxF@(8GzQ3w0T~C;#G5;u>#|Ka{wJ5@mHL0f@-PS>%yLC&-Dp z)18kLpeI|pN(+ z&`2H~v{t@?Asd&S&+sYT7ba4c-SiG6kZkIWD*~M@uoWq40}r6mhq*GK2JLJ$?+$3E z8SRA%=jDTc@T|mmh*}h;h*3v1m(z$B#!#P0%PxmRa$vi#Fg4YQ1a1*;{ozB2)`p6Q z(6F`r^r-J}+?{joN3*N7v98Iw%Rm>backCqi_FSi zA<)go&q_?nV$LHFi6&j7Lcoce8T37uUJ0DKYL9n-DkKi&W|VLz2N5-4IjtPK4?#`g z*5h7iG_XywW6N$60XJwNb(F$+UI)rBJNc&v8%C#!0qfv~qO1&vULm==DW`ptZwD(K zhBD2NO|-*r1=hWNP^cB=tm01XQ7#6^1~AL@S0LLCg@Aw!==v1B#{$v3wr-6 z4mQ;Bgz*L;>=Z!ado>S}tvx|AK*WMhNfB)(*m?>+NvPQD-}uEtiE9Y8{-tLnUORE+ z?z3%)@cKf=&*WpTA=>re(5h>Qc0JpWT|>0%|2WvVhT!Z$Z(g9+rsF|=&xDBp8l!@hWKPcYn{b5`)S|x2Tk^sS&EyqY>?wfHRVrmy>Q_ zz!pV1nOf9kqwu-|-c;yP-u@wH2Au3ch%C~*zN*-sxz zTtjj8w;xJeLvi-m-W5gjrX*LEO~Xm-uTw{=(Cu~^!@TA6{Z6X1nYGs%U^V&R&!5{6 zuAwXK9!gw8D-k)A@Xe8fDyqngO?;%GeA)4UczK^;MvJx?3lST+3$F{lYLxfX6|bRi z{`U?xuCZ&M97^N_KCk;yI_pe(^kjg0x@94{PJ=Y}$;#UR9^tSTQXMDBZvT}-iDV;J z)pTPFXVW#m1OiPwAi@-gwyFT#VbtZ3@!X(Xnhcd&f8$w+j+0^nx-~nn%g=U$a60r7 z+3a-qtk>F%p+tio42;`o_<{Vch?x_doU4kG=J&$G`a4y7N13|MT1S<1f1PFK>Cb zzU<}?-5j0#?Bjp_*a6kucx!Rz_x*<4CVoD5>U-{f@`fXxyyC1E9UR?GoRR#(k0$c0 zDppgbDG`u+rJ&&Z8_1+#PC7>E=~j|Sp<9DI^{E+%n?zra>RC{Elx}`7_YoO!IiX`nZ$ZwHR=fh1FWSF+Yo^KM?+9i-nodj4$Y;K`a23YYh-okn$tp~N;w zqVj;^Dcr!4IH}24g_X%DK3%PRKy55=)+{Boal&h>sg& zeeftAg3fmJYg1`mZT;;ltRG)peYE6Ur*u1W9_+5$BVw03g=pWVgHJ4k5`$x|MQgBx*`0j|)$Z|n0 z$24-7(*PvH3OYB$%Vj;Ko5c3%d2v+RS6^ZMLf=)O>Ynjp z(OdVIEo9W05GK7OMRrgWq96vGI#?OLAXHsHyqYKfx43uV^Pm7<@RxzJ73ONo;oJnG z8zE?@ovZ1EKq=K0*|31D8ghD!)Mj?!DJ3k$rRPi?tFWnJ4H%%0dik;U?OfA7|hBYVq}MQBpG|)8^JdNWr*GMb|9SwLcUT z)l`EltB=O*%igzh!2=iL5z#hCeUNq}dOa*Bl+pNrIDDOnfZ9+Y)6OXDBFc_kEl)i+ z&`Y3VF`rD`GHv+$nDzNL|28lW_VElxj#_Z44&~w0StoutF?=bn(hsI7B7@)bep&dq ztele@BA88PvBqc_Kc0Rtjv|9Ey>#}&oeRCL8($R*CB^K~3Sx$Z(!%LB>Hr+1KfoUIm~_A6ry?J9lymdxx)H!f6fv{64xBg-~q?c8{eaM)KXEG z7@;P9nU+E1;*mGdn}CK+&3U0ps!6OOy{<x|-`cM?DmAB8{St1AfOX%V3q}Xs z5sbA(L&!q#Sf>T7m`wVef;+{6Af1*PP^ulKyK!Ysj?-)3x^(@7+ZSdb9ySsWj>WJQ zLpS<%EvQGM^Z1JXTzHrDbSW=!Wt49FTYwlVW<#~N8%~fGJMY<>WVS{!qJdgWVu+_# zT7UC$>*$5(sz$>m8T7!JPbUD$Vp4Y`8Z&>!Fwn-uP{7~OiGt(7AC{Ujs_lE$P5sYa{8wu6|q}S-H4&qo+ zqV=b9l&4R}>u`y2As5UgZ$?bG3oFh3HudXY=kw*#ej~l(R-Q`2YPAa2GHK0S7m#$u z$Iy$fy|mv5d|`zh&L`bC-6@(kbC`s{3lJ7q&A>PU(s3%STk#Crm_Yx_0Xvg0^3>gs z-mF#@QSCMt_FrFhh4l;barNqce?zHE25iKI5w=|Rn2x~3H zQ)eQ~6xa6(xioo?2KSjq@BAw-A5HkeXl5(TDzGK5OuS}P&dKdDGyBK+^AD{zUjCVP zVVidfQV5FScxna1aLRGjW_BE09z0-fynVyIaqs)?-M{<$?moKn?wv1sE4RP> z*5ACP-TcQlzw_i5PJYLYe}2P$>qp-D&bL1P_~#%0z+>t0TW|hnZ+_{cA9(cT_isNO zJ^U};`0o4v@&50A)%?2j9dtSucHk+47*>jPtr5dymYZeMrHg55wBGh$uV-RAm(~5< zd`E4>Q;#Zr*Vc`&)TeUkMA9*gyLW~43$a@p#x6VRMqDQzIfEh)G?^IMe1IFsh|EnL zPTR2$z9iZ7H9#KSE36;K?lW!G_w%t0$i27g2!VH_&DPhLICRZmlHi57Q3(>r0V%yG zJLQ4koHue5*(bQU#L)cgX#RCBw+NFetEh}Xp++!8+;RK_GG;)y`zywGH`+*zUn}v z-M!HFXL&i@!2p5K)tCyDqZ&^sYa;t%Y~)60QYDa8Io@@_m)mS#=<$0tQ3N3IEUs+G zW1HGWF?_I*n5)y#Dg$~q$KpQFrK61YF2vWsjjCZg?+n6Se04m^ z-*kC@x)=Jh7W(uUkSPdaSand!OFS9Vhss!9Ni9X`>hz$NVm2@HrE)c#`x`H}-nsAw zO=Sd_a$P@Jbc-Id^rt?9xCM)Jqn#+{tu<6}hG(hOjCHRj)qiq@^$Xwji6CIQ!;KPc z8kUQ>Zy+qlE|W8ztYwJva8BY_RhW@^&pn+*lcA|c!;t6Ih9V;Ncz^U;udsf+<2obI ziuVOD-u(e2NZ{M86MFSPp9teB!zIoNYzEE7QN;V=sm04xljlUfj5g_f!^XwYO!|f^ ztY7%P80);B&3v1M>MYc&d0&0=nLBXrwTx=>N2B~yM0B^s{J6S>70M^)2+tBIMiYZEE zx0CS<(0Vb$;8@q<6PH@=buRQ>011TC9gB=rKTAl9bdkLBXm>kYgnrN#f%JgL7=21! zfLX^d2;KL$Wc$p6X zH8EufIF>XnCMm7}KJ3ygBU#QeGr5^jTUxuI;c&U}1Ow|S?I0fbvrAi876u1YZD;qktl zyuvAuYdjkpJACT(6!6WArVC`r^x8avbL);fm4W!NFlB+VV;hFITK_b|mt`%0Q#*Gf zk5s_)aQrTb%dKAwNR^B5MBH8Ep4jyrki^(E(&?HCglq|Xg%$kGUJfJB+ znXhH3xtT0HY%YuLsp<1sS$eUN1l|&`VKm2n`R!L&zYvUxsM8-vQD5W0kpOA=7Q$mV z?(=Qk84iWs4&3aagqh6+s(o6|Jvw7EcdCw-8Wf6;-@gpbzq>j;d(4q0YBlUd4rt><6m=o6c_Mw`2PoyjOOlef-UN`r1!zzWLIi(1p>XiFBF-K9B=iab-3XW~$2w$II2=iW-k6$7vV@ zKz}+e0Ss3bC3@nxM>hNC{QoDD8;}0befaiIfgir`??aBj_f_wH#U3gC&i9;L2ibTv zBKsvk+Ls~4k(Vk9e71qI*9E>_0D)cyY5Pe4YdHd_d4871X}u`?%(wi=&+9z|>T`q( zeG!NFl7;}xWagPEfZ)6cVcG+ZE+Sbk01rPj*cR@-QXjyjtty0oitUr`plUB|)__Go z+@J0mNcQHEOff`8_gUGMucRG*B}#P~Zdc$+m7S>B#$3)Tx?K;fr_dV6e0TdBz8Ohl zzB}k+4!sXvzM9lM@JR6Ar6uuXwC4v7Yk$7q#9h%bjU0ccaNvzG+; zKKI=(Kli=wI61qva{xK(>;QLW{{Zv-5P&q>>BguqjNRrWv=EyWlx8C zCdw=EbPe|Le0~ zeW2gx)jj{)OIJ`}@c9$VJr9Nts1g8cv`1|u;?$_HxdyX1;SZ1F${JcEi zBYCf?&|ik{{R|dvbGh%``9sf$H9%iqT+C5m=(8GOUM=iVm-yjM;uDjfajxZGr(`ZEKl3&TJpm<0|yTj#Fg``~%Q*bUE+Yuk? zOI+r)$fQvvp zpFI^DB=v_F=jUCQ%yV|CP-wJH$e1=JT~5*5XY3;%VmT*i-1VabFhN88itLhUj}s^% zHlK9q%B`VxNRqY6Vp-$OyA(UZXD9}gv)qPSP9d#tI)Lz2@&wpucQzw>(2lHr0eD4U zxpM|oUVHpM_}ZP``00~9Qvc+WH|Kk_{xhup4?Ots`^)?Az5jUcZ@q`fzx(&^{_x%H z&9A%rt&jgC$QAe}cmC@;8Ixcxo1ncE+`^-H(@+~aS( z^?PoKx4!1)zrOk7dzAl!`CH-1A2|892X27}pMJ(J@QaUr$)Y9coxY2&BpmgfISg;w_Atget93_=<}vr^rw%20R4AqF zpqXn97k1cQ7%MAXS$Um$(p}N4vJLX#y0c09?i=5BULrGW3B%$Rx1`78fKHf}0HxLt z#e1u*OU(D^m`$>VJ7F3=JODUzy&k5Ev?}yq4Q@tT$i$ps0LNJXs5)9LgN(67xdgoH zqJHzAJnK0`JJmRD=vG3TeQfJ>SD4XSb=h6B+R$pO4J2&HQ=5W*cmSg2Kw{b)bxNt9 zj|U(}Uh3!;wB9+K<#2;~qm>Oc^*D0d+m?5K=3t|RiR^SY&T5Wv2ID?AYt7vhW=F+HLFU^cHg(MT~Wq!JN6f?x~*Y@yj8W=DAiUT{M@00s4tiCbi7aQvuQO5 zLovj0t<#x}(b53yy@nfWrDO?C{b@r|6W-8*j{N=)UV zUIU$-jz1E;ve{O)O5&)N$u8_EE(K01>~_%I0%Y(5=1_v^BWMxn1+;E?v#g;r&MUV` zZ`at$&eC}!#GnOaOsiD6eVA7{<$>G-r<_fC4M>ZUx2r+|L1D$pqbykmlcBTNEhK#& zHsS5Rd$2*I8*|0$BX+>TI!nzfXD(olZd*Jvi)$v{_NbUyQ}uj#=ck{Qa01lQGP{WR z)g-N{#>dK>AMp(1j(HbhFm4PK2g+O^-yjbqhH~Ae(IMAREtnaaj@`-n9Xw|m0B~)f zKB}wMHlI~0N566ZPy(M){o%;#(~a#e>m2YhG*Z6m0{L!@la-Qb5<*7Xs1+?A9B3kP zkdfQ>q>%&Yv8^ziaNV>XJ4%Q1S!<2EQO{cr;*{B~;PNhfu+hhOgqUXAT8kG*&4uCy z&CrDLbay3=u_4!G00V;8R5Wz|fRzNgOyjYI#Ur(!H!et!ck*Eb?jBXQX4Rcw0|h4{ zVwz4akvIOYgN@#BhGs_45R$%HwHg~kF-Q2Fq%sR>vMr))1?ZT~Q5@d;#GwSbHPBYuLTxDBmQ%6rBs9x+ zb;3xT(asN+7`+Kr_+$vXw}0py`IsZ5jq{Y`7rj2!R{gXVcYRA-CRDuy2u>&HC>hpC zSG16Y2NUovMIx1+o8r7T?NfV{z(yzdbl}llm(bR-kS_JCjAH%dcOFVW3O3?(bTE|X z>~=f1Th(@{cTjXJpr}U(iYcx$6HTjL`~L4bFOfAm)z=45-^pZcu$AYta?wNj3%?$) zjfRndL&v4uU0B-k{sC5+RfA2XR)u75TXl;1$&sS_w)s|W_V|BVctl3Xw3v*@HO*7Y0H!)SaK z?K{ubw9VWdZpj1(35Wl$k`s9#T0`N7%^CFxyU$ ztv^^RzLA8ixxwxp;Fwvsp(R_hfxf-A*4v{!O4+u&sR9+-4ODEVc3X`~O~~Tx)(;$d z?!g;-vSqWX(q~3b&tP+6mmaw-J=QH2dXsRPk9i>VmcH`~hZ62k8=(kL6Vg^o)-c)8 zgoNmM7A-qVEu9r}YArrho~lMoKX^8}j%j-1(W+N3EKa9^Zp#{O#UUp_&bVE4SxD`# zanftoM11pC4>s)0!f^7SQfWF^BU+uWJ$^`710vFvs^N{tvOjKkVo7E9{>Y((ZUY5I zg>vd`J}8oC;X^h&1if$bATNLx?IzYuYJ?=4@ZJv}N@xnmoI^))JsttR0n@j+CDB{; z2O5*9KwolZc4vDwZZ!)Ze9L)>9h`(RLWQW~F5;b#$`gEs#pKc=+pAq?GY72r=&)N$ zk*GhsbzUM@XB{M`#hwOj+L`B-G{nUopZAwgNu?Q5fUvFDoz=3UAOGN?gza>}d2bDv zL3v1CmMvOX_Q&cf0^CBB$QwjL|}N6^6rkmi6b$K8x2t`g`~XI%BhjW~nY1=-AE zlF=uvRx)FHdi=0;C{e`dOsBSmjAzq&;pd)!8Tlj-woTgFdZWxi)zCx0wl;p_Fcb5P zE!57`Z($IM6XMq40TLo)7p?U&!pa2j(N5D)%C{Cu-v8o*jd(LyN6e_h4r(~X$hm>V zAjQP*dl0zMUjg-ubvCtHZCcT9{;@-eO~Dr<#EI?cAn`jn*kdG9Y{2mAp1|^hQQ{>9 zvWr!a#q;3D4l7hR=;*QGR$hOicFUn14Y~eoIW}vzq?(z7u=7!%S3`0!4nGRA&n)X&*n0)xl&q~NZ z?W&8cl4!JCQ6_1(43~EuL$$~8oMHr#TF@dU^Bi{arH2wDx||Z*jMdW?$S}|A-DF(E z2-azKwK*;%Pe)DhLI4`n8TYMj4@D9i2eIr|eja7HJ*#t9F!X$R36Ha@{ zwskU)u)FfX2s{`Rl@c!`wFBmRpNY~{X>KDfh~uf@^jdPEh3#++SMH<#>R_WYm?XM6 zNT9e|)FKxqHM{Ap6Wbna*cyR)GAY-Xh$A}A~wV@EZB7hJ?_pwINxe)O$Rz4hkfzx+6S{P8z`_RZ?eZ+P_ckACl??!*86 z;U9iDeDH4`{D}v`8~^c*9|5NWkM95Y{oww`?)}WY`rg;y{YQ7d?{4SLzqs>lw!XH2PBOu)V zq&$eIgV9by`bfs^Vq@q$-DGW~WE>ef4GS>mTQ(olbO?i^G8T3?o`w zMlt&!JQspa{@$e(UT9zw&qT}{m1}}f$RU$okRv0I5-U>}Uv2{?4Tp9%ogaHkl@Pw9q)3#uhsh zpe~Lp{M#ROg+F&`g%|R2uWsSry0pRzo;;_b za$m~)V^{tym$mN#*6C`o%afnJw89Ia;Y#()lb^b@!V5jFG@Yx=%%crV0USo73vyZ~ zf9<2L@PE3r!V9q`2Fg{X5;;~?G^|?I;|qTBKVDklg;;ZSh5y6#DioLIG^Kqdn{Q`6 zxDDCl$HD6-FRk!GthrL3^yII8)D`}f>s7b}f(P14CDqCazBA zFKp#jVPWf`lZgPvfQjHn=fdv#zx${w{BxI9cwqtH=Od=V*0XfXE`8cgC&xTYCx7;% zuJF%XTH%Ei#RU3EQ?^4*2#|^^8f$njfcDYx9@!U$shgPe`kKd;wwks`-*qJ>O2#wJ~;!D>#uuRcxoCY z$LNGMks_H1L~vex6n?(z?K}>h%|R6Ipw9F%NCEoNwB{ObnQgVPB+(S6>n%C0X<;~% zHe|o8vglMVQdC-TXuTIVdTA!qbt^c^oqTyF)RleSr&(St3Uut0`%*8a`F|#vP@R2c zpRG)&mo@z5nNUFB>HRFJmn-4*d;NW4<4Y5p!MVv(WlVWbP!BdVW10B#0PlE-7QhL+ z*?Oim+iG@an}^C(X@{U8FISo(Rx8IJ>LxLskIUJ5H&i-9ezP@{Ua0hu;Y_d)h(%1h z7J-O00Kc-nTS_idStio=IoOvNatvH5)m-b=EaU*bzI$rF6J?BnOiJI+%RKZNY#g~|J3df+}O??dOd5H zwiA*Jmb6-WRodV2WCvZ}qi5FdejH5pqwG~8r^rh)Zm)qZ*ax(0Ydl^fxA=S=`@k{)C)N8uM| zYX4P1lbVT{)dN??(Ua~@F8HNebd`7%bTf4gWupyTf_Llc9gs(Q9yDJbxUZHl?V`QG zd*1SXKK^&Sea4RmlX@$x+s&@2Im6WT2$mC_8LldyqQ>CZ8QRHRu3?rhEZOG?xFABG zi_3nFl8!%+^t*qn@2&mjH?2+E*=@fmce4DO4ow4p{xCX<91x<|PUnV2J;A057A&7s zsOHC^VTVtiPBJk2Hw+6=>rJDO3i>f3^?4k{lRZJxz4M39fy0_HV%1-hVa|h0q=Y$_qqlS@8#RX1)z6X zx4pj1t#gnqrZGt{YI;!*wvxWWPD+rzFbVrAgr^6cz4xZd1+mS~!rVPRC)$tD2jatv z>b7s6i#dWPUN2$(T-Ei`ZDJ49b;XMKii^AU%NqXjXmj<-iS!4rRL@hxV7zrkO&SQ* z&==rHS*xUxKdJfyx?)OvQW!-yn?ud6ebqMcl~7YLju46k*GBVZ=?!qCV0kstHf11I z{T@0nx(yI#j^ViGQ->}AG0d#5>E=|KdmzIow>wUdG8xFIT=P_iUo#`xAf!Dy1&r3hXIW_5S!7&Fvc&%-=z)^1714W`x>ZkUq`BWKQ}Mx|ACjc&;Q?l z|9|DJpSW@N-`@@H-njEacfS7ipS=AEpvZ?j{_BqokO|O#^mC7bhyTmN_~E?=|M?p~ z_~0wwK<@vE`@MUA``+T#Pu%*(n?G~&QzyT2@_i>?b>p2^e#gGv4 z;@o2CnH$&1e%Hx6pWGYAZk#R0^Px19qeT`gwgS-97{R89MY-c7MG_#JMXewM9@YlW zeWu3k7{uF6oIh*D`kn6pZTL1Y)af8W*69qcGL+pJEN(WvOpzzErG$|p!;L{gvp&g@ z*@g_SZo{*^%0=j_^Ns=RJq7k!{Cbd$;lXa(0L3(7X!)ZSLY6Qc6-0M7Utw@M&<8^( z_sG|__dakwbKlYTV+-tDLpJ%f?Ys#0{atUqqXAQ&_~dWCj)L-Qn>rlKy=vQYd=0lB z0P~&szCEDp*HD0d?e-kbVb<@Yz{Y;s2(K@tb+w0HGHnuIZ$E7eXseaxXa$ob2eSXz zaMP`Nh6KTD0FHNZ0i(&pov1|G#IJ4d#c8tzcJ|Yz(-BEW6&Hg5S`0ey!0D}rLFtW? zW(!2$HHs!jsL^rST?Tt?J4e$d-oGX=Z8U(xkitH_Q``QgS?ow_D3Np8$;J9_HD6#RB|?!vkh<9A8G>R>U$Nk5x)mp;@;#&5X$N zA1E`PWcG`%K&LEatD@34rar-{5#Pu*?YHJ%G+;*VQv16-jy> z#{gOV%0w!=>}stg!>W?At8HK6H$Jep_Zx*$b;@HwR(u*#{VG~|eaAFlL6XFFw4f0% z>G*7kjLU$0ZF?{Jjkh0DU}v;}vNoM3Z90?pjV1%q!sXuPe8z=P1AH=(zGc;Nzyb;n>K6Pv}^*34qU1* zxPgu{s8evnsJ9>_J z5VHYqv+AaJ9UQK0e}XfSc(-oiN8$y5_)S0#4mNiP(4%jjJ;(@9>lzN=8JRE>D|3)3 zh9I^DqNlV-NQ!%TP|XySr7cwWzhVv_*{Pa@T^@^)55vgsj20q>G}Ed zm*$1gi_Wd9hs*a2&m7_1_b}c$C+oCLyblk>?QzbH z5aQ>Jz9UbX80SetWQ5cEFwR*g>%2{zj~vR|Bb^;#MBhI84%H?`di)R`AwBtFq%%&I ziI19Xx5qd$LU;Dw(f2v$O^or_VQ|;f@+QXR(I|DYa@>SyTkG}^N7w=njJ|`DBRpjo z7)desuoPGKfTTKVpK4KL7tN$oqLGF>h+YYzjglJm=CDE9FGQdcROzFl{dznDT+Y-a zSr+s&Mkn#xFIrYdC|&UAd*3C8lIGTx#O0?9{RdzTh^deWNt{(@@LI!)HVYh5D1#VE zPco4wV^mQh>T0a*KzBp1tCJi_Dkl9nr^Rnp8!xnf5E z0pX!!u^-SaA!8y~ga1pgp7`|<){~sr7PJNH@*Y zL2{Du^f|1Kdet5ifgUH;+kir`t*1f7e^S69j^e6z0v{Fy<3nj>?z0mTqz{>lcqNml6^@zgf$C3MOE`C zsMZmIIT&pZf>*!U*Xz#z>#>tBH}GK0IxN^ieBn2PZI_iTMn6#w+S4Ya^Ub-8C`JlZK~k83D6SPJlLX zK59k1Ezz?1Efu)_VLj6S|6{d*V7}6>+hc!Jy }qZwiP?r9#>24o-OIN;Ko8gqh>b#g=L zWCyqXj_A^xB*y@D^ko0io9d1M?C7n^r8kLV01mF8c3K5B#{hO%1%hJ$JFJ507=VLY zxt&%4?-;-itAKM1V24$}jsZAey3;DS&@q4=RzYP9z`;GLlas1Rrs506036&W?(F5& zr8k}L7=VM7#rEr+OK*CfV*opPQ+Vl3=Zyh4xK`e26_gzV*kKhgjsfhj3QA)D4t9V$ zt%9Os06VM#+A)9~RzYD5z`@pbr&T~X2C%~_$U6qG!z##)0XW#L@3acCjsfhj3NnrX z?63;bV*n1A?z9S0jsfhj3X)?04h}eW)_djBn-XIH4%}6CwkF1C|GzpVPHntw{j}AO zuTV=rS-f=NDRZ~Xo<03#@avuWTp?c3T$(xmgctBrQ`6=lj#2!NKAeCc98ae*(PSnb zO@>j6@qmo@9&pW2L^>of{u!icLsWLT<4|x=#|OEM%2l zGK}{akK81QDyUwIB4ikk1D*q>1=@!&H}9eQrP1HRkeDSR9nRx3lMU2LQ#hcF)smEG zr3Pp(K_%UzT$h|DRp*F$m-l7^>9E*KqT@b1<36G}KTm)H#CfdFcv3R&O^*8NCyV30 z0B?VzoCg?)s+EY8d*!?jp(sqDygoDE7~|@f+n}w-U#~{#emNgXf+*%<2a!TJ50bHi z%R|f`Ct7N@PIl?8704;^QY0f<;jD!!K-7(gL~R^O)LZXW6XA3yPZV-7@~{#$wL_u~ zqgoY@WK#s~F1M>DBdgIu!5FLM(V|9C9&5*Xb)y<9dh?-xL=ojkt`Ao#Dyi`e78z6` zGVVf!xYZkpT6##-`k_R<)mtl;!ea%jSj-git7z`XuwQHL5av(^ZPTq;Wx;RIR>=9xvN_j|Ca^6%d-NpumLL^rJ^@t7})nUEcFC@FTi6TKu@+Q(? zzK9@tK8RHb&g`V3?HnHS7M(SF#-HD7R z5dI-i%ZC#6R!7tj$k!jHQn~cu^jd^cJM~&5d^*!sUA#y27NcdI0_8*E1Lz>58Wo!H zAm6I#tc-*}08s++c&J#sT@!q*a?l%ghY3GO^C5LWPV3PG!aXGF=AlHrHSJs|lFOs1 zY?&z>PNO=tQ;R*quXEniAnma_98pcvg%lK*n+Fkfi1E6ZM9FM)ii2*GR}h5sw&Glv z@I_&vrH8swm#Fd@R8(r?#UA^Rs2hh8_0}y+HlIx7GFd8$9#*2>sku55)9Ada)MPb6 zMX_2oY}Tt(;UJ<8qj9Mg&lOrygP`Ia!CUbl42+)Kd^|G)lRb?NZ^|6rv@9h}dNGqpH4A{)1N zaQb!dOf3$M(Z>uP%+zuaH<3v_Uc@+b@3 z^o2XnVe_wKhX(<2Ca~{13yN`%6Z2FB)vS+da&K>%e~BTwL46?5!$C7(7CWR$w(wlC zB$xJry=rP74SDLVNV;a$brDpnQiyakN1%Rz+%C1zAq}yd=<%%puEOzskjh27-GEr@ zs%$DrwA=L%r>Co>zR_q8qIe*aivxdrP};VUg94OL+o!f0(U_(6<2iwE${=4K3G!x@ z{_3s9t7#Y8vJ z4L|-DU=LQfI%h_`0Wi-X`k?EN5vxo(neAI})2K6Pv+R!Z zVNk0TOlE-4Sdrz#yE4_q#Ib!*bPCW^mAj{N>!+_9oYoGhQRM0 z%IA5}?#+j6vn>Jq(Leg@!Jd1}?%}^3^xXJ`8+J&#~kg1@vx1EW|5*_wg#adS7;+g>J98^mh6ELW~97#4G_iG;Iqo8(h z)l=vW+W1~B+2-nQG08|CFYM#wwvKJJ) zs})Q&9oR<;0_Dkq0@%AR8IPg%Xa@k6O`{y!2fXITkG`4z@ARqnOznE}uGGeFHeS6E zTEBPwI*|46`)gONxmLfrdeQ3cmCvuJD@QGVY*_%c|K7b+UYcIKWih?*z`|=5!t+0! zzkVK>yL0YEb7#zcZT8~XXV3i0j5c%h^v9=b)9Zlo6Y!yj(K+Fm#GZgV9BJms1lR}V zK$d1AC1KG9RO?ezO^o;28j677%tzpgz<$?U_HH`r+UIL8gcJnDGEnVkIdjHS{vp&UK?@RW|jKTnXwPf+5|5z8~a3 z?-gA=U9lPe>$jIL*uxFqhrZ?+pRK*B_4fW{_kHxL3$Hc4@XGIq^B=qYL-a5_W~^4S za4}$_scM%FBVM}S!c*C_Xi06|N;J(_nk#$S<%U1m$=ZzHeE)OK`={snzw@RHM*$Ai8f>M&g&QdaN|iK1e<~j}hE#(8}zwFlM{v$m^$Be;EQ;7@N` z379GsTg8ry*7{K@Lb-Ul!h-V5BLgvho%s;*j^cl=V?Tbs{+z__rEAZZUjCuXai23k zo&NEW|MSoE@SHJY1)^xi^e44wC#{(YjffXAt#VrGmvL7y@AavnxW5XDX@h)(Hsd#j zzs7wpamz)+w|@EFqi^2zs=t53=dU}5asBR1yFb|dV)>3e^ziI4<3@1cG2_ijYLG}Y ztHszL8Lc96h|LBIwQQFy6b7b;3{g02Z7T02A9>(CZ#g&U`NDNy`@1_n@!bb9H-_GO z9QMEs`Lnvm@A=fPpGFU#GiKZq>1MIYb9&2Y*LqgGRn8)@< z2KS$v`^FjnbaLSJNb0TydU&Rd7%zh>UezGw0WL4qdb!lG z8lJ)?eX3L=aS= zO>__l6mwL{&7J{L;pz}bMWtBjYStZe;{`QV+GK1T^|5{L|M=X8Kl+vBD_{RA=v`<2 z%R8R?jllj?XZ@nD;-{Y2rHAmCaRj4_xSq|ZywJAVjgmhft06VnC^Jl=QOybckcE0& z5`$rzqvEMY(ocTRuA4vm=FeE)_~Gu#Z+}f+H0+9jsTtan*Pne`EF9ub=f#FMIFT+o3_6sRr_}Ge>wo@04+CSyfl7vwBo1bTas{^3l*)x0O;T2(kX(!9 zS-IoOSov%tEfJfO&`*E;m8(zsBm1Mve*N_1&A0yNOaHEZ99cQ>q?P0jYD_1Fuws_H1jz%g{iVrsfxkM-<2h(MX$PKJb%lp*_ejGUR`OkUz z`H3Sneb=j=S^0tgw!44w7j4TxG6W~l%TjvdD<)ogm$|dE`*4L zQt5DpzIVUzeQ&*m ze(kycMi0GX#x)o2!$rAa@&Tlqro&lJDx;P0LV#!pMc(uqU9#HAz%JUiISE~M7yRU9 z{=HxN=tq7S3i0W;T=&#_>fd9&(0|vjp7nu0pHsY+9(u-%Gkyfz7^_}T@H|3*7YIR+ zQZFuG@nSC)Xq4St1Q&W8Gn5c;8}YfX)vg)zA|HI=GdlXw-0s`neEF&J^1XqMoIUpT zpJJ}N=%IVe*g?)yoAI3d^x%8my*!tC_BVeGpDJFm|HW6Fe)lVX{F&@s`>zNXe_y4C z&?aKdNXxm9fr8DX8;da=ItFKQkw7;kxAJmr#3Br|GH7Pka8$Lhm?}9-c8~>>#GeX8hwD=K9pR51f7OZ(noCMIXH4B}>FVo!KF7 z{`B$a`^8s$;GWmfL)VzGgHR!x@$-neNP6jm7wmrRx%WNk*6W_c9s9ly_IBO+o-1#< zc>4Amt^sq!>0`zY0-J2cPrm$(NB;Pz7l<#v;-ja2`&?=LxZ_?J{>uK(MW6YTJHPn$ z7hL`YdboGY*g@VJoAE_|c+2OJzu*0fUGO(gKkEx0eaD|}yLLc)_Xy*xSG@4wFFE&f zOZ4!xF=GcwW^Bgy&SFab4tpvak64ar|oZiac}YSyKJ-@9(CEr;Zsr z$OvOI{`Yqrd7t~Yzd25t{txQ~ohRLO^~%~d-0Q&&;J%ZJbBF6L9P{>@hum= z=9v4g}YHsgOe_EK`EV1^C89PWhVl&?VvgJ{hw>s`mc^Fn&Kd(7BD@SV;0ti6{}cLaaQuuT#{^{{5T} z2RR*t@XN>eTzSM57c8Cs0{(>K@cBcWZyn&ye;B!C9PUp14@W+@J;5^| z@Si&)=<@MJ_&g2v8l}>id zrR)juw0_tt!os_oI$yU$$!%8PRWhYOJKmEf&2ovjs{%@d@;&wXO>KA@E>Etl? z27lOveqx>Yn?NV@b~@P_7I9eVWT)1`PfRex!x1Et>!c2JQrJ!>Tb=q3E1m4vy6XvX z<$pN-VPc&ue_{pf@l#WuoL+s;+R>{&T4-;avwFp9X7$*q->m)f+QnB+7uOUI8bF84a`aoyd}h{r{i=fDN1cI+9O1>z|v?CYH-m`bM8Yn zh1`~fE>!LArS$^b5Ri0-Cpu_Ybr&_aH4@fdBris@~mT6AGRq{&9*d1 zMLd*IL2)V?M9Fa99nqV3sG3%(YPN)R(Qd@>xO=ll+Z05h7VPAhl;0hzdLl{2KX7*_ zB@yojtfqzPQk=n=aLfo4vvY5BOz{TC6t8zo@jAy8ueB-aauwXBwWXlUn=y!r2e3e_ z+z_C8s-X}D8VCzPSDHrkXkj_+m?GtvB56}N7>XZtOz{z$LJL;P@p`gQYkPg2l*dOT z7}e8;Kq76aq*eU&V82Dgr7qPb7g@&?7dob>*c5Wom4Z^Rpn}xntxg0L`7T{3hP$;& zD9@B|Z#q3llBzr);JLdTQ`~7&s7kT`C2Og`Ku63}yIhZUAwta$=u$J7$w?zU`ReTQ-G*iMMJ~pisD%L&8*_7ivW<*{g`Y-k=iZOfMOUg}Wg?f#u5*h4~9l z-W=@?rqR`LAH18$8$_Roi#UiP@2IJs8jGfbxvoD=M_Qq{PL*;7((@u-czw|^#e!ps zdHczge61C&CEAGU!jqhk4qA;UNK+5Fqj9z^HQGfl@SwpG{;0CL=9t2Ma?U{?o3EU5 z532TsZa0TBiopiT`CKayNhM&7f$qjEqIu_+2Aw-<5yvmQe2 z2{Ed~L{X5iCdFxLG?k)UUIq9o(7~iyF&6*enBw=2DSl^Dr>2DCuozWt5D|2AkH( zjYcq-Vj(E3CxgwIGaXYPjwxW9!oiy1>5eImu_@|}7VJfOO}VNuv6f!WhglQug%mW$ z>lIgr@lo5I0D@fnUOHf7~tPTX8&1WTPlpqPSSsmK!v zf2?k0>g`^Zff8s%_qVuyK|}{s)yw#va-@yQ0mV^{DV}CiV67}G`r>78g=mS{epf2S z8hzkfg>@62LOx|#-8`9X2DNVT7~C;M*fB-Orf@K>_SzH!vYIh-F|FjQ^*TK$7Nt6| za0=nmUKMx)hr50>7AXn@36pbkjwv?R?GE~wvHLLna-GdG^)!nk9<1VNv}CJL>7?m2 zS1&Y!V%%j2eIXTNnUy1Kib}j`WHMMF&s3!VAI(HcFzXW2iN3Fd)Z59HQEN6)s^!Cc zM_gu86fA$2Yp{7(P8RD$R|3)#xEkqJ;^8uDLdlYkb7{2c@)G#Q)i#BLvAen6b%5$} z$ItQnO^V4kYag^JxIwNWc)5Hk5j5k8wiZORNJFZG3vrrHw~Dgg)5@omGVn!Rd6r{} z%`K#ZJ~rnP2ReU}jjER}wQ7T+Dnl$!`OsJg>2Z~sF^J=>qFXD+V`^H^GbBd(7T-GB z|EH%uFtvWi`i<*#ufTjs^@x@4t-O8Z zq7@QE1N?CL-80eU%V*xUoSu2w^xZSxn|{~wvzC53^?{`iF1={_nM;MGXD|M8@#Blv zE}l1?T0C{(w+o+M`1=KR{ZG4&-uT_F#=^e&|Ct%ge_{SryWn|o-akJ(_wRGBoomgV zHMczbjoF)LjahW|h>aUIxD9C6s}DQ@7(Xf4DQEi;368Gl-MYS3#KVSVx$;{*6f{@Oti3iU6VR+~~^bHbqR; z#bS%&D}$ULLA)(2kPJelTq~Y%4=T}e5B9W*wK(arl;yi^3gFS4)!C-Yi|!k%in-zh3P%5NZeN~i=4=*1EU`DsXlj(cK7gdwhZF! z>0$+>VQgo+0xTBJ9d`kOBHD>mk29)ry>4OxhPS+NDaKOS!ai8)`2&d@@Y@wIA?aIw zkiwnU%s5n!QC6Xcw8cQ3r!$EN?=8l&ouU$r!c8m~Lui_xdz;-yIOSo(`EXI{b$L{m z10LXT6&d8?C5#KTGudJ(Mgw6tdkVgEmQ4Yx-e?HdEIr$*@Nirsl4XBPkJ({fPt=)NuXn> zLuOl&7ms3OGU|nRJ>$+t%FQ-9Zx6(lw;oLO3w=HpMyMKBz+;_qj?HNqv^S`OO6HYF zj#Ff|V4&pOd3GP3Zp;s58o6vDuXc@W1ho>aeyEqMC0$+K-He6WMysDn!$D@A-lT{d zN-h@-1)5YPk#ni}5E2YXSfSMpLu5iM%h^)N#WHwNS+qB$Ee}a*mOGcMrhxA`OjzDZ zAkog4P$}W^DOdHpqkuK9yFQKA(XKgM=oU!CY1Z^~T{P%%f)}j@GfYU#qSM#f6gp4JO{3gJ=&XoECB1?K zYp_-ndxDJDam?S%W}?lQ7A-6Dzi~`)zfIAoqI_5@Hk))VZF$;gHX9lh*k2|L4Xh{n5-h5Pq8?+QGG@Dg3EGdij;gig_ z{1tD1 zRm)g2sR+L57j9BOa4z6!)eSG~Zs|%21GOrHaxM%aT$&NCPqm>~y?|@Yyg2h*o1zlw z)%tl=r`o=bCN(NagwD8nfv6mEB?vL=t@V5U3To6Oc;*J1f-$?fLckaB3i`vN|ijiwbNAX=WJ zBDuY>d{tBmEW%N2t1NJAbJ0E?k-@c|oypBmM&|=1u z>bvmif3o|)%P3s#cfv8eS#i0mth-e0G8ULUF+akqXhPI2HxXnq-R!b`>?Wgra)8&Y ztQeDpey5*9;sf21)%ZZF39$fzX9k&mu$iFH?9!%mkvL`Xu7TxhaHebmlZXq&6i0h- zG^w)Q>?nt&=!JuNxG-~-jSA*uw#F+A+iax33Aw9hX?Ot6B&ormX9+@t_rR5bo)-G( z(%Cje5Uhi6C8Q|=QE=-_$Q@@2?F=_4DBWZ|&sS4jZ!}k7HN#lEe*AKl1Ho>-S%mte ztcBuDkW-8C>ff}oQ)FH=3HDN<;eV7jhWtBnNuQ9@*jNwYBfQ@am0n=lE#QpsV} znxJEFXJFF)NIB85`~goT=%%Ggqng!Qy0~PY9?BqrDDBVHeT}NO6D;-^BhF`GlMeVi zy+M!>8yyZO>8{68@y+X}X5Sk(dZMeM7?G0OU37a|B1Dtmc$L(0gG3sq#JG}6D47m4 zcik9O(+XybR-l(oHiB^9g$9LiFR%4vsu8rJi59GPw00j$RGMLB>eV(yw@GJML~aSd zf^s}m>RUCd1<~CQX;s=}&Ft$~i(*rSP;dSzn-p0O8DJ7-hIe-&fp!o!!ChLSjdklD zI3t*Hp`Z2!2_lkc&f2GLO*3IJu>>UaVi=u_#pQNQPOybux*ZMXN-?XhXFHUKho!bM z^Des&%?0^*e^G)bh>VW>ctQA_6i7hO(&~ zB!K$1O}$@grTTu5!9h-Yv$cdC_jzjxn1ZJ{o1(=R!s%+O)FIM{Kp`PbH4QZuNWn2R z?i!G}8ZAUT;d-{+o3(e!%_dYTf~A!if(wF=FmXXq;UEYBPsIa;d?_8aT9N=xrGsX2 z#$NO_>&Z&2+eoF|a<7d={bkGYn`)(1;4&KB$uJ}>CasE&WkS&AC1sPRAvqghya{gwj;~dKqV9DT38>D$1GgqP<^f5tgO)21!_G z$EaL4$f;<+;9!c3)77Tm;<9G4PPt(p$du1$`0xnCWXn>kCz6>$(Z`|D7Kk_1RHT;8y5Uw0 z2Mha^8|`r^54@g;C57p}M{bdWL?EaUNI9qmO;lJcIs18 zyDr`MTh+*jvvv;Q`m znz>^pHvPHj2q69|e=fUrejbz4)Or(Y%bi( z_SbC$4iX0JY*x_G{{Muj&ra=n!N#9AuG~0o{rdG&)^1zNto~?~S-E4SzWkkKZFza= zRZII8->`Vj!aEm;`R~swb3d57boQ6CFPc4W<{dNm^arN*0v3PG54)dT5V9w)Pwk0% zFxJ(QB{kY@V1cgLW-3J@Vsr{6RD;Aq#enivneC@6m+u5Fowo+288&&jtryz~rTd73 zAQ*`SSh?N1dDxI$_e1M#i;lH<#AwpsNBd-<5tQE|`=j-|R^)Hmvm; z;YKA6oe*vpqKc&pgkYswjXbSt2k)dMv^e3AMdY@|BLgs%O)x~YE1KbE)zzgSF4%yO ze5)&WnstE(XNr7G?0fJ`GoK5GyT%g)M%;e^z~Tkcmnh&4pK~c(FE56B^_-0G`IPQ1 z!~{R5#xtIz;3rTDxY_;8 z2n!IegFKTHV|mDIOPdeuAT{I(daux-F}-FI(J+>l@p9{NnC$`K(XIVOfMqm(9VC66 z7|X%NFAe$|jb8`3ASdqck>j@j`Wejt4pKu-+|QQrOKqytL9)n+yL!y=dw@FG{rM3V z@YoLWOHPdCq4AsBd|=>#G-*o66GF{YH{0e=HAC{9cwY=RAJ6#R8cHti&u;43K^DpF zu^e=!&w!prmf|3-?=CL1mYk)s^&&D>vIJgvg zg20ITNq}WEJv+D_+8)b6%{B^p8ky~mUM@X`*%F|?k=Z)9^x6K&Hv4;|*+w=c=-_H+ z;(oT6Ee^UGnXQ96oQb=7>`}ZmB?7xYJi-FR>mXt0#8_;z?e7nbMC+c~*)x`d49)|K zbzMHa9~*U!PVMaR%42t41qqBRZN;qcb+#=E{r%{s02~~JY=6Y_*cj}tjTMljpP2f= z)SNVT&fHUG@0tDR?Deza>~m(HH1ngG56`@GrLz)SIc`RrIcMg`>7Px1a{9(;WjZ{4 z%=~lbkDR-A>IWOSjg!~^xc=|!|G0k9dS?B^wLh%=>)M;v`fI7RXRrQl^^2=-SnaJQ zR!>-YVC4%duUm>NJ!A0~i=PHD|H@)`@tB4C7CyP~%7xYfws6$^&*ncd|ML0f+{fo` zSo+m2YS*b7f7}h6IxhS zC0J5ot9U1lm1^nUmgUe=VyVBRO|3o1b5gSCQfJbp#F9B_)5wx>%3OAO`UF{FW!xyT z6w}i|bbQJ`lL-?weqz|o4#`8tCKc;#mZMEZTj++FWXI9lpIQwv!PZk z3~EcnfH&BdF|hd5#WN;v>e_6&?K}`)vLLr|kDI*d zv6D7kUwqc2P1hEmIcd|?#b<0>GX2jFzGMII-@{!3wqmz`9sgqehm$t_`TE_HHob5C zKPMeoD~nH`wCVEVF_Si3T0DBvri+V5P1b0LNt<@( zFP^k%Xa1tirrWNwl1qcBsp%^Z_ZjeiIlj>*dJZJ}U+XTtYyMpD!~cE$W+iZyGTgHy zWG|n+hM3;tz`JVuqojk>Y&NZSMH&^C-0g7(M>RMN6L?9qDvDkebdV7d{KXA&`5dg6 ztb-yJBgT9FpBJBX;Fa~LYU=ih+Zi48jG83Nf_}!hDlK>fT~S(_(%N3wV_^zyAjXk& z4^dphw>44fh>BO*rqCXhRJ~}ojR5@U9Tfw8u%0Rcem=0Cs-jCEiRGxbhvT3|?k^*r z_TMU?m~9vM5SXK)ux@`4f_xqb@%aKHh6k;Zu42|cldgB%1k)KLp}h$`w-;p1X+>K| z2<~-iMUGaDy(W#qkH36s8iw3OJzzv}abGD9^$S5SmMmZ>!dCrhuUx3t(Sk_l60`^> z#3a<~Ym}R1p(J7QH4cjJrL|`W%%reOwNHj z;r63o8y(?)Z`9XAib%lI96)cgCQX(TY@*B4GMkL?XK7R zMUzcaAeRi;(v!Y^4U`B5A{x~PX3dT+*k!7>*#2k|5vm@)-;JEfR?<~e8BGwWSS!-0 zd!zw@0jDk{F6g|bVFEp9ag^*!%6XB@#iP0>sY$tVBu*Gbs8QW3Gi|R?^}91xqYGo* zERy5!bS$;cqj(T}AC{feX6N5$4a;rLji?sy>d+L;SiP+F|i5Cqh^yz;c;6aiY459jxl>Mno&c( z0Y?HSpFqeLqusK<*idW4z}E>hi!7U$skkDdN zJ$$&^u=|m($k7{ZavR=qd;S;MGK3s*0Sj`o$D)yfKLvx-2Y$w#2}QH4fOo?d#uWBJ zjW`}Ic6dugk;lJ)1?8M0X;f`R({wv8xM99iW=n7YtH$C;K<6@4VP7&#B^#W-6HmsN zOg2N4x{1f5WkzaJEY~AV4OaJgbGf|5LG0@e#29a!RM|L>Z5@6@ih?8`S9bWxDlf*Pu0w#fA2{{pjJxIA{oL|5;910 z;o)4aaw;r)2O)zRw#RJa6R>sQ8chvbW45sg*gEilriSvEEja;O2M*8FusLQMoq(+a z=Vofy7_%iNVC%q7nHoxCwvp}KF!uz6ew;}*OKcWoCCQsaOm4b+WyziL^(qAutnX~1Z*8RJX1q)%r-m$TL-?&)UY;Y8=8Qv14m_QD2&-+6R>qp?vG3j zt7Eq41Z*9+A5%kq%=Wnxuyx>SObxj)+jA#i>%eQ68nR=y=WOqX=pLuSl2u>IlS9tT-Hs9|YhwhmIZP{ZQHY#rp^poa9A zt$(M5P?(slgFF?~keZmSBO&C+Y<&|5!9hL;YM7gtts^01CuZwN2$?Zk?@kLLJuzEH zLP$-_)rF^0?Iv9rV2Q@p=J(8$!>a zE=2YPFfJ3OyebjT_PJ&(8gA|t+-$#N84zbS>t;{OguQY^>ne}?LL`k8`ej9E2jZPT zOHo*lp9p!%Nup5`sj%Vm2fHD&XJ)FYGOzUHMmDc%ou0TaO430B@-{4r;e~Wrlli?- zUn(vp!@4huPIe(Odi2qCMrL^OLpJT;eYyP`;<0Vojf_U>L2cUee>M&3RFYb=62NL* z9_Ih|*|h(Ov}upWcPN{7ci8kkNR{ZUS`#z*C4{?bdo?K@D4Ij1|0jE84WYQc?`WBrXw&=6rHmt(o178O&3A zBaFaft-S@NTkgi<7Fx>bk-e>Aal%VZKX`Ot(^sj(lWfl(a(ZWzLT+Qz@F8v5{lDI( z&j({}>@x@Mln+a1I^Pp%b5P)r@6kfSz{`O`x)a^!4fI+b+MN#u`4S`Ms7OQM_Ywh? zbBDNO6NVnQ&Fw`zZ7~_B`Y6)RAP|~n=s-Ok%(FO-MCpivBtRB@A_D`rInJ-NV!Ep7 z>AjkiZS3WlLR5{HQ@EQz^M-<^1f>uT)$5%wI+1ISA3XZV=9J-y4|&r$SbzP+?@|9D zZ4UaIw7LIP4y=!I71(J9)}Z@PuepGvg`x;dV2>wAo zT`o)M)Z_Q2+n38SWhGj*Fu#s~s**R@9nL+Ez2~O$UdeZ z=6t=rWEs_9y9~(`sw6n86G_HZob)70^=3Fc+5NxI7jy%YTbXu&`2WvO%_ZiZ4x<0x zH~WIw-0bl)_sx7{xf9C4-@2tONy|<3fl-8fJcGucF)-G9#uN|}c z2x)G#n6+jsl+K(A2I1VaA^;lI-HWVdQ2BqEepa6!i*>YRrnz3u)ubk%a;1BQg zx8nylyb?+4RW~R{!k4u$A_PeiT)dZ>;ieggCdh6G)Ccl;QjirEb|x zAayTWr?70+sMdp|2g^ozN-1*4OE&)3MM?genzOpQNAi0P^G5$llgK` zS+@&1>86E8Q4zC{h}wxnxCR}_Xjs85s!r7rhBS>1e7dnFQK*_25~%B$sYPeYs#FbwyWBWb+7@b|5^x_0&TpIHb~ZNnYB;~3d)Q`s$W)M_6Ev4TdF@ z=Hj~DhgU6oT|UlRg>oqcCH0&SadPua&AqD1(sf054ii+`zlJZm#6U zq+%&s(7UX=nG;e9hb}O79~~{Eba_n|BMG)!a`R-2tH<%s-hDWavv770)HAIlNu|`} z%5j?%aHbORW(U=vRH$a@5?>ln35N8A`U$fXQL7$96JmZrlAE(`o5Cz<7-v;{crb#3 z8g7Gj+J#j;X0i!#y{3b~mL=9QU>o;~lK;ZNSI?)EMeBWwvER%Pt7_bk&Ang~dhN zo(zTYq(lCu*mVuO-57zl$aI8nxFddVAmhu^DX7KuUHc2PkUiX`4dsvylcQ@ZAEHBi(h6CCL6vI}iM})R;$=kXAt|Yt01PMi=Zt zp+>V!=BlM?Mg%2m86OeorsOJDPZgl9+J?IARMkhzy;8Qt4(_IrG7Q*Ng$YSb=7{3ay5sdiYv*J(P)Fz;;sy&b0IGltf^wS zs26EARL`rdkrfMq11}McS2Wg|ZVi(vnN>QTF_OVPXmd+a` z1XiMfECtHjmHM=mv`{PGFe$AI@#(^>U7pb>1*+aU!}jCV0o3!kD;1PV)%%8!Yt+J? zh)N82#0VDLewttW)FvvIB)d$hQ3g|6MV7n>@6I!1x{3LE9T$YQtttW6o5cYOt=YBV zj3SpvbP3XxOe_92EczNSrE~&=LLO$UB-Th(7;iK|gL!4YAWS=QS>J>x~p6%M7(fIJ4Qr+0EJU?Bq zl%TO_j%u}q2)b0UK@n>@+16^PDUHV2NSI*cp-r|ZsTk{#X{6Jxiy+8tqk4?Ucgre- z)sjYp=+_&V(C=VTOo19!pEkGxrgf8fhDvjA?c$;wpiO7R?m$Bs$#K(mqz!7ngWB+= zv;Y=a92ZL{SYOO`Wo2v=7k_yngW{8;2-+OMVxcQo^-_*!;!Fy|xeO)hnTmpjxk*fq z6H#j8Uo8-MV0NQ0VTTdRHptdk9(22MB*rIVPOhCXD;m+#%33v(ghF3iAhOj+I1WrA z=B0)hU!kjRJFXSbv}%*1v_MRtrl@ABLMvO_`l|&ZQz8=S2q&3kVi+Bx20??s2X!)0 z3a0=jw?tC-m#6XGoc3lo-3&0qFoOSk#-hkrIZ`g_b(#j~QhG z*2Zz7p5T;vU6@KLh(oP)t4K4K>vNq856+u0oK7jYrA9!ZQ?C~zgnWUI*Q-baYy=8X zyqgyyg^@8yrt+M1;R_4IILQx)X`9a&gj8tuBOSb1qYV;ay19;18nugk7EihD*08g& zU^R5*q=sp#SgLbgDF(5bf+06s;WFJV@L(mHXz_(;8mZ+=_LhI^{gXj=QE&bC7q7h9 zsAr2Jrss_wlx~;YPBQMupeSj&WS0xkNQcZyj>-=mk=lE5j&Q;lTP%%o_Jk@Z$y|b} zhWXAAE@p)`BU1`qqFgH%FUg6vwMXvm{hw>!w)X5=C=&YZ?ew0s{@i}|(x0q7yYVX* zfBw?ryZ`HMWqohw4>zwKI0vt9{`%HGy6~=x@4N8)PGS4^51-t4|JL{H|A+JGhi5K? z{1Sc_j@%(AHku=PydY=mn(B0M6{hv%6dPKcTtjV3WIV-+=~AR~L}JxuzG*3uffJF3 zgd4-*qL>`h=NK3f>T!`VdzRjERNV|H(#hw)k+3bY^Iku>|cI^gWKg%eV|pis#rmIw9zfw z8HVeV`B9RVYUOCNpNiK7f-q3xNbXO-^12@$XVv<&(=Al=bYZ{?WUW&wSK?84fI3+k z&a3GXbtN~l>0!~~g=(?YWrcBfIj{Nl_8S~LEJoF1Wf;VxWvLLa)tfhaJMs+09ZFWQ%g1~{9JmgrGlPqBoU<~T+&MARkfJk86q!+{SkpRkCCgelZ;}9|%*9l!J(The)0V09Xh7OmDk-8rGzD#BYMD}{UFuGk zIIO3iS)Y%|^&>AGhNYaFs$&8{4W(KNZ)6*BIA0Soje3HouMhMbO3D$WT9)#Yh0$S43_Ms)%UCVsG4?c8Yn?S>`ak#UWld?OLujHkuu)S71zH5E)7Nz7@6I zAvWx3Nl{CrZJKtBBaWmzqikWdG1#pn_#`+*tLLFkTZf%+f-$msi}heg0fvRVLa4QI zO7B$+uHLT>S##K1;;{3DH#oRzxSazvf~P6IASDbDY?%6L$EdmxU1kRDM$sZMv=nO> zlg3eJ7=^miL92_UCwhAXcIpD=G{$3Zj8~kAjN@^VNg1&eF={rqJRJI#OQH~R)gxDb) z#o<6VCAb^SN9_rclE-(V-6sW0D#)o-Nn|R)`dDzLm!NDUD zQH?lKFAJd!ZD4b1W=MGXR=bqLWQr0y^;9Nr7}M-nI*MkSgiVEU!x*RFA(@3KWmH5N zG?^8XTu!3cL>G&uq^|4|SHLl&TdC`gP$L+V$>!W;GkIZfi$hY0_V`Iw*T51O@+c0= z^O+`JO^@r{0U;XExXxfPw`-@oBPiK``b;&QskOpEw^%zBuocWu*ONc^oYGO9i<0~ zWzoGhZ-6zgJjRwdY>(dH03wDMq3D`&SQQYp)k?`>NmX;Mq((d|IdRo?8SQ%cLJg6Q z@}3K-T%+l*MOsohLrURHFWv+Cq+wZwGABq^=Q3DR(h>5?*ln2u!j?SRNe}Wpr=3~i zu=!)Rb6B-Z!%=`xq_d-BLZ7|G0d92ca?J*jd6S8(RP}tAtayz`af-8Mewc5APL8;` zJ?+^?^-P-`(ivTeBt}ruBfMx*EaXdra7Kv%5I8U)A1tcep>=mFE`%(T}n$XLzv zhV^Azec|z29Ky8L$K`H##F-R{Bs9;=bewuWMhguB%sO^c6d0t?=P2UHEsH%g4d)%W z+=w|Pfdcaph-qb#hG4)JM=_#Z)FySpp}i~RuGea~B*{(~GiRnvXE{6Y#lO6bL$Xc| zlifj!le1GdJg8RqiBM~`;-pd$TV;=|F(_Cd7MryrH(j$6lUg#7sN|s5NP}W(xLYVr zt0>2o!XtvNv=t1TBqK=)aFErs?phuuVm8}JDrvlwhyBiEGQ`+r-CTS976%g?oyR(Zp%9+54XI1o5w@L=qY|kXN-(YJgVabL3+UJ<+K%;430tZAs9v$}$8%#WrY|W~`#c{$#ujp-C<+3GwY`6nvoEh_} zrG8lZ9k5CH^!aR5-po^O^+b;dqp5BpuFyGK-jbdt$f zM|7kS9*}u7HmqsoY}ZgTbrP9Crc0urwprH}vKfcZKrxZ*T+z*JMJ_pxBsIo*y9kbp zmwIAfzO|?&JsUz;5y)(on6a5Fm1E#~1lIdgyogORS)!5}bV!U4pre=smftcwb zRI24TFz#V@tTR>&rYs%n4eBv)3Z9RLMJU>1(4%p=)GuIZho0oPY#c94_$J6-V~yLU zosGw$a$+>4$6ypIhH<6AV%1isRV2GB861{x<&jB}NoYbGMY zIO$!n-3Cq9c_HVp?9`+rf?4W@t*_nU01{HA`L^ro^|0Ux<7~a=LCN$m#$xHR*<{+C z2}-xYcqny_D)mNGQIcItf=8*eJg$%B0uD|w+O*ap8a8lgg<8=_)Yh%e6}Hzc4pZ1z zAssnqi(|gM#9^0N`2YX@+TNqv&u;wY`Zu77>uD zbl+}*j>95R6X^}G6miWqi(R+X8kOkQRilF%cD!uS$<$RbQLa^cHc_niIjvtry&mpX zYpgH@F1d-`aO!1JUOSbmAhDcHjNJ5UZi0T(7w+~~-*C4N5VHDa-2{=yeAJzgbPv$_ z_`~AJQJc5M`olpAwfYbKo6mC@AYyP%FhFR-{QCjoC*S@dG|4-3+;U1m_ zy{GGMeNx9FnRJ3=ooi`Y#a1P|Vpc!D>Ap^1o_*I`r>}o-JxFx>o7QRMo;n@KvKw!I z9ZkHII*Ovvd(FEZUP+Yt?MizjOI`x_-?SB{mli0j8|C>ZF-eI~GM}ob?ZW-%{X@N0 znylgD7F;knf$8xXOratgI5tS)HDx$vqZzGgMy}a0BOdQp6IV$>H#Ln!vbj3cw%~#2 zlxu3igh4DGmbVfUty)d3Ht)tj(--sZ2j7@?14Qz_X&r^{siWwBl{y+AK6^fo1_*_i ze?Q2)e*2RAZ5eVCe+zXwnvC5u%nco;ag(-dErbGv-3_Wpj^aYLN|mljkx9?a1E0Ty zn;0q}9pHVhrDawDHLgmnYduHOfP2ZcY(?loaW$V|8%ZHW0Jkg}oOeq}LClONjeLR0 z7%i6OuJI$z8Yx~y%m@>7(nBM}wVH)SOI3J~2H^;PmyLRUeqY5;(GJ$89c3wZx8HQ% z{~!3Y7uH@{+y55_-@pIjzPpbf^tU7X-*mCO_sZTs*n4s>zxUw5A6@#hOTT&P2M&I5 z{eQZ|TzY8t-|hYnyPw(RcfVuj>+8>5_}!gO;p{SOGP@H6*(@Z!LkgbA1W6}TcL;~5@J%gt`Mh^R@-;}BvR zP13+)R~^#i?n85g+a;!!+L|OO#2T=X_9&v**{P5n^jkS^98cPAcFdqu7KX{)h3|?N zsdr0#vkKgtdYo;OcA|h4OZ^d*h=>#ou}vwTFr)eIl#q6}7Bb2SuhA`KhqbVV;l(M} zVc3+!3K6E&Y(tnMj$JI?aFmW=>?}Og+(ML^3U$}Udr34uLfrN=RuY?qF7SJ>u`b?| zqsCZ&$;nVK& zvI8~R&GFQ(G9*^dtGo;xOQ|SER&Ax#;MK4t!ZZjDz;^!8aqq5t<+$_TELg36X}z=X zWArkvI?ZO&MG)fFCSa!@#(7k$B38XNa!jHqW(pm`&A7BC?LV@RQ6wB&%yp-ESx--a zUtuDr6x_Vk)SDf0n!}qJ-7_O2)rA`S-?Big?qRa`7mJqT`Mlw_>I0;hDa2G873 zsGMXagimv$t|f|zai^SNbG5xcoFhh#7{~Sea41`-mT9Pv0h}C61#M8&z#Zj=Ohg?R zlG&+Gti6SAzL!-5hpG}KxD+OP<6OR&FqEOtosP?_!*wDP7##se2q_~cHvaR1)#}y2 zL)VWHEBjKcPNO)f4-2h`a$#7*B6M@@Ax-AzCf&g+ja5vW5mjLQ|re=clDd3 z;{c8T{t}zNG-u_mep9q|Y_6=1Gl;s$jA`N=kszxLy$Rfn;I2BLrK;XGiddm*$AGsw zwf55s8LK-tZ7)WNyZYVH+JC>0vASo#PHur%-JfB5;e+dTa1|?KOcqB+Mh*!lNx3h? zrV}n+vQ0`}7w(@1w9t+w%t3mI*2^Iz{|dZOJ2_lMyz8-ZeVl+S?Jg0#~DnJ~22 zSjkgT$=X^(KDf%b7w4@ml4z=vPCU2*>nuM1v;tYO=#LYwgq|$w;^=*0DaLW0eG* zu3M(D{_6|G>NnLF7eNLtTgMCnVk3IKW0XK-S*l{9ecU4nB%DsC9lbxPCZQ%V!q|(y zxsbucI5gQTiKA(EDAdckD+AAkJc&9YU&jk=5kWkno~@@9a%*>PcHN@hD&|U+SdSf4 zv}|T-7m2Z{>jf=Z6WhT3iZ5fakp%N`cDewNEy$uZy6g(Tcb>_mgh?8fD@{m=6v6!} zSIk9{*;2pO9^1PMPGoJ+Wm|<>TPE~DBwp~M46XM$&q!9&MFkOCrCu%^ZXyMHec?Us zX4{oPIb{e^xtD2HS`DY)Hmt@dgAigZ0fu0V@?<5J#!-7Sw2+ZWutk<}AtTQWcs!vM zaZRp__!z2*M6O!(8bmD&6%y5f6#9+@V)Z-w_4h6so~n)pW}*_-gmjc_vtd#|N+9Gt z3Aa_E!lVT{F-{NI+_+y`#BaD*he|oLt~RO`PQ`hCl8WRgjwrA_ept_0L|*Pz4bnz6 z>jJkRm9%iyft%rG4J}vNS)~t6(p{Q^cu}#r5y=*4d!VO!$*i|^jEL16Tr!Wf!@wsJ zYG&vWVheoI<*TuBErqsgIdg=25mtdap+$6z8wG_1dg)}F;~GXZA4}mesGV+AV|lk| zT8O~5hUtn}%&FDR&hg}dG6iOcXjUl>4O)CwOmT+DV{9Q&lMx~2B18lRwFBZs1KV*I z?SLX(#vJP9*eJ#hglwZIS|IF&ZXs!>iYto2la)fFHIjSS7Iut)6tRKU*-^5h$2yR^T6Sh0XN5Pn-)QEQLP0b zb{XdwLRPV@zqde4V3s4Sh%in7KgWsbMO?O-8XU1xLf^a8PS!CrUpVuZ4+` zfe_6U!J$2|PB!B4DUTW@I3KU5V1$dNhjFoH zIN^G3oR>$|`mZbyJtM9S2#stFM)hlPph}3w2*fqY!^(6U0#2mY45gIxijCUNB1Uus zHhL&hjENMC43cPFFAXg=lk93!JyMFYFfi2!hDRkVgRMWakYRQU`9d25{wwiur#X&V z1$v~0Gp60Z`<>R%Z4|7&8!pG)#KuSF2v^X-Ga3e5$!S(t8v`?y)D%r03!{ix?A2V0 z0y_m1%m~u%!i0{x;p7nRqpgbBb}QLP%r#Xw$%P|>wl!%(Toeg=Mx$PUtI#gFU^PhU zQnc8b6cQFys#D^$5N`HdRcFeBa82W@{FsW7tR*vz-Nic8jVWNe9WGP}33frGk&|HK zB^2wLat-e@RKh{i-STwA*p#&Up+!5;a1F$wSP&_g{Sp@M59=uKtbhyILXYVV8Yapp zqMVpCBArXyV1xN~_5XKQcNN)J7AUiAolEG}&xX{^=*ETJ_3eLt;Sbk85B_vMKR*C(d`h}N z0k_TuGIkqC1`@#*n#|6Bx5aF7D&rPPYFpF)-!$yu{2^4x^XI;O%}9)%NL z12GLUE1NA1OOiNI0MDF){dHeRr(g-;CUyVsIMiXYnNbTt9ZJW>bd>4tTzyW(ARB+i7L^M)?T4;u;!TE@r$cj`< zF9?#x#9EB*6xk7(Aj)_av(`WQHtaI|oVqMmV&!f>OBt|U;rq2>rZ%m2r4zag!+^!K z^D(My>4RpO$;83hq$=30PEF-n`D!Pdqgt{P9ogfKwf>Q}Uze4p=hS5&z_JQOd12bu zDI$uBS;7N>UMF-J-mkf1AvS{Q@IV>Gi)P+5`5~8U^oK&!9>b(ILC|b2SJqOVwf^|q zu*=SClo4>HFmc+o2@LXDSb7Df#$4Apq07=@6{hG)9V(>K>2BUuQ*jvzxBLB4Ia`m` zb82N&tk)UU%D2HNtH1raj5@bbmQ>0hQb>TKy+)lYNf8ODP3#l84AYwyn~;l8ze7kB zgO4|&MtE2uTMZ0CBJG^6#|EU=LSx|sYpwHd!!Fa$NioyZSRfpb8h}7+Fb7xD4$7xb zGXEG6Ny+^r$yE(astiS}Jcipwlqkj9u|YoR;$g}`Q|YRThruZ0-iBRvUNdGIuE~0> z+k(-l+{>36rfQ#T{?idr&m|I+n5|eSQqareDi$Xq(}E|}?NLhBUA6&bwQv{s>|5)# zw_lf)&TEu`3=x&O0r3QgD4AlqfOCVg2?CG6g5=IcMk<*^3XP~4PA9A$p3yQO#Kg&I zYKdvsdbQt`^LCEubw*(31m<6O8+O@wjj|lkEf@scs3q)bU9uHIp47FI6cgvBIo*>} zjj#;K3AmY}>SDVo#k*5kYH;HQ9FH1A*sHK@35>GF+po*0bDJ?uQ#AT83$g|^^fFu0 z^HGXFX~x8zVYp;9d8NeiNR5yoV%Tfgf{GW6-UN$vqhho$VbpeAcEBjBzU{hfa&D{t zbRVynd4|rUTpG{RDu|f|t{EpNCMFpM+(it}h$IOG=?H}agH_|5TC@f;sH#esjuV|~ zJ1*pa`LDbUyX?G1Sq6);lM+i!*{PB(6(~aOwYnhO?rvKKS5slBsfqDE2e*=u<}^V| z>4rq}X)S}NyXkm>sh1!+$8(^|%5T3eJGa$;wx3aItj^RZxJGa(4jJ15Q9Gf_u#8TI z3#n)hA2MjzMA=T&(RFp&a-495E0-Jg6p8i(G+Bbcx{O`;|9|t^-lwI52w~0zJ$l20IFsKzxzqk)oiV$ znVK{4X?+920!|1(no(8U@<b@y>IuV_IT3cF?G~|E2&HljT@lWsa_rqMY0f8 z%SuFtL4<9!T1r=q2@FyYwx)WAxe7d7M)gLfH7WAZ2(44`HV+PWhH;P=$krk&xg0K< z4&0MI?aZ$Eiul>ldXUj|eu{Wv{@=H~A_gINy93~{BI=gbYYs;yC=CCNQNT9{_~p4l zk}OkO#H+|k-%-K8`A&OprQX4jd!_|$XY6rpGVGFKzXsX)j@QUaxDCgyPCFxu7+*8- zx+r#ALdQ(px8A8;Y#OQNM%@y`l)Yr#^J3MM3YBB!VhoGLakh>(x?+(B*`EefjFGMu zc(+=rf%}t66+`LzVK<_})f!;l9%s%An-H%G^M*#-Y zx#=C6Jjt>5Z+t=vIwRi9U4N`cealmZO%ucfncpZ)8rdMaZSDg4nAo#=VFZGcFa#k@ zI045YC>%lWie>xm%is%h4^ki7l$EwPG9B;?ziiAK@l79p__&chv8#YTM*Y8@)b9D? zZvE}VxfKY!^$p108L(G?xh56+9uIWG7tkz7o`w}{X2EzuA0oBs-q z-;A5TgDG8<{MOC?qV-(O@tZS$_)T*Y0p#97bA!N%dsZRGD&?yLtDD_S2AAoK$Kahj zD7HamiJ@4!njGefagdw}*7E56NB@{9+Y%ae-f-U62i1gHcd@%<{AaEqkDt2X`5U;4(oas_@wup;0T_EAXiMi@Sp%FyGxX zrl&Tj7=11hn?w+_Vj(Uvs1Xs40}df#B}K7v(WQ7CB>x_@?!V+lCqec=KJ%{JAGDxS z!4=~P1WF}YlkX9bE28yGzNn?mp<{NZ1#_D0aIHwHQ$^HijxT|{*)$8$dAaQppsrT* zYPVejNq1H%UA1UB&^&+tfAPW}ukAe-`b+Sizy0~!9{B%C4?O$eEaFcpv+r9EQs+DH zq~fm*PRfZ8Jux^fz-rpJqJicR|NBADVD6fKgnX4$9ccOBe#)M(5M&0-BbON$+cU$QWhL(pN_^Q;a z_NG}UZi19iHZx#twPxU1lmk^D7z*ToOeU0+!$-vYv|5p-i5f;)1EZ}(q6G1GsZmZ*I<0aG$;A4->0lgb z4X-iR)C$^ioi<+@y@^VX5=avaNet}P3ti7nvQ^|-G?p24#_Ui@!<8v+8u~TDu@D8l z*0*6hE60_pS4Vn!^=hv-b}hC|mNU6hUoG}Xs>oYCWu>ZfFm&AX#j9=3?-_0yl*hqE z&7|)i1l&B+c;iTNev)(75&!M8&>LU%;kTV$+QIV`fj!e*Om1xp?_d#RzjQm>H#29( z?m1`PPg_6gp9GjQczzO~tRDri9_MG6Pb{4KRvYFwZD<2rQr=-`7ZLxX{)ZgJc0RNi z{lG4L1Mb|qlcu3efX3WW?$6J3b;gp*AAr1=Z~rgb@YapczX^TM=4Us)wn1Ec;lf{E5Z1rA zz6}6p{5gpA)KUmrnY_4~2{J{%-|MM5)_ppjl zXbJSp(X8zAy>^T5#u2PjbUf0a1{iF%qeHHb#3MODsP#dvb~n*VXQKUDG7`216nV_| z4byL*&2z(N__JU2KY#9PEA#xojP-d&Zt=W$1Xpt+?6jvjh{RLjs9WZe^-gabC2)w2 zRFF(Tow^v!SHoTJgcJI+nO^ic{`6!1=g*icGrc@xe5T)di|K`XIM9z@gb6JknnU8)mwtchrOY7%ycn5oz3xr&+aFG$N&7P(#jkkn$d@AJI5S% z{84(;Z?XwK=3ph+QA@9S z>nI79Vh&sY0XgLcs3unxL_;fNAr-~sGK-0l$WcgL;61i*Q`wJS^_d*Ee2$;_*7@fT zt<3SES<&bC{#zXxI^v_LdKE{A2}qM&=tNRbx1ES+hsl(hC3d8DT`o+4UzCWXk14R- zE7kcfKQgiQBv;K;!xGmN(PO^1naneJ&Q0c1@AG*+{e3I*{J@O#dEQ|%EvU*hoG3NO zjp%_>9ApbMrYq3eL;%@;D-pDxE<|NeqYi9ck9ppHnS1sKn48Rx*L|j+__39lUY_NA zrgxZ3Jw8R8g3&BZi$z1`^HVA(C(X3T#%pcX6>GJrgOKSekr)Tnqq)iSp7l9;|7K;5 z56!a6Ce!P11}{skVb|`py)h{K#U{Hw(v?Uho``9pWyE#0mBt&&&3W?JWS;rLeQr)_ z{&{_6hF4}8zZdT?nK5NjXi!F7WNSvd;$-ut0egZ5mr$=+bEMH=A|Y1Qq*3-(FD^`G z+9EOna_1pI8_1`i7eD}RbCYC_YT%Hk2uX^hU6fan{aKQ1PJT{qU za-5q?^KqY}`G2p>@dGn_NpV6!3KpEOxmclwfzMGj{7>z>S7!R?EbTMB!*qfa5Iiwt znZBg-#y!0rt>oKDkS<(kG>W=hRwkh484}hSsnkssKf!dK*|T%gDLvrxm4AC>zDKi^ z&-V_~345awtb_Z~R-JDdOu|xZ)r>^gRv6@Z2L%SQEz%O{xC$yD-)%b2tnWRnuCDh9Wc*<9?~%!gYf*tsG>w^A%8_5M{;{R;RV>&DF;p zrt?gubJO`z$Y=VoYb!IoJbTP%dWY#u4E0_~_nxj^$ zqr|2bsIWE2h;1f`9l!Kr(|Kk$`rP>6@;~brR%Up4_Wet}xE`tq6W)knYkxf*fa^WI505 z&H19U!1zpyhpRI6@Ba&HzqfYy&kmnIln zeecVA*Z1msN0(l^^t+dyy;Qw)dG{}NzqI?QT?U*Jyt?z7J5TQvcOKmS)9o*AKe?UT ze%ID3Tfeq7-om#Y2>r{@iy~DVW=Jv)PZM?8yZX`A~E`H_W=P#-k z(Ti&r{@I1+FUS|5_5ZT|()#DtpICo%?R7wFr5|`^UQ{j|sVF3rBpHNBD~cCM*Qk6& zP3U0J+@s_OSByxp2H(VqR5wb5z5ve*ACm#;(c5hz8Y}dt9>dksG%8HlmXIRhOr@7b z8I^Y%p5lT{zK_v;OcPEsa3zrkIaVvCOZ13{ii(lc6ax8}1gxFrL4uhc*ofERIY#p_ zsDW#UIkADa6dlE(VHYlP5m1)0BDj+b?Lp&qy+|-1O}>fwa#SCq7ga8%b^6taR5v+X zNyG)kWy;({hZ{(}78xcQLKhiNVLstwIzC2Bh3yJ2#oDffX7Uk?l$<1238M<#MNuP` z$;CU-aR%JVBv2pI_A#B#h^5-Yl86k@ZYBx}LUC#}1$^E6AopRKn1VZ-YAH#ivkvKF z6dzM}s&&s`8>*QrD+~xPk0;9|D2aeT;Yar8v+&W zVyd3xX zl!hDAjN_6b2vtkhd`!#7gsr4l8=;yGYQu~bjD}k!knV<8sz}j-V}1mn(Co!&DCJ2$ zM)Wbm)$82*m?wNpT9X9eFNfjzQoY4{ST`Nbhru?j>}KPN(Toj7@T4WPY_mC+)ATVh zhK~0jCSD~(COWLj#hN;{I@kb4iA2)uh=Y8q=Qs&5(ddMJ6`p;}$3)!H&~AZT2NiQH zF$top!Him$%2sAlaxf#qdvQXqp~R?5gkFSaAN4Vb78Yq^AUjGUSR=F?@}wsH_k+%3;>*&=W2ND%YuHx5G4AG*>D|6hX+<<84f3L%$5q z1RsM+Q6WKMipfJp)viM_pJ37ng*F&H+#Dewa}Le4pwuu?g+jjs&l*04%mEFnH)NEF z@ht1{t$ky4i|U8-;W|0x|`)vCs?f ztmb2s#x#@8#1NLLC+s#6Yc*nBBMym!UZ+;9I3TCbq>fWKJ>eUnpND5vAJcJ0Y(Lqr zL`#)y8hE|K%teWBv`Gsl&ssFofW!+juU9*v zpM__PkFnV1aDZcF4kwdMK@J;nCTunY+}0<_Otw~!>ZY9O=BHtT4E+o|qkT+_r`ydM zXSO3fQ0pSxPa`=Hp{i!kE|(tVtMRM`OK7{2bc$Z+r{P)2$24Q;(3q5f%Ujqjr0UaL z-o}l-na=9T2wy2R*nTuu7^S*-@J^qHXOxdg>WP${^CTQ>_e{j0U?|0gJ7jq*6H05E zV(~077%Djis~Mr6f@eh^gGa`+#thU-A_?5~2ZeIfP$$Vop2>~vRGDh&av@^pOq15w z&`-j%f{%%|ihY9@Ct1>k!zP|dg_&luX~#%WWjU8KK#fkCw3?7SF+x89&+Bjj3wXLO%}Aaz2JrB#oz=ZZ+j9ona};pyh6fbBc&Na805f z9+m^dMdYCcASLR0CnTCy3f2t_*R zTn;vGPqF|h<_In*a(RH%xCk-HgN$f7yWGJ|7d|$99gaJ6PQo3ufCvT-9wVBoWn=eiY86x*f^_)r)h)*!JaouaC(!Xl+0i z2CQ4Ahg>||q#B?cD#*R9S%7@Z_xPBs>~0<)iT!lUh&k{Z+!0l1!N8nj}j#>RCC-fP37V|Ly?o>k0!n3H4 z32?~~`eAs6`j`N>4xvxOvxtuga0%dJ5FZoZP(JiS@NAy*EWk0jj{%7wFB}Cpst$b$ zo?Y`X0gjDBKM2o0P%`iY~D`7R$5 zpk!(2DR}ll9~0m#F7!!w_K1%Oa2^)=1U&mr9~0ngD)b~g`+$!La7q;VI6V6f9~0o% zCNza-M?R(W&lxb!|DEMM}&71o6kRy248iDBMiSBPJ2%P$)EkXYcnh0ellf zV|ezkj|t#56Y~81|CP1J*A52tgZ3zqrs_|C4oQ?H_|jEdNM1zB6=z{my+Q z^gJjNnoo-jGi}f`H_hOnQuoq%_#KB^L6&UKT)OcAU(839&`(`y(UtMkad1a6dqcig zwA_=$EROl^aYho2EP;Zo1_7P#IHvQHZxx*Y3r#@h=$Ot=yj64ptYNhq?+0umU`TxK zY&Itxf(2RoS~ngBvOWxC{rK6kj)&jf7XU$4&Yc@qfW&J+;@??W;;q}yAnR@I#^vLF z{IQiK-F^rkWStI%(6<9&zJfisvar(?3|UsN09yd*#zQ_E(8)i#GMmMbd5}#5X!N%o zH+puqW5*pw%J;G=@N%OAtakyO_Z`!@@mA3Zu$0zrd@Eq%tMT=-*_@=t0hZv_jc)<6 zd^P^;*|JViHOnBP&b1O^pLA`?VVn9{1yCR+e;!8V7Lv0z>FMK$x$_ z&#o-&bTwY~p$oDTk#2mm&jxhz53kJTSd9bhT|lGXecb3zpY5};iW=YL`xIo0W8HWc zVB?SFA6l8s2~$#l%}(dWJAp)hEPrZciMNgA09z#O#y0^;zD@YSl_lLVmIG|9z-xWS z@oW8om4%%;mhXP80d{TDjR$--py|)7%;tD32iWI%>~y`kYX}9{O-na+d^Vtyrz^8L)|&wPZ_w!N<3>-; z4ii~PZv%Cs{t-u5VOopov2+auun9a}o6>J73aL%t_BQSw; zegzlj1OhztykEhEzy!|u6|Bz*1Xx!EIR#%u3)bcY0yx_Ai(Nk6#}1_69};M3vxQY3V|mB6FBEr@bSO|&iNHg=L7<*h0psHOac=)=T|Te zOyHbffj1`*V3Tm(ufPpV;GAEA8w2 z9?S^@xD^eO+vx1A)hB8p=;r4)UfGyj{MyAQFTUf#-@O2@{{jHN-G81*T$i`z=U&QId*xfh zQxCrGb`l71D<8z|^O?Qt(t`Wimhx6{4{(LAUvB|A;GBPLOFo;KFg&^*RtaSN-zJ zB5zau05|{ob^c~ovMYlGb>9wT?gLG-M@Uj=F{<2pI({HeO&%0 zu2&b$R<@|K&j(k!`B~0<66E$j$boMEdIeCQkN(2S)Zd(|fO@?QM9xQler1ujjs5_) z3;J~yh?*M`a%E9>jQ#-U=)j6Hi(#;~m0MZbsiXf*c?-zbX`hZi`m-z3xes@N#P!mm z*~%7i_I7-w(SO%vZ;-R}04ELlb?WB$z|W?B(nJ&Bgt>pc2!zd5Eq%7Idrve0PE^(F z1t4;+YN?e)-ll2+P7d|!`I}vdtt{#eRSR%N22_m%(&iJ*V=GHLUDfUya{w+&kO$x=KpucF$OG{E`=8ry?mxQsmwUgr_oI6s+56z7uV4Ddm#$xGTsqqQ z54*p+`5Le|`O{>%YGKuJtF^{$!n8`@-gvo7v5GZT!i`uWfi6_{QPIe|hm& zz`??A+&o-BH$J*~k3<$IiY__Vu6JjD{@mKd;;EozKmTJ;;Z)G_p63{pM>amXu`)gJ zP~7T$SU-`xe8_uV^Am9zYYj>#RKDrXrqm$SFFw%)io&j0qm=hy?My}-LFUmd?d z_Ousx7ij(hnN#INPc}}aQ{^m!=5nZ0A9LIp3G=LvJ zO~zfCKbP_Sr$UzXeh&Gb(>gh_JYgQc%+#rJmO*nl-*>8<L_r^;E@&|^8ssdAP9L&4r^;Cd&E>rBv>E#(W{uN`!S}EQ-+CIcyFhbd51uM# zS*?#d;9E|WvkaQc`Q}sQEUWdgocEq8XBjk?^PW@XEUWdgoOhopXBjk?^G&D9Syt;~ zIqy1E&N65&=YhrjKgj?0Ns#$(3ikRg$o4k`dH#AJ!=Dav`?Wz%wj>}_*Y@@t_6bt zdCG$1CtHQVI~NQBWH1YIv48e0?o&6uX~7^sKC~ct&{kpajs=4N+0}x?Y_7uKfdzvg zk)Q#b3RYooxL^?AsX;Qnt-|17!61PDLJ+r+RT%6q7<|Lx@SeJ{w_p$;UtN&I1ZQ9L zK6T^LfnT1$fQDp4KHRFhQQo5-w3qy6k@XjQ$7pQLvIfrT?@ZKnl!SNp>MT5{_x2z4iWS&T?AE$Aqdpset3>;%1T=tnGSdc1pe&}Gx+hF zwZeO1*DoC&_5XToKkjJv{BgJbcDLU)00V)yz9EUAFs>$vW>YjZ(IGL)>a-ex<(`!2lx=w&qcnsjA#?IsnyNM%PNKnj6q`zYw}Ri_WL{$@=-OIaf6|u{UdOK6-PAyrCdq zoe;!c`43n!9}8dMZ;;_w2)9+O?+-(NLiUNsn%xJbCXRLfVP8|j2m~jA0wYa00mmUI z3`~mu)rF!qL5V-`fZoV7eRaL5QT}*8L!(xrxjU9lS{c!!7j>F%)G8IDrM zwOZPfkwL2#aSf?_txk+NdI)#w$kAv&zG$&tuNW^5>@2 zC)?YV!e4Wqp%lKD`yK4@M8uzEPn>4dQGs-{9+OO$1ai_&7nE!fhu|36LdQ9C*z0pq zw%q5XhMlQ9Ozi>hg-yjNf!H*mr^Jv9j^<+TrVmuN}U6_{!nS zhhI5->F~>kUpoBa;fseafM|f{4xc@I`fz;MKU5E&IIJHshq=SY4iksa;UkBa4<9^y z;BfnJ?cnu;*A8Aic;(>bgRg+dfiE9?>EMe8FCM&b@chAZ2hScneK0=gAE+Rnpnkv{ z1j~rY+c<|tXgYAPg;Cb-c{;MGWz{~qz*?(#O%llv2|Kk3O`!DQ24{8uT zyZ`k5c)!1|?mw|#-)Hu7`;YA>_M!bp_Al>0xc|WZ_Ws)5>wB;5y}I|x-phMmxddH$ zo&}W;$2CiaT52>LiLiG?6%7IFai4YWeBy>6SVCaF+c4%$$_088dU)_8K_a>i<>WQ{=e+Kd7LX*SvTI@NvgYVci+oC!#cNzW^KaFz6{JTl~g6EtW`;6sS*rH zZK*Y>C8>p7Z9v6g0EIzOa6{Z)hkX+T6$O1&a0ORXKtxnt^i@Q_w{q_7+m}q=>Gbu& z@cVsk|1sY?-OoAoJ?A;kS)cQqH-+vA-5q*)=*1z&f7NjmdTyu}!b7FdouN!95_)pz zeCX!TgF+jjmEf0yUkrZEj~elb?T`5HNWN$L-tD(+ziIoP?Yp;MzWw5DXM3_e+J3J8 zx+K0`+P-r;vmM!f^7i@do3|gdy|KOGN22`V*5|f9we^Xuk8OQq>w{bG*}8Y@En9Ee zx@YTd|0T*7Z#i3&t(35`1#-eDLPrgMu5umB5z+U-Y9|ek$+@-?!x>fe!}W6Sz0< zmcW|=_XO?^ygcyYfD@SbFI7G_&5&9E= z1?Z0fqR<}!%tL<&FbDksKp6Tsz&oLj0elwp`vC8Neh=X7(C-3#CiK4nJ_Gt4fVV-v z4KNFR6krDW2*5P-TL4qgZvsq09|o9!egohI=+^;04f-{J`_P8~J{9^Pz^6dJ3h>F$ zuK;`!^Z|fRgnk*|6QK74+=Jc+@bS=l0p1F|2jDLBZh+^ZcLBTwdMCigLGJ+gSm^Bl z9|OG&;G?0p0(=y7FTh7azXb4R=zjsc3Hn8VkAQvw;KQMx2lz1P=Kwww`dNSvhTa13 zLD0_tJO{lQ;92OW0q#IQ1#lbsNq}3>PXIgv{W!o)=uH4Opf>_shkgto1pO$$AoK=+ z0q92nu0cNxa20wzz!m78Ij(;VdL6*8Lazn*U(gQ${0j7g0RIzu4Ztr$uLk%Z(5nFc zJ9Ia|{|mhm;J-n40sL3!2LOHvdIiA$1ARZhFGAl3@L!#$+y#(MppceytJMKmzIl#Gwwr2Gj;vgGhiCs0FYLH34D}0k8z&0Ex)2nF~I2myE-ln0oFasV?B z3@{Dd2`~&j3*ghBI{@xOw*!1C^h|(Hft~^I$8p9EzAJ`u_Qd;*jPxCf;!Ve%3t zE@Au<#x7wL;5HNixCLDRcm{epz)dI&a07Z8z;$RJAOt-XUu`d)@jufUjLY3-AZm zcL2U-eH-Ab*S7$^YW)nryVo}X-sR8d=IMW6eI4K{)**o3za9eked|GhFJBJ;eA)UM zz?ZJC0z6n>0r)-8e*?6juLCrpuK_fmuL9Je{{lFHz5-B#{u7`IeHowv{Rco9`ged5 z^nU@4pnn4>K>rGmhrR@mgZ>Xd7WyK<0rW2bp9}pnz&`X(0KXIZ0>I}${|NBe(B}bu z2lNjBY3OqRDd_J3_Mp!K>_VRb*nvJhN9doRn*e?R0{Vge5d!*wJ`Vx?K>q*%{rIzD zpdaY(A)p`Vvk=e^^ce`~2l_Mw^yA-M2Kw>m&_F-XUqe7Y&|g77KhR%7KtIr*K|nvy zCn2C8f5r{;1N|uk^aK3~1oQ*_4g~ZA{Wb*j1AP<%`tfJyKtIrLg@Asb4~Kw$px+1q z{XoAS0{Ve|Ed=xfeJBL<1HB^z^aH&;1oQ*FEd=xfy)^{%1Kk?}`hk8i1oQ*_LI~&w z`uPyh5A<^(pdaXGLqI>!&xC+}pf`trexRQY0sTNf6$1K!eli5~1HCB(^aH&y1oQ*_ zSP19``q2>35A=o*&=2$@A)p`V^`VRUxhM2@KtHbw{Vl-PhW-ZN4~0Gj@CQSG4e&Lg zzXJH`&|d<4Rp>7O-W~dLfUgYw8NjR+?+*P5!0!tEF~FCE{s`cU zLVpPGg`qzH_=3>K0X{$UF@Vnt{XRf9^m_oE(C-4YL;oA#Ec817r=i~lXofxt&j0I|uK|=p9|9DvzK)^Hy0`~yz zd8(#x#K+PpMRttfLdr{7@uXemKp*feII)YyqtT?#7nyX|7+nh!C``+TIBP1lmBiFw8D>_R*3phIM+wyd}lX=c5wnak1jOeHI(5N%S- zG;H=lp(~{`J;G~oOf_0!sfIb8MlzbNjgX~I><)`=aQ+D_!=6&mH8qJR#ydw`pM{rtS9&i$O+-W(cQQ{HcNzqu=}lPfkH}! zlH*>S8knM>4Xmkg$xTKX`(8q$v1#3w$wVT)DLn61j*TUg{`KrZzrn)expl}fJLx(F zlu2XKzPz6%J$9Ct69p?Z(uWjh&Jj!kn(3YSzj1v3|PHZ$@f#H>(E zPTE*`9~-9hLd={^G9{06lD!L7D~@*3g(+;6_l{SWQP_C2f8zY%^&egz`)>mNp?`}1g0s!DPx9Xe{P@nBcb?~; zzds@HOh5YnXV>1pcJ~_RzZdxX+dsAKZr8UT@1NHG?$%Fi*<01EUH|>S-|p{{OIP7ZJPf1_+$O|1b=Je4gQ;g$oeDwHwAy)KZjPK9CVZaw%~_CuMNrm3G&1J zHwJ$-_)z~m_5*=e2ZZ1c2F2hV-|jmCH~3w(2A&!u*4B1OS($b`hvV|Ynl&tRyRC(| zXudDWnPo+9I=8v3=p)W;EGzo(bL-2BKI|N{tms3}g)WM=o0BSCo#yxs!_1?2Tv8?DXo&E7;MSt<^8!w603hjnQHD`9kZs9Uj$}QA@?`HGT zWkp{E9V{#QLg;&z6@3Bp-OGwTANsCkMV|+~5VqVbMg9@j?x3K4P=E ztmwlxiDg9}wuvt*`q0hBvZ4>!tS>A2;LX~yq7T}vUKAyne07TD+J;rog%KhQ+=a?I zTRnTnvZ9ry{N&)?d4Z23 zWko-`zP7CBXVzDj75()3$|X@|FrH9+ZCv*EjwGt-l^1Ft@ae!mEGznVfzK@~`nQ3< zUsm*Q0-s%0^izS)EGzogflpr&?V@C_E${_;z$5HXuP)BuSCgyBWknOKiDgCOtMO$; zW2>=cMWd_H8~6XOx@!G4{eyr1|5@Ou!oUB2ia<6ehlW`=vz%{?9eY%9?8U%S1hkp& z^q=NwcOZX?a5cxDS)PXAF;}ZDU%v;3g;#Ew8g_fy$r-#=EFrmug3{_jr#R<-^8NqC zn5*+t-#+*Mqv>q)syn4pj_jqVa;}@LHDhQxMdxS=Wk<>dZ?d1@a`Xj_wCYTfiOuif z%`4*?jx2^GEg1N))DR`uOt}%eZC5HauIb3axKgpJmX>5jomRU$DG5XjE#w(BLq^LZ zmMYDLwP7*RjS5&^wBz)Js4k%cbvmvh%k7lTi$3=ikng%bxfb{U z*wJYXap4H!8}7mTZQ%j1q?7Oe-^Uhj-2eBF4Znr=|D&mF@~Zd$<2`*mj7V6~(|Y5i z#x^Q;H(!oKykwl(M!61p&D{-^Z;o|Z7xs%&BikyVPSjDmqq-yH-CQqQ zPs@slCZof;#15&fVN)flRhqVQOtaCF2%Fzeph&`wV_hVL&}q6iNp~eSx14Lzyy$a} zgxOuUgJIsk|9=XbyMEnG;7$)q5C-Ab?93(1Rw7rwBS&5mHYqM`FMW6-bDX{`~Uapul#Rat5aYyo4)Em z-zm^2%=5!pLhzgmmev~P>P0e+lQWDZ%W8c(RaK@R?+DJF@0@bE6U7|A^#v}1z*TDG<%zMq*m{o*9u8EA$8ct3TiWhh> z?GCBjas&Om=)UFo_y3t}iK`&!<7M!MjAbIc_5`1}9gBI^Jn`8C}C zzfa?xPZ19tUcb5b|D(xheC|?vtRqeqtJbIcg-pbaqMoNCoaf<&nO2()RrpQmTV`}>^Q&Vtae`XOXvTOTY32klnC7$+&lODz+=|# z+5X&)vi7iTHux6*-u1Ir@AhxblcA>t-n99#vtNeZvsvBv$j%oxko6Cqwbq}xy6q>v z4nHqhgXSUp*Y<2(nKox>hq5YdxY2R4)mlDp#*I;}7%g`C2u#IQxx#iOQrH_4eM3j! zW(UEteVPr+X*{e@G-66_woq*Ml9Iv})Up>wv0YL*VOth@+Z7zzIO94BGAtOf)QvP9 z-zzyEa|e-0XU0#k-gwBG<5tzIXZGSXr$D5LM3TT_W6`!pGe;~ZXG}F7R}7*ga>%&U zYYmWcvc21Ej74RFNYyegRrnT;i3>}`;MpS%{rYH-%N7We)T+}ykyeFXxt-H`Jwt*c zx;yZCQF3g!Y-hYzWdytzH_0(3QAs^hj|hZRs135-6oun{uoQyQ>lkc$y@asqnLT`j z;U3rPRcF(I=Mf7W)?atTp=EYzg9JMC+B3FgM@B+ai-~k0Hz`Jpf~^+{upA%QrKp4L zO~NCcujx{Th|>{sVtQ80i)vxh_fym3O>UekqE0g6#M)V67j@nGSYh+kHd`opJrcne zID`gA9EjGSNIC;NYuX96+*fNvo8V|Yo)GG@a3lfuvA#hu^q8^t#DVJ=afES)Dvl;f zrF_iG_3@gLPBTQlEjv!kOQA(NSzyPz-k2odQPEZrq>bh)*08$3Vf|OGi9cNIh2!Z zXf?)(&=y^e6fi98H*~@5!g~$Z>UU~(JUJvWHKW0ZY;w9VO|Z1n68Utj4qLoA>haws zwL75gT7^P-<)+fANJPC`Tc`&}J>pOor>52{RBDC6nDYv9cvx#pXwTrwQUBn3RvA>9 z^-4XOEGPEzlpvC@3K!U}%Cum|_I;Z)v*(ZAm66L)h#}`SykDB~iOgk+>$+}h|@?tJp>cVoYgd+^9DU(>aZ|-7Bu{@DHV>W{eQ=5c^`y62Z`4NYq zi_n=FMwMN+o8dcr8IDXUvrdX`C<&>NYR)q4X+~?;d0~&t+Zle)6w>A_&15OLkS(ft zOw8B}LS&gb#x=8X%CDyAjM=UAM)p{*_hy{KAxNu1lMA{y^R*)mwb67?jk=`>5l%*O z?Ie}5k~x=;pvjzockFno$qjRrzN86zrs56?QiNk>ZL*0)M=^&}lDO9L^5&!*n}}Jp zEz-JN9cb{bEKRw5rKC~=isMmqLQo4FpqsCWLnej$AsTfPcRNiAM|!5%>{rx6#?kez z3g={mRq6w@)vp#4OK`a9h=b6H_99a~IxUGZ(rhxg*>>nMWLSA|gg%t*v(pdIF}%=`dVP^&470=}iELx4>cThmSa@M0LQy zc-7#PhA?5|p#^7W4YHujM&)?6J;D8ZM4e|yA1!n4Jen#n2CI?h@n(XPw0 zI$soGMzrR3jZAA0&oC5c4l;XLS4t*TPryljtUFy|YV|EKopX3D*6gErrPLv`o|PXc zoV`mA8nQ)V9j+z zu&l@>6D1nMb&S z*BB;RR=s58^J5pST6{R{_eK@P$_^6LFh6Azy-8nKq8=(o9QYC*&A^yK=xiJ}qx^Jc z)T^;ds*NLdKcVP(r)@@sOo|-u#V|E(DQbyIV!fC&lu)@N`kl6oWP43yK5WSZ_mJp@*dI1JTNzi4J!?##?|U0%sa@&Ks~v{sE%3LQ?& z8pWJmEO8{ZhsLs4qk>j4-KcG#910I|LsSrI4kx2IE1{0dUeBd8uHKb*QNGM{N32wb zo5N{|GW7N$hrhalgOo1k{IRK)j!de>q}XV-!c;pk6I8ZDV3V)3IH@k=R@j&5!(Ph2^NxZ#p|X&OY3Ouf)iC_ z6pLEzGSM9%F*a@3Gm{NxiHSddD6n#Uitnq@&HxqmN@-Q8rBzk8yiDE7khYUy^pQI(Pbh+{vAtX* z<<*3OH>y&*^}6VKlC4NSUXRcm_nhGyhZh0q=>d! zgNlca;##~sR5^HW0!P&}u62fP25ma=FsZm~Dy^{kD4|q}!*Mzqr)ra=XjR}{nySpC zi9$DBtUL4Mrn@+Y30^T?G!vMU&C}QfD`LX5>yE_fBwm#K*`yF=fy4Hbk2r{3quHu@ zt#W179gj&eGD73wQARH0bksGfo>J79R5szLiM^KA<8(S!W6Xwv@UrVn3@b4x%dwG_ zb$EYC<{)lF(Ta{R-sM@;RaJ#)&m_Sd%{uO4J)D^xanK5JgiN7E(MyySzS=1b;>9`< zj#_#V>qgVVQcoew$bdlGdp(`0Se_~;vq~B3Hq)s|KcB0WVHAxqR!zf;*-SNFh&9GS zdzUPg_)e?Q>iS(cS!%-CqMyR{8;>{$SfMO4n%Alq%7qeHuTg}7c!n8Qd>u8}QEvjP zF|jJ3nLV_hBBGiYn^w{yGKl8*Q4P-L%gty6%VPD$sBK8GR=QIl6?B*DHd)(p_0nwW z(1qEkzUV2iamAR`$RG_P;ZMx%L8XPDT3qeUh-Ng2)-%qmqv%qHwDf5?iP?J+nGsz} za%)jbDWC z<|8*=6@b^?yB1yj+11^Z*I$PXfcs{#{^k4r?#6F!)WD4zIKBpU)-HEy_*9zYCbLF9 z!5Fwxm=+wE5FBhqjyhVwVij>%tM{aCl(TAkq|g$^X_bgJ5!i^?rombTt1zW9BfT-{ z#;I{r$}p8?qzq%bDyihFG?7<%WJ;K2$5|Zgp~A|_#;uomket=b$9bggx%p{2QZtNr zew3Wnt46J?;z?|Rr@LY!;j*-_SEh#s-gd-ctJ2`6T)09{ ztzQ$FC3tjJR(6gy4O))Bq{)dhzk46ISku$pp&pCUw2;fh`jcX^ORGd962l`Jwg=;b zniGp;b8r$7b#wyz@g>xzVNJq>6mDTvj8-WjYLTOOWVa}`oU&P=M?|NTlZ_6pEp)JJ zx38=~Z@F9_t%^!{dc5Uj)u^m=#^G+YN294(%4=!Eo_}!WFj3O!C*r-@9@cP%HO=df zYL*hV`%NcWbfZYbOR|kJhoidGiuy}P72(9hZlhHiS#q&0P1Z8dMdJAuB(S7kktrw-o$rPF1uO<8m7g}oRh!EMYWKAqWr>MQ^5Jq9#h)=Us>jL6m z4=t8^si^2?iVZbVo zjxQ8Q0?+8vvCN27u{v#7qXcPnGkJo|v{ROysSsInZ&0Y9@Hm{Farxv}=gY}4LlK@< zck6>zGTt56Ezw_~wj1fGxjUlp)}V$L&8jw|TBVhs6(?z_7wr8S5?u%$&@gg#UP`F=#f7Ua>)xrNoSqwut5?__=?m~U+@GC3l(wBJ*M+(d1SxrnUj8%@1l~gWOrX?;is!QXd38xS) z9d2}BqtxERRd(oVtgff&XtmP!lC`c{ic~Px?fbj$iauov5qlW6ORlhs@f78H)l#d$ zx#gUb;}`8Vc#A(FdcqNpWVlzEXsH@M?(nVBpxzbnes1UoY996_HDViCPl^hI1l`B> z+Hf{0uzW5qj7Cj0Q%nwMFN(M^18Xy6#lRzOw$jR}RTCfY4(qL6iyW|x+6XhKNzGf_ z*nmFf^SI?QkHMhI<*Jx0Oor76m&r%*T!*HuS-IVcwFtN~;N2GHzfLgC+Iwz;%l8zz zf%hYhzlijxeyd!ou(oGR)dp^L)krMY4tt`LtnJEnSr}Sl0rm1aHEj3z#daF{d%r*Z z*_W(>Lz>mSL9E+}2h`S$@r{8)e%OSq8df1Rj$M%iC#9|iFXhpI!==@WtQ&2GAry!lZMH<(F(y$G8xlt zm5VVG>lrjwjIm^1R7O&xh*F)hk`s1up^iwS5tf5{6~mntD~sLO_IaPjQMXR3tdx&5 z(u`UZU8GQ@5CP7iSS;s=mg;XW%X&t2xslkG(7kadrIdToYMAJY@dSfTrXDJAWxDL) z(K3RQVyr_lDuXrr^&B%Ux(!&MG$lvFx~B~muM%wjh~ItOb-6z1s2~e^ae}DjY`7Zt z?-mP@maAquV%@E2?R--n7L)CYJR;FOODb!zVRYK?`&EmfBAI5U9m&-!zrRyjZNcCE zEmUdRjSv%i7vU5>2N!2~gX{@)N3C}j%yx6)*T+v>=0TMme{4-~`83fRjArpkyv|pv z%oI(xRd^`X>3%%l!gYN}Fnb0Zr+I&4#NT^$#|hqF|3?yZ#Vn>=w_Os_iTrrdFXuB! zZ)Wa#)4a}&#|@3|%9KH{&f=Y!4RvK@^Zl23^cb&LG_bnFr)w3Bslu{e9Ofk^o#(tx&JjXDM#g! zjpfljlI`Znbcxpsgjd$W7+!PA?Q$ne%wl51#XDgqBU;h;SV&X5YRz;e159CD8J{*Z z^Tu( zl8oBqxH5@Gs0cdKnqF2>7=0+2NH(Sn_{P*fSeeY&#BPp}X?{cq2IGpg5embL>+OKw ztwWzb;t?+ON=>)Krfs8%xBWMpm|Bl=n-OMQv*=c&B-5PiAqrMS_cFs4VW!$@HR`~Q zIb}M@B$=WqDyEhvnM%4lm3?bfNX#Pa-G+`&I!fMFD=gerY@@e$Lu0MGva6o$EA8=ZFn6OfsSrxlz$u&&fHup|3Ras*CG4|W(%4HtyPA8h4 zrjSCBD|af%SSM-A$^c`FsotQ9MVev3M4EgQ77${OjpAi9*(Ss3EXP`{u-O`ik)+$@ zo0#9U)Mf>iculqlIYI2Uc*+{INBP3otn($l-C1-^4*l#!|9^TO|8GBZ>neTx(&EN% zPhSHsL+;97j1Z`=?zOmlE0;0s1W%%p(6<^XTF<5B;Q$}Ejb@(hQL(Tt&0u{fT$3Ns zl@S7yz~S67xPjUq8!Tstn&7){MlUv>DvTW$@St zf!`vpsOuRaP!)xdbzU2P3u6SvqS4G%-2odSlX5O_`Ak&H@REq->yjaLjFi-9i2J&~ zdfbmPPN`fH;?y;JNTD4pieZx3 z8Wjhu)U;Gvt7e#erIYc7Owx^(_tOTWmojZxyO2cuX9(v`W>al`6Ty!-Gmnzw2a)W9 z$1I~s|HY<6%n#D!7k%#cAG#|ycL$uu2s}koye2a~_P))rp^CoQR(pz@MaNt1Dc%_e z8QjNe)gfohJCFz9hQLyezLz5h-mh2h{~$(Sc41OEuZ(N->WxJb>8r*F)Ek)u!52Kb zLCI)+%yD!KZpABZ%^mB?#CNB&9AdwNWcTaW9_lmQDvt+F4o>f5xr~Y02AL8f{>xq$ z_}B$TQn^~q>PQk#cgzBrKRDU1c~Fao&%T)W#K~kzdY? zFBScm7=fp7s|PUxOLxJ*Bmc3hd#7mK*RLZu_Tk-cc1LiUN;<|%`PRLBBI^4aGz)^8 z$`$h_-Pc*A%2!5KvzJl4K3qrq^;W~bZ_$Yy*AZBp38LzL>$`y~{gCHl;CaVe2WAj%g{L z8=Cf*uGx;z)s_5mw_D~#_Xkx!{@YKWa26@{Bbs~4k;aX#m-p9>OFEL2o1k4Gb3>O!)v7b`}a=CA#+ z%MPFO8lID$ut>dIt&$VGLiAFzgwx41xn#r;;wj9lkd0EV6uVFxv#e_ijcO_0<%J6r zZZ%|PSa!?;j$mz8)^LCOwCF7vh;v@_x&Qy_&dSXz-@CH$(G7I{L+iIg?}bvKw}qa* z^1bJt@4xA1`LFUR+yAi5ZT-#GbAms2=96dI!6$Bhe6tq#@xWu(UcdJ6)mQl$T#uj2 zn<40MBFDqIcAy!6)Ed-`FWhtHfh4No)tIQZO%RqaG8xfZ-1FbQ*Wbi^9#Ri9(E!9n`$k4xQQgan^T4mDXAzaRo2_-e|rSeFwE5m!KSvOMA3vd-F z2~OH{C^Or_dq%iqOz=c3Oe7oD)RreKX~VnBtmmJ8F;2ZcGX3*GXJ#!des<&|4yx8? zaW<1>^HG8v)#P!k=pRs53zI}Wmq<4H2^c1?UiCMnW|mo7mlYJZC#i zxF*W=)<9~`q-$7< z6$`LL$J*7BScf&zv&5dlfPxvukDs;sd)y+Hc;?QjR zms0Xf&-WqNOHmxr9dcEZlB7tgG4(jH71jAEoZ{1kOd7U%y5xmpbu?bv9dw5b?LYo7 zP|Bi`M@`mPXb(ZMr=T56Ux+pqkKD`*b6r@ zH6kbXri_1wl!^~yxn41gdT`B%nr^16viYVTX9CI8a`rBUQ?-1~X52dF>8$8`mM%~T z{N53VPP#3KyxBoXywP;JhBDN9l}x&s&Q%bFG)mJyeV*0GXTP&;m4T- zjZwR{W{9}gld+o0`Yt@>%A&*f%I_a>XwLjMzhkpWs#C>rzT)&7x>)XeatIArol zn$+78T`dq4NyhV7qRx1{*i#&%tRm(xGTvju)qbg$7&A40W}6WG-5@tL%6jz}Kk+@X zGo9%`oBEFTQm2wx;-b`F9&ym*b}q_mdMtu;Mzcn(Hz`N-Nxp(kD*bdLU$gKeXALCVAq=X*mL}swR<%uLKO;P9T)8pUl#3R$sy#?5NlXeYbftF5fttjdXEsj=_ubW z;QE4HY`*V^gX$J|-_@#>v}mF?&3JwPK5Y`EF}b5o)QaSo)oIFqgMxCHJaymzq z>QwjrX@oTG)Z=)Eb{R{}vDJ3P;V~@5l*o3jAhCSBKiOme?%t*F^j9EKJ zjVo|nMHDTbbyCGjvB~tTI-RNMxn8bo?iGflpJ89|_tl1do=%lcE+YtvT-R$BHnx1N~ESYknYR;@;$M@gD zbW@uZ2E#Vxp8+R*-=01H=suf@;zqH<7ZVKa4EyR(H{VWutdAWQkd)H7l81PB8?H)HqSDax$A?ctb{_5dxK!uEz}K z9wdA9_(;#8nq*h2g=j4z#b)tg#J^t~?^yXVudtmySB|41Q;$)*%6OolW_LhRw$W+V z%hkmpYxk%c#6(j>36>TolaJ54jbf23frf>exnV!e zm8S}&MYGtrLKplvWi*8b6%(XVHeMD;tCJhj^1x&gB-%~Qnu%1UnRW*yb1!A3_ETfm zq-0Yq!4Zm&U$~HT>E6CxEsgi1T%%fT6-D2<0?zEJGof8VO}^;gD8z~Oj4v*j=4R(e z7i>haO>@M^R=ulmy|N!iOk)b&nGPs;{+YWWl58;`Y5U7wTyp{vRCJA#^Z3sr3&3@elT5h`wLq0`2XFNzXYBcWtlhNw`PC1s-o47Krl9koe+~U+==C8b^sLZBf}inUWB>l(cLt-u zVBk*!KOcBufC%iZ{m0sGpZmkLA3OKvbMCq7xm(VD>FkHkzW%Io_E~2ivh$g3a_cMp zEANFfUs&DrUx|N`|E_?!^~jyj_9wT0VdXEDTU6VPb=nV&4LZCIW-n{380yWvfi~_O<(kj+Sq>0 z_G^|EUf+K8_N$i_hPGd|{inO#fEiqTA@^S2n+LiG_ujc-5>lsyK&RXt!zK zM}dJS2A;UAa47JEz!R1g4hHrDd&>$30*?4=dxO0tR*iyO%OHXH=un z>(V8QAUvhUpo@VP&Tf2Q;{(eI?`-_?#xE}`yuI=MjrT7rytVPZjYA7Ok@Fe9FM98? zId5*fXX8D~3U6$@d*j{93a@XxYvWzZ3PT(3+<51*!l8|KY`kMx;o!#GH{QOiaA4zY z8*f`ycx~gY8*e={*OMC3#=RT&9%jtLNTS#!Cazs?EL6be9d|4%x^esM%Zjc)^O?(v zLeF@{vZA5eZd+C~n9VLL8pvdp6fR7y{1x!ZM&>Z398{5(@}fUL(J-ZnjrKq z(7!Az9P;0p`sbCEOXAi^Q@qelcc*(O%utL~!bSgbx>*s7p0c>}6x!+RbPk12$~m~x z-f15SpOkZOhuk3#g-^;ku+!RU9SWb6b6}^r(>xSDDd)8vVuv^sJ}Kul|8+3@aH4Wj z&O42r#-Z#<8Sm70>W3swn$NEV-Wz!DVa6v32i_AnoFDktAnsq+&hZTp*`C>U&QT2AOJZ>7=qEUsmCR>wb zg@aq;t?{zLfh}!ITUK~&OWjfr^>-5IEoDnN%=n~b!upw&GnadTlZ2to|K9xXWpfT~ zetq-n%L)fKzqa|cWrYKqU)}ubvchYd|F!vFhr%bV6V?My3Own?`acr6vHrjE&Db03 z|0^EQy|MnkqB3r*|F5)@8|(ioYvacH{|ZC7vHrjO0{M;g|BdzkjrIT7v~J5}B9R;G z|0~Cg8|(ke-Q^T*8~XYe*T?=Isv7!2NDh8JC|9_m&S=ciea>nGE0;4)Q^NshS5URcXq+1GUbi@n3oZSOKIg48_Q*i zs^2XZ%xSv^>k*;dHzO@p8Mt*PoLGjy6HiSbKU7tZ&v&AH}AWPAtbsCfhPb09gY#ZjsA?x%{alRC-Fqn59MN2V>5lG zpuMoJw8OqfAUSN=*INa9rzUWnRq*&z6S&SQxD^mMMW)wV1-qvvaGh0f9uPQ%ZSP%Y z0=EDHC8{CnSVbphe$=u~I^3^Ojj@YYIE#$=Z>Wgj0ixRyBK0egy{J_4pM0TeIviE_ zq|uqg((w{awWxI=6wF0RfTpWA`<~te1XMR=Da zd7)x!I-J$Uz8h+$&r#P~1rIwlf$OY-hn||ibymSc0D)6#TwYotnUP zR>8T81ePD+uD1%#o|?dQR>2M+aEc*z?>ZCM1_VwKR_XdadH%vTY@M3Gb$t@@g>N`> zY67Q-QgL#;jGZ(4E6-Z_sg-lTaqi{kis#nP{?6G~o~@tV+4=a+5Bc6MH*f#x_UpF4 zWBZn^zuP*$^%GmeGoL>5mNUxcX!G-%KX)dw`LvCH-1wypb0fC?@9Xbech@t}SE2Vq zFM{rX)cN{e5={GkvSFf5ra@F+EL+;HU5GRsfBydPRdw`#qcJbqfX>UJhTqJ(=f~OZF zOnqa)>n9aFow;hk*B}1G>_P1$^7kQdNIrW|Ju&U##}CuqaZtIa@vCbroxUI9T=t-R z;>Q;=IQ;nS2iS?Vp1SJC1D!h$O7rxq8>?4sZt3Jz)32?adE8602gMWVFJ^H_KYLI( zG3_G#!?e#jKra^keyAs|PW{2LPiGI16TiNg!{OI&KgeIy`u#Nb#XjPS=1$*nkOS;D z@2AzougZS?rZ;8};1kI&5;!EEJ-GA4w2L1GJazRR4tmIsW)E&V>Erjw;OOJo6F6g-9b!bT~+W8gf&tU!>Ewqw7h@4mlSL>s* z2TwaO?P47q)?fBu|HQP5?2pnu^~AJ`HFucySqD!#NpHzTOF61FUvJ~v51w+8-jY}E zrsjHk@=1D2E_M`03<77$*@GvZRQ&r8I4pkl;0Y(DT`c}l+PxFgE)qXXd;7uTPpq^> zJHMjPex==dVx=wiF;}ENgWYkk3o^K$UM{)V&W|zx^MUgxmVU7Who#RR+;U>t#g8AQ zecXv@7fXJa_Kt(c&I|1?maf`GBo{llqe8DiU%KPqF(=mAqNN>WuznMgJ$Uqq>=zjv zvdcq5*A3scc`@thGwU?_d6O)VG%MtOwJ$D@3d}6^DySAhB=PuluJ-F#a@{1W9 zlFuGI;>5Iz;hgbgDEKwZ1V`ICC*3`H82V^8_ZR`42Jp^?K`7CV$CA&u)wPc_f-8bcPxlCez8Pd@Ua%lK6= z514O`2C-yno^PXU##trhH8T5>R)X~dL+;6i#aYyC5KDC128Z8{2c)KuEN~zUdi}&xBQvG$%oR*+FkrOyZe^g(P<5F z!Hy>W>tIG-@w*P?fwH8j$r%@$@Y1&Ezn&6_fF~p7Y5Oe452ni8F6YsMFL#5_yH$Pq zJWTo1wSmg{e@xE*dg)i+P;QXj(cg}ZhU|avNP?N~dW*jAY=5ALgE{#t^S<~10r=&d z<3&CTBrkRZN5nz;;XpggN){RU@ZSc<4w!lAfRL!oL~C9gPyvVCP2B&`H?g}(q$2b2 z=h*IMW}>D1g{B`NY1V`bh=*zlUrT1|ayQ#4%wje($x?FCPCN<4m zs717)ToEh0)bPd`!*$YjwC6{O>|~T!N0rON)`bk8WpWx%r{mG0+`3?y*`(J)Dg(FZ zzfqm=UmoKU6D7A)g!I_F=ze$e(%D@o80Ov8>wE0DYvtnGdfgSk%(}m7mqM4)k$9=$@baSj)>7;MlN3 z)?#^AYb)Fk8;($};VFdCOOE$t+ZAb|MA40ia_yGqM?uGP>@|=5I+2tf_HndaR7Eyz1gt$n$RH*jO?d z&E7WXH&}Q)uOly+WhDyh@C73ik2GjuR?2483utmkr1RaL64z}W5%*Yx7;3!_6H(&=#{M2gJa`);_p?&i4Y~*Wb5#)9OuEESbq5N38AP6HzVpMuWCD zR&31a>CIM?9FD|heKf79@oB_$;gnEqm+;zdz0Ar~&6Einn_yj~D=$WKTl>CCnfgz5 z)u+{1sUw;?Iz)w-<%}?4lJCo+lpv{bi8(by?Z)SO;0Y}6~_3kQ2^ zFSwX#*F>mN)u`lnCDON%4uV^}RqOQOP8g>8BdXwy$SgNjaSz^YO?s$nx%P-0!o1~n za>in&gG)^7nyARI8;cv7o=?yEEhT69!DOungW3%xjm0PWP=$-dNM=`;M2X1pHQUVN zesInTz8I2x^*=6V+KM2AA7EK*RP9OM?53NrKg;&UWv?AWgal6ePlU2&Dl58a{}t5& zTjy-0qP4|Z&e15NzQA*e=>#$aD334l|93W`mfe)eIAk z6_aITfH5dh&CX^7PDr&*mZwD=cT%FfTcUMu(iC(~#v7JnS{-H~)0JPim?`PX>8jR; zC;yMVH;;FntnZvh=&IE2?Iy`)ia+(Z#ngo9AYuOnt!y3J`P#kUkCr3 z}!Zj4#_sv+ocO)Waf3_+=GtU~xfmc=UTtQM1aHIkuZ zZwHPb=~xdYS{hRHr`c|nCXuzqUJ^Z%UmJpEL(vdk4l+WsK((@rQEi8u-jyB-f}>w(Y^@9GbJV- zcN#?m;uytW$rxpe3J0w<4zKAw922!@gi7H+*ljeuI5{NBaxTQ}$P_4+a>XNpOX4w6 zAOyNjh3ZzI(aK{1B^#lVNIkJ88cY-9DL`UON|bVZgC1S@CJqikQzYl3q6F&Unl%mz z1Z3FF_8gT=$PgGY;PIF8KC4&kAWaa;Q7OvhhSaWf+oXweZM4`O8BrjS@v!+>4p+jl z1dm2sot`(BDrcQyFVKt^(TE>)ce=S^DW36rEU0R(Az4LaO9)>T>UdcX3oJ8&y0-Q3 z)iP7)Hn4IT(!E##=v#xl@o2_lWIG+L8uc{lP=-M|1{IOk!f7m6)oO~-3Q}UkOl0CC zm{Z6QG}DqP)YaglhQ9;Eo}JB>m?%&ct1B{&Qnbc&@RpkBIAZOxXDwY%P}Oh(qtQ5> zK#8_UjDSw^TcCByt>a3dsfT--r&EzwRQA?OjOup@@unP=%|eT+Y6R;muBBV?Hi{5A zGg+52AP073be2xsumxHra=ly__B7gY)x{Cn1dt1FMKaz3s^=g}MZ$1PK`Sk7Y0J$gAfE)M z+<7JMi8^|U3HCh6WU1R@)M%pAs&`bgtZeJjyM~~&oQx(LKrWtW$FyQW1mTNuo$HAq zJ|a|dQ7qb}6t0zWL(RZi(Cqdq*`!9N45=C}2gAjY)=yqK1f|Tl146}eP4!3|8Hn&! zARldIq^zn0qj)dMR0@Wo#!6_R^67M>Yz2$G1_-!9I8|CpckyhF)TKxXNhh^bhzX7`eZdydqC4tq zRXH__3r!2EhmyXWJCP?l9(PFawozS-!j)JEfjzl3Q4VsHshFhFNd_A|tJxUAoZQ-; zP%&1M!fe|?Y5>73)M0S)(Cw5Up-ei6STRvZHxjBtRYBT_J>~HyUq5VpsspF#3hpIo znMXRD%OA-36Id|p?10puU_=Soa85E*tZZb~f?*_0s0AsmB-@=36H1Rhr`2KWQ>_M` zRfweDiuy5-0;Q1j_+iWIjphm(pJ$+8rUZK^M+xVgYcy$=D&V0LF{K<=(kXmYuzcdC zEuu{pNy8pVhz28?t`|z-wpA$=Ah8)tWU!#e-E=jActP;S!D5=AgPNF&g{f$<5aff{ zXm^Xu_qWWTP7B1!a}M^F0lP zooi$^67L{7Q;!l-2`|)Rqt9vbyshVi7E5@`D+#ccs5;<$UeZZS<6}-J8iz}G+Ap$E zI>|e$bZ)Jqimh5KtaK7-i%)@&-{@q*E-l^m&JiOZr50Qsh)tw)4A#3`I1whh*&K7|!~v`^25hm>q^fW=?M0m~;mX|Keaq9VbZ#Y!zlET}~hGL7LiH_NFpDw|;179ZijNOjt2Qg0Y>6fLNGw(lV6qph6Cr1_=8&AB0I0&_4Pgo9(31wR+8D6yTBS+1 z{GD7Qo>2kUpt$2{@MPpW2tAag68HTdyWR9wO zqw!`?kM*a+;|8$By+uLR;c_5oMcWcc-ltXE4UqW@?0kwi-6^252y#D%BWSHIM^i{f zV-B4#fK|qT4MQEWl&Y!e0PIZBwK(n7)GnNhrJ@}xNGHT{Ar4v5N`)+eal3y2D~|!| z6^W{r1sAG#%jKwqv7jTBhx3*X7gUOEv0ATCB5^bCEOY_e)d6f_3|N1y(aGm&ui(np z(M&qk$ale>loTATe8_-WNxv!7JO*7!Q~>P00jx9ztPQan=FrLjRvZJ?hJXumXn6oD zi~(yyl!Q67G=M#S3|JdVAIzb}0c?H@SR0}s%%Oz=?0I|N&8;U`1;_k;1f+ga$dD5@>VQO+=j=EsKZ7F4H^$`OL(0!PS7+fBNwy+}NqB!m7M zErkOO9b|(^QgoD1MChV~8uQ`V2pQ_A zNh?(%oKVpNI$>r2d(Ieaqj1~_l7zY9a)GY7Gc1I5V^lL4wP>R(3C*Od>aK=KOkk`4 zb7*>OSX*qT2C!$3!PYD(G~{)*13_;x6AV^rZN-r$5tS|F3Ploif{dQAT-ODbBe0B5 zjty(86D9_*{1|LykEe)HPBY4r^-QIQG|4zrjYgGt0P=@Xt;K`Gpm?O2fwMTXdC}Og zb~<76r32X9@n-vXW5f11KCH(5VN#5^g1Tu*4T;WY^&I%tWHqxQ$BKHb+@z44A9TWn z16Xbhwzgm|7#kMKcC0AQH4x4c;uWRgZmF%jjHBL?T=lxJpk7Rv9FIoPj>T;D2C&&N z*xG{a4q(|aU~T9nF`JzMEV#MbOA2V+h6WI`*&e_$W5C*wm0>np1K9K!ur{Ppn9b$@ zmL3DvhWrP!*%-i%q4Q+NJ#}g7=Mzt#NKec!%)N4MV*cRN%Ix~&b(7rW#QtmcM<=dZ z{p{3hCeqW3v)8P?WZ{H;pV${zxpBq0eBJUHOYdGfb@8o>CoWtu)0n<{y7JIxJ_nN1 zPzzJ*us?4phX-7E+#auY9rl5LK0F*@@Xu>Ne+y)4?9`c#0P^ib3{n~S+FZFZJqJSr zU~gXE2Cxnv9uYGx<{Ptf&xZ$??wwez!>%2wT8ou3LOX;}hFOrRmPIO~mmVtzAL64E$Z?D6_)a^Mr+;hi4+7-Q#6V0^AU zG(va!gE$8dJ2#Nmw-4IC^sm?H@%b3NR2#iF9)rW1CnhFu+NtWl{o7-^2Ogdmy}G~U zaA2tnfmIt$){`qKVk=MATIp7(-L5jRmK?V5YAcAZx3LyTe-{Z6x|>zN#b?zK&U#9W z+vGCMMg=7V4BXMSuohVYXY_kHB<(vRI|mOJ)g6cD&P(kWWAZHzx6Kk`rC2S{^+uf8 zEb73(^>eRDIm4}94$4Q3pb=^pA(O~CK`vf62f6CKeAOk^y|V1EvN=2su>~bBHYF$A z?a|#tSdVcn1KsVmmT$*k0KG`r@;Y=t(nE9qC7G+=0^b55bCMYQwSqo{&7G;P?HHAyfco!Ua z9Jc)Sb-Cv*IgEp+=j|f#qEYQN{Q)b_)3rQE{GCf?pd9aLg#Ch631{lOiLYmMCZ~jw z)p)(4i;7S0T1vj9OI{8eufU_)1u8nY*x%*)b z9W#jL(=^`U^IA6Ij}VIDQ6X0&5s78mWf_Q7Mk|78puNmG7v;E+3K!i*#bxrLU^^Kk z+;B0Mk(H)c48Z|1P)AUnZXupE3&~>DRyvbHJ3PTkDRlIPW%=a~ae$F*h^DK}ctt4f2~;c=V^|I6u#xbIWCiqX^{x0OKZUU zCxaS=TBKf*D~+}o8ci3zw0szctl4nI-E5c6by_sT3tA~2ajG7qKvRey32eg6@!2*N z&#h;@L5>N#(XJRF<3%Xw)RS@zOGO$cQ6W>DTXZ=1WVizLLe}2OOmnY0i~~Ycf^J@E zgV+PsO@m>WZZxYBE&FRownX?99o35xDmA5bSD1&hCYg--E6y5g)zTaO`?WG$S;(!FTGV%o_@JJ;=i zb9-yu6YX}0L^Y18q$ENWJ%E`#kB5_MWy`}*?VijRs(yd66>w$N@CX}?=cIJA(_>Q= zELs`$@te5wVGgkZ=?Qtmbu}KN@pMy5cu>8Xl+wjs%=Gkdu|x%Wv93F&$EB4f z9`<|Xctgnt3L2A%l4L3&V03#enJSclD~lCmaa?Ov+q2NgWHURGTwGP2!-12 zlnKG!Duz0p!7dD2Y}moNtuk;jWb^ns)HOLuk;7Od#S=Zq53ZgYl?+rg%4o6>rUc+* zP>YrfngtFG^_ZHbdwMoVMJqUnnd3$sgl4XJm_sClMU;T6hK9i1aFp`20tqHjQ5mj7 zs5xgVn8q7~lqaEJVm(%NRnqw=(g?O%NG0td6BQg)(PoavWiFYDH>>R!?l06Zg<5M{ zEslwCW|P%WUJ7B>Xg|!%KFk57npz_gbcC7$TI-nLc2;k=eDPMz3nQhno9cnEDN1J2 zkhYGeI?YU@-2#3Ql%q#0B)Dp4<8_ZSY`7d|!tHcMETJdySskoVST;_zcnxp2WXYgW zGBh$CroZ+uhcFZ^>KQ8tj{FUz*B0QmkmFnF5Zv-eAXEWFB_!)uuC2v9>pdczt9QX_ z>?E*=vB7HbW}+4dJEIhVC0Q4!DsdoY9!PbN#9B7jEl~&yVy`+D!N=Lq=q>YN>R}GS zCQLV5oleISO*arp*F?V3$@>t$6>1fneuq}=IkZfp2UF`7nM{L2z7h#5rHrQ{H9HZ= zfu;&|3l|d-;WO}%yQpviTT869Xgb+Rb|a!mM3X8|OdfT@oBsC09N<=@3OTzN3RSvh zSP~ks?O43Lr#N_qto* zN|!e}G((h%Yt3}9#>hPZlM+V7Y+LB)ci}AcFb7`%;dBJE+L4|&!Dk)GxDLfTsMCzJ zwHiSLtX@V(C5yq$bpwJF*pmz~MSq+?gW$kIhd^^p5YVNVP1slSGr?%KEM&WxH4G=4 ztzI>unkd~g*jx@79S;{j^li57EA*jnGf%+d^H9|!$BvX9W1n`#L*GTv&L7n~eP)jF z&3E~}r9VxaJpB(7`#!TTzw$efp#2o!?SIURJ!s>SckFr&4{E=7-@zqN)#>Lx>G&qD7;E$#eqh)Cp6_>naB#8D z2XwKG8{@H?zBOg`9bDLIw2f<$y)OpFYV_ep;D2c}cnZ^7glq^k94A7DA4~s*kZ^En z>nYfX{2rUnLr-CHtI;-;r1$;;ALl7}My1>OLr*_A0r>P+laR;N&Uqvk<$dFX-B=BM>$z}Cyvw|qyD1$K$iOmb1vFkf*HM#7ipq~DEd|WlCx7v01 zY9d^AVgHGOnvSd7_Et^9VFNY-r;o82xOm@X7xcM+g{_V97;ycya1cgddb|;|D;r47 z?{mA-l|15(_kt?VH9KW@Cb##Q<5;6>R3ESZEQ*))ww{92ksX8|1mzJ;*xA;@Ca<-v zay?#mhPkBKAq$kt??N-x7&iu=o!*LlG(Zo| zftm)}w()bB>|N7V%Xb){2WLTjg8_OxGed3Fch~?uIJ4D*$5m-+tDeILXy5;TdP<*I zD$d2hZ-4vsw;uTG^}wt6&2#$7DwiXZ$M+NSNKGSuUpZ?h=|b4MU0LO{bsJwseXS8Y zpA!Rn8y?X+OBdP&7?E7tSu<(RyqSIOd&?G%ez*5a9WGmVq-N6KtNsYtLKxWZA@DNp zkwL%eV-$k%xMA;6^~TBvT)9@T!8%Pfn@zg$R;a?Uitg>YquIJYF48?()TP{pA9WtH z5DZZc(LpKAz#B*aDJPL61ZUf1T5FQ30z78gJ%UHReDm)pp88l?8vt=T2d6u)B{q)9|DUfVqi3|c?-aIFM?tR~uhqm4upSGm zi8J5@Om3~tY1ay?fm@CV`OOHYN}6pO0X1v~B4&OhEr|wny%W?Y%SIM}21=OIy$F z>;HkTz1Y+zCT7@~GpFyL{_^xy)Ai{vh~fYF)F<|TegC`nxAsT(KXvuj;Qs$LtKzC> zb#C8n`>x%0;l9+q(^l?X`TWY2E9I3JtgJ5oX!*wFOP0CiGnejP`ts6MOZBDj(vuf| zzWDLQgNx@aIv1xFzOnG0h0a23;c4@C&EGu#y7|Jq4@4b&XYRVWm(Hc;PMf`V_Vcq> z&X#9iFuOW)`^@z-ubetKb>5V7YHIQulkb`AOvWakHgVU)&5QF#exOc&_Q=2g?Qj2E zdSGMv?8&3d!qLnsCpf0hJzD<8Z8^RsKe(Iq=#l*72X?a_Js+KX|8CZ!v?t%aoAu$Y zM*aJ?$#?B$J$hX@dDU*#qj!0eZ{DT1y(1^bC+~v1Jy+42j&AMQmSS@1%-yUZn8Zr0m+@X5PbkG`X)PT9?RbmmMwVK?j1nKO0L zF4iN7w?GfB?q+@Dvjlyzwwv{~9$end`p6yxOdhwJ^|l_I-Oc*Q9t7A=+|7Dh4^Hi3 zz3l>iYX5H5+j?+fH|uS)VP!Y#BWD9xk0030dfRMR*v)!df6wh=4ITL`!8n=T&3d%I zC;zmIwR^M&Cx5@2^=K<6|9dy=cc5P$wU%Odvdpf_^&3YT}yLYo5?eEE- z?PfjtEGO^T#oD>8m3QrCJv!$nf4s|kd(?MjmsttH+j#us=sZ9?!Vzx<@Fah+thYV)TXwVF_9Q>QoAtJ5^O;?&x8)X}{M2sN+j{WByIF7R!B6gH zy=`>gu$%R^@%o|Nthe>aM|QK`HeRn^nu$(CCq6eZ^|9&eCq6g-r3G={{l?^TR&HPZ>FU)B ze^{-qzG!u2-*=XuKJoi~*X#@LYb~)$>x;ixym{&4i<^t*ES|Y?;pEx#4@`b~{w?z= z`1YTf`}*8fbH>~Y=9Xq}n|;r0V>UQ@!t@<8-=Dc|rWfw^u!)IIe)#(yU$m}id1{?% z8G5n{ev9 z9<0mxL;y?1crVSVRY$4dR48rfieZU@PYyVn9NBQWs;#cSX9RglO(koBDg&L_YN^LT z3|}F6Q(D?LD3K28fxN%eq$)L6J)fr(U$I(D11T?`1EWe@Cmqj0EaWI5Qee71tOL*2 z8J%uujHDtlHD*+bI2?*{O_v{&Js9FFcD#&_Wc;Rc`s2eoLQP3as9LI{x#f^#)Tm6p zRSf0LU^$T}5h9V0+gy+dh0x;cXSeErki54QuJ&45rra?JterP{F|;87W%FVHijwIZ zi3_yeD$f06Sb{*)43N5Sa7o;$#&x2S?ncX=AeIH1$519lVM?@9aOHcjHvQgViFmJs zP?2aMZ01T;s+tn~wR%}2GEHBiWZ)7Jj3!eVjD<*Z^7~t?L=e6T^{RwiCuP1vM`BdR zgo=@PA?*p*X>TH=7ou*FYUO5b9h9INTEhb3vOt|xCL_seFep{jDur03pveL~PbcH^ zCoR6BmdrVAtB!Qt4W!~Z#)@D}(4=}<14Sc_6qJipTahTvnr?TYRNz^BrZ_Bt8ZD@h zq`H{j#9K`|on~St8-;~xq~nPsQ}skLq7`Bo(eX@v%Q-DiuG2`}>6IMKZd{Cb-Tn~S zL#eVH1nz4cPn@iBu||<{XF7qhh`w}KML-Xt3AeY@lqAL-_8WLfOE{7>tANFPkmaw| z6b}b?k#eRuIW=r}6Y?bZG9J#g@(673Rhnj{dWH-pYfwCv1zA|onp9VqERIb5a!?|j zZwgo~CB=#*Z<8!a98u!4mW1@oGSJ+JQ4mMhVjegO^TmC&A*=B-0IYm$;qw#jJ!a|1 ze-Bv|bT!pTiD(GJRFAI)QAWkD#EM=Y-*p%AX4DmkaV0O1@inI(zf}U4e5?x8=>@GU zC3z$hORBh~b<~hi%B4JE1Enn&+0}jM#LI>y#!q9hc;B!Dmjhw#ux?7OW-wIfaGgRt z4C+zHl8ELkf5hn&y^z-`o5dw^s{|)?OR|^{qLnmTFZ#){)wB$q=OCjhuo$g(OsYVr zJ{LN3+g6EiH7b~F$Dw%mS~ONGlrfVogR^!gUJu5A&ZXNBG;fa&Y74_>o@T?EE(W4O zuzWDNbWt#pLR9xKcw5%0dYx{Ux<;bKkjj0C&s*{5p0^*9- z>QR5TLxyVpkfImLT!prlt{j$NLTaJoDl;(Ti8TXZAKxqU5JuG1Vx90uMUYe@5(v7J zPPesC7?z;wz8)O(Rm+421X|h)Ya-XRCyqkF|w-hxN;78;Q8FuAxRf&N_?2orI+6u~Io2&Qmp$ zn!9UQB35k|Lf*E7$P!Qs55%+0PQnU;gjdl{tP;p5a7hW$5vzzI)881Dz=Vz`lV+1j zC53sVo}7)SFs5tOSfjyG3e)tft#Cb#$C~)e;;;nTX&_`h#}N#aioy-I+m%NUA@44m zKzowwW~^q{r}{Z4C@uSjCBmH=&olYF!7`T9W#v=BM8VW>A6_ z3`0x!%vhahr{e-t_7)OK5T!A;6>YT0v^%2&TB&wGYf&qo8nPNcpTrU~tb>=DMo0qc zcmpu>`YUcC3^Yg$F%y;8mtGLI44SF+2XBWA(n6W zy8$7@L^R-CQw}EyPIQGc84X`LdDvGzPqIo_hG`YUJ*WqA^l*+;J<6%+s+7obe9+34 zK?)xv;>pcDb65f@bPO(l@gmnD@v`b{kdb%}Rg*@P?_es?OtRsglZ+aLz{JH{B^WUY zGQfx)w^TtWkj5d%kzAwaG*s8o%&d|+{rJ#2`Ydg8Fe_}PD^J~k}jAj58^r4*e= zpr?ghE=SMwC4u)8MU=2OE%~c$G^@swc*dNAwn_xbq!=m|%br-V?RGc|kOYKGva(Aj zV9A{+MvZnJLO?PrWaivK3A)$NO(k7bI#G!x&}=@_O%;7v9)bn3ly<^K+QnCMsYEuM znHr8=8cBtKutAQdjI6hjuR}dXixqtJvLo3N6t#hC4BZ1->}dsAynD#Xop0t4x{^`D zP}-64IBQgmF?)VL@2KUA4BUuYyc0oLuaaB&#gG*to3u&08W}RKvSD{wrdk?b0Wuj@ z1wzTN428&;r^I`b*2>nl1Y|U$__Q}+&^()#BaRq`RXbE7=?fPv*`j;71jdKdSdq0B zFB{guhYLPBZ{?7O6$KH<9SvmAXf^|jm#&<6Y-2Z+%*uX{ zg-P$Z14VGDQizTDz-S3WRN7ITWzsfD|VbMnA78)W;zrX3?++7#gJhEp{oq#NoVLrq^`~0xK$$Db$QxYLjZZU zbYG@NCEH0@Gd*P=)&^P$8f%oR-e9{8wdYS7maxK|AYCX4-84u%g&INdjgU?y^eCjN zDxbi+#aI?^$yqv!%&!ki)QcL3x#wFMxaW$Xsa8DD(u1*7s~p5c)gNw@n$<*rYPW;d z^j2S0o0*d3s>ta`Bk4}1l5{Doq*5uul+wwp8Yp=}`AQWe0d*9opE0b%R3HUvvnJ~$ z&<@GQky_Z1i~>mjD_%)5fnYBx(gq(k7<1;u!xF_98$nC{h+2nJCKF1e^W~JYk&RVp zBUD#nFygjMnQPXa_~f&OB}5@p2O>ErS!f4&5~NWxn#~;30ZX^D-622%7$VRSN)&B* z=C|s|B_YP0^?9QP&RR)HWl9FyW06!Fyvs?cnr7-f7XelDf@fyyqlmKtUT=yrtay!w z*jh1^C|Bzxl+ZKnDyAt~&YN{`ii=bOlTRJ8N`v?rKP<$&nW~5HH1J{q)(s;j5C&$Z z)e;ou(V0Isz@sNePa2JR&ldKkQGs)2?C=Mx*Wk<95YF!DY=1 z_Y8ExP-9-O^V2NE$m)qyi1i~qXFRUuVJVr$YbKBw$-*hhRh<3s!2f^Rm{@5q&V%3n z_Umsw@V6fLtMveIgN*&f4bp}H>fRAcN4iM+`ku2pxk38AK6KYJFvH* zayqcL5xKmx8>Ee}v+>;^_ms5lbKlzyYxD@VU+U=YVRqdhU46J+ovI&!8|3I&bHA=* z47hLyVE0j-sv{e!;ETc0hMtivkny(3w7s85$>3AR>XsGTmBa)s>5hO1#TL&rzMfvEAUWphm zegIO-1lkTM9WUlAmnjGtFTwj((;q`_klV)Oad3m&hPmrFx90|V^knMCF#rCp?*@5E zWfSi&%$I@VTN|s#@mrXEkH*6Mh!`XP!t`ha`fk)~BueIUGEb0?ZblgCRbZy+5D$Ea$x5pLjAI00nT4k*XI z_#M!a0$zp?MM}Iq--wr-ya^1!tI)3wQ$4Jn> z_D1Lt&o?_esdK>y8}X^%9?73y+f@JcVp)rJb(`TL;~f5nOD^LUOL z_Nf*l8k4N?2JVyDm>{M?hA&bIBsP$=VMY%Gpd3ShTc#pmWR7_kqJ>1r)T+gZB*F1u z4v#deAT@A>_kmnj3L1$BlnFGdQau+JuOa1@PwrX=Vm*U1v+=Ua8|lhYI$GozF54;; zi4CinY&esl@pd8orh|#t|NcL5A~3O^UA=yFb)UHMk(H(8^Omk(np@--KCrMdug-mH z?#Z*t%qM0}oNi2gb?U6i|1)v>MBw7}O>O_gq8YEgKGV}5W;(TQBc<3D(_whY-W;d;?C!k3|MMrRgRQFFTZgE`fKXp5TbXydM z@;iG|>~s4SJ^1d7+GYZV@LZ9Pdb9>H? zQs54|(7gjr=lUG~gXsVK=JUp8`rJ*a&-BF)&+S<|N`qUyx&5`dH{aPl)33d@|MTBH zWAk0#6#IOSSlw+zx^MA)C@>G?C?(UKUIT1jrHWC{8id&zaS$D>?zi6C=lSKE$L9I$ zO<`mNOxw}(*y4G(AkN-Q2dn!RSNlxAgpbYi)XnqzOpjRIZDh!^IiCir`xkEQbG(Hg zo8!|r^P{V~4ei7&id(Duo)5gw?XwT|e}3+LV>3Ks^Spj9`X8Q0D|RyX4QKpWK>AZD zRVs4*to{ah?~jU=KFd#jrvLNPZ#^2<(F2c*&)7V78&?~7`yL6`o3sJrMH^ht8R5FT zZX>+n*v;1ha|MRAr4X$Tz<7y+h-Xr1q;otVTe&iY(T=@~MOY1gL>m4K4r9R6W zuj&8%aM}jf+%~Q@vI{;EuGjyx&-I3$4X)e>*Tr=k+4_!=>tMI{!7KY*uRqZS*X%Z~ zHgfenBCc=fbN$c_V{<)qlkIao>@BshZbRAj$hXvHAOVbA12*JsQW+TL-Xv z&Wv!Jx06lKCda|f^xCKOIbQq9qjB7}_CI@*>2o|{XKLeIdHBqaP^jMfEj8GgzK82G zeb2YYW!nG#f9}MM6Z`*Re{%JAt8ZNm?fd1v*Mcm6KUleB#j*U2HL5kn*HV5eavFrwJ4*J3pgX8>VEo~MSuYwvm-&j0SB;PW@$_qON0cJ8*P=^y&% z_$$u*<+HEL|F(9%d?U}j5fB?WAJuU2>1@H@t|m%ht%o&QxR5K; zq0A8Y)a&sN&Y%B<&+{Ms{GHOf9shB~8{40Fj{DR*zI3*^_MT^kpT%z02EYyz7BWpw zsT^o|Ix<4K#AHp(W@W}rm@XezGUI&S@RQD9TN(oY@eohnb=sjX{^4`Cul~mkWnF$d zDgOLh|My2%z2~BL+;HRkJiA#P0M{!4!%=9(n!#Q?%xA)!&p1h4Q!Gf8jHmCx6}buhz$PC%b75fI}sT zSDY3SjwLxU+eY+|H|78;$#g5?m$kgF)^v5n0!8KXTa1@ZKk1_Q#LDZldS&9uv+zHi zbuo6&KlR8zUVCR<|HcUq{+``52Ee`)={KAyKZ6P>Z=ER+8MR{;k($ec2H;9jsKrF1 z(Fh2ETye*j0z@W2CizNdD>A@e2LJuhT8)d4Ub6`&xOD|k}gEa8$m{*V(x8i`ui z8A;;>qgN|AQpFONMH^edr?x&i+dAd;2iOBy*RLJ3Q~!SEe&ls)-~R2j2M)dbS>Js8 z_t?$i0JsyeB1YbUnyS%7646voKx1+jqq{V#h5W@XS|KzR?irb~Ib{4w>+t^e3Sd}^c~s%{2Bc`c2gMu*HS42WYdb&xTFR$$d!0E0g`;C ztsL4-gXn-@f=&=@ia{Ixt%rBbZ*LZUJojF!@R{FTT@?TOzrxp@PrUA{UqD<>SZ411 z>_=~6H{}6v*B7m1(%pim(<(+8Gn1pqHVFwX)5?kEiU={ec!-12Dz0oz=`X(XC)9J_ z_T!UpKjnE(6rS**=eIt5_2*CCWPhJ{`OEW-cdp&dZWac>a;}GkFrS-BaREy~ePYh1 zYksFtGm04^-N0bI%Y_qRvYX8fyP|vWa|`Ev*MIV<*LI%h*g)^A zylLg;FW;iDo6-Qd*l1E!xSIAR(-FlZ@pY5dvLx^m#bBS8sJ8PeWCgNL#OQ4ex0iqS zHUID(`i^g0a?z_!`oJp-+|NIJLcVv$$Ey#V^6Pu_iFbI|O>qFsCp>Ad#G9fb!JNfP zEk`V>VA>WR;kHIa;meQz$>&x5_b)u}bLV{eN62fct5N#QZ)bk^ ztl5L)@<}KC9lI$EfT5Db#96gj>10bR$w$DlAHZZURwqKvX3ZfvLjh2U#~Z>D!>;)L z#uJ{y8E<;h)yz+^^|##rq`T1TK6K;9Kls+K9$1Z@2QQ@A&GQGqHjdbb!2j}I1AE{e z@#|Mi{G@j84O2_E{_2(g{Na=GpP9V;r?2@3_VNI`nI8b#I1?WNzjObuu5mt&ali1- zZ+hwLZ+PGhmxQi=!6%<6Kjo+Jxlb0YKLpv$^9H~+jqZ`^9HH_}*pV zmOH+H{zwkqbj6>Z>Nw|z_kQxmPq3Tk4uEZBLKy--_<@brKePGo`467)q~AX6|C@f@ zn}bh%aXa@+F8KDynYSyag9&oZ02nR@f>yLGg+Zdoio20;IfIm=h|`?{DvP}!$f1YU z>T)!NWV9jheJ`H;(1lN1ow(yo8u6`{r2aVdxX*p<>83NaBAogDAAV&LXE)Ct0NW7M z90KpZFnQ@8-x>c(lNpL*SoW z|BKfu*PnRV@BWzj-29Ip{QaMvw{}g6{axYK+cr--bZ`4P>}GBNY(r3P2z=N3Y7gGq z)$+f3T=>vO?|<{3p|||;!GC$j1-IUH$+bUf$nG4w$qj&Qq*)mP|APPSbuY|K^aA$> z7nIfOp8h)HvM)X14@jI0U;1$jx&&o6vjboor<6nB%+F5a{_DSplfVD+8@exkrQ$t1 z{+&O5@Cx*SYcnr;_WA#=KbPHP2f#MO0*AnhAA8TOcfa93uKdjJS04QKJ$m7Om(L??*&lQ{_0^);r3VDULZl64iwdvu+A`e6W7*f4%EjCx7dVPyW-Jj0@MU_{7Uk{?9*p*-d%?Y~y%r$oQ7q8gIRCxpZsy z1@m{`cISJ)B)-CP`vvcP%Jgr(ef6a;JLz#t>?Sn;wsDp<1g5|H?5Etf5qTFvb1%B| zB~SfA@Q0^g{p1sG|HoH;cmJiI^uOZo+0E3jkKwpet~6b7xj@(485TmjF{+u2TC`D? z1dzm_>aK=KOkk|7z1uUseq-)%J8bMs9<`sllBKiIhSPb>I=f%_Waqv>ZYmJOsP{Z*?-&q5ADBn|M@@<;Dptmuim_RB~So} zuAaK@5Bt6f;{RW^kK5#PS)-spYGdJIi-3e_`oEOP4O4 zzZ6(H0jLDtym;kebuqel>cSruzPfPD!pj!81?R%T{O$8M&A$ey1-xYb#JRiYzA$&y z%&F6VnEvYYHPbJf=BAxGOK0c=1$A)%NA|mS&T_+5pS|?*MdgStKYGzEk=`ro5#^iU?n0QNL+AWT0 zw=kyN{FrugW7^FQ>~N-_r>SZ=VF)QRii>S)Qp!1uHpt_OSD4cCLc?S|_C9$`VA zdMVxP^w?Adi&n}b?U;PRu)TZQJ$_8P$Bk)s!kBjZ$Fy4=({A6Gb}IwBXwxF=L?g>H zgs4+|M<30oxbT%R?Y=su-Pgvn`}&x6-x$;G-v@SkkC){!?fSCdXN>d87?a;*iE@@!nQ$y&bw}s9G^U+6rkyaR-T7nM z<;S!;Z%n&$$Fw_VOuMs(c504Bl}stdRLfbNY^Jc0exBRf$L?u2+;2qpv>Wa>_O=`D zH}Aim?NwsH>H$#e1=U)!k5Pd?V5YnPZhe zNj&!GCo7xva!pGTd&PD}Q!}b|pnYJ2sqqpQ`U;)2sErX*BU(`r1gp9^I0PjbQS^}68a33;-BnEln> zl@5k71s9JvEu!lYN+|^C=aiF05kb0U(50HS4h4nl6(Zn8yez@A7$6Ae4dxC6azLD_ z$GO8wFhY|qf377)QD-mFNr%S@IE4C5A5J;qp($r$seZ&$&VRHi=ZF6`;LgAjL3aN|j!U8cEHO)rU8lg(Cw*UK*49b%3>;aWnVxKxB` z>ArARU}dkYv@OlgMJ3a$(?+oqg~Fsa-3qc)gL9Q7Bp$~3XhuPEKGx)0&3aK6du#{+ zzDSv5%w4F7&PHYYDdz=K4kTuc_k->GiCJwd)V60_Us`6TeWX9|XiPA>vGRj`6;P=& z8vXV3?9=S{lppa1op1R838bXgCG3!pmkMU*dhJ7#u+P$NU z21ex+xq`A4vttxOIaMi?>J6h>U}ZPzbd^2bfTS0byvH0_jEoCm_6T^rA=`v7{#EmO@mknpVnno(d}p64Qbi|3;yakY$|V%(5ObQY=!+ z_=`rt86GQ@;$ND+Lt<7Nt_MJ{V|w`gU!7PvXW^#VCr(x$rH|kLHPycA)J=M@{WHGj?9bMO(*(ZQQ{ z$2q!VYWv5$T#=1a%)uPBGpEn(uWwr^gCCP~)OW-jbsgV13PF8|vK{BBy3skH5ZRv9 zZ6b-(Mz+GbQOJq{S%Mg^SFwq0B&Z9gCl!NGvG=U$G!{`AB zk5sZ@Dm304?KgcmN6ClgsExJ#k>LIHCsJ5j03!&C&*;=ZfhFl`yfA6(e#$lDI}|ezC}v6A+CkSTGn; zBiWdbV+nVf&sKaGiWSCFA%096bPoMbg#QvAA-oM%dzWc3}dsphfKi^pT^->vl z<|_+#EQr7l|Gc^XoZ~^BK6d6CGc@qPPfmSlDh6T#Fi`kW{&msi$gC_=3+pz5)z@vr zpRb9@G?!+C0+~?pdaj!2MMwNbF1q|BLk2cb*KGuvLk2cb*KGvTkHg^kLk2cb*KI`IkHg@3Lk2cb*KLI7 zkHY{SGO!VwziuPkesAT#?feqAk8v*>|Fz^i-*g##k5$Qh; z1MiT54b*iTk^ti{@C+H)KwYyi&AjOHvxW?8s36!;oE?XOW5~dUT!I}v z*>M;=cgVnou7Vw%*l`$a3>nxEU|6@IsW1+MGlvXppxV)N9f!elh74>-I;`7JX&8sW z8AAp(P}gn9IgG>L^dSQqS`Ky;LH9nxx#;p|+h=fGPkJx9{8>W=Htu2V+#`qB?ku@Rnks>|u@pK%V#<_oeSG%F22qJPx+kU!c5r)@RT5Of**r?>yVJGFZN z_wfI1my8F6L4^-nNP=M+CEz=p7b+#8PyTT1VJ^ABKlV8e`HYaX8bU)p?B53EMZx$B zlva|ozx){_coC$2>jp+!>Fq#U4?pDXJ9TaMXBgHpOxAQ-zu%A1Z4B~5-lO_p#~PZm zrTD~*npWI{^1yGW`OZO%rL+K z<1x!H!~Y^OvNAIyl_IKDbaJNUbJXS3d-s0tF7Lf~ao@cSdo2!LVoFZWuoUSt?aIx^ zUO@onrUR%@OL>kS91rs$3gAD8TKB!io-;%R?O5YFb^Um0g;=!{gA;+ zo{)q0l^{IlEe@zqlq~d7D#GC@T1wPYEIE#QWeK7gzEaE-5eF!vjQB@RM+mjs>0IKV z$lGh#W~Vdg=ThNfE<-eo1#F_q(1GW@s(Ou4Jnyx65Q>esIh!O_TR~7;+uoNO*?c;N+&c^&EA!nf4*EY$Peg zotY}{4AhvzYA1z?vGOQRx`;&N(2?>nS;*lQa)sbwwOwc0aFDK~lf_)`u*df7rS{NZ z9Gx%CELupf zYnVlb{Vd{#xHQoV1=N3cxc~vmE;q55*ER6eTmzD!TDbt*;{&3^rP)ykPfGdN(G=PB6EA# zaemzP1~UT?NGesrxW|JQv3zJqkDFNsL6v#dCj_Fl#-Q9>e)+`{88u#z$h;Jj2$W$y zl^8JTVLQuK#FU?&A~H`-?hzd>CZxPvvO|`Uw2Y<&TReffEc9I}zy1D$&g%cS{Eov?cQXC7x6zBKVqF$LSj0U*CP7|4{o~ydSwkXH+ z<5;pjtQYF#Uc!^}WSOpmXQr)~9w#anSqwtava>kgIxY#4;!K7QHDyW))5CEUca&1z zK!gYaD|_(biHuy>^bg1AnhVa=TFE5Bqsfe0W@}TeOhyK1aTxc=UbyQJy+|uqgeqhf z;{sS(YGq|HU+DYoe9Q(;5tmoEd+o&&8M&zG-^bTusa%Ck z<`PV;Vhc-IM*_)CnakYnPQRYQl4HEU=h%!3JSA~?IyR2C=`v}fN&`M>b7Nk+yT*gL z>{S;~WQ&^1yfmDJS`3dQWGawra$cB=Cc9Ha=9Lp8aWG6mxd;chDrs^!tdTa6bPpr; zC@{`M(%o(LP=BJ~1tNRp1r*t$<}zOy3O8t!lyI4rvi8KdLTBsKtjy!7@u?Prh8t2N zQ!kDPqE7i-xZtZ{4#L|@jrw_ye`w99EPDjtuOl3Zin+4y*C$ zX}fzvMwe)PIgBE zob{p((bX&@1+Yu`%F@y%f8oRik&9Y(1AeZUq7Z%@Ceut2lwzpZlhCvWRd>jT!o0hM z)GNVIS8T~d6p`gjqTU<~5h&mjBWzgaeEuX~2NUA2EG=!7FPhlMA-S-1#93oRo=QlS z8to0|k^vd#plMHDu94h=7)JyO$W(OH>Gb;%rb1@wUQYwAve_Jw5R(|^?S-&s+#EC- z1vr9*$gZs=*Fl}v9uW{^@WA?MAhxGnII*FzMLDUT&oIFXh;wN*(qeWL7X9v&IPD3{ zJMw$uLuafx>ISmzPOj{42mFkL1u9)R6|cGzwoX2t%49srJg~NJ11J63g%caRFtG*k zY7R}Lm_QbZA#g6n=J146o?>k-L1yS$kD!oZ7^`Pe!-ktBGX(+VGP!}L>91v?p+*qISaDjv~b% zo(y@iz8biG>-D9|;y7K&xsenSg=q+gEe=-F*IhWVVGEnv{Di*<4SJ;L9FlMm%cfyZ z!Z+30&~z`rz@@%aPP<~2E=je@(wM11l$Rt%;YL4(4l#SU%OVJP8Uv}sx8Hiv#75(b z+qdBz77a@YrqT(E&USJvHVT1h*5<*Wd=%-oNbnw^(adDMIj%m)fc&XK3FbYeWXzxG zbdY9!SZxAp`wQ?SwsYacHeA>_rk`wwYpjcB$N>X|vr&IIn5a*C^9NGpAvGH-j1y3n zOrV*en{f@xd`d10kt}ETcwMeo$Pe<@(gau%J^+@4r(86#k>H*4f?hIsvv$r0<~C2x z8&8sLGF|C~r;5#Ei(q`Ig^Dm-YZklXhAWebphKaNf?{HVRUK#>U>QLVQ zla>!Gtp-+qc;!!5KE3kD%1^GmU}b-0+43FB2QK~UrH@{!U!pFdpvK?d?R|FdU3)Ly zbM5`a-pcN;?|y8zu}kmTcb~HJ{hcrD40pJlJ9pN$|6=>A+aKMoZ{NN3S6hFu_4cjt zR(|WboBwRw}pT#;nmsxd2z$)g@j^6_GG-g^;JK*W;p~+dRh}DI5*$vi9*-hKfFO9Eh()i+eY5aSQ>Mt0g zs!u*^{i@de3x>MuFMn8nQ|tbDL)|C1|AyB6bB4N4aQ}5p=%3ZPPZ9dBX&gRdh{FWH zf2VQy^lT2lsyWA}v?gbq<5x82_@wsJ+3PvhuV~0VVF;N%F{bs)nmK%2>#jM+FKbjk zW`rv4)En8CwC+D|s5_zW{!3c-pEJ~bg8MIO-9Kum`vmtdYRdYE)_saO{DQ{e!-hCa z0Q`c+;X|`Id|oq$4{A-$n8W8ZbNGPv(>ZhatcL9UhLBB6@t@Jm;eA?n%^W_hQT&9EP@xDsy zK4rXb(Z>5qt;w13zF8aZE3}`^jrUC&vX|c+vNvkuy`pv3#`^}1>Q5P>IyS2fZv)qXlBjMr(f@4q?NKdlL4taaCf@mh`Q$Pm?u zh4Ufh`Tq{fvz9Ks@zR}ppV{;7{>R<5^0*=uS%0yTF(xjMeH0q%s-a-l8%@GcAuKYZYr@;c1{>aWyzBbn`igQ z33juHonwa-P8b| z^Xxu0!EP3@v+OV;OtI;Nd3HZPh23;wr|iy6g64Gha}(@l5j$mv!I-0(?AG+2X~0xI zI>Bxhu~T*^jM@2srzn^g=0`*;O`cmXU>L&(?omV55j$lE!LjH_CeQtj5SIhVVSh*o z{g$UXko=zDsC%vj1Z{j+w1V3kXBVgGq^`5V2pB`d!???>BV~ZuhxCG+N$ixJ6ULBk zt2fXiH^ArB<8px%p#zm!92BVyEn! zFbdbXaZb_+%E0IzonSYM*eN>)j6!3v-k#X-%^%$(6YORYJ7s5w5u#ghGZPEXe0C2{ zu$x8flpPKu&PdqPkD3?eJ15x9B6i9SgAs?|b$9e`VnBEAm|!=H*g1CgtQ4=e%#ZHv z6YLNe!Nma|)9hy=j&Tl)COlK))s2c-u5~Q;D6jgJA?s zMn{2(-Sd2QZ=GN_i`XeU2u6@XiV-FjpZV4(XZ}YDBQ1P#etrN9oEXn8?k^aqXr%mW&jE{=iB9hvMP5vxSn z!`NsTf=C=ghoG7iQF2qodaNM2h9S?;wIDG1(?9^`r5YF$r5hFmMlTuZ>iq!A3xHjk zDB@rk%j8ie82nfpyos6(h8Z5GG@B8oF=3<9)sZ%M6V)3GW0@ShaCHdy9RgvSsMTPY z-|3-n`RV}hK>;5V6&VckIeWqk)Y;e6X`=LkVOBR*r~YJ_yV_H*fMGARw8Oc>Z!ln4 z8?cE=5A$=S8=7PFuR{Ttt_H?L(T4?r(ZBr#U^*HY6ZIb!1ZIk4+8P)Wr63jrMlTuZ zY71ad=2#O&Aq-=goMwO~n%dw^)POL|@WxK5*MxMnp$*HEl>k3wjx|yH z!7#tGbF2vXD08fd3J-?)=;qkErcM*391OF%u{!l}4&2q6f&~maZ^t_I5d}lAtsik` z#=&scTW^V;K#f4tNh(XX@L;6pmcov*VJz48u~lurCMrkF&sS+^j@5@?0B5RbU`!N| zxOHI48WeaL3N1RJL3C#X%Jcuq+Uu8g9@_X9@Si6>Pe|Yi2|OWzCnWIrNZ{4twXB+% zUszu3O0LV8JgX20F||Oli4{@JKWrkix!T^uGB~|(h>2oWQxYDZyAh-SdK?OeNTd95 zX$$ylx??*4a|E255a+T zv{`rcZa)37mrlk*qcq6Q;Ul}%A#s54uKL))N!>tB%tTqK441OQ9vsWsyQ8SR5FN2C zrdCX_MAqZ%w?lZ+A5P&z7AwT6rNdY*+H5qv#!8ecLkH=PU$b^Cqxjp^r!FtH&YDqw zgQpcFzwrKMVk>^m-)8ReA1AMyeB4l~3v??SC{v>*^**T;fZt+Agb;E(eoL??7t$qtRI15slw`t$uE=IW9+K-1eR zS>1O$8x)jv{AmD z%P@5`>umCHFBK~@N6%Nue))1MmGrwBy+H~jTPajS4evyNOcb+`PTn0Y#RZALd0(OH zD#aXvyKnCiur%^TxTf5~LUt_DWsaVwlKo`^WZQu%3hFKN9Dach^pMyH%}JwlpH4NI zV#rrwIV?}OoC9AqU1yG-tCIba0kYk6AsX!mLN+qi#2d~sR&z4Vo_(B3IThf z9Nnvu{Y3+03#lRAFVwiCQv~TV(zYnniTYapem(*+vd9n#3gy_^tZYLAKIVu}$$rrQ z*+?>p4}54o7B0A$ToZ|S;byhm?6k+}fe>&ak%~P$t~N$84rJ?8$^OFpV3Pe*U!Z-k zoC`J_LovZZy?%k@X|X#hFt%#2%?v~R3OTfe2Q`d2a;Rj#V1O(rEfNZj1xK8&4{_9A zX;+f2tS1#rmJ+s9%AO4oqeL?*59DHsIkKx{KW~6+GuTZA;c;S+j}ql#&{e~#SZ*X| zao*vyrKx16H^vj`VlWL#mK@dGFUi3Rl{s?l(Au<0z>A|5X-tV z`B7IQ;S|*Y28O9*KWl(&wFI<6mx|$DmqRLKu9ry>(72xG29V@rSQ|mnK4+WDMqNM% zs7m%T2FTW+crP4pm1}KZjD=m{P%k6_`z^+uKE#oUj0n*u#Jc@jf&eN*RI;BoK-Lk8 zXP^N)47%ZLs$F$PUG*-PjwDL$v>?HKH^KE%7@3sWCe0kdD%npNAPY5OV~kF_yAj4u zqXooPP8U4EL^j_y&(e%t_A69Hh%kxeE0vH1}b`zAuZ zn4`N@vOjNttcl<)=IB5r`*Q}!nh3*Uj-IWO{ip%5CIYIMqq|hHA2C4IL~Imu^emO^ zhYgT55dp;Mh1T!&5KcSNSpaHTbVwRYrA6LnKzyMhjAxg~AzDoA}2FRKS zM`DhCOeOn117uA^7BNS6s$_q5{_F336R||h(PfqFdkv5^5f#K7-Jz0wj{&kK;(M5* z+f}mfHbB-yqz-fR43+H97$9pR=!Q9Zx=Qw42FRKStznLCQ^`JRfUJoq8RqC|D%nR2 zkTnqu!yNsnO7>v`WKG1rFh@V4l6|KEvL@nOn4=$7$-ct?SrZ{E%+XU-vTvXN4t(E4 zkP373Ln_&~86ayS+=My$L6z)V4UjbvIKmwLfJ*i)2FRL-0a2d+Z(rh;F1>8;|Li@m z`_H@g@BGuwX!{?w2V39Y>TP~+v%B%#jrRKAtT(NHZI#yka;?7l7pv8k|F%-L{8x*x z{GH|E(w~9uvp)|$8dwzsdf&u^ve;;`2OssD24KQC=bQS2kNQjlFyYHa0C-gZCMMr~ z6JBowfX6g|1*^bq8o+{8;8FpYSg0251urxWV8JSQfoT8>R>AXC045goMSH>XOaoZ3 z3Z82kz=Boq92J0x4bY-haIa|q3swPP8o+{8;8X#aAi8K3I7|arunO!d023?RV&khH zd=ys!nAocBo7jKO-yc5sC}tXfiI5&608rBaOuXqd0sv6~m=Nn?-%3CDC~O+Qf>i*S z2C!fi9I60JJOV8?T>8OB?=cO)MBIxJBeamIAP_SV%eul)JSb1iSQ z+_wCoWqj$^K;s*ITF>9Gyx=|wr(X)TI>)`K95D=<$?+^XrxSwCb5wes=cxPzZaw!p$DL^$Q3ptUIAe?x z2{_MjRC}J|sQv}+K4-(?yAOqzmUg$UbF?96FZf{yhRYNWmicV1KFCtZfgmP=I;i`d z2$CywT=qgB66qGo-7ZxuyE3dOwH$E8!MS?nXtvr*_xug|3S+-MtQi`>as3tGS;IPO zit4b!be}GRlx*bz?XAT7@qAlw=eF%Pr0;QNwO57mAEr zt->(V)ekG?CW|f6g(GGTn!LJW1I18>Z zAHeambA&o9Tb1)fgZ4zdRFp!cmJ>~dBju9QQE2CT6;N3`IdHfgXuFAltQ#@eovH+c zRI=(N(}_qZ#!L*WS?$gnE10x?R`&zF1fQ+CrFtMWDq!9s6m7ClBqT*5z6w$=*LXVb zsz{*ZyO6r<6~vO;73h09Or}f?C5qJ#iyOoXAtx4M108-m6u40=fP$)N!#aVCG8BWm zy`4ZV(j0>-L01^$`mmld1FplEwm`9FJrLIK_Bi3vXh>rvI|~BGlCT(ejmOzKfpt3L z-Y^~{lVu`&m?>WFwL?BKGv)l{TL2?60@=mJmqy~u36{Tp5C~JOB9J-x@D=&nVVbSywz4@y%hBZ-sWLCQgZJi`OCY%U_)OzlAM+fz?zYtDV z>1IyO)j9uQJfY?vfzVRWU(?4lUU?yOeF&`?&@h9VncR+?ut#U*xwnG${?ey!`K38ysQl+XppXJluhM#uz3f>yj zfNa5tC)-ciD-9N>s-joM%JU;wuRP0k9qM;yK`^KR`-C5uh+~}}VtIwR`@~q*zc>r3 zej`7t-Gs52h%qz}P-Y^&@(OcseOMo#0T#Lb`w5e;x+R8_mcZ2Vsfr9cU>RWj8z`R^~N=8SbD$OH*pF!|6s|?X|eMBf7{aA zmM(es{uspm|H*D|*Rs>u{=IGY)~h$azPY#Yp^c}me{}r?*59^1ckR)&oz?itTP)wS zw3q*MSzg`*9dF5}b+oi@0ZAr=rF8@Gyfbf=?VAXltz3N>V0oypG!W~nvAlkLrMqt; zoVIrLLBI-CSQ&`_)mWX)AhB;E$o9(4)dv8#XWj?683+s3xZN;`#6&_xGdUgkt1ngH zD1$c;POQN>&P8J;UE>w!)%%Z!Ev$cH{sd=a*b^!1%!K*wn+UQ6*vBW>KfWOB6YGPC z8GYZxS^}_-POuw@l)gE+n~>$ciMU?iD#K&!OY7rXC3iEjyzS}$-~rR1us$-3XYx*~ znRJr-CIXg$y!t2d8XCrT!>MJ`$4uz2b+vaauBG*XVK`I7WkLj%t1@7zt{Q#AEYGYO zCgQkjSG$0fx@zknF=vijkb=Pm!< z+^qh_pZ#B6et91ppIm;WQUvg2Gmb?jPDTU15X;1Uxpa*5x`tU~*v}$0$w>%(SDi1{E5lrC4K&l*1x+__P5{oFQ@EVb6l^39m*K$dK!B-r{f)%V-CnO4O zAxxdf6eXwHHrD`c5xl|&*-)NP9uLL~NyyCykzR11h`M`S)GwVSYW-pn_hp%omyR*+izI5wyC6~f*+faq zj*F>Wh|k~{TbFAYl%6K)7K)ZD<$S_h>gHlG7tNytjymK@vt5ci!icJjTK~Q!6QTTUF6>G8E|RFpMTy#jw`eb0Pa?%pNuts$6p{;#&@@qF(0Clm z$&^=!#Xv%cVqc(e7%v4Aqin*NtocAti_LH2@>p9Hb^E%g$FoGO4-LwAf&pKYjwQku zO4N&57X3LCjB6w$u}%RFbux9zCNQokqITt~nLdtqiBKs#ww1E>BvwuOl3+QPFdxd| z@d!=y{N-jVE7HmY(z-6{(JWExU*LGN4Czltlc@`}ERu^_7X1m1WXT$zBs(!E;Y%|u ze?jU_5w#mj1XHkplW)g3+{XScrSl16ZA_o7Y7>oFQsm zv>60ZSy$E_k7wN%Nz`y`QC96EkZ8(p&jl(nJ8IEciC|*v)CmNnc_Kp{(lf48g&stV z0^v-S_1FS^M~$los~9Hd28F~BY zG>m&G7L8RawM<{gXCOSrp=>u+5s)z3Z!%ES9_3LABl_xGEnXLOZ{dm15pH5OG;5!meI1Gi|fyL9u8%V#2VK_PGVJ4YN3x z56h*LGuRCW8u1uTH@Cnu{FY^XY4!K_9^S>azp+)>Tw8y)m9VU@NK5ZE{(yfATP~Mw zA@L_js}guQZ@ur8#oH}2e9fuf7hRyq3|rW42t2tKrGX;@`>BHHhj|gJ@#$$E=HkN0 zu*C{q%2VeCgCvi0Qd4NkHa-ERXiqNPiBzCs+?{R{^y?`sImQcoj?K9Iz6zJ8W8-+6E|WH@G~lB)H|DjwYy8<6 zV>D07`cs$-CFMnJGQBjMg<1@cBxEX(YjR$gizcT%rF!MWNE{54P%gs3txB344r`=M zB;CV^J(`w6iKM&R?4kZd!+TcB!en0AbmuNZHP5W zZ5x(~h}bAdeu&S6#ZU<1W0i2n4|fxdXe$MYwPB}F3Gto&u)y_t)nJ|(r0f||m2M-OAfj=e zX_sttl>tHjP`vDboB5^~i^c3HU2tQ?)ZomdJeVhC{nP1%lJcUaivTOq-s~tQ5*+QL z>PfGdN(G=PQg(aTaemzP1~Y?XA}v+IxW|JQv3zJqkDFNsL6v#dCj_Fl#^CI9(Vr*f zf3^AICTw|Ohb&%-Nd(F;pGpjv^st>}D`Lt&ZL{pj$vvXO#e|fX1NGd?NLohIf-Rmv z?Za`zjhAa3M~ao$Ub=i%%Ds6~)<6DVBwJqCLw|rqIH)B>Gi8KFoSkT<6#=$6#g<*! zAh^ghAZf%OWCeMQ_6D#M$+Mvt&8Fj=gw*VvY`@CJJJMMx%gX!z)#Xnw?LKAmoomkq z|9Ika!6g7HrM%>%Qi_Q{p!xAH=PRW+5KwxfD|ZA=$~QZ_(5W`cO5GMGK}@TZVj@aN zDK%myqmtU*M7E|ol|m(E?Yo%AQ*^t2qK)3tvydM7bxKv8P9Jt*H5KXO=;__xHJ@Jt8 z^Dxq>vGqR2!EMw*(8E-^-fx9sI7nFE?Hy*Q)J<1Psft-%N=vm!MWoYcI~6DG2!9ac z>urL}5d$Wb#wyWHH4&4MPK$PB+qt@@8%<{MmZ#w13g{rwl2Qd|R0+G$gue#mMjqB! zr4)7Opi;_1^=o;>?_OVAHe*bS^E>A>zcUf0wirJ$>xeo(IvZN306JT0?_bH2dR?KT zl%TrbxG#cv4^+zmaMiwfUMO)rQSnQqeBSk0=Kedi(7pyI3E)Ru`BnSVxpAxHPJf#W zCIy|3D-&tGi(I4Gs1`Xf&xut|0e_Y@*XLwTnOtxc<1ro`$ga%E!kNFR{R>>PE{4j;x132q~ORVhJZ(%9ZT!08}FBrNArRT)5aulv1h6DB;UA9I*&{;4K>|h^7u* zDTvm;_F{FJAA!n*=~yYBV#N~Lh=xnHmIn!jakApaB-YalGmbzNE@Gu5Ax8&`n{FJB zb-J0l44av+rW7nYjun2KCXQp~-Ac6uyg^-%6(D@I+Y-5v=JR(do@zrNm;erpLx z9RHRF^I%`-oUg}+*->a_O;CK`cXkWTgAwe-9sX!>kS1!B6!LZwwRqCwC)oo@L_=+< z!X8&~Qwslaa<3yCPq#WC$_y;<2#&+Vy<9%VyP68xj4=yGg~M`*mQ{&58cjD1`pP2QFq*3iMfhy zLp4Fwp##r*RrT6^is!vHzieikx;W3XPkP=IUz*|_$D!+wlXs{qIj|>?gGx0!)#6n| zO>-OHvrYlecZySkHgD{>9iF+&*oQ5gF2|z2+F_drOMz$#DZ&kw6QEWl8V9GI-5G)L+P;V$Og&y7)!>S zP&?SAgNKC-62LkK-FPH%aNtS}oVmj=E2Khcx3OhL8M;>g@AjqFEM00}THAYM@2=g4 zcb~QM&Yi98`?vlQWB~YkQ1kEd#wRxJSpWF??bbI~m)ANV2fzcXPhR=Z3U2wJ1zY~W z@(+RTkM(n9`^axqZ>d`iR53GhgTk(VWxa1EHOKamPlIEi`k5g(`uAb`CQ7CO>3B6b z2CAkRf-@l<6KPPIM;?F%TrjgfrsNDJs;~h8xHSP7PtI_|dqwsMeVC};);w})0x+Jx zVRDmiCObv>=!JkIxG`omp2Ok#iGiu)6xE{_08Ze-nALcChO-YhrgBqkA3a}FtAQGD zhU{AZLexxZisI4p6f|Jy21>_iXtdj6ChEumTYj!K=v$T`!9@Kz0OvUx90MijjObt@ zvUlG^t_c9=UJZ_c(shR5=wEu7O2x5#L}+jfl)N(pXCf1aiHs1qGw0|6Z$Zb;-PtD*92go_}***n8@%@KEeS-p?HKSXu!}7)cw=YXfv;w z><-&Uur_D|bpQi} z>D;5iF;K725S$6=n8>QoJhA~a%FJt^%%O&6%FJsbrvebb-I@Rllt45jfC+t=NR!Y! zI?x1QpmO4D0hq{=P(FG#;Hb>J28t?b9M8so)*B)BC zYxV1^N2~1WQ&zsYGFti3oxsiqxBvI{Z*IS0JGk}Vw|;u-VDqmxKfGDlgg5?r<1-t~ z`u|*i+j@5C`+L8BY5kIWPgwfm(s(zs`^fIvrT6as!TOzhckVq!IWDlTfeGJoxtJ)F zD#IKf@5;VzJP>#Fbgxe}5gbqVo4>h!=hBAK{kdyEVW4}=pBzM!^*rn!#(SUTveC{re0nAHkMGvc z-n#}|#W?v>N3nk1y#^*W<6S4GHR~5Nff^m$u|Tb#IT2`$k9v6bpdR6>l|(4v56l{ErII33%K$RiV}j91BplGmHVs{h0s z45B|Zht=STIT-9ZX%4HQ6LT;~;@BKkeJAG7V0x^ZbA_3Bp_t9di5Vh4>(iEBI1#A9 z&NtE*>^y&rfDo2nUL^U6XjjXZ7fN3Lgk|}{i8&Y)&Z#+Ae(}T{40fF~2g@&=n1ewQ z$L3)9{0r7-LYbG0c02!)-10@=8hzJY#`~+U)`jl*w^Wud?cBTMP!=4MpOf_|D7UMw zktgVLHQk|tM~Y6LKF-?m^0zc=C>rd1Lt94mw~ChkbfM(+bI1<>mi$vUM=1nqymD{zJ`{jdIond@O%kvt^_Gr`q!Je?BqFK%Mos zs{F|99;Sxf?%2S3yZVw7wHoYvBcntnE)=fv3nj0&n$_Y$$)8^yAZRszVw47jb83{U z+=)>d>^fQh{JW%c)#b_N^o0{?m9^MnMRkiZiX z_%|$p*95K=mGHfzr!DWBIBs85_#Ocvli_>juAhLIH#32UN&uXhBj4%wV@$jXn07yA za#4Mp!Vsqb(SmgaUX9Z`iMMs!($GR2A}}0qn0jF*CyZQhy6EK(1Oz`Gu59AP)pS=jBjqVk#K+t3 zTjaj_hcAWA*=ugc%x^Bn9#@FnFvi{v8r)=z{WRb=8)Hv6oftqeb#4&rM4hAK{q!)H zO-6Fnn3Kc~k`Z439m`cdRG>LY7UgoE=s9nCZXhGRVJIS1urf9}sJJqt04PceDsK8* z4zd6fsie#lGf;di2wa1!`h*l(aey{5#o=*0dzc|1)Z^=i628{B*>N5u9B3};bs4(d zq6}T}?6qs0;@MZ9x@=}rFzwkhF0m5eHrM@kay_|{zkYkmlg zZ3M%39IoI^0RonXZiNF^&(1B<$`U%qVHKypSx0mtm~xSAsw4Wv94SKOQm7Wm)DL?l z60IZy5?ZW+{Ip#>Mi=5uQK*3X_~gyU|MTq>!n=!}mZyR;R63Mp+UfeZgBOXzVwWd8 z1B#+U_QU9qBt#xbXGkRC@{R;rE{~Pi($)zXMuUuMM7VZM&+&4Ft*VgXac=A40_f`>c)FnsW8198S5*%gG0cB6- z`XhgA01Zo-s4osKK8vpF@ekcDR~F`1{(17Ubo341ch&EInsr@DAE^dFT67?vv|o!> zSbMb$@{biWBI^~Lj-1esC)iO9&LWu<+Hq!c5;7daX{T9`Pg3A{zOQ_HS4@Ch)`7gV z&$(^gWV&bv7o#wL7i510kV`s{ zd;8^bk0whUibL9%v0$X9+z|~uUVcg>zd&CAq5k5~q4w(hHr@(um3iXAz83%do zWjc_%`x%+Z4C-{YA_fyeTugTyM1q0{gsGKAt~5+jxP3Gr0$!qK8sx45^7%gXJ8A;t zgF28q`yq}@B-svJff-43WsCFTp78oSjgzcGXGm^`LTf2~ZP&5m2OM&O; ztKTm-0rGww$j$wZ=xo^dez@lzN4&X|H)C)3Qn7A`lO?(2q2K@!8V=Ezy}+2BCpQ&% zcvJoE8U}gCwXp*7nWvNahMC%s*CiI)5`a={>Lo56YKf@wZ4M)@YDuY_e})iYj{sz^32!$s)F#2i1K}T z(=g7TxYko}qNg}l_DuxwYdFuomYt7uMZx%Vb$WWnXAL90*O>K7CRJNM?CN2kP5RYGt&ljVD?T0_9e*djugm+x)=%l!9=EA^v zDbCH7qOO*^Lj~jRZ_UD}Ki0T%$F;T&!MOG2Ss3+k`v7B0hjG(P zrA(6;)wR<4fP&HbC$lh4kz-Sbal=eSOp_SZwQ}VP3dYr^%)+RTs{r$QL&11%tu*n< zM8h~`t(RP#US;4zJbK^dN_l|3lPNr5f=fWmss;rg2^S}OC`TpmBKTD<)dcXUc zsFUfMnG>$ZAX8#X65_uUpx!1UZ%?X{~rK%04@=Gt=(VQ`Nth(yT0}Q&2McU zZ1C&vvi^bf(wcwuZ7aXKvSD#84?)An`*X$f3TRWf?>_nmH=eh&A4jNc(kIkn6*xYK zz)-N=$D8#mSsutugdvkecwFl`oN2g8&XCAN(Ltl_E(g<$$Wg;oq~Yy$WHB~O=|Ve| z4aD?xFn0Wk?-hsYFn&ZCMijG=PTn0Y#RZALd0(OHD#aXvyKnCiFsKI;;hJ&_3)!*A ztj)Szuh?0`(1*`lZl#ibSEDyb^+-?}G1TxTbi+7K05xME7|_Q(r}`+Qh9IYKjY~R3 zGF?gAqEIL5Yx(la9#7Q3SYW2*++%rMlikV9K|P{U@y(?=j0ZFd>uNTpj|(x31}5~MFWDCi@G zZ!ioP4CC(8VVKBxF$1nUaM#C7ri@weZk;wnf$Iyvo1d-X zRlV6nDveoqjf!0#h&o^FH}qx%*w0<3_G2O?#;k$dI>FCzgJ+!%Ba)UPNH-ozQ&_a& zt_y6XUvPxH2;HjUH8MdviE^aX^Y=S8de$)XayHs-zCLa+3j)AvMnd~4Dt3=wv z*k~AnNE}0l^Gd0?7)7Weri7^q9^V1sf*GuTcg^qZ~ z!5x$!-+QmCJ8Bf~y*J1(@k8hy3N@z#`@KCRDfR43o~FRubR5$5Sc|J-_^bh4=Ob+@L{`e}K3!#eqc z7XzK}^K_zr2)|G|Syc5Z58k4^Y(0q-MDw zIEBz%T-@*58UofPNjWFpt&1=7hyuibuH z@ZPXaKD`ZeQkbU`{p0P0(#fLyx} zFO*IeweRw}+Pu$CQvntRLHu%7Yz0%PX$5Uvpms$gJg9W}a2ic|>{TX^17~Z_e7+TG z7i%0b&ih55%_T&JgJ#ql^CSbQL^xzrC%^L#UG@IIMO@k{TVD(Q^Tg)~3EVOXJoF>i zO3J0LYcE(f6H+$+q)u14P`T1G^*Sg1cwG82duL0%?sWE6nu$Z*X*aWP>2*&161vFi zoc6ibIbo$z(VXjHGY%*5X~=K#8X1n_h;o~J>NPSHLIxltsf#;EdrP95p;m}3hM{Dq zIx2*RHD}yijrxXMeGK_-`ab8N5^vjskmIljB|KRYiWeKnqzeWYLuyT;BBt$_lwwP? zKT1ZZVJnAGpo9%tKlHc;pg>vGTkvK=HK8CzN3oJuJZSpH8FuWVja(x;8TypF-X_lW z)w|xgYl3>$+qG;amecg)F!QcA_IOMVCf*xNzw2#A$`|{tx2OctoV)x<+`M<|w~>$G zk|>Vgj+vK454;$W@)mLg>WD;TSB`1H(J+>y1P{Vjf@G=BieA1^t&ImaUlFZR>C#+@ zA4BC{iS_vs9uYtAj9McUE=foaOUu~f_j22{0HK!ZtUNZ|uQY~x7+OQuHtU@e*+ zONWC}plt8D+GH}Ga~LaqdNTAW-fhNk!8Oy@{oplT@$R=cmd!*#oA&OR_r1}_!@JEa z{lL~AdlDBD!Q21pTLt}_ryFk-j$ghiZH(1doWEI63cX!`y#nmC&zwzUJH@5zL7YYH zfk7*bZ6@VdOP~#&^uN91BXo$)_l=zAk_&ZryJ;VC}wRe&Dptv-ME#{t|EF91;!t^PrLtr`x3Ww>1BKW zXYT=!0pR|ff7%&s|HJlR>-$^1&F^h?H@>^kUjLi*ruDC_(%N6H)mQ&wwYu`(R?3$D zY7v&dvs_&IGthnZ=fOuytAaqADY|6l+SdG7rUxGtOam}+3C{=sUIkzx0?*9dIwJsz zrU96^E@=cn!88C9SNDtn;7kKpunoLK1z@7`)}n17ZyLaYRq$fd02Zu*`&0lX;z!L~ z8a85tKWQ4kf>rP$(*PE%f}9G#MESKvdqLJTfCZ}{V;aDMRghKzn3&%etpe6GfCZ}{ zWg5VORlukKOb}hP3X-M)ELa5z6@ZBt_(kstX%&Ep==FUQ5A*YnEgpO{ZW=(|j&;k_{*w=AUKhqv@1hXR`?Rx`};UIO0Ki2Ja+SyDd(Tu?v*xNwiM79_dTh*45zR=Z>N>I2V zjmKj*Gia1!N&EnznKsG{;IwS#+#xQNO_1I?+;OCw9N+M`%7$K=D?qg6FoOW^3jq|ZiH}{>G$aMz+S(MGpR|)~$!T;%T`{OWiJeKt14#gX8n0{P&J2}Ur z49{19)XjRH;|8ywDhcN>T+WTkVwov=hj20FC`RK@uM}@|(tOC%sM*~-Rpeuyn|}or zNpx}y(PhW+gJ3&Tics#f80+-CY~c_Ku^@3wx&|`FW@IlW=LUryU3MPSoLOI?Uq0wz zO>gl~lwG_`_}fX!JMv_QzGxxjHk5uGSB4He?^V@n6~*&jn_o6#u8Z?L$E4>?@uex= zah#t{v9!m*J5-i^u(Vd2%Bpgw`Z`KQ)a)q#->~sM0PvqIGfF_t9H)3J%Zvj-6}Ouj zn-zvwIaw$kNbvxPl?(AejcocOeWnympaExU$S|N5G43h)5a_0tnOwG33Kb$CkAEfM zZ&o6ya&LGTXn9BeILwql4yjlU4aN)ZQ4daZyRAeQ_t0%0jkDt6FaY-llBdoFik?E8 zElFgG$3mXgp;2Q8LkGqV!Y<2-u`6MhCJsF2KRf7lwKF5D?32cR>kHzTIIfw#3pQ~u zF>Mz-eHojZ7*8?v`TO5#;Z0}vzmH`Zn_oo(HsG?erh;k6Z+0ESP`k2$p6c2qjOh2= zY=LdcBrRb`xR{HWPMpTpX4*b(UV57?V8qtQpg?x8@ih;M+l256Qh3Z7WY z?QFDVq$!q=W+z;(Bo79WVXj*AbbVE_*$BrSjxOqQ!F;vt&UBGtZq$r5TW+Fa&m1I! zQIDbQ{UFbbI`k=lStbs1l(5S!@Vw6~e`o2^f4ub3OE0^`T)GRy0sQ{nhxZ=XOYA)p z!~^_4yC2-}Z#)e|0Q|=K!|To6`*-8JKMrC9es|~nJEI+H=f^;-!0&AT?Dk+gx_ueM z4E)yCd$xL8;jP<2?7-JHe`d408Qgq2h#~mu#-kvPz`On{8^cronX?!hiv}tTDX0$1_Mj35Nu93%0=N7bHbFMj!Hg&8yw5C;e zsMu}<1Tol;Hd^&qPj?Es*Wc?KZQ9%G8Eq=>$wr%Y_qs-#cJ?|(o3{7bTGM%j!q@C; zcB4)4HQZ=ZYz;Ho6kS7&HbvGDqfOy8*l1H|4KmvFaP81&(|gwL(V8}x9_!{@VJ2QE zW^-~v)EVIlu|gPa>RdVTg1J;3D<@tsx2b*Q#0KUz#aB*jU~W@v<-`W&HbqxXY+!Cv zWaY#L<~D^_PHdn)7*&N}ZjdCyrCu`9XLTw7d*(+q70hk=$i^c^n?Ahpu+gUP+<2$a zrtjEzhtZ~Q-*~&xrf=JLo7S|(M?JiIP>*ocN&+0r1a!jMy}WzbXwy4(?=af*_TAf! zHhsqKGmJKU`tH+>Hoa~4Hm#}P&d|+LGgcZ>Zh748$vO#G@3Y=#wCRhiFEZLRYt0&M znzp8mHceTzy-Q*4u##47Lo&Z<+^TII3bZ%YDY)v52AwOV3YBtFC#=o4ZoXB+I=AUt zHs4~j>6FYONuRQN(y~)N zU3-SFy`J&dA$-8|F!_ULhTr*}^DgIIqK5)F1M~p@T_=E=>@5BX4QLWUATfSUIg$IN)%L zkDfvZ#SVTMywn^j?S82x*9Pt|&-J}De~>UXA0aJO$!a}HUgbsYf3GjF%YzB zXlNQa*kocV;lcEdm!6q0KW#Ofn1=OIyOdAm@t#uU>iu-DJv?CG1yk&U;0Zq46)39A zi|HOs4_kzUP3(BNse}{L`pkscse}{L`pkrxse}{L8o{MzAzd#DQpS{0E|ahhbm3xp zD&gd`HoaksC!0*6Ep<>mi7E#P@u`Fp)4Hzr^J*ts5~!T5&^`af{6WIhRKkhnwRWbP z_aZixaKcZ@Qq})O&@Q+&QBB~vTzzoRLUbzOgrDeWEGJQymWJgM?R2C7kdRK9$bM#T4y1j)Kd{)L>%L zJZdW8grA(5@R3sqC#E$`beqLOi$?L9*-dzSZ=%VMm^Q5^2Dv$4Q72Qi3uLF<(->Pj zI1SIAW?yV#S{q5agI6YXBXRlwdcI(D7xZw+Yqj=~|em<)e?i<%dD% z_`_z>dvJE1(Az5IO5skm*XAIgE*hA~89g*L=NaIma)bN$)}5Iic4T5iV7Y57=SLvXr*e2OEVVWoVcV z_9Q+9Hc}0Tb14cMwjrg~k)xS}H>tOo zd5DE(CCllN{?d<7gdeid5-iXGc3CytNm!0U!c{Y-I&{}2{CV|*A2`P^ok{P(?dqgn zt=mNm7$5{0h`DW#>H*EZjED0ksPelqP{MGw8BbS5Gu%LpW6@kbYx{n*ulJIf^qywD zYBsu*l0H_|wZ7w4qhw654Ua0vtBpbZSPE}(k+iGZJxxsQ<`kn?wHv#f=j*-OIezg> zdQY=n!EK$4@olP}Bw9qzYVo_nbW+Amz1j)+70aTn9@*=%c0mSun8D;^V&Ihb$Jf_; z(JXo=cF7a{+*JK!gIFmGRO7Vj$=!Y&$uh^Nlzq(ZGX)MM3U+$f8(J*Z8thuVj=?v3 zxUcss&hZD%r1v!IRU;J#t6A9ezdDq2eSipON;!k z>Zfx{?$0YhEjS~tSf2t~4pT}f%@ zgia%zizKOmEc<#tvtGq8CevaG5m58pNT=uJkM+p9tSf$WR>td8qbipd z+`ipZ<@6BQg}u53!~5-hr*r&*ne?7!-6(NPoeCdg6Rmo2S1i+=xW6#V7HxP5$nFx| z7KpR1m?3jLPd%1S!btL1$UkrV;4huy_nk@aY1YZSY&pAj*NLI!fq~}8Hl)Tp$D`Q< zTc(pmU8;8@Vj`ba2^~g2%6DH}E{^q{Ka<|mtXB!m77I+K*j1Y>qhzXGH%EIi9|fzT zWnnfDic8Q?E2&Kd<4j-}h>*W4_)uILFVMN$+XaE7UT1dC(hHbH}=|h#v})$x*gU z8)B)=Gjb0p=KzsxW!k$QrXLdrrdIP@fB*f#ZO-v?XVDuyc&b0KUS-9c8t41nLZ3+% zy%y#mVLt81O(B#~wX91k{d&2Kh2c)Lk#NEV%993O&e!`==lD4@={?PQ<@rhRNj zRnTZBtsSdLRYDWmdRO+(wIn?_rbJyV+f5Jz(_@mXR%q2qR=7Iq=RN26eP+^on)QmR z>lBbQVja2GgBjekd9}(oyJ~yLRf|#1NOx88SkaF|%9U9hj>ZNl7TfpVo1Eik&7}7< z>s37@R5J(?w}7Zzv+CtE);tz*28hdpr3}j1X;P2odY0{!I&4zZ=w7H^*!Lq}?~$4G zp2mOWj-2#RjZ&i7INX9F+OBKZa;a1uaM6~;x0zlm4}l;NmOJGTj=)^fYVYq0*E`3z zXVQC`b&`nGLrh=fs|`i!moYnEgiuy?;+URS1ymgNqXmSi@!fJq6S|2`Kiuzy_Ro{P z-mRJRp3Z+G9VgyLkEM>qYE&Xr4yET>T5UIR|m=V5{lzJr@O=s{#)1w;$wZrO>RJ_6YHwypSVq#oF z@$V`7dIPiQov62SV!f)S8w%1Iz<&H3-bf+-)mE!yR(a~}Izqqh@)5fn?FJBaYt4sW!g#TL)+=$=Y-VeKa)&mxI=1BW?uf-UK zPMPv{j-*5Tp-A&YM~?9G<_;J|{{8pp;W^T?oht0=6oK}BGky=TlBY1lkC5C?_bJ!^ zN0#gT%<;dw_{S(13!gSOJ-e$AkuDTxGXN6iB2CUJ(S#`xZhV*WXuS>eK?lJ|DBZp1 z!M-FaQDiPlR8^{+Qa~o_;CNpm-9ku)difCL#G8a(m6ICD@kc7#hp3k=iIKmpAX?w zc|k(psUj$U*RDKAinvopP|nlKrwDaEbp++TltJX+`Fnryy0;=IJ@a*4ulb&~JIFRDw6nmpT4gX*^X@6D3#VW>Ln6^a z8&;|#4|f&80~>1_#x}Y@sM?^)@h| zVX4Xk#ZouS=MlsIa%p=^S`n=Qn@iGJItd@$C5ED0;Mr1{OBbaA-ikB~w-y>mh1oXg zQPX`#yrFk#@ zn3)n?GEx?lP%~w~bT8sU@uppETf4-JIjCU2>Av6X-~X4F+5P%|U-kdzPk;W@1App) z-?9g8T)WxYkm)Bc?{8*76Rwt=u%^e|@IX#bp&Dt`cwT0~ycvp?H8Uqx+A;qlcwf3l zr^?=-UD4|M%O@Bf6~Q^>1@e?Bc;?wW{8jS*|F@uSE8Y&)-K>-J&pKiWEl1l}iLx`* zp3!R{WC({dg8@WEYi#0F-+~O;wyTP~e;fNfY;A_E{J&#w&;Jo?-;SDDAC<5n#-?2Y z{*P1UY3do?)9YO%G$K6Z?v_ZCidT#vMpfti@5NPKW3k<^UoG z_dNAg$iu{3$~GA{-Bz$x*MFmvZFq>$6#Z0J`C16aU>)fX^hUZ%OPZEu-A>+#GZA-? zWpi*&!?4J%*A3;Xtl*+uqZdI7dUB?+gwg$`kFR%~u_xM#J9EsY-`W#LNt90MiKnbV z`UmfcbJVW+BlX14|M0|9mma*Qju?)>`zf4;#2jK=(cUVP9l?IzRdD$^=;9VTx}$Fr4pEz6&juKJIj94eMj``|C#&OyZ!oq zbIhfCzH(dDI{Sy2dF%hd z`&Li==K5RI|2z0dWM9`kT??RC?6hf*Ls=YzfO^4XQ^{SCZ#FYY0=15Y2W-vHHcZR7 zV2eq@Yi8P+d%PAznsnBSJDI#uDBxbne+1mPpQg(k*& zE*XXBRHAGM?$M}QwG*r#G_cz)lsYLoX&t4SO?Jjj`9#zIfa?Dpd=0bTtG{jizX?u% zvfNw0ESx^Y@R@T)PT2LE`hUTNw=W!d{SjvS7u&DiCboXO^@^?Z<_|VsyontCx5Ljr zd{y9EfoBKcLti`e%tPUg|Gja;#_sxE>reJ0{y($!#I?&;?_BM#Ub6D>mF~&|mj8Cy zSU!L0jwN;JK8x>OlomIA>U;kWyGN`n(enCjXWk!nVs^JJsbDo1jmATQqXM~MpX#cy z7Fp9(19Xi(+Rt@NGBk*@d-)OVt7f3Jg?di0ODWmw^kCO&RWzU;LdaG>fU%ec24ONGXVzbi(#9m`QMg7Z*~&M$e;g ztkGwQSOaIO=}fg#i82Jcmm1N&bOu_x-mZvpPaklZjBbEsuV<(fg4STPRhBqWO{a@m zdPt#C%4YYl5$#K6pyfeGm83rGQWZ4n-^!AMq8w}4P{&1jQK_o9!G5xY)BWMV?^|?4 z`{EgBb)p4^>QW)87Tq3fsa7Y6c4;A{n{8HTqy!pNz0-tstV{P)%76d5ml)B$Um@L$PSSMT3E^P z&%UKzcYlIiJ)(X7476s{s+)shvD0ffqSjLs$ql(cx>heHgBC58@;OAOF(KqR0=swB zi1wBlXx(U8;NWbc5$yV_VkJd6pe^!#m=GIfO384elx{g_AEM=u-wBTz(LQemS}X_& z4x47VVb6+^xZ-B9f^0$|EG{aH3Y0-pPxNq+>&Sj5JaR<)+!<&)yq`0dYU;?KlIBS< zql^C2wzQ289iv~e!%n0cRXS8Rn=|_C-Xlh|&zXT1u)_78O%7Vah5)qcTsD@>%d~A| z9I)SwL2XGIwCeE?SR?%1;o&3NXU{qcJvmwK3wP~lN23!@Hex=^3 z7@E-O6rB>g2ajm?W}rq;>_EIV=@&4y_-7bm+&*kDyH(Ex)@DM~PU&F_Tx zi1y|gXc4krY>GWyZ=qT~6HG*_B%tzifi(QDmf2VbhFbtk<#n;)J8x`6`>Yvg)t(&= z(SjdS5(LvmJD+6ZLcEsqU-ou#4Z_MR0zb$hEE~4{EiyWy{j(Wp6$EQF5?F+4d#a)P zFG+JrKbLb`7e$lQ5{BNhffQmE%O35|+sKIanKRIK-8!r#G_^{Wxjq>M=v1y*uhfDJ zW#o8ST8rA8kwBGCIBPa8)l$Y!~Wxcsoud^S?)tf ziO3e!xQ-)so2Wxv9+IL0UqP{?5y9BK-4X56W}r>+L#@%kBGFuD*e`Hp|23_jou(!x zGoe(mjf4v-?+Sg{g@S(H9vjg2q}b$@oa~U`(db98-vtv6-o{P2}8w11L2db z)6SsDa>%C*j%bh1KpSqN8rbq|!b7PD3UrN7o`d`eTohd|MfHI}qn!6w;C8R&580I? z+NaDw>%R-`ddXoZbb zq+-=Ll}-AuI6C;CfM7$U)n-W;L-3yOWe*$CUOxkExyP3KOW3|eMZO;EcL)t_4pTihQx$Xu@i(0Z zk7%Db1MM6?V6l6bk7%DT1MM7Nn|z-lZej{tUEpyky(Y|Cd|3d12*>z{K{>Rw^!R5!EF5zJK;PT8hT7dSY}QF%3O2 zIuDmdML8-mW`)ulr|o^{)^3T96Ge|mGwK*@6e_*MKquy)Iuvi9(`t<`^CecS2{tHNq1YZnWPmmT@c zk=Gs>9wClgvi+m&k8Hna+uTOC@3-~6tq*OzU`yZpo6TozK6Vq>Tsi!O!*4iz?V)d< z;&yxGD!u4expP`IdN`;T%0oXO(DZY*l@26jT-m;R`|er6OWWVt{?{-F#3Df$dqr%Ym)H)~w*A zz-D0c=qi0^e^AcUb%Dcy!zYPr23z;v8>wAY^s~?9(ZPXkJ}dCdz%yqB9|=4o@Qhi( z+kvMCo<1vhD{xcbrdh$8fg1xi&I&#pxFK-Etl&W4X@RHB3O*EgYT&7}f;R%k1IK3t zuLqtIc*?BcwZM}DPo5RL8hBFRNwb1i0@nwwpB20uxGr$rtl*`MvG*F)Mg=_2;WUpB22a`m@!a%?e&#{pspYX9X{<{$%whC#S}l zhTH$n{o|9w*BL$MeCsw-Eg;y-RVpj0V!pj$4J}Y>6;bjXiI|;@+#hNKJ zl>tXm7Acnw*40|`_uv=I3SQm%^Q}Lh6}+{FFiScX96$0WZ@+zv1jVJt>W>t4HxwJ_UJZ>{`(mt@Qt@#y?-#aUK zb@}$?+fO#{Oedbz?eA=V=LFbIx~(i$E*0rqNigi3bg(SNYT&(rlV+UpEVaD-?d@-$ zG@~#mXVY0PMfcqfofpZ24P4)R-{$*f1+Q(sck?%9oQZjL^Y+c(^v0RME1U1xe9x?A zEN{Mh^WC$8mp0$E`L2^w<4k7wIkw+<@+@`6L0Jjh9yr;+GlG``?+Ki2;2FV7fp-Vq zeZuB5f&=dgyz4|~xQS9CBX*H?nw1G$CY}Dl>Aa$>D6@iBR~jpgS-~qS@`^kwczLD1 zQa=H9&g8D^TQA&t;YskB25@8Zj?Ftxj`o>caQmCv-<;Ltr4?yKIyod~>gJW&O6>~& z=CinXsWZrVMmH_WWkx_m|0UzWo6jnO%nDseCTE2%Cla$lm#(?yVg7|+7F(~r`s6tD zvNlOOH91pu{g+L6=iqf<&A)~IW?MKDcy;Z0YrokR&IDdrd+yrF<=Z(EczNwPYtNb0 z=B2e~uRZ(Z+&L5T+TPmUN#Zl!qXOp!&Ycx}C~%Jd-|CkZQVSnlSlnBE-Kx8auAaT} z&ns_PxppPD{n?fK1)hGGUjEkFRm*Q*{L#|W7ax20cz|C%zDzG)eE8y}?;crOde7nq zmiUd(7P@uz=09(~dGm>z+06?Me{<_~hu^-vxqaKhM+4v8kT#wlc+cX|g`X{5zW(d= zzh8gJy0#u$U-hF0UKzOj(2v$Shdyv<@6clp9o_iZ#z(i@tuGyU=@I=%eC?{W&DAe$ zTSx9Z#cluKKZTa%r5!@+Fk!K*$YHvLb(08~Bvh-~N@vP7DAV_o5?hK`mj`_?Zg1Q< zmS}hKJWmT$(csa10&107129;`lO`WV@c{!Riz2GG)vDV(eEfuj7UXMwF}m0wsN{L1 zRnK@bU1>z)FeMr#Kij2H4iZkXEhYj#A4^23E*OrduvU#lG@}{PDj-$U2U;mv84glW z4y*KYgJCWe5jO5WlE^DDw&g--I!9Y2GaLArnQ%o-&G@~w5QuKnS$o3GEHh{4X z3hXioEhR*TuK%{m8zwp$R744oNy04DVt65ocR@JhhZL=UWh@b{Wa^AvR&2`#88D*` zU{2-ekO%92-mP@V8GwjU$`V*Hyz%w1gqb5kKx=?QepLm!Qfe4ztPrncx`kjY%;a-& zCnj^DO4~CN%dZZ4Io+-l)fqqyLhBv=|!n5LtqoGHDXr2X`~X%&9OwJ zX_>&$2wqQlgiL~3)GScVA(II+2psQqGgKswWMC1_5rNII1YtXckl@KN&kbj3iHl_- zS+DC>azG(braX+zq9Fwo3s8LJFHcAatxCp7>lIA#i-Aa9G8Zx%bqo<-ipl*mUSiV8024bq5 z71N}fA(HKi=|6%C_ab6NOVbV)VsnZ0+r|b7}m6rZ)B$0R9l!SCCE^X6c&oFR5S$?U;c9Nw~OY&dyI~1VA2Lxv(h4sC$ zgdNe8h7vDEI34%jK6?=pwK$2ukfcPn!E7hP30=Ca@GWe0+>U&=>qnecwO+E5$z@XR zu!TUJUbV1(((+Uusbh(dlq4XE$5w6wmgU8re3#DSO|@5NK(Cl`WhvSf%LyP%_5dZC z%2ksUCP!mV0M5LKZXTXN;KNwZoCSbHHhLav3$yBjWq8Gc{S1r^J zE|d56-$zyv5Rk_3Axx7nLOTYKl|FzfNV=Xa z18S|;FiU8mm|+TjkW4PVcx)^&NX9|00fQmcAMq&Rr>#qOIDu^{sYDtG zFYk;cEUV$iwTPO*4iiC10GpEQ;G}~QDoeK$dN|oE+Zj$&Y~s*~-ZfP$l9Ul!NJL|` zyh7DFNQ<5VWtD@ObslpF-hFi_5jGUyl3Ol*}$5+uT>!DL63k`*ecg|Z!` zCgnvSq)XwNpYB*ODkZsAjq8|F+Q^@f2>IC!!)Um}6 zhm28p2}H;!p~k3MNy*{qevWL)7@uqgnPQ=>CWE@P{PD3wBq#wg>3SN^ROCTQNr&pH zQx93?45mu0Mo6s5^?aj~ulLt~IhF`pwQw&{%yi2suP@d+eX8RTjRZHu>m6^T3NW(MHPZy)X6qze$;6WtI+gZ2tJz#UGL{IcmV&W8 zC?7Ops0xMjY>&f3c`)b=qrm=itB2p?}DL{#$Q zEb>4#RqtB}o{yC4g>D4fym%y$AC&E6*FSqzDz=UU3uIhUWVHynJ*nfh+UayzR=|ST z#rm1e3&%QIE!t|-34hTmVxrO%36gC3rN{;;lH{w|VKSo9Wn&TK!a9u5rNTwc~bp&72Lx<#2^X2dNm`OGNs9;&Z%(D_Wgm)LZffD@ngkvbv@IDxxUaYNz_rk3ouO4M#ITsFRI!bWA`AqMkW=DC2FEy)*}adWjUy48MmCvgRvGE ziD$%OTv1&qsF&8!v5ry`j>IUd$ih4X8v`|;M446=BHGQQ+*T_zmbN0W#HIW3wGWOZ z7_Qsmv#9Llly=qjV<9*<5=0_Y4{GCi0`WZ`Fs&Yy@DJ^e9!t`A)&? z2_b@yIPETxZ}A#Hg<`cnN{C#%wEE+b1c@GPhRedPrQrj$qF<6dZI-<2CVfn-Y zhr0EC*x(Zc?pYYr(y?&09!qVY&nx=p;Zh8VD2E~6%8yN0`bYQ= z`}zMaTKLJZ5ac9IQKY#5T`j!J|*igV`gdcjgF#p2Y%jY{49?J99)# z&ticvwqOp{ojHQ5XR)9>wqOp{ojKyLXR&}ETQCRf&Kx1zvsl25EtrFKXO8IZSu7}x zEtrFKXO2MdSu9}37Ry&EEX_h3+7U=G%uIRflw zu^>OTU=G%uIb!i=u^>0LU=G%uIl}g5u^>CPU=G%uIU@XLu^=f;m`s=Ez1civ`%&f;m`s=15#Hiv{S|f;m`s zGPCD6n8gC*gaxy+-n;p{uYXo@Y{48!7Vh0V#^d^bcP(tX8~?UiTfFP{e3k!``g^Gd z{2cVo#>pJ?bF4CF_L@@^RQYc?=p(>>a@13$;06CUE3C}1KAt+I?jL*(`sn}2d>uM< z$(4JW?luO6PMi7~#WUTQT{VWoWOX-{XcQE>&QbMx+f%48GK@6iOv&m(YS(Gr<8-%q zzeF^n6$)J&wl#H_Z|QXIDB!?Ow^^B;OWQhnr|L}uO);xoe0w0j~GWeSa0^JpPn zg!pdK=zDD%DT}Bi5IHlOi}~-+B8svr>Y!f;#kb1V{NJa0PFJ@5tocuPP|K96E!DJp z50A$IVDwLN9gIZ$tQXY^RZp1vx%C?j+ZvYmBJI*q01sEGsOljRBQIDb!qN)UBCEyiFbG`?ghIZcLb{OzrCHfkHd z`hTr|a{a{%A6{42qlbRDzOwe|wObEAb*&xX)*iVQSiNi8T7B(mZ|lpe$<6%s)?sG# ztV1`ie0AlGE5nuaR)6LEzi)%Mkg-?99pWpeq#rSEKEOYdB|VToOO(AMi0zrXmt z#b+%(dhuZkKRNObft!xJ>_{{4?nCcCye4_$L&A7`~Js{XBFbeLasaP-L7MftE zAH_RZgXo$O){B>G%`JQ^;pzw!Hxf;Y?puwf|HR6R&{(Wf5EKl@s17;oN3lU7W)_-D zNr5Z7;7XxkGOaXyg#yWa_7V&0Hf+CV% z8z6$$q+X_7?MDO_tF?_j*76gwV=7K`cx)9My>zMAl?GK#I$2n@)o`v#2*Yd=D7zIz zHbJ2bNphkf2cs!bqwhCXVIkEjC?^I&DVdLj*-8`1_dQf$f>E$OBqQWNmMKTlV6?RQ zpJRzm3xI=A zA{p(c8?IKS5T>cu(zb0n4ih(vB}`c=jwRaJJT&C&lAn>(gyl+gKtU?VOM1G6q@8@C zjiGIhC;ivq;nl~TkU(pNR@r~t2a;|F zI?Z}l8m8^6nJwocU}V^2sMRmNUjcg5qqEq zGY&(y2mKhBDGP+%O=rvkFBCL{2-qhiAR6;fOy)ZIAe$8%cm5?&oggnQB988D|e42lmdv? z$T%K!ESt*|I`NXoX8q*;rdDkx@s=YyZp8LjCzM(H*jPd?G{6>C$yCePlI_2b?b2F; zWtuHHLFRG^uZBet6o1!kQvs*Yz$Owu7BvF6B3b5BM*it zrykG6RQ~``$h8wKzo?9r%*W%hA9-Nof+jQJ#Kz0U5;eHqZucAcsAfKX1- zBHdPIC_quSPv^W$v>~)IE93Wqm8wnV-Aq}Nvu*!1v2G8loG6quuE%>wTCMnHuBz3R z#&!XF;d^5pd=)R^v5qc6YSM0$^3~8Fl+VCA)hO%DC`gDyrxR=G<;HV+jhiy*`&r8IeNGY61?4rU0wW z_{Bwlnx=bRKdRKiB^f}Pi=*ABoRvW8VVvaQnmvRV|}Uu zY`kME5e;{n9vK2cK{uBy57Sv2ZFX7+uEP*tBn7$?N#~rTk&P+qL(8$ON7xjl46Zyw!r26c^zhRm>+T@mLiyA z5?+)bMa+wtQe2}~xUmG(p@LKg^+Xg5YTc;H3{kL|(4&eF!~IyDEF{5IwvKoqdSU!D ze~#bq*7dQDP^in7D0blAr#UucBT{FWL|nBP?v`4Rm8%CkVqT-dybxbA#}Yw@rwWlw z(2J{ft{Shmb4*3Yf_5s#;Wg2T`Y)9Mi1JhG+lya3A(3ONK{E-almewCbAsF|R%{bZ zN68@Rmnf;&3I9cJs*StS>iDf|B?uUCi_9pt1G}kKDagoqe|Kr;Nrg!i3QVEf<6A54r369lmaoW6c>##Tonyvoh*mw0v;DlI!v?D0Jr;IM@I2ZfBEc@RicLn!-*De zp`2`%AtM?G3Ej(vvlYL{a@~@3s$%pIleJ>amFve6W+E!)X;$Ev5?~{!Gt}V5z;RFz zhg3V0N}`&POZVwPx3m}Q9%v&~8sDJaw#7()8Fw$P}CV6Cr5^BgFYsH|x( z{rd?CIP8~$p<-RASi?MF*vR5m%_&E~A*3|KVXc~KrkmOzi}zPfNEme;0jQW{;F^b~ z@&c+Pl32#i-mUeF4w!9Pal?~Ut)ydt@vBl%55}P&kLd1DCjI>xcCl&`>SeJ|KA|U@ zxLqo16~&(mBC&d6kK&5VfH$&yzd{WM6+ukb2gzF1K%5kd`^&27nDZ*GyL}QlHiB*`g2sK1EoGJSO|Jf#vmklX!;sg-VL=e=RR?NlnnN}H~ zL&a=e(vv|hMGHol&x&=A#X4mzzJ302g zme5|kNe&2U`BNi_3PJ!m!b{M#P&yeSAdfDAp{!qZtO(?WR zDim*fRI8Ge^7TTR)cpk|5-W8uyvT!aoM&*Hj8h%NjOyX7ua2#%8qqL?1QWSJ#XkYy zUdS)eoe_s_(nR}0tL2A)8bpH~#7$-Kf)f&{M!M8#CJ+Tlg<%2ag7IP{fwTn(c9aYQ zfpA{)6S@n*`1(i35<+O8xIHf^YKkj4UBKh<7GaxO%*w`)euC*@6cg#EL;_n&E{tQl zDlFu30*Oj32Wtz0k(bjAG+c%u4)SX%gv=NVw%k}b(X1<5*hotyQin9jmf5Nw@(B2= zvXV{r5mRYuQHaavLmP)0*-VAVo5I$k#u9Q*XymFK9IGb$*UZwu4?)$-Q6UcY&|1~$ zHvmWoA#%Zs3R@@UNyjTed8iY&QhBFAD@~I1XuU|p0D;jpkxE$uJywF`ZYaL_`f=Oa zjLD+IM$L$+wp}p>jUkwnN!wD5Qm=?J1}N~5mk$eOX7i!OY{ci^tH$%xq%FN2YAIeP#@RurV^;-xdmIy0>5wW};dEDkbF?%x(VVMBHM}(7 z1uy9qsI(vVWGm@zm$bKyu@16mm_h#t9jlr{v&)cZ#9yMcpczVIc(T&VhB9m`!v{;4 zxjhbKs1(~79>)7fCL;P}x9}#H;Nk|aqXe!Bxo}uEZJrsjfZkWOvtu1{i5;3&M<_J& z;b4-hL0k&Tx&8$um~xdOpDEUrZbo5Krm~$FOJrF;qB#Wi28@H&qB%DO27{P?bklHV zP#ZLr5L|4u{jxDIu>Hu9gs74YA(<4hI)>N5SiDg~DK_ZJDGs%(iCPKZOC2sD(C)Aq z@Wv8K*^5-&I8iN>vbACuaW%&vW0>T2K^vho*QqLHlW4J4W_cXKDK?@|qoXCl(RQW- zhSNnjiFS}S3UvCJTCat=V!|<+r4HlLOAj6EkUGt<$5WkNv5P5Ux~UjUPUmu+vI!z) zy8@QsVXBR$8+9cxUa+cRj{rQ%t1wY_m>z0_mgLubKx%5ZkNQPnB)i&m2kCw_f|ORz z8tJHpOVLsok44M9VXTp?3{nm(vT}yQ^8)36+*9*4I9@OGln$_X^XLXy1$hoYYFa&% zFT_3N zKWD&=E60{UxBSHA%a%T|WG!8=c*kO6amy$E9sJ?;E?JT9m*0`HNNg}P;1tr2FbzX; zYrzU;$KrIoT+)pMl*)xjxn0vpB5bQwFcR%N`#m3p-+S(rF!r;TzW0th&c5@PiO+rZ z=Euj*eG@TszZSle+q-x~+!gEHgugWvh+%a z{pijMUwGSvSAXgaPv!P591%ms=rC=@p}LvvB?o!D))EXg$z|MLH4zgsv0B`+gPz3m zjyWd&-^bo|(O%4bim`t7N7~$jpL$;UeV>2Qcc1bI^Am6U#Y4^)zr*c4U_{IpRR5~d zv^~NTCc_i{n1n0n{MTAKmXrwZtwmh;+Vq6d*xKQThfAFG^M!-um}37j!J17CHXv&sq}JQ z(kyDR{+RfCtGB=1;@^~h-`~BmcKg1JZE_$3_uLViNa#3Rq%WgHADIFpzs1!Oe-EQ2hKXUP!zq)~Z>($K1i>p^& z`YZ3D&wKIzy8OC?^p$`3ko`Dr?}D+#vzMqmVeu!v)qUWLkG}c#&pq>%pL*l>AO6lK z-17Oa-(bAuB@5rb;>BBExR~3!?}&JgB9&v}zq~Q~ln<{y;R@r&KYz`C1t0j?%jEv? zXFT93XZ0U`fl9yQ4Ia05{)l*vGL>Uu=rdmb{11NT;*W@b|HZF9^f5nAfnWV!M(9fL ziLK(>zWVydy_?%RZ$vytp~^9F^YvSg|HPwiJm-g>xaPtyG+wjwpRc+0?4_^WjQ5}) z-gMPjm$BU5xg+8^N>z@D(PzE?*Dv|dUp(R)%p=t+A9M99K=kczPQUg@?80xnFn+^x zUi?#T@0=0w9K|Zf#IN}7_rTWE&;9;g3+6{}dHnOk&XaGw`MojbR`k3qx%HRgJ#Q>VoIK z>Dll2=6&n;eS7-5gThOQw?DjePVSqZ`Qodt<@UBl#B;=fjfpGI|Jh5g`^ned^0t+0 z&b#Rs**CuE>T52KzVKVx^X~fFHyy6R{;b#>5zi6cH73p!zVV_v)>nfO;;Falws=wd zoXgf<{aXDWKX~Pf-~XjoU%kQY9Uc+S5w$fYzWWo8xJiBuedRCk$6Wi5UpSw;`PPTt zrU7rd{64v-JpIGLH~G^oFz(~ogRxHZ@f%)t+l8Ng?H9h+IQsY}|J{Gyk@&K6$#t(^ zW3RmRwrf(6PkfWxJ2WDmBgSe>e8+3<;y-=O_22p4jns!9{@pjqrYT(YoWeyH7rge7 ztxr9n%+R1uGJMv5p7W8%ryh9UFWvP1ziyOX z^L1`-eMCG*MAVr0HIHBT?A@0gzWAf%&!?aD!iT-}32S%W{Z{7AWB=#Px4!y2AHSl- z?X8W7=LmEf6My>64}8amtQW-Y`ZQm9$Xj#X;RiqHyKjBVd%tCUQttn~+xiD?Z*@dG zNBq*5_+2mh=%?;5e)=u^_}#C*_TQ^|Zr%E|C|`KBTlwbGpP^;{54Y#%Z}ZP?bA%<0 zi7&qIOJ8-}zuNEo>F`_6+oZ-`0Ct{YX^3?fWmk`zK%Q{m0k7F8_+#TN)A15kxd5zTwO9&#gWD z&;RKi7ioXDdi4cA_^Y>--jIFbC!YD_r~TzacRusAk8^vABjPz?gT};9y7clVF2WCg z;wxVq`rxN-)j#{9ugfpqy6Mnu@A&)Gz*ArFao?pEM#OW3{EUg;{F7^M+IXeXojo;PM zy}rw2pDZLZFd-8LGnQ?6p+LurYvrGZjNDKET7x^mK7GQHBxeEMVhWB%}W?E8K6JUZt*N9X9A z^E}^yi_wnl2XF0v`5Etf$BQpW-Te749ZulqzWkP7=Z;;eU)aLAo2?6f>-H^0{MU{lXTGs%%~O{v-E){q_KIE}yV=V$)J z^ND}|^qyb+?fb~wv5|gZ3nx&vF8qq)?2iv#ni-w*=j3<(?S0Sv@Gsa;J(K_X!1t~@ z&V608|CLVe*igT)g@Ywq7nUx1N4)m*=l{xo=3m{q|I=5phmVhcbo_qe#TOk9+)mvh ze=c`ypkLU+PVTJ>zrcPS^6SsajpH{w_o}!3Los#s+o!L3_cQjpSA{S6zOZoK7guc` zp9|03Ketq0de+hziw}S(w69(#*DqfC_1fpx-m*4XBi5c|f5`q>`Wxd+FMV|B$IC~S&zk@K{0HWb&6nr>^Xm&ATDWeZy6|+{4YqTZf3ker@@tpnjhi;E zT-cnu-`28)Hkr*!H-5YErN!FfGeAy&A8vkS^XAQ~HnJO+tp8^H3+r!PzhZT;npnMP zvYp zvP6Lp^u6goX;65pu7HJrBAb@B2fkfDbN!G(9rp+!Sy5}l3{{a@wSHq;Y}18rFWhUE z`mKfUnx%eo;X7uj-&pvzS?bppzGaqr&%!qi>W+nV#`;stUiH3(@0+FGyYM};)bB3b zZmy=}|vSGPzokY;C*h8ol;nv()U`iwr8w(y0`9mpd-U>LoT` z*>3C73zlADmU?vQ&&*PUw4_Mp0|#e zrJlR)G)vvPe%LJaob{*8s5|wqXRkZVQqNj{szKecw(eW_!Q9;9xBp9j|HuFSu>}5D z0{>e|z~~2d?BL(9aHCo3UoE`REcNvZZ!k-J-NIiPRJGa+;!RpsdRVm84EN}59sI_N zHil-YFWeZIrM_T8HA_9Z(Kk!IdP6Zwy=tRpP?q+hx!f>8k_goP-+J#Pm5vlZjPxij^_730CVGxc9r zjOotK)Ss>xgZ-Tc(1&4RtFU&aJ`LuLa@}#Zar*kDW~ryGA2bZuaabN&#?4ZL%Q3Um zBg;{<)IVLu4C;;(lS43TmU<~no26a?Q(Nc%zcxGn|F!=u&yD||-^WX!W0S1|R-^fo z8^UIX#&@tvqCd0;o@l5XC0-(o@{N%_LFrXb%MD{$lJ5Icy)2t2g)-X+=YupFqS&~n6zkMHxqbleM2h*!fK%h%bk39Nk4>Kc z*KU35tmF9MvyK*;)8jqsi0a4b9@7=wr=%-dcw7I6=Ze4YbimEF2L%W`IueKPqUVm%76gsgP{}b)M`=FKodwD$ZP`mvx1~ z@fm!Y?~Ux@fAKdzuCk`L>L<_CyaeE?iLQh0aNl&z} z9{h*riNEi3z$>=9r|;3vMD(P3qR;QvF8G+wL}y)eyI7=!9-`61G$)Jg_CTz*NF*?I zR{{;OTB{u5r_@MfcJGPxLn)jO`+J>H8tK;tUKK;7iEnbqNB1IqW{@Z$en0D)W*Udd zq*_gYENX1#5ZY=_#^9uNCE4(kgQAdERK}NVOq4M#u?iD07l}i=b>NBG{D1GvHg@@zW&4u2cpEseKbrsIyaNcF1&P{lum7tyg~TrB~^n*BmLf zJK(g56A2(rr>lVY5uelPaHGC$Z*GB+=WR3c-~kJpQ>_|#P&4l(Z_z*BVZq3Aw;6ef zmCOiMjl4uN@5N8lKOGi~+`G-lCtFF$VAaScYv#RhQ~x~Af|2KJGxB09SsSbxd9h~R z)gRVBZ?|CN+1rdfU?s7GRU;2*=3Vhc{qr6RMxM3J$R}CJ{b1F|Cu!yl-mHJ#V!_BW zw;B0FE9oJu8mYTX_s5!%-Je@9@{Dap?zfUb!m5$`HS<~?{qs*O7|k*8Z(i?wRx>6&@?ueHzkqFE!i*8jgew>E%( zyj+<7@=2c7|M7DFACv%C{J-+y#eWMYpLTw0vcuy43F92QPR%&S77llvqHk9b&!b+i zx=w1KV;|xToG|40VG2nN6C+M;(wdIX!MyJYk??*Qpuk z*urthQ`HsSk9NhA8tK@FxLn#I=3}mOv^=56v>_4GPE*DP-Qjo?=(x}6Ljxyr zMG(F_5zb{2!$7_1Bzu)G$#tSDYP2(oCq-+kGOA_B468oyEgN+Ao1% zu5z9O0bHkukDrR&l2d48on9vhXc^*IZ#jidQfy5bmc>DJTpxxGO+=B6kD8(OxS3Ha zcsoJl6H%Tp`BcYJ9J&9)bH(i|{fNFf!Howt?%lX&({McvwrpZ zc)hzWtzW**gXjeK`jK`2x^w;D`o8tO>wDJW^|`f&)*f7YVC~+ud)Dr@JM9PU`|Nw| zd+e}%ZuOzn2Uj0hy%+3hxO?@k)mvAOuimnH)9Q_@H>_T_dd=$9t78z|P+Glwm0u-S z@zo=%{#EDd!PR}Mdsp|Y!mD#D53M}7^1#ZyEBCD2y>i#ett-b@Zdti$<;Im8R<2vQ zX65RY@k)0^TDg3MUm-!nf+H*b73a#qm3=FFSN5#HD|7He@PqIJ@V)Ro@ZIoT@U8H1 z_!jsk_(u2!_&WF+_-c3zcVP*>9OhvX#^EEdA9lhA;eGI4cn=K2bI?N|?!g1lz0f_- z-OydotrC zm&s)u#DwrKJC_eG?_1uxyk{9+o?Cio>A|H3K!nJ9*6v!nb?x}tEo(Qe-MDtc+I4H! ztX;h}UhA$&YnQL_YvdZfc4W=J=3G0twr_3k+MYFdZ4Shqc+mcU{a*V$_PgzO*>ANU zx8GvF$$q2#2K#mPYwTCs$M&vWvR`iJ?W7&IAF=zF?q0fU>DHy=OSdfDv~=Ur4NKQ8 zU9)uc(s-%6BrRRO#4nLc_|lOj|B`d*;L^UOy-RzR;HA06hZY|M(J$^@yl3(5#k&@7 zT|B;c%i>LoH!j|=c-`VPAX~+FvAZZOUcSgLl8gA_kw;$Z%-`_H|Nr>EKa>)H78e$P z4<&TG2#)J`A&l$z1URPS1#ncy^I=TK^T2*5P3~Mctm9rdq~keoP{*_3BRZZ1|EZ2= z!q3w24EUKko(?}l$J5|H(Qyy_bR9S0fQ}omU&nRWr{fyz)zJ>4I#4jmWZr|LKlAJTCSeu{?B@8C;y{4IP?$NvjoqT_Gi zC+qm1@Wnd*8a|-of51=D@gew$I{pgYuj4P_eLDU-e36d7fG^bX-{2?c_;dII9e)O5 zwP<=DgwNCQU*U6g{3*Ow$A5v((eWqn**g9+e3p(shR@XTNAMXsJ^-Jt<3GWt>G(r< zkB&cpH+B3zyrJXw;B_7Ehu3tx54P)gFTASbci|NszXQWMejA2#{1$A}@tg3nj^BWn zbo@HJsN+5Gf{tGU5pT8o`6}p*HT(+nI~~6avf*gozXbhX9lr?uM#nEe|Ec5M(64p; zJoFzreh&2I8vnD`3e{{-}} zI^GWbRL74)|Dxl^pr7b?8}!dQ-U|I#$B#lk((xnUIikt`BlJ%?ei-_pjvs=4pyLOj z@9X#h=zBUIhwj(${m^|nz7ITwH2HskzN_PVq3`JU_t3X>d=K<39p4RoQ^#ANZ|L|g z=<7PZ6FkE-`M-m{rsF%Huj=@AP@>xJH$z|6@ok_)weL4UU)1q$p)ct8R`3MWe*YWj z^E$o-`kan$1|_Qf{w9!{NW;H|KCR=8(5G~KBlJlf-vFMU+TZ^Qx>Lv3LwD%-I#8n8 z?|%v1uH*lKKCa^p(8qNA3+Of-UkjeT8t>1ckLq|m^bs9j14>l;{ngNib$k`{Asw%S zKB(ih&uy(Wzc(dd@1zzI$i_4N5_{y@7D3pz>{C& zy%>6zjxU1VspAVliE6*U0D6axN1?atcr_?d?f0vox9RwNP@>xRE1|#D@p;f&b-V(U zsP_95dW((|=*>Egp*QI`0wt>beF#P!8V;a0>Zn3*(6J9nRQp|lUaw;hdYz74P@>xJ z9q9k)*al-3?RyLQ3mu!#Yju=CiE6($pzC$4L$A?M0wt>bUV~nxqX_X6}%9iI!vOWOD6LNC$rInbXO_+lL|gI;9d3w3-p zC{d07Z0M+tc`(}2zUM)S0tDl=D|O7k&(o2Buh20KPjyVe6CIQASjPlB(lHJXb;RL; zjxku(F$(u}#9&3o2;9>#40m-5!5tlga9hVCa7)KOg_}A)3zl_!Cfv~R8E{?4KY=A3 zpAOe_48Wp}ez>Zm53cCwh08jka7jlGT-4DGUv6N*z~}1df}dmHWd=T5M<<-u5u66s z5ghf_(E)QhJ{8XC2u?NY_!OAY5gc9C@gSVl5u8uf@yRf$BM8r=BZ$4Ej#YO_4l2}(x$-3dx&3!^$d z4U~-b-2qBQ!>59h(eMx`84aHTN=Cy=LCI)%5R{CDmw=Md@X4TLG`tv;jD`n5$!NG2 zl*|?e4D{=G4k#Jz_j5o=Y$2-S+2D?~?`MNM+(MU*XMsD?zMlmw-@?N>o(Vn8K!<@( z)$t5qxyCyKSiXgq>UcV^T>E}HuzU-jtmA3Ga_#$Rz;X@uKu2NO*$aOk%xs?r z=CEgial$)*Peq_Twr_#CrV3`2YheEP7BEwM#xlJ0WiT&mf*IHnm}|Wr%&I(#a|@qY zxM88P;GF->{9W_c&tE?O)VYU%S*HFW*ZLN#3kTZiOq2#mdEy`qM+!*}v#d02xyf>N zL}0OKk`%q15K7;LaKXR>ClTo5mQS=cBZ)Z|WdDuWvW$7+b2xVO)*%1S|ShejW z>Zm`Fi)%8dCW8fhLb9op8s2olN73=HL-5u^nK4FnVo{EzqrQ$i%Z%Bor>x0%G?{8B zjykYVA<#nynRX!6iT2B`)I=o6z8pclol=LGM))2otE?vD)?~cXb|)rflJ!uj>Zw-B zNi^RUvoX9PSA@EpjJiNZ1Dr&LbhV?&xHOp%U&=IC%q@l~IVXa&k(E-)KaPX*k8A^R z*Qv3G#!5|)vkj|hGJ1juPoMA6ZUJQ1tL11`_JoEg*8pjtWGspx@siIg<)Zd{Ml!B)rO;|&irA)z5vIu;)?`51loXY6 zc!)^2*i3r!@p6C4O097}UPPjihN@;Gf9g zu~H;e?$>)H2J+r|lHqQ(lOjX7F5}Q-n)TSghb5ha*U`rEB-U)^i-l@i5jy!H2QD9s zHbWzj{Z_69H9b$&WVrO0_Xz3iKoy)mS4JT_aUbpMH(O-8*RP05Ln*YWYNXm7>-BR; zlPSBSqi)JOhpv&=PugB}9!ht}T9GCE9J=svgxgkn*5lYkZ6isGGdUKvYfhZ29 zEhW!q8^g*_0(pHrYLVk3NwMg1jW9k2@;T}4?NUvqUZxY^9BiT%5EB)Am=2(2N@OUC zZ#u`RDdukv8zCVORT){AIjG4*N|8d`=jU;W$_%sFTuDl}n8~D=Ogck^7;Pd7$lxah ziV1S0*>;I0(K6^xEd^fGv|CWDsw z9^ZE2g{e0+KqD0&hwy^jZpFRDQY%W87(AcO6!1`Js{7o<`u!A1a7t(5BKjE*hILc* zV4TAzt$eNBb0rFCB#0G>(2)0ZNWJYG&}2jooya^@VY>NRMfUMxkB>wop=#2WSZAnY8abHM}`EpSYD%(r?VaTn@qkU{UT)eq@<_G>cEfZ*(sOgPGx z99*GSVnV)AYFMjygvlVhx1vkZRbU z4lBt~8J%*GNHx><42nIy54lK_X_5XoKCSvlN*<%`sMM3_pb%zpTFg3F)+@xpDrqML zQmB{p`ngb(>EL}DN2b|PAr+{V@nFMK_v9z4fL45d2S?D#V2WnBGS2m6+xw7fpPIO3pAO!=t@eISWXQ3 zMVFM$4|q1Y=z$KgmQXcgf|baiuVxt{^2H-As#`}J=`7_B)RNsnbf`Ee+q;l!&(>r})=^b*-ikMq zQPpXIiR2Q6TDO<3wLl_MWl#zFK#n*h+~`zBws#`eo~6mC<$^mqji=-(&rZkAL7~&~ z`4yKVN6@KYfp^G}s8X)W{`5F)`#a>?Gc}na8)GZ3TD@2B4t*fMCQ(niYQvPv+Z<4| z>S#H~>AY0RjhR%<_73FQGc=h#>ggzTUKnLFQnEMBOvZ6%8uhrxQ!yA2Lqu85l$ybD zJUkBC-i};*x+XKmTYaR5f}k10kl(|y@rlw#64Dgu(E}$bCu8m8DBOzlRhhEgj9hz~ zCSxIuk0!H6ld+JYN0ZsqWGp1#(PTC>84G!JG?{fx#zM**O=eA#v5*Z%ld)?u782cP zGOL=5g&a1T%!;nZe0my92G(ROWS-GvAWg9MNPZnv8{XBAU!tld+H~M3Wh5G8U46 zXfi`h#zKA%O=h6USV-NW$*7u)g{&N!Okb0+kZ?njQ8XC~ximDHo+e`vtk5gx!S{ic=?++(`Ym`P z{tECu?0nn(tA5+t>=%GFV&C%bmhV`8|K$%afh5PrbB zCumrVB`_b|$uw~?DatKMrW+kcrf&!?8-m>|cps1~fZ?Ak^l8+gC<(D2^b=H{tM-v0 zD>(yV*5~ih@|NIY%n)oNlWAIM>M(r`~jWm~8~2SRyf;@MLo6WZ5mj z`KKF#t!zFiItSilo6R(P0o6y>2YzR10Jc9@Q$3u)v(6@&BS5ZKL*suMg0eE9#b}J{ zby|X_JHql z!(9%!dck&pbow2+q=(~D@hy$Z@0tmEs-YH&OeQfwaJSvHbTJ#3S+ze_glV(v^How+ zVd_I!Ik+Xbbi@am-28!BW9W{gUo5MjIcA!URO^b7 zPHW(e zP)oN8Y4A6slaAHHDKuu(#{AhcL2lHa(A8ng8_2iNo)piG@Y$I9(gLsMX`~_ zJk^nmAWc_W1si`Q}L@`aE`50;l&VR%Z zEH@QKk`g^6TPt)&o)E!g@JL4>efe6EU_e@MZ_`z$EBU0+b1eMU5G-+HvE%KbY>4*P zB9$K2?y91aYL&cX#^Y-_T#h{FYZS**qs7g?aV8imcB_7{O*`0ZPOBp)(;iflYOLc; z%A-tW6hqx^P%q6)Y37d4HQIQwfbmRxlE@7wrBHtAaJo|!HOv%Kz9ADPqahTJ2pMOr z!x`P`GYy4#Md1U^KtXkc(NVP>ZMre9EOleq5S2#Byl1G0ggll@VWW>*xX%z2=(Idg zz$=kvuO+(TnVb|;BvMw!0imzvP^Fh=JDnh}hK&{nL53jT^t6$>C*H&drJPDty-2gK z^e0Sk=+69V0NYRUPX+Bu$WjM|wWI7=*8%MBoT{LO~e!o$u zY*L`Tg&I?shUt)-t1=Ov-#-oJ`l(=aQmo|Kk{c^c__k{Fs^=IAvxPkN{mj=NB6j?-*_5V0nbEJY`BsBCoqwpnEmCE#iK5*ZPaGzZ>k@g--; zJ&nkM$A@`>xnUI_HvMkJ7oSzeO@_u~Fws+6VlJ)iU9L_>qX84}@eO~h#TQbUNhOhr z1k-A-DjF?(arXSjTVq$=n}|<1mIN=T1e4XOD5aT1gm1F`bigQ`jgJ~CBWc;|t+a8@MYExJc{=DO-MmyN zBSOa|5$SH3b{0h}>*S5nSrQGw0ZK)=L9Guu`I4vRP{aASkc!3xDV-q43hN!?k`e=9 z80$vqYzz%S-aDlxP8rX1Kvw&9TO7Duqd1YAQn8W0UQZ{By@;fyA}#y5S)_7|qwkXpoLYV^Q8IHr?HzFF0l^*-~OG z1RS+iiJgc!cefoddXBX-jC%xa%Ncb}OP#KdWh<#+qei%*LZa();O%z8>tT{kCDS0# zSw-6a*$`AHIYd$orZy=HqoALsd%1QPk5_`xcsm`BIFh48KN90q%BT+ey|a6isr)e7 zCIyGz*YF@nrtG6Sxa?IW-tC%H7&4cud&?6wZL~T2*A0!LlMWQqeGV^?in^qyve-m75iMJRS_pqDvOp!;aOX?v4&tA zbL9pTt`qKgB-g|jKtnZW)h&-f_+75is%9`(EheXEwQY<5Ru3A2u^1QgM+f5+-ep8F z$h3UJT-@o(hl@E*h)Ri4Q&yF!1H|#&Y5ssGYCiU2n@Vi6Z?GF)p?x2CF!g zPK!xphSJ2}_kqa3O(ALABVs7@*HhuJ6c3XvrpY7_q+0i-$3i9+2vw`~ildq@w*x+| zhSiN0w{*x5^!3MHhI5p|bd{&LW+=n;QECW=w7Ib0Af3eu>kLY+ghMa}0P_RGQZG?a zl8$Ob;8h_LrMir})G6kDE;fdAN`om@rPJP2co>cv)o6^WB$Q5+d60>|lW@hVIFjVE z9#<$D54&=u8l{q{S~iG#V~%clX6Z8wOFiWf+M)P%j}Q8*MXnyHl-rqpxL@z`Q)hu0Q z+FTi9O0#Lz^9@1g$Oo3_itY^CAzDha7@>PLS_ug(mztKbTvN=_jj}=$BSR26(-3qx zn^?$M?hn8{QGxWdY~i6_x$X6PV3L42cW zRze$9G8Y;I9c8fi*62@S-ENp1RmQWaBRm^9j%8O4BO29o6qP_m@l2|e>te}#LyVF#-^mPvnL0I63rMb9BciQzepDR!sXz=dJu!pQnFs?dH|{*LrJy`|s>` ztajo1?60(+zv{Cyi@#a?_~OeJsl~ku-(C9Kg|{xW7CiI+xzvVlT0VE_Uhqc23wB6q zZxm?gJllO>M?lBsUH-o-pIE+q`Q^*m6HNIZ{&}993q1VA%71n(xYVBOTTyfpxLY+y z1@mM+2|T;sbVw_SvzhB#;Umv?H^*8s&*Pp zqUfB4a9=wgN)4M}lER=t(d*@zX(vB>puWuz>}8!?QS8$^B7ltbytvDIrBfT8VUGJc#G=Ji>wlHEox|N9qM9n3|2!A!kSo_l&{+lF{2(*mPj@ zHpak+0uuu&kV|Cyt&!g$k)%RVJ(9)4Vb755iKUja(GZ!K+ubob#)S>z9&7nYr=gD8 zwN!+nl|mxuaggz%ix&rdw-P42f-7Cf zj6JS!5glQz5*}zK!oh&Ym>JnFGX$%okV_9zZoKa#eI27E!- zTG%iaHHIlmXBmRUTznkKW+jh1gk`&2%Q@itSiBUhmFZxJMoO6;cn=g(d(x~rGKOHj zJqp*wj5k7H(}L=82-C3Gr2^hE9_=ZyX0JX@MDbJxoy|Akw;O`-6v`4|Ispcxak0sA z%6QzWkYu?fNB99PN@}H<4*LS&#no1I*zYm~6*-s~PqUF`oDYb6%q_KXF*!sFZU@2V z6K=mR=b6%?K(!3PMWgelqy|%&sL@8l-9n=ktX@thD;Vo(b6WOT+@mUP@B|;XshCk6 z>&EzoQuC;<&}d?Vspu4^1z$Rn%*0w!+Q)>44H_fKe7!lEL_ExFoO-@tYQGtAs16|@ zS5l7lC>Zq@i;Blzqy5og+gs$gIyvfQ+~Wjz-J#EL*8gQoFdL^8SG-5@Og|Q7LLF+# zwJ|K_Al*W&JB;D+T*u|A)GPVf>z}1x%>>=S0y>Ozr$d=-j6135sL-A!Ftv!ryEVTX z&(K09Ud*pN}7ntl|(c(HcVYO z%@7PKPPFV8Op>J->hgs%Q;cdimm?8(CBZ4u(S)U56Wf674fy>bvf-I zz)qWJ!t3t)x;@du2PVM=Yuw}f#fG3S3r4Uhg>sNo5yO2V662AGqcKSh`+gyo3y|F` z(~GwYli5qPYYah;Hx*3PI?cpr>%k#iQ)4c}RQu^$!$xWlkAVS7A(qRzW2FowmAJk$mJCk8|Qo(*b)X7EZoRjhThY*ma#8H)7x1|7LvPt{X{ zj4F8;mGg(G3P=Z{j{HgRCU2|7*^K(9`k1%eYzK$sc9U_T;C%{}s^!M{Fvt+{zR0u` z#a)?F%U7E=E3Q@=GaenQi(4AAVS)p@gBY$gbrt(gkHhVj>j_d!mfZuXTCXeR=D?T1 z`#qOY{~JdQQxiG49LbFw85um8Iu#`V;vrAQb$79maikKpbcI&al0%YMqdHb*)gdTI zn60^Gy4z_6s>S-S*lb2|dCPe=6O=;rdJ65zhTxjh&`5WiE;i%B$Bu$;s@AEF zh?b*+bfn~ohl-d_Eqm}E1A`vVmy8(-%2! zAYPq<%~Z`2c%!ZN$My#eK~gHI1`w0$?3S9pK(}w=&8W zVxqte^Cdr6J97;LO0L%pjmuTT)O30@Y=)CAa^kDGRFtX2N<_!sjRuLTiy(AZ5^&;PMee zFi}?m;ba%g9*JO`9o2(OKHPBdSuRz>+)5Icx;cUc8KVb=#<_G5^j zBPr;MrvkWN@eixBmNP4ysFMt3+Nnsd*+Cq+W~7ouYN}&EIo!2+(NPopL|Q^3jnS+J znC}@H!x8YNF@S(A$12~#>QosI^K8h~KxHu!DY%`<0^zPZe1*vDo%6pLg8taV=?K?- zY$nqPCt69+5qgTHNOlU|nU$+FR!oc&6EX^_ajR74KWGSo@b~F)hiLbzXk_S*;teD+ zZVo*>XrF#J>6XdFfcK$e_v}sX{6jOr+92LgsT5enM9YM?KIE~CJKzxOY|sS;_#6`= z;(iiL5)6$C?==Knc-UQv23e|!=89#anIUnfKdFqoX;+hORB8xG6v5=8RhpIR3YZVx zd=ln_{{xr!WBkAWfkOTtR`C+(+WCjWt610(^LWFncu$z;$bD+&IkK=b=npTv%I^nK z0ijcVFXqZt?vhFsoCW|fv-B8YCojB;+vhuB$}xU|@L(-ojM(A8A zLooR=Z9M}aO8-{D9f{tmou17k$cJ5(U zw9uR$w=147(UJSqOmt*n!`-RsijU58;cAklGY9%^}AHwtbNnqlU zkwX)Y+D~Q&Vymvk2yv1K;dPYhQGQ=3LQ~`uT8X)X>~w{eaR9PujTk8b4d2*x{A=NH~eFt&(*66yM8KmiT)u8s~5#Y zzpLb%>HH+fx0)4&JzNcU>%Ojn<04l|9tt7>aAc87H=Ol|I*euybsD+D*;=qsMGpI= zI>UjL3vVOFRMceNWc)wv)`2T(^Zx^LVEzBqn+HG!Kz#ihAP3-YL2R}s+TUxBf|z-Y zmEWu!TiFM{2gJtuDkOnyfUmG!2qM6Rz$tzaL`{1c$O-t)g(Dy??QLYPGFCO`G|ox3UB7L*%35EoJ+y zoLJns`*|#7s8c>VnrX9e)^T^*JOTOWaHh?|Dal=G^UFsEGi?^mSME}qPd=*7v{^Wr zxl3(c`DlNp&BB?^U1~$+qsmO1h0~zB)aH?o_Ga2FoFm<(Hn)7VJJV+21nMrex#Xjr znKqyKv#Yz*2KtirOq+#Mue;O+`jXa6n}zeTyVK?deMxhs&B96BU1|e;i9FM0;f(Gs zwSm5*G1F$@bnh;;fxe_Z(`MmZ@GiB1zC@a7vv8t#m)byIQk!YBa29!&+CX0-&a_!L z#k@;xpf9P;v{^X+yi0AMFR9G5SvWbpOKqSpDbKW7ICH%_Z7$H4lxEs2oYvl@Hqe(8 zXWGC~s!C@(rNJSpLB6imYULp^bmFB79dh)jJex{}8x$rWlYy{HZJ;l?e5TF93GrQO z1AR$hrp?0H@?B~JeaUlY+AN$(-=#Ltmpo^t&BA&1U1|e;$z?Nb7EZ$NQXA+?o;}lM z;SBvQwSm4QKhtL6^!+ZifxbkTX|r%{e|Opt(3kKtZ6|eo^Reb{2cDk~ZygRcJAsTV%%);U#Fun|po~E|g`bG}t7- z=2KM5^0mwL|LP$QsjwionXe`GvA`rDKZ#TGNsX^GVUH?BsGd*O_M7f+O2Q7;^M=uXrVbhZddf2XwlK_ zIVBezEsXC^RabOA+7-2j*vVUT^nmnwCt7s$H^C{csu&DWVk_n1I6O zyuW`K6Y#Q%O;T8|PSbL|7oZLujz&p;sTYs5U5D^4eQ4N;13pYaG~*pLu=BgUG|lnQmqpf65ym_g>&^A5fx9!(UPZgxQiT8V|<7Rlv{_x13F}G zfa(Fa4qS27e&ZZ7ch4Niqeg7j zc;ViK;~;b0T_9847-Xh9Xq$tMtl!k^l#5NN(^sGF_dA_yPrqc5TG%f)8tk_7`j7mc z?u`2N4ozj_iE3@z=79ey|KIWgCa>o=$@7`ydAE3@M6INDY9KILPbjgxC~dnB+dEA1 z-foh2vq|3DO!98p;we&9%(crMOp(KCd)S|ByN~5R+u}6_)o>DT2}PkQf)MePu#IQ? zf}xim%P}e{7poxn7(*qLZM>zAo8*1W;O$h-?=;E#yDgs9F8|whg_`7fO!C|YZ>RFR z+a&MvCV8JT$@{EH-e*kmK5dfsDU-ZUn&jPOl6R*`-W?`+UpC46l1bhdw|JwS{KzQ3 z9eGCi?Z`9AZ%3X{emnAv^4pR336uAGyHS2S`R|{Yyq?icckKUOymh@Dc`q`_d!b3* z3rzBkn&e$=l6RF!-t)J3nO2yY)az5cCx#OhshQo@->+s(@+gx$(j+fql1G^2rA_iu zCV5Gdyo57$%e=*7XiAml+o86i#>+Q&U z$RzJqCV9U!$@_PcykD5){hLYN&$oDT4a0J5V$!Y+m|n2MMYfgS+-pqoUTu>1DwDkH zO!7cRFKu4D<9!@kUDf9Q_s^{kRuiiit^8u;lPhmn>4O>n6X2i2cP_qqv9|b(#nYB= zTYl}b405}lwe;hKA1-`o;kt$D!qXQv=f6Myf%#+e<$3@7`rQ4C{|LVx?!i%z{rN!< z?e8x^Hp~cguI;C`kJ~?Mf78-Om#$x`FFkANjKv2wzp{Dr=2e@_=A|3I-T2bRO&eEk zWH&BZ|IPXr*5A5*#X7lu@!GG~KEL)BkfXk33)#+D{>j>8jaYk<{h`%gu6}y;M)*!m zlR4je$`ZA(uwRPx$ri_#I^%p^%{2St?Ndto$L$|COZ}MrV`izh*>5vTz14oJS?Wja zA2mz;i2WmGssCvIN3+xq+dpiU`XT#=%u+vS|Dajw2kaj(OFeEsZkGCf`}+-QbX2cZ zguFZw2zpd)X7O!3fSt9oW~q#wF-xWGv{`D_o;9dDHtpN(w;NP?8tT`T3d!MWu2HP< z?QOTU_}azSnx+2v;-8zPUcY#~S?X&RUt^Z~>cv-^rM_zMRR(p(iWTjmS!&*%H%k@l zf>|nW=gm?%J7<=fv*!$IwZ@W>d>F%8A*oL!65A(H_N(kynWaA8{(Q64EA3aBr9RL8 zJhRj*>{l36B0QROo2*X!% z!wgPUibZOh!`NSBf00@03+*p7OMQX;1!k#7?MLV4<`>M3^ZvK8whBwyv3vXK##hZ! zzq0WagIa3{qtU3rC5YNcnP$|^wl=UREY6-=J5u?@*>h`0Dz`X$ZtY0TEzX`>`2k2V z(~L%nU645|GZE|CY_&~oQ{%RFq)u!Tv(&L|Y?eB*jm%PqwxL<-z&0?b0!bIsWUQZR z4+Xk4lD02r+MG70S?XciVYAeOwu5G=m)I^bOMSBK$!4h++b%XsJzzUvP(e=VATvse zgK1moG4;t{TXR{yZ22;?)Mqb0+blJ|JR1}4Xn?Rh^Q|4J{PN7VcBFF4GvC^Au>Ti} zf3dpC%$+y)vbpuv+K<+%^Bap7Ed#)v(4vi{Kv*%;V&1?UVPKs%U14&G3Ykv znYItizuJZ_zkAuSbTfz)@W5Kde(%ZXd5_K=U9eTF6by2Pg1l#)43;ZZJM6$e=^=8N z9pRZim*7QbfJEcbY%zgOsFKflph9Qj0lzv4(@eOT#vFnmC&nOHDiI2k9LNMh;gn2y zL6FFJc|Rc*Ni3FZwAl#D6o=DJb6bm8%spZQR>rcjKR(EL>g{P^fR(~ojEh%QrRk3N z!p<71`h(>pTXzcwuuiNIt(JKyR^@7nv+HUEx{<73lvsvoq@_5XY1gJ8rhcv5-%pe} z(P%E!P2fy*z=DHk>1{UHu6o3VJlAQ0#67hjR~FD3*Un*NB{5FbLvqE3#l;c`E8i=o z%N(CRFqlSNE!vf#21E)=4HIeJOUfXTfS4(gH6L4Yy878jj>IZbaR0Q!P(dj+YDFR} z7EY?a7cv2Q^CLE722NFQ6R~Vm5%>}sQX`F2-Es^y4) zQzjx^qUyDp?C2(QV@FN~Tf5UCUu_Z#)JJ37+Z~iV`^Qpg*h@sjahmSu$=%$@t5iNvjw!>jH<%&tYS7~pNmtzwtKh9PPOx-%RFNTe8RMz8~PEm6k(sFEz;X#&J$ZG-~xcA#BTg>EJZqLBwfUiYY}4ADez zf2K!vbItImm8=BW=1Ae=+iY0KAD!3xhxr41c@$)(L&}GbGwEngl!ruBtW-K&z*Q

    dOWN0u-mA2WCe54r411Z@Z)>XU`#KLMS>K){;nmjImC9#s6YMKG1CxdQzBc);ra5ou@`gznjVC zc`hZYl7xjDjiN6SDfMCb-jKx>(4S`n8~%p8c25K$ERqKOoie#dL+506*N!n zue9^I37tsA2$fCt=^D>%vti-qkJyl}bJ;?m=$hbBEEpyFDudz8JY8glbWa*(iFoV( zvG*omj#Ss#a4*_dj4{R-;{mrZrV*`@N*l%mZ6&FsDwX!d7*X1{(pFMwF$Simr>Cc< zmznOKF~JVOgb+;p6T*iH;Ukz2V}daugb+gbh(jC)FCm0Dgoj{%A-Be7kH$(zPOTXBthukf8s^$tJsM+QTTToQ*q8WR@(I6^SJy#=w20vY9IiFSE zXlpejdtRB#MX6*cj{EUG>Byxi|CVtO-1RFQoIQ^_Si)+lu!C^t{Z-0fGL%}$=%Cth zvXMYq>#2k&CmYX9ndwxW(N%iEM2Gc4-X9kmXaoinmFva1xIWbIw(x8^hjLY`6)v_N z>mrZ(9S-46IbF&(vPe0erLEdt*H>yZY}p>G!SzMd733mxy9GsoFm{S;CEQInQSP*2 zTD(y;H8lRtz)rUk5gk-wbC+R;^A9Lf+Z?r?2UWG8PK%Q(*+@qgw|BhZE#ol$H}AM+ zu%tyOg2;JEs0>SGF{pu8X+X&li^G62(Q?P2%hs6ze82~r^g=xjj&ozycn!(WtR>Qm z#h5#&yScB7lf(1pm>*U7ctae z?np=D83z#z6ri{s({j~IP_eYI4Gy=w!y)MHdkhKCUB66C_d5QHV_?yC4K!0uu!Rm3 zgdou7pjB}eB2&Fi$Esz}O3BXV6KN<{>mgh-iL@(-X7ZV~-WfLIjeao-#k$b*f*~K* zv3IOZZy?a}+IwVkiz=iB_-6f~6R{^e)yJ>N2)tvI_IknRGFAyg-IcR&Whx$W*t7n! zEva)sg_{J@LhGd{67jXrDLlXh3Ocq?=ms1`ilb7^oGsO3IRK7^SuC^Aj zR^SDqx0x(urb2Znqzo6YiB&}mlG{7*UH+;nMNYc7>bp8EbhT)%uEUbdC(bM9S#VR*(W`Jxs)zMr zTeos!zp}M=$K&>*rJ#f2u=S+f)+KE|ou!a~$}F99!EQ18%`K}l8RV?!RFJ0Y17qLg zMc59a?HLPHtkO+toh?l!ST@A!wXng$`k-v6F>@YkjbRNg6rr5Gq%WF*m}AR0$alUo z4nDmnXR2v32@Wf|oo0>Bk>X-)4tIJ&1~*;O>s?k1is1%gk*OpU{bUOkAC8wY?V{Nl zM+YdT)93QZV7C(tYX;ePEkrSfY&CISJVSeXNtWW$$)eTe^=5lpIEbHkXU+0KoOR`=+$c9U*UaGCqQIHt=)tc}bRJWkziP2cnCSu0oX`igEG_JQ7N z536k9aK<~sfV*}5GSO=Ejru~CWvP&jY3j*fS63jZT!CpCw5+CHkJbu-sfs@B=s<-Q zkBib(@V1w*W<3VNRYpT?ecqP0wb)Q!7Yv1cOyE3wsOcn|18Z(TCu^=sJH0i!kbeKv z`~P2)Irdk?fcO8>!_TWuXg-?%`q8oJsF{w@lQYxt2-$T$c+ace`;ZErPHla2rF(a# zWBrNEbZjvrM}7aVEl@d~FO-O~M$5EUmz`n!1A7l8SR-WV&xdF7t)SnKh!OqJ@jp7Y zo3%AO5}!uxbwdzJUq&0{W~1ScSUXUDG9Jg1G1lx(kPfVaYD2n?k3wudeNykEbC?|} zw$+lpMr_7F`-4SSCgSB#CzsY)fe!y7tyAOQUgok;HQ`I=(}i5B&B8w-29s8c>JSqz z`tYTru7K?Ch`8~wd* z%f;B@_E_AQ@tG6dBG)pvxDvy7nx<;79`g>Xg&fbLV`F8ekoHXDxED%r#lsD&wTam? zg`kDW+XJ3TNAGXe%eX$-4-vVHJyO==Mpv{=IVvVDm!%EXrrr{ys3w#MPgV=jOtKt+ zYH{0pbj*uBbVc!DR~*5d-nT27kNW7?d{RC-9w9H=2hSDXvjA>pXL#>g`KynP_55@F z6Zh!YWV9SL6-T^f4_RqbAFJ6TGt&dF3ki86M#>v-R%YrPNG|7L)E0$@mq}fV!Xve&Q%}x#Yne{FWP}j8oV~Y1{T(kr#dFilW1=ID$F7Z&x%O)fG)Ar7Mn*-0Oqqitly0 zBW+Oi?@Gm?M^30K8ck+?$9u&5|2Rs7vFV_{5_N{KhTVnC=ozn{>Bk39)N)2!Z924g z4yjlomilpL#c3egL6;t+P4-gKTgv&lOr+h7vI%Y4gLci?CAg4-FNMDPH3hhGm09Iy3?_jT_Jmd z$x`rDo{rVY44tIlng{L6F_D>s+iu4jj_@E-WgE`po?h)WB3-mL40emLQr2><=}6cW z=@^I%JXXzxow0znk61&+UNnM6BXzGn5zW%E9+VWx^igjn+>gNqiH;2-4m|GDMQqkm z*%7l@w-f(wiay-wC=R>g25iN7R>;N(3+9x^P3vv(L>8r>0y2{6!Idb? z+9qI4bcHF4A(~W2$7Ire zDy@YIzG3aACvEPwf0f_}DZ@W_uJ~T3JJL?a^6s6^3C(n5*8dmrza`x^mYce+kHr(# zoIj8^#`}Xp-o{4j&S_VP8+c6B8CxB$2YZg&6&WNMq4Wh(7cTW86_RQu@QmGQ_eFxv z?sUx6@|cS5e4*CEBWAi^jmIlA)l0xdpd@_iE0F~*S9CQ(P$iHU&~7vz3eaek-riB% zrszXglpl7*5zOg*yW&x+;-V*KRooHs>3{HCaqFt6+dQ3luYIWF6hq09-)ZHJKmVVp+MMpg^^az* zLO6{Mw`D-O{ic%1n=PPqOCHkR`rMI!%*?kQVQm zXIyTsNY(3!ax`IgY;RTMMIXAN?6512U{3Gb6^~jK7d<(v;*Rif?}O)x?{&H(t%|01 zuZkzMDy|vPZYC5wqAMayaz-C%1Y11=8Y89tp73UM~V<;u}y5pO_w^?12f&4;q71e1vx{dUBKMafE_1Sf3pOH))tlWRqd zS)~f;k=ANRHu(NBPnLW^}}>I3IXdpxK+9l;*827c>w#GMWqppw%? zgA1RbiR!@MqtlLpt6)SBs_N0=h3R-Z*|zv=w9Rn*Upm4_z}51$8`U=6E>9cNBx)+B zTZos7_kDDb<=WYBjVMf4^s$=PYr<_*VZgNw0bQ)^MQB864lfcypc}YVL`K~lc;J*Q^?Hf z91*Hr_2UH{;%+$d3_@Gblhqa9T@?pCk>CGyIg-xf z$#|T#!u@WHn?XHv&dRc6ztuYaQ}L41Z`3CprkEY73Cwi#u?i7qshYEfF(zlc>Vb1O z+jQEO&L^2@We_&k9Bq5TUL4Soa??|(+Z{zWiw!(TA=` z9CpPK%;|l*;!&#NfGZyHD$WPq72$Jqx6wy(Sd{Bou|hW9aTikVYJAW}G+uosq2rpq zlG#zz)Ve3DE55rb4tgTL|ARehb$o|MTooBpozU9`s0&9dWq1&w4+nzz8m?t6NWNa~ z6=<4e^r%+XpN<^&skqmrFI4qDtD)2J#Z1oZ0IGeotZ8qRLC9KuIuJ6q!*+kr=i#*R zWS{Ev8x{u{Zui3hr`1+5CtMYatr;yRGDf!6VVz+=88L6`D$Y>!Q}O@11Lt3pe}nk{ zLx-o=6I%ayOVGjZDBp?yb*7`7ZirT*K@7I$ z7Ti|QVK5yd{@?ng144aJ*LzO|e)UVo3Ek-+W~0Gy)JjJ=J3{-i&K%>aapm4j-d77b z5_zuP=-9$mPrzUA_$_&R$YW~`k0_4k(y_NB z-07Tl*cC@Gr}zD(<0$bzVnI*Nn~o!tdOrzVar1V@Fl9kS=>_Y+Q6 z&XKKI&_ton=v`S$wp6GZYSUD9rj*58ZA0Fzrz^+3(_x8(xfew_&ZcjNqd7yK$$3lG zip$+~;v7Ec1#1RZrR?4-5JBz=Z9m8{o>cX_sVD*k_IReZ10!J_z{rE*LBe?qI`nhi!hf1%)rK}RxLWV|K4 z*=uzN>5h#ctVJ&iIa^{;q!dQ*g3qUA28KWu-j^M>D|V6*(oYq+K$)Yai_IqD3s2{$ za1zqJ_!}K9M$FLOL@kreB#SeIy`VKE3<*4JGi$w&9HQgOPkX3BZBXqqA)DQtX6bA( z+_i1%RotfNLswKCcEu6Q>3zH6QR07a#gnrt?g*EAKX|L+_FVDZ@xT6rR>d`=7RyoZ z|K|htY}*-ZP`a9#HtBu-Tr(QiIU2cv(?;isaHG+}YWgyR*NNkHMN_!a4tnE|dP47s zb{f;YQWO$Ud9@~UBr{zJ1nnFiaOF@UXLZ}Xq$|nA>IE(u3(XLICsEWD=}er&xO9Rf zN+p-W+^lD;+rFR*qQ4#gpF8%*nEG1PFI5Am%f~5RQ?%r-%4@QhWo4)Un3w!Wl7C)X#D^4pX-*tEaX!MPSxFP$c+qw-IUK#$hfU# zw1g)6?Svs~GHYUFsMQ%5g0YM{)~rnVZF-ZVkqrCeDDEOlcr??GYfw)*Oe8@UQCmFK zhh#xW1X3j(``Gcir`1wb#SUtC^wz{C}PDu9S>vH;u*^9)S2SmzNPBS`B|l87`*W++4px52%^d^46~5YA?yI}wx!wWIaGb{O=B z3`Xd6N?Y>}&_K+_dc#;ipVny#v9#SCEQFk~ssWGT1cT;~22N0%bvq1tLk1)CIyJ&6 z^L7|?hYUvOb!vo*65CnK+{w!@$^WYEr}^udA0R~f`3gPyhniPuc#mQE8T zLq2ONuBkY%R;E-j6Fp*zj<_OOU7^%W899?TQzA=V9ptOi8LOQ@sI6hNJtA35M|)vk z;n>@d>z3O?1|#%3#Tw9hs}xUkgZ`qo+4J{%9xY)&^}RvfYRTKkOvRtBT6-RQ&$=B3 z?2y3-y-xMq42v`kti{dQ%xEOy>9=YKUK}9JP)AEuysnzJYiAs6yAs$AgVvD22)#~a zaJ#8DU@VxuYY66<442lVoUF+l$i^dO#;V7>2$`#;OC*7Ahe315V1!<$TFGq7T0nXf zF`$S-rq?i|b{j{SgGsI5L*y7orx?^&B26sk+zx}rkiiJOPWiZ+KVxtCqi!~U_8G?2 zvNMr%xM7PR)eMAH9ly36%e5hc5qh1{hl~Sv-K@{n@wTn!bET^Zrf7?LxHfGy zB|N6Ixyfq!nOLk--wp$2$Y6wCr(#sGMAs{9pXew287GmY%NewS7EJI4h^74YY`s*7 zn!51SCbJy|)gglsdYv*AV&--ni?<>rP2Y%>IIFI$V_0v*4VNHdkl!@b_V#V{P|4KY z4ui^&!3e!h6}!C(WyP#)J?Up1mQX)RU_QIsk_(l>Op~qV;bw-ONI5ddw!E=CWH3Un zQzo`m%M^9A*Jv~&iB1uYWV`-W(NrbVwoHH-Qo&#Nbp9n=#v=F&B5mF+Mn4jGKl>l9%j4cT7WZY-u; zF&6UEl!_TmCDGS6+ifD$?dM&+Fl#Y3;;n7@LSe{YgkGm;V}(o*bxW~F_4^fT2GP+~ zoQhTRL604+8{An3)h{{FfCt&ue2^b97@^mxphs^_88}B{z*QpUUfqXvQ?&sOIRg!z zM!~Fi`$Gx8hYm8_wpQodkiiJOPSrH}4#D(sjB+HbF8IjqvPVgG%vSdpEhOTFCmK<2 zxPWAGj%}^Z*&%}wdY!UIH1>MH%J!X>pf#H4n-iEO-|qWe&U`manDVY5?e@@(lrczb z#}YF`1|#%3bwa--UbmbcG8m!PsT2AM<+^2h$Y6wCr%vd}@pa2r4;hTm>(mK7Fyr6< zpEb5;O#KDby{ZpEM*lxmoG<^f>^YfUx+3|h#3BA|(VL=(@GhZTP?-GAS-JpMQ1 zGtl6~{@FRTs#lMV=Vk(9V^YbuQ20fOV3g48?HkwSda0Ibb*ee`*inLDoLPSI&tK#5 zsTa0;$$IAnt2&;i{vDo^qXcko^K8_+xmKzQ4_vF;U^>ZjeBv@5|E#rbraM*iY2fMo&fZPjKLW`Mwva?e3GzoJxjBZn$as>ss7E0A z=`AGnqnrbbN>b0$yIHh}$`MFTZ6T=}<)~m(k~*H=Z*6SijU$l!)E1J+D5nXdl0EznwE5lDV=3rWo=CmW-Z)bRAyKDmjX9f9PzTS#6!%HhYTBroRa z?f>E?7DgcXi7h0jM>!uEmE<%}Z+UawS-yM(lILt8dC@3GD5H|Rh$p#kb1$^-itUm- zmH+R=9%OtFdV~A_A31!zYJ^kPV}~sN_5FYIQ8OJGPR>k6Bb*PPtkBwv`9`({OQS2EERJ@--15KRlQ^j!9!RQ?ggV)?mH=W+NtvBQIIg&a5@n<>; zRk>tYKjRLNNX`?Q*5$Z>Kih4ZGI3AY7I)YRLBd~QW-3_BST0wq4VQPIbr8it-4H4a z>h_9XS5KCG5mz)xbz_Y@N9i);HZL9Xq7QF6o^jX}M=+=N?TSaubYwVUDvl5QQ=Ab# zlzi}9@jds8H(zJG_x|I%p5mO?Oh;xt!e5;{;-zCtFyoAwVnm4X#GL-T+Zm4y`Vr1l zWI~~7U({jFn`VOc0oscmzbi7+NZN&T=~lfM>_$i`9WE6Jd#!HQ1rzbSDX4P>lL@oV zWU6_?HEktNmP;+W*V;4n(Iy*@8E9SIUGt)Y$A*9=8Do$SXp(}p)uq%#W zPVd_lk6IPSaKuy`A9z=w&8Hc@Sc zTM4p-6ESDmR!;bhjz*x+Y@32jB7>tOmgre(NF!T|q&c_U-*y@XOdmn&jqN2J-xPi5 zil-lT#SzTu{ko#*s8w;$Bc|f`z`NoIpUzJLSKNG^aST^Hp;d9rChgHTs7;rrV9A=S zk$$xvps^AgO!pv3ZPVN~BKBr37Om+@RLg2(^MQxE_u%1oS^N6Q| z1O|m%JRbH&Th&I#*~z;43{eUBD1ElzGv)OIlanK|eqRuZ1bYG&$_BM@$zE0KEcd$Y zuw#2qad^?e6=jpNW1g``$J9@%zcz9IM0EV;>ayCb7O9?5-J)u!E>|g(&rhDKyj^gO zl2bay9#x*Mcu{eu;u-}$ev#s=NlN~b{2ux2#LIF@elENi_@85M$o9%&vQJC@ZTw2< z|C(5ku1dcsy;$<5l6H{EP8ZNt?wOBIiikFIQ#!*E4Wy`?Ysz^?>p z%{!5HL*s~{rLe9Zk968qr;o+HAz`e`VYJ0%g}P78sy!k9yWww?VlAls zI|$)kc!g3AhqXngUQ;wzFk92vZ}|pQS1n_-pgjha&kcVgS=Lz{i3$>Om+G2Q&(ZZ* zO1VdYAyDbF<`X^t0^j9 z=%fC8e$Xci^2qQv5+ScAAFwp~o;=rW1bk_>-!ed{`CQc#V%)w?k||d+n65-R<)0b; zhRNtmQW~-mLP8mf35%ijY>LtNbVZA`8`U`n28-W@+lwu~NB;5QZ!pe5y_oJ>2wlAy z(C0h)9P1h+UCls4j0vM zR zVT(c(iEBGeq~k7C9Km3QVk13@w)?Sem!S<@s>T^8uNCXn3Ds@G7A0q-t4o(W))I;o z2Z?+oU1J6&Yb71g=lpqlCr}Qwd2>c96S858qNi#@ZN)%`H0W&_FY0IPjJ;;kXc-o( zP*Gjq2Ib$Q&H`Z;ye&r|T8U*d=@!G*^4*@7Nd_!T&BJA&B(B-WUp_XKi)p(fk@JWz zds_|qK+dA_JAi(H0d-CQNn*op<*VJ%hhSWO~|UJBe#Ck4H1mI16md zD*n{4MVfJ^T&y|N=U8*BpRd}=71}}2H7Nd8#iF^ArRB0UUG7}iBlzOmEy|D-F2s2W zleXb=8A|?ONRx=5ol>X~a3+xe)1;|D+1oCA#(rnmB1INyn*|H7Vc6%U0w%~cibrg< zc)V4%YMXs}5J$rGRzDl9OX9;8$!wsLCSxXtZIE+Oy>_+kv&Tt18_*MJO+S$D#|BtS zgMiW}rEj+|1{?;v-jS`>HIYUqZJ>)4V=57Aw6dN`lOQdLe2PJ$w6h?*f7l}4t)ex& z+HZF}R!6r;BSNNDeif4e8sVMD5d<}i0VT^U~%;Yu0-w1?g zSGY%07PgCK@f1S@B7t18sYeSWr_rL;lqc0nWRmqvSJEA}@S7d}7HZOwUD_LRIBR+) z#sz|i!AY8XT(sION5k!A2RC@S!qN_t7y`MzmXiY@;CxDn>jD- z4U%3GG2rOLSB5Q!Vu$u2eYZBCF=%ZOL&DfL*!p=$Rc0$t(XQSY%%yGKRJm3VJu__K zwRWLOau6$ERH)`Bvt>;Up|g!p6{!^s{-6iPkw8Wti+R|Ik83A$<5OKvm(}V5G|W#a zlIm66WYFl>G?8*+;P6=o7-+@TEO?-DwpKj+36G90>f4!`ON%B-O*UG@TFx$>&XaCy zmLTXt+y+iv8Mu0xiEj^ExFcDk4Raw5d#OWcnNAk#+P$GL-Ud7NS4l`w+Q6}PnG0ux zQ^OXnT(V7sog_kPJhh%1_n9lXR+53d3W0XCpzkAShth{LSk^53`mhBof=M4&&1#BC zT``@uWi^>%(amLw`flCQ=Zg6t%5-sWM3)hf|6dC%iZ#FjBbhpCa5?+Anzd;%>X1-1 zWz~5Hwqi2g>6LYng!t08TX^9|+paF%H8e8LgvXFDkp2!{wgsHDJsPDl6&lj%#%uNZ zcx~9iQ9xrQIG&}lIjfPd!4w`Sc}-eVuRcfwY$3|f4|7@$$C%7`b*K)z2Fbv4rMNZe zBwCq>&5>{=Ob8xHVu`ppMr5%trb+l<3bwvIPeZ?PV-T@raXi&6(1;Juk(l4>iRr6c znBbZf6Wy%F(4xN}dSyt;k~7p|eOI~U>DaYhB5K8BSSl8__u@>`NmmS}GG#S#3~TF( z|8UsC6tA)occfQI73wuFRdqJ?oebWn8(6MoZPiK^x@5MqkjA(oyJ6VE=yk+g{=T7} zL##x|rbnDkP0V7XH9Cwjc;IzGvyPf=I<%LNd}G+cz(T48S27e1nriu&hU=B7x)CAj zWoxY&H{3T!aKrh!>Mn+un5jVIJi=zRl6po+Kfyjz>mb7gi=V<&!h*h1qkH5^$+ z(}`9IkIr1`RJPU{g@$N9S!s38!H2b_+|~)3PwlbTsQ4aGGW~6zeA= zX~@)^3(@Id*At=8SSa92MaHk#Y{3oMHOw*ywwh@thwDwPES~S=n`MW|U?lKdk-(E| z-4~?GE~jAEutkrv)sUJ&6YCSEPSK>zy6V^<)h=-zwuSctX{H#Y2D%QB3&>i-7Tp+G z3F=wa;;+O#HfO6IF?q8Vj%EladUNMm>`Q$(S`@_Sp1nt(2#1_O!1`CSwWqW?+<^Y z+RV9(g=QE|mM#>46ZdEA6%JY|?U{l#+A)+smfjzMc4`>SD#5~ZoLxp25jBb_2! z>O_qVzm<%&n-+(!V5}6~M#7ur+zF3WI*g*YlG{hM6a4~Ltvh3tzyM0lo0D%#2!BMmZt(41wb}DY2#^T8~lEy(lYVt*ssX;&7wOFYHB&#Szgu}^agbc6Z;Y!Qp zdNot5&QKiEC220}FJZ9YXz$~(lEY~?hFCJeuq1*pSe8h~oOF~+^&DP(*gXEx zVT)KE_xQj8yFRVEKop3i4vTtm(wM5TSg2R-db2&o6Rrh21?lDgP2>O1ZI09^@&Art zixJ}g>xV5yi2oOcEk=m{OT!kY;{Q|e|H*emuxY#82=V{ZLu(%){(pO~G(!A;^YAxL z#s8<`|5Ne*srdg?{C_I`KNbIrU&r|W)cyaHFK#*(W4QnSvBTG^M##4F zey>-VkDBS&aB^lk9wGbC2k&~-doNNQA=5GX?o7vq6PoGRg6NK(|DUu+f*DiQoFH&3 zohf69C~XUqu1K?@9h3}c9A!~17pY|9f#bhkl`NuF#2A{cy6g_-b$a(q4)-=Wrz!4m1Uk9ts0Xt<2aahkncQCf z|4q?{HyzJ9?203p)BASCqh>lboSd1CM@X3U!E?o}_y5t&*BS41(0AScKcSh9EpXfK zsF#j?%~CAyvl26%b_jJ9Xgl5ty8Jp4Bk;IALj;TQgd>dST+Z6@7ag;7iupvS>xU1- z`b&@X@Kd>YCcrXUee zt1JVNHU51uQ&*XfS5Dh}Vd9DWsNhQD*7N^yccYSE*X;>@Jex{QG} zLd^>(njGUiC4|tM3UswQAajLe(A?8owfS~7l{4zyyA)wGDWh?aWxN(VOwLxy1A4gPmZx-fGwF9@>p$10jR9lTFhRe-!V}k8kt-KQB7CqHt_%e3w`SA*J|l)xS``uKri`%jzGif2jUj z^>gaKgjWIIQ$M2qw)%ecx72s3e_wr@`s?bislTFLQ!lF*)VtKzs{87;x(;s!a_Xzq zNp)CFs@-av`ZBdqtyN#7o>HHyK1+SNTA>!H$5d~sUQ@lI`ibg?suxtxs-99ku6jiE zpz1!=-KuY@Zd2W?x>2>ET2U>iW>wdyICy$ zj}$*pJg0bC@r2@0#Y2kw75Bi~huammD88mRq}Z=mRLm)^RrC}sMOBekT&+kd!U|I1 zR@fAmDU1rO;v&VA;#|d9iqjPeg-9_be^dUN{1y36Bk|Bk~93_sQ>; ze^Y*&{AT%$@(uZld_g`dzedi<8}hO|E5Aw}mj~rOIVQhCeyJRlUn0Lye!l!1`I++5 zyJUCBZk636yFs=lTawMou9ppD zZCOoLl%-`?%Azt#hRYnX%VlPnPIj^E0@)7P*|IZaDw#w!A^oNFb?K|ppGkit{ekp3 z>C@6Dq>oA;lHM=9M|!99cIhqBuSpL{_e&S0bJA<2J!wl?mFA^aOOw*Dl$5%qHtA(j zqf{%sNIE4wS9+H8bg4orl8(VEkk=%yNPZ&uq2vY0vy!JIk4qkrJSe$Oa<}B0lG`LV zOKy~GNLC~Zl3B?$5>C>Pl;OR|Rg$Q$%T^hCFe-al$<7!Nd)4z z#BYdyE`C}364a=8Ui=KaEO|`)u=oM-z2dvXcZhEl-z2_4ye3`}&x@}Y55#S8OVoHpQ9pcO36^c%LvG@Y<4)NLIGsG&fL_8t-rRa6htD>KYekA&V=sD5Tq9;U; ziXIZ(FS;hVzOgs%vHBK)E71>v*8r-Y9S9}zw%yia(y@SDQhgf|Os6mAGt zgbTu1;Wh9IrXef~v%;%{abZyC6Jo+EgqI3Y;U&Tgh35;;5uPbLO(+uz1aAr65d2*5 zvfw4bi-PB&Hpi2K#{>@x9uV9sxJz({;8wv+f*S;Df+fMc;CjJ8&=%ALML}9{r64Mx z;DwDtaJj%N&`u~63M?m~H`%JJFKD`eGd}<#8 z_~bqf;A8tP0(^AeX8<4BHwF0ZeLDaj*!Ky*KiYRT;Q!e7QNX+ReHiereWwB5xepH5 z#lOE#3ix~bV6he7vTq#l>x*9o{H?`4;0=o%z_rCD;J!r$aAC0oIKP+&oLfu-UcY!1 z;FlJYfZfF?U~4f5$Sjh8r9~Vtw}=6z7j1y4#mfP|xOgexm5WBe#G)QBw#Z9FEM5Yr zS)2y^jYVFf&o6!o@X|$IBE#ax0kw;~EEg~GvRt^R2Ao<{04f$GfM+iX0MA+ka}a-M z?=^tZy*w>p@6~`m-FqeAKkSVIzPL9G_=CL^ z;PZQZfY0r913t6Y0r>RZD*&I^dl}$&_nHA8+6#IS{n1`6;N5#K2E23cg@FIhUeJi> zj=ehoZ{2$i;BW0c3-GIZ&j8%m3%U@k?FIg#<-HQXg}uOEgzX&zoLvC^qMZxCUv$j^ z&%d+4^Jf=${?&ydU~z%xpIf*Z@Tvu#|CI|o|L6kGKeWK}Cl+}A?gbB^W5Egd8w*yz zOBX;FB6I0m{Es~#h48gKAcgRs_J9<^pX~uDgfHy@ zDTFWX0V#wp>;Wl+&+P#zgwO5)DTIHy2c!@_xd)^Wes2#*A$)9)8}Q*h4!}Rza|PhR zJ(mIg;T|*KxAvfbf3Qaj_>Dam1OD!w3juH0GX?mYdw{?2&>r9~T-yWug)4i2zi@F6 z@E6YS0sg|dJrcm#J>U_-!5;7kVRs%pLfD)i0G8%Cz~X!hFgITZq~|MuSI!pzlk>b3 z!FgVaz&tMnIiCRZ&PM>Tc}U+Xw9ONMx_Qur@U!!v3*q_mpbOzA=06Ykv3U#NN9GNH zXUu~~2tPCrJ}y+ve-=&)%nu_|79LLLh#S? za=@R?ivfQ;KMDBvyRQTMyWL*`{NKB~fPcN41^lbspbNpD?FL;49@!1L5PWAhNFn(4 zZjeH7-)@jX@W;DB3c=mGK?=d|?*=IZzqcEt5Zt;Oq!8S^8>A3?Z8u0EIJ6t25NzxQ zDFpMoK?=dn-5`bF+T9?9V6gilKyEkqwV<^dG$N?(=4GkuJ{PdC8@yYP-wk>Zq<5bQ zNbcq(O6}$)x^g#YM-bXA1*CQh0sXsqxxBl_0Bv*E0RF~YAJ9ApIuhvSKu3a^Ina^d z!Z`-;Q*&j&Pt3vGB{*j;19;XPFXI_=yo?{3<7HIO#Q?|WLV)r)usXry953nE96mNS z`PQ5Z@Qpb;;J?pZ0r=Co%K%@U1KXSYmpQP#$$y#y+nfCH9N6CEKg@ycO};n>l1~2h z+z!Ay=gt9qat>si{N5bc-sB^5V0)8)G6&L4-aiMnH+k=z0PwCkkZbaeT_D%w@9gRU z{_d_e;MaG7z9w(l1^Sx&>MqdNcDX!hcOTq5qrKs%s4Z!@a&jDt4nEUc1zMYo?E~P}yvoNzwe0SCe_|U8l@PXMG zz(1OWnQh`*v!4OHdv*%&>$5umZ<__}Ox!#R+L`$3ENExq&@5?*GqExY+L>6I z6#(w$pMFnVw-dB8am`N9&O~=7XlJ6i6C|3b?*xe^YCAzY6X~6xor&U3(9T4D=aqn& zopHdcck;5NcJi`(ai{0K=aOv0ZltE z1k~>2`D=FW0K9l7&;Np*JpUa#dH#}}Jb%GXp8wcR;6MJ$oxp$m&7EU_Zvg%Ae+TyC zuLAk;e+KU3F9Y@Qe*)&?{|Lm#|K@6dzX00fj|1!R2f?Jq?}Hg&{B9Wj@jGGk$M0|g zd;>;({B{`h@!P=n$A1T0fBd(=hQ@CM6B_>t`2F}27|{44*w6T0FrV@HvjKQFAD;!= z8NVKgj$a2n$FJpSepw)tiN>!Lz<(H1epUUZ`Ze_{>YqUL|AP8i^;7D{A@+YzeV_Vn z^*15%zgc~wdPBVeasRCP8a1bGK-8aAU!{(#gKD1|Q(vLJRE?@HQD3M&Uww}HO!aAM znOdNFOZA58=c<=gFR5NsJr7w0o>V=idRX-UR2#TUb%*L!)lI4!RBNgw)x7F@)j-u& z)l@}QT6Lu=s-jf5$^jV&%qpGgV$}tz9jdccXQ)&viE2XmOXcgzSCv0g{z&-)<#WoX zl}{)iRX(J=UwM!6PUY>&Ta;f@9#ZaCE-L4g*D8C;ma?kME3Z~2m0=~RbSrJj%alf? zR(X+fN_npGEamA+g;FFNlfEf^P5KJdJp7^b1?jWWr=*WdACW#Ny-#|#^qbP#q&G`% zlx|2@qzlqn=`~VL+K`r|S?N{MxHKsBNipda(o3bN^b+ZX((|R~NY9j>CY4DAlD8yp zNPaGPS@M$PMalD$XCzNb9+NyQc|dZnk_-EoDiGLt|PW-g^3Gt)i zhs5`b?-AcAzFm9^WLG#O-Y;Gh&xx-U_rxu6Rh$=JEl!HVVp8lD+r*cNjbg3%BJq^? zT=7}r)5Qw0NDMnR(QBetL_ZPz5NbU>D|$-wxabklgQELHcZ^Qp6QOg-?Mgu25X6KoyrLE>xVaI7e}&;xvU!A&|c%e?$Ir`OA^YZKE19@9slNaS_`IYi0? zSbl+ghx}~$8FH0eBA<}`Quey+RoTyEKa%|bDs?<9dqVc8>>=6xvU_BA%5In4BKw-` zkZiwfQ8p*LR@RfXWK~&ScC{=i3(H8ETV|78CNs*kvWsL>vU6o;$xfFkWFnXV_&MM= z55fGWJa`D^6y^FMm{gRDhdv9q>(HkGuRC-e;Fk`;#G-5;f|*6xJOt~Kk~su(i?VbG zCKqM?5KJb@>>(-O7Y+#l$wT9S?t@t@LgYxqSJAmecO~8u|GJu~wSOWaiL0FfR zpFBtde*7S;OUknjCIQbp2aVcVMS8>^dPKAiXR_@6-n_A2R{Y)_Xj@-_`eUr8msv0gCL9I z>4PAP;;DnM#ws2^2(l>t^dL-?itii*SriW)oB;fzjcWn_WMcq$-v;c16!&hhfd6Bo z4w&4i0{-3x% z-3^dM(cIu=sc*nUuV6MV1uSjA>Z-_X@Umn#cvjvmWam5DcTk-h~UY5(&d09;Byeu>8yeywxhdrI*)9W?B zsr3rr`Rkw^#V6PEfS*{;0G_=LI#PUe9dx8PWBm(&A6bW$OYz}#Uar&EVK<~uuY;x( z@^xN1={oF*6q0q&mqN78%Q(4i8yl1VpLN(7$$zna8Q>f1u%^jhUk6F$|Go}(DF697 z*rEJi)*)Jz|8yPXmH*TFg@FIC{%OD$)?t;CKfewdl>hZQNG*SM9i)~&vkrQcKec`a z;GeI9?D8kqL7VbFU562nKe{dhd}JN=P4e%o!zwNR_WC&BLu;^mlHb2}4d5TI^#Si& z>jK`h)&~5>S`+a1)?iHJx30mM$bWkc^eg|ZwH)AYuEChdzp@5emanaS5pZP<>{h(vRrcjv@E~q0BBkMnFG8;I}Vfq&p814m7jGW4S41O(5(E71H2T915rTX0nn*@ z`~W#NCi_1JfWPdG1HfPQ{R6;X_VNMXFZ;m(;4k~}0pKtD`vbsV_BRKBzwECL0Dsw^ z9{~Qc#}5F1*`FQ&{<7~J0RFOX9{~Qc2M+*$+5bEM{AKqY0RFOX9RU8aKR5vVWq0fc z{<7Ql1Ap1?><9j`oAv{L*;n@if7!-<;4fR-5Bz1T`+>h~?|$GfTi6f$W&QoYUv|xY z;4ka!=lR$6^ZbkZdH&h`JpZfr^ZeucdH$jOJpbr^oN&2%@2jGuauK@hVRq!L}k5T1P)$;(q zxB3ad@2-N^0LfE-PL05S+YyjHyb5cj^xLbjUP%9B6;=xA1FNuBO7B}00^Yj{YlQR< zS6~k#{eu-)9i+d%(gFP46<8Ofx30h}B)xeBW+CZKD=-U5Z(M;DLHd=Is{yZH;k|hO zN&;|k1?D2@!V1hq(mg9M7fE-mz+5E#(uxbPvtkEqt-zWnZLGi^Mp|1j0p?d=-IwN8 zcwbJh@V=Z{fpt*&#g!?*$jS~tYULb2V&yDA&&nBqt`%Mu>k2Q+6)U_fmK9zW-3l+u zMa#S_pI+u=*|E&a^2udhmQO76vV3$I);Q^z%Z0Hq$<9eCvKR1;WspMhZ_6NsAb~0iRg@FyMEW)qvkwh8b1z(6Sitf#pfSKU%sD z@DG>11bF9C7x0@)Ea0t6b-gxqJyUBC#xiMkJ;s(1-+GQUPADBm+EeNd)-Gr3paQK3<}a?Yjo> ztbIMekM3&&p1u$CB2n%Gy+~yHz{VuvePCmf$$h+BWBb6y#Bc3`U5EJp?1S}A{EK~H zW8#0?2hxfEbstD4eq|r57vi7p^8o(iJ}2Pc@3R8_-M-I*ag1rkzBQ))hB~PF1;pb7 zMApBpbSwT9;vG9gE(hh8%YF>egaxAFeNt5Nw-Bk$K+L&Qe39rcArAaBM08!@4#D>z z%KG@^Ba_VJnG@fhC{BE6{QmLGxB|L9@qfar1HnWfCw}VKMc<}c*dU)bk+`wmr+sL| zLboHK29mM`bre<4TcWO9O-q&xW<2T%ul6^){qt>f>$I2>-fAmdHWDdQAmp)Y62_9v z-*X1)E-V%aVWzr1816-KXePYc+wAtqZFHNnc?=q-EoEsMI{uo)SI5|l&ezYA6kBl^ zjP-P#95~`lL!})FuXZ=P{j+U!YaG<-uA<*tvZZrqE}QWhlC^x$<<3^JiBiRj>2wAo zBnsm~xq5h&+wAskjkUo)aISbKHq=MQwL#$klXev$&a_pr|#$pn)Xey1cI}u)OZ+83m zHo6T|lC;6B3waFIRK9FzIaB3Avr1Mx?H=OSWgrV6<80+?0kZ21udRH zz^6e;hn=o^jY&_29Yl&ukM+<9<|wz@eq9-J7s^%C9A0g0cKg^iy6qH@k~K=w=5jR8 zL9$L`F6l};d>OXXq?~lSgL>_aT-`_6+|X@vv)e!2Mz__Ht7CF{wWLu~gA`ECYBfox zaT!-A1=Dq8CRKM6lCNXiYkQi>iA}AuU^Fs9KZF z=3C`fsusvOa*ZC?ZGE%b?{1^pj2`PYJo*IAb}>sM7S9+_qcLE&SnO0U?e>WY$ErxH4%HhhG){HHtCh`e|JOFUMFS|6s8;G; zsGnJ8gEf67>Z6*0DR44I_Lv2-$rFsTZ-@sWv^S(tIT zT@^9Q8GVAjqBW+$*rvO*_o|@94OLyRKt)odqZM;7o|wzuaO2iuyri$daLpKt3of!w z>nJaXvQ(G$9u>4r^*52JWxh*$w+dRw5{1eE%H{|sw2^ovL0OTeuaQogq7^<560S8p z0c|3mHWV>12+1z(T`FjOW&%P(EF7k?a4?Z{W_3&{P_vnFTh-g-jGc(x&J!YBXDeWd zSV+=+(SEQ1omf0@tS5(jvRs&)$$0&k< zJx;&ip}97rh204Xj~U!FZDrzZv4HEs{wi3@&g|0Ou7Xxb>Ae{;#d+!?tIy!EIuo`) zom`x4LIpELYb|~fGgsKC4+Vn|@6vu*1#K1z)IEW6I$OaAT8Dbvb$7uGdANWX(<6+x zZOLQ6WQ}al0#nOpbZKu>L5sKp4Ai7sE~_pYt_WryniJWGH<+qK_=2uswXpRtjE8D; z2~6XscWG}`K?@a$4opU@qL*@zhJ;?5jwEcZU^;|FKz7}T2QR?9uI#}B4G?AT>e7Bm z1#K%2QiY1)2A`#48Be+3XPIm-C2%FC70H3{+f(2}rPV3TsQ3b6xQt;}< zfIH%ceMM`Ir!x(Z2d{67i#fxwVp(gtTm^zWlKpAywNW0&1-*0KTM5dYMJTM-lT$-4Lie0eOjo* zO%24$34Xz=Pd9veQ@&1yYT5$S!ef~P-J*P8#~A6-enADTXoO2ufr&PQM8;cb5$%>6 z!i}(@)k&e2l-1AWh(sQ3xos`r33QkCMisOzS3%TOjG_f3m^8!Tu)AF?>O7f_t`e$4 zft=S_^fqk;yI9KslZCsqpI1T4d2C3lQ?NK}O=B=s;2H^S91{^UnPkG*0%~x0b$-5> z5d1iZGOA1aITf_kSh9+yb*%v23E*k2RW`#mYXdh2V-VrS`GUQk32H6jih~DBWT;E~ zSrxRtMv>rbRf2YjB-}0(oK;&PoS@3qurAL9Vx>9(rOhpzE?L107VOe~Mg?ui%f~%U zHr}EU9&4gby`x>sxL~MCW0n@q$1nmiZmyXUD_K|&>BLfj;mxgBuk!}wdHdr z^3g)ZO1m317Db|NBw?UETfNH*(Z{Pqk5r3)QfOw4+8foub%!}V0z zkD5(JJ#R=0O{Z%6u;=sZ1mJr=(1PsO+PIfjY>mcEKtg5rLeR+`(Nhe6iugj$BG{J__=@x^C z2Db=1cr#}blNM9anDjcrDnn7~x*UoJzZQx^xYL^sMe`=Cb3z!OUD+y!J@b-RQ-q)~OafdR-8+HF>ceP7{d7wizlm3naIt0T(l6Kbz^$TM9Jc^FcX}J{ZfX4Mos( zITR0iEfj}vr#Bsn-MXu*h9dH|48$qd9p7)4R2f(rFzoq7m89bPNh|` zCFnv#JBV7E!B*5>00$t2sM8gpbUq4mL`rDgoZ{Nigs~dl{j4}fDv`Gps-1QrA1ef_ z0Yj8Q-Jtre&Jm(nUvqPSWWqI$iJIK4D0t^vH`t=-rYBNHfBwl(DNe8MW(+md^yLhbth#-Ae=Fn)n4Pf(W6e{vpUig%k1gbOo6O|y z7agagF3M*q1)Rpsu`Nh78*_&@`|)Bek}A7>Vx^Jes%^aoEkdZr&(Mu6W|PBS;G2e| zRiAQ{ZFq+>wXn1%S(ClWwfR;>6p}Yd8c#uYoR!VJH6>pRLpc_dV6L%8sg=~ z+cXs6H)c9Au4|?v3v4l%ly*9?aJq_>sJz?IP9kL&l-felT$-qW+Ny0DrQD3Z;n>WX zK{eU*Za;LmN>-!I#g=Ku&b2H)X`M68Zoz$p&agkxnv%cV>XynCLNQyE{EbfUJJz`-04k+qF(9w-=u$A zK6DK6LhyfIDE2*HbWdmcA4T5q{4Yh$`aN{O2IYs2kkc8h*HUhjG-Dkk?JW>NS02<3 z2FKDo8HVgZn(cHlHCv~+`_KQhjX*+ndl0-6FE))Ge=N*%t{|wa8g8ZAO)r}?I+BTm z5W)O5!sTn0U5MFO;Yyj1DHO)JaHt+Ja}_&mOnC@CnyZPB=yjmrkX{6PR9g`&Z%od0jLu4c(5J-!54+ad&F6x6mgI2#*FGrObJAs6m2~*9Mqv<00OIgbDLzp4%L0+5D!$ zmUOx) zK<=>*PMk3Q5A2IgGSY_;h(fdYIqYHreE%>3AeG~kFz9pj2ZkF3R-a$87s3VaKL$m1jCNoz`pMqWVU~Cz$OSZLuk4Sn!Ga*8D9uXwTU9v_Fh6ho15Cr(S7C9{s;( zZ*Cs&lfPYf;r&1V+k5C`ZNjp8EHHgJ%ag%izkg=<25 zvn9&J+3)>Uc%`j#(kJYF`Y$ha{cOwgaqW}e|MVRtO;%hu?7@TSWlxv5$lDX1B3H6N zk$jXb6m=BrVpvpPap-&ny(vJ6EN=31z9zCleDzcRc~tY57qRQv&))Fklc!I=&h(kn zy;l~O{omN}%;R$p-c2vNyTrk!9n|=3I%+J@DHcrO4)}`QZ^@P5tjW>jY>pgV%$AMT zX49}T6wZ-9KE7k}%b&dUn&PG39WNvQ=a^rdeC`AHpL=oimp5Egeh>_Wt4mxEgoZQZ zF(J-&#vV(R1;kZPv*BVQEeb?9z`7CvQ1v^GI69m>6dyb0<^wl%p3i@L;qqrM{NjJT z@A{*QbDJ!-pB#GY6VGpXd4Ggnc6N!A6*%m*`Eo^`^2f2Z&(g{>sbHv;Fr*WBDkWlN zB9sO7(c50POx(C>pJO)t>gr#8_Z8oTFCG{KKvo<>Ti95UUtZ1 zj1!Jf+wK5gb7y=yZzCAabP9$57i{`5r^6danIchZRcq_CSH}3N2ha5${?HK@vRi^T zV=nPZ@k!^n{ea-|IoDq?b>Ft{ZKjv8F7XgINo0+0eAmy;xz+vLj*o3Q_oVEP_qrke zfRp>)$NwjM;In@=o$#I`gY>e!OFYC~5}A1KpFVM1>_3-3dCKo5&YOFB>v?;BaZkv^i|>gU-;|ykx_d2JxBc#u`?QX@e3@Q8sY^V>%@UdTu7$*CDE3EV{Jwj) zKKryIuYdAO$1U&q`QlU8eYUghdlTo-%O`e;hqzlJ6JNIRrsu7N z+21BUP>Rj`dL)WIki6*Pr|9K(c8Q0$T_O`R---Nu`<{0_2LCtr`IEQ3)9XF*ioZVh z#m9g9q5FP=KK0WZ_NAA%c8Q0$Um_EWmGY-{K5^7{CSUl-<>F0SpB=gP&qw?rU;V^! zTfauX8n}U?mn~i5A#Rw+#P_`VkzfAoyV>WT{=!Fo@Szt==l;?4i^nc|m;TyZ;g&rs zm(Tu!UN(1$hqz-R6QBR}^Rd0o-~RZ+wReB##mo0N3*O_5({)>PCtmZ?1KbY^ek;9< zbcu(!Wg-(V-10w1+_(Q>`z+qOPq?^OgE;R?~P_s%~&F#S$?8J5Rb{aurlG5+LFzx18k zO%L9&hsS%~*naOg_M_W6w;%l{uRc9bFYCL+L)4Vl^*L~N~%epS{5I0U_;s+nU^XmKuKD6nCIo~`rM(_WZOXl`{`ZJDC z{pODEjeq{>TMnU@p)Td#->*YWCUf4kCm*zZqx>Ck5{xaRVu|Gxi{ zSBngCm5*ND(j^|^)`?8K<-yxVkGg|2zl43ou^fKp!x-QDFcKSVEUyTn7> zJdug7d-q=Nc-Ide{Nvx>_rU$j=iQOs%>4SNul$t@-t_!mvGjE8v-I-uUE(3`p2)=J zLtkFJEMz%*_JAkO4}a(Km$x14`$ltP7>&Pj;}zefOt;g^o4dqA+&+^$=(OvSwHEAmQ!*INBzNg_Md+=@yAbJar`8`d|a1!i2Elp@v`l|mi~6;hd#a- z3CwQ##ZiB_tzt>6Bo^QZLkv0dUJZlK7-&u@SHhm(~<4rPL$x##%V zB6r6)kn_P7Y}>v!@iS$PcI+cB_84yicI{C=yhj2w54N=zW-w>iBBS~`NA1jKl95keC3oME(Fin_OTt?a_4;d?yJY|c;J%2 zCqMoqy}Z;Vu5ocgu;cdSI!w6Jg6i>70WnwgTG}7-p>al=vpcGskTc^gTqgeV_g-}B z=Poh+=zE8r@ap+@|MA|hQa`$I{4cLwq{~&0J<+f?y?jKMc!=95GO>VO+RSYF{Hr%V zf5RrzrrWpu@rCD3yU6piPycZ51JC_(#din2ys1k(#C;T*_||KVx#`F!9@zWi$KB}v z_Fk~z8swYE{eSsAUbS6$>AY)h4|;iHmw1S`xiazB+c!RTQ~uBIe&A2%?sLR(zxmM> z;$xq>>HB}x98|qUbLR86+q>`oNB%Z4zj0<_{5Rka|KI$aD=Z&5&6Di;@zJFrTzBm& zSv?=gOJ$c%Y@?oo@Z3AdvJ|wRV{YC#stYCsa$N!U^EQE?eWJw+hmK${*uz zT3fH~YY4swK0uVXPGyUwqE-*-EIJ6%$8}~MIHT1XOwiVRiDUDbl33enHbdP{`ngiE z1_~5)4|TgD8+=t1N=1pM`_8|sQE50{>5OVuS&!Af>Je+8oADgqmgGp-c_{;c2NZWx?pHim)Z6gq#*5};g_ zW{Jy1nP#+%r!r8)3UO}A*>N|j(Hff0ILQR9gH@IJkh>0!`OZ6Vc}a5i51leP%+8&`Gqf64cC#SH2uD7&+kH^X>6WM&dMciK=82ve}>Y3AA(xP0g%*gC_eyT{|* zgXMpO-lnA?7Kqi)*9!>{ZD34ZAE*g`hzU__lE5?WDll=!wMxi|YXz#&yb9{|fm+}t z;bKN8OW~6(fxrQZBn7dYXWK=#(4A3Mp9GBEmtt&#t+Ca3y_yFE#d<#f`iM2D2w8w91FSK7##L`HrlDQC^PQO zR$>tcTP|+Vh11C_Tg5zOrbeQXN=fAj{z})YJTNH7i}CJsDcO2yi0ub>S0GIeN9>v$ zhVb~ICkN>@>Z+5&zoa_so1Pqo8Rb=0aXCKUqRGMX7ETV`t3`t+htRj$H`_bi*viZ?q`jO z0^N+X@my!KH-)oJXSVK7VbK`oOxNgUfKB?_O(X!dV@-NXyX;FQ$tG{ILU4}ML(bY} zZ&Def)2a&Z)pjkr z8{Q_JQE^+}5bSPO*lh`yvepcqq#Hph+%a1c4u8#tIP@+jrf)VtWj)fm`ybLY+twf> zZOiM8juUa%BlXRxKtXKc`7PyyGh;D!8f?hqrcrNNAlP;;kj8vO%TS?Bh+xcsJN^Nu z$I*xqRWII-ge(z)D8^KV_e$4S3GZP%cjf*6J4V=%4d;MN{bzx^`tJsL(Aywuc@yLq zuY-K}BFJ7}0lDT&ATxXcd~PUSZ5QAbQKul zM0{z8b5a#1xaypt6&T`ZS&af`53RuZ`oL9Z4XwcX`oLB1=_)Y9%Zv5)fp-tBz&e|t zGqeKhY=U-IfgxtMrFD*i*3b&9vk96*E3nQcXmk}A;)U3HM?rmP1=iUFwV@SQXA_8B z1%~kK^)^9uXa&~U1eKu`SZ5QIy9x|Jy51%z4XwaBo1oZLV2EALI&aIbDs&YX;^pqr z5Jz@vpFf;^Reop%+%DE&Wui$qV2t>5Tnn*54zz|>?2t3$P8K^AK^sU5Oa!YzY81!~ ztw1Ck2xZ8aHW$>A`83g{t%XiH6Ktdd(F8&Jg9bhpb1_H?&5&vo$aWPN;>>rwO&|=d zz&e{CGqeKhY=U%Gfg#S4mxg%4U)u*j5rUD($it(ct{ppToVgj)UJuV4GyU@XuV!aw zo&?p@KR#UpwaAVDrvVRxx?&}bbzx6XLGEC18t?$92A2jk-xfe+w_8E|w&I-%@;2REK{L|t$7C*XJ0+s&`T==hr2Ny11$j*Od{(||_=OIu}?Qu{w z?X1~nRyl4sev%v=8{1gVC&*4K8DKi~hQCy)q{BTY;S-C`E5Y8%gKKA`!4+Qu5qxtep;Hl76)0pG8-@r-7> zX1m(P)0%CXZE71&Y2K%KpW4Qgn)hnntG4lk<{ZsAY8#J(>WF8nZ9JwqOLNvPcV5dy zHSf{9XBTnETA}z3>)`NWzSI=4RF6B)gDROCwTnI-nyJYtP&iYb7S~c^c4}g3 zLT%%j$-htjU2Ws($yX*{QQLTG^5w~w)i$1-{M+Q;)Ha@&{Ojaj)ixdnMN3~&+jwm9 z#mN_UxyD-KJo&=p3%iItHLsJQ{7I~63-}3V-%K|-a_z{qY8y|CTr+Zw+Q#D}SC3r1 ztFf~jN|d6xbc^;A6+a(#_IT(NSc5ND+jtTb-n~q1U4 zg-dodUTd7kMy?vUYL@}mYCLk~$d$X=t~GKCA71$IiW0RPn{Bwgt`dPqoMMo-ioK&e ze((6bY8y|E-!p#CuI{b1nzRveqz9 zBqkEa0gXpTH`YOl)+}q${{E?{>iDyuru*W^!{Zw#p9HnqTa(VoeJ6f7@x={SZOBfXv%$P!dhy4L zpIodip1QcW@Z`eh7Fr9Ag?%P)P(l5c`LjV)^n>P}0hP|r-SFsGbS^ZvQS;xRLSP6i zFc)f~nxkj`GW+1{#j|J5o-qEEv7^T>9J_bsm6=Dz{(I(%nbhcCMjsr#cr-q$9eH^M zUgc@O|F3l8{Mb@85G;0lh|$NPR7yZF17}8)Ws0fgodt&;jZ!XF3zjE?)fk<7{|r0| z$~XS|N6iRyM^~hxjMK-WSx>GA*;4*S+Je-QEeN&w#h}+{NV}a}EfcBMSbQ!iBVfi@ zGR3HRj3aDiQ#@z!o1OY*JHV&BC`jWZLh)3|h=*cGX@36|8TC++ilM9#gt{+7h{0xA zD0zgSwxEL{z1?22XG|d^18cFlh>Qp)ZHau!nTKn-+&#D?EpWwo(VlT3)_ zQWh=*tm51eGJ??xd2LO|#sU#Ko)-)jP8W*0iJTQ~*0m5?Y$cuMOfJ%4p}DaxqDr!5 zM@|Uid?V>~>EVi&iaDJnp-9VdqU=x5HzOiOx(SUTIfD zT*Vaew^~TcoU@`HJyMQGS`6um&Yv#J!16e2t=m|GS*OcX5h&%c5m=5ewrbg|%R{;g z0Y|VJ^tvplW?7aIq$>8R(^m)vjVUc$w?*4n%Wfmec!PE8EX6pfv(>_!Q0{P=(_|T5 zeJoK$O$n#hD}odbqMu8n44p5O5nTph(R$h!73z_?qeyCevJ4BFEn=t%BG_coM%g_e zX@8EfRNO&=cB96o0k3)mmX2UzQ*(?gqe`)nnxRxHoBXXfqHP8Z#u%H7x*@n4u^9b2 z%AR9DjiXGM(;O`$Vx^R=Ws4QUaLIx&wFHMoGY&tMu-6&4K8CZdtU+&M%zPW4eQpKe zC959Rok-H4AQ!0VNEyH<)F_=zc(O$^8P1!^P~8%YcyjP;TSi1n>4*<9S&BwiGe+h; z`b^H4ZP`Jg%1oJX7E>us8!ff%F$fyRRuFER&zqxTS+3r&)so4kjgHewU&0My4V;x5;Wls2VV%*puU8QQ(VZxlE){ka=Cd{6V z%FbQ|H`uJDvO}zf?T)aGX^_!6&zRC?m#xUy0)e0laT!cbPp$4AJ4aS3go<{ZFOCte zPPJeSI1HR4XfZ)flmwR7di{{k8jaa`xRDz@Sw;j+b_S0i^-RE<;psvn!U!RptVF|n zyai?4AuP%?!dNt=g(e=65kyfRbtPcJP3Ut)0*>I#YTVMvMGYNKJIPQEgP~-ydE=dQ zY2pAG;Y%TQYt8Hm>g~EH!R9*If|GGKgV9PJx4`~pt<%nnjY81`&%Q@SxN%!3-Z5wm z0k%j-sd^L*S&|?Zd?CaYVp$ZyVpWql(TtM0@%#z`V~lZ_@}_MrhZSid-dd#<0Li2I zs3DQ|g!nc~qllPt`N_!@gsaNIHH)|9#TxFAV0XIOm3%dYWP-ed3X??59?Up9HmsgV zk3J*IaQb~HZVAS?tj7v2*@$@yS+67QWHW4T!!|M*W1A&VrlM{sO&u&F)K{0C-d9GT zhL|*3L6~Zn;FO_gfZ&){beh`juq%zgwo*Cnv6;cQgW}9eKP(k}p-SmmqQO^>`K)XQ z5yK%5#RXA}^x`fymyddCdJ1IEn>tmNVa~>rh%an+nT0r2@Ky5Wd=%#0IT(VT{}W~x*-b;2QYw1r!<{&>{j@=-QzEjkU$h;XKa zr1@CBUAGgR0-yG|9cfS03me!_!`o=W`G_8Mr3q^bn?Yp+S<~PFwcU zEiIi(K@r@Fq}xu5&}_L(<$3}h`Bisf5Q%o9EO16o(~^qkBk>fQ1RsBUv<=EgCecu` zP2eDMTWr1_cFQ6Ne@HAi1j3RM!f>9ex10to7D!e_cLL{prlQ-XPp24`uHfTOtROsv zUJaQHE^ z%|LVcAXYWD;2cI|3eilvok2ogv%SReO$dnv?2QyU@;MoSnQ$=|$@?lC+s-69tR38N z1mD);2pLZKoSe?%H8XD z7Da`kO<$S}$_S%JpUjHoMuN0~J6ly!@HBPRHe2Z=f>s#X;%n6OkrHdbQ1k4H1=Uxi zoBEUNBnCT~)3Ms5V4;~jfqTj>#;m1dWd?5MIETfEwHnTfy9So|MOi7mAJr4yM5a=1 z1^Bkr=0GsUO!EvA>u{NrkPjCqeca_L)9KMCyNGJ1gXVNypdp#F()EkqfH<7_{FIo$cS1CbD?Q# z%i;j_4;_gtaKH+vE|)H+GCUUc#PdPOS8nMHE%W%LD+qI^5{$51J;a!>4p%J1C|4D? zdURFD2C62z{7IqaMBCb?dAuwmL>C-HylF692NU)J!Gh7kELfscN|+5+w@viZDSbQx z=UDUDwKAgOLOOhb;!u0S36iWq85bywATS)^M!Z(c9gHTq5*dqD64Sd)3rh^W%KKt7I&*< zAWR7goq9k<2pO-n8u#d9Mb=ht=gY+kT`%jH6y*ZeXmVib@!G5H3Ckz3-IXe@>VI`abr%KnI*t(Elf8YRa;BX(cT(ljNnE>n;lPzGJwj5|v+C&)_iVM{|W(dJG@4~jFktZcXx z;~Yh6z`$7JrB>UK)+H?!ScJI^vWP_1ZDT+c4W~7bM!ms$QeQwsAxJPmYbma^RHHte zsuz9n0zUd}84=6Fur?zSj&RB;#%aBn3dEu{re62ewNx?bu|&h3Rs_|B!jmViAXHa% zocetiAsPfXT}|rDRG5oZ>=qtzyKS(J$=P!?lgsCB*eu=-toK7YY+*^d|L+{RZ)C&6 z8$P_@v<*v(zgxV0@tj51;+_jXS@`5ae!)2Z>ijq7FPe|eZ<_n<+--Ab%{k^aXr9n~ zT$9xpW?z|o2z)sZnLT{w*_kiRynDtzvoQVG^tIEOY2DP{rXHNyF%_OVZ1Ojgw@kJt zt&?*TKbp82ob_!Pe`)-I@e9U7dhDjL=GduYv!nkpdgW*mJn_E()KU8bZx2nJ zbkfMG*H~eLUg_`Q?ZE*sodQe;Zzl)9AO#o%Zx1NL^p@3OdJDXr7yvUXz|8P={{R@G z07KyI_yCwm0cL`?`v$;Z1sDu(_YQy=6<|hqyJrB*pa3(#+uZ|T3g*@WbGrt>6r4-1 zhqpTiz!ba$RLa}#P=@Igyac4w-Hr`_DVSRa%xxb4Q*bUFa4y>bn1Yw+fR|VYz!bbh z2fPFw08{W19q^J<2f!4(Lj0R7b3wqlECXN)UIGCxF%N(#m>UA-Mh3tXoC^ZZWf}le@Dd1k ziE#i-!Q2oqH#`8Q;9L-JF2ewrf|r0?+uQYuFoR`vF9ClKZ`Tcgt!{3E1(+Ke09)O; z3?OsE_ALWot9yyT0=(pe0kGA*#9#qlq8$KR-Ah2VGI;y(17NFriNONAWb**n>Rw{7 z053Ui0Bm(HF<5|?96JEEx|bL%z)Ox%hM5(-#0$={hbA!l z%t4R%2WvHdpWjcST*U z(ZbG$Pb%s3$O#bmM;G9$`NKCK?&oV;x1wUZ$Zg%~qNuAy2^R zrYI}LNOH#1$~kQz3JC~~LPi(Lpy`?w9k~Lr_f++3}VUnEf z%PQq`2Fo!RdhJ75?RG0lhMfT*=R12}qf$=Sxonc0;*V6ysjVdkly`YU=F+Iu&44eyJW)!L6Zo7; zIpSJ!j6HH}_K4rDp)R$13HN){@gZA4C`j!?aOxwYi<&IjHQ<8Uw$+}0hpt7|IdWY?0@J0Eyl?uf_dB^*wvKUZF% zQjV~eoZk7s%P=TO_^F_@Ee*8Yqb&K2qvuyj~pVZl#^IXPH&uA zv6!C@yHR^slCyAvN;&LWa(d&GiP_zOXe2_|ft-h5d_kq0GuM*S8>ay~Nmr>5WsIjQCMU)Xqrz_=i7gRVn9;wdC}!+m0|1^*fz*oRs90k5?r}v+(N? z$C!C!_Nfiu+i=Z>!iEz!%q{+8@w1DqMd!k83)>fh3mfNu5AM2r2;9Iwe)i_svuE+y zLuP(EbJxuIGtA7<(=SXvG=1qbH?5x@o%;UNwNu5Zlcwe;{}bF7XivH(_n&xr;>#27 zp9oEC8vn!i{o~8yXN+sdUK#uL*vH0%G4tHZbC1k@bS^W8%uQ*2tof8i)L1opF7CJR z>#^z4$40Lot&Q47_Zs>6$Su2`eiwc{dfu)-|NGznx)FdT#!ep_{q8EKI?}Vbr8fpf z@0S7JQ3CWj^XPpt;M+=o-c@S!t1{pbB|z_TGkUKKcvuP0yKao$BLlvr05JD10Hb%y zfNv@RdS~*{yJWyOlmNX`^5~s1;2|YI@7y|ihYa|-5}R40u2Z&}Zq}WWfDOfIds#Dg*9Q0`yt>OETcA3IL?f(qEJT_bLJUEPaa%xJL=l zXX%?|z}-rKKEvH41MX4+^tr|tWWb$DfIds#CCei5FDn81Ed3c7aGL_a)Mx1%WWcRTfIdrqS_XVc3D9Th>t(mK~{*(;3MG4Sn z=}*dlo0R~4cKC!0xJe1n=NccE0bfu8^x5G$8E~T#pwH6R%7D)+0s5Tp8X5381%R>7 z(pSrX&nf}>40n|b_>2;u&o!=;0XHZC`V99m8SrT(K%X5xDg!Q202ulJACLi0tPX&o zKIi+k40v1#&?n&$8St1ApwIapmH|Ij0`yt>TQcBBN`O9J`lbx{p%S3aTfZR#{$q6j z_>!jQNNw~X8Sn!oK%XytT?Tw#3DD1#6JQ6)g1r5}(1*DC;weSj-u zz^9Y|eU`pl27FQp&}Zq(WWXns0DXr0hz$6+5}?mDE|me-DFEO;OJ5=bu2ll`8ScX} z;2I@BpW!Z+0aq&l`h4jk@R%T7+IkCrWRK%Z-TNCtdN3DD;n zJ7vH}l>mK~-XR07PyiVEEPbI2xLgU)XXy)Mz-3B+KEs_a13sbz=yQ$pWWc3LfId5X zuv`Ccbia}L56tX8aV_}6zkk38d~kC4SgGjAj{QfMhRBq>b{ck-wE+?RS?}J&)@zyg z>#LCsk*!uLD>O{L)^7VDa>1@%^kj&P#;X-Q$p~VtRBgu_f+!?}yijZRR!0g|ak6a3 z9YM?+#4%qp;p=;&tb_u@b&JOBs=alZROsmqg-|4o zcCWw*xDsH7rJ7l*)-9wxEk^}u(?Z%OmeLiZ8VQ(6c*^VX2myL?G!k`L;SOO>GKKVR z*DoT&?ih!*LP11uV@Y?zf?#zwo7 z$4G&AVSRLcGr_A3MBUpm5Qmv5Rw+#;6$^}4>v>-M7X{#J3P|Pbz(wl3AQ$1V;*DLKtpv(@3jT#5M~a0Xf|vMlFmjx(8vU;P?8HpaBR2ZF3IbP zq<=Hs_86h!mIBe(j55V0)7czk*?OfG3BbOzh20GDEP#a1O{Ptj!EIcNtL$*s3Pdv) zgi8TmF5BE}r;I@l+v3c&SP4~AHw;WUpzBgez})iDQrs|YGG9u6u3cc*9x8&C2n zww{-Y|4LQ;UfYkndsj8$l~STc>Hn@2D1Jk~yLA8ff2*#oK;S=GaMCqC0n{Llvw4A) zlz%<9TzIhimy(XMK0Qjx3-CV_@8T1zR5`f&$DF|BcKsPF=`1JpuKRz4VnYC>l|f%6 z@?F!7RAuZfoNf#{i{dg*=Bdqj!7tj4Mc#*bbAIsOywz4NWd+pVHtL9cwbjO1z1i|6 z$`hB_cE0KJfb2pDUK1Rfoz_e)N`Qj$kck5sD2hT`*RGLBu1;aX=DLT?=WRkI zw#Cqju=P^D9ExISrsNjwp=6W~hVAN>d8zBtGT&G}s=Lg$M~4a8b(?NXmNz@yNULaH z`QTyZD~X_I`dGa}=s#%>{3gl=o4Q5A2UZA`YWA)XQL5)FK()IYY&~pNtRPshSHNea zTrOBoI2vBTVM`a2nL>4QxXDEXo8G`X)158$EP?8ByS>mSxQr`S4|i&GI-7Cid??6~ zM710(yrD00xvIg!7SJ|Q<&wF2B^uFZjhiFIaK0v5{a&KOw=0l2L8QHmqtb{KXxxyC z(4DXq?DwU~YmE)I)OD%;-@;WR2aTUUvM{%3nE&LwVeS)iduqO*Ib`-rv!0os%($oT zn5H(AHcT#FJ@u=p(84Ds|9kR`i9b*5m^fto{P82lE*(2=^lPI*aAW_pzJr5>t0r`P zfBEmfSnm;7xN7v9>&^LZzJK}%yl=w}(-co0efoh*K3I#JlU83Uz!xmWB$g1-6oK&8 zhN*7R6+qQ`79`h$-6q&8ZY+mG4zyTrhU<7fA-Ie7V9$xj%9c!?&XwlW-j_7;HmI<} z2sAonVPvV|6buj>sUr?P>8$ICwjow%M;c_fL~(8`MYC|+f|{F{5Xq_5sDC5K?|^~4 zEkNE7X}na+d+nK0Oz$uNIR+pH@j0!cwaAocL(vR*0;Pb(VypWzp)3rFAiIMJ(P-D_ z%0Z}FQZ488o8R{>?9g|Oy6;jV+%kgvT5QOi$0M1D)(2I6SlQ8tgFHTL4Z<0af2%^* zv<4@pTGU{p_MV|j1HkUkN&0}UdqW{(6EnmKPXeW4CB4_KD+%dRwbF?+d8b+L&6qqL zq1dh&)ti*7@@$OGxxH$5AVT zJj4c2wY=8``N)+)9-@NQ+Vz0c46?EJ&4x6{!0JmY3JpNfc`}Gi#2fRqNAlb$-WTK%O*vt!tUG6@y4U zl8@xlI}QbMq}gj-D^ab;>6yI_SurWh8DnNQ%I70cjuIIt88HWJW}e6evf6lEU*Hl% zAOTscW=cijGgUs;`!Y-#0Cva0l0Km8>skeDMW5FK>!6iRx16H07}sK@OtFpPu0V&y zJbc>`YzOI_4J3NR^1d8x=@i4+(nc!pv9Zy-Ek_V^$6?R=TD!3h_q{@239JKG6siOQ zXcXkzuQ2{xGaeCQdY=)2LShtahafy-#e=wy4p3MPNw1Say>A%hz}gS!BTc92TAeIg z$|0jaKor_M>o?P%0c$a#~IETz8x{|GEFgYCI8(548HiihbuNYCo?q_kz5 z-sLgk83$aTZFLWYI0YRCr$UyRuc^9Y0~c#!?+#2-3+zFsMn-^I(282&22WzTTFg~M zTKOP2fo>%5B$*f62}&!tDpbqe$lxulrC zAZ>SB;<2PQhXk~FkbYRqwk-~4m@#JaNk_rc(PD1Z!}R(Ih~IJSisgrRv#7e~|FC>| z$1y8CTGzAg)u+^>SByEt*|O>`4K`-)*D%rmbnpL<+&;45vJEFJesxi|@W28*e*?(e zzjN+bQ14Ga`^ao;=DRb(^pn%oY0cEfK()WmP98Gxr3ugYPsST#&yJllw*Tl2qXz+s zf8r0jbCWcRzy&*ra6X23qZte02o%kBcfMx!`AxwNYe5ZM3aT`;DC$fmO)RuAm1`uU zIbI|QhYcljom_kMIeKLwnB6IDAdWtLWGO({Qdy>n=Ta^ruXVCiI-!lbFkAT(#JvS(-n zH#qr9wPRyF@uVx>!nIDDg*OE)0cRm$MOEvv%AC9N9YCHm=SGSytFa_DOBKihRXU6f z&ZKj;O4Hbhxy`7j4maC6M=NB^;FxN8uZz5$(xyq8OPpx4;m|TRI~-@JS~?uDupOTd ziA5-a9gNuprYrg28qS>T&hEwvblaQ=vZ*{3qmyWW%El5Yv&m}_@@2%$q4hGV zhkc#8vjE=gh1GXk_TR!zX|p7`OfbQQt9iDB5y_@CMr28Av0jf-tzt=pnk`?Wg8IxY zRv?2SyphcneSTjnWy|D!4M#MS^Q`W^tGSG{Z<1Ulp=$>lrl7w8lCCzrX=}z)$@3^< z@=)Qp#nVW-eQpv)5=OKhRW0xJE+cJ+B$wf{Ii8C#&T`V1MY3TC<4n3bLVN2%o(Uw< z!9cXBZ`Dfyn~_v4r^jUuSaF$lHW{&4jJg_MK#;875Chk&!k)G>>E(DL6ajOJ2)s!b zW=f)JQG;EkcmK=o++WfKbh{WS>+l9_Otn*fUBJ(?ai61QHI(6urB-fvjJkr^-!fZD zFw<16%WG4fw6&9BgiWCCa7wVoqsfT9=pfO0!k&eSkk;k3MH^vLrbrw8Lf&43(;Hn8 zD$n{`VQ~p6<*zE26wo3t6A8&FVIEu-)%bjSquoKvf+1wb^+BtJGUS7i zsK;5=TXd0JPPM$(`>(W}mHaoUt+*{nvRu#wUGYq`nu5qeNU*w`S|0Mc>vfDSVs2f; z3FB_na(et%+NpI{Fugw-!uTwa(FK}b9ptJsjHO%@Pd95NJ%xuXo=#Ro%n@A)S1oF= zIeR}fWOsHq*Pz>uRFf&ywaqkrG&dW9?eh zm_*Flh(A=VcZ|UZ2kHXl*6F#a_aPW4Grgh=VKpH3a*QGv*yHpI9-3=2TG*YS@R-3( z(^e+l77I8?pI?oxlQO*r+=ZP}Ko@D6`R5#NgU(N`bUq6O>YhM3ovmO5twTNTy1M}0 zqHqB-rbifW+mgq4LTqG%mbdCSqOfyfrSt3hmG;Vm`)YfHaUf5cW8dNfiJfCWjx@)< z)dv#N^S=&UuitRyhCLSVUo0;kzVQ8p_bx#5Pl25Or-Iyn*UbgyMl`o*&eZHNd;e@1 zWcvU9%zI~`4Yz!t#ew@i&po;kV4#QhWHiNirI!S{|sV^57;Gzzq9G92q|#l%1(5zC;UC|k1k*cjht8^G6l zqO&n;_o76tz@yGYlqxsdSfo!st1@5T4fc&5P{E><1KY+}EMG{(8dPuja(o*)fUox$ zV`Ha8k!Z%*EZFT~%;v52eZY@zJ9PkG??cf>KObdneAMqiDH|y6m3LBo{XJy>U+*)= z#%PUZ$ZRN(rfZ2-wG$@LKE9I&@b#YJZtRFmAma{nKv5oFqt13ZWFOy21Ni#!E^NCiYksRxn_pD$9X)`rZ$41LchmsBK7UCNp;o6> zv)8zMu^K7*N{znpJ8}SDpFi7+VOObD<(+)XU&w~&a;1-NX#ii}dZdEy2xY#$+ua+h z;6ppwT20lQF1wY;W3fV?ewzmH^{q#?oWDU96KFZ$=IY*d+Mes<+c_(0AJsH$_Pzo zi}W_L>3R*b<-OrIzS7fM@NI_<;Co|y=q(#n@EtOMuP;6dRNm%`k+wq0UMEFQt5xo^ z$H4>m`r;#iaRkdXysXt*X0q)X=IG-)NSUwi4*Nzwnughv+HR$tcmT)-&NUV;6nVH|PgJ__qBA@b$$9#aG#MU2v9S zC7y6PI(c^=-+lx5`r<;Ocf-gie=IpNGCMOmIeGfbAxp5xR6>SmqKc*IP&fx$8^%aBAJ*nDhfe2C zAf`;rVbSKwlq>q$7i6Lct`4u=%R@j$#P99#rS5N~lv{m+2bA>mD0%P_nTxSTP;lIB zMB}Nfv5LjB=}O+8bJtorLeC}Rg#vh84Q_8Xv0;@wSW<6sNB6f<28Y#@boVHE&=P8( ze56kc`|UP1?={8ToY)Cx3N274ROhf~Yc)cjb-2>mJZv6T$%7>I<{#<)R!9{0T7rM%#*BbXQRo3uqN7QpiP7#rt{)o3Z!h=2h^9eHar5+ZXvI_yXuAgMR` zr|$2U6b`gjQ_|6+Sg2a3wP^^f_R5e?T7fIgEms{as zmE2!aZ{pbQZ+HkLu^uJ&TjC6kX4!3sdc>x~QjRd32{xHX8~ENn&`P+xf;U^W(G4S6 z){(<1xu2xoSgQLwKZKI@9wql(Y6MI)Y>E5XdZ$p0F~Mvj*~p|kZn(xq7`r#c>h(>G zaWXdEJ*<-ZO6rZgzx#XV5K7v5l-y@Y;6MWKpx2YOq&!Ji1GgACGDVRjlZRTq4rVSk zveu}@QKNE0E4dF)@6lI3+x@MSbZ<4EwDu^u_mVwfkGl~wgZ(X%iu4OE&C(s;rB=?rod*#vYZ-umDt4={^kCJ;W!6Y1r=5r-4czv7|gGB?! z`AWWw7R`smWRUl|%T3gj#6tG=(1Ej;q~6OD-QRl*VeV6Vl-zU4$T1bB2)W`Nldo>H zCy|JbiROw0w~0*@9AtuW#c(3-vRXUhup_yrq~1%v>;8UW2qjPHQF4zZferAY*w*_+ z0>-eiPe9!fSGy2P7b|*C9M_>3>$f%-keFcTWpNKly%(gwdGv(~hEVe49wj#{r6R_5 zjv7c5Yb6zOQi9E>OC@;BVLwHPnyD==I$#^QpDtkXKmJM@aPD5>}RV)u7*2qjPK zQF39aY2l50h!>7*%I;qz^((ewngiG12n#D-RK zUQ+M*Uvz&zKg39G?NM@W$!8`YG{nMTDhme_NoQ8alma!I8MjrvP0rYf*zMpQDqLqP z+_2F-C#m=BiPG=q(yEm_eAyx?dGtSa=9;A;-v7U*WFf&<*WQ?GB<+6v7wPxY=PK#g z`&9kD!}@xPwGHNq({e@r6Il+7*fX>NxPqQSH_>6 zuQZN*QFHvVNz(C@f7p6@n6C&{bnJgkzP8<`C4GPLiuC(QrL2Md8;!%4jXh&JwKT*> z2t$tPl%(C`FG|0kz?H_d&+f-B!#!gO;t6-P2q0) zA-+FapHx^wd|kF$Zb$iJVM�sZ@vyQr^E!Yui8qy!m{NvT!jYlqIPvIi3J>QV`2| zw!M_6h*ZnMq?eQ!n9W8?7Pp((YPWa;rK#pxeZ*9mmz6cX&~pLvP&^gllfi0~RV2{3ssY4RDb zco}Bsx{LQkZC`HZr@Eg~tY_s<#%#JyU%wVW3%#HE!2Q#S`Qh6)v+OFLJ}Im_F{7`> z#GE0m^YNI288)$v zq7%5220L(7XxJ0YC0<&(nq#!tA_d@QL%F-?CJySeQn`73biD;zcjkIoiO~oq3w>9@ zt$ehMmys3JqpESDVL`1ko=8fks={5FR@?$#ZsLf{#Ytx%Q^XQ(3i+0_N_-~Ei3H5Q+wbpv;LFckxwx6-z?IpRPZv@@04OrEk-O0J z1xPjr6E9T6kypc&v+S`Ri~pA)DE)D+{F&9^bKt%g53V=4mO!VCUAbdA#k+ip&p&Aq ze4OIfo*SUz6L(C|G(RCHBt6hHXvp#gXWK=5Ik)JLv|O^9K=YtctT8C>0%YA&y9IF8 z;MsBkK$A~*4ll1oeD)_@gE0)0bCpMJt>m8VD9k3U*C!`o8@`W?VX`{AU#)j192z*< zY%EqZ1NEInOK#iMWk+p*x2GQ5hlApMimBd53n!A;4BbM#P^qrUl^P??O;K5QZU9|B z#s34Ry?1SVWaFN9_n+MTqj&z)(N7*7-Tt22Z#w+Y;a7p&|37^2GY6IZf3a`v{np-e zZ*zCPyR-8@@4RpOhqh0)eq^h^`3sw02AE&XAMN40_P0P@mcDVYpE1Kf3wXeD#KU)< z%K2JdKiFG40X_BLCt32vXqPNF16h*Ty@&4rs(;PtC)!zC^~=lm@qB&gf$77y1Cb}E zBDdET`O;?F&w@H&KKx=J3ONyCvTPRU0q@~kfS6MY+W+rQkFkE@9cC6Jg6YFI1Cgf|wEw$ni~KAL$^b%O zK711pb!tKT|7~qiFSDQwNCd!w-gr7>UkjxD&f3zhwxEk)ZpRtm3bcn`bgIs2=YM-` zbskwz1|$N|?Khlv`?qc=#r$G4UKY>-@8K5$b_)DY=Y^-F90IXEc(B$ zE%LK0Is+_$`S2bPb!O4Ov9_p}S#$<$0x-09&xiI?YfHP@qAvyxA9vOVFwh>}IaTM> zqJMpDbskxC25bV*?W6N<|Jn^fcD_6wIiJr01?xS$4b(Za=wDr1oyS;o2CUcV!$TnQ z%%Xo~ZIPd4(HVdW%!jvts56WHFKdf>nMG$nD*!`#a6Yu3TwB`J7JV_)Hwz%F_Hh4H zol}ec#MA*1_YCn7S+011t8AybZ|wiQ8`AyKch3+v@!UHeZUKG3 zG^p=?|JwRIW*W@!-j(Gx4ScW7^Y8V2YwLIQ)N*O8Gh88`p8wyyA#dD!aQFA_ej~^L z`1+&YIf`%p*6rote?1Iued?Bf@M{P2{a@L4_ddBd+x_^iwez2LjO}0Eo^1Uh$OZU? zO?Bhn1NJBXJo~}p9bNBc*^=HY!qu}M`~znSWI(javfFt24&&JmK9gC2412wG6nIZ& z1u|@e)=}U~GAoc_1GZg>dZai%~9%f9I$czb3A zZg>d3II{vbJOpn$Qy@d5H$4Pz&8)x;55Zf`6v%Lzyji%qXFvGnGX*lh(`7j*JpFL+ z><8bJS%D14v~?7CV`c?1oMF~c;EOUVkm2mOjskBuQy{}Z`=)K+3o|Ql!$a@|nH9L< zA-H#@KnBac=^?nAS%DiKf;*WNxZxo|Fh76mVg&sTvC-=ctYvbNW z@BQt2-vm$q34jCm%Xfcx|K0oUKDGZt`+x54cW#ASEr1MI2iSnW1<(OMbmzG{(jDl| zt)qW;^x>m#JF<_+qj%o^jobg|V03_k*nw}n-Mjr2xAzWz?(q8$UpO2ee)Zv-Z+-IM z-u^G$`te(T;Z}63ed|jPJ`J!1{?@_!_kMHlAMbr=@3}o`58Au6`|ox?y!&T&&E4|u z+d+-NpW6A$JKwM)?EImf-R*y~{e9aHxApC>+J4j4C$@fU>w{0eaCrTHUbQdqrQ6-D z_YC_b)~nk_5;F_Y=tZTA5II{9)}(v(j83OJf;cgz5xTh!>%GHveHtw{Ql0&>UE7*K)(T zSejgH@6Bt{y(y)0hP{}l8Hx?Wrq53X{R{1F{OOeLY0#FNwUn+uX$zrR?sdz}p*eOm z_F{YA_P*`z=6f{J5t$l81rxcaE$e}kT%dbv+8ij0+GJ~5!>p8+PAp$SwAp-PN>?72 z_~Bw~)O5Znm=b--4Boyb-4~zHsnGzybEzwqo%(DLtfGs^b6ZJD_cX-2P3)|@s_O&1 z&1oHA>GVRYTrt!O-F^3ZWbaC)x>VdQO(e%I4@>jJkyjU5+x(7{&d}m8)~#Mb*XvR2 z&XkLEe{W5??@!g^#V#lh(eU|7xzrQdb@xJhCuwt>GM=u6TE`Daze9CO;RUkIPp?V$ zdnsMJ#xEJ(S`Enx-HJ?ec%i+m*_w2A+KqZJH%f7(PL|wqL#fP|3+-|1k#$q4l+~+w z;7gawD{e9Dl#}qEKx3x;?+8#iR5T_O>5Kyks zL%n^G&Rmnuctp1}6vbPJmF|M4yOqU7quLs;NvEIDDMq=W&K*j(>k;F4R{NqAZ2j>y z=|*RCvO_MK!Jy&ynk4Bjl1pG-TfH^u#4|eespIyqQvH%nrKZ>TmQo@4h*ee>XYh@G zz9!v^DV-w#P&c_m3mrw3h5>h}z30}X`x9qosywZ|bOC%SmhdK-iZwN+qLr6cQ#Pht zgR5OM=dGhP>29ZVdU?=w$8LMPV#`6|hZkqk&8?KKTWv3tlI1wv;aF6r6aIoZZ>BTu zQ!xNH-CEJnsj&1=;VZ>*;=Q; zfy%qS#Wa}ah3~TQi)UsaFYBc#1EPiHNT5m;RlK;6r`{_E4kxs1!wTe{IBoSB)eFsS z{;#Rnr_!bSpr=;w*@V;fWNz6VYHZ0i;)ttzrhh^1#wSv`r$P2@rYoh~k9u5Bu8wUf zjwzw7UF?UPewC+@{qaX+iaKP0SK72;S4dx5T+r?Qw8w$Tr$cuZW&5xvX%xW?B=8UBzabrbI zyo-CojsKF;Jq_M=6G^*Ku0;I642B}vu{%^fx(IWZZb^p`(^H(fsmC+Us5LC<;z!y1 zyJupDvK?p*sW&t$b-&UVh8Kro?emtd< znvv<*LoROgqr~FkiztvAKbX?-({f|AROs0XY%yjo*S%l_8y`>UnjJb?REBj>q?5FK zsd{lQxAopN>5k9nq;M>^$0d6a3Yr;^F)n=i2i2b6q; ze8i~bubq0-pbb>zDkd+DZI$Xm@+@ws9vUbmR_sh?mR!Q8WU*4i39%tUin8LX^Lf)= zYMjNa1WcGa&;naw?Q&lh=3E)k;Q6?;RmnP7r(3#UMa_(tkZHD$} zvP>^SuR93lb`xr%=(Ro32}<2UVQ5L9ie$GIyZv&ki1~r&KyaB(TH(?y!|YV5mm7_` zLUmTva=$lV%S0Q8A)|v*0La1B60Fk1^m?h|x^04ns_S{;Y1e@#e$(4N@D-=&y)S&t zW|kP&%#L`P>HXLh5PvoNF2hsSncJMr{dxqTAx<+q|JXBG!jm8Gcc&O>pQ8ovVhru2 z1yDZ~eh!Ip8k2uYu^v>!(*aSSQ-`{|R;~qd@i@(|xya^=IB_~xJa&;STXbP?LN7|7 zVtnQPa_G?^!T2psD!cfs-qQxbfS%F4x-#^y?b?tfsI5Vd24&+XW(BXsN$3i)3Ts(WR%YW4cv2xEGFuRIr`g6}yD~a6O9%s?H7E?9cl|NB{~4mG&vO5VAIP2M{+BkhSbXNm;fcBb zg;!&8$PhyP_{ky5S3U&~_{?_nbUXYNX~g&EXmYq1Fn;yqVC!v}gqV5dg&SnqbA(qIxT;-1wT>P(n7l-4o;<)Div3npJ2wy{p7+`wnh1bEGLv-hh` zAjFO?6ZfgOW{*2PXUz7YSzhI6zFumHv%y5c0C z7#tkS{%9eOS6EBb`plHoP>gFJ@yH*N2k{SN^f1R~6b(-D19POY+Qt z1hhVoOYpn~F*CT(5q!B-U{z*``%jt^>}6&5yjI5oQkK_;n*|qdN`3qN16CWTkObn zr6b{$5nfBwbu@TuszHWUkF_}ps?;!+qYLbajG5xf9*jADr63cfH6Ht<*XB*Duf@&A zIvTts)gVKw$4(2pWyaMSz<8fBP&4AhX);4PDz{`ws@Cj`3w^1)$W@YfEjxJgBRg2X z)#F7i8IN(6F1WGGpkh%DMBal&We)Ci+;Oo-=}w+CFoN!_^$p&XYLKDTW3q@#O?#o3 zh1Lk~@{LMPDj4PVitM^ntuOL5+(kHHCF*WTPBg>=W91GiEi{#dP z6=pzz`;Ko_v4X3(Y*HP}F)I+)v4by4HOSEFaUpSBYT9lZWkpAIO>JTz{DY(Fnx#pG z)vUoVR;YKNh}Ye9GWT>T*rKHI^8r)7b$WSOHOTChHG&oE($WR3(OEHplG`N*& zkfFp$mP#Y*XmF5fkfDA_mNFviXt1AZkfAt9mKq`JXt0-RkfHKOmI5H_Xt0}VkfE$d zmg*ksXt0xNkfCNsmeL*TXt14Xkf9(*mbx74Xt0%PkfGW~mZBT$Xt0@TkfD^v%|4et zcwr;eAVZywEafuR_y!Ljq#9&+e7f2D@dpo|O*P2ygmbf(aHsMA&8=_Q*y-(j+4k>j z|J3$h*!H%|+iwKf{U5&f&+h%@d(pl6y)VA|@w@-m-EY0C+(qy1-TCO9zkcW0JDoev zfSZCJKl;ui^XMy&?%w|Qx4-xHpT0e~{pE+hd-&6bfAMf}SOszazkKV%x4z}p*WJqB z+B$gg;IAC44(Nk-?0<6qhxh;NzP4Z3KiK=&-e2E)cCWMd%^S*FK+&o&DAEo`HqcGZv61Y+lI}by7-sZ|NYwh0-&w zFOANBzKZ(N==`Uvs4tDqf4qwN(&+s1Rn(U}?U$~izBHDaxEjn2Qniu%&`{&!bVU;5rZe--tm@BMGDqQ3M6f9@*kOWyEruGlAC{M_$; za?42ZBJt+WUPXOrZ2t9C)R)HQV^>jM8k>K074@aD`RG;Dm&WE_UPXP$7yiXn)R#u` zpI=3NX(V5~Vk9qq?sr|$GI41nf95Lci%oC-v#Y2tjpRSQiu%$V_)o5)zBKOtI1}|x zUqyY%O8(JR)R(N}r>>&DWF`O4Rn(WP;!h3p1@GutBz-oTtu(gH^_Sa&F9kEvv}#n zkY4LWt5R*E4Z`|lRJR970Ogv9D#6j5F^K9k=UjK(^49ZjI+vCqdggk4gNxSlZ#n?9qFAokH!zpnJ(rdtV&$6Bz+7_YTv~?clxs=@bIH-Uv<#6b*OUh4lH2Ff zGDMwRPZ|W~lEZUp86r%sDGkgex6Y+yh$gwFG%%MOoJ-3PIdV;DU@qA|mzE()MWcOTJhUkxLN&|Dr&bhPi(I<6@V%q3gr(lSJF zTvJ-!e10>P_U@aF0eRetQ#7C7I2V>7Fyne%Bh2R>oD0hkgK<4!xcS_(=fX0CTU<|A z!F=u;&xK`(thk;q%zW+}&V^+NqPU(g)O_yi&xK`(m$;rV#C$F}7nUJ3;(Eg1v;6|`s zVp6TBzRj4|Pp(dscqUIAvEp(3cvhMAo3c`alHqh->?9o~k@=!e)Y#!F80#k%7W5g3 zqfYG5Hk_7U7E5jj36o|wU!o<5;p>4|W$c$(Jt zSWkpsjf~1y?1>qc`Okk(Jj0-PiYH=^O|!hNC&ESKi7*E|9)FC!P`|^!-(H;mk|Ze0jM}E;Mx7_0$vLmpn0p zHN9d_B%bJr&}-5YGwgvs|2*-s(*ZZTxKO4G&P#wJr${}Q6P~Z_iD=;zOX#tA%Ml`| zY-{M}sZeTpV;HWNaWW|5PQmH}kdV+9k>R{xO+jAqweyzYdVZ=@3vRy|>HbWHdwj>4 zt>kLe9me7;Xlu-3>1wqTx?twx6QbRo_r+u>%=)c`n`a5V;1&Sz z$Xa>J5DdDWdLs0aCuXpwSL})S6Fo8in)JjB2gKK?Cn7IH9)OUW^~C%cs@G#ZvC9_V z=w#jr^6bPD8!~~i5;-i+IkFddST8o^Nr23RN(Z|B?o=+^@0caUXIg%~?!fI}rS=Ll zYH6uL8STd*x?mPESI^_fLOn4t0M4}t7c)Yeth}~~A~4r&E4AVTjgo-~ah1d|>T+qa z-kJ!y4m|NR{-53Wv5n0iy7$xfzU!U|@c;MjzIXuL{ceEx|J8So?tJXdhc+MHS>7S< zd=bF=|651jc+@<4+y19-|I+R6yZt9`cW=M@@DqnWc=*kS{loX(`t@5M+4#*{AGr0k zx9%VO_Q6jaeEWgACGP*D{Xf5N?Bn~pdq1=Hmp1>{p1b!|d$)K0)$U*24R#}3t8o*S}VQsGw!$00;wPZGsr^LKyljKl5mW7qebu^FLN2aDa|1Au64%Ygi}7md97Ms5vXIu5J0`wh8?n4 zr=tXOSJ(oJR@AIuce;0tv>!dHTdx<3oX)|qP>ZpOJz0e`LdX+$S(@e4QH3eauz8`9 zzx(!-1Hb_wm^t)SR~MCeNE6*!k*VR;dST`A&4oDjdljfDB6j!a7t?;|T2U^}%VwXe zOe}jO>QGc13;7rtj-YW^pUF`&t<0NDPCWXNR7Ss4g4&i@S&iy_cGxh76VI>vR1(gX zqK}4{T$p(61>Kx2_uv1B10wxa!C44Iiyt}Kl(UrqHz5L5nbSRqVLLpUFXd1ICbphQ zIrzBP8&-3T!l-KJjX*G&Tr7_fu_ed3dCd;HY>pgoX3h|IZ>1a^UK*?_{*cU%R#s3< zR>Rz?QS=+p%prPi&KcB23K=PCfFAz&M;tih(+kUN`j{KAB-WZCk(6skQL#R;dUI!j zlI6Z87@D;6-6=;~5W9JG1#!WMpgc5Zj7w};)pL$($c;fK84ag8E_sQRzx`s$QR6hE zSZ*^hgQ;OvZg@ki%4zKo@|QWDi&>hR_i|jlYogmnDMuM6I(?zIaD1950HBL;Fs?Ue zkm}{wI$K2Bl(M3%UcE3qKpp`tY{ zxx1Holvjdgc*p4}F+p-GBq!LhR={m-K1J;&rMNhRfL#q{1kKXU2U8iv2AXtun*bQo z;eeb%JcYqfsa0-x5tReL*H*D>@`E;6YwaJT9E2y=d$<>e77WozG$|VO-e5(w(QwR% zb^*YUa|?Z?*BD}JJLM>ZJ~1ga%$%Jd2#31nV&Zjs?y{PralSU?AR1Z}`*Nc~9(s>B za&=EQ<%7L-%{E(^^rqqOOf?d+G z7t@jD;EG>tV0gXV5DXaG^?&b&`PTL zdDirn^X}GzlmpR`mAb?W`A*9!($c8j#zvCr_~>NRYm2bzOoqc`Fsn2hTfdWXK+v#C zwWFCQ!{t$RfK+p;1m+=&NieI{)K?VYm2&0LvfMrVbjp!;Dyq|BeLKXEIVz~bu_iVe z$ZS+8%_&eiquy?H##oFk$-Pm^k(Q3ORS^f;Emg3i!&^7}8O90A^>i@DO0 zH;Gj>Y+B3ClG1V!)2&Wc9SND5G|^n*c*_Q4{2P&5T<-$6>a|O=?TQVB}Gqw0a#wS#_6( z_BjV!@DOwg2p0=YM7$L>8hbp$C#`m?#K5MkcQIfv!sOt4QVw%5pisI$Ckj?Rk%A5` zG&rwZbb1_CH*r4>n7KCXPwDn#`>Rt9W6U8~eo2%uRxFq8`Es1&+ofsGkhDpg%r`4J zT8bgh>M!@(DaWLZm2Fd!!jVqPFr*eILD(Y6hKcqmsHrRjwZaP{KNyjF?@2itMY}eI zr$a<+YN64U$I>G16nfQOw>_we%$$?EMxgMB=O6v=DF@^(n|^ueklHNb4ZGjQjZUN5 zDfU2J5XxU5-9N8gchXhS$Du0&DkwFg+9w=}()9|zbnvgw*ik9;^Z`=~i8 z^cqL?M;tjtb2W#6O_9&*SVb=uj2;~F27qUFn3fwTWyYULW<+e|Qx4V1HRNI`?1;Q# z4(Vp8H=WfSyh!FuzS*u8)L<&$>UhMqHoqX{P=pH0HkQf&AE~jQhuo516LE4{Ts9$C zM!PX3`bjs>K+DaaNI6Ea1(%C%ttB)|HXlK&7M2Szd|WAad%?WFn&$_jQCu3g(QPQ@ zkU&6Od7uqi8UfpFUYg=Uu4@g9(O4{-y(w6*IipO{!0q?H>=6e__9hd$St*QqbkR2R z0HvKGFfd(oYI+?txf!GlwN(XgZBbU3n-*VldyNifpH+;r%d+WS9K8O`auKtjZPBo)#8un^S5fje1rh7Dl_ z*I?PjBccUQM7v4ud~3=;T6fXYO7e=xlQYvdsFjMxP4f0%O*tA7JwXRdv!M!}TOxZZ zK=5r>+NIWnn&1W8sEfpcgvI={aq#yaae&+HHeM#3j)hPQ4R#CK5bFjo02GFz2IfYs zW(PPTGwt49r5tsiuR{s995Ka-S}&QXfezz9!Ze)7^;v!qCK%UO>;1X4|2I+&u>3WK zv%XZAk^&qVbJUJoqdF1**8Xgk@8SKaX9_C@6A%94BM!MwIz~h$1FFwjF`JYY#azDA z5iOl8hvkl`594wN+!3nV&pqN`;Y5U+SbZrFU@zu{5P%7Cb#FedCa}Hq3QDiFnnZ%& z-~MYUN6ka$lq?9gSl8&%%JvGlISHo&AJM8)aNEy{Of(8%l6uFdA?XOsZ|eX}u_Od+70Qp1Vb3i9+_9h<4a_9hF63JP25SZrGn1nezSEM@ zZM$3Sb@0&~qOc(ow#mvITTJ+Jpb>L`cE`t-7c8fx!Dx^-^4srCyFs*AotYNunmB5< z+GK&H(I}`{c_v&={Gv@cOr4oF=ktnn@F!9Z+!QUgr;KZEnZ*ZTw=y2#^BmaxM0L6v zhs|(iz-6=yb;Qm8;St9ODJsPotI9LF;(A!WPl3WlR1)_VC^eAAZf-~p?GfZP_L!8T zFxT8w!h69u92&u7qDD}ZuPYi~&ds|+k!e=UE-68ADDD4`DF;mTil98ktjrYZP;=-H zMh$>e)wKI`hqD`&j#is=C${{=I{4cuM_%Yj0oIz=!}c63t5j3$%!X|v9+J5}kDwmF z%hh~HPAc8~M@wzKh0}dCU|JDAk|RkAO7xuNCpMDnwsSC1D+^Y;QO!f;?$Ot!GIHTC zZVdyqpHswkGVvCR)^fOt9Jp19gO2T~m3k#^=hdXK^XTj5$}LcWVo_J8EwK($L&DWb zUaB|aJlAGBC7tH_{{Oy|Bg6S$PX|21`CmQb@G_kL-;{E^e*S;`{Qvs-|Mm0# z>*xR1&;Q5n>*xR1&;PHV|6f1*jIL+`xxu($n7+uyhSCqb3>A3Xe~txp^t z-}<#%KYY}@^}?;Mz5OR|-M?qv{q8#-yZhCfAG!C_ho3l&0J!~qpq{|rI{5b6-2?Ri zKG@j*>CF!uedFPW_rGi3+$Z+;_Fla6p}p_k^N!xO@!Q+qyY-2?FW!FlJ!1R$?e1M~ z>zlXwcdDmR2Ty(f-`Lo^zX7j7_E{vGzrOJupY_krlV8uy+9Auv9&QaP<-Q#&1$HHb zB##r?)JnPoTPr1#ZpB@C6xvk}cF#D1q~9mKeib~t=AZ}@*?!9#t>{70Qpkpja-ES9 zf(qw#1|@H$Z=yjtS0-_(Y-{Rr%8S)1J1fOrRaIx11`jqOFAs>#imY|sr@64?2ubobHMr5a1N>iLYc$gLb9h(o#_xo`Ea#PNLNf24G z7*+gySHs(_gFl$cXm|0ZSzm&uo?4-T%%$1F6#9}|>#$`+wFEkBB7=6qFJ_b7!<2*N zqj+XQ8fH-4=`!(FT8}Iv1D~HjQ*lyL3|j+rIy{>557TG9ph>_;WJ^(gmYWKG((qeT zP;LYd6Ai#cNbMoGZXM-^>}nzId}S)5HXUfRz7%LYsV!pO8%G>f7RC%u`SY1cSM*Uh z;-tQnSUW$Ra#RVpiQ-#%}KpIft#gPy@tWdphMoepK?_A7;PYxR#MAB&5EpRm``Bk zpqY3eT_R5)Qcv*$VbyCj4nCQ3l-U|lp{7J5(I#pfP^&QEf^oq`jX{eSm(65e1sCp! zVylnBD?&+MRgIP*uzlQDB9|*Fu_qRYoKo&PH8}{VCAwNoVF&B(q`@oU05zvG7*SeP zWX>{FpK6rhsW2UJAeh`nm3euFnSB^$r0!n&RucAV{o0sVLTDFHVy)j9Vn|qRDjs6x z6}aq5jZP_|X#$H{Tk%;xLLLMIL_AuFVy*)o4SM{rC2{rIoUKmLahqAV<8p7x@l|!X z^9v~lH&CM$JPbR%#2@y@k<-@aSYul6GkK@nfD%M&^gQrb zs-xPHC6a!Q9+(o3u9OvsNa})0LUA#_$35b}h*?XW7f?QO&{BCuCN}9dg+@cjHRrbO z+Bn7sL%%@gH@-RLXjwDQq?#Rq8_bj{uPo+Gaz0!rL$Ojzd=Lg!wF=cxLj3N=KTSE9 ze!GkCEgqXAY@&hB09(`Ia8@^yQBBnQT*)!YORdLEtnHsUn@YlFK__aIO6S-@4GCIZ z8*e1=)M-tHW)F(!Iua>Cyr`Rw`b;XKt`Rbn;Q8iiXp0y*s&oz1sMpE?lOrWc9tTw= zh-${HDQ=}RVn}q_gULc4;bK{sa8?d3<&?a$Q0x&`ALg{$1RXPhL5ypS&EH66Accql zab|3O>C4oz8d2P;kdI6CF5a^^@XT$MN+mPIig;@`jiL&XT${)@%UG)w5JP`FUD1V# zvZC3Q79pqr9A~nuC4vq!eaaeyBE3`}iLj8EMs*ez z_%2s(EC`00D`jm^DOm-pXY}=9me-plsCW*)m3!g5-WOdQD~TO?DjGg*fM~VJ2=e)!y8SgN z2Z-~o;(1dWHs$CE7#uXZ_#p3~{&c!* zF6{cb6c%etN zg;kS<<-FHot6-ififA|-aekR{@BX<|2DMoBThOe^jH-$&vP84P5>lckN!2!TrBzks zntC%KN~Ou+52P}js2x$T?rLmtZh$FHR&`sxiZ1obP(e!!*t(WJOi4sZ=Qk&26qco(u~8$#~Ws4XGgu zu?q-wx@3Y^Af|NmzolQ7AYE*gP%=-Ca-4(_lkyl;AU55(V!AMHFrsG$l^NzNh{>(@rW_!| zXRz#dyJMk*bz#!AtK$U-x(OqkDw`v>PJ;L*d_HUA{=q*^hhy0uLyA6e>RfBpX^gS@ z3a=+!AdZ-3EUFY6&Hm9dJAX>5u^yj)Uh#!%23+MLKw`DmG& z)F%v>_PNDuFaz-@M~8P`yq=86MFgqlMSR(3qyUK~E$1+Xu?vd}JOv^5e-oHh#zSpt zB@o3}OQQ$-n{ECx~Gn5K?jV4$@`Ilu_ou%vFT&`mQ z%Ack&KQ_avLkP&Yyt2riQyXB0Bq?}BZ7C*FAyKGYm_w`D36h*(Lts_ctAW(hOUuqA~*su~ex}blcTZ zpyk=wq+@6&JZtGnt5p>mxKS%wVT&4I72Jp_-L-NZPrIIaV&NrE%wSEg*c0I=dLs6k z%sS41TlV?yiD%#pPw~XlGUGS5CSnl&#Qc8;TuO6hX^se@$>c%COH+5}?G zd9~XrC8DWxiy+MxXyqFdM-IY;?mzQ~zvSzmd3kY*vp?w?I8}hU7xJjBm0a+rX9fx< z%dc7_*6=q3q5mC_zN>okHHn1+o*TB=7OW#LPkj`k< zwP~#-+T%hHLu|!nD*d{zjw>~xSy>N>{**=CvBS#FjA@S5&W$&9G>Qg_L&UU;oJAh2!zqqE~~+#po6b z@|3BLK2*z$SeRSUeaOC#g*~qLQl&eEHd!stbkrspawV-WkfGR2lfv!FlhqL&aJac5gnt!djBK)#L z2Yh;3zq#W-o_{j@UrZtnt4jj7_zGRhmg}K(zs)D6GSU`AJQ_AJGVT?c=|# zw@xAfuOx)ao64d-_xrL@ZPARgYN6Ab=jcS;EjYGeOK5`yYGAYj1=pZKQ^Yt(yTz8$k6py#c2cX znxbB~@AP?LFeexqL~&0bryQs?i-IYD7S)caWiSwVNb?+f(J=>ldnW7l3R@~-k0_ej ze0U-*?+=Y6EvYqG1}-eL_#&N{K2G!@x3!K2c&b5$R*!3AQ=M~!me*>!nZp5s z$F!aV^9ejhfLRR$3y}y{gF>-ASx19HssV%MT7*_+#vOfD#57Xz0U8pnm!m>hgk~|> zl-Ng93YJ)X$dN=~>G z!sWU<)7f9=9 zfTkK`X!RIpsIp&n25t{=h&!fy4a{T!2DFr-(DM$PI_4)U!5s^sRqAcs)oIvT*K1{qpCZt^@pltV^sS}6%b z7j9M|*O<7AdD36%6ahD<1v)XDIKbAj11Qx%jPx!E1;e_7uWYWD4&L8z64+!}QxAH0xH zHOSEFv8mxq8=W=KIT9I7Y-*eSGC$&b?IpOg5KI(CRU(5X_il z;L@NFHG{TagKCx(P8~NEIWd_A#FXz3i?ab*nXIEhF4Z7ItH%Le5fC555?48CJkBLr z2`K?ED#s7&#ZrCLE_#VQ6RJkCT%$7UgBSi#szHWUk9jsH1kB9T)CMQbjIJ&Y2O|re z(nxd0cuYX@t|pkXWfYU^e1rR`1{qpChH1%!y~#4R8vxm8G#FM)6NE8q&AO)5La7Qb z^AngS6$10u(csHd4KlQPOjN{qGl;yBJ!qO!a5YCbtnJB=sgFT|4@g`R0KDR$)vC4D z^2R@yYLKDTV^gw5JX{x+4Kiwt>&~dpmRU@Hk&0BZ@e3x{FXsJ;5&g)d7r z$k6KX7#5XUA6bl;m{5bhR|=LherQ-+G}8QhGJq#_1C>G4Z&g}n6uvaoAVaIiL%BAt zi=I`gOe!_BC>47-f*+MCj)WA$NrOY+B4ZTIhLl^&7v7g@kfGIMnXS5nh!ke|OQS{wVp3(uq)WN7vH zM(bric;P*%1{qpCzR?=m4_^3^RD%qy9^YtP?FTQsJJldVtH(E5Yx}_q?@Beu(CYDx z*57{c!aGw9GPHVpqcyo7yzq`xgAA=6-)Nog*WdqN<;#oDfq3xf_(xy5UcJ%U$XCDr zLr;uzEWS4595YmUeyv`w5--1Az0p8Nkb`*kbn@6o$Ed~5;Gt)AKnVd(na5ay7;=74 z@%W{|uUh`RmrR;y;CM6t+Akd^$ShV-0cnciq{M?g+OSuz(aG?{la`fa*jPlS*e!OQ z8D+y&jqP}|Heq<7y?`|jZ&@ri+s#n)z_V}>&5uT@ukd88wJqmhnq{)(96LQ&~QNWV%_t?CI{^3@Um>sObw ziO;Pv4FZAHHZ39@tFg4M?TWp4(wQbP06u85W6$@nuzpfg$M=(!~Y#^-Hdp!JJ;P zD?YIzZt=BQ5jUJbMybcjgxMQM;4!cihCQtlCulq(}#DGmrB!f9qGqU# zP72Y&zHgQ2xKdaeCzbWy|4+LPT=5kDzp?p|jkNmT0hHGN`%Lw}&5zvq(47e2@V(*a z=a2s8(Kj5?M{m9Ti?_e`_Vc$px8Jq>S=ImcKYd#N@3i{g);E7v{l8C~*Z#1W$+wh@N;l5{{7hG8RWxIXYy*k#Yqv)xa2}+$jweQip>|6QND7i?51p#ibGw!Mw0OHyy~H0K~$cp92YNvgF?>q#PbQWk)?T z0u@ubYT{O#(X?Pt;;Jvj<*3sJ3D|}|sV^3+e+NrBSbhd-b1gYsb`y8Ofud`3VrJxA zs7_MlQ9wft;q*;PB;7mjOF48=NB{zp)UiO^LAj?Oy&BSthDJA6iza-(!E2L6Vd)x@a@0;%BWYOIR%Jz=Rl=G;jTt!7d;JBYCCR|OlUAG?LB7(rO*PB8Uac$5&^|rXVuADq266PC&t%92iQonYeS zH|3NAA)L5vk7$+AI(61uuyqUz1-^*7NLxUP1 zyrLh7O&iBYaA?QflI<>avm{coR_qpy##>Smd4>>*xN23y#gNu>B;>9ZmMxW)u~C_l zA_x=aT0$bNG~zI=>@teX3PQ|E*1Wr%kFB{N#aNfby^b^?DYEUc$+AGMO1Rsxw#H{N zMuF8?T8fHbDuC3(C{RbS;mAu&5=h0FElJyK+2oXJd3|yx1=Je(qj7ss4(X!qPtm5_ zk`lU{A4H>y?!wXlQT0+s&lPIG2Hu>Cm{(*O)=Mi%EHgMNjI0W?jH?qaSAeyI#n_du zw?ow^R>fT^<#2O7p5PpvvAeoZEtLz)`WO@_Z&x_WRO-I(h9L*SCq;7d2(4`7&%OF>$7fkj_&+}REASkI`&{eE@8GCBn`dH_E<4u zf%VH>w?=!)g6z(z9>6o&_-iSL6I#%8GY18UfB|fr94^OsM1m@M zCoV`wX}yw>-WkI-IcmrQTJ*J%zvvnzfwOGA#5S9ySRo6bq|u<@+mpi-Txp~eUad+f zm@24KZp32^B^Qr!Ba;$JwqHZ0<0*jT6rzSbx%FpKtyIhd69(E2HKK`6X*20Gosv!T z#Ce$^2;G^_d`dEO4lV6}BIS^MmWdhzL#G{%GEILa_lA;a^n!j|sZU2kL6BwJ5EglV z<-Z@pLs!qw2n&){g#mbrIc|L;1p*4>8*{yaQTB7X` zZMD<3O2jEBNVHL-phgCXI7LMbiV&yv+vijf;1SwekgtF6eYwB)+yC=UowfEld*17; zz19Ro*6jCdLK-Q!1x7`yI0L&WslGWjSYLU+IIu6ciT!IJ7$_%)hf*S0?L@kC8n`y>JCdDR7QNSRqmrbYLQ61fT z=lVUyX?wcpmI=|S$cu&4rs{MVndj5f^t3#HmE1Tb+PQf_AerM!YYGq`&jUfOn#{am zbEyHD8G`_KOr^<-x#T1>oFXaI_xR*=FgRMfD_!-tzJ##os3V!^B0q0we9g~^IT+W_ zNxSI)_e@OzokB%jiIM9(dS%0Nrp2n1>EoIJS>19K(&V92b|9SNJ1JcqX)BB48#fdPqqbJ4(@W0OqDPj?f~#3cc98YDS)+xaq}&>=_)5~K=B71#X=UrS52bs>F>37o;QBq1tudBVim58o?{US} zf)Yn7Ljf_LyUFgf(O4OUlsc%&HJjMqRA3U4P_5fw3@dL}#Yu8VwZTLTtYjFV%;*pM zx;f2FfPodXICw*t$I|aH>7iJiW^=gRJo=)dq2OX zP`ydIomNq;+P3HcBY-eyAVzQ}2Q7v?x9n9lm(NYo;E*5|iu7c>Uv zIfu`-r>VuH*Hny6z`&(M@+GL3BcvMVm8ra|^ z;Hj3gq;9T4*m}w+^*GzYn7Pss$66z@cJsZ&q%6`aB@3paeH^AmsBnqmQiV2h?#Tyl&}SUTj*YGTJwxSZC^m7Ky*uk?l) z2ly1P?5{o4F6VuzTXj{ofGuXzOtz1)y`hB___PZtQ`xfYZrLP-uG=9Z_&Sey353$s z3BqGW`3k-)=QKvmt%mbunqJySV-76jn?kKNmx#mll3_Vzdwrd&*Os^`tX6q9U(7-X zr7=7%x8R%%V*fR?Rc^SFm7TY*^O&ZxqFlC(RhdsCRk=(#i#%g55RF-+mxwf(kc1~- z?OLZ1Sv#CvPACTqJ1Y?7+0yA!d$Qt33#TJzT+_(+ot!%zAnnSmq8EkO@j8#O%KF?4 zX%B}ZvNEr_5GG>^ig2lkRqND7{RzjQ6{O2#nEiE_&*ix0h-gdCTkVNd>nB@WuUdl} zv-$!o8#T8r)L??hf_HTnhc8~|(MQR`FinWwqQ_g-?Mp1k(t_`-WWRRgg5`cKsXfO7(GIlSv| zx{d}AeM6uD`1t-m@>2;S@@IkP|GioKH$UO^Klc55UmsJ0NCVO4{(JU*ZvWohvwb^= zHn98F=p#Gti2lURKLQ8+=l=^|yg!-{lQAD18d(L=!kHMS4bzm9;kagr##D> zBU74SEO7n{&j(8>P1bNzqNX@P<87r;T=hAFH%_jEdGy#>*6JEW&wO@mvdDI z<*Ou!1E1?69QY((2IJ}J+Bbi%Qtyt-X5=`HNt6(u%htcXtG@4KrL<5lV zGK8MIYb_PfLm3UvG3vlov|nPz&P-nUOl8zsloyW4^-NWA>Pkx2nUlA!DIg@(wc4rr zlryvnUSU$E%QhIbv=TTT8GtDCgVqw{wrVpcYybcKk?T~!x^w1aL9Ban4%ZYW>wstuQwotEgY3*mh&(3za;Sh^_PJ zjIrW!nk`O&V!^1leWSN1<(L`xe$)Xz4pI=`6D2fH`gkF+-rDtBuz{PDHG*bdhArTz zUtWP&=!0r@Su2W6EhG4Px;YuP3?{Z2u9|#hwn`UpzG4ns!>_|`6L=@jG<%X^dKyl6 zY8f1z%ICdKY}3t~<>a6%=_)vEqjl@^K}~Cy8z6#qMqCQxRc6}A4A6E(s;`Wl%}~;i z-Bqn1wWe90Ab4j{;W|29=?QJj*P-p2 zK7ezBg5(fUYeO*w>nNJ{6}jt4LXWT2*fKrKboWR|K3uE-?@7H-$7_MPw2rMsj)MS3OBBWkb zST><_y#*9?qZ~f154%f99WHj)u`Bu|Et_c)+{)@#^9kC)Q_$%Yv3hnv)Jx8&RJ6O9Nn@I=G(d0)A@-4Q6V=bp z=KUh$v3iX}e8;kTa)~#QL60+AGEYxajdT(BOW9mmXsll&qZvJMSTYK5&4!E3g*9!W z{b4n0cSUI=PR7zKZIMNwZ(79HVT}8kyvwE4G?;d(we-}h)f$}BqKd7V)6DWq7_U|+ z$1+Q8suF+AI*)YP^qmTglMPr?I7z35Zdz3d1@7S#%4PHk(vsL@zTj1kKDeexNu}y& zigT?Q>QNOkyTB*8W~##&EYqKr=zb0#G>p12cb)y|hT`0Z_TnE|Q$R~3E4RcVrRbDW z%_f=E9I1CQGNCtDC2j10Fa-`hY*w7c?)%q;3!z1|xGD~q%#t7G2P<$u2^MkMoz2Ih zD(4&?@1g#vt_hV&m_^TL{pr~0)|84eFhpXN0V@1vb`EsxE;(OyMszxp9*;+*8k$(| z(|Vq{ns%IY1|K&hR0fI(11ya#gj~8Kj?SgJ1(P<|IXxRL;>mR$E=~Dhv(@nHoR$Mi zj44I5M`b;?(!HgXtaXRQSq`Y}vJ+_UJ!=ZbC#G#_rYV_{woGw(O_-AmGFB^wXLP$u zNgmC*O<;qiEq0j=1w3xG)KZ13j^T^}&&@!ymlH&>%G>h5FLYO<(a57yxk`dtQ<$}- zs-s4VOs&!bc!sZp)MPR0mRv61PI0^=my#*q%C)|z?0@Z=LPs;QyviXttBW-pEN40W zMX7<8bxxd&hEtvEeym*m^+l!UhGEdpo_+#;NYNp6mHK_mwgSF`0& zYP9lOIC?0oDJ0KPJ(#LaZMRTm$hlQ#R&{ain{cv~CKf4MYjbFsP0CJuJ?i$PYI~T6 z`;=M~4WNh`YHF!Ff^m*eW_SUgWtA)?wOPk8pvYU+c?d<(G6wAtghKOGtNgSnX8BpMgUeQt+5N@!dysCYMIvY^IjbtBBUU-^0inbX zVZJaQJJcj^s9kL^Du^_46yH#&^Rzm!noUMp&aC;2A_{7=JGX{y7w6i{3>%m&(>F|U z5M9quJ#0=;idUMoW{t{-RThAwaKPkp31WvL|RQy@W|1xuBBSILw(jMA!TGV^su%}HWrAwBC9~N zy=psrvEt(ml~0Acb=4&%C1 zT_2Q7_7LtgoalP3)Em$GwU%nNiJUp;V1$yEjb5Lm43)R)^n3)v$uD7pkIw^I;0Ci`d%+-*nync^vtxIGvsEX*R#%eYv3JVt>D_q`2hJ@*5 z@{NV6v!lAx>m0s$P0_)pw#}DSZm3#0#~oo{Qz&aO)r6C2vOi zpV8A%{tMI7QP!808ry4eMVvw?l4xa?GCi!-Nr7B&LJOz?GTC|`wnvom`PUUcz0-kS z@oD|@Q#~D}pHfdpsTA^=yP|>>@|`w8QI?}w)fvkwd8l+nE%qQnpO>o+WLFy#v%HWB z#Aj{ksO77eW96$(cACO8bT|d33|g~4fQC&}1#u>q9kkLf12q;+%z$Q!u$y^jGKL0A z&#PC#X0uAUeTL_|RKAY$nI&bRgZB9>9m7{&cg5lpU2%)%^f`CM&lnOX|AiS62gKLw z@xbE=wGs(9-%3qti>8FV36Qm(kmDK-HyaltMs!`PVx+bufuV%essD`Q~v4|p1!{|{e%-4zQ@bj2;2)92h3KVwK7U<>_aFTyO7T~RhAn?W#zFMiP_ zSCp8!0lWzD(Vt0T4^$BDfA3V}4 zbw!b0DN%-jG1R|5P$0X~H~&`Y17Z51+c8;@li;9W9)_UQ3P#}^1SQj1=o$xeKuG)= zA=BHq>DgAubRb^g!7%-;Eri6b5i+%nC!cMFOa$ec{j%$R3w{iBft&nga z-fP|%rrQX)x<*K78-GCC3JC?`y-E$!FWaIdzw{a*uWaKoXj>t#1mfl29HwvGLde%& zBjhW$@hY^fkgp8HL-)h))^UgmFuG~3%k$S=M|$X9IRn`m1h zUlE9x`o}Q+w=INRUL)k?ZQK`aE9B)sJot5C+Sx+L#Wh0SwvC6QZH2ro5bsJT>XpnE zLe8%d^5xq&LE2Wxmj^;7GhzDbErgt1Bjl~y_(|GU$Xf&PZu^!n{q`+{oL(d3%eHZ) zw5^aY3&gwix-fm-7D7(05%Q&%WU7oeWdbN=X5s>qWO$6!=U5@7RpGXj%J# zwv#iq74oHlcrU#^Okc2tkmGBFe90wn0K$V2r)pw7UzV~Hf6#Thc|k$=Y}FxvCErx$ z)lAWG($2O*z9bOu(sRS~C0htNx<<%Lmn4p}Yolg1SM%8Nl2*A~-D0qbj57w;oAj|d z-e2TOt6bG5wiWVHAl{2sVajeHRUD@J zeTB|XYm^LixeVHzZe5bM1mZo94O4TAlI&e0AF__gphfzj6y9yVnT$ z!b@9xx!6|77Y5=z=gu(wrY(f*TqEQQE^VRH-B!pK1ma!)6Jh$ZTL{^{M#!5kZK3Vj zR>+$I@t*mrFfE){NO1mtmx?~TbNcw{W2cXvK63i-=|iUvo<4AT|LJ|F_nzK!diUwo z)4NXZJatdiQ}*zcJk=SBPS1^JaqEl z$pa_%pWJtH@5wzUcb{B6x$ET46BnE&U{CHinVL<6KpeNADttU61+;DR7Byq9> zA`d)v;n53^TzL4xLl+*r@W6%pFWd*t9o%!_?h98h+;!p33+@H=0(;?(3-b&83-t@P zUqCNF7jC_9^MxBOT)dFDuyg$Q@ngr20v`nrA3t;i9o>3#^U)1Q7mpH0JBN=SK6d!% z;Uhq&@zCLehYuXye|X>Fy@&T4-hFr#oLji_&^=TS*~2>y=ZF2n`r++|=pl4?>*39Z zHymC(OdReUJbv&Ph=cIR!NUg+9XxpOz`^|o_Z{4OaL>Wr2Uid7I=J(|Jx~wWgF6o9 z2mOQk!R-g=0d#Qd!OaIZ99%p|9PA_>Pdt`*H1SB{;lx9U2NMq@?oZs8xHoZ6;_k%N z#9fIy6K+CHu!%bo^F%*UPu!kB6HwyT#LbBt5*HJR#7_M2_+#-$K?H_};}69jj6V>+ zKYm~Q-uON7yW>~mcg63FyKyzn#_x#FJF&-OkHsF1 zJra8;_HgXM*aNZqWB0}Gja`l16T3ThXUqlXFxc2#u{&b(SUuK{-5x_@Q0&&&&9NI| z7h{Ro&i>>3kL^FY{|Jcv@X-E)`w#5j541t|?%%V2_x{!WyY}zgclXtOcK?q3`F?-D zzJL2Zx)1H&x_|Tj4f_}O6Z<>S$D@x$AB{c|efapn;|GrKKfdqy-s5|Y?>@eIeAn@v z$L_Iu%pTuyJU{Lq*N<;MMvtN6TaRx(zTx=dapHLA=y4E>;?bi=jvhXG=;*l3dE@mYy43h|kVe2C9TpdntD$c1<@kqz-QkqPl6frNM=kq+@V zk=h^};!y$$@i1{E#Dl~uLrf%+A;uH02r-tp9O8cBwhg{K#AxEy5F?3~g}9e^X^6Xt zmxQ>JxD;UQ-x4nl@qZ^?6ym=oZVB;U5;uqV&xsd?_)m!!g!p*krV#%z@%#`!mUv!> zA5GjC;zttC-QW!ye9i`+9pXPEt`G6=6VKY*f93|CvAKU;h<}&37~+Q$ry)L;I0^A@ z6Bk1K>%?)0f0H;0@k5Ej5dSK15aM4Z5+VLYA|B$SiCBn#p4boZ&l1rP|1=Q^@lO(a zA^veyhocZeT||7(bU82^_L-yi?y5Z@R7rx4#8|HlyD1IF}# z{yp)Jh4=^YkB0bg{2xO6{rE>h{Jr?!hxohkzYFo*@ehaiE-)hm^uH7T+Yo;{{x=~$ z6#wfGe=Gi>5PviNR~!7x5Pu{77a_hA%qIc;|BC;4h`%2Hvk-qR{-+!KlMViHh!4g; z7~(tPe-z@cg4rmbe|!7`A-*mChavt-{QV*Ra{PTEzBT^d5Fd!YC&XWh|3Qeq2BF1xxxRn z!C&9tuWj(b4ZdT8zq-M&v_9~-=Hga3VlKe53d-{6mJ@V{;FM>qH*8@zXeKfJ*o z+Tagv@CP>dUpM&u8~nZv-m}3sh4?e^?+x*%jtlG@N9$MvcYfO z;5TjX8#nk38~pkW-nGH6+u+x3@IP$uYc}}R8~mybe&q)53@~0ye0hk4#2Z7rJ@JMR zUzhl@4f-LzHsOW%nuHtTs}oL$uS(b4Kb5YHYkUP zB%~122{FV}LI@E~@F7A8ZiDOwnGMn#q&7%ykk}x;L2QF}Y;d)~FWunlH~1wR{NfER zL%fn$g!szDJj7&T7UC-s(-1EwCL0`wcw1r=;>#1m5N}NkLVQ`GAL2_By$yCld`Y4c z;-y48z}Sz*zhZ-7{eb_6^#d@hAAn)~01WE~U|2r@!}jz+1KLEq}0T|W~z_5M*hV=t5tRH}3 z{QwN>2Vht~0K@u;{YX5lpV+nHYK;$i*7z9$~mPwcznVg1Cu3+zAw`tOW~ z^%J{09@bCnKgYxRiG4?WT|eI*Z-vi&Tf7@m!Pjl@wHtiR245ZGH^*NU;y1;=D8z4!=R^Dkuy+db|N3|?#Jl3z z5Wg;-3Gr*=$Oh9P{)c#KgK&sn19n~k{a44Yg!onQSBChN@nnd1#$U0)%OQS6{I(Fk zJpS?!-w5_?0sR}|FAMR@;x7%+kG~{DFMcURH~!)fo%oAFwBxsgXn|c{KySuh7@`q> zL5O<%rVzFG^Edds4c_>){r~67j^(M71K9u9pD?f5Lg)SOTTT3*osOqIgQw#Z1btz8 zI^IGR|M_QLbv~oxPxW-1eo8$Zr!pxd@M8X?mX58e>({8ueT;E^;JIe*WSvIW;9Qk% z0VhX{rlrPtLmI(Z8yMUJCZCOaRm{h(%uA*IKxsLmXHH*+YbuZ>pcY{lD*zDL6+CGW}wuX_8Zg^Mzr*dS%$+ zX3bg-n+RN`-7O9zaDq8KQu3-fo|n9Gy-ze2S!A56jAjPg>c9r@f~?8BGU_n7^0|VJ zhp)cwiq$8&;ug*6bMA_0VC(y&T@n6*G&cu5e#f$*Otn#Mr4?;hwNh=o`Z@UjuUK3k?RUCX zT`?8|Wzp&am&UW+1XZOOqrO_89~fIU5MGa8!G!x>;ti=lv=9~6Oq$;H@}6$5hI zwyhPw;WdmK3=L#_^`#-(e>n$R9QKBGcG$ItJ<4e@MYgE{*V6NBb39q)n>N{KHwH)! z&lEsjS6>pc{pWMArFd|lV>z#>WSh_N6R1LDVJkOnS)4fU zcWfj>gCp^9ze{1OcGWGWRfNr(s@u%K650i>x)o&ZjIX{ZWc&Cn_1Rd&x-RetRyK4~ z?75R`s|>>njS0;aR||4hmaBt;g_n6qhLZJGX<<*YtUS$hIk9Xa%=qdpA={6ggDp7d zmgO~bWI!xElhHvEcw`;}KP4S!(gi+3Wu9Wxu1=650t(^gknKm$!M4*&>B9n7U1WSa zOWU4bP`UJ|+(`|K`Egnt&KAv@o&ug81z~*kg(2I2I0suYsWtLBQkCFgeZ?+x8X8C= zu2`=nDX7)3tjbibG~ihc=`_YyUl6kW$T`^dfkVhqh2ncG+;>_qRM+f`A}l(LV=Ad} zXOwUHypcm^u+kY{y(wh-_vc_+rFjDE8%T((QhYKebqTian;MhS(xkSWkA?ICRY=&H z^gxw8KV?eq;uAMx z<2*J(#mj!b6J;jt~i`RmW_AF zfoOR6ILXzuvD>iOj++FY(!el&eaQB&&cT+3ixY_~cs7ld$9SnkQM8C;M|mzk@w@Xm zfzc_{1b$%0Nl>=W3fcbUIoR@KuH8eV2BM{Hu9aa$-f*DRXxekAB{Al^=|*WN@#?fd z5#y`R4B7t0IoL|6MneNre245}y&PI|)2nP-?wioOoy;iSQZAL8q@5+&GiZGE86n$8 z&%w5fqiwp(ORah7Q}grX9*rf<*uk=Fi+96RUehjaRGQM z?R2Ug(7-N)Z2$NiZ25(-8l~!Er%|yxbX{J}+R0_$a~>;9*^X~^YZW)u)xqKj%)G}T z+Yg?DEuEU+)md6!wLO$DXKKN8q|uTShW3gyAb*}>>KuZtlzJJAU`HX_KRO57<@tSz zkFOqvY#%uXTdZ4F&bhrC-O#a0%xXROo)eU-+vCa`OysS_2-O zRzK?uTsOdk(f+#Je9~jfQOBa}Ks!1hF4qjf8Cf;2dnXh)h4ex)-v2 z_#AAv2sb~zx*M|n{d2J0B8L3<>Q2b^_s+p~i*WPfv#$u*{_Z*0ZgHwub+jpOX z?H1>d#%FH~*}m%>Y`6G|IzD?t$o6;6!FG!;LgTY93)%kmIoNKY-44G0w|3sPbNWtT z=}(?QC;tMh_rDF;>AwP4?ta_u9lNjCc^rs!u0QzdUWD4a zQ~^<0&@PRN1BZ4*5d~jk*q^{xcLSO-(q!{3t<(_* zOuQx7}9M_2bF?@9=pxMIPZtR2b*;fQKh9+WoZ86DV zZ4j8I=%sly@0T3KTYv~1MtzxtDk)p#DYwkT{s=z%@_=SC<#Vkn3<)W5TyZmXuw@}d z#vGJs+QgxrEl!u+{9=`M@=)v%`0R}Vjh$D@0}}>et0u^B1YBIJkXMp=Q)q@!#?)#I zT0P*TvMA?kPV58l*&70yZYD`URM+;n*>FXw=c zHi^9tKJx;ax|_o2x|ZzMb+ps-i_0cjpUraIkW9~TIMt(DPSYRYY9?KYy%#=n0~(3T z@Wphm<{RUbmC{lM=7R7V8Cb3Yr?eJh7DvlfrL!cY@eagYz7u>+c_KV<2Y-%GWGs?q7qOs+H-+2k~dw%W;Y8mnLh*$|8BkXtRfJSzxD z>|yxK4Bsc!GL+%4UdjUB=K z>v@?G23%M7P2jHA$j@3L#lpt0CAy2)@4{zVKvS;PC#?wt!<|90(U2M?w5!yH1I~ly z#;mlM7biHg(12>QUx>XMK2rmlT(v;LY^LAIbPQk4l!5N3qZ$_O+cu?lBvhRn&}=+0 z5u^}%7ks7!G>vu(GL;M|38fjuO>G*4oN@gzIT>0tQd>0Zg_&4tH#`LNh2MeC zL#L&IQSnL(-7j+;pYAoPWh6aCQ&rE)*ff>miw;vNXPMY=Z<4t5KTnm$2rF ztgqZovR2Ad*d9jjKtmvpD_Uq zl;uIto@^dy5pzpfbjhAnOfsDgtmY{WM8VATyAqcI3KFCOVq~4s0S$@|F&em4o|1+B zm|x;Fngt&1MK#wT3YJ<(A#LBPNhpAZXv^B7jVW!a4_rc)%yF5b1P~;& zwZJV>E(A10K(mF`IH17;nk`hqu?OKZETGv!-y3@ee0E1bvxPD@_N(yODxldyQyY6b zeDiTWCjPZ-vj60nHYw&)5U- z*&?9XLa!P7CHQO}&}^Z!jQt{fHVbIB&`8F90X~}sG+U?}V?Pg{O#+%NbcwN_gU`kR z%@&Hn*jwPUQ9!eW7BKd+@Yyh+*+Rt^yB|Iq1TZ5?B63}d+8wzO30nHYQpMa(m&}^aA322G|%@!(~fTj@8Y@v?{ zXl@T^wotYNG_MP2w$P*mG_MV4wosDw(>&{(9fx3hQPL8@vJw1r3iX?JgD_lRa0idB3R$>yS87u>l` zLrBw*UD2mKNF;3@1T>*EL*lHIqvz5xLAWA`!YN!rqwfz0PW2Rpk(#TpX%NX4%SW0Z zpNk%4c)kbG;7hz=SuP8XdjsAUNn}J|+?1gOQgQ`BvOU|Ad=|ApfcmUrDI&rUMoJ;1 zNExtxrYB1*Z;GC;WLQhVRmI{W%?K!LxOyCv(zAl(IlIyLy3m$D&$n|D+&g`u?7{*B zNpxpF_!SiH-nvjz!6Uo7(Dm0p!Rfid)wk@fe}J!l_LKjRx*p`X$@^0%(id-d;)fsJ za6vAHkA#AL{9fkbjn_Q4cN*As!MS`6Nhz4*8qpsEMH@dv!QG5z8Wt7(@t~}CvlpLt z4e9P~?&9-5?H+pZrcb+L*DcBBN#2Y2`#ZZa*^*pL ziQWy``EE>AZHnQd_Xq!Zk?z_A>#)FGbymYQop3-b5lh80IZLD%+97x;vIH4jXHg_% z^A0Jbfb}acl1k*YK^sU_K@a)ldiX^3gT@imEl6TP^6$2RqZbC8e(vKnAA9fSDiwR* z<|-X~|K+6Z^pCDjR!bbCrwz(dG(`eQTm=*Q3D7%n4O7?GlL1a0Ga+Q6->p+%kn ze%#aL=uZVtJ{e&naI4B=eP*?WoI`Hg&iYoh+&g_eq`shPxO~(K2c0z z&-r-Yi2X?@_};D%xjiUQIQFOO8&NwAs+$Gk@1|mZwt2gB?9Vs925v<6k9MP={&$hs zqnqC|vA@_{Wn+K2xyr@Y$NqYKbsfgQX%TTn7ZWv4CIljSZ%||>6wYfb zLbwXb_y!vNp5Wah42MOKeK5vZiUEr*Y9o=G!2Lr>&vTgTkdzaBcR>AOBkQvYnkE(A z#9fiWMNeXJo3l*}%XwT5M>Qk*_ra4#mLz#Rt&$$bME^3#p|y< zV*i^>Nx-qc-CU(&k8Q5fu@7&qkl5dCt}?N|-&|#5AK6^xV*jwYLSrA@T;*dQTVFks zQX~WsvNW4xGA_*kkJ(7{H^MGTD}v7pkr8O9w+SSd5`p(Aj|6HXNS9#IPCGIPoN9YC zn0Z{<6~U{@riev3&rlzcRkxN1Enh=khISrCJ1OL>%K<`u211k!~nv?30HGvh>iYRAom{QME^CY-wQUW z`nF*4t|HP-^sj<@2R_O}f`B1}#Yoi5;F4@)MZ%y7-yj%KvY8YQ z3`#&$tgi|(i6fpxfiUwCG{_P3E+-N!I&LHchURckR-EH-nwW+K1Hp9?GOWn;LEmB= z)C8?Sx47ts13tT41OsqE$mt&A2o9!8f)zOikKU{@I--*E!7$c)A@th20<;@>-`)A={@}J@tdtZMa9KDaF)WKX0ox4ki@e<6V)E(;CJ( z7GX+R)V5tJ??hH$P<P%pzkjzdK%;NEqnMzNTg1K&aNikM3YLgdSXMM3`ha0&Z~b?j!pwhw{W zkJx@LTy&mhQOuPv+He>@Bl%8*334G_fyr6CBk7Js>d_wzCii_1hl8S`9}TXb>*O(1 zr3pgvNsQ$bML`HlO{GLVLV};Kck_4_LO_UD9+w3;CzA%uX^|I4cEbICq`L$DJ^lA| z2|Qf_pHm5Z=as$a&QIbym-8NF13i;Uuowj_>m9IF&}@%#)hvUgQQgbqIG$!T$@Qq{ zH-d3w??t{0Gm>EPs_S#W6kiru2g-vTF~Req&gMN5kNzTfaAy~L9ncBv?Z80-h7zEE z3zSHok|fr;A|-eP{ID0O5;j*SU|$=7eLKZzsd^lVXdJ9XKNOU~O`6DQtV>XO*7w{D z2if3|4w>h4+{QCbFIKlwNJ`Mq}&p4=W~A(_NDQ%@@e&Ns>#!o-iKP z&TSkFTfi_#r6@$93`W*yI;FsbP6C(Tw8_Yfp0-Vg^|DCxJAxND6u`!u)C5tnfOY`9 z@H0fiAazRMR0qp~_`9TMf$eKB{Hw4n3M|Q)cq${MOb>H(nvM{`8|&0*B@hC%6YxUe z)lG&h+MLT#ysU$mYle&2zGnd?gX0k_8-aonH21SX+uM6NutM-iUDAAA=PlpGJRr#5`_TCIlOi>j2$#4zC!T#VfL6^8(QIDEslSHg0fT!&e!T;uO!$(}acN zEJ8X!o$>v_tKH~}CY13tmDN2(foNQIbq&nqX%3D4Fer?jUHqoT6R#1z%O{d=2srFR zvW|<9V$lB6j2!6(b(V?#UQm)R&y%w3&|+4RP!hLbT7zJL00HAg&iB&51BE7dT$c7k zE+T@*BN!pXeq_ymAJ-KX2W+B1bL_}8W}1wHxGtNLGg?Fe8DB?YhRBkHK+BFxn1sRX z$JPyyipW6cvrk}_K^?sv@Y{6by(kSr#rMJcgg3&*1^lo+N%IMt)Msp5Kr^^e7$?FO zb{$R8Kn-fLEGT)6W;y-n$3D>^_M)DtAd%OA0=ftkM~-0N7$x`|?dyC*2F>?bJZq-Y zjA76zgP<^-CkW1oyao&fHwtM~%R3qlwxl_lfNYfYjl9L-iWSL$-;YG#ww^a+uw&*T zInd0GX@t#Mx1m+cXCwKb#mI(hm>8)@KweJ;8ux$R zea9!H{{P)oL?ONJIoRKg1YP#ilf5Uo_hip~zTf+dFZekNeoYBo^WV>z{nHGdykY*| zD3`wGHYz7AJ7&wlia&jaA+QbXMw>cQ>N;xj)>XKo2rpR#) z?@BP{gLxpb2nKu8ktG}>Rg(k@2u3u`h#oX2Cz1od?s~D{J6C*X=a~fTDJfP43v2_z zXiA4%CwgZv%^x6&qfk6eB9S+Ondc@{&=|=#Jpy7d#-lMo5_L>Y>x`w-UgQd(*u&&t zEd%E7D42)IFbgo{9#f(v>R?7>Jh0gNBJkjKj9`P9A*6@nuH_nr&s_YzYo;IIdJlyK z3yZkH%d?owP)5iQ%=6C$_KmJAq6|c$oNN<8 zJo>RlPC$7^f!}?w-`U+s?gan--M=^WBjAhwrJXpT$R_x9 zat0L55y14*ll77~0@f$N>g6-zlhHnX@5#&b>2i6xT%M$>J>4umf11VKiG~>-mDe%b z^H7fl-#mk%9qiFHOEo-9U|pb0Fp2&8dIxBM2f+Bb=VKtM#LmtwBy7T_K+~#*tBz{$ zoWltyDzGWs0a_#zCn7Ha{C2aEFAer%vajK;LFGgvt=fc{B{0U0{&|q&aZbn)f=s}a zq??g0urZ2>7RjPq^jm|UZ$m9gqS;i|OQ%^8sHFrN_hCPm*Bs9x6$8ZdHU(FqIYElP zE1We57>Pa>z6+_zf=q+Y-vl^Z;$g0O^wv+B1otUi%23hY2_B8onUoU!Ww4{$J7f@A zG&vL)k3@>WrdEu;FS!5mtS|XEA%Y`L4grCqMrNM0M8^=gJow7WWf`9n6h_G-TqFa^ zDJr{`6={NbJqk1=axf7_kHNm1^C=x8Jr%bkU%)6n_En)sdwY%??yEr*p4})&o~_D6 zKBf67T{k3HvV9AzGh8E53+6B_ayyu-_Dl~1x!u`)iOqu(DFO-9UT#*VfL#WMnUYBp zKuwqDTsN&`aTANaBPg@U%?I`(F~&FNmxYe_mwgZ9xfMhv7O86fz#L z%V0&r;jmMF-3EeR!>bc2h|G<%U`b=AAt&odR(PGEhRfp`y*z=v;1W30$60aK3BO^l#@o+mg( z&{g$t{7JI#kuIRWsG1hTv9{r03d;#PacF(wnd>B$CREmuWXG0y({-hb-|$JTAbjw8 zh4pm_S7~r=M$c!#`b%?3B9aCz<2kAhjO*Q6=7L4fk25s*d8B)biQ6O@-s$rf_Q&{8@pnDD`0#`P!=^&bgcp8Z! zjK=f84&=4q$Lk1E_GpLpT!~Z#;6sTxTs={_dq}SHquMI&ugE zy-h@9ff7BI0?*5go8u*uGCg4Er{OH4mE*y4m!nX7AAW(W~c@ZTCyeP4A0S!$YszZZgeGJYsmYmtWc?xgflRg0v%LP zfVOM`SnnzNjX)o~AL!mes}7P^1j*NcOOV@*R?6_5Jh1l3>K6Y0u=gEcl4Vu>-CfoF z-gM7Q&n&yZvaoE=HVZpLGnE6mVpq=96$tIiIp>E6j0#9v1eCZEC1)@oASxzMK@cP< zAc|r_MO0M8|9RaruV;IAot}lipZk5xH{a_zRrlR{Za6ob^E>CJXp4mj+DU3uoQZxl zz-^pX35@`NA%)u{ZA*%*uTI12FD_|3_{{z=$Imn8OrwImAMc4A&T;zBle@f&r zfxIx0DKM~_f@LG7$|8()02?pem(UcD^BhU5y6mbN1;{Q1%$q+3wO9fz>`m}@B_sQa zLqvWQN?F7p1OSW{915XwgzaFW=u=J%{2VF4WR#2`I11@7>12|oQ=)=vk$#Y#phOy@ z(u(Y{62$U|3Ah&LWgezqnZqRBG-5QN7_O%=(a#5UU-vvf>ZIY9B%Dt@FZ#it`R)r& zjF}9;#+(E=J#vI^sY22PpjprWrx{DeZP!q3n3qJ1#YHd+!w-?;nndd|me5iXosRx~ zko_r`(OA-U;f&3zx{VQQ&%R6yuVp0{u8QY5&#_dClLSe!bJ6z&FW%x<03u|V46Ia= z6O&Av@pHbTgS60uon&bb;7>djc~oQ}@-WAM5EV9)$@@}TV>#Oq1d_#Z!oxMw$83y^ zvnmCO?dV4W{_n@xd4`uVnoYB0R*flwmlYwBOyp^x*IncziC|!fu4Lt9niAszPJ04Q z7}0kHBksPc<|Q1%@*UhrxiM3XVJfa9;YY}MRik{&F$qv2M`n=t!3LqqJf^F@N--3b zqj)AyxT?y-R)eK|xT*_kdfHVO-k>Cn&mC{37)d|Lu|)LmgDC=WQ+b~AO|TI`M@C%MiL_wPu;%3e>>Ve0 z$1)_)?$S{pb zj>o{P=?gIuAhy8jjX6#v2$myM$rMdK9;di0SPc~>k5d#!Mr_C<3d@cGXh7iUS^`V+ zHpQke%a_eehESQT5#fT-A~BRi=%5M^RbAnEeYqM8tiAJp)LMZ5Km7N@7WiQc{ICW7 z|F^(PQP>E*^V)@FmJ&rJ2|JNsJq~x;``hjCyMtdN;de`5x4s!jSpCEA4$|#?+S>c! z7KU+{9BsKGtC}>JXQixdbFqX^V7BAYERTzVsr!*$Fh%m7<2by-%Z#J@im9yj4t8AE z@Kn-p1Q(9ISU#I2<4G;T28)^yeQ&TfL_|0_7T}8;M24_ob;lJhlb7QH1MmgdRm8y= z1p_2LOwwm1N-zvXH*{6dt(1?8IG@J>@6F3*O5)fA5qD@x0b{8aeQB^r9G7vKaG0bb zdND6jfhk{&0h>D8kVwvU*FJRF;(yxkaf8!I)d6Tf4_7S6%TL&=$}!VO#WgP@ zn&6uOIp1utZr)(QV*_?089NSDg#Cxb=q_dRxUVL2A`!U(60QPF9lWhf8lxlkfwsEN zf^#ViXHbX_Cz3KWB%noN;LZ>*2OtH^bOajKARSIGp0)CZ{mtf$vd{58qZ+Ki_?86g zWy(rp83xW^ydXKzF9$8T=?W@FV}{}f=nn(7@3cs;dCK5SNi#%Rp!uW(+6dqZb#p0` zQhYAbh8J%D4>Lj*Kwm;wp5o{dxaTlJ-gSMPkKPXHmu{j!bDhOFoDVMd4aXkPupbziju@uoj z?XU^?-fCeC7Xnlgx4!RiFI>i3(2NJ zMZmXf@p{U4n7pZgW`M90vhHfhY)-OVi;O&JZwKWx7I2@cjeo(#tf1ghC z6y@@uZiDj%G?oi)WID9ZtrFCgCwSkG6wLHVUgVjK5YrqI6hL{CWEjyS@-ApJqc0D7 z;;i6UwveOwDzhsQ;fo9V9s< zUEphXc~<{(E&zDEm>4v*e#8$(gAI-08_+SlCzBL-+MWWfT~LRE$C86Zza6YC_c29{ za=_|Jco|=m1x|CU=!b*Eh!46>E|-oz6R4$AipAo(OX%Q096XlY7zP@Z5;Wy0mLW`>awL;sWR+Chgo!duE9HPRZPnq=&fL0i=2Wcy$0;z zJPs&P*m3c(xMS!k@S%hs&gPUDW?(7H4%jm=)zm3COB+hsrUc%A%F((F+s&jbOKCS7NkDtt&ld!S z*D)~Z#~ckyXZ-|tf2w(yMuVm$A{taFN?|q?j2IZ+OG~tQJ%Il}IeHP^yNB`Jd%$;R z=_J4uK)($tD4lb`Mb5QTilOHyGp%qWCcv#FHVNBU*cCk(N?7F}Z-!wFX2P6ypTkq!9O6oCfC!z^N@mzGuN3f*^FoW(_UnNfM(I z64*Dv=p7dc)rSZqF(&6qHdwf#{{l1n!lHEQ;gN+OU0#5A8hdLAJl132mnK=9>^d>s zQF4(d1%oxd+z6BpOy_Yri)w7E%8bO#4 z(CVC~eNN{9qRBA2Z)=#vEzkFMyboYSIm=m;&Kh1)PC-Nsn@rfOL*<#sqoJWA96@dE zMSi)f;`h8a4*jqcVOVqJx8YlWy{vdDgiW;oNdruP8uHn&iC(%TN6@qZrXE2RG%z6R z8D0h@9Rq+94Xnut)56_cl8I;_%Ih?hXEDKG3E1)ax&t`{Cke@LRSc|7o}gNaOnTzl zn=d;7pt1l4VV$(2ZwSPAMFESuXRKGkaR%}ioH9TQWssm_vIX!;!eoLg+*ZG9*FoJ1fJ_l2qmIS+*@m0Tfp0`Bn~!popKT~m~H{SBHOZML$D4V(-fLlah=jM#wH0Rasv2RqMUUA zFzm%4T3I-^H!GZ`!D7w`yljz{30`4}xQqo253WywX8Qst#VA6CGZC@;)V&UQCPz=K2r_12I2j`R* zK+8f20J8FYO4S_<78ToAKDJlF^@5ZXTndz?z=9~|4A3h>;4YI>A`HBCt*gPAJBVZB zGg(#>^0-4{k^4bmCnb#G6>x)Ml35obmb@t_WmTiWnGNjSbS|O>bDl!bc`9-; zXm18k-Jlp%EgJ*OhT*La_5~nl#%pz&_tTifBpn-L(h?`)xD{E3Ult8$XsFgnGvjfD z#Kdteum?v2h}zOA%dtt&2l$|^-~GRQU${ZZXrK-94Y0Ra;LFT_IV^6+Se!KQ=${1x4cs~I50$?)Cjns6 zwFrU%#R(3k3nmX34@r+3Ue1w7&`ScqC?`SKqo=?y+{j~=l>%2|JLOWC631l?P_dMm z5S4hw1stOfQ!u1Eje(_&!~m|Tg1;)pSU9ZH!4KEqycEcSC;?stHwjSeM1OH_#Un8r zT(nu1CnP}-U0PV)8}v&{ECXv|F!`7)nFLJ`nbG{5#3uR3k3!Sl#8|!tTjm@XEfRK| z^l_NbC_cupDWA#`TsHdaK@Xhebr_2g?-OdvnHpjcwV;kG(whvkj7Ij!3mTvVRtOE(Vi!{c9Z6nT*Dl6-p3Ai2tmk5_gv$!YQyh=o05mfUS z__u&Q5tH{8KN@gPfLfOIIDp=47F=Dq zfOk&EkuT%r>im>YPLq6yrDC~meci)jFYDRM0Ob;xSroSKl}JY(aW;N(0C zS`2iIaOI%H7%ZbY1g&OqjRX6i3m!YK4+?)!MprEWs{s-WT!nC*wZR`&@x263X4n8f z3~Veja3Omdn1x1JmShvoRz^?*&^~R^LH+iY%tH?dj<^(TX)phE2?axo2uC$B@cDrQ z(P_;G{F6x$Ad_$gV{K>UW&1N8SeO&k9p64EU@z}I61F>+7Z6!CD4C%4(2XF&*Rd{Z zsD$Kz4bpIUG0S00%8Pz97*xxgjhWPqyFmtfx$O1S;ceftg^L`q{v%;A8++*yMSuvG zk`;MO(B**5j4J|9xL`qr%x*Sgu?(jtNEbBDs-5!iM9_;&M$$}4%DE|sj1_qpym%Ij zzp%Qb!QI%$TsNN0d0C0Z$(ZX2phA@}hx7=UrD4I3tU&Kxr$Mi09nof>Ujb+5nYP2L z;I#`?z20U`9MdT7$M zh^bjc!1G#`QX$|XCva{mqCy>y(=kAifz?%mi6WAMVs4c@3G)G?1H;fHOQ!b zl^rkyDd77(!O*^yqquw;RLIb{9t#jjh=0m^QbGo8yzdk7tZLCZ1Y)72$S4>@a^z_+ zF0P?*M{)p*R(T4 zcD$a`F#$B0Z0Eb*|fj_5faoDtwhn6Dl1uxDf!mjsBALC7n=wl9Gw zjwybeV(iG%U=XYt67Pda!8IH>>I8icnr{enBk>I2*>jl2_-{YWvGjk$;ujD{oz>oa_avI4G0ZiP?B1r37ZL9fv?94R)S;L|({IvvTuHJC{u za-eIj+`eC_6JTN?$rz!rfF{wRuMKJv5py!Q5&$#zQRpNM+cw2ciH@XmiiNRsef`HLDY^cGZ#)>m4%~F)bMDmw>G6ohffrQ3|M5i4XHMkDZH&_x7>Xa`9hwtE*n|%^C>~R+Y5~vabz|&j` zkkwL7_hSktQ4(pfI)r8lwq-_O%mFdItH%QpjB5eZ1fskoORC=BT*eI}N3Vetq!!{HW$j8bt>eZ~gbncs@vY7oWh}IGt7_H7M&E z!y(}2U?K&;CX9nKo^3#A8Yn$t!hsSZ3c*aOUf&6-UJD*DlJ4+0Yo`I>!VoG9W5BVy z0tJX|-PYYW4$(g~3$WpFL8oK|$D;2HYI!RuNO+FLjac0D3p)l+!_DUu7T`SDm;!N3 zU5K6R8oX(05Mu}efjj|tUiV0WF)SKDJWxwb1lUtBEI2sgNCX7IHDo~rqw;bkXt1Rv znK%U=HZb$7E=ErU)jqX1__BLyw`+Vl1^hy|5tcPD8dqg5dKb;qfq}O*2e@zq*gKn5SQZu?#?Haf!-xC0z(JdJ zd7JUT)kG!ptVfE1p9ZUMBp#$E0g_KE9wX+(xCc>JUCL203Su=Wf~k0dqGll^tps>_ zBKkI{5nMS28`gvF10})I0w4A!)DqHMd_eaH(=s^IUkA{h%YL|+B)XWm?w9WJhX^M# z03--rSz@UoOyYY-iC|FQ6410O*3yK9IvjyYteru2>Aax2J_xMFkHB zcetzraZ@4F2T7tP=0q<=ad83sVL&;hWN|%}$*VcsfHgiSSlR2ywII`rd@Nm)0Y{Lm z0G{oNr3b!B5K`~Aq5qa5;OSlo)L+oqf$l2`OVz#(?BRQVkp*+_Z?Lol-@{0``1g*w zGibpT2sI^$`@8Yuz=i?G2`GQX68#up-hMsk6BOVP4ol!EQ1}JB`H>F9sePc8P&FIR zstn9UK5avwSb0L5`*9L%?tox2 z!NV@*C*aW^2S!$p)fUDJUt2i#AoN=+#g+Rl|K0MhFaPY)2Nz$kNG#?SPb_>5 zQIPV|3zw*+M=q@|{>9=4)<3)c*7c{YkJfKJ`Mr~Wbn?y1!t&#nuZ#Xo^dr%iM7ii= zqxXq?WA*n|U$bhhHdY^u{to&oR70zg7ahNO?K^9Ku=cu>Pdzy}`Op*JJ@Kg%Z#?mo z6TK4;IsPBVKY9EO$GzhnP}w~i;za#r6vr;nQSg{v<-YSP#jiC!96S4^5fFShHIbB&|%qcRXfV@FN8g>D@+>BG^7A2sP&^z2cSJ`8=>QIp<+ z-g4BWH={RSOgbF*;R3w6F)S4eJELw*In-N=-lBKZr0$}7)TGX$bJV2vqJ7k))}pna z6#V2`tlAjQx=n-Wo4Vi~suucM^tDG#`Wo~#M@{-_^wmdA`g7<-fw?lTx1$%Q4C0DY z@$*-ozt6yxlRj_tc^7$==R4l8(c12q%pP26_B?bYuW={4DvtsnNdj;!IyB`0_ z@vj^;>6edx8TKNdJKBEaUX|)%3)oY&?GtXN!x|mabZzrcW38j>=uwj*>&Q`)uB@*d zHRdBoUVhZ1FN?hFV$!iZZdF^`CWGye zPR*44L%p@|f`yBFLh_19pTBT%D!y{k=Pg{Cim#aKa~Ga_R9>I6@SKZD%UY+|sgiZR zRM-!wbEsNygZiHwHR+dDzI4>2UtIa(QImdQjlK%IPIb$-W`wSgxKg0#=8N)i;%GY*LXqyW&r`Cmq4-_ys+`73=lJ0#zwm#U4-1 zy<)E}>BL60NAw}AOD72HQnjV7tRLbba>M036eOXrQ*oGKF0WZ!zKs(_DWPsTLwR23 zySZK`9%H8Sj5e9bQAQsZ&m)8%-B_2b&mia{yca?>eEWy2Kun?$3u7s|6*a?*pnP@Y@66$oC{ zC#PdxDkQ7Tcqa&6)oG_T@Xl~L9ARX)&DQl9?6PWycv$(?Wjv71WE8LZiTqeq{FEp+ zyUt{|%gv=M%j~jdiKk;krzRJ2n_E<`<|n77(5YILM3pq)r(svk5ajN5$=S*@8&X0m zwp-wPykXe0Zg)DF*UWz37u!UocZdh%n+H5>8{6{kRz+cDuh6dbe5}%-yz0Ok>#evF&OXIfK83IKcD>UD>31L+KOrG&+2@*sro_13;9B*zGilEb@$jSr7K&=BmLDXu zsd3lq)#4eu#u&LRtC^mWc)6Y(Cu%jjBJ31KsZAG~(DOJsX!Z=T=Bidwo3t&wTNcaX zt?5|mNp3Nfp8(#g2t%?r+1cK=yG{CFck6Tyv5-6%4TV8PPd6P`r(>lAnfJ`9JSnGg z#c>KB$Wn(^d)utBo!Vw|n`S;%r`($2*SEAfmMf@ihAzyv=u8WA{BnIqndS$DQnxDG z^^LwPwe(`arRpYGGcb2TA7Wv(bHGF1Z^qJCtW*+uz3w*6QWX&7{OaO&kikR;fy2Njto19{=uT zJlMrb-BPzX&a$_<#qvl=ZD&29<7Vdho+^lwHYZHuR9h%)ZjX3+pwl^}qukOtd*EaVWTl? zwE<(*W#@BI!sqtr@OVIe^D-VOeGGhYRk*!SX~1&0TN&cuM#?aAutm1~jK5PB zjLmV*8BM_K>!tOr;!dSCDQFstjpD*w7$)F4Pz|oE^u>I4r$}r}83k`MMZQCn2Mu{J zZIus=$;HtD56MP`*UX_2W9f;U>u>S;aMUBIwlnVFCWKq07_|d-H!)>wwxPKP1+F?v z`!-mjJ-+}~;||O1#GsfTCB?yH*4Wk)`S?T-HfocRh;`g?Q!X32DR$lQAs&!NUdBUZ zN5gvfOs`31wQm}2rooVa=fVlFjs_Kx>o0^v zxYRtv!_uol9vs){l!lVoOXRTfglqLIrc5%{Y-_9QS-Vbk)HJ3OJm1qTVNcIxaamtQ0b*x5qKo%Npu~yS3Mse0nD_%@5 zsqCwIL9)KkQT+)!L}jp7r&zuNz5G%ESjGJ5>u(f@?9A+y*rnkdEnTHNgDLt2Wy? zykw!5E!~s3KPVd-?0ND+dE!soWh!IgLbXFS;*OM&dNx)T(lbF8HhVj@Mw878ak$*8 zrO^`s&_9i&v9+5>b}Id(-zZ2OJS|ieEVogZ(D)8ln5&px=cO{XGdk243ttV}!Oe`e z%?!yl;>M(;47YQ`sz`(TWDoPl+glI|x2nr_ekx8>WwV#H=j2Y@nh&-BZD_{Kg2N5v zY!xD?RLPuL$aYekl$e+2c4~tcw}&JsYzboK@^njZ4jT_nFaiGm{tM#5`V&t6;>pvX z0r;KcXV#v&`uWw=u@|A=M9avlR{n0Kw*1EBrRX&BZa@`0aq(9c?+Y*7gMZJs<+i6l z6lN;fUsyPXE-o)W35{%0yg(82>9$I?_$)t;RftU7%I-8RPMr2|X3(2C5aUGYmC}9b z5y9o$k5J)`aD8_V#0@KfAh$X#moBkl%dXB?;Z9TFqV)VvzS<9-uW=ra$%?#{h{f~q zSS+y{&&A>L`mJOpd#G=oano&n!0AH|I9=NGcyf}@r{e)y5fJUk`F6QIZg|^dlc2i8 z31#$+Du>0#Qvgoya|$>>@Q1lFk4ph}xA*s+?>jP&H{Ry0FC4qbMZ`-Lf z?a`fhuR~$Gt*Tp0swp;as%|44mvS99mse6#=OT-6+ckjDugqe=+sj|F_x$rCb9mEj z?x9v(*~IWdeN1!3-5EJ4%hi6t!pnS2j&0SNB{O%R_jdUEKmC@0IzC1{^-;8^QCr-#jv(x7=n0eBMFU zNjP=ov)}<3AJlf)864R9(vU0GwzsHQvu;_H9l27EPqtI-`b9qPAnR8g0RVM={wJS# zCU`#Uxku*orrXRzqjYJrm*~uf6$*Cy@t)I+$7-6LR)krbfiV1uQG1wg_&jKICUK>G zm7{b|)_=Se@c83zJ2HmCaUatDPg^Xw{>ne44D_MuF~TsiK{+ z@^Q(lG%>|gQ}s-$d1%H|$HQttww$rm20dO~E+5u{?8&)JM#jA0WiTcyZtE7A3mbIF_7{joA4>J@qiOYP&p3nUu)sW z{BGW+2K?SZ){P0?YT4WMt#(&Q%QW^NmfhP$(5tu-obnX@~~`W5-y zlXdas0iV(nkId&SwjT_KmlJ|I zi0QDT-+5iXBBy(@&i-4#DgX8(b9&Ql@}W_>w26=1w2kxFFekPr>}0DI(_0m_YVS77 zbIgpjrzw0(+cCG2+m$=Y`W1QHlXY4Pc%;8}WFBw4O*$;=&EYWZRFb>h23{D=l|-`~ zS2#w|HGkqz+qp!O&A_=)hRE-i2I)mvzaonPZ+L3&`OzbDc++j-p;mm%<`_;MmAE;t z$yrw%?8LxtqD}Ti!^1~gTumFfb(?LwTMdU#-LVyidb_Wd+top(IcQf}4&FK>wvP$8 zo9yv8dH>z<7(evVGd6A$0v>NYSSKI7Nym$0i7a^Fk5C@rr4mKaVk$GtbNR8~nNA4^ z0GfkfiiOeEr7eo7~N`>(!en;ksY zqARULVVbdnsin5e%1|LkJa{2`2UFw`$7D8OuhH0h z?py_v)FCD_n~BDZPpFLJiS@}AzB`?lcpg{AWV1$+RclsGI58$Mtl+uy!EWP-OlAW1 zwtr^t`Rc1+l03v@dNWrVYfaNu3*xj10sQz}PVR^*7UwCNwraz&IV!Xg({{VszUnC` z9k5q<^WO7aM`bcN|3?=;vvAx(|7Lj?KK$_Cy}kvWh1~W;lXdy42Crh;+snZ8R0^tg=8h9}z)P^f-EXxs zS8nCKR-_=7tsZ1{R7norx_B=qy!Mnc5;*CG{Fsz$1_^MuJvzv=y}z%yDU~_%RQQ^N zn4v6upUwcW*9>swwi(pr2|>dsI>&Hw@Lm}7^O3n+Y_Gfbf7>-E-3G1PUf0IK${$N5 z63I*~7l+qb%wVaY&!2L}V6j9xvzJpFE_mhb(qJy|l$02jnqz@awFR?wejHE2Wmsg6 znyZC#we8d%iw4I)a2!j&n9(6dg1o^!G@Xv;ABpXD$$}D;k)gAlT6`kM>y7ii+Rkma z^5r_srO&hJcD7sZ!_UsBy`AG@RtjtOYU6sLS59Eb5uQu9a?SE%Iz$69{q0`=yu()J z8W=Lmg)>hLn#_^|j;KHoU}!VcT~z(4PmO>mJ|Psvs~ApK9wh&>C?@lJgXpeAF>$XH z#j9A{|L=+7oun=u^v<0H^bVq!h9o~YQB0>Yfo$KUD7w5k?U%RP>ZsAKsYFuB=Y64J zRA=#RtZ=@W1<2hjvCXvb7IBY7v74GFW7@z^jQesq14y;0D;8TxypbHG*h0%4ja%6= z?^Vjh{wUc@Vou5LCk9{x5eEfPPcyTa+Vhk$IN&yi#aWV2)AM=JIg%&_Ro@rIc_@lk zF`T|{QOw<~D8}!VqIeZs*n5E}rj8(rsUN5)rn1@O-HBouB6=s=1CeRu&duS9@!n2x zyP{9@JaevIEsaxJW@l3FOYDvZ%MfI8PgX_E&9@5nIUZ{WCE2P-RI$Z?>w22(7v1xv zWIC>vtCRe;TlCq4D&;W0V!7vu9+{f<#Cxgk|+jM z-xtMMD2i7xoW5^S%-*di#_pA($j^k`VWKv2>J__9*W}$vYiq7g#$Bwi!?u01vt4l$ z9UV-h&i_48yz}}81Nu%&W%7<2)If56aI0b}k&E4(C}!tfvT?3j>z@;R!lB0c`PojU z!)mo=+_hTW^Mi!INSS=OG8OKzD5|jnb$-;S`2~OCNYCcUe(^% zoHPq%S{M|?@+{ZtJIZ!LNswi)TbGp)+(TKxtnS#WI(1{7J(ufk7mg^3P<0T+0RR8# z$a@#o|7rbe>wmQVo9n-@{>Jr}u0MO-Tj$rGu-;mK%=#nNAH05QeevYCPX77H&z$@% zzzMwRZ?|tzxrdV%Ic4- z_E(Fm$<@u(>sD8e{oAo`9Q(qtk01Nsv9}+){n!hRJ?)r!3_CVBRyvkC_K;)uJ%*s) zLH`>4BKmvihtPMRKZo9iJ{{r=;OG!7qiOUO^nNIc{5$eD$d{1cM?Q?a6L~fALgX2U zju6NQsUR8Tp~(G_V=Mo$^0(k$^9L)xy7I1-*R1^X%7qnUg3vIYSo)czXD&HQ?9yzhvGk~=hc7*7 z>Ez;nFaG0!Qe*Kq4nF_z)4i_+PDYoP0>jcDT2DvwX#RlwF*JwfLQYdaIei}b z2=oyLAM`#U z`JXC!4SG#T{>vB8)9C4t{AU(Dg`Nt@fAqoEcReKk_WkHd^khhW7URwbS)%5_hobyT@A_4d=5Q^9t+7&y$*$cA^8acMNlLpf2W78perHy(PyK} z=yFJY_={*1jfUh03upw5gyb(j4_!i+Lh={>1YJZIL-M^}MHkS8kbK97kpD*hJ0#!w zO5}UU_d@dL739B={|d?1y%PB@^4*YpwU7KK@}D7jdms4^fb~1C1d0} z$ag~W1&>1h4f(f_e9kwJe?|T^B%gT(`4{A0Lh{F3$hVPihvd^m%4wA-Q=y@)_haA$evO`6J|yLh`{B@@eGLA^9Kz z`4sZ0ki3CK{t)@Yki6bRK8buXB=7fhyBENZw{KWU(f_xPDXh?qd6Uc8Mzj2BD)Xh%d-zkv48-X-!6|NALu7wv}RcfWvk&`wBx z`$y3>+78Ko`U|v$wnFloXVE6w49UMvq7AeWl3y>Pb+jImUwI%}Lu(=VB?a9^w?pzX zA4aQaH6(v6hE~u@NPe&h`{8m(zW2Y;5?Tt$cT#8(Er#TqUyK&eLP);;jp*ai$A{$Y z68bpwaUuD!*0r~UjBhg1*B0uq;QE)N- z^^p9|-y^?<{8~u<>rWsbK|T_a-zp-%iu`Iw{-X@r`wxfYH{XtY2>DP*{@vG*4M1Ck+&jm4aqmX0eK7ZmXQ4U z6!K=|%^~^vpF-Y*yeTAKw~D+Gd1FYvW(E0qo z6_P*oG~|`YD?{>GZ$Mswydorj@(l8FrpM$&rc|l0d{|tFP^8ApT4Up#{&kM<&uOiPyo*R;*-$R~*JSQXvpGAHO`KgfX zZz0b{o*k0iIr1#zSs~ee7V=EwnSuR(bKye^>n~d0gp+;k#23K_;Cshk2wnj1U#r5o zlV4pv_R3>%h~xKU*mC?N^1zk%tu&Uux~wfnqc4q~jeHoc&i=C{eQ9~|WsBz^qyNQ! z;)MdTK=l_Ma`R@Boa3I782g>U_PnUy{&Xz>3J=6Bq~*XR^ja}GsCtwYL?N} zTjpdwYT;6U?n;M4t6j9BK0=1eTnaY@rrVo>kNkzl?-%#r%~U==NwL*rDwpwR^JX(P z^%H$3+lpmyMQdmkf6^|1gS#*#D;q_1q}3_Om{jL=ZA37Q!#j7B0~&Q3Il(=KZYG-HqL|!V>(2$6hLII4!F+ zOxba_=+s=RH8hg1Dg(a2&0=-E*Z{Zfgp3Jke-2K%_o!xZ@xo&+^%~*qR%dvLqkD0s zDA$_UFm3LPrnMSgnBvxwsA?)6zUOQAAf8kM=ni6Mg zmaxtHZ7gkWHOl1*Z?OENMNxh>!$@@3D9(A!l9l|{_{uK$mx{Z5kfiwwkGxb^j;><$ z0fhR`v)W{5)~EZpT@-6$Yd&hzWHsLEZ^cV+nM{VL+_S<`{Du6b!j#1JPJU<1O2t;D z7H`gt>25vSA2!8Fqc;%i0+lK^8$C}j{38SxxT~7M@XuW;Od@?hTe2H<-JjJNsa~&d zW5dxr5ibhat!6GctxRLP%`P#I7w%cjVEAV*73S8C9Ae>~6$ZmUvtQWFoBg@$z~Jv@ z%AJb8Yo@kleu-B5`3zC3YWlXQaV~hewYq8SDw^@ocw{c5_lvuJvoq=zCL~6dG0iWu zrR`}PM)ytzD?oe?e&VRS@5NY3%vd|dMroT6dNp!5BkBrKwFia6-i(*`8oVSb3Hm~6 zzoh$ZX0dv_ukRLA5>K_W!&)h6x&5t(YameYVRy13HLHl?YLm*(4;IIC4_UZ0-Yy+qQ=Z4?!_w;sZ?40>-eNpsfa@mLCvE45~g8#)_Tj2U>Rf}{)a z{em8{S(-s)6~~HAneFYuU~cwEZp;mayuUTuGP2b~vO4#7v)wJO-`~Im$K$Bl+!?kT zv^8wnJ9jfj1(U`ILQmgx5#LH*h+V4KPD`BoM!aM;3vOgE^^BoXxYA~cm@`R|stb1iw`#TlHTzJHOLDz4>oo{+&T46_k=hh@ItM`=F5Q46_ znL)fzQ5UH2<&ufjCgSgP9IkcxeE8>@2|S+SbM zDcyDchmvrLhPz+z3+MJrTHh2BCYi}81#4E<^a@_vn#FyKueTGPyOpc!W35=Nf}?~~ z>F2KOG{Ga%uaNjtLiTy;Ry&0P62qX{o$$;bE6by9#9d zJuEFJTzKfE(yjvHeh*8_3Kwpg6T-Z zIVTM4UDAm8(*#rFQrNCiE6?s>X)u@Ebg8sCR;6oWRVb90QZ<#I<#)Dn!`(tjo+a}Z zQy(M~`4-$fRG)3-?qO*#mpu4VX;`PMb;M_XSxyj~vZA2FPM?yLVRo_QCPC`+HG?!xQ8&mD_5L&?p`U1 zSFz0h-xI~7Asyp)L^=kZGe0<_V=|G;-5t`g;g0%phU(c3=G?@o88dFol&MxF=EgOy zJ#(!h(;e!=@~8@cd&uOTfQ}23LOnAk&3Y{*RtpUtBTL#qFwUQ^b2WAhk*&Ou7>_|P zCtfuAe9Y`Oc&3}_RmPP*!;GnRJVk8hI3`g{otun_%1(XY)8!+IB2;}}6n8>Vyo%xU zeT(8A!u&2pF?+8R#jDr@-&;j7c1KYRFv34LQA}iGfp5=Uy5f)nd(zx#V%uOznq@%u zHY^MwZgI7zOyI)OxvUqZ_&J?*1Ruu!JrqTVTIl1sCR-(>ej3LItdy{diNQ>bmAy*l z|FQS(QI2G3ePF&bD=V`K8x|NaczS3COpT~`KUmD12q{7eNg*jh3Q3`vZgspAPep_x zq==A)nKf-M9uE|QP1y@J$IBYS@P>Vu0~q63Y~%6TjBN}U7z~(Q1G~$zXE`j##@ODg zR8~)Qc2<_EXTZ$a%6~e$@_yfUZ^ZBW;@*gRf4{-KDXg^8QT<*NvMx|gtt_*|1z{x{ z(5g3BW#O)4s{rQ$Qd=@U*x^obKx;!`pepJ9Rc zM=uqxPEa>z7T@@+eEd`#wmx$FNlZnUT06Wp6}7~p78PdIi|Ak$(Mj6%kxJxqgvx7G zdm*-Z-clHiq~$1j)2RsEw+o>DTy3e$JqS**h>Hz2pJOigoW@P=2^|ExXLK5)q?70| zac`m!^&D!uT^q+1{i516uw|tKMVvm978W2RZAD~{+3tWU0*1cc|GzifsqB1YXZPz5 z^F!|N(+~dj!S^4$_n>j`7O>a>xTTg%M^hbApeE-)^kh|3B2Tq5lZ@=|Bw|@NA*WY^i);msq z@8lyVf938+Pt22@iFpBr;on%Xm<3@qw~X$?f%a0I}X2d|2Ou3 z5S%>t=KVhhgpJ<1_gi~Ey7z%?md)_KwfEv2rs=FuqOsh^*|bG9f@G3waUgn8)EZf2 zshbUb%%8%AefwP-ixML>TZ>w{6dM*@qr_fmU?*_1I+}D$(WEUJ+$lbS@g@r^~X zbe*NA03KCF=iO1y63iI|Io&KT*)_YV!7DkL4yQGRI{o(>3*BdJA4j4wPYcdi#n7Hn zg;sD)WEeyPH;qmpxkXM0m^=O37Z&NTR#kwks#>qNyXg?fs@4dp2V_-;%YGpiOwsk` zy|t`rTiGvdVr)-reJXA&^7aJQEjz5qV$l&McaE2#%H#~i>s*HG)F(yZ4Y)#&sK zHoXd#At^J%Yy?+n2i2S&D@m#8kOIO=HKP}ebkEHb6kZr-AG+)9esG)n_Pos>$`!Z- z30fBhYOUN9T#F?7)poCFv=9x7#a3m}=V=97jK*@xR-^O5=9W8fXx8IqS`;c^Kvq*& ztDE2?e4IN?I~%L3nK*PA$Eyj#A+)iu%fl*)wCnD0Vl+ouvs&jMVUPv}ZVZ!NHsGhy zedT_Cv=nY_pqpBn=G3YaayiyTRd8Eiu2G77QHHDutWoMfiC#uoauVB>L84%da5jZ; zKk-jz8w=HJ*r~!l(l#EukO`Mtfr+&nRZNh~RitWs6#>t+!$#WgP2!$L^dfMotDpL}S{D}c)nLHIgGygtmf#FmS_3g27xm#v)Hxx- zdZ0ik)g|p?%*$O0pWA@EeP? znXM@}OALgv6)CpbCeVUzj!RP`VDcH@SZF1_JdbshJML^OqL7{jjVj)xmvF*xgAo(W zV#B2zNH>#7*zZl#mR`aHMcwOdEW)gvRS_#PTlCDu=YztUwHUh5o|2tro375BS%xgY zE~L70>u*8(7N9ZlX# zUbHp7F*j6*k|_mZ#W`O?g05n#n#wmvqa`H8`K@SU;X2uXT(NS~Cc*9t%=UpkytIey6RwEzN)QgXFeVoDb^?ZKI{l(v*jDR=q83SM&*^z^jAY=RX~ez zG$pA7DfL_3CNmzE{0g5Ku1|@Dc=Z2nEc7l?771iB3#ta#7Q~sQk=?efQQxdZ1HRFb z_^}`{-L}Z={68BD&6Tt&p3Bs{M)zb@##J4fE-X>EM0yzx^A+E6L@p-d{OEXN!C0Z# zQet=rzGiC}^=1jFYdxmw zvKfNqXkmD1!<4$Yt#gvP_ZK!6(tJp3ZNk>Erq0FH9AQ0;0QjnB899b#iRcQ&f~uyT zaFASDsD=@>YUH9^H-*0E`;yPHLmcnb(0rsAb!0dRTagJq{Ow;tSAr9-+oavL$KfR@ zXMI$}*pMkXvWswlktrFMQ%Ew&ws`!WO^migHoRKCkVKv+qce4yc)fud@tqcn8h)?7 zXrROPs+&yhgXPAe)yobPDmnQoF3dj1qC=qO2s?_+1HM=j^N6@Er?)O=B@OWQ zFbmWSnwFeix)M^+$>SNhSeeQ~2RlT31{y=7R*@+Ce{U0mcZO!&%6oNk#V5;dJjvuL z+nY4I)pR!X>Sb%Z7|ZIwU-<`@`B7PGm$y~GiPD7CCJ$0@&|xef+tF_HqGbbUm-A6$ z$}b1T;pc8*R4fnGhpCxG?MXdZ@++6g=k02{(XeZ68)*PN7#>IRd51d*Hx}jY5);s} zxZ+E2qpVpbo>DkFZ>-`@a|KbT+SlWGujq@y3AwRgTRc?;QXRqy2-+{_)nc)d=My5r z+@aW2^2Tab>7p2_2ga?38w-kQBb8PfFNe_>S&F_CqA5CSII2C7Y79t(#sXbBt3rT| zKd`YNqxBBMs0!Zu0+n~5&NMFrmG^qzUN-xSj!D}5V`G84 zK<8*)ZkJ|!r47vo>VgIuL>;zXXqjA(vml)mo~l(N|7fzYK;scw!gSPxk~nWkOME6Y z+k{#f&8+Ie^$bP`y$M?{!}h_aZAv1-nQWVv*Xs5>)(7hL5>U6748WXkOuK7!dnILn zy1mdzFJT2Q5+vvg15nb)76o2#_e-H#X(zNJ5O!ra?RFAFP4gg9cco3IAcd*6!I#u> zchHREfmteIL&Z_F)P8j?DWMh%xv!PincW40+APfqhOSw$WjFx_c?>pC}{SO=K?mVKQiw=;)Y@T~v)i2WC@5EPC7zsIyIoS)l1vRUx$| z&Mnv_>QVHnoJUJ_q)pS>s049H7JF7vv5zk&iQ&L5hg!K^Nvba3n2)Q}LTFYZy6eOY zqx;PmCeunEAI*eaWfLRBhYME1OXYs4N5*V@Zr92)w%rDDDMH0u7K54Wm|@;yje{@W zSOiOr_aMv2l%6cW4pLYF-U+*_avIl>*rlh63+S1eTF4xI#>T=A1=$>$9h)3ZRjf*6 zU@E3NEd|Qr5X!=qiS>hg*h!;w|K5eg+_Z>cVJ!h=dmHK+c}eIPRINV=5~#{+jU^}4 z1w`$&W@F~;V;c*$#B!v{2BwiaU8PcKO_^n!g?N@a#E2h>$jr=}%kG$i&d$I*-~5A~ z>u-A1@8QT?kjNnvVyWZg0whLG8wf&y4{J$^WXZConE1pfg!8l0JMm6)=O=c-F~)y# z{>P91>fVdT&d$FD=i?7{zvSqrj=tsYHy?#ZUw(9Yo*n)iPytxn{pex&@Q)q*@?G!X zdk@}oAb^wpm4naP|2O;p~CMR~bicsnImc z2sJ8YQ^1zeQ~O%2UGdH4qO@?0p3vqoT*i*rO|KeirD{Yxu~dg87KpE~RnzU|v1~2W zP6U+LoUC3|)j>sx_U~U>$kJ3~8>}aaLg+fef;W`D!BqM)u)NN_z-9x7R#%tw3C}C0>sW{lUWUlS54{GTQ(NkvtA$n+J!|t!NN+| z9tK%Jp>=dnPKeo{@Su)mc)2Zumt=+0YloHDm^=N&%W)*>bgmN>heL;q+-Efi8pbUZ zOch#fSt{qQx~R;IfK>h4n}RqKdAKcGj=(2+tE0~e)Ug2LZ82!so*~ZTxP>F-m{j`# zbZavMpE0cAT0j)iFN=v%4g5YUvnAW|2QUyk>T8TP<48m+2Z^x%eHR6BMlU-yYY(cZ zRu+r_hqOn7W?v(xQeGaFnZ)g*MURb@kuUn2VUR}b!9qSoU}NML(pZ|#hMjhO0xFSM zo6&=zh`9@k)f(yE=Wi_V6z|g93g|3UV!^HY7@~(k*R1h=eV%}w$xC8Nq+sdSGIqXW zV}Z~Z>5k%w1X*@J8mtDHX=DUeosP>!QL^27kC|7JdJ~oQiH!xMPoS)`qVXwPlltih z8x0y$b(968VBEwo2TZiM9)33XnCW^EM~wXScO4e0P&E@H&1 z(j3r`_w$CJDmA`n>NN|g#C%E>Mx~UsOsfUHsw+^3i}wE2#$t*0BD69rp*4u^LzQlr z*St1_x%EyxHQD;2@AO1|vS1Y9VDlO(E~=aY%Q}WiO$i!S#<9^EPo&{^P;mpQ9S3>^ z;`I7pKJpKaHZi~()wJ7)Ft}HN^P!VL#2^&14pSOey(VFT8`~W5zz+(nBf&rze zu~XNYb9H4}EKJpNA0LQ&fA0c_@qC!(i(a~D7PR8^Ng!@o1uqpduuEA2b7Mk}6TLUX z*-#WW>(h8HfYom{1}ivM*3!W^C#_yIUow1koONI?Tfsn*i0#Jewd@9I&Mu77o_; z=2&n?ny|Z>#Q;vwYBUdJJ=3ve(1zG}4lkEhLzr87w_K)#8q=5OjcFy64u5^qD_a)8 z=Ye@MtH5J(R_aV0vcYC}hh{o(I!QW1qZlva0os*z|LMkJQf>@-GZY0c@3GFT#WhxA zABya(2c$QdE*n9uMUgifxFPI)#m2&-RKV_TmdO$sb2GL=fX5GKR#yfek;b4`Vd_!4 z=ubU`J480H(z+RwHR53xn_)C&AO*(IJZXbPSYdD~oil`1tBkwM7~?k~#@*?7Nf-4| zYhiV%#vrMSle7n@l1m*UH%-fMQU$4-Jwe#}!P5(q*$9}P2doCSq zz85@$2=d zx_yZf-xeGLFPJFTXv9{PvI@tzsyb$_FihbEekMtyo;Ipd``^Eb;dA3~>Jx*+ZL*`j zGawB}AUkPOSBE;`sDKeR3$Q?rn0)`wZ!Fx2o{Ix+0QnFLUM*`a0w0g7-6E=bU28h? z2s8qfE>278?r(1_>?zs>$CDg&P{p%hN5nI5;4i28GfV^v=gFW~riaa#pV-35%_dB@)SrnlQ%Y7~;aEnQ zxXhO86)Z;U$R zraM)jjmjMVyNyLvW!Zk%n9>F*&zU9b={8@+-BFe=0wDc~fH%JVx;nsm%u#S@QLf>9 zpAJgw(w?`6aJW=8!sJ?&5$$2+aT|R1f)=dLit;cVkhmAY?lom%*#tMp0c>WT%H^T)^oJ#5C=q zGHpvpB#p~~biApeGuA8R9G$f$5L~M$!(n%_5Go{YHRh8sW%@m()^2x1jhFMIS2i)2 zHazjBq2Tnu(bYWiszaq-^_?OyNKzf!B}}S_V1X*J>cLlESj48tmULr#R@-rVsU~uTS?W!r(ngk8D%lD0H6Sfe_<|=VzHeIk) zb|sx}p3XCjnusiF^$AN(8yZv5aREh#^-R|5dD5<9QGLWqsep6&$t!WjH5h?Ht^pxYa)L`_#;WN|#(J}gUnd$};m)^hMqXQxW=cXc8)_m=t?PDdFzGRC zfKt(GfoUi>)G(+F!Pm|NyW?#v)KXjM)-iaHRu!TVDnNL7CD!J%5lds4iK67pG6X_2 zAn49;a|esktcEJXMN9CD@xGu5j`v3X*|7O*z=j;FH>;LEL|L5!f=j;FH>;LEL|L5!f=j;FH z>;DcMH|ZAahWQwqwEa=tf4=_zBewqEY#985UH@OcUHOz>|Npm})%-K8|ILlXGpzsR z&D#7K*8i8U=bvHy{|(Q+{$HQ}|H7Sj@7#^=?%(+oQHoHf^dlZA+`rmU{KKQkRR{*p9H}9i+|7b7R{k7dcyZg4C z9|o>o`x!la-|?RGMd#Aa`4_#QHwVk1HZ(KEF}q9@DXS~AG?$`j8{yg)HyUCnj#4N= zN2_91TxmXe_+H@roonZBe?gtf3t`|EdjIELR&6F%0=)9@Js<$O4)EgE0Uj4d|9A)-B?!mSgR2+o ztM(qQ*07dx4<@KIk+4)n1GqF4vt{5Ahe)2E@&M)xrxi8FC$!Nv6R@tt0 z^|;ZNt}53*y%Hffij#eAl5kf7z5H+n0$t$YbG8n29Ud?gCtAU>ySx(Mm4`71aDj)n zY#rcwJfJ9!clEk=m0K0yAzI&xH6A{D>lioV0lhMekK+MBaZF$sExyuX^e|j^VvUE- z+PV{0@c@IT6WCRsw5~X}A5PcK*Ollqf%8S;Nwuk)xVfh(qH~q~^+}b8c+Bf%C3-_~ znmi0ZqzmNz7h6ZVp%TFqQ2**gvE@A$d|fDBe&~Zh7s&gJtpi01Z57I`29Mlbq3@#+`=^4#@(StIX{Zyn=i`w4=O-`py5NJ-u)@xgvdq=wT9I98{Uqc) zo=zL&J(`R`U{nOhNtLVDcJgon0$%{}cc@9^3VhUE`WHrb%1MuxDJ7)VGxE_LI5Bd>)W&j;=$H2ZUiFsc(qt3 z>}x;-(#v3jN54M3T6abd^>ruKK-}ND6IXx;!6*jTr$#7T9pcVIZS9@2cFuQSVA`BH z>o+>QX(lth(P=lYG=CBjA5X6h5)EpytQS+6s@l0846$6AiAzyqDTQV^(~Y<>X7#Fi zLLgB5@D}=hIYcN^U5q7x{qK2)`F^XeXfyl0WRmA6Pdn8P?(#+#0L2JoU zcKmRB8S?Buy^yo4q;efks)iA>s>Iyl`cYpKvJFlAdbLIynvN)Vxm9WOJMo}2lM|+v zFJZT$xe3e%z=f?XbV z(F;m6=RJHk2!*ak_4`|o>UCKdD;*8Ry-{jJmZHxVbxPvtCQnvsL=klxi84w1Ww@cB zf#f}W*P~$ne(Q0)E?Aw9G{?5uwR3OuR41; z0@1$cB`~z#+j?j>C;uB(VoPy0wnr7U&cIICEe~p%fy@vH?e~XRnarBKNe81w-nd$^ z*X#fNz2(mB_Z|H{_|Nm7=U3nl=L)?4zxt-}i6nj1{s-TC>(SE+$SI&Gt?H^IU8$8X zM5C&_WEsCzNjS@8W`nEGkk|2{5f{93^CBF(Yk~H+5%7gp{ToS`P!zs~gy~&xd#y)6 zuofX1I~K3L76ME%kIUJMwn)L5*Q*YaJR*oGsx#F|lr=gUAV!!>x>7LW#YWn|E6Us6 z`_en!`gjNeMer=-I#(ajS3+=h<`wsIU%gK+1qKeo`}aF^j;Y-#e=p~jD}SCld+aI9o@gz_NyTrEB7?O z1ojdb#)gXp?12pDDY($@dTdw}I3`GCdNc3d9234T!8ncPwfi!b#rMxC@+m* zoNaa79=p8PK-xlwx{Ntr#@70-zBX-0Q_Z&4)IMvij>}vAbi}L;QY$pAWbNz;ueq?t zXT$56)!OQkN)?#B=M93jF!WZ(N-lbHGnEZ|y}oEk8 zp@F02>HNzc^>p*MacU)*m5eX$(dA@*oomoq*!o)X8>rn_fk{Do)<_weAmp^=E+DOS z&*Fs$DMo45(JC4^oij7vJo%08=!86LNq|R0yq_u+-vTFN!0ywwwkj_fv#FZud|B=& z*32meUZu`e>Z1;$B<-j-$5#SwQUpI~#57=nxbO9(l%Dk3+vrfQRa?N&-}~P8zwet~ z-jtyCJXV691ly+r>gEyz(HDT5CzduHM+tzP8`|Kmff95L`P5^H^t=SU=@Nv2Ett!I zuUsYD2Y2G};vIKljVT;($~BTL*JCx6fCehME_1;MA69&&KIP`^*0S=b?ER50LG-p2 z=z3lI>nqTc(&7`TKsX6Mkso#{;_oeFb__T6`iE2&3^e<@L35pjzB^hkC0&?S@OPjVyCQEE{Bn zr)tFnx;OG=WKo^SRDIs|-!uVWU#{hSvD^|#z(K-KCT6>0E?BIu$qL?CSXPuq7CBv* z6U7Dxv{u#WvfFobwpy?OM{!(>rK6@y4JD*2`7=s%px&^t4J|IHKN}ZE3Kc{G zXMH_UmMGe8#0x%4O40>8>O~>?_*(XDFFu2E&>wwzRs3SgpmB zp6}rGrXuOSQ0=UQrQcJHih#7dpnIP-l~p1J`ZOJ>Z@RkbG7+W06z+OTF`w2~SyZXZ zjfI_;F)4A(DCiru-9+x8=5WzXaCI8gFrc#414rqkVFxeQ6gu#0Q)@7}Uy)=N21n+H z+sy9}1knGJpt|mZR2;vSiq8P2PdF8y$aIWcv)KF;NX2K6)%v5Cif>FhP|mK>A7k_d z&P^+xJK(hd{>f!JCP`|Y+t*HXh88&tI@4f|PL{3WUVNX-Gm@XS@AcIv_Npr$)mvtp z7LlI#rc+VPv3nLsZ9ru%tt5}ABreRTD8?eHM|YQqp3YfXnzv(DsPay2x*+D>J#z|0 z3v^-9%SyLNv_=cPJav|Q8g{I<&l;8On2y&&-=yNpuchKM!08iC#S7h)H@g51DR1h%D|j@JFum6HxHc8#q7E3wN7LbA-h}QG zT)WvEww5cc(T_8>6(d;%S`u!xHc-9k;A4Zq2j%`Fzyp|wTTXgBUDjGU3R3F%RU)-u#p$`D%_I)e?9a~D!%ku zDn0|8KH*e^uYUmoQW1OO3y@ENRD1?O#!uB$BwwG3*12@)cnl!`zbS#oPB2S`^G zo#}8vGRLVAM+SiSqaKB-DgkN3mb=W|H?NA5(_Dbac7K?*Xjw_;fRJnm=A{Qk*5J^33aixc7GPaOZ>$3K4jmyey}FF(F}^s7gI>*)PQ?>hR@qus-w zIsA^p_Z+qkUpV-kgMWDNR}aDi=HN5-e|`T)_8;uOvXAZ`?fv}isZR34#9OM_aZM=8(^JhQ*=rTX;(t$hrbD-@BxFseFD04>1#O1ASqvxshtNVt} zUpar}Q74{uulMhK+nsNF)Hoe*!%3g(7iQ+RGMl-&^Yr8dJwdl^e0+kOAlo;FPvGqv zLnqL-jgL-BC#7v0AD+D9~KJ~;W3lP}q}@&3sdpM3GQjrUI8e)9H5$av~a`GS)# zc;q-rtBrq#5lb!F~`QB|CAMbq6&i8EF_-N<5cfNbu#)muKwewxu zHa^(-&Ykbvw(2lFSM^#gTSim;3Yw2@lefuj8z0^#ZWG%!KDdqF z#JNV{d0>G#^TtGkm9cbB`%N4YlaJz2;tX z+s3!{8hedx8=vge_v+g=KHd}dgl!ui?bY^b+crMjtL{~|ZG5oD@9|(}_oc7R?w83C*h~ zpZ4~@Y5$up$M@96``@_#jhDSJl~8H&&Fb9N`et66hgZ)AAMSkJ&ZFA=)W!!p4|X1G z>v@0YYj++M=cjt!+xeQEuX!}Sr}cT~13Mph)Qifj)oAgi;FXy?Tx!MD^JvFMzj*X$ z_HI6{@zK%8jvme4pW68F=ogM2&EB8d_~7X0kA8mJKJOp>+|kc%+j#HjUmpF-M_2Bt z1N_;epMB)`Y3qX9=lZ$6ePiuh+qUu1`S^UiZR10*MtynP#s}vwoxilRZR`E>cb~s| z+s1q6?>c|iqwD;%Yj-|6A3b0HU-Poy`TGB{ZN$&l{~zB{{e1oZ_y+Um>;K2nc z|F0_=&%XXY|Bjvd&PR54es6cY`#XCd*njKZZyo&V!4Dj~?>syCiM4*;-J`XF-@d$7 z<~#eowKm`GkKg%{s@G5GeWqJ--w9_&**0sIzw^_Fzqs>}!|%Ei z9~O81#vy-RIsAI+~Vhg`S2OYEN=h6g+(qPnuE@bunJ+SJ4^cxpKy6y zpR{U>DTah!Ne1{a_%v{t&iA zpSksoyBCs7xsaz2B57^0Gj4T?pw^NJAPft8@v72fR(zG~vNKy?xDj-;8HZ9ZQ=jQ` zx!tAc*}|no-C(CkT*|xQNbuTJrv*dyaMX%UzIZbZTLAqu!d17$*ZEqnC8|lO9na^2c}=2^7#~Vj})2lVnS1rw8;fzv(O4SDp7g@z zVl0v1q(fa{!{IRT7-+WCRztWKB}=m(9sf6*aqzh)cL396825)_0jWIig2V5*GpW0I zwN^;JqYLf1+SUDAKfH-iuoF*LxK$fUvZUO!qKU^yB_gbG(lD8|urSCPrr`*Jdh7c( z7VaRCyG@`M)5r!<)ayY&nz7_iKF`j$Wzt*))zBblte9e_8wI(7cfmC+cPFI;ZFfgH zpCFu5_8kL;=Dm4m%C&^HCHQfP^-o{kjDwN61aIU;GJ(nn*t$FC!$Ae3SOwGNaN3&0 zovM>oF`)5$y5XqJrLI=(6*{!SuVa!+BwnIywF*P@E7AI!XdZw^>ua3MeBHsf%YwQ9CBx!T-C znvPSiFbwG|QP{6GIzzkD!W5h>DY3%k2Or&7IJT9P8!f|baKjlOMC?}QIbV22gm(uS zC9}3wvP!Bm>iYW|#j?Vdajj0NEs3(p25n_a-HaOml2#4q%?Mtj+XHvE8lygTsq|S) z`US%fHM>nDJYDbW{ZN3LZPcD6i=^9X_sB#*mH{hFvE4s?QG5z*Fd4z7+~}l1t%w)s zY#=W5b|t7NrJ*wIObZsH2NY12@;5113`Q%tlFp1JPy3a@YA9ALVxjwd%dErsl$drh zt>WXVANo7LvKhEiEDQFB!uhE z3Z8WrP{(gn!$w}7R?@jI9c_fZifUR7$0aUu!X;29@2oVgZY_CvDfj&{X)n2;>Gy$r zo8m=BKfJkAyfJRzsTmp(P{*keOdODAuNbU=td9WXb5Q|MpB1&1!Wb`Y4A>Z}(K1S6 zN_*LwIR;6O&bB9ICKKJs&ZpXp)fW)$?Q0pT2kAQq95fcl`8$_gee(Pa?? zGo}ryqE9a9acd$TY&19vvN>yF^kfC3k)t>zf~XE_%Q4s0&0uNUBP?^9D`M5J_~~wM z69cUi-fZ4YA!jlODIkNIbbB4s5_6@c&;8J|?6{Q}W`L!~Uv_E1^=Dc~SCiUw&P(%D zwClE6ZsU0qLA=}?PgXvybXB)c9e(y^;IQV*u_ZQX1)VI&T?Z&#R!JU+Vv^~&LmEjn z_0hT9sDy-)QDk7g}5w~S9A~U4yAkYRrU?wwbo;YzygpgHM0_PKAird==aOZLvJnKef7bSp1d0Y(g+GrY8 zD?+2A^!Yw67ZDHDs=~-u&Mu#tMcyiRr}GX%X_bN7>@4hxX)n}Lsl8l`bH3L|hXbAN zBnyB4@S<0FUdb23d{HN5lc|eAt(FW@R&C?a5=t1#CaUvtG#l`qAMO6m#v&8)sEo)B z$l!Y&aUlEU)Tjmylh2w&Db`rrj*`Wq>yFdIla0kJSjZV!sPl%xwEG26@GF;7G;zc# zTNa*$A6 z4W$g#N;T@>a!%rMN{;BRz_=Lftkj9Ghtn0w__~5x?MZnddD2{ZHnkc`@5SMMq?>pNvAtf$#O-T)C#r0+o5O_&}crYL%blARwd3( z2h7oQW1&xL<7!HEmsv^;mVm&(^RYq}lGm7f!p;Xa79&$y=&b=+VOLhQQ_O>?Ssu3PCfKj9 zmzL6GIau*Y2?iHy@4vsaAfyUjMk{6%fd|(@$2G?%#_Avjj6wcPY|-juJcaujXCHpr z#-gW;;Fc0IiM!}Nu7b>M%E`r?Ev%Igq%^zf(n(^#6I1qE9SY- z;hULh=7tp&0 zH5Gr7-+pOxb!AK_=<-O2g{ojL2200X#AwqOR=n5g0}CE8He46htOgMFKVt|k!7Qk zDn&;txA{DuYIrsdfhKN);fqK-|MJbXvXOQrBF1w^B3w&qs9a4ll*WtzhwEEd~;AAnNISle(nozfXb2E-%9$;ZTSk4y$Q^_3-hGn_g z7JyjywA6+Did}c-OnKFkq+1``#88ZCz10yq6&aqBKv}X$d#n4T*&eCNfQnjx=hiGQ zkk-Abe+veSSaXYo>?hP@Gmf0DO)PCxAoG>XxMihP=0kr;jmyo@ZHn5su^RNcJ$%A& zC$07Q{}*{wOK_30w3r>Yuu>+L@ zlQ>oml5)+tveWC56YO>LjAo3%_F{$*{bIqxyxA{Njb^EbYMjP|X~4#^J62ns}dkw@j0S3y$fB6-a2>qd*!C zOPb0JBIALz?#;P%^s`&{=8~he98VXG1*_$uyF$sNX%ld%z>y9R2CmGpLK9)JGZeD! z)D^&>&C5am%nK7gGgaS8r8Zw?B!A^!B?-U0!Z!|@c$_r{7j0l0k@`6q2K_jpnI0cl(g&I9DKyLtX>(Nhd-J5GjR4aT2+8Znhj_k;pWsY4=fkV?%={MRy|XZ z7gckn)@#flC^v;AMnmAXzH)h6|M}Lzp7FMpNvaQK2Hdi&Caa1ot|o1PuV84OLiptp z&n6b)vAJEY5VrAPyzUJc`aj#cH&N-d?%zTLNKSAEka&tG=mxwXqJ^2 zrfQmmwjT7$mxKPNFSG~M26&U!OKMHS70jgWUdhF+u>3gf`URFd?efGM=Au8TwS!Vs z>)Co~T;oJ|(y_vrTCtYX6viu`qoL@*OQ1JvEPr(C-duy_+4l0#_JenWh-)nW8O)>h3XH;%x?OeR#1PqT*8o;a)Cx-O3{Z|gtaI@rxve#32r2ycl|u63(lZA% zjl*+mN|GLej)3;JQeRfhhGDh41Moae0j^+W7^TL8-nutn=>K5r-dutw9K^7gVfbVT zo-v)4rHz|>!J;wd(DS&cIsyV2Hyu>W@iU);Isn#fimwJQ~_VJb*fcb_UJ&-|f)>!^~TleM~ zEEn6q+_E1?AmSR!KfZOu$FMB6kp#@1c@ z-+j;lk=9uLpSOb#E@Q%uhUHCQ~M4@|IU3r!B*&t>!ZunW5%vH0=4PN<}8TP}(*aw0Jq_AA2EI zx;zsua%BPNfm|W1JV9aS=0U$fL#TytS zVmrb^%EmmZ7cGdD(xucwWWCl{MQhDoxiklLxeg+&vHZ8Uj`TV#17Q+21s}SiJ{nR` zy%Tk4y1ZyPT@Qnox*+Q*UP1g=hLQ-}R^jrt{>awBZoaKo*_vO}l>*OnPwP^FIB*7~ zCX_U}n9s(I%1E0-;t4EPw(boW`VVj2n@cP=x?Mcy&~dx2GrduSG%9f9 zg7>>sUyh0bCcfch3O%3sR6b(B3_(gDX^|kC%gWhaM+oe`@i* zAZU3CDLYDBP4l(Yz@MUj>A!yC|b74C+t%=D^3AHtRJ~ z!1p{Noe#C?nu8u+|MfCSUpMXxcOU=T8_Pd11_3|b!qCDfbisbeu@<=6y7Q0w)-r9b zt$FJ4i#~4pbET8%T$@{u{o94TuC1BjFMjo1!+-R9Vwsw;9^J)X+%R+J>)~8De?)t9 zJvZwnif=7J^S7<9`z4`5z)0uL&DJ1{f-p5m4nurP(& z7}z1xLZ_1S=BZcJrlS^erxA=43J$axQmiLKPf3dPaNjK0 zVVqji_g(v#87;t3>`-lIWL{T>?XCn%bGRGPJs-oCs}W%$BYU75lK=;zJ;2GjsB7~@ zVr}LYSL9aKwcauj9Ea&I)5dL+i7sl0!Rq&1Hnb^)rFv~|Le~;c93Zq^FI>gCM+h__ zka8cqBSI$GV2R&rM!ASpd80NnSGoxC*}%!bS9qAIG4OrICPJ$WBoEg8S={`XSq~k6 z{rzwGptc74*S&N1Syl#5U!1&tR!gog^L4XYgnA>`|KPdxps>Bdqyd{Ydc1_{ND17@ za!t0o+#tZ~gSt4yEI6)@y5p(u|>*0={a^?&);NfqZIjMdTEC*l79W(LM-%l z908f$Wk-ylUJlO?uh-3HGlb@s9S?0WZ+-^3SOpP;5bX(dzmb+D(xN+Izk9C&S-`shz309fMmYkG1EdoAa)?9q~eX(TJ2t}O%VdSzm0&=nqGP{ z^uz-562nTz7juYpxp^_FrQI65lJjwswv8I&VJyb ze{g62C-&dBPwf5H-nZ|)y!V;AAKm@x-LKgBT@da~{&26}KYVHDPw!lhE)2T5F1&W8 z2AXlNe(8;VuA=k_eqip^(v5zue*1rdABcPP9XI$v>8qd9uk{0(aj)LH(a+UyUQh5t zaj$;Kjef3v{&<2Pl6&<}-RS4)tLG>9!MRtz_(nfh-{wBS55~RvMK}7{3O^_f@bmT? z{X8yQyZ&VSg#IAht6zAdpRI6+(g24q-sop59HKP9;TPQGhuRK@6u{xzZuGMq4k>`c z7jE>k9S$jg!?)h(=jxZO>+csuaIgN!8~tpDLki&V^KbOC9S$jg!_T|X&vrPZ01p4e zjefSn4+Ze^xi|SCx5Ez!@bfu0`q>UYB*4#GZuGO=eJ83-0#fa;Kb^lHL-ycZgHk4K=H0)cKw=rAR8fDmXBAl)l8X~NJC-j;mM9enI# z>Bdz#^sAG#PHx@1_qRX&d;kBQ{{8lfmC(NT!`atq+$ql>+uV5+>KNWrfz-nML6i}2%G<1Xo$M?_KR?^`$zmj z`9RdIw_SvT&JNxMe1g=i$%Q!X#xsgjx5gLZxEs$X7|$=b2q)lPA>vL zFP?uPjyt^w0KNF?3vt|y!vH+J|4*%5dt~3g{h{^L18&Tps|FCj73qgd1%@3qaXE<*ErT8qRcZ{?_*Rsy)?-Ec81W!Wa}k|v zDighv8Eb7uiN#uEv*gcYqnSu@!WsSn(QTGf^(Hj`;M6O?%>Cbe*XV%Iz3u}`CFSb) z6rWX&+2hlt?gQr$o^u}nr;pE~`#=!$JIBy-?{&fiksRY;fmDxJo=t=T^=hR#Zm@Yg zrqmBX0x=D1NU%EL=?72UQfMR1dck0011M>A`iC?J-VU>)Xgp%IIDzVoppopar-0vE zpU@I&h-t#%26zwxt5*F`??qBUJ&ecF363X%qMnd?N8OOS)Gf}ZJMIG~SI*oAJm}oY z%uQgc&mAn@_8~cGT>5*bbC!h8y4^TC5v_C6K!haCnd#)pRn(i()kt zjE&U>G7)>FaHgM^1}Pm%Rp^qRw5SGf#3Tl~hA0iXM+e}#-byfqyjBXLP#R=f76wtK zon;4-k?(>S5`v$Q+}x--pYFI1oa~>u4|o`^tJAzWH>z`V$^NSu?U?UA@GNP^1G*2K zAH4R+IrFJ;aTjS%?+-fIQN+oKJ9nD#cgB^r0$KS&Rm`T22C#&f;{H z&uG1HHWUX*+aGYSA0olwpi|a3l?qkHGAaq7PB>j*4^7&#{GDV8Z5oykH#6ldOQzH8 zm>9tj3>5-REl4#-QUmKYwMrv_P7)@XfzW~3kX;UT`_sW-|F-JMo;}!)!|Mm}5dz{L z^nBTPKbU4Fk!m6sFO_gPFQBD5j4;ilm6+t}T|Y`Kf3G^TsJT#*b}o(+{efu-|c@ovWKhuAKx4jZ)2u zpJ=1W7|b1xsZcAYqvax^fxs$BVMI&@$`F6>i6{<&xzOoU0LPLvsyA~|d~5`8!GaFE zrYQ`uY#R@iN|*%=N&!qY86y$1;<02Z*i8z#RIgO9ux>k5oa8i^PO*4glAvnXm1|G% z^wa0u|9cs@r}zI`YvZ+R@80`|%_jK6v-Ja9@LFecsE4OO%U>DIm=)X8ah_R0=Oo|# za^}#bPwZT&W?=RlX;w8Ka1K3>`|Qo3SO`Th&^EXB3rnmL*#Kj(lUOZ);@*bX@Ipw{+J2|&?^zfq2{hZrf_TJFuzI4uA z`efz)t>fv741ny;_uTQsz1V3FU@dX8o$pnJ2JXLOfgt;XA8^hF5g1YX(aT(0P)SIM zPA1Q25Hz%c#X=!I8I;mOAx+ESI+PSd6slX9q>d>Mo`=2}?v*MNq)!x+BQ(n%VxhqV zuQJqVbi@rBjKay2ql6Bbm0(qon%QjR5JYHY(^)K3!Ar4DCP4P|pkJTJG=m?hI%mjZ z)ZKde?9-=RTXPKgncLo5!K5|r+Ana<9uwVtz3B`u=V{M9k8bW=t=%bVR|jFeRJk$T zZg_u21tx`aOzs}2xLM$N9OtWgVetU^_sx&lo15*Ynzmtn@mrvpWNm-&=vE8=?w>jxf2*Xk4z`A4aUUM$5NMZ7b0820G> z3Mk+m0yA^h*-3-FZ+Is44#5__L7ng+Ik=O!*f4C z0$^F7o-GSJgq&I3;l$jlH4kB8Y;`{HaIE5#_Yn7G{+XwTm^t(IUvp;T8LRXx9kR?J z@|9Q?_>rK0p?Rvy27Xvf1dEj;Oxwv==^mX8oJUk(CT#77A3X?oxB1#6T7cnkGaG6{(4R%RNehk9!8iqKa8!?M6r5LlEI4oDQ zV>nHvqjZ9g^syF~k*Qq7)lHl8>ClK1ZwuVeUAM~@jlDBV>j})qvRH||$3#ZDF7$;~MKYLht;AKH1-{6H{ zA|ghpUPT+$8>O&<8f;mzB&a_<)g01hs|932xOjivmAFm9q1j1grnGcXGg;EFql@ScXKcOp7jaM>x?+F&%MZ% z?OD>KrLJt}Zw8)oWqWpA+58Y541k5s+%wVGA;Cwpj1-|OW>YVLK$0THiQ=S*^4WGX zm^n-r&0tVLlq3Ct%}pvRR9i3%w*v5CtI|v2Lq39-a0QRI!gNv~xFlm{JL5vOg@T~F z86tI9FSM0%$S6_SQjg@L-DIGg>Se%5tx+fmu_{6EE~k0>)9q<~_>}BS^B@wA2c27; zxe3hM{d+Mt_AGktKEf-&jQO~G@_O&i{8=&__uof&JE!&A+ldQrBOct9?YzALZ9U*^ z1n?hN?A=4e8CdHHqdH#T;U<2lB}WQFma&FX7+X~}LCIL8nMHLDF&INsY;eN@= z56Uzq3vglBl=0C~QZYjo)(j1e?tn-K#0GH`iwNn=;82zWL?nDDmxn4?f=rNftfDX_ zESOcR1tHy&dc3>fAkL=)&AS42yH_@L*EV)PJDu|LxuP?S!^|V^dpu|D`w# zSZ`hm8vj$1xGn5F_Rhx3Hx^t024~$8(qwp4K}N$05(OblLjyYI{Mq1Rp@~517(;^W zf=bv*vzcx+#tnp+s_`XmOvg-B%z*$mBxO|)wN1wa5X3bcqDd+VFeeoalqVu^1L4MH z``<}O%Eh9CI)a&LB|KtS3e-(B)2uYgOu|-*O=B6hYt)fUs~&3&8)z=Tkdl=SJ8d9N z8}Yoq5i3gBLZnqeGHL@&+ii{0C8sTb?N_P=z=7!M6)wUtsNbl=p;1YrH6iNo8dp1@ zEaGnoac*2KL^7zH?KQI?nzBF}<35t8HL8^$REkQRYM6y)ty0UIKGN^6TRM#3uYBNkHz#|WTD?w2xAs+LFbs*+oO_NKJ~j)QdU+aWw=bEd4*H- zK(3reSw+Qosxe2(B%+plWvLsq{DXG7Q_qGuG!%jArD&@XO~DEqD0ONj3+i-f=^iP+ zbD5N9-`HF#DX+?6P*yJ=r_g%b@5^$PaT1}SJW-h==J+8`} zHg_zKn|-p~R%63Ts5aC!sG=tFsfYq+a)xOo$|D%<3Z;NA&8P2`^6lIF&RgHfzkGes z>^Bo_>lqXBbVBkp`7zp}iMOiorJN+hRm+*&Vo{}naFd2*PmDB}yzR*sgvQn~sy)fF z#b{nE`C|nNj#W#ou%LED)|o^TS_4r#bu9`B<%CMd;=YEcgR~qCxU0nw1ms2wNtXfis zfnddGgIu66V+L~fnu~8)Cgs^T<(EmynN=<0ND!qON`Rx~h{mB@wg83Ne5W`^%0yf& z#8stgNSW?93`xU!uHH@reOSzIVil|BGjU1mNI2aJdtbZr?yWCwe$)D|g1@}r=N(^lyT1pvj1Rw@+Fmei z;YGvq#YD zFl=2r{`J?t9rQ_WS)W|uEhf6MEhY&wIVB8Nm2RPc2xy@kF~LS?PM?HL#6laREMbzc zfvY6ajW?iHx`OdBtfX}-rdq2HqXB{OMJzp)NoPU2&oEbY-6s#f0`y6DS)W|u4Jn#l zl_iTyN~s*h7z$EAeq$@kh4k`dUZ0S`HW|okS+g(-_4T?{E92va!gX8qt{Mlq*W^(> zL&QO{wNc{K_rQI6$G z^K4h7#$_6W8p;&fgr%}px)LyCE*orN2wC#Ag844lmBM6KhNYaI=E7t?MKUqQb)Re% zK%aD$^~ojP?4s$F%_n(<*JzWW3?xUIeUUU0DHJNPety_N3TlXq`o;>D zG3UQX4|E0O`EZi)&rrC0Zsj<9aS34aRyS86kQ#Ses;gCN+U9 zJ)2GyiV?Ds54-M@UmJlwQJ3|}CEn$Myx%K26pB~l$qsmqSK^cgn}#ZFvno*Z96gET zQBw>?L^KsI;U$JCu_%;e`we(95on4E$3yLMD?HHpDPK-!%2Xs%jfM;E_2ftX81zYd zS)W|uJt4BPJzwUMNf%c!-APC0>~u2Kgxi4}o$HJVc6F9#rRv_vWSyJfI$! zMrvqhYW9U8G(zUOA|rHq?NrUDHPo=x11XI%{dV5$*4>2ldE&j2#H70q?79PlM|;s|z)v?M4R7lz|7-0}la(+%U*Y8F)ZF@DRDh z4TJRQI6y$c;(R_%1lnvZmG^ZEL*(ec}a->+7Dm_F?XTuLS+e4w!2AS2FC=t%JC++dfJ?#_kO->njKs~^tVF7OW zvuS_Ym~{LkNVJ~r8iV=-#BS`TwP3ewKqavl(}IO z9!aAKD-`pCn9!>v;^C4?r06jQW+oRhpr;HxpdJu-kRx&iH6|+k2`Q&1O@@w=7|I2a z=y>cOHbq2RA5ZvD7;QA6-q05g z#{?n3yOKd{${@=SO{Hw5+s!zm`eHmQ)qS-%l?Dbz>~w>Zh}cTewMm-_K?iCwORL1B zfdw#*k2AUvkVZ&amtizf00)mnK2&FUtt!^}_QOmmmK=pOwjWP5WLoGa@;S^(JKkYe{XQXZa^pIv7q2Fl!7)M+JW?O}Sw}P8oPWJ&1*^VboV9NUX-Qrc^A2 z_(>{7)pXEp{brDj8p9;;MQBY}%nbu#%D@Ba0TfUM!%QF)m3h3{QAYx!MSM~^4R=GL z+!|&5*^DM5;K@4XN(PZB0}rSN!Frx=s%(`L!dX_S)|{C9;LTG89#9YJ@HjsN zuOEhaMzoSrq?PEYB8VK7V2d^2sUgrQI)~wPKM<%J25*`&@PK+CDs8b*Eyub-O2L&P zL^T5N0OyheSuJPlQa)R;5D+IKOZanc7`$=Hzys>RSP@F0kRLWh4aXG(t;Wc7F)@w_ z$%e>)1eQk50BQmk%evu)!5gLwJfI$QN?kZT#Qe=Mn3uB|k*}qqqX^o?S|PJLO88+d zm>C5_I##yaFnIlx0bdW(#iksoRfH1RiRQwCI;lV%$QMr%m0>Z^>e#Del{7KwV5QH& zpLp-1QwAPT4_5jP*53d7)_#2L<_B)(Z~WbjAGnde{#V!EcRhaXFRp#hHR9^$uYTv% zH(mMcm6I!>{m<;bXCK@9y}fts9qoQ%_tx%fc79{$?K`j7{^+)~{m|C0Z1uNZy7}Qv zZS#d2KeN%=c;5O?t~b`N0qE8Lu*cfgMo!%MFAq1s8>DiRqqvbUlruvqEX-J&gEHC@ zGO#m#vZE+$CzQXu~h*9HFrIF61EmG|k+2ghY`{yp$N@Fac zs7wJkIK#LFHbs6#%yN-I*w>g)LB$xyx?Lh`38P7lJysmpKYN&ukHZOyDG13PE9Vtv zXw3>ik<<}^nf_3Fph_U2>^Pg3iFPoNGLtpP=Z~6tmn)c2aI$2v$Fc+ar!HXSp^Rtq zYA!+euzJ@dFh-ulBOpKRgw4bTsxLlC_IZ>U=Lm>BZaJ`@a{)VmW2v?Um-yaD;;{zK zS@F6c#8Zi|SrmkN0j;K^@fJCZ_8RQ5A&n{rcOfSe6g&^J`FL!7I*fo&+9MEy{#bBe z|JVgAh4E-G(+sLPt7)k!8^Wut@~9m&!V?B~H{epai&?|Gu2m}Ral?WABNwnFmS}~N zJt7STL%k~)np%}25jj(iWtva~@#kv{!?2_v%QSmjcVIuW{5a)+0bvq#~y#91N-|fU_BhuvB!@&u)pU5*2CEwd;Cuw*iX5D^$=#AJ$|bL`=6GdQFsYx z%^p`B*xz*l>*45&J+3&gpL7B1;XI2yE<3QFZ~^P#kcvGnIk3Ou0@lNM7JJM&u)pmB z*25tcdt7v2e{1YhheYgg z&Vl`pE?_;J{IJJa2lm%pzkBR{r0DW$U7hbPRG`~ zZ?CPd_FBWO=E3Jtx%mdJd@UVR`T1dkB3uf7*f`4R=3^O^uBuyo?HLo6-{CBMja%sQpp zZ9VA32Li7zocJ1tlZ09XH~RIXLs)9IiowjpAjd4m9x+(FBS!M27UG`(-?R1uhnxe6 zyQ#2Hh9_(_lIJ>ovs&$vp%$-n2uq3|O(s}X#T(TUsdV|nP&FDI8(gaw(8mykH=5m$ z6i6Hekb0iz6e_4T%EMWZz{}mlclzmnX+8frsw6^_kwg=n9_Vc#>VC#ps-nK<=y=e14Z~o}!txab0>o)#; z<0m)Xy}@l9tpB%bFS+{Jt3Q7A9anQ#U%vk8EC1!nhpv44mD0_RUU|j-U+(|h{!{yp z?H}&_&HB&oePr)D_ZsW(-Gg`k;o4_*e{J`BcjaAtcN_Q&y#M;w?C3jhT>H|_)$LEN z9|Hovc=P+NJ$_TU`RL8P8@~;F5c)UXa^uGJPhJ0^>!a(@Yad*?9Rm=^(n9wYfH%dB zgSwv|w54L7V+^CAx5gk5RDw?QbfP}2DhW+s8DT&a`B*@wuD#Bw!Skay8>`0CqZEme zy{h4}uq>@9CD@EN{5`8(=?xNmCrhx>weGd#8hT%&%l~8Ah>=dgB|XcfB4a~NHk4AP zJ?g?nRnW~zyhu&LA;#=9$YR2}@lDejUATl+JB%2R6)YT!DcSL;z$G({auSV&+9}H? z>!q?UAJ3<6l&3YaL@U_qRhaRl-0&y*bf9bsdae;skW3~omcgsu5x5&x!hL<`d#5!r zVw&K^R8(X`VukPHnuSVZxfRn>LBS^lvEYD{TS2};S2q9lv_`Bh>+M`SC|8gaVb!CQ zmB>YUW;z_;D-|C24e_j%?dE*c)fb-DAoXAo4dHCNpfr1dWZyqQOdSmu6>JPn`zyn6 zrC9Ta#^v1ZFF~6kSW9G(ctEOkf_*0GVa0SWrSym2L7SF|<%MQDPz)cp|}Al()8-X^m8*NmeEnNziDdR;slX ze;O)egMn(i9EpU@e67^zDD5JdN$-7OS|eHJ2VtvZ8I5iS1PdgHTrY%Fg)Bede02fu z5~x`~nWo@_uAjD%M1c27wv&l~X!}f7;pjRy#N}9Tn2eSno$3ojv5+fJ_=MW~(`g$C zHN%8itq_m#NAbz=w=+|B|tr3rcAmn|;(0C!zC__HfH#SmCgGm>HgFvw| z!8?#YH!0CsYUAN)jcAez>ppxa5`MkcO!)OiB{%7S7i4_FkCya|DU0b?k--PX&Lh(r z6vTj6&N)NZ%E^+(q6Czw=g4{#>iF=MEVq;*+Y2^iE8kxK57Qb%rxgbN(X5<|Duw7E zQL9b5y}DX!4e9cr5tu}~Eh5z~v{JdvcTa0XTA{jX$-a@?$+XEDgC?z%;JOCt~9NIhl{>s73rBUUKA4p zt6ve3aI~LHs>N7~g@C6DN%^``0yVBZHLVe73k}vEX|#|~K*uP&9GZ+$O_1a^$3=#j zvIxSQaoJF-l{WTY?9>3UYm0QQUOH$S&9>8lqA|>t(^g~c zou@UZK5oh|laO$>o1h^SgY-;?9hu?$M4=PWe8B3CK_myUy{S%X_@Q1$8Y5<-o0003 zRxTflR63C^C#ivu36&Bmcq_hLt0nKqn;x&QuKsOYi*Hw1$oz|!ntJxtc)A1lN30IUj9WBQ| z_sPj#rRhh+dcIcU>$Wh?t%CcExiMRi-$^eLyJJII*|%6;b5y4j-rTw z^b~3BzfWuQ`An-==i!opO>z>!ps6AS29DSe^P{-c9F$ZNE|!u(3)(uJaJmeHWYUdh zEZ)WVZcr~;Mr1shfPk{0)Ues3iTs2n88ct9_J-3ox-h4M44shid14tyQlRiTfyIGSPp}B#POOK1?hTi7ck~Gw_ib_#YObW203{+-lKX5an zH$F72(X6Jj!wkhhdV{6GQ}1R{t|p5SrpHp5`hWzfawX1e6OlT-DNbt$9Gyq|*>;j=2kOnT{xe<(GTY8neDSg?lYhv04b z`qk4KuAO~%e}7toPl=Hf-Ilub6eI=-I2+-rRdbjq7LY|) z+pAPtZH^n0;cB>CrLy?|l7=Uc))y;1DLA1~RL@#kad&6hMpZQ-f3=s5_s6;pH6xi( zKbJvss2r?y(!7*ldd;|m;v>x3mZvo;2IPaHxd!gjc{$$fmk58k!sJzy%IO){ij;bT zKq^I4g~rw0X^mp6C?JyDs|AL5vLR>WBq9w{NI)F`ljqZ*QM%fyb0an-?S99!Mvm8` zgV3Nl9*Bm&jSsr9dM!{SYhj#}yH&`5@Qy^{NI2WR@~^K)*S>4{68BjzG5rveY6(;p zk3fx<))G*Wqsj@RP_*Ds6(=k)s!8<7mnycy#p^%o)TpD%2t8}EMBKkT^%pOe6h1g2wMY!yprsgmc=Vq6^b!i@;X zVo>SgmNag3Q>--RM_Qy&(&PHI@3HUyU$B-~yNTcEUjM+gzqy87y$$yCSNF-i?*O~@ ztsQpzcMzf z#aqNXNQdtW_TIR5P_iaku~Wih37w2fiGCv^2G586Z2}Jr!=q{(?hNxFS694LLLx25Ej_p89?muROrMsEMybon zT#V_M?t8-zbonjLXE?fy$FqLM$ZN{j->>i%{xaHxt-0~+r7bpP+M$en>ent|OT7?de!($$g=F^#I zFK>p>Mm>sAgFKf&Op3?)#Z)(G8gA*iEw^0k7k`VZuz^EGwTz&D|QbLVBGR>TW&eu3&aisu?O#p-9rSMMf7Il zb-FFL{1M2R*zvn!_wd@pEf2Tlmh-(p><|!p;I7y`JVRMP?_5Yux8;_|cd>4y75CUSy?uy;Rn~z2GW}|MpEw}u6!0Vux8;`ey+G_i zFhwDEKf{B^=^}bAcZuixzm|KjUhopGYHaJh%nMN5W2 zz|Q}7eNA55`{?fX?zFc38y{Q$zBT!pbQQbtYg-%Fzvs&Og6+Q7zrGD`y|b~AzC3y3 zE_+6^^?!B=MYgI4n6YOhJ=yxnB^24Jg0(+0xyII?x_lyASv=xcB3oCY0lt&QiV)Uo zXl)}~oR`p%tk$ebxmmBWNpL$g9GlUgfvRe0)TTh7`({5_jFfaNsFg(`r?3Bu%O|px zg`aeu_tJ?BTU~VcXC$lF`tc&ZOO3w%zg|L-t;&{BIXpdyFo{kAODjS1T{`ut-*~)Tv0Jq7C7Q{=X zs8}Q^f3+l(#kqMR!Xq(05qT&a(@9NE=j4d!@9}AQ%vFFFR7#G~R4@&4bCpL4pqSrz z`9!v|lmW4-+>p&!9!>Ofs52rdIT1g%xKWz zlmL;16DmlHNsYl$_B-tR|Ep`#+KunL{+g>lbA{de)7{5+p1<{jn{QnIIQZ*T{oIz` zvA(zQ*u(E^tgWs8FAEMq&fWOa;e4?g$&sb%B|0}iS2v|+*nCWaL11T#Esc5=uB8^E zb2Fh=%|WQoGeBGTLO(4bB@-=)zHA;1j3&v5UsC#bk<++ANs@FR>a{zf{`N(pp1mFq zM>64JEFFs!FO{fQwRw(bcn*Rye3VjBq?xXdEx*XkGt%L96$DW1m<*(3=vqC*vQ#=v zkGgfL+BFzNL-|}A>a*iPITi+@zWu}sc|%u<4RdxLzFxSPxJQi*zH)8aFd02GLN za!1tfSS0FM?|{20Dle6&S7xi9k-VNj)Z;s%zP&)y=R5-LW}du6qQ30uI)SK1cSL<_ zk*H_A0PZG~yi}rI)u?`EvTy=XTX#hLEsI1wd-H!cPvm71^~!dO&rEX7>(}wMmu`Gv z{pR=Gyn5sPH$vBb=^A?VV<6`LZ(dRN|HuA!@4s&Em-e{b5AS}{&R^}kcjsl>Pj0_z z>)&m0n}4#&ZhT?wCvNIDw{JXs!*~6A0T4SqF1U_weBY`L4xYn#P6Dfx0RJSrBj� z`r2v}|7owY<5InD8X4=>jVB9sBagkvOMLO=UWBKOOyiL(u!{KXvP!kzx@b@40iid8 zul5j`+%=(=Y?yj~TpP-UT$feZ9E(rpKzm1^Jr5zvUAJdDX4U!wRWzh0v!ERmwBsR$ zx$Abix+V5&syGx?gMTt(cPVJeL;P{qE!`P6X}b|HZalbwE_Z`;Gj7hU5l_cW3J|i# zjfXhqt_jT>H%ZW*J#IV%ICtINz2hbU+OfxthluB{+qr+-#O*ExEqRDm?z*Kr<0fV| z0>+I8Ki1`boUX>r?1J}n+(ZE(d)#=4dhVLgym3Q;_Uv)vAso8v_U;`wBxuJTHy+}n zyKd+HaTBq-6tv_awz=z;rsGC?GJM)g9ulD~_nSQ(HuHMvEvLQYAp*MV_U`SaH=p*B zhmh#5+qt)wZe4%!O?Edu_JY;sMsuZkv-=-g0Q{40wE2Kx;~@gB>rX#5H#eVrgY(fZ zS#2msS06olnKJcm)}DMlU}W1H4-pWTyG}10$zVmgzOl+>=|4U!@ySPl$ZeDBA9J#A<&Mq#Pjv&5H55zI3KUID*Yyu(+YAV9`lJ%k{%2wu0zo<2-}jTUAHr%%t2>d zzu>?_AWqj!UASyN=hr_~X58ijL>z4@jb1(|Ktfbhe7KfrwBi%9Cm}sl=vB*X8?6L& z0Ui_?$u*y;GGD*8yY{u4Kd^Qax$#>!gzJBK{r2_eUHkrPM_2#F)mL2ku`9&>$M^ZY zFYG;W^QpZXyHD@FcIQJoiS18rx3<2t^{%ZKZ2rI|y7BWHx%E%4>uZ0%cKl@y<7>N{ zFZwbeo~!Ay18J>ADo>N-IjZ3{yXAepmY1>7dAM1&AzC?*>Z~- z#5%WMc433FH=v6eoGrJgL9}!G>n?0?_I7(wgR|upHHdU>f9-`0&K}z=YH+sPq6VSP z?U!EEARd|t2eF{RbLAE^h=)42|H*|7&TbSJH8@*tQG>zG?U!8Gpi8yDXUi>W5U9nA zFKlqur)tp`&X!x$pucnbMHeZn|H1}mnJ;Q^w%noy!54n@g$>Sn94u;Zw%noy;m+;nUD)8P$y(Il zY`H}Z0@L7F#AeGaA_nyJ>V*x?8t+97&X!x$ zATaV*E^KhtmM&^=w%noyfz92&u)$e9T-4xfxkU{EecijT!C6bQsKMECiy8zLY4^eg zXBB@@gR|upH3(I2?`*9-CNIFjX4TMbxrgc?>4QoxatlNiz-|Bkt#xwk>Ob3i$<_(@!*f5LCGf8JQ-+g6?d#SLh7hV{)xMGs3<9}c zHC9RSdP>YPb%ZckQWV=Ix{$2Y2SO*j{MdCSxy`~HYJu~aoE|ywaQ5vapYd>D?3DKq zyJLQ`8V{Ea^ODt=qG8%ZoPXL}@>ChK)23bPo(yevj<2)KQS-CPJYaH| z00bj;2BQ`TUMU*ji{AE#(c%$*!2Y=YkrX`a7ps|K$S86JrKce92#uIlE>F{>smo%+ z48s;OIK&<}IgGy`;dlh+@I+*s$)XvSJ7l`(VOp!UMrJ>k0n~*fB|^FEN z&7_!EokT?TuvtuCVuc&W%ia9ZuwFMHT4{!awthGsE2b%Shlu{6hQe;7E;##9# zY-&76bm%f;oBioP5#MD#)pI7b!TN!R(X~27#Qsq#qL*2BPHM%w_g-Hi1-wJRo^rZ_ zx++i84xY_-_8R1w)H?{bHGA#^IL`Jse-VYtA&9Jkko5H|MGPWHvM8#`Xfzx(bRliz zixwn-bYuX;kfjc94*I7vsj4@4wWS;8qro5qIg_g>Abvj_dX%p*LRh!In9@Lq+h|4? zOJ+fAwgwHfhjN2-6J+hk4da2s?4VU|$9qCI#T7@r5!$wr=|Djlih(*vcj9Lohmt8} zQXHG5buus*S3^>PbTt9`gW%JF-o3m1RM*zKryA>CmT;@nJ9M7jJ?o1@udBwbkaX<&dPrOq&OyGa9uL2z$-=sI<4g4pB-stNuY0)GeG5Ig1+@YFA%!)qe=N5B2A zr+;7a@>in@SH`-@^22(oUu)ogvm#HHM_%8?i%-qC!Ol*U*ofEjU6cWG|yPY344 z-v7URjbFR@#Erkd@y*x&$Mv^e``c^dtABmfy7E_72K!&!H}?M9UT^mcyZX+5-O;u` zzun&Yvn_e^bDPq}pKOThf4tsU`wXbQ_~-Gbi7ipgdGTmnt*hkYPe(ixSgBX&<4=bj z0v=pw4?KibUG6OU_|tFkOkkzns*gYYX3qpx+y!rP2zc-+UUe6|(KCS+cflJx6IgK< zyxt+;0d&<}@Tg}3EAE1jL%@S>ui6p@9RePl#1A|K8eZ;w{P@$jX96pAe}4SwfM)_L z_4I!HX}?3jgIoNny8!b{V8va4dM2>qE1XkPykY@rb?t&wSfCtc3 zcfspC6IgK<_#6Tr%fJmm4GU*nm8htoYb z1YYeB@Nl}i>MnSdX96qkf^YClV8vbVN{4_4&{cQAD?AfeX%}4C;?|DVKC-s^J3BwU zyS6*pef`zXU;V(<&efY&K6u5r@`nAt+5dO@wf#r-{(SET_q4qmYadyE<@)cgf6sbj zmR>vTn}9PhigA|?bfxg z-x=>jcDA>Fe*4=%^nhR4dJ;6cELG^{&n!MuIR7u-xD6g*Z9LotdAfNz*DAASELtiQ zrT&bUtBW^A7=FL&4iegZ{~NZ!3l#V8_nyM-{>uR1W>;o3f*s59B;IQh38AIt=X0CB z%h>(Yo40czFV*fpyW9N(YCHGpSG#Z4Iq?&X`%lStKlA7|c;n+9<<9kivHR)BHh7Wc zzUuko$=yFZ6P=G~Ya&w=h?K^whq&g}7_quKwFo{dQaRUj4rMBc}@Jw(>>#jfZ9x@ahLo z4X0c9>6~-*|2#7bE15=TUF3J4KATKj!rvA4?)%Or6PH!bclGY~pG_t%Ax`Jv?>p<} zp~z*cg>r&Pj5DNI&oc7|*}dJo_13d)c3JtNZbr^M#o4N!jb4}VcLlojPtSD6W!3Wn z-Fob7khz4I3UsS-CQx1LcQwn(e zul=}v|Nr8(%G%BHjUT+RbDh8TGuK{vwRh!LuDoR5+WU z-}ur7v;Mxd{})tP_H*l@ljq-DTNl3}x3;#wx4ymo*xs&}aA^1K)Rza++FjR-?T9aJV@a8|~K1PQKda`1N-=?i^^cUx}bhY+|s;0O=tHKa!E6AMQjs2^vUdq)@F< z>575$+ov3DPxR#scWiEd*L8lIt{J}eM#JyXuJ-KRgyieCXI1Qb0oTKYY`8vA(pZ>z;`}<$z zn(x8M6`SvUy6(Zl=9KR}y1taBqwBx(`!>%{{-SH1uRPhedETe%9^6n)dETe%OPM;l z{+qvJGyUxi*GwNe*_#=q8(y4XJdRRF*Zr2IG zbNgt+`Ta+(89sEfGb6=aFM$#7mm*V&B}ZY6?Z;CMnHKs9c}CZrmB&Z2Hph?r)*_Cx zuRX!aW83C<-^xQxW@(j}G_U~1@o`2s0@4Uc>oSZc3WB5;`B0tZwW?U>?^}5+W$0L= zUwF68@E4wP&G5A+TQJU{eB*F0Z&vSIVQ&l-7fggE7SpEX*_)UifC@ubc4|M_9pOdmQ~ zx0&9z^6=oW;8BkpYxLuvvN`@=E!P}hcCt2WjXXGNoKifsMoYzLbNjcSwtxTVk!ywz zKlOwy#aG{5Y3?4h5mN{R_3)rV_xe;l?yJC)fkLN|@yHjX`^8=)8Z87>jmib@NYOBO zNnGw#;FzNy{wbT}5C7~UlCuw7Z@uiP$8D1LnIjKRnNLYN8zH{kNo7)_Cecz;!)T?| zPRtlg#~l68_u349=zXpkzV@kawi(`MjyyQ8oHCqaj+XLu%+bHrZNC3zQA~kz5AJUr zJoR>)?|tUTgQv$S-+Ro_Ql5@E`d5xz|F=bX-_G&0_y4}NA78uqft&doe|O^tZltgO z)%EvXk6-(XYu|H?0KNd<3335^_R7hX(Eexk-?NYH{odZY_KtQxv3qOxH9Nnt^Y)!r zY=3my+I|S!>i4%^y7}QvZS#d2KeN%=c;5O?t~b`N0qE8LuqW)+Mo!#&&BG0&M3tKy z#f^laoEb`CVP>!$aDfbkR-4tLut9>A48pqui74u`eKC~jq0UBg*_=a zu&;i&pA#z$h8*VPl)=#TTz_^99|-NT#c5ir-h~R``eYnU)Or|M&vR^&Ljs{Dl4wrC zq?W{ELG~o?z`n`_Td_tHbX}|a!*HKWb~@pn(vT&iHOv_io;4EIxHnQ223GS$_9W-P zeuE3PidrC2dTbdRB3H5?_9W}TzS0HTGTIX5 zK8g6!!OplaD!@ZBUKVLC-KdVzXtAg0RZFb&`9u$6PZ$UG6)xB&VX0N<@Hjl?@n}As ziT3hl2yN7(7&XXq3B;s$tY1uZlcvF*WE|K7H?T!Lu2fkp)J_k_!kEUC-lXgc^cBV@ zM2s4bDvfk5ZINmh$Tsc3e!UAJNMkIas7xUoAH%pc$+!H9nB^jau&*(pf{HPYb-P5? z5=N68dy;ZsL7)PV`7S{*1tHmE|$4lZ@YDPm&Jo!!BUup^RtqYA!+euzJ@dFa|iJL^>idVKcFT>Wfd3eI8}T zIRcD?gaiAK3)lf11NW|QiSLah9&6y76|W0IJe3HWMM0<+&}upwZ;``jufd+s4(!X_ zz-BVzF5U*Ool5Gs9>|Q?0ad_*LaRN=l~o};k#dA?bz@i#z{VZem$`ryqJCD#tC^No z(~3$8C5g~r+|_&>Df@F-GAScM+YFG@Bnp%_=D>cP3)nH!3-U!FhcD=j3s?%{(O{+-RC89-QdKsDS6k&#J7|O_Og5Il zrE(XuhIw7901HMrurGB1OJa#uDA^;@U@+9Xf}yEZDH4%0So*k9?-1jl-QGq z1N&kZutiS6lTjFIBPvUc{BacG1ywS0WV313A!ev@)l8^TL#+@{hp+?tA{Vg3kWS`I zxaO-Bv3#K!6T8ru&=?LX@uI;E=vuK|gf;aD!^YoHgT zI4raV0^BRpRI8yj;c8tJQq68k0I+X%U|-+@HcN96oop%-y^|ShZAOX3T4b~2&t#*S zNOHm%{sGZ#mQ(d65W<@r*spN`Ys6U8hw}szwj%v-slc$qCN3v2qGiwnqDqaS8sCbt z)p`ur+c!F}&tHD}=j9H8J$Zuz`_(RBJskV9C$D#4pXUPBL%tREz3u|mLnapXB;deaTmD+o%ds$f;&))Lx`6d? zlFOc84(t^dupSOv*%Q=(-FE@&;e3=mK^)jU7qA}QzOg5;1H0=2*24iLdjdJIJIkM| zc{wy>PmUbeZ5OZ}&iUAr*Ez6TE?_+z&ao#x2X@m1tcNo+_T;q=?1l?i4@X|?$)N+g z?gG}s`4oHd8V7dG1+0fNCHCaiHtg;{EI+I9at_3vyvl+7dl#@CPHv|0|30vG?TP(= zzVqVs53GK({v6^;Bmm;VSZ8rzJm}8yu{{$Q%?`g~n0=AgYItY1a#ZnFjSEt+RH$RWHBA?c9cT4&@3|X`e7kR z9|>9!PB)@seJr8{I`!aTsNmEvNQ-TYND%#imMHN@eobv;vb1GXxB(|ew2(A=r zPEaA(k3$fKAkL@T;opW&4W00B<8?1eyE1cwg}z*KV@FqWLI*vly|Mvj!b(lOXud** zW7@dUlDnYm9Xi(zQ z8NP~G>O?R~NFdC0HGeb8CzN4dAzf-7_;jFo-zq;fur=@7YwKR7*VSoW@Vw^D(WN=s zF}+6p3TcNE26zZUuF1L`ZR`=u=HW2X?U-fmC)<0K0$#2Zb~6t;*bxM?qp!^!?0U2; z`n!4&E~Qg_f{e7pbgz{Rb#i%`N~LwYGC7PZh!{%r^aqZ1h*sD_N&v4fgpgIlV_`a* z#jN~bqSrNpg3BPZX#|Nh%V@oytqHyf##S(zvc>~4WaLqy5z10Hjw$^Xh*+wM zIN2gxMLP`GpKebx-~5!hC+2Q#z6XRm^l&S_d=oUYthJ4bIqKA=fud2&Z};20buNaV zAO8Ocf@yo$^37WpYog;`Fsr*u@0=YeiXE^|xJbul-3{zo_b!PwL%KWBn&QZb>s{A{ zu?-5}7xxzjRnJdT52}Dr2w6DWwA)QRT?bKka|n%P4tsgdY*pKuIDjTOLRCv}kI1le zYS_v@c)abfmPcf|5KCl6qh^w)7zS0?k`e0!vN#vhx#rPCx6m#WWDa|BHhk0wQ%4!8 ziDa^5i42Q!T?rqxutL2VYpY^|)w5hzj<{1m`_t|Cf9p@Ly<$yVyZPNWp&P$^&=FTVR}5Fy~Jc7AZ@E!&^i?ymjX zx_^6bP23!B{^92HHa@TsUjO9Q_iy~=#!qhC+K6x5T>tI$rvd!`$KHF#J91Wc<9m1S z_C~0t`#OsQ7}=vfqP$9^$is9p+zfBB{x1s7g&A@9x}{9t67j+_TS zxUMM8%b-h3CHIQE?1ZyD|H1&oiUcbPQUl=J@af0w66dZv;cUgZz2e?G;G7fa+~inUMR8MF zh0a~S*d@-rcEZ_yZWasz#OPt65$u4p3u@lbrVmuT<0|H~h(uEbyJ9dQw#;N}6j@5rd&b{Kk zJD6YH?$Vrw;1Y`q)YAMLH|1TK?_P1Y9dIhYNDBf%O~O=YToe>^*-elA>s{j9c_*Ch z8EC*IIZ_8Mv;6EgeCa8>#JSTBIOkNO;!VUvn@tm4;k@&Mt9I?1Z!3{ekOXGNnoldWrL+=`L}O?}W2`i3d|=Lq&C6 z(w8_N_~tHgj_riA-GAyLqv)DQa?2R~>p$?FUE&5o|a|o%COFzB-IhX7b=kN|V=aj{wn@XcW@Q5idai(wFCC;IpaJKs?W6HEi z^MqtAakL-r66fGfINSXcqg4}Ecu81|gK}?|I0tsZ**?Evq$DG}XqoD=J>|hJarWj|~?|f{RIQw?O+3u%`qN#{s z$j0*fT=y4G-X+f7op8?aABz>#ra*JcdFQ%UYP-bQ*a4@wy{sHgXibbHabY=MU-#?> z?h?+^2VmbNNm<+t+QTY2X55YNolw zVeYw0oX72iv)xZEQZ`VmsVU2O=ekRuxGS9F8<+06XwPHzod4;6|Dny_--P$wxUaYG zvOOQ%^O(IK+WX{|Bb`S?Z5i(>H+r(WTyeJ-Tg%kH(6P4 zP|NA;6Z`h;Y5-KOc=#r`$@=BDSS4utk|t3cqggu3((V-x+sVM~>lA^in99+DzMKbd z{`BAO%0Tyuhwg;4ecvk9RZg)ej9;#YH^246yTp0Ozl~F@ZC?+YrcuWvUJ{r4h?`%s z%k|@m2k(TleVyV|(~|3gYA*LhH=q95F56Sv31|DdD>f9A6iB|#Epc9O^)7MXop83V zhX`A5N(e$y%YD(!zkQco;#7CSImaQ;y4GYkPG4S8-rU*TB~E20ob5AwQ*K%mhX@3< z#9{8XOPumfINN7%LEC1QNe3$Jh?1Zy@KA^!6u&@RyF88fBeS_X5PJRa*c>8?7*CiBXFpF9G z=}n*b>Mn7hop84MsZR2OK{e^-a$Mi^+7r9Pxojt#?eh-OkT{&@X?)dh&-uVEaW35n zXZw60Di~umn&@g>xaqflbC)=|op83#2UeXY!A+{Htop54+a=C}cEZ^{AE=BWYJ^;u zmh;<9&4=w0=aQXpw$BGxlR_xad5xu?-gNikE^!{X6VCSefRRz8Y4J3+To-P-`(?Yt zdB9FM+vfwYmg-;>G}P5S_m4ldOPu@fgtL7W{MHUQ=g?Ui z6vy+NObE++;Tvz*?LGI!JK=1f4^RYaBAh|+#tP?(UAE_bJK=1f4`@VEEmXwy<-OL8 zFTHA)IHz{P+3u$Z$|EYpQRwpAcjMskyTtj;op83#J2cMJSrJ@2F7LT-eCj85iF46T zINRp~gg^v@u*~}MynN$BcDv4P?S!-4PZ6BbB$N{QrJvq--`%c17w&|!ecoZgr7(Em zN3_*A*t*}Y`VAbUdjA!NsUrY{+6(PD`SE?<+4sPG??3tSlY{+lKXAdx+R1~Pe|q9m zfDs@%@rV;A4}R|Wt%r_nKJEA$k7xVubDTbY!T#p4zu$lQ*xL`{$F4Z|y1makCLX)+ z=(mpk+0kblH4i;}^VGga9sR9+&)U=uylL-`KrO**k347d8%Nqlpo4cj{L_QU;p_Ic z55HvZ2lqk;8h}OM3x|7$tNZ`{@PR|0JoL&#;eSQTZ^!s`dLX~g-Fs>gy0x4QL8vK* zqhXirvehx)N4RQ&F-W4eW0CAs2`ceZXYaGu5*3A-RXV*!eyU?*nSy09=oWiU)6P^X z=gnd=8YML&(8KZGt5y;qp_ON>COIU$CK&NZxRt~h;ac-L$t za{NUXAsBO*C-%-(5^+{aEWc`?nvttnIyB)X^C}s^1t;mS^&VRGP|S>nt}2Z8$}5RP zm&0DB-~nQeA&4pTJ6=(de2eKOAXB$$;S?4tHas50&fbTtt#UgxyEZ}lx`cUypryL` zq}ElSG;g)?-MBuqldOkL7&fQwJ-U{FxN1XpW-OOfTXtf0W=Lbg%U)2-i9)Ublc+Kf zXdKF;;>J(b?MSYRg=~t(J+Moqn(tHEVq(Jyy%3{h=Nl1LGN7L)-hM%O@?y@+5^&}w!8pM%itW-0G&+_<)iDOl|Y zskAAEnYO*@ly^MD(1KxD&G)J{5__!a$9hbT#Ep-yB_hSvXT|X>1e67>YHltS%a+|S zX{Z$GwKAVq@PUyNm7eNsd~_u-wLE==rA{`;HiMx7L zhXI$u!tA%;-gsO^Yt2eL%;orb$rr*dUk|y`#(UNnm3Rt!HM*Y*vSLvxjr)-oirRRN z=NeU+>`D1XKC0<9jS(AfUr8)7jT`jyAPPHmLWR(VOip$e7{Ab@hQ}7$J=Q9fEDn+g zapTQvt6)+Lo0s^S>0t$3DCesKpH8wL4qVLkmoGxEwfN;}$kV!7ZXk||)8d5bnO;|{ zBN$g<;$qs%7BsZ6p06g}5M-j{#h$5C<-}-m?y&BTI>u5Wn)N~S>i%gh3HhUX0NVN!XTecM#Nxaw33L2-DHuWb-5e|ylnPH z4W-jS%6PFgBwAey1V-H?6s?g41+%rNphMU7>0-t$1VWET6wbs^ zWTmL7IG3$#N;^Gh)HGS!$(Nf*cU}+*VMZ}HFQCPlikfnd;9 zju`hwDZv0B?5M!)v}S=A8!~diTPsAWAbp;EWkFd`)o7z@QxRo zxtx&3tvRVMQ$ugpno&I1^Srf$Jk>b6M6@$C>Bw`a=jBHQ-y+eFf+BK0AxUIVwCDBw zg5Eg3mf(7^&F0fOB<9Kxs+P^ltc<2zh$y38Q=1nnlG{d^R)6GeV3+N451*QY%q6HG z+5*>X@y$h0=cS5}o8rC|oibRP9NimwJM? zQD1$&dEWGBmsL`!MPBH0Ev(Z#vK_nHplc>O{OMRxv zTG6PV)Qd%0u{wpBF^bqhxeZwhsn@CxHYzKLI0!p7ATtW|zE=*Cu!e->gi0;6MspRF z8k2Kz1dp)M5FKx9t|d%BVd8VX!t2#(I~hy0b{eV`IM+t?5oyn6)V#D<^a`W0xaYNN z38U2S4GYCKhl9j4meMI2E%cOLOBx1hP-;)mVY#mt3alw^lvfRpnTiXOl+(!Z6|y+v z#Gp!Nh2Dtk0rIMR(x?}c_AHI+{gJbOEl}^kc0Q{@%)$!PGSR|BzvS0RbjIcgld`)r zygf@uf5@m0U)eNsT16?N*+FKg*#e4NV~~~;6>=Icvq;Ox4RA7Y`~l1@xD9g!F)I)q z6ukWUNfi`D*Ze3IW4J9k6EI5+<-qNi)hSYrx^#1Tas@H%&V=7jCnIX{(%w^>GxD6@r@Pc%;u(2)u8uKX*iPnB2;Wxnk2EX{HjJh+ONGXWUSB zhE^Y`5>`}!+jCPK?0My?6%kDsY~cbCW~g((_ORJ_8PLA+8R=wcI&*j zu{LTlD^ulYK3vEM3)s+XQC8+9C9$Z4SyUI+qDQ)xM$wTQ&JV0wF)_eqXHgs~<=RNi zBV37`wI`)A1n#)IzMhQ&iswqIQ3)vIkt>@fs@baH#(=8EjX4+TaVN?xQZ4Gy9Z5m7 zjG)TuBri9idTGzCYYE20DQ8SpQq#zxRTM_4j<3u?u3~I9$MamJmozBT)NysZ=NW6G z7)cy*D1-Jnjvo0vIwl=ea}Zpc`15Gq4?HyJ)y%9@1=AHwus+6g5(IQT zvXj_=2E5T_>)B{gmj`Z#f%bfLB@y~%YCIcB?WA2w0|7$ASw2l&hMu`?8N*<@j%d=1 z8RB50x3QLJR`b(FH0U$a@_5D*Wo}qZ%$$%UwYoRZCL^x|mZ6T~cg#KST1!A9%o%a~ zGypqjK#S&B-HuAr#bFCWCKN>wbudMC%V~K|Y&>u!0XAI*Rch!HxH5n;bJnaXv=pMX zktUVv{pvKGv6D_u?==K(&$CutHIm4NRknvzQBM%B%=rP@j&rPxkr125I#4HX^-!|6 z2ubt!^8A0-&DZZadHKQjAAIP6cOEG3f6M;di65Pq9sl;`>yHP=zIv>4^b1EHb>uTg zl*2b3W)FS%5Wac8eXrPe@4YYHdzX!8ZyewAJHO63@*Ejpz`M`+;q5=Ty$5dZf!lik z^uUw$9&lap(B4yg43B$t-yCy`0*!f1a0ersJSRXhptW4&%xb$uj2KadP~;Tc0LhJG zI`E=;l11vclp|DT=8jER_F%_U0R!kz%Jn8QyR;PtZ82MPnno5U{CG0))NRyu$D6m= z5OQEj58V;lOSPm{%*uw{f@P_LXjoCJ$s-*d#*vd7Da0wSTti#45jP>JxQvoXGpv_v z+-{?lVvZ{9J1vykudQEyWMa&k8bqm*ftwBUUi!d{bsi}wsoo!ay3Op z?3SyPqcq}kfNzp>Od2b-2W^|SD)gzQtU9XVsgA-C!Ble_I#bc|y-LnRB!rkbU@t%} z7z}A~(3U+SB4%dLQ;i{rX_V>QHXDw8;z=8;Z?SQT@7Lfa(rgPzjnHNmQJL6M!^*@8 zsEbf;&nq-E825)g2ag7)st{6;+jxVjYJP_{W((a_+qwzWTb)IT;}tPifn22BW-@MT zuu$-rwx>-#G-kdu^}E|_7~a+mJzr2KF{bF^P^^T5bkOYQX`+Fte5*g^!C>$rq{-K^ z8hol9a5*00VK*$K?O{wJo|9;*jwjs~CYuIP6oibU!yu#JEu`O%z%}2nsbEoDUjPo% zZ6@r!>^2*^p&FaK!!*nmy?_DFoz(Fpt^#{TPFNsf7gIQ}hwCo}#HnIlE0(xiS8U`G zu#*D>rwPi^VXN05it>E0aBSVKRjYWuC~lFo5GPs4^~Qu0gYBV-Y_nnC6}Q>o(xYi^ zi17tuin&^!8_WqV!|E;q*e%t0ov6l`145qoF>#7Y>v}g(l1N{)u_jJRjuj5Hv``}D zA;#tN$hdDYxP@^_Y0K@j)q&ZdEtD60C1xAbZ8mJY;9qPI&|(!2Ek!U&4T|?0nv)v2 z65Uoft4M1aYP8bnkjtxlWS;W71*Ru+-5_K?}hAKn~UJkN-bjEo$wvI!PQ|-n`D{FP({vb zX}X11`jz?c9fX% zaNgTqgLZ#%n+I7j!%*_0t}R7*d!XvY$)GSlWrD^wkQ536 zexgh!q%tTOogx^RU^m&JYj|Jm&r8VwL#N{{+ZU$ud2eh@`aP$gwKHd1KOFnqZ8lUY z3op%Qaxw>3nnEioro&9h*C~o#Aj2BCiYl{XX%s2Y^prm~1ViUpx7ByLNZ6nFhS`O> zGzo5jT2^skVeOPEg-9zLZ;iS$iVfJF=4HZMAmeVh&4vS;x7kpvC@r$zXi+_+XnJgi zAq6<6)fV=Ynpdd$1SA^~9cfmoiKnV_voO~iO?m8Dj8TRn+4M&xA!-)+*?8J7V|tV5 z>vfP~fo!#Xly)W~$_?rXI|>Ivyv>GV-@MI+ybtA#2!&$O0&L1u8}=Bx79pt8ow}Il z$vqz)#4UPkK&RraHqTe_MG-Ftk)}EFP=zhKl84Q4rCITmf)SLQJR2q2d~4XvXvrU; z2t`F!A|f%p&4zuaZnFWJDY@1p*7b2dkVm3zWo)jPcM1)ESZFyt)M}1J4vuCTa>|{; zwhykF1{$6l*PY&ol5#;Gl$_+ylIHq7wh@)<^FdN$=3CU9?Wkis3{p)Udj_6u?}sBd z|0^51g9cTvId&UVv%+|s(VC&2n@AB-pQm{rSLabaz&Zk$AG)>9%!LYYze^VzR4Lal zR*RjP7j|ccNe>s=Xe_q8g%ady#MT@|MU!auMQSugGCU-<+i>(P|H_6!IO3XxsaDVR zQw1LeavmX|U=mv{5n7NUl6_Z*TR01ya^!AJ7vhdq(Hr4-;?3B0FEnyR9I2>1)b^`| z*-&cb)s8dRN~9s@j2pO%#UX;vC-!!~IPx3+VgsKE8lC3HiEfTd?YTGZLghG{xeSis z)4=NUNROX19FMmKrwUD1%QLxX)UHfehK1UqRwzxHFqsR8X0;(!R1mb%s`ov@+o}uA zv}ZLPW)LOv5T_Gk+j!W2%WXEqEP^*7PVHKQsDpM@r&aJNzdVd>r$AP*Zi3djQ#7tj z#Z$N<6q}8I7kN zo)t3+f{k1=svA8S+{5aIg--Q8xI;pylDVbY{tWL7DBJ;@#B4)O&Y2%>^9z0|`Ei^U z^+qhFisz+jKUSRzI`DI5Nu;_dKXQuQrj_XMsgb7$Xn;0SW)QP{Et;T%WL_2GFtn_h zjt$|s2d-86?OKJ~(sfGLk{v+Zd1V!6Jwv+)xMGY&de|Z8o%Fwo9-D#Hb|9Oqh~+a$v^2(KuBlHS5jbo~&E7 zp;5xTQ?n|j6sJ}{!zHuZ&1D^=h(OKeXc~i8v3g6Wg;_9Ea6g;FJFlAub4QMSVGpzC z+C6*UxmVe^|FJh6yZl(=*ab)b{^;9JzU=7fqs^oDIr5Doe|qF;CvP~S?Y(I4l}An; z{?6g|Z+!dkvkpJ%@BYxjM5-&^-RaUZ|$UK`flub+gE zCnpb__~eOKo(NAo{P^om9NXQ=XLU_tC6i*Yh&N|L!+|Q`DhiQF+Ze;uPFh2X3TF(> zxT5s*bTmEj^|b_@?>Des+0wI8&Q}J6Xy;wciaiEn+a1^hE{f7qJ?-PjiS>J!ImMF} z%qboP4_eqLYq;R83{K0qTU2qupp(Ts>E|mWriGk%{R*Q6IRiM+?G(xdP00frFQGc4 zljfihf(0-nx?D6aD=BQW2gkc>tHM+*vc(XdkZjx)y#cSJVqfN}kSX9jRmbh85L^3zFgsLo_c8Wv2wn6f2vOdoseS^@zU`Gcuc zEhdS8vtakv4-`d6a@{#!0+;&pyn)3{M4oxa{$MRp!MjdRvH}5bh-N_gmeuSeLh5>r zR;8<^PAtG3iUrS@;<3A|B+Q5rJ8@_h>lLNcZ~0A^XN?6(BSU|vDhh||V#SgsMP3w- ze&{SlU0=v08my^{ic|rSMyyEa99GV11wLa+3|SXz{-_Dkc8)%AjWH>^9o#@-8P3CM zl$HZJmAHH^QZ08{95<~-PVU<+OS7P(;!2{G&qulnLJ6Dz(vjcsH(v2A@fLQC1GWPSCj%$t5hY;#`BH1p<#WKf~~Pw5)}u$8_mar;7RIK#%uq&nDKzfW3Yq++q6 zVrr5KO))&q{91Pr$9RD%0t#;u#leMteJ(B{btAvV7`18?(;Fb;k>a$RoY|3TMQqND z%&wiU`SX5-)e5F6G{)1t-#RN%D@{{Es76J(O{8YlZBUbTY?KRG(d$}S+a?F~fxl=_ z^UZ%+OAI@G3=*o&kn&TUWKG;dP=YR?3x75Y3yr}99;R;3SNp_~r>-UXIk=e9v3`=d zJjzyU31JV4vj&l8kv=-0rmEq$JTMk4Zhw0%(c`-&idsa6pgM&r+->86l1p4t!>2aV zRv}#tHN+H?Y;a^9dt!M;TDB9Ivzuz8EtjywkB5AfaFVDn#RNZzZO<5Ufij`@o%Qo> zrPvL9q*NKvVWCu)YbYLKBOD6nbbA)yiG{?&ff`DQxcB~RtDJC9N_8Qe$r0gM^?^5( zn7+n@#<+luN3DWU%Scdqpxg0|{Nq}}9#%>MmP43kt6?xyns4R<+R73@_dYj+pn!5i zuB$BIvA1!-SqZXRSHl5N1bV&p0uh`Yc$^T#j_HHN-)BcX$$Wc zOQ0X!xmoiPvo!I{qE#BIV5gZ5Y?k8a&G}kFb>~fuZH=n!IO<}o+Um>ZoEw2?m1(Ux zuP!vP#1c9Yp@Rb-TT3V)u*cU+tR%@D!XOurJ+jAe3DlR3K>(-RRDBO7O1xM04xAk& z3N;U4CaT4(I;5I8#urE`PwREFz>AXwJD~E8Qvq#Vm`AtP7*dWNc-?sBaNuoXKFEbI zxKzpPo?Myq>ZBCHvOLxG`8?$|*D(TC6Yd~V(FszM>V;7~m=?QoSVuBAGKF@f)2)Yy zi<=fvpB`Axq83+@%OLon-F0flRCAJC10B>6K_=-*K{I*{QRkHT)S?XN;8~Aql-ma8 zrQEEDVL^!tFm#}n#$hOfM0S>h+b}Z9f)VM(@WBh$?Vyu{h_EatkqZzaHt$1aLPy3y zS@5fTG9HO}zQ7`sJweRP!dimL#^9-E(Fi1gYhxV>9ID1%(46oN(Zrl)5;k<0X*xOH z+q}YKQc4gUv&02`uqAyHe=|6TsPpT)q2ogzQs>)x<|h*2?%M9JjkVS&^~Q8qAYUO zfCnY7#x@$hGwZ>KQOZhWq3CukX&y@o*d!Vwxj8I%0`t(7Ypak#?D2w*OeApELvgBL zjp}&bZ;yIinDmG~3j&`u}9eCU}i^b)#W)4(b3!tcl zz;n&T8U#8kw@bZcKpE31ZGPe`1`V#sLY)tD?RgU#Cn3_!7?X5calZj-isV~)nGg}J zA`Fh4{T@(8%$b%Ab)2bb7#(X4cGv!pYY4d-3MOK~Xo2%ySDaAJfpr9*1%<}krn-58 z0G}QN28=3;swF}BVPNC#tclRX#A|4Hl#LIrBSS23+NPOK*eyuPXfW=uBcw?-yH462 zgI!hB)JA+gn}GTo-hBT$Lc)yaaA_1Y`)?vr(X|GO8zP!`xg)BAU|w1`W6v@OAnvG0X-s+ZHJJ#pV39RJ*&&hAbTzy4pX z$Cb-M4#Y`jQf0?PWb*{lDl;vnFO96$eCGBsvg$cQ-;UK2>zzz1?^}g}9|zThVCles z#Jt?-IDIsyk)k;)=?$xsDnw}b@CkB-(ZV>0;Zj6w=j&*dE2g4a$TylzYEUd83&aay zvhC#;1(!BYTn5@xIKMWnG03247kCM;)gq9g6<}b39L_+Hs?bZj9_C6;BdlfBxzK_R z{pDJsQi=vCpNI0fgh7q0-k9QpFudHhjlB}!h%wBWgr(?c}v;NVz2iE|**H|3Sa z%#|}&E6?cAuRxpY9YCvuk1$dhq7AS{n{5b0Ob{(ywLthvJm%60$Abzq)v*Kn;^ulT zX%#~uCxm!vfr@5+ROv|>UKk+#if2|SmG&D&r)Dc14BU?FSw{)B@>nx3!;MgZ%02&S` zwvZzwLyJ4}6YE$jD_0X+W(CW_d>2XOVW+McT;}(q96eibISnkk^ehpCl82mFdz4k+ zoN8&rmZNgoreqkcCVg;aqVSE-?U9;TE0a9bn>!&rK5^jx$@Bl0*Wcni&i~h}B+lpj z|El%4J&*H$XKmH(=l|Q!|F@t2Z$JOve*VAx{C~>2{rrFX`TzFw|Ly1h+t2_17oGqA zVSP3@kMsW{))MD&{(r##CFg%@-SYDPd*}ay>+|4wod5T)CC=mgzh_P86VsWs*wG($2byKD@d*-$V z?yAtrFDErDR<`r!EDip33$9d4)n8dy?-CH?B6G~JTWAm(1uE=GRFiZvoT(CMg_k** za#;`-?Sr(wU%TQ_cRa8XAG%~FPc85!<`yVuRGoMIas#Osy;^Zr&zw2PxNO4RhD2EH zk*f_&whm(S2AR+(>vD;0D$opu?LJ=97P?dtipcKDd!21MNV5xU$DOuTeZMSlb3XO; zKncHnSJto2R;Bswy>aS1hV0HGaa+lxUs(yX_&?IS&!ar@FZ1r1J6)9}`mguxe_fEQ z0(|d3uKZD@{Hu%Y!R<7!&a8+i&na`{^5#NsL&7CYP#P0y2M(NZOkWm@pahZkYkOmH zdRYPLqN!a~nP$pPw5Fp{g~|+b(ssT|7?;$Cb-tO&Ai9PK@si954oDf``%ty6PBCMY z(YRw&GM~Z5R^JC<6K(4<6ig1w9*h6&GMCGy17CXT{;SiaFI{!FjZ^0_K>p)=sqjD2 zm(HUm_%HLNo4@jEr+zdTVYijp zTdrf5E{n*vqBwA;|4k0~F9s|(LSftveCHSZZdsl0Khf_>MR@7iztr!BJwJxVP)!;V z<926?!6DpV=yXW3p05;2aVl~MWhjxUs=v11ElM`uajc-&25cf^L<}5q0;*t$fyBCD z>P}{ScYK+|iXIfSRcGi1jYT%8(xXDRTAJDnS1M8di0>zz^1Ll!oyxQgLP2*||8Lp! zW&J;JfPK)Ox9>Um+LQc=@1JB=u&b=}iW{@u?W`p6$7?rR?ZnEl$B-&`rC z84Ja1!{u}8fF6=rv1V9qyOVSpMc#MBaR)ELF(7$>7;~-o!q*^&Uwv!!?@nE0Pygmq zpH1H}p1u6~-?{ewvxoiUo&WHy8(*WHxoD+Wn#)*EZV00;J6Byca$v42)CNPq^+kWUZJ&;RI4yYHv! zKYaSdul(Ls&r80QyWx&|?vwt`#ebE4>tc|{eCEQH;=U#-wK`+v%LdI2O*7+A#V9d& zuHByu^FGl_=&0Cx;<={k$i+RqY=m^bbN2xOT;N^E7Qh2oh*XBkPiT-(5jCg z_np04?|SK%j}MN${wsT5_xYzE-TJem8~#HMf8c>{c|q|{o_`nZ%)Qoq9LN|urWPJD z4P~1EY3y0A%PP&qoRV8*k!8|24;ZXOeGaV^KTA1%>66~_#G?$y?OpO8oD|~+KI_UbefsAvev@|Q9xKI6 z9*`Q1>}not-ARe#$%zE`8l5&-B4SBv6lR1C5-Qk`nN8PBIeRYu zlW!fn>8UTe^FN*9LLmk%^u*B_A8W z4R*$~pj?@pjYCbIkr`h;>(b9UJURM|^asqVpZLg6fB1XtBf{ZJZ+_n=zDZp3bhUBQ zwE8mb%w1QC{lu>}r9f<8xl%iv0ZKGKtD_?q%M_l9gIqmhBD*N0B6fCEJpD)Se%Rfc zr!ITv=%wfvzWTO%-Tl_1rR(qUwa4GL@P~uxTOX0r&fI0CIL=e-V4m0Q$-=WsPF0)N z+nr9GZ6JVeu+x@{7Dv{5ctTE`byvJwe8zK+YyCGqcJS1P=o^0>-}JFlN0s=~H{bHy z{r9=Ux9<8{?aZCmig`W_s{;c=^#Cvo@(U#<)F{Sh~8?tRq}@{0$xGk01k?vQ0=T($V#ur*Zs zY!PI%$7wjsX}Dh&n>gbaIw0m6Co=M^OV1u7zv>$9%JJVzg&!WfZ1m9=zgL0Z^@wkO z^ch~~9lw9=buI189aoA|VFKG~2d;v&iFRs*Lc7bh%2QRG8%~9vj4ia7pde3cA)g(` zAO8A#eCqOVh94SV?APA$v7f#4&e;CXfAw>(>)iWW2Ve2}zq~{{^BXJ01c(EK`7RuY z^X@zdlv>s58H)itMKS@90araEEP?#UF_=-;eSD`Y-|_9g`pNT;-S;m{`G?oot-C+; z&BtzdX!~(b$`*g|z21wj)Xv;tr5Gu4=@)5dPOcOS88}cV zbDkO#V{sZdtumg=H!4Qb6c!z$#mU`bxu_^I_Va1;A@Edv=<$0z>Hg?P9`&;`KmFwI zzVJQYeM|g=>*)XU&$pCrJ_M%C6D!5jNnp1rQ?1%82)#tpVjo0VWAkxpTJ~^Sw{j3s zNWwm#Jp*&;a+QAT>#d-B=YM?VKi}~S(XIdMVb?wA{vWy0ddZ)E_Qy!&y)U};T39=C ze5F{``c;O^LxVz~5Q}oX2-%&$n~8Y0I|El0NuXJL&1@CtwX+T)8b7}Oqi4_cc3#Yt_9i?e(y*HTN8+@;(QO0l7!LAvcu zwU`zeVLIufqZ)PArQi3tedsMu`0@8);hHbmrZ0^@dcSA? zpKlOP{Ow0y`IY$f(dJV=()!d3Zk@g3Yq`&BXO65C1728=($no{;}M?@2ST5sdU#ha z^G%XRnM|9OV=U|%Y`c55D<1k3ni^4Y-E&Kz1Pt}hC9w?4@=>k-Te8EQ4) zx~^A!sZ-S3`GU}8=8a5jhvnkg4a73WCWWcJ0O>X5{D{;%DE4NkY_$l^oEDrlkWIidB%X~DDW?}p17GLuh-_oDk{?YO3X6XTouXt}jfA+t<`|Ue& z%~zf~c=kR0F|c_5N-+smqi!J+3e13+*5bRB z>?Z^_eCGB0-~53WT(k9O?EA0EpLxYV|M(k*kKB6jy?^om?aby%aVIS0y?9cnXp0!6 z6n8)_7oY%EiZ)>*qiKMKlHW4)$$Z*_VIVBcJ%Y zA3j^5Qo=@C(*oEZ+7vfBLa2{^;=^ zdimA+t$pvf$|}kyAN7ozUjk;gjg{hD(g<|6EmJvt7)XRIq-EI#sdvrUC`P9>oeA7t zekw7+;_Q6$MO6>^Bd5TzjDYiceqLc;Q{OGkaEw zak8#5DwNN2VveSVGzRlyN)r7PG62)HX`pJ`Gbe>1*F@G`aZLQ=1Gw@huYLH1zw@-C zAL!lv&KG@*_=a)u-S(4TX#de`pl@qeKVhY~N+O+nuM8MlIlw?!#=Cr;3#V~AOA*g?-J(UV6iiZ++uO-t+3mmCSPPOHb6U ze*8)?%`(tzT<#19nC|*o(gpc@i%jXLQcQIwW}}Yi0U+H@OFy4BFMR9UuX)y^U-F0- zWcM3C;ZcwMQ0W%&o>wNXIef4EuRiqF!k2E*uKw+nVyp^lm^dX;%#1R?VVQLW8ZfvP zHMl^gX*mG|s(p0sVR+^2M1S;scR74?^TQWkavglt_1}HkogZG+uYbzNE*<~&KfdbV zp}i+xr(J#dN^#a!1FOoIy;_xQj$oV)W-!ZFq|64H-A)&pBsm13I-QDrc6Ph|PwwZv z^+iv;^)Da(gnbu1^704W`t0lf_z|~!=zDMY*%SA8kN(;pX;(jPrMM|z-M%sB2Hm<4 z;O)Qh!2S1sa{q(&KXLOjn@`?^j=pE#=l4BrUuo|b z_FlCY-uUvynT?0;`RbnMo$oEczL%Y^#O;57dk@^+1ONFwAfA5QK36<-9!%t0A{Pzh zW+38JdmJIDJ`=Ywp@^rmrOxxY?I;8WyA3IXV|hoqp5K z%^R?pXsrPSvz;`a*K<__UWg&*Rj+)fji8=n!Ulf-~CZbopu|c)vk}W%#MzaNI=Wh#{19+%GDXn zOw>wmv^>HE;4lFgyL3=Nf?uf22%ko?Kv--i{1i_=a;cLof&0@A%7RVduvYF6?J3d~ zsxF#T;#z)~5{wNBeU#`*iJ5MNs!SrIc&^4=(DvyOE}he#)>5aMxFV6CS@ltM5l9*o zCvC1H2Gg{ysxJJHe|)_2wabj%#h3K=h$g1 zb)HB4fh{FyV73`x13}O-5)aN{-4ajhOP#eoj85xw2lwznP?&Lf2o-aHTV&B8+;#>E zrcuNsa_db*9c;A)d6CU#_{<4Lkv?L_$vJjvOP!ns*b1vvq-_m)E>lYlH`i}SiNUai zYEUS{v?aq{FL6L>`4&D>>tpb|K)DNk*q60%JF};FT3zZqk7^CO(WxwT$}T3Oa5OJ< z6{7F8WHwLT=JekV)Iyioht+&`0#B!7< zw)vPVGXyr1lw_#XS`u7wDSFaXsNs-K0RdFS4hPVdPS$BxCdD+4J<3G^Mcg?$<)zMw zn=&RhFl&d*yrs^Zc#=AWu@VR+(cHoYd?JoO1VWQ`VkQM&^&Fkj zQfD_uln5{A&zwpv5j0#Z@Xd)us1>U3f>K1~X+n46Cg6uGL0h0+6A6ex!bV+jW0k@B z+ev7})8fibkH7*-ngZ`l1vsuntt!$t!g^6cGokE~kO8$)#M_NdVW|_c zVXfM)7i&^2L;&9{D5D~kB%@2uats@EBEYUylK@Ks&2qcZ$uD&}eLCqVZC|Jb)2v-1 zVLi$!eowJkzvn1(chQU%9a7Ql_I%6m=hQ-E=6x>)D@VYn=`VbvLQOo)Ep;mHOu~v? zoX=7$u0mWC6#+w9%(D^K7y=fouvygsJyzPWyse=E?g=ar8+P$noHiDV?M#2-X?Cfz zmM#{3z+`JOp#E2@+``p~Jnq*5R_0)XMmuxK>1oqiH6IeY(a9`z)*Y`J%Uoqa0*2qY z-i&>^4XB$Z9i~#Qu{EqR4Zy=M1jvY>-RNvAb&_48J8eTnU+mYyrpyg2R)OreYAp(K zx1m6eSOFJkdM_2|ttR0Jwv#Y63h0;m>5L$Mp+6x=JWVfk5<<%l8nUuTx;&H33nU(@ z#bKvy)(pgr>P4ClC7Ebe(qU4TySrXBr)&gD01Ep?v9UDj52kaWy} zI~4`M$Se-B!7qMy2wKPgzje>?I~{)U{s(Tn^*{dvaXZ4luLnS+)DwOlDRmx~ggXy~ zZQ;MmP}Auwa_wPj9Cbi(ypacjbP7-z{$-I;=W(mJ3^h7+9(Q`Hzdw)5#Z|=NIl=ws zW8is3O6}-kOZ#^Y@ZVk?dnx6NM>BUc1Ks|(i~f5frKWccy)*N&9(WJ9MVz=UMQd}*)%uOyUy2}E0^T37S2 zSzNO11SyX5tXHkhEI7-XZuyd7+32MZUnx;_$&;COnXJ|eNYJ=Ub`+<~?Jh!b)pQUk z^|Zm&k6-%T)y~GL^B7G3aei0*Wq$Wx?nmeAh}$9#!|~jWT7jFm!8tL9|3#h@Pu*y` z@~K~y@p6tYKKQ~s1g)aVm$I#n+XeOGmVUYP3DB0&Y^`v3=_;&pWDi~N%T-M8*&qEv zyI1OfXwLRO6T!$*(X820J zZI6DgYT!GApZ_-qezu)u>sEi{j}m{jY|g3!I&L`Yf&VE2p#QwU%qmnX{>s42enkk; zMAUh-Hkng`Bzmz2whC1~UE0dq7CS%OlYAM3NV#FwaAnr;y~A%4({vt6U}BF1nK} zs@L-Bb)~)cx6j%89G!Fa**iW8aDfaS9}#UUhiTR9#>~8#Fhgn%6(?6XSgV$@yZI~t z24d~JfCUHW6>LmPrF^e5u+B(b1!f+oZU;mguYJYAG(TC~@!)gkH{3)X>TqMvqQ|CT zFXPn-)A00~n>{xky@&HK4IJ0rv9l*+8%RsgtQ%bu$LDXhJ=Groz^LWGfd&YJ5CHgt zk*NLZ)KMF{Dq9}Z(`DIq*9Seyz880fN4XxaELPWfLl3f!vhASj6GNiqLOP$$Tleqs zaACL51JP6vOwnBtKv}IfOoC*vYm}*MG)-Q==XEA^paTW?aVRm8$JtQx3Y?~EfEtbL z*73rXqRL7d*R5Hlhlj-+5Fw=$&5%4QP#LJ&ArexMjD%y^ihu%HEq5hqeH4%_%z|S# z_33wl4pg^1??;5!zTEb_*EANre5RhC=Y@}Z-U+^Rf_Jdi*Sa-STbe!s-fj*TB}w?I}VQ95i2=(Gczh=!dDCvo#0Tv9SwkZ zR4VMI13+ZdNy@Z7>_uSQx`~r*Lb0Kc=$nyPKB=mGBSx13ZG6DAjlN=rQCHK5xnrL7hbsihui6`4{Z%LKf3wD8(-dd&Bo>RH>@LTf3#Nc{gsbi z{o?8!tB+oJ)$-Sue{y+u>Azc50DpTCS$Ma_#LXYK7xQuC?uCUN4-w#JpV2s$ng*me)jp&F**DmYRL&&`gn*BcdgIS&qjU^aU_ZE1IArzbX3wE8+$LbL_O=) zg&hwe!Om>+VG#l;=Kt{vpWY_Fzk%J^p%#s&_5#aQx->AWM!tE_w*EcP(dTw2D}w?9s|Y z*u85}r`vLH#+I4Au#Lr1+e4(jYb@V!-jJj4xyBE;@)vMM;qG40!eaZ2hd}?cV;LMQ zC;3ajk>?@){~^fZ_CQOX?Jpj#1-O=X)L#Hgj_ofVZV9-SGvhD2mMGg_Jlq>_E$VcC znKd6~?-m=2Z9eX=8o0*t9e?pV3ZH9ij4OWuj`_=TEG)Lac(_n-cAU;!f8qA7Sn_Q9 z^>EL?wY;PD`vaC7+kQPCUeEiwLeooo>Ig-O21d+s0z4?cwf%Yb@Wf z-)A`rpX=GL8~csS?uB>vo@rsR?bpNAhqL!aPO;x-Sn_Q9J=eodN8VBU-Ld4@_Is{} zJC2+g`+d68o#(o-dtKdWpa0*u@P>s;{Y%Rif9~QlE(jNvw$<&`tzX!B)~2+%vN7CP zTYug9_piOi_fB7X_3u}Om499NvE|P!zi8=~tO)=6t(E^9+${Ug?VUTe_nx=5WX)dO z!um5^uX;{*>e(fymsPKY=La9e^9frh_k!n!y@-Y9 z1s6DO{U+CVj(>rBSs3r=?iaQFz3>#?!ur!553H@X&iIA?fSLW_Gr zOH|Mn^;FlQj!qk1*3p*f1uSU?gX9garJe3JvnyRMi|y7PXv49Ue~N23&S>Fb9c_&k zz*KE+{mHHu38$+)yI}DWvhQVmb;k{RyB78X?|hPL>?e4qhh_MVhb44>@0KaNh4m*s z6nGv)cJ}<1$OG?uf@_gSywk&iUEF()CF;OCAMaY!HQwoA)oyv`6-(NIcRtRwG>>NL zVQJsm`vDt{?VXQx4af0L536>|JHLObHn;v5*Gr(&)t+6BdAaME67J0<_JuLUdy=PjY4y^x@Yf;x&zlSxyW&O{Xn!&>Q zMc2|int_L|ZSC#YaO|$V;2Mr&{T|l*mi0ey#y8Y_inbJ zA6WmEYv?CfzlR-z&faB9Y=ltS7%~ zkAn6at^yMwz<%|+6qtA-@av4#f+7$MTToAo)v8?yq|yWv&Xg;oQm<4dIssGbDms~> zI!Z5*vhoA8RXkW_bJt%wV7DNIo0C)~!v$;QO1vY*v(YM{LlKRPmE#pb6^XQ(YKOZ_ ztQigPlQta1i`i&=lBLaJO=WhY)jlEv*_FO!Q!` z=jVARbUuFlQMgz-l4Ex#{V}a;eM2RvY%`~%rBQK&ge#Lekq`qkqKE=E zDULd9d{U!B6~L;<9Icn~;85@G<{}9m$GC2ZlVt{an(!w@WJ~YzT8fp#yostpOl&Z)Kg}$-NNy)&( zY-V@EloG83tRP}BJuc_Ee7_Qn7!^IUo6!h&RWl$*^%H8?crjetyI1!YZ11+~Zg?=+ zvlmP*y*nKJ59!?=R<$R2_wgmU?FWy*U4avTSRbxd#=3{M?gkWq!gJ&Q%W5>pAXEw- z)^`UAt*3~5xhUs@Lp{p}RYe|E<3)Cu=^B;mpJpfda6wIucMDKNrWCf@CtC$ZtwMBw ziH>%qUfIf|sVO83=}l6JKzI(QMCp8Wl&Y!&R+PI0t%a@-h^F-Q0tD=~&}=>I@|)6D z-HHEy+5)q1>Gq5Ndhrz(zJ1|k+uz!rY<**EwE4A7ed8+|+WME)2WwwgQ+%KIbyq*T zDy@8aMOgmiGQafkCCBA@?v+g;5S&dOTgAqv#X$Ff}&>t=iCM40RRuf>-ol}-*#8w0Dy;BbT3EMXK(l2c30jr zfOFk%yzQ=>X8;~fnBBBM_5gr~U$ZbM6A%Gk|mMg3JK`4`B-D-34jS0M5A! zQl0^va~C8J0C-?}-d&LJ4B(u*AbtSA!#U>p-t)NauGj$p4?Emm0yoayFWbEI@`a}^ zyk_Zwy!Ex6(Q@RzAl{sycuJrE;((GDt;i zX2c#$QgWmZ=+R;|BZYSQtt{$qvS!d|@kK~eD>6}|s4UX7-rzD!0A!=QwM7(C3!TPiWQ8Seyp!A!HuA(!hkZis!kUYw+>hA9;%v*}$|p16Dnf=*)uE>-Fx zs~gy`WwEa`Dq^TNX=y=#EnCT46L7?YctY8sE1I4P4Ye>9ZGaM;3gz2Sx>KMzv^t@C zJwt662?LfjLq#s@nIUc!@XWR1J*<$+Cu6e>vGA%p77vVaaYwB&Y;Xwoqj=p+jN5RN zkhDhFAXAE}CL-~q=FcG_5sAk{dWQ*AD*)CMT3}i!v(+Jm%MjEE=<qgO^mCzWBll2v^sF`GO035U%Pm$MA2V^(1?5bUTB(9U4_xRei%hoYFu zkm+tAQB;d~DvIo&VJlThM+p$BSS?{xoAkqZY?3wjdJgGP+0w8YjRWm)KPO3-TSmRt z&M3W@G?rC0R-iPZsM z2Ow3Am(hSYiG{UlR+f|a7>tTdpjqzIk{_bWLJDfP3d1yTxy`n6bfTJ=jJvHqjz~Rr z#wM5JhZstT76XY!JfAYvE?P(g*+elK0g?zgj$wXlkCZ^^dW;Jh($2VD86!v}#pfex zBATf*E4?&G&q$`& zP1e$*B0Z3^(NJzQG$)p!cYBf0PQwp$;Xy9VYkJg3$dtdx5sA8Bu@;FX(MJFq;Y%@GMhmGkoiJ62u|Q^QrQWkU@G0> zYOxwn0c&tBJI3^25A7IOZy=ZTOfEo}l>k8l{q$udOLU2WV4?|0?hkOGS(veh^;-@x zBy$9kjTY;ym_eHrP?Mviu0xa7h=L~}jBM4JkpfpJJ!)Y{jJXmXVyi5l%tyi@B?u}5 z72vypahR2Pr4tFN{YEMv1=!22*dT}J60Nj|A*0@eIhNko_mu6!@qweFh7hU1W+xC$ zU}Kb_lscQ}p@m=#Z7AbOg10)_Dzh14ceG%Plt9Mc5~7uwoX=+ZshXAby56saMpgmf zXgEGLl7sY6$LPy_nKiIft|?kcwzJuBuRQCVmk!&3Z?(yACl$#fCXnxt4H*~YLYGwZ zY%dxUfKjX>Ps||@L6n`^Fat>PDu?Ew$TWJj8)M!-mUjy;kc0qx6=v`(u1SR zST3RTsW@3qG%X7w3_+Obi`C~IVkkypwQLpAI|aPMRk}vCCX}+|fTp;{80e%+ak^$u zak}jXb|#@g7r`ZkO({mQmdZ9l1FqMB!eYLxW=k5IirQA0-=GwTu&wyp&KE&NSi4qh&&d}8~lI3(`L>Wld zvMP#}Dne3fiGhBwiUOG-=tl}+`m#A7GwlRsB`}Z23b;%bX45%ue(Ml}kWP(S(TJAK z7Lx5eIVhDtD%TAHNV{I{)Of6%EwlN7IT5v;$iN8k(6ChYbD7=%Fo*TN(g^3xm>i_C zT{P3otD{mX+NTTFd{O1C>L{#*8pZ2kyVuIiw8JuRh@lfmvZfIfMiiPYM07P0Xa*wH z9!w1eydUFaoluAQ5O1Zw-a)%+u-)N14JMK84znZ3WLdIk#0UchtG!{a(*v7Xq|H{X zvAU~e+ocwfMhrqx7sJEzj$ml12oH`6S+kJk*>N8ZVW^RXi*+EM&T`$D z-YNQ{)F8pdcL<}GXE8NjNoizg((NWytV>y+4k!i6Y@x2`d4Pw<*@}YFXIHcEec=cO zrHzfTdY1|*QK&X-vIyNtwZh)du{G-^8%-K;kcJsbWg0nY5}FN^%vIl-Wuu8=pSJfLN8Ut;xF>Do>P9 z+xjd9LUmkAMkW&~#X})X={GVM95xMY2Z4H-xZX%103~+yL7I<*CL&RsKw~nKV1z<7 z(Y4B(blKFg0jDTddGV@5t7L18%$Vf*kBk`$jz$Z&jdxBA(hJ- zQcd)Y5><$g>qLbu#AhoBZN2KS9TH~SN*$evC=-SVQBh_{mHJ=XN-ib#c8kg0k7|AvuvXsXBv{i93KaP`1GEue4u0Tq#SRPYch&{Vv%G%=&G2nrE zQ$-@pB#vuMUej_ckJKu1w-13Gh(UCopas3ECwf+O8GwMTQogJd)75&0^T$D}j))}F zXtOO~oj4T_GR17!q{l7zawjno_!eE4Ms%(;;^g9Ng_TXhS|7j4(;E4^@bWbOe{5mv zC)S^|vcDMnE{OO~&+^r&{k4OL_@^%Jcql)6_BQoQFw@+K_!Cq0df3&Qh90`Fy?XGz zhdr~CLlr%2zMmA#G~~45Z1~c{6H$CL4*2Al#On+#gaH_|8J-ySI}wXY2{)otdDPE> zU@5CMX(Drl$&W@kJzclH3DRJ*&@S}Z>klnd+gv%r)N&*?maP~^25kZvjH*`h6HcQO8e7k1^D@@q&0Yn|gT(|{!`#Iq9jHDH z4D>x180f)3&d--lNWOcsmy|}2j4ydOMLEfrPKd{w2@HIAJnQBYfIK3Bfzcp5H~&0Z zY-w0!L`~@QxYo9!Y*E9?L~o2L7(ucvt7skCPF9n}v=n2nJ27xYmCAf+l4_^Mu~Acw z#Kc@X(<^AhrZ@?QXsbdH4)iiUZanzKtE2(;>j#Nd3Kzevmil|&IELw(cG0_CR z#|(9qo7U!#6{~(mN707Qg<$}&F3P>^X08o6_i!V{B39O*GQAfR!#M8aDKnu~ah`1|uVXN-M|6gAC*@a6#b7|+|PhZ@<@XHrM z+i%~lZGC*Jy7|S;=Em1HethG}>%X!7q_zLLw&Q!VZ+G>VS3@grTY1UySC(J7{HUcn zmY%Zs$BWOjSe)nI9b0#<`wmj%_`Z2|5p6e_525hPUf51ARkil+@P;)! z!k~zOjO!_|XA}d;eyUmxpdG-1v*y70#@Y3^uMZB9#ABUm+5i>2QR~zqU9;{lVJ)Bt zDQSk01B0AMcDkWrrNpv#`fM~8IN#S@H;$9BBsI`wmX@QjMirwd(K^+J1T}38Q92iD za0N0Xs%$-)_6Grty>r!(@-^2wJ5sWdyc!6>iMZS>V?tK~g+?IX6vqOEHq3D%Qi(T4 z_}Ih~VcKfi6{l&x>N;QH^qS(F%YMfy2zaS5uH3n7p|Mh(_`c#gt>N)S%TBkNtW5gD z?kL_$M?*?iF-loE)iek;R6@{jSSU4X9A|w-#N*1HOOC`ZyVm~%iDHE;M^ktm4A;`x zE=`+Rv|J0}F@MuK)&qrj$4K`@0i{ee&EL6bNwjjS_`c*?$HNa(FLgWkI~N>DUv#b8 z5lJ2@cL;m8Tav6KF1|0g*6HX+f|u$Y!rpC;tbcN?)its_)bF@sYws17Fe_J>@AIyO zd2|mC*+nflFL&U4&UHI`gyW&)2)p+(8;#ZLpLN|h&Jg9Heh0f}I#NF4T4zU!msF=) zdlRQ=KYjM4GFNlVL&#xLwFjeAABehJzUn~_j;B@+j~CX zTH+D!@o*1oW<{TEQz))zuUFM zBev|}G-%UWwLfR+@e9kpxm;UbTl$kF{?hMVddZ~=7vFeMy7=S^e{*4c;aS^%zx^ZI zp{{kJV-^9!55w3*xZmyOqNyl7){{m<4r>rYtw>owil8+e!Rl|FFwgR8Gyeg4Yl zR$jM)FMn(4@r(a^ak%*Og$EYyn7ci&_|tPAeeb~|(*lK+Tgj!9m)D`t>{sRGbX{W2 zG(C~}^_U^*RJAv=)v~mlTX_+=crwG_Y=x1_EviQm2_iY<8S5@VYS=Sp_&x0ue8K23 zewsn&^R;R{H6%$>&a&~@XyV29oyriN5r6q?hVMO^ImdroCIySH^2OR`cS)hf|! zT457L5gE*gTzvN_3`3#WIpXZlvZbX{8P1NsE*LJJ#_+h+oEiMKv~ViJW4(MP!^M9) zmEo~oKAYjcp2{#X(}!ob;o`rX%J7)k7{j5Y4y|+Nc&1j#$_Am#h+O>ksSIZesa!sr z;Xj|saP~{z1;cNh%JA6WHR|Y-Zbl*6R7*;<#>ylkZOm>NwwacxsRh-C{s|LIhQ#|-;yhMzx;p>=S7?V0}B*M=59cPhhUhJEHeq{Yvk%J7(B zpUvsI@RO%9JT?WS zYHc;i8X2rvOb;g{Q8_kZf8tbz$2{e1h95tb;W1A+o8iY!V+b5G?3AwMRbp7Jmx*SV ztdG0h*r`1@*vH1wQ8cC9+X)=lb==jL#Hx4 zW`}1p{NSk!kJ;hb3_oxx!((PclSsMAH;Hr?i!ogxmO6IB>>p2Mcx-?@o8kNI`2Qy^ z#1<~SrkW!}Yt@Z&;IkZ}aV}zH;S*E6-d0xuwr9#TH+; z@D+>T$lo2$*}rmWVUc?Qxv;RYzPP%2>-y@Bhx15h1Lydb$=PQ*tBc*e`*IK1zlG1u zSzjJI558k({|9WmyN7sJyqsz}cn>#R&OQ}du@T~z>|f%~JmCEPV>msWi5|zPk{r$E zV+JV@T0Y&h@+{0a)fDIU0nT~r_2cdN%>8HEID?1nx$NaU)xmjmi}dUkUA8gCKV|=> zo32~*nfuQ=hSbAp>`6$^Y||;yPuNIPzu_9`P5aNZkv{9NO_#isrF4)^cY@Du%_SQh z@+$im`;Z5W&p3wB!(sBZ7|(3SDaKbHVBB$y@y7j~OA8xLJ8sxxlb14=4#epe_SwUc zjqQ1VZvXz!*E}G6`Yb{Z$HvzpWVLF%)~>`WKtF4}k>eWz=@JNr+t@m@1Fd5Af1@E#eP&TQ9%v1$8bHqMJb z;2P&M_aATLyk>0j5U=3iyk=}V6Y0U&wE1Wo=@#f3=}r5OvyomiHhBnTaF9A<)0r3# z#-_D@voWqqt}))Y|Jd2F$wM531JN0q&V*=VTlu#AyE=Eyob`MA{J*;NriGPPt~6F| zTG?2B`||HB|F>mj8CZVO(tDQPbm?Q4{_@gqUy?87FNH2W`r-o@|JTKrUu;~AUA%nZ z!x!Fh;m0p17s?mF?JsS=cl&kQw{N$$k?k9|KCtz+t=DYHTjbU)TU(oVZ|-dxn~lwz zHa@iR){S4c?h_0)avN9HKfC^S>u+Aaef@>&Pha<~eP->xwcoMs7~pFm-&cMA;Cqwr z7kn@B#eA!)pIm+C>Tj(|tGQKZyq!UukrIa zjvJZX&-DG><1KQF_y4}e2Yr9*aDA&QuCu%KzIWZcy7U5z>t8wK$EQo*15aLE+O=N! zOXrot5#M*eqwilhkB$zqzWW?xZ*hfecDvPgucPmuJLJ9muYc&{}ww*#VrRFs@86BL~gqg3C$h>tUbkP zj4g-!cvG%D*=dYT=as{zTzis(Y{PkUO=CRKLALG++3Z=u+7q0{SaZmo#(2EL)#r-q ztOKq+&e3<(AwNOi$2$71IIkSh_c4yX%g&=~^!+{u+0ykPd$gnPqC@WJ`zVL&f-A1G zL&Mr7$5sB#AwNOii_R)a%`}m`+X>0lZ*?DwLYi&9Yzje~X zjj7VUZ#oZ8^`m1~@YdEH@^4%p`I<9mecd5K|V(+6L09v$r<-@iEz|LLTM|LQ3HdFSEj z9W<-7?_V79&s`t+x1FB&tV2HSAm2Yb|NNP2|M^>vuur=ZHoF7p`=&$wsp}*EhSPbU zbjXkQukY*5E1#JE%GaDu{J8VVVgLHR>h#mcoJZGm;#VB-A9Ws{-cPgpG`=r8E&36M z+-c}9Ib1*NitFs!!1qN*-w!$DCp5_yoL4?L|CN7on&bn{D@U5-^G=ieqx0yRCi$EL z{{7Cw)0^ZN_|G~`@;-;$X_C)4T;J=8>+H<$`?RC)dmQo;n&eZCzVCKkIil|;9ew}7 zd324wpKy@<-|Iv6aYx_3cgP)mKjv`#J2za9)yedI)Y12E9r6?O{fMLQyPQ{!==)(u z-v^vW*Xa8p2ig7ChwOunzV|ugj=mpoxZdlE>+Hvj?;jm~?{Uaa(D(h0zVCEiIim0T z9DVP09$lmFdmUu&xISd>arAw=L+?AIB@nR2E_s+gjGm|{YE{LuTRgUR46t%^vLXWpHXaoT0 z#Ns2%y>d z#inAF-jM4JEwY#0JQc2Wgucz?c=2L8d1GJY466_XGvusyUSy|#a#OtcMA6rqVGss^ zxfM^X<+9Whd2=#=P;3`t~=ui!4>MewLTgp z!)OFlAquGN4zCylKB!>HKy=h=bp~Onn8FfPne*M&5X(i|wR$t5wR^p?U|#9rn3_V2 z-OMO~jM~+4^NPTtt`eKUj_M~AFZN=%c820De#+vG2a`QN?>;d_)4zM~_HdW; zB=0^xu{}bbegY87qG&W(X&Ae$~Y$+U;n+!?0Ocv^> zeu67|F*4hgSNAX2u58yK_dt4nt{gZi+uFZ-SN3pl`5(}gAND%ra0G~ktdZcvyuXQJ z-)!|_mA0~5v}&cqNh?(oDKN=I7m4roG>nm|#blkhlH%y=o|QvU0T>hckWmvlF^vXN z1$2@cXAxBsc2PqocSFgF7OO?ZQi_xFg=Dfk5J1DaOp`>C2`pU!`dN{cD!XG4O68$o zC|64Yh`U+YR^4($E8Xa}?T9~Sr5p8NUFYYB;D1O*^fFR-OE>!Pd^LY(DQbT-_w}YM zOmpTwIEC6?amH8kLzzOufm71_mK0#n&l*QSbCPHC>O>@h+R) zg@?%X=i5$~n?sW?3A@p91{dL89+JpZUa#qifQpw>TXiS@-?LUW7JT>l?(hXxKeGB$tMQeu zt^E2*?a~`AH7`Bp;#)7i>|)@;M=t!-h4}W@wts!Qw!N|SKewcXLJ`KrzE#>Y2) zb|ba^&Gp|}f6@B(+MCvTYfrPH2;Q9>Wao4JKVE=dGpYIn> z{O5m<|G0D*UGVP_-uEzluLZ8F1&FIJ-B`NyDXXii3mc2`t^h=54_%aYTej9cR?N`@ zx1_A!clqo)1P|QuKZvWFhwOfA&ZbIe_LTdO-8=7Gv>4suba-Uw+8$74z85@7cTm z;fppSCS?8I{q7R+O7oE2%jU70-7Y&sclTT3HoNEU*zDf&XT&QfhwRKb?2cu7IKu9i ze$r<5%)4xMzkC<*%F!Xa@jQ0(4dw1T|HNkZn8(`eUi*W@)y5&a56)p1ov-WzkNK)0o)3-Phaf{`Aqr z)!ZSwd*`v6@3Z&++Q4RagKc;B|JsnaN*uC#=R9_^VH$^RcmMvIYG{yZJ-F{V)6-o6$Wh_HQ9XT*VIAy>%YD z`DT3oW9=)J_y449|32;&#MS2?vitKn?2d&n9k$(lU;M8&Bl*SF?|olw z^VrQde|9vk$xaazjU$xoY8X&F)T(Fz({P!*Vfz4>j zZt-QrRp5}_8|I;#KTF*A+y$H68-L3FeeNo8b@!0n@6Ta(?6~>RMDG3QZ`q7~bKCm8 z_hUZd>Me)tes3PT`JHg@Umx5Q{pnZP?B1>rSN(_Ves>207ci6-Fy}v6E zS9cEC{n|Ws^T(2Vf9Cx*yPL{3yPx?iarNmg*v&VTdtdhKLv}B_gSdLrA-i9hhi<+< z+*|*78`n2u_HX?+5?7xzWoP@tFV15(-|p_E>}zQEdf>&f9FSoHlxN3*6%w%`aI(5)*-tepT}-~Z@%;O_RY$Bc5F|0=kGk;KL5XQ z;jIgoZoByHi{^!ITrjr3vaN1?VXL?K*-dfdlN;^zkFGb@KDhQ0-}`*^)qhy6to-dt zarwUG+|u1knZ>s)CM@Dd`0w^R^;M1|*LQfZo@_OeiB>S8H25rE%cp>F4W3C^00(A|Q3fZ%B?B9Op zaEgFVPePTlRk*m;C^bciiR%Q2wqmfD&yMI(ZvZvo_ymX!DXYRM(6LGj4`P)>Um(W0 zNfRj+L^O+MCMj(Qn5H3Oidam=$c_%5oqzxKJGChS4|?rTh@KP2<(4`u#uP@Eqm`me zXIo%|6U9n@V$fQxil7lcF#+7PfjULtL9ZPTMGxGx!C;EOgI->$K)7jx{uF@+y>>hl zOK{T$$`pZz>J~d5ibA+)gWeQ@>W{|Cj26eqiq3YWaIG7sO@jmKk$5YD6*?&@nU}!H zpcj{Rbg2po0Y;DJ14FC8YDB8X+xkdP0Aj9-G^|1rOqz^k(0otV&Q4}<`<>k>0=?Td z#wMC7g>^YsqC34zxs^e}v0y{X_G0mLwp<)nXsg0|GrdzBm9S1JW7W{BT61*c6q?(=0TqqaC5q zA^BjS&hh7sKyuo^eaoIV0&$7}N)^I+t=P3H*QvY_A_8Hs)X|e!GtC#mZ5XaL>VY~_ z_t$u8Cym!pjHz)sKP9ER2{Rb2)wyQNXk~&qfhCnzEC}m)!vJvl>}(9T-ziKHc;MolZi=81$;GY8+x5;Q zn(Bv9k!OoQR;Y$2K+p8orO8<3=-7DNtd?jm`D@&?L2HV@gI+rx^3u3z19pmlYoxJs zv)c?Kpc=z_J!BxV0 zoG?3WqFfk{lBGtNB=Ct2TBVuD>Im5JkWI!-8!%G@9x9CNcu2+KrVU;?Mc_d%FX?LB zv_Zq^1zsLwptOL=i^6S9nnK!=7-6X&MwS+%oo2` znAviOhp{ko6pV)EUQ?=LbV2T?lbnRxh)C0%Oo}*@?_G(Ok|VLxqdM76mqvo3Hqs&^qE%@( z@gU8q0FWd}E-$5HaVkT=S{rp$nE60;J0RNFfAqmLKUqBAuU@y)eB_}HH}))g5WCA6 z)+IT6NPUsFdH;lI_~a{o57WSL?G-medtFrZGPnTtT_IWVWIq+a}dA}fdY`n5qUFWT$C>R9oFTWG7 zB7}P8Xg5*V%~^9iUDG2&re$O%W>$&Q*luaJTY?$IFnT>g3thM8RmgNk;6m~Q?-yDv znBx?G% zNmEr&&E-UIt8RJTj|lsZvOVuLjYTh?spmJ*hmU*S3BGiKcTBH@+m@y(O1B}{Uo0HBVbV64JXSgmno3L7%r>PJ`T~@0IU^yK%!Cvv#cqVJKc6WM!Nc{ zwN18Uu8vmYtB+Z{vhtyo-&=g`h4)wO7a=9YFEk{lX# zLPW4>atT@EgGyhJ3Mv~-=A@V(^p{F~VvrqoQ`Pv^zf39EdJV&a8L>}7<7mFAB+Ym* z+}BeBu?Nchqz_dzf3UAHw6^u3DMdfm$u%mXK}T_tHI*1{bRt5M8dvEFY83LRJSu2> zN>y{()(58)xl&j0hekz+QA|ZG=3?Oi+!g!^n2IOS#xMh>%W}Hvr{c=iJqHv_AFKxJ zy*w8Y#1N89iiOFzRKas>4G$)x5;5`%O10)Mri$6EcT8oJdg*F3Il|K6dL@@iN)@C6 zcdKG7o9xhdD8wmRwP=4HRr{Ai^atR#ZCQ5Q&m!r)8Qbg9K# z)#%F!h2Q$4DMh9{PPDsC;?Z>0)}KoNN;TgR4|kqGg&jW$U*OD42|#%OmA(DV1czBAzBE z!-=3pdV@IOPqyh8iz|}aYz>71y!Gm-3@i)a*$F~6!4wK*gA)TxXh+?(X(MusI?g;Y*dbEz;y_2bD>H4ufZ`+7-h&gx@IL=0__QwqGs z=*Xx#(%62lgyt}e97F^4R)Ntg{$>m6W5Ho5m~XfIz~7An8`5o@sYD?L;Sxo53`g@~EkeVAVXVpMqc#wv zL5wR*BJ}1Pr>M9NRSj0@VkJnWIDLSyXlvZ0`_PD=uzjM;PndBRWvfa`*@UMu$YgPn z&Brr*Eo}$^vK20mLOnj75oO3vg^QI?05&J0ze19m&{PJ5P=34~7z{y(;xhV3rMUo- zP;p$QlASUDNwrwq-{&(yZS$6?48>n5!Nnd&$Rt;eSrICuRND}0d8{aj&6K7ox?Y5p z5*6o{)~6J`3=uRd8Z~P1N|b9b<9x6asIp>8#!@gV4&GrxZewZt~?e zYlwkKSR0G}v|c28;}H}{*XXRKrqW8GN*4qyyWVpsf@FLWi*uDe;V&l&xwz4XM9@Eu zw8d7256~!}HrhflWg?3&oljSNlF7#olUd|-Rks>e>nv``;H^SPatb=La>Xd>jv;%Uyk!ETc zN^3PVl@yS2QIa_%hvUek8IOc(d@Z8M{Q8$13b>z$!ObvRW;?}3DV%QlCk=l?OqLN@ zO9VUZ4&4OGM%to~rWC9Rm5K?Fivs~F-C_Z(ZuaxFVNX|dr9TAHX0x7w0v)X~T6>v8 z5$;>g8OrM|7>7C;(JbISM20KW03~3U>kYbEk`}mP2w8j1l%mQcOG&6N)`J~XYf}UR zjzJwh|>s4$)8AR@p`e9#HVDmU&Y({V+~s+mriF?D>1 zOsu`0L3J4NMWz&G3?%FEYR2ye(j9sT>pUxGg1i}`&0bWBFmRI$FChrBEud0v(5Q;cBeY1^p=B zu4jOeR%--xxL4v3S?#d7jK9m$EBjN5Zo*#|`IwBtlSrkO%v&robyLk_RKI}LM?kQN zWlckXI{e~`rWA6h3#j>Sq7YO$rAakg*-AY>G_7$K(fj#6KBgEsjsglw`aU+L5X&(r zDH)Tb$aBqVF)YWbe7M1lWRb}yK&hP!7qUg9EEl!qw>lI#K1k{snVnEIR*{%;l{b`v zD97W85FaUzb3iSh4l8vuy`)Sjthl;@k&EX6g)OHOl}Q^H>Nthw2jzIKsmE}j&R2#K zhBnj7Pnc4)D(PZSEl7Bq7Of?H1FJXLJOc(QfE<^D!yp=(jG$_uIUv`6eoDcrC~e4% zaSG`qK@tv=eWh2!pbXPZhNaRVMaHZ}O9~ZLevz9}G!sTX59R|r9=7^~DaiS}Impss zgKvxVY{(j+;RaMRCM3V`ZifO%Q*8ll(5-5$mmm?n854mX;7{QgZBUU;uM*A)a%>pl zeLpj$Uq8?$l>#LasW9zHDLs%3Jvv(b+LVIU06&l*S;((2 zLaMD+v2cyf7v*4_RC3@5tq$}EL16V3yYBQLm84n$UKlhY286RhPQU}aK?EYQPL_uL zE-=p4JI%_tOz^(jrZPxIN$63%KG8WXEJ1od*=$6pSObA#Lcb1Z0Mz4SPyi`N>y91; ztOE}Qbbeyi*lly< z3i1=7lT-+}<>%oLtXG5%%+P6i`KN(pZgGdn1yT{d(Sy}I?$7fTU5_y|L5g*2)EXKx zogc>PBx~iEFtyZFM3&~$qF+dfGLBObt|Ueia=Fb6F=G^zGu1qr15F+bC7995A5JL< z)EZ^YQ4C85(MGi)iQz0zD!`F?s)nM-u-K@wJyD`!x$N4lQwls_6;mnntXsg{YQ7*= z(#?ti3CV$hC#rxFS6diWB1>_|jIX_TN|8#y8B`TDD-SHn)~p=>|3oA5Y(86p^$10q zP!9-`W;I=iFTQU|k-+1K;)e_l3oCi4hjeZA6;p~h zkhgL_k($!2;ni+Ahyl4K8EYnho*(K6LPw;^1DF*?MQPD-53JWghDx|B;0dvm&@+vO z*&j6JM#o6kbT(op`W=i=OC5q-4o+pDbO-VmOb8Y8g+haj}VwfpY7|SfdPq1buG>WPD0;@5qF;0z`Fh;GI4h0pVLP^N7KND)Bjj?6N^)i>P zBndjtwE`WY8HHPgA>RiUUpzIdK(zv3O`!#*l`>l$Qn(C3jesssNJLgfoQP0KrWGZl zLi))EA{tisZ4mAv(tsQd+R~s*Pr_sz>KAA}o{Z=FIdc?=j}b^INK5T0#ZW4Yq$V2- zvvMmGPBz(Ly2p&dbUEJabw_bo%*ydjGTX1lSAOvTR6}dD^EFh@L^-V!!kH{>W>BDJ zG%!vH(6u1TAqCDKNmw7uPnwDtU4iE@YR;H7*yR80^GraRFQkf;d(Wy4t*L_(@G$$dRPC zj0UTv98LDaDO^Xp8Ho3ZQyBmOTLT3v#dNiv;rww>8LN^^qs_K}b>dV!$P}|>lODI= zjT@(W>PQfXiZQP!=;BZ3OChrr+tbyMg;a91PC^%fOZ!%6F7hSG^PyZorBjAWvxbVBiVQp0*u z!{EhoE|!do_^6icg94jt^vY5XDD%zqQuly@$t9bDB9Rg-&;bG)H52hshC~O$ED~X8 znwcb9OnFr3lm7VnR}Rkqm+o2EzIh|;OIiQ(y}$3Zz<<~luntY0{NSO9hY-87!{)9% zG>O{Vk|*U|v{GtVC&>D+(>QwOfm2FKtd~PkDQiqxlXOTNm^FVy74!ZUsx_FP$Y#mx zC?@x*lVcaWGMG7YJ^9e&%zAt7wuNV}p&jh{&drPH?a<_43;DZsXmS$p>pe7qp|G8b z^Tb0FaF;TAdJP(2B^YOh9RN>FXpz8>biXHSu#vGolKV;`7?3#YdIeYw)Oj%sf^?0R3!G_M zi#Sm=Y5{Z?b9Y>6t3Ew6dD4T2CLYZ7{5onXQN1yj1sP~GS$ za-bavw@`PXK(^{rV}Ig<#(u8x;clmy6B2+v6k|WLYUue)1^+>feQstxCelj6SfPyc z_-s7fK~n>!Fo+_Qj)q%S4m)a4=?)>uKs4j}M|QMIHZdszWE0p}M2K=}Y^_0t9gHGV z3O*UN%wkU)2J>{QQmLXM3sn(vgiB;DXJ*uF+rZkx99cs$ZOVZ9kq}lMSv#&SPY5kz zw=?ruxvZaH8@rv$T6BMD;Ot}P8DqC|nV$Gfbz+K44-t5_@p?JPKX~86<%E;J*}O7v z_04upfiAOI98UjcJD$th{?<9;RDRCIbm%0}*Zb8Kv4T?PO+5#{?5n$o*8KxWEpY(n5H!VH>X#}FrE z+t5{<@8IB-P(IS(OaU#^{1CyfpxL6T2EzR-W(09}dhe)yLIPYbc4v9Fz5d7T`~P=e z`ngN3OMsOH;B6Ow>f%c;KF7)h@YW0a7hZhf*;Yn?w`~8omHBVS$_wzutsmVgZ{1{N z2l!Jf|6gJA1}jIvA8q{b2C?x(D^tMlufKdfwf0v#%r;)M_E||e#1BPz0kL9 z8W>ufQuSRvv-ZEq9hf%L`UEfuCDyEdh()wsH#m)Z$AQpR@1{ zx0waN{j7a7-_t_|S*vTo=(xg#4O#U=ewiDpIKt#R?OxtGON&N{MBU04zV-}opRj3i zSTz+u`yCc-ScmFhrq4H7q7Hx}8){N;gzq*euF1e+oTt`yzHyzG~N ztg43ly|xlC2{9-2D(y^#?*i?1oP#iW?djlt#-<7NM1a!yb|rXvWYvK*C)k*=T!mr&7?x$^H?7k@O&!Ml`b);%%CQO%sSk^Fu0MEVcZZPOF#+ zfDCT3qvp84)}mUk-2fvY3D1^O&}i)@a6fL-uoV#!@E8I~!w&CP*;FoEiD9VL$%i@= z(h_TNf^8B#iVS4eo(As6Y#JHLMXWD9Y@l#S3RGzf#A&KmXquE_WIH9cl{12=VuKlI z+S-laK4#OD)CmP)Dc*_+9p&VR6zkQa8GoaR4~rGChv7Ook#aysOts0ir-J*aO_M=- z5~1KZDFWBpv_C!0;pjMz=w=WcCV_A}nU94dr~;TcuyzBukJvQbFk--YjimKTGo1~C z@!S|n1WAD#=oYK7)|-^Ls@ae6X?E=?;QkA38q9A6g*J0~HjKxyVX&gfKnkenrZ?B z6ARI6PXhO!Z_`M577k9TZG@E=u34Ex$C8wVf}@^Lg*1s|gWVy*W-36i04%~mPO*4d%|DU}#k9SICn;*RAX;kW^CPK`O42 zY|Bd^39=>2k{4OFEK3e)BTKSmEtYM`lBMJ&Q9}!V(~a5JCv)`Psge`<`#TTh{AJNPp(t{PAw;{=Vm&->RdZ&iNiX zj#Zsuz}ua8P%)T6!byxunxOWWBATzHN|QvOg;NsReI9(8onge)KZEW*7e39*FxBL! zn{?3zIpzz2-pTgN*M>w z8oO8F)8q^@8u1o~C&Nl7*%C&DmLJFTX#+C@&l~Z*LN>?7MM_c}YM|~O!>5TkMzs&Pi)N8T%EMFcLdMXJ%YM;hum}CM1LUmHFYDt)psfMGp znzG(eiWJDSCPQ4S;VPv;$wFoK2wr^H)(Jf)A^mhGT@A<8EM^oPoe3IAd7MZoh0cV| zvyEZ5pmQ~Hh5?^Uz*(iHS<_2k#S$}W+NhAti6CG|E7aXg%uacUrV0X?RK*E#Y4;F5 zy*0zccvG+fQOsDp&|;E}Ua!Uvz~>!J?GJ;AQ;?A20KzL=3nq6D;M1EkjO-P`r^$*7 zRcd{H!d3Bj0po}5z(f%m%9qpBAf1Dpaf0H}-F^5pHp7&lhUn`?ywk{`D#HjJ8E%N3 zWI9ndEG{5z6ea004fm%de|HZ)ea*Z-#dgn5xTxD3H`~*UmmIbW^@3X@M`A00Fsu+l zc7h9IW0Tz7g->tHFapN=Lcv9va!`+Ha!c-m{1!@QT(*jUq zjSF6wprGAN`1DI>n3f+}%|R1445jbXMm>10Ttj-EE!#sb6iNNEs z^H=cc^JbU`%qu&837sx@FFUoFM2ko8}NvypcmPIM?3|} z+<*v&%cGtGIkEwdcnYM*20YR!n7#jRZa#PI$lv=R@SkUXo*9AvzmLE-Jpc3svw*jM zoL+yluJ4lr-ZD=`u$XyZDcT6T{%HlgML2`!@}u&=U0{NW;QBnMhV+Az8Xi%A+yzb= z5N7s7GR7w}LG=e--&JO#vKkjueAe{Rw1zZj@C6WHhos`O8ukxNYslH7u*m1tFr$sc zCP*2F+Kqfg6Q$%0NCwf@(;DOt>@uLoXg=*o?+~Lxc3f`W(n}zo4zD-*WtwRACK(Tq?E5{U^@B4t)ERlUJg3i@j0akFP# zUrcRh9=ZHkUM{eYgW;U)%ep!H%gxVnb2_t`oww^4N-&ENoc+TSSI%df&i?p>g2zvT ze+h*AU$EqSS3Jpzr;5gmw$7s^?>sf*+0*Ikn`V*7#Od?rk;pGykMaTL;qRlX9?Z~# zvb#Rl2lM~KoJ^-@vFZ;z75j-?uS$d<=(3RXZwc_NK4DS84KYh*E6rM=t=tfZ zWZUnxS|Ga$2zfeT$7VfanB5`I8(}_0fM`q8G?=WAEDo70iTi~y*W&cypk&-K0Sy4p zx)lm;h9;myl2fWoPSso+=cKBA!%m8LF^-L@-2}E;ve#MH9}sptiLl6KTY3BRd9$s2 z=Y?P^uMT}=TRENmpK>clanfc9K}*l62kaow=nG~mJDNCN+RC#;v(MpHPNd+c%le4iTQA-GxlL>H1sgxP zp=~^S{m0j(^&=qq(f_dbUb3~pD7!DY)+!W9rBPsl$wVcTTw&UJ?UPeCqC{M?_g*{~ z`{EVEQmY4Lv-e&!7yF_W#72nQX79alF7|~hh>Z~1&E9*#TM~(Y42D&~lmOl@K9@oW1wlx!C8fAT~mnIeYIpbFt4^ zL2QIbboSn}=VG6|g4hVb>g>H|&BZ=z1+fw0+1Yzn=VGs}AT~nCJA3bVF7|i@u@R!; z*?U*!Vy|4Ya?Fqr$-XG#6F_s*qy6OtHd;M!p1pT87kjjV*a&g*?7hRe*uxdXMhK;6 z?;Xs=9;_fXLbN@5Z+|X!e+97-0`l2=dvmdSD~OE{v(Mh!or~RFL2P;TuzvR5&Rp!y zwJG40oQjS{w$UcdP6(7*USJU-|Ji%nbFteih>Z~F&)(abi``m5Y=m3@_TJ`P?B)t$ zBcuzk_crEYH&zfEA)|o3w>}rUzJk~YNeAq`wYk`}hgW%xl9#~VecMdz&VOG)Y=qPW z_U>EfVn6fnim*}YGP8FdpNsvw6|9Yr0KwjU%UtZISFkohjs$!6&2zDTyMnb5(kR%w z;au#eR=VCv(!XQM*@nG-zbFqKDg0&G+I@r73T5E~)7aQ6Mb zy7n_`SAXrQarHZ|zV_~NAEal9=+o5 z&jAC#*B^3+uRQqZ!M{8BM+dEg>-&GP|L^zj?zi_J+xx4%pWXZ4_C#<)@R{9T-u<@S z-fnVtbLTg9zI(^s`O2OB?f<;}ecSGKe*4*5AKd!qTj5r5>jj&Cu=yjKC!5UXmu&pW z#*b}$-3GVus`bBI|Ecx6>+SW&);+5@ad;1&Lyt-A9Yzk9A79xuI zBT-uNp>{S7UkIIi#TjCK<64g}n)pB-5#zpMx_&pWT|jIcJ_|Z|?Kwnr2X(e$_6=9$ zK=mJv#2x@SfKIaKkXq5;#ylDTZ3Q=Mi0b44kS*vWa}L3^LQ`{8rkiV7;{job;YG;7 zXP}exIRvC=Hgl*>hXWeaGA>J+xdhqY+&K8;t&`Ll1Pqu{tQT!Q6vpFP!))aY`(pPG z{`}?%0ucLq;C~y}P#g8EfnPB+A;&n~yi;CMvUBhUH&2o?!2WzrNRyz3dN}qxv@r2m zRJAVve(%;v;@qZuV}w^nA*zI8dq5a&7heMGZXEo|t&{jU!pnIkh4X-fx6~9YrzFu! zh`o)2pMg%`GX$9SI)q{EIbiDc2$*a#1{gM)rpfZqdR7WK- zDCo^LXe-YK{eD;+_41Ug zne`>eY&za_^W-rAIh&3l)g)bAVCg2s6v%1N=a(Sc2aQ`NUwYmV%Tt?0-02Y`%&0L< zuSYBaz;u*vp1f+VVm=+9L`NS`dV_#~sZ(y3xcmjgLGI?s^%-C>9VN0Tlq#W7$HtTo z^85V@+x+0Qw@zMpZd1Kc_9poWIw~>DxRjq(mzM~bjxW1)@``f=HkuYJhp$PdN{ogL zUk;Xf0jA?ipp$E72rwO!5s6p$P*$o6FH_dUUp^IPyW@G#$;;0n0SZXLtL0*4GAaO` zYh}8;N@mk>aP#D40CF}RZrRY-0jpxVj(Uz%HtompZbuvHv$WPri7rVlf?BS&^%LjUWit9VsKxT1M=H)9s69fW>qKWl75Mx{iwu zJ{>x|>0SVQ_}0lw&uywno|VTGuGt8vF4*OLbqTP$vHzcLoxJ26QS1U@V|>DqO1V_@ zM-;w1xO*G>zXF}S_zVH2qtK-qea3FPZ9^>hVaQyZ@{N6PzP;!iLe*Hmz&0>32a05| zYTtdpbnO3|nkhAHi3snjwVT-hp5`e&FWG^XDoS(=q9bqY+7aIbFrskvwsh25V;@oNmvX0T$B%_{`~X7c~Y0 zrrm3HHMMa8@GZAao_lT+(U^MEs^SgvecvkNi@~x6FdhH!*2#0u5kapXs^IG=FH57m zijCRjMFpng&CtoS&k$fb_(8MX7bvX7Rf(RH8xYG|7VM6FaK1h39HP|R3LpZYRa(lC zm1=vq{2&0kV_&^_auq<%rb8f_Iwy1TP-1Mts1I7XrLJ%9zX>`yp6l4(17?wJLkdYD zFotc+(uM*swZPWI0sN~s8wz?8Y=5t~8&6gt^@0%YRQ(I%@IYeMkwySo_fV&(Mtv*)FFLfU*{k{JHovfcj z98aKWzSS?Y;Njy#3a5fUNsv4Y&gWT>>t)3;64o`mw$Do}0HHKL>OT zKj0M2UJ1Lj=UmN3n-f(nPZb+FKDoD-uBM{g3IxY-l%~>F5T%k zY|_i;$@X6V=522Q*ar@B2;35ltc42C1FuE|kG3TZJ9`pzdjbHn7d&vtJ@kX`4p@Ng zo44I_9S{97xU}7(H#Oj#rH=P#nGB|=BEW0M#d#}5B+denyp&Z8Bd$$g@ ze|YUBYj5A!_|}c*ZoP8zyN=zhAK3qo`{gU<)wk^anT2! zX|jfW%~TcLm3%YsX`AGITwUJpN8z~*HFtukjyd2nqk0>sEjd|8QB^balL&%_Lt)S< z4gGk`<<#pgrs#<-@5r7__o#7D2>Yn-(ga;9sYP0im(WVFfW&>bhF^Q=!*wQ{P}Dh|zz=y-T2Gr}M*iFHANYyG}F z^a@B=EQZNzRZ(XKBf%m9R-bAGP#5S)HpH_#@!RY31+jj;mTmF1oEm6jLQaPM#G?m! z-Ayud-z^RF9v25Se7YbGE{z%2W9d;dfN>)uF_me8LW??UVVSWf8AYUs<>R*9YQw!U zTIpo2Rolg3ZZs)kJqp$PqE8r4vH^l#cRmae)?~n9HNR!2vY>WtEynPcQ7Y-NaMo!gK%(wGF7P*%zSVn&XRJK87=DruAQ18D3Z$&A%4<4n>-ujhJ15HFP= zuBIz=1!*`FU54yVx6usnur{^GL>d!f-D%7ct~Jd`O(+vQsFzd1SiK$&muxufpW8rk zIUDQgp*^SumO<+Tp<%riQmqxLxe_SxhfMOr9$)VEpz997CSqwZPosWrjJ7}p8?Of# zD3f{3At(7Z9zY#YE3oY@(Y&S&7&@m+3&PN&1}>i0+DkU%%_Gqt_mOefL*;`L+M`(%aC_!ynJbp~8x}HkZnAUNz|y%b`OH`Lx=SvdK!B65^0f zn^q<(hh6l#XSG1ti++P+#YVi96s)>asBuG*nq+CISw}j$+sIxK(6JICsDl&h6!8k&t0{wFCd?^Xne2oHo`tTPr5b7GLn)a`a$=FTos>F_ zSL{BVpg<*3yD4V7tZ20X;j3|tZV{y^YO&*DS?ZJWnCdUD2jYAjO0d&)l}Q6?1&D@r zGa=R~!EvULY7;qd=Ykj^U(}Y?>tkejnJn)2u+K(s9tkU4anyO-L7zPdCH+kble$@Tru!?OFrS8+ZWYFnCBC}qdA%z48?m*~=f8lD(4aGF6aU%E~=+pV_I%v!^`+{*iI zs;)sE(twC;yD#>uN^#6!c@NZ>CjDy-ujJ!sS+J{PrRj}ObNR5|{Mfk-H4@KIc0SqE z0!)(Y*dzn$YWWs#_Yza7J&hGmqBHJS(?#++XI0U$Y{qGCY}y2ylX*@dpbDbd)pCO$ zIeIs2sJd_0C-^mE$ktj|Th4X6e5q^?+NGsMxuu-j&_IbwwIx-3(Nz;E(>6-Dk?O-% z2~HukOmAXW)TY`W3}op#MW)le6ewa^X^YbuUr92PL0*PjxM@x~w5;}fk~UCfM?&#y zWe=@$oxnOS*ck8`km#5Hwur=JAGC z?M|1!_iTOFxeXD#7^vw~^j?p0r)U^MHL?{aYLF+$?$oPE;QVeD(2hL;aY_chf)^8< z+fawFVZA!Yf%1HLs-ceQX^P3RC4`Mv1*!&)=-_($!Zf4`^j3O z=fI2y3R9;+Jv`uP_!{Q)8i87pR1XN6~!FECukqhfJdGwm55FflwCH7h5~KFjj^Nm@IlC2r|MFb z;Pa4FbNo?KEV<25yJlq7TCL<)v~I59mQ~to$9fYKIMxHLYUKwUH?-Py;A9k*_QhfI z+=l9)ZyIP2PmBkneA??Y%$!@dWEYZfpElEE0#t^yKxJ?!bKR`>M+9HQJAp9vEhm#4 zG8KXDW+2>ElrTHVD*c?3AWPKHzBbbPI+N>Fdygf{Hn>Lj-*vxeSE-OY*7=x;#wmxcDb+QUvE2D@NdQ}(; z8C50hI30|*I(d!ehlE_}+J2$Q=Czt^Esw*&&w;1O7my=GVKJs^jfY7+GpZJOh8UGG zGOjievu&^)gd-6qQ^u`g$GMImq~g~s(8=dv{uj@dB zy}}b(y))fWn8cN}D6GE^53@VV!yJN2p5u-QYOk5jReSB5Rxs%E*F?PIfv!IWx*j1W zaOJK`V?}h8+gI*PW=h`l{3sEE508X88{b8C=~E?>rj{1hEHlr#;{to$06HBZv~Xp6 zF6J{?cbxnBR%cXx_|4i`|1auO?u>!HTcCdtf)Q8l-)EDB9p%pGOizU9#FhJXu1CEy z1bSxsGeWrH%Kdrb{#5VSXPP2JFs|IACu#zlVgOXlHbsQ+!<9RA`KCY~uql8+)|o*O z!Yo(rdvt@uJLb$DFzXQlDOa{Mjw*cV2Lv3z(y-Ac#b za!I~<*(C9fc-HGjS`_ijp!*~HHD@{=DO==|blg6^(>WX22o%c?PuE>(WaCSFXg;#m zoqqrfnmt(}5Q(g8(1T9;HvxUKlRiQc?#lX}cmjVl&@(%MBM=L$tY_(5eB*phpZ{n{ zwN{$b#L}G320*;ip4kJs{zy5tmKU-x@AM0=)1ek}tY0N;+-4k8>L6GQrc%6Dqo*?8 zsWf^CB*C(cJdt_$_3%oahL<{hwvc&X&}<<)BU9`WLNhIv5qy#h5_%>Z=6yR5+M?wY z+c5}_dqZ8BV(Q8UJ!m1ffWFy61~m#X#)UhvPBU3=^ay1L1$m3+pq8jO+=OIGv@_Ck zgDl~%tnYjwTX*_r{fa=&|L{=kvw^ca z?)1*|M2Kx)xnJjc)H@2$Gdp-91iP=?pC=wX>K*w^Q-t{Um3#C=O~BKVfU2467lHD3 z9!-!NH^QrP0*v-e~Uo~N#` z{y^f62pZv` zXcPk*rmYIqhiMXp1Q!_Dv%4IIJX%i_7gRB7x z@vT&HdeL>j)A0`D^owWv?i;1`M|(c6y6-X%BFu^K!ZF`R5f0%8@1v*!mI!d3c=9dp zq|Z|k?1`g{7d2*|>K^UFUVQD#VF;SBMa{7tS?P%r(*?V3MneTII|ty@nH8-)bBzX~ zH6y1(W`8~VrziD$F7Etq7d;yQ1`5wxNaDaA^g%(~j%fD9nXV_wUJx11|8ZvH!k#4~ z7i4tjhR^X`ad2Oh(U*+-zXJ*&QJVE`-sp^55%hH?{Qhy10Kb3Ej+=Nko5{dH!vh(< z+O>p{9*z;CN}1DMnS=XkysoF*n(T}#ppIo;x@CF!rj7GYpB4z#vV6j(;{}Bs<`Z2x zlp58L?wMeB7jGT)+=JZa`UBymia#)5_3NHJPG|nQ`003|b({EjtIdhH$3aKoq<2^u3#! zmMdz5Myk?HGlC$KgfVSTvu%=<@Or(Nrr~ZLLsQC)UYVZIt#miXS6PCmB-b^IVZ7^E z-D+3&rWo7cC3b>?;5;Y3@^J&0_#WSUyLbB1`CCP>{%Bv+R`qUD4?1q5csDbref~uH z?DP1IuZU7Q=0VJxi`UD)CqHGO+|l|&uz}_*n=?aJBREdZnqEq99KSzYbI$;1c;jpU zSK_R_;2r_s)1~svq>-n5uP6_yrisfqqGGT4>7otdR>%u06Ps zzhyhMiZ7TGs8r~Zd7TkukSad5Cm8yQ$7jCMWH2Ka1$>G#*HBcI&fH6cku(G)u3Cb*VcM#SHJn{mmdGbaqY_QUD1!;cSIb1@UV99QwNRx5ADBs|5;BXhS621J*RUuh#=+Eb_otRvS=;5elbPkIjX;Ga-)+v!nT>N{WpmE9 zPXubT+nKxUqT9+IzH%hY<=sv%cRK>7T9m5e!@CV&`h4~0S2q0tt3Luan|8MjRL)m_ zZe^8ESp5}0crf4*?A?0!W#dXd?(+L&l+dv# z<#C61bHMcZ`T3fazc4@GFpCgPF}#c3S9xRQ(RzT&2muw^-LC>F=jZ3ESMKE#4z&oe z6x!W4+*kGGEBEdRsv?9_=yzWaRL#%NFI%}+Pdd^f#8l{aUw2>GV=MRTNy;MJ*dJ`Z zY%RZjV{QE#*Wa}Mmm5E^@%0<6E5Eb#zOA=xQClzC{Lq}6uXgyM!yh?3Iez_N?eI$u zKC=I@{h!*uxBu1quipD4;Oe`v_kUh_@6r2?kM{oY(Tn!F2S0vreGfW*4`2ZN)%Bk~ z`rz(AxvKB>chehxy1Tven+M#^KLuQXuREf44!3`2`@P%V@o*d4e$K%=j^48M!Gm`k zaL2!O^<8U!v-a~dTMn*YegD;+OD-1f{|Q_{$oGH$T>Tf`R5q@IG}TP4#b(C?hsvbT z_^9XhiX{t{hOug~iY3cpZGiGaCoDiaKQu?!C2f?#;moKbWRp-k&pWhx-o!z|&Ozqmk9Rk$l8lNh+ZsU%c+D&!$~oQg{}Qb{Jp-k{hF2`Yfoh_U)>x?H^m{Xkw{6=_d!HY0oTEtc}-N-85@wvUCGyGo838b~@O^z%X^~w-*RT zbMi%NDAaI-hf@p^ZHZ+?OFRF^8KN^DVIUO`BeR{h95d3j zm?^X|M-Q?EqRAt=DHkj%jN#Ogd%6f>?o(V>Uq^L zJ52@lvON|G;C2V@O^TN2g<5_Dcp;kmzq~-m?Rc1-Hk)0J<`Jl2$O;DaT*L?5i3N%3 zIPr-NlC(Q1j@;+Y5V5Xo%Y#f@Z#EzW$t&4ZpNmf#ePPlY0QMQTn=3n(pnIA1Us@o# zY`$70{Vt#GNPz>vHtH0bXd}HGs1s1g#UzVk(VOs)QRRE(VmO!<8-^m6psnwpBZdLO z+6j!0C5lj%gRsWHFX}Xu>{B($k#p39)7VM@&w9z$~TZ!WrKz`mG$dGmv{UL z>rBeiET9uhGZlkqr*j@!9WWu3Np*+gE;~YUgG!~hP~nnOE#q|xrCPr(I<1bMR#@8& z&;rr%YOwBw@BnktbvjYm{Fe)aU8A!#uIsXZ)uJr+vR=+Ne5sQ|CQ#@H@n*IL=-%Qb zUQKR1c80(!W0%qTRL748T+7JSDA6n?pIfBeHfT+Wm~z}hH^s}4g#e_x=Oc^&u7RY%GKSHg$al=N3vJ|V zs5+VY;Knc%Eg9ebHY?( zIA7Oe{pQy71)`RQ9cpaoB?qzHDvngFgeSyk&&w87gBuOhB%W)@Y+n{OKd?X)>T)8R zqsu`VktUc(4%-M;m+1-Y__0Pks8aP27poT;J-P8M3j_*V>Dqwn1{tt45;4{cfjTm2I3ZMq2%wrlH-B@1$PC3ImmUTaBMwucr)K*_j-L{Aa?)!|%oO0$sgISZ zQYdHEzh{9+#fllNWtt*a2j6lszSpi|2(Q))6Yv1&53+8$Q6c->bWmCQ@B)EUX{b$L zDUDF8rKv^vTtImSV2E(CWQ-3-wKzJ)pdv{^TW?q(k{&qx!e+9VohqiCR9eWmi{P@( z%SKU#;i*t?psGIfN6f~v7l?!cGVuL$3aV9%EG2Y`G|~~Ifs<4_9Nmmd1GU`_L9%*@ z-1yoBf+Lt>F`KO`^?1M*0wiy$(+QQxh^p)>I^tH`LN znVE(~j&cgzH)Po>m7CHqE^uhS)-@U-rcmJ4aDn3~#+(zw}kxIA2HCQN33 z@hlj^a2q3N&l-yHTnFP5SWFT$B4(6B#3+=MDUlhXv0TT`F?(-Z=zsu?(V$Xw%n6bh zC0VK6lba-&usV5kL=ZS$q${-^5+FXlEiVu;ZPcrb^bC_|Lk@5T-8eWlW6m^?Mv1y& z=Lmd?KvQruGTSenBWyQaqOvj6Z|IpRHYpi?%_roTS7tDES`!kQPGw3yBoSqM?;Q)o zpjj+9EZoQKv9P06f70s`m^XU7J7 zu}LI4UCB-EojF%SBH4m+JvZ)RgpS9pfiq#uTCou$%Y46$0XCp{(=#&(D$&Z1Q^2v%p_&57In=tHHK!U& z53o@O-}}nr-&%WTB$w~=(sCN$Ns zJFG&7XYMiA=-JM2(9^vnHh={;Kdtr3@Gxu;VRekTffc}5#!oO)=J4AWI_Q2WWg0al zRcQfYnsU}ng5;2DdK^z4!>=VuqQPyois;mzX$eFV0QXJu0@e_ zs+U*WvRv(r(oMk0M03;1;WsaIOk<;BqRCIHq%rOEG)}6=5))T2dNHyfiRE4|QF2ps zVk~D4U%fzdm`p3>qE*@Buxx@0EWoBhtJD5qJS`h8MR(irL0d;E6J}=vygz=y&LAMM zVB=vO(i0h)V@JHqPAZ-u)MJcU&gJTzcA?sDTRN*q`@aYl*5~}C_F(N(Yvr|HSlfH& zp1k$LTaWL>_ttjbzx%FT9lWn+c6YX3yz^T-@7Wn{et75gJ4cH*_HAsF*?#WUe+BR6 z@4BjA&20YY)t%$tI)2ab@c8w|SFZf-l^?v~U%{_DckLIB{_D{XA3c6lI(qTphYx>r zt91Cahs@#2HeYt|X9xcl)DGYezI6ZN`#-b!wfm?0|6u<$d!GV)g7X`Qc?E!|%@_{66TFkdx#U3P zNg@HD+vAH|ye7TcOuQY2%`HDQ0@A5UzrnF7y2u#GFO zZ&^||5147uCLHIG{5UN(4RHBToKo_{UjM;`jzKb!jF(el1&)uhNnWpxZSX+1`k|hv zBH-144B{w;NN53Ff7=3K#j;~~nCv#pp4T7}DbLj4EEh|RdUnu*I3ynP4S_GgEVTd9 zIRYeZP-IIe=F4PXNv8mjCOa)v<03SyRIv$H$TpLccBk&-2>a+@p`$9mp<5RNFHvsm zVKW|c(mB4{Lo?ugNP`eEuxh*()5WQC_&W<79E-$Vl1cN>An9nDPeK)2quq=If%?2o z+#3hQri}EYDS7xW7CL&c*(b0@kxo`{h@=yqrLjH{%Q9|}GGS8HyGlc^4Z!8h;rA_c z$V!TzDnVBn(@;b5V`NRr6uF)`hKPb#3UHDl;{sb9b%ONtQ$;pI#txu^gTja;7Z#TBC_!RSOM<-T?6&E!8fxEAf%+ zmxD$JhYtSLq8}ODMy4SlHmFXlE+82-*Q@6eeWeA%gQT3dQKV-fjM=g)2l0gtIGgpR zO}kU)kepM^l+1C@7l&p=x9w_M0StQ{ZzK&1o~j3nb>+rTrd*vAouE=uvc*h=jkVM< zTqfGRnp{z^YQ?ibRJ5Gw+53O8&>;l*ac4aB1l`I|+QhN4L&3>%&05l0sAz~2CPf=`=)W^Tq4w1=xf~COq>2f@-IS;}-y2kIIf)pX z;u!)S>y2`@PwO}mV*tZoqLR!F(3n>5$8m5?H16abh>_EDW#>OH5Hyo6=|z9SR0^ql z0?>4hb+#qgg<3V2kPMcZloC80HpZ5^HCZ4^nRvo81rM@f8ta6lQ_=k4C_M?~=A04r8DW&g_aQ-TZ=bN z7aI>maD79Rvt?K)7g|%LRYMg)?$W^74NCSfU9d}5A!te4FI(tXotbs_>lO%90bYJ{ z2=jouSWIU6U1#F?c1yPVomi0M(X1PrCapvv+1&f5X9$eTXmvuV<>Ot$peiY_uazcJ z=>rb#oC?ZsOIdqdn)G0Q`|B5oEJP~Pq8*a45h-TSp_b~QfV?SHo`k&t$=Q6ij^^pY zz$5psEf5)$j$>7481Qx{9#jlwkZ=;1OcBjjQl&{E(84JR?JO=bT%<4az|*6_ zi#0|=ZcCVCg;=weqo$%)r-q*C^rpHX61K7Px`mF!u$_n{y9pT@5B(|=AS1F&rv$7$ zQiw@kmrxRLt>&matZqGjfq*CDxXF<%k!hu?nL@3_#RdZ4>d$IqO2v4qQ5#Ewre0kh#sI}#Jl$iolu|>oWIpAmCB^qN;Mvv`DkW|H#|6SefPX~fYgtkR zLC{kJcW|%n)pj_Dh8e@g zW*;1j9JtEW%uY6|qgdIqq!OX|=*Hp}b<*Q}IE_sc7-Eh&PHI@qOj}IB8Np7~QYstg z&7=gE#~$ZzCKo!oSqZMX$gu2pWI&YX_xom`xZ`RrO%!O7AOQh+ZEO>EE3^K;FA#iE z*Q|J=Tfq{eYME#eWG=3;TBmNAO*7dY6JFF6*$cX?7Iyo_l*M&xM+MR;SCZ<>8f-JQC&*w+`q=sZbm8elF+3*HQG-u(V ziQ6S`y~V~#BU7=>G}3U}P6~(4e2mo#)bA|5d`!wMnn^S&4P=x6z8@HcxS?g+R#6W- zg(j#}fP(x#U*_tnadv$?Dask(o~9u{8gDicypw18$$qhrbIUPYAjF=FbzKiIN0yrF z?_YF0T`=n7ayD#%TtfpXrwzN=6Pi5@38++$EH{U`o41g`IHT^~ULcSxH4?x_1ISK! z0k{et1?5578p(2YP!8fyx`8(Wlu2k1v$wd^o&bNU3_dMXA>R=RAM=R%GzA4{kx6q# z9emU=P!!cCNH$&BzO~Sih|w9SltU^NH`8`{+LTJ=2LqiI>u9~)6EGzym6HOl*U{~> zg$R#~M!aAo6cvyP*2WDg$D%eab@60xf(%-{{3wx61$C1V_Rjocm?#@Q+!9AqRZL20 z&M$Ulxmc!9w<*irMxtFQ6hR1eVI=MR_}nTtY_;%V3j;TOG1xEfiF ztk*i-Nxm-?%Sn7!Um$$C2X_^9P^?#`HL`~kr|DwIuu!>!hu}+ANuXk}5nDF%(MVFlMOG z;A5Im?!+v~MEy}5L8@b;?hd;N{OntzYY(y(xSB-D*#HKfLaUA1qoLH#Xi!%k_XZqm zIn{0~Nj8Oz`a;LRqdg+7*m5;H>LV_AaE_{IwnT%A&oZ4QM%u`WPi$^VHTS-L;rSR@ zM6lZ`_zXaCDR2>Q<|`RiDvE*X7eZEvms^CvPnwi3ElzgVNCZ?;sm9p`Q!kR^3}6fI z0{+fCYNQO#Z1}~vKq7#QmT7MP%EBm}8pONsgcb`WR}!F1J=2786}M1~o1m(XAPV56 zq#Z!a(BE5}3vOTYTIn!X2UUy5;3B@ynIeLt;OV1s^`;>zo>Uj}R-GwlHn4?`zDD)S zac}zQ5SCZnqH!5BYd+ zWD-)ZAmVbYtF30wGS?=68nruAbvgL7PD+!s3SrbvL;HU zt5mYVfcL{fRjc5`^w8hgnCJg*yn5~E9ee+M+XKHm^YhFIJa7cwUOaupJV)z`*RR+8 zMqX=Bpl(pvZz|=nl&SRStYJ;dZQx?4V#eh=wyxzyN&n&DGArh2WfKp|(TWgPGfxSP zlG6o}Xrg4JJvfClLb%q0a=>P}Ab&p@U{B1qo+U|L%(s4eSyu@N@ULfpzz60#7OCmb zoGKNfX*#@^FZHr|YZ{Vvp_q`kRwL^wl@y{(25ArEGXo*B94FD8jFm+SylI0pbjX|_g8z@1Y+53;=@^E zy6w|zv%PfZh3iqu_CC736#t)cFGUCre9&IH7}@yuv$J0D07JmCbTkogAIw+veVzm4 zf&1=BX_B)pJl%~Lhf_1+mIrRcaoOQCohUGknv?^jySP?R>LG>V2x$lfPN)?^$`iF) zvRHrmiN_o^yI5pE6+sj)Q@-W-3CNb}4nLR(aW*~Z#Y_3IHz7fs8o2$b*1Ax$%~gs} zQq;!2GBtD3WHJz@;O2z`U-!Y~?TvmMTWurGy1v+mFTcMLBSg7AJS=n7hZ_8#jTj+* zbN*2Kyz?SN2%UX;8}akRj_?Jv5oa0E58Q~k&@!ZYwV&oeO@d4#IRLe%B)Y*W2{>$q zEcnoYrMqIcpBYF``-zy=WLGpYqywdL7KL%Qvgv*z)h_4MTbyq-oeYy3;<7QRXIT zM1&r+5x?NQ%P0oE39PUsf|B!S_`jzo;b#q)`(=}^*u6FPaI@ zPx&#t`XO7+Ui zk3M$vb4Twy(vDtxbOg8oe&q1&hy3BI4?cbH{)6v67$0BqkcjnEdp#0qGc32Jk2S?ddHM7^x_g*DQ-to4O8(z>km zg*8&Uto4O8QoOA7g*B49to4O8@)ei0zOY6Tm$hD=_RZHkb!+h6x5R<8`PgNxFU;jD zE^B>ZE?;t4>kG5{`!*EndRBp z_~>P=muF|=k1lJyJUbh|f3R+?F>61)cKqJs$B*mBk6ronm0!Q|{a4<+SKE8l?x%Kt zZTEY3gWc-xD|bE#YX7}w$KNUMT-*M{_AjsJ)?c{xk+q-R{MeP+l~)~o>gd;wzV|3N zsvf=a@RNtXdib70|FC>`?cfs!zkKkXgW#Zc@Yw!m_J4E#z59>v*Y_XW`}FpAZ%?+V z?U!wReCrptzH`gjDsFx8-mmX{|K`tczH4)|S=fB(#z!~)!^U@P*c;f!i`V~b{b!&2 zY47-tj(_*nTfm`s^%k?SacyFDnQ$uC_;BJ??a@eWF0JuJ`_-EM;qmXRD)qz1zrAYH z4;_DC@6Bu6k`MF<{_ny7kM1dLaFH~e6npllp%s>E!EU~F^JLYgk8j>ywdq?n|Cd#p zzIpR)t2PZc-@0nkVDs@+n@%_1vT9R*^UbR^^)|z^rW7Ms^pwt({X!`ejH~kn||xsI~PqKzG43O`2RkWngav@INxkArKVsxC5c|@*1^vl zd}h_AKYj3bt2X_qgHNy8^e6v6_TB{Ck*drazkA=l_jb3SB8muNgJ=_OB~_JF5Rpo1 zNoA>|DwV1vh_SX*Dpi$ADoZ6nTLnQ7TI2euERMUVgP-F#iz_OQj?P~h7lv^L7jV>Z zXGHOnLH?Bmh3p# zV&il37QKAqrg@9LV&k*(7QJlaGxHX`bmP?Kr=_ap4W~7Jb9Q z>t{vL0!o#gcq74iW=AUBbq`aR6>Y_ww`g_6n7639QlGb|vZBvhR9>mgTU1)n&Rk&@ z7oXnkrv{djD62)b6fe7~arovb9-FgZ44tzedj6aR&wt*W1)q1`wB6lKvQ0BxNu`QC znM;w)!{@)X*RTEAyhUHP_SSiezIN@pd5gYg?b>;Zj@GW3x9F?auAUWbQFx~41%shg z&&$f{y0yaw^u`l5a`P5_{6==(qQ`I0^AW=ibboU+;PA zp2XIjTW{HlZ~kQS4V&j}{9t3W5#4?J?kjgg>$k06w!XdV)?F{#_3X8;t-WaN^wlq~ zx~r#pr+8=Oq~)8It>qJzKDAU^I&Sf!i}K=@$NJcRxYsPMEHUajw{z_N{hj&!C%M-w zOxWh##P zO$_36LaYZeg`!(#Q>KUZMH9AjWPk9J$blPOG+{eO76(6x8@SPM!gh|V4SrHIaHGM5 zZF=XR`THRdz|29Ns?#+n$SBEtTNMgQy`m&=oi=!kNb>acF4EISUU zIf_>I+}(7d!vXmMH)>AU&XFKtzmGf!+{m7=og)dtejhmxxKU%mc8-Jx`+a0L;6~Pj z?Hrj6{N$hDM&^X=92pAs`$$T_jf@G~IT8~1$t}Q*>JzqeBo^53BYyxl(kE=^NFK0X zn?FYYH>yq8&XFKszmI$X+(?_Sog*2*ejl;_xlwh(c8jpPa2IYRR9_Yqy68%YzkbA;C4?<1N%Hxego=Ln_0-$(R(ZuG(l zTX1Kq`Tem@6_R=yYnC(@57y8C60R5IL?M|i$q~04tLc>*qk*Bas&S*r+-#jd6ywA; zo?`f}Y#DIT!b`)R>V?I|m2f;)^Wq6+jo_fwzzuHng1Omtt!l47j3x4L*QE2MS|iCR zNfgGwN;}iQa4N&GecsD#SeGbnbm4?;XpTV)!yEpV(mp)_GTM`{K?Sr)iaal-cOo!zeX zQ>k(+E*4cfP%tQq7=qDaS(TKYk_2g8_M($vAky`M$O5SnHxedn19NnO4~J)N#824H zk>tUT?mZJXWAJsa0< z1a|+~?sL~axQ_3-ZI`w7owXOO{(SY))st8LeC5>TzglL!H~I?}f4O*xmjO_J*n|Gd zb|0#)sa~R%1wTjfoe!b&Gz+ zNcQU6_UiL-EiiX|zxU{AYlkGS9?+}D$34N^^*k`)g(psNCQgFy=>yly#?5jv*>VIg zP%}zrp=h!ePvizbIH-AHV`Zd`M~%6wdf4ndN93Ut54~`V#cOt}RkU~<8fGexP-+o^ z8^Y@SymzYa^;*R$5sh&nF)6FcBnGzS|14_cfNEuN(M@=YhN(yS|3*j zJ6AQ2+8&X9(mHgZhh?lOUKkk8?15M!#k2}GT8hTRK2fa^YzGN=!HvLzR?iJIwLQe; zxv?BgQ=aeDH`bIu(ezHUqKH_ z6?0ehu$mG%+*VCIR2pL$i!)KK+F%FmRGmV(kMy8snP7=df@+Dv0KhO}2-qM$3^?Ze z-8Qb7MK$_~pI&#^cw=J;&VjJ#hRM zyn4nny^o8Jx$8M>{BkpK`pDQgcU2EReh(L?cql){;x*gH4a(eDem{QMS%dYQ7;uwF zsmE6g5=kpC4yKbxp^3;|v_G^Cv_g85DrHcsnwdKV+ru~ba8qmR5aVGPk6#~`Fmq!$ za{O9{a`)Fa|Lx4&S?-RkPd$|N>Kp5ek87K`+xG{3Q4i6xdVJjD%-ya()T15Bc=e3+ z(#OTl-0gXwUTTNZvzmO|^~~L(2iD|G=qazN@i`(*rm&ho^@4V)Qmhrj*03DN#}$B$ z*pw)mp%z_dY8J%ud3o+eJ-k93Zm;fvNzTCdxHX!)-TuIcho~_YPuf1Nn&!qbosPsq ziCG;!?w#iD{NGP`u&{f5adq{=^;JJRn}_U#o!7|KMeESbe{tXV`>!rJx}P5yIefi-+5S;% zjCcDFcvt-FjAnTMFu(4Mb7hS126g=X!LQ7X^NbNX#tHrb=dz!j)ePqYXU(0FE{`$3 z_jTj%55(t2dg|!>G16!L0qK&TSE?D(={&bH#-%a3cf5Q2{Z4moj87XqZ{Nc18OGh? zQR-*EHiI~wns$ab#&)eW{=V+Qxe=Z^I`6PvT=VmK_TXN$hg!oen5xw84b!|kNC<}; zl#S6{b=vs*)wetp#>2OMo~}Q4jPXIb?qe4?!+7KbwDaOH(e;L@Zs`k>j~$0mG+bT-@X z*qf8tzBSRK_8}h_!$)BBu{VD(#vS`{icuT)qx1Q>F+ObsAHE{_*s{+c&Q_!yA&#+` z9~^&M_xL~v9gfh)hX26`cNFvl;h#?s#^y$N<_H|iIr4{{$NGLBuglY`?z6kiv#Yvk zs?OQt$Y?w7+tiu)o;undV|~Efxy#QR_=D%A9m8WzWPf3F zjPb%Z&yDeEqqD~tpYaEsTJ!UuKEpT;S-ta#bZv~Sc-8p3^pHE{BQ)u0qredeeZ0*- z5TV=_@M4w6`M%QA>7IrvhwbC02rr-DeC^zQc-H8wF~SG!lYLwr%n%-BpS&a9$-a~R z`WP?s!FlnH&;O?^hzt8(viEm;U+j7RU*!4n4?G`!*Yn-)dfEBf>;JLd+VzWF_S(CgV&w9qN*@*Y%N4Fm9LAc=?z%d?#yC(pAT;KU|HJE2Z zvOWRe<8JQQzH1-6Zr22Wk2mK1si2o#v>`@<12ICd8JcLZUOH)6A|SlW^2tyK7a^yR zZl*fzoln3AuUqpEpkOEJWe$oOnchGdP*A-!EC#|(JsVJB?TQH2%4>4E&B%GBZJsH0G50M@R9RjX8_~#|H|So7PkI! z{gzeP`{A+A|I?_l!4^Il<1C#wlJV711WPf1@8fOnY zYHFjQ*#clhAt`SAKryAn*#>DDU8{@->vG8Lm6nE#e z2aMG6Zck2B)7b$l5C_tfT5Xk~oKb@F83Q@M6QW5Q;gp()^L!Dl^Io#!2vVj7Ni~US zw5J88>~L+Q=TA{(F`4@jUTIt6bzERWAIP%Kdk%T-ayYgIoVD0%8xvo*pL>>DKCpNoIrFDODW~19s`nEHMC81hoEUgRU%kU_9MpA*u)JaXD=kh!)pEh7HlO1hLoDVrEFE zu_hGF=6R>nuqcE`(&>JcNQGK9I^S|QYr3O0P0{mutW5fTEG$3AYH#6P_A*gFW0EV- z%S7#C$R0cGhW@YAZXX8aN7L@3WupGC(d`GWl*c*~HT;lyHz77GN>rlRiR9CT)X>Vu z^g3c~XKXMp*Ls6au?EX5l@ny;k!vHb1JPQL)-8O%_Vk=o7J|V>&Bb#eIHjavb^vfB z$(c^Inr1M7R_Z||5>6KjB3)HthDnlA-phSkkc2i~FWIR?vJP1>FERGG$GVMiZfq~1 z@OX9IPd28{D0QMUoPx>&pvwZul8QEH@@dbdtS2#+4}a@ zWm|#GuWw$odD_O`Z!|ZawEL60)!kd`A6_r7FYbEpuI$>c*REZ|R)4(u+STaFcUE4p za`y5!mS42|jHNFwwU$m?{M2G~agWFNPw?SKxfS)v>HQXr_K<9wDRp^?%D_RK?nela z(bZNrZKwL}#vrc>!zQLtvX@&q1c$F4ZU5rOkCXoGf>XcTecZoZ^rX&d!}nkLXCE-% zy8pTh&WXSD75peWVH^*T3?tFq44lH_9hU1^T(rU>VU?s>RgSbQYETluu-G(!DdWxW zj?fP`z}LQf4JlAd3S~%(G$kWOeY!1gF~l98ugIQ zl@z@|)XH_<^o|*NtN_KL2?QP~hxi62D0q{x{F=r(jA(l!_wv^uDq<~-ni+fhra&iAKmeVjeDUzxAUXqgmIjJB&Qt%S<-FU zmJRnay|#!1izP+qRhu?MyYYaniHQN_P8qMi;G`RF^sY>w{q}nFrZ4@(`2Pg)8y`IQ z79_Ip$+xe);tN;tBWl9fM3HSbZc-hyjuRT%uar=bJ7e4yB>kmBfDX%}j z^Jh=sN92UDk9ZPO#*tTEef!PFpZ1Z@eQE8&_bgxkh0ot81h0M1S=vwcVLv;K;5YdZ zF=6Z@aKw~xw)86Gx$j!~%D;d9P4|8I3W<3BTUWBj@A}rCtI9v#`QrPY_yK-|PZ;}% z6ftG|?eD$z+7qsM()+IXP;T`fzMT2Wbzl0cPdn@X_{~?}@r246^Xv=w5jJ7$BQC_0 z@e?n&{Y$&)4W2sVeYbq^-xr{tJ@<^qz2bR+b8gwgeB>)wgOkg1gf67Cgq z8Mp^XMHk^)P=*-DRe_5~i|Hm&)n*FvzTJPFIF!5e(#yX8tFM4p-(|h%T>gfwCq46& z67Y%Ve-uBs#E;MkV;_+vr-<)(@$&yV^9xtq+W*EbD*5?0GPmB>cgXj=7Cip(H@);t zo$uuN(fJd`J_1Bc8DCZVbo)&oyYIv84}3d5y6744{eRQ@#BckRpCS9d`mPVj+<)++ z=S>*b2z46}9KJ}f?W?y>ZT_=3#iNC>#Z=d&$_utK~f8~q2KJX7DKRR#1 z*hg50DdYb$C|-TWTmD16wY2y67yt5~uKUerE?d9;i$7j%zvc%w$Deos%a5KrVeBK0 z!<2FF&j0+|eBkEK-u{Ar%RF9;zu=9|>-W+(pLWj~2S5GQFW!B6lOLTsVeBJn!<6v> z>@A;(hVMP~{Fkkqc<*hj@w?~T_T)2!)6QI2+I!YdrJFp>d(O0v=MTX!Wqekz@{Wtn z|HyB@uAQ=d`x|lW0`jzXzwt}wfA&ewJ@dBb|Lz8_E6$lP_7Qer%J_xwFHXAiw67`9 z-Sw|oH*epA+!#3JZCy>ea*aB)nTHbmC^}*6BhuuQ@$qLLcR6-)_1VR%50sm?2gtqu z?0od@i~stCS6SfY7v%r(bbf?P82bp$FlGFaJ;tHa?!EknH~rvyz*^=XzVqZ8fBOXK zR`O?0`_yMXjGg?GJNQv#!q`U?hAHEVzxA1qzVSzQUT}eP?u~kIQ~T9>-h16EZ~Dpq zKJjJK zCX9U;I8GVga@A+=qW+0G^~O6-dg{l2xY}`#yB2yg`<(pyU)#TN>u0OW{0N*d_F-c< zWqkRsKY8O@Py9sXdnbM7w(n)W^vusa>&|~T?eg`z-e0@^>6a|L`Ir1CG-2$+a&F4_ z;;oOpF7>p(GU8{f(dXW^b;_Gg+Vg7sC09Q0TTi^|(rcn$emXx2P8rW{4>o0d2~W6p zUg{ov@9&mReg1dv`nQ)n;ggvUyyDNB?|J1ne}2|on754uCX9VpIZYYA@T$KlJm+^$ ze92cE(We}L`>$4S{?DuK0OZ|2`uUw3U&>m{clpuwgs~6Xohjp!u6kR2{l>S}Um%@y z`8UiPZftTVY5%+}zUjELfLngNIP&`V>sOrm!q0dA z{fBq(*L+a=+${@F`{Z_jADuN}?8AOy%J?kll1ruSTQ2_83!eDftHTEq&+L#NUDvPg zz4z9eFKZlEcqTtOFk$S&E@H~~n(oD$$A9X>r#=6SU%fqKM@MJ>_TyWnQ%XN4#p_R2 z|7z`Ie)Q}KV;^QDQ^rqw|GQdOym;TQE@l$nY#+S+16TfZ^^*R*ciepYIsdAC^}KJV z_|dZ_jD1+>Oc{gkc>lk=di8kzYa7pc=Pld6j{eP^SDgQnuN*hzvcEX+)Gt2ijr{1$ z31c5_JyXUX{CPa{{*`B~HXJ?r0`bHz-~E@`+fM%BGroH=`l{DG|9#g`{OFkz#y)&- zri?$T{rvL)nY#L2@EK3P=JmJj-y8q-JzLiV*Mm^2@QTEtzu-q_Oc?ucn3*zO`={SD zeskO#E(ac8JpZ0Azk~Vo*Y|(_sXt#sFWdfZY|pRNAM&Hqr-0b6)eBQ$DtN=~uz(@_nnvz5Q`pBl@}@Z!G<6fmry$(&AlcUSWcM$ZKECTKi!a!R?Edtg-tN)v${uXt3%dim zu3f)pakT#TyNsn*?uxGeuRZTwSNCqMFKvE*-`c(}FaKcQHCxGj_08PYiEHmzYpp$R z>xPBjZeF~$XZ2r}FWLOdJ>T5=&Qfvp`n_LYI%98h)!lm0p5wj9f-hfYmrq^#*{)q{ zx6E^e5L{N5_V)=l!{+Q^ztQB2nLbb=2%XO9ZmpWsT{6*UjG9DcJEgp2Y+OI3Fi;g6 z)P^8P=~AJBlSzu~Qc*yO^ec7*7jk`szyd`pTtzl&6AHHnWEF8p@!K@DRPaXN*mWs>p*%joa#17UGMe+*%-#z5E&U5bU$2<=9-2nWOSn# zR5J#;@$xA}s+#V#dw3e70InZ_D(yto75lAl02%bPBS&^m`^>#=g(o8Lfl|!nK2q*ObT19N4 zAa$`!t5R%3SkK~No-8eHOi^{a-hDJ==<#;7Bw-LiWytN4M6!@vl{+BLA<5>T#*+mK z+BjiK;RwxgsVjtZbVzk1beL|T!&oEIEl9(N+rXlUev{Xu=_adfJYh;vkjW&SqjDG< z>_p(9n~v6p+Yu#OE)@jCrX|d~G3bWq1EjHW{FEYM6e3JsFP4aEPHz+46v|8Kc#o2E zUhvvhrR?1U39&XnG5GE?ys`9#c}CNJX_;v=nrbv40zj;kZ#JxIx|)vIbREq^dhrz6 z^JKyhN(3jp;D++vyQdUkj_ib$S~A!c;&z@y@`PChYjRE;7RZ3s=@L$(7cH7X)86|p zQwlAV#q4mUQ)9xq%A`tof{_c}!yiTVdk;-0To-e@MYo%bunL;Y zn2CfVv}2_#Y!3hjMYJ&4iQ6rKs2lcDXhsoDR8_Iu4|Rqts>cg-oM;#Wq!Ldz^mIn7 zq=o{Q=in-e@1714w@0~lJjA8|nc#96fi>c7nKWyt+e(K6O)&;*w$ttA^-6p7L(@81 zW|!?n`lU{)fM)rAG@XgZ+n`g+kU^5KU}y=njFckOIc?VqXB35;Jm{O4L^wuW0NB12 z#^a%Z2sC-`Qiv+rlG3*G={Ak5ykSPc0D2H@6uj28Ahg7Fr9e7d$u=r*zf`6WbclII z1@WTeQcJ&>QZxt<&D(?g|BdON9rS-Q=DNHBSP2yPEuEm{@j+YU|#IUv|Rw6*I2R6}Ks-Oe; zHWxw`Z<M-QcIwIqh3k=g(Z8e}S0)N(XbfMbC|vw*v+ zjrBdpFUWsl9iFzM&_^u;<#AWbBhgk@k7gt}W`&voH{U2z^(>O3gMd~n)AssI<}BSz zq47?3kl@J(mKq2Z@0#B3SIssV3BwA~9cKH5Xh$`S^$$<$NGPtr1&LwEo5t-SWHwS+ zSIAfNvK6Umm0BlWuL3fiXrRdY4O5CpyH`nC-V=N@)eF0P-FrI-XE_U?kPeaWwxta1 zz2AjmsBNsjcptE=F7EG(LbV)J*i^1C@sBBFMVofL!Wx)tk$PRLeV7Qmi+DM?Af=h~oQISF@k?7$Dk8;qtQ%uoe3V?AS z6jHu`Jbi~0SwF}!u4__h76lUrW*!Ruz*%uYR9QH0~sJR>;+18vx(qK z|1qI(`z{^NTb*185ivne=eioEXx&mEkR5a@F2SYRML~+@oUXB~PwUVlF_mfs<3k6M zbAX%dtEI4{kYOfO52OE2l*hi>h3tW@E`j)QD>$iL-jV(pC|S>9<_0gl`>~ zQb=CtNT$-pGv!VxB=v1`C`TIMTwb9Il!(TLct0s4LKgE-&GuHhp(WGZB4Cl_M2t%z z!FVeoK-E%|)7y~_MhK9WXvXvM-s#fZ@8_#gAlZY$G!zLXDJu*H$yi~Kb6Tn>77#h=5O@@_aW*hZhqg@rv07ihJZnc_Y5ep4U4Web7ZYeLSzlu z%ugwJq1!Nj<}7KsO)T)nX8f@#aEjOnUj zx+~$J+>W*eN;l}WHRi;#V3#X`nY3q_8p_rI1=;%HlmZ!gR%*>aS1UEWoXZju@5eGN zGXy8$7^Z?OI|wALfu$w-z}{IMHBEKmDO%Q2c(H|m;XK)KiGE$S;k?j8*qEjU)Ih0L z&KoN;9n@G@^md0dgJ_XdRP7BUrI}KPgqq-M992){Iqxh=HbDbgx?|Q3zK^2egq96E zL^R1ue1i(6v!K>;x=xc8{qWf5u^xD=2j=d9 zOZCyS$F^u!R2Gk|d)i@^rl4oha}-O{AP_v#7VSc>?8Jay?RnObW2>JF#=jazI2xPV z&8#)2@uS9Ol#Xp%Ge3=;U8u(d?H#Q(4-dyTrh1^Y=A_D=>a=ycF~$T<3V6;T9s6Oe zK#$=2K-;jOSILo%3lB2!99Q#5O*f+mFg7ARswYERk*hYQ=k+Sl0le*13*f^UE?cyd zNrwqlQ7PGKBk==Rwrf9P)6MN7!XAKJVjH1yVU_Enw3W3ws+9+&Uaynbc7u+@vW0{Z zPo*1le`t2pX2T9DscKjbCAS#>hrJ}ja=Vof+YXDlSw^qUX)HEwx@Ror4fbcq?3 zgJ{_4fXN1vOC>D?Y?SFD!Qr{;HlZX*!|0;ST$YjEV28(6#`fsU-@EPjc!K()-FBjr zQFk;?x#*F@|Fr!++(E~T_xtc)os=(vHC1=8P#}YN29^DMu!kjzD#d5yaz6tWT6MeZ zdUoVuvIR}dh7VhLPTTLp<>^6o$B#-+Ip~244&wFakqgcvU0)&~@Q|Kz3>}u-esI7h zuuPO6wp!Hofoi!fb9TtgXFItZpY(j?6dmb5a!46=UF`s?;+XERz`) zx>~fiU6+d8Lf-;Hs8uqRWI-p>K+^664@hpcQ0o^`aj}7vNr^aM*W+5uuz)gs0Cne5 zxpC8{D)-FatDKL=**{w4!Vjr($p4ip_y3f0YVd(7cXT(NN2+pR=%MZZc`AV*S-z+u zx@CmjL{7-YSsIaIcm@lrsUq3x#0EpVz72X~+?xo-*7jnj>!{-m_eH0fwk(=8yWKel zK>%R#D^&@>a4>p~Sjj4=J=yjSfDYa5cLsD>(0idcBqw2U*k%nFG>Q#91e7z4U>$VP zZ5_hlX1g^&0DZ%iqCy#K#qLh;!9|DWdl{|8ffJof(o*!%xu@Bfdz z|3CKr|JeKg|GfAA@^lgO@&13;l;W}X|NlYn|7~x}{ilBaFHOhQpY;8|Fs9hpS19;#m_8#d+FVqf45|w7ET*+Mq&+dQ=IRbvcC zIbx07@|L`eDS@aHY(i4rtoHIG819+1V3ZGlt*Sk!0=#EZK zN!VqPP9n;&B^EA8fYntqlA4JQ8bxT4n-M|d|ODM})p zL^}mVk@8?ULnLZgk7&(88mvEY235+3;+b%$+A>MGN{F};C~H|wE%Qpk7Kl`iAu1Gu z=Cc`e>KL+k{H#XOafM9K!qOE?vI{hl%F#$^s1Api>3Ib7F_}%NsUhN-PE83nzAz&! z2Gd1XV3;b0!b}LaO|0saLfLLFLdNP9JAv1@NTg+DBkd{S`nP6K)v%34I^N}2hK%b{ zsRrc$>K00 z;gk)rxr)?{>vAFu&E&XyHpn~ufnFQb3!c#@DiM0wGfq@m2vE!S+<=zQ(qeGPI#$t2 z6lQJQddjRuH(L!#s@6!bnSPF{!ijz)F=RL(UBQQ(ji*Y}HTVZ~;>x_a0BgjOnKvsOC^~vS2)nCK0GrqZ6LxZ$g5m17PJNGs0R4h&55X znXC(j8NeJQ-h zxAj&I_Z}FaRJ^I$>0B%^YvZm@%m}Nz8;oFm5Q9yJ=jFUrK+0k`h?MMbF065(q6x{t zwA15fZCt)`Mkqz)Al-qRX}6*b^O~1V-W+)5&~z+95?P9*QYj_p*rYAYYFxZ+Mp$kn zfONDI$SMgfU>Q|4S`&?SLas)|GV3ujE0$hrdhZsqk+bX48DUB7V|qsCXfHgC)U(;B zG-wvWNI;4S4MOa6NEhn&YoP`_n*mm4+R-gS^_mp|r3?nP=rk#iR4i2EY8BQp3nnDB zVisH%LZNhNM!06oYUBxls3L9Xwg^=s%F!UU5)WIHjl~okDTw%2-%|C>xatS96xNA zg=(*nRa-DsO9*M85u<`bP@%kyic;ZbP*uwrELMUUFI~yj6Cjav@F(J?DTNW`wbB32e`xF8t%H#n5JCeI&~PNP!Y0ZAh0vgCx}zgiVz!bm{%Y1l zZ=ad26jT~;+al>=J(BHvQDM;p0PGnIwT`gfMB&VSgUT0?$z#2OvtOiY;#PW$Ol)(Wi3-+uU=aPXa(@7-F zAf?qMHDAv)N`sjg_db70*iqDQv9As!)yV>R*$q%R8ASya{%|n)h$5H-5A4RZGZG4hN6@>aiYp*dDlljF*DL4l53NV7go%7D9n` zBvzEFC=#sqY5+_PtX#1csEhsCab@po?kA)N&4^Kq#{_dAh3P)m#q$wFsTo72Xe#JX zW8${kBq3%tRjof^Mi?Os)=5W)LecZ>hSL$lE>uf}Y7HJF!&NHnc{z0A7Tj=<*~w}* z0Nlhayp|TNv~zaiSkkYG{wK z<2e!~87`ho&MlKCgPu%=JIN&UXqHL+7|DbzL$c*Yia~;wOd{yHiPjZ?If_hBF&Xm0 zLZ}!WH$>OXE3jzUs@(}FEKp$LWx+|+067;ET2akTpJbBxb$zUg$ z1Ru>ZIaYHQt{B-ux#k^dSgv04eAP-3Hgq&SiN&0rQGoJ8SW?PfVgv%Oi4`V0%(WOT z6QHZEk`BZmEF7yVn(bN137&Y8h3vdCx%r-NcrxklBopA#EEDV)=?TUJOF)xTLTra7 z%eIIG#dN-X6g|N@?7(#Ud7zvUgUyG$iIfSHN)Q{l9?46)wLlidXgwt;zilFlY5 zzQD$b`DOA|PbTgTGKoYVy)pr_$4XDI4wR|0e90gHiT7@wK{g~C#!+N~#`|=w-6*lq zbWY(~)c`Y&@hbp1EfTaVwwcXl!yqHYAi3b$MKVLvnLM85=9bBqFZE>7+es$Kqgf`$ zN>51MWU$g8Yev0@DzuTu%tC<#jv|v-oF7((EzfDe&9j14OTzIuC_@#h&kXHSBuZ-v zkE>)l&~P<7hQ@PfrijzU`DJp6CzI|@GKoBjWkMgTeHTV^EU5*R0_$=@zg6OEULt{` zt?}`4ryF&|tXjatypz#FLI)JoK#m2&{h@bdqSc*9fw%asplLRRvW0St!G!YMGP(LH zPbQt6WD5(Dq>#e|3iuXcu@7uEp@ zRd_1P@Ip?sLiMmy>gl`}UBHtGmP(`xIwo_qBa_% z(E^$Af@jPv6XW2-|9|1^g{{kWZ>?PG{qV4#%Z<_5V+YVz%ZvMc#OgeD4xmTPtK-8J zb>h+HBWCB(J=**-IL!09X}hXZF+J~Wvpoj%K>zFiBA3)|+w;AiIHf$q z0buOtGNG*~YE|r-!d_q*SFntO?O&K|etY%g`p*xMW)|8@4O2eo^`?hk*f>21&T zdr;}=ZC%n$y)&3r_=gg**svSA=TuV>Oo`2YIK^`q$f+mzAAnEj zC2{P4KKaqmW#j&z9aRzKaqA|3(HlLzGj7g=xuP~ZHSa%TpZp`;|BjWvc`yWoA+Ls` z`kQADc<j|RFW!sO~IZ5MsuorxUt|d~j zWVPo_G7=;u#a^M2hZF4vJmBjxgSmLa(wuF!gTZ|^Skjxpm|_hh0tsZbR;)90t?hQr zThmL0D3a&r^EaO~-E&UzrklY1(~XY^zB>monAhL@sEK}j1kjyKH$Ecy9(}s;%fQEU z!^k4-P5KHE+n+8Z)YsCT*gb1UFt#t zXJ?89v(cLdD>Gh-8OO@{xzP0K)l5Rq-Mqp_FYYD4j)%!FIv&JT2J>Qx5k+ zjY1O1v&DAC`!C3NanZnNRYMh|UW~=bj<+(vh6d+CdaK@7Q$0vb_HAsgTu9^TX6y@j zS#NaKc)GcAuy}0q-OoGSfJbvS_OrXW@ctF)_v`Tmr};_S{viMR*{VHFtoPrvPkC@w zzq+kA#y}Lk?d6ag%(fV(jW-5?5D<)d8VKveT=MH-C|Ld_RzW771g<{h|S^L(s!$8Mny9ZmE6 zEc8d!JiiPMTey#|dH+RA_Wu;kd&np^LO#>8fddF_9mqJwfX_Dvc%nFvf&(-MDxD_e zWo2qMlG}RL3oGNP?<4X54}qmbv|Fc#>0k}x1Wu0I;WE?~+hL-3fNGFcL~M1P6bPl$ zdcGPnaUHWF8Ju$LVh9Y?;A+E*`ef!Iu{uza)%+0C`-3^R*xsZwj&ifHEH7jh-n+VZ z-r}D1=PrJI@$Jj6TT8D!W$D`Gh1Gvw{p;0BSM#e+U-|jUCs(doDX*NleE0I_mXNKN z?%lT>+@cr0x%Jdt@7etE=0`U#-xM}a-?(ef`!_zj@!F;Ou7BQm;l|m!?_J;A{iWS+ zUU=_rZ8yBhO`iZSn6X6dIJz=j?z0 zq>-fE)vA0j#kJ)^sDzhtu`rdkBmhfi`?8mVi!sMum$S7Z0bAG*RevyI!!`B9q8EZL~S8cfrEaH2SXWg>DtH zU4;(|$)?@XI%%oaECxUf4Hs%LQICW=7F!7x6xmpECUvL)%_ZRyqo{3K_Ci*Q5R00S zTCNWC1AJbxp|+MO6_Z$yWtPs}n#e<+^!SELLs^cWSTfH>UaVudf{Jrn$)s|rR8ZAxl&#U zkuor(s&YJ>pul>oj8ZAHEoNKgdOZ@Mz0mxalZhD5om3%Y0_%OxuB0RKaHGegyh!noOS?&} zB!#hXA{t^DPX$tNVkwLZSMN!pQpj*mFK|b3G;*A_Ypd}~bMSG2= zkirr?X||j|ooF~$CMk!DcEoZbJ{-bXGyqpR3ei>v21TQq7gHqdg%_Vzfh9mXXCeVU z*b0hTMq=|?#f+!(<*Ke}p;od13{AJfM)JrKJEe$ump%!BX+?OkkLDbu!DidFdU?nd zOBBx4huHvZv{Gi&b{Ef^QXthqvTv~nskxF_gj}YT8Dzv5Qi#TIMYr(^q;jO>ghRm6 z9n)q6Yxx#xD7?T}YE1VsYV^X1LLrC?ebUaOU^C<;*8m%tzCcYw4ohyfXjO+o(*(R5 z5(rIpqpnn`wudR)5`wLAD%s4cPL7IE(89k>KuHZGF7ZUY&?G?{&zJ&I?$On_1L|_S zN`q!D-hg{Uw5S_}rTUaY8nP7?l=5^s((&T)wIb0LQP(S2A;rsCOEBS(m8b186DiDM z`%4uN3oE61J=V_*!D6&u%ci_6CYs*R5-zEA3L=k2fV!0E8VgrU>nQZ$o>Uj}h?x;; zLLdz|X}n4FDk-cic7RL~k^(Kh697qk@rLO@XKR>5MU+fm4mLwV*vkdsbaEYc$g6__ z07wi$hVy8{@?2Y9G_8UxR3lxsM8mEb5^gj0)16o+uWD#6hci~Dr(@8W(<&$??!_&4tpvjYiGYqZi=|AN z7_>6+){yC{?m(;ND-|IbKo(y%ts=sL2{CFW)M42G8*!zcz$v1JM5%m|(lk{@m`>d! zoqn2GomLT4YW+dLNJq?iWdIU}=d9C>pia0d019A|HFbmW8E7brF;hI_R z$YC!x505b+I@c=Wg)muH1-S#adz8}6d9y17ERH61sHGthC&D2nS47%XJS>&MmJtEO zRzoYK^BvFEW>Ca3Sau+k7O$I96v~?HIC8xtr7gturl}GSqGUz2oQ5oO?OeRtOTj8z zVerMLP8zPpVkI+`!Nd7dqnGoNObjF|i>KgNb=b_x9T#W?TjX%yg^%{k!^c^c)L5Va z62%#JuascmLBUW`Tw82H;;EEtLsH~vgh#t zP1Ar3eVW}M9K%T}GgDJlNhMWDO{uD+Dt()nq>`$nGFRrNyJ_*-qV(bT9G-fipQ2&| zqFzyaibLaY5fLsj$RG+RGAJMdGRg2(C42YTr*?Mjs>6mp_kGxZ?9c8#>s#x)e(U#J z<8S?bwK(7Cf@mB^^%FNXpVS7%;DHaQZKfkY@5gF&hhGFd8=8{l418kUBoD%OVCNEL1dhXaz# z&jA@PU#jpZr1PzO%Aezg{;)cX^Qi(~lk{FLGxaFN0nzd`_3c^T$q9N2v?#)ddhI#` zrAjf0AhbLi(!0|{KA260(#|9l`>Gx-datm!@grD*vOsQPQd!=qTU#8fcDp&H1 zDyEdGe*hJ+dVvZoU790=AQAU9kZci|uyQ^{tNGA45QTGDP8dY0GU*@r;-yHI8%LIY z60DX^xurb1o|=7M(E=6lFvvHVBqYh-@+)L9gKI)xANQnbqZ|&)X<%vLAQ+>TzBNZE zG*T5a!x$w6w5h5Ut93FSsEjn9uK~pvftj|vY*f#qDc1FoIijj&x(e^jrXV$*Vq@(z zKB&j&u%N>6QmWp~)Z_xI#hE_hb^nhUqT2Q~r-7lrS|pk%#q~06VFV>fp_FJ=5e(+niHi1egLFmZ1mO$Y^+%GF>x7WcN2>F^}xZw@;VEEV*I9kJ7rr0P9Ys+&r1aDh}ex>FkvVd&W#dCtuYr(v zZ7k0K!=Ea^jYDDB*Pb-QJjVez?s@PE70ZK&&oGz35}4?%Hb6e5FgzCIOoFivU0cPUCz#$p=h)i>(z#IDXC6q@ODRO6nm|)$6rRE zF5HSTUNFU^0=ZlocL`hPZUgWCU%zy2bMLKt_wRXkzp?w!?oaLtyEp87aOcH4FWlMO z{+sQWZBtwSzIAzv+uGfH$L7y%a+{Ce_=Ao7#*^1SvHmOT>iXu|>()|hk6ZoV>Q95y zf+wwfaOHuO+g9AmZ(siLWzzjQ_y2TH-C@@^T(5HduA!o3%x<3qHjGLS!Hi?06l&gz2yz9vd?hv ziFtJOtsi{eD(g33jD{u%B-QK+BGJzDTGC?DN3X~?e*HzD&O7Xb=SOynPppF>vzPhC zYYqi!liL-6ZajD>P@B->0^NA+p+IeZVlL2)Upo|NT2uKJuf*zgc&L{%TJCg#uK((x zKy8Ap1R6NzA+i1|hXS?v`6~il|FuJb+Jqh#==y681#0sXbAhhE`cR-Tok{B{I?Ys= zems{MonTf+HPrpuLxI`^TM5(`Hv8kbA3PMO&Cg#EsQdMY0<{S}E>QPx917ItC*}fm z|N3pKY}7D$yRfVMTHLRTud>g4rhWX(8>XUP9dFkChJ$X^hZ0STxqPzEvS6kw$C=aZ zWbJK%RrYD133kE8Mcmro9O!~wkd-d@qh`PIwRfKB=gltCYwtMI&zrq{*Z%52lkE!U ziYBl9^?@eag&x=BwYMK=vfWS2HF@o=hdR{NkZCEIBxvwnAEfi@W>43l&6geu)F#+U z9YWC6>HtMIfBZ~8Z&ruRA35wVo9wUj*XDme>@Sp=JFn zKkvO-DZSq1)}hweWPe3#mOF=9V-tE@YnHnX1+NS$dcROCmn*uy+&fe`oAu9CZn^nf zV}0v7uY;_fx>y!)5D^F`K|m=R>s$H1f#vP(_OVud?GKogs~k0U3-&*Lr7J41v01Q> zJl+%4ps`o5|Ao0MOJLHy)j%=1Y@$``)N8dI+nJ0g+SIgz{Y}5!sg)y#^0x^(x_X2B z#w>5YLSvQ>ANHBe`mFj4GMlIGu+MCM{7Rn%4*Sd|@_3&G5BtpK7v_Ct-0Ph?vUxoi zUhHmBt)vq=wqe_PRtD%*9IOm&?tl+IUnl@)I;v3cTxBEyC=d*8`DXtAE@o-x>6_8j zKLvk$|K~fB0B~6j9l9(-qe%&F^+O3}SfzS;7p_5_RxVrX@L1MMNgzQo>FHs^sh{wk zAA|deaZy+CJS@=sAORC5t2g-&8lK^c^1=BQ`Vjis(^xd#5;-mgM>2YONGq6}EN5Cg zmlx`Nz8fS0BX~MMSkiEs+s}=rO`(+wCx~_|sSv$#G6XV*gYZ*XJ=YRIP%@>ToPZ=1 zehFXfvh4H&CeNp%!<5S%_#(_NO2ec}5rB)+#VB%q+r9|hgE z&~cIi*~W1Y@h^@K%AwG7+{qU&fGgbCsOHnB!^pH%oOVOKd^TCVcE`zL8cQ{UU8Tf| zUZtg1a^V)=&U*1$BMNS*azQ2MAIWMl#e`K3iV1Q%N~ZjyAYZNxfDcb>tY7S=)y^2k zqWz9I5X)SSz)@RC%4SWUJ5B}<9VZ>w+}RB`3mqq&3^xlMC!K!4GWY}RR!^WVj(4ro% z*Dr(z{$T~ytJSsBv@%+`f9c7_pZ)Y1VXLc_0^N zz_2_c@v8%MJv+-gV@*OnC<0CFrlWBw5+x1jF1}!V%MGv^P<)q(^M5Hm4Jt1lW9k2kl?*UlT`}AgS}b{ zPLp{U6D3|6kEV@4UiBk`W-T5d`?*-I*^CAPL)h!(drDu`FZkrP(8<;jTZ#E*O$W+; zU-q)!Q1<(yuKgNQ^o&44@?o~1B_e4#826`%h|r0YLEKQd?(IUg4la>!vr0OC_iH@J zSh%WPkoff$CohnVan;d77^A#|PXbwf=)uGS2Yl0t@j)6qZj1qsDRLx@fJ68G(Q+Cy5?x;7%bP{SuW=glg(JZer!gH>b6qY!@WzX@j#qF^_La+$ z%;g3>CWm27u=Z>O2i{as^j@?7e~eYwqCeKJso`{7F!r|hdW;aG5$Zl-P-!hR(#tlG06)S%_q^hU)k=lk*e_@6;aVyNhqb<{c)+fe3T9&{qN{X36=1n8 z#?$3e8f;)8a=#I#LR`F!cH5PN;Oj}KFC_^rrZg4WLsI3+GBxxkGlgPT*UwLNkx_AG zS`o$ca8R61S<~kHo>x3(&cMM}_qpCg2%|}bX3-JV2x+laK1z5BrBpwYt+&wBfSqWR zSFI5tvacUgK{A{h#9ROlu9b_D?Wq-2|mx-*`pN-gIusdhq;H&$|*eDiC4-hKDx9jnAA|FXt z%57xIsZ9dTc;hlx4*`d%NeU9|FxZ(haPZarFxaP$Fj+>KsK}wIHcs{#6uhr0>3qK~ zbNSGK!NEe^8>`@U7;MiOIQZ&5iUmqN9r0%pqi(wiq8gJ!y%_O{Abi)b3LfH6Bh=GY zipdE+vcq6&&Y&NGX(&eNor%~kKoe49TYlZ&O{tPnWs@+-*pV9eC+c*Jd-ol@BRbE5 zCutl5B48y-?H)YI^^cE8n{x&ZzPe8Zx}`uL=J1|~O-6aSGom{avI=LX3Ar;Fx58{R z9#-m#p+`aVQn%TcEu_dMR=%4fNMjdrPyJxGH1|Lntl%Vv8t2`B?bI&>u^t*sjoqr6_plfmKy3(~o#S;uOE2)R~D= z(WmjU9R{vB0|#H-_lOOtIVlz+O;O6R0j^qQ#MZEms!FXh8R%F!m~46DWH#rsC4;3o z0|#H-mxsy7AWMSBOiWE2WT`lW#l%j2(tv}s7fpwW7I=4}_xywi+mOMfd*%!re04wN z<%*-a1{FZ0S%qb#g6!jpg+SP!(V8GMbsL6pI!*g(@oy=F;{yyBV z_JsU6G7NO`SO)7S@oq6U@y24<)Z6xFr=DKJPA^=#YtBH5iK9dzUh_h%HwyLmk_-<$ z(>k2acvLwj*J`6YQD^%+6<7A-wLqp82cG%04$y~wu}tIr%m{e(_j-P%Co6T3OsR(T zc`qlNe!g|jr5Dc`IQZ&*pY`?XTsm5#>+zt69U+>Z3?MDoqh@MQygEqs3aIX7f~C`jts_g1`h5m&i17Bo=c-S0|!T1`wq@@PCx#-=hE;%5A2^5 zIXRX(oq@6czh#M8I`=c@esJ$k_W0d@+U@LoZ0FAH&uqVB`*B;px^?5`Z*AVZ@w*$u z`UlqWwSQO}t$u3t#jB55`K6UdFaOB$lic^a&%1u#6<>N2_z3tZUwYP>8+gA|)_>si z4cmby%iNd3$zJf@?Mu%zs=1@G{>0N)<5vZ;S?nI~kC+4F?>pE;wlDo4U~?O2^b>5e zIqrhuWGkv%dIqTRBB=54wrjlNF5_g&S-bRfP?I0jbd&9xj$Yq8*<6Bdz4f45A7{I= z1231wL4YFWR}W5hn&nIT1{=`y8*Q^W&}9d^Nzmx09W?r}r>~}M>9RR^{l0^3D4=u8 z0i7GZTXY=kN!yps12%?fJ;pYh1*YX-52{?c8PsT))}w9Lc*L|E>@{nbo(gI*OzTm$ zYr4v`9PBDVw?5^dThH09%-Xcf?t4x)uH{QlHrRls@7ZQ^U|J3~r?pE@IuP*g>1*@i zvztY^GUzy>|nR2 zT)F|&Xz22a?HZ5hvV&b+?b2gFO~xFsY`dnbblJiF4Rq_H54zQDyE1ECKGv-cHgn}m zk22VRrn_vjInZSX4mY6D=MEaZbozG4mM)ubpq;oxv@h)eHb9rxzHOV$0$p~n3vFN8 zJ*e?pr^ib?X#b)beffl3NMN!*)GjEbBGO53P>cve4bs(+Z`29&is8xBo59Pv7|d$47Ctzd+=UF6s0jV_A2>cCX4$*;?4`i7aK z($R>@4o0X&5tWaRu0}H+fh-UUhya&mkf|ADKbZM69cDT(+#gSI*D{)Lt!`L>koUyU zOvdL;-R%aJHZ z4$y3bF^UL8kW7scrNJN^=7ZsiSHdr}iZFd%9{KU znh75!V7%Gs3`&h!`Kk%fcy9S!oB;hk$nc0>Xm`b4a~dC9jN|^CcigK}k@SG$%UXb! zVpR-{`-h`eIf3(%w@6;|1Xzz2rC7U0NCLs~VFHeNy^}1_tfmGbkdZGwj@P7ku_1{_ zpx)^-N(sU9a*a<8Q$?f`E~DXiq@nZ}Y{0biQB-S%n8I#@(amGn>|gLB8?>a~(-}dS*GEF$E$8pDH$k2$PLAC4VQ$#qb3=Ib+gj+Uf?p;TLBkT<#5dx^@&a8(G#DEX^pYm@w^P0HpQlIu#!XO&qw-T~`oGP0HrTV%L?F&umKNlpFRNRex3MMVYj!OOZsXD{tSyknZ>hkMOld{?U@_LdoIXx>~ z&H(>)m8vxH5ShdwGS|R!IcbrUv!!NB?@>8?Sd+C2ChBHfn+ zDoh8D&VH{wP0D6($m>eVXEiRzaHu58^|U55i!GU_pnkC!TQp)I#kYwL9;7Y0O`LnmnJ+NDM{$+r zl^;eOYgCHE%2gK_#iL7iBG(mh^aqN%-1>{s4=Blbno}R9j*) ztwpI?9lUM^Z>2%Nhp`rbS@H+GmS(!5Dm8@jea<0>!RbZ z>Aea_T@Bt&7iJ#j#&wgEXLaCcclpPXm!q?*k|(>)_GPAB>t1+Oa@||E1NYxW)@{5B zcw}tbILtqE#916S=^CvOibnlL=(L5_$V{mW%k%9(6YCDcS_>w4NrR?vVALHZi@6JA zxX*x~mATFZ;hL>6kdI`C2}H+;ZkrAfA&u{n=~yW4&-Wu7GfIsW!xg61?(=?J!Y+iP z#Za`{Li7Y&^9|EtqAV4|gscTfuTSre>ije^Mi5)qAG4;<{ca+LEg24Q4ix0^FS4Zt?k+$Lk2=78k7msUL>>tTI4B(Pp6GX@cJdd+XuUc06|=*S!>!_WOUKCq%tuZPG1fE4@yLF2UVy&pV-WAoNOTn#pwF zM!Q+lDibOhoYYGGaWNy0K@fm$Jg5v}lTlfb;Ao{dZf0{4khWS&Pjw|`&k&8K&+q@k zhxh*uwCn5)G4Ow+Avzf;9Nqtaj|}jUz5jTGkI2km{7vfVbQRgxkk?0S#NJvv~nd)WZ z)3G8#s@k5iJ(f#T7jzwtC|(ZDH-?GHg?fu?QEk34?xM6xQZs( z+&z2W+Pe#6|GRVN>pPR}uWpaFzPvTq{L-er@r8}<`sdf%YoA+du6|}!Tlv?O+VZEC z7569H71zgI<)wcD@RNV;e&Dth5S-6RY^k%2KXms4FK|r2!Nc0qpReBi!1HGW9J~y6 z@|4yNf#*3U;NX?59RkmFOyG>W;5jn_4nBI;T@ZCl;EcN$*j8uso7LXHVIcr$BW;KGc6gO8qd7d*=` zfiv!cXU+&bVsQZPe&7dZ1RPusIXS03eP4d}1J7_wz`>1>9Rg2xOu)f*+YW(SX9OIa zJDznH>^mlK#$E6<#{|x}3vQVaaB$*u)?IMkF@ZDff_eNued*lKgZO_xw+HS1@@{zN zfA18w-?=SsFKxBARySX@>DhSIMs)qb^%t!D-CA|^eXHG-kFE??_Ld)5-gCds{RG!< zxe`n7U(!!L1i0#7iuXTptpehHEbZ4~_*4y*TJjVQmxJWAfvF6ok&0+VqA-|8yNzBW!JDG|9)zx#M+j-&{{&FSZJ>^NI#!MM#PTR2gw&ea zZndL<5);RR4C!qm**2H-^t|94hEmD%Qa>xQb&gR$jL&gzD$wxaI*uP3SMGm2sP7`E zuQdn^SuwBsqvLEPIBfJhVNavd@1@`(tyWv~s9TFh(^$TYjBMBU@F|(D+Y@gp_7MsFaG4dR!4~*K;(8#J#)sKhEf;J8s;E zh+4MFHarEo;nBQguNUmJs%>bRu2WhoTP3ne(_5{^D`8Z!U6o~6nTXkc*bqSR{u>QG zpy`2D-75@~YBq`XB0Q$m>9Lm?B;%=gv(lg_y_72WWI5@bwiVla=4x~A&i#*_HTs7A zbXj3Uzs|Sux)kh2D4-*Cj4Vc{gE;K(mIPMnQaG$n#eiZv(4}>w&F;LVVyk)o4SZc0DFFe$=c-Z#>&<7j^;#Ma-_l zb2UC^@BvLf)7%$Vjefv(_YNBEAd0E&7?;*+Z1&_e8r{5q7w|Fk_)ODnoEWXBa{msf z&(LEBX)tWJ|EL~sgL(`-c987Cc0DFN-a6<~2U#s_S7qI$X1C$F9&Z|aK+_$>IE+;%-CHC~zZ=#6K4zHX~W%^t&ZHC{IOfTo}6ar;%nv2wrrK)enPzfV6+T@bJo z4E(MG@j5utVqX4YgOAarXPKDdDvP;y@8x&S8hz8* z`Y+qkW3#{O%$c_N@+lx>D6)g&n$ypK4hSt$(q`ou?9$Ta#^&nk9UD$ozE`Qm={o>d^Zvi(ZZdxJ|9pZ!^RZR=y*FRZ zpIdSrFmxFV*PKNEze=f2U*W77?4F|=zn}ZKlQ^0e(f9t)Wy#=p+f0fxj;l`A(kJTK z>3mlWrWZrT@6a<&;%i<2y+u-=*yJo*XV4LGp zE(>N`cd$D+pg3RqpDsp&+Y_HZ`~Bm#8Qy%EH%W2b$=>2>DV~~zZ{1+`=sS(yk9o>T z9L>v8FdlIxj%!Ym13Kh58;^E=!r-`DIfz@W4mM>6 zJg*v$PGve9k6gcJFm?Z-ZKgM0rcGMvcCx2)sHMhu^sa9kmh`S~f6X?>r(CAYpBR&csfaA=zQ0Ie0=u%Q?^Ood^u%O$~7lhGae?{ELD1^ zb2Ns|cYW?zv)^w!iK97s5*RuOgX2|0r-R+`0mlVH=c$P>jiK{hpY|KS|LuoQ;%kmW z1eS8R!S|}6)4}fgfbWr^^HiS3(D|-^L5$y@oY?01^vg+u=T$?egWdT7&#Q*cQ<)k= z=es`gB;)tTy0)3#d^us#QnwTT7Kd6o8#@2KZE*bXMcW*oav3uZoeuW<2NVy6&Qrx` zaC`3`8^7OoyKROyUyhrkxbEb%;A$zJ`rctS9zEnWI6m~IlQ^1>4#4I&W^jDkl|ggO z$(zjsj>g-=)A#nX(df-7gW;QLy9~F$Zv6#rc4_6tV4o%qhq_ItK}al5$vgIoW;^`@=6 zwqje`oA2KIh0Pz{e8$G-HhzEO?v41y&iZ@Se{sFM{zGeDT>Hbdd)Ja{=T`r2^;N6# zs%PcPD-W&Qw?esD_v2k3alOXXbcL3_x%3xTA0OEJFW3F{%6-4ocU=OJl{?O#5U(-l zHD7VFDHOrsY>b87KM-2E{k&_j+6K(WAuKLrW9f{*#8OURXp&D8WGW9MgaysOv4DqY z78hZJN{NE&vz9>f+{qJSE`bvn1atkXCD1%@Fk~7L83Ip|uFqHk&7KkrL38;80LESa zW(hQR1t+D&yhI`?()CG8p!rf8WBE*i6jF)2>(ds%px4}q1eHihn5*NML?X{-Qpvpg zQI9jyu)pzXvlEUNpJV~dRqjnG%vWmWeGG}V}gid63!t62@pCF4EnCmTLR6dOA1^{z*vmUy8hh~Xx244$-s0< z$fQ!P&l&gs+e^yQ-p}qnYx_^P_>E7m-@SIj%7e?#cfA|@@3Z>(5#^;j8}2*K|7dw> z$@Q~~cX|F}tC}1}(PDzZnfUb+LX!;8XBKof8q5kowbIOns!gIIH~VA}TNFYwHVtEA zsl%y*ES~j;V(PHZ7i)dV&o^?7VJIT$+CcL7=zfSDwSlO&9~Sk`oFrn1gr0?LgK{Cr%+Hve`J7%rMuLR-e^a zos@fAj-sRq*)LD~F_3nI6B`SYuy`;&RwgC9SJY6VIEpn({vtB;MKC|(_caAi2lv;- z5E&)enO3hqEb7ZAiQ2r>K$!%b0HX-lyv5+L1dWFhgn5g(=m1a5{??0$=v0oi!Bhp0 z&^kOKc@QZ-ksZj1Mzfg9PVhv(QDJA%_(!v7{0X^Hs)1l=957xM>)qX7(n160gXG^N{m9a;%LOl!D6#r zY4<&H#w)OBKG#dB-ZGfex>3cL7FQn@^`(aV(-&xUte=@QMq|#K zPZfO~HAjxeb<)!yk#Q=N2D52~L-h8*k7Zi1nW&c^7WKdKev3s@{?^(KI9T-KvMJ@sgbLGD_IjF0>mo zO^5t!2?>;i*$Tw{u&96XBvG4>ClDBAU{Z`z;&mnJv(oBBw!y}v3EhYm!7-o4#TtT; z@Gj751kFwh({3y&j<`&&sS-gH1(6F2_?VrxrEru|NPAi%rET2*hu!$n%KGwK zm+xCnEU&xY=6>0^ht9p^+zZbw?LD;jlD!x1E$u$E`;y%k?k?>-wDXdk7w#-=KeYXl z?H7Xh01s`wWb1`nOPddEzGU--n@byS-MDWfv9Z4X*7f_=6YJ}1Z(X}@EwQ$~`qtI^ zRuil1D{loi{s}kk-gLdq^)eUk+FW|u(oa5o$l&^uhyVQkAOD9*;I`E}&aa+u@gF$m zO?c&s1h}ibpQc=YV~K~%GZ=$$D8ezAzykbx-m~X=)$>O^H?UE%NzJNja>i38gD+V z$%4b%9F-yBjDdf(JO;|q^W{$;=8`b%s4G5I}w zF=pV^4cA{-(8z6X8B*aCNuEKkGcNL8gJg!9l0$7_}hNf^%iTq zx&Mw_ngRT+zw3I)8gCxGj@+FA{LX7!e`<-hx)5vL|Gv)-U4LSYH`~vVdoh52_g}gG z*cxx{za!UU0RP~tTz_PZH_QLXEg8Vy$GF~XjW^5h$YmM8zvw4ie;8T4{iYK~e>45r zof*)aH+nwyG|P&uuFgQk+*e$GV2w8q{6}uk0RFa&>;JXJn+^BKMH;{dUgY|HYrMJR zkKCsL{1gA(^(Je)+3=5Cs{uS%!Tp{!-Yoeew`-tS=yjlN_xFrJoTR!i4gEii)e@DC%0sajiaQ%ig-mHH|oD>26 zf!}w%-WqS#zazei0RIc`alOtOZ`MCSU=wLHo@0%-3GaRBPqMk{?PRrOFU#=_|jrJ zpHGumY{ozNRoAas6Lb=R+2te)_Al2WSJO@gR4WH2l)4VMsxi?)_Al22r&+c;~$L6{XgP<(b7tI z#l8HF<)2?JFF)J;HTPefdk2UJSU&gcy|3;4#ojCS@_V=Let!4OyFa$e?mlVflRLk& zbJtFC=Z5Wn+0iG8|F0hjKr6SOxGFP`4L~E`7<%`ujv;@&A6U8L#FK|( zdv`%f$UH(BAY(&r2{I2$2FTcjTY}7Ekpb$hy8drV zka+?$K!vjF=PW_yQOf`U|H7ZO0v+@H0igGP{k-elmY@aFd;dTFz3Xo+K??-&{@?k$ z>s^+h1#No&hc zk;sh`7)#Isg`FL{Sb`Sl-t4T!5@eq7fb?buELNan=PL%tI90I(Es);qD8&-AKoGO@ z6HCwng`FLmSb`Qw&p0Wu1TE0=+3|=aXpzW`GZ9PB0@Iisgjj)&or8dWnw@@Ff)*(3 z?8w6sv_Ryua}G<;0_n{TH!ML51Tj0&u$l)Kurtmwtm-*-lwruuIKZ$3Ef`j2rxuo= z1%@#@s;~qt>LufR!VXs`q= zP@>uCf)(i4;R4Wz*^z=JXn{t|&Jip@3p8SOcwh-ypb@hZ153~XjhG!1Sb`R5gmE@t z30kBP#({t(Xpu%3rvR3qMH*pj{VhR@G{V^LTY-*k_YI9OHu;vIMH*r3>@7iyG{V@< zTY?s8gt3RW1TE4CW8-cKTBH%iZru{JpmS$ibW6~J&YkVcEkO%9cXkJAHD@fS$hi14 z{Qp;6zq7P^!{#rnJ`4Qu{h#Yx0zcNj{DQ+wsSd)hoF2`_l>R#iQu6BkUZvP9w0m-; zS8FLCTRG%|e2X%rIta94WYKmKJ9JjwLG066#^G76`^vD=>g#~@?YHbZA?PSm>Z!p^ z4E}nh1w?3Md$;S&WT49JRv)wg!0x=|;rKbmM>>O2qgI~f-9F3=9suQMjXumPf4k9& z*?$y3;$8z~eDWGog$KN$uu;RpRNqj+6V{d`7A5!F;n@}ucKoIv8GW_vV@ z@>N-l$MI`U6kUw4y!=qd7GLowgT6#@sRpgnRL2#mY=P zo#AHIbR!}9@bdF!)BMEcBsSQY4L82%cRJh{^Utwq*vWWhe84n(%)^yGLIcOVa$T); zfHlk}WP^xY(>smgztvPa+%LaXm~cq4tH$xE7JJVQ(WVbTDNiSHVMx-o4#ZcmS;2_Sl)lHU4E`%c`qxtoGeq%&hjG1 zEpLG>EwGMxxV=ZnI%YHZ5KND?wlS;R5-@no7`2@(e2*H1?{uRus<~EW4+R3oHfP~9 z(@FKl^;U|lPC9G@(lH?}MboWDQD++AhIX+R(t`b9qCJ98u#5yBG1id9{-9TxuPqvF zFkP!{z5kp@#OnnDx$~Ba3p~2$Il%P+}6u1kDa(*;WRsE5j%U^++@t zNCar`GAo+DSfe1BVHp|?v^C@+oKB~Ekx;$Cun;T`CON9ZXXwbZOOaGIHMwYWnlYLV zl-;=h$Cv+jX#?B%f%VU?|0Rgl*ItjUKVj{2Yk$7>^J~pDWbLNa&#b;>_2*VKa9e-l z%D=AER$j1j!}6z=|9It3SAOQ)`_8@Y+&$;`bI;uS*4}&e9^AWYFSGZI-EZvv?e1%K z@7!f}Z{7Ji$S3%#JChx1=V{wt-G0aRtG7qn>Fx7dU*38<@EI6v5nE5){L<#zHXqp3 zH_yUW<}54b(nvfZNpx%HoRi+*YAOLmKXaqEkAi+*A2 z3wDeC$JT$?E&BPb&mR^=SSilb*+i=iPh*|wdiw~4~_fOqR&}AH11Zf#!N*GCz~=)wShywNdjPY|IC2|PA&RxOaErK z=)W%gtKFiXUi!4%qMus&l-;7AT>7NlqMum$gx#Y5vh*)@i++6R<93Vw^U^=tE&5MO z|75r5$Cf^3x9CTgK5DnBDx5{{7P5+b#N`r4QLH z`oX0S+AaElr4JkwRR@zM(N0$dS;ORUdlEO9;bvqrVz+2`Gif7`k6cq+-d8p`3nCPYm0YO=lT$i7?{6xD&pRoJ{yG0+r{CK-X zZ(6>|ZqdgrKhAE^8<%f9D4MSI32CSdDPme|arlrnNno|KTC!WTxLUMZ^oLh}*ly9n zYQb*N7p=a?ZqYkd?>H!W>iYE_*FAQN-tD^EZqd73ciAoaV%Lj}`~Sx-y=LiLbg#Gj z`kjyOgtpsT4{m;J^NAac^6R$jLJXYT)U2V6~1@DcgB7k#O;S-Im5_Qs9p zgWTkD2<}$uscKa85U84jd_*skt<_>>KbKS@^=vIHXqw!Q)!HYVm`2TBA$H51M-yd@ zNy?m526vFyFavi@dTrd(E;Po9-1(9Q zcDI#ee#DVPb-<;DS^Ld^Kr@6Z&B((fr7O`iLDb59H3Ex)X#j)#m1;gS8f19SR2Ewt z(+tXu6MMt#DRfdZ%;nCD#9h7UA@ztW6$grq8IHI$TvLW-p0 zA&LkL`&J+qUP-E!yau@p@An-w@wlK9SO63^Lhh8Hx0 zyV8sb8xEmCfvu!s<;ehvP2wqpmc(Fr($mwqXa&!O!okqEFBDE{M#TIC?}TO?D|eod z(qvn!(`r87XqNIrzG}M}ktk?J=1MaLQw(Zy5X}y#bUZyINHN4{`P8sV7eV|Uq2z`1 zOf1X9L8P^l=$X9)?Up+~ZSyRwCPvLzOo*eYekpCg84=J7_DVClfk>4^{FQ8|lF?H| zI93Yce!oZzGbLiwqlrM9$YZRI2jioY=$ZW%?Up-FRiq+Xkw7>~9_==IjRbGM8DY>2 z=1MalqL!_)4Nrk?cr-8B>jgWlY8#rS>y*~YR*9_A^j53!N*F!K#3N=;%9EO5E_Yt) zXGONoF-o>y8uz9G4cl)<=-y|&l%6%?hK(hVJ-VzgqF?9RcwGv1BNT`*SjWg>bUKK` z{%%QNr7nfT`cw=kC(*MxiyzDQdwL#W;28mgP8|_?EK2tUI>X5xLX|TFmq%}vx=9=SJx$meM5U^xo){L1Y_aiV3#VEZq5!(f5 zLTYTwulu_xRZ^;K5=O~TYT%!!(=mQxZCESRo~@G^3uVBjsQ% zTmXyi9u48)%a(s{`K7KeF0;!|b$|BUE8TBC zS3egyx3>4*y;tvbz)j-L?uT|C+#Q~K=WcBGF*_gK`OTdd?+`nW-~Pn*?{5Ey>n+>N z_EWb$yY=R+mu=;?Zr%K%JGc3k%~!Z@-MnM-!p7IY&Hm4Bls9}EOY84kf8~08J+i*G z_TIHuuXWa5u(q@Mq16XhhpVyG$EsLYgKueR`4cwE{r|6s zZ@B{UF7sYrBpqneOt|Oe&|YC0E)RGCZzd+ae#oEd4MdqE{HRuycAhaq#5)PP64$xr zB-jM&;BJJRH1t8b9B!9jBEkn6p|%)8V@OVecb+z{qupzUnbt6<=f-I~7fgGnfm|N$ zh1wV^vveTDmc3{pSfAuZJG*m))(rL96gee5M3?q?E8%7%i3UIjVX!A*M$yDjEvR@G zo8*<9)j6wXs~|ISBr2mmmRCuw4JBZvm&Q5)3JC=1LV$^R8eAi;1$LI^h<=rp8qdHGt5%AWfk*HA<3t)8 z(NSUb-E%~zKhB}uQoj`sl)BwwPHrL{enKYdy=o>N!t|J!^#_nT4#8X89MMkHu}WD~ z6P29O^LC(mv=hrjs;a>AbyCHXbW52Qp}11k*J5)-bAnZ~Z4Vdh=UH(OnHH*Juyx7f zTDI9~rs`uoJqV?JNoibm?aUF4^q`yd4U}+xO!=8?ep2o?5jicS##I~-m6N=HV0F3b zFSK1hGe>Ccl{Iy&*#-IIa=ObuN7Tca95arG z2Q65u>l&Tbhy6sB3;OY{-Wp;>&`ISs8Ehx?^}ctx;sWVVqhoHE)GJ7%r?mK1rKY7T znj9%tds@urW#d{SH!TUV41sLSf7#FjxP_vmA8bzhg1v|L{yfPPV0t(zZnj>ObDW+vfu|=scTW!Ovpv5X&r{2=4LkdKD4j_Rx z=?g_B5%*^g5Ww9SBe7;n?zFlJg(oWgUaij&$$YC=Ot$b;Tmoi608$J{vYT;_gCuQMqcEEa3F71hRJQAI+wLH}VPOEKb z6mD-04iK5t2rbE81?(mVU3R1t=uoX4m&xulB&C~;iC;=|stOt!x$l@G!u_#IdkQ^& zS*OCm0V+*BzHz)W&f$s*_tbJW>x)DgOsC?@sW~DrQHx3)Zb6-jqz-T&SRsv)h>(D) z`81S};@oKD8|a9}kZbRpBYc`4El^difDOf3O(U}4fCDL@8r#UEBfexYsi9LXOhwVa z`rK%-X^XE0>Xj_4r5ajDMCB0_>B>@Ysv<%&4W~W1SgN9=ura*coYjF%rQW!d?_$+) zHVXntO_@I9E0C=;JxMhX6@wd@J`qTzk(96;K0svDDm0~`l!oafo$J>UQJw_x=<}#r z^i`Q|(^tazwhVZ#y?&0E6r%{GNZkU+BG~Fp@fPmy_|hMejh$;qoAwQdBU4c03|97GP~&#ds>9!BLUQg+}!t z^6jC6#8_{1lS&jeNQif@6YC1tbbePm#hvtE9)-3X#)Oqm_e( z_nVPXI+P0unO4G&H+sH)#m7&h5f3NHA{T1XepyPeq^N7_?s*;kaIGAyj7oT4$@a>u zA|u0kBgjpJP_&Zc&{mKiRzT`kUr})#v_p@?lm?QoOMzgz%_JpB?oRr_T4R9plmI?d z#abat>VdE*t^V1(j*i-HCX01oZ+wGQ$K5tNl*5{e7NBO23rID-+f8+X5L9g|Roko^$WWpt#8Btz&_nWEikuITf0 ziLMV0OvfN=;_?&bh-$su%|v9cj8*EMaFZ&l$!WiKG+U$;c0bfm~o6UL{U;g+UQQ|4OR%n3n64AOL-!#WX>Ul}>^$V4d*Ed#z z+7v}ZMFtt!pgF?+#%b+ibMweH`AT9GiK^wAR0x&x(=6t#Vu|6nSkKC#1m@}01tAmU z#_YkuhY!J=;uR(3>PT1Jb?p+u1^X2BRnzDD%m{jdi$IdHw|T*5hBVr zp%4LU@m{l)S6f80nI?j&+y~LS;hvlXkAPTL?Eq0DJxN~?QGGfZZ&Per;l*ecX4t%s z(dw-_PN~5L+KMCsEA0bBSm@`Hl}Hiqc>~E*70W^Fqz?Ngf)vgQ6b}adF496O81DMk z9Kq1T*pODa#;DqD>yV13>L4vaCPTmx1{$DhI$aAfQZ!c6T_2hwsD8Uz7)6u+AA4^e z=S)`Ci+87!?(~j;vdZSmqOb9-q^hzY3{+*Qs#L0y%2G*{3?Zo`wI-FSN>ZsHD5xWd zugvv#f3AQCUIp>0*N-S(MHm*rg-sY8MP*S|MY-ZtQLo;Ut~4{#(?cr53p3u&^k47W z{e8~!oO7P%oI2+`i);7Vd4hIuRZPnWEfe7WIVEJ8sceFmM=f;hZs%7N<)qM-6Ard$ zwqpoA#W<;s#A85V>WtAFR7F;Bg)s(HFZBEs1r|4?aaM*HDPyURiP@KjTZ4Q#y(!R&d)wQ*2`xqmvHGrV?U|^W09c(}&?{9~ClSUu}*A}&CV@dj{dSw z5uao$b!f)tW@4pnrG>oR?&m@I2xwZVz{CiS^EFwutqh5TUgJ~5rY<((p*Aw@C5Lp+ z?LsImPUBruV7Ow;GSbsJR8ttZBpkl@j3PfNC+kT>E4R|4UX6!GbWE#PD#L*^fzk-k zT_mZQ+s@5g-QBsx=Rqfw*|Z%eKxJ37kgZfQ#DbnG#GtP;S)|$>Ni=vKoTvI6b@+Kd z{{PimM{e|$kwfW=B$D9FvHHb4LiO5bo_0TzBBnYKa?H_bU#EX z554oS{ce9?+WoT_qw`q7G>~actl(D=76l@MQp;$>OT(g4>BW#U7aEB6Pg}-NaV4rK zswjdlSoWG=%`DjE)fSz?2f}?0i>lR3CYS0L`|22pOC_*WCalqzl9<|1ui}zc^@aq- zU6frE2qHU1|_xT`0}4af;7Y=xc>;f5KS0voQPB(zQk|Pz>N%*Fjj+_lIu9 zqJ8c~J1-k^^=#Moz+ zjeXe|`{hL6(?IW*BhOHSY8NDpU%c&LEQ=d3l2%5{*8dj!z4PiLF3Iwa62dGM*-jEw*dB z$yNp%l$CNa*U;lvuTev1!ez1*Lr&S4SuiTu;p7?y%7gORV3xh?`qHu^w?1oW*+2LQ z5Mv))T{pJudre}MxIZj=gID6qEql|Y;m*BkY0BSrNursrj+I}0%f4vkmk0vBejKF( zQqSCNM_ta}8iQT7eP0a`JgzJg*<8BCmOELtlOpRBumR1?(pSfNj=~GzIw6IWyMzMs zP$3U4Wa0UJ-(us}glad1wmhQVz}!jF9u_iCA&xUTdCkHt<7#hQFNzYW-71 zd*9wGBM;lt_cEZ?-|f4v-L-a~wR?Q@!pMi0UIeG{=ti;4_BG!_#CpiM2AoAD!86oKY~0V6>?rskQSmNbDeaI2|_!pgdx_U5^!I z6EU*G#KXTdx<@x&Qgbx^4QReGMIPROJ%@oJ^nsFBK|@_~PwU+Q9c@ahduGo{J39IAv?JW9`!x~u>+W*#7&n!-XKQsg8)MWIE{p0#PHg0;$CzFy45<%C#)(_#N^oPIHgNC_HSSkxCdc!f+`#k5!N z(POv8h+eN~)dg@j$HIsV9lhP>k;&!2m|K|E<}4Ci9;Xf91t;|~Y6>*g4dPd6f@PXXKU>agA%aD};8Xca}0oly)v zYMTiQK_;l!R!MHk$Lln259D+W^%^&DG$gd^Q<&|(<+L)a)=aB|bX(RTOJzoJHf{;{uxiIGn;6XEw8NQUUlU7O zNd)it$-1K$aVa^0EY7k_SYPPVLTf^}wTw(NX|!9ZxnbY2q@*RtNuwHvbt2mV%NKMVf(Q?NVN1A1$d+8utPd#OD#fREE7d^YMW>WBp$qLm-cC$pwfjHL=*8D)}Sz3wa;n%1MjU+?n>?!CYF zqZ?EFh)>ZGbsD%#b%t3UYFZeYS|p^oInJ~xAtO}CE}AaaYK^An<-<4m6k?-fyD^38 zPs`(6eY%L_6}gz5(30M@q|so67A7n?XvigfH|bLdFj=mp#8!&)fTK~LjORLo3NcLM zoS2kF+pFe_Xg$v2vb_BcpQ52-D4g>wrV%&2*}U72vm;6KvWPqB!+5(J&rR8>C{LVp z`28!27?E@xAsf%~ZAx_drjtw4F<$rP2Hz$`o(B#Gi%iR&VB6n#_TpTsb<=$=4p(xC z62~dCTE3-atU(SXnwAA}@{$dQ;hb)Ls7i#-qDszYgPEEm3vPVaO^v}iq^U@604k=L zg+#`p@MOaFY*vOs6Q81#1BNguw;@yj+mt%2)}f@x@Sf+;jTEoTB}}4IwPGU6Mf^9> zQgL2eOdY0OQx|ZxkckiYN_#YIG>0*zmqe@%>?ZkM7tu9&`&B*O$N31LZ5=5b4rt%#Z9{1Qd-I3LX6e2J*b*8sZPg8 zSggh=L!`>rq&?fG$hV7FL)0s5v+Bk(vx$=`4qBP{*roVEp)*KAb;s)A@mU_*`Gik_ z^$Fml!!r#Yv4kYm%#9kcSW+rkibWQ*m_X!oBY_M|B_CcXVrJpcULNvLYr+YV1=g*y zrp&pTy#RiB1|1(R%xq?m!H9ggcDftLV>Q4>?fnZm8!HBeaKa@=j9A6zLoYVyyXu&p zah4<|tpQqZ3>W!kS*ikWhvU@@GLx#kNEhcq#T*TVyu(lhs_bQ{Y^^fYlKOPysYKE< z${FOi>(9qDJ_Mgm*07r=kd!d%Nn=HC<=sL>DuN6BOutZt`n_7ZHgb-ijknIk?BBc+5)?ZjZ1tmR?0l zbxANST(k%nTyKgaiD0E~^9dB2E>Y^BSyO2zUr!6Bmm%g2mvq!D4&wQG8a2xBt}tpy zzQ-UlNDf>JH^+-%49*}>Rpu6os)CArg?wMk%j7V|V4W_MYMMK%4~BNX>=jiZ?uyxt z*iMdW?eYvqcrV6t!%;@*DuY~7G;AA!c71;{W&;Y&AE^x}Dn3QCIP~PEx2Q=Si7S(J ztzqPNwpGsYre{GzQ*?3&UYjHpC+cqnOheD0sj^k8XPR(kI9Ez|u4)uvW~Af&k<#Y! zq=jZAo^;c@{${?(r_$?v9buJ9OVazwjB`n$oJ>z#vP!2+u;kABXgQvRn;vWHVzQ|p z{E1JIwpt(qG?i+xv#OYbF^iF78rM<{M3dDzNDPgS__pbkE%WdVr?1#bY-Vjf4^O9^ zOs0jj>$!O$m6>3uo0|>@;4}%`l&VTBohvj6D z`)|MVw7-3I>x{d1QefSFm|c?#l#%0Q{Voo>4;4^P1@a2%f|s zu)Og}u?*CtoUG2$jcIz+UmW#zF7C|+^;f8D(oC9^ZeB+VN-I&6640bo*@f@GW2J`Xp zjeG8UaJn0eRcOTnA86f1eo=<(nVl{5&4Gq-vE+!%b1mCqk&!W@dy+y?!b3l>F@^sf z>du4nc%RS1r7DHetk-j;>X>t6h{f3iqBdI^o<#etZecdX#UzrF>e$0SvoVE#Hg`AR z;h#5MH|I`U?zEVy_OK#jkfhyoaG{Kv9%N%_IY~^irqwKHx*0C4qzX>qyYp?r!~Xzq zXOwGZ6IB|L3iS%ptm;`uA`As{tx_^J?%=3w+S6%GZ99klXUrxPH~Ib8fX6d^3d@*Q zXQ{a`p&MA4pJ%{LMAm_Ag(2bu-_BtIJk;r4Q>)7--@Y-0e?R7Kz$3Hb(EyjNb0gO+ z*t*t)s`FtopG|lj2wYnZ##GDf_8hZTa@h2~fA8vA9HhpvQ3K4YggiK4o!K4S-joUr4J*R<2M4xE;RzSBwXzj|YepW2w>RX&B< zw5zq!qE@75P1CH`AtevTl(b#|UY4F+T7YC2QZj?ghseW!aAOMJS<+P~14nQLBzQn! zJVqnsVRo1urDD*~Dd-hG;o*6w#ph)vz5kOwj{#YUr!mUltR7r(;)ChTW=jb=*{I~) zHey)WdBcgzAm+y0UCqY^6#up{kAL+kxPsc|xM`PdrgHeAPgLd;!z^W6Y_Z3|OgH9%yw}Y|!H&h%KC-)# zY6FUuR2xv7OO@nuXco@(Of%L1b#Bm(q9@xzb8Po0-D22`FlZ0sd?zm-)>r)qPOskRikn^5g78XnQ##U#o#k5P1c-mKVDqY0Un&gHP9g+WhnN zKRw0SWXzLwVv_7B#kkYVnl&O_D;NV6lQ}=`2>pb9ECTQl8GvlVYYa2KRo9s<+~OM#modbACTIp!ivzBIV55 z#UU*V8G4pG06fSvYJ$qA57mt+l#MB5pCaB-CWwa&SfesCTvbtoE|Dq_11jaTD=En) z937riSrUh~Ug}do@)(V~b(0>)Apy$IM%6{eFeIz1Ik`;2=nTr!OtUry5t@7N*_h%F z4x;bfI^0sX4v}!^{ulDX^$|5ohI*5#Kh=xrpx5=u8=Tvis%_# z70Xb$JPq1h2t$C+;&Ca#d#&Z)b0D1%QJ8iU$b`|bdAXhkw8a5am*?yPb4zu`Wwo)> zO|S~^<(Nq@mn`RGm%+|>B+ht{;5LS6$dwwd2np7ZU}nk7V?f0PzzejEYGMt@K#m1I z-dMB}Ri;lG<9c1;OF8hk1hC==U=tvMi{w4AmZKT5t15<+Eyr+%lIsKpk#SG~qS}Z% zn3AXC%3|2i%L~GA&FyDC0EcXGgN7_G$Lk<*iP#iJ3_MmuSeP`2jZ20Mx4pb7SFAi6 zw=G_@>2iwC^$XJmPXm|mM6OeBX*m!)Eu|*1xqZ_EZ^+8I8#QFr+F&T5l*f9dylD5} zD$hW^M&4b^@Qk=H+OfG^K@5<|$%7qZ|kryXRVl863ms>ll>*x$`=e z7$lk1L2}f{#6c{%OTfHHyC5k8*Eda|W%vVd$ZnWnj)`7YM7zA4C>KzM&9jbJzichT zh#ttj*;0CvH&YUtJ?fENLaTA$(WyFPvu)t^5J#j^7VXTz@{c{>hRiMBC@srQ%C)@i zlot(%HCU#WpJU*i=`t-_B(i#2PL0bcsnnSwtcFihx%r$#3&TpE8qhrkYsTvgJN1BJ zK6`5`G`KOJD-X8v;0WB?2t4o%^M)<|`AoH!Xe@g9bc)fdLn)59l5y!r#>j&@V{t%~ zpje_)0*4kf*>Qn`O-D?FTs<{9!SjuwE04P_Sj*B6ydh(6*jg6P(>hvd#Fb`OD^8_W zG1+ZGm#k&DCAFz^wgb~pk{Q*|wz+^Ciej)3p-~=4g$jHeWY5Zsix^nTp78)2vKzHy z=A`*x1Ok~y!jKu^(`pNiu{w9jkY!tC3xX_=yhbvyDlHM+dS{F#&|(uyPVgL{Zp467 zNvj(#11@k`H`4%w(UCJ$0HCF^NKEx2rZ z*e<6VYC6{AT2KSrIZTFDm1=94x&3rM{{L6Djvu!FbGDovb%mt{fvqZrd@xnh+zoMphy70YT3`8={< z+DKEd3QaeW)8;i0vp<_ztwAA!*Atzh)OU&_`3%27y3@-;r#2DrW+35((5YoA(6&7_ zKy^>>$GZkba*VClDh8J$x~7eJ%?WoQCH9g4gv0hgOeF|%x|jIE^u;8rORBpDMt(_v zWh3BS#89QXMM^CL3crexQt2dmuffRKYxCZKyk^dm%23g`l-d|Wk_?iX)bgx;b&jau z?HPv{khe>}&moKX5;RX>VElW9)W9%$)jC?yXM9~|C)~AJv!-8l3)k}T$~6qQx`GrA zrq>`Flnvf`$&OiPu!&n44g0`XzNpqbsEAk%yN3lT$c-$!4kD#qqTiZd+TF{g?JFB7 z(~YycOUeS@4|cSvAzrYfvFY~hwgGC?`wPMSU`bO46e$K)^!(tq`1B`$YTjpY>r2{p zuRrKajR|N20#*n5GMe}euCf-aYJsE6zpj3Dw|<{BcjwnXbJtYxe1mTch*?cCf^SCH6%I(r|;GBGAT<#CfTfB0ZH)47LCvBno(#a76mm8 zYTY%Hc>&z^=E`a8zK28?GH!`f8#)S)5h>wyiH6gO=gUJP1@g5KS9@GaPq595c1>b9 zmSbUh!W(v`D~yQ*JyHn1-x1~NBy|;~2i2~4?W#9vHgcds``PL}TCN2k#C*BJurXUDOz{s-e!1BINy*0bEyqC5?%B|p^sW;B@lJ~T{OKj;9>yYez2Q1Y%qB+*O{0EafSz-lmpYsZs%hv7 zWG1aS3h|xpjP8iDz=;kyrCz4ypt=r{`M0=((y)`RrHL-s$t z|JMB<+aK?f2fuvq5>QFtAt#?cdF#oK?LQ&<&(ZfsUl;xEXzTPPr@wr9N8}}u=j}v8 z9}T@JbZaCXc}V!v;kSl=EIbaA;V10;^UnKsUbpkzr_bBo+OF(=e)rwGuMQ~(cP!gH z{%2qEyPwaWy2FK@D6*86ok>kamRv{gjI@hImrnl3$IVK7cSLG^Z&+0elrGp8j&O&M zIm4~3eiXeqV0(}C+nWwL*Zk<9Qz0^&{s%Joc;@%$+_r8F*_Xfnh zXN6m*S04|E`&dBSM+4$M91!=XXSntH_lSVFZwQF{`hd8H2gE(h$4%B$lZ$@G8BScg zK0iJn?r{Ne-{|AYuG;O6?M9ZfYeg<=&o9{JN!!QKl_Ch*5lM!ufM;zXCtbiD&jRAi zfVfdW+%O={2#C`I;xr#8&@*jph*ghj&K7#rV=jzK_%#7>KOGSF>VUYP3W$4EK-?>R zTus5Lx!Wxc=b9!C&FaOg;K*+U#QjD<+?xX8emx-W*8<|+7!dcXXSnt2)g%4(nixAK z4Rzs_?1swEu;B%LIr;m5xc}wzUAw+K$8T@#cj9XQU5i`oziV-;{dX;Hwf~mKRc+Q# zbdZ|}FBC!I`HSOX`~6spvjXDA0dcGKYAxT@dbJj}TCdiwyS(*x}Ag&t_*9nMw z-qzOA8|lHH`#J*tIImp~{wW~t9|Pk4cR<`f1jK#8$92|y-~Z7Hx95@59k|M;m+Z+}j6r`W_nj zZ;_{m-xYoyIFC!Ae-3?L=yBUWwEaZTz}NIgx&DH^5O?jkzZG@XSBPFc!F=fj)w!a! zyMmqqh21ByneAdo24pR-6b)=dscNkNC1M_-#)d?q%gNgH?*y#iB`Z5HtGmATTxp3- z(=Zc?j5Q?zm760zU+ER+B^B3usXR64!gki`FjBL{yy)oq^8vS~-VC@|fw}qj()V^b zd@&d2l?|$S_pUFN;w(Eq4lIs;3wcE=qzJo|0ttgg#d@U*8H`j$ViYK8QYbX5n2e1V zPQ8Su({pNk-2-f%wPZ6|UnzSvo%eh-yAqpXl|ma7_UkWrffV{|U5QCkM&*rWh0f!6 zya>FkKwN02-B7O20n0ND8U|*07Yzd0?a16hW+5KbKa6YO@LnJpyPz!8@&b>i20Sw>GrVoVSRY+9d)>B|iZ_KR zib6nxX3HsCYLFh7+n3j%i$%|`Y*0wOcYV4PXF0W6U~yI&q~@rFlI)Ip3oIp}%)%%r zEy_q|XH;#b6D*||P`wL^jf~kKvWR9CM!D_)mS-9y2WEK}4H`|68P^gWP)(8Vqcg*frD6ui*Fj^Q zvN9NzT}#bmK$SKLceU#_V0ETJgTSorra^4BNha{IBH`^auEvXFxhs`KD6UCJw~*J+3OAYD;7H6eF$Vh`4Q+dFX zSsN5P)V1;)s!Yi|ogelMmUX&xJKpP&(882ol)i5I-R?&_>r1Y$ROrih`(h>cO={j> zNd&Jr52kE%K4s7QYRSHVJmc#_AkLXJwgZcE$$BpFT1usJSW2HLa?F-U7UgqU+)>Ji z(!hFJD_@ejrBQ=R0BbY=%QI_yZeW&ou|^}sCTn>!Yngqb5w}6EK3ZsuD&0lHoH0sG z>ajDJthPIr<7n4G`9R+qZw}1rZq`_{J&^C0l*^#<7HcH++(^t6V{K5Aw#sXxEIkFe z2w6zc$N>kYRy&{C=LF{V<<%0oaC*E_#726_q**7I zXc7mpWHi*ft}ew{TH|*F7H4IRLlm2(%MK?@`7Xtvagh2nRe(9QZ%qOBxij)={pMnv zlPL*wTRHExxV~0>AZvs#b~_s*IIhc18I;avC)N6FSkJVp>13avtU;X^4k=G)!pY89 zhsM`sAkLXa3xUPCM5Bw$q?oF~3)>=eEuq$hp4uoVqQ)CuDy`xT1kaFNsMN_Jf^vNT zSe|KgD=^EuXtZ9I(y-SYV3OYHcX=-}*L5ZZyCb!ka&%6DhZ9UH@(|3Ywd;Mr>TG{$ z24;0PjV5z$E?Ju5Y_)D``4S==HLdOskfqO^TURvgmBwb?OtXR}+)d3YDEfP_9q)u1iaCmKxm%EY3=! zvjwDUJ2+e=T+1Aq=$x5>MW}N=Wwo!C>>J24zWzKQ&Y3k<1B-KsHL{4+ML>-l z%5D`rSL}`5^4Mbr7{4HTV{bl6ahZHdEfT!!DA(J7<(V~dfmzlSAK&_y?GJDN;`WQS z-h2AN)1N){PD`hcKl%HUcb@$CiE;9*lZPIE;`mL+KX}|eP8>&$?l^kG(F=D*JJ`;{ zL!S!$&(QUw`q4KZ{`=tv4_|lqfTlagR=yrYk zsh3%hnFIeyQ1!|TL2IDYZ* zi-Usqk6(2BqTt}~KK^bnqThGPsD5pKwUU@?mWtxAXlYHbF_yKSP`EH9yQfb+eR5E6 z61>M6cijfed6g8SBkN=gQrh8eZs2YN`snX9W^`ZS{=UI zaf#`L!8knr%<*S}f)9>Aef;U5;QixI9e*k)IC}ia<4* zeEae3=iqe<%--<}k6#!Z{DR{b1O@LNf9LUc1_eispMU)Ppy2TF;&>4hymRawdqKgW z7S-@&$7}*#D#bKMD%o-@kqTOB1-R<>>y0 z_CFM~<-Pq6?td^Scz6E~_x~^`II{nN{SO2MhxgyV|Nfxho&ERizb_~_wEy1y_nuGS zx}n>D&;FM-;&m1nKK|hGmqunv?jHTp(JuuBM?i+)bMy;GzYr800v~*D zI3J949X$H^qn|%-Sn9Agr8b-If#GlGJ{(WghB9u&M2O-Iu~!J%j} znmq5{I+>$r6#W+P&APq)L{3y#n+1oDu1FNcaStOdPWH)3P-x`%I4CrHbQBc2b9fjO z8ag=mX7Cjrq+9#@=Oeo=gmN!hkJf{NccZmvEhsn=twyUs!Qm(u<${8DqLpYRC^!@? zN6Y7`vu-q^Y?M83xS$s8W}(opV;I)RYT5in)rtH<eYjz2+(%iMj;|I_G z9YZml$l+~B0BqM<%sb9;-(r0=+5igk4vMM+T zJ1}UxR^TO~6z?a}ay~cCS;Hw#;0x}6_4|<=O>hzT89Gqf#Wlj2XxR!e@5kz8PZ%ng zYPK{CQ%Bi^8QJ$KkQQnt#rjl~%Gq(Ut!LOY0UXUL{Q-@Enj;R>9+)Uo6I12^aaP&Y zfpeT5@5Fj)hHn{VrU)|Gq+`4|207n#sgObQ^BG}LlWA7?I-f;?#S&$@t61ZCrSIX{ zLMq=%^Ziy!Tu915H5$WuqD_)xUf%w-GYWgLVB%c`9>Z04l+24ox|wR#6GDq4X*4%1 ziZDIZy$oLD&5&PO$)0DLR(>&P7WBMCx5u!YYd6X|m8c9!dBRQyr7@SoXM?US?`S@c zNqS*bn;mIdYbEmwX*3k;qz&@h#ce37Vm!(A)CqxkF*m*A*UhmV5~-$Wxv&74adN|Q z2lOPL$#+e&n}%^J>#+PF(N++>lMcPm=V8}7%?WHy#z}VGPQ~0RjEbF}KtYJz%%E(% zp3_?~uC6qs=y&-P7MWV~hjj_db%|+u04bz2S*R_kOr>a0gQqI=B$E>btDE0`yiYM6 zdj(_2lr*MEc7^=BUY25Vt&pblOd;79#k$TEm616mk&s_*&Q>YX?PZb)qV6MI}V0Luzm$ z+e-I6Ci<)ug@GV3#VHpW1Hx(Z31?6dg-L5VElp$fQLmU08pU}Ni`N?A|Fxo^IlI`Q zx;>t3TIocs4)?SU#%tZ825S2>jN}-aW~$Aij_iHjrx2)YiPk~#3TZmaPC%_lQ01K0 zkdfDOU`VRhXswV=E>d#I+kZ4hE zRj>>u16(;L3(s(7!%-61`+J{)PKaF_OLhsGcDl0Tu#$|mcw;zD#g$YCo603Yoi?Y1 z5f}9fAK4U>t+f|$46bxWJSbu)V6{vSoiM|iG!!)~1qyp4(_L5K^qpt>Jg9sNZ6kGB z&XNT@HBZqKYhFpo&AtG4QW#;il7+*K-?b56N;pvE!O)zA??1 zkvIAj7%VPF3+zOmi3~vPGYAoB2h{ZjX*cB*k~PQeVm6H~X3m~pzY)~iP7LjOeD1)d zsaV20iK7?`F>OgHZ2z^sR0vCnJS~^?N+vrgR0`Ea756fAIVC!1ngjV62GHCqGIR;r z`eB~}wm>zXGMa-R8ih+eh_@$+9%Fm0sud&UP6p^s5p!&LKt=qDc6J=-^NcH(>mFQb zHVM-kFZxKsU@E|qx|u9-)tWZ2C$7rs`~G}bi=tGROi*BJ3#KdE?U_&YM@SL?IE)BrZcImnIO+fqmsyW6N0L?Vk3K6DJEJo6w~-#t!Rw#tlm(GCXIx@ zdqqLO>29;+jjd@b2f~w3&9Y=!=U7*dF?EA=Dq>17s0HKhyx6ChqDUs$8!T7pTXloqmdru z7Wst5vJ$IDeXHFddD^aDuFM*N{0VTslo~ed3Dctc?ogW&eA22@rFgENLqQVEa@s}U z0=KiG&}B-I&^f5v1ZGPX8d)JeMg>6u%8Oxh&Cq8YC)PV6k=}WX-w$~R*Ff@dMQ9S} ztW{yq8PsjLY2HGZf+r1{^=zh98W#pKwYwUt0alCg7Bj|T_9E}(voJQ!;@M&j8N)>o zES7=WeLi85i5Rr)+qu=LX>mQz4{{2hVql_g*^6wa*zS?-ITItYnSQdGsSv|1j_vwo z@T?YCN9ORbj%G%UIL07_N!LP9h~~tB+FXnbf@P3KD`^l|$p2Qd>b0E0Ot{L_HDI&?J{*g5LS!T-F8zI6D5%PuI31NuLdy+ z&QzHaU#aIODS7u%J_S3onYM!=UB*SJIzb>a7-kKtuKLD*bDC-Wl<)SiU~1~eU!wdA0iy=$HK9siH4w4woIUHpC zv+>@{ZHXm zo6Lb?t1Ya`;~fZUjai{AC)jco#O)qcpvAB?>c|Zu?Ts>%RvqA~B^XpKQ>=kSLgPh=+dJ`lPN*=bv^Gr@ z`aNQrD0?u-7uTF|4YMv$`2pWS+8HeE=GCUYyZUM;1RUo_^e`i(suRq^wXR&qqN+G^ zrv~`0Nz7_EUlf{19gD2y9M@uQJ_AbSLUA(JXtipCDV0m1eR?uzr+a$6L6*xErc-i(8SgM0_;Sou_==wkqY~pi!?fq?G{?-H1Bv;~m=B{!O0(bfdz~FG7Pxh-0-2va#CWPwfCBs zSDtiKt!T#u0;ZW_Dr2IgrE>`ht%{Sm!h%bS9x_7JZUq+Za|V9o>P)VIGaw^F)e2V} zdt#RbRcl5{VbXIIS>*E(b}>d)9T3tMvPaw^B=GzbQ9$TpupT(hR1JSar(xS%JOKgHj!B{YJU3J~=gS_aVgiHIZ_P{bF^d>Qz%l%^LGpC!KMzKXk7_d^Q6))F3~g zBtj|W^B~VRXym?Bw@83vC`jYqNBVl*!!*O{b63lFv(H?k2iLCYv;k^V^Nkk74M>nV zMPHC{S4$DOj&U^s<3&%UsVqEJTMMh3X=)XBtni&-z_nr7^`&J;ZY7qM{ezF#zOsS3 zZfx22x|Kushh=}oTluC-!-cK+VA@U=!mR!`DB)G+!3xliRag)C~o7JSUW_bK;)Jt9~SuQr1m%y%g9}RI{BvR~@ z65}A)3m?W|y-vx*5RvF{V{{EAZDJrdl{8MmHhWbiUA|(0(wxa6if6s~Vs4?J8hRt$ z#BD4scneJT;wb_Yj|}oneb)75{{LNktF-m;thZrF ze;{-A!KXc?Au}9x@_|J#$ zIeg8bdssSr;=wVKsbH_+dMr-g)!R4@Z6^qDP(``TFo@w?2M)`{~c0epje} zS`XbC`ptviKX~=Q^q_d~`2By_|K0sp?K}JA{x?SdKKib0X*<2Wyq@^d&$)#6{L7ah zm;B>f!W>8m&{RtO9_v;Jf~ZfdzSX&yS{4*4`1A^@8TD+DFBgiSWI%SJ7cR!sg#IA( z2SK5~ANu{E(BBLFo)6_gawFX^M{K7_S4SmXx<@&J&mMj@DD*Rjp9u>6^xM8zI_FiTZ?k5R0k^nF;H^_rF7wf>EvxEZwm^& z?c}zg(6^qvH7N8gCvOP~{q2+A_Mv=JV2bnnOvTw;ub7?E7b~5Fz9IAtL7@*1J$waS zyAb{L;a^9s?bs#D?t{O&Yy?&lyY7}VDn`YiP$4P=g|?!tpwMQt85GJ#`Jm87v~lu+ zE&j5R`O1E`5@7Al{kx&x4GMjC=-ok~?+U#uDD-zizY`St&d@u9Lf;X3#|o-ge5YFN z6%h4w@JpwM2Z7ZfUmq@d7ls2dd833Y-(pBH*wP-r{U z4hns4=($0mH-~OsLHUK&=vPSKNudqAdUr@)*ttXh5&Dmy(0>d4TTtj1LthLE{m0Nh z28Dhh^o5|%&xbxA6#93e^H1Hi-vf^eJfBcoL z`wvL!)oiUjaei+9b3vhhz5mxip`YDfoekHYw?4D~ndJfd@mrm()9*U@_mdZZGyn6! zson#pdH3KS52oNu?trtn1y26v?%(bX!QFr!{%lwUr+PW`$xt8M4oKkgpz~#>;f1ey zZn)p)Hc4lEWrJkA>vPb)@KrZ&EWietYJ&*yoQ(z8AVY8v0lwo*fDO_qZ;}!?hydco z0^Hy!5H=Rz22VljOn?nE`$kVeb7KK+@D%VH3vh#{pm8R^294h6DX4EOzzv>)+L-_w zr2D>dg9WIb39v!p?JFCkv0m@h^ukwh8w+ql-Kt*rs>;R!Y>-d!C z3WzfSHn8n0H&}qenE)F&@?F{B{d~QrzzbiM-&lYRB3gn7klR>*4Klk25g>aezy=P2 zS2plK2qFN!u>d!C3b2g@xWQBK>@xv2@PWCpR&cQWh%I*Owyo_S-hKP>)jj%(sI`3S z2e117a!~Z&^}$WR*(?3Y2Txvi@&eyCVDIz0f4h9I_h0e?R@^)B4I2;J=2{&`qI- zZGU?E&AZlaVfQh<55VDTj@YGFz}DX!Kl!fS2TPxW-FNQ3Vp~1_H*oLZUyH1-5dGA{ z)un5vG`Fp>Rnof)kD3$mSif+O7`Z+2;ca#6vLXW)?>Sb?*OsflBm5V;uh{zLi{zL7 zf7RyNQmH?3@)92k;<#tEmNpR^`HE2?$eD1VL!q~yza*@OzU}-aVLkM=(A$Fw@Yc}k zO?7>%Zwam5RM$g)JG6RJ71!6-4FB2rT&*j`d3*T7L7^WC|7lR@2g8376#9qZJAy(# z5dPz!(D#S`C@A!O;oE~k-y8nWIdom=(!UI^98tuz=JN6I$`NHf^kd!E)UUcG#+hyHnZ_42hI`e)(Q%h!77hr_Fvul3MB4X<9l)THhO1)dHmp_i)8}%HisTZv7(k$KgLe`kAd4UApRjt$w-c*1E#Vr;k7V zx)pS7E$)||JU=M(B`1rZ(C;W zC>0d?xG1@jb*;X>I{dRip+6P=Zz}=Teg}Opvf8uOe=>a_a=sC*RhJ(;dErXdngXKz z-EMI>*EDfxR=Eo+OXSUw)fa}i7WzLU=idTrp}!GX`B$xP^-YnLf0ej)3SJw2eb6a* zP55;|p+6n|xz%i~U51|+rGt*^uSeb-6#8qC-wFzSW8^o3%KD^eY1Qglb@_pl?>v6; zSI=cgTr0rKfXCAR%kR|U+P&a$rw>`p)>`N{o}LDUKKAq^DD*L>$3dZwK0OKwebnjU z3c7Zaf7;>7xo7=3^P3M>E<@{~-*PDX0<1skyg9U5v(`g@D|EhQt?ky;L*;n;&$gbm z_0wCY?>POQ)9mTq$?u>1z)A7s1eB)xFTltD;YS}l`q3l)=+TFNa`?)_=Nvxa;3EgG zJ?I`hW&acVuisbpzcu>#=&wfS(G;i#`0IQBy7%Q| zLF6IfGKd8Dy)|&LK6U4g51o-`Zuz>k zv=^D5)82H;i7$Bc-UYuHPkyK1k<+K#bj#`51}@gLy|aPPZfS?#3TJM4$l4xUEK_}F z502Lk{l$2`I|Vo!`lGeXFZSS^dFGbGwX_$RpVQuSOLX1HLFo%08+Qu6`_M-}^OpTp z1N--;bWrAA8`$9^N6V2vSlfY%^M7Xt_STI&l)6{`XCuG6mN|5xfpg}WTO#Xd@6q`? zX~S!2FOK{nL{o7Z$PgJF!pGiC{v z2pBwJW`fPb;$fO~91O(F0KqaaEHN-cun!pK(^b{0YQ6jF-ut?``$16sr}~x7@}2LT z^L^*bv+Q^6+PMojmi(&)Tse2wj@A7Y{r#>z`<3Ss{~%ZOtRmS$VF&j?L>efqnVMy3dMp zi60|)$@s^g&CVr$Y@e@)-~3Xf`z$|~_%VW4#ItjW9~<})@$_8c#|T~#PtGNNY~Zhm z-}>^C?z8w@;>QSH5s%I#e(WJ$5x@DR_qxxf=Mq0g@QQeNF7adQew{ej^Wa?K#|T~# z_s=8V`C#{Ss&JF@h`$fPE8^a{#E(t<)ylu@TYgpd*{?X4_%VW4#K-3nKlV+ph~NBD zwEOIrpG*80!7JjQJeT;fgE}JqZRZj{M(~pOw?6yCo;ZlIKIqPjc8(p?E8^G8k3VzI zW&YU5yn?^|RcZH`b1w7847_B%`^-KU_L%rf*l&Gi9TH zf8!mx`^-3(`C|rNGQZFNZ(aF`EARZCcZLuD?Ze;kP_x|sD zzwzG3@BaO}pT66@^LOt2;2rGt-?;sKx1Zkn-)?=+tzQiK`G3dFueCKQHKneRF;~wR#$ah- zH90QleLT+AS+Mk*el;iuwtypevl`E%)s|{i>?n!?7&1n!#1Y&`f8IJp+gt!G->}87ogm9~M~H;Yx=m~P^Iv>nw3iaXH=d&1zHM%Q(S^}6CN$9( zTIx7sSd0-Gn}-BrBw?~`(4ar4c2m_JdzvAErsdDS;S_Cs0d6ma_UkW<78=!Ko=J*r zhA_FjjiG3#C1NkhWda(reQ6u$MY)me5eB&Zx>K~93veq9&1G&4m~baVg;&L9+uYzS z8dp|m!HE*xWL>CStP~P=^yiN+jP_F8{=!qV>$lDA$4=3%E`YYTnssWiZMBVI!J&M< zgjXS~H{DKa$Ok;w^^t=U;V-HoXAagyEaGJt=~QBz#)o2V?0 zs}eK=EzC##))I<9YyB8l*_WT9jV^$;SYsQ_b}O{IHA8%pFIz+_4_8Zp^ptp7DD@)g zcEz6SQ{bNPWv6JT7eFi1Rv)3}u}_T^DuD;{XyS`51gAbWF*@yNHa5bE*Gbt3=Vsg}3KO5VyKX}QGb#k52kF_p8zoDR%r22H1INYLPP_|j9f!3EG_MG6_~ zXbPIiEjxNIOO-98uKTRO&ca-ltBEj%gP;#JC2)*C>lCejepTV)dMw8t)dk(J^+H^a zHDVUo1A{7Tdzq5ZV&^qBte4AOV3zvxFS#&U(7qcLTPD2;YJH2gS_>9#cddpD@Km%{ za@EHzd=ab7YzvI{ol~@v3vhcWv=2|wdKW+|`jHBJlwR_wgD*NV(9yIfdEI8slIUV$ zXwE|B$*Qq2AlnD0Xn(~8&>~D%SsBW36to6$FdQJTCp3ON*{Z(Xt%~UwZey7s>3ss| z?fp}<;|rj@LCbPU zqW#1L&|accPk(;v6s>!Hz1HJP6zl2FZ=RxcE`WA$@d7mc`HfSw_65*hqNqxLe*F}! zbpf=OC=}A4Upqx>UI6VSs&Dk?S5MIz7eIT7LY;m5|FJ87>dHGm^3FRCf9Jz*1>gOD z+5NwMU%B`9?tSOI2Y0_4%=G*9cdp+4&9|Xj|KqK1yZM)HhBtl&eAoZR>pyZGx%Owl zxBP#3H97eYC%@_B8vv3o&L8hnw{M2pH$1rV7+WmGVA!Rn5etGl(4aKw(zB3=;4($G z96jpi1DxITryIq8lrjRTtY#s}O&on*?^x^TqA~lXeZT8(@|U5v3TL0X1!(+IKx0c( zoAF>KL{3-Ov}-ff7^to2Gl=i3dZTqUkz~HpLnyM$9)VC>D#2+NyKX+4xEpIR^L0y@+H+}MX+nuueFHL-cS#|vIy3-ok|pXraLPyEVn^SbEmZ?y@$y&Ar&Es;`_fmE_)pIT0E=p|^5Lrx*!MLgs<|O)JA?uB`Ip)&7&C!n!cnM=f_=SN~6tTOL@2cWlC znM(w$7pC``%DnsSfX-fJE)l_An9h;PH2ZA=Dszdb_QIsztTONWEc5Pe&kI=gB|_l~ z^YT(xkRXYVLC@Dv)*qLP?<}F zzZWL;W|ev0$Fz4>dtSh@FA+0en3sdfe9xVCs}~EtMDTci0P;d--n`)B4{(5)K>7R_?n1_N{84xIZU%VS>+dC^ z>kIR8mj0%I-d=w%5q4jg-s}3C06Kg9y+j;-VLC_p8^74jOT^|ECiQ0hJ^siK23_rW z0n5HbK!0Ig4*I)~|F55X#g+Ttcl#@^|0wv&fBpj`0LGKZFUOO-RZ}{eH1>gG^@y{^ zlUzdi_d_u*qcx|$zl6r0J$UF6Uf`_3L+^Jm$Ky%9K;r?=VC0K2o&*OI&i50G&Kyqy zD~i$Q$_4BW7u)49MhPC~c8Ht`lM2gHkb$UA;*(MNGE7I{cdNy>c-5R z)1qi06deN!_^15b(Rp5Fwhiv*o~*Tg4?*8Q^;i z?5D=f*ONT)KWi)y26+DbjRnFxFoJxL-BF_o>kpsM3K3!)^(5J*8njtbv(l^kjKmb9+IWpz69_JiKWk8hpe$1nu#kBn2;aS%?ub-tgQ zyPsJH-tvpp0pW`frSN7U_(y#IuP^@7XRZ4b-K&2)^K1nKU;V=Oin2(2FCTk(;_Yd@ zj{7j*@AN?CuZbJtG$z#EI;G`o=+=`{VT-i@LDC0;oRhkX{^W zf8hK-o!uz70QConx{`_l4!kErZYl`1q{Q?25=NI<=1r}dHmzW8qfK0jhkK)f#?;u_ICZ!|Tjk?1OsnvyGnnh0QNCdd7L73v zr5ea*8JC>W7NxHXo}p~Gd^C|q_DJ^vMT+^QGnbnF(bxE!f9^dVIB0=5e9}PGtPY9Y zyd3U^ZJ}{=u1!Q~%MZvIyg)R2r$&#r+tIWO4K`R?#t3A<&)O)=QN~VPE<(>UI(~QA zW4qW)#j{5w)t^j_AnVhEtdc6vSRL_j|M$JeL$4|HDVByN2_~{;ZJQ2iq_YSdyoo7e zCX!;yVuUafdRy|b+?m8{nNT8Tau%`{m9>T=wTP)!>OT@}$!JJ% zE=iIAK6l&3=!l1FpLoT?#WSVf?q}y)zw;FjU<%%@&J{X$2ZP1Z+O0EER8})_btbfYjn`8iy5ReejuB+ZV84b^9%pc(&FmTD4Z^VhC8&= z>sLp6ar^7u-OvNwHQ5*VQFjl3Y6W;HkQp!4qCD1HjeeV2;7jdV#df z?Yal|0<+Z-#FmbFtVb4APR)nO*pbsX%dkf=LpGGs+<`=!48$B+~(X^~U0%^Lzm;!cI^j4jU@Vpsu zjl5)5yV(wAIyI_owMXlGqBYwXoC6uQ3hh#B0;^F>}yyBsd)J8>pjGQ7-Gik@RY|%;Elkv%N-k3b?C%cI% zYWcSFnC9JyI+%9pWpCM^q`pORQ@k+-olP$tj1Bj6Kl{X3NJmw00~$Sa*de zE@NTdvXQMpKJN6)J`7LS4$aCQ+l*JO_5^Mb8)O}0>CR#qv>#>_teTi62r;KD<0Ok zl2qgnl_?9Z;VNpAj8Vy$BO_u%jn`Qyf;9MnRSETD+UR#Jp;@*ug?AX>Lk2UFWOA<> z?eKxT%oceGiZD`@;G;+BCNx!dWDk_VPG-W1svPlf{k3CrMI=5R))RgenM+@b0&0`y z_-JReyP-9r3tZoXHc7!Aa8W^P5|lyFK~iG`g*Xi)mYB$TlsM~Y0h0Gt6H2URPP*wl zDpu>-42A_GnW+>L1vQ(=8^txn3coNrNcnSyccGt>8B2ULVw3jj3d`*xOB_D4291hZ9#C3Mujv};2Y!VJv9^C^6A?h8+?YoWHgQh)hv-MIlXmh>RGBu1LKH6%JH{hDI$UC48 z(xq{Js*oGX>Kep<$0}4TyEgb1M+w>?Y%2kPHw-w zd*Oe)m|m$vxttMdvB4{AEAkdDLJelQ@Z$OD}9q1oN(-+9yssuz%fa+>Pfv> z5zLE4B=a1oLP=JmLEkMruV0`vjoE_B4+ZS;HX}o37dI%%Clh1Las!@Q^Ia$)w&Rju zEp^-JF)e%6o^~EB=cXg-1J3Ws0oU{zJ3Ug#JHPT34`jXRwQQQf#-6^446ExkqR5WR zaUbMgg36F;BgD!yZglK1)K0-wpxx~y9^n@GHc)0%E0QF5-VKv@tKzCWOOVQ%VC*Bs z;)E?Z)0chGPxxRtNRD{8MZd-a$_HCC$Tpom)DCklf*3m31emRFlJ>R%9s!Eu6j9~+ zaZ4_`q9z)?tMwZ-Zj>-)G|8a z;Us;T^}gfy;cIEwolK zT*kg>!kw<#t7P^uT8JHPjJ14G4!Q)3LYfg$ZO2zxJeDUNNt6s9oO%j@`Hv)v7DtxF z1(rWbHnJ%m+r_nSevgOVl$}-+2u-xKJ6A_i=88JxtqFIFjHUw}^0hX&J6rKi;U7=t zrDQ;^$u`k~j$}N9yaPyPD+W(plYQu&c=jloexpk23#zA_JXLZ*+ zcl!6r#ZjUwvtkEtLQ9%9ZV%X+R6=Ci@LT`n0Ylp9IV&V5wc)7nYaDJ_5-Pu&sVftHsxI)^3G)k6~>%1~^N zAW(h1AN%^;*C*|X8(2taq)3+pLE_PJICrrex zhMZX`Ra?A17meq@?_|{4xsa&k zthB_)Wx-7C&Z0sxHp9JSJ%F7p*{BA+n)S4{Oa$<(Gk@J<{MYA6`S|1QM@h=k+wvF; zoO+BKk64Nu*~6t}7^!mR=|>XX0n@>{mNQUxE+WB0@BriYb_sADt%e&GV@|_Mfd>sW zUO)YQ^gJma_gMQ#N%?Jg490BvNV$+3S)EwY)?<0tM$YmW6k~dWHE2aa;W-&JJ#VR) z9eB7IZ{VGhYD)?{WqS_0EurvrUH)z7N%^>$+($~vZ_8tFT};lhwXvJiy5X`zdX6ah zSyCoJEJvy~)K^QEXG`7Edh^w&uM!08*>kLw4t1oQ{KI1YLNJHZVR zPu6z5-SxJ1;)I|^Rmefu_42m-v2zp|KJIMz5mICjro62ShM6gSDj$LH+sow4PpWFD zpB0iY6*}=vt4Ikq(e=o!r)=z5t9FZsbG1)SCg8Dxs|0o`p|87(A30CTAK}1n`_=-# zzN{vnk7ZgZ(Ywl|0O&E8r^oF1}l#3aN z0n5#ql13xDQr z#{RhL^%q=asIN)->V5EF`898rG=_hKqfc0VTX)HxyS56PR(4y1hv(!Vab>ONoZ&s_ z&ZgbLA$-y`QWt!NUe~to?*tlt3H$k=F|0W^&3mFSJM(2w5~y5{(b{E_bYTXAU^QNOc$0Xf5_tMFQ`cy_ z!(wDzzOK{Y6h66f<^G>PN7x@>R}DUkye(JVGi4!@k!c=d+HA#X{ixEQ=q$DNym8}% zAg#&q&5X5#N#IUhXFwuok>@F@2J*t`)djm@UVpm!*Fe(aH-}017wlc-(qWRZds|_W zXbvrL1|p>XFxGnbR)z;l;WVT=3X`Z{F9KY zr3znGuZ2lq+VRz&e~v=KA7SsYeQVx>%W)g+d7>?ELn}vtLSya-J4>OLL{YjR>uJ`X z6DB&V?PRkO2BDY~UOi?+IMX{c6^2~|dfiX|J#ZrayK^KBeZd0%OW*Pg@Gb6b1*O%_ zoxvjbPG)IY4-IWu6;b8)t@~ufkx5kQ{hAbexGj=|l?D ze_hgl4M_S&-t0QCFIZf9Y1g4)Z_Rb&^A`AM3VGaq<4itaV#hn{g+**yHq@3$RHD<( zq`l^5WjSGz!6fk~99EdfuGvi7Ol>Xe*IdVAAZGc^3XKuR%@008uPmtawmwO6buEwN zt;x8eYnUJt$V@}}tScU~%jq2LM@)`3NN(E{J}`SNiH7IfA+m(YRj@TgC-c#b^}0g; z&xiVd?8^OLd;34!__`D9^AhdI%IWVf(a+`V?f{o)DSK9TfU~Qin#xvVUTs3# z7B_3JX|$s>n?3JUSOD<>*?sKnS8=p+953#n{IXbqVn+b>?H>c1P?h`o!H=!NYPH`8 z@Pp4Tk=oZ6?kf^s77Y^M?;oG%uvp4f*haUuv5-E(qgaiLMK_nFP>Xt%~$PXHg) zp1@4oV!9wHT{hzwwr3Ro6g8Ck!xtA8vPZ2H8{xN422a-XlhNp@H0{(~!sddlXiTP- z#K^)MN+@U{Iqdu@ZFQkfS%t<=*ZyrRdHt;{F&v6pKpV|L{F--4gL~M zw7%d5f0l{8Zt$ON+m|bU#P|04#`4E~SC5Z?Z)hVJ{z1M$*~B8NGBH-KpKdaXRf@do z;ZKG135?OI4)Pc>w3#fs3-ZvKp-R9!k=1$`9*V>B5)^R9WwrX|P6vWceP|dUI1WCJ zd1ECY%e@_TbpvKgvJ9l8N(jtM=+Gsp z!6Cy7yUnyS3H|G7Y&0Cf?7njV%e8Ya}d&|D@X?<_mzwRqf-fqvgBg_84qZ~b} z@c8p>*`M=Kep%OW)ED859socM_wAO)Nw8Op^F8O@#6I_9{R~C`^(yv2SAfqt%=cz; zj=_E|hcpfoAJ}jNDT^n)(W?JMUc0@}U)22>F_1>2KH9}1#k32U~s1c7JlaC7@f3-vmtEKmL81!j_gpgVd^z8x$qhl zA5dCg&Yu=fSIvO`&~_IhPaJF>tO8x}bux@=Z@A-_m?~x6bqDm0l0X%w8?7eEs$*%D z&(cgM^}rydLC?u?)qhgVdWxMaIx(13!Yrz%2&!JJ^Tz4Y_x1k|wpWBJf9~YrZ-2Oa z_{$zXy#IIZ|F`$Q>z%)NU%dZw@BM>&|NXsZ_eS>~-Ti;={;|71c-Oi64R`*J=bLW-yW4*j%mtX<#%^D|^}pQuZMX7U-+b%#gWk=*dh>VOTweKeH-Fj9hd2KE zjo)`;cZ0j}<=6l2^&fxmW!Jy^x_tesul=KI|HHNKyQV+LH17fom&t7`h)F*FT3FL0emgI`Z$J$17VITq`c-9xwu;* z$%tAAqT2Ddtp*wvyOA+8=Fx1(-n()NkzlJ?;zW3-A9IDUExJ<)qA7QsNla^AZj8YK zqyY&ttUbf7e&ztt!V9@WY2gU31_3@>q}~2Z>r4_)7FC}T1#1Jb!x60H^~vJERa2Xp zIab8}&YRD~1zWbZ>RJX*yq!Uev0d=>f3f7Kq0m%!p##Kj!@%{rD^^&Dm2r1AD~I!V ziNq^~Y<0P@Q;YcPEu4gkr^ZvKibc(iYVc({C>_qPI!1Yx)6VP3+X&XcQ=8 zZqg(@zFJnC5hTHopxIf$G+3i9BHhsD#?|Wwh}FC>AkRPo!rR%nWG7ijpaoJ2>p5CG z(pq5zZlg!GjotVIFCe%Hrz4>2gOS8^+>$J0BJYMmm36yH+imr+5!%`8X2Y&_GCDvk zHj_bP`X~fDxRk8SqT_L{-Zqr5o>^Ej-qoO*u3LG8U?~WG(XAvjapj8Zj6u43TP^v}BM5a=Zk66DqhhLge8>9(TT#Mn(ubY65jcYfbt z1F6Cd`t$azwd=uEEpNRd*4Y4BmNQ)h!OpHz5r$6Yj!?G_B#3I;)~%LLk3w6sa!Ro( zT-(T1WztSdg}lIk$QT*4oalr;K(w1K!jqCLVQp%xF3g_Y0+}RmBA7C?mA90NheK*M z@D=Iq(-#n#=5Bh0>a;pUiovkr6I$8N+N;<`kzJsxrkQ{&pOuu|`L%~-wB=wav_i(Q zSB}!t<~lklJ(oZX4lXC_h7CtjTckI157hOXIz)Ul@a@r(Ee8k}_F=&uVhGjaX~djF z+Da9pA=z(pt+_8<`=1UFm`~xLFG#Vl$xi552^08*Qqfg1w;Nnt=WdJA`$>BRMOT9Z z1XL3aM0luTl=5tud(3DNjh2*SSYqR|W*ywa3K8wFL7FQgvcH3g8Z@@R z(@-_3HQDkTwv$#nZa$~gI@mVa%|CX40D~}1R!&2yhsP_sH_oxu&{}w!)nn!5CC%<{3<3eG?l_{I)NP3OC3-BbdvvHYm2$wQo2;tk&AR zJ0Ib|;-;AIF3VwNDGAK>x@}yX*W-9U3=kpp*tSrgLKbAvRiRGH6fnyY`5P~|%CL}{;qY$9 zISyK&k>m~e%ve?1rPJg-K?Gt5N-%LFBd>kt)M%r0gEC?^A1;$73n(bwNXS}327_p7 zBm_5H;-=n6OqeWd1X^95qe7o>y_%1H3WqehiPk-QeehV+%bk$3S6+s-%<_` zaoko!6=|_r76hHKJhUiKDl54|jgQkd>91P-#Yn=0opbW|1%%_b+FDDNvF>tSE!Tr; zBz2{_ilo_C0{;e=5|_{-&S_Wv?h6Q-XF!|RWJ!jb@tmC-Sk>hwqosre_{Q|Dem{^I zW5FxX?Vmb8L^~pbs&F-1F@wleR8^Ducr;<0Zf}ZJV>Gs;*sVc+414pZUqC3-)E;ga z*}<|V&b(0+1!V=ZIV6$Ra4UP5r)AOKVHDrAL9+#=-@sRYe*RzBaILPC_y!qGUH<0yIc&mSNH zqDC?ySxnMw395@74P46aa#$664Pk!A2!q1m_91j-oFdENmi~@5(IL!A$+W(O97JUZ&Zs?(^l?LvDAWpdhUjz}(yTSRQ1(xf<>(r?edP;IDbaD1ZzZEEPuUz6CcRxo`PGd-bP5sA&}AWXCxcl` z?INt-8wyG}?KViOTy8i=H`&0OZG-#_J6aw_r$hYIs)@}!EC_23?na=Tc7U2+~ zZ`w`Og^3viGc{*NSO3ES;^HH2Z~mLpg(nQP=~VVGTx)Qf9M+f{>*T8Hl7(w;>OSoE zCu4GhSyRL34kX%cH}t>_HPpF?WT0hK&5{1l(|B#Tu0sz@nD*M+*(z>jH~#f0L}a%c zZ3kt2rDQa?D5h%^sd-swG|&wuNhHBrFvPrG^hd_M@4ov^dPThQXHKsCo2y^<@PEDf z{nxbXKj-$pz5SWnzvb`UH)~##5>gHd$`P=XR zt(%LRzx38$y7}PY{PxL>zjpN>-}pT@>Kp8>?8eW&{?Cw7~g>XI42uz_{q@a!mw!rOR70!9L*-#}@J9&;!(oCA_+fEr> z`I`qCg6*xz+NvvmUW_Q2rnN;EaRb#`o1m8vF{jbh2vw#*3%YxVnZ0%v_&QGcG16_9 z7)#jQW-CrfajKcrMz%w6V=~ol981-$ue^GyBx7{0w3Iosa~Y(SEZgz0VUUbW+mSR2zw7A%y@R#wDOY0{24#_ozV&m+)ZvQu=M6<~;t{qqa&#dCt+^{E3T8Q-8=C_} zYi%hi&bM}*xDvchPzb>6*;dsfOm5*^Iue;P!$`2!8m_Koi8GisokqBDWOVZn|q!!>YrT}r#QB)ja6 zDE&^NT{|2eptaD{p(vLPSS~<_&UC^kdTSH*Os4Y4i2;FznI@QrxM{N6H%@7IDjjzg z3rE>CwvP{%eA`|HrRGy9BFn?jW~|m$?-D7wEN}eKfh%(~+*-4NRgaZsJ9gZDu%#y6 zj9&M2ZYm<6)szLsNH&dK-eV6m1|Wte_1UP;dFfE#wcTo$RPAEHiY;#HFuqV`%So_G z)Xu#_(CmWq9c@`4=cx&0N+h})*2~bGy{PLs>)?3MsCSy-syoRQd=bCoqG=tT*=%dDKjnWBn~3d^`ytv1C3g&B=8FF zINpu}3CFcM*IMlD?>f*3L1dn@W8MQ}lB-}`EwzNyhglwyvLKMV3EReP;6185y8Rue z5N^@MsvWBkOLQ~=@kKKoaEr;>#*qe%rfxIwdRae9wgaxde(<5=1l8Cb!-nP@^cw`} z|LkT?q;PB7o~FSdLL3TOjzI&NenXx}2d?75EYiy~pzEUc)DFWL`IK=M<1MyG#9l<} zJQ$MQPQrF|{lNhuQW0(!2eL@gQwfp&;{)X)GwQ(+P{ktl}Kd zzzZX9{)Gbsh+0P+F;87IrmCqSdhiw&`EIIA?KEp6lH083t&Hx%((QvLj!iF~EZRwD zA}nGgUxwQ`2Z}>WX*q~>uUQK#aux9BXB~kob~I`* zgBo5MULR4v(I zuC7-UHZ2A-{>2;Lc;NJPL?NJ#o(2m&9fZyXaTRYqk9CG-kl8{~M^NIGymR#r9BA}8 z26I`byTph5jIIYgw@C`3GXvfF+`wR$I0qzVti`E+^=};@xL#3$$JHrU(M2}iMBpKf z;n-BDZD|fCzC14Utbp`r!e4zM-8Qz&b}|gXt&Kq0O&w{G|YCyY|6+chf(6z%oX6(B*Eue#R zt`Xa=GAX%+Me+H%zeo!>k9HvMw`!sD%5$oF^??R>q>#sEx&Z@cHNPntVH39#&e&Sc zj@qb79}Ix6qW*FzSNC4{c6l@_TUb3Lys{~mLzw}C!DmUUg!?Q^Nc+~+crG&?gZWhBt8L%^^QbVeCAI+WJYJ>vio8hTSM=8l?)Sc@_z zsM$k>jhkq#-XGr^s>mjg~FM7a5t&bwrC=bp$y9E>fz*ci(#oV&2%y&;;I_YKzl!g z3gv1M&y3ZG8_p2NLU$tOl>D8;+p#By677XzFVwrC<1L9>w4x5yS({1R z@f_Z{=A0hqo{aR{(YWX=6``d>CRl;zh+66b@4MRMj&%) z=7M0f!x4BM$@QtXqBk{~4=ja};UyP@4-oxcXFx5}YUTx$Z$mA#wW~x0yfltF z>$U-&BMJ!D8jDxilRtid=m{)Yg0_<(jhBn*055$8Q6J zjK|GRf=a<-!B}+3t$=6{7}?B5fl5Osb(T<*g#1&?#sD9-wH`{*g;k0r6$qNXh zZD1S$ZVC-CrN*G#tPA=o@g^ra3iwzOi<53AP*>3$IvE@`5COT)^dt#jzSMndWe7Pz zjFEM|)5sjmI8I4}^MbC{P57UCN@Bc8X|yvVL1VUhG0hgbv%@u(n5$02a;!3!o4U!(QZaDU~3K}DppZ0UQzZS`YWeU@hOJ;#xFHRvm0ZHVITF4YS12@vt z*{*K4Y!kR7Qx-ov}hsyE_Cn3)indp z%P}URlV(4%HbOyr-b`KkT~5xJ7Tg+*=Hf!y&iWR6?>8LQp?M`xq_w+ShZUX?>p3?W zw+&`Y5==68p$a03h#?SC&2E4F0fJY@79$(MViqi@e7PplJl0}ir0AH{6MNolw;jMDq5_GRsQb;dSm4p(UFdvPplIgeQmJE8ljVltFYrTTo_}BmJVdcrtnMC^3 z-=xS6ueNE6Uk|lnTN4OxJ5sgm^QCGzYC=o5PYw`p;vz`0&Iw?0d~QP;JQ@UkO0~?M z=FBi_;08OLo!d$0-r*t9-7HWPWz^No_Mvvh5#*@n*=2_ac3l$HC41(q+j?y6it65< zJkZd%;Z9Q6L1gor1knOquffAB3qA=DEdw8#7}9BWX$!pT-Z{Mfdjx5;1Q%JXV8RFo z>w4*g2-#Z-LT+%e4iTUyBHNlr1GKvH6Q?xXP=tI)8af+71X|?DEIG!7v5*c6cU-T+ zlY+<+Eg4m9q?mGESR+1Gw@%9STl;5OXU zAi)i##@0AXaS0l!ownPzDT-VT$Ch8DnZd}?&3_MoFG=H&|F2%T|MhpeH-7R&eU1Eo z{_BU302rJ!+~+av2Pa)3Ip^)=F~Mi$F$wKud)WO5e&|yq;(?rR{5qiGf}-L`XR%J>S}d#ad6)jz}%p zEiy5Q4?n(T%D{b+MC>5f)H)1Uzg&~(fOBw~iM1>$pH%ovRcf@GKjBF^ zbQynjMq1++wN&VHsT{!e-uI1_1E0g-r1$wNKAX7_Cm;wzP-4GyV0q7yPpf;&`~LCCWlHJZn&qM0_gmf>wseMd90oss zkrIASbBs4&CSh_Yr#sd7Q;y2Z73l4E>Sd|jzPjS`m!0445Zyh6GqZ#PXHe#>{&W+t zP*_FaX?5sVu9+@ud9^DzxLOaVAlZ#?@5&FY?3fR^u(^D~a01K}O;Ay*ty&?Dg7KPO zdwBr5x5(n!FfF8dG9!5)+&SxQ7fyh%UT-U`l$}y%wFFZ{Q^!HO1athz3uWh@E*&U4 zD1cM;%Kk1WfP1TB?vJwb_fsIxDu6>f?^pJZU1l)p>8K3v+)hqi2fq|la|1WaCaXIhDN`_rc*s{=h;WTJuQSra7bixV3fU933vbm_pd_woP7 zul(^V@BE2(zV{vao$C*O>f!f2r<{fB*IJI&|%yUi7xT>2)NiH6sRI_z{nN`%9<5Ba>do5+Z`oF@rwR?x|_; zNv})dET_eJCsMmHNnJV(KIwHZB_>doCD9|~FPR3P^g2O~(FVmMM?36Grokt@&LCJE zMny?JLjJ|m;FDgb(3+rPK#=Z;&l}U=lU^6&<8T~&0d>S}^l9)(ucIXd<_(sN9qI4d zH29>~fsrN&DvnaP#~Lm<4L<30G{X_3s)(^8 zIL5;X4P%ci59Dd^Nv|Wp!$JcQ(4#9WX&QXe>qwL)7(vCwBWoLR8hq00V1xw`_ykD0 z{8-Hk)8La{2V*b?V_)ZkBoKnH29>~aT*5%x$~GEE>;;K50+V|7?ZcwR@2kcX$iC%sPR4Ozuyk~uQk zTs#dv>2)%WXa=fb;t{XAXc~Od>#&%nB^5!zkCcAlH29>~fvz5t;)+NeS&+YI8hq00 zFpOh)BEi!e%L6C>--Yw^mW8Kny?g81w&u5b<{z2=G_lAzA^WXIcv_l$_w}>hm4p?f}GDRttoTgn+lv9EM!Vc#z{&<@VWb@ z0w=!^OpS=fQzY0ues?Nxa)96|P6DZwNdlYu&Q##!&>-oI2F@vf>cHH;PX$g63OLAk z9#1HA+?cy}DsZwj6h(+LAmzG%%zb+bFzB89#-xmEtb{8vGk@|_;N&+7bPOS36&2w5 zr%eS;Ql;29BWj75keEMdDsXbD(-A$+;b7RL=l2E|^e6Sy2?qzSiV+Hl$trXDl;Q$U zII~bS3Cs{ICc^W3rUEDZjUx?(qWPErT(uws5d+5>gdorFoC=(5MDQ>nsmL)&Pt5O_3Y=W2 z=wMr*Dd6ZM zGbDo~7)D{HmG^|LF-u~Gk>F^an4g;poNNpgyxofFY8)}$!hC+r!)TR@5AWRS{n?l)6`leH>y3N8@>nBeCAb1Lu&l>@q=&M_R7nEUlq z;AG!Wm86J^Mx4dwel-<1S*r#uVU#ZLBscfVsldrjCD9y1qPT#ubB|61PBuqe)J0iI z#tdcdk*UDR-Uw7aaGjLEBh9&op@kQp`eZAy$x7`HhQk3pt*GE+e?n-4=5bw-;&Z>4 z3Y_c~F;RjggEDw&?&nj1lMRSya54czg&275XH$Wb?ZA>!!jMQEP0amtDsZwoDO$l4 z#6Ypw+(Xlv$x#=-u<7oaQi}Q z;qsxui+!J_zY$Vt{oEy!k15E zHM3f7<)bY^)DfWBC}=v+S_st3KqbZ$SrVAVMrN@Y%nqs@TU3LTR_qKy@6#CMo&sm`Hn<))yYbbtbTx@^VPC4@LIN5 z?{*`e8YJi9P&LSkxX;l2LO54}g2{xu78{w3&0w}J6zv#M1&=_npazA6piiRXQ5GZ{ z4<|c=fgyyWx{m|TbbDEO4H=n5W-vR@S=5$G?JU6q_BqeWtQ5wRaMdFv!DCP&Y7i1uAJnO!#@I=QvpgKg*%3B@P_Af#y1i&*_M#Ono2L7Q z&4c78ML15=x&CC6@h80ok=r2dF{fH z*$ZbdD~3fNhs3l54=zGw!s8sjI; zaLtp|yLgenA|4-_>L%M=3L~W;k+%H)IE({P83C*p*Iqa>`@$K_8bD0XE<_oMva;=B z1l9t>0Pgdfc|+`VMA5s$0oA*IME2C8E@ecOdOIZ2lVAY-Ci&pxgXE579!Vx|xM_VlZh{w-k|ppS z6Fg&8BJx^zWHvm5*?J_wnoKc7qf*TnT{%D5Wc{$+O;J_403O$OLp`-5pxzKCSG{DUjY~jjEy0oz+=sw*z&0>4 z8<@eYrwBw!vZ;0(L704}&l#4^ z7>#hc7R(M>HpruGlF3jw80xo`6fg^o%tAAm1&_PyRU%!&T6wu56#LD((9MO*#aPN$ zZAk&$lABGs5$v0VfV}1%nf0y=>#_hB9%@sAlmc;X{8EWBP~n9 zy*|$%Zh5M4Uq&d%YX?SV56oazGm`a6-B&EY$*dkMBx@?h=cqnoA>~{cBl8)5E7nXW zvPlih4xW)&&kSZeL)=%De4Nl>8I0*t2`$8z4Z54>(tIJTWV}9GL{%Qk6@i1DKQep% z3}#DsINoBwlmA4Gl6fGmn8{JXv?_6VvdKIhn8#CXfi{OKh_aikT=Zl+2}`Xcc^X3yh|(Nk%|SW^2FB-)%szhx zvsr{C&|!h~s-Zd#l}KI1i9`!84lQnoc&gxucm@I)BQiBV7!%GLnLTd?v(AI>UaZ_m zmFsy^4>A>hx6XHwRMQWe!C^9&Ne#kKQtt$VV7xtdWcJ({%r*m&d;;;CYRD9U;u4Hz zf~4QCQC*-}+iP)DpiUXMV3R=%OghgSnSI_2X06#J$mF$iMrO}h(c7vflq9|YWJ~+4 zp~AtFO;%#iJl4{(y@(PjTE%*;9t~u8j12M#ZxvDN65(li3j)moA-uJ-M`q8S!ECJ) z=n9%)`=fo;40bCmPuNp2+btSSbH#j(14=*9D1#eW1XRN6$n5G2X4zPh@F9(27D)j$ z6o_gRGFis(LZxgVqeMc5YDzIP=|~AH00(>S$n0}xFzcf%H7`^=DX!uvdlRizuu;p` zeM6?imTM|d-z-$U`8;NZV_^BQZ)A4g3}yqhl2_>z@+uK)MPwXIP<=1oAut-NniZC{ zGjs~dU$gMY)|bwG8T{dtpI4mpitRftd1ZFxOzcUn-}|GJM>2mn85$G86PAP< z6T*oh!cjFUkSOKKVnq+g#PO25qF6G=D4{_kA(yIQvJPbAuuNqrbr}pv7TT82=b?Qe zQ|G9BuYUCUE#wOy@PhwAyxFRpwf`*EY+rBzY!7PL3oamJnLp?~5D3gw-|$uXRL~~d zHfxi~qo6;eHaT%wGg;ik1Gb=0AW5n2r^-l=(}d%sfd!9mv_4^!1i8<(VYN0;XgF%+ z2EBv_D3e=Kz!PujZKWR?f{ay1?=;WcCQt*k$--uBGI@~r2h}DgDusF+(n!BpQcLxa zpoxCDR0*W9>~Y!z^;MD{4G13gf*=P+EyKgAKZtD z_{?o`+P6TPY~8F)o^VEs!hPM>@9I9>n@&bMWPJ2{I~|V>b7(!lp>Qu&fb(St^>kmQOvZDSj0HzY6m!0PsJl*=oCuz>;SfU}ZolfFNhNxI-^|<|HkZ+;oawQIg0aM+2 z)vH%1kwHoj(35TVD6J3q8@Xi2lSn6e^`lQ3W^I!@A0$DW%x%&pPk8+B2h=73?nI>( z2WdkLFV<745if1E(}{8eleOcu2~x3bDO+p&nr%P`IVcs z$z&w;52;Pq6P2wTtypO<)dmSps;XZF3a-&|x~3ebO=5%zPHantU0@pn*jwmkgiM)= zcmnZYJD~TPo_4rk)(T;=7!vA7Uo_3!CV%=5piK^K)+Uod>OZJ9IZ^8_Jc?(%y-uPM z=_jCUKu<#8*8TDN3G7dH;xd%)R>7$>SF{r>$dWdcOI{XDLAkoea+d$&HA%UzJ$LS}C2ba-oLML14 za*Rs%&}zn?M!HZ0_w#AKl*rX<~HG9<9Pwz1#nByS(k8ZK;Ka7hbb)>edf# z-3MOtGjrdZYj61hFyiLNT4T1)3;tzuafbL}^?;)rbKvo^aH}ptj#uqoqd{TMyTn|a zA?mnM*jUHu_|gLY=;Z!j)kUZ>yGA)|z^Bg;tlVHT7VTMe5xK0dk)V`|Kq)iCE;mYf zVm{ne7eUR!8Uac?2uhqGthrI*kp!Ujs*5?lc#0 zo*Ckw8--1Gp2>~Os*9j$b`5jbfKQ(xY`Vc_!+Bi9Q0r?bD8+G}8KS5gr5wk3T!dE( zYcWuw<2*A2ST{<1ob$MdxLRunD9Lf28Dg#*B^~2DF2bYq;tnkepq6 zfx`xT`V67u4K^Fj<07tHU%LR5;yBL?k>!n2j^jKoLe7P?C@9f!o*9D98znx@d0a%H zt+fa!$#I?;;?Nr<9pgMMLes!`!cKDm=b0foy;0b7=b7BYuDS?mXV*dw8}R8f1hqHV zY&egL2zPxg2ug9BXNDN}Mk&W}9v8vy!dd{7=s3>|;qQ$SALl$S;^EesAC%-c&kPaq zjgpRW9v4A!;5=$3<+vzIFhxak|zF(ftiJ$LU%w!v2Ld4=B;; zS~CRxH%fe5*K%KD1}ova$5BrIUB(cl`}e_-pU@(vE$Lt?mE3egC$r7yf-Axb;=@_sw58 z_m(X`1AvL2t5(U) zKHheR-aGFb{rdeUBRRQ{y6XJ(nnUuUqa?Sjx>&((kbL5ByE)%&4%4?i-}&|J@0*$L z>U!1TdrW6{u@2tgdvvhgoad;s-vG33-f-uQPa-nmk36AX&sncHJdf$@F4oW+JRd*Q zZ_aeo+5f!jFn!C*X0Fk**2@mlV>-Kw<@FIat!B*D<{U?z{mnpd>J4|k`C~J4JY&5y z+1XvJ%{M4+boNaz4LjVftBijA)XWT5)-9(NkLm1O50YuR5yLE8hJrSvwVk{?;c}&d zm%24!2(u|kk_LHS7&}snFh4{JR3ZZ>Fk>_9d-6stUF9!`S&w3J!{=`m>$#FU2KPrxap{~Z_07h*{|B^aJ=gE zGjlv+JvZ6eUEEvVpt#Z5H>K!syW%0|*F%Mw8J@eIb!stubkf|h+Doy;G!g~(=;a#I zNeSU<0pz+xYMRz6!QSQ|J=7q%-Wuf8dins!m1rL5G!EpgO2up)aB>iBez|ss!?Jg> z^XqWSlX0C~kzaN0dgcjSsl4K6JZh|)>&bn;$J4~1P&7G&bHNg$wON}8CWFl)Kdkn% z|0Ax;BgS8ynd@2W=?U*#bhB?g);ou`#urI`XlfeM?WBi8VdAC1qQkB|aDMHaV(b~e3 z@qL1iuC8C|@I7Xncd?1y;QP37ep8+!hQ;%pdbJ*$nddp{mpD9+8RuPWz&CgvGtO_y zbTrPV|IJ~VyK!cwXRW_v!b|7f?9E+z>1dqS|Ha|>lBHQWI{W|gw|r#F-uLWf_dL4i zhCQ+6A1@zXjx61~^y($g?z?thwtL^M+jli~ox1bWJI$R-J3hR_*fFV*SazrOYItiXpi=Chbp7coD( zm^9ysu$+dlk^GvD{zZ+_uJ)vtW<8`r)2&d*-;Qg!{Zkzp4B zzOmtjw=UiI%y(Y%caJ>n((@h=cUfQm(#}Wv_uh5Ur-|>s{C9tPT~u8kjtsj9)r}1g z_WWk|8&B%L{hlw~7yBY|RrFQ=X+Qr#PyN!B+9&USUsd`~b$u{0>>@fhHvG)%i`RGG zdGg1<_QoB}TV8pv_lcF=|NZXwA2{tb7u@pvo0`Kt>Uw`<*dFS5M2~tSrY&jFhLn_h zsjx4SvIbFRsKHptBCGEB2z6rYt*y7vEjd8 zc+0b1Xk7Fy?{~g><@KLC1^!{_-fNcs?mr@1e{s?KuYT5Fd|6%ZjtsZG3LNKje>5f~ znMNa-^Yk0w>80OK@P2<0<(0HhwbTAqKO7hv{y4Vfv>%2aS$)%ix2U}bt>#+q9{X3{ zKItpMyUzLG`+xF+O+Y#oLQW zl-%&Nl5naa*uHGF6G>7)BESTRU`wP=3Sv?(F_|>gP9pI_yxce9H1E?`D9Z}5U^bSIhf+0PQtBDejSh0>)%*9|x#N_V z;V-kGT;|$u*qz2*^FR2^+fKjr{%_=NE8L^5S4W12iL#a*1~{mmRlo~*9$Y>vMRkNg zdx54HvV5&hgf7tCPADdf8GrfqQ|$}){Oi@Hoc;2z{N>r-d-P@Ty?dS?zxiVy`01{f zz4xI9R@L>&$S|sUGjdZX>3RxcioO_ls2@fZ%e4AKe~zH+L7vxAX+cSNH^z$POHOCs z_1yHej~;l=%fI{PPc9QTz3$n+YPTx4Km2e1{jO8|ado{st_rD_B2lZ>FZ<96+b?A6EtR zjPe^ZvCH%F0|9zjh;NR~*?ZfkTaP4=zm)-g%b=?{nc5!!MZ1|bq_|#`U z{iYwGh0|MS-}>T9zy8Aie)#*>ciMk*{eS%XOLI$?-mb0}M}}P-sgDi6?w5D}*+-)v zy7$&ww?uyV{-3?@&o92=ns*SFg#K&g9luF`thz&8FN_SkI0GLWzT}NJzv&Hs@v?0< zWUqhbjl$mc4fHk7dd`E$nf2jk|LUF3)@^k?KQip%hQZkIC!y}2d)AQe(I@mOj@#NnJNbhFu&qj}5>6(tmv4rysiQ_S>09+Sh$y&-cE& z@Pex_{iL(zZ+MWo?ed3DR@ZYQ!!C}8$A*7gED=N<ailvo{G0psp88+>c@KX5rQI`vkwdNT?NgO&&%GGF>y5wq z@aI2NepFr0j10RtdmS5YJUzZQvV7;IH=n9~F)hS>A57oz+z&7Q=&F|9T|3U zwmCNZ-fPeFKIhxX_Z+_M-7j1IG;wd|_P>4R?VtVdYrgw({TaJ=e4kO*QzOGJP9(>M zukrb>e)s)7?+ffd^IxYW_oe1P^~zhDqqf=R3ataNzo96p1U}`_+YAp{J|sFButjaY#2deA7b*;cn&Ldz8!G`h!~^ z`rR9UQa*S4+N=Kaj?doww~u^KZ#-LFfAPq$i&M3+;lSBf-~A`)#+i3)_mJ=2evjwI z;2Z8ZUAp7#Z+-NP>aEW`d7HX!j10Rt1REQ^`}#LtvGdSxe{|^2V*77>OX1KPK77ER zyD@pYy7MDf{QQgu&r#R)kzp4{S7XE9?LQ(uaHiaN@6Vy!>mO`i_?^$4OWt(d*D94; zAMM=v$Q{?H>)Ob$ivy>z;eU8IT|5&*K6n3tpZ?23#Z%9|xNynvdt0dwm4lD6ul+^G zP}h?q!!AyT#)i*+G<@&}$lu=c^e6;V7M=PJ{UH?y)eywum8$WT&(;t58sp`5q zGR$NPJdoRv>tsm}HX|%haVX)5badFkV1F~C2zrwxVSA_rHWnM6sQViWZ1>|%SPi}#C|k(n7sEjueyu;mipR% z{_^Rc`_-wxy#3z)bLn5Nzw_y5eo9@JM}}P-nv4xMwg0;2?VovwPQT*E_dNUC%F^{*FCs$Uv$fDp-bPjr+dZoe)_HF-=(ZIUi*oqcZG@U za~@IGg^^(wf&pX0t2@7c;N@@lvs+KS;@WFI_tmRDci(}pTv0upO`Ue%e?IUhfiJ&* zr2oJ5hg-+_bq*3>GCCN>9pNH z-2IW=FW)WfK5N%QyFRt+wY#)k=j?oB=Vy1W@4R&9`8$5I1{le`_+rGK&=55_==(dx=iNOaJE?=M)P6LJi@#l{<@W&eXU#tOW zt2w_~(c4`yG3<0=LeJAs`l1=;dwm&e){AzCN<%&~p;np&*yjl>r8f|HA(l|1L8Un~g* ze(oCavoS)nnsJT5QUkr!tJ;x3LrCacCgR1~ra6puqNat|ev+md^=I7Z8gYYb#9Lh> z-ZDn8RhU6INcTp%xEf_rMONt4CDWobelYa-BK43Q71EK)(0j@O*9ebm#Q9@HB3elK zFq{!RV0)M zdo{62Gte*vL|-GmYBEogo__u5kZXk3HR7Oa#76CsIOT2n^(+rJlU&0u3YDxEPcUK{ z79v;?vC=6m%lp`Y2iy9O<1*q^q*!cKY5~%lY=PW*ELsBjMk)xz5thkDjr^w7$iR49t^Lw*o0Gcqik^a0n1_q#^CZ;Xi2azE=C_C2Lc zyV7o!I<%Q&GpY}R6OFWA03q8<0bHqSp~|*@a*g8nXVCMj1iD+l_e0Y3TxF& zz>14vGSmztePpRr#3N$E6W92v*b~}_a>~10Bi`v6@s15dnrX*_^^}O@@@5MNM-SU- z5I2}kKN#|*!jNjEN;#obZ58(Zbd2!Gel;i-1Erk56HfMYo6ZW6wi*ifdc&|@D0MO* z?_t;1Z(zG#H$s#Le%kI8JgK^kYY>!#eAR+iCF6)hHpL;sVRC`4i%gyGcy|R|BLc1w z{xPB-7Lxr?k4u!ok%0#en`lpJ_9}8I+$Wf7q}6HG1_+ZXqxHF*Yed#HA~QyG%POCa zR;nggNQzmnHt6U3Rj=BOk^NMS%(n+Eq$;ObCDz;h;tho0BSSjV=Ce=-57okvwifL( zHKbegMR>DaQ3jkp9FOBndGXDz5pQz#|7UD@?Utnn=JtS`e77u|v-KTY&sfUsy?ps+ zTVA{SzTGd`b@wi1=bd|gyBv1)Y1VSJf z+f;GQlT5IXMXRj0&-M{MXo^Uc%5sSnepqkTS+Q?oSkqz#s-#X_McUmx%AsusAVCjR z+8!TKtucrk(IXjpsHPiqKG9^r5Z0$-vK+9>tA!X=$-`KXs|||P5)t##NE4!JCBvw* z7F;d0Z7&|HOWHsvtYlb8ljL-cOBBhL-bf6I2@c!8c7%f^qh?t2)H6M)fcA=FC7PAk z7Nk&SFw+3Bz=Y{bGW4KH%e~bQgyLn1WEz90m^4Ch)15DoAQ!+A6)zi>BDN1z_;!V(3OY&#cKqjts`!HU}X&s`FA}ZRj4S8sPR`+$Aj1@tPo?353Y9)jQn!rVj z784lLdxFgK;&UFyAq|G_h~8CG<#>e21u;KEMU+M)+p{dV1tgJ{%NQIG1gOF}oSrpT2RRg}bGH3@Q!SIA-q)#M2d3x9Q#L$6=vVH~#W z24|yDX`m)>CPqX}$&#y0yOJ;CC4VA?W}(1p*lyLbMPHf@g(8sNDSGK)k82gf6*f~U zR(;)S5AD&7It%q9Gx+3>k8l&jE?OZ5GMrDj9Tav1Mg z<<(}nKo+yXK3wmmqrQT!#B`_;)b(Pnr@(N(Q~~)L1Vc_6@yJRZtn73=i4XeKT!V(o zK;5!Z&!#KIy4}7Y67hPs#*T966#G$|NNVMz&km`O-v;Z}Zl}=Fur7rdyg$OkQf7)u z4GOEBM7i21fyjA81k*AqWm-)dYZ|SLp~Uc^9jfqXIfQm}zb&kk@*n^lXVHGS+vNLr zKZ#6m*z&Q*aR?9ifh6U)R6H}ZaMEtsaV2Nv#BdC&*3e;K$TthJA~cj$-Yl?q2dWKl zqu!JGa+wskVnDVB4Lw9l5|WXJMsm>4<$d0jNY0!MZsr6Fp=cqfXi%%z64<6mRro;?TdmcSQdcZC`muBb$D({7rnUxw zPKJX$WFr@m&cmr2y1Io8Z zQdN6a3&U1o0?i0YL(eT74kG4J{-XK%)7~dotibvwOg0L#!ULN6U4BD*J zA&*)?b0Sn|dpd&0U$D}if*xTLMF=hR6njNNutuw5Se=T6vr?X|4ktLw5l@K&Q}PjQ zMTGq2cr2X=mT7|Y(Kgf_RW>RH5 z1#Yl~nx#QHkfH}hP*Jc@i^Mcxl@MZLhw|8rF=)kdY>Xp1LDSBqElE>-*-*TkRAXj0 z%COzUO0H(81BqhdY+Ipish~~H7xNEYH8)zr%&l5PUJ3Hq-~g9Bm9QX`IlpPuymh&( z2J}ot?~olJ@m)6?mUk7T2~B4`SeryCx*N?SW}hOU7Mq6}P{WHONY+%Vm?vbW#rjG* zNpZH-p*WW7mgpo_;wIWcKUxiOuF_9QeJzy3OprgTY~>rlP`Lnd@ygxM(2hdksux0g zajmc_bo_yYQ38p!x+$STxO~ zTV-;B!u(H;Rs)K3Beo`*9RUi#VlQA6Ww>oe;hLd!djymvGAXzlNamI7Y89sgIirv% zWI&REBDg#hfJ7DQVTND;;TkoW!m?UT=$es&vQpBAEh|?ttzxyuBb^?Tnc#5o(P{`8 zQ8UeOT~m%1V37BzU^je(m=;Ju?DWhuNb-;l^{efor@TrOqgDj=7fhnd#Cwd^En|2w z$A|QONk)u{3}p}uiIm&jtgzBb$C7GNh}X1CwQTkqF?NE&wxhGA6=+v2y%3BL#dwYG z*P{7^H-;EM-WK&V>Rp9u;(d*-213=qsxjmWevrCA4TVKDjY3q1Qe!1O9x>`dK#;Tj zxZMK}70`I2x6)HO^<1}LS7EA2gJ%x0$&q^gQ%CE;6RfcjL@=eGP33~sW+_Tkg-FIq zvkad_7&_VYVxk0bmEI~OD`18vvQRiaNZ{RelH{-!3-YK?D$}$4Ng^F9Vx<%n@Ap>n zlx8W7Oopp486sKb3-&}k%-Kgc6#JCYHi16BO65ZEARA<(0@f|smYMU_af3(T=CA;Z zN`V(vbA5!uG~4nwsGQA)b4jgBCN0{c{H>0!oplN~C4&M)`q!@vy zX1l@0`D{fmE12z}w303b`rvS;Eem;G9Ynn=857ZjdcMx(`z+EP+MV_Uhiz{@!oebf z1kz-D!2N1K^XrkIVh{7FAul71Oeq^ofnyWcn`_bj zq}>vgpg#*^E0wf@_1pGPN!McyqdveUcNaVV;s^&YO6K?in;??|R37w(QI2DzLZICA zR(-rQq;pawt&xn`gY{L>Kyx86W$Q5QtG3GuH1zo8MmAv3P)lj^MAnx3Wf=x@d1R%N zsmeJ4hl{PEQI0DNI+<@a~UOLDhR2q0;rP?hh1ZL;sO|%TM zDRuMl34hpmbf_*4s)@V>q%NB!bRc#Hcw7xb9D`XfBaqGWqW#_*A9g3nS1=Z+b;Xxr5j%;0$&elUC6uDZCjK1&^(GiKo8yG}HNv&Rfv ze3u)|npx^#bx?wEm#`~7ZIW@cfqZ_L2OLjX4-GP5vP88dKkvEPmS z%I10-S6+M8n1PEI0IM!8_0PiK%rOHOR##oz{-1@x8Dj=6th!McoQ1)2#td9MK3H|} zo?sRR&mJ>y@t9!M#ruHGlh0jw?df9%F08J)ct|h{gFhKFaA9@T#fyVk7(8ptz=c&e za)q-nIBm?p#e;`c7jGD5VQ}i0fr|$WZd4#P7t*-$+Gma#xOnMsq8U76%)rHy2sbJd zv(&*U8-9RBxwgWIQqDsab0Xz~()2Lf%k=rI7PV{n6ch@S>4Z##d#f&6vo29|-U+?bi`sJ<;xY=*-_{EOq;?EZA+aKCq-S*(N^1_c7 ztgR1hEzJLL-kkgXTz1QM0sP5-4&8Lc0@$3pQK?>a@tS?}Jj91?0%`$(fSX02i>1ou z%a21h0aXE)1YEproP_{TA8<*)#d2{L0zjnz5OA{!KEX`@)C^n_IKfQ-R1aJdIKfQ- z)DZvy7e-I)HsH`rKt;hNffL*WKy3jKaN$@db{lf&CZNgy2)Nk;xLB!fzWO|L6Hsq( zN#KMoe-GURR32OsaIssMr3Qc+1R&sM6Lf-`0H{W|ByfV80H{m2ByfV80H{y^0xpc6 z*j?bEn}Aw{O9Cgj34p2vAmGBWPV6r9&`m)70uXSs1$41d*?jYH=q8|&;gY}!-TfT8 z38-nfB;aDRI7h*^y>z5ZicSSC*{lbwd-ZGma=mDE;uXT^`C#%KD*sA)74bH zl`~sbtpNVq2L+;6T(&RWX%|yw*Q~W|@P{wn=VoBscL}iEXqnyrkpV}p7!za0ivG*V^Fo0qnAUFS7R8azSq_`FaQ@_KzKC<2AOUXMQ#a{g($n{KCDsZOf`4AeT6$`uaYY_yBu zzkT5Mqw2ET>9%9)#ZCo|{v%a32TqZ0h}TQvdvc4Y^6qR3k3ns zfX^TC27``E6oF2h=`{Lf`^FM0j-?Xa#AP*O;n`q#e=i-QVhG=OX|@IQ=GvXshM@@}j%{!yG8lxS7o<~M4z4?8KsIX|TKr(4DDCe`f*t1* z;f5@6K`|Nv_xTlBG(zQJx+>yCGTDq&b8*sNXrm-w3?zkvVH`J&zMvTUwL}o*&?X-V zwqdll?+S-cr{*v+9mfa0T`-UH=~rB_^;Ooo?)1FZq~_fC`-$s$ktg;%#}y{}lJiFs zy<;py?zHglp?8dm1pZ`O^^w~J93CS^b$7kaX{pCL1q6P-NvAmK(2n~|_!xjsarSUD z9E^Z}a$ILm2M2Yc#}?#b5KSn_rWiy(%H@fnhuem1X8K))N{f%j7pFcDj z5RW_YgrDgC`&L~n2OWo4b+LXO{d*S+?&D8vE>`y=1CMpxhTlYI7I;HM$qH?pwmO1*ax5D{1OJQ+?<*2xLuC3GHmD|V^`VcAPDVGhI(O~ zE+!ECpfV)G!T^G6>0DQ4vAXb-yPV4S+DQsiStdh5KA+e%XkD{ItP<@BY-70J>+LW; zC8^~*7?jsgx>5@=jkeFOvCVMRpuEwDogpfirV~l<^eUX!8rf)<#4-~L=Zzkca>@TtzNy11Y9lvcNoo9`pRCmVc%{^JckF3u|*!)_Mp zqkr$>H1PQIy^FBIG4uVAjqE7bcP_ z(;n2aV2||p@hAlT`h+b3A^~sUNmt#qTu8FBc_x-cp-MZLp=c`5jpZ8|ILy`ag{T4c z7j!-s8}QkuJRbG+ejwZg&tUuY(!n&+ZbDe2U#L=K1E~!e29JRMvt~Aq5Yqk_i^Rww z)#J-l)<@N&$(k>K`z^Fd2fKPAP!qky{fbhHA%$6%0HaR_(E-*Je5tt&WZX-)LI;BouiF=s-~Cp^Wqc*Ga- zIVBu-Exte9^66o`*opT|&f@AwQ7>1zvWUgX>4RiI&k0&B;3X}(2o6_(k*9Lj!^%ap z3ug^p1{yLeK0!iUK1H^*DIf{)iG#5a!74IM%r=UuFe*xyD zsE{l5y6Fx?v2-74`s?s)9ntx8XD!}df63^SwKL}?N_pZsBJ}@cM|3kgUjm%t=w#ze zb_b4T&-y)ez#{~lBcnzKC{B!LlgEyo0)X=q=erZu@1SVtNf+cCQ=uDl4XwuG2?)5(T^UV-R<|(lR6-XFb)&U z9Gn2hOJ1)tWsI@I_A4a=8pc|apzW0 z;L(4PbwV^2kX=fpfzpga{_(cQF`CgIIvmG*CP8$g0qrb6gU27EF8CAez!;KSg`QIFFe521l?lfN^)MPT0roq#Eu;^CpMn4`D;AWfc zwWMaqz&i1y5jKVBu=mvW|2W=9;^`7f(yAQj4@#YY$(StV&u4&IE>Mao#<5yQS0Ydo zwmXs(?=TtI6zVqL%ITz$Ps;TUCHpj~-Hos%3B_(mm$`Er8E0uP?u3`COm~@R_C8FKJ7wKo{UsyRX?T1NHw0cm4IQ zSMK6>JsW5Qd}Qa#b}~Cp14;q6?YMjgvEvk=7x2Et{vx_~5>O4ed3$^N#X$T2?rrbd z*4XyKZ99O1z}pup3z3Bdpd)a@*5cN{)-6Cy;QIO8ym#)mKvUqlxtGqJzvb7nDhoh| zPjmFm^$Hgk$ZjV{;!Pskj;Kbx;P2{{usZBiR&R;Pa@ks6vNNbugYLS!0GaL*nb8s@3&Y|h%5Ke$xZV*FO)leFySiY3( zHA=&Qw`f*E9o1fxpu?v)m~fhegjNb}XGglSxlEQ-+Kr#YBFHN&V=cAg3iWLbTM72cKve2wwA_|d` zYK4_@IoC$5`XUb<-s@lnnB3P}K;b79Z3e=b7M;O5Sh{A`4YlnLdhu3PtYUC{}g7zjY3jQ9CFkwq3dyzF3d@pv-b z?dPKm-0i2VVcV_;6)#;a1j3bQqv=lzC9@VC1Y4@j=so1hj%#`CDqFek}4s18a;>z;Pn86MDw*_C;}=XAm?TZwFIw`taT+=FOKv~3~!}AU%rUQlkUdO8y;&E6s+I-GZ zEKw>AJ-WvZ<32xb3{zpwV+q0{3LTysaf4C=GUw?6-JKT9s$6^sMEv-&R#26y$+gjv zYWoY2#TZh=!EA9blGP6|biP_v242e+>)md|Q-kDO9I6Ic5%(FoUkK+aP%xPQnDr|i zOkF72F{0Ygc`T?wAtC6K=y;Un)m%8)84L^|9MyfCRSWmBi!o^Zkb@cMENaW8c9vj_ zPR^6c_9$PH?V(;Ht0k&j!lzZp7J(FieB&YltzY3_hGE`l5n7#AVqtcOlv9YAk?e#& zfGI(nLpo8Es9IJ=Diz2@7+Sx4^gUrb30FN*($v#L)F6QF_n=M%HO5XdoaNz+vKlrc zXjU{6ix)%dmyNikJ>3#k%wiE96EVW4dTrdIj%2XG(TPh!_kaoal!HX9`>q7^V z%OsmzhK2~Bii5(%XwMf2+cay!RU!Zck$G|0V_LpClV*AjX5e7Nu$W5;OiS?aLK-8& zc?b;IsHZewx^_g5t5Loo%1yEcT5<70Xua=XG{#Sw;hHC_ckv>DMLa$<)lIg$6h=xx zB5nEoaTw2FsE||^Uj(i99E<^^Ywbdmp(rccE=FK2Fbn{XG4qDl>xi;vnA2(!Uk&Ce z`r-?r^{#_SH#o14O=R(#eZD$`(LgJLhWnAeYR7|R63->;;w57D@6wXF7p;LxA(JzzDB|b>vNslF)cm+j5ca8Dam;>ttw3+g)|dlcW1d3L8_pVY0B!JT8G6HBV-8>hUSq5^<^Zza zHDeU*ftuY7Gsaj*cdW|`t zuG1PbUSkfZgS5u{&Nb$Mxg=pBUv;J1{*`gon6F%84ya|b#;9w|0S}MY z7-fw);HB*8Q|JwOjXB_H=jlhFH>5S@fX9%hCFqUO8gsy_%+ucny)j&44ye3x`UHAI zTw@M+C%MK9)|dkx_MLtS^hSS;Ip8JQ=@&z92y4s%Z?{gr2zrBGV-9#kcKTuH4Q`D& zAP#-{h0q)98goGW_4EtxJ^anLkXt`-bo8x9>d_b7`sKS{cK6=#H-c~VKYnL<2fg!~ zPX6)9pExN_x+kA?`+vXvXKufCo4fTBfCuo34-nh|{DpfTyEnSe-~X*^X25%w=l}Bj zFQ0$g`B$BP$=R=*P0q@v|8dP0xaJl-`ir+ec>I4I|LC!J{Gp>?Jo~A$-+%hIPyf)V zeF~qR-2K_R-*Y#3Is4$NQWBtbuj8c2kHI^P!e}@{Lm0`rT!@T9*4jC z-E=(n?uI8Jb_23+$e;oxpzKg;1e~&-F|pZUIgNsH8n8xfagRFr1__`RX zlbMq(s!*c;7JJhOT0*ggf_jFY*@I!H14zH}C=KdqY{M$9_9}BDWAecmD}UC8fMa<8 zVnc#=2;!?Z{aDI#bg~-PFmM|;=*)_ME1^bZ*>i)1#5EfJ$^uO2iq&Z}?`;k107U=b zHiUx_{1uB)yyy;V{G$W{M^BaZO?=V}Hc&J{a*08^i%?;_zUEc!gMULW7=lvqb3OaJxEJ z{KZU^lo01xw5q!zmC#k0D+UYSI{$YY1l_DuQ%_0De#sW&f{c517R?(;VE}3paQ)&U z<#AbMLDl}VZ$23DO$Q@Bwn5;wY*1z`!lL-j+1d;k=(*P z|Mr6sZyb#H#0CNGBDE=}5KN1K6b6@8ro- z9Pe=DrKBV+r8MMGYNe%pDoVY03RoC4B6J`A4;w@^Qmdufq-vBGQN{{+eNHkQtaQ}5 zNhHopN626{1$T^)b^hdF#Fre5_~H!$N){DwL`s?;g*%M2WwSX8xo$^M{?6AEw>^4}VISt>pVXj;rfcVKx8?7ouN=moQ zkdE1aM+7IXXttu2qaNEyMtEyjOVn{cMv3sVKW~dDljf=$79@yaX-y=u;VGh)rZAHf z(|{a>a;uNDIXpqg&oWj3zgE(M5e&k@ppFdcuKX~ib@i)Kfi{sP(|I;Jz$sd6$+g%>8GJNP@ z#KngH?_;-)-Z=Z~4}bCQkKFp$-P)c1?#^#Je(%W-oLt`io`)Xb{Odga+M@>#{*MR7 z{hz=6IrqQn_&ZO(46p)<=Rb4$m(Q6;zw&O3#G8B!feVk9_PpPYk#;(qhmby@mqUA6 zU(A%L(SVUM3QJm~6UIaG825Ka#Z)#@P|ObQ8N+umc?& zJ1^xByoMV0ld80^(4s2JTD>~#R?8xp8!#)(rZ{D;S!# z%hz&Gf2PB(MTPnoLBn+0lv!nQ%urFaVd79&cN^X^~-^<06d5&9L+#p z0C)yD=%}P6R}9V!tD@MYV`_#kY^5+)Q9e$4c)-xT-y#^)eOQx4L##*WvOF+284lJ)) z5;#C54k)}_j_i~nRJa@f_7j9#C^92f`EF}jsX$`{(Z-co*okLyK8%wbwi|L4)h}{n z8V~Y$L`ity$^uR?yE-)9)FFsNt*BDskoQcNWu|?0(8aTcFA-9M$q=gYdX*6eSMt+5t)PIS5CPZR>IocWeCyZQ;eVKk4|K zhPDCm95xBVNwbQQ?U)@+D`UInYRwC2RLq7Z-(3{#8Q`p(`1Fnr$KUr%hrpOgBEc(k zvzKu(v*^@(w?r;^IdeJ<+Vwd?ZISXkZikmr2bFNi;ZmF|jOutE;96@epmLp#7lj>_ zWLlfpy_()yWEZODEe%o`sk3%BZcC!t+0o&o{Y(eHD1=GJ9(jPEt^-(`7`N1+kbFLq zSD0z*q#IkDUR#)A?UMF~5+E=~7hZp1wiars?O5iNX!ghnRyFV?sMMf2ZD)o@%7q_V z)QDeMw&wZtWa15PaOeEcGacO3JWRt?g`25F!xrckDjCRlXrld*P>wlzv26LELI5fB zE^AAzX%9P?I&esOy1-j2!=ZR@K=_=&cD-54Xyc~L$SMS=wO+}B_@|%K0dRIL2gP97 zu;fyVHZXG{&<#MIRZ=)Y(dJZ2V<{$x3-Zzst)h#OirpfI3fY|^f|RY0I^bRxU`Ohj ztnY&xfj-u#>K7T~EGZ?mQkE*TU_hq3EN3S_{!9lOAvv)_EWKu}mkgTn#3ik&?{IKG zqcS(g=!(T~S|V%Kr8MLRwxE{Weo62wE+3(OVcGrE;*|+EV*6E7Rl9OyirB&)teDTZ zwZaO>2pA1*A7$V!M60np29I(Fv?oz~-y>bK)}bjrcVocc;{$R(5ILsEMWK+EPm&~| z4Mep9p>Cr@H>)xxm&Y`xs}OP#rwYf(lGO}RQ)g!`?);fIbqHG{zA>IdL>&-qb69&`UswZlX*A~$A`fymT4pvPkL5C8 z|C1Saq{LI+A=mejGqV+Q15a8p(kgE?rj=GB$cfONjvMn*UAEFL)yWe&@g0zAg+l!B63}^pOgTrB0q5pw zVmpfCO9QppZNLqR0wkP*`(#O zy5>{uFse%u5&9FN$VZDBAuH3cq++T!h9dG3$7prbsSVQcD3l_ZfP<0K5Kgqts!IYAG39~{7P6)Q=DYAtHhbpr&GiZ&?9*eEJX+8wjty`5`F z6v%}!T3%Zd8pElOYQtf%tdMOHYp#@w1t0(%p^NE~=yp)f!_-~gySv}^OovjU<~?<( zOQp$>E39a-=w<3E4Jy5MzT~|~Z+F`{SJn&il4V)OnMEaIo~cz`7rCL0lmxIjGg^Nv zjfPyoi^w_y|i6Na4Rzgvem?ly)135~>WN3v;a6>l?QE+uJ6+!yrN?}^f z3rY=>F4|wU`xAYoP{Cx^FYf%4H+2Y#CE7x&nWr~P{ZYv(mjt^vwF_Z7nzCYUh)RjW zq##_m%H>R*2f1TI0-kUYELfKyg|}4Tb+YU%xbn~$7n&V-NTq@g zoQ~p^ZIML!{n})im&z2AN4wg0rxH>NgYKwW2)198cBdU z@5cAVGiL7~sis_$YSv2A2|cku7M_+ieXJ*7VojW++=>JKjuU0MtX!4|FxPqB1U@;E zg~FcE3w8abeed1c!~b;p&FAkw`}up}wTC;!!n zd)~kI3wQXF3SbNP%scPw#&J{ck=04`;#cZ@fP^s~^4R{(pQB-Gk4* z|M6ct{i~wuu}x8~`ybu_vj&W*e2I+)&_Hj2jMG}%82crrVz-cNRKbdRiv!p7 z_JSj~zz=MJa^}?n%FCAt3Cd*I8U*$G3`fE~402W>w_{Px5@{DK2vOTwr4HZw=`FBl zCmI9g-Lg=sm(gKeftPhOpO&#YYVo723ir(kN{?rOwE^D!dt0Cd9(}laPz+d{!n-2d zQ-npQSj1J&hoQb<%Ms#N9LB<$8{ql-wm<~#$#5S+4T>6PmIg7D$>JPMN>%VKwU+`y zc5UuSHJiI`@n2lGIC3RJpDcVpF_EokUlm$@K&Cp#V{O8Y7P^g@J!y9ob2~VvU$zC7 zS~9t4`SZ!hAd0j!7Y9z(i20x=r={gTMEtRCr@Le`Z!%O%td6)>$m8u7ZT)ly6j!f-p0!4}xDYLcVNByX6& z;L&WM2oBvSH&$Vx23^KJ}z{(qx$Icw!rStfKi-)I&CNEmHouPvo<{tsbw-2IFBVdtBAsvvsB!+c<1uY z7Kx{JK@e@muZ*S`w2Vp}spg24P#z{r4`Vc(wy+dWIcJM}^sz0lJe`c{T|fkz4ZH%> zoLqW~UO7d)dc#Uv0^bxT)sfWh@vgh+5ehkW_<^9ee3_Y>1n+3S<$E zjA0F29`TtO!ZW?PT>~Ed_HBz~MYF3*Js`?8#hJ$=8(&}cMwH0;@uQLCZW7I=TUZIM{&ZE2dO zGr6RKBPbycvvRAwfCZ%8_s0Cx%4^(oTIB3j)%(NKEs}MTIOyYa57$Doz}?cM$3<$f zm{Jg(4N_X8`z*~ub*i@Y$nV$!X(Ve8=6xLGu@3@{HD=)Dgl0qo4+`u$*C#0o67;Gh zk=L&Lf$iEsW^uV9Xk3pUv;4~IvV9mti=kOmbQq+-32cDQEfT@h?Xq*NsZQr)ZNU#z zrP1za(=Mp2QRjl5zACFrP%8^3$x>0mtXSIm)#>MNM^)#wB4P!7jy76pCS^&Ifyt{h z3asptc?%4u8Il!_bhm?Z=RMmlPV$c0Oi6{xbEydqvlLQ}=yU|DoL8HH+mN(5Oq#)v zoo;~-KDY&1BSX#`(|y{*Z8&lV^SRftuD zU}R)%AtpTqi`q+NoOi&e3;=%-W>Ff~^}LkfL9g)&x44bH*gd9Pq$M8`U=S&7&g&6{<)H{PyRxBlT4XjF?q ziSV;Ayu=X0U|nNM54eUmFsp)EPlwiO0Cr7S(YEt{^5t7qF%=)-paP;d)Wcexz;ZJ{ z<_)+44ZJZ@p$pVjuC%3Jn#Y3Bt>O5<{%34?K!85v#HA< zv>(GDacMgpxBl!F=z$aj*qVbY#(e7PJ{ebqDb=Xyr6V7=P3_!qXodOQTV3hF@YH-f2DN>?*JgrQzI zLxQ;svw4YXl(cd^;J54X+16Gw)ew>iD>XW3VG{gm>4i*NW+_9c_9i9pJ~NjvGR z%q{Tt`KHCxmuj=QGZ=~5pd!^2zaQiLC?ct(#p%O-eGDET$ts4qwe8XB>~C#VZJG5z z%88jnChc<88zCU*GM2>lO7`cN=GGfC9)VLIM4Gn#_(NM@L-yifcerxLRv6H{hs~>r zPDNooP8Y}`a9WzvY{wk8-0sNtzVljDtN@Xhyw$6ObD=s-s%A_SDlv~H-E7rp#VN7k zYdr#jwgzzjz1tS6vd>1ID7BJc6vt%Tq!EUVl3uf_^|-Py)~Tjfr~CrD-dJyKTU@1R z00s4ul=m8m-6$JGh!^9flP2+k%7uw<_xYld^vvr|mcRX&EMNxEQ}3JnkWPByN2^Y&^O zLskQa<(2 znX7erAcWF0J$wX8lPm~yPIOIiYjP(ay?6TMw?60S;jOzrdbfV(haP|JqhEcL+$kM> z`@>&)=%0MS(Zk!{d;A9<{GSiZ`@eAiqxb&jRrflQICBb;leK>B zGMTq#dYzf%bTjO+a!)Jh5;!y2Grm7B4IN38Ry~*$LW5T>Y&>%;Ls8vKVF#J%^}9P4 z*Y`TlbO6a%({6o);AK75vXwSn(2b@QrLpQQ=z0^}J*wogQns_i?9 zEAeb+HWUaCg}AO`vMn*6G}&4cTnHc+v(R5eewGQs(z0XSDRwVQAN`aqbU_Wux;<1|T?-jry0uBmOeQ9%w*XCMt%2X1H~q9BuUheOIa>=9i1XK(7DmLoaS zh$+}ndm6l8+yGuR)P5QAJ*{Idd4$B~AavVV6yasPE@0+J7`JBSaSK#%HyeI?mf78T z5kY-e#z~B*s&%sl)5rxo32}VHL{wKalOc#i>;~WOzV@aLier_92+f>UUGL*4VUEXn zp||}NNCL>_W1qu&ZMY&EI*wd+2=ELySULetgYv!L?L>E;F^1Z0mnecga4rzPXbQ_-aXi9t1BBL+e_a@kVgsCgM% zZJDT6ILeWyI-_a~L1AT5iaouq%L_4;a9?;jd^%Ed@}e_e;eIUhx@UnsdXO2rfvxk8 zJkvp*Ri~01(5@qRZOfZBSTlQSPb`kE2(}Upn{z?(tF(=UmsGscAzXhF;;OQQ7x_Hs z4CvmxHwVXox&iW$AY86IohmEay;w4?Ee4_mF5Zze*8A>mu=V_X&vf7`Ji_OqE>Eg` zva;+m-k=*T=QDQ_CAloJYL2xQ2~lo@mxZwyJ1y6w6tc7QkcRJ#)oyh%M&n`9U-m(q zDZ;9rg)b)Ig*5X-a1!9aSHBjtmoAR&*u(MHJkx<2_GzCRSIrTlqY&IGC4wZ@m)2O$ z!i6%5JDKZ<6Q+(>my>1Pvo%4nISp%7P>>Mgd#m9%;YQrV9=Z*SF2!aJDm-Y)g=v8( zBs%8^+Y8kqj6!cmhm+Tz>Ci(5SdoQTAEe%}gILMSopEzK1JCFQy*}tDRaEI29gAz~ zmrbkG;^cHylR-%rrd-FmJ{c7%OlaEHhIiANWRhB=-tuwgAa7t6pi;MSxqR99OdtP!o2LWXqn{ zGiGQGPIy5SQqzgVvW`$2115@&u1_)UWI7t`%*BHrd2=pQ z$Y_QWrCObLMyBEpJ0q_QD%do$k=dp&6jUv(*=W+%Xrp#1X-mTv(@JmFWrb0D;>$D$ zP9qQ#pwp>ND(18=j)J&@S|EAvmB~58b2^Z5$&Vy9DorxJ9HtR6EocP?uQ6s6c#i~) zp#df!Bbak9(^d~*`DA4YnIKu+O3fOJ17b2TRk+%nHKkP|bAm7e6=a$hNQk6nya1&`an;gL zaJ+$oZaSot%f)hqbrB0)weTpx@yToi^K*0vS<%!su^~0~`ei0sLDdxqGERk6*JlwO z)aB}qdb1k|A-Dg_XF9}2d7#7HMO$QApnyw}!ll_TRdYA!=y)B6!4-2(3yXnVzAQCM zrFNa^3uQBfMQ+?667a>&dI?%yQ6{MRRf*JODt7pOc7YNT3iGM{h$1`{7bdE?GY-c; z^Gt_`C=9z*X%kds+EhW65;v8JF$c<5&~2gV=md$zRMcf)^0GQzp)(^F*A+naR*vL zqC3TTVG?D0OgWk3Hk;rx36;Q0RPgBSa#}Zdt;x9U#w40+d4XeE$%%!fUUDi<#M_DM6bcn_$2|In~axfc=&_(6M=q1qHlH z%@M{k2^{)S6`SdoU145<(iLoVm!0_>9?OcmG!alAslqCuQAKhS0tvX~gP%Y%rd)=>%jBBd_W7)K z3C+=BnGc$ppYwMzO{+@NbOU1B(khHY2QNPAa@TR-4JOz25Iw(iAuQ=1nFW7f+S6EebaJj)IdqLnr_r7F#^}Q3}4O%tm$>3D(|5B zyd0wtU8?19&=4*XYDp8|2xMSEn`b!?sM#5ZTi@{7(Op2icXo8Ch@}yf)?4F{(1l@p z?u(*CxS;+SsP#!jN_C7(6|(~yeLKDcSGbb^nNU;7k6Y1{kb`N#6=Q_7oCVA#;|}2q zA_i!yz*-G<)hUe*7DI2!`br2Mp6}@3JR1fO^Jp_gV^}GVm*RwK%_4;oiYDC};~))0 zWi!8%YKtl;xP8bw_lrp&g8U#*WKQHKx}q-oyDA+2gH8SaKYi=rd(Qvhoi921)1TZA z>`zPowT}QO%sja+%zQw(i&v9l@6ZyhEB(H(Li!6?hz=;CaaADpfMOU|zdxYh#)}Ke z9-`nug_&7UzN~F3ERKlnxwr6$-80!*|5q~*HFj2qHR^c=qF?92%#C_wO?B|XN~x%y zES0jhbSmW>PsAr^<;fG?i&+^qE7PWB-~XPp@0 z)T(V(770EK;(7->#*I}JV(2HmPDdKmqZ!g^w?@fiih@*8V=mIE=E9BGa(fg;Sp5de zYUqBIQrBJI6lT`03o{>JOz)oGy^xCNQ|?EHI^qF^nLkCI^y2gCy*vOcfQ&8cTODnaA?DO@7DA$ybDRl#$P*9Z=gOG)62E)1ukI(2P8ns1mc0|ayEH0C3T!(y+;%rZ! z3i+ThaQgYIvmgJ$y6bEHh4hWDzS`yi%k2R^_wFq>^%vEFKV-RC@1j?=yWgx_4rp=@ z6|l2>Y=~ypd*k!mK%b(8~l zEVtve=dD?84=~qv&+}?GJ?{m+^nyd`i&<_zMc#4ckne!y_J;9(9kAZ(`v0F?mfI#^ zSbpg?gXd3%mW=5`F(5S5jAxkT2-9kl;XuV9N6)3KHK;hL2OICa{y#_xnRH-q;|Yo; z`%W?x4TzMz)Cp=1%&OdCSO%F*G0AgjQej7V)+Fc=7qP;1mgoe>derNwe#_`Wy9F`ga7d_18sZ-Xybrt z z)zSBE=XoZP*B*YnwVtMrzVDEt>U%e}*;ik}lG3W~% z0oqub%>jkiFD7o5J7!nS20ieyVE__vchr9yvw=Z-YqL3^X#3vnJ#RL3(9YUy4k+}# zcRM>~Q(I3HFq;GF!|&bH+nY^gXWTbtQ(fz`p3?nm&+o0zwb@ia8*8&Upzi#ItZVd2 zjrnw<+6Y0@#_omn+n5am+FP5=0X6IQZtr=sDT8*_W^+KD`@P%QF`K9BX#!?*KzaPV zn|gb*fnI1fA6e@Ix_&_E{k`?MHk%S?V{JAEl-+;ziZp_iks$Y`$bYO~7mpsD-e1Q*UoJDa{yz(-t9eaHeU$ZS)0uPEC+kHvtu@2 zu%0GhHV2R+?A_GcnoYCyLbJJC>jS!e0P(`!`dpjM=YuxZW^+Js`imkI1;i#mFtRhm z+Vpa=n*sASX7eG?-r8&qC~UuXd(WHA1!(7LlTxM73oEvTNV-zDqyPl=4Qj^VZV@%;ta^`g=F^_GZ(3q1k-yS|2dP2h``^Tc2yQ`K_Rh zt4%5a$wcF&K%2!#0b~+@uePKoJ!j-eJdL3!M9I~43c5BPzWQF|ZOrC_puM%(ysLrE zZF|p~&F6r2)@Jjr26ndX?3m35*3$&c=3Twi-8S|1X0vcwj#GqT}F5 zJo=8~$45VV|CjFn$@}sB_ul*Ad#igDQ2+nO&R6GOe)f;gzW3}a&pvSazny;X>Gbpz zZ~(e@KYZsG@BFuS7I)rr@@K&{!I!T8xdSHm_+rEwb9)(f+`JbGMo(PsW0Tj zWp}`Q?GJr^4(|Vv-+2ej!2aO38`TE9>x}pRFnS002oP@%5kL|fu$n~o1IE>N6Qj1> zx_N_eesVoBdy05w%ICMQM`lmx^CNS9d_6LIg5Pdr>W$rzIX{BIz#lL&n^ouh*0okc zXR;7U7X)Rdi_n?~FZs59Tdiufw=ABO)>(W#U!2`T9A--ABBz2HbVT-+UzW=+ov)MP zx?_8a*mUe9`HDM2Yds$O!rscyda}aq2w(ig`{S=zUCr1F0mZ8w#>+2lHp}IcuK}Lu zA9`{3j(>kW*n9f?P3O4(-CJ{zl-vP%ynr@#6*DS5YVTH9*<(?xHQ%}@R$8Y&^R9}A zcWmhNZ*N_DU+tf{_UTV=$8KNf&9OWEJ6l8DSH#9pPybdMENPFAUtQd1JLF5-Ss3F; zTP*YOhS(Q6`jzd%^vVK3TN%$UOh^CfdQx6hU1*OvhpKNqc2`IY0V0*K@un^yZu&{qpsk?z?=*PV4bEUQa9D1r>vEAx{#EFj0IZ?F{k@ruFW(Tu{nl%%*%NxxYVLmg+G_R`v9X%F-*kPdl$0K| z!cqriI|kKG=-p(;7tGhu4{rx_v?uuO<_oIq&ezeO*$(Kw;-3xZ(VyQA=)TaK1A6qM z+X3BI#CCQ4q3Z$N^Gy5vZg>`559prYw;RxUZFfM=!s`LuQ~a|5J)2(-=$_D<1A4Z& z9?(5SYzFi!xE|0w&(hBi=t*`xpnHPfZa^zHZuL&S_FWafd%QSFujgw|?VrupNq&|8 z4+t+FWM{v0_vF*e7w`DH=F`Am;|PF+$LV##aFg|MmKO zSgMqv7E~@*#!wS#l*^@Rv$_81CoAFcMT)<}gvS*~F&LSz`OsT0TCV=eG%6tJto5bZ zm@|-YJ$|cEb1LQegb(K3$}lESCL-HVjn>UJOmnzcGoGfPs+KsZ5UoD;q&tEF*Knd* z0iN^{Dus9IIM*E3tSr>S%wKvGC4&dbANsnrK1s0F$TY5+)+}?pTq_3E!4%ah)vK;= z5+2R#gvSHS_0_?HeLb&y)AQEZjXTN1>%ZFZj?Lb`wzTKdfj>pw@lF#SZy4`-ramBxIInw%d5ZYIxzNcJbuGCGlkb~e~W)~2rc!i!&p1kzJ5QqTm^vNW9{YD zCcHM&7i~QC)=l&O&>@kiwctZCm9Kt(K=|p!8`}Z&+%MkP;ux$*yT1D=N>=t`>)BGW zG*`>D9+S|>*K-x;fezNp=iPFhC499U#Y=6u-uAw7y&Ap4EvMN6)%^hcgEq45F zeOSNgcUMk0cFikqU3cM~I%2Z|zcl~9*-sWzrHRp)-e@keimaCNo}NRMCr*o!ntq*Q zs#IO6JRQ9ASFNEk1I$Db?{>p{-cz(Et1_vI5kgQ|bI=iVNasf7W@RB}9LM7X*f(pr zVpV*p0xEFLroFnyHTawyJgs$COf+p)S8Qv&JFl0nw?F$e*InP{|3mBi|3CHkcRqgj z=)ZsDKKg9%24MB@!w>$a2fyb*?fyRn?*Nc{zj*Jv@3H6q;rvgWf93hTvmZUP&pzw) zr%n^_9^miZef=(U=jZQy;?C~?Zvy`C34Qzjx&4E;<=dy=U4VJ~-lM;Iv^cuB^)oMd z8^FH)MJK}9M=v_2H$@R5ZG(q=77MCN?1pwH0R{~!d(|eh~-54eN`sZH>q{*&A_X@Y)Sx}jiXW1@|#>)i0 zHN;*nv#)>XN??D$q)9tL=CmLz==O@;FD_A-GtC*||51SWG5 z>&b;-vL@j6_Q4r*zgfPK>%hMLxmN;nq>y!%>GqO2&gq=F+O3B25)B(SC*`+Z34CRV zxR(j6@AQOy{exEmU1UXTq+rXSC_gR@hSqMH%F6`q-W#*8f6kRa$!1K0&3J+!K%ruD zuI;iCy-eUu*ZaU`3>jTV<=G6=Vr&(~vbWsn%1Z>^h}f~OfA*C?UQhjY#~cj@IW>_Z z(b;`J^fG~OO%_-i_WQ2{b|=cpz>*PZGITeaQ`nwn1K*sK-*P1opMdv{LI`TbyBeMW zw!u9;1->~cpLHdW;>8RXLD4W&&W57pEcdiS_~xX1=9R#{!cquf^lj41`YJwI>~i$I zYz%L@-utcu_U7?&(q>drz#WC{F6Mi#*!7$1;d`$HN{OS*=OmY+GYb>vMiTF5HuamX z_ns?(69E+Fo*-@z2o0IbRN+2Mv>v25K z0e(!B?6GRJTYdH=W>dR4DUYrMDxlKT$QT)sE`)^J4mr=b|ui1`bM_Q=iM|}seoLC-P83dH(l@aO5l(f;Ypv!dBH~~M2vOz9K*`ZF5kTp z_{x&TFBwDVW|!|=30z1-HW--Gg<1$oTfw_~E)LMmE}vWp?8?&xp{G=P(kGX>z>M~s zW$5M>aC;*#iMa){lvI(K_=39f_uS~qH@kd%B@m6oIcfT_gsbFS)GcbyJ*9lpu#c_; zDgq*|tK*SiyVrcD({1nPDdn59eCtZ!D;Zf|Jj>PAjV{0TyRHPb3Ekzp2DsEDLmf~1 z$bK7rwRL0pdhP2sWB97K(J#~RhS|LKbyosk`4IYL0&h&pYhQaM@Rd(wUncOz0{hz6 zTnT*T={{*yP3KKN@4;{N{&{$GS|%r>zC;^?y1A1wf(bH~hS5af&P5j;xkpi(Qh ztOf+&tts4AIh-#!q^`W9$k_9-f~#sVFS@scUs#n#-v6=F`nK$#p!WLr2ZZBa94k2B zmDyYFEwzvMnqhtB79iaz=sV^)@8Z9SJ|tp zu5SVZ&UIkm0IPXN9``~AdqMG?4_*F-cd!GVRldc+w&|KLxR3w!ak3XE^6L>8XjQBA zCK!PiM(Cg@UT{=T%CR2c1uwhp0h&cbW57g4-x8-{bAVGiPq97uod*V}3a-xS+AuNr z7zZzrvJR{-S4Mr+x9WhcvIr$?1~u6XAq5sOw5Z+R7WqevY{AI&ef#X4bw+$4WpItnox~8^e0METxMKskP?SjHf&#ART9bmn)^5 za7sQ=nF!Xq=ZMu8Md*IRJK`Y>JNzpK2+?+!={yE}3fS?Z z=d-(iojmE)WoB>q;-|~Zg5&k4u*_@=YQp80ZS#QBt>P?cb>|#-@o;% z_rBo#U!8p2S@rDb^z{74&p&Z@e*V$Z*7=jOUp@QlXMf<#y#2Yy|NQvJj=$+x0#yY6 z)zOb1ed6e&xBlAgAHMU0ce0cB-@)#@XZ=$7*5}?vjxW8?1NkAEzI^TyuU4tm>kq#Z zx@{g`2DPfnOSOdK#&fF1#4<&6s=bm12e~wV~&Y%GQH_ut7*+BXs1hFeQn$Jx#_QFSIFQNWw5; zcy!WK!zxu9qo6A3gYVgBRjp22HMT8U4VuBwPB8SV{XQG|E;S87uGhRFqFrgGN%`>h zzu6$lwM1x&CB0*6aNnxhi_WS_4Mg^N-y6Tf7a^T`DWA+luM>?%~ zXWT10hUnAS(T6sOJWb}Q&;b=+tLYGCr<6=;a-X!haT5*Z1*=hP)NzM>WO!n15fNPj zD)?dnRi%72GVGy=YCdM=o7YHv}XCZ2yg%28^j7{=RWKZ6Ly{%R8*J~Ry9DY zN}ac>E8i|DQ&~5Pp5MIpa~nh^*@~t{{aNgWD!z>UC}`3hCtJq(sH$Y-Zt5<_%Urh%`})y$ZxOsP(xDZv2i2y` zg6cN~-wde|of<}E$PZ!4X^s=Bi^$>apV=VjdcTfL#4!)m+JQUHhMwI3tj%Q2n|ZEG z1m$LpLfD~K9X=p72$HSEvl@n%6+w0U%p&=rRGT1Of9L}8#nx01pr${?mWqD=uUsL5 zIvQCcbby*9RZGT{t{QrgnTpY7I(0O5V}1|=7S#YBWViporVV1Xin5~DGy_WPr+`-n zf#XS2Bji@*|SBcSt~=e`|PHTR&5G6V`-^nxRx;YRi#vo`y=py1MUDIPb*DA zV$=~c%Bns7z^08d3;4fK$s^=Go6t6(BCE`iRyTyX8SjhAsBSn-tW^gq-{bdg5ISwZ z0lTPo+>+4L)D9VR<%zI}$@ydsI0T3RHEfJa>ZoZw`qx(oZ?1zWavS<=uq-tI6Jy*1 z-#J)?AXIkFkLOJq%bF9ip9I3ApWPs4F}Z4V1q>-zsI?kF8d{o<`U+qd&TG?DcDbfH z@C>m!YCihJ6#_85FqGS^VPtvUg(wbWLH0|QtK}NqYOpDV)SA8@>d@R#ZvWB-(L!V* ztj2j!T|qRjEmmQQ>(zHMCJ3G^PXJS1>SABpQXIK;p$VgxqgjA$&JpYeF_9<)JVY{0PDBsM(TUZN(70qR4C(nMkS zwCQAZnCW}2XQF1WcCWNS%tXJd^66A>b*6AHY#E*YAlFDN9}x*eKOHnGE(12pd5mcby)MD}|3jyDKPDAyd|I*_}d%^pZpB5iuEf{fVKkiemZ z;aC|0b(>V7oDVk$A0^FXDpxR062Ll=S3Fa2aCO@7>)@jSP4x5YuV@v)=(6i=Ue~iuobN{=txeM>d3-_D*_;8 zEC#fUPTB!4NHR47T(2%*KsCr@ zH>)xxm&Y`xs}OSb7d8leQSI|kL{wBym-vXLY49#ak#TNV&gRn!T5IzikCw2EJo}#V zZS&~TkB9lV(#D8*9Nw)^ZTg9r4`9(vipUs*W}t`2qA9$Vt3iCA>|8m)0d+3tX% z$W@uP26TT_D`zOujqt@hZqUHrYJqTE+B5;krwh}9mBQ*MfM^b@xNh7)0~2dQOgZy2 z6nJFPuo`Yy+cXi`8HI(8I|qfU7fs(p8nsf;>g$3J7v)ys)cT=NvKm;scf8sl!c23O zmR|;G5yG$wiW)Avayu*pevnrjlx`T!UQNmKcwW9MZxC)%$MJ!M`ECUA-t=xCo&`3k z5)!EiU6ZYd$z;{6T4PT?`K%4XaZ<4A!3`p}JL)uY8r{UwhQY*&9d5{L-3hNW+T#XZ z=%;^oi>S}gDn}HtSV=1LO02D%%CHUv_{b_J))F14W2~pdm4$oun>UDw28b>f&BB|^ zV%{u^JgK)uJ69_~1L-ZjltX9}Htdld-rlTMzV`pJ_vZ1g+;#o`*)nH8r=mcWtrrha zxWrB}SthQXOeUF0GFv9g%qXUlnPj$PGLt<6l;TEJxV9=@ByHds6OK;rHwL<2-L4J@0&8llS{G+vojxzk8xEQ~PPw zPv-JDvnCpZ%=6&>v^Jj_Q6brhwrwd^({*a)*B4``ST)+9Byg2V<54FNYgWz*CT2E_ zn_fp{RG*%O71xiF-xv&uZ zYO`)M#cKz?2~DZdP710 zS344Xl_P!a`xh2qb-z=btGUpaJHrMmM(544Ns#PJ8M2h0b={c5S6D_+6NtaCC?znd z%TDuAy&uJT`M{g&M6{#!jJ8E)RH4|UP@GQ*d91(TE-Z>+DM>2jCYVA+D=OrNd?`tb zF`rB4#BwEP>R(WhzL&8CZ)dR>pu3k%%GXp#!RCW~HE$4fzhRl)5YY7(ZB*Gg=;LU7o?9D}yX zEC0B#NNc8Qq!}5n*6fnnt}5Ncpp2%C68J94(O58H!RzQS7c1$Jzh77+S+M61&v3%1 z#0_Ms)d790ZLBEw9lbLUwOrG*un~$iwA$*4sw6SN`Jt$c>n(;&#wc!<&SN6z%Q-Ih zOS7Rjm)ecc&zX$A^}Q!!U?ny#`0XqZ%d+5}0v$7A&&DVkqq{Z6N}QYU#ZY!%B?obm8dU~pj^J4t@XSd%9KsD zE6Q?IQ>aFN>)^zKVS-t#!Wx{$r}4rJYo^McFhb>WG}#A@N=b4Wouu?~1Ks@Fg~jRH zBCh;9wCI96$8JUA8#y_Y@_nXm1boXM&U4*%O0R^a8b%mtK_N|%SiNImG0>T=J5(o` zx@cx;d(h0JJ9yAALHl^hqO!?}D9~;(pw&L|7I5ib^76m4va;IVxO?UB{=@qY?>)Td z@b1IA4(~j?Fukx@ojYb%Izz*FW;A3#w(i}! zXY1~*yTCh-JGO4$x^3&$ty{Kk+PY!u`mO7>W?MekPjStbyd`YWTUT%4TjyKvv;y_@%J-o1I(=AD~&Y~H?k+vcsCw`|_Dc?0OkaNXu?)8DiP>tT-Mn)1ip|S6FWcPOTtV(f?nCZH?m_NG?n3TF?m%uwZUglgZ$WNCZa}U_ zu0v*sk66exh>Qpbja-f32#Q>ZT!CDUT!w5RE0Oy_-Nt*t4v)JdcSY`u+!47wa$Drq z$SskZA~!^?k6ag-Mf`{rxh5hg$d!>RB9})li)=+!HtyfJZ{yyLdp7Rg zyL0c3z1#P0+q-q|mc5(yZrHni@4CI&p1)`9U9%_e348S3)qD6Jx_9N?6?>QOUADKi zx3YWx?tQ!W?%uO|_wHT0ckbS?d;9KfySMJ%vU}6+4ZGLxUbj2j^>?k^Yj)*bVVB;$ zdKcfgYvaz1J2r0LxNYOsjaxQu+PGok`i<*0W*h#7wQE{p3wR$NKH-x2@m0e#`nz>o=@lzkc2NY~5eC)~^9O zTZDCb{p#~K*{e65|M#Q+`dax0HrLnIHa~~_739Ale+l_nDUu1CW1#`~l=gkoQC0jl2)?&ye>*{wZ=N>Kz;~$H{_on zzYqDx$nQbkh5Rn$A0qF9{2=m9$PXZQK>h*p4#@W-Z-;yz@;i`sB5#9y5As&XcO!3s z{C(ujkiU!E4*4$Rw;|t&yb1CSGkXFF^hZ@+wGh)`R?I{uJ^O$QzLt zL*9V=IOI1WLtc;k5af%HAB6l75i)=rA^~K8_>ex*hwLLBq=)n%dq@|ui@1<3;y~Jn4QU}3q=}f29i#)WDyavj0lh=goi9593+jfkOia+nMX>H6jFpF5e71g(2xXD zfV>*XLw+|xL4FsKgZxf}g!~R93zWD5Ci$OQ6TkQnlvka6TYAY;h4 zLtcesAioX4A)`nd^4UlV@>xg{@|j2i@=64QdRsiJVH>&Z$++xd@Ay7kXIm4 z$fqFBhI}$|7V=5Rvml>{JQMN>$d!=aj64JKa^&fdk4K&c`8ebV^0CObLVhFiRLI95 zXCS`;xdQSs{y2ar$O+=o2c+=Kkq&0WZ+Ztg&y+1!S_ z0^G+P+dpY@^MnZG6E`D}PuSdm1Zp9_d26p`P2#j2J-XB zCn5h2^4E|Nr}0MWBC>e@38xzx5$GS}BN6By?kK>r|r9D)8p{wM)|{N2bWAm0`FIOIDcAA`Ij@=?flME(NucOoBwd|Twt zA>SIg8}coYKZAU8eys?>~3% z3wv|$)HU7t{LXm$bKAqM&u#_a3A7KQw-@1n~qm@gjWOp2>!feJSZPM7tN!X2E z53Q#gmhV(&<>aXZ;81{L_oYl0^!q7K5;75&dq%cl$+55|N%LXJV%(G@yC4Y#Eb*b2+69TF1zchmU{JskbNlEL3y4DjMxoHqCbLeaOiX4? zI~)!B$r@VfnTd2a;|G`~I&CjAPY#;wQLQ;`%^P)EjQZ0Uoydb_!)&JGg()1I3{r6t z>##)0YDLXV?cz;Iue~|8ynsvD74_Pi(WM2<2BTO}^+aOa)$=o5vbk0~L13uepUtVf z%D3wSkQPyFTVv%k-@sK+0AHF;TnR%5D4830tU3%Fp6fJStO8bT3#EQ17L3xd7F`X^ zOYMShTVBAWbwPA#0hibX&xQh)_|Qx3g0ssDxU?>K78J0=+`hEUiLbr+nNYwI-H0!3 z+udt#zH(^+m$c9BwKqRwX#tnC$MLl{KOG8KqCxYecEQt@7I2AOaJ005OYDMgg#woN z&`a%tr!Fnv61(6G6hQMMQl#m4ht=hF&mD9!1G=JQi{xCC6|NK}`Yx%6?Fz>zm$u#Y zwKrb@1uW6u`qDOXzV_y)EG^)Yc5c4*<|i*L;F5ODzV_xPK>NRu)lwU|L}?4^Lr~Vu5;s9;XRK#{{5Hhr`GxO8;4>~(z7%gC!l@n^3*c)otV3tmNHCo@ld93vh zUa*ALl?$})9@*M_Ho$WFywww^1Wl9iFxwQ^ZaS@tWX+w;;$ey^sL8z1e}r0hk5#_+ zUB|!Qzk7PEH@x5>T0y!#E)_wB^PoXAeQ_AqdtuOOoi`qgN? zQ-_KlyM)$jFVMPkl&O;Gem+m;L20xJO_y{d(E^xPgp{0c!AcfqozA{je#E0!(zWEmy+>hIBNRlXqJr*!L|s~h@mwp zYAYqxe1uxJj#a)Ley?}DVhOGDhiF9=eZY(;CeLKSJ6Lz77D_@~=3+W0l1ZKH6jDtu z-jJqh=8;|t7FttK@jou1b#{T)&7+WYWj##rvA$jdS9ZadWGAC!>D)9CR8n?)Fvd$} zX);cc;v@WC;Z@*mw;yZ$o!?$U>+~U7b9J;;4May#nz6DK2RjMc>0Hfj+p`ce=;CnR z5#**O)>!nB-VZ>nZ;c&mecRR&S|=B1MUGHu+GKLJH*@2;E;qAKb1vHDyxrx}Xp^GZ znNjun6IPZIk5ntXPrdz_$6DX~rX{qFAEI@Ma%_(<1{PYMeysKOZ#%WtXWuY7*7_yS zR!bDJI=O9qXdR@B-xtDXtJfbK>wUxfPOJC$`TwaauUt8N$>Gxve&*nr`)}Vb?R|2u z3ik9rZ+Cm=7j_=A{hIA>-+Is1H6Z(32N^Iwj66AVE^=n$O&jF;N5LC`PppmBR##uP zx&uDsQvW#Tp0c?nV*BlttzW(PI{TM-d^%cUz4e8A=bj8kd&YCXXuopm(Y{hA$cGl` zTxLzzIQOJuImZY8+EdF}Jnk&BaO$6XA{gd-j)(c>i?854YC{7>e|kS!V)@WH_bp&37(uT&^-vFf06$t{p$tS2$h=!b(5p{9+Epsr8$AY0?1l@e6jiph(istydEO&E>nt$#wU>F!dFFSM5 z^1u%tK}#%TSm(Y03<)FX=T1H3gCb~&7xvD%%fL`Df^ItXP+t;3ODs8n2s%7=*>ME@ z?5Ril$`N#7-Q{IgAdGVd$8wG%=x0tX=OlubSnBc5?So-p1pV}xi`IjF_y{su4X`1z z6r)+Dnj@^tG@eKgOI(}i(TOZR7AoPiJc#$pT6t=n+XF+w2>Pj05BZ=7s-!DMQ!Ed} zs32+-o$ka6ax52h=HTLMdFr9QB!c8J# zi>B~sJlC8wMw24NNJ(0=F|0u3TU1VL)IbF79J}l|f^IzZXkR&kE-bWt1ZtabLo-t@ z7HxfG;e?zB6ZtsS?x>aNxKfRVhU`f~FKKiYC%3wP_Lsmj2$B!zFsef(@3o2}0^6(J^>KRR%#a_u&GNnmsT6aLrtrTDF zp$tCCOXFdSJ_R2-fXo>d?J8N%U$*ug`%Ib4g&n_zPZT{Eh2R2{6X>X=CBglu4UjVzq zzZ=<$d<5+Iol>|DS46C1BP_z@G@huzib4X4~XYAyVm9*#y-aOoK6n;b=Th#>^ld^vT{NP?H&S z_la-}wqIgnXm?h?yqqI8Yn@&$E#OUNAVkv%P@Jmmr`z>d)0_8+^h!5*D zxE-kGdR-|-P-r(8t8L0x(W;nt+WfegE2F!6i!lbxGN{^TlpR{`by%!a&S9;LH>RBI zEF7mWkMA>dlp{a^tBn_(SX5&jZ`93XgIH!_Ym{sji=d2^OUnvc%*mCyQgzKd0qWSU zKYd{l1RV*Df#P)4kN`IlaaW&=3*49jMdL(|a1vQp9x`>+9J1RxrDVFA}Y^s6kyFUvpc&@n>WV*ADJq95|*#15n2=HK^H@LeI7GG83Pp-3gQMlxAQ<@4Q_6li3f(9upPIq zo5DPn=b9cJ!}AhYHbGOy-11sE6hHXY6IH2dGw&3e1&hEGvuu<=tNCa>Gt-8JX{ta6 zf|8|-$|#W$4@wIQJV_AIHeajOl}Z=f$~Hq#+A^leay904w8|i+xwQrsxbf-1q68P` z2py}hH3})8%~T~qYqqP&!F<3)eV=4?g&`9yx1}*^ia9vD7y~U~X|hr2L^Y2}&+Eks z5zc!9wdaxkfaBONf(q{<{JppH?gg!dJ-Q_7 ziS1O++7y2FY`ZbTM>Uf%%|XiryAw_d)Ny2kj%R&K&o=q29V_}~OE0Tv%+V9A8lEzH z&04#q;SOciPHOXTWGtqui5w^~CsYHQ=P98ZP{nj|=DWj*6~zj*z7#9Z@JV9jLyP0c zRLW>l?wi><$%CFIok1p*(Hdok8^?@Z3p51n#tF@8u_V6p{)GkJvc_DRFVx~Bmtw6n z->R9V0zEBpZkuXk-DpJ;JS!V@^z{>qWK>7n++=PyD$VvRZ_+Wt8XK`dW++SVc4BI) zZInBH*_dwaF2+a%t;C3sn1P(1PgTR8^8TFd5cRNN`16^U7Bf`ZE*9$MaH{|dIB}R2 zWGT;?mdp7Vi#qWM)mYw zD0p3`WNs|Nik&OfZniNgONy69x#kqjnIvr#Wz=eoI{jKskeG^~2H6t6`fc#Q*;y8k z&pVlT%&N?_6q_%O2rJ(LMP6k`@+r{@16fZJ9go&`7ZuUisXa+WUAsCq6qZZ2EGF3v z(UL-?;`L-SUts#4?xC##V>a}ow750Aw?-3IDwnJjMMFa2Ax<_(tB*&m&^j?2u8Y)*yw=^_#?VD zv#t7M;^ch0;*ZnQnWXU}Py;fZmuOy!=bW_K$PI`B*BXke&xFUZ#CzebmoF@6iYV9I zYD}%`^JG-8!dTGDxE+6>lN6}(XH;YHR8VZ&6?XNpCl;XdiZApM;W(VLs-`GiQ1?p# zH?cE2pQm+|L&s8z4NLlP^P3kI`6OXs?UGyIdhxVQ(bPCdkK+COJm(cEZEYMFrBLF= zIf;rKz|iH%5vAl584Yox8!x%tOfwrcK|#7?N$Tflr#T&i3cWg(uMWR)F+>(i=X|xw z%ZXxvvy0$hh-MLoDX|!ti}zL&0kwsWH`$&PwP|B#k9g~%(Vl~(26XL zv-KbhYo!9lnqbJ;j6oKG$fr`EL}M(JCew~N=bHm;-V3WSx=>(dL@xtUnv&&y#T!)7 zA@aM6F=93vlLw$0SY9Q-Nv?&PB`q@`venM8O^T*u*|BO3fL)XU4KYm+J~m)nW$;tt9Gh!kWj#qFsy5z3!ko zPY#j4T38G#_INzU8pW|VubSq(2D(D(sjO>9m4sO6%sEfTI)XZB;A`5#VxZ6g%{t>j z11)ZiA*l_NT6_?+g`0AepAlk*tg@}C;>=uX{a+UrZN?i$dn6Z^hh)3%uh+w$^{Lepf3)y&hbqSj8RYGZ_+dVlca^c__2;!p4OD}}EvcbqdUAp^Hy0(J zInC~&vDheEvGIW>7E4(SJP*5itTBtG=)}OyI4WlhnFPrqpI)d^w1G9w&0HypD~)P4 zB@fk9Y*KW+d8QTbOlzu~SGkHw_zW^$Sje`PcGyuT<)Ln}8O{>(dajuRW`v8<6nHN1 zY9kDr+f8)k+`^(Uw($a^Q=l@hmz+Yqix+EG%kb zGYlw=a@dSpt9PSluMw(wc9zWX6Qvb1)PbK*syUQ4k+&}_syxv!G$W@4A;E&AtQhEQ zSWjt0krN#73d;%SsMYSK#`WRWs}~lcEzs$CLMS(D-8|V~3K@NtV)&HknuEGLu6I3r zGDya3%w1dDU2%N3-Zeliac`PvXk$_sm_%k&mB4$?VZ}?w((Sp5*DEcqO0C?p7=yvph4;Gz!5(V$W@w=k&Nsd>3>w1*lcuD^3(!OglDPq&kxT6eY9EN3+$ z@TtVn^h29*IpPv;O;K8d8Xb)`91b-Nhr7~ZxsmwCHlE6s+^KA>4 z$cKVmZ#5zot{pe(7=1gAqv)~p#s6y#+XGNq8`Pi$e-%rnQ8W?9GO2hh0siF6c2_gUQ%wLB z-Z|RJk`3{ytIDB?4kWeH?}WW_w}N*jhRD#aM?UhlE7P`|2zsFObG{U^ncRHpOvSWD z7)Dlz&j_&GXJtojKxU@$m~2@6vz0JL;52yNlI66HLC*Q9N@Ts9$!9rvE=bMZe~!9QOZk$|Vx9hr23v?VM5XRY(eNS6fws5(Y*l*{W9i zTFdQwGd}1T)jZCQg#`PEryS0o4N{4#@Us=FQJK=!vXmBQt+QSQl-r;5P(JNQ6-uw1 zC5Z%IEMZi=k4?v5-+F0ewD37oo}`Q5VSJ|BMoAryE2YHrEJuP?jK|;DMXriTaOjIE z=RPpymN-*?)l&|C*eRF#x}0)LEC7G)--yN^JmpfC^ePAXZ680weAydOHPOrFOWjPb zk)+u&-%noE8CAR<#e&!9GXm?3i1OJ~0ppCqBfk;#jWHv0nYhE}RZH!K;Z-~#h+(#m zV!ffqSad--i>7Dk4A_bz_r*~xo}|WlC!Me6^VC2EZ`bHqi~;M>4(R-Z(`1-V*H3vP zdVJ`|{r|wzF1vF7>gs1#?^r#z`h5>x^)+uA|9hSP@D&Fi0&D&C zLwrkLeb!d&zq1XF^8&!S|C84~v-Xxnx51Olg5LlwjV}7LddDMLVi0qxAC^eTD-y*_ ztWpw50yNyH59P^_86{`)>13)Ug{x|Q-rQMq#}HL-O4=PhmE)+eIZ8y8knH$kCZ*H) zU{L4=_Nf>b!%`qaY0)(UN`Ru@aOmMzE_KTTRSy^5Dgg=RwQ3*BU54UVdbBZIJpTJB?U-#BMH@;KYGT6lY;2*Jb2PM=oHNM}sYv$tD)% zWVMLl>3SlRI*QoquP@#Uia`Z!XdSRQJLWol!&hTvEl$~{lONaSlVX)7`%Ehtbn~3M z_I#*H?3bb#)p7zWq02m8P=|Pvp49Y))ydLKNLRat*RhH=6{3;F5}eqNm6bA=>lNAJ zYz%TDWY89=Soh)sIS1Yt7XoL>X3=bi4D^lL7Gt=omc-){lO&oQCpn;+jtSN$TxK+j z@p0W~_IlKyJZw zY*q%VrEJNdY6-6~o4G!n7!9ymm6&;ALEpb|F$SRuYH32|hux@EPAKG%ZA{WhifQ13 z8t(Jcfi)9di73ZvdmmmLN04lTWe3dHK>7?{9cNsQv;s*>Ci!7H>d|N*_FKb(TJxrR zi|DwxH43g4^pEc5XWC+6LAdXW3dln~0CW+5lr{$&$EMS&T8tWgBz6GN>lG zG*irEEv40pD+)8l^2Ltjb&`RbcJs-0FpMmE+6cpOlr$$OP& zl=nPiFcT^~oA*K#Swy=Kc+zafc`8}udc00_<2)I)g1;c zam<_N)&8_9xGp2|R-!tb`%0=LN!eC4R6!F{qZ3Eh_=Tu;aXM-6ag@S>RD2fi;Hc){ zmdSJkw%$q5V@hTEt`rsmcI{cvLNuFAF#|GIhP^2swFF@xih)q0Nis311)Ww8b(~JM zfMI=h?c@Y!s`I`xntEnACrX1RXgnDOi7J&~nCevu&M<3+BTsb*+0xe*T?|DtPNycm zY=QoA;~_QxIT=cvZSu23oYaz2s@I}3-6Bb35(@I1g{roo)Y)+vqlc)F?z^pwn5Mvj zl3|zZSt81@K_@rpQIkR*-+0`@LaQ^gT%}?quq;*a8$EO^4=JWam^$dXCdFEOAPyVZ zQDC7FVPT=>i>)k39dmSItbtpWITht>t0&a)m~-=(NfcY4T7l7vlVmTM$(k{DDR3o9cw?f+m(kd41Ufx6qunIOjI#r# zEUw(Sun;E&KT}QClVy-$70nHFap;d5g@TvWn$5m3qbEcvsKv8YX5+Od7VUmH)vCuB zPO0@l^Vwuxn?|J+Im4ayjBi$^d<)b@7IGDFeR1*;xk^sVm8VK0qbS&DOs72Gm{yvF zj?$Q@Ej&7A<6}j1ZJUX77GtpNbkZeDabcb|5E!b+N`o7G3bb()b`lmi!{P zbP#rJY7v+1dNHP!^AzEUqv6g3K4M!?<%5=8c9Ox|D zRr-x0o6BKVsU7!(kl8p`97h3@63rCp5*>ORl=|6hCevkxS}BqCy{Sc{nR1542l%Az zZk!x>9u=~@SI!09ek@w==8Q_r97V@s6fRFn{`3eWe`g^CV*_n;#dYrSXc*L z{kIbfbIdp+v0SSSlRB5OCo;ujnP|tZ5pgj2!KyXL`WC>MW4QLlg+-huu~wd_iQQyf z)447$&?sISOjB&UD>;T46F3c=YI-eqV`pIz_40j^<5<5xaIk`%ol2Qhtepa>hi+Kb zyQ6~Q5;3x`;^OLGLkl4k+Vrq%QIn)I$~D@lMxi#6gQ-~qw@cVu=#}%8_9UFBGk4?m z#c6kzkt$s#$yeGcF_U9yTuHO}Y!00%F4dQMS|g|Eo|SjJT6;0XWW+Q&WiCCW)A@0) zVQ`6bW|&OUis7owDweI;nS>a%85!MpZ!^+X>s6VEWxS{lT#*5BIAK}f|a^HsPS1Yz)HnNdoZK|9jswD7-GG= zuo$_cA&>#+?4SA1WfQo1I%xq$7aP&;@}A<7F0^_ zdc;Kcy483xjpJh3tj|=itTBxy^Nd*{!2(`6RH3W zcqN?+TX{Opw|Z$a>>BYNODQ`ileSoQ^;~@#90%kuu9QT3`=OCREJ~Mpi))v0uFv;3G!t}9Jeo_| zu|PT8el-98(ft2M^ZzgUg6Y(M0B0b#ME?J0p#{H0{{J5>##kc%f6@MzUn2kiH7BZ0 zpZ`xSESAXsU*wf8lmEYH1f+m7BLDyO3yVkd{~yi&e>DI9(ft2M z^Zy^s|9>?9|Iz&aNAv$5&Hq33W5fU2A1?C$hbw!}*?LRl@vFoCH6j218Tdt4+%Q<= z{yuZ{Xo)hxUu~9CCi!q#PHF7xGRtX+^13khcZt%$U;9~6nFnV@#n4Nd7L~>`$9Yy? zHZ6+H1e`)@vx+%V`ueElm29lij#uLtk(?P>wyO8-N=_Lo0`Z755m}$|hW-jd%O~j5r@XJoo^jR}D^+Y3M`n5u?6OUVU))p&TmtjJ;lb?WfkP613 zJUM*iQ&jZFygk-REu89Ug`kZUd9EGnyVNjN^UC^k*6U6ub~_gzarG{0H?W>GF7-#o zxZalV>cBsnF4zohmsC8=W^_d!1*LlSbou#daOjIE>OU|=mpHY5)l(FE*eM$Ox}2g* zY(IiibeSprH9tku{|~1q8h_a2;*yrP2ZhnNZM8b|Pz}%?5eldSvO6jii&54* zBiUz1a=x50(pUk@)HC6vo=lGdW`x&3vQ+MLyWG;?&=*s*|G*So;>`Y4Pto|pPEqvh za*8go{RmFcWv2Ak{1i<+c#2-)ji{hR`@>yD9iphZs7jk7u*MIPW*YXeov0RZr-U2D zMhaw%d-K3OtC^$PBfil+3o3#&u1Xh-nGmlRMUb&4k#>R8a$aRrt&@5W8-m(RgF4-* z7YoAN>y=a`2J+J51+aQw&DM(fVr)jY`6BC@dZlD?-EojR-BmON4*ewm|B0(Nt{mKd zFj{}o-o1Oq?meqF?%%ijeQQ6rbN5bl`>yTs)_b?8&3A5o*V#sVtyF`!f>z7}m5Bj?Pm=~@g?Y6kJc4Rw@+2buv62?q(&g)TP5GLBg_O#=c zdi|sxtu~5HlJK-d|41gZx#*@r{&y}2=Owabap2Z3pRaQ{aipk@jP*O*bSzTr@Qm0f z7u8-da2l#p=N&`IfBTp2-roPg2Q)ZXg7B%f&r-2sDlWCseJjdM2%7a=RvVy{<7uJP zY`9}dKN3%$UVcUzOpQ@)J}QD#yt>Md+ZSrs(;v`a`{jnvS28IsGs;uLC~6j}p)!#` zp+cRgprFEBMNf@`qQ~e*T1nQ%#I!LT>b@ssIg%8EOk6btZCsDGjI>W;BMIX>Mif1x zmW1MDK&muL3zR!la{2||!``#cYdC#FuS@$zXY)MN+#ARumd1Ov z+1O~Zs5VOXYBo2UqLoG;KXPg_uHNVtQe@c)QI=sPvNwuGGdd_WW`Z3OtWT$lRXZ+C z&@=5pRxrRxY-sZxrc`sS{srH|I{qLHI^7S3y|}Gq7%AwM#}g4Rr2T9sGct4zC*~E- zL>($NK#wF@*5`J+#cFg{st?RG?+nL7ucykG=WF$Rr8nz~2B_d0qt5i$;mp^Vt`2Gv zjd?FO?_Qu`NnR|HKF__Us^6a8wpnXF({8|16A&0>ELRmf5j?1|w7a*!xcM{0}#6_NY~=SN#p0qswG zq7#;se7iXYg`hHFtQnIik*KG5_YBb|COvP;x|Wg^hb?kgzCgqFU!K(LAH!sZpL7nnzw*sn}>b+X-^baYC*PW?-YVIp#rqtS%Nd zK{f6$p0!gZoubb08c7*B!q!IqRM9oF*1AB$%FEAdNQd!J$!&6Bx$GK^A}9$BTC$Ov zn67oiiB_wHBT$^OO5ro*XlmBohM~6xU}sxrIxF>5G1K#yi4F>r<0)I~yJ^R2kvXN! zoEc1O<9c?`85QVUmhVz(?*a|TOU`RZI^|N)F3sesSMDUe>9`R1`2h-&-msuz4Yf+z zh&m+NPl`vW+OSh=6go{6e=A?;cH=BNWXi3^s2BI}e0CZJv^Vg0TOPCrCPVW0QI|+E zA=R0QX0h9zkLQ(4DICnYekrs&?P4lJfu!4c+AUWJ@eHH7ddMhq*H?2BP<5=~U7%qr zJg)&2#znz5oq*!&F`~$fnI4W7bpfa1X}L7dbg4E!oH&(C;wa4sGCHl#=xj`>1vXwL z+@aPeRXRbV-HU;mY1+7!V5L#LFM@M5EBZM$U!h7BmMTwM7se#=g$Fd)q2!l(Sdgwd zC_U)=PLrI(rcJ*^%XB%h1bp1BReGVw%KQxnpwt2L0G90 zM13yjI4KZ|6%*z3w$+_W=}bjGqLS4?BO$AUSwGSDye1k{hYgbMIA}+k+8|$9&=P5T zV3i&B3}-dK{v;)&gvykdRm6#OfiJZ4U&F*s3zaT7u|yLse?%sDJEXH-n+&U5Vcd<< zVF^5w@pF+xkH)b=*`3B3S}-%Ugr2TX!QMcFE*FVW%`jRLrm}h3%1!ctenz9`M3Lep zqZCx_io;K)7f9Ir{_`5f#Vjo~c&#_YF(uGJ-mWysXBtLTof)>Cn2UX)ZCHh(s~>?w ztxe-Cn<4B(%nFr`3%=tpO}MQZ*5*6pSaie@ld; ze|%oUQ1eHXYSmMtbjxA5Bq;3_ZN#!(-}S+z8#BFfRpX0NOLUJkRxueT3aaCEOQl+> zEjgKdx7yK03Qqe{XSkCxcOb%qfXmds8;g;ypF44Y|8F3_<4 z{_`68GA}o{a=Yp19m014TUJ2IHi5V>XFOoMVN>TB5~C^0e~OLlX3hU4scShZ-@o(yJI-+72HBHIsW zaHw29PSl2KJ)@cKTvmcY!r}|OEE3s9%pZXMyR#gg9bm3-K~O~Xygoj}gzEXCIn zGR@Gz(9x4+J03)n=4oPb;{gp$wv6%PWSkt!vfTycEC+3_QYLj_WLBqnb>3`hILY_A zohfzHC#UnmpcQ%!R#K9+DmMYI463zxr2(oLHj=X>Pm5Gr7#3s78P{=YL$*I1+I>l^ zFvYfcK|Jh!)A9O$bmfgJhp#=X9DMQM)d%eU=l5^kr}zGO?`3=B?q_yiy8GQbpWeA) zC$s%m+dsaY*!tMkk8EAFdH3cEH=l+43GzJTX^{^`rjaW)-n%i_c*6Sct@qX+xAu-T zYwa6W-?FN&?tzcK^gq=bzjtFzY;T>SA=l9o-M6YYs_Wwn|*jHSq*!(3~hCucnPT4)%~3(p1{*UN)2>134NiTQVfCNvRkY1&(}LWJj!k z{$BG#wDBTZfsR-KJ-p_JXxT-yh>lnR{krCdXwF5n{2j3Z8gmWPV~c3{I${Mh*qR@r zrxww|J7OThUt|>v|Jsr0vct_ z578ToXgND#cdp+(X`XatsV1*7mz^+GXlT7~wC>F{Vel2(VA0nNb&^N4tlgFf7VJdJ z(Ge@4-`4yPZMBG&y(3mYPp$bOT4)h1v?EqP|E&2Tnq?6!q$5^9qpV?iUJ)%@N34K` zSMx*kxFTBCj@a|p?}9X+R{26@UhhBxcE%D+y9@Ffg%^EUSCk_4<|^&;(?lM8HX&ju zqGjob70@?peu(y1M9bU}E1);l{1C0Mh?c1%RzM%D`5~HK5iPhQRzSn6VR}~)trI(9 z1vIXjAEH+k(K?|cc4_^m?fDw+k0L}p>NWB;DB+0Y+;{+|sS3_(Xc>ZGBdEn`>vwN^h! zAXjFTL`&ZhyF-AsL%eemEuCPoLusq+h&@w)wnKyx6D>%v*rAM|>4-gJqqS8F7CS_& zCec!L#GWod+aco1h?Y{Y*rBvt=!o6EvD&ul{{=t~z_~}~K09~8Tzu}hxqWA!oxNxF z@>zb?Is3MmS7sib`P9tP%t{M7wZ*Gz4n@=tA<8kPM- zc8lx+SzLCUY~RUeC-0fOe3GAZPQFe0iu7UWr=&~LlcXBy)WlO0cTQY1!A_Vb4jzAg z{QmK4#h zC4LI1`oEw5S)gr!qegM@=8Cr-a-fce4X;&;xQp`eJ)gLyKitJR%1$IicoNpU+3Ud& zOT7m-a)m(LM+D-8j(fR4c^~THkRqso7G_g*(u0*ebfEX>V`hgy+?fJ#X9&cdE)cg} zAg(D8*XZENY&27~JJAB>4kEmdHucVf(Ju(TAnp%coQ3zgcqiua(%y2r-g@tR9z9YZ z?(G6`^8#^i6No!PAa1ij+~FM@Ukzj6teis2I1Uc!<5q9ae35=uAns;?xX%d0-6Rlq zqd?rJ1>$ZHi2Ia4-1P!+pA?9@P9W|R0&yP~h`Y9nE3u|#3`HFn;R)6n`4ZVPU#7dy z5&XKi?sLTYxVpe`YXWgqfw+o5Tv;HF@8Xi7SQUNGVd|i2)1nPH~K-{4Mafb-R9V`&{R)M&KjvU2BoB4Rd#)skoyMuAr-L;^*_r6~g z1}g}I<=f}~hmCw~WbVw_S7)0uFU?e^Uzp~m{y3GF{Z^Kld}cB!{e?6(@x;Wb<3Amz z#(p$LO1>}gjXpT)5r13k5Pef*1I+*7pB+1~v3AV-FsG2i94)SY{<&kPr>nsb2bRN} zLJHBq-PK@-v&~@+EQM&`>S{2=G3YR7n?f{jb~OO;%pN{vbU7SMyqq?`A*cy~AO|{0 z`Dzs(A#Wx^dA~QOO=QEv9D@qcz|qxUh*QvEjyBgnf8DXu-qm1;1JhwnL4|05b~PB{ zY;~9eQz054T@8jfh8^Z?Rfq<*t_B5z+e3pK2Q(3gINg|4=W#&QvW-=`Dzy}E%p!Rg z>PkCHB{MtBF{}^`tX&O;IE5YNXm$Pb-yJ(GT@8jfupQTZh+Rh=WQYHaNbk z!4STk-(U@n8(xDUjz5Li;GJC!hVbqD25Vp(UV|Y{mW9~B(A8iF-)?MD<&K^D;WZfI zs8)y#bX^UG@a_BtYoHxogCS1Ah1dY{NF(7{a&n8>&IK z{$F-v`XuRXV|$CT|KV5p|I@>tyLnf&TMlK5Xk(vnwjWV0uyj7cwA+qD%63f)(AJ~- z-y<}}z8!`2|D7F$huAeczd`pSU>9iq} z%BXScAP49}g*3G4IvuDIuzJDhH78&TX#h=sfmS?|%16r;j^@Fn+yYFKH*lv}h5xPuQP+O#`X$YBdE-ls;4Zi6qM&<7E!MkcqE ztC-drAZ%M$7^;=rs1LMS%GI>0Xd>;mgW7>iDjQ0K1=cKTUwXS{(dJ#1-HUXH$F%it zc<9y@88VrEk~!T(kCHGm6GkAq*s5 zaZY#AAJbSI9y3)%)#-Y`l>i-&5XuY(V`jJE%@-CcWl{-iHDT4UMPA2&swDBU&E*y?I8_R2#uHd07;e;D!<(eDu-09Y%^Ab2+HNrfr5KrA9*v6yNr_BLVHz} zxQ~Jo!Gwe6;-O&G3^6uV)xc4MGMP~|7r)6g3b}j;YLLFBz(UvUOK;czo7^>G9r?Ow;&Q2K;x*~TlUt_WGWGP-ty7B=%!Gbo z4CMGdQKd(T`q^7ZK(XSYcnpFVx!JCaXJzaiN! zu}j_}eR=fh(OX9sN4=wmiGM5p!c2Ac`{MJ&q%#F`Z&-{4WAq&dhKKcB_<&$5X+%>tSt=0M;1-f0ng!cuT`FWQk-Ee}W z#99Z`?#Rn3@ShReT}FqpRk9aVkBWB97x1f%b2XqnB9=ZK*1c@!`(50 zUR&LgCNiaxjwm=X4Cg^OI$L6jN!pih`jv>r5u#OfbFv`mwzS2_s<*(R7PGQkbz+86 zSta+l;usW&H!^HIj~NTaMBG*{MYD#Hc^>m7Xug=KpvjP5rINd%2GC@?5)gg7 z%i%|La?*&{pqxXF)IyLkV{ew_(KsphrdV31jW8}mLtD9uUGjLB12Zx@#)u(0YdCMx z1yea=CY#Jyqng!?sJgw!F%RO2>!L~<#HsC(U?>iU>oKCh7^{#kUT$cO_HdC)`_UTa zmVKlS>x|8Y#wpM1ji|cq ze=(aDPHSi`0^Z~-4l9G)gd+8e4uH5e{Hv0AE-RB9{D zGB`UmTlg%M@xW1KlyAOirm%l}$dzz(sL~6IAXo*x0x#SeNTih+~Zz%9G&jkl8){%PvPa%7G5) zb=EI8gh5MhU5KZ2s;Yy{H;gfLvKF91Hp=5m!A1Kx-{tUlTui1YPn2quCLSlSKpK|g zO>@Q-^+ke_W+0r@SmJq52g)Ehqr)*wE74DM$G{Ar`E}N*_SaM;Pd2FumRMy5)R11I zfzw^fTGg`}45hVvu1EwTE>jdP6|K&ygUsonG^p}sAZ;~^M;rF@(nq=+L$oX1xvW)| z%qDUTN4^FHqZPB4OTor6;jg(k-j~;_^mKzVkiM9Qu#R-s2R20WQAuZvVOpYY?`nlJ zUR2+Jysk(fYcmJ!h%sK**)bi?rZ_TGA+vcymD8#~xh(OPE=R_jOc$a-L|1eh2|pZ- z(1ez2a2|QFXm-cLb~6-XV^KS$vx8*IkrC0|>p$=O53#3OpzHD>`u~2j%MoIt`i#4r z)a8||HqxwvM|n%c0(J+ORiaiC<;@4m8clV>}vD2ef97 z#YuXLL3zo?!Dy|aVl06g2Wp`uu!42E`+V#vAt5f_#I2gNhV?c~OpUF(D`5syQuNxa zJV;oz!^<8-bAZd zoXg8CE{wsae%c))g4RrNJ!H!?orbg-e4fe@6dzQQR9Nn)TF87*uS`@NIPZa`KHBB* zak-SKrn4vXg|f!&^078kEa9kIN(9QKcs5>RQ96<^K^@?AUo- z4u(w2EtyiqjhoW?IHgs3adQmR`|~DM_MlN-(#Nu_n{#T>!dUmA4@=bD&IDvI%Y(K; zxt6stW{ts|@u@O!h^Wy4BVY8Ca6g<)LbBhr^Z#-2_K~?q=B}UHF-OcDJ^T0Bhi9*w zJ!954d(_OUAp8INnX_j?GYd14=|`t;oL-s^P0OdnQ$L)#Ve0HD66F4m$R3ehFWVs_ zWJgc_ee&VS>n6{b^i3WmeO3C9^yAX)QlIo_>Bz(nCvKd0_XIVuFd-R#bo|EgrSZ_X zd|W*C!?7F2&K@Jjwv3HP9+6xx*&!h$N00s;Wc^<^dd8@4^eFMG;)lc^2P*#W=l_-k zj*;ME@#cmr9yf(D;FX=X){A^7Tkc6fn*60~vry8pi8E7MmAg_fs3 zDiq33U-=FRE)s37*9vybOCULiJxhe5Y~9?m6Gl!I2t4%^fxuHv76?4~B!R$_D1ksK z+%-_W8q1cfe7uI0&7g@(IoaC^W*i$22!(pa387H;xL+vLHSQA%b&li9&~&U?a#)ge zx1XoHrD`_O<4loEBohjq6io_+N<~tk&MpO{k(PJ#BRmus4TBIeRP_wis6l#(dghF9yUMTcLX-+8g z1ZlRrxz(!qq|@Z*0@U>`&Z+KdPSAMtf`({g^H_U=r`u6Ge3_%8xk!Oa&>WLbfe=2m%CKe&<{`=&cLJh1 zp}KUH&X?Pv>n9yCOhW96h^m}|D-?2y9G#2NTstY6rRtSRxyE4Z*{<7M@j|`~v$9O zrA2s}tI}Mg;~JF~*Z@*PN{v?CH;^mkiebjGKZ~hhQ$iJL_+8O57w{Tm2Do6ri!5d& z12Ci64oXi2A~%IbVPm#yeL3lwC=vAELZ_Ew82 zryAd4Ah+oJAQqay=s?;WZE#3UU(y+CNpj1U01*<@$hCXvAdu_b%&taz<((f94KrQW zpM^qK-Wr{iPXS)*SxfCdl^ey*q+4Dcaj=ZbbbF@#2UbUCNHCv3i=581gF4Hntx;jP zRrkh4v39usibc^L=FLuawMy9@Va@n|J6|Tv1}toG1PT_$jkvRVrw=b@eN54!=iSPD zEE{4qRGhc`6J3jQ49{{+CrFqnyId|Ks$=P@inFVDI-e#GD$l#s)-7?BjY;S^yRjBd z`~7ZpIDz9VO=7`1yMWk2RfzSKhy|aLGh0BkfJsPWaY)&|^loTzeRpW_5OoFq$j4E=`1g)qP6-g$Wr8z}s4*(M1xa6A!0*BK` zEoZR=tY&9>akVXv$3 zK#rt5RxiI`i=n(py~Sh*`7-)+kqEO|B|!!&sqmW)Eq2B*2MnSL+-Rnl;SI2hvSq4D zd&S2%j5(by11eAQRX(Y-rNe5Z$!6JNBVb_a3pQWLMkT@y)Cm&mOjg9FS7SJ=s`zPV z(*zRx+9n&Aq|?3h?jo)2UZg`5zWApXsj}}PHT+jD(jgL@I*W9eMg7M6@t(Ozp%sgC z?dQ!mdnZ*YwS6BO7u=qthPCKmzp?7FCKB#!gUpd3x3^Miu+=D)%hfch(87X+eDinG zT3L;Uy`U5#3*rFU52KvTwT0VKb+U!3#p^S1O2Y!_FLFLInaJbm(iYU{v4@Cy-m5`$ z?tChMX-ZhtsZy82R#y^=HUj#bMc^U@m)?&5pO_gbiQLk$C&%uUJp^j_Uo5N1!k~)( zp_8voJ_+jhUpsm3Bs24unMY^tocYMij+y9;dFH5@$?0dOzcYQ)bZeTQ4veM7t{Jn> z{Q{if&q)3tc}Q}LX34~su1 zzGQBfxFJ4Cyj8qOED}8>x_7QD`h@6w5i4?!|86cMy-C`V@>0DtAU$Z}m5HCtZJpaZ zao5CE6H62H1Tt~-_;=^V#%~_KXuLA^?(yLGapQ-My*90%K4|Kdsh>^VHFee0yQfl9 zb`YU3Bl`pBMIfJ?lRh^wBYk-GSF`uceroo8v!z-8takQ5(Sm62$&)A1Noex$NnnrT z-|YMEp`%;HYrdsH2A$~D0aQrz$^fcb^w$AYrRe4URJ~F3(g3PK^p^otz39aORGsM0 z1E^Zj3j?Sc(eneSD$$?%Q*}nsa|5V)(H{p;wW2=^phBYG51?v9zZ*bRi+($RsuDdr zfT|Szrax6{5dC@pRVR9802LDbY5-L&dU^m=Df(r9Dr6A-VgS`BdTIbwC;IsSs$TTu z0IF8>!~iNJdVBy?BYJEARW1720IEv#(*aba=qLTD8l&jP1E_k@qkXAbl}@}@Un-I`csgThq9veW_izNf7TJh)rszxjxK!wDj0aUekWB^qqdTjtzDf;{Dfg@-3 z^WVSz?|pUCYINem`cfgaTJ+5URF&wy0aT^v8~v#&qv-1cs0Pv322k~)dk0W;qI(8V zwW6;KphBXr4xnm8cMqVdMPD93Rf+BzKvjyq)R(H&XvLcbP$BUl1E?DDp#!LD@mmK_ zRpNvD@1z<>e@?Yge9!=@Ui_8;RGs+10aUH{fB{rUy#D~IM!a8NDx}egel&op6@7mI z6%zem097M;WB^qydUya;CHmd~s#5e&Uk^c=6Z&(ijiLt!Pz|E*4xs8q4-BB{MBf=e z)r#&PK!rr#9zfNIzSY?)fB&!FG53@a`g&KZLi^K{$XiE^Yd?2s59-SX8EyS(25Wzs z-rS$2Gxeuw;r=w}#P<2WcjPl8bKjl2VlFp#{M^*+k7qwQTc1T{519Go%&nmFpKoUK z^dG11n!a%Ql<8x^yY;uGE}KeE>8Hj)-2ca9yv!uqd-93NnG{%- z^r(p!C+?m2z(jmPJs}!@aQw>g{P@=KY0&5I`mx3sI(DGsY02j#?~?c>M~prrHdktSvEae6wyZdWX^ zgBVts_4A6Q%peA3?+7<4mY4wy+TPhZ`7y;(dJseJJyfSc(o}ST((qilx&AF|3;2 zrzn;pgBVu%A)r_~RdR~7-$n+ln%<;h>6Aeuta6-Rv2^kvhE>zsrC2&?5W^}@I~7aR z00zw}$2k;B;Xw?m{D3N!LW3Aqc^XkH1qU&#ntXP}5;=%rmE%rSECmKJta985iX~zY z!>YLiE0+9&7*=`Opjh$^Vp!#AynNA0E2p!r;k-EVS^Y}`9Y~z@(f~F zH8qY=EV&0UteP5|6ico_468hSsA9=Eh+&oE-lkY`3|Q0ZRa0YBv1A`K!Yaodpjbi& zF|3*z`zV%>K@6)ry_aIiHh=*XYh4+bH3_c4I*4JF<3x%j%OHkTQv*DanFlfSdRqDy z#gb_dL$9Z$FDRDaK@7c~mi|?-bmAa}UQbJZtynr?5JRu0rB5rCwhdzF^|bVNilwas zCZDR;)6xeNOUDlwLD}nR>5mml#|>iWb)58Z#nL+mG4xIi>5mjk#sSx$T;=I|6-$Oe zBdl^9xM=+#hE-GJA;pqz5W^}@g9lyhplev=IPg>n4H{w9)BqQ)8N{&4(_d38srxg4 z^58vdS_)KB4Psd3xZ4y<%0UdPrpA{POAGz2q%p4Y^rsa|ia{f+^7KuLCHWwRRi3_1 zv2^SphE<-vTCucc5W^}@gNOBF1~IJiGWSzLhfuT6 z(#K@ri=!S6+6=TosS~MX2Mc{FbiBWXW*Kf@DwS8tR#?ZE*c~ocBGu8 z@j}gQvDU)sENe#+*%$*G?G~#O$ZUpl0B<&_;%c%HYSS&s6pq*`F*Q_nXW%+USOKpi z=_XQjPZO@W2)7m^ihNA%Nym_MHLk&Rm?c$n!<{kIoiR|KHjV^RUcU*%R(Y~Y%5xQMHQ3}*1z_p}cANUjex~kS!tOwr0`=?yVR%n6 zwWqm3rbZmzSjMJ{=G{<036+-|_5Qm}eMLW0_ilc;gD}}Jn+J98Nv6gu8)Rymi4W%t`e4vT z2P{@C6-L!briAW864nqP3-{n`w~YqOoQj!LbjDM!{U6k4mx3NbY}IwyG{M!ex~ld;o>Hg^g7+{ zpmmQj^+xU0wzSKwN@YAq-GpiEO43%~0s(#vQ(IuAwVbkGf>dX6?y7y7V>-SQK8^)~29%br{+CZ$h-rY1|j=TeM zXA-P08-@rJTFcbdV8Rv9HHvN`lhfM^cD!MYWkF$3m(>(Zl^L~K?o{jOFy=^hOuhGR zQ-7$Rse2#tV3Wh`BK!pG-;+%3-mu%E$41*A9uGLc!%|q|U`;hFgs)+0bC3$vQ7VHI zb*v1N`9{DA8<|9-=9a@~gYiRhgbSAgbzKuQasjh?uid7;te>fSACD2M!;Ls`%H!Ch zOub=ywJK!S_%dXspu(0l#FsU(`l(8U))VH}* z8A2D#M}jqFzG&~<7Uy=G`qF-;?tQTcAs#>Owc@Tl>bcszQG2zuR`Q`%O$-Hvl*w?z zskSS9=Cz)yVJjl9+UkwG*>2@P!DMg1h-n$E&W4sLYpiaoRC&^b)ogWG)!9I1cboc> zex~kyC9{J&B4u`%aCDEF)f=`~D=V>pHse(J%F!Swu!m+DGPmZeZX`Ji%Vv>k*cho6 za>{Vl;d7ZXDi6+=YmQJ|r70$YI=MUQ?wERJx2Zqa$JDD{W<99SWOAA*=brQm18vyb zB3jf@N^32UGbo)*IqGJMRwA^PsX;A56J&H$Lv?e*&4L<)$!O6|;^w?5UX4Op+=PU4 zWxv+sbeiBoC;wkGH8Rzb9wgZXe)<3R=UmaQGduZ87qFuFA=+lYsTA7R+w(!nzWquJ zYc>@cqL*|h9cY;BcaYpQOy2(5+44g){9chHxI0^(=cB3rR6=l$PraU>T&C;gwhd#2 zL?@LU6uZq9Iw^Vo=o-{cAKJL2L7iULH?x;z>$x;hg!UN4oT+xwqi48QkU_l3RDC(7&K&~ zX+ZooeVi*3WzcSq(U7{T6^>O6B|C~2avlPS5)F?#Zm|gI>;)D9xO9+@bne)$GumnF z?>$hovF+%Erm?TtB503aazk{uUu(&w86M=jHzHMrXF#!Irrc=H`Zwn0I>Ih@fz4oP zipu6qhXS*&6WML;bGJ{{%g#ENy-ZgIML-ECNky;iR>fDzWr-i_7} zU9((hrnDA6U{^q-Psp$os$7{`+p&ac*h@dZ< zutH84RFkmB!VA8btCFavVz9x>CQVgRWw4iwv3S!25kVRSEhkV|2Zz|2Ewx}q;x3TI zXhQ4_v#`X#_N9Y`cuslO>77b0718_+awF3`yg9omm^eLCc+Zqhhjv=Nl6i4r${Jj&}u(v^}WP zYjyQ7@%YRyN-5qhS)7HGVf&pJg#M?=POgx&6M6*n^t^6Bhck4iP z{@6~*@9NW|WcLRAy5b%FShQVQr}Hbz#MyuR4sPkuc>C%)6eeF~KouZxt#8ctU%Mv9 zhTY~Nm9kw>V9ndSE$Fj0$|f!1H_#b6L%MuKfJGoCpilG8!j_;Zl&Yo~A85{Kebdj| zaIJt-(L85HFQ7F}%KP8u;YV?fLd4!Ve`w=5)mDcp@wo?xY^3|b6f zPG@tf_^QJiYn08|dI-tYwM`}xAr~J6Jt5>6#gcm3V2S%^_vRQ|YcMoQ)%_5p%sW0F{K*3K_?Nk z2t|vALZHT*8qFq%S24P($&%gdYudpXRzaR>%U{*|@5_A&ihl zZ_!7T?D?&5H#vI;sWDzQ}mM=J(;a!>@s{JXG>hf%$^L?V(+19hwM4oNmmj z^EjYt*~Them0F57W|6!Lb)}u9l9}C{u{dF@oTFhR7AA_dhBdfqBbpgn0jR{e5sEnQ%B5xH0#h>t7sCmZ4#ou^w0|Ui-fBj&{v@_Wr@8H*iaX=`~S(r6C>h@*$>S?QxC|}lVcNCjvFNpkLE_M>i=5* zZ={Hl|Hi@oKOJf@F@DOs;+y)UBWsMk(Y||9Fq3Da;>P|pF}^CbUZDj4RHAbSTNzUJ z2Kw$fR>o_d4GS65+Ay|^#xp*(Uuon5)<7kiT`LvC+>ELbx`G?)u#*dz!!C+oELIvy zdObcJWTXE2A$lWVlxEdFS9h`RflytL%5ctYPzP+ok~ShZ?N)(>sh%s=5&v; z@<#0#h!(}rG;VK})tJhrjjFOX8d0ucWs@=2ECje9L+ZmZltKdKA`dxqddO6;s0k?N zL)elp=d~#fnc5qy{J!<9+#7DXM_GBJb_~>7rvh59UuCqpS$mVT6}<`DT04fxU#3}& z!*7nVJRDOdOc510s4>W+c_0Mo0-eN3M=q|;=3_aVG7)lQ?FfnaDW9%FMM7n}L6xWRfF7~S(@i6$ zi#sXCmA41v;ilOXb(&MVcjdzePvq69pZ_u&|KCga+-+ph1?yS4Hx+S@vhqf47yp_B z#MiC-p7pG}Dz|nIN?zW>GiqPA^7-poxi{ByPnyenc!uZeRz9zN{(tPqts`@vnDfm3 zdG^Dz=9%Bjv}O#`k58X7EuZ@S)b^<(WcSI6vI8f-JV{SZOK*`uuRkgaz}=!0DIDNjsGAymw3 z2|NA4h|3s?DPx+Zg$+Y`D+H5O8m(px8cuJofe2EyR(IA9UQQ%WI$kHU7w-Ra^@YOe z_p~lNZr`)^|I(9xxa2#}{`S3O>!^<4a27*wd$ppq+e&D>Y#^yZN>dFNvqfXm#G5N_ z-V~2`Q{JQn?+T9j&pGje-~Zz5%%S(69{AAVkG**G2e0JE@4iue_sOrmGO$0$Nu&ev5}(#*>@Fn3DZ7IIdiDI7ANkmGr(Lk$ zrH7vJ_UN%Qx~r~Y}j2u(vk@0^J5o0pyXp^=NQ_hpaXdHk*K{&n+?lgQTFI)e34y9(zba4`{s+P`a z+>?j%DXR-gMv~ez!cuJ0Zgh7A|L$t7>8`u4{lQC`ThE=^ezW-RPEPFvW}6@1|_`@C?#qhGx9rITkax$Hsm-DmLl{AWLN9eU9< z-$NAN+J7h6I=myefTTQjDi;jv(pExUWlB)C;f|o1at308xtx;J2h+t2QFmzrUBMqs z&uqTzgYoMu82j^2ozH2W`^#)idAu-eGfZ zBNg9&Ka!h0hHM?y5!|p6^)P3u)FQ?bq>pjVn6+Y!=IH`OmNiws zbn)5uZkpe8>hmu?_6M>B!jHgSkwB1Ov|3LZU2wG?VVY&WpbaRUxfG<&8jB@O6wlCk zV?)bDmZw`e`oYYrkrzI*@Qs&lx$Rp7 zpndHZKRaOjtk`uss89ca{00VA9tal#g6mva<)HF7T*=05^|+t$fTAR@M#&WF4Xcg9 zXp)nsSO=Z1EYIVw-&uM4Wewm|z5M!Dk3HwNw?8_1$pbWt#b5S(;fV)F_)9NM zegD&6eAgXh3xwGK!MQl6@p)B6L``MmL2uFNbT%E;aL9&%W=^G~+60-smaH>qt=YS# zxa?bhvHXoWI7ye{zkbKnw;cK6f&2Xu`sycMoH%RetnY*SipdrTp8{Igxp7y<4ety0 zIzm}StwHj}qE8cLn5qvpA(o22Y$KEw%oYk|Ezt6G`xbkH^;G+*q1%4S{O#I9fBc>; z4~^`-bjKDecH#s3SAIDM_6iWj0tB1G7!|Hnl%W)Yz&@(%O)vq*=g&0dVL1;XD>OE$ z8PQ{M24B8QPcB^+pStx_(K(_6a$A4&f%Esf=DSaCyXEHUPf7l@TfTh0glvKEl(s2U zAowUDucnPT4)%~3(p1{*UN)2>134NiTQVfCNvRkYl0a(Hn_YAIgYI#`-!Jpu3O*pwe*)?+jb@KvxCETX!owt?|blfk5+yeCtDz# z1PC4?xUxIlu99Y@jZdEToH26hu3shIbMV7h;ISXvdF*B8yFx#D<`aLIAX^|z1PC4? zX0a>yz9sX`hTj#rNAGz2E?!&>{dDx9OAb`W-}`6-y`gy8`(GeiAp8Re9wHR5EBL#& zJbFXrnWImbVI6H73y&S`3U=O1iVN=# zKmFECKR{52`lD9_75857&V>t;*6rKx{;BmTvIW95+NOxgs~KxLZZ&wVJ}aswmAs8{ z5;3&Nu#T#Y=SZ(EYv&;~123E6rkmc2p1kipU$ek32AyN)ee5Tj3?Do4x$LL?fBnD( zh94hJwm=vL5IjUsVpnkGbJrU``?*8Dy6?2~jC=COedd}|#nG20?%#a`=t76_XF zf`k2;kOW)jn$KOjA-21N5@vofr+LzeEylJ0bRIh%#bk_bikHR$B0^txq@DOoX zUBQXbna|ES=P&*6ZJS0S?|tdO++$~apm@iAfB()Szd~QWqe_x35as{`4-v%G6`Z-~ z)*n55$m!8vKY9J>SGkUzIQIF(;b$oR@|(lw`NK7zwr0r|2wwn#NmrI?@+xx~Bxe;G zS}z=}dvi_L0&xb5zHX?KJfdaownVV7ywMYPgdg~h>DG@Pbd3TN-GE)L9R2xSqkq_> zBENFSuCq^}7ReR}O8|n0h!E>){EG1xAAI1r0~5Kk{1144^`T#0aaQ2oG5dady%mZUx}L8 zZ=XW8Ko|iKJVZ24SFqy)_kYgwhbPZ5r%%+}efqZ#z2c`gnirHW?04b(+{;hg{v1ZO zKzINUJVcO8SMW9W%+x>MJoJ$h-f_>np7~z=%MT?Uyz?`Q-=*KmeM7O&_17Iu?gGdD zK=2R|HC@3!zyAKpCexWB!?n*`^1y=Wvfn@PgY*9KwbR~zn6I(VbB}-VSL7~moDT#K zkv-lOd@d2Z?ScCaG959Kc3tz^gG?Ww?nkrF%-%hF>1-&0~*_-8^>wSai%d!A!st2aP{Be&6_2 zb~>xOj7=WVgCQ%>?bCLkUOEhX!z6@96o{n*UuxQm#@< z_k>N#Bm!}x0&!x2IFUfyi0r(Pu8vhW0NW|foJR@TjnJM-t>;=K@s7;(aWR3o=#iti zXmh3RY~(nUqC>v0nX1ujpl4i@Fc>ThcA_xY30?ceazp~R!D%i{u}HvW>5b2qoh}f! zT_CP05Z4fhJ5wO;j4m!4!<+SlH`J_DJ%v&!)$5DdFAJ1+RtHx#RkIFjDB#DPfpCs@ z#<3pX%5D;fyRnN~caA0n;%I@mLztdNAZhFMRh2RCw=K-{GQahC|heNZ6o zVu831baCtY_jZA}+XUi1Cv02W))7}JJ9rOXiP82@HBLB@V(+##evv@j`vu}!0&%+p z;&uweUD(C>O(lYEMs0C-K3k~q*4`o%xR(Ut{vr_fqCniAL0RbZn$Ktd`{RaMpzDuyZ-ZYIi2F(hSH+SvRgIO46*O5W z`O~&+&uw9}`jOh~os} zN&;~^y0{fDZfm`6E$`#&;-muQO$fw|3%ri*Jguy2=k7dRAJ?6y>*JQ^>AK^V1zzu1 zxBlN}M`re&ylm_k(PuYwv-zK}e}M(gDYwM`D9kj(x4iWeBYP^O=|N$PH48Ht*D4S* z#K*sOWusv}y>;+Id;?s&DB&;)4po?G-IUw5{_9poGxkREwz=Yke3{O-OQdW+;y+uM zNu@Lx+ohk@tds&njeG$Q`Ir(eS(~Y-2AohDY6d^43I$Zjax?ShE2RVo zQyyx#>}j*rVCO9ULf(`OG*v+>;ZWu%P%NXK;BwwAY%NOX3e5ymw4rD{t`8=vK6MEX z!uec)31-cFjko(yZ=slRaRp%o(K?sjDa_Oo{i86`5R+-+)}3LkbvMK(?2TDR!!E?v zi)psY2`W|;C;}7ejWq`UO)F{L!c42z;{OUW{hJFj8I790b>NyTZmiZ}yinI`X(Ivx z$pRTCMK%zlma>xuk~JksJf2z5Q2d)N%oGHvG8I!sV{vg?K*42iTCZ$YSi1>s;8d24 zdhMDf4!Ls)ZxK|RGem8%8bq1$UL9iF;wbuJb*`d~MMHcLrJGH+7v(L4xhb#^YWvdL z^%twHk_Zn0DQ@BUUzSZ0pgbML>bNDyWiIZWnq}X@Q=Q8d4fT zoGgH>d87BIf?02+Vh`186_g{w`cm4aOH>gooXfCPV}jv~?kx#dMUSQ4^va{XMipet zhx8PYab?Xe4_Ts!La>4veF0FFs}Q4*H0IYtiC`*cu!l@`Zi~YKj@_AP!xf+^M5C%^ zbE;x3sx{@QKoYju!&MVdr)}y=o@Y{dP)WA^Km|%nuW{*M<-MEPwWGcA&X0(Oc}(57 zm8V<1^43^OYpjkqM@K;oJ#eM}fz{C&63i!_DRibC)YhY;QGshuL&1~xjf4jl)fzeg0vwuOuPin^cxfJ z9RKV1o#V}M=opZ(0y`LdPxeI@@Yh>)mlSd{; z#gm6zx^+>vTfig`z@t925Nd`w&g;(xO>Chz4B&ME<6_z7YmZ;lcK9cy;2+?J3ve}; zuesTD*o5;jGwA|_<7ojfuq_o3s&ms;GP|sDXu&q)f;8dYM``+5z)lrHZS(sqRyf3! zLO9c`q|E`Fw=`gUvFz}Nw=F7H2*?#u4$@msrK8!LGZxDR`U{YXfd+-+7Z*ALfc-iK zw59`fQzq|c$xxBt@GO__tHIdBet+G%sMtMz+kM$+z*;Z!%|NtZE`n$-Yya_=-7jAr zzU%&Y0cl`xDxA-nD7e(jM|+RsUN_*l^x*9Ai^q1^m)*ba*S|Ng@1-tnUEH!ld#*@^ zU4#RP+dK)RX={dh&t_j|Z@d5NTNjU6!OuA9roHNDWLQth8Ex1ymA=MXcK^}4`P=Td z(AlI3Myh6x2&FvroTs-C=NrfGy8j(31h81nOf(p#iI$RPM=(_*N-Q&02pR1+$7l<8q=uNn*E30$G~h|eA^1` zeSTN9Vy?g?CysMYbKTQ_@-DCbBUbQRN_AU~4|(I+6jCbsYl%kR;g{XNc{hLC{VRj9 z*7&Bs+Wm*G5YT{|p}5C|x+^6c7RTs%|M~lm)xW7D;2-WUyVx|5CsLM72CnjPAKP2| z?sW~i?mukz_-*&IaNQZT3d{n_`n9<+7w;1$|?=q!A9wuX z{>#HJyT8IjOC>v2HDyv(qLirghS0n|{J8Xp!;W9vugkvd{upk-xERdl^Ej0Z#L_8$ zU;mBo1#jBAxbF(>5r4f}_t4d%nW^P$X0*|N^kw(&vx2{oWSXgL&Kyd_OXie?Bl=JN zW%uvBo4@V;P(DC0VTUD1dXOY<$|L~0TL?0@TS zul~0C=Q;xZ;r=iZ$dCy#QHGNx+ZJb5TtKs8F42VomQM!RFo!4VaNqG|$xy*89e*?8T zLZmxeb@hMxla7u~9(?V##i)NDrZe#p%wgS`8mM0Ts`*_Fz%QlkAm8ZbeEDEIT@piw zPlT|6m5tl3ebwBq23Xpe=0px|^mTiRjjAW$K6DUaFdL7>uYJ|*t_He&HXdhk`9P1R z$MtB*dqyj)T~)p2RWqo8?O=wd)<}s!ImMQO&5{r96+8THf*GED$u+m#vP<069F6O_ zn3PP&rIx4W@*I9M#K1GjYi|3wUE<~1AX0%3lWaWP73p3_I{Z;+BL2!<;zw4bKYb^F zD!Tb;A zzc&8?xYwXIe`5apxzEo%F!x(?<2iorve|FUK0JGBwl~YpT4yhY=Mj8iX6X%c^G~fS z%)bArUw`(?|E3Xm9yYhRI%kmg9gbvp<%QTB1ur!YIQPWxifggC%Ws@B)U}I~b^C+Y zAIjHcOCqrsQq#O2``4eHim-sk*CrKx&}oXLteYI^VO`Or_n8I7tcI~)SerJp{tEy zNfXz`u%org>1vhO+iF`IgrX*n2BD=5!p+(r4;X}|HV6gnBRd8`)&}7w?b5D6XlQhA z)GnTR5ca->G1Rp|c$p@y4T7Y#dV`Ku_ZP-8)HLzDE^+(bF|2B1$ZFzf3@h3gGTI*x z7(-DT!<_b!9b+hHW0=)0?HWT~qnpt#o@opz$G%`JYh#$!#I-RjX@pbSpAR6+X@ry7 zM|Kb{YJ_R+(k{X`YlIWp#WNG$7v}|ya9k7D2;Za;j%j~BfbfkP;i&eJ9fV(|5sqk= zb`icoBOKN)o|*8znI*3g4r$^VVOAp?)c$+`VMZey&_1$*a84ua*DmcMoYe^Xw2NmZ zyl-yFXoS6*xJEdw5%y?*K7epaBka~bvV(9^Bka;H?IKKTgq_;OGZQBFhFn4;e7z>F z5sqtwuhaf~0O6QM_*(5FI|xTL!Y|b>?IIk}2w$UJJTu{a^K4in{1Q!EBOKBQzgYY8 z0fd7ZVTbmS9fSiKVY_x|7h%6f*rr`PGvR$RtxqFt)xb^(k`Bv@V?u|H5%coHF1sbOEkhKv_Bs}_{AFGtF({oAnecxU#VT%McA$pzCycr zX2Sb!95#*c<(jxg*s2k}O#AZzge@B37ik~aLD;Mj-q0@XB1~z7FV!xdneg87bp}!+ zynU3PMwrlAy;xVP`))_LCVu?v#5c9!dZ8wMz;K<^hU+5jBRhu6qz%^#v`f2&>uQbJ zg}RvSyKSA&hU@v7xHeo@X{|m_SF8K(Emvy&y+9K`pubmW{XM3AWJiB5*ZTWh?b5FP zUZyd7jxJ{VZXz$z`ul86T{l9I4ZodJ*zF5nTexQaO>_S>7n%K?neRe@ga6%j@u^GJXJ+{;>6sb0 zu!>o1W?gIs*RUBUfU;Y~%HR<9mzx(|`WK-|#5MUMt^r^{INyD$~nPGo72J0@hP*s%9e|&2dg; zXDy#0$k1pATerUsSXiSyCVcN|7?@5FiAMW>#xlDzb^slhZAzBH0OzS}$cq)jv(UGXm} z!z;e2o8i@`o~JV0H62Y+DokT|z;txvh@$CegH`!n7T3*pX^!N*K{;RK{Ji#uIcDVrf52P;ngbBOLRi?Y9 zqbVw+X-p>%oJV^pnvO1fh05^-zpb0&MW>##cRHG))|y69n~shgMwQzIzk+^#cM68j zR!4DHq)$<`O{0h+{m7RvDALbIH^}p^K8)es$1V8aalXoMSENr-{!L?eK%^hZ7e)H| zlj?{*W2Bq!#;Iqie0N3q6jkLkzB?lQNS-LtSARj}xpu2=o|m0^rpj|yq)$<#PUE>N z(vM_{B7NzHD$^CCZl=diJws)>E7GT^dZ#hfBK=5?DAEmIQaKu(x;b8S>b$*?K1KaJ zjiMImM^aR|Eqqq}iJjEV@ZwYJ>L~7b@Bq_P;(KZoCl4M+zA{yV{WCxOw)*o&>BC6w zU6g{szNV7g73@<~_0vcm5bQ_tRfGLA-#e!M{Qf`a=DTrfRpq-Y*r(WFtnu9u>__rc zgZ(rAy`}zq;uhUJFFUoO^4t~dQ*6l8c*0aZDE>X<{U1o|C3UsSFv&-yVAun!Sta5?fp5jmg-CVX^<~Y=$ zy37>E5L|p@7TXzI=7mt7>M~QDPM}-gPM5g|>QP;0io*(Y>)GQnFHp?{U1o~o3v{bG zv&-y#_fXurP~`%cRoW%j;uC~iGhF_xoo@4bD$2@Wu{m> zJbJb03@-C*s84m7DHi{9>)Yuv=R-ZJ%S^F&r(4e+mwA?ICg?I#tiI`1b!L~@yKr6H zdZx-nHK+a~4Rmwac9~~D9q7iy<=SyBOa!9Aw8vR-3`n!B8;OnPM$Mx4xY&vjp|1E;Gei zf^I!~TxL-<6Lgs=76WvvIDT^HGb9jR^!Wz7aIP_@K%FpFoKVR zQy>qXi~Sw;O6;PAug%Ic_s{-%4>!RuYHVSiBwMGkdrDYWh#R z*ph6;V07x&JBhgEH*Rax+2hk1mw){xsPRUq@u*7C8rd^C54raih5h0-Go8(!W`@0X z+l)Fz_kS_QUbj7*I-8gbCw9lS^^dA9s*TA`>l@y+J)k;;&orQB+uoC<4evdt&F{CY;fIPQ|2+_wxxAS?hI>eZQB$&h0kPZ3g7!&w(x^(Q|N5|v?&&T zv~3EVqWev;@Z`2BbT%$X#rw9+s#Ew(X0=j#bBq_?nS^&&FYMuT%@+#JSUgrJn24Q=k+Arh z#j<$+wx{T9^0cQczH8f4bc*ixl*M;%dy38`CZ4kRN82_E^}>E89V>ffcOn+|?>lDa z02|HRc_SRe8{i=76h5)yM{lDO^j(>4E;&zpBnj>zYi8r!3!v&qRQE!DT}qf>0Z zeU@t4K=-6#tse{rn(=7P-%awa?jAonz;Wj<)dpIx@Rb6PlA>z8%dvzx$nOOl;P@cd+GOL-qgX zUOTfgFn$|g@Ry(c``HNmbVdMHJOYjrQ?69WjK?fT+sQqz&3JB8WOF`#=nW=qjfVAvXJE~Bi4LC~4tz-` zI}&O+ZzSQ}OgbGzv=w(b^%Q|cT}Ks?ZS>GtEICeilt$VjwZ|t;#7If* zNN4Trn`uvbEJ^u9pYnQYqrOaZYfMLGXp0gor>wcAn~!xGWIf%8MC;_B*zG5**=)8? z2vlQpjQdhSFVk!>WmsZ26c5#{ea0@i2D+SG?K*V!Uzk0$VC3oB=3gt#PLniz^b43h z-mV^?(;Lc#s#}1s4)Scb#wq^}GG`rxLZ#Vn)a3fek&P?G&B&(5oJ=u^LDthLhco@^CY~J;O^ym= zL_C!3$kvF%H#S!}vNEjp@Rm2oSi1gnn}zjS5BOefh2R`fJ|0_doidWzVh1~wHr9Y+UJAeobJ9uJ!2OiF_^KNK4Fz&iNc+5dT z;!&`wT{zo7BrN7bPqj)azMx(5!!xw~qfA|-o!(Tlz3H=@Xb}Vz3ltjzB^#)O{G0sQ z-wiX~8lAL-$Bw{#v8MzE65)t2LP=*Js$GYHsJ{QdcFsIwv>AS6_)A!mFE!UNJP&*g z+zXn(iv1A#2=-bmiCwVp=-ltknHS!+Ag_OHeQQ0le&O0bti5BcJ@?f$`|6XcAD#d3 z>g(s})%1d8_4zAbpZ~$iJ675&_T?v+Kf3(-<@ECNm%hIA_NDgxFD=;@pIrRt;_DaF zi_bTHeg2s7?Z(#Z4`)9z``X#$>;*HA&b;lAFQ10LI^@F7{`_>0fPI0#U~a?7Inw1` zwONUViF(KFWx_+k+_I#Tl&3r9{SMLGY7H!&QXt@jfelZUS*vM+UQ;SQYI`X^o*1(A zMyJ{mofH`>=EDw!XM9B1o(Lu4y?j165nTO&CRoV$%G{_WIP(QFo$;FU^=iXSgogEi zO>tyPaZ+{)ekaVwXljCgI1zN=wT9S~V`af zc(cHzG9&LeF>C}Wn};=VLsLVey7Ft9pp(f^@ubj6N>ZI`dn)CaKa^sKcv2C1ax;iq z<*X&lrvd?Oa8_>B1Vh$gBtWrz0WXaku39ndO%~gMpp@u#WW|gRB1|ceB6&Hg*>T0G z3Hpfwl=b@gSgkUW-ebc3vZEI2lV!8F==Q{W zr9g!mwBxQ&&PC)?mO*qF$3&N=aruRsV4@fulDNa=w0Np*Z_C*&ij8Ea*D}#H zQz{q<<{PX?^wJdy*9PZRnqWLB*+{xsjmRacFr43KwR@;^Hqfp| zquSuCXo8VCQ&ggoEA5C`2~*e=jaDnYs40}l3a*BW^f|n?0-YW4q~whsdU&>ab<>7 zn)6hnInhPstDKc>yXs-CHV>|a)$%Qx zpeMsx8zFBw zQmxh!sYW=L?$q0noINw?9UX{?E2xLA10@g9;JD*-Xh2%}f4rmJ0P#RD@uef|MEHLo$}BNt)-(d}Si& z>NV<2Vr1<@FS8C?=9J{D1`~FX8$~b6Rsluaql)~z(U z-A28pbOc!GTB!RdZE)uLnqW%`r$<399NVO8E~~rlbmrR?r<`*;!Fz7oTnHSHi=N}nVkcLz%px2bO5I~pBIsECz9(HCv>q66EwRHBo8 zPo!@ra&j+LkqIv96iT^<=H#>grfIBNyLh+R8AfnEq_8xdq(dEVg0B)u*3=7xOj%Qy zPCA&ZN1Iod{!b--k7K8vu4Il|F)BB3QLu0&u{AX z1ff=9Ttst zT(2Hk$Syv1593b1O_Fkfkkq#JhJNoPIA+J1VAAg=ON`LZx7cQ=@3bg3Q$Cb#2FF~> z)2sG#*2*w63f6F^*2m>9Yl4YFd(4Czm85;Z%ZU(yXIovWU4;g3rbqCY-d3Y2wEA_* zyglVqHNj9fDkqDzYNUXBv*wo77YrB6OxP_X&6I;K^rf04_N}S9M|%V?zFc#hkgHfS zd3!-mDBO1lE`Asa5S^Gnw1^h&bGb@0~_UsEy<9x-77xpjaJCjkw^? z@cyV+%a0{e6EqN-ppOsK%>&t#9<-w=cfTssb4<4Gi*}2CIS!psmMfWDtJunE_MiP- zP0%%TaK(D7>gdrCtH^eX(So;P;q%o&Clg8ztP!EXwB5sDsyi{GL3;vl*=2UX3Oy-N zrwU0nX&U;&xr~2IS`wn$>T3mK-E=O?NDjaDl!G}mjh2?;t~AL=r5W{u@HZwodug=!#~>ZSS-wwpEkhQ$WWj|y>Soc4&Om|!Ip zu96>HwJ=`1WFqJsdkX_sDG_ZJgEcbDRuV;zjY)YuF)qcABuCMo7NRz(;EiY+K~NJM z^_=d0NpawAp_UfgnV>g-GfA1tDDduX3CQi3rRg_?I@xW<`Lrh3Pn9AfRVmp!)^?*W zE0u6dmU`n{ux5kPTiZVL%5g>+bcoviq;vkX* z#bt3*v>?~3}{BWxz}F0GF}O+Dl5;K{m|me zmj7w}H<#~UzHK=%_szu*8h&GOYwfKDZ26+4e_wk4>|Jx0&HdTpRWsjPxM1}eOD|gg z)T-O~gz=*buQa~FSTYiZCk>y(-af}*1I&v(Yv!T(Z_K}U;hj+D&#x`7wcxG<`#R9` z{dV^18B)*ZPJf1&=jS#=p^|KiX*?SyE8Z~eX-0xd%@em};2oX|*V@Tyxj;+aM9+mU zzYa+VrGAUgN+c;T9oB8jMl+FOmLmPx5MyR)`9a5=s+H~LZfqQ!|M`hTvFk9Ul~!&P zNl_KMG)M@&xUCzFW*tPg5KqQ3>7djh;}uV4p*>;6<%UAj+O}j|DZA(?x4m+!Cv+kW z&YFmuZ4DD?%EMKn;&42ey=Nj3<1Bf9o=bSxa4J`bI2aD+sIf<4%{H$Y9!kas`gJ?i zu>}Xl`zNf*ITJ;Z6?kZ0yuwwoPQqSuB$%&3?&o&_H^)%jqvqT1ujAds8 z_glmz8B6mSmN4Nvc#A z#wYM>b7^V9s%#sOQn=dYYEmbfbF*~Y+e|r&#d0m-jf@5OB1?$I;NV!Cvwty>$kiNa zui~jXaUrBoRibDO& zcQ2CYr20fBz=Rc%?b%1QYFBKSD1X#B>Bh)j_5^~2r6f8Nbtc0yc9TuAB}ZaT$`zB* zuwt^1SvZV2apofviDAU;B$|F#sa9@|?3q}MafNzpt;L06eWGc%C0b&OAqO>?UNlc6 zI>VG34u{8;NaRd-yHcswWlkYtU0b;B9f?*ml?vynR)NOn+!G0zZ;@$=EG2EFNMCH1 zMyXg$7)l9ue-x|WCZ*ZJyAfg}SiSS>NJ13jE`foMkTKRQjVn@7s71YI9xgQ!b<&f_ zB*rpMJGyyym^OZFqGJ>>X5+jl2rU&6|?@o~ZDvC`5g(XM7Q3*Vne zSZZR*?&3MQYHm2W9OrO)oI=GFujad4lnRj@2bmgkF4E)0zNbkzT!o$-ZTtL+#pUZK zS^;y>Q{-G$%H$<&1tnh^XX+_jGC5}-nMjyZsYKJ1!z1=kQxSzM5l(kY8Itv~370hp zry$%Di4RMyrg`bj6A6N+rF6JeXY*8UY;ID%;mF_WCJWtMy3REcwwT`|JBc8Zi(*zy zBJVF3{Jt_}Z5G6+$ixG3~~q+b|NNzy9>m1uLJgLDXESI*8erD`S%&+y4t(`Ifo?gzzy%hDln ziuW_-Osb@eSVEkCV8V(`MJ1&}$=-UqA&|j%w&L$p5>f{)4t9H4OQ)UBG@DMQ=Pnsm z7r%2>w;N$7lli#0(~OkUje$oQS)#c*NAi_y&<+q9B*XKil3)nQ)y;)E4r9n&LR z3dEZpnvi8dp~yin79P|7fxTLdhKp^Ftu<%|QkgY=BGE|Z!vd~Y!*V8GZ##OnR25F* zoMI!ae54&n2a)caOUaZNEhd9frq>;I*m1pNvy~$}JGNOel5@d{CS9@Cr#umFgJdz+yURo-b?S)t}=PE^F(R`8)R%%vfsb>nb zQ*cFg#Lnh43ASXi(xzC(!xM7`VTj?&J)JsT4Xg>19X z;$j@zwPSy+-2@q8;NfikR;*JuOYL63+74PWxFT8uJ(f-T%cA5Tx2&U%onERe<<81? zMR8=4@GjqL^gJOk4-f03qXCP^d02-}Fn0@7B^Rya2bOWwYkcX-AIy}Go}GWZPfP}j z^A}sZ-JKmc>+mUefTv}J5BK~{Ygih@%LBiKq9DC`oTKLp6Td1Jyi7Vc3T68v&XpgV za4J&nxzojnt)F%$d@f7C!^UxcGlyS0Q30R(83*nt^h|CG{F>{lQ(mi7Y59{bI%-Me z&HauLlZF!KX6L>>QIQF=DQiJY7shT_Nh-&uJvO_2?96r}PT#0l&dL?a%Z(VhmpL|3 zku20WuZQwb`MNSHr2HJy3uQX-WPtbP`&=~KB4ocm;*S=k`M=R5Jgs8G8RuPYJj4@Z z&032)lH;B|FUobI!=_peN3tpo{S-a>@d+t9?PT&M){+W@^L1x8o#|C#ouR31_4@(? zH+YIJPp`$`4RwUczhR;%HbJa7${KJIXJBDij>6pXo_V( zXN3qP_`$FQ-Z_y7G=^l}8VZhbnS>H-NR4RUUKqNY)TpnN2)LNWH%nkE{`gWTELBmU2P&H*Yf#htmmU5QzUI$*LU3@y%t(u)ddohO>-A>L_Ov*5+%n_8p`!!c|2R$rTuXdb#ztHlw zGUl$s&!sw1bJ^0*mU4kWIp)d0vug3>S(N`j>zP?TWxQbFF8IsO{!KjsuL+!5LW%1y znBAD7-tf6gpS2y7%rr&i6E%fqszi77^HZegADllpMd9Ovl9|*x&3n_Ao}R?toe4_k zHZjmn5{IUu_u*TS^ky{Z7CI$79#!yKx61N~+yF2498xT-6x{*aiOqh#>^(()v-e!n{|W=BFX!h3|C5UXYyX}cgk zTJF;mS(h9@8afd%Zw@%)9BdBh}0d*-|D(g#&SV ztT6ug0MA>CW*coIH$BBz#*wI|iy~nSvyr|_)RiW$b{z)dYkH@Q=%%L3p4(zwdopwn z-Hm1WUm1wg+;!D0E~k^w)o43;de7-k=hk+~Qv}pJ_?7~nB1^jxa@BO%{fY4BEFsrw zAskS{ftm3`f!rLotIasm7{m95-cFsA%WO+F`z`TqvewMoT*?qn#7bs($AOJ>tD;=& zDILBG`||QjcGs0af8xue1uA@Tu@Emin`)0~C2XPl8T-)Tu$u#xL6hdk7NOn>)g$!` z$40r~AkrEUoX1WHj!3cW%7nvSALDPiMEBU=@mLd$mW|?wc7wA!{dtiSLw23pQ`D}* zxVu9-Wl-bp&eH6;EncTL?#u_p-4wgR(8J`9{ct#K1AF3VXGMgcQY0x|zBBQ>!)meb z+XwdI;{=YYu8U*~WnL&M^&a#~b+-uL=jGm{L9H5rTEHA$w5V4PC{O?|!RAh1JMiAR z>l1q_xa?_rvX>M6QRO({vxnqlA~;PPapQ{ec2iQdByIl=bN!1YwRh-4Rk>g7sog;X zu)Xhwf{K2E(~ZS$J#@OUv>Fth=nA1G8F!P7Mpuf(1ldzjnu&xrc%nVbI^_ZF_YwKC z-})&P6W!Jc(kCT~l@ns57j(yRS;-n`z&DngLA)KSS?!~;JD%)P$)SVc6J0#o;4OkB zoMnidx85jbg>=8#iFx~MAhy{p%cZeL-)&y)I-G9Q@Bf#~{NBv^8`n41KCrgA`r%b} zQ!vJJX!VMS=~trwDWQ0`7xLOFVTa>uy-ul*7_YhiNO# z;=Mw-(F+BeR zW6k>WsdUmN3prDNSW7gmM6_9R%5BCP8<|2jQxUhw33F*nQrW<+zf3oqNtKF?VKUYV zJKeqbD3JBrJmFEU0Pnudp-}JoZB|R!u8iZujHAa(66|zM>+}t}EAOAF)BBG|-!Sos zz?fosQp~4#Nng2J6Yx~LWi7$Cj_!fU8V)tcwksNG%PO6!Mkjxc=O3jkhaeo_JX0K(;c(d8Rlx13FJZH5YWAgl=VLcAlNZ&cDUQu(ZrupAsLm79eH_98&NIa!8sgT=phnesqPjKiaGoiS&!}$Q05z%3 z6Va_{m-9?t5Y#wr8G((oJS9A*Nm>DYzrBm86Q*YCq|8`8`Ui=S$6H!nW6*n;``x5Cr^?lK$$_kb&~``~`V2NweK56?4mkIq$QAD-rrGf59-ppca3mkqBlg8*vvS_PO<5sxX_nCW@A(!cpf>JN%im`-O8Aq&8?^~7I1G@C?i^_@KOvRSL zLqv*e5LLxx?`G*rnK)^)4k%c&38tOed<~DUGo&5_o2=>5aWUx4B4%+Z9LI93dK>#!0?L*V{faL^w~@VX`W@Y0r7cQjRCG$+6$9>GkQ+ z3ts~4em|h;&5Z^gDM|ND~8@PxC9KUhAdC7Ns5wh7~5_m$=H{BuP+RiAp#;CoSSA-061vaFbiI zk6WCqJIJ;tdOf=I?pvgo==IQUkH;CWxdgkHw>$dq#eY8@uMiR`WrJ;3!cV8Fu4p(> zQ~ZfEoAxMFT~^}=_SdaTul3+@#)S-fGIBDQu*s2H+Kltfd@{)goN_Q7GG{9JuC)Vq znlxiWnwK1-Ak5W`WO%6emz3LGy7caQzMb?}jLBK6xkCi$_E3!HYlDi}5C~BO|axRJKG>c-_nt%y}$=FFXexcs$b?H51zKX?&Vy>I>bfu856sWT&-PM4X z@A-#ivqDGuU2CT0tOxCuY9gGK{bqaG&h$o5@6Rc>U#Cm&A@h|axH?(;$!u2W<%^jX zmnnH8sW{Abr7NM%U{GuMqj=0LhsNoyk7BHFQ*1Dy&XZ8@wYu~kGGBS+bg0>F7`i(q^_(=W->6QabV~u6(evHDsFbeEmDzsF=n|@QTyp;nIC_GgD?y_8qXUnZ#%P6TL6i zrT36|(iJWzc)>NU^{A}Xsd$^w7{l`6ggfH0Fhf`y6K)uwf}5$ZSaXgyqhTsP=vF6s z9lG=$GGEyhrB$;HnGD>=R_uthTo2t&UdU@q`n=ZYF%#*{8cf<~Dn_`(^N6prdj0gC7e3Q&~LeVl??vxyPS}os^i*$>= zW;YbDI>zd}0rgsS={;n=!sAv}W+J_cIo}%b31>f*>5+UlSRqNPzY8}=2Hae|G?pS| zipcv*uu=^fQSV1kuSJ*ML*|WCsp^cI1&JH>dLhCavLzBHB|aLq1hQny25a+(?ox9T z=DvryHnE92DzqR&B~qvcP-`#^z1-rNjyM9C752g7y6W%i`v0m)*HCgaUkQR#@|IvI{uVqx`uC$`1d=;_D z<90314Mlzgw``50bS&UG8BE2Ct<6fhSjCwdRkq62cHKi*;9L9-$t6-y?{&)USL@Py z$b9ALc)W?upww>^tD`{|K2Wt&qEf4S;_a|6EN^yLSZ2rPam#o=T1}>$xnYqnLcI>< z_7l4F9x`7=98TY6z2)8n)|KvviCT6Z}CuUBuN_h18*q#J2H8JLZ(y1hV zV71~Y)(}yG*c8ie=<`#Y$9wQ@gJ}p(m6*v%gHe?az#{I^kf5Z({p_BvxGMjnOY!dw zY*lJ!!Q!Wu_h5?D)1R0Llk=@4WOQ(1rcFq3ZQ4w1_5yAyycu@Gl8J*>CW_0cqL@?I z8k6Jd_z>3QI_t#Dku(UyZHq&KPO{CGITG&ra<(Dor{MC(s7`c~f|-mD!fDP?ByGJe z>xq!1K2uk4uIOoW^U3iE`{;y=a61N`*ub4_7aN_Hht`u2jk-Qb%v{}0%$&lR&RyIc zT(VgI+0f@Ej;0O7DH1cErbs$EF>_xm{wy)`=}pWe37k4~BUC~dw-bEK?n`oJZ`GET zyl~OT3?Jpq?QpP}h$cq^Yh4hsE$`XyzYyWJ3agFq(}Xk8s?si|-}h$))_x-Fab%@v zx>bT3uE(KZMB1#$wraM+n`_P7$P*|@n_~}?G5503I#+ITN-&(aP_BA;l+?HX0(D*8 zf6+g+f^PGp*=gR0ovZ1Fa5xUi1E1!$t=>gZ?(S2W%QwJN4W^i{4!8|ZUL~qC+0M;L zPt(LPQGRx(;gBllpVVnM`{paC>nuy6!`dijbxY2HH+oB(Zv`t}r#%fTJBt1|73T)r zfTUU_7Oue2O&bx7l4+OOSvmVHh$J~+vn3nHxfNJ_FiWJeK64({9hCcC#}ICyFID*t ztU47qQ6>BETP0kt@P`M^O`$EvxW2U&O@w<^($dN(o4pDPi{+Nsa9&?ZH0nBxyjNFG zEvu3Di-p-~-1Xc=o_&Ah9S}Vs`7Qkk2P`iE3wi`LDxVppOKxg+4PK3k7Ov2@l4WuMn3`8<9PnW7# zY-E%3c7;qUA`N>y0B~y3U;&h#2MiDQWrMaxHN%UF+Ys>Aesnv{| zo7Rkw@X-!OwG$Cf*yW0EU?+ou*v`vxm1qlTNn-+?^#2$o&=+(Ht*`;X0i zVlFiM(5w#%|CImTa%ThMdD=Kl-uE;~*+-|--g0Ms!eEM2?`cxD^)Qeo4CuT(a94%6 z)f4lM3q7-m9lM1V?XZ>tzIMl7Z&!yQUmy~<6s3)NsbMKl3YBO_11&e*?ZPrrxon29 z6fCg1xgvy%4VyP81-*1s4};o-!4$1dlSHnEL3P4lU=4e6{`kn!Suy*TaCHFn|Rrow;(c>?yX# z&S=BW5OCK})>?FiVu@OA(5j6`I6Ujgs${zxwqlT=I|XN}%$cLM6kNzqhOjc2?36`s z#4Hp9OT{ncVXB8CdUS^LEq9hD45nyxV~U)3Jq$_{22-@UF-5Mt9tPZm!4$1-Op!sa zhe2_|V2W0!Nt)Ng;N}T~dfDo1^R=+xU`!!*B|s9PPBL4qdP-C(Alhr$sx2c)<({Y7 z+?XPvUJrx9gh7&z1!6sTY#4mmB#C9N8MTEQ%$RfM{lz*P@C8P+!1NO&rPw#7NU%RT z;ro_5Z<;WeqScKl67TgexN*W@idHwKNXOU1;AImAQ?xow2EHB!H%u603IlGG%LZsu zYXHxf@-@wb%OS{PtGy`mR?_2BgeWAxo`-vu;=13LjKl zq1q@Xr;<3;W8*arH*-WY5DrvRgN-S&^^eXTzvWJL!eEM4H>Swq*TaCBFqop%jVW^b z^)Sdy7);UXH0k_$7-T05;8CM)IuIa*Xo=w)Qb%xf0!h{ta*t?+g%wWCO$kaT}EX2QVD#`Ekz!5Pu-cP8__1T3%u-^$j;CU`D~%|}wc{J3C?o2oRu5g-bq zVA<5KI2s*iEF1Ta@nA3^F=95;;Nv|{E!(9;q8iDk>_@Kz+;V4H8w35VZcMTHKo5h| zguxW8Zk*HCjknyHoG_T8)s1ud;^me*=?Q}=T0OUGvA5irm@t^)t;)GwlY-~}&GHM6 z%&b4L{`mT1>yNHKvi|V;L+cN&Kd^q^`n~J-tlzzU7u*|g>-uoLwO(1jd7W8LtOwSw zhjsb!^()qouOC}KZ{4^)v-ZT=<7SL>qu0FE*@ajXW53WA2 zdLP`2aL?-9t9Py5v3l$3aJ98sS-p9cSxu}4RRui~p$tR7!IwtC*Gadl?piIvA! z9$R^I<&l+#m+|E*mX9wVTRv~uxIDA;#M0wSk1ah4cQZV^^w83SOAjpFw{-8)Jxg~l z-L-Vb(ydFwr4}rTd-D>rlvoNZUB6^s!k4aCI=*yl>AWT5(#+x$i;u%~kVh9ES$uf$ zp~VLmA6UF^@!rLI7VloXYw?c7TNj6mt;NdX&5O)pVll9I{i1ylU%X=R_~Nm}^A?SZ zGjR9Bk>y36J zZoI;H+<44*p3!KWF+5>--0&D&fqBI6u;C%YgN6qT_ZjXr++(;K?#Q^qa4W3J&@xmE zHyap3!VoZAZ?GG1!xe_(hGT~F3`WBYcmg~Q9s`epN5I42A@Cr00Ne*pcf1GO4ekPW zfLp;3v_J*i3>c6A0dPIA0~}ldj)P<1JYWPf*b~^}*kjnEaP8+|>>=zy>;bp~~8EX><;W!Y>2h63U)KbUNLjvd3!!;IL>!V?RRFFdyJ=)xl_53M{1 z_mPEvvBvqT?=lf?`_`($n#}|$*oVQ?H zn3;cK{_*+8<{zDZ1g>p8H2>iI1M~OI-#dTL{N3|+&EGM9>-=!OHD8&(d7hb1%m?PL zKfUBNd-v(zfA-t|FC$=>pPMuMC-^$T?}D!({0?{&;kUq75k3ySg781U-y{4c_&bFE z4!(@=|AH?e{8#Wrg#Q8_LHHQ>0>W>AzeV`Z;PVLo349LWKZ4I9{5tp>gkJ-nLHHF8T?pR~?nC%K@J@t(3jPS; z1KF#z76~~!rueGh46R5ZzB91@EZu<3f_e9E#TJ?!Z{P+o58Ok z{B7_?gueyefbciLuOfUC_!We|4t^Qo-QX0$Ujug`d?UCM;Tymf!e0fiNBGO&bqG&^ z*CM<9O2J_F~V1Z5yDr1A;K{jARK`{!XZ!)4nPlKA9N8a zpo6dn+6X(Kg|H2p2wOl#*aQuPGN>bL0106o)DYG{6=4-r5LSSQPy_-(0q_WUP)1k= zC4?N{5EelZ;mzP?gauGQcoVn@;f>%%gg1bfAPAM`#DvA+&*O5n8}Y5t_j@2r2Lqgd}(|LL4{{ zZUQ^PlfZ`11gr>801LvafEnSHfI@f$AQ4^$2!t;JIKmCEiSSZz65++bgzz}H8sQ7U z34|Acs}Q~bT#4{Pa0SBWfy)tI04_s#47>>8bHN6}=YUHQo)0cT_$+WS!e@fx2%iC7 zi10jc5yExw0)(sJLWC>e`3RT6^AIk93lJ`XV+f7lxhfiQ!?O{dG@OsnWOx?Bs}0XY zc*5`ugjc~QG4=CT7}gP9ZdgNjnPC;-iwr9WHw?=NFEuP7yu`4G@M8EJr^>(3U_f}0 z0U&&V0YiA9VF4jzjqrJfIfNG&W)U7U%piOYcoN~W!H*E04}OU7S>Oib7*z&9{tAMno@vJdzt4A}?#BZlk)zK$XLfUjZ5KHyOd*#~?TL-qk* z!H|8x-($!=;L8}Y5BL&>>;t}tA^U(wFk~O_1q|5-d>%vg0iVN=eZXfiWFPQ17_txe z42J9jK8+#!fKOq_KH!rWvJdzKhU^1Ajv@Pizrv7xz(d&ARr`RCVP8Y|QS4EKAHlwg z@Wa?w5Pk^zdxRgv{tn@T*q0H00Q(ZcKgYg^@XxSE5WXM#0>bxUe~a(|?DGi!1p6Gq zKgK?b@V(gIAbbz@8HD#^47f$+`P2N3=? z_U8zH3;Q#Kzlpsc;csB?L-;1_PZ9n)_5i|P!~O)}8?iq|_y+8~2!9oO55ix;?nn5` z*t-#)!rq1Q|L5L4vocsbfxQ|2^0R+G8-f3?M*ya0J##xf>zpnN@817FsFhO>-2ZSu zVd@_zJ*!+BL@KRNhK+~eNoBo`G&-yO1A8|lq3o-s+=E49VcOKoC;5_Rk)E}$-KQ@- zi?rArho)!MnVc<>E7cuBwMJ4r8IN&shp*p}lIdqUG<*>-T%P)&{M3Ss|`=HhK)Wy#rpWc+kd8beRI$O2@Ss6EsDI#<)@H%%r(m+ z)huavN$FJ<@ICy+NSXfahQJ*rAu06q7%j%Isx$+<9a`z~Z%ZyxOgAa&#vuCHu0( z5tdlo?vm+fB}pixKRmRXampF=;{3QXh{?fXBj+uy6z+6Zp?F~AQTy|PFPY|z*iR?&rg*`4P~;tS%+J%4@4xd# zcTSJ_At?)aXxZ@|tf> zC)i2AiTRul$Bt@w)BWmJ_pNF!Z<=K8`@Z}2>ptgqemdv;&;I}Y;f^BMAkd?rkr=mu zMji-zkhBoPAa(6gjhreV&ZSVQu(8nOV@;tHWR`_tqps*uL#^O-g}~80=e6ahFXy%E zXAhRP^v&2+%P{j*6q>|>!;ddK%NXr zY&Z=o;gE1;u!@BtJa{}6wUOrx(wBELb9=aBgw9awOz~bl$e;!EXgWpbWGD!<)Xq#~ zs!s<_m*Y#q7%V<2Hju}YDb~ScUhTNJm5W{0jiTV|;sTbT>M*XVz@C%Z^3yl(|F0dt z`{?e6PTq6vJM%w$`X{e}e0X~ITf@@JQi*uhJjz zAZzy``+!$%FE>2BLwn^{Wq4}Tp6_&Db9#@6w8CZ%+%e7hcDji zhrG!A)EMF+BgOLU>gg`>f+%F~kO6**#V+bK;JIQKxvWHY5b`Ao(I|w|2QDzd=009Z z*m7L#aWraJWX~d^68Q3Nfz^RFirHMjwZ^z(97Gx&9CW}87TWxvML%wJyppQUsjM+6 zH|q>qe$*kDhR9lz+RT&FR;N>HxN32cc~!bcDKY6MjXYHMK5l`f<)`O%mq#J{tfk#8 zqmUiYnJ&-nc80Wl$0xKyXT&W#dRAq7d!_vjS<=N~7v~OmKP`5VpS8V%QOHWQ+VhVC z#*b_?E|zRd8t}1g*~$cs*fgA%X7@RIv5fH;4t>1=8R3rK*kN& z#UIz|l%sZ-N)_)g^y9fL$c!!4X{KB9)r#H;Th09@`sJtRmUsIWarB;}4C0 zy1yiUGku!Bdj6c7Uv~dr-}uq|MZI~Wa`JQcMz{InZ@uzoZ+`sf!*_n|%4eOlu6}fx z`sSD2B5wcv%Q!Ol;FTLo7yc`!4HNBm#ytYW>vB;W0rP$?z(Cg%E$sBTvDkMKeO#a- zoSW58{VvFYkk0pMpaKV8KNChy%}=U$Wi%Xlay!Tm?mG}AjLO18frol*E2d_+&W+m5 zIW*q7X1((nPdP9+Mn-XeNKlG`;%S=ndP5cnfRA?O-r43Q9O{sm0>Mfnib#Dxve4Z|3pxyujGva20*Zq3@xD zz!W{7(K;nMB1V%k-YvqEQ*w$bJ{U=rb6|pf+=G)o{dpPqzefkLNgnmsYAHlR7pCO zA*ox;y~HM!meiXiW`}R)axoF869il{#X%1>aX6~D^xOiPjy|^^b`6=5sDVxxO497A z7{1*f?%sdOK`0MO;|VcF=_rEiStNj1ttV9(s2-5P&N6I)>m@*XZi zo`IZp=C@h`0(U1mvBlx$$6ksq11AAJT2^ZS&XQ9oaw3>B%rfJZF<$f)Zct#XIL$<0NS^vwnb4~>z8qGD z1Ex>v54o6R#w`+MkuFOjM0UO&UVqAg%~~Q>rV`r0 z#Xvx3z{o>zU?OxFNo$qNTAZbcS!gih;wjp4G-s&N%{T&Ul;9V-)dZw!CAGlJGp(ap zz7O6lR z&h#;;x0KEzamL1|ep*OPeF3J6sFqb_xEyL))lfBSFzQEURl(+9xkBRw$YopPLnBPu z-L8YBT}_;NP1xNYCy(Cz;PGm{I6iGi9X)jxeuy!QSQ$_xP{p-UeK>0XI$iGyQMXg? zXBwrCgwq~8L6nvv7Z-Ii7aR6Qjec{{FPI$I<-!&=>}OM`)iBB^pFA9oj1%3-(LXvS<%Muv_9NC{`O{h8L&Y0Z}%tN5^Kc z38URZ8K7=Wuufa5qfUQRheASzxom#OF)ZG(w#SD%?|I6BHp5!W%VT)da~W@{ zf*X|u))BRoYQV7yHt~7{RzcV1bo(?J&4U^X$*Bso8%|ks_$I?7rc=X9O$IagP}PTS z7MFpk@epca`am!}4DNUdnP|dft0M0Hm8Tq3upTZ3t}j}sH7byj4G*ZPJ0l2;gkxN2 zbUG|AmgiK2YJ7WH&nXQM~d}cl}5|q!$Ge@j01gQ zcd6M-Y(-Ofi^J95xc&E!PcCCE^!pv-X9$$;c!xk+ObGKwX%$(`m?}Ws% zPoU;3oFiLpaiZP(%}=+*vL8Kd3p1_e)z=+>A=(9}88}79`LICd!tkvsL`5yNG$*NM3OnOL?m%(G$0>3_E5?42rg2Gva@*6 zn}l0!aphk<<)Bz;sp-=OrdipfClOqm5fwzHtL;j;9?u)iX0z->zC1@xhj@frS|gB6 z&YITQp#{bvJFtuZX}7cL(40mk3zhOqP=tAikPZsdPUZ)a7z`(}Om1=;f!r1L$Ga^n@lf>@9-cd=?XiGuq;C>$jeAP=ju%Kdb2xT7xTUg@Bk` z>)#N|wA-&5R=L)n_N#+x*_EMFmO`Xbu9}3U2vr%0V1>e}b&OvO0#BIQVZIK}m|~yk z<*@=D(l*t~rxLsg$=!H9ZFjeylHUCz55D%(cJX$*ke~(_RL1%Gs|G06LeM%6L@d$~ z)COp_)hvg4tm(SKh|npM27bPNtpg*tH|w|N3v_Nd^Ca@xRKhTW3Q%?P2qpHUT@oHj zW8Rrii0H(EE|{=MZcpB~-;?wB#wC|e<7a?edo(>|FhgvzH#dx-}+OxDkuN&A{;^}@_-+n;`9J)V$+attb#w9%lLv9z zNIS63;*&W;h}$#4sa1`HI4FbwXu?p;`j8j3b$DphU~3^Z6E}||S0DB|G>#kmlVk;( z>;YTvfmD7f5XF;1hYO7cGMSDGz^LtqR9TghxgdB+K?156Ve?Po6>R(vn6A_@%Kg-$Gb{F7(}8|?v$ z5lW(|65{3h=~+c5bQev%g;S6OYt%>Ng~Qi5hs8Wi%VEbW*zq2)-GQuuajVeGO{O&O zv#NyQ^8%QQFwcoGKMK)UL568zQgulFNw|Uy_kc}(sWOaQPUJPNBvlljP4ZjqG2WRr zdWOJ){c*D4939hG#WSSEdlaEK<_|hw2d=OdpavI{FBiNcC-iV z2+)X08w~qw(;QfFvp!u+L=aVp=?tuv`ZYjw`$Ln7{W$Mn-U`;+0~QLW4S?-Wn;=|f z2EoW(WMIS50eZBsbYu~c3fu`bwE<}f|75s=9X@Q1`Ft|=6A4xEh&Sv`Z@b@m8rN3* z5o}iAVTy`eza3&z#l&2I>^gIdv~|S>m`x2+emPOGu z!Ff@abG9E?!G2&5SX&x4cx2jgq2i!3ExUkL#UxUO^r$E0<76*v_RB1QknSLFvi=IT zzXvQBmGdJCzROe@r8w!E{-lG4Ris^wI^fLbV840~*cnUZNAmEbH^;Izof1NH!&Vg8A-f>ri_JwQB|eWUc>^n=9B~+yl0zdhE2_>X-U0moz!t^tgE7%ot+O zq$`z9bvS^A(V_s_#XA3y8!Omf*aP;EEyPEzuV8E+*7tq$Cs_oIm>ySFk_12kap`le7O|1^c)5 zfIVbqa`v?=*pKf4d&th@>}yuAKd}ewAv=?^#R~Rs?g4wq&g5*qg8eV|SUBnqHuc)W z1*w7g8D^`63=?N45tg;6Rc(LJOPcC1ng(H`An<3i73_bp2W)+SYB-x#<_#y3M1>hJ zdfFUe!c^tSfr$bHQvhJUlAkvq{8_ew{qa3u!%|yph=Suci7Ezk)kN;&HHPbsqz=uF zaL=l=x*dA%CAkaCS-OJ#u{~gYbFrwwX$OxN3U2#2j<+?AS4%+Cs};KSvW&>Zez#|+ z0K}h7SFnF$57-%IW)csei&8@vhl|Q6==L&+>$|F7bL_q|fro>lL1952O6zQOmVNT0 zd%zYf8e5d*iASMRq=cGDj>7fERm1GJ;jB>YcKlM*B#9`tKKal0fIVc7dbT>tKKa2t zU=P`&o~_QZPkvwz*hBWHXREX9lkeXH_K-d5+3GC&MZ-@uk8VQ$d3AKb(VedSNDKDWJi6r zI?F!!?mb`+*-@XZ&azMb${xGE17fD|XREX9lmBcF*aHHg@Mo*D?32H|2kapm^|RGk z_Q_w`1NM-O`q}C%`{Xa~0ei?s{cLrXeexIffjwZOezrQxKKZUacGL&NEaA^qXW1wJ z=^n5L_?Gi$tF!ErkM03`;Qs&G@n1T+`!%ZWaYQ7?IjSX7h{7 zx&>DlhJeYaK|LmbDe_prIS==9m4juEaqU2xOk*p_KjLd%ABBcCa>8Wt-g>=QT>X`0 zmOv0_yjSb@OeoC%>h(HEs3-ZG?fsBgXUv_z>9?6GkFFhCEhcq*4X72 zwW%l9AG_tpy{Xh0%8Xu&a$zUC(=~O)D2`w;EAJT-cJ=9NZ_GdRmN(`BHg|c$%}#Nq zLx!83-k661Yz-f~y)oaRf#Y}MjkyZM{Cnk%d2YPQd;Lp#W0uPR_*^w67b;`Li6}g3 z(*qadjV>`@^~vCIg=oY+DFq6@KugB7D@|YC@=${r1A5h$GlJ=;4YWv!fb4)xKHw0` zr>P8Ke{mz6QFX#GO z&(#3TgMv03Urf599))4pqJp3-Q8vy;QLPI2vtBu|p+xhe;>+u4mSP-1b>12bQwepT zK{c(=?JlU3e5d?~9nXhK9Z#XD0E`(oe|u;9+M}T|s2fZTX!3^HvwCWI5&9`N5dlq@ z&nH|+?B{yC`t-cBf79{Vd)C^v-tp-H8?1{rKwHb&rIPcr+P00=Vr~*!n%@uOd`ag9 zc0Ti#NwN_C|X@&G$a^}~DQ8O2KR?@kynW5}8ZGemU3S5?6-v8g2Qm`f9P91aEa=Qqj zVuvOPQPz{JDO;^x*x{)j2ogouq2MW^xLE#-y$$B)Qqj~S{BFvs7Mdyp6hY?43t@a(-0@+gB}#2g*nIyt_6{e35QP7kO10^x7aup@IriWDyWjPztKa|RE43gD3Z){@0E)#@AE*PhVzE$e)VFqOImyNr z$=jy~T;m){^7fM6cYI>``=i0$BwstLFG+sk)0(_>$P2?ylJs>ezPKW9E!lnhFD`%o zx$^TkHnuDBwX@n5$CJ|oZkP__c(P>oXFq54`$Ny;c&@QO_pG|)`1w!k@#g6P*Hs&i zf#zEm*W%43w?A#Het-OV3~Sr9__=4`7Q-8-2i$yZ7%s(=FJ^dS$?ZS-YpdTsvNyvo zIjbxge#O%|yncGXCEAAJd8x&V`CeZ#eewm%-{10Ed-FX#D=+yze#-aS=>fNM8@``Z zrb{m7d2Pw?PkqDk_cwpz-aNnHthD4=e#-Oe=>gY$8=lV|c3sT$>XPG!x#jOa>F&++ zwKHJJ^qJA<%IN_&h#RKst?k7euPoVp=)J4o-@P}-&pj(XIJ&jrcxyTLoE~r+xuLk; z++0j?$?Y3{Z29{e@9oX-;o0M5Ej}|l-aI|vX7iJ^xC&5k@m<+!IGPuhBC4Ufi5PBHLY z-X>+CXsdo!BT@t|`6kbuh0ay#>DiYo`MzyLI^a5Y!}pmH=|Y~X5h?iCl4m&GyI#NG z?9r0v+eV}V?uR!#-!>v$$aFO#dEdTdI{K%3Gkxvsi_Gy$Z<6y zInk1%^AGms__=3av^^pnaDBa@xEYZyq`2f}N~_;Lv^T?tXMcQIi_eTmH%D!F`fa{7+cGH!yU&wJ~?9|sSIrhF}Z;qdP_W9e!e!#izhT_K9 zFQmBS)?E!p-5=YR;q81QPaXf%(S!f$!FQc}@q^?;L!lU%3A5*GJd?{o{Xm{Xrff{s*qTagDq7$FBbN)t}AduDe&?d-cwhe|Y74 z4>)W1|6|fGy~bR>^3X`-F}W~i6y+q;WHL)9TWgeK|H*HB>(TKi*2(kpq-;LM7iLRs zi9U=Bwk7yB$$xU4+){^*B2web3~jO1#o%oi`xZ&8b#$&dLBnXa|SAMDSE9u;-9IXax~PX5K~F8gq>+wRRpLMl68*s}O=#~m2h~Lb6WUjik zn@eYtT%BfCFhX0}7^j=P-1mBC@}pnaBv)uoBk6WR1gXOLnX~;~W6kC0Z*G!H*c{@s z?qIqI;)r+WyX*1jFK)h+Tja_hu}MqVL>y|i9PA_$Y{1*OdZHxj;Y?L0y-_k#cBVe& zHp$V<%_S{WFH&LAKfJP zQd9A)gz2F$#CVHyp)EJbe`Aw859P=QG^z7(OU9y}JlIX;0dIQ(5|~jtcCFDQ z)8XB1di-UZ3-B6KOGn>pKES=6&G<7I$C-(8J zO>%ld_Gr3i_lzb^QtEj30CW7V4HqJbn@A92;V6SC+e3F-skKSnjeC*X)AWu(+IVM} z?VghAo75ee7i`x_xlHRhxEJqE{dH^NW~v5*P8hVj*(lDfRCvxhfL()|Q@A_TQnJbA zuQl?}G|BGXa=HPJ1DTt2WshRKa2g5F?m_+dPi?>hRh)AjqD@VNZo=8}?pm=o;1`A2 z-5B)0-GEDWkDunF9FvO!4+nv{ErYsF9&S<#fk2={JJ-+^%?z6ayQlTf-cV^pRF}#X ziH9(h1#@k-Ej_%U^51NdHwO@>#se3%xGq0bI@_vI>*U8z*2#@TLgaynh%+;Fsnmvc zuk4Qg_ciqkwN}^Ye|Q5Pw&y*_R)SGeg`!2E&35l<({o4XdKW*pfWOu23Qa|Rxj3b?ec(82ZHX{&l%}HYm>ak%@&hMYhsLoSPF5je4XeSGxS)~%#v7~~LB#M4r~4Z()6dw`)f;FEym$&u?#wA@dT6!F&1FFk(m zCb<_76fqmX&WK1+#qqZF0XKud|FTK0u^FS9d5BJTp0e0@xO+M}`oT?dH6y!9OYX2E zt~+F?=ghlD|8RN#|N4=3^dNk2{r;c7U%2=4_v&|l`R?G(Z{PXIo#Weo=Jw-TKXL1Q zC!aX^vYY?iP4vb;xG}kL?fQ3J{{z>)=i297{lTl9E5CYWcKpwd#iM_jgY5eG;Jr8d zH!S80-}l+42S}P-tYr4K59hG8js|l9JXWE)Iw$ni8$t(Ys$CMH?X%m}A?BN3mGk-hycG@*XxN+2@@C$DQ_V~G zioApaL>2Zf;rzj|^`?@S_c$-_0JVj^%X@n8YriSy<&^Vs4v=5iyPRhaZu3qf>sE+1`BCrrXU+SeotuPp|eaVVCK4^759Zd%)AF zy~}&nbn(2LrRg5<^l9&MwoDhxi&~oQ0dJS~F6u?5TW;)LpSG8L@=rhDq0-)b)~4&f ziRNW2yUhXbV=jJMw6WY>-3H0aTXvfR-nZ;s-m~4Nm6x;ZHV3><*}I&rZUZlyX}PU2 zt59SOyMu;Y=)ewE0y;4(x^63XtR2A4B?w7fqRTX$y_s4U%@1Xl*y;dpbpRqIY zYU^=XO9y#$Fbp)Mh#lyLSEJl^wcV8-6}zKoI9CwDp2DF2@*&n|b0}OmV_j?oeleb6 zMun{rrFxiT<60n6C61AcWl>(#i7w*HChCjT1vFDQVuEF21#$9l>lLZ!(GxMQk4b;} z=#h^?`}z7VKYi`~_(wnK{&>Li%8Mt1t=_)$seZ=&5h$*D-7fdXol%evm=c%3hxje8 z)AuudE{PaZ7-s-hLBk5rYz%@$wXOwLwFKt@51_827wm?xTNdPiwa5YPj~BkSTh-!) zw!1y&E^|F=yXU7FW3FC29%y_gz!kM68?hXW#SQyi4t0CDtV~Xg~qi; znnE$8GA)fG&m>XurKK*Y>R2R^GSx;)ok@xY6mCw5h@-R4V>y}i(6Ne+91}7Tykw1- za=;GS#<=wOQEwReSl5~oU~imNU*bzvuXJ z*R(rqaOaEz?(#T(JHJ6(n#Hru$M2B!?I4mXuq2KaTjr`)za<6$=KNOS z-IIwnv2theuUf7PR{zuYtoc&6W%&s&t9$|0YtPl3sbokiP8B@b_dj+E5b2r0z`Al z#*U42T8}Dh0C!l358DXr%I#^17|V}mW{Xm*)0Y?Fs6IBCVvTgXF-oYQuO#gmBii{H z5+Ak5MY)V~xLbXUhCR7A#42q&3>Gb95o5S+B#*N^%+zDlE1_djGweq~(ttfT?d)rJ z`KNFE|G(&J@#w+#Kls3dcisQ^{qgVNIzS5ChEq%iC`n#@u{Mz{1Ygd2e z>epW_Uiq~v-+JXMk3V|+o5%F=(b4x`)~52x*IyRWr*S@A1BccC`9^s4eav$%t;^fD zQV$nXII_a9mj|lyl$gnH?6)rK$Txx);B8+j?StpP0B`%gVIREF3-GF2U$YOM_X51_ z6N!iEWa7mWZjK=xStLg(vHc>Dd}H_myzSlBK6vg6@U|Z%?1SgL0I#%Ne-}o!Cf~4M zfVcgA;NnQx-y0Cr-Yhy#Vi=ee8kvffwMNvyVOS`Y*sc*Pn&9h)&t|oM!x{ z-cFF#dHa3;3-HeM=RNS=_X50g{do_(uYNvWuG{%`NzaE6N%%zME}Ez}8b@7Mr?*m& zZ+z7Y@Xqz;J@C{Q;GOHwd*CTAz&qEUW2v2SoHdBm1*5&Ql;0EB^8@6%wj|u$ocUZe*xaP{Jdd3f6#=`Pg3TSzy@Tqu5?9&Q3gNu0~ee{U6Srj1VZQe{F8 zp;?bd^1Ph^$=fq(0c8Zw6v@Rn(My!0x1H1|Gu2-1EJ;GEGD@p5Fcvi0f=zQxM=4Pw zqf9Q#nv>G1tQ;F0s$9+i0b3fNDqxs_$3OiFR@r44COlS{1r)Kv&Rhk&Qg<<78GdcVTZY-r!@~;;iHo928hI>fu!EL2 z)Ut&ab1A@p)s zk{9C%ff_ritkoFH?Le&tBNpw}hdnDb8|5BP*fX7vis+;h|DS&atBk*-;lBkSoL3(jtz3ILa9jgUqI?Onqsm}kOJH0J9lDSeeZuWW z2$NL`_Dq9DT2gg<-v|*=2M{HH0(JazuUwS@NbWfvlF;^S95K(5yPFn8U=Q1I!(ya$RM`sihF(nm*!dg9bD8?HY%{J!6*l zvcLVxRT*$;>oQcGN&yJ><}(2Y#!$yqDF)rO`iJXjwGI|Ry&(+MaY3GDvSG|S5gxkz z#6;zB0QE)#W0dxm!~Z8CR$xDm+Y7@t$|Ze#!+5ne{1#r|M=ZUcRzIU zo@?Kk|KZa=c@5;x0$+dYS>UC5NNv4Fdfr3y(vCPhuOH&mw0zEUh$GM{{Q-_lyIn zB`jW)Qa^t+^wRHh71WH!k7hlI^U2!dg#@Pa@~q_W1kvaQj}@~5@+MGk%*SzcxJU_T zxL_T8GOuU^HW+p**&~KxtALR|(5@4cdS+LLdbnSBlGUfLp9MaA>sjCdz3XDXwtWq8 zXGEX_oC`lShIk0UgPsMxLk9S%eHK^&a?jj+Ald1h;sFak%R0m(N6?^>#to9X6O5;r z+?>xeax7#3ZP&*ZKmsrSE*`~WNEq{F6gODGPb9lF8Ia}jV@?!wiffZ9Z!(ieaHnAh zAZ6NcM#+Fu9MaO1R zuPWfJSIA2zncutD|CM63_*^#}qigbmM-97GTCk5N7|8qiGv9!2GO*OBl*VW#SV*1p12R7#Af$|bmu5hVjvyfAby42wrLVZ_yRVc^{cohl6#FL7-y^}-aI%o;+y z#^AM4+$?I%`eT_X>lxORYf4(B!TFxU&GOTi(|mgNd8=uDetfy^^m|V8K>c?++$?9& zRTufBfqd0r3-BG&@YBjrcrA=*Yd`xiQ_Rgb6_{O1wH&U@7z*=ZKxeUvl8C)oZoqh6;C- zfo%z$;`q@V9ZtxJgrOl-vf>(~&`C_{bi| zT}ORfmF7(WCXEnoBKcDS0)aU=upW8pq#RIKrC3BDYN&cmSzfRZImzOL?0WbD@4DVR zo$MDqdG+bbB~x$)t(Hvg@#RWrZdvwnVZPI{OP>nMK0t?Zw`JEk|u|n z9v9~-(hMNnGNhD)k6uhI}@iv!ORi=msNq z^Np7G(iS(MFuTa+I_x%cS(shbg9!M$>v-v`ZdhJgUvAR(_TO?nmYd5>*^Y2h9fD zf@)fe9SSkgEj|_=Gf-og@qCa;WnJgGZpTsrcPNf!-UefynhkJ-OCCi~e$B9~v1yRF zc96>_y!W-Qd0n%u{_3;{0Q0chl~QH(>A5ZC7i?d#w56r)#sMAV@@#46x&A9}ONTCO zx6UJXoV|TYEbH|hL_QU|8?{Qk{#^h6A>EZYRTC}tQ3b+BG=p1^<)`yxUekoE5d$m> zW1d+m^(rrIh~V(?SR9}*4_RYMPC{x(`+O-z6xl)Q4b@~hp*Na4SiP1c$+QO7(#ixt zC*E{ct449!(~XQq!pFD(xB}wMY#yAU&HW~|<)`O{c>Crjj=D!bedSm^{>}U22lWRx z?tRAnyVw8wo8NX)Jkf7DH>)?V-T1q?kAHCMU*5QVeR92dmydq>>JQ%kuIqm}R{)^yfBDtVx$+xVe)L}J!SKq5 zuJBi0zx!)PzkT<6Z;Q82@BGs{KXm7fd+E)8o2v_a-rYZV`+vLj#kYU_){|RbegD5c z`PU~ubMhbG`_)??zxi)(e>f+2!4GiNx^gPEe3c|)w$u*tlgr7-uZZc`7R|1*5XQN# z5aLy5Z8$}1u5jlA>l}HQB-b0)%B~3UL%qKkG}Dn&?=+ew+VQ)kd?=i@t3^b2!dpMN z&e4>{NM&jwt|;~CT#|uy%0O+d>t!)yu>b;Cz|_iGL$>R;zGsy~sOYK3*=QbMP2oeO z*cOLg8A8BvU(BEVR4RTZRYp?_X9m#q&tB)4BtF_#Vzx>(g^^uw5lEXOgwv_wQo9Gw z94et*qFA;o!i~RhaE|ZVu$qneir~NujE|fKJHksfze10ZD4x|lpF=Q6FO|W*Z&Yr5 z<0^;PV`>DEXqY*~2c@VxbJmkTS8Qii+jOguJdz5LLx^x>a1-*%|FOl}nIE%hb~FtzZGTv5@uIhE(Q(=LQ8)4~&Z4i7XG(D@j-qlSGoEXL(BdMxPbv6`e z8gGS3U$cUmUYS&bgm5JUo5x9%CD+4s85lgCB$ksYk}KQJ)Bbaa>&IfK6;x zaDza0Z84OVG#%^+4KZ{*e8CNtIR9*B}kS2_Nbbq=U2z+l6RWB`b}NH58s#FI^6 z42~E@sDL#Z9?XHkwACF{uK)5nM_E);u02(26Qw}9<5pl*yl$aUqh@TU6O+DGt(tCb zqebY-KUwDhdT1|+f;@gpI~~{yK`^;_QxBSu?$OhXihzmIsT92~z+9JBIfO-xb6ums zF;>}x5*i)$MtZqw8TC1fSy?UUE`s(T*8Nd(Os{h!kvgY|{DC$$9@VuZMHVKb1hAp8 ziUrWFYmQSCTwr;3z5%Rr#7xP@2lc7nT6mS&$Pf_=j9uQT0a|xzVOD6S%-AEus(tnU zT;~`Q%0g*1DwC}105TzwT#BV_0W=X`DJKrpYNP`Q^=QesT3+X{a+SNeHLKT6B9O{+ zsa65Y8rMVcDd)3BHb?p@zZmoiL-P1{RylI(sZHay0!_p&=(w&^@T{3gg%dJcz_B$k z>JgODwP7cz-v(|JuUf~a0%pyPvg9gF7SxJ`flN2arr}!)FzmDQezz^5O`R%C0Mo0j zSs5+am_-Y7P-p>7#}hLNM(i#yKnqRCD|ToqjQoil@M?CuvCa`Wk~AiW&H`r317zB8 z3qdp^ChZ`J24Hbif{O+?3oBezx&4{z9Q`3PDbSPQm`E#O%~51Num^N*7F(TmQT&LsSOHc&hoTGC`wW z*dgU`G9L^~om9kTu^!fnB`K`9t)iCQd;gjhF{W+1&kt?K9!2mFRsG1%Xq-{RZsJc- zxwJ?H1T&`LM|_mTHD|zs%K06gm2i^_NFn;`I9CiR#QeXEJV;WqQIjwJHZ7U=a1WQKh`- zjDUHkcpR*AScWV0`YGQ`Ejy+_y2ARDI3Y@Dl#UR&5RHUF$8YgEm)-cQ>l}IH+1MPF zdTBfg+kDfMMXOyTK_{dLPL7hHzToU(7P3?1nz_k=@x&yUj_RbG1ZF810;28>X}Iav zde|JZDoAr;JADu!ugI$$5}>&*&QB7e<;q}?c#d0XXo}%zpfWbfu~DyJNw?c0^HFrY zk~u(p`sV+zW+g;YTro3WqxFH*X7bKQ zUisB^4iU(PqfsfMA>RW+4;~NLscVsRVmnh#PsDtd4DzUVHCws5S(g}vqSQ|D5eA|~ zd))6L<8g;?1~x? zQ>L$heE?{=C=rTCQ@Cxfzk`c%Z{8waZW;_+nG6w@=%J86C6qABZADPq4zBT4qAXRe zD(f7KJ|W0sk0P<$?#DSp|loMKW%EB)9$daJMT<$ztsK&1?7?7 zL3JzB!d5mW3)$^&TIY}%t*@i{93|lVO1&EOW82WHb8xDu?LZF3i&E4ylRjcsZmidE zLTWJxFj29fHXE^GahmF+$&8Y@&KF!P;%9j3kh4N38&+v(mZ^;TcnQ(r zUSgE6Y>3=&*584Xna75ZFyq)tU22SyqTDIkgCQ^})M>9h)R_z@TGK`kzqOe;>lrU; zt}zGWLQ_Nt35p^|;{i|gxKfw^QL;!Eqef3`8pj)pLYr_M7|dr(Wl$9>!#W3yXlSue zyhbIC7tpVGXJy`uCu}RspFHIf&m)C2fvPpkkGtlnlE@O&tQLrQ@n*Ei(Ex38&@9`I z!}|3$sHqE}H7~`uVDR0TF3uZWuIUpA9p3mmYgWWaT_gZJ=|XBa2Wd!~(f$HzRpNT9 zHmlFXqz!sFYDyt~^B2}RsuWUI2dXn~U}La3v< z);SK?3HO)x|F@6oNB2i}|NfoUt?xhioEsm$_J3Xd!0{*YzkI>ZH@xc`-gWZ6Z!{i$ z9&-J87k=9>HC`d-|LoE{rrJPtFv`^%rKH-Ej3QfZHaT&ZlNn@dBw} z7w2tq|Drb8zWIBV+T`*?GA1fj7V|wbHR4+1+~*5u=8?E}IAcN%Y4dqV+7eCHs|C*X zhJ2fwc$Q0-lxnrEWCJy;E?CmGH9I6xLQ*7{g7LlEZZm*!Lk zVm-`Z@WrIm15i){Mrgh_+0iD=hQA1fSaQXhqGM`Xn2eMBvb-kP5U#Yk= zHo5cSHaV}@^y;(;3HC0_PFfDlmGCLyp=}dk6u9#VpYV$bQSV@ir^L#00w zg!~Z#6~e7ndFFJB1%C>r(jvA)T9GlBrC5g8yG_3BIB%2N7q!W@>eVaNCYNO=Xfsj# zdQFm0OK2gWDw(o1+@*&A%`9wTX+@|VwtSal+@=La8tKkOW!#++ZW~AHWTBbKvyL4y z`Sup=p%g~$-6sFZx8!Yd>!LQ>}sj~ zOpyn=%6Ea1n9e6?Jt>by(#%7ui93kvLNh;dtoyPBGa)G`Du?w11=_n!-uQQUo19$K zCfjP3uT+~{malzVbq#mYX3L$Jk-duIvjqXbcg*n+VEw=By?31B)YU#d<1)UUN(+Gi zS$0|KVAgUsgd*FrEf=}Vg<@P~Tef9cuA+y~VZi|sTIjum8cOJdPU!H4KoSUqPy&R6 zPAI=Cdsj;)vsoK9Z@%y6$NV+V%sJ8va{B0!qnOswKFUK>@LJyG?xx@z zZ*h`-x7QoRh8~lwOxXV~9s9}H+^KWRW^bLHpSfcu1eyO2nEK1q-jnZ7W|bc)k5_IE zZUD;_w<}Z=cTGg(FUt>-y)D~U`k^#0`9!i(vc>p~+$EX>6LpaB(O|kZVWuX zbe$xhtJ2Q?UG+oVzi^u_N4pKw+R8M|YHPj}rq~FMIn}XXH63YcOEtZ-$`X3Up>owp z8v3*6YJYyap8Q!jOD}hN;#?{bh37aBNZ6D1q>J_z%@Ed&vQe&Bb*pr?#{{;HbM0F*$7$U=|kR~cTfjGOLtl=*BMlcfz>XS8#F_yqE z8kSXC`2*PG3Zcnohi+0#7Uq^`Yh74Lt)}WY3NfClxI-nYn~f&$2t@=#Mm@sn8V(=g zDp(0`Y2|LPDXq}dGebA^)x^f-QMSNSVU^xmt+#yvXTTb4vNemz7C~tSNxM+3-GK)z zRI{TA!24%c*joR7etPKE{{8*Da7D4ajHKXaSK-U9C7aLHEv6>lAoccyr`gdPG)W^u z7L%oHpkAp{*_CF&9ij724eidL2JCW_X_}E>vT4yDN^PYcHqsB{hlg%t zkuVbO~ryn-=4((1qY)YOzlc))#*$k3_sGk zh^oq8W4!SGnTM_Q!{*MRTl@E6v+!Nu^0uCf7)VDV6b>3;wH;NfQ}Qzg#LJY>DjPEp zU6sqIN~!f#zb?CSC&3*dZ0;D^omFAe2@*9oYHj$sPSRntxx3LMJnznQ@@39pH0ZN% z()D#BX83AOSTydqy4yFeo5_5Lu=&^5ZOc_TzmwO-8Z=>VVj6pvE9KQ8U(JxoCLPwc z%HVcY;8$d?hph_!?6BIOo7R&*|FVcpX7shZN2BvKvnIE(;41NccNHz0$Ovokm`u4U zTj3n>VlJpd(<`@!?)1a>#-ZKmhjAcLZCi_ZpTS08e6pca#d&MK7BCw!PCE-TrP*S~ zWp{df`V?JTxgBhzAI3Kf-N+(g+%~vv@ceC8&*Tu)8?tB-3{yqThLlQUujp|NTV>%3 z3yVhAi5FIG3!ChR@%2MD`IRuHh_pW8NTmI?xH}Y0dd#u3I<4hxbX;e|bST-%Ia_6u zwr<2>7%zuS^~3nOp_}@rFpigfjsV_j>Tx^k%rs17f}ymnc&%JB^A^t9^m3L=(ogyG zHenIF4Q#C+#@7zr+P@29n1sHdJj!wvi_`g)D95)^Q~BwU8&odwpKiqba3w z0X<#ugyBT66gJbpgRWXPzs;&BUgQoMfk&>ea%rx=qA5%2aUj#)zANztQ?y@ z0DLe3;4k0)e0v4{YgYjBz--q~v4lJ@BRmP#9sU2DVu=Xld>2b<)+}0%d4z|>;z`G& z8d#9FHF$(BOoCiz=@CI;U33Ef`UDoV)(@Qkv@IC>w{!vm0p>cAmKQ%=w7BD6Gr3u3 z)FErFzl_Ys8dap`p$+Lukd8MxCWORd9ZMCfX6uwAW^e{mp=QU^O_;uR9+-N?yvdtr zkud^M2bhGj*)HVaPN$@4l=W)Fp2)f>b=sS687%WeDa?DiT2uph=yDpwQtG(f-eRX& zP*bR`+qAdl)3uD7zz{2>rCE1EX20t&5A29yZ(AYqj!8nox8Wzp^%r@I)L-6r_06${ zQ%U&Kg6J5`=>MhY=w~IjAl*ih5!l6+^$9fk9x-(R4}J3rBegn{UtqRaM^Fge{kk{1 z!Dw9T&Ay@$YSa^%cs@XvaF>P2b7`XKD7$Q}LN%AkW0Zz3Rne}s*82L~8I4lY*=jR> z9Yf|jO(w^evYdvCr0wlOp#aPEC+CXf zED5REWyi%vX?^5l#{;|UG_ z?-TwSQ#cZ+C(&9KmWmhX-nGq6Kt)v1DusD;`D9*SaKJ;oJZ|5oi>qt-lJ8o5rR|3F_ImWvZ?eBCJ2sf}yp7&W@QDR|tllOh7< zXtv$VcYPIepagS{SIpBbUf1?^HE5k%5huPr>0fN#>(^R5b&hX(wbe3en>WL=lHEqa z?6Y!MnQ-UJR!hB5h(%*8mS|;L^TF`P%C~FBK)A`&zrXvjX_b*2g$O$F8BwfG9EMs~{Z5C)db zmR1z1MNg|yVO)`7X*g1we%FQkf60Dh(Xofeq(76^q*mz%(ue1sox6PQkh$>OjuMST zKK|VJ72^kw2ga9=eKhv)?B}yj%w90tn#E_goOyTVS2I7EDbD<0W^VfB=^Lkyncio5 z#k6GV*{MsX4wxc97;LZjRPm_dTm`4FD>k26Hu=HigOg`YRwvQPO_Z-Gg>3(nQm0ga z=fPFr(22Ju?wmMzB0piCn3lgNzfSsH$Q^LI?zqrvX0CxTe8>& zb!T~CTq0TON%8JR#+j%W-MqEcaub31w=nOi%@Ln@}4KKXs%$~yvPt5Ewv&XQ) z@|oRdb{|$)21n!v!wO4hcAMF4SYgS`t~0yN%LB?`uJep}#tg?vKDNp_;mXo+FGmO3 zcny!1qjYt_z{qDmnf+u~VcG1*vmXyDES>#m_M>5iC9@ySez>ZW>-2f{gV_&O8TQyM z1*YSwRV=K%-o|*x!qTE(_V(G^hZR=L-Zp#Nu)-7Y}J%4r_TreuMmmVTI-L>*d!ED=d>=C%N z6;>)QR$jbXI96ygED>wvIysvy^buEScMm^Mrsh831i z>!3$ZEsu8bF}DNO{q)!V2Yu$_s}T zo={$(ykJ;ix$=DF`NIm!l;gPI_25QvsW7~^OZ&< z9!mI%mM#ikD|Qy%&5~y%&kQT9kUTAUdRXBJ$y1W2h831eo|HT}tguY-gye}~g{6|m zC65m)ERj4Wd2ID%Ue~3QJSur~wPE=2G8BzEve8Va*dj;|zTh$|rB_L>8dexcuasUn ztgu3Qh4hMHg(sw!OD`W*ST4OxdfBkTGU=t#ONSMfN-vR$59f7VI*IgR>BXzZWL?72 zi=^U1!xF8w-NCkiiu{z-F0V^?LVmLR)G|T!~yT&`G;<`PhOhH!;zeXbdYX zpWr6AVTENA^@;kh!qN$Lf*n>^GEtkTt-2WN?jSS447MK&f?qs|KQH}cK@GR6DHs8|G#=M zzTN+?xih}q|HHCw;&k%u{{Jf%k?m)9LQi$5ZXe4=0<-_mv#@J75*>Dwv6P zCMxo`S9ke(cEH zje7lN;~jjCePjpL^AKPoJFuRIz}9zQgtYSYJp|T~9azspU>Vtg^*jV<-+>XH@9TRA z_8QrN^*jW7j_kmC9)dmk4vb)QeGkFzBRjC3hu{Z&2S!MCUcc#?!v24gvBub(dA2@t z#q^)1x0)g+PgFjr90$89xQQ#}ugbqKi%L(DJS35f?*;|e!_Q&X!z;?%zWWBg19l%< zo^or7L<`pbv1lt%pT9)nX|iQ3I@p%m9#u0~J!_4G0wK)EV~b}~>JamS11ZDGEk(Va ziia)o7CT+8vF>7e;hAlLR3TMmsw#~dF(YcVCW)93Sjtod8-?8{ujxj&Y2k=$!;80! z1;ULLN@YKnr1SP%w_~+-Z7_L?^hetSpDl80b8jJ;%CK_Zs2e8O4SG#C3>{xbmA0td zEj~n~%^GXbV+b{p#!SG}^u;VjH(vE1F0E5#q}S%&LQaoi<-SojjIbM}HQg}50`aDx z8IRbzS+A3;YGXR4(kyr^scJWDVoeDP>x8A#Yz0evZSE~(haFaKX+7#_heEl0J;4X! z*0|ltEx0}Z%}r&1-6*celAoLcY9V<-Soj^spO+HQgYHPFd@7=JZ^p7LFw0D4=W&bGKY%#i%Q2F};bjw{3(z z9$IT`ETq|6TQ?TUeWPyZU^l2W-GCJhgSteF%IN}StZuPtEZzc}Dm(F5T8p3%ikD(+v%2Z2K+U7FjE~ zD_W;6)zq_?r9b@KCIj~>V^h(Be$j-IRhK1S5*w6 z&sx!Xu!7R8BW*2ZjMf}aTDW4xRxy=)EVs8`rp< z3+IDj<-SojjFYp^jJd}ikc{uETsi)pD+d|tAx{5SH8rAy?^nM-H)n0!lao%$g-W%@w*hI6L5k|a7iEqifhBiU85 z1E*h7-X(L**d~7|+kE`)xyQ!$ojYrYR}THchqiDq9?>vHW5!cSYZx_G$-`=S5w@hm z>!iM@(UyW}CPZ4bTzv9(1C0cpbJr<#4bN0qLpPpjMJTgA8==__EU%tx6(cD!XwC!* zdL8((uYqYeG*87`fr=^`WVO|_r%1;91vx?Fsj`n%DR>MJt^je(dsx5me9f`xb z$*`OOr})mmtD>!Jh}qIaz*c3bcG_aJB3{1%mV(#${E>tqp=ssQC?6*<_1NKqHgqYi ziO8GEfp)%KE{Dy=L?PCpO&rSm{0^6qiq&-OR*|c#+Dou)bzpMHHG&PPy&CtC}n5Y8jDMLdQ_B)N_!mWRj_nMdRdxkl;1s zaj|8>(}?;4xO`A1+DbcXZOWrdHsTzM2Xj?b$)V}+xU(7d!%D~)UNI3Ke-<@1IQgOd zHf-INH`I=}N?hHZg2kY{XhYLBH&e9RVQL#$s#GZ#;{l&J7sr(Npdb2d5-oJAbdhR@ zjU{6}Qbx!K-HxP+p-d~Nak=rP->1#c1gg}4e#u~KJ5jRbiWR%Rf!I45HW)_fCcyD_y<2E_l8Fv8C_ip(R>LukI?YxrZ69gIObmznwb8B(r%i0ESFNaBAr(fA9kJ@_R+{dn zCDpDLd9AVTp$v$squ2N?Bx7snIw>!_17dh9R4L*UM86wY(cFQB?75t!T&JqxYOY9o za&*>|$=7Kcq^iT{a+J@cw4^OC7(gtmF1I(|jb(p*r*!wSQ(G>Y+q(FnF` zR6x_J8Ay-cpLchXK9+-&t=>Su5pgnHz1_@OcvIVsWF*%NG>AH4()yAOo>sG5ln7MF zoYUMWwNSSm)72|n4)QDIIu%SGAA4e;VW;3VH#od8tewO%n7-yn6pR{JM%+s}V(F4U zRi+JG$W4^g(h~<7SWu%S4P@C>V^cP+Q7d~eM>1v6kZxSK;6bDsvPHsd(x{d^Kj^r= zs?L#FTY(B@V+OS;L8wTl=N@~3phV!hxg(St(Cn69LTq=ouSfo6Ehnd@d9wN4vWmqw%RXx^&# zsPrYX+Kt*$)c79bW3Azld59P zQ)>FES*%kaJRE!g%NzrZn$KN{w&D)ng4AnCQWt3u;V$D$QaX3L>1oahu&z z^GXIK?QPj=xn3x9vACIZshy=@$zTq5sH{I{XN#pAp7s=MEbceh908era?nU5ZL6~y zMCGG2T+r6yLcFDx&t~T0;mH0?Fl52|c6q3^dAmlQ~#6#k_h=4oer)8Xs!06n(mQ4vwj@+1A*# zwiFlj+jWzl_cbuPi-l!wJvvxlJk-$#D2!K=5waekw06{|@>5vaj+8xlSDu+1Otdzn z4ok^*>QOsV55zNkxgNCJi!BZd6%%Q=dGWJE8*U^0oQYBHIq)iOB;1^a$R?|J!k2Yt zQwEGcX^P^#&5YSV;JixhZu>N*U_df>=-OPC8pUX{F}v2SE7)iRQ9*u0yx{Ve<8-O0 zBifxP9jW1kHa&LUpbfl{iiNZ)x|}Zhxv-yh8ySo*#8nYB-zdhy80POJN)f_Urp7Pt zYgh{&4XX-h^sxfjsF@t;M%P@(rYiBW*6CymE`6opjkyhhS{9pJwJ1r~XsyZ;v3qfQ z$J-8N+KEIs>MohPIctqK*dmE~y_9X#bw$(UAkb~~pwH8>b;*{!WsPUGjzrU72vswB zH{wGhTCG<_qPQP+<1s+*U~#ZA>j*AOg(>l&74P3f4=gC)~eGNq5?ct)EtMIH9G z-+_a{qQqv4#41*O!_cVD;hO`7&vnL!-`QsCR8)#rwd)lSTq&1OF!<$E|L4wQSY*gFDO5qgY))wq; zK4Q;W{g}cy&_LAwq{~s)wl%4ip3cH@+kREaj76L(UA3$0*fQM`){N&Z$DpZP?XC4dgm^IxDcId6G%m zvKkE)geAdXA;JFSLtF^k$Naj(tla7J{&WZ9jtdTkn0DowZLcMsao@dZ?C#?sX; zoIjYTn{5+$Ch0X;%@H^|d0NG)zO2{!^UZ*4uz+>uP2M){W=b6wR0$ zSS2$R7+l??A%Ikinx@-Ww51SBzTiczR#y;F<7S-j6(aSLCS)Y-;WDO}8EE*D#j-0D z?zHlRn}<&)TP>okWYckT49(S@TAZi_Lzv&tt?1;B4K(tMp&q1saSc^+nzb$}+#oIK z1fAkCx-b@n#Z+_#chJ~PIXT5MgMQeZ5qCt5Al`rq$1AwiL@St4mF z&T1-NKRPjZiejjZ#8ehb&XI3MO>I~&-jR+_OrvRzM;N$7%jd(XYOWLam`(E22W=#E zt~^npU4@9=ix=wNO0-z6*}BPk)8X;EQ)zq6>Fsa@j%21T81%zSJ7D#CtkcrsUDRPF zE!{{rrS@qYUJCJQal5nDGCE*=i=a+1H_)({3aV_N?N3mMIgpF%ZTSZ8^5VvH+LH29 zo($`2YIQ|V-VsoIIPgjr%y%uEU!`F?sS>Qw6|r<{Uc_avvi?9Om#YU$U8i0Zf!9pB zY6+lo70hnd>MaLc=~|YnloJ%3KZuxv$g3?HeK6_s`NFnr5iVU5aL1?_V$b*$k%3pK zf*};O`c-To7qq+b6=Nb}Y^7jry?P+5_ar@)K%&rbQfwd&t{n8-RV%t21rMySrEO;Y z)=~u^;zq;8ed+{ zRP;%0Q|o8F&YCsAajY{GB_lC?QlHLent6AsV#IJ7#{%AUzqnbzCcIYGdh}AVGI8-ChZjN%Sq%S|hE3d^_@kcoOwis<`BgZuaNBR`p1A^ssUr|Bf8*}1+ zm8*Nb@{#J*CcSxWe590K>!J1PE~`epc_JUR;9*SHHO~`v9o|(NF+W-81Q0}T2xh)E zA8A)#&v~?HxPpZZSU2jin_4X7qc8aAT0B_I&Ci=0A!jKYSpkpdAXs^YF`Q_1;{j|Q zx3?2{U#44V(v)L`*Onz3##D`oRCAnVC~EG0*9B5wd2dG{mX81K`0^2Mkadszu(4$N zKN(9S9AaE7mj1O|&3`AdmM!84Wa#D!sd_H^Ph{M}v+zI2k#E-N)=CVl&8q#KoE|r5 z9jH2F#sc&6t&Bd~A(48DXa!cJOjdYGW4=t#U!R>DcSO60+roA*mpG+jM=(&g%>r<+b~CU1>z>EbhH}tC(KQ~*1~g|VBU|~m|CUkQ(L1M zbf`eo30;SQcx0ouLw{0hj<4VI_rnIF@&9BXjxy^mQp`g5a0b`rKn zOqYv;*<>{dzV1I8cktMrQ2k<2LT+_VXY#7qHs;E5omks$45C!k!UgG2F-WHMA&$&+ z94@|F%fcHS8i95Bs;Ne^4zJ~|TOY8j%zQPCy#FWo`a!RUr$|cQnSBOREI*hxc>E8B2+?|*iA7u%*{w6@fVsSUZp=aU79p#kv z;)}ykSC?z*HVEs%UA|i5?!OdCO%9fY>EC9-Ke%S@;UIS#H2TtKr|o zwfzChRu!2OY9YdZ>V^NV`qMx4yKipI{}!EFVS{TVGQv^kA`Fvcs$5J7&VSYRppaAl zkAjZ_e-=2&HByb-SN_)5Pp5?1IQ?JgVuoJ*JDk!}8KHOm{}ijOVngUo{{pOj+-PA# z|G{bAXx0l0iA5hbwi@FmLvbt^V9_WM@~JbHrrsY-q$q+^bA=TN*NPGwNagj7uV42` z3*YH7)s(-KphDeLIi7L(GwDiKR}HmUyq&j~wO(h2<3m(oUR~WOm-j=b1=&NdAAJ>jM8j-0eOky>4voZ*wotJw12t+%M)Xp8M(C z(R1Co(%jy2p1IxUc9~l;x53=_?B8c!nSEyVzS$dRFPS}U_Ly0ImY&^b);qh$?26f~ zXE&Uc%)B@ArC8qm(&_i7U!DG+=?A87 zp1y4QjOpX151hPW@~p`dCJ&xuCikBVOj;(jlglT+GdZFBNcp<*x5|fMm4GXiKZBJ5 z4pG)%wSb_~3M&R|tK1Y;4fq(m0V@YQ0&auV1I`8~fCyWz2Ol&?eDgOuLJovr*G5MX4r~5qlkL8EU8}gi-lsn`m`Hu4M$){wW$=;Uz zLH4-pF4?uR^JOQ?ek5zk^0KJRDKpD!vHy`%U|%_k!F< z%ceJ;mQ8&CPgneA>cOd7rY@g4bL#l1gQlue$tnL7I;EM~W@?iuIpkq@ZSuLvhbDh1 zy+C@3^ayE7N=ai}pM)HN^uacJ_VeUPW>m?UTPL&)fX-f){xWpydO|r9O zE6MEmm*elQd8sje&zj%A{q6tW6_}ODVJc4Lgr}e1PdTle7U6H6S57IXMELZl%1Py< z2p{@~68?zrfsd6y2}F4BT}p*gA;NoZRZb`;M0odxO1V-l!aHcCOequLtwE(!DHY+( zn<^zri3o3aO*yU{7vXg$D94mzBK-L>@FnBrzx@TmxYvLpBed?Lb=FM*GXk44zCgO9*RB0TM9{w(P54mVgI}ccMgHyg5Qd8$Fsq6 z;5iX)e>M0G_>Bmc-2nav{ErBiECJ7gXGOT>_rNpY84+&w5O^9qEy7J#fTzGyBAm&C zC&7~+W^jGkBIPZ z$AE{y!yXP*n+Pug;8t*}2+w~W{1W_9glGR6+yZV9;hB$so59T@JZ(#G6Szr)r)I#7 z;6@Rid37dJ!HS0@s1-M0n(`;97942oL`yxCUG!!mbKj z4Xzep^M3Gi@N*Hedbkp~N`%#4fGfe3BBaj*SAZ)-SYW~B;BpaWqu?@dnF#ljf=j`r zB8+bbE&-Q_kfg!I;9?Q_KLZzmi$q8q0xkp>iqOr13%~^;w4Vpg2j`0rwSe=$c_Q3> z2XHPpSA^yOI0u{~LPH8Jw9XbGayR%H_?ZajHvwmXvqZSV8{kZErU;kE!5QET5iUI& zoDNPG;gX%fY2Y*wZh0X1Dfp=fH$NHt1pGvVo9qBi1*fit&sgKios>I?5UneBRPHFk z-5Sarlskyf^qg{g<@O>pd`G#Rayt=fKZR?{ZAFNzP%c+47vYYmavS9~BHZpH$iS;nF`Sw^nW~!X<9y66F#RZtmdY(fxWRVH zEtFe`aQZ0a_mtlgp^{c^uH0ON6K5+oQ*I_g$(73QD!;oLKK;q5aOu3M2tR5ozoYz) z2tOz(H&JdP!goJXZmir`gzxOG+(@~R2;V$PxuJ4H5&rpI3xWPJRx441O#^#q*#CdLooP4Nd|lt%gs2 z{&%<&I8lV3ZU$C@l_LCTB{%_`Aj0>a0LO#lMfkTJz;WO>5x$)T$AV);_*xMh1C9~l zDUYbEpU(%z#6{AtJoL3=Redi}2nD!9n045#I9`a3DBPgmLR@SRk)91MR>_;payCpyx=9k07iu8>;$TyD#EkupaLo)JiP_Vpe(|ldI1e+ z5uQ>8B~TLK%4knu%8H1zXAJ#eMK050_+3!5n<@N zU~jOu2zTW`0whGZ!T{nRE`0xYkKH>q_v^XS=CX6@+0SMloIPcB|Jj{qKAyRE=A@Z@ zW_EzP_PeH!pN>s0pL%!dwyC41!c$vMzBPIC`aLJVP+pV&Kn`Tj$j+6OWhR*n?upNm7Nk1qnB>=z z(}af&h|7zGbc!R8URux@A>Cc^BczuSbc_tyR`E%~M->V_N3)b_I=iZlJ70`z zYz*P8t4i*C2eU>}>bT-?q_@AIlPLt+hLS6rEo42ZxGrQ%<}#*I(4Aotp^8^sY+&kA zGS>(Pnwa8;NG~bqgq?Vx>a;qoNHd^uN1{e|#N$*Itkx{UIZ%59o_seaEG3M?yn@bt zf=(yWK`S&$Vr?p9GPBL1*_+N-Dw>+A9jYeqlF#IaCB~^(f?x!leFdGo%T;OVvz(hm z61gZthT%a`XFh7vhjNK1*=Wb$v0iO1p-xzFL1!O9XN3HEL1%A4XM|*S#bHP7 zB2aNC(u)f^S*w`pdlR;lcQ$+&-D;{pyl4dUj&qmdwkxsB$ahTn0N^uC%iwQdV zZi`7F;SA!g1`(GtQKoHfEsC>kUp3ziYv^ja5w29YmaEp+i3&Q{@MjMd2O~XF(4k6c zCp`14aog)Qot^RLv54D;dV(cYqhx7v>4GT|jF#}ci^LTNA-#y8GeY*f;y|Pq7Iff; z3pQ)eb=pxq+3hCrvWI6W%F1Z8IU65}sVfnCHOof{t;M7`0O^GUofuYcx=rzlEu`a8 zE>l>QW8yxtsj>QzG7-SqE@J{C42=NHNl@@eFDU3_d7sVQ&iSK3nB?JfP>!G`Z;Pj6 zrZC6p%p7lkC)*NTgm=X_K_?*SAf1ja3BSfTYub1z8?Te$43V})YU-qmOovd9j?vVr zUc|@1dlc&U1sy)tb~L;?wrf&_8f2~2@phuIkhe>yk`admrYCqCOdi&m4)`*Hj!)2O z!b;w;b~F&khf#e%gE3`Wz8;6)WZ?NVODV}Xu|n8rgjHRQecuQ{XM_}gp&hTFGeSPS zqKouAf{s7xiBLq=(Fl|@Rfn@>$|w1Rh(%!1Re^(#3{Y1J5ZsNY5?k=>1)H ztmd>fEJE4I=GHxi*aRTMEe zJ(Pk&dYGW&#@iHMbo$$AjoB5f>B3Yvjp@53Kgp|9e59O*FWq%kJzFSv6?LR%>$j8h zYa`x5$C_*)saz>yCdsVErbC))PuW1`y17EA2&3`LLP6anvAp} z38%4G!{9MZb2Fc8#re1oo{I6RYb8@gXvZSxcmjCGO&ekjR!zI>`f%RRvU|cr*rerr z4PI4eY9&XrST=P-l%RtOI=Chri*daLnsTT>Nc zZdB0OOVAmCWkAr`Q_vY9rC-4!y*&gScRrTV*KI0`tAJ{{$zUSt#Jegz&iK2YTDZ*E z>v62qQo(!2tEeHp-31-L7v6sf(qJK5gpPB1c+MBM8mVZ;tn+$YagDm)(z%} znyTX{#CWRW4#9(`Y&3zxKn#YAdW6+A96rQVuo7NH73u9N=tS89PlZ)_Yqj3?1)KqE zu*udeCR+rh86@pOwRQ&{uuyPm5l~c+o>|ZtfkQx1MtUYe2W6UOB$#Yk^j2*kUo98; zw%%MwBmOd{^}4z{eAAw>rja0&SxQ=KL_M zi^ce;7iBwkI+n#0C8TE%bdvV6KcyjpHnX`-ID=(tu2p0*bAeuS}9o$gdf-yp*=r!$Sl(he+5mMPX12)aw%fTDo( z`e$&B2|Czx*piy0t?12$@GKpw=i%8oPu}9!>0E|V9xbuFN>8ax3JU4L+$%U|g`H_$ zV>flGrb;xFuIF-uKf~4HnY;^&mWm~F#j5d_OEJvD2s$eSolq+hap;oCjNM_1RkV3; zDN%Qs%rs_o2ka>`Q%Hx^?Odf*_GA=!q_>NplkK2(GfrWew87$KjD`$bAsx?D|$OVFwi_lW(GU{U8ai60QlxGXm+hB8BvJ6m%H8<)U;kj~mZ~YYd8! znMxP;S$(LNa!^KtvFMDL>X;^?F5rs&k=_o1&IruXg3k7W&InYFyH~Lv z(%V+h8G&#>u`kkFF6fNFdaKw6>1`wEqzld_?I`iylpfBGHXByr3ZEw%|F^0vjdcfqWp{vXq^XW+Lp(XYGh5phBz_9lQ}S zL1(F;!)P3&nT!-F7LUP+xD#9~i5mkUcRA|v2g^8})7jiEf0yQXTF}{A(5Ym3-e_&P z@H!vIsS1wcj#Ma|&>$AtsB)XMF)XG|x_y}hf(klI1Ra?2+Kz@1JFhWEY7DO{SKYpL zG?XkRD#ikpEHtd8ygEZTtEfrP`M#ht0xN=`vz4GT0tJC0f%LW%bVi^~Rm73r7J|+Q zN5d5{r1w2RXM`iLg3jiG&Io5g6;WaTzw_7)V{@0x5wMuqS+mxeH)nb?rs)@_kDOLb zJu`K{)H3+(xivf22ZSzeUI!x@^ff-geBs@@JV-k^qogG+WEoPK6gKpJ^1Aded-5ZIOmHG zwfn#3z2VUvPkeyvK~i&Qc!YK2z_3b!>_q(Z#;ca^bVulrEACTiL+>3hd()m*oPXN= zzufV_hjt))kgOaU9$~RLFuYCnH~T&Q_L=4f@4MtLzxvrnd!BG@?<0R6-Q&tLJbyUn zynW6kdys@28XjRqIxsAq-)r)tT|V0DoD)nt`43Y(*?>#9n^vCfIQxR`xy&(F#mOEd z7l(#NShfxfZ+YX+#Z!ND@>c#|9r;LgDRZ*v;JD+W&#?E7*mI*JHvCiZGO`Cr!=d33 z*0uw~JAUx|pRf5scG_Mqtavh)dhn5VoacUa+(qWXrZ>t~etFfK@OFb_;Lz|03*dp_ zAKd%A{8V1P_oFZ0{m5g#I&JFaO}Rfz@B94D+aS-sBl2j5$W zKBhd@r9SquyfY{bxbJ9Qah!d(Ua3=HG7H@3R`I}g0}asIrAE_?32^ESKdm=nO28#OdrT~Z>r?B-+-l2k*( zBkU#yhI`+6YRsqT`Cd%16y75-EwLRGPcvUGS`XA$sQz&hK5JjybKJ#efows@Bifs@4k#Y zb9Bpg#lc(pRqy&<`5}M8yW3o`=Pif($Q~qthK5Jj;S3Bvi@fl7>cIOC*)G1_U%IdO z>JP`*zuOX~3qIWc#yw)c+2EIC50X1W!y{~i28LZ9Y_i3PU)BP*?N@nGW1zQxJiEcN zlfH9x;fNdFzx$1=Pq~TgLDFVuc!a&uz;Js5`TIx9etXAZnw6i{FFWCV%L_|pa4B=& z)iLQq-}}jHXOTTf#taRQu%Q|l{-N}+gBr~A|CmlTWs4vGdgG(6IeFjQbJ}P2G{0|s zVRLXH+1ssec!XWo!0_2;pQ8WMhKHWqKJ1pgwmbhcQz7u^!>|ACd!M$?zS{n*`IZ9N z+qG|agss`Y@JnxLA8`C+>ZSXa{N~zh-Tlfn;E#8I^3cb(K1FVk-1g(|>&c$EZ+L|L z+raSgr{|%9ZtIs;jy*VW_#L+%-Tc9o-cz31i0m2rhDX>54h$bw_}t?@{OFk_ z+r9nrvp+v7cHdpq3x2Zqk+bJ6`N$ENKISvBXXqOqVY@gm{O7Aa|MQO?wqCf|Q!oDh zjHjFzoHz02`JRW)`04L1-|$Vk@x(r4Pv18@!k%(qn5$ONyQ@b%bNTf9*<;VBMDBZk z`_JF6+#hmX_>kd{6E8lT?CJW3N7#4{4FBrPP2LEcj_VHi*#~#rx!L0vT;(}mz5Qhe zpYX`b2Yhms`K&LK>cNn{Bx24Z+am~XIF>f6^i5$H5#9x2D@892g z>^@(G$H|_iZ+L_)?7(n!!*@<9yoda}^6^Q(+Gw|>C#3&yz#gCekb3NzO*5Cj@%l%v zl0Bqvc!YiK!0_MxmRP#Q$B#T$ijx~0H1XGoz%#d;^6P`1Y^vsdFC-Lc1+ z@Wsb}{FD01yR&y5bNa1E&i(O*9bSC$SB|sh$eyZic!a(8!0;!pJn(q)?USCYZk*cp zI^+A!H+lBO`={o9xx?uB$}zINdJ&G?DhhtEgFec6?pAAPptP_j4QH$1{Feqb0q{oTp!AA4PEd;A}rPmaIm;8S*g(| zzxJZbcg8oVKlUozD(uuZJi=CgVEELBZu#;d1`cSMF-P zvgsiI-#<3J65Jqre%${b*slNo%H&ZKyqaG3Nsm^V2>sEwSzB_!fCI@RPL^IeB% zg)}FwQO~6+Nv597)r(aMX47aiTJ2&9U8CeFLc=4o1B?)}Ye@jh`JKEr)}RS{6VuqU z9DLaq^3@EPY|>$Es|;>eMZ;RW9=1v^meE|vaa@YZ{fBbD7E+E1&19HrBUcsjnGRT% zX0l!Q%QEl4i>(nspu$mWveX*6PUD{vnh_xn>t9oyn0k>a3h7e|^B1A?;D1S&XV%Kq zlMNwX3(>>k6grJguQjSoP-M}}Cdv?Z8|s=Eujd2q#JnMkX8n$NlL3BsS7TkIS;nmm zV>FMcjbA^JQeDbBDlAN5ZiS1kdJZ0Suw_}!ljxdVwMH|~q}3jOt{yk|azP`dt#W$1 zk#)w3@DvTta+Zk3P^i+Dw%1y(SZh^vw8PpBT}yk}LH+Ews*u_V6T*c{4F`FVt_w^8 za9$D;DNitukMdf0V1!g=K>}7>sg+aRRoRlu7A@xjd5HzlL5gFtSuaBa)n>UY+-|FL5d~(B ze+|$<#v`5U$NSncJr{HQ>qfT5tX(Tdh}r1L*;DpX*H$s)k(g1kJE4@> zYiyz=Z{C-+RZ3yEonQ)`Amefc3&wPl_t(QzT947Gf-4iChf9&x3tb;X_Rgy#dxYC{ zdqa(WHm{X*By7zi z2xjeuQywzT$M_(mW~?$UtV(K9DTH3JqV7>6geh0G1ip4;M;cs(&*5Y?We!+uj#@Mo zcQD=+_`DN7NkSn^AF`4mbtqtkKkm;>OMl)4Jn@He^v?*ll znkb(050{M$Nf^w1(vfO!UWn}DisPevVjVfMg{1FABU}C7i0o0h4*IvMNC&X@vQfS) zFBaMTnGs%*|9vK{WveG;bZepUt^4q+bFOjSktsy?*N!-id98D4-oR$E1vDCJXnf^{ zw;8HMi=j|JUr6zK&KPwge!{Nx1`Sj8KLcG$`qkf1%63e#Q(+bNi-K-J}o zT57p4MKn?o2G8TVyxHjVw3=yG$>OeBH4!46Pm`#nn89hPN>iFnfvfT>ik141b14G5 zF694DA28+}dvJUjAK$sp^nb(D4`KHC_vapiC;Wdoe$V95ljP)b$+616EAN6?_>lWw z5`(F<_m7`6{yt3FNdvXwOU0v#GZjUJQ6V4SZ{q2R^CxN(drZv8UyxrWZ_6?H#7S2%DtSk8n`FDWGpApgE6y3`z#aYpy3Zzd5=-=$)MqU(THg?CNx_M#~SdUt6hV}!_Jhd-KuFV z`uOxs0}XwI=bDZvQSMSzC#rRIO=Txq;u0Ber{xV~GC3FS=9{sM5uHA1pb<#b>~t~W z&9$;_r$)!B!U>0iDOL=DoFShI+dV$3+1H}jxO#f?z6NKgHY^D>@8;B|wx#6Y8YQdW z(aEX8zHYoyK#NSQgrY@wb21aMK^tyOB3(xfX@}d*r74ahG87sk%axiDDHu~|lk&ui z&1lnJ4NUn58d%wzvNWx*)Bu7M>PB_mW%XgSPv2>jN={b*ulVi3deH4Oqf_S&ywbZg z*%DW4rQs?6db}Pey4py+n_~HN(4teX@U$A5D3j7+sA+QNRT?_@yoklyL0`emc6{9q zuJ-D6+76#b&3Oxvp?&Z~TnD#yP{sLu=5sqXKBNj7}xmYa0XA1dZIZ_F0l1?W)hb`G` z&~cvkBI$y*rVX~M1}x!nM~$(tCLRko-FjQamC(AIbk}Tgk&I+O6J76^qwtVtK5D12 zj5}G=QnqBoN8=iQ#Yr`YX5Lc^*etM$L!FXsJZPhpH5buhAw|T~1=nOsj&v ziaQz%#!Ts`TMr*CE`o!}6v9!WdZU9mQOeRW+iOiowp$_%e%2B}s9L^|!y>L^IjHv3 zI*7Jok`2hj>kSjn2BWD=GZZsme5P7Sgq?M-#f8+>)*?zeVP%2_WVKhdP12VKZE#Lq z(CDV@d_I`AReULUwJ^va%rI$_)`D9(g3N1@1s$C-Nj?~8uudbNtAsPCE$wKU^6G*! z)8LD-j1M=uEf^k*XERJ75ogob`2GWpTCHm@|9|Yg3D_fBT_^nJ-TU2ITyY$Mducb? z_$HOgc2%IZ)RL;CDzzk4a0!*IvQ@T9D$4~LP;u|36~^^>_}q2I6%`*x9cLWBL1$bL zP*G775d;NMki~KNP9?eBeJk%o8j+w*+gJWqh`^RM6U{OkPBQsy~U4hHa6fGHP;-V!-`{k4p zG!wN+Em{=Q&0%Oeu&`(+I>Cy~jL=yc*JQj9>kx9*qV=IUtYMijnWQI^NH$iI5ODN) z?%hT`jSh+oq;+&`GzDr>ePz@O7itm(-p06`w#YPwb|HuyZ@h15K~v*OGG=icPu8+R zUlhY~#*BoJnkLl?1}1S*IoqF2o7UEMFD&X1S)SRE##lx{evi>029=;#zLSvqeItlf z+aq=`%)$l`G7fxj-q;7mj3!7dp=T3U5-i0dTrMre#H?dfb7iXq*dg_`f6W;G(MlsRb(q!tLn zsx{Up5jCCe!?RMP5o#N}6bVlg9NFFf=z>&LPTGx_l9<(@WD1+0U{ut~sSe6V3TUxA z0gAcdO0o}IaQQPA^)5AOtS}6n8Q9Tz-%KcQ8*n7j$A}tIUoi0@rD&J2)q7keIB1~{Bn9hV)RMqe*Y_|f9g+(rg7r1X@~xUiLl^W1n)OwL+r zWX4P@1t}GYbYob$2(syHFJA{+i{t1pLFFJktY-DrtZ9-UB~MQ6x49$`^B>NpZOb-0 zG5DamU`2D7Se!+I5nk^osx(p&F@Qrf_% z6dIqz)5HFR2iF^_YNTW^Vs)x3NZhJX8DV#!v_GH*Wq6oOPtt@d{DU3$(X=N-YrZ6T{G=1DnDDo)T z-F?S`73ghi@eC8|PIVTRllo8=d*LFQgLuAMODbfw6UsEA3FjW#|GTh2BAVl^91Id^ zz_myx5`s0*L&c_3nKI%q*6kG;ED@dA)|JGIH2U2)qMc9!BlD^L3mMk(-IfoEJ znxphqTV;lj)FE1XTMLUsvsj68?VyY-|yG#m?afM@;Dr?HHLQ4gtuS5 zu&`CbW;?8wDM4zZQ3!ShBc6c@b`)tU6_IK8P^=|4$b1^!880o!2pF*0X|F#j;6geR zZGs-%XbG8dE67RhjKynlwN1~s@%G|<^oQ}vqzIQLeXCVRV`-(C4^u6q-)faP)6mIG zRH_JrBw4C(fuEXp4EdGl(t~PmysUV? zU@8PW?7+miPSY41F0}C05M2DCZ7i9d!B8d?CQiO-VS!g;X*$IuG>fd4iv8(8Ecavi z%mmaT(Lxahiua2737BiC$3METkVaxh(hxlgQs%Jrtd$%08eOK53z}g#1mQSH9UT+n zbvQ^IecjSREs(5^G?`W>gw-;(k&F*3lMsSstQ=@*22GZS_55I{b4TB{u*gY$d_b~h zE*Gr@VK8}|5}-58_BdOo!^uuj2xCLJ-0029hvkI@$U26DeWsdN5*pxL2YYF7D@SS7 zLU|628&fjb7*D7eknMy6+w(VQWv6zpQI0a>WUbiF7i)TD%C%D|#aT$zn^XbdvIB`>AVZ7(ucSy{%+)kBD! zY(Sn(I^W~w9S&H1Tci{T#&vMd zWQ-2LvOg=?wRj^9V|9xjVHz>b(n_Sfe`SG@$`A=GqLkY?Vi29cEh1Yt)%tW= zkBjL9-Yr+hp}5B5b`9BBB+jvj7-zNeZMI+5r}ffI$H7J|CGcEakZUYi4l|iV4;^Oe zgV5HpStXHXhd^SXtX`>L63&c=Qmc>zsm`(rjYP*0Zjf&1N}jOBhhMrer!@$JA>I~1 zK7?t6&*#bRv^T55a+~9oP$VB?g`C)|4Tj@JrggOQaQ(mb`oA#u?!)!}!}b5e_5Z{5 z|HJkF!}b5e_5Z{5|HJkF!}b5e_5Xvq{{Py=Le0ne|A!V957++>*Z=>2SpPfc{~z5D zH?CSI?>%|-@jo7a^YL?!e)dQ>{Pn|^9=z?KaS*t2`^qVhjQyOwpV_;%`=57nJMZ3k z<@QH_!u{sfXzOtE2R0*t-vfCAe*rvX@khVO?N)^^Io|N2LiWfYPmV>gS{rCHLB-S* zWj*%Pj4jIy7iDn5eOP_fm7A;s^7w`y$*xDJLTx~nZ3g6LqK8v~Rfw}e7RSxbKFtY^ z+PjPZne>v34=u1uA_KX|6mK$s%X5MEeQ1oW&BZyOW8S0z!V3U`4|TD%5mvL5RBuuM z?-bzqP$pX&&p9Q$$hc(OBmqtY;P_B8TN~%z%q*b6qyUuDdVPqXtqtYfdc&>;qu(SP zE}+^z#MRd3awlBzWrcm{K0HE=tn7dt<~4~ef#jO)>)Z7z#&MF4^mu0G@-)O-huZM=KKH4bo`aP^_vur`h>Tw?&r30EIV4r@bshpT&TsNak_ zTtKybh(D~&VSwjEhz|*fwejwakPyIeBE*Lx#M(Hn2ss5%PK5Z-h*%rS zJ3>OPR;J(l3Wp1*whvi}wYl7hkl?byK2!=WzE-kEgn*lct_p(z?aP;BF7@`>T4dbw zPbdH1XYMN-SHJ!07oYs%348pm!=|6T~>2vU~o`-lJa z&;t)W@X!MfJ@C*24?XbE17~=ky_?Mn`#?5*E=LlMCqt2VisVwHYbtc7Kq485%|?t^ za20q2tr002oYAk>TjOfKR&NYkC3YTlgPr%RyI_e()MSyVo6Q5^Q^mI@d&K- zQWe9+dkwJXxWVFZB$DlR29)brcfjIM1R7h+H18_#h$hxidNM9C=A_xw%x+F_mDu^r z6|i%jbr&og2@eP3Zo~DcHNdW}fSvQKyI`S6sASsqiW@Jk*~tpnInTNS7E40O>a5D3 z{%dyZ20QOrcfk@+LerRh-9OmT3fMW%x(gPE;zCnqn*PBK-C)rq6lF%>uAA#wcfg_v z2(F~+t*)!UBSH#j0_9Vws)Qhjj5RU$Hr&qE3fMW%x(f!{K8B`|o`0~-6|i%jbr&oK zK~qf1aQ?vpD`4k5>t3)hj+d(|4dh}sZkz#j=CkgCMWGN#Q^xbYYqtGwZZJ3=2H9`g z?77_)91F)mE;D?kyE3zbG|Jbj!$vJl;U>5g?4q;%i50MOo^_{Y04&DaWvc3W)FT&z zef$itGoN)AEE?r7*C8z>N&k|Je@qP1A@L)e52Iu8{ ze5XBI1kJ~%>$3z^79T(zD)&mNT=>1+F|eu~a3#o*FFWV-Nyz=d-Lbk?dvF1MKO$&w zXs%P}DS~$2Qc2+9uI|YpexJfEu>>4-q+!mKH>6K_wkG#dhK9#t%^qLx040t1EX6_y zn+94SnI^b*-W^A(h5zQE728N||Mx`-3oKhUsx7zVuz68?w6_CkT z!fnnF>r_01Ct5@y9zobTDWB3XJZnPzl0qlUf!du}GpvCj>&P3-_&W>y!y5na>wzL2AX^Lz2zKA=n=g^>EczUFgV)5Ad z;$(IQ9nv}`6O|k_I2rzhg> z)gg0q`BoJI?|1ZyL-PsKnWL9x#jsl4EDXxf5F{f;$VAO@U^z0u35x+CGab35+D(zkqG;Bt5I*B!mi6z6^5 zW6AHt)+NrndbZ)|iL6J@AOc1t^8h208+;ysOzkvwHBj>7Q7wBG2 z6{QeCb$MkvxSpisL9U5T(pC>I6ZEK?Fd#^q)g!|F4{CjOSe;3H3Gau7l2)b5$^K+e zr4mp!DT<@MP>;fhJOPr3&E$REuJ#-BJ_* z0-%lyN1iI#F{kQ|{@=;lH`t9gZ=8JpN$=zZC&$Nsa{Ob*)X~e2 z(np_v_`buRKm3|Q;qci9A3XS_gKs>@AAHf3k6ih0S8iQ-{L1zHPwu~Y|MmOj{qVh7 zfItcG`*yp#U%h*{^G7>BvNPJjcAl~QuI-=Pe%Us&{miZRZvFh$*K7$}&))pt<}Yo2 z<7R&Iivk}B{I|fZz~h1I8=u^G^VRRY+Ps=rZ|S`dYO%E@gy3o`EK-ebura~Mbh1%x zG$-09(q=od-R0`}3Jjx0`S4})Yd*Frrs`m#KJ?;r?$(jzLd=ssnA$ zms3z9LmWa2ivq`Y<)Ut?%? zq_oFuh%z{X)1Z`+W-_s2$4ZHl<~UuMn6%W%jf^Yw(xNg9skP!{W=TAa6d1sIM6+BJ zp(U$YQuFm7NLHT(>L601mm$fLE6l%{Gpmz5`j z?m=d0A@=Gs32!567bxLC&599`at$qAvw2#HYdDFGBt6k>jmpRN!a~WYkiz#ULe0_) z5UJ#gat&zh@MXNN7r@M`iHM0TR_L}{fxlW<}wlOUb!|Zr_wzNo$RWm~lBvBHj4&5qwr#?Ic`TrbvvY`_)+OTLBXemNn=XaA*+w>JYg&8C zQ>Zq@!WFR?szzA59#mK=k|Z-dxi+zm7S*8TzA2Q_a;-u%`;mBh9Objko-%GFC5){? zdMVSPg25t`wgStFlQprQLt9!o*GUz{k~-|wT3vONf(Ttsh^7|f#+7Oe!(nUZrxqBg zgun{C9cnRrKaVFGEZ(SyqkIf*=maH6W0jDDp>#s(mv`T@u)t(1F_n{vgh+)-qeduJ z51MIqCc%o3366-YSgRt#Xah=?120@u9F;~u8A=Q`G%O0DH%MzxMxvw1aIH>=Jf>w6 z6Ra;#lb(FMfItim9cG(#RqEngJfEMU5sFiQpi?J5C{SI3shCoWw%|s0b0HFCC%d>X z3>Sk%8P16{vf2wxF*B=#@_N-$MzoO5TKy*4Ns{CJ&s$(5IV8i6CWbH;@&?e;G>Vc) zmc*(Si%`u(eljX#1wrX@@x;NOEi7VHHkt^gC88X*d*p1G5aJmaG20ax@9|t>2xDe$ zHsUR=e3)EX6jUY2AhqCNR1XzX*%4%E`J|nfVmTRwYckay;p2!<4I6u}TUsP4wj75N za5K%b+Mv_4sYryDv{&du#yyG zY_{EnG@&%WM4hGTl5wyQ?XqD$%@3ePHxm!_OLX)@j6&A!@(`Kn$?Ox1{S^!x>@P{ichgJv!^ ziV1yJ%~};M)A-EF z*xe$7gnN~Gshg~#d3ARo!8MwtAX9|0z=9=Mo8byEzNMw@T2|+cxLgAB-yE*+1keQD z@6TC{X0WX_;ssPG1e01m)aUx+cxsS<%60{K2#tcg>gvd>%GvJmA1*AWHA+fGXgJhw z)zsK18o_gr(kz$M7L3YZ;F#t6kv18jO!a7?TsfMC1%>EFv`NAki_`WvVhhD&oD7nW)&+nJ^O<>-5%Q^ceNAR=S};h>>D7r3rS;sVK&0jEODA zTE!e*%9iRm6iTNx<6zMeNBv~89A!(>bfU{;3YHpc z4|5AvU0aUTxc;D$;D%$uDOy|yCumN}O%%04H%s*X zVvaE~fj(up2baWl%@)Z>%x>4iNhKKR*SgbMNQpwhaZ4?v!&2xlzrbksD!Ne@+xeyu zZQH#h(o6NpsfH$LE+&)R)L7^wI#|a{9n8h;#P&iTYt(Ab25@|W@w6Gqm#u6}O2T$mkEhcF%80pYM@lyNcr>Js zx4(PAs@0s%QrSwsl&!H8UnsOr!{%7H|g5c1G5IXkk%TLud~ndQ3ClY6tN_8>ki<%CJb( z+r?zE31$(tSx%GnF|xJjsYgZ_=+I{LL{q8R@lXuNzmg@KEnAH~Pw2xDoW^?9F3Y1- zV(&K=7&Qe5_TiHV9&Ha4r7W6qLepd6gxZbqF++k1ZMYm}2evZa`j(|dxZNOek;=jx z5u&pZ+pJ4tGME&G6w?mVMzd5En|w!|hL#d_qf)XQ&J3eHc3iB?L}T1&w5QeKAcMxq zRF)>`bh^S1x@0$>2rR~rk=CI?Y;qXPRwyHAwrbU6U2j=bxt7$m9C%8*Xg1VdPaMg6 zi&@D?txoU;*N`&#xK*;dxnybDlVgcs3C(v>`fx~&;mM>9b)v%d)kQrDaIZ4|~nTH@&6?WT8i!fUl=LMlAAyEM$x^28RP@(L|j+1D! z*rkZN6p#1KY~0#^|H48^#20@A7-ln&1#6*^?qD@94zu@Ur9nSU^tC}rlQA%n zjkS5a6~jkr#1b~%y0BpLSs@~FBKX(<#u4CZs8vsDCe@r3Q*sr|bvq!nZbiu%t*t+p zPsm2;ST&I%%SC3U!|8x(cFNCPKv0#09-hTR>50~s zqeKxC8gK*1qYl|1p^|!Sh?jOCX4G@HVh!tgN?gy#G@3df1i7Obn9dxELD}NTrf#RcdK58*hu%h@eGn z*_ePyvJg}C(Drg7Q|9$qFC&!uXgq0;^W)KIY;|=gJ|jY4=tzRKX|+p3Ss?1a^Vtgw zT`nuV5XLCg(nQayT)kR`Eu;{)!%}K&r}QKV6c){X8&eOLU3*3i&xSdDYDZYDIg_EF z6-NuD8j=V%?RqFmPffm14>t9Qb@Hpu{{OQ!etqNWD^C9TWOn?CWBcf%M}xx;A9jHL zfAh*;U#acCXTP-f=X)>Nedn&W^Nt;5`|aDgt+#AtHh*iA3H(MN1&p8OKQ~@`xKphP z`+ictU+#6=8?QZBFz|7S*H7B_i|=pVcX2@zl$`Rej^ujRgZAceSs@wN`FK;zg(krpgtOuC&BK ztv(Uh8p^{k6KmFxW`7op8ll7*Z{gM}77Tnm^>Xih-?;Vi1p^EgHTQKl( zzx;CVh~K#N#Dam3o9CB%b^OMy*@A(O3+b18Vf@Ce>4JfeQ-GIygZsv<$%288(@K|n z&f>O+Xcmn|$jz*D2SdUS8u7H&K*?b;Wvt6_p$%P57 zQ5#?Gt@9hVMhgZ$wvsRRKKhMY!vzB$yX}{IP5s6#Yr(+B*%m)%P%l35bK}-v!Jt&9 zMlG^A6G}GP zrsR<(akWY+%nHFUX2S7UnYj4o`i)!t1p^;Xz1D?DC8@}D6J=vmIIUTQA{`gRbUV-R zxi;OZ!g!kDr!8yNT8BYz!NA8;ubGt@_#k34^kAmahDp;jsf&^Zh0#_#m`+3rL?Kj6 zXUkd`S%*P)!NA8;FE^+Aja!`s10R{;{5+RHjeE5zuhSX-tjaqw|9)-hk=RD3?2Zkf@vknZa?JOzK_9#lH z`XGy2LeaWL#8!LlUUG(7*od>xa*;Rq8ZI~wzSeC?P^=e$>tZekHmu}GZ$cJpJ&_e> zT$V#vsHsW^Jxa zxbIudy?DW0fJoA<=HXb~dl#V8t<3Y1a|;&Fi8=*L%eZ?kY*?KPpK{_V?7Yy`BUkr% z!YUerp>s2=k`)_=uCp_wS#Oe!cA^yndzF>420ql2NO=l%2g-G|Zl=gOci%@HBhguS zfJU+!qzneSGSQg2U8-C4GDy=!!q*j>Qj^0%RIazIS`<>@VS0+z;yug`r9dGcaA#l-VWc1L?5l`Q;x*gybTS3UaDE_4#2IFM#lgb zOCTr``@b?0easUcyhvOPIuK^=@PwZliIGc<#AqTO0uMa%&@n4qua>x>T15x(DcD1i zD;cz@Hk9@-fq-O!)l#VjylKk~J%#NFvutcB6WH7HUvPT8zgA ze64S%!2$o82#$pGW5nt8>)^1NOk+Af?F}LjH;UAnF-cGB1G<(hnUQ)dimr9&IO)5kkcb1Ju%9eazkLVM13j?-6;xDRxX;xQWg&)HII$g z8WW?S>WfG`y%UK(TGOW-iSW5&;+fak9{5P~v2J|uBN19762q4oiEtctZuXoxCU&b( zbQC*fYJQo&>qO<;g=Elo5 zuD2fumnC0D*~|1b7mu=hQ?f4@ubeAo8Jx8<#$**e~=2mX8D zS-{{D{=Dj0w{urFu2jE7*x0zTe`ROq@qIsAkxRY9g~H>-JLCcLTi^D%r}N(@&bwRf z7F&MRBe#XC8-ePV%^3z9hDX}%eUWSlPoFWxUYa_Ak`oZpi_I*+7VlNgZXT_g|v zY^eGrdFaqP_~88alRijhT_g|u?78|SdEn67`zhyl|6Oa7e9rBRLvqm|=db&UpKaMY zB+XKFaPix|;;`HP9_M%G;5?2ia@fzg&AB-4``Nhl$#LIdxAC*iZ{RuSaa?JU&$`Vz z9G`nPZ1((Y?JhYwH=Zx{;vV4k)_-vrzV%-}=RAgPS!U4nq+JYm{cQa%89J9!FJ`#w zaQo-anE!tE+6=$^HsdgS!CfEj_}Lm>GF-iBcQM}`hw0ycqx1XGA77jAwcE7A_w+8` zZ9f~yOTKsIkT2%B?J)e%C!OEFd&}B9zxX!g@QmE$x#efOdCBu$IpvF)ZaEx3@FwT? zZ?CM)^f|Xlhv_}N(Wakm=_S*J9>~QUHyw6=t|0s7JN4F9R=#6lP<5T)e zAKTM(ptf9XSeV9D@|{_;Y;^ZxQT-|g`I-QQoE@3q^A!}lrurH@VdCEt7c%L{qV z`^*0^kMlQ2*5>)ew_oV+d`f@mW9xp&^C|u1g-qxD<*!1HM}O_y$@)qgea`I{I82|? zU;5bMUou_xmltxJ`|%fehvP52acyrt>-O{A{iP4t`z6I?e|aHAhuhDM=fA(XHp545 zf3@SqdzL&Wel`&AcyY1hxp1TF#G&_N=KR+_o8;;~7g+Lqm5b!DpYsuZNgg}&e&qS{ zU)%@DuXK?-@^gg3FG*+d_|_kq4?sWkL+g?}*!c-?|NrMU_iB5O?H%p@+3rv8zIs>M z{otOSpn?Jev%8hsLeA&?pk3RG8J%`_L_@+bc@XHVWb^Bko-+J(t zgYQ2W9;6PQdF2CFetG+cuiU)y(ksxF?fpO4p6&nW{%qst_SyYs?R|Lf*Y;j__3c-G z`0C^;bM@b!eCXuAo_yO$!jHfwyh^XMlaG0oUh&C4dX-*jRUh># zz0yuT;#GR(&HsZ(>DY=-{{H&bivnl&Qc?G#DUY)5S3^E;OFieg?iTXQ^B)Vl`KCO| zy88vfqpbUFV;*JQ?;1Th*!aS8zq@*PA9kY#{=%!Y8#nN7uhJ`R?9aVQyWc_JU0$VE zTI8R3m0pSIKlLi@Ze)RXdX;uJv%sHtyf`=Q7kE55;%;eyKR&;76mpl|{Cuy{?)Sd= zOs~@J_rCdgUZvgdee-j@O1t0t=I3~ocE6j=&+;nmem9$+=}|iDj@Hd*c$Hr9$!B<# zUa93(uhQ=Jx_RPNdZm`f9;H{3o^Bp_m0t15p;zgZR(0T2dc}iRyh^XMs(r80?m*kz z^D4d4s&>yW4K{gPvyaUkuhJ_%+4d^EQp+u`(kr#x^eVm51_NHDSK8o)S82Bg1OMh# z+TCIU|LW0ax?O^JJUQxauYvb_m3Ftlz+ZcncKa{zKCjYl{{`ObReHrEf92s3H<1^5 zJb7il68KB6(kuRZk29?ZeDArFi--U4w0hwDcNvYiV{-Eg&TmsucXOp5uK&w=yWX{i zk0lbJ(8KjV9CJ63hwFbhvC?lpT>r=2{Z8QF`hVGrJY4^u_k9+I-IJP!>wot;WO7!)yKojq##j>>b5f?Kk;z(k?ZSB#jf!GiN|-zNcYmxC|-Ng7fcq$PmcXs06`7u8B;t z5&>+P8YdZ(%nTW`k}%=$o>hV}^%7aP%Ml@##f>qC%rT%j29=AJN!j2)nLMGv zA}OEcYKU#jT3D=r>N8B2+A%v9H@f-+A99*acP6clCPFYYwB*LTRX_WVtAF%7SG#v% zauSKK9FMW$gXHSW?2=kF*&7H=#jds$n4=g=jid-oLC$iuXrwgB)|EtFnni#HaI7+H zcA!|Y*wJZemWA@2Mruk)d^SCA)z7-)>K{4J)$UCdNfanFrC_A|K)JekNv_7UpiF7u zEU6B}RMghZ$q?iQI>XiIblM*kL!oJph%r2r8POrTGVBa9`9x`qBuIM1bSj-l85O69 z(}8^99asPGd9HSEjR^{_vSOYgQxBA@FRQO6!?9W$WC_Syv24B^%0*CAR1#;n8ZmT^ zC^Ndm3&}o-f37f@a!P)Y20Jy4Avv>Qs~GuKxFTTz$^*NI2wPJP9d95mS5t z&plACzO=qNn##6@RJoah#87e|6Y@xJ&u2!i83;DhA+kCWk`p97kTI3jB#`~63fK5d zE(Rw?gJINY5VgjHZp~f&1$SKigXgts_u8FHv3WI*Wia|day55ZeKj%EG7+W4Dw(#3 z_sFanMoYx_tX5s;3<)g=5Xb=&3yMaoDUsr^Rc-K)8jB^=aZMY?W;xC!Tzy%6b*jx(C3>XfD^f-35$RdV1i5R@a5Y*Q=?$ZqW7D(V zusmoR2*ohbK~bFGJtS7v^0)=D`AO93mFC0Z=ihPl_n+r#_m@iol`)y4v-t;V2Etuh zUmc~gR9&EFLns?gMfGqyRVB|H7L(?zD-ZGwC9akzjg*Hy3#jSFVYG`w5^UzkRzGL* zsZgRhcl9&xxcd9fbG7?}s6fe_q>)%A|3Gyhm(^EO#6**$yckZVxE4KAQa!$sI;*cn z#bLD-VMGjM13^lm3R(pV<7>&DUIg^VQd1eeu=jo_yfsXHF&{GvMy=Zy&$z_(jK$9=-qQr;o-*&p+Bb z{GG$sAHMkTxd$IO_}PQm0dlZ^<#(_A2apl)+Wz0{|NQ>T_Obngz2DpW&b{K^7w`Vv z?wfXBzKerwfN$OTo}J3h_3eMy{>ANAZj+nb=4S`~B=CcQRsaHd|Ng6wPYzq(0tyGd z)rZByqMuF=B(@)a1pU5zIiTJ5L5)o zDuGXU8oJ*+OLF;CD$Ubk;N!9F7d>*$n_s!=Kr?b$PS^c>LXdcsm2x5j z8u&*%4c)Io;z$fZa!i2>{DYTI-F9dOrPv%s$pzjEZ{<^Jj*>8n!5PqI{@&Bj9f`D@ zku@Cj7INUjo`&v7EJ#RRPG_Yo75F<(L-$+EOQ}LeOvw@(_>iZeJA{QahSG?F3rOID zo`&v7WHKb1D{z>=1^(94&>e{?s*>3hk`^)zi=&RAM^I(s)WBGJ)UoG<3fXGN_ z;4Pkp?r0<#BBjzI%J6~T^;#;pojPACq`jb4Zp)x)G|i_}StJ9$v(W#40BN@eD-B{;5EbBvAwFGJHq}x^kObfO5zgsk2{){XQ+q6;j`4keJH{>DxZGR zsm#m!&~cdyoz4~Pt9G?BbmTZ+^r)X4HfJfN-qqQ0=-eyif1wJCV_;n%jGI+(bXMH?5D#)*zn?@(XUCD>K)jo3!U>wMxK{_tA@npv(#Wa~ z78Z=u)!B5n==X7g%1Lw>%d<&ao}tn}?&tE|RAPYlH6&fK@CJ@2tA?0iYN*|}>l7Z6vzUem>qw-> zoT`KF<~8A_@9tSUQWf=C;UpWtObsr0Hwx_A%Sb$sgMh^tQ zqt1Zr5UDh*UR$3oWBySmQiD(^oCG}vR17D=@lYrThIgmvB2bH?K^r`vGitY;aD6hO z&dxFLl!#ipzXFfF&w)OgOeW4vPGIPj0oND|q0?$9r<7V;7G%+xRG~;o=QV@K7};K{ zL$zyc8jP%f!~IC2=gT++Q_ZS_%CvgrslO!xXp zo@AJ^(bp|DZQ*$9dWncA=iUBIU)I_4v}4Tjc6*BMnFou^uuGwM2H)1zxQ-g|@MY`OoZ1cg)G z`)=$&kROBD#@-xU zKTTjpLP~XPDLTF$&!L%aQW}OTLNh93dd1EsvO;J$vCYIph73hZ46<0HWT>N4MLC5s zdM`c2%MqiS#yhdLmChJ4vR+P{B&a%Q?9TrG!RFq^=H4;9W$wLUH+f}q`)!B6b5K9| z?v3xejP>fL{EW@t`jj8>@BtTi;MJAg$G^7x>~l8n;gI_q;DdC+<^exW9fsmx5d!}S z2X0^FW%pI!2g%D1_*IO%UjFtAyc~9aBz}OrJh`;vrYWw^G!>*GabqdOsBDU%a;>ut z=Hay()C?-sSuOUUq*Nf1td4 zS;rr#6szRh&8mW0xs<|k9VVB_!)Kf{#oOu-7su=fQuth1Wh!KGmMvgxkef*1GMPv+ zO=84%Vyvnl_jvhruhw^8^tJl4&pAB;yFVI!+MK+&y5{Ga+&F8VcSTQY>UqJaQ0?*j z8XTEws*o}(kDV)YG`QyHT+@r%B{S1@uHzL#nk7N0jMmTwnL?(PC5L=we%+2S%TT*h zGE<>SfoaoBravnY`EE1Gw6ghMqD(OamEY^;SD1n@X6KnS5S6 z>ka^3Wb}L?T#C0;d1i3K1|R8TwU{w1FFZhA9%nDh%fzSUn&;iN{B0L_IqW{O@c?;wlDVvY znXJz$;apv9GYw_NWXiZa%7xFIV082dIig5TObwZ4e@u}&IST8s+^{7~r<1bYoK(Vi z&FUpp;hwhqUC#dhBOAZ6arNfa=be1dN%Z)Kj~_dF%h5{?|LL%G@XiDK%KNT7apf8N zZ`l9Jy&vEE!rfok{nDLZ-pOtM`S#1UKDhM~a95xa_?N)92b@RzpZ}xZy1Kh5JomV| z_sT~Gd2%d@)!IOt2`V_3N?9(|k9+oJuHl(zpSN{{4{E5|E9X@;GmbE`QO68rXXQ#p zDMREy3*>C^y+Q(eABfMci#n`;J#O>|_H>IpfQ zZ_+hJpkxo_$>Y_%m#q!9_~L%fm&34oG`p6`OxlqwRgGF^Pm+r)sv~`H|I20+ zGY;1yL^PqK#^BwnqE)_i;IIKze`0Mmi_fdqTA@O}5`!n>d{miEi*%cv^$i2-LXj#y z6AK|zPIW4?d?tb>K&7uND?NLJD%1vK*=Dd(jvh`0Rw2%Aq285tuU@Zljb0Ac3Pu$c z+Qb0GAe5{3^)^KpN)dWo&Pshlt5hdrC}9CQ`%600OQPeR9bbc#_?Snhw$f{+%?JhS zBe>tP(`2X=4Rwoly`3y(b+cRPr#SuA9$@2y?__N@&Nj<6KF`WVra}&dCe$G*J5ejn z+CUVe)(TZCW0Ht!#Uj%e`>~nTy|oK4obVm5jd4c!0`mQiF@=WZUdAY?bsR^_dP?{f$}^H2(3xf}HWr52czqmV!5SIK7X((Gm;;LiC}mRnXZ zAC6J10V*9>R(f!8g4sLydYtT^=k!@Cmj$jN(;^=DJc=}X zQL59=M7v~=87M74XJbjHe@S%QQ`Kt}t6I92Ct8!RFosKPsgbfX?cpC2BUPBpRzrv-AxTtqG<;~ z6X&`&UjZFK7hOf(hg1 z%K@SjzMZuZ?+M>ZZ$zF(giQZR~j2IH(i30Q8_@1Y4?Mq1r)i9sSMk`_>7G=YRy9o~EbfH~pY zUK`9ad{?#vu7Nc5?2wFhi#fDXs}9h_Xf{-$DUBUXqA{o#4=asgvmy-4flN``aA5gNrJLaozo%*N4Vy%gr#vucT$^*I$|y1JG`bTHiAoGvTfyg2{m zTH)*dOyMUxFrYJ8(rH{0os}7o#DycB^ni+sLc3y&gKZ7&G^19Yv?t(LTnmp*R6LGG z#$$@mZ;k;QCw%K`vzdo)d0b4XC9F~7!(FsPrFAhnRHOn$m5@S-BuYx#=riE%L~|;1 zZ`uIE314Gvj5ESFEK#KyluArG2{SL22GweYFIzpeTw{oQv(ZeN7{s@m1%mAwH%9=` z3E$ecD202cV!-F7+JJd$k}*Cg+Y(s(rZnQfUDzHINXMCUgl?= zkdU-A;ED3hfx`w=y}UM?W%&9?4ht&XUsk$w@!^WK!gpoL;Uf{OezOPIIPs#d&1N1i zJ`%ZhZ*~EO6E81a8{>?4@sak_S^qzKL*KZ1deYx@J+jjg}lGB)44Sq;1=P~LbqFhBp#E8Z~Ou2zL> zJ_h}3KKl5JhmTjhVd9&BkCAm90^>OWACnM2W9d2sY~KVf`4xdpfRBe>_ABW5CUD8GpgSkvW1f83Jz-}~prwnXYPPWoj5Y0) zA{NOmHBu{7Bh01iQ;Gv0IE=6WXF3DqnvV(5#Z!S-yutKMz{iAP9Rh9N1S%6Gm81;< zD#cK)8;_OyW+q9nC2AaI5U7|MgbjR}>*u4<@j3)ra{@l*>6iTqn!X8K@+)ZgCUD8G zpgt$yV;OVJ$J}_GHemQBaLKQr=9|DJzk=$VfRA;7==SiIP)E-QL_%ep9D#8!;v#6(XV_m12WNA|>V8_jVfPaOA+jwMbGG?Cin zHxu_0y{IMKEnrFQrl*nnDD=|ko4ov%bQ{h6_KjW*=PXa_Uc^?UrIy6L!Hd|6(VW=V zd*Zmy!!2=Mc73H!ev7Q&P6{9(^t|yMWtu1jno;Yr!IgaUx<8A{> zoVF*9+h~r{^2Bj>x=Wm*P%lr65 zc(!htAA)B{tK2;!=9X7Mds3gSnpQrg(S50GFj?2Je#hWq{Xh43=vX~C`hRg4ia3&A zXDX!-d}jt#SRNfd-^W9A`9)jy@a>stXmi zb{7bn)w|a4u|xvsBt7<=CmyX#oO{O7_rL3tCvsy%qoetX(soR^f z1Y5w+nZSuGh9vq-LC8d*8JikJW+AjQxK1-rg|f#G*J1|9SnId5+!0X-v%uiD!U9A7N5{nP@|3D1>AJqSg zN5b&=`hOA;tr^sswegTe8k-4bvN-<6*7O_jWdd_=>PfHzB@~XcX6iq zY3eu@;PCC>n8;pU)iVd*O7eoXheNDiXti7N!cdl!nA| zi7M3BQX#bz4_WbEx2IXweGl|?#V8@OgiYIOI9f(?aiTDeBBx0{7C+6Pxzkfl=h0NX zJWh7W>uo#US51t~;nP?tf|kwGcD}A>5<|1okK|2GuCl!(Q^(hmM4MNAq5t=lPwM~q zXs(wRd9l^VJ0q6Pn4~Vw*gs9taliV1D~;EgB0edK`RUdFi-!}@a~pdEIi+iIYgEMs z1Em{c1(ZmjSi6+h%{a@LC_5O3ge*OQm_tL#D+kNluf7)etKDcvpN7y;u{=@Cp zZ@*~kU$=hk;HN>3zqcKH?#4F-o_7>Dc;Dgk4!{4fy7kRl)YkFlPj2=%AG`XRtJu~3 zlmB>Pp2UtndHkm1CypaWZ$J8ujgJIA9{A-0;UzoZCd;OKoxS+mM@XxQ8$Eqe8LPEU zwbsbHms4w8^oWPtA5{+iGVp^Nj@BXg69e$an~zXgep=V~a=JCJMH8p(f@|Hj1X={u z&g;Hv2VC~qT$|s$6WxRNJZUUW>`A6-_prv;Okvd2Ivev|<2~g{eB+r5>QDWBYzJI) zTE8N9m#{nEf$o4?ChME7-Z9zv&L`c7q%x>JAV-Qe%45bj&z)VRg&Q|M_@pPs{~vpA z0`5v))(Iy$`+5;vKoGl|3r#PfE0s!8VXsswl|88~sS04IR4P@KN-arMs*(T#tuj2W zaL4PTd@v#de0<9A=zNHXqcVaB$|54e;sT-~DoWEVve|zBq|TwcbMHB)t}-+DO`oTq z+&($K_xHZ_{@?#n|Lx_;>7+L&jJa+YMX6Z6zSsjj5q-;PjD!?E?*vo=UZ3ELzWKzV zWJvn^9U6V>i9^GRUhB~4+fE!BmLhOy^uIo38JE$UNo}S!xtixzn6_1+70@qyVli%;nCt=Iz3@>@DGw(_*>v(vEz?XO6K<_0`#WiMDb}Ai} zY87+(q}4R9?@CwICjG80PTYAnxFsEleb;Gxhvax*e8(=G#&<~JbIW5b7Z#k2edo87 z{=o}hfd0F`rS$9J6gxhxogtlbe>}y$9IOV~ffzdyTMS z55DL>_Ap|Ly6v{`@~iX!vh(06wb&z0gHqw@y%z7-E!J*~#A0rA{lI=;P)4tu7Hdf1 zbA$4lJ6obJKP}df^!FEQ^go^!Ye>;+i#7Vwr^OnQ;-t}i*&!O~KDaafmi~5HULnUq z%2amTt|&>ebz)4!t0H|*45MU3TatT zUi+Uq^@(5Yb5`CS&0CzM@cG5``W4_>pX`0#sZT;u-0zdU$D9^tNYMvYzr9DFR=<$K z=Q%Wmu8(>4zW3CjA?X8$?mg<#JN{QxfMTh`$~k=I*)_(N#$+ph{qlOC$6`-84Z)DY z=XorVUSBqiJ(&b!i@U(s`Nrti-$-K5I0>k*Jog7w?5QW=99Hz&aE?9gBqqaBJg|w0 zJ^dtT!U~^shS^h{Azv9DSgMiFJ!d&q1#S;1d|s)d>lbZD{_(UhLbBgq7?FEU3nQfH zwS^J+{0%zhfrSyd7xX+&`&aiq`^Qg@6eI7996mpK&&Ge=B(D9HG&vQ&!CrRFr4lV@}>0*1vARzAk;-ajlOTzMpIJ!=4pE6u+Es9mn!+22)O-Q1Qk%@C$S~KV(Xyo} zc98^kJ~=A2@wz~VWb4Sh-LPF|LMWZ0t{C8gmsZ8*Ef}}VF3e{a1v6h9^p*Xu1QLmo z6E|#dadf=rHzCWjvB9Du6y)x72yRqaB)SQPps-fDq3?epkWlLbA(!hAeS+-68FbLg zH`ur*7g~8o#Cjam=IjHTW$T|$Bn&S* zR;m&Yv)Pu`mn!Z49e1&M#q7&Tvu;-txkky&3TYeN|K3xHUY86jtq;zqDv(>Dja|R=;Xu%|de^%?B31O`0vUUR;a^5UcoG zi9jNrrMf~IU)YHOHfM}}vBlRG+HevtxwBflRRovMyI!M#DA;jwCDBbI6MYWS_$A_A zCC4v{WhbxOBb*#&lWnFhP~gtA$~0$`Gs=z{NTd+Ou5?kq5pUbMc^{++nvJJ%l}gX+ zY}u8Wh1^QHC0k2Fn_Gbdk~i@JYk*{|AWLo!Lf8t?ZF3ziMNF;QJYB1IhtFY z0*R#Eh&Sx~ypxCH+9a*gi#)7j!?Yq~NUo%nlTub>`}jOTMP3p}B=BZ}LCZF{P;KN9 zCXrMTF_~LH5|Qs<1H7GMr)DXi;}h1-51&XRCT*_4f|W(6#U>nFvKKkrQyFB!jgsR^ zKW=xbS_5ed==LO#h)>&%W~vmQqpDL8(Y{%syF()G6f!Nc*O{h^-U3QCaxCgZKYk)n zVa2$Wg9}Qtq2(%qJYu?zl`p32;5NWgsq0{{O%+VileT_$CDB ztf)F>nXJ^6;zFO!CUL)F3Ij2b)%;<)+JXhHQMR1eJtq=b#F-O~%p}S5h`F#Bl*w^# z%7gtRNoCMk^u&T*)U{ce^f&JfB!+P`8E=C-6c#ebP@Tzogs3OoGLfia!<1LLFU)Ke6NPpw zm2Qe^gPw}mFgb&)mN}hz^}O5H>$94)@u5JX1$xDaRwO$;SMPSSvurL6A@UepIcruD z8ME&sO5<*Z?wVHQQzsH|dSvE3r<)%p5zrgSzK~Ts0U0YovV#~L*O%tHSS4$v%~>G9 z$v&vzT{F|0P6l|D>kb*gubHWt>tvgLX4ayRD$xeFs_NVC4J24wU1)8jO4CfA;0rTI zG(`(VG?<%`HIiv^nt~EYT}dh1CnZS7OS&?cx>7BZaC|*(qgb!E5LG5UR*MDNMj6Iw zx4~LYZS(nojwag2=%h573#4jKiX$gEVC*7T6rBi-Ub;Uh+L)XdIz-XgrUQu@!la~* z)f-4z-f2l*yqwcP)5#RTicpJJ%T&9@`c8-Bp&cWTD5rWnuFPw0630hKZG`7^4zX;T z!9X610@_0{b1^at$`su>Y1>PlM@;ZVoG#2bGVTwd6w$`4QL3S3>Yim%i8@@xNO6n_ zn?Xz6As3dQ3bXQvZq#~spKMx`rHq>6xET$V`JJHcE}pn=$OTPLhG6804*rQ zup)uUXp*fF+~)5D5-1jjjg&p;7Q3|5VzR~TXqK)Z{-|HcIH{K0gw<)AO~@`1y%I=( zG)_L-osSA2Uzpe$iQ?SGc$!L#C2}xkMx73|u&H#ZoJTkQA&^M)QL85;lgp-B(7AkT z1Wmda>bGRPOyt~D4^eYdT&#fo^nV;kKnudJbVaByIRel3JAPhUXhyBvh-Y(P;Mm~1 zV5x0DjY(;nT}cQ|-s_?@Rv}Tni<^tGY}XKUp32KfF5low-pu!giL_biV#gmm(P8tx zNDdMN0Sa*U)1#(BX}f$z;Ci z8oUIhbuxo71(CPVOn;uml5CAPglU>p+jG;o^zcBUlo*zAHPvY^khsR`1SKY9uagvnNt+ndCRU1_qkK<4dSN6Iz4M~Cvxoj3 zcpxYbxe7^8jTXF2)gmThoxr-OO1Cnk%}&>M|243grX~B(#{*@Y`mK6OnVB%%nh`q)#o*n z!)d)ornri(vc$f$fAFRV6M0AE;D-+QgZM#g|0DZv+#>AJbd!+j`BGzg6A3dGpJgf3W%L&FN-&^ZPfxxbdEi{}g*A z*fUUwJvw?%^mjKLu#4dPV)w>=@2Gk7cyJTp2M%9*I6tf(J`UVS_`XZOSZc7V3BV=t z(hEn{%Com`QnA?04L8TBq|7l|zSgEIr^x#^79WrYWLRk7pb!>H91vll`Gb5|D1Ly4 zh2{=&0km1vTN56e4@kY>E*Mc+pWHs!JlG5i-8k3?3ymGb!a}15(Xi0SK_nz}|6lh1 zx@)=zW{_g=PKc&3_IH{l?}u!a~2k`SlYhAvHwF ztB5%;-!h!LL}Bf*b~blT3-&_j#?EQMUI>ltoEGeb(CE%-!CnZB?3@!y)G>DwXxTRh5qN* ze+~sp}!IN zjj+(SMcx(``sQBnr zT50WW?{0^MZtZS`g>LSiw6Bc|4cOQ{XEbSiel7B~lhV5IvA!Dl zYFOx3B40Uy)^x4KPNxk*)(RN!a>AOkqR)yxD=f4V?SzF2Q6VgpkMhSa0GD;2{Sz=X zxzY?1HQelt#a2)B$@OvI(O)0^by(7!4F zErQ}fasTjpBDKhSVh4YH@a7};;O^st!{R|R@}A`y|K_hOSNV@#4`u@%zxRc`|GxKA zd(Vk|ZtsrWukQX{>^CpH|I+sE)m`b>+J!H@ayN4Hv7HYb|H96zqF*{(96xPmwDD^@ zPu|%*{^Mx((l91mLL*<_{vWYhqw(lFqA!pA*wMo_e&q1aHyAKyFxkeoFKzwV)-N5t zacjOsZhhBLZu2iUZr=FY%@=OgHXr>UC!GDuYU)F64q(678CU5PG)R|*)~v;$qrPBP zO_)sBIa=W2{nSEMMSt&ID+x`U&nC&dH18Qr0x1@8c#-QiYokm?qHs>1&EQ3vAUml- z-`e@@K%#GVlDvu-eivFyo2isK@hN|%7AmYT?hS^`bRWTLwc&yhqH-Y7?IqZ3vg<*m zQhsXn(w#0iYow&JN{ZtX6Lmt>$8vepoVSU?M+6cYm@rk7aXba*Np-F^Yyz7wl<#!C zg`(6fsqFSmN=U;Lvi1HzVgOfEY9N)TjtHx@&Xnh}&_eHuKA1jYTk&k2BG9oYR55P* z8-YY~99Jsbw3p8!eT1lupn8Tu9dLrA>5dv@1x^(0{J= zR$a@cBx=A?6hDGy)dhi?-Y_R*3ofgdc7n-vO>BZKiE|hv+(HP#u4)+A`Y<;-iaCbM z-C6=8#Cp!-5vxh;1WT{luw?k+u&Ja-vs5aGcD`gtCDo{-ZL>()1vS~!t#l)gWx2@5 zR#vJ0OmAWZ%&|HRC!b~Jty-aq7qc>>vBuaL4fMh=S?lJd1hW12frRTI3E;hv8;6UA zSq1B#0@|xATB2PU)W%gsXwND*%OZ}x9ZYKv@lwiVJa^F03AUAm7_?&>yhVaF^HxW+ zFd~k^AY}}1c7@1C0$)w=5l(bfvJ;0X-fiHZ(7o|uR7(?eOf+2{Y`SRMkbr2^MzFM` zS$uHThziy1BxfDGHzyGr>)O`#W}t)3%NaUu^?XLj`zk%k zN=$u{;L38UXIGkVO@`am%6wd5{q3JyNeqd60me9|)5?m9KIeroiKq>-!BbeSov!89^A1Uut-Bel#_26 zqk04EQ`!H)z$$P+m$r*i3glO3N8J>iWVL#?RqK{5kl+`uq{uE(Z{ib#B=&;k#3AOl zr4HK6@yWbtf{bO2fl4&HvqBe!ut~ZTFO)L6wMQ*vwTr0L^MdSkXx%crE0J38nB5La{gjmG$&?5<0tY&Dd7!t_j!R1s5vb+ zZDlwn8m$CYE=@}Y3YDcMO28SuGS7|(x#4(2I*V+7eI=nS7#rkn9B0@eRGe7$u#0;M zziX2iP7x&@){Du>n1P#mL5RKz+K{7{wFQpKid6z>&Z?+8UeGLtwdQ=j&Wx!ttw422 zR!4Itlcl9zpu*EKO?pI?@i@}6T^wm>i(<}{@daBar=xgrQJILz#+<^Too*oEh_*|! ztl5bdd&5#a1=4wfG<`YK(rt`}l8RYn#YUOwQTpbu1rj|bPhj$J$beQ*$U*{0hgHj~ z3a@htEgcNcrTvlLLlL`NhX_&oP>w* z>X3_mCXnC^u%gn;q}zx!ET^hoqG#o~1Uns0$sRvVO?yn-R`FqCXhn|$S22R?3Z1$* z$)^_ltfEtCw?gtWK4Byg(3Vu2$uu;sz_vR&OCfZzN1h(?q{-$+iY{ zf~O!Gqgj`k@##JxSA~ME4eV4}S5i=h^*|EMY$9VoiM&tpO6;?NihRyP9iEn_R3eSh zKGIQH3@n;sW`_rLI3X2#UCcw;15OHRtcJLTCMYSisFG%7W@ZO92+wt^B3W;wiI^<|vs-Ipu!J7!R{WgF<_nSg$svT(#2nAcAHRjgcR9R}!iR z6}!_)rYWcfpY1vAid=OFR5ucfMs1d)w3d_}>zRV5=^Ob#Vye!vPO;7FZP-b-5=p0B z?|ER^m?yk)uG37SRlF;A%iXyW34%(UX6DT~!}J?nvbWF*C1>K0Twm+c8JsZ8`~q1t z#|q0)HWv%F=c%r3a*)Wu{fs#0Q#D@Gdg&0OGSm?ImTYEyl6>DC?TY3n%go_k?gQ( zt|9v%^`X|C`)rYRTca$t@wPz1nidx|IwcR&APc%aPSp)bOC~hW!}$t8JAcB_`$VRG(h$krK>GKwQcsd3{bH33@{ zHco=1mm+yL#b?x1ZW^z3>S~pDkYN=D!#1?m6sjW^ifdJT#GofTYE(8qZPc(~4W;5n z(Hm#56yF*lb&%FLuXU=hzmOFNte%6;eNzmcqnVtwDMm-zbemEO6BF(8jdZ1>rw2~E zZ}bQA0!61JAI~~lKO0DpWSc|_(^<7>vx5P~f@Y;vg5VA=w%S5L1sUyice04%9W3^> zpg3}7r(d+wMtoiX`%3CF(F6;6eQ07CY_;L3bJPLLg4jr?__qftQ1Dc^TEd$KJ7UX; zgsf$1Goe9dbF!@U<8ntbvzfL+Xtj<17mTwLXOFe7^OrzH9p$UVIWz7rP!YsLp=Psw zKcTks`HCzL)EZmP^CJ$eWu2qeN|I8*Im$3dO9wfCFwBOxuRK!336!8y2u!Ycr76d-ni}; z#*#lZ{SFte5w$qHXg0>Ab1+)z80MxH%fm77C4F&#)myVZpGnKB-CU?*qzPF^if^vMEL z2;?9bD5(p-rMJ2b5W%7iu6QA=_^dgtw~_{0RuLkbrik%`%+ZyFbKnO$Zs6tdw@$3; z33`T0Id0sW_!yEMC&AN>Ftem-OaX%9b5e^NB;2Evo!12t`O-p7_drH*EF-0Ai%~)4 zOCuEbN{pwI^^!V**q*5mz-!*lt5y=~+{-8YX}0PhGGeJ&!?0*u^@L8lAj({wX!_ke zYLEGRQrLUV^8UYg{p!W3+b^@l5@i=IV4u# z^rD^Zc2F)GUaId@T8WO_wp#318x{xce2S4f?Uw8F1$LH{J5UENu~*;~?UzH) zqG%?A(%BMMnD=W%$4r7(ZeyxebD}wC(J|V|jf<_il~;2DdnK`={Za^8GNC8&ZhgRX z3m&g&db#Bd8o4g6QzN>HCW>NF#}|60p2ZRN3bdmAVhGxNJvDB6BG!y|a)K?j1)=UI z%ZsULK#hgeueW%%UE#!fX)LiBra@*azQSI4(u(%p5VWbeXNd_`N>of5jF37bJzq&;rLo^I2XhwI&7R8|hU1lO zKpS7tem(>(p~T~`X|s7n8PP4jHSJ98_Ka0H44hV3z)~wmjk8sjiZ?-#-MON@Cj>3n zK3mjFG+Zf6@<_pI4hXK&z^e2x?hbQPr>CME-5!FWO$!9r9V^<;g`ln1XQO1JD5o65 zX1Oj1ENpBO6|dYfGKdOZ)zDVK2jwa-4twSH741KUpw%IM+)pDjznyB$=iSn%x)>`8 zZh^g&`2{%JU~mP{Ft+Q-9Lip~ZAJSJ7w3b&%oCGo1r>}ugE(RtW#h6XG+I*_?8qO1 zc{0D8=@9j_OU?7Z2~S+nel`Sce*uQ_zT9ln9N`v8g{0v@Ug+^C>o`c(S)}8oQfHo6 zlzHHUTUWGyAA&Y9o8`OUM0e3n3ZQ?Tv{HfxE7wQOphu%i9D z5VQ;4C*2w{o}+QbZQ0(eHf!@tZC;OeIWi~bMm5(O=!+870JZG#E84#eK|AFWRo-t? zU}g|YQ7~bR@`H&0H>O^O%niF%kxF|n-P$RG(8wcCi z)v-7#miS(>q5gUJA0h zo;u2Q8=_(L#|6+k+_IwmbO>5-unHu6?}JlSL)avX9y;hQMoDNk&>^|+lSHc1?Uu*P z7`$7vS8iU>{&fi2RC8Y8?IP~xx=EKFOB6a;(EOy+n_;w`8rZqKQtGh@Ew!u;H?3&@ zDg-Tf*0HI~VOO&2Aort6(PU0Ab9PG^rL!*7E_;cB;jv;)1=fE5iuNx<(AL^zW3II4 z3q}$ps!?Qz+$i6w^NoDVnuC#>)p7Djjscsq0qtW~w4Vw=%S#KAoRsRrLE5L>Mn?6a zs$a{azUQ0Gss@g!_;hVL5&I^`UirQi?O%kTmD6KxV#cjRs*ht6+Q9TN*~5oY2A3DD zoTC|e106QX%~AyCh!=qQUpSU=q@8wF#R~A~a>G$AlnYIyDVS7YsK5VuY@%Vxp4a!Xk!Bb-uymMXo z-WBbihoD8}W|FI^Gra32M0v<%Tdh8vM-o%0I!d=>aF&kIbfLyrO+fpo744sepw+9H zWI=M6YHetCIgjfUGn!4H{wzBkbUk4qH2QV72H8Ut`1X-2+CL3JTct{1{w&SAV@R(| zQtTk@6{>8VMUzg$UBs6&m*i9%p>qcK_Ip;ee-eVW1hYKvbX{X%mS*B$;-`|yCIZj& zL7(dAM0`@VYe_q<@)eA|^4%-ikB6Ye<}QYV(=u5I9}HP#%$0~D#M5Svn}99xkK2UI(mdbW^k?|W{h`n;K zqWwS!T87WtMbT=_;sc~JE|$mrVZow@m?l7_Nv;glz?_Pxrxu+YsAc;r+V_W`y@5>3 z?3KM0?fXK|-avNY{eve)I?+c&Vt%Z#|I6S^|8w`z{fF&+2At{t***IRI^uT|yL&r- zvGePDH|>7qxOjZi&Xt3E4&EJoMC{h1-5q@=zw?-b+0h>!yB{BRu8ONB2K^>6b6P^wM)L zp_d+Z{F&poA4CEh_lir)b}!Bncrc4RG0a@Vts7MAX2~Mw4NMWtQLclH7p6E{+Yc72 zt#E@zxoU$~Jf%{f&06{Kbo~~==*|;1sOZguGOuxVuh$rX%+YP5sjT1p*AEAiRxlZ? zK(wZ~G@_SdKEG1p)~p+ZgUPFBkSDojUvW7{YYa*iPFJlpw#X+#`lP}$HUMzh&a8d9zV&R*N=_p!zHLgIDsMtNiH>O_R%CYNvlDpwxKM)S~oB-DEak@}0b#nEF z#!jo{^#C1)gK5E|w&}j_&c{V>I_nfXwa%=CRP+lWarcJAeLlcdok@-`Cq--I<8?(Z zte*ghyeD{!%A_&Y8e*?3)s#|s=;Lelnc-kYptam&IYm=F#upoENuR8pW>CN=+$z}fbOw{rX#IAMLHZT6%K~1jO&}7)|@F<$-2u`97f@9nSL4%gPp;tAnkj}KrBN0EkIau_^^wb({+y-DT*6pXT57@*`;+-T2x4?7!v;($#>GF8J zLEfNOeDZ}jHo$d;s?e(2e&14B`AQMj*W9up1&`Y(Fn*N*i(3Rj;jsAlLLppSj}eEt+^iE->KqJi&PN-fZ13(Wi#QJtZXW$suvs zkT^6XE)x=$4saI^Fy6Jo4f@2~Fh&yR4HZZATJv@P#a^|-Dg1z^^DHm7yhf4giz2mF z9cqS#K@M#iBR2 zjP-#-EU1^#g}5JF@qv@19AWmy*od-)0qskg3MIdp-?u~{c>MO+&>4n3)|;l+#=DvfjaB-n~qC3O|prtpD2m_>5A`S z+@FB@@vW~%gK3c|OAFH@Dbo_b7P|EzNp!HXZ{YNFoz8Ve92iEl2DKWo=D+CE0-Wyk z1zmQiMghkteR1uOzZ@pw+dgAdXkAL>KdAxh{_M_PmboiOWS0D0+4?B4G!HW*i z{d@M`0A>dsx%Y>AFWq}4c;o)%U3K>+uoK{AJH?&I_FK2@?I&)1a;v%Zvs=5HZ{M74 zLK~mic=ZOq@vzvtV=s!K;C%lZqR)*!GV+I!mtL=xj9vT&e?75>zI(_6--aF_?taeJ zbHKfv&s<*U9fzSrHUU9!H-RQH5EMsJ>({}a1tac$_PIO{PCE-{&)`WAcR%Y~p7m>P z&)|WGyF2IcAnP})UY`fa5_b#d@}O%iOPb%DVrHH$kb9utWAp#S3)93O$I1V9jE!y?M9szMkoy!w04iONC z&GUGY;o>j};;?ZpPq;Wtf;g<7%M&gRlOPUj=kkP$!z75q>bX4O;wK5>r*bY&xOh&2 zcrKsIlMEHl$s}=i>0F+0@tg$lTs)U2Ts$X1Jd@}0gp21Si08t2JaD*phCw_N=kkP$ zXBfnD{#>4L@eG4_#?R#m7tb(==iIqG;o=zv@r<3z6E2=%5YNv%mnU2t!XOTxaV}4| zID|nQKK)#taB&EOIDFc9JRm3UgL8{qznxEjIDG24JmKOn0pjo}=kkP$!vu)KC!fm` zE)EkQ4zuU-gp0!jh(q*To^WxP0CAW(mnU4ingH!;`dprH@tgqhoH~~$Ts$X0JR|4v zK;hyL0&$o;mnU2tLLd&|b9utWAq3(uaV}4|_yKpX-VL406E1!rWcmL8n2py)j{nQ? zQ;+`OsB`$YhqJ?-gU9c`c^}{Vqdjr=p4}Jj9$)&YOOM@o)y|#Uzp-83`q-Ab`A?fK z+5E19pV@frMl$w0F)I4Ws0D75`H6q?i`d5Z{F^BrB6!FH|NruU3Hs8=b6$MujJbrw zdK(rNchec2H)?{;@;rs>o#j~y*KublOq-xrJx}3!t9X{eb=+ABQzqzs&r`VGe4eFn z9e0*O#035Hc?#G2>9Z8B>iCtSy!r7&aOeb>1P z*Q4(&h3mMp6oM#y(zy!PgW)WN>$tNNf}n|?t8o22=`4loxU&>O=G}LmqcE9WAIM%` z;kCHyD+D<6?mNy^xE_6HDO|^$r4U5v?dK|7kG``MuH(*92%_}1a}}=l0%s{)$DO4R z^cGJ%SK)duaF)V#+*t}iZ}F6K6e5XrS6yG>wYcjm1UU2VThCFL%&u4H>npq#cYTEb zXWspUa}}<4J7+0e$DO4RbWe{zSK)dKa+bn%+*t}iOLO^Lh3hTISqj&2XDI|N&Ew8f zcumr~GZbEfJ3}D~TAEwVRk+@QoTYFbca}oX(%gKm!f@3hiLT?$QV6O=;v9ttwBCYT z{|T?fU0)%bR%}$7KBAd%8%D>#XCmM@JW4n=WyMF?UzWwa_HgwhhsU|%X z9HB}+vemODnse|Ypr!+XccdJ?^4sE;-g>HfLvDwZNupGhYJPifc|eSu+`MElQ@|pE=W7 zUzw_Q$#~MlvWzE56IpcSo4uSjDib`%P!&!voQl*~+l~}9e&S<4^#{w(mp%Uab0ew5 z`h?nxZ@!9NibUnxRu)E=7VclJq)OaEF~N=RkXu0-4sSV~Oqxs#+3L`ra9oG>2q|IA zrzQWyL<8J(FDmlQG`Mt{pNx4*TTE4)D>TPzHtsLY`_T`tK0khjk=Lvdz4)f9nWd3| z8&^i|U2fFLx|9~70y2S1-8L^_!mP(yTGm5o$(^UNCBdGkOwn)DzGYwNJ;?ZTdLQn@pc&++jxZl zdE+x6t+3lnNqN0&)}a$Wy0YV5?8Tj>Z7)i#KC_`Me8N>^X<_ny3%4)N3Vw}lf?8wR zExtB`ghD~9H#J`!lr^6$&L~~&l^U+V^z;)8l_@Z>D@qaw%Zn}DUfTHl+n1j&I1X*; zomZ1fOP_JSrCXOP!v&=A31M8(llmB3!_;grgI-F2RaKe})7gI zP3B!sbF`i|(%kvQ_HHdLowLi&#ciSOy?hm3+I!di_HJGtqnV_KD3%TKEYFH|+rk91 zSJASL&ga|Xjyi*qP`c6__YvX5vqMWWyOZfi4&m3$rG?XXFF*ag&~`rIYGN${HZHTN zNxe}@TeHqw&xxJtq?e?a=2D0wzSUt@#Uxe)X+9Q_Uc{h9gsfdCKD~C;++hrCf#9|pu0Zn%)nK#*>0UG zJByx971Hx^`n1BIbU+t2?n~Q-uUmfV8=)<{`RbFFUVKmo)b*&5>ZVPuJ_RWnxO%du zj`>8N$G`ZeeYS8I{m1+Nzc&TFGz9ovK{)s$!G2 zQUlk_*ZtFQTs7_&TDax2vhW%&-hB0rwW8j+Y#NIN)yu1`5hI9skO85p z$Ma=uJ|6NjLRO`c?trUrkckYg%@2k{;z$!6(EeO#(OTL$Ovgr*A);kI%_YX}a5n-<+I^=c-bWThq; zUarQN4`|mfv~$(}u&b7c`{mGf-gfngOFOd!M^+BD`g>GGSLiJx-TH5$< zXiIOp`h=yW59);MCL{1O-02Wdcc#19#jF#bm9kFBGfcE)#ZA7*k;!gPa!>2jNhfrn zjjK+m`k1AS)i;H<@v&DQzupP;vkF%zQc0()nWV{}Do7vG_2#O;&Pv^!Zt8|zPf&R= zuYI%RgSVgyO*X!6v4b6*UwGm$160W8+l=f1!n| zw&)pOTUz*x`$AiI=ha)57CxZu+`8N<)qI_{FmA@s2-?n$9EdMN%3zpq<7iVShds(q zK=L&EO*dYr?Yz+5Rh#tWk1XwdS~sk{`-fkPv?3pf9DeZd4Tmp0Y#qiAHxE7*yER6~ zUa;}ujbGe&;Rbu~rh^|k;1A*lvHg$izj6P?(QjOO;U)IcZO8W=f9UuP$1gahk8eHt z#?c2sp1?cyBYPj-`^CK%?y-Bf?cTThq1`v^zF?OI_paR*yD#>k*c+nF=;I?_i+o_` zzMT*6ym9A;b~-zWo$c*E+5YA2AKC6~-?hC7&KJCC>xZHrjJ`hlye)n!z7^a2$mSb2 zU%c4@_XI{RefZKZE*IX8zIyZzw{KoE2@zyI1Q&l%trk~bSaZD9bS1sE_6inVWFQtO}|;c&{?lJeD?|Kh0tF({JpTyS0BDB zEcE9Oe>Z?O%G!t-o0L~oES?$DVtXwHjy`_$p|H@89sSV>6eo(kC^aDY!3&-n(^@g8O3V z=ePbTtO56IeK{=jb6Y3*l`l5nAGf~v#7!z1y}7O~ZzB{IM3eTh8BKZXq3wGif!;JE zaO#Exx=u)-GYJWtj8A+y5Ss*98Jb+4V?}Rq&Bdu3f3Wd^u+ZP%cz;;vdp6z|7W(du z_lAZ3-o_tIMXNh{DP+PU$xm>-m}p(gs|`Xza#Vn|?-3<)F)A%O)VB#_951mSTg0iWcEJjw={n%cXWeqSKf?;yl0v|}bgpJ)($d(dX9D3?w^J#9NqpI~(;Qg+p_Otysq{uzd0YW0XH|M3O)Qk2fYh&Pk9N7#w5_c0 zH;}sGmgfKk_C)p`WZ(@X09g^Zb6WjVsb zS$V;6!4?_t&9jz%0ek5b&0LCoa}V8 ziH@e2qC1_O5Um-q==6w6r#Pv!C zFAL||>iM9WxNKU#bu}@OM6+kr#A%uC*AZ8=@@%7p<1JmRGb1Fg8X!?N!^wGSQ0-Nn z>0Mx{@n2mN?O8H00#*Jl!n(uLn<<=Kh&IY-lfj5aO*EmA>3l z@dnaNef!nKYbzb7732{?8iSkBrP#bn8Wi+c3c!es6SxfD)kS`nFs4885 zEPA7Z+55+Ss|_i^j)Y}f^gm%Jy1ul&RP&&r=xR@%qiXhYShn07vqbbY#~z7uQ#4kC z=IdV|1*Miy(Gy*9(w$C5fWVxNMi1;Om-Xq|u5vYk?dtMsxO>(Z`g(@{D@VJTY$mhp zw68nbon*YiU<%EryhA#Cd)A-M{lTnnYngWIE^RO#4#o1Mpj3+%@vpAgcT7h;hQQNz zHP{qJk#@ffvWBX0IL{)a?E34@HWRcSVow?O)iowx4$CmD>j(3*g(pD*XR__{gp4 ze(SJ&l)p5*^!~kPN8cWK*Fp2>;fJ5x!neNT_%)lK*!(Zi+YcTad+G7hk3JsxN~{%q z_U7}VKY4KX;q#9VH%pt3ytKXl*~2%*F7Ng>KE3hgjk}}wZZtO@8~beRZMzWI0r1|P zSL~=e>7C8J+YjChG77wF|MK3wdv8DZ^xjY6u2HXeXx)eMV7JsoKZy-Fc3JMylnUn> z$OC1aLK{|QjZ(W=CI<^UTWv!^AK8C@ATcE;$rfQL@gX_n+f$H>RivPb2HrU!R3K<1 zjfx%2R8kFP|Gk04P?x0+4@#;!%nUh*kkW~1rd0uHX8C3hy!i+|JeyigJVot1eI+qS zgNIgxeB{(L+e z4I7CO&F#p64pJ!g<~7(ce5RGfXhQTUXHe4|LwP%*CMZC?9Y_})9tp)adi8pK%%Jiybe6P%Mpo@Kr4@R|me=<|k6Msu!TT7N@)QlrC7aNtyJc^+2MK=v2*_K(s^{ z#pb%7>lW~oRVO$mo^CFxrZMQ(#zhIsP_bVMBywIR-cBZ`G)By(l(U#~csk3f5+uro zn56rigi{@g0x=?@WoSc=ULIuVWKqnHit?hPCr!LREE1)Hie^hpxuhtvgi`KsL{4T^ z>Jfp8RKF>rRUOat?6Ejs6bLb{Ig~5KHCVIjDg^Rzkxi@a>s;id026(6Hjjf0(W7B^ zo-F8j%ARI0kSCL#l}3p^ma+T-zBfkEbl zyj#sSJ0;1na^o=?{hkwv)U3MD)lQv+hI6Dw_o3RDW(P7zbC=Np4UdERYxJt^{z zK%!s9reHs~nWAb9c!)xIV&2Qu^L@z>irF#P*5J4ugQPVT*+2q`p5f$$Stm8K+%eTF zlIb~DFOXDvs$!+}JN&{$*WOXY5|5T6zaw%g?gS_u^s5`^bW z5_C5`4ffy`6?;h_QHCp%-e{0aO-QK#hJQIhFD}}MpDK{mJi;Y)H#zKeoVZKG9ur8E zIzE*xVXTl8iEh1H?{`OPt!6mg=7^+iyzdlhd0wgyyS3OS0|}zW4<@}L?m(?1)Q%g) z|Bt;l0hcAq%0lm%hdVV*)7`WTRqYjo>ZD@KjBF9e7&GR1O1jC;jL3}4jGQAgBO`&r z=JtaYsK)Y%ej-8v;sc%!`IrpLfKTd>6sYQ14dH&#U zoqp9V_-Px)h2O7+Xu5|H+aT~P z_VmyiR-!O2c+HVV(=C5k!@M$I9wnUsm)giEhezqj;TqwnRKHS|<=He}sJTQ_HfS>! zFr`s`TAQ?8jE&5U$fr`29Dd3gVb^*xEz@9jyM35f6m8hFMyQRm2{KDyIcT-535w=b z7&`2)5hh0ol$pZ@RcS;PyXYV($qGYYe%cBUsUM5OeiLNe6zTZvD+!mVs>J2^UkQ$sMm8$1>8ugkWCDQ zc77mJe3J(b;vc?qjUcD9YL&1Nmd8gtH-qOjg|fputS^KC(rKA zUY7=INikY#bl`E&s(X_8K@vzKHbuNTiftii; zHbPV7evgJH(D}Eo5DkC9Qfwa4HIGP|Oup{tdvO7(jrx%-nG?3y%RHc9HFN#rzq1~r z-xwkmiUdX>kY>>+$V{cvoW~k9%fJ$vn+}FhBEeZR&+Px*HKIK!q;R)uLEyr=Y(vtb zUy>jWj~0;I^xY&bE3G`%q>+3v`;RC7>-a|93bI+b*cnVpcp5@o?3eBuhx&q?j+PI9$B^at93(A*rvoRXjLjmJ@)x3)45n`O4e(rjVZiFrbR2YQw ztVx?H--lAS(Ta5_s0vfAG#OTnez)7pg~_SAMpWC0l1ejBzbq9p8tu)71S{fDAq?)0 z)`$uw)0!R)2kk1~$k#}UM+mXr)ooQ)$Q}|a9yqaa9o~rUZcd`HD-!5r&a#D;I8BH1 zc7!W6FEav0Y<1Fd*y*%c$jHTY|4y`i4#}ejofskqlV%eWt7erPb1X=gGo$jftrkHH zc&lkwKtd;f6t59xNp8=jA~J@%CJ1+ytj45W!J6H^)9_8$VN}a1EQnc0)Q|qu=E-%d z)|yaRzpv#pnr;i?7>jewL?PLssi3Wzp7%R5AyJc~x2_RIsvlIwfnI|fQkvC-eyvd~ zm3wFqXGOB$6n(iq(t4;O_(z)qK)n$3gqls3xSYU&ecvloS_^}i3*B)p$1B1Cl!lAW6t&Mb$C8AowHt^RJ9KVP4qAQA$aOoJ zLdzuPG}K9qBD)ys$>hXzY>=8nT&a zSl7?IH6n^}Dz#IpVppLg-MSeyuv}DXINFkLzxzIMYezP_^^+I+ zHNW6FgHd6sBk6Pu(+Hw?kURuu>^4(|ys#=d3KyyNX$lnfdd6sY1P-IEQF~Of?Iz#=$fDzq0@L@Bi5SuK{oW#Qo!YKY8yP?hWsK*52pc zy92_3zxnR$F1@@9*!zt;-*)E2IC>lheO)+Bii|_fCEs)DwtLiYF(>|MK`7k4ML!eSGiegZp20 z^sPtpBX;jE9KGrALxp5;D0MW>dB%2KbwAZiB+iQC_?9-|WK z)4JjoXmZiuwE3hvkSI?PLLHUsxlC_n?t&fX;C2Js|IQ5}uhTe%(&1o`vxn0%qrRX=SJ=m!+(b31O5nkHG`a(CGO!*R>wrVhJ7!t_F?&W(q6OQRfXirMi zLecfJ53UiGU4a!}?}HNLl%DWipBYjUznak;W12N_!L-mmD%5+8(&2hnOAU;l7pV$A z>6|9imdqFW8Re)I?YsqQha{oeIIhdRI$Tf>y^^0a4PLXwpm{?YT4E0JUWl zVm@=aqmE-uMpFUj(<62*mC~LjeS;Ncwl-)i*f>|0)nR2ZZVIrNnxJ&KXn>;VFfL&G z+8QC2QVJvluo0`7eN_oNxjtMAhh`V=n8Wmiu=@+ebQkc-Ji-8pAXqv+I`A=;S1+u}9 zU}`bO0u4%qdBe}wLoTn29gwLG2Wbxn+nXIX)7zi7Mzo^Fm=cvB5*cqqpm~#>dW@zsFO~A;NX?JgqTtk>J`YPOcvT)x$EKoMg)z7<>ymsA+xzUD5$jXmyzb8& zy_zs`W#S|C`JlT93jMrhhKkMCO5}o>3jyJ(bTd8wjn%EsyyiA5aA=5H(k;)2Q*`9Y z)4ED`X*=iHJ*_tSTrOj$|jQb!7^48j{=8ij^5HWA^ zqmrC&wKBeivL&yH=yD9zyLegh#eS2~#y$RgBb7a$2727rJfml66O}Jc!SyeeYAA*3 zNsnl?`64+9J0Zj$e8O5PdtU0rC5K5PlPHd+?V?-{z)^abOU-_T@2G^Pz)<8>l)>p{ zxv~RJP&8hn4R}((7pYY2ES@ifYQ?u*VxZLNFr=o4@*EeRt(U~Pp@2%f)I`n74p*SX z%&mf;l))T);^PW=JS3(Ib#50YjW|B}^|e$6R`&X%i9VN7tK`pYe%h>x*+Aga@wm{Q z)@9r#%zimUqrIP8BgCdOOJ=r_6?k-w5%TI^^<`rp3)p@Tnl8#axLOqT|Y%}A%c2MAg-#BPYR0ODkp}G=^2{MyWLSq1?C$U zrhs;$DIfmBHG-It3_l%m?e=t*Q%TwyU|b=Km3h8W)P`}_mD{lgH{`f<_E*=4LNcog zJu&i&;tb4LN2j5@YV{zVn6TzFsmlsaun1Wi#3$=x{=5*|1>J0WAn8zRC+t)#IdzGe z@quP^{ZX|tPx>yIK)lYJzI!uYaoxBHdb3ofchlI190Z=31y6 zjmqVGr&1t^#n98Ic~r#RQghUpj9bH;3?YkI&L0ei1hc=smDnMt|LkwC#z<3MW2IRb zrhzfklL%5s1;N^`*CF$G!>hBiDOt&5g}^GEubm@J>2&I)Q=wplB|+|%%iy+-^i_P2 zLE{n;<+%wp7`bdG9_;C|jjlcTfs7^Cjgm=)&!M6^q) zGcQUqMJ4FCC37(6ROynB_F^$~J63f3og0L$1;7<(;NEe^g^YPG&rnEKgNDwmIpn=T zJxm6slqkgE{S88g+Hk3FgQpu|CEuFXb5g#l_N(~Zukb_(ybp;p${VNr{$F1sdb(6= zlr(AF=&I2$i0hM5ai&^qch(Y6uz+c`Y0{eDJw-nF`8A@GATBHEEsd@q4Y!;Z+PPAb z7_fGIO3S0Fmtgd)=im&y*$?fvXTXbH{{PLx}*~ zHr9y4^%tD9?T!soT9|@3a|Z*kh-wMk zoX*CIgot@tqthzZ9Z_>Tfg>Vs4G#9#V}u9|79+x)rgSlv@77{OA=?X_hyY&#L(XGN zbE*|F8Wcwo&me?IxHlMLi<9UTAY@r%dC@mr2Qc=YxAKePW0 z`_cY8_WsM>_w0TC-skKc-TJ4uz7hy?^v2)cOZp!$nt0#A;r_dy-G9R+;VSuUjv`C7 zS+lImq)5DPe+9j)u3LUP(nZiD*H+Zmpnq zFB5pX)JsJ)8Nt^zUL_uW(F%I!GU#?O7E@Fuo@RCMLi+H974-IH(CxA+;M0*|*)F*6 zBOZR?3VMDSbeqqS6ByR#O;XT^hhMOQUc$VF;oIzhq)3Vy>)R$rJXDs@y?=GdsE-je zdLk`WeA!@0;-S2PUc&8&LE9!_SX@bxHA*sxhtdjq37-@OZSzT4rd}plhvL9Ai7V(& zUncOjHR%rROQtJuEb&lSLI0o2pxf4@vyNl3nnxSNLw*JQsmq|-)+DpQhFwabc;aDs z1-*pl2ZOe)$>3zssko8^pxg@jlb4OUZA}bmQ?|^jE|_6<1%2f*=(aWKWm~o?9HqF# zLuLj2i6=uJFVUs>UYRl!Lzjq$^a}caUk2Sa3EB25W?8lU67lf!m(cywCqprO+axH% zESVLdqJd{!te_{CLAUivdL>sjTwMaAW-I9NWzcQqD7-0GOx~wu;^BM+J-Q6KJ;Q=% zGd5YF8H#wAuAqmPK(TFql?1j#tDY|bSD&q*myqsZ*fv?I?2`^7RQ$3}Je;ne`@YnGYy7zmR zLAUj)dK&o7ZmARyc(j6E!e@kG+y2T_EL~J}gJFn=!xi**E*o{*BzVdbSl{wUU`>M+ z^tUgAZu_fjo1)F?tnLsG`zz@GybQW+O=?*ysiJL&BJr@dg8tSeP;}cQD2Mk-q#}A? zG3~CP|Kl>~wq7+^B0Wvu%fS5O74$bRgKk@sCV;yQ4nV;ScUI8zqU}t`2s(Xa)U^%b?pfZ2FQ1 zHc3(h)6`r+fBiD(wl!5$%`{BXH^5nQebTgR?uI)1d421lj=#8iX^i%5V*dA{^!e}+j{jFT474Q;R744t)Rbh8Fbqa zg$mEJvf)#}>E5$~{-?{J+lI(-j8;*BIe}H8x`O`lWzcPFs#ukZ0hUq|90)#d1^thg zLAOo9@*GlhDc1)k@$MD$mo9^D8=~af71Q#x3K;cWE9if?47zO+Hf@3{HbHjF#KX^B zLI3?_&}~C>G`hqK8fjU?!^#T!?=FLGTa(6`6lrR#trHLZ74#P`fr6aKi|LZCQ7WsH z3Qi)#LvIEBx0gY;O~L`2oU0fD12#!_1^tD~pxcIM^A)DT8jcHA31?h6jsbBq=A=PrY8Ta!*w6>ubQd0-Oy3i`8`LAR}m;Y@HEcB~R`I&B60naiNt z)@0L+qcV~rgH=LZK|gp2^sy4h9Iv<(Yx0V<=KoJ`-GA4;@3?nRhuIlWLKUHn(>0y)!<^b_OoRWv&scCfvOw)B-15_ z*>a!TfEDqT2D$o7_y=MSbHEP1J`>rJbK>j*RG(#^4tsMA@2BRe1} zpJ`cz!l(x#4|f%ESs37z`VXys{^(^wZkz3UpLwaZ6!JCfr)Ll3_Rzp8Uev(!E;A-6 zgp^yOTDQ!5b(Bv7)fz=0Tyvfx#H&KCD7c?GkxO3YSE`fM&&yu=;3vo!ZOM500C5Vb zMrJc-*b#eZ8LLgPQ9KhlsE$K}>a5i*R@GW>03xTi3IjY<#w9cJE1&z`)z3?P_%UUF z(o4;yjIW^BLcr=g8%&4cE zVvI;(Pi~^k)FAQ&7se=zyG2R@K zbi~&!xQ&yK73CHd(q&bO?7{vX$t@EtFOO9?NKp8|D)Sl0}h1VbgN`z_tz zNWV`Gov52HP9Rs9!EA44)yUSPIeY^7=~LxhlApe!FP1;e=-To=cqv%Q`x@ft!w0ep zB4r{sTbMyD(ABclvAeXRcq;7-vJuo9R1z8^%}M^l^1g;R`l)g*xtL#3#pO@!vR8We zG0lI-gJtB z)@YgS*q60FsSRHb_iK?)0nn%&wFR?IA;?^CF0> znE4A|4V8%u4LiiE_IS;iyt5RNSh)^y*){o-X7XKILf(EL>HJ)b^WJ0x#o`#Zn)w+k zS&BuVi3+N!j0eLG2eJ-{%zR{#kEpkws_xZcD9Xn$ujfJ6%4s$I}ENvf#o@SG)!w~xSmqMcd;Mzhy^HOCg3H9)fG6zvT0a(|@Y0}vQKb{_GAZ>Zp(I+3fx5cJ7Q z8KL)HCgb*-95}G~TQY(o6_TISDlQYxVLip_QoYU2;zcXx+Kq0N@fIzMFk41BuGMyx zadjAa$Lgr@j_p z4nwn^i*-hA)3ItM;)0SwM#5^&kQV^=dte)Qbx0Px3d6+Mqbfd6It)Ec-qm5~?ccSO z_kntCc^|xFFXg>(7z&4((NKoDIoI+QQBO2G6ySWS@ZeNzm<1djVG~XTkz$ncNawFT z3_VTG)nVw9SN`+KmtAK(uK7=S$y&;J;V|TmT8qfa;Z%Q|g1kMQGNz*;NzisF9>8^> zgt8p3lhJgJJSs$U@i6o>NmncSC%kPrqn~(5VY$b@>j6(Umy*6_HQImBg=^A;!v|G^ z44aKH2?G8m4$fAj=^W1^)dK9atDR6E_r|ZT>+8eN(_~y7hTi&5mNI^P?z&oE^Z$Lv zt-Bw4_E(QSeeXMdKQHxvjQxX{fiIc7BrLgDmPCO&xaYG^-OaY>$uK~S@gy#mJp3ds zmK_{$Ne=R$G?*4)qdFx)Aim!0ciW2;FShEi)?GB}%wi<_lrX4OilthzYQKZqJeiAS z7Xg<9dRts9uP$p#abC;E^3-`<0+Odv18#30OVn%mSUxPkl8yx=Ep*$#1#rwrE$z*zKA}buaBn8kZF~Pi8vj=O4LpZ^A10?2Z;cy{l(yQGo?)Q zL4a}fIld6x03p~iKbZ;BvFnB$HqLyq-5R;0Ce1X}GV&bTu|~Z@1qDH*-q7$&x1nLr zyA4KY;Q^+!G`5%(D^9%x$kDFB3%GoAz?Aa-_$7YncV8LoJ=npRZqDzZC;8nUxF7B6 zh+BU2+HLm_$&;pl_;O8cdEHGCxEu^YD^powt6`&An|7C!olByz57VOp{?2tJu?;;E zzWnXxpDsN2s=HTz_xKTSJ>aV?wn~11U%$5RHPh1SAD1$&<=GN)O|A5*;46H+wqyuh z{WWd}@uRYYIkqA~6%(99NPCJPYAT zD|@~oM}txmR7n(!7NG?~eul-u41I2bpe_G(FE@zSr`!CX4j3)ex|J#;yu-O6Q<2pL zF&IlibKD8MK2a_?+VjO`#gRvG$?W4D$}I`LZ5uuW$qn7{#DVDG3kqKFWR1MuaLwt1~emlup`%74I~*lw&2);7U&H$BcKmhdtV6 zo=mBG!6}x*yl!JhF6aLfNF5YkgwckpO&TN%5+OZ;F8IFe(>lofjk7_==|`5hu?Pi z=k~t@FcEzE-f!>y*xt)~<2`!s&A0yjt?zsF9eeMez54e*{@Wkr8OR;H`;wbq9NGRD zddgqk`tfM!lloPyFNO~x#gq43)p|Ql>EvBkwZ0ex0s5|7)p|P|;l#eG^~E^>eVbRc z-VPi(k*{d|SfKQYcvb7|F9#>=Rjs$z`V;!{);O{)_bGWr>#c}6qx;UCpwqWr)%r1AzVWKo7j+5xzI#>ci@F3>b^EH; z7j+5xKDny(MO}ivkFIKcQJ0|ay{lSZ)FoJbe(&x;UCpp##^s`a)mPyX#ytuN{l^!@W!wcgg{$jtYvQ6V!3_S?Rjn`T67>BOSGB&VOVIbfxT^IJs$*Pp@kI zm@a?hs@4~E3Htt_t6D#%%YSfH>x;SseSiN|tsm3n-?^f7;i4|V41V`jtsm3nw_nxz zqAo$--*#2&$8`D4SGB&VOVIZ>Ue)@dF2M|b{Z*|m>Js$**RN`QQJ0|azjjsYi@F4T z|COs+U(_Y&`>U>KoqtT1FJ0C8qAo$-UwKvQ$8`B+SGB&VOVIcGuWJ36F2DGy))#dN zX7JBl)%r1Ae*RUhFX|HXon6)XF7=y?VWZ`Dg|@R0lz_IU2K+>w^a1JNv z*@@5@rDd+hP2pPJ07O?=HV12|xm_r|sM?rSafhAjTT{C@>pHzjK>%sLx?Awb-p0qV|L^bo#q;4=E-6>vFSzaDc z9l`?s`f9${wRQ5PCeX=So~D!Sz2>7zC-hC}q$1S2JSwv|u0yyg_d`|(d2Gvd?&8`- zO+{ZSt66WZk8CCu1Ebd{&!V{|U>ZCDB!+wyP?U~yzGXy%SJU^dt&{N)(8-&hrjzX* z{i8`I@=fh0tUMArPARnH)Pe+GVui91Lr>5N-E1PAoYPns5TG4Fy*X)>=22)*qK4?# z@)TgvRC%^k0{l(s;ML^DYwM&M0G)i?({!?ZZu+Rw$xZDiw2$j~sn|d#Z+e zW0i@X;3p-oHwD22t(pi5#z+&XRhu)_VNo#}T2QG(UcWczmtlKQlSl`zCM#cCC(s$t z$s3=hlkIn>k1Czq)E>|9XoG<;2MIY9ttc*2!A#D6l1>n*y@;v2N7fcp&FX@>+=XaR zo6{4yGUNO6xL+|544EW$TOYj2c6Du?JohZn$;UiRCm+S{jOoq2GY-dk$C)*{4knj{ z#79}C;*_4G6Ek8f)~x0buC+ixMTvYk>GY9&vD&Zkpz<~1_S$st#cW*t=fHhXR zQcra9iy-KvKGT-RO-9jK1S$AAw%sct7`kw5-+tES9Cm7OnFb0B)wzVB2V1$1Pl(Ta z@7))_2n_Y9@BX+4G;Mo53ZJM=bX0X`3gFRa0oS>RVyf80+Kp141~tsl5;c^sJ=8jk z?2_2i$3A*4u}v%2 zmuDR{1RnYeHxFGCk+w3^`hY9C=c-!A;p@(<(B?)nk#(T~GY%=+)J>+aV4f`jMt*73 zUXX0u9gaZN6Ya_Kd*wrKeY=IAS08XuyA+Z|J_iDL ztTd1HJIv5-EbME`vy2(;y|@4)d^Q+i+)UbN%=YH(u@$F=+DHI_H|mSx2wT+AHq?zY z3xzX6<_6au;qfrz#)}yk?|CpDPQqnAYfnpVeBG0KQ+9z8xsz2&lQ$azbg@Z^w zG~};6-iI?5lIX=b7-#9aGFIh7Bk`hSlpqGex8_Bs<>+u$?10BF!yrM6>e+(KnAo+) zd2MJlFq#w$wRGM5T%G6Yfb(_+h-vFZglbE9b5bn;iiD}^kp+?OOR#{JbA|ZYLtSmf z+k4mAb!SU?z@rxiPMTX(6*X$LRBqa-P0LwsE;idfLJt&qoa>0qIYZes$iKEcA9mg8 z=Aj9qP#}yVZ?jn_my+-ZdlwF4xL2wTKH!k-~0BpzYhV zwd*E8o~7&33O)kG1xi}3Li)qN^?fBpg~hbrgVaS3DvoElqFqz?QE+W})~-7SBP?AP z&t`)fYz45`H(;S$>Bh8(miQ5u0_!b-i*j)$ zIK2YaLK&QJ*c52eH#$z-+8`te_1lob1=Y|B@ zzD!!XZVcpEy6z5SJlB?I?YbQ>!qRnjpsKm{2v2g|HW+W|x;s$KTzkCNx^4@Mvvl1Z zsA8@?&X(&&V5p_*?!fhO?V+xA-R;}1wd*#Q@_XfIv8Q;x;qdnTziBkxo!=Nw{+bb z&Ai^I{I#z89x%?*b#FA&^a<~}>-_(pyLBf$d+z9a_kQjp{1X4i-T%SPz?V8NDa%}@ z7mItlaLhd2MYhNSaFB%aM6v+<@m!|2?_$(%2OaN{g|%wGgL6KaEMSNAYSqD39@cup zT8i=!APU%CX_v#imMCC7`0LA3B$l0BnWad822aWRz2vz0812<^tk;{kRV<=K0DoeB zaJxu@I$X;zA|oXXRBrdMgc~OVcog7rB6cQbM#@x~bWrSdXDQa7!_Z<_34%HI3Em`MDf%UhrtCNg`z)B-h*VXiVksB{J+9j{~hLOl^323}Z*;K6!_ z>i4gixV3!rWqxq~C3%$}+}^v{?RCquFQoy&{xCdySN4I&@{f0*h6l&eElav$KtPR2p7wf8wDnlmJEL{g* z)hR(s-D0=p);!p^>9Y8oJHsie(~$zf!D6P?pLLlGR7`~`wd;Buc=V;mm0yxp9`}iR zyD*LIyq8|;ap)gOkK5s7{v?lEGPM2Sc-(89YiatAJJ;*oy$HaZcG>R5p@oSs$&1A} z6gp_5ukk?xTp;$ta@j49?QuG<_e*WhZI536?lm>@nGru%X+eZUMJE5e)Ee|)Zv?k1 zPED&w#Da*h0mRKMY0~NeI*?M^ERR6UT}>#Mb>_K2K7h-qRySH2R1l^i(oyGRbS*NS z<)Z^%`qHD9#HBBN#as4n_SNmWyBG3D(wBBU*lwLmAAh0!!||-QKS2<%vn8z>u(j0= z@hJa)`Cjq3-`!aLe*r68x~cJU#692QdmJ>11w^Z~^F4EpffCX6K@VKD2iZt%Bd*E9 zKHzl$V!odI|L2Hd$0dkP&xD3?#i)^%qdp(=y?Ctg{&Q_WqZ-VGiom5cXQ)hsL0+!c znt8Jpsb1R9Y&)DnJclZ-(pHNdHMaEyLXlvPUCJ}dZ#n#%=fm^D+3%kH@Sb{) zIQxo2?99A3y7e<>Z#()wkMslW^xy2GN58%ITc94m`%l05uy;B*&7b_v$q(KCg#GTl zZ@q8d{^5hS9ODN+c=z`Xf9~k5dvD(V+5K-i__CufJNfbxKK|d1`*;7u zoe$mpkvrdVXLzS@*{d0)Tr%4{c-O~b&uLnC5S zt1Z>4Wl-Q(YuD#`3M>%8^XJ_)aOjpwuo`!$c@-4U00p2c(GVOwxW%-W+6gwrYx!uX zjs>x|0iJy6X2h_9EQ)j6t~;uSLuMQ{Xs9{C(^*$w1EHEA(+=PPt>ztmJ>s46W<*?3 zp$bRmTU?}w^PZ48nJ|m{g?a)$EqjZRhLpxMROWeT4LthP4G^2@oUyP$G2?loJHQ$) zKCa_T*zv-+9K!J2XA?;24AYUm2JU~+28cG$d5{+JkS51P!LOE8yy^%21dFPUY9m2E zs#F#r214A3y8l}nAk?RVzN{qODUOaXE2uO>o9EG9l`4%uDH*1$%32f8@RV7PcwF58 zStCS?q$*_+9!%*PKdj)i5vo%-Npq%=DS|O_az+a$bQfw*td5k+YE3}K zUCVMKwma389OHqHb!~es>e1OoRJ}SL(Ivy7`C_#a=Hh~u>-rTs>10Kgvpu}i4cVRm zN~LaQ^<<;RLAz<7IFu43uGl2m9i`2jsg_z!+Ycj^fkY}N_!&Hm-ACS`ZbZ#b6Cx_| zom_W;q6RkTjH4k4luhXzI9{+V@YNI^7u_09Z7k<}xDnMKQ8ej=o*WWLS2g`s!OH2e z*RD2gvg5QsF=L}mG6Jyq^)sIQ>CK1^rPGXafD*N6kd}J60F*#3 z_V7`j+Nk5+$8CTv%p`KYKehsH$t=?c7>Z&IN0MyNDL1)CZ+L4{;_4ZQc=n-N2q%+-ShItwGC zkc=J04i{uv>)DLpAYwF4y9;Z=V|;#NtLLDe@M@Kt6hxtzE2U!_6B%_Y=EWGXxbZLxYkkoc%x9O7Y5T|FYJ#NHQy)=d_{;5Diq_| z<{6KF_7M>76|}xn&j`ktqdBXah514^9JR2n$khi081>~L-kyl1wdEZC^$oDdV++mf z5_+XM^_UiHa)7R-B$NDb#^pkZii>*3^i+o382QQR21pERWv4LArZEBwi&<1`mmP1; zso1n>PP_R9s6shz%p0uq$gK`Gz$ZGzKKc%yDRr*u_Va^QJX$(6`HM zV!>t+r`b7(M-ZDSK@ad!jSEC;ki#lstDsS!;{2n@`IQZ@Gxj)4a7RQwh8F%rD_FcS zB#V8=z{{ONl1-#h*m7Hf8m(vbU}HIhNX5x+$M>hzhEPM`pcqZ2GQ;p4%;jLQKDGkN zw5rMEk>wmeyBRUe)x$y9CyKqgj2!23WT!Md8!BSEvSA0>Oh) z!rBFD844%t)Zh{w@6sbEC;3y-= zt5&m8Q+;#>fHy!NW1J`wL@Hl(gkrY?g-VGdaB%q1*hU%a7(F zPw^?%AI+OxNw5Zt=GF_=fGDaaV-Z!PTdP*-X-v}_b?kl2W<=G@H%N>xO>!QKdt|3p z5u$9-pd8Q7pgvuKf^NW*v6uxLJ>nan;)Ak*G+B!9T1!AEIHHH;zR|XPcS_ZJ!JNX& zWTrTs;Rbm0G50@kb<*u+hu!snL$xjDyRp{D%WhJwNyQ0<8KzPgBL%t;a+LsPaUw*U z9=DmBd*8PKHWS!FKps&CB~-^6d7(9xXR&YO^sYpbMzz8!?Mkm>H8zso`GE~^ps7X8 z0Rc^IR+nnc;*3n=G^mpJ6k4Pi@Fs0Wb`a5vHfr4e>PJ9E9#8aETd1Z;=D0Cw$Pt?F z%*ct_(&1jGSWKyqi0N>1TG{{p4Y0R-8G}v1r5V~AW-O>|6h&>^f_rt@R?|XFgd1gh z5iU0C@j-n9?6~!)3+HN{(l6>x&TI&+8d&a#KBXdfugML)Ni$LGHnMrfeeDs@;dn}~ z^Gwn!4Qv@)>pS36LKER|0uh4-&ULY{%C{@E&4_#M8aQzBU2wH8<@9PB#R6tHT`*At zyv;2%SfDkljW|}&3wYJqEIH?2@(5@)2g$JC?94E_5Bn2pt~DiixG=eeGNvb4FM~mu zA5@=jfcO61M%1nW_D(OtM`Ei>m_2?(HsE#~hBHyJgCPPET-sVaHWxy&Hr2cT>jr3R zH5BACC&8d$0}5w>(AlEI7U*(*J}||x-Ao(`7Hzb<8S&09ZbWT_#aeMhc3gvs;%?Oj zw5-LZ1!Zuh3OA?aUTwB$Q6o{xZS;8O2RFdjLh?ftYk^m>@i?^6;hd{Bho)pMWK}Ln zNT=4&aj>Ra8+SbYrj4k4GB69(wq2{seO;aAluD3;jEbT&b#smy21+ct(V{+79v$U= zdNZQlN~m&g=xYq_!Fe4=Opz-pniCe>X_i&=LJ?GPYxfK3#sE$~uo;mo5SnWh$NfYg z@+D-{oHWZ~4-SSorL54HRMYw;4{RyyX2jDk-T+n0)}dey8>T@P`Y31HAo{z`=iCZ@Lm}_7oEerd+2%I?hy=3q_&n3re0qk*XI@eF+XH zNN=(^ARmAK^85cM->TiZ|3&wH_ud!Y{q4J-f9JREWVe6wc6$Eb&ZlR;ah9C^+Ue-z zS5F4VzjE9=`sJhe@RttT2mk&c+W$pR3-A~Af?FQ~@XP*ja!Zjaz)-<@+lEb_i>|X%*btS%4c>!QGt&xM3CC zSqZR1B=5}zl)mut?VSaSA=rGMe&TPp!}h@HRL zpz9aD?u&L7V2A1zPY?Tf;p<-5S%4c22Y=z~zHlYL4%q@Xt%5JuS%4c>!RPNRzzwTl zu@Yd1fa9C326KLQcB{1a1ABMryKlVTx%02@eA}HbxFg(oE8yO@?tjhgU%dTYx4(G* z?R&qv_XD@J+n;p)%je&B{{D04{K46;o&Cd8=;U`#{@KY_pMBu$U1y(u`kSXea{8A} z>nE*~JmB^JiQ|`#`^TSoe17!PM}PBZazq}z;qYe<|JLE+kUxCO!M{ED&V%Ye=`NkD8&(nwrT}qPQX}O6;=bIjbmcO<0h8OSX|H z-?y;++Rljg?2M?c5wWP5^?|{4Dv&cPi-Un{fy%gqsSV{i*8m@Yunwm5#Rbj3<;OON za%=2}Ml9AHLx$BL6-O0B?B`S}akR|IWmc~|Tky?ca`LrnM0;pp6)$RFdY2iK6hg|a zQLS6%y*kRLfohGwcjm@C1s|s0@W!1H8#CEqj5k0BjlBnQTXuCcR0>WhD{yJZjfX9rCO-h>x&LK8q?}{*5G-{=B#GUt+aT^#T$Kyt#S*z z=%0Vq`Z0Du{MnrmpSeMdMTu`WU9&AF;Ome-$co(DPRmIfe4uKGVTbP*oz(B!!L7ft zGvY7rjQFZGA|ky?&chk6#O7Tht{ZIu3bh#|By*(0Q6){N`61Qr2LADn?~K?O;tpf{ z^Ys|bd@~)9gW;l=LyH(>MEauDZVNogGK~rom3uk4RI5V;cX0X_*9ceWWs#dL%%B$N zYT4@8UD{DRm39W%250%F=|TCLAEhmSG3Ib>)G-6Efg*9S-YkryxW|@G&UZ$f z?Tk2GBUEH;l-jJ1FJ}4XEL)U@ZPcV?#2||n+BIBX;KoQ1cH{Wo*X)e=Yda&pdX12i zsRsL5qma!oK@Xe)j|^&xD`PR;MnJ%oYx8Zk zKjQbWoe}8H2xN`GB9){Tpk<4)0OV z0gWNMX>FbV|NdKdKIQalj^4QU{@u`j-1??w;7gBRGEN)%dk?;Ry7wTK3Md;SrX+Xz zReNSv%7yljA<}tq+N{p}g|CLnM23bPB6#{W<5ou0GNWcP?^oN)#E`dR2)axGdSdGS z6AJ+EaQn6Fc<0pp9quBZoVqXR{9KIl-ed&D;uyD@`57x&ibbG_I$7k#gJFl8w!1`T zMo&sN>@|`EB+Gn+@}i!ayPrg_rff|b^Ht_!9i+J~btSJwdj`FbAq68~njcS*dY!3> zMWl#cnwnU$7I~c*k`Oy;LB$SNCku09WHJw$pi&1_HtpA)5j8>FS({E9 z@&C`>d&jv|Tw%kzD`|B{T1lpx?gki)uzI1Z_by!(imcvy7sc47CP2JEz;q`NFeDHs zAqfct93X+vLMXwZ8UlnCLWe*g@7%c~%U&;L*YW#>_xol4dCtzAIdkTmnWMR7&gscL zDr-8@_DE|kwQY&ByfmkFdKzk7shanxwS|bL>gr1^CAq34m1~x2d<88G$r*9?oH}<` zq<3nTC@dCN$)&b5^)ot}h=xubO&p)}Y+i}8m`(rjN|b!%N}MJk@c;cF8vpT1v`GA+ zP$Swb^~;uSEM4!&rBb&{(uJqmJduJyShrRk(yA~UvT43~D{<vplyvBP^ai zS@909M49R_zQ$Lvy_-!AesN`Yol?W@`wRioJ}K5zS0Wrby%ODP zG1gpNSt#SPRnwZLs2cM&c6cQgEjfu(S(I4x?kqgP;tIIbs(K&+KRyrk4T_4`mxf1M zdxf$%sc9~akovk*rj%8(L3<%^S)uUu zlw@4z?I{}my4z?nG!=bW$0}bEDQfkh%zqHnCY&nf?EQ>b)k-E3~C zR-|N69cac2jkqxaLqtova3PEipTet~ z(fNNhHFUVLsr&x}M(!ICoX-Cjzt8)W*Pi%rqRxGnTZZrY^Wb%mCSHN>^f$3j9)E5; z0^jA6%qN*%#$ycU*u!Ji(FaD2uc*IS-+Qb9u+IvJlzU2Emkj(9>_l2nBqUm2eDcd6V? zu_zOYI4fTt(|7sm#k2&o;JPSnwU*lQBn$&4j--MWcZPoxi1w7lFd%Y=&^`bzI^o}T7p@G z8DCV)Cp8Iu4Za2;y{@hV#)0$bEdI7Ws&-_$VS7Co=z7Z~liJpqLBR0ks~6G|%!2FA z4OzZ?^#WRgSp-6#&FP?-tj77Y1hWW@zH`HyFJC>6mf*j3s=A+%T`J#pU}YJ7dT=H;vRrzMyL*G1xPS061_66UHjoKJ@9 z@Q|i6t`Ag2Q7akuY3#C!q*{ib=v3hEf;p6~n-XoSF7CB7dmcM!n36$Z`Re^>31-1{ zQ9sl%H65~APbKs<$!fdn=mtYRN6#sY229$Nw&-Y9(uKaoQJ#!_$$iw$h}{M=NFnJP)p{m0ZdhSz#Yqf?05d9}>;@^QM{T zU~gK2S#Vu6i(^7Fkzg(@!7RA$+<5oPSMNnjFq^YCa(B-bs+x68ZB*Ly)?nt8dfywW zN8(+Lw;)efwfc-D?uSRPVcvro%)~uu31-1{=SJjTzIqQ@g4rB?nZ+rMndo45T7p?{ z-MP{Gm#^N9mf*j3^kMnxU1Q4xWIA~*n{_NwrQ9%>Qnss$HpQ`2KA)sr@+fs^JocX!FAD2 zef+h2^#m=!EV$0*snXZaL=L=nSB?C3WaPJkdj;nUECN3N$NW0~DEK7cH@uU1^C#Y% zxN)L5!Q(#0y_6f}F5tY#xt?PHuYsFD9mrv9fL{_J_CMKovd>~`+3fLkEuC ztN?2t<{iuvnFqpH0Y6~m8Qif)$1WR-jO{i0-010}^LF@=!O<+37h!C^r%*_i`|V`? zWERXjvc>;-bIfQe^c~G9Td^6W)lf*M=8gj2KMArwVQjv)l+5b#?MjQZmwFa+KIWJ` zd-YWAqhmXh`_?IpB>nHZu0fyfbUIxq`3&)3CS%B#E0(V}HuLC#HD=g&d&ZV1r{;8Gs&^{;sf&}T z9~gUv(hrTzJZ@m@88+S4*i7xfUNdAt+l#3f*vnPP6|4HZ-I|6eBz2Ea^r<&!LiJk6lk2y#x`5F=15ZrsJFvzK8*k6p@@;aD5eM#6_EU{b>@zU-45c3$ zo7it)>=`!Q*4V`UvsC<+#wHFJ7<-2O)5a$DRkY%$e~U+-i=bD+=}Hr}2>6_Twt_#zQ#3=}#;_Ctk& z)fm>h}91qXziv)`W z^8|ARa|DE7g#Ri3UHSaUS4o;M~qx&soP=%UQ!&4PzOc!Rf+02U*TCPKe{?m^sID zR2&g!F=rlUE@uvh;EaGz!MpG~h)rN4cnmxMHh|l~daw?x1#7@+a4t9lbU_tl!7>m6 zZeRw-0~HW~#b6$o3+4a=4_{HnWdstJos;V)i`tT=pC`!5$g^bo|}%*I~AWjpL7vKQO*w z{Pywn@Hx%e@ipVC$Il%7YPDBtD2&fUb_2%kVbz}>*Tox7g9j=Pq-hP#@3F82&> zms{m#xy!gA7@xw-J)Wy#KE`~2xq*2*b3Jn%b1icXb2al^<{8W`v&zgemoY<3H`B~K zo~dGrn2VY7m~)wPm;`f#@hRh7_@rtRVOG8i!cGMMHmOyAdG>l z5iSE)A&i165uOaLKzI_k9N~#z4Z;(^We6kSQiNgfU4$WU3Bn+_7$FHRLKpy(2>oC+ zLLc}JLNB-wp$A-m&<)N<=mOtH=mg(F=m6&-w1aaI+Q2yot>A2g7H}3qGdL5W39LeB z1S=65zzT$Va0WshSdLH&PDiK#ry)EZoQm)`a0K_B5Upoj2i&_#F@=pZ~2v=OR7 z3!w@$5h_6gp#szq%0UgG3{(+HK?R`%lo5(S384rS5iSJZE&&;Y zhk-Q0LqQ7RVvt052uL727{n1C1Y!smfn^96f+)fT;ADjJ!AS_`ffErP2u?tF0Ei&m zAA}L^2SNz<1wn-S021NeAb@Z#@FUy{_z><1ya@LI9)!CCH^SY33*oN7iEtO-KsX23 z5ek3}As<)~@_+^51TZ7y0uw?GFd_uNfRF%sglwQgI1aQ3SwMr3364j|0LLL51IHp9 z1;-#90Y_6v{2LsF@Jnzc!Y_at;lF?i;pae!@H3!5_)j25_$iPf`~*l5ehefCKLTQe z9|94=55Q7{?}H-{z6XQ|-vx&w{0CTq@Evd%!oPz<5xxx;BYX=Sg78gnFv2&$K?q+5 zixB<|EJXMkSb*?VFdyM7U>?H1f&&r03=Tl}64)Q%Ca@pE7s0*=UjX|cd>-tL@HsFS z;j>^bgwKFI5&i}2fp8<(9pTepH-vu%yCQrF?1Jz~FbCliK!EUZz(@Eez(e>Lm_YbP zz(x2d;2?Yi0E7<%0^vh|jqpJ*j_?nFh42BuM0h`7ApAWTL-;!|its)#g79AA-w5v^ zzC`$2;tPZuh<_oxoA?~zUBqVye?$Be;hn^%2=5?1LHKLpV}!pVK0e0KA0odaw!Mb>KyW-v=)s{2q87;aczsd-Bf=}e(+IBse@1vYcnaYf@Fc>^z!L~B1&<^AF8C9|OTc3YF9v@^coBFM z;Ustj;cDtj?nn47@Oy;kf!`rK7u<*N9B?nfv%x(G z&jP4;Yx59!WG~*2+sg_B3uscKzKU%HNw-tuMnOJZbx_u_$9(V_yxiq z_&LHZxD8`@kq^$Uely1hNlt5rOPOOcKaG@V)@qhxiVG>_c2gAo~y(5Xe5n`2?~L z@ofUxhxitO>_ePKAp5|(3S=MR90J*gIGaHBA z_90df$UYE}ec-(cvJXULA7VLy>;nXF|I}!C%%I)MqG$+8F2x^C~-c*lZkI5Jc;-g!V`(}5S~Dsi!ef*gD^~- zjW9%ihpiBUg}2Sn1!BNoLZeO=ftY}ciUK-DTUm7)rlR3TB- zJM(*fPsy&=bYVJ+pv~^^1+!D0Ea77=M^J~z28XI8`IX5*XIgnF5Y3srzNW8j4>w4e zu^TISQoX1v)c5N%LT6ebX~naVzCyEOor#Mp6Le<_7p00BY2PZfR+8ObTJLjN%A!s% zl`X)`5m9T)q-j>8-g2>65+)ln?~Ft+gYb?28xvBSH=dN1q_}{rJ`;G zMCOXbB$mLF7(Rv6;x@@k_D-s2ttMRoQ4J;u>J@gZGiiS1vO#B^j@q0~#Ufovvk$`# z=>krj5oUx_!Q5`;oZS(N+WJAi&o57F3_I2toFdW9on6R!k)Um zUvEf@C570V%!aC(k|bx(7*spf8Jr>~)156`)a(a?hH6j`Gjs{XZF#^MbX22VYpCyt z8!Q!>*ByyvZUW4c$Ob6Voc>BADOGC@NHv zev{fF(zn%-oLb>iw}Td^W5+s!Q{;p}XE1f1u%Ok1GNo3@D73|tVO>C0B`XD)tf5Ku z?X^r(>Gfm+e!ptRI)hUrGU%)k$X1k@RHWEz>0SL&Dr2`8GuE(8CWR>;ZS9V%7?fzt z8JD(e*s;#w6baLvEnJkT30sbE%AGHGgyK*uB!xK+(8rSkX15K#&-WC3`HIlh zn0aU0Op(xFY*LR*6Al*Z@wiVLXbNj(VXK~Ldh;e{tKY6`4Pm7@Z_*TFa=T-`#$hkn zYq`GGSSVI&wp`pjgDIlOuM7@4YwCPGnMDdSU>b#Dn=Y10${Q-VwrT5E-S)mnB@L7D zYSZ5(9hM#IOrBp!4myK}{v38vW>B}2#a`Jh6lv9YEg7{q^}@b5Dk-PPOfVM62J|M) zj&%n8V_?u(xK)ZrJ*I3TU8_nQcBMU3k{UAkrp;9@R{G|+Dlg9lA~4seCa`0j!O8C* zbXJhX)u~oIRV`M<@x0nsQq@$Yc)lBMwjzeI)1239bg8JuZ&dD}&EVws4LY+K^%}J$ zVa=+f29vd@QmN9qrkE@mJzkh+Ggk8JLN<51T_!C%*cqJsUb?e|i)4wWNz#aF+C6V4 zC~3!oo{A}x(**-+)?_l9w8z3SpFWyO zsPbN0q7w0y95DB)L6ncCnto5q@bzbvrrV1^tGNfA&EiPoj&%mD<{ET1i_?oc))};# zb6~Yu98{cLXXaF~See(U)QidDO2?qPS)9n5d3W1P68k`vvpDqh^~VOLnT45=mxh?@MqL8=xi3}8g{HRIQ{j5&Sr6hVaGay(_c5}Y!;^# zcC0fv{k4P6W^piK$2t>J-~XFO-yab?Iz|ev66EPv{QU33xAn*JCwR}mckpd0Hvp9x zfEb~30}v8AFTl1L0l;^_Nni=_PvU;~%quyzi2W%0Ds~>ekscp^a{T-7J@c{fbigz4 z?Qxr>XYC5#3E#>*m1$+pWxU3?jjXPJE2;KUQ0507aE-mkmOh zwxm*~TxK%cQa1Z3k6z<~xi{T5chLQQFG3MPWW2ySkMY7jq`P&+L*p3?hq5+Ad}N9g|S)X_~D? zu?3!uPWkfgGEAe(r=za6>qURwY$yjKNmD47ZI<*|u`93cYw8s;8IUz9F?T0bQREZS z44;lf-L8=hNnK5r9VwMD)3lpR8FfmhcbZ8}v|&v~1L~$VVukN_C@c z>bhkPlUQuw((%gM#ahaw$kgf)VJ}@slq?OYCv8>A5|+HpDRyP^H7yL7ZEeX#s*$JY zK5}khrIGUci!pI4+sL&@Mb)Ko2Aegt-kwnuvSx+DmTa5D77f7K&6(})X1JGW7b}Kf z!P9rv`pTL|qmY?YwtB{1sMf1~nYEv6ggN`tePo4cbG9ezYlF6gst`+%k!m(m@V7-S zue+8G+bb50%&AD*)dj$(TV(WEjaTC~>1C~gSt3+hlGTU;1`$)*$w;){^LJpz^{_4@ z$^}>|x<$HB=!P=Ja;#B>Cv1dWk5r@x$%4kD-Jxs7i^f_`(M?$$31@|UH{BxDEyHN? zC3jogamLz7SI(%YIr>h8CSlMQQj)4OZ*!;QHb+!7@(kS~Dee1&rcx)WXc{!hq_dDr z+k7r%(U+2XBJy@1)Af}i9z{4NVje}eNVsGDs?(FGX_e}BC)jDVI&k_)lzoG!Wa+78 z+Dxiqk+>{{3=7Z1SRg@GMLH{)ig#nKup^sFz^4i!Wn1hwcG||6+N8;qO0}Xc%%!9A zwm9B@w_f4UF`wH(Uo{IKz7{p%ib!OW8_d$VVA|4RcGAMKw8^VyA%3sETpNr z^BSMZr3+Pxg;6?Ed)pf;%XDIEQ|t{Zn$C!$o{jjrVYj*vDTphi!yRkbbG?wE>FO{$ z^fXDE)-%hr(&_uFk zEEv+;(lLpoV+ff-x|D@|Gu^_TkK4^opQ7xk`En*>zhO6Yi}}1QClAEZ22~~K)hJpC zaZu?Qp(7u+nlYt5Z-t4ND>4|bJu50S?BQZc7Irvl?UuRjmS$^(L@^WURd{sPhgM(K zj^vafEg2H3BSBHkQR^s;b(JL1PTI{`tvsXE!KWLkfUCkggOx+a?Ff8`<~D01J)Jn|i;7ZG zdpj#6?PjUkg9afER|aPk))&4I_LSQdm*|>)eJWK5T70%}yp=3#*w|Z% zL@D_6DdcH6+l5lY>&~ZBaJ!)IH=ONC)T$9zli7Ghoze!wVb83WvkC1tlRBo2* z+|Eq5?Nbz@UVYpZ&w8~In=O*z_;Cw$J|VZqdp1?Y;S4!qeVZ^^ZfEmuZ%r7;1^vOU zLt*h$lI}2<-p8~m?SLZfsMV7yN3&BZCE~i0C!z_6C8S(eSH=Q`rZ8mgT9d8H=(BVm z<$^L-GvqAsS}U|v2_2Or=Z+YbIyxG!D=q3L+Di*5r@CVg2f(Yig-j}w*cCB{SW;8V zLV0Js;xVdAxqe>ilzYT*X99N=@`T2y8T|>}qLglT`&zS4-YPnM;cVIJPg*r{y)@cz zl=TLS!|&~q;iM%Ww-EG9Y?XxSy0|Kf`-i}i&?Rn6iRF9N-r%Uuhc45ld(+JR}AJe@?yKH)>Mr(Q!fG^zd95FQ^nZvlvQP2xumVI-lwH1mTXpA zS_A!`vYUx^lwF$iC`1hDR;OhY<((x>RAn^8EFQg8>ngVz#GZ7E zY*?hNY7A9tK3FIU_4Q&-uF8@d!wTg24~v9l?!+B?x?-ZV%S$ArcCZo$@ve;}%cfDe& z1XZKYQ}_QTj@>^3PwH!#OBio6{>b<#<2#HdBgi5FvgLV0@EE>$$8`Pv5$oaC_~LdCM=7^CZT2YE!>@32 ztcOSTW{ttKir{u!y!k95co+}nm-xywgL#OSm$NIbvgO$%j)<0*vkShsJ$V)4sR#Hu zOKEvIb8vH7Ud|Dit6)Z4r=Aq&2yt`%Oqy?VRF-r2L9DSu4;|z1aPwF(7i**%Fy36O zn5{ILN2*jJ5oZalInD&GwXNnjhtZRW!^KsGlZbQZfvhn#Z;ZpimlCmZrJREuL{V>? z#aQV8U*AgUhv4xM_~LezE}9zAgmW-%&Yn^8sYiV|2jS-9Gits~?~Aa>S-3ei`NG{< zV+)}|n7EjFN5WZvbS@Vt<+|_;N;Em0P(niJG$?ZvMrNHQyIE|JRN+--n*};B(x3`gy-M_86bxTGQBM zE_R;(#8tMi$zJrl2cP0gQ|JAj*g1ZJuWx0uJ@EKG#uvA5vu$SH?$~A@4f?L9V$Bq1 zH{9ii({(wAo|a&65lu@TmSa%3eL1!ciUK{yE?7N-=3A&ofR#JAG>vlkcoc&wc;@Yj zNAb>dqhR4t3@)NkjNwrX3b#LsspoJx<9HN<=39({iAOQGG|ea&coc&wcob~h@^w6l zZCiqW;eP*ys|@=EpJN@oHjNIzXLz`;;)~lGu3WXvFY7vUGy?w>3;ZaqvW37eV}T#Rm$nl4B`om6 z_~P~jp1Rclo3OwS;pSN27qP$(;^JupegO;o2V7+ffuF|$KY%Z7CGc}t;QR5#?F+p1 z4>*kW@GKVi_qaJ0_!%tl?{M)n0{;aId>^i|g}@t8{=c!aMkd>;(vet)4>1N z8aTT>NmA)$zTF#LG>fRPU;kCql-!3@){*Jb%P6JOrI%5;=(2?)^5jxyN8D)#>-~n=QYH%pr-!VoRArk^X$bjY+HqA_XAL*x&SHD% zQoUzJiJhpS!}Kp_D_3LNhOjn*{+m&|`oM=+&1b3$o2$k*jp_Ms+%32>>`y6HlH@vQQBT~ zw<{(1K}<$dPR2E*j=z^_R!uE+)!SFemn!>7Nh;^lRC2N%&SI7d7tOM?))^_d`?{>C z__}xc zyk`H+9J^Cf2O{Han`CgSl}Rm}ZPS(Q=t`8TzdEyLM3vV`vT1KQ5woNVEx*$klecQB zgd&%YRDEt~rECahbZwurX4&DDsPDDi6-%p>)7TAGX}zB?HRJ_xLn-$*ZH}U=6jpl_ zHKkpbEtX`Bdc5SW#|koI&*sW`pnniIq!CND4quu}+C_(>veZJhvwH2!Rw6ZYdL{Zc zuf$o*rmz3V`D<1pa@5nVMA83~D{&T|zJK#p;>{7Do`ckW7* ziz#0{?Qdec;qP~rwqzo*ycE9QZ%2xiTrA~OEm`VUw=@Q2(Xk{W@)YB>NO*@=;?j6k zno#%iUR7CXRA#EBoW4Ix` zm+-y?8e_{0JjHI6^Q2Mrhr@Fd^` zCU7(mgM-1oFa{ut_>A}m%%b=g;!)yW82#@S7;oe<7{g-)(IyHo?q7hg62}oT;!xrM zVmE?df5CpA{Wtb=>_4%8&%Oi3`uhR<3ifx{XR>?jGCKjI{5fEz1tt4%_I&o9Y%YxN z_tE&9<1fPV6@M7NYy4+0vftI?7mc4Ye(HE_JUxB_{BFW9e&qPl@kQf%kMqYFFq+@r zSueAmW|6%EVLvr6?g?%82W!QU2&K=g>Ye?=1Ni4%2!@9de68E;-klYoL zSe#vkS2L2b4L;t&i^1ryk!2eDVD~A4e z1?7M7*oOXh#nAt*82H~YV#UD!LXz^oxEbYtu}Ucai%H7=Vq;PM7nctFFXS-rzmTN- zFU1TFlk&fqr2H?%0hSK@FC;1di#s0r-xZYq#a#^j?}~$lbr%iEg^f!dhW>X2<$rPM(EqNW{4W+~;D4czl>fzpr~EG-1?7M7*eL&tODX@0Ny`61_ePC) z;C~@G@V}6x{4eH9`Cr`e!2iMuL;t&i^1oPK%KuU%tT6PyD~A4e1?7LS43z)HT~PiP z&pyikVrEdl0STAgpc$+;AfZ%)-e8kKDdagI2mW`IpxiIr!@&EF5|s0$wKVj-lDSbQ$GvX)XhQJ4#UQmM){bEhXs~K{;Eh zY?K)K+M%nZJS}~dae%M+Ds)OMUOnOxn3Mnv}IzO%P9X!bD`WT zeUnJgFtdw7+bue_Rlvkx%j1iPmrTC2zluxD0D3?mN zpgbyFMmbcPqWme{V(3mOZwe<7)Y`zA5(MQ-;S?HU6O=2ZTCfSqlTw`7#K4gf1m#C5 z{V@p>t~W{;Aw~7ZBn-F(lhETb2BE|C7=#v5)RJNl8c0!G7zE`%seTy*X6OU!H}Z+ zJ!o^;qRnLsAw}z80i2g(q)uKq$$cF(y|QvA%mdYAzem!Lz<$TAx%-f zkd}&ag%lSSL3u(-s&QiA2w4Q>2dOd^0cS93%Oc>M!W6V9ZVP$gwy-vq3UWndV+8CI zm5ovF|DPK_e?;(+;7!4cf+q!k5ZopB8I1gYHN5*jM{ug3CP)iTfHD6Kf+Gb>;l2Of z0=|F&qy7J#|1$q+{=@v=!aM&T!Z`om<$sI6oZsZ<_$Tvy@V@^TzJz}We?R^%{Bhpr zFt-1z@UH)ly!&{+;@!%-4xSjefVYy@f%p6|9?7%8c>Z$UVY~x*yYm3NS?X4xfc56@CYP4So!+2bY5j!73Q@ zp94z7hcI$+90Y+KXn+DNfieI0Al@KeAfAA6{(nRKl(-Q_3Y~<}H&1~P{!>JRa1nY! zO&mcigt7g3#2EV%_S@{2*neg}1f%-@oP9IRFO1=T3yi0588qDg{{E+d|7qZV8uIV>EOE*O^1AC^*E5~>;GKld(AhW^N1d`O=1XrLoCsNH$;j97K*kUV8rp*Ae7Ly}^~ zAXcRzNo#UdACsfRsvfQj$u1-*yFs!uENu_#wjhaRXhISWm8#pob>|SPGMI$Dod`*) zH%OiUNj#tkE``m)Lox(ON@s9DL0k$+5|fZa0FqS4kn|5reUPM8y~;Z*^$bhhL((-Q zosh&TaX=CaX&+Xw4J%lOq-9vwJS0t!#4;F%bq&K({jgLAN!*)uSgIM4)Xfr>jJjLG zb%(c0tEl@Wtf-@LU8u>UhUAe$QjP1v3aVkL5|Ws&Vo1t|qzsaHK+<99z;i(Z3_KSk zDbI!Fr92mIMtLqw4n5Z@%5zb5p*WQ1!X)LnFiCkXN-|hs=($!=o(p$z@Q^%cNG`(7 zpg0SM#DbIz;f#-sy1J4D?q32o!m-9epkkNpIJq<{xnE?rT4oE2LfTTPZt@V|Z=fWiA zxiC5KT<|r|z;i*8@?2B}NK%dolS9X}l5$MAbl{la8!E~%VZM}O!X)LGFgb8curB2W zap}N0!qV!nv@#^in1te#hGcO_7KUVgNai3(+ihibNKzh=G8TNBmByuzObttuLo$I$ zC|P`1H#Q`fL6RB;1(XjPk#j9Qa>YVd#HXQvMfLp!_c; zDgTQ_r~EG_2mTjw82aCpl>f!e3 zXZ@|rtP&DqC(S=I0rL(1;wwH%mP|djL%Q`Ei`A|5`yDWze^}COhb2Z)EU0c-Oo(p8 zOzM78Uy-@=nvO27={Wq(YQ#jglBQxQ79@KXQ?r=zR5}j5p%a!A3l>wd7|+4b#Cn4b z^6bOtP1T4_D^5trUWmG`7iByqV=FF^H63|P*X1|Bw$`-6pXoaLnvUJ?={fS%MqJ{| z#!cCFLh4bt^qG!BgZhx5KFnTu!W783ecEhT;w&bl4yv!7BZ&Hfyyf|bkCH*0kLe>> zpFwN#g}n7(Adq*73aD)N8sU1*GaF zn^k?=SEyP%^{f;Ozh!pmT>gmZ8>DJD(|4At$%4zQE&2@dY^>lcByx?0*FW8?);FZ_ z1Q{&nm64p^pNVwRhHNa@R0x&XqT8&A$6N8bsbMpu9nuUns}I|(>Ysg;s;3^BA|qjQ z(CM`r+}|WsJNr9J)z)URT&a5t-D*M<$$O-7X&@w)LnT}PxC5aol}o}=7Q7kbjV ztX4J1y9J4=7O;9eDVW$qS1nnsXjUJ(S=B%NDpgNCP#3U73=yN33`V|5s&?%x2V!>m zT?TL6RgSnk1yb+xi6gdRWja+GgiXD>oRIopyvPC!LRweV#KN#$)@+L%adFfgGnbuY zrql9-kg6ANR`pN5O4U;%1i&Cfc1zgcuxh?Zs&?+IpG8x)pA>tu;i5H1_N~r@KG5=t zr=8X1nAFrVn;br4)0(e!%%+&BC5|b)&4Q=b>n39PZc?mIH>&EehjJi?Y*zL9uTu5Y zv)NXcMrVgHRE?o;l&W{utTsig@uEfQFv{f~k=*4dCygFmc$!(QYuUnfv#VF?dTIef z!ItorQs#E1>$b|=;wIVESfbr-r!EZpy~tM|yjj&h{wh^ZjiF!)yL1LE%nRZCMyYzI z{VX;W5nnYND-=b65-GL^tihnKGmWbCQ7eq>DUK@(;Xps*Xy)9~nl7!#G~-@_%V00` zqehFPqRXrNaMKBA^+B6eed|}Ldg=jwjh=K_;AWB}ze%cg?5v+fqoZcclbWR8(eaC{ zVY|_4_7+6bsXCpOSDO5_jT+o z7?anV2wRh;Xx|mm*@cd-Uz{+h)YiPAt|6PmcziSaz`O)@QNaKYe8;yN*dG6>So^(^0?fRR3;y^%cT|=m9-d3dPHrN zE}kTn9#uKl3>BJ1qpnk-?*9)Txot#nqrl1kh<_Dd$9t1E$x}_d2*0owa-Za$!d<}m z1N;hpFK`z~feGSg@ICe@%s=lM{}_I0u4lc)x`?HQk^IkL9>I8uaVm@%_~2M=Z0_jY zqiGn0@3x)w0AWi$Od&=|BdY*LxamX_sG8ihWU zlt?wYs-uyON#u$N`Ce;|D5<}A^NYeGrln_vzRh`Y)wh9a>O`h z*QWL1en+pfNVU;mw2+rOL_W7l5iy6fT20d%4JOL@LO}1tjI-;byz}3kSfFHoboWKa z+W)lhUvEr4&731NeRS0|>fI-vASX{pjAxNIilzV*YEPbTI* z`?I^&>k~iu>s%0drDh?7##o`he`XpFRD>jlIyGIkSIa z898|hVmyo7S2W}J8&`fY`n}p`H*LJ;Qo7l8A$EHOuu%+4fcZ`JofXy`B#yXUBozMgAv=??y^%WEd^cf zJZXuOZDr0>Yuk)LQN>VC>Hs07W(U7{bxUQ_w8rj{Qg-d zSedsxc8P^~=0C*bWCt?=Cy>3V-Cwb?4RRfAZ>Se@G~w3FUqqnOi$| zzc;>p#o~MXFhte&;4!AVYf;v-P$jiSx4QQ)ZhU*!t1nu3)V+^9ZQd*S3^`dujAxNGjb?o6Md`aQD;|1MxR>9zcEOcfZ> z*36`{qR(ov@jiI;^Y;!kyHC7Ybjl&mpT78t*KMqu-giBCXL6r&{Le7X+>@NlBgQ6i zL8^;7)4r&&9m?ejRuV=W*G6>(Ptj{viOn%{LDw%RooXGP$NPqr+z+C*_bMkZzW$&S zUQ``;&m{0HeZ-XQjXmmoKn0O(7?8j#u_nIbt*`Bdg)#t8y=b94# zk^3Ei=AREWUa;Usax#k;7xPI?LSG9O3Z&Q7m6SCekIv$6>!WH%rW>}y2lZWVxnxq? zIvJYrrOM;0M_zi7!E@C9H;yvL<{x$Mzn_@*yOnQ+?Vf95zga?%lNrQV-h&~l#r}lY zR`!am=18#sGjC`twT`Qt?)fE!a=hs;l^Shp6*KcYf zq_G1n9QNU#CfReT=U#f@wLjeCa6QCYd)W2bA0HIHGD${Ao%c#syKR zmQ={PouHiT^`e%dy`l81vg#wh;uD; zLQ%wc7Fqdd#*dsje%qsmJ@m(mejcgZ_b_?VLW%MpPtM(I#U&4APq#mC{NKpQlM&-q zNNf*vf*w!WuaSEshDuSNu7@SAR$FP(=A#v}A?sHvYbBQw&x&82W{a{`Yd+lYD(}qi zyI;KJf(w8B4<*bXW7b&tJOi#Ygk`{nz=($rBLcS!4&Kb$m?x$upid{+#&us@Ol? z`}YTzohClraX@GE%vZ1d_rcfS{MJ$AWCSsmyL+}!)vRl3qtd3g27mSIeQ&59iFY;L zf;?T->NA$OU!0Q<7RW3A`qs&l_uO{iCqLikD&2!082W#y%-QAQ%idtBh|)3fSKtB( zBgSyVwQ{B>X(p=9OgxGR>rmSt(`K=bD_0+O&BQN${-9&U zFXn4@SrM=1NYMZI`8^N*!f?fh&y4R&PKFR;N6=d8WrDrFFA+<4TxNeh0CRUaLoPj8 z>D5G~N~EEeX?;z_fU!89evtLkM-JJi#yH0*KZxfGs^r-RZ#npTS28y3e#@JXaS$;M zCHf{)-2xL#C_0{|OPTG6?C^E3s?+Y2-S(W&Np`%BRy~$12WTCi^ziTZ`6=&$FV}0I zxZ@licf*E%-Se+Qj=uda`H8zo-h8IrA}2}2cosp-XvU}9e95iL*FAlicI>wFnDy-9 z;^*Ez`kACjaRJd zqzV#`gR%FSqs@_5AdA`mB`=Jz$2^DdQX^omuTPV{Roufx z!dAAblJ`umuBn~S#(frDsAZ4EVEPGfT4`?O+zpLY>oIAqiOPzJ3zt#-oCN#X_3P7L z<9_G}XkYR0X`4?K7D=tvMyKOV)U0lgNE9ombh4PSq6ukw?nbXu(fA5EPqfkU!|f21 zCpwU4mz^Zf)aQYOUVBLoQH-b?W; zOv$orOH2Z~=jPU_Q+4c0K`!5y`a@q;d9AhA-rB#txAtE9 zw>B>1Q8LThy-6J(IV#h06Ptl1rM@XaR04=ET%#KeCCHs3>Tu2t;1Xrs`+QL3-9?dS z8yES0a?IO4$-_NTBxO69YIx)oU?FzPsaWll`b5wWj9&>{>LSc%cl#MI}?VeGEo&X}hJgK>txeY0Lh6W8>1E z+@CMTgKV?8rr%Q*?kp;QvavfW{;Y{u;^_F{lP^0_~I z&&Z#Pr|P%2*uv@GYXf(dFy7tRoh6L7*v<*>wLl>Y7$5)5#)Vu0<1O}Mta}nDaslJx zzuvgWm%w<79UAAJ2#Q+3`1r3jF6to|Z?S6w`cD&-wt(^R|F>~zPlR#yVgStTEjD}f zd&0tFt|0^=<{-K~2hC~^to_ikL|OJKak$Gmfo07Wff{CgW0^$?7= z_`nDKrvyq{!uWSLF73%Me&nNUi+ut89=>pA0ps^7e zoB!&jd-G$@e(7v>_9=jG?<@BIJK%Twj#K2udvARG4gBPNC*OI(U;nG?KXm=`uAdzL z)c)I!KN>L9O^$9~`;BXFyq4YX>>nMz=kWfqb?Zm={>7eg_}RCM1v1co-p>ji_fYccc;~nV!AAgEi<}k@H3uh z@FI+#mo3iF`J_S9v=onwQP00<@Kc^>@Zpbb;NhIpv{J`$sNR^Gh2xGFUo`m1Pcm3| zxXKXpe7VnxwlGv<)$Ns&G!;e{4Zh|{2J;tZzUOIiHl6XtG$icAFB6sd_@cpEPc-=O z)DEhAwZRX2)&^hkB!kGq9)dPxR98zGW$uP5Idvv2Ik;HmYfm!xNG8eW=Xl!Dc&<{L zjLLPbZp@sE20!zO1|NPYpbf9?hoAPW4SwpA3>Ge4b=ja}f6@~TK0MlhDmS#LPmo5t zCC=G?CzNdSVjJFm)&@WEi3Trzac|(^3r{ro@V6|ea;2}#bZXLO46a&9MlAKn0Q>wC z4L&l!UTyHXCmMW;(dHALWU%-M^sXMUKmADt3y&DQdOZC2CmMVhdSGy18&Xo1M6y@v zBrVCb<43Uk>L(g}WKOx-;Kx1D;KOqY@UW@yEKci9i}8}CTJA^ni%9v{CmGB?jAhUd zYol@8CpeW6%C-V>XOEf}4SvkCHu$P18hm(g09C%)!;gNV!H2QD7+@!MZ!}`+(~@IS z(nz0-FIM@L&)VQeJ<;I9g9E7Y)gFH26AeB*IDjhm1+pIXaW@DAbi18~pGm8hjWjz{B~#PEF4zI=VI@Ofr(9iyq#5qQOUya<##;CmBQ@MhbW-ouH-K z3QNjuo*s-#0i9m-@bp<5yzxYXk09l04^N(G@L{BYDjSWsR&O-wW?d^WHC_!LSu9_V^>%+q*87w~1hF7l- z?>^Ds!yW>pTs@86S)Bi8c79^#*4u71Z~l*)|MX_<>@Uu~^Q?0Er+{Ujxbep~zUjti zpZwv;TTZa+zjys>t|Q0)2jH{2d-SfOHynLB`0ju0wNE;H$6<2#+`%s$Ltq-^K(h?Ilv8|jy1W;)kOK?+UPcj@W2Y$ zMntyR+i56byy+0I*klirU^oJCqwD}b4#<|wSif~Gjl1MV~6@|J|M+9 z6F`Yvp3-OS=E+ zrP=S;z${gB>6Ru@vuTc*<22ST&0F&xREO!BnXToRawP=uV6ovg5$XQNEzN%W24+VU zqCq$?38j7~=$S1Vod;OlkxHJ`o(gmS(?Y1G8}|%Ty?n?fyKIo#tD-TnoE1wv*QS6f;Dc zjc$KJ*jj}{fVUsLH2V)XFiW@FQ#0)+^&*plOD>hEv+aYjXH1-!r{;;IL06c z%)WAI_M0~_J77T!%hbg?h>|VNW~{FCvS#eY4Iz@pp;#W}tBf}hMcA%M_djZB_M0{^ z8&8KhNhya@5!{cMckS3};3~L6+z8Vs6I%4x$hBPy&NLM;CVb@5>^E*;HVkP*hDw7N zj!D{~U_Tg71VCjVW*DwxH&D?by?!TPybdGX|A?j8Z`i=>7LiWU{SRN7{raoJFt!O; zlJ37^Y4*VeX19ntlJ0-l((GF{Fl&x7lbT{Q99PI>JCjaso+k!0OBKao-K#XJ1J@T^ z(HUmyV7$GxG<$yovs*+HN%wCq&EDI<>=uzj(*3ig*{|Ec>=rRX(*4t=*{|Ke>=yUv zr299PX1`_wvs;ArNcT^cX21ICxPxsXbfo*&muA0e1G8HM+DP}0muBC*f!QrWYoz-} zOS50Of!Qs>XQcbrmS*3yf!QrWWu*IuOS50Gf!QsBVx;>AOS50Tf!Qr$TcrE@OS50L zf!Qr$S)}`WOS5mh`k;NAkQC`YxJ3fiw{O_M>=uzF(*2#K*)QF|>=v;k(!JL&&AxsE zvs=WANcX;EY4%GtFuO%;h;;9DOS7-r!0Z-LAkw|pF3rAn1G8HMb4d5Tcxm>FH!!3RU7Ag=K3U!-21B|xUz(k7V0Md83hCZ#X?C`O*)4)6q}%E2!@dEO_pXS8<^c9>|yc!f3QpL+<0*OF$Zq}fBDd#ty|ztl?UoFef`Jp-rnMN z&DAgRLNe3Grmrtvmb-L|POwN%yiNM=rTiALEX&lR7gBn^aC-|(U6#8vHpcP7k!3D! zTVJH%zPd%1iI|N*H?~XD4T{l*L2LvuDCUb_ni{wOetK4!f%6blEEa*mOY@N$H7~%` z(3DzgcP7LLTsNPDd9&hLxk72AH{(%S#cJ7HAQHMIpd~>~-KgFuRx6b2+zqp_b|;S! zo8%*1Reg~e`0#9>N_0eEwU+9g1REP{O4Xpk9T7o_7Ze(4j1dS<6i6OwIXV!?>am~Gv;JEGZ^F|1a|##|;0qzyrqi`8^z>bG3irSl#F56o=C zY%tZ~6kJBGq*^@)y;{4-`*@O@&4viE8SWNU2e^B)`#@gc?!LBrdyCHW>~M!%hPy2y ziWeRC_6NK;+}6Ma9KAFO{DXv~$=HZiBmQGTX|oSUHZ13YECelw=!I@qHwL|lw^(v6 z<|1PXmY*ww>%n4iTLApHoT=9T^-#URpbjlG*S}plw>~7H^@3qwgXyW;^X%TDte4vE zO?z=~v2F(Q!;Ai0aB?*Pzu)nte!mx-n{sWfxqbBAU%8 zG^cr}hlEPj9rnyXJ7>SJ^ZjS{&RS=$K7H@**PQ;q>F5*#8Tfwj#{C=Z z8y|o2`zJqm5}jZtJJ){!WXWq?fA#TukALtu+AADmJMTH(*=-*E!qL5>*3ql4z4zJ= z>It^W0@F=G%A+8 zLC(?|y|hJV%y?3cf+^PVGGHsR)1t`rlSY9(;rW^fzHTB$N{`+=Vpakt;f8TA1%5gZ|AFxx=ZTv>Z3 z=OwmvaxCYDtYk0_Cs)8>`#cEC>Ojcw1*=8(s{!3{LqgQnC8}#hG-V6(Ff%cgP#4O5 zftZzQBM;8A`4Va5ntkYVe1+XLY;Iu3r(Ttoe z_A4Dgng;a*YE5Qw_J`{dLj%k7AgH8>Vc-wA0aqOI4qhuxi7GyA^(R7EFH-c>o}at= zr)$N5;3Yq2+)fQfF&oHh=5aV2A)#xnU?vl8Y^WCa0 zJJOnY>FA5j6!uMZh2&Ap|^DA=4T5_3=>T#{9aD2j6tAC_r`= z4_YI+JVJ0qn)EYVp<55S`I(|JD4vp?>Xhh2$(lK7?|o}UU*LKM-ryp`tDB+b_G28I z)}^vHwb&fi3LCx2*oyQ1Ir-#4f34_@VY$o+R)bVwL#`B9JA)6r_tnH{V6^C?C2ztv^w_c!-ktIT+JvKQZ7xTs*vp}sVvKv|b$s`_L|cxU zBA1G6bt(^sk_1^)E@Po?xiq5Y5tn*&pcOD==C9x5jV}l2#x6zpRsXpvaTdi{Rt*Gv z;%ViXCIWT~7{POpJ)F&{vRXIVQO0yx!HbWigKtb6B)Mgl!^Oz%8l@}6ci_| zep*Zd606L@wvBONbKL<>KJr`$CS=<~gf6LdGz{W7EC4L^xa4mPRF7fnh){2&CL`0R>%qrCxp}B%c$4PnSXv4fHPtj({G%`j; zu~czATOEA(TG4Fgdn~H(0%{hkm@%FA^{{HBWuwxzB*v+V5|!5JFz2p;y!YO<;s9{J zGU*~BmSjcJc`^NJg+3*7mewPIVI#Hh=N%m}X) zENjkKxia1vz(UQ1`d+@mDb-*=H6njK1Kh~16=V5L}hbZD)(|Hc;;ao@$;RAP!KHk#O2t*@1QILstspfXM!ZSYyh?5O-2$H#wi zuBg#0-(VVKGaDE4X~JPndbg<8Ri!1PL%vVyIx&IyJM_AaC+8if$%j>(i0ku8n=X>o zx!eVtOT*c~8`H%?UPmWseb&N2tlnDj_@A#!Y$BCTFp7m4mq@rWZ84KUw>lrPOcF&! zd(yAvTc{XR^xS&n*gIP*RtMa4Jf%32fuvax>5eM{K;=3rWRMgP=#omprFx00%hPoo zPnfl0#UE!!`7u_RmQbYCV&+khWvw(j5+_i@tShK*CMYxuhg<>gg_!Q674aN;K=isl1x%le!pM3dRu{fS~S-)y3Sak|o*AYmlP?vm& zLF2LrI52BGgO=eEK0NQ@uRB+CTSY;%J4$)bu*c;|6>s-BV#;WZrl*X$>TC>x(8mxX ztmpCTpM0*!jW`4wq##+uFdP@WA)D>ZyOh}1y}4qlM7e| z`ATX1wZg6ogv>CXO{8Hp%t8u8O@n5Nd7d`vJ7W@qz22Zc!DKO<>g#^9|J!TD44JD{ z3XWPVp=sC=CWTxzFXMxe5l2>-sX=f$%_ze#MAwbFx1Lm!S;7;dA2(pM2N=D$#>6Cb zbE0H2_4%X=?s8Sf9l4J+qRIL-?%S&plUZrt^jl#5gQ96faK>=p;6kPlR4Abnjd&#R zrd3_D7-qfXJo!g!#e`{;xv8!vS~C>JCd8L`7_sG!1J)l#r(d2L8hcSE+z#*OF4t59d&Xqh^7Qi6Qu`aNPB{u?L?kVwHi>Q)h6THgTJ1 z@kBXr=V+JU0-LM#y46&cL@7VO&wJb}SLgry&gs45A3u2aE`QZB{6oJ#sRh3B_`w$* z8^Rm*zh=IBdkZqtZFg^Ti(RXD7IatV-UwV+Y;um{$msdqVFZr1?Z5Y3KLi?b1atF9{EL!5^FN_a`=H%P@uh zP+y)(Rt(P@K9>w5@8JW@?BbO^y4Cq~2bg&%hZYy136~zEh<9wS(Dv`-bG}}N9n`s7 z=~Kp#sC#0o?eRrSZt-_pIwq;mQ}1jt(+SbCl40OcGv`j8D;7gP1>~4zGntWKt0Ru?B=UfPVB&d;gO2mF zh(-9-qX2y<PYV?_MJSg?2 zWVTfpX`C&mPam2@B+Mw#Z&K^JT~` zVt+4@y+vs3-#N133wLoJnNmLhWW!e?dx58?MD}HlKP|Gsp!C@2KQ9`}dGoGQsTLfp zaz|7FBPGpWE*#9qmpOWiGpsM5xE(%7gYzy zepB$^^OiwJ_U`R1y6e`+erzNy`oTch#-I%4k3hC(dARM z^3D`zDmE6>vu$sfZ!u4exOt6^Ty#Q44B=LwJM7&Sb|<9!FxhO)qPrBtN>12RM)d|S z>hoFwoZg9y&|-DjPit+NHSOx-Zvc2V`k`TyQm?hreFvAg@V zyXx+z9)IKB3&(%C`@a1j+yByie*e`6KXh0+{IH|HzIJl>u9KfWdGks8%bD$wsoy4 z>v!&glG0|-Hgi@I4Y_KMwZ@>d z0GsGRe%3m8(@LY5Z-Py1rebEQ1Yi+Bg8H-`#Q_HP+*(xFwqQP!2a#`5J309JbFcbc zfwr{4D1>WVk?Z(5%5>stJ@AHUrH?v!tEFNU6K@?C_P|TdKKzQV_}8iouTBf#OEb@u zM-?qb+v?EFO_k6Kv9vTMCbe-%>w?(40Vy2)(NaUjYy8;6IF_}w*h0a@@)Rk9)IbBZ znM|5eCe=HE*zohZryTvts*G}@3OlJg?Ai%aj1ViBrP=8aBtj}HC{vWoAx_mwc7EWv zJKwa@p!{ks0o%6B5Nt=cf+FsNy}qJoV<@UdEamsnnXYH&V}?2T#+61XvgXZjJVe5z z4K-aPb_xbXN{EV)ot$QLJu?=p(Bmb1@0V5@ShEUa)^zNE1N&w$6q;qr2rz@nBzQoP zVB%#lNl+C~$HSjlYN$}yNqY1kOHs96A>!PiFe|D}DC>JY(uf;4Lyf?3BpIc{Yj0d> zu=#$6Fz2;6M-U!T^N5n$LcrFlh=WwGWwejtLFxr%bAn!TS6&&DNufe>-nig;!*Rmw(p;;<-|X1iID5^!+N?Oy-gRT-?>hhmd}dcH|?MrFdT=Cs+s z6|q7`hcpg$y{Q((bryrJbyph99BE6_Ou5q)`;ObACIEFhi8}RKq9nnn*rlp-2#Z?= zbu?XR^v0+hC4MWTDGfNM*O&ojsJ51KdQo6>phCbj5tr~A39|RKD~)cyCc}x)^=E`^ zsM%VkKgkH=ep;2tnN{%H4#Xn!c~vP6_y6COMlI_pUboA4YEfv7iUd2KNmZp?kMk)z z^()+1hRJdk8Wr6A*RC|!nW~zIK@^2f&jK+6Ieq}9nF&>F`FP*q)ozXu8>3NQ(2j$p zhKkF*x$Tad#oTmSCpk5A9Z!<#)65{@5lF}+*$y#j;9_jvh*uf~a-dmF)vJ@Npm=3l zRU7)WJ~vFZTNt$F+N6%^G&fdF;pG2WdDUebW~V$fo1&ZTm1VSEF_F@MSBAwpJfbsx zd8~HRS_P!@+xgG8(B03tV(|FT&+8UgzjLQc1!cHMlViu?QjBxTMO^U-RUA{W$9U*W z9S%`&V56LU>FNzKS%)R8yeg+i#+F=4=w*$zIx(vR*Vj=6CmSVZEYBShKdZ0G7?aa3 zT9Zr~b-P56vPM`#(0Lw2w6(~3hndcjsgo@P)h>SaxhoAMEXlMeY6dkQ%>zX!XX*NY z%R3pnXP4V?)0uRO{Y<^vgU&u~rJ-tcrVw+}dNLqjka2rj?KJZ`-OSDrmxpO6TchA< ziyq?W8K5f$D*y$R+AKbYJdYnqqfuFMkq$U;)}}m_*BOj-1SdhJE>Ri}Pv5=NP&1g& zRSHBJW(W9O^z{ljpiONj$}sV)mT6Z&!ti)nuOlXY`co?nxTY16fXheud7l>1QW0>8 za=gT|p<$KV0+uiHkdY){oF}ku)>SGoBQExnr@SQJTmC+cO1eby{V5l2QeHL>L>$jWH7p=Ee;|NB>E zP-U?hv99KH5e}a;@k%kvNKwEh6;Jj$LzD2dT+bfGF1q{OD-F`F7xN>TODnKdCu&A> z*6s#gCc&%>U7djMXcZz{kl`%j?tjBdgP59NA}?6pXo@meC#u8Q23IR*f__OJeZgbogu?ptR3RNWW^lEXOZyOERnt;#`tqudy1^T80zGf|@*@Rf8lfFv_H_|~O{GD%Qyu-kA93@pg2F%t}Fb8(oKV^5Ka1=o$Cdt7C_@_)JnXdR2@n)tXBd{VQ^+VZL!M!$}458 zL!wrwM1;g)zS~Ntg^bghs&h9WwQ{~($dbgoDnuY@<=(HY${17COeAw)YsBpM+59}E zV0@v+CnB0fTM-D6XXedXHWRf}W1Jf{R5l|zy{_6; zp_AImE8B#Gu$0(XUTc<1w9)O(Q;qOvNzqS6UB6zg5d|1@o%>Je21eG)3UNG2;Q_IKa)@~hK_JbIcI_{wV!`m0C; zaDQ`))U{i0r7rf)UlNIsgBJU7mz|oRACwE7TbvJFejd6-cHYbOR{wS*5iUD@*f3QMK zU|{QtSiy&;gz^Optbzv|0*4KR_r}@cop|gTS#pybR(WVq^~GuBwFkWyk+?-~`aqEg zV~<7RrB^)vwqt@mU?gsl&id~u5_1nD@$$=$Pm9ES9(ybj6_M?eJ*P6W0+1QDoq#VC z5OEBRxVVWTZIE=i=b+7kU9)iI>CZYmaE;70dQEyTC!mRSCnpcRDMG4@sfEqdEk!OO z;k|(ZS*p^kuq|$0jT&`&6d1f+%SI{KYn@v2naZ)KW`}HW)VS)s;`jvMK2<8 zi{A7BBN2Tp5_2Dn^TrPtiCg4CR59ND9aAH8E4K}85r7);W1iU2XM|UaM?2JkdKeg`;pBjl$uGF;V*1R_z zriLYsQ0q=XxRXmIL5DTJ;}FTr>CKXonecr`?L_%e3UOeIr=iM$cn9u>9PrR+wR3&3 zoD@Q#Txhq+O-K&%P##o$g~aZQNZg_~eZWX8J{F1a-%BKJ5o7%FL*k>L1E{zN&+Nl< z;({II*&z`DbT^MfqRos7u}AoQC&^Q2+$2Z=0@vD_byFV9b5$PPV5#Bt+GJ>gNeCEu z8k!%#V$5lr=o5?xB68!9tsp8IPAlw;U>LzK>r?3t*Z_AxoGb`}Ehv#RB&Nked4dj` z#deN~xoj5cM}}z<*+DCpXf0*5VJzLE>Hvw0@Bdfr%y({m-pzk;^A%^q(_cP){>J>| zmrg$A`d1wP?(zS8^exx^+cp01ZHGq(+Wt@NzhbYq`!l;A4-`JoKW}*M>-TQ$>>8gU z?Ce}W-aS10g5%?DvR$u9EmrZSSF^ev@A_Z&8$bEV<@3Sg!Za_Y$#}zSzJ7P%<0rn~ z$D`X@F#5iPr{ii5j~2fD_#ZBwKaoDp!;3j1fQLI5JiK$UwHUA z?{CAy+gs2VKbE}YYJU$Gp8oI)i|3F0^2RNC`@xqi{Jr~re-CbN!Hc~1_eCD1@zs7F zEIj-hya(?dMAgW3Kjef8pa>e|hoz0eWLkpL_7y zg{Ln;`rbBq$=9B4iuApOci$H*pTDuOkFR;~#kY2@uYJ6}7>#ak!2`W^ag~1c>SkQ{ z_D{ZS`TS2Cd-(i=bkT~R_Wt*Mc3ZQAq~Nf+Zs}Z4K}51^XsFYyYC~1@YmgmNkkd6{ zNtjSCw&L<;%+++lXAAfK;a@JE-}2a3eKE<-8=ikKzu@HQZEz|t1Qnt}WG4b#ZOO$d z$X=1L2tpr@X}sKWDd?ld}s>-ncz$%{IL{y{(fs z7Vdq+_b;D6vW1h$1t(8#4_MZp=Jj#4omCBa*p^1NQyIu)UNls>9VeZ#G|L$(q!P@w zPM$2>dr)3J*~d9~F-g-Ko_jD|IQbG}U*Cr3{6!}h*EX*1lGhjBeeItto?pjo;p60j zkH@#SAWh%W$K^WX&HrWL<5zwA<9vLiN51Akyzud%b;cG9>}wwv(X&^#;&Pqw6+gJ} z@J)}!%)_aqAm|ORc`&}<;kDbIqvIMmlEICSW_<<+xF8T9>VoWIHFKtLk=&YryWBKZ zP0Y22%XLO1(}^e=gGfyUg;2_si%IpC>x?gZ-@?N$`}4=O;UgY?#)D|#;X~_;E$HUg z9zIHvc6Ey`*BP(>goVFv$ZzcL?FXZUzYnc5w&1Z}`}>l0#+B{5TxY!YQx<-{?#&zf z`Nql?B`PLWRLT`Q8 z%|E*N_M6|byR-AIYrlByyROZyHICnU{H51E_4;$ie|h}QYdhD&>y^WIU4Q${@Mh)a zb7y~f_Rh1n9(RtRaq9D8_IpthH~Gyp?{f`+(1$0DOVA}9elO~cup=R zZ9Vv>8_NBYm7MBoDWlBYP$fZ>YfHXp?|OAZId(%iW<$BkhH~_Va^(%>s14=F4dsZX z9D5~x_gD6=9RGf8L%DZsDEIF+l>3DZ<$iucxqrK%+|MoLuFSQ5{hw_p_d^@X{osaj zKd_xOdQzoFdsZ7BD>8_KQ6mn-|}FD>m|J)i&LhI0RYL%CnsQ0|vElzZof za=*HiyBgXxc{eqWtRgA41c<1g7z?sXf=y>>&nFWyit-B516lA|pTZwkZ}px1}6&<@$6rPhTy!ny0UpTg}r~%dO_= ztL0Yn^wn~!dHQO(6@IUlTjBR=x$j+k|DWyHJE!Ii|t_{nY<@WA@<7wY?gN&lGqzRp2tzld%ayFaJL7(O9aJL!?gP3E|*LP2xk9 zZclXD^Lui8&d{-E2*adCQi%e4E<@{H-^MY9q^iJXs`x;jC}lOcJ?|`jCm~ay%M-v3 z9@iXvR+lKC%MQQ|#2KIB2`33K53a!XVczP~)>O+0g*s^_HGEkHNCppVRGN8>ZkY|S zWGOHYu58s7WzB2gCNU@r*v>nu2z)53v?Qq`30y+sfn5oXp?bjRP%}jZm5JlRdc}IXAsPoX@;^XWRtAynZw=Rd>J+w`HY&Z;$`$X zl*lgSD=vl9#JtsOgTy_cc^_^veCE~&@Dk=}qfm+os8T8sV;!mKiObhQy)yMIB1|=S zf&l{7-QRfmHCZivMom_x#;qVPRAF=6YhwkK^boOSUeaV_kL^pXD#~V?s_gZZ7GJD2 zRKU;8`;t8^Mu7DmZ(~hQ?{q*f`}LPyljWWnFe%R!110U{tb7y3%35GmG+DT$$%uwq zpfP@)u|UQbZvFkX z&3dD%FU*xzt*F}LOPUN^c0{P4G{ACN9}#C`rD0A>S*Dinb%!uVR|SpecZxz}=lY<@ ze(Pn|WVvSsOgc$;k)O3ml~Y@7Np;(fL0mSLk#mpmaut*Gaw+U}I;A|@E@x^)Hk>s< zstr8WXDv=~wcJ!rhP&^288+FonlXtQH1GjgSR0wusBjGOPWV;lvR+ooI{B(L33C|< zi}KwDxb5kTqZ%~E1YyiHb7N;HB>_M1Ndt^!zx}dnGPwTCnv93^fE{!^Hf{rwlsf5- zI?!rCa$&}7sbVm{9DfcE@Ko|`FYYBsty zB5Q3&inL?`#?*IST&e=y?|JK7#-~F+t{=WZ^;@?pVfW*v$B}Xc_*uO=**wtZd z4`)cvE$+NsdXN87_Tw!g!WLJsZf_Cvw3Oc>j%#_*X?aKJr5vf-IQRv?!}l}zFK!6^Q=;XI6To7W^c#(MN@XiaAEY0@3wXd6sTxh?=~>5TSx#vo(71o66b{DN9iDJ` zrs;6w#6fiNPUJSAdNT%xn#w6*gh&=?1W*})~MaD3#GQLww zOB-<$FRKo2N`b`8#sYVX#LQcCre}w{OSy^v&T+IY5HH~9rE#DiBqUv(nE4SbE;A`F zF8M8WFXTLaag_Pu^GwT&Kx_chmnKhssNNq+%)EH@tLxJsonq>AF*_6%gx9gbSHJcCSvNimyc%cBw1DNs*+F|$0-;cT7< zTcvS?X)-$VYfZWu`*aWWlVaG4y#}sCt!$gC`9>=kPBg=pvIBltnJZiofs1;9Wax61 zgkmM|3p7VGaC|e`kVVzOaPy|=1ARHogG@qOVD8xsH^s{?L~k?PEN0P1rr~W3To^ts zPcQO!GzG5;f4cM{$fCzTF}=v;90MDr7kOm7dJ$cV#ez2k8QrbnIQ}dY1xe;#{OvKw zgC0-V!vxTj5o~uvkL&W-Z`>hEcM)SUhLjl;a_FR7bLu8HN%+pwQ=-?4qd994Jc3Mw zhLVtdU$2h}b$!H$fVH~$v zF(}-$;4ac;{6?Z`_5j(3XjdDGq93cHo+Y+pL03RL71COA+$K4Zj1j0l1YmTtY&o0#_r7Ni?yKhV;N^=6CLt91@uVLd7{qPrOksoki6_ zXFs|5Jv;2q&+mX3{m;31cJ@1GZ#zrQsI!ka{oT_aIepz}_4H$I{NatCxb==3Uv@*d z@d+n?a`G=vzUo9i`K0TAe*NEEf6MhRy#5)-|NZzEkH7g?KZcKYj^1(f9Y@X)dUWmD zZ(jSJYvHxezIJ-}?!&hpP7le$58wIAgZCW#@WE>j*n?N?|H1C3@BIA!kMF;6zp?+C zy+7Xl{Jo#vd-Gm<@AmGW?Y@2Y-mUMrY9i@M{91UIB-CO!^Dz$A{AQ)|FUjafpA z-Mqk6=aXzjCImTwj?Y$JxyD!*NBK09rFu5s>qj%97b7JLcH6JrHPM&e@!HUz?acWi3WHnYD*Xnqz zlB~zl1s590dY+hJl1|g!oWN!nt{kq|l^uI1VkHok#Z8e84tIRLBwL+q+7iY2xN0#V zkCfP)7ps6r|9F)%z%jiUfy8;top@$>D$g4r-?@vjTwLu!nS41OhHWt}r+hv+Ua`SD zdXcLmK7)*MQ@fhCJZGFhU9nwH3{{gHKF1JE-wE5ep&h+*RdY3+v!Tk)%`~jhHK;BW zVKH+5~ndXvPNNvj0B}pmOM#o#(J(QL3}*TWsAe36~np1S?vO#)0J{s$7_=Y zT2zf`quHK2Sq1L2auKSiJ`6HJ$mr4fopa=XiaJ_?whESQ4TB+u^cxhDZ`2^a)E!nRO<~V3eR33E*Q#E#d6XRvN75;j+5<@hT5u_sM|&-7G-O6@w@?~P zHB>wLsI>+Kay$;z)HIAygPtLNw;v~gU+U=%Nq5E|hD(Yw9_H1wgBAO@!^A8r>PaWZ z(7ZcVM7>7}Ii2-Wx~6j^I!<#9wQW{y(mea)RT<2PF?v|g(M4l|HXS1$4Vwc)3n-cf z3<&yU1RG_UwHZn{_*W|p+9_#yyVx!AX~bisTNyjTTtxIJS1`)mNu7|yuwQoC4dytz z(kK_{h$~LUwSbixO|WyM)sYNC)sb>pZsH~$reldKq3Db`Zm%?`R5r1=-mxcI8xEq5 zkSn4-$lk7ZYfvvP`YoHEB~ViyqWkNfNNJVfXzasw4`alU$4#aJN*6;AM-l59kK*$t zZ$b{0jiAHXx(rSREXiG>Zcmd+DG8i@=o1JV>P{Yq%GIDX3q;tIG*#ID!LQa_ZuZoZk5Y`)vBb=hH*QHW}u?jn4Hh!DWw z{PQb~5|^t_@-Q*!bZoDbV#6SyRzQsgomv zpwr9O2Nu=n&dOaF!lQzkDKkB^5SQ_>x%0oSG>R!4SKTh3a-6HnEJz8VV;<2Himib# z?X_BI3^Rm8p)=*^GglgUhpV!7b=HvrZjcX>2|WtS5j0SQ+^A`dd(EPraR_Lb7p|>) zdk(=v3ZyTfvTOks`^BM^;iq_|7Pz%u1W``VOU172PDJf&J%7M`S)=N-=fSnwnBn?z z?85g_DfG84u}!2s7lDY)3F&M}WN z>f@=?#E_$JU)IA;#jxty2Lb_bVC{^1d8dvZY39uM7ZIWmECtj`^N5V zJ0(yMGr=$|lSAWC+LUk{#7>gDI1<<5VFxuI=jA5tVk$KMOI@$maoe@asK7a;HnW_q1H8)d1cma)scsCqg zd;7|(Ap+7X3};`$G|W65 zFig1@l8mo0md~SdP@%)Jm20>|5Tc4>t({d;6x#-b_f{@Lz|opf3ll)Bws0{I{lMh4>r3LW(n4sRWO z$EqHBsOCBr;wN1woiy@!D@uf9W|gawGU^YUMnOmLTFpsCWp|a9z}7^z*{rfAHef+? zW(}~f3j;|IIvzCRQVI>GPD#PavE(vGZ(Eho7fA{?97hC>C>W$98LviKA`To){j}L8 z!_h1oPJ`<3@JCk~z4g#sZGdpvq9zb6cQ%950!6DJl5KcASZg!@ z+3_6a$904PN2xZ>$d$RTn!yNWyR|G%_4v}XgA5zg(f6)2x_!khw({dRGmR1)!A7~H z!FqVW*tS@P;LM!1Gd>OX1@l-r*T_m(t~-SohMWxhpd%LHyjj4&F>EIrS&_hx4AyJR zW$G;d|Fie*;j*J;o#?7{zi))**4(K33|5H_t~@9qtH@OErDdJaDNoWU#V=rcHQ)On8L4QCh}Mny&R;K>AlwX{@(Yy{Jt;A_g+pUim93bWy@*>9=1iIRxKmkM7FFtGQf=S zAlb?GwZ4OQ$nd^fGC-;0c&uDPxqK~G;L_Pn+%VeQCK~5Ne6o!T!mFLfVdJ&Qi@fAW^+u-$#JKm zCq%l~huN$YY4&nuq#z+R@D5_-nDN%@+^vM>ZJVzbg?=*PaFd)yMeSUgn{coZ3WM__ zUk*hfD~I4bbm*#}lWBo#*)nZdn%d%3LnEBth@xXkF-DGuwrCe7$<9FG;yW(^Z-Kw= z{Y~)c+j8D>wO1Onrf`mudhz&RN@vr^faLmhGL5ttT+usfDNXRztd7YCNms%Y!UaaF z=k#8^5HzzLn1ay=oYK)$kE@2FN|Y6BLK^cdwCO%qf~`b0N}VEI9+Hh|NerTWCoB!5 zaa+q7W<(4!wQK`NyF%Cm{{3LU?prCWTpC>3IsD;6_~5q=u>GIir}jR%SKs}c-FNNY zv-5$S&Fv3wKYQ!PxA4v1+q5?RbfdQZx9jb-udaP??P&GiuSNnN2_#oO20-LT%uR_3 z9jqKptG!Bps1$=Gmg$IzVXlivXe!&ysd_Co$bd&o!}^U5H=-2WRghsWrX&RzYj+ALj%O<{L?*~$sl)bFgpO6g z%MEd_RI9Yc_SDLZhLd_~1Qx~FV|#vwPvw2uc|j+5nr4-RFk0?OH8-Rv%wK|fW=C~9iLkOl3!BV!`D~{sv27y@| zB0wn8lcK8B4tdvbI^&S^NC)UU2Kw6BajiSjK$$`wj!h=n7B9fEt=VO&UjrY9QdT5K z>O-1Fl!E8c{hA}eKz>99dS-OBM`noOQ@s(54T{r-Vt2}D-Oj4zp3?`%LwX>Es#*sL zR(R91o&~zLfvOo@d%bkH24QU*v6E?)K#cBSNa^Ja9VGLdQjXJVQg3oyQs+p|s?J}% zi?<`fBht(k(Cy%`(~C!@>Cz-U60s&t;@mJV2%}!2(vD?@N_l{0F)EE^Mep7I8eLl_ z9S&u)gYmc`TG=uML4p>?!$QhLIdobcOvfflgQ8uH!H@wt$?j#-)zq+(o|Y*CYo-T8 zaxr1a*K)$lGN#>be>#;dXXg>|#F%DK04pjuQHsr2e%J*L6(ZNJ!ow2XZ5JjW_d7H# zjG9^0o)8yg47{-6G#+UJTV`~Xt>Cc0OJ!A~f-pDaKr&psoz8+AU9+TT%DJ{?P#Ue8 z!P3C9EiPRJpkYQ=VOT;fwN47!SRtVy)|A0l;c*R4Xi}qZGLSY3*Uf3ZjbPq;dlp^a z3iQqBY9xz@kff6}Wjqqmz7C$fZ7t={ISy;$-7;0N2)iF;fg?^hTQYi$9@xhd0B#>~(sj@uldE^%8`WB#SM%QGL|=hGEhSTMpP56o3%FMosLhpoCb_yDeX%>zhwHEaVWSI4XCGSjecA%4(HtR!1=< zZH;oN8e|gv7S7TXO~f2xd7@7*bcGWO9R^4Jn@)_G4#9rhvJ2!W)+y$Qj;faj4N#Ix znoe+hnpB2zwOkt)otEMu&8#P57E>TM9;pLcW_0zD_t^8mxOC-#h8bObWCixD;Vimx zK;Mk6KC=3H)^{siYe3J8u0FEzde*Z**D6poqib|hngp9zFgJ4OViVMTQ}R*TZh#l# z_^`xbu^idW!`V4#x7*HjIKVCYkJmj7G29g-;Ayo>g;?n)wj~M1oX`4dZEspCwdm>S_GEj>@nLut|G&5L!z+j1cM#tD)NW^IbyterZ~$3`XrSc#XcfmOr*^louaypH8D(x~QncbRtEw5nF(;a6#dRG*gI4>{&TU z(6PjqTh>Um@EVkKf?ctesgm`&9f=3)S-OTf^~hkTkMM?g>stQ#C8Hc!e2se=jWS%g zs8J>egM^4^t(MInoZgcuAp5{;5AiF7V{G```O2+L{`_xsTM#-&=KAxw~ zNvBjEsXF3#mU@Q->49eHeIlc#QPgK?d|Ha#r-`~EGOZ#tt!o9^=IMDVpsXPxdbeq$ zpca?a$#e`?kkGg^X{951$A;u_hbb1sXxS=96G{T1Q=K+VDN<8|aQSQd#Y>kgysudDv_kb^jJ_nk1YXDFOirh zusW6XCt8|81p>iGwlOjuCz&~pKevFNK#iBjq6v+sLHC~hCEux;CM%k z;&r@`2^QpA9x#4p*(iq=KIlE2*7AjAy&)pt?y=5S)5M?&V)^R^3AfJ7dV{G>aKyIj z8eG?=Jv&k&6j1KDQ=uoFaHvh=ST_-s#eT-=Shud_pI$b~PvaLq=)zw7(tM<-M7l*I zTWwXv5L2bbE$FPIUs!iKtRzX%42DuL1P5ydE7ly^wA)THOJ`xbQh-%uWK;&FTSxh) z-1z@LT-hJ*T;BMFz#l&KFZ|!M-~vbBz17FmSuE3cB?5km9xeY~w~#DgR+jUuWC4jY z3mp6S!ge13_7jCQZ|~zv;n~Roe0)4TyR7Q$dUj^YlaMH2@kwc>=GH_3?&tbDi&Bit zz|M_QoP|p*3COz5y9#`k;cg7D@4ziM6b&Q5k~2#vw^M?JWG)xdR7M^gD+~-E zxY%#~3(TGk^O?*(vtaU*&1@eZ%+F@FkD6s?F?)6c@+9$k@!5Ql*K^e%t`faxwyE4R z|L3itlCyaECCr>b-YMgdI6T|r&tzN}WiJc;Vj3z;)H;^2B&>*O#VFU4n}%IraV{eZ zTpv5`s&^hL+06n^SGlQ#I|3h95UiP)l=*C^g@xkB*~`eWG&Hedo#du=htKdKOJI|T z#_3!SG-BBaZ72|Bko<7kWfN1=1hJE}g?cJl4$rziV_fF3&YW>C3iv5~=ld+ZE#u(R zal!ZbxviQ_zwfj3>t>vfYr?Y_H!B(V)G+Q=az)Q1*Bx^ybo;R5T2?6JBBfD1G>uQf zmI{dj?id7?kepC}nhqO^qY+B!@q}aE@##es%bEHy#j<9()fe%uX-6UiR1qs`3}5o~NRQFp{lCWVxt$WR0y=PFLFU7rYcG1ZG4V;;E#x(>MX z-i^oP8JFJw+`z@&BYQo)pnsKI^7~-BkUit{?fO&0tozO|1e|Pxj?kA{^HqH#{(qKp z>PbA*U^sN!tX4?Xj%upSBcmqMG5Sf(wpt0Ym#&2|xx|EN2(B@`(AWS@CU;E4S~Oa* z)d*K*(#=d>zI+_*cO@iPHKifi3n95N&eaN$JPuD&NvdT)ZM|byagfhL zuZ4^rR&R#V{V82dBH>KZQ<@og)^$L{+57)XKn1`{4;}u?;X4k#dhoXWf83w$ePwUF z`}ey;5amDE{+n%U>&si_=6~BXHoml>t^eh^vi8L_dG!mcQs7Sm;>zcN`0alld_1-; zNt7Q)!i#nEd+_n-+yEbreHZH)_Tb|Y-v(T$@6>~jhkYAxp+0L5J|3DI;KQBqqNf1y zZNLRj0qom=3!VaKZh#Lj&Wm*>eDLvO-v(Up6uj2A0T(<4!MOoG+*~hu3SQ&efD4|2 z%f1b`;3;_Z+yEb*zZX3Pukvlc1y8{%eH(DWQ}Bwp0X{%4dJ10d+kgw6f|tz=@Zqa{ zvA)y~K7KSez=!Yj#X8?U`1nhG8{i{x;PNjn4?cdMZv%XM+Vo<;y>kP6c#L246x`$6 zfD4|2m-sf|f~Vlca|3+b^<1pK_JfbV$hQF(JOwZGZNLRj!3*XF`1nM4(Nl1@Zv!rP z3ZC!VfD4_1-Mx3MWL7>ASo@QW|9$Nz*B)9cY}k9{y%+C(X+OLFyw$&0{n%#r(&nX4 z9e(=o!vTKtcej6a`-9uf-Cx{&_|gw-b+)3r)!n0=zuEct;XVNQ<&8@R&)ol`{r_|4 zk)1d1yn62o`)@z^Y~U3uf4BOcReJT_z+WGX4zFxJzV$C#zY%!f+6&e{xBlbn)AjWF zvp3Mq*Kh7_d}eEX>-)B$?XT?p%v!qsI?b7EGWB1BoEE6L<&aCo;X(S-(K(ze1$4g|wuO^lF{T-xf?a??^OswH)l z(m+u;Hq_t`KIKa2Sy3QU)6j^=%BEp_n5jLN7&z@G2ofO_2%-*&G*TO3m@c*tf*Uw}hMAEbU`He#_NS89M#G zT1O2ym(4elT$68@dOj!>0Si}jnRRqTdw9z=GRlksvDCc%YR0VA~j zuufS?JJ$)5&vi5TqA_V=SrkveQOwxGISU!)#MDv`M4x)C*7gAywI$il}NIA!40wr`3j-@i3Cl74ms%TuMcHeKb5K z=>fm_dv2<$be?gVD%2yQ>8PHNnj>qNbK@AuQPj!!j^k0#g}6gE*;hmW!PJy(JU*<(jGi}SU9a2!FE2%3n+ z$Ex8>(Lu0kauZT+=UZ8M1U;+aTjDLgtvVT99|uUr)!{=TePUIm#VeQr67Ni#;iL%V z%_@{5jT{J3M>JV&Q4O?XAY+gg_nB^X%yca;XR~~cWb%zfD9?}TwP|A5wka~`a08^@ zwkN4+L*p45U4OHy!^aHbz9rnehv}jBekiUEJ)EjU%(!TjF$E;tokZCPN)JcreuYeB z$2pyhC}Tb!PnHLPpl^xSxDp_NVW^5`LqwQNG)$Q*!makOHJE^0UrgVSbM{1aSXe8k z#ohZ{39%4PHlTEm<&ve)gl^#k5lT%8qhNZRZ4_0ILayB#u#KQ5ubdp6br`7>vv>n( zhOFVZIkszcIj0%elq-!=gfbd6gIbJ_*FwbF2|s*DywSDFhlHCw*3T+A;fIfP^?+*? z#gkD5rE~d7E}4S_qcZ7BRAib5{1qMNVAOQf=Zsr38>}>D#o-xVwp&E0PQ{K z>hK})4&M@Q2X82MG=n})wc26t&UE;AWzpT9`*`?yzANFQAKaz*o%O@Vi^TqKxoJp{#h~6JC!J12;c_2538uqhebQ{;X1)WbgUuGgCmXGFxSb^{!%%l<3fGQ- zH7`AowjTFv&CLDsNYdqylMQ9f)@0gF(j`zIWn7aw8U=}s>@bbmy+)<0>jrqtG_SoD z=y(Ir@k5?bzpV>JyFpQ`O)HX_wgo1aX^lO%0VTghaMEMr;}Bc{A^?KMDC z2x$62&zf#ch~Xz&Bv?9^fwI}s`2o+$&R;r*`C^$SXpw;23*KDO`IAux3WzM?G6Jr>~Fvk><4*106H^Uh}Nu4Ep-WCn;Zh z5zsiJ?<1Zy-a=m=nIx5KF9e!q^!+Z+nw~&kAK4_q(s=<;Hly$RJu5q(zKeN6{5Xn= z*Y2KK1G@e`&(@sK*GDc%(CO!&box6zC#pNQ)2C8C9{F%i1?D{O#GLQAAahPVJo|9e z6s|oNSTm#V!=9~~)7OV5qIvB(K*x-}@Aa(X4Ep+Typyjz8)%%-_o`=&x6s#zf0=Ua zE}&^f-}iXd^aT3)aHIlD=UG76jK1&otnB=ieCpxZhgXw$?U^%cK-a(Bvo$C5_2G~O zI{hstoqpGH#~e>d^iDlI`|zX!<~-xXoOfQ3Ij0_;eRvh^uD)pHO)H;U3H;{Tx39fv zEwr|``X_-O4ZI`327-Z&mCtQ+;68qP>yNg6e(O-XX?0#nVqq`69zI8Xg`;470?tFabLp!~l%+5XAU)lcc?T>7K+vb-ye|_`u&BmOhL(0Xq@vwqLoKdk-k+K;b(_o}f7Q4uQLFckF^uAgvPz}9Ch8=6#Bd=!$NrZ*w)+c@?f%?jyFc^T?oT|n`(w8qI&GSz zlsqVv$C@D*P;#NaYYDdJdVm0kL~XF z*zWZn+kLCYcCUN+8Wp&AJnFV{7~N;}cG2KXNzoT(ZR)Mr#9OnmYusSOF;vE5@H+db;B-8GNx9`V@jyF9jgzsGj(^Vse?-FDr+Qg%2E6n<jE#KOM26ZF{bH6Ght_So*#9^1XjW4l*+Z1)O} z?OyJ-V}~ZVULF`|*2-mB45t>xZ|(UW+da=?yXSgr_Z*Myp6#*SU2Z#&v^&d>Sl@|KDA$V-}R*&r(9^1XeW4i}DwtKVNj_eegZ3+}-&YI0~YdlpJ`WtxAW4m{F zZ1;AL?cV0Go#V0H6ubnz(#wmGi?{Su>ttOmZFjP+mbQDAN4?LS<^R9DDy^*jWhTzbo;gTr4s)DG`H_zwq@gIDbT$Nl&0hxh(u@A@9P`{msq+AZ$<^UhD~ zylH25`xmx5+t1zl)ve*y5vUFLu1$F3k2bDtTv`9C^&eQztxBtR1wIkz2ku?@?Ujda zGg^T!+~yD8{Pq7dBT!y{fVy1)huGrBw0ozh!->UwrZBWpC9xz;)Wu!o;qw5I#m`~O zZ}tMhxqyo|j2-~rdM@DN?cMUri$LRCz{UGF4}fnu7jW@b#slC3=K;n}O^Th@I!u?5 zld6i224Y34Eza_L&jozqq#%iVHFS~_ND5?8x6`Q_&C2~nz^ms1E@tiZ0QjDB0Z&az zt&^8p#c?^EPBXS8S%R@>;k(ZRj4l@8xObWtMT}EbLKqOhVU5%6MZj-A7x0PH17nzJ z@{<-Ztddebml<1GVzCr!;9cheo|@(5fbTpP@Dx+BWl$}ip_2l}jaf!k>{FY=x19@k zYEqU1K6EbNDW+8TVirxeDjiZESL&%|fAOI&@ZfoXktfbFn3RGwu$y?kPEsj_FJ}5y zWpS3@aW3HE?Vbm~x1S66#AO3P_tu7rU|D2ogsC#YSgqk;(ZaW#3;0AX1HdMgEP)6r zt=pyQZMNBw7t7WLoO1!6ILlxRr)|ZNbpz*GqJ_)JK67ewn4SxGYL=G+PR;`iFW#Ho zE0yg+sa!6}B@rVF#d@#1IELeM0Z&cJa=_8KfTy_3r&$$bp>Fj=%1mpOZf)_xATT@^ z@YF1mbxm|geya8A$&or_Yp1$wp9^?ub65`8KNs+c%Lc4NXNX&pX_mU}n#T7*Z6$PZ zQmk_UPc56}fW7koLr>fsJXvU-3wUaCSZ-nWT)7h%XytI1wtB3D7_~hZnA$jodLH^(w`@gk+xL?};z~1lfea{}d`@!Ac-+TTpxBJ4K zKiIh&d;$2O?f<&{;ca32-mO30`q3?AE4cZk&7auRH=&LHzVY)LZ{K+1#yW@<`1W;t zeQoU%z{4O7{0n|-^^w4Dt{$#d0`Cu$19z?b9#DVl&;3^`I}dEE?`&`H-0r3E)bSZG z9{I~hS2kY$=9P8k%2mph-wE7SesQR7%J1Cw;l$PQ&I9Y~+q0kBBr`u%6-fBZr|0Su zS4&G}7bpIP?3Js7}xZ zhTo9A|7vRKpfB#XH#@ynyZsWfuqRrtG&am0g_d8?yIbecjTDUfdaOo#^|oCYH)ClKWQqD_7%77s?{RZ_3Yi zm)O$IFB)*8^NFj`rLv34Z^}lN$}SGv4cYszhL;Z7;?8ul(`$g;p{4SR|p@Y2pNcHlc+&CP9;T38^=PT=qVgNESFYZ>bmSNLa?|kn$lteg|_ zU*yY8{qvE3$x`LF-=<%@RCaOnZWx}p`l6+>ivxE<_R7_}m(3cy$hBKK{p`d0ufAZ} ztepy3xe2gVz4YpS9;LQ+}!gHP^Gf_mL}CpS=`dk?1!8RyXtD#Tg9!bC&jCasF@gz^(sxb!GqM+uyhG zjKI}fU&O!p|KF$)c<<(8-EE0_=i2**fg>hEQr!%m!Qml=z)JK75#iC9ZNV(I(L@_870)S^0`O4aIFQl~TJm2fI z!ystHt|CLL&Rs4<2FLAcGu|HH0!Nf0QBZ(nwyG4lE7-%nGyvwm?LOTU1-)Yq?AL*+ zCD8m=QVWShz`iUrN-fl!b+X>Zgihbd+tUK8N32OPVuWIs4Zhvv5gS(}fmb8hu7dV> z#3@BV3d^Q5x(sF7j44Z!78_n>*)AxP1hFxAB&M8j$?ErT>b`f(>=|}v7J>RQGi$&k zH|&{Va3lsnVI%_2x;`sRwE39vH75F4O8>EPMq7^moYYlX0Rb-asXVl zGOxss^G$FL3@(3Xz|AvgR%r6I zPwttx_;Bm%Ow35SZr(FC`}z~BhQSb+(u?C_Bo@DIvDp$i(2asTsb#w&ueS@3*rISw z1i^W=_HjtC1t%9zSs>L^>&~l$G4YI+g36;-p+Ll9C5keu(Q?`%I5N(mNSVs@;VNPB z`SzG}iK1C2ZF985gUMX*bG9 z`&dqQM8z|i7k(<3=eOG4e8ov4sXKqNc+-7;DsG>B20n>pq?Zy;nkm1tGSbl*i_a}1 zoj8-A16#1Ah9=D)cyDv_3ZG^8oj&h|qtTfqXFl&HD*0@t&(l^n*H72G8ipLlBF)&O zWhI?HbbOhDCK;+wvE>1K$IrXtT%k2WDvptyAVwFfCJeUOb0C$aj}*)3EBGvh!_IO(kr=dA01*}Gc_kpKVJ zR<^bRf4lk}tF_gaUHXAbYT#D_*Dl4kL)(F^-@3Fp%l!{x{I4ADZ@yvkVB-%CJ`3Xf zM+eD+XKp-e{g2oG%`D^p{@YjoYW3gl=l1X3`-{Dg?Y(D@Uid`)7sBo`qbLDgRFw@+vQh&ZT%g)uh{v!olgWF2wYyH)?TunTYvrrvHAO(|J$au z^@Bd&yyvMBGqiw+l}0-$1)Bub9LCa3sTGc=(-mgYA_vnkHp~@Ld8o^w&wjltQ7Cm| zw3TdBsh~J$HQQ##9C9|CfI0&-Rj<~B5!O+}5pN9+9&#o4l46lLiyBo*{c0@ODDWk7 zTFyx}gV5So2?|MTtR~e)4my0XZ;2PpCDPMOt*tYoer?jk%gIzltoO9)6bYwj124cZ zYO3{IvW^#+@Y?%)OMIs*G1RML(!eyizzxV~I>Tiws8M&yBV38J+FCALtXHaREW{0V zzsr>v71HXUB_+^`Nnl70PcSuxgey*KfVO*P9Me(VGD2+K8Gp-HeM|h4ZwWWyEGT!G zjOK?}vBH=tkx$p-c}X-ehwd0zv;q+=T8-wB@id39m)v>@ek9xk*_nDWYDI0MQgOz5 zET~o{X|t_O5>lkx6cMda3de`_K8Fu_{Ijmi`;qanN?zhhP`Ef9+14=O=yly{Senj* z-Ph)6N0LiZc#MW8GGdJSh49@KSE6L&R3?M83nC{OTECs>XDU3F#ita<7jsV7)Nr1X zDn*jN^uw-1o*xMH8lg>KY8-Cnn@Vb=Ih|mQmGlO|IUOcWcPbf85VT9~eE8|PoAlX_ zgq!r)4{Pst`+et9mD z_OU0ve_Q$nB>?s_R)66318^E-)R%PuYdOgTn(G>|aed$@xgdi^S)!kWg=)DCjzaP7 zb>9+ez9m+79$Wbyp9lIkp!vKXnL@SHSLH;l4z;UFc^IjyMI(f$aVnwZ`q7D9(S!Ub zHh?nd^Zwko#20)^{2A!@og@>_u<`z&fZ8F+g3=RlScAtkrUq5=rjgB;x)GrQO|woz z9Wn8(pLMPBA@MW5CESWI8OlQ7(IhHo?4)Hy8yv6M63XKkQsm862%So>k*Pp^(JAbn zu-1pfXI!g%Nc^Xh`SAYK=O&QP_;^lyhpPhwd-tSabC@JTW-gb&`ju{oP&*yCHlW3{ zF%e45vS4r>3EFbYiDZ08xW!uhNc;}8CIyZ%nMgQMVPnC%m}?C|H7Q3TV@s9iN|P2o5F3T*flaJQZeJ~G5{qRr@HhvkAIUTNgjm@^#4cofU>23yD=LMo{i z^8Bh>xXX`(o1xy1#Pi(g!(bsFp(1iqs`N`RmBQ*1qD(Pt5DkmNDW4xU*qA=dMNoe2 zRlX%&=}N>5y$~}D%XZ>LJI;(Janb5bo7o;uF@+X~K~bk5r<_in-y?iWBz;S`Ra7!? zPqeVOQSXliDon=_9z2mI21b#n(^fkSH>f6Cm!jR6BnMbmhYtzHw*>7T*rJRN<9D6ZKJp>)D{eo0yh>2B`~Tp|&#qkh zu}hW1e>(h;!@|MeAN z%+@=%UJWAt-@f^>jZbZiH||;g59|H)7p#49%~*TR>c3m-A`_L=DIM4eVGSXq5z5?i3~9rg~%zIPE@feI~AZ3k}k+_Aw;G!y-_S} zOIjt&Tz~5v_Q(UQmBGuniXfg8*|u~ zE)PdPiqO!MmW>wNPfGZ(J_--I6JkW@Y+^{oj3#(}bwIhdOtj36nd@(v!`|lsmL(|J zG!p|B%9uzRMu3k39SxD?bi~MyrD?X;DzT6?1y!On=K2G3*n2&|j>m~iTN3IM#C9sE zg0n?D)zzl)2A=OK`GU~MIW>l6B5jmluD^K>dyf}b8I)+!#I8dZQ`uC8DHeFD55DCM zLb?%!E3FRKLk&d9%S4>H{-!zXOFY2(NSx1Hug_s$>;cwC0(|B=KZkvh2Us6D@0sh| z9QK7CV7t{MTUPZfTPG~m8AwLBisA#!R41KLX2K+5sz|o$_&BVCv8~NvU*G}ON4|XK zdUXzaw+C1s$?=)%>>T#_9$R(kT21Oru1o41$y}%Bu+Q}X>mz|ZbG+LRDhIPEi4iMxb1?pMo^A#iSD?lpH1M zN+L=pMm4fnV6Ka`KjJ$10S}G7MO^ zr8(@gJirdvjv14sx;`ByOO7GQc`_w~@^|5oK#y`GK!X~BX5myk|b3H$YeTD~EANl#2>$y4XB@eKAyr~Oq(c}e9Ekd1Qa6GYyXuMot z9A`X{q?Cxn8&1C@O~C@o&S4Kd!1~DT&s-JseI)W{u4m@3`yOC@a*zv!OSyJfk!AhKaxqnvvdndK4!hw2Rtq;1;PZvl(|d4J1=Y0UR@`W+6W$)yiEc5YCQ)2! zpiC+T#`cYK*mV!Exl|HOkYctI4-x6C9*@V{xD8kJ6jd=IAR!KebGhL_6>8` zH4m^p$|o?_ugqarJ;3@XgTP$Be-0b)0PCaj0dxKJbJ!IRu&HE4H1vwlu#~K*WI_=x zl;xeaP)i%5TBkEnFsG+POmnP*cZYAC!EXN#53oL}CNS4uH;4Tf53oK;Brw+#bJ&0O z0PCX~0&_h+hyAJtSRYjlnCr1Q>_2&c^-;!vxgMRv{^N3|nj^78jiYL|)#|kJ0w1Q6 z4M~ivm7HkSX`=usHUX&(aAQpk3D^Js@s<72&Ub8H3Os)CFYDh#{CY;}~g*)GV1*)d7Eu%>H`vt0(k(4tMi!rwil&VW9A43sLBrT?M%9Hw0Y~X2#!& z?fQ4ke>t(7gerDWZ1c|NBx)5NExcH&VJI#h!I%J^(GLXLrCq zX0E!Y{W_oEHZ~jgJ9Gy_;5c;K@P}qo!Xu*6>y+w4DKxb6qEmxQO=KJWbraGA?eEuU6D#i&p|7SnN8vtpKBkSTTUXc z+YR*fZZ{9ySR!8|2yn#ovtP{s7Pb$!1kzKqak$Fw%n>s-v-w;{%(LT%p1>&2`_O*&{O-?Gb;qO+AjNQOi)!Y9E}2OxJ6c+`(( zE~CfoVx^>YQ?**`I2Id4`fNTtE}Lb;X+;gZVo@qTz>*0WPDq$~nJUU4TDj&%V zitP-A_P8P(Q_wJ!)^RwQ!aRjLATa1UVBUL_$9gm7eMckUXSrS!^N>X1H1p2j(i!Y< zOL9F$?3kbABXB$_=Im6tFFdnjZjtHlxSp+?8^_En^5#iAW}tB7wy`}rQx4-2TI**r zJsP6wx%_2AB$}y$%+{z7%%fGR%U7$#<1By2i@l!4E+d33SZadnI3=OzsDzv=<)uC! z)yihoC?*IN#O=w`Q4Qj|oRXW8(P9qi)yhJpgttf{ZlPsyoSj0GB-`py$3`XTZL!a~ zK8q*Ve9Zh>l%0>amF4d~3+K#Yyz6{apDXRG9F-5qHVXSJf?$+#A&PNxl0&pVbZ<0cZ4k`B#5 z2Ak@(oLVu2#pmt6?s!)pvBGCZ7(A8nu8UE2^I+s9QFdoP0p8&Q9~=uOqF~<6Ja#&U z&?&YOF-nb=6BA3t|D^ZAa6JaNBs&^jQlZuo|{dAk`mBxZJTj%O1HT?f0pX|k=b=Z_8xBN2gp+{4$1wK&y)T#Dj0pVKUV6Fu|P>+0E7J9J&buI9L>Zf?P7Yp9# zTHxch(oel`FBZJPwZO;4=8=!vN-q{%aV_wHdgS9`(~AZ7yB7FBJ@Rn}>cxWByB7FB zJ@Ro~>cxU@buI9LdgS9~)r$qMb1m?J>Zbs@7Yh=u1wO8Jk9^$Mda)qxTHxcl_Q=N_ z?D99@haQc&7WhCt@^M}3#e(R`EO@^JKJsz1>&1eIYk?2cBOjN*UMvW^7WhCt@^N46 z#e$G)fe+LpA6Ls>EI?cfe4rlrxP|s&0qk1f1NF$qg|!z8AlCvPs7F5Tw!K(z>{{Rh z^~lFHxEBjv>ssIg^~lGKxfct9t_40&FZKfRp+{fiTHxba^J4EL9(we$Yk`lOj*I;^ z`_QAWb}jJn4eVm`!|fhkTFI~cPT=5k2R|P8-h=5u=HR*ee|Fg4j_v>S{yX=}`!5DD z0Kd5R@Zo>ltL`0bE4zQQ`|;gJ0{^`G=G|BCd}ZfXcRsk&+<~_LW&1a`zkhpk>(hb% zd+Wnny{#)-`p!>t?sakIi&nW|#dE|s-Xtr-F16M=oChv-YgW<9wfkJMmI}9wMqZ0$%yuO> zMUfVhbjs~m-U*^AOvJ{Tt3w@-lURl#P^SndCRJF#f>yP{hXBZ=teiMLMT>3_*%W(kAc`%8AoNZ$YEnM+bEN3ZsfU=iDWPf#IH+2 zH%4|4L7kMcid4e-jZlIyMloI1c`l;2mGBzoT2)BMjS#_eJcYIs2_%M)5zCB9!L&Lo z%4#ji+O`78sag(NbEA+oy227AJ=KvK5saAXScp$VYExac@LDfYigu$$w4`$bEkv%l zSrfWIqFjoYT%)0KRs}2Z6w(ufUZmG*la*k_v=fD7nHCIGTtntob@Oz)8Y%-%ByK=U zWn!x2v*94Z7C6qS<=9CIL1aRW$pegBgIx)7I+%!NM$ho^n$n}W1~zC>auF_&?O;rx zv{1b-MfwSOD6R!uiA)X0C%ML8niaF7no=<05e<(;N9d%UfxDv$lI~?{H3aKG0XI;l zi(2`tmI)IiWM$xNsfEFnpsWc~A(x01M_Du+Hw;sRErnnCD_2JX< z6LSPQgSL|tf*PSQ$19XhbSM$l0+(G0C_JR%Oi)ZpLbM`>I{oA%lw;vK_;?=gXmreU z&=%AOpZm#`N9Pj8xYpr?dat4a|M_~#9vHoDJeO3)_F!5K@ncD4dk}*b^CM{GBlD$e zjHrAauSd{eVH{$T!Az@c*lB9e;qX+cl&OjuAVVnwMGdj(3Jln31Hp;(pwUfrhZ0^X zR>x5~2fPpUF>OrbX|xiPM6J`1zs0plrl7Eb4K!Hm=cEEYM9EIhsv{klMt0n48(TZPI z6GE8^(QxlFYg3efiKQW@7gk^E>R^Xfqc3A9Qs{)KQEsSOA*m(fYL=*L{YWMR6e@8K^U=NBg08E0`{Y(xbLjl2X%HzSSW^B1b}KnxF!XD?#;`MpeNY z*a*~##zdw80$w$w&qNWm1v~L*HB#jJ(gb9nTyeuujdGmKI8v-kX_H7O)v-D>2$5wo zG}wJuHp-KUX~oRjV_RecKjG>qwYfS}Xs7aVj*quQD9vGsddLKR>xqVK4A3%I(zr1u zY;ombS0ZoM+SzG6>9nTmpjM!Q(?(uK@xp{36o?+CX|P(V6v%Qt9I)Iq5-ETTiSb0S zZ0Cx7j%cxvozUPVt<2ys zy86)ED7~!{JepDRM7S9p^{Ps?!E3Zi#s$4=sl$}O$BPzJv6)8L-Kz9f6tyHZ*G_Q3 zEZ)f=aJFBmDoRoe(HS|eR+Sj5X)Qacm;pBcSr?#qC5jlGK`0IzeY1$^t$s|eB*men zrJ7Tw3uQYjVQI9y@;z=h8mV!6O85FqH#`Jc4jY9;wHzErGddp3r)4VCo{l38bkJ7f zD{fe{UQw(xmmwNczCShw@oc!)O zDVqIHpYy@)m^%DC7u zS_5Qisw@bKtA|&9((OiAZ%3?%R;ZyJYXYIcOt_<9h1kRi#UL}{lqI?uA2+7?3gM>o z(Lt!MosO7|3f5{%eJnn7>}n)c&krY&DAI_*Ri&Y}sJ_vc1D|(|3W8HTQt5=%R%jTj zjrl=Nq~dlp8b-!WjF;76lOU@jWYQx7E;+Q3RxJw&-H;e4T@fK}I%!V~2t>h5WQy?89+w2HYXOLL{<#89NfI25xBE5G1M zbPA2^NG+CNecZ(|k@{p3v3m`WQmSlKhh?6u%7sazkCkyK;BM=hScP;dDuhO|q}TX1 z6Lp4BVJdbzBL|L~CZU9mRWvLJPGL3f>JV5GgKG1X7K$6*@Ja8n2+`O7tKaQPTlEX0J-yDW@122i$ZbYTW@L zkl>(~on9ebfJ#&hp?eyIH$Y&yki_%491mKpw$Nh(wyPtpVq!#zHDX1lUrZ#m%s{24 zH1OsMMWs|@Si%{)*G#F^G8ysLe8826sQFx0$xS$>n{P+D<6+wBrTS2tr$SbxGY~4VR#3$OQ>cLGN1+ zv_)krGE@|$jy|PhiHi7wxe8^Z#L5+&%HqK=h^@fGCB?~LT?u!XBFM@T%s9Ee5GqY6 zuylXPl^6&;yTUNtdN9*e$zmi#M1nOK>zY~*>^&j5n`eYlmTpp7zrKJZ9JMw`*N{F& z*CL&8r=g$>US-S49Q2?okeg;IPF+mcu`1n<_B%P!&IiZk`Z(Pq8)>|3kv%p@mr63_ z2Er>O!Zqm%77c=Mh;*gd&{7RP0Yxo!Duf%kX+6TSM6^#59JTTrZoi}Tge2rkBZ*1N zXcFqngC3Hp2oy7RP%9*Z zJ#)lnt<;3e8R4iAUil^Px+VI4$K4+VxftQ87--0gW;=@mJ%ltk)bkP>(6zl zc?XMR@Too+jZ@*SQK&G ztLQp5mOw*eU5p1S)u0RosZOEYaj15+U1X*$h7XpATGyOtygT9DVQ5r}XXD{uyP0ds z`930y;s#Nm2qw%lN2N(A(ki9W5C`$AAD;XFuc9ma4{!gEjh_bp^Ua@cj)0#LxY~Ve zH1`-*0vG$8c)^qFwgC!fCS~^VO>yps<;OvI=IG_)i{aS;4L&|SpXInU_w@&cJOzOa zCt(Knq4&&`-!uQ`t5tJSvR@sKrBriryX2Ssa=wcOLITQ#6$u#M)_7wn&J)oJgfuzCgmt=yItu z>D1w<1mie%+#g^g+Adm6yaZMCPOYUS^JCJ?w>e#jGF2K*HpkNtTxD|IrnmIR^RCYv zkvAT*XIy&!a|0K9!{?Ptp|jkMpY~kxacOuKm(B=1duq&Glqjgx&5 z9SS-~{0%_vB>bg1<;N<>fE2D}AeljNITtcPy27v)O4M6@glp+z6E5d$DQ|}>VkhPt z3-WQAEn3{=Y@ROWq=Ya50c%Di>?)#bnv)G;9u&FEJ6L=Q}W<80# zLn5)+bPl}5H^(p<3y=BsWh^I6z?bF;b2%SpIX-i_HaH$R8Q0`H;hh1( zYa~e&Y^GJv`(!hdgfAOIu_>D>j?oc?;h9wY*zQ#-*`^bl7E<^`vf@yV7b-2w=oBS6 ztyD3&3c=lWcoMOC(PZ-@b^XNuKR5IL|BXu@y41P^fhd4qKm6d~TMu6YVgWvR z@aVyt4_*l(0zSU~{(Wx$2*d^a(%!>+%-%g9I^gGb-@RMjeIbYu_?ew=+sW@d4@3%l zbo=ewneDqkyueRvO}3I-&j3*aAKA1w-?+ICVh4U?qqp(;jcpJ?@c6pE9$#MvaRfiG z)>#X$t$=8P53ROVp}X9BMsjE&lZqf?yG(%eyw_hnli{#v&0^wChg9ZO;$>)_N3 zw?o|!6t4gO?0pBE8`aVH>Fy+*t^zj2RJTCjIN$DS)gS?+Rqu6G2oWkPt+bL>Wmj5> zj_Jisc9th=&bj6lc z!_Bv`7uAs%F0pz!X?ZY`7SEZ5(0AMUv47T($sVGFG>NK0H$K=9;Y&Q2iY#`WD z1ZXTnk zUd!R6Qx1nZ7SV*2kz7MpjCTF}+Pm!9YUE>egLG z6%&Sf3euZG5YP%XR2luQ<|MsJX>{YtjUTVvuN3j z$^cZ8e;aHc#^K}wMoS`+rm}%zg~+F)`66LzmFsM#-<(@xYtU4Raxq4{mWycR-vZl* zayao+OB3*$LCgd|Asg#eMq;h9Cqk8Ik|xU(U~45~b`6Br(1!e*VEYgb2lVT$S~$a6 zGTj-%?qppHPBs7oFzfY9A?A0~?3AxUd!h~!A>}uM?SnZSUEIo$Y{twe0OyNH%~@np zsSx{CU9U!5VS+5Wm$JQ(&`8T0k$gU z32Lh4ywMwWHgiyl!#R+{$uLet50Vx`DxW5s+K`)#uCY_md^u@%*FjRNVSO6q2-L`z zeDWK>_KHqB>S*4U)36a4ZYq&TJR2fn@q*IfyNfXhv7-vTB%=l14-5tT)z8dP);kc_NzaDJw&*3l{BBb#Ja*?Px ze&1Y zO(v&IP%eB1$3r#wbzpm64hOTi5=krB!ZFs!Ur1!V%{=?E)9S59e4KJ!lc%D#e~b+!PYF03cRKApeit)z5CPu%H_ z#%aJM{|eZiu*^srla1~?oMuPWtV05_h|=}(NBjV+v{0ea6X&jD-7mV`Hfg4Zda74UM zl()f+r*JqTUKDaTEe=P-8$b@H$>E52wa4Kw9FB;0b{tNF!x8Z^PQDS`Sm$s=yk(PL z4sN7591*Y2X8iHfg4L4 zj)>Py98R&*AK~wjIGh59BjN=SheL2UBHsLPIC&07#48;RC&%H4cvr*W;2e&KmoV}R z!HroCN5tC|`32y{42L7ag=>J3+>h=2tEc7Vw=?yA}{?wwDZmCR~Dv)fXn zY_m`cHLwOWMAw*(si(obE)O!6j0Y&O@rp$VatEv>P($EaySf_jo7ByO+fCyjdx7=k zj8xuVuVoWXK#e)fh%gH#*LBW?C)?QEs)-_I(=KbGic^@rn%Fm&kQh@vOIXZ9vySHO zvL=eyJ9j@#oUe4O413iR+|IYKv{k|(jZ(Q_En!V8kPovq)o?Nr&e~J8dPW;Fr_2tA z-s?2x+)XlTDy!{DQ>9efX&;^i3kOkO*5;{4bRKmkkqS+lpqQ$G5blQ3WNVe^AOZU9 zwJ7EGx|no8XO++^`oDc6( zokYw*Otd0$K@ffQPV;g2@I)N9`tN{Z6}QKS_mL*2)Ge|B?n-h>9k*=lFgfKeR0;3L z_|ERJY8AX-mN39lP5CLCKdPq6=7P#%tmEETszEyKYRYHTYS^m0m6~QYnUC$fm#|<+ zc@18h2P zCY@$w@)3CE7dzVSie`6`8}3lExn!@4`1Kb`JKK-(oz-lW23#=htf%ga#UR9>%@_@l zj3H*B>uN`gE)n6`V8BR(0F0~!3=q&rlRLdp{s|m;)O=Qe^OCpo8MSY0P?_&T)g{$abA+14aL|EJn z*LB|if55=$15=-wymxY+iQ@QE<7>uFAN|>=Rd`5s1VEKILF@K z8lBIxMKHO%hsiN9o)NK3j&bb$CExk1iePeC50j%}Trpyq9Oc-1_ixC95wf6W+HYxkeMLjkQZpIJoo7x5qg? zw>xKu@Kf@9&Mo`0xJ0>g#^#@VmizqaJ%Tx1*&gFKowFK=@{aPH^5=|2rOBN$Hvi}+ z+~=z&3g+?9_Gs@pLquvzo<;tgu_%iiZ<{~hK41E*U=CNc6`U03oHM4x_pxX)+aU4Tij_kwlSs`f|^lapec&0?9HHeS~B3yWbbVq^yU@aWU?6u}pSOY>z#_G5Oe&BAAr+FgYg1e=nBF&WY{Oy*VZy zT_J+W;T|SO#RLq9WwLW(dx*P>ZGL!AFq13WLmZQH)<}gI!}wMvJ14e9mAi9dd%(}} z_`qWe@z`q@?fU;;W$XWqye&;iUR{!F{T?D8y98LT=;>R%q9US+cFJTsCq4lN7V?UM zAInom5!UMd-Wd^5RsC(aeJiXis>~fJE?RW3cdO>G=Ef%o@`|dr_lov^>bR2@Rwcj| zmSUOe`@)LNXlB-f1cRs})@UZoFtdaMa%AfjrO6AJz@pXwxC4=jE#h(Qw1t&Q#he*L z&pt4Y1}SX_F+`#clcktT)f!06kO{Ie@i;*z8DmUoEmk6-XknIskQ#0>s0vxeV#_o6 zC}9uiN?J=a386{7$(|ABJLz=28=r96R!tN!n|4(bl^y?DKW_^7vAlKMWla>3k$v~m z#NJSZPVmTl^_=_GAMF45aVIqq(rL6D!M>WP(P-RAu9a`ld8nYZLP=wu4j4TSvJ@;~ zMNPxj^yJg9mj=T-U+XA@CoP^*IUMkzF(2Yfc+Ei4QJ`8$og-hdK{ky!f!o-$av?ei z(dn#SYXEbF24tIGl>#~`n2nthy`qBV#$ zYN&(_?x>+^bD;?jlsBogRd;rppka#}+YFWn1lQTpm_wthdumOW$4l!VmCL}`>slb0D3=Q+3!A{#Qpy;U24Q7p zxvqCL@szEaC}KA4vL<#4qW0CqzJ+IZdrcG(dA$2+Vz1H(ssGo%UhU5AiCP$5u;6nm zW%iO*51U~!3dRa{oXFdBA@)UQFdathFq5pfXPY|AW)E0){&Z^18luiTs?ixCtpjPe zizT%>pejN}i>gQ&nu(mwi!%)~&eUzFxyFRq4A)?>ZZT5!qAJ9uRP{zQSg6T9hh)=S z;}n}5B%h{)M$fWc=l%bq1KNSf!uYFWM#XI-2g@!P{`1hul4sd3ckpNZUhCJ6oODiV z)nO*-0=5RI_jckk>X|6S)T5A@4NcO~HcRKl$OG0k;OoG=_N}t+e%r;#5P8(3C4Xa<3vV^kU zfB}y}?m|lG;@WDu!kxBQ+Nyjk!192w$+4AX(1pf=7@HH=4d9UgKy+YZB^DM^;b|+{ zj0F%qZgC;D6lsIhWR;;Hw@rsOQcy)1fQ?31Kx0ibW_&=}P;E5Gh{@7)Lr1B5i| z0QN8F#j1ss(<|K=b<8pPTCxZcWa5p z#7Ne{5x9~JQZ^5p5h743G{WZY_-#KXdK0EYHJWJHmdnmW)l9YkWYbMOYzi219rYVi;ao8$91$@jY)oq#>9e0y`hu_g<>_o z1+96)W;Q*7zMr0e`AQ};Gox|Xg8o)qowOq@vQa7}K?iJe)n>{Rkuwss&Vo^T(qb`r zV;-B`Zx=i!huqAL$*RT1M72AQNpPvgq!39WAP}~?Nq@2I@}lWXFyiYoCi+&epb1pz zddtFYSa3R`uA^k4-c(aGMrtdGT+!hVrDJZ~9?_G*2x@SIk#N){cuWrbDLW=B7aNn_ zsI1*ePnK#wF@gSQ!47ypGrLQq zgHcA`@`ezqX!fbynlx)hD`&}AFc$Mit|R4KD0ECVzfG`Xa_C}X(i`!zTaC$5tzAZRCIyidcgyWfhFwg;WCJ5m zzqQL?L=$$}T&S@YU46P`PE*A`j0xE53*C*z zguSyY)gF%$9;=}cFsbka;U-eKTFoEqcRw-Kl*tqtF6G%WKfz!&*2tLfM3_>~Xz+s9 zs!t~AWYto0m@+P9mJG9bpFD^s7_$f-llvcF$K;^J#$-3XGcrqdXSBM59+SW7DaAZK z0yPE#V9Ze<`<%OsrWwlUEwROlYGi_l*X&ho1)O0|U{oD+CBe8iVJ*4QY=ibi8f3^8 zGsVn)G#nE=CigTr|Nq!Pc3|@J6Gx3bJQ^JNo&0p!vf&$tJ}r5X{d%eXoSj{-9T_}n z)j4c+3CW`i7z~uXhl!ZS8}J7VQNMdPQq9)V9V={5GLp_aiBK_7N(bE#l}H%-T~iII zc)J_98lAh9sF!LLw5Dt6k{ge-*i4%_7{XAqIU3CN(KrKJ|07f+!$dLcK)rRVGw4sN zni-!{ZHvq(<47rzgqpsDD-r-3oT$}XMg8zXqVC=LO?IElVv3j%WVaIaQXK*i#+xp} zRn+3ruqpR5!Jw&$S^8}_NVQSZXP63Hv(C%_v3w$sgY4FztDw(EayezGf*3RL$V|YT z33NoQ+A8XY77#VmyQ6y|&RERpGP9|5b|>58Qq5|!&KW4_0W4Ijgu{3MwU$}uYN+3= zcIX`@YrswDqJ~;F+iGd*Rux;$FcPo9L2cews??Yq;0k2x8Xq^S;jN;6a3N9mZq(Z> zmb;axmuyylBxYV#)XJ@*{?0<8?zP!&GequAqTbD|x2&k4t)hNlAyN05Vz=2BcPmjZ zm0{zfG0L){2Dgg(zZMd8uQhL*iE6hJ^-?V$AB|m)6*aI`)b}qW>Rw~^HVf@;CF-Tx z7A+r%S(_F0>{e0Vw~(lnJ$?gcG!zR2Bi@*QcM`R0sXT2~vg!e{ny9kO=F3*$LC_ve z8vC6VCP}*ER?@VV1O`v6}hU&WdOcF`Ob7+;cx$Hr*8c!nSV&@98ZmX#8T}afu zSD0YTUIdQd^Y_I(yrM5l& z;k7dz^fgHl3sRp55D*=Z!HycPR?8#O(_i6ISq)BuQt8$CNDXSMcSmr4}ki9#;HF+Z1fH`7w*OOD4bKRrx#OOkuGmg5NEfixm_z+i~t?v>j(JE!pn9%ERHbE(AEEP;%Wh|mg zk)<$Fy+lLP|8K^DJ_QDbe~zHgBgE&@sv5YES9_XDEI=ofQ9Yo{ zLY_t%CsR%;Sx9)oP`ZigysixBMH)g%5q7%HYTmiIjc0J0cVV1OV=6p%|I%q*pZdCe zbcy?_M?1QyDn2Id=$wh`?ADqixm{&7+&RHku>?g{x&I5EyxbnPWt-`rEPFep;F{Ul z$qs2%3!Z9c-P27|&T8~C6rHwK^ucPPS*Dv78cviU(pHX8p{&h1TSp?%oj%ph<{53- z>vQ5^HC%BhL#Uw~El;P?Xd)D}=5z(GJ!UDWodm)(AUhpXRTEl&Bw$1WAnR0f6)X|F z%z%J8LJ}>DmGlyYlEo!(o8h|7$Nw)Mpa!ONlhuiDkN;-;kTG=hGR0F0$;dJCn(QX& z8`49E!$X%3K0P=nIhF-nf==SURTNhkUC9r^UYT79*t54Bog#-%AifYnT)hJq%E z1zWH`X$4ZTR;9%NdiDhp?OR|9!M*1-p>V5OG*ZDx(o@QWiD=cAX7oMgJMPxSoxNrt zw*C`)B!l9hmMvHc%)l_fc7sh~x1}3qI8Z3yJ~+vi%OPjdxUrbCLm1$wm<#@L9a32t z6F@6*DDJB*z@D<#dnpudSGu9porSDFV_P@sVYYyllST^-Ct%IGvaT3pMjdvyI$o-o z15Cc*NHaxSu;iz+3$fSh859b)OWn}2-8g+)H$>EeT!_71ubWV~UFwF0?Z#=_x*?*R z<3jB9dbEVX?NT??Y&TBb)(sKWA{Sz>*M%VzZdIM4h$MGQoy@|ROR&k|@ z?ZzqFx*?)~jwi~T&-4Ibvav}D5y&OW}cBvamwj0fD-4Ic(av}D5 zve<(h1;cWfNVD!+qxm5 z2PqK4R#k_U*FaZ5j80n=*GMlB%yGtnn7RD$%ZM#YN@cp=?a8#!IKK& zc5!-JH$;@lT!=k^yZF3Ep1afyZWpJvbwfne%7wZia2JPq;dZGT+%8_5(~Y>=2bOXW z?a<_m3DA(iU3vtu`$52_v$;KNz1ut#(4gjE!L(2}1n%NcFWlnkkr&?nAZ{10ZtI4K zx|R!dL*OnB^}_8^H@IEAvaK5;%1kcQ4S~D(yfosw)D3PIFK_FHh^mhZbwl9x2lc`& zZt2*;q~f+ea$7e<6rxDy9b`1ixPq2CT= z2j3jTB)^fQ23}{;7yfhlCUh9Xd}6BNib)2)xJsSADI}T!N_hENAq)gN42Y=lE2b8% z5C#IG84yueR|o@s(F`o{5coO_h$w%&)I;DE&A<{5fk!k0OFRVb4g(^pNiX#fxI{Cs z#6#c|&A<{5fuqBK2%<|p1a{F3Eb$Q7It++711|MuWbH5@;`An_w)Ns?fzvlxL^H6Y zPO#HAnME@oBDhS51WX+UM4UXAdI*f78Cc>WKtwaJ#6w`{Fd(9${L;oioW4mfnt>%A zf)hkDu*5@fe20Mq?a^2AFW__SySmUsw0)nPzH5bshC!LgzlSmGf#Ml=IUJOsK910skn^$=)9Gq9vXz}^3k z9=K~@>Kjw;$-hiqJ&8>Geq!Tb zTr0wLQo|FFO8xR)^7qbv>9AD%^7gNfzV2Ul&PWHwF>Ifk4u9#W``#SgbQId2?hw|+ ztqf_=p-}+N7m=E?$fQys4eZjZ5m%TX%dExLnhCZ5zpBwC{I@^;aT6Izxz}7dJ@)1O z4-PyuKD6nQnEl9e?N6TesXtq;M%zbq2+!CG0D<@+LZ7cEoL0t&yBukC1c3ls74rl& z)pFkG4Lh4TDAgtW{)N=#YwMSf{11NnA19ZMHopGYPYu(|Z~k%1m5+Zq`KODsX#2+6x#k|mt%D_Z_8=YK-Enpl1@zmFd{~O$zyev zaJ!G)z)cpUQG!e}foPZTKWz2?%?}>^@)+A-q)Y~*lFkd z{cyB>M2D~lhf$X>@y$aj$LXtwJox(!!+$uV_T76Qnfdd+viHKwOGgL)l{n`aw0(Go zFry(t8ebq6iJCLsT+K{7^k~wF)NLuRzYwj&V@$0SHq-!{&(tOS_JCF4uV%n(Do-fgnf3DNEy+_jE88Lie$Q~VA}va0<4?|^;shur$gH@FxgD>3-9ro38 z9cX)PhcISwC6ZRMg=1zW!X~8mHuIq>X7%Cz1QKvWLuQWyfD9gu4d-Rdqz){!l;{4c z@>z7#@t=I@=3l*V?tW)mZ&<$k^}AkO_Q2}>(Ds@R;f&TbQ$)`VNPf}HXAaaXP?&3x<0pFN`b;mvm{&i>~^5B|*GW)37j zIQH~={`4KRy}Co#pelyqIjcWF1?tXd4Ta52qUgh#aZjWbG-hYq88t*F@mNc(?-G_@ zb;Und{m}l>tuq(z``Tw>SIO`A@QwGJ+T&FJyyyP=qR($a+p9W+Mfl&kg!eh_hmjAB z@BHqO_OWZV56s?$`Tx6m)E%dMFL(C=)%Chf1lnHNAuPfp)+PLG;=#89Z>+e|ptH_g?#Zw0&5IFjmMaTXvZ$OC)u#c`-=-1E@rVEF%zM7_!)v3NzyB0% zAKD>o)TJvHCuYbeePv(4js-1j2vS&2hJ8AJ%Uen5ik`UB9gWj~tIP4!kMalHd)B3Q z9cl1?)>%~j<>XIBr+m+TaP?nP4ww4atz@E3(x!*q>WouIbO}Fn*jElc_`YZNy{Pu|ffY1&o?_X<$j0No@#?y>2VVQd zS7v{Vwh!(Q&Q?`Gu~kVH^++?Hq3tRxrl+C|(_oANs+l$#H8!t-P#a85ejcCw5cTC_ zzI^4t9hd&&!Z+SL;qFT+kMH&3y+-6#I{si!0l1`odC>6<5CO|5+C2Tv_;k#7^9 z-FPV4UeO_3u_9h14;g`0F;YroLl$kbq@yB1Ljm_Ym28GtyppL{fpW{qug5o*FB|>z z`pvp8UUTE}gP#4yUtiuVi}+?wyYeRoe&xo~j(qkmw0%H_uqpsF1KDC4tQs+dj@98> zi80kpg^-T+X^KfL9cx)?TC3U{;Z^#L8OW}<=9SxiG5RL?`qO{h=Z~KV{Pgm7G9TW5 z^%HkK`oX79Mcey#2!~8;?if;63jtOVVzWBo2og{$ZC*ALv9p0S?d7y51XP-qP?Mh( zUgOH^E-M7|zkRh>KWAn8t?JZ4S6%$*+ZTR)^A#`NbY9>{w7p-4aHFBKX0SwzfvA>W zpF=`!#9Shp+Bo7Y<(#fW+|ddL13^_r&#%XiPgzcT^QtF_nsMZ8?+fQ%>-^4#tN;6g zI}iNurdM2R|9&dF9`D;FoVA(lM5@#ZxXpI6HRyIleN|(tT2mH^YS5p?BV~ON$~m(p zZFle<8iI9?z`pniJSGOFB=$H?fa48(J9Nzn*L>;^cV3US_vsK8;acbtzE|~yeLlZF^qq%Z`Daj- zI`Uupgr9xlyQ??sef(eF89hQidluT>yF<8y8bjWE!OmuLtAtuLtO%oeq(+c=2CrM0 zpiYxd*)lPt0T*}~U*mo0IGxS6@uhq4>`_}S)FRuK= zJ2N-j`i+<3Z=Z7AagQjZlCwVXeY8E+A*|6yZKa~7#ni0XVra&g0I()Rn6;X01vAY+ zNkhVqx|(UUj_}+6+p>{sOYc4L=xbMb))SUHexg71ozZKT{o%{f>qE*TLf=`5wnsaJ znY_gYpe82N)F%vT72rr#%>Y5BgBG0)FxGXkoK79k)x$P7zvn#xUhqR77+86r{1)oS z|N7pG_CuVc^@U^qH#6IE{PdNTo6)wSL%7fi&JYHDBwM75<{3vq6GcLB86^oAuIaIs zvy!DWeqS~e4D$2%i|{`$-)sM~i5vER{Y!s1^NQC$kAH5%(UP~H`PSGerjg~YP}Nr&YwVPMbMvk2+vwB2ahKS;YuLdTZ8WVEEPTb}Rml{HiZbuRm6I z?U{E-&-}vZuj}paeEZ$kL-!0Ylk&aMw!A}FgoU^}ct2|w9v}Y9jh}mAv-$dW*B!Xu znfni|dHb2s0lW3`e?IfMgQ93#)*&py?%5?wzIj{hjN31GMj`P1h^IK|b z-nn{>NhBu78YOk0l+#A`BH>!k@bEj~||V_>rHzv+~rtCtEMM`HvGP zoOI70E<5)kvi|E+U-q5L-Tx07$_z|hIrYiOCni-B*H4@}v9ZyX;&+O3 zNB%r=(a37~9dfViVc9zA_u2dY^TXMpzYS#uUmmPTUX#=%lLOa&1Q&glY7pC*E$uejGn+*F{*s1oP8KU%t~KEH#VA(h8Z!^8%~6PO}Ux=NS@V z;aMVvdPAf;({RHwmY+53D2Ol&3Fe1OEKuCA58DE#x*`lYf?MdPx_h(jajGlAz9YE3 zxvIMt+YYC?BFsI4+v!o=DYhwYH5Oq265P}dRd-&Xc~^BOIeyr#i?ACB=4Y$wPHgG4 zaQnvMv3-J6w>K=ESKV=*ArW?wC1R*Iz`U!vV=OWVO@2MXHN7!~a z)fEw1Cb*p*)s?eNajGlAY$CX+9jk6{*nd}bWgI_j*F{)c1oN|1b)|f#MOXzEkAxDW zy1l{wyy_0~42iHNED=Nfj>WVNz2I2K{W;<9^)$->fs7ojr!lLd-cMfxQR>~13*D<46kz~1$bYi4euz;5F;JXhwj zu5g;pL8NoFav{;=Gn1|EpVI;YInW(uDdN(lbdoxCaGpt6%WT-{RXgLQqezVjuN zm5j5A5U!FLEEX!^M$^)AJ97~xLruqQRLVXJ+f7F^S z;+$FJ#bqe# zt8sM?&@dFOMt8WV^0{i$VXFZ^^m=oW3H#B4-A39=N&=pS{CQxfQzfZu-dbwbRx=hW znnc~=q@4gr*&4XAZjIHUOdWMKOxXaHaiFbqy@cV-Kt5|q00y>Bu)2(>^{yt7h2UxL ztg`F{9X>UXZ`uXs1>?HTX`Hn^+|js0Bx^-Xqs0$p0yPfQop#FV1Wn=btr;gp=Cx(V zPxM~8%Ni%*yxdRYD6B?t^Y-7badVYR-B-DGcK6bNngw?+%pO$28Ng}vKpBI}?KIRQ zZm^{%OR5H##@%WIq>DPrjNX#kd0i?(R9c^$Zn|4eWY*(CoS-x9q;#0u8peGUtg0uq z9zW%=r(m5;i~E=YT?35`qq8#GN{4*5y2@{|85)2Eh0gFfmQI(n|B<>R_F&tSA8`IP^W)L1)qNOY&#J7)noT|1*&(*%|5NqV zyX}sfyRxaNbXsoP?7OK&gL;nx(WwT5}zd!iDurEG!?wFIH^)+3)QBhl=L-H zJH4r8a#XR}LU3K7?kOjOX-s1bB9$yUjpl7eO(R$@CO!3lC+kl@)u`7(1WB9SXN{Td z$s|ZCosL3T#QPUb?l-s$Jn~D zWuxzo{&e)}(UV5aqjJUX70)YfQhZSnQLIo5jyyhc!^p)W_())6x%_4ML-I4^Nx4P7 zR`!PM2H9mYuS_XBK>8QyYtpYulTwFteE5HcUmCuBxH_yH`t#7;L-nEf(8{5q!QT#k zZ}2wuT>&I{Px3R#HIg$Wn8YBN9(a4;`GKc9_vq0RC4b`nA^BD3zk5FZZ%Safba2hY zp!`wZcFCQBtgJmRE5`GfqAJ2Vi(y|z;Mv-`2=EbZ^`=v{lR{289=1A?gbUijtT=h6R55PI*u zLjE+5e!md(c}XedPx0vY2}1Ank;D3{W#Croq{;)b!o{TWVmy$oiqu(Y7z1Kq_f0);o zTY2=3zC6Us@D~2dJ~Diem*LGk$Q&7dhnL|^{7>7-@ByCPZ}UIyP+ye2X$0l}#mn$p zJUTDK`+2J06hw8N7T(9B-zW%up1R%3i|ZRadLMD!!;9+%9%PQV?&ihyb^fRA#B~?X z+4X`ro2SWl^5Xg$kIswh4xZ|DLa5FQtB~K$qkmNp`aA`{jh}Lxc=XPcyOo#WSNJdc z$nX|khS%~Sb7XikFT-p2pSF|XO+33-^FQuz%FT1X%fHRb@XI_pFT-!~RKFyM>b%9^ zn>_keg3#w_{f)c~ujJ7?GW-TF!!Pn*_L1QYybQmPybQ17skQ}Cou{^6<Rzx z3^(u~b7Xi8FT+dupSF|X)jYeG@IUTYhVyj!%e)LP=Fxc>eu<}gkr1l$D%#7h;?XY@ zgg#G~ujFNT0gv91;TL%sp3i^TM}}YEWq2MBGDn7==Vf><|I>Cdyn<)<9R9~0$`G7) zIQSed!}UBmFT*xZ^=v^@=jrlB9{nsq=<{^>a$bgK^5`8IUdGGt4F1bLGTgw+@N^zz zjtnp5Wq2C@({?hvglG3u{>L55aGow-%**f;9-WurMLgA(Agc2=_6vFRrXcirOTh)a z3>hB1Bg6A~88-MY`^fM-UWRoZWR48aL55aNg3ho|j>bN9SdD zHcz!GgeuUR4PJg0k6sakK2Mj=`l=IY6OevmAcZzhtO3r@P*LB~w0MJM6;1dy~uz-V4_)D*T z?uH^(t0l77M`Rc6T`9O`=KAiFWV*=>i>u4U%Ff^vYuRmfTnb~T?JT4nuiSjU-%h$F zU{D8hrM>!EM6-!<6iyR#$(}EHTlQ$c&}jJJMp4TY36D2Tr-}_FL3)!8vh@xmV zQON|Y1*EQ}T57l5sc}`bmOxlXhNlaeK&VW>&e^;xs>LjkfLotVQ8Yo@)BzRbZ2>xa zDS?@)B;rOaN`|QGXm_A&5$2lcbe(fe$l8jnnkeGk;4W(-w4f&TDF`I4pd~w^q8)#` ztcfC?M(%!^IA7^l8TP6tw7n+wFIKeEnh1jn)+W+6W&?>j`wX%H>)|jmt9B$4rgX|t z&#Q=OHqC;|-YTZ3nn&McYtFHZ*K*lXf}*Qfx9(wqWJ?L+WQ{sTs|5gd!8v6$mWEU+ z_WqEFd(wK6TZ0;qH(Dq#Oez%3`NA2G#;1y;(44yJAY0QRHHMVJo`yEroM!V+ zU1&@hG!tcj2&??LU=)Kx8r7`3wez)aTV{0&G3fldKvwHSl(VQ-Sx?~*W6mZLO3+_& zX~LnFC9E$Q0c+V32?1(54I`o8v<@I6DMK@_as~`oGSUhqkX(xl6^%?tSg;w)b)C~h z_Eq|pFj5hdNwgwz3tnHngFX(u6YFj7%(%I+Ibs!e+cA%ec3IcGb$&!g(cU|D?@_ed z_|Aoqc1|)o3?t?AX}*GP=ht5-jI(GfjB8D>bB;MG0${yT5Z5}mUMzG z)RCrfo!&sU&gAGwxZ%vB z{;D^Z%NKQKdo@U8vv7_!5_UkB(dS#y&}-?DcD{P{bgt%q-V+ z&i_BUX23S^_<-zKnL_#_>DQ$-sa`rY{PW>&4>yM`!}|{Xdgyl94@W*XcJ`Qe?9kD- zht8OKZv23;Hz#hKs81Lt_L-7RzBGB=#7`7|R6MM>blg7h`1or>u7SUdBqqmYUzsdV z9w#4C969p0k?)Pnj@>nM(BSU|?;SjMFfh1E@;{OX$8Q-wb?WM=yyRj@RC0u@DF0vi zGxDqCS-DDa;?_Gmxbk&M3YmyXilQ+JqA`drMm8JOYg=xdtoUiR7$)U*(pV@{Hke95 zkD=@+W|gUi&1g21U9yuzV|+$5#)(}FyP=#VmFb9C>40$~0fIAb+G4})-a?ef=U{h( z2@|2R4Q~(g9Y+hx!X|xgAN~G>#tm$;o5Hs7T=;#_ABbRmB;Xu^Im{LJki_RwPp#niz z{jfFdPUoBjm{mu6V8)b{>?ayyU(p!LyBKA=-(J^%^%zWM{Xoi(7E|#=zyMim{!G~y zaM~*cx04Ece75mtMPoc88sqz2jH1ehntaNv#^zQl%W9pstntvKF733={%@2y3~Kb;5BfTC3RnSZOA2ftZ%fN(a5FQq5#5YTZt6TCbsM z|ZRC{r=DqTIy*s|H<`sfNv9)fY{*3RS<^oU7MN zfm|#Jdm7$~yHQAIy)`YlFTeW4Y?z~(1fOu z4_Cp<-|n_i^?RI*v5D5rbtIZrT4Jkc-BUbulVR zn>wB)(-s8rdMa$~ZBMcenH%U#uI$Xyo>JPx;BIHlYN?HyL}T!Kl!)<|?6#4>)qybL zovHXRzuBHCYGNr(J>h63s=lDg8Fv;xjLlZGAzC+S@%wU3 zAW{n>(TE|G%tk9v(dE@ocD2?h0;8+7VlW<_(()me{f}S&Ip220t7JSmZv_u%b%57j zarjjP-qc0yUMo{G_+80-+M%pj3k6TkHwlTx07YW}T?`S^z0}1Jp@jS~gMBv>cPN97 z7;7g$)P-DHZ!AV#M#N$b!Txx`5kp)?NSjC!jfr}<4G|c07emA`{ERL}L8aCC3)+ZF z1!1OSE(v+`21CvRM=^HI)6fmbtTZNSVXqlJ;BL_vcWvRyQYyE6aKYxF+##1YVpq{L z;bG4vs)D|*R0oo#7M4lGROG%FcQLftbWN?PrdlXtwX^xO)l4J?rp$gFn-DzPFdMVl z3OwxuQ-;t!D@9`*CK}^V(HQ(PY79k+h>G%Ll$Ch6?)Eol%wb#2p@cn-44lLCY9nj; zRqBzlYcEZ=4G~K<&y|R+^Q~?hA~1g6#n6W`IvUP14zIDGH#<#)oqfTjh7kg4U=$U= zOYHfw83UtsV8t@g82gCE*qdGTAaN^Sw+#`qy?dxOiom$1+lC0OrMmwAO#|a~#f7pd z$tH1q+5`PjOW*>cJ;_;KFH1?5Ho407j}Bq(ZD(K0Yu`tQB66K@c11C{TKaqSMI?La z=fRj8+|z4M#lrq{gKAc+b()Ep>$p46Q+Am{s9~ivR^{Ac3z;Q*1F3rLsdNpicD-+c zDIBvB1OU#o*ujmb*&T{5P%IOidtd^Cib)m!BRcy9V8DjCb&piP9EQL@q^a`8WIYE0N-Q z*8$3v>NyHy|JM<~mgitxXu=|9V@~cnX%2?fD&>NX9y5cK%jSYhV=g9$Bw5lsl1gPV z6v!uPRDjguKqBwa?|%+rkLgCq%BiqtAenPq{R{tN*ks@(=?lTBc;=X5v-6fD{F30V@JL2#?FTw zHJUZprw@cHv?;GvgAF4>Xp(iO*Iw4tvneK6aTQ~skhNu*R$B3Da~6uIl*&TL>-B`0 zf?DM*`u!OLpmEqRo4?d#@-bfq6X;^)y3T6exw-Z@r+F8~C1R%Q(rKP%zKgYwE^%M= zXh%1?m78gE-{}9Fw4>8xo!wedA9RT?#ea;YZp$X zq47A$YN=&tWRhmq-{(_04B_guG4FIlz`8T!vrJD%)3c#c64&nZHsjKjr`h^`i4t4n z#nYTM$6~FpQll=b8mhcMq|6umEwZ8W=e=-&s$*JTI#F_EGY+a+okpTs)Kc+>s&m6tUb0hvPVQU0nq^kz0t0n>CyL`tDZmo{{v#|L_`Yr zI~RYv(XN7G?BMMqd-|UMclN*s!CEfEK;OuogdV0s4P^;2%;t2#5))4Qt0t2!l*4n@ z)^w!N@_6hT9gJmW*{RPG*%|-8&+Ut)9ROyU&S%n1j7cQR=2;!+4Hz5sdYLLP&4x#Z znTzg*zCyHECohi0$cQVfj>PIouhQ%>gDqGc2pa3NE*}_dI%`4N!0v(^KXi%Xh|Qo$Q@FxPd?|1WuVz&Y^nz=(C^Ao)A;N99+r-uGkWld@mQ?vkA^ zi^*nW1JcdX8>CICQ+k+WlcXjwOIEP&?jIhyfB3!OCx)+@+B|i`RCCHXb=c&eCcig% z)nsAvcs6gq8x!|UTs-mF32;I>{_6O*$4?*kjjtX1+t~NVt{JP08OQb?{r%{JqnD4S zM>V6Pik~ZPQ><5@Y^1=4BhQaqH$o2=hff$@HuNTczdl-ewA3dV99)IP@^}>O|E;^+ zNRwIA=&&M0&mB%S`NHH2+mYA-uypeI$>)Uv4^KWf`J7PTp~)Xi{y-@3;N-KD&rVBy zimlz=0nd}qOg_UVKlsB(ZI9SPe>ahU&{-s(d~xzc9ynvP1rimv0WWwOI96o>RF9er zkDoGricsL8@z!`tDDdETbG*q<=0({UXU3T={Yxi9MAUAJ8Qh*+kapT@JuDANz9;#f zP~btycO~E5qEd@dmOL(bd<)^lY(x`^HHtMtfyWiA6|02;k119uRtW_jRjgF36bh_R z9Hux-DDa5lP{pA_f#r%r6o&`}mMIQa94r)AsyIk-kWk=Z#es?gg#r(;*=kk@1s+r! zpg3U5JX&mSDE3$EzlE@ua(UQGW7d;0*j(;dxzsb8M-^E`Rw%GSkx^uX0*|oOdoiKF zaz$E^778p=q!cNkz*0q0k=z2@3nOKy_fw1sfh;7w<#P%Y=l*c>4OCwr!bZ0P!->xBYKhrTxS zHKD-6L)Q&mClq*S=&M6t6$(5!v}tJ57GW&b!J)4VePs*bY!c5IDjo}7AY(1EOcr{C zp^zRd-MWM>20S7?NV;_iT?|+*Jy5!J30(|WCS4)jx`ZwUER`N0JwRv&ho$>Vx2~a! zQ67@+C*4md%O~6zRH=Gz62qV`I;$3apSkBY8$B@QCF5lJ5%zmP?+NJS`MhCV5Kolu%%)&6;m&aqXae;s{hbkit3Y9BpJ@n^-;ifa`$g;jCL z$e%`@9JzX=GGb;U4c?W1PyQu2DL2Yj$lj4XF1u1zlo@3EOW&40D*b|#ke(pjclh_i z4-a24oEtuF@PWb02eH9p2B##yk=!rYFz_&&{r^*vWdpw-VE;3{@Exh=m!V+K7kmEu z|A_>S8uqb46nM$!F<^{2TCUqmlnKFOJvQ5seMj~c3c7q`IS-o6M;nbsK5Tbq8`YMF z>UC8Noji21P|)~LTqx*ghdwJ5^rWGago1u%=rck=PaHb23!0-GMh`-fF2Wo#1&vez z>yhi=8-s5M1^vz7Z-j#WdhpjmL0=zyod?Z(a&)>*w>(aFG3$0@EIquA{c7x2LP1{} zdrc_lFUNi<6!aHkzYq%g^Rb`vpcx-yExProK)_-}UHMF|r(45Q!&5>*Cx<76f=&!i z2n8J<9v2EaHasR2baZ%BD5zpsAry3Ectj|ud{{0NR5mOV3Mw6z@}Mak3wT}GGFGfL zY-u0S=owhqav8r6OQi4go5rZ+gm8;Ub4M}f=wael#x>G&h

    mxY49H1-k?y66?+z>x!mg02`@Ar$n0kpqN+?mx1>P|*EG_T%3FuNt^uVCsUY zy(X`mT*>D31IF(gKVj^tG4JTBqbDnVt0<4WKXT^Ci2QQ-0kTc9PfBl-YKI>gwhp~8 z6d8PN5SP3wVI%`AiCy|buP;oTbe42@WMG7&GqS*=Lv?TV$F)#$EdZ~@ajVN%SI(;N zP_-V26jGj&J?61$qgIna3Q5(2&6K<5&7E|`d!M4$6N1|9&D_qn z%ci*^^I!xh&xXC02-DDLX1&>Un6(;KnIXvPA=$@K435?@uS4bb zZ=D#v-2m$GXQL33thJz7RPD8EE6VCRdlLyKNF>LElofvf3ov?fL`|DAwmPFW8jEbZ zYI=Q6NV}@u@)3NyD&snnI~cG-=}auyGC?hWB<)#ORIzz;!mSjWpNKR^?QoFFyDbhs zHJkGS_AtV>yN+Iu3u?Ew`U>AJ<_WAbhj27)Sr^QbF16o~2%Ic4CMcq}d9%1w*Qsp_N%Dh6jy=-iM&% zY<8B3=%8$xPS_xS4YON%3Et^sNa8D*6&dlDGi?lO8kCaB#7=9N3GQL9Tvq>!j(lbMj-rfq37 zw8oJnLuHDBGwdDHs3R$F!GTkzXklGI+lp}gRnY6xLfVBFm{&m4Uk8e$)dx7e&1|`x zcLFH_3))-j0zNpF3>plnRx?>Nwp7j*465>tb!Cf%ZC66CPYG(bx0(t+zKw?7?zP+T zroNQdHxZ>RU~J8<%PYgGkf&TsnH_3<#nVKzCKpw+5-qbYJnQ7<)d0OdDX85A=CcM; z+m&!MXs&|U5L(9@45Kb#6k*0AzE(9$F%+$LSnL3qf=nuvjz|V)oq?0C_|xC$^$9`k z_SQz?`)gJwLVC3!(g0GX%sNfZ8PUN-+Mz@1K~FK{Zs8>+t2G1zAnk$G?o2wxtc!ZO z?Vc>C-QMy?e7kOQezsoqgOEq*cQhN2&WJU9G@P)O>kRb&*n1afSN60n@VwvWTtsO> zpu11AX&#%LQ&p)vsu7`5&s3^X?Hl}b_pZ%=ap$5HNS&sr`AWSpzZ zFPbesW&K@@q3(twQ)h$13`=mJH@5k?)Ies#k|*6RY{`Y2RP?EoM%M)fu#1+(R#SThsswe zAh4!3X>o}%z6H?+G)W$Yv+08vW%04$j0gGBUWu(4J=XiN>4(3(H1jfUgcK+82K49OsCme-U1$Wxdw!=~*%n=V%kd(MSvpLw|S!MgBo z{z3MtwHNMl<|~$viH856&IDi|lL-99k=3~7N>NkJL}P3n!i>zImCedtX^Qn0HA zSoaHDb1&TItP40t<>dvkb`T^~E3VA1B6R`g5KsUc2^(o899B#woVOE9B+X&gYNU(R z39hjh?sMjidL4v?^3{}a%bxeJ8s|c9%s;Tv{scm8wY8FCv968O=FoVEpuN5^s5YGz z=tw#L$0W#TY{tR4g- z%vu-y1-RN?xX+oNr`^ee-b!femg}!8L)1#fILI$5XL;WjR@fXxs}K6aW{nYHakZS6 zy_9x~bphb2d*ME3zRD*LxrJs=CxXc?D+HZB9LGbEOxQ_e;TA1CKq^$F>jsQMWh2U7 zBr3jm0C0aadsW#B_c`+wJ}W0un+wdfZ1;nQZl&f|RB^IoThv&Fj7kvH*j_MPHp=>{ zt7m+)UXz9X0$h17+~>?!fwpp1Cj9_fi;|A?GJc>8#v-NJ4#kRWbCkgKtceWmmO3!Y z^8_)J{=9g<09V=v_nB(0C-c?P0SV&+BhE?2AEM%exz0_&BIJi7bos!Wm3g@_lcHw1 zX*M&=)6niH#f$m*`?FWYy>OqSzp9rk`}Dwx>8TELvR-4&2l)dMlbbUsTgpNLTybri zTdHFV*5)9S{ewAQtb^Z|z1rRj_c`-q8Lss05fqLy6(L-jR_0ZC@qn`pthPWBbDoB) zrLPDv2lv!E8;)1}*eb>qz!mnwea<|o)J#WPh%Kq7%dycSLmTa`eaabttc#G1iaG-~ zx-o@xg)T_M89+J4Va5FX-Px=BUbxShuT*q4olKh*)adgKjZHAl8e|V>8Xt9A583iG zgCx4XFvq5u;cUb;YB6jS{r&Fk`Jcb=Qu5MeaQ^?&yMG;=|6jj@-Tt$;fA6iIx%H8o z|L@K4#?RlFUH|3l+4Wo3{@AtaSC>~m=gNNr;sySv%auz%ed+7KMYHCGzY3BFTo^vO zbp4WCzUyDON#0*)#MGZm#Btw@?3wJX@Ex<#DJja1Y;NT^1JoyKvegY1<)qy1^Sql; zH9f|A!?x2Y6RI?w_GI^CcOG2?XkYd+KqK}>d)q>e{l$D9&E{&(Qk-Q7qXoa~dp;5J+jsk!HGzPj zQDr%0Z3?ds6mpPf>N>C53K@55NtiFO@}eRL8bj3}z~_}W_VxF9Zvr4+TY!Ac-XPEB+dJ~DdIc^A({PIG z`k*Q-2SZ=4ktU*LS}bxYidC4l-{Q1I6ukD00PjB;6>tUBwV zQ>OV@FqrXRp`YSDQ*R(`2uZ!yRt23x&e+~`PSiM5Nv3Og^MM*qqoHRHY&@pH@aQFzYMZG<|ce!_afR`O>*k=x$^D1{md4t$;tf8vbK>*N)(ci}wvP677HRh*jt4od z_)eNbG>)%Ej?zlGxjfB8yEOsR>T4OGv(d21-gHjUuu^jbMTCMx_;AtXSs;C(L zftoX4)M~S2NJ=K3hlm@zHV2R!4MX>ad`iRmE1tmN3Da8jH5&x8>B3 zupkhChD{4Pg@(a<(>c+wRV~tMvKm@8-Iv0Y&RV$9P%Gu;G#xMK zZcn$}iBOp^Dd01CgU|6f@p)#`y=EA!3QIG_=rgG6ta=?{!Ur%@>UoGwc02~{b4?{H zgBhUr+8EH;ER5m5GHCN^)-YV~ts@NPuOVbY`8FD*Z#^Cd$ql`zRxa6hD zYcT-XsPUKY4RVSaFM?8^4RUndFk5at>bi!L1cOLhK%yc|`!+vm;wZ>w$75ij90ACU z8h_c|kWZ=cU{-ywUCVot^A*pZzZK%r>9u zRAqP^B3Y{zl3lpb4+NYbI0c59ShFnxpW+ZvYX+^2ifLu=2A}ty6Q5`H-&~$MO0}*a z4wob*jjpv;O=zlKNaI5X(Rns(<6~_-Ppfk8wLYM;QR6S(o6bg!=WElX5|cD~c?n)r z^tLG^ofT)Tg&x(Mpo-M#Wkl$*JW3_6`2b|2#$UKM$SG>f7{+3-?k&Yyvy3z%sst() zCGADM-k};(3+X~QlK9o3-!D_aYaW2ysPXUF8}cbN?sH3XyohiMwc*-o#d&rY9?YY; zB+tw=QR!)=)f{E2iiE=OH5Wi_)c6bbhI)${&li)jX4A<+ZKUnI6V3w(cjnwsO4hSd z5-_P>HlhG2DfR@6pB_NlsPX6TjrR6x{LFdjfNbf-`Tt8UeD{TyruTmNo_hDU@1}Qu z$DO})r+ND~ZU?uo-TITa@S8t>bA02U-}rVA0q|$8)7O6S+BaSOt*f)E@450vt~|W_ z6PJgVFJ1a$mp&JynLxo~KHWbbyZ9)*R)|l&{wwzxb+uTv{Y65A%a|tEd^b1PKC?mP znQik)io_}k&StfKV=V24iQA861&6!x^?$lIhm)D_&1a)#TONvFK*Y(3i2uo_p8{M) z7kM0;Zy^Ce!J`S_@D*PnU;m}OIc$>D`{`SYTff=cG6t8Cbub(@cA)%6V*vHF;BEfn zy`fH-+uo3Bxek}%sJnLJ9h{dR#Q^6a!1=|!ah{sc-h6%yE<@{4G?jLq-+MFy&&R;? z|Iyyh-^uX*L|9cA!P20i?MCu<9z_783_$+j-XL$8D&Ooz1xD&Ej#%(J-2lUH2p|d# z|AoCF-gbVhJabxklLQJQHJ@zE@*NWPqp;ury7>?G=CGsTW$^5(9ZONN^K8(;;G}~; zf8QGGPH1zaX|$Ps+88stA*lPPIQmR*!_AvGATi#)9w&yLC- zfGE`NlY2wFt=c^s>hs2^M5-`cOF?vzyhFl%)GaswG5+1XIqaxi@$72S!h5Hv-8YOn#8V(z5M89fKv?pPwb6z%Fu@~7={K;U)o)sdyhU0o?k3$ zKe_kwpSP?55D1$FiN4qoR_D>z1CU}S`a64roT^`N9fsnv1vRZ{6BKkp0LmIDl^c#NHfs^b4-SP!+^mGP~=x|HuN*F2>v6 zx^FZ3(C`c|apegUDZnJRBkvRSdYkbEHc?O+XIIR8r|HX&On_62w;$gd=d|&Lz`)us zJI{>g&LaanzZeQXw)gW-9|~YV!_9tQ*?kLODCos&07KzN_l9`;q410a;mx4{NI;oS zu6Kj_tw&nH;jVc7NA~8hI}{)c)8kgy-FZVFyZuPr^v}Qk!}ps`q--f|@r=l?60@C$eT+Z+G+>R$%Gy!-R6 z1P+nFw~Zfv?xtc`2nrPLZ&Ezl^tETjuyXmd!Yv0_sVC7LyppIB(YkBfFI=)k(n&f?r|C%USu?7w}ByTfNjZ!{0qUDdUS`~esE-lOLl=E0|Ne1ljcwqDqIbTm~ z2H|A2i+djH>6N_NThlA|g>{K+H;!YMHF^V-24$Amp*e#HziLI=gh!AeZnA9~Hq5m# zZSX^EEsXjS)|Cj4X6A`kp0BfPiB%AG*?>g8>!7`HpdIg7@3QE6QHbp7rc2MB`#v+8^?_4}fR!x`x}X^CTk4J#OU|cnQA6uU^||FQ z$4adih(6I?uFT%lV=ze3kLl4g;OmNIo2nIK{t6q6nC^?$A$vGw%}E=j2rO@A_!w5Q zWm|6!NdduY*&;Yb8-dhQs-_a@t5zAu(MB&@FI%Em7i+z4yV;mZtKQhuR3H!*Na7M{`jTMqLy7eI$(62pCUq^m5O-CKKErXhIdMLJ~51Zh7|Bk0!8i4 zB!Cuh9HPvcqJ*x?xu1Kj6)~fak0mPEQ}2qd1HCKG|DS*1n=ia1-TMpo?%WOT{P3Ok z-Cp1Nd$*vQuif|;H@@!re}C=QuF+Tj^wkSjK79FmFMsZ(7}O*9AZT!IKOcMlW8|d^ z7yU1jFI>2Juy2M_$!;wA9xN)WH&zXWA`6F z+#z}A$Y+FZkDaUopI%v`?m)_i`pjqLKIsK3ai=Plt4MveJe1_^f}Z@xH=lp< z0FtF0lDCc;m00hDtfJ1<1#hPYO0T-H?B^ZPT5IH>)b3Z2tQR=zDt;)*TLnGg2aC`4 zf8Lwq`yYQ*LGmfhzIk*&$n2XW_Uz#y~J;;uBNGl(jTd^sV#U~7(JusZjck{NtdS3ATns3;f@6qEgEBHQq!uQJ2 z0kO#^eBaz(rDyZJQZTIlT=9wh<-K`+@bL!=p5-SzFCQHc)_lVAX%E`jOfMH4tDjeV z*1l?Qrtg3Jr3KTcW}{0-2LwEyFx@)h&gQt;XO#ba!4Vzr&GEgDk9PMN2Lwx>P(0aZ zoO$P=;08b3eExyG8Qy>VC505LBcXjm7DxSI7XM6=-ko-#Xc_eq;pJ1QyfgO@}j&w>MOeR>WL+k`$U?d<2wOVdU zOeN-*e$FMHs6*47(evodSH0Z~ok?=D&-mbP6(qmx$^j(r?~uHGv|M6r-|sprWWHAL zkkH!%J&y8vhv3H{FLXPtwW+CErH@mGlHBYwz7#D;e(6^qK=S=NBySxp2Thub-0@m% z(;UreZNZjj#EW}k8Y7YmYKa%pdp?|=Nhg5*=04fYxP z$MnBRa@&Xb69vaFqMpa` z8MS%u<1a2azGa_rfKTs)<7S_6rW7~(jL-ka1;a1+ea~aK>u3l2j4#?@c5~=k3Gb!&?RiSl|vZgq?l-_~^&Ke#_th^V%U+u6-Cd zTLuT%&JHnQ?Zd#{GC066c8Gmy9|qQz!2wpUL#$8xFfg|a4zOt*Vq4mWfw5(9fW_(% z+tEG@^euw}EK-NqhW25gZ5bS3aXQ4>vkwDx%isVT(jj)7eHbWP1_xM&4zbVd!$96L zIKb+2bb$F~9|qEv!2uSNLkt&ZyTd>FadFGw0CUJ8R*8KWw6_cnuyq__a@dD~aKd1J zL&hQYgnbzBTLuSMDGsp~?8Bh7WpIEE;(X(KKl*WQ%isVD!y#w=ePqzwGB_ZL@_eJj zKKgNX%isVD!TE;AY~uef)-T-o;0^xD4}o9a{n<+bf8g5V_uTO1k6r#lAYkc$ne6OQ zxfAye7g0N_aVPYO5E+n%wTRS7=QF<>cP2B>pM~QA_zO}l*FF;wOb1LKrw76xU`#$O zlrRSOfI+~*rzw1C*FsvrxRdeRAJ2izzwrZcI(QOMHTPGW2;ttu%Zqmo~@f!^#9m{tPvqdH>qQ9fwa1_LL=*R4(LZHPtUOl}tDlvcc)(ud zvs4q|w`wAEu9}F{usV3)sTWgL=6F;~$N6!&dwfhh3|G0RS`jPZG?UKjVXvhH155QM zYYBhhSi*sTU<;PR@ipHdCbBMhX_umzA*seI)|2budeN5!#3ga_pKGH7~XHkE}rjpd1py112Lbv0#-f~ zdUrsy^64w!p-bqK6{om>^v4@-+x5p~z+@(_M7u-B~A_jleg@*=Y3iZ5QlR1x@GOx!;NW z%_93eKi6r*Uv!N{;Bvv_RL!1)M1eD_W=~>la_p4bqBqxIpD@sTGV|MR)`X0vgd)9i zH+o^s&dBu`5%`?95rOo^N+Wk*J5Y!?6V?t1@V zPm^Y!xwZWiG<&l%dRxsdR?25L!Fy@;X~bVtvq5&G;zr%6nvD}0sH0Yy4Q91~HAdzL zRUcwlY#C(QKkm;adCQn0<5gGjU;HvD&-dM4&?pbvlWuK|HglSTR^G7!n|gEXlHL*) zhU17Hf=h05t=_G}5Bq)3UlmtMRb)ymY|edHU&^_OO*Pd)8jXjmoCI5lViL{9X*d{z zdrZYa1sst&^A!Agk{F@_(r=;FllcGlUFcqT>7)1l)4kt+_dnkK#ykK1PJa8hZ!d5C zyIa}Ke|s~%@o#QSum9%t$+drZEx!8eSEDQc;!1e=*Den({puzE;;&qEFZ?oS{`{X; zUeB%o=lmgl*7NlSzViC~U;+ok_MhzxeC74@U;^jrk$vU$*@nO&F5v@Qr~61?dN6@= zeg(z#uMobxOA z$iW28`4zmpA#lJ$&-)d8_+SF({0hE)L*M}2KHq4RS6+9v1opowalX#yS6;UdCQ!

    _foZ5IDf7 zcD}D*>;IQ7+tCvqc2e+=R(3eUy!ybHTG zw2M{;#|3;ky^&QEDi?tUr&>0B^kG70E2EYz-C=t*OxV6&%jaF<;k*`rnw`xELb>BI z9eN;zPcf>19kDylW`1En{KgNwwZOvWpL`Di7Dens-HzztlR|O>tu`%J>SH5=8>nna z1&XcP+T@fUs$t`1w25;sVbxh%H-8`E9$QNX8v%7CFAPY$<`AYi}a*E4tOSgc&yda zagSdu+AX%9bi=uo_+;8Wo^yD!SDQ^n&ABkF?3Vy@)Ag-pzyA%*K47>Wtl7m9d#Yxi z8fo|GUQs_~%|5{7bNY&Unm6*CH2cg2^;6L7dNEDBt!94;-pJF4zo=#-W$3wa25EOi z4CHu1;RHFa2r5GueQiokszRSKz0t!As?K}DyjxDF7uM{0uEaH%nJkBLbF{41V#0Lo zD%?=2owaT}+^-YWAs-d!KI2KEMEQ`kH;fy{6MNdlRPv zBvOPEox~ZREz$ip%Dahzc312-LWDju$)u~G2-|Zt4o$;E<&N=s6%7Abd-Bkg0+F6F z=!%(;@-b_oUScv)_zE3VOo9}Zf-sd~_e*40=?DM@(=XXAK=~|IU;1am`)~0EyhH3pc z9z^9y7bVF)T&u^`DW@naxF@idgUfv-jgBTwlefiMgBnXwnVEUWr0O{pd5*NJV|dZ5 zOcp{*N|B5Y97f0}N zvOJo%RZz-r2z!-~k%Q$rS$PK4C|jvIfCGI%_rj5f4yy^Lr1#^+Kxe76&rHcklJ!M@ zx2(|34?p41Wvph$lrq@orx0nIWvt7gwILITbOK!LDD9rw7};E0jZ37sPBST^xc zFq~Q~7Um968?f8#nRyg)V~Jaqa=O{PA1%2-Z$24NYhr5mbgREx6Z-0}JjJ237*c^t z)g?-ySj2!*I&5h@b{$6Cf=*|KR2?-_6bbPouV+rhUJGW0rbglcwnWfc+G_iF%%Y8Y zCMoMBKSL&bmtWqu+-?gLrIr>wC%~Cj%UkWfi=TSJ!Rii2bbS&gSmsOAh+QqErRvvg z%1p`7^U?OKm7+teqEJU>$QY}9B$XF26-1yqa-C9Jtw~~Q1Qn@mCvYcAf+;uq@!-C< zaK?RZGA5(dXf`7)J=)=L>)W4j5X&AuE@KR2SDkf-n7aMdY^E_$cjh8VT+S66s@MY2 z3!6u4H>cX&d3o-{VTbLH8hYPHb+7K#mVJTjj6e=;^lo%BUOq`t2bFW&RiU& zD|?s2$DeT6zi9ILdJJwTPdG>`ox@?4(~@+r=z+@ zkOGQ2ds9nf%TdDPSO>D(F+0@w+z~6RK50mz9+teZmFf4#^K5B&`l`QfH-l!&?(S9) zyZBpAIEd4F7s#_VCfQ!@Bx0}6cPgnaFDE ztyb`8ZFf{oXnD)^EQ>{HZpI*Wr(RlztBP<`cIz`NZg`wxI}xETV%1#JMi3uAvgB$0Hvl zvkB7(JDG~Gz2So9(JmHQOU0ltZ{bPWKsWtV^9S=*qTdIFA{3Ebk$At=T-!-3?aq^z zzVZnNE-J0*N)rlaBBY0%bvGD~a0H>%khDiopfzztL3{HYf{sjXicDm4EXZCNX)6(O(#|ZV3X6lPe1Fy@Ov99!vDJ$s5co&5cizR-fAz78n>Fj=QO-EM z(K>JTyLmnA6dQ?F$(!kvJIrUMEBc5pKxmYLBG$_z&eIyJP*7SmZ!}FJk{hs-r3K!+ z3O5Jy^>D=r6;Wl}==O89IgJ(> zw%lVl{Mr)^)B;Hucrcz$<{BkVh9RFeyR0|Ri4+vNre_H27@?%%6m--QqM6&8uw{!D z8(M=h!k&dJ3_J~Jj>>am*=30u)vH;aaDPsw5}&q2UINtvDN|YQRwK!ZY|}Jqcym8 zAR3BT$pRzmsx+>E6gcBqD6OriDa4-Eo^UOF=T%(z@h2Rx)WzdbDeDdryp=$qxT6}C*+~&J-l;otQPVvqHH!_6+w&!)#_SYQY z1}r}s9DxW{G7#pnlr3cq(cLO54xl)vASLZ2B@l@#&BrL$aOy#HUtP486wcd|_RQB8 zQ+%{r?eX$&JW-2!T~m4pG3b|6WU$hk-I2=;kv7`w&v6!&W0nrwawpa1)DgmG(>jCm zJz_BS61s`?EBzp7xk6+jONBvO_=L+d(;|K7K3Kn9ilO;r=#A`=-C}mt;)|XVLydO~ zYtEZ1U+VcLEn{91Bx9d7Yqdpv+3WWE6|WUwd~^g%b7d6at6c#`rWk4s+p~#GV{y11 zmfIB>j+;4xlNA@I@%w(Rszye*BPD5&gJr9^I~*>4>{PV+J45oy-+IEK%8qn3_t4d1z-klSR>Y-P?!uGQ?hQvhP(!^% zSY?Y1~^-Een{k>B~hpK#Df zX=zTXnB;jCvHabY>im*S*pEvZ4i^DgeJo^TMRoq@eiuh)U)wlbH7AV+!p}r(M&F8;O<+%y8CMs4AV3mQh9< z!zO~?$3@DU8ZC+TycxK4Dv7&=B5(gMPdK>axl^|&7zsn(bG@dfFK9>rr>CY2>Lm3W zgE<6`jdI8cNBMXW2ds^=$)v+0O}wc?3R80$N~v47TCvWQhHIt;E21LbZ~4<$9ftkx zu-#hC2h(7 z;n3wuZ9*M$U=zTm{RNiJol->RHr#HqYdSV^NL+~Wc-gd%@^y1jm1qQ$Izk7pkTWHu z)>VY!gA7%3bgh%a_B4pQye{vtTzulzwZDIXzwi?mul?z3vupUZm#+S!tAFllewDfW z&#!*om4ABWFI^Gt{p`IzbuYR1`|jPn`wMsf?A_&E>hABn^H1)4_mwZa^ZPHWcUpJ8 z;PU<3|Lpeve*4wi@})m?`+c{5-3Pu#e8{iiNDm%jYszq|Ou7aw2zhU@>s_2@cw{rXGa z_0sUA`o#zPpZ4L){-vWJF3-j-tjEPcBOjnFt+d2RA6p}($!x*cnn8Kpc+mohH8{MSU7pDBc zHdie|)6g`!{>>*XlCchTb;PT6bE_g)VdU4!V`S3GlJe3PGLdXC3vFOo=r!Y{1=C|R zgb-0Z42Ym4m`JakE3GubFtO^CSX3?7!TDUl=aJX0yR*OPsJXduq87mJw z_v#e7>|ZNHXOE$ zHneK58dAkq8ZqaJ!ks^}Z9zL>pT<>&W@roQkID&N!&kbe^thFcl?HK7&-+F>8DsT3 zzjM=KkX2;b%myhd`8{+{niEdb=4r+b1Wp3i=&LI>m@$So4WWDAv~4kyqlP38wL$LM zOT2EWQ)5i9O%JD=W1&fwT1ze^8FMZ#Z~xLsKO)&#&|}Fi)oVi(hDEm~PuQqB55WE_ z_cX~c!m69kh3NLrZ(D?_N}}bKf~2dkx@>fi$R1cd#F?ann3EwnC5?F07TIWU``fmx z`hhYXXpqrD0;(mG6TX=ZNwKB}IMr(nr%LE`koqDk7IlFJPD7oCJkbTq}7o;njyud*;)lV!a& zsZX0CbnD-3`yus_CF@Lir2_S<&EaHa==G6jkNGi3)SNUFTQ7N)wI6hsx4vhKG01BX zl=|~{4vsG1_$kQ(TN(678>9PZ+9?O75ID=qI;3tOCoM8r=+D|+WQxXgr8Dgrfz|+Z zo)Tsy5x@jA8f$HgBA9vedUn!6Z1gQgi{xX8$5kgA3v>rn;mO4aB zzVUx-Tl514^E_JUrpUZ3Fw)iblZ90x6oUFgu+amD$wMCllP76E0Xi zu3v!nUpc@xHxQsOV+5|`q14{qX>g%0V$==JZ|wpc)H zCu`LtJBTf;mzph~PpNto!)sAkmF3WItJA1PFT&{ZU);9Hf)$psrUgzEY1M$ssoCx= zvcb%v7T|CrmFqmw@jF~6A6)pmCoO8Z1UCAf*kO9>kZz{4l+nG=Q)&bikI6L(iYW{< zT~72%;H%q?n`<_^qNFx9ju3Ly$Xfa4&ET4^PvuI1YnvENeFiK5Pc zKy`H8;~PV)N7C!zSVQAxfVl9I_Qin|%2$4S+hT-qjh0~fBVgR3^x;~rnRaEdP7@k! zS6`kA_-XWxye9qz(jqD#G2R7@_TEpCdzuN0w`U zkd7Q@Jrb_`t8ELZyfS4qs+)=_2lQ-Q_bjp6a-wQK1sh|ppVP8ZahC*^UemWN7{+Ak zAmWN=6RqFJwYau0nh@d;UBk5Lfa{H-`D|Sx9Q^W!x65W@?09Taje(^aPM7Uf7nC-n zJZLtFHC|ie+4be<1@}ixl24R;ZsO#wv2=w4e_TXZ+B~@*EvsTF`ZEJ3e zz%QVcdy}qHLv#k7fO>G0uL@P3SP(R{opXbV%DI+0qb3}}_$=JA6clODO#%TGALY5k z4-}lOGNr^7uKeCDDb#ILCbBi2T6o>>njz`AV=*OwE6!S1q-^Ar8o&!yokN35-+j^o za@ww!Fq-c#%Y-g)YEU0CL^>9{vcZ?#s%>^isu5SH>)*I-0V~!#t#ORr3`sK(CZ(k# zC&~y^kThzMQEd zovH2R;QH^{wkSa(ssXua26)SuuSt)UB)&>4x@N%Prv$sEymIkb=hutow}!wA`qCz8o)^BR^2u)GTaCllaqEJbK_Nd zdA)5h&*TJ4)C$niKI%HNi4vJ&2vamxt?&srB$v~=3E|_leBC{1Q6sXXH?148AC|*xN9t9#F_A{>HJNVpQt(GfNo#!4& z)|yfysy3+HzWGJl7SmED*uFH3O>r|QmY@ua-yn>6pU$!KVyoM+Q5x1C=4qbDsyi=2fE zvBw6v2&3b{s5=|4N@cf!Oz|-$LG_Z+LQRuzUft@)AVPgG_~ATfY`Z)z4}`ALjPyB3 z{$#I5Zms6=z|vBy=n;P9>$Yq8a8V}tC{bqNI`SG&un>?;KsavD*TWj4%qoVq7Dzvw zEl*zHu*XJC1!>VU5~U|npVC_*&MU*xeAvj+UQpK+a|k9>qj}^1+IAxds=$tllqfm! zyn3gtm<^Y23yvZ;gJBhS=OuQk3TP|0FK$magTQu6Rk_q|qu4A-XBh_q&@f?{4urTP zfWN^Lfe8t|!pIljbAn+m(_HlzOcitD$~0&i%c$kCjh<^a4kVHK=6eeQ=aXK-E> z&)rHJlc`0Jh9H7=LOW6epRNbEq7NkbqI}WWiGU4l+`RSWoB01r z-+JNB;>NFE{m8y;A#O(PQ4^S4ml>KfJJ@XR1td-nR+GCiCKFpkXfj%HNk9D zh)Q3cATOT&1@AXwi>wWM;?R@JIiBn;?Bv}7yq=Q9or&j#Yp=Mge`6$7a;LjI2jkWUC>QjBgn}*pS()W=n#0xthC~Q0olVGIEvG|;4~(U%SA2>L7tt|a8w>=x zCz^s|2EAEAHLNuA=xLW~R& zz<%~M&Yg8;H)roA0{ygjV~|`8|G?1!H%yBGadg0y*vlGd(@B^$~g)C6g;He(|NpN4|6Tw8yZ--onE$`H|9|_!pSW=EJMW^mKXEI#dFlFhT^?Tf=}T9xRlz@h zhJU`NSd?{${ z#9@rgV@7e;H1Z}hAA!pnOM#@8HZqEuj5q3qIMyVo7I@~`j=}ed2J9k6ZWVtgp@GcM zSr0)$`XYQKYTzb3$@K+ouxhXIC9lb4K}*se(hhn|uyD(ARRlM*1bl;mY%o|5CCGqC z54924NPFa#g#s%V%ncTJc4g?OfCC{w{TQlg2D%ccB=v_z+#05v?>b}fU8E9ttdWCg zN_bdlx!{68NyBE9j^z01Fr5idipg@?RLbdaqAkbmeoXp<35?VpK;S?4;*XsnK!Cyq`f{TDKiz?Gik8}kEX3y&TU<&IpJwiCi@9WjT?Qe?DkM~mDGT#;MIAnh7h)geJ0 zCVgZ&_8 zusdAqTy9zc-z-H9YAIrh4GKH3yim^9i-w{gN;S>a>+#d`@(&d8|6g)RzwpwJz0|q) zTlXH_Gw%M&yWe*Ay?4InPW$$+-hSowdv5)OTlCFeyt%mfH8=j=jrhh@a6`Xx?I*AK zSO34OfAnhU%8y)m`SSmB`P(ml>7^gIq+k4vi{E2Po>l>Yjm9-aUyBM$^I@?Utn%I6u+Y6Q_3;&aRC_68MHCaZOezaQ?<` z?upYq3ukxMlQ>az7=S|+IDh`<_Qd&yvv79TFpaZJrpYM{y1@C7uig`Y z-T8pxWPzb9nji|Cm%ndM9QzC$WOqIgE#7f>fhIR{V}E;39P2EcUH!2rvQ6Q(VsGX* z`ud(Y=2Z|G%u+BYajYD?l9Nq?oAkC5- z#eY>fi{P%}>7?4Anw(&=KoFn&wSTx5LH||xES%km#E?k5$w>`OD{y{2+7m}Q3ujls zC0ZpKszmE@f%D0Gd*X;^;D91DZ>>*bf*b=PA!-g&;QZC^-xH^O7S68X8IH}GZH1$x z0_RWdI{^!4;p|Q%defmCS~1LGf&Aq6EcfgWe-_TJf|DFb&S_HuS4x7JxW zy9&;UIKi`aleP+++0{M!!<~h*tKf_+5>x}^sVXM=PyPXQPn_miIJ-)YlZMzdZO$+X zoQ}IE4to~P?tCCO2#2s+W^<#b#CPn8!<>b)J0DPtY7%Wj!;6Xjla9U@PW4s#Z6-!| zcP`LL$`C1#0HEc+N}WLv+Es9-*)|DUZMHXpfNt(Z(0`RY3ujmHtQOeZ^^n5jeD~+?p!04cY(>w!;5^=i+x=xc8HH zzw^%EHhSygjZa+vv#6o9~dFfNbo8nq5wYzFrA5n6rE2X?}3hFWb<( z9u9{*NKal<*VIg3V2e_Za66$fn=}PZo6I^rB8h>>zWW_0vU4*y!^1A=1S-{FR7;te zx;_-4=rqNw{r~K}dAua|^#5N~z3-ESOd?@!G9*koQ@wXbmZ|Ql-uJFvB|5z?)q7X3 z*-bDca$^z%K@bGNAP9mWNQel6_y~d^2!bF!;^X(ObFY}(xyf|Ld_SLf{MEiEu z8t$?~<;&=lzGUq7!}D;om<~+~jL(oUZsyCJbgJMVxm!c0rA3wJ?!q9FvK!*#{Mayy zG-CtjJeDnFnQ50q&u8K9e;V$xL*>gn4s)2}M#)*v#U^u1+|@HiTAS@MD(Ll7(G2O0 zb{QYjWz#Wtk571`Hk+Y=Eban|TSX}Y30^ymbB!_#h;p(g_m(H(kE3RT{5wh|0_ zVu}lMrN(d=XE&Re^w3l+p<9+s^!yr17LuD(nR+43TlKbjE9#|+#ZhtK83veY$b^ov z_dX4G*`YehyeKy+6CXr6xk!vl(TRj17ievEyPanAJRR+nOo?H)z~zVjfxU0gqst9` zYzP$%`Dnr038!k4Q7nUwvUflIcG;nO3ewM)JKaH?oAyd%t{F2~Ov%h<&wfvJ)wk~_c!|!uczP{84A`o&&6i5X{pehmJM`u*mGoKUHtl| z;VwI5zKpl#61{AJYmv!z$`DNY)9J)ylZy;G?=Pe?hN6wjCg-&f--w#}uBx@h7vl_C zFyQ8Sw=dLnHSH{VvKoKq({Ps^DqrTwp)9dJ6Uy~ziIJ8_s8e>w(DDxo4mUgq;6j<% zgWkxjJ&pB~3>PTH+ZFG)HjVntQ3IcvQocOOtvS$V**J3l??UlEexdkLO!rsaA9cUg z{X+K>-S>6h(S2R_72OwfpV56>_hH@pbnnu=Rrf~SYjmr+SLk-@&eu(JeI2K(=<>R( zE};wQJUUAELY+moQ}=w`3A*ESN9hjJsdN(U-?e|y{z3a|?a#G8(tc0-E$!E|w`)JI z{gn1&+7D{qt9^&|E!x*>uhhOu`*Q8Y+H4S} zyjpXq=B1hoH8agn)7I291POWNs~=R~ zufA7(xB3qCt?HZAH>z(?U#GrCeUMGS`s!LSosd!aOh3YG+SXEF( zt4>pyRVS;qtBzG2sZy)N%14zCD<4$eue?`zxAG3jSV8x=Pw zu2Wp2xJq%E;u6Jq3SQAtloe+wSVd4lD^63G6(=jUD~?qhsZcA#@<-(l%O8~AFTYoQ zxBL$It@4}YH_C62UnjptewF+(`6cr6?+x1vP)#=$#_{yR+gP5V`V`ZEjvwS zmYpoyE<09sq)aUnOCOa!EPYUVzw}<|-O@Xxw@Po8-YC5RRiwB^dX@At=_S(hq`b5x zElbamveKZGmYyaxOHY<=mmVuUQmU4UC67uTmOLoAUvjVHZpj^zTO~J3Zj{_0xlVG8 z#FvQA6Z7JhxGX+P%!-3zT6~(=EIwJhU3{$gNU>Th7CkC@ zSoEOie$l<6yG3`1ZWY}ux>0n4=sM9gqN_xgi7pYHC*nmdVa-Z<&i_AFuKqnnDH9O{ z{!08};4AQpfP3%@fiK4|0KN=AANW%IJm4kxxxn4{9QYD^2D})b0x!ZRzzgv)@B(}U zJRj$Q=ix)(x%dD$$NRt;-UCkYE^vZ(fMdK39N{e>k8{8w-UJTt2C$FUfjzti?BZ2m z2d@Czcp2EjOF#}U0-JaN*g&;cg#A&+^S~PZVqg_N8(6{50+#U?0ZVueSj4ly0-gb$ zgQtOcJOz9)o&=tav%s_P1n@<89GJsnz$_jGX7C6wjfa6LJOoVQK_H6fhQj~@+O!M6gp;YR_VjXwi;9DXG5So{d!v+%=#$KZzn zkH)tEx8geBQMeZPOk4we2CfDkiK~D|;7Z`(xB_?>E(dPGWk4M+1!{2#P=kwsYFq?V z;RH~L<3I(D0p-}=fimoWfKu#lKneCJP>lT*D8l|5NML^f;@BfV4EwV{?C;p0fd7O2 z5%@RkVc?_KAAoU_?03MwU=INw!F~(;Gxi(cpRiv8|A;*Zd>H!`@DJE8fxpLo z0sI~I0PrE~=fK}$KLh>-`zi3(*!{o>l8K*mrQw@V~IH z1HX&i1-u9Q8t^;VoxpEnUj=>(y90PP_7&hau`dI^f!z-LI(8fIF6>LduVG&V-idty z_*Lvy;2qfKfnUKs2mCVjS>Wy1Ex_Bb&j7!KeH!>h>{GxmU^fGA#XbrAJoX9T=dh0h zKa1T2yaoFh@H5y)fuF`c0{j$qBk*SI!@y5s9|C>?`ylY+*goJ**av_g!`=`4DE22ocL3jqT?f1Ydpq#G*xP{b!QKjd zH+C)Xdh9L0cVTY^z7u;B@EzDS!0WI#0^g3k0r)oT^}x4cR|BubUI%;&_FCYZvDX0K zgk1%^2D=jYMr<$e4cM!Jug9(cUX85+Ux!@|d@c4W;A^nUfLCFc0`ue+YP)FdkOHB*u}tCU>8Y61di>w5b*K~053Zq@Y3@D zmz)dOJqNsG2Do?%xM(5~;rNAP!Co*D?D@Q4&l?K%+<{={eZkIpf}M5+JLw2^+!pMp zC0L#l?64`=K|`?px?p=X!FH>H?NkKYE(^9*5-e8~Y_lNP#yNtm=LK7Pv0$rb3$}8W zV9PHOY$+$$Vpgz)j9~L=!JeHG?2D3u&9Z{cBm|p|3pN!KY&;^^WK^)xkYFQ0!G;5Z z4fzF&>JA_q@Cw%N5vo;vq7*XQn1FIf;F5XSW++8ohJ+S zlpTWAzd*1jKVPsfI7zV2KT)tJK2NaEd#+%gyIru`PY~=0#|!rO6~V4-6YRF<2=+P8 z7VNW+6YO!v3ijA%3HDjX2=X!EQZDutz;pu+MykV4rcMV2?aPutyv&*uxJK z>|t93yG18homQ|~jbJru!Kzh)RVf9lR0vif7pz<+SeaC?Qi)(CV!?_B5w0MV zDiVKHsvkKo0R*M*DJ48Uah=Bxkq`C za;oep>&kPK8D&)IRqj&Sm89|{J3qn9siLQ-E6!156j6m&u}fiBkcyKO+Z0=D^RvIk`M$?lQeCA&>_i|i)ZKH2rMYh_oc>Xq)2+NGrQB?su5nUvjih82D=p0c-6cu?zyL6A}9@0IayH9tI z?k?SJx?6NN>GtWa*Ilc-T6cwRkM1JfRM*qhb?4|Zx~R^p+oiMXNZm=gZMv|hoNc({HKJ7i)yR^4yZ_(bQ-KV`?d#(0r?G@TR+KaSPR6nw=Jx80-Mzvn;F0EZl zYERN`({9yn(aN=$<`K<9ng=xZY3|Y7rMXRWi{>WHKF#%-Yc*GEuF&k!T%?(5dYZcC z98E?O)p#|#G0_4Vp&)mN*p zQ14M+q@JpK>bm+Ibw(Xkd)2$tb~UL!Nxe5oU9Gx8wMTW4YO3m~>Z)^88C6u}70w#OMgNF!;a`^$a4sQUNn8Z~eg$zM za1U_-@a4q$z?Tu{0bfd-3%rDw19uZM;7f=p@M2;DyoeYBFC<353kV)~J~0HIM+|`H z5`EyD=mBR$7dRz4zzNX?j)@jH@giV}$N`H)7FZxMz;lQ+Fi)g_FD8<}vk4Y> z7LfqHh=>DoL=2cEqQDFh0j7yCFhzubNg@bji2yJ`_~WVFR8? zSb=8{7U1cG8F(6D0=|$i0#79jKnFnr?Zi%?jW`8pCG;RgG7XXdK^MMB9 zBp^wg2;50L4|odkT%exV4m_DS0l0%W9{2)c1^9eo8}KCJIlvQ%X9J%{90z(FrXa& zJ5YxI4^WE#4Jg4M1&Z;%0!8?L0}1>uKpcMrh~a-0i2oh`6YziVKLY=TKMZ^n{{!%^ z`0s)LjsFh#7yKdMBlvHDf5v|U{1g6b;2-e^fe+)q0{#L2CGhw7FMz+p9{@gt{~Y*R z{Aa-5;6DZa8owX-ApR5JukarOe~JGH_zV0#-~;#%fj`H80Q?#Lec(^=dx7`k-vj;x z|1aQ=@$UkEgx>?a5C0DEhxoUFKfu2Q{62m+@Lv3z!0+MT0R9*Lb>MgLyMXuLUju#z zzZ3Xv{Hwrk;dcP<#=ipmCjMpMH}KnmU&n6)-i3b&_%-~Cz&r6T0Kbag3cLgVJn$>{ z=YU_vKMTAazXf<3{u$tx@J|E3h<^(B1^i~#@aup#;BN=M7k?Y@J@{LJ@5ZkMUXQ;8_%8g- zz<1(r0=@&k26!F*M&R4=Hvr#;zaIEj{A%E}`0Idg!Cwn}GyWRjoA9fE*WgzI--z!8 zz5#zV@b&l=z^n09;Op?qfv?411$+&D8SpCnQs9;ND}j6QR{&p)?*U$czZ|%VzYKUe z{!-wp@JoP~;k(g!f9pTa{rgk+f6)n|t-#|&M*&wv&jfB0Jp=e0(UHJsi;e&uCpsK> ztmrV{F`_NNqeVL4R*@EXlt=@7rbrEZhDZfGQltbPAyNPj7s-K#iDbYnA}LTOk^r?L zF;F7{KPFO(z>kSkBJg7(r3n0(NI`%f6UhniVkR}1o$zLm;gT}5)t6X z&@Olc^l{?PK#cg4K;rMjAA$cvJPiCB@dx0e#P5NBC4L9|Z{i`~Ux?oVA0d7N{4?=u z;Gc*Gfqx`^1$>zJCGZc#FMz)%9svH1_&M+);%C6$5 zkN7t5hs3vlKOpV~exLXz@Lu8@!0!=X2mTjv7x25p*MRpBcLKjdd=>a@;tt@qh_3+e zCcX^(CUHCP8^mqEuM=Ma-bH*7_%-4Sz&nXsfnO!Sj}dne;Kzus5a7p%FB9O$h}#M9 zW5jI)_%Y&31o$!Hiv;*F;tK@$G2&JN{21|h0{j^9IpQYJ|19w_;4Q>QfuA8h0{k>_ zBk)tihk-W}9|C@o_#p5T#6IB1i4Op8BHj=D81X*fM~NGNA0gfgypebh@WaHrfgd8S z2Y!%v7jPf(PT&WKcL3i{TnBs~0e+0QfdD^7yq5q!M!bgrKSsQp06#`tPk1XA0ysDfFC2SBfyUlZzsTy5pN@|2K~1ZuLE97ycYNt;x)iG6ITJ>L|h5HhS&>y zBk^kB8;C1_uP0W4R}+^5Uq`$O_*&vJ;A@CWfmac)1YSwJ0=Sph1AH~{a^Mxj%YduI zOM#aYmjGWy>;_&&yhNkA8GEkiAq>TKVidKVrIl-LM3I&n#ZYDS78LayiQ@m3ax1#4 z+*2HjqH$inO?Lyj%R7~AN73=iq^C)Kk7C;wOO8Z$PA?IkDtbuuMbSG&XNe9+N7KcG z9{(=>Qrv)jAA80A-g1-QRX2-joBa7dfBMfJ_|G0VbbG+FdqotBZ8unKBxyEU9Y%x0 zJeU@{xec#PuK1(u#LXt?f-^Hovb6@^-0%s{?rr-k*-Zw!neDmi^$nj}iD$!p%2STl zeXU76=PnK+8$QA8evT;g&pz3#mfoyf89(k*M#oC1Kje$9V9pnFH?m>xpdWwSCrFan z{p|g9(BM45Uf=t;Pa&Q3s8Q(meUY)VS!39Kc{F_7CrHw>`?&o_W;GhDhL&fLNN@Pu zN-;)921SbNbYi(`vRJPrHhjXf``G=JECz$wGI7p2{D#l1coNg`G+Y_?T#ZTF?efkJ z?9;RMS27z7CQCO|jm_1OlF@81SUNf0=iKnQl}eNkMWWG2EmCs&Lg8Gmv*8n- z-COrpGFS}g8Mi>S)86p8m8>V(OqEmRUMbe~aPdxhV_UJ^M~N!2?F{2`qmntLqA@w; zLshC14-YH8ei{{7Y_f%Lz*Kh!=1F(i6=ha@Ij%JKWoKM3M~|DMWO`%oV!NLyXpS3E zeRo)4*;c_YLM3}gDQho6h0}cBK?iGAuI{yl0_8D!mz7$Xk6isqw;SnH(_D5CD^IBn zBZ=*PhM?&p$(Ci(_huQ&UW?PU*(foeS+jAvowxNGvx&nrwuC~ghp~|>W1i;*d}8Kh z{Fzj!H4b(U&^%Jm%$Mf2MBHttFr+c*E0;)DZr+Pz_|B+1>h^fjH!tS=He-F5nygIf zqgmVE4OWw7&Rw2%XUqYbhYOmAv|3(l_hEvjjmr8uZQFc0^w!!*JzXFxGb87=+iLv^ zqc;|vg?K77E2qXTY9;AOxkqu&EE;P&i=H7vA2{|cf~F~Ern2eQpjb>elf1s)(T@f- zKHl_%NAvN($z*NbriW@3EWywU6$-Y4{l?ts=32depizt-Fm|1wS*ph-)~3H)u9t&_ ztT{pFDEliFSx=@C@0^3rVg<%*r6`nIsaG>L7>S<#1))-i`A(wp%M`W|+Y! zV9&Pcgn3x-+gW|3n~kJKMpx9(3dfj<-JzDQD(llkDtYYsRO?w zji4FuO=|OoH##Yzj8XBPnXR!!#xiS0Q**t3o@`b9(URGPDq+_{E8fvCG6?eN92+dQ zW4UR2<4lX~RtuVDha+vSyG>@hb)L-y0%T@xj7@Ao(pv2H{e5Ph4pIH7o6D4$6=#p_ z`zq~W+MVXycXoWY-B52%*--o zZ!@`)jZti|Rt);uF1pZe&|cR8-;q+#EY`gv&eNQS3Km}Pr)j1+tT)raN@Z@d*4ov` z(3xxYN1d)ipIjM^vyn(9o%8as*t``gF$aF{3PBUSJwT<)g)&{}$`lQE!fq^t$X zAC5NDlU{Q^BkPrxb(9*exI85;>MbX-C11DX9>=Nv0sWKWb&4eD^bkemAn&_&4J#<$FYf3Q` z8K8a|I~9}P(yWtNmos9GSZSxfIZJmEani(RBh4adE*iStfVYvDtdyIRV7=#0W+Q=N zEyfSHjpwA;ZmFQ@%0_b8$%xF`^Apc#SD?=xzkqqIcfl{A2fUkcwJc zzM?%<>>*MVnH(%Y~|)dgNqMPx$evor3%w%drr~jft|KZ z?bFoIW8r)#TXfgA(xd}agl|_UPch5(CNpN^H9~B+NYJbs;u(Hu^LvuM2A!^ly81~j z){D1-gGV%#GRV0sSNd%}Fd#@UYWD)G?fx z%l>>j6idb`xxUF<3y<}rnaca}gKR0+ElpPDT{cG5J(D&~CmC0OF9r|L#0AaaJnOb+ z9i+W7Ok^CrM4FxCZAN>(F>?gx8H#CEeBDXHnlENbi)dnkrnSuvm<&^A(HGEf&uoKe zH<+MI?x~ducQ`s*na4*Vi``=xQ7hp}w&1E{!)}Uo^;?6MhuzrEms}`lMkWR}5a_v> zhMP55hK3A3sgfoe*J`<&<4$Z&h4YC-VpcWUn3XJ_%EX$9Xf4GTr-OMia=^LxB^SW{ z%!lg*r^6i^vM9M`K4xpzGXA8qlgSL46xp8@<{1Ne3lyD^x-0WxCR+DJ=aoz*I35+| z^Vk9XJYUfCSCgioKIIyhC+yUu*Y_EN)sdc4WlJ-d;lo3(C5;?%bl#q#{drK$-A4$T zF@K6H(W!|(!cCZVw>)qZI^Ef{VJb8u`k=oSDku0*U0<&!hX=3IEad+a<20svwft!D z73i1${4Bi(Ugq8VY$#uU9KM})l>44w#n~*zda;(j!g@%9BTaSFF-LqJ?Ux+wNIn?O z=0{}c;9t&$vcgYTzTUD~72#Q`7axooVSOaY)=Q32pku61Zht4xY^80(V9FHE6~m!? zeo#$9Rp4c+)eBXJnGokmmRM#wHu+|h$B}Z*lCuICF?vEZL&)TFdwXM>hp%vP^X65A z$8~+xTc4Td&UBz;=L)4%u2-*d?3j$iqy1_&*!5B7u(cX=aGTXGnN5kl9t6SHqD6jWWv3YHpN=!DStdCZ=$z;c~E@vbj8u zFtrHj8<9JAj5-ZA%Uhz(;%4&7Wb z|69(b+Q34#=lx=va?P66k>66uQG6D?=ryGi{H#pVHeb}qS)I%*yV+c-H-@Nk{VYFj z3>(EptHICyZ)Vos5qmFYN2^23?0q8J>1@B3G;5Xgl}KsU^;a3&bZmU`(M8}T=wd6!#wJBCzV0I zRT)=W|5r0*rsGVoT>&jB*g!iet8*)HTcEl16 zjy76%G(7!6%ETB_iMfv-rGhc%i1Yf}>3GwdYFeB9l%-HMSZ5Zl$GJUmchBTDR%yeI z?6hR{h5FWU#Krcb^BweJr+H!M$lVC>zaIMtrhB(8uKla_I&DbvC(TS?Jy8 zw&Tq^=1g@OtNB}%*=QDb1m=Tjs~4CzsujQ0!9?fLnT-pw zRE)h+4YJV%WGkkjFJtZcQjuIG8F1=*ooUu<@u53mC8}MEh8pOgD?}stahGDRRDo<{ z0a?aCM)Q8pz%(%y#ul!erKeGS)4?RYdUI!Hp3(X^(`PfPFMZD;3(Ca1W9i#R14*d?o_}Zw&puS zcVgO{pw;%akL|=qHTFs|$a)u$O?&d~ObZoF%GDd)7#Zx18f>Vb57#_wfUid9A#z~0 z2J>M<107)KaYW=3JPXJc15BneE2T%BI%)R%iy2oqlx-AAU)`JIOm>&UpD+X_(TF94 zjsyZ^-3!P@qx^`j4O<md}f=* z{O&>{ns&Eq$zg!+rOfQ!^Ffw6_#N|Q?!U8p&jZuPU%ZoBUc)a z&^>4-RA*xys%ds|nV~P0s|`c!-g80r%mrjeHa8a^1Re8qrV`>TK^xaiRHO-^l~|ScSajWbz40@ax=mFWL)an zW`0{d>0|fKK=$+nWGflF#bB!U8+|I7tXV^o8p^_Hqtixq#I_^;q1j>agew6*JLcHE zQ;@RZ)Sa;x^tDKEX3jY3oltvN}BgG_*H@L})>SW04NVp; ztN2HD+M1g;b0&9iP_GSwHWW!x6_4F%43kDQ9Y!GQSU|Rspgo05nXe_ycJBaWtqaIH^4@6|-FV27=4RV945FK8rKHUq zqO7h^n(OxJ$$_`Z&dIn1ePw-+wJad3_q9mQ6*qEJb6f~|(QZVK^;k15Bk3DdBCKuD zYtqqlaMmy)$Jhf|^8&KfY0yO(x*lh#S?t1+wJ9mo}EUCCcvQKz8Q>vK3FL zQ1o`^^@@9%X%%YGSu;7Pcw_Z=!IcPPlc-!YX>>+xzB<~sO^`ih0okfOq;I?2qhMj+ zHH9bxSuoCB*`R(rFqM)$zuD8yTO%f`(LO-OZ3ATW3&=Vx?PR{;jVJiTIG7!#%CAxqbTj)WS>?pQ!}?(sORhUmbPs(Q?&+_ac?b%S+-M_()(vvj`fF{3NW z_{fBwT4VQCLG}d;$W~f4)7)?JcgxdRl}nJpbT->$WNAiS#~;l4?ZaQj3g=kwfKDWj(LgrHsQl>#xvMgki_Oc zwbd=sO3b9ewl5Ja9z2@cWWDQR#i|Z9b>i9B_9Y_7g_~+u+QrJ?3D)w=8_Q-CR<%N3 z(6E;XI~VRtX;48oy3l|%>jOTd&!(>L+b19J#(E^EGwSuuED^dM{K|i!&TK3wf;yvG z@5~Yb@1g0;0VM}fXO#c=nida}4^d|}Rxv@HQLJ}niQxJWb!Ov&cY9Ti`f?_kBuj+O z3-{%~_3p~547IltwYNk}y>NSv-8viAR;8#N!RIXzWG~##{%ic`>y)g2ol6AX3pe$o zF2J<0s!U;3Ec68pdx>a$;l4cP^F-?dKBUjiFXCoQ8*56Ty-%!nW{HUZ;Njguo!MAO zE5C}PzMO#e{u0Xs3-?91*VJCcPz%CzT_Ut!xP?uo>*W`s_JrxWM9jZ%d;3k-%P&Cf z2-9_mMSz9ddHi&}{Cw1uaEY?S!ob2!J^6GsZFoeraQS&cU(m3ZSWQ^CFaMaXm!G>n z;6wUa%0e?_<2BjEip%HgompZ<;^0y3LY>)oz0|&ZhWa8*rX|)J7VgVtlWB_D6DHFV z3lR&qxBq0Cpmv1Gw8YB9!tHFBOyl*hbBWc8g`0Zv$+Yo`vvB!H=nES55=$Hl_vNw4 z#IFx{iJ<)8@!}%K?*WB#(PSE~cV>xD{t$I$<5g|`@&W3LaPC_o{$IE+aPGUjk6I9> z>k={h!YynzU3;iKVY)66%rD&DKc}k@|L?>uI#l=nzb1N#$b#}_ypaeKIDR8u!;itf zisYa0$G;jy%NjA+T?c4WvDto_06BsN!D@ zfY?`-5wjUcTZszuqXYJtAH=@AjF{CxTB}^nv+Pkp1`qvP&$Hva5EGy=ej2Wu6IIwSnx%7LZ+H*^*tgg6u~ZkX>R0l3hi`k&$Ek z$O5uUEHtvKW{|yc0of(i71>o2$bNVM*(KHx*;OORerN&NC6)u(RRhR=@Zbv#OFg`^ zN`maZ1!R|4%41h|g6sztkX>R`j$J(kWZ%Dl>=H|H?5ZAQ-?xD55-V=(>d7E`!veBP zEU>YwJ3#im3&<|9n8vQY0A$~@fb0_MV(jYkLH6ATkLj0s%4YQ>kiC8Z*(KJr*wqt3 z_FW6eF0nktu09WB-?@P7601<`>T^N%9Sg`VvFOCEZU@=x7LZ+Hfr(u`0c794fb0@$ zMeOSFAp5q12l?BVSRE4L|83a&F;vk{r9Dr(Me|OLS^W<6Gf@Wg73D{i0mXL}Bl*wd zY1!{&Md_bVHMV0Um!f)UZP5=z5tJu0hyMn@1V0=J{IC2JR-;k^t@Bi53tW${FKY>_ zH_N;Q&|Qt7HqJzCEO2qY-o}CJH`|xEYOk$^QHwiKiwj)0uebQm6`bu$T*x<8L#QPa zYH5K>`Sq6eUvgdM8IFrpt3lM*iDy@23tZr@H}<59t_M71vCMNGrPYAY8#MF<;)3&HEy0h_ti|tDUPu*1p^+xc6 z3xrSWz1hSME)iGNR%z6t;0G6otkzrH&krsUYBg3}s3pMh$7e9*kt-H z5nk3-PeUyV(|3UYbG^m=rtcCFXJhq+s3l?gE)a9BxAeH_yF?g@4xLj`W5Q+30+Hx? zV^4Y+W7_y2#xl=>lvW)=Z_v;e2w>NHvp#*7h-T5C?dyYHAfR1n&>J7dSSAWa-Lb8A zXMsR?q3#@TRkcL)++DSz-Uw4`f%tj7H=9hYB?9Z(ss*(uOsxe%>-84*n_5f6+Kp8+ zYDt({3q;%NEj?~(EfIjDuhq2vwJs2YFZ8t@aCN#w;9goa3cW!?Um$c}@6GzuS|W}| zgEp)WdVx@WkwG8uXvQ+}zfzRIg4peZoKSvBd8sn1JW=sS#pe~TCQiivh~J9u#m~Wa z5|I#sVafiAAT#7>+ZyEUubE;AgU1CGgM&~<7`aMMEB>jfU8&!_t1?( zcPdQB_-wA3F?Flq$tcEzhMJeGZ4nR5e3ooB#Q!YCi_k1ZX0Vr=bBA_>ntS*|h-V?L!HSa=7@hc;!iuB3r$ zLL*CDb<6u}Teulx$)2=?eQ{l70i zhaKfP-R^aq=7_X((Nq)17dj#*SRpLDeyu)ybV}P0-G?JKczk-M)F*l<#-k z1xG&S%vb`$!MxtnX99&{J!CF91{1M!Z3~yzs z%EMta;cWL!;rPT@3r8!pcHcq{_1$i&Q;R9SvNn#;G-J+&;v+sw+KdsDkJM*Nki%9x z+UXC=)j@NR@5Cx}r8iB=RBLVM8GqM~5)iq&*_?M|?w2c>aWXzAa(-JhTO5VhQZ?6W z^JUJgc$d7285TNZ|M8*qUB$-zwjz3OalrJ9J;suDbd&nY2xa)pwm4rWNpXRq5#9W5 zl$7_c4Wr>nO=qpBY20S&97>jGXA*%xx0hvI43`M!>ZR_)`)GWr#^;RLL*7$74PRo^OO{f>vw%h8Fk(9f}@oki;x8~D(vg5urz@wbd#NPESa;~Il+(!wW-Q#>X zjq%tR z<8+p-H3H~m)1j%?#eTH5MS9Y&=BiXaP#GJz@+_M2$8x+oA8y!vdUM-gu#>Y$qN4Bl zx>Cpb7CdW5FQ-NeT*;IS%uIwpreoZQo+tVEBWl&7RhL$9?R*u zxIWgZMP?}{8gD&5uA!qak_yL7m0Zr7u~n&hO!~>SEu!^Cq00?tp;=Ga=dA*TylKx09W;&ExOV-kN_<@>-c3%0*pB4bz=utXHY= z-Wg{xnX*3Fbgd1Pf2J{@jrx$iGfPYphB58URD7urdIvIOO7@%G+@z8n6#RX&tgzNb z$Lq9ohm&rfbdddd#$`rXQeFLtWuEidDgVeknHk*Egry#0Wb|4amW-w2wVBL){oKh= zsYa9!4E$U^=1Lmdk#di7G|^dwuSZx)`h&GLjEv7+XCm2lD%5aBEc&Y1-sc6u*D14W`7zIlFI^GB-N&{D>*K;!N6C;Rh`fZF7+|3vZ`Nfn>v! zm0pNE-BWOh={8M~xsH`?q@4Nz)t-055pEQ5kIkJ{d7#gkjM-4fSxq+BZ3st&u}ka7)dWMfuJC5O#A6;t22w#AUndV_<2v)Ur_6FNml z`6?eu<*AgO%I51?_q@(JD&2N;PN_e&wndjs4<}52Hm`@rEwWy6WkcDivFR`6DQ~$o zarsKtcFStba9wp{ZC80S8CTs=Gj>{rZqDVLgq=a7qn1rps-t4f<*-NW1B%JT$CO!h z@md=$lvFho^>=%5#_p}!`!t8rgy$!zo-b7iqrMEheltBC+NqG}q_r)ahAtmA1t`kg zb@=q*Lcg9%oAkMi)8@-GlT%ltWHaQ3&5EZhK6-5n%95{Axp+9)C&S*nvCXwc`ADcz z$OV!vgJaHy^X?w&<0ExO^7^$cY(Y=YZ;901s20M2tj!~(VZ%P7dnq>9=coR;zSQ^S zvyt(1LaZNF)`UIbpSgX(-lUjqPm7jVd1z1YmR8Y~&U$PelB*9w$#^AAN{ef4SUuie zHQ_6IB4uZ)7ce>VeNQ;tMhV$xnPlG99!~Ay#BeqZaiaApXmMs(+Q`h%CV`sCNw&k4 zzB}sSyTMi@nlTpj`dpZ9ndYvcbnS$bNw;hLXn}SblB8)`<&)t`GR*k~PC8EqQQSJ$YzQbCr>wnLu<-{E=hvrK_csEWm6_YJYfl8KV91*$fdGF z^m4z?k*N2Y6~{1=Xr-sY^mOL1RgI2$EzBjjX0XcCh}+i&9+)I1jKNz=kW8f2s!%jr zq}^SgsqC^D=QWRg7RrdiFLa1Yy| zLXb5l1NIml!``;^7H?hOq8b}S+|7!~J|Z0xW@2#^P(Gu+-d(Zg_*QI|Vhc1~@U&=# z$gOSR3OUkAlbsKcMVcO`N})kuJSaPxrCv7EwxBDtbS6@XWU{#swsz!ZoVW zg-Kal5zbm@(y3TJLN^d%+ zsNT3s=-0juOTt1^KDJ?OR15J|$X4~}-GLr$=%7sZCX@)zSn`_kwqi3jIe)DQL$>B* zQ*8^$q3lMra-vR;1LfSnWypFRbgwtIrzg>9)b6X0BLCVJB=Wk{%xQ2n^@e`GQSOYh z-WJ!)Czv72W-_g^t#7fUW5sEg@W2-7S+tEZ715T4G3qQA11+|a$yr0~h&SiU^0m0N zZK0f~t^x1j#A~x7Jq^>NCQ8-7GH$b-LU}u=OsUaCbw5fdsYQaZ8_{)Q!A|pym}EUH zSYnGWuk~tVaJ!x4$lG8Nt(l*R7zPohYZ?s9hGDW+%>?YxVcnlk2kjJ2ExpCAwJo?z zeQdYnL+Mz~pda}9(^fgxw2h(zXR_rC%_0pxlBO%E!q6;!bZv`ttUhWKN6f5e3PnBf z`6Oj8W^G)++z6HR)yb6iW#i!>-zZEp=sg__6A^Pvb%FFzg#Y~fXAk@@?SVZPy#6eC zEOxPI^<2gJ^ZO|CwAo~`_5+b9z2RZ>m2S=xpE)TmOb?h!gX*U@zAo^%VhL8Gkk^x+ zcJd7&&-L+E(k3KS9`Y!0LOb6blq-WqrzRvAH5oR^`2M2fAM26LW-{8SS}zpZ&?BUG zu8^7BXt4?{pqzJh!??+M(t}CYYHgc}l&9Jq2cm@%+pSg;u~yC<3Je1lI(hv0DD(6F zO;}Av%ecctHx^9V&;(N(H>W!%Tz02p=A#Uq&d!}t$HbgXwB0)<%xuo_le&A1O45~m z2Q-03VXe>D{>Hjl8=6S>+&eOMb3GnSCT5;uyfJY1+l6sBl3{AhFt>9@g!6O^z2uI< z1T}$%v6h|nvD~yT+HY8P6S|n{(muNQC*3`=NpL$cp?WjI86%!My_|6&jaJz|6nhLsLF7KATw^UNMr^TxY1 z!d9hjhez3X(Prl!U+k74-F#@R7u#&x>XiX>rai3vv!{Gz%0DiXw!w&m4$N%}yK?=g zee587!f7FN<|*a4hpA?L!W{P#Myxyv#N)PGzG^CB+~KX(DwL}=33bOsLxYvam9K)tF2Yy`q>%dqW$(_X^~vp>q9^PeB^;%bf5=%qNDa-_CQuR z-Zt|y5^RI%-#P;u5B>$mD?R(#==K?lfAqYxRsdwECNvks}*rjr7dpBKMTD{c|CIJj?hQ*VxMz zckbXjM&8@0HrQ6EpW*!O^t6{YrE?Q*RLTY&=zc_(BTMD9H)xp$jef4qhUT$cqeG%) zpmHz5_Gx=K>UA!Vej9lpVd&t2a+`XfO^@1t*#lXg&;xCf^#4B|=ulh(8PU9=l zat~x}nFdxPsvi*uk5l1txoYLp9oAVlbM+Zr=j=O*kwlJRhV%09-_`?JHt|40@qtCJ zfzY1ZbXxpN9tf2`MqSv{165*ld5{ZQc=xDD4|lYqEnjso7?-`7!KB1l&|TOJ%9~r* z;q9O)k3^nIy%FBhC=O@kc-mxX*mi{DJ3_HUC{av=cSPGEN4V2Q)wN8)a=~Bgdq<5{ zwOtLxLZxBJ7jf8n{n-wN_WP_|o4+z4QDNg9XeqBypin&mp+8Sn=ST2BXz1skFU0?1 zVuY#3@&_eF^vi#K{<8=EU+;mJHugSGsB!a(Jbq}?r95s8z-+b&v+{roZllSv$pyC% zSAr+{XDloH>7M}{)Mr|I-be5_`$w7wt)dO*-GfA$X4A%pmxQhMk2ImlF{%vH=n4f@ zjyJ;xgjA5-Cy8J;(G|SIV;6zw_X9ofp(^NPMQ-K^gUmwB=CkEG6UG<|GE|NqM<&I@ zFgQV(c1?j{sM}~H+|7hJXUdXK*})nehEOnL%hsU38c$ZXvB;>X6hyD=PtxqD#X z_PCk3dGVvCwA_OZN3_WJgfnrxv(6YPCtLN@43y8flQokxs@yrxPP@J|wZqI*$epfc zxmdTO#Y^<%Kb1?JLU(MkTS`2Is(CjW93SdTy`E;vAD9koWu7*T9N9!LJfGK+;c?nv z-cc-g3;kro$z**~%1|*>9o=lmUfo&f$FtFKJhhYb_UVO6tHIFMX5#k8X5tb@)4|`k zJjhHmJmE|a3YN4Go6$wQRw?Ihbg7Nz2H9YO5#0i9mDb zG}?@zV!%0;A$_DXv&aG@x*V{ep|98fQ(|^Z+fv`7q80n($4d8z9~Pa1-+})4BtN^i z>^?<)_9dm2=P;7Z@0^)U8?T8%=rSv7oQ|_bH(5wKMm`Vx{{OM}C17rqRsWNj%;esg zWTvbwvUQ+9VHoD#o12YAntk8*K$&LW_dOugPKC0H1M-!qG)Baied%D ziU>sz$|6)Y|8L%;(A?x^CT0BO`#+ZFncvW!bKY~$Iq!RKa?g_YDn+$`EKM3^;<7X* zF6(WEj7RAx29$}U*XgLLl?5=1CtcRdETy}7DLe%*2c4518(-&i@I_$E%Zk0$%;5_ z)3}^UwZv4Z#86uxpQr+IFNg(Q$}h5mS((-Px{N{v??SugwrOyPU13Y)GGbFee&a7z$Yipr_W;B~e@z52{1<$x_||hPD(#dbL2# zcc>LgK~8E;n>65iaV7AHeBM6!-ge-ViMf12tv3Eb`NXzBK2hQ|Bc5?;qIfE((p2nL zeN9g^&bijJOR6iG#BpaK;#PVM;aZ%ifJAtg-YSg;h#=v%yRyktgd0ju>y2Oxwp-`& zxb+h4{P)QX-v>T9VJ@FgORm3AK3TB)iJP>?(tKy!qqo@ur2yd}oGx*PvrCMXxH6)m z$cXsPQXvuIhT=JqNf!w_b+KtnHEJidF_Xz-jzp~Hw8Ct&DD`%&(lhUUa`iL7C&$m_ z6KZzM7s)63g}R?8Em31EVhASHe7#Cd=$#3*)9LQy6NfLE%I6GDbtxLqJ4IZ-$B2Q% zl}SOskyYWIvczjFxa|gkkta^u?PjG;DtDSC^Y+QiMZhP^=kf`))cOnMlZCpU$kc?- z6^QBL_M|rzuo;Mg*DLFEc1irx2_50d|_Uc_$yqwmkB?eG9 zYLn^a?UUT`z$eGfLwFAi0* zWL`BniFsmPYn0HaZ2E*a=88tSsjO5Mns!ZE6(MUmE!UX!O1VoRmCoBID}Mlda?Bh) zX{%-b0{NtBT&R7b5b3NbF&4DvvrcEk>QH5@W{16lPb6BgR<5!d;v%;s9}d@Q0`N6K zjw^Ic=SiC|W(M`jl_;jSg6ddv+UU}GJUYEXHvh9rwY&cQ|Jc3<`^K&qJ9hN@qbkm; zoQpUU>?het_Q1&1BU8hV4WEI&g~zV2zha6PI1!X^?-B+{Xo;XMn5 zqFHIl?J0YmGG4ai#_Za-G*HqAN&$l<6X&O#7Op=Ls!d>y^mtWVbNLNAmA;r&82!dz zB}RF#)L|iX&q8sh(&Vu^<*L|36>~}T0bEzI`6dW$A}-g*ofxP$m$?(%D3}L5UeJL| zw3IC@i^asHyx$Uc&b83W8HCkm1g1N*nP5x^NYFPwCN;aoExd$iJT9(iy=rsBWh*E_ z0-imbYoWlfy)UDg0v(xQvHHwF{?w=P2@yf!cDprNqp*)xb$-9Z6^qHed3hz1cO|Hu z`#UVKG*zt6Vq6MlEzArxmYQr?3iC=VCF?>iB?`|Bwyh*2xFU((C`m5lO2Y6Av(rk# zKq0H9&XxFF<`aZx2HI8<5L`jjo^UVdO8oFlf2WoBVxu=IUr6im!ZVDvl|VHJ&*zOu zsKpPT%X(yZrmxdVWHMR^Qq%oD*Gk;*n)BLL;(^RUYfPF7Eu{5`@S1Zwtwh*sYSn^X z6Mp8JuQXO#{OQicg#O zZ5e5VdgNcoySqtUr_7>i}y@c148`^(*=JWXk&c_qVoRxa-8~2$(J(N!zPo>PBklhyv zQ|}5L7T9xp5rx;3I<16RbIAg=9PD$gBn+=9wylKYJW<75)=+1>e6E!Q;WdR$D+$3Q z6_tWw{Li%#KfEU2wi1T(1o@K8Z(qojc;Pj~fg{0Kob`pP{mB5*wYOM6>na}6pILRwZ zswwJY-?-9RDS`BaQbrmrClyjvHBYhc-aEH><|nD*NdgBoi29JZN?{(ahN87_A}MtR z{8?#Oma}zxux*M@P{o53YJ!(22aMEXYuuHxd!lZQ%;_*FLwQL`Nqz9`pnaSw9y~MS zyp*TV{@52Z74$}v$rv{JC2GCFlSxsouY>p)RXonaF`iKyO0^!#7&iCts5bG5HPOb0 zm!o^>wLbTRo;?fI+nZo5 zp88QTwkEV^u?0<1>$DiG|7S7|?qi>iEN0#ae);boV1Ww;XHTdns9&;}v9RM=DT#u# z3F?AQ*+@OK)`WS1J#?jn^gZ;-?3|(7L!Z%3*+}g@QtN8{2NLZ&C<4;$L9Kr-7=XDx zdy?ntDcAqL(RZo3U7u ziEZRQrzcHtMI}>&%;`bpWhCWR86{e+CnB)8!`f`rl~yKPQ%Q9!qO|0^C38t?&V~FF zSj|$l?A2 zJKsZe%{_W$z8jJMe{v)Ca#E}W&Y)DeeAYVnKePcqRiIwxk}3KUQGd(6Bmceo4kP(| zx_y@|dh%tbEK?SfV!2NplFaZS|__c#R;1<(*sqVwg_WxgyFn}|&rM3I0M1RAxyT zRk~C}V-9C0LG?^IMaUfBlBm>m8A@kM?ei{U6#%#XP7NgMCWqvNWl0 zK>;xiYSZgSbe(rP5Da;XiF&V-da>@O)p2JZ)$C1Oe;fz@$gBTr^RHU28;bkv-#Xq~ z!GfPr_`RugD(d$proD-%x32u&vb`m5!CNnP<3Sx8`cRZ?RIh!eIOO+v>qT?$*Lc() z-}5`TrF;GLtAqa%O_jj6x8VPv@IANr&V@J&RwE|@1uEe3J7yvKrZS|FkYzR2OxT=? zhy~W5F&K?1qav#;QQ~1lG8H4`QJX8+I3f3?GRI9iCyG^<+B1>Usk2T;L?5su6QG@R zF;J@cjS`t8FLw%zL5Z)7`9bGf`GiYcQP}iR?}Sn3s80D(L6961FRQI4T{I@i8(jo= zZ9AXaJimVF_1k=D_BeQ(uQHbPu;&);xarUX>Zo2yQ#(Ho_i|o!xD7X^Nc@Kmj>h8o zNHha>!%_7ca$S)?G@nU$tIa)sRDGvT5+IWtY+zy<7vcg8n*;@?b<)O0oi64QKtQUP zNYsy%Jv*v%K~2k!n)i7H#O`bUzX-e&0PG!iUXG^~1__r#T8m(TLWG#gsR|i`xFWH| zO92VU5q70RNu|*g%BT1F0o$gQ8S>@C1lKE!q_Ucl6*Mb1R>Cpi6zDQ;776f}+5+C{ z2dsf2X7ZDRF%|(bY2hG{Q9MXoj2Uq z#|SdF_l@ly+cmaxY{%I4v29~p$F_`Z9@{jwacslb`mxK$){U(mtBhsFB4ew@Tw|s& z)!2$L;TSfyd~E61lCec&$XMU#?$KSNJ4bhnZXew?x^;BR=;qN)qZ>yzjIJNOe01ID z>d^|A0}vTqHR>8QjjBdhj0#7w(dDB{N0*E)8bwC?IJ-H!I6FBzINLefI9oYeIGZ_} zI2$<|IO{o=bJlTIb1Iw+C&F39adAu>6=wxU$iX=Sw6CKWXZ^)5oDxqc=zzG;hn=fhPMxI8{Rs+Wq9-Ors0jl8-~{pUp~BUc=d2) zI5Qj>UN!6*HVvzWR}2e>vEk*zONW;XFB(RM`_SF!E_5fl1Ko~pL${(^(9P&3bR)U} zU5{Rlu0vO&6*PlJ&{e1lHK8hW1u8@_bUC^dU4kw`5ws84jqE~pB0G@n$TnmvvIW_U zY(h378<6$L<;Xf@HBvz`NCa7hxDXSfLRKI`1Vff1OOYkWA_PJDSi4!fSUXueSle0K zSX)_JSesd!SQ}XzSnFArv(~Xzvns3%E5cgEa9}c$ii65SxZ?xOH&L;O47l=1zX6**vr{V*-O}q*a*9CWcSD}FcV_O$o7$KBU?wdjBEyz zDL0O67+F7Z`N+DF)gzUW%t&Np)rf1vG@=?=F(PDcV{T<`VQvP~DK;`UFxN9LXRc$e zW>%OPW`wzl>0+9gD&`8NkclyuGnX=#Fc&cqX5YZ>fn8vZ#g2jP1KS3+4s03NJg{kC zV{q2il0oFi&KXN2`H^jry zO-;ND;$i5W5D!Ja2k}ejcOf2veh15k>z8B7$B8 zk%fK};t+ae6R&9EEW|VxTTjGh&#~fCY}ZHPv}aBFQ8{a z{3Ch>#6O@Oh|i;Lh|i%eh`&djO>{uqj@lvq4z)GW3h`Og0`a$~8RBnH6U1jwBg9{$ z28h2x^-a`4+=gnKsDb!PR1NWIR0Z)VR0;7(Q~~h`R1WcRR0i=EsI-ZaCW;|$MOQ%l zIeI$8pP^rd_!xQ`#Gj(4Li`DO3dBdzlOg^X6+zsB3Y#c^_z22}_#>1D@nMvN_(PNn z@gbCe_yZJ&_#lcw+>A~^{62aT#0SvHCY}iKeslujedq}g??sP?cn`W9;@#+R5I3R6 zLc9w-2I8IQvL+r4@q6e|5WkC#L;Mc96yhD|kr2O)9szM9dN{<}(ZiZ}D8$>)FG2hk zdI-c@(IpUXK^H^389lg(2Q~4)CLYkl{UP3j?gw!Lx(MQp=vWg+A>M#;AYPBMAzp`$ zK)eHt~Zd{?2;B}*i2e?u0DTrBAN?&v68%jRpMl6le+`j9 ze+3aow?V|vUqYNhpN4o6`cxC2Y~mA5e7uRjXyVo;{=A7lYvN-~{Am+^(!@t0PNF}C zcp|z5;sp8##1qgTK|CIP7~*pDhfRD4;<4xtARdE0*u>2cm!aQ>cr^L|#G}yrA&#T> zL0pR73-L(wo;tEFK>iBxd<6Q3^)&?ghqV@g{$b4^&_Ap-2=os?=pTU4KLDYB07Cz; zzKTHquvR0`Kdkc*=pTU4Kdf^R=pWWs5a=J)ISBL*>ud!2hgCzMe*i-N0EGSl2>k;P z`UfEN537nm|F9|u^bf0yK>q-Q{$Z66=pR-Qf&O6?5a=IP9)bR0D9)Q@->JGWGzHFG6PYDtZCv`o49%yeW35ezG6S> z>noxhtiAaH; zvKxw3QNnKE`XX5uuF4yd8Ih}uD?$Q!K|thVX$3Z(OoufNQzabnCX!m8FJY%9kTM=@ zT>W$=fpI6x({7W@TuFl#GCsnlPR9+AlFgYax#Vg+R`4ig^1LNHu2h1*t7$?d6BwIi z&dSu(JnTc8GNW9xyeg9i9U0xEO<^*~OBq27pRDjhv4AE_xFuL_Iuy02YU1%=SX!!S zGd_7a30^&?B{pj7ccOXmK2ac*Du;CZbW#|Q6!B0N!?pe>FPBi)++2RCYzAW)`5|{g zJuVkZEMW9l(OpdHBRXqY?WJ1uwx&>(Qbu~@Zc&bBQ75HkjlxrlrUb%d-fZ&(?D8B> z8RrwA&qByQ9tp?X<#IXZ)EL6$T+LXZriS-j(GcpXDkn*C!kLvhf@v2w$rplIcvTIl znfB@2fow2loK6;`Xutuu(O=zbP~&BJ)+bQIO+Wee$Zi z7SbrQzM_jq)VSf0r{?o2K-qh=qEVK|)wV#=YR<&;DX%H5Qv1r(Anp5cQ)t8xa!Vv0 zc|fiV5CUPyCo|e6OF@N0miLN%cFbwdIK*yqsTLpirV^nB=F_8G=+d(ZkzE!vnvn{lNHX*xAcxI{HYN~Q`0!sBI0+%6Hv zjJ1@pTFL4B5o$UoV@p%0Wl|jvhl&NiUV!Jz@yU!(?DN&U5i36%Q7J4Lq1BpqI?Ls% ze%z7-!>CGXwYh3as1njdftnoHcVAPem?(w~MyEJyNyj{(e%k0ssbqN6mx_dhim*JU z^_v|krA;rlkIVHDh13)&CsR&GDpqj0s0qS-KWv!PZJTu9C7x(Ht@NAi(rnUZb1LI5 zycXuFG#NK{S{!l*WqgouogXikQX#FxnzJWDhNL?kj8ZRu|E5e6VG^6FHowcqwWx`L zMVGLuGKFj`?#qBTJ2r!-Di`I7N=%R*cg7Mnsm5VcXyV?Kv=CQN_x+iTi+9UhW#zOy ziVrod&G95t*8`J^F|rf zhN7+-&o&uTfbvj&JZw&SE7R$sKI)X^a^jRbRLNz85oNky!uVi2l{98|(pWJ{bY+FM z=8hYvdF_iEicb4$0;@%V2jv)HR|ou<)KkkCU4>LJT})>Rn7ZckXaqzg?lUcJ8<(PqFKMpZy8mlkvie$gQh$1BqzSuUi}DoI?e%Vzw^xY0cxC`z?O zZ%Hq)hAcjC?B@NH-E>S-QNKr&iA`rCQfDHX4*H`mb6G3o+AJQIQx_^kB64Xg<`u|f ze&Ki^<#AN48lBQmuxs%hS*i&|$LZ>W*y^cUxRY+0OXAf%1oc}1Waido%56Hw(Zy{55`@La1_2zU~^Wx>X zqE_fm%B*DGs}b5JgK4+XQ6!`$JYg`)%Nl`4Zr~TpRhfERO*IuR+Op!blR6CN;E_#z7Roc#=ltKsrF%-*dcSg0A?fmC z{J2sewko`?v``SJio{%--j#G0`L>9&n#zyMb&{~etG9ym<9JMAEhbfz0PX*JL#SU; zPFMUM1wU%ZWy*dRPaqfJir^$J3YkkmyBPDzgF>^^s^rSYJzA5dl#_Z?dX-5Y&%0&R zsPA9V6ed zc%_=y6^(;?NJL?eKTu+@9wjIPFHtgZ|FL0O{twCB^Y7J^EeDD zW!xcl2$SG}Mz4&xwImr#=be_C4U3h-<3XidUQoE~uA)b#PR4^->h<@Rn?g0ViXwy) zg^Hyp^YOJM1u;1(_t-$yaAJTkm=34?H*H!8&#`>(wf*EQ>hg$rO7uP)n?rx z9uf3evM!;)$u)vgFB$iX^>Ka7X^GgtkmNk|0qVqtLA@E=k%&hE>H=t&mLv*QZ#fqV zO&6W4HdgKCCldu31fv?cf@Eu?0CHu zl}!nbor+FI%CesPWSKOnlQAb=Q2BXTFmo$g)Ob@SsVzKi@TMY$q%`GlWRf0>CTF9T zP4y2nHT5J47|)Y6sdCkH8VgAbm7LkYHFKlF>6G3Z))=!PRhTQNkmd1EC>pb+OnRR! zCv&N?Mi;flr|(Yq8O7;NVIGU8W$diA2Oq z`a+s=dNL4^C8HW5h-tHmge6lVc&dsVD{>>mv^n3*Eov;5Mz8+fU)5=}J zs^3{mMKDqW>UgCFy*mmf=SEZ>aHExqG*(<&HW_((dsD|kFkhJ4(?fOLw(#Ij5uJwA z9(!LsaaK{+@bu3#99(8H6^gQOK9O<>vlb7SgBu_fe2pPdE0a8)xR!CJrnO+i>14sN z$J6cEjC1QdSl9HSo%Np&-#c%8m(9xS`c8eOFLPNz7L0Lyu6&@BHik60s-hMZmkUNP z2U#0d8N-BDtXC;`M7dy+H!b>UVs(CQJ(+b4A5_+V{@~Pk>v`;~tga{bGd%~ExiuA! zCrp9?u4b1vQsQQ~hLBF@FUke>q}UfTc&52ht0|lHnf*;Y_s$@lThoEMj`y8c|M`G@ z-kL6*mDV-g>pbdTmJ@S5A%RR{A#K$(8RDk680pdGmHcYet_llCX*x5lkqUXWQm^L` z+(CCgS=aF%-Mn=?VpcNNhcu2UNu^^?97pGNX%pOaEb|zsyS&iu|RYAq9QFx;! zry?1)=e325gKQeJF|=ZCGuCyxa|8VO#=JFLIxD7F5nU$5umrE1Rk`J2Vb~%ISButK zmG8F5lGB1r6!e(pmUw!-O(_>(O%l&_qfKEd$ z_1<{I?3XDT4lN7#!3ZReOW@?XqGeIW;}NE-GNKp}M5c=hxo)~#@}~t=jlg18H#LN3 z`kXd|XZls{x`tP+oVSK2&Yo7+u;ok-x`aHO8_7nqV!J)e*O$Ywpj{wy61Ji(Y1HIN z5l?9dYBjb}ys2S_Gks2d;hFx8Ep>gbcz@pdE}K2Iu5Zhk?wi)e^CpvoZ*_w&$^0sl z!X@-hhK;%^=@La!`eZ&7kBe(-KT!h@{GzUz{Dgk;lM-|}oO!B}NdywKL8VYa7%+lzrq8J( zJk!_RU)S-%L+7pI5wjxdnI5&ulwMb$5Vj_>aqzMumW<15nUdKmh#CayGWb@zps&ax z`KU|SG-mTmpHs!UZeN>#Kku8jhD&FK6e}Xjdia*Lsbcd?pK~}8JTFUU)^#lXnNCN7 zdKxXA6;O0!EyFEhm5>bCRfHiF*XUw8t!_GB#S~(3CE>AX^O}M=TQs`0X=%Dg9pQ8wbt^#M=8yt;-hC+4I&fyJd3A})@VygC(#JH0l* zcS#9MoeLUmf?OshGdXqDD6!{ht;2~qr@rvSjQ+K5(d6oR>$_~0tn1ryV)jrM+VuKf zC+3`b!V@#}VqMS3_`LNzc9vV$v*pC>p^mPpXUmB>r>5}4^gmM9H1OB?YTB6pcR(Ne zKIFNM=Pv`65#x(b1XKT$huRhA7({w(5hoMOI>ZpC4 z&Nt^hvRzO<46sQaPbARxxjkH(|F#D`|wvTo*%!QOH1P!G3? zy8C;oRIJnYaLepm+1SIAR;NxMjYq?}>i>rB9@Ixg>PBrWzHgO{a70np%EtPPy*XNf z(A}FmDZZ~en*<{XfhHX*8ztUiMLd-dmIDq|Fsv+a4OoN}`;1Nl4%*aCm5pL;iU;z! zrY7|JT-oT6=q;XHULXvB=W(xTLckYDO@t#F63J4z2$AHOGAUtLCx}JOF-tyUjszw= z;C-OfV9X`rR+*XTTfon1F` zr=86cb!uVoh2Pmdd_MVK-`Ug;SPQwc;f%(vJG} z=%+BPtcdfu(x5(SGY8W`k~EhCN}?uoXcbYv(PXsl`_2{#Jqcm96r3`Fwm&iz83@~= zW>Zuon=0cagF#d!tsz^sDD+y@CP`i+PL{(NixFf^DDWa*AhTOMdU324_mM@uJ|>L% zE3uk-J|oM)&aPj2y<324b^>+_IGoYT$LXGTHf?t<;eTRh_s|!i^F6zV&-I;lcKvIJ zf72bC`n3dQU+?`uT<^=%z6tkrFA+i^?Osw>h@3(MQgNJ~iaHY_@YRb@!& zj)ZMBl{W0=P8t3CUjJ_?scqc2J)tRiKwqJ3wqj9dV?4JgECH!{wz!{{!E-s8FKA7w zRV59EM@`wJSm>9O95Gu-uNOum8l}Z9%-6ILw;}HLD7{XzbY7h|1ocaA)c^N+``+qf zUcrnrrOZPI-Whme;Ff{2K_7rkpzePasPkV0>iV~UI{pl(+g}Rm^tXY!{MDciALAgP z-hMNv5Ru7Eo1<)G?*JE*c>2de6Y!;3)m{1#9xp8?hJOF=dKHc=sIxsOX(Eoh@z5N&W`}K@H>Mu zgJ%qK2S=EHVLmkUuc4=g?i;!m)F_ySj_7-9XmIeEd43GX2ZH^}aA7bZbk}&?l(AqI z)&yZ`UX0-eX@x9mL$-{>&okHb8C3*R*+;KGZU7W3fWEaDMsfu>mn3ng#uTUK`*1F4 zp(9C>Cz7b`ZfcSt=ew}jgJZ?)11NHE+rV2n{aX>)FErQ?%4uA|@!Qgh}wx3tj_>Toqm?Ip^&xs{Gf@^HB( z(l%zBb7LzVMv@|z-fL*HU#mX|5lISNNrj4<|G~MgMIHg}fdVy16`{USU)h7PE^tbr6Jy=#maMN{UTeeEUj7z zpDm~t5WHy4X!Ls`B5uxCaWhX_GQb5V+}_DhBms^Q_TA0HQaK%vm7`eNnssqyUb!C= zhHYVSt>%c5YM*pcYZl7`WxFRxGT(2A+B;{dA!<`Ylt8DmNlVVSLmo1y!0xWu2uUs} z@DUz+qGIEv3WA(nFEx1`X2vTGQG2HrHALOj_=mkSdK&+5XJZYXGvc1s@FUKI)+0-K z%5I^-H7$|Z`RZu8U`?C-DvLS5^_YEe2Dc$5a0cRPz0TTpy|Gq0ur3zY znKB0I^+sFhczo~?Kj-mgsrihYU@IMuBm@O{p)G-)6KJIaUm5W8!DyxJrtWX0uj6LI(~Rg0O}YsLD>o;`ch@xY69oG(om-6g8L<67xJLvvmv z7!^}*UYuiF>2MM!4MkTaK$Uk)D;-ASTy3NjY&MLt5#CB!+u)HA!3CFKMI0YAJuYZC@^KrQ>l$B5TyD%~S1sa4Q{{*CY^&m2Cr? zI0v@T@e>}Gnwl=oIiQse=ZbhfnNC41(BbUgN{4~Rce7k4ZF|_>uZ4~;0*_htykFh+ z5WT3CPRJDz>VhuP_K+~vN+;loaAi6bQd8|R+DgagimtKJSKms8_~ z{c$UukSh?XE7DNg!|_M0bONqG5O+Go)boJ-VJjV{NzuX^W zyu|SLy$IOp|D1c%fy`jg+)HkCFZsfAXSJSt(*aO{9+IVdNdumb0{i!@Ko3d2^HE^G zo)zdJWqUpfEP@L3AatQ!FxImI3+#eXs6Y?-<-H_&&&LHEs6Y>?*~@xJMV&kO=ytvw*pE z^{l|cx?sb(P=OwVF0czWe5Gdv7T5*nKm~d@UKchS_1q0-Lj`&$Z0IF#ZtiS@b2rp_ zR-lK80`pOz+Oq;ZV*h<(zaG1FtOUCKe`)mf(I0_6{=QLibO?0lzk_oQ$G}<2ew+PM z_Eq5R|H*9j$P1t^`ffbi$o|Kltpc2F8=0{r=zI{x4f#a$seD=d4&B)>g1T4ZBb|5!I_yoDNQGTMzAYoP|RIbEp9yb>9HLfOOJs0CEL z(Qedxzq5t15qjzlo;P=+ZuLB{iK!cPt1p3fj6G2sx>2`!9%!QPM&0Uppoxy2niREp zUfYej)t5lFrYCB3H|jRetGZFQdLC$^>_)x!3wanPif+`cCj_v!yeDc|H|n;il6IqR zjVhptq#JcxoQS(ox5f#Oy`l@Xur*GAy-)9n`pey@TcZkSa#}a));Iy0oZ1uhDcz`B zqY7wpayRPMr~;aZx>2`9m9QIiYn%Ytf}W`P-Kg85ir0<0HL8FnWH;)zsN!~^7PLkc zus6{YHQtT7Evm3?)U8nkG@0r~-4-V&b)#;L6CiuC8+B`(0DGU<6ZJ$l>ei?Nnw-#$ zx;0LKCdcfd!jz58+B_`0Zk6X^ zaT%!pM4kOnRTsgkyN=jcR1xo}Jx8xc0oW>t^n_vpd!- ze&Kg^4=?Kf*LODcW=g4AJnHl8Y;w+>{W5GsX)A{)obC3jh05T}zK zTW-o{Fjs6-B21W%Te)P_EDcxu$!K9BueQj%(pbbap-%b2VrfJ!i)X72y{=G9=4x(| zoZkdxkdsQ1^=jnQED5WTql{h}Huk);Y1`!pI#nb8yLWaEuWdWuvwP?q*=c9P3FIK~ z_U<3DKzD5F$0O>#-ZT7MytKafQux1nmnhGpPe5JdKJPe;{$8rc3XGa;LRE4b5~-q) za1&``MWQPx6G0PZ<~t_hj-sQcD^+vYKJPeB*sbu(Z4$9CP!yX}Q$o;7amwecC`Ce3 zlq(_QWU5FyD=M|Kn)6f&M2VcJOhv`6jKgm=k_oBD0`|YniN}pLX(%g6I?R^%{ObRO zTyW`)`hV`8`u`rnXyJAo|NrEU>mlQ+(~j#@|NpQlVF*th)l_k#CTv7og<=GWgSG>lGch!&VAoYilAO!s42@?zAEJ_C-MoS zGcB{!1bA9b$W;NtI3-Spwbr1R=P}#0R#`@8FDVVAUJy+9OF6%BiXT$vlnP8=F-;h& zLB7sjOi07?dM>SBI@n9~&;KX&UEMc!`|U9I9UAg|zcN~x7&4ebKO>&{c1-FNE`zSC!a zbL-_ltzCE3?CPH_%N+L5@87v{#fPMB*4ZKs7vVS-_JrCB4IAQgx+?}P=06|(t4*O- z_A81$86QD78zy99mVyHo^s!tyk~chKU3Ls%n?6Z zzv<6>)|NAlIrCoj$FB!}_m}r{V93GVgV#zMHbT{_+jis*ANJ zWPb5a(Qf{qPWo=iaIW<0i666cv({E|d>rG4;vOZn*rj2+%sJ1fuDo^T-7RL_>!#}u zx`yDWPp{l{@zK8ML+5)gn0QS$YiSk7!!e#w8%j}YvKrzZ-|t4wjl1HH7fwIbfL;HS zTSO}5VD2Hs`l+v3OvieTenB^DZWTx37zsLKhp8ot4RH@2J-zt^=GSmHaqC~#9>rM7*ZrbodHmJWZvXTB z4+LF1R+CK4g(ooYzUleHb+g7+aRd&!Ae%ijVZ3@XW_bVLw}^Y5f8pETy-U6FZS%y6 zJMVMcJaqYniS@(Z*Xw2tt>Qow5;#W{LkzIx>#>7hUzI@uT&(XkOS)^OSfc@|I&(0|h6;W{iwV z@^L(#(+2g@qF5c#W<5k&BC;zLcrFkPm&LeBsHwRdg0EqcFD^}7;hs4DFa4YQ$4|Je zwBbnGl9ln&a%{l-(Q$motQrc|8mb0^m=Fh3Ax!DVlM>RVjM{@ftDx+SSDZyt#i|U5 zjDd1R-w?d~50{dh)wS}whddQxt^L`B%4@Yh{L&Q%-FfrvdR)yhkVM@N$J&~xVvver3-jJ6gH1vPw;+*C7F1#z8CY;Fj?a#Z!C@-0b!@a&0ul)t|B zu0x^|!W%0$JiO?mUq12Rh>q!)l|#WnZeH!;r_~OlCurA7CNrsuTg6j@fbffxVUs?J zJFH%lak`WiHw2%3HZpw&Vte^4(}r_zJI8h8>;aMA^<6XYQv(gr^`&|ZNBj&ob%)@`@ei-WX3Tog@QwZl-DhAInr4{(M*(!Ib22%1{bE!$Q|CO zT!py;(^`i?88+G*8lU&zbN>5(d-I)dNE0XBWqNP>Z{H=paZqfW^{euE$6FPv|LT~P zK*3-jV6Cc5NQ|*$%I6Iff*CC##eEffwZt{&!X}AQ6|ZT$Rhh0SIPuQCTchHWMYkt! zPu6B17;(I1op@ied5r0O>B+0^Vc+kV6+^+LsA)2t58A+pC%g!HqZb|gj8SZG7P2Cl zF3gWdRD965x}PN@(VyJ>^D7LF`+sogQ4gN=ZQ+39fxDz9jo!NU+)Ivs{^zTJ zjaNXy;#^g1uhsC7UXUU%m7?H~Bm};otwvgWD$MVxWos2oEGij`&8X=A;cDxt|2XIA zSDqI?cE2h3_wQfv%e7}5sXXNA@4mHdEPe7zj@i?p;2x$OHEg_i?YFKvP?&r>bIU8< z(a43r|NHPpxB0e@A3SnD@}WxU;}OT~mm8*-|A?Z7;D^J%So+X6|EK@mBL*ROp=vC}bo8Wh~aT%m?wSNi3%R=V`-eK&DNZrgbUe%Ga|1CQVT z&O2Pq?wyZ3_3Y7(*;Aq59)o&p8;FvF)I_|y+mqa(*$f8Gi5Z;W33!r;y;g1;OUy~KXici!W-bAEKc z%Q1U06x>7oXhZOQf?NK0|Ldg>M$ftC?Zv{?k3Mt0@a7GN?c^^XU@g9W(|zEe7eT>2 zR7Exfue#118a-mFAxUp8=3>Z^0gx`xbDJN z-yX57|NC$%eAiaTEEfvy;fsxi;Nqhfz52}$&nUk5ljq#w$BD7{O>Z3e#(|Q5J^KFg zH;+17E^*8fP;d`l12hCz&lCNw61!-_ftJxr9z|ZTZhH3jr(CiA#l&A;Q1D*7(C|;k zEDiz zQ~7Ye@sGQYeRJT|>;4kXYVTY!F?ILz8(HV?I{eaS;*Qyqpx_=VWg3DnT>7(UIr-J& z@0b<1E{igszIK~-^QEs}a?<@z=zc0pTzDSnQVj+7P^{7r{PB|?s`o2a^nB9=XKp(- zf9qfWe#75Ry7}6|(fr>Z_wY@3pIQI>uk3rIZ|oOivtwtBEgSv&=mVqYflhz>ah~Jc z!bxy=>`&M~2R;7W?4w3r8@X@fDg1&yQBKII?BQk`; zdX{w~=;((JeK7Qsp-YDBAhZ9K!A*nJK?&&H_Z#N*%n);GVE4d}2QC8r{toN^YyX}7 z<^C1@DC5_l(_erw+4mk$XTE>PwO?YMc3Ph|l?D?;G4Qb&!-#2Ih=XQ*lRPd#_xI%5 zL+IHEQD-*LYHsb4E^GoCHUYVIaThi|4I7_advF&v9t|6hTzgO#Hj;*oB-b9;g^f$Y z#wFJt(1i`BVZ+I_`*&f(XxK1v?SAxZxUe%DE+p43>cU3j030}AtP2~BC2?TM(JpK> zF$r3Xt>tuKqcI8iGGQ&7o(-c3WekM!NEbF5OJcy1!(G^Db^r!;0NRC(#sL^`0Mdnx zW(Qzk2e7)Z(S$MvLV2hQ8%-!jgB=j>!bal&K5#&+3meVG;e(A6rDx;O>;N9v0g*0jG@%TdU(STPu+caGG{~I^ zbz!5iBoA0J*oBS8B%pY2CP2?d(wGD^#GLVWVWZgr;FdGv>%vB3NzmbU#@mIBX5)}x z_CC)BvG@%T#!)6>^ z*l64h+RM+_yRgx?n+x1+>%vA8lU&~L=)rx8{%GIueq+BJyJ0LjcIw#3=yRjr9<7Wj zIe+Ec$C=?+IOFVH>_^y_u~)KBM0cQfq35D{^f2T#V>^TU$SgZqX3EXK2pZ!roCDP#Y>Klg3g^M%0Z!Hfs@{Qh73 zKV$)n*}t;C{~(%db3t3x3({aXkdA?xp5D)DFdRU~KuzoGA8Ih{Psh+!$~4$u*pG&x zy^f99U|2-Q&{{?WN__?z3}bW*Z5Q3&U>K!ipr-ovGa3vWI)*kY^)(pSbPUw;FvhlVnh9NqJ zHY@$J!7xb2(00+g8w^Yu2D0s<-)}Gs&@r@D(_t9ny#@nKBMvZMfs%E>_(y|*LAL@m zyczE{82acKsNv0c2Qo1JMaR(QhqoIHpU^Q-!<+F|gW+R3hBn9TYA}36$IuquZ#Eb{ zq+@9F^cxL^4`>*;ZI1hUgW;ca3~hdRy}_`Xj-k!duQeEGC)>9L-`^Sx@6oN$=D1fI z4F8~GXmi}o2E)5_3~jOWN`v7YI)*k+zuaJWn~tH)(|>I+yhX>*=INIj47+F;h&E6E zrNQtf9YdQRUTiSDLC4S*8h>suJV(QTwJ|&e8TyZ;VGvQR)W3g&;Rregs+IcpYcL#6 z!yu$ybpN6T!(ns`)bQ>fYcL#2$3TtN{?P`*m*^O%vDD9LFdRb1(00-62E!5>hW1Vj zBMpYdbPR1)8g4KgOvga=bUz9i7=Nc>AgE?#>}W8&PRBqsE8|ZMhS%sA+V=Dd4Titb zF|?f;e{3+kO2mJ13_IxUK3Cv9eYR+<@E5uj+Ct;m2E&VV3~izD+Xlm*=@{BV<2Mb4 z9drzBq47+E;ZJl7ZK3h&2Ez+<3~izDs|Ld#=@{BVV_Spa4>Sx|+x_8}4Tk6G7}`Aj zbp8GRNME>b^qZU$MjjcqA+NH|9Xf!yao{w@ufbn0)Sors87b1ga(u17uaEI0jd~>2 zCvuO?tTmb~a=Bh6uDFRx$xSFQuTxZ1E8*{XuToSC$kL=yCN4{3;u|BJee2wxE)G)-sjL5JxMhlkh&qQCk1E;UL>G$ zR6SNntTtJ)hdgoI6R&0pJfl1stHvW1Yb8Q*LsqFICE|zuIaMu^6{mrzC7+u52|80# zgHtVaX%udoOCtV4nL4&WrdGOnhA1y(NNOAbho5V6fO)uBxRa?<)uKhF_NOPg3Xr@$ z=}g-F&YCq{2-Rd7s|GJv^mr^mgbc@nKoWac?)J) zC^G5JWX<)cUh%1^e?ezz>IrI8nZ!n|-J-28|HU~#WD8|#h1YL45P_OT@3i@2`c#B( z#mEk(mZ;@iJRr#sASqg%Ph<)r9Z@K_Eb(-?LSRaHLhH~JymEy#5D(W)efp=S-b!QY zwojo_huS5U7_=_?7s%8#<3dHXB24J*c*K?DNlXq=(#_4Nz?Aw9rk1(OwN#eG{L@*| z3+@eEk=mgNy4+$LI7af-AnulN1qpH5XAPF?rvCD$rv5pdsi_ZiCcRGLGHKK*{TIp9 zhK0HuzVPX#e~2ZCIcQ|7(+t%u2j`- zxB9(usd-5{e0l%humA7&`W~HgukN|Jy1MrqD%%RGY%7_eYttdxpc*<|mc2tG-SUTE!+cS4zl#z=B$Sn_GUjEWuC)bP+x_{Q-r8p;gK zX4zkE^ie)ltz=U5TzXQI1IaqiYeKau+P(Ts&o%Wgoz&FMR~T6q$P^yS2+?b)sZZ?{ zhD_JEdX?2%bcms7ACqa)napvoFql$kwT(WTp@+2+lR^DOje|RQ*&8k?JqpoA(L85n zWj4$O?Ou&O*VMmwQd2u$`NeEX)v`=9d2KxwPiqE3B2y0OW)d}|ty77yEHYEZH+!?Z z{B03W4SB4QF{@cJ&sL@l-?)$WR7S)bC7ApXx_40aU0A|f%gK72i!?A%($N(8= z6VpU=x$q=b)vZuJ<^zZw>SwTey1DQi0WnVut`+!_bVL^?=8kj3dZ*UT^#@Je_#!#r za|Qq(ySY0kG;77n{LTtF_j&X?I2bw6@2d6D$Y-De2J^Zw>MNxnFGVHVkn>bN-pGW7 zNZxFLY~$wO>cdrrqkN+hDYYjUcCM%hZQYz;=cXdu zB+vDWddw`6I2!ZidjeNS{nf%0$GRO}iNr(HII?7jEvxFl@9u3p09$@{p}2bFqBEV| z1JHk*-?=zeX?0vT8PjYxu)CB#)Z3;3I^H|nU%;Gnb*sy^-e^6w-q`Jo^nR`huGsuy zdD6hp`&MP%ee+4vXy+RXC=5}K6s_&MwPL=R%QtIz%lPMCdlv52{-b5%%pQl4v-q}`47cSA`S!(2 zHEPAi#oxgrJzunHXBVoqhc(l>g?0m6e9ZfSqR-!O6GkJ570vV5C}e0QKlDes9c?%y z4f0%a#B>V7I98XleIqjH!$S6aET|NiJTV&~jamh)n-BV6=(cWpvjf_(+A+*GS)Y#W zKj|f3FbKXX=W|>M>wrJqT*rhcSf@mh_@Jkrk9A5xQ}lO3Mqh&inFQPROM%P;F<7nE z4>X#-aBn1}&m%sKK!-sYD+C&(&qw+*p)nC^HQI__9F4@~qT||Cw;uDu2hQ7%`N^sq zr#-#l#`o_XZf?KWMV@mIFnqWfudE9=hL6u{T{pO+^+9j5zZ5=V*ml+D@&9gGVO9?BJoxOvbpP4?;od*)^>?4y zZSQ<~r@sC4b{YBiI>s_0Fys2$GxuJkF{OPqP)>zM>7qEUB zAKcgw1Oa+3Xy1vU-7b|lDO%eZoi`bd=eY`3jM5CNb=!{pr;QJ;JG7k?s_l}tlcKMk z$&zv%d>Srwi$aHNq}!S3724Ju+D^LGC2c1KXIn;_$D!?{YhBWIQtY;6w5>X{oph~B z+D;1Xwv4tFhqjZhb(ywfBfq_{w)*{dZ3x`+`y|kI?6odwJ0URK3oEWQTM=m15En~rZW)`@(>XD1kam!fn z?wJKHu6pDm#@sR%OlKCjxayIMV06n^Fqv84;;Kh3qSh^A!FXnYi>n^F2yeHH1*4e- zF0OjyA`ad%77S+=xVY+(i-38{STLAb;38Vyk&6I$XQl(Zr*}GByPRoc`$B1qIUbyM z-EYn;aBXD1sd&^kRn_1xEsz)w@^etmS zcV>Z$s~)+C=C_OmotXtLu6pDm?B6mLv}YE$xayIM_<+k;(3)A`;;Kh30tqf-L33t- zi>n^Fh&Z^61&x^nF2W5SxriY6!pjnO-Cv(s;Nq&Mn_T3s`)e}`Tm&#YauJhonKr1- zEO2qv)BPBG*Zq~51uj1Ep6*wuyY4T~EO7DV^mNzbt?&P@Si5s&GrsZ34PpI%tlzbM z2;uV3vKGc+1p=ox~l;5Z!NP0 z#hUW_0A1) zu|RW52071b%?*0P+#nb0HK$|H6{ni!2A#Wj%a&ihIVFRfrzGYEy?$!CbWo2o5jvEVa1^TS9(8W5+()z3#&33<4?mG+gSYx4!)sm(4IL5-) z&TpxUWs#*-EgTE|&O=XD1LW^}jb#t0`l&9xILE@Hd8J*f#4J5ESzKx7>4CXp-7+`G z#lp-f8FbuqJa^yCz#eNF<6@a+X?v_`%>M3cR$jU0Upe^xgSYJe`o6aJ*L&l=t(_m; z!MA^BI}ber-31*SUO2pQ>pyS3Ve_Xq6B~cHQCn~aVnzfIt`FFp0_cb8fpRoGW zYG>th;8OqIAJ2FHd#~V19#|iACGh3KUv7c+J?h5F?e9A&NMFGDggoi}+ga|W+toO++6&{^&zg>d^Gev!f}rq(AY zyrSF*3jJvN9&VvR=hXQmh0by(DU7u5VHYZNI`l~jo#jqa2ppeTsL<)qCn0c))be20wA@D!63eJ3e&mODvdUx^zf0xhYue*eDLu8!+Q_!J{%u*537fF9IA)>A$54$ zVdM}#y!G&=!|M;PJA@8b4xT-D=HTgrrw*Pxc;evkgU1dYJ$U5c;e&?`9z3}J;NF9~ zK|X=*LG|E{1NDFh*#&Pqh#bHNw;tSdaQ(q`2hhO^IC=2Q{?q$U?LWEy#Qx*^kL^FY z|H%Hs`w#6uxPSltz593XkN3O#)%`p6)qQ@S+P`f-vJdayx_{ID_50WDL;EXx&w?BU zPwzdo_vGFayYTLvh(oHLpu-d z+`n`0&fPoXo$gL`=Z+n9hu@)gZrh3Mz&p3@+_ZE3&UHJ`&dT<)+s}Y=5KnDCx&6fU z$aio z73f*8it;q{6!awY1oSxc81yLg2=p-Y5cD8)KXfm2H#CO2P!+lZWK!TE3c3x7KrnPG zbQ5$vbR7giD_hTQJ+t*RIDzrx))QNgZ#}m4=++}!4{trR_2Ab1Tla3=y*1wIf_#g2 zY^huP7PWQTR%8p_x^?TOt?Re0+k&=MHlN*mX7lOIr#7Dixfvecd~Ea4%||vL-h62D z!Oi*3FwXuiw0G6WUzacy{BNji)!B0%t#- z*m!*7v5iMTHphoI9@=Gh}9pWJ(V@3Fl{_a51Mc<-UT2SHBBd-v`Jt4-a#>fRlD>K?yG?cKH) z*@O3P-MeY;`n~J+puLsdXTe#Nr+1&)eRB7S-N$zy+kJHRk==)PAKHCz_x{~`ckkXE z?{-11v@>yNEJy8g)e!|M;NKe&GX`n~ISuaDQe>(%u;*41@> zom#(bJpzLB-@1O&`t|GAtwZZ8AS2~7YfrB|we}=9+wwTbQt{~8BWn+@J+$`V+Wl+y zuHC&hUhA$^*X~$T*Z4JR?Y2wPfz<~u{r=_Ou9X(pTJw0I1eCCuhG?59h_ac4NSotO z+~yb*vzdShn{f!YISNH>#vsh*+o0QQemnGbo8Jb#&E~g4Z?*X?&|7SNGxTPg-vqtM zW)wngjzAHc!%*1f5EQaG2nB5pKmnWmkl$t>jrOmH^USadgp_kiyJ#@XzFN0oY^Gl(Z z+WZpeB{qL0^p!Th7<#eIFM?iV^H+d9XO_L!LD$&~9ym6G2Zznz0bny2Fl+|HzRh6x zwiyh=HiO~RW-u(;42CzG!7ybr7zk`$gVt>JKpvY{p;en#z-SEY*!njBi$4co@xKCC z{8<2tKLcR#zW`YL&j1!b3t;j81+e&^04)AL02covfW`j+VDaArSp2^MEPe*S;=cp1 z_-_F${$BtVe;UBz{|R96-vC(r*8mnj4Pf#A0I>M404)AX0E<5bVDVo7So}!&C{xblJ{}jODKLN1#j{z+H?*JD65rD;i2w?G(02coNfW`kWfW^NLVDaw(Sp2&H z7XJ=_#s3Y!;wJzs{%rt@e+$6k-vqGuHvlaDbpVTh4Zz}G1+e&W0E>SGz~Wy9u=o=I z7JnST;$H%=_!j{z{sjPw9|N%X=K(DKIRJ})7Qo`40kHU|0WAJ00E>STz~V;%EdB`q zi+>!z;vWOB_(uUO{t*C+e;B~x9|ExW5de#S5WwOe0I>M`0WAJL0E<5cVDa|?So~1{ ziysEC_{%!z^zYDLj02bd5VDSY2i|+%l_!|H$ z{s4f*Uk_mM*8y1kegKQ_1+e&Q0WAI+0E@pGz~cV|VDVQ0SbPtF#qR^K_-+7;?*ae} zy%&0~&F_KUWAnSAciTLLrZ!KYiOpkZZ1V^j**t`XHV>eI%_d~pY(R$1eW-7959-<6 zg}OF(ppMOLsBLo#YT4X`nl?9}hRt=TZgUN)*<6LHHdmmE&1I-;vkvJtm!Oi(MW|?V z0i5`?>X(P|Hs1l=VRH`3+59f(T{hni-EQ+cp?BK+4(J^=YmjDh7RuVJLaNOfC}Xn% zDK^WHY_kMOHj9vGa~ew9EI@+IJjC0~L7dGj#M;b2jLj)1WpffrTD)~9fW_|xu=qUy z7QY+7;wgZ|699|H02Yq`EFJ<_JOHrR1hCiuu(%IkaSy=aE`Y@y0E^oI7PkN_ZUR`` z0I;|YU~vt=;wpf}6#$FN02b>27MB1lE&^Cw0I)a@VDTLQ7Uuvgeiwknw*y%GP5_JF z0bsEPU~v|}VimyR41mQ7fWU@-w;F%DpH6u@E(z~b8gEPgwH#cuffW_wlEPf+^#a;l5-vD6oIRJ}a z4`A`@04%;0z~Zw27QYt2;@1FJd<0s!aoS!wqL#XS9{;@ z*W-1U-}g=M$22T+Wd>n@7?UJ{rYBPb8y7Z|jT_cK1@#Z*!&hxQe()Cu z-+Rylkp!Mu`H7|8Ab`~$TdLp}zfffT_A@8s)Cz{2i+gDqmosSwORFAckzmldNXHYY zxPnOpt$3n~1h2@_g0o_bO)#pecwWCy(7Dz{2&$kGiEK>ryl$bOGfPEAO>$}`%W^5t z8x{&W*XN@co}|(uuCbnTq4js1IU(^_(AlWTtcpnj2uBxH7l}F7%ou?YIFjU8_MMBw zoZq?_MbOeXO~z%-jTnUse44{zDw|y-=3Fa{vQa9P#wl5q7l}D<9tdfTi<~Bk%@F-e+|a4JD0IK}hog@Vp+K(x%t8le#}*>mGUL1$l4B`hN+ zIf;U zJuRJ5G)xmHj`6Suy%p;-mD`_%`kZ4-Rv;uT8N*YgC$&(}*-1g{*(i_G$ym&jTqGF0 ze5mlDF_FPlBFXTAC$Uh_*>5x@Nk|0Cr$i6EP|(?Ll2VkVST;jo9%`YWv)@RJ5T(+w zXpHla3k9#}H&}*>Cm4m+Jn@Bs&VCb*%OcH3L1*>E77AY8Z+xgMWI0}=2txJ{3k976 zX(EefWDKLCvIk!zh&Vfd#E>|Kr4uUSxoMGL$ay3A5f{BzQ#{Q!vL!#uHK&$31UfBzVOpW-Q9{3?pP?Sx<1GptI+sGYS)D1ws=% zfklFW%LgkMj%GPAj>j31^W3&b(C>8QG?iuK3`VCkk8h!%v#&~7A(r6@EzNt7g@R6v z@hG2BIX01DJn%xn%R4#n4pqg{bX*lh&-sOd&Q4A#EEdbgv24=w#zlg`%R4y?OHmb9 zqB5zZJ=h{apRc{S$fPr=6y-U)P|#UNo=K}h7Q?`p^V)@i z&N`+@CKHb*d0O{Ng>OV6rXV;7E=>S3_ zIa(;_d^QqMmE~zX8|OT?fEmhvh&MN<_X~0&33{8V3La@u5?pVgnb2Ak}(lBf~1ub`IdsfOIjw@VW1GxK@kxWqisVy>C; zX!D>Sdpwj9IHcdF?eBb6QVKe9V_xfxbdAVHu8=B`G%1$;L6M^YH%2 zz{=xZz@WTdSgYs7nlC{~OcSfCwVJ=(!-;ZE^@{Pb2psdruem0hoL**FG$pn%SeK1n zCQ}s^sw)c0arus;Nh}$oM7Duuqei7NorZ^0uO#9`jB79>R)%4rv-)G# zV3VC%FJt?vzl$)6Ch1Br&B9exgpciISe0aOkd2%6g*yGQ>0@bB85Mm!zZf70MhsK= zkyn6A(LTuYy85HnUXz{L?4Rfu88y%=NG8pxO+;)rN16C>lMF1Tqe?iO1&LZK-fU=G zX~LD#1PjE(XpQ4jEf{T9yj(ksH=AIR@guYRe?Ppk|Kja$-FW@#4%be%uBz(aD*p`5yw?PwH$#zaUvEy7bwWOY(kL5W7tc!fXQ=*|r zFo*(+j?I*k3dJJrCedo8DLs^jeR42d%1qKU&!3N{tNxLOG&PzyABYA5SDlKG#5qD} zrRruQ6YL6gTB@E?672>7gXg%~)w?A@U?xqUf)eMOQNO?%qb3%M;hn%V)#ZZchv(3? zLBN?%nThwZllZhV47ZodlwwzXmJ$uUm=euJXL{inJ=o*(q+ zi{wc!oD%H{U;J`PG%)IX0aK!(u-|`TVx~dxJg2M0sXCP_Mt~c26k$a6*}|aJh!Q?V z7W|PUS(!xJD#(OnjlEZq60IEw<&mPES0j;pnH1ZJAve~E^Wn}pzTeMx<9W6`%#dI(V#(NfTB^It0V{@udXO%F_bina{!eK^b=qA`Ammx+sztcI29y7oZW^1S!wR!=wh-LemQ_)5<^#+R%=IlN;QV)TpT9rm1f2(pdV zI##01n*){|wnZIx6JP62UCg{`Jb{*)tL^NOh~EdqkL~O!r6dS>Pz>f{a5aNa_^&Sp8=<} z!@Hl`{lxAEcE#OS?tF6RqdVR8*2dTEgttGp{Rz)^t^SLrx&47{ar+g}pRfMH%AXwG z2hO^8z&Y1{1!wENe)V2(a^&UUEc*9swl{<8fsM~@T))a~{F28HPF26m^QY^7cK9n- zzH3L;^_P2&vPepAOyd1M9qQD3Vk=FvxPRQBhT0aqM6(ZwO37Hu5MUCGz6VyUfW-JEib`%?j(jh^Kr@%%n1`keA zA3IG3l5t8vYi&M8#^xIDo(p)lN=BD<7dWlf6fe3QR%Kb^ONl(%Y;_b_tZumRAmhg)8d`?p|c&-s_Znj%h9}Gei znQ9uLM$_nr68MBKb*e1yHH&z$No1sYZ3>p0^69`_5NwOJ1U1YzsIXiH>!mw&jbvm_ zYf}VLE3~WSNW2oOCyRnYkrES=XM*d$FxObCz(O}Vs-eMRIvyqUTy8Qg;;r#0+#2v07MYwt@kr%qV^~#|MypJUcL`e7TbvHX3tN zSJ(gSOK7u^)vD?+Q6I{QVA^0uVyqJyvFH$0P(>0&okiu&)Zaq}B1Vk5%&d+(cg`9| zH6?Y_5VF-KmDc%YrXNPZx+?0E>g5TRK?F7!nv77AKGmQuTFcZ{#Ofej2r-J?9q(F+1Xc#0UOtBK1 z=<3zDBA~%ISlAsEyL4wRxH8wMHUecTs9H=Arv>hN_~t;rDyA=`}CG>tC}xrts$?uABmNE*OBrBPlm&#{HE}UTNm@lP;#F1*BIb%RRBFX~MKCgy;k@*SPlSxgb($ zRC;8WXeCf@%C8Ly6PByC2e3aW!FAA0-okG^{?-7cgZODYu zg!?%xGE#Ir5sR9^LZBKQ;Q3@}ZtD8)UKFGaZ>yVOyWYZxhdaYAR-PuaaXmMvCnD$+ z$suA)z?2X-9|3lLZ!QQ=W1}Q!>{8Lls%l9a5w&Tp#UwZ{8jV#nPR+Flx-9g_;;dCS z!B#wbl!LqKC{LD=Sd7Vs`l&`cG!AQN7-=>0gl_T}9qUyR3elg=yWAGo7CRI4Md)Oy zTlS@;cB4~DwJ|=c0XE`Vg4w|+5@gg9{p{$ zn?j=C;elpD-T8goIebpAkQ2yivE1wWm{d5Ww*0Y5-Ar=rW@kzjBK2TZ)=;W~&HWMj z%tgUEQSL@Yg;dR-Z%2x4*hu!}QL`OQcBCoRd2?q&i!%iBXhwnIW1LDA5JEG<%m|y4jV(2%yI2ZF^LP2T9Krq zMxslBeFgJYePAxwo)AW=Y$S68Vj@zb9Onh__QM;A+NjouL>iPYni-j#6rJ~+mA|+s zh?8_AK{m%QOPJG83)RG8tQTO#l<18pF+Md2by{72tUedqxMwcd4;TaNBjGE2nV8oR`kuL9-iT!Sqn7L~W_h2##L#6_*J?$7J>PHZ zDjaIl=|BT-@uRt0c`i+E z^7Z%3HRck0MU8hVX|aLiNE_{J zPYC-5CdV~6R8LJ(foZvd#OHO~gyyCy#()Z#Mi57HNFz%HxN?}T@>xBanPl7TVLvyF z4nfR`M#yFo@JsiRIms~E(nQIiU(Aa-%&cFnk;wX^ zS*Ao(xX8uP@!|uSL7LfSF2|}&zgz8v@x){-C9?`=2B(Q(i%J<89}!33UT8iZul?Oz zV`8jnm4J?i1vOtC`n!I%oJti3v9R323UyY?`Eil&^$NmV<2E>_VmD4gE5K$2hx4rp zuJ+=1E$J_g%MB3?sU^6W@->>nV&CKv=zQeZx_&N5PXdZG^v2bkSg1Ghu>lwlOGKp3 zF{A9L-3#Yp$q9^_vOa%UK^MnkQj3-&It-d6W2Ukus`=3DaGJ`LdL2AaqH9V|suhbl zaJFf7|JOe?*BIxMzWTJ$$<;=5xF4vt$5Uk3DaYh^HbRG`N(YNEVEb3OK5v|rADjyk z>WIw6YOqfL6S%%qC}BhIw2r0H-fAgRs?|nXqAr*c8=K!p2%P@08z2^m*$E$i9)nncf z){YUiN0f|fO_^GC<#SIq?@gA~}UBM#wCi}kW)nHobHaaM^ zgX8(5W8>v>!Ei9$o?tXp@h0&Wh6mc*kn5XmwA%H`v940}vEE>jYfH+-$0ImVVb{^0 zulTafp*J=SrCPOiiT0OZ-H(}kAH`d!+U)mrk&s$`a^3+p_h*9OVQSC=f=@<5uu4-X zJO~f^IxsLC(Yta0O&5rE1Me0J!o|_Cd{HncdFyeoQ7zP{2(A1$*BLOi4AJjn?QD76 z??$R*$K>Hq_~L#1*16zhm~O`_CST$?H8IU3=u%jjB&JcRXEyoJ$XDn}$TZ|{j^{mR z^~dIQG>Bm?!blY|(g0iBrU|{v1R|3lq5EZmQ|aM&G^`_`b~1PIE$}^YA@8Pwqao{lsQqb$`FTyak|hd1TmyfB-?GQS1(%gt)XB<^OBJmO?u;I zokN;w`AGFw^;)0sd6`IiB9295NKmRyD@;)vGk&>O?`R`E+s`L@fip!T6I07d46ODH zYwWOsl^ix~6))LPZ|Fr$CA+0mJzSUJSTTeQ!b);bYpQ&qlIF7`-+0jB@t|-tsh2V+ z8bnOJ6%N;ZevKp;IWZwi$)e7Q8QlDcMutxsXRO|O37h)bQCbh4K`8~*&-#1tluwT z{Ah^RRIoY6>@uKWq8TvIsKTY_Gg<~?yHv9_ouvA5O&MY>hYkDhdCrD15|vveY%(n8 z38{eNx?XMuLLyp2a3r})2(_}!Sv!8vCjxyf{CYN$utv7fmy7>hAsFx8}v#eh>zqEuE{yr2-OG5&@5nt z0*|F>g&R(=EW)5{KIpG9M<#-z!?E1SrIldRU?a(9zdXu!n@qjaVKqJM6^GyjxY0t! zl{3VkM>V=qIfKiErq~|~b%zbR?|jaNvNslpRho6P7($1fLI^&V?`bhPMAr38(F__x zx-{S;UL^n@@j-N415ed-zZEC_%7i58+9;_RM(I~Jf@GpQhG{{onZ@|cw9+! z!R|eMASdxU64;A zQ_Duv!946Ot9+D+7F+r>2yV|Lmn1XsXo2;bNU3pVFiuWdeM-~SbQT9L+mIbLL@qT$ zH8@U98&!=Z;rb-Nq)RZ=@x&yjqVsSJSOm#=Osvo?*z*u`K zoROLdJfCgi4USW$aiW*185lfodf~jdZWicR{MVRHvvmw<6(3d+#b_pBadsQ|}YPDuY z>m}pGSXJ$%Wv)w`=~_B7&;!E|ID$V3^~XAIqS1PB2^%gwF7!gU*h=K(C`Nh{BydSJ ztW~G!$T-pLdyR>|flZnyt*V?+IhrII-f28Y!?ivK%eCAzUu`!rpD!-fh-?$Cf{!Wb zbf92PVE9Zwk7tyOK!Otot%g4C;;f@jZv6eFc5q=seu5i#bu1>l`oJ5cHLimz4Z1S~ z^LwclRZ4WS;zL-?-~!ANHvGdS8&tVY3qgEr_~SM3CL-tZswz(TiWIAHlOY-}4hjV- zP#X1&Bb*(=is_s9xOBTW)=Gs|rrya9`ze-<<@(8rP$FPlsEVVcaHdE$lz6vRnna^x zd2AR{($Nm z03B$Pfo=pF;X#)yLhOhWsgPGe$#|fZ@RB1n z09MPVrZ-+&TVgm4HC%7UIG zijzTbTbthNW<$isyW- zz8J{Xz{+>B8yxGEz?nog#dI2VuBvIxRzVv|&d27~FI=)gjRX;YwO8w5vRnx4Wt&AFal_hWc6f18a7Th7cW$~WW$*0WGl6(J}_!Vl8*}6Mh~TZlTsyu2P^d& zgEuPy1{*V=9mwo78XTH3p3U^VqaaobVrV!rs<);S%vsZydcgwMpPak*+# zghDg}?kd^JPaXGS`!knpXjc5wgg?0okc_+Qyugfe=a%j zri~C23^77T5_F6PFK6{whgrggU%zAnpQ9(Kp@!(IsKRGd5^J@~d1Y89 zOd&BCl=IGZ__a$maLp;-4V5{*q~bAeE62ClBnDHIS}bbe3Nz`zSUsQa265r2%Skfm zCu1aS<|1S_;H{N0Hd@rZ?M^LEtNb*lRrPSTJI%spl&TCCDm!g9pGl_MsYbEywBhG2 z*&u>X{iGn!>EsZ6B~I1~{a!y&?iRA05yw?{1nJNu3FZlA@yIMRqTp@B*U80Ex|+)d z{8EvGrwGMhm@l85vS=Spv*B_Gi7jEn&sh8a&#e5=%HcO1-hA-DfoH!DzVoMd|L5*I zcK&wft9Gv2{@C^_p|1z=c8$&dwfS|MuLJSli|c=~K3)IHwI5kac>dI*u6|~9u=*l! zi7(90yDr?as?6fdap!puV(%=WlDhq#}$U5iS)M6^DxVc`lp*I?mn>bkvsCu`r+I6@hau zSrI~zuu+yV!C{4V;U>!ntD@DVjhJmQy6obiZW1eW9Tx{xeD%dDmd}8R9K-QESK*3L znqjqW+fjU}^u^AgLYJ)YBMA85N=eS!U%PN4FyeKu163?7ZA89V%Nx4U>=g6OYR53& z6p6s_C0z)D_%IoZtBxv^FWdlhz1?cg;?laV%x!x~5ezh$`3&xyV7=?YtAHZg0}4wk zI>rMK6hYvMR_2{giQnoO>-VRWyvi;UdPG&UHoY!u6IB zR&%~ z{0QvFs4;0e6us-hOMxQ0*J(>DIlAgf5rU z&N~76!&h3j!WwtgrFAUs4?#yeUFr|O3V5Is)1;$B_QH!TBlfwK?ab0fIQs*PqH-%) zb53mD7rF2vyN0^Ht(>8?MsZ4KCylN~fXz8_=BkEWXMC(9aIPj^NCb)npFB31GS*JB zhEDe~{z*NLJs8Atl1T4jVG zR3tIY0(-8zXpeMC_FU1HF50JX;SiW}%f7y{EiP@2wJrUq$HWmXtyk%IzrgYZGXy?a zfYtDE2CSGC8ME7EgFd_)&Vqe7-3tdm$=fX@!qQ5P8Bdy6keXX4FjV7o4 zakHD8*2;P!gvR(OKM6Nd5++nH>;skOEtUMzDxWv9=zg$6Jk+5TPSfRN)Z1;3vs91< zJ+X+jDm+s|6fw_pxnT#ZU)Tex{FW+iX;qhvF<_;T&n3XC)5w<}1{G$vEt zKzkAxgBSdIY>+MIX{$f)0&Qn+2in-BwJjcPoT0pq%HeiC8&;K0FFX)@qd^}bd_Go% znQlzU*J2c`mU2ljJFakY>B5d>j#YVPX>;b|PJ!p96S5iQ>aBJmUo!P>(uX63u{;_2 zg?@zxF>@Q!M1De~WKiwxi`7oO@Ntx-W`54VcSpk-@9h}6BsKHMplZ|xN~p_WY{u-N zM5+>MRH+b?EDfnFjRAY0i}oZ>$sT6_zay+a!Q|-_DKp`=52oU#5^aVHx-<=>3lv<` zroHJH#=v)*fdK5;x@b@0bnLkzz~@msAU2y-Ay6nMMjdadO0;STpAIK`@n*U^^aZJL z3?BHazMNnHdp0lHL!Xj8&KP$`!%R6rrv^%Ml!B=WQ(-h;TuCO_P_Avn#c_RV z)J_j7z@CkZ_E4u}&lPVD88X)b$LOdOrOi2JMC5sMKvaxoTaEP6T8wLz0`X9SZB9plU=W*jv+Z26JMwu=rBc1H z22|Q3Vr*%Z&l?e491EYF{~xXFjkeEi2& zGe&-Jy5-nI{NXFl7~Op2;#jkF>c-9KD7(CietG;Meiv?QU%+ubYW8fkO!+(<;CGIB zmYU}u;GZeKy3>OZ>tYK}4@N+N7dR+&QsjZ-48O_)RcIjO4*^S#Jy2CGA&7F$><5ZM z1?wkpJ;27}qZC^Q>$wJ9ZwsMj;{2fBIcIe+hdUohb(z6T6mOK27!>`t}{bBUPbHVVrL8-25 z1rh5^;#f2yi9#vfZxiRlE*fA;fkv;MJ;&1#Uc34LAn|gwb#9QIsGV%FSq29W1_7}T zLOFmP;H6L@QfZrI0_}~7K))xYixGc08Sb@lBbrgqpXZw`G{}Uy=R7YEy+TOgDPBP|wbU095oA)Rm8!{X&ld`^ zlrU^(2b{_27)ee8Y>Jo1jL<`?YMIWHzG!{aPgAG@pID zBNrXy^n5AsCFx6UC(w@h@0D+*UmVZ6`4~e$!~k%9q7Q;IJm2rESgRn}sPEmOJrKrey@BNV!s_YN=Grr^`B)uJTIq>K##< z3V5hosi!00ob9>37)h4Sr3#sRIS97Dcm3gNs3~@*L$+897OGf1o#`tfGd)&%A#hMB z4YpqLd6b%6zPMaQCHAgAj2vzr{N7ge;J+Spw_gtx zc0RrHQwML`{j0;@+T7WA@?dZOkMzA zQyHNbR`O$v@0Z$X&ZyS4sibD*D@rX2}fxaoMVvqluSp{ zV;0jANfLY?;;9-j9JNf;Tikf1Yl&CPBq*$phIoyn`x0vAH42|jgB<1!=FC#u&?gO= z#VQIbfK9-jZ+0#5O|B)raVCKj67>Q%0?!##7gezFutI1^iy#QKiB1bb%M2wMJ=4_1 z?0$465kcE1Md2ZN*Z{l!2K|^;9MRcW!JAEonmA>m-63m=4aOf^H(g5@GYMaCAQa<+ zDk}B}KHaEwyQWf9VX%1^E)+FM>`4i>?d7{2=J2=Y^{9$HtWg4cKU;dV#&;XT5GBh< zEHfrMBIfVGJfS4e`HM;}A2`@_EwSNR zVtpn-_J`vT#<7%+1>iF8t9i*1)nX`C7V%NBT8;X`^$A#+A4m4*eK%Im_7g46pP1rm zOB{3)xdyNJ2$&j-m~j^)39q-=>F}jIwmO;Va3L{vEisx&xacKM%ykTg?E*86cZmqh zl{)1<5s~7#dSJ?zB`!T0`#KCbx>!x+=okOqOu|K5y~?%3d@NDw;aZZw!VR-p@%D=p z8&n89YE-!Z*qv6YtFctEkZQ|Z*186j&CBwrwic3-4QZ84Du8hIZ>={>4H1pbyCHH&Iw?QO0l-s)Q7Ei(zRjApn& zNeN{V-A=D!rV7w0`>CgGwU-I+uxH6pytE{MDm zpxP{(%OE-(ELGrUs*{y5qK@mMQm3V~dh(0Ct|i{!T4L_CE_(Yf&UCnt_=TB-i#gnn z%p_d&tNGl=g~T_3ndjp#H2eHg`2Sg3Daa_&@J1q3BTIyWVyY3zWm&cPVwO_@_Z%mYfcXc?6vMB7A8?Y}Am>_4Dy+ zd5M?(+e}BKJV}5>`v_SX6`E>bB=ndGlaEGZ5SgV^>%?*aub_$Aj8 zb05ss%cAIoK`v^f6qHKMX*yXnYr=pMYmq28XDBkG}e; z)#A!0f%wTkcYfHv4i5Ia*$8;#VzvrxY*^ji~)#kfQwytZnkwTV*u>h zfK$E&=Up3c%D3Q+wgE17C7$*z@VYkOlyAWsTpMu8x8R&@fQ#Llr+o`v@7jPS>u{%F}>2DsR=d)l|)jB5i<`4-&d+JIBO1+TUZaIs7Iv~R(Ut_?Wl zTX2JG15WuCyvjDf#m4N@z6GyzZNMqtf>*dU;FNE{%WVT(>=HcfTX4N=15WuCyv(%$ zr+f=uY8&8U3(Ap;4N=QHj9%i}fK$B%*86{U<j)OPs zKfC|&{rmUdxqs{4zwQ10-pBUdx0l)T?yc?q;qDLae)aAzlS3TR4dP|96|8*!=d*-X^(u z!^S^s{OZPsH%1%G#x3jry#Cwk-?jekbz%MN+P|*-?%GG!?pl-A&aJI_{=oD7p0Dy` zJ?A|etN(rVhgRRenp+L7?gEpS`#Jletu?{(+^Nj44+;BKV{ACXwQT+C*$-_xOZtZV zu%J86Z*G0|?1wfkD+vzRBbcCF5q4zjGiN`vepyKv3B!^}be)ICxBlfdA6i>uF4-AE zePpWJyWC12y7kXzKjeA-6+;mu6dB-!tn-xV*8e^Gq1DSuhLKRXRTW!T#QOs!S1v0V zLP8-r+T|zCn*Y<;2kvr~3{pg+>OB6x^?%MjaOY(ugGeaQ&L&&VFj`yxc=mz!URE-I zg#0u{saM2C{D-p-yyvo#ek25r68B@yP%B%1fA)cQUslow4p2AAzL;^==zpJmVCpOh zi~W}A47IfdN=_~-i69`#o-j&W(ej|=*jX|H&XrQ}W3-PhD+z<}6vmJ!Is?C1+K0}P zq0zWHY#wvX!ShQ7tps+hn5^^1lr)_s1M#U@x+07a&~98-GJ*vCW(&_=(U(BUzO$sS zV3J*Dm<_8&y~|35kswl}B~ClW&hBL;Lr4&oja1qh?qTbhvk!C}B?D-JRWfPkHUIAH z1MSO729ZESSGma*cK+?z2U?ev3?PAUO*HENAA4^C=So)Bi+6hO+jo&&7GY)>xGZdhYF1C+Vc`1+G4t&!?H`IltfUdw%Eqe&@g4WHy1~hsga<51!hr$x-;v3=dYhLDj)R=7zE3~fn#?9zwp-=N2Q}2Q36Ii zVn0keO91Tr<;78PYNQWF5wqE~Os9_i;^L^_90~WMe&rZXFfu(JPOx5DG(CwyaLq9sn!!rx!=5DUJ~A z@mPGVeatI-_TuOjM@M=H*nbb^-oKY4K!JvtI0U<9t~jgYgh%MKVBagKznPJ%nOJOAL~ zNSPWrub9!!CoYb{QzPfqB-(lS;z*tvIj?Te&c`o~Le7yhivaCBba5n2jYPqrS=T$l zv7P*}i=*Jw$ay7wcEHHM)JOz|J+;oTe9U)#^x{aI8ac0Y&kh(VOpTmZduQh(7f1f1 zBj*;;+4=Cr5$_l|yAaOK?_V5oQzPfqv)TF3#SuF-a$e1uoey3dF;gSw6_DBay^AAy zY9t0jm>kI0kNJ?_y*Q$#Mgmuda%`}0%rk(IKr+<63fo}pF4WVhJ5YT4N4WwEUU8f+wkbcc9ii7rB8N#3m(15rdgrn z$j)!VqdRSy70QC_JP3~nn`VXYi#xvok9;;w8Of+wR_3c!M3PJa#GZF*NZrR#*0ANT zx9f5#**4k*ftGzc?}JBPn`VWNXf_RQ)2#5-XXm}}2(xKc_)M|$>+lG*X;yf#zw>MG z$Yay2@P%RLJ@5#zX;yfDzw@i`X#DMeg%{2{zXFdSTh0n^NOyi29$g+|TfWx9&M(=| z{~KFp*3RC#cLDt0*M7dHfv;)cYZ~}}Py=stjien#Nx$Vedsp8Uf(zs98g3`oZJRE{ zf!l3nXQk^C$niwPyKlP&V9tdEB)z@L&X)siPRG4=GYKMq3ABpTg$z#{re|z`zmN2x_{Y(S&v|H3v z7$M5cGC zjtMot*}_VF|5DArCsV)o-fuoX3hu~C`c32!CosnbZ=4T+kNbcbHfsOLVMCMZZ1##D zO^^Qy+0aD}^lYXOmc_UYA7n)T#QMS*&)sQj3AM7~O)( zr8T4K<#E>V2$g^e+JNWO0g{VUhMev%^F|_@OZXf3Ws+nwHI0auJ?YB`!5JYBWG+?q z0=zE(Q@{7#H{3l6jO*FZu2r6XPmW4;9FVq_XC9EyapSmcm1jeH^;dbu8;@V%`FnQN zixnRFXH~tJeK?+2&(+3$^5N(#LTXdZKOCz@x@Sx!<-_(`o$`E}CHs`8739NxbBj$S z#2&$8{*uwa2U;Y^l#L*0F%gP4N}$b-XwZmA^qB13n&CHQjQ z6RJkZWlGZEk4?R8G^^>XVW!3v5ytm}Rd%kO{||2Z^!lsUHl@v{ZG3*?XE*NM5I3H* z{-^8jy6}+;KX9RX!Fyrz{724z@A<-c^xXeF_n~udJC{3m`Rsq3{hhPlbvAqUh5P@$ z|Ni}h{a5T?0{H^JdFCx=?m2VE-aqX<2qFRWy^FhF-2IK+x9r}%d&kbd>^!)0Wk=t+ zxc!gY?**L%)a~20{(kFMw%)K6*?JbpDfq?B*R8*LU08pj>rY%i;TpIY*W=bcv-Zxz z2N860ZO!%Z{|BE+{yR?@+Pr(yHTxorIsHWXk%!mv=;l3J>$3?^f^dAyan;`PS8rY0 z_#epTJ)27t*%8oeSkSA7KvVe8)mLPNK1#gN1UOC9)6Y zj26mq5{>0tSt!T3qK9(6eE|`6tg!7N(T5g*J5DqP_rZm7oHY;+<^0}4IZmRnoZnd} z$GHy<<^1kKIZmRnoDVFN<1B4;DChkPW>P>z#mEaz7j%5fIXKGfTso<0Kl(`R>b`+Pp`kS)Sn51%`t<&A^tBv7i6F z>(RB%3mYHVxUwN{Tv-3u`oV?wUHHZeFT1dI{(a}a@%+oqubq4F+&7(L&uyLk?X%x> zmOZ<*|J(a-*ylm~|L>f6!x{d}-rn!-eaoJ(x3~MdyWg@a?C$M+aOcfCft~&B4{pDC zJFvaK^}($-gE;>SAj<#Bro0|qe}d~1uD7{V*HhL$x%Rf1&$zDl%>47${&>@C;I6HE zZk_j-jveb$m{gJhPK;?Z?W%p-r$-OE{{1Ceci*}-=e@vja*j)qbTl9^fc)L~L(f@> zniLgT(UakzI;Q@;+ZSO+eNJB;4fDLNL;`*tkbm+^x4Ir$Oz-rKGUwM-omH5qP5(!q zc71Fyz0>45RgOl38WW1!^gsI#u8%IJcOL3uL?a`L5aP%9-}-LXM;6mNkKUvxOB@vg zCC_a6wa>Udyoeq<_T?w0^2vBCtjGyK|7q=eTwhvD@ASo-4jP4POd=Yv>7Vyx*S{^M zcly|5g7N#A1kZ>z{qECU|GH@R9D64vB@{hE3p{75|J~fX?pTT1Pb=YwOtQ(4P0c^y z(n9RV-h{}(q^cw-IwAw|NB{EKu76of@7%j#Q3Blr6`Hcw{?R}BC)YnOrgz>SL^2!| zLrgMm>+aDHeXHxA7SlWTK1qeclB8&?X4C)19j-4frgxtA5j7!kA%)QuoBpRBa{Ygc z>5thC6;_jSNK_@8{)eCH`p3oe&b=4Y6jlv0dL(Sqf9LaD|9dgL)AqPzl8gvkGHTn- zqi@z+|FD?exp)1E5XowwWT3tGAAQxox&D6P?sJL{Xh|}rh=QL2Y)F)nN!xQg+W1e` z-z=tg?jA+~=ME!kY|N&we$@5Xi|L)aM@X>ogb-rr$$EkzkMq6UM0 zn?8T9>n|76J6As}MUsj}l4H;L=*vFk`isT%PR||WC6(g>VPU-c?)sAJ3k!FTQ#_j# z6RadL>iE>X>%GrgDE-(&cu-`Mx(eDhk5~WcZ*u+lVtVK5GgLAZ7HEwd`##qN*XI}0 zJ6A7Ag_1FF=@9JI|I{bG-}Sl0^iJbRO5tEE5*7Wn@A}lweT(bQ7SlVmACq{RBqL(d z_C23^%l~ox=|b&0$*BXkp@`s2m)&ed0ATrw2p;xXIz ze2V`**B>pWciO%Zpecb&FbZkYUtV|p;bMBH?TR5j7)eUBGG4v2CD&&b(>skvqcmRT zSUF^S?nnOS3$9PUcuSjq<8{1_lamZ3Qgkd~&;Dmj_xhr(bL^QfnbZY;k|A|_=f3;S zFFtn>^yA;u63Hk_Cqrz+R^1~H{;=!O#q`dlmjyj82EugIzU)8p{r~Cu)MEN$OGhOs zhL13ueQ9~*)vD`}#q>_Y_p_v5N=OOTzBD}2_$AjT7t=eJo{xmpI8BBU_SSzyyUX*T*OQ|6ad#_PM)(&8F-1H~rJ)e?NfVR2orsgw?mCKwqKd`>d`SVTBQPa8L)t z=hae;Hi(}s)e9hE1o8N8aQNX3ic)ggjx$Qh8GeY42b>&!c#Q8U!Vgayu>^VWxlOc6 zs4>iENTrVPgB~%A)x4;z6kyO7wUxE_h$o0&>Ts}bM#aLZqY2XiKgnhbEs3o#5=1aM z-7Lk@XvwI?*@hGpFGKODx0_>(q3#v%KGw#RPA?fOWHJptfRseJFX#%^t9t5W_%fXf z_M>Em0&r{^uHGn{aRqB8a_`^q=ITp@RC`EbHvmu?x2hFs696|Jr0`pzt8d#)#k z6FLtmr=ea)<4bfLQ@k+N9$q41cuvR=It%&fOdBJ-AgvpyYi8)YuHpSIE0pw04B@BB zB#}izl>lBrc!pCltzxix3EXOObvzzFwQ1)YW^fRW_IYVw26KF_UG|ynL0E{ya(z^R zE_c~7t9$7tLk*C07dFc>uIh}bXL=dBT&RZ47(-ROHMB)G%3bzy)fCIih0af=J~8c= zJ!aY~?AMh|+ir+B%e1}3tVrRT-n3Up@%)OLw)6gYBTXB!nJqT$>xeU+h5WRpjr$P$ zIGJhMxE0CA^37tt(5(`__U4SO%MQ2cT%Dxu+4tSj}Jj2{|S z>s2lml(GKhL{5v8D7FSXdOrYZeXm-HB2r$B)IzvPST4v}N{~W&v+j8PwGCc;eBuuK zz3(S3{n)&Fo%4<1eNP)@Y?PO;q1?KPa&85eLzLx4sh(m#qY6yKkuna zqkGmUZKETuq1!x)Zg$m}Lv-16y-_cLLnH-you`+8xM_pCePsWB&fd}xpD}u+4bgKA z;>I-s zJ_VjvbLbG7-7flMTw^ct_z&5*9{yCJlc@>Than0~`C#LC+y~+9S zvr%67fu&JCZKT^MkNCPH-eg{Nr9+fQeBFuY#*SqFlQz1u!qVuTG16>wvmMF2icW{< zrX0zM_{NT8@2NJvz3*5W-;+m48{ce4GB4%+5Z|FAIT6{|k!=0AjcohLOCx*2NCl2W zo*;8Lk~xXbhqxwL;wM{$4aoK9_V4vCEe-3IQ35E;IHIsKtKe<%)@w0kIx)MZHz}8^Uf+&#||;p(>3Gdj~DjQ@$la~cl`U?=OJ{y%fIiLqlg3H z_9`{b4iSzIkrVf#eTY2#S1%g>zH4cOFBmB{!l^@Kg^Fs22xlB8C*rk_lZQY5pZ4!B zylrW`w~xX$-l^kcg=%hxc#j+`C*rgZmWTiB+4k?xZ7z-Txg*)eId!nCP(khx=hP8% zB2xQ^dHA#c)BgP@KfW~5TSg%p=@E~)Qr)^kq=$#ii5Ts}=HWkj!T9(0ERFG*BMBTf zhZrY^%?h6;4d{#=rk=X@s|of(|Y2ukt}@T8k%s3E8*NJ@obC->;pA z(fK(6`1F7SKOJb05^EqLV#jrNDB6W9s`gY#k}_s_djtA|4CEu=hfF= z7S^)kPKIxv3Uk2YK=)5Lu6t}=k@$I_rXMcn?-1vG?ADCJ$n6YSsn-=fAI}sEQO6Eh zsCx|P{_)3kkIkzcKM%eW*0SSHhQC1f=zZS%A2+tmsf{zI^!z+X)3?X@ns<)7m5=s& z{gBaX(j}|L(f)zcHWxtmfNk@2$G6SgYWUX+-gyB%8T`U+;{n^|!trf`&MRm?555!D zvg1yMzd-j0*f!^nZ<{&w@aI9AK1Anl8|S!N2l>*lP_5C)T((6Qn0Bn~FxUmqVW9VO z$Mx=+SEha*c<1T5?6~U!4*~FJXMhLw@Ml)Jp9AVxtmv}iZcWzXdPxJ}+=&pMH>o=B z?8`A<^C$rfcK^7+g6i+H3-2!rYTh_!F!Raa7a1&oPuzdzxbBg8wey#Swd}Z);V;m= z4}jmB0e)WT{du6KZ($I$?6{NPY%c(B-)?q~g9n|MAa9lob-E7gp9ji$=~{N&b%DnL z_?;Qx=e=N@2Wt9uGhg%0aVNhET%dXEz8BoTy=Odjbzh+*{=8{SUjgP%!#VEcw@8y~ zJbnMx81MKxvqELQd3cw+m7&gYC*xh9cJw~_{>^LCSfS3_ylFVqx$L;>nuZ5VW8>O1 zRw&p%58jfO9n?AQx~74EX{=wH#tNaV^QJLJhMzgAc~|DEUV z*njVt*zRBMyk`3eoA20oiR&To=Nt9&y29&2yX*JddVdzw@%!W)6=2T8nF>&WFsH?% zyu^@w4e9qZgoDy5(G~ddKbvWu80MG;Dl?=-mq|;Cg_=5_=;rupEv+U70^g|`O2d*E z*3yt_Mc{MQOo=1}h7ACoL#+nJ(-fRV5`(1u-x;W&@LV^8pdG19_T?B0XvMq~EA&G{ z(h9~DSZ11L1_>22e6g4HD7efNEQuV;K*los5iHBc3*jhL)R0g$>yd0(14$jr0=%)Z z9t9ZuSV{Lp;xS%BWCYgbgpNgOh4EOu4#px_G3QSVz*wUbQ_5W;gk(jul8pE3R6LNXp)l4Z}9>&Fink)tdcZFgtnTTj9T=54b&tTZ2A`v8lo895C zuGp#$URCuc=Be6w3Q$Q>l_IJVByTEJ->6;9>0Pys(ej`l7vLr-mh_OqX)|^;?d#z_ z48k&?IWYCl8>c{4D1B3@`bO<)maPd8i>s;_%y=3| za@gzyx$F#8Gek4V*LA#Jjc@^9SvGn(!Xw9mg?=X1g1C;@Qn_L>R?1M5T`gQy_1r)d z=dlWPHyMfRhC_ZT5xw4)Q#;e^N%$ivhnzgHY z>1v$eyMa=j?Z&7AMvz##Rgmj7O)P}6trk1Lu<|fi8$x4W&0SUXN9L*8`GO`9(EKqm zkPNCfm8yp~XjjusO&OA{NLd`{a0LkrI}s%|Ygbczx>kx0)o81Q_F9#Ql#S$>N;z!} zD=kGC=GeA8kRoO;ZwpI9pvK))s=i?kqycA37-PhmaU-6H z%jKMvWoO;1$v$32b7iv*Uec#W5?wUj`;YeN>w)tElfiTr_ ztdeX;!elvKZZL6N^3!y&jMJcUmftNE8us)5nKj?q`Rdt6_L;q(-hIaQ8#lkW@k-Z+ zz#kXH; z-h^(dHMvpGs$ZV_&$UbA(rfaxzXxlAowO#-bNr@SlbieWzINqVdQDv40@egQX-%95 z{!O(eH}^4r?QXQ}nmqh3Ij|<4lh(xf4e4fD6X`~sCoIOKVO>k`teHuOvR)J59C%m- zb9KBurpiNiFP_&cuwEH7JDALt%7aQV673gJPd-XhbpkJB+x}q@lv!Z)m=chc2+J#d*2MW1^rl*q8+D%W8Wt*H^-xVr z^MvoDGmPGqxkxlR(9t*&heN}eZ|)EZZmCj8<+GSiOG>a-tOq1Sp-n!>1S&&NB0*vD zMYq;$DGeze4a7NxQxZ$B$wNC}O`wz3#Q6&QrdpF5b)N8CIvWomxuGbjv20nY7x5@G z>#>m%SPz_|nn7T><^Q2ARY+>mR^(J z{~%bC%O|YKvF!evXibLljj|K2fJkw;n54jPtzY|gc;`vqglgx3x%?&7Mcbh znmvS&5RIfp#()Pi2L`M$9&eRwWTJ-Epc6upq(XsYeCaj$J<5Lm-*i2B?d&)2K4tR< zz(0KL2Wa38TO-b{ck-=IaoxT`a@NTaC5|d(b4&7Jv+A9!kj`Wm;8>-Y;CTEBu{h&$ zCF24f*LsNGcKZr>VYBL;98YSqVIA#kd}1n*O>lC>GUo-!raIaHbp{Ynh6l7;Z-er7 zN6Q7;&9YA@V`B3@U3n55@<7ns2B)S;Sb|opk_i=W@RvJMq|d>Xo{3dsl{n1HQTmdq zOJvRPo2M>3k7nrdrBQ*_~ZP^UG*=2;#LZduG9}VAZFz zYJre!()ngPUnDZ6t`MkI39W>B%6+f@Qo4xKLJ2ZXU#7DfR>PMu-PejFi_scgYN2F? zX!dEp%y5?y9@Hxayn}Ksme2PtGhC(7sxWy^KG!u`bU+`4)8Af{pv#Xnlb8ne3p< zcC~z3kZzpad1l$&3Mr`e#=ZTz542SoIPBw1+hKaDoz4&bBWC~C!#_>Ud(7_If17!>0)f{?I7%8h zcuy(@j{j(5oZxYQ9CyP$^0zUzOuGIUt4b+bdF-FTk)F=lvl|bZBzu5=8!I4ZG&@D` zFCHN1%jj-k9It)Cr6^L-cpI41=OY3osT)gic$isNAN|6~PX;wOLJrVVXP#wEuM9H9!Q;8OM z)mvkRZ8?{K1k2{l&kvUS)QPR`P<|0L*uJo-!mJX}64mZe(gy;vhhxQ8r z>(rKK6|<=9@5K9f1od)Uyak8bZOOz!1w3iMN<_B;EXUR$nWM@9Wsr+lW+xxAx`kNO zk7E(T2(E-O_rNC7y@SRR-@8$ZAC`n4}@C>zgO|J(InU4N771#2I;@Zk&Jd!c+` zVFbeTiwbzdSijv0CD~yrf)z=DQ?r8Q7WGEG(jW~2)#3qnE?wrT88_ds+S>j1uh(|%b{G^ zU5C@TX0!^|iP_0(g

    b;`Y1cJs{>3ci!2)qJm4PP%nV?}Mgw7DUi(Tp{U5H5JYm2B@3|GY=K^Za<_j z^o|r`LvY9~)cbxh=&KlQy*BJelun}H8Ng^a?1^Q$oNx0>6AD%j>%m?%=Pv3?`4Y~t3eEU#e4*%|iarhuxY zY*Gm43=)?pU^q%C;Dx;X#!yqJXc!yBdQeZ~YFXEvlLe&RaMs@gISx82wX<=G(Rj@Z zi*cRPl0H?+aWT%V1x>C(o|%XMe3N(trgEZ~vI=Q9C>kM74hX8J+KWjbEYF*6RaLYo z7q%z6%JSksS?*9X3P-bqms3$Xm#VpgNRc3|1i}t6LZ(Ez*p`LeznaLv1B}Yc1FT`d zq!gt*Fw44C0#CaUu7(LntKMp$J_vz$_Om%bY=z8pr`E@nex}gH;~XSx zed{4b5@a7$V<4TWo-j?p^aaRZsM(c#p0p%GN-8V*s+d?Cvf15_OekQjU`FC*2FzuU zGh$`YEkrHN6C5(Y^S8ykAIf@qWph|J*Yyd7TP`)3T+Q80R3!oNm8>dP=p>uCtb^WX zImJw+^)M1>CM0&_-w!E>q|hd6txO`v4}0!bEUFIlcqZc&jA%L&v4nUSRijEU6?dH< zQ}l+NI!6p6?N)~Ei5_n|C+55PR+;f)xFi}yNz~0oFp@6w?B?c#qTi$Ha2<221IgPh zdZ8TO;D)JAsn<2z;y}sNijnXT=}LyOTbxi>YN!(NfR|o!B`HvByIpFs84=4>YeF^H z7yNXNfqOKZ9{RT4GoffDOr%XD>XP9DQx5WcfiP$$S05zDdT;w@oOD?!3P^K!=nX$oeYjY(j4q3l(6fawOc!Yr%#omsFww z33{~|BE8La5(P&99N+`>aIK|P(Xf%u`39P&u=m~xMS&@j5i-m(Xt&}=+=P&8>iBSw z@CFllYB-2l1R2)o0#u4`J}{v$`gO0`sD*p56-xJVa*4xR(lCURS(d37NHbIpgbgep zrF?6@I-$sUhEX?Pvr>46C3Cq@C6^CGWp6bK>VoN5FJ9T#d^~8I?^Zz%nucPt3RW`3Z%Z7lj~j3sw;F7o>uuBU~aNhVWF~ ztDhtc$!SC~?JZ)F4}(FqN;z0{%QB$?<->u}e=V5+Myqgqwa=lLfIz z6vC-^o*EG0EK^J;$!54rMtw4!Y*Z_*pPf)d(sh)E#SR?N@J>jV+C@Fu*TO^vL}4@% zQaEh}OM@iak*$r%QQM37ERgrfx8v>-$pnXqLRg`)itY(#QhA8%r29S6t96V*rLz0$ z6B$a9tgw*cDT{?lh$jbBw2C&tIOLC;A>(xQnPw4|1&P7v@;R*JN?nKb9l$av$q)J|(Dxdr(CTw^YVg3Noc!w5#H1Db0d- zRZC;zZYbc3wX)k^H$g?mP2V6+_M)Ioa zfbxJoaBM)StL>z}!HT(BtRk3bsj(iNP!J79>A)#>Dvm~?dERJ31G*+BAT@26iLL+w zcg$89g!Ay$_RkzrNMtD7?H3eFQ44+qEu>;m9(g{^D z=l6Jon4UvA$$C3j!7`BEj}%k>%GUcQ6qrJqb(+X05?Tclaf-pAJe`T*VlP;2Ai9!i z)6q1zi^mGPhs%M61K~DOYJ|K{E2HvhDp5(Rq!RGObBK=j5D+E64^eNhLhdFfG9buE zYiLsGhI*Bn83~v32AUO^p&9BmQX&aAbHz-9Fep{q`uLclJLHgB6p;Z%kPY@gA6GnF z(F47_oDLVf@eG8R5hf94b47Cd|AObd@0xzryBYp%JP#{sCZsgVX25H*T-1V!1-Oos zbdGBBP@A(_97k3>Y0D4yg{}I8B0|@2Sf@)KT*ExgzH0gPT7w54|HJVFm%t+Z1_$|h z@SQV$X7Vg($v#ks4D})*D;5ikM(+$rBl&QrE15 z>_dr5H;f@yac3mCsSn$E5e`_ zXPtfjS!Vxp``-+n^xuDm+56nyeS6Q|{k7fL&O?SJ0>u5I7eLtE|5f7q;VK5=8T zfvkUIy|upYdb{g3!04;=qh3L`73Jc&Yi0US6!ze9w@}v{m0(UTa&`C`ds4sh-YcGM zNx68>5za7_Wy@-JiZ4Q(T*PUrpyQnPTiA4@7((Q)AYf+CfBrT39s>3t!(zEiO(B4w zr4fvK1sLXZ_nGV!7zn!Shzdb#Ko49=5B1t<1s#9efAWUgpl*an3k6^~wuu1V?8G3ZdazEnI1yc7_vMdhL}<&biI2Qg(7>t7WX| zSVHe>uiW88Ft60umkGf=SDp_L9Nj+%XdMUF^+t$yYWKv-YKJ-~bV1vLR63hAOf!4s zd4T^>Gq(9dRwuz7^MuRtpJm1ufs~_WjQC)f$oKn{vy0%;dc0=F_gs0dQ%7DH_Q^pm znmDbFrfuvwfd5e&n_IDMY4i(i40YzhsaI~d5gf5G8v))7keOpPw(KfBW@FEG&TU>n zx07p_EoEcQoU;6t+W>;2Cl&$&BO~J^mvpY)i4A`a5zINUZUus-w)(u5LrYI})>gj- zNIAOI=e7)5TFSMp{>m%Q0^EqE&3hd#6>_u@mc=K*Ujx18M|AK2_~El+%is zwx(|Y{Eu4G+$wxaD`>GbIWy+eE6=bI9I+;NUT>YH5lpNpd*$hX^U*B^u0)WB^!NFW zQ+Fpe{y5yrv$ojNfRw2%Hm_65$))=arCi%$_gr}@;6Anf^E$08&3$_PzuwmVksC5F zcL?el%rJ0T9W7peXLerV%2T%G>Ghw}5@qQLPL}_^y(>@NHpX*Xq0ZsSaS2QLRcCG$ z0Q00nFe?->yg^``$#($E6A!_xP>S#dfpKakb>#^F%d>&~w%))Q1#L? zy5<`u@&9jGJNt8c)K<~;mao#|``5&dH30goigp~-w}C#ZH`K(Z^A2#kXZEZe8w;`G^#MIrE~rY7J#WzARjfdEpGwWajhT?;doU_*WyGrT%iq#F)w+0 z74~wwP9pwzz0vCXG^7;H^F$5~)S|g1n(@m2cvAdCHrfS&mVL7z!)uTL;yQCPG645YTO|#KJ zh9@J@Kv}u0c`irzbV16?EscsSnJr*X-L^RYh#yAZd|85y-Hs=-#o7wc%^<)$8Ih90XM zY7+)p(@Dtmgec;;`; zzkU16x!wieIcEC@_ug{8dG;rFzhmo>z4vX`FMN3ab*{I#UhcYM>qob`Tl8*b>v5ZZ zy!oy(x1T?|`KtZ!rnvdkbB~^V^~M)=Av>~vJ-MM=D1+GkzgvIPxp%nUclNXEx3B%{ z_KSD^bLT@l-?LNP!FM+IUcPt7?!WH7|4ib{uUy#P|MRu?pB2x)^xWf@evQ8l50lKXLti&~TlVp0NXCCZgD@n$)aX=%C;fc7UbqWwZ5$C#?8A;t`$98~@N zzBQ;fIUU3lh45mW&jjjxveKlZ+5I1y$bbq>47Os8Kof>Ei0#!QL!w&EwlZatA&_c1 zFLwzw+U(`fjoyU9&3Gg~Ue>*oZ`cGuGPoyzsbs!oblSzH8>|@!veg>RrjK2p#06P> zD?~?YoiO4{GioUq?iQF_p{u#w@}QftxLCkUsu?2O?p8Lwej+2E=#wPwFUWYCBg8}| zQ5WE#>?yR`-F7r2$GmL;8B{5uhPv=Eg_T1X6B2xC*J21a@XZw@M3sh^uP`WTEY+`| z;Y3Og6dDA%{@jU-LIg{TJ}sp93f=%=^ahz+GXTO%43G>E6Urcho*l}3(^Em$-?j@) zXyRF3426X-6M;KkY-k0E^6;`Z2ZE2|s;?2sFu5GvMM9VO%I!;Pc4(B3)uvrB$VqFiYMAOa4OqGedlkr-()~8TH_0=*| zWumOUst+TK*~$($qQa=dq{M(H(@cd6uKriY0-dI=ONV5;oiuibx22`=dMZz@Ol~OkkjK>f*jtE2RTR*ckoaiN<}1qKaKu-a{Jshg275TaN2=R5`1 zB$TMnn|wXxW*f!&fRy~#M^e#t9u?)+4YA)AxtsWY(AvP)*cNL_A&;M8JprFy8YQOrN_Oz_eV3 z5xjB4@9(jd1Q|oUlUWeM0EDq1@7TaiZwOQo00Hi(GVl!g)oKe|%b`rg+a*$=ytFw9 z^6h)gUP`P53L0>mxRod?41~+E8raPpF4{FSQb_P|2t_t3JCm5izPA$hD$!C> z$#nx{A`_&0LN*+!YOR#sH|r_|%t2vyx)W61U@M3c`C+Sd$Q47IblDr#v^jm0MXJ!tuFMkg5mYyrF5g>Ef+I1qZz{X zBvP_pEEsJ%5Xu$;Y_jF^;8DI~as6_(fkoAxF9HIn-G*BOLAjNUADPIo7^|DKx}rCd zBSop|_qSD((qybLfZC+5fb$)?0jlJeD-G9QO(@DHqH;l{7-n?DQ2n@Aa+7&qgNtXn zjMPdcO_b@?5?F%Q)*hKqWXfKV0dvN8G<(qIO~A&9^P zhf9j(MS<@M1eEjkQw=YxhWkE_aVKkd-naY8iHvg1T`orJxYZYfSrBHM=wVcY0^x&{ z8uJ%rUe5V~eYeF!=H^dNC`toA-y7suf2V{5i$#$kNpEogqO$lvI9UjX&^Si9;|f|w|DEjYgS{f9EVlp(ZWde}y@ zVaO8uhK6TyJgx-Fhye;bTQv-E2ji8zb>{HA3FL!4v&RM*0gCm#Xv*~Gt#rSIyTNsZ z@_?#E!C{UH%4)B%%THwBF}{Q6iXv>PbR)|9qFgoaDcaHZao%0%bh&oDMF&ew~;Dn;uM!F))1RF&f1j(|xFIQz4vye`Cp>WUBB#VJ~35uFA#OE3v zQq&4bGu|`G3B4#bYoT8@GqV6~96@%afwPXOhgkVpW^a3ZBx`W;-WT zT2hDr=?679k!A~9hil&Mz@lDP$S%l-=x3v)5a`@QdZ1FHKVUgsHWF=jp#qYSy4ud+ zzRLT&Ih_jVJH_+#0g{Ez}-4nK>C@ z+gQNrxAPE<2>l?UGOpt(0gpuVomN6D0mJI6)$4HlOWy z*qBdj4vGmrE}*;5o1oHjN;eZyy$m%p_;9~dZ3O9LNX#i7DdQd}`GGp*+HSaHpzFUj znYoprSzV69dR+#WMKp?WonSDMZ)5|WX17Fj^09uhHw0RN)+RCSea@01;l{9EZzhsb zAJRCk6bhHDVqPp+yuYthiWx!fXwgh%caq;^fgTkln5@Ua838JzXt8d_9mG>|J%#nb z?cPTYnPi|xC)<{={=*Yg1w>{Vc&bxW@RFefG}fE8pBMkt)UjKNds&D zqZ5k3P{MgDo(x-IMGThED%=j&_+TxScWeF@AvB0os@z0mR$2SOiLT0Ff{)gGRJ9ul z=Q_ob6{_??dA@+OVQHYYebKbXm(>h2?b{S56h)y*p;Qq>!DAlE9YEz8U8Cz>H&g@Z zk>La~puk&|1Y3}NoBj!^yi&CcJk-swo~%i<2DvU=YDcIxEql7Lq(=(YbQY1zP|kNo zn^15ia7Ch{d3W0*M|I#!5w=KIidh)y#5&;`8xmwgs5ISFW$*B^>l=#rpy}=wv}R7> zG78X-SY`jElbP2!4->=LHb^88tDwY`KkeZ|iW}~m{3|_+*umnPsTZsU!^_!(e(MyAUP>!FOWL;Y&wH=8Dhfy)mQyYy~3@Wl7chOhj zDA1*&9UNBqdKM?pehu9_ye1~pZpT*AJhW%JR5G; zguQj(PEO-}EK3M;!CnruVu&;qvjByt5Q3J}WSPP-BbniPT}sDMZ#G8my~h6j?^%26 z+J$#sh@AiT^FMVybN)%^K5*_;=U#aBPtJbFS$_XZ`|sL+<^B^uCcxm#3-h14);qUezI9>q!OiaG9UFhR@zxD`{h!z0x&HF? z3$6!UUDq9Je+a~!_#+-ro9l|Q_su7FFuxtfK%;vU!7&ev;yIqqIzzn019Hsln@$e- zzs3xPQOu$Sb!T{&|KMeSnY4HRD$F2Q4h}lfR1_5GLw9i37}x7pU^Taexp?r>F|*gL z!psMwh}i_Wj#J1WNIZDSnAv?RGK2e3zv4_s5D#8FX7-J%F!RDF)D9VJ{ur_sjhVf6 z6=pc>Ari@;CLd#V=a|`RR%GVW*lyh!mMI<(V`lfR!fakgck#eCX7=h;n9a*{5f8j$ zX0JLqQQ+$SM-kYAHKe>UaOli`fCFaI-f$IWvomGH18j_IunH>(_IND5*5(|z!~=B9 ztiJ-YxfwL#foIIjT7}uXEE(|t88hpx!fakLjCcT#nRQoTHZS)@Jb=c`I;$|7m*pZJ zTplwsS78PcO1uU`wVk_6Jb2-lS$johGqY2~1NWF&>*OH*tNY&r5>l|Lm6IdK_Sp-@ z%$h4QLo<>!WREesG-lRVh1tAB74hJXF|+zA%pe#+GVz#k+%-Ob%&fKov$+W@;=%LA z%&IFgo0+X59$XwVtE|FoUQ&v9@Z2%8@+!>c<)DZM&lxi-t-=hXdw9)!oOFiZiwC!l znH5)LHZylbJb3n)S>fcc_p26wpfHTp^^S1-Jil$sEWZk~dATRz!L4Iv#wyG}9*L*c z8J3-QGx6Y-F|*t%%;x2!hzHLaGs~{RY+l}pc<{_Iv&;(2=BAg32j4JemR^n7>}(S8 z;2C3PuUv)Myu1!=}nAzQ{Fq@ZrA|5<>%uHW}*}NPR z@!&~gX4)#u=B1Q~2TvR`ORmUlX8wq9@C3k2-cwg$HYXJ%cJO$>%h*et92j!@3dtX_ zgU5~8#+P8bLX3Cp;OoY0V@t4IA;8pr{(tt`x367z?fL&afA6{fIQOcvUphNDdv5=M zee}!+&fLBCcOd)!NxSdZeG#bam)`#3_8YdJw)L*9mu-H0Gr#d~8{Z1P?EmaK>w3gh zS^H1G=K6oi42r`v4^W$meC-Z0H|6`9usL_k zldq}e2gp-Q3>NbFi1WE=8DTG*7B=q*;M&CIUF(+-cGoF{&Aj%XEUfgvOQ)CY7-yaD$fsTN%I&UIKIsV1=E1 zW0$~w{@-x@?b?}#wu|8ZzV`FqSp#phM&TVrNx$`Rpicc^@WkA^&BRd&-o1jFrInM&|d_c9vw7%{|1pdgB_f(HAX4#IN~hzb#L zpQ+>#wUn&1wcGB!=LNSM6*1?Oa!5ck2_k?Aw2IV)3{M-TW@4VgC5@%gK!DWImeG$C zf|fVZgN{i6lbCr@Iz(WQdPzYT)n75vf)IsrQqp@_04V~jRS?VmAOTu|v|B^a006Y} zxd+T*lI(P~>(bpvXEpDNTUo5Wot7 z4rEWumg(q2bG%*=kT8`8c4Flto8aWWjLxz?Hr3J0%Nanx#yEXn#<5ycrJTLJuIf{GM{Y>%yaK=MH*02*>El_ou&`n~tQF+Y-R*K?(D#P!VEz&FnI%xK?u)Pnwt z`h{ud_GR&(C%B$3n@or6xmKv?tK@R70{3bUgL=kw(;}mnjc#74oZi!^Rfy(+q5rOZOvyt{7 z?~$vT2hC*YLav93)TJ~ttRXy#7TZ}3CW_tqrC5AGv@^qy(u+c^V!@NEaGr#u`Z02; zW^$lJEx2mj`=&caA=}f8tRtRg-hRDto@PeF%KrwQ=43bU<+=4Z+`!RlgI^^_aB?Hf zW4D*DHPQr5^E8b#XM4+eHhewa5+(?{x#!FR0RKOG-vK9ARldDDTh5)Wp@t3#p)O&k zmjHq3z4w8zncmAzpMd~D3=!SAVL}s-CJHKl6a_?7KtvEkItVDeNbwI!ks{)E&YdJP zb9e5{>?Xl{-`APn%TTHKOnX{1V!hRp212@E2pFBKnSK?5tReeR$HOROusN#vyi=R z9eV#C>|^we-U~E?d}ws}**H+MB6Y1E)18b4jm|D3nm!SI9PT>6+D*YFz;~ zjpkQKpR!`Ls4cM&j3;KZRkAiU!=8w$^cJg-li_5eu0(<#j>CuJDLDRhd;8jHoFwgQ zADymi++@@%y1#Le1L>kt&2L=W6P=ESz&b{!Lo){RgL%}m_r?Nc^vFdKv!2t=@%FMo z)i{xNi+!Er!>bxG?C6B(;J#OkkBvikJcyqv&O5 z7B3raztip(n4IRiS`){kR7padJW~^q=v8{VD4rLX z3tFpH&96A81@eHw>f!p}c~3cCZqH3?Eu!9D=g_6ETeo3J&4Og$0!fn$Q1yr%Cj)JQ z*tK~=LH|VRMD=LC=$1E!ulb5P5kXmixwIoGl!AIWljvocfG3yAEU1m6QD$qDDHOM+ zOW#2>?fV4Po!Xv0!Q-EZSEXbMsa7Bgy_n8~z$01q^E3aYK7 zi+c0Om6BVgt2kUDL}1&&vQ0p^=E1VVb5!$SxqH49_RW_0R;rD>u-7zKT*0Cb<|Qdv z*eZAHX07qCIKeR#%)aonL8)@armZ}eFrrRGeQ}tnx!rtg0%q~>gaW8DHmlWFG|8IC zh8p6T3zUeUU*43T7>>h%csxZs?QYef_R;Rz%MMjv*dE#8G6a+%yy858hOeFWL}FQo zz^QhN&0>i{9rd`Rc7xVzRJnv6UO>TSXv>QqilrKfj_J`dGIn3uH4P_lXUlp=#qZ0R zd1Zx!!H@*?h3RnK&B+EtB^f_D74sHKF1y4N@oTd#B~!;$IwLWk)f9Ka+?*^aSFOwS zI25BxhqAM(W+G-*mo}*mRa0G*>a-P^HZkyai_8vK)akJL&@3{(*^E?{@TXw9W3@l7 z)$pvWB**Jux$V(VNNSQC|vh4cGr6sGwprhPcS_Ub{^rng5uI2=B5 zq3wfwQe|}JgVBJV!z&d-c2hPy?G6-dxvWhwrA|&Wl01c$A4=E)?8OJuw80GJnKaj8 zh&W*E1dqGS589GWsaaGCMW%~h)wD6~wE2TD&_+yXFT*et8YL(1aM~mZV?I}o#EKrD zcgm4v3(C9zk0Z#+9Aa;e>VYnO-MSI@zQ3AwO5fn{z@r0~46GSgL3@|>)5!-XYm=_Y z!zTVZaq~oe;>3x8@rTAYkGsd0jlDJY)Npg;G|_3)+LU->F(Krpyw8Lj=v<8 zV$svI9Sf2(O6fE%g+!sZtHm@;r-Do}qgE|*x*ck%Lr*)PV?pwKf>i8;feR%rxq-HS z$AaYe{#vo#X_c$3Dl2Wjjs?kKhG7P0t<`K%D_yju9Sf4v^;vZaiAZEtJ9V^ud4s1e z>!-LK`MS{w?6%w%xVR2>VFZ-!ESg8tx@Tv zPN#_`=~$55sZ?f#&8E^jm2#T6V?lB^b}H=#i_>h=YiXj61<5VPBo-@_PK!(lwYoJjiB>^7p<_XE%h6dRVxvT*k;!PQItd54QemJ&iN)Zy zSakMId|Yz7aoaUgt5k2)T4=(K1?${n*RdeE-N@xOi^?d5jg`jjSde`C)h>y{CK4&dHX5g6LGtZ4O0{wr zPgSb2)7Tvgl5f8ij>LAcO<_0FSe9S7C@xi(DetSqwC0 z$AaW`<1{%na71-UBs50Hg5m4mgpRCmcXK+9pAAa`DQrn zQl(OAS4h>gm7VI8OsJHYjAnyK=G0p|@v+GoZqULZ$|;tZ4YcDr79`iHQLS?;v?htz zO*^(@L2_SIi;NaE95rle+EmAa(T?a?kbF0+E)xvjDs`KUv?DtfBzI!D(qRn` zT;$TwR&;7NWI~P0DYBZ>Dy7)ciH}8YHx8%9;B>h31}$xQ$AaYBFBVBOO0z}c*3*`C zEJ&7OiQXd9t94GNopxBqg5-81HNpNbRl8(X+Myi_lG}~SF0R6C0#ae~VA(v~6DhutvPPd;-s8ZWBCYMp6 zG@2JuOy*SEWjdYIAvM+a|G&{U^7yc^|BbKWjd3@}S6%}fLp296MLgRyMJ$BtKp}U~ zNRINeqgHD<4A%TKDa}*FR0m;6%PAreho3XRI8KF7n8A(|)n>jTp_PX9p`gf7w)?Wh zijc2xIyuwx_x*QR=WLlwUC_Re*4?)qwih&0m=y}2(P+-wn9*p=9u7qqFrR=f$u$H+ zEOwcf^~bnVCXO&$H2J!3J)uevO{zIbMr+YAjn<+i8589k`mDk(Eb^trS!>#=P&>7L zOIGJ{%<98jRC8@s&9`lS&7wKh=C!D=XytsH18AAN&F)*Y({<5{;8Y!8d*+jc0tRJr zCMh2--=^8KOkY%#Q5%iPblk>cR`N2lio>YP3Nj3X-BK_bl2*>5_meqFeoeTNltzp& zV!SaMHnLnor%S-fWQ}m6huAX>8yr*Qt{8M_-fWtkVKU=lpFtQg7PA$f*PIKPy#}#1 zSISSNj5du#k&7^4rnX)Z$}rKT!_}yDH9L};O$WC~P5a0JJ5Fktou%ggRvl=Snw=t; zsM`}=h*)$%xr_5UTk@x&t@R0YcqpP3Qq4f>gxl}z&UXv^w1 z+OW7C0@S`+Y?lBrSdCc`BaE3jU2{xv^VtTKTBy=_^)gvVnp8+bp`s^KnMxNK(hyH6ig=B) zNv=4{VM(&6_1shSN5P_5h~Kvb=G#|aSQ z|4jkvq7FOl;fJISo%a8KMUNug_y3dMc%hEKL!oH3n>UZqHc0 zB?*De3la@_=6<8N&bLqfvFH^`d0>ab?)`p#VF4VUn-?hnby`c~YA<3xF{ z2kl}c%vi)1+iGV5evc1b9r2e)vJlAy;Q#Ho{r^;*czfg$<}0GyJ){XwhZQC*Cn*n0 zv$`bDZdauA`C>U@^oxbojD{~tTl6tcR1#nO^O`l2c1cwkp^>MM3wZiUG~#g0PDRv# zJfFih$;x@HQ6Ui~yWs!{^ShMsNYbr#E25%o#q8Gc`BJ`u3yT-634x+V zxkQ&<-~Yb?#s9BOu9&!eLOK5Yczo>3u}x#kM{gaKk32gPrGG)M(htLaib=sUAUyo} z@VAB!8T!ePbnw~1=)e~P)q%rc1VCy3GyPE*0kDS%|DE4{V;3GX1jGMf+m7RMIZO^e zV#!A1q-Ds)H>kq@cTXV@x$x*AldhSKD*Atet$j%U?%~=a7aldFYq_%aNZXhwBmQ^i zid}eQYb{b@HQ1<90`$Um1c{9btA(v?&VXLnR?KH>i`8JGN-EF`+uBh?Vxz*^#zvXxp%=D2TiGbFTG-mAg6M^9kNIqEu^MbtxgvUDTS8)^!fLQl zC6nldZIZ-Bh1FoA$~Msp+XRV?3af=}krBCL7mkzIsIpNs*r96?MupX2ql%>63)={ZjS8#5Mip?n7dASHjS6cU8)aPPUf8f!HcG5!w)V5F zy|96KZ0)fcY*dpKy|4|F*r>1?Y*h1b-Pwrsf7+dWqvs8O20wQH?5=_TOKM=_2{kvd zsbT-dL_dp4P`h`8-Xc-JFm9bE`l^qWE07qEpN>*wn}Tw zF|;DCHzby1?IJlZz?Z3IYPUZtD8?Lym`K5NMGUpQ<}m7Aq=?@m%{VZ#Bxg#c@#il z9nV+}4^q$MP-EK~@RYdpb?d$XYA#}<0<%dzsM_F;laDR|33toKBKJhGs2(>-^5IrG zd_hK-FVa|evqiC;FNl~*%2{7Ltr3+X=Au$ta0|F*(NqP-oNFf^#1?_Te``b3R(7~c zqvIZhHO*Sn{)m1{n4Nga=ZS|?+1x6DfRNnwfkQfp^kReQuwb?|N-5Hqd1hrBXzhxmU)aXjHR<$%8FmrN@Fgz2-%oY;q zpjeuXs{}rgM8H@0?JQ9=$BHWQqF6$d6-q>Qv(!BmiiligvD1`|8F^x!#KiWVtFl<%S>DYDASRJb4Z;u+LZQ0S~p8^gul%W05}@|7q)4(aRlz6Y7>{a{c-;mB|;B1 z?H_}vdc^iT1~E9)(Nd-C5|kQ`^Ie7*{V4m$W(E zq922JZdE#}@-TF)B-<@kOO#V$zRJwbyDiZ|*5kAq1SJpKAW!ges+gY@;z;wR*=biS zCUs27qVj?~n0GVW!LTJ(Q8;u{Dy7rLO7#|(nT0N$SpV<0((dS+{O9D~CSRR=cJh(Q z`zG(4{K@3^C$EHi0ya*bHJO`?POhD_PiiN{lY+_PCs#}!Fu7!Mc;btR4<_E4cyZ#X ziH9ccfqMsTnz(M_vWe=%c@ve1^h9vtv(o&`0H?w z!DHk1kN;}?r{gz{Up0Qw_yyzVj2Fh^x#?BnejD^QeAG40B$4(jJjU6|(d~Cn52^g8+^U=SL zzB&4r(I-ZKKYAD3$MD0^Yep{_{nqHYqovX0sDIQwY8;i1o;b=HJ$m$z(Y;4UM*Bwo zIr6uWSK)q#M@H@&xf4b-`2NV1Beju@BWI1|MxrBYN9-fo5%Gv%8t4+`myxG==;*g=rrt8 z>^@X^DE4DUTWGTb-x&!N8!y*l(P zjL&f2(4BC9$M=V>9I6d%96D<#HxwOOJ7gcy4vB{ZL&p!T7&>5R$^C=A30yaUbw{eW~})c|AQ$bo|f_8h=yU(!CJ zy+eDM_6+S|+P$!$ZutS5_8Fkj=ytjdY}y-HHo6sT+6!4$x&>_76Im9z8Eo1ES!TKk zY+8aW6Ws_lO(M%kH-JqO$kNlbVAD9VbjTV*mX@vtn?{kPp{u~A5oD>6MMsv3t^k`b zWGRsakfor@!KPtk$>}n%X$V;|x)f|0M3$5;0h7b;;?g+)FCvRWX9K)|EH<45@I10ubSA)a$YRnN0M8<8n!Xa?8Dt%gtXX8Oq#pqulpk#z)p1;8W7T0uV? z;9+DPPG1i25VDrjmjOJ8tY!4W03JZrq4a|R?nl-k$nqiUVERD-_af^c`hfuVAnQQ- z0RUf%tOMx#1AGQq`_uOW`02=6O5YdYYml`seIJ0IhOB+)djtGbWbIAg3*c^K?M2@c z;4WnCL0C)hs$ zE<)DF*oOc=1z8^<>ttkohwWA!fUiQ< z|6=a~T!^f{AxnU)cd>T>&PUce*xLZ-A?t1IEr4^8^%nM5fOC-bSL{uIvyt@%_By~> z$a)=n4d6^h)J^Vo9$KN?xjVb22mC}cg0{R!YlBI_CKDS#h=tf!H+0$ERCPXhdK zWIc&J0r2I>dIEbK;LDKpIQAI84@1^t*rNbH6j_gAj{y7-Wc?9)7~lsZ>tXB<06z#> zf509B_<_iJ2zwCV2O#S~?DqiQA6dW09su}$$a(;~AK*)o^*ii7fbWZ}`>@{v9O)nS zTkKwdBmKkf#eM^Dq<`3NuzLWG^bflSyBpw0|FFBUy8w>#5BoLtD}W>YgX7OH0gm(! z`z3ZKz>)r8cVfQ)IMP4t4(xV-BmKjEjx3~q*zMSD07v?V-G<6HV^bd}bH-IYAKkNqV`=E;S5Bom$Jy1pZhkXyb9#oP3Vb^2d1y!Vf z*mc-7po;VlyB1kU|FCPYt3eg%A9gi%6{sTpgQM?vKo#j9_8sg>P(}KOU5Q-*s!0E^ zE3nH!73m-LZR}D|Mf!(bhAgCi*rnJdpo;Vly9B!!RFVE+7h@NJD$+kVs&58Wq<`3E ztOlw`|F9Z%A*dq#!!}_!s3QHtaI6ZdNdK@ZHV3Lm|FAjiTcC>c5BnDOO;APphkX;f z0929w!Ka09fGW~IY$J9)s3QHt&c`-@D$+k}1NL=LMf!(*9Xk(Hk^W)lVP6APq<`4g zuya8b=^u73wjNZG{=sLDvq2Tn%8iu4c0dn$q|(m$+-6+ji~AAF|CgDTQLERW?t73m+A#b!Yj=^vIs7Sca# z7E6OF(myPXr9c(wAC|(Bpo;VlOJWI7CG;P$l#) zjDJwV^v{jCK$Xxx7v=<2LjRnY15^q9b6|E*CG^jZ*+7-h zKO1HRRYLzPm>EN_G{ZnHqP$l$Fg(*Rm&_5-n098W&6qp=T3H_5}GEgP-PX>4RRtf!+U}8`u^iPb5 zK$Xxx5q1iw68d)vb~30E`nMW85mX8NI|*5Y{+$Rjg;xpvI{{k-s)YWn!i1no=${Z1 zfGVMX0*nu;g#Pg`4yY3P$3+&Qe;kYrs)YWrF&3y2`p3eUpi1Z;6JvlXp??f)8dM4W zJ04pJs)YWn#Et`1LjR7#ra+a@zbWikP$l&5SnL>3CG_uT>_|{0^zSHS5&Cx|b_A#r z`ga7j0#ph8TY()8s)YU>jx7gOLjRUy%RrUTzh&5Api1c9q1eHoO6cDq$RhObVC*1J zCG_tg>_AW@^zT6I08l0L?*MFnP$l$le{4TcCG>AUY$=!{^lx8mA23Jg-#*yhV2;qg zy|KN(9HD=EVS9o(LjU%}_5gE){_TM+0ds`@Ex{(i9HD;`*f^LY^bfvFfjL6|#;{Q^ zN9f-uHUj1d{TqRgD077V(cxRi9HD>E0CR-?4P!%Kj?lj$Y!J*5`ZtITfH^|{;A1+N zBlNEy{0Gbt`qzgnq<`@9OE8D@4}Sg|%pv`QpI?AEq<`@9UtkXDAN>3r%pv^)pMg)n z9MV7VDYB6M!Owq!Ii!E^^J6fF^bdZ11m=+b!OsuD9MV7d`2m*`1u}~L;43l{};?5{ez!>19M3Kz`NjWFo*OHzHvbd=^y-j3(O(?gP(r|b4dT- z=bK;-=^y-j1I!`)gP*U1Ii!E^^EEJs^bfoW-`nPp{()DJh4c@8z69ow{=v@|!5q>* z`1t~uL;43l{{rTa{=v^bgE^#s@bh^vhx89T2c89UNdMsHpTHc_Klu3!m_zypKc5D3 zNdMsHQ(zA1AN+h0%pv`QpHF}}q<`RX@EDjw`UgKBMeG0j_ubbw8JPIv1bh6l@xHO( z=>4NdkDN4_rBna;n#*$LqC9TeBpt62aclQ{h#!o28--6KkN6dojKV@Hyvf_ z>w{0)LqlsC#|Jo6TZp%uu&D8-4BLt8uvG;2i6h*3Ovh>o*-k z_|=d68rimIgK|Ie7RR;(0q(vZas)ZG{!8@x+`m$}Mh-XutE>WD)77htaIG=-OtF1= zn_~%YAk*D1I`Zg@$I$OHKcMofHJ}Ac2>mLfDm%PUWli^zYOC`W&(O+lcfT;?QR#Bx z_f=GW&8x4awG!!8f^D~wa#QpczqTyebaxFPrwTOUccf?6me-2NwUZlFHr#NHY6tZe z*XGaMcK31^`Ip{^erKk7_VR#Q0eLyy@N%f(CDqRBEnaR51CZ_R-4OCCc02kVPxS2F zp|w2nPT25nu;CrmPVFt;k&fAQcX1Fo7k&`^jwpL}@t|4`xyWv~IM8sBYJ2w<7n{!! zcK2@pc^0?@{SH!!-Mk*MZ!L@bYf=^I0m9)$ zo;e<*@{Am~3VN0zdDa*p;O1Qx+oCNiL?o&OdPWQp*81-sP-?_F7hF>qZ$PuZ>(bqM?6DzlH9nFNJFs>);&R8{jga z8UAwk7PzLobm*a>bwiB7*9I>cRKd6aw+#3PmeL-gt)nseU+cf5Ujw59+_D3v4Ah$j z25ImZ{lGG_OAn(o`^{lhyl8gIy%_^()Zg~_$rm#DLbqBnD)wsyFo!e)TN~ zipl5uD-v(61&qN^<7Xo<@SlZ%ahQB=D4$8%=7F6>fPJ|TFgBCVF_aW}<2)zU+0JY!tDFaRCIR-v0>M}XW5L^c15llvL4f^hAz(}&;nHC`dWK3@nJ zgUM%DbJ8?GvLoglqY?&h;@yyPu3QXSX)K(c2bLqiK3WJEm&xNKU7AF69$1zD`*0y(943#g5-FYjd0-g=?1P1Xv6(zp z)|8Qj=Yh=^Zd?~BEa5X7#K&W z*4rIq9aiH>0_?qofN_}|LBMG!lV38`c!B`?--UotMYmDoaRTgb3jw1Fgrme`5KIT& z?LN=lxSQ>x)2Q(%;nzD0gW_<46>VXD&x{aYZ!Z9hGLnrN4-;T-Ed&e(s%aZKMvaFE zu)i(@j4CpW8V?d+Z!QFkD)5UM4-jB)ED)@1To*O&C%|4`2pCn^7B%i8z+PJj7}fHO z68Azd9eB0-+}pM$OR;XD#yx~zuPg|P$>tQwiG*XGmaQegUS0qg<@$gcpCQ0rS_l}` zjJ_H_odA1rAz)M!?`nJv0rtW|z^LZQ)%a-y*k2X`Mm3|Y#!n@{{=5({s_ADn?k2#V zUm#f9DPuM6BEX*OK2f%<$x_axs&OX)_UuBysHQ*FxPt)u(*nRKX9m@{odA1gfnaT? z@zl7D0DF2NU{tehYTQbIJ+%-ps>v}mZXv*)TnHG|OqLoq6JSp)5UlO&lNvV>V2^h{ z3bd`AQBKPc=l=)3-#5x0z7u}z{@GmvyK7*VtAPzmYHQF5!3!i!Gp$s!)7>?O6zc$; z%4(D2m%|Xuk9DvHewP$39Q_k1UT=M7939MRK8@COq8t^-29o|j2A}kx+#2 zbN(6J`rl@P;#gQ$Cpl#uGr5p6`5sA6+^;HJh)U zhM7$32k=N!r=VIH={{DHIt7aC4)Zn4L{J!kw*i>BD%^IdBp{lYL=@+Hq=nhT%eRgU+|{`g%nsQwjc9LdJQPDGNgOkYGuwpi9 z&gXnKaYZ3dXJr+;SEOdLjk0K|5>;mve2U}x^?eMe>+J-UAuoj^niiD_pmq0bIFd%P zf;Oa}Z}=nGblg+kl6hdcCz(pFgppKWeklfPh6xus8H|-MPdxf+Jzc;|J`;xk$$UKC z{B$IQUzo2+El)>OPjl@YZ!cz4T?~1*STTe1FO*`I5(f<7fX5c(hsBX;k$_X+Nfa`- zFQk#A9Y$YL%%6_pYp z)k>Xu4p-unhvNZ7LXqd@1=A*{FlJHYd?`QQ9-WRF*$TNM#Z_7i3};c)Q@$n^y7YSd ze^%cmeUrCLdMEdqxNjmqapd@)#y5@&;i>y8#gBhr_e(=hFr7 z{O0AD2AL%?C^@Ar-wES@!@&!Zx5;m{xxvZz?y*x+TFA?ZCU>l{a@?n z^t}N|dj8RF9vd|6uiZ9%!J}IAf-xpoTps)5z26KJm)z%Cr5!p1I6i;m?i1D&Pb*yU z!uTC~Y5BFJnMlt^Y|kILq|(Em^@}&k+F~`i|cTwi1s#}@rw!DJN z;5Ny%+Rb1doFEb^D&=kAaAo?mPsbzgNcXE1B71!_eZs%4dt*j?;Asa0HlJoX@`)d7 zHxIYMF<@M-Y(eTHZ#1Z1?0$QG_o&!wx|iR2G7wpE!3+Pn`3&{smx9S>mOp#`>!#SZ zUe#_Mng_=#mcj*cwun01*T1&+9=RK?=)3E&5&Y>1>*UY9cfR|hM}DpTx2!*Q<*Se0 zsNFo+0>>3Hx!i=z79j6jsKaSqJz&Ziy_0doyX#-STKD5YsWv+vtwwHJe#yrh?|=Wa zn0E8PJUEWLlJf`K*4?-FU9rLXh2?kWe!)NGnq^OVulW60n{W8*v%h`)*6*)7>gz9W z&~B!+!U>pMb}k`tk(bZw-aYv(3%1|!S0A%~pX{MyzWx3?SDiKbC&~2xUVOiI#lR>1 zXa7~ZxqluU%bQcA$qSoxxakw${^Ekci=Y3DcE@wEuN`&5r;i`vzwSGumwvP)bJ9EB zB5cNet#DM!!F9MpMjtrk$e*8b<*Wn}|U0IT|zccp4KZ34DPo1@&aqPri=7aN2UGC8hKfdnC*R-{>$Z+kKgzIp7 z^M8Hi%lDl7tAl;`2}j=>9ds;P_4XNg_Km~KbKzxCev-Y1-Eu9V++$t6#HZ^gymeEjwo(?DB0vlWhNS-1`t1O1m>dDFXJzj9B} zVP~IIy43ypDJj+XVMjWBKYrso*Zt)hZS9O!II4x=I-KFqi?90L>L(wJ-SNsv&)j7H z*N=a>=|X$R6kqC6*uVVXyR^ z&n|l_(Z4#bt(97Qqg)=Y!yR<-EiYYqz^PWnH($B>#y2Okue^Ts>FL9kpLxrz%Qz2D zUV4nSR&0f%%4AZ9yYKthGk4f8{@~-^p0jl9N8vq%KU{jm*B|}STa&i;PrrKj!Lzir z!aTUP>&AChfmmU3Z8lL%*D68pkZ96HfgoBd@CH)igO*# z{)egGj{iCIz&9^{^wOt!yz|0qcuyKP4uRhu?)>@su$`f;u|oGKe_C~8-8(UZR0x^ zs$V?(+Kp#0_xZ-B=lJ&V>`~L)e)R@zEz=4|weDPpyLUtCEaSJX+5az}{{9!I{rviq zhL)_^uxj+V)84uI8;5Uxc6>-%n{9=oT6wO+;Rn8bV}B06e!pV*sHYzN#e+WVbk6-B z_#WDH>(9JvZu;b9Z7tmj2cM?6DP7P+-s4b*3*Y#w+rItzx360Lr2Ca6>VLCKKX`lJ z<34&|{Rc;{uNqIh)}pSZTKWXVQg$8e;nR%YW4yWlpAK_ARW0AT_mRqoJ^IZ*es2_9 zoIHu~pS?cU){?EhQJtHs!@cpK=+j#+nCZV|AG+@>?Z^Lq_4cdoO@m86I{#lU2ZtW} z*@xO%q6Lm}IlB(GF@OK6yKjHr`QfjRz2lDi?^^eRi=yD>@%vw}T~IuvT6nyst;Jj6 zs1~&AaKGE@-M|aKJ+tt1F!Dk8ip+(w=YRU%UmnT|rFX46Vm<$mKWb~SRye99?K+(P z^9#l&9$P&lync8xb+qU<`!(mXKQi3UE70vrLBcq;iwk2>u}t^J^%H+-Ll%)8quSBgoR&XfBDT# zwbwq6iPr7A->;roagVkZng`c*X}bCa=EO`bRJ zb3Fb22dw-@+FGy`j_M3k9ghD;kaZvZ^D}<@@Spv)kM})l`kyC>zw`H7UOZlTL*keS zZ9i=-Fb}TnxuZJVca+D^oTypz#MiF$T{iH<4Yw)pX8-KoH*V}ZS90uKXRYnqM_co^ z!cnc3*WteY%a=dZs8;-7^U0q^ryNJn#^3q&HOKxY_{LSWzg_LTZhyA6=4*wcI^k1? zd;gN#_xkaM$6pxx@fG{yzuqUaUh&{bw`VekUjKP=-CLuMleIN(3moP0dmZkve{(P1 zTl4fMSFB%hM0RQP!_sjlR_;IHpUk7=iNVUHCtt6vdFH{jU4XB{9rNJL?#lmsSoru)H@p^lBXHbaYqYhst#DLJ@O8M~-SUlyXrH5BKI7~gp7Uos zNwxmf;0Lrjm~Y^FnO-^c@szeU(+Wqm2w#W$PH@I|#2vo3PT%vgP5+RY)IWUvl6}s3 zAKl;MxwWDiC4;y)X!Tk$Dm;G)ea+w?hJJbl@I3nzQCxWbWY@AB68YA$C!Ie*IF4a-O2^4@0~(GG*jJxF zUN7I_!dnl~GZ>BX^aJ-7oxve$G=?8yPcz%CZaxy@JypJHqsC$Zl++Lcq@`P6lX@XS$M;M}?*@^m+H9(70CLk8PR zsvG${fChQ8D>=EXHLjd~giN?0(UqJG&>)W!lM{X}o zPEM_FJo2c}ARl~x^@d2-nj^QjMtS;*2ZS5KU7aU8u+@3thEUhCWb&y7gC<<|)}_`5n!c3`XX!VSKzWy$1Q%dXzw?dmvrG;BBy4me-9 z!PAwT4A3B7+pW29$s^3%GKliPWNe2M5# z;f7PYl9K@%rc(Ih-iS!3lFBp?v*Jp? z?@Po?AIn~LqQ6TSA(KL$_p&%ST~t&uSY@!RI;gXTO8SbZsI{6PS32uu>BC+{xZq=( zcm`>xs1p$!EP_L6=K2)2kkKLzyIA_TkF7&oWu1fIg4o=s+X8)%g~~dcKB0*TBk=pP zdX`=pjAe=Mx|8s`EN|Do48`nj43JSIXPuR#w`{FHO6$oF7l~E<$@e` z%olgLDq|b0JE&vI^kFxP6AdU;vs$S+>$dxiR+gzvm5HOe8F842)IM(WM_C?SMH9=U zbDE6G9t(L~qJ*{iG!uDHLbm$@10q}^?eGTKy` zh_lkBWd_xeN`+sJWFp0YHDd|dMaGCC?&c(TcEhZb=i`R7@f7TsZ|{Otwxe2^G(PJo zyAxJxysWkRm{qI~fI5Nv(4fS7!ZjyG0#Vu(RcaVn)YQ z^2J&cY-MlldR3;|QB@{0%T%ne(jyH;y-cpfrsO1o$yl2zgFD{kmb}Q8v)c8dj9w~t zvAMR{8A(O&l_=CMzhPE78*r30UY;4Yve$RPD%(-=FAcj5alJNRwet)td(16!+szSI zn<|q!7-~J6Eleu?ENjFT)?{@GRn*9mah(FcBbJKKxU@{$%4o=xfe#t4?Rr(F-(gjj zj(L?_mQKb@yJc~=$8P6ZYGRnY=5#I#|!CZicPTW1=8UNS#6~h+~pD=Xmkah4MgPR9e4%|0-{XmrV8trSe zBl_>|pXqxZLiPMJdf9-|Q9X6oWIAfRZ`)pfF4bn$?vaqTL`uW z+SpW^H+v>(*V>GbrMb1SsrGdC%+hFWYpVu`E^N9DY^p7uJ)@H{0IaF%Aw_eiXH#wa z?3seppmmU-xj~~i_WXcUoGFvG)bvcyxj~cX&Fc+Xi@a#ANvdf2J$tc5x(BKn@&!@_ zo$ns)q8IU=H@S~45DJHs0^KRwdAj=I%`yGPq|iwgeR>cW*Hy zYVO|fEGUzi^Ef05aHHlOaWO_l75B83Ko+lrk-rg4DfNLxSe+4Fk(E_&$+7mtQPF$L`G`uSV9p_esc$ zrtS?-IWl;`m|LM;tQYm}?W>*$k(=8Ylg(tXMaq&QM@DujY*D-4?oY${9YxHXbcjjc z=+MqifGjQTjKyTYXs7P^t+`j0Mmt+mT?Nrw8Ud5ZV5)*a^Spc1D|&MyfDh4QTOXw1 zldOgGBujBVq-UM&*c`a6djjfDvQP|#s68}2PD5>AF!BLug?zGfS+7^z(G3oHGYYDG zdsQ%OXl<1Yq0pAlL_DO1(X$g<#+->Ne>ecIZ|Mq|Od|KHIX*ATR?6vuqNG`$4X9=v z?6NFs_2@P1uv}BI$^8Yrm7}2 zG0H;$Jr9Dd?+z-6-`ou;vu*T@-%jq8kfpgBQfABOnWecKD#&$2b(WxK^yY2|TfH0- zG`D)nbQL`lbZ+(JeJyn@mLV^iw3sqCMbBQ;n>`W#k2cdc`q}V9@MHJS?i$#UHLx*M zTZ>}l;ImD!a;Odub#J<)7+q9>>zK@rv2rN;4vZwy9P!1QFtf}~E;k!ecvM+oL=-NI zrsM3AdCH}$1P!w)d){J^E4kVt3M_TpVa-8hsE#0!C=x1B*sDx3O<9{;pAuRGnuLUt zGE~ymNHCXUYOQQ@n$Ks17)8!J3M!!^Q6wGipsVcQxTeAZSvq18*z9J@Y$g^DJ4BVN z#m3HxMYEznD#9{t36IqwR?bf2Y=pofiHCz32ga)FjG zTqrzTmxwpW^Y)Q!s5$^DvDip90yYyx;b|L|tCSG1*iuh z43e5!xk+lM+Ut&!8pi*dQUm3z-oA+zoQ1vM)SWJGh?ArP&Cg>0B_+ti@SEuR&*(iM z>8iuvixG;CMU;90OtcUJuI8K!JdQhBF!i+_Hv8{F{;P?2@)e(Q$qLuJk z5x75)&V}KhP>`)f%OzDO-X6K+^I4SPQ&VP+b6RWjC=;%n{?O~Etl?yG_MVr zOp{zvJ+YEe;P!bBrU7I_WhSstWLhT_D(Et7Ph1v+fd(^~ zSj;UA+vR$rJ|D_wW%(e3#Wp5*QL(mCmg?du1w$6p8`%uWtTL38xmaE?e27EZ+bxPR zy!3kfzZHEy>6_d_}<|9!Mz7=7+6L7BW-Q}hy5G+4}?HF`6qmBWl(1_ZClc(m!*TOhe3!z&4B46}c7{#iIigNw7<+>KR;1|6X#<*HAS=+cU-77JAEsTJ*FtY8b zMi?KfnT%?6ddE~FjGnb%vPD-TjIFgWvhArx73gGM!O4;hU4)qG{$$rit5AbKr~2)1CdMc*?RFRYo2s*)PjxII?}qlqn; zY|&Q+V~sVFQTg3+Ws4?*k;t0Ks5VqI$fyEjb$=U(UJK)tEr@J87KNhM!YF17Bcpoc z@8!y1jI)K2QN6zQLIxwCHIq@@k!|;EI~J#+*TVQ{3nQbN$L!_GVDz+wk!{aCgR#{X zMz%fo3`SmSBBNZh-7)tJ#${VDS=(S{z1%YxwQXT!+f$7&rW=W@Z2-F+Q;jg<+k(j! zU5zjv+``DVry60jxMnh{F59Ta?ICk7l#J?~zL(bMS_Ijm@0qiejH;3v)wn%Z=2!&T zBCo8z{{Lj(=y_mI+LJr^<$X8&?i$$nHL&r7nvd9XuzzD_n@ts+wY$*ty61q?d1jkR zG2rZF^DOhiR_o{OIcTc_B#K>bTSJ%mc_EX zmHe{;b|`C9W)y~LS4quq=fuicr;)dHp95)rJ{QOmxwpRh-3Dm3cI!$RAKLR*%G%C| zQ#R`Dm=R|)_zbkIrtOTl%3~@iOj%h}mkO8yD!Jath}koKUzt7Y$;tT&S4<>N_!LZ; zXYmRF~KSV(z; z#pHC>oQJDu8Ar|+tT-Yujmd9_xC`#QgE1}YX-1s5bht2kOv_pwa=)$OQ1*lEP#l&} zAV7N;+A2@amM%twL#!vicB7-Pb1Xw4bxJwnVk=%JiMZ8@k{XvzTU); zW-|<<)tQ(MO0ou4BqDZb)v8<|YN;fOVlPu4G)lDzUfwaqRXBKlTgI-51qFp^l}N@E za`-AfBN^rDV{X6OGd0DF6nk8QLzfN}r&RMG#o4r1tKzh6wmVL7Si2Qx=O_-{daVyjU!Nf zU;ui!HK;{<&|3TUXiwW7v0d83;Gw+>ZM8=sF&BIhO;{t1hpe_z*y{03DFj-0le{Uj z3C6$h3j?CGsSsAyGqh$Sp`<65&jjkb#ahqS!C~!;FewN)y{tf+|95vYJhxag(y>K*%VBY zlkGL`y^twn*kT3Uyt9{TqqQivTBIJavuLx1BDpB(+!XdCnNC0j)}RwG1wv2B7+nP2 z6Z7b(z#4RXra<8Ln-Uh1@5{Ph-Q)AT zwDoXxkIti`0&CE5nF6jk>~jVdeszz`qhm7#9BWz<4wJwdbS$QT9kJP*A`Am{{&b?zi*ksK6R@EGD1r%$Bms zh11Z!eCQ22_>HzH&b!BW!+ze5t}R%Djw%Iu4|KQBqoV?Ap=+BWy$8D6+R?QIYtT`p zQ15~6R)Na?OZ@{%g7$`wZKJtpqoK z{^9F~4;lL9kQ>JTiw#Z;Y#ca(b|3B3{-^t=A;>QAZFJT>AvD3Yz@*;?f zp%W*g8*zEhPRyT5+;&rVR71k)xCjzWBcgBjOtkHQceaI~?I5rgKLrx75y53W6Eq*; zY*0`MKfGlpZ@p!g_RP^Sk_eX^)v4hH)p+2>S0g9TgwpGGOX^tS^G?Yc8%7V8{xb>Eu-!n(& zR!L4iw5?TgIZO^eV#!9y(}`|efSf?BazoEfkXj{WR9UF#eBu(JqMz4u;LXmL*c#5M zxmC6gK)bC9I6@|etu7fw)& zIgoV(LK-ZnW#FzsB zdg*2JBI-6Saq%b5;H@}v>#a!bbU&$1Tdl>9fKCu?F440SZHGb1{Zv7G1*9NkAl@@Y z2N^&_Z8uMh3_jU6(sym&#O33h(MLwYbQ-vl1_ly+*ADaGpZ@zl=lX}&oaZ}~MiFXV zKz_-A8Txja41>FPx;z*%{CEcZkJ4cjdrD4 z-eJjKc1|9{MlC-Ew-~`~M!SD@Vhwa_$mF0tyHZ2m#nZkv8uAm}YMJ~HxhvJOZb#*c zlJAu4ij7)+q)RRH$(fUPp<3S66EZex`H$Udc^6g;_6}1Fx!-A3hIw9PI_ZeJ%IdgW zY>N{Ktjn$WXr*Pj)L0T2Qsu0|CC<1*@>zx4n&2qH*__Q75t~U zaJO1+U20-5cBQtw!!l(`Rb0JYZE(hgwqS%C)Y@eXCtK7eRhn1{_uVH08J9zu^p+(o zk(wLNnTza%%$m^|OLDeTsNwk&ToKPIYO3WwbgSiE_?3jc!(K^bX{~{g(|8$0rk-IB z>zHDVE7~sWtyH9s<@w44FK7yJZNeGTN|V5ofKjtcu1Y0joQZH?{JW?%<16M(O||?` zw_0vp`q{NB&ij6@VxUDxCeJCBnTXraC| zf{yL&gfr6Q*EK}rOC@uGBnnXuW3C4hUKr3l97&_|cW6NieZwEgrsJOSmPqW&J;_vZ zB}{$?Q^zq_Gw>`0pTStk;Rw-JI|f2VDHysOaHN>Xp0{3txY{1VrtHP2#3Iv2uvrW? zN@>ydAY?H*9g7$88FMDWo?=&o9TxMksB#8PImC2pb7 z5z>hGS~=g~mzg3QZ%`kZ>Mb7n)=SR>U<>dCvNKy_STAp^SW))Ib?fF%sQHo3e!Dr^ z4Au7+th1CEZn(R3cH80Yg&m}B(OJrPiLfs{L|Y4*AS53;~M~ zDO$Y-xsYY@2c;Q4S03`g*(|zfam9?;E$legFR$yL)VI;?c2F$Br5OWb}8Vn@2r^cl{sszB|B` ztGavDR$6I`O*fFlHU(f`Z{H(0-}m;PeQ)P?&bjxTbI-kV@11i@ zd)t4oT{r&5_$lLSRSjc#N2U3NkuYjEKDY5^?WG&s#xpmJhPw=JGxQ90?Mn>j>A#|X zhkmTNTJv=EuhpMazeZhBV>`F2)v7yGZ_<2Bb3&Wed-RW5|JU`uU4PzsVEysB@9EyJ zd%iBFyIA|c*7py8`O=(N`@rF-hav*j6xElIY0igaMwVR1WpQ9>%x??F!el;yhN>AN zUqmgLeugL!Xf|0o#~tJdZQ!y1S4-=|L3r zg91F6dVC0tkU+dvXVyLwb2gj9|GhA0)Kcxsp>J}v2s`|M+ z+9umIdktqh#UfcnL#dqGMOd9^EaB-i7)F<#QurIGY@eyyV`PAU%|1KBR4YBkOotLB zd()1%NXn84Cxjwm+qiL`g3P;vt}tvZ_B^yZX?0aHJr83Yr3lBc>p{(ADiCA&0YO3$ zujw&Uiawr;2xfb!ns$%v4zio?IAUxn)i7hZLd7v;Lm^LD7q*` zPeP7TEmrQty_5$^MYvqN4~7};;W`1l@_m|O1-r6NP%>hUw=4*5H z-Z<&4SrTSn$2y|8V5&dlvy|pdQ;Htdi3rXJ6Ha%#wRk#Z6C9}E>IG`NQz(T~ZrV@g z_;?J0`L%aWDY}9!J|4p*#=;?x$JI;N8*pS7#DNYxWljxx|L0nwk&VZO=VOBPskdzB`PgvvYqS<)BPUG zCE->(kSG&y-Bs&_+72t`OxCreNMVTv$)eK?xjC{%50C;zWgX67FYl?K!%omnC+%=L ziVr(Ob$CjVBV&bLwN+t!aHH#>Qoa$4weeU4b@Jn)n=E7P_7HdGJs#WE&XgiU1RFvj zahXg zt99veJ6Fn+NYRZ_{aDiyW7xHqPbm@}d{hgXhX{?C{bLGgj;Uy;g4xMTAWYX4cEr84T~ZvpKg{^@J$}+Kf3U zibL6G&C(56J4qh_g$hG=yzIsT>6*imVO&FpGtcv?_lOj%HJNS?lcA1~@8P+4b%fTt zNVy6Zu?nb(2{@wAUnV~OM}YAEGVn`!~Ir~LJh~8 z;8-eFYIR1*aKD%7RW0^Wsfa`}RLJK^gt=4?$S)4H%#>o3=D>wRXdGl+eGVD9qa&h4 z_R}cBMG?WwaT$MsPun6HfmKcC4|13x$sF8DW;si&_L%7s+LlwK2ixA%CSDA5uIPD_R9!+ApnkQ3Mau)OXzdl7wjo{UQ_gY2<>vF54u%Z9 z@u6kh^#$^^sfdhc)N+#v7Q$*x2k&o=^7*(0V{C+jN!DBKO1$eS#l}zs9~yE~iZpI* zjZ4*LqUb6%%uKTECBrO99?gmr0Ga3feL{RrTM-{VfOVAUoqYdSuqwKCK8Pl zAM-&0E?XPr+IY^%(qq2T&Nr-U$EOr*ae&yHp}Z$s9QJ}qKNy4wmI8TV-56@^3Z&g` zM=PCXN}#mFlp>z&xq^5tPDaUcB3grxQjQyh3Q%J}#Is{3iMMC{+1`#i1`)G|jCUz`5G|!UaN5-!GVwr^rv_t8RTe26mX^!qa3$Ttfyd*^7s!Uq zGwQff{w$mIHQI@Aor)3zAH?WCc-j;nm{LTOa4?igTVQX5>DxM$jw6~cpy4{|i6xqB zv}6t!yMti@t}*)QFef8JXTpO9_)x{0;Nq?k+^ADtxEH6x$*dD8Sh~XjUoey5Hl+TW zsf@7K!*sftnm@p@F7IH#2ZOZ;%%%Dsm!C5G^F%WkXW=59@M_#sih!N;g9T2g>F*<% zWISwl`&)H3O9c{kSC(-$Bg53Vlx(#^am|mX6#itZ2m6qO5Mf7yNGzSLSAA}%g4qg@ zuq~Uz|e88UdYiER7i)d zl^~R}2a+&DCc?I`@nPF+Y?r+oKiNUokX784|9vVWRA^b8bqnXWb|^~#^X9R!hfZV%0n*V;<94sLgA;hQ z9L<$C|8h!^sk{8~FjQ}bvosRw_!~Xc>w_GD3Q~d6A;Ik%QiLCYBcyfX79jcc%ccL` z|GM898)c3Lg${+fq0oqJG`rPy*@hy4ESUcK#gq&7SwnOv*h{Cqn}4&P|6ki!GySQ7 z2fxn!oYTNL4V=@!??eNj_M}}XRmbH<(~&BG{hPA08|%(}@Crfu8}5hgo#WE$=M$>gUHLOOX~*}QAbZk& z^pe9%8@~LceU)=gx_Z=;c1)(gf)I$(t)h?WcKiNfvpvAF?RKCmI5?Wi=XyS#w~yH} z6F+HKd9Av@!qYPZU>b2w!wibPS zay)lXV+02OKIH6lB4*{7SZb*=8WRvYY17i%C%b_6oqUDb^XDq6&3Kc(0f1ITVfw&~;D(pnsxd2#0Ub z++3X*6yUlZ-WV2w(#$VMx<=9)ahYmi;zJmYPmHy6va~FYk}FSLgh9?#F=_?J?O3(c z>%smQ8fjbhPnWR6FW0TQdI>Bsd+CFVm;0&l$e$|soSZwz+DISQi&ys;OUQv|%eHcz z=?pl$Y_kJzziLUeY8H8L#!{RZcs5+6xOlqC(#~We;4blY&Mi$>wC@pz&H32Ec{va> zH|O)^Q_-_m9PJlW%C!o@#P2G_Mq_$I>|`LC6#iI@M9Txus?jiM`B5ihgd|o59*Z3b zRrit&%pJn2l{B6A6!NSk6)2(A1Wm`&MYa?R)$%m!JNjho@G zq0?adBhlHVJ8U%|P)&itL6VJFjDmOV&62l(lO9iSJhBAcyODy0LHlsI8)$@*!AO~g zT=Dv3Z7_-UeQr{b090U@TzdpAk?Bp=p)d~aGGYjUPP#tL|L;>je@z?FUZVMh<`bIN zX&RbmY0guBL;c>J&+WW*XRzbhdF=N6+yAtE%XV%X+g{(gYwPV>*KP&2F5LXd=EpW) zwOQG`Y;)7}71O&-H=1ImCmDZf{G{;>M$UMJ@nIYPy76}#FW5+LJk9W1!ySfO!5smY z;W7H}>Hk50Tz|D5(QDVgw0_(Ac-;@O3O=Cwi0&1-lFq6#YVXs&OZzE=YIe9)__@a)rtfUASsySz3;OQMpcis|sCshf1%2~!=!LP&B23vCI zd0PTM;pEWs)&c0I44RntZ-8#dq387t+Fh4JFX+1_ho09r;9ZqNFX+1>ho09r=w(?B zy`b-s9C~5w6y?wh)+tQR|DU+Vt!+Ty{k*&Lx}Ed4-?t5I-Lci(+SK2#KcP3R|J6F9 z`;IQBGiYznUZnY`<|>UweXaWOs=rrVx%TtTKi_=3=_96R81FS+XMFU=`v5DMAF#i< z2Fe3I`Km{r{I-rH!Ka5MkE<4+TbF_IGOipQt%h6aUb>!j4aehfM}kn2hLVsffAYOW z{`e5I;_|JvldV`;Tkh~;XJRWBlP_L*d;p~E%}QBSQckgylV7^K?&K`@RTbnE zbC+aaz3O-mh(IMGmR5~ZEMhkG>J6IXJP-aqz|7Pr_7MLU>!FV8aRQ}82#dSc+ zY`g$xQlzz$<3+~y2PALwM%bdOdq|7?~M9R{-Q;Ma`I(;5+ zpPeL$RVAksbC)=M4hWcadSX>IDa8V0oqnN=)zs;;lUB?){mQaaid!K%z3%wc`yIUC zBa;~CJ7S#MlfYD#fC7QJ!u{%>5fu5C9q?=m?z-fejN z`U|u_(_9VC)~zxx@@vP=J7zb4#QYci5v5!8vL%gH520+M2s3W;IV4cYxhsP_8YY6y z@{qB$hK!R!Sfo++dLnope7zWo+AAy)Xyj35KZ7Wf47Ae@dq0QZwRF@*dR-JB=kTx( zDf8I`Nu-?(EE+E_$u{cD(z#gNXQkmtI@!pv@qmR1rHQU9(2P|Fmad(%*b-!^9 z4X_U>o7B#dO`PRUz>^LJINFgDa3V+LM==k!#3ptuB48HYUGT;oB^Q++gG(_9uWg)) zhYP(nOfr?YtH+NSHg5^HqQ0Qt>vV;wfMT0m{#Ia?+9CQp1Xu*v0z*+jZW@Q|{}*~+c%48&4Mj}5!>BOj7B7htl()R*P(wUx5% z49}l&&U{oUP@iDHrRF+UbyDn{2)f*yLG< z*aVlpEkA^80-v=U;U2u2MkDc(KCu;hv2d+V29P|B)l=~iXHPJx zToKKt(nCkfRZIE39FL?JOT^Z)`|Q4ui;YL!N^SDR9}Iv^{`fGP%%7V)q_Qx>XD!#Y zv(ir^pa!SYRZStZJJ_^BK6Gi`_G-2^j?+RiRq=bxbvrc-k`XpTggt%+6~+U5B!WZ; zzCRvw;g%y3wMEl(IuuoGlh1tu*yNdq*+jZw@K72jXRnM*x}W55M^G5Wy>Qz`SJQ#8 zi(N9GIIIK0ibHsgjMvg_Gf26T%I5{T<84(le8H7!bGBfkly3}z?P1HFvL%^FGVHf0 zw#jX81U7lbVK#XPue;#0S3oH}^jCY0G)!kBDW;RC+nuGRb(u}Vg;8)oCzDu#Ws+nY783M#SZ_)`G=BfdmCc{N+(D)`46-x|4KO zQn6~z4#o-Xi&JrX+DSPS+vGVf0yZHJvB|;>(T7m%f`VsjE$oD;Xx&=NI}5E8SE=S5 zd?yS|-!JFufH<02-r>!*Dk1X_YWl004iz3^=JF_OYh-z%(ro5iR3ZmctYB*~tUW=| zY1YFkwn_Bgjo>BUuCA|X&)3|i`3p@~^T!&4`t#~H@7%ug+MUAA-u8pr9|t%8uiU!(})wDrQR*w!Cy-oN?2%^NqF&BvI&X?mAQFgZ=<8}Bpzg|Tb=W20f?^BZs4sBT~z zD#PuD*BT0jJ^h3FkAs^88U0h&f4=^q^%t)v)-TfCuX~^FCS6eXSnap8@77+Ub*bxW zLakDLTJ>60LAAH`;M&JQPQhbxdk|}w_A1@v-Z-fE^)^}HxrYw{4IEf&NosAJkgg-a zj>dgoc%9mWXs^_%R%|dW+#YJa^gWmT>`5|mmfVhPzVxn-yj69NEYbWq*5p#tOYeHq zZ&i2863y@GO^L2es_v2{k}lmQl3MRq-6=~X-DjH+v6rj9BugY+kW7ezyy}axMADs# z2@#r6eL;o@WLlA&g#e=aKC+`W$`UP6#C`AN)ElxyOB8Y6Yocm{EYT80+;{9-YP~Gc z5=GqC{jz#pmPoqZm?)y~aJ5dx$MGe~dr#oOr_0G%cu$_lA>XOi$`UQ{@%wiFTCI^K zS~hAg__kUtOSGg*_Zja~t7M6m_`|(F`<43S6tFC);7d$<@2Bmm|CE)q#I*N*)UW!j zEYT9v-ut&7Q~gHHv`f159^YN&(8%IR*2p!c{RzI(bPu6I6Fu4#*(AvIrm z&%ZrE_3yGoOVoYOSAMSgg)GsM5qr<4U!wZCEYXq?d(VfTs`{BM(GtVn^VgqK{Zy7{ ziF@Dk)(ccWktJGUq zEm8R0Uwo75UuB7wcYb3pF)lhZJ($9bO9&#HO9sVZ#Ok9R1LM_TcM z*Vbe5f~AXAJ$u1Dh=qtsqK}0td7EXDg=Ma~&+jJB+JcCs`RgRT2o%TJN+gnvq`gck z?g>%JGQL-?<%d}zitV+#;S`i~`^QO|u^paEhJX>0uh?TniRmOVn?p~{76Iv#_rf;X z#`{z0z+NXvS0Jd%@9l*lGS~EGed%N~vZ8>{scU7E94oWwWDfO_3BjQyNfxtI0jk+_ zGAA1_Ngvbh=Ni>%-J65_F_SWci|J$Ls_-RcH-I z-mWz<^q0HUdUM!IauK8m6A_MN_NaKP4@!Q-xd4&%9X*kirCN_$#%!nW!{ST=5P?}IH7J(`AWN;00|iJt)}SRE3}rp+u-izqh2l7BYX(~W zy@XJz?^@F3cIfDJ9GN|=iPvL|Js(B%n*$#*xgrG7g;+V)Z#f*rCchi+)rPph<8+`Y zAWZ}mDq;sUC$cxrdAdoaQ1vwv!&ay7DF$|fc7aa~oyr2xr0dsR_oCes)ky$4A_WlosFxfzxa_vhuItGzo*d9=Rp!q5E`yoRZDJhF$0pVLC}6C zwh3*fP%LHpjY$>SN!GiQ;TDvnefoX~6pPQ|lc(aJ=K8(Q?$oozyWD^?5O@k9jj5Mi zILmvQT^G>c`)!=IN7BgYO6pX@_vv%lNhZ?$f7Yw{`oX`u#X>ee>D~Slu?ipP>qG$u z;SvS^K61ERHHgn7T;c>d$s4^iT<$>#LAFsFJSf7Rx;5fp(@CN%6b9XV0BY5vT%&I- z;niNJ1RXgUZ-3W`H=PAup=tfm8q8E%oNzoA25@wwc;AZ>mjqn%-f$!IUuV zn%0b8G``*VTw~1ml#Snl)BoExuG@%gJlXJT!)L(>{xyb>;feZR>p!o5yZ#1!O1}qk z{@uO)uJvQ>eLhsqQM=A8~jk}dX@7lOaDfG^bJ4Gn?I3phb<)8W;S1=zeE_^6z z1V2il8^#T#P=nE+6skAsl|t8z>q?)RY|4wXU&n**iL{$^h(w71z)3gtKXOSNH@>Y~xGs}Qw5C%xkiWL^TS}z(V@9s$tCIadT>3c z6dG6$D1|cXj8dq7-LDktTlb03N^a1houv^I<-A17)C;LY)SuQmDh=Pztph>`I|FgH0*)3d0pjp_dyjp9Z(#fGhdz z(Qdp^3N+bPvD%O*>)LA-0EY0qX+yb-aB}~jT>JaA zoqyW7Y3Iq?-`{@kc4wQ|x^wGwTX~S-|67~S-}G*3OdmD9#N;(?8{ci@jnCZp)y6+< zWHv4~+;4chp>24${!aay^;hXHUjOL&^VhFfU(@}I?)f^O_I~Y4wIS^VnlEWyuX(2W z%j!3)5!GWHorz5tMM9T{DFoBaR z36bfEb{$B)pq%~UW}NAg%XA>ihjJDt&pLV26P~F9DLj;u&TbQ$EeY#I{F z{4FPh)W7mv0djm-X0ss8Ph4rFFhPQNtolK!11z2=bZf+b$FQ9Ef?WyEq`LrCfrK+s0@ zq*;}*KV{a9`bo1YBV9D>M&qPem9dzbb))i($poTcqhg6dWdEDTo8%n6l#?^=h~+Z z^r^6Er}VjY`++_ck}m3V?f)F;Qz46~KG!}ehH#}Sk(>&zeO3(NO6iw|@U=S*ta{k~ z%o3{_K6x-q6cWo>6&!0yR+NT+IWVh2_NUBh_|$<}6_PHR)$r*9vnpgUHLKwh2MRrG zyJv|)*GC5mRY)wSP}Cw_Nv&UVpiqVEPbqX=I8dlU(nW=?j}H{8ki}G?>jQC4JakKF zi9)$&Pv=CXw6gk?t{j`Rg$FbGD`p}X0Y9!;ISi+;HLML(HKO0zfO&VAIdaMPHCQDj`H)ud-X z;Ck1r=Bl2QJJ{U?m56KOL9~PQnd~m8ni>ZMPW#S$u8?5csHcS^P};k^KyI(d?^mV$ zufJYAdlPQ*mB8XnxTPN|taR!#xcG)a`05W8hL+JVXSI8rBYTA#^VYE~o^=L8>Ah?( zS*W>Sq>s0U-CC~VrH*{@O+d2g-ViBrguj8JCI9Xat5?F7WUUn;M+6q~;jZjBU+05^ z0o2*^u(^cPLj8&6{)Y|JHKTgstg?e~!sdHIH2 zuyA%I6B~4V&afpMYFP2MjSc05ioKV}mdmve6tlA9f_2zrI}QhI1K$HK3q^~sbHAsk zw4@WZ2xTgv)doozGnM5+t1p5!uHzMJzKBex@n8rx^!!6^Lv}0**v}(+_aj0 zWxCJw52m-6t~aGj7Z~p`e$;rAv264kpJLqH_{_%fjo^mW@GHZ8h7TLwWXKpih7C|n z@U!}N>05ee{m1L~tiOK!`t>}xEuhkUM0ZTr(3y4TX@9Q$oc4p-Yqc5e<=RWMD$Tu` zH)#GuX4dLJ*fJ!>K|3Ns-CN2RgYb}ckN>!{okklo4mB1Yo@#g zo~2WRTOEc+D%ifq7D_uBE_u984em`C9wCy?Rx~m^;bNT{TS&4&lexfUb!$_C^q{%@uA%IAGT1$$M8pw0=))ecSLe=2lx*~uG@;Z zN-t^{{@{r^H8`3zY>DJc_w5aj7qzk}qLygoaiUgCB4kD@kNqQ^8XO-RjN&87*@EE$ zQEMCG^Es_OM(m$Kd?>Fq>0yoG(W2J$2eP}xY_Di|l-Qwl@y|;-^hmKoIuSC{p+|@f z){2kj8vJmv!5Z%rMDkxAja)C1|NGI%*F|mrLL^_JZJns?pNo(gZEHns|4e)|r)`bcvY#qy zne+(5pcb|L6Omlhwo2ssV?|u&^CtmquZjJAKqO!6ul_&9{{BdWoa(Rsw_<;PC_b9) zul_e;%YLA!Wzzgx`d^FvyOL;yQop5ZowxP$d7}(a3)#lK$@WP?Ch!kchMBz5g#rw#Vu-sV=1lQ3vDyDBK9mis3pqXYKNU^! zO_5wQ#ZN@8-%!MLes%$-__0X-^`ntLFr9q$UlYlfPQLmdiAw#dh+3l5ABsx-iU^re z>JO%qul~#8Bgy2ezh6}Ped6;urGH=S-@W2Pd8JF|GX3{NrQdTPdw=rP|C`A5ZY5k7 zGL!1RE0W)JH1h9=9llc}pL5`Ei@N@j_;87?za{GWiy~x3*Z(TA{(|^uPM6;loBet5 zp{y=3=}e{nhN#QWiR7XKe_iDISw&puXDSHNe@*njJ4Ev39{5#JEB{ACEz!zXM6KK| zLT0q`WzhpaBR-OP;C-UjJ}o|<)7rga|2`!?l-HVc#?jv+YVDH;vZo$+x7eY75&yiT zLwAWC`h*CX>Cl~Gga284G}quSi4FeW;zRic&rfE+AYT+4{Be<7H1iikt{+pxb$-eK zTt6?8e^e3q!aVl5{quj#d27bkuU`Uwo%=bbf&Z^H@ci>mbS8W8FLIpPi^nh+C4&7j z$4btYE^Ui?)D zd!;8c#auc0lO!|FG>S;~eGhHdgy8V>e9}FI3Ds_uoPHSinY3x?`BXIIa}V}h zS)mkhb44FKZeiebv)^&-?v|@~gR`X|PXXD*li8#15j%3;QUZ+yeKjgx^d?=clHWUy zrwI~^G(CeH(kAx`oxL>DsiSGg=D_#%xII_7L3-dy(ZRZEyr=9#gw8G#2^ZWU3hUa- zW1FIsTKioG=TrQNwjmt8L349)W>6q7ino|jSdh5#=0%~7@A zEWywKji6;Hju&HI=E%q5`NUXoCBYROM>(0o##wtH2Xgw2y-=62(5-5gEoH|Y3q|ux zwdaVo!}eMl&U<;cYs9C=tp-c%1@g8~FHj9z>&alz5yia&WvQvKN!LLpzwC*YfeGIL zzx>fNVQ0CkDhWERey>q2P9~;Ow!s%CL8izRGA%BXD^{|NQl?d!xWeq6f&DSCJQflE zKg6Qn*-7^7Dq@<5F4ONefcm+9qX8^asPgSbRuEI-T|D{f%#1)V3G&+^8Q2P2Ajpj2 z`|FMAYZzatwkOt`yv)rlCnni2mk-+Io-`S7gAjL6yBhIbBYL`909!{dm0??tTYx zTDsLXQTW{IZq=%B8R4mjK56OGr@pYy@dy`zeEeS5VrP6pkMDZYjlCgk;p$~j9F$JQ zS&u&^SPDEAsDvwCqLUOxQN$W72hv?b4@)zNY&-74c0=^8J6TWri9rwb z7n$zBlOma}-6kN>N*@`!dx2Odl!`HpJ@AQjBsy$3?JJT2PCO2Srsw|&^=)g~=V-0k z^EBVpd`R;eOe4mj zu-8}B`wv5$d;ZYVz>_s$wYbtpVG;+KIV*X`xIDLFg`h|LpBWsuB90<3>B)yv(zIje zKVKp4PZZ*An#UmsZ1si5shp&J=O0suyHO$Th6S93>$BawRPOT>;+{K?gAthU6)37D z<#+uPHOzrM5DR1_a?Y-8KEVk>o`0Pp)U`7*fo;JYXdO2K#k54`BW_oS`;0=|r{{4f zLRiv*Q%FkXeo7(klk+$PAqYpRQ>;n({mUE<2mM96St^^A;@+eX_eO=dH_YQef00T= z2rkHdy+YjU6yjbxj{|yvqxJma*nN#c+^ZGhUbTRO3U#3)9UG5$r9#{*=5a8BlXWgv zTZlm~pYwVQB|XEDP@S_b7#shj2=$MOQ2#K~GQ@%*;D)mh9*1mmG9l>v4=TicKq2n$ z72@7Mj{};+oIWo*mbUMG3UPm@5cfalaS(!|?s~F3zs~{m@?M3w_b9~u?Hmpz5gf@S z%a!@~2;}~aLfpIOaaII}tNpr7>Urn?^_;h$gf-Skhv&DXf#K6i=bN6GbWS_}tqO5} zt`K+YJPtuHZ0u}?rAd~~e~Uugo9A&bf}w0bSeNEIJ^#<~HQoF<2R`MiB0Fo_SN;1;^nz9;6Q25hm|gK^Ki7k~)# zGDWCc6rpZbggQRgei-EWLAX+*BTe;v{!10&UZN29VuiRD&Evon4h@n?S3s)Y7b?WP zKq2l=C+Gi{to_~E&MS8=-TvTqVC(B!y)FIb>o(1%51FFIZyN=0pa0J`5W~j}Y5kA% zH|Z}}f5-aey3gr~+Fxp4to=jH-)NlbJJmJSZ@`=Q69A+ClAqeGf2a+I!zYh^2plFb zj`cOAi|tbg6d0WkcPVKoyeQePW;-2kaA2>2A`G2Ee}2M*pxUi}FfR;)3Dj%zq@;<0 z&yKLi&k4gVFb=lWV?pWEaZ(tL!I-5Q&|6rFgDVj!N9AcI)F# zR~P|fBppa3r8B$|VUImsVJM6d1-BzFt#hYD*afF642LmmF%=Y~t7av_9y2eDfN?0! z`y=CCc>`cbl!%tV(nb_=M^TJjs z>69AJq-W=!uCO!d*?IHAR<-+7&yFsva_f$knVDj62xJ-%ddyr1MaYw)FcQX4dK9ur z^H&^xNvLw`_UVZtV3bUx{A_TpF%YCg)Yj>VI{MY8%B`EHCkph+%2Iu9aN519-D;Y1 zjFq-HWi1>=EhRe_l1{6KtFU&f@pOe9-Cng@H%?dB(JfZH)o{AP&ctT=d0|!c*Z%*gKd#T{A-!t-bL)S;-dm^EAEo<_?)|zK>e9MhaGL)a?VGeM?G@Vd zHQ&&@M{|=Vu6dIB-_@T`zgAsSKSRB_^VywScX;rv|GU7K{vp*xYd=~0=+m{}jGte_ z<~A7s$UFCkbN@M-2A-}BUvO}!i{e%kgDmlA$swIdKwb}!z-j9li*TZ8vyG(Zry~3^ z2^;}=xcaVEM0%%Qgp(3DnAI>_wri8_xrlJ91P);kI$+tN^Z;IjTjt>w5`z$*lkHKG zNmGOq5;&;G31_=wdw4k0y)zha_@CRHQfwSiRJK1=ixYsT8XsZ-jZ%1iH*5L0=J@8Jk_pu+snB>VG)jnyFPve z{9*~*f?84UD4boPnTsTF0<|KxGAl?oW5u>#D1k$$74iqh%!;mFFb~H_)Itt>47I`| zA0vTVQH#|(a+Ky@+#yJ8%%df63rKfdPbO_EqS&J(a00auY^h$79_WbNA1Q(3s0EKT zdrW<~nI0j5Bd7%x+_8>lIs9P~IC!N+(#06N!c6B&;1F0gWCza1iuiusJRFQv3p8@r z=@tIIBY|5{f^3aPoze2PZ%g0=IIYf>oBb7L+FFEL8Uwd~MPoK4a2zFwN+s7=(HN5i zj-dpeOxWrx;6@1?MG34r>^Ne}ySgEPLnwjNBJtjeNT8pGBP2?|L7~@LG4riU;6U$C zzS!fW8yliW>LhRrij&d&D7nJlwTp1;&`}vlFVTqHH4-?E;+6ze>aLhq)e<;{;zXb! zM5Uh;iQH8ZIEvzUG)LD~%pPkJID+Ds*JjHu4|<#bDS?B}o6vGG-IbofO&hcM+j%%_ z1)m>{rOh8p;3$e=1$yX~o-K=w zd0-Ka@?3+PKV&|L@0&l8z!4NfnhClo-8vPy|4;&lQ4FS=d2aa=2sTyH zr`NfB$-H^*A{;B$lD!q0xkmy=0DI@iNiAQxZQd<`gEZbqs@rl%FD!^%y=xH;#{=2q z@(8?nrvwh6D8v_i-0}##`K5Wdg+vjuKHwK_^NNl6q6BV55v!A_M^;$%3*hDKIY)az zQ$jV@7~G0skTq3smzMkc)-$)(*Vdk_dET1t-MXtm2EGqz+u92?cW9mm?)_g4?)-mf zyS;tk)*V~V+k!T~wt36u6{ZJFZ#IQYYoHR~)yDHSKD5!^xX^Hi;dus-aYBEK{)+Vn z*5AAyT3^#Z>aVG9QD32YK=o$u_P)0EZY7OZg3*PPpw3~=Y2d$u2ACU1nybRAK6!#9 z^Y`KVdCZN&)%-AvlwUE_V}hYjupG?Wy-Xn8mQJJ0jRP5e#1Z)QW%%JzeylrQOB51? zc0Sy4*CSj~dOF11*puNW&3AHwWlMA|+D{5mrHU@5P<~*HoYi>B9DDT2_TdmJ?QbI+r$#+~9M85K z?Q~06SRXLQ9wozXL4Wy{TL^-^2d-gH+d5+|D}9Ayjy-ZEKa3=$?d#{7u|n8ot8^>r zx;yT7F7kVX48QsDfshwfeZ{eNKnt~cpySI5uA1~NI&rfP3;x`u6=k>5YW?YXryY#Rs42RN9H|Cg0hTpt>f&QGGk*mq4LitA7-DyXM z1?j6hbId5iZ^6C__-(A_w@|6<@VchVF@p@h1$!J?CzUy-m*KZCetZ6UCC0_d&RWej z;Pa8XbRUK}wl2eO-oC&db=%07^@b~{SkLWBM&o7a1{rfqC&OFvm2j`7M<8yGS^!JTY@jEyHi#zCeGT96Ksd{e-vZ_j!gL zdf~b-b4(?}Z^6C__^rwCTZqqeC^mG(qRy;6HH^o~)t+<@hPm;%GW-_op@84@GW-_C zZ-fodHg_RX@ivB$v@6$@zIQS=UbmXxLU!Yem=52|Is9pF*i~f%o^EGhKDzN*8GZ}# zSpmOmWcbbd%fz1#yFAX^IF{izKYjtfL$7QzHwrTR=IsmoStwP<KqTlIR~!`HtE&hI?$!OJ z_UpRg_N`!F;8{DjtDm^NxpnW>Uv3RSeSk-7eq;09o7ZppHy^9IO0{eHuIcYgH<==) z3ynWC{)6hn+Utxj+IfR9WqgwMuR$(=kL;{(+_Gccc+SSt3=e9r)B3a*Xuh*k)BI1( zF-=(WN2;6G{#pG4^#|22T>F)R_c#DQ^uzyoHm6SkYl`Yi9C*Y<$C%=H$S2Z0i_k|J zrJOJz$aovc215kjj?@a{P(C`gZaz_@2x3KBnG5#1On=bgV^Af4w|fquAF*3=NFzD4 zb}QC+r_r^V^!pTcFGATcWH|2GxdvvzJwh6eHe4knksef{O$Qqv!e}#_9>&A!DTTuW zC*z4~(ptAla;zo357yuQ!<@0S{rxFL07)_qx;qA6S)hReRjvDRf4G)_xHyw867@!Tvs zj>lUM@m&rSX&L%cc~2Bnykl&eHnCMDEH@}e8Z4+l1y^ilD^ZK*J#jk7<}Jgty;99o z!etbk>6Q&ono{scxYzQ=3eiT^8*NxB1B5e2a~8hRG2l3SXc_18;mE-0A6Vfe*9%lQILFvZ`D7DQ-9M$M z59;P3|bVMQX} zb0qnpTObLC$J%RmgX4guT&WCum0(!)@o7jb;4Z41sapd6AUzJD=|Grt4oaBUUJT|s zX_yK)vo0b&wz+NUsfc`~+H;%vUa3^evq5m}wgcWz*_JIUy2pj9B#@ zQ;J+}U?EE3c#UJtP}&yBIQ%ZUlI$}UHlJ#dh$q}PmpTc=ZPT=-6j>tS@KoKsT$6QC zb&BK7Iq{)_-_P5%=0$=@`M9DYP20uxX^`tjFx$7FTBSU;^>*Trz04R(vUT zot;udBgr5-NEdM)?dC>-N@^hB2oH@NqoB=gsn?Spy51prBR8cQPbng9HBO2j%Wav>Uq5^WysRDs1> zF`?^>wwtX|d(c3!xT!j&0LkdMNVZn>XCNwpwF#Hkmo21xj#31qVF+gi^;*3=AfgRt z-Ehr5g(L1ElfH46z=B@Z>A(UsxWeXaWcjFo(}8Zi(`Yr4p<2k|E$b+e!t7{;ZM;B{ z5nF;4tgOpi>Ip$NRT{<^OUWI{dc4_ohKMlw6H|&PL)Vgc7ajOo;R;jp+cN3efC!c0 zk(D0e7Mg3Je4)$`ZbmCoMBqllnQ4sK4BpE-0>efJ%O}FYQ8R z{a&+;$OfQFG8_stt2HZaX_I6FB7^=~x&-NuO(_^5+D>Q3S<+^4Cx=Y6UKHG3(u?%U zjtU!e4@ieSi@FKl*sJ>Bl){%Dq0N!q<+bD45zm9%R3R)7DFysZ!5)sZIoymRv8um9 zP@1n!DLn0P7jDG6<9NOrW$B0&4!E3FzT_>`Tb##CAzX)wc2UGX)Z8?saQCcmy>H9( z$9}4cxl%z_qhCkDysce?;U>kU`9{9(t(LL4mJlhp9EIXxFD5iPA#jJpljVI(pGZ50 zmSV*N=Y=*M$+ZO+3>iZrg@bbpcin6|CX9o5o3o#)cB6>P>(1s9d$qkvYPU5igl$BP zGCq7t;lgrV-i}7x!%nN&!bi-2&syOWTSyK{?G_doI?Szd)+2a_26{?iYlg9YwHR!) z*t)}mMe9VJXGZ>brr<5aXIGXDWY^ih)8Vl%9i1h3ZcK~xSgp&x4Fx{Vu6#kewYj&5) zxCaVG+mzYXsU^%KI9e3Sj(|InWphy13so$v>K{Z3l!0+~+UptR;Y6bdDmYS3Uhore zv*qK-8cGe#)uzQJL?QJ{L<)GA!6L4*K-k?5KAq_l8vc^6(u<_5jd8{na5`+=pd->s zK>91D6iA(Ldb+(psvWn%>8`~CS(8yT$wN*@xLl4@GDV-KS8A4gagBL@GH@VeE*I=H zieaCx;AI?6d&ymK^{N$!&a`rYe!ZPb+ec}vn;C|uA|M{k=8Cx4hGYUkPqFO@SX=an zE!T5#$~Epb(u2_`mWw!Qt#y+~0X6Vp$HP)KYdPX93;pO=AlQhrnX$CN<{XI*aE6Jw zDGRhAx~|zB4g=u~?xP&NNNmuIk(r2JZTXuP`5)u=j}JjE;xEz#SA%I^Stw#~8RZRFo$ubj0nH1@U zy)KJa`yW$^e!ASlz#CAnK_+U|4(5j8MA_ZQbV^vG7U05YmhO{+%NvcWr}53+Yc^6L zffy8vBr$4((P684L)LOmU_iL&qQD8dcra^0+l=O_o!aTX3{K4vCZImp!|Zy7f-{gP z3@l|bVFTeH>S+!=NuQJM=Y-8A4klcQM_z=4_N%3<}2z{hBuuH(IwK+b#*tUN;Q@`;4viIhJj+^DZ@YyAw zHH2;0hao`VU`CQ9Sz?WBOSZi48%-TslI2~NWXnqul8`9`Xqjn3X(*J2txYMVX@Qbb zZV78cTPTH02;AQ9OK({MEhPoorj)|>>h(#^IiBO0GZRv7zdQ4Xe`n6|^Sr-zd*1zd z7C*H9f%R+FgX>4;Pc9u<`{vq**KS-(tUYV>yQ_b?`ueSv)!gcH9Y1hQb z2)F|Q_8)96TPyZX!}PM@fd9{bV(#t(e*NF>SFi$u2`ixzW>*~~e6_d*ra_9Q?4}ZNKaUwyxj=V%mosaPCZvqn`^#_gi`}f50sXh z&okwsKNMrwUe;vNWZsImD`a3y7a}ST><=%l9o=D716S(VSeq6YsvW7vh+!cYD)cqh z*NkP_P9;G~o|M(ZN95|?Ow#b?YR>V9Z{_RB(kK`p>IU#-CC9uTwGxYTFuEJ#+f>n& z(YdvUK;`1+RlyTdNiNqXscW^e7++1OKeu*D6!Ditd^Abu{6u!EWh^PM8eGE z!IFdM^Vu3-4~+Z@7fuHQ6Z^po^$mXjj5^^Zj%jXLH_}Ekw&6OeQ zI3WeCaJyBod}F4-MS?`LLRKQ?=F=w<{Xm5u$oZ7Z8rEX2q&Fa>@ER3Un;j1mtmj*D zgU-`TCf(FFz5^wKf>y4nYEWx<%|b$s7*WdzwD?Yqvm`$&*Q7uzkm={M0Zd$5W+y(ZB46Kw4Qgz&Ih4LY%;TJ2wk*QLy zRP~6G^>hVJ87w`2BGK!zc_!d8nyo?DEBQ^%+0~lCusa+}GlVnP?3ms`Fy)d5g}F~o zB+NeNSMzwZp6b+UZGlqha41<}uwEy}CkF)$!z!I@LZ%|x+_!fmz)j>)dRVFnX*x0- zNKP&kF@vs1C#7dZcT^sl{fMmgYh7{mPbLyZxId~iuqc?1f0NW>J1qI|&^ z@CQpP*>a5Qa*35+ok-|$AzzPYNjlby$F*7oWPTKv#cn~ZX~VupXMGj6U+e(a{qx=( ziC~YFYF0fkAc8$-t;XSvT7@Xf{bbl=qGlisUa_P{DLJAoeSIR)84e1qhP%k~*>cb~ zlJdCXt$XFL?(cOPonDzFT~Z^<&B zYECTSV^$JZ@0p~b2Aw)L<}!W@_=e6$y0v6?V8l{|R4Rs(K8EL}-fJe-G9+reIlC44cWj8{8hs@lX9*<9UeyRtISdRaAO2x{AvsfL|C zKds|*S{`ttC>u*9N|m4>($3&u37e!*E}598Ky;nN7?h3S&azcQ8%UTxSmp<94>ihF zI>n4PMye~{*pY}NQ&|JAVxl`l_vLVq_Y)p)XCfkm)sdkG^R( ztiX>0qT0=fo@6%?X=AlQmm(Q6f~7*4IMwm8DVItvyd9rGoU2Jye<;bjLhZ6j zDQY$A&ElF`(TrX~h@{kzW-;c{r4xyoP>F<6o_O0W={`E1@P+lL=_h?f+}UO0Jgei0 zNT(vIF>&=h6Nv`iN|gd!&tq9!D@Dd4Es?MB{X$Vx^3iyx-yH-+x2y}*#L^pg(x@df zEZeB+Y%c2Qg}c6dAYqmBU?|Dpc|~e^sUe>L2X@)S;#(&Y)r>3b%z7<(kRH06PMybT zHaF&Esu9ljK#!twlwOOE+d~PhOe7RV$ptwnskuZt-e`es`EZgRnw>ULYM7aFmduu_ zgPhpw4VETbS3w0tosX5wAsJ_cMgrpp{zNp6V`QM%=v7={aHFNpiGX9ixj3o8psdS{ zVZ>jJ_@W&ulQCmnvNjB5qgd7-HQZqRti?h!5z&0yyaXrhK{@Ri)+5P~TW;yyrZ1=l zv_T%M7E`#W_Qs7?AW*}NftHRiubN~-PAG1t#6)DHglmT1$8{|uA2tX&F(QhdY6deJ zny)~Y8sh3?*&M87qCKKD9%YBhuu;RK`iL>2W3$^~lNo);FjYU;1H-}rC%Lp!xMd#| zboWO!vN5t!l$6P*{8G;A9%*7Pq>|v6q0c8UDL-Je`NavVV#SDOhV@D+6jS<&Gw*N7 zu6~#1g-oPAj)B)CO0gP_lp=KrdE7*zfXj(i5QH9(`uT=gAO}(?8Z7E#idVs9k7y+1 zPPfvYU|3Y=Pfa9py;gz;W+3Gq(ZyCLR8QpkPF)&$i}n66AMRVZva72!Q%YgwT@#6H zvQUhH(~$~GlzRa)?C+Km;RfsEVseYeyJCMdbSpyAEo7E&*pYCixTqGYrbM!?#A%JQ z`gF9zc2kw?s9K4t7(=vTQYTcw7V(KhI^U5kuVIw){xV+@0w(Qt55;UC=yl~?dDThb z0|JZ3B13F(GDHQ_9`GQb<^vvYuqtl&n;_`8HvzVJUA|x@6{_MUhlOJqrzWdQM<wl zt2q^CLv?qmFbw)~UMd}6!+h7rEd(YKF~d~)5kJQH6Fojo5CtJZs!or`8Voz$oX49g z)uP$1m?()$&zwj^dO>%}H8ymAvq99;1-YIPYQ;vjHF7g`PloicfeIEb`^W-1*+~or z-E!GEqB4{_+)~rAkrdz)9)GI>2E=ARBo8Qm#2-uy8B1N9?0Wz+;jYpc0XF-~!+1VI zCi)$(jDa2f)-cP3)p}K`byV4uIDW;Pu<|H6mM)sO*|)rdjx`PtZA12!a50zc;^V}K z*DPG;AEg(4Bm6ZH1M5vXBF>WWZA|O6XV8 zy<*yI$kldH_eO^-o+~%fdPG|O?MWK!uqU+wQOnm<6RtY&E?n=|JoN&LCu8kYt2Qin zEzv*H`(|PO{SyglSR6Q$w5KHv+m+I=+DHU3li`%Qs_3fLP4Gg$XL)>uhPm+ii3B+= z$o^P{rGp`{#dUa9)#P5QlVpivIqDjRLwMaY%Cf0QVd1kA375YjbUl8&<@B-xf-cpC zp)(Y%S;ZRNs-_raPcn1PIP3GJf#?1# zA2-`&FW;;=>lIEgLO3gB!hFrCZT`+AA6Y6KYDWuVAj42*H5a8)`JN??4LQOTam^oU zsi9u0&sIj-ymLpwHKt^*5g#3EAlnyhUbVeovWOvL*3&`BX^Z!FHAz|TOJpaFV{(tBebMcF3FM&yqI*;@J z$%NW_9_Rl%hmRM}|1X~ZUp)W6c>aI!{Qu(lzt?~9{Qu(l|HbqFi|7B3%uXj0(f=FI z|84*OFIal~+{$-X-m~(ml}ndDw|xDwd+DE+-n;Y*TOZl#Y(01L-pyax^Z}26@86KX z9skdO^L*#p*Vle+Exfk4`q5Q=^>W8o!TCLhejj}|T0|d*+>Km^&`XbB{Pg10i!WOE z`-L|z@bf>O|KNNB`0%@L?j$Vql}|iU_ESGBo|yW@#s6L`fr};ZUswXd38!P>%BL+a zqjTuOi)XkdV5XmbY1L}0-cjb;zw(t|aZNsj8@rDvn*aU`Mfmiaxm`tHeygqM%Wu9~ zxKTTVqUpz8dldcEn{7oO{{!L1&bbv~XHoRBPuYsdHwrhl&#j2q{~Qa$_AvU<$81GE z_zU62`mUny%qpYlcV~MP-TY)*(ald5Zd7*_Jupkr^aIa5ilmR*ilmPVH!8b|zIm`B z7dyL*zVyBvQ1qpDUM1WpA41V=Mqd&iXDjmmu5hDtPDQ{y=+s;6Jz3jo+gWpfTC1E> z5%708Q_+{MK4vo-%nK*BA5paMqJztbr1zH*!R|7;_qX2%6n*7&e(_5w+h0?`>bFuY-)DPT|DkcNINh zh9Y{uqJ8bl*R>h>y26Ra?JBx-h9YWz*7h-ar`uNayf1`LY)ur|E&h@jipc%WEcP-w zUHpKp=m{C{dAj%`;l$=HudNyC&Ukyhr%fUDV0EuU3!hlu<+U+G(HTyLb`_mwS8YXe zFSB{EZxl|f?ebcmp^lo}u}}Z?L$;#3t_z<)cX>HxC?aP!&eKo)TU*g*-eZ40aV>me zWmg?COWo`)ea}z-$yW47PY0j({Pf?16U)2VT$!Pcnyv2id+cct_za&|+~u`2Ly>EL z``X(q?zzo2HK*UWZu5FKFwhITycTDuqh?!((>H$5R(InEg%hBeK0INZpP`7HZBb9Z z>UV8LuL1-8s~=G`|I=BD_753*GkE$X&$Si3Pz1mh3`+MO=eOJ-UsrBqO z_2u?!fTAx~AHTYE9q5i%F3x{){?y{L7rwXn+Qn}_au@$CsDirCdE~E=-$a@Siy$kf z7oKO|@87s|0yq3WwDh#K*DQR@zU9A=U;XY65T9c$O3r z+o2F&BbsVMPL#M>n;5v144Rp|EA=O6txWWkzKZ~NefkuW=tr8GH=d1qi%Fj=HVg!8j(@`TcQI6HFq|N#wYCBwKX5+mT+ePPRC)Qpt^)^&ogaIdFm}HRAI3;cl1T ziufC*tgu{C<2^*k%cgT_5l@2qFdTTH#&}v>O9z))i{k-$*+fF;v1B<+SUo0)w-piG z$a59S=`233cDNQ+4q;`dkQ-))z6dHni9onumWw&QIm8nKvJ$S;1*HxURB)rgc=+D)U%8i8cN6ZDP}`9d@_Cbg!wkWi9Qv49+z zq~RO*=y2Z?O{TR5ANCiSup()majMo1a!HJrQw=&y3`QX$v-0i9y+nUvB!s9~!avG^ zhfJxM*63TYx!j)XrWRj)i z&`=W8DCGAWc&Is&`NgMBXwfwhmjWKG)T%`kwWfpXcy70$yFD&&jj&YFS_vs*6!ogu zba)owg+70>KQ@d;k!$0jV7TPVR|+Mq6?Uq;6*shAOlfGLM65Z~ecI##SpeK;Pd34& zy|`wuWJPXxW0qgAl7@%(GlW*xx><`4^DNK~8T!|NJm5*;oi@;oVYfIlji>@Nh3G@I|&B#|py`MR3y z*6}=tPOi=eBH5VHP}y7&t7hx*K2>Z)+b*KPB-m!ipohI=%UQ78CKlk*PtbcQ#4csU!df=ltz+K(oER*TRQxFlETV6iBlAGu=f zSdllqBM*ajsdkzv(b2dy)can}F?sId*NCvvs4|@j*H8#T9n-#Uq$`LnZB~neZcoL-s|2`bSIdP)F@bLST3n*Zdbo_MR9Jb%L;~Dy zE{nnlL?`jY!OP%UC6sQ3{FbWc`W;Gal|+wAulW1XnzrQHksyU6r7==F?y2k4ct~d` zF4edEMp6`lgHTJP>Vra<@x{%#H%}y*rKl^*1zkKTvL#K;M{&8@m(5|jT}>Npa3^ut z7-#Ti+%GM?ZXy93&=6+EY7Cq*+49nusKvREvmQ=|%3O0?Zg|piKT@E84~FH*B}2b# zsXbp)c3Nqy;%;DUs%?RH*EL1Tr1DZw9T9xInM(S6mbp@zm^Udx`bwC;o5}lPu3<{{ z^1Mp`52Ky_L9i5UgelU?Jj6Iaq%XkYp!#SY1I>oVWX3%JlN1W-*YWbn4R>N{owJ`Lqi9{}} z27`r=m=2d%SGZsmb0fB2?IlF#IByc5#SMi~saZ&;xcMJWB(l{`NDGg~?oq1h1UEv% zq}a~ru4WbNq9xkVS@Y4XlkFx!n<3{&DgdY&j_ z9MM(ihEjF$NFAmTD3h2_A|s41rp3}p4>lSW#zKe=EB*{m#cHEY#?1HGm$QA=R?4gq5pv;W6Ny5su8Sc(VI^Zm*6l1s3NZ~_hE{rMBGnw! zon@asX!shX5VmsFL?Xi&W`ygx!0l`QQ0nLKK`x!cl3}eK497ZDIf;#e<5I&)GmDLh zL_#NUYe=hso=?~5QohjaDO|zpA)*CMc5x#GZwHc?A&nzTU)z!JCi_7raE>@KymHhV z3-l?)l*{#0K1)cnznRN=6^ji=t;phJautZgI_{M2D`gW-$&@Qqt)}(!J)FgACh&rh z&6O?Sc}21cy1BA5mWx)btMn^F3dwyiH5x0TTS}zdsu65~JnD3Z>t)-$X1%a9$%fZzO9Qr=Zb`LF z-YI#LrEmi{!|A&VoF`alXCeWjEchCcaAEEz6A2Gq4pRwAttd$&h9?A1amdFL>5P*j zrT(Z8?w2y|RzE~>h54PK+}&vfrMvo(^Fs_9Q z%j=Ug2v6DBZq>U13;2$)dYwWk=NHU=CeP`ZngLIlQgW+2iVmIRN_irIcO~7!w2Ovi zHt-Zf6o7?i&32S8roiKFyp*V9fQz1vTT$mGcc=qcPD}|E0X#%+x4N;`(DamuV8Ih5 zMZW1PM3QFNSMR&~t|34F8kmNE6v$OuY0JnBE8e(INJ_;5er>iXmUo z8%dM%Z<$E+TiJow_B7Q<-V|wnARo}NQgIx&ymd;hHbdYYS~8n)yYlAJ&QM?~*-WlM zG#UoMyK9}AT*cCZKDh2335OGdm_T(6jvR<7v#_{4NyB9PJYUyvx^1Kz<3vGYd#sn_ zBTi4SH3*o!!C2$#t#LBjT$wx)@$0Fm0v@4^lF9%)1p}|}hUSCa0?0AlIwwZPSITtAYP@!rPlW`fjfYkEGyRMeE%Y{;zwLjis9{Xh%%z2>bqk%h_ zb}~60wA{`{a_HBp%^|A*=VKDlEX7EsUo`Swvs*y_Xd;m+6H>BN^(oE@cqP_%`zwk} zGJbzDJ(NXrH1PC8V77pD@|E&p++r)1cei3ARig4nA{@wPO~1hz7Vm5Dfl*Wl zWTL9K&p4V}Z@;k8Eg9|@m1>MziLjpcxN<~36s1V6sOTCQ$AWk`LM9VTRLY90PXIH! z3t4amieNQVb7!2xxXUOJQpiol2v4L-&h3!^^~Ri=C&T% zdT{H3t^2p`+xp_x-CK8U-3cN9+`e_&)~#Eowr<+GerveZ-Kv3@0Qs%>R$%K2@HQXY zx_s;C){(7Cw$QD)&4)H0+}R~zqolfh#PR{<{g{2Z{D_f>*gsCQ}FuDA@CSj z+q`NszZu^QY+kX+Y+{?2Zywz|vU$lSx;eM;(8hxs4{Y4OaUX~_aQDVt8+UHp0elJE zwsGsmsg0XJ{DI*{ccZp()kc0Jz7g2CVuRVhHZI>dx^ZOVk_~iYZvCP42iG3}F$(Tm z|Kb|9cKO=TwIgentf3%M!9%MLu0F7O|LT3KUtGO=^{&-BLF|ItS8rRrb@kNhO{>?h z4p+OYH4x1pzZzc+tX{FotYWK|uO3}JvUCk$1!$0ml^%#(_C5cN}#baa`g+9dqbI=!573=>6z@ zAWp*F=w0ZY=p7(R;%(@y=qdCj5IJFpcF`Jo6`DumXaKzeWl#*g96gF2K`%j3;EC}e z)rL*`c>=s_4s;V{fc#F9b3PA{pk9U^-I>#^|`f&K*Wg$ z*6v@sZ|#d~cdy;GcIVn1Yqx_K6t}LOTDxiO`nBO&cdfQ|)mk1zr3kEDvBoTYap`Ul z$KuYVJC<%=x^3y!rBh2cEnUAf1d%OjOII!Bm*PtS5OtDS!j>*yI=XaZ>5?UMX>ReM z#RnH3SiFDnJ`nxl?!~(n?_9iN@%F{r7H?fVwRqFw^^3#B?qY56s>S?bd@-Vg~g=Oru_i3Dg5Ij&cxV=oJv7 z=qn*c&{seVqA!OSKwk#YkG>S5fW8DGkFpSbCW{8sZfw1@V4FGi0;bfPbY_#)H^aT|RR#22F55MO}45aRRE7eIU-dO5_)(C0&Z z4*EQZ$I$0Od^UO+#G~kQAU+E{2Jso_vmrhWJqq!u=(8X`1$`#OBj__AJ{f&F#3zBc zuQtyoqEChRc=Ra{FGY_)yaatR#4Yqm5I4~$Lfk-~0C63CJj6BhQiu+4o6OdSpqD^g zLAM|-qni+y&<%)-=sLs&bPeJ>x@x22C#VDBf1)VFAEOAw|3FtD{s>)$_;2VE#D75- zA^s3ufcOKj_igL>KCr?z{xkAZhz}w^f%rY-KOue>`7y+QME(QfKOjGX_#NcmA$}X! za9jQXUG#CMF`FoJ>LHrW(PZ0kG z`7XpSf@r(;-~Sr<2Z&!lz60^|$hRT>B^W>Kzkd$-7R0-eZ$kWY# z_-W*SL;Mu-cMv}TMju=LF694(_-Dxf1Mz<#{|n;BkiUiar^x?=_)+9*5dQ@ED#VX~ zvCEeKF!B|MA42Yh_(9|}#6Lprf%u2Wmm%JPd}ImF*WJ`3?3$Y&t_Ch}>BZ$mx> z@i&l9LVPRo35d6XQP-CLb>!m^--7%Z#5aN%$M)~vfP4(%>ybZ&coXtbh_6Hb1mbJK zm~DUW1o9DxHzI!w@do6>5U)o*1o1lLgAiYh{1L=!kw1iZ4H)rl`7!bb5QoSIAP$h< zhiD=1huBAc4`L5_A4C)RU5EylFWB;3$Gj8RS2pK(H6uvp-qUtf;J%jGFpdt16qT4J*q;y4y{6bHL5_o7L_4h zgGvy`Xa(X3EkhijS3$JU62w0Gix7M0l@Lwz7a$tws~~pKBE$|_fY?U!5L;*tViV0m zY@iv4bu#INS)V9|wiN`f-p5tRII9f%W4c5LiDB z9D()YcoCR++w$88tRKe<5m-Nt%Mn;Vj^`t=ejLvOr>FM!o(nvx+jtoQ>&NjdFmJd2 z|4andkK-8#tRKhI5LiEsry{U^98W=D{Wy*wuznm*MqvFoE(NoGTmEq%&bp14Ah3QM zn+U8Q#|8rH$FYvU`f;owuznn?2&^B+B3LunatjEoAICfb>j(WQ0_z9;2?FZ}{Z9ne z5Bg&S)(`q02&^CUU%`q4$fN&)!1_Udh`{_ybR(!$V(x98F>lBFM+dcTkZ>Btz_fp5jVuYLTHG8flv@Xhma8OMqCj893dcn z7QrEY2CTGf`A;LqA$}5hF~m4j%t2|r&_mF2nd>`^mh`)Plx#1 z$kQOc7kMhg_aIM!_-^EgegA)Pp)|Ml?<;=+;NriFCGfK>ft%Eudq^TtKB+q{!znk0 zkpxZBAgFvcA1+MC26?tWHME9N#p-(NgSOxejLfO4xK|(RX-%b5s>{b-aui&!ebLcl zX3tQ{?UH85ic#;V;P(XS!mqvhSh?Q=UR^Dv(=)*@U-hbEy97wwMaTMTr!b}d*o%%{ zeQXld6NnsZ46Uxt30Q+p6l7PkgGaW3f3Ap_Z>0T9J05C=Q{w_~t-)?{$F9BdMMw80 zL4jc0RZk~A9fo--iGfzMsx5Cs^K_)`NS(@u6*~?Uxa?4%y=+UOyW2sLiWhu|%o$05 zVq^+@zhj*}7X^1UEmmWlesViikLk%&4|vxv)ZCF|F1Rgv8Uk>d*KGEQ4p{(6fNUJ= z8j3Ze4rmUxz8ROb z&bV%8OG#R%2bv2IyZYE81Z*Ys4hX=d9J>~LM^?JM8u%Rveum{__R8Q-pn=&X2mez7 zez9#STP19NJJfgE->eQ`OK3)2t#@qsmL`>3HO=h3gkdmj_irUrF)YO>!FIjdsThm~ z;7*_#qImb0Qn2Z~(ybjW-7LJ1^U}vKa zd@%R=#*U&XBDQWt27Wnl_9f*~Oy)G3?eUi8>Bks1M<37l!@+Dk6qKn9T_|gY81h#; zSu)_l!|r<2s>{()yE$r+{i1%{-^ry5Z3!zz#g0&rv5u_Pyi_>p&nclHH#l~!&8Oe7 z87X$B2mM-A4#@ho*S=x*r+&`k=c_I@FN$8>-v=Tdi*PG-Fh<>Sn!CZ`FIYwE+q4enTpEv`!sZk5ai+FWcnz>MmR5 zUfDK|6y!MJGevS@$aaZC`AXTgOYp~LU1~o3v!Tdk$<7`8%Z{xCd9@4RVRPvMKR+6C zd0;2rHHjec^EH>a%Pz13&1Jk3i$#e}o~g1elGkF@1DIO&XKV(#sNmuR=P-N%Jyo@>N= zv0}2Xj7_gQD%Q1dFqI3}t7?_0$AJrQpL#fR39^3ewKpx^Y}n@VO)uDEE@yS?pP9KN z*q?*BJcliNOlE@z*s`|Lu7F7iXmvk_`OGA7;0d>D_Z53~V-u0!W8LmN+^>fl+^tj76h%mC$b z*)zEV&A@inWoG%BNtn%I*Bil(V`(7`0>(zGdT_fK8kKvr&*u}}ypOzKGtez}3AqvO zQ6rz*%JhwPx8M^zskS@}=PH9_)8DYTFqszln43M$kn}*VOX08#{0@7ALb+HEs5M;W z(qludjlos2&~aL=aEU{i0Xys0UTd%apECE!xvlEvXE!h3cnyf*7X&A)M^>+Oe8Uk! ze-E6ns4Jgep_YGR>8DHb;^!7$yl~6>L-SVxk^e*f-SFgFp13tPpLjuHZq9+uFE3w- zUh>FP8{C(n@s{66O~+N;8E4yyX@Uvmx#KQg0-j5p{_7H07z9I)lG>(C>MsrWdF^>nyL5`$ z#y)CHT^$63M@enNCiOY{8Sd%N{qcEFd)ySY^<8QhSd5vLPs%(>YWDlA(|2DAKWDw% zJmXm$Xls{DQCr(bjUEhZR_jqxvtQDl{w#dI_gU|Gl-kx5wbe(cQ7leTZ9b#KA0;*W zY2oQleHwm#{yeB{PEp&P4%yU5Vce6ekCK|hCiTfh_<87ej-Wy}rl^63I6Hf26oXT) z9tZ^*e3aBso7Bhu27aDCbg4bzmUWxjb9PNGvX2^3>G`|d!PI(+**mwrMQlcY2K&ID z&3xQ@hP{2#Eo(Nf=kM}b+2!TNDO?iMvFRge?@-8zP2aznzaB?~r`wp`+7kuYtX}?iF{So-C^3GWgAkVZRPrb!qvpl}ba%nG1 z;G8^~%_R@6*GG2D%yY9_ve|w3S@3gw=rVlzEvU`TwaadCmmP(ZAZ|l!G^5q+T~ME! zAMC1sShxB8;eChZ_oQ2psn)l!%a6oKcRgh`W?JCGwY~+L)9-)Z{`^4c(9E82%gWZA zW2av@>?zVNF&9qKvfnEy2Un{Ew%2=ym>}7V-Unw%@B76=^Lp|v%XW$FnY!&67;v1V z2A)x;cQCPsr*7vwG=KxcyI*7zeD|qY)TW=x-tdH5mH@TL9#gVsV8HNLcr-O}BiQ|h zq04Wt_04$w3OeI_<$g& ztc{KHv?p+Acw^RP_RF)}cAZhL_WJ+1bN#ukS8qKN+}ba0{Agnc?$W=B*5O%_m_;NBa3fYq!;d6Fcyx?zh$1D`!Y~< z?tia3Sy)LVj&IC8G72#uIx!2Pc{HXEGoKxdrw2khkfafVI| z^>*h%^-p#HXSCQe+}q?%W^4v~b4cL~m8^kx`4`GyGL?`{rh(>H?$wOr3{k8nlhX_5 zXD18MAm^8pHL*2F7KMs2ePv*$i=1f&Uv)ACQrXwUFtDMS7DmQ!n*U)_*==H1o=gJm z`|2NP$NGJ7=)X|y`|Cepm;RpmceA*gEe5?hf1&!%UjNg+P;w{ZHiJF2qpGCO|lpXu!~CtUS(|vorh}U3W4% zUA83drqsUCIivkOHVmYb5x`(?4{*D1@JMUu?@w=n9J&k+>H%Sp%HAH}CUEFuP)h=Fuc01Wo_0Gh^WTJ&_|qv^6eY#AKX19*_i-X375 zLIr%yTK7Uz+1mqrK>OYvKvOtPb_${P^c3aLrMSNbc&BTL#Az3Z!JC&al>OP;$+Tmw z+)0nkU{4RAT{umc34^a)D1%84xb7r3l{aeE?%cUIYxYf(XUuP?{m!`vRyuhFV6e9Z zQnPm94$a`87Whh#%H9?TQaN}R?a-<0ZGo=<+V{3Va3|~FU9dxI-`@gXK3z+*cCikv z|LiSrdP6sN@?|!IJuQ%$wOw^+1`pfdmrmJW>flYCbFcaj)&8dUajrZ0lBu$txq0&# zF}U(1JJot-ZiXD1!QSl>=_CX6@9kkTH!}{c|DYb`2C3}rVKX-)4xP&09!5`>(X7pc zLu=pP!ziGC?>v9zX27BKpS_1o@7mex|L4qoYHsTtTdx3~{(BqW+h~Ay^uN50ti5-Q zU;SUJdB=|(1IH854}&*1cO%}FPpl~5E%>XKpSkqMz;(&{7hkyWg#~f`Tl24*e-=>l z^Y~Xd^`w;rU?dVKa`^Q;+|}OQewekkRZcw-=zb;8z49m)qr2(MTIpVS>Iop7;~<^o zL#MObOn3FqTH}^ZJs#+H0sTvd*1xx*?n~job?y!Gr!KXt;mQQMc<3a~+E(f5HR&TH zW-f*crygf>0NGqPG>6G}u&aC4GWWVum%#i*(D_5Jm(H5MGghX%91dO(gCgGADdM@u zo&xT6%VTe4`t${m*qo3+e)^b`n6)rhPHg}Vpj#q8IW&he$By0dp0)hG^3*y=2Xss1 zKM$SG0o`)u!n<^84d@5m68Z6=^&i+RXD+@$b*=7H*MA&3iL*D`>4oxcG0s|{7fv~B z4j`L9Iy8srZaHi1ecdS(<}ZT$`=OUvXV2gCa`}<`%~+*_B1U$K_@T$10*Gi|l$%YQvIhqH9cnR^0Po>~Iw*xmA94xP>c-E!t`KLS=$DMQ}Z?lkj?KOn!{ALoVg(Y zGB~%B!GAvV((LRRoL<5|lEImK2g=Fo00+B0K6q#jXK9Z!cL=UL`D&1k-5$Sp=yVQf zk2CiMN++)c`tA1kPlwijV0)aoKXAkR$!m71>$`_e;_THmy_Vmtu34K1g_Bp?96&bz z@z5Nm+T+aaf$L6=Vg4e>Km3eVQqwE?-TWQAkpPN#v{S_2`7fYAlGSn8n%+$XB!)XA zzWrE}n7N@*IynFw?B@Kyp*if`S$L$RXKq+1CoPbI-SNM5=oHS<@n>#aTzRq&(y=@K zHxHf80UdwlmPP4gZ>OZbacKPqcKn%lASUnszdW~g0=abI%Ri6h`^7XbmcY-n1a5li z%>(Fj;3>N|_GaEcJ9oD_>_cGk>@VA9?HhvEOM9PcpWkc!{XPdClZR~klbCf?X(|`z z=homH@AGC|(VI#$wjE5EQ33c`?j~zKZ$2@?z2bn%8XzP@9N*6NWZ+yi6bxlD;8i*F zb1?PR*uH~kza{&xf1S7YDV_Vkr*sFuh?{k(($2+&`VnAQ#&)Z5;ETB99ogL>Tg_C! zN2~RLSXMg&;0;%z-4vE;_#-mc3;4@XJnGgi_(hz`;2}C-MF#0@v!eGT>bTC1T49li z75rSb(*^q3qQ^Ym?*eD@-ci*@3_E-{Koq2Lu+a#O2SO~s_jp%ogjI$`iK)A}f%kAP z;_R%0hq0ZTEoeTj+iyM&ye9gyGas&t=A%|7DB6CW`ZF~jvu@B%nUAb#8iRqWQe>2H z+uY9M=>k#n*W_p~ny7P`z+e@C(ps(HZ{#~_ShLLFj5$*&7}i5es$UYzogDJ(i>EACn}7DKXLxhpwm7FA zzsKUtYPLT+i$h$rIFF6R0aLvL{aa009Bn(JRY9l~MGP3nhq*GBRGZCSGH&&vl-AQR z-IcS-s$7dVz-W4q#W{1u`g5{72M__)2lyWXu0i(M9_rE9o>@KO(byh0Nf5JaPp#R- z^YM1eYxv_>z&i-iz2TtJbGM6PqdXjygJa8#Cn${=UAWhzMxoKg*=&?fMaP)W-C*1; zmu9AWMltJ)ltbAzSs&E`xkiGjNVTkZJiMI=j+45`=i^Mb-~oPM+=B`m(wK@HOnfuN zj4SH%9L{T!o%PB4|AmE5%%PtE9~b{!EP?-SCGf^;ZXThL#0~hbMRrHe3%<*B^)apA zYt^CexN5m&D!@3{&g4p3r&LjD+s@4}u1Upa76J)^PODi2(RXGAsE9Q1pm)bSWe=W>RaarH5Z)KZ2g zz0KlGK33Dzx@F1|9DEPn379n6$=?q2C z_WN?o3`M_f-1^yKWgs#>uyv-TL)r)g+w#z&6jQ@kv`Q$Hr%w7)vEySdMr9Hj5s#T& zEo=sR$IHC89oEvlg4{no%==t87U%-cW+`<{_-3q#KzhJM4`5~}z!epEXIpyMqvjrm ztuq_iwb#Dsr8f`l#napN?)J|5{?BglL|ruOk1U?fZy~eau>YJkln;E14!Y~!t@vT6 zAmTv;X#?wS+MAVh+;KG-Dg^^&mIVEFq;cZeE zEje2wDk_=Cg*>^+upDXAxsD|ZmE%mv3gwBA6l+V~wt0Lzo9y{GG3MgN+v6rpg{f?? zx83X3x|tLQJU@aw9m=?m3__PWN-(bnc~`VRcWPat!TN^6P>bnBzEj6}k3e^n3g_yW zr0%W?p{601b=OdH*FCXqfs)H3Dj<2c8D970YxTr&`j8v!J4HWfj3wA99ya!`0>^A; za{;}tVNJL7M~>9(!E2AR<_mBG4T|v$CcFK3AB$Pd`}}$)+@A$?ces8z+ZE%5>xbz++Sb!0GTc_GEpp(_w1Xb-;7q0i3#;VXv?c+=V&2 z)8UzS2B+qO>EU*^qi41;re*YFJd5kY+%~=KZU%*15f@AWZw0cJd*oKn9Z2D^OLJ-z z-5tC3&Os{)U(Bd*%}ys#tY#`TH8u5~*B)WdJ)%rs@thu)_s>DkZ=@q_WoMm%+8g&5 zI0HS1_<=LfnT^wCcY!ldpp%{Hbl8WpY1}JqYxGEtQWi#c$V$V+TE&K|F33L9YBMI^ zsgVBMqnLr>t$s3EkwT`|bDXBkw%6BmWwWw3M%K8Trt@_+Mk^JptZkPCuFix4VLh61 z$3VF6N|SWS86T@ODm|HM@~sdbQ)7Cj={c0|ahUaMuYK($H(za=fj4;fY}sD;!`{Q1 z0p`p#`;`y3q1_EG`&Uz)XmZKeuJWHlCxS_Wcpqk6gJD5%*vstqi`rQuoSf0d?G?hq zmjLVymvi(TJDG#~#H^ZnG=0KFzyJmZuF6scFqgz;du5if@oDO-()j95Gn9Eo~L4u*+*5DTXiSGUz^ z1lo8~bJYtVM7!1jQO9!wDJGkhY8Vs3R=euU634a5@uIJCIDh<tz`S{kCK5L+ zo>ca9idjAoFK8pHH{6Mwbd03jc4ZwnSh~;bC3^!KUA)IQGd$A)#$->)&EDqUyXl0- zIOIyglO<@$Y**=G9*?KWmhy&&iaKeJ<3$UL=TUqhD?WIS{H71qIgZ`-OZjxb-dj$7)OUW zD@7XN8lMRQ8=~m7Zi~`ubiQ0rt3oE6k3{5hK0eGT8f#>;>BJz-9{0H=Wl&aKK9z3z ztdQ?e=l{S{hzs03T3w6J1?IQsK*ZJ>h=RDh^5wYI<-$!Me)@;jzPa>)rEAbPqZJU@`X|U= zp%1NQS1)sX-*K1YO{*VWzkmG$=eYn`$j-^pk+3>3$!o;20W(34+HJq#ZD%U!t|x=Hq-COY{MT=og+%~=~*eldp%O3Lj=2GwqPVWYOcjE?WAEC z9aSpVd!DWs5(R7A^18B(vd#}Y`ly!k$~=clo`@@!miIBr!?} zmLkzcG0$OSw1=s*5Gp6>PTgBIjq#wVOh_ffVW>&x!NsIz$e_|y+7iO)Xqs`EvEJBe zXx)atUvDv^l!|UnBqAx=$t1dwYAYk#yN%7!fDpWWPrD$w>7l2tD`LB1QiiXv{^t{k zVczr@Inq}unGLTq9%Up|sij<2PwI|Zoj@S3s6@PI@B?veZ6YD5@x04SYht57=}Z(; z6Ad9!X2*S=bLo{#qM%1b&K&1S75TM^gz8SWESE13O6VDlE3lsOP^i{~WRG!bwB^dE zjrd4a%rP$^H%uh_Azv*gTDTS}o-Ut=A<*nhco;4J9H3!8u2Xj@}-Qq8paj7zyNjJ!3MfNxFycX|ynTXd24J2xu^ZAsRN%qUSwDQx5geQy@ zrC`xR2kS)NXQ^YdnhMhC6jm|HY)MN}QZ&X94MO2oe`z8SV22gm(vlihi*-95Bb5+a zRdX0j<;8Z2OBbbhbQmH?FQ%>bCix&sqMS7Is;Qeqk`QY{He1Uk@DbYp%f|rT9}=Ly z1S5QkL|-(Ka2a^Aoz7Jg!mudhgT+A95+j~YNh5TU>`}#1J&@>U@pOno7ACARPCq>` z3trrtEr~@_(M%r`a%P*OVJlnm7qi*aC^KgBMvt3+`lNfLDvXlAU2z`~P9$npiqAJ# zz3QX7x6mqA6gg3GLxrsz@m>T!Z zNGVkxyOIKnSF`a_r=Q8Soi&*z^|EU~XXOgnY^d|Uxg$XboLINQ<&6Wk4D@O(( zd}u!xh$c&ku95aoPXEAH_X#w&_@0S`RiQO!!PW5=$KJMEqlUm0pxJAT^EtUFTj3tl z$CDjbYDj11KMf@!Y?P9R6)#;Y`E#bL=&IKwXR|PJ>AhYgH5gU>!?elL-Mpc#d}~L- z=SwxRI%tFmJz!Ef1_UIfC7G>k0R@oQV>EioAjA}o<{7aCDv463UZ zb25668Kvb=7u05PBnS0$f$7Lm7pY?FpPZyYHFXW!ru?!niYQT{PHXX4bDR{(q?cuS zh8Uw5ugDhJP{hGPiLl-a6IcqgKSN25%7sL|HpH-o+^bSjLku@$Zh)s#iK^Kk7nUb! zco{M#`RcW*+IE*CjFfDWQa+L`kO8vks}rGdDVGuJLPrEoq-n-4ZgE;179 z&HSh(7CB+8Gi};U$l)$SMcqV=kRq!)%}Oac14Ny*Ok)^La#YAIf}0v6FUPe8y{z2i z*^m&S#dz0CI_{pZikSU4UKlCvkwpd*Qr0a}!I5E=LRg#w5f1ot(1)u;G45iPUbrLC zEr=49&|TR;Z&b9Xq)ABv7MDk6y%(Sh;HpsA>SWwrN?m>BjzpnV65D}BiC4>JTpD>o zxK@?3JX1#<`b9ZT`nyrDlqHw{2TUW( zr;b}#z}bzhVj5`Oe}Jp0J7$Mv|)w1!`>3Rckcn zx^-8|7a@`!HtlL9bA_PaAXq%tY$7|Yq{8uB+;H{TaPPR!KOXplc^a>e7=NsdMXKpa zd8}Y&wB@BR$My~@edtys3%KAcSGJp(s5jWnTDp!Gvb8$d7C_$_m8n65VHn4=Hhgn$ zIq#X}V=gc$D;MVR4+5P?G3gn$N6xA;sCPqFLYIoPUZv8R_L$Gbq$GMtLI9$J?L}>+K5fC*1{0mb$fO zo*xKtDcCH8Sw%=t<7X3K#)Xf%t+*@O$MVI9W-#q$eUx_#1G6Zy zf*9$!q>aZ<@(~v`Z!8@ThnoFHHXalEcxudRSv6nFNh;4L$Y3sr`6R!@J3hZ7;ijtj z5T==Jj0P*IL9H-0{8U_Tq<}kX7tv(FD-J!}Yzy2XI$;&9#q&~Asac6Zpb(>(-XM?+ zxwQdU!YWh@_quYUsL?Fax#sG9p!$gOtHMbdRDEo@5_&jEXnw7383oJ5)FT5hw#LG^ zp)Kvq zsGFSAt%_pE$z_!eZ4H}R-pCajg<{LerK~vP^GlT8;X0kd!uuv^G{Qxfw?wDSS__xx z9;PRBU2`?ZV6u|$`3EUh4MiBL5>E|4Y#6vCY7l{5CW*V6es4KKHA4ZBs5bKdAA8>c z=SFe;f2XT+HKCho0|CtWIPI#_jJmX1^)3U4dhcCe2pvo?*b1gYVnRX*#q?fH?*UAQ z1VRS`fzU$vkEFZRX{FthY)JCY&&LPZ@2vOzzG-h}c4yv0;{I5i8I4Uxvx{}S^u=(*>Qx{Tr%xx3@+omN{NAUAV8{JS&vjknpN+pc{_Obr@f*kg zIDYo{DdR=Br(ZUXj~_I?-}r9hzZmZx`^VT@W6zB}Fm}_}pT^D^J9Vry79KlvObvJW zQ)3IqemTY%{e1NAqtA~%IC}HwWuxbgt{6RXG%|YFsAhEOD06ht=vJdWBVUZXJ+fis zp^;lgE+08>IYwe_rOcwQE)r>Gq@0} z1jm2`Z~_Am12*^-*bWQ~em(g9;LC%L4c;+$)!;>gXAJ&&FbSV07zZVToWZ>Yw;voF z_-5dPfma3|AGmYi>Vb;~&Kx*)AT{6~FbzltxC46+>@WcOzwQ69|5cdZ@UH%A`hVBI zs{gqDbib$H+%N0r_3zWapns_EUwt3-z1H_+-`#!J_WizZb>H!QnLclyrBB|+?_1or zW8ZM^cfB9?zTW#(?>)Wide`=rdr#=i_WF9Qy^3Bz@4me|^^Wv>-?Opjjh?4_?(Mm5 zBsy~Vh<4+$#4dXzo;_e}Kc+%wAff$<6BuZ(9H_c5+#{DE;6 z<0M9&5n$LE7~=rO62>l!vF;zcKdt+x?*E$1jdh#0{H^^O80+n0R8oCxc;Ii~4e$mb z-uGAVI(VHB@7)u;23{k?yHf z3DJ`P_ksHear!E7FSwTw4_yVHf8Ilgj`zXc;BG>+@xfi-Edg5ck8tIMa0R%65Px9+mxId*apW6t8MurP`-Z@uz@G@Q zcRlzc_#+`QR)R~xrL*GmKfVk80RBLT-yaMv0hbWsH%EcBU@akj`7roB_&p(hVFSMd zzazv?-v$?hiwW`LzkrLtMTGd?*>KvskPzQl3@!i{5aOHPfb+rmg!sl?Fh|dMg!tM5 za4tBP5MSB>oCD4w#0@rZHaMFQpWPL#0c!~HDFZkQoJELF1i){>Zwc|S6exo-AwF^i zSPfPa;=@~kRbUk%K6oHF6P!ti_pb+MfHMg3z8*NMuO!5~p8=r z1vm|yMu<0G4px8_gm~j_;8bubA+8I8Q@|;Nc-23^$>3x{yy6$&BybWTUa|z72u>u# zi-h0=Z~`HouLZ}0;|cNH3&3&UI6_=w0LOx339)=M_%--7A+FjEE?SNu#FZ&p%u%2+{c$kOpZ&oSp(HkRrro-5?2)glKiaHCciXO@kl~;)G~G zK@7wQQS&v3f+!)X9t05(AwaZ2ysvXz64(q zV&Bc+pWvT_*u5wC0(>zmZunjeJ_nx@;}#H}8OZ#EAl#IXy( zGO&yghhG2=;2=b>0N8<@5c{qKHee&f?stF{SZBrOzS|F2fQ1mh$^$bn6XItF0uwM1 z;zxS{BQO%;2M54Ck%183{sHKLo)F*K1L%N`5dV5U&;l(XzP^d5TAGzsDO$PA2k3BV1)Pp4wOJii1#so0w@Ua9uANLIU(Nu9*_YUA>JwnQXnP7 z8;$`IAR)wckHamWm=Lf29S{K#AzpMF5CS0~p1&tJ1RO$$=WGX-f~ADG#tse!2NU9| zd%!{9AVOT}0SAHu3GuWLH~<_#h^O2L_6PeD;)xSL00e}XI~4E%pAb_A0v_NIV!{Bp zfJ=zMw*d!m2+{W@+|jZL(S0{y0Tv-TPXH)D3GwjN009UgT5bVMz$8S|onQ(~5u*Nl zupiiu5Y=~sNia!>*mGbBSW=1q$LddtR@) zx2xy+9$(J_#^a1*7-;v~-RE?xx_*E(oBxBAv%L(x9^AS}=E96YecX|8gq^Z{siaSm z4zUt(A_Xi&zzJwnO$olRa;6G)%lwH0>R>25zL_ex;^aKiSUFtZ7LTqO zEL8=&sY5WdsIgkqq>8}G$tu{5oq#bB0aDcaV&sIySUFJzyP*Rxz5o$0&EcGyoZb;D z$E#r1cLGM0D-tWms$kc30!Eb@5-UfmVCy;rYncQRD@Uqe*LDI%mGKcPhpS-MbOJ_| z-~ua$s$f^opD%$J0elw1XPXk@067a3Rt{FduIdB~Mfj|s*=&q8@fdW%2|cFv=gY-`82Td5!KrILq|{u zk84#Kg5)fOSb4e%c1b5-RQWBi^5IpmwVi-bC9%NDhgHFT-w7C1sz|JSXcg>t9fGyY z42hMORlzRq1dJ-l1Xgxb!7l0qj4G>yqHKp?YH;EFf$@pHPmvx1E8D7eUC;>>AK~(| zVwYXj(EFK8tZc1tAefU5R5G=Wjwy7uA!`go!$u; zAK|bvQL&wzX$C7Rs$jqA5Dax?573|c` zz*;Bw#LAK?*eRWWQRVW)%Hk^6$(?{vU9zpb|6jt`rmOek@zJq6;2ZrNM;{q2jP4EJ z(x2G#ZLg~Lf)V!cTf?h|g+reXT|A_Lk6Ql>AAAiB-r95d;PwL#4rB-R?0*)%H%H(* z=>eD-@QlI0_%)217~Z~v#}8+0(|vz;s(Vq_(_P0qWFg`zy43vfUfs2J)?DW z3mC4aVFbe*)na2(s!5iF-jdT1lZP`da)x@wVYGk<889cxMKGz&Y}BjF_GlUgKxy~} zlgV(<0+Q1PE}9?EP6x;&XI5Gijr%bR3=sq40Y+6WSHw(y^29LF0ybtcf*I1K0dqkV ziW$Qe8$24MkECMcA{%J{*#ZIS>x;6ksL7+32?MEsBP=)Lk^<=)3>6(9pPU12k=0%d z>V=kEFjGk54wXeLCj%Zx3rJ2HwkG)SaETPkVO2TR$2 z*eugE^+O9SU}G!8BG2Ek&9s1xcaCABM4V=6A(JkdopEu(5id%cEHwPofO=ZMM)w7a zJimoHTENCuhDCPAtsb2be@&$n$#} zoEETY=*{1;tu%maLE}h)7N!ywJ8r_{Zkfg-FUd-_5ZQfUKQ*9|7O-jP&2OQC7O-)W zf<+Fhyd`B0D~7_Fq{>;8g|g%#%V`0dCaL)WWwd~e&H?sAi8taC3FSF)%4G9v3Q>)P zY@zh029(eOHaUm+EfmuNHaZ7bHWh=xk|OC7W1c|7KrXU~7O-h7&krc11#BG4 z(82^}R%SwWlh%ysWy(S^Xd_#A2pu55aV*1rIRCu7loqgYq(H#=mm&wy0yd5mSmgNu z_ooGH94XL3l~Q3>sY_vlB!%UJ#eAOpu$UpB1#B8A^8@l}0UJ9uw9o)A1ZBlpVN#{n zr&QvCrfFX0{nUV5TEM1}GQWi!TENDU0*fq37G%-1P3MmI<;jFG?N-(f9 z<^=HOz=zK*pBK;n$g|C0U~Kwyjg*O!VX&FG;d;^;Q&uHQU_!f5l5uN-*({HxQHkA# zWDs8TGDa}WiZfj8j}S*;lD&i>y2k$z_rXP4 zzs@2JWtqe)h-oE4e=6^>v-v!g8cT{~?tnHM;dxBjxG0FpZ3e$o<5V^|!T*GZqI%^= zDwclA==MtOrI@~CsLr@4tc*^;cD$MY37nm4*AC8GAR?>2E$n$x8M&7vsx}wW{D*DLYU@Z zLSG0ey%Bx@H_EL#ez4?=xnPVnX!WK4~w`JfBlra%Tk@yAsuNv;kgP zlTuFzI2OH7z*NQ~C8HCIa&Woc9|{)uDK;W4dQrZ9D#8`W(vE;3=7^;-Dv4eQJtj1# zt@!_m^n@v<$}&|`+yXBbl!v^k7*FiQEEc!Q5|nUpR2vUPlzJ0an3C~WFqowxGc`34 z;BqsFso=-3kRlg#p)Sm>$~z}wX};5H7q%C2Zl>yg-uVBCtSb_%Wj@-32*$7)H#h!2 z`RPridsAoT_^F~IZ%SEb!C424I%iV=4=!xmjj8R8!$kw@{+b zGW$QWQ2NiTVt$6jS!MH5kJnlWAiH0SgIoJoA6bFV?vg2+x28&C&_Kl_d|bbW=7vcS`Qz z%b6?#ZkHRhDm3iMB_(*!sC8TYyb_+XAVQl@k@3Xbs94Ah^Kal# zE{7mt{Tps)EFKHGosns0BRyVw=?{Zf1kW~K~4!Xm$ ze@}TmPIskr)qk0J-~`KB0iSi3mBfpi>@KMW@#eTo4wHIiCQYU>fy=}xnYD$0N?^)4 zOa`|`l*5for(58U=%gkIFJlkIZ8J|BHX+k5!sQh+7F{lG^ebI%MPbV4h7TQiOo0LO zz$ls%YQF@A&=zV_`a+Z?S5Jg&o+8S0n6>Gwj}sC4r83;gRg}^`bI2vi8=0~QmnUlP z@p+}_wfuj3cKx<%eD(OAFtVQl#_Ny3oBwBy>^^+MunfkX3&A`8E5X9S>jotQ8wLXK zw*Sihg?-ocNqRT*2712hS>3Y_<2DA~{d#wzn*o7-f}ijzb)R0ZA0+NPQC>K%e}n24 z@%$HI!>e!-8x>Zajp~|lJ8UWv8x>Zajp`n9J8T$s1KcG#57Y%Q?}Hp-jK z?XW2t*(kBt#3Ea&lFrG z?NiSkXS~)(bC@VDKhMt&PH`Rx*aw#iH!=Y&PH{8x*awViH!=Y&PH{= zx*awliH!=Yfvx2Q>~`1=X=ZDQRcE8Rq1_JKQW6^#R-KLN3iteM!Bq!0_J4{S+yobV zmeKlJ_WYND!>bNzEEy$MosH_=b~|hblGv!Q>TFaOyW3$qfW$_H)xg&B=65@6`!}<- z#HzDVT@7!CO+aF!!m6`T-5qa-jZb2u!m6`TT`q5jjYndmx?5gngWqU*VSN6bba)lF znXM&OosH^7c{^+z5*rm(16#{0=k2huo7q}o)!C@-q_@MyBC%0nHL$h3wB8OI+RWAx zi(sR?)!q&p(#S@M)xg&B+Iu@}%x1QhSamk4d-3hCO_A8Buo~Dl@gQV))qc%vEwSos zROc<)DcdB8jS8#Iw&^b)!>g8%*r>4PvbBC+)=t?bn%G)n)!C>XR1Q4Az<>ea+u}P)h=R`S9 zqu(h`Nr#@SYDl9+na>iRG;%Tyy)DZxP5TN;j?|$HTcU}mDg-Y(XSq0{uov9jwOnG- z2%b$^kgT8QC}>zZi=*hyL`5FltSb1FO0HXBGK*v(kB>DS?=IG?(rI&&oHk&L;g$J-F-(@L(a<-V3MTi6w95~@Hknqy)=mwq=|x6 z=?@4~0k_41@C79xCoGlm(W0ssxE1DLE^$`QkM-)y)SX>J8Gy*ZZ|BG5(B{Bu8 zIO)p7q-<9*>9l&HqJ*iah6yly1}RsTme?%t2yaM<3c(9Cjkr7{^SW&Tx!3AXF%xJ= zkdPSozJS?e3aJB1m>nYHXAs`f_}8hAv5u`Fk_A0?g~0el1RqE#XQ1kO~73gvZs@XpTpvtuwvM2 zaR|Gws%aR5>PZFS6a|p9SQ^*p<9?G(U=ix{4n!9Y#X?F=fD;kN_;PmdqN;{m5_U<# z7PT#=2?+JRxKM8nOT5NJC>HkRG)cXmEn>&jaBooLGw!Zw2=h7Fq)jYgPdiN+zu78q zmT+7;9iLW6Q$D^)md>dHT3?`KV)sjCG+0Vcf#nJ3_zEW13sVI$C1Ov`!?u}nL2jYs znRGD&fe@VdXll~mfrii5^ z?uc1|XdbVh{YYW)+MoP|QcP;)nyok}248!$OS~I@o|%p(`@Eh#xUz z+%WCAAl!dK&8m!+YvQs)S{qCnX0)(ks>zT^r;GaB877}Cca^xxTz*=T%ZKwlSWP3H z5-Bq-RYtE0xEy(AG8{6A*&)8g8xo|_X{?AExbS6_fyIsYA~mZtnYeE{FHq!Do@uFE zl@K*fF{I#rrIq?*%x>}z)*287rig1SX+-9*sx@L^*cbPjlkn6d z%sZqI#og+Vm@}E5ln0q?`@r989BgAG640|L@=zk-av>32D9 z@g*!gNk*;r*|QNo?&gcrp}3w?%B#HTf+9TWoGgjOg=E-2SW8|dLo#BWiJ7xVat_^e zl5dx$roB?NsKCtXbS!y{g;r3k z&iCnR8j^rB5OU~5sU(M|F-Ip=+;k-?onum8%sVlsm&M9@Id*~B=x3~~YRCluOfT)Vh+6mvpJ;5{{sk{!m`btGlJg2f`1T1&c&4w+WRaBWc)5d~zzh#`ZqRk%oH?&+S< zFa&&PNsFq&vb0)j%>?BFlUl4+JH!De;?pIgh)ZdTd*C$JuditoodS`@Z%utcWATDoQ<90=|tg4n3huQ6~I(n8lzq6&u~m`S%h!1B<%4? zf7gdKjVwnSObZPxjmV1Rk+h#Hl$g^Rwq1kbCDC*??8OTfFJ?`v^1aJy8ks0Rk8`bD zvmqvDDI+N)<3&s*0e{kH_t=cwsFvli%k^qoI^2KBj0P|0=6LfiWyWtwx%GaJC>M%w zU?Nnmz-u*%V;ob?YZHnPr@#BLnns#0@x!1NAv>Fc$8`$3-IFY4IKfC(l=8EcxqvM= zZBeJ$Je$2QR?|oX;!%Y&1gAVxL?0+5N0(P+xZwg*kgq$V1DKh!ir zGC=`m@54MScsE6t7DYu0uDIkciIXL(6PZk6Nkqsl6>!|lc(1163%Q|3^)Nj$Z)lPi z)wlzSQYPs~lPpitVKaqUc7EK0WX)QCKW|1u>Px$rte_q)M|4JY*nyjv&MaKdp++BO zV!HJ(ZL!K4C~B2`8)_Qf5SA+xywjLglMSJHPc&ioYkU!bAC;G)$vixm4fFa&qamKM z=i-`%%cRi;rz3E2APOhBNXcsChoA>VGSglz6DjyP;gVQn^Lr%f?mgfQ+&$}Vd&}Rp z)If6$)OwW(zP`iRajlM*Vj9J%=@QSKHJO!Bc`=pYvQ(+8&11w^;;_#?T-!-V6LPIr znl?s@oT$VFGi0lak#rV!^WrWc#y7De+_;S^mWZrj;qWarj(94{)5UU}w6%aMt?+r@6l`7T)mqqp5}y#*8K3=nnpOvO~p0pl6n$LY7wTF7mX;yY>hY^a5!{% zfjk$O4k(kdqQgIYUR6U1vjTc_9(WK@jOa}PbvTa7VNOn5mSAf{Y>sxCt&>bfj1g&= zJ$zP8BjZyCnI3yS$wFLayDp0xP5i8<1bG6w?6V9KGL`#ZE438%R8n(2jSh@e-!^m|FFYo^r{;}m}OAY)RYT(qa za_^vCe{9bg$ILmUM->Zo{-;**%LXX zFQJWb1R;1n-5B*5y)w4aPIbpJop$=Yl?MI~91nKIJ;h2FQ+umcER1ENE^lfft71+x z?XRC=glU5_)#LlKC*hFFIlbniaIFstQw@v?&8FT-5b*dF6I$L$2>VgIU`Glt z(kN-~8e*mB@J_-p<(`@kTHfe`TK3z`^+6m#rLkK2Anq0)6o3!rxs`$RlSZPTdbV z3{1r$Y77f=LQbZ{l_=OP5l2puXK%6#in;g!RXXBltG#TOm9NPaVbk-9Tw#K#e#y6tAWAU>rF;%sq2Kbs3``O2N-g78u)F{O-4Bc2pTm;A)bVv6sTn^9ZpPJALr>x3MP{EPO zQXy?6aMXX@%9~=5&8X3TqB1Qk&3{;gA}*O}q#0$OQ_ z6G4%P&@PDJ@Wq`l>MGd<37Ott6dF7 zdGQ2OnA9P9j#Fl_va2`$&&9tBee51qv@j^C9=SaJbhn2_cosK}fu#p4n!8o{Dww#>!T z>NnX3$vDC^ObL+@*v&S9-mW#tauaqnZYpGPnJuIBcy(US#1vjgL{N1~U`QEN8V5WP zVNamSjGHCYW_7`kR>Q;LGs}e8Ea0c?T!E;q?SHlCRUg#X(g(Hdx0~&Q5K14!-{OOQ z79T`;cY)-Cj1x+y3JD~$Az_5eR)k|rG$)c8)b2cz3`L`eGT}mkYFExCPyaXiAPRTG zuJpHN|J1M!s>CYkzz4M)7!{gLd<(*5va4>er4N#^0(>PmA@Jo`0TBzyTBb~nfEm8p zXP0s|sYw*I8^s)%wUjk%`u<-TEf_LWJaI@=#0{9Ngh%LI{}XYpkd&xfy_W(|C;{2`)=t| z_P*R3hIju@@7cBMk?ucr^ShQYu3=1%|9%{uvw4j6YvJW$#>_1R7K_hEP+K4$YD_4C z3g&!)g`jpx7$aw@S(HwOMJ1O49zZr}todRrNzN2AtcmqkLI%uX6F#C|h5KjEUJGG^85G8(;4B#T>0=8TSnF)Sz3QG(U!_$($G zRf*&h5?C8_vL-qzusR)&#YD6zr_|XwowSLL3ao*SDaLhXvdd_rbdn}I4k{4%Ttyo> zU2mO^jSBc?rCR1Emrmx;;$bK55+$5LSIVVukTcc|i<{`E!0L1?RKPQ8;oYFl=|oL* zRA6;F6cuobLU{3@b2?!Y9Tixe4n9EPxZ@IAymPuk$aFC5fPkG!W>w^jLU4ZmfXak%gR678gPZ86!0L27RDcMj zo~BHRi{>xgK}~d2V0Ah!eE0>|Zb@=ht~TfnY@%bMe1T0V4Un_m*6H9k_(pM})YLP5 z5~b0Z)QAERZ9d>l6^t2Dbq^oVimoMCosNa_d0t~N+vK9>r`x}YjtZ7p7OX>-ezOhJX zmnBotLS7t^=_77lGcDzG{oEI%4Fxs6S;`TTTf6CD*;oeoweVpHg}6lRK8#GF%`)!z*7 zOR^pj@KGKrkw=3~aPTb@lgVZw0$4t#+#Sc=bI|yFm?015$ChBOs4Zs`1xu=UFi3V| zL)XF0&+GrY8=%_x|1IzT|94yXzprHQ-P=&}b8o8HL!15FyZS!1)w?&&|A{0{RIx!w z&!B(Gleb)|C{ZL%3(v|5xILbv#t_oyitz}h$a_S1Qs6G+3KJnMGnkaQJZW{d`2YQ} zH$mgOx7OK{TEG9N8W@}N+#627lu3~IB9vt;^0L+pV$o)trI+#l+2hc z>gG6DJVYfkZ}PjhSk&mYdvLjG!etW#<#LmNIU&L^n@N!NSv<^$I#tTbBL<7mBP*bW zoL%JiCUmIPTvW;V;e^kD$X!xxBI-ok4y#To_KIVvumrveth`*v#wsTgyp_8@NA3>q zhD>KFp^s~80h9|Dax8xJ-5Y4>gIY!;+iV|H&8OAU2W`oZlKxrVy;Y9Q2nzZBU$T23LZytd0A4k%;U_(V|p0#*&WoW5ItA=|Ixd*>QV1`-o0@; zc=tx*&^P(r8;Y_ieUMH*r7(JQCXt%&4<*HsDW@Q2$eKN>B*KxVIWlKF5cY8`S&L!Q zZy>pf6BesDBnf!f*#N4^r8rYTUR+zWOT%2I3-yU3Qqhz!s*5^R=6KxZbERYj6K+#` zr_g{pkPx`EJ{V<6E4Cm;X3-(zTXRvkpRd?8;KC<@^6c+-)o*^Ff&#zFTpq(0EeJiia)i zSWLomIpOqR!f=1WEOW6o{R|Rf4^GHqVVT91!3%+@1kdGxukw?!gi$7o;4#0Kqjqu9 z6M=}>D=j6;F=r3dRS%(6=bg%C zaoL=N_CFsd=^AlEvLDz4bMF#cvq5Jvsnq5~A-~j=^kmbdC)t%raQ+mq+!k7MQ`63$ z zZdZnr_vrJuBD(4KtS96OM+%;+E-CVUAIIVh%9$BcHXPxwJbaHYX48Z?Ss1oYWEU2- zai&oX!|-ME9FB1+>gIVhPJTA#D)J=7oHpf&N3&B2wtgxlYAe(Ow^DR?&$_2PQgN3n z7uO>_P!8*#&Rx>qs{VQ0CG?lI1GTMv+0WvlE1V=>7WGyFp;S8Uf53~?$kTyf0uIgH zJ4>)#eOW-8_`dbEiH4MqR2kx$oF}ZG)_WbAd2W-Rx3D=Z%Dql9kh5w99IsK&joXE$ zsHUi7i7f^pl2%6}VXdE!MNK&@E6`Xr-3@r6nM8=IjEi-s6A$}EqLeixhqv0(z69Hm zaB#Vy{De5+w??>nRtjGD%Bf*~`>7bBf?-+>23?U2!!qk)60_7DFR60U35QB)X!ChX zrRd9-pY)6JaK#O*5YKS~R3qZ2a|7(=+Z8QFX^TPRh5@TGO`(ncO-q65xXr!kKxPQagsrZc6V}h!sHfL&tuQ4d6Mkn8k>QR9` z8nak6Xu*=tc`aN#JE5HrYjU=jH=Yot1-t+&Sd4G-dk}5Zr^D1@12?JjXE3eSiTm=I z6rQvtnBidD@5f`JC`S{?S$tuQs8qD^C#1}x6Val2n?R=#8Rd5K6hCT@%59E(Kx38Y zxG7niW1>>@+Wvn**Zp1Nfw3pY_8UEOZbMAO{2;)IAdDIo6gK{SAWH# zT~gK0Y}lzms{ zWxx^@$D>NoIAd6CPMqJcibW&eR6a*FZP(cL<-K8zsVD0-HdHr^%SjYH-p2E9s9?sd zTJl>sRnDJvZm42ZU-ffPOJN#E(6CWQ1;=h%-m7BT93NL-!|+eBix?{_pFOS)?Ao)^`{#Jx`lP_4 z$~`$_S=IBZlj)30j60NiS_e)HeoU=Gs5#fzFYnLkjgGTN$J?_%6nAonp0o( z8G%QYrLw~stIP?{e}8cG^X9fK+r7Md#j=)j0=(VEV`~fYEcvMLjAbq6gn8?)GABI$ z%~6%lZ=Y`4${ov#Dpt0f6L?gaFlVfsGbhYzU1d&q{>z6epI=S2ZQai0g%#^s&IuLk zT4v6iv94iGnAg6_obdb?w(94dZQHk9c{ft;n=>a+<=X79_f_VE=RbYB^7+|8ZCkcY zc~{Cgfhxu4jA1i#!n{>hvFPK!R6aNE(ScpNz#41q-zf5hHrCg?`?ZRHd;Yz%E1&O2 zsO;i3_z^fEY*XI3(vmIx8)a_M8Oy5vZC<;o{_UN`6}#S`nQUeEi;|YSUJbP&1+rNzrD7gV%=-Uv~Atad^_xYRsXi3>UlT3(za#Wl((buZ4ZY4DE0V+IwBvj#^8t{zAZ?BD-Y{~i6o z{-M4n`quOr`*!HPwf88-k&KVVv*2d(kjUJb!Q;z zp0k@f%igAHGkZ4=W;))x1NPbg?`FZI-K>h5!Q~4S93EXdBTeh%Iw3h8#*EQ(9V&SE zaL9RB`^vQ)erq=YJ!QjkgUJLB86dp0WBBQ5n95HxJw1uc-O*Mra)HNYitCXiH7bqF zqYR0K`B>3eNRY@|6u0k&nc}vqOjleMYwo?X%5%HE>u35;yYTh?(|6-c|7lmXx&QRt zG}C|Dg{bwPzUyWyROAyH9VuT><~6EJAv+eGze0!DvlZH=GF^poxhxXn5NoFYcshe8 z`VWgU2eI0JhIq4W)Fx!TjfOb0ZPcb}a~lnDXWOVvh*}#Bp|fMWP2V^R-2VcAa+^^zqA}k8e|%u0pwu8&)Q>>)f9by>Z)#3c4n!z5T*R-eF+-nAViC)tlM6K5hcFnd|&{ND7y}-3rY5kn`I)1jj+Ei`PUMI}9 zSDO$s?e*)~_G)uOF?Y)E=FC=To621est%NcSo)pSw5l|*?Rc(;me0J z!%K&UhMph#9egIh9qI;;gR_APOb&iMc>mza!DWN{417Lt*TAU*rh(o1Kj^=)|JVKM z{+;^X>08%#RG++WtKQdoFYC=R+>HIYzv*7zeMa}8-FtWaqwCH#cD&txY~xE?zVaWf z0e0_UeT>!{O%6gH;@#0x*R#92@1;>GQPO5{cBjCrThDv{UyGfJn>DmBi`M3tXRt3;k2W|&SwdzbZf zQ}l?|9byp;4~I$7AMT-5qUaBI(<)K)hr4K%DEh;lv`Q5H;SO3Qil)7tR*9mOZbN$Q zRISvy-z}tRS9a?*+Kt9CwxG36_pP)_6isvstrA6T-%P92x)!=`qE(`3q8n+IC`R86 zv`Q51dOfXD>*3dZ9bMD59!a})uCfcAY5dkS-Ro$TD8}Bkv`Q2Wdkw7;MH^jBt3*-V zSJ5f)D2BzKX_Y8y?@C%Fim`MBtrA6*Urwt;F#s>4RiYS6f1*{Q7#4q|Q{qxo`K7c< z6t(vUS|y5#xP(@TqQkDGRidc9-_t5l^zz@)Dp6GV#k5KkL*pVkB@RU|zmQgmqQhQ5 zt3*-d=hG@t)ZTftN)#1wE}egEZQagvJQOo@_c^pm6cv6ptrEq+SVOBs(Mo60Dp8E4 z-_k0zuDtFttrEp(T1~4&(H~aPDp8E4GijA5I?)-G_^2Er>5$(t?hMyT;J!~A_2JZ6L3|WVE055^_z%sD&;A?{y4Nea( z9QfP7+5z{#p8aq4|FPfSzfa%$eOL5_`6+g3L&7uiG- zxj`ax%&K$g)XCjQAr&gr7PUpHpH=73sgpaD+@Q0X#X^n6JgW{HoVx7ft|d|^#0rZ} zp*PK{v*^^xoy;hdSj{G}(XO6VN9okb-OcW>D{N*7CL)YSXw;jQdPbdG?$8@l3Zmwj zbn0YRp_M5dR)pjT?-gz=Mf>g1}k zVOq6JWD?_q@k?mbn-+%}rPSe2YgIbMY}qI1)XDzbq7mU@yG1S{jNg||ojl^qHmgbm z6F7^6v+RrM)X5EJuo=Xd#)#=fv+DcMsgnmRhN-n8EiMxhW#5}lojl@9Muo$q6iF1C zS@yl?)X77}V$!ILl~F?&|0^2xrX{8Yw;PpOlhFomS5%&f9eDhnbn4`qH)+i>qe8D1 zsbD{r%tYUhsr3$?0TJ2JFC8s zPMtjBj4Cazl*sf_qTzO#i2=JKojSSZHA-y#>D0*$hgrpTmBwJTVYBMn(5aILtWu;mDkXN@E}K=~ znogbE?pmA9YSo*xVuF1uI(71Zl{!p%l~!+%665`sbn4`8qZHdPncXyJn)n5sI(fjV z%`okiOfS=0X3IWKr%vv5T7^!iRhbPc!c~sZs5iZR5L=Wsp-7{$5$f~OhQ{y6=4pSWl`X$ZjB}iGDhDa<9Xb zB8|>wG)eH;Ue`ybPVRM5Os|$pZ89<8>w0O_QSuWgtqpfLaJ5WA7~eyuPVRL!sYD|c z+6{K1&ok)M$-NG9D3vm`!7e7IiEcV|a<7xir8HnTW1%g z#~nqdPVP2389d5t)XB`VyU62?q)~4;c3XL^Xf+5GTCvvTm|0y^_Wyf!-O@E)9^Y&1 z)-inajnUL--^e*56Y!+Je(0T{BZqzgE&<%ZM+Ofa_+;R?fo=OQ>p!6H$v$WA7riI< z?%cDkN5*)G5$XQ1dv*7(Ake?*C$v`73lshk*K_$)*TEaU(eg_6{FjMCYlR(>QQog^ zhwP9}$*As3w?nqHiL7NM5!JX^B|ErNvdt;kK@DV-cGpX`>12Vm2llGx^hRFmi_di` zs*9@g--ZsXJ)koxs>_q@Q0?Ct71agT`Kdx{1&ysmbrZT?g;cjy+acq3O1A0k6W=tN(`M{W$u_4NS)Gz?PBo&9WK?~vUX7bWhIC4{Ipd7k zDcR<<*;Er*%iH$#YTTTX?bj*U=9Fx*k&MdjddW7OEURO^>A;TD!QRwJ0m7UWHV_{^l=NXzf0ol2KhDZ-;E}PRTaC#X@WM z>XdAA+U!@2WK_`rewsGhvs1Fosm47zCEJ{8+`WmcWw?TRHEs^sqE5*+XPhnUlx%a_ zY_~=-s!FO?bs90(`cRJSNO4hG=3zB{`pGEq~A*qx#PU*F9@f zUH<*a+2KddJ9+VTS=lA34+A0Nfba6dQ~j)NjhJnyxWvqsk7#QWwB-Q~!m zpZs9mfu3)f8{c0Mz3B$#HUE02wAGt8e0av0cD#IGBODuL3NU>>Y9ieutif%2`yJLb z6C=Akd-p?6y*qVMc#qYB4WZP*&tJ>9W}kz9JIKb%2Q7e|{%bBAf-*U-f?sQE2^aUHC%RvHS1sMR z^F6y999ql&as7?Ep0>vs2fV@;F1y@(0$zqYthsPZl*twsEWTt*xZ=Hgo_q5(r+;z# zZ+FK&|NJkX{Q0)S&|5d`@tz=k%MI(UzUoK34EIoVIIci|GFd)_EY!Lw#$VZQ>ocrJ zEY|zpf75-w?UgUSxclKX=3^p}n-VMa=N~$Sm*FmHE*u|aqM@uPnQCdBLK)(x+vdUkpK(qk4c!pm@{GuJvE%49}UQcY{?HXi%%f>*`yjoI>< z+UNg%|H4xhAMd-}et&!BAhcD z=ezQO8~5~nwbNbE!PEZ!{T?5zT7Kb4Umk&%;cjIv92X{gO}b)*P)oQ!e(?|0zn0xS z-22_1{`mbduU)qEnIHSV5{!M&JGI(>{9gae;AOZUnG4531iYNL6es6@u8mpMCwuhn zM3ym+@Cf4C8~;bpj|m5j_j7Lf?5z_`Z!Q=Z=(_Xjkvksz;4NeUUWU7dxo{`~lgt-faYxI#JMn?7 zo)z2vv2kdp)1L~heE*V|6xb)$T%U*v^ zea5-50~haRPuPF`{1TP=i`6N{@4pj2j+fyYeoo!-1&DxY4(HUlmey&nJod`s9Rcyk z5@%@FZJ+)$yH9cB`@1TZEdJ291ikSGDPD%F^to_+gfDOwbWO>WYaPY*3ip;C<*%pC zNj!4>)5Z6Fth>H6=)b)4i8F5cV*f{7{FQhauEXcT@en@W;220pLoFg*@<_be(~o|Z~OR5cp0vx=fZIjK388zMqAIbzZt*v4yX4w56Ppq z-)e{~5v=K((mC#xe82Ef-NWo3)=%SQxLTeI$3gfUL%?nGwzTfGW3N6^xz)kny?M)^ zxoaQs&4x>tui^He^ZWi6)29~BTx!P4a6LR1j)m~qri3^^&RJ4xirosAf41h(#oUuG zdF3?pqEsuEs?E8e+qR3Uw9a3_BC?AS-Xp-&I{?whaA z`=x!=yT3W@N zkG7evw+=n{58*lKZN|PlYXx40tJb-2RPRV?aIWFZXzr7T@458C4cv=%``g(ki1~ND zxW{c%-S549=G(V=t#}!(OG$7o-xbv0(wVK$`=8QXw&*hXlP`a0S%36>%fCJ8rIWP2 z1z$+sz|U6TN#}>+r8Nu>M>ubg9N_H>n2>-)W!jjmQtbR}Q~Ayi%-Q6b9Rzb0}8ci3G-$#0`PQ zuq>#c26n>st6aBi>;0~9g>hiT_MBbUUwHN>UaL~W+x&)IMH zJ@2{2@y%5TcFrd~{hX=dKYsb+jtA}bwgfNl&#lH5t17&!5yGA&wC#^NOXLkDAR>!_*cU-2jl=l1Zll2oH>yL~b^3nC& zJ4c>4ggaxHBL4m2Bzy{Z5C`jlcWzGY@u1hn=Jo{b+6wsE&Snb-!`_RUOLJ2&~x|WRg?3 zlw}ZRZJwV$+<7#3V3uIao{YJI9_pbFlMj)0i~ z<_M@&GOky3sVEnIB}KW6Y-4rMwPqn-7#9!IW}G|v-Cir(1U1I?+G^2N7$+eJLkNPP zF}29v%%a>wzR->1umeI;tu)&M!IV&=)liZ+S`IpQcLOaYpHrfU0>b?+yC_$K^vPWw z&eLL}yyMOK25G6*c0jR6oT?}AK2+arF!^3DE|65h1K|Q4kjia)D3Zey5ZB;|T`bvU zc56K>MfrI_?83P~&Yr*H5w#Lpr`jsytKbXV4;E}PpPeyuP_HsS+EjJV#fDb*%se(+ zJ>5f|tb4v&6@#{d@HNq(svbC@g77utsjIqqlB*-Sx_7-y)sZ6IDWG>m?!NWo`#UOSu zfO&lpZ={iRBpeV%R1$I;^MsXN*((of`LHK!wk9S4hCZqPx3KiaIm^euf3E*rm%w!i zT$jLiR03~)`RF-gW9Qam#tsD&RL7Z;{km7at7hyF(8Wh%iPkB^U5(*-(IS4aa>AQTxLmGi8yNDl4q9pvt7pc{ zQSJCkhbMBTMKimAvC*VHIuEI4)7j2S6iyi$Fafv{*`?q>EbVQ?umUr1J8_p2mK@cc zFvY{ZYFnXqxn98$317AW)4}}i9e*)c2p8&}sMzWCbFTC*mmdbByKECqltrHJ_mVv> z;pp?uP%q>g?A9Z!n34V7T(!;TyxAg`XqIC{1LyrIXJODEIze|-8J}`iQiuM?BX4>6 z=mu4N{P=P8VKfJ?n)<-$|39dY6D;oIOZjYfCR!zE;_>NmAuIm8b&ZiPUQrsb)YRyIF6lgXK{R3G+7$Tkh4m*F^n2GJ>Hl*)FY81 zUkmq%KRGdk*y3GZnB+h?sb}FfUI`P<>QKvn5p&JET(~xv z9HCWSvo)sCI#yd;?(Gsu5;SN&^T?NXvZrccb|%K{^=)c9$gwiqmbe zmvRnqf3A1g8k5PkhDt=T*LQoFT|6TM@(2oby0op5Yp~#_n*3qY-DO5{Q`8*axDYOvb(Oo#^330JtC@)W=iuplre6T|!I! z+M0=_{zJM}ZD*_P@gNVO>iCXbJG8RZOtz{1qtR51bOW?W$p1l%y^Wx|x)!Kc_OO6i9 zq>-c8>5=fdS4kKsmsY!(O;+}5O`tgo)jUw12u7eMTDzo?YZ*{h#|GKPYC%)p$JE_!EAph^^3(?q6nV8)!~BYxMaK2 z%&Je24{J;)2pa#>kqH&d*lVg(+Cm-b5QC`Fk#@OKEsiu4SErKc^KiVb)FS<1El#-w zg3kDI{=^Oxw>P9gJLwBh{4Uie(B=?LHG^VRstMi@>Q4s(gez$`b?6-r{gFq0@P^Ti zsv7uV=P@;4v)N=j>=~CnlSoOSF$D`98-^EGuMPeZuX*)Bo%`lVMLH~#k|zR z+wgA9jk+V8SAjEhyjBekUAy4)42WFLns8>BtQ2<)>nWCyL2k0lK;|hK_f)xHyDB+l zJW>*yaw;@ny9yF#b^~Oo-j~61J{eP_Sl7K11#ew3D%Ngv>{Mts%ef%R4o+EWe@?y# zBGsWk^2iS(qvx)2;q++jXiXS+DVwT5%Nk1T&YD!ADjX)6CTXm-=#}L1BF+o?xpnU#!+H_iKVr zLP9k!?}M`GW-Uy6`e6q&?!fF_YI1Ec`Q2N`ta79 zw;tM}wr<#bdh_wk$2RMm-p$)>->`kw_8!|CY?2MKS*%~Ne$={eRjdzKZ`t_z#uFRw z+~{wFHtyY+TYqZ(!|QKee`uWo`2wF_dwlJ&wfdTO?RHQD@UxcpSl(cfERe;r`jyp> zuI{fYs}HQ+vhwwnCsy9M(q9R!+`BTj{M7P?m*2en&@#1r!_w1Bk1ze$Qgg|_bm!u~ zEq;FS{fj@a_?ks@abw{x7JhkQv`|`bF1%p=pXPsO{wF{Uf$03s{Nmh~=H`}}xsc}Q zHvb(KbrpgxJ>OTF z#Jxg`BS1K}z!$3>J-wG7$DuR~o`r*K(h=0-R!!noG&qVvVMKP7SUul!|85fZZIihF zea~X>@VlWD1tXHuD{50ZAoTMV%)#c(!REA`BT*Q3d4_U9C%kjtGKu>)lelkcap2=Z zDG$f%!_CkAt4Z89Oyd5R7Kft{U8rPIQ+)r$B<}w+iTh_Q4nrX-AFAc`QPb!C$t3O{ zwKx=o$duA?>62KT`=44IghE(4nXT6K-}mb#asOZv_xBncNuv;&a^^*S4EwpiGl~0} zN!;IRaTE$66~DuyS3>9h#w6~mS{#W&aIl$Vl6v_)t;OL8P3KYrNw0Lz{k2Kl|1gRB zD=iK~XiD^P?!3N#e`yl;7bbCkuEn7UO-6&oW=_xd&rIUJqQxNyO$_V>hdu+%xi4#Q z;N${wax^;rfu8T5n#BEyN!%Y#;m}qqI+f4j+?Pz^{>UWm54AW7p%J&e7SUHKKlhYL z+#hIhBtpY>CNov3{@m|talnkAB_E?OdO3g5B<>3)ai5>UQBh}oI5iJGXA<|MN!(|( zI257CY;@3>(pR4`iTgb*4nZiw-;+4KJKniZYj8M_Gv zID%Hv&Y^x^z3E9c{{P(ETj#c%n{NU&|E$*F##=#+zvr#VmUn=fes`^8m){3!_}#bI zSoqn6&G|5ZUgMufU;d_@weW)vhHtgJa_-U{28kiSk7W=ZhV&Wi47@N%ZP5pWHp1y^ zikvUm`~IXatVlxJJ9;}nTKO26r))2L zp>ii!(kIb|~PW^t!?|AhGg1KHm*Eqeq-P{`%34fJZ#s8iaSCWbRXupD>D@P$JwZQ8zUi*< z+SDi(m?RxR(W{lbu|&0Rn&xrmL+NhC>B`2$=zvX0b)leFWG8f;0@LQs>1hKK2!d2| z<%a$v8p+da=e6$(5OjLyPntFa1*Xj%)6)iq5t#627^bG1gGSxgcgrm9^zNS^dlF2W z^V8D?qKK5MY1i7V#?wLTCTSjb-kXy98Kx8SWdnY1ufa0<8>17tP5`;zJ}q~!T4yJp zHIgT8^84s!Co{&_D1SzmRZ~t$sPrrf8mVI8|7Oy z>Xg>LF`hVj+<7)6^xQ(!m2^lwQOFnC`eeH&OdQp^dBJq&DWe>qMx8fl-4J@*6?Gm4 zoqzsx=MD2x8g*W4-55(=k26uN%g}TuAfV@OoznA!K_M!GuH)JwgNAy33U|7Tnen8- zX`pRx8O#0niH{(Saz|ci=e1@8Fb__3<~&K;fbP?8x_QR5G4jea>b|~PW^t!?{{-1n z>iPf9xp&QN4YzLI{Dn=<_9@$W>nE+R+W6AOaN{}aA6dU|?RVC)YfF|Nx9qOIcQpXA z{ijzJmVa!SSo-ji1-#7Pz3}mc*UtYNsQz~kAaf0V9-X_WtS*GNALQoNe&_T(`5<~^ z;y_{`Lqc!Q93FKI?4rWOJm|tpUJ{;L`z_O5NR|u9R=mDgV$Zm0!F(kqfeP;q|T+iW~ZEO9WgK}eUQGG0SsuB!Ec&okXG*| z3h88dZ!A9zmzfn29=ezYTu_zEZLkzEd>~sUh)GE{4q?dYO zYy}@7j7&}NDfTXXipQ^cKaG0v(8bq*PK@W=ubJ*dz5^oG_mXMgq;)vw2->LNLhj;& zfW=r1{Hkdd#%cgCGAPCHx{I#`jK*rEV|IyquI!7cf%4#Rn1^P>-JE2{m1Z`CH0Sr;PG{j##-Oe?& zK=ro&(GZPJ8- z_&L*zW~>0<^EA7zL;9is7>qZxEr=Y!!vJaI9gc0!#G?=js8-FzU8%0)bS$3_2mh+KH>zny;8GNX5=j?YI! zoL$S|==VlvV(=l@OCRDVu6ZwwdXc;61DzPJop+h;#0hH$xO0+Nu;S44z9mLIGQ*{ zw-M_3e{ueA=GLEF$$>wv|9sC%03@`&S&bPzp3vHGft@}d&4!K>CkDtFt_~1_bUXx# zAa>Zk0wt4aDVppgQ?X2LCr9m2JfyUmZ~*s-m0Hi<^};j=lX*~+V(8jLifdUItuEat zN}O1Z6nB|G4GxAqUaHA^-86zBg`^`K@CS&9XIM!)xP&K19S`K189ZjFx^z6DNLOKM zUO66X)S_^L2z0vLzO&mFrFJJA91Qs;#0^D99xh(WoTLlIn%2*1c-ZlHvQzI*)dr`> zlj)V0+Uj_K;EaxdSnR8u(AscCsJ)pT2?)Z(4}Ui^5|Bi3)G(p7Vkd)jZ?NPj;GSFq zZtau?F1{OQDR&U7x*)q}ClRivQ8zuj{3LDRMlw<@Mt5XaF2M9DI7wx51w5AVBg49o z&ZKr^d)(D1bi=!$b^~QeiL10~J8+@fZSD5l(Qbem>?9;tAw#tKjjY|(Xxd4ax!7EF z=;MUe&r@~J#bXJr4JYu`(>?gfy61#2?BlxUY-uaL1ByY*!pOb=lk)f|&Za?u3e|`8 z9}8T)Hd|02cctM3BtIlKnUcU9Q6{*{EkSu1ccPj3#qjq1<9=S*Q*&8HZ5p zLmtLA*ojHJ?1v*ojwx}d$ut1!&_No2ozXc})x33Zlm@_XzFs|5gMDwRnp5<^HM4tk zdf@oU>KW1m9Z;%bk|M_7QE}?<1r{$zs;DlcaofF2Hka&_)r@uO3OzLH9=_w?&fp*= zBd-4T;GZ<(9+Nxz-Ar=`Fg!x&ZV& z#Ouz4U8YKnkm}GU`Tt%!_u09v-{1Pc)*H7HTgcY>=2w6xz>jREHmS``+h5xLyKUc= zv%SLhTY1v+o*21HlDx!_4VIef5&=r-MjwcwSQjw?X`EV zb=LxGcUis(V*h{2@_m+w<=)kAuYO_meXDO=l~(UtU0V60l@G7{;L2-PkQK}FpDur7 z`G-N|Ke23G`tzlaF1=+bvvmK`4U7L{@mCh#x~MEZu=u=%uK_QE#}=v!u7&5%|Kt2` z&A)TLJWtIpfsX5b;8A)pyKooV>Zf6MdZ*@2$ZeuIXjENMKiA+I*_H>-#FaekmXO_? z+*e8#2RurtZJ=B_DQ9XiE?6!y9*M2`I5?LSVs@Hwvu!`fkD2HFN`KIEFcv#JN~&%A z!BhlsuL!81o3uRIuxtMMOW2lc>o?gq_!a=8Ag)h(OLi{ z9WHlh0O#XzZ>Y#R5^gT!M9Rf**ADYOM~ zdYuLcB9e>Fu&fQ2P=R-k9$e%u1!@C|DDCa_2ik~f6}LTl?NJ~;z<~e^!Y4;+O$}=Z zvbKW~hdWOlXiWf@3aFY-y;c`SR_NtU*Euj(SS*d`FMGe|sbyN1`->BdK^^ z5%oxVdlc6ojbbt^)b=Q*L6Qg#6)XLCUr&>~dn6v0ra`ELh1ebm8j3iAQJH47rwytA z(!{n$Q4P{4ut7yeG)NR=>PyS9fpay9a!C-;5N4y3Jfh&)!?Cwu}%iZk}cN__#MKM?uqlKyYao_eRphrSUN2D9o ze<6VMYmgLz(he@@&%Ea)RqRqh@Q zl0Z;Al~f#h_i`2K)+4bBNbjuo@KuqF28kmm8ZA1j`Z#A5>Cz(+ap?2TqUqEiF$9G( za+H~cbZC%9VInGby9NmY1L&;M7xi(5mJ8dX2lPlPT(U=|=KRaHN3Yf(VFV#73g4TO z=g#)%RT`vGJc#9`+oM+=N5V8vikT>%*S}9#Ua~!Ug$7B1g~U1Hygs7da_`-vmmk+4 z1_2$Gd$&jTYbY8;Pgw5R9?=>k4z_xGrQFv?9a`?*9#I-3Q1VbW9(VKl84r-81_`pV z(B5=dnv(ln+ap4Q1k5Racqr*(zAbldk8nMbtajw)R6p<79$^}!QRs!`{PqadARz?C zlgY?XpYgzQ`}PPqj)Z6g#zH+MHl>wryL$v5|3U^nf|lF1N05dhICdg|Qq<1rrTL=m z(XIw*#f!AdQ@oEL+>77i*9}Env-lsy1a=Y;KQUq(K_F7+C=F zg?c11;N!8Wb!6Kfy+DIBawW0=)35Wp#UWlLm>x6eiT2%9KsFvOT&{g9NG@Er_C| zkBqbc$X&&kDTdsVp-ZAZ5>A%dWKjQw?~^Aq>-D4W#R6T z?UZ5O@lOY!%&eH4bsSuV)fhV`~UU&f4WPA>wmp2f$I|Z4oLvi|GRbkHhipr7ipvf zuik^r$%WPpUyQ)(!{xuKY9DUDTc4j#c@r|)m2{GmP$AygX%6v}v!1RMN^m>nEY-U` zl)}9EMv-!$^?oX~LtxoUzA(Bsp;`6+zB8UE>Sh0>7mdRV&zI8fjw>3EA5-=UYL)M! zG`xnB`shtp#h>lH7c3OR`hS_YbH`b$*oD5*t`zeLq$$-pyJ)%C0 zykm7s`D`v`S|xbRUXXP~DUN%u7+J%*VfDFol;?Ll385=DxbQCT&G@i_AZ%vA}^)|e;k zC71o=i($0twKMa{mvzG>M;4>GYR*4|IZr6v>?f*8hDlWGLnM)+{rLpmj-o;pyX+_5 zh7iqgQ7D<`xKO8-#q+IZf7gri4!f8qF}#|nMX`1w;0P%hE>HP)c{JwEb*N%0CpL4r zbS6`kTofc=c~=LicB|=TL~^($73hy|9ftM)JW?nJJG@>b#iWDupvrd}kw`tzC!-0U zz_^p;vNTAu4hq6^!6q}PhFZ8Q9jpf(xNi`qfbJfY+r^=`5$CxbrR^Rxa#6%??#cN; z^b_BJXGQ(LDW6lYw_j=fzbTqvwjK9l)vip{!%STO=IZ|$7j?P}|Jp~?`hP16!2kd4 zbL;7qd%+*qf1V8zcynR&Lba65#ojR=w%PYlW-J{;6qQZsl&kRvH znL85)4SQZB;1jz8xm`^cy^eTGsI=s~tHdY8oO?$IKq)a`PSHr$;2Ws|oS~6wwaS4aKYfyq6~d_ns7@1ZFNxt?I6LHno@BNcV8a!p z#XD;rfvuPLsy{M_HHa2vRE%n`t*YyTyusiJ4fr5D^2q;v>*)CwF8nC`w$)?0WA;Zo zU91n^F~ghzV&1D&_flCUSK=CKzQM5kpFxT{b-B#cTlPxApXHk6a+~k>`Dz?eD%DK1Q43f{K-tUgzT^rznRF`cFXY|8OHAO~rC`j>8mr;VD zPLZjWg(#j@H6G}WsT-DVd57qEy;7!WFDoKV^ypom*arbk(2kN;ph~6cCxciqET_p} zEf2HJ{7$!!;WCWkZ3{w_rmDr=KsDV`+;kgucKhhi&KZSUs_GU}=h+GM{|v9g)%t&y z$ecL$iMjPZusyu_$Yx}7+xD%6cdUQFvc2+?D+Ak;wx3#Jmu_49=f&Sz{C`(Iv*2E6 ztbS|t$|SE1+f(e$*GSJWu?|?-%nA%v-*C^{br$*g)$2zEQDDoZowze(6H91@sWO( zVH$vhc3;j*&Z<>YvhV?hhLZ7fcaRANpuA-v&9Cpdy9CSQW zWsPioTi+~nU>h7jV*{bpEnEr+}h_R&~UX9XgciutgoH%wm@{K-Yb2hxKTWS#sh4k!ZwQWL@^dDW@NoX z+SZ+0cbcVI*eIBVDjSMfXnrGq06ldQ3KqdEG-`>Og+?rq18Av~sSdlDX3?8;drCuY zqAQOyq4fjS5156%-}-*D(Dzy2cK|*0!nF`ZKVkiZS?I^DA2-|AM{JLng?^vy`^-WgwmobXIB|27!d8j)>P2X^4pi+egN$^hT$6H z@FkSW(ChOw_0creN3D;Vg}%Z12D8xDTVHP$`u*1LHw%5ldTAA$Y7abRdul>8B>MXH zWWiZYHIlJZvggsqcG#Y_J$>&|cz*tTIHB-OzTOL_5*{{D&)7XW(_wSdB+zCv3A9>G z0yj2H0@v3~0@v0K#O^Mz&A8j01?9! z(k%3|Yo9%Up1PhsZ@XmNoeF)@cBuoWLO)}>w6UKG{j}}V2Yo$tV}GOVjb@>b+77f| z=v1n2upMZ>)1j}o9caJPq2F)2r2S5{*gjxlv zZhPs*udfT1udaUo%8g6!S$yUE=fFQ-t)DmMe?Yb@Jb3<1X<#1wxj_aE64&WmmIZOT z!GPQDrp0jB0aROgYcw(K5r(8+KV*{V3 zUCxGUJ|!7H-e`lxL9S@JOi7LP%xRUY428pOMR3bbyKe(Vk zWT;$&^ZA5`v3YVZ9LCE9E=lCdSx1WRMzXBE21G3#iu%utM6J(mEkwNu!RK&!`Dc@; zx#3k3wWrtPqkKQ+N(2&OOh}Ys#jI1FA!^s4>@1_+ysL~3k+LK?a5mskgGl zSjAs#%@DN%F5>=lr;c6_=qA1wN@QEbR+EETbU57dMbK8UCiJR&uHwq(RZ+(dMg3(1 zQBPIoiv{GUFDOK%$g@b)NbstOnujt~HYb(YcC3N}C0J@m&_vm26^LC-M5}`W?Q~Y^ zXW&Hkx)-v^sypVcIQpgWtQHSN{ijBv)>pF=f}%U>4Ra3lOFvt5 zHNsyt1%kUXad*%UmmB^{cp!@$nJCPf)vjC}W2%lEjiysrwBM3L`K|-@ad>jCh6FtR zaN3!Y$Z)fOrpCHjI284t7>RmnTMPx_axf5Nxo4HAuhy(~Cp?0XXb4Os$_rscK-y5s zEzVFNPIoZvZ9-TfSZIh1u1pevnq03iT#+e|vdexUQjh1ua1V}`#|k8RDC$2p61Bc$ zNlc7K+>wNbXP#A}c3q`y(ObTd89=>wiK)mX2IHc*l8nvT7VZ5K=L9y*U>G7CZ71Y) zIGn8v#$|dXo~viP-LeA8+^3Na?HJGM$f2meWF%_61(NVb8CDL5e8FdvsIQs@;$~W6 z6%YH7d@C8FiG-_+f!fkDX0TY zL5w>S;1r1*NJv`_yVHF? zlDLq8=>}Kw%@DOcm#oKo=?lILBNhia9@L$}cuX$I{fakG&qu05G(ka$LM&dj*T(t( z7k^>S`Zdd6fj_SQT$jLg2|Vj10FoTvrcZKA85QX{y(EsVmV=tBewME!c}kLFqueUM zkL$QUTPdDOR}1mJP^va^cC?ivlO@8HOknK}1p9pP0Tu9;>V%VrK_~-oXFDFcNy8+a zV4V&9JB5ygOU84(Y!?_%nIOAcY}rGKJKJFd1a|R}AWFrZ9A1h-{zO_6elXij@0#({ zg%W6z{i+^a$GnXrGSAvQnTX;m_aX^*su|Q>Dvf>RPV+V<^joId>X@JSBvCPNDSP-1 zvzzYN|Ll5_9bq+tL=F2^4;(ZT%X0o>`Qgo?9ILrAMyB&&7Bj!RBV z$n-mr(C*OJW9?MlzJmoz*)B8Kt)ZEiXCdfNr#`ub#R@Pj)T8Ch8LC%aek)t{G3AE}I?t{vFa{Gd#(GR7|;| z6XRMgCkDQ&yW@0dh#Al4nd*0TERNCSyaF{q(kQ7q(a%%^Vr;k4=hJmz*s3z6b}65A zg8jJA3l(XL}rX)N*yGYCfri@oFI)5! zF{nu4c)_1?)C8gFt5mvaSdIx5p$U2OlB+=%$V$DE6F6r$iy=)qNVpS2s+WXJt_gMM zll=ea+=u42er_wbb?fHuZoYmKvHg|pF`M7|ZR^iiGuE3oej9k{L+gLB{-f)@wQsF` zXf3sN&hmuiVat81f42IgtG<Vw*Qk#Jhf-coQBn@)1 zv->z86}I4KB87nDAl^{Ur!&9VeQZn&I}0(R%?DD1|(`4~>aIXCY>kWuD#N9TVF<3o)Za^z8n9V`BH69|Y2Ahg2*w$rjVE zx1_S(m-=v)Ax7EF+5HP+Vi(Rr%qRgmyZ^E=v6r2Z*vuU1?EcP}*v?sq8KqHY_g^|D z_R_NuGs?8i?!RPA>?LO)W|)lKyMHer##!z?f2z0n;ny_C!p`n*kNdTK7E&NlJK794 zd40?RyMND^*gaR|*&0}IWpM{uFx_);5d1GSFJMT`h(L%VEXeUZZcW*ch>+|(rnzF`u`q}++ zV`AsdLX1Knq}XvZ^aYC8{hP+bZaNDwqa6P1{*7Z|H=c=@L7IPd|G8sg&pit()dl^kIAH^;;_&$A(+=N6)_q(kb7 zLcY*0>WuQEd1h2=fZex^iP_FV%&5o!yKfy6vz~#NVI2Z?e`8E+<1ECCN)@pC>tkZ; zXCY=(&4Ars8xvbQBe9tU57>Rnn3&}(#Efbpu=}fHVykB%W>hwT-Cr3KTRHtA=y0A< zMpY2l{pB&S<+BhoDwM$PFO7*UosrngS_BOAKi4JjJt6^+SN;z5 zw)f((yz-P$L6g&$lG!)%lM_Rq^uBlejU2?4M7dTdB>MA81Jw3JI_-GhOFDD8h6j-o zp@dR}B0SBNA&_|$Tpb^OBR}Du^f-&}vS)Zh-3qRMj}J>q%Yk834*L^Ptmm{BTz$nm z^x|?Xz)Q|nY5)Q~hi*HGlx2{XY@97eck4bYul#rB;p+HJ`Rw${GcU#sC-oIvj8h1S z8a@_*{5bs@a6={JnjB<^URJS3{X%`HbjnUS?oILWga{Wakz5)~PH?+^Id8^2b{K(s z{;)h~$~8_2w;LEIhiI}BW83i(C`uW#H;09!r|eE9!2A}La_o$fG7 zi=e`*W7u@sxjs|C2#?cLmg4cy!JBb^bY4{-`^OR<8qUG1r#^`3$D(6v`?~tLTrVC~ zaJB((xtfuYs)Nx$aw!WRoQVx(7c|_mJPMc7?_|x;7dVSYRi3-CR4KI@3kXCBN6~ zl(Q{$`8@+V=dc5+hd{lPDi?s31YNnLY7P@3YO3XEd}q|2&o*;T(BPNv8b=$u#?_Qm z6>GPVLatcu6QzE>8TB`IsB9^ZHF}hf6+MMkAMtpcjoxLOIZ2`*GPPp8+{R;UJzJEi zVLD>3)Ix*iz(c0}aes`dRa-(h=e8HUU5<1<%2|miVHGG>J@%ky}F-vLiLB!#-N8 z$D4k)6Ld!v`pG^q6aok4&s(3iK5l)?TDN+ww{Lu7Izz?nLt&l5Q%YVK6>&yRfxwh4zC zf1x|ZDZH~csQW-Bf`ltpaWc7#o?d;7D+Mb~VNfW5>KQpFn^DRU9d7=uCUO7I7*}ZL ziUCPslEDZYD5Y{Sef9GNevB*DD$RPO*9(W+d8L&u468c63!F(@V1kQuT-konJ@mPP z{#4J!>htU^+-4H@V#^pXxM9UmkxL9gj}(cb^5&(IVopz3{wxwI@tV;2e_ix z;%YtM3l$AVd~7za$9;K%v-kZ;Us#cZw#OX@;eGn2i1{BhiTe*@T(O&tg%VMT4YC8? zl}&~8Uf*+1jfY#zwUn5LclAr%MyT)Z$MtpS@0!5mVY84aW(!KC7jkrZ0ewmCEpxC< zb1<7ZnAIF?!yIgVA~;{JD>QrAOfw$nq*~RozI5>1Hzqij-wynpvvyBP7V5nLqgMp; z&N16U+Yb`OwrgC;Ei!@&G?>Zf6(_@RdC8&*^hb&Hvs6mkam3 z72ckXMm^pbSIiZ3T<3n$B;v4?tz+Kd*>zaR? zN!+*`_<5zNxY$%h>Pn8n&|fPQ^x9(n(_?x{z9{#4y<(i@^S$OU*skgNH}__fxF4F} zvUxG&N;ny&;!s*V%TB5Jk56!kPA^+6yIsk;FXRXgroQj|$^<8eqDd~~X$7iXIa=u{ zdi68+uM=F{JFp8qf37pEHXA}=pqJm=A5U=6p`%r3WWAmwYEzb;{4LK!BIlYg<{0(EiIhQY*Z14`H9PBCCW(mzitREbNeT0fC!2g|Mglv@6s3C`UsD6NSI4-#l#?PZ zLCVIuL2oz%E{ycJPfYsP@kZJ7u-f&6t2Mb62|cD)ex5wnl8!P2uk_{r*o5SBX=t9Pbc&;%%D;!r^RkD-%`4`^RjvgJ?9?ai+_Wx&vfl^7}+x z@9sATqsLscO-Cq`Z;P!$Fz54&C1pw%Omt8-pHd2rM$n^_Yl&g4UekYzxzA4Mor?R+ zqh1c7U^C^?Kq)FUE6C60h1Vf=zFN!-N#&IVdd2UBf&YvrDk z&zIa&3-?VEvG|+8Mj@C?v1u;vV~aig#^wOo$^O{fLE<{_U1E=gu~71v`TbBj$uVM(QhO|{zSH&trYZI{larh z;%=C*wR?oL?CWq2sTGTOoI$;z*%`x1eZkf7C0ilBF0~7hRzbhBJa5dkbm|8F3HAN| zuDK7)ZH>0>+5Dx=fbGk+rfnJA1ng}5+C~_h{d?={YyWu-vV6i4Tm9{*!^xh$DsmN_dkBhM`}%+gwBM!s%GVl?vXGQ%u&R%TZs#^hO+&e3?D-q-C&dTgc#Q30=nbDFx662LHd+k}7UCGpc%~_dU$u~=AnHdS`NQ_r9 zGWo2`u4H7AmYGq%k4APS%;H*RMxLXG%qSzL%hR0@i|>k^m6?&Zq8Vo5S(yP3(3wt~ zW|#?DW=1}NLYFdRAsvBF4yBnO%t(!y0CW-ojT$jG?nKyAm-5&&upd#K>!z z87ZPquvo44D(Vf(D@J+?R4BpYP2SifTZsCD0}SRb(7vhnqeCpO->(ccJd+`BQi z{?z)1*WbMU&^on#!`jnpkFPzpR$ue3-ER4Y<+GOeSl(cfERe;r`jyp>uI{fYs}HQ+ zvhwwnCsy9M(q9R!+`BTj{M7P?m*2en&@#1r!_w1Bk1sv8@X>|+1!duZg#9lz@5-ZM_L>LfUdr4>ax-LhguwrqKMS2IrWbY z)~BX$u*fIlg6>k%`UhGZgrZO@>#t7LmmEJ(9gY{I214jkwRjb82xA2BUtZo7Lae+8i1jMPo3+CS8L5 zLDOc};wTJ;LoP>L?-*cvK#RjMkWaT3PUs)aUY+uh7y1IHF>=_x0~AwwG&h2nOLDcP%nC-utyU7=ti-vT2`^^Y~HjsDBU! zpgTDA($FJm3_|=axvhVgwT+)359yH<2EobZ(4l`2wh>x-00#}L>0U@LKU|B$ zQJRhxqJI6WoDI|B5R@kEc8^1!v&M!@;fQiVntIZ-!CD-Q(s;@haO?G&4btKul*YnL zz^Qj2u4R`gfleN1-(0YX=nlBZ}>U7Du8q?5S0!(&pG+ro|B`4K;+` zK>yfc+tK21l%gZ8zFqH-V0)<+hoKaes010k{4Wh*)Zu!=2owYjHRi-S=TNn{0iO3vfgxugC;C2#}*HnXh!F#!@Fg5S3 zS{#lNgrcO%Q+zkHI1DB5c+6g!!j0YNj>ZejCalnFIQ901ZR`kkgo985DMrNh)OuUh z@I`2pfP;f}ZA#yd9i9&9AwYgmI@_)$^z&d@OOHfxI-2gsr{>?%6prdSmHw0+v8cu2 zC{D(hTytvwSkU4y6esxdAUe?ZZ(fT7<|H0XvBfEUI;X`UD31B;_M~2KSpQv%gHasK zXA@0c$JhF84GyMI91hihU%4LlEiDen52~h9F8%kg{+k|$(VdvUP0<@WMjd`P5XFK~ zT|r6c?HB96YU$x9Mg}|)zutMqI(E-Iqz3_d1jB?~Q+)qXLl44G40kz;W&Jw0{!c9q znC)1G?RngKxqN*JhXU`>s@^}v`VU$hf?`M|##Z$9mG$qnI2ahHY$aWtIxqa)aU4bw zC`!vIIjpx!tYe3ufqAqT+G(5@fvpRBL`$eb|ug}K$t z>MK@nTKU?_ColhWKmWD)PcAMk{PDs^7Jg(Qv+#)v)IN?goikc#1J@B5fW26>Fvk+htq~9(00?FsR%>cO?##?w%xR68*sQW zu-&w08x=R(cGI41jL!~1&G%4ov(FB^7H4>N;Iufyvx9#MH~Z|sYH-G9K5+^+`^+b3 zafWBUvB%V<^?@6og%etOhG$_}gEKz+zfOxYJo`U*97kXT$PQKsH5L7yV|$whM*@9- zri%9Als@fh)&taKdr?9zECL;s_Sh4+q*S59E^i% zCXx+(@PO?c%Tal`Y! z_Fl>O{I9)N!VJ&<+IuC;@cgg6S28~T-=`S|W_bSB-Ya2-=YQ>e5xAW-IR9(!i@@!o z!TDc%Uu1m#zgII3`hs?SK#^^v#TmZq zy!AK^rw!j%UZcTLhVLP9Eza;gB&Nj~zDq>3IKy{|um)%RE)mk=4Dac^8l3Sxy+?~P zyr*}o@Bi%lTjmzTg}dheasKhGcWf2GYyZmT7dGDo&f~A%wAsF7d#|l$V{A8C|IGR! z>mydb_15{fZ2a}c&w~sB;f>qY|8D)4*Z;5e`1;*z|Frh8wTo*HuI*U<)$%FJ+bwAe zy!!8}pI-g()xs*dy1erFmH)a@TY2Tm#@6pG|KakxmpjYO<>xGYdFg{o4=?$aZeIMW z#h+VzbTPR2qJ_U*_@#v(nt$#5OXt2Z_nT(6%I^)!30}-D+(i$dIya#0?rzVS?Rx`V zS91takd$cjIbS7~_8{P5k^$8+d*bHflNNHei#RHwv^SaUwS)0M)Dw=$j1(d~zy@g! zXotgrDvgRXV3vM$(n6|3{ceRMi|M403zmh!AcS}lkxCg90Ipx?N>Zl7V0pTp_pLt# zEoSHMqT_6;s|+hOZ#W#oay$nUz9d2w`!t*iQGT)EXQ{njl24Vzn*A&@Ej|k){Qp4HrM_a2mOj_6_5e-Ii%}A+KPBmOghWD^h z2@Xa&xpuYYgNSMj_h+MCZ_Qeqw4kbTPtLVVp)$G$D#l*ud;D?HGeq!E-buLYKE5Ty zV^WFsq?gG_3%uTODxjixvlx#$#a#FT6?6``Za%@=t0EU|$7u%2;dQ=L+PHYoq9X1U zypd=-+6~C980PLLE8*;5FDMNKSN;N@Balu?Nel`5+KrPIP_dG)RryTLju%60X6VSI z6QBkc_=$4ZE_99k;L#nZ2GP9pY+9UlH-kN+{_fr6}X#Q_<|8L?=B$ zjw4HZOrW&lnY0+R?apqxLqpD7QOGw$ALE6jNHryP&|)3;RvSEC^ITw?v65vn+3nqE zI*><|G}R;;m2S18`0LeDdl>f>$=C&dgX%NYpw#KLlFZU`Cp_9rBQkJBxNxG`EQ$%q zK5(J~cbhM^T!UO#@~{Cq)e;2=Zp}S0Y0(u$#!Wipi&hFB!!h0=*(W7vo6bT`-uufdZR zwGItO_BsySiH394a$(rRX(a`R%EN4m88$fC$<~qy+MaA#K00Zk47?#KYL6*3SG*o? z^b(ymT8+cCIv>juaLHY4hw4rWbHz(bA3JD4MuT+Lbpi5J=_1=jG8mI|x`OSrS3!G? zpo5J&;6xPfHF^t=O&)E}uSXgARM7dCL!fe0nB|CI19Ym`3oX6ge@ip6{ z2Q7$Td5{S?%cWwiQ-P|H3|?vlOJu*QG&+ucO?HZ+gDa+}^vY<`LMl0%_=TvOD^sbC zLfK0~m+%*xY`&h#rD-JVL79$FAq7sJ`{1NS%!~Jn!3#Zv#V!Pf3{n{~(T0LK=siAG z%t&as2o2CuQtpSBUp8qWII=yq8lqx$uOe1*P8dmg)EDq2Dsmof3knJ?ym7)K7%7oRxK;>M z(`cQp7ac`hZe+7`0S!qhkDH)es1Wlu${e?{K4}psOUVvbatJg}z!&@ujw>i_?gAT% ze=75oJ`D)nhMrv@nnc=FiyY1mUO|;wYd1V4y`uj->Z@9oZLwgj{NI;!%jsprlU19J@ zEXBHn!d?PHgN=5RbV0R^+N1?@AsQSs7}NuW`f|C_EY$OM6ycKzKi$bnZrD$S*if*Y zNce1@o3wC6AhP=_7v~cp>y}fwG zBgQhKy^|?ag=o0iER~$zMxf1*N>=n#(*wL*u`eu7T2RS;fKJnKG6V|R3#@&olP94( z)FYkYq&-2}r4%v1TU?~=Tl<@X7JGTTaiOQcJS2;0oJ*8EnHb7;qi6t@rM;5agZc`~t zNg2eOdxYfkxaD9tnRg6@cIG(y#W=^ z_mU1G*G#kRP%gtcI}KlJzA|Y+R#~RA=e0v(u>$@*Phf*$ATl8PVb+~2gyQZ>UTpc> z^%%Bx@1zAD9n$fd6e>2z0Nsez5iuH+i$M@bohGVO9_Wg8F`21|cyH0hgW3 z2R+Sv-N(9N7R5tw%wH6(2dfsX2q4i$6_uk!vdL48xSxPRnVv_?doVsxbdr0VyXSPJ zTdn!8PI`sPR6J4d#Y2rjx{asALLV(Pk$eHpVoDZ{`~4RPrp~u8`Twx@?!m5X>0Q{~ z`@G*^=HVHS$8+wS@%YXe&1tn>-G0rq)ZJ>mTHP(R)avoI-0$~8-L3AH#>3RykT4-I z=UgA4fU+w%b^>-e*iaJ)CM3or1XEy00VX^g8wyhfOhVv~*eM6HB<*vzw(i#1n(Mht znY#NAoqPAzw|>91daZBuTI*ZCFSz>AQ;T7j?H0;3-jLi*RctBX{;=ugaZ4(axSb?@ zFh5X+?Us=nKe@4h3DV_Fzfj?L+KJnslbiS?@2PQn^ovc;8QP(+YysA^Yi1C-m`M%0?Q$jx(w!A_Phk<` z`sQs3%|$<-z|gWkA?H4vHwSq&K<8o!_6P8y;QQ0A8jBb=T}+ODV}p@RQB*DlV~L>& zM<)idIu9Mhf?Fv{kTwgKxD#15ZpppR?S8>VHnC-q%EJs?rxqS z8k5TjI07XjSeC~2vKlK;*EINyh|7a`sPVSeFJ=?qW3a3LXoKNZ`($wz3H4ZSY7(!w zG9SSDpi&;IsXKMynj(VvUYRTG)r}WLZKNJknSvX*GPlFvif?^Y*WNUL?Ya%b*b`MnzpH4WS@ zU+JoqfM zT0O$67(K&9Sciytq36U`MyD3!v>o=ll@=mEl3QF%>rI+LVkv0%2jOUH#&TGoXRSew z+BdT4v$f3w>=hTAA zgC;tfKxNx c2}6uwsOVbw-&>;$37N0dHXLR=-g^`VW0=y%|0UXy2Jtx*=sU5p*F zs4^-s*0icjgJ_lJSk?Qkt@W*4n+Hsoy#Ez`{f9Boz|99j657}S&?fRYVeXw`q|ARRB zb_68FzWWR3+U2)z{C~DxkC(*%pN~O7_r?I6k3rcVn{z$}W&i&4PF(-q`2WwGs|T_- z=Hb)lzqfb4`+V#RvUk7xeC*5qSdjCvFRSle91C(j#t7LP3vxcj2-&;Oem<@r#9r-* z|KEx0-y8p5J=cEwV|XfW`QF7bJmvG>+Z)4EI{&@BF+AtbUu)od7sv3NKY!gH8+QKu zwYK+M;r#jQ{&R)%=dU1WZcqIGPF(-q7_#%{wEJVo&Y#onk0CpMPP;#b?EE?H{ur|J z=cW5&$j+aa?vEime_pyjR`1>T|99j6!90EM-Z{Tf0&xNtpZ~ub|Nn0M|LVQ&-T40t zWANUM|KAt?zs~=6)jPWLrJD)(=iQ%oTj1Rmc((=KnHKocmFFK@^f<$UH2r87T#hn{fD=Q`N6U|S6+2>x_fXAj4 zu1Z01(9WdDuFt}?AuKMJ$fvJyC96K!M(5mX6 z|NMXUJDxvUxq;u;*m48=M#R_54b;%Lx`7MwcCCK3Gl&%6Qxa|UShrJZ{RbLEYe>+E zK<=4!UxQaX){KI$Zg6googygXlCiUbxp++Mds1ip{;MAo72sR|;Dsswx&fzhUSA2~ zG1KE1^RYt(P2JaR5~Mc}gEkqDtvH&6DaXF;JX}>N8Y2TMt%bE>v%@EGMW*VGgC-dQ zA_BR`qGa|V5Vgb%9uK0p?Lr{#&v6@eK4H?BS#=&;*HMRytsfd(^VZ+=_k#M=L*UsFyayUuuPc3!zY|2$+^{_6QENjSH zN6GfY1(L8`m`5Cxw>??Do4@!iQ~tix(>pC^>N?PN&%H)kSD!F`*)vJe(X^gb)-$76 zp<11~ug&OXcR9H@0ja0OAad<3?&?2G3gcJ2*f{f^iIyj;jwT1{V%6_V&2=|vIbS3M ztbxD2)e&9nclHg26~fz{i%W2Izu(cqW!B3`ebQ+1ECyAYiW;E3s5UUI?t<^2VG}i# zMPGn7Newgbzj+0|^1t5rhc|xw#^Ofv#(VEa_dj$0!M$I+_Y?O%cdvEt{da%m z?pN-k%{>8OFf9Kck{PeZodyT*L=+mS^qFnin2S0!7ZvNzMtvcYTzMieGVEHDs~jB61go96I4wzwzFte^@-a$`foNC zR1+_Y!~~5hT+3@@ZBkrXehu+rb||Vm5mlN_73Sw%T{(Kb4uF|jC6-v=gf7Vc0MhGp9|6s0+pNtFhFvvm8@uJ^N=Ou^2VTCG9YqHH|@7rK7)7GM!^13(% zA+5_1!#3-%HM3{toFzDZAW|)Bih=EYEk6F$jYZ#GGGSHkGpf)a3f`3Q=?(@_l%ObL z&FC!x2(u+FtTfo;Us+oSjUm^r$skrKB0@MMDq}R@3)!6Tf{E+2d>(T;GTfPomU8?r zHyFHVC{d)?6S?alnV8a(Q3-2KH81iNWf(IJJ?&I9Zpsoz|I@nST1iYJ(GN;AiqeTN z5v^uI!qlv#3wER|@Jdf0;c=r>%1^I>Xb?SYS0<|*?X*&I%2Jub+_EO>AV>3PAP42x z=uLz+UgX%*U{;lxkRXUnXdpo?q4S=yYz6{7u1mS32)!XQhh}y-uT=&pas2b^YN&Kr z)8%|&^A$!Z@EW5s)pVkiIGD0imv)8|5CzQAv9lnKH&+|f>a@lf97yaf_+&$rJP4b_ z6uqnoizdUyQE@bq3u7hIIOFIC*A-X%hDWwbG*o9~#coCNtR^ZnJ2q8KWK5xB=%qO< zk@l1>vuje|OQ|}Ptv>3;OBp4RakQ*j=1}TdrZUX5oQZOk9F8S9JEk`lCYx35wyK(H zSDKUu_*7F6o9ifnIPiLql@O3U)si7QCyt-4Ny%BE?n*u19{7_cV-;klgv`)vCfiz8 zBNb?bn9Bh=x8y$m?Q00xM;h{owE3`E0x^~D7y-{*Z$u?aiFLZJu4-1bW7MRI zskRasu6S^=kg`bEr}G8mYU5_vY7sWaOuS^$;g5d!=BL*c7ZeeM^iOAIW9(XirdR@- zb&%4K6NPbCPfU|&*tS%|aRoX0fsMu3GiKn%OTEZLrCd*VS_9F)tq97dV{ikMXRGMK z41)m`#Yg{eT@it^pd!=jX&ssqI}GR!dG3TW6GHJy*&EsO3akd+5+sa8-n%g%XEU3P zMH{3hk7%w~%@z%^U!Rs1MtMNSUQg-{%X|l6ads1;C=k)SgG@YpP>!WGGAwApq(7R& z4W{VQERnTqB0*OZc7eB!zr3!6Zp|9$M9icyUYVs6i0?QJHEq{YmF7aH>W{gS;_A!C z&?q?mF)%N`@y_fOzRgnCRS?tJN^q4)JVAk~PP2v$NJ$8gnhLJfm4-dRo6M_2bG-25 zlg;HQ&F;mOX{+M7V6JN;##mAYAY8sS79)x0D#0KYOH&D^?W}zAyVn@gf!`XV&8*R? zh$EV6IwNd^cw-yyHO)zL3F**63t@1tV|et49QDm-Z6+~jezsT z#IQObY&I^1!NW~08dRfY*N4(vWz+G{53>eTNrP!Dl4ZhSbOp?5F-FRv-#Yq>YlxBC zSkBNIfQwY5`kupG|8X zq7>j}rHxB1gY#pel02uWMVN{>h0!Co`bte%aWms zOIacv%CYixGdtQ`R1-~FpVaFjp%P6Mt|7XN_lJd4BS&~FVKNVm`EZ0y*mUU}|Jn5e z6hU0I9$Hwr>bF9;ObyUF9yWX&3bU4+xOggJbI;*JE@d}&OGG7f%8PM7Oa>8Qh?E0v zcXjMhClms>5%^PBK$oPCgAG^X_-8g1=L#*y(C3NLs}4hdqKn`XAQSm<$V} zVp>6`K~rJ*#>^WI$4y5lE{zh^4kx84rcX4ys&Z!h4I2XnW-w+kR0O%iwcv#7x&taq zX0{`T^Mx>^#RY*z^CnaaHgEM4T*#VJ!h&EHLg8pAt5{lC+E|w>nCe)ytPJccv&oPq zjN`=`LReOY1)ChIR@E_BPs*pkyl0K^D8l@So<|{KHiLwLsx@cRLqH1}qD30DhB_$w z!>k~|H1}BSzX&qt%OoBqLFmT>U=~PPCnJ~NN;zV zJ8S)(X6)oGJdOVwa%^bDa-K@|bvQwJ=YtHmrVM^bG8g7aI-@y6Zi zLK_43V1x!=a#+4n5%aOrp^fDv&$)^xn{vJ6M-4;9CFJOztTFUPI!>vvQI1eINlCP@ z2&a{y0`>y^hK1`D&J_qx>R8kaIoiB&>lkcy)4^yl#HdBL3bbC_iZLH2M5&sKZUiYk zf#v+Z$jiN>XB&(HQ6>!6PLwLR3_8MDm}X`o!B?BMHU`&`;uMT?1}Tin&@s1eD$Oqq z8)zrMD?yR!OJk03TfC~5Sd2DoDlF3pm5H6X*k+a6YY1&R3l~Fg*h(>`PaAO`nWNFP zhS@?$mALw{U(rZNfTjz~IA&Jo{~tK|$)gA6{hz=8seAv$-G6nrdFLnYyyy1v)-T;^ z0-e2kH-hUwcm36CZ(RLmLzvVf(VuzlwyLyftvglgl-Tbtt?3C3>m<0}!rjlz( z2T;7aA{YMR>T}jPnBs??6Dx|PGm2NvD3)NTwwT4uU7g5NieZp}(<~Qk&$$QiyRu^D zx7VLPb}+y9J;zu4K7PjUg?>MHz%UF|>#a$=t9Nzy@R5r!&qZuEBrPhLFR^$F7D#8!;VA6tL^`oX;3|Gd6xu~*Mp?DiQk z2!_gye5CIxbidhR>&@cJ^ElpKky69;XR?o4X?Hxl?>V;fFwYJ*P!xtrItA9EJ63N} zTOD0r&g|BT)AYYxeSY7o`(4)NFuzxxf7goNr_b8$&6%MJ?#-eo*XQY7 z(ZW-HrWGeaw3wz=Y{`~uC5y{hu4f4Tb1Rno;jQ?==ij+vxix>iIYU&@eW@=`S#Hf= zq}(1@Y#=70Bazv1$?lHt_2j_);T5~V?>e{*KlJ=NcAoK@lS38Vmr?SR-MPu(Qhw{n zf&QfxKju#!%+2K3Z{^L8mgQ>mpyu6~e<$cd)hrMYlPpMtteJ^FXo+GM1 zwQ4{lJecJNpO;oFx4iG!9I-c%>nY1E?|Uh`_1sYUCo6Vk@nCk}@*G~VyJ+=L1sYlx zCtEyax4U|{l;ZjU7uglX;#Ur)_@U>;6~(QWfU^l=Me)K!hNl$IO%RvzTTc+5TCW~H z^>YXFd*5?t#qYui0@)YLcgpW{g1D5~dV+YoZokJrd@!^3Jb$upg4i2@`r_GcRu7jF zTRqEf{p|YlZyw5Pwf_IkqaQnZ@M90O2lwy)wfkSb&)t9Dy?=1;Ke_kmyT5w(M?qG< zPlB9&U%B(zoe$jp+1tPScK!DCTYv4=mv7Z>{@a^B2=e!R2xRa3(Hj_0_xri)f8aWM z{k=fh?}x5Q*FJXjp95XL+11LG-?;J@fvO*I<<`mHIC%r;41VDF7mt7V__gDQN52Aq z5B))3XYYVlyAON-7~DE~Xf|WU8w+xfi(=Au<9s*8R5+JiaHwTUk*1=rGuyUmD*Adm z@Np-1qX-xH-83`jZTs7{so1myLSTc26!3!YG^Cx$W7`&coql2a&46>W&NL@o+z}Kk zrVK})?=EAvZNY$^u3QfKdhbOUt9t7yvG6=s;Zxg)&2+TbO`Wk_2Kst;+csBJdeNpy zLY3-Ga-qb#(%k25JKMGc9$z{-SvP>}97gx^x1@kOZ`MgGhC zHNI`j`d&AxW4YE!R4SJ8@opNU^SF&|Ta(OyaMVO}vI#xNG?&{=sdV19zHJ*yxJT!4 zUu<<2SSaU0{L*&Dwr%@Koj02rKd|uLnCrCqyL)%%aX+(dyQJ!dyyQhHHtD9Aw(2h% zPw4BP-nLaNbunLP&S=@DsB{r{@=G~?_qJ`Q&NW%^hl`vYk8+~b*iF}Sp7VEY+lpy- zB>R-DEj-R5Th3%RmD73K@7%T>@s=D7TaLoXdK4HkySoc;-u64TZO7rzOlV?cM!d|c z4YjwMBImsAw{P2~oYzx=Hm)`1A%z zZ7U@{H|)8VlSm`p?2THx>B`RA*0yaK${9sK=n>$P$#NhW^Oru~>b5OmlDruktXv<1 zIGCm_K-^)kioHqbT%`g(br zUQBURAS@>lh(>nDbtiavla{t^u>eawKkGT>q|U}NP&s;Ov%uT7Ew|YY?PZjv7&*^| zn6aDjVw*GedhvzrYU*nYW&}N3V%ky?{ic)1yP35t9~=g0o9X(+)Hi$`0%bH_G;LZ> z-8Lcc1Gd9Yytt$6W?TB`bPA>>dO9t@yZe3TsXf`I#^D4eL^FZ4mUFYglg#c`%Ei<^ zwN1?njP{7h4O4QxjtnGlcgxtK_VOoPctLG7*%Kz8*KHvFI1<8P#JbULj*RVUp|5{( z+t!tMB2R!&ofI@cl>Fi?xo6MYKHj!fg65(R@(ucKUBqTfU)s$Nci#3B+qQGr8>%dc znUM*k5&16uGF$BRS6|q!dPx+HCf#AXsgShVT#`#+H&@EVkK)@v8E^IytD`MjX?tNi z^sFN-NPBnSZ#|0Lh5vT7u-8AnO%2yvy^b~0Ml=)*I``;Z-+M8&Z{4Qm^u5M#VS#9# zncirtrn~z-vPEro?Y~V8ef?uEsI7WQ*ydY_JsI}-HcPhhg|&MKao+aPwyl>MJVgMB zyBY2rmN%VxJs?nODw!L|_>zXg7_Tg=63v4MUG|+>a&&a;g*Ye##a7L|& zzJwb3`ZvFzw(2EG*D;6w(7*f6^b?wso+)Behsl7$!|}Jm8I&zqEYQ zw(Z#LYAT&E&1Q3$%(R*G@|WQcZrd)pic%MBn~-864`abiUQ%D|^$)zTUG)-1NxcTH zjrhT$Ed%#y?dH6__)&Z#DC5mua_K4Y)}ugPO6~pI)GpnEzL?tkwy9mZ8L&mI_7ZBV z_5Y(I^bSMCq*{o1`R-u=zHfBfz@-TCu(Cb$3Z+fQ%bzV)BpdiCay-mKsF z#T%c$@!sn{e*I(De&ky7>d#*NzN<%9{>YW;$)7&?rC3cC1b`s~}UpRlES z?9ImyFMX;P>=(T_X{vz>gmBF>>1_AqSA;IvC-t`{-vlC2-b5DXg&jzW>@Y z7~nhsI3GP2=WHkZtb7<;Z|ks2VRsoleO5eesaFmze|w|&974^XPomw$C+H9mfLIOu zhX+HveBkdo%rg>2xLEC|1hMOVCeNO%IDlsS$iWZ|O{ zZ$6mA>7m#8`}uD^O$b~^FVeo%+Fhe-&mIG))!_Tk!B8(5d{6~0LfuT8oOh@Y^xCse z0G!p}`=*0&E*N}!8!CpaNULBYtv`n20&K6@B;^fTsXp^ zebHxNgnt}BtVZ}Z9t`pF5nkEpTxZqX7h*Pf_N^-ppqlSLn8VHphxSE@feL=?w1V%u zJPzQXA-=l-@}h#(yQ{f^s(s}*uYCE+M^66C z34Z(w$L7&*9zDBw`G5Fttv6=Z*dAE*oXRDEm=YK+_v&MM_rU7x8yBxuFMkiW-UzSt z*lnzfOFBaWi_#0%gO_7DZv@xab6ESzIzy{~MHS|``R8QQ+p=^<#&4%)u5&Y@knQauFCekNu zE9(qx-y-#LH0urX{Ky8k(TZgzGe^58G;gBqS&m-*{%yTsoFCbHN;*SYZ7g=30k#{} zc{!HzhJL;kp?zhYq3v6E9)hOrpzSN`3~k@?^AI$32W?+jXK4E_VI6{|?4a!{>m1sJ ztI|Wzr$`g2wEi?JMgX+J(0h z4nga`745>Z&d~PVH8=!~-a*?})*0G9N!H8JR_p&OCqI33=l5Ly4e-yqKkv4{Z<7}I z(sw+6yi$>V`hoM;1}}8qmk)#;!Q8!K^3|8?kLNBmD%H(9?z9VpJ)*l%7_X&=y&x&9hb8ute0C0YRSu9YHJ~B496XIhCQ}< z7ELNy4{r?{C8KV<^7&O8&;6AYueOr9{@g1-3i-VL{<`X)|NMV`*p?(}% z{Z{$nH<`Dd&3~P|(gj3-d*58Vw8(x4&0Jp z@mx@?KH=clA--o_b)f%$?Hhpp|F0k2`-bE1x%$g@|J2n#b@ltNwy%EU@h9$l<;p+1 z@|7#EUtz9%`0n?g{MyM+pM2@spEwzvJi7NMkN?f_&m4dG!OuVV!3XvOvukU``502*X^IaJ-Yp+d-%Qd+TXwR zZ*Khzh$;Ao@<(%|CdpaZ|nt-S~Gm{`!qScKr^BF$iwdfS$nLzBa%9!-p+K zucb%oVc&f5t+i6(U^&-tt~i^ zhKCggm7dRuqk+~ev`I3|*&bcRjaZAh<)lZ74YO^G;#_}Hld{7 z>`Vxg_9-zZ&F-8Y;Y+Ab`6_bs`l-cGrh&SxT!s}FV}Up@vY-Vo!Ma+1S+c@TTD9Z3 zRvtCjllN{c3Vj?Nrn&)P$kM@!#lkYE(qL?}U5o5+N-fNYvDu+pd~m&dYJu2&xnUS~ zxou={0a8oKJaNZb(d(iL%o~)SO-tCMHNvib@7jV}CSITqLOrJxu1k!k{DLv6W3hxv zeQN-r{-7v^6Q(!D`4eShk;R!jpBXunqJq~c&j*c;zOQ#PUh75*($o8>Yj zGcu9D?PRaYFIhUHQ70(dRWef=q%-pEuu{*mQg-8m8;cmv>zryyivf@_Cj2^+gZy_r zk1Cm_DfU_mqdjh=+L#+-H#Rb@Tx2-&L0wFFbXjZ?Oxi(Guij=-yH3hx4Nt6j*hAPX ztH)P zrAy@fQ3sph1)3`RZrpL{AyQvbGUto9KMbyJG>bS69-{cdOC0p?n$%#XPkM8eN-2JBbIzd9uwBB$s&U!k;R-xy zKqaIwhTUKY-8)%ZfaR|n54#K>ru7jG*52Ys_Nvm3#^qYVZ; zolh{%77ZDc3RSM4Q`Dg#wb|%yP)y{kDxoK1YgVEge|Jwrx}+uTrFYosr)tiA!fZ?Hfrb z4UooMZL@cO=f*;tX<+eP&?^1Gh%@0?ry6jb1lE_Oq7DRsyBU~5Q4_cG?B1z^Tl7N< z5n(8w#UNY5(6j6ztraA#kqjtCTHv1BEs0b?#*I6lU1JQ(YH_is8dN?-44|ZD1`)TM zvYb|~Hq2rYhWJ2WXL+JevfIDDu_$_d;7nXBo`!kFo-PB|;@pK^Cy+rMttKE12Zsoy zDJf+)zI19);z0V30vgmBQHRD!G%`lSP^vO=9mAp7Jm?YTaJV2RlPmUGnRi%_nW-iA z{3ZjkzhpVc?SLjgGrUIiQl)0wmW~gwsb3=G;KQ4yS`>#7&QPsdEywU_FRG6^d~eZA z*tR3LdV=Vw-OAYZ+G%jINkTBlfew4!N~s|o4AgizTgWrGD2hfMfu~`!G8R3y55WxN zw@$u$gOT!lvkY>UkWEDJN_x*0qc(y%M5Du!5*nED5Xv+!b-T)q|8HY4rzQ!<=+i#Z zk-*eaH6{&uRAZBxi#k26KOYXsW~*XRaPRi1pN+^s^ z7n6~in0!i%D3mHmcuXxz^DLp-@Z{u;Qww@pRX9rxg#M&x2z+6L(nZ{%Iw)k85|C}D zI$hLcFKF<$|K`Rbh{6~$jb1b%S(RQ&Q#7Bp%2Yfh`PN`LN+7pf?bj*0by`c43nm?9pJGoEckf7sG2pQR9EiQHZ?b^+GvRwRji8mY`f`rT0t>8Y*065 z%_cb%WsuK=cBKgx-~OSEg_W1P9UaKFFCZ)Cf;2@6esQQai(ZB1NLZYd*qZ7VE92gc zzq7G0+6skKqRy1+(Itnm>uAeIeIuEK{Z4{HSiM@x+$gQIu@iA)p?W5hFf!2~NwZv? zl~~`c>20G@6&J8N4X7g07OYAI@Vv6g6*EvJgqlba3SnlNSD zC%CxSUwBpG=!+W+VN%gF)C-j2yxvfAAy+#%u+saMOibch6N2PQsREJ~8Mm2@1uw?( zjN}>88Agc2=95xE!it?)MAgQ5zcH&dbWgPfmy2)z(X|DaB&>ycEGddh!|!RZu8pF4 zq)mGWNw>ucCzxTuQX!pnZvUZER)VsyG`S~H?qC!dwLWiyZKefO^N(x4KK`)Clsy$O}GT>~?nn10n-iCT6>`P8%Db}5vo1{~N5$o!hz1-Xdxywke9 zNrEx}xhAGS(tb1^)kljM8`-0V=2wlBG{f4AaJ%z}W=&2cWaXZ)!BDFjNOUOllVUMU z7qz8d7g>{`XOgK|#4@y<M7lpxQuAR?E6oxr~>w zPI(|yTCfaFhw(jgW8t8EyolEtiJlhIDWqGq?lNi`b+uIvHK8)C2}zwTsJfmVZ&(e$ zR+T@lF24mPT)x2mA(NeiRNARk+ zw8awHjZ7oz)+iaIyl5hIn1k+JVl`eArcT*xmpWcI>?Xt@P=~gcr3>Q5CpQ=!Z?c?~ z220c{I_@YajJtwJRMYBm;LCz3H*%#o%%|m&&^!8vrxx0@ok`OiEM$lbYr^Eh2QNra zG$K%tA(PJ6ujOTeTu4AxF)w;dJVc zhA~u(r%b)47HSK6&P*I0iL@|gqFHM7=dCN7wFF2iqc}a&0WZ?9)=>Nz%TH6IF%9Z= zuV;1TZcodE`moU;jBBT}+E-!mS2p#id&snA%~^L=jHXjouNt8N#$%ycAX!Vu2c1c2 zR4Q1Qf?SE$=l{pQa&-HDyUu`r-u?NHu?4+=kdH&eM}R6%hV9?dK{WH|J^Xueq z*2n8BU-PoF_0?I=ivz9li`HA8VFAm}uG;g!1Fe0-;+CE=~I?7OWbsiu4{oQC+`+*1iLbr$UZ#94(~(##;qCglZ{)%dam2dOGEHSD0nbYxZa&wu{+J%0YJD+lxgi}McX z!g$@UodYWV$LWAB@ec1;oZyak*uGf(b#e{!Sc^8}V4s}U?&WJR96*%aY9Os`r>dq) zb2kCMUL85V>25#w%Biw|FL@gHV`cT%(_fvd_qw>Vzg>9f4gk--vFlUMmo24IB1LP! z6~_&q9g-VIc1C>7O_ux@G#yKEUi>%ES(lVm?bct~NZ5h@TQ|T9ce#qHecSJAFPVzo z_W6GeTC=^-UFwybf$d6rf~IW!v6*OUDRvmFISa<{!a@4*2yex8nsAnrx9u(mIn2AM zr1euW_9rOaU@9ub>R5m;$Bdv!L#*GWtDPCjPP8Wj4y1^k2_fp0#--6*VM^2jb~53y z;aF*>mCV}}wL}v21KlO4`sY8tTK|9S=!-`W{@8=!{hz$g-}~G5;(PDC`;&M3cmBqm z4$%J#Za=v76SwG_zk2fvH~#&N|N6#9um9!i+O>aw?Z3SC%~yZuYU9dZx*}b|rSDN$S(zgfY7oL7UDF5THZLu%H<R_?Mm)GsQ(khH9PZxl} zr?=_`^cP@MGjo1d+vH#dZ*`lWeVPN5?H;&7L0AXlcGdC@M%nIx87O_b2kw=yJh=4p zJuuyUO8e9)4=(?uPpP^qi#dKeUoqHvN_#ad4`#6GfuDbxT<@J5UB!zPIxbfqI7q!` zR}k|HPvf1oE$&n8c!wCg_B7h*J;i<68V57j*2_|!&OrIwBdoYjP2=G5FB)MXK-nH) zK*jZ9y~u-6wntd7+eV=D3so}?F8%xn^FjIBKEAk5C*$DqUp~Ti^(V(qy%mG)5%%_V zI8H~xixKATjIcdQ8JDX795ljql_&M54yf6yub$1Hdqo)!W^kc8mh#jFK-(RCuPWog zKo@m%3!rRw^u5}Q2cw*I^zVCWg3{5gZm=r-LdlAQOW#=|y!O-p5ZklWURB0}A^}RPB4hDK_SHBHb>mNS)($RzO zzw)7zKXU)q?EHBbQj)w}AQU%vA_K>u%e>u(+3J$`npd-JaX1;C%V@txQI`|D+( z|M$t0_gwvxS07#ZyeL^b(3Q{V^nnvcIJtbLMi4(1F}76;zd^~$U$?2 z53{yWekhJHIt0g`%SAWhtx<~Y=ApRvxibz^r&w$AJ$Sa1gaKJWnj#w;Ino;ioEcRs zjp5Kqy2y-3okwH`Hq-gE*zDK)6|%uFXmi<_n@wGFdW|wC`%0(VZs#?PZ9O#UMz#oB zOBEOKk=Gz}b!U&@UgL~|G?x9Enu2iZ2ISTH?p(9&so~E%Qn(npB}nOm4Wr%wMW9Dv zUYJoOUYOG@C!gx(NZ|^lc`|YILW@+SuCA9lNwr^!ROF#-)mzHUZ)z@;cKy)l@9q-Z z{nuw4K$N^vnsXJZm6X#-J6mK+w^CYwj9yfZP#`$eSQuj|p#$g9v}lPiY|Y(TN_1PU z0I|hETUG3OVMMYMuRLyx>S9QQf`vR>(0W6v_ZV$Ck20stVY?Y??*7X&4!u%))aS|e z)C}N&Qw#HHrAr3$8m|{g)$|FF3#x3F@sbrkia};G+0~nFInhXSifWo$>*jGvw<4(^ z1{o{INMM&erXD}+HwX%|mA){h7vmN&XbpCr#hpiI9J=7}f)<#fn%fm=;yOM#>&@B7 zp6iav$Xb>(tEFHz5>e<8+OD)MA;g9J$+%gj;pUj?*+K|eqWw5RODV!Ev9#-91e!g( zG=t8q-Wi8PLQEbf#DUf7=n;wLs?}!PLDTE2H09c;+8{Ln#8DObM~lczd*y1q6QZ1b2U_63w=3&Jjkf_}O zs{A0-%L?UiI;qKn@?bm;9*IT60kNhb?zPbZ2q~Y-1~gcB$jG7>J)duIj@+#99grA@ zcz9`or(3f#4*mcH68GGmYS$NJmil4P*5xrxTWonc;)gw^mX^fCwHLu7f57ofzY+-3 zsV&U7dVj*gp(KTsP}N&pfeR9}hcg4o!<2_sqT5};B&0sq?t*J(quoAv_s^YiaLt}E zjGh~T5Y%DuN zs0Lz8LDR4>6$E7$5DXb+)v|9BA+Ig@MZdwS%JQn4VUL zfgP69sG$~vN<6C4G&0M23f+Pm1&2<|mX)!Ow0ScsjflV~*dVL#c<#%*#6le_=U51t z5L~&6NFwGNmA>)N*K=G|3|tQakYWv6O1pmY_Fv!PP!W+9rnzIt8O8#njuEDTXnhc$ zUYO2dPm%kK4AmoK2t8_1QmHOX)G|{UcZ*71Yd|EH^kz8J9JEw_IkO6kVF_a*fez@C zKYhkQ%vxc}WO2+`Qj)1qr#dC#iaqpU&-0Om7*;j0X;3v1d!$TkeK06MyjU%&m7(e` zxsH-p?3@}{NZrCGEq9Q#61b(d9(J_4JLXxdJK=*?x8n?V+u`J=&Nv7~%5uvZ-DY?Y znT=Mtp-rs@Y39R5v%Tz?NE4x;1&I+F92GY40Ze4oK6PM9yyZwi|!%jKerJ;XKR7PNP!F6f<^pl;RL^8d5!$ zkGq}7t%=@<=?%+|_<}+##sk5wM#TzJRcX6A5_BzAjpk4lYyE!U3srl`yG!gLJ|OF% zGt>RF&dSZ8$9Hzd#obSxao{Hy2p~qsgQ`Z?MyBn|e2Q&Y^&&OK9GGHoh49@FZcpP! zr6ySoqFEfFCeqbK5ct2SYC`J3JXFkriPP_tYG%O;%NFtQ(u|z9{=pfCp%9r`f&{Jl zayc*5#7ec=liM&fiW&&gQ7w4LhUGCmCa_2IQZ?;Pd5wm$SZZc?+T`*ow7@8~HgY)K z?K&8S*&t-U)_W**^Jo^%z;v}-wDP7A?k0D>_IQg!DNca1?o9^>)z3pL8>Ak1gmizz z_4%qDCA=4Q2~oU@XI`ZAcdYx!guJj4S_ zBqB^*gmu0rb|h_AEBxxw8Hd5R>muFMG1Ee!>rPPzYcJ_urAUx*i^UTVNpI!NcB(Qx zFmIYfrLHyN30AGiU866DhT1ZiIGHahMPmlff`*sQ#0m;Myfm@z?Qc2bz@Y_kG@hqS z*`GFR@sJ{#tqDAY@k!B1Y-r{?;e6J|rv>OyMJ`mT;8s$rpx|CgS`)p~%*-)DO{peS zD3n+&uZlG}s6y;R7i$icNo(G#3-O3+Hu~C5pFH`IGY)K-0dKNF2P~}ZnDa}D(ha(1 z0dohj%4wZem|<0w7~l*3Q3bp?3O+cp;|6d?QXe-waN-rS%51UkUNoug;1t!{*m2wT?|}<>P11L<_O^o>UO~0i+K;W z`VG=BEdrhzl?5aV#={AV4P`_A|JnNzFh{QH+}_pu7%&(J1TZExX?nU=mCCGIN_&-5 zT1tx<+A68ER+XfZN}CysJq8Q`#-t}aNLU;v4|pVD3ol{c!VX~xn+=4JN5X?7yhp%c z3y-{8Qjc4zR!P;}V=&1y-`8ViPMve^x#ymHtL{1f3Hm@6!i-dMdG+_fmY*Kz5X3qj zE4&*=^4XFvOcF$*0m^thFvakNZZj0X*_6xGaKyH$Yk-~Gjx2$>%e{cD=qwexVl3TA zKpc4a^|xFcs1Jd1s-JBz*yDTi$F~sO~8U)cUdnbi-S}DX0WfG+7 z^Ys54gcuUM5?8U1%ieaD?QJ^7)Y2YK&3QyAli<2OPg4}wDu8*S+9^b=_&hn0FWE_- ziwXdqy_5v#oensQaNVT#?=Gmof;2WB!J2g0-U-Dh@T@;i#|j;$XKx2%EE;mv8a%=j zc~7yGMOkaeRcO=pz6_)#DB@$c9?J;L>#w&rx^njV>%Fm9cn3O- zVYAXpz7k@>7c)o<^gB%4VQG0*>M(4!gCGu8T zP*Mp{BiVH#c0VpQBW+ZoVhRo3hS4M$u;wJ(ZjITi@nj<40ENa5yXiThL@+1*X>&4q zDfYlxCrtSg%?VWthM;DEhuSa$aYY(-1X5++m^pFTnTkqyC2O87;Bj)7-DYhW$%_ZU zR^Katf@>_1<;pQ7+g90XBpGq~nW&$2n4XjE?}ItDAVCw>0mm}#Q=0mS#W}0*~U8%XlEG*3j)3so=Ze zw9qIw6dG!fEZgu!P*Cw5Ys(Dy{w5TvT#N}sU5R+ibnN=_-2#{s_mnv~l%VwhniC{+ z$>xNCa zLPmCTZpqQ9fOA(c74?N;(PYH*I{DMjfjOb3&B+7!+=X1SbC(Bq=MwHv02b>3iS4H1 zG?6ZiTPLo34FhmOc@oX?ShUkfg$peQ?4$9_ZWRf70&LdFCka+5p}f5s1Ghg>FH1WD zrsw2^Zv%7Unl>j7;5{B;F4a15r#-P)x*nqgdd2Nr7iuRel$5Ga7#+on8E)JjZ|{IhWbhW+?S-w5rV|36Se%U$`v|BAiS zT2bSRoCigX{HpNY>I< z>bZG;w8U1^&2%N>Zufd@UH3Usd!Ql6Km{QGYJ<;;R{d+B_EfeiH^Ch*cy@fnq1x3*p8yQ6Sv?wGtt{?hs_9U|7+BI?$9#s{vAAkgB1kGJupT4NnMK9Hzor*WyQ@a3&?VTW{qZ*9G9JsO0nV!-Y) z2jH&_9Uu*;d)vz_d&~U!($JzX>P;c1;*55e^ttp-hp;|3lCw;96qJAK}cqtyWTQn6aGCc||+ zp36xQZwC=~u&m$TcI500D9O6rV5I{cj)O2zsKx>kR-tR|om{MxuGs;yT$y+FdV#Pa z*0)O8hA$4+ONvD2cvJu_VIqP^cX*-=uuvQkqKi3TRZUbYe08hq$Js81@;0Zrz@rTv zbPn*9`vC^2?3CjKjgnF*LY!%P0?(D}v?CzIOi49mo%oX>_j zY);-?P-U9)IdR?kHho&^hp}(J^j6O~MP?QlQ9vq2^ zrJA;#X~hT{MXuDSiJ9&`^1#&@xhiiXS`!=yLUv&rOhC{!X6+xK25TPSLc=3QvLA;6uOi%1q-O{rIDD^yTvRbfA? z@);*2GpIJ+74^0iQ*!lp;rntUp?YB_(FmhN^K_vcs-=lQjXT{_n%x9P#N(h=LPV+v zl1upzdxz(9Tol0h59BVJlB@p~ye}7ywG~uU#a4*u1Uc22<#;a^@+42!>LnN;&oaGg z*DYqk1*flTgWG9}#p9Y^&8xRrOv%;1%s*Fcy`4yPT^U)F?tz#09`vdj1MH6x{(qJ8xL|5Hg7lSo#vI zL#g|$&aw~Wqtc;VE!MI(8Vb}6Rg?9y#~o4W;9lP6Jrg#WsOar-}R&Sd% zC0GCbe&4U^q)ImY-dw`E(;{d(4h2|mNeIW`xD#ONR;=Bq6Xc&ef>=ATgXQuyq2X=R z2Xbdj$<-et_T{2jfO1=RcsSewy+GPtrdtz=fC0j5?QB|dK>hvsHT)Q z(FkWSo9JtUc%{jGjw!hY`(Qam$6_IZ3Z1TS6(;F(6Fb(l3XyoACz1(&oXGihN^T4Q z8pYU%tqtQ&$^qil58tDnJ8MR+&2T?rt5z>LH$zRSz~oa2p3v<4cJIVISn zYbD4p6BS0vamhk6+!4rPQnT})s^@MpCD*{812#`ti0xzpJ2J(3E7^=<&vU08;X)90 zCGsjGwAz^~v?0aiqB#a~Rsi-mAVXe*sa zNWhp%GE~Zk6+(h(Y)6m-Aal;!WO31(3`K$5JJfR>Gja)|eW9IUL2DyC)hV>AVQ*sT~Q7_zXbA?b| zYPKSL7|8^SsR$mDyJWT2I1Ng<2$d{(;kLV?+C$O~?n^^>Pm9k$Zqt-pgMF|7dzfgk z5l`28`5nbo3G~7dR=^uEl*J;}P8lf+))?%oJIgpHz=hUnRETPEqokV^M)7>NmI`4wiaNX5rQlg*Fby}b-4CKBPT`hG7 zw>x8rf>+yrDfOIWO0I!FR{~x&0|lrap3TNRv`g(-lVQ~hGU)S3fuRCH5${rJFzzbV z(2lz&^*YI}=FgON|Buf6+03awKb1fAh|N!L-n@xy{Nu(eHUjIvSbyt!ZvCpYPp+L^ zgI2$_`ij-S$}d*lx{_PDYWb7PXF8(q-rK=V{xp;OFTKLw&%NKm} zKc9c|JU{=?xsT7?GF%CYl{6B zuI)4X=OZ#33r*<1@LPRm|8x{)Mo5JI3s2}X`|eSg8Nm+vFI>}S_MM|JGeQvbUwC|< z+5esnJv+mQ^L;5c~rqAqKM`1=FC{c=wsl&{Ev(M~*9g&&U9?F!cLpFPK zpV{9Zg_#vWaXwRYR1YzGRG-;5kH`#@N=>1Dh}m!Snf={SnBfSDM$1k)e~8&5`^>&^ zL}p05=kp%c>=Aute|r>W7=psNRFpc*?BRW8Uq1>nBMeUeg{%6^zIGI5MsS+`3lHlv z`|1&yjYZA$UwCMr**!;L1`q-XsmQbs``ML!W_KTjnGqtU|H4E1%)W92W`^LF{tFN8 zGyC#%6u|p}BWwek0PRRH`ifQWg$Ds<^vaix!fZS^rvJhfeZ9VT6jnw!m;MW<`pmv? z6lOTUW3)?RCwpjRZuXh|%~6;c;a~bMZ1kCZ{wU0hz%Kn4*89vpcNAtun3nzvYkg*) zJtDKQ5SIQ6t9@plISMl)yh{Itl|Hk-J_<7<=t=*DEC;HpVD9nsNA^m%;KC^cng&78u){wW6&~L&0{QtS#nYEuTeG>d}`RB3)E~N!P z=RsJ@l{-Jwd5|<(CDZ*`mn&zKxK2)P2R11mdpP4Ovx8O0644gnoWxmC7sHo37{4N{w%YMV|8(YSx5U6@zpZcPn8Q^L24s zw^emeSS^D#o8Ee*NZQS1vS~x_H~xb3jlW1Ef4y{}W^&`NU)xXvV$C3<^S(Y+Hw+au z+&|$>lc|dX!?@PJqDU~k@t6Ki280ZdK&wTI%Z}tS?|~wcWKm%j0}B#{C8KA7d>R55HZe6JIgJ!E2^>3jvDt?0i>y`WDU1KE3#TDRIS@1(Qwn7 zZRZ?lp2nkS$!o6h7pV;$luym??P%fhrB58HVlXTU z8N3Z3v_lb&Y&&p_F2-r7+A7qEj$0&sJf4zMm6Y2}dIl9*#|Z-+C>9U^#8@W8$8AVb z5Iq>L!p$95%K@;_y)~#7LHJJ0?~I5oxl)W4-L`Ci_M_zz%KD2?v=EQCI_bRZws)Ws z5Bcqi-R3Y?%%lx{kpKT13lE*y_|!&v{k!X(b<5hz*PgKY_EqP~->hVozrTF*@}}k0 zr_P`H&CNgCe2V2sOYdItFW$3QT=?BEPXMQpBk`uo=&Y!T1 z;KT}CW^ma8|K%+p-`ZH5IdjXY@kKb;Az)9lbdMchU`E*FTa}3d_1R@c0`+u80txxn z`b2^HG)W_YdO9P4R{7T2M1lG|HY0(0IwOI&d~0>0z|jOYgftSUr!x|W$+yZA1?o3? zBY}E4BY~)Vt29v{tosOWBv4OhBoLNw6(5K$|b@;4_0!P=E@f_;uj0A#pcxIwN{rWN*p`Ol2AXtaboG5T~ zeHo8XPiG_$tiu~73e>MJqY>)qj0A#pn4TmM8(oKn!TS)Mfj|tb!_-89`t@ZnLQGF* zBoM5_$|Ql-QAUPyc!{uzwWs;4s&2#$%Ri2_F>i17&ZbVdR}q$y4m zs6YN0jZjZ#BoNp7|0QRaW=`FG>MZ~R;KoxnfCKQI%?|)9fZC=P-~s$(<8uHL;JF*g z4QOL|{hI(A;Ewg;`ZLxazV>~95pd^PXDz&T-P#Pm3V6%v?W;Gg+GcN@wXHs8<$Eh% zT)AuIg)51br>(3n-@E*w&n&%ask~%cT3Y<#;$4fi z#iuQKfI79KqR&G|RYKYQLk|M0nQ&o0fqeeTw|@Z4|C{>$taX8&;Z5`QLu z=Y_dz;5H}&2K7hTbOZF}0~Hc#5o>h3IiW*_izPX>>&&*S30DFX(rmb{ooB%lH~`4N zPy)v=DlQe=tf1fsZaRsZ>{WN7MD+dpP)zat$&Z%`gPR@wE$m)exvcxLiyv zk`aSk*XFn8T>*zLoM)t@FG%5TV!O@N*@lx4wso6=s8VBHD_yKtmQ z+~wq9N4%935C^vK_WdU)I=-!V(q6Hy20-hII@OKxf}M^)5h7epsUlLy2ZGx;4*+C;gboz z61G)^Mo~mjmfsdzzH)~4@V;s$L=<=P)ur19Pqc!dS7)IDBYd z(d;(p-snK7I4bY@shNKtJfSj0G_O`rXRR%?WAzNpS~TRC-O>8?z>y zOc`szo_Ip4V!WtqXL;{7+(H@Wyu~2MH@kduacO@Z!MHO z*;G3pjB}ofqC{Litg`q!gC`WZ8&?2gL(-cOg?hA8rs|bS0+pN2~bvvB47(nWW|39g5$VQaZsZY6e7N`hYT4xSKq^KCR` z!{d!!ezzBnIcj`4i|>|p9d;vmYo;|f z9pmK04s(*_v!8+!5_Id!>;J@1kDNZP1&j)1-_mbgID? zq18_eo`~%#Os(PdTeVg&-97(QpGo+(H?u%-^;DM4Skxyjhs= zbZK|1nec@ug(-)MelFN9p$TusS)|Ileov=8tL#7F<$H+R){$*eBFZa*z=X4nS}L3O zQ;Bk^OiE59ES8g0uxj~#7oW($&3w-b`CN`|yahhSCkt{1r(947jUw(qvDdPP60vQm zz4Uv7C!&~u5>Y&#VQTTDl?C0kY{5_`MYfu1&>?j4kXMl4M1}FOtNUv(V)xpxeB6sL z`HCNu{sw}el^^KCX77mUNTA_wvUN3-3RIodWqu%oY1*=0#t++ew*x{cS!Gi_DC@7p z{S59y$R?G~7d(nwwANFLPuPFLEtJ@jE#PessdhE8i#O6ZyRAAQq}Wi3nOwBo>eab$ zFNduy?mq#GHn>}^i2$so5Q7;OYUWZkYgQ`eyDo_j07wjhiTA3AZT^LWCjwC_P-D_= zv@51!SR7A7-9kRkb&9+8K*sAWmO_xtmTVSkjrmi9Cp;;|iGY5DcufGvPpNP-Un+AB ziYvF$vJABJ@wDtjW7%+Z<;{a9Txl{<4KZFqgz8+Q9;IS(8K!BJBzk<9b$J3j>DeVh zU4mM;e(;2|8IBfP8BZqCRJN50=w?*NZ&S3}SFpLsOo@%|SCe+}9r&0d5rr;DLZG2s&Ne6p8n zGB8wA)KoN&@5X$RTC|Z3n4SC9Rr4^|yN9;m!TxyNKsMP53eG&f9f{&xAWO!|ZD%)% z@G*fV5WJ*>LfLeYL>0c1n*VTrtA&S8@&i`CJ78t=?B-nU5>$=Wlc+00Wg%N3R&g}qer*iFhTEG@c16IRF<^iia2CU3?Y@B_|fYtCJcfe{tFyM(|YS(WqCd5G6 zC8ygd8;Sn$5LvXa_i|{HcDs3tgyfqmAanpj!`-c!%2rPp5pa%=S|YR zbd(Opa9cRtQuO`R)?YM22Y^a(Yh6&)s9x^vqjWZK)C({!ufB9`{RN|R1cqavC26^! z=lA?kIxB{wT$2~{HLvT>8>K@r(Dx2tIu!N%ZW*P6FpNxsKB0%~ck>7xLSmRLDY?D+ zr2X~hjMCXKjPO-bioUpO{p=_m0S?wqPb#CYTwH(l2ptR#;Ak{j52f_usEyL07zQW& z;YwIfCy&x07zQbMrpM~(Bt0EUCUbtbKC60N9HoOX)W+9C-b3?I8K$$^Pz<$}Vj=O+ z`nX|~4!m?=8Qz~bWWU4+9RcW2zHILuq6>}CS#21K#PTJtK5u=U8Knbe23;D0(L>`1 zjM4$QP$d|yF?#!XN9b@XhS-#JrmpWPyKWz)1FsxJlWHbhdVbFsrNc19+7L>eL-YIe zQ96(?2(TWyVSRb+`qM_}KwLwRXhHNI@`LL~=?Dw~Il{4YLa!e=N(aUZNnxdU$j&xB z9ZdFop~HRAiD5dNL^0Uvtg5M&UhdK;9jL&@i$ck+?_s>YILQD1!pz!jmIu#$;lKRN z=Vb%@`di@kC+}hXnuA9SWwwE8rm+P`moBpnpPbodl!>g%GCNwf{%Fm?{gTKwDq=zo z)^M)e(aIX8)$3!q-g;jE69-KBiX!R^cyWFNo9B^jTRR3QQi`J2>c&HERQ6^O3EB zGg6gPbJ*NV|-oNx=xu1kcizHBY3I5Sy^E>$7QlrF|#8R{SD~dZ~6amo(^tMriT>l_&@o=LR>>fVcn2kLKE8;}4 z2eKKSc1m_nt!b?=aJGnqJx~C5MkQYeCIyN0#XQzP3*1`4E-d0HwU2$;k;@7rBehd-if98XqZ70Xug|Av*~Cyh{v;Cn~JeK9B6LE zd(C>hCn`Ldt#|A|9WCIivR07*W%JEr`B0%9BFA+pA}4n#npDa;{Qcko z{uShY+2BV%v>2i9=*|#awJ8Vks7+NFr`nXuo@%uQqv=4JVPcVVsn+&Hl#@f;%0NSiKxjJ!&Qc zrQj_m)^vt5@qE}G+ff+IEz2RO%L+bRB9SH%2vUJ+IYbrGZW~c4E5UMV$FY_5)cDj^ zHD@BZAQwfqpqAZ&Ke@z zSpO@$y!{g5?62a3_Jq&2!<%3%Mj(KW_g_n~gLq zIagBBma7)sN+gJIECvTa^GexEhI}P13AOzZUp?eY`mHKJq&j}dc{9h^JptU6@Y*T? zwpYxyI>c6-43%6>*vjxs+$*T{os<*Va&ZyKBjbV8mf)?o%Ms9WqQ#R~x|k_@<&vk| z%y31d7sle0N#u$Bp%0v4xBr3kVz9jdKzo7SJX+Y5PHX?7gMd2pjbM0BHv(%qMlWfw ze;v}~U>%KUVdP#%TN(ZDv!>UDF%-jb#(HM({(sHP+h$Jfo%*fKcWwqZ{%%9rSX_U_ z`qtX}*4Wj5SZ%MauDp5$TK=nL&hlN$vn>x=dfn2K7eBL@S-5xM1q+Xy|FijL%za@l zKl{_!7tcNhFuLD=%73)Fz_RSc&6$9Wpo$MD05I=ZEZym-x8*-t85L%PK71g|2y54j zu;rr)Gs5CEBg}GCVJ9);(x@;a+~rGS#>JxwJBb+=jw+ z5@B;k6?PKWpFOIulg!!7Az@?V!+{x3FtWRDKB}-2jO?!Gj0!W-`(R|p7goIM>;gR$ zh@tWD+xLlrjKHR+gD)5FdiK#o8DTV=5p~niL>U29PZw3bt2Z)RkP%M!feDR(tj!4P z9#z=!jaa^`b5vm`F=KmFm=Qwyr7>gcsKQQSM)jz|PGZL9s4%1DdTGqqII6Idm{B>Z zu#=dven{9@RQv-ooA;L98JTocVJ8`xI4aCY?}L$@ zL|A25m~o4Mi^7b*bxyw{D&JK;sxYH3d}f4|jw;M3Kh}(};!%Ye<@=fuRu~m#^cCu0 z&WyfdP8ZhC|6dSi)?TpO0DoNmxom;U7PxGI`^5qPa+uVv%7!3^3DB8sd~2TRZ#24Q z5ZXueF)0Z+K;j1JPinKcJMPw-fQ&X*aG^1L8^a^nZZQ!~;}Gm7iaonGhS#g*@=hlh z7fRhwI7BuumQ!n7>SDH@;Z6JfC{y>a5l1+PjzyX%K3y~z}v3-DSi2;c9%2# zd-8BqaQ|i(JT?4PejxmKki)p9!o-{Q6F?4+Q+(V_yRma>+8yuy2k<0d2s}PE-MP`- zB9N}rL&qZ(rCVk=t11P0Rvby9N=b0gb!Sj+x{IQ+1(MJKljGd9BP8Ilk>*Ya5L2^2Ax#YUQPGvM7gsC zU`y;SXC>w^lX5s1`q{J3zhcj(g@G42@5@XwUJ;in3}7UAI1HQtCe*D`?XPr;D60sW zrl3vvuV4}NMYaT`QL4(?=SRSP_*S5 zZJ#4!cjbJJQmEzk*hxE&=W>}HhgY#Pf*ckyDOhcFl2|k=x>Z+M7V9=Aj-#c7P*Z6k z-ziXGfR3Z0G{+=cE^})_8#*WfQ1^&smc3>Ed}(M=7&V8ReyrE+janK>vm@`8v}E9` zF`|E4KW4MUNZa96GTisW=&c60%kXCT0MFkudR@|u;*ypjO-R}rww#M>s1yKONIAtw zT9r#FyqQ9FK<<^Tl;DzNgAb z1yQVOI!KMefr3q;(;TS>=v3DbU5|w$f+9%XmO`i9ek@s_9Zay@ zhAMSfF8GqxT-e#GP;{l?N80udo%4pRbd+@mnwSm8y@hlP!+lMstpPdAR67FTIxzIX z{r_X;muF7B?$l#8-@p0HjbCiMVq<6hjqBmHPpp;J)>i-fDz)-~mHhJGfldIAv%K4a zEZx1tF8=G{-s1Mc>lf(xkIv`km*?)9d-Uu(XRn)i-wZc+b(s4lWCGkTFDaLq{+G1C z4d)-T#IidZGZ#Oq5g3HvsHc)BsQM(%ioFxa_}F4H*7Ud&(Re|h=6NWC>CE}x1l-Xf z?kI`i2%jhv9McYa zJf2vyF4Qy92nA>zJIF7aJiJjLV}uliOiRd~e+(}%~0BA7Ls;!1i%9MftXuARH# z{3C#rVOK$M; z7@AT;6{l_{ObeJiSHn<%|GJF0?pvSN`BH)J< zik-NqlVVzrao+e4AZ6GaH7Pc>Vvij1d9$M(_QnST?!(>)`mQ5{RAkzE%qr7k(0Sv7 z`jd4K7!mNv(b|s29C~m$(*h=Xj18=-S(_+XQVG=>`T$Cy3p{h)4vG0wu=TG$m z{2R)4v&-@^n3DyEd2k^A<3?CqD7u zm}r=^Wm=1jSjsen#%P&T_WTkMH0<(_QCgE}LE~J05l9(!`4c4R?Hl#L2x#XU@+95ySfjWRxgkT7+&@%>e|^LNfCPrc(*?bLOf z-vqq?!kfz*AK2(@Kk!NmW#`e zwcKsF!{P<_{_j|-EnPQ%#oWi|o;zoo{r>D5KyQGD&wLJuJM=@HyK?c&%u{E^zi#00 z=wgJRv7N*K8FlU<6Zz9w)VT*wuoIsd_d|2%6VpHcrYh#LS+nmV^Sfe#MpcdfB@24vK^m5F>t$7P@o4uSZ* zJdw}nxD5OdhrqZj6Zx3(XOI&@om-l~2i!iJTJI!?hl>;WjQXL0eZVyyb#7rIA5-xV zBSAczpU7v_pAGc^`|{jGKBoK!BZ2?SPUJJ{KL+!Tk-&duCh{@mKNtz(>dh1Rn2W0< zh^x<;$j6i)VkC&GXD9M87gtFTSD!tJk8RYSjpp43;_6Kk`HcFr(R|rJTJvqKT8w&jQXJ=AK-`LBtD0}vW`7(U{>Him5F>t{l~~YR^UJ7 ziG0lYj}`b&X(FFd|1s1D_)l>nA9Ma=#rygHv;R1=`uU|W_~Y`=e?|-3Uf6qzmT`H$ zK9pQ$RNgoJ1ClPe%rNnKBET$R_@Pdhh1FxkisF*Ej_mh`%Uc8uwR~+W>JOmd9un|5 zq_FIYcc5+tuK*aFVH7pvtWuqbqvdvwiZIGw*NH&7t(+VzI3imepS(p2V#MoiQ|Xd} zFL$%8_>K~9ZH3$6C>y^B=A%nYJ;=fXAm;{OQFSuyVJEjmhbY-qO9fpH0wtjc>UX3_ zTeL*m%XSya`1pEK%vu3Z);L7`gHO4D^GG_`FDZrmJ_WR#vYN_^rR+%l*a`4t`&K0V zY+H)yUd^gF^_=&8FwYyVxs${d|NJo4a6RsqEPk23&tQe{}>VN*l`fgw_dR{P+O~GR~w|<3WGcQ4IUJu0u&RIyFSJ zszJ6R27Qe>*67tjr3f4qg3VOp*wrNoB$G!2y`;}a77GdiV+t<2=|(bKW?8ucN=6DU zm3C|;2`uPxH!>*Z35tmdlBgBbS|PzY`LYl$*uAvq^LVVOS~cx(dK_kO96>w>LqB`= zMO%BTF0buK( z_z*1r{@& zm^#XPP+iv-sOdP+N2lZntdz2qi~kL<{xex^bp3zX2Ydu$9S)aT zkh z9a%2t+-g+qY^7SM1lk;$h=jXjkHmva$I1#$bKz1O`q{Iq;C**(=8ZFRtDC>SfvtUJ zwYV~8dDW72;dAp6`0EGG&)lugT1wq^<}tJ5GXzGzb3qX8vUgHQrK|<0YYJ5@&rkqYPy}n*$cR7<_3%o?CDI{J9N9}L~5(Ne##kiLd zvPrI#0rY)ZDc#E?a%w6WfwNM)5byRP(Hz0Jdo{vuv-jEv9;Ar|L6E*^%0U)=D5gYu zX|F$)4ZsL-8Up`==Wfj`-EdpR(7yU((EVj#V1%O@7z;^8lbh}vG3OQA=_r^)kGq^g zxL&rcO|TC*h&I7^i$4UAZ`!q5JDiXJWeJ3ekDCM6flQDrL7LnJ`5s8+63)fU7oWyNqH9 zg`#u8tZ&G~zjbSF>CA1}$IKZ-i9`3W_nVCeYx^j10uRM%4IW{Nyr(OF?qnJV%F^ke#9O=@7B?Dp*UhccO8&^2gFGXTS#V^|G(H?MJPpVIw zmcjBu@M6x(b<d=A$8ZQc;j{q8*S^qWsfn;T-cZsa*iA2&KN=b>iHbbQ$zs#j$WoV3*?bvoa2oiQOQVg+sAo zIVxjgtn94Ck{DV{78|rX3AK=ppH8&58#$4S`ima4(Um;AH0&iDL6#k|^4-&{{L4C7 zq>d3W7}q88Wmm1jdvS!O;98mz(;1G#qwe@PD@PNoqeF+;F4b_*8L^WJ`)%cHJ>^HS zwi=A*rF=0QVKciZ+a9v=SEgB6f4}%Z@@4u`MGTiekQa-MV7bu_mF>-#Snc@sW4JCZ z%Tc5t(+W-SRtk^uM6_O}n~0N8y1t$h6e73DLOK$#)u`&QmA^dA%KB`A2b7h|m&lhr z>0T~@sfn@}%DM`TIFgkj?(7&}cD48@=a?gx~WFI8NoB9sp)dNZy1$hxiZGec0cTns`hv@#xZt< zak*0{9ylmVOHUxcqb30>35}xc$j{Pwyu+95h}cAOe(+1}jlJt-I5`bRYUBHeR}%)NuTy z1|#ru@OgI_Ag2YsgV}mg7lMnydLHrS*p^?aW@~Pl^Hiah(j~ZPqgo5s8lXkaxTJZ_-^w?pN#nY9h$h z7o}hS*c&G1D4ZFNHClq+lv}sa;$v-a<3o|r0@0p0?j4-f1|i__v7rd{_VSS)k`B~5 z&Nx#I2j!kB%3fdFQzELEGfMB|y~&m}-8{h^TuiVBbJk?95l%EyTOJ7@`MCu(=<<0| zSiKrUu_EuZvhEtu>gF6#TNrmZLb%dR$O7SNgvDA%O1d&~rQys*3Ct7QX;-{EPIKUY z+R#DHRA=w0{ZRCfp-^Px&zCL~VV6TuF@xY%aAke;%;8GaX84!bD*)7 zEg#0aTb@ie6bm41YMOI3iy|ZBs0cuQ^y7t497>b} z{P z=k8us=bp3JUi2?Mdi|dDKV83hJ+l6UwV$kgYV9>^^)=VpRjdEB`rg$$Ra6 zue^EX)|L3mb<6+0{F&uHocohGcJ7+lAI~H6i_4znM_RsT`G955lDCkS^`*aCdfU?T zmXb?bi@#X>{Nf*Py!J_p?Cjj!-*fMb<=v0c$&_Sw0y$yiHpP+J;ulw%NuuY+oTc{I%t; zFA~RHy-MEgDAZIZ$SdtcqPhVtt=+M9hgssqwexG|%@QxHy>#uRW{Kz5Ub6NQv&3_2 zFJ61`MW>rKz_s65`<+vdoq6l8$;YLEninlG%{>=4$xYHM@xrEU(`J@SiWfaqFLf=EMKsE!7TCPEq`PA8?(e$ zTRw03yjkMMSw3g^oLS<>T0U#}?99cWF>PIK;+wcx&X2Hs-12d=#1FT8%d(0BA1CZ-?n6Ht;w8)fwER!A#EX`uBrnNkiI}V&DOv{@SDF@XVxPsySjL8COz}nnFVfP zYhh;oORL=6*}2HZ&sYC+^_JDIET3JDEMGhO`PtXa-emdt!k^BDm)~&eji+ut6+89B z&0lPOVe<`}XE!68*RF1@&a8ZC<&7&huf!IUi`$EH3tyS}?5Qtpyl&&B`J3ls^G}@n z#oQMbZrS+!+_kem--v8HabsrvE9-Asf8Kg}eP?}P?Vhzat({v-u5GW)%|2n~XLE0u ze|M3B7$dRa&^NP#K6jJUyC@{|GJa_Ktb5A!*`n0*H?JJP= zrz9U=C^s?zf2ilkHgsG!+neouQoibTbHSS58xz!cz**K`U0a@Fd5T%mCtIFumh^hd z_4}lS3d?bErdRDcxo|UE(JQd=(~X~+CH=|9PxeVOa*>goGRM`Vbi}X3^k~cLuUUVM zS<+Xpzj{DAeYT!4_YAYT{@cdCnI*k<<6g6*Kiv4CS<)YD{J<>fzi#~NfONVA>~r>g zu9-3`*HeVkQi<>E4&8V5c$|FM0pQ%OHM|53A~ADRD%S<(;Be;9t;#6 zw(&Kyq+i|ms#(%|HtyLcby8k?IU7nQm~O&HMSc3uZR9f|KWtQ~J z`WdsN&s=||S<)NUZ`dcDx~)0p90O8j>Uw&r<*8;#NegL~)Ml}nB_%AxKIznbwX@k7 zkWODP+$?97G(H02Bg#X)u+zwuY&2MPnmm) zS*}l>yLe(#I;`3&wWEn%!>cBJqDL3C7k{{T@nkWT^aqO2|8;TyWWh`&{r=+j z_w72B^#3mYU$dnDXYv1Bx4_QMK8`6S3ZYo4M};Gx!C8cD>7#bLZIZCnGD)baCJCEO zlZ1_iNkT>0H_+6B$EW5$WtQ}l^Pk)&oqDMM@y3t$NvE#T*RH>IpLA;Yy|8(~Ea^)& zUt*T@`J2x-OZvRc=b0sa&gOH>lAhf>YnJrco6j~&dei1jW=VUSJ+q|U&F((wRL}av z{3kBo|LeEq%lH2ou~=<1uh-!AFW>)zRwMUW)Pyec}zpIZI@ zBW6A@b1J?0{>^JQ&adCQ{>-%xtzEr(+sZdqp0@nD<#kJQ>7FI~;_DYJ3#IwL0yp_D zoBh%34d9Un#Lq2P?cI24W|n;-J2SJgJPVp9Ew3K*Wws(HCY72(UH2{ZzDA4yfV&_} zZ@IfVE4}2)AA5iQ_a_aY#SXQk0IKckR6D5e0^Q$HG+K7b`4g(PqAB%}zwiJ4rz5EL zES+l02UY{BA@QEids3?{Yf63SoBiMaU|O{+_s(dlJ^o-qEeEP$2ny#?QOZCyt+spm zY+5vpK6p+4_qC?=df47GHNCDq&}->HFQW$F_c@xx3(9oOmNcE-_fqZm`yXUlv#a-R z&@_Acfo6*bni;hO-`DJL>DF{D7d8FfMQgv`bK10)kK9XZTJ9WZxiF&T*p}k^THeHN+|_=+V^(+)k*ZvH?!qn70R+6|-hPSc_Jmal5R-x@Wo*_C^VQ!^|3>taOeVh7+@QMV6-E__$No-sV&p{8^i1WSX1o{i$<#H(GYLBaxV^4 zqlZGt!Ny?Fy?t~lr|Q+;74G_9nqGhO*G77cLb@X4Ef3p^>6~n6F0CMv$a%5eG%{H~ z#7)(#zbU--hni-0{=SiBV=NZcG#k4qz&L26ZkNPPcD!c&O<}5D{Y~N3|EB5n>Ytj{ z>rs0VO|PM_aj+@C7=TA_B@%9Cyk5f{?x%NT?{5vSe2%8yEAKU}-_?7prr+4D0Y(v+ z^k&(lzUXh?*T!rOQ?1zF8eZPl^5x$(t>q*4!kU(2w+0YE`%gYtXbKWHqH2_}<9YFjc?)*6^~vC%#NHZM}!>F`9lu z(d1xjfMEo-267Zt9dA8JFt=*^Tf<8}qgn2yR@0hYxknpr4G;q3nM|ao&$k`j8m5{| z)96K?)qelZ3r*{F)m~7u*x0QBA`uu1w~L%=WU;|J!_?CH{?>5Yhc(rn_eLYt^boFK zYX|_<#%~Rf&C0tJdwfJOgdE)(rs~z-8lG#@^ty#I(o5fl4(R3AOg0pN54HwF)UAEZ z`T=;VUj2>X?9Vj4o^!jAUWe>;)t*l`l?QJNkP%wRzFzxaD$@})`YZA#MN{pjZ<|)_ z>OHTf+EDC0*c%`tFoS*7#>C#K2JG(;?I&njc79}5OYQw1nYn%D)LT#GPCXQ00JJx+ z+qh@rMH^3E|CjYwuZP!X*WR|4UAuDiBde{|Ygg`GdEv_S%m2Lms%6^p3(K1=H(DOF z^r5B3(lv`;T73Q@vGARRS1ttS|KI$Z=6C0>nET+IGWYn|FVEgSd;QGc10fTBxV_qf z@QC0wfe;@-9R8+KX*;V)N~}kD+*8KX+Ln{5M-XqzPlh=;5qHTxfG+{iJFffo=kI>7 z^rqn4AJ(7nKYr_{k9^wc*MIZ2|MBP-yyvwcPQ9?HaC_B0V3?0_ey_U>FQ{?20m)Pk@jEZpkYjwDZO7>Rrlzd-IQ`9~CP#7vZ*_2(k*azHhbp6Xyzc0V#!lP#1ee-+1 z`Vfo%^{@Z&Pd@R!?|<*DAAaiB-a`F>e@|SXhjl|qZ@beijb-+&E$gjkxyL^FwljaW z)cR!0Jtv&=-Ag_HNq=+e_RY`#?zJ<)y~+YR%nSi$_oX7$(R^|U8tC@g3zWu%IcgY{Ux&1le6`Aim@jb!4@*#G3m+gf|hkB9Sy`Mh&wI8hr z^Y5GqcD{Pm z>8o#fS?3{t^63vf`ahCqUwiNKU-+3fz3#yex-b5>E3I^=y#h{R z{oz|n@AG_~_kEw|nRnjzd7k(7_rCP&FTCK$1MS0(o8R;Y!p~lK&ll)D|Brda;X9wP z#6D;J3B-D7n4J%HL$av^%0n}|!R~FxpSeGGPmH_cv#+}5^3MbhzZAXfUk+aRbI+ZN z|9pq@eg9w(>*6pwFYI{M%g$VS;#=PF@9*|}Xio}60@3Fpi$$jMU zYghl<-?=sPD`K4=mdy>je1cZ9hS_a&^>ev*^6%b_pYQ(GrTSa%WE_`&bmjaTKlGF3 zCrdwt9*jNf1;l!Bn4JrDdFc{kr^a?Q^tRvrmGS2vxakd7+$X&jJ$~VX#sB<|zf9fp zn#}WF30*_obvLnI7-r{$T^^pz$fIlNzM6dN8-H;)?BRa+y8rjt>t9oQOY%*%YaaEj z`>y+&!*}2Rw9a?mdmgcV(>8W)y^1p2Sn2Lu_`CMPSDSZ95)q^j6Z0lm*7Z3dd z{DIFWe)y|E<=!B%o*!lh-gaG`P&FxyWq1Cw-@N-9KYW+-Nw-~m;RkQJ;;HQyHf#_6 zh90o{q$!o-1XS6z4XGTz5lpy zlV5tlk@SA^XLEkK`|5NyN>*&nm z(w9Bw-~KJ}_wW6OYajBxomkIqW9JFP3vzg@bmzU}E4N;8-@EH^pH;s6 z;*VeX{cB%w9rDX-;wZ6BZ(}!h7rxJ~deKvVJ^Pqlufr=x8?XNnB7gfA!IwPs*j-n= zSuv?Yk9yoMi1pMkyGc9eeReN=&b!~ayZyn}K22zcj+;;a%Jui$*#4XEzU$TVU%9{a zO6^$;v7Q`eH)+GW&+a2{I*|A@a^=3qd@B5`mw)WQ9rwTK0p`P(8!uh@hbO$`x~DJw zHL;!;W;bbrRyth8|qc5KKsNMhZqt|~j@nGV-XAPs*SYlUJqurY{J{|3dfi&}eNXw(>%Y&v_n}uh?(|;&)}ueanpmfX*-hI2 z?X$c6Pk)?$uCVaRdou6NtiSdpkCw5*(r2&lL_hPyZ{2^-M_zjcu}%)No3vfqXZOx; z?g<}x?)~ED*RbFIjqsu4pXbu~i_Wv3dj)y#y&wCNPd=AeCx+Nf++^*u`@*&Fd;QaY zW3sPV`09T%+}$5}*{++9f8g!!dD4gXKYGP==QF^92OnlPX=}94&UqE}6yL92bomRP z^e^9vKXl35t3URSKmEpSH(Z6>^z!H3hj0&K>)6m5V8V80pV>!0?7YdiB>JjPeDXOr zCNDhvj+J-5cjbF;+3f(M8{W5wRW{mTl4=WS^b>feYXB;irD&E&u+lhhCbT z|LhM|Kk>ECzUkxNu4b?P3E_T93ph`7nBAm}!#=zFufOTV7r*Id5st2C)tH7WC_Tme& zU;g_S;LpG18zQkD9%eUbAFa>s;5WYcmgUdXe}pXffAh=IlW#x%<~LmMhOb?4Pq6Z= zOFvw>1T1($+t`iW`0BHJ!1vJ~JoKO6`oiw7qgO24@$Ls-^_J%aa_9ZwbDsIQr~cRb z9`oWy6YIfYc9Zt0`t1I)_@l@RmW6-+;ERrZ;RpBJf9%mWfAcf)OP_r?`n}IbzJF2R z1H?Kq%x==|QlH)Be|5X|gBu_E;WPg6A6`ZrzxG47y)W~g`kTM^vwJ@8x$ZyLUVe;N z4-B!JxF^(ScUermI{NeTAN<+wKYHE|UUmKzfBw~fzCZQd^{apNwJWPPTy)O^*!p!t zHZfrzr_by$?^>*$|JyxR-O+M=*SDyWf58eCxOYeE?7m4+2hh>{o z9lFo%v;TDDRagDz)s3Y;wEpDMtA4P2#f?XPasPp5y*ROW-s67yRON85{{Q@WGyDE* z;RyKWk?de{v)Dt(wUO%QBb^i#pTLN`!-PcoYn|BFxu?9rwR==IJSvYzW(xu|- zHAAd*?|FS@AF>7R#g*&bD`5}Ay#}CrMtx^|w^pm})?+>08nMu%nd%C5n)Rt6hu7%_ zSj<6c9t=h99osqm{9PvbD;_^(->Mi{VTP}s7IDll#r~p zST<1BGh{{cR|(PE4!aZ^f?2B=iHc|w&BYi5ml%Z&8mfsQlGr3!Mit6N2Fu|PSu0^~ z!sBemaFUzuj@Vw;L49g{J+PQe-Zp-IF*YdK^)ghM`>M7?q zEAef&l5>7n{r`#L`Qs0kz&s4qt0wBPVv1NsI?a$!VHBU1W;5-I7)TP?$tg3o>vwpTA+PzCaiglsg)jCO3 zRd8PjAQ8ldA%U-;?nT<${m}Tb_>;~IyE2@n+j00IU-xk_*6p-W6e^N}ohnA13|=mE zBGq&*?Y9!qW(aTVajpW@5nA=*VfM63Q(F~M5d21+l6_?fp;{#FOXMIjXY$Qp(7S53 zDh+4K6u}M1K0}oot!5*YA_#=a=yX6z5Y1!(&S2oyMoGW~+2hK!Qf_Z(x~r+XUBBg) z7Zld7TO^aWLF*T6aXaHxbsDErH-VhJ2VSY5>LM81?h`OyujPxPR8UL#da0`lTdzWZ z{XN|q=Zn1m7krTmcAT3^_FE0Je-VA&)KoD1HB(i=%m}jHPz&u^p(=Lgq)@BZR$vF@ z2lv~?A@NWO85=m1wj zbg!fzS^8~#&P{sb1EO#B*AU4owT`C6odF81i7Bk2weYqoVu=bFLOtnppqz}f(%^d4 z*x;&@?e6Xz`03Hmg1&_n3^A{WJzqR&H9C%P)1!AKy|S+hJ&tm@RxnIW?2hd-hm6|c z=zS#$u-gs!uPWr@LZIH&omI0gWH?IeNHwZ%+qsmb5vFFseAr15Y7SS}vKB)d!F0zd z(V`iXSDT7}TAEW#HKR32qe{G;s5_WCrgqxtREI9Be0QM*W^=@;bVb_4PusC*7vGLU zDUJ$sbZ^Shpu9pF!@b^UfIu>i>hcz3|MvGl%}^(A|g9hb}(&^@DFb*gWW4 zdT8lGOD|etmM%K*wF7TB&^X}R|Iq#qf%4$Y{)_f~ZQmRAHTHS;{&w#N_ujRa-unlO zUtN4XC}-o@^BYjn_Jw;=dmg{=;KIEN^##xF-|YUt?icP(?SA~O2Y21OtG3Gxq60oK z|HApy{Nv{yoV$0fHs_xG&FlweUpSkZeLSeOc<OpDzo! zIMfXCC3hR;JdT7-dj+gR^N9ji&XoJRA9L><5E8h<0Qb39m0(tsGp!P2m+cNCq*G>8 z0C(4-fvg%NVr`dyaEQIzapS2S2p(ttQRD|oF2DTkC!#Aw9|Y$X*; z_YwDK143N%g#)d;7OuAQXh>oM+)9~+MY-lx#4N8Mxu()obExF9`h@eZ*&yVQU?C?` zD`_QGjdYy;948iqmVh}anNQY*kke+?F0Pa2`GjL7;S-fv#MN-NhQ(PS zkLo3tl4ep(R_PO7H2}m&iByrS(=9kST@Kq}0q*2ES&Jk(EnN~U8dmG2f*&*VK^M;- z5XQ8m*p4AcD$18SN`S~yNT4H{p-iBu(>}=tFO*y%qH#i?Pq<&)AS_|PW$7)KOTck| zoWY!kFPmyL@ity6nntYdkF-RIF`9$1nOzQ~kQVkOGAS*MqElNHA4u(R!fjxXFZAau1cn@x# z(~z-JwrP`9sg|@cIM)maVJ4T;^G3B(i`j*eX1SftgbTKnl91$-@QQ{tNM~b3%HacZ zT>RvK5b|g_K9r0JZbOTDqizUy@MTv@u*IAQRztL9JMkjrx9WpF&b?@$aVsjsik_k> zkz6+KvA8xTgnhhs#dK+U4yhsT1ewMiMFJn_INt|EJ8@EFf?<#DslbtH$8txRklo3| zQ`s693C4mc+O11eHW=9$)QdI}cC%b5T>G{RA)+s$V$MIB;=%I^I^@?VQqKJ(W=GikUy3}T1EFj)R})C z5E?Z$=!Qz|wj(c8y|^o2#7wv)lZ4etwVY5?ZtE$j;N{VcQT^@)VZoIQ(*7nwRAJkp z)(7==Vypsx5g;*ByA~87tvyMV0UWi1gvRNN201g};5UNd2fk`pp zJnF#1R$BE^Jdh_VKV`vxlqRCcqLT1nJQEZ__J!mPnpa+E7W$xRyf(`r+P!OKb0 zp%Q#wbC&3VsFGjy1)SwB`=~|aT1q$~2?-BtR~Up-8K{Va5uakT_`y6bym>$-NJ*vIh;Q%&IPjeT zVF4@?4Tp!KJuZhUDW)>0SOd3}5aO;`KO_ zLL6Mm1sK=ULhqF`7cytR?r7}Zzw9XZP3t&F>X zsBN9lAl7tOizz4~7g}0{ibBP9v>wj6uxvKN4;~#iZ2*QmO%J$UE!DA9?@A?QN9!fU z_KWUBk%yIh+~qSlEf>@qgNNL{2R8^;FuzJi_;}2t5^B9zhvFFFaMu!!QY`QA2U{Uj zrF9A3u&TL@v0+85$T;wTv)1DM7+kipG1=jaQFbXStRyf@)v=@|f*1V3qhoRKJVx2L z+>GJPWGfZdGTxOu$;RvL1TGp3Oe>bWQV2DzN~n^@H&hiLh?n_0-pGFeB8s8=x}UUL^x zjyBfGS6a+qHs(J(AfyX0pURg);z}``ESlbEQ50OhXf(yxLJYhQA|_5o2uCe9n2iIE z84xC$Fx0X=Wz+~Yo1T1*RSYq}1`(KI>w+`T7NIJda%g;KW3hVM24RqIw~VIg@2H&G z%r_O4)pW^V>5f>AgGX+_WtZVr5wqAnCSci#KMJxF=G{quVu$|1Y6y9;wl^_eOl!^^B zXK{Ul&|8V~a!Jk9k+8$ydx~mNhweh_}0K5?<)-mDc&OSd;xES zBavXRs%43loFlv9mFZ|T;I1`S@W_hokp0mO&#^KfBr|cXQub##M=2vGOgC4ds!q7# zOZpXD^Vmj8U13{Z-GL6K->Mu({taQd&cY!X78MR6ku{rKf>C+J#)#+mc*HTvzN33og7aQC}}n$aXv2E zm5~U;K^{f_c=QO~3`Oi%s+H4xoz+mZ>_yNi|qjOI}K}R;6M&j>Acb zZKQq8WVY(WrEpzZTLh4+UC7@|4Y`^3b_COH9*XeG^$5or9}U+!icc8yDLt&^S_*0oPh@IAbsSv z!)v>G-6oTJe}0J)u<=4HDdiX=8p+b>@@Uq?KvPfNZP$?A!T!yxlku4yo45hKFCdLGXTKZ1^DsQ07I6w0WmR0cJzl+3o)>w6J4B?XmZ=(qqp?B2yEvEmt-YE6ew`283pfqO|e-R4Vs_IcHp5d zZn2#S`6-)2Feyg_OvKF_6Y;%sF6AUCN=Kgwgy`AMcc&KOB(~$6l*4l4(GCFW+0K7X z4K&7fx&ULdT5dYp1^_+V`Oee;<85bBrVE%ddt=J}V`_|(+YU6OSX*spQi2S3)anY+ zwViKIEySqpOiGslU2JW1@n26}RAVaJaSU~Fi|v3CK~6^~Qg7FWS~wh~qb3ldrzuJ+1uG)IejjvI{UaS>UFlIsoWthlWOI*GE^tr89XWPfIHgk3enZdf0XrUn>mo)e3B0IjG1jLU8S zmhthaF-~rtqcOFcQ#L6Df;%dAh3J~+$EFry)I28z#`fa>=khcA|9jz|!9S0D9vOkN zIReisuZO$g^hcYUA$$`p`qauj5;;66oF0Nt8k~2sDDkZUR5;!a@}^&0T}AkoX0i-U zuG)1pn9-4zw(Nwf0t+YOX0qDo22c$J3Qw%NGZLfK$P#v{Ue~yIE>htL*6Xg3ibVO` z2GvmOfjS*`W>coCnB+km&j`e%(nBKwfP|E26JtVLi-)1Hk=Tk8<-{6TA+yNP*~xK z5Ix-hp=+Q~FFB+4KO1puUCGBr9!@lXF2(q;K@hWc;LMqspE@*9oV5pEk+>E%m7%D?9IwTg;Iu7Mfgdl_{gA~;wSMKQ11iP$Y z>5z5!mTvT%S?fw9cJl+Cc^DLI9gdjnbsdBYFRzEXuI5;K*wu{9QapRE2KwLWYEIz? zCaqO~mTvW+J9BSc%n?7Zb-mWLfZu~BFhP`VKZta~p7gegaoyNThoh&sUD~`LmI|iY zO_}QE6r4!sdgkH5%g!Lm+1>qh@YPVid+auU+rAp_18%n9yYbR8sGpwimAC>wZ{^*O zx9)w}m9a0+h>(3pwkddzzF6pHdG-EOWM0|$TN8x>-|b!Rmw~SYeeE*e$fj_Dzqdyh zPwy_r8u6w}1mHsm?OTd|6cfe@?w4Pp)3g?;dE&h-mc8svNX1uH@q>zlUGAi zJ5RuB{Hke^a-NABfZUF*>|@=4P1OR+kxmI)b_kH_wLF1v8OmA+Ml01qp=zr%i*i)D zWupDevC~;K#T|h_rr=CFn^Kz3;2KRc%}NjsaV{#<@Fg;OCY2Pa2qclUOezi4k|fS! zDW*;HP9@xN%9#qyc36fIA`q1}_~` zkN3W3@7;T!G54JVZ=HYkJT-su>|L|m?B%mR1WbQ6`;Uh{e&`j4Du?`s77zZ*!M{AH z9}FBkwDjerH-X54=PgB-9=qGveSM$%Ze;fr8wd;69Jp}*H+TJf*9Uj4?<(wn`+-{z zkX^~fznVhx;@lBWKEcRx(Z`Bh-e)(cMln=TVrm_As3nXpsHO{K2qY_UIa*%&uN_nT zq))+$$!t+1bhc7)c;aS9lxe70G6SBHH{$aKtP+QIR$waZBximzpolnePp#_pwlFH? z35i;!DaRGR3Syjuij@z;g%!oZRF~kGy?DnI7xgI$c^DT%Muc>M5YLKYFnk$7HN0J^ z#V{5r7OTaTX45R>^W@CWcHEEO&c6*@5Har4XN5@Zpx=hGc+5$r>tw*&YL-;C?DTnI zHPtF=d4Y`z7}@gFXa9c36o0p4iu*2{cLHVbZ#k6!=VxyB1@G&#$~y5(!b>SktdNf? zcD$3|x?w%laK$8=?L<24iU+-LAxt?IKhdYixmZUo?v6;>iV?*eWW;2kG~_E$Fe`d`?*{4W?N~Bh}3WCZZtP{@Cbl8FG0R=8(JEmKg1(5@>#uCM1 zz6)A?ibTN`L5qcwLZZHsQ4YAAUXZqD2cwCgKW*SPltzd$>rioWp}u2^+Kwrz1B$q} z=16#%a3SH=7=cJjcD)>ELawmSua*lZd-v=36j{H zyFg0}?-y&1Tt34KCS5bqdmKBaSluzjm3@kEsL~QT{uR8GtOh+zrdVo{xl)X=oKoHo z?v3&56y$S;n5wnsUw2ILz>X=t)ThX3MJ)tz0krPQhhs6W?9tH%X39Q`SqVo7#zDtS zh3!a6`rzd|rnqd!6qgPtYCP$z(;kRZ4OUA@Da=AdZ>0pa32(T?w0$h02Gvr$z(3}V zJEnNUjwxQ>r)ctMJ(ZN|eglG%22JG>&bV5vqiEZ1mV(||EMH7R=_p)tJm!uaQ{2A) z?K8r;cDjA3cHrTxKE)0msmBj|edagkx*rc){{7*~#!LBhMlCsO1XfyU1Ys0&lTyT0 z%>+%1@kV6`8C6KK7(u%Ci z26s$>?3g0Zr>JpmL9p6EsS@L|(RiB7$dRa?K)6ze5mhS|rlSVw!V3|0&)~&q2Nb{F zF~+YP^Wy9g4{sGL0qPj4T~D@jRB3Zcqy^>6j76at84k2~Ce^BEWT<>#rv&?VO0cir z2_C8YLUy~@Ae(ie3VWPfIo7BmFx;%t5zfme{6TLDX`=4HzC(SA9lYKBV#gFe@1OsF zW@i7Ld(NNx%+Ac@k=Bolz{79^o_F;++KV}O{Fa^VNu^%T-p;oB3u`#V{`{m+wvld$cw-W9?E}H>jvo zX_bUryyzCI?N-@S%&odAs$WsPtSi)%bM;lxbNUBk;bt@8bF@N{OsVFCWWOy16lcSX zBOqp8N8!I2)QrD1MAl_^(6FAmnrm0&f>c_cn9G!s$W zE;S*KZPE>sKkcJFAQBlduQkF)SJ9sB4XpN*5&oUKB(6Zn-jtNb76yNPS@HqB*r0a3k9=*lmOj6sk z=W(2mc${+WK);ve%i%GC0Ph15_d#gTknic%Q9fM8@WiM|#%d^Nx=3YREalf3 zMNhexZC9(=Vo*>V%P)wrNP*P4wbMYoThOu7Isa!Wu%D)+dfHJh)T&{gcPp3@DlqX*ebT~H=_D}Qd zJgZfLp_5k$J~!d?zw=FjbMm|!?!}~xxsgXI?@O{4S#GwAA=SOQoK1t`m)`bj!e!7} z$CQjrr{!QmX;n}+7V&4N5V5cO3>p(+xAnz-VjUW7ouGzosSXP0Yu@QBdb! zNjTbtSWt1KT&qwhm<%_Zja6`?1P2M>-g9YuM!_iy5knrIznfrll2@y9HY?lL2EX@P$cc(}%Pn_by&mhnJpI|$-QG#< zr+EMvbwwOpM&a^K%rNu(UwC=?9XR%r1odv+vp%akLMPca-HHinJC{|Yt(C$!EQ{#s zs)|F{GSme1RTJ%~kV$I^oGZbS7Xf<@<938LoxnNpUZZ)$UGKH^VK)3U8f0aFHq(WC z_a0~`4f#ItXxK2VJ0I8H5ESko!GFfORGr?Fm+bOoC^y+G^ooYMW?qbPh*+wQCYG(sGs4}m_aZedv zMI~Q>X@CM{HKmSK7%8l~DcT~CDp*cf*+9zSRf4NYaBvTcd^J@h2zE7?M#V<4m}ztV zQZ=Q)Y^quFb7ag}opQTtF!X(8%=e7qg!X{c-gcvR*~1_O>O=;nzU{$0s_Q43b~lf= zBx|0=4Od{rwoQ^vJDzHJurtf-?axkamy=U5PN%Oo^xyxF%)D#n(E6cE55Dstwe
    u0;3zw6@p zH_sz;pPQ3re>J-{`xL}ZsSEK)$5(a+HK|0;G*D058PueL zN2i9W9ADm*+&X}h3QcVe@ua#&(||p7r(lz+MNI>C#ZJLaf5s}uFW)KHnatT`!(fwY zcAcF$yL6{uXEKaW*(unW4C9e)U}GzjZ4Tp^fIWGqU}s`yhj$8gCUbVlFxaF)+8o9+ z>DiNZ3U($vd*U$IB)KiKVn%jVtkv!2sX{VRf)3d zTT=XdK8ItPIeXkr!6uc=n+EK$I|MtUIeW}7*rY+)9L6*0*`s#|c1Arre;90%+$VZ= z24K?hM-45$C+y5`LQUGAIQ2fGbo{)XK~36=n+EF8&Y&j6kxm13aA#1US7S?ioEoZf zd}&BAph~J?0^2E3I2KH^T-H1G6Ii` zz&RQLP;`jw9@cFwI&=o_G9%?6yC>}Wn7f+cQxqNYgWCxNnkpwoR+M0ydFy!)99j)F zLr!orE=)SZbm<87`SF5!u=K?ch0fX)yM9nQr;6}?jxQaopvBfIFEd)HgfpxOk_ zBe_Z?T+qXIL(i$k_OADC$L?L1eR$GA&ekd$-6i{otr+K>F6gk=^;>TFj>K9Y)PWkV_h|~i3Ovsb^m{ApG4TcK3~U1OyPe(ghhqbt*ks+G18GJrUawP?oR~Ch75L$*< zWxIwrDXtR` z#azYQFqN{GBf6ry(Mkqf{Ot;u2|nC*hZ%p8Qhl<(BQ3Oni*QzEd~!%OZN#OxQ_-*^ z)fVg))(&R`#6bt4a6$#QPEVJ6)a^Q`{|8q3OEdD!;>Y*R?sx5)JNU7smmet4$g{7Q z`^26F?a;0}`8hHauvyq{v2esu$okD#wD-4Oi26H1sP2k~0#>jRut%(m*YuX^|mSv+)>h744X z62lHx1{R`mCr4$&oQsa8#|;@w$Q>o<$svVKu)wIER?1th7wnKha;;bmT$IewunRWy zw4AKZzWQM}WXxF_vXH9AE2%aMWssC&5LDXM;^FeRAc{;e5kS zrNji!aphdCYR7Wmu4G5a2ej-p55FNJ&z50^4UTeVK&}*}(xFB)UWJfi*cvxv5?N-D zToO)@KFrQ6NZc-7# zDF;VjiW{Y1eaw)Bj7}_&Q@YO;}23^fu2T6X+l zIAmwbFwqE+$*L7GgXpP9h9GNL3girq8#0wOLLkP{gfod0&tgI}--+jf#0qIM5FhpV zu}auX)I0>J?p>e#i-+Nmoi+DwFv)6!Nk;s3w#wSHqI>DeI4ujfR>~cP;BYoaATCeY z>992$Hewu=kJLw4J6nbs zb)#`qO2#?{9ET$A0;ER7sAG&_BK~Tpp4PH*#>*B_E}AwPx(oAoT}V9yGp=eXsx(v8 zM8x5%fFZl*VK`)G%l$`GQe>Rv?RL}5 zI~?ss*n=ft%8WT$f>AT5tgmPVe9@SR)r6XpN9Z zNI4ON&iKY?S)g1X!#q}C5=nnE8py`Qu$)Th0UM!I5zz}iEv6{JfY|2JV95UbVK`)G z%l(Jlk+@=@d?f|b4!@QTr_F*o&iw~F1vo02NxvPywRDgrI7SYIMOP^S>d(7;re?Uy za=0YhF$E0SE4uIh$$4?+&=(H9<xw} z;ocwZ{m9id{nxIK?mE8fnY)->EAubfwR`@{^Y1uI`w)+?d1M5B z*CTM*d~)s*zLjf`9p0uWNv90MZjGL~&b_GDuA)Y10W{!w)~=KY#mbKC{mOXribmsCo+l!;OyTWK{DcErviBmADwZ^m{sS*VDzRJ9U_X%WSdW(WGH zDKjM6l}INRisMBqge6Cs&0W=R7b~?G%@RRZYQGPe|x+@!a zC>{ifgGwYFWzo+kLIy zu9l1=q8(uyEv#mQgAzB|u0MWKxx!kORmoyh+0r_3V>AkQX1&i(ET$hVQbb(Anb)wc{!3|(id6eIy`|Yx5JHS|Y z$?Vj$CL^~;$7SZr{dRN*(Pd3YWKkniDS%D65q>lG^xIKEtKEo!y>Tv-sRqL(VWi#M z9sPEo*m4yF4AgCoGvcalvm@{hXDVGW{IWg0Uqe-Dq7&YJ7&-L1sOq9m>POTM9*6M7XNNh8=^ZV`e zqN1b_PEGSjCQ~BG@CfeA7yIp$mcS~ao{UF=fuzz=+9T~|^Zj-uxx&;EWRcNAIkXwi zl}6gl{9C`B5N=niDxYpu%DU77+X*A>X8TUE#0wM~$wu0CRcPP=rN)o6oB8uTzc@xT zYDJ@rQ?{K6HO<^;ySMe+MhVNwX^>MJSBWB;ttQI)NIO@bT~tYjYeF-s$)ajl#o)H- z_+Y9f;nIoiN;3YApqW-}ca z^?Y+r?YFCOnKq3PQOZbz`ibS-wo&PiGPw1MkQ!K;jpKSbX=vLm_qlz3*>apQffq<3 zI9Dmv1Zre}7f)Pc{|d$^}=SDUh3 zwKpt$U610pgtqWP##E4?JUY97&}UbVw&Q^^TT>A((CpL-_DFYU@9Vddg|^uil}rai z(N?=&9ktxqkM?2dbTSI=!`A5_638%dbkt+4_SvP1CCTQ5gl>fOL^Kf?eN4{n@3)Ib zz}zIe)Z{Uv=- zvD%>02xrkGRx)Y4l^N;A>>u}WD*_P|5KF-evXqLYBBTA7ySU#@Mq+%k78O;rMbsKf zeA_VBy7B){o_YVwp<{=R9K7!!y7Z+b^}ufrtQ|PK|LyxD`@XQRviH~Ee!nMyH~7$= z&+n1IJNsP=Pul&q-JxBd-zClea{jLQC(XTME;jp5vthvI_wl*)*kg8!Vsh~arrSIe z;g{P;};QYP>JK%c6n-an~HcQtDrxyuD5^boQ z&7z44hHw!w5roxBvK4@+7*b81S_1jlqjtyRU=|>^*Wqx%UY84k?0}{*LplumHyb!1 zMb0gp+dqOfiPs45C0ji629wTb40osLkX-X7hHnr0+hBWRO&ZK6#oVloYyIf6Ec*AUv+S|8J( z8Qamu^4dh3R1#wvG<^hZ60c3PNo6XgLDNRiCh8uBl%?$bFWW+%gn^mK_h8VZI!UPaYarPK*l1IUDW?!&jt^tuj`Tg&V5uJ1Y8+{|sAsK95> z_rNDlW&NM(7>0e%q_xP1AJ~}(?Pi0Y#P^&t%+2J{Zu07tCki|FYCvzzXZ;=doD%|c z^H&>YQI-4m%xxsFcI!B7&tIJ`kA9*7oRO=6ToYYQ0unLA6--L2jz&bKs~xdI*PSkE zRly>em|{jf*+L+y*)93>tDC2)2*{JNmE~osnT~5AD4V5Q9mYXgWy;x!#c?zq)Ljfm z324Y%B~sOrRgOTqC(#V0mz_?tT-FnIj0kc`yop1qs-u1qnJS?b6x?z-Z@J|K%j;Cv z)f{VYaW#|l_3XJC=zpiHIfWmX^ky>Rdv?C=8Sw*+x+Q9P7377hzb{W<>YxM%@B+`A zzTn1HPl6nd?lZebn-|1V!Bh>vqnq{K%W>~B92C5-6{_9(_wMemgRh4A-D9`;+t}q0 z0NiW=uK%ppWWJ#03#we`Vr}K!udUPjvMXa>o)IDYj%-u#9(}RUtwYlLQ;~UP<8N@+ zUxDxTuJ_BJ3P0#;m-&Xf?7F7Yc9)Zk_)Od-HM-U$$_)y)VX+Fr2x+Xt zcj%6tPB=0Vn61Uq5g}BrC)rj&qH++|PSK>0!+ex06BX!kM${dQ-9n}+I|6sR-mm|E zc;@)bp;sUB9sJva*`-e}HI~jh@cIK!-v7$|7w>z|zN`0sac_I?qZaR4+_UF7d!D$k zwr~W*>7%>8wu_$s*1SD`aPAdzm(9Lx76wet&F8lLYtLAiOI~(^xJX`NHe+Ej*AYZY z%n4Z4v_>Q02A0z=JvE^=j(7M5Cx_$6D!$rY7L%(*5v4?hN@r7U8*=E?jtN%0H8han z(`=3mGuA50uU+4jW=UKmrk18(KoNp@8ubS2X+xGmD>^J!}=kmiaj#6@gsX?jja)YUrhKvG>duIiQuJCdon zTcKn#AbAs6gsW$oNvf75Fh5*t76rKEEIL{=7qsGuhO54o1EAM}k&aCby4@=t@dnye z9c`}$JRtiW3Yem=Z0a%IK}6~mORh>FeiiMwFp0^9%WGMHxC#)_sS!^s5%nXy?sL>) zUbw0A@p@W_QU$coQu(SDZTRYz!MhOLDyX!3*{rK;8Gz&mNYSa0M*R7afUjT^e%_z; zCJ-dZV1Z?`>~)kKmN%Fz1VBx$T+!2Z>txD^$!lo<=LB$(so}Ohe2%o-$$(x9`b(J% ztGF=_67o1o!6M_qS|C9X=8{D%NEZlHV#+{u835L`*YMO}C%0G6sOnZZ!3T+14CEgs zngXvfRMWRy@fQ+FCk-{0(*{@pB7EG;sU*KfcclSc4^1u2z+UlYC}PJ_t(@lTtcIdx zF9Mh4wo;;)Y|ihB6l}^>rP%`QN0ny>F;e^*)s+Tx{o1Le87R*VqN{FQBYS-&7yszg5gAkY zde=~&hk~xghA-VVyGHaxNG?A8+=~Fay<#Y-T0=rQ96$(sAjCE2TnG?aH8$8uT*H72 z*DfU&ubx_lF@}613T{%E(+z7efU^p4u9_NWi}{}5KPj*&zlH+-PQc$kHUA-_-9VTa z%mk(@I+!ls)DR~(Rp*FCoH*ffPKt42*CJgRKsUWp%P=s|6CIos9d+wkxYs{&(R1ps zl&Q=T9_rr~vz!oB1%@~@7-ILi7XfzpNiI=}Zc}V%PqtD+B1}>eMofn&qUREzrTI4?1||sW^l{=|^S(WQJah4kIWwD?`ShN*fL(y= z4*uZaD-Zs`(z}-Az097u#h)xZYw^Xqf4RH0=$bKi{diYx{yX!f1OGnvwYeK-zjUCm z?@RkKdq2Iy7yoB0TzhSDaqfuQp9cja^IKD5fc?AON8G;N4_k9qdOuup%CwsUi?_`! zbh82Gj=0(@3Z!Iwgzl8vG$h!LB2|=2np7(aP*f}>X-X#f>Iy^YM||0)tp|OnV$j#- zooTC+F9ut2xQLYPc)lHWt3?Z^9WGHPeV4>oJFT=MXuRB#h=!oVM`N!Kd~s6-D+7CK zW+dOJ*=fcUl7@l9*`VR97Ese0GgjC{Gs?-nNT7Tq?l72@eACQk>yS#%uB%s_V3n&*9ao)KLPFjoIhRAiO{HR4l5IIRFIkplNtR{F zk|p`(ree#oEWd5pmhEtIJLw)KlR#4B)X)rJbr0(<251NnXol`-R%X#*b$7x5O~Yhh znrV7LchWqQNn8Rl6<=mBL-Qu+RMuTeltTQ2eY}izV?X>i6O`9^*I@k)k+Wj$zTJsQ#88Bppe8g+u#>2lONPMN zRaG(%Fp5k_Q3AhRx+uME=T#>%5Tkm8SIVJSm`f$fX(wVO;7BHpc0=P4UV{*|ogc@< zgGv&=1DoYCJy!68N{CV$8P)1B`KnefG?bCqmDBke6h90$`wbOu-OdPkPRdCJp>?>t zQ%eqJ(`2?!=^-}EuvRA})B4cRaT|e!da{y{wA)Rj~O!ZE0|v8Ayzga`;hbT#-?&lg_kmlOwVq<;gl zxX#KJqTNmxTM2+g*JhHE(U)1GIy*Oa{>~{GXv5**R=?#mx^lR})kkcu#Hy)abSze6 z2-A6sHu+MKA@2-~VKbt%8ks@RNf!ns-b#c!+&CS^buwNv;C8yoW{eQa9b&hgX4lG$ z)f&w;N>aMTu(O#J8=pRr0hC$}QWR9I4Vy?JQ5%JbRv}h~v-%)XtVEOHD%_L+P>@~K z?^Gi8Ff1ttI*~P0L8-@h#SRIBv~A}I?H~gcN-~63VCFzGZ|fK3BW|)MG9W`FHq>&C zRMAsqf-RNvCMrb?F5MlENRWeQ6qSQaquEojJB${B*v{D+WmozH?8&#eIPA5QU^|t9Fj)N zp=+gBD48_Drdro(RmZk6R!!qeX@{dDVd##SGFoCw4EAK6t>ofx)er`?d>!L4yN-~2 z+&~DPL9INRwr>|K-gcZyE8nSB^y;`cKK&AQPjIjQ*5M8UvXsE<`}DEgz;5c7e9dihZSO^niSE8ZIm zKT@JY5lxzHCy2mB?9QQ{R|ytjs%;@MjPk`6H71czyPxb-o58v{YDaSR0n)Y$ntj`j z>CPzAQdzT^rE*d=Jqz`6BYYwQMK|pv)}#8Gz?W)linQ%SRO>fMh>X$6O7x(q8$>2% zVfq~_8>-UbQYglvPM#lwXvu;@QZY3Wp%ip*s1(qGEah7LeAc|3))}oq7tN|&U>SZK z(`S?4uK(v{PT}$7$I8hn`b3&t2F5^l^ zsHNl(g3wSLQ_~0Ik%&oSO|b`I3=TUj^Nud`)G`HDMx93186?NKda2n8r8-oyAtl`? z*G6Gz$1LW+)^Bt5aib?|**tH>MtHu&&Soe2KKMXs=iKI<(CI^oRh9Pp%NSl}6n^i) zD4dt@QH;XxKHz^&y6GVd5&|zoii)`@GxexT~!F_;Rqeq`&b~bzE zd5a)llDz`DH8<&T#zTebImAKgQ7GNb;M9I0-LoBlj~c*AHGve=Hdd$#G9MF2x9s4& z%(=;>syn;C@;>T`N#plxuTI0UfhZ-*Zr2N=X|VYdZix5+39~gcnC^V$|5)>zI4d9Q*rV{7D;QNLIAJx$q~h80}ZxDx04Rh z)2Z?hSQW$d_!`2c>LsnJbmc%>NOzm*Fw;k1DBr@WEjSnU1)~JwoPyD}77pbhO@Ti>Aux4$KBXhXfPntC@C-#yT9T8ayHgdcD4SWSO@IP(b zlPAS)pFOqd11L6gab+&GYCB!*HZ<`dpk{L8u+;@F!D^boRRF-Z<;89v?*cEq3tWWS z14iOBA_AHz@U0#evpGH;$qE>3WqQ?EQ7%LQ3Xc`(XF@|<7_=}60!dBt*I0AQ-F}7A zgYQ?iOW~g0H?C9p7G5+l+-5FYgg%TQAwWKN8IQSh8i0v2J#%%OxfYWKyFpXd8QcPMXb;^nn^xabShdcR$390 z!0kG~BzX3oH1R=qPx+ulcKgsi2wmudB3}<%$roau2gY(Pme-6ADvEX|fRi%78BY~j zQKyqh5Cx-(3L&{uYdP&M+zU!3CME`7l|Jaap8?Z)fn$QbZNmL*To<$F{1$HhPHfS< zco-KH!b0xx*<2SSmLa*;2!blXXdv9OqZPS@)l+txsP$oxK8bUs{dlKPw{?=f^iC`~ zuILupYlGS(nih~9C%GT0qXRObRH%Xwrn1sN>R@sZri%4?Q3?v(cr?M2DLd4xQ~6+) z7l&enQI*O--S6Uhsh$?u_-e_wZr42*l%4B>=9=w8yCCniWcTJQgTjwpP^AEHo8A56 z`d|f1j+&Mn`JP?|S*U{bg17bMo(q~gFCNMTL6|GTToJk#gD^ ziULP9thMz>HI1jqk)mf}ax2U-5fsFiV~`l@Ht`i*5a{~-{eN%c+PCferLXrD*!n$g z;_F@mVE6y@`Q88JQIqFf`JUSSW1btD_u50gE-rCih?Vyu9O;>r_jr&qs)U|(K$eag zQrKFGPxR`1HkpJmi->U)t+gt-w!tRkug2Hj`Bxiwl`L3!N6)OhgY$1s<}bY$O^k&0mR(*9yf9V>uUHe#QrxcBm3?$+~BJN$S#p_ zxCzYbt8eMOjt|1l`XKlCZ`JMpqKWY^J_rj(T(GjaK1d^bX0K;K1;mNyTqA5yP^NB; zTF?+{n#FumHEU72U1ihu1ds7D*jq?Wj3Q=|kaz}^V3Okd4WS^G0It-~2{f2^HEDK? zVy&CKzFwV;%JT)5igM;q49!tO#-eh?uz8X}a+jY+eX>)zh zT)TZ(9~ANK4(Ixy*bS{;>tRtjrGTY0H69=+Wtpe=WyOXo!|vM1QO9n;GD+PJ4^Bd|IJOfKVtT4K1_Vr?jI+F_^!DwJmA&_HQ6 zD#1!eG#H-ELpB%8=~_Af=Y~|HRO}?nve9F5Xc%s(4Wn1~ZT&+tHS+Rl{WhP$lGE_MNqLhb$ii*}TSXdAP-k99Gy94|}gjm|L;y!{8+9@G|c`C@MN(jc61RHJ6Tz%R*AuvQ8RcKa_1HrV$dAgi|Fp9W0l^ z?Ksx2cSxg13yqv>3pOg`jg+o-O?4mPYa-F!2SJVd@zsJ5-L89UZ)L6znrpWY?Ss6v z&|Dt`J-&K-e)j*1YNE~fAVlCSX-qM2tXMW}ykd3A{ixCvDrqR0!2k-+fKnky7p=h8 z{Qf_5)(5#!(5vqMfe)HHF&@eX!67#SVebCFY&A?NJQ{J8YztQde7x^8c#}wk(jB^t zWrA?GVOO1Ep%0OlUVD!NjS8*S0$E80iONijuM2oWP(aLe7^#;kRHtS$iY5ZoZ)|^D zFOAh;(lJ#^HpUuXElFAw$BXe11u-D|oOYr)4&u|R`XJDC&j;ns`=CX3`_Mip><-pE z9|T7q`=IB|2hELWo$*0En#c4?vyF!;?L3nhB}}8!wbP@n(5t4~B84Ly1_y@)DhaIp zYx@imJ?n#lYxtl=6XRigP&k6QS21&a5I4|Ja5l}?D`<9D4UwdzU?fUegE2ru%k{;8 z&C4BSKhlLRe+Ee<>jN!1=txXG%eD(c&;1%~q zw>CEU17Txh*S~q?$}|4G2UwMh>d{XDA}8f5tXA`47}>qe#&`Zd*MHgle9_-skOdMs zjVReDjH1S5HP0;5Ub9O>A{%0H&srx!J)N z%XQV2$RGDUlZ)g!jj!SYV1beByKy-`>2kd@sA# znXx|KiF}J-ve&depDWW}eBAx~OaID}z4TuD*2eC1UhKNw_e8Ko7~509CfL6htIUeQYt9}fazr@ijsOAqf?lzweI@2&;Q|}`}s%GBDqc@ z1bJ_Sm)>i-BRSWfahO7Ixy_ z5CF9l$(E&|IN$G1k{d6^e0KfY=l_%Y4*mS^KE1MJPu#1!lFjvR3n9Iy8k^(aF4Ujv z-#-5zn(pTp!YfPps(Uq8%DMh+Au#w<%2WRBLg_sJ_S-jH>3;j}%F?~&Ue%RuzJFT; zDn6C&jDNdOKF`1XmgUO#Tfeiid@s9a%~;Q2*w;z0ZHR zhG@o&Xtz#8TNLlIuGVh3LjCK1=Y4)*4be(7qTM_r+T8e%bw#`B3iZqG+U@hdylC|? zZ$R}K(QcfGw&Nu z(XO3{w&+}AUD2+&LVfmw-sgu`70t!}+uDBb#2NsE0xn}I6;D&aPM4xaVp^R*XBtKf*S+zmJ_s^(;*QTt~|ZVp^NZx zBC~r%m+BE=+eExPou!yl^KUFy0p*!1_}p-dg~s_1*9=H7ge}Z;cJCGXI!&eYOekxi z?N*nqVTcuO6mw>ubucmyQV%n+N-cN!OlK>d8M8K7r^`cCjEvH4iWs-|_v!K=Me~lz zbVxL&b#evUK)T&>bZjAYtr00nrrB>~6huSIQ7IgZbR$*(fp{27)UXn&XcTp6qs z%mI%_N}&33EEP$JQgdW-@e-Uj&B!QDcgA6&5F`|{JGoE1Ob8Mocg4JrO#(o9N=&0w z1FlqupqR&gI$UAnTm=-9-Iwd4U9}Y!4mp)vi{balfLV=^m2SD5QTt??Wyo-V!E>2# zvWE&295z?!%TtgLx9i~gfAikEz3cy@&67#7aKHYr*Z-JjMRQ-4kD+j%rxc_mnR}9g zzYcuvqLZwLa6)i2ge<)OAC-$SDT!klfo)WpIix%eMc8b2-x^km)v*i>DXYmj6{6Sx z(W$O41zCrMUcYO&FO~P50b_(9?@uRSjC=`ixvty-rR0=S(ja>=qQi!X5uy(|X0-uI ztU0|ti126Cf|3cBD`|>k>SbNbgaY&+(-;jq?OrIBYB6G~7Aw{T6gTYj7}MllVMPcM zx9fNB?tzv5+kO3wZQs@}Y<+x7+j_n4i@qPb^)GLI^p>5`{Le@?e+KGwzswWx!v#DRd-+K{{n~` z==q=W`>uZW>d{r@>T7qtxbtH>{hhb%Y+d=>mG8QuUU}X27q&mS-P?Z3*Y~{*#1njO z^Sd_H&D$Hlzwwh7Z#_0YbMbc`f8(oI1K9R6w|(X56QY@WkIF0Z@O9@XGL~{w% zR4UI<8l4n&Tr4TE8V*eS z4ND1U4K5weDilQuyz2XwrG&EvmjsroF)30=`|d0yoHe)@$14OQtBT`EG3*ZxKuo+=2amnalYG231y{GES`H2TluW19WWx8_rG&GVqb75FRu$!R%J)r631=-wh{dR+nkCtU?=?#aXDx@z z(gY*qW0dN9^-{uF%VBT|X9YJE*7vHVgtL|-rgC{aFC=-+_sXS&vz8;ugsg}hE~>s) zEG3+^98HKzd6pznl<(zB31=;b%M!5+L9-<3d)YEqHLFyLp@}S?&t|#Yvi3r=c9YMs zaVeh8q_VzmTuL}=H!OHjCDUmoj{9D^lyKH=BuNLY}c`79kvAl0*DdFr~WbjOu0dEbN@m*a? zI6D}klH(F-A)Xg}SN1kPw?S-{x3B)%b#dnhx4z@rhj#|o)SdXwORiqqd-IjwzVfMS zZ@BX8-ap?gU*WF2X7j&q|K9dL+5Yxz$@gC0pKpEo=8f$quD<>1_xpGJRbRmO^Sj@@ z^~YCRJHN5}uXZ>5zqEI>`8|7w*M8Rb@vZ#Ux88iGKkEO!U3mA)dw+KQ^*4U_+8^Hf zrCZ;3t9mPX>*~$_`{r{uJ2#w%-?MIh&&GE@JeNn!A-HAu?zGz!sz9-F5AMaODhW55 zc3-GxlGy_~rCJ#*TaDL?YLl+3^yZ&<6n5MQvvh*N@-|f|9(G3EtlkML>ESRk#A-2| zfg|lhWeCS4VqMcF2+gI%~zLSmgKZWqpxLJ&Ox{`0J|7XiOa2x&rEtQmxyA$Ox4kzk*FL-4Wk}O z4eddi?iLR6Ek`KYSgVRQGBMlt&pe7Q%@NtdF>-)p3eB7*^));jErohqBda&^RXjrG zyJ4*8XhLn{cPCP%2VL&a#?d0@glLJP&;f#^>oqYPf*onxXcBr69Q%QKV%u%^#WcBa^{3gQGfXy5<}zRvu1;B2d=W$Wa#JDltyvQn`#o_9Euy|LRe6 zQ{_lJ*29t_91m)Ip*F5ID3x|ZtyAZsXxizGM|FG9XogWAHEE+p9yS}bu+p_sv3#zg zM0ALbWb9N?Q;Xq>h8g)m9j%Kk+}ZfI6AD%o8eEKxRXL7sS{1sV(B+(t$8#Oa={nWA z8LL;K+PF$X8{apfAkA`GsOnudk?-QRq!-O-)da=nxq?ZU+bbV}GEE?{ljk#BAt5@OtqBDl(=7uk)@uaUm8p6y z7ZYO#rcqOZ!a+4xzzZER0vCvSm)`ho&)C~e54E#w3k^!~prcj>P*`b#BUt?4kgBG_ zB{-;O^QpRnxouGSEM-3wD@7JQI*Nf$V4ctT}b94?NPr52buA} zj9ST@I;42CU(WXH;jzMrU1Mk@QkXp<2p=S|ic+r^a$+Y`!kj@q9S^CdnCRCUDXSUn zMY0@i<%UfT^$1#}gJP*wYGQCz&O%0rFrr~A2lum<)-N|xT&q`%r2uSEb%4Fk>qP5t zRD)yDR)UjQyq+Bdhg|5O7mAdLn2_srlwk)_4-ZFpnMXH%e$s>-4->S5Hao*|J;g{Z zGNTkDQVWanSyI9d;dMh~D&My?rTtRKr% zjr=HExv~(VGP$VI2{MAG%q0mm$ESWO~B{m78odYRPQ7x+x7L0JY zASF^f5#x2>-hQLV`X=H3BsQG?oA{Nr&lr$fTLl7H6 zQY9$Y?ZG9kJ5t5YLDB>zIciM?x4UMU-ZcH*5Zc|Oj5f+izS->Z3Y_YM?HbwXYB-Hm zl7(1MDWy2rh#uzZ*w&;C9Lc-BM?|Gj3oQfZ3ZZ${ra-E!oe1+`{ z`O%PuxDbjA$7MWPj-~Y)U##Opni%4nKQNK1D;1O;((1Lt>EU7Iz!8y9zEIL=2<%YSlKs z^ZMVr)SJ1>!uets4;LwxwTq^0r*#uobM;0$60AfDc;8HER$u8Snu71%q#y0}C{b^4 zsVb)=iyAqUc>Tc4CNR1d1qIjhd8x)Rm1e7-L^qzB^aI0rIG(oKBeiu1kGPu9L~NwP zRx}e}a^M6PX8XvfG6pK#{G3P84hHjaiXSxE1vqvXSCROEu2ramT524sC0JufVQu^% zcIdRtb+@y0YH7eo+ zpJ)zf-|?i4OjZNs&+I%29S&=~=y;ev7}8Oj1}{o1*E(pb;9wLvEb2YfCwUaDT)57t z!PJ3pXd$Rhw)WNF3S>Iula9#O6sSz;J7CiMgt_32RF*_fgOpd z#WIcstAh#@qDPI4S~;i|hW6IsWFT@<2=38BM1`6lH@)Hj&(^B&Jxqt3MAwD~-BD65 zh06Mn-pT+aU1~$19q@D(!2T#UC5PFjTnQz{T(Z~6$D-(|Rr zCQLM&RjDB2Vydjg4OCB|8d(+b&E}-%!;w`PRpopnh8Gi7UNY!X2ZJivfpcIc%~Sy_ znY#_Ok(4ptPfRErz8owXW-}>r!wg*xC5MsZSdgI9Avp$%4OU|g4muJW(OSN5n@}_d z{D6XuRw*6rSfQRWG&*TK%JPj!CdEgoPNg$|`>ouNk$pMOv-N7xkk-nZ!wSaa!VXcY z6C5V#$->BV5@X4LQwmj6`!eI`Hz!S4m4Ol&$HbHw>4M#jm8%y*Bwi2{GbhTmf-%s= z0?`nK6t?lb6AJwh5yy0@Hdf{F5b7Vc8WF4uALP>66n>~4)Z&S*Zq+bu$Zh`MgrZoB znY9qyI!r|A%AtlL5`YXC2a_C*!v$@ej&>n29Vv%{r0;+8;{P{)Z)5LQcFN$N$3K5- zHSnR~y@DHpdGwN#XtRYmlaDL}6JTwb)N?vSifj>s zm?1g+DrcIaqYV|j%{Ch;J!!((D4d1IlvZ!m@+lO69D~5d(@iW1UOe5zdcodjn^+Xk zKGO>>Pw4G7K8TeRKBJ;kHKtniifKDx@RE$zTXu`-wCOGncR_jCi{i-<7!BEtq?Dhz zg`R1%f)@}YYgg4|mdx{fEailigsSO$x|ZpWQ)I9nclMz~rwsu#_j$o(=ksRWfU6fE zEOw3{Ac6{n?g8kIGpI82fvr~zbp>h->sroAomcB+(e@AoV66b1%|)3Zl`$0(YCT7( z55VS+%&G%JiHGbS2qv&paJv_GN^-BC?kJZ>nKeQonk3LMmNW${StVfce5`ZU_slimN9KEw<+xVQPe#U9z%iU{;(76)Zfy9HT$bmp&6zO#lWh1G z$mcu|zzxm`9b5fEFx5L7SMs5of{N3GDl94f#{S5NiA z0?h2&Fs2U$z9rNu2B9>2Zm9zx!({?mB?oPn!sIZPZj|_5qHA;#BrS_%tyW871GNCE zT{U$J9Nm=mL#Duu)o`)2g~92-7k3GEHn)8Fyua+=6;{)x5w_}+1%w2Xz=74 zdZF0@htGxjVWDmnT;Wgio@Cm2e{^M>$TK74#F3o{?$H+uZdQ`_Yt7PY_kW)NsNMgc z)f`wow>zE!?z_wRu*WO9%SDs;VcaE*L|vKYuFP5$k_|9vWth`wR;<*9jc$>O7wp(T z?-#(urx>ge^#)mQG%t^XTn<|_-42M3!Y0(N?(Eyhs3^7h=!gygcWIIX)h&rU4w0R5 zN-r3!m^-&ntGl}YZ~R@q|LP~MB0Hbi$z1u&mBa0S zvt8NV*gD#JgYSRwUElnn%~t~^Ux^>{==D2>fZW))lPcmQp6Md}XsQ>8Q)7o>)Ns%m zVHDqhxUy6%j19<6*NG5xJKh;48n&9z>n7D{gM!whndSYQ3HO2Bqt}6^-uTSM9RzNq zsbZ?T&yz6(#RCka=SCW@wG|XHJN;%gNOlK$NX=K#m7AL6=`L_4*ldVU;pnxlETG%% zF&`+fY>pb0!cHQ=*J^nj#N#4SoDT%!i85Io;W0xVbp!UwvP{w)L5II-+F_{IgtH|q zUBl82Oya4ufJV!OP`-*J%TBPXj3lD0B!B#e3VY9Nbi zbgBg|$VTyDs~IiOx(sevqiQz@fb45bF|B1nv|_>`ydgA;*vhh)eE?Q?SkQHVX5Tn^ z6=><5prwY~Yo?u1hD;FxH)uz!eWqS-YGxy&H+Ut`0>`cWs#*?=nzfZ%I{(fvj$R4c z3xM_nx;kj~%TQz#D0hlTt_`Xzg?TL!9~Tozm@hUWCO|=k8yu;v+}?xbvuz%|VyeM( ze7}aOTA)*QyR*6lz6! zpm=pmie!~qTpElSl+CghZ4e=@&%i{n!(_|E>fL@YZNG8a;S|{!MhtyWHOWG8zb2;% zrkxvfIzcU0vCu{$Tg{CMA&0{-?DhyBt9m+>)*996r-VSC++F#~60K1r{}xmfd@+ zn*`A~dI@OKoiY)8)UcDI03Ocg%^==4FtcydI+RMZs*csDh;b=7MwyDFk}EekZ_0cF zXwRK8x{X0iIs$iWoo~q{DqTnx26esBrP^4_>FH>Ubqe)tXuscDxxJGqQ$2byXeSEV z!LhpVXKW~Ai-1*tam$0CrFk*_$ zbV_<%N_VjRDoDH*kZiEMFZaMcE~fK+wa%^F)bjZ`8cND* z-%8}QUbs8zG~-#8>u?>zra~p9Vg?f`94KqUN^Rx#PVCb>x(3>DO$I9K1+CP|aeTc> zQlM4{%>_XmOAe2c38u%&b;(S#mEp*Vkt??|V={a0FaeWc`JxENm|ho@>}#oHy^}45 zs4CiT?-OK4gp&g%n#na4Io_mKZfd#7%tp@^j&@yHK(`}eiBvTu+e#skSUiy#1x-as zW~xjqpWreCT(1fPJea0q4Pj+jCMI+D$iKsR{SAUpJ_v;)=zJXJixE>FpoqCh%?nnW z=fJ9;J-WIhoNXWAEziX@ex4-Ik9L3r?xcC3fkpVmD@$;Gsyuyk1+?N$l?SaX!XI9_ zm6Hkb%+WUB?oN>V+!x{pugv}Qw0QPtYi5*?Mfkrf^Itv}W`pIGBcCh5=^icu`dwLq z`!is3W(F)k>b>{~Z>1S!)s&Z@v2FFWvmWP4vbWZ+!0!?#9OTpT2Hg|E6pI;#%w4EB1bFue$e=-Jjhx zc5nKB#{XXb?$w{X`t;T9oloxMuKdlFAG#uJe|h`4?bOzP+4=`tl<$whsX%P=_cxC> z-@fq$z<$*q=~&tJ3C7;5FW$P|p#~gPwM#}67h7ZuW{IezNOVvfmu$S;Zeg)HUhgyz zIX`x!5dTd{|0^2pt zQKVzRWBZLOuwCOwL^{rRY+riu-trDtXqBUITd1>kuEA5rh^ne3vT4H=DZnD*AV^{8 zkj0{acYr1E9^03!z;=zZ73nzbvHgY>*sgI(A{}!c+ZV6Ewmjl>98v=yURP*`Q%Vk$ zKrw@k1j4%el~B zMs$%)2ZU-YOw@5a->KlFp(om^8rCQajUY~Tq>{iV5FXpBE3jSTutYkJd2Dx9V7tbF zh;)p5Y_F`qc8zln>G)k9+wB$Du5rvE9lz6KyR`z_H4ZeS<9B#$eJikCi!5)39b}O4W!~e%+_lpS5qKP(&Jhku_B3@ zbo`XdcK2^qVB2rjB36gA=whd@1|rpV4z@CRGQc^w$d{q9J_yu|bks>{vUL1bkL_Qt zz;=y;66rYVvHhzR*sgINA{}EM+yA}-+cl0qq~nOk_P?#bc8#MB={W4M{qhQI*EqGH zk3)bhzx!V=-n>27;w~z)MIWOcufJS}*MhQG$zufY;&%UHEnd;21A@rUoT9-|<`@R7 zgxx=1ht;CmQ_OMDWAiW`DL0vqfhl%yGbD_NVJGTXYn{9KXe5_Mg{a zw(#VGIX>{1{mDAa79Ddi$8YwS{qZ`?7M)lmkM{vHZugHa-rzs5{so5=%<-E%x&Ck+ zR*NbCF~?7O%)Yb^vqgs&?*8Am{lN|Y&w-D}KaVx=SObqW@DZ-6fWnXSQXbSiex!LRi{3OKOz(8MjhIU9caz!DkRsY_G2d#{tSZIFtnhxL zF1H&1Zrd_AX~{m|6$?TT4h9GX zu9_@jnyCzD???I=JL2{wSgh|4^d>Tnki`JbCTKCBbhSVigt0}dK~c9vYcR|xgz&^& zFKo8yMxcY_9v4ozU>b)#H-`1WU7FkVyLUhO)V-qXK0a~UeJrxUN9R6*Uk~o%c}X!l zo4xY9MUXGaURjdjb=pIP_Oms*Cd0~rXbehXv899^8323(Ou1&SM)c`cgi9s6wj$ohtT`!EL!RAl6A>s!x6pVt%ariKZ1wpvM z0UGg&1uU(_;=-nTI`*QsK^tBfjWeO#i=qu??^<0A&96JLXdXX|$3!F1g&8tKs@l$% zIe3JDDks!_T!i_dJ*u_zpn%0VJ)0TYtN_c7kq0gcNPU?MnE;OOJ5DLsk7%JW9!VvW zD8k3HSi46>GUb*dD?9~{%BGMi61`Z^8Y&K_q|&J&TPlSUMJ-1+HENWJl<-C+9v zk8k3W2=vo&ad6agn=i3!slWNS{5 z3a2=<8ngm+6W}-`Q)RB~q>)%TpD&74QHkB{bYW3_p?xJJ@t`ufZS|6lZdWb0P#)|+qLy7@;pf9>X{Zhquu>gJ6bfA0IC z8^3Ym(>FeGw?63mh_CJAd^b1$V)LJDeq!_QY(BI3+Ku1Z`@xO9|Iv5n^o|7x{?e2GZ~o}X z?~i}`YSzGOuK17}K7VCSDu&;A#}(fjpYi!`uM*4?o()R&L*H`6_vDj4e|}0nH;&kE zJ#oeNP9TA{Fj~f6dDRtP0DMKAe&vDZvHj-juK1$hkurTW6JYBvPy3gfK0h7F&t+ivi&Jv(a^!_+hlMHm=`;ub zd#Ae2Odp<)O!6O2bBFVEjLb&t``|--#Pys7=BF0OOv&e4pf=SCJ^j`kt*BG2B&T0Fr4?mrfz*uEezjI1H9p?0u>pNEBdKQowj{3PN`P(l?o|=-s?Q-PoRNGHY$>(UBnQHs3 z)32P;Ha*pLbo%Iwwv$uIu$3g6jT7}#Q*B444&WJ*3q+p(#(;ptaSX&awvJ2-uGM%(Y2N(QYY+3bST|IVql15@&;w%;-3`j(Zr zp3Oi6w*B^Le-EbQbNc(XX@B25{mQBSJ~i#{{`AqA{=W5AdSfbcdHQ@RbAKvxY5M1- zvm85XcR*%)O0HjyygL=TI3+*x3hPYuQJ6lQqmTAfAMc%h<&-{JQ`U#mM`!fWoQnOP z>BHr{K07ZU$lsXi@|h|5RG0NB*LSbP^{iC^UDl@LPhXC_IwgP8D&*%%WcZ`gK0SH1 zPuP^}x30wX?8_eXDKaI0Ay59gQ{GBvMno_^(& zdBIcG_jl80%quw6jq;d1qyTX4K_ zs`QuNZ+o&rdc%~fxe8b8?1BSK^}&?fSc&|cf9!eqe_KDcvG>`lKMnqQ{PS1?k2UaB ztAP(~-7CA7T_1h&)Lr?aV5o~@pl0sMgWfqSa7&HDR<~qyt)>YMRN)Z1yg)FrD1Zta zYo5MODG5;7P63GKc+m*glEHX5V>gHSSXMF30n!+C`D~JFjGNqO1@yz`t<9NJ=!uhe z4`@##9@g+7TcCxCD)p#@l9kQwnXp1o?$uT0g{RMF-SLF#^ttw=@%wi;?t5(YlVZgE z>e3q=7<}Q9H#qYIpMQh1$T(f;E{k!7g(3^#6xp(&4+n6pm+uu>M#rQv!=uscpva8J zs9Y}$y3JxdHB^eEed!yVYJ6myN=_RKg;JO4L3MLq;ZQNd57MHQ?c=DrO*fV1+h0cR%BWPRxl`3o?Kye0<#yvu62xK&ShSDT(ht*-n3 z1&6GWb2jc?_}vfta9!mEbI0xO{5i2`q#kBYM8fF8Tg^<7=BuWt?03na*clLLh|7l+ zI$Uy$23idD^>#1AMF^@|zC7+R5k&)Enh(tD4~YM6;8q+Dxqj%6hOW zQydB!1qaDSJDg~ac(G#C;x&E0o(yZe-V1?}gn2nX99OGY3qe+l197_!aQ~2drK_Bv z-@A5dPJ}V3o)_#h&KZub5(U!g_v)768c4ZNw+%3sc5lv`5jj64PD2kbfh;@yX8bBg zH(qiJf4UdzM|3TI*VT=}z&Hcy>w5h8(`o)7yOU`ytXn}SWc%r`)=E}+QY-^Y3>B*Y z=R#mPJ?RS+* z$|$9s3`J2=uVZuyjWhFO)y2!%Q>TeOR=OXbM>zAsI9YXFCc+|Pbn!%si33`z$%F%X zhUQv9INJ+SBb~N{MnBz5RqBOWxmuMQupW>caV(D_36`rUcrFXqN70s%8wJ@wuH5Su zk$A8d2XMzgk1Iu3@D`{6rR5;FoHQKG&f-d?o3_-(xQYzov=~9d3ZpeiGuRu6(MV2G z>+Ld8a@u3u*zYFPVSsWmpowCZa!5UpPMJ0;ECTfd4k`$_D70Mzg!KRfvBUf0ehOrL z;RK%G|Gj17Cnov-ue|wx-F)^YeB(cY{D0#5A6);RufOBk7q1;%i|+mQ-beP(-T!a* z+jj&0U-y5&|E8$&8vXv<^zjZW2wj#KraLocd*gmJ5Nm8MGgwN{JC?q+u)E%J8+ZNTVVV z#d=+u4`Boy%|}^IIx2c>iz~1#)|+)>0F?&&xvq>=jVRyOsXiWW2jxn)$QCQ(jGpOd zNKiUZIx2W<3oEdtLD{EDWFRz3>1NtQBiI;2LLh#r+*WwefyQ!_A1NaS3uiIu=)E4> z_pZQJ71I0wpR@76Z~g$LxnVShn7V|Kl0cMFdB_5m zkoVZ;S75si49Ah?vDH>!yABM;k?OHkS75si49Ag&MdDXhV7m?s$B~Cc;?J$Xb{!aw zBM*zjFR#FM9T<)y4~xVvt-y927>*+ki^QK@0e4^>yo@6ci^MOkz;+$Hj3W<=#4oJC zb{$}gBM*zjpIL$JI=~i39u|q8UxDp9z!pay7KuN-0^4i}CEc~~TVc7;s-br2|yJS-AFvjW?7pdyYuEE4|;sF3SmXG%vN7KuN(g0$;E zMI3opBz|fIw(CGe9C=tI{=^Dw*MW*S@~}w!DpIL9f*c&VxFUi-x(2hwAO}Y-nuy@X)?v2j>V!FRF+>D^ zWF2OU&Ya!*|JxfsuyO0&tvB5Kr#DkKe(Of}#ub47zklu1*MzYt1 z=+&RP3h#Vs2fFg(SKhMy$?Z39{m9mvd_U~_*3BQ>d;?(d{QbQDIeyzPgmv(iA1$8m z`=3j%Ex;P@l8b%)`=8@H0oHi$t@GAdMS#@W0<7_VT15bMZ2=zf5HOwqYcPG(LqM-B zz#|@lq$j`{$XAbg8d9DBYphTnEnxKfpG&MQz#6aoix&{@e~w&RfHhV~s|XPH1XyD= z_o#<}SX+QcJOr_|1$e|mfO`V0!SqoN!MoNL;1LhOJ3Rr`SfoE%u=dJx@7U%9;i7f- z$@4|4%!`+BmFM2RR#OY3u~up7ZR<4^g+p*rF)Q-S7UANis?R;OEu2>1q7A|W6+Q&C zdG)!suGtuR`SGeg7hSWlhZwQybJ)6#JL{5M7O7ongBcZT0hTf=do^S)Fc)dOX)i)P@j`ppD1g(5F*Kvz>Vr)7cr`vg&iRyf6pu-is%9`U0(MLcu@H1}Q)?yT+g-eu~ zHeq61u$cr(h6kKeK>|t=E=TDCGwy^NWQ8)dKs1*tHj?8CIc;+$7VCV56=>m1K-pI) z8h(x-Abi)Z6hP(KGogYmAlhYva~IACx6|`O0Ve@-lV@{70ns1?FAP{8LyZBE1!b^d zrxV2;I6~r`Msg@(`(m|jqzifxby_JjJfbciu%4HYV3O&TL{kZ=WQVXgBbzjcNSlK( zbqMcsT`r?){U9yzd9$9kAkI{gQMO}OvbUJIE0qoy1 zU03t*(TS^BXs?gX)r9|cx|$38!1K25nehXs^RQnaPtXTJ{x1CJ`&LgFaznErcOrST zhb5z2=+}FI2L%51ASK{^_fzzzkc=Vs=`@@BOus#LC(q_Chd_fT-&<3M=St@4P2dz)8?om-&5n`5+#CEhrtxvV+6fO!nVc3MJH=jsOD7A8T2`8v z#x_z$5x)G&3@yfTol>lYa3l_sIhAOXHIN4zK=leK5=ty%id_}6hOIi&M0yDf4T37X zQj$i(8AOxSXi@ETiBz6)s!bsQ^-Y>+#N#WuORwt_{J(vlv9a~#tsmY}x32pBcb{?V z$8Wvo){UD#d$V@)RX09+V|e3@*FS&#f4Gib`<-hazlQJq@!mh&OYi>G?vL(1y}RfC zrvUlyL$TH5$llKO^`S5#&LcI`a((+Wgp3hG#)uAZP`K zA6?4uEYb;JsAwZgz;gquH?&iEHb|>oJH}t&C*GY<;FEJ-enwX0+_m zZJ9+OH!I;!E@e2IK)eFOPc3CQn>cq{X-kz}n#@(Ya#1c3C61Yu@F$isJZmcgg>^7A#&C^00}Q<_{pUV&)H#_s>x)t)Sq41aJb!?QC5NJv%^a^?SL?>oR9 zx2pW#n^)&e3q7GGVF)2F&QrCdP=aj9wp=93mL*JuJz@h$zCFtaEn8^-OlE1DPp-BT*)Gc*BC?Mw=yf zKyPUBtF~phAs+6`@Ri##Tsv$6U)J3CfFn)Wu_Gn2HFT8z`X;|(8-@t6)|Uf@zS!6-X3eCf6fH~4ZhSztPKS>OsuKE?#a zFN^Cnyz1WifB$E8+4nZ&-X8cX?17;EpEW)kKB<5gvQ>rMyX?RQ1-3u1*(TY1GB(fN zPB_;d-2Sy5KW~2~G&0?$^^AkW{>eQ@WcmcOA}32chmv$3f!r*r=7dhKlBS2KSSzKw zgCf}|w(KqLrEFIJ?|1hd#L}n<)FSZ=e-b)Zh3&AYj#SY$e|XqxE2vpmV0eZ zSkAi~D(cl#zMXIqIEoINj#N11^IoA}%E~Ok%E?r`6VXy_GphRCS^~E0lr61vip8u< ztfRF6Tga&CBg%>;Vk-iiw5b6P8fXPe8TJBOpFxbD>Y1d7?o@N54& zG3rj%|3gMGX8|LlZJ!#4L@OF)^d@uU%Cf_<{x7S?2Mf*je7 zQ^}=g?+(Wn-2un;|Hg6#CKA0M3AU~m*UkFKz=KQ&lo-mViwY3ey(W}1leP&sKu(rD zz-2lWL~SN}cU1j7$+t%_@Q%Aq;Gb`Km20CC)+g{dx3cGGR@?L@Qal1m11Sl7$BAgwbBD7o8MF45waFR0PxRu=Qmx~A}|Qlqr6OR z--j|Sc-czC_maFuraLQL))gW^%W1Aukb#QzFf~DEb-$M@1vjToymMG{DQt3`Y;@!9 zq>kHK{N3>&VY7A0x+UI3%Mr48(CypYm))`&Z`kygvqLza^$Xp67>AcZ&L-nEs7dLG zpdfD(UEv$5pGO2SF0XKQoUqkOr-T}E*7kKP$@Epx_7go3NP`9@;(+I+oXD=K@s6#1 z`Q<^pJ7?#Z_gvywV;q(qa8g*>!WwsBSc10QWo?0Wbf+A8?e64%Aw_eygw~6L(YQre z8ll8*zS!QvoLfdEqeamg(`1#fgi%QzY&1C|k?%Sd8CC$0mJnf?D`k0qrF*A zqt`RrZ0PQ$z`pTf|J|^U+eMIR7@nt?#;JywL~?*-pfCVLDp|M^&S+#Xk=b?4D0Otg zP7kXk$caQb$QG-;geOLdUAX}o9mB4Yuwv6jF->#63u@-F8`KSGRk|zj=&0cMXr|d= z!9OzH<(I$U^kep7hJOaQI(KsP*mO2N#o!T?S}D6uSv7)F=1SSEjHZosyP-ha%^>_y zaN8KO<uF%~)sz_4 zQVdd{(*vbdEmc}M84-bsPJl$XVu)S`XW?WWW3z30va3GT)f5bmOLA-zx3)UvSU0vD zu-=Tzg8>XGLn-O>EFP9yd1s9F!bQWA-G+nlsTPrM}j@Op3HSA zH<5|(^2+i6XPERb9wDTTC`KwoCqTJ|i|J;ID55J+5tq@R*aIcj@s;dyly^Ha)m0R; z)&jV2+th~3g-T?Z9KCl8M&r7mvQ~bf&%{Uag`QOlMwRAiO~ArRq$p$}%a&qy!~W2P zXr$8>mL;N7D7C%5$%cn$KUjEIElGljQcG;2u?T1cuccDCgl&5qj&-1%vsF?&kVlLb z?c3o0Z=FV);Q!x$>P1sW=)+Gv^!EpUe(*5|p0f1n{WtGFut+UjI{&`8pUxefy?ADI z-}m-CY+49DygPrM5xw%F{l>)?^AA{j{M6Cqfh?t3QcY9YN@XP-VB>f7xM9quHw|Z>#-P%i7!4;<+A3AJ1ez3rwFFY}RkhCcBY%vS!{W_}1w&lcH8J}<0{feb$ zRioF5hheuRmIz?C3hws1og1=^KZgT*5GU+G;im3|v9>l8`-vo8V8i`l3DjaaqTYy_ zy%=TUEo3aD$-w)OZ~J5dJq*zujnw)Q{+d*CPRVQFGe2O_M%UDL27xgvyz zepnf3PPID(ao445@JgLT6lJ)jKd)DCs|K(CZs&$Mg>>jbgWD zt^aQ4 z$5z`6D==sVe4-h9DD|;bGuB@#-R*aGsu@7g4Cq8N_D}<43-z$|SC)7C-OdFhwzdW~ zS_^{XW{lT@d#E|GTlKd3Zo^v8Z!vVHIMfZ+DC)_E-&_CMVml`f8WOz{IAM=HRN~mJ zg1i0hPT2zwnz3@C8G9(vv6Xsj*Mh)SOBZbYcRM3qZ!@e$E2|4mG-D5d%v&{M{nBr@ z-`%NZgo9>;PBdc=aIRaaw{~T&ZwRuB#Lfe3_ck(b&^ z^|foHEoX}j*^`~qIoqg?jPCz0P2p3A?So%GK<$6e;zQ@3JNv7dr%c}z{N*-3SDbc* zxOnlE+F6gF=eB4Ov39ux02zwx%TT{7L$ToU3*j^I@t-{ z5Yoxi$v!900d2C95zIb2BztLrM>E5wr$TI1jaR!G%!fJ3>}ANY3}`GPkwdk3!OZ3( zRT*M!4bF^Y4KmVLR=^vrm+~VWlBtSa_0n~($JNP>V7qh zb0djFG$Cdays&GVa9&^&VJDlcUH{#~{6x5mY(nQ~2C60$%B-s(nlB`fM#Zr4&UEu6B^88dUe4^=CU7~hNq#4r ztY7rrqx^)ri)=zwOPtN74M7ZZkZ6_Yl9;OnKomioTjwVdkYr()a1uHs)C$#O>7d;n6_ODLjxE$a%9pdZ!ONaqNbLf_c%_ zMAFR%)t*ayE|nLBe%*LavI%=v%_lLsqgx_p&`d*#yPj(0d?h+r z53KbQ((3@0lt4i=gY{gmob8vhaH+{jS&fXwPG4)a5E6F;FEsdBd`MhK2ZFd@Mw z(;m#qt@fZp%T%M^b*ZqKuq7yjM@cs(art~&rr2G_u3K*Sd0-Q6C!5@Z*Ih{FuJV(F z1yveWpN$bQLt^WtD9|a{o7sfyrP5?jI65wYyg)Dm!DvVOOt%A&&0du@(gU}i>)3$T z6%(eL%*kYqXW01eW7k#h4Q#^hWRrXF9FJt~GC#oq8L-pE8VVO^mJuP=$`qMR{3IH@ zbqJGv+pyc9&!Z8g%sa7g5gBmpQiQCevdykYYDsg@jgf9K$z@^@A)4NO>}vd5V3W)a zHrbG+y9doDFn5=&h1uZF6;d}6Oe;<0yppJA1&H6oCXsrk9I1k%>BBDQbx6q!Q&zj# zNwpc#G8H*^A35mqo%W!m@h;2IrBa4VWfQxePt=>2f<6A3`<$uS2hV(HCcW>QM_zj5 z?87%7)(-vZ(6bIb@Zfa^@dKYZP+I!Q(q&7B_P=ib%Hl^B`GxN+cnh=hFP}eW?(gRk zv;Q(%-{(yKY5J-F=kJE8+qO&^T)E+2W?neuZurZc|6=+dcmC$yFYbvw;LHa_e-`&0 zFI)km5DcS1DkNENthrDQ04KhHU=(3UQfaRH;;h!NT^f=#BWlL;YBR<0xpiM0_{2JK z!HZEC0+LjXTaTc1hd2ZZ!yqBBb;n=F>?}XqFD8Sllrg^7ZSv_7>#h6ZfU`l|R?JR_ zLtzlN(-l@-$7~nk8kYo}37+W8;HDxBVpYHHx7RVdLmW(iFc{{NN@P7q&{-$vLlFo; z(E~Ydo`=`4--`Gsc=F zw`tVZG5fPP5Q9;`(Ns!b_eCJiZ`b)mqt>qV>PjI~UEhBP{&|DAt(cuG4k7&DYS?;( zb|LPgn~2+-*$HuB7{R+jwX`0qau?!0vO(Nd%ua|yVFXLZbE)+(X1fq~<9cyGATCS? zJEiqq^=EMa0wX9Dt=sDn5YH0wF-<8_kybmJqXWRPes`U94}4&QxUHC-5EtC-MD%1Y z9$Uw37vlb5gSf4jtq})%j_s@mrrL$Lzu!#Urp!)=gJ1+|h{?wKVZIA-@82M9D`qFe zfp7q8+|V3m@16N~>%<{=I7m~;s?782nB5@`!BH5-yK1_ZU-!jXZYiD{L`aLJ+O$z4 zThe;8w*znAAZ{yWC&VEzj1BAvo+!nqb_we43(Y5y=60Tn=(5gE?A$yRG->_-?od+ zeDelzTQNH!4uoN-*mk4t-bd!O8^mqJY>hauz$%ikcX4mpOx&i-PKX0w81RggBkol{Zbs)4(VPsyjnYc}voe&p>A++nz*$orW&TfA7x^*iOJgS5cDqU3~ z>$>VFbn=11WZimlI(xDXe(8Gkp4UjVs%o8mbPvU-N3V&`#Q_3XWh>^*Y)0w45cai!-}>O?C1P9LFa$J}A*YTrM#jWJ znhao0&3NTBuED!DJr@-dr`s zPLXJ%cJQ(_+mqWvsmE;fq-q_`*r@53U%v0T*0I*e?_Q!!?`5>#b$+)=iK)MUAMMo< z*ZI+!ibj7KPujVX(*|F>w~|v3bxx&{)2W6V3?sG-Hwi@U>mW*tD?Z+&>QSJ;2`#k+ zHVRTphEm}c#F5c@b~#U4<0qS^@`7~8>vAQL9+EVQN3sg!qMf!s$jgc?357JCA&|DD zp;;zPt~43V$@P7S&xDyiQ`ZvyAe_Mk85aw5v7Q=2p2pPWLdlQr4whro^dQ_kyL+rT zp5_PBNB7`!cU`zaH@=JCTe#VD8pd)uU6iG~n})Fn70V7lQ<4(ddNNXqWwKGouJc_I zWSK~VAJ60K!pDhs!GD1%c#{X}K?K{~3wLz13fLAF|qStW2c+A)i|sCNr1j2CkUk_yxi za!~B`O^nP35PoLO+^y#YO%FWpS+!&C$n&08p5Dtc^{(?gWTWS8;!B$>Qnj|BbjCO# ze*y0pi|htP5=UrA!5S4Fjrg%fZhQ3KTig=IHVod;$hiab0Sx}UgHw#eoa$f?qreCc zXVbx6EY-KEh92&9v%)YFr`+61zlLxeRx{9ab2(wzxvnBaQAgBuNsknIN=f*gOyvD%lk@%c$wWU?1)OPoFAI9l7N22M?cisCw`p4nFWeYv~J1%ln_P_?5-; z7cQH>WgeY->FjN@$(gtA`@=qA`pr|n3kY`jdB($6?{j2o+IS>CHMM_ndT#FG#f77l z0T+PJ6jy|S%QwSmNu-gu3HF3&g!ikcYJ&7KA&CUTm(1e@Fh?NIJ3shY2=2e%q8uIn zK6k5ec5*;&*1k&YK%IQo6b{#|Q&{_2sDrWc*ibna-Q&~ez_AWNsC ze5TvPYRgI6c4V`W)f$|%;ugClzh11Y3>@pYrW*@}?yT?JNYm1bM!%o5)pKTSL(d*v zJv`EP<%GVoM|q~+b^Ho|4OS{G6&9N%rx@mBj2f!31jMNg%pBT50pbiTAJ_NKJKkON z>+DEF>Bpnr#p`$7uji~DS~mh_j0ns7wG@{&af-C+_*M^6oIkzxdL6l%8XPn1j>CDyr>rB1> z=pNoF{#nU}YP5IOasNm=`f20eKi{e&*S}p1g8Sk~$CHBl;?YRcN(lo-O|dI{Jra)E zBw1=zN!rk2&0;Yu$7=`%_ws`xeMjgV2loLvENW~zk?~tGPUDRJ`ZNB;k#5oFkAJ^- zD-AdF;#sQ;>rB0HRCLfT#ka^RmX#tUS8VaDBc+oBBs=zAH&6lD*F55(o_o8Cg z6wmN>?#A&9bkW#jp7@?!D|!Cv{7A`@;`;nij2tw)#9)OCmr5o``UY22%y}tD6fzuj4Z0*0qSLP9p-qyt}CCt^B@=p_3+rE zu&G@ud(P_2NZFH?Au~t2C{KD$l&FZJY@x|;gbD+ds49em>d&?dm3Xxs%`53dRk&lJ zv}O|8v0KNJ5F8tIE28dN(=%81jWk^|3GLzO@g5`7coG7~OC|8?UF-Pp)#>$2Mx)4SI2 ztktPeFP=0BEgZF^c(#<6=tO;a02HGc9ZYuxbs0+87M-yqAR}m$Y{NAycE@7wI40!I zo6d1$I`;=7CC~lwR!XklgFWNn$DTG)@}vMif7Ev@tk$AyjN0;xD_FfLLbYs(Udi;x z99IE`T6e`VSfZDf3&%}84)8mkkBno{IcJVEJm=`HH9Y^=Q%4$}6g%gR%EdZdk%~^r zs_}Nzv?~~E^j4~>$)zB^DmM#wfZyMMQ&sv7OK%c8chq+rlg=uP^ga9YyVm#UvCBvL zo)kN0_i}u9yhl%pojd9|4o43kJJ7>_ziU0uId<7d&o$v_4~Kil^;{ENdadhpo?LkGTc;L-zU zFMVdIx%8m@|FqxOe{k`=i>1Zsg|{tm^S_?IW}ce+$=u84h}mz?K4|3|LMAy+NVwL z+yA`xy}Ntxr?2_yZ$J0_-y(y56;~fJX1oV1rU_&C)#qKyec+5QwZZRpUvT}A3)5eE z!A_@&f2&%NKHVjs%gn7TcEssB&!eyO;6#+b2c zJ9Yq&sCSpeb^)&&1m|mUpQM^#q1skcYIT^^vVNA1*N-!HIZ*&=DNaJ@4Vr)&w>B#3E%zrgF$ii^hqD*qqQp5bpIvQK^ai{ppj{FiTd(%F~&XhmFo z(3o*Cml`mrEN6lepV{TU&so%v0D7p>sLP(|qb)mw1Xuhm+AK^MUwq#i9{GyXfvc~* z=r-rVC;syGG*Pd9{e}Y{xIT~n&4u5)@0_^$z%k>#?~_0Sv1(DTBC8Y0m|QykAB zT1D%?w16j@gQ^%WFvx`QCx7zvo1LT2U4De}p5NU6iupf2`;+nKJfi*0k8}TA`|JZ^ z*%yne4;VAv1F+SEaq9sG9{Jj5{p``Vy!5Q6#8a;VKU}!*oz&HToST0B@4r~QeEAdN z>is7gx20xD_9duB4w;Th4%01i7;!MapboK~LlKF7Q>+o`X3jjW@!!4Oc)=^aaVC8B z*FJL72cLe^8NdHP?3X{=|FK_PRuflJZ#i9Dz2BH|G#+)Lz2&&v6b8c}^+x%{T00NL zn7E>fEA^FXaoO$bJQ==YR4hK_x&ud^t*-v!rvI2a{Qd8|GxV%8E3cJ)^QUJ$?QNg< z%G8g8QE}gi#sw{&WV1|`%7GmW9rgla3|BCnE6D{3ciG?$y&m-;b~$cMH2y?<{^*xK z{W!$``+xF2a-;Tz|M@2Iiu*t5vxjW#;-#Oy=r=WS^*&?9^{!9XhRYpaA%uhuDg%Dm zX?R{Fp_edo*}cRIS!ud>#3t=l11^+?y@|#T z{^k6OKXu8kW~-+??D+@2^0oAf`2Y8>Q%`-(X!1sPcm3~sZ;pf_0zxb&yH{S8Bn?Gh8xJFz(FlM|5 z5VHy6TWc>mt^#@E3&R^RmUhdg@t5uSZn?1`tt z*so@8{0dmU@8h06_kHNyzZX~cj~Qnaw?aTo-cEU9gNqx3c)h68jt5nRl@vS#YeKI@ zt13^nj*klP`bXSe&#wN>177v-ulscMmJV~KjKH1Jo3Ln#^VnDQsw&Z`p5tL_q`0y#EEydBwNC!hKjSC&*Wct8-(=-a~4A!uZuG6Fc~l)82Cx^t(%6 z@Ezk7-7{Zx`#FUB!%Kecyr}o5&kn@ZnK9!%nDHKR%oE0cc=s=U`@M@A|Nj4mQ9*uq>z33xpa0Qw zUeWqU;S;An`J?yy*ssOasWIa{WQHeAWd-Sx7wgv@zp7*~~jL%R0?N9$><;Bwvc7Jq#)wljK!zAhnAlHocDfh?llj1`;1w<;qq?`U)wWpcT+d(=O2E;>%?Q1jv4PE zM>%19+rhWK=dv4T-YQ>_z2MSMU+|viAm^7J`OlAeBZYkG3(r(CE%DfJ%yF)17_u7N8o5f@OG2=aC>L!eDT%Ee1`Qgh8 z#bYlU&OYINm!0$FAHMc;uXxd`$q)WB^QKQd3m1>|#*FunSer0@^(!y?>i3^V-SCpP z-}0#RH~#T=7nL4==;p7ze)aEWA3XQ5cU_Vfk9Eh4_mB>oFh1`MpZNHd?j<+Ee*oWb z+r!@Qg2y}pMPGUQGfE%-);Y|xU-r${h{yaf<2@v?CX8RIyu^6al@Gt@hA%Z=*vtJa zbL0EZlV>6NeIPcYUi_WgWbs&M%yv&3WGnDHL+MH9wK|Mts`|2Qvk)#|yAdfIz0`Pgrs^z`q%Kk~rG zlj5=VnDHL&WltENbJLT*B0b>^V)5^<|Hbvqo6r613*Y{e`R{-AjaTr} zzux}!Po5zjYmFK2ArmuU{H5C;tbF4Ez>l=kjL%$s1$z0LzdG}!xpQt_eK~&Uyoa_w za-XHC-%h2cJ~cJHytumb+ymMDzc}!NrP~gEd;fVy>hm`*esbx92i`aThI!}kn~q$4 z=xvMY;jip}^1;gvJahhW^Gl1s^u`fPHc zI(z!e5B7iL&^Hd-vNI3gciX-X9_lWH4?p1ue)!8yw#GqL2#smCfiK7 z*wI~Hi_{^O%^5{X3iSm7tCMYa{+fgDnu2!bd-4}!8RbNrLQ1q8TqVvijc6|!D-CN7 zhBrK3DCq^djmE7oRPfUkZKgb-uxzALMbfZ3G-9@A!fvx>W!oANXDuoOhVuDlx|QiS z5)ys*gOhe>?TFm9%l!mdje|Un@=ez%MzN&Qh@)1%mEfsxM@dBLZFBmS6ACrfdhN_Gc8ORa38J#by15@Gvoo`re1 zhL@d;VkWs(C1Ju?AL-3m6ACr}+E$Eo%n%4eR@zvj2b+UhHSVTyJ!Ikt?u)f5uE>l$ z^9NwYm_DkoP$F!W9lXt@LYyi3KBdTMzFw-4LZ@rXT!$+08i8slQk5lhje8^XH5`Lo-qBvDtB`JuxOSS63blFN2#MkZe1FyEU$ zI-#HeuG0<>_T5G?B{c*%3a~0qsWHF_Vb!2^YrP<}aIMzMS<|1JP(+<7*>O>!mWtK$ zF+W)@51TF3E?2uu(bvMmShE^UxAWCBKlA(vMWjlocCQxd*PLKjlsP!nY8B~GZ7!>MO1s-n1jQ(#g}M74rznIANE{3NXoyuJ z<$la3N`k?N@wChZWml@{-~yhW?U+qq`o$BILy`zY)tU}6uAoVsLdwRlGUVF>p0wSb zQ6~#=MJITo#yof|qTH=VVZ1?FExy;PSS>*wpgfH=v$9FYX|Bl*dttgyVs6&j_qP*@ zj+O0Mss^K)ts`h$)jEu$^iWx#Rj1LT?Rv(htMN=zqUVsYQf04P;ffJINf=%gNhuk5 zn2Z3$ZaHeWkX(c{gUlMCaI940&zOj4MHK}y0<_XXB#j|L*@~un1Io;W!jL{>QMPXw zL4vJDP=&eMClpN^fJ42qoTd1_OIO1xBrvYUR=a}abc-~T380YT6z%2AsrODO+>n^Z zbB>X=c_XOO9trmX+trF(G-uV-gy*@wpTpgJfj0N$ClpRH4G%@H(9=zqFv?1wgR79F z)f09!;ipN5rW{dXU8;r5J>@tBrDpA7o9;m@-x)GSgcaFBmqmIxg?D4grrM_YR$SDh z?$pO86g8Nq2NZ3H&2&hJWm!77T2u!^{cxqOCrwC>0PS+6mP-uD>6c9?s>y~axOF|+ zb9zb^r{V>>LdmUiA8FOnF*7PKy;dz;Gs52NPbL&9p*v2mk`B8KT#xu*r3HW~t%kO3 zW+=nqZjTRAgW-y7GqW=j3dLj7f$P+aq?3!+>t0_GgG^IR>_~%XqFOEqf)X6n3vIxg z`p*f45C!=XEhR%CfT_wI+&48l37bQt5J}>O(L@+7cq!h`C-9j^k0}&3uGU$`~r{$HSB~{}T5DH2+&^FwGBgmkKNs!v&mp(9|;1xG;Q@=C_E~cRt z<~QSNIb5L1Bu3SmCBOqP$D}Zb%vwwD8&j0Kyj|^todW9*2)n?-YPD4a6CT@W62%@E zt6+2ogS@&M1D39xPz2SDR8y#E-8vMUnfL1<#?SFizlT9gQOF0b(4X3&> zC={XtOV{GLzS_4;kjPhp@;eCvIBZWS#C)zVHIh=V%nkcxxs=fR5mjkK{g@E1q@k`M zO6{~h92VWf)iH&_QZ8PA2~4o^^-3$8YNJD_GP9ccldQxsE0QH`EcsQXj3c}EBHRY_8MnVxWxcU}@ zQ{5oegkoJYFEM%DY%``1bIrr8iB^SpK@X0oTjg*U3;cqur69ALhQhYb=R2VW(9e{r zs@NCO=EB4tO4wrM;54Dy&khC_sezDUC$o}W$uUvHMH4!e9n>g^q}ZT!b`fzVhV-OVW-?Q zJN-z=qw}VzgFqo43eZDK=At`kNANFsNefrH8WKywB%^f*F&{z%K2 z6dYOTPGltIgaC0EfD3iKCDfCK+=WV<(ri`*DoR0(RwkPVYKj{yG0vS(h)Pv$g=@H) zYE>b-ZSXiqcOpo|Nj8}IPfsY!EEHE+JRb5} zp2%dIc%dXD<9yGpDOMzI`-a~|y@B9^%=~936p|9p$FXL;1Xm+orI1#@KA2{RI#(&y zgC%IDH1wMxD@uTwr5{ZwILaFYK!~vhnpImC;0M^cK~dgZkLm$xtv(p^!lsH83?OPv zKXOc=0BJl}Fl!d&w@XPQM1yX>ZP{`}t<<7bxx;2Q`QK5%8CY`L?Sg3&J!~hv;IH-q2 zoqvSlein$%kjy%n+BK32D@d^MHl z1VoCZV@hM-lVE_{2e}6?ob;78#PYcqXwU#uU~~(qKqFceBEl|0V?|=vPglrvg%=}TwrH@tCuWIiHRJ6+J`biN!Sp@sQBekt zl%=v&M>9Y^i-${zVkAO#a%D+#>Q;ebGmC$k$Vfpowwra>2;7jkY+Ynjv5VE>t!^qN z0WRIvqlG-5r?DZu_?rnuP)0ih#CQ!0xk@cjX-iSGD04X*rUMu|uosb9emI?umUQjF z1CO^HZ3ZD6;d~uWVbx@Pm`ypP9lWt>MbbEKIIV^kmup52As62SeFZ2iuOEWg8@$N|6JV4x>f081`)Qz+|+Sn{1UWi}kKdq zH_IisNX;`7?KbNjzUFw#i;7S7q+U?y&&fyFXq0aeP)W}!z8F$fcR z(Mo{3qRdj7?bD7dH_}F;LLo&07GaU%K^GGsyNeY`OE(Yc$6H=1MpHpW$|jHX2~Kt6 zLmEb#exg-(K-(NNG0_XbO;3@%!>^lYrKF8Wnz78Lng$F!3T7i>snqEv3wbEdlcG`? z_39 z8|U5m$Il;_`}zpV5wg_R?8q_CYh>pLy@}M}u8HKl6y`m+brL zkuMy%<_K}b3ikBNho5lxV2}rR+w?E@eQ4kFf;~TQ@Y_p|I{2=Gmmf?WJmbKB9QeBf zR~+CEJYwmmOCMT#9+zn~piZ<$>#b%l%r{Qngm3ZZal>_RJ*62nB_Li);trx>(TdtG z`%T{{6Y|oh#uSpZ$+b}{tyJD zeB2UPW>^Li4WQQy_9BTq?r2N5Ok`x`n8v!rDDGk%wx7|4akt1y5v8wa#Z2&^8fev< zK8EHAdMP@gNWg{QF_uT@SqjQ_f)U>AM=hjcHj@Lh9o%vab{9B|;bDdgw%kRqTn4MpbPBc~Tt?V##s~q12oxq(mr8RG?&KNCxn{ zSR7xN40gSiuF4o!E!q&^xOpox3(M zCEKFtdZp>riaHuB>k^EVs^-fC4o8at0CVsheD*OD3O-coHS4O0GqkMLQmUSChKT?* zyP9)-AdH%Zly6lUT-UUwCyUphtFVgNAJhwIRtpu3tey>bWev_jiF(&&EkSnL7SDRt zfS&7(cbs~vZqwavB1RS+P*Xf22qIErfY4qeUkLDpTd-weQV|L4AjSAT zEjly0nT)Y9+Ym8EbeHv_OB9jW$ueIL zrOUy8xmpGUhDeRC#0KFiF=U}ivkE(SkRWn=R4W39Bl5lUl5Ig<8+yEP5U zEm=LU-cGdvs>r&1PUk@kqzlbtQJ$Y9qO@K)Cacj}B@4O{Cy@%0VjWQ^CK76JK{`&W zqHVU40g!xsz%NXu|2>@9?0?8atF{7HR40)j4TjRQgQVq4!9|@?)b?AYY`@RZ)jVt_ zYJwv#e0S_mS_>(8$zC3<4jVWTjSFqwQ~O4%9>PQckoH>{3`Dl2x@j(^gwpNn?4b zsvD(jwU{z1un#G*dYMJ`oj;+dGhJO!YaC&FLzRf(QhOM#1>25vt*Qw{%{Gw0~pOC}U@y^_y|fI=w@2J3a*9tQo?squayU&A9c zN@nbQhv`6!&g^^FaSF+&5Y<66!(@x}FyamC9;VVxdk`nnNH5`ML8{V$}7nANG8p|+M_QIU#9yefxO3#48z(rHaiI|UxfEF3%3o%;P2tB?Qde?9IjJxD(e zlw_6$uo7d0+E~T(q-0%|EftR`JXEcLt}&tsm4W(3Mt% zu6l(UAmOnt?ah37LeVjJv{V+=BG@O2NQG3Tzzr(Zvz zXd#7EG;KMCz?SJjn&{ibN-p15VN=aiDjwbB@^wqn2PtOZK@*DRAZM`6P(NNR4$VgJ z`ac>}ZX*v>_h;Wov zaMdma2OXGEJSM}kwF=eF)MAWQ$%wHi%s`;sjkp>%?(O1WmnqtRiB5Mm@w zWCk8+9(>bSE3H2W(`7wmc{wK(WaphWP^q`NZc%LxsCYV69|kMsaJ@~L+QBy*zV?*z z7`o0DQmtkfM{{^is6++PshA?%#1lhQ$)+QWneSEMp^ldLeg8G-E6pkj5vLyom2{<8 zj7k9Ba0e+18Vp+qMsjA;z2(r8LHIc)D)P=Fu^%SmFPhT=A~GJY$T8f?q_qs_0Trtrj>MW;qwy; zG0_VOK*hrnMP^wrlTgc=t;A@#t9I2&%q4leLN?H@pUF&@M!{9TyCdwxgw;||7{tn( zij`^xmvo9!E<+7NOoBH1B^*^aBu+^Hr}qiq(A=aU`5GI`(xwp}flCSTv_#g?N+j-N z`mLtq5pIm?Bm_MZ=E#E|n@|YmFhMhNm0$`olcX51UNe2wv;@R@mG^4wd4KivCsZx8%6^gw{p+Pwp%bq`OPc1CepR|WwN z@If~#gAfMqT9nou!G?BMMqoV<+8Bh@bvlOA2|!pcom4J31(eneeE;`QT2Uw{;j$G< zYb{w%VzCZd0lk_;FL#o>?V*j9WARC^5tK)*F4t+7aIj9=J!O*<(fW%>_u>7JnDki zFG{2AwsoUjbIjmh0xT6^ELFp{-H{Fr&pd&T-ZA=}@escQI~fbU*od8cicrZY2m#@M;igc@;hdXtQ;JLZ zCY2f{yA@bs(V}GJi{7$5v<8D@#4HWLv@YVO?1U@|L>R)MSTw*w2Cc!6)mNGrg`^$N zG?FzYIkclzB`Pp@q0{st$F0CkaQ%-9ROe zn?6G1PaNxv5&0iBy%#LBQ}V&hW*n6Gt8}?NJhI))oXr8h!HBBiO;MU~A1xLY< zOeWQfM>sc8Y>P3nqX$xky4xHQ;a<96qqRZ8i4(qFMfe^qb{!FDlO$1Z$$&1yNTQGO zNjQmSs9nL1qobyeCbYR@-pH3;_@L>%p!VH$zJ&i(`qCcWm2c)t8_^E`GM;tzCW-{} zTJU1MZRl&}uu&r#Pu0L~spxt4WO<)Vba=>J=bjm`&DvpyqXQkP)tG zLAft9srK47LV$D|XHUJV)eyETy@KYNL_JaTD+t|LF;*C`k+0X{Jd@H|l@$j>Q@lXo ziRKF1#&t%o$_=iT9Cn#1iAT&frRDKhz3H+AMOx01yh`nAQX4n@IR1ZX>f=*KZaVU+ zBl;2G@E;C;`ta)EryM^2&~FZX{Lod0o_y%sgTFfXk%KQhSUh<2z|Rl-~LjfOFT7)+zd$XVHvez)Z=3JT zKXLwGfD`cMx#ryC<`!qaGW&*EdlsLao%!O-)ic!@WM*pLP5WN8Pu~Yj|6%&m)2q`@ znLdB&H-TEa`T zUF?9X=Z8OibD>23ei*KQa2wZx`?I)kON?bZO7!E zl|&Ys;Ofy4%`i>L?Ey)b0?h2Vvn*OEKhwe~Ai<|&tOy3xnldX%cO}~=Gr>#6)&I}l zyTG}UmGz>XPCA`*`T;Y{49{Wq%!YY1>8{EHf?_b}4eQSN|y}s{09V3&KVVU$MOIj+Kuc9EW2FApK%@BAWC==yo zzf1?pl3W9CZA3>8KDt-O_)vl<;A9?V)wZ9eI$|#|z{Z7Pd)yS+e1n^ek^!$YAZlUA z(S?sL&B~MUgLrQJ7^&`qyWqGdSxsSWvJVNh_UM2Z@gOQG%wX6`ptM55 zOghuxv+?Neew>NZp~5sc%y+?kExeEEn7egMGbJ>Xny0<5Q2PyIj9)HHew&lLkC`8jk3cRt(OyHqL52lS`1k^FAj679|_QpZG z2AJpT7`O{XV->u_gvodd)>>H0E2O8>p(+aSATQ+uZKKH#k)TVK|m8Q_)pxt5Mu0I3b zP;&@*n&(J_2F?jgonRf-OLyhj5!nDOKBDE*JN- zk|J6f_0qbYXX}`3rA7+nd?w(h(`0mCk>hH@cbykMdBdLvXG(6h01+poML5joXv0Uh>6l8M z>GfLvWQ!2-whB!|p6qp-V25N}6e=mtG@VQ}GAKrtgo#6dkDjSxcKMDPPjz=Iy=o~# z6dFEl>qziUuA778#=~BOj#Y{oR!CTxL5}bA31c4iAb=H*TYBa zI>tnhS4SN_vg;TV@mn3QgOAo`dW?r=bvz6o*>sGFsH=|G!bjGb9^(O3btm8R2{RTV@yOibx`n?$8?N|;H3@{zVfJ!F%g&4QH8I(QOB4FIqD$bD{s&- zCZdTtD)5y@bc~5Wp^h?qWvXLL!~k`a;47MrF%jNV$EZ5SMC49K5xz3fF(!g?ItuU= zMaP(kx2a>sI>tmOO-CNSGSV?7qGUR9@D*9dmwcb);4C=G)5TQJ!fEIrDSG>WQ4&B<_t`%yUeV&j4*iqoPmi| zoSD^|5eCnjGcd6xG_w{o!r-}c1}0XRW>%I)7(8drz{Gmh%sSNwgJ;hfm{|RqS@jxW zaMzrHiFL7=_3-(3MIL+CopS~zR@r7&*G3rJF=t?6&2A=ZRub1-K_(WakR|TPqjqw#S)4pG*ha zdg3zB;{|$5qzE~`dpOo}GM($2x1P8UsJj2*JIq|iHms`4>51!&rZ3eEKtFM>&Ic6T zL~2#TeCEkl-)wo}k{z2B8ukYv$ma?Ayy4VD)r9M|0zKhNB1ptQrwcq;9wigiQl&M# zP6C3CZMwXmzUx4quwGE+iF@pY!mB_*uW^7qejkX7NlmX4hakf&PT$1qKJhYOPF;(M zZM5^ZM2<^+S|9hsOMyP!LriR%8P@lthqRvv13h7&$Hdl{VLc1U;#*IIfHK`3P3(dh zR<_`dL7+-^M-$s!hE*+Xr6Fjc+0ETCpz{GmH*sicn9s30Zav{YF0_gLt@BsOhFmjz zMxk3z_>Sw_#7>W4J}r>SVN|CXhLh#)6JDT6cTW>LI)*i!<(?j(Pj^of`!|O5J?WmU zC*WfXn%Jc=tY^VvAfQV37!%tuhE*-~m|*CP#zQ}`uk!&WHgUXcn9ntj**h+@iT#)J zS8;|sCU{1n`%kz5p9ddYx!4_+V?H1o3@vkVM`Gv>FUg~qhC*QAec~lRrS3W=Ha`rT z;4IhK1^RT?F|pNQSl^Sb(|+P1phq9sOl)Wv)^loPdk`qoM>Z3C7lxHB_~`>cmF}k| zHYp6NTJEQd?_K93+x5n(w#5}<<9>nB}5X;j$e`iSczMun}epK$$zQQ=kBhg~1O zYK-#===yQjk6-;T(Jiq|XOd43s2E87nC~qZW5e=aEdRx*u+#E;mftff?67>n@&%*9 z>z3cO{H{@9yXAK*zhhK*&GOrp-!>|2v;3Cjw~Pu~EuXi1-l*`Z<#U$LU9G_LD#h|y z%V)2CxY)%QDHG$O<319b3}|Md0@q!4x$ZJ5Yj0&&XDz?fs%bjBlTiI5=`r%xYt0|2UNTAh}1|>exS!i=E z>zk}^GAg`bea!lpQDLX`QR|~dg&o#6THk0?c-{I2>l=&;+pUjSA2BMtW}RB6Mulxw z&8is{wpvxIYE*dDI2sp9ae|_sON((mxyh^7_UvZ~XFA;qw-O zw(URJ{u86Z*6lyu{^P5{=PvB*HT$*x_B`P=J88exg`OvDvlI4fz2bSoR(sigt*1NB z8mrrXwEai7*8gX07Tj9@Ke=goYyE#>2l>|e|Kxu9t@Zzjrg3Zif6^GY*8flTptsil zCp(Q>>;Dsea%=s6!Wg&K|0j%bYyE$+)3~+%KhZR9t^ZFL{f4?7-l+P<(%jbB}`aiCJe*Hu1+B&{|pZy=~pSAy(ePT!Lm)5?y_Mg{&bZxwr zS$iQk_5V+{AGXQ1l}vh6i0 z`s!_uQPEdz!$w6T+rGKz6p!?^ekNALIuSOal1l z(rQdAwUo@`$(oXmEE>y&xE?hs`fAr3jf%d?^#-G&uXH_PRP+_DsZr6FyELPsFLS9z zMPKTg7!?h>6r-Xc*Z5d8!GfDdooufnU=g)P4e`b1a+~!oU-ZkHcNit`r<=!ac>Y)a`{wP(Uo989 zXbe$nGE>c>SgFb{6w3)YJ4QwKouX0EJ!jjfsM{$R6@7`5H!8a8Y#9}O$k{Y1`k<3D zD*AwvH7a_)lQAm#VrRps=!=|J=UL~rfy>UT^DLBMA|yy#Ps?$&FO!|oVsrWPjr+h{ z&pA6XJZ5t4jIXo(I-{cE_QOU++uN@_7Cm>ySKdw-6~(q+WmGh=9Wp8!-wqlTjco^v zibl8n57-J8%cUtFl_`dva6}@5g6OZ2#aC#%4;d7E=)t-2X^F}9qP1uupH53GA`}+S z>$kA2zY`J#!fno+K?Ez+oHWQ#a96&_8GYIvhB*0SSqv&csm zT*0w!P;lLDP|&_+P;kv=P|#*IC}>^1AG9RrrRjO4UOhJCxht2ywf?`^*HNTefZJ=v$A6O>l?i84Z{m}$RTk8Q?kDq5#ga%|YaH2FuDx)%iBSSovpuGOdpdOv# zjc77MvAIEPz*V(uBT#Hfz|_n&Q-AG7re1uxN!6$nS&3yLm8X@dV;5xV7+=eEaFi<( zft-gBYw;RU6sR*y9jiha-0KbZQmm&eRQRlnCDZM^C+;UhY*myxbqa+m?sls%oam-* zTr>4w-N4kL#V3EId@@-p6tk(?)5z4ogD%L_@s@^F@NQNN3&0BoxCc))MfnU@t6HRXr>Sa2fA^}Jfodu#n% zHP0Xk!4nfh!L-<623!=Hwd&VjGxcBI$kf*dD1e|uHCHO3R1AMQbs#?cqFQw#?Ij=t z2CceZDo4f)%z6vc))}oj*3H(H2@whB;=W)xRmh;S7pDe8Z4{?sT~(1{fmW(h^|(XP zS*xyJGxcBG$kd*NVKGyrkYXVXwoac;ts1+iR-F<(&9128iEx6e;_X@#8r3>ynHrg7 zQIMr6pRB@#ayYG(xnWdIDOitWq-Mk7>#mvl&u?Vv#bJ@mHy zwFuJ8=KBhPFqxL7BxbGp;cKS;$_-2%TnwXtr{lF6mdoZ-Poq}t&0W}J59ezhUz@ZD;Et&#JZ8fDZ8fpWkpbv|$ zy=Lk^yOF6EXCMR~L8BCrj69t>5buS}KoTrjPVs?8s^6=2C@s@TL@J%xd|$hMSd3|j zSY8d~Kw_>W6&coAi9s?^?)s9vJgVe{NYOp4=PM)`E!Ahk;%ly%`pY*m^?m~#}ru?W%O{cAVUGP;IqFo*`lW{@P z*Z(%lGgmg>>bT4Hec%uOfB$*w=*o1K<#%kk*svW7nYM43SKfbCW>OQeHuQX$W&%{q zcfPkX2p=8L3}$ubIdrXJsaRGMI@@=VWee4VjV*j;65jAdzEooVvIr ztOI1O2Xd3^xv`CQ7CMTNdLvK*0DI)#YXtO<^v8{kznh0BpiRcoiBWP+t* z`KB-l@+D7s5UUCPZUIx!5U7q&8w&=K8WY^j_eGGkkRPhy@gzZyhs_a<$uL};Mhqn) zofTcr2)uDcn`H#PWHHm2&JV9*)bD)%D*et($M7&{^H1iB*PU!{A-K8R$C3))70{eab8+&7?N6_JFHvoxX$|6S?Zse02-Wt_wVZmqz zm#QclPT^X=GLUaNKlg+ml`6w{xEh(nBm8bCEVOzc&9dI>(Jih% z>%p%El$;eEc;nl|E2{2|PcW8?jq_yG8@*?FqlvIfdP_DFHtOtA&Frwa;9V!9S$&sW zYbarb=Fz4;6}KMTqYv$J113h?kPM+!^ANo9=GZ-9o^JHU*CsmvV{ySS&2C zA!>w2a!kL|$fO}P95WhoaYhhzDpg)T)E5( zeZ3j#(_@_oE65;UvUcqA_tHJ7=k`Ew7&x$}4u@b68rkg&26WN6qt?g64){QC-0A3D z%2kij+xs~lWo9NX4TTmU2#8)E@loe7|GPTq%M@<7Fxq*&-VhKu)APb%KHMD?5w@G` zw;`285nmxbz`9f>n^z}EH9W{;yVa}`@K$cx^M({oPP$rpU!GQ)`xrk?(Ebsg=WCEA zghuI+wBM*z#)yXuVjzP(9_rP`{ftl6&>}u6%Dd8NiVx{{i!T*pvXC6e89C%fKu0j^ zy_XO4deMRB>FfV{KoE?b{*HC~%Ju_WKezR&&7av!xjyO2ZhUE@yRqWzJ3isS)_-X| zZ~u~gY+qe_hwYzj@3yU3-)nWRergr7{F222K5*eb?BPS}t03AUx3clzrEEF{PHU%P zF*eLh1Z^@|ylQkj^2~!Mnn~aM=Fs7Tx}JwQZsYz-#S*HFqhn-{Efj{?sEQ93&R&l7 zEDatR@?Hn>gw3RRHk01@&6|f0%(<^@y!ff&ZX(@u`|y51$E#P_Wy5q%1~)d7bGm={ zVxa6*y0ZHWD?2^=xSTegvYJ195m33WtGw5+%4<6dvYAZR?ZeAgO?k<%s+03BGbyq` zJGu{O13s{_agSkb%Y9(+(9%r$ZT9e9osMqj7aFE>?E0$ljNPEvm#!B31%@M#FE94> zSD1k7o5%<%AKnA#Joq3e_VWzWIWy9+narO3!xsXrGe3H+VXbHQk%?>p{Ndd|<;;(s zV_4-0KQfUhpnLcNpen34rDq#fwXgy)lQ5ut_0CEtn@D)jKfDttn{_oihLxSs)tJb9z#rZL zRL;7ZZNn;0bTuZD9<&dib=8zxhE-kX`X(|TfOd2{&^B}ZO~cxj`@rHg0W;|l*u&d& zI=bt-4AZ$fTjy!iCj>{8zBf`1ma1Z`tV}b$Y$+?Gxu_o-XFSoyAX92aAyDjRUM;rs z{N=xq*?MsC$lgraOZo5_fX-~lUN=nV>>=AkO341<4$wODBfDX(XZVqcGzk3RHc&b9 zqcy`SPxz6E3<%xBEud;PWZMj@S{$-XWI|{kZUTL?F3@UN-*tX#;t&QjU>DFf^IPz2 z{k+3XLk)QGB7~VOv<=tYE9sSQu2|k-saby(#6HDsKLuj<2i9%YFK@nKGqd^Z)vvC8 za_bK_h>gq6Z@50;dW$Q+@wJt2ZhXqoa6mgD`^MTI*n7@TJKwq8-+txJ2Ug#@T3)@^ z{yX*`wZDAhZMHw&{L<#fEMK?$AM5XTzSjBBTEzN%+tij?`|#Sx^_88^?bx@!V7qhe z8C$Wf+qO3y|KRxbownrx$I<$?)_-&Mjh_AGfZ#blmfDKv9Fc{;hOOM{<#v|nQ1MK= zK7gv3P9Q2bdec&_1a`byx%9*xju$9yB9z4tUwHQaK1ayyWVz(4w7X5bLi@UPRZb&t zBJ2qZ-gJ^t$a2uHc*A7|NA9Z65z(}NR8mIGoER??VWD1>L~W{N`n8PKn)b0I2IUGt zPpK~!qQFpoIu=Dct&n@bHtBw4^H_%yiNi-CNp zs0FhLDzx(pa~&oSKR-vHv1WWoGLujxzze+8lfu46KA!gHF{m3Mq*BT~1n-@)IFIeL zO(TRkf{04#PSDNOiLz2IL-AU>nduCx38L5S4)T>wy`{BDsuuA0o;y9(F^%|BQLQ5@ zlZ3C=#fs%v9`lMLmLC_V0j$?5W+*yQ=hebW3?ByyA zcX-sMH!m(${M;>n|{kc)n>?$Nn*F(Z!HksP~8M;FLR4P=gldqR1L98?x1S2`z zrz9ZTubM`DW{$uq@Fa?NPpL+z)$$C-I00VfKp7@nCsjDu2nCu$4-qK!@`wdCjezEe z^eB@lu@e%^TX0dPO5mnf!OLd4(Mvh&32=;G3F6+(L{?E;+}fTuR#s6wex?? zS(!lmfoa6=&k-iZk7GYiwj+#R2-bu6bclyzVoGiow33e0!%Jp~JUx z$v8!1I+500-#=$%0`d0kA6sD;mLwPcpL13wX2^=^avZN%ir!E#!q!KeRN>N-MvC|O z!ML7hdRRKvta{VUM2{^BIaOlrc*9(W3B)77FgLZj6FHeJ4g6C#9H#}GZVc5ZJ@Q5P zP^3a-T38hxkEdi0j*NZl|L2^Q3B+;VZeo7L&vlqsLB7tkt=?$59G}t8|6jO5uk1Vq z;_bb0>z}tC+5E>%&Giqi$;LM~#?HTY%8qY12J2s6m+XII@2-7qO|*U0CRqQ%+FJd} zDrfn!g<1LU0Dj}2M?cuJaa_U7F~Y?r4u155-5CKBiGs~+=%2sY`RE5brU{tXb~Zvl zG)>?_GtoZ!!M14vCicROSU{K&FmW(qX7k$!0p2u$3vL4~(*!QK4Kz&?xZpOx%?Ow{ zue#_qz?vp-!BxPRCUC)3(3lZ0aejBvRY032aKTmZdea0hxC-hs0wy|_i>`v#nI>?- zRq(KB0vB8bubmMv@zIN}g4dWPaKTkjn-SR0?rXasLwbWt;hcz9sztvFLvnXI?xs4u z6hbf+ip1jO$v$=Yf(cMF0%&}o202D)x8ihTSFGUCOrKB&1DGOs!PiZc+Jr{?Cs-&H zy4c5{kA9FeO~Ax$^79WCAN^p}GyxOGoJI%`GXf@#z0I6!8X-_IP2hs7plq7J1y@07 zM!>{1fQznzqGMz0eI>K<>sD~8Pg)+@ex)_P@nYxS z+kedVvF*O~E4F9vgjRli=L7c5t&ccAv;76jYh8C+KJ7eQecQ@EIO|UL`UmV^*!&VW zx&NnCV&`)Xi}miU+qV92D`tCxEwiOq-syVFx_$Ms<2`GCxb~5aPp$s-nz9yKyUp^A zjjyeK%K8cGTWnvl_wBD*Z?A{eKj+{aUdMmh{Mh`~f$J--PgIF|NAQM0$iVY{X8Rs8 z=@vTfk8Dke(vb6|bHt!SH%j_&-Nze2xFa})Q*_Cw>V9s;*_jRRcKt7y1~%U<0kMDiquKB7)uNp zK@4FkW`EI~RRJEhOK4cgwc~zwVGp+_W3#A$lI@Z z-M)sGg@*kJUeNr5G@1)gvD$!3j)gXZqO*7^CEEK#0Z)GxLa_tdLolJRaj zmtfj(!tV{_eI){ix{XPwjB9S6ZEoEW7m#AX5lFU$C8I)v;(et=lAeb7QN6~$%H6{A_W;?@|Vk4?N(WcW3)RR7@(@d-qS_Qw&rP;C*# zib!(`Q>3HaVgU`U-hCC35l|1&Zw@EzL@t9vazzUIkxD+pR|GjDxN}OY4$CEiS^f1n ztMa(iE7uU(pX{qBpOcG|z940UA~+`VL7rGlit&L`koURc&Z9Y^!bTgNCg3yT5_~<( zah)Kl$_kzy((alxDKzAe)b@4r5!rcMA2F`&ZICjnNV#Tas3oQXG8uu(57}%hm1}vj z2$o9tJ(E#BzW%_x91(XU9;6~2mYMLSN-TzV5v{NE{EEy6YKa==8L$&nE7jxB#>?ic ziXaxjFo|}HMSs)n8%HTEuEwIdKBmxVINYlj;OMBr2>mJJ_}RIRcBbcPz~da9$c`8a z3Kc^YD1wa=DNm|9CqtK!q!3X&zKK_5+H#gHlqRhPxQDdL&RLb{aUqZvYi`-! z8}t;k8}C+GjaP+6u|~j>e~fu06swnLXmx%sVN@8AVLBIRHGF6z-cL?Ekv1I|BJqr( z1nbaXhzV(R97@XVwC$d`b!%)9K_oZEq*JMOdXRPJp)T_ES==rShQ+5tu~TP8183nG~(HCW&d8^PttFNRBI+36q_dL-cAHABTI& zr9v^49){55pb=roC?W<#qEnirJXG2*da{MenmT8d!%K2EA>_hMq2lW>V>Rh@$Mft^ zPB$S=fk}|zCJG;{3wbUR zV65+)v&x1%UZq1RP}j|u=EUzj5plAbcI@dXpZP9IVI`gBo+@31plb2aswO*XLH?P z9?gZ51Ad@*#uA}sDvsyQ%c1xOB%6%p!)baNuc~5Pjw?l_NWu}JN){?Zg@(loqF5k1_GeLK#Bq z@o8C#XQ_)PfPD zkq~NSF3k=?5MNRz?pCUW((tr6?2_G7CFGnRosZ!0(99PD%a}`b5##I2*_f5Fr<)l2t@ylz*QE-JY2X=bV56G6}@UJ+5@u; z(9H?rq}or0#%VMrg-d)NK^h|3hl6sA@Ye=u3o=J=G|i`y&7f9qU{qSbBjD#3y>NYq?oc)YqW*Q&3!<+-as2-|$ zpkOIp(vX6ehYAk+Ovl(8P|$jX!=VX?n>~ps$)Xy7G$d63v!r6PgNg`(w83|SnC*+l z2zM?7Wg~tJ8-cPkc{LM=_Im9yT||m#lZtUZ2K7<#b`e>t&JplHEO`>5muuCfaei3J zL_j>ZIMayxnqz;j+Kdjo(STO$6e!1h#xRDHs5pVE^gwOVJ|;*^QVo_(p&4XUVcAB> zTf-75KBgoFjD3D0I+|9i@qRT%G&DD?_$zo7>(|u`ImH_BwB#%1>y2?#U{a_rzV4b^ z*NqfVe4LUsEX##cA*}4KwuUg8tTpp2#>H?)#XU6C$MD#gNQ5iCajjmIBT32iBXb=H*Te8iA0`{>koJXyhCEg#Qk9j2x?2NR z{K6x*>g#$FzKUah2WJ!;_ULAgN}^#DrqBc!3#xg9qu53+Tb8t})DI@4Y&W4*8QX8p zbjY<cl`0i|q>9#noq8NEtG%?hL~3logN7qMhkVQ`MuhlclFLUu4N=Ts z0pJi^#g7SSqZ?^;f;J(4V@8!9{Kc3E$r8VZb3%|xdp#2Z3UC?~?u+We$l zE>mO@rIB_<>_O2cuC&6z5vxK-6qk!xzqbW0Q;+!S5JZx4{`ijlrq2IOyb)ag)K2^6 zbW|HCswC5HEGPMUp_Z>5Dof)4xWgHuR4mfQ>y$TzmZRt^mT$Yf<$CcVj$ zmP+QUC@ruU6ALy&VKOTdXH0hzX09!FJLhz+9$#o}1790IZC=z5MtBt3vk zIoeXe6@DZ@qe?2`&vlFY(Ozg6YV0CS1uhmNfoV3H^X~>DZE~6mWt7IWTMBv6&=@2- z7cFO7mpGTE&%xd9BC z@K~1UT~;Qhv2mq29u|h_K;FNVNypR2 zoYRgs4w;>P60%0iZm|k?id1nR@rIc}*J;bsTO02F|4 zi`3eNIW2q6w`?`ri2dG_dFGkA6 zgcL(0A}dal38^qjrtqv*6PPTzPla;@q|_$SeS!+?3&Sek4K~A6tDlZlivB{sq$G%1 zB`ghEIo=;dj5(rS^yBz{Hp_=rHmA-Xu-^{;aO>x3XMwkgN2_{gsRv^=acS`UTj>i+ zeLrx8vzGe7GlMakxR9s2sF^E_XD{7N+!j1*QgSk4$6~iP9;D+KIb}KUx)?ly98VVW zRqKHBlME~d(9uyR7x3o?V?OnTTfvyY^y)hpj5*-(&tg%ZIXCl&y)e#Vu_g+K{ZqD& z_G)4%;emb8q`#y_{Hmf2vQPxay`h_)o9)tmv^pB|1Tk!K;Q_rn+TTyt+JT-gxEm?AC%H)>-9n{UaTJ5Q<#;dHnR->NKNM@lMg|^WMLrUQ!A8H4x!J7f z`u>A*WSQ+hfMCofeD319n=^wko9S*I27Y&Hf57Yq7UoW8@BDp-I*xB==P#u9LQ8l4 zzKvkar}`_P>n0~-0pL?N-1!@H$_ZY`YdLy6RS+h`rboU6yQ}3pI0&{8FX8=|mPu!$ zslm;!iVC%8pKtYZN~p#N12Ta%OA;JXL;gg&i}U+K5DFFNqN?-7nxbkF;~y5(Z;yBi(o;nS<4 zGki(^UklzbkL31U8dhgp@gTUjEJ(8$;NID|3yv&C($btjryB3c4FP>!@YJn50x*2z z6eF=puYnUduePc|qTR7Y;CpZJO)Uk&M1J;6xN-%j83FcYl|YJRMUV1$S= z(P1!@B#BnH85q+O!q=1=Bp>&)HH62Bcz-{W8y0r+W!6g*r2*70!E$m~EKj*{FoR>& z+-Sm=ev7BBY2 zK(AW#GM(mh7b2va+5;G=y}F?1Jh|5w^6Smx%y}+!y7}L0CjE*2Vbe!i-}c70iAOAF z&5OI7EQ35Si@tkqXu6pdzwNwR4T+s?uJK%_Bk7;)%B?5+mJz3puGk0+*L27*VOT@X4c=EKpvHW_2NVf^MZZ}jEN zZc3(M2uTK;Trn?*!=}n1J_qTzfBl>5zqWp~&a8XZE%x8C|A@V3e}#Q>?Z2*lbWK}Jt=(n&itXoY z2ey*!UhCgkKW%-&`a0`NR{v@BH&)-b+FA{)u35fd`JiQJK`hT$`4d2J_Oo}8v_0%x zJv%joe>a#%8DR1J%Jtv&4qmmyNAvJJ?^M!GEga z0AOz!;8exs_YT}kDEUuSTw(9vCCdP(DlWZuu)B=XsftVP9XzxQaH<|&y?5~7GQg>N z2<{y`uncgj9s+v@_b&nXPI(->cktq6fKwF*?H#;m8Q_#zclQo1F9V!v-k0|d?pp>p zWu+JF9o)MF;5}97JNFJQEd!itd^>vw_bdaPveGm54qmtnaLP)~y@R`#0ZujVjlF{x zECZY}t99?-`O5&OnzwE5;Cagci}kSa|Lz?;cNt*OA2z!a@Qh`EQ)d10-oef?z$q*J(cZ!KGQg>}^qYGJTgw2a3jLYAgUuxX z=v3qTg}npUGQcS-eRA($V;SI7YyIiH1LrcpDYJfT@4&GPaH_R_XzyTs8Q@god;i{n zeHq|X<9pZM!P<=hzVIote*fNqZ5iN{l^)qUur33fD)eCQV09VbRH0jY2bN`kQ>_)u zW>%H~PPNw8?mhn4GQcUbKD_t%qssuNns;gM@i#63gif{A^xordSOz%N_>jHFA6W)C zWu=$yJw9CqIAx^=_8!-k0ZtYA?!Cv=C4f_l%Z=Oh`~R0#Ix9OzI}dGtavR*WbB*+5WTk#M&>cJz#sE&1?OTHMIJ<)z?}+ zYiX_g0rV~?XyG+j^3pzr3nRu0NGYm8LefBXmVlAc%1p*^gh{ zPzLfWIJ$YoV!67px)Kn%^!2>E;gPBAz-0nufd*4_qH1O@B zFcYarhZd`I z6X9?IG=Zcc4&Jb)>)P@z-%;bK?rx=5EoF#8!>5fsK{>apvQrdERS*V(6A8SHaY?;e zmE&q{8CJHuS{M7K%OXQSokD>2DlM*4rJCiybR3{HrpGH&I3!7N!Moo^d^A19m_v{S zbk>%ehS^-r^fOGunRaI~9N`IYTrMWEZ9L*@@1mrF<$6PWFk-}VX%{M~5-9ZOxX_&8 z9DvIUef>CRE-fdZBOlXYpDH?@bRg*o`a8jlSgl0H84-lc$m6@cK{i=v;fZ!!%rI0o zI&4fCKxcSNhk2^#m`MC791Z{*-5VN)*<6=ItkLOpI0a&rU1C@XwYhL^%p}K1tPfZC zVI$k%r+Fqn&c~DOxOCVDy7XR!HmvK6Ud2S(&(@&?H0r&|>kVr>(W{Wftd{mu6qeE2 z`CWCK6JygbH1X5jHYK;l{y_)x4W>ge-l_||z11xvPNmJ1_ADIW1;qv3M3oQ8{2 zRz{kADHQ1-ok7=IsZA%+ArF-5{!AHGdWJunNI%*-YypkBKa++vp73WAFN-^eO`r*+ zGILZ7Yg)`?R%{jqrF^9a?+W`E3D+clpd8%Ae8GG#o+=GuEoe6zmeWuzKnaH&P^Wjp zgkg2pbsuJOkGc+7piJ*RDu$IU_oT(;jG36>%ps$*0YxtxW^>$qn8`fqI&2&lx^(`1 zH$%zW7WV_pBpwCb2YpPZ_*Bs`5i(mid_7>JH@|{mHrLGpOoV2Z4(mXd-fQFy>pG*? zFcFEkb@)1({*la;*$96{r{?OW%F&0e_4AI_`|KA zTNXIS0&h(n*=HAo!2{){mSYfz+%IcVp&vl2MBJBb!IN?{QW*38MhlMbhO0z4U8uFr zZ_7rnDW3VL(nO=5-P1C2A6UQFW#TII+3y!j^loR}(>m*c@^|L?@yTnEXTOuY$@^CU ze*n_sOPqQCsyvL0iDpvVPYgW}L6>?8LhLG#f=?k)*&m^SA_U^z5PY?F^H+Y^iKj8@ zv|2G#<4P^Q5G;DTAx|gT6x&0PF0Ih%WSP*IN`X+C2|nXXGSybs8(usw+k722stade%bz!qcz=^-eI*|Hqk&X&X>-72>G=4B@^TJS-x~~ z*0Ff;@jK&F_g+T<+)D-1{<=P0xbC{+Q~Ufe;0r+d-TyP)Pxe>oN~q%zM5@9k3b{eT zm*Uecx!vHp!@@XS%J_0@n3ZH1yXi~Fp}Z1NJ-z)w3d(2ORNS+VbmUepkoG6gNhu-@ zMa2(8CHvsCh!>7{kJiZ$afDvZDvg=K;`y)T&2eGutF0Sw}Iwt#B-~jvm zX0=h+?d17Z>E<7Mj$`bUB>Hr<#UayNAj*QwCvi1g_2-8Lyd`mR5DKJ^rnV`mR0=3k1XP z4MW3FCGg%cTuw~#`DrsB+^0tkGC>rXazfe1$BpucQxG^>K%mOa4-Fn|f>G+U+N{WO zsZ^kwP_o5%`02jNYn{@l6W#ACgj(EJC$UO-mxhZWE!Ug&;fRm-Hb#P^?gl}mm@q6< z^EnXzx>+hf1A}WLz35f8j@)z)EBvD`CRbK2%}H6&zYEf&V1Cw z`N+)0X0sVi;d&z(%z0jOubEu4XZvUugff4Mrf-XfQ@Way0Zr!Tc{dl4G^~GYXWp#BX+S76^Iuu#9K!k|fxwHtqK(iD&_2LvLW-K1&xGrQISY)M9Cm?Cl1crFe+z zYB#?)Q<0h^L~~Us%O=%uE6i8g4N(r)Fty!CiE&R$$wNhOqF+#UNwN!q5;U0-U62x) z)D(kN#!qw-!)ef)YX-npXFOKQKv5!6FccBwxadQ!-=P^!uFn>{`c&Ois|9_@_up}} zuJ zg518~`Mt^6p;fQj!nwiC-&5D7b?P_Wo4}zPZuPK8D#Cz#FN0W)Afp|&N%)5?utT!z zYa@QBkt(VU1RV`~J@V!QKh#3IN>?_KFFP7aihj!uAS@L^-mjX z=kik=e6MW|Lt3Y&ChP$=TbI4w{f>qbNs%;yZ%o5*X*gaAGUqd zmau+c^&eLW%guXi(xnmZNqg z*+mCaDMbVV9j;XKN{s<>?ZVG2qQ?2x7&ml!KlP{j@1Opm8AalguxO<{2umT3R0dqJ5kVqItD6~R!=6~qL&MD;>Tik=$I918>hs3)IXZQAKMCu< zKYHJdI4<6-1{`0!z|nEp#C>9u9348lAOCgz_lJcWaXe+myN_O^bG+}#%3iL zT3%@~;aD?(NntitENOH)t`W&GoZT<&gM2aG>h91VKjx?(%b#z>b)DM>@0tC6kzs}} zKDw+k^ghYZe%ZuL>0^c`Pa4l>YS%gb$lvO}fAkB6nZD%cKAq{So@Baq*~AU&W2UE% zW6tNhrZfG(SM=XM^c#lxUOu{4=ez$TU)yC9_q31sUK@Di^Lg5IhVOgk?Dtm;^L+8q zrG*w?y=>x+_c70tm&49yYSlUZ;7j%2?~x5Nz31p2o#}~QY1Pd2?_;L(vG#n9t2(|Bsu<>Y_t6U%2Otx7$B!u<4?x8ds*Iy!WRNWshS{i!4;CJ<>D>O!tabjIA2iJH z($U?z6|WnBqRD74I@wK9{jxR%x2~WYyk1)ci~7OL8LS*nsfHxH z5o6it*n{sT#p|m&Hc%5?nanetR&vlUO;~Ngj}c$64ooF2mXI^LSck z_&9bWhNrA}>F9Y2RlR=M#I^ZjhR4_M&#&s)2=t~$bdqmQ87BFXqvz@*PxR~d%O-Bx zACo+zUq9EVvvKGRZ_)Wasv72d`RF-1-xK}%+GP{>^^f_U=-1EXIrDIB))P&C+Az-t zkDjgb44)i-OkDIo=DF}x;QU7L|FQSxajs+ao#?IQ)~)3(5cZHjS7X+~lx)k3*kxOm zEz6c=OWs0bv{{m6EtYIab^|v|=P@LZM}exkszns&%~ia zvd1Q&bDMQG2|2$oWjYcAGrjNN=~Jf1CZSkVFXKFE?@?Gf2BZ4~B-F<(s%14`NFI!g zUIepUA>M3*gN$D`eJFj|;Ush}$Jrz__^v6(!QTev_?&~MElxs9R3O~x`Kh|4bbg0V zxf$=C{qNreX1IIs)M+aon}nA53iq{EoIeGf`?Y8`3H4q$CE43MgJg2?sTxc|Pg&^d zt!2Jae2wJnDd=2|vq?z$%9LZbeHO>Xm(GCWw=8hnT;}cPk{oB#kf2RD3ZFQO<6=8* zgXj3@CV%P1@89^D8*jfM-gwoGr(gd+uYdgdk6xc#uU&uX^~=}(=-SU+yLC;ymcI7v zt6v812Y&qO&8y9;k*nJ~f3ovSJGXa?9en4$?f+-{Q``S~`@6Ou*iLNU*!tquf8P3` zEoY0|dj94=g7*j?2AY8U@~;e@uy~>co@jw5THuKmc%lWKXn}9e7I?uLwHk`V!Dl)+ zl8QnJNJd3pU3kdf`MqGU-#rEcviS(w)pd8#+h>EpJ`)V~=|xyd^=pc}81z%YV4n;I zd*nDQ6;E~r9ZfF={heU2-#!jY#uHMnUA7jz{njxUl#0iSx}9ehVgE50?3aSUe(^Xg z8IQK|rNN02@(aOWKOYSCkwsWUDfPV*efo33VE-W)>}QX|5^=}|Pagfn=Kc4}QU{;xM8UY2w5{`srY>4ebHQ6?6HzCRf3eZgSwJq}C3v2oV!O%{Xxt6;Dn z2?qO@$6-l0N^vE>wixsWgTdY%4E8UM!{TtH!lw)RV$dH5275Rd?48G9F&HY>R8?9G z`hCG*-y00}j^nUs3?a#ZGF%M$J;7k#9Srui?VXKF`IX_N%YV4>s;Q3uRMG#mvjBj; ze+el2AL{yVecQ$(R}R24{aXo(dS!WDvzi%hG?d)t^ePGJ!@*mz=`>cg8 zMeh6_=>L~CSb6`Y$M{VJw7&Yl>Z*JCE1Gy@aaY2sJvSJiWw7{8XVsq@7-tzQey(5D z=LT;&!{CI(?&^c*7%YD6SbgK%KszG>ID-A|)7hiQYHw~JpJ4zk%A&4z=LToIERI9R zFB*bYg>wv!OCJIQes1vQGblxm?=S$@@TR%J8DFj9k>ds+f;cyL-RTidipQ?D=LX8@ z2JsUjzN_-wpn19hj2sua1lQ1*8#GQgh(pH>z%|t824{VhMNSNFW^QoC#*KJn@!Q_2 zGB>E5Md^41a1GVD!Rya5IFUdP%nj(%4PfL%$5rMAg)@-@<%?&zd7$Cnj4(;ofkQAUn1uQ-02bGkrTJdH_Q!^ zryGD{AdlTDK`SNZ2K#3iBu{kQ%jO0zJHsG#f>L;H@X|935+_FM-rOMY=bY7-%ned! zL`a@!*5usa#b+6uxG%kEZt(K6435u0pjnZ*!C7PJ#OW=ok-5PO&Wdp2zV!UL!7I-& zNFApHhW9Jx2B9+y62~tZ7(8!oaP15O_(Zc_of}*|!ytbA8bB-U%ne?3hQY#_On}lW z=LSzY!{Edb9jiCy1~<+yh@WVs>vMz383xFS;Z1>+ujKir%foVDFIalDm4$KC(+4Ne??w7NStxbJjIaN>Bgf(YL}H~6-*3{Ld) zv*!lidX~ZQegF}kH8*(HSq3Ni;hA%TXPjklq92|=LSzb!ys|o z09?aU<_6z#hC%!|B@p4sGyT7nAHH<$DO*2y`6b|&Cw~6Pw7@^#KDag$C)!;JEwMT5 z{9Qo{I(yT4q|*v;Q>SX8kimC;O$%E>OKfhM;g{I2b$UVVG6YZ2L|NEWH{~=dRhl0k zdw%0i4bH8`IkCi^THRe)JKNVet!H{(r-PQMa;F1|-WC@A-$135WCWDl0g9&I(FSNcSnCaxjhm~ zc@aIZiO)FB`7Dr}7f>D%pV{-=^t|-hubL`ocYe1ekrSp@H~Y(BB`&zuX#x$yQ7a7iIuKOCf$nVjQB=v& zm>c(8vruchm^9ASQ6nRWWwuY}GZDW*H;R3%ruh|FHB3?~3UwjhD(e~qPO4!YLu_)b zQ8(4=oBSBF>P?j>%f%QR?0EFqrGwkeTit`5>Fwt2cHzSM6dt-p|RX!1pWQ>R#*9ktA~Fk?57I?DxjT zfT)|fEcRFj^S-;YvdrT3;xaFCGV@N!rBkwFUYhneO{=qnbivfBn$ub3{@83u zJFQbN?diV*58gL{(R*i_nXWm{Ns4Khdmii%e5SNZ2C7d=)h^_9wf0^fqwI94mnbn5 z=aF5ul&P4lJ*ci%`wZT?=fN%{;+9%Rq+Ct0>ba!plet#bGWH5XFK-A)Bd>P5a2F+N z`9=++V=lC()N3Xo(Vn?SB_)YW#Jsxf5A%|%w77_3wYpfaRc!(RmktK|bpQVwRxVw- z@wpoxzVWsjZ@7`V@ucfty#CSa@4jwb&tJdq+E=eVa_s}xzVll18gy;*>gTV1n$L+`6J%dF_>FZhUFu;~VeU@HZ+OFWR_t`E!>)eEDsc-*7o~ z`AO?vT>t3$yVtGt{Q7-sUtN1-?E`Dyxz=2R);3o^zxt8YTdT_IYgeDS@}-rJue@i) zU#YCT2wcED{=9IFUfm^gX~pyjOHU7Swuepd%EFV@_1gh)2LW+E5D@opK-@b6;%)`R zeSbjQ_swucU%>^2#~S)j#>Mpd) zwJ!$5{Y60Bp9jSKSwP&M2E_eIK-?Ds;{G@w?(+e0|3^UF9|gqy*F)Suul3X}*DDwO zp*&D}3;nxse?Z)BK-{+m#62q@?wJ8`&j^Tn`XMgeZ?LpVwpg=37ByyZx9-}L0^+U* z#9a-D+YX4^42atZh+7YcTbbiTs?{>6k%x}y$-rZ^#m(fGza=2<$pLYP{c&#G8v*?t z_S?Dst_AdW*uUrc+X?9Ja2%ZLZ)@&PXqFr0E>8+ACtYmk3)VuOU!wxz$^mhufH*QB zt{4zU%yG>UJ3&dSR4Nq*Cf#If3)j2$nSi)Y2gH3UAnua^agPMV{Z2sKZwJKvRzTcu z2E=`0jw|F_#nMEpW^KY#$DX&iQ+|6tAns)Wap8ctmj=Y`1;o81AnwHhaW4vpdtpG_ z3j*Sv9}pJ`h3pfEH^1-d z@&tk(qa1|5&Wykk%5BRiX9Xd!y)=Of?t-o530!O!Y|aQQAxU@9U2tV-0vFr`8_N^8 z*enCvdS{usS2Kgn-~hcfrci1TMG>F3kuCRhBHoN4ey< z+tg|~xeAS1B|cK81`*7tKsmH!C!w5z6jSU4Pe$*(Id6gBa(T<>m!1C&>do(c^U?&C z_+}e~z?+sPu*8n8AOzkxBd~U4@zj!@=?H8{#uKvc=H-I+)Z`*m!_B*%N zx8Ae$l+6!rqE|k7CA;zYjoRhEy!@8SPhS77wZC8cfwhg*A6|u5KDLqsE*|5bw`|=) zHzfLn4_pfEja!Z4prHzUx!c_*C2%UZh}Wflt)?}_ew-dynM4MQH+?*{tGhCr$DA%c z$Z8dub-Vb&yVzqYi)U?8S*$SwWL|qnhR(LFNikD12$Vn~)gqliW42s(nyynA#{IG9 zdR)A+&ssV8F(RYidJW+6nroLrQql*9uxbSa$#X`o9?kR{@&15T%JzgAU<136ma-6w z$reus=5lg(s&wnsAl^QRhjq<_U6rX;RPN=G7$GymdYW%*oFl*t)=0QjI8m%7$6VeC z9PdbcSHATs5GMxWB!-E-ggJIvnxp4YsMU?_?}Ke&&AkDcH}@hEMUpjKZHPHEc!%G; zBNue%D-XMq#A^F4luueoZICUe{3wq5qPx#VOKG=Mz+$5WpFo?4sbxxmFKNDU@T{@5 zSZK%Hdc~9rxa@wy@!WVNW9Xz;L1atJaY%uTjiX(IkL-78w{Kubw^yXgW?(M!J!0U3 zUw(MO5>#f0c#9S#r!2%Yn1icmAC}tPvAS2U@h0rZrB;*3Mym8~qcv7(v<{AG^&PTZ ztY_{inU2?LMG-&-IV6)()3}e@>wC6EslJ~r^XfVHqjX{!ec~6 zzm)=9rpbh4V|iI;ois^w4Rqphh{v_sZY_hjupq#xQKVG1b1hFJ;J{o?OQs}U>T5=B zP#V#8Z`9Sq3NLv>JP|Vo^)>39V+1aY_EDVJ$?5^+LpTG^(*-@xH=6mR59 zy`}}J?cSb^#N1S6g1V%RVu%-*%h!@Ac6hFF8S z%Vp)%0Nx`bLYh7Q2%A7N)A6#zD5Bqr0xr|>tApL>WZB~#v9Bn@w3Np5SWzW7hGzFu zsuxd?)xOr3(ozWy%;mK48v*gA zQ!e1LmpCdZFqf~5-|*oDFL930`QIG^jo-zRR4^I5^pMOF2L?SxWaL|WfXj5yE%EUt zFqgXz=FxAx1Voyq?GoQ80!KP6ZC?!HP1AOX7xIDQ-JP~CI&8ru-Z}@4vyiqg1fizS z2TQ!(4IJw0r|n|BI(O>@Q!b!+FZBKOVMcu|ZJ&R5!AqRJa{dd_Kxuot^gN1Kw?cr+ zi(ha#ChecR^$cUcC%Jn{2H3(Q;Kt@(o|&nghX`QJ+%opk<43|jDf z>Y2SuysDd41}#&;HN!7k-+ZFNaf$Crr=7t&J#uPF4)XsW0_SFj=|G| znNJlDXe%aBD(>ecaIm5{5eR6E7UJD_t{bgs<9v~&_A8`-_64`<-E)~$5AIuCimj5U z;F|@C9VFd7H=32|wbdek{pX;4Rle0?)WmfoM(ipK+YC*D+x<(z`Y zyvC2PDT}#OV{c?y@$H{4RYT@_SxE`2)Dpndv^~lm*H`=NmP=`SU`0; zk;9>qFsM^zw`|yOyD=g6RH>eo1LfuP(x-WO`QXMZFQ2h;v8%|Sd71d8%*!Pfz^CQq z$+h+4l%l5)0hP**!T|}4*{Hr#hdeEbKPN38O(I4Uk*V6psS{+ajP~}le8TC~OlVN; zJEq=)Yz9tn<$S(T?AEJpjooJix+dIvB2wbSMhj}MI^m*wV!ndWaI-ui6Y(C7XQMqW zYPaoPovv!boFt+nT!ls@s2%BMBeY^^qi1BaAL|ozt@w7xddgy$d67XvTrV0Vpi0`ZOL3}6grpw01Fz+Hr z?97E}MckY0W6e>#(lRT1YJ;3i=ec$S{SH5(ma{YzdZ`rPI?`>b&`s~(+wr*}QTQ3IZ`+a)FT*-pkflprh z@txi6uiW_TjStlHv+}G<|LxK*Z&t64ua>Vq59~nrt(_mc!Q6N;s7(0u^$%Qs z)Aj1{f3^MDgX&Ax>DATU{K%nfa@c4yB(KwLUtc^UV)HYbp9u>6^ya67LO-?nsY58= z@^|7sw1%>|d)-NAI8%&=o`)tw3I`*(n`Rsz%D__0x)u7P7 zzw-Bo&}P@h1((S1)xvO0ll8?zQa1m3^RI(K|7!EEwtsRZ{KRXMfGu#?uDT|mC9TwJ zR23#ml$`NG$F7JgVo<2CA_Rr~ZH~;t$dhQJK_yR`OgtM6WY>XnaP|LV%E>%+?*zVXHlc;h42zI3gB^@~@i;|yl(q*>!C|;+h(>N*&^3Ju=bwKkFQ+a%r0EhJ^k}7s~ccn@8)V~BE}KAG8p?M zDxXp&RTs%vZGshXBb*pWf+tQOXPDK7BU!tv!&0raE8!S6%r)8OsGTk7 znlxBEe)ihajyNPMZ9CsIi)WLa3I^HBdcV zYe+3FkF@hO%p99Uyh%C@l2XG|TdOCfR92`;rCnb0#;TNWd$khL9o33%eW4w$JaWV# z(do5(7rglLF#{%b@J>2oj$%=TqvGABPqw}M2u`?7H;INE!p^ht2jUmWPV z7)vx#iDAB*>(^9_SF0+M1KW)q!WhvlYHW`b)@o)QvbDfrIbXP02qD(KY zm{Ll@n}yzR2p4-Kr_-Wbmcxc3^H>OGnzkYhQ)VGOnT*28c07?N@kA7=+nH3VD5_D0 zw%|s&fuXw`>DvQqKx+JCT<~$((iS*e4j*x_sC)#{k*_oHP_jxW-Pcu{- zYbJyV#&#?1b|F+JTiHpuKHkS_IK84dsXr z^U1#5gVQ?8hg<1zm?1`E&XNU1D(w0|-+N4ybC}4b@!VK&7B$zl%_9y?40t^1{++0`Zmni4{zW`t~d4VvyUXq=8>WHy zW9Bi1NhSw6m2`IV=Pq%ceZ&FBkwm0oL_N6 z%#hTVV-q4qn4&-KW@}WzfV_sJXfz~rh!KtstxnbG_w}&o?2Z{)Cqc8d9idSqOSQ)0 zYs}5Y5r>l26echgF$I=rkrk`f>>;gmKI_4QYC?_Lqf}8Yx}!vf4nZ}qQ^=}@uSK){ zvadk$7zo_u)P6Y$b+CNN7a7BJ!R~Na*qxXRjVidOXR|DpDGqsZAuq0c=$<%~s&%{H zAhOaJ%mSRF)Dv|#9?rz6Va&kEQf8tdkU1DqXuK7G!v~KzG}~FFn9Nc<9!cw_BMgOV zr=ho~oL1M&zC_6Spf{q?RfxKnCT=D1Rnh78#8;;CUSJMenh zUZ)7{W(_uP`nH5iep&C;>egVP9X5XWhy!90O>lGT+3mDZb@OejKeU_uPSTFztwLln zj`#Ss*9d#I8L~Qbs*xC?Wznr?kyw2UN(E?2_Ep37No`0_d^>8idvQ>z5rD(N5eGMo zrOO4}j++GJCr50mk>_6|J&D6Nc~Xx>Q$cyZq1^<-6vfrsNjy4{LYLSys!BxR{+T8j)z6W&1|6c33o9eLQq0zxkxa!kVv@n9 zMKh1?j)sI&rboO~Xu1MZ#H~e*+smIkYKLyyrc*YprbilS0hqVtMagh9W+Js$n)XRWLF4E}5!(6`*Q6$O~QbxfnW|_L8LuOnH)nW`^ z_7f@(M}W&J}hWy*`s6>Qs) zXiAc(vEKHYT_$PcS|*%QaZf-cq3iIeVq@f&oHQEc^FGl}v0X&% zrmMIkgSoq$YZPg>tra|10;O3Pb)qjmdl!d>-!1zdPC^}=*VK4)R4V7H>D(~Zji&tx z-H0M%HG>ZnG(>W3t%&z*PS4b##sG~cCsD9#%|beb7SgKZ`FM(|)!4>(jO}V()7Nl1 zr*V>GgR+ok(xo$Ql4ObN01dQvQjNpf5@Wvb+0xdli@CY_ zhesUh^-;H^*gB>qGEFopDp7yX5z3Pc)gdE_Vyjq@?Ss-YoehzMUz?1aQm3X)V$~cH z>DDV9W5hP}up%bl0T=TUb&-d)p1kYU^_nR3y-H!w%JPO@F&5fk8i{iDMgayl4T4kNxGfSp`pQq5OTv|oIqNW5-;1Gu!+c2WiaTv35V%GtQSdk)uG$b z;oU)vt&wD-m-Pi)<=M8#EO1!+A9ry;DZ1P(^AVS<3ACM{I(*tf`*D?U5Z3nPQr>UK zWhzQ|p{gxwqguM{vdAQYV->fUa`iM(aeL_;uOzcWb!3%NwCAOrT@0Mq&``u)8Ljf8 ze9Imzez08o{1FE%-9XfwRP5Od*vpb}tVt{#XE=T$=WxmZXY7=WD&eQ|8X7|5wF%NN zo0Z-mNg{)q6=iEwt4GNNLP!`XnZ@8?j}vt^?(Md@{!lEm>g`@8?_?TGJG;;h>%6)uW#_(>Tj}umJV!JMy8+i;Wn+qJaK6At&c;)BCR&Q=m)BXS9 zOFw(*#s_ayum8W--*>%q?JL**)wTT9|Lf`xUCr$L&CczeS8xBt_N{GX>kC_N--0*) z>*iZG_pf~J%3H3yWaHBtZ`^qP<=+Nh^zU2$^>usw*=rwL)7PH9`k#P8|C3jKVMSQE z3S3|O4|98ceU+BB^XFGRhn6VMXKt^}+~NV;E>UUE++Llz%>{6~L_s}sdu8Sp3*dH% z`g!K|rJ37o0Jlrj$}0JlP`G0MWc z0Sl5`Zeo+PGwC@=9y9DQj*>8VJyD5dyTfQpk(q-x&fLB>fZHW1;hBR6XKr5;!0i%6 z?aaYs=JwSA+%8eO&K!(qZeJC^?GmNx%z;01`^o@rmnchT4!oJ$R|IgoM1?tX;LhB> z{QOegkTM33FlvG}2fggLIvVHjdNM+hSs{nn@kHK5S-vD`L=%~4%)w~p775_i84WU~ zD-3KoM{;g6A`}Q-DPozueN4c(6g^4W6}?no%2tUv7|z_L0=VtwRo-n!d;4x{pXY?U zDU3&rSh~+t<{{pkh|QhQFTM z*l*bl&+js!Cu9_z+!`4`K`zF@V=t+4nNe*ZfETL0HTP8KjwiuY0h~T`c18e3s zKn;EscwaFm8K&#MRA|9 z%u0T+KQ7mFuaW~@)}OhB0=VUED2~?gVSNu5CwYWUmKy?&^VPk9nx;osd=Jz>fc3h_ zArNz*&D`z>aGPzgmB~1t_Hdz`MV0*$A|o}k7r|U zx50HLm;-g@HXOk15|R8eGq*1Y;C2Z~2j+mAxqW^Bw|bA!^X-b;6_9KiSS^%5@{Ipq76gUHTiF`RfO%{rxrq$lg5FFL1cT8K9 zY}JrKJx(-^oO%ZE5-B#TIXY1xzpR5mX8>1gq&v!t&_R)=?PA6+FMcbVRyaA50WRoN_yon!aGH)=>PxB<(erk4)NqQDuzPn~Ao^bFu7 zhU>*;-UXil{0+*Edp!gA#NfTVB>u5I12~yXCC(h|97&Lagut0*i^Gw6ZRCQ_T|^f{ zq+*IE+3{Wi%QD%hUZw9{oFd((vC2Tu!AD?HNcGKfBaBE;$ZR5Scd0 zH(veae%+CeCfcFo&s1o5QyfqGN|E|~^H@dvtYH0QY3{vFIf*C)pEczGE#v(XRy5P8 zhTb+23h!so+!)OBj@?rSRFfmK#TF%W`+ex1r<_tD-_H<6sR1?&=2{dTB~6V^2oQ#) z3sNdpZ8rvkDlP+6<0@QCC$mVN6szfKRncH$Ur9I3KJUgxl#}3+F63vE=ANKtzzi5b zB4&E&APe7SAAHL+3nSnETrlSZKOZDW7Mi1BU!5&{x?JCpKtoSgLLJlS@RGvyyB)JT z?d7|bMWV=A$(cePkK|lda^mhq+le(7XnNMh+eeC z)S7%cW(>?R6q6%u!M3U}BM;SB(@RIHa|xBxb7$TW^S^Z*$#x}sQDZ2TnsPX?&R*g% z@Mxj#47s5M2E%te09-x>x1mMh!1*WxF*}+xyVaZ}lYN@u(G6?L^kwsont248Ox$o} zOAfe{2agSG2%7yTNsPwh5#;4uhZ0e9I{G6$y4+@B%^qZVR*!8D)0v{wO80Vv*dx7S zFB?nuNN7JFZ4Z4DA@*1?GtMYFMKxGBLG=ispX*BFW|tEi*f`~q-7eI~05uxWjthx3 zrF0aZ^$YkkeqEuBUMUx85U9h}lMTBF57G{VN~t7Esgh7?(w^nyDpC!LCa6gz(iVqp zU+xzBiE>6Td%dJi)*xe4ERXXtxIK-FqKKZc5CHKa@u}{{_WB1eJ^j*mudKXt<=Wbh zu6_H)&u_#p|McbXl~-@qFMapU>$l3QPrLEP?LXN16sQHzuYYy@`Kv#;@$)-hm}1vI zc=@L{b65V$g*-5SZ8HRsGdV?;3A2}RfGT;(F1vQVj~NQ;3OFJvLcX78N6mU3DumKQ zF&R~hJf;#&t&SFCX&7O_V#zABOOrT3B6%Wb)RmNyO6_{-G~L1~JsLMmbdt5O#SJ<; zKXt?*PT(1PScup@X7>h0(#Nxs98D7{WvhzJ0iB{D*3`1#15T*u^cB1`DYzrw(M?vB z87|-8_&k|ZoAI32>yKKDf<-l^)7m9-X~pyjOHU7Swuepd%EG3QYkNl=;7LowDHcf( z#X=u7Fj+RmVc)1C(Kc6(O~_(IO^@qHt<(zXUPEbdZL8XIG_#!>I=W?~idoegI^%k* zcU z71Agn6&p9Yr~tO3@%1s{Yg#E8^9;3-bTnFsf&GL6QHNsn)NUh}FG#fr_sYSiMD~9oU*s;Pnxn7^JMTItZHLy*;TQ>v~v$f37a8DHAum8~zhq&MN z!qr5&FZep&@w+{mfSeR6sj@VXO^e2IqoJG5wt7w|-wqQ;vC?@u3bKn)#tI4%{&YEm)6^I1ky5hi9i22`C;beA+c21+7HFBPgu zpa>}@hov}Gt5=oipqMbL#bLBNiAA~@u||5>z(tg7&-Vh{qmGJF2-MI>vsl&=sb;qo zF8JkQ+NaPFa681Ico(O#@kH8$CPgTu$5A&$Idp4O=L;oJrBn8kxfrcVgAs7yJI1&s zH6~V8E`S06r&&=7m5J1Bg136yWI`|8C$HSPs~vO+5wc>g5sN3Iv1B|q;+bKaae87` zZx1qMABi-$9y2Hjp=wPs{3OB=an^#{{;(jI5Zy2cjx=&+O;Kanrqt`%&M4K|E$X8x z)5UW#AV&1Bhum5ohFPJj0#jqq;3-I*~%7YDFgW7&F}r5W=@n zEtCvXDLQL}b*a-U8rGmC*|Bzboa>|oF{U(%lb+Fpz{lbUT-Yt1dy@DAJ6~9N$D

    xpIi6C6i3>^+=)I zVojGO<2lX?*Af{&)suXxQ}rm`=3x`Lw3-vXoy^?6upwdr)xt}rrdV8J3>PU+Q?L^)~M(UMkU;4;Bc{T5G+?i zNWI+=syGLCM86%whuUtLGnBHPG3s?oZ&$(V*G1Ws9p;EbrlEu=vm^TIQ*e0VOO7)8p+JJ3vEXgend6=rH49;=u9}v@U1l2863b$^eyHnJ#a4M%GB` zAg$ddj_O92tGm7IkS0c>F1K)l*#7tt2im3OsAI_P}$y;aw6ap}XVqMhYK2Hxz3KL1ePz2t!xU63G9k4Vh zj!}#Eo6aJKyC&~KGTBW)QMT0P{7SO~(<37zLSVJp_vTRSIsjQoE#!R_g_eXiHTZh=fVq<^qSyAHF_Z zdHV$p@h4o}dkef}Wme?85+Z7{>sgb2u8HE1G@`rC!0X7mH%_;d1P&t;lxvtgp&57~ zq&w`m6U|__(=l?!NXk^pbuUpXvI^VxYPv6v6V;?7Xi7)k?X@PhD)uB*0sEiQBah@4 zhC}`+D{7n;RU(ce=i9Vf=to+KZbObBMKxJ#`9!vni8FbG#H=hDDq6VTlo_QMFH97T z$u}8h;3{gm={6@_r(%LS#cY%g;3F}j2yI7;_G6fpEZVD$?Hd*Q6pcjc4*56R};p) zteVXw&;T6XKmGpyoYhAz-S`(bUUvN#uIH|O>e}nC{?%3Y>gvw-?R@L@`?sO3U)g%y z=HG38&*tuxkAM>ZAK7@_AU!ePmT$`Nx&pD=)qDk$X>#5f$l>uzH&*TFFpSmAwtRVrD82z(UZa;o2bxP@aXH{aJC%@TSx^%1`)bs z@CER-co$PxKixBGTD@s8Hr=-uE65ICC%*Y6Q`T zglgbPLdnEbGv`NO9!B#mv>L05^@0uUg1T1BA3773Gl8y!ImQ;ZDcl{XH2)wpc<6=m zP~lLx-DSIYFXKTHm-QntG-Ap)VUJTttD3QThS;Uqgp&gMZiU?uuQMW@Lls+TYdA}0 z7b{jz33Z<_n(lA74;ei4f;%yX>2fZW!Ie~DBFe&ei2I{Sb=Xaqu-YuuSg$x4WNR@v z#`hmx%+%na=iiChbcvqJ67i~2_JEQ^!Yd;#Qb+1cttOQb)||u!kx>nuY8X7K>r4(F z3f+kbV`bir76e^05;bS+CyIjB><&{tX|;Qhps13RDvTp$m~TC*n2Etd&$|<|(YE`k z30NfKGABhnHD4VH@~|Ck)hG><>k-dLmv~F;*p~dLV#Ws#-G3)$(dFV@uiNZ%gKn6# zw8@wn)wKKo&mqNOD~_Z(T1gT6Wy?=Js+jQLq1`(%k&c!lSsK!Gnj@l>0ofnM3PmVW zR-y{ju7F)59+n`Zkt*`&V#Wp!-FGJ@A<|MZETl@H3OQohcowx>yGh#71U-(n(_I#_ zduFx;^hFAf>N=x?hn{;UCS4eM4ZBkUA|G0{TeBlrCa1Sz{e(f3N-aB=;;aE%BvG)f z?jNY{fwlM6q7_fAta|M5_EnnsVjW9^0_B< zDkhk9pL!?e64g+TDkhk9pK>SW5~Wg)Dkhk9zvWKMCF-aiRZKAJKKV|}C5o&bRZKAJ zKIu-(B`UBURZKAJ-nbKUiSn#R6%)+5*YCt!qIT<1#RRkNwRy~EEK_%L{;trw?|z4~ z?$tYimMA?qe=E?5K(punOCP&*L>=_`<o5WHkNbfGJcn-Ayb43_X3y3lpV&4=+h zgC)E!Q{8?3EOztZ+?>G@Yn{-Au2F72jLjJ=;Wc!jE0&uNXXgx-@EW?%^~=qNGjj$@ zcnw|XYUbv{={bWXyoQ!2S`RW3(K&-9yoR#D>)V4cc#Cmhc){qQE`~gICQN zEa5e@L@j<02CtkmSi);)iL(753|?`_Ab8b&Xo*t%^Pe0xL2}Mu39q3gWCMaQNF0uXT1HipF0G4Yqb$S8)Fc&) zOKrMqHK?iwO<05)H^xLNQA>rEP#6fpAU6s1DR6F4@+(eOK*oXC44rZBmZOz>N zRsgr`&;->zv@G~mDp64wA%`~N4SuM`?dmNPO4@2VM%7*wK;S9o4c9X00RP3 zs5If)2CfbHMLFEi*g#XQtkg(0Qe!^hw|bqFVw%Y`Qb~;TaJcNjDrI%cd8-5(;mXYI zCjz+Tlg$yEXq2Ip3dEAgIHC9z#qj7BM4}x#uJdD7MkkCIYXP?#Gq=AHz-_5gFf$ZW zA&c6mSFccoQKUA(%(~oE8iI^rG)y!IC@bIs#=PtD%t?X}dktQecx! zq2J0Bo3yS^MzOdVF>yIlEo9ra1-fi5F!}KSZku3NCAM3}4u{1pPl8q=opqblqfyrME;5Nev;~dcA>b3Nt zUulPhc$>>Lb2S%iUN6~AJ2OmVoOUA)K0h+I=K_=eW!Fu+eFAH;HM+>Qbf+jR9^MgZ zno+r}=Hw*UQ4br82isJCXo;{$#j4G?=vI=IK@F&3V~)(+o(oL==KyY%N;m4|+XJ#1 zhq{SE&-CkTsxP(-(um>_D_w1|VKF`dg`6I9doD2f(Ex5GrB^%VPwogg~R|FsXW=J$|dIZTwwA)1#rvJoS7RTqxN_tIdLsHvT(kWV6z=GnN_Ve z(?xXn7Cblzx#EtCplM+cKZif>XR*5NaCF3w65GsdMFc+RWf1?Ki#VDgs& zxUHEnMb73t52*~RR4$Bnht*M;j&vqKI2}m;l_Fvf=w??UMbHRyfyrMC;1+749;S}y zPPSquwQf3I)Z0Cvl96w@uG(@^`Y49f`URgL!2p{JO#VUuw{AlmLalDcpEyjVfHn!G zA&v@UDUKExN8(#x3%AKdC{}2r%q9w`=R+quBxM|P`O3~kzjCaq3JN`?hns}{bphrlLOb|M?YE(HyXFy%B& zAQ+y&BRz)fc{a&kY)1>7MpHmrKF^romIhaa z;W&;IT9&QU@*V}!VJz03)HYxB%?e%D2{01o0+T-z!0jNFjJYrD&lC(Y-7+?d?*Gbij{!~gWLFAVDhH}xMkvY8Px8XetWE?xi)F@v8tPO{2WU_ zOm7mG;v-0GC>+?b#N3_>Onx|kTTo(1xp-7E#WFHbJ5Wtwm%yhi5|VAo^d7;JeoM*3_SY*86BBYCH+TS(K3bWnN5XO()t z*5a}W9VWGKmO#gYUem`UMJQ;FObM_pF)(v`E-?9_0B(yq+kgvD+U{3v39;MZ3@9V* z;;B-B$6{h`2=q}nHKX=&78tj4fyoaBa2vr}b|y-=Lfn#o`c$hj?5e$b8msrf5l;pv zb1Cp8BNJnU3Ya420+SyI;MVig-5$?k6AH3jJ3~rlw};Z z*vF%^PT>8WFh|hcx9Bb9*i@`QriH zT9sO>>4Vi926cRGh)n!;*e@VX!PSja-3%MeJd4LVyf6e^HW!$De*m{4BD1+X9dkNh zcdt=PDRI*6xMQWo_=QedGc?^QN9Zh7h$F))0P!Uc|WgLncYIFU+Ctuom=jOB4Z>>D}asHJ5_=kV% zg9p!=Du2CWZRKL$`7LN6ovFE(I89w;i8oYJ`HN-V5zX*Ryni~93Xxo(iZ>P5_>TLR zc{zES^pOnau3?801hC_MO6YFkk#CT8*)a-0jzi##&P?C$ZmpLD;C52lh1rbhFz$AQloX-pja-%;gru+RC5Z4|_Lx&e-G;Q@j$v-&uBPR5t zD?Y0r*UF7~@m&_sm4 zDHCy-iTVLB!WI;)rV~!b9`%8475L-hFbjT-z*Lb+HCz>lf!&>VQ<1*MZURgqNc2oG zPFrrUSV@TDwQ71W=p#UvuiZ>VJg;mJby-$E@ukEOf|HQ5gKSAvDyb#4rc#ws>@QW7N-bGxuOui4F5rwxUu>6A zL}VCc+(%JI2XR9k6cyZH+{Qu11<`RB5eNM1|D>w9_jYyP?&Q+4d>_U06n(qj^Pb;1 z=Y3C3&bw7=KAb{|o_3&+qZ2{Q5p0K~OajJpSwBMemg+Gdi)8)08u2;&h(8kwS0V*8 zKz7QR4&`kxXfClHD0OH)si~x7PVf3dH_`ysR-b3+-L(af1+ddWA<0>*_*x}4?{@}* z6tTqSiKMdtvgkN)+8e5CWd;KkOAJ6uM)4OwqN zQZ@+cpOuWZ%SFBfO5hoJry($>2Tt1{3H5pXba!j%rd`Hg*M7Bb+NCY4}N7T5=J099SFWC(-OsW#kkdh2;OE{bSH zUh-r9!Vza3Wwfxn7qqJiR+p-omVnQ^N=92T

    P?bZ^{Y2gT7_a=^17h@fU~5mI6%bN?b>=tllO@Wy@d*vCfpesk~jd#iiB zdnf0h-k?QGMSPVCOj8lXP9=R% z-5!s|>3}cS^k~aUBwWv_Dh+uT{m_Z~Ew@-1w7|5e&*62b%atGpd8B~OKrOhuOeF9g z8d3c+9rZyig3U?J6JKGu#buUT=$2b(gBJA|2}2T6s^L;HllRw~?kpp!c@74i8?hXo zbr*CzU5e*&!Q(z@xy2_ex43oCqM&I)nrPdTj8E3H8IL4DDP66Qd5)yTW)cN&P-rumW%;(R zMnIuuEL6rwZ{9~_>cQD#Ew?zva*Lw}EqFDDDb6n1srs~L2hLPoi<}sQ%X~~Jc;%oH z_R~yRf)rP6{Oy)oyv=fpw+>o>EE*LAcrUR*e_KR7lDM2B9r>Vl^NMZrtBxlD|J&~~^TEz#9>*(|~ zmRnqHxy9=SEwbJ~l*5*buCC^&72@>*MN&PjqoftJnrJC79@0qM*Tl2V$y+S9c%S7K z?;W&Ym`;f*HzmC0_GM9fv20fx%Y~@!i!Ddsa=BT-!)XyX(@5#Dn=QBad&@0u8nmG6 zz7CBqXDh`FnbEmqv&9zD{w&^E4mIQ1xCXbHu{Oo1qOg1Id2XS_wU+yM)1XD7#Rd5& z4=R3Qtet}rdI?_E;-w1R@bIuFO4a2?+>^+W0o3-mZScuFhGM870grLS?vL3y;3ODvm1+=Q zY?-+u-qA`i$flKIEZl1M?~*OlU+dFbv4w)`MJv&5{k!V592(%_64f7c|6|T_Q6K2M4$)1ucbYsfBc^$7V?siFMq~uq3DiE%?&? zyUES{5w@)?1M(a@H?1sVA$0HH_5NWi%j}SYhhv?m@DufmOjMTHtl^M7;mK8&Q3OuH zyv2NlY6lS_+z{EMpNT^;RZ{d~z(=7fs4ZeIgLH-^Jqi0evLl|(IXF%H!_(BVrD9#a$7UVA6bvM|z

    qr=Dmg-d0E=BQPLFtMJ?o9d9d?H;E7m7H(K;>i3wEr;HWS|HA;*m`rRKx`C zJj+ikyER1vSJQ7ee)VL-682Wls1+SGv&*%W8oySC&gng@Czf|pOPG0^+*7i9Sv8bx zs1ju%2zE2qk%OA?G+3+G@1JWXBx_snh8_G+^OGSO+nApYXWZLtQ`vkQ-rg+?IL02< zOjTl7XPV{1F<6PV^EF>lO-P!n?x=WsYL;d?aLwn>P$l4_ETP5ARW_@o3Y06S3C<<08*_R6dZ!daxwznA;$9pJE!8xH zL!AXdPIwe6`xk3ZV=6Dx5g!u{BA6?fh&mLzB5_JL;LS(lIxD&KZmZ>sITw8O5GZ#K zfdGar(8E;58gffpoBDxhE%!zIi4E`I* z8@1A&u)ZE}p6J`02e#0)s%FMsaOlC|0IAXZ5I>6}s89?9b_0@wTDcCz;e>UAC3*<$ zdOb6+8{;u|&^dh7<0BdCrG{$~xq4FHHR*+;JDV85tZS0z>3A?a>5bk(u7S-q`Zn;; zHCFwp-{ue9k&P0v8+YU*-B|5T#PI0d)XmQ&Hgu=xsw2D`7+ZoNr>JXkOId2e-3ll% zt`jhtX;rwOSUB?9zwIX5>|{Z}orei!DLz70>rsC;?sMh*OYs1gbHs=WUoCYyYDA6~ zfD=G9l5}Wx6?3Sdl4KG}Wc}eJp7c>%D4&ZmVj-4M{pS2_jiCcwGT#4B9pOgyUNQIQ zxmV8qY4)<2$7ZxWf85jC{fFJ1>EBPcY!BNSQ@@*1CVx9AO*}MF8-H-TGWN?cVb{-h z@uLrn7Dny|%@6;%{N{7VL2|x%P%e&8iDtUfEfQ%bv{Xq&IGROcaXwTDcT`%9X>mZ; zYS4w9&CPfD%?p+RAR$+%lCAr*>QaVqM(sp~q^j9ERLNBrnuw$8jq7MPBB40a+deVg zer5oi-3Ng99i$+oTUaDil2N_KAw7=GbyGeN<5(y-y&af}HUn(07IN-v zYQW2Hez9c$JG=xhvJ7B{m*A{E04w<+E#y%&BLinz2C&0RFmD;a4lltOeE=4i?z9%1 zZW+K1FTuV(01JY(vk5vczxlL201MgOb~XdpMG8WXk|{cnMCj3}A;O$whzF90c~fmp)bGr7|Q@G*tWL62fzI0 zqb&olV2m>Z;3(?=%-_G80kF3Zz=A!?N@$B20CScB?5GSF{{P`!SC5RJIriw-on!AD zlg2I{J9^j8c70;k)qB6a_vXFbz2x4L<{p~6eQsqgKj)mAoV{=M{j;x_jn1An^YF|a zGjE+K&zwIqv*#y!J_;fL=sjoc{^Rbi?Y@4uy!(>f$4oyk{mJQTrgPKh+Wu_2+jf(! zZ41~=n0j#Pwy8Hw{hulO)cEB0CvTb5CnJ-mO#E)*ixaC8!UQt0XZ*+G9~r-LJhhA6 zwE$`h-aUHLXnQm;dcw$qBexxT)iCBx)rs1nEIt#QM-nzGh0zRhpE$BQLnc}fjLQRMLlc`u*J5b zhKG@a4^Qp356{W;E4QK^o|9>HE9&7nnQm=GJv=AV@>bNtb244oih8}oLeO;a+_9G) zM!wy{6F73!7Odf6@t%J9R@B1+JN>e)s5dMQy%qI_>5gngyfzO~{d6npVX3nHa4YKJwX}W567@H?pmq{vTTyRV`Oj=cJ-kEMKCu<`208iYR@56-{sUW4Z&>;FZbiLe z<=?XfwQK$A*b&smr;3b-*Z4$~ zMOv*Pb}bvFU6{W~I4Fg~c-dYP zs>Q@IT9)iR2cM?*&-K~9$pqUREYySsQTIzBWHI8%b@CucSChOc?Pa42^(DJ=W(R6NKk5x7Xv`}>t3Y=3KlZIJY% zi-B;yhHw}yv>HxHNrgMJ)^(NQ`(6SO`T5uqjp`t#tR#NzM z!^I?g%We`=3RKnSTI?+n6$m6Z-ruhTVY_Dg8rAbkC<^CUr_&Eo@ll{&fy(-zV$*Fe zC@#wBXm({XtrH;)_PA>F{(dD0+tnu6hO3TvDo1r9usfRVH^^0>)b2$M-z#LKkH*$TyJl36S@Jzx_0l^|@d zF~K&ELjGP7@zYv?FJ&53Z&`5m^ksh@_{j!B{;tnOV5wY%##}&o`;{PUuQtI}#B-<@ zq|^6@@Y?bs$chVRi(OWAKn2W&6zXV1XsR&bVdGt}gY_#x*se0cmJasRG?xo?7InOW z;~Wo1^5r&_!Fau$>>=Ss)?a5GLI)`Y>HYmm5Vk8#uvNpQNVnC+BmN*ns~+cai!Ajx zwc%cL>QT($XL}L7h(%(VE>MSlB?#NAwl8@;Pc(2S$wlnlwl@Szd{9s|2g}h6tOHjj z5#quSw;0H%jOwT8m3}1%+Z86*2CFW(rp6PJGn(@R8kM%!t5u|Kr%;F6Nu=wM$W*Ya zwsm3=rC0iuAZ(YLV4G;+?t&6ypjyTB~AiMF$W0Zc5?mcqEd^Mv6YHw$v!lEB#6kHq8Xvgg;l; z!@iUpLPH3g3@3c3qbV@0Y_}C`yHu^$?QyR7Qo0@x=#_pY2%BnxE$>m&f}<8h5R#Jv zjNJzkPl$~~Ioj0e2Ht1_b@M}MT&U;4-1aL$*m@?|;w8m+6vkM)P$dyZNTAycme19= zR>4W*8Fx47bcd53jU!w1O1~0>t!sjAZ%?DY(;mg*LB5xvIGJY5TRZSa7Zu${9{XU9$qRXx}uCDrD`}41?teR1Yv7! z?Cmj*+$*Xc8iq+RB<-a^h&=HgsowMtp&eYdZk|p!d5rI z)`G7nz0$7)VN*=7wc!6qukO*FyQf>R&8 z(ys(zt8MRCWu;~+z0$7)VXK;8Yr#>AUg=kYuvJX3wcvb2ukjo;p>z)DQ`Apif(BQyH+8z+w)z4=*dLH@rO zI7kV5+7l^ZEy(xw@;4OkWQ2OR+D)F5mX&6aZ&qXhM5o!^(55M2Ey!*^5Y~bU9gG=2 zkn|Lk7_iD;?|crGy0{_!Opu^?L1)kz0P#kT8aHc&mK5m96WDpQ;2LNDK>RwNO5lOiQ}29BME4 z7yMk2PkE0x)>bSkss<}1nXz|z^}vF}=oQq-IHf=(8}yhmR{K zWf{rxxLvMtVt^FNS*_#NTzHW#I086j*QKzz4AlLh8{w7R)l>W7mD;Eko!hzX#=cp2 zrIqdGQm}v<;@qt=7}`D#M${e}0QnSc98V_x)@kfrTSmDXl@2z>RGJDL-mqT*R!16) z>2V^5Eq>Br5qGK@X^;z)x9Uvizy))>x8Ms#lG%0$++Z%?qMxnh%eCZ@2V<6%n#1e% z)5V%6RLVpWOFWl$uu#K+Lv=D4)0ke&o$i9G8#2?*1amD^A--1WKftloZdo{RvYw7mT3;&3kQS^%O(%UcWrLvvudqKIq|%nzxBA8NV9RjsiF} zfJy=24^M-3^t!JwGRhEaqZGz) zk5t&*Zu?<{J&fexL@Ez2^n$(X7q~7Th(TIbk=34ut*cU8^rNvRIA%tpuJk3%LcpO-Cu6>*}CXUn9lS zP^5|xQlz0PB;$6AJV@&RDs`jVzyAzLyOG2L+_r+MdWJB7d@udO+-B!oXruUpQd;I6 z&JD&-L+LH^8NfD+>z6>uEl`eWSa)|QDbsrU^u{A`UMXtEvXpsIR16GKOE|c7(1eiXKQ|34f4Ovkf?!tE4N) zk(c>F;C#B!@+~lWJWlHnxWi7Ts_Cx10xuLoHD8?LS=gIcaK*}*KpQ8W%ECfeN|36v z)PWa7&KZ@LczYw#t#(m12SN zVv1LNQI$!Cgb1iu8p%1GISNYR2~a`G?s9+C6}j%h{5x;E{vY7u-~N&7E<5UtU#5ev z`s@#1^GWqT-t6@InN_0C*j-jKav4SWS{kQoNwSG{@phoxR3W#I$C(VQQ>xUlJK_~; z!1%$Fo_E7hBRW1F^%Ec5bH}aO+E=4jsW@}51AfPEK6TL<%xa*|*n$;n!1$#1IPshQ zaQnYce_y!f)i3xIck}n|_55Pjr6(-}Za-d3-{D|Z@jhbj`__`Ow+- zq`&but0%&Do$`BT740+Dyn#eLzEELU!Dk1>I$%#NMv}c$CDhHpvWSDy$t`b8gnLSF z!1(K5h~NB|o6^_p`nSTT@BQ@L3tn-|HShfX_cj0L?+}0YtIikSz^q=?ZhDEIWbT4Zp{k!_^}xSSROpdwq*S#{S#$<~1Jt>68~*UFFd-u^o51&;SUKYi2- z@BOD2yIxC9$Y)=A|3&|J=E=bbhc=#uu3;+1U%s&Lr zet!4m@%OKM#um-op)ji#^%++PIppyL8(z9uK^y{y6hP9FQrRvGMI@)VQm$^IQjB=n zbW|BIzNT{LL#Lnf-{;@|sCv>PU;ftgAGaaZ~M+Ip$}xA|LS|t)BfRBX7wd~#uhAb1I9-`cK(M>m@Aae zIQ7SOUip&r#3zf@uX-c+XFhw%`DdT)$$KUtqb7uY>zK{E- z?q9xp-{%$RO3%N%_p#9{$}3=Z^Y$5Au*VG;zwHCi=idCq8}h%q_`b8-%x6#g?8E=| zzdyY9(YtSF?|9^oU%LcIg{RNhf<TvlDA#`cUQdl_Vii5 zy#HTkxTf!W$1$t!KH~zL>G+*_*;(L)XcpJ8mR@an^0?g82vEciB>auT{!-h{mW2W1 z#%n*k_jSMckFVb7YB(nE`q{@ndB+X6y>8@o=#-y5GW&Qpe*&}W>NB=rl^ZaA__V+s z)$e}h(Lm~r7r*}WA5gEFyYT+}IS-}oc<5T~fph=S!>l^{j4jyZ28@4y;a9KX&mI5R z^7S*ntNrLC?w#M>^TR(jzInmFq-H%a4Y zy`TEiBh3${J-?zp{N|g!av5_cvkLbaTd>Xz82|JW=}W%%t?&J)@SpEJ^SVaW{=mYU ze(^y24?p?hJ)il>-@HZmF|!H{=Gc7ppS3x@A^3XTw|wgNPkwdxMDDIP>h0IR{m&oy z@qhj9PoKZ-)2}-AN6hL{pK)0O1qLeagwm+`wPZ&N5R3>jo4CTvV+_$iV7jVv-&^^t#LCtmOJQ}V(@<{!vm%ipb zyB`1V7aaYOqrUZ-Z@vAC$GYFltStr0@(lZ)#h@MD$ z!JRH)k{lJYYa9J5)OUXNU4Q@QKU^%mZ|XgFefXty@@3!lyzOJZdHf$voveK0EjqIb z(m{cBSa1Kz#eY zuV3_i`&Iv2KDu($T|fBvZ*Ly?^_91s^Ur&(d&jS0SGBrdVOBv}CcxOmFgYFPgdiJf zHmYI)mRuoMOS5A|HKfQXRLjJ*1yu*>RM)2BKhx}8fgAT-_3QT|FZ;e6{P|sj$S5bvm|IW(22N_-5r3q&oF zYRka^;@4d1W*&O$jpzLH)#ATAy7GVS`OO1gUO6U*+~B2dJPG^uC!5SFNTUQ8hts)i zg^q{ZE-qLMFJ-|NO3L++Yav%_xt1bIMvy7B7b}V0HN)V;8;rqqlth z&UasOS^P$3^^5^yuTX5cJ+XK}c<@u_oSvg5E`Io2 zy zod5XqL+3K9AYBk(Y{BL^VEovLjia>Nlb!3%q)-0Y!Y6m?aDwRxv+OA0<(4#96 zZkfbuOuUj;6TN-SI>Scj5K#z3i-aD~v(+IJ^7>mz_&9E+Gj&sL&mt0+L(= zl}7|nNr`VaEA3Q>%*h-_$_}5;nGNTH$#CD?GhZTDGOtngmeflzh;Pwe=pn8~3S`Lj z=^m6R#^q2ZkS;C8V03V4SwnfhR@NPkU?rbk#)(jn=mZg{=uwvy51xsu-hh8GE4fSF z&5iSDM9gv>s)o@>)D16&F^Z}HZ{o#JoB;;}%pO2+&vJs%cssh;C8W>+*+rMR0!SF% z&z*ay6=B<%HAb+laUL0t)-qZhYMfsO0o+J}VyMggOt*)oA2qUtZoGb6XDT!J9@%}Q z*9AlF!`??y`3yy4K3NGd8A#AcT)=D5JRfP*_;NhSIUG98Fd7ljm_cUvBPlzMre(OO zQ5+HIMPm}vwJUkL*{YX4D!kBJipor^i$v3vTtKS3(4_(gjyz(b+!0kLj0i3gs6~v` zl%!5pyxu0LJ}$@ek;5g5UzcckF!Wcxa_s7f)mmTuom3WF@Msyi0UDGs=^5Uj>3 zY?}%mv3ahg%BeyHjd?gvfg$UqNK|bX$vRCCQ9IO>sdlFoXn8A|M3X_cKeSYG+f!w> zQ^tekVlL&Yl+uEVM%j;h(Oj-~j-JQSOku^iP<@jXDH>J-u@xj(+iwP4&*(LDgWXEfq>2 z!ZPd#Yt^_1(xR>g6I0ybP>mAn99hkX73|0>9W43cb-S+F>u@;-f<59iD68NLC#qGb zR0J0YEgpvoC5n~Hb(k(TNIj~1s-C>4g^2_fknw~cMqt@Vv{<<9b1Wnpk#<5chm<>yJ&;(_#_&DeNqowt><=Tmf^+}7E1X$8}DJ8Za1Drjuyk3Cxte;u+tT(a$XK9P!+l8YO=gD=kXwZNsD8l z9CXBwd9EbjkkE#*d^RelVllj#3DZ=848yIqgfpy0w|ga4?l?kqf@s7Aaw+P~Rk{VA z8;S*pCYB~@2&lz`sUik(ImpAeLveHG?f%fgc4NH%pD?0~?0wnX$L4m==4L)QbKD*U zWbiw4`c<|c+T2t7Cm)(5CT<%4|HfZB_NlR1kn{f&;Fdr+!k79hPgzCxj*RkWBt}N2 zZKD$tm)fT0Em&cs^ZM#-)S}ov&}M2>dgHzSL-xNThjRj3pZEcSt~_n^5(D9j4kDbi zQoLXd;lN{adyJC?y5H6M-;bHbc<$=O2F8mIVw{+_U>RD&xbd*PJ>Cfe=|l6z_it`C zjdyKuiaf00J#aQL5ydxdApGSS#`l94n#Ot7>V*bQ$3dK9^A_w^Yd8;m z_-sF;V+O_tzH5B{+&t3LRxcQm(p^@v)mxO(T?V@QFYkZ<-t=sqvig#}Bhzb|G;Qd* zm2~oJ5C@*6+fSo`?cdMqfB%bVg!@*{H)iqdgX?|FyaglOfm!T(&~5Jke~ba|hnE`P zKRWMlActM5uiUqa41qj)zGdFvXBFhp2E2bm``=*;AiYB%kD9k&JhTe(CB9G2B`OxyOL_%|!qE zr4~Tihd}POVg$Ada<>8Ru8;P=Z?gar9s)US#h`2z~VIcg<-KG(qwtAj{aNQx@ zLRFaq(9_ybnA6YS zU>fiI>bVBqb%%5dHvKichaA$k#o0fk-`1DW+wM1w^Q_ec1LwL!x&;@3HJs}X>Dwah zAJRW988iBsYfU3PZS|ZXDcxno=fI+r_7CZw{D6V+Q_nYx(RlwqZRCoPz4g6gb8nwJ zbM`H>=gr(O1Mm6Dp49Fy?Vg&xar!9R-`l)XU!HPJYLmxITs3k0`1{8_V|R~TvgbSXcw`(@i_ms&R?hZ!O=9CBhM=UHnd2;iK3Hh_~e z4TlpeT#IjsN`Vv0N~?Xo*9$@O7E+?sS4hz7#j~JS)^x8MH->o&>C(z81n6?f=rU`% z%Y(W-Zy|-6v=RVaAx2jj(_I1UyCPS>gf91Iciuv7wdx8E`g0ilF{b-FbUT>0kZ}zN zupji+7vQw%-UhcrhZEr8ES>WfQneLVdrhD63 zf`;?z&RdAKDXv^(z%e99scWC=$UuJfG@A?G6cEm~wx_Jws zCt%4gcw))OzZP~2cuQ&mmzBXQL8qOx#Xu+UUUU7gf z``UPs=`Ih^Mhn3|)fGGFs;`X~n(k_yHd-*KfTe|>SlSCr_qMe*4jcTem`#c+kO9Zg z#+R6ev!;y}vi^Z#FFi5r^R~a9Fr|&_Q`Q4*v~XQgUpWuJ=}YvYX*iomw1wM`^2#FU zvMQcs(Y(v)XKiMy?(9_>hK;mgW{%6br!Y_BdEYi zR8gPa!XDVCw-64sc@T(&{e3ep^uaD?KsnU&eA~NOqrtY}9~+ZBfmVlxe+)Wrl`Hi_ zJYNA#vC;yOmzvB;yzvL)T58b_0XI(wvMT)e^iW6c z@m34R+~uY&$eD$#yi_kV3&{vv^9xl$@q3h_LV8?mZK+q%tIc*VBoKx2QplbTi=LFk z$x%WP0(7l|h>mEf7|h0EpmsyohxXwddjD!iz+zo?I$yNAT%cwmx13gkgt2}ty<7#M zq}oEe&0%`E4TWW-*YdIMddyP~_;9J(i5HtbB^3jarg%P@@X{?o^kLOpI1HS8V+%_T zp9ca#7O}Eg#CRW@52sCe+#Q(ufm8KzU&Q-?LjcxG!9JqZ`vSIhm^*ls>5F$8WLI0R z@%>au#xD+)+ykV>`0Aa!SSbvahnGPTSL2t56RhEbgVAgQl7m{gF3ia^&mpwy^~}I- zjK`do9X^%e5$OLbFEx^oO{|{&M7WWK-D~F_46e<*t^e8&h9|wzTY0i<66LV*)xeN} zr(x6I1cwW#Ag4mgtN-pCvFQBhp2--0?5zbbcun zsB+-~O+h4`MFqJ;N84q+1tTIMwgO!YWa2bJCp)qci3q~Cj13tinp+q5HyAolL=bMY z?}>0D3(~c7is*b+Dx#I0ViQF?@aWhOZuAtLx()ysdUib};YOe=kJn=)zuojSq{>{i z(b4i5rcv(XlB6E@)*57p4bjeaU3GJW3s!TDmOTf!p?Aj6*Yf|JJf@87`SqUM?mz72ryrkw^Yj_EPupHRb;p!r^3J_)-}~aZx6Hw_ zH_p0ezB%*qNzcT0Cb04Q#_QuFW6Ic`UHY!0MsFGQj(qo-^xC-VozEmR&tZ9P2G(Za z%Gqm~Ngfz`kGEo*G;R199Ig*{G#@sP8k?_lZ5rUYcy?sog7MEZo{i@#{#qLJw*>mL zU;#AU-+{+TOSm=#`f-4MEExYx_jBm&FIdv#+LBtZ>zVFq>m?mFEgBQRU7IlQfT3G3 z0Gh@#ux4C2cWt~s=;vFp>X|l~Y(3~3|hF>x3=DS zQ5HdQ!D4rTX{JGa4kXqo5FONChtuU|KAbP>@j|>4$B8O#8qcB0V5Dm?(37!620>

    )_ttqlLL}^3TdVi=C2MJ!*ZDlLY?Y&2>IMNAJ>v5nxX3H=l^hIkcQ93zNW z2G&b~h+KwOLpiyY+-NGzQi1Tt&+=L;&+!a#|@2IO^UCpTxz4sNeBWr$22Va&gU_jFc>4SF$dO z4gKsj`W zq5MR=Mz&l^IA?DzTyG%Ks0V;(ffqD8A|n7Cw9#f3gcuMhw-FVvGyawnE7Wp9+q}m> zhz*iXWP|Ml#v~erLX1YLq^j{LPgaDYuqG5MP$3Z1fxF^U!#3pYdRS&qCwDXN?E@dkz{8?lvCmze8d?_>}Qre@A2E!6%Ie`#TaF4?bi( z*xymucyO!nV1J)ZNxh z!4w{F&~Q*tSGQ-lpw`MYv7(a*0lqdC35PKYsUk8cM%r~q&84K6Tx(4BvA{MfOBb3{l(e$W( zg6a%2(W=>7Ei*RI9E9jvy(yPdY!lhoew}k_n$wS5<1w2*=1iN55XpF(C_*<10R{Gx z`pGD*3c0x8;^vPw>R`dDIayAyYBMFe>=NYgX+m0ydpQzyw`Gw5GI2yd9tW$+`fCgX zUrPF|N*D%Wg_=+eWkbHEOS0$FK#Q_t4YuVFr~+C@(_(VPI1IKkxrVu+(0(Sa%9#XI zhIlq=sS=@7J?SGTn?vy^iAqWm)3)UYx^>uDi~cpDA4sP$d)#IZMzj#4d-B+F09>;L zgnT7X51L(yRX{51xJ8s&t_*7B)v46ZW-+R&ZGC1R1^?kmj}PP7sfZ1usptPAl2n%z$G zFwgLokWxz_Swc-$BE^tmDsLnFwAG?H5ijo|0b85WGm$s*Rl?;K(@L`xF4e0pT$UKN zrc6IO9w;45aM^?>o3xiwYPDE$i?&(_mX#EP5|$uI1!zayQz#>|7aMhiQdm@ogcZta zce!z<1!N)-FO&DVVz_=`0W9Q$4Fm`fu3~WM>1~9)W~YEwwC+Z#b^ui4V#*Kc-)(TI zifFp;Ok4kk(M?eS z61EW@m-_$Nd-rf@wzDp9-@b2OzFUBR0Yb=T8pwFkIeou4aY$Xd>t5B>-PK)H2Ai%+ z*S)K{s=97QPNIwi3``Cjn81Kf;>0n`6D2B+LE}TjFvc+h5u!#x%{UQJ;}|t+)QGdb z(>rGuJNx+U9TRZ$?7x1yp0`&2YSp^bTI+p(FAP4wX3cf4MvnYZzmI?;?1|l~=L!Kn z_uFX03C6Y1KmMn?JBX23OJ^Al*Ck;CzTZIZr{$a)g9_EXdII;CTcO2F;+fAM@7yc9 zcwpspNUX(HItrQkeAsgF^`cc(8~v>lijE7-8gpaL%xVYE?tTLwwYg~Q*?BuOk`~=) z*C*x{V+xSTslDp;<_ozYFXgIe2q#ZJ|4H4&^idlo=1G`uU8mWzNouv4kRzSvm7>^6 zv9$ISfiZdnZ|r`e=bIDP5=4IurdCY`UjUO9s9VchL^D+w3V|oo&1x0AoC9ZfN81Q+ zDl$?Jx0x=}h_x=6PX^7jrdQ*LAnbYFfeTcybVyOCg4uiX?v5(g`^<<*^$Gc@zR|Rc@>&2czcnx#dTgm>S%VcU}&__no1&^b~h>%*FnlC)0 z{wwwW-z|x-TloL~%je(5OaA{a`TxJ<|NoNz|4aV=FZut!Px8-IL$K|9`jHg}vne|C0az zOaA{a`Tv*w|IZ!`_vrl}JlOxiN5Ad(4?X;~!ykV5XCJ=p(O=#F*@xf$@O2OV!|@v) zeDuK|eh`5;{ja|NtM}i3|9cOA?*1F^SNH$fz5j6Uqxar*FT97``)Z&8`2Mr+Ig^k5 zgIAxu_Vm|Jf8q3$?zi0e*LVK%ogcm9+dMyge;uQz|s&fGP0 zJ62OSPdT3u(}9s+Z~m^G`Px(jLM0S)D>~!NN=KL1oB!xh6_DRQ-((wBqO{mZI9@Y7 zBeJixpI%sRmvS0n>q24YW(si zbOK7!RWANe9>tS1=ohhSf%_z&(LHB$3o zB&q^IS~QRgm^0F~#lP&rdNiM5Yb&rVYt9FG%5v9QpI%TQ*IQoV@^EVkfrv)*^=0pU z{K6a`2*VupR!hDI$XLBA~i;CyyS%9$DlG*#kT|I{K zJk_xQVmetC7x@akzAJt0Md5QVNif*(h79moQdgtvo8$gP&kN^RI+^IJPzFJlh&05b zYq{*-y(pd6C6z-H-l8LpQ23C%)_m`uUYL{SWL$W2n8#hh+@j)(^t_89X;W-No5wvX zv{HL1Fy{3wX8-r^O5YTKHDu|CflZWv;`kS7z2CJn4{(35HhIZRU4;}k6hF9@`@XR= zcWhgT7(WHRj}v}Okh*cbxwaks z;Ct(^>iD&V@BQqK`5HJN%rqfe6)a&Al(a0aHHSWL ztwY%$gE4Zo#E4*vWY=2HE~tQUk#=cwW)vI<=H9NZfB$>Gv@`b-L%;{o3frL0h!>I- zc5UJN-?HQ4Ma*iV(FBw@?qmY3RqI;w-*Hj6Z;-Q5W|&LYqZGvzHrHD3*DeZQ6~x$D z!w?WjkrN);Uf){0i^8WaLd!hjt z%$X<)mV~S3&S=Z6(cHb>{NnS@gZThk64p{1Y$;e=aq0Eu|6=!~uD^5#(R?|T(r_lQ zBpqIBelf1QOW@rYM}@HTkwEtI!HZPm>n;js#;~JEB+BDJzC?zH*G~iJg*AjtRf*U# zuowY(QiRj5r6TUEootG3iDfim12MM7k+r(k`m1)8=%ixiVEoWo>iT*(<;m+GbMFT} zZ%!IY>>C3h+aM`|xVrwcVehYAn1`mm0$w%#Aasy;okZ7vQt$o5g?Ru3ZBbbRZml9k z`)irHmipfRbn&E)F&NAQGG80p=?3!W=(Xlwb5Xdx)e#E!q@|><27`cqkq@MgU6^wo zGR-Zkz<~=BshRHeqwfA|F3h2fS`xe|PGf8cWJ`_f>v8{8J99hHXCrwck{p?{sIH8z zpQ-o$@a{?NRb;C+OIZ8oX?CB7uNYfzA2QM-M{5!B3{e%CRtVl1IyVyiy<5*Rt%wEw> zdOh>~zjAi8xA*T3#l1Ukz7yXu@4WHO=ni_PcjvWtUVZ1~cTSEzcl7C_Pu|t;ig)R| zZ@AmK``%m8sR{qbO zzV-A?r@^UqDxT7(Z#Zq8zUK6mr!PA_I{EC$r%pa`^0AW-pM2oteJAfedB@4yPTqVH zpO`0aJQ^ec}(`shQC-v8*mkKXy{xkqn(^rlC_ zBkhs+h<@~jN3BP%dGyLhFMD+K@Uss;_3#r9Klbp$4?pnmeGlLL@Es4|_VCRQ(M zH$EIaL?8AZzV_j(AHMwI$%D^5`1FHMKKS^9k39Gw$ej3|2S4`U?GL`|LH59X@Ess` z0{-Cj4{8r84_@)${)4^ypSk~Q_kZR7NAG_K?5Vu>{yXnKcmJ*T-*i8?uiY2#)A!$S zzjgmL_g{JcW%rNnefHj`?tS9k$L@Xj-Usfz@7}xbz2n~7?!Eb5400yE@!sejdarly zwfA0q@8$PS&OUec>9bFsef;brXCFNKsk8T-{n**t&%Wy{J9E#z<7|3{pS}L9c2+ri z#o7I{z0=Q}{@UrU9DV%gBS#-R`l+M$9R1kQ+d+oL?8rU(j-%-je)RgI8rX$-#nJtv zy+iu&4Tr76*Brj`@MVWb2cJFo)WIhXK78_P@V${g+GlZjaE8(9l;h!nt50vmv zm++r1;XhfzKUu=>y~GFG5{^oEQ^I};dnN3YuwBAN39m|6En%gE7bX1dCH%WecviyG z5+0W@SHf%wQzcB4Fjm5Y5=KfGD&cP{;on)p|6K|9OSoIY?GkR5aJ__UC0s4x*Ou^q zTf(m?;cqVCN(rBr@T*Grl_mW3CH!?I{52)~@)G`v5`I*|4@>w#37?ekaS0!l@L>rb zT;ly-FX4Y*!vD5}|4j-1s}lZ~CH&7z_@9;V|69WUq=f%b3IBr<{?!uxe@gi8mGIv! z;lFc<4@=|N;U6kpFO6M?KU%u}juQUi5`M0Pf1re)E#cBQcKCgz>)%_#Z!O_JP{Q9; z!oRPCOLN)bcb2aI-V!cKI4|L(gyRwpOE@UubqTvATpDK%td6x?UQW4&l=E(inC4 zJ4)9}W6@!+biGx=rLpL+QM&%R5-yEJho!OTur&T0erw78TT1wwO1Lx@9lpABy)^zD zeqHJM*Ou^CmvCt;I{d2A_0m{$_^@=nG!`A6madn^rNh#=ba+s**)QR}OMLL_CH!wo zxHSG8l*XTf&y;NbX$hCcoP&Q{y8aJKxHSG8{Qc7P()e@mf0wS8#-D?~UAq3uCH%Ka zxb!`9@YhS%f2o9jv4nr2gnzz-|4Iq}CH#j=_+2IZ<0V`g+Yf%EbiFjTANCw+WRuBM-f#;o@ zNNDY0(Q?_V4kkKvgC)W>chwaIojO*3+653hKG$Vy?NC|tSMFqv zM>%cbK8nEfcI0XBWaHoY-0qGB!o=hDWZHAIpn*%+jM5x$jG$3-GDZY_;nhi|*BcO& zaXQ-FQQwYwGZ|XKE@n4@CM>IktzJOpNORNyA>Aw-sZczLlp%Ba=;98;oUE7KdcN!q z#|%5%jN^V&Rfes>c03~+@nG2s1v;SO=- z6b$q{L4Zv&VfuIj8p2Z$W0thGI%%@(6a<#{wW0vZ5ScCQ@4`gLG#N`Ekf`btzB(Qf zYIV5ot`~-HL72|CdBd}3LujGe4dTxKx?>g0nhGw1*2edu2Am4*1wG1gUY*jSILpJT z;Y3b#Q;conXeWb9`c{>yQjv|VA!b6?jJBe%xi!f6QKS|)VnP`|Y(}cqvDNmlS`a>W^O&Ebdz~g{n zyqYNmZ4h|P)FzZLYOj(0q-nGcc1@RbWU(=VOm&4(<3W@1mPT7QRx=RN?i-39#{7_( z7?hv%qQiIZe#5T2lLZdNaFRgoq{T5VF)#;PNhWlW9Z}mLC5f&EblLf%v)vu0ITreT zd$sA$SvaNxRK)pYDYiXEsEfMlAnk0xD^fP&4|n4Hq^|385>m&0ljw|WH%4-L+{vP~ z*I3w*u$;AKi&Z)&;y69}*4=Lanc@z&h_cqGH?;ZMAO&Zzqk64{%XX|ZX>bN&K?L|k z(K*~nQIOh3voi);F(|uk3qr&pEiYU^mH!9gz3Hrd_gX3Nf#Nm~`f38v1GZtfE?WwZ>eu^sR=RZM&S-bZ)8 zA=Nb=hz8FJbt~GD)Jj!~CFM=g9smtwi)EV@U?b!3YS;TgG@q zW5I%_?S2*FGmkT6_|*mAG{o(G+7HzQ_152qtyhHP?m)W{|<1U9d6<>zmqCj_T1iLDF?n;6MpoLl$*nJ`U;ypAY;5p||4@ zat{67Z@@ZbfVbL3q7Yk5^r--=<1xIgHMD$0xyiUTgEV2uWI1#AXLff$p59kcZ8&eX zyNzbIJ=`kOSe{0_3LBK zo495sAqj!yn{6cR?{wP9b|$CQaR+wQ%u^QG%I3*h)Pr=(&^Lx3@PpwtPGD5Bql4f1 zJS&Poy0*4hXT5H^SP-+sE)+pfCj-LKXHIQk%Fw_aP9}0I-TSM%J8&jP`x7%`={ZOz z(VjP!qeSobogxn09F43i3^p~b#kiLyX60b2Kn}oOv zTwZ{-5Z2dB6?buAE6(s5akdLkCqWSL#8W$}I~_F^(PS{w@L9T*7nG&u%bckWW>|w> z6`Dny?4p$^N=EA-y+W!;BD|#1ZF6XFAizHKRFE7-Z6fv(B}tCf2ZMXNk)0x(ZL(}; zi>2(=$8JIpD}Cw!>29DABsY16v(_my0(m$v_2^D~7vK}wp?!*VIz4eERf&wH;EaQ|MYA4bwKkXvzzhcawSubM z-HG#%OiXqs%_7`jj@_`Pl4c1*Q?_Sr-tAei(1QE8Qlpn>OS=1ypMP?4tOCa)q^6Fd zbkkZ=u>hP4oxU*|Q7L08@Fb=UE|IH180R7r6^YEvYLO3I(Hbvu5^q=`Q1aC{1U3|o zS=TxNH}KkWQ`kVKyRdnR%~=*|K>B)B!_>M5x2BjS=*pyosPu<2t=`Xl{HrkoCxXMSzF@B#z7BUr#$Q=I>;Pd+x-To zj2kc+hfa$4;=tWZls0C!w!u@#U^8yu3E3ab)^h<}6DMcS>;E5g_U@-AKYr(Z0KD|~ zQVl$@2L3?u!~35XCzce>nAAB|Zi2Dh>hiAFZp~KAtjR7$t$H|=wh;~#Y3Rgp&Q}yN?U|a)}dPNBoP_Ywg#%of@EetzPRekp@|JKG+UObVDH>?`gBI zPd%~OZn|5JQNs4TmJ?68O7O*JAuz#mEpCs(r@EGLz$KG0&T__6xx=r@>)w30;&I0K80>1PW=3%KQB)F!+W0> zC%#2<{nGWk?icpF{}gfJ-=yC0#flSuLF4^R6esR?`;X<1uWjr=x>TKGIFOZ>yPdl2 z_-UuvF6tDky0xd|fl1E91Vk3Jjr@z(!0%Ey1r|7cuIH!MD8?mTuxx;jcd?09RmsGv4qddJA*E+nFMz7>B8B-|D{;3$ zHD5c;biS~$e^cca$v+;CtGCe*f8PEUNqw%L#x`MykGGW2!q;#hwnVvIz%aU+!AUa! z3FCV!v=}rtEUU84Z@R{QSyA%&bT6QWzEsm#=ZVwUl`<~oH)lGXme|ku)onf`R7o;P?cM{l|~~xM$z_q3?X$v)}gH_A9q&uD9;lFPg-v{aewq zZ;{92`ksB;q2bC0stFRq`C-0DtjJO<*NXGUJ?6I{SHLwyK*oT`T!tVyLwSmQJTgC_ z!#;k5FSaAL`<=%vrI$Nr~(hQF4tD=LTc%#;E*`6TI-JT(^+sNt1;TPW# zeMPpFZc}SP>eKZp(_uM_WCp$YoKI7eaVelZjCKd3@g{-^EpKK+Lf=O7F$FE18KTd8 zd)iwvPY+lVuk{5(>~ggUf5VRW_|YHh|AV#OtM=ZycV7cO=5O44EASqlo_=)y?e~A} zlm=e<=1+yuhq-dt4tOqOHL1vo%!vS!jq}CiHT^lOa^R` zNm`p}YgK-V4>r>IN*m;!=c+txRP5MkRyBgRo7dS-7ep4XvNY!}T0(^}wF4GG2S^Di~pAgtaiUch0Xp zI)tH2Ct?kcmf8Gj$kZ8k$zh92@p!aaa6M`n!&=9X5O&t(xWu0MXZ#6j#2TV2W?`ymt)hu7n;4iYtl4wEn-RMNKiQB_BpJ)u|edkm}7W(U`V1;ucK4okL@ zc<9vVZE8e)7hBUfZBm;J;+)%0ig7yrOJxqVepR5G3wyQo$NgNIX|X9JgBjk(n@cpp zi{>_sak3#*3^@$NeyzWVt4KQq@pf)=&~eNF8^b))9?7jFT(M5(D2N2KE+~{kVxMt_ zAWH@syH|t$4&Hvr!Ec%dI!x34C?0deD#%Pc-m-jsQ4AF)45mz7v)9w@2&A>I)I=y9 zSX+L9j{L5(g4>iQ+pIpvr6jSWsON2){fSFtByF50aN?2!QJC^%#2<2c=JdBIU(%4=ZqfY-rnS(|oYM8gpsS#)h+%lH# zay9i5t^?IYe68?HO@n7Kvaz)?j7+o< zb1oAOtj~^ST6ew$Zb5vvFEzj+V>eaL6(`Lt9i*6?Fyv}Gp_18^cX9knmmCaI z#e40LTHp-ZbVfGZC)WM75laC_#r+xtcbcgiCT$X`%sd=)A4@*sm3fHOIb3W%1g(hu$tL|v@s7Y)#Z$7h(Jk# zr=;^I%3Iw1!AlOBXO7~LGC+W(w17B2?^kRpCHh6j zt|rx3rkC4QZ3Zu-m1=7#Oo7A9Q8$s5P;9MN>UqMgbU9JtV2iK(&BPpDjeb7-$`S`c z?aWMLn=d&4P3TP;JDkwyt!6213qyC^s}5Vy(k@2R`BFodlL0zM5vVfal+DyIVnT4b z)d{bPlGxSNFq$qBb>7L+>I$2#y0wN+q~|H@2ry)G#hS~rkXFsB^>F&HE;)#z3L3D& zEP+4gIkVSyGJ%VUWPYY^;|~ zyO3e0;w&ea1mt|Q7GfvE*<^wF_*T+ztIkknjx&ZL8FjwJ0vY*pIGAn--HYZ!nb?3N zLFCo3`sCn}!whvmA~+nL)ap}dVl3C}68BnDBGsM13&*LIFH?5oj8dc0xp^L?bA_FV zlj&097?TL4fGMtOEZqIummH>usbzJ0+HZo)Mm@A67q;9S(xGXDbQf1PkdF1>23u=H z6>EZGOA$yXO}vd`Ym)6bgCa=yN!Mb#M7oXDn8{Exvc%qb630#$PjY-fiZjMdX#dI> zb)sEz;KNSQhdY4{k{rkhF+pX*5E~k&`_1VTXvCBDFf^s^B4#Qr)W}2yX1fKmB{;3^ za&@}DWW~Wi!)YXe;l58=enXW9y>rGbyjYMXGH1a#Bx}@FIf#Sbd&vPzk}>Vsno5i% z&*F7K78g#-B!QzK9nkS$;%igeOOt$1X^@iL04MgjOU$dvLX;8PR(wIDa7`O-CA5*& zjK*Z3^xDxmm`wQqWrpJcHD(5Ms$7*aI)3Vs!(zJ}Zbm9;SnVxms3L92bKnhC_;xtW zNWLkKH)N}i_hqB9YPQi_iLyq-2OCUS@a)`zICM~3<8(Jin_d$1hE%%-a`>DNXxM<4 z%!FWA#~x0$-qrT-U~|bq-+(VQqpG*L$!u&kHdMzSQ*mPjoHtg8HlfqOCh41~ouw7B zO=3tG6ZJ3~g!X3L%`j9q!1=Dlg_db7ttA`}Lc_h5r9veF3trG++*>^Ls3n^w_k9^3ibKFfxvqk4S7) z1YlmPNm}dtiTXNcKYz&qq;thui-kJGdZO1{F}56%y}mO*D1Nx?`=&CjrMj26v|7_Tz zy#o6@ebl+Zn)k;J&VKveD-M2oe{b(Tb>|=6Sswk(BmUlBIQ+|xvWK5OIeln8_{8yB z9*B4U4Tx074}SVy{}8&nIr|f*?>hb3lV@+RtbfVmr5gBh)xhcSg$|U^gE#`ydO>P7 zQnb=-57()L^cVKRAPO8^4DHlyZq1|y7N*-aKb&uk2p{PR@V<=^nULrqCYcO5%0i)& z;bw@~f^ zS}<|zYqQM(WiYbNtIheGh$_|gP)x98YZZW;&Z52+d|oFPP64A+$^`0X_Q;%fCD!W( z()nyIF=RGKESN<#64_E;ppEu-_x6r{{RJMrGDTRHNENZWQ1hr(_n~MgbO&ZXrRt4k zi_bA3D)<^*Pb&kL-fm{~toL;IpoUU_B)&I6vpTFa-Iv7HPK`JQ5dDo2D_|z3{dvlG;@v# z>u9Z5hTgQ=SOkU6S2N*!NO0Sv2pH2JjDgRIBtci&$Kjjz_8$Jk3p~6z@cZJ{)1cAC z*)GMcM$^O;BVE+Ar@6o%;hU#(5Q~wHhD=2XYi($T#j~d{t-$+ymz}E^ri`E3STMgG zw4Z8qkeL)~%YN;gB5;Xf3TKRlB>13B)2j+zrwAC}l}jEN(5FfZrl+q-kV>?x1YqJ< zdQ*a$X-+#+_?R)L^rAs#~X&p^3@HU!x3nwzxo1?XguAZ zAx3-SaeGy5Z4oISWGKP~Oaii&_16m?93cs+6%=Y^NDSVjyiNb;etD=|1j?dg50wJ_p~x9FOI5yGBnq-`*x*EntwrxV>0JI1w+wpj2gB{u9`zn4qU~ALBhIGuWbjo zm@lR^uWbl+FtK}KtwN0>WVTJg(=DoCq}a^BS8dQh^F;>DJr!zBdK9*Otqxo4;qvy}f(n3-ys}W^Fx^=%re3kDyiB;2J@JQ7Fs7-OX45M67gerPV_b3NHF};sCX304kt!Cx651oRoe!r~x6AVm*VL_b&7V56 z7TB+rw#2rsY2YvodV}KFw|8>?h3eR@ovyVRScJ0Kicfd# zVAile8ri3r6dH4@TA?OSJ38A>X-TL^qTO80#Wm$vyo(s_s>|x>)23o@RD%bhhC*+{ z&%GvP4*c_&cFgH|YM{0aC6PLIuBw`yehv8ixLj9lu@DFk*FBV8pkVKS?NKAPClz`D z(j;3|2&8)*Q##m%h9IL-z+@HiR1ViD2Bab!cXCLQ$+YE9`&|$b*`J&7cs;{Z*o)2y z7b63^2tmZ+aGOpc0bjpKJ`L10Q(SRvvfgN2F7)dnxd9MX{&$)<3b&VYrM>sV~(#XP7pZM^W^I1$45Z(xZL{y8wL~{F35(wULxDv4c434HmHlMDLx*HAkr_wC9khsSmOrB5?4`0DMJ&=KaBMtJ*P%5kM2BEJZOZy2C0yaL z_kV)v<(U`iBkXTLh}pEBMNN7u)9|wBM#6-gs$*!tQw+XzT#1Yu)AhiqR5`C7;r0GD zC=joSB8r7o8-uh?xCz>s&R0-9>`Q&57Mp`}?@1b*cfse!!;iecBS`QWA%u~$9?jd; zey`1CHkfy*L6_Vl;gl;#scBGH%jokJykin|6^H$WYcjfFZa_R>w;OJ{a1+EaE)?J8 zRUwo&E5bPspQNjNX6^0WIlJV+Syab$MtX;tgM^uMA0#XvPuOnQ=%QT>lR=16yQei* zBF0qMEl2{1TawRY4FUm~{D!@W!~*x`$+kDls!~^34AvAsslm|s)>Zf-(?(*kO;~@k zF|NLs2mE9I|5xpOXz$T?KK$In;=yMhBtV%iy!YvQ>$6Xt*+3(1b@Is*<@giFzZ)p* z$#*_}XLj_lqcPCbV-G%ZK<)ZNb>kUY!1u;2cIU?9S zl%Zj<>3N-QL!*OUo9#BthRRf+DXgdD8U)%yxG8umq}>_;|8-|mgQ98(1jhS)#$EPU z1D2kACicv8=Q{?sD77;2t2aHty9t9=?-<;o)GzJ1;4{yCXge-8EA>sFw|y>4C7fo?QuLUfV1 z5LO?JQMlba9TG^n|ot*j3Mxi;m8Ht?%kpmePQ@=o{X z+*X@)=49Y+jF?u;K&&=Vs3Vy0(&{$AVs=p%G}Spz#vY^kB&s9Z07BEvQoZs8{>*bP z+cCIBsg(_lDSX7~d_{fZPk+tZLA6#MOvg=NGF^ug}Sz5NvyrSY@{GT83`I;NgzJElRD7wT|AaZAC7w z*MlVQ7He4M%@Mm5#GJfT!^`@uO+Y zWZMDIu-T5`p3-pVX)iQ;!p046^cL>#7~G=NN;k?ziv^~Wno6sbo*Qa`wnm;fE;Nmj z1)7lu0^hC9t%$iv9o*Y7=+jx16mo(Nj3Cmcol#wZ++l645Y}28dC+P|1VF0Gk_NSk zEFwDJ4plWbCjEgTquq@WfL&lwZUSe979j%1J<`E2(F2<%PYN=9=DD*SgIknZku0Oe z2XJcj+d$C?#`MJ^FI2?K(yo_o7C_yKK{^dlQ-{z^7@Y1H+@jP9igkOVb!@GN6bm-` za24OAv?k+7mBffx?#||NUmdEHYWX)|aI#}?i&86J(oo-Lo;$v%gPR8s-zLQOCUtOk z$KV#RWnbE8m1my2vtw|J7?{c}f~mq zsUH0VP`-~JeEQLw4u0f;d-x4Num1TMfn9{a1wZ4M$)v+YMLg zGH@B&_gPs7&ZCLwv>dOV8I?)S1y)aP)Ur)f6+jfcWlveXy3~<)f{&It`0k{eGG0|s z;~d>?a3`3q=7^xUlQ842I=0^XU6=2o3h^|%1!9*BrT6h3Gp){zuufz0YNRnb$})+M zcUi5s@+&L9?iH=1xmuDQ!cM}mFQg5e5fsc@X<@hNd!nwTQbd!~musL2a%{Nc^K+pmpY@cQ zLwy@I+!--n^7Ag+8-gT`)wP6jVc#S=1}b`!Q3zUg>iSYeD7q8NQu`BLZ)#998rqzz zG&{Q5%g4B$cGpy~##W)?xzTw_n2fUG3rQh;WuyT|{41-Oeg5)Q)C36Qq-GxhK@LFB zQ>$iyDz%$UeAH~3=tx-dUbJk6S+`XYYH1G~GX&Le4lM#cYN+0}Hz(C4rMKoon`lhh zg9%XNsoAyj!k_YxiKQNj3cwdI{z9M94}4J^`ctIq4~NJQBvVXqoKm$uNN1XDh9bR` zmXsUJcxN!_Bvg&L0f#^L0*9!OhCQiY>*>)%1X7CgX}ah(dJ8M>(}f5PY@P7aNoS#m zY9+pTo!x-LWn9c{I9N;I!VbcUbHy7CS~!k03V1PM+-_Ip5doyHY!saVCbx&0e*+Fb z^Flqud_A-$bl>9aLA%NWm-!~sg4a;h99xaSYKw;pB&zXRmt!hM+y(1pWz(C*VCxec z=u8K+yqK>yWQWSQJjF)s$10h&#~SCEC^6YGGQoL*z)iOpbTww}`1f3L;P@_O~n&TUu;@ z?awv@gZ3ALSS?E9Bx%iUQz++)O)5B7`^8ay$$_0Dxo6k4wuJ$wfFj7W*#K^e(Zr~m zf-wZ~zZ!6@3dnI(c?xVQQk%J@D(q_YG_5DKM8jq}NU7(Dn%m|#0n$N0$PCa}IDcY> zzSGk9K=m5U#FdhoS2d?mK}3w12Al4-(1fibak2x|7B!g#UU!v5YZj9#=B0~HJb*Nf zb+i>V;kqu+-R0$WA{g1kllhLctexXPk2Dw?8{HXkaTdk1tGgYCmvM9P&=fUNPp4$J z7>4;ISS4c&p04N#7HvG6MD)cL9K+9@!l|s+nmwOlBZ=xUDHqWp+VY$#5s$>wLc5gX zahTjE#7%_w=kn5xHA0%nHiAdUd@5g!Q942|-vt`3mn%-f=ssNszJ~KmpN}A2(iD!G z`Z?tX-Jwr;!+fq*gt^WQSJfEjdq&k)gDKP;2#C-w+E@WkBB8-f=e*1%ezSGHT#i#- z)>z23@??w$_EmShJ6~VopxaGFjI;!JNs(bbEG#IEIgFzy1Luqe1u^)r-ALonpdyhV zNY-e9X32|76R1Ws*%eKz+d)_GAjM|oMH88fHXAV8oEKYUqx(L$Ud=(Q7!O>euZ-0v z>q`#EBySN-geivAI)yBCtsV8+a7-1!h%RcqrZiK6c$&=^qJq+WhMu(s{X}!~^{85d z`!+mV5G-0P>Wju&pBeL|UQrsApM$S0=x*0V$lqnf29)}R7Lv5S6S!6`5vUFQsE#$o6s2S?MA}AU& zlexGU`UbQOxPZz8kU$cy`9OPxXF_K(a(NU3a?emckFUB`9e?j7hpvy~3glz6UR)T( z$kt;7D7%s~Bd2HEk=Dm0)TN4cJRVdQ%ZQ~DLbP-c5s2A#1mquONw8+|JV$VIAy22v zrE5*Y8u)&AqCdhfmN+2OW+dAMh~r+Zs)YtMq1tG)j<$Fo!WUd0NCJCP)GE?mWpeZQ zl^eLIzV?zszeea8?iWOLLo9nxV0Kmzkk#za+hVmQX1>@TLkKrqCqxBEa_AZ=Lfc8N z-V;Fhr%U9(4Y3*uvK}@4kxW)6_HrT5veDi_6ckeY%K5zA2_gK8*b%CGi;lkAu-35N4HxG7w%l&5! z@N3`t?cmqG^~S%pQ(AiwotRep%J6raBW~^4)nhu3AO2fzOh*Osc(Hz)bL_YDp4E4c zt>63RCm&{BRPulPv0K04u^au$m+1r?yfW?cudMRr;(m*>^Wa$((D}Bd|e^+U;kT;HRGNjpsN1so&5WZ*c$)UhQ{$ z{?)$v#=rI-Z7Y0M?%iYS_g?knqr@Ba z#>Q`WY|tCOOlR=mm3{l?U)eW&xwzlr^gej@jeyScz44VdrgKeiyv1?-|Hs~&$4OFE z`QlZTTSR6pt!&Mr6iow-P12DO8JUrY(z!(LduHT@n4L>x$ux`Q(vGN2D~NkL zI)Z!8xFIgI;DYPmDA34^HiC$bqr4d%(Kn;-=B=!za;?ny6~F2Cc{G1~i>muQ=iGbF zxp8kS=caGH7N~^Tc>dHXH)Z1t&*ihXMt~}qjpt3RsxKR7DAEF48{Tm3xl?PKoQ;aU zumssS!vp!`t)Z?t-E7=BwK=_PoZ(qM$j0D$w^`XaTR+U4DjTuBZi8%mp56igE(>h9 z?Cj^u_8HzEDBa2cb6_@}HMKb-vT=qN2GY0sfl8Q-XHKnhQ#Q`F9878dN*AA;;FSw%tk`f7nUF!XLuzbd8?;uPB$B0G_^UsY@Fe30?=*uMz?ot z{Xln`Y$T|`ZqM)@Lg`i)Fb78T%GBnJi0Bz!JV@W_1S(-fFHfyNG`-fQ~8?v@!N5=(VdS%IRP8jI{@&*IqtJj%x5CHQFAPhF-gBqMX55e~cW8UeipJ zGhH02KpavNnRUn?piE^fkXBCKNDoxe!nuPZ0Y>Gv0|867_)@YhQx z$swkTLjuI%OD4*hu3rS`*98;h^!IDr{6m0#oj*~|ba6<4I6QBnoay3_0C9NkBsutW za&RDL=R`UE{TkCxBO%vrz)i06) z{W4CJ(;v^{@|^WNe(&P_>f?H zu9zris`w#6W!bg86Xi@d&LkM;mratRoNk69XS)1Sg8aH0bC6yDIB~Sj#D_;Zk$480bDjGDIC0Pj#D_;Zk$4*0Isi-6b@cr$0;0aH%=j5 z02ks(3I{L5;}j0I8>diF09Wiu3J0&);}j0I8>bK}fJ^%Xg+rJ2F$#y;jZsKw3SbQ| zN#WocV4T9icHdr>t=b2iuKP2&T3dPgK}HWsOY>#bCQ}3c=L&qKOLo^J{Ec zC|y6BQ07^#0KcT*Y9!aSB1=UpPr2F&KSg6b`i; zqi}uyub4YFCtQ`yNsobN^gGbwXbD|K9z;f4L+O@O2GZg9N7 zvF+KTIMytJ>pWw+t?qX$hF#Q%n6e={0<+L|2Wpn6XXs0?IM%TZ3^2B zxllq|HFG&|Fw~Gd(=76~JQ2{bW*jeX=;`=Htwhq+;om|SO<4SeC*6C__hx==oAa8Ktxpies$C`eEyut9_x{$MDO*cW_;5zl9G|Mq= zLqt#0d0o2dv|7DGOy70DRZT|#${ZlcB=Mrmmc&)XNu3^dOYc*#+KV;uE z6XXrfv=`OfIlalMv*pW7$XfJP3gIDnJIBkz`umG(F5+Vh-KteVjklt{6e+gp@{qh& zj+dtz%HJ(dt8}bgJzn0>Q~Rw8)H~K*F+tu?ex!}f43`W?a?P;Q*JOF`;Hf{3wW}t` z>mNVhUsp}Xq)ja2%NRg4e4EAP9~wW+1bO}O0{qc2>$rG_iI}TtrwN?;q#MrP%g4(b zdaA$m3u}(GJrm>&^>@n?4;^clO_0~$Uod`C$h&lcyrKM{(?%v;7HR7f;T&rhPmtH&Ul1=_o-pTF+ciPn(DiZ(c^8eBH?*DD zx-h$AZFPdYq5R#l(7R*p!U^(*u1DIUwvjJ19l3N^=c&f^)xkn0jjOH?v~<~mFBb@v{1p#h89czku||!TH}n*Kn)z8hL0-Rq!TguVB-`~i zn+tTTW^`=c}zMYC6{BgC&(L`|E7@lvI+8r=D%32 z;PpHBOs*CSFlk4?GFYXg`}|)v$IUI@w)8UDy;48&OUVu5GlX{uUNQfV;Md#r?={?O zUxX}NchPGTb8`#ljoH4F{po{YMkC|1xOL``&tcHE!g#9@#*J7ksHs~_@aJmGr0FCL zMWxFC4qnS@n-0IT6fp(rNfV!s1p{r12^=QwDK;Gjx*5j#oW+#n;~A~SVxVn6W+$8n z?e#iE0-S{$>i*q;aUPSg9>Ci$QIMiYGk_i*TxqZ5H7)c$Q6YFg3o@YYO%4_D)vFqsHYZIm2^`dR8ciUM z?V3PLNl$|J+MI@3i{*>WcqXEE=$j*kTAOQjKtZcq$=K4H62>4e&oIeWS?+AcYfiON z?)O!6x?Y=5bF6{Dws&)>a>ogi_%(Y!nZKP|?5a|i!z(KqW z&&E47WxO+Lt_J55nhhb9rJl?AxnkW`GlfDN>({ocrCKT9HdJasFP@PbTU-hb_2Q|a zK5N`i53V{wo{-IG^@sKP(`u-}id3+!DR6lYZ!?9`;F53ErP3ykzfe@c%cAVmP@g$&s0TL${-7zu_}w1Y z=`>ek&h3h7nrYb$-jFAs&NCsNcM-J4Ygk{c3=Fl7D3{zpx3O5xM7##QzthfJqBSbh za@6^Jz=Dx^Wyc-?_0KYJu9lt}>NCa-_2Ayd2qv#!*bwk_=daUcv4=T2+jLnpdX>Jc zBkT5+!eK_2vhv|dC^vGbRfaU1=S@1o9*wG-#tvaNG`#7cCz*#(IG2pc2oOp^)D?Ry|$z$fK9&FSKL?4Sq zJ7HGtC&Gz9jt<)5$w(>^24}ew1)k3m?xeFEHZ}`%!vGh1$f==z(U_qg+Htz+%VKBSE{im;^M{fxhYuXa0T#6pKF#`=QA2I05Xu^si&mX+9xDb&rj+nh(lK>B z;@34?1}}$&u}Zkec)2>9KqRMzddIk-9^5=zwQir)X$Uc=Q&e}H+ckmc$$X5?DdM=o zY{R^MKS4H=pj;)$;DO7cP8$u<8Dobn)O@P66&&N~u4toGd$s7Wr%Gm{H$oB-zNu1S zMQyRW|6iTEd2Z!hE3)PHFOy66FDVzlzj%%8hq8?H57IRHC~5^K05XzaN^X=aiEj}7 z7ChzulQ1Xvi{LiF#S3p=5Y0E{cK`wZ-G4XBj_enM!VA~2sAB4xcN5D%485K_O9{Tj zk!!lvEV1Ym2hR1XGi+3WnEoU=(CJt9I=ye}dxX;zfl&6}JDIHjA28FX+pr*3?(=WtRGW zvLjT_t(Q-&Y;vX#t|4bB@0U2D?pg!7zGrG{dYL{;bwAk=Rj<>RORI z^vrq5^JUHqr3s5i_5y3VnSR04){MyX87dQUM_vXrb~F9_sWooO^ce~hW{zAA zG<7rmys0(yW%?|231vt2^xS&x)XFAi`p^z~ma>G2BbRlp0bSoYwKcs=pQS3H?8v3P zPMMCuuT}WJYmD%ri@58QCD(CZWe(Zib z@X5igeA2(9z_8QGC)@P|>G^ps`rO0hseSVIUjsh*t1Wyo^eFW-^2zD`0QI@2&Qts3 z!$*NnuHVWhgBRq}$|u{EC(qB5*5@8iPwkT<_W_@Dw(`l~!`aixC#U%%+2@{PPwkWN zyMRyHTlr-04&3SFlhgd!?Q;*jr}oL7vw=@qTlwTPUUy;Jw(fd<9`Zi-WPEC$Am;<0 zG`H}{(6070%9GRmY4CH8(5LmukAHbF@JVAUpPa_m_>OD4uJJ~VkIm`AsT^sJn4Mk* zt15h%;)ut_`b^ajlV|1bK#mL74dpoDWoWjY2}Uhttj06Jk~5u7Mj3yltu4B(j)=+b z(d!~p`{XCT2R^B9<&(i3^XZf)r}?A&=iYyq+9%(=MY>#`)6RW(Zkbx1Tl(g_XX!2T z_b)L^7c4%p_|e7k+)o!N*ZZ^zOLiXH+-6>77#$q^?O<4%5Nh!|~Y9d+VN~u~X zY{|C*g`y|QwfwwXLs!!=oK!9TqDLf1Lpxq=;Z;wmY+|AwXE3968EC9vwyWC)BHt)c zMjMH#lZkcW^5Z??^kDYK7qXY-h2ICs7L7PhJT$xBU?jWpqMT7e~aUE{x1%T*} z8$^vRS}QopMpK>C85{1BSs63L;yM)}r*U<(9!Yc<-iPJn+I5ZdkMxL2pU+*C2QfQk zE!MeIKwz#|mAnpP>AC1oY1 zZg()Rr5CG+;uKMnGKKKG&D?61qsbgwcGamEBUe-2Y!El4nuHe|fNiNeioH=St3g`L z?@o2BDX+?IqgpXl(U>dfYj$_V*HBc66vCU?f@ghf(93(o zO3SDB5($IO?GDPZayrfPg?L@ztg3B1-R4wE!e@&RrKqQu91FT0vA~vaKY=H_>V&<) zl@o2BGn;DV-1Hx)eZ$MVsPnZfLxg4T|iNUnng zQ(;9^EoU5Ix}L9udVLhVe1lllH^Px-ENCFhjWEX8a{6S$s%|8m-cn9g2!d|#sm@+3 z(DOg~mmV=0loJG=HqcsEI-U+_6;!ObVCfMPjS?Hk z;dm-k3G3AX2V;)~2;3B~hLoXfq*(!Pye27S)d>#%hV!vttw)SiHFAHw6l~jlM4HMq z-Km;OU3EAD8bvkg@;Vv6C0D^+X~V_@Q0ft*m^aK;+nqL+%CUN1oe7!c)k>WtOVPH` zU{02lHj72o1TJ3JxNvEY7_w@kv^%6{h@6KZ$w(`hvpKm=xYhEK`FJ|6BMKEg7dOPH zb)x9?Jz|gw<-(LESqgyz+G(mvx;$pDGSs1HgAPQDJDXOj3}lCJ^(Hy-uQrHeI~VdK z{iO((%&9}Ea>!svhf6VK$ZRo}tJwr;&){V_$@Q)Pl27-D0hhiIF*_WMx=I}jQYnYm z;FBk4ZCO>1)iOb)k@K}FRnyK@*G(0EvPWcMd4~bK_O9~r1%<`wbeB^8GQp6>D&D9n z9Eo(sUn2;WOVRVA@XtM>wHYila@vqGtWdXF)?_%Q;A**=+fxZvOFm;&JuE32wrWk?Nu=;UB2MtL%p<; z%6mjp*c@*=@M^y43uyFmJkivUWnWeA(s$I_K+;{X28~Qg?o#xwIZ|$e=%!e`zMip# z>=^@VPc<9CP$glv=Zu&$Blkv~WG-B6Rjj67toBTEnQ3w@XpdpQw`Yf<9wNkzPJT z=VP{vxxA4bZ|f0t4YfQ-5OuGXZq+^2PMguIH2!n~i?R6_;Y~&qReLrTrup71H(%Qz z)*I0%#o$H@S+A_QP3x|X4+y*QyPoUYT-CGL3wIf-c)JXdwx8-r;tp#W0XCoSI0}`Si57S zWAaSZQ%lG*cn0j=up$>Qxr@$buBUMBXB$MNPlMxiqU6z8R3Xl=6l%KbEfP+l7K#)ixM1R$E1xlc%aqow4L|66u&n70x!SRabAW z65ZD$;!NBaBq~vRn@ZGzC2g&l_eC-}ZzGzlVx17qnKQ~*v}iW;ydmi=$Wn@Ay49BF z>}oz6Z4<_v!BeWMwQ(2kZCQ9lGVA3`RNK_nor>{FPoqM|Irg){w zr^#rTjx!WW#8Nm_t0%R7c`uJ88xbd`l{Cj^YY7#2M>SyZ;7)frLNh8;mJKFqWVK;Q z*4>mmpzKYn5@KB=+j11cTt>&%tC_0Jt@DI)WsL=ICpk(_a@mYS&vQzLw&jTSE*;W; z>k+GAwU0O3O^psHgim{#q?0u_(++u2u4yxvMODV?nYcaZ<$5tL?M-rQS*=g{^|(Gt zI?P_Pn^D?p>NcjbHgZ13jN$pZS*0{EPG7j!M^SP;;@AS=*K2G=16fpGME^96+oi%eaVyj22p5CIfx5@z@b`l*cVJ#+G?UcdivJ|jpkWJ@2SR!hzo3yo3 z)C<0($jg&G|DzYKFmoT9wADL}{?-eeh|*zjd$>|D6Sc6am`-oODO;TPFfNBvm&bF? z5ZITRvKDV|s#^X4=!53J)sLRWRsoN=l$D}A>CZO}^)~4U@^PF^2P{;SG4idP%@kJa zO)5?ABDegpbt0z;6N*YFT2jO-B+JUH!LY?f1d^#}yq>@`?T)wYFyyVFMqy(*{!91y zzi{qzbFy2JbA^Y&5C2#Edqd!Mk%aZc-mICQbhO#`-{ z;k}t%*8v5k3ft&2aE4cWpnzJt#DcEv>U&wSYf7b91iO>&81346OuK|CTL8n?Z9nE^ zk)dN*yR1a9Py@pYXxHyrf6=Cz&Xl@Rx^EM8TO>2p5+7?9VtHuj=<=Hc)omJU#A>lh zv|izXfI^+;4|e%%sg?%+uL3{A;i}eRwXUdZy8{USCz@m1U6r#KUaVv|U?5=e#YBux z7pt|aC6cC7y`T4gH@wi1Q0qaeZQl}cQrQL0G{G)c#*X|@|w*Y8?i;&&yvJgs0G zGv|siE7h*o8WfXrs;$bV#b7YEGPF}ocerNSTPXOnySiPgX1GGER3V=rn)0EDzg>;Bbgm{aXy913GO7g=#1LK=SgKC

    bY zs9UM17{m3dIhHEg8@b%*%${{>7|875*8#hc`aER$;O+gcn$1~0!VnOaAPFiUa5g6_ z35Z)H>~qQ14ef#W2lPpaNGP#xgp5o?VxO|Eq|C8QN1^rl>2yA3RJAcpK9@-;Qu|W- zoZ)?3#Y^lBC;t11s7VI60?nm0OdKw906%fvrHIq-{=>h~2J(M0|bic0o50-&CL9 zJwtSDUm{}vS0>^tYk=-0W*`xtdrS6zC;?9y(9M}rJW zHg7qtQ&D)!sAw3|g$T)*+nv2^$XeN33k5Y~F0Lt=^B~ZHkpIpFkhfWkrG zE=$(&c!Kkqa4k1g-gUbU^6pUX_JwZVy)`jE%Yt&-@($OG%Db7Ds{fxaXfp~J$fM2g zfd3C=QXQ=D*4J?3R*pj#!>d-oY36XP)a6E#Y${ggyUXA1qVUvC1FVIw>|UC$?5%p? zlCk%zzJ5b-fZ6-o$ZIQ5_*{!bcm13J3)X0i&&0aMZ2RJL|N zt>EyEi)*^9EzZX#Td9`XSI<*bqUtZ{%wb}mzEaC&Kn*-gY_B_>BH40)&^KBkr72ZT zcko(LH{E5v+jTJANQL=1-`u_P!u`TC1WyURAh<;k72pEV{JC?#My$w7B+p3hm)tJN zNUoNwh<_&jtoV@lTJe?QdC`wVcZxb9hiFy!@4|1a{0NA6cKP3yzrK9N(o;)cSh{5? zx`Z!@79U;w_~MO=-o;B~&&s|ld%vtAqh&j#|04a0^oW#|Qc@ZE6ZG#E9$onO!i@{w zg-g)ap&@iH@@M1+$cK>T{CDQxH(wUMbDm!L@Jf5dzH(vG5HSSudivB&iYr8(`Gtjx z(oTJ|U9x~ufa`xxB?roiE-e3W`G*^gO;vY37>XBLkx0$OS6hQM&lhBO%kJJQ@}OPG zTQ!z_UiSHowgrz%$F_{&Qo~xPfp6a;16CksB4=(G!DYkEW-}YKI=N=0ek7l-(e^VcNz5*+JRC zX&W!d{z~>&8$Q{}X4&o){%Rm@!~OUY8xHb!L;S!CMAh%77%Puo}`3&}#$HWtf* zvf#9hMY4b_Fl}QYcqfRNwy{9wm-&~knY-dt>TdDFhTyI8L?$>OIIz)pt7JV#_C4A6 zrfqySn6bV)ZR4|K-;sT1+Qw(fzAgLqw2jY@eM|POX&b*-_JHhxX&b*t_D$J0r)|6g zyao1+X&bM|zApRvw2hZ#Uz2@p+Qv(=`(^h}+jvoSpX|PA8_Q&0m3?*E#!}gF+3{%` zqq2Kt_fFdwk$pw>m1!GGWM7tjdD_Ne*_UKrnzpe>_C?tjr)?~h-6Oka+QtIe7i3@9 zXuMT+O9ZbHylUFUV!^e7Yo~225<~>2rsu895DLPAQ`7TSjRk^`;MDZIRpSLgP!Qa> zR&CW-5D=W2og?)oTi}g`SP4`}cC}NPf%St}+!1#+24kznB5_;Xp0=@2+!D8@Z7dKs z#m#9OFNhoB#>QZ5WwW?0u5SnqyL~Z-+f=m|8ew0)ndSxtLn6k+*tCts;(g+M(>4}~ z_lWmQ+gK>ROnlk2jRoRM#g|UoctL!L_>zs0+iEn#yT!XV1P9G+t-onY)jNes*`I0G z2S!dTdX?x@0&dk3FnM%?fG}i*z7#A8m+(9CEhu|F>Bezv!vEX*W?b9|Efof{EP1{%~c)Q^3(>4|e z-X?h4w2c=8M+8SUMsBMP3T_qL3Kns%-(n&6|HGd*y0_J4Mzs9!^25_M7A`-u{Ln_@ zt#)=IWGAw7+QvfU9ORs78w-%Lk+Y|5ynvjAoHe)Mz^#U3`3K8Ccwztl!v25KjguGl z|C=7)zp($`{4&D}`~OX$@xuOpb7;J<|KB_sFYNy}kH!o8|IMTE!v25LB>KYsf74*R zu>apY7%%MqpL>h+h5i4)J%x?!@m3q&7xw>sZzIF~|3Yx?>iNBM^KYHM4w;wygXF!E zoJ1p$h#wMvP+S&%N_>rYN%WZTpy(r_rid1uA$)x0JuBH2YW}M$;)P4VEs_r|mzS?u zURru==_5A^5CdP2dq+IDg&3zb<@n;f-^D9Psw2|DAv9 z96svz<9|1Q-K-3SfL+)fPiONKp=sv$WI1fjS0X+`tVT8UMXfhciOTDKrIF!VHqPwR z%5cbt#q6xkZpm?_xE*6Dy~3>vfEN!MR>p_>Jla4k)%ID+xkg>n6#dOQBV5Jgn9-9^ z%Zss$IaLYj$xztk`i&57!1k1X0Jn*SdU$K41D4&Z)KcuwY^>d zU+N`7pq4}3@=w?u%$+6K&(U0XC%1E(oPvJUR zvarsm(di`ZR_m}tI-gDwY=90~t3IZw&_vYwCTq-dmX_SkM8;yK z^c`(DYql6eCU-oVv6aIq5@+Mya)@#U372JlSC64A@Qrpk5X_~)*EEW-CK#{gbyzyC z2Vc)>sdQ9pB=96&&T@0d*BOX`rd4o;yC)XLpfWaN6 zDJK~;r1J{7j!Q+XShE_65_PKJW3_xGop4&^`9xT#U1#Kt@<^65@H(ok3WQ89AkrG? zR9t$d;>H z*c%`iGg~if6S`z89xkbyTpOHn;^*42R$i9~7R^QUE9;D8z~Ut;?wl)Cirj-Bbthc-ezozRpNgbm0acZJD)clfpxho>H+8(yOhpyot>D%4Uy2 zRg0N**$OxBU1!9N4VO#d!Z{lWMmXl?U9L{q5hfd4DjRVqRaqq!ReKw44>#9~tGL#} zfOEy7;bhQIFnYB0xFL%bFuB>L4>uT9x)30p8QxG+a^kC@j(CC6#{9c?I+V-Oce^*{Z;Ipe{xN>G}D#%$KBKoOUyR$Z^qkwnuH zrsMLkSHdf}a2BIs+%`o`kWU*7(W>w>Ot5m1==R2(v~BES`w?ePzGZ zS&+L+MThtY>kLKOSJvi|fuJYP>N4(9uAwqBjDilfOhuoTDB&RmOB5Wfpk?0GW5li5 zxT}d1yoU9KI&O|LL=<_0!4Tp#oj5o-O=WYiL7w%6MIY`lOunkAZL)E6%@VJdoXwKe z>;i#Ax&p?$-d6F4yj4w;04KJJUJn`ZjwR`?d9CG;GS#d%F{{~Mw;G(9go`MZaJ8X9 zYOTf~YqC(5`SqD0-gJfXsZOxOW3hx^skMS-Q9PQec5HM$s3=El){={@xGg%1QgFq( zj+%+A`S_SFV9%9RW^EwLkR7&xDdR+{<1b|61=i-%1uKNpgDmzKBoDq!52sx*T7&DN zcs9?sm}uP$WJm3*B*D5rM2 zGin2Gau=FdBy15qw9Y8{Qh9UAUeJ`}v?I>C)UB*JU@&3e^sZPTlPv^^c7)d1Npn&7 zzI6s);DOo6#@;}Mp|Y&TRZp1eT0=>}q$_b-Jl%08+;ul?%F>eidklx%i8(1yX{H^| z*Ns{((Fia>Ln9T{`@$ib0BP^Al=zN`S`hCOfKyH4IiHuQ&|WvL&a#G7*5$Cb{ib4v zZI^vb61-C$OQ$GLkszb%vu-?D&INs(x9;HGjM?mHl4ZBtOC@tDzs+IR>)dU|Q|x4X z9abxR^}0>*jyK8@ai7z!uG#E`vRYF%W;0O(V+$l)`5;f?d7rZw@u(T$`uaE?wdJaA zP)*4fPG(dIv)dS=Vsg%9Z!1HZbit9YrgAy0Kd;m&C9CT?m=@WHXJbB!cLdWxxgD$6 z>5{P#wHYF@8s-QooXTlzTT=zE5qG(YnU*I|ch=q3 zj+!FavW8oDyr(0iAT$cngPTfC*6I(1&004&13X?XnVn9*oG5Cv)qvVjidF@Vb%r-> zh+EW2M=PNy;X#!uTC3FDwPq*4f{*sGxW{Nx`Lv*r7HygT{yM`Q_9bys0_#{(3BAf} zA+qI&)?(wUF;_W~&8pST7={@t4s~vReXSICyE<(XuXAPdg?J29Lo3;Ey&~RX+6}@} z;oLftIoGkq+6Fr#*|)C4nJZ*EP9yHGMzyrxtkzl#ZH2LtqxDHw$)5yg6E?IBP$5Rq zoM+Y<#ze{*tb|%@L2cB>t87zQcGfzvl%25YJGw&MV2fe~N43pTg8Vu|8#d)w-i8=!QW1>sbJaBjB?6mCu)e- z2#e8bXy{3k4wQrN>qthjJ|o7}p*o`qIx#8}OqlGXHQjQj3r$ZLvumRjP{OCpaN3SP zNraWim6(8?-yK)`7>g(F!>e(BRFR=;X~q{ehm~rdmyRY%?wmu#R2%M)qnT)~8%1K} z0@ucCiIS6xhtnRfNf$2YN&${nX=+%|+E5tt4S!6Ru8P-}d~rqF+faF(4X=xB+BjX> zTIN}8NM-diQH`~cwAd{kr8-V(>tXT!bsbo=4Qh#DO)`L0%CS^nlP0`~ClgbFsOV8<+pQ zd~oSMm#$y@(_$OEt>2XXhqR9V4y_{phLpiq07da{#Cg%LMLFTGgc-rp0&d}{1$O?) z`NZ7M0Db(w8}3{cvaDzK42L4^p5dsWtq=6M;m!-80W+MHG|S;a(-`p5nGKlX+@Wa< zc*)ELY_kh4SdW2nIv6b3?bZ&csg|PanvA?qZrN+5LbaSCF&@`r39?mG`c&cVcER~G z8?enTI1d_tDLHWFA(%9>HC-c`Wff$(nF%_fNhR}GiEkmn`7X`(0~~Z z4%_Y+c+t!TY_kh?%xu6myI=(xFhgW-w+ohMHej1wur#v)+w6ixXuu3Yx7!7>nGM)x z7f7K2deWt2wOqkft2pY7V%3kOon#3V)T?`SR2=7nO{$Wnm@;3Xc5kx*C^TS(bLO_U z49E?4A~PE>LluIp>ml55r(|XWW~lBljR9h4zzk)MW;s7@8UsW#8?enT5YBACHoHIo z4Va<8&33zBVP*ri)dkYIx%rB)G`I5X$}=lZubf;tvGU-`11rZ@?p`^za@Weyl{;1r zuN+#rX{EJNT1l@QSP8ATSIjH>SEv+^0Uj&EI++`a{0vagUb&r zA78$E`PlMZ%SV^*SU$XbX!)k))^ce%y?kIfwCr9sFYgEE4PeWAmRFZ|F7H@Iz}W-O zEF%XtOLr|DUAklG@Y12Bo0eKjrKR-Jfu+!rd&#`Ce~DVc zmi8>IF6~^}v4kwmEk3*W%;M9FCl^mFJ}ARvdt|G!ow6M=L^daVR{D(eY3WJn3F(8< z2c*ZPcT0~+?~)#s-XT3KJtVzJ+LD%}Y3TuJNa~iFrTalu0!+F`x+>i%-62J!bLg|^ zGw9RkN%REzAo>7$9K9PohTer9MejflqleI&&=y)k)93+ExxkH@(fufeV(1=p72S#M zKoN8fc@}vFc^Wy1oIoB#9zc#GcO%D;yO5*E9mrwi5ONdJ0_P{BkpoBwaU*7AKSCiG zvIkiOUzF@X5M)mBEV%pfwB)4ZgyccV1CryCyCugYcS(*)?vNao9Fp85X-P_wwB&#! zBymg3lKm1&f=TvBRwX+nJ0yr?PW-I+8S&HNlj0NN2gMJFkBjdX9|LDI92MUoJ`Ack z+yuVnD~Z$M1LBa_EjEkyiz)E2+#d0&c&B)W7!l8jo)tYKdRlZ+bVBr?=mF7j(cPkB zqPs*#MR$k}iw=oy617AnQCf5We01d&nMM0Wln4{;5v_`Ligt()(VXyE;WNUgg(rn4 z79UtVzIgZIvBkR+zl9hKc7J1jdSyGhoPm1Jqz0q{w@TV|H+mr=q8 zg%1di3-1;l6W%2}D!fB@Sa?WyldvT$2~G^hsNg}t1A^m%y9LJtcL|OP?hqUn9C|Kp z1vd#=f|4LDIIteU;LA?;a#~;(?BBTIwVa;6W8>!+aO)9}2o@I5KcRnu_($}Q5T8Y# zh4=^b4-o$Y{SS!0M}H6Tcj)gR{s#RG#HZ1xA^rmW1;nS&ry%|u{W-)Z(I+7uLytlH z6#6NMpF}?i@e}ALAbuSEIK+>kAA|T&^rH|zf_?9 zA>M)B0r4H^J0RYU-VX6L^friZN8b+dZRpz|9zl;lycN9_;$ieK#J8evh4>cqEfC*~ zz8T^z=q(W6guV&lU!#8w@r~#kAs#{xL3{)H28gdmUk~wh=<6W97JV(mHFOQ)YtYv~ zycxY2;!WsH5N||pgm?pb1H^;qL5P2a{uRXQ(d!|0&GFpaMLQ4>fXc1xoEkMkpd5Ap9L(HK$h*>lXF@t6xrqMJ+4&@-G z&=f=#Wg#ZfB*X-ofEY*P5MyWz;sNvk#3&kt_-gdk5U)e8gZL`+RS>U5uZ0*vBM`%A z7-9$wK@6fnhygSJkwF=Ve$)@qhx#CTQ7=Rf>VfD+-4I=<3!)QsLUf=Gh<4Nt(T3U} zT2U)R3u=LAM$HgSs0pGGHA19O8lnL;K-8mph&ogUQHyFJ?nn1Syav4n;w#ZtLVN}K z3W!&uS3`U``f`X@p;tlFpc;r2Nib33m z?t^$GdL=|TDu;LldIiM2=w65~Lth5*a`bYDd(b@)FGDYbcqw`*#7odiAnrzYL%bNh z7~(E;7sQLuiy*F|s}L_lFNF9~^raAAg1!Xe1?UA3&qvRPcpiEl#BxL< zLHsfLV~CHUk3u|wo`CoW`Uu2_(T5>EggylEN9d0r{t*2k#D7Hp5#kTfA3%H%eGuY5 zp#K2z`{?%}eh>W~#P6cth4>xxI}pE(ejDPq&~HI}0DS=BH_>lG{090Bh+jv)4)JT~ z*C5`H-VgCU^gf7RMZXI1IC>o7z39CVzk+@R;+N4cL;Mo@C5T@{zXCpMm&k^wSXk9{qcWe~12E7m+8CCm}w8K>vU<%AkLc z#}Visuoj2@L4Jxr{~$j>pns5mMxcL?e?p*tkjD_{ALPdf^bhhV0{sJ4^3XrXBM9^l z@-PDZgFJ*l{~$j?pns4bBG5m`KO)dSpsE@457-Al{~-T>K>r}$N1%U@?;+4X$afLw zALKg-^bhiF1o{X076SbPb`j7&$Ttz_ALJVd^bhiN1o{X08Up=;+>b#2Aon5AKgd@R z=pV2Lf&M}6MWBC>uOQGr$d?i5ALL62^bhhy1o{WL2Z8=UzJNggfE^6<5At~g`Um+O z0{w%07J>djK7&C2AfHB{e~`aNpns6RL)QIs3^@k<^C{$05I>2065=P2PeA-Q@^OeC zLp}!aqsT`gegydl#JiBYAl`}G3Gu_ohavtg^0yE_gnS6%2ayj#{2SzNAbtS(0K}un zQHbwH-VgD8$on9^7kMwl_aN_q_-^Fg5Z{Ho3*tMGcS5`axdY-mkas}59l0IiZOCm9 z-;TT;;@gn7K|F#Sfp{x&E5yUdVTf-<-U{(8$Xg)38F@3rTaa5Iz6p5~#J@)V8sZy~ zH$psw9D?`;yXz$d@b@?h-=6i#MdCNfp{}=GsK&an;_nZ+}M5ofA!oO z=T^S8a%AP&mEFt#y8OB28^I3!MPT=R%@VTsgT;3(DrA2GWtVP|T`c{z^s~|%q$cSO z^cIvsUyA$!>}A`C4v|TIBymd40lUnPh%4fki{1}*gbLxGg!c*j z=Yct#X*hXc_fjlUWdHumFcI^ho&1tpr za3V?6JsB>V#LVt?A{;EMgAK1r;qG`<%1TOQYAX#*I1mR(YDAZ?Vav$xZ?X&wpn8}_ zYKCd}z5O)R2pTm+d-Mml_0z}^H1#m;`#xoR!*itCtO07Ap^@1aq0+tHaizyS^tTtl_x?|1-DTmirz%WI@q2Tm|WG6_cY$WT- z=o?M&vgvwq9VGXS=+bbsKYD3nlVu>Lt{g!d4#gAqUe!;N57U&xp?Km;zJA&jBWS~+ zc;emf>!4X)hZ=8xF-2)>J?3@)5M*P&{$*jy~GW=k6I19LnKfJh9WZ ziMVTb&FO2j^I1ib%$PjsW~bCj2521-s`@xZn&FJ`44+hHRVAwzx*AM4ZOZ5=x%XxQ zVNQ<+U8F*tbD7OeU&h)9DhfQ^ba<~N`{6%dxXE;&?ZOeX;mCgYuD|c6y>tX^IIbW5+b{OhUNV9<9CQ!AejsZv7(p8j zx`)>WhUEMawBew8*mZTkk>`z|4F}!Bd(P~m-8_Hpdh}46qX!=jyN9JmHW5!nk4gtmKUKx(L?}`3%6K~h8Q! zS`c!Yh!LHcF&z@_CyBWS}x z`lXKzWYUfiwBaEA(nsI2saFRNaw{W7LopnqUwr%K)e(rkR5~ti(|LDUX?BpRbfr^^ zHMwlirOQR!ai7DK_0r*Df>9U4z4h$!uziZ*Nc`fBn^)Q(5|>8Mh9mKwH-`GUMJ|q@ z4M*ZV=OBGFPBv;ZhC}h5l_xgwcJ1b480A&r`E-?_6!}WgspFFsHSaYf>gAj<-u4$T zYnq|UIUhK13~X}_a?(+XhJ*K>#bBQzWiFs8>V0ji-$U1Nu4D6K>O$OOHez899%M77 zvdya@Tw!l~W66h(7>MCuzenoXqz4Q;GJ-Z7yZ4BG)lZX*pbf|F7vB8Qewug$Z8&y6 zci+4EX`*2oHXOR2x&2f9G~o!^a3p@_T0uWeFoHH5iJv*^?ftZc5wzh*{0#C?KW%;l zZ8#7={Uf@M*4_UTbDtfx|NrsQ2bZo|5-fgq@wUZRFJ1`t# zZ;^K*`y_vm+$&+k|02Fwe2(Z7BE9fY;cJAK3BC(TRK7~ExbUNe_bkx!znQ;xo|zW` zHQV#gS(IG1SG3^mO~{0rP^uN1azTkJNfO*Fc2<JkquA=8Y=`p~jWU&4G4Gpybj%fo@kw5PCOarrg{wx1efq8aV##%}^k}iF3>3R@ONdZo8Uoui z6`iv7n~!V~d+?wtJ;N3K)cyVvPN%emhhu zHKrQjoS$EOcb^?9c92Jcg8`qrZE?zu)!+5;!b!;6xHm+9ys_+gaKcW`ZBd-v>HheL6gfOuldNFBI9hy zLmDh(iLkiG9?YJ#a1F>RAs!CQZo@E)wD)oWaSD? z%F|#OEf%9$%A@z02)d`~)q|RFN~IVPOHaOK^Sv{0qZ7;KxnjYYOliri+nVg?l8Hz@ z!hnAP#uYcmTAFa6?l*KAy=7kdx*<(!wR+P<`Bb0YbVuLl=|gIR%iz%ZA{9bUstw?C zWV+qS8?>x05lJbjq}GtNR)`3b-00J*hBT?bbz?ZMo_yVF`ZPtcM2>0%V{J^%yTSR= zZSc`^&C+n^@KRG{D$yxNrAYeQR45K_-X1)dzII3x2~>^H^qT+Gr>RYZZC0P5WtEq; zoX*+`^9rUc2en{aVU;4~2{RgSFDIKIU1TpJRFQ$HLP0615mVih>?bxU0#jsokf+v+ z9E!wseY|vNuxFS?qZ*~q^z}`|UAu8Cr#2XzC2x$k2ilZXl``0L7I(^-4_WLqX;jOb zfwHHnSM$tz1p0%}!dGuxsz`7+>c{|GsrM;TVbOe*i?X9<1!n+CJvGIXDusd?F||JN z<_r3ylEfH!%z!)+oCH52c%LYItWTbD+)BhhC=bUJBLe4%)aGT}uH8!IX!~v^e9&R9 zIq%dsc2^=Jri>@()qV>{T?uxvl}ZsD8GIRQQr%!SK!Ypmeb`+FNg(lxx^-<7lcdbEG?4{U*~BWRn# zW`pK?NxwGB2->Ec>(L(j?InFQ&OBmAzInN|!TUvg6A!EwxqP}-B6S%rT_Vc<3RMT? zH-hcB%f^zLR<&Kvns|RC(jchbb&E8Ouy50xwV~(96Pxq^Pa8+jHbvqF?TO9HA3&o= z&^Ar*8#K@6wG5ycM$m@i>aj}(p3vz>(1zpcvE2hVd2}Oa!*TW4#hv~k(GJs8n`Y~c zzFma%)Ao;`4ae1EqG$VQ*NmWzh%4d1y^U9nplw=tZ5X-m&3^LTOSR) z|39}l|DL(|_bmK_a6$5Jaau$!e{bnsi=0d;okzYum)V{zJCHI@Y@4Y6Hw3*#A$)CO z!C}z0!g#9@#*G*__OWg;!Jn%!lLnl3SX8(mK|6o1jz8r?9VRut!`H}Wi^%LvY+%H zb^CUmbAI37Pv@N9>-^3cm;-MF`Asln|M|r?WR1cV4OubIG*oxO(}c#e8M4t~%sMsK z{_tTfLu7(z7W+hRGIgqCr`_y#Ma)XmRef4DbtK<_+D!|!ovdH+&9zUy=!Pt)UzuSl zCAsZ#1jXw#%fuV;I>@c0U7%&KNOZNFQt<<>?ez;&QRIEbwLB)qv2J3T$mhqH5~Sor z*N4ruKYkGo*;Q#-ftj=^GFMMJDLSQ3IXaReKGC(ptS^?F{>+#{Zg-<3a zyoh9*dbz7*Jr%+$O1c^YL-wf`-H`bW>WU1rP%+7=>61g)Ehox0t!Kiz24A3MXi2R> zsd~TA$`mzI9ai*uA>=Bu)WCFs$Q9%vNg!x*F!tKOF#qU9H)LV`s@y;7q${ec6MAvn zF)**3>Ep13)Il!QYzUJY7_$HJq8qZ{$`%==px+qsDmPL_4J=g^&i5s3`js4BKsW=wYgmGO2_Z;)}Tm6zdso2-I$*{5H8Lw04W|B{|zQ$?Z1 z5&=jsnj-N*ns28sGE6%+GI^DD4BW~wDZ^*giCeA6HFYHTAV*a~qaYNK!8K_dXxX2> z2#4&d470@ft&tY*X+1+ol!sKR=kf-9(U4KN-%R8u6s7kPWZ%&zdal$i(gr%ohJCJ@ z5mE@EcKU8CKQPz+WOo0*x%R`?4qm$Z?OSgGfBC|n7x@Uh*S)uUT_%P1duvylq3xYd zxuzzMx@zyKw>+6i=HL9}Q;uuPg3&eYo@|5jeDLo{D3yKpJD(CfSLx0V>_OPjlVAPR zVT4pZPXWPElG)7ZBP7as>K&kg2ksi81DpwPLrVsnPZ>ux$cuY4G}Tep_H@~q!I}Nj z={P-gdD^uZ1BjjgLpYQ7Rl>*|#lWpjJ`s;6y9p?fi^pS`)Pom75EPHkxFnFV9fVx~ zws$`D0)Uwi%M*1~dg|SPrYBpj0{$%x{yy)t;|lH!`B%?+G5?>gCWkYwnMl)C!OhI< z|IThB?>Be)I}nm#Ph`70djs&~B4#w7&nI&;g-`@X0YfkYNF4%tx$Bf<6Tg`g!MGud)4naL#bZxOnU z9&FCqsAXvG7~F&@R&C@TTZ85Vno?vRM z!^13Vjd0A;8stQDkCTIZV<@Y|W3~W;%O=n%Z!|+B&(t1h_CUU%>+gQ|#y4sAc4l_> zEz(*PZNFSUR<*m#MRpeGbT?2sp9C#j=x$1rMo^$?Lk_a}Dv~3o zN^{Us+pyNhj>%GCY-c4HAA$n{#D8peQye5~w0?(hT9z`!>Jn;rxmsaZ@Hs-_$D)y; ziV_`+hhfrS;vR-G6{$Kj2MR~P2m=aK1QS15;*Tu}RNvrhW;s`M)xuyUcQflca5wLr z+`B%X=EJopMtgPcCjGZ{H?#TY+%z1;U(Ez8xer4&L@;M2w(MYVu!%qQd- zke=#Tx)3gG{;5Yz?=#X>#jvvre2-prOsR41-5j7bx2^f4qP=D`OkxO z%$o!YM+Zca&g>&S!!buRix`Tq@O{uI5ovBpjeAOs#fph zeJ->zW*lZFxgN?5A(Lnf)WpaJ^~T3GjE$GA|Kz%|{>5uQu_mqU zUs=rmgS&F-F1EQ&%DdR@DUJ-pDb3@KLX`MP8EHQd0XEy$946XnIhAW?`T}9mAiBO; zkf2&4;PMmKt08jGkK?kD=x78uxw~7Ki!H1m)^u~#NC`3aE;1L3tRS{CPEH$QtFD*yY8aAb5LjA|^CLOY)=49|R6Eyi8wQ7U$50uuOlH(+QfLCE z?tax=>{qQIwulsXP*uvFPF&W2nIzU#kug^8Vnj6%i|52Xqf z-=#D*KQ6Z#Oc~Djsk&WN1*U^JK6Ur4bFpt-LF|NUSId1eZ-wPPcJl+`UQZcC#79%HpsRii|mLB1G2*eSQ)jJH32+D7CLUqbge8AbU`q0 zE0y7e9#@X1?M^;JB&dSM#=sJR273FJx!AX?AXXqsqH1cS2B}=KhWcU=^D87WcI$9f z;5Z>H^#saqT4W5M&CkW=R}hPLm10ZFPrS*aniC8tOO-Z)wGxiR&^WA z7DU~J=3=20#CGLoKdEEgvcOho2ZZ$7qzY3l4b+4os#Q1?v22DNuvte3dYhYz&8;BT z^&91$_uXV90WnxvX5{r$gYK{ZOo1Kfzt{|4DO1=(N?Yvi` zjZ!mKRU5@pqTInj;0_6@a%iQ5BxRLGGvKJZnYq}^ZGKW?#1TFOtFV?@ENh&-tO+&Q zOLRR0t7vJs9+p*&1*Xcw#pxI_vbkihW4HR^71E;hM>*mS#7)1lf#Qzo%kP4DJ4xH2W`nFLmJ%4EUi2c8;~ zK!uSWMoXv@~K|{0#z4D;xxL%k^48}=Njn$Zp`Aj}e-Hp%1 z##a#Q=du{M&LgZgxbe&9ik&pt>l(3IQpv+UaKF<`zJXLw#clu(c03n*e4C+F(Wc6p z)Dru)IS@%^SrZ(7&>6u_SFOoSsl&BfX*Q_wF5Mf+iW7{=sWuz-$Yd>#q^Y|{bFoJ& zh$ZBDJU)%5F|iCvEXJ5l!4tr)vem^Q1XXIy{G=fSVP}Ga0-SeabFr}%#7csR;++!F zWjPU+V@4~Us1>Q0D0gUtx3Xv@moXWrKlTWK_RVv#Z(c!cOUoNjyv&NyPDrvP6GAs}^R!kIVS_5NyXD;^63Sxt?T@rd-KIHf!>LQvYIZ(@0Gd-)7H9RWY zjTe<3sCkvrfd_ljTHU#<|!xt{}D&Gs`e8>p2XRcH>Lc29$Rh2cpvK zq-lt0e46Fz11#h^7#M^%%*DQ81+fzYo0^Ik=IcpwOs4u?N_I+9zBIN(+z)bjXHp89 z>3S)V0AgQ17yJ6lt0f)gJ8(WTN?W1}ABdaR}eeQcGFX$HD%2NgSOf=x!5uET4*b?4g zGKioLC3LhAbPM6M7|KfMjI|QskYqod$g1$9J#b4&u&Ozni#=RHY=m-M)ZH(ii~aHy z#6~E>McsXBF7~Mv#73xAMcutU7khgJu@R~@QFou5i+yqhu@MS8QFmWE7yH`FPZtg& zly;)-J~0>j#0p|{+7%%rLs{`o9Ul#~AP(NWB%LZ!k~<}x?Rprhz$HHf#Z9TZubGQ| z%?e^8R6(Nde%V~?m#rW+LSY^1?yKiwU%i6Z2*qNkyRVvyebowLBix!&cV9Ud`^pu> zM!2Jy?f*B{K74K8-~L+gpD+A*QI5d-()advWb!?U=e~R`ik)AsEAM;?yv)a~Zhs`dlyXL4X%8H=(yJmlOeD~ljEyzbTBfBU#Ab^btxR)4chTl=hQL>6l!;C*FW}J>SDvZ;*&d&~N!KYa=tcSxN=%8GhTOXl9r0Z1L zIiG`$eXEt?G_*C04@E}k&~ngo(xi(TdZJwjb-Fi5(V)`1gx2X2rL{XC$CM|=iMBVL zfV+}RSpv@slXuWZ+(&};64}}1vn_**ElGB|_rE(xB+%~XI}NYLWIGkh91q-@s90<) zRe-a!f(U9Z=^CwWg6@IaWN^CtfiJzcH@ix?ms^X%x{BLcEmjG7vAZKM%u%j)&usC9 zr~?pu6=Bgc7w|~axO$tuG_L7k2f7G00HYGY2cL7{4|?D%kafuoQWURpu^&DS*ZQ0M za3Yg@WIzMa!FWb3%|_gB;qlh){(_t6Jdv zz^UJ{L{Bi#Q#dO*u-2Wb%pnv{wDl4_)SV#&4RVc2Ge^jh!!|^}IMy(@tkO=--Jm%&-2Wu|CTt*~uRO`{;bWA>^4ruZ!|1ulmERJFQu6~FJ$ zz8b_{{Y`OxbGrMJ&-^}fg^0Yzi1h=#IEUmc)Zrl%k5JZ#b~cM9ob_iVD@)Cndly>S z zVt|NJ5PCLW6x|PsnT4WU;=N~T{J35@o=DH)W-q+|M=|@@@x*$|SD0y5oMtt{o(7?1 zB)p`R1l780MK|x(i81u}QLO<6TcYGrcl`F)V$5s~Rq6rA2F2jbX(Cx+O6iUv4bh~? zA_+L?Br?qwJpp%t=yAIZyL84K#R($dN+T93=@V)kR__q-3c)VGE6%zO4DrVPyRMb5 zeQaZ$T7PQolWQMc`@qJRZG6M#bQ8axy}o(0mOzDU;?}4Zo2R5~jn6LtRW!Toq+g zZKd3YZ&v0U!59hf2Ex|_y^xowoMIL#cwR#XLJ4cBD2UF63S=<$3mo>PPGk=IltZ_Z zUIi=EOa--*D#k-Sy-;ulLN^z0PEr=ERB1KdcBmw@@ePqVzJ9@B3|NPr$U}x#DwaVA z>~N&_+r2JlNKCeYCQ3zy&`33J-n8Z%VcwBIq(jUT1{Pcm<9UWELI^V9u^N{JA;Utm zQZ3aa5t=}2|8HcDKRD$8H5PoU=W;cH)U2>Twa0LG0yAC<^Lz>1L&(z#Dk*qPAMAYM zv>y?^hZ`ByA1>Nh`8MaJ|I=w3C0^90;0|91A+>9b(^xguvuR8khIU74$VqTXmew2! zq;0?Uzn*enlcZHj3`S$qrLcxMRBKLIzUjuecT=J1+D@olu;Y);DVGKC*vuGVbf@CQlt9KG{g_PrNZE4cjp|zR97S!+?Iog z2*{U&I`2}1R;=bHjAbab&~MaHI0vqUTx{>PrySw5o-O$_+XRUlDm68&C+wzCsu|sW zjKjzIvYGF8hgPzIk!!`s99U$I!YPL@q8Q(x6a1)A0r7C{Owp~?T76ThrYuiI+&&es zlW81H#@AmSnd4=r9G(ZqjkKnsPM;GC>0-HFI_AVuD=qaUH-%A9#R?UsM3>VqS&z)I zcFJM9i3&d=VGo4A1X5ciKq3@46<5t5#EN`FX6)ENtH_GS-TJ}E96tc&usKs0VL~XM zazvQe7Qmblc;?Ku95cwKS~Wjrl&k`lPv(bXrkdpfOEr}o3Y2F^zxz zUn}&(Dj}66r&~_si``mHp~5;G$CPj!#0aPSCI8ncMYDFa}!66w-x4Kam4bA>&G`6(qzu&hjwX zZ#mkRoK8An1dh{5Ckn?ib5ucuv3gg0LtX=QG9aoYCecAZ)=mxrx-95YWtb$=<-QB! zY0|)pij?X)l#8KJ8IY(9@afP(xf;)~Zmko@O%SnAB=bE>u|_i9$>k=HFADvBrdL(+ zHEn~5%t1%ypiVg=sCxC3gChzKHFPDs#(TY3SMn>#4&fK#yu{XxMm@>uWgA3%Y7_F- ze;=9S+fO+dt7UsC5o^$tBIUg*2wI1Wl~Hx5cXCdRNfEqA)Vp1~Pk+%nB6EDrDMw=r z33Lz2jS9FJ@>WCa*SZ;pCVf`Q5@VA`QWFGZ4h`DQ&EJm9@mmWH8D(|8(d@>9AOX^j zblVk>sMep_p3!K!9V4DZ=-5;ZGWCPEMCQmx=73H)BFxUe0(|0MT(-9V|L5=LV-@)E zbUUZy+8w3?@?_DOV%tYL#6+uhw02PyX=~UL#~fd-#$UNuQ%B%fxVH!#?>_y4a7_<$ zX%5LuwG18eAT*{{b1{YWJDoBV3{_mm@Iuk=D)cL#ip+8QwC4f1Mu=A?=`4}XB`akk zn@W@rI&9iwSrBMQ%G*UOpEjBq?G>kcgCGKj8M%$oX+Nk!)9;wse6!bU#z@xCs?}j^ zYVl$jYw+o;=w$U+aikVW?G#5BTZ;`vN+-PpR{O13;{)y{&vdU zuD=r0|9c*s5oESraWi-A*SG#^-vqVZbJsp}(B3rmKC}N@8-Ka=&09aa^P4xayMMj= zv9-V0{Eb^bzw-^ZKDhq92Orpd|G}$vTbt?g77uO8uUtRWvyMN>G|`b!^P71PtXUND zn+?v(N1qo<4-k{rG;v7iu4h5Xy;*8b(&$xrI~VRKrLF_R2Pf_T}BdUQnH0Y z+J*_YfvX&4mYU6}HcaMyWNfpnnPSs;`n1LMe|8@#LzWsoHq@E~U!>wCwr(Rxn^8+z zZJ_*be9GtTIhag=Opls3)KS72frTD3AEbv zPg}fMzu(7JfDF3%Tr&r<+S{I&stZDAP_~7PS?eOzWXZ?6HO1%z^##;_x?sc=y}%S~ zC}mbvvLpu@c-}gqj?6kEqy=bwsU@R6rG|P?9zSpc|4YmT6q@j_i zb0QT(`faY`*l4N(d*~E8WxV#i3r0ht3yq$^dZh+9QpuxZP~gt0WC^fssK%z{R-ciJ z$pW~hUG#C|zb+VcO($eC!L}Kf&&s3Gq(GzKMMu{Y#etlinmJ)SE(-l-9zJdHdV0aA z#n}!-(Jl(na=PSVvS~peXuC&Lvviwn6aqt)^L{GlE(T{~bHP~4rHYc%uGNR5EY-W?`DaDSrqt~ zSxVE4-WJfICoNjs{E<^e9phn$54}PK)4H)lPe`!62^XZ|5|YVF7F-+HL86r7niKN0 z#T(W8P)So!I6mgc13Ko0wP7hO3aNsvR2-PbNWSjI3O-vm<;CFaExw};i6F#UGC4sT z5^;ry`N0^=LLd}+I`sotE=ozLJGC2fxM1A9KR6N-*U7Aqnha#U!}Y1`)XNH?G*Twh z1Z0|+nmQiBX+fAS`gr5D3#g6swCLDl9BCFSIP9evn2b$8B(FVe*NYvYMU*B|W~B6} zMISdn)c@S)B!rq<%9!02(gXRb5y+dmOs2@!ya1Fx=;jiP+8y>t+pq_V&+!Y37W)Oc z2%g-e+b)6IC~AgS&F#lxC7^M-n-|iYhkXY{pZHKkjz>&?v1}I-{{X zX{8!Y%nVowsXOJkLK=29XBOh8E$$`m_fdjUVW$sEnjnizm=83qo=f>76)bBtQc4*v zEL9kAB(boLorNKTV3V?$m_mHLR$}Uyq*Fep4Jeo|ag&%~jPltKFAca(-Cfw@roI@Q z7?ol&EhRk!HA-_GpL41b3As!*o%E+UZZPyjtjtW3frXty-MVkcVXEr*gIL#eifkg$ zNhFOy9JoWIT^_flwFX8Fs%EQZHoZk3x8AVmW0-`@DrO;a3J$=MIxJ0*0-e=U7GqZ? zZgEnENJ~q{2CapAT>F&;V~`^|K^B~;40BKzgL85LoVfcsHOvKg&46rgJPMIoS1T;$ zj%z=*V4P@7Q^@Ov418)!inC$R4~#f7LCZmR1a^F(jTn?b(CET-Y~7zbCSPU*qXF8!i9HSj8)5wmpMz*1|j+QE)D$dRO-@n~VdJQ}) z^?WM>dpOd=a&e)}NmVfw#NmpGi4-#dXWICEtGaI-%}kFNXC23>r_)ePcZHzQ@xb4z zLYuC7u8MuRy#ktVC<4zPB1FvagBs|EC%NjL46Wd?KT$Y1*M!W%Lx(Y>FIr4-T0{mBj<-U zmuw&TYC~u_UBv$sQnJIoap(GA>a(W-OKf8-KoF?39{(yMy7wJ7Jtf>%By6 zLWy^hRb9jCS|?toqynd>)Le0~T;2WCQ${^sr2;874Pryl)!p&XtAZ#I5xo3?>z0rr zn^RdFq@bK#Sj+(HznTNn;~H3!=O%3+IV|gAw5ruum!U^g6{&|APNj&hZqRZe_Yb#%!+JSCW zr)poWWI;hbF2VJ!*o-%I2i5 zw51*a62G>(!WcNEc_yWu_Ra6VHajQ&yYOex$AqJ5ZOM0XEO^GFmRhY&cAV}c%fq2k zgj!at#5RqVPrArLarQoV>y%S-E0FAU;faF{GGp7z2dS|*=({FtdZb)I{4$LCrtVAf z!js>u-J<_i-$T=P(9kV z)r^AjIS<4_4!c;Z>r+lpCP(=$K~_L*PmL^Q!%B9cu50td4g-A>V`V6kY%@ z3kUxk=l=uYLb=SNBcnl>xhvIpUFLx}GEtJbnO3%R{4Ck>k zrSMpl$q;IWz^fIh$>mU#z*8Ba(mF0RStvIg zHPEcjr0AAAL{pCKC?dF-!B)xtx9B>ElmhwxZq4j&mj5q;GF_eBB`*qD{G8iSR6|^{ zqlfa~0W*6r`0#UNNtfpTJ8z3$$o~ht&gU%uAFw%4D)o`!@gf zoKN#0|6c@~yE=E1_}jXh*(`eQ-B}cWb-^@z78L(^a2yXmRf!ZrpH9qHF&Bo=-w$(yCa{JiB| zU`rQR$Hn>oK1b8)+#sXp|2wa|voCeuC|-EG`ofEyS+^g05c9W*|IftpkBt8(*y*%V zgN{MDvLbaSWH7Jc-Kh#ZG}S{`se>X%K}E*nMZ5%_I?g~nMpo;g!L%D0kh@7L5W{M< z)ycCusEeB?j0wWDVYbySIR?sNgJE4!a#D?>+DKXPntB{6>HWjKKi>P9y>H$7y1nBYpSkfbZ+yoM?Z#Vf?C$>B?(@5oU2OMDc7AK; zhj!k*Q`>p<^*_J<^VjcPm#?Ra*SBB2_5W=B=+=9;sI8|qKe_qQ%@1r2 z!R6ldjeocCp^b0c=xr1>4mUQ}e|i18*Uj~}t?#e>`?c>|^VhIzzrFTS022H;e)i@r zxwf{mv$MN?yNC2=<)8a%QEpeMAy+Y$wqD!o2jbDQ`+#Hjl*5%c$;F$hPz8m)1~#kH zOB`DV-0`yqXHB>QOt7O8!jD|omRp`ZT4DfAu*c8topJQ+5!-gFtVWQU>5XZ4aGs-f z^z4l@P}8f>iO&YBocJ$p#vhH9(+P@wBd<9rh@d(E@!y9uBl z;LrN)2j8Hl6L~0eaO*YCu07)Spq6sx;NUgSUc31Bsa!IfIoNy6$=9F#9(n-4yzKD7 zjn|w!bN2h(x!>;|oxFQC1VDv>&QtB8B(Mb#34*pTyoCpo2N-4t2ipfb$0zSP8v?k2 z^+4Qjv)Jp|W3k$7)Rr0657v%O-g!34K7x=O)*Oi$0yzyM{{b9O`;XZ7&caGA=6&AJb>yKj!)b(4zC%o6}%D_Nq+*;E%JnOzKx$e zI&lEU?hg3G>$hngTrAc~tXm#c3)HYRUCy?+z5k=fC-zwrq^9*o(@D=B;zF^cP3!LY zCjRm9$@q+;;Ei$3qWT?_7WK9robx!_oBJO*KC#X?2w|iU20CKE>jb`4v8m#68=L#z z^V$>h>=U1>#SK33|8RUVI+F>KR3TFl$14qr==kAiygZmY>-+!B(TM>#7Vdjo9(Bm3 zOOSn}XfjjUZ7!kM+JFDiiGIca0{3g0P!kbL0O{&PoDa@1y!YrtJ7WM(tEQxhA!S}M zrg*tz>*pBWb96F1W9S#kZDC9xir(-Q*$&#vgTJ-^uA>w6j3I>kPCFc1l}>S_v1GBb zY%E**{?UnY#sG80v26rCsuxLJq9|^B4w8L*G5`#VY0<(>Y^0E!;_G6C9al+gi2*37 z@!FIA*|&Pg=_S({knVARaC9P{foPx@4iBXU3!aa6%2>O+oM?A@|LcxUdNYPoKQ$hz zLlWuNeFtq)%%Iv_o||{}`J)r*j6oX?X{T=bAb)h5Y?UkW^1QjTPsUHgMK{-goltP2 zRj`SmEP_YnNC+?6$?nF+J{~_2&RFWK;+tcFoydV)D6|_qwKOu<*Vp&odUW!2GgznA zI31Ifei3cyZLH9&>SQxqV%gb;j!wFBhPi{TjNmHB60L`-Q>3jyrRgs*Z12DR=;Uk9 z7}yrfS7ZfN>>11MVQcSCk50HV29_2JUbCRqDYn8FLbxfNYvGeeC+rymqtLLXnQ~HBK?`&c5XjQnIffrO zIw8*(CVXkgVk~K*f=RO@4L$#*zyIi@dB#venkB9VviVA|gAcU6cg_*)eb3QJ(pST=uBjdmlPFASqjXx-ixH zMPy)kC`>eh)-t56z2}ZjYG({~#{jElWMCBQJ!T^Ho99RAn~zSaXAA?j&$bE_D>IV@ zPWA+WJU6W0cyv-ZV^Ba$WV67d7-k@fLsTl~7OHz+e{@nlV-VU^qS^-cez$%~s5oJvy&T*3n7v zjDZA^);0nMa@p{&N{?#2_lqn%&c zdHVV{f|&m7)(646{kLp<+xnlZSJuAg+NS`+g@4}jz zxP2HQ`}@PAc)35Eq5I}H&3?ZB2Ofp-!9C~PcHVsNO*4!S*?NRj@(YX?*!rb-=eF+t z_zbWAjVt3ly!XZ#-iK^GLXP?c-WOs;oXfMn&u!iOi5brE6D#9<`raF6I3Ke02ubf3 zI3ElKxE$%+*44%gss8bmkv@6v^-Eg1ei$K-enc&u+q(2cGmO1kD`R}(-qXvr9w8_H z0^-8fFICJLHvUIvKfn5)t&H%=d#_s>#p{O=^7}tLikB{BuFufD{kvyBk*|Lg#`Hsr z%VZv`Jr0*JZXZU->K_r~eC_cSKR?6xR_;+4Qx9PTbNQR^efbRIL;fg2iGT&h3;fZg zV>xU8QBSh5Fxj(uynqhqNx2%luiF>bI z_D2zFEi51|{L!VuIK%b^ZT|BYRz~>by(gAN@p_cH4G)jvr5mNWKYGpkW*DDnJPKp- zAzPpOqt`59+&+v@tsx@Dxj%Z<>t`5W^<9s`xEy2({Lz=qFh1muB9wkuV7$N|T{@<7 zfAl4PIm7$1Ppypi@ZPIucpvgd5rUu>cpvabm*SlJqnAK4oG*FgUg+GoK7H?1Gn^0k zqXTJwy6Mw^tp}+5Z3bwdbzgj2-;q0kr?|{r=vk_vF2`8`h2O z-S6Bz+WAL2`Rm_z>)x%Sn;*UY^6h`R{nXZvZ8bN4bF;DW`y2fFC)dAe{S|9}Z*3;x zO8&g(;JJ5h%jE0d@ug9AWS8%79~oot}|BN|Tp2!l#<-&hUV)N7!7fjAxe zr;X?O0ME>uL@2qiGM9R9;t_Rwgd6Nhgsa9@h$(u-v zPZ}W``pQlHjo##3j{H+S7H4=s*Fh1(fNRF%VlG%ci?2HTqUdR3mT)p?92 zbFq%<)K|vyf*uQtPUmYutU`z7nl-ST9+ho^BmjCbJ`&672t1aC`^B=A^Fg^=EPlIR z(r9ed8<%WV)?JWiRe5NO8IP}`$Ek}JwZ?N_2k^}Fm}gO$%-E6?4%&H42^gcl~_Ys8P7#}+y(8;^q5lzaneG3HJ_1MMNmVo+YP4@3c{Om21t8j zG>UaJn`C=Z5nj2y2le=CK|3=&E|hXmbL6U{VwIrtBrf4&5H-W&Xsl8yyL^u6@Oh~y zRVCG2xt%3Fe#hdIa$;l9PBaKUWr2#cIZ`(=X{KKXnIyZ-biyujo`Va06I6rMR&MHV z*5e0bL}%9=r+WM~Gd!T{ehOr7v2?1F_ePB4lpUxF*9gn2Ln)!&h?8BZ0_mBGF~wH@ zq%Y|4S1&qkB@%q0)R-8SkVLu#HdTm8oKlKuwP^-qF_wc8WWh0{>m%fC!5y`zx~^)j zz|cyaDHk3`k00KroOarHP6v2qdK}@s^B8M?0`cF zCP%tCJ+IZ}hDTAwQaPjI$@mMKs~)~0!rlK@mMCEoV`3cb<~ccS~JR-1TrB6tm83Zrc=2<-OY8q7KuWL|-cUVQGAzPD^jSWIhaO>l z79Le6@l}*a$*c> zm2Af{v4q6Q$34fZiY=MMEs))p)JM6BP!=m%0Y~9DXV&#xOK-FgF)!&o5&`x1KnGVU zpA{FWp)#tQ!xl3b4e(y-KO)sAv%q;Lh|NF25javV2?t{;{@R?cN z_rQOP`hORBm5crG<@NvancS?F)rFO&1ZG-ddDazmWauP1q3X(Zu2`1z;^a6F^~5`7 zwdZxPen0*A`BO4TiZ|iGorIIJRoiPzqm09t-Z5X&J$}UdG^k)2mX6aJX}TnmB9GNH z+#otqy@C_*2JH+{tlo*YXt+}m?~p~OQVXiH{3)PMD>*QYZcTZjM@^x)8PHQy$gAVZ zz?}pv83)-e6L8#YPVC~HOs%B}CAX;EK?R8_%emu5+Z`N}ZIPhJd{HA2a$Irs*0I^H zz@YsZfJ^HCEr$NZQ~&S6C(nP&`hVwTf`NIooy`|2fMT_k&ZCnTJVsulcQ5MyeerB! znAQJ_;JvPHVn}~JCWZ(nw--D5s0-aCM}K}pJM+e$2d94l5U_d%c@RIx_5Yw`X0|%M z&@0!{f;xhbs$SIcIjfyaRY1P^W>QUfpoR|uUK5TIadsqn@oakT1|R4BKhjZmh78kHp)l`&;uL$87>Gy+F1LXMFvW*Ujxe+%~rUU`yNlPn+t-?`;g$e|KG8`<*rE+HV8)NB?=|`L}P%G8yGs z_iFD;pLzamkpW!k1?e--e`RCsz1S3L`RbYlu09@(y>NC%SoWT(RT2nPVG062^c;EHwN z%OeB0VjXyD4j{r2)K%-i?Z^PG^b_1z|I%x(zV^Or*MEBBHJcx~^{$(LcKs`Fx}btz z?ArS_zI*$}4?cOIuN{E=f4#lm+Yxq2WtN|}f02IZCD zCng1)mCHw)!6ZlL&BH{ejO&o#(m_G<+j4AZ^a}ZDtAnF*STY^hDS*^d8d6VHx!YL3B6XDB zkbqkv2q?10^pZ|){Kqp4V`U1;PiD}O?KeoAL|fk2LZAXji)1Fm3Y-G*M&M4_3zFoa zzIxrd+qma6?3&x*shVhzd~hz2%+B@;7}ywAF`;xqDViLQd!Rg~%ueW*EozlqlM_8q zVyMqshTGGCPNv)KrWxzRYf#1X27M+`r5uS)av0Y{vgMi~#tXGZAX#SmHm1-4G90xv zhP5c0)X!zv+8guXuWMU_!|FuH z^!+}o3*&yQW@*!~;k7k2MAslJP6BO4T2q29XZltNQFKd=z zw|q4ed}6FM{lRUKXtQlf=r!s}+2*YJ^jsF7PrQJkV-(;xKgt$V28r3QF*S*X<~53w z^hod7F+P>#>LV2Q7@j;Vusp}c(^8JpibauRI$d`dAk#`bp0@Idsh)Snf@bDjP}l0V zT*5H7$kMFG7s`evkC$^s9lYcWgHzyeP@)d$>+!A+DvLJVzFTLyFcX8huEG_k3YC{l zqdA2SQ-(CuNodRzM8O5&7|mSR@|qy4bO$p>xb9AJeWN|H+_YG}-L_aPbVFaEL>Sch zV-S1EHuwMe83wh)1fkwcfnAECnPnKl+SRIQaTJPaZGu-M(??6GGALMaC~B$VSaTeN zXw{SExZ`G90-8(;a7HGpcrWx&sov_RL$_DIeQ7$c8y`5szzvAfH1xZrxY>7D%G9lR zgDDO)*Jv8kK{*EMIgYbc7x%QoV9)~9F(8g0p+T;imnWgvv#Rp6DC3Mmm50?fg-shJ zE2X7xt8TT>K*4BfMH-_Dt}ZW)!}{H8*VcAE3sb<~Igu}X3pN{%cZ3w2ajOa~6v;-Y zbp*|l8iWR_DAp1vNsVY@idu+y=r+h)b%=u*tX|BbsWE5RL#o4>#U_EIB#o+g)fC&9 zwryi@yV`4T+^FICa7a$aKCLbLht263hK#~fsaiQ}SKM3{uA6#?!FWUM!NXCIW+C8y z_!dFJ*2FyYd&z2BE@XNT8*gOqZ_?%shM>9u957XN@L> z+8!Q&^~bV4#NM+uU(2l>vYB?K0GBB>srH4IUeEPQv0~LKBu2G~m(uV;Qo>W+oH!)? zL)242C{?-*R!Epc)u-M-tkAlq;jJ{@BYJ90Pmhdvr5(ctw>52mVGLafM+1)G+d^l;v>2h#*SRn(QH|d6Fszqez|aYshK%M488z8NdSX?lWR}m? zjRww_Id{;?8W|WttFDV`huH~FV*%$R^V9f<%M*bavqez%a-bxdY17DOn#KNTB1$Gi zuAq`IHYex}XZvl1gs0RzV5 z=z-dI7>sH*T0zIw#W7oja;+RvMPnT4Bsm-6JfjoySHSQ~XBfOhv53SyT*caYSiu?z zyMdW0+fFAOde8?=G##bV?bIOn&~AejW1`l@8u-*UKmp^FRBYw5@e)i11vFMA;x&Zh z3vzm*-L3{oNuoroW4MD7VfD=ACCu7aJr;(h!ZfE@Eji+17NhZuFu-~k*w<)EKRXfS zyc%!tA*gfR3g8tmz-JgVR+&JVZoNbDiI`m;2YQbm=Denj&^o9h*-+g^ryw(elRgZl zx+2LSzZ5M|kS_^!P*;)wFAee&kdauag`l=C3g=po>teU7hS#F{rQ)E{8qR01NI<(u(SW0`#-k- z+`h8^Rl7e2as^CwiQT8yKD_lKo8P%P*(7$p_TbMB{`Jk@yZKW$zwM@V6TkW79);HbyI#93Ri)O2Y+d?h4Xte-3 z_J)i8efi(OAgo09`VDgZ@&)qs@rrcTigf0jPS8d*q89W~MXybYoT8mOJow`k0Bh0S4Y}sL|80Og1PJL-MzWIg~>0ZAg-P3cr zODk3!ymUpnm#j$l#iw+aZJ4i5R;2S*q;uzVm-~Z1SyA3-MLJ_eI(1MgrF2ashE7z9OcW-^`igeGN(p~DWe&Ljk_v=-zz*ay``Rb4)&e_NH zJ1f$?X+^p>u1L4A%S-Vs?DA5&Q@a!{^#`Z;EI zBHbUYNcYJV>HhPIbbokCcd6cg%ZhX#T#@b{tVs8PQ#v1qm70fX!h|Zps6;QX-q(L% zMY``_k?tca(tY2GbkDCy_u*5zOZ~>LuSoYxE7JWUSpQa**T9eW|EDdg4afHtlOGQi zX*d<9%f%dS;w#d@E7HAXMY{ZobkK@)xfSWME7E0Fq)V?zmpY}p)Z;E@m`mvvGt6ZV z_;38z({?YV`^<`Tf4w5zU#&>@mn+i!#hmW)8Ri>Sr2G06>7H4U?%gZWy=z6fch27b zZ?66DwS$-Le*4y&z+b-b=S4mO?{)9Jd|nvXTf5p2@ufGlvxhN`Ys-SsHSL~ktD^yU z8JvVN7nCP`DoPfz*_)#%(R6eA2$6ahKb?vY?0C@|*|Xdn%TJ~*O>jG7xcr^ma;CEx z*Msl(UH~x5Ghlh5u1fP~NB7^k&gz)Xt1UnDNdKYN^D~i;`F=H#$!8(Je&I{0a)L83 zy@^Y7F*8;zPMBoIQo9Kf`Ow+EB$=E-23Rs#Ez6)ZDG>5FFQozkjk5&X8;v9>lYxR> zPw}&E9F+T+Is(~2hE-x<62z#IZ8RDhnQ{g)z#;s9?0pA(D_51bB}=kKSGLo8doM4A zIEppulkyVv-n)_)q27B}$|Hf~g^nLV$!6(f2@5Q+z|vt^7FfbZCy-D=LI^Fi(6iL< zjyxXQBY8CA_>p|yFNWVQKF{}@^S|ewTj!p0BfVO-E6=RzJq>wRZ!y@tp-948SV^l} z!vwSV(Q(ybq|_ay(J6Spdu58l#@lDd1u*P)3T#@)B8zLWc=NNl$2+|rvF(VI5X7yy zXK+AUgAe~fJ*m^mG+VG2-LQYBbP^MNahwrh9BPd7#?zT>qEl`|8F-omG7|rW{X{}8 zB5{Zk|10rN+v>fMV1fAC)M&`{6bwX(6rO^lkV=%}iE<%xv8C7;fb7_hUHRC^Q%zxIcBc$$M=* zi{4t-TULV3pwy9f+k#T7N?nGTHhSr_15G(xo?cPgwXG-E*v)VgE1TMyeyA+RT;8z3 zmepFFZYz9vvNrUu46T8lDcrLsv>C6*;LFJ~3S+yL%ocm<^s0R|EiYIURYTny9kdI< zhAHM(MX;VV?PWOJ;8lmi&F$UM$>V8$z+uyA_inh+Jl)~u(1weU9pqYe^FA(sGJI@i zad`#?j_s)`*oDx`@xhKZQH5;7Tsu$TjK?b|YPS=;9K&{;=sUKGb;T5|W>k@^C*d$e zy)u|z##D@~WU-PgX{tL~s}|GHlpb1lJ<*3+eVZe*;xVpZojSaes0hJSZ|;OaZR~bC zTH~s%E3+s=KFp;ym>T|Qw5Bi^qSm}>rI**W`eA>=)9$wn))iyHY6(@FEBdStW%eXG zidP->yxR(+lklGR;y8!RGIj5Io_ez9P3cQhddGT}zGrA!9e2nsoE{4`d{)`7z;TQb z)tWfX*m-Y)d>hjYnY(J|PJyf0wX@4)J7(yrX(}p}shTwz)GNARBdhjiTAHvsX{*GB zA*(r@SHLy6LSA$1wX?B_4WUj%TOQqttBA z1}~KodQPU%Gs!g8a}uo{>Slq^b0V!CD$`ic3AB2)XQLwPb3CmcD$`icakMPdgk58n zWm;(4Q)QFTmS~t%vK`vTMgwk=X$;OvtA}c8qJ1p1dZ<2xxLzJyfQ# z9w}XCo5oS0118;}v&rayO)!mtt$!wh z54GnIdM>5aLuDH4c^)mx)Sk0K3ys=y2xu46>Y*}?plKH;}V??&9rvpok7boO$qOGT9&B|c^WOt z)P_8jmSt)~o`UcHMcjqCrF#|?;H#n%!e4lU0ONgqz5@U8P0!H&al2Yhc02tXwf7dd zH7z*wqSU!N?7x+2s=ql6+J02l??z=NX#zepVl|I{ZzaswFm0$Q+y-q=U)1)T0ara{ z4t3JzawQQC4XkDuKkBXZoCaerDl3<)=5#q(&|+4D(GFz}qwwXDm|lmbWT8O>|GPn& z^O{Yaq)gFv7PWnMzzBJ5S!W>EcMY{YM<6J773-~}%#}}?^WBu(t8yE1J*Rf8LpIi7 z@hMa0V9w*$<)boJIVE@Ex(3ehSQnJ7EXMpehEO|Z&ijL?P3f=NEUAKY;4|dLWn)!X zR;J9SEulfouOGnhTy-TR4HlICN;wxZrQ&IM(-=dW^>p5naTQd(zAaFeI-1%VDpOx3 zh5tjjC(p7nwTnKhtQ^=QD;uRnSE<|zNL$5BrJwKzoibN(-1>v zC#ze%p}ZqqTK9X3l?{WCswMFml8taW8^g4PV5^slG?j)%Bj~}rK2={A^;!CElWMD# zPn>0C>LTFTWM$`YkF0Fc$^Ah^KO|2}W5sqXRcO~-fvHw*`PI=(PF2nZ?aq$YhsCOX zuPRq9*rUE;HY+dq;+VPSs~EcJ+?EmJgjrUmuCkw1R^F>|*;p>b0(D2QYLdGK^@1m; zbh|>Ge&6Xdla%oEA^8;oR!W$IY@bbwU5Evhtpd%RY;}sIy0_Fgb%6)h7Zry(gfZVr9Ln zAC@LmF=@i7b0iDNo?SL&GQMDR zOHHkq$=h@I`{ra(JB$?i!AK4-YpbMWDA`1KviD?fO#X~6;V$-7I(J~)6yF6r^5=B@YYC+_6o zw~rXT|2MwiH04g<*8594-Y&{2T&)P9N&%^O=sT zu3t&7Ke9CahSTi#|1eaA!5WRP>TbP<$s!B><~{Ijm#~h(1_bS(T{dL}JXD&a@#L_FBH zn(>O4qS|~k)9?*--AbnwQr6VIfGrq669Y?M?vbHTsJU6nTLPsOWwW2~dHlUrLE*JH zBW=`~h^~Y~UQMztZ~J0phrDODrltOveuw9?pgx%j4=0)O{vR8+fw#-x`+uk7S1IxL z|5&gx8MfMRKUa+x(h#juzGJMC?AbmXK{Khv#E2uYRHtE*kFdD$%W$Z-7=EoWiWiMPWE3=JS$W{vbd$o#wH7Cul_TdID zuZ;w~tzx#&U+E7WrJBYC2f9{Hu4`KI^%Z$6ERE*Um0nlxP!v#SV5b2r-o_(jP$hh( zS0R^fnEqMx{@+BOfDZd~-~XGC3B7IWI%j)7+pFM(UBoqRy#IF^J~80$|FIao_HJT8 zpNWZqZS35{s*UYJmom^#2{gl}`C6^AK9TO9ZC`x&-bJHRoN)>exXAg3zyF7-R6B>6 zMU`^+q;jIYk_~55#k@;_c{I}oo@T%kY|Ci32b8!#OE&p)&Q_C+D?OQf1U$iV<`svc!mtMUT zSejq_$>L`gZ(lUSY=6HNzgPSk@lYH_9z))Oq!A~wB>XkZ$M-&ATc}+4!@?sAZ(Mlc zLVm%xAQ1dY@E*ad1${wKa0>tL{7>@V!oP!`;a|vK=6!|tKHlBD4o@}z$N3M<59TlD z{+atR?oHxr#2oNAxF2R1%z=wVKNWpV^lDL8bgf9kJ;?bx=aWlUac)^UkCWx-IlQ^g zh){Bm{nXrn7@ZHBn?seO*L-4-$XcR$nK9>?%gcs9)F+2T|WE z33j5sc0%eEOr$EPpF0p6QQyl6_9^wX67|K1B3tTfA?m9k{nhr zS4FTB^)(Vwm5iir|AYnyA_Gxh1;IY0zIvj*a-ztV`s#@K%7{O0tFM+ohB87%y+4Fp zL)2GFuoLyYnvlB6NGf$3j2tA`FWVjaRRp_ackEXZ*1nWrpJMGR2y3qpMYdS`a>Ck| z5P#ZcZHz$nJVwa2Pe@WJq9Lq(F~Lq)TTMv4h>_IoV-oZ(6~TVt?%0(C`vtpWR}ghN zVSAnS5sh#y@y}Blp-41Bj3~0D5eh^jM2SCbYlJ)jI70l9eo?W#Gr~s55rD%4JJASP zLTZSS)QOIiA=rbvV^0(8f!(pE2y6QZ_9@m*64v$+MYdQwL0H>M{AruDpHCq3FhaI{ zD1g?E6V`SU?1Z(iCjcKH{&@;uIRW^5qR1A&G6L|l+$ji30N%$0uwr5^mJ)!^+YWe@ zka{j7soUpK*heoT*w5J=yM$mrdw1-Y64pM8V4q^`6~fwQ5=FLH`x3(1XApnd<{r<3 zdkF>G{RB~))0xypKCybZm}sHX2zJ7yE+V9!%1G+O%zq(aqf-d>DK@%*u+hmxku5el zK-lOc;!oRbbUuOXL`KLac0v0I_Tw3`PmFix5fwX*V4qU4bBT&A6GgUE>>T|5{|YZR zCp>ZCTMIWXT(f`(J}Y>s;8Omt_#fhT_?Pp(1GD+(mOs9H>+< zMcpD0e@XmOu?PGH+zG)OzmcAFr97duq^n< z^>9o%#GILKohiqti+#a|FXwyV2?b+pn}e?4qdLA9o_{cAogC5yAKNe}L(8liRPdn< zgNAeP`O%av!5>P%bb_ao)gQcn=srxnG4^v z=HU%vGOW_1G3Q=DxY^8S9Et9}wci3C;bu&k>CB>*Z(nZ7Fc92R@zmuQ@Dg6k6#b^% zfuAruQ`Sktg8<>hOhv4FG4N~*3*A^XYpYcarGB~DG(>`>*HGtkeRN%lWQMGh1{MBbV_0A)V%?y^M>Y)F(c3$Tu2bjF`jP>Yr$tSS zKcxfA{p1inF2L!5Av2vpHQW7PDxLfJUP`CB|J;u^T8km}&DNUx=|*cYWSwlSxhGhf zJ=e*!+VMmxm1(u+ezwun4E3)!_1up(bT;7=;B9-Pv5CyU%~Z5WiH_)UXyAKWk~L)J-y zav#F)|L>c-ac;S_yu5Vl(rJtDS-eL4KCv5o3PeOtie7;H7I`Ji<9D}Ey70*bx!}tJ z9sePIoj=EG&HrdV%KZZOYR-2!uDQpd2-ZKDqlh2_S1EG`(iVBpq^q>Bw8tRxw6!jK zy{|7Rvbnl#kZZcCVV$a{k*hVTkawTEtqbPOnV`R5aE5($t2;2gqiq^0cT@=V$e|va z(yNi@oJvL0=C5|qicA;GH9WnTN@{P^49=l7Z1EW)gVytJYv)qClU4W`CIy846 z(n!Lr=tiij={6V=$zZ%)C=Dg4h}o8NG+f$vrx{nfD>{9r##j}dYblk~!`Z}90geS$ z+oNx&l(tkM-tm~QT*hwGx7wI1pK$w8T|bozD^rrFPU=q9+D^t;*1Hz0FrTQfJsPa} z(z<-ruBs~nfqJ4)uhiQvr`}O;yQ8kYMV*n$9Ep5E+uJv^=yJ(nFlK4xUFKNVs`F28 zL;4DD9i22983w`d2r%xDO<{$Ph{I>#<33kOA4}JjiFCg`DAzK}3# zj~$qK0*oeC$+zoDUBPFp%Nv2Frh{et;X%@E&nU5eYtSj0%Yj&|3vXH;J1|@VOg|Lv zv^6EG-`dbRRrx%8Qde%$MzS)mvm{NHl@>?TUd6|UAH1Dl6RqC37xywwBxS6%XP#-?#p?*b1uJ$Xl30FNBQNdK= zDUHM3k7C_+$DzygvrF|eo|_dV#bzjP`28lfgEEj)BJY;_2K?&G)W7xtUqM2REE(~ycRd4 zWwM+P#wNP_U5ODmArT52TRq0A=>3Rv~(HE1e5MO~(B^;ay8 zPOMTar%bYf%g7iD>AxMK!p&wN5pK(C?YhdY=XD-gbL@8pisz`fn5J!TmR$@nmIXW89jE*n+Keyw&s;v;kkKoPo<+yLON? zs^DYa5ml?4W2}ebzYU@}+dX#{O=faci_h8jShGq?Uu_wB15)^W{4h{6+M_m!q!F;I zqcz5=X#H0~{mzE1JOx9Agxf}o8e&Qj+Dr;4U1@~WVK$q#+svb>9Ysm*IS-n!>aT^PzhE&^< zG;P%mhB?a#jVq^%GR88^e=ATQ?!W$;ByH`cngeA%kgKQlIz=u~PGkd>u3M!=BbI8? z3r8_ssI4tA*0T+ag^g&NoyOroJ!PaYFWL8b}ZK^^v$+z z+QZz&6#p#|&FQS=jY?fc-->Eg>ZBv*PS$g2YtGYZcLqu{*N`=8`bxJEQ({uaRigJ_ z>UKP_R>ZNuYWLJCW6UFQ)%~bHg{e(pg+3jxNF1^}rtQJmb*RMLI-5SN zRCrHAxrwtx`U-E~B+4-S`@fC*u{mChcj^4!=D#xk=K1!#Y5p|s6Wot2e{K2Rc2im|Nm|Q$^4<65*AJzhH&0ZSDJc~zy2%SDHFmbN-dCG<9g?yp66jbzI@xM^}2gz2SoaZ{4}HN;y&buXLp+TIbIz^VjZ> z4oS5gwp4m5jbz34xgVnb6J6;E`#nWhnhJ#TN4nAz7WqA0=?RPcj;{2Czy6x8^n}0u zipJVh*eBCSp7^58`2}6+3E%!1U1{o9G0sovN>8-pkLgNJwB!%zN>fe3`2k(&iI#kf z#@f`vC(%fjZNG*o!#I!9m7cKnBXp%F`px&~N>5n(VY<>2*8VPC=?QCphpsf$fShmB zm7eGc-=b+7Y9~99M)Jh#6`Tj@N>BLqH|a`Gw9Nx_r6;WYb-L0M_WK%L=?VLNm8SH> zBLkeT(3PI>*DukPp0LRm=}J%d>zCe%Grz<_tIv=AcJ(10j^HI9e6Rqg>!a1G@-qg>}pUV9#x3m22 zA>{MdELYZxbuP55Q5Ng|`CUAaWspMc#>Ai-?3D7UqSgE_`;O zvv5H0b-}Fyl>aFImHdOeU+~_^yOw()=Np_CapZH4&8;!sjdEVYxae~Q|1lPD@vimm zw1uMj>%LMcXEcU_wm~)52{g2|uz_;Wdb)>q$h+gto$90e>Ip-|)o;0DR{KC(Pxd1% ziagH8Xa!Qc_UtPToR827+}`_T*zE6zoDb6qqz(`a1b&EC;6$6u{P@WEAgw^^lrj60 z6Xydo0#y@h+I@9P%~;KQqxoJrX^S-MUL&=YAHRlo2;ISLROp1;Y=u0&yTZc<5KCQqBziSi=MXxUF?6qLd=TOe>sZa$zN-J=}O-jXL zu;r}5RDU(U*Jrh5sCN!IkI)L-Y9ftBD%}?hXx%h#7y;CE>SPPDQ!lJC_Tu1Gi2&ln7TcPv1y^0#RPQkNO~W()idt-$T03v3iy zRcnv9o8>{FWeV1FHcyX=@IhLE6N5p-r;XV%wU`cGqqErU#e|tE@S8LOC-)iq%sods zSToeigHW)Rix`F-s=#m13Y@5NHQP6&Q#nJnR_hN6+F?RR75D(Hz=@s`F>5Q;Y$DVx z8roi4r;|-k1%91Y;6zVxN9yLZJz49Sf&qQrldhPk0>4HlaAME1FA_`|Ivzv88Hm_o zhJn^Yy(-T6Dy_hYo)XFjwWV^c>Pm#|zN({?YElt?g;wCifI3LRPU5ZgJ)wNDZntPl z6K?ZmT7eT?uTXRsI_Yj_=ytd&IhQ>Gwk)|)wlgs=e~wPz#D;cXKb3Y3Y%Y^AozR(0gH&r`wD~Npz=^Ks z4;qH0tVx%vcl7Woecv~M@H4amC%RrUTY(?Y65jvHJQXt-y({*Y=e3W}D7lX#^v- zny*=%=*#~}D{!Lgd8-viQ>X1`tnIX=mFS1FR2zPRPT<5@z`nexSIDKKu~IMU^7k@j zpM@&$$h(jVf(oA$zF*i9s$u@Wk1Pxpt`hu3@JYd8fnLDnf0qA3zL_uJ zeV+GXo|7krcMD!V@0~xH`vCWDZkT%}=i8jua}II#%{>a$)Ba$0EF*__JR$zGLvlF{ z)tLDEnfwc{n>%>N5^{*oLxc!thpmHBLmjf#-yOxd`OA9jj>VbEsRA~Yi)SgHSO`#* zgIUTa1Z*l7%~VbuxhTq!naZgGHkAu!DyNQe6y*yul~V<5Di_RDP8|^`%K0;uQw3}& z$L`?Gc12st}CtC~7cZUX0Wmz7J{9o(7fsUkMiAH0Jz({9vZbyNMR zhF@TJ%*|9z6+kLKa(Je4>JYrCTzudx?8wbCl~V<5D!*x_a_R`Psa$XZh8?+argEx) z4dvGyxnX9jQ%9IhWpN}2s&r&`iU{iT- zrgG{Sv#DHgf*d>2ovEBEU{iT#rgG{SLs8zIshlccQ+aEqa_ShfsT`k}jx=W~rwZ6q z-k7PJI!IBJ*JmoH3fNR$o2i^SL{XGiXDX)(*i>Gbshm1QQIwZwDyItAR9>2?oH|5N zlow|zrwZ6qUYMzzIz&;F=VvOX3fNGN9m&n)-Fbk@j)5aPFX1MZzMH^>XW6kMnOVvw z1Z)7m>PULsYC>eE_2Wch<4xuBcON{G!uf@C;HkZIlYimV_hLtqGr{jP#$ZPhGkK}X zH!6Pc$n(d*g~A=zB1&ox-vrJT{N~`1_{?Ub_R>uOf|DZHk?Uuory|%uj~zKQlb5P| zgZJtq*NuU5X#9=Z$u@y=7tVM$cI4Wb+NlyYv|n{3Hr6hraX4x(+tfZEI(Q_y&QF7$ z+S4}q$MZ^LCU`1<4e;2J@JwE+@(tdrj)Z1aeDc0)p!1@lEiex=KlM;!&5p;LES0*YeD6mE1peuY}^QhQ^Gye^{c?Y{|sDy&n* z5oPeFnru|Met>3W-Q^TA8z|-zWYz0vI z(bmX69MCh+lR7Q<-rTsYcQ{O*m~CNUR4N_23eFo(XR?V-8GpY{3adcA#0MX#kg!{O%k?&z%XG(X_5>9n(BQJD@m%BMTr9NKUZvV&a9Zr;ZQ zP==57;MQko;8<6_@l+)~A>%-pJr=CQi_$(+iBGRHdKBaF3X0n0L@(3G6!@?;wdXlZ z6=lhqlr@W4M>*v-7JY$m#@$z2Z7nSpE0mI0q$%}=lmWx;CwkW~U#W#G`hndn_h$2v zM!{n&Y7+xZ%9paJF~1x0^zybuK^@aOoK{0Ilk+N?foQIdrJXX1!(?@pdcJPWoO5Wr zb-$ageCRv4X$_q-R!Ic%1xd)M<6lRa-rUz*Z8*5k{cq24i`$-8iR zEY$E>W&ikT)-j?Q{PFj>$|)V` zW>IZG?U8aOXo#$00ZiHI=1LAMB11!2qkZ?&464t;m4ww(K+R4=#^+C@S2W6jyr;-x zmY%ox--`jM(OFCHW7`tA7OFD$t7wZOXWmvI7 zIM)(z9LEX{i!_7~PK6FzAf7}ssvq{Si~BWzY5(X4vL%4pO7D5+na4zNhs zm^$jk(ZH(f7^5L3Ef&cbVWmAprH@t4!~-nSHr9^1AsXD5ubCb5L`W6@nsfi84E|9P!sOU2N`SH=F2QnGs0$d5Y2iOV`cQdO#P5}fJOSo)KNQ* z23Gw_#%Ks%W|5c?R@z2X`U=KL?r1AL@d5GxixiHvqgIFp_vOnOqnYB%ED|;5j#{8b z+?O%N8n^f|ixiE8qh_cH_hshU8n^i}i!_a}SxrQ{zFDc?h7Yhv<(N8Z#L>X2 zs~Dppe3?a(Mp$VBQEBDuqYfsU2}(XCxS3#)&arkBUNIm0vVt+1DZb1iX=CoF4r;`G zSI^C zYiUn3aM#jUtao{L3A{B84JRNDHSnsHneheC6 zaV*xuKx{-`%Q?)dvQt?zXRvP36Y(4LX?IZtL?pgo;4Uy0>tZ0bV($0ry`fW+ zsoDa*t~y!Qs#-}+z-kYrOUi!I+jiyys74`0AzIN`tdoIQr?Vclrlj76JY;DPVqK-L zrA(Qmp7fwqXj@d#_Q3DxR)cE2G8ecD8H;r=5Nj{#>b73W-i&Cf<+#77HDvReTvpoZ z_pKQ(bdWN{ER~o`LFfs>u~<73v1kV-zKv-Uy0W<_FZeXBbW@>$uXpI>9u-!Px^%6S zw(V~vP0qkw3uCc124cg-G-~yzVs87;9Z=dbl71h}oep?ON zRxlQ8oqeEoz}eSnWP>awL-W2tLgh5s+S#W*}BlNa!6F zm9LadRjk>%Mjo`;b6Jf#*@$=YX}RCuQ^U0Ljhdpb4cs+97HeW4HkdL*QvJp-os}3+ zUA`3Rh4sdq9nG2~I&Dd6^SEk_N*gBpj`ad}amQkf48*c{$|Z0YXDrshKrD-wTLO2@ zjm7F2h-L9gOJH<(ELO)rEQ<$M0;8M9VzsmP)NBr01EZV9Vy|H!mc=P$V07bH?9~j! zvN+xgjBXfTi(|ULXgC&o6$7y>&bI=i!C35-48*c{RU|O#kHucWKrD;LLjt4T zSnTBt#IiV?3XHmAu?(``usLK3j5=en4BmTT^TbDB)Eq+8MxLxz4ECPv=6#0Tv7QTQo=KHn20; z6YLH<3p@u{><(_xtR3FK&S2xQJM1j*u(~OrS-W`yJA>WL?y$4i5*=W%&$&gscGEic zIly2GwL9!A@YL6CgmA_qID@Sh0cXmgKZ`wCe(eURay)`F*p?A0w~XK{_G!hnAyhRU z!5M7T2vyrga2C5a*tCOG&0;qV zJ-V{dvE)o6L5BM(LlUb+Nd^ztvRD=Z%=Z;?xsO~tq9?a zJ(|Imo`5sOqgm|r^J@jDa_rFzw)=$2EgsEs|35$9m=pH|*Terj_w$?up0mKQv;d4Q z+qV&2#^SJY_R%LLt`m=InwGvnCEeTTvYF39;HwxNYC~7C>zoOJ zSW;tSQ>(6}N-G6f#avs-gpabkzx& zzUNV=l&z8>U@dp$5zmT8-_I#*n1N9!<+$oFy6j~qkIuuLeWd2FIYOU(#7%Mb9dngn zlg_@MP4))d4cP>Nj?1%{e#hn6rc@XQL~aeLX0fywCr5eq22~Tu2B6iMg;lF?1JZ_7 zt6+f{5u`gsrczgpxUOABn#wh@aWaLeVb#uRvRF}9oh!PGR^H2*0+B>brfoC=QJX25 zRQbl{DJnb$R{>rObn24* zK}eQ$`*qI1YA+h=_jLIck9sv5Zs&E*Tt}Z=9bj&GwvL4o_*ei z!(gcI4f```nNkVgcuB;AeXALd{!L~q!33s-m&Bw&UpO5|)M31MwAgBA2c@D;?NQY_ zmWFgCq-keL{*o_S&qUHG%o8=*+@WH(p|i_+sc=sn)I?mh)lA1YNOXN#MhO)*Ouz10 z@$p1L=E#BL$e@nr>e$HBvAK%ITYDmX&ekc3EQ+&l$;dN%vYW{D1I;*rWc|YU1d5%U zbwVbDzZLDexV=tZu$#Ibf)m3L{^(qMVtCo9oC7RIuf3ZXG|$Au@U#bdHZ#j1=l~Sw zp4^qmx23VAq$+!+YmbHM@TRu4wCfd`BCm^ujjC#G zwN=W*q&>%K(z2Sh737X+p;fCUn`&=yrMsH#4N;?aPzesgT}3}QbUO6ubluo0dxmD0 zMOht|-Br6I>*$9tPhZDuRK%-J#Q)ppK0UYmh2^`K)63GOzbt)j>CUC+FI~F$)Z(WX zU$uDM;>F_Mi9acRnK&Xo0G%k~a$Nyt@_{ zG!6cGrIxLC%W_2!xdk0Xa2|C$6tV_v^|}i6MH1PJrK`uhSf#E*eaV*Jl@1R5QnRKY zbxU%{3(!#*=jodYs$NL%&q|tdw?CFNHD$)UzLZa)(srQjsTf^Wcg9+(OLQgVFggn1 zJl$APrZq%MsNYhkNeX>K&x#c?gglOvez?Hd=m7z7NxL#1f`vEUiUuLPr6dC)Y5yT)I?H7WBl5S(B#} zskysacfs3h$I(zK=Q35Cv0kF5&h(HQ(UBkLY54*TL(}Z2_byMgm3G}h8c2FYeq+Zn_f_Of0gu{sUV)&q3p z!Fe#7#+OH{9&;=kcA2}{KBnrF`Xv~Sp$j$rb-gJcbbAYhaHZKp`sm1w^Jts~UpidV zWiV|qCuw$#S+7)~Y2;$^qQQg8wK@!@cCxF=+Kd+Ip(7X0qwD&sshVG(%6AmDfF|tj zR`hLq&5$UYWD%*wXm46%9dA#raBGk*I&$JXhD2WF59>XWhBxCg*|HUNET!%y?1Mzp z8&Eppj*2&y@55SpqeurGIdC3%q}^|8e1)hy<gh0JbM%&M2?Ga6;iEUP$;{y|Bt?Q4QPq=k-b zIFEN=D?8LdOSB%6)^jdv-5m60lzq&s_1lJ+qg&GE)c(*Q>~m|8COWd>Jeoio{x#b! zSd!I&*&RQj|ML8dcS-9AaO zV2N1OHN8ABYSsEL(P1UeLq0t2uNnf?%%GN#Re9Npb=QT(P9qDkMHYT-Z`j*&@mMG(MGENX8l0zTm&KAzttx4@+gr(8tK~}Nw2@G`QdgsSbwb<8!P%)3 z?wG1xB#(}+#(CsQPeSWRck=zXDwf0Ajb^87GB{)Iye4K%#|j;ZEmy8*Ov6qN$)Tfz zI8WJC(K>W)G%D3}O(Dz?&%3;xnm<#pMLdI{M5d{!0+=_UDG#Mc79CxM^K>c_tclhY z@Oi>)*Kd}qy5&5Ig(UJuwl>I0LrR&XSBggLXh4dj(b1JSPs849RoY3FA#apNw7!ro zA(ckzuBbaFbGdz9eXkJh7#gXvI$A(G$`=V1{*g$>W9+8BE(9vZ$4~uXkB#4eA zI1h{al1Km@U5fLtxbTSh(a{Rd!{X*5;zLK5;5;lYAR=CL^gNu0Mf?upK}Q$kJS>89 z5H~uy2_y|bPmqLA}I-?Lq})hJS;MY zAX;>E7S6-sat?A0Iyw{QVR6d_xf&gvf%CAqEQ1_GN2lXFEaEYctI*MDI1h_p3*<_4 zbSlonB1;8w1v)wf=V5UL1i2g?os9FaxXXcH=;$PzhsBi)M1zh_#CcdmFCc1kbOO%9 z;%gtGLPy8rJS;LIAWC#}9L~eyJPlE--~Ug{EgxR`+tN*oe_6ai{3r1MJOz59KZrWW z?~s=8H^RoklM6M$F9j9;&-o?Z6TAYv|DWUjh@0U&&PmNZHkY8;rr&(u^}I~R%ckeB z@&4JtK{wxbh&2EfF3tqNb*urf=*vt1TssE9VjyBOFfaiSV+~-Bn;^;>z#cb2WDJ1C zxW3m-5M~WvkDDOG8o(YmL2wL!h0wijf&gm(d)x&6F#r~mKAXv&$vEH}17NWlU^Ao7 zz9VzC{6z+!R5X0gKrfQvPNJ#GRgYXE!P1dcHP7HieL zZUQ@N0DIg7Hr4?4xCyLd04#*=brV=v1K8sxFpmMS*vPQiz%UsHOk)5nb~t}eB- z;MV=?^KY43{`K->%U@sq^zwf!zjgVpV zrOzyVVClZ4yO&M-<2f$SKHz@G0R>h2IwbAK}M@ z|0aCB@RhwEEqJ3~O>nEAB}fZ`0+T>3xL9zyK*awuJo9)M<~aOM z{(Jau;veO|kl*HK_#wWTui-zBe+D1${=)kO?|Zy2^FGP@cix+Mujajo*WqP(VV;GD z@h;(=$rI22b^e$0kHEVQ|26+!aYcN+*e$+BjEeV*PZaaO@4=72gWz-E!{F`UpTWz( zO`rWaqs26hI>1=2lFpRxi;<<+)KGxv4A#cljIxj0WHuX$=7}hG(nRj@4go_K!YULMxYMr zB>4(0sDTz(JB{7l0kuN%D-#feqM5@`R5AE3lHJI07udLXybEzzobJIsYHP z1WX&`cmD1HMqnh#KRq89fPo~RIvePLo+N*71UjH2$)88z1feC#pTxj5;2M&A{BPiD za5YJO?}OkVI7pJ;od;Ket4Q*}D!39{Ns`~-gDb!lB>A;7!R6p`lKk=y0R}LV{KB7r z253m~zpn#opeD&r90e+%BFT@wA1Hy6BtQ5^pa2Sz{EwdjIgpd&`%Z)NpNu5m_BcQR zN|JB>D3AgvNxo4ER>3MszV<1OTY?PA<6cK!6o1l zlB|6MJP$mNBnxhEF}Ro{Q^$dez(ph(`yjXwTu72vzX@CbE+EM({s$ZY2S`%+6rA4A zCrSBvU_aPTl9xRR_JMsQxl#q^f%8c6;u1I)oJ*2tzZjeY&LPP&E&^wRvq|!#KZCQt zStNOa2Am1bB*~?(fHS}uBq{EI)4}N^DSRt94V<v6b82lUfHbAJc_3jUQO z)Axh7fwz%lVinv6?jy;#0=yNxl_ak#f`0-3LXv@Rf_uTeBycxWiBpu%eZvt;3 zNt*<`5xkKk&A$R~0B;~k!}r1K!RtvYuxCh)rlCqbAe**tRlB=%*cZ0i0a>WPk0(X(*#TSAR7?I@ukHg&HuOZ3vP6Ky> zJ4y2F3&E?wt4Z?A6Twk%lq65R60Cu>4f5Mx{}NaPizNA#w*fH_ljIj30q}<;KlL*p z0wR+9_%8th5R&}xFM$vUN%8~7fd#NYlJ9#t5C8#5z6S$*z$eLn$pRkWk>nfS59Yx< zNxtqifD5=JdC!Ld2XIL8?h%*+b0qnin?-*Y{hcIV_HNPNM1Ld6JM^NzivCKHx4mBU z7tvox^47fQ&!Rt*IW9{xa* z{+Eh=FZw-6dR`;?o#=NY>1c_5EBY--TK9>5Bl-Bs3DHkQKPAb1 zuMqu2^b?Xi=Lp;!{g@=r{Icjrq92juDR+r}DEc8up7b`+gMDz$ra_XY*iN3c%e(SHNh#nR_Op<>Vh`uZO zE=fN1d(n49-yz9AE{VP^`Zh`aUM~8U=vyTD+dAC)Jw%ef{+#GR(Ss!Ukc$)DXXdO-95Nj`Cg= zs32I4t>H=kpkVVUK%R;4B!xn`^DEfVblzGGq=z1@G~vw3YkJJsS*bX)IaPJ2$$ImJ za9wJ!nPfrhdXy&q#C@XOX=c`+KHnO&DV4(5udP9w?zFashl8zFs*b#+qbav~$@WTF zh85k>yiQ}RY0?$cESJ{$e!14ws<#vkd(dl)p)t8lX)wu9f39UqDP5I;L1(Hr3&B!d z({W`((y*<|@!9B^>4M4-P(0nU_#~S^)W1^^ft}1a?QQOk=Ju_YrQVeo zs&yDbF6*kP;w$@238Oyb4Ju`|p`oUVb#3;(UYbY*j5)6(5gRajeSTbZ*zfKrjn2pW z-78ZZHq*}DO@Oj#{f@;K(7l<^*v^QQc-yU+zn-C<)M>#NxyQr%PJ!bS8B#7k50y&s z!2U6BJe`40RF~UO2A<4>OgnMIM#OwIQNaV~$LS4-R8-sQJr)N!iN8&qTcN_u5~)PB zRxQAs0`Ww-kifxjmA#(qVf>Fc#&tXtkn3Ulo508X$pro^`S{mTAywM=9Y!1{Qh4pg zexK9DCA-=XsjA~%79+qYPy5TQd%!7uLf+vVl-E?d7ass z6owl5>v@A_*TYSqS@xzvj#al)*+XM10h=<}NJ(WuSH2lZw_Kif+8OGpRPBs89IToK z6=$tgHud$>38UL@4Nl`q*z|@HzMqbxj%2KdmY06L(z`4S2TT7u-<7044O;{4M|(o)v&kKt2T4ljJp?54rMZTvdAne%DC!8 z{Qo(4{Qu1`@_z_p{=4uSeG|s{SK%2=5q^Wu!l?cvjOV`|ekYH?82%89-uJ<8*)ABd zZ-cS=CZU9Jx;u6`!BQim7a2)}QD8~bK z=FyEz5L`tdU@nzStRxczR}!tjJerbC6y@xPzYgDd#X15Ov6*aQGMON_d>sLcNKZBq zo=gy6>j+rHjk1XsWr9Gnj(|lJDx2t2CJ5B)2w23lvWaD7fj+px3A2d~W`Y1+N5CTH zm`$uP69m$A1S}$$*+ecgL9n`xfJHnsn>c7D2rgSkz#_VuO;j}#1d??GEMm3U#Aq`? zaOpY%7E#=6qPJ%UKpnnwWgP*VP)rsvgG>-yvW|d-R5p?2Ob|S89ls;J^c`Q_|6O05wMWDw_jim-+9400v2Bl_x3FM@SO+N5wQ4NvbV=m zhwnUp9RZ7jW;UO7XP?L8-~UgVd-vQjw$xmFhxl>v*&rZ#Ir1=alF+$uyWr#eKk!Yw z!}A}2cg(Nm^kKng_U9JkZOcgJ&>`>X!mH-kWP{q_G)akFzV%C}4BM$3^^|;H-qb7P z($QF{7j^l2nX=EaO_Ef)ew2)#WlP{O69$jCWpPx4!h=tVQiN z6d==bvnzssjN73anp-ZtZE;+UQ-yPLY_d%41aEuJWvP^Dx!I=!?4 ztDSJDDXq@5+-$rwW>>>*05-b;i=>;)u&~(;Se$q< zg||~V>SLv|t3lfhl&~8JwzNkh8Lb$NN=38jHW(7gV7y%@4JE0F*_LxOT-tc28CSb2 zI(?_c6d==bvnxW=9tv0u{$@2;ocrtqkNP?)({i)-f;82TL$~5>x)qDVBc||nDo1@m zadtIm-AWFJrTKA1#K~7HhW~Ij{fEV|*VC=Wp8Jpf zwz&c8mT+hc~Oi;_zoDc+@99nUY28`48~xFv5Lt{&n+W?vO9$9Oj%lcOU1c$Mj&JvUav010P|WW0TE!_SSaCJ87A< zv!K3JsEpi?%7T3;!sxCToGMmKPj8&~) z7uvyRCr(*#1KhH-cG?)wscg;-X7AI8Hl+td-N&vkKNivS82~B6F8zX=I|9`^g(}&x0@2OL_?)RR$)pgG~f;!0N56w>SC}R1g-&kY~ zKe_0WRlIryFo+t+6TB2Mwn3u?vKSnX8psp80y6gT4jjlLa2#qNPw=A0*vC0=AcN^! z*uSPL5AAy*V_nnX#~oA$#I6=l_JC8LAfEczrw#|Q@M=Ch=?NYtEWcP7>!gR*x`PRs z2kfE7@dU3DjBU@Tam)qBqsH+BFAj`-yaUHE2ONhQ#}m9BF!phV#xWaS(h1g+V;^d1 zuRpx}9E@WYWe+&@3F5(zZO=c(F%zEj1gp>Gmsw+tq}mJ=8d!;6;S7?HM(W z>EL+OIG*6OgRzfy;5ep%<51&xf|m=%KF-iMo`#onf|dB#hgy0Z4=>yY<9Ld)2b}sL zjTsDw*xTcH>!t9dCs?;Hzd#*l9KUOs-yg?YF9G&Y<9LGC7{<0|#5mr1F*qJIjwg6& zVeI1_Fpjrg1dcjK|#kYuqVw&hyQD{0ZqSIE=9-!aPxl70pL?%{WR4bjg7t5-I zBoMSk4M-`bEYEm@4qK#XtLB1{9PZ8~!{(u%TWOyeC{5*@HC5EADi#QB)TYe@V+p^Z zQPl(^WDN7E%KlnJ5sk@Rxt4M!m6N^+c^5M-xj&f&mNO z#XVk&UQSeV)ojCS@?ri6Y9*qGLaj;FWKxxM4%&XPO5*n2IF}*d>ZB*=vP&S&7h0F4+%UMbIrLnZhCk?e`nh1ti zBc!2(22xH-%GGEMDV_QT69gakPS56bk*u&QPW+sStb6CoQl3hbC-3i2t z_My|S4|Jv!ktP`_l@%_X&Q%YEBZQ@HmZMc`I+P<59iz$_GRuPog`$nk#Deie#03(o z6>LPoV5%j_A)QAL&OYg?Xwo3TbPiK_5)xoV(xYyM6AGUk%{2?Id>~vR^Ilgi-!aW( z!ybRk>9N!tCE(DCt!!pU=k!rfsXD6hWPD1WF;p@eBC(@~JQqJh}U7s6m8lj-Hb{2W zL>3HEBAW=bQf&{}PMQP3SiCh8B3hQV*WSuy8g-MQV8=S|&?(nVy9P@0c9Jw2)H01y zr>b<3nl>#9SXv>Mv22b-BxQ>d!wdeHA=gMaXDpUb$AKpCP&Mn#d3?=S;vGt*(y4C` zloF^4)mI(zRw`D~ShMn2uI;uFDYu@CDs+KjQ&Y$k!XZ^5O@OnvM{KpS*_9<60b|&e zDCmcj(sJgzLG?jik3)eI#+F$L-9(-XmQwc-FPaf&XLAUVkUs1Y25Gj z2MN75fclWq@Ek=u7oPsQ$huTL;Bt0j&8{k4uliE~sUxSi6dl!W8O7D=LdqXel;c%o z#^-l58-@l_wR^p!-OzSbhb*1GWuP>dbK3nGTf!EpIo(J-Vzd=(7Ij`)4JK8Fwl0vg zSPC73uB}7DGa*;HmFT8Cg-+NUbQod{Vo2%K_n}go$<)r3Bi&3YRrOff%3?HLZ7m_ZX@wl5}pq@(1hkZzmJH>7mqz|fRY zQ*b88 zYp90fSSXzxGIaVJXlb-s=>&=aYZLLfU74C&+t7j6Es$KOf+~Fau3MEdOGDCV2g%ID zA}yyqYz)~z_yv2;>k2xDm3jtB2~SMYvT4IjJes#?9Z8i*;jn43iZ;_m!b=S;bqDYdXdD&B5(9eAsQ28Wfl;dOE%nUtDD-Fa0b5|35$-CVF{3Cc;mKd8zo zvrz&G7;wyEv`c5)`d~hn1~Y{#6E>&gU|;=^Qpdp13GzfP|4?%JN6=7v2laI$30+-- z`$;vKwOi{BNy`&ySt{13#o(!0E11OZc4xdZe!Zze7C^$#0%3Of)2?E7$j~zehGxMw z^faONma92q$JgkXEx1PJb{G?8kXKcWg8PxUGnKc6blv=n-HL|o&TigkByBCC(`pUt zoW6BnXtr&`>M25I!)-}5(g6`{bY4?4TO(3QdD$HC=#gqSgZQwF$2+4ZYc-R@8IRTc z-GI4b4HbtBT?-9uBL$7YW!L7jhDOI7B%P^}Ig{6g+7Z1i>J8fyx-g;3AqKT}hO}k< zzHTw7_o4-BwqXnfhICFn0hN|OHt?!F)F{ZxC0kPy3M4YYcD5P_w?)Bt5`jHcPBkOLrrU;(gdQiHBDJ1f<;_)wL0OkXlfF7$lgk4+-d|5 zyGU8J5o@9j>x|jzZZx|UXSQteHA;9mj0{_PGgJz4WY=pNn+3BH#$eW2mb4%_*TiD?&=X36CBvL5GBh5g1Na^(aKxy8iO1i4*TCL_(Dz%P6 zJ=S&y8{q%!F+XN+)MJ`zBUJZz(X@8P(sCrRvawR=;8-~mGIkroN`E_0IzbxjnV8@1 zvimI|kELvlgM{NI*RayZ21-*>7YL^i_9jyxi>@NCj46FcL79rAJH$HNA1^Cw8D}_T zGs=A4nP9j>coDow)Y~;j#Eu&*!%DvnmAVUEPeq+}dy*l8Nm7h@N-jGhMbS_q5z%Cv zow5|k#e5Z&!8?OzF(h9omt3)+-B)tt6T`P=)6c`xmqjeesKuOAsLRoeGZS*7QdLNc zHS_XXRxN8)lw?fP=}LXp#Edr)bCFg8s|70o0;HBM)rT!@4V0!sHY95DgbbJ>4?@$Z zA|L^{-IeKv+ijn!7|+J_iV|ta#EmiSOrFf9(m2SdZ6foAdZ}4^=PeJoJ9!2wH8g`w zG?-D6xnphml5@--b}Y6t$Gx7POVFHcDPo zzJi(>UQ1GDwwan$FCjM?&{EMTt4n9Kii$a+C(8Onv~D5XE^8=dEF(sRzMPRo^tE_A zL$uW~Pps3(%n%OL7O$CW-A0@&b>c?Xus7s=ZNClUKdhK_n5u0}D$&ekOtJX7Jmxak zDtNP6m4)#vp)8vHIlL27wVWWf*76TFxU2WukZ4v?cwR0~hsdx&gF7)t3|Gi4xGkg% zCYv#%9^Wdoo$0#--I_`@`QZFfT!-Z>KYZDL)r2uKwO$yQJW>KI(Zu;cJ6vf6yhldiipHK!BT$!bxz&xHl7 zc~b7q8nM}qmZ-KW$$Hok^X9?QCg86k(I$d7e5M9iDyYM$uFa6hf(a%w<8RtqCd3gg zyT}yQtRjWV(0HK#e!mS-sV|(Wb-EpCI-fH58(0XH)X0WPRf%@YMl@Mb+O1Y~tE0tc zNt8?|>pFYeZfaWngwcX_ss%Ew$@)^}0OobqjlQgmhhzC{8#fl} zh9MiKU)XQM`1AR!yKae8NNr7?lt&d_Suh^W%dxQEWoQ#Rhn&c@N(PC%+^V8s)ADma z}P_ACk8`xvWxcRCN{Z{A^t6YHA!>Tfyx0 z*NfhgEabDNlp)-mwP^IVQnC=OIVD6AC*$Fnnyu9EhTR>qk;N(@W6cpB>Ic@^18h)r z@*!_BP>E!cc}*x?4H=Eua3!V;p;ok7%Oz9}LSB`qGObysf|P;L7)*Q3?T9&Ki=eKi z)u}H<%krucJYR}L3Dn!bWdS=f<0qZoiqUEIne0BN$J4+ssdA)Yw~7I=1gRXKwY6wyJgZ@Dc&e6PtVOKVzHf6EW_NZ(@m$V}H zHJWupT3tn3#!}Z@w9a_E5oZ7oqzvJzGt$bV&0+7xDDJnxZ!y>meu=UibyW4bf}&hQ zoGz1A8E$10az`m|uawe6t*$fXXNy*svX)bVAArK%2vKZhtR5_=j3a0f%crXqUE1rF zX^Ux7J)D@yT56tRFcdGhBaynhP-_og3*PnnZLqb1rDh%pDZ?^NyKPH`W3o&=4{ra+ zV6EaeS5Yj>0t274lVAUFG=Ph1y zK9&NjhJe>LJV!Bf2iRaMHdV=*9hXH?!7?7G5;-&to-+GWksw}gh0KArA{CKF4Axn# zR-@7SbXLsVNjlM5NfA<(i;b*9A1ieN+Nw69ucElCY|QGzGi1=x^7~V1GSc;i4Pi4{ z8}5hG4zR%_&&m?!e8r@$mks$^s7#`9GG%hr^>u$LgLph9Z_(K#jLO+&Ia9BynmTi# zV8l&YYapD9x@9(5FRHG+UUE_cjeN*WO&RxBr=i;QHd3ulChd<#gEonFR-3mpl1hWl*;GY!2ARH` zH&}JnT*MtDu%@;e=-M=G)R_wDtJWDTM%0bTyv3MkHGK}7zd1ZtvrgJ?14_i35qWMy zGrS=lOQ7XMrx8@>ts$3HQP4Q*>(p3UUo5!_q4I1^Ws`&FKb2tjmMtn8Ji;mtVMzxsSY*kgQmZq#7a~ZItS{e*`yTPP8=Pl~P zvZOJd%*=MOib6Y42-MQnK+7lbhGR8-7Ec&eib}WWm-r$Ym9uTl%8WH+jB9c7ejCQW zv!BgWd}`EVj~dNnK3wywBqV7|*AT7CTMMRYvL=?5>e4=qw(8F>zY3u3+i!!;P)bC| zgf}KjIa-~VtrSb?z|5yehgTqhOIFbD}XiBlLCWM!(nub^2YLGIcTj-ML~Z9!u6!6}#LZi?^{(Q{g2u9=!$4cWtqb z(SglaBl$$v8t&vP5o0sgz=EZr`GM1%13%Y;^~LmTTH{loF%+rXp!C$H_p^O2)u*4QIRQ zmuICjf=CKoT+Wtvj5CbL$f8egpc-fU9h?4=?wo9z4JXrC%qPHxw1G-l`5|0tU51>o8-@Mnf)fguJMmtRak}$iH zmae%Q8vf0ad)a;)6y88ATQ_t{C6BseG!iyDsmx2#H5UjQ?RP1%F|RtDYuak=+H87! zgvP+)L;G!zV{vm(K}H=NZK58mAoW(!A0hI-W;9uoc0=+EN+@H|GWY>;HWD+ZQLK_j z3>kx2sSE3HiPj%Q45hF_*T`6X3c1b!p5g$FiA>t(-H|z;h)h>1vW_!Bom3&4fjl%qGwR ziZwzw3YIu6z1|x(d2(J$sf=`u71QvuFaB-R{{Qt;cT9=DDt3up72O~*2!9J+HPi~8 z5`0V`;r|rm$vPS2o~!Ua#QhF8#T9UF=R`QuU@w51^=Hy!a1d9I!|L{eSMEO|9ENPcV;v}MuTkQ5)JW~UY*fK~6FBB^p zSymH68W~5JXapPPlDC$P$yMrGZv9+o$C-urmEX8${f?ino^GT*|AkjSe%F?JX}hm{ z{_Dro@kNjv87PiP9om3SlgJwzWW<(|m}KQPnJ>(Rpg6Dp zyL<01x1RMM$?LvzR7Cc5YlwOGGrS{?KB{@O?CjkSK7}uWq{l$9Lgk4ej$|X-j;bTz zhnp(dXqt`Ah&!u|Sd-wXx5SnyR8r<{BQsF^)HP2`-Cg?lNw25H#jkz)wxboto_W%- zGe3)e`wr$_Ibx%;<& zjs5)bOZI+s-w)qd`J3{Nt1o-#aiAC^9|nr6cCuEi$JNHHNfAPP;KyUBG*oeg-5I&d z?eiL1M7UwBCJLHxeqizTul?@xKYLwtgyuX3#=KQ|{xzX9g;(j0rv2BcXNbOf%1c+^ ziy)~mP&~ngje%n9(K|xxdKaMUdPVD4yUo_JQJiZ%foZ z^W@Ik)<_?JI4}Rsn-~7-ldt6diY__}j(rPj>VL);LDF8Jc!Hma28!!{W>%i~5dWdG z?)asnCfk1A<8%3AZaDAzkAB@mg)`d?# zyRkmcee6rj8$WjB&$j*SJ8Q1k95~}U_##NM3lvWfBVnNU6#lOH{OR)y`oiBHeeTFM_1Fz~TwEUt6ZNl;&4|V!v|x@o0zjr?p|- zf4e_+wHaRoNo*;_;|Km6C|-B&r~im=${uU}bo=U;f1ZE#A?r@bqc1)YQXY3a?WeaC z{P^NIeZ>=Ol^7_FXd0{E{G9fdAGtqs{_9_ET;~Wq@zz&psS{Q!#M|XRIrUTc;ztL4 zJbt8xfnvpqi=I5~jVD9iAAbFlM_<2b0e|4QSD&?Az2f6%URT?C(hV(qabsWc1R)g$ ziZ{P?@*S;BTOLYZS1f(F8$a)Bvvb>jxc#_YH+sJRlXl?u=i-ZJ_Z3eN?D&6+`%l7=lF8b~ty4#;!fiIrbS3JSSjDceB13#~Q`1ZzI z^FLjc3^IF1((b${I!AMI&wgU<=O6!#^9FqJ%)a6Yc3=z?*DwC*(Z}BM>SN#A{kKiZ zuT^=jlX{;ad$(soC+_&)OaA^s0AJMg6;H6`Vxai6&z`-Pu6yR2zAwCy-o0}5_P-zd zqW)_)DHJoYU8VM7iHk34`-&$BnK@8={#SNyLv+vF`y+w+djH?z*PL_1dGQO*_|m2~ zfBV+&UYO(k7GKo#6;H5JVxai!wf8=E-N|o0`{1A1SI(}Jys|r@lOPZ7Jm&dFx3LY^ zfAKMVQQcQO!S0BG;?pl%W7-wF;p_kYE9DEhwdt=s=HT4(DcyYaq`U9leb>KTF2EO6 zeZ><5KNu+1`P!>vjMnRK8Gk`bJ<%b4S$rsRqVG?OzYm=8*2y=lm*9)azTydj5eyXn ziTiJNeF3@Rh|~9e`Nco|;e2c1dhJ)g^_S!1_sVW|E|#DAF21PfE1n=~z(DbX*w^p= zAqTy3i(-3h=jE-VnNR(KKKtWub&kJ_cjF($UytL9^1k8;_VEuC2MTB1`;Be1$8En} z^~I0BaN>jh>>Up)cz54gduYo8Z$5kKqxhn%uXuv3Bm>1;-_U%VcmEr2o%W0m|H4;q zv(#>R{gc7UQR_c~dOrW1Z{GxNv7~*)6YLooD1Q8m6Q8={TNi(c@Lsa@Y|Yv0e;4}m z-%9I0^o>iAz1L%3RxaR+8~TbT*d#Jg{KdTwo%Yn0@2>g#_s_cW8v4t}K6RU9>gn(Q zS@SXZMeBseueujsJfp98f-NBf#rJ>p6Tf-%N{2=DovUxZ`}=2Uww!wH-vf( z*qAX;eDsQ69M@gu4z8Out^oZ`+xQLo;OQ>JokR`6nydYzTyeCTnrRf?6=)`yW*Iw zf8X_+>?J$??ub77tl}xhbCv(j2~Ju3)rLRei);FdC)i*yP<;EYkKC~7%w4~}u|E5~ z)A;_YFU`#yb?s_V<(CgVbj8j1kq_gGr}Y(2uv227_`ZcJXI|O()ss)UpXZT$TYlR$ z&oW;?ZL$CP*5|%-?RT4(gWK3s`-&&n9WhY+^*f%3?AdqM7q0s6H{bm334cv}HS`hJ zx^wqBAD2ECeeS5sEcoIneZ>=Oa~LSj{`mSUuUK)-&TYGQ>?nApr%dJUc;HOpV;|m` ziMZ)67=AU#|L2(!oXXWP@1OSk3zqBuzfuVjC!PLw;-m?HSbk&BP$m;9ZAGn4reeit znM|fgqLcx7RpeT=c2p*l2?lyUanc0Djm`u(se+-*CkO6O8BUy(Ad;O%CzZ#$HWY1k z%EjT|f~W?yR0(tn=#i5T7Ls6i4;9raQ7+X}B`O)vg@fcr{r?+V!{Nk9pugVF#7R1x z=Ah|YtdT5uWve2sm*Z%vl{KfV8_HO$B)1l|NkoHb(6$-uD3MyMx$=8XoMboak_Gby zmokx4xHCbyB|&Uxq`KyOI_b){RQ^oLM$|PPnK6&AlLsT^4K`G#NCsNqhg!9bj3)wC zi`MU`IPHO8H|EotT!xCv0Ma#%kvNGu^+DpKHE$GZd5_d$XKP$*}iKUM8>dHZn1f7mGMb z9&;YEo05u1wG|+h;Gm@>s%phM@>s3dk(OO0%pNHibXY3ycV$9STh^(UH{=~tzKRyW zBj+Iad0mq<<_ySqF$GnsQqKY{)KFX|n;3PN_d3|M8u>xffhv=& zOv7z$qOGdWR#V5?x>T(YE~mOh(wS`6!xdM>RxTA~rS5wl?8%ZmhiQG*oX-_eG*L`u zU#C(l8o8F|ErkM@&=haZ}({Y7O?laTu35hT8Cg3Qy6iDx~_o1L8uzm0{Y%>Ah8lz0k7f~ z+Vzl4VX;{Vq>`xmtqQBvqqQ2{#cDIP&g@Kdq+kPuT8$^$(rRAnFLym*L*8WYL>jTS z(qAn)yOm6OomRX=1Koyf}Tt^hc++ zP3J*efS(J#MzhmSoc`nV_eDPteNI#mog+F@_`L8Q;U|RW3uS`y1u_AH|8xG=_!sj% z{EzVd4Bjxffk*NzyyLll3g|m?s1%#IS+C^FYZuwivBb; z1C8Xs+%bk4WVSBYRDML!0?-wTyB@!9k()%dm;BiSh;IXx>Buz zy@U}?eY+GUhxZQA=nOhz9n;GNW-MEo4*rd+m_t93e&qO-SI}3CTbWH4(M97{X3+(7 zf#6E;+R2#zpyEz2YppqI85eFzluQoNIQ&8`TVxWM#;wc}8AZl%D>Fp~kzw4*43S=> zAGb1Hgou!FE7L@qM4RBqShhC+bLT+CU4Oz}GDHk!kJSoZQzN{??_{xrPN8$$%1oh6 zXdAaOLx>8|aVyh>W}$i9$~2)-XnY%!6nX4dYhkbI;(OF>YlZSHhKyTbavU&s{%mWe#^8cip&^+1$0FJqn7QE+}h_zC<+2(M#TKeJbYRogv7`G}zA{nGa>we=}}n8vWPwU%&01m%D=W z=jqRbyZY5j-4MQg;N@ zqdY#sfBVL+yIf^1e-nSxxSBcqbNK(bvs+FxoBvV%N5|F7;&0?{9Jex)e>VT@aVs&?rCpV zUT#st69q(paVv90PSM-<@yjj{I3lyie0cpoH0QkC)yv-M9$x;D5g4r<|A_;7Vv6+pELte{hoy zZ_JndMeOkU|G=sA@cRG2-Z;GeKfufO*ZQB{-^B}C_m zFp*1S0l#bNL~_wO(Fvki(J>;ih$EU7zApS<;qSm(1AB!(7w!Ui3I9v@ZQ-rLJA_{m z{vY8s5ToEq;k@u7VMACFW`q|A!$P0X4t^5cC{znK2-gT_gdY|jB@_smf;R31(?7oFbOsaR04_M z6u}9Cl>#Bia_|QKRsKuhH>f@Q$N7)&ALQT1zmtC}|7QM;{A>AF@h=1KF*Nu^kTG#H zALBduCh#0h#h36;;h(@?$rtjOyf=8S@?PRS&)dU$oc9RtLEe45J9)SAZsy&{yOwtu z?_%CnyaunxOYt`IFrJfV;%(%qcoN8VsB<+Y$w~q z-pE$5CG1n!C$Lwtg={A44c4oymsroU_OKpjJ;HjBbsy_a)~&3YK?I3wSy!VV=S~fw__?WHK3VFkWT6#CV>uhw(V$ z5ypdz`xtjJZe`rexRG%!<0{5wjEg}=%OZH+WHSR}I2k6!Muv(ZVVuG^fw7VyWH9M( z&|d{V1w2pR17ch}LVu8cAN@}Ht@N8g4$N!mSAn-=E~Yo=MS6<9nU3|JC$U09kChG= z5BI>~9yr_shkM{~54`6dV9sr(Ppx3_m~-brc`lR@D8o>OpbSEZLy18dfYJ}84@xhT z9w^;Vx}bDI>44G>r433elqi%ID9uosD9K^Y8DUuuB?6@Z%5$LnD3lwaJR8chpga>w z9h6!qHBhRdR6(hPQURqLN*R<=C^tZP29y#g*F(7u%C%6Q4&@psPlNJQC{KZM7Rrx6 zc`}qUP_BmZBq&dW@&qW4hw?ZmKMduuP_BaVLqO6Q%(-J=c_l0#4a-Nt@{zE71T3$B zWic#^U|9&I07^cTJSe$Pa-d{G$%2vzB?C%2lr$)(fu!u68k8>`6fYT+E*_LF8k8;^ zl)8gbXHaSnO09utQi9EF3`+HZs0{=;5Y>UG3`BV#N|azTi-S^OP|6QVxq%=CA`1kt zH!~=u2O>2P$-&HI+nLJgUQNJF9_(2!?{F{Bzo3>k(9LxLf| zkY9)|q!+>q*@fsrav`{o+di&5CZrd_37Lh+ZW^2l1QzlNafP%(SRtzrRY)oX6>#W#t!ftCN|;w|E~xFXJq zF97R)j~Eqi5^Kd$@fxt|UnO24=7>S|0MW}}&HuD$w`iAWr|1FEJz&MZU38OZn`o=( za?uvB-mi$Vq6 zxLtS?Sm$pQUM}1sYzr&GELi1-gdXsN;wGV1CIK}g^cpn^>Ttw1VRBUmk1C0HTg2&VY2 z@n7ce<3G*c&ELh}$$x-<56Ek{oqrR58-FYRa{d;6n_uB)`4{j*V7~$iUe?j_rTjJg z)%;cb6?_hViuW4tW!^sC)4bihUA&#V2YC1JcJQ|IZsKj@ZRK6g+rn#u2#Q(W1-uZ? z!$WzScv_y6w}!Wxw~Dud$Kg$JU*o>a-N${JyPLa_a5#J?so1?;@7y_xLdiG zbGLxkjI-Pd_X2K+>*1o@OtzCHFp(v1((B};=BfSNbKW0&DqV_#o5VufO8LL z2WLBHE9WN8HqPaoEu1!|!pU+j;Dk6H4hr^pXgN~O8qR9YD$WWHhcm@~jr}rvANy(c zZuTzrPWA&JhvNJWB ziuD@nW!65{)2!XBU96p~2Uz#8cCfayZend?ZDn1~+QMqHDy%H)0#=CSVWF%|EGb2W1na|M&boMODjc$u*e#G%~H*u~h%cz|&aV+Uh9<0i&7##YAV zj4g~dqr%8CE?|Th9tO(T1a_848EY7;8LJp87#zkF{Wbc_;9a<<>AUH>=sW2T(C?w| zpl_$&MBhf=O23@Gh2ExD=vn#&U_Z=~F*2A7&<8E_oe+`zuiHJ1ijk9t+66%L20Rvw-Y7Eg<_|3rOg;7W#e*l>3egNE9BY z@47&_@3esI`!FDl@bA!zE%e9d>d7U&TRpD zw*~0l7AXHl{e2CT(8DcIE{^)1@^O@ePHq8uxdrIv7AQYQ{T({G1zgwUj9#w`OZ-H`oR2llb1L;mL}{;4wLf1cu>DntI~DgLQ4{LlCK z2jxEhK=%0uvd=q^{doY${yYF=pLZbpyaU9nzULv|^N{a(if`)gkneel zZ>kLWo~QVx$`s#}gnZ9ad{bq}_dLZnRfc@eQ+!ip$oD+OH&upw&r^I;WytqD#Wz)k ze9u#SQ)S5aJjFLvhJ4RMzUL{vsqZ1*^N{a(if`(B$Tx`8Iw(WF=PACazeB#~A>TcU zZ}5E&^4)`c_b9%p?;+nk$ajz8oBAH|-GhAhD88xhA>Td7caP$m`kvyOl92Bn#Wz)k zeD^56sWRldNAXRSA>TcUZ>kLW?ooVGWyp7r;+rZ%zIznkR2lN!gM9ZWzNzma-#y58 z5AxlEeD@&VJ;-+t^4)`c_b9$8J&^An|J&=(59^}3Ux$i;l zdyxA%$o(ATehzX!2f3ev+|NPo=OFiUko!5v{T$?e4st&Sxu1jF&q40zAop{S`#H${ z9OQluaz6*TpM%`bLGI@u_j8c@ImrDS2JR0XVZrpzE3^yvHGIEAM&d)JxPr4<o41 zEE5D3rvi4&P7u<&Uw?ww;-h1+Ok&`Oph;95uPE6lq-vBo2L?{6#EeB(yXKBq?H!~N zZ-<6+@K6nwk4LCpCLYT_44|SWRT?0LbD|$T>+KjwROp<4T?{0RMkzmNs6D+?SFfyd zf|PUQhLTn(&1^`8WSt^zmlJM>S%b;D<)S9*DVpM$_Z;vmUMTq-L~KJtMuHe(B}++O zbcae#wOLOF%7IeOmIYZ_GhGuAk0Xvu(_>l$DODif9sPkIEZC6dBRITA@mO&ck)r(z5g7IALM^jOrxbDd%jO3HEJOI z7@+9FgT@AZ7mJ4@dxHLYA0vBe)GD1EOlTtmGi5_@Ifj=l)dw3-Q#C7Hdv zA+E*Uu42}rPLw)w#N>U?(@ofICQ@m{R^EVXkb+5()HHks2jY+!G?+Ug4d%50MIs_s z5ZbUmBtxud1t)aokTK{Xq_Hq&TjzJnHzd||v23v84w*oXdVd80vmX_;u2HHHnNlj* zAA+$=Dq5#TqfDidN|g#}|J1>B^RedQroNl+fazv@<__I-Bl~Bk8_J9JJyNO(g6dM| zcMmf(kZ9C+U&etSBqW;ZUGo)C^&W#AYE&S!CulyB zytdcs@fFnJSffN(lX-R9SyLL2cG}(z;?9Pu?cGqf5hfey!L09@yh^Jj(^`CmZdNM| z8}*nm*!I7MD{#~B9DsfIe~jUbmU3jP2LEWM$j=jvgiBcR^Mx6 z&vy-8>Po$RSNpGx?5R=76x1?jTMC$oT7my9ja!i zj#pFdaJdkE&x75D)>FoHEt3%|#XCj2R)(nZ&2Tj?^%C}Wx7sbB`IyXIY8%=`w2%T3 zZZqMIRH_PfRk%MU3lueSyCGaEx6yhdu5wpQSevZq$GfYgPMwPX2Ue}ZDPl_Wi171* z3}4Bc=KP3#JuAypF=(_$r*8m%IyC=!GR8uJ<^+jC!}4}BEH_JIL0!XY>HmK%X3=$% z#%U#*HCEH9W zylS^G({dvHW61l*Kz*u&1K>vlGpLF&Q(hLA%TT-27YHa+ ztt8eO(Perh8Z;5+F1YFPtFtz1L*^loD2SX`b~@4()a;9>6p=zpt(KNSm$5&9U3RFz zG{|HgJQ9oN!U~_I&#FhQQ_R48c*q7k@RCSuD zR#jCK;kY*DaQWnDSxM>)UBv3E)-6~yQ>IP-f4Iwxrgp%X^tdeLMm(3-1oCBhFh6RP zA)#6+FG(168YzZ%n=*^G;E9^GDR9H9FPNIV3*|%QJ0y`dA9`*xUobnki$z(sjK0ZOGk8>q3BJgQWCFUTS==6MM8#K z$e^#IPqbC8x`~vU49R#(+HJbRo>qDKtq)+A9jfWylvk#L6;&jtvL|zTeJT)%1q{j& zU55CY9<4lB3U=gWQ^wE^NX-_1CoZ)h+KyG1vq?NQPs?TVHMCkV%BDVWUDi2dx3Xx` zQ>fJ_jLmDs){6J4R586tIYPe+!k`?9azy>1j$Y`8g(LR zQdHcPOnLgv4_=oYvimYKZt)eh^+3tgj+OM~SV0FO0*;#g_4TMnmQ$LQDpy>Fo3f=? zGeHuuk`>Jn`ji68G(81l)|ahW3!uy1_yBgccZv3ju!G*DL(>C zL%!H{CC%1~zk}sTQZ2`f%`y_{R&zRCr5z0FQK>49%4Duw1SwDd{R7x#hiWcElVmC{ z(+7%LGLcI*0=bMjqOgwWGNZYXRH?{LIEPBx7HP`YA#>Jzpk$22odv7bUCOn+a<|28 zYuCNy>Az9G|F4=#PKnPK-78ul%z`)PkLHuSAMs|n7jYiutYI&Jce>AJZe#qJ;REkO zPtiQnw@hr>0YX%2}M|~c(_F#`pRYQ${jp&e#?AZyjI8SIJo3c(2>VJN3 z0vjzuHnL_XNC-WljV#JK?wbDRrw(SLa=0JYFPbSE&)I*inX?n*mWDQFi5f@<)QL8U z8WAT7M7^{8b!SqR((j}`nY+fecXrW4*}GxCy^PrjQdUEI|MBa5ZFxHxz``fr@=$h8 zA3L_4r!E>PI~Duwq|Z)}0UO$R;LoqiTS})aef3&q&>&Muxs+4~OPGeOdJXzu~OXIVS@zUT42wU7U7Y&%a~Jd?8X9lmFR z9PrT2cle%VE$v_XpBzW^=wt4&EnU5+8ycmw*$I-$PdG~ZzUR^BDI0hB$F}jrMeVTf znIJtqG!goqWxJ8G?cvv{&qwTITex~rGt`T$*$EQezq1#YO)S}G{d?z8Hh%A+gV`t> zp4Zneswo@a!Ma&*j93(9vfHeOiU=+@MG1Y?WR03!_BME$Bh{H|wobun3kNdL#u2P9 zYj2;P?|g=`_kqH}>>Wn5|5kf;4PiWa-!e6$=jCgt*s53K53{-y-Q z6=_vNi#MVP0)zG*!1}Uw_F4b-9h9B_dCk~%p1P={?0g676C^T+cD{r4Wi9Qqe)nG} zOYi;2*p{wdR1A$$+Ux{5kS83aeb(=Mk+Siwz}PmPxF{cHeS+M^&_u}kvfW78cKfRS zXKq{zso($gw9J(Ff5eO8g!l~6Uq#;+eOeR|odR~`e_Qx5p;ve!h@E$b;8KB2uoCQ$ z{||l>WVR6Se!=@Pugp6W#HQN?^7W;;GR~XedBJs@I7b5Zl7El=DR!7W%laehKUp6K zxdKlBd%JIEUc$68k7hi@xP{ST=ovz=*ZNEJ5?x1Ufc%0t&@wdn^c&MVr>~hlfBN*P zSElage1$XrkH-v3#%3n%U1tTN7i|AWmBat@f$f3OS5F3Bd6cLX!*e?r!B79rNwX~3 zXn2C98vXWOnpw^`mpT2eaWXj4@C_qqb9?O-)l$ciYlj>Ub)5fsg=Si^k!D1rA6`Q< zF4<`KMJVb>cRorpEZJ!IITqFErZ3X;OEwyQXhSvnwIQmPpGnZ^MYCIfqJWaP`qY>lbsoz>?nk5^J8V}REXxb$kjTjG4 z{opy;k_gI1T-&GiK0#A2d8848=&9{rqbZjfL?hbVe%_}POC3i#Vh}yGeTKGlqY;DX zsV&#i;A6e?sj<`&><3C=C36*xlaM*iaf_Cy!#%V_!Xz#avLz`K$ z(Wvoo_uaJBOEwxc9`3x0cG8lKMqS!({*-p&l8r{pD0{bknRddGjYf@!EpgiMOEwxY zqwFmlK|5~AMxzE1aW3t{OEwxch_ETzu}cl25tr$9Ouy7@tkR4cMCV*VTeXyNno)yj zU4!+X zg1yu6THE1*QyM9yWS&UQZ?M|KhKg5a&8HHL^31Xitlt@hK%oKKb82-m6)Q%|WHLn( zr3~0eDA#I6?1uhlHc2gO*Lb`5hi4nARia#~gLm=!+dbdwCaHJneg8H|sZT=GwvAyVRB`#csx%ohWmS4yrgkge^Cl^sRO9!ilr?9> zR%=zfL>I9HvS0Nng+2*fE zeW;AsU~IQgwZSWIjkoo`f9hb9)W`9~_0(X$N;f^p?;tbFKCvAd?COIJ_K|yWCwMAP z4a3<99z6H!Pw?x+=#U!|{2VhXRKV!HzRULMc%Tk@_+e20nZ3R;O4*WoeW?rm9{2i= zGW4^7WbFkB}lo7z%6(v6MS z>wEI-znbg`ekB`C_M}roy;Dg6|93L{LF?VWx>Cd(7*yvFQ9gW2K1}%tLx7dFY>Wiq z84h%@)b(2iSW5SM1yQC}Q&Yl77u)g&JY?8tN` zRx+sDe%Bx%5EWb^R&S6XQV|v7q)wa{|=gRxX?l;U>qylg^D_L03wKLwUz?$6CkS91f%PafvDEKd(-{@3qYJ?rl4+* zhd|u_I>G-#0RF=OY81ZjQLR)`o#LQjX7qMHZm5=J>7Xla4CW*nw7Sp z+YOa0mbO%iZwQ-xo>DNrU71AE9>ilH^rc44*cmU(O#jpYchvfyH>I5t6$HEa zM(z&I@vO@kuh1`;ei(eY#J~9!3mneog~ZHBCg$j0TUF}e=QSUo&HwU4ww2LPR(s{T zRIH@R2Tar=~4%AYt1YR#+yV&kJ@Bb8IsK; zkh0H#N05jYbvtEp%^6bgpUjgY3#-?ycBSfPo{7{trNo(MqJe;CgY*og68v)c%WZye zP1ws_li`Kr2kM#}s{KW#LOqUGED@7g6)WiRa4=E`2el(M%NQ!npd%rP6;m~3NN))1 zZ7sdO(~ZZ8rpb`Uf)3nNGOAqCXgv$A31+~8J}2S}jeSk#p9j~3wcIrszNP;_U6Vt# zfywAAx-9>fy>}0HWvl8$*Zck8c{uVooO4)!o<&%dcLfnlr7EdPQk7IvsXX8i>XmvX zRY~Oy96U%PzK~AKZa$lJ8Q41%{s9All8~i`S!{f^Ec-lW6Z}Ga}1+aE_TypJeg)#g`n|8k1>%*fT-z6 zFHrH9la**A8YqtCdd0E=k)Bk5(gH5l+exT^=o#9q(kN5QV2NzX^_+D66wJx`Wpi?V z=Xh7m$yGh}PbwBuCbW{D_6xcl%9ctRi!QQValc&1^`%NXh51OoL{%)jKtT0Th8o4A z9l0PSqj{}`hts+_Fv(&9$%AV@40Amv;?u#LtX(oE;q!OcchQ{qb5}JdXtG8^W~da@ z2Nj4_xRRk3nv3)#uE(;tRbj()PlyCt12LrISvA+KlN~6Jo+%u%4i7E?=#J zF>b=qVoc_Fi3_$+Cv z1&gZGB2048z6+ssU$ZwzWQsx9M#gNW2*q^8h$rb7qaiR;sbnG~G3qL4RyN7Fvtb}v zqUL%|b{+?FvUJ&;oOeRsRdaG>>j}~0pmcx?hI(v8u|#r1mfCtv>iAAHIWzVWg-xr@&y ze)7s}S6E>JF`LDe5}CxJbSc)Autn=h+z=Qk+#84M<0PGd$*@r}aAw%@r=$XvD@79H zL_r$n8yzSbH`CQbxfGA%NXf09{K-d=S^ocfmiFGV^{lm@S-$5sNB)08e*$g+|EXKR z!vn&r^KM5L-9~zNa_3y<9eH?FHvPVbhjNSE0v_HjE^_!SxQz$hIO`Vp7J2Cx0Qt7K z1z_mn^rXY~FgfOmA%9>H9EzC1U??0h`lEie2j$3AJLpfMWmFe=@|#GypYMRQ)OMjO zj?hHuq=aPpDf7gX8#P`^jzyj^301Cz#{($p_gfjOqvzsdx)O|c2AN0{32=5g*$R+M z1fd(byq1g7CrZMdf6nRp%q;+&x&=H)*VQRv_w6TFH%rK9H%|1fjam345!$)X=rWp}pvdN;* z%ax(7B}TZ6g%(l0+lTr%NP2q`8mCVbz1xnfw%&l#sg!jxBzVZuaXbs1GEP1 z=Xcz|6X%DfwH~Ilbx~??J;MoKb5dnr&*lHRQ)hk)+@6C!hMm7Ic>OzLcjIph*H+Nr zjc*8C%WSE@YhC9Im2BSR@~fKn8hM1<4JiDZ|d#$wD; zvXs>>5xxAKhvA?-w#5v(HBs-hSR}!!MLlyu4E=n2jI{%zt~El@XfGrxd7^~psd78j zD#v>TnNfI}Ln2+;CIvr8ky|VJ)!gwQtPC*KT^90b*PUg)f9<8yWgZl}@St;7XKohO zwDMwZoK^JJVi)cMV;Aj*7YD$7iwws%695-*jNNSm;J%4s7q{3e(AJ&C-9-G5Q(<7C z=C#luVJm%z3+J0jDMk46<9?;1_@e`q&4`gm8E^M3v;YwtbjJ(5G?vt1Fia3^endtR zMOMl)Nu*6Sf|CEl%9o-^Y1H!P2ZNAAn!1=b6G-HwFo?!sJcJJ8C*ycGsSO~yPpe9| zUFFb|@d$AjcjI(@?*D)O61Q~t#)EGhykY)&k+TLjQtGk1puk7@;zr1a4eQB!= zYQviwU)*S}e_>r;`}~>;Dy7RSPpnAGpIH``J`Kn({(0oxZ&;Hg+DlF~FR8dL&%X7@ zyI=1afQQ6tZUDS)3cv$XFZt5k02q4);2|5D8vvsz01sK4%U5I*wmzGnbey77PH-MuLQ5BcG)b}R76ySttNT(Jz;o&j9340JpLxMCS- zPXTzyzvm?no*ON&JOjAmDKI?)xZ){jO#ygFFL>3Ku<04V6;FZT8Nd}!fj$M`A$#Ig zTSCnibsTwDz9Of3>M?!kZf#AK&=74Quy(yKmZi{r)F+^IPxO zy>ICwd+%BM)ZVAoUcdgdoiFUASMxg`Si5)Ut;@9?cIRmMySKlzcklLxw%@k>>h0tG zr)~ZH*6%ERWlLB~Z}~U>ar5^NwS&FYCl2AmjeYds;|D)?U^ysO-!#W@wEU&j_pQBW z`9l|9F3kSoUzJvl@>xi*iP6Buqg`X1keNZbRUgY(F;hjWgDwb+w3-9l40Jm3fjXm5 zxfCe7!O|I=LsFGs4%}Rp1P0DzQuPpL=8dc&*OiDY3#9{PMp4d|WJ*kRX)Vx7;eEJK z9~(hv*w7(K9Hg*ttzJ*Ji|xqR-umc_qU4i0Y($LYa^3E@&Y*0%E?aFWnDROeBjeuV%{dNW#PjC_NShAw06J$oX=jk&p5;VdHjXI9M;v zDX3}@leC-yE{igL2wY8tO}$FWgRU81$YLx#*6b);_M7D9CubB;Efi^G3`8t+rHXxQChgPNKqdWv77E(uOIb72UF}j`}Xnuol49gY9 zuv*c9#^>O15pU}lI&UHrpnBN=&Z>4B<1snV9V&!uCF;7Wh3H;5NM~wrvRKc`2hx-x zH&(_hGXRyeFq5_op&mwk{&b|$&*z3>rA`(b!Elgaiis4wwKSs`b?YoS2!?rvXu_>H zs@qaq26xr9VNnI4<5axcu$4-qT&Qk7eMT{CNMl;oi7~FWqeZ#HN2z8K=kW6`yj%YPM zpTwj=ubaq9*yamn6v1G3Jir>fNch{P+U!V$VbHP!)1TudSPgedp-fZp#o{Au_t$3> z#gb5BWS|rzX4tHhE@vXFUy1mt@j@Ky3bj-}Im{&DbrRbD+KfUs(v`9j26262C7O>H zo4Gbt3l1~wwAQydG=^%CVuz*u6uJNT8AYxVq46vhs`T2GAPh^f06X>>r9_KkcsLYQ z>k)e_597Taz5mV`MYwI}j6%YW`%`^ClCJjHG@LIaBYnzf7L5|qEC+icGSqz4%?D-_ zR7*AV_$UzUwAzVizg{$uZlNi5ApslYj3^Pb+I%yfNSiRG7EW~09I_p`Q|U=b5-Xk)WQjhcE+OQjUF zQ4Ul2B!y(gtR3zptVSivH=xabn9TuU7Ci8d?|X#s_M#6-^FcEh)lQJW7SsW}t@F`k{kgoKdt4-SS7q zNxwd>wXjS%pJ)b)P`kjUf{`3Y)Jmd&5t8`pjGu*tX^X|856NK zc$A_vB7I(psZg#ql0vD%FuttKDToRN^_%{Bu%`D>4R}%t7)i8q>1ZMrE3~v$E@Du1 zR?y{@`)3o_Xdqo*+&_r2*%Ak1?Lj|33Mf<%?Mh>RzaaUm5giZtvyutTnvi5F+4HwX z$(lJ}^14pYIVHr{BdZ$86pIMYjBDl5P;3OC6>3hA9`wtgxE~g5M*_n@1BN&aR%D`) zC)q{_?_o8<)QjU(n_T&x8HGR%B@u=|RX=%zNA=p+FJf7o7elB<#(aU1(eZb?BdwIM zSMHfn)WSnkNaS0|ByU#+WS}dz1{BQYLRzEhhwMtN$OS<~>3mjRf5(i1#nTxjru(IS zAlq(~^|mFZgSM|82vQgw9E0NQ6d%@;CI>HlV@6TR=?2O+OZCRc^dVYW=?1|I*J2}J zC$M-&OC;2Rk8f0&Txj`kXB5TZAZa90El?7_Rb>(2hF!c%aHD6Ea!FQOEm7EL&nWT)2B*zLsHBWg zK`fv>7!LPluH5PDW}e z#E?^AecTDAONERrZ@h3ukw$$q)$CXy4Q^9-g-*4zC=rlLVWXxq^)O3>X*i2D@w&bA z_>6*Ra*_h6QO=M0(19hhVWUTBcCLfUCfh2hjg*w({DV$eU!BhfAzJlT!fa=WNUfcORWm&+2nEM%sao3l(u{&+IHbl# z5VlW+Y&<)*bJavtE(@^`nN3#%kd`x=a7-NY@vZq@DVJzyK+5$7o-=$Yj*)p^9mhz2 zBQLcqc#KF&mZ*<)ECy|UVb%uN^)sDLv&fqj+hC%ZN)}O> zMRMm|2YXA`yp8U+;@k7lHMP3PW-;8(29-oo@6q95GGGOBrBchP=1hOFV`~kpHKzBP zAes7?zZJ;7)5hI10#5wDZ|PT;4*%_8;ou(*e*S>m|H}T)?Bjd?eeb9C61!j6eaG%A zc0RlF_MO=Fr?-D}JGAwuTW{Gq+5E)jqni(H{MU`wZ@h5*W9x(U``7;Owa(geRzI@Z zT)k)IH&&FDr!IeBSy zg{-OPLgi5=>myqdmV|>UL|~0nzb`WrYs%Jg!M0EndaZ=eRMh~2wQ>a?ui_OVY9@l* zxJ`=;W`#yfF$NXs5;HNUY)uzz^(<{FO10**CAsA5jLtzvV1AfaUz<#}zC$l3nM)D>V0c@L7w#^6V zRx{f{;zNn(inz@U%Z>9>Hb`}aM7h!LYZYH@P#dNyr2!O6bRu*-Q84QHhCB-88jbOY z0%L1T*%~g`7Aw`TB*mJh;O`*`aab9O8MKt98ab86s&+I5K|IKHXKN;4t54bLF4zv^ z1*{X!42GQsln98iQ8(Etqahe6h4@0NNj0o|O0iR+lmasoZOT@2!L}QN2x*9$W5U-a zI5=)8kw(1cYecfWaic}BW+hgLCvwSFpI|2Hl&$K5ZM7YiM6?p`6=6SI>Wsn$F)EOL zU#C=)?4X&irF}(>tJl~X#7vYaTjc>IIZ$P>TGIzrsyL900)BqVI+;L?@5Cc~DwDB! z*|sS`j#pTtX+`}?l#BP|Y*p=*d&w5fOyntB*#+BRIM2niVow>?BQ-hJZFhS8BudtD z3S1+^n%eWFWy1n*5qeT9+H1=qS5GZCk3 zMHg&)O<%0zw?cf;X!MKV5Z5;pe~k47+YNKnfY@-rH_)q1I}V>i2e z1XZwLC0<|=4uU||#%fLuP`Lt~7^u}A+7^sxRc9vrlr8UqEi3q-a;i@lcsL2kRy~7R zR+>RtGM#U-)dIqg^dQ9g5<w=suBW3w}UL+9o72bf*lr; zP+lC_Y8Q`GyQ5;Z-zr5DVPI;iQ?^wXY%@Ayr3XT$Q-NDXy{N^}h62kC$Q}*}i3S2< z0L+jtrI&ytRHkezZrJ*Jc^IS!;t(UH1=RvfiJcH^rALWiJ`%I46k-eUZqeqGBr_>b z*_K_fEo&mUoQ~9TnQ@K@rzKyz;5VRQHPH7p2U^)_`LzTD0kI zC0ot=Vl9-$#gf*;`gGBT#^x|@q4DOp3AT3GDch_IwjLrYnMr2KHsgXV4l5&D2$g$S zx^D~J!PAA!PY|zATuGRYzY@^J;b#!6MV`R zcfq!a3?W=2hipn82D;5^uo9^>^fXR%Qe#xAi5;w21{21of?#z>P1&Ygu=NlE#!Qk^ zwn-OkJwz2b=l|8EUtZe&>^cPgx!Zr<`i5&E@K8v2`}(q%D$bXW+3OxOdtF17I^gYB zgEMRiJPTQvuy;7OQrLX9Brz9Td@ORn=Kd@8uJJj_xH zRDMCH^E_`7N%2N!(1uJGqrBsLrhK5ye z^VS-$xmfWSq+!j*gBm2VzJ%-9%hXNpOxI!!mo42gM3H zLM0MzkJLyU1Zxc20g96bXv29j3GB@|#k#ruQ!|^7wsP63CMEfPND}2zL~GJQt*Ew| zymA7Rf!RVU>ZZ<-y90%fUL!fgCIoP7HowDJAD!ak2yv4qEzW% z2x3kmiUnrNi_`$K^<&Sx7H}R|UyCjuc_4LTwYsJ*wd$$}z=kg_*Au&llck$C`lmxb6RrAG#83BOuL6~jZmtn>Jq3{M%;-&9Z0A4DO78Wqf_Bdq5wI`a zBi^DDpcl6wIkPRF<`ubQqkjv}b7*hYv+1rDe8ZiVU7T3qPWt*VSQvKp(o5LV?=0`# zyYj-7@`1JcgcISvnqNNN&F|g^V)9?H^M#!c?7Ve{UH*yvPcA>ab9DHw)hCvIXZuUr zA6okRX=MLHTVLJ!?X`Qie)3>%LyOsa6^24joUrTSkYrpTj zCOG?&VB-}Vd+VQEe{8+Kj_yBg{b_5TTKna-H|(QzvZ|15ST6G{oj{`^k#cbng zH26-vXZ56dth)P$Qwo9aj7)_O8emsx4k=`i0|8IPdMlMk%Ww!oN(o|6L>Xoj8SH*+ zMlq--t9(Ljxj`Q&f4>D}BhTI1Eg)`cS-ujgpMTZME_!f@GB?1-m zqqN>D^Ju7+FnaA;emKlwN--$oD$}ZN{fk*YQmHZr0w`nIcz;+|$6ebDm!M2j4n>Wa z)edsBCbeRtaHmdg{rHT6vZ4)s2x0=mdBInb@?t*HuL{9J0N45kr8Cu5N2e)UA7LB5 z87kI4fOQ`X<9Zd1go{}nu2(6_pbO)i!mG-7tfkR$5+tY459a+~wWyjw<59h<$i9w% zQ(+cOAhwbVDW$lmRj{nUXImvYKyG0(R5Wn-TN*FJ1XdN)3@_rXF`g~+O2O|2U&??-JXM&M1;$#*Ee|rCD#5v}_Pf#CjvE>0?nfOy|*z3NqewBPcdb z!dq+8m`||)F}%bi0yfjCWrF2mKNwZ|7?Y_pYSr$P3uB+m_0lbW(|+Y_s6cHFyqu!y zl`0>Xe9Z#n&n6ikFO^FOh*VCbqhXVU8|@wy+TEFfVwD0}O$lf`V56#kK-PFH2y#Y` zAtMot6$V|*pJBK{6y=C@WKMw(N_Z$00r{izl%K@;x?pfgtlH=Md?RVbX}A$@hWkB| zT>haMg)dvjDgzR*x#{8k>Q4J~X3<$tB=n4cc|J zm1-4XC7kWo`y-{w42e3TxAdk(`Dv6x)#}cV&M1mGA_jLyi6WCQNX%EQ>gi##0m8Zy z`7&?lGzgueBS~B%gw@~Ln~_){zuapFBJ8l2^e08u2ZJCQCdgr2yk#4RRZxMPX^O6g zHvh+LKEUo;v#eSsUtzP^ai`ZFbF!StN$H$K$&q}x+d(P3n-j#)ETUFyacw5XW`cGn zNOCqoQk2?2JEWQ!7Ois824!@JX2WKYSfDk?w!q=L&{sRy za%M(h5YaKw7RLDjh%?8dv9N@YEOedypK7YQK(s^8qFycBLKC`qE3!|AUxBGl<1K} zqZvYIael7nE0pWP`txQKawQNpt2we}=qRQ&EL*LJVbp|iqMT8R^%xvXRIFUN(k0j4 zJEIUKr8tHQRev!+DqsYH^$cKXN62u5q$3P1#R(+DYM4px{y%dH$`ZJ|mJqUdQ$>1W zQN_R;qFOLucJfBGq%WAnah6@N?NlrIIK^aMK)R^uTD`4n^ZR*^=M^9~2@Xc!Qr)WHOOj zC0Y(v)IrRu)nIdk26{?VsrQmtwm#?!W^|N}7D~kS(TsvGw{v{PkCOz0#+BG05Q^Fv zN{Tn}5CZq|h4CQU37A z0ix!bsVJF^7CFj_)Rh_&m#T>w0nSl*K3?s`!r@3W59b4^1l1-jN(#ozVGj;UF(Zg& zJ3~AE$P7~o6vpXOFcayRT0W5B(P}!a*XW4d9;a$y7LSUZh^i%fmb~)cXB2S_H$t4& zWCe8`jjE+kUN#`OoywUlBRM3vG|5nfl!==5`t0F@2*xch7eWQJ(n=<3ToZ~{ZJZiM zE2C7NwmW{7<$YygAQj}*IYo%l#E>9{(~KnzTiHA`DpHbx`-6EE5!#TA_%puHDAJ13 zOFuP36~u?h5s{+NBBaXg9BYVCDOLALQehzE$8o&QqNE6R05-8Sdp#!l4LlGCVPwj* zXeyV}hxJ+#JaZ6`WX?+0f_i{&;}I(>CCHVR&f4(j({{MT_@XgM>=u1?(e$C=QMpkd z*l?m*$i%fqOYhQph~AjJHxogKSS%`9O@9HeD^(DPMmLKpf~R~Ec$CrWVQdQ&j4P)? zB(eD?vo?HcK477C0r6vC_2>CU86uNSn<$B$em@Pu;U#=%45DDYSby1+LKsTEY*eZv z*osa6k=5FHe=;MBx z3X6ecCQuNKTp+2!D}Ono5Lkjx!5d1P!C}rP&}2B)5#nQiEeqb5iFm-5tJMf-faipb z-yi2bF3OzZhmjV_e7xU6t*P7;*b$7)-pMz78wH5Rc4E&9^4Sg;J}aZFk2kLEAswh zOTX)4r+(LeW!iJGmCla1bRL7EHA3=rq7B(Dr!}yh;o&4l2Nbm(i|E-%tqvc2ct&B3 z3KC`chAE{omYbQPmdLcrH3B>sE)=6-e>>Do<@{+SL6KYkX-2`wV;m!@N{lE~3A_=f z%Y$J&TpRW1h+4)as@WX$=mHiE3hS>v`1l>Y;1FyWY7x;u+ZJ+WNlZ~3&h-tU*-2Tj zIp_)@Bs1WQV)(dcZqKAd#9CZuq=hO)xDaU?jc$>Ld%>VxCE%czZENvJHQ-|7;_+*ROYzFaQDTy@gn3zWc`EH!bfHHl0s3C(uEV)Cq%1C4Y z2cvL=rFV}HKXzv`I2=W=y6DsGLdzGE>a7mc(As^o!doLWmCa~laIA=E;GK}L|3T1e zkMDe@{{NMwrR{&S_BY_4@BI7|8-ahOUW+=8fgi4e7yj-az+N7cb*{9z6J@X{o&pZs z>KJ$!eD_=~Rp-CXT@VVu=Lu5vfg$$8n&IQp!6 z1SH#yffg1fJC({Hy>Gd}k};^4t%OFyme7Sbl_6S!2}?=%*kog>Xt%=0@bs$rHUSD3 zdhQse))>enJ3J1Y4ANDjn;VXgyU{Su_r+Wq>55G?PxYeRyh)Zb1G1eqsc;~Lh-Dow z^?CuMse{WoRnekl%71J}`luxy$H7IsGlaLld7gINao&fnMP|vcuBofrAbZZacJ zc97DAHXxfZjYuZi4VRO0LLZlLVieO#RIG@f*eBQk+;GY^IHvfu(cmN<_e<@V%y*SG z(+yfDVsJd*1Q3ugE*%xKNPcXx zrEou3EhUf?!vqG>K=1p*hCGTwAWzH5iPQ_JT~vv7v~aaCWcVsoj+6bS8i^qnR5Sqw zJQi{$_|~8!FH+-p@5|T1&LaIUzjyh_1F0Lc<;r^xLwCD5a?G8F99lP2@vQgWLrC&J zIq$u*bGT%0;>D;eHp7>C?<0X=cFN&n4|*k z1I0?JD&tz9+b`N|nhDUy`CPp02Q}OVJ;_d)DdTv|YVjEySKfP345Uq37;=MRrcxP4 z2~iU=;06<16hxbLDi$sG*=~abw{3}jB-uoZmg$csbJaW@7>3Mduv&#jnOq-9jA12n z3=!={o^e;I;dJO1STiqr1IGP3{(l}E92C`Z4i4XL|G#to-(8ZI4&Jf<(C%;SFk64M z`N+o8*B)E_zU4mx|9w?IZ<62q+^v;|A9%B{w6yZV3oJ$CeBoLeK~kW^B$i3uMWs0d z)K!&%L`#Eo3zX+fT79vZq)Mf3nhz{0zm1ZGUc+C7&Hg9}fixwQFYpP(SDTP#;}1)VI{5EfR?|T?2U#@2a7`s>-w|r3s^MzUr@4(*}}H zYXy)%L0(Y7IUcKkEJZllZMP+%--i7iF-!)B8lJJrG=)_mCOL@raSkrWHkpW3y?INF&5)C?mL%B{^Mv@&WQf3J|=`Ztr zRYvf%pYX>OS(r-oz1N5Ofs3U2e3)CIlqi9V|9Ii9G9Xu0vKKSDJZq(DSTf83hgh{- zGpgl9rL7ZIznm3(peULhsvwCfk{%$%(U|AeK9mh z)p0$R#jOe(rh7sp*cyl-9nXUF)s;MxXX;FBtRRJA-p9m{(A0oD7n;d3UGKHpFqHRs3`svq)`neYk_4y+VL(&Ldzz{5VR}J-59buA4iZxij zV5G;TvXyEW$!w-M+pW}EV7C$oDjZmJ5<4?xuY;*!^D7W)HWRTtyyLSuwyT3l;?) z@G{9oI}n7{ea+q=kpW4#ZDh=5icm~fjCc~HAlDF>sZ=r%k{ERrG%K6V`G0Nso~6CF zY&~o3XTXQ={D2Yoq4jIu>s+}1*t3?8JmhV;In+`DFLM;}M|;a&Du`a4B8LB$R75Y9!tp_$3$3~S zpl0q4UIR|pK0N(&UrRO;(Qg8ZER|Wm_Gg#6& z!Rg)fFY{;bpsx<|(`DXyF#aT(n@B9`JdR(u%qyc1oJ^x9y_$s=LwQJS*;WuaF0{Z6 zV426Oh%VLCN(-nBwWlhxhfr%!EsJ0O2_$FXC$ERmU*Y^&NAP>22Ypy(el-n zzIGdh8vJI>jkAhQMdZ4IfDBe%?3KgdocXx%@ci4b1b;Kn)Thy~w@n6dJ>l2I8|RCT z{kPaFFmQLe!HxuD7v=>T<&Y%ANisdqhTUcm_iL>vESAm82ntkBDz#p=+DJCDSQM|` zvF3%xWaC&YMYYx#DTYD)F=`l!7Q&f0-mvpXvKhh3JS_9YVy< zzVhMK_0>152&>fSe z_pg5#Gzxwq>-3BFR~}$=qsNYil9b@dE}txyi|4K@=c~#r;CuJCPRYs`Qz^-fWQ}W} zBHzy5mTc+oUcOETL2JbkQLg5iLQ0JnDLtQHkj1T?UBi~&^%?;6VfVx{_ za^PqBweyYB0mbnKTg(;9Dnc`OA>K+ap08U6bmtFG2h=5t8w0xY2d4w-lJx9=?);b2 z0d+}yJ5h_o&iev)K6W~wF4@lq^!K-4zO;7QEtj_LY`1p*uhVY1B)(m@;NW?0^6p3G z-EwOgXWd%`jsMu)aPbE*I? zNzcyH_FtSTfJ@@r%~K?Fetq2j^r>FEWIt1Y?LPN5P?*8_vkV+j+?t$ZP&6IMfwX&+EYYe?A>Gm*{Vd+WK#t&Y4TnTjy;3L#G<(lEq8|*MI$Vz68#+stwXi zCow^*7RDEBRnE@WYT|T2T@v4JzCcdFa}u(OoDQf<_BRG}6+InLm!!82=ql!MWMuFj zp=vO`gt%%l2opu^K;Z)8)bJ>})LkkB7m6_bRa9xg@^bY{brQahAS(mE6zo z3zz=>bgErqzcJNIUpbv>m!!8&_0s?JIP7Pqdg-f<|Ns7_H!K}qJ3Kmg$HB(_VE?Im zZ`!+O_m_5Gw)5|Hp1b|y+b`aF=hjO$KfIX-_XC0Tf4%;UwfC$ctDjhHto*kXeEHMM z#ih>z26yr!|BI!K6>v0_wq4Y3o8JQ57`!771>Mxi8$nA!(2|RiZVPU)k33X)dvtg5 z2x#oZ4@=uF3cbx6yPZ6TBj*MB$U}8FVe$qC4d}Xy`fxX0wH$dU76&^0`gx~a)QfY| z>2T~OlNyK}c_`Zk;JogN;hc|GJn~TU&YX+^npc83a6dS=s``%1MUDR=OHLbVkpoij*!ldP( z0bO@d{&9}xjtoz8-f0)*AYFC({6X)?Lk&qV2gV$Zi!zd~;G93!9(kxe3E=2+I4;Uh z-WePZH7m`D2GBT~?xJet9L)kv_fXDKo~WQjN7G#tw4AqiM$G>Z0(-| z%M%W?=xDl&5}@-I&uF@bIBe(q|NJFx>F|vQ-#B=~{@3?kxA(QZ(e77w2RmQc>1}^` z+ur)pR(tbrH_eSNZZy}wu&%FteobBd+^W3t#EP{1nPp+=(}4WqpGO{h?wTagUe=u> z57y)IqPCAb_M9mI4=c@)2UF<=z_UFA@L)>Z0QfG?0Iqln?wtbgu(}?3FxhTK;8~sl zT(Jz?;~Bse%fK@|1Gr)tc*YcfhyBh~%fQn;1GwTTc$#MbS3CtzodWP6*;hRUPw@=k zil^YvGk`0ef`cgl4@|Fm3idq%xZ)|;n*ngYB|o}i0J}387$R_B)QAQAN@2uB$Yi=3 zSCV`@Rj+783!{~ByCQ_+jTSB(dDy#N{siKY$96me@DNGt2Eevw09X15(##gdgQT9&j7A?3N}0gxZ){Tp91i347%FL{6`*J^9;blxyFqata=7;#WJws8Ne0G z!15G;hlA-=%fOOn09QK&?|!3a09QK&?|x(oz=LF8?G!lY{|A=nrNg%$t{(LF|7!oA z?R|CcZF?`;egE$F?7VB|aQokF-?#O?t?cIGo4Jh-Z-ha;zUQp{A8QY-erUC_^0}2a zFaOWwA76gq(ysy*vmb8q(1rx=&z6o7xW8Ot)iSI{>-oBDvZ-98MkIoY1c&8Rph;rI zksgf?;nLs%OG=Z4bhm=^2{qB}g3=c^Ie#H&e#tSrX=_ft2ek1@&;~t98`)?HuUaf* z$rNFwO0*On#!~TOdK}C4^|%}@vZHb#7k{9e*AuBUl4-U}m9$ytw5p5Sxb^BrntV5C z?*z05GhM*B(N%H2HA>~`Le|uCpg<<5SVp!aEC~lyh`<`DeqVOo-i?bJdGcb=P7t(H zs0qDRLTIXL0Krc!)l6-PG+x=6nDQH@V-z0=n%XM#VLjnaBVg zdH%e^9zs1HP?7^x7OS8YmYLgj_?GNg0L*=JFdUSHnOIcN(O##PmVB7s z9Cmc0itr6Jp`3DoU?h)nop;P zU;)=nc{2gr{IA9E^uM zz$XvP?i`GV+ps4IOmngWusE9S;a0{qmRmL3IsdP&G?#YdjSTqbJ3rqUfhTVSeyDcs z6;7I$ACE74iB7s)yw9b1af;O}EH)Thl;*|5h2EW|Rcyd&7Z>9)X*G?2zn5xRY9(Xp zGN+~6NS|)8CFyH2oOXRdT^}!U z=j4pL_uBV4+Wofg14*3R%ly@8H~j6;ZVxy2i?!Rsp5rdpZf76w=)b7imcfnYiQyh} z(z*EpkUs{pyanoh*dK);A5dTCtG7)O<)mi13v7cwd~tfVNQ4izKsiG3ByfVk{yr8( z`IwQi3o>WPk$y!Ia&6M;57NauPZHHe8=W466M6%RQkj7`Fk_(J{7~wv`9Sj6(1vy+ zkW^B!Y%Px>c!0yBp+O>r!d+R)H`8QWED=LAhVvvK8zguJyWc>tZiIL@0-#*P|dSm~5Q)2Inwxzf5 z1afz=oO@d{FHWpzdoOAJhKGS+cFzCzEs;xy=)qeL9@ziq`$v2K=U#I6{X1XVdBygB z+fHqLa?9NO`lhk*-t{l9!)t%I7GM4Ns=D%(m7iGt$K?sg5%@kpeEK6!Ubs1ZJF~Rg z_UO|05-ng&jzBZuJ;u_m<=Xpw>(z&s-~naw0?;IQkg>FDx^D8uy{4DA0d4XiXbQZ& zTH0;7ZtCpSrkCJ>NB1TVI8*j8XszkGwcB3fN6x$Vy+jy@llvVwPUj8RaAvLxFTn%W zE%7>dI$U`gvkf7%} zQ|3sJ;@ZQ0dkH%4?e`LJAWoj+z;PtVef-8;f;_|$JPJ>qJ?pl#dsVOUZ+W2XA?)DD zgCibb|E^Q)Ro4!}+fLan@d{q>9C`4q0_^vmVy|2g_VZ4BFNsEuJh=IRsyu6keQCG+ zWMTJ!$C}&&;7qL`>l)4iE9fD*K$$!fG&!|`CD%=!v4S1~3$)2IKvPpI$hdCmCM)P6 zx&Tb=(@&?i=(;tJD&;{<#L3efIF1!8xP~*gf*zs^fHppL)@^B*cI}6~eYej?0eE{k z>i$&;!2Xm|?D;Fgem-cxOL~JN4`BsB+zwC0E%#($_khQm8~`{|OG3GZv%r#gh-OeG z`=H6GB_UlmdB&1>2xQPEd!VVQCCR#O>LyF#A({b9?e6K+W?Z-CQKdYDJcyGW2aaP& z(yrmmEs2L{2B3}Gvu;bf#O2q1cNBHt#pme7g8{H_onptIEbJaUDqe0_o%8>_OO2(& z*BpG{;3@mc-mmRFW7pdG=*~miZ`pc$E42CMjZba7eEpqkUtY_vzGro3<+aPdw!8%h zJpDX+-!K{BKKhbt z`r*>D^vWs1We4H9mr(IJ#9M9J->J>J1)X|{z&xb(KXG%dvXKsnwL=X zIo=xv@p7DN4#M%zIKN+4b&c~y*Hi~*@CMFRFQMvloM)}+ z#X&lg(WwNicsY2^k zTqC^yTI1X(?s&^0F*C-gz^0S1i)B1hjG)(+1Vq; zO$S}>Eza+J{vwPE#<6@3rfYb6KoO>2*blldI)nYHIqoYS=jd#ZyI`!B9>zUUg`;Jj(= z@o=c01aYpm|{H`(Hcdc-K z?eTC`Fo!r_doCSD2V3kPo!>8GTqC^y8hvgQx4hgQ+&qdFhc>C9ovRUH%=|fwn_g}SJYt;M%$NMBgV6_Hgz@~X1;Cg)hjGKp?Sw~+ zQ=9qFy$;5Qe()lUw~XUUu2Bxgn{1|s>w`JQ1vc~2d7tX`3t!{leR0Y)-lJ=zgZCzz z>ERAyj`xhsycFluX1>7h;C#WCT;qJvwXB2lCY$Nu`eKgrCYyOF(y7gS{@*%CA23`a zz5iO~oRqG3$(7?#N~bpSd8&i)d4KI1<9*lC=WV8kOOZLmxy`(E7#(cSe%bW*54lAM z&i~8I%C9XQe&z5Bho3(D#No#dKXUki!}lKk!r{9PCx>r6eB`ius2#rckUh*EB8Twd zOAn6@pMCh0!;OQlAAITHa|eHR@bQD+JNVGS`wxEU;O7qBdGNM_Hy?}-tb@kEs}BkX z_`xdq3!#(@7dnlUfKHU))%)v17a%vVC%QH zetqj#wjSI1>8+pK`jM?4*y?W?Tl`jKi`qiBUbY2oy=d#XTTk2C+Wf}mmp4DZ`Kir6 z-ux)2!SHLF@7etM&7azQ`{r9VU%%PeR5o9;$!w-Kzkf5b`I5~CH}Bm%*j(HA+Q#2* zJhAck#vg6`?#2f<-na3K8$Y{oZR5u_-n22?Xl;lawT=8na^vM2fsOCpxNqYb8#^0I z>t6x69Y4MPiS>`Ie`Ngw>+fCvh4pu>PuAbM{>XZFU0Z+cI=h}-N7mu>m#!bJKYRTt z>lU0h1MQg zyMOJTwY{~K)vvC8arHB+pIrTe)!$zI_0?ZleQfooSATN#M^=AewZCet@~f3qY8735 z*($X9qSfcFK5cbt775{oILD&`tzMnZBAb8eEQVR|F=1LmGkM( zcK&p8Qgc52>CWSuldALS@&BK_FOPHND9_$GtE*>b*Bl0O7`$cx!=fGCYS~~`w_2@R ztrLu_qt&gmTWYl~W3LU1F_=RdjK^HTglmBi3?acBNz9d)9AJ__obV-aNB{>&Vv;}% z=J={gv#dSd^-5+E$nVGg;pf$Q>#euDs-CW{u6iH(YhXKrt*_C43%1kP`WyPM!FCE; zcVjDwt-q$f3buJ{eU<(S*iK;UEA(H1?KrmnivCNm9mCdN&|e1IQEYvgz6)$euyq&x zC9oaF)|cong6$Bt{+#|(upPwKpJ9u`)}PXU0=5I#`V;yKV4KC(7w9{|wjWz}(tixL zec1XV`txAhi>=Sop99+-Y<-UYEZBBq>$CJ{z_trppP@euww>6zgZ=}s?ZDO_V(S>T z{($}z*nT0lexLp%*uETFpQ1kjwqJm)PtqR;+t0_=C+OS3_GQ@mIQ=oOeJQqXr+*J@ z+p+aA`gg%LgRPI!9|7AoZ2bTux-TF z2kGAe+Xig?7X1ORt;f~}==X!|Eo{A?ejnIAimmt2C&9K3TPNxFg6-#F>%H`Q!1fVr zy@!4`*nTdy-c7#?Y(EEE@1oxcwlBffJL%s9+t0?4WS)?4YffbEO1^%nZgVEZt(-b}vuLIj_*t(9s7HrelI!<2=wjs8z!4`$BtLc}5ZGf$p z(j&0Fimj`#wSuh?Jp|j!*c#FUu)Tz>0o@1Ni`eSZJ+QriEsgGiZFHb*5wUg$Opaqq z#nv_0>d#x>Fd_Vxr88hc_%B1J!G!Q%l1_jL z;XfW*g#QwB983uR#pxKB5dMqNQ7|F=7o{U$LijI2hrxvKUziSo3E{sW&4CHwKaLK7 z3E{s0&4LNxKbH1`3E@9K?E@3Se_q-HCWQYyv>Qwa|G8-wm=OMR(M~WS{O6#LfeGQi zWAqEbgz(=B>C3@{@ZaV13&4c%-}C9qz=ZJMW%Q+BLiq1e+72d!|LimaCWQZNv=vMU z|5<4Zm=ON6&}J|p{AZ?3U_$uMNE^U}@SlO!g9+h3J-r1cg#WhaqhLb#Pe&gC6T*Mb z!xrJcBlL5@gz(>U>F0n6;lJn5mw*Z3zf0(6g9+ikXVRNsLilf!eg>Ej{(A;}F_;kk zyO=%(0 z5||MFdlG#Cm=OMZBK-s~A^i6Q`g|}U{C7V6crYRS_jvkoU_$usar9%sgz(>E>BoQx z;lIbw=Ya{~zeDsvFd_VRklp|j!haj|I+zgtTc_8+gz(=QO@j&HKbnSMLii8T089w~ zQS>sH5dH)73YZZ7TZKOb6T*MX^b(j5{zD&_z=ZJMBE0}6g#Q-k17JezXtynOfdh!zkzpyG3GybH~ecb#{7r&^L`bKG5^7@!mofa=07xh{Yx;${0ILM z{skCg{)2x3zYNBh|KOM5U0{s)4}J-L5sWea!7su;2V=~C@Xz6&fidPk_-F7>!5H%& znhSpcj4}VgFTgv&81o;z6aFz6WB!AG4F3p>G5^8O!_R>+=0Es3_*pQ<{0Bb^KLf^? z|KMlfr@ehPjPj4}VgPr^@t zG3Gz`3HWg^#{36whaUrD%zyA>@b|zN^Bu%dYgC)7-Rl} z?|`?0G3GybD||Z`WB!A0hra>FnE&8!z_)=h=0Es0_*O8+{0H9x(K`a>Klo<&CNReQ z2j2v50b|U6=q+Ujj4}Vg9e6VsWB!A0gg1dP=0A87d;=I`{)2CT+hC0O4{pN=7-Rl} zWB7V7#{36g4_^nynE%i_&uhUL^B+=+dkq+4{)0EdSA#L;Klp0+Dlo?U2VVtm0AtL5 z@CNuwFvk1`uZOPyW6Xc>74QTYWBx;LP%j5#%zyCZ@MU0(`43(PuLWbwfACs(9E>sl z!Q=26Fvk1`uYp&CG3Gz`Qg{^@WB!9z!4Vi^{zLC$Lomkt2ZwL~#+d(LANIf)^B?R% z4U94WK@E1n81ou%tidW6WB!98tbj4*KlFxJ24l>BP{0=EKUjt(Fvk1`ORxyWnEzlA z7Qh(uA1uHt!I<#hl`s#+g#TUwuK;7he^#BE7!&@> z!VDM_{>#8LO+W5{<-i9HAeI=yCsUuKuBTY)v1`S(x6^kX_`urIq0fS^fm^^8t3O5R ze^;$uwz|CX>6PnOp1pkM@{P-$<%3IKT-sUUm!7oviN&iIy^Bi=cP>;H=tI{ZiX3|E z!7m-W=^(f9-Hi`z)HaT+|6u*2>!bAxPrbp-jk|iC!?eVzK~)T=e5IBr*l{QJtjj)R zVdu>2AU4BZ5l8y|UH2ouQ!I{>Iln4}eYqa%d=&02mWRrL8tv4L`|5u07JKTIyAqVM zMt^!8k$Tmx1m&znAHHNty?R$LbXFJB1ug3OUFFJ=w;UGw4X%}}hMa>|wi7;ci#~W! zaq)GhZs?4KA8CgaI+41k#>Lm1x}h_=KYc?Nw@=;B8H+yrhA!T6>W0qfVtPXtZ`_q+ zpY?|Bkz`*S&spvC^zP!DJyRw3S*tyx^z-7)yS-spsgA@()0HWg+Ah)E;X8ZU<_t^B zAbg4(W-RI|-P6PA&#>Nk?xFP?+5%v`wH53=i)bOQAV$@q@?fQt;FS zddB*v9c#&Vs?HhTJs;ex)D63ZXMHbzaAEp~c>n*>;%5%5_gDWO!LR@P`VRaby8|fg znCItd$6T@)^jRAri|iBZy;?MraJ$NpnloqgdAfzHp<|Y-DMK>YF`xS^(?gRAnhhwA z$C;7}+O20wEi}iLDVzsup2V-@-_Nmi?98cx<~ZFqd#a#wZc)OqpF35_-svnZwO=Vz zY}ncVRNApK1^#GL658#0+~v-gl2D&9jjHuzC*)N_5!T+xdA9=5aI4r#1O{1Whjp37 zTs+&{8dO7%IwhfBmHT>OkaGvT-l45kXEJTClSkSk6(is6bPG(c>&}{r;jmc@rB%0n zsP}g(b}7-3q6I&v8Y52L8OaV!_EyxxZLx)dtm+-Hi?2p0b}rUhYE^6rWs)MYAPV`e zicFMkOs_Ya^tRngKSSEFd3xj z`@ha5d*3@AnM>wPJ9ch1`32IB?JJuY?8dXPNi|&T1?6}q6zYg-I??2kxuoEXC8DF6 z)H7HcS+~a)XmlJxv;U~sgiG5*$G{O(S}wcMubKr*EhM&UcB3)Y;T(~!$yaZCilL;I z6#^YYbx?7I{EDI5VZ+9Zn9xFHo7$~pIbSMn_B1(Jl>_C9b4E6qUiz<4+OdcA$$dyW z_AsBQwfF0pX+7&R^W4w!hs8zGj=gf_W)~*r*F14ye(%> z@2qc6CcB5f?&5}ha8~Gjn!r7*(f4%5GX#S2?Wt0sgXRQyf>0Keu7sup7G$3zX(PUb zM?1%|*_pGlVsqphT3msY*t3O(in+=5B2q7x)k;!IaE4uGt8~OtG>#-S{piy^r`!qA z9X0ivg*3<7+>$KCS_QFE-ctPWilHDBhcS1j7VimL$!@40S4thTCD1Juji!M*hDEYZJ!!;mXgL~G*_#5qxFm& z={X|RSihS$q)KAg+YIIUg}zPbZ>n*=?e6%3&3N2W(U~<@cIX+nT@fYJE3gK8D<3ns zy2(;i8I=wf5?q)|_%czS%oY=!qG#_%`r|jfXd1yBUN^lQJ4n|94!6NrtE5}`L^zOb zH;Qhio=`Q_D^<8mt><(ngxX9IvD0yA z!(5wdxO6%;$>Le7GS4u`= z)oQxg>CX~DW*Pw#a>vVQ*(ul^HBr~;c`7lznQ4nTQ_USUm>f=fDcs_$4Ts>^boLUh zlALgrB+VS)nRKMrbagq|U1)fX8NJ`bb^Jyx);F0W%(>Hg9=|C)jbIM1o9=>SO{85> zT{|Yky@5*8mMhtM1#VdA#CS_EA8f{Q(g3whEIUgCsc8grc-=H*x`QqguVzi9u&ZWr znyvom$i-M4&ax75cw(KF*KJCr-IAG|C4%HMf;qg-lL2*>2>59Pb10xAU=&?4mH(zl9kR9L2Meq93rdpB*vK~g6K4YIZ~C*lSk&=M8HlXm_w;_o@^zvMBtxBFh`!y{gn?rev@w+!5kS*_E%8!_)Xqv1assp*^qkKg2+MlgrE=sbBa zW{JQtjbM(C6Z^YfMx&Sa3l}yHte4mBq4|{$ujrQFw{hR{Gnd}6bm98Fi*H&yf1?3| z3mep(lpEY}=%okmUwiSv?!pATVD;nhu2lbHe;@0=ZgCZ_BP?#_>kV^PG6zk? zk>G8pqDfY&^=PML_l4_jok5pKD8Y7U(DbK0oAKcwCWkrAVvmp1j+;&NB`qxo{YcnX zbo;ILRM8kwsiW(@ZWT*B>XnVRWdEN-xP!pxwTYP4%9 zzT4IU7Pfy_OE}xSFe-RUp;klgtDe1@_AB1gF-U=?$jh~|tKaGuqKw;@Y*gIYc79-P zSA$;B#k2i()mjy->ZaCcasjU5h=p~wftVjyWGB#dMinNR;{(1}U#=GFPG3V4w4OyB z@|uC&%&0b27`Y4ursEk^Q;fdAC>m#um02cWG&L#}#TEC&bYjvHI$W=5f{XJvvxTfs z@pzP2c28TZ(x+n(q7`GqlV=N=KvFH|)lRJH;N$g%;0SY^V_5W9Eg3OmRU6`Fw;!n( zih|hk$3ufCTe0{DHvcHb>w|gEh?hNFHlWkBdCr!V4=ZI&C}vtb>O$$%z{9$fJu%Rm z4~l_Nyl$pwDe)yKo=;fVqHdVik9Zlpvq-IgY6veMPFqhc;#GfHZzziEg&{EWGh^c%yKdLUQ}a>2Yh)O#I{ zK_eIE)Ic}Z)*`4=xa7lV$PAkSIao=S>)!G}9(wk~K$RX8gGH$Xjpm3}W;mM^sEwjr z%h6y`$+k1dFhZ-{RDId15H~wEW4Tt}Rjov!CLjADX%SIxZbk5(6#YT-#$rIfZ1NiRg7>f3CtJSXrt#XqoJ2gY4tv7I`s-@Fl zM_H4#*N<^S)>_Fn*v(WX=1iA6Zdo??^G%m(N)%$jrpAWYK}NI<%}GZpkuY~F^`?5* z<8a1n<*b&~njtBe=(B}AF)Y3AbPNSG9PF`~st{eue?zcMC=qOWX zL(MWDmIGXJPYegXd^(0+&1GVmj(R)RZ&&JVK0CC;JT@ho6+J#rL5HY9sSw~YRz6bfANI33UU0ewJtdS-N^M`t zy(fl6;dBh$dbD3JmQ0pXqinYLilr{wD)cQ`-cxKf`<_NE)bL2Ykb<~c?Pl7};vjGK z*u0K}yEbY?bk4G|8>+WD>4?qN9Eg=@CM&va%;DlF>hcQSM5yTUBzocEps^>0<+on9 zK%N&1n|U=~iDaBwz%HngnJa{{qkvPN>E&I5Y|Lg%CBYFDbXIS!8r#fp{Y2km@QVq( zC(=L+;^vEPugsdtE}L;A`j}xb<29C&4yANhE<1X1KPdSFrDmOL47~L{F`R$;HniNu zXd~0{^o`+Icf_J^6sd5s7dH3fg_!IOD8-H^hVEq9C2v-SOxf*c4VIGVKxLBI45PEv zZSkR)uBQv}5U1%Z##CI)r^UmyiYldiVfUckYexs(LE5<|gaao}$57Ym{oHt3THT{X+)#LZEto+*vY5rbSx2C~7ZlE~ETS*PFKH@D-~ zsVAt)JLW<@}9 zSm-OjU}P%$1L_cUhI}EG4CzZV#PGq>F;wH(qR3ZDy_#`oQ3o|0tHoojLEmO%Gue>Z z@w0L?osCvqq0NNFRBokhgQm&p^qD#auGjE#LOt4afD9f(o^J@1Rf zMzX!9E#OB_$B-`g5ni<0m(9k*!EUwCKdd@S{b5wjaAGUrOYybv-tSTy z=F>5x3R1CctujTfEg2gfVW9X}k1U5e0f{e+G_LCyqK#PXbl1Eo>HD^<-c~S0l5)7& zX%yUvwwUNRv&nRDRPv-6m3X37>LkK_@o=Wv_YGoPzb0y3kE_8YhY#rwM*HfO4~iiX zAEcXsbUz;%4f5HXzdUdj3r&~i<$dUx-V;*kPOv;0W#r99O=Z!wWhgjx z?NmH#R;8i2(#zB%OfJzV^QIJY*clBExpuQ(QhWop==0|G9{$<@r(+P3{77yq*=Rjz z);Uu}Q_5@NdWe~I-kz99vx&0UVMX36i<^$3oiBH*vJ~PBjgHHc;RXh?V&UT^w>lDh zcG0XTeT%!~_d5>fB1*j0OG@>0HsJIIazf-G_d&1UICMIOsww6fx;;Wl3F>%nH0ogzGd9tbC-2Y4)l|anw>Jh>&fIU5Ih$%p2~i_@ z-{1{G*L+wIUA+;f6g~Z>)^p2FzPKlbh4SebO1X@SGX-i&%r>w`in4FyF$U` z4RNU0QmW3RsD-S$IYE3pAVHE1uDI0 z%qhm&Zg%f<^1v;pV~};J1QTxw!A4Br-43U<@2~W#nTRW1uND2ODr;JtkrPRexXJaM zqT)-3U1d|*XX*6fL2tt8*Eh?_N~jes$OA_aeT{F|dhzVx4BKvwSZ}fJ_r;>DGVJet zbT2;UbPTOP&@@V^$$nXs?LxjVh?pc}x>WNwTz)O+XnEYNR5~oX+vd$y-OHJ~iBY0! z6Eiy1Y4Zrh7-#DReY$MKmo2mdJ-#W_8cM|2|b8kEr<=p$w`qk^)`jghawf1{!C)VO?7agjhjBB4l znV~ZDv*3TBEFI%R415WE9`ykA1!{*9C=;~+z69O|>c9@b>Q`3Zx!PWR;p)c9*H+%Q zGFb7hJbwA>%O74oz8qS9%F@3qeQfC!ONph!i~qj(`-?X&W*48m@co6)E=(5k3%WzA z2Oc=^g+p(h`R(4YBrI-L)~SgoN17A6^A~v;>auV z>e6lrbIVbe(Uh=l)Mu&&A=_(*``&h`ueJKQq%PzM>5QXj1btOst4~W*jGPp!lmePs zFiN4EmPy+6qk6v3*EbnHrL%`)9;RK$1|7-O2c{)Ti9kHg1w?x#Q1H7!v@FYykcR!bnaS-U>l*EK?JDd9lXj+&!*%B{2q2Bp{H zHuski1FJ|kuq($xOdY!(*Y;T+T z(Llq|Vyo4JS8vn}eN|o4XDr4<@UBxO47}Zx@9A0At;t1eE^jkhM5|r2Br}GN)QN@L zLZA^HrakMAotDVCT()S$kqM@`P{tRKm}aw*>FEr4ecsEqe5PnZFd1vQU@f`ut!as5 z(}vCl*sN)@Tnku&Jws7;Ck?z-(|pB@X3>fXog%qvhMK&vF)hKn1zTJv7yD?xPG5>Q z?4?AwV@L)BOHtvX#*ko42f1OJ>@!j$^es-vcF8>X5a>@! zgdCz^?f4^giL z4GZQxH_Ta*NsqO|dNQ%af18$=eYFCw7~~U!jJ50$ zf?039T2U<4x@ghc_?~s7+6}g*-r(;0GUDQQrkCM%_82u27qpnc>WKHvIf-|4RlB2w zCc=Hcq2w>-9X)fpk}MsY24&6Jg+egW6L_EKaO=^2UB&1}FL-P$uAyr~gR5X2G`o_n zzVhv933Inu&59PrFZo*Bpk=Rw^e$&ktD_Aqirh;E+TKBiYnrW1^WvwbC3Kcnm~qC0 zghdUMElspZ*WZjW#cClOjFy~1F5T06;t4~f(O*zW2?@#2Ms=pPo8X0Yv%b%UrD zPMMlaKr(ym)|dw^wm9S5NL}kqOALE%LtnE8(MDjiEZLh)Ij%^(Y%c9B2+6P~+RwWg zrJZq`vhW?dC7Pj1)f$uAHTN*C4ZXgU%@7H?OJ4Lv*Xr*%lEsiv4`l44It)%r3hVT-q>_?u3Mv zSFGDJgPb#vk7c=%JzKTc94;%f{Jv?4{-7Rpcpa*oi>PjUVwehfgD$^aNL2Y|TWwV8 z-kK?tcb5$2)oFW3J-t7#akgkyYG&+3HO&b@)x*@AhEXImFvY5=lC@p8d3)9Rl51Lr zCJpSWKb@(kjjA>s4zuYaMw37Nl(pa_?h8asJQ>_a&Q_CoGnqul_Lt4bm`FgyV$(UDm zc1!q*KHlhxn3Gk>CFw$GV=+8L3xRdJJz~x2{N{v-=?$B4du$;yEz#~31EW;Qq&H+O zp=vkPVhX+-Z%syoelu;Z=7)XWAMd6bcE{562_?yze-w&agql_3vh}tl+$>d1hH9_p z%5WZYCm67nESf(imP^ZDCPPq?4FSIttTl_Qt0i>ZI=iQxk;G!J#pg>yr`y)Gl;Xw= zXEJ1;HVvv38#dcqMr#=4R-k9g@Fg)lklH+7uUGXRqtWg*Ds@h^sGgPG=4c9BA|b~U zooKz2G>?iwXU&e5d`7hvkG7Rc#*(4$6(VA^8e5j8bu{b&CwiC(4YdC6N!CU&TRLm$ zja0>Jb`A%LZr?03!&s(P?yp=mEm8Mt`fR9;Mrnoj<%6=2OE|lY8Xr#hVoiS|A%>Fe zXsqK&JC=3_Z8ec_n@wEYs1DGsNoU?z&x)C%F(<2HGZ53qRClV`2uJ*NkseIzC^;;F zo<}k}lDTl#YAr>5HdPlv>sb{sVs)gl=!w|Y3O#?2T{QMC9nT}#F$S)7_KlnYD2 zjB2h0jn=Z62}QHPMpv(^23?H=vv|q01n<@&AxAC5#B@!6uWfT!GO30+%vJ*KY7X)D zND$RVE{7Iqm#>_bKr8cof36*jTl5txTayQ=B-1EoTfvsmXHcB+Y*JUYj0CCBho9dq zVH%`ki8_;xqjzm@r^S>r5i6IgTPte1+$>jy-dNmH>bvaf%FWXftjCzrhH2T=HcQ5# z&2Djudcjm5(;K6F&T$tKAK8Zt)(Gaq@_PSy>M%|+~IVB zJDcfdBi@+8n~HKFXEP>!2Dzw>JVGL54Z3EpjZ=YG)jbP)JwZT^zF~&N`?XniK+QqYAaMWrGloCHJi@6J`u9+leb?G|*WS2x zIm(YdrtK&%>5b4#{T<2>xCQP8*MaA*{^jaRSD%S8#`jk)TK>Xv2ffpOZmGHSgvCEx zlolVe@QH=e!utP6Dj9y~h0BXEVeNH?6G``|Uh$%38O`MgM+Wh|+4?qRHXY1HY)(%r zS~vMRXzph%=Pa?VF>GxadaOn6g}A6{)Phos-?^N)eeDcxxuDWF*Gg!*ZmG5hv4+8r zQUYSp<5P#ljJc}ufqYzO$+>|c#P7U-xP8qGZpDfeM^iI>iZe=R&B84QS*_-d*piaN zQ{-yxc(t65)jiR!Q_t@_pSZno2Df}A#XF1@wH9s^#CpD$^7_JTy6a-?4qaM^#=EVI zUUVr*A@1OJE+cMVeYlfu_Il_zNz)rDr+TGIbngn*rqc7L*knhox1wo_&dyjOgBm(W zz@g)U?OunEi~?Dc$ob5o9o^eYiQ8Ar;MQ-ISoH4Wl=BTKB#2UbkTBS@l?W$S}VN0>2j8d$tR2>$h!B}I!@H;l* z_WBvzy4YMX8mQP^9dE~9td-IZU&S&?2AH0=7SGrF4r@FZDx-ZVg3Rw&iQ8Aq;5MnM ztlaemIo8)!&||pId4!Ibw8cg^<7G7xud7_GY2n$F%I{c++Y^VyOeGU{^BE!4i}+*Z zLLs(i9dVOU7=K&rbzPdy97yAj9Z8-k%Eo>;mD0tmc9*Z+sfK*s=m>Q%GjaR!8Qiw| z#(p+sPDPUDaxBRgliglp&{Q%7ohKqG;ci`%MxAh?RCl1^U?Of`HiO%Y>@igGam|$O zh|ZCT&lD`Cq~E0H*Y03p35GyHx;KLUQ`p|a&nZ=`(ZXL~m1INQbVJeRl~ z&fs>A16%ygbBNo)3~uK*g~jh&Lfp=9z-gWXTl~(miQC=`(av!Si{E(`ajTvC7|!M# z$FTUFXA-yF8Qjiswu;}`ByQ2}Lm?1~2f7(&kFC18-Eed7?!$SGVevc9AZ|M|xSit^ z6u)yZaoe83t;=snCsWllQ@7=#c~MUJ&^i+9GD!ks5q-vH$dee>(MvRw?DIQ^iCg8| zCuTP1I5EZVTtwW;Gq|1O7!<$rbmF!(gWEaIHt{=8BW{~BxSiv`62EgHaod=|Z9ogz z-GaG@_NvvLLZi_%Tij>^mO)Wj*5+tML&2eJ4;AIG6+O$IO5E1ZeY|EKp>BTXDa37U z2DfvZSmJk{Ox#vya688_B!1^f#H}=g+d0k_@jDj~x8e+L=g^ntcb-VxR?dC?U>;3q ze&-3qZFUB?a~$pAcg`nnGc&lIP^LXMmJ%igh4&m@Sk0WkVGq|1OWDVN?zaT8% zec;gjhweLc@1c8;0^nVT?mTqIq1%xL;K@U`9=hew7^whWduVV-IV2$+z|5iOA^)Lc zNC{AX=#oPh9l8K%0aAw!9K8SFeMk-Po`ZKEyzAhdNDuJ#gSQ<#dGJ=G2sl1?!@+A0 z4$%Gr>EM+IGY6wc74X&7h`26G zEZn_**P0&b`dzek!P1LGPwh3iQIfO5H+@sT+{;-+)pmiMkSL|3xW3b&O(=`rjqgMbrhCuimzL5-9@SvN~S9Vf9+138<_}t5>dOkSd^m_1G%2szQTUN$MAMjeVuR~dpkV0T)CA#8Y zIfgU>^(&XGT(oilQVFD14lLild>_&Yyl46D^*h(^Knjkxt)E=Kb^R8k;dsOPwd;d* z1*te*xt>{%uKSUWBeSkwzhwO)q~v(sI<4>_@h3>72jh?7nBW6Qs|Wx75mM^G_(P=AgYkZ((u488;f(};fV6t>@9!g} z9*qBmbb2uUCsOIb_#g0kg5N`0J^1&1NT~{T-y# zgYnx)rw8M|AeA1B_rjMF{1(#c!N0$WlzK3J1L^c&{AZ-ngYlnWpWxS#RuBIDk4UKp z<2^{H2jf2=l^%?L4?6_^4r%q^-(N#YJsAHM>GWXy8>G^M@ov~8_}56Q2mk&mQtH9@ z6{ORH@vo3d55~WM62UJ+k>FiOvj_kEm!LrKi%72r|NbOW>%sU5q}7A*<4CCop|^bx!XdI`P(dI)YqH^B*X5qv##5_}zW5PU5>M({Q8g#>SemlJ#yd;!55;PVN- z5?)5|dUz?pS3o<#6ObYJa%dy?GH4}u9kdX<7MckjhbDs8KqJAcp@HB_p`PGXaEssw z9wj)0I)VfEJc50AgkTRom!JlpL$C`kA*jM<6YRie5p2U}5>((OK^Z=SU<+PMun7+n zY`}{M*5T6$*5K0!R^f#NCHPc=B76$L3VbrbEPN8d47`9~8a|O=3O<2g5}r?xhmR+i zfR7^>hmR!~gO4E?h365Bz(WMX@F2ku+#nc)>jXKtMlgVc`fz_^AtdOB6hR*Z1if&T zpa-rHbi-wWF1SR{2^R@E-~z#8@BqOVQa>YjIrUS5FQ9%x@cGn_30_7$K=4xPM+EKE z4+%2V{RC~)e-pG)KOktKzE99h{TD$K^`8Wd)PE2(P~Rh{r|u)TMg2R$qtw3<)KULR z@OjjC2_B)oL-4uOw+TLn`WJ$iQ1=piHuWun&!WCb@R`&%2yRmUjB&kf`3C$M4Em0??t58H^r~+;#UY> z3IB><9%=UB|Nj!C*oW~7q}PY>#qchIFM?kpm_?d>`0q1Fu@7S!>GfeuA+%$mDYJC_Z@bd)2NV5LOl=?8f0O|B$ybLM)VZ0Qn^}ebz3d zNc*fkl_Kr4_7sY=&)SnI(mrcXqDcF!T|klcS$iTy+Gp(v6ltHe^C{9kYmcW$`>Z{V zBJH#GSc zHHsqbvj!;AK5MHKX`i(finPz#GDX^FZHXf7v$jZ)_E}q?Nc*fEph)}BKch(d&_AU} z`_Mn3Nc+$~rbzqH4^X6i=pRv}edr%jqZXfzP)Nd2~HuXV*|3dv1!F#C>5d0SPeuCem z-be5o)JcN>M7@{b*Qxgq{735D1n;5VMerY}cM|-2>Ng4g1@#VsU#4y)co+3{f?uM3 zgWwmbw-Nkv>a7I-lzI!nKcU`C@J{MY1pk=2h2S4iI|M&Z-AwRv)EfzYmb!`Hr>Qp( z{6lJ+;2%&Eg1=9V34V%tJ;6^>uOs*g>a_$vPQ8ZU$EX_#{vP#ef*++`Meuj18wh@c zdL_XRQ`Zx`jd}&a4^bxwevo=O!4FU`Blv#mI)d+`t|fSqI!^Gt)HT!m|KB{YegpNh z#c%%V%-??<$*=Fg{@sDsoPTo=r<=Xmwy?kHboQj1#mPj^kO$nZH=QZn>>SBoaFWw` z(yx$zKSz2S^6%$J1#{-~v~#RG&YlNc^7gA@Ih(8Zv*AQvXvF<{HXq>F&z*;OuL2h? zb@u$o^X37!V|S0JD)0;B0e`4AnUU1>C^OzNjNW8Np`KAyz!cGI4r?T042Y`D>uUB< z5`qHCXP{U*YH_R9^as5@lf<+H;n8Qjb(T$4Mrmx7LLvJgjrP3j&4p@v%Pm?g?M}bS zSkOM75f%r~RJVnza-mpla=mmzAF?axsAPq6lt=Zx8)dvj*VK@a zxS%Af-Afq;-K0w|y6R%I)!$+<*e@;QlLOX={_5f*dbGK=l4#k3LUYLG8p)K_uv?S{ z(-R`-Kr$QP9TCmzvs=_;EbeC|Yb53hRW&6y)T6T%S~e=wJVi@DtRL~#4d~P*$J-TL z|G8BSPTl%np*-Xd8x#AGhx}naA90*v?It{Q%2icq6oy#X54RGQiqskP2D?G8H|7lt z5@^-yEEeXhqwG7vjTSd4Rr|TaJiFhLyNz3i!|&o2)Go&S~ZX%D-Z6M1q1*LBj*H z5GKz;SL+B^Qst_Lu%ceShZQ4bhm6NtHqPp+xo|9P9@dq(uiuIKEyMWT|aUmR- zhWu#8-4T=?z`Vsp6X9IOWZ4=SlZINO<~1qRp*3I$kMu&u-jdMqB1f1#vSm_Gt{hcB zxxyQ*BS_-Rm2I|sqS`s)&JBFsYK#}MZGB`mjU{sFyZQf@7lH@YFNT(tPa@;je;)N6 zxS@VCi#hD|rG@=X7dk74>CePrTH9VLS%SqO$H<{@CR7-4x_qlA)uQpXE?G_U=|RZK z#$=R+6O+I^X-3cd7C7(hV$bB@!TOZHBv#T?p)-AY9y{AibniPTQZLi%@#B}2&6kBr zL2Gmot6>Eip(nJ6!r4_9?~2#&PIN8!|DHk59+gAxPXE6%&Ql)pFZQU-A_oulJ$*AJ ze~Dw9#ZfcZ(^4qmt0Vtt_hlJfD|UWLy2XU-|3T_D zTg+#Dkqs5%@myD5>6?#anNH7G55xpU>GhpmBg>^cN+owhtki6UmJ2CJp)el}b%#3< zSGdxb-3~2XYA56Dpu)07w>#$V76!#5gRQP5kh3%kovzbY^JLtqMAd_I6+-Ev!DIEO z`bO5-v&!XO$M12r%N5OLOIuvCO_}he<0(_`W1^;yd0 zj>CRr$TY>YIhuCPIBCL{jz-=qsyF-a$a_^DEho?Zc-_B|$2@%Gonb7UVRTFdr+-PK zgS=n#(4(~~221eKznQ1)y%0*?!sv~iZjiTV5rffT zHbgr^u{}h+z8|fR*o>_n8rnTU$AA+IE^g?mI@Kd4#peiB3S8Ui4|%fzQ%hu2!58v(gD;wCTH5cJV5#ACIbnUnSrQgaL zd%C8hnJlyJP_*q1CIT|)mA|+q>Kr-cXa5%Rza-E1(8pLqDD;Gfyp=QO9&=|K%V$ys ze6(W|tqY%3y)QUyu>xN}86jNuYQm9JSf|z>8Dfez+#7_&QKYSPqMBUNj?^FZ6wr{& zShGVe+?7zSriMH!g{nDkgX{59BJB-iMZZ%_HsoL%%>=_yt9#%z#Z6U*zf}ntjnTSU z?@g9#>0rNM(1u3VU~7a5hLQ_C7csp(vO+Smd+BFTNS;Sya`*lJ=?4l24jn)E(}P!U z{CMN4^&hPd(XQ)0{R3Js`(`=4u0Ubc*q>d&L-v%lIq*POg`?f~XE^K$MJG}oN8lK|#8pR>Q(Ki8aONC5Ll z|ID$%f9`3`H79Lz2e7a6s%uVK=MG?wrPW#PfQ1AwPd4H?&K%7WfO+l!_PGz3<_=(= z`+#xo0QR{L7)Stf98uff_xfv2>gNt%pPOK7?f~|=367Eg<``Q0`!(vCle)PB*ykpA z-rND~a}yjP0nBmqalc2wbLS3VpPBGEa|f``ec+P01K8(2@N5#m96K)dyAM2T?f~|= z37$E30Q=kon0nD+kHP6b>x!1t1IeFpS0nD+Kb?yN0{{O=VZa9F>_@9T)>R*J;-0RU9 z?PKVybrhYLzY?8%SI`OeYbgqyHopL!8ovac{$`f%SoWin)0xG47Nv#z76uCkkc(gF zk9H!mam95j%hUl1`=QRVeq=Z7`S7}FEN<$}o1;2c>v~6CF&5E#^~Frk6Jd#!SFBqvZ$+X|hTANPZJIHQ{ z?V!L9nPI{uXVkc}nl6ZypxEvzTtu+AOAW@SWVyHx)hlZ!f-}{-cN^z!y-wDP zq?u}c=OAHaw_IvMNLty511)G3RYS9ESDU$_IBW;hfTvdOcB*0|8!)1JTiOY3rh08> zzRzB%qO?)sVJ;TPZKd2bTfl4$MiL%xGbpNDDP!))jchF9t$K^SVZ*`nij9P8T5n*c zdiSnN?p|+7ub>=+qcmC@u4gJuCrV5hb_rQ$zpQs!{ljR0iJ00(cTHbtqCI&+v(Inw z_&zta6YNa&TF!jETDO>BU7@4iq^5eda)UGl}m|eBF zq^}rK{U)lnp`Gy0RImBW_41`H*4C`@4QIxhLb3f+l^H)IkPmWNU zJx4X^i!0$o&TF^Sn)QAv;c^S%2&y-yo$$<5?^*aNiZZeHEY?QU8tQpa-tbfmW!tjH5{%$4=<8*p|LC+{_e}Mkg|9d+l0%8CYe#!&@ufX>G%=uSG;z@HA}s=v+z}d?==*UwUikunWH}Y zQ0+P$22CBXhB(s{ScXSS^bJ#B7-qOYGvbQyW~q$tb3{AgoT=Wk@YPYhBO6O(4K|m> zJ5Z1a;?a>Ic(fJH1Pi51Qntj)NNB~K3w0}oBp0cuiN4=sMfC=>6ONhcJqurD%zfc# ztE3H0%^^zQ8xN)XTce|`Y)=@PV>!PdrCd(0F&Oms{GDRE6e%VeZOm70?ZmN}>OBiz zHGB!7iPV1#F3Ijo*W{Kn$;d~WIh2RdtX0}wue}k8G=v}$uW)CL9yQo@u(*9+sGWG> zO!c0Hue@#}pFSE=U0kPI6f^ZfMYA*nG*q3|Cfm-0T1YH5S5#4&;(`~+Ta~$vIfm=K zTsv|3O!c0HugZ0mwe|z`qP@cOx*mVI;kV1eC>D2E29~s#?3T@8XQZ97j#^vhfH;Vw zB&3+1joOJ9%vA4L_{vb}qQtl+?r5oDj&M%tXqz{fHDf7P)O)STkbUGxC#+GUvoLC# zy{23z*K!`MqI#d9op}CC^`3>VvW1FfHfgCsq!jeTvZ~i5jC?}TV+_n^wvN5uCWsC3Yy}r7u9ZU!gB|XTsohW%S z+O}#*v>M62zvglD<1Nh@2xGoFUpsN>O!c0HuUef>Gw81eMkZq@5$9~$AniA5N2PW@ z9%uQc80niE9>XY@Xm44$xc6w)8Dj8!18OJiv(#%n3tv^Y`VA}kLfHKO?7exsBWqnJ zd``~V=bY}oE+DSZ*NdWUZdWC#q{4QoEw!cg)C!lFR4SFFlFGh;BG@Pj0@oRN9ozt& z8DGT>a2e*67nDIzM`xIERK{&w5EK*<1)Y!jK1rRXyK_3}-Rj* z@B6Iv)bGm$DCT-*U4pCrFcCshr{15`2Fe)m@JUILM)^{wQfrNCetN#1+#B9RH}bn; zysAtdt}>l=S<8(|BIohl={VO1OIOjOM%jVbRRV8t!^CPGLn838Swh_I5cvJX-wkhO zHuAe-yuvbI$aks!jfWfg<|Nx7ZdB8WhsRJx>`k3nvP{_BCQHE&&oXT<6J{%d=GRNW z?h(;N9+Flz4uW9|&1h!a!<3)xg?4%#^2ne1h{;2B!*rZ(_=7 z8Sv^8A0OUCHuAe-yn-8LqHa_YPMsq=bj}Su9d1e)>ESS;8ME=Mf|o{>0VGkCu%2O{ zVH@Fl9`O6o;my=WepifFjS*$sm?0orcoz!%zA|vMar@y>&ewAk2T7%57E32a&2m}d z`(aK&xUQ0#`~A@HW^yCHE5<8PObiQ98AqCV7yK~E)F>r-4VcbY3LoZbGQogk`c)ux z$c6aB$+FT-HNp7&iQgaIOl;(L#dziR2MFvv+{Dk_@aBz8{30vHjT@PP04Fpq2NtY2)oWF| zNynD#MBAVm4-YF3+e|=BRUs#2I4iH=ZLQk1HQ@IbPcD7u4znxofL;81ci*r(0lWGi z1X<6lJ!tP3?CyUi_S%C7_CEwN!#x9Jg8Pkw9|!sLegovsdj-g}_Y`nSAjfJT`yB($ z4L-H^snchGlY`fu)j$TnAH4hxm!1aB4gL$rboUJ)vmFG^4}K41u6r%WRQEh^j_@&% ziS7-D6L603o%@}AX8&m*lib%Hpdh;+v@6G+7kdiGCHPKo9?`inKWF%KiRNViIs3jA zVVD%`I-UlnT(~} zu-aMLeH^}XVb_v+JTBL~LA}Z|WQz)fCA)*~+0gF0H?;e%4eegPu)BXfer`j%cSU}y zGghs)ag3&W&Pc(A>{5OAzAdsV1Vr9Zh*p&xDt&G;S#H0h!wu~YHniK{&~9%-yV!U#M!8Jd2H* zfu>t8`^zzL%WY` zX!oZZ+WpCfb|2o*?n4{e{qcr&AB^noJHNeVL%Ual^~}fqvDPS`jn9qR<-X(d=WJ;A zz=n2LH?+GF+1+=(JYTQfmj|!9e7y69&&D|1DpOS7`_|)IH?(`@hVA!;4eh>fL%Z{J z&V70HxmTE-XWXj$y8lLg@4L>yHne-*hV}UV4ej2vq1_udv^(D~+?V&S2WjMgW?j2I z@wd;;7C1SEcBq|q@9e(n=A31sZ$t#ZbkN@?Faq`fm zkH>xn?Cqbt_lL1>iWOoH?tX0VE#PcH1bg;ZUwXme2{=#qcd@!d)|D!Y4=Sjb)@XZ2Ef7 zkotW$tW9Sbhhw>JNR)w&6xXU0q{@)cOLRJtQf-5HRa6hV55=LJ;%4Gu3Fw^n zOaifNNez;e5{mE`H@m}hK2JCm_w(KyA-1T;qY+{(msvE$8bA+mHt|YRiB3*}!lao^ zOf9j1;AD?>_;98M*yn~@BidUdT3aK62r-~)6-v$kfzw*6R!MpWWpRB?AH!KELyWy# z%V#I?yxzq0rxX{6aVZftcpC&6)nf}E zk&Jk0C}G`(!_C+v})uC)zBtOw*Ev1>)IT z`*_xsH|4i9 zscGfB=F#P?5ocQ?P9p?VoI-gN8Y2eO>y|oJ$BYZL&UA<)vn-_9twu2`1k{N0Qv2s) z;ueS~g5@m`&svnK)VFm<9wqB6gAy~$ZZK@lqUCb2&Ufu9lJLrE*Fr@zDY3Ed*&6ZP zTO+>f9Fb}VqiNSLo3*e#fbFbV?ziyyrLcTj!q8d~%Ow+)M7QDZKDIUDwOb=z6Crvf zv8I*{GhW`LVy*8u6*E5uc0@jRKjWo1-b+@V&CBA^~aoNYNVyjgpjun^hW- z1t4S{sQu%w-x~3C5u)nk<&j89Q0NxeoH4EPb%)o%RiXkVmWLz{IleJy5_p+C+T9wl zvn}HAAGSvPeS~m(xIGGr69%i%u!15=Cv00phAXvbq^Wfo1tdr*F?gfr?|shJhzGVt zT#XRr+(?pODdF=7V0=jim(pfk!A3?PEt4ap(v1{a%&4QO;_v<(I9Yh%XKrqJ;z@V6 zz{&0>chbB1&hb|t{pX{{j`*X~!~cHx*r9Ro$%D7=zhytMcW~hEe|Yb&_I_w@xc9vM z;gxq^J-YhFtN4{~20DJ1FTekC|MKUb{d(-N-M7W|V$a>xWA8nC<5}_S>ENCGt4^iU zr=0xS$?H#^b?LV*ecvUZZgc$m$KQ8c*?rm0Z{J-C5aVa>=*z$R4E+DmH-82mvh^cg zc%Xdcqp@p&?==SQIGB#5khR8?KV5pgb)NYxO1E+EFCNn9^39r)x8VoMBbPUS{Ke#P zCqKUTyE|vMs`bnR<;E6|`*h0A{>e|OprX{~B{{EXv51M1SS*5!&UZ9ODv?5vleeb9 zBey6`l+ftpEzdqsl3T#;Ofq=#6SrD`;&jfLbRC&@yn)xMt9KL+r0<5#<8?<1Jdiq2 zu3ZH!a6jn4>ko>H6npl`+4i8q$?5hW{N!Z+%{x!N73bUkz13LvEA%}w6P{eY#l8)} z_h8@Atr!a`-YnZ3$eHI?a0kzE_ipbf#=F0N!0a6E?A!ux=>MD`S8nym4Pk5hB(i*q z@?;-0?o)3!@P&Wf>L}ZJ|8z%5-g(U}>L^!l1rehQuFVbw-0B-DO-SCo8}UxY$6kD^ zH8upVYYq6q9opFRR)^Z~@wYov>}9v;wITE(y<#uF#gGjj5iuk-dgMUK&WDgqVe{0W zbs#xVECuyaKlT#pK>4b#+8n=n{x@k+OJpd zAJ)+ez}Wfx&9UpEnp~!MFFT(mHh=i-m=4a_&=UF}Eg&1X)22L>+T=92ca?4<_g{L> zhE1Ut*|7hzb2e=Hh=>jQUw6)ijbV4QVgDP>*{~`8o^06v#`8|z!j%qPV(X;}HRN%t zDvj^mba!_0!>>Q@EvCzyJU8ofqz04X>VFnO%7Ryyr*H{`(nm`kSZS)7_Kr zIXS!Zrc2K}{>9_!(XSmfkM<7#)!~hU?>$KE|MLC|fUde9`@7h8#GbzUu3c#7J>W4* zKi*^5p$C=?I}aJ@PFSclCf#&qLM1B=dxjV5(|T2Cjg(>uDZ(&W&*ejd!u4xCvo#vg z;QP9qP%%bsRc_w9*WQkT|Jd_@pCs^;YYXyBPP!?qn($}kbj=b?qMj~^NV(UU87$$} zr)4>>1QT>)KXpusmZVF-oOWQtlnbi~Z4i?AgHIoHp%oVr&~JL()?fMK6@~!6=xt zI#wx8NwtnRXbx+$o~XG^4BFV=owRut@H3~46;g1us1B26&&OxUf>lDmrH~|4V5gO| zsC2Cfkunl#S*?T{`&pvRGv{Ffw3#6ZInk(b)nu;abb@jX*V3?zB8?7XO^XGnh4^}- zqczap=2coxo8|dDqD^*Q9#C~&?Gwguto6#pc)QW*g74m&odh{8PB63OVo43{a%d($ z74^+SdW$wF@G++iE*HBqi*MCbbnLZd4u*_QKiRDhYEoF3_DHSM3agz_kWA^=Q6ejO;G@|%-rU&F z5^a!qm;i0S4Wk4`+D%wv>S4S-A;)aC?$sL;XP6A~ZG999HM5dR;l8`EtM#;5o{J*d zq~_%TRcG0r;!Rl`S82C8YhwALh+{KvNE%qTuc5UGNb~KKay_SlZCsvPv`GRVbK3Y_ zx-a6cp3r5dQO(IsE0jiB5b(6m}?2nxHQgq<7*|27kQ1gWIANafXtM*`Dl{> z{^qpltA#Gvqw_6QMXK@Ckg%uJge0VnFe zHaU!h6OF3bE@~oYcY`vCHnsk+4|1LGR%3*dn%CIe&k}8*d6)og;;1<_vdu203>~qL z5RBi2oIV<#mU}&HICZ*wK*Zy$K!TpIX%nue&GH%}qRowYc|a3qIaCD#Ml$VGO_3g9 zlWcm}oHUIdh8sPtL*#0@q}1zXtG9VbZ_(yq;A1gK38Y#vL)^<^4L{x+QB?&>p;{=r(Sonyo6N7=+YHd!NIPf>84L;J^Zm1D%ZJ6Q;s0m~~ zhyfpxr68BEAco?5;G1=2oJ>jR#{TZ4&DQ`wbJ~!lT3Ry+Io?$~HOEz38OP0xO8m6c zQwm7Fm~SagZ2}^!*cJ+=NtM#;5-pE9>`RaLjK-CEbHg%(JqRmWEF8ZNCOKFECk~I%Sd^*t*C~exz zU=vgyZ(N>RwD~IFV@?}o$_4q1&e$E0^~V?44y`L5GRe}I#?7+%Q4{ksnlUxO_vzOL za+9DroNsk?iwo6W+uocu&j9}Bv_T64@Z{>C&Xn6zS}MBvuqma?DW8uEZI8;K$vVVR zReCh?H}-cYZN3utnbSsf++mw=TlG4)+t-%Vc)8A4Y$hae)@rsB?}JG^KA7ekbgEoRlD!(&$8<^oiB{s|*b+j}^EPe5^|V=j zI~LLA>GSe{s&hjjO9JV{Ny%t}oB}~Bg^+k90eAW=o59+$ke`NGp&W|o&C7F(HV*+G zbJ~pQflHXx(Wqg?oiR{iv-B|pHU((~Bh;WMSN&;9s#a%H(Z7GL@bfn=_gdS0Iq)~9 zjb_=1=Ve==3lDLkGO;EV9^6V3t5Us8+5HR+L1iR~^c&&k{_dpBgTT*XZG)*)58=m? zUKJt|mF#qoYnx;W#r0HKZMVc~N2Y6iIz`vejq9~Un``qh0orVl8GU0{>uGc6`;_@U zJEG0g=H&rR{25LEejd_@Hembs)bmPjk&XTS>AlaLtgZBg+yV1S&-ed(JAc1(${sbr zfBuF2ysG{>>DW_VdhP%E(%px)$jyHLfV}9za5xC+-dxyb&>MO{(gFOU7*>lwQms;q z0(CAFb^iBzh@WJk^u5GSw#XPd*S^^%XXe6wi=?m%`z_LGFSJhX)Lp+InhpxQg&Oo- z4f)`?65XBpKr2P!8X`0ey`m~M^rEkR*h)htkDn}-i% zGX!GQK&R-=S9W)*u@`}CAdojIE~Nfo1ZY;Ll28^-CO@tHey7ga7HQyb>ntwb6o4{3 z|4~q)e+0eOyWjFem5YP{o8ELQjV4gOmC9^u^!8Q?xA&};Ne%skH=G8!!f;sV1%hN{ z*|rumr+D*5NSJ5h+BYH-T$m>a1x=~LB{qBt^P0sY* zBxg4uRvp13j}~%?Ouo>|)(2wVgL6(lORmuS0}VZ=?Fyy%JA2QkC+>M{?$FPD9D3-$ z;a!oQ_w1zze%f1q-XmW0;GHFT{QWpP_jGqZo;!+UdAxRIF@~-b2YcL%y%%t)zEPnb z{JKSF^MF9T>-D2cJoDy$Xqz4!32BM9gtWGRZZg*l!lu_Q)+?Y`NGh?2WX~(LieV45 zUcK#u-zTyV^3O4sY?0gf_AqznIJxcQa_<;|Hu*;wE|=e0&zXE@r?{KdpHU&Ld%3>% z6a|JfP^{>0_nxAlH6nLVLNW~nd@b1PW(GAqJE*iebqu5rD`6=yL)Rdv-%KG+GtEDK zAuT@SuIr;VpR3A4B2Wv}>y@6aCZK$1&M*ZUrua!GXZ6~xS%>oAIy&;kExR@W$!do^ zm@BDbo$g=fXUP;e2*roF9-?7M9E|PrX?wkpR`$H=_Yl(BhPa@2e@pUp!h9(SzxLYh zLtAimlkRIUjEC z>_z`LQ_C#@$&~`|%Jsf)o0UnUMK;BP4slZ7&sX?@0xl8&HQ|=*_B)yDEPSIM{+}!m zQ8bhJw9NgT1K$=3DYr+?&)Nmz=F{z*-gUoS8Vq}0q{4kCu~#n8m>c|9z3%jSx||Uj z{1|5kxpA^QX_P8MkW-n`sy1O+i=FIRaN)3~!j&i9ERn0@`ER!K8{8aA^IJc>$ z1ys(sqb4g-;8iquaf+p8EFU;G>M-XTGom`|@nIKRrPK*}dv-p}-(%R?hBaW=IsrR= zF7}R{{TJ*%yuY*e{=FaA8|^WBUljXD>>XF%fAt5hj;=CSzv#+GuDs*QYp;~Aq^=xZ z{*B8&bos@X#mi4S`}o79@#PX_7% zZ@=`aOE0_xU5Xw550DFRcAP){GN2~-b4UOBsCtARogDto;g27_?67e76$hU>_{D?o zK4>32@8EOyKe+!h``;S-_E;sBjveoQVE4_tU%xBwKDhIVo%bHU_4s>_@1ZuZ2fh?} z;^$vh3&i&xjotgZ3FN|e6S4n$o%M1)&)5&HvA&SyJofL`SuZcuV*hTP_3|Z;*uPz8 zz5H%J_5kD!Ji*?q^m5hCCo%IEl{MkC| z3vvJGI_nE@|I>BW7vlaW>#Q%t{ljbGzFf(N);u|NA?_bqXMG{=f4t86Lfk*N&iX>! z|8Sl4g}DFzI_u>oiT$s2))(UbyX&kk#Qk^HSzn0z|6FH%A@0AqChp6{esIl`lNaLt z>+7s9#QoRSSzn0ze_LmLA@1+r%KE>ov%bKRUs-2;fhE7Z&U(4IW52l0`T|Sdw|N`u zFZ8l^u8GZZ2Y%X`C&PEg2F$AOSZ95q-~8-4>kF~@kL#>2wDx~kXMLfy-@eZJLTmr@ zI_u>IjQz|y>kF~@sWsNnh4%i*b=DX9!dur_-yI#$&whL>>mOTZeIb@VvCjHJKYQC& z)^FL$`bXDUU*O4)th2rl`8Th&M$ik4{o!@i7fSL&^Y{OszwAabL?WFgtghEsx^s2!6+V(#Zlg`@uGM-qk}J2Yk%iZ1d|0pZOCIO%spcR4#S;*m zg&`w`mph?o7!Eg(S5mPTIFy8GGIwTe`0T5jU6s<60MM#8{s!vL?JdsO@Oj;1ysa3^W z9&I+YyEby@hDBLQNR&d|YB!0}=Fxyb$NsX59UFlYNVeH7H~ghXtt!;=w703V%>ofB#@e~TDqCV zMp~E13OQO#O&fuU0Ii@>xg)G7-i0&(o7!EQxD|2Ai8-oL!s*=9?c*CqBb^1&_~P5u z@T)bo3o=_8e4e50$@D@CPA$pai(RlMc(&Zc(PG@YC^t2x~7 z3<;3nWTI|Psx%6!@kO_*vBjrltBQB`LJV42-DSJ`e;T%$ROt+;#uwhM#ulGCt*XZI znr~CP`+udirW$Ebji=nM#ulI6tSa8}Vsulx`+qvKrWy#S#uwbK#ulG~tSa7yE2;Fd z-Tgl;SyPSqO6tkCtFgtW39G8H;SwahY+ju5PcY#)eCf3oilge-(U9HDFMU&%0fXEiPlPsK$lc)tlPg{{qPB zY9Q!Ksh5B5q8eW~-y&^sg?ZEBt+KoO5FwGhe>I}(jQ8Fyfr@<1%RgsPkuLxO@Y`Ii zTUEdf=kW`1yMIL%_qYIm9=Oe)EiU}5D(`ZGZfbY`@~+`ecE10AWcSrON6!V{_P_V= z+YZ&kFF5$EgEt;j53cWjZ2u?sXZw%r?_d2-SHJbDboKME{N|PKz2aVZ#^sM*e#_E4G6hrfRB-~a#bx9H93 ze(cd}v2VY3@YB#TsZ~uR^t@xMI(l=mfIhYgx;!UKhH2xHW;zslbG(4Qb`^Aa5vJk= zLI4S13kG^~w1B>56?A#wL*#T_&zT%aqBn;N=&M&jm!~bB6bpo@5fX>q94w&UwhFpD zaq|*EXnCQ*^0{W^|(B(a)irc)EGZ{=qZ}t|@SFVCCPh_}?n?z1<^1{<^cK2hM zYq4*AUa8Wmx0kjzw7>~0uM!eylg`4$x7^zWl3k8}ffiW~=kk(@-V7JeSFD0AFZ2tP z%!z!S;x+UpyMUHgKralDj=_@!Qk3%m$}FIzRnTRxd;u33Q8Y;my-6>iFI)v(W?ey0 za7LwYNMSQ%$5P*-vRU3EZ*+|9Z~ zR#$9Eb{LWbos(EVtyNx^xz5>ysbd%eYJ227>d*0;;WoE_2vYEI!8-Y=S^wHMHDUg0&nOn+0<7~SCw28>~?1@xOXg<5&DVAGDF(df-! z0e$%@=yD4)vYgX7T+kHsW^)0(xeB`6!UB`WMV*(J0(!HtfPUjD=yEer45!n4L9+m~ zzJR`L6?B=yCU3AbD;P51aBTs7=?W-%_lgUGOid63kzrI&SAPNhhE>pI4igL~tF$g- zpubiZ(3h-&E{})?%}F3X4r73M*jqrqeid|?5~Ku1Q->35&~qva=+~`+F2~i%=N*o= zSq3zdyMVrU74*W0=^tpY*KKX28$+&2VUg`w7v?u%qBr&2~Kfv8n8)PKx?a@%WUE>8simC;z3sz z7f^o{beTj~zYh=+fb@AO73J{vmdF z_29P;e)wQ|svXd0UwrVS{Xe+U-T#UGm+TAsUwr!Vy$|pG^xn(&w7qM)KYukF`}5e( z#$LG_-+ABFU%mQ0JO6vP82if8pFT#eJnzbt%fAJ_DVSW&0WE>wKl_QZmz)W)hd8YS zUZTPMn!nLLe<3V!8^buthz(#nT{V3;%hxlSJ>f{8rB`6Gd8$KVyOHn;XT&v34Ed_Y zrs|%Y4zbZNEw-~Atqepz%AyaUi9mAOd>M-UROCZ-McN`DSMHCysS-cXLBjZE)|X{| zEY!-lHpu(Z$ca~n*rjie5Qb_y5=d85&L&C&rk|%fMJ3fWtbsRzB3cj=uoC)xuooaeVy#p)E{{rN$fUPpCFLF+IhEV=wS{DUAQwR!RH_H0E zd{mFklY||=Eov)a0KU~u&U#(C*+|q2cBQW8YEHulrWv#ja*yCyrVQmvWXeALwg|yx zDy8@o(UD9(@C(w^Whe!wgL1r9YYzF!7@-B4Qzq!hk45r49Gl5h(@;VeoYF+Lgp>mE z<5ZvltOpYV%lq}JTWkB3zKc-@a4$Orq7R5Ji|Z)ebyUrc^j0 z@jl9jQ&^VUVF*nT*hObe4@Emuej zld&7ec%jcFg|^n!*^F43^oK&lAJ#hT@zWwiFF%}?8<5C!6tX6_Dkg_#oP^{d2tsm_ zrQ2!GC`nicQx3j0LUiSvRhNxaT58I2SSy37OyvsOvHe;&8RFe6S%|AG!VEsk(E|TbTT=R*AYdh_10+UcHn`U9c5aK#*QX~86C%tJ`f>%F5bmv zm`;yC78Fb9)YQ(nCu!6W8RgS$v}UBJwA3;t!g&895yBG)z0u8vsArA3o?6B+9P?Y; zfX^mKDN%x}?L1O#LSChR^zH~z@y6l6@cCZOZzc6=K2Fl=Rom_95I(bmEB_sA(@_WUIY!>_RkGqQ)&8nVRLcqZ3lfKls_mhh4J=R90%G zb12dh>QZ82o4h>?vt(1!TX9cGxwtlJ!qe1oGC~+go^Od@>!qVeA7sHpG;1(oXT6Gm zI&etyfzJR#>gU-+-;yFsSV@m26n(0|-YEC&ct)WLh z7a@RpTb__DYgUL0mG(d@FhQb^@^m*}EacI4L9e8kRA)ws_QCrjgghDQgYihHXi9vh zRwW|m8GJ7Vk!?603JD0J`=y!dn5}T{e7q`=2$va7YpKR`q#>?uX|!t+nQ6!lcs~WQ zvJEK(a;-5M?)=iZ50INSWLxbx#)g$~pdnelr#eCp>(VJl*0@O zw~9;|FviIfEmPrUnk$!pdY2!|MTk5P4ML>SY7(YbH%3)>;!kFUimndnO4sHX%nXP; zJ56PwLoPz_9!g=OVvlM~iws$_F(>ZZ39sJHG(FR8;#RlW4J@XX8t+AdS1g|xrkt7X zx-t~cB`4kCM1U$iT9I|WO_s6Zv|v?zg;MG8;HRKHZ}%afv&QEWy;P>*WGhBkMnio- zfm?5s+2$m>Qe^toR8udgUAq}gI4oP`l-#I4N~J11pOp$+5=TvS)a9pqwLBcKVKz7N zYK1}9-ua8D8cZpPTXrQSBW50UCm90DVFRH{LbS$K+OuvwF8g)Mqz2I87oQ_YES}@b z&1?xT`2B9G6>9uYA3+cpR?^)LB&bw%T0_!x=;(tHf;PZfDTiqy+(v5MPOTW9nS&Nj z6!;O)F!n{>V!3IJmS|xgiV)yVq@BiH4My3469k2d=I6j6U%8X(aCrlKH>*q$G-wXR z@$qw_rXssdA}9{gc$2W|DR?Yv-8|Bk3^@T0TqD_2y>h0}WxO=ny7K-1MDmqvk8c@e zG|$EJEeK6Q=o=e%(#VU&k^8t-=+vms5v4osFLlOz+?&`02Dr;$l!eV(+@(`IETbJWHQ9$afa8W9HIB1k#HJ~Z(PSnmKi58;1sl5salR%&E}{X!}=Ya z#np}=w4j>ZK~iX6fHYp0kv5Ull71@oj0n+mtb~l!+$znhRFZF%e8lss24TRVUk-Z% z1srz!d^ke;d$kDBfCH__U;}(K!U8QN2?0yhsG;q!o+ZlsurmfL&q_B}RraF6imTOB zJ)3P*yIE7j2l)xT z7I?IpdvsKfO1ILO2y|5-#WtereThibCcGg+1}*k7Y+?{fd^#b7U=wvM3P+`YPwIlo zXDUIr(0j6Sps24XLL02D8=T@fQ61ss9eH8Pdupeo#>n`3iKC|Us0Y62Btz6$p-C@<;p zu;TCaqHt*bXxy0vISZp9Z>UuJ-Eq-xjiOVg?B3@e#o87z5eRysNR`W=4fJ-b(b zGj!exhq;}TI2LgG42?sf901KK7y_*$yPJtHnc%J zlb&RNN^#q!5;X0IU6AA)<*6}RX4PR^x$;91qS0q38AhS7nFwbm23*49U2q_f>er^J z>d0zJsG|jyLefC5_)$G7vss21acpm9vwn+ZS-PYZoCJ(FGjSeGf?L;6k(XQt#*Tk1 zLb$!8D#IN*;RLQJcX*zkaU&ZXHtNv03nblBgiHp!NsZa#?}!lPQfpxM&{FY`bvm{9Z7ucucN?ZmB zv%R_ogC!2N^X3TQR8SajOn8;jT2a0^w4m?AmgY zg}6y+zJX`Zc%!T6HL}(nfsFhKzg=}$r^8HU`7+dPDTmLAd{}a$K1_YY{>pc8fqr z080q9lrrLnjc(Gb_(X%FoMsOJx&nfZ)sT2avk#wlju^DX@l>Yrgd&VOnfi4u2->y} zlFYOu^G0ZmoMFSN-T>L_PWB^2p%X`w8QZq}24#+!aze=xrfYP3I4-(4F)U)RGb+-P z+BlXuN91Sf2xTC!6;Jn)XeCZdNqSW6N?vZ7$yaJ@(zRF!1LydMk%Dte=@g0v(00d1 z`Kpe)g`$Vq%|x$|PBbd;3{3-Rcf03J95LJ*MLuMdWoBMRR+j45~{g8YT0N7Y_%*r>m)-XY-WptRv(io79v3758T+J zk&pbqta4Katc0eGz9}g2X%Q@=2w`IP1*(HLn{Y!?1y*)ahtY9Ci%%uBKp5xZZWvFt za^3dS#o|?`Rq<*>){(g)1))+}&LzYBM&yH=@d#n(i;ApqwYZ$?OFY6Ta7WCLN?rx0 zGy|uj!j#=%_y0ITFw>60C5%D2TmUD7ecK2M>1<_E2M0_R%#&V?cHw5BAr_$BSDho0 z`Eoi_NQS9~&>)g-U2!LU9%O;;AU%gFaUxyn_6kgSfFAwV#lYUm@u`aAm;G{Q)~^w% zP6}2HcuYBplj`;ivq-sCTN!z%uk(=$N^IxFak*a!YPA9G;Z!Th*SS(b(QlyjF_O-! zK{N1hPe`3Sc#eRz?tq7CTw$V*vu!_Ja}7D4bCV{o49$>X&?cVYvWDRvM9YO1DJDv! zMw<2U$i;d=I-v}jxtYdWc$sL+ifM6%u7GJPq%OrGA804Z5L9>4Q(8qA<-4@t3~U$j zG2hOWcpB;&+CC;BUI`u_RuJ^ z0s>RwB$G=jb+0=F2YFzLHPt+G)**e?J(kaXBzYkrcRU;vRuE7YhlZ@uswXK1X9dv9 z-JT3jdl@alVuRqe7|!I(aJ9$Glm->kl%j;_HP1dVbm*j5sV%=bIC2rsYD`sC1=r zr#~eFFYT%`UQc3`jBFFWg6;mlk&m9s2F<=R0=YSToT>MU*-RDfn*P`ez(O2HMvBZd zxh~^`hyNi$G%A=&cFktfP^C$tjVTJj7Di+-EBF&T-!t_INOetT`LTcep$Ji@hZ%~8 z#B{t(IWiRlPO0d%dv@rIOurFexd0!Vn$q;JqyHyD)bMPOwFd&)<^{TILt!DC>2nh15Zz znA&~TIU=cfBp_;oaeM5fR6#8xCG~qKH9#bmEH$alz^IHH;~Bg2KO#gWje##H`D)gR zb3~0vz+6u?>UGjhYK)IIjCw|IHY)?N9v**Rgm7C3P@_-EU4iKkO26M_G}F*6q==NO z?y$<2S{aIMvXmkmpD%sfX0r$mX?j@(Bg=>_b~&t*%@t?77!re`p z!9jX;I!20EzgkCQ(IG})4@Rmm8x9F&9Pa>1fcb4aDNx<8)=zUKIN=FK(DkuIJaw3e zeArcm5R5FMcNm3MxD&MckaOk8J0T#09F{R2KnDS#UjSyPDUaVyEtd#H7y0kp* z(?yu9RFMKi<0&H)3!c#(2<;r4Iv1P|G_sNGWo@$#)nKd9F5@#SX@GCq$_+Bp$I7#I z3dgF2ndI29!$@(y#Um<$l?z1~P7&>9ajXZmOsk(}Q$ZtWT6BAUQh|wjkkrrMMX_3} znCdrDR1UHgr`{U-tgM@1>4FYxk1G|l°#k16Y-3 zK*azpm*v*D-{`k9nl|u27TvVQp8mjG3R=$&Lx5nRUx3mvnPzc4Su6N!CVWtRWTL zj(hospSp*|9ty&s=T%DX;Et=93tipIFNx}1tRaO-hs8iv2#(g-bQ-G!{1A1*B$q1Ucv?BT8I@YC zcWH~Eic`BcZuQYrM-gnPj6&4F^Jdv$)~nVR>AFZ3w868QDDe#!nfBnpfv6c{+GUX1I?w8R5%Nvy@GASzq zp;my=6a=SE&m-6`OYw0BcE_+EugA?4o=Iw%s@7`R6xcnocqcU4rd%Lk?DT7*wi2>_ zF-h5$tzgw`HjzoNNH7qn9>WcEZ;7Rd%! zfCV@#F?CPO7N%uXOrflq#4i8+ztH`EG_Y^8|9?-^wp;A~e;`6^vHveE5UnTn|4;1y zpV1+O ziT(ebCsWJ+j#%YC!%v@%sJq`YGN01v&>O7cS}LB6x2)e!r%GqNRwvq%Xt~2@+=iHl2f}~@C6>oKi+y&v+g!;{X061 zzV(0SI)cY5CF(-LG06~GIkZI4$4DhsFQLWQ+bzjm&>(yx^gNE#IC z!LH188~yrh$nkWxFiN#xqAaweVBpATP;JPSvJ27-hC%bj<5z*IvT{%z%Z(hymg*I7 ztF%!{OoHJkl`M$U953`b6wDMTybV4$DQ8L`|A0iQ^%izL;ZDR!ePn`*a44yo7DFcM z;+U9!^@9Ncmj#E~b4eL*E|B)32TQ)Uq|rF9IuJworqS!v`Ec{f>Fze2_Wp^8HXUxD z|I^{-(OVSxbO(9P* z=RQ4ZhbnGG@Y22}FHPuPUq~zoHl&G}v4hG*y3Ca*t z6?zIj1E+dU&>&ddEQ?BlYFYU8v;>mGa-&t!MN2(zUUks(US5BlGVgicQrz8!uAi}< z2WKz#ynFPe`LC8H*GSiS&a{7q-m!2ABxn2eaG^|_TFkpZ^KB5$g@;#(W?r0Z@9Y#X zsLOpi#oW!~C3_}|AOL<(vWKl!-RvshCiHc@o8lUIZQ4a}5>-93#Tl$rEf3nXk*K$m z=;J1P8C<3xPiX3Tle&RU$2ugZ$?GYiKqDETASg{|$p%udXRLwevPQb=rE;TMrA8p` zsEtl>%}O>f(=#+bX=VBxX93MuB_PKeO*8YV&!qy7Unc{yKl0i~b|2az@!b6reZA&nY^OQ4MGagFQkloI+`W(xY0>S%_P}e; z7pP!O>b1(CSJvvCc`w{*)1 zxoX&LUe9BRX1X#?R|Kr;c@>@Utoi^l$U+O;H@ja?Ua7d}%jk8TQ?6&L5{Fk2q)@+Z z&`<$R6F1~0__hpa-{!dib|bB(yjn<3Z83$6^eLjaNj-a`Ow6W8mZ^6LlWs`Hq%8%B zwnc0q39U6TT5Zixu}OAVUyz5nZVE>Qo^ym;$ERq#oAJ0|VTO$AL&k1jPcmb{AyF@1 z>NVgoun_c~dtFOMQU;UCRe{ki8u5_l|%sd@TkCx3ME=9B)(Gf#Fez4y{%m#j-qJ^tYFj~ow++lVem)QrH)+^LHT!UIBABOu!Tqi#68*moxO_bin}WDJ7bYHP!-`!_RRA z9S2F)BrR573tSFAlM`57EnplLtE~ktcZ`Cf;#!WB4Latp1ukdiVZ|J+<#3VLW7W05 z<-9y1l@|!y6b&WjtpzRH@d7P!ot zyv2(eC6N>x)7Ju*d4pl#Gj)(Lgw4kaYk|wWG4ix5=LuaWW7=Baax_>>maqa($Rfq{|fy;!mfox5Vv+`iIkzWhEKsYj| zn3_&dLX2MvTqYdPbCLtdDmXE2EpVA|I(TP6b2)=yW9(YsGU04mXH-$isd9{23tW!B zPN^(dbKrs=qt^nL3FlZAr81hXim}`}!Y$)C4J59`a*{*a>-=3%CS*WbRR=REjEYff zfy;!uofkbut_3dh=5}iH7`_&`%$wVZ&12YF;4*J+=V6Z#Yk|wWxt-KJ_Q+b`GH-4t zQjb0F1nd(p@6>lLzy36JvU~h1`>~_%I;_Qh^#IxZ?koTK%m;t^%>29z-oNR_1QMIr ziOI-LN=Rh575d-r1#K@@n<0`dr*s>%uiCaI*d zCzV=~3Or*{sqA}Il1hNb4kC!yt<9IK%H#{E)_jkx|GV>tRB?q12Vfz8I*U}Ar_`PG3hy~&*B(hP=gIZpkNb2q!ZNj zW&&hZ40dg8KS>#aWQ|Y3!CVTA*8tps-aY%XLH1T!3>jbvj11V~D+u zqk~L_rUBQMwZ%b93WF?eB1x5_o@mTQAxJb-6yxa*3(kr|Xadn?t0SizfF?t?$Yhu` zhF2m6Jm$hf6yRCFybM!j26$T99~UkjcOW z4naFOepaYv1uK^gQUr#UP=ha(D<*Z-aKVyqx_rS}$=)ynlkGU*OooKE%48UC;>uz! z0!U5#ZYLaZr%O0y84#GZ4mj1VGc-&Ae*txy5T^)^&=YKlvE>YwdI=DM7`e6;5KeXg zZ{f&W4nUJ_Szktn0qaXARW@?qu11b>=N!1BJ-}qB1p(*YVb+kSq3Il1Y&V@)-W#X& zfac3)3%F6CSisVV+vf2Clbv+nnJlF3?0E- zru9_bNR&(2xQk7sk%lu4HEQXcn+xVmZP81X^!^6u{!cgnO}15m>C_j3#S%~I`2q;g zBtZq>U_l-)0#1vy<1#yj7~JhrGYK_pVjvaO3DJH%jmB(PQEGM618D1%5NX+0nz zjI((G@LLS@GM+FqTo&*~gCPjWIkUzq1a8V!@;)X~$Yc<}b#BF~kuur>)C42PA8;l! zY?;X%aUxo>G(ufxAz5&_7%~leV9y{gOX+P9nvArwqSso1#W=y&1(#D-1UIMI>b^#$ z#rc9h67D!NATV`O{{Jap(|y~W+ky-Kxp4D>WB#f6_s(19{(0{5Io<3JX3w2HZswaa zwVBsXe_@)No}2pARD5c9a`$9t;)RJ1OrYbx8($u`j{S1%im_8ie>i&H=}jFKW26x|hlPSoEw*A3yoi)2-heaqv#(!z0h%HT#d_ zNBjc5hWhO9g*Q}I=+$#&!3YYLDexlW1q%j377`69bE#1<*vnK($Jli`(%bALYgsnO zd3u6>rvKtsr;k{??%NN{*Z<`U|8~!r!#BA8_Kst|;J@j{s+^R~C%N1yzToZubJJZ{I^WnVJ^G{c*fFke(yO8@xax`e znULA;tb~gQ)=0xuQ#x+M1vqG`KrN54+6wr>Jlyo>6vQ98?5cPC;n73*fBjkE=TBVo zf$aNqKmNs?|MZ;owm;6C`JH=CJ&ay$%YuFWTH8=yNtE*qeVum?uiNxkJYA&a^Z+jiRrVB8pTCyJ;8zhBu2AOU;UH##i3_N{N3A& z_TwKvllk@c9%w#);mKs@AbM4h1=m_`+Cw$?VkI0k>Z}%HNGt$4MW55`t+yIvsOx2r ztPcb0Ac~Fec=m_ey-zRm|Mc5u&imJ=*?)WSU5DOR_`>~;jIr|@&kI6mj$~` zIS<6T?bSpHthHowb-?^-2HaC-I|A;zNC2uaoTVD^dd8!8#WB0xKYaAj+y9(|p4|R1 z{TrTn=&7gQ^v~}1{PZ^Rjgbcm=Uqv!He|uHsa7R(1+i{2i%uk6V7m51zLU=c$}K#R zG$tSj&Aa?n+G(eHrik8^3m)<8$L@Ld&^F8JFSzQYTj1j3f4%2|ABBH)*h8;7=ymjJ zT^8)B1j5}g!WwKDINME`axt(|l{Y1W##E!$r9BQd-${l2cuBPM1Ru_C`}SXd_RGl^ zE;0S(GM=6}^+xmR@9s3Zr@wJu>AAi zxBS*P{oJ!G_GkU*hkei!Z@Ka*dbKJGrg+{{6!G25&SMOwbf*Nmb!?kfwFG$?CxBjeG!eg}aKoJ$K9GU9-QRdFH`4=K_DF z4t@3^(-(h8{dwWxslvzJ`RHY5>FL$7EV$B!+khyD4pD|Q4v4iWp;Z#gwOCT;4CQJ; zq1eoI8$p6C+Vnldx6FSs`S#aYgKx5-KaKqK-@aCe7XM}8@|E)Pll;ejaPzhE^lC{K z9IHB^Od`@mqJaCdi;%IT5upGL9_i_%O+maK6CDN`HiaYUYESUnFEU>6eC6kbUrwI! zk2~*ahtGfBfAt5R{K3hIgC2}l-s60NUM&idg_nh%<^lDxf zYzJJh-I%YOi&)xRCG3Utbd5*A#R$1jmFzT7qE@bEYONAtPy%G^#izd0xZ|>8^j-d& zKXT?j)xGV3LwAQx{<8Om-Meo3w|~BjUd_pZ%?XJMa9((f{$V<0l`TB+tJ0_??8|{-eHe|LITC zt65pF=mPUXu#lb6HINVyPq`YfGv_GgSV%9BLb&V`{NYOi%E!?|S^)ra>ECo~Hcp>8o7h}~>%NEMxZ6N{CY&PgB2jcBy+sw6lf-k=HoBX{kx&*U^-3_b%bhx zR?-7mD+wHO2o#&IbrnTtiQ^O5zs}n~jU9KV znO;rFf?Yn37%h5jCvZDl7Py`=#8x*&{ZQIV< z&(FW;{(kH(`peIuAAji4QcH(s(V9nEc0_Yxh`|q#b7yZe}!?QP-9S z+km^hLl$iHS|T~962{`-1`Moa6RoCZQ7qC8o1JbDHHRxlh46enT5c#de*Tt&uVB7= zO$;K%m{U2ZaUb8TK?&HT_^3*d2KX3T;w?32=zkJrtFHZhw z{^ol=GW<(=_3g4?Ff*>ORl>pMng%aviH2Hbhv2KC$&w3>>D^9W!wom_HaJ3{e3~Yn zzq4`a#f_sLeDhg{pZ4dA@4subfVUF1ubh1E3!j}c|MD4nb*C&CP3BBQ)DhOzaW3NW zX3FL`Yi=cdon!+ijsAqcjK^~#usqK6+--h$*Yl3|{N=+(qmjf-51jLd7vA;FyB-fM zzj$i;$SV&1;{uN+;vz?Hha1jvN`ca#f2ht6wMiw zVeHS(t$*e#DYiX7bH<~;zV*A`UOH{)^?A$iLHE=hKc_Ca;$nLBY+10w4MizBAGH+C ziEtuYrLm9+TyKTsIExY7`%w-!iB8dM6D^d|cvI)ocx?5#;EQMd=_HbUD1G5)E_=p$ zVfhX{df&bCZSu=DgLp9#xG21HPfWK3<22$0a zNfCU)ug{qO!%<)TNP7I=xtp$WjQ{-8cYJB;>FMw4Kfn9=uRUr05WPytg4>wDI-;3Y zs}R%0+i?+G&D+*n9Ssw1b3{ssa3NA0PkN zzYmSL65ri5_n`H^{`id_)2pN`n20e(jENqQKPqt4Z+Dfhc_+O}$bvN-DenpX z${6~Im9Jg?=!0(nqs@pcSi^zsp5V{?eCphT zpZxZF?%Mu|p*xPgW%!m`p8VXWb~Rpi>jn7G@t1w@IeHbB1#38Z-V>a<=k0>=i=SOG zKI?t%HxU^9&aS&%Z~s(xTl$vecYoq1)Cc7A|0CBA&AoH##fd|}f4uhhngtF33xK28 z(LG18HFSmBg>cb#DUBY#zBYXH1wT=nH;nV=sRpW z_$cS0UXOm(v}ZGcxpIw#1b!6ls_%`%w?rEOK|tYDU< z+hUpzaHV9pu_Gju_32nA)?I4WE45mfk06D7rJXU>wcV&f#V{-KftOUt*SwrS?+RYm6bGZr+62R5`o!>cH8r z7+<9%XTSRO!;2cmuUp1D03++2{WvXDM^eA4g)u=cuVHvP_%o(qL_X+j_5ku7@EU&y z{)7QQKpzZ7&S0Zf?!xy3qBKm{UKiU=X+%sBH3rL|2Dv zsa=Lha6g>Xcr`AlB^6Xx5ClRx~ zVsWRpS}pnwKY)DK6X*DFpbExa*;Kx0BJ3rm(4k_!Qp6DI7K2GH91o*QZmVIx~nd{8{ zX|_G{!c24e`DuRYxvBc(e@s><{xDGWdndv$rP+kj_A0u`pKkq`*dwGuSub+qD}k0RjLT7T*^&EPJZ~0G0|h z0?@ELt_A>DQq%}wi<gw-^9e@&o`hY>zBz z*gx1_cHz34AgD4Y9qr#Q?lA01aDxi(9$?ylb~dGk`5! z2ivvVtr>uZZN|+P;&$!EWB?jAMYp;MT$%xFaT7Q-1K8pwKxF_Lgl=^cI5Y#;;wG@m z05rt*;uZt2$pADg`)w^L@UGo&(+ohv{_o~XBfEB=r5S*R1qU?%&XfUYSn%2ECOAVg zfGuuK%5gvw0yJHtTGLuu_(ctA=oihd zYzN9t+{sP9TeY&@j_qKYJEN(ewqj93@;bY6tb_)<`qiq@^mfP=HDt0^SB?Q%&H`Gl z+TMtkTP@6cJQr=x zfgH#%8h1FI;A&ew==DZaJCBA;{OrnMK%?Y57pm5{#(6ZP=I2)q1)3!1xj?n1y_`ow zwmxv4L!{;c&a+FkvVA*GKljn1h8%x(XxN>|uPgvflJj&`Yud|sG%Ujd=b4w93ph_lwX%IX zPe1>amNjv9Wlll^UR_j;MsXeutLfF1S)fI7p7!Rex*I!>hNbrG$_&sbIZsQq#x>5P zVMRH=G7U6I&eK$_X)ou|uyhKXXG&@=;5>qAW&3uXehxG(tF!FNq=W{%I0udUe2RoxeGYY zsMK7*d8(?F?b~_!Q|+{@sIe;}5*qO871d}I=h3igRb3edS|sNwZ@zl6vGZtH(vs%? zM+}`dwC$XQ`xa*A<8u$r9X4B-`O?fG)3vE@PaQYeo%q(oN#mD}JuwE4UOe*4k+%*n z4?P7c3|(}}bz|FxhPkuwp`q!i;qmdEQ{#&ohUy!_-+W9tK3snH&whEC^zW}u+-Ppy zKf=9e@w!n7@6yY7#}+jV_!Yb_N6zLr$0US5KS%oa7k^ZZ^Mvb0B%H|0I7b&XOcoTJ zYvXQnq@xnXC*CFf`_pCBNRPd4SVDT@%ScDG%p(+}y`bG3>~jrHdLSAqvD^BySE;!uEp~qisz!Lq>ILiKNW-i}fw+G3y7};0j-$tR zN_Zc~H^SRL*uQ9Tb=SH$(lGZ?@UDrYO=ov<9DVD963%a%RO38h_55{lq+#Bq;M^;Y zHbpAO(bun&kbdKrs*xVMdfvJ?(lDh`kScMsDMmStzDi0Mzk0W7j7P7Y+aE_7=3@#( zC5|?QC}I23jq<<$ts3DetKD^Rq+tT5KpYT9o2Hn_ar6)+;r;xEc>Cu)AdWig;z+|Z zP{F$C06kJ$ALd zE{-&0Tq#JEINB7W97p#iB#ifdNj1i!S6lsYq+x2RKvd#rQ-~6_d))HBkElj?%4&06 z9BG)#Di8<6(WW_Kava@xh=h0dUpK@g4Krc|@0vK;6sH_Vx65|@@WZNc zp0LWViz5xQX$9wAakME?IgW0Td(scRpc?71tBrMWq#jrWoZoy7?zktAF5B z)fkUnt@p=~hFQ7-QHi5XAxhY;|FHD$4R2D7@RZfsx;WA>tydrph@(wUw8?Sw-cBd=~|&5=Wb2l;h|MM8bH*wQ4a+{QqZ8FhkpZyzP^d+qYf0 zEw$~mZHFv8weY2d8y6Z2zJ=q#nSdY6e|#!Af7$%bdDCQleqru6b6=QSow#U%nXAmX z=8m5D^ThWiK00NcePQ;o**j);&C;`{&Q8qyV&;LFYi2StZ=3YY969~$)WMVgKKbb6 z4b$J8zID1YjZeR2dU)z5Q}<3?sbmej_J6Ng;57@pW`Wl%@c)AaP8`KYws(RVN75b$ zbKPd6UGzq5vA%u#QB-9)N3UEo=i&W)Hb`S^#pwq;dj0auD$AKvmNUxb+72Eq`0PTH zN;S&yY=O7;>ov&bx&-U1IN}ah*yE|DZF#)VS8n9LdU|ac9$al+^G02jVhneA{r%-0 z?a8a--i!;dClO9G>?`5nPJg-kRF?as%5tAjS?(UW9H44#foplhFr6#Zd~RD2>&N#I zxtz$RgB@?sz6m8vfQxBazpCM4Zob};=|j!IS0?M$u>s=7xf3*_HJK$Oucy&pWfaLIy^!=n5q@yzGgEM$vOIp^^^wf zwNA~8Mgk>!I>h=*(Y{hcPpd5VyIwiBJ)7!M63uE5i?=-X{?1_JGL_{nRax#5mE|s0 zS?(g0<5I{ah{F(`~f1G>ytP9g35CLsj}Sjy>fn*wZ{qtXOee{H9l4n`t**Rpt9VX zd*y817#sAv1I@ZC#D+L=UFW={vfQb1aER@yCln^Jve#1rT(?EB-|Js5ixXR^Qr*e3 z?U3N8dn-c5)%P01rJlH4s#W)*88JZc@o>FWO81rPp%Y_FKuq#EPuxa@$R=j<_7xlc zYESK^<-Q`TZMP~JpPgt>rA)BQRJnYqPu?44ac#a8BfYU|lP)^rti#2leZ_|7dg3aL ze7GAcdIIG%8LWDJRA0GMdg5};PT5=aXQ*`1l@_ZVXJ4`5KdUVFCza)TAKo_Atfm3S zW7+1d`@EFh(cj??J=~MGX}Qn$%K7txmuY2NL2ox%Av~>6|9hZ4vv_iWR0i)dDQ`6u zPPaSxzH(7@#ps?^dy%r+3ZZT#)DatgHrL-~BRQ4jvNHeQ@a;ph@0~hw>{{?2ul*fp z7Pu_GdiEYdE)7eZo98(7QQ?^Y*Tz69JnMRDl>ZGO*QTqj5*Nwlbanmv08%ZsS`oO4 zAakqjFCo`DxvwH2m)Wo}4^Rvd`7qSiX5qR-dzP z$optn@9f7dYFJ*D+)&FwMY+6&Md87OTpHHt2k`){-(-+rttI5zboE)PuzAq-Z#qb| z)&}hWAb%G^E^stU>JD>Ypar;*B&x<9ovuVvgc}-_iryWt29c$%H(ManRI6Fa=hz+D zrnOn)0)D?^2OJ2o&f2@8?sN^(heeO4h&WAIq8n<^X|ib+ z!n~CuOnE{q7U~To%bK|*cerDY_#L9h$(9Ntgd3YVQzH!9)zLsouihi%V)hVnY4EPC z3qc`iYDOzwJ5h23m%KRK!UTJhOjh*Cjth!}i(x0Yk^_TdS)5Y&0hC@H#N9iK zt1&6=-op-S`Rr_6+?fW&orWA{$#J!0RuBH5Y5gMh#nIZ`-M>>LHF>Vl`*f(?iv=ME z>GkqhNvfMjbD3nbB#mbEz@}UajBI-Xm5t#_vdn>pM(OE*UG1K@m!A#%wgQDOmtd2X zY9-GmONnGDpOkd1m7Q!Qg=9niQNq~6(+83lk^;BpH@SW)S=l41m}iT7{u^*?CRyq2 zlne0fdM(XJnoG5O)DCB=1W}X~ z^TI9Gmo(Jeod%Z)_*{HGknZZqPIAc#K^?T!@}|tfQajc0+1skd+tRBm{C}qoT|2bx zquU}2|Gn_xg~0p^^S8{q=boLran3RO^z8d*&z$+~%(XMt>0eD>HEo#s`PAi8OOrpI zylC>|iGQ3pZ{p44kBzs-j~)BwSYzy{(XWh_M-Lx)WF$9o@bFiL+ruXh{Tv9_@aJ7Q zc5DR4r$4kEvxhOpj}~LBjdy2@nP&g;?jmF~>-7c%G9s`E&P6(*WZ$BScjXva*ezOw z!LSJ;iflK#PT0}1un%e!rcW0;b#|SwH_5_2phXx28?D7ks<0l}8)acPYZ0c0jTSbY zjP)-akt;_5Vchgh+cRlrr(75cW-!{9E|wz>lt+k&gW?ip$Q!U&oE{$# zdxR|ZdKF^Z`Di0T_`8JH*NRrFB&p9i5nawwblL09PQ%|VVHMV7HHc*cx$=5h>~$){ z5|tQNkHZ0$50%hNu_M+Rf+6WG!a2bzW>6a=q8)vyf{EEGxpKHHc2$K~8yaD<9TKCB zV%2C!;of$-E_nEQvgHhRMJ8RgGpYw@50k~dPlZ@Mo(2@BRb7DfxZMR$-KPud8z~23 zH)m>DG}@pHzDm6e8HPzWnLDmf}}+Y-wL@hCyQ0VwxWe#g*YM@LZSEgmLDhN{+HL#tv_ELH_`iI#JV1tE!+GlAsFh%8nGvxt^+eB{coELH`T z$fAa`cjU^DELH_y$fAZLa^&&_vRD=5Ad4DKw2{lZWU(q}K(sJ=kjv-GVpZ^YENVD1 zMlPQxi&a73p@m$WTs~J8tAeFN3uQRD+?BAva1B%S|Bs7;Yk?Abb|Ez!ecQ4vcd!zC?s3VckW0L@ z>UZ{WTAWZ`gw>ENAeYy0#B1=Pt%l&+zg@^zDRp4rO6jYAEf54j8}CAj6=Ts)30+Ia zmhn~GjV0RTX_j|X;plKls4IvuOC7RA^cD(^8uLPlMB+Pe3mpiSV`Um!@>Up+DcLo^jfksZ zSCtMRzj|*M^6h(eAvJi{)&=6gYk@R;4gNlXs0E;=UC37{0RO(bklATqe4XRm`!2Iw?x25k?}iS#-&;US&Iay6^6>z|nzA{oxx%#ad6pEpSUD9!IHsU?r&>hN?DNzztUnvy|D*?t(Nz%w zSX(IBaFP`(1+47I#oPpE@$ATFz0G>aSLdB=iZ_c`xDpTd2kct< zs{xSJ2MH5Pi53ud?oXfvsh0 zo_ZX~+E4~^v~V^OsTEAclo=^UsHoFTyUDJow^Q+$u@iAO!VyC*>ZqICj!KL2#@r57 z18lwY>dOBA@k2KaZR591FWkBS&3|Y9ZFB!LSDF3W?2WVYGq=y2KK&2V*{LU{YEx5_ z*H0QI?wmMd{Du84dScs8+Ram+8N9Bf0F5 zuz=Ura4UvtEPGtW4s=K^qv0M5)%P+C>Sb&|pX4$cwsuwPTkA4!19~Kv(XiL6T2G(L zoFz3Aa2XA^T&PyHZ~R?j&>^{uh8^t9Nn-cmGG?Gp zav2R<+p6`gbr}=TBe{%*y>8Wd`dr2+H4|_d4fLz3Rqfkl`gwMf%LWMxcx?@v=Blym zaTx^YkX%LsQL5^j7Y2QpV4zQO84X0Is`agP83^c+KTJN47%h5jCaM9XCci(KnE2B~Zv4N;uN{BW*aKrH zjDB`BIP%jGVfbG`rvDrevXy_eWo%Nu3SnqgMFR4M?xSU1pIdeTmEa;$U7O&UYsQo&8rs|PrdmU;a}bIT~u2CmW=npKg_I9LofTDUyCE&peBeQ6Nlhkskc#|gMcckTsDC1^Wi24Re5M3PYTBzb&c(G% z^J~jz05!4)sYrO<*n>38`g6-~1uA6^QjrY3-h(tu_zTOY168sIsYs9>U8ow4;z1e~0cy(#P$PSgigfUeJxIfvKyDcZDrFB+kt)94gETA$6qX^NO7EnYuNW+={@F2a^WReG+r+TKmZx8C9wQE^8NG~r*a3l{pS2Y~PgEXuO)Rs>LYGe;m zk)6J=2WeO$$Sv!DO4)-{WUjCGAPuVnh2>L#D%pcnWU~+QAPsk!01w(BHJRi=ZPgR! zeS1*9s5?Nb~>Mq3qDY`{z%dePo85 z`uE9Q6NirN9zA3D`{3uT`nx!L>4{S#JGWoT4h@ayH`vZK_Ydd-J_d7OLE38%0RB-1 z7F!JL(kG+VrU#S%n@?iaZW^r`La43nDA?KoGFXdabTf^W%E>6*@nAv;5EQoqsI$$$ zrJ@H*mr{9~)q^^HKxUUIgYphL#KLqZCOt>>SsZgUQ?Nk@6l`LMbb`9xOn}Ua!LF_C zCn+S@t??;1m`h=~7K`Af5SncVY_bkm*5QsAS&Yht344x#gC!OTNV+sS8_VYLph@SdO3| zuaT&D-E3apiJ;j5rgn4;9l>0t^;F(SluOyTi%q1FhBFQ|YU!Mt3j#s{(My)}{)SC5 z^_yQZ^+s3MnEQtaL6nGMm=h1h4k%NTTV!gdz7Q;ycv8<7xU!fer~n)+$kEo>+pFyu zVsN)h%_P*YiGfs9CqzS*MlDZQp_G|sMJQuR2|7d4?KR7$UVO>ax{bWL|6A(z`{Rt) z@5F-#l&R6JGBxJG6<{MbK0|n$Eq>A(u%u_34+Th&*)>_L>fu! z;{_qk=7nmI9B68uFf&}%RLX#!ylIPA;}rroWh;3f6DedeNIsIVV%10)1t)2w4&=C( zOub_xQ}>f5(s+;x`AB~_d_b9ctNLnZDIX_meyZX^f}L7EOc_H#+B2xHwnl4Eq01y3 zA#1R}!9J^*OXsX;hv~SnYLU~~E&4Qpk_8lzy?XmgrapNiQ$u|TWqy>7#bdae3>;9V z-m1RZ9S3x|HGSKeJw&?0G zqLp(6++nI7kiGiYmrVVZjZCfYGquwZbA=)(P8?WXy=8rM#O*8CeM}`6Co#y8BmrFx zX&Yc_M^KFFlcu;n8L<0P`Ba<8cuh@{CmW?a71)r)tah&{l<*g_qCD6;<|R{~w1KHj z{hKv7<@I@jXe@Rh_0@1_tGpV8yvYKNaCC`Jb|D{8v?FmnKfu&>uf7&*AxJmYOdF$~ zXk1Jdy`3Q0bUQOOmT1L24Y~`*jLuwEHucdjnR>%p$NP6RJQ0kdJi(aTbpUlBaA>Q% z8aS(;&f!Ey0MnHc=s>6jRV)rLHEO|t+d`dai|#gSbR-Nqcx$X96!AhW;^{`4dbX=K zf)R15*H^#kB~zcUkyrQ67GrKYM8qOg(0?GAddp^uF0YS}Mj=sJ7erm*V3aR2u|cMG zwA015qh*Yt9?k&M933UX`bfg(7hTP|KS3qfgf;1|V>}hctFQ6@Z`BgVD<%i<|5v5M zQCB90w>IH)Z%TBTg>w-9zhR5{{~OVAtA*LKWmh0gx7bX$*f$c8P7iHPCEg$YQmwUA z;`_`0zfSJjb4w6V^2+g%!e*06(lGEmk^xR;+vX%%VKJWr++RW>&P*Z66jQZwDdXMY zB|>(j&Dc}nPIbQz9($|CP}Lt`s32?aw2ZJZY|dujC{nc&a0LurESTGbFws0=7C~og z%e!)D(XO}0#jGV_u~>H)OA&h=a#?#4U z|HV1#g0_Fzu|H_nr{74Y4*~Z+APbhgdd~qu>HdaSFc6Iv>xMk!cD&|DC6n2(NEbVe zRHL2jx3L@>xKxUqF?sp&(OcW{l>Xk;un2ND^tishDSY|2i?xreT zC~mAbDY&rc7cc>!N>)Db`-N8UW z3u~2l$S)UG%kq)j=J3`Y2~Zn|1Nr|C_(0SGaKMq@mvh0S$XI&;$Ey^8F9DF^uGUPI z^6Xx5CmmFOCF4$S+3+ZQxa1d{9&6K@+`%JoE(@33Id9FX_eHIG8sV2bNgIP?t2w+% z?RVT�+6^F?6hv7X#^h&Q#Es+m5BU$cEq@)e>R13tqBO>sE+x7Q?Idw7&||RZCls zR*dBgP8U(G>(w`_B^Tu@a1Ih{B5hRN2ha#!9mE~r|1U{#C-MJlaHh>OP}IiVK>mLX z2`gLkLDTw0)WE#)Dn-)f%p2=IhOaSifL`ZSBl%j~Nra{EDiFfwvz4UKY$*2ym8#$#gvPP16_dhlN7 z9>=9&qY<*6mP~}wwyM;@I!ugFgiH~a(?qlo0jcc>;8@<76n%)t=UXzl5wAPs$J$tc zb=6QulJ>N^HmZs=z$L?7y8&g3F~$y+0%qKRi*!A16P#X^-Hr6>pu3p?)7G6M%R}29 z-FDZu;TgrOo!UKh z6<`nuOqnMCYx0T7M<#Ecylm1n`IgCriQi59VB-A~;>6C0(&Pu_}$=qL3unf zZX7=Xunc@@>?33E8q1D_#||ESe)K1!4~*V2`i@b@=t-mFBYznA#K`hUdxRW0ZRB`x z3gPR+cMV@LTpA7y!^1}n{cLDx_<>h!R{omcSI7cy93MgEN2WhLAkk*}n`e%XoVasj z`hMl%J~OM#{ky@_-#8pp2eLnc?IyDN|7~yKc@h` zLwU3p;D;2zA5k9e8*u;r-t=b`z#mqsD}WzVRNg*FB$6!4@%;3sl!qT2r1w6>4j)h+?%NLin{U(iDt5S8sjk@J zlSht^=z!nfr2H&Y%PC%{R=DX;C@*uP@?^lv+@qM{2BpXvQ{1h*%Js^ly_)pn%Bx(b zJkvu_Kdbzn0(_eS_}$9GeFN^_)0)0j0erPm zT><ycPbC}4Y+>^clu@p@D)mR1@KJ@ z;LDYt2LQfN0eqQKWDVdO6u_4%kM;t5y#n|W<>9^o_s;~TuTubDtW;M3uPT5qQhpu) z`27mt3zZ^k0KZQGe1Y<4FTg7b;9bhYeFN^#X`5bF0H3c^R{+0P0eqhF^8motDuB;b zimU9^ouNw=zTLCO8)fK>3D}dX|&jSEor2uXzMb-d*mjbw{ zJlYHJl?q@%dAM)D>&61_Q~>izbp`Mh3gCwF^8mn?D}d`tku`uXQvlbLM|%OjQ~_L7 z9_}0Py0O3|3gC)TT>*Tt0=TUFJOJ=T3gD7bWDVd86~IO1(O!TrPyiQ{hx-P+ZY;1% z0i0K=D}c{eROSY$6cv?w9_~}6aqmP7*gAclrt0fD;&T;4?a{u*P#wk0dmarib60ua zJw=rFZ7Z0vYOH8nhpDAh�=t<~vI=6q-1*Lj=|Elwf)mWwa0IM|=o2Qf_YiwRu zJSC|-+RHg>3bJ>oLblGVRi&{KN_C~NDvH#%tCG6zJ5%oM|KB$>f9UiL<0lW__XCI@=p##p8mj81RQc6=8#Adr8#5Z5Y3t%{;Kt0W9!FXOQNzZ}s}xC_ zZ_KQV#n(1wK(F&EZp=UiaJBD7-OUn~EIWK1ix~3Y##9br;soMfidlRncaRdf6zA}> zNrG@7LCbz`%#`pp7hpVbFX(T)Nl!ag2sK?S8vz$WJN#8wvJS^mPAp()_;N(l(V+e5 zipfwnWivPub$Zxr%+|@m9FL|;oIPZO%!anUqw)p@>D8tDi`HsV{>%ePbPX}Lb={3| z5GA^n?ne5GuA@ZPDxk0X=uNMrZ{ynBh*zoO=qcYnN_2yS`IVwXU)Ns&UHeIi4j~qJ z!^|hXV{}_Q5aDV>@uoA>Xk{&34~_{-sK;QmyM0R;3hES$cH@ry|IpWXCt`q7;2I&( zgWALPMuQ5bUG1i+4MPn#;~}_Iw^MN?Gaf4m<91HZzyuoHVP#_eA{eZ4%?=jskkMR> zYP5~+yy*4Y7#ptUN_6?vLF9pa+jmHjC*|8}aM!JiJj43P8xTta2C2O{;om7b_BY>l zo%5a#ymwEU-z%GM3n6-B!~Opx1Wxbi3tb^q_7qbH+HwboC8rw^ji{NW0y_)_o@`cH z;Fk3Lzj>pD;@u)?6oT{)1D6ArdgS9dC|aPwne!BE@}LNtXF4f!1YCu_RAWm%x4B+p ztZ}M=XJTEt98CrF4I_g^%%UX(j=AZ}O@m$K1zXaqOZ)%RrwkE8-x!+u%k0Bb-X;(8SVLqvxd~khNWrch-fDj@^MD1$ak6r z3|t2lZy^@v45HM~SwpdwXo&f-f?scRy33|oxt2Etx-Qt(GIra#a#>(#N2@(y>ea}& zlAcJ*(6Q#~QJTRsc&cSDxv6HH3}n(S$dBtFN7-ODvChe7do{Q)LIrZ7BOyRZu0UC7 zIt`-+sHyjtOf=Wwazwt!1c+)lFm+I`Mv6fZXGp+fT`L`?QwD=0&J<#PM+K?dJkcgA zluecvEjq33QB0{}Lo&8fnqsVeS2E9-adS0J1oM?fA}Q4IZh{odiB8aN@LMJx?$t;p zs%5w+MgSv%&&$^^9EoM6E4!5F&&~E43Bttix_~`e0HR4DZt0#4=K9esS<2f;1fYKcu8ncB%I;%C( zY}mt`ONZw~=h)r78gx|@jHyo11jkc0tm!o4nXu@J3a&zcs=7>GTUggNfyCTQVEpo4 z4N|ZX-i(lUrz#yRlwkeUV#pBf`24y+g>$+rNM7%8X0TQ(J3}ZnaA(d&nkyyI8Y-FL zVo`?~*m_9t7%b^btq~M!xU&<}Hj+r^bHP zs}Uf|JQyE`sxYoE5Qb!%A@m{A=N6-;MkJd+LBf=WsrsRGV7j8zP)#E{6YJIRS&J4b zmBtMLk1Y^w+cIg!U~N?}B0;lEBOm3Hx;9QZ>!^O_-+MK%N||6HBAE(cDNC5HhjG?y z0>*=zVys%U2Fwwj4tS!aOkm{sUdP~~-4K*%G7e`j>Fg#1ohb$B%q_T?7MOg+m*EQ) zifdSz5}xX*a7Gf&4&;G4nFiht7fe{l4H@%X!`pQujWArbMD90bu(L>*lU%(MY|_>UX=^pzmO>n%t>py6SG;~PU~vD9rxW3fQKR19P!f1>KNA-PWmqo1Cqh*`D2qtsD6^Mi%GfW(+xyB*A8b)0?SI)8KY{a0; zCgCKRU?NV7Pt3)@IrnNYov%i$M%0(rS4Xbz)j(>lxIJIBvk3vqhk~9MhgNMUVy|YQ zxSda>vU$;z3@1Cy?94}dHK3|D+3_=i<jkZry`B3uZbxbGt<7@L0p2%j5({z>5Tu z za78TnLf)UVCUfChtl)_NLLb`MEnCHAAsd))_i7mJfREhPt>a`S$y>af$!xMyyj8S; z&Q({^K?b&wj(F^vt^36`@NZpt=mpo+es$gA{^G1fn^PrCKL!ZTui&@HB9%$HCB5Qe8^g& zC>q>^U}MY0RJAske!qq9Qe`L0lb-76MLitGM0vfmoXC&~IPY}=93)p@AOhUkFU z@Pk`v5G;x65{(qgP>Fb}$?8!$U-AUzN97u3Ky1~Hdc#CD8~4SG`UU~MPliwaM-8-mw3y^8-lViW-_uy$J4Gw}O*)<*_5&AWRK;ekzab=j2li&Xs#n09=lSEEs=1anba*c(ric3Nb!y13hw$Ol=pXo~n9;gUHN7qLiJ zG)!DBzo2F9MtScAFWu8n;zABaN%*I7??gGiE2|QfiX+Mv1Sje zSA^fZNMrLN^&W!tp;_%cSl^6s;BjdcUiZH$(rXk~+*vDGidd`RtlDk9kjvR{q}?ea zW^$QvykbcjIoi|E`^<(}r7Le3y2g&pm#x&gO|ykh#3S{D-x_9u6j+tk38*<)ETL2* zUiFOqM^6W0ZF4%&>()mQZ$8XN3nibMH=f1%uY-D|1(3Dp@q`ik7v>8PfopY;+Sby$wQcgV7heb3WNx!I zSv$Tz(Awkxr^9-gLris>{N@qRCbOHhNq^?;fz)>ew`$ylQf+U=(@eT*tu$CSqbR|f zf(KEQp@B#<12L9rtK-sli%v0a`+w}c3!LNFRVQ3kT~%GK>PN_fyht)VG#Q{{+LGlL zuVBlvY}uA2S+*=sCXOV_vh}bm%MZyzdI%em03n;9hb{<3|i z<@qh_7hZvH2@u$XKzM$zS9VW#we50Qotl~E`=x&K^RK$*o^$^9-gByo|==>dhbe;s^ktR z9s)$jdWs@chOV&7n6HzG+8`XxrU6-uERC!<)Ne+LVMop8dmK$-e2C4Alfx3-#sJxQ zv7AE*x}L&oYtPBgUj%a!IB8CfZX4ZmYu9PLcV&{UfHx5a9JvX#OqhpAwuG8XSQC?D znj&i*28E3tA|^U9jO^K^n2DC-brD6~0^N`?SFG6iIBgP1JejA7Qog?Socus{mjD0G zjdSC`H|;)a^PMZc@_+7sJrg4U_+XSL(~f=Ib#wB4GIQzvagcShEZrZ07PFD=_j4t! zmyYD;R-EPO`#$dDT}E(rv=V1NKdLP&6)IgvG0aG{ZIU3EBb%Sz8srI_%ttu?H1VX4 zCh(t}cD$hBb7{vz3sU z)p#4Z>$KwtK6LBMD3igfW^AB^2?FHSq{RGy?5M*WG(kj-8D(LTOIH*OMYH8{0^y_a zYOA9_X^L;L)UZ~%!ez=vG=>b?F341_rvW{Umr^(EdJHvyM$e<75fr&Gu)(ho$Tkik zVX#*Wx;{%gZatZH>?7t*Z@CG5w#&_uR7rp9)sn=@3? zfUUbtI|ihK$7f>7`LQBUAU8na^xBnPwK*K?)qb;{8HczyD7zwi440$F<@DWWVrDqI zSeS^;5L^g^n02>~!X@--385x>Hj~gri?R`u(wlaRk4|X3XSh|wg{|?ZTN~L3!5h62 z+rGkc$>_k=YW=cRadPW1pF&>OXKBaslWE63Zg)Gi$P1q`&F|9`9e0~{yx_b~W7;uD zu|JmOkg4|25HDaxoQrP=C|oNyGQfu<6gnj%duZd^XsFm&s$ghR0mAk`>5kt{ub<` z-gnts@M1N)2kRNAq*(&wnV|WFq7Za!RjY0&0yZ9vagYKTtB^5^9+cb7NnwN~K{ZOX zP{5?npqAv(?u;SzZutMoM%L`P7+1pN($#V}4@gI^N=!vERU}%)I|C-mn2ZE9C98s_ zhAOB(s6_gB5xz>hb>%8!TQUPKx1-Csj5_W%OSl{vG}nCUcwGm9=zafx*@m!j@sSJv zdf~O_|K;5vHkPN;W+69J z)1_FYoy5evYB8N=gVr!=kP4kPfbkT7k6Xa~++=<7?bX*FANdC0A7N@li%)8rnDsG60KBv)7T$)4lKG+T-0R03Vmh`?(PPWD4qQj~l)L_+Xx1 z2Y~Jyz^RgOUwd4e0`PHP!)Z@}>Kni*PeI2wfK#4=_7s2*rKddwif;g?JO!;O03RId zer_~bCk*8&fE3lRCbhiLX5wSHOA%10+5_d2ok6=~vTi?J?ipsQH`2pM#m@=C$y*}V z9&h>vaH@MU*B+OA131;wpKFhcQvk6>wVGpzUM~tUHK8SEM`(gh7(GxYDB5MRS(%hW zc)SxT1(nmD0>L+cQ=Wp?_y%ywQ_z?K@S*gyr{LAT0i5y_JTwL1Tcfz^kSJ zd~89TE*ZVo%4aTZ{qL=x-umYK z4{hg7YBnR20?pB34X1{KqO4S5Cv@Z8!0pj1`P_nvx-IY``lCl3x@e2n`Nt$ffXfjWkg_ZM$E`g7-$@E!di|oI|z}$G};^qB?pNzwPY{Hsi;{aq|J?)ge~*& zZrzPCZMF|v6Duy(N(BtmX;poKbFHYV+zj&etsrlg>%t{Vm5IUrAihJr%U1p z&Wx+UPOqR2OWh{sw8?^<#qI4`VQCtV#i{O~)6L|`N`lTxS!-Nq*0X4kz;lB-D2J~n zq>Pl8I_7?0Y6EQzLphEZ(v@_-SY)jP5eWtz-3_rs#_BbBL5C^@w=xPA=)gbBQ1wcA zgB$VqBn9<*jVe+ZkgRKF8w!#i>qUl1;yE)Ka-4c-^Y>>G+8~l^Aws$y8*=4tx;)0U zVUV=S$*K|#mz$K3MXgpX7fdC$iZ>;2x0)Fkm|AET4W^&ZC*u~^hX{kG`eeOaNd!$E zhAdOyH{LmuP!v8HpO9_SVum_i8<2&eTxs=ESeGrOdU-<$OXFNRlj;?>{>x0F+12}P z-LCP8DiO-6j2ca_j?x5W|5itpb7G;LwsKT7jnmsV{Yp$Xa z!EuJ(5@r&$38+%n5i%J@tYR%gBv3@4GKtBg5VAsS&SlF(tIvw{gn9NSd`rvf(lolO-)rB;NIlm1Zu!txvFqNrCmwm$-UT2f=KlgGu#9zU3moD zj0izNa){^J1f}zu+Cj*%!)cXXu07T^9-B$z212bI4r;mxiosY)%g0P?(&mGe&}7i5 zu^2`H6;a_W*eFuTvxS*N zx>7UH$e`2;TMceRGHx)#51U}eN0+gESeAliw%g%)+15PMDA(CbPn)gfy**$Sm#K>JcE zmDqH%)5)FXXA)rtoY7hBay${D!0{Lp(b{f;C#bArrz5Bi?vjjS> zt7*AoWT*j@abOD&ENDa0rqyA4gj5Kro9nynL7`E1%votR3hw%=!6DHyQsYuJfyGTb zM};{zozvo0ZB%ly&8Cs6gF;q*`&(uvh8?9qAZ5PjU=mxL3>A^6=$e*gkZhA|b_zNI z{GKQWG3B+>UZ6c=m>;Huakn!G$NB{&3I&}AOr@%3dVn}W5h_x(+^B4Te$Q52v_~)m z;Mf_}5mRA85>qv#hEPtyS(|C~=yHLubh0quN_1}LJu@3dBv-}guoBCW(Qq%Cpn9>= z&`dN7*F!?P{l#wO)fiRsYutxaz>mi zP|aj)VDJ3Y%tlA5w?cL@O^`Ja5Q6}}CxUJ?#f`4^fjt$aim05>d`tpYKh4SPhz2mXC@S=)=_#|Jk%K*5ktpvDr1Ek zgF-~1gSFf^4j1@IjO4S0+|J`OiKf^ZGpdrvnZY7pz-bEYcsA38c)LON`UX7+%evs2 zc}$3J{^CqRG>l0LtwO+45}-UAZe<2_$}tSg$>x>1WENvC+6p()EVg@dsCcErCPCeL zlQX1p(iy|URM)OqeKg;8Wj@{4&@R=;4qHfkt2DF08F>?|poptBhuL19og@coc&v|_ zLA)BYd%=uStxhU=aGl=vAKa7(_iX}dDr79E=Of^e>-33Ho~3lI8CUpX9hHJa49OU% zwDUVNi3*WMlH8=(tJMXnL^ca(D@gNVKhZY|1u4~PQtr5}5FCqa_ootRmf6DV3{e*yhjOd=%*5!b(Pu*NPEX#>9>Ymy#f4O9(H6 zH=)>Y3}?b=!RDw8a&ESepw&*QH?*x3i#iNUKs?=T!sCqIYRYucR%@&hjN@2_O`yr$ z7tQ)ni=|8HVzONChtWtZ-6O}CJ2BEpIR*R;(CsACD$x(uD^2X&1G9dBx^YkxOe1;@ zByUwf^bnEZP%DA3jp)fyI9e95iQE$E5%XYHcAj=e#qzj9sCGXi!~h{nasZDLLqW^q z@o~%o%o=)!r%MT$#|~aNvjO!}3B~Di8K_V5(nRkwnM`M7W93d=L`q}IBFB1Dq3pPM zFnfW+BF#p!?RK?}GG=3HtY6WgYDF7Ygicu+fw9QvhEypmX=HN$UuHJ?OsU|c-RdYa zLiuJGuMeZqa#xDvp;EFE4i|&fWUf@JB6029pU)(+M9?makwL>~A-TL7oG4~AH<5-V z5$&gX!4y3zl>qU8JBaW9zf%bewv#QTM$kr(1EJIv3mvVNPDR7w0KBaRdC1g>@KodP- zTY_dL2ACn8pE1s07z2l^(W*}LEFlewEA#?}H-r>i&F8%Jo`JgMbcMAVvC!71XY-MY zw}VZ2O##=)!K6c^` zE>;wCFih|jGo|-iEmr8t4qr&bcm8lDkpTH6dS6td#YPOAU!y@j+DVlhj5hHMS1D&= zW|(1}ii}8SpEq5ou}C*0SDWd!!Zd5qP`pgi)<7?t5GvG~r8Y(MjSP+_-BP;p!kYqY zpF?W(zGEV0x7!F=EO$6_KiCig<<>MSQ2Ja+TQb>uIq7)rj!%_ppFo{Xc z<-0P;X=i3@CyrAS@K)H3B!$|)85wLUtCqIR8&=q4Yy`SfU1W9R_Jy%%n-W>4I5w`vPC zH7p9jI9h3@B-*4>BHoeAgD>>%|2x=t=f>9A3vW9gJNS)#EwFR;9lMF`-#TM}KR@$7 zU*FsXGy^Z&I)-E*eh(MBA*WXChSoJP6NzwIJ<~EEr0J3^0p^z!jHC*DHjEM>7NWx` z2A2!HXgW&vfzzcqIaxN^i84#Z8?BLPjLlnRB3t*JFv_uexXd$LI<2xCyh_#dqAMB^ z8KjBGm24ZO(eaW|PRGM)E)GjbqnB>BS#3PT@D5S0hRQ}bnJj`DDow?k;cPNT93AE7 zpD@btd$>ptIkh6e41-p?DXWO37y+*jIJL>vv1PuT>~W2Rt1u-h=x8IQkg3GWsM_R- zP6OdcBa=g$^%%HrAR7(QM|}BZCynxa=I1?B_J@$CRrZ&yqJZs`q0@vVSD+MwB}|H~ zEb(O&u9hcuQ{@~ql#5qfvXZLz;##?vQfO91`A(HC*l?o`Pr!ZGw^xjpo;1q$a3LQ; zp4N(i)hR;DAWXLc$W=4JVmiZ$(6U0lbjj|^&A~8|b)15Xb9%27MaWhpt&K}rotqHV zhy`{R2ZjcAkZzCiOHLZ)dzfMeIW>wMv|i@)c)v8kR6)&Tc(w`WV6ZWiPP+9j*{j2m zb~9Ub>*cf@wUS3h`3p`O<$IVk z2PvLb@piU7DRJ$hjPX{rRvFjADT_`oTQSnj5~3tVGQ^8G6wj80ioxZ$n)h{(D``%> z2}tG4DwJ-7gGYV&#V3vOJxqgx6i;hiPLmq1lX-d~86}P2Xe=JCcbATGGESycF0Go% zAR#McrHrTbG?EwFnh-Q8HQpJ5gPWe7>c@_*%P%@Dzz>oT6G(Om|iYZ`CqEi@UG&Y#ED{-tgtmWf!sH!?x(K@yMYt*Zh@E6uDvu9uUE9G+2mxH^%>CeV>{cuzkb z8Hom|dv? ziD(7c9IME&krJJtT&rRfhJ=h&^Qdy`*%vy&kz+>*5$`dkfN<%wmJ8gD%7)Ws%22P6 z#b74e)M-4vI9w(dz*bP4VmE;9m z7v2u||C}=$8(Z%>Q#gP1;J@xaxc8~EZ`^&!_WL(K2L4CssoDZ~R&ezUgSynYI_!Jr zMO^}3-m^uRY>Rk%B!%f{DXokFF9!HK22Ud;%S0r+qEO{jn}y2|;odX;~~aShz7^ z%Vc*vYV-%B8wt8lc~Z=Yie9E$?XlYgw*=5*h%Fs`^nM+Pp|3gN!SJ*#TK*plk<)rG zu5 z3RcNPI1JQx5Q@kQ+fhrWmRtN7E~uR=+P)stfmkHeg*C0K(fGU;Uwoz6DZN{(+@ z2|S(&Iq9)Ok{!GUg#hEZnxz^My`_kKSD|U_=v&SRjP%Y4BOQI(UUfiWw!c>$u-D$>fqf zTtXWW1MVOFZlrB&T=>-!Mmlou?hXP1qtp5piiY}hmKc*R$O*$i>Wk|bAdzdyBAo)~ zTu6PQCDIX!YA0f8vXp{V#bL+8VhirG#dd9ej9F*J~$X{x+ zNT_r4aaaK({jDdAbok!gCFCckwo90aC-ReOrleOhg(^rj3%7%`yX;&cRmhJ=<#c_B zBzR1#>2M~h3gBy3SQ=q5G8rl3fTUljW~yWGJiYZzX^Q|OT|Qx?;d^%vi+4|L4-17X z+668>sX|3a3Dc8mrI23c(`Y3M3j$W{R3M_;DozTnVW|`k9*`lDq=zw~ViiDAfm4XP zN7w0Bf|0&*!bn5+?miapoZ3ECHbwHaTr3Z66)Sd3km?s1gS?$3>ogi|2@FV%Vv6Z< ze-I|Bbgh~sGz3b4w+_VMNJh@om;?kel#lv!bYo)^{P&dSVLVmeZvVaLo$~)}ef!3_ zH|_n~?z6!!pZodT2;2iB0IomQp5!6(QKxuv?cp5lVFD1KETuhMdi}AFGR)-!E`AlP zb^WnYYt@)ETa&A!QkHHC0zJ2J=CQn-Okg&W~%iXRnMe_f;!36q?m*2bkrW?92jd+dJZOOqzV#zDqKmihe>ES zN#?Tyvq~XDcD;+or(K`%kf~4dkoj=e(~G=QzW(@WijKSGAzN_Xr;mpWh7-s0kh%R) zS4gz5tF>X6$c-Y==#@dwHp{sVl^R5Oa6MTDZO0hXDrtA^>|Bg6bp;{wB|N5p>sOmv zJXgwOxHO&zw-%CvNCnViM4}WHuCxl6mB~i?r7~3OB638_wz@PC?-pGn)EqXevZU$m z70bfXYw?gxyFTL~>paOr=A#<@-=3P3*M2k9ynXk1-!r z<9EkH_B4?P-XlC@Fa+*31QXd7?6&N&vOavj-4@_T6#ROBbTaA*SDGn$=ysfTAK{@= z)g?DU5kgnI$>Hwn$1DaDy^30>2>XI zd-}dN+`R0=yG}0<;eVHb=x3!^!fUIVoh~Rg(1Ftd_d7}i_%sFJEdZXU_lBb0nUU!= zWl$Xp{^q@l_4@LtVGo3X4F2$-*Nf>t8k+2SQXcZVecKDc@hA{q8g~W9<}1Zc(cyai zvOZ*`s}h1`1~I_8tmh?A(MJ#jo?;nr)81Xjotsj+iEfe_WZ}{kD_c;=Jj7sqr5cG- z$#Ju1QVU)g%~<|jA5e)G8-?|G`fFlvvzc88|vy~|J1RYwpw8tx02Ak2N$ zx!ocRN8vW(mgZYr!c!E^7t&fyO4?aPYYoeDI<&{WcFx*yx^7#8;V4wrq=_=8YaMG3 z&sjT8*B#bEP0h8WyRbG{w6;{&ZEFx5NoZYBy_>$dbJmX2b=z79j>M&=P}6<)?UBcv z9Y8g?ce&jum(xf%&%n0RiH=hZSmTwN2-i(pF$g*YuKBJSI$6qU%H=$a76(kE%*M55 z56<#22I=HGJ{rRaOW&hjK{HDUb)n2Vywa>!DT$_AQ;XN_|SSenPzy!W$|jnF(Zgb6XT{e z$bn+E5k^iN9^6C|X$EOFi&^|S2nI6*g>1qRt&z!?p?0a)P4&xkPH))`QLg2Ta+0k} z6zSw)1Pz&&>(MOdl#yIFm2{#^lm=%tG{zOt$Rq*Ik@HAClcf`#E`{OPB?k}&xC9Aq zEEiIE#+B^UoUcCem_0+_L+j;amhKQJ)rezOJv5|SMTnOr1B(Fi2qMjP8g(RGvIhNF zI%KYcpg%+4L+fR`-ei(VP^B#8vRO4}<%3kHFQVa8tkqM}T)%?kyOse(w7#|u0&9lA zht|tcOUjxI-3BF$5CjN!$r=?d>7tpoQq4jb&olZKm#uFfKbD%&D zBF6N-ilmH+801sIAWIBgp{Gb~Ikygi?hFB*5BE8yjkGwtHE5z{#!8L(Fdeecx|NMg zKsM8$!;#~B4WTc)RWg&oVhOfo7>X7)1tgtrL9w_IwFriBAhDRO)su9Kv9*)=cOQAo zm?7|?^|G1c3VbOB7+EMSjQ5+c8Ew`h^Z+KKU1*exn4@Bzvf*K&O|OGMpCRy})em3s zIta8Gf_$MjMpTJS4k8`Pp>+tV_6SA9aN8AYNN30r4Zw3?)*YyL*#}|qItbJm0v}pW zmj?Nf$2v0vy-~KBtMqNHOL384A|5F@YNQp+V==-S_1V&d;KNED2Q>!tWgnc$C$j)Q z@>qL@z=zh$J}86NL7>bK_|SUU2V?F!2wF1)KD3@L5AP$7$uk7c7!U3vk2Plq{1D;# zU~D~kEBukiq!|JqS}*$`EL{hII78q=>*>;XFA9GXy@iye|9L^Er8Y%sc-tZ2Zc`#b3Gj?H8LDLtq!+mo7YZ;WZat0rmoZ;rv_A zzv}!KgB^jNJ@@8wwR4xjzQBhL-gLko+y{0CKCu7#eF|^}{!8FD1CIx;1=zrSd!N|* zz~1ZksJ$;Z`|-0sdiLwi7S6t4_fL0!X!mP(v%AmT`Jf| zpF8urXWo6Lcjhb4>}~z_)^~5|Tk);!&0pVq`(}F+0SO?#;%8?8I(+#VdF!%RCrc%1 z2suGpESHPW$iz4**d0Rsnh+F<%Aks5Tp<&!k)gnkK!;!C$-qS}%t*K{t4X8?k0wDS z3#tgUJe$rXXw}V`dOZd<6AT$1doq_jnP3hM@)DVdkmYJHs%bhEbt({QDF)fLbzVt# zIm|6|O}k=yGGFM)pp05#A_6AYq>?zWsnz{93%Awn+Y%3aayN)Z9x+)sBthmEd@a?UE;h6c7iGLX8UCk~%pm z5sQKQjp7*`!bnkZ2c>+3=F@3A#Rf6Y&V8OtsgWJ0BtYP!PV(_K+G&Gazq-wZs&zg{ zjMCLE7XwEv1Z|UuC-X8-CcQqdiYN0@Po~_C%GF6+i6*0QaE60xh8&dIdD=_?8csaJ z)n6DQ$G1TXia=jr$*_tW$tX6sg>*HQjtQH+*#IjSF z7kDx;rxcefI1+6oYOO?)Y`MYMST6_ZTEEb4wFhXC2p0uI?G}n&f1W?JLuB>bpwk|A z$*kEorH(C5N*TIeZ6*aL9>NUO?%<9TbMsR__84T!7!k{zt`b6m#Ww<3*`>DR% z01iYF^$1gI_kx{fs6|i5`gxuV5AG`pkL{?QGfP^i(v2kzF*AylJBfI_-X}0aj62OH z;F2rn0zV8LKF5=h zP$tBHn};UFi4_T_b5y98r7g6nbO$wEu17@`t<Ma5LErgVa7g zTAwsfOKLg&K8tHYUxIr^*k$-ogfF6ECl`1hbohCm%tUfAQW${nGCGZH3s4NU0F-9D z85_5XX+dX4GB*qvM#C5feh@l*mM7E4^rDa&qJTvPg4+F9s70V9Oe;m4fvRQ+I%iNB zrqATFiDcjhpu>xvOe)eFqHPl{^i@!!Txrq4cnNijBc)CzyOm;jz$LIyE7Bu1&6Bw> z?N3cm^-SFC^b$jv!t|^&rp9GC#$%kF?lfhD?B{JnO+*;XlR59na2>qrxH=Uc8CY90 zbICYX&gab@&!U)CChKj<9HV$R*D@4O=A0)ZYT=Bf*>;L#im+*MgD4}QFf1e^GM&#h z-61L!nHJ&>nnh3Mz>~46Oa&KFmnm1-WDPU8ZZF5$WhtnLO1$k*Y&I$+tsX(4h$pk} z$vAzu8V|W*HknjHh6Z@|8hND)mt&^DmT8U%bY^jdbqS6Gbw5WPL=gI7OGSvc~o{)_Z$Q*Cu1cP>5V%QlOjX@?- z(+wgc%45Myk9)=7IPhNR@T@08T6hhx8q!#}9o8r})fsC}amaU)YJtjNgPsBCC&+dx z%7vuB_d|!fo=hQC2-PK`o{AD3c$gk36QorpqHH-N_Ju^l1chN_DG95voD6&)bhzWm z1c}}#9)lP-S14r#H(Y~yx|F1=k#Yob+fgJIj3y0*8RFe>;0fq(dn!Y7nRGo@vdyp< z4>NRK=%55r(jwU(U>d!$;Fi-Gq*hZ~+u z4wX`!o{`8hVy7D%k}`tR*)a`#4`V8hKv51Qz_Dnt*hK>01KoJUld0fA6BI_5;=^u= z$cAo>^2Hog+GCuZ;JsER~ZT)>?Pp0e1 z_}G&TJOq~f^9#HMy3z7veC*N%z6H7=dopYbJagfe?Y%oIT4PiN`_(ya zfaVg(ASYAIpj!(mXs;qO$-tYT8%3pXYyVu zp;$JlN~Ib{x?Bz`=jIblwm!I0;X+JUYDcA0t>V~5DYmNQDPz8iKe6Ho>x-1qrw+0AVN0oN7oHWhKWM1K?Rw|sYyuHL5%rM7woJ>!! zFgZ;Z>hlUexl-Z67y?H20@_TCwY<=2GdU)M&nx`vl?vw>*DhI;etnYc$f$ys#a_Nd zFO1<|tyDN)mWb4n#?FZ>&*pC+1@_wBg%Q%L%HOCoD+*W!%U*hsAJjg<*9|ahW!O_Up$5$#? zSbUEDVHVndwo<`D;2izKOu_$JHOa_Aps?kPBmkCcz|hWMStjd@<|p}2S1Mcxlw=8O zf^rlp(`@!FF;A5W^9nzFsoB=tL5sv!rxh`@Ms)O6%Hw6j&4YXUW_KZ41 z^x4e3!e3q$x#5M#Jvs)n$ov4-1rw)DqM(*lNJ77D-|xdv6bs`G&LaURw`qHdpQQw{1p7xl?soJ z_%tpu^-@6-3Sz1!bi}@xtO zsGgy9iDh|feo}sRm0K@7k2Foeuxf>HyULISgp*u)PTzg2@IebBc=Gp!pYqQCUjQ-y zF24H0PhEJ{`S!UFoO{lJwg0R8Ul@3J@4xN6{Os#@|7bV5^Oo&D*iM{zZ0m2f$jx_e z{0k6R^7Fb^9Dc#YjZOKL^u|VDZ*ynop}ieHJHf`6c@-#Mwpf&Pa?0<{rtvMm{}rnD z`IUcg94Y7g{Jrk-;fp=IS8wCp_OlCp6Ys4W7W-tJ+aAKibEcnvwl>a}AHK-L8NQA4 zjGvwFn>c&sSnfA;c_iy=AU)$@j0)ao%vc-geTOgfkUnr5sjvO^n@DGs<|mKlmWK}h zX73Xzu8r}fhcCFe5x9vl;CZQ^?fsh&rM?Ubl1_%`VY~WP?=$q0wGm!Ae7-k|UvhgC z&-yt{xHXElEhzHIIl^Z>cvqO|XZ0A6(L0}+q0s9t9e(~C$X!3j7k)wRdhovZ<An|uIPd$twQ;`u@HrmNJ60qg=R-Gf-mxN`h;+Ily=3YEFNN1edf(x*J*0Q6 zNIp)bZn>!;%Kei=X}Ti4c)Gs7c)T{ommYrJ{EFn`5bP$zn=8_Z!|2WQ^S{#je8J1t zMtJG)S>7lfS&?@AoVnc^#o3BfuB0bgE0a<{@=KB|IZS!(J!cy1&;BD1G}zBO9^Q*zw>I9(hZj7&cLckS1IC+pj|BUPIH$oL zc&~?Zf3!Bvmmi+@aNZH@J`Oc+;=Ci+PeeKm_T7(pNYDP&+DPv^Jm(?3BiMZ$rQUMW zX|SJ&aT@H~FZ3{OyK7^7>EXeAu=_Z!#xl09l`G7RQx91Bf)+mPA}L$`pI{CpPw48jq~M) zXFZ&E1iO!u_?tNI2=)_^dcpqDzxqS(^RNGYZKU@d?s`b?2zDRm|F_(98tf-x^n(4P zfBD?$r@l7EmmcoS2fL4SgPRa<2K$K+J#2qwc%L6Xvo69Z|KH~4Z=8F>-t)KL0e<=1 z4;X7p)Hjx39fZDjKkZNei>ZILj(@1+rQQf$r5aAx%W3urS&)+pYNpp}A zL~+O}n$tCK{A%`!EyeTgbRS$aQ5p_7Q3TiJWc!6k4zrnL7?raF>Pp@;Qw-(CO0C*3 z(hOTI3s%x}DJnAw29(RMM^7^C`ivH+|0FGt5AQm?KwL@-bk7H(AAr7Sfu5!Sy!D#n z#og0R3lt88pxAM7hZF6t0@A*7W&CO`-5!kl6?)JL6$V4K7iuZ4Z8{yImM*5`8IzIS zQM$rl*uTl^rJItVw@rKS<=!a_=-$Kk%S|{E0r^ZqqXa0By&j_k;k1nrkqO(QxCrB| zZfC;b`BI9e)8mvmfvQ-Z>Zp3Qne8E0Cu4hvmOG;XpavU&8-V&oq-~q#C=F#C^~$6N zYyFrNO0Pu=H0?TwyEkb!EHCcjf9c9#`$&m_ZH*=wS4T;-~C6{oX)wGt;3w7A_9`n<#dyo0i zjo$Pz?{4}@;Xb|PCiK}ZH{Mfp>LRxSWR@>`AD$h9Jwpq}XT$a!^DsYEY|qPH z-tU(GFCIx8%l~J$NK`fRcu2`cn2x|Ei=e1&q_#-Aqya*{fsAH)(8L`I#NG4%g(mdX zSTzPBYBX%8^nNIe*r7NY8tL_MGOl(+sRZ~3RWWt7-qx!Fh)X9_BQ&a%Mj=@mr}JT< z&p_a1i6n6>1=0AZUB%P%TD+TH*FofY`TxY$hc>qP?N^-nSHSD{t}}x(nKLig`ird( zUHq+!-*a(%F?aFB7yjzPf4T7Jh319mg}w9t^Za|yUpr5ozaQ`d{NlOqIH#Tas&f|) z{`lbi2j6hO9em0DKZERn@80k4r}sY}@CW`>;LU+o2SS0Zz2DyZ-aU6OzxR@}|NHFE zoxOgxbvAx>AFvAk;O^J$GP{>|KC|;nJKwou?7VX4^S1wF`^UE5xb>E;*KEOn58(ga zd}8zA&En?EHa@lS;UyasXMSPHA3yh}|G$qw@XSNUW%DnjJ_bIxs`dO)QsBo{wVq#r z0`FVZdOnpa@B^z_&*y0c-nFXrd=ggR9jjW;=LZG8ZB^^}^r^s`UvcKa?#N|M!n? z9Zf8Z$?vXey@2GmR<&M0@@vOWWpp0M#yeN2j?c%!{x7a-y)ZdHyQ=kkEbRZps@4lr z`J<~^FAU&&R<&Lj!0%o$fb*;I#&@hxy>M&v{^6?D^OLiGV^!;UB>UG_wO)v`x2$Tt zF!66#)p}v#U%#sL!o>fFRjn5${*hI!7bgDURjn5$zPDoH?;Jolv9apmw~)WKudD#F z5S9B)U#%OfS}(XsepTx`dl?GtW2;&(xXCM5wO(+O#H!W{ZW3PAdcjS;bXDsGH~ErP ztry(nWh>lden>V}MP+0@&2j&wD}XFiweCNARqKU`-(J;ve&PfFx~lbjF9ZL$s`dQD z2R^;3_58#KR=g2JBl8m<`1p#+nFq45YB^r0q6>U%1?+`eTm!$is`WgQz^WGlWMN2t zW0mfOA^GpCTF=KqVAV4onU96Q&#f5ldDt6o@y`D*Z!jAd-+J*C7ryfXbp9jf`E!4F zu6giJ2j6;d|NhVH2Lli8{pH@Hd;4dL%7q7CKI)~-y4&u{o4>$=p&ZOY(kqUqG@gOYAL{9KmMlC9;}oqtpY zrXG0cd44#));2ZU0Xt^fJT^a+-jS8?Xw$<3y6%HNY;8QVG!Wxa3D|h0XJdU1tV_*N zz1t$pZd!Bnem`BS%qL+TyE`vGDgt{~fjuAmS!>(7bh2Lsy4RWUc+W?&kkJutiSgO75%^we8Uyx`^J z3sr6^zwIN^qg9~Y^Zi@ek0V-JTl?ZM=0lHGfECXZZ&^8xq-||0%22ik4MCD*+kWU# zc8UI_)NX6*zr(-JJX!_@JpaDcf#VqA);6%(tLNWcyeZ=zWjqu|hw(VJxwTO|xdhU{ zmpwP|!A`ffFE2WT{3r$Nc}t)VX1lfR-F9!~Q6AXw9M1={;o5fYaFO$mlE9SbB0fm; z);8teS%gQ69v;wj9|V1C<9X6;3c!ZvHa-f+POcPM@#M?{d!F0)DEwO6-ch$9fE~|m zd=zf2ZD-DHa^5rnxA9SVwYI61ZnLoO;|+lDXx766y6&UQYHd7Ex(yC&cy8mP1nJ~^ zJXW|(2H5l5#zzU!+V+mR4F>FZZsVf_Xl*-lZj<(=3Al}q8lJUHt#+IFo!^<;pdKF3 zbsq&gYvXy+ZBoF7=QgLBcYdtf%<})=y>V_5_?F%0fZG1g{oHdS0Fp7sPbOoYYS7J9 zBY3&y_FzdhLTp)fl#dwj(t-TM@bc;cAB(`UY^tM6%Y07NiFI_Iz{zz5^NWTjb+oSF z?ty@Cc20zeqq8NpEb<8_@wqn#maY2qJ*GdVSc72wNIeIb~(?`sm{GeNRxru(Z z%gxecOh4XMB`r5rz#n9VZ8#j?lKuO906q)KzwQB6?L*o4WVSBkV<4HvQTto@oFtr zRiSZ*w2Ew{7fuU#g2B|gzXUy^rCO3=Ld}*)m)Ww|iRhR+>5@vS5-ygFiBzPnw8KR- z(>C&`X?5U1h#f{cWhdE-CHrCp(CZWu=8zJHNw_KI>k3|Nqw7^&cwGmP_YIvJLof2a zv9Wo&ubAsbUSu)ymc-JM=$O5{K10zlEvp!TI){$wrTE{EcsQnr%)a;UV~%hZ4*BmW z?ekpmsmu8fhvRXts$i+J$9iq6mP%A=l$}%(=}0JB>rLn`9go=tdF2YKDpqUGk6eh07h9ht-1uoz0=8>e_##<3i zFxZZZI;Nrwu~hAfV5Sn}xJMZUI+a`}^LW~I;OuYQx-r<3>DQh4W@FP25B*&w{STaO zqto=9Argi?_gMO#i!8eT{eH@yJjK3}{=N2oYm64E419c1T=rgUfW2iv6Ez>ZPg8x8 zVJo*Ev;)~0csN9}>3VZzyIpCn;?_&oUkuB;edNI5s8_IvDf;FQa5u_brHC6J7NrKf83MC$G~$SP z;q3gsym9d@7r*$zcU%ab|L}SG++Uu16}ai|4F}KN|Ni|_;FkmK!0z66?S1vx-#ps` z8T$|KUflVfo$&UrZh!Te-#pXa`pni_w>CGwZS%^;Pi~aKmuKlZzPkmsN#wx$FM;x4 zLbNmXWGME#u9zn0`M_^(Ow67imxH*&7hE@98p72YJ!z}?0ujx&%4vgYl?}WZY3Ipy zbO6D{xOqJTth^Ff`H{6x&=>dU{ajq8T*rXLtH9z9uWj-6zL}rv&N|oAz)~1k`k}Qg z9sPRiC-dTU2iH+=x*n1P?_1m0%DuCN)CfP>8PfHXhX!>02iHb((|LU4T$tCBz>4QQ zKd|;W`V!~yk%6IH{~BP?bDsCEZSjcn_{hEJTz@67?q0=BD%b$iHY@k4*=d4*ch}kFW0-;9F<&Q8UQcSD|h9Rd1Z3G{#W+WMD-+ATZBrF}H6hi}fx zJJz;yG&kdR#>8cle=?8yCR>e+3xx7Wl)p zjrsIQA9*O!^)L0%fUe(I8_iAM^pRa+UjGtc#dDr-TRUT5iSzi#HBqjAF|g=4&tq#_ zJmNe)vP?SH9{`p-=efSNr8}I*N2UpIo|k*m1)S$w*EY7&c@|PZ{A8m@*T2X^1G@g` z+GuV%kB=M^^ZI3A#dDswo_w5g?3uowj1%ws|9uGc2{Og6+p8uEg51;#|bMC=E z9E|rrvp)>{ZNT39n?397C(oL@f4yt$d}2r2{)_Dn$op5e{%Px5w(i?}|7K?6_ke;m ze$*#kxJ}da?Wf3bkK@}dj(gah{BTj9c)^{( z_PhmSOJ6^3594Rkv*-FX_Sy98Id>X64)^Vz-Mz7EPds~DmT5nkr>C1@dF_eM^9{hq zQ}5&l>a{1H%@v?NYk}e?DUYAx^v6VE%J{rA@ruPOmY#PfY?)2B zhfZZ266zI+7f==FQ=wkIPZ41lt~bTx-KRgI8Km#_#v)}}WG|G8xg(MnO7*L%>LgJq zSFM(Za9WS)-I!w+G)bhclqI39_VK11Ln2t;l4=ykRUwg^R5EOrK?EI1u9xfUb$yop zIJuSni08w7j%gz;4sQ*bsF|@+V?IoWEVOQABNKWI4LTe-&est7Z}(>MX!>J# z>UB%Xe4pgPb{QONnrzUq5gv%Z|k;Jl;<9j~le0*bA7ITZ|o!(M#^mc@&Dg2fI z@zRc;_KSXE$nlTE40o08^oJ2xp_Fzaf3;2JjiMNKtd<&5P!{S^?Iw&F6O{w$_h1Yv@(msZa^|3LLHtv2iG$jO0kEV2)9#uW*jY zr4ZGQ4MV92boZ~J`c>A<3vo=hLo(j%bgho6l=xb97>*Z<%_yaeovR9;=yi~GN2`>& zH6afT2W%{CTL{v*+NPsrM2y?cAZ};MBNFNJSSGn%+<9HUng9P~8*kgV_{2r#!arYl z_l4s5&zyh9`Rut*oqO9k^xzW*-*)iT`=8o>_dd73ANc7&FYv;>pWoB=o_qGE0e}C+ z-4E;vyZbxu-+9%}&h`&(bK4tdo;bs7{qxqlw~Cve*?h-l7BKd|?e4x#vWLd@7A+rq z=;S=`%gkUvcM=Ajsn;h;w;Vyr)qy<4YfY9_1QyZs`kfEPU^ng$Mm9*8lplt4nggHEYu><5~h_vEsd&X z2|8y`8K%$Vvxy{o*qW;Su{G4DBH*g(wh0&dDpy7;Ejk!4p>A=c)X8MGQY;U+1Qu#V zdZY#%OrEO!(KXb%H9^%gakJA)3}p(_v(A_rm*p6badx`Xlo7I@w-GfFVKDZvIaT}q zHPmt)yz00*6&@K_TQhUXI9AT*%^uI9m{un1Z3!%6csSQG6!uV>s{N5Q)QVa-V`;XX zBAFs=THGMY2q+8-$%stnvrTu1N=2rHxPxYqJrt*Ee|QbGR+XvXBI+{bDx0if2G{N7 zSi39*6;X+|9g59Hg{0LZKy^KPC`{G<&>Cu;K3t85TrrzWDj`D?d(B2(>B8lhDX?Xl zBSIZBJ?zD5?YzVuzGkZSeQT)oQS-qbHl}KSa1FIS3Ov}uS5MWhQM%%XOOri(XsULN z`V>Dk8SLS!rfSzHM)5;`$sRs9Rr`G>=jvY$(p)B8&y{R5EXKpkAm?JG7FS5sjbSi0 z&|-2JeyVnjs)ql= z-kZQVQkHqZec#=qA}BXKG?z`7BvnZzh+yTa9FZuZm(H*-f(W9} zEo4DFzypWF+ZA=aS1tv-58M@06ufWOW7qxOs_vQYsqRdwhDJWWkLLFa4gKW*{NJ~p z_o(-MjwKB)T2#*~!>R~f(BjQS^*l1HiZ}%=E>cv_EyMN(JL)XqZfiSLX^J6Hp9_>r zwrZI46f?E9Hyrf{VU&uqL89$5!gjq#^)MOsMisDevpdob!=922ZJR@8l5ydrGvc?K zx>h66^}#-uo3!$!pc4bTK9>xuB9=f)dP=J2lwnl_3~0$hN%b5utcvIWElDS-o?V7j z5eJ~fONZ*&WLOmr{aRdKs2(cAs_@`n(s1uX^8rk*K#yAoW;5JV( zfOrJ)sILY5@opqm<)ajD64NM@sa;!UyF1Uig~Wp;i~yPLZFRANO#HI z5vvVHI2z3~?QxScn`+V(G#hsKX&Z%8)i&<-VGck?x*lwlqzgijRq6cGUcDRSeM-pH z+z$p{%S{L44y2sjD~-E1vBO%Hot+zZ(012x|L_G(>x#IM%1XM-K5ujEtMY>FA2gB% zr<~p52KHRaSui&KfhlKU1Nf)kE#z`MZ+C{ACRZ!z54(9=-IJn|O zNd7gj>q%H80+eKauwL_KkAX!HSVEtqtFZ?@mR|anHjFj zwsY>3)$V4AY|0a^wS80&_od?=)D(^+J58#9nvxOV8QaJN%CK9sF!ey*?`tjZ*<;t6 z&1N{Ga)T{3UAq6DeQ@Z+(eDiHI%VPRg$w4tG#{V)>0EnmayB;e)J$t;V)};ZrKt~3 z*>=5Y@~e}tn)vm^zKQwqTgI2iJ~8GQ{SL?i@TZY0MqW7l&SB%w=Z1o_f1iDD^s4`_ zod!JDp67btpRET<`<>%Mmt3~1Pwr!jGPRRhxzo1KGH%hA_B)QGa7*FsJ_@(U^-+je zOZ)9dRH#f%-cO-YuAf3nX}|4=3YF!3`zch)^;3wH_M=BssH_m&PoYw-pF(qK|I3f4 zP+23lpF*WvKZS5<|3ybss4TMDPoYw-pF*g#|H30GRA!&-r%)-^Poc52|7Ay1sLWW{ zPoYw-pF%@v{{=@>sB+*VO1XXt!N5QNND8-PS?|-rEpmMnn!z}<9#NrkeDzbPl!%P5eE5h8l>@q;LZw_kg<#-AM^vaB(ESuD<@zZ!fE~)qj(V3;+HX7(+bxCe z`>@?2*N3eM3}V9(6)Fd3KZQ!UehR@L-g88SO4IG9P$}0>Au#dtj;Qc((=}TADOAe! zQwU64cSMCs)9t5FDc4USF!6Jbs8DIT{S+$Y`Y8l!+wu_=D%Y%j3YBvG6oR$wB}Y;S zE6*bODBL2~ML{Bhxy1^fKZ=C7Z( z%sn=D^U2RspGLc1y5i5m)aP4<1Mj@$!%Xk2HO5|d(NpJh ztrFoDt0K$SQr&=ytTJg71WDCQ|MBkc-1_+%cH1XTy87PVTc~CF zD<|Cd(Ado{-F@PVUV8tnOz%t?SVN4%8nEko!E3&I-LW^;F0Y;P-T%Jft`olhKaY&V zH+GzD{;y}%_f9jtGh|>5Q4VXsuku{|*zEalzItKRdDbtc-*VjFzjn=I7i2!Xkl7ThEIjjNS@WWsI{$mfe*d(tY0qO{pu^` zf4ZCLoh}1wh;E-%$xuU!7}mw)zT`TmQ%nC%^Jz5LaY8-CI)>EE0#fANNYV|u$~ zU=6VjYryya^1nv*{_lyd^RpLy{=j{|ecwm!z5b5>eDtP|J#ZZS;MUu(Lv_0gMd`=8d2$FcAK0DDdA-%r(>-%b4Yji2LsP_%08g zJMr?urKWG4@VBr2&Svo}Jb5DfhH7v(`$gAByr+JJ>76PAYlwJQ1D?FOG;@OaynDXo zKI2sTuNP)d-*egO%in5$!AC{MSO3nNJxuSEwLVrK^RNc|K=Yk%PMm%9+e_`=o^k3) zyFPc;Ctu~fYvHU{KK|%~AHM2;fBbx=cd`tuA?jfbIKFtpedh$e|CiTZ>75$=+zZaW zyW&CplyLvP9Md~V2G$Vwum)W6fAJ3AxntkJAHDjX8^eviN9q@mNk#-I4^#~!?V@5-;PT=k5<{%bE8{lwe8{o31^ z-tjW9hUkYi;D=XV_ifLo_MvAwGhcYmr+duBuPD$c{u4*ycIf%oBistpd$A0xA^u?v znEJQTFF*F{@dJx5ezWbG-KDGl@|lmkGIC?eDg5GucU*7|^b4lE@n zGhwfp3hPtVq%|RiOe_&Vjd>%7t^uF>fJs-{ z39n&#FI)pw9|y4p{Fm`7=2sRzbnW+kUl&eaT-clXT&D2pUw!wb4?Ow5FF*T+zrUL4 z9U}v4h=f=JHgA+J&-OHEdLrR zSF8d5H1?lR_XR&^xQ|}_)hk|59{<&kF1`4gU--WMrF&=Jd``j&>=>j$1;850RICBN z?B+KLvI z9=+`O;p5(P(J_yH@6&HE?qhl&Z7BfOP-9{ZI4Hi?{i=t5_tVcg-gP27^x6xMWn$@y z2lR=X{`BNIOJn!Fhv|WIqySh$xra62-Ya%rf0q8kAN`4V)s@a;KXK<3|F+xmgBSe= z|7Y%zn@)P(M~-EBAk8QM)({o32K>;MKXe&-_jixAz1{S!U*%xm1*wyt{Os31^Gnl} zkN^1bTR;3Byf-&S0^ozrDyDvM5o>VR4X0e-d+JZee5d=7hcEs5%ZDZ|fAj~<*PU|9 zFHib8_pASX|F`ho?BQ_zA|uw|O!qr}@fPyKSAFF1D_-q!#bUQPj=6sRhTtu8e}nJ& z#ojwk#(Oh|!}W`eSc5YMPk;2jx9##h@wGzYEmwZz%1cJRIP|lJJU_eb4CirsW>!jg zZ+Z*dw$Tx5aJT>UPyha+cRuke;)$yzw2cA(!9#{%fHhK9Bu=;&&IWGChzI6FgZBF%oOQS1zsW zy7ob=V7TV4;s4`bd|C5PnQKn_*Ehqq&wlgfFmZ~T>48+209ZrP-!$kpm-qbD6GyL?F>!{FIp3javc~KKF-g7qj zl|P;H$n%oSjUe@-)D>=$_u7LAi?b5fH^_|N5aU?EX$mnZuY=+faTp3z>Q%&{7uUO@ zzI*BP|ND*irmsD5D#V-u)_EuMz`W%%*=rxZD`R;sihMr4Le|7l! zY5&v%Q_kV}$z}k!Cmt8G@Hc zG#-rmns!&MsLSDS%wF?Hnuy8iiCC(%r5tEgb8WH^MsWkZSVJSOax+W^o$WZzut8^9 z$>GWk4m@JxJQahzTZP%6zU6ImF>@-7nMohmQ;G(*C76sloGxTqOI)^KvbX4#Jx(X| zHFLhfH0vqU!RXsmHydbn(3rt*C6Xp6=U;R*D(*s=%0%pRsu-@e$-I)oB^w-ycm$>y zP>~TfBv7v*Yq2%!r9gxeM8W0ey9R&8?{rk~pgy~lG9?>@EL~*+HSjocoh}mhdb}0U zo@&PPULj2d(hWPE#LS{|v6AQWq%Grb)$Bov$@+qVlEcXx9J-9ZkT>uG(qYS#lM#Hn zFxR#dZ6@T+;!rAw$6+|`*N2?sk~t?vN;wqe3bd0dgxpoM>8!hQ)wbSj=bTK$(Dk>4 zh7k(#{>8GF_SEw+DnNTlm#5v~qY4hwsSOUDuHTHcKtFH! zmvc9LNv2V-*?CsMVd%OI4y}~k84u8gcq!nu#Ne>WRmbsAMr3_ZxdA)_GgLd;?CR}C z{gS^Eql&gj6A#Bqh@MEa^p33Eo%Tlcxl}zP(vD!*69Ri+o6oXHMVwTW&SqTkWX6$B zRq(Ka!|==oheiX{3vhwA*D|q`19o}**q&Gl3&CuLtwT=A;w^V_3`Tq4B{oFk9#g}G zbTZLG*p&_G8!>;%zyvA=XDO(6p)ru1!38oJS?I-37;iT04YcAT$*RxZCMpUJ)9?m| zYTe+Cmq@fxWL<4G%o8aij6xuszuj@V;sKEPLzgXhxm+W*L?>FQPK0rjx^m0lhFv0T zuvs&uuGx|-ab}|(%J@-VE~U#_=|!8^=@uhIArvgJWk=rT$SOEY{^uqRs&fl0X?SHG zoF`!B^&1>2RVoKjMelyy^CZK zotWg^nQFvtipBz=BIG1R!qBosDh)G^+v90=k)ioimGTtWP%2Xuvni5M^ux>vn>g6o z?tm?hCyNd%MyV7#RoJ*D>muCbMY%o}>5K_TWU#sT# zN8un%$EaKq33LlFtXZkYV9O#+p@ER2O0+$xa1pb0(6WNV>?WHmR*em>(}9aQd)3H0 zn+UBc+AEpq7;U5I%-aT^ifsRr(?DL4#YztImxAsemHP0+%4&RZ6UWHetYX2R)2 zOkf#vz;3`?3{e0%YQ!a9MDQ7FK~IE$@sh2;F}Y?6@i>fRnK$MAA<|;a`pOMgv#JBR z9;<$`?C`rnPG89H#9JxlnlwJy^qI)u)kmLPVUAVB6wy z=%Ru-V_C{zF?XutH`at!n6cV*Mz&=&Rl+rUz}`hrCLJt!BQ83l3mDMFYMF`Qlow-N zL>lY5x>4nNF}-htLtfD3bK#P?)%M~n=tZV!vWd1-m$pSHcP@Yh(iXg}Yhn7@lEqp# zf@~y6lB;xx24f9TXtUwA*AGc9SMfnhg>#ZL;a;GVN?Fg*+)+i-5XioT~=7csqo7$+ie(F<&|i zJE|d*-!7!x#uV9%Ei!?eALa3kuR{gmDZz)e6dcAL-r$hSa#g#JV6#4Fv|@qGVXWSA zweze9JV~3DkS)vUI9)6k4;Plag?Q8$M00Eg^M#y{Pe^7>TsFhl83ULB#A=;%`NLVC z*tFCZV~tEQkU@*p29?ftXnQN5;4u1@4G!6mDG3%Ug0y52pOtHNQt7gf>$*5MlcPL& zUzl=s16Uam~}+X@PAJ8JGO)M(Y+USn*LulpPU zdtLMwTwtMh)Qg;g!^ozwnPJ)}YIbJ>=Bmr;Wl~JZW55GYwo+|W!$g2WsvcvfX!8j0 z60IwS7{-gGlAyGGFw`=c38Yr$oQY)D#bR!6G8!xDM6iAsoQuA84r8g5Ga}Tp7*S4( zZ3Tznmt8h2?`emZ+Q~>JZb*lG!5YZ%Q*J<&h`*5vIEoHh7o~-u(6+(Wh($=*3QHbu z+=m;hFp`g3(~Ow1h;-dq4-}n|k~a`bC)~9RT%f5?C01Ch1tSbr;>&rSkC5d~i%>ob znaueZg*<&u!WGdLm=dDM?I?LM?gry8UyjX1F;-(i>hklu#1+$DcJWNQl@ zU50D%WnYu5uyJ!GW1?uG>x=~0kk!yyj7LzS>@M=%4ql~srim)Ia`T&}hb)tJ)FP1R zr8rL>kGR|IR;N^U#X&wL!khzHfP5t^S}5DY>=Fe9i!}-l(&eU&L7i1c$5nCqa`srp z>qPWII_HMPKn|+p?c!oQ(Bf06a+S7|)v%YeSCx-qerTf~+Tn~T+6PA!sTr>B6;q2PLSQKji~+cG8Py^xcw$Oc{}QNpHg=`Mj7n zHS>YEe-X90QyDs4aNE6wn!lE$>k4z2yJLex));Vz=0qJNaN|Mlq?pr9vb3*acDO7= zO6;c6)fn6|$NZhv5*baELb%%)qmx~SV0Q$FfPss`{=BV&@qCI3MUb=+3wXPDVUZ4% z@i^YFWf(T&W+)q`+{(?Lb%;X;iut@@N876dp<*^~J61H%6&=_l1Y%}GhK$ixGS|wp zrho}u3K7nT5!D;rO(y{t852yn%@#M+&6EmsBP+IYi596(-~rM~E(WuNBcIE8z^v-4 zb{d$kqwI&pLmb*IJsd`INI1eoAis|`xx!#8LNp7$2-q=#Rjiy3r7E$w$ERQF+ByWt zM+>zRyeFwg1Xq{Q*9^G?Mmk$4-gcBI!mh7jvA6&)a+nir`=fNU5qEfm0#_+2ILsfn z!J%8su^i4dXqK|H*rY#Rr-*{fXKgZNA%}Nb0lytZLY8{nxzxhAc->Dq2?L??l-h1D zZYglpxX^8f$U2jSItI8AgecaPwk-ODMu^H_uBuN--LsRl<)qB%}A;%vZPHYW;J!h#uFogkO- zp==st5=>-7lRq3s#KmUG6)hF);S80kg$ZA^uH4ejy;!>cUmOYyuK%}x^0 zGM*ay1wZj6o5mLF(;c zT<0k3Vu3g~8P6m<0gz9vg@{W!tR2I_D`uc!WTIp@m-pdbvpW-Z2*FHPz|y9ikI0$p z2BY7y$m4Ers^+S78>woklX4JUMX&BV5YoO@{DvMW*$)-hc`bj4RQ>kX49WbQ>E=(WQgQah94=|bu?C>L?H51s; zN02{29ah=TYBC~vj1_U^+x4NF1S>eJH`}8XPPw;T%vEy^Z%T+Zqc*+kdQAR42nXNh2-c`;bJJXFpt;(KGHwLsS zdpg}n9Ig!9T3Nb9f{QC2|3)`H9c6y4%AStt<64zHooD7dTv@+P+UUlo(=xACWlyJN zp2L;(B!@@;c6nCvvN zJH0$%nhEUi^5oJ?U`LmyeFvN}0i!XJ54d7j9X6Q@WWFP!Z6;|>M|9RuLA1gyyo3}G z!A^(u^iDqlhh_ph{0Qut3GDDAu*n28fbR4oKs6KC;YaXtnSh3^=T2|bFOmsp@IGDA z;GQ`+1NFWG7iuP;!HrW5ftP6}puzK14S@?}0vh~uclr^$R5O7cegx-hCa}Ygz$z2a z0J^h}V1D+GLrY`l4DE{U8e90l!o>?y^B!10{%)4jE>HnQR zFrAp5o%+}myX&s0U6Ws&tW2If@xVl5VsZST@y_@eV_zNHJ9f_KH%BiU)s1`?WCiez zJb(DU;o9&yLyryJFt;F|4;<-Z_K%~#Tl+is?afD$eC}`0^}usI@Q>{Qa({W0BbVG| zpTQy`IF2UEJ~EMj2#1jNL=1?{!=uKSKFI`@-EJBCTsRoProD7Ca>yE%;JKUnS17i_s7o z_i|#~mcux=&zDasZx_k^c?q)+G^L4(xmK(BVc3Ef>&bQ?(g45tl407{s3(z1BU%rV z?u=y-YvE~Hbd-x7h8D9?N4KGP+b8$)5~dGoo6uN5XmQ~-YqFY~Ru_YGn-w#UB)Ua! z6mnL$IygGzZO+9~rtL_xSw}_$t`0ofP1?3#=H&hiCOjRyNoV2_3S+uVki=|8zLKxx z8YJ*S4#(XdK7m4JgsUw^F;pmJl1wx2sFU6XxZB%;nUye`Nt$KKdb^k5Qtf0Q=Ff2~ zxFd}sO#|g>HC;(7hVp_?DcWO;foK^^`%+@dMN{@%FfF*YU}hvtPY4Cwmkv2aswt#k zn=_hnVF@e4fl@m$L2MS;sKaWuq6VvTF-oT#ji7_~Q&Df+(LsaCvpjM?C$~LN>;}u` zOdyz!+MPNf5iAitRPT02SvHPj{T)${=98ha$xbdZIba1vu|y;zzEBC2d8}^XnU*j? z?EubK&Fae@R;ZXeF}6u(t)?_xsI*PpEZ5}eBonf-WX=RHc2dzMM~dwr1M*_|+-PB| zEwK`&#o&&56HKxc%M^9SMAT&q$2yrvH;-1qbsmaS3Azz-)U9+HUgYxuzL9P=x&fRJ zvV97A@^S_ zVQQ%CFu0%tx&IXsrUrlb!CBw2{qfCDrmx5P;A#Nm{;0%GgOlyx>}cfvh=i#j3-#dS z)8u|e!Ynu4jEAn)3Z-Dgq_bK~fp*@M(fOQ+x6-Uqfvz_OWqlZ&1y_dEh|p|ewyfat zrfom5%@Y)Q%+(FW=L+k-w{ejL>p&C^I09Zm&%ZO*iy;Hp}Zq# zfKZ#2B}zVX5#`XuGUxS&y!EK9>EZ45Iw2^pE3Hdw$Y70+X!t}*xF_gCDz+*4N6je@wxTA8N@Y``d5Qq_NH^MKOxNpnH#?$`+QQQ-VS-{|$w(a%>t!?1P9VBg2r|W!>6k|k zS0h#vVW1p^sE(v^G`m<%H@H-^M0MI^glRiHcBSJVx!)sUg8Oe^4!mJJ@9Pxz?IG3 z=)p9w8seV@Z^IO27a))_bh{$3=54DnYY3RyzAS=P07Vw*?bE;l59W>_6j{2N&(?BH zrla(6l5()txKf&{O0`w{BnsJ*=1ck(E0*e9sV-*-e;R3E5&bim26kKGN1F!LY_S?Z zySFU^yUduQ;dZ_n_IF@(SuEKDHH5ZDt%e3-b~ievoS_sdxN)pdEF66r*ig1(izXwD zhCL*v@D$Dz@f2+(_K2b@gVLyDkDbVg#V(KeO<;va%_2wT$|9)5Za3?ps<&%fZf6N3 z-%Z8*R2gH?w%4Jq{4y*x9i)N1g71w;Bk%QzVJ!iIJ2&#SDZji;O66^mB(qg6S?456 z+gv~VA2d2-l})Z%(giNwNaHm(XZPTlWGWjDM_9z@s-;s%DpV^+TDf)& zN>|!OQ^{;-LclvYz;n&6)7yZsl0B=cxPcio9T@xTMtZ|j9C_L3HNx=DzECUH8L7Na zy|+ zvHK6Ij3Csq8w+inYu61gMaSEW+Hvo|edL-+ODpvz$_Uhe-N43iT zi%T0#CWH0-WFnA8MadY$u$48q0))li>4u3U@6?9`%;C)X3TC#xjJE2zwqWq&vNWA4 zfMb>{sC+F#4cJx8R_#qkQq<$sENx%*rXrSDI*{=a%R)xwk|Q;JZ4?e28ig7xY4EYu zfKez<(%X(g^FQS%)DXq~&p!&yJ30zsBc$&)3hS()ftHhj8VH6=B`blFn9qj^LMW7t zzLt|A?4;9Z?}l9IkmsmJVLjrBN4bitkfKsSFYT(M`cla6Zsd$`9mK`rB~Lic5t(u> z!`l!Ls2XP37G#2q4ul9K^D(%SZ5T`riJyUtno`@+{}7ia!7^QD=0%rs`aGpA1fVfumTw@ufk-P0#e{ch^MsavKhQ?98K zCw~Jx0B)WXCmoZ=PyA})3lsl3AxzjNUNruT@z0IlxDZ-6YyN-czdHY(@xu7a$B!NR z+1O{s-aJRloj&{L*@tFt8{@|=96M(8snJi3uFSS(@m=5Cb;qttN3)|Bj6Q$lCnKL6 zxo-Z|^TGKu=KebO$lSZ;IwRSU3rCI}{>AVYhHoCO47-O<9s1MIL#gu>FREvck;B7_ z`B=Bgr}G@26IwxAqlGKarKqq=EiPS+;EhzRRP-y3!X_3c&QdFS+QgY^MNgeLL#^m36H97E zPo6kkt>{S;yVpe>J~U7lI2238sB)|0RQm5vy=m%8YDM2T^`KhOYo{JiEBc11`_+nG zGj*R@(W|HKT@xJ~ z5GWfCV{XQu&nN>%#&(UpP_5{~*fDBF=f_^4R&;Lc`D#UH$DXHFbY^UqTG8pT1+}76 zWAkc7C&%X0icXBpsudj{n^_lSb6hy+&z3o%*7Bsug3^g);)N3@s1-eC;&`>9FPM0- zTG8iEyhyF+^Cpf{E4pjq*mY5g4BL6U67oCh*(kVUu`4I|H%{EFR`lA5o79W`t6I@F zO!U-?-l$genu#0Ki@sT{=+zUeYDHf^v7%n|dbOfgOZP|xJ?XwI1o zguHkmm0^?@xC`*Y`Zk9jC<-mCZ*vBV8W+~LIfF$F3+vmQ!J_(wgWH@?!@;%lppcp) z;7hd}UZtDHyg2{b_0Ae7TAJUlR#ce3T&-wv{xY?qh53I`E1I9bRIMmKzi&-+pdZ-m z59SW8+=E=kW*?tBxN;8^eQfUF$~{o@`*R0Z?t!BJHFt339u(0$`@Ok?%ke0e^@o0b-U3Js#jiNfg0I@J~`N3r@sYhh@;D`q<~7< zfI3WuLu3@NpjSzcOtPSRRBJo31wo33WuLj?d}L9dV=nPfqCsn&L63o*P3W*r))050 zCF^>2B@Bq^qz-teYGQTZ%v`IBB~D8bgIBJEfTlgtM)S78@f_=2u*swcmNm$%kU+sj zl7d@RE7)eUc}NjRp<)F7=mBNS*Q-{7^1}p*-l1Aiw!zivi6qMcuZ}|-XA{_!8^Q&4 zOU|HItppCLc)My9huO=aRsa>-7DfQC9|X`Lzka+;wT>eXMP=o!E=n47XI1)xW=ISt1as`YHPj?1Q3@<5ej9vV(0RIAcv zD9IIG;sTnj;n+eomxCsg13Dy=(QrolF4W|tWd3Qv&m$D9?4`hoKvXQ zqc9mxdQ8A%b~Fvr`abl?CZjwBNv@O&M8zk`=H6NK!;>98V)8@za(#GGAW=> zG8ql06RP!XHkl;QBbkhbg9+7o6eja3=`jJ5(QxLVTGf$FMtSU!TuDe=K(jT}H&o5# zpvk-v=#WfC!%2kdx8m(g<`SS!G8qlW5vuiVHkpfo9?4`hoJ6SBqcE9QNRJ7ajD|{v zs#P7?WR&L|$(6Xo1vFd3NrY-HYbKLjiLH07hVuv2`|-_Pn_h_mO*+sTFlTnMG*&l; z7<;K3jUYl+AMUn!H8uI?{_?)8vn>cM|mz0SqVuDHuqq^GX&KP4$iS8Fk{IAG#nbJHsh_fz^w#Q&~2ne{rlCN6A5l@<|UC zEhqIT7Z*IRy~{SNh|>!SEtY6o+hOjw_-KdpMa@1?ZHaa;j{9Y#O(Y&D~n=o1~|&C4^p<*RT$3pJGD8+PGZ|;MQeVqP00FSJha4 z`g6=SL_n}AC}j(#8QG{02FXjKWNS3;js?kE4Q6A%afmIOO@T(vRDhNPNn;MqA~7o# zAp)*yD1#d)D36)APlcm#eLdTSsH+c z2uMNVZKi zatID)QjTQB6O0j|fX4?18WxswMyzo<@3QOl!m`QHQZ??Rrh{>JRiQT~jk`Co!&;V| z!LJ8u$DMw=ai`%%ac34Ztt+BpqrCZj>scB}jT*=cBhRt@e3_Lm?b0l)*UMA1^xZ_7 z%Oo2`DG!qrq%Lnl*8tbg>`O^ON)Ka_{<@7v%^KXs-?m*_fx-<5Hdzu&JgAADNEZ2| zq-!&FvYD(WrL;CCnd2HC10f8k3_Ue0zS zgCAVxrODtKT;>rA(s#O{xg=pn@T|vgYkSyY%+!sfT@4`(E!U#%ESxXp9UXhU;Na?D zd;^+ClZC_X$=Yg}h(8%;a-6Z|UFP)}HlOj9O=%SM1R#47AtTZSe7+&ey@d(HW5@^E*z=pV7igs|6e?G&Cssg!ov&a&A)N( zsX6cLJ3-F>$n=M&r>ByWADo<<$dBJUe$rTd^gl*V9qA5#b65|)u=78cE%xADL&MxT zR@JAxc0xUzE=Lcsoxk?USj}~kb>m&QiR8tcBdGGnu0S|5Iv~H8TR?aa*VQr z3dck|$BHdG26-|pI^pVB_MFuk+l{mKMq|f%KYNn_4&Xt&+wZU{m z%bo8bGZj)?buh;fiCt}v^jSYwHOEtW%e#iA*Evp0)4P_d;B|^?C!m8VO5Dnf{CSsZ zhKs$INWHje0h-W~V*XGst}Q^#IF^q(qoin0inLURSLtRe5BB1)M6x*7kEHTa^s;k% z=SUGuQ|6l?QwF|0KTrTUGO2>YXj|J#4URxGrDkyJ@fwRS@i$V>Y%RlXKpc6#r0iK%X5a-Yz0Q?kzVkcYtFqXKziLgfJz`m}1AC-im$ zn&hT|KCyAB*3SoFo!qA3J@7V5W*2x|>cn7FHM`xtMTy;E)8@f5Pmob<`#^|wcH2yr z16j(GCH@16rT;?JEKlm4Cb8T!35{*^VLu;;b(Whw90p(N%af#6C-L*XUIV{V75qjI zHf4(u|iuWV4B?Cy&1`~;!T%s@~r4LfejvWY7eis$>92m-5WDhzkvphh+i$;G8#>0a2~na zZ!X8!5QCY##d_7$&O1a<0jOfL(BXzyh4o!HXUjvyJsnW4Zuef`rmAWL{NXW%P^jt6E8l=oOs4vJhHEn=QhwCn!(Al5A3flebi?X}hYM zGn3Txy?d`-?oCTG&PwO-8E4y}vU4+zY3p^_=5h1SI^ztO4xW)&PSUBE4mR%@pQY*G zxqG~WS9H((Jzn4Ga@#@tw5Cf?{uG$owvGl4t2M~h+f`T69Tk@|k(zZ+Fkdr*Wz%P3 zdBj*j8(64_`C-S=SHWO}iqTPFqE)is%ut3jEHHQJk!rpq^;vuV2Q zH>`JVx`dyF>2kna_KZx?+stKa@0ZWgEVib+mgCFP23OwB0P|1twQ@1pS$8DZEnWZX zjCz9=cm)^}28#jF>vg7od>yClr-6RP`E;fMgQ>s2t%r6}K2s?Z40Jis69nZqob8w% zw^>S#ickoLJ+%&;alnKvdGuvCf&l{Y8ITa!Fb2ULG8=Ze(=Luhd_|P8f+El@tQ*bd zm&5sbS1h10zr7UmS7SC9Vrjt>V2uqXRxEn)YBUq0@??v1@OFbul>>lDYWm*2GgIdd zod)v%&wP32lIdSfUpk+e`|r7{=8m7eWA>#ZCyY5qzcrd2`SZx@cU?bx=lII_>0=++ zwR`x2iSoqo!iN_elf=YhLk~@UYWjIow@w-7zd7{Eskdx?eFWg*=Kp?%f0qK@$*GZp z?|UZ12w4#n#cav~s2dUn$ZASCaF;`orD-u;Ek(n1U$j7w1+h_~(X2vZ_D@Q6W;0~f zSEyD;S-A0#Ix`GeEO8;=WE5bF-7xDb+2eLs(Bl!)C{N}U60^Tnsx#>!iykMs{;opw zkUH23A=Xyi@7&V5qSH%zR>&D7?Mp#QCW)hkU9vc)3@z5 zZ&d-dn8G8#kt3RJ(z$dP6-8P}dG>2t)L9_J;%VD6%GCdd)WHaZAcaT-Ri?&Y9NfC^ zY*E(_Y(t$HLd>yDA%iPhH(1@bx2Wp}wxP}hAtp+w`<2CzRkZF~ThtjL1nOW^MOpH8 zL!ALajB$>NDQ#E9v-`#tb^X9L)b%qa73#jORA;h47E`obsFoE^=8!r`0w#f{kWyk1 zPj-EE)*Y|%zOEfqp>8Qh=Il?lQ@1VHhB^eYK%O?4+|rqY)%|#jx_)3A>dcVEh-PCQ z<(#3Sbx$bOnJtjj=&9qmO25wRJI$$>#D2R)9RgVmm{`gvvt%D?o!JCgtf5*P-7>2T zo}z!FR41(n=3+eRq5HKC(L+|4bmDg9e6ON)f83(3-}182IxCnp=^E=%E>S8Lpg(L; zXM(KOFdz2S`nIlL`&G2=&tQFj;u&7SpLO^xU228(MxP^AclPa43#`|7OKdBskXWp< zF}Cgw@=0H}=F3*2MWxZr{za+IXobv%K&LLI6<~)9#%O`edX}q~;tGkyR6dQ{)3#Q@ zm*jIHPsoKSBxWDnqOKpiU5lrq0r`=i01-yNcG`zeOFW zxQH;$OttT`Gs2L`OcXoej#6EvTa9;7p0j7MaH{MJkXtrL_bJu&-ToYE9at62RF^w;V9HSyhU9pQfb#T8oB^7T9-<9wGN5TF7YbTEf z|9bA{xgL0~2X>$bUiY%zycEWF?c}f)U%kP;d1^f&tXmJ-{|VyQHRP1pE`0WokE8OG zeV}Ki^o6JII&iRjGzB~cPm{vuPXiZBr=>WqbXt`IC+$*AX`pERcR|X0AxTL`@`JBf z4)Z_Qz7FnA#lM^Mh_!EEoG*&oJQZ6*A7P;7B4V)-ad!o!`Me*6c`+g|W_#6N5{gLi z=+DJVt!ON7(PsnYpgz+T;kF}e3E`%EH5esffiGdkBJ4E99YK8yW}3^zXe*O%G+-7@ zyORN<$$?bk=CrTFHOW-e8p_yHLK0CIhbuQ-io?zJ=41z^%BK6VeqOADAD-G_YuP$% zH@g!&BW^f{J1{*X*I|dNIKoZNW;ZNtK2ig2`TDu|g7LUnC&A{Nh_@c9!Mq;!Fi4n= z$0JFv(`j-NMaIrLf;D|3(6&~jBrCGhl)__S!;@J#2mOQGmktq-W|eIs_*Kpv+uy>)T$%6O2fJOD(1o!dw;--eWK}1w)i;5KPf;3^Z%eoX1)BafY1z z=&#}lb6enQo?^CRblPJEyH(6Mt)?p4#>>7+Hv`cO44YFz42fHbc5_dp#MNs!q~BBT z!WM?y!^9k7+Ai8DW7?W6vXN@T<}j(egOi#LuHxIhS!t@d?wCzeO+S0xxv2(uZmK!t z@wuZ@%@N%D4x4I=kIETJj&!7%b4oE9L!2<2G5Ji(b=n=n$`Ft4!R)yz z$|96EXZGV%5q1gHqtEjL$K71j$U#QF;%l0h>sB_LZ)6IjH6Rx3)nYCgaT0{l)^YAJ z!Fo$H7|mr(?J!lWxXN~Yi|9nX2FO-%X1t-Y1B&I^0W+6V=`|@eeeYiB{r}XVFAnWm z-F4=|#~1MV$L3!-_g`}t&wg(A~^oYI5GCn7(V)? zQSZnDBktk*hF#$6&+gB@1C}w4BemqXNopD!@+dgl15q?zav?s znp~uz7lee+8l1R#-vLA>pdmB&P7^T81T>_|))F5)INo;O0h49|JDLc3-vL-NfgMf2 zz3%`d6VQ-7e5W6QQ8R%Zegp>11a|ll=w$*LKzI5P?9ohMhabUtG64`C8+=o+ z@4#-&1T>_tR6}4!Raw8oW;M!A(yFZA;kg=RCmyb>-&w@YbmIwHl^uOIO7H*Y4c#-e>%+S) z-nDDto`vMX%>0MvFPFu;s4p20yXWNEQJz>Ghu#V55gpK8NC6P~hqQPz=o&y;L z%iWlsTJ_7Y52=9V!a3ZIk)$W&Yx?abAD^mLs@_aF6%$*5KsIPFh#m|>C|#YUR&g2j z%PL^&se&zAFd4;!%j(G`VlYIpn7(Up`%%=GjCBcXRUaadjM?C%R(&$;msG$SAaBFe z$dgQ!s1T4%=Pb0-DZXhem9yDkh1OTShNi=qM@@P^wd$2&A6)FDqD_tz+d(FmFZQdsv}j?!?$8owb_MbfMBVb+cTPr;|*`%91$~Os#rk*auX=wpyN` z3vZ^=EjCmpYgs*EBaAQ~iRNu)6BkXnnhjgr3?nRsQmbwm_I?$xofL_innaSe;*Lzm zN3b5Ymhc#?t^|(fsE!}iJ9C{%TSwG9)G8*!-lqcAN<$fAxsI1z!Ab?qwkx#BjRYcQ z617yDS#!&fru|X2EVB6mwd#^#?^OXCgrIu2<;Vb5;v%09@Qrk{(GB2)kR^PXEsxA%a7VofCRvJQiaKK=>avAnolK;g zN2`1}kK$B?Y#e{?{ z;L)`zR=h^7+GW@;sDbSid^M{z3MVK>+l5%`sHx>>WUwNZ<*%^L=*O~*H3vpnIavjw%bBF7-WTlBiaaOa`kl4Z%3?1Sfo}_8TNB3U}IGu znQoM0klReb`2-*22t3+$c~F5DXo~VXBBVJUsz3xudRJc#V9Dvv4z9j=XjQQyM#SJO z*-A>^GivoBnd4^$*JWJ7tVII|(&42kB*vf!(;)TsFs^fyb+JI4FX=J~Par26TZrgg zy-?=(X)PR0MnsRXBCd4d^{&252K$sYFtats+PBn?_O4zagZ;Y}FtGWtbc+P1^mg^G zzElSLq!uu+1wi5*M@Q)x?Oi=z2K$5-Ft9Z-+oKgu8NuOQwaQ>0*8QVQn_QHRY8rRr|1ri&TGVxv$Ybs%tqT18~okEwvQKxJ3l z#)~#KQOEcI;fZn%5p_T|F>8q1YN=F~Z<~|BWT%m(R?RZ(zo~%LP#2b3HOa6aRROD^ z3@o(@%dj6&0jr_pE42#Aupd?dtD$-;wQ7`MKQ#DiLrcL{YSkdaeozIhhPtfOs$Pcu zfC^X*Wmu`zJu>Y3RlsVv*r8U>lVRVd0#?H%6t$|8VecM%2CF6aFSUBE411RfSPeHD z)atSfd#4Im4X5?g>PuwUJ5<1GI0C0u&yisdsDRaQTu-f@EyKQd@R_Zaf~C~zSu*VH zDquC70aL4I%CPTI0juF$lUhAPhJCjRSPh4T)asH9`z{r*8V)wStEU54a{8Tv&jb!_ z%1maX8E!ONA!UAFYIV2F@wUN7Wm+BW#_c%BKc+H%5?)c&5*(YZ|x+_2Yx!L_ApPUWtdU*D#@FBb- zX|QE-e$;OFh75X-)k6ewbTw}bBaDkGI;Z?=IvgQZD29@W3h!lkH)M)J-dH!2j9@}D zlFPMuTer-cMDhP&?@i!bx9)oJ8P7h_%-pO^y51Y6ZOYUg%ezycktNxdWXtlZr4+}O zW!bVN+mgJtY0@^d=|%%IBwYzSmOLIy9ux=#9-(Z3M-~S1Us=tLD~MtDJ#wG1}phIscEfjaZv2Zy^b)ceh3)31h_b)w`otejtJ3>FlLit(dUoqv=>Lde=4KcZ;S@5G|`e7rfUhKUK8ii{s>bi68y*$#5Aynql9fL z(%g9OR7S&}ZBUu2+~`Qg$SEsn+Q|+=vSL|2&8liT*g9#%oV+Jj*tmTfBgS-bdzADS z-9&B}$Y|8qQ!Es%g650iX*aJ?l^E7a)|grwePqgt@A}+iq~vR05yN4??c)$2? zg*9FOJQc#yL!rswB<9s?f?3LOjMQya$Z#`f_v~1`#)^7BO6F8u*?#LZMwFD}zP2^) zJEDOXy;P|It^&~|ierZrPuvrsY-ZG=87Z%Azwr#KPJ@ZplgZxD*Q@eG-La!WXee}a z%x`c^Pwb~AsKeiC8rz$l!m2zbS%;7hvJUf%=$O8XJSx7x#`arC?^!M_2{r^k!nECRBPEb-O#Gt zY)i7-T)mgDxBqM!gR!`1wrI<(S|s14Ixzt_1nCU?m6|!sTV}LAkfX`eD5j-sGyr47 z4AE%yRejvy(v;V0R5XnCCPvw2+tN$XWXxX=_Ob&?CPSW;y=e?R78$0zS*Ggq#=5*6 zHKK_iSR0Rt?pP>e?k>~CaQ}FiOcqwwr!i`}(#?je9$-~QcZUpcy*V*ifG>W*ys){KL++)3_}mmD}OYN zQOR@ZMybVhGtp)@h>e9@SeGRUb4qH^gAKJnb4)}CR>giWU6?Ht1BbiU+Q8S zun~}FMoqOSkC>MI+2a@`g9a;7!UhOu23{hLL0f^{V49(YRGj0DniBW#deY zM#`PQ%F#Mz6I7eaw6(k#DdK!a#`q@i_d|2_K#MTLasR4wCPt{>2njV@2CDB*u(g=r z&5U#$8x^EbvB2eh)n+$pM%5VY+MdSX(|xj^uIE~VTDJ{siV>_n4pg#P1`qSz9szW- z5#*%^*|3-Ycp8Iq+O^Ukk{DRk5*26dXhRNbepZY3^EKdkPzVv;qGpPL&S3e>fWxJO zQb_{~__0uO``I3zQSyGw-QjcjR136VnUe-m2M6uKH98{$$688<&^#$6Vu>ZX+SQn0 zrr-@$l-^)etND`lFggfC#lp(Rr!i7mCxUx&)lku)!;Zo5HE+Dz!%eWEWOs6vRJ7A; zh3t`#@h^QI#-OtOj+XKEJHcX-=WFb+lB4Guq@dDL8!>(l*A>oW`|1^SWA}Gbh|aAR?cV?#mYs}pX-;3ZJ{H^gJ!W2 z$s~DIuk@&FqQ{6KD(w$c)5>7&BU2eW!2~F zsR1cc$Z4)0IupY$Nhvn&r`d8Mnvj(dV;Q+_G}R%9M$)0Hx_ey75QBl7T00XXS`;WN z!*>+b9Gbcq$T77@tz`ohN$SrZVuBMN~yc@OKOTQMVfQ$GeQq zcuH7Lt$O;pKsLP;7SfYsd-*4)F))YV^PYZ@?8|(q8tiqm<)nl8pkGgmVY8Ym`b^fX=Xu_!DUJuotBiwme^4U(gXUnRH;rLiQQE1n zPKK>lA*dQG#tUWx9(T?lD{*_pdtAb{EI$k}}A+zCH9c-TJsXCoVo7%YS zr8LD}zhfH17{zMEwmTPa79iU{oSu{EZs}JpDFPOv<(ok8xVH%^A>~)J+ zPYcOI&c_$!k>VK9YO=r%Ycw5H68S)7<~CmQB^&y zfo<27ro#y>Rb-;+mZ`*q{t#2rcwWtyWm6l}siu}$JyU@dTB(Dv+}jxl;0S0i%;Kp= zlgpL$f-j%1_|&LhaSClSUZ!2+X*OgBC=IERT0K-WeWgyVT}#A-hF21Zp)X#sv+l8* zP{ayOO?yO2C?vB(jC=h6-Av zW*4bEpUw^it5xZWY&et+MOnfvWo)lxg{7V$xCd*$vT%%Fp2o%%GqiFZU^>B#}F18I*J8z^RoQZNy1D=D1_xCibycu6L0wWg(8YX_~YmX^Glo{0DuE$CvcrZW7wR;!)?^GIgUc3119(O|r)hwa8%*-qaZNDL`Y z&sX=BGWO;f9n?cvfge(#o{2YgyO~bfeJ5Vb1$=bZHv6$MXr4+t6==upO=)ue|LUcm zU)p=;?!WJjcb?wqqyL0j$Ww^9{ny+2){|Sc%|F?!Y&@~?4eP(VF0Y+lE3W?5YIfzZ z70&gmE@t^raQpw~ztR zHrb9@YuHYNZ0;L0=eKn~74t|LxnT@6nlA|c3Z5_u&04n;sbw-)*r-vCq#VDId&j*m zpE6h=)%|=<5Q>5j&YQx}>x*)pXj}KG@p5nkY!+4`EEt2z;P5|Nug%5a#wmjZQr!oS z8|_LrR^VgJycI5IhLX=+#xb^QB|^e5O>{#<&8X^PD{IeF1~*I@ERgDcJI;B>1uq*& z4Dw<3knZ#_-fp(lPPaymy{$nfSShB4T0ftki^0pL3>HXrKQv5-)k54KtVP6H1m|ia zcWBVcxig}btb=1ua`YD42Qr&Mei5BfN>I0KYw5_(%uq)$qEMpa` zczGE1P=;o}Q4#K^>Cd90Z!)mu}GR@72Usc?*Aj(NR+|)26nJTx6@5e!3^OBxH%HaH6p+Yr^Qf4 zB|NNFv5`&F`wN^$&BNg8DT4)4UF7xBJPfXyGFae1Y=41Mu6Y=om@-%()hqib>W+K& zrVK2ql`2PjMA;X{u(}oU)EFX7HPeBiqw5JKLmPZ7Yh~F)1mBMaI$#;z_9iV|$PtBF z#vM)*qf}p$IovFya*hz8JhecFYc`KRExP00-6?|wQeDL5?K}*2&eX#Ej&v9CXFCrA zbjn}>FS(02tDT1dGG(xU3*1FK(aytQd&*z||F(;`nVpBh)|9~l&TAJrN1lhl=9IxA zZxk0ea-N65#+1PVsV?I0bsh%mQw9s1d0*KVYIoebHf6BD@%WW}A$G^Tt5XKQa0OdDW(8^~b<3m-lmb^S-Ml=FUE%EMM7nI%7ADXf;CKsWv(AJkv6Qyy08?Zj3va*acyC7+rEtqh;4b8LkPeJns;aeeuQf?+ zV%6td>Ad}>Gu|xEI2#E}{`CmYpdcPd?)<+UX#M8#yN=pzQLNE}RopglRep_yf;XQ`8Nd;3kTHYmq>+jedVhdmznC$;Jg1hB|0 z=@0s87CXLexOI^;)Zx24+X5Kss3oX z5f2q4lT!2bL@?5n`b{E}%@4em&`;LjN+Zaz3#UPR}P9~S5BZoJR6yP25txv2YC;G)`5-N(@mhMuR6!bjmVear!9&az|uRz4h);yngG6EfG zs>~CS3`MF@zHzD=3fdmL9+FyeqaGcF8i$cEJ~aG;xWAYdYW8Jo9;Fmg<6ep%Gzyr~ z7n;?q0NfCzNwHRoY57blL<;Rnt(rP0`n%&~yoZV21nx76Vb&XH#dw2ta^Z0sT+As{ z4-cxwK!`^U$@$y`g+bPV=6z%1zP*X&y}z`)$nbjQG!MI2^N!G^$)9En^G&@dPg>f< z0)j=p4XKbR-wqgYT1ppIg#s!<1U!TIPJ;Ubkp38_d=zZ3<9tZxwKMD#9VG; z4+XJH+U2V-k&f3NP1X4hW*0`m9--KwLNp($(W#DEWN-;<`h;Ai6>45~V~-KVo~rb+ zjXakdXMK^rmygxPlqbk(TB2w&z#^BeR-)N7-;JoggOpgInt2b;mXl_1lvG=?tvb(kmnHG#1mu>V8aLr10iFPkv&nuVRK{Xw4xu(y_QoYcw z$>z9F%(1z4H#w$xu|O4Ktva3t@6B1+q&NjP%ve;$MOxq-z!EV=$YPW^#K(5Jh1-Kp&*`-M zmv32Ku4>W&I}#JDs1yfTap(lozIeOK1hb6K(RqB>>D0%n#m7ee1F9MsMKW167HA;|G-m6_H) zS*kii^UP8QI8=hI?x@|<;8F9;1x^Mg2`{o7gy9$6EWK#EWP#(Gqvn~w=(i1am?m7l z0xLqp*3K_b9d+nTc%1bgQCC~7_8U(Q+E%LyrzdA;R7V?czswb05ChLMfYc+mGh&5m zI-LJ_U zy_COKSNnZBL?39OQHrJ%uTN(fiwvbJ!+~?i$^x!Os7viP_!M+pVhM@!JFam7ud$*c<1xq&G1f zN_{^Tt%vO*U1121f1D6?n;5rywm%o0&kT8Rl69bn?{)6GdZLIQ>@Dvv(7G;bOgvH% z{Vz&ITx5)%OlZ#!Ig=spP3L@t*k7Sh@En23Cg>5)N+wg7FXIkEBow%$`Ty8BEyY{i z!~op-iv{qmK}O2FVd@-LkEr#)u!E)i)pUy|3YUM9R;61BsZsO9TiHq|SW5s4cHn(p zQBYZ+fU$uoIn}6=sD&zQ1-$G{v7J2ACuuX^DHqZ?9uJH%Mx-%l0SjR{jcU?-kY+M- z?jTIEes2AL+Y;K|{Qb3*>$c~8>GzVr|EEg;tn5#nUD<`i5mdovOKYx(T1gJ7i93~wp!Rv>fz$kqnT>c~`l*x)%3yV~W8hua!o2o6Z3=WG# zyb<+sLwP)8dY!tz=wk;5VXxjYN0?!HBO`DkdXVuAiIEwRx3qPck0{|p5^`kKRKt|~$v zu;l|}9=WQpx{_4ni)1WuXn0sH)r=?($=DqxMyWnL(gon*GGoKSjnDwbbzUG`lF3_l+vqE|=sgOtPG`|QGay0+zkr$pqUaWLL zD=wj>!Dno@VMkxFxJ-o%Mk2w9h99YrwXC3mkDbK!hYZuaz4r!wCN39~*`$XB0J$ORhJb5=-r#q7ut z^2ojK1>R?TWk$#a9#g%z6*BNlg`AunUA!T1*$V0RUUCDRF)P-PRL3L{W@>ydn|Bi3 za(5u}=7>H3W^S^{OvJ&c8ZaJq*$TzB~?ZoX{w+rcj{{eThx#{xH=Jr-Etx!Ps*Qb%@5N8ECJ5zG=6cvp8+r*y<_Jka(F z&*q+|8T=7Mz9Pp0L60|lsrdq}NgCd-sl2b&OD#J{U$)+n9(vSk6fwRS5WsC%g7LNJMo1k=?QyGejjXkVQRgy%m z2#j-iOu0#xV-7e&7cuNG)RppqP-U#t_{R-?uExM64qestnllghlq~mWgUvwGl#eI``vOu^37nz@Nk9l%Nc=4iRDy6B8 zCTEwERp8~WLj3+qcQ*&Bmy3jW9~(A!kq8-9rk#`7aX`cA=CBdeDghJhoh9r__44=W zBKf93;kJ(-nQ_`8+ihTCGLRjHij?eOn^-wTvcS@H+3>LyTPS-X4SwKM_$CP~N;`Z= z$91xnj%Z}L)=D|ivR#mi1!2x%evQ zy=Ve6-N(`I`JShaJy=uCly1aa4?KaIEuutx%U3lc^x#S7QkyjcQm_&YMHdGg| z4RzGs#S8Mt5xz3L`1Iq#BgaYick$Hba%VE(aPX4-={Piyaf(m&#F5%k60J^N?u;Xt zDM}QL9XJOyO66(tAX{aq%U(E%HKUtI9uTcWB(GR8x)~cCn!rqYpc~~?qO1|Z% z4$BcO+KB6>p$r8-%UC(3sVQBtpi~HgVpFXsdnk&rNFg4wB0VPt9&X!}P(HzH{os5z z0D@tV^{GC-;+#G%(59ZVK87!;k4Ks+dco`C0(Jvmc=~v;&H**M#0>y19DU9{xH z^W~0@p$K+Kee9+Rl@##n(o}p(cxanc{=leOzCu|f+LqFa6>6ba(^0dv=U};~s~3W{ zZS~k%kkInVfl?9L38ij}jF!%`biS@Al}O4+X1q@cjYBaMVq#>^<_;na zhRZ39U@Lc!bK;TE#Dl>cCKZB{tWWjv<>&Nqfj0G=^$}jbI`XXTNLxNHczs;pSn|cK zkJvNx@yb4c3jv3~peHzT{XaUC;=V|S9-0GBT+$D#P1T!~$4;y)*wWZx%8I{V?)$t_ z_VO(V3|e%})2Zbc;DoF|CUCWC^qSs&Gu~$pnsqazQhi%Wh8s$Y(mUyp1?+)$<6LbWH(8ukirDb*X^wQqbdr$2>x%b50>AlDH z9^HFn@8P|N_8#1OVDJ9Dd-m?$yK`^2*V(J@-L@z0@q3BATld0yp1qs*PVU{XcVcgI zZ)x}G-KTb++FU_!V_Dkw;tPibnB6=hqoTudT{H3t^2p` z*}8k{&aL5AXRE$-+m^V+ZzZ;F-3o7cwr<`!xpl+ViLK48rOl@|pW1wK^NG#Vn~!Zi zy7|cF!TNs+`M7)#O5aO znE3R@QyWhL4-BU_9@}_yF0S+IiS=98!|R^)o7YdS->`mSeRF+j?di3r)}CB@0{DJ-Z0*stN7f!* zduZ*!wFlPjU%O}R?zKDDhHIU*`r2)4;u^n}Si5yCyyjWEdF|xd4QnUXHrJL`pI&`x z^~u#IR!^gkqK}{tqYt4Eq7R_=qxYb9qj#c1w1d{s+fWhZ(FA%c8b&?n&FD$=2J{5F zi7p{e178$RB2OTvk;jlnkw=h+k%y26kq40bk$aH4kvowg(n0FTZHS2QNCLSP39sI| z8ea9R-n@Ep^@i0GtDCD!D^IUHwesZ36Dy}LZll1H#iJ{atUSE((8_}=53Jn3a?i@$ z=iBYdoh!qY&PsjdwrLAq5m)%#`buKu*0Z}ryY=P!&;I@rzHmxlW5wkHzDwT$kw)JR zF@cUD#?cYP7&?SVp#z8{+CPImhy?0DjG{I~9JL_cigqEs1?@omIN$ z!w}ce5X3b!2yqn+KwLrn5M8Jb;xg)mxP)R8ME(=?K>WYxA;kYc-vsgBQ8&bYL*EGT zU(o}I|AKxs#HZ0WK>TO)W{Cd>eLckgjlK@zKcTOM_>bs5#D74)3gX|RH$i*~Jq7Xa z(324V7JUuGzd>IO@vqTWLHsN9l@R|DeFen7Kwl2=N%TgDe~#V&@z2ngLHtwndWe65 zUI+1y(Q6_85qb^8KSZyF_yl?t#6Lh!K>U4l5905kyAXdD-GTT!C<^gQC<5_ubQ|Jn zbPM8dqni-Fh;Bgq0=f?Ix6n0+zlp9w{9ot_#NR+&5FbOAA^tkL1o799|AhE?E;twN#4Dr7se+2O%-N_kY9!PapZqO{21~p5bsBR8RC7&qY%Fh z`6Y<=BEJanqsT8n{8r@WA$|+;a}d87`5eT1ke`M45#$kw--P@O#1A9?1LB8}pN9BB zEZ{Vs?D^uIvtqu&X!hkge{2Ympdjea{s3;i_2F8V2m9rTkB+vq1Cn&`(N8tBI$ zw$S?_HqrYa>gcyYY@qi-tfL==sG;8q@iz2ZAePW?hWHKWJrLi9egxvzqu&Jat>}j# z%IJq6O6Uh6is%O*3h4VG7SX#Q7SQ)W%%ksxm_xr2Vivs%Vg`K=L>_%N#5DRYh#Yz+ z#1wi5#3cGoh%9>h1d%(ChakQaf%Svjj==gs-hsgSLEetQ`a#ABtRG~A!1@7mNmxI~ z0D<*`^buG;NDqPagE$DRAH+sr{U8j&u~uztWy6xI*YMqvFQCIagRF%VckNDG1W zgESFXKZuUN`av29tRFCsh4q7I2&^BZhQRtkBm~wEnB&6wK?DTW4^l*6{U8Md)(?_L zVErID1lA9dMPU6P83fi3m?6XZLDC4UAB00-{U9j>)(?_IVErI00_z815LiD5jllW= z^J`c?NF0IngTxS6KL~}u`awtp)(=7;uzrvz0_z9C5m-N9c0R42w;=C;_49Se+aZ1} zGKTmy$Oz(_ks-uekO9O9(uWvEdJsd112Kr$5Cez>(T{W?`j8GpFVcpHAtpo*Vn94Z zS`goaG$FbX9pW322E+rT4)LoI4dNS+8pN9s72@lWD#X_z3dGkU6^Q#t8RAzVw?VuK zDM37id;`Rj$lD;k2Kjo3uSVVq@l}Wn@s)@K@fC;&@#TmB@kXQw@dl&-@nuLJ;`K-l z;&n(C;qcYkd6+MN#ii|DI? z@4sK(-ru@o^Gln7jeFMrYCX309jpJoDy;nQ%8skP{Bz5%1`(d?&$~|Dm)rw;4{yjU zEp2TsudTgp^ZNaY%%#&x4;#AeN-mee`rt5*cbh%T63gy#N^^?-*r*%}ik#<+>G(6} zuE)Ehx_;UC@bCT7JK*=OOPSl9eLf9HvNI&F+xMmfUsj2?R2}!Hy#29TOJoOHlg*QK zB%_b$c1sS`vm-nqiX91GT49RQv-L%&&DO@DL<%;fLv)STT~3N!im;HgK)t;Sh%{cp3wW5?}y*dEI=|oL-MM9f6_mU2eDGYTT>b| z9|?s78#wQaV-#1>MoyXyWF(Tbqj@*AXh~i*q4(1tf#3TVAQ_t>d160ZN(`b($RE?k zY`9J|>Z4X(wgpd7mfXOuNzoKSo@$TD*qnb+k|!qge)2Q$`%?>$q-IF&?UP}#p@h-_ zrxN0lX^*$n^^C%4$;2{R-G_y`3R_P1dNp+vFDy!OZ$j@U9)sU6EVgx}HvB=H%N$ReI%7bS^I=sk2B{F)1p zymf};_9D(?7bUqpq4$HQ;P+qwl5d$IxwVMD*+ofiP3V1p41TiwClADXTq+OKc z=7ipZe+0izEx($HU6kbdgx+_32!204 zH_12N_vQ)7x168!t}WvG_6*63-zOeF|6Q9f{q(yh-|x77Zod2X-7?{Oc%JX-BA#;3 z@ICjOYYr#Qs}qKw_`u}*$+yhS^L6({COm!Td9EztPxlPZ3*YM;U!p4$j`w>f-;a&w zW_t3z@Pz5>&ogx`;&=C4Go9F!Ii90y!tUE%2fugB&GFUuh4z-V&T!nCEX@{i%zK97 z)ZfbS6erxi^|O=jM>pqYcXsX4AD#f{tfcPnsO^ zB`4NR=T%&YhlMfa3Cnc8gH@Amwp?zXBGpzcipTqel@~Q`XmG_D4qL`~4oTMIv;$S6 zNeF7A*sozjZ8*+X3)x=0+Lf3DF0_awJ@Q99`$Q;QBr-NNA|z`}Hd5nSnvRivUoq^B zjYa|RPTZ?U2O2>JwNtfbzbZ9VJClwPc&0gSj%GM)(C0aDKHC)BDZ?y;!vf})6Ts6? zEig*RK@UI7g{sUrr|@N2D{A|tc+70q{ZuV(6K1p2*K}}gGfwI3KumS8hQgK1eAD#g zwF-TTPBoOV)+F+}s#na4I^t(IxLW5q5K6SsF(QdwKHHU=`@n8TG{$sAwQEXpsnpbaq}(XkVl=`VeO|Blai$fuje0lcKV|bmF>W?G zdZQs^I->!Xo+$|ITsb6$DlickvXqeSS9@xbWc(SD4RaRZH}drcUh}$}N-&kxD}#N4 ztLBc*)j=y4h_F^xPFZ+2?oW*};nv89;of4pLhrK;Ug%JP8soI{ zxk9AT%;+_uTWHpUTA`t0SXx&kHP^-by62SBEbD!mO|;V~A#P{Ys6E4Bg*(q7J*=e6 zajBkiTJAw9JH+B6jP#5H)ik5>R9DYoEm}25wc+1y=(I5?>%10-n=~COcSzb$=(@p$ z1v3%v#Ogl2*$$>jJW@E7&L!$jGhWa;1)?bq=(;|`Vd?Sn9Fo8y`Jk$eQgYb|R~#vr zaWFY2R6~PKszEl#F{e+LB|S?N_TBABxzO{2g75o>DxStFT(=?!YTlb{V?nXeX62F@ zN_(OMWsbV|x$_)YHB90Cl0_M#Qi7~Yxa`&>ui7v}O;u2uQL)?Xw!}o;KiGGJC+k=) zn8X|3OuI_@!o76A?_s5Vy_m^I60D2`L^@?i(l`~( zi++}LSM;udHK=-@25Jy3j4k?GeXR>YxxF`H4(b zE)Uw_VxbplpQ@5wj^$!iS|{*3OYNps9pM%$k)#&!=|+3A4)u+Hp5y6`Q9L zJv(hB9YM|~D@H~h8paHV&7Jccm{c9yoELn7szwD^ zM;OQ6r^B%%9-|wS#ZocNP?-jsDhDaa7Uk#L7Fp3w zC%D=(ICvwj&z|Q%*O*MKZ}(a$|F;)szh;WPYH)=km6`_cz(s2h9?r0g97jahxrxJLKijP)p*KU*EDV$j8*~6*EHa}3K2=~nl_WdZk7s*h%hF}sz*r_(BE^bSYPXVZjq3AO+exc>o6V$~ z!!FrA6(vDWQLid&jT;P!Jdx98I26to1C`21!KjkM!n7l|T6timIwltKl}@a~B#Wsq zpBV+ht!9rK?00H@lQh#}tKSOy7@t5`!LBgWJ-n*u11;N*w1)M3+9bOr>Xefb$mUl*oby)qLgdz zhxFut_2|VmAqSjTqdTngG)JX8jiCv$&`WfSN~~M~@8MJSsUl918p#y9#!!?;cq=E& za9IAxISw>MrTR(C%CTBUANS%ECSzndEs&&F>}dgP)X_t}h{lpqcz+lkGr>{E-P5E_ zH5033h+#CkjFF)whMrShM{VJWR<3DvshH6sy15w+=zpB&5M-)x zqCx?s>j;$47>iD2obb7AZrDtSk`(L(qGF^bbgbrnvH(nW`p6M@l7O{G{b5DLI8)I_ zgn!_uzG$g}kv*;_YmOa0RZQ1Ycq3V3NBOi|G1&NEhQsP(=Qt$B!4WnHsNDf@5E-gv z8*;YVkupFN%7ia!t12BRkZn%|b29t!NR}NCm8wT8ikV(FGtf%uV53b{Q{8b;N*Z9I zA;mj2jR+1-l?MsDp6pRgU{yGd6G~yWE?nPmjsqPp#zg{*b{u8~>fKJg3F@>mdEW*d$xC2ea|@#iE%pB$`e%34(h`)RZK4AV?rFh6G0-;4UOM1Q zwbY?FlxfUhyZ&L1*MxMaBb_qql>)8Mm3mKXX1g_&EYEO2bLTmPo3SXT_-l4AoYTm- zEvFQ5*w%wts>8@e#~W=? z$MJ?qc-yCXxo9`9j>|D6WwzP_n|kKR?eVPTCg=YvOK(^LH%o2=aOs~9w?1ZVWHRNC zy?tx{j63dtC+PDBDJnzG&XiB(b1|hXlF_b3WGT>F5BoE@f5z+bp!aC=?LMFH2iokq+j4>3Ges&RBSRC0*JQr3dg7SGmi%EB_vSG)&YI9_i zn@Qf!g!;uO(E;|@W<-Eo_^Zqy9(#5XMou3D${n6&t8t0WVO&%62R)T|Q)t^Z9_nXQ z<*<^;_G3-pkzz)K*XQ>dMaGz2Ud|L$Y&6uzNhM9oV0?7EdeI5T()m^;6Dx=Gwv=kv zyi8$z!Q$FL1duU(tg7}^6FO>t>xCuf_xS@Pp|@t|?lUrFsX59;HGd)zq%qz)3{kUd1iM%v(v%(?8O9NPrp15MSes?XwqIp_~wTqn=)!S_r%DGjsV zaMU?oEIq?`(5nTBtm#~2VT5knC<*1`h2%gyxK7X15Td-4h)Y1i3s4jFPaW3d2 z`UUQFj&o3PM6(A)coCEH28y29oXck0bbCEm1h_=@mas4u@_5`wqzP)}LblITr3v>* z2CEIb(<+b5vH=U>QfW_u9%*vua`O7N+i$;n__6L}yu0U`3&y)k^=MaaygR~p%7tC( z{~P1oS7I>wvW&5l0dPh;FFu={w9E^Q*gj7K(-A~KCvf?`?T#LVE~OEZOkeIGl1(%?EZHf1Nng%Maun{m!3i)K{d{f7dZ2$#)6uOYXBnTS7?I2cxWtR8O# zNME*1rP)!X9wX*?B_HrivJUzl?PHxu-}8~~1%1z@8t|3tdyX&^b79}}JWPbYEFHr$ zr}JNy9`u-r@Rv>IOy6^ExWCAq-}BVxoFgzZ-#BYcV@`K|M|U-sQh?{u)%d`^+oigi zbPUI2%hthhZ@-G`Ev_Ai70Y?A1+IqXzzG!J8>=RlY7hNkIKjLe3)=_bTn``hTKcHd z2ZsO&kpkXz)1xpStcS~OJv8R3m1xX590G%Yxw7CHcB;N+uo3e*J-SJfcvQ&gJZq1= zRkp9nHjj0*)Lh=E1D;9NZ@+zev$vF6`k5u(1SO0B`s|wQKFh zcVD^l_dCD1^YI-6yt%&y^`U=`eir>OdfU>^tQxMo>-Ee3y8QX&2bS&S*z%1_f4#K> z-nswC=6lg~#^*Nf1@F|~ys^9fhwGnNf8V;i?p|No z`Wx_~{#&MyN+eD62+zGrW=m%M0u z;*D#WWtZzzH&@6dlY&kTm3X64kgNHbRomL?yH?+IHn85%GtpXVU=w`4sImO)naUO_ zqw>6gH&F?d<_)|7p1F(j23|)6RG2sL8d^k)^9Ei;3us~9z$<7T&CeUyh33%Q!CGc( zQYpukGnz%SXAG{94@ z?%9N^J+&g5`Oz>V6o(W|Ty#8$U{8`sqxyZfN%GjxOqY!u7H1yQ@6H=|!}U9^-jBpTNN(w> zXAI?MV;^^(xw`xA-LoCTv4@{4yYJdPH|8IgvI`h^x$|s>j|;qe$L<|xDUU6Aj5kz5 zi{~mG78qrkoq6oMV&@g}243BH`OeGd4ZN~* zJ(DmRR%4)-3#t0-0mAyu^*h(k8+dK!x}EFh4ZON@?asCH242~@X6KrD1G{#v-nsf* zVH}geovU`PI+JkcxFLTPIYbWkUbFPxXSD9G$p4%zi{skz`tDu3cg-7kZTCI9?>QTo zHo=8Lhs)HWv37A>aAvP?uAv`9KX^9qaYO#<>Yb~1&Kr1T^^VnZ3V2+~uGM$0zVmEl z9~XG__SM_ZvN$e(tLO*N56l~Q1${sI{&@qtfMu+^&r&{)2KqkqeP0chLCedhIxVG7 zB)E!BG`Uo22(H`CbVeK3T{myg^=q%4H|W|m*UTGq_3EqV4Z3pGRr3aQojCCZu;!Sj z1ormMRKl@S{WW0rp$AmgJ-7OvN%`1G==$cHHs3UF;I++z&4YOZuWr6!^9}O`UfF#8 z=IdWt|G%{UfA-ywm)8Ghj*Y`%kLRWJ{{@Bd()xdTpJ383URwWO&@f(F|DU~TGHDtw zt^Y4*8ZWK?pKY_?rS<;>P35Ka|J*B&BLbdT1suCe_R{+Q!oK>Y_5b98BpkI~TL1t5 zVg0|e^0!N;T-?&idsklV`Y{&=?EQPd+5c_89{)W%Hv@a^BCx-HH*ykK&F6s?{5!X9 z0M_dhz#9E<xXNB!0smpVYPC)toa9vJmRoRpWGc7ZlE~-zL(iN1e)X=cn;cxL2CvXl@DBoi%6J}Av8=cZ%icq23QBZzET6eKR4oXF|FVlFxTyG{W)hnMb10^ZP!oG4DP ziENILIY+gflV_d{oSeKHm@-#EeKX5;f_;zSMebPC2hWPR?vYr;AjfNONRjV$1GJ_3xZh&Z~}-Gpo2nDn|-g zA(>1}7=i1}s%(geWz!hXtCaK9x<8gg?PC3^c zCug<;7xTDKVAJ{Zq(6B4w(p)(&eg}snQhlXJ|=RxSRp?dkRFeJYEC&<9VchDzs|5k zl*WO7M0_IW&^4!=6UWJ!?ME3dp3I6VA_LnS+L%+$-f?nf`%x(>#nUk=%}pjIr~mrT z=ajSi>~j2(*>+tNa}*(RRCZEsr$6^ybIRE{PR?w51CN{w49gQaVj}0$^N!o-adIwd z2T?If#e{%QpOO2h6quRV{zhlC^8(Y0gaivInD0zo{$s6;Og-DN%!{6<4JTk75 zZ9lNYH6V0+kjir%IbQ5l<1M2s6i0O2X{nj^AVpFG$=hnu@oKBmz#}vXyaki^SOU+p zWFm?w-r`7{{5$E@ z1xy>Ha45qIWhAlzC0@|@T(mBExt8KfO=OKy0+a>P4(M_YaG-Ec%}{)jCo>cZRf8Ra zw7fJ7E1Mv!H&Cx8iX%A}jk|m8iWb8S;Ez1Xh<&P>>uA9K5{KpNL9$;R^eVN)n6>yy zrtNz33$MsH{R$OXEUYWkunr8+b%jQ|Tk$bWK7Le@;rV>F!lgr6z3NUe*?NUZ7szo| z&Ej5_>r~psTD(@KLY08i*4nPf3$V!YSE$J19Xdr}7HGv~E?+b=MZdusnIl>mX?sdc zV$>U0yf<-fmw_TwZUXi7)lwu}gOvc*`GG-R5 zdKFtHoV?U*A5mm@WFQsNSf7ce+5^^4cYsmGK&+{9-m=4ysyn4bBSz5OwQ2%TO#ch7 z$gY@T5}YMs0UzsChAb9M7raa^=Q~m{)gV)8##=PF{5j~dTA`E6)ahIV6QY`#^>y+o zGuknHPQYO^K9)K?zQMm1GQ7%bEWIWR~0udRJ5s{e@nGqR} zjMPh#8PAMoMnpzNMnZ#nm%NAOvSg|TaLGbPL7mpM9CREIL6jC$a2!Mj#6b{wNc(^_ zvushBeNJZGzPDcYoZBT-uT}Nesm%O--`-z*-`+d+-rx5dZ9zH2>4M|5O$3Gbz#2tf zz=7q#EbjPzu9S+MhNS?8f9!=b89I1gCgUPg3E)bEFm^D)*5jbl(~~ESWwdGX9nGdT zR5D(~tGLbf9L{O=G_4#DRFVljpUAu7Vk@qI$&eS&WY25(XZ&o$*{}`K#f<6j1B7;F zTJEWb{A4%8KD)<=`V2xmvU%0WekTJ#eQp~O61gCr)Ogs z{KA>6dTzr%S7AyLiMH;o1~Kf&oCH}<_Ocjhu;O5nEH>K^WGp9W77DzN)!Uh(v6{J! z7It?9sl*)-*o4PPhUOyiYwS7@Aij3c|bh!(aZl{!~%Ez@{8u8X7jmi zgMh7(q1NKXdcs+ZJ{m`0eXT#~1(j|sCp?q56*_Mb&0(*LAhg($Jaedtpj3Ara z0hoKoV1@GKz%`{8&TO#syjCe})b52Mt_HBHZY^TOz=b%bo)p>Jb+=~hq`!97JzTKK zaj?P%B>}BWNGqh3sdH7>k4o5Mnj#OJl~ z#zu>Yy6kI72a)RKVhFZDCH+ZLZ@XnI;b3b{+k=)AIL%&*aBRQD)Mm|dY3MX)60%2( z8ZZc$N8S(2_Kh!^*-Ys9?IbCXP)i2b*Jyx?1+5;0jRg2f-qsF>Lu$S4!)&O-vntRK zhtR3AFYzO3r9o>XA^a)EdmfH@CZCrMT6r>4N(MYHv(Ju8ZhQjV}i#7eI8O!OxN3UGDyLi#eW}?qA zNU>S4VkU8v4EQBwh9n`4pY*QZPL>uFj~M|eF>THaafWS*a?;h@B!`r^f@!Y{JT_bx z12DFIJqS|kMKhaLKew5*qpLkA-3-E(F=Lm$9ZV)Y`2B zNr!kCc1z7QXe?@_O)?LK9s|7XKVP|WHMz*@?&WV@THtwD;O^gh(Sp>(p4+;O!#4J4 zGnpiQp&{K3ILKN#PuwnPuQFKcYEz?L)XUb=G0Rp?nzARSJm7yXTkGw@E&Pm|KonSj z{Lfdee9Gs)aAw;)w<&?~5rv(ws?kYp4GRV^9gLzUt=s5ufcVU4a2 z<^??Mj{MN~<9cPKQ8|nfumIuD&i_B}XmRE4kKX;ZyTM)Z?u|P?e&;*xEbn~5om;nm z>h^ctu8!fO-?+_!+JFD*)?c~x!J~h4^sPsWTfJNFJN<>z?>>FwR6hNzlaHQ!&&k)E z7$+aN`D-_S;O2*JzJBwy+OBFEL(P;H3p#THvJxURvOj)&lUMbM;lQ zDc`osSr=_lmLKKaIHOv#HP#(0Cn*uB(}F^s89pT}$b8?gUs~cnUs~d0JBiUk>I_-W z!B^u}u5x{Y3NV-E0B26NG>TT*VOG3qi}-;0^gn-TiT`CM5edXr$gJ(KTD2seXkiX5 z>OMAV>i%%HCOpG7M*>-pbMfjgUs~dqE-mqkI|;z^smHNDUqT~lzO6gmX@)X$1-wY= zO}yncyY7}^;vT_~clOWY5)%98aS4h2^H}tKlNbQ@swQ9|9EIYx!Fa_I*W}s(jIV-~ z%-~tsP>iW{^2)9s-nfaS=0Kc8A;SjlvRI65j7dpeFna!I+9HwmcsVe}EAi&+(h^^C zX^H7h!j_iSY#6kyJ}4kam-cAEs?lx)(O62Ee3iGRD3P`xeB3Z~x@G*jSRN;awug-z;G#uo9jpG=20l~awy{+J`k*H&qvAksHdk;myi!0>{N_r$TN_s%0?uNP#u2 zMQ?8N$(HXMb6-hI2**6f7-eoeSbO22d1;C9r6r7=#L(ac{eU|AB_sKx&F-+O6^-*ah+&)i8!OTwLR>OjMWRzq`!_7Gp27NRuyU>!-J zY9@IoOGQ|-xAsHgB_zK1(mGz>N%Vqx zA*guXSrem0izFvGAR?QS8{3BKLJ(f@xndX_<9P};xBxkPu`wUX-FpY^A%;DL_7H-Q z4}R(UXXVPDew!6xV0+Mo9WM+}Nc8n5=<7r%-|Gpj|HkBYP4l?n{cG}J? zcawwlU^vSya3>Y16%)ctu{~c1?|%a0$@^c8oPJ5-y*w%iN^w`PGTcG}OU~!RxGI7o zAx5JWXn}-}b^wpimpp;72z2A^)JgAZ-?M(a8{rp5JeoOkCiQkAvL`g;6QWLfZ$wBi zfnvy0+4#Kqc0<%y$R%nek7 zoER4Ukw~u?z{Ur-feyv#`P1P-&eXnY>y5&BIEz_%LkiSY=%*SJFn;;C^*uA+R)wS+|zW0u26 z>y0NYwB{U-2)I<=koSaUhgmCFB?F$qr2 z=~<6w#j>z*)~myy^V)0N7ERq5!h22S?_ zGjL@4+#C?7CglLF?#Yp}lrrif1Y|IA)^!kfUmf26eP?m^A?x5W)6VmYJNQX)_nSVU zU3Nx%WI|h}m6J~+Fo#FKRgqK{PQLTJrxas(d@}q)pABd`&xhzU*{SWgvocx%zdVbH zx7H(H`jC$FtiT5N5y&cJ2+3zmJsiCxM{ZFGh(zS@*Pq}bZ^4DMvN9R(}jtE_3K5d@OproL_2uR zp;IGb$fY>}@qN~)_b}YJYD%YsGHdlQ4jYGUL@5z$3%Q$LV!{3)aBzos1W(ug{|4iDbMF zBdYn(Tu+av3y)U->ODsu(t7GdzZ^T2o3KbG217q$^2=_$lqoeh=DW$x+JxQ>`qLsFg)1-XrcnOtE;Al73C zY}Rw*OpxcAVlfLGGITbq$+4+ih;kYxnxN|-@=mV5?+SVF=~u4(%(cI8Ej?83{Odb^ z=}vz1_wKarT)F*`!~c5w8*Xd2Kl9el-TI5SvRmZoZ(PGqKXUpFr`qXfp8VX&Up&c9 z$eX`$^M`K!k4K}M>doJM_;rVW_wMgG{@LU2x~m^AkBOuIcKAL(9Q7BjCP!a?^ch#N zga2~yg9l%GAOnT(_;>dsN8|Gk{`}n!z2l4hf8P53NnieV`HZ~gyzlC(*}|PyqQM$a z2+^Hbo=G@1opPg1$^|1Q&H@}s?IE}=*)RdIZWF@~eswSC#R#||D*L9vfn>d*h^?Xd zxFYoo6+5-%8fv>W?k_v8C+q}|>Aj#E*DZk3O~+oFH;SHl^BGxnz|q1RNtm z!m8Y+71BE7;Omgrxg7I@y~b-lwHF*y_YMue++nzje7sfV;I7vjc zF&=SyjaUE9UQkywZr-kyk*-o>Ha3!_pcW&m;eqW+e=}5hd`4xBD$-}ae)uv|Q{J?|a znuU~Nx@>3&He4lWm!6P*xPKhUP7u&}>VlRRVKOwZMvKG-8*rx9=&?jMTj%Ov+cgod z$Dv+J-Zyo%*VxO8cs0ivpG81{Hl3b9(0D1C1FtC!6uDh3%#z8hh%omWPkVd8J~x+( zfuqb6W}XIZhuCbyV!U!4sfTu^LC4N+LyIHR`2Hm+Wl#rIAu*GVd)~ML1@RY~pVq1j2j6 z%trUoaq<@k5LO4TE?48CD%GOwZ(3TQ6S5d`6M|k-QX+355}z;gxtd{gBJQ0$-M4fl zY^_JSMi&x{jAG&(*-(_TZ3Hr%lfxVAU8Z=*v+Zm5;&1eFm zeJ0Tz!o_4_O`AogplZg1UA#Bi59D|L%wCW$3A-Sm%!B&P{%o`udL^{A`F>*(NV>1i zJBGoG389(qhnze1UeIg|gi#lTje^tgUZVL}XDjlnY(lrtwT&P|=2!wQTF`!2y`$`% zeACEyL>rfq5N@WfuUkq3DOYCbHiV7^4hdbP2NH&m+};lWcRG8GLz9zjYzpi4qTJd^ z*4idkH5g8yq|>C^y-ifEnx%z$tNmDY1A8bqq)bQTvau9aQ)4k{_Avq)6((DcAkn8q zMeOTCm0c0uF078+y`Z$t#k1QEbFsn%mE+Jgf=E%Q;8?R#w{g<8=829ZNT=Ax<1g+7 zMZO<95Sdjov>9(?n$wD$w%65oX_g~cj9sGN;-j&%PWOUe{!p+34;052I{920&R00% zJ8B?p49N>DakX4ZShKw(Og-9L^(Ma;?3oN_r5(IB#|@gGNt&}4YPD3H(j2B$1NYZm z&NK~1+iN_$_fSxgW0CIBfhY>e!WYVprRf&M=-Hymt03~|$Zwh3K-g=%`sQAcZx7o+ zlYzGJIGK4GN7hMe1J5P$QGL zHrBG1s+PdI`V5`6D6(P~#(wHJ`Zo^+O`6t|Ufi+>WkuCAIR5LjvBhLvl~H>`H`gOt z@0tdx?kD6M(L=#D4@j|FQG>2Q`tul0YIBC$ti()O0otApa<~tbIWWzB1~~ehy1napj+FkrCmwUj+y0_tKM)6g2%|`5;f~dZ?Vd8<@ zOyy}i+FSML$M%AB(5TqfT5=(nXSDHn-Q@7xQbq$|>hqvMWJ^kx(@ z#{>i!ch}0iTjhf;nsz50xezdRZ`9+5Ms2oGVli2|GURxAYHW?QZLM*Xi>GaC)9@Bu zm0B@*w~y=};~~D+2+dt+J>0ginC4}@n4lA*7n_rHG>N8tY*Ojbl))|2Z9Me2UwSCm zsWoFt&F2w2kX1K8H=VTK@X&r^K(*O&VF)uEihHH8H_r9Hy%%h>R)M(bxdYQTy;4|5 zk-+D(bvcGYY3%m-ZL;AeIuR5JU7)~gT5s?-(xCb*G zmknx)cC`)=I9iBF&9n^#oJnJDdKTcbd$yQ;-{rX{;sNk%q=ngNE$G(Q*Is zu&gmPMVN5|;h~leF{7m7g8_sty2EZ^)7=tGSYpR5DQ)j@*M8r=sT+>fU}cUXa$knV z%yelDr>;Dn8q6Xh2g_*AKZK^GGH^Ba*AQ+b!g@$Grd-g+ zZQzNcNk6J6YVWECfBB)H3PJcnX)>D{8Z33xoHt!&G4XIwUd19gMmblQR+?lfk7F%NKv&}?Mh zX-{EQ2J?R!HuVfo2mQpbS9VxaP8I}vtDaPQK`Bf|lWCQ}e9lLa2yW8RdfEwj7EG~y z5ppA_O!yJLobG+~1Y~Gk$$nFpM`!2%pLzHnu7JA#f8fsFy@TAI-TIHW?%(>9)4zH; zJo)95uRr;~?H>kr`k~_=I{xCLe|Pk?H$HabLpMI>`ro)dy7qUjjSv6B;U7Ev%&T8_ z^_7Fac_3W*`77$K@rU2`o7#z&%3fOFr3Efz0q2_zZ#eS3+gDy)4pdEstPQNJrX;sS z8w;0`Xv=9N>0%~~hui7IcL;O2HKA8@U!%e(O{AI8Q$UF%D}Ur(%l_c!t#jML`=+Zv z4+8W!BfsC~WLZ#pYq5>>7V|vIyY6ysrqy8J4PiJHIF3*oE9=5~-hROJ$8Uer0Z{ch zum6r$SM!keLadoiJ1xPVnkb@L9JB=&9x19hGq+L;WW`E^i^J@NRqgh{pSBtOXszTk z1I)hZ%2|6r*I}evVr3xfxs*u=-C)CwJzuPw$DRHf7uNGm zw|Vo+&K?tRo0f*S&E*Pg#}2cM@-~@<@x;KuWs1z#9$vw=CXTITY6aniRXy8n9xX+D z?ly0J=~;U~*M|tQ+E%PPw*@#$7jf<=8)Uh(Qg|?T$OV>bXfO5~EvjZO+@6PSvjsZN z+=hhtad)cE64Q+u&FQ#9kIdRNwgGrcSO!#Iooc1RtXi6lzOo1gi_%_~+d*H7J*nqj z!fk4x@62ub*kZdz_}`AEn<{jSk<%L=Fv*#JGWV%wFh+F%zLt3E=`2T zRWz6`hK|%9Z#n`MFJetqMaGcviIO3bEMK@iJGZglTmc;opo47|lCWTvdQ302y>zkYJ@SjkW+dy)9H5{1{@c*;mT$GU z-XpRXMt^*ueErP~XgTx5x3r*5oZLo|GkLFCyb=(aOlOwm6vgXrE+555nm943nc8@- z`0w!V!#7i);LN|@>OixJ!c82rGC^Iu*H%0n z0%PFI=R+W}>`drIN?X$vw-`vm*kMwNLFm+jCv-8Ndy5S|8_w#|yRa`m?hwwK5zu!w z1U8!nxGHIKD-^>+IjB==ql3*dqtQ~rw1sy12@w);TN^>zh4sCC2rPk~Gsojv5!|M` zi?el(sZ)er%20dMZ!pV-IhZVxhC3Aoe%h=EFfLs%M&IEgx8Dqasxuc!N@P7CyCm9h z=e@ZHvPWiXGG;}YRZ(@m9&84fJ?L=I7&0%c>hjJqdvkHt9?)`}Wh}>6Mee zdh#VFpK|m2Z!T|s_VGuL|JUQ!k3Zw+M~=2fpL^pcZ+z{IKXm< z=^B0b-wr=~7#x1a)t|Wfl~?J5Up@Hn!51HV#+4tr@|90}Njd!9r+xGC^OqKQX@M8l z0^%F_;p_5~Uy$*~sQzAI%8U<}s%x~hc{Iv5@}sK?>W%D>mA_~@rKEv0oi^aN^U$D8 z6vJMTW{EGw?L^m?@kHt7YUhzPf%wMq8Etwr{o~#7{_q>=;p_0z`uHf()GKdifH`k5 z{6=!f!f)%-+c~3leR@nrd?S8(A0A=ZYqnk~vXI`+dA>VPC8Mg7On=NK(KGNK-N?8I zUib{WM_*N5$%Mp^hbu{TX}}n;)mFpD+7~|}YosoGITjx1F*vq$j0=ndzv{kAxK zdy951yW~?g@nzGmqIq|m^w1&ctYU9l?K zLsj>B!}u}0=`--&{>;vwdKd&+0XR;IMLLodq3b;Q>MOqS#m~TdyTQ)!G?f}COfGGN z`6%M(np!^AzSp0Q_ZX$!D;L1~qtC#5Y!tfy-XD1e-eaSVIQFYFlfX9W#$lMzV4^pSeCf`2H97J>zA|o@9Zqx^@2#9y{_MKm7WGgI7~dYHqb= z592G9Y;1&O2@i`fb(SERWZR6YB|g{1jki|S;@!83o=~b_wMnLnvyAQcV<>T+YQF-f zk?}OmZ6{wO9{2`5Jz8kL79=1;}DQGs$Arqf)fJJiqB5Uc<$t% zeV&IE7v9I)x^gzF^E|Un(a0Lh6pf1$Q@YP~yt7|9>;WQ)pf#DEhQx~?}AO(3; zEo5|W`C2J=%u%AHs7}?=`W|U6J)*nDbJ8oigwqY&ZlR@I2@%RVcipNX% z)F&K|_*3H%|D=q^w97}rYT;Lj#HV>`EsFVe(2;Z9re+%wV&kA#a2t!Ld+aAoJT}js z(X-=`e6I0Gz~otpnI~ozSHf$?wrNQnEK9X@*;&x7HJPHp>Yi&d0lV^Rgrc*7^WS!W@m^r(pPnsZK1`a)~cMJp>xGLE3KyV z^r)MVhPAS+`2c^ds}lheVr`Iad4bI0v##&r@pIma$4mIsCmfI1Q{xf)q>M*pwUQUD zSnf`2`Sri}e%V_`$C9KLw*_;}2LsCdex3BO~0B%W(L;y7{! z_{4Y=xj`s2S(&wTW{GnJNbe~z+|!7t)8u8fPb#&H7%-COAhjNVyq8RWf0jiAIhhD% zXJ(mw2^9UaCaQN&A-ks9b*tD~v?*9YyGYK0p9n5UCK-hAFusS4bI8k$+O?VOmZqF$ zpWMu#)hv=|Pp;ChTI=n}pP+>I0UjZI{=nPHh($Vld`v3o2T@$9RNO zTv?4Id>EJukL85fwzuTP)@Vy8>8jbm6NVw#P9Z7XL3j@&bZ7~_UDCk-Q2beyJV=xWZ$1NO3=PwJc6#D<^O}1%FjPKx^nl&?*8ecqoY54H@w@rdvxcY-ubh4 zvO8aR=k|^R;N$iGLFNC?IQ_+=A3Huc{Tru$@^p06I(_Bj-=F-gW8~!PPRx_fx%vOz z{K1=V9)&ljHyg*le*DA7_m7THvJxURvO#1zrFPoEOej>yYRtQ);;; zGvA>)qBm^LM!k+WFTu6(GHJAlfn+0CnTL0FMTRvsH4|z)3tCo}jRp-_73rl?_P0G| z&4BBMqTEx`n(6Vbc>BMe>i}g&q%k!&#ksdJ+EY88)OG_e2d3KVO&jAOHw+hEz8S2B z+U*-7-5L(iM4f#-V> zDB0DS#93So1=-YlMO=2q&FvZ#wRYJOz5RJR3CYqHNQf=jLD&uLf?HCfq(z$nImUYte_4(9IYzK zi1G0FYjzSvA*8ELzbA0nGVWSck9BhJ(j{*veBmz!N?7!Q7*hqkINgJFfM>nZ;hof8%geB@)xTMHj z42qg<$ig-$I>ODwoQBzKY$AnEF|D;rb;6-x9e(Rh z!l7Gk?QBTW1}UjXZJZU4os9;pP>y27s%myRQMc}(5r|h?I|+*!Tj0`*?KT*g_Cvig z+N{@u-A;q1LZ~~G(kxz$6LnIZ?!9DM&1*yF<;Jptd9(wPcY)Gmi<7g&!JBQw6{(&- zZ+I>3_-l7Mrh&X_3|GOjzeZESH^*w?xqYwOhW#QP#ba>+tr-wNvpP6f}&853tVGm{$pc;ZD-Ivs&hK)s4sG74Na6@~C( zChTY@J01g#YosHN8IWZ{D?NE`R&933Sh>0wb!wG%V|SkPMl!nr(XnH2&^o%=@%#O0 zBM&hDc6)>)!jQw_ ze&y8Nr9IG~tmd>jym`Enpi1x*R6AcOObY5>wsa*jMH|MQAkdff+APKfMdOWr4IS_2 z0;+|1wo4jp(>v4AY}@r=ulz-9p7+LF+dgb_Wl~ zENk|M&A9AY@M28(3nwmgZxxZ(_u~Rtsm5$!)mYx3bruk{WErRo28WYy)JzLFE4J%x z)Eeltb+F^d(1@DpHa4BC!_sbMQhpEZMwo3R=#X2OJ=!SY3BH|KOgp{t%C1%DW~ri@ z-OI`oIoesn%aiXjEn=*vLt`S>NuQe)~6A{6!*04Esrzqdpy6Kj> z{x^3L&0)5ti6zIo<-}cg#!4?3a%e7lJ-)$jNK|zO6QNz)oOxwi$DVft{;7viLwza8D5O^?{)M=*`#&ykP zG1IdqK8K2@`>_YA$Wd2aHgbcSf^5j96_=#ZwPrmBH9CRN9APZTI~YR&bnNU})rb^b z?{f1QOOJ<2h2auWEFE?_+jcCD@6N_%)JVaY3bO~^xrDmTy`ECZDxeUd`zXR zI1|14?EL@pul%_ycfaeda_86Y{JA^g?f>ugpSjJ0DgfVdt9|+_r{8?~1t-67@{K30 zn?HN=>u#dQKYjd_$M=qY_UN0B_#3}|o8qNMcqWyzda=WGf6L#YOFr^o#(JWe*xG_WE86pe#bfN zzrFzMB@&3$2fzIs_UA4Ddx>OW^}%mDhyB?Lz+NKfSAFnl=dl0k-Lp6^ljf^F_|$XQ zpSb|+C9-+d2cL2d`~O@7_TtIA>Vv!Iu>bM`u$M^GRUh0rhyCdbz+NKXw)@~VfR#`G z#k*&gemw1xY!Xd|ixm>`z_*_7Z8p-3KSqxq}zb*v87Y6Rgd_%;*B2DEv(p~-v82b*dMt7Y(r=^OiOBE;KEA-55s7P zZ#1}_d9madk?Aps!PfmGEwnpTqwB3&3_)t+i41=J;A7Ed*82h8~cm?By0u145}azoLDjBDF>Z zjIcR}{m2Di(LTE2aCoXUU27o>TLfVg-~tH;v(ntFSA!lVn+#LXYJ*kpSLd)lbOG4G zl&@%uKs&*%Q( zA3)70gFtS%nZUB=BQTL!z1aZd2smKlK)qj{!~We1z>a*}(YK_BQ#hH(pd@7MG%UX- zr?i)GTt}VEStyXAnF;D!-d~-={@?{*-4WOBwnq&#o%S=j6(MOsi4seqX60MWeiHVU ziMuFPEhbd&7w52l=K`>?AfZKovK`2jCyTi9grR1jbBd3((9M=DSC(NicG;C_srU19 z*dMq6tU59b-7rWsF*Z$vmLNI}5Qm%LH9=AHR<9G-Gf=-t@m&C$ox^_r1z<QTVQdENjA*;D9gHId*miji`?oIudx@LX>izT__WLdXdx;C8 z>iy&#_HSJP_7eBpPHud`mEM()9UNV|@lS4i`;Fw}11H~eQ zdi^`Eudg%LKkeGj9M7+P_{hKZWhch*4_)it{DEulyZLK}zi{~7w?26I#?3!^_r|T> z>34&Q|MKByUH#~-zjB0M{hre|u71tQM^8WNs&VUmcfRB52af;L!LJ?sz{%H~{=&hB z4qiW%4_>?SvAf@PH@Hh4|ED`&aOc+TpS=CsZhz

    gI>;{P=D5*1rN92RAOXd2r*$ zuQV_8!?%8PzPHkmC^)i~Z4VNyRz98zcII_Avev}sR$0}cVsgLO&=p3HwQIXm3~(s4 zY*En2CiIm+uHx>{ZE{&jC7{wqu^i38`A-=UMQzCFl^u_9r*9&oByV_NjMO}o6184FnK*+q+OUV;zH3zdsH|RoBxO8l29T-g5WC5j3pGgmX zbti!$y)#aW2E~m^shiFvkijPQ^q#^6%jJ9yN5h~(R2>$>8{e>#fL9W!6<#9qw47Sx{nHSj~GY8JuoRDkuvBwT%X(?gXv8D7tl1!G{*FAb5KHedn!W zGjC0d`6OqoNCPR^bE6&%1chqKz60vXG>R3%cKe-b(9&*v*-j#^JFcEKM%Zd1k4thp zbvK~;?L+_up>?z%5?Y zXfTMn$TU6p$W9_mdj-_$F{xeAX4<2qn27-2;|Aw?oMMev(u5Qc4pO|MdnUaw(IIY>~evIIqL zCM~d`ol~%@wb5(6orF$k-oh<(RviwMjgd$5Y}_LWg`j)wh6(nD&73sL*l1KYc86o^ z2pkHx`5fN&J!;r*Yy83BVgV@ApOP&hEH??Q}D$sQaki zXpc6S?6F%&*YsTTkPbCy3_W(|X(BofuADEk8B?s5QJ>|eGrFnwgjtm=6gU|%nwm`s=ukhG z=rqcTN_1$7mNp*u>}n2Sy-5krQ&W_?jVvNd6&m>>(l_tyw({BeGDI4*))9ryMyy5_ zVANENUeKzY_Hv6e+9b~ddzGUL^3EUJwW^y-v;wMCM{Y~a*k#A4qv5hb=Weo5^Mv-R z3Eda#jwcqkcLO5ZT`syDI2|iDNNEq-{;Hr2Vq<^;-)Jm#%kdf%1-BTx=Lxs}>L(BpU5{U$zeD|&+9eus1t7y=U zCf#lZE?t))sD*;IK->6X4dJi{YPMQr!M4KV!A{8nRTivlPZmovu2(Z_Agq*%m{@s^ zc)9HbpbF}uvjXm@9Wy(LfKZI#h9$~`3WFjX^o#Y1(?=L0WJR*>%;uVDr6LASl5Xx! z|JjZQdY$w~(%< zu5@}n31@|cs;q&p6%+?_SM`i9PCDljonF&lms6M<&;zW~9u8aFwpaR`>x5z(RpyBW z3VL-ngd+=g{^?FfSDoce5u*i`pdFQlu?;(tC=Z9y89J1$wva26T4K<~8dZZ^K&W~0#XE_9+=yr}M*#|pu{{en@!D*y{8q15rh_#yEXT0jF(+oV ztxk96&7J1TUFrn05@pvcBeHLi?IK8JKpfC>iVYt&W{qvTy~#rF^DMPSKqYJIF2>O&@5W3qhtiXu+O?`DY>co=F>o_*jbu8Kx@HC{w3Fee zD2tBo_Qv4!pBXuUb-LL}(98sECgj`oY79;ro9uK!XRI(~!A+n!#6c|YtHf9RZ#ni%92GmsHURPsezhp^cEbyX!d{CsTW9lP$Xq%c?Ji z;DnwU=KanRm1qlfpfmP>gciRa1{w^P%g|VZcVm&2XZ^ydNT`j8|ONWJkT@+ z8*St}`$iFi7F@~Zd@A@NP?u zn|PNB**s1re$tKOei!rY6*?8D+~xt_rhKp);X0#**x=zA?ZZWz1({o^r;fpH-B1QM zEW#iO1j)yuc0zLK_5F*%h|pFB?~pSvPNICz?ST6(bwI&qWYoGQsfZnBbV55F;c6Ew z65i8`8m${_E)SQMlWdY;jE%Z(%Z&Z#!$Uo>d$XI~~KpnpqG0Q3Q6l(*j&8=>=PCt|(oK95%}~n1vVk&}=b_;_7Gb zB&1d9POBDQ<@S{2dIi0hkRICexV7pMX3HSjP2Eez*s?mukDVgzPfKy*-ytAGDo2RoQ9qV7oDtX-+MtpFYIQbS^UZnnmhlkrlOp$0mcB=ooPEt%tgqJK#PY*!h0%$y8W7W#~v8#d!!$FXmB;$2o=1=E%T) z4Qw>m9-+7#)`drb@4N0`osIRm$y=7ob;^7+>d*AZt9w+e#G(tkwa_-eE5k*S8_&Cb zmfU#z{^z|Fke6_(PdFgY6X%L26yt!ONNoJV2jnFV2R^w25`SkvHlJ&DV~fPk0H2s` zto7rbn)8$)g_6-=t3!;@lj5N;=`Xn8if?icSlAd?~B%ILWXy% zmYr7jlz2%YrZsR^5oIow43Gka+C6*Chruu|+kD}UrD{9z+*KGCbw>|}BlGvry<&+`AT{<|ymm5&^p{@&Z4cJeEyf8yZRu72;`AHCH*`9Ds+`ub0u=%@eN z$*Xt2?dGq7%KzVRoxS<`&DV}UcKnemAG!Wr$KQNx9mA)?qu)6C(W~R5Z@c<|gCDp{ z9(?H5hp&9>^xqr>N95J7z4c`WuOHpG_31Z${Kj|O4Gvzr{@ZS3H@@)3?YlSb{P>;k z09*rKaJ@P|_5aS>>)Xr|->cy#e){N#=NliFxJ`fZz2_1o%=N(+M|`U~t-of{Tt@ zq7C<{)7^ngNwO)n*>bZ{ciK=)jPz(%0qGl#0mBJuw^IspV@!^WR6MPB{Q#v*{W{P9 zB|0_lEGDW0?j?|rKU!DdE}GY$!CNJd$1$&wrw2O;klm)5oh9JuEu?z7SouS0vg!uI z>g77Y)`gExIuo&(*;T(DON%9jeNj7$)VRvRcj&_DDY6i6ra6nrniLNU4=A zu$8fKGKp&S1~D=04_aL2XX|E}>YJ{1^RJ&vlvt~qnX^^~rTDrAyzm*g5=Ve^GP{-Z z>n22u*ilW%h!4p(|Mb2iQip7FJ}GQ%&#G74rratk7f;c}SR83pZ<{eA1V!eNb@Q8V z{mj)5JnA|x|Nqhg@36qm5hWAeu9R85M*0(wUEORH1U;U+6bT1wAHsw>9*+G+FxwQj zp`9b@D%M;HGBJh{KB7+hyxs5vRAeDx zfUIL*t++-@Uh&-s7QA5YjDlfe*3j+4oetZ<#~Y^x#T^F_I6iI&Lr^MBccUEQI$FK>+Y+3+t>wn{3Ms#KOrRain*Qb{V6R3%lFR1yRR z+jc|LQ8Dc{$&EAw$i5 z+tx^S-E=JF1F@FgB+O#cIxA(vniFGcY}(!ByP}?<%1OHC2j&OEFCXnB>}w>7EsvS9 zB)gG7Biyftm3Sr=%Q)k$x}em(a9Yo69`jS2Z6YBxnl&_@^re^vA4;1&B;(;$Ugh)I zV5F~k>dBDg3AN4IXliO>C&6v@3F&WLg*+8a}lAI9YxGnAzFUe^++|!BedS1O`t!Di%hSONS%Cly zjYEx42TgT7Ko5yhURp|(>$rp5gUD zne1?Kb;%F2O1{#q`Y_sK1WWZ`!j~zUea|aKq@OXVyK<<+w6mB|4BI9oR}XUw1e7s3 z;iECthodb%P-4taogRs`DL z4Z)*?yn4N=B+#J9vsHIGf^vO-RWIa=nwvJK7_7#EB|<7ryKN0V4FwY(sNAZf#d3rT zmz?niY*ouBI<1iu~N*e*Oe$KU_O4rnW)RiXNwrblx4y!riGbW7d_cjmli#)`wrJLnW3Jqk+cpBAOjT#md z3hhFQ%}IS}Ze!_&RGBjytfROe2U$E~-eKgDh`^S3GM-U-ai!^vDDg(5mn+VnVB0`K zw6B^%g=7%oB?(J-%zatN!U+-Q8-YsQTP^w(HE!7?cE?b1B}FVadu|5t>&~vb;flDkj&Ir;W+GFs zQ3Fe`h*DLf-|Uv?aKOcA=?YlQRwBGuHyJ+U~Zn%}F&=AKa}f5mnn#ly}cL zH#Bmf2ytyc3F&@s(Ie%<<+jm|n7RKAq~ME$xfm5P(;?-ywkX>gRTc;|k#G~M<)peZ zq}E0Aae~$h;cC6$_t%gR-f0x$Xw^Vw?991#1r!mQ;CwZ@-0=b)kB~?C5MI>8Rx-@# zZFfA_6cjuei24@o9D{b5Hb8Hv>k7ejm8Og7M%I_-JfM$NJV{R_12J`X5yaIFZOpFQ zUh)1SNa%1AHwvYGx?+f8y(j2q;+hwYkTEnCkC&3F+4VK&oBkJDBQHk*++5ORDOzzh zq_W_za*`G;mi=5lWunvz4Uug&)9viS9kvDo;*61_sB#8@DJZPeLuHkP$hMhn6{r=I zXt7|lwvPGx!2{sjtw1`mET_$l=DF^6kDjg~sbL`NDZjfb=7b$>i*JrPr+WDcK#M*Dy z-nkZB{pspItol~&TY2S*bH}YaF5GeQ@{P;Q<>xuBbqJ1?rT0Ee`o#@v+v*6=k&SbXRn(TXP+_ishRU<=BGbAou2yR)O!xQw|*l3nBDzEw2tZ= zoq_)^W`G_f=7hh&C+T9YRXGUu{5Emlo$rXhKmF5xUIPDxK!5(j&z`+=&$Vk;-FDl` zd0(xnetLipfhV9Quz_EC(J#I+eNy9F@RhIr;75MYy7%qpzAycb3vc+$M~LOOpY$KI zyXirE2s}X#avS(J)E%e&Hvg_y{qPaxj^{k*z>UBC#Os#t{~)k~{Oy^c7hOc%OAoLi z@C5V(Ht^m9pZL6fy7T$}eB7%)_L0ke_s}W8?Y~~zn_GCtuZUBAe)_}zP7h)>@c0A; z8^AAI{=|K`??3#d>(Br5HRk)y2fp%_>dE>KxJSP5<6Lc~z?pyk#p(0_9Rg24QeXq~SHH3IQ|9iA z-+cc|@Bh2+#|zJl&Qw20xo=u6oh2OiDR>SaMCK@Sxjot)dK6884fmzLyy~B>1^rk2 z#@PVW51jMNv+G|He|OzE(kuU7TzK2PFFBhYgol1k(6rbFKK{GyrDyTK_{%4`Fgv*2qU5sDJwX=K}qE z{vAE;8wYNGjr^wfzvNvH;e+7e6EKRbz=nI?BcD9q_=g{T@?*}AmM^&GcONAy$Gmjv zyMr^&jlFEwMa#-R;Df+cxFgdQ*l>lnz4ckwgZG{Ns*5go?s`~y{e#&v^f!sO{prR( zyz`UK{LJ-d;REDwxKV@!HeBvayZ?CX>%=#maLseRa>9deyYp(vd(S64m!&>^a`CL^ zJ^Y?C@xj@L!;PXWu;KpwF8CF1I9K}DH~k@h?H9@Sopx;Dz8Br*uNS|*|Nb)#?&Xi* zgO?r-H;S~thVwo0whuhF`V#TxS3T=8ApOPHhyTRB@wHceaNp_kuCrcy33CBHc*)^# zqi73kxIf?Z4fSWfnsNVy51#twvy^8(Tj^b`p76-mmhQO7QMl^LCO!DO;gXqvxWER! zNWcD1;?2jt?4&Dya?7(Y^jSYxFsRtG{`khrF1YT*-oD%Kp$9J>0#86)U<3cAA1)pH z&-35-#c`)xzT4RM?y27`yc#YYf4sc(@t?f!&aeKM9=vD>JOO!u4SbjIJ|DgCsn_@a zVdccOCR&G1*^Pm2^%<8!}?*7pi+OJ)C z$2-6F$ciIP4?IKQ31|##;KrK?|JOeGzkYUn>=gLgOOJW!PaeGelXw2%eY-w+{``&a z>7Q#ZH+Tp<0g-_X{GBgL_ zczh~@jXA#jTJB31{ovLs9{HE`xBTVf>Ex9!FLGDYyVlyTxZyKD{&<`oxa>I|pUhxm zj*q<=ed{ZF#+rQR6~awlJH8M7>nmTqc-n{L*W8^}uKaS89za9j3Fr)L;8X8Y^7p^v zr@wibcJVX6|HI-#KiKt=FMjdf+m#b<;eNUEIghN+18@jD0il5n{GZo?$A9FKbmi_> z@B8yFKlx$T7bCX@&)K(!*z=ZSS6(ztevcjiL*NN04Q${S{rXq)H~;>a7k~AkXT0}S zFHD!ed`Y%9rVCC1fC$Z*#^F7_siGUuf6{bU;F2ignKWW0>1g;@4arf#y{}J zD=Qc8aXxI`*`6^3o*+fj20pN_d-Y4^o)>vW`Gr^g{k~hzdF2;Q6xJ`k@}i68;D;*L zfAUrIV9yYEf>bjbIPrqFW`)lY?T_EpOx$wZDffKu?LRy73)7#+j`8NduYYs@@94ql z_8gB-Z(sw*B6ayanHRIq{@7RE_~pz0QeDNa{P-j1Jp1MUdeuL@?K^i|alG~Xf7+Zl zwIDC-ng8+p2j=PdMU%---1+gH^3IdiZdto@4O;#F>bqB|)uolstkhPH-*MB9eLK!t z{`K;Em($C0j!!zuj^{1iu+&>ReervXZ(58kPA&Y$+;e8XHv95f*UV36-ZR6@IHvYJ z`rnbB$);a=pR_)lIn9kAmCU&-f6y^*>uc zA9$U0e8EHb{*_I*%SXVuwzlIv1a9tH3+UpDtmFJ6`2HQ6aIYH$x3zojCfuD56fL0I z!{+hM2gI}YJ1n@R`K76)naf7NxesqX4EZ|qeQEDO(e;I=kiKE$sBH(5XjZo>DcH~qSG6wv61 zyKnUl3+OfPw~nj#;`>vZaNj;M9PA%G%zJP6pan#(n8$m+`cr&AH3r<){@0tsy!Q_O zCfs*khwo300eASyI&@jw`|aOu!rgWpzMtHLyJgg1N3XJbzwlBE?nSS#2K&Vq;`@nB zxNnSrdrb57Lk4@*zgTdm{mFv+_>1xV_$J)fN5MVLsd#JGf_qNef_v-3`2N@?+)bn4 z4&Q1Ho4c2tXu(bW+=9FK9r%7^6YjcEaHH4Vz2dnR&@W2XvF42I_tXbg7SJ`;W_^$IT73WBO}MK@!Htg4 zd!9RG0bPEpb$sqJzTdeC_t8;sqa*b0KhIe}ecU|W{bx3^{|p<>S{5G}0p}gP%I>NE z)Pk#<8_}Lkzdk$y$TNCb-2D;70($8+mS10Z#o7B$v*7GY|CJ-);KNVCp_jP3Qy1g= zcWrv~o>35^C;sk2&+_HoRqM#V8Q*`>CfvJ6!HvG4-0l6a1@v#$=5x3A8hrorH{t$q z6x`#?*D-IjfZky37Iz=>7JUB+n{e+M1vfgp?|QIo0afodk9YlY7ry^*HsRhe3T|{r z-F4$x794Vu1$X01&fb5F1!telk#L8H`k`m@u3x?Y-~a4QkKQuEBlqaDdDln7mPcoX zERQ~lS^fW_(+^E8K{NEs`t(CHKcByOtkxT(|tW<<~AVR&M{&4GYbMmn^KzCg<;*J#mvUaPFhC*Ek;DdGpS9 z&c57Y`I~#$(jRv&IL=r;b?x@G_s@KGv9_kJoxQd;^Xk>Rm!?*)TaDf1<=NM}(`p&F^t!Ba(Efg2HF{Qi9Ht zdOxC6kam-7#=BXr9u5FXfLe;$8nsNwi%OjyTB`_rE(O;^xe8nmjCzeDBcNnfRwolF zBx4e5&YiWPK~!8+TcZ-aEK^h@E|aTt6thqz9FDjvo=lej7%~PcCMCzrzYjHd6@U>M z)#*KGsUP)IULsdSQ9iY&B?hQ=7~bRd1Zh?;aeiR=1-3?At_vNY)Y)UQf%$5JpKKS= zHWF%r*{aIN`D#B|OtzC@v=&~rnW(r%#F?*ELiK>JXExG8$y7gv<@+ib%r=ujlikn7 zo0&5m(Oa!dUvAq-lKmc950JHn%yT7OXvU!+$ak}5ZaAe@k^wX-^)ap&!m|#$aTOmA zGiKkMTwAUJZVcm@Adz529x#)r^ODOK^kZo-ZX`)Kk#)S-wo!;nS~3~%q}h^aa`z=8 zjE{3xNZic*^H=3WLn})Xl4-aDZU<;<&}}b3mbHMI*CiihX6V-0V4R^Ejegq&E1-f_ zf~i<2uQs#Gb}ks#G%0L}gjwz$ZMwOpPstiuz|2F<2Lpb*Q*OnA=2V18J(V+-Y(^BW z3DR|`S~AHGA-DnQJ(DDsp+iYF>?7b@sM-LX zfl4`#nc2}y!PWN3X~ATKtQHB(tQleFUuch`8f{Bxt)Jjv-HC-l;hI*h<#0jhdp(s# znbZRLn#mrn_?_7$JNcN;cHs_R{4`_XeY70*XI&i{>tnHwpUpObooIu)eLPK z0+wOeD3Yvn3BJ{g5=6%7Qv8TUd(Ff{ZT4rjMjG#c?qFMtd6`s0OZPH$Q8X|n0V4Tu zK2$(k;k=m&S(Mqz?5k~!R5irMyro392sWXd$5YT%J*gXgTIgi~fTtp;Cykf{>EYOn zO|HZz*>nj)i$FhCE(Sf(n5)*xBe8g0%GFkx% zXQEyt7V3c!RSE}7WHVEum_P^VL^KFArr&ORg_VS7uXA z)a}VPfTkI9NFflOvx$#*G+XwzQ!D|4SP6dAd>;QN=hLW19zpcPD_$<$Z*qRdr_w|Q=#uQr3APOc2pK7VDAOwnI7jlkj{&l0GwU6DN#AiE!R7}7M+u-imZbs8xOBF0zj1y;vN$5RSQVO1(l#ytupo7 zp$$&uh-#MSz?|mpQirtB)|ElwJOfZ9^-lT)As*6{Q?rCCrTMRLo_Pr87NO z5r}hc-b|@38h~JQwZ*S(Xt3FQvf~nCQD3l+u(fC`!k|gMAjVs48HNNt)na@^&PB$i zi?&xf3hU`Q7oc&Ixhjg#HM4!1xvjc+SGk{&YrHI&t*dgXi=dX@Y}+X3@nnYxlYmmp z#3FRB92fKPY=CTKOeSyh`~i70FI5T#_}Hw?eaLAE9Ly<68V~d|Fd6%eZ)`daxr9*Y(3QDnT(%t=EHb_ z%4o8bYK|iFKWL zRzk9?zp`z(YF(BN=o}5p9U7sz80|I)1FNvSC*+Cxp_Cvb!w?a2S5`k{YrtGvs$^A~ z)U={f?e)EREQYm|Am7Q?utq6EmfFEAhkBIo%1dmG@yYXd*cCOrwUY?_U-Zf`mjE)<(dDdEX#og|Ymic_!H z&|R%7~paP3L$5@I+E(wVpt^QF`B&`6A`3YJByl!ftFn9L3*)CRLr^( z%B(S9hIS;vw$>Th%H`XYk<8}$CDyDDN-=vl|U6UtC|qTgJrS>Kh`P5@EGOx@-Wh77k_PQ z0EHfa`v3!`0ky?7nWouDv)b&n190CDq}fKT;7`=U9ZYb~?A_2HI$qp|)gx3|Z%{I5 z_Cyoh3{6&1Zvd0cBx)_;6ic+qvkO15HJqH0lbv0W;JtWTb^}_GDVr3+WLhpKx@jT+ z^b#H3EPm~V9X7Wc*9&UFYB!BJg|-up&B**sFvy zl6=hPj?t)E>UT}HV-j_B>zS_Phk6W=fde5G&Bv8~s*Wt5GxUmUlSr$Uk8zSA#(;K! zuVjF*kpumbP*9Z&Aa&e%rH7ZHK-giI!4<)3x`jw3I!85xfS7Db9Zxiy1;e>6s-zKD z6JkKYB`3PM*z{Y5Hi{sR)p*i?Ty?*YX8LA-)KK0mYzT#zy5FR&Zs?wPLNNJ<)9iwM zmr(@Kw1)Q3cqOdI_;!;pnpl+draCZ5M;gJnnqg9!-*EX|zJ-mI*pHQxdJ1bX?y}J~ z04$nmV~gmW z$c^G`6OsXcOA(n?0j_#`4KyxxGIDIA`6%C15?LToCOm!BuM;N2V6+3E&2*n;ByTb) zdP8+IDH}kZjV;)1aCs$NZbZwt>~A{5X0;XR?bf1n2nE1soRh_x)xtC%4&ZQ9nLl8A zRZlnxfba=TsK=X4Y>`T_LuYfLPPq_FvrO1UlKn)iS?{C18Ex;Z;8^FSbSkTcQ%T5M z6GG)0nZSD)ROt)7R@yL`V;fwx?4uO3l_6qVsG4+oNxv+5>qQ*{GI3A9gXL=iP$g<6 z4JzUZ30@~jBWg4|x3TM%T2d}uF}ZE($!s*5FyL0wnevsSaweXL1_Q}nmQwm+x-TH} z_B{sA_frMI#it3}tH*J<<@L8BHL(!N#_1y2$kkz}ktUQp<;~6?XM0s}g*+v;X0q%i z`Yy)bt&(w?NmN9+5;f3@p5m{vApfQqZy^ha#|zt7tIfB5ug|0h22{?8oKtJVgfO{FqFy*@!2(vzi9f#Fe9 zDj@u{q*9q6)9SB}N`+TSy_3({b#g+`G_E4-lG_clID8j4q-cCBHK0YERT7$d)zksK zPbHU%4|01)a;X&idyrju+EyBg_8gN}D_B^b?`ZL4L8t${+u6YZ#xNTyRSw_BLo@8R9eI*hvO zJ;cv-LX~mY#)d<;$c@$pW}8>)(%ILU5jnxCdeRZ;9vPADr)5M=kfQkXiO3;G<2Dg# z@n<$wp;ORwP8ZBoea?C2Po5|tCj^Y98}meomRj%cF)0^RMg#L)GaoL}2|5}m2P!qL zya&qW_U^3_amAz)GJSX=5vk_YXwgR+S`A5&eKcpj9V1kgO-HI}y@piEnS_q%?VQvz z+rGgqffQU|rcsi)C@K*8-jEMYMUq{%=H62+@liQwcKz5(BI6NqT0^(##`Zo+H#Whl zdeRZ;8X1wUr)5M=klXvTiO3;G<2C^aKcxW)!jQ#Zc0{@{Pbu1@Yo0x^Y$l5C>FrIE z@}5wOhyXMRHz>F6G5l~Btu(Rr6KBYk%swSz#R&O0#3dqvJDV2FR^$~|Ae|}4sA#b2 zszv?^Um-Ke3Y_(kEu~iqwW|Ggv>NGq z6IGL3O;3QV*ZNf8>1*X8p_4u?Tj5)9#EZ#3T1+NHG1xRIJjJNmBjkQrs0FcThz-R9 zV_qSxp_>6|J^vp!8=BgA@!Fr(`m4WR?W{b!qVD+B4te?K%QeT39p$ATEb)sEFTQf| z*o7+>UOfNVdEeYu=c2P;nGMZcH*@y%wbL)2`hWD+VCJ>|GoS2_j_T<(1D9L@&kF*z zv_3)I`8qy+Df#xj=r6g#z2U+5sOuBdoR8yy%l2Rb)b$C9)3^6Px&oS~DDy0ATbaHo zF{*TX`&Q|fTmjnSouF`ieS%u_?VEL9as^;}FahfN1f|W}dmvr0cgu{An$JUL{K(bq z+t0Xk#hH(>c4VwgYom(a$Fb(zVr>-GrnOPE@#9!KV~e#>Sew>HmClc2ZO;~Kqp&uu zjjF8Q-r6NsoNlkv3D(~F1eNmJuhUDec%kjV1gPs1l+=&o!Qa{*On|yRLGArG9-L-- zFahfN1V#GecyOxi!33!56IAn$eS$LnaXdK1 z_Fw|kb?5l?{^NLXvhBeHsO!P;i2=s(VE4v47$0?g0tSI`JlJJ>FahdQ`;G6CD^9XK znBdFdQ~OQhk}FQMJ(%E2$@)|J4)>BPo^N|F0qXiw`qJu>D^9RIm;m*u?eumu|NrUr ztoh`<9p;D}&Hq1||9>?9|7iaI(ft3T`TsNP{9jrA|7-1Q)CBqeFSj!vC&>S2Y>f%> z|81UceuDh})9rCgkpKTv+p7uk|KD$G9L@hfn*V<^|Nm(I|Iz&aqxt`n@27Oz^|znGuN&+MI7E3f_C+Ot+4 zSpA&YRiM23qSa@t{BY&7D{ojSt@u~gc6@)wr*~YwgWuuZv9kQV<$qhgZ25v^czJnx z-!wDroSvDwW6m|VG<(nN)w8dfJ=bxc;~K|n9OpY+j-{o0mablU)ySve)90v}nccNI znl2H#UM#ZRTG{OMmphCK3IKM_?wlQ0bY|zwxT4cLr^gkY+Br3*=-Pj;{r83_N)ad{ zRNECj)2U_Acx=nC)*fDacwEunto>$O(TCO^8dvn!Yrh^>^jB-YvPDI`T*H}am?Kjd zF7ahLy>(dbHTSrpt~J-VqR<*Nt|+($jw=eR0k&vOX{gPHVNjG_scJ>BFK->z3)fya zuIS&c{q4A-r>&hfuIQ<2r;aQ7g0&ZnE4se6z9E`whlFk|VzCRvc_V~TTV`NYSyjdr zl~?6)MH{P)zn!C|r+3%bzABb#f>>=B$w1q{`7I6*#F&DK#F&COKBiziKBgdsjXUo8 zYJFT$X;m6mw6h^MlZbe614P&r8kT#`ue5Uk1Kll(&gid zzHaGt zK0MQuN)f}i&{sZlle4*kywui34D^=B07)H;~TziD;l4Vk1LAJ9}L57QDgJ5 zaZ#i5(Q!r5d30RS$b4j6(eQkDL$s__X_N?crA{l^jdXK?E%R#LE8a1#=l`u4b@-j`sTQzw@lqKuIM+WzA>)o%~LmzEBf`Rua7Hw z)6`AlihgbCYvYQ3b?U3*irzSNNt{+$Q zOH*GOSM<86>&6xR;?x(%75&207seI+{M6^iymMjMF$EcBOu<}kOhK9+Q!twyQ!tYm zQ!t$#Q!tepQ;?#@6ig<^6eLNz{{PccEArCa^VsyKM?O&>{l#C^44AdX@T0ZH6XbGj zpPO{y$+{v5azQMa^`|gw)yL@FjenjD!^5gMO;-i3QBc&9pqfM!rl8BDa(`MDQcaMB z_Vg~K0^!3dsUYwvt)z0pE}sc_#L`AODD5>O#bl<$5`<>Nk#MJuD3$C_qy6_g;qL6F5>euC@YmzZ@*Y>hBH!*FkoX(6moZXqT?p_ z=DHbx;u^I`Bc>8;070Q_h~)UN(xLFOFDLB@j#vF}4c)H(cRw1D6RfHy9g)6~rH#I) zWkgPp>-pCgkt3=Y!68M%W9uFd<>MIE20wMRjc&j@ve=m2yO-_mg|ehO(t+v?tb`kA z6p4xvFhb;Aac_X55|j%vX}g|yLJ;K@9VKWu&e3uASM}sHDztt;v*2O zE7XN2o3tkd-69G(Soq_!hHgis>(PjuU{yWoi1dz(Nbl1!A}7dFe)>e@kfP!7BNDU- zZJxS_bitz;a=fXitIk4oDdP8fT3J^t%PK`8Rh3by5_93nl#J6RE2~=WnFJW7S5JhG zqZ0Ac;S#_msyGwp=?3az5_>zO;17B*kHsxZ5_|luAW~;K6(v|#6&mhC^{f#!8ob%2 zyjHAnRIut1In>u~m!P!hLqh>$tcbLRZbu~aXhcr1s-AR2dPYX1=V=*{6J+f_eIjzm z(XdTKS~r2{{13E5h|Qyf+Gn?EqDb5q`>r=e^-f05K_nYcaMBt9mNaW)e?0 zA^}%Aro^r$ZGnDB8(K z_9PfMLNNI~OdC#?!=NwIb44|yGA1F1%N+Vf{r@Rb%GB~#mQ#*<9kr$ZT6+1?+Tz<6 zKD7{-|N73$b{@0#p0zVpKedXjJg{=n%CmR;%Z{@a&zL`d8kxI$?s>Bxo<(MEpQ%m% zW%^@N-!Wmf`%~U`;o_7)O}*fG>!RY85lQJ5+${)TL{MNmEy9SHWX5gT$Y-62aKZ`o zlVl6kc4zDDB#Wbr9+TstOfA)KZh0$RGzpspSwGXd{j&ZC%lrBV4RKsInnCl?L>CXa zL#@2#G~CHpqm~2pXc2B165DEHoSY11D!U0zt`d!)Ow-LqK82~7$m1A_3iY}o?Q7YF zcCQN|ukMr)f5n|_CU9?nQd?rL4lzBwh>#(?P$=)AbEYdin^ zb$=;RFzH5dIObyjt`7DJ0hCDkG)}?DbdxSrHNNR2TCsH3w>z6b$W%xrx-mXoM*}_N z@lMBv^Dc3panM#H4yRzmWcW>*jOnseFtYWkx1o7r?s}~*F{ljq+!#V+>#pn*vISN5 zb!=P5uP6MWP`XC9kXnIiMKw6*s|ORFV#zP!02e74t)NLv%#@NU9DYK!s{7gpO+iep z#X?0R9F4IBsMP?tWCH3Dkyw|NTc|VcEW6945CnT0(i1eLA2byuibf({;Ns12BoYiZ z%ZS2@kcbE(CHmq~&C41}ssL+w zm~ys*094h~DpyL^I{g-%?3diT6HL`mS_IP(x`H|JWIppa>pETKtk*}o#Clz*aP?gq z>vNYP@6xNHwo4P#j!9-{{!Lc&T}?r4h`KJ6c5!lPmueEYYP&>vS4ULpoGzL=hKaL_ z7k2R`U!WwE&b(xwYR^|}9dUIM-8KslQr+c70mR!E8h8ltR!CH>G)H$Qekv#Jc?*8BG`H zBCCoyjdL1QHrxsU<8Ka@2gqan?MocAIYC9@ zlbTjvYaf2l<^-fxPs*mZwJ(0q<^(;UpOj5cYae#d<^=7bpOj6wwa;wFY~5}-*VE&7 zj($=$-K~AmgEkeEfcjY|4o8iQ&)JPN+h9_Y6rtHFhs-BSqv_%4M7Pq(WV9z`)79FC z9<=H0l0G48HYzFmnX(@t$~~vosHdG2qvF+aRSzaGSHgVjE8~eLWfQ9JJJ+^(%DNMV z-QEgZ?DtYCp2Q(P6JiXnOHTVqHBcm+5vD?m(Ir9I_GV*89hlw%f8*HftRvMs9EpPS*5~+`t8Tre6*fi z6iStS`Gb}yNF%DzZpf7>AmuP+wjxTjIzc__kNDkMIp^`mgg#mW>wM05VwTv0mg251 zjId(63{RE8G%vAea^d;oWk%8!8nmp|YZMdm_^3w&tiD zy2aUJx?mq;-Zz_paMHp1X3+10yif>jh+Fe9x2H5Y6u+@SybRU$az0xpEv)Pshuxd`~Cc1*7Vh1FTKp{`}d;N#=^@N?^^lE z;%iqfS$Xb`f8BvB-?3a@o_4&|ar)AKZ1@Yg*9m(DC$#FaDa zKwoD#x38N@Ws2bx3!Bia+FI#lWjdARA{IR z>@KGGalkXq&x?m7AQgUkRoA8bC4h4X4Obm)! zhs&9bE{RAomP$tuO~PY%e2c`~FQf%BvBlUCu{MpFulKS=mfnK2dwW|yuPr1Fw{>Kw zO;avkB0xk+lbib4V@w?xYSR?t4TrOdty4V4sDAnwQ%8o{GzEINhM3w~tTl$IpNPW= zoZf%}%r7LcCXo8p2gS|bd)$&%>ur5Ax4T~rYHX1+1UiD@IAH|(Tc-H5$5=Wt(jk^y zl_(nzZo%2!(y5QNbVQ_0OE6T*(RpNIOD}kgr6VI9VhQBySR+5NrS->JIwI1hB{xvU zI?cz#)VAm8l+7*PRc66tF6lLRQOg&|ws~FfU`e;9K?WOyyW2`OqJ;v}Y;`N^1=w9_ z#tS}!bmp1?lcYT)bz5y`mGbn645kZaXEz$wV@=Tm>=w&ZH`*YOVmggR=|ru(b(5KW z!=?{VyWguaB?y;%wOX^Ah2ER{c3=L<- ziT46oE}26ltZuf0ZdTnvL6_q3qM)=nHsUvXD`8As1J=W^31Iuq${nIbRm}F>N&@uwdcNHT zVt$AwBNdG_+bK6H31n-L_RQUzKIk%(F8K(_M;K5T!3oOkFLQ9d=8BdLP|fu+!E!Fr z?FM|tIvoiFePFrQrQBr??XLOKU_FWS`>f=5t7^a9O-2*xiXYp{6nD2Lwp`iqmdXEh#Xwh6LX ziD7zi^4XlM$WF+t#sC2TDu6!Fyft>h-a{WMhE4QLGaD}khe@;z!7{E@#ioT)Q$*Rq zEs0vQ&h@NM5!R1KVOyeHhtoN=&{pfFq};C84@+$}1sPyvX{HKXR$w;s$Ajl9N_{cbprP|;8#1SA^eR=?5*Iwj1Io^ZMs7zrC@ z4LBpD+(Lt`GOY*_O4X3M5>{C*TujmxKw-+IUMpKKmSC0j_l=0RnJ!WBQntgD#GpIl z&Q)-%ue8)^piMGCIWlHSv^8`y-D|%%@L7@fn!@xX%XRxzIBw(}q1)1V2N%a?ELlI& zd(GjZYn*9?!c`?Xa3^aJ!GY4x)uyu`&&T4Wn-B}f@hJhw64Kr=+O zk;1T6Uxj>o_#y~pd(lwUUqPZ|)fdTYkD^O3YHMv?Kdjjf^E*~Z>SvyGq9&tzLx`<5Cx&#?LG5idO8(=yw5g0Fo? zzOW5@({7F3Dyk(LIfhrd`MgfnwJy8m?$lDyw!N|)`gZF`SGG|VIm052$Imu~T&4$) zy>30xh=e_W)lcw^C z)Qggux8m*lJu*-MT@05jCo6lsbPo>HdwXau5olDpt?C{Zw^xMmrkOvO309y+Ipgio zaLNVdI!Y*x!J$Ue<%LvFnCa&O<5lEXL$@Q+^Jqj)u&TEIhP+Kg4x6tYF(QGdWkgQ! zDg3W5A|D%cW>ky{Q_ta5tI$&$k&l!AkE5|zhzm(!vKNCqdpn4$FY>hj1NQ6HMr$ut z4Rt9FWvB+tKH(d}Xfvi))3FTS%Cy=p7rdwL$NQSmMfU1Sn@=YhRS&eX{*dJE*Oj(g z%fM~MueH)*(t{!_r@I3wEF-{8D!EstrECrg<$*D)R^iS3|6`}#GPU!Zwa=_QcU4;X zl9}T#FWtz_cs0{YAV9dGr%X}#YwiL0U3aPHE*(@p`$qPOaRF>y3OC&3L*H{xhpABX3Aevjqj{Cmgt z@x;M-TjOYg28J6h+K;i@pK!~!>9-A!zZ%=a=MBy^XOVj0=DJ+m_;TVJo$mPP+zoHi zw@-dtG@ZKV5ovf-M!B~2b(6WJpR;8tF0D_{I`N205szZL7SZ}`UX^=%h}imVZprfT zzy8TO{_elV_VkoN-tyFW=p;_iVsXP$U1{* zL-wGB&2v4f595Y&hwMC6wHm_s2Gd~zVn42k;?@t}3zlEMzHE5>_1NB>JYX#E4%^WW zmgoCn=*WE=H@rK-KEAD&);@mEgBK2u-yhq{69zfU%R}xn<~HYWRKLd!FDq?9(+ga& zXy*TJA2infeb3LXwT?gkn+g1*xA-@^>EEahksJOUwrg)Yf7ad4JwGlEkN<0I|Be}C zE&mR?`+4*%kLoG8;ortyy{%`~UVYCGtHa|hV|(_TLB`yxH$1cNc^-9aR4dC3$2NB8 zZ5^{by5G9fx##{1$M$RYAZ^Z=aL5k5FiE!fqjP8P)#7$L;4paa^I1;b7Z}^gGX^Qk z$-{zog4UTEP97nQxAk`z#^3$3i56w*r=@x3$Oibk~5iJUuM1CurOG=rXmw zDQ$lN9ESKeUuyaI&2NtF+RSCU+^|uJa{{{T_9P$-3$$n|1q|Qy9Wtt77q#b z=S@LBVC%3*WwZ8u+rrtZX^QN5#c9wPmYkyk1VeKtz zwY9U?j#>Tr>UFCJR!ghE>cYymR{n9Nw&Gct*>UrZx9=$LfR_KV{MF^RF7wN0I{xVR zisMZtFaPN#d%$%|uV2b9ox1qY;ChS%k$uYX-j872i7bdD&4G`q)kw?>nwQtx=1NyxEN_CLcBW`;KE+L=#ExO0aPcA8KRfj7VZOA#_S{lgzE<%;ZrsSE}Km1W%dEL~t_bCp(oE z3poA+9$2z;qySB!q!F%3Ng^M~04S4C1A#OxqEe{_HC?#?fa#zUNy4n-5%9pGrIU%v z5hp?nZz#@c+4X9A82J%UTO88WrMjQ`-gYzsMLGy8<05K9Baw^fH zNywCP)@vr`cp~7Y&5i;%rkdrgQpX=R96tgF=ML>~jG02$3+CdvSU6o(*f1T07%GAx zHCMgZk9NqSDmz0sDN>Fff`fA`oeJyr0ui9kD@||K{0iHtRR}1;Vok3zD$AJ1orwb~ zStDRVar^)rMR1@lW8pW_a2fLl5&hzn9LqN*{E zQfs9fsTdJ-b){%1QZ|=rpQ=<)1tNPbIu&!=4i4g$P9TM%P?0Hj@=aF3ItbkMI`ddY ziS_wrNEbUfPqmBnVl@CU9Jhf3%+iUa5VLN&lnN`EPJ?#`!VDil3w~Na;27IbI`twG z@_YOU=np$?1qU%pr|OZYd?z9&S*OoibAupR^ioPx^5|8@1@%gqBu$H+Voy&^Uz3wq9X=1HpGLmWnc*GYkVmZQFtw|u12;F!bUVc|LIjF}Q&vPS#w7cNd{ywW&AxdJ zz8M?@hj!9cJcv?MEEVr0f^b~q)s~8tn>?!+Bw0yCF3JhHZeb!-RbcFvGlous*h#Vc7SO_siwF*X6Ry((UT*%bQ!tSMBTi zkIwnef6n=T{by@+Mv+Nfb97GTi-^KBBH`VihK}Cs!VHJ00-1GGb*L0=rdJQ<6L~nS z3*}}a8qt6tIfH~04abxcy8Bbm(Z6(IeE6_-e-b+Sd>6)tH);1Lprg-oVSKoEc0UXq zeXa}R!{4&|pP-|sT$pS`pEx0=lP$s)ID$D{(Fw)%+z@gIItwXOyON+&fnf*9_I7_9 zIzpx}<5s4JAmwnSgpbG=(nljhd{Cnds;;Yau@Q*lBxg~>v`Xy$7<3eOVSIQ3c7GH) zin%a8uI6`tv3cD~qE|GV?=ZT}xDT-Fz z=50Wztk~SnCW-hG^MIV!%);M5YO+!{{)MCCxSt^@F?r5juL4 z3*+OO%7xi?VPIHj(xgcB!x3QHn_~+d@p4oZ0!XoCAz?CW##*s9#<4+e_q(B^H@YyC zQ;nqzx*Rj;ab1a&yG%A&=6h5Z?K%YBB$A-)SCY%uu|a(IyP%^txG+90Pb7u?sq^(Pq;8XBHebs-F^Rm^Txm4xb@?=zW5e?>*~#)z4?_lOE*9D#;@P_h8yCI zPrCl0>+;?|?fsj*__cooQ2}3b?XA}y+x`9B5AK?~@YP?q`oh)rRp`oJUHRTC&t5?; zf9&#)Tz=Q(%H`Kz`h!dV?viyWwDUJRAKH2Q4z_b;`)9VlVw>H54d^iVZ?}}KCpQ24 z=C^J3He(zAu<=6x{n?+_zvC13ppA{!x_{Z-codoQ!A|o5?LpPh733Ya_JE`2weB!5 ziZzR%r#GIE?R>tUYiG>G>SM=HJy3T{Q;~PvoQEAyHe0pCR(-_h=}sQhkeNg229}+M zy)h4qvt+_dLU2k&HoX!S&VF&FbR?A|bdbsjdOl=DKq zR?RA6FOdO9?Zx7Or(v(m!>WlI%1YguPHJqX)D+Y0b7{Uj58Ic~JP55zNM;`e@eYIT zV%?Y1crTrTRYawqEa-A((#zDlWrI5x@6J4|+BM5;KT~eynU*!^aOnAb-kyh@uu8_n zBn)JxrYsfJndZ58x8`B9@+8w&l2TXCOfbixhKosar{}vl4{H!m+h24VcF=7y$+q6rhfSHw zH5OCcPtW(6d03DWugGOQ(dlAEB3Vq1oO9_(CEr}|Q6KE@scL_~LJHmN*Uw?ZzZ^h>$Y38l?ZD$^X zvMyQvm^#;C1FZP%!8~ja6-e7*xdJhgvb<6t7IUJHVRh5d`xD zPX9c9pY}uunfN@oW?T*g3 zP`Ax_*tNxW=V^ZZ?e@GRohc`9Joat34S?Y-C+C4)r*;!nb*Y|-J z|4h16crngzo)F}AYkm@)lqoQUex9i%NQ<9v{ldA-YtF-x5}j?y*=oX;!Rv4}DUHsB z)#qWEQD?$fL?R_M+LL68>Kf<5Hs)b#c#4%f`9#B@hYhVeV$Waj`aJ9yt@YS($4(2R z*>dkR&r^Toc5NOO70FJwJsM&L$rwthBzDfFS(}HolNMn%7_l)ls_Akk!w~1dzW%m4 zFDz$@NgS^ya$A|lKx0G_Z7OtG$dBE7+Z1~ahPU0W0@%BP)@kZZCQ-``dsv~w*E5C1 zgUJd(n>UDyXUw#A7GAKIt&&8i^GRmcR=cgDEVYgYl5=J5L>c`wQnEv$hEAipLw%M{u+YclUN@jjCt3~KT@x`Es-&%@GXn^H4XYe;e>tO3gUEY?&%jaQh5 zErS|6g95JSO69bKkI}-pFGsied04&ys_+_?nU{qOXrIwi7HgKA#>>sat_>GI4O^Or zT^oKn4~yPrPhdY2yuhxFI+}-7WV7NPf{@$AlV`Xu!E0ke=AYrd|G$3YYd3CnZe6i1nuUiqCX?BzeW{OskuOW$KzunmSuUqY{?ai;++y{@j(4Xh`4x5*^m?xgrcK`FEYo+LJxP%*YvLgH; zKED&V#?X(eK{?c2YHnz|zv`KqG6d*jByIeqce#tYkeN*DgcWsJiqYhTJyJBczpPT>W5W;(G{Mb_l$8)czk%0 z^g|h7a)syTJYzZ~Jeqe86cC=bxyuEF=Vv`5J70L>3mc{%=azC zJIp$KE5PCk&rh$<^I0uCKAdUw!>0j8S9pHPGsZdL@!^8g55Ev#a)sw7J!3j0JU(1^ zKzQEbE*B7stsIu73}$wJc>M`@COoGYeAjUsgJTe3Cd|bTW?zz)fIhren^bEs?JY!hC?yDTs&u<>)0g5M{W_G{NGm7)oY4Nh--ZWSF zzQx0wO9NQ2@AXW>$@YB`k+8~JUOUW!X9s|SevfCtzMGroCnUi-%m75Lf&Y)55iOBA z9}fEZVH#j`4g7a|#yF=(KHU2HVG3Y!4g7a`#&k*!eS`x51D^!QTm%1|o{{;KNFR{~ z)B1m#S2p&(>gsEEz7l-+-}C1^mySMvTA1WZdYgU%Pu8#CLSYiOBwc@G&Z@gbqSKz2 zOAC`MZ58Z8(A?@de&R=_<9$fZ@)~$P^xH>alC@V_-bw{6nr;{BR1v&h{a)3vzBcN+ z15OH)oHTlYP8B3^OFvy$g&8xh>VZW}awVgJ=2tR6J2YDcU^_Dj(^ zuF@JV8>CT`%%RawYqe|(GaV{z=5SrmWR@wLAWAr&ZPN)1G%R-MIlpK3bn0$}pi4)e z=N72@(${SI3Ff*lJzT1GOZx#o@M`B{vsb_pOM9 zeJMJ~?%Py`Y?qNSS3sh4&T8gHWKSpSp$@DdQBYSnvtP@pwSjFx<(5;XDvn#@l<0*W?I z&q^+AMb&EZ!03Z#1OZR)R>ytjvDpRHtSocH{p*rDD*y)`ckgdY_tsM*fJ50bLF;0p zA{)Bw5yG3)SA6vT8I{6Gjuw})a-SjWV5qaQW zQwFFSNHGKdSYCkh!nS$01tgftQgc%t3uf4kNpWjf45v+;C4^jE7CU5SpUE>;l`*h# zU&GDLRLW?}0zXbseFN0)8|H9gD8it8N+{>z8s~&5F7WI~Q(^%Pn8XT3yh~4rrrab+ zv82K&I5izBnP4m&X{bqlEU|bsh!*)$aabRv5fdIsY_HQNi9kzWLE+_0*L1W@K2w6n zL=CkhMocC-PB zq=Yicg+c<|0{yziO330Nyb=o3ErHE|96zCi7CoN3AmV44Tg#@Src?DqRBwG8_i{#crqFy zM%8G5RSXergyS8f5eY=JAu_5o#@f)zW{GmPkd_kOZP%OM2iD{@>(*p3tmT1PlMB-m zzEC!D<7zvim4dv+B^wxn4VSD59uGBBaaajgY+P-WTEhVW3O7i6kZp%hG?&EtttO?0 zl4vSDw9fA*seHpS-G^|8H&5nZ1gP?tC z*?xkmq3Vch7+J1tmrHg(T@eR87*uG8qUClD)PzX5#iR?F9wsqXE+bMHmFLq5&uj9@ zKMvO9lh>`uVi3jywI&yJ?!x)LL^W6rN=H~TomBMTxQN%6=m|CskNdGUALR51+cxSF zsZ`WZmyn>Ytu~TcUXKn(>5>u5c2SGYC6c9#h@zb5HQB_#n!I}5nk;4kJWy+LQTquI zZ01`=uN3Sx!M-B1Tn^4Rr|(VU)9)twVx0Phiw^{f-8d}Y^NB$ zkyTsA@0SZ}f{*!VFHpp*9YL*02&!|vVZGdq;GGWe2{6UzC?SHhQJRc`OVMmr64P0N z@w}h>#?Sqt`~JVZ9e4Zx)xm!r{ygNsLk@W0!29=)zTgVO$aluO_X5CHMWL;a`dSht zvdlXjTGFisG#dn1-$^Dj>1*Z?M!P?9$*UhlLTh-$J=0eSfpgi;%oHzttdauD$ z*k+Mf_jG8L61yvPpPj#uP&n**GM4%ogme=r4>%-4>v<<8kAXwq0j2UfWUAO~<5HFy zc1q1Sm878i?=#en*$@H(1TWV1r6E-OH*vw}8 zSO_$KIT)40y~(h^WBCGTM5KjLBbrVqplF+fW^qrw_@+~T_SyGtAHCT%mtX#dIdi$n zlw6p(gpsqp%z3-=l9`CJyxq~kDIOcwTCr&AAjAjn$J>Gbx+x{z&F5jctV?mxWmAQHCA4$-EC z;INdyvIpuQ+q38x4}ycDVOUp#QLQ)1;bLom+5>9>GQ+~YkEpJcJ=;d&G-y1XNkG--Z zw2*FTJzj~T-Ev>8wOdAKRL2YHREBTkqmoP$a6A`OsAwb`zwZD549N&^WEd7&ak9li zBot4}g*-aiCmgsN=Y#PW-00MD`%1pm9vehEiiBbVP(4KngWBB^htnX&h-<+Os>%|4 zRP1Er)V{ZPBgCEhv(LKk|C^WoWCQ&F;m<=3JmkPb4m{+*|8F_)uWnu4+4#U?8#`{8 z$$Kt+eCx_4Ncq}4^ZWSwvwuDNjbB(VPdNL`<8S7v^75S7m?iUDh2a(We#6U5=INRr zKmU1t%-PS|b8CDI<>mKTsN*Nk<30cLJeF0kTU#)ad&e=ZbsZXAE%xISY06o3l{!-(2kH`KKLI@=|;=|IGQ%9xz@5XWhWn&c>}@z4gIc zZ@*Q#^@f}Obn};Qe&FV_H;Xr)xbcrSe(AA9yc6>} z6xxY;9U9z;cpZAM6ZSgv$(_(_Xf@4(2EI~XYRTkqGGP{L;qU(F?mu}Q`Xjp^^E&j0 zcmL7r&>!0U2d_haaQE-M4*h}Mzw(CGF{*Bk6-?#hMUWb0~?qAJ@uItor z{oU8U-RsbIUH=bWhraXrzxO)y9oN6j>(JZRzjZdWpfE~9%WG9fC7m{HE|y5&e*gBD zdmZ||?Jx5>^u60(>UHRQw!g&d(06aY-|NtKZNJa!(06XX*Xz)CY`@3r(A(SZ_B!}D@$yht|ueVQ3#@eBO zwS8hT)(-v4?Gux+cIaPhpO}obL;rmH#AK`;`e)lGCS&c;Kixht8K~H?Cwfxjr9mB| zWPP!B*UlgB{ON3A*AD%oo&V)^=pXLZnV&tLhoEAPDW+RNX3`OTNU?NVarzwD^nAKPwk zZ)|<_)?=F=0@eS25In~HX&m0TGOgRVvG;e6u5BDKD_n86?o7{5r{(71b$|*KZrs@W zThFNGYlHY{(fs`N!##ivRBqhZ`y0>Ljzi&Al}K1j$A4V)-u_uVyylYQPX4bwlQXL; z>Z4<{eYgwIfWn^}dw=D*@4?wxBYqlAHx91?l%VqG#@=6gMmZ-zKKfKQ53c}JQwjPD z&!|pGkdFq}K!Ps2OXf<@pL@o3z632Ms{83^p&njx$#MDoXP(KKNl?Ojzis<)2cVfs z(4Ts4ey~J>e6;Is9Bu=YQwjQCo>9(8kdH3k&BHB#YAQj0;u+N`3G&e%97xcnyJW5e z{ZG%>&Xu4uY4v_ukE@3pE;+6Q{SVLN%p}N1Ukm%r+X0%X1pRl<9T1jCkdMX|jXTc) zlv4@%W6vn(B*;f+i{_nY0jjA4{gG!>rzFTvYm4i5zQ|oNSAzb~Gq&?3Xt4-@pWYVg zoo8HfTnYLE&*aP`$VYPv`_2TQnM%;_dv1fUM1p*Dw`klM1C&z<`hPv6oRc6Q?Jb&j zMgY}Rf_~34s#6l=r@zJZJ41KLTnYMJ&)CkFpv95}ei~e;cN~`-SAu@WGdVK}^3mbK zzB2%5rV{kqo_irIksu!}E*f|G0OeGIe#3*_WmEwgxyygfw$f<0VdEKV`J~Xu21k-@^X065Mfn!jM|+x zKmfX9Z0!A-X9P>s^lsWg1YX`A<8610>0$yz|HU&RfBiAucBgfM;y-&vaY`xg&SF(o z<23(&>svPVzWmB3f&V=GdB}kWkpu6oA7O6d*jIGs6314Vfc2{4HQhdRHgRk|e{_{n zfrPR7bo&9)gBmGBk)s24pYQAsx&3B()To6f4iCy1Hz#6Q&zfqZ?)1*2+pkKFTR`#8 zEu(*xQC*l;Inox09y1CBv1Cm^`82_In}Z0SO|q=4mgD1ETj>|6$OyM)RRk6i`0gEs zUZEtuy9Bs(7nao1Ufw_AOo7|w8F8x=z@6*;rm9C`@s*YDGfhZ`yL1xiXJVt!erf1H zW~@8O7a##iHTJ1rc!SOjL zn3T$OJ~)oBVq#oxM@=o8rAAtjO)3X=TGQf@MAFgs2PTohJ0%S-6IssBB2=X$zjc{8E{VF zqL&Qkg6)}qt$c;7&M6Xry*X)4KUKz)a{eB>hb*@^tMuT-Se$SuxUvC2!5U{6Bob;$ zLYfHM@koa4a7v;)NF;MzT~fGIkz{nCU=FzZwm6ifnuS6(fsiemfuSyz(fU2}pw})M z9YJK_W~PW_3oHsp4iw%*BDqPG=SNuvrsx=M)uZ`%jVNeEo9V}dQZ2$)I}<$Nt>KnC z^=F^Gw)M>$=*Bl~ZoX#YqnrP5Gk)ncJ0IQorX6D^wEg$nKXT*a+wa|`w?Ad;H?I8H z)!(}MEt|~E&gPHa`iZUZl~29${@vH!C~bVxjSp}Az_nNH9bA9a-e2tf;_i>{{omK$ zbpzUCZ+zLc-`*`=v#v$A+PhzJ{e>&c<=+I+1MTavtLEkK^}oIJ50`#)^L{n z1D6U{{_)ml?0)R#@8102lGo4pj`rHE$K&I2J<`Zj)KW+vJ4GBCU?ZXx9BEN8N?=kj zMW*_J7=%?v=y9pD8*iNfmij20uJ!d~)f`iFMafz%IxLJc*+9aphDhmgoC&f84c-PHK`5_p_%56A)h|Rh_C`g7s^r$N)>YhwA1Kd ztfGdPRw!BG+ag;FFk06c@=!B(40q)tGg8~hBIbypQ6$`UVvuIqLOKAqI>g9=@gzB7 zLx~cc=yi%g@fdJ3IRoUaQ9n{0x1z9AMX3Q1V`_yFDfG!^j&gPhgEehiz!%QNC z4dH?nLLxRv1cQk&g7kPiz{9;JX_6^iQSKFyk?19yly81y+=PTtCo&lZ+U>r`W6tmx@bcfz0BfY2DmvjY(&z}h$kdpslQ5Fp zKq}>GDhXwo0ED%}v8Xb`c=bQb0JTDh%QZ$=r4PwklS6o3h4C;n2xg5auZ^_^tGC1) zYZYdTarKvHfK?F>5pO+i$^nv zZrHJuf(~pH4reC=s%?`gE_}?KD}QwYh=yvYp>iUUC1qWVF)>SG^Sars$o&p8sg(M$ z$~d10XX7(KV+JT!tTx;l6+!*xOuA=NJ;r7xg@QIJJLM2-XS!*%s)Zc`oeA9KpPK+u*bfNF;V+aC_~VM*k>#EBZyGe9X+pgM9u3D)#>L~4xG zCZvu7co{9w0R$-yx`~2D2AIZZwvLx(N`-Jhf9MEUKo+UVgs-{y=gN763`z)PW?jJIrx;wnsRV8Q|tu z&j5=&f&z9`l!-i_ zAmtq1E)L6+nqXOlnTgx!&H%G$)=0ooZ8(YzdAKMA`fPt-D@85VK?OYWT<;ig^X3dN4f=dw z5xm>Tq>Cwb(8`60ww+hl`Xp_IC#kB=#3$)7oNmwNxb-E+fbnj45*eeJx@k`av3Rc{ z6+4wIQV{c8NCb4_F}Y>nTvxJAOx4M9q~pZ~WELo?Cgdpx9U;*{Yt-VqXoByuHbyo$ zn$dNk-JPk`-rG(vHqCrPKtZj3wUppTNv=@K&^##R^7@mPS-@GCEPK$P7?V zP$R0#4IGu?!odTb&B`Sar3+eG&kRPT5j`$w8q@2PW?a2C14Q^?14<02Nj@46*hV-s z>9^Esx)B;nOuRt{tDxrzltXw1ooUD3jTu)svM6T^ zn`c@C8+9_l6Jynz0cJ;mG&fFV?YiAlOVI*UMGZ1&R#UoJF0|1anX+TUxGoYiS>3xd zn`4U_s9_ArIgtv2K?$pcrI1upq6M9E;+@hY9UCQcS~<~~Eyv|wzssDVl8zKBdRG(V zATh3u;bJD7&kgAbDaHdh(F=73_?S!0WOegI0D9H5p6>NO?)DT zDoN039vjv}R#rUOR<|~0bHqc*dYkJdvgw+MPKGivltsy5F~kWgN;p1%a``SP4qNHj zI$oP?I!HQ_*2^|t9)a4Z;XF(U$y~E+2X(2r-$g=B9PFn~wvFDkrB_N>GYjxnd!M%PDQCCU$952RSA{C=`NlN! z8|!S-K`4x*YH_GTVDU*J$%LJFu9PzxrSd4-5Q9#FP4%-%g$^ZV0riR&iJOhEMpMk}$Q*31IrXLIC2HQ4TFZL-kmp!HFQizH*T z5M|JK*QrN`kjmxCift*)8Q|V*3qTSBV6L6fXcSJeeN<_;s**9PMA29ebJSog(6T3i z7T2F3CtHB9_oW-3e1hEi$OA$b1eiOTv)J%nqGb64!rc zHCJQjVEnBa#&oR8(N&WTMuR~m+7t9Rl+DyQt&tr!>OGaq#K>rY*E#*<3?PFyiX}om zdt5=aVt6ZNf)6*O0-YVB5xvetMs7T}nF|-b>j|VrCw1zHbJo46#O_(}OI9=5wQNwp8Ool#B-|-KrvT70DSWL7dl( z)J&0Y&R$pILTA*&;>9W!2c5FG;iOW<92%}B>La=`CL%_4m|=rqt(TjfePMaoH9K8IM0C@SIxL4 zb`uKWbn+NkjO$H3+d$+-!7evbMbIFHCnjxVG-%dO*71*y+!u`*`u!g2FUBbwEK;_ufF=htBETgxnf@a?aRZ#KTHy5g827zKP znu|npOti#kXetrrn}n5n)U1tgRjK1O%NmV!bRs7f(raeXD93?6?Za0A9B&3Vv_d5{ z&N^I85=s<5;R_6vfb~wZ(-?rxG#o1>Q+fprMPpjebC~|wJACc%lK{R0fDfuz-Bd0^ zP2zoKR2Ug(9BuW~LM4>M3v5#8TL*C`98#~r?y@{H0(fXF~&BF6=~ zl$10&gN%sd9b(K@H8fWY7Gs5`I56OP4O|MVIOiGFxgv8WwL(6;--Ok}nJK38OX zq&#@WH!m_<0FNs&KC&G=<5>`yO?R1q$n>kgf!m6}R)s`Tjfxl!l@uyCfs#2Cg=KM) zN4mjcK30oDglAOei_Bs!g?zZ-(gLRKBiq321L&uNjl^F{750g*wx-%)r*b-u_fCQrzBo^fdb)Ao_x z;F*>ak(mG-uE_YvXYd?bzf5Gt0G}%|K9U+dURb+*80d|@SKe>&sz0PA7BcA+4yL1;2G21yoa~k>0M@~GI=-5epN)~`dFPa zsSe7WE_k}DVRKJk9&_dS>1XxoIR5{)Hm<+<$`@|^)+_kV{t)#;4!i^$0MWeURfc(f z`zx%ukh(Y=gK?^@n+rEQ>lJB9=0JaB&YFltq9HdJX=&%a==m{rFG-}>=>WKYu3>y@ zV(eC#T(A=JNf_k804keWG13*BtQ={J*;1(6LQJw5M`1}BlnEoAN(M942(diAw+))^ z$aU=%3a@MT>i1&)`r4@P(owb4v71*gBkd9RRIr^J#y^csoMsb%ac^-sxjeRRl|r~! z?l+Sl9)cnOd}+5U)M^rvSjM@r)*eEM)+C1l2W6%S_P24z?uCKxBA8L5O$-660UmNc znFJ%p9)8YT3&LgglH9n_x-W?Pij4l1_6Hb87m5wi}GcB7jD zmvIMVGN@^~q#O!z+rw1@!W+shhCYz2XYb@R_)Yvwqf{?s?Bie`HTzop-9E2P? zo{onDwQ8nX&vs$iiMNRyLzKd$szZ-xaEDA%!E#W1Y>8?==I6{deZW1va64u_Ie!pz z37T(z=cz;1!zP|`@1zOm>{Kc{a$oLFzjU>6YD0PzRjbJZqkl5?IB99&OMhKFBSH7^ zC(*)j@cFC(JChrAzxF8xx@!RpOYKv!mqUB)Rsq$Bv>sF1WJ8#wd*gfz>o@c9YC@in zsRHWc`vM5I1rNC&`y`o>1~x_`#g@|3*+?%67mZ1*MVB(dpzd_zE!s%f<7_XbkD{u^ zjY0faqlSd+R)S4TfP=ki*O;Q-lx3t96k0u@HW4$px7gp=)VrE($e@{A zcKbb_e}uax?#r%h`qA5qGjZX6mL{%NE)f;Vq^abv))|-#(Xx}ofT2`~OatdFqr*6s zG}}1T4=yutci%u?A^Uc6?kIux$wyrK`Up^>N2;1;J~9(2bxPT>c(paNUvWnPL-E-9 z6Sjvl%!Qc%CiPFT9skaiKfPjI{*%k?OaJ3i3zRL?xBqavzV-WC+UD=D8P!z6rQc@62;AI8y?AxSuZ8J@niQgDC+%s$X5K`e0t8tpii(v2edVH;!1PVP1usE+N`Mci=EXW4pL@ZW65zwjc(LxZ=U!;} zCg4JSXwSXS^i9Bp`YxY)L7x)f!|{62OVIF5zy&Ws-8TUjyacr=0X`mj(MzEDCg6gX zK%Elc!&QERD21KkSV_f4sIVAl&H#%RZkWUt$Sr7+UL;C>e&qmyuMOv(obVFnq{n!^;rsoYqE zBh&yC?cxy?cpU4=WMp3-+K#4LAfca|poxZ+^K?^sJauqW`rg)2%GJBCP=MZf-!a$A zhey2&A$(okXC7P8 zl_hpY7oU0r3Qk>hgk-q7g-XYNu{ZfoG*5%;gNuTIT-`nY?4I~$b;fC0jL zBS0}F%hW*+?22sYzP+>VgD${1d&)QKx=mg(-z*%2!Xek7Ej2f`&e7#$IGE>)S$N#x zCR#p(B~o2A8q~&VGSROxXs656vyuCE%p90Zk?Gi-+#uiYGb2hDuOEGP%Wv3 zhDoFv4GRiKK%k0EF4V7^O36YZgFc1|#3)zL%1}z|)O$u%j&l2bvxIeUFI6ayrw+`` zyPc!tbekVGBhyVqKD^MJY1%rV|Eb0zD4x^|Hx(0ZFIWP>@Z+H^z0E5qK zG|IER0*|`ijXoHMAOOBp^E&n*D&!gm&`=b^>bP7>6l)aQ)M{XUEeE32OtISmCApy5 z{YOQlCuJdlKteQaqOj4cWe%WtL3OIxhAbqr<8)Z49~3e%U5n$PehqO_67ZEe#(o@X zf%;L!A<2?OtDYQK%_1l*RXS)fLkaZ~6#))7VCq2g-lZQ=uI9b3vgzlTdT|?l?5yT3 z(WNEYaU5s=3TekwAsw)_>Xzy1N7$ufN|EJ84td{f=as@0#WhOdqPag|hY%!=tPBBa zp&3vrp&l!RvAW$VLF1uK_UNA7wK5!~3N=Dh`nmnC4FvDLPqk?&nU!fFlS>h;WTQVM z60(F8sCW}EG!Dkmtb{^H64V+3#osJDgimUSMzn}=DapdJf)iLKY==o6O65ve2}R?D zgKn+hX`4Bo`pp0T>W%kp+(K@4Z~VLKzjysJ_xNjHvHO#|n^&K@(!2b@OTT;RGj@#a zZ`%6k)@wEkVBkyj^E~#h*sk{U)66Gd{rru`Eo?ZZy48u<3}wU!SIUFRIeLj37X`?` zgvcmDCmBL3^wN;H+$ct&#SCw+;~s4_n_Mqv4S^uDOfi+#>I=tE_f>4EnFmP0G@|#W zksXzLiLMaFE0tao3l#Ekr49-bgq*%&j|~%KVrq4`Aa#jhY882lS;t<-J<63(x`=}s zg{dlQP>C^Wd!I%SOe1n{8lz5Pl%x|w1kNqTs4iwBvY^BUes#DpMTT4;OR78v%9hJc+$>5aVwV1 zqOzS~DwR+kZ>uQ*iX~Dy3X4IRr)83I*M(a}-XfR1j(c>{VbV1cD;g|;7cvG@7%!}b zp~;R0*bd!edkGuHYjUw3>p3w65nCiewXu{QA1XM5DEX9DL_#8{zXdrfrDyTIauvOc z<6e~XM8PzI_omSYwM`o1CMU%bXqD8Zl9p0C@nC~>hJkvro|U0VvD+@UlU#fid8hMD zVZ`gW^=v;!31ZI^?>d-Ha6d1ruqIl3pWGmLe IP238;zkOPAyi` zBM}MI+f{B1sxjFnvT{8Z30$=vi{sW$G~RS? z8YtQvlRX|AmI4!X#0lYyn(K<~G@)j7y;)$}8KKlQI$0sU(kRW>g>%blxR>|GLvb_i`QT0OalVb*uOUoS}rS^HO7RDkRsTIB@#Raml-mRHbaIoNjkLB zH3N8-)|utxMdI`2;Z{%M?6^m%VOHfV+&0EMXVuI2WZ-o*z{9{a-gs{s+JNeYc*3d& znpCqa49qGgGr^2LDl);iNEE}-xGbS&Jwn%4O~ac|MdFL&)>ngCxfa|@Hw_=v zjaE&=+hI0xrmWHW3PtBk!#&J?&b?{)sJOFg8r}}Gk@(`c^;L7unTC6qed69Ue1zey zn8um1E?&p2ujI0N8Zq}U``P!V;UkD|6?xuHfuJkSf&#CvE^^K^;$Ry8;@&iTgaECY z#_4^0mBEd~7RRlxcyZ1&++FzaP>?B&^?q8Z=y`S?Fzqs6e@54?k z%a}Ulegg!}dk@A{@K(-PqXOS(+L(4m{{(86LnI}ASJA#Yj zm(%SSjjwJ%)$NYx?x@cVnPI9OtkOgY^hFqtLHiX8;b39j-3PwimfpgZ+rw`1;613K>VP{%1lfy6Or{6N0N}OfTk7&wrCjpWdfD#Y@{CP zhC&)SI>0-fkbeJ-mFsj@kp=OYsUt$vC~GeB%x6`aX;HH zE1g!n+`=aJzs(Ho12ut|4TzJvc!D2=FP|MP8|sQdoCSiTw(vx*MMGGP_mq^ z@Ukh^i~w9IlL=NF*MvzE?doH3Wi!(I9eR>!!p!?uu>akj3{9wOm0x9WrL~qxQ>)V`(>qTwGKj)e74!` zl@ng_|Bt5*6!98(E#2GrmW}P+&9Au;-uwA$joqy)Uwb*a@hv;QyyadFJQzQ>)yq%6 zQ+eIi@=KBE8TWDyM@J$&azvO6$^tS-QPa;onL;KiZiQ(a9b!#%m@DONiQ)$;Wwd31 zAE&6k0p10NIh+`Zu+d6Us!`QZB!v?h@XU$W2IH{=q=23$W%uuRu+C8ApbCfkxh6WI zO9X&T*K(!$C_h1m*^)S4Kmph0{~gA<0~{N+C^A7+KX&K1es~VL+QJ zeWU!bRs=!kEyL9aQOGx?a(*(^uqN4*1*TqYBLfkhN_nnCiEU-lAb6Z|}ee5Pwulu&h&l!I0%2-!JJ?Tup@LgS058j{D|Tp^bqX6}md z_HFs{Tiz+J;&1e26Jp;C2BxCD%1lX9CaOteI5hM@T zbVWDdwmlXt(<+qG3NK*wyDVO3D1wU~gO^H(@5>xtl%c@-8k>~bf~kb7p#&zIJWf;7 zZDM@d9HZ$n5v+D$7>?wH)Hpy2!;S!HvPd*b<6Z&6he|`ObqJNJ4(AL7yv7^{UoKN| zKU~gV)TT$JgES7B`eG8pmx~fdh=U>-Ub5+-#XzxawWXShG;0=X$$Tnm7klGQ!pID{ zHWi6N6HX6+Cc!W>$K~KUE-yO5A0%1MUz9B4DlCO^VV+ZMiEJSSp6OJB%n~l+A*3q} z!&(!~_cT2m9Y>PA;UpGUHO-M#x-?2y)gYU%Rl?T%(E?h>~bSpGI)+?jdCR^C*w*NG&)_v zWsJ{;LBI1}sMlu_9X42k)m)Ax>)j5O!PHDA%Mjg(E=J-uJ-_?CX&skewsYKlyz*Vx z%Lv(MD3eq;pR^sinHpzWHfRT|Ea5VWBWW$2DuhY{yBm?DYLT&W@#HAbGpPjVav6Z* zQM_B!j5I#aWzZu5~%E8j~M<3~I}ND2Umj@tM6;NrqM*ZOrY*-!CzmJ~scObJ z7~o-6wfFnr3f7e?y>80=L03D@Ha9Ggz}>KV{iob`S`gXr%ww7gI%-15Q*wn?QOnJe zYozO)oK$j#or8Evs8Vo3O;PkDr_xp@b2*92H&fLf226k!wGGSVemZYS|=ofeDKoLPzD|&j~168ejNY}+F;?iCz zJ_2rE8AbF%;L||D7S5AyWO$`JFu(}~{P7hCioHvKd-i$xt6Ie(!Ih`IXvlEmTo5>w z3GoJ!MTX^eq@7{x0tBj}W#h1%W;IjJpxOIB?ImrpAR*>~RI;1Z@G!d{Q3bKOZzP)s zjL1X=5EP%Z$Ls`ForI<+wpDG^C{)73Fj?$G3ms}Q7E-3x3>vs)Fseo3SpoOv|97Vj z^v-?%e>G_QcdLB!12-?z}aqwrf9l?bCMct3P@5Q?EFeKXUnVE`9ONFYY|K z{r;`r+KO+!cjNar67#Q!TVFr_uZN!>a^S(?!1JGdG`h91sfC!0joqu8mo7bhb^Gx& zCNu(*F=3STVr868!g?3Hhp9A{s8{Kn8I0OA(T+$Vu7fTq-?cucZhN!+WgqQM+~2+L zUg73HldAXpBS%A*(kJdwy7jn^)WI30$8YKDY20$DvwqM0ZLdhlJ)2JW{AV3Gw>EZX zH14|Ge7xReT8RSFEY-(~I7H)Rw!@oPGNqzq%FLzkKxUZk;gry4hBFe6%haqV(WTA! zSJU4g^UTFJ9SvMAzWE*(FFo$aoWk2dEX`}xkxYxZhNgfHZ{?T+mB^t($_Rz(?RLk& z#2JZa4~FYg5HGou%CB*MSAS;}y^FWj&p&?Dcj-O2NAJ$#O0_$-8r6_t_Z!wIpBU2p z0nS!T3QO1HVIsj{sUeG(O5^AWOYiQZz4dhNxD>wCaDP`w&vbtFQSXc%6;d&j5cx*D zXFEf4LQil+Afh}LOZT*p-6JGBTIH-X(kjK5=@DJFDK%5Ih9p-;N;_y| z5DQ4@l9ifNZM?5LLbNeTk5kPcda~#j@oDR$Vc+@X+2uY&8Z$GKH}1d#XQsaSx3gA z9{K3qG$Zk3KUyc4E^TlAWB2zLtg@SDBOtuuP$qY=|;Jz9IOIn|?B-=!D%s#P3a zynY0F)I6g{KDrj2)uXj^PW34K^)8(e-ZPz_eWah!BOm>OW^|s?qqUSy^$7Z6ms0p^ zJyZI~QR9pr`EY5?D4m__*3vlDqbI-3rSag?J=6GENA*QL@^SY+BXOojYe{rz+y80z z_nV&dOyQf3YG?Gw$Bpfb#3kptwO5T(J$m#{U3wpXZWX(@@7#X0 zmd>djy^eF~eB^ar>2%-!_crPqH^2JE8~1+bTIuQ^U3u>Et9CxH{rQ_80bgI#&+Ym< z;j3FuKl)B(V`J-cR}dFpG)7o5T_{pq0p%W?5E>5T4qQ;665i;rpyj6;O!Q=&Owu7x zLY*5f3+{*w_y{<%IGF@+?*Gg70t7Z)2a<<1>p+o|%riK<^HFTHNPpxA0 zqP+4{ng%r$lRWVtu{wWIk$0HXs*;h2(2~en8v?a!^EDY>7891pipda`4wfJ$1Wx2y z){f%Qd^cFA;DUy-?FPb@^6^T|vD#Bs2k)|aMH#e6e6g?xPL~pSDV-Cs2a44f6@Et+ zG2TpcazvsiQ&L}OswIh7#%k0SD#*BAqzi#j%c*BcF3w_2RZQ!Zu#j()DNL1OY&4gQ z1{qhX5AL#hMJ2RId@&-roXtvUN=zk-4-~5}Dh4mfV=RzT22Xc14oVaW1jOodOAH7G z^X#OjHiZ!uOvSlzo=Zq}yv5pyI#X(5qRunf9$b;&2{mQ)lXqFY>L`AuJ{T!wid?ZQ zlpibua#6OLkP7J*87`V!U*yR^4kt@h$Xdc`obG`n#{M9Z9F4LXB^V&3BAl;9@Yaw{ z8la0fAL;bUV!z$8>#kG>?y@?%%2qGNMoWyCOmY;Sd!Sf-VNueU#0o~dOHYWV+$2e{ zq{1oSgaI17kXM0MgJ_X26^Hdv8ZqIK#P&LUk_fZ}7DGFkuIXr*e5M4Ci5lv%`b~FP z9a+Wd#Qa2;_pNUTy& zJju#jTSy6IlnaFfx;2v2u@bU)2(N?!Q>os+%j#A44e`Y&>7odZdn{RCqzB1X=Pt@t zGs}G+yUTDa!!T0oY8hCp`);YFR_ovxBy~$_-A79;30Sg=fpHuX8!eu( z@qNW6B+g;t#3#Oz*iLM0V*@_1jm=@liBD`y;8)%1VP_@Hs(LbBoX>CP511WQec!99 z_p0iZ-ccBI12J<-Z=p_PSfs8Af@B_E1LJAfa>SCO7DBC&dc=*=N;2EDdMm;PvhU@o zMz)yhzV{!G#~ShYT`|)i(*~m6v5;hYO-C1+nLtog_BgCaz(M8l!4WO3sL`>csp`C* zti_>ITbl{pQZSj18Z#QSlFw+Dv@~aEu4G!YaIe1X<*8O}HPyY=qKkBsK~uyW#Qun+ zTD@b|qMh_d^4?aWprr{VNrxO6Gj1MaEm}}@)`=oX%}UuqWr!SCx7rz3j8p~-YNOHX zu&}wTol-lDP8V0=?f9brPfjK#o|w3C{M+L(#bdI6m$^p&ZPYgM*`0bM*glTpwC(pj(euO%?9>e0cJHec zG>uO#NO+mtS$DVF?lp8dknq+h#ou(e!`*H7qFoLo_@+t84++D8zRQ6GKi8d2xpmvU zx-JJ2avSYzG0)rX#kw2>%h-x88DOYV#1~biBcUbIfT=7whufgcsM(mqt6OR56NQpF zv_P)}@C;(|Y0PW^b2m_oQ(EwvEsUg_K4;kPvZvZ*gD0$vf=o=sRMSEw)0R}J1S$#` z+i_FR*SNRc>+NzN0d+x>Os0&fhK?v@%bG^jOsPuR8mYrDw8D8okp;xOl*8DJli_d} z4m@2BB%m%x$T<$ffm@sd_)O*t6a`Aew9!CR6-2z5dey65ao3juHJw+h^Wr)Jr;SdN zhRj+^UKkEsA_x8zXUo$FrD9>iPPR!uZZ{!JC0ohVacdwHj5(aliczi8)3pT^sQBvi zy6tV0#hd18%Nbmz09u9chz1MU(kbt@%!NUWFgR8|k2xdHH zJB~X8?uOT_bu)=-rRqwR6VVdq^QHYNRmo|$>xm^cxgf!F1BQdEx*SMAU6A0IF`NS- z{(t12k=Z*ZKQ70>FR%E|UFiBHAQI@>yDuDFP+>Nj!5P%;sOdres=$i27SrNOb+Z!5 z)T)^{70fo;71G}Z7t)~>)@~jY=&e!@NPD|?flIV3F|^@~x>~I;nj%v!BgnkhAayZc zUX%6d0u3}&M~fjWl@SCOF{7m72%WFvsM+|Ih+r%vWD zqtISLF|olBcoJ&a4T``k(6w4TP4`Cq44p&1!?0BNI%V%S-h8jkwd@lms>KKV1VO@2 zrkLK%9ZcwNj6cpVJek+#P-DSF3U|5!>cE^jMZ44*cPiXMzU@z z(uGAUaO+hC7`581v%sYF?G7D86RqafFXp58{vtWLVAcj~F2YNo#AB#kIveFZjsiP&nA{CB~sPSW5v{Rj66JAfdqGVC9qwa703RslIZvQ(~`% z4z215r=h)X=e-&_tXPGvFU23wXHfB)j|3O`u26I;UeB}O&wOQ(mzU$_y`pEEST9?< zsmlFP;}+mR&n+GIx&)83Nb>de7?5INC7x&Ee9DE**xlR{!XJ4ayZrPpS!3fYz3Fd4 z|3sXxW-R=gW0JXz-@$kM@gzSw;gfE`Kk%>PSFA#kGnF!7#cY;XvH>RY5suclkqK74Tp= zkA!>($mO)6Z?gI8AY@o=s{TY06}0eca&P$f$03oAsy?%jMM?yV9s{ z(*=*ks89Rs)pXhEvZ-8ZtzSjDv!TIPg+hX^_U=PFbW`Yc*y`Pfwx4rGr_}Ob$ILmy z*Z%H5{=3^ijux&cgTH(P;61OMJgx2C8&qm^%Piy#vK=ievxY^c$)R}J7NPyIaM>LW z0dpmW<&2y|8(74(>L?rCwjxu3GN`YZE!NX!k51QGw6KkI$cS)ihtj4AHkT_+YuM^{ z=zY0jq+(-?9x_v`ECw^qqRNbc6vy_Q4`iJIL1`_bos8ibf0*FRaEi{u*gY^YV(egYhAEktW| zS);?7h(wfWx@Ib>P^ZbW2)uv1j%0`|+Puk9gCSLEM48NG45F6b(YL`5P$seaMZ0=VN;}0Yn8n*m-+s;0prf4pb6utGei3#B)enpF`1vvZ5(O zgB3j9j)Gj*Zo)(9a&gc0uk;?PQ5U6Cjw;S@m7pb@^`WG*g0w+qT*inLjK+{PLX_KO zYn4v6%Pz*Ss8tcBXsO}F^@U2tQ8N4ej##l~iKIf_m?}lqikt=JCeR%^f2CKfg?M4- z3j;}bnjZep*(2;*<$cr+d_j=#?94B}lqXuDy@coA!FPNKZ|H;W`2AsVJX@Rd0Syi6 z<2wvj+F|bw0~mQF3InOsx~;F|DTB6cX|b_j)XotZQ%RjIXg!Pmq(QA*j#gMK-d5xO ztl8Q2E4O_m=WtJj@B>fE#Z<9vwWYOKisiy_6tjRUx-~~|L08p)hTA%1x#cq9R3j75 zSA8n4*HC2|ZzkOAI6ooxhT^YlLtm2MM_(NY~CO;y`2j zk6sa_FgW1EJQDBu3Mt^r41mM#_$TC2AqMT187XTaJZhsuCTEsLC^VE0mnhbnUS7YHkFaN`%vd>jgK+Kp73YgQ;{Z z+6?3x%N2T2RiWw?wuw{_Mz7RjhGHeEjJVvbj4R^~)Ujl-Z88}l-n0;5=s@p4z2_jW zcl;B&1Yz1Sz0(Xdg(ZCFxHAWu)DyACbUx}S`IR}!hCQKc^?iroi6?Zh>m0u)bf9x8 zq3-#>`*t%E)x>pLCR;I(B}T7h-0f^kX-}I|5w68K>y#yKK^@^tvAz9c-V=8j>%OWk zWc0UK%`)SPI0>{#h5TwytzvHbKrv^1sOn~|xloC%)ZA^{;7%8sYLr9+C9Ihw{PA{9 zm2@Rc>7+BUSSa}uUZ`*ExrJ4NB&H(}T#Xao`?n>Frfqc+v z@w@Z5zt!_Ru)qTM=S(Xbis?9#j$@hHi(Neno4mYa~N2*q$bBh4`0t9RokC$yZ zCgi0Xl-q%5DoaGnZZ9zf9Zu^~al+>DnGL>18eC(D&dmbs?;&7wCK9*%IBPcN$6_`@ zX^(iTCR31P>^Ulnl+|IA(#5KljDBsD=(Gjc=OJJTchR<_BojKeUC$MR_LM6V&0)=` zHd0V!it0cE&qtlrY;DPf6CGB7{nZ{9C_@wRRr4*TW($?mEJsvfT<)J$?^jh*%<=UR?|td{t8tTe3ycSfGMA9j!FcX$i2ugP8;krE0ZwX-9Jf zO=Iq2rc@2ISMUI>(U~(Uwp_LZBY36BrlK6tX$r8vg@BFeEU9EB5X<{4EN`u<%0isDc zYc0-HcYAFfWietTIt>B#HxRHHTZ~EBz;{hHyDg$j<<(`Q-q;LLfrP6WH7unIiBKy= zs>?+K(6YJ!`)df;W+K`k@s*m@8)KtLqFKtfllr30AEdC5Gj24stxZ=F4f`x9Ft-^2 z_US!#&O?PtmU6yDhDzy(rCslp1PwfPD%A)~95NrIkuO94@Ky!6F`ywLME}P{Py26p)o<#uW3Ha4%YANier5 z0rtlbuyhTLGc*#(k(Q;BBbh1M155dA3$u7Zm;udXJg%zN!Q~nTH<$!kfc+5!tS=r( z(xsL;><&Av!AvP?CCx|#x4Dd2bw1g4GPD8jsDdjAt#Y#jcDZu`4c$~z7gg6~|nw zCB&%^o$CeIZ$rRJ2tFY?*9oxSf`F9}Q9^WL0_-;-U?pVrB|1?7_VMA-2U0Te5}k+u z`xpeQgpd-V6Bb|}g@Bb1Cqi^c0rndZuo8klh)zg={W=7!gg6nRLkO^s3=b4okPs+B zbb=_WS!BVwcQ)bfMYSe~PSUbJ~l2i)l=Bidp-dl*(s6wt$U#YZEYmumgTNIcCivW8n1gr;5 zlpCfH#ias9Gg2cMw<{LPCMaLg-&)C<-HxQ!L${QP815z3%>wKx5U?42%WtSQmg=^c zz1m8JoZ%IH4x|-nm73X%&8lm)N_oa*PHPF2ST_lQr4-%QGfNq?glVi*ibzHS zcGi=LH>^gj+pSEeiS<_tu)82&X%?%sLv?E|Zp7mmQ`V$b1vy(&+p?E!Sin?3^TvEm zZLgFwN@D$50d^h&Rvk@o+B8#1c^n9xRgD!Z!tm}%NYiX2N zz|KLyN-=%cuMuEpAz-~lHkyj6EM6bwNku9_JW(z?P{dRB7DBa%#TINek`1LAHPJ?5 z{b~Vr1_IWDW&9BkB9dv~oFnSfp)BVJxb-Ct1c=aDpUSKbx3Fq1rVE2fxJrPXhJZ~L z;>Dt|%s9f1X3lJmHQOp>7R9#IzTpjZ>(MpfpD zKCDSa4Fpruvld%elXj8mI;R4#r~o?wVV>puS_-S^Y9KkOMuU3tY`p0MN#jwEJ01+V z>ug+KsYh5p?nnVG(+jZU5U@B>!!nMhMNO<2Q&v1;$apIaGaEA444PUhtTQ<2wmqxL zC&C=Dt`lGt5U^4V^>wWPD~EuUVyLfc1lTbMSSg13x>|shLBL8e)YlOKb`%0uilM%) z5@1Ilj7hh;sWnD@1&$6jlUjqWVX%?{{|5qAiv7R7B*4A^0V~D+U%x_t{XGP%6#IXDQGk6O!bX*1 z|F2&z!2S*bR*L<lX^J;5(RMB|4-eiXhf65Mcik0#=Iszka>|`%?&5Dfa*Rc>?TD zcHaJ%kDkHD|DP#;JNV_q510Yqg&f=PLY5%^!&kh=3wcYI`+;SFCA@73PFND8OK2}a z$On5NOVH` z6+pF0lfJf0c>@?;$mz^>G?4Qz8^}1ioUt&~#i|<7l_Q{%DWYD)+cBnTXoE;$C@*Aw z=v^=5$c7iP1Yz1az0(W|$UW`{n$#0ff*0~}(xl;D$bE<52`}X1?1iic(MDTsHz_t? zVv+{ZnZ-33bGzbeG5%Nr#oHMd+Q6-43U3*G%edN?sck#xp+P$R_Ii=ico-{KXm?>Z>)udV;;o+OVGKU+iukV+wF!wMf>9arR-M&j^V-a|HoECF}R+H%%{$}Fe0E~d-vNWxJ^>rG#hASldOG(ujm`Js#c|9j_Xc+5H z0=Y&Az!E~3yTI>x@)5IL_4RXa`|oV?`AZ+toO!|X?Y)^hzWn9i#Bct`egAPcxfbkl z44*r;3*5TQJxQIk=adgBuXMfkBhNmt_DtbVmFUyE9*s?&Rs7oXkCJNv0a!u=WEc3s ze;z%AHQxR6TlZf0*bmunpX>Scc@K>|{yw$-o4>Mr_j^CV$u(R6mJrL?1%9+z{PEKd zlmC#_mk*rcxuLlDo42j+-v3_B18*(u$~^RxnOyS=z!HjHbb;sYx#04D(!A%cOaAJC z_3E#8KetBQ^FZrM6CZr7?Oyw%C*Acna?K|IOQ>bh1^$d|*J&U94mWY~duMh({IK_1 zt8ctf+5XXwzIDd$52pX-s)~|a!vtUnB`UhWpa0OzkAE2pe)TiA?Y+Bv*A*|^ee0RO z_~IQ`|Jj&v&leszp!+nrRuX_E_!V@4f2e3Yd-EMHzUk-qRc|$Y%k}t8fBoImT_64X zrQhBD?AhxdJ8hC&D+<68JPW$OSGj|~fBtWxS?#_19%kP1#oyn0{bTFz`_^-N6rHDE z{pH06{p4Cf0G8le&;@RP`JC7O=h_?mFTR7@^A_sB8?U~5_Mv9z>33f7=HFd7wfw6G z$hCZzWB4$DF0kRf-){fep~!nzPrfX3lhyP%t5}$P?%sd@*N;85`1U8h_$r{%IRRLL ze?b@c;GbXh@D1wXpWcky-Jd|`9wVpz@{@PHt=j%R@}UQw{*NcWNv>rDUg)>un!?^*yU6lWU9sEWyX13+(FL_RMD! zjXS=SjGP(#(XD#_ljnVqz3b-ad0+bE7yt3c@3@#;%Lu>{ybQX)H~-3i*GLx1u?q7?eG89cks(@fBIkUuK&$jA3Vowde`Sp z)BOD4$v^zyIn>k>Pd-hq(E_jpUxO}i{n2;2uRS?Q{3CMiU#V~V!bhLXKmDb)YQAH9 zVshe+-iBcMtr_6{hpa zwS)jH!Q-F{ym0Y5e}DCB?rpx|rM>oVUjCb>*09O5&P6}}t7ki}x$li!;2q>zygSD# zpSMy#STkp(DrHWG7t-c*SP80(p+!wXZ%!quEA?`nUNJ|zvWe? zjO<`xeD&>@e*Y7nzV#z7edNBkz4Q%o?Ir;fKJtrqo$=fo?~A|tRO3TGDb6iFb=KaSetB@z zzgOwdeC>;1+q^*lmf(BP1-|;vA3FJt_%FUt`t&Vd`uA(eQ{QX)>3tV`_da>*ZTAHZ z-TH=W$+hbRUr6$FM1d(UQ7U% z@C|Gic+W{^eDp5-l-K;%n?5mma^rL7z7%TCI}+LDdq4M?=RWxAN54X@MFn69-zar~ z@A|+=sh_8>`HPSJY`^xAr>-&`eDc}H!r#lk)${An>;4k?(C5juhyX0%$-E2fdvEzm zZ@&JSyASMr_m@vUhrMEDx_s~5KRfHmN0XxuuKqRgCUPw-084lr>jFQzXEylkzfi60 zKfP~k{+)>P!+)YW|Ky+k_OXxL@Qz#F^&+d~{r@iPmU%>egL{}{d3^{ zGv%Npnw0puovi|O;QliN1|*!3J6i}(yd1NWcQWdQybv$G|z4&1*>G6NE>1H)Y!58OX5nE?qm zC>RFj1O_DB_jY;-W+gMQ!%HwDnSmW%f@y&P37|W@1XGe3*x@CZ6c~`;HoUWi=nmXJ zAuu4}M!vH(*bdx3E|~!dSKHx^x(Du8NM=BSH2}kaTwp+g?XuHLFeaIS9bN*NWCnJ4 z2}T75B!KSp5{yV@U}u-$1GfnbNH9ltwi4oj4;&B}kYHWztZ%}B58Nu50SO)j!<{1z zeBc(z3`nqSVHmhsU_gQ$zSB$4mdwBoFF{K(13S6|LjAwdzaN=9b?Qxu%fT-ve*Va3 z03>F)6)Vi%Rr6$;V zl`&x6RMEsCHM|++%&ww64GIWE4Irl;Yqt~jwk;lynzmn~C||%-Rm`MPF+pt()5ass zV8$MEEjCLTSAqp)I1@FcRjUD26>Fsm>Yxd;E4ElkCn_u+_l5&@d&p-`1+s}yR2|Y7 z%k`kuO~ScE3q$X^MBlLC5-mZPc1-WI0~0eJcRiA-h!R|)kCP@1b&2lR#S<>k$JZqq zLG)Yafv=)m)#$V}1GR88XIFA{UC5-=6wGYdr(unxy`ISweXU5I2#2=q60IwtzM$E! z3~M7{he?@^V}_bLueDVob-ydG@)>nHbJ>XKP>R9}%HX13OVVC1qBR6KB$#p;!tRLI zfRhbds*ZRU+ijDdAmH-A3q$X^L|?z*5-maJc5b^t2f0K`*=`29L`wze***rlL?5S( zksCsgOrmMxiOe37xL*<>;0j^slDQz;dU zB=ffwSBgrNh?1NFA9<28@3 zwj8UMl_@#$PK(MG0mY+11|n?x6}#I3ohIPPRb3w2V$y5LWb2gEj;g#$ zAJr(dEqYtYRs7`9@jsC!Z6EnA({nu)6wj%>RB@C1 zXY$v`&l-Cz@bUko%scwSQEuevkwX*rP0WtJV_c>9*X@|j$-}3N%kWF*=qZTQ-C{N9 zuAnbi9ImBWFrj5v3Mq}#on30DOFCy8WUfw_tETdj32*p(X0+8}(StLGPv*(oAcahK z!yg=oqEo7SEVc!!*BXxJG(^bC>h-m(shO);-sa0;%no!>BCza~3&VvVn*;JL~7%W#hJP#;cD~VEn zto=eZx{_kZxXz&ulSNam4 z2`S){hbI6HUO*v|z#lE35^}5o0UhVb00D(3b8G=s^$MtjOmfU&1>lAkPR5>N@r z@Y09nfEF*H@U%7wsDx~OrNd)@5-*_elm-Z>gtUb5!!kgT7f^VLf`CfMs0eo5^x-qb zDTJsfAj;^bDJ0-a4CD`=0Vo;3fFOztY-mHgSSm8fczz|GXX3R538e>^!>0psyc|Ll z9}vkMC5IB~5dalD4G`m15TYc3NbJ}u*jpxiK|&pa?^n!%4 z2Bpp|fC{e?5QPmyDg#tPLcl}1b2FgGt2;!^1Cin;S(Ok0!F1Yy5-&UuWe`M4M+uLF zpooJroff}bKzJaEBZ$O~B|KVG)4P95NYt3@G?Cnh}?*`DhY8Fr49$E@WKO8 zVnL)bKzJm?MWj0oK#><7i0TU>#ZAH^!3mV<)Bz=4cpwTgh?I^J9tqx#KzLYwxq$FM zlxYx&9b0(%Zl@A*J0?3do*OW9i0Td^H=^)J@R}@js(=bFJP^emL@EP>M?x0Jbf*F+ z^1=gA2STK{Nq8hU?lGM*pu`IgL}>_-(ow=A!9xuQPl;bHAUqHSBSd1y79ON`=aJwp zmh2RHZotqX>P(2-Z0tN$JZJLbSt8v^MmZxI30BCW+2N#&HfPc6v@E$fSIuG%vp84k z6aW=ocp%D7h*Sm$j|4BYbSDoe^1=gAkwT=nNq8iDRm*g8fD$h}5Jf6PN=FHggs-Rg z@Bh!0pFJ{R9smBgU-4tb0mZ!he)-wE-ni?^`A^Kp<~~2SGW*=@U9%U>+%t38^h4AB zsh>_=H~I6)*G`^2@!;4$k2z&e$!;J0#pvqjxg-Cu9s7S9eI{hv=yamXi5WOC1JE;Y zaPFR&aT=T|r|+!ep}1ci`~2y3hT!+n_&w7+KXW5H`zSB+(>;L-_e=pA*8&<6yaI>& zxrsFT&ma1pNkDHI(39W-2TkwD>65-^0?<(dIy>q-HsI_5Vl&P!6A&8-v1-smI;Pm@ zdsCTp#YVyN1BSk%j(a^~BL_5iv5}C!Vt5|kEyX5&&jsR`Bs?$1|epa{(P*6(kt<&~!Fm!+P3qWU)B9@B6`Rv|e!$Qr828ZpY>3UNfQGPx!Urf054k&r*qj3B z@nR#vsE4MvS!_-Qba=6mV9-O;=@FZg_+3uKgip?&bA29SC4Pfgz zq~-w)UTh>7^1~f9jv+R4fF3V463lsMdYi>&7SQ3vMuI61O=q*%bp8L|IWo&l{pt8g zqwj>a4^A{ZJ~ObITT5)X9q;HK(&O&W`@sx!_aB&vS%OLn?)wt9q`~g{QUQAW1~-d* zuLFO?ZY&g2ak7!Mu(+#UPsMv)Kj9qsd-p)T*JcO);cmx$2q)Z*kF(n`s^2;v?6Muz zdR!TYmo21RnZ;}#$x%9$zZolp5)M@(VlNVnsyCJ;7SU}xKgP`VD4tUh=DaQo+{=7H z(nHj3`B)&NB4ZA`<;(d=yD{ZX)cxiX-cqJ2xpvm!RhjMTst57;G0JSEB4{Q+Ch4@- z{?ELJ#S)@a8B|IovZ6v&dZltniz0l}Ka|^X-+l#X zZP)D>RjCoseW2U1lQGpjDqEA#*HYGMGLmtXQgk(AO0W%c#ia9VlQFxmnx{P3?K}L3 zW0q{XlE(@Sv#Z>UI`Rfz$z#D90VLM&s&R~4QTs(UsL!98{%VwW-P0 zGHWyiqO{da+VTz^(GFHkQO50qbol3o-gP^^X~XSUg1qmX<{>-lc6^+U)$O_+_X+QD zbUW4~=$2{66YQd{6;S7`QJd03_{vUgO{sO+Qw<|ZWJ|7PhHNexGxdBju>JZXjcB7u zW*ZHV*|?CWT~Lu0qLAWm&EIaN94U1&7iLRH z#Yhw}w>zgKj7VM=F1t!WxU}O4KXf4MpupA&FYJGI=IG9vZav4$mO-qleqrA$#n|Ha zRViOY2<;`r@eIBNORzcz9b^4pK=5q60>P{U7y)M($XHjUDdF<8;s4KDB}3{2@I%<{ zsiRSAcnKMJ>a^;Waw@S*qzs`@Jj__}0PFUrof(I}si9N2fl=n$M8sUOq?Fsf?`fBt z*;0h|xpb)*QmwH}5=#`)(M2lUO68-m2D7MNjB;64Rmz}xQ2Q`sj%Evl1`lGM5aL|c zD80>$L0``MGs}izF2%XHg4>Kk-S_yRi~j$w9(n)RuFvkeeV2XL`1}{p~QQ?;@(7@ z7dZ1!;=SJe8^jH(rlEa%iEs4XziJ%DxT^O{)*i-RJ&brCzy@){>a|1rqCIN5(f69w z{awZSzGpt@Sly-0b4chxxuO2#Sy}QE^+T*8$4XalTCEg3LLENzV zs-bhAbKEn9a1!o#o(V@h90gf=PAKG_o<<<>lyjM4S7}pLZ-V3n7 z`2JNy6tyi+6Xn)AGB$M;IO+_mnxXW!((nDN>S6R%z50D*@MBZC{i~`jz}CyI?A5a) z08{5W4Xetb-1l;@F?)v9fULgP+ zLs^u)igpBGdavEEx;T{kt(E2Sp?!O~-x#@Jb>Gmwy^L@4-M@O-&{^wM(j$XDY5Bv3 z)k}vG-%8O8tCtMz+e`ci;};KQyjQ_`7+)Ald}|rMXlUPF;v0-#vbuLl|MESTDlo+@ zPCUcvo*_iEhOLyru)2F_->ptN!|H`Y`))0Y7YyyYl_(lk&mY=%D`_ySo;S4bRtsWS zJ$Go|tp(wnVSUlwqhdo446A1k?Ys5*o;9@Z*6Vxb(7s!*?-@h;ZneJqS5F_JvHby2 zM>KZg^otCurwt{()&9DF_0*xndxh%=@$pmChSgJs67L1rSn2(%Cl4jwyGI@&uHa3T zlZF!S1=t|YfB&x@xnpG42Y21LYi|By^SSwRfX{z(?$X(B%-%7ZpFMZx3o|X?`Tywj zozvQ>C#SwR)dn{IW0QAH>Lz|Pad^T!{)_R)$5+SoAa>whg+=~L`Mcz}d}Qo{V>gb? z$v!5_$<7)5{AhFZ(vfcfGJ}7x+h^tYrL%w1*Ekbu1sj@>>j0A>Sq3*F4x+D?nlo!~4k91^fs?6KRY zpgjJs`aH0&G+3D~>CW4Fr$z`WHJa>bgJnBN_vS-QXkAOZU|WmEu6c*3nn zJVG@>87gQjl|pSuzvEW-JZ8$o<=k;5 z6L1D>c1XZJO*wQx0IamDv9^(+&1riuQFi-q*g5ReltZ_Qfc0t?cNCZ^!)4!=77}^a5NimfSSHVIdroCX!L=CO{Cjzqo`^n;r9DHkbwP* z(iQ+YlL?Pn{aM^zEE(IDvb7Eg*uN+(0k9Oa2U;d$4zpCPG-IrUVL9wwlrs)B<*LcK zJ)5UOgJ`r`><}kVN(K_COfyFLYpk{AxBHE-toLn0hZ+K4%^0?o8^Ms@+KhR~q~A(G z&PCrgbf_)>M#~_Hh8Od;vkf*KWvh7x4p7~<4IN?yK!3YbDPe&oZM9YyPm2takbr&L z(4m?DSob$0E{CgS!?{Kos5n-G1nk>}4pjxfP`>Vudh8L_S#(>7Y@iK!9I7`jGm0ve z*rAF5i05)tvE87mSx4Mv&$YcQB!~SHUlsr@^@<}`CQ@N@D3K3Vo3Lktx?kc;0w5MI zX0U>}NRs73Ea0jVz4>FeIA7KM5?>S~elzEy#1{lgwMx#^PB|~X=7QYtkp4p>EdUy$wo0|o+o(Dz2qZP;@_isZf0kG^SS&JDwn&I*Z zSES;`t#E*S5B+Bxq6GonOh^<^N}%L3TOG!1(iaJ1t+3M?bOs?Q^~+OI0IX!0EN)79 zxtiHekZHFc@-p1_?2R2t2!Oe4HdOPLxEfOm;sK|NhP-O3`d2G10EP&oFJP%+O=lvQ zx0_l<$PKUWg%&$>lK|+AWVliVi!|saZLtJ>F#>K5mHl(LA^^54xGQBd2l7Fy#qZAJ zen_LN?==)VbfW<1DjMAp;IGwUYPL{0?ecSw9QLcw4FX^!V2ruwQp}XG(+;;g8;86Z zD*I3J>jglNM2!lSEaiNQ43*LmOB?QL(EmVz9lA~cET)b*Ho%PNl zv%5h3z<-{3%gn)<8)xh@lhZ$$et7z?r{6doo_^Kzg;Os~{RnstymqQM<(@iwN;dhK z$@fj(IhmVuOzxfd;lz6;?w+VlTstu`{>=FO<9Cl&$9?1HDV|Y$RqzeVG5_=z~XnbmAX> z{4=muA-i@?Hl5ruAZPk@`xUZFZ;(x2C^k7(oRUt}t4`C`T&$4o+b5g8KKCd2}|kX~TI6 znHqe4j`;cJScPd+9N*dEhsPdY@2zE8e+HNyfPK~mZ6QC{v`$1l6AHC=o1E5)vv!8q zeekSl#92FCY}N1dHLVtB?KH8;rddP8j}rYK{i!%%aq*+0ChWE1&~6exJm!S;UCO6d z#R*#xyLZDAr|%L;-Z+TlA(7<9hsP$_Cxdq&V2!M@>7MQ@D@M$Rt*k7mHqXCsUz&c6 zI5`_#HqG0e;s7>25(jX*NaTj2r{!2g4vJl_hwt(y;*wt{b{Vkbw~4IB#3q|qKOjzW zRQ%|ub-h)b!Ux1w0~Wp}4jdPo zY#Mk~+#&tqN4+~_MHCF5`1w(SQ5MICi60(Q`}$1T>5@3<-VNH_9kQqbN@mb*Gx5U0Q`_82e)d2tF{Vv|i%kQ0aK6hAs@h*@!nj-#0wEc<=N^)w?6(Jpou zruvKibiQ$yUutognD28c~VYB$*vDc*6c%P<3hD~C3k>RAsuu=T; z0EQDH!><;bY+^VrGJLK0(NPTFBr?2T{P5Te_Z_n(A`kIiu3nG+KkM#b(T!%>l8z4+$=3`azUbz+lE42MOA zwc(c5E11~h3g{kQaZ(n=9s?%DBTmZY zVv|ji;ua@mpZL*Hlj0I5<+7uh8N3tqJpfHR#YwqT>@H4cEIFzm+uF^0-G|vFGgC7TH{;{G8Pg zj^Z)*B5PH-17)LG&l$B95OU8?Ig>8NL2X331|qa?-h1?;lJF;*LbO_NOvYDyl(EFe zaThXZf^s_2E;F}St~+8Rp5%xYSIg8A?s_n42+$ci*|fG*mgcf8MKEHOU+E(RP!j~b=s4r3n6nX)37FNc*>nsxs-l2>T}jK`K&#o+IHG}Mi}B@vp7DDvQ0Xp&p?Bkusf{>f35mpaF6dr2C=OXdo*#Z+m6Y9r zZ!aOS$KYd3LejMD#UUT3Ov*Wy41m98?U$BUUw!E(Wn9uw^fsB>Iq7( z)9I2LgHBoT71U{V(V`?(fw-B;)IEm9#kxzhfhR75E|1)mtF=RSGF_~iGI&4~bxlLEfX0IzrDdLqvx zkBa~2(`5VsogPpac-zA@seGpF^neRq(}%W~LyKNJmZzyIrwpbz#Ab5Tl)8eI zc2#TJ);m*-ON8(qq%>Yd)?fPbI6=x_OaQ&nmsU!&o0b72Vw!?DWT{Uq4Mv z>!+uuzBBc=Q@2l%ATHp4CLf-B_he(zIeFg1e@%RP;*Ar`#MKkK#(w~E0<4Z-H@-YB zQ+!?Ve#OlSpJK24*Ybap-zP81jq=mRo*nz-*x@l|Z2#D)prXJ>WOvJMlBs1Aqu(6; z>(M_M4Zi&UOGbV-^6#R7I(CHuAC<}W#2OwlQFMT>_&@bWV7mTaWMNT(Pl1VyhZ|&o zj9WnRiB`)__$W({yNGO6wkl3~Je)|zFwzvyyO_M$TBE8xZ51Q09eFKmWBJJH2sHB8 z$Xz3M!6M5>?i{)Eo4!(u@py{OLObno#d}>HC&%}X?}u$XF@DYX zHL#7x$93a6*v5)+?YI`Uv3y)Ju7PblHm)95!#0+UBjd=1oDG}0an-nL132i$+(CQ9 zZ!ZBKz-q#Z_3F_~d!`NBSPp`~;K=(r zY-5??Yl^RJO!Kg*Q+!47l?`A=%jmNDf|R>LFoCSE(d$(?Ik|UoFKpw9$vu;MU>lE5 z?w;HY+gLGq;pBy|jpdUUOkM!ncx>|g$@5_w%O=m8Ja1!~hfdwOljm*#o6L=1B+JG^ zR)5}acQm5C(k%Oo>@%>9CuEy@W$9KSz}TF*Xz%4>d}eWG5w@{n=JJ`#VH?Y5_RZ{r zZ9Fz}+013Ijb$^J&Ri-^#?S%ITrzXX25>yjQqCwD%bFcEbC`(r-pZ!skIFZ8qu8*< zQ}S=fzX6LpDgU~BW9u4Hx_W%Jj=f%|I*9Az8p(9Nji`=MVhI4!qKy7%*uZ#{~>JS8Tk+7KY(pK zE&sm!`>>6t|2mnN`29p~ z{KfIk_-@5V6@>h=^6=PGW4DY=$vU#JQTxaf00jITob4=6$UqrNdSn*aGc*|mOfW-V zW{L&E%%Q1xAg!-8^hrZBXEWFfW+#JkNegaBVoo|>P)7?!o2rqdI#=@S%mH>((Cl=B zBNnV$YQ>mo6?06QBlKB)HI!G8p&Td#!~~O|GLXydtowbSBnoCch<28`!=`7IdxoaW zfExDEaW05qfd#oEGOcVVrZ*WAHYP^UPO5?`GrIuVLi0Le;WF`>H`#|2yXdXyl%uh9ZEjsO5(WOGIWZj2Sc26yq#?+KN_8&g z+2IF&88kcMp3_W38zjC`vwCA}6iGBo`F2uY)cJ!H7IMaorna@|N}^$(CB-E=3xLc; zJel+N3{9#dZaI9txCPX0$f2PGg=@S{qc_YMQ&d!AXcXFql6q;s~x4v;WcP30vM-thU%XsgAdiOvOpj3B0S zdxoZJ+AO9^TCGN()+XDnK-J^-Aclw~!s*q;putx)C9x%2B#i|snA6cpmpkVJB7&IC zLK7JvrrshOO+{4}uaEMiB9$PXD3={5;;DNJp<2XZ3$_}`hEk21Xk(^x9v~@*=?pZ< zO=9Z7GX6+1M`s#1=ZN}rD9bqlZheVkLFO*4Pi0nzTUa$0(}jVUo(o6`Vmb{?>L@V< zi6o0fW0`S;9nGBC9BZ~!%5c!0BQR%sMd>Igxw10Sa0Jm}5{T(JfSMqtQ_$3oEvB0@ znDF7K3r1tMWvh{9j#z3_%}`k5ro7c?!DXd943qT-3+^yc%~w2OG(>gI=Go!JbP}4K zjop;VrCpwSL|L%M945kzs>~IASd)qx2&SfIEw->G?IP24P8BM5&H`iv2~zACngC1` z(*Y8s;YdVRtlL(~#b$`8Q#i}Ik+7Ln7vhe(-pMXS{S|wRL2br#rgJ7BDM*kUn&c)4 z@+nzjX{AyRhfAt_u!zTFZO@Wgl|)=j0a;0AOfi26_o7vnG-f+z08)YkjX{$-N`mMb z8fR!Ek|QlkB}Xz-v3`)Sp>x z!55Dt=~BxacJrA6Go`4NG$VZeL}ON+PqrNmU$R+2JK<6q+4Tg3?iq zUTIUd6Q!~~6rwzhY#KvsQCC^N&b2(JKKw zgP438Gh4vi4HV;)7QAK)Bk88k8TNy+lkKv>6IMnkO}29~AjM12%>O`>I!b~x$z;ly zYUqekwybGX&6KL7t&utmLo1vYSZ#=TDTlEcC&S@Df==R>3`o$-3((Y#EkS)prG&6z zsW zH=32Y*$8dC_wUJcM+fLF1A2I{oJT@F#L-+@(YIL!FQv3eTit6(Xq=|FRnI0hjx=Sm zvpx=*-Vt?5ceH?x8qnbqElUh-IHRssD~zVdl*_0ryBnk~=F4lcK3$-JhU#cBgr(BZ zbT;eK!Ksc0kh=7S5h-sW(4>x~NV>k~n0QCc^8$t~;lTu&m#!9p0VCpoCA@+_8(+sB zaPLD5U`8b}BjH)&55!ElqXfL{+czTRu>+cyfzK+1&N3j$>#u~z4rr2t^!G|YkJn!b zFCNhJj_7Z;vjpfE038W09nf@oUQ^PYD*!Qmvykx80ZpuDvseVA_{~DXBL+07W9v?D zZm)Rfa-J73YzZ$N(7bdviwnl)YEMcxQp<1q@rly9P8b z8$x*zpur2Jgl7$Cd$%i;djSPrC?&jNKvNhblzRX@UMM9zWkA#0ER?$e9bPCUykbDp z=@H5c0V!T6B|K0-lRCCg_P&wCI~VZ0fMGY`UooJ0=?Z0`b3UNKi;RS43uqI1kjR_| z=VMdF>ad0xP%lMo-~QYsTgy(;_`$K)6-F1_>ZaBh^%Xce*i32- zx`x3<>f4Qqj?u5Q^R9^6TA^8`Imno-!xyF}1qq*Wj%^lK7>ZANq19>l+#7>((CLvu zIY}glCl9UJ)as$2BgMA%ra7}~B-!Kf+(a1*s?G8qGIgwVRw zmZ}5s8S_y*?ZMrPrY!2$ghE=YHnC_(`m}a!wS>nk@kM1w7mpg@@&NHe?;Xs{J{h!o131{OVo$Eh#Um^7yz{o3-kV}hbkgXK4mh>(9oSABZOS()^LnGos zS(7)FHmS%wNQ}oRT@?f~a9pLJ+_oYzi@spm7u8gq!Jx*Ju@SXM#Th4^;dU|*L(=uQ zCTTY>vsyo+1H8!XU zgJQLaTU{=Diq&8_gvHX?a?y?X+YN1)+qT}hVk$1Hk1eY5nVc3=y9g}y5kor>4-)sO3Ku|8&&21-K)h z4Ln-`!6PktzM|;C?xt@R{>by#<)??q8XITnO@9-3OT>#CP&p=<+xWdoQ}HA}I-wC$ zY%nYh{v}KRMVHwO_=g%0>c3uhspM$krp_<#cJJG2bnE zxOAx<4EjvYoV{5~>C(B9aoGfLWu3lgBpz33EX&bFha*Hq)g)eaLEg9dp@Z#) zxBpL*eSZXG5V>^h4%zp2k@J5u_u^c1_QSKM&M?!Tnm&0d54`^`o;W!E==ieYHko7e z9V7oef^GFopiph~$%((8n1NSd1`h6CyJgqND19kDGBPzes!-f8DUb~uYXZ-4R(|KEexKy!G- zTAQE6YmdyLVnKqNk;vlzWA9C%WJ#+!@v5x#)%FljWT&x3<(c_1BO|wFk&HcKkBluN znkLC5G9xo1Gcq$WaswK=smD&MEOslPY>wEVqu{uWI;hz2cLDzD%uzgl=>djO;BXwp zQD;>Ck;`kkBdg+7^%U@&)79rxzw_Sri+kh#?svZ>zVF`EA?U`(oVI3~j+3+BFS~M@ zz7H>(Gdo~<*RA(XX8KXBxV%oc$i^XV&uv?>l(Nu9xQC zxpkh;cL8?2QBmBvUBBF)>HF1#ll4umRLH&S*8BD!vg<Q%>KI4C(=f-(HA1?KL_;l*VpVl^JI)34AX1~9F3#rt|o; z&@^2uGT>G-)fpIOIe+_(Ese7dxVPv`h3xm0l(pKhq)Ot;TI zZ~psJXV&nR`*vnl{DE)u_nli?e4@(sl8)mrAeg4DL21mXuJ^Z4r>x)BrX-RHB=~MOr|%4 z2XY3>fKE+KW^%?9Z^o>$I(mre`3CuomE4}G_rciwcjC-S{@{IEGbJAp>Cf?Xc&X$G zk^Y8<>gN&Zr>ir4f99?;>wD{cn=^eM5$VtIb$O}p!y^3+^_)kf_dYPw^L@u>*7G^{ zZOrtXN2J9(WaB(vr`kp7BS<`3T7n*7Mh#Av4K5jqcrtPxP zzpp@_7*h)C!7=Dk#L8If*i#+hzEzBd2eJ+p?l+_yHf z;=>}+_IW-lK4is3XTRb0-@LQG?S(TX-v*ziq<_cZt{=Sjl`|zD(b>=O*=4EZ37!3h z`p!H18}Fa#d(TtPtnaP&-Z|6v5uN=UyVFa3AJ*A#sOP-1zy9`_o^N>ine}|my|0+* z`H0Sbj;-OPo{#A4H`H|A*n~w7fiu( z&aJ=&Q}DfW1Bb#b1QJc6nxKIfpeU3zt}VAci#W(xdP|dR=C*H)py>1%efUe z$MW*Vi{3l$f7ZDbxX`2Qci#WZxdP|dDYFF1BbI z=T_i?DR|0)g0tsxxY(Nwci#WxxdP|d@;J}8!yA8#yYv3TSs{$=B18*kjmZ#+5lpm)o^<2C#2`gdRc_~m;p z%j?4BryYI%=z~Z7qs-CT&I5;^*#6nWA3jtLpMLO#ou_&g1N#S=gSGvS@87d8@4R`t zyZ^N9&+UDF@BW8Bef@VA&tHGTbqS%w`ahqvm5|;vo*cTh*vM<;x>TzSo4_RDjGN({ zn-@CcQ3}WvG{)%@Ih$8Cz5z2{30EMI+`D@TY+2yI1nGErYG9UAkK5fwi|5X_Eyi0VJk_Vz)jRLtzIM>-B0ZDvRNUM(Onh zaMxY}yR69;mF7^(P(o#DBh=7LHgI{YY7=P4a|8!9oI26(v(8fDy*rYZLE-=MpX)rv z1$2U-hSH_O62`OWu$yYe!sBGEStaX$21WZ`wy-=g%`S|y{+1=sB-?c_eMl7^bw(vV zjo`9XL$qFT4E7o{R)?kBsMkm~4DXxI+yOSfyad*H057v?JnZ%yCC7KuvM!OhFtQt^ z7KB@_;SLkdL~>)f1#tWKmq5eI2St^XOsbSMuzY2l&T!2_QcD8^q+D`GwP;+6!3IA` zY75|@d>trDQ@hvI_!bR@A*CEwusAi;8$uWx)q6S;DDQVbWH zxRkXVEN<3JtsIqxx$D3^cL}UaQ(_*A8E6!cbJa=|)~8*rFzC3pYj+3H+_bEdDZq3T zuMGXHTU`oY2kHfwij&Q*kQ!&WvW-vCc*3h%-)nYaR(;YFv&Qv>{QH@U8?Q|7#W3^s1 zMpYV#zLlG5@#JtMP?KD;rddlKH#e5RlEc!&T%lo*ojl%bI^&X}YjQ&ws1ukli@GG0 zyj%xTKe|+P?W;?mTEay<)oto=6LM;@mt=DsqjQ-iS1UAINr2I(Za3V5GqLM2=dYJQ zrDT@9`$Fk5)iVLBZ;ZQm4z+EjPnuF`>QaO3q;3>@>Hu6=b>};;1I7GUZIkM#$W6yZ zO(&{js9b8pE+*lg)rN+m_UY<$TCf+uqw*3c!7)KcRWclo=1gsncIy?3jMt~243TNW z=z=*e8IO-eBE0}^eBX6oe5hzFQn6gLqiJp0AZiF$$i$Vnf(S7?B>~njRmirgOP4#m zE>Y~{)C@j?a|vBx3B09bm2_KeA`EV76bKr&qvoelv^l!&+VOM}X_25h1-8ZYG;X{wE@ORMg4YipqoJ}Olo13hMeZ>5*WH=3B)KgYll-q z15Vhz8kw*wnlSCK6SgEK;;PLK8U};QT*_UBob6keKs3q?`i9`x?EQzOCBC5U8APRVl#*Hqk+h-LFjI@Z2$9jKY9 zFh5N}O^)Y_w4NPPfDk2WLzD*6XaUK>TB}00F=+XY>rcE6^b(J%m1J#L0WmV3%~b{& zMGS)m*9O5>5+l5fp<@gmmD1P8)!`DDY*i~#0;-rmCTW9cs*stE=~|Yq2?{i13kEAV z3ZF4eW-0N~a(FByTdc;|8FS1}Car8G26!c}dDW`rb1-Nph$_NQ8VD%lVoT%fynQJV zoG{S|H!0?m!znRsjI9b?pTK1k93g#I!30PiOb?fohfpSs?@l>)fn5Jq_ zag4c{S+;Klr!~%~0w!JdvJCTsdUrWkt>2UKR!Hl&mH<_W!sSLamb0abI*=(Vl_~ItJ>t<{sXNkTwVU0$oBILdX|=_rYg zvniWjI!@>puLA|k93;I=X6__o$!xrA_6jk=VcXqIVHCq^DK8H#;52P)sp;PPmcT)a zAGOMrc%oue!318e404^)D4|Jcxziq@R+F(%6KA^Dmt1yR--!HlP*a6(h?V;iQ+XdCA6;mjZimsRS6=u_i-w3Y3a1m>x}#dOn;4s#7n2 ziIP$1kw=+i3 zJGK@#0kzU3ESPZVYzIX;ipttZZ|UM&FTD;#%Dh$Rqa%P72Gg7Yu?i~>S#Xpu2{uM{ z%Y9Vn;Yt-=&K%Z0Hv9h1ul?@Y^N`%m8ctG!>@d(ED_ck89UyYxGke)Q5yFJ0aJ z*WEwbedlg#_qN?jJAbzGGdsf_bmxiNpWpuQ_Fdcj_A|G>y7imhF2n6x&)xi|%};FJ zyII>zY;JD+$;Nv(dK)j=xE%V+(9eZl9-=}|S^w+xUtNFgy0ZSgYhUveJpCtf?Ma(= zTne3hHY1sQWcIdb^zc=YYu^0NR-r9UKAnBI+C+YSP(Fzo@h0s!C&(;i?m2(ZGm2iOP#tT62XLIG9+SD5wy>p_4O zrq?3Z)`9>lOot-JuM7hCnLhaY$nl*)06)_QUyK~TA_(AT`rt1k$1e{8_?bTVFOlPy z1pwlHrVlIW%IKVaHex?t8C2~9plHh0h;O8U9!ytg4 z>4Tq*9J@h)6{g=CIUWQ7R+xTAC-4wA4!oCoLx0al26P2{*81Xv;NuE?m8l=(+(^}-X5+q@T zX+3gW4wA6KG#@!G1xZ+8T8JFKG)Te<(@^BN7$jkZX(Dp`k^n$-g=su;d`A#qg=s8u z{NjLjh_0CR>5=2xgCwjl{rt$W9t2on`nJfi7NpV&4~LQCLXd(VfZ$EnJQTXs5 z4}b6QM-J~e{QiT#Kltdu4JGXLM-{qY* z_=U|^Y?7gug^17-)*oE|;QDwSUVq%$U#xv#w(Q@S|Mv0@Joa+O5K^doDVuM3hBKT(bt2=U9AtCj~gZ{J!!WdZFUaTc2O zqMNsGo)t0^(hYk3mFf2K^@X5!Spm)8g%5=IW7HhN1*+zcQcKm?AUVks&HS1mGy7xP zLd-D8%znu1Wd=d-va%<&e3#b-y~_${ZnJBFa{Ob*QqFOp96x9-=hcC7{B$nm+#M*# z51PxlD^SiUKJcnQIeySw&MO1u_!(Wk&z*sC{Ghp@s0e2V(IWZErL6N|3=Y`QmL6N`@nZ1f9 z|8EgrWYvZ#I6=N;(-2Mvy@{8%JGBdR%-{o zkDr>QoKBz|KWHw;3Y6pTHA^|IKskQUTuw7kj=$F|n- z=5lI*a{T>sDaQzu;|I;)PEe=zLuNt(`_9Vt^zvOQLGQ8xn!igq$YybW zBv{HR1E6{|~Pn zKH<`jZ#{SY!#C{$^D)Ji7I@9z-p=*J%;&hhb>ll}z67x;FR$iF31U;plM=+9;}X*> zVdr_S@yy$wBmd0FRSC|c;JFer-!Oa3On=AdRE@ru(Dd0S?<%Zr&CO61yEE{TzIw2i zJ^P`YC9{Wg-LloF%+rou&y)&!?Z0`MQo&d(acX6P5z8yn(o{;u6LgIo*5ZSTH0s8B zWvZDWDymqY>Y#U6izw$FeM0o|uuj{;I0+R}=`?-?)T&WkQ)(JpSISo_T(yj~ujW81 z+KG23e7T%P<9(zjD|JOdVYh;I2r}N9COEW~XaH^o?{(1j2=Ven&$I1zoJPIlRePA( z*(*I!oTu51B~sBS7zgIBzDUg6zMh!*9L{uc{Vs7*zdOf$yo>2a=k18+NX+~#>Pa_F z%-Vg~ zJotYaZf2wCd_;OEbNTJ(8~0Bbh8JlrXOZmTwSm4xfny=uF&o|47(5%0XNpuCPS+|; zW)&D`e_iU>oiGrMWxR+16l3X_H;RP4;eFP0kwLxa_ARev$F;24=yQDlpVb?=!2o`q zxx=GPiJnQuW;KmY?0MtHbee5lX?Ni(t#X$gBx(jUu}Vr)0f8)@=~+=0$;ay>qHI6v zo>wwz151EdQB}Ol7|^8Sf_8Z@DcBRCI*t#jj%WZ~qoYxI`)U#pqgm;ScfSyuQg+Yi zjT<$$=X&>g9itXcr`54i=&^kmI#bV!&tBc@dDj~Ewq`x=^~Lq`Oj9pzppUQiyc7D; z3B6;H&+yySJLVm7=+!)E*t2y1&z^hyTqFB@6W@n-idiv`8+3}<>pog%PbU+nI(x1O z;MG(tF+rlabgeuZHYvWXDWrpBoRTYXP(Q`O6(m8V9=)?qK)1%F65tg#+2^VZ)xXlj znZXcKljB&7EaPgqMUxqA*aNN>@~tsYk)Z}jW7a?)@Me^wqE) z!&eu|(EjG$$M%l*kiE@IAM?KWBbPRJKel_ki|uajJm7up z$9A^2AJ~4)HnzRJ^}yEawur4=@5KM>Hi-}!+FSqq^*5}O>j!Ipxb~)9>H6Y+{kN|F z>9PO+TeH9qZQXIpso(Px{#`qgp;=Pa#a!OYSNZuT{aUE_g00(cIrXEve}kD6bxO%% zoQ`|&kG%Mvc%bx^qj3a5@_JT9v#1CE><4ZM-2m_3tIdlnEYrG{muL7_y*&g3%lE%M zjLEW$sU(8T@K`q#4aWNyB%GpZ5GShoT>sj8La|`He~*#kV3>xnoUYE~f5$6Bkzl-k z?~p=R306gzBWL&@`r**kKzzclKZ?r?qY8Q<>)}85^>>9{5{&ojPg6*aka+JgGyIoo zp*w={e*Iwu%c&Zs5#kL0=^qKbI2iB$jzZ&_C?bT4%-;WV4}3CodobSbzJ#W-w8FA7 zH^cw-i$i*#_xsJq@;XN3NlwUl@Q=LnAJT!+SH5jZS)I%>SwWp!|JT0|(t`1R@5f|D z@w|t_XPxhJznl*hg7N+@5F{zd5ahl5-1;A8LuxSI|7C%|n4FyVN^Z^d|MB}nN-*Bv z`y?R`i5S8wbN#K)hvZ3_mVkXMj1sD*ctv$|9glJ z^j`n#E3~8_IFhAj&i|3uQ^`Q-E8pXk9Kvf_j?2&3`>((9iyOBeNrBUC60qR$&>%!|NeL=7mW9NuaYak*+Q07 zXJglcU;Rjk2*&%pml9w?$jeYcpW(m4gz#Xz-+L8|6ACz+=jZaj^y?uk(0l#r^Q6S7 zl*DQUPy9!muO$MdubjyvDOQCAoRMbm2mjz}Av758_r3z8DxxS#{A}cY@c(&h2nojf zy$@$Fx{#A0b=LVF{12ZB!NGXH_t6E46;TdBXR+hKcfU6T1>^nRm(5Cq%)oMP=DiQT z>CZ#iV7%Y^6dLDw8kXhR%;~}L6GJZw#{22ZWqB|03Hi67Sd*Ta3l2mV7%Y^5M9Wj2q~lUzW>}`4m~dz@Ap29!ElT$(DQlMgHL>4 z=(b?|iuWOypo%h#GBf?Z<7uHxFy8Nd6h(T$mlc^=?D*`L@K7ok@Ap0#$*VYtDa73S z*FPN!o_WRm-lyR*k!J-g&(GvP@P$xt^oaT0M@I8GZ}tT-Gxz!IUw$SOJa)$X-m4c1 zXr2(|{A}#`><8`-fwS-bdu!>nBkSPL_K{1!u=}*_*KK}n28D&#NDI{G8o8 zjw`o36WuyxTQcKkk3qcb3&41&0a&!8X51V;|FcuVGt(+!gB+3_!OiT5k=U+|D{d9H z+9g$)S4(mBh|<3G&`atIl)R@rwp)W88?TOxRB(K`iRNSWdL&-L_Sf$Ce~OFBw_ScJH0z0R^Dr94Y2!HaUL)nt-N2g=$6&tozPF`^IyQjw_lUyL{U*0ZiW zi!}zf-IjH{^7OaeMo1EWH5vgykIldQ!$0#l`Q96G;`ivAi4$geL7ZSpEH{OEEK*eq z?L?>2vdlcN;Lpbi(aooIqB-!cStXGvr1MdxBLg|!jrUR{+?F~Ca#|ldK({@z8G`l} zEfhycXXfM+Z}T|0^+ufdd-+Yp$wl=OL{Yt>$uwytbSi2ZWk7d1t93#@$#N-1oF;6% z*ucGnnO3xwXV# zae`b>KOrzu09r{eC1R`!R2&^1NLtH2iIdDoleyT?i%9l}0&`te!4p`^V1d$rMT{_0 zfimr6xYw_#f~`_Y4$m=rmFjkM}!s!uYFDKn_ss>m=5OAz_9 zk0-xb@i@8VMx6L(m^T$C7u8P?vnS%!STzbcB?Ke5IAoOM(MkOz}^8lahcD`XYMDz_TN2Do^>Nm{Im9(i4*ps z#&}q7m9a!ZZRuF89RsTqwpIeP6F7lrjflf!REi04D32R-H;E+Kev~gFs*x4jW=53Q zbg?$Hy)jM1eF5Xq=uBb)@WI>~78nK}6n zzwB}Hj2m%s6R%Ie3tOKM3M_hlFCb*kkYi9D&DgvopU_WWKoO@-wW*F_?}N8IVP#0~ zrdwhcstZyR)*6aHJ3vtfrnq1iXj&(6Ek~SvjQ{YTdYnA{Mx6Lp>^D_Exv233)p@do zf%%R+&=f4pAedB+oixTHoYOa|12;j8$FzZ~R+meHY%PJbMjTNeq#V3_gWvh11M=XzH-2gN})T)Cl*<;1J6%Pws7)WD~ z)Z)DThZrOrj1~wwCbn5IT5<8F0V#?rFsTe#(?~X%jswL)%hl*ubpW&nt?l1hAP7e! zV$=MnNN4I5BHIKTpgtLBiE1hxR#~y{P7`^xLa4pg&aW=sW02@-Wu6%nsaTu=GF`q2 zqg9zR6l=s|Jb|WVJXz+tNuj>IM2LBr=Mr|ISZo%0d5F^lpj#?+6WwYpmk;OdL54wi zvDD61j$#X~!jM-XVcI7uCL@k}|>W-FyO3ESO-s^WILj87*@AH)k<%?|6RZWUwQ z+%~h&$~8qdB~}q)oCZ6sNo*{St2&Qn2y*DHI>(8AVmQXzNsAYbesqCQMv%?Xs2CNC z9l9CKLL8u}I^RzL5L1V<^&DG(MmAaGb4RaOAk1uzlMrv)Dh)EOm@brCxq7_a;F9?f zIciN%3aHau$t{%XJHy3$BzskRJQcfxnyWVk^?GF(8%Bw8RfNJlL6>O?ZVIRto4DG} z+ZQt0)2UEKOvMtl5|V6#bSsRI*-5Wi7^8++$+et1R{{Hm1nzxsA%kRNWeo%o@2oo$ zuUE$!9If_~K*dflV7x!<5b_i!C$UtbwfEwM39j~X7sRkjZ`zRns&D`$qE)W2&WE9zKLv)ge zQMywo=2C)4S*kamwsI{4L@1J|wZlSEZ6QVfSCSrQx$uvV0Iz3lfH3-8bbZBo4++}eHP9O0d890~>9iD$-&kr-P2 za4KvWgFcllab~TH^vOXL~T-IV?1k=iZnKc;$6_p47y`75o@Peucn0!7DhY&v_PVhF~iAb|H-*-lFfGtJaOushh#U~uok0H@%so$2uK(RYDgz#A2_}T)2=VM?R z%1IdzjC=b=uz+-#mK7g%n0$gOHJu@yjeFHC!mP7JUAnUN_8X5f-{KaF*RUH(N90?Q zQRGuiZ6JBu-LX2ATr(a@(nizWzj4Caq!6nBIa!zInbzs zR@+0dVX0iI!FylwGfFkr#BHXQqO^uvF^iSf!G{-GKACRIn~; zfFC`!~`Xy?By(nuoRPumBflgr7&;!_tu560?f4bKJC%}W8Y#IID@!_ zbt$u#L}Te*QOkf)JvqSSf?eL7lWFOmsGUV7^2=y29<41 zyze6YOgPH(a?3y=7vh_SK7sgLyINtR4h$?VwbCR5G2YZjb%6)`Q> zVr6<*bjd_58+z9Qff5Kf<$G$HW?NdU78P{29d(MR(euh888$8rt!T&O8@+nyv(XJ> z{Z>0GdHad6Xf3WQcG8=-4G?b!Y#Qd0QN}9N+CaM9%#%}*XSAgwW)wbE?8WeePP-fm z_Y(cO2o0*~UMyP>C#~*aWM&J+LbMUxc+NtpR8wxTO1XyF5SuEv0$eMmdSS1o&X6FH ziA%Zd1^|IjjF;E%SRlf#K4J7hMGco_Zd&j5`^-om4?6$@PX@fzt-u64;i8pkZu71= zqB9*pnQVMyRnpn5#Wwcnj zmS;r*8>QMRuBPb85Y!b5tX5z?K{%Vgu|RYOw2Kv@`4p{HD9cNVA1+!k%!{6$xiG@) zjza9*R*%!qU7J_5E|;oN{Pj2cLaYPC6B&*Ta{2evmqy^zrw z_w1oP2Dqkus09TNUWvYO9x?HsNtjj8&PK)58!&s|~tUZQ?e2AU7DTFK4^86qqS? zsA4ILPE*51HadytJ9q*Za#iF_rrd=CTW#<-PE)uQbz&~?LQRZF!#$m?HTXyv6{&v%4^0;C+UtdLDm!x~-*9sq5Q*ea4O#7cu6 z1N1UfG^8&OYLD$vwSECnF|gV$(ybI>*Ye>>7N|yO>s7h!Bi^79Wn;l7x^-a@?fz-ME9Z z(>9nQ^H#&jR}CkJz=I6BwwR7}c+whZ@uDsim9)3^?27}g%Er5DxK_8?R9EU|%VlV) zdY@%NcU@ixapOZSO{T$ctxT2cbRQ@U(`}RyPzjXELvG9^svL$#3sG%J(&+YT$P}rDEqYg{heY?_d%2N&# z`bsrh8mFUZyXiFWaV0gO(EY`%#LlT+v4j+acKB8t?^gjkS=AH50AQ1y*f5u{hYZC# zfJ>Xg{ymHL7y%80AdwwoA)3r+rqew6f9tS1%BGd{{;M z0Npjq0;o>BiU3r$9qSq_5r%sPphdIQ;uw(dLjAC^c#o*0XoCdPBV4XGpnEta^KG4L zOmsA?igBu3kjtTRR0_BD#`F9Cp*z_WG?YK2c?RUY$yZ0l_qF4ZI>Rs_XrUDv2`X4oaz>TNvf`dPd`NQmN%dP^NYzj@6_@qtej(6Vq;FD*UKcuA1$UYc-?Zni48?ny4Vw zx$K#4_s(B^(F?l|^}=&_(*JQUOw71GX%I@K|4sHnaHIgqj*At$ov|#rRbeoo02Lm@ z+aALrZS86foItK#aiDY+AGWNk zeT0ltovMNxIj*bZ&0bVfAAK?%7V=hzgoUQAihQ3b)Rdmt6dbK#)m)%m_P$c2MS~nq zl{Cd%?biwJ3NW(Ls*`V$ZCe#ew0AU` zV@jKi2_dWcL@!i(^%k9G&~|=yvcqh&iF)fwE$%4Y-pC*)XG}9Qd-di0|HHM$+CF$t z-+9N;A07V0`a5?&8T!;(V=J-gUDMw-FK97%cWm>x61sVgb7;|vJF+<#ax$6`Q4(iV zC(fXx$j0VZ6DE&5Zk{FL6j1QK^6J%Mg6?R8E{?Pt7?5ir(NvCy>0vt?V4ayp_DyKAi}Et>f|*cA z^qG#`q!kNO)R99?PCCK~>BCZuj#ASSJt2A$!viKboK#W}keVpuw4&?rzA=iI$&$Bk z60&YWlU>wFWZa&zu{^=4Y__oEJG%3tzD~?ajXpHQaQy`LDYgPhFUJ- zBA3kM4Lt9is2Znu(TipFO=q&`g(yvj7bmpmWyZ~CvI{%!j_A1Vof8#swkbu)w$@~N z9C6b9DP)+cN{w_X6>DZXog}Vxir&dU2yICiHyNgj1g55jUTt<+kvx+%ZaS02n2VZZ zpxri8&}~(!gL#Qehy^8AiFs>U?;7VL=Vfra=agFxg%Q&t;$>CIYnhseMswM*lN(lx zAP_EO2PMgoI52C6Iyarkq8B$}DzXGrV_IuWdv($qW$zog)Hw0xB#I7^mToJ9Leq`n z31^CUJA<^N$gPCc@ILk{v3AN#@ZO-(X7(P4OG>gQZd={!hiMVGuegRNx)5>g<}J) z;C)<=RV_}jO1yf~h?y0UW`iuVu~ElLO)QC?!ZlPEhKQhN-P9;tndDkl*_6O&!}GHG zO=z+Uo0oMb+B7DrO}4KqCQGZ{dFAFwCWFg5SszY1Q)eU+GECKsGT9QM^nmLaZSTks z=pAB;w?*$P73IaU+ARMc6?)6s#J5#O0?PedXx4j(+5*e3U%eJ^ak!hYs&N zH@U?>n4(>a!4qkZhxc$$0XZ&BYU)X={-aqesa_`-HgFS5TyDxp|(r;Y4=h90r z0bU-!|FZjm-IwpOyU*JB+ntZ?{Me4UbKB0r_Fr!Q!uIjDvi<#A|7+_Hw|;V~y9I4M zar29tzqUg7e`f?BU| z`SzgJD_p)UsPzh$Zw+d_!sT0nTCZ^Vqd~1#xcrfTKI}J%9Ps3o!i=FG4r;x^`#nLe zS9pJOQ0o=m-xSo^|B|6M2DM({{S85_S9pJYQ0o=mUl-JRh483ycNu23?y5{VvL|39|= zU#UOkUB2*LLwmwOJhuLiudEs$TmSpL z zsauX?o~h9nm8$JV$JZrshGcS79_Em5l3gbpeq#BCR#~-Nu6gS==bSVN*F8^t-b0>>ob0Jy zWdWZ>C7jKX5J_|SZ<0kHRY8mkyQhF=qqTM=u0@5hDp%Xo)GMw5oOm7%54(1`j%C}u z3`>)RAzBr?b*$cIW1=&nht-i=t`SqJ&zA+;Gxco`nHoOL)c!qR3FS$aqESk`nM{3Q zXMzb+5+uy4Qh}yuc>oE71xx5jr!^rjQFg!4ldA=|qyke{XyHV|q4TgHj>N{OopZcW zlvGw~^wI3h)R~7&4V`9c|Ax7jn3RycqMU?!Q|QzU6*0t5 z^6IQ4<;yIWs+-j?L*~sg$;rqxZ{)Lp!FJ2GQp0N{G*wOv?E1{q>4!|6J*%k;v?^)1 zu88>mm8OQ-ej%3dZpu~0R1D%|fE2~}iKaG^WVwY~Im}i@J*qBuMZwI8nV1UIJNbB5 zVmeT_9Uo*mjyKfRk5dns`bDRi+P|IpjXFFxm8mamR`E?KUgViN`H-o9=rmLN=ege~ zuX8h*`ogZfd~<3Nd8ST0Wa<~5W@`V2%{Qv%+*GE%D6jtJlp69(4L)S*7o28l|E|Y3 zis9T;roO0S>EE0RLY}GP51IP;rW19AT^_A7f|-g7Sf?@O<~w7>h--KXxnZ|BPPN4B50^}Act=AUfdzVZ2u zR_O0SFAHs~zj6Hu-Xkvh$35;I-f`E~=I+|=?9sa?`#}=$Px@~K%C|K zjsG$-hw>RmpfEe1+p?a3%@xExWRKv1(&mj{h0Q7ES?QzEX$%Ojyllx`& zxP2zM{$nglxdQ>6;uAJ~wZmOCDpxtQQ*U#T#<13{$$hiNFq5nkod9W&M5uVLFyiwf zQS#(|(LJ`#B-cMtSjts7l+RBh%CuI`l(eBR!Ax|>CAuZNQLUuAN!T^WB;+MLsir&Q zgciZT>9Fm|eb7B_ok_0$W7txzkZlebSE-i-1!xwqc4pEWXE;THqSH=^t^lK08>qIu zvI5>cLNQ|)y=aV?$^8rWxOpbI{?U3Vm#;fS_Nq?OI@;lYK1)v`-GmXr@=;5(9EiVK zsiZS>v&0Wvjk5*KDEDC1ll!0DcXW|w;>wo23WAQ+538O&9lf|IRv(po2oV`a(K1U(K?Wr_AcjVpbFr2 zG^NmpsRL4tjv}(usg>EDq+NBZm66dNiJsg~xySW0$@TBcEajF9of_P(S1~AxO}lMH zuqBY;fcThbsLrI6Dp*}PkwILhJThXDbnxM z`pt@JvFO!mIii%H@>IZwk?GY&ig22SWkyGRuMlRr!BY5)-;cY;)icRGC0>;)k(4g8 zc9hS0dBz*l!lX&0T_D5ZWe1SkO(9xkOEpucI5az;t|p>c5R7YuJC%IGV15Ox)b^O)t;Hc)kwWZ;Tp)Lk~2I1hI?E-liX9{ zm4gVJ4WncvMPhVv9IIJ&Hx?-j2fYZw8BHxEwOsKkK8^C_VaDlCMscMz8;|~@dt5q` z+*9IJKPiH69lBa3T2?YTZsmZS!5c|g=;bqbtSdD2bU)v#a-+`lYP*Nk)t2Jop4^YP z$1gpT+*9Hed-ba1rGQ3(bR36y-1Q3TWTpm0QDe3V@vT}RA=L(jZZ|CD?DlC#K-`mYx)<3<#>3LJy0t*aHN*&U@Va*EcBSdwDKUeOzD;AOx^J7v)@ z`4TBsm6k*3$(cXD%RRp1Oma_&SF9!sds5k}%u%C;SF_ECSRSVhHl0z;Nw4fx`?BOl zTB+bUftM2_Dk3KxsIidy;wBO1#qOxE+Z`>j_%UikX~4QB23=GC(I@ z6_81Sow5tt!g>hX? zTBCf`%@G3GW)W{3s@kH~)MAv|8PWM%BkIznG`Enep6Pv0>A(FE85s^~0Lds~wHJ|H zyg>E1tC|xxj0V9p+N9RBAe$M0S5s7`ASAosWXA8vJyy>2zNf@1FQX!qa;hE49JHv2 zW=zFgKkjPm2y#5dqb-BP1a4?u zo3qD`(kS(6mZ_E#t}$uhAR-X1)+iJvWh&Z@D-1b_r=d=iWw2Ri9lFQT8D4JnW7ATu zu4bnEv-3`riomnrn4XL0p#-aeH6sFam8*Q3%6cVBCevp?}c{Oj(*}0-2ddBc`3B>&h6ypr#IT(U%uy}m-LV7dF!=D1$zv14h!5}-M-_v z^2}v2g_VoJUeXKgt@5AOr$zgT2)*@t30RXKnAO}$=o; zdIoE$Lp8^cBt5h&O7VJGT5jpGn8tf@)_$mHZRmw}m$q*|E6>O`^~3`g zZW#avOfx&?B-A@ZLDm?lKKANDjc7?5U(`X2N(>qorevWMB^_NH)IqUjYAvW9$?uDU}5u0LdB@CJ^&vf1!WvvK=T*!aRe zfCy8zW%nu_3@uM|hgC6MAWm{*80sK+W5{+g6VS^=G|1$d^bo}j264blKFSZLQrF6x zD(Q}ehdJ^KZe(TuQst(y@`Z(qnCKOP0vLeJD+m^*+JkmkIjM>-3>Ptw8VzW;OnOBh zdNoKN4fK}ImkRkJAYzhZv;YGFWi&r}n3bP@BP;voTsM=IFYJtqS8PjAt46+%K-nhi z?U?JLrks@bE}Nd_y7_6oO{7-N?%R z8Rku8<%>Gr0uR*6)Ks*B)}w&dSV!%IBA;}UC_5Q5op5+sXf#J$ae{Q&ETwnjtUap6 zj4=R{gN%*Eo1JXddU(XR?M7Di&!=xHD_>Mx#>WEJGR<-)*JJS*q<1X692HKOV8GQB zCdq|ZCk)nrHq*3PT2plkNu=DhryNw@o2TC=Xmna|)Qt^|l4E2-JsOTbI@x+o>b%9_8u^Kw2(EF9G0;wB=FjWpw?#*#IP_cd$WC_AwtDVko;X-@_f+j4oUQ-9bTBeA#jsw&E|AAbGtfx|Z+Du+)$_`<;l*MD>Uc%52*{AOb_y73QN zkK6dIjcXh9#uK*iwLcGiCiI?=x%TeOKM!49|DOl_gUrF&{>S(4*_Zd9w)gqH`}cZ# z=}S+&B<+4~_h&DC{n7)M-n{wV-PF#%?)=`)n|8$A?&XhPzUQ)h`DsW0?dU^C&e3g` zBD-JT{gd4f?%p+9zos|uxaG7}=}N*UiWWJF(0L9EeJ~K{A8km6X1yxsVlE&0nIItO zp9~NflaupOmdu8JG7#vOC<%E;#1LM=L+=U%`a^b(#c&aWSV|7PGZ5&HLM%!%Fr>1I z5PC-tFzH{0$~i<4^9Wo}LO&M>T-hX0F^M3wd;tx8CC|f_J1h^npO2->Ni)0mbq=_snJRg=M+nE$`v` zx%ntaQhM-)%klBoNOe1mSET@j|(Z)d-f_g*+}A4m=)+yG~!X6x3{`5scFQ= zg%f^gqFxa9_C4ohp0bHVEJ@-4H=(*?uY!|#nt*4`Jm+B}QPqyU3QlSoWdx7cO@x!Q z>g#(IoXk^#Bto&EoAVH=+xIFsnWvsO#}ITh1v}T<_9{48Q$4XbPtsAJH=w$6Z-S1= zw&5q@LYyJ_XjpaYUIiz=7!7;qWSDWggQ{EhDmYmj@kk&LN{}SsQQf>(!O02$80>dSi-oV<1r3#Ae?8+H3B)z|ha_^e);@Y8I-NAaBMt9uok>|J94JWfP~I2~7g zWv_yhRW6ibVu3^mHY?Tjdlj6la(Hn6MUAf z7@o(Y-U#%25>|bFuY!}gN)imTbx$VfgzCDz3Qp!K$%OnKmP{}q)wO#SoUG&liV-N- zna6O|HG37DtmI@Q5f(zQzhYHa?^STJuKJTiob|h*j~Laqy$VjY4lj$-0xR%dM)kS9 z3QpD>JVHj?aLN+oR9Edy&^jqdvC*gp_p%;Vb?IIOC#zh9qG=))i^jdG5A0QNvda0q zDc%!|C)tqdlD&-_lX(*laB-3*-GOj^Z{K^Cwad7dj3krZaFkYEyjQ`=n(7XE;f0Mb z9;a0A->cwcO^t;KZz@8230ig0%7kV1_SJdIzu{jt2UaGZx*2h1sRnjoq;{KzS-fsx z!`wNuPr_gBqkpHZd#6zYkCzXAooC@WF7X>D`>2=DZq8ue`MvNNp${@5OQhJi5QW`E zvKNIXLolrq4r$y`LsV$gQw7mgDKuh@Ox$a4MmwVxUh@z5I&HI?#(dd8dAnw|K~Ovo z4@cn(Uga1xl5f{GY~B#ccaJ*Cz2a&5sG}H;i)=9-X|>MQ&wYq$!ZO+Nd}T7(r@oZR zWS=<+qS<=3+!^p(EiBfJt}J{@9X0OB;hDQv80E?& z=%huAh3Y|iPmY`1b}3lSntf%DTPpMlZlV}Vm3#0Kd8g~H+g(yk$d^r7($8iYSSDeD zfV&aiRAS9#vfs(DObpLgCX+LPmsFYTt5X%KRLYIL))3R|hb!KZzuGjUnB1QFF4E=8 z`@9hg*K4+IsZ zWimOfdr6haK08l*QH;VoV@YSNW%A&I_n#`hbF;?rPLn*w@8JUMi&Oyr!q#J+v8=P^ ze7-*i>;4gYulvuw1NBa* zdcHW9J78ARN>)dnE2}5x$MEi{7arH*LH;F`)%$9u%;4~3&g+18bd$ky!<3_J1KzyH zySny5CK|96p>4icyKC{qSt0KBRjnr6Rx4QRiBL382~n3REQ!cc8l_})__?xra(S2% z1W%NS@zKyrDy#R^LXm{;xXLzYlOS%+n7akRB=RX~Pgj-*$rbWPv(0R%JF2(1JlBhv z{Z+=1b%dlwAUtZNq@p_}B~w)aSxWOLSsi+gtbW$hT!!&QQtpHYo*_gSsB z`J=|BKO@wHIOj`om1ZFy-&0nTJ+m#v@}V~LH*Xp^NrNL(ZIlFf??J>p#cb4A?55c! z>#-K>s8*9oRtKLet0zwtqOlOc2U5xCOQ{1H>@%yDL^mH}QuTzCvt}(g0lhtwVH#C| z$*o(T;4x;~X0zEAN_a;GDlup!O!-XGQ{hIPh>0BJN@Ahq5$P;4N=GPJ9eA#+o;-#N z#}i3B9808LQnfm?&pHs%oU#P1ab9eusHzLP5O3}ImNaQ`wNnFYzTysb@@1=iWMevm zvBN3m^Zg9Gy*tDkSz|0zNLw4IR^v)m`=2YTCzk|KoFjq}BE`jDN*zdOUo8&kft?@O zhJjea1{Z9wzx=W*l$vtrN?kZOHM2lEv|A4AH&^O zWUJM!WVP?PvU>8w=b2-SmsD2otG=3kQOSxXpUQ$`TuZG1J=KyA9 z{Xaju{|q=?bHMzE;Sc}*2Xo+zg^T;@JexV^fZ2^fshcb$O=K`E1`0we#%7yM#%T`p zf_1(lkW_5c&-IPaSV!J{un>h;rSpvxUZV4i-(Shl6Sm(^Jfz#G=B0F5JmjbiHmCWG z8xHAo+xbjA-Dr#Xc3CRI-&>4M>q#dblIe9z>3lygbvy8v#~rdUSIt-^i8-a*Au!^N!*aFg3-knIolY@0=XaWjmLp*;Bwbv& z%82U3i2-LAi$gwP^^3uFvhMSUfpWPLiPilHu3c*d5>Eb* zx4{p<7eM|0qF^9YiU_%CzSL(2OfqbDb_YzPME!&{|)6l1AD0bo@dr$<%)+uG$dGU~wU|=!d?3Q3q8~hpNLZ_SQt|aBSm3%=YUCobH zbXbYI(JmLu(h93Ja+zAG(di!LbQ+D%{4L$dxBK~a8Wpw4<}ezq7PHOeu&>mRQl?YN zuhfAPS9CDj>?<*O0&UtoswR_LilWr$H*KSMEK!cs*dn}iAhpKvLeW?0Q`OA`i&XBE zi&CcBYp+H$p~y;B{F7#m)W1fNb;lxbJOLmu!gB^b{*KcA_Cx z8f7Wn?$uyjfq(p8Xn=|WE48rM%ht-d|I+TPP^}lS-8o$j7yQB=+g+Y_SUGkxe7_ar z$CirQ$hnFoqUsUs-L5g#?x&Kz4xVZR8OHm9+g-jzCOiI8wN|cFdlGJB8nq<5#T4ic z`&QPU6u2U}VnvQ>8Jk;Lv6iPEZP~5fu&r#a__DDQ*>+0-k_-e#jizf%nmQr#<}Ggb zn%dn;(qX$hvwCrNrQN+JH#^O+v+vrSc~9-`e|$ijc0`uqY<_VWj=D^)1$VOsjrNDww1rG4&?tO-6*1I|&cN@}C)@MRVc>sP!aU`G3tc<|u z$7(rOefnoOq-Sy~sY8D$OZ_q$$Wi1^5AznN+>6=W7+rSwg*|pRe(x|t^c?keHdyzh zN`2_5$lJ|iH)nfvhPBih`3&U@$NSLX(+eNw^L4Jh#p80P;Ow$E^y8L3zC|K^1GghZ z1=_tj9>-(66;H-e=B!%`Nilzmr1M@Qo%FkF-6ZXxM_W2wdYCWbg={rdvl$(Zm08Cc zhIuDU`pPihzqo^j`O)mgDcan=>uyZ{-R@>(6n)lwc$)rd55sUhU(dpcER6YoVF;E# z-YJ!vux%W&G9a%gB9=SNT4uD`<_}qEsn2#cY&5L|l^bfh*RH_`q+YGIQXQUdQ7hBh z=V?)E?yq1}FLZ+KusEK(<%un0uG6XrHryx#LeW;cE3}%>4gdP%ZfZ+5KJs;QKt{7j6-18Zz~D@lhfZ^i!KJ#!zOS^n|zJQtE%Wqjea`|=3>y~Gi9$)&&(!EQ!Eq!6>KcSz1vzEq7)up#C1(%Lq z+Oo7^X}=|v{`dNy>c0!E27D3v5cq)p-TD*t(7wAqq(4S))E}&WnSKuZ0sIVn4}2Y5 z4?YPl1!se|>prc!On0vC9lD0@1Rblxbr#)5-79sA+9$LRYwy$Esl7@28SRI(=V?#Y zHnkaTR7+^B+C#PbYt@=3HNVvSK=TdFmo#tCd{*-xn)hh7YFe7C`pcRFG#d4v)xT2T zuf9tY(~ugQ`YQE@)#s~EQMc7OHK+Ed?drqS>($!Dzbrno_`}6-F5bNOxy8#D-@AC~ zVrNlYba>iH|?FPMMld~d!mpO|OnUGqoGA2bi<{xI{Ur}AF`k3k>)#<8%s-#M) ze5yC8-l%$wYH9W#vybX-etKhP_S&a^`ZxYdrqE+&KowLK`W69HKt-X>ouCZL3bikS5-2J3 zh_8SmC_Y240XdNSXPO1sXXw=+12PJI0}D<7Cn)q_4M>BuLSMVmN{?6QK~3;B@HU06 zKMlMUyj7vEoCC*!;}oh{4^kkd(D}PS5+t9d5B=j;KmdY5pL#h+fP_Myq(B_R75c|x z0S|bEK7JhF0H@I3Z38h7Q|Kf6gD8k9^e5W@3s{Bj-U=chqR=0H5_Upig?|4&5CS2E z-uq?{1VM%FBtQTJ6nfX=zz_Tiz4PP12Yd?s`X_-Gcon++r+@*BLT^q28qf;8=|VsO zN}*r46L^3}p;zAxNI*VAzX=FHDD<-*0XV=F`pH{?8@Ltv2@LkJ$13y+7dQqSqtFlc zz|r7ng5R*dRaw<3s9H!93 zEHD6rLb)1vBY2}i+0(&huvwwOQ@|162!;C20*8ac6-r0JCa_7N+FyVJzyS))y$l$E zQK9M2f-PW+LXYFXk>E&$ZoC4#2E0b0uj9b0!K)Se>h0hlaF9X|yc@gX|r_dj709v3`=tFM;8lX|= zRd)e3P%HG)Ca?$=75Z@zEPw@tUU55^2lER3@S$K1%qjG;?LY-o3jM$gm<6*6z3^x- z17;L@_NTCaVE<6)S;t|2$NsL+)BV`ru)itvv?s8?Vt-ZW)?Z>zVNWS^XvhA7{Y9a@ zld(Tze^zLFj6I1xsnEtxu_v%66k59w`xEvjg=P+b6P7V_LxEgS?p2lQH6Tz*zd64Db%wI`z`ich2r1AeuMo+p~w6R`!)9K zr|ItBeipnRy#E>c8E_G}=o$KHa3Q!*p+7ktTmUXm=n6#CVZ;e7XOh2HWYcsF>rLcg>d zoCVHO=nXD#COA`}Uoe6*z!?ht+!^3>aJoW2b1irmc$Y%2d<47`yi=hcKL(rzPE+Vd z-Udzurz-RVKLn?MQxy9CtHD;VRiPIi15O4fEA;#u!8^b^6nd@=oCHo%=;;T7w}ZDU z^wf`m6TyiJ-Kqg&FjnZ>?*Jn(dYV4y=wgpxk0{joAK0(3Un$hI75gRjONAc!IqYHV zVTEqK6Z-}B3xyv1D4bFMT%oVyv7cc-Q|PNNz_LUjegJy_d*EsMz*AN1hu9Al`lk;=_iFbm z^s%$C`>^{I`r8w+UDz&#K5{c$A$(t<59hG&Vc%2e&q~;LvF|GM#~5}mcCSKz^bza_ z*bfx?kQ2KHyGNn-{SEsL_8o{K)I~01$XW)8hheE&n9%xMN z>k7T;I_!4rc7@)s6T1z&O`%`>8V3I>^z*;PzKVTSq1VQ-uV7zM=(ZEETd`Xedey<$ zc5J&sKSN=+V7Dmr${%AlV>c`G6JN&Qe}#VR9PCTjmlXORr(!o@H!1Xk@5XM#ZdB+c zX1LtDL80$|19m-jy+SXz5&I(cMTMUCF!lxP3kp5wQ`qOR&nxuZ=VI4k*D3VOJa#R1 ztwK+KA9f9PjY8jfC3ZD-_0x3sZ@&!J;FrSn|Li4m$IdLBup}%+mYAhumz+z+rA=^6 zzkX@ml4fZ}|D^s={log*`d#{+`W^c1`s?-E;0ph8{U!SI^=In0>WBKKzN9}vFX$tB zMt`i{3D^6Z^c(c+_3QK+{S0^#JPIBLyTLB76Ek9)unpLHY#pY-W^_;L9@Rap+YQ&Z zJ9RsB+jZCLw&||aU9P)CcfRgS-B#UD*VL8Z&VZnc=osCxI;YO4+oao|Td!NE)97Zj zPih~9`vkkSyRvrTiQ=5oy?n)5Yh!u^Dyrl~1uPS6ON zh=$P|t8r?KnoXJwn)RA>8jWT~{iOO)^~37j>Rsxc>K*Fs>g(0p)K{u6S6>2m8_ra3 zRS(robxD1KT2M#SjQUu$Q*BglQg48J59`z#^~~avi;pfoytsRD*W%8_9gEu+uV37@ zcqQD4xMcDC#WNSTE)Ex)i>1XA7KO#gBC~kxqI1y*_a!zgu3ucYs9Bs@cyi&_xv*nl`@;2bm*UEW%NH(LIDg^Hg{=$2h2}zO;e-WYA+o?M91Hg>j0>9_TnR1*m%u^qOgIn>;WDoT*GU3gJ1}rQ?Syl^ zO<)6@d98zUq!~DGcog=;yJ2Uw6SmmxuoiE_uEZ|KF2T;n&cwE2L#&CFuoEx=?q@LA zv6yrD$>m3vA70+QylZ*q@{Z;0%hxY&TfTDn^5skBch2vCJ1E!BZ=1hz{_^=t=Fgu$ zbAIdmaK5>GKHQDkx;$KNE|->1SQeHe%gpkz%g$xv@}}ht%j@CZlxBHm>B;%h{0Z~I zd<5>h96RrvH_mUG-!Q*^e%-ugerE2;xku+7p4&aQYw6LYhnIFQ?ONKov}0-e()CN* z;J(a`x$Se;&uyE#a_;iEOXkj>J9FvErOV+S*ZE6l&TX9=&Nb&sb0;iqT^cSm;l4{` zsWivH2DAE4J*QG(hwH5fH|Z@1-=H@kJXCK)xIzC$gs;#No&L25m-VkfxTJqILcRVVgh2l)gqZ$7gtPi(gfsf371aGh zuSfWIJwW(3J%;eFdL6>2^jd^})@u+xp;sgPlYSB5AN30e|Dc~o__&J8cnIMR@F2pkg9i}a4t|L69Pq(aycFTt-~$NX4K6`=7PuJUnc)2h&j1%8 zd>6QI6)!;ePVhd2r-An(JQbXe@D%VKgj>OR2;TwDMR*c82jSbn*$Btr-3UkEEQCXF zCc*(Y17RPWj<5&bg|G|Wxr(PDY=ct~w!kR}n_w%#1~?g^1m1zL4o*T?1#d@K0Vg6X zgE7Jq7$GczA;JO}Ak2e4!W`%!%z`e$4Co*{0kjdOK?~vWpo#Eppn>o>AR$bFI>IEV zArwFrVFFYT#zA=%O9**TL>L1Fgi(-3$N~{z1mqBgK^9>MWDo|y2?+fljnD^eET)d2Ntx`V(L^!rzVBM}}53<%eQHzGU$Y+l795bh5SNBByx z3E?ZiVF+Ii-hl9B;82A7fsF{)f!8Bk28XQT!K=7o6<@cCuU*C0tm3N?E`fs(>cOiJ z0&pNg46H|}0|y|~fc+6_!7Eq!D-f!|%MmVummypL`>mk%4`3GIZ?VVF@8`ifgmYjS zp$aS^oCSJ>GXSih_8$O4_;;W~_*bAs_!Q6}{0mSc{4-cY_#{|B_ym|o_$M%j@Q*-+ z@L}vntGIg=A42#G>_LP-#~wiVGwg>5e~R6Y@F&<0R`EWBKgM<;{1NtjguAitA$$n? zF2V<~dl5c>-GlIl*mn@#k9`~A53rpG@5Am!xC{Fh!tZ0>MEE`IE`;C3zJc&w>`sLD zV0R$=4z>f~x3RAy+=<9oWqX zzm9zw;qBO$5Z;E}gz#(FjR?Pr-GJ~b*!2i+#lDE}7VPr~Z^pi`iq|6iGIkBZFJV`& zpzeBr(y99*K8=DQy}JJd*P(QM0-*HjJ`PZNbsqzAdOr%bp}$`NK8Nrl;3|ZdgU=%TF!&6@4}njw z;-?T^20n@KgWyVpmxBL9_yO<F_b>-7crzSM5M3w3mDQ@`*{rMtGy0G z`f9JmkiOb$Fr+U;q%TCIFGQp-M5Hf7q%TCIFGQp-M5Hf7q%TCIFGN}2Yp@R@y|2M8 zUBwTq;w7ti@hZN56)!?~HFhDwZP*0}KZm^!;Z@js5q=gse-+=eis!B3xvO~2DxSTH z?_R~T5Pk+b6XB<^GZ20XJ00OCv3DW75_{(=o`�u~QL#0y_oa$FZ#lKZc#Wg4kht zGs4&Fw;()3eJLM>UjGJ!2j~w)xW9fQ!dL2Fk5H#yk5H>W0HH?zN`#C0 zS0G%_@4t#KM>wy48NxaJeh5|ibt|a*CHO1Chrv?_e*yl2@aN#q2!95iMEDc%1i~MK zKOy`P_#?vI;138N0>4N2Ab1?%1K=@)KLn2=ydV4y;Sa!X5#9%WgYf&{*9gA{9zpnB z@GFG(f?p!M4cw3LYv2b6zY6X{_!Y1V;jQ5N2)BdpBD@*gi}1_fd#iX4!Y_gEAiN2D z8{v&$C&C-R-8%5=8E*EVnZ=tle$DIEPrHn`%-EW4KX17Dka{nD-AGI&%N1c$1g zQr!c0p}&V+g`Gb8li8c#Iki*uY4vI9iJ_4%M0vyvPE(^?2cL}Zb`)-a<80Cs zsj#%vaF5BRr#BH!HIKseqxE2};Un`zujwWStxRqr-0W3r4ENd9?^b!-T?vn+@Mzc$ zsQ&F2yL80+Vg^^QLmo(%V)HHNzkh3jSOoXAq{QoEF@(tLm%PR4f;3O8~W zIO^jTqwWayQMXTE3PqI0fifJY&`&NETlTQgg+3B zB}$Xu&95<>xW;ff6s{Yo60{HZ*IT^cYed^pa`L;k$nk{=LpTSb@uSVj+LKTb%sv!D|e+0fiek zIM!XF2X&IIN zLXDpYr#e6mmvM&#XuvTgb=~be^e`+m5Lxi`R3T2zsl)u+2L`%d(FgdXyyg6$6$Y`*Ji{V_LmY4`N^B-#r_aRw1 zN(|)(HNV7{gZNNNwnG!~jZm=Rpy3TiIJ)lh);J+k9Zo8@4t;gl&(>O=PHqtH5-p$9 zEl?A&&CbZtm9qVok1Py>u}&(|>es}HaGOxDL0>BP@mQO$6+-o7BVV2zptSpg1}gYmtF*e`bKZePpq<8flL zK8@wC2FhJuvR~|neB+E1^Y$l)-I)jE=)9$1w!n^)SzjX?5eK94gm$L21vAQDb&s-* zFjee?BZF$a7<79lVw*V?h3k(9s=#wT=r$!rW<;vQO@y1BmBTfA<-V6CJ*82v6b>Ys zlZ|%f4@kX!ydD#CwL!$&N|ITr9+_+u$F42dG4fZV(a1DP6dJ8qiJGhcGv}=_+_|!PJ~mko55st1K>5eXYG`um@G3bvH}CeQ+JwY# z`5>JS^#&7}H*+nDt~d0L>TM>K7otfbHON;dE0hLB*X#N^A-0#zdxd_E^V7{|O{lOn zhSSO6VmP!RG|o`Dpj2xNI-bI$dcTK9o!@=l)xp05{A=ZaT==ytJ06M=VchG78m(4w zvd*cxC|rN&2_}*pFOK_xQ9KnI`z9)hsx8MC845neH=@`6)>hLv&+l0b(tC>pQ>t`C_ zV1n*M^Y~;kW-ivv-aJFkd~HVm19+z2*OU5}!IS!{;8h+USPyT~T!)1|_NLOl}-nd{ajMI)F=?~NOQqaK>{YuOeiKNQ4!HBeS{Q4WG zmbgKd=+sC<*N}8o#ehK?xG85Q)3!Q`F(=iR?3o07e?4f1Ekj?yy1qEIL}6-){1TXX z|8#lvKM`6^M_;Okh6SgoMudlHax|t!`O>J~DH*0Iv zs8DtONm-&{3M2y|vhEA|hCOdQC7Ae}FD{we0$ENQ=~iVB#Ep1?Xyvp?S)!Ouv#z)= z+NlqmhEgQbX4zJRX9&XD=Uatr-B_sHC=w26>C zX%42tL#vs{*an_@mT}d5gxh6Jf-g-iansZit34d_8!{xL(UR4Id!o%m-PjX49&grN z%*EVz%#;nc*iHiPwn|3bDe^a_kk~r4#L2QmN*WO5LfB4*JmYlNYDkWWLX&mba}3pL z)3sK4=%b)}LR+S~{Pn3NR@?a$-&k$uiBO1+jiOQWNQm`J4D6SR^#aw2vC!a)p)J@zJzmyo3W>K(E%8=af-knZB|6DkIxgREfD=T}fwS$lIa4y!{bbt^h67!( z#y|%dFWVtYOwsRtVrq$x%MwCXiUt^SlFy9EIzfg*VN+hP1}pf8E|;@hG~p1YvyFl+NA-ea*6PJG9!EE4Wr`wBcf*5M z#^rQOA;C{A!L3So+yg;Oko}r7P!K&ycd}4y(o&OZQgMIERA_s>@%W(J_ITzuO)YWQ z)DmxyCD=aQbaA%0E#Sze4P-mVcAC8eO-pztoo7c}>hY_(kW>aLht;v-W_TrNxaslMBpbc-#o+iy>o zvs9$VItoPC$WmcjBH$jSOZ~(+$MRlXZEA_CEa5A+U4*~stJFg7GCwH0xfD?`2+>5K z=&`i@OsN?U@ueQw8_asAmSCoqpk)bfr7!ggM#pGNp_R47n<+`Khm#I+oTV6ZI9qoV z$2HMh4|4i6d7f0yaKG#C$={=PlIuM0+@_i;+mKUL}QItWfHTwIJbeIbw}h+z;0V zJN_ru2QT(F9TH<`&yl>{1ncHXQck9m3?>F~z9l!80_{8tT<;!Mn*S&sz z{oHlh$7Ua2x?)KaMi}yBcL^arsj#+w}Q{YeSnJ=NKFpf0>Bo* za%JJlR*F)!$TqP+guobuO~}t(hm)4)r4Ns(!7CFWx)5U3DXPXz_~0 z=H6<={8fAVtNb^V?pQOFw&$W{u_1($rlPYQOK@z-i;Eee=I89Bs(@nNx&eeO)gfExz=dB5<;x$9-Y_U2m7%N8g7m-A)J>ne% zQw+1jAXKwgNh#&>8PZvQHyW=k!W3AwT&p-!Jsc*@HFqc79NH@Baidm_ z)jjM@txY%uGVostb`dF}T>XcuP zB)C!45s=dEX5ZN=<=s@U7%^vJekz#l8JuKKG7ye%NVM2`?J9UomdF+9TqhveOoePV zWZ@FHu^e{|vp#FVTkp6!wn92-NOiJ?Ky^Xh0pRl8C}Am(q45aX?lA`2Z3|Jd^+JJm z&{vPxyv>LsQ4U*2Vyd{9kiRj7ggkeRwF=2#Ph|QG->XEcF#`ejzDjO;$6DsiHgl4I zCMgXGbFIM{^|R<3vGULw@E7V;eq=0nqK$svX^>2uNSo@mP?R#Kk`~q>)=aiUP<@Xq z(c}VM-qE)vQ)I^D9uJstkV!dm?P{aQ>KU$T2qv3v_^Su{W! zl(BfUWN!KGji|qCE{rXv0wKl%15c~(Dv_1pK>tQL<&uzZrOd%xCtM>7)zPro?piax zs=3s44GW|@IV4BMnnVwa4P8$zLB`8U48ahl9&7gV35TB#FoqIAh>Z^IruZt;bfxf$ zr{f7~4%gi8X*qtElSHJ#2;v{R^C3c*UM?Q^=S@aUMOGOk7`%NYmxdcPLPssAc}!!J<=V?1l` zj(b%pMi+b~cqBAT+Jw5PBw3<;wwCm^D;$QL-#Tc?=5lf+O@Md_ti94D!Q|P4%edtibUy0DxHe2*F24?8Aa`|w| zYe+Oi>+D}vCFo+C3v_zvVQLU!yCBkQ(Tp!6Bx{C-G`5>Ovu`%e}XtFAqqO5P^X~a#uk8aF= zLzWN|1m|E1Es7ocB!f2+9Qt@)+{O0IW#cFkHw>NSuF29UI+FAEu1er++*#{M`GO^2 z3x`kLk>l+qm$^xZ)~I2eNS8`5GqZI(_WunvH(;WeKMe6-XC2Mv2YIvt!YEf3>s(`7;*P5P?pNc6YjwypM?t z?IzJf#3`4-)s2Kk?CWHm+yf`k@;YoKn=R=U5@It_8fQ$yZrzcxJAJ`{pGr_w$>Ela zs=vt+CQGy#3>aJu2hsM2NXpeN#ywod9?9Zv(g=5AgPsz{ji_XDt|Uts+LX=K<$|_? zD;%wfi40%jO<9&9t1i2}G$sqVTqmEg26{==>d-tgacwffcpZ+Rn@k7Wc-+qiM|79< zRkPlpkgAITTcT3;^&6_A(KlklHuO(Z?lbWGJ|=d?tx>jRHlV1g2PZui!Gt=;MdX1=Sl#Gkr65sd485d*8kR4`h=?%J!&zw}9FH2Ni(AhERAi9OVsh>6$%8tOe zO&4*|Yi;HQTiPLXan>Lf3WHgB5R4UlImT&g`_oJ?WT{3fxXUZKq+&PO4Ewt-qZ_wd z2DNtI=5wo(`rhpE|J*_T`{#X`19AyA45GK6YmaOtuGka_+*-`kG6k;Q58K@)fswLo zMilXQ*Im_ZKoVTPU6fpex5d`WRo3NiK+ii_7j7v#>h5^bXEF7;o{=ey3{JOhbyCtW zG^nA4Y&P9FrZ}>7gJd^k;A-w>wI&AgWIezIN)>!)_d2!zkiS6;oX|&8(b4HUa;{{R za?y3dTXqg|B)~(!K~~DD~PAKU~?Eh)7?uH9PI->Oc?Uy2lk)WpV^7)R$>dFe zY^fVe;Q4VkI&k@00@+Mjial4stIf%oWseWW&1TM?G#R|DNQ8nA;tTQTxD#_+j>SZQOfuS9_<%oiO3*iG6x3nY*-|l z2~VflmV)+ZJC_Vm`FvDy_IYAa6>i2e?WZd5qT4T~R zq`9uS*z($fEq5j0unXM~yQq>=-msL4BsCbgBHd`1GSG!?v=?Gc=`oe9wwqkgBvvG@ z7cu#(S~;(#S$zmA_WxfC&;K!LX{;@1ehSa?cdL(wR`F5`yO!Uv^jGb}@a(?=Z2)Pxe9tNIp+fQCpimk#S|NBteV8Z7SU5F#Z|vlYPLeg4 zBG!%E+B>qfXwZ>0$dit)L2|o_)(D)btQ;1#%A=ZD7^UliYqG+^&tv9g1%jeUZuQ zUngHQmNjUznI=^&^TB*y^fP8_%fO{*x>2s%**x^F$5DYW;R^SQt)y{d6_2xYc;Km2 zBcT+<8EFB|cko!eOjpH72=kr z18636rAL|~){Vn#vL9#DaDyP#O`5aAMs1vPNcKof4DsHK8y|ayzML}=CJTeL zXduWMOyMflP`+P8cbnQdK#xG&u@PE8TNeh>D7d))sDg|48qRYD| zm$g$!*j?WIn68*=*~H*rdJqu#2HC8Q;(VhhkmKI7`b19FV2W5bS_g5nUFuQkrZnWa zp2&8)eshB8TIw05=kUTCaFG_BDmy86aV;9eWDTZ>bz|7rFO?OcCqGe8-t7=gmSjP8u3at|ZY>L#;9jkwM%V z&zDO9U)#nEs(qJ#O{Kuf8cY%E#&AZ+ z>=;W$=mm&k#n3pDb`8^BE7M6+yf-$Eca1fxJ?yY~T-LR;g@UX>z(ae-K`}%-Jkr?j z%#BixkSWCGU|p`b8s$VVY;zCtB$Y{fHhO}dM5YSwYxm?GAVB;zx5Vp&@^T`yR@{)%B-Y}mbHI3#9?Hr_PgY(E?( zoTE|KxfTt=vIc(3(B@8t>w|o3n6tU;eHRsXjQcHn*^wUCe1gT(%6E-Ktc-g$PI0bv zuv8e5Vy;LQ8jazo>>g((pO>C|(c5JWrkH(h^w`9-yEKxj<>G*IR2=Z_i7C!k2Yd#u zi~3Qs&+qL;!j@b$kkY?rWnYW#?f|N zs588C;}mnswTz8rSp%ovRdw=Nk+lZB?qFizE{I91t1r>9bUc@AmlK_gVZid97HQmA zNu~?sc*f2+5^)Kyj+mfO&CsbNX<~ilR+SU1-h9$BPTL*)TIxYb)?kWQH?~bNf2Gfc z!`#RcNhIjTs5#DC`C2d%H(C<*0Q5*xDYUzTa;Ul%4T`b`Q^dM)ik7<;4I;7zQ}lBi zi%F})NEyf3M$^IbVxV6shRjsb*K`;dN%GmPoZr|ES3V=aARO4h(&vsoQQTW&mx zwgZuX*(H#|z+sVMu5cUAg$$HXg!cbKUS?F>x9;Fhz9=bcFol!3ed`Ac`TtX0HUrMn z&Moc_fB5&m|MNMZ-tS){iKq83nS0tR%oIcR^LITH&fiwNiS5Y?qsy@;uP{?g2=?qH zaoPwboJ#H4E6nq5&#bs)d4A7`lkRI*r2g46;(y00jOE4j3bQB0U!YeQhtXwSaqPH9 ze@xt{Kcadb+TttQ%YseHSAzXfl4nc~am%Py>sicHF=b0wTdvLv_6jo+M?SMHZgC_i zV(hKgV{W#?*Xq6uK{^MOf{=++Dd=OdAvOw|Yl&z%7j1{~KAX>*PemN2o+oY_)`w%( zNjq2 znZ-sZ)pgo>y|Rg6+NDg`Yw=n0k%*_tx%@#omrT`jo~UFL@mLfd%#SQXyk^3O?qaYD zUmBAIcu$FU@!4vtFyhHI`8q^Nhn4*d^(C{KJbTLA*@M}QQ!J04f5|&h+gI`#zL;8d zW@~fUSITXV1CQ<5)&G!bcIQ?io@OTtg`Z-VY|jJR6x#)Ra{pV;cRJ7!?20b`+;*SW zr!vaL|F+NHdZ+lzKnwj9cUPj#HbmsaHz=zH9V7l1H>9Q&=V8xon=iB}I&8M*bUPXm zI;kkU&eHBuj-kg&$GuI`TJ0voW5HMBBEhUP+OW}aOQ7F)!Q)OlRT&CmsM#Thd0)?; zv6~pbrRN)&oSO&19A%7)RHke;)`~4Al^Skt;4YgzO8Trsvly$?O`B_tk+&u0dxJW+ zrC;x65?SYHZQjW2D@kAR11McQKr_Db?8Ye;WiO~D?y2E9|1}%l6syNQH9X|a?|-M& zJ?%))yS9|DOIe#XWGU2TjxvCcc}YmNnRon$rAhX1<~M`)p?EdyAh?JvR4<#=i8i`SAQH zOC5NZ|8H|QXzp5aFRfFZJICuEoV{)K0`;5qpV5!!&cM!`-2#09?5}%#`7&swKdXDI z>Nx28@7mcXREL98v0sD8;yUf^noG4t@LKJK(DMIzi#Qg?HvYHPrSc_?DK1#d$r4jM z0w9qD?--Fj!;orG9=y;>x=VhmuVxbyk~Kn?lYTtWGo|d~OpYWP3#+#ur?^^jo&1d{ zuJnI-YKbq&5`kulU#VRWhw@#`XJtoIf}!G6!H%}nEZr{XK1!X)>|T(2^kn6Rjkz- z=~!uK^bZ_OGHU9Xqftk(PF5*i`>6blDV`0;kDRzEuKLJ#EV(J3SiBZ##ZB>yBQte6 ztg?hP+poroRNp5hxkl1R$17Ef+hen&<#`Wee_w{bjADsJk9nVv4)I zKa?fbenCR_XIWy3JImjdC8oF+eU~gT#ogV%t97%R|65(l|3AVgb9q?%B)5>MxN<&E zEo@-9Al{7j0%JefCuz|ZWc|^`i1agsAdm}?Z<#`ZJDUt zQ@o2>?MkM&T>5RfCdMlcKV>pj%{9(5v@sTAMywUPELkXYv&L}028}qml4;ApWmK=e zq?)2jq~uyM#j6_m(lVdOhpB?0%dpO(p^Vut28Z0{NRLxWi41_2izOu6cwVPp2r)x3(E96dWp(Rp9oZ!^bjgK90~6=}PW z33!cBL$O0f3nq7wWEVayOL*8~qv7iCMy6q=O=kF7DQ35LBebm^801I3rWodptr5+S zs+tIr;HG%xbcFm3+R%v6WZ2o0@Ti~7CI^HkLgw0BIYx~*yihM>C4Y?d4DCtXR#_rP z3b|oxEEWu;2`^RSg9PCz`b`DOfV-;M*Q&M#|gG@F`xY{P~%22(EIS>~<7CTHbK1WR~p zC0DfOH9P9?9;c_(9y+V_n1^v^%@$j_p0jFK9~4b-_ga*-isU4zXrqg9Xy|Wf98^n! zR4>q6pcgIkjCt&(nXE4m6k_Fp_83`0C>2aZ$6!mQtF);vxlEkhNoNW+Z^96iJmyB3 zvA8^qsIP6sVci-Y!{?_- zOP@;SI-y93hELlAV`nvMpu|ca9}nE_c_4p+$&YfDL^+8^Z5gAXAVlyWo~!4_wnjQS zO8e;qg=a&Cu1Tyq<#fB?ebWwSGkN)d*&tH(s^IY?0}*q*=g*nSf}>Q<*E{iIx8|I0 z$r8>1m&lZJBU{^m$320ByVWXXI8!xMjhP!AQ>!!Xk87@Q9M8>ivcwcG;htKRSo=%3 zcgPY`yo7s;{E}+zFX2{SIZg2r?sWMRd7hM5Nz_GfK&Sz?MuYLYB5#Ur&V3W_FszEX)dNBv|+NWh0thQP?E?#QVq#5uRQ&{=t)Qmm%?af%Lk6J5gV zavKu)NVF>n#v;+=^HI01qMLiy3_kPJ%)$aephu2^a=C7=y}LzjU5e*Gu) z1O4&(H_jal{vUg99_Ywc)eE1KGf&-j0`9#`_g))8n$R_;!b6~{QmG6zrlwTsHCE=N zlFCq(5y1h(={bH5?FcvmD){)EKJh8O_f)_M9C3OAPN=AWJ{<7rw@<3O>CQRHN$zVD zefRVqeNM7|Yp=cb+H22i{p0aFkH6`-0@4|8f)tY9zWLve+@tu>-r;X-77o8_i}@9w;DXSBoa+#mVt?e|B%V*4%Iuh{0dpT6~HTR*mB9=`d% zp3w9S%TEJ1*7PA^r>2D)5Bjl!o5c+9rnZyi zBww#r?0&w>57<1VcBC=V=o@NMF1Wp7)ITiF8U!D1x&ip(V#Axq)mLlF4?wR4W+9s%_Ql23RqD z7_OQznQsOJl}$vk-c%e`x|AZc+W}LtX%wTbWuTHVu_UW&Vols{yqv<*bgx)y{cQ;Y9@8qQZ>5F=B zG=+O6)#~?3?2yOh)Tj}Q!j2*jhQZLx=tuu0ECZv4iISl-L_XHjF{$T_%1p9Bv@+y? z=NpZ2jB3>qW>LZ2qc8vv@a%9bqYFrH>g3fVPI9*5*T$q^baM4Rfis%s20Ynghwjm9 z!ZJ#RDp$0u$`3IIby41#Qi7^Z6X`*j&N^<3EQ#ap1RQU5Hp1{@Xo>8{+jKdTvuu~q zhi%oICKJ6>mCkzuj~Kb6Be4a#GQw&*uM5kt2XcOZ5vf`%+Kq$w02Rk4$e_zuPALVr zgICy5!a;IE58C<45RnhY0hw!9Xh~$6C=IaklpzUBB%!h7Sn2q9w?l)dWP`rrAMrO38Gkg&m3kRjfgr~v8O%v`s5~{1!TX*LrMeC9 z_+_ARu)DVtBGTYFr#OKer0>d{2d?Q_S`~+*YNZ78lU}o%%=H{`I?VCzb~yDxIW!87 zF%Y(-)WRXM1YY+kYAVJ$C>c}ej$ABI>0-tt2{j!#4$DZ!r(FcqR7qy|yw9;l7tUAM zXw@z0*)m~(+sPn7!l;0#)@B$^6it9M7m90TGF2g{fzpbxR=9`}GbC!EpSb_}~@_K9Aq_SR?%6??R!-vfVAmVG34y6GS9~#0X6{ zNBwF!Yg8u%g`%x?nZ}esv$OekAuI9>s}F^&Vx2nAfN&XENJ@<5LXhl9T7@z>(wwpo zX{WU=41yVF9jCMR#~}joF}I4F6A-e5=@J2LHfxxxPSQ2BW?$S}K;LBH<{H*$%4K1g%<D z&DP0`pNEhpYVH0+h|u!=dLZit_>PvE;B64!u~KsQK`mT+^uKG>^(A-+?pOjG4T z5=^H@T|H6=%ScVeywJ_Eb*WTDTWvDgnPM0S@6q%zO&LuqcDWmii9-_BH*VA;#nX9< ztR$d%u}m*nrj&lmt+qPpc#H~W9}9}88eoAg(sEI}q(eZP}7NeeTrEMBEC#?hHtuQaJmi~u*xdYN#f2DJGXA%aMzY_~WBdxAqF zN5}{a_i7toI;`5&5@HqSeJM+b1_{ACjc6d4b0y={T_lM9)C$Ch&6CX~t`6&u6 ze`rBVM{8b_70U_5p@V5J)$npgy|f)_9i6m_sR}W&s-nf z?LWT%yuH8Kd;8v3Y!5)D_M@%e+SHuCV#sjgM~p*Nvau zcs&qr?@JaGg5nWrNa^n1ETH5>WSd!jm<1buNCjwod^5{~~0+lc8f&_ZL z$|K9nh(OO*dF1&^#arMdKVvEAe2qt*w-mI%OFnlgXn~jf^rfH$M)aIzMs&V((6Vxn z1@>@dDQJN`eA-gb0(*FDDQJN`Jh~LLz#bl13R++f&t3{TU*nNyEd?!T+Gj3n+VgGb z+Ol%s1upp+OF;|l;pt033+&RNj63 z)um@ISk@8Pd}9etfNs}1NG8iXJ+Ci&Cv6Hrazl7k|G1wxY= zOBxHX;7mZPjm0T7;x!8r6J5WEi1i{@AC^#skP@|#K+VcPW@V&uc!|?%vP#!2L~NB% z;k2xYV9v?{yz!>30vXuu#G1mY*%bS>Q5ShLo&o{#XSB8lXc4?#P8kzhWr}vDHk^1< zf25{#CR?o(Gp+*hf#g-%0YAW^MICLc)i#o)IxrG@Aq4)x5$z%^&s+DWl9q+(UD7gF zPe>^`QC7i2x^~^R4SQf~&^cPB0~t*;eUZv=mL^&t^s?_6GDrFj+DTS}&ZsZf%23o+ zC;aF(Evpx4dEOm-N@=-ym$aPh3MJR*I-Vg24Ja^@w4a62=V^JERZ}Jgmwc->PRURq zpHFGid@h5E0>)BCpR2H@?K=gcSG!Hixr?+s?>|4Kw5;7FEpvvhA!XQd$GoM}jUtC5 zS|ju>cY2Hj%MKH+-7$iEa{zI;KTQOj$on9^6W(e>Ey;k}u{tgr3a{LzW#u9*&ks1C zQd+)SemRwc(i73lq1|Y?;m5NOuBKt@99w2wS*C_l9%fwwc1BGdt;7s!08U_3G0>qQ z>c#2>JVoe|f4eQqU*zn5)~lUoFWC1wv%>$RDLEXF)*1Y!QhU+BYd68ofm%7$8qEeb zF4?pYV_F)W4vc9qc%Kfw_^{X(Tc?9;t_^cKjH`lfLAqIMgNqrcFdL{d?Gn=RitQv+ z29KcAiK3cI5363L5#ykC2JvjOn!yJ;)RKDjLLtHWoqhovv?}15TJ6@@M_gdYXLdwB zX*ynif-#lumb*xG0$#P9A`(cIlsmS(%qZVJZ-SA4`&?P(HCuFCw_XTnA7U8B>`S(3 zCcDv|InIvjWZoN3hPO{Jq>HpXKfC{w((>JM7l~Ssb)hz&1XIQ&ID^&7tl?qjxC<(# zQ5D_GWI%ZDX3EDK1}hVVsKD!uftDSqV!1qROfveY*ScNH;ze4XUsHc7X<58m)3^i$ z6(?OsN7Il2W0Q(Ywu?yT94*t;RI!@^DYY=o9k&Y!$Z1uQwfKZ{D=;@Lq@Yf@X+RSi z1ope#DGL{Ad43b(Q%cKsYZ{k;Rm(GaIdEIdHj=&sr>AP_yrGZce3hEi!Mi6tD%ng> zDv6nFA&=DKP{vs{UFYQ02w0yVxGE3uDv(y)wWlv4eh&>h6 z;#4`umKh}(6c`PpJE!$dDxP42R%$p(nZ;N}bk#&KER0RFM1wG@!`qi6+0*!cFWLC1 zjmtlC`HL#^RmaltJxA|3`nn_a=z+uEIQ+&# z^YFog-#+*jkO}Y^`@g^c9s7m-NA}*o_nmvCy(_zay!*YowcY3K{MpW%cHEs8Z2#r< zo3}gL(AGz`-n!*)VUhn8`O(NILT>)U=1*+CeDlQ{{~HwkgnwTAYJB_gM|aLY6(q3v zj+jacX+5V=87e&deetWop2=ePyoG5>j#iTi+2C%%(Pi-Yor$!VC^zBoGWh&jGO6TpES*-^TX1L@e14H6CpnH$)tr&OS^BkQ@cG$)CT;7i zU|5QH6aK;_@H2BRBb|^fD=p=2X+F9PK0leDgfyQ^OSF-^S^5i>!RJRwRbtY%%%z!I zrC(hJpSL=elkBXOv4mT7fBrJ~yw#~pf=)>wA3A?i_|I4dpSLadS2gU_1{MQJHrVD$7Yt9$k`_`KN|JSWRMqw9&A z!ar*neBNx*lq^wNmf>%?%4aTv&$m0USTkvfAoR%1(x0&ee&$SzvNIV*&SwncCj9Bk z;Pc)lE9bICmI9ZwZ^9p52A?;Zgsr66L`G+C^|*(Y!RO7!5I^H~gg8YHk zBy;jj;U8EApZ7L8n@cBkffjCA-Tlkp^Hyi)^K34q>bYBf{=OyfGlyWQR7SQba2x%W zv$}T~eBSDEs>pz-HdgMI@b@f(&$m01)O9^+W)inL?B!+f`F58K)1oESv~LN2X&HRJ z-Ln$I@Jupo-*S~F%i#0vo=u1vW2%glx!LZ=%i!}3m!qw0K53??TV`{#3_fo*bdENe zq+s*6y3FBX_}N=z8hD+Xp)A~JQa8m@wOm@~t-DPdWII%TaJ2X8GPPs1n@vW)U#6RmaE)e2A^*@ zHkHx2tfuB~!MB#d=gr2<8fh*q81k)#i!6bkxy)v&f*^>gRPI*YHQ;Z>SO%Xr8-vS8Qc6h~H#_XBK7Sc}-fR+*MvHbvu$UW0`>IzigU`2np66|Fc9u@v zRNbpyu?#-%ZIX62r|0r!{+95cw+udSHnw7=QaYCqZoyx^3_kB|Of@UgAZ7t|OZe$B z_`KESEYNh|iMVoehYW z0S_+#kn`5J^-u_S?0i5By|b$#@vTQgz@z5_2y(u6Z9NhK?p*+g&HIwAdqTiH3joBO zB7t%uzBwo z`Ma=$MeTr}DPbcl;eRbEVcz>k{`P_r&UDeu3+gc6MI#>z0Uuc)(p(pXpvZ?qz=szA z&RpM){B;QUs|5h$PHRQtk-rQ9A6)>z=G7YcNC^1z1pxF+rT;7h{PiNhog#rs|5XV1 z;3B}hc}M;<1iWtn0J_s!L8bp-IRKt->B#?F4mh*uiu`^E_@f0P!DqDehaun(7Xi++ z!v{jZFP;y;$uk}3ogv`Y769URY6-N%yFmlIR7Xi*x z`qx6hA1?r)XDafzJ2jFK$(a0}`fd94#a7Lt`2?2j`K7c^axS2l>0UuZZICCL2 z^8OI;JBt8kZ0WZ{z=sw9&b+IL{6z>@Xc}iq_|*#lXU4eqgn(Z;UnHD7qorRC0YA0? z5IY0-kqZE4>hQxC0M6LbkA{H%cacbE`omj8z*`pph%=t=zl4AvS_C+w*0+R!H!lL5 z>C-zybhqrX9XN1iWbh0Kc=+z!`sE2zdP>z?n+FE(E-85#WrQ zd2I;zjzxeob@=uW@a>BLXH4VUW`M01o(~|;_@%9A2#CJS@q>yVM4|bYrE^aYw_Xqe zUa+79c)sDbu3i9`ufx{!F96J&_ts}zZf}|^EtZ}nT1Nw6J3MlJOA}`v9B(}vFn=P8 z*Jmv#;fyohdS)onGZz5PykOmW#_9QgbmK=hF2C_|?$XCEefK5d{&=@#HP|#0qM0NM;4HYm;@IMy&f0MMypu0oIvnc+tscIGq%hMY*S)sBxDOt zw#z1y4AbtLd^8W4(O5&}+7#~fqS<703^n3hO}p;T*wQPo<;QuS?weSmQ1F{fR2Ion ztx{LfVNmK$T3#xZuhbCP?z5w`cD+AiORd0m+@r@SiMDyCm55igSiO_Tq~b19b2v8F zU9< zVWf7wJ7fFO71)A1hIG6(?wd)tuUMsNtDj3I!5w>6ZP-Oj8p%Cz)GF6xpX_MYJ2SQ~ zS%K{ki|PW%b_}KMR#&8A-9C^M#H-12Sx;{FtQIC$oH9AFP|#%FjO}Nxz?Q%gea?`I zQ=nV49#^JTR2cW`$k2~DL0#4o@mLu~5!dTr+V%E~?TatYl6wUfla+?jAhRhyW{o`$&WPr4)#mSI} z>s`c=WWw*}n&qinQm3HF+!@;!tsrd_ZdCHEW~^p7ZLDt8WtqV}TWcpug{W8?bh4!w zSLz1+m{HQMH)m|)D@g0-qm5QHXf$n*f2GiBpz#v8c9xc08XFCw5ao@_99ehGsRL=( z8#A`##rc1)q`Fy{?8JDUa=KZiP(Z~*yPP)R7@clVhy^)@TUZ1gztst$w zdZ1+OdTqv*SV6ZGIuynO@S4bNp%@2Iv1z5vC#YCW;`+62I+KJmC@#ZBwh5%I&e-BB zu;n@eQRf`D$5iAh$CYKDTk z*`Yz$QVVNJL~cmx1JcvQD(JkW8Czrpwrga;)~*+4Y~dByu8}ZXyY9@`LMzzq8X2>- z>z_Mgdu;`_Yb4j!t`}x(U$_EWmo^km8nI1R7VV@4ae1Ln)2h+n+;S7?q9zSx0ys*^ zxTRhHoEh8b3T(C2^O0!RAD^*(!76OAGCY|OwbZyk*2Jt=@APxif|9FX1XG8DZa1Ok zmEJh0TEK2Ud&c(a3T$m6Q7zSTc1ucR6srZ&oZE7FC`~H0Oo6EC6Oc!qEOWeX*A(sg zXU*6?e+9NF)j>+RLN!r|7Fne@h;hYA86!C^r;W>L5KoaFWWoI*$jgJyYtPtz#tLlf zNxM-^QYr`TkmR9i3++-dYOI%Y#mv}>M{Dh(KB*dL*NcPE!J4ss-U@8TMQdn);Et?d z2NqHscUsd@%p+vAEb3_o1mj}InG$JAi7L=-e#Z8>7iaOkQc1*us7J+XU>Fzp7>Fg> zYfM^5eiD=@DZv*CrF3s>n?5!K(wZ~2pS}XyWWmUddvPIZM+bT|YvKjc*H{Fk=5pl# zN$7N7b~8jb*#M~mt{XG9&sl-3E2Lzp5*48m+h$xEqNZhV�`D6YxqifUIWUk-6d+ zY7T(3`i$+B71)AQQ$ab1PXe@F%w)?x$qVSfQNYPA>Q>rZqN{sjLC9e==WExs8QV`= zfi0zgz=RD)7?+}c+wKQeVDyS`wQWy3?zF9pM_97ZPGYjq0|utf*gm!b+qzvZ)EZTu zAVGXcQ$;9d*h+Wp!I-m(Qj)KaGhHv{3_-*`FdNU!*gm=fTeIU}(VS&C2{%7!(F!u~ z%X%@^adUAAceMiJvePya=!y+ojWT2V$O>$8g{IIf6738a55bcboy+K@YRZVgQa;7F z%~a1%`q_cotObI0U7oRh_6ls{R>K9?BE^JKbU74FOc*SoRBb^L8e`VVL1wG!BqY{J zGeBBt#`ak&uq{jy-8zyp8?j$ATU5B%+1gvBlJy*+P;MoN_Ef6sAZHF+^PK6z6hrV<7G8IT?UK$jI|j zZ?ED0XQ{N;aKJPAHO7JSQ*W;^?46eZxZdzViuy@m*zg<0MynA_PQ|)C3wOq&vU#cB znl${R-mPWi(cDJsDZsxl3-|ot-YKfo@q&sMJochTUjEoQg1iXutvmfn`Hs}vH~^l_ zynVM41IJJZJGGGW69YGzu})$<88;KTYcWSBh^ycdsfuRZ96jkwt34($z=QIzSoWWA zDsC?})ETcC7u%ysyLinUraRgA^jZZx<*5lp3v;hYn$#t0nBE@qp54+x1aK%X=alM5 zge^xPdvcmmi)0++Yw1dI-x4TuwZy=)s-LFf4zBOaQgIJ9udHERcdH{p@%X>Xj<}Ae zcpO;RCvwi8_O=%`I}NYUAG9j|zflLgB>?D-k97w{kc#V$es|g{K8gJfLCJ;v&Lcr+ z^;~PxObPL;&1zBWyA`yER*iOzD+^AC82WuW3xk+2AOkiK({I!~Cm8gBE>B%ct5b4X z^^PCB=rpe7?4wE_WKRrYXu_&Wqw>{&><5xPFpw5D zf$Kfq6UGXPgTNFMUe2;@iS(RYiLNvoairw>UOf|l%z^{lsrT3xm!KYd#i=|4?^Gh- z$4;ejYL!B8Dj1A|@WdF3#7?UY`rVhh*SAmm-B*`3udHEAcdy@(=k>dP^#N_&5pk{` zbvvU-mnpzR%fPwas}kI0e_&N zpiiQ^fe91_C7e6VPlsNe?GZ?c6D_wgNpT!a6bS*z)Vg*i8?(EqTuO`QYmi%e!tN$5 zlKHrDHI}S-jDVIBX=JQvOna0rN-5v1qDWPk(zUvqS3N4(FmQ-gY;D-k>{O#>Rf7^z zE#n{ooh!t2n6QeSacY9lK%5oa&1uzvyE$F|Z%6cvqc`sVslc}g03|H7c(0t~!Ct99yQZn|j%{C+GeQ09#)CVIlZD*aVC?^O?ql4H}s{e&e(*8eaYwZ z*LuA;76%F0T1tm#B&A|a%wQv1FB_d0L&`1Hx(YF?%pSw419$$#jc+cV_Pj4EY+hlz z!PsPa#lT~+Vz1|zgl#pq@lp&q3qw%jwaa=Kbq?urGCW$<3IS~_HBm?DoS`C9mR!%d_RiL>EPMI4BJ(M5fW0K=3sw#&p$cTJMSR7Nx8( zcfn>=4~^ZqY3ytG=DRk`z~^jH{GS;6ztT|fM2&s^_Vpe60-tPS$Kiz&b*6iDY#5W6 z2#rvfub4F6CM{7IRE#8ajT!b+N&TuBD|U7EiSG-@G!YxB!nBugsT5d-NmQdb#>=t( zRkT(ap)^X!qdYEvxXUyJ%OKktR7SIS`5L74M^{w{ZlG>Tj9nYln|{SCP3dtgi!eZ( zQ-85^UjP_8sQT%?fcnj!yN@bgh<^3`2UqTR9yZGy`Dn-Haub?z#YCU2G^n0=)`NEj zL8}2~ATN5P>a_d{DEicM7CK&`SgAX$YN1oTbIhG_o_W{+c;|MZ(=S*04X*~MP!v9A zqu{e1yK-Yc1B=DLo(5Ix3c}n-0{FfBbVnD#iPO@+ekKVIn;K{KM;(G_qeY6Vc7|Em zDQTT*RaIJcQpoyQJfT1K3edZ5FM>c3eCV;Vw7dwmHf)Zsja=(me3GVM&%1U_j*l=) zZ6~je*vZrlM)l+{A@$4YGev+W0c1jObL#RU8W!H16hVH@YpO@<1M3>g8yXDGht+JB z1=%#?>8h(LPKQ19aF3rliP>OSXq@f_J$6oe&W}*%%->FJXUS}cj6t)-?Q978#L#=2 z{zw8v<5tm9l;xFUiTqR+y0X#c#$cbSo7PoN*BZmC4a*rBtl3IbDnUNRmXlyKnN{xg zA^bwaJgw^7WZ)B1<(o51m^d}p089}IU@?BGw3ihMlqiXXLKM0EO~oynJ#SpS_2s#v zCfveDLLl^&{B*#k1Wg<8L(#As?Etfh-qpCxWD$$XfSaQ?M$JzgcU&OL!mY?qpwkV( zn|qP)Y2D~9p_)W-3a-M8(0Buv`yF>svh`R7;nFD4%3kfz7F1N7Q~<6q+igX1sC>OI zX-#}m?jPg_ioCY&)eTWNFBcS@P`h+ z^zd^J$*uerw6!03U*yLlU$gT=J72mh@4amQ@Au!m|Azf9*w0_;UP@hhWb+-HuLCgz zUbgeOJLJwi2agQu%&wG3M|G zLprI`Xmt}!K2vRGtE!xC%<Jx8ATk5(+Vvm%`r z%GWN}ndY!FF!=T)ooYJsd&rygigeV9bT3YHZW% zr03dq=LIX$J%2^I=dMV1WktG2SEPGZNOz(AecOyqneciI1cJ~!o!2SJugv$ioo`%` z?i*I53;X%S{jERacQIXWMY`^cE_b2bCsw3mW^@Hq z-S@6Y_dP4pefNy+V*R}?`&XoU(~5L&T#@b#A)O^=D6X8Vr(2_(ohQT|$(&!?910_pZg)d z3-$LMAwNAab*o9b)&;>eL}`5Hbnz&xhk7C1AT0Mne|n=|UPu@E)7*u0p+C)CSl{nj zQ7_-QBHim(q`T2CFO)Cze;4cdhW%YQ|9fD?_V|_+>ArbIy6Y!@KcfUeoTf?#g2?zp-lUFGEOH@;(jw)d&?_pBBdx7%bzx*LAz!uEZ5#xHjv-9szVJ-8y> z(^jN=>WXwvxpaNwd8@Q$$nQcw{_+*+rYq7-fNURLMZW(iG&8+i+np_rGbsaxy$g zoZPqh)tA3*`L)s47yM7Ks(w7~ZNxt|UD{p30xV)$<^KSA%dwyx8wfjWkng zOo}qEw8rEEy%~aw=dvTFScYmwUK#Y1QLP{mjSgEDdQ-yU+pP&|aouW-Ax1luwGrhI z5evq3DO%{GC0~r;1IC(0;W67Co9SAe>eow#0h4h#>5J@MH>^j?r-ag=ks~S3!J9b? z9oJ!5>Q~6z*o&rXRd9F8QELvCU4V#m}C`IcWQnh)6 zvsiUBf(JhB%EIM))<#@j8*wQ_z%I%Z#Hw0I4)cwMn=yt}i3Z_Ylva)>;gpkdnW-BS z;|zAsqah;Rf*Bg5e6A9(Wl%9=pg@euM-yF;y4-Gf(P|+r)jP;2)_LG})<*pH+KBhA zU5`iBMm&3lNKQSiiZv?+z9hq%Q#p{~g5dGeB-^6vNvf3#GFX4Q#5>nUydy+Rl$z0&@SZ&CC-OzjON`U?JT$P( zba@cZ*6N*lwj0eOW1ZUg{k0Llw>IK;L&V6I2F@^#^B=6v8Y;XEx@%> zvPI^NuGNXwA{*->HveU9#6O3K0WLLsDu;@+36EgEMo;uIn8yO%$2ZN$shMoe!Y zVvQ1B85Pq(&FGgvQhR#XY~kueC*l>5kj%riVI@z|uv6P+)<)24Bd8GJlZjf3wwo4) z^Bp;5S>@g&z#FXr-LGNkVLjg{7=n+*^49Tht&RB2wGqD&BC2fFFG6TTX~w&;OmN9)zp3jv zTpcS3)5I>py}y0H|Ylin(YtrWi^8`Zq<5NUmKyVjZi~Gr{9jr z-VpcAF`qVZ9-3mNP4$URQYkAEMkpnSDS-R<=IG()tc|!5BHT97?})-wh-P{c3{_(7 zR*Jzy#;t<@o})N7tQYF(UUFjB9=_2B*FZeDb{S8bAviXccT}sSq_eR^yH`%POt_{K z(jcnwWT6Fie4-h?)2oXZyZ-w4!9%Jv*5 z<)I9N<+FA=m&NXT_1cJ6g$QaiOh)re0xLLLP}9x0GX<&a3Y|iV?N>=H=?}AYsal_E z*rm6ujd=471ez|7(Xu-vQxH*UaWyExl!s+351!l;<7qisgzEJsB8#J~8|^`@j(Fh%kTZ;=4b9cu=5AIKf3z`n|r&N{a=mTxA}WJ z_eWlN5JXbj-*@n)edypN8}HffZ9jYO`?fx~_t@@-PU%iDV1K~h`OKZY?e}bd>vm=P z+SWg8{r9b}+%mTwi+m{ZqmeI+WFt@6{DaLmu2+{E)b}Q4iW9|g3zbq8+;4X^y};8B z)$6j^Va903usX`(q>&sYMBGySd+y)ZI0!o>TZURA->;Y4Kr~yGe1?`}p9Y&F$GfmD<=inw|LEq=n-+LVyVfGp)_h zKJe}>a^vu(-j1F(6E9^CXt5sCqhlHE7m!jmhDiu5wDFp#0>3GXyc3K-6#s6XeCYQ; zS&z+7+#pxB8g$#`^hps*fEslSD4TAB7fp4I?3 z{b;0H?jk9RGqPp3onsO{s^%EIQfR{&fh5^YY43+lABMZcjw^`P3GKonk z4_BB5Q|5bI;5EI#!7#McG|0G4ZTx z7`VOR&u5Bw+EcbSHa36m)3>CXO#;VI@b+$$^K$(;Egyb-@2ghZKLsqmZB|3b7<4ei zP8awrHY{YYc6Th}8B>J1NNz9;m@3grjS}88*-jt4AE3T0!oecbB2~T+tD;Or6zWl^|9HURE9{pOBZORhTF|t zTq$HmO}JI8j?KK*0x^3>&7wq3)l|=AUjaZ*d2kDjTp@2yYczZ6^dJhp{MlL6r0i(u zswmWlm_*+bY`@o(#A?Asa~4-?Fi>^MM&EJ1i2NsD*lrJ!RUl z4n7{zO+RQ(^JdvD5BY!`z&Q_9$ArH1KY>EtHbY@}ju9CPyvwBIBANE1WigWgRaV_P z!iy=AcKMpA)}h)&-}ruDGXHsoN~aQ3G?^{BafjAweh+JR)a*bXLQp}4iCz~Ha%^={ z$B71X-{Tt__k7k2h4YCKSxF_jLAjEp?U76N%wCJ@Bx!`YUi6~LQzAt#MjVziR=@-_Rt%EJrBt zy7~zbPMVlNlo~Ba+O~yT+O}9yrm&;Tv@Q?p-W8?VGXQ}-(v~|$c^jyI8-Q$k{a|jz&nPgggOix)|1L}flPM6b_ z3O9X(Aq%8j{t*w1qc z?H+6=`y)XYRi7PBu`R&z;OYz|;BK!2UB$U_aw1ejq(O9K$(`oXSdNL4K|d|wB$JNY z@vLwkcw~6*-V7B>%Xw&Ij8imh>AeQYQ?Bor*<7_}R@(58jynS#6V$-yD3=DHy+RZb z5Lb!L#J60(ouVMCie9^hPwf2F9-r!5g)0)zt6ak@Cn@qiF#oyKoR!5TWBuNMBs3L+ zB(s*v=d%S0B@$!D@Q8NX)>}?3Q1Px=pbowcXyXTGC|FB5a8RA5pmc(c;=>$xOIh>A zIGcbZR7H~6`gBN1rWS3FE?oxJyB%u7>++_lSd!W{6Fh~s^wt=OsvO+`xqP^B4y9E) z(e&!h(0u?nn60mvp)wkcLtZ@rwi}=-+~?epIbsyG<)Dd-u4b#SPeHt-M?2-(z5u%N zeKQmf;yP9o7?l$UuPF7Q%I2H#UNwPpX0HbxYNvExQ2e6SZj?4&3mi|-0SDwGecJCE1WB>SV>kNcIAQ*-N$7HxWCS?$G&o6;-jk$f9 z0Y+Z}s!`?f{I&C=_kvmFqqmdB;xI~zMcS&*aXtDH&@QW-l+QI^WdFA|Ua)&Q={Qy| zzw7eX@8g$0=koI|ee}{#UV6(AGKcavUbV2SJ$$ZolM7WVPL})CIX&JBN=ys+g z2~P#f-b$i;+P-4g%cM!#YCBm7t|hBs9ixN#5Q2hK-HGa2XXyN-WzO^FHO~cyLRwT8ymaf@uXj4@*IjtnMS@^;%$fLGAxuf z{ZPo)CgG&0@c2-4^f;fWPO@StUGfWQIG!xsT= zq#(9F>5ZeJpMKy^Kw04#ei!nXk|&6MYk;FYOe@*dvYt+(?E*vfIF4mgk|0;{2|i+v z-wp!ue(kKRN}j}FwBBp4)=&^)}XalV+Wqd4-b7Cc%(T46luL4^l;lg}a zk5*Il2{xt8Y>9N8l3{o2HCG!#QcmtwExwbcFp;S8ard4gSXBPutgOKVRt&a-+GDad zQYWnuI#ddAGe!42p^#?cuoh(!)p9x8tQ~^AuI=z}!ccn}JA%?ET~ZhaM$ zi~87W!n)1X(~d^)sEO_U7-;42zQ~lIkr+c)UBN;?#G-nqLRPqZx)}smw#hqfrCbwh zQLkPrvPZ82sIZm6rdciT>qC%o)~;u|ZPCd$y;fB%7p!*4AS4Fnl7{FijdGM2J~OjGVo`_XQl^DC$Ub}2!2v(7!4!;d_nYVCgctf+oKPJ21iDGk&-!A?tF z+tyNjfzH5S54J%WDY4uLIBF6dk$WEmdjEwG74ISKz6OtdlpomO7-bssbv2`vaulm% zg_PsAtW1Dt9_ya`5ZE3HZwvN>BrN4B^>ngd6y+i~Ev{FzrU;_0)|w>&q*D}egz3gC zqXpdqPJs|oi}+pOsE;;87CTlIQCS7^44W*ZqtQ}kxk^kPjWxE{sry96fsu+}w0boP ziDJi~`ya<=61r$HG3dvgG$$i&Cddj@IgNuzv6vd5Rk}*|eRYE5RE=*d_k+`=oyiQ< zWN13qO_gwi>S(P3o!MR$YJHlvDlTwL} zCCBWVBWlekNwkoLTDG&b-A93gdixAj&1a|rE9V&?Fq*EZBem(0r21Q_+Q{ZtIZ z&MenP(Uy&QO>kKT+)(pq-c8_RD-om7(I~}U{)?02A&CF~>W$;C+W(84e+Iui`STw} z1AF)W`>4umhS%@EnJ!?BU?>-de7hrE08WM>M9)OBg<k3I2p0SQFO%h=UH%<{BJ4&@U?#1WWDOmA0O*Dx}fPsoCKtyya)YrOAc zT6PKt@q|>$^~X~A8a}GmVm-XJ^puUE=-o2iO zob7q1!I9@;7N34J*E`Nn7x1stJDy+!^|Qu%8p8QjgyWm_o<%tZQGjPz67C3jeA=;| zsIh}M)~8uI&NX%@r|2%5>zldmSR*KnZpW2If0`mn*>bl>Tkg2;xGB70b=W7IE&$Fr zUAY3TkzLD{8FRo0(_v|xEMomB!CcGssVo!s#khmo2a zXHL`71Y_?;aBnc~%zX0M5bEH7=&boawocZ!Q^r@;h=?_#Un7Lq`2$wbUAa?@vLNKQi2)YD?4SQ zlDsNb>fIE0;yBWV`D`ugO^g~D=4D(MS$s*7L!pY4ih2XY2JghVRKA9blL7I&`2_$%q~M2I8umYC`19zn-nN1 zSo{Up<_j+uKpcw^k{Lj#T6Lg`z%M;QKFW01q*ld0*zk%VaYQ2+m1>cEP3cDvGGB=;d!lZdMxbsyF7)6!USldN7;JTql0yd?R2^s{ zxS9u4@uEr7af_cczS-~t12TKl%%d~s&z(8nnENVtiC;2(YWjw47fn4m<(+*0#2+WJ z3YQD zLaK4URBZ%9K5GcJC#-mw&92LGxpj>DneqA!kG{#QeqJ$th+Jb9$A(K2SDd?QR}o&g zAK~s!heavLEl1p2P|o-hDZK<*ji}sR&yJ^;7xn$T(lM3adEb{WvF=bfiDf zaIQ~gx;4^C6{C<*KO6GMNKapdRHSF_M>?S=`GSUYxLn>EltPZ}BHsjJp) zV@3_5QPm?o*%35|!v)jU5LIlo2h~r;JTk)5S1lV_(YT}|*TR9VxaA_wsG=i}9(+D= zD2(f$9)NUY&MQt|UD<%qu%sgyfliDAjVax(Vtm8v4~22#E)1|oW)UUk;r+Jze+Tza31@RfHR3gP;f zFRBL&TJ)5CEjmOvy`;lkI&_O}xm+1o_|pdVXy*+h<6T-kS;afB@I$;)OOiDmX4}bl zJkx^{xtw3@HTtwI%SRKq1- zq3mnL!P0+gWq*YV@BAwVpEr&S^7PfS_jUEq;hfMDGkzaPHBax>dz^!=4qdBa3`2*) zc#w%db@i-$U2Pb4wT_+F1G{=ltFB`6rv{(D7!~2-C&z&2zMwYb0e0PNx7(1ao$U8g zQX~<9oOw^agTO-C8JFB-%#L#uZE zkzbxS_;Z1Oj{FQ7 z*aM9Emd!da>d`Ayq_K~UjCAMf2`bV9%uyc8q(VqJ$BB}+0X4k#oKwu2JBqh17I-cw zWI>>;G8AqW8B#;4?L_X}a*Phlk?|)gNaN2B1@fRVx^wls4IpQibfiAgKn{AG9-+@? zRd^Q-9FB{hJQT={eLgp}dn_{cz}TW``W1$~hO)u34V`*q;Xk*rvtM0o%>K>7hbFEW z#{C1a`7Rj$<@njN4mI|l>C(lAXQ##pvw ziGP~BYZy~-_QkQ^%q?x3nL1(oBI7gLzBYb?@pDrjG2Uprcq(W-2SopWaB|1Yw?L)= zVdk~d?wPsif1m!~Y;C$V@l)`w0^F6`;%9K}fn~(B(_JpMBW+vW1?B6MJ6FqF``L85 zCx>f6&XGu`dPvw;K(#xMI{ISn_d^+lQiZ_@M*zv;f}86FqIr(+7YiP1GQ>2wOx_lx z3vj>WK(cdB4mh|-QmoYqMYBv-v*uPW!uoAx@V-x~g=AOOn=D3KW=|&{6=~O8YRHkW zz_m`ZRH~qYjSuC7Cf!cKaoH?(^Q1c}2Q9T^U2-6z$1-OfqH4rts%H%+=?03*y>!SA z$-%r-4EiA|gE4%Kjr+X?JjA=JreU->7EflJfpF1k&j+LRLZKgZ1MFdsxy*uaYmB$n z4mMcEo$X7(S!O7MPh%0$->tgjMw;*{sIOnEL^6qP$Dc0QlaX*wu?BKjGS{)pQo2zM zFB-8a7)9JvsYQ!fI@l=(YBsyiCM3Yk*`TMAgM$^HK&6;?S0v|;AGX!#*Vc@~+l-u& zl_bVq!-|MP%O$qtO_e$sxf@Ih&QdHHlKW*Y>2vf#4vX>bp$rnY@JR;&x2rUtD^xO$ zzTzNyB*?4Tx1yrxW#ARoWh01w)Oh}|=558x^p{%-;miamE740X3w2p`2dE-aT^5j9 zn(c)XRK9@Pj9L%MtRFm6&9e1MEKAtxrLGJT)%u)}lYxVNvYpQ$_BQOykTI9ZG(=@9 zgDv*(R22*y9fHwAT^mK^QnVX!_2uJHj|$ z@P^n@(~=AE4oof!{d7ECV;a?Xuw!<5lLflNWH_{zG<|i*5p0D*X{wV4jUVIWLYg8x zNtg=N`~D~u@t|@aP89KKnDJCiUmT+1`!M9|G9J7K@%3Oo;y{{yXAx;2Es!bN5h1Ze zE*#C;Z2qe0{vk&z(k+A{7P=F1OT-I|nCG7yO}f;jPsAxG0} zvDrG6YPCU9JuL2Sl_LQ{5YTKUhoC({PNmv#nXE8%+oUF=QH}VZ28<D?W}_v8!@L+8YJ|=GV5$O+ zqaaT$DRi_q%pACb8ly#PK8=I8W3(W&Jz2~j5lc$BT%{d-5h0pQA5lp-T{Bk?WpI{! z4$Wj+7@DPR{+5>(8z>U!Q?alko6g5Q-8w@%J229UP7EXSa5ZqHw9UriR=y*+1K<{C z*WPM?XZmIE96?DGf_OFBElKVKW|UDiz;;2}N6tTh{MkCZF~d>I~G zz;1yg;~7h=)rPHzI5WJV%tmUsgQ+$$Fq!9Z4~-&4x}8YjJ=R))@>y6y3&lDJkzmcC zGhZ5_az`Y@o6gvh-gYiq%9?xLY^5h5WRsUMu3PAUui2~>3zs_DO;gKMdJ?XfCA!zc zCBK~tmr7+nVat+I)eUe~3QjseW{TcAIr-*c&C>};iG%AJRHtEgDFHX-MJ#Hduy7gf zT3jK~Th8F^8YTf5!?y}t+}8KWQZwLA_(B1fV#8fYa~s?-me_REmm^X!f19)mH7b*w zJZC7w=Tk68LBJ|XBIKy&L#Ur*>-K1&oH4sA=|WDHFgWB=us%9A44=ZeZA~gxt#vFh z8$|Y`D5X?0<|c^7%*K;Vhl3P|hA$;5bc7op_BrQ*EkY`jX*y$N6fSxL5?6{^a93b zUQ+0?;F(UbhzdTl)1D-_BzSER!0hH;sTp>gbM<_U?#ko-Au1glz9ENHB~qxFDK#V( z=@){OY_40)0UOfpR`9?8n7DF{_((1BXeZ0%p z$m6hs0plMS<4z%Kncg#$!O=36ZrVY>YA?aWh_ckhh?dP=L*3azs2a^#6{Qa@ySo+J zmwRq*hM zQo4gg)14;bj>l|mpJieg|Bo$}dsL*r#@j9%+^7Z6q>HA=VlCE8+HAbn-^^fGARMtf zaBlpCp^R)dhUa*{1B?ElWhz1gb)aw{p}y&$1fD8j;L!!b5uwuU67-e###VRGm*uo;x;wb)+N+7beuf)tC%g5pR< zY7Qi11sC9nMwd$AN`%2|)83(sSfK}_HFLjEMypjwQersSq%9%RS;}{u=1ixP2%=S{z@S~b6I}=mWe_}0b=aV_ zTdfgTlCYMl4r>7M*kh=}%RmGP40BD8s^Orx01r60K#lj{9tnlTFayG5bvcp{EJ3pu z^1IWDC?zP)!8heJ7tET5PYBr%&%3%US5yMW_gucwv_!&ntf)jGCXV^4HO32-s`j3x zoUhLQPOG`6;+67b2hBQaOidI-r>&gu2JMNcrS1lSxdLX%QcZ?qwdm}-f%mI3n%}S< zaT0}lxr&SA2)c~-oL!EKB5+4wa)`_&Nx0r}ks)w&fTOcF&duC9<{JBlapU#IlyN&) zf7_>DnEsdP>*szjotl5ZAj~;tuUj0O-adcQ)MHb(P6<0KI z;`UuPnVdXv;?ao@PSodLG$9kV@#p8$GmkEOarTAr&(Ddb=J>Va3FE&Q>|>9N{lm5| zj-N31p>5f1JB)u={L$=*^KV_eZBa7Zy69Bp8y++LUj0LT;z42q&X)M(9KnATYe2)S&*?8Y70VR7vQfNT*Z^;%&BH&SuH9HwYSv1J7WK zE-&v(r;E1Eka2ucWAuo2#6s1Yo&cDH1U{MW#u2JkgZw_);dVM0Z^71eGXZO%s!9C# z8e=FP;Bc;J_1gt57id>mhzomjW;u|PUG*|o!CVolkQZ!0t&WdrjP7(TZ_oSt9ELfY zy>2-QSF1ZI za{|Otw6;t3m@{pOS24sw`aBJ)oXmBT?X{5;)fmkJo+&otA&wI3P(AMR)u?=@XO1^J z-W=CqtA2AeX|@Z^vZjwy=^-OfHV|i736#UBQdktIt~qSC+RKF?Qpwhny-t#1a!kQp z^z^k>o&3%kV}{K-al+pTqX{>N>B-80u#>a7E74j!45Kbrj7<5vkjuR$@f~Z70bFKK zySa!65!x*&VFaUM;OQ0wf#_|89^I-zHjh=R#n$xkHjS~L3I}Qxk@ApX+v&CYn)I6hs%K?=&K|uL+(!2UWz2Ty(F25GFLg7 zGdKGwc5RS9p-B`ObJ!dP8$yCBnvQ_?WaVTl zeS9e-R9!7A>w(+Z1aGg0f!h{wn*%%wvIErNOq?PTW9@gqNCd>|3*}*NlWu`TC5&5>IRD>T+q9hRoDwcO+A)-b zZ7G`>XHg|tmB@e_ZWY^smb-=k@4?*Btm^!on#9rLt`56#{>>U=1iYqY$&3qR2xxYb zyw!>~Lg4AWU+$IjHb%*WN+7PHSnOIg{h#k>j42tzl};L18neJQnmC){8jz<~>bi1u z(gml{W(Y+hy;xSWs`IQ?M;U||sFMzo&m^Fdoc7{W0PiTpQkWroL@#Rg;(jPn=~IlR zRC7&DA`2(d#i$aoI`ZXe46k)dN)pf2QORm=iS4#m!DWlvEFf;paLmOuMtd;ZuL`Y7 z+|ml$;&NV;i-^N-Wit`BBW1J2RNG5h>pdl?wd&mM8q``Xm32DVO4FH(xm%HjCm%?* ziM+iY&06!|$u?&1dXwcs7uSaBsduaesxSb8)#PgUF_e6r$y(-33)(2iWo$%p8krK1aD5@R-wf zS0LM9iy+KnsMV_3%v7Pmn0=5JL?(#!DLV=kdCgQUJf%UEyQI}eE{ncmq2-WzO~I26 zN1M?mErVT)9EgA5%R?)bLKjR{1Bu3GG^nv&Eark-v{145LA$$c89$Sy%W0%TaNV4e z=%)RYGfs87+K@8_HO4|#Oi}Kd5Oy?dnM{LV8udmi7R4e=JYI-(0|*U65u(Pp*0jT+ zK@}C3%UmESSV=O~5@i9&9mqniiR9WThc(epIc!qJ;qTczYXjg%8e^|lOgYdLR17s5 zIiIbc2_w;9nqqt|wg_b-0Vd`WGX0k9&<23<7!7K$Q|&W>M5hcnLn(l^nkjqjJvM>& z&E|fV2hUkZ8wR?vrq$8#u*O))6L!gCc86*%s8>oAbHFb#+k;}F7ina8(I*wWELhGt z_Ax%BL6!O`aNrg1gXMA@Y(Z4fNG<5XD`9Y$6LhpuIzq%zha*FTwMot(X%c-NmorF) zYAEdWhk*NxR8=Y7dk-QFbX87v=8wA%t6T!Z>MjWH1Id7SB5Hx|i(tXcLPL?qg@ z)5-N)4Zi1*8F1*8jD`G^s9Au;+s8D{csT{RLoJExU}E0OH`8T1h&G@!mN9QL1qIq6 zX95>;S&yKphw&F0qg6?uAddy%=KU-TQe?#_$%ZyUJq%7boQ*=vEt9TRS}yz6tks%F zW=RtrmV-)Zx2u{=7D|*gO_Xd1oZ{+WsV=%)!FVKYWx8Q)1Q=cmW*6|D{V(E2Yn*VM z4OA_7&YYmru}oJ=_xrdUR(yd3A5MWxB~ec%CXpEsDpxZB|2$+2DN(0~wv|C>RL)KF zeBEaExGBimkb^`iRBtj)OZNt|(P0IlM?c z+KD&&X-~9hYe2MSbjE(EF*1o9IHIqFT4E-hbuwI%tzgY8S94Ue3~jZ>S+YQJIqusXk(lGyxAeKaWmzXO9Tu;uOy=#58u#SK*M(R{{PhY#Mt~_8z!f& z2fzG3_jAqk>Iv$bqqm+guIH}p)^~6=yb3(z>vLqYM}Ru6-3%V}>9|8Spx5!ve)C6w z85?Eo^&Hhew}e(>VWB2UseUP$qHPYWW(5I$Jn4M0*=1H)(DBF4tNCkr+tcqB9Id~)T6+L#e&nKV;PcLS*+gc^~-_4vaJ>( z?a*>s5Rzyt(;)d~u^C1W`+Y8mLWL%TstjXgM=o0q_@gUgPZ2tbpX(7=FTHFFW(6Ty zL1-(NkGq5AsFZgt=de0m2{^qd2s0*HX3d~tH%!> z|0zHbM;~1;pCUT`A{9}`ZNkkIasL&_Ka&FP0|1`9?`kMzo*BNUTYDF;hG;nW?d{9; z1Q0mHu&UXcKE@RiWx*d4JC;l(lyq3L4WGa2^trq?OFxH1`qg-ZvX#+DU+f)z5QwHb z#RbVGWr2epdp*S^Vjyi|)#?n=>0HMXlyfMYwMqU^w^d}201CE8X}Sdia~tVqkYX$w zs|PTys?Zs5|D;TK$VANN_Kfx}Uah+NE`DzHxWNRtb6n5W)R#~1?0=DZr)P%VEP%y< zS9`(8epG2h4(MY=b|cHri3ADq&M?EEK(%&&wCN6>t={ z93v9h5L9;uLJ5WvS&%TIf+i&)ox#Y;;m?AFm3T5L+0zadwMJa^7TtonSd%EHS$U;` zhn)EuVoNvMey2CgQx;!{16!TWIGd#QGr;WDFwDEu4z0~WU?vLr9`jF?Xzk~i~QY}TMB^jvJ?p>qa z#RkMyi$#6@KHx1;bcIm(7WlIoE`n~;LTgx{b~9XkUje?ftKV*N_kLL1{_i&Rbb-MA z6yUsj{WF6NWe-C-4gRQN9OBskayTvT2R^`7CWN)3IELibeg_UVL8`SI5HUiVX{ps2 zG{D;A4+BAeLUUt79fynn#X;F++AS=jRMXKDBOqHG)ukg8MBDo*XSpj`QJk)1`mO*I zJZz^MwxN*%8Ic??WskXds+?)ND(-}A4W!*ZAr+6bX`aEPM#{^%AV_k@L-|HOQAySk zc#EW4b)kl%^h(Z!*zp`(OB4|?t&C`H)T(Rm|IZ!!#Mt6<3xBZ8Fb4JimK> z*W8!p8gnNYKVzhgriK4reD@-@?bdBE!(E1=VSM&Ov+ZooBD z=S}|Gxu;Z;Sx8mE)e8 zx%-}Ne|+k#D>oruJiUMHr#|!Dv%g&U;*QRH|NYn8F%$Ue@}S^u)7=fk-QXq;?*Z3q zk(%fU1)LmKCH&qLgOy|&^(4E^j;Ej<3qAPW<|&UA%pb;{`_c1Pzv)Y-{l~-=fB#p$ z(?5I1&nK~yKE7}TzIx7}V3Y@8k?VGl!_O(fNHgLByO_~v-j)FOqB1oCiTAmJzujyV zz?t4)EB=|>`*ETB-4gfcz1NR@G9jbdrt-Gp{l7MG`Aaz0HSjw=;XQu& zsiV$%r+C%JFL;0B?f<^*t}o-OXAKH&+sxT0c%fqn7WqcZg*3n|q(sAO4g14FqTVd< zwggR3Xs-Zko%h+B_FBc-H?F2X@Qv4e@b2GU@YnXU%C}y?uHJIhBg~@^*~M4S92D%Z zc_cp`FZV$k!!>8JUoc0zQVnX$2myjCXFN`XvceE2h}wDPPv7*@?df+t^kM9g(-IHe z^wtOOX@BNx|M|&dzP;^{?|=8Q54`6_eDzg>f~_$h%_JE#9FUPhM@*IMa?zeNE0_hU z$x@A=(rmlm#1nl=EBN7WzW!Rnr_b8`8glNaKfY`G6WcyIAN~6~?wP7B>euIw zt32||eN0JQ{{3^IcI}71zjH6yfxj2N5MNyy6l}IKbRJ|?tl~u7R>NzF1ZEeCnYxE8 znPXK&43mhTY_!0&0_`m6`TTocpMU-e{DY4lN4)kI|Ma(ef4uPZ7aj8-I{Rx6O7G%+ zmcv)i7!)k$%?xjDF>EoB!%I8^J4tK05R8K>pB=?)aS2_@kJh+cpU`%1?|9?;Zux2X zhPQnu_q+0=-?ay?T)g>I%Y8o^`@0i-&ul;bq1*A*U4w!HjWBpqfkYxMq7hD(N^s0m zr-C^v;tnz{s9+)RIN|fF*tH$ZQ}^8c%#B|;cjm)qZ~No-zVYMMo9}+==v|W)8^{_aO_{tVP~yl>}ko(&!Ks?Xr7rwdpuH0ihA=UZOAF}7z@zv7?1?z}IGA#I{t6nX={l@ZTi#G2w z-+S^#Dm?M8r`d0P=i7eu;tdzPz}|B@zIy7Q;AC3eACE(UuDOKtTTD`{*h!xf^^|Qv zH{nHEq%8mv+51~@?Igr?4*eh4Cr>-=vp;`L*?rCL8W+8F;WLKmzkki$Uz@$+8@^9G zg|F@y6s#k{#}M)TpT4U4m4AK1yYIZ{i~n}%&9)ER_HR$V?H?Y8@Aduh*!#Zpu>H6A z>M4VQGgt?$5UDC6w-M4=;CxPh9SbsOhx8$#3IF6kA+RHxhhNr*s zy!@S|6TbOD!&^Rm|L4B(+!q49(>mohOn>J$`0B}ng1vSMLN5EhZLycN%K zoh-qm$#x0~g@Z~W4sr;@v~!PRZh7OxDYx!9%M!om`P+j^HfTwHXpi)4d+B}Wz4m9{ zyZIaV>Pdrwbp*c{HpQn-dEYf>JzV|x>-hXTqmS>n?V+8a7cO{Hcb>cWn;)fryd#dU zZXXn^BVxs{;M-?De0*T9^{3xI>#}=p`_X@#$nJXA2TpzR*Ka%at}CDY>0=+h0bf0F zP_T~2aKnOcJ}z8au_zryagh9bN;#Uj{ zj(qrm=e-yG_xGOO)4KZ7)%?xh%RkY(`|8WS`On7uu6v&P?OuHK_+i1L?-33Qj$fmE z<-GTvcG?SX`qr&iU-!+gKYP^f8?U_PBIX0OTihS1U-njfb#_p&jtCaRf`9Qb>X*Ln zV^d+n^QPLcib6s!w>KCIY-*M(wKX%HO-t*AI51(@K#1DV{mUmd5e%<|#J;41E zU!5EjtRo2F5b-PSKc0;plL+sA`d{yVF!Y7ptKU$bt?fE%$A`D)KlVf7p56HB#Gqgu zaTA6Go21L{c-8Md^}D}&?8^5)kDm2kZ~fjqKmPnjPP%-0{%hz8^yNqK)$u{WI-VB~ z3%+>in`eIVJA1DCms?rS6Jjm>)Okm5Q-+MU1b2F&u4@MZlyu6vo&9=iUXFYUdf@j!U_ zd!LQIF#Q;Qi!vxU600ThBuCfU$xfgUcc86KAQeW$4v42n+v8RY&h=svS9IjGGuU(g zJ$}iTPQChB%Z{t=edc?!o{#?5=f8W$?zi1NuiWYQr|nNN_${qL!DJo0XLB`}KrYY) zcQuRPnUW=!_H&t7uwSwS>XuH!l5YpnsGu2osdDSP=gK>XU*3HBJHL2-tFZF}k#E6& zbp80-bFJ6^@>|bz@LS|T!7dlu^T{~Y$(Z|8H=DDCsi;(_hrCpTVc0~v9?oSXz8T4( znqM(yc;@&A-1lF2SLe9nAGIb@y=6@{qnC8|M)ZE zH1grc@462=PB{IQ`SZSz#cyd23g!}aD%Nz!vK+O6RU+Ray78O>ehU-nY*=X#NK?tT z63IYCa|&;M@e;>JKXT_6&>w#OiMu}%D?a_&b3eEzaog?Oy&rx3x4*IDkNE0QgMw)f z(lQG!XBD{H(Fo#BN=>H6L-}5dbj3Z9Qi%*UyTzPr1NZg@ONH(QKl^>-{Hv~d((;-6 z-S@ul%a`2p#Qs`Ewq4`$rxJ1s^jgSjU#lu;9PK%a`lK+WEZhp<= zaXY+sHpxfd`_8+e%Px8H?||kVJ#6DL2n?Gn$=+zORe;J}k?)g;0G@MGv22iVx%=L3 zh(vR7PepTB9{A)+cH2|0c(6`Ze)+zOzxVXzHwC|)+46r6@AwX#m1(T^|(R7I?iNcf{Mjy0bs= z#wVYHq#u5|Req{6g|8kvC|Jji;IQBwuli*3v_}_y{;s2Do_?zGNav=R?Z^JIbv^XC zldrR+p8ea#-0Zfo)YzvdXC9xpZQ}9?kdEH)enWfww}znMl-Zwe`}VdEZtHD}YP z1bkreos0b9g^NcoJh5=k!u1QUU$8DrnZ7%YOY;}lTS?EGk*MdbNckDUyVIEdHwu*=9}}L`4i^; zd+wj-Zl2yf9h<&&?9=1#nSA~DS7tvyN6jH~bH?u*KW@Crh@U&19G{rjndL!dVD)GJ z{}Y9XKi>)pVNZrl^*3gApEBKLx@(O%CkO zvyD-S4YT5`I4be%tS~E#N<1@LpRH@e*{zy+!sIkLwZ_UEWRAPh4Wpa&5JZpNL>2;$L&zLSWT{tT7wCMuV z1)~yAna($zKPvI0={2U;j7n@W`Aoi1iRVpTlXstTZ`I!uraMe`tc|*@h)o|ieS8hZ zRD;+>0kj#~26;vI{3j7mJY`0>THrrXkbP0#YPyx|*Th2iA#GJlQ=*|2!`;yz2T zRm;sTeqwQCvu`kc%JeA>#+GJ(YW&3U6GtVU96w?Fgi(nn!0f+f7q{$_`B`aJ+E?-x z)6d*&bG8Y(4J3y9bN^_NvptY(F<aEw@!` z%zS_5`)dV9ec8BBNVPlRPCCIA0`3h)e`e-QGjAG|czWiIGjAM~cxvYInaf8do}9UC z=CV;}J1X&<@fhPVqY@j9M;nhGmDpfB%6QbM z#Iwd%7+*0e@r-esaoecG)5b;P;;6(^#s%ZTsKk>-lhHIP@q}^SIKS50TlJuE&N!zP zyydJmKYrTyX`>R)jh{Mx>ZruV@g3tkMkO|kpE7>RsKm45Cy$>zD)G$tN#iGtN<2Nj zeSG_o_5TL*cx3%Af;gifE;H9)ab*3!t}%|R|JUouk@f%j);O~M-?#$R zCgto_OZp@0|MlbO$ohYs!9BA6U)LB%*8l7EIBQ0>G1&*}9Us4NbEBY z6FMrf(PS}MMkO|wR!l3S63?2>Go3do@r=oAGLK3;Z93OkX;F*| zl^j=)l2mu&<+FKX#26Wsc+MC$hDRkf8bij=sKf?i&=?$*c-9y&uHDY+Z&kq=qu;o8 z*>FqZX`|1$b{}a=;whuo=p7aFq|swsyKA^*$rDD*h-s&DTM`@H#;-e7pcsKnETlA$yz@s#29hS!fu zJZZSZaLK5|6NZZo7q9inmQ8n&;i9#Io5@hcho$(m*OLhI$wY5MpG=z0F`Y9i@r3DY z)7fh%wkp|lmg%gug2hbIRr7c19#7Vbg+W@I4JZ~2R~fDvmDpss(s1SI#8()u7?pV5 z@Fv6B&58b2nCA^|G_2j6*phh8aJgaa=EMlZml@V>PHahRG+b)9bkqtO4Z96%S0}bC z*bm zbdRk6*B?3@S^uwZjU(&-b*FPj*8l5=#*y{^{zL2|>;Ls5W6p47{l9-~%o>iY|M$0s z{`J4{9FYI-i<9HeOx!#1mYIiV-Z#@Qlns{I7iTV*F-`yH^oOR~)1K+0L4?1LPF*?` znmTdv$CIBJ`{Fisv0-{_@qNaNjbAW*)O6|OsgqYuCMKSmzj8jY4c#^~f7;yB;6?xS zbA_?r8E+iFY5bD$<%uQZbBh-&n#S*+eQ5UG^FN+tXVKXi!*2}_8um`$2TD*&n|;wx zHeNh;<_Lb?An{?T7OGb>B1>T%p+h84j~}F2@f4b+uC2*Ld{rzH>@*n=K-nQ1KRe`T zafuFBCSg0tQ#QN5OoAW+M8{spc*{b)E z=-HEAq>(68y?$@J5$ksx-mVJ~6WnxPH%Cu5N7ph*k1r8W4!2-shO!3544G?X!#skN zAcgNGUA)+#Sy$FiP|l>22@Z31mU4tY?MFbqAGqe0E7=HP?OG#2CmXNiSfucJeX*p*?qw8fa3QaoBd41~ zQx7_u>5I)qA;Cp#W;b4jaUUFR7n!8l*RG~>Aahbx3RW{dvAUff%Fw~V>E>WHjzk6G zy`>T&vUyWlN?Vogc>gIU&kb~?a>6Fh(do55lNRbw-*l)!1 zEkO|#vC*;w`{iN?0r4yG&MS87=FkkB4jGz(Bkhe!1hT~y3JoK*5Zh0L1s9Lv*`_;! zqO{Pg%l-mGr);im+Gy9oq4l5+j$sdi4C{`p&5t{Ilh2ap}NFrTlye#_rUfAAH0!>!1 z^3`4g!DTbtkcoo4{cE~89@5S6Rjs9ZQA;ll5}-NS{Xn1T@?@mi!^$;V$0NlPq)bTd zZlFWhvFzNUZjJ@r946q098Nao635#>I=u*7?$bVAuEJu2;Sv(*^v5`l*=kOw8?^Z%iTCUl`M6}Qkxj~e;5=M(qArF(Z zD?ytbJ}?K_^fJJD8HS!uNr#Vd(XiS&IEtnd534f>7!`VPz7!%OR3Z>#rC7j@WE)<8 zItMw?T*~i{q45SLI#}C@dlrr!qek$%|3@Uc2d*}!=@{zo8p>ecq_^ks(s(0c&iE2B zZ@Yo8R&&=;VX06RD!46DslucJeb=O6@!_%G|BtHm7t1_oD?NVxa4n&S!=}4GW;Kpf zp~fbi2bEM(=ynh&m@h3iVUbSHQ%#v25Y>;_V_lk#D3jBUGd z+xErl7M@?=7iLTy)BOD3&#%mVWsWv}$LKfQV~EUte^#CypLy%_AEt$A!&Gl-Ve&nb zwu#SAM8`ifo*(-XDB|EBW-mH3k%SlMxs!G_S%yu5MBN=3OJwkBI8onl$!C)c7E5J2 zp>Wl#gvVtWpnU~OW+ea2wr|V@} zIz)aBt8D|qC66ER3ypr##s|Z#ST>r-hwO^N#X-1JJMU`4j&#}E2T41Mc9GocP|<*@ zpExoaQlg0#z2r|iL;`Bj3Jk^C9>tC(l_)1-2|R%nQyw_p4fv!~(-yHxD5$hutMmyw zIhM`JQL!U4Vmg@$hUn@B{8|gYbfv8un3jlOqLlN*vQ)E9HQl_VAS_=?*23v{#+LI% zd<00F?6OAk3?xc>Z2-;LXVY`XkBnxJfrys*1ELNrd3x<=x|0pDvg9L>s4wbjK^><; zaA7~%L7G*z&NGE%+({R_;$9dCy%4nYaU%;oI2Fp0lk~@ZNh?aX0wI46_0>wPKosrf z9au*Sz-DJYmMO>}Cch;WtnP(?#1$a%*pVgf%W@_!O$CdNrSeU~buA(#R<@Z{FBqxw`%*c{9Bym|{Q!t9SsZxs;vvjai4%BRRpG`=R zLNw^9@jtKeEJi=0iu~6n^hnK$2=c z=0=vZ(R}Dgr^4(#6NppIhjCam+Icr zfkf3>OpGjXowd-B?1kBT8jz%_@A$})4p3hm`Cq^QJrxL3)pu-UVK1#Oj*a@FBWnz~ zcZZ5*fj0heWHj0U)saF5RQi-*rHwDXOj?N?P-)1yq$BYRfOE12=MR4>IC!lV;FxeD zUGCJJT$-~)xP*sIwEATnrdyewC7cL^TW~HfsSe>u8k`sYRB&`;&yn_S2WWsZVtjsN zG@CmkIx_2sdrt&Hfq60heq^DWm=_%>chvR&uCY&zExvVe*TTIEA=7`E{%ZbL^ViPr zoV#N#Zv3g?7vSy9$=hz;wqy3X*;mcHeP((33)AVTUryaH`Mj|>$xe<kfIG7_(J}+%)Hu|`#xozxu9l&$;*<(vOe9w{bY&x*&doBbb zFI*T~(&2oLEb`!CMen%)$XfyObhw}+%iBL^v3t%3a-2Yp4&QTRIUD@j>YmpEVK5M; z!}%Op*!t-a4B6KJQRg0r4~B#I92F0!x{hq_BUgRRx}FVWsMb|S62+Y% z!4=NOCBDbVzHFl!N^}lb)>tgs4#m*Ao&_YR)>TI~#gQd!W?jz&@>DI*kz{dXdHc1X zy606ujyf=P2PV9sXNLoGNyP)It|NKl$avNU<{3bS zIxuylTilsXxxiglvFNV(=|DKl68;VF4;YxcfCP15>d3n|vV_eB=1w3_9hf@OF^(*6 z{lGjO$WaHTj_iyh%h@n6PXnUVfvF>3y-4>PZGVd4=`nGsgAonQh(DyLX97 zi)UzQ*Q-yj5WGyEd-^V!7O0Xal*)9C;43Au0>0a|{=^${?o!EG-~vFsBuW%5F+vp( z!B)s}+3sD0(yEo{HZ8Pd@W+dG>G56Eg7r-J19}|}->yr508$iM0384az_y7NA(a$~ z2mcimo(JDkjNIUfo;rVnKkgy~Q7}UmtJ7++KqV_`MJyJx!-A-?hgvGvn1)KJYT>TS z)Dq}Ms|Nn!1i#k^xkjrkv3u7r$)!q}D5QHucTwo!ZHTBSj7RP@@+9u{`dllq<&~nP zUd*HdemPgQ3zpAZ7P1aC_Qrw>n=NLcaye&z@i%=@X*L(<@T?xkm)|kKL7gA+wupXB&nw6q2FU$`8d>+L)^>)w^yNmPs{SQIr%5Qa1#5+KpsfWE91n z5gTN#nR4_ZG?80jg6Tv#Ul#l|oN&AIJYr?QKt#QHf8Lq(c*Er+mfUrjI#v}y1}#FX zJqgsIsy3R{VpDsV23kPUa%rvKcP;4|HkVyCd4qj*`z)O#Ry*T*ShpTeyLYujjuwXF z&psI9o0%i4^Z)DS2sHDOj!eY+b#c9Y)iK=<3OJ-{{0Ve=<5r%kf$Qz>fd-Tj+6*u; zfJTQ6KFEZxx4tN$G7}1AgRW`T7z-|CBFJXsKr`?JGl1=A=Hk%cj))mFCND-#& za&*b~N;=BdiAqOef=LBKlsYfv?KVmrSPWHN^%^ialW<3ac+0h%&)C}5zSzqZmfcu2 z6}8smXs(s+#90wMqAMWnT*%puWRsLLWhr=ieWF+Q^?I4O6Q`AB4z9Am9>Wf*9TBk3 z=mv~{b15lh^LcARo$5Afy<|M~AQXq1q7SMaXp=+Rfq^3hC`Pjab``~@R^V`2{Sq*7U32W}iR#4lR{QwN zyOn93T zEf4kGrp`Qy+6I2vrjrPj2-)C5ve1fj!HZwWUVwNPuSBaMn+vU!n^XZutt(7A;is~P zJ&80LNz_RxJt;@Z#hTKm>{+Sn%~#vyO4DAfd;O7KH0!VMM5Nhii*7hcB^&l~6N{wz zdb80NFoz(oKs*$8tT>Xq(n}x^rcNRw7;&}gKrhwx|J3-iV+%{h`=&1fza07b|9uNw zO|G5_Z1IKHE8{Qkks(-*Z?=VD+syRpI73v;m!5;S0lkic!_C(KdH`Mm5N_~$_Bjjk zM(exfZB>v?wLIi`q^=Y;HLYH2s9rDo z<%lIE%099Jf&2HF5Q7o*n6EE%5`J%P#U9ATmm&GEO{=xn#r$mxcwNy5^^!9kuw7%uy)g7vu-BcZac~8s_p=R)AYNq47TJ3E;=k1%Tjb6Rmpf>Bz15`lk z-Io6)#Zkb%-*9*FP_Fkus;6%7SO$}>%3Gqq8!&}ew?oZV3#i1tt%cpY)*TpuukGr$ z+Mf=n`Jll4-yOVf514_-{S+IQSqGLqbma#ERg^tLz{4Oj5{p%1W?33ZEf@GHlV z+}iIFO%bG8zri=!9>c3oA2h+5eStxle?t2LLykk*mmnmwD}B1&bkI4x-3vt_M!_8I zz_LFRmW4=w0qp@3)iP9f9kzW5G!S3dMt zX}mYCM{T#hmU0OD0{u_zi#kgVM#MgS07n^m4Adsm?EXdbpTSZbxYed_iS1CW+ontG zH3fp!nrq+Yidz4XHrfs{P=N7pCvwQ{R8@J?i9Arrc5JMtQceZIyGd7&7D7I(*IKSn zmR8#vAt)B{p;i(0w7rL|XsHTeUBRj?3nFJ7MJ!JhI93klNhjp2vczpt#Y%Q=Q3ps=)GzPH}2FQxY6ZQs}2zPa{dQq-34ySx5Sz@4d5u|SGRTe-Sa zmuixj3~}Jt2NZHRo5<0I6oOk`JY`Kf8=2*F$4g}_awRP|xn%)w;X)2(9IhgpW9$@Z zb+o8(x!XrjSk5iCAxjr>FFW{NJ7IID8c26#IUPn)InN+r=e+XH3b*Ah^tJcwr0&#E=nmgwdJA|G(u9Qlv+|-YKQJNzN`jA<0={% zvl*}l+F-Da4Yu*vKH~*E7Da?FQhE`R;8{~%BDQPIaHrh z=ib}#{r?y7#~%?H@y8#b#IT4JS|x8+5d0;dkC8_-0S|m_yiy&>p4RnX4WX;z4Z-i} zdC0=`5^}xNN}$UvU{0%ko45lzCliw;P3`Cw&7RuM!-s_w^CO zMO%We6u925Vj3H_|tV zZl#Bo!rq*_i3pIM?6!4eF6nw1;R;w)uH-c2291Fjd z`3{*>ed2SS@zbY1_oUU!y*4+;=e*yOK4;^C-|c+vHrvSG84tV-5wJr5f_cHl@;Seo z%tRL~A-^5J^Fe{Re#iE&XUYS$372l%fN4GI8wHtgwZ>z#mP)4~9z-a6l60k!Fyu!) z*OfSk&DA2&vLO4rTt1Nz^Cew{ykxx845L)?dc8$AQ|0SE#f!&uchVy$9$tgPo)MHP z$Sow3tJPT*&l$>%paHky+zq6&Je96i0zI=z-{_Hls_3K+N z-2C+3H#U)tPwXlCf8+dvjpwa@c>QSY{cCqR#=Bp2Jbv}T{o3l@${Vk|`rw~{fJ^v! zjbl?)6WflX5mX-#DG4V^S~EcvWVKru#h}_i;v3-1PHHqk6ra+qLb}g5igznbtl5i& zkU~2S(lwIgh zn^j}ONLhyZnNTj1_0?0^SPw(Qp-W8Y^`Ms5inuRRYq#C*B{;+%;V>MP#Y9hNqY71Y zM?*c;j|beXexTSwid+FLs&KmLOENhvNgtt_KLv8(;Vw5-4^|>QSV4$f2#<9NVa?NT z#juD32JV!%-|8Pmy2hZT5}{66$;E3@OS86vBXNcU-SfpsI2g~eS=5WSlVxJ$g9%3K zBB>!V67UMw()1h%ZagB{QZtCUQ;J{g(1DH~0V&t1Ru`?yB5HvAA1v1F*8Ij$N+5^* zdb=DSrK>axQaEyrNK~-cV&(U4ap=hmNx+55D9}pzr8=4JB}G>%rz0;5W3K&K#ug^U)7i*m8kRODf|(n4aL{-KBxcnOb)Eq##A z4AELjvT#^^{VfjNp&SyU)nF==ZV4R`t9n$uCk(rsl21jw{fMtm5K31HH>*eOObtO? znh_&~B4uPn*eJ#`awpyGGF=y6S0X`KhzukYN(qM{j%+jqBEggSQAKBZ<+6pt#*5By zzzk05uyoI_aZy66)Z#|AS|n9)&K(-!y_qA(o5F6hL zX8oaPi7aDv!h65w>--S*~W zUp?Zh`-pTZaabuDnn-c@pxSEzKORV!g~Q4xZgFTd<^IqU$gm~v$`7o)2oQ0KMqboA{kPcUJtcAnsUAH*Y z(qWn4#mKM*%h`bs(^a)hdq(UaMo8T{g-5&Xc4mmleBp@XI*nqs9}M@~N|vCa23F1l zN=URs_4uM+4oct?Pb}5b-e&)>kP;G<-lUCG2t!3a7H(KMuxB_RAOUeu%*O%{;vaEb zDbOz`Gl^6pUu77miDe@?QSW)uZR3b7`ilJ))@Y7&mnVq0lcmH+E0-fQMiOd(Sn^OI5vj#05=E(8%Vl;n5)(q zju{j~T8-(EvQ~{)IBdP}7Kd`VCS_XSNSe^nt)viZ(w$^EDRot(=L@mY(AOTQ@lFt{ z6ps3Z=#WO*@l*zUSmU{3cp!sZn02-=aA!K1h?}Ho*T_Yb3<*8VW%_JX#?uk-NO-i8 z;3R8**leERK>J}78`49N&wkj=WO_+(->n1J;zG_l^Pf{9blIYY=XrM@^pR;xdyfK;Af7?87m*2?iyci(&p& zvVemrx?HBw43TIYHOj@L$LsC~FfS)ySgqABx>7Mz6U(wviV;flyZs9BZzhj<-W zXV6wDb-?MBl-O)pY_a*FTO8DY%O|wU>9j5;(YmgI>r+iPjG``(Ot>~kvr(@>F(?*k zx{qQ+n8o`+xTU0#a-}L3O7S5?a@l$q3XrLq5pnyJp;pi1?!ckOfyAy&dZgePT+~BE zQL%8?M{jYc(=4s0z~ECYw1^VKGor$%DlcoIR^j?;Gw-W#?1)n%>`|@>NuEqSruxbi z1f{4-9Bc%OQLd1VdZ=KP>AT7eDeX#zlkB08Y%&?9ixtH}F)Z>Jm9lbp#w`x2D$9*x zluF6nde7CA<#tdpm;e>>LZJwp$csFw_>gva*f{Frkw~?dPq$!(jQTV@!!c>f3*K%j zz*0!;>Wy43FeoL-L|8b?)roerlco(ZN)SeSG~=6lU%bU(=pMw;iddmJ!JR83aZk~J z5a9Jy*n2iROS`1O^R^ z$flC?Ad{_1Ao({_EFzi*>abKq=vUNuR1Md1jXD9=Q3fiJ&0b1~6N3yR7UG1pPww7) zgu@6T{A5TU1iPtj#FwUsVyEX5s$4kU;weAlALLXpb@;^$7C5R!n(dOL=L^|U(i;qV zbj-*4-RYzrWuQ?uoaqN(T`ERt*PwBTHqf44@3U1+#u7$8WH1(6?0o$Z4#PScka1P= zOMzkDH;^mYfYxaEv0&WS)m<*GkCbE0A{TD5$Wh*(?g~DMa|g;&fnvN7Z4lS${eQuFpC#)R4xU#akeF>k8-}53Kv4x#)-e!D)NkSYLk+$p8QTHMirp9Ld!` zTxC|iz9O74$7}YE<6G10o-2DVKa7QlFj^_Z(h@h6Xd*>hN6K>-iszgNLe?;mBSTO^ z`C5?z&+DilTYFGdYkp>6^fY|ZL#Jg1y0)*SmrwrP$=(9#6fe{JT0+EHuU2NPz6$Iuj~m+tFi>= z9t~=Ygb3nr7(k#b8i7gytkBvo(dWVk$n*dj~dEhg?RRgP((4f(9??*asR^A)<&#U(IrUpRuW0NYli#X#eCh1}i>qx- zCu8Rs4da*qbf$hATbj=8{no}i8qH%IXr1~kx3t#V_^pkHG_>OoP&xHmc4?Jo{dW16 zH1uN(sG9mMv$QI!-`aReqjHP_eN(@UF0JpJ-`aUgWAFH-K-<)BBTH*r+=0KH$264V zmrUqP{FYvt&Z*zpc}-*Q_{HC=^iu*$gvY?)jWoQlIalRU0TU)h8P8 zP&IXV&(f;Sxx9_5%TDmw{vwC5vi^c~$J*!C9$b^w?s9zH@zE>416~A_uH1F-^@EQd z7zg(ptnPny|84u?{?qpUbnjz(oxK*BL+Wh+FM>mbldp1`$es|;T8|jUwum9=#$JRRz!*P#e zef9IJ$E%su!BWoa`tB*6%%Z-1;~MTSvcnW~1TzfBYV70gFk` z<^vb2!o(Fp2z%R-e&11YD5# zNHpd6rG-Fit42v76pBP*H0AiTZ~bM5tG7nVb8cmgC#Se58V5-W1^ktZ^aZRhorKc7 zoTNB`A|0Pv2(&6pM`Nrk#RNL(_{D`ls~dx(d7h$D5lVLa!a|_6-;g0RmIA5HqEW{u z7Xq#QhDeD5P9-BW>2oG^77UhYMrb~q{;BhaYKiM->(3xU=SC&n3ulW;slIR4E- zptZwsnQ)4cIEqU;erh4m+To%?0*i4d!IF-jTnMywI5w0LDG*M>Mjano1oT;Zjv!!3 zI!=mG%<;j6Kx@yzLt!z&;1VS{KClpI?KyZD#N{P$Da<*3VjA?Z4Y@ZT-zwWAmGv`o>>wRM-Dvy}b5~wUXmc9qQ`W zRtqbC4B+#B?ti$m4kG*QJkxi%@5|l)uwk2kjSa7h_tNfvxNVz&jZNES2(+dIY;5A& zd2DzY0!`ZlF4+VP+XODz1obHa8;2T~Z35jkflD?)%{GBcHbHetz{X+EWt*U4o4_TT zplqAKC7VE-60mXpb=f8;*(PwwCMen_aLFc6rvz*Wy=)UGwh3Ia2?|pJHXdQU+$_2G zKb*Hsz($t5i;s-%e>i8GfQ=)iWeB`-O2Eduh?kpn4d4IpD{K?E z)K?qtfB0V81TOW(%=;gH`ILZ-S9mWs%jNwKzsxp)OLoJIZ336Jk=3SdJB|L zil;D5hPhh7!%lP!S!t>Z$o5t;DZnArkqRSCl+$u63RediqTv+QQBXr}4m8hr&)2XH9T2{uSirH-PtL_=mtij1V0a0-I`&v%3W zpt#zuUA=ylQ#<$EgBrtn;huZKJRiT|em;V%>Ob<;$A1vmBzTcctUG$&RW{)-$tKhw z5vyuizD~C|3IdM;)-ri_VrswhHVG+yk{8lY-GvlkovqYB_8Y02_Tl+vV-$e5Aw;lC+vG8K0-FRbvWa!i^Sj9=?C6qgLV`3d!oWz@xOh=-N}@Z7 zsCxf4HbF_vTPWacN^A#ml5CLusKM2|-cG&7uywAdXaw0z783PLEYT3~q(ntXF-$LQ zlhrQ)oA@uXiFK3qyU8XTen~b7C+P%~@uWu`QE?~vC=9MfdT!?@Slh?req(=(NZLbpJ^M!;rUFb0Wej^%<#4=HEzMNUsCjad(C}0!cMK-bS)PFbG zguSe>D~xgAILIH>J2G2tKn2p<7vRQi{3PTn=8}vTJUEA^+8s|sOr%9LQbi!lTPq^E z#3fZSoeV;;I(TwZ07rdb+aF_vrET(GzXELHy~rlkW0&tLn_Si$k87SJks)}AY$$@a z5`(Zxsz%?&CRjD*(eTtj4X|DRfUosEZMilm=M5+uNoin8(ubWwqt=br>ZOJZ@*E_& z1euI4ZIj>r8n6j+kxi@zQ{PoKxva4ZZ+O_Uw^D z?;@M@W0y8|d80{pGnz}+(jp(p@Rep!6>nz~ybF6X34v=9wGh+~;4Xiz+R)PA=v~2x zQaH&dk*t#OXe zJ0h!}T_pkdJN)y&Lw9aWk_Xzk>U@NQbRPBQD1m^9yaVEr;^~OD+XeHtH1yWcp2wHU z6C?1BQ^wo4`0DWk4?WdnU_e+q=zH!p@!Ki?4A$@X#G5gB%^tN;oHg96w46tAQtoD>W^lMN=^zkw}Kj z>BBAxG3hegII?j+c^L*zKDENI$ENy1q++zmoS!9Gv;+5p-CQ+F1^WJ0siN1)asp&b zl!+r7*OixH@FbIg4XH=c^0%0mVemwgfeoogHtsbq!{7-f0~=D0Y+QC;hQZ@c8DNYj z*7d@nZaQ7AVII1za!Dpsh<8eSpUe!i*<2tL(`&^(ant}QrKom(RIhlVvRB}_UMf`P zg<(>HY9YxB@}q?lgc>&@kh~0o$C(UlNIkM~^LZHtk2M(t6*3!X#uzUhNV{`2Xqd$) zE*>y)jYurj6tkLHQN_znqN13G*SawV`-}H4j>Vf z6KWpe;-H@g9(s()z=qVLgxrQgLdjE2H5;{{nk)Bm6?j-uq^{o^tY!p|lng7E8E>;{ zEW_XjOa?Zj9P*8MUJ_Dlvg>UG%+c1;F0 zvI*FUm|Ug}oF;=o3mb%EScb{f@Sw{3wFVvu2dg2FYLG~lL*Sk40G=4M`Z$;yZ3GN1 z!(hi`U_`&EBu= zy>;*2y_Yy&=-l1;%Fai220PTw6Su#<{mJc{+sW-ia6jPJw%)pR@7D7+|9SIwH-BtX z+w^a4ZhU#;LmS2hw(*$tudaW5{r+`!{m!-jckP$g-n5opyXN>u$8YT&?EaVCkM6!| zH?n(&^Bc}jIbVO}i&x(3C^%rp>gtzP-?v&{eGzzP;6JZC?6}vdul~|^c%$L_>K{!D zxYr%`Ivl??CrZR?J^gn4qzU-J+XK7->$a_wDHieqd7 zes&JPW4)T=c&7>Y#5}-E9Ud|PAD;)X-u-gC!vy@yJb?Aim*d0)d~_bb8X4j^HUU39 z4`AJ@b^NFacz7OQ#?o&$0UwzMupTZt9y9?To(Gt*^xI6pznKS^vGiL_z)#Hqz%z#X z5fku}^8ho3dy5J9&^*A5;ofWlJ~$6BW4IqS0UwwLm@(X&Ou$df1I!rijV9pz^8ho3 zdxHsh-#oyK;a+b7-a8L4W4PCufcMM+Kr=4-S`+Z@d4L&9-!uU~J`XTs=^ruy@0tgg zvGi+9z>m!X%vkydO~7~_V8+r9n1FZA1I$?ZeiQJ}Jiv^l?=u1Km@1I#q*zyv%v4=`itz6p5SJiv^ldnVwm^8hn; z=$e2ZnFpA$bjJj|WgcM04u%PM^E|+erQ2ZjaK~*|5T>Nh%#q}sQT+iE@GElwGgoCD zKVbrXc^+V<3%uV1{L(zYjI+Pb1bliPV5SSa*93fO9$=;myvGFm;yl1iuY9)&_=S0Z znJ)0-Cg79v05hHJT~Awg_#KWPerZn8%XLfYOh?NU_omy6^9E-cy=em8I1ez>=nWI_ zhIxRQMz5QI*DnpwO~C8s0cNaJGXbxi2bgK}stLF`4>03K6%+77^8hp6QZ@mvnFE-~ z^5f7CxGn76!Isdr-3!=BGAO8^68r^7X%7f5ZBd);_fM zB*)J?p0)a!Ret4bK*%yb&0~2}1xaaFj=IBQm=Ahr#NDbEWnb6p!IQ99DdgoUf(gCs zU?jCXHOdp#<)xGM-!^-#ew+p>U$Vb)x>+wnHoAVH2$70bCj?nXDhTc= z8%ygsn^E#LJI4Z0b?x32JI^jGt!iP$)|qQ0xnq991ytL{+YC!{F;kv`3a3shY~#U& zi^DXQNeK&Choy%zhYb(NSq#wT1ReW&ZqCxx7m-0{RIvPyo z*eMws(GQnHW@b04d5i)s)5*N1Rz%&4Kv1yl@wXT>+BaA>jl=~(=CzzZHmb^0uho%} zz|vgi3>Gg1Dkp=5jlhScRo-r}cnQ!q87ypsIV`R3>|pU?pl32z*a&7=TF=a2@gksX zGFaG%8dzGHWw3Z5P&FAWY=j3~eAv8<@60^c0DR{Kr(|qI3M^gkg(KI@TMdBBJ*Q-B z1Pxpq2=J&{CU+d1Z~-0SQq$9%axsU|>~R3-cm~j6BSc~8o-lcKVeWA= z!=E7Qj)dw&4u@#bNDW)fh(y+_=>!?EzH)g~Y(V-5>3NE|hz=4Rx04N_4~JPHPw7Ra z?e{eFw5s%ZoNWvIiDxR-vxHc3FcL!EevXrT{fZZM>FGwo@M^wtDaG)-%%HG0!MS`j zS#Ly6U?4W(gbz`fY_;`4KU51P&mySmdqsiJU-FVUQhR z9ys9X<_V>0Q5Tt<0|RDmJHlW%0GE02EccAC8&hF7>(}y;bSs*$K1h0W_In$F5wACL^1LaN=Pd#L zmi3ST><^N>f?40Dj+|Vd3cGGcm>>2Bq#9SVJ|Y`AdG1u$bL|N8!G1q!MB_C}8%0i@ zGZpq6o5Fl8J}r#u*2dl>e{%O!?A=RLadD~KK|s+sn|Q05Nl&6k3V_pRP0li5Nl)O zjz4+IRP0lh5Nl(Ljz76$D)x>g#M;=7<4>ME75n5R#M;M675k)%KjH0M2;@(m zI2HTECB)j;7UNHzFctfRCB)j;`r=O>KNb7d-u!L9}J5Kz`m8sY(7k^CJxvayV98AR?EFsp$ zZV`X7KNY*bgjgFFVEB{0so1?G#M;>0;ZJs_Vt1DiYhw?GKXFdQI+qY@W6Op=*_n#n zSwgIh%^Ch=dn$JO;tys!8!`OJ)>Q1)5^Qa3sqiP8Q?Z*DZ)Y6Y*c_RB|37&}TDkIy zgI_*);(mMYvwP3j?K!{TylZC&Ug3kb-m>{;o7l!f>wmk>tUbK8>3I3-$5*!iVDZmu zo;T*MtgNapNvy2wY_D!?+`GML=cwXz7i4yC@!~ON6C}g?Z~y84nf(4rbxt4zWZe^g z&C!^h@V#-1?}nX=*{6JO?M+_Hb7R8r_g^^q{pEX?=J}j4GvOJ$#dF=xQOUP^SK!_G;~DN}Pd^J0#U3A^9@$;t13|JbED-ZiGLtn8d}+?lwPodcY2 zS-!32I_kv~C)|GLBa`2sJ@fqLxku$@o#=2Jp0wg~ZnfgUk&XN3kF=s;e_>98&OGA|td3hqI_+Gvw@cDFq4%?2oc#XzdE@2nS~4_4(#Dvg4`w^0QUTt6O<2w^}ANz^P@1J?X9FDWrzB)!N9JlRU zr?<&*d&2G`zcKm!@U!M{oNdR$@k=en-m-Jy{*>eCf$zl)xiz8pQ&Z>pso$AL(i)Ks zmT@nckUY1Hv+RnGTAJ_C_{9^x z=az9cK44Dyo>|6S$aA`kdtZ0L^Zj$;pMA5Q{+#iPCOpqA<7|8po$|CCQD58%rpvf@ z(t+Q#Z-)M2^o;pxLs+CxVw^)AaPA8w8kbLJC z=aIBVl)vU#;}=Xwo?C0$_!vATd2X$Fq3x$@&Ew33={vqWkLiq0T^-*uVR~i&-Lmt2 z`ABn42ha;!bL#hRown)Q=W?`$+rQ@OIB4PMu#*AdJjV;$ak?IU(|acE__le$?z290 zbsVs8blUmyJzQ`EYwWkK*=7y zPi((>JG}jPkT39~TR*gw-g^Gl`sPoj`un`j)>#x>OZc2e)R*ZgH>+zXytzZl1uzt z8yD6Rjz`Y*0$z_hSk{%i_3YCbzc$XFD~WjAfl|Ct(k#P~Gj?s9J6F=AhNf2=o zOw)vB4T*5R@Y*4X8;;U*Ho}q<7WL43FMz=d;V;$Co3H^(&AF3n#Yxhx!?ea z@zof37X-o*PCqn`OfV^8&_QEuInD5%csSUBVJM5Dxh!01;6^1q6pZfBr}@(!tl;!P zV|s$=xX=W{b;HJxjjF+LfaP6MsSPnyrWjXP73bJqBkbptK(XQULgVlRVCFcdFIImqBzu;WSd66j2XF=n8CVj)ky*cMIp>c46u@OV!Jc7o7 z3C2c%i1QiH*gwJ8i2iWi4UK&hjE&F^=OHxqPB1p&Hk?(FgEg@PB2fKU~KIAPB3>)Fg7mQOfXNKU~EJ9$JvwD5yU~m5`>-X({c)!0- z>_6_{_t*bu?~feM-23?6{p-y z+5GbEH`ab(_ZL_He)qMz$+f3D-gNNdtuO9gUHy&iRp&R?@3<0herDr8Z+kXB==iJk zk8i%@-~*ejwLf$Iuruv^*3Lid{JWjE?_}3*UircXv3l>0Yx|$UeS*hrRkuHP(A<6} zpa(7xT=)ZO0AE<>T!HmFRvtR{4;TJrI#CbRfXgSe%jvW(CegaCrStWs8%9wVNOxZw zfR7okK`|&6X}Wj+dWsk+-e4l$0;*&3Ohizqy35y63w>qeHad8f!4dcF z*GwH^GuiHzIaf^JwL-8Pt-~rxM(WW@P4XLz=;1PXj?HG`19>(p6C*}y4uKy9go*dwhPHdp-a(CS=c%> z5o{}4%HVPYg|vDqB!zPYxCOgCqL@a&`itRGwJjtE;%G7&O|8+@fx2s4?k;>s5K5m@5-xI0rwvwDn$ z(>1ys#!A&*V(al!#K<4dw8QB}R0#RG5mwD&YCdJK9)u9QMhxo)@vvSl=Tnsuz4f?L zRviSFVgo~o@E8#sw7hwS3HcSV7$X%r)I!Sfp$cJIepqsT(nQ2sft;WO{B=B>A7+}d ze31d^ZG}WXUGQRl^nXK|cE#r^MTF_6%>j9jubpj0MOOA3u)Ke(B zTRj(wQi|fsxi>#x>ZmAEFzAIks_yq!U{8j^Ss^l{lxRT(uYjmV!XV2zNJuw~O>>bs zVsN=VM2U=>A*uRxu^R2NtpM&0Cm50wp+Y^$-iS1A#0Mz5?KgD{-Gew<5i2w&xN~JB z?kO5jqQ~@I{!BT61oXa!8^zSH6fNvN*+hh;0MQZq!FaY!Xw6pH?~)q>9LtH7TGl`Y zY8JeC(=&j^4eaY2tH#Re-4$PH9#8Ez($i+Fm zNpM3t9^#XAB7lgDhm(74PqN5*hHcf@e4(kM84E*hJy@&~dQ!^c&9*Ns)&}_;Bw~Cl zL!^C$5g`=gnGC&CFcF2}uq03&I4OqeqqOALOYYQYP!6j_)a}o9I5}VPsv*tr$vbHi zQ7dFM4sCT3HSkJkDVY|eAlgf{LP7BQCx;AyyL*-byo^9%gyB z?kx$GW~vfsyLav}b?|t+FExC@xT3^eiL_pgMk}sSGn1!@SO)Rs4PVVg*2`p~y89GU z2NrRK^g=AGL|G|9CBepA=h;ro>SjMvog%l%nvg@gL8(W5n@MSCVphwD?0Z~U7o$+}?jjjjQaxrf)Jzz5Z za2N6Q(mg-Ad52jK%~Kl`YP?aT`F_%skaGs6i3Qy2X^!-iE1B_o*#WIVxPfjSn24Af zO4l;6c-B1_K(&#aO-0zK(jT?EdMw_NbT^C%u2Fg5WjFr2iO5lDIxEGykjk{&U^Mjv za}mT}p|n_lS84^0R)f6&szm&yjlVbR(Ub>Jk7#4jIs|)}oT#(KWLj1`p(^E$WU+{b zg#-BzB{k`dubMgv^-d;`29NrHhYys3Li0k8bhApvOD8I_R>VpjH&horWs2P}{bN|~ z=~`Gu!_7vPfoYT$+)2ux!IG|AARW~O%2gRrQ79QpCN^58j_yzniP35>6-u{+j)+w~ zs@@ZZT~5iTqTYVQ*Cz<2D}|fY4Z^I44}<+7tXO4bn6G%Xh8j>oTU0t7BwAr|dO|62 zC0AW5<%tcCsUw<>5~L(CC9T#f4I*@qf#OovE#`b^ypqqQvr)C`3MKTIu^TmY^e_VB zXsYdPMBD>^R|rU0&J`Y%xV~N?N25Z$7q1s#y2a3I&o&Y5&@kW5gt8tl5l=EK19=KA zmc+s9aH@m{VKI)yHDlPU`iS+Hnh14}!5T;p?N&m=b{CQ=URSP0Lz)_E6asa+5yg2? z)7=ek)v;$Hly2#yd6Cuh+7fuqU6z456FsB&&+lBs{ee+0Hi{_nC;SG$?X| zKE=4RRK8mF!d;dllNj0Y6k%*IN=R^USPoGw4PAe+iAV=XE{A0mZ*S#lrW zP7#n&2^uVKsu0C)k=5EdQT2t=D6DAkpp`9fqfji%K$_Mr6H%dNWc<3Y`hF9^H$z>X z$fnDR6qZK0WZRGn0-h$i*+@H{jt?QD9qX{-pjKM{w24RU>>ZyJ_;BNH;#THWJ3TRP<(@kHJ$!SU2!I_Aj%#Z|J zsEh)wlwYco>0VNFrE+R1$wz{vW*`qdV_0F*jIsK96VYhO{h=q2VMTOQZD(M4kbwj- zQt!jv6qsn6l?ss@g4d{P?8>o;(BT-9Bl~>1?K7BCgblhdh;BD4`79C^(}WB0qk5rC z#0`4&aVDac4$A~DMus(5&JKK-uBv6)GhzoZLh9BjJlbuyGeb<~3#;TQ0?E0VpqP&Z zAjCi7xKf~BPG%CRM83)}P!r2WbfVt#q}#^Y@0o})?T1lpNDl{kc-YNkddXa^*nw+t zA?F=(UYS#PI;#^D)pz_?6QR~4Sse~$Grl&7k0_pNd*qR))4-J=Li=k_iHXYfcFtcm zR_`|vTp?A!1ghN(G~KCOj}M9dZYTn~7|E~HTf7b%_{i;wwlWFFGp1d4NM-W1(U30Y zhJkXn*+Ph@OKA&4p)zbsg_sJftQ)Bse2wUfrV645Hz}x8Z#6nR5o=V+j6vtZN=XPA zE~rPg;c!MQ$5SZbcKk;ZfoI}mFehs@BJLKGBEy!`4V{cYDO5~%2YN>(GoeZks}AV3 z)74ebD4=@H)yuwI5>WM8(X=Hihk#T4>^USQ=fOdWx8tKU>QGLuVG zn?syWwa{!26}w5I+b@iKvO0={3R;+{mDc{&tau>6C-ZQqRCJ}_(m&t$}Ohmi(qJg-V*BB(UAW?cVx4>XvDRYTlXPB2oDz`Y&D**`x}Y4u<74{4W&jopfg7*;zSrIIFcS`-t?WSVN1Iv9hLL1H;tRs1|v;(TJNG>{WJuQL(( z1mo>-IU&g4#bJ~y&;=t)LqRgjFd9E(Q8%FtTLTwo&^zYjF^qW>wC)K~ZrPU^Wkan( zanNq_R5jNwz)jsVOv-Mr%!cx1dPkk=7!`&DSYmrR8NHkh7i*YMR1xqoqWJqo7SGlA zo(iU2UC-+M9c6O}B>IyZgJl zKeT(#?i0bifY&?o&Zq4By+nD?fAL7dR;3SSku&8A;zS^Zge1PPYK@+O>^)S6;L-C!WY_O|&|>w&8-^Q1DzJ zzSn>FTE_+fD)Z887Ug1GHtX`7jeDQobthiCx=@!jqHc~ZAMx6ig>tMhP;=yX4{ttT z;e0N+zRCIK{R?%?q@KHd`yj;4`xeSMn*m~SNnj3V&*9BiTRER?wo}fpTByr<^=OVR zn7BDwBnLUGcs^&ucX)GX<$RXHDd)jLUCY@N@e()t3*{_lF9bQf*|Ty!n{@A#b9bSx zoFVuCmAr|QJAKuigy3TgtQ(dKny3UeYpbLz>MXRo}-TYLSx=`10zU%?M ztXOrO?ZT(J3JZ0eZHNW+0=}GIDCcY?7svs=oU?L1>k6lwU%5yZ96T4*1_y87pWz^J zGrLgEa(xvJ5I0}3P|jH=nO}FpW=-2JkJa+d2GFz6eZ zg>shb8!(uc<%M$2c9aF}?7n(4y%9RMW;@H^w2MiLRGphU=Bt807ZVrCS#F4e{8w)Z z7RqNUdrFyKsA{=h2!URhS}13^-UET&lUyk0Y@07=LSXg8LOEv}Vu2htaWlS1&dfEj z+m9Mye+dKLGUtj%=mA&HjH#b!9hwuMYQ1BC ze|*{9cH(3127>LR(43aH5f^j!y+A>;t@h3lv=NXr6)?F#4XSqc>6M(*YnKx)?M6d4 z8|m(g8+8IO)H-OtiLg1J{WF(zCa6NA-U00kV4%5c<(#3KP5q3HYIJ7y1Q6@^c=x^c z0U?Uo>}WuQAN+aRBt{2h0|5W)k6#X#OO_7-7&kUMaw9&tNQURk}s^>aga83``JHYPd- z459UV4Syw^hpTA5THyqde6?l*fEwnjGLgiFEg)-a-weOw^Km zpjz{JG7-6WU5rM%BvZ^YSPw7Vecyynw?1K{PFw_-rKH}b>i6BZ_J)n|bEZD}q*XhS za2I!{Wqs89J?W!10swF4qqDb;zeDbN8zP`v0w-vLj&s&6eV|8O;FjNxBl^5v&pbyg zCyjJ4pX@}%d_xOf52ToSZb%4<(A3%iieNiF6r4GyYteMzQ9B~cP-rPll{qZv&UA1z z2j7qb322~IeL0AQn~_43tG4N2l=7kK(32>UFy60cB0*Y;bwoWL7>aIB0g*a_(JX=h zFGZls*m90IsXB1P*LTLxo;c#0yQ_A>OE1q6k?%=Iv=LQ%J4d{Ao&G!IfR7NEOb5+w zzE&=r^E>aquHShB$V9_!C%{H86Q$~9DKDjvP_@_L+O6xlP@p?QH(vIK#6;Lz#gSyg zKWaQ`ziZcGi2|cuSFV?DjDk1Zkzpeg(_&oB7p-@LA+O$G105|K12+Z}t#nWGCnSG? zOA^Jljuo%_Do}!{B@`x@D_!sAd6bHy*V8DNkC&JLCsm(JfKMD>n@)hiRXZWtm*;n$ z?@7P25oLZmzq6!=_zpSWbg?>dyk%FaXJl!1=SdtkHhlrH@OqEa^PS>wgoHx^8d}aXCshZY>0JF$ zaQ^r^YtCys|9$7*IsR%ZxV7c@49MP}x$=c8@810K%I~a4*Y8;Svn!P=fh(H_zrXs* zwWqHo4nAV4rgSs`JfGdH)G}f3o+pJJ}r<2+IHe_8!<{ zciz7D)ZIVd{nYm7w*PtCv-`T;)b644uQwjIy}JF*ZS_(={Li(UE1$iTHu(+fr~LvP4)=(v zi1J9&E}&r-P&bVc97Ww;!VSZ!&+u~zEo>wOBr)_12en!YjaA+Gs=ph|az)l@1_ljm znD2ktHu(Quzi&nP*S3|(itMw=cw;oEH^nqNaLXiF=HlhPR*aPURCti6YI!==kyH8V zWB=SX;v2RRe+JH2Y~I27*KVhYe6t=^RK*vV&Jhq1nww3_P^D9J3uS$pHl#HViKdHif&~Ll%^POqXuU|p z+dBQYZ<;!6ApXiW;xA1^jgb&R91a5r$bk`oN&&3bsmCN$2T`)&@(5y!EP=O}xV!X( zx0;BuA5-ZT6dJXWLfqYt3{=^dS14a1T7lIDROsvAg&anx7W+@U)3%NWZ6n@ho0WNY zWniNoPc?PeSTlUYHsZsl2vNZe$vzOv34lt>GEk0%jg*bj#b=B;EC@xYiYU-?j*496x}$=KinuFY5pmylE?4n_;>IthImnqa zGoFUu_>=ql+`~VIGtcvVo_BfPE${n%FrUy8qW0qZT(3N6yw}yJ*D7t?xkOxxMRO(s z1~X*GCi+U*fHT=1lE!KkU&SZY^BHwM=vf2ytO2EFsK@C9z2Z@g!I?+ue31k>o>d zIxx^MT8F0I?t0}x<87Wbu6389#cVYX$mv+h(x`r`3z}KBTS`W>h~Uc!dP?tOP&kX| zB}~=5DrjC6RJRb>{4gD{%<_N;tC8$5$3`l_Zb>B)G6UFpP*3%W5fzryraHMXSb5Oc z7_2;Kyu2S9djze||#6ix|k$Qs;W6s5T*-!6qACL7tXarmv zX^6@-owt?!xXq(dPoTTiLTq3#wB2rp8r>2L3rMC($zW~z&#ne(k-El+&av&J2Z6ThE`Ox!j-w>W$E?AvEMv)JrWGvAwemvdjAn!IW< zKl#*|vcnMg(Z!c8ii^7z?pwHZN}PJ>)IB?|anBLv|1f|1&M(jVr$4ZI z=lWT~{A=b*)0a=DrcYjpJhH@d&K0g(>23bYBmcJfw^6A*upS)yoQcWbOaPl9w*B3; zQBv9fTv7utTP_5LRWmMDB|pib@lv7dJh`qg1DdwAW?e;|=uRAz2Mu@P@S^cN*QQHH8k z7s=Ejq?KpOxxS$U(@|$34LfhD(Y{|WovK8Zr$;66qA}yS?00y!G3#oOBALSpG?M_D zaNTUx3%+5tqsMSQ){Eo$S|XOCnL(XQlB~lQw9ddmd(b$}v&ONmM#NNcte z?)r&XsB0D+S_vjsj5p1w5az9E_i)Vlz>W>N&YRhq)PayFHq=w~6X<+|Y{=0_&K=~?4$&l=xw zH41eit-?H>LD$LAQb@dnD0Ix3MUR1dN8!5_{2DCz(nkk9XhMQ64nD;MvCKU5#F| zfWh^+8f4i@qJ$C(+-zmLX*jL)$V_IyGF?0+il`d$ALl;b^fFSr$F!8y7yCIFq)5JF z3>B+FV+!18^0{u6AzO4`f|USTHne!mm~>}mFBXemS0umDq zGZPDEG}8)bDq;Gyc=-wL<9{zv`)=2Uhd6)NNQ1`9SfizfsdTB)^mP-?eI1UnIW}NK z(?Y+PO7&C4K*;G3P3RbRF-=EVP6}e>41my`yr%JTrbgrhzSJUGOk8A<6gPkoP3z9Q$my96?4V z@lv!5*ILQCg|S??nr*e!MfYVs?Lp&4&o(~fY5>7@t{mZL z8Sb>n5D5b@f4K&w<$fg>3}}&z-3Mc(MA$ae*{6EeIMcJn8Loy;1Efl@q}ADcwh*-Q zgS;F?${}ADYFjz1iNPsWi1|zLm_6}n&l>LhL3^GN zKNOScY%@>bG@j1DRy~o$3Dx1V9IpER!2w{y71L%=DO622@@csxaZaNZ zaS*dA723((Yxqk*hnuZ?)~I>bsJa@1K!Qf&!y#eXc9=nuy-H)ywB%UGY=i9<*o}$} ztYzVaj4{2jruLw*v8MK*aglTUf7YhO)BpbZcWPkNf^;;>_ToT06RWk9VV8^oc_30u zW+Rb6u3LkFK}!jN;aV&rAt#*gS>rs<8qah!P`3~ z%0(-wm8UHKarrCD*Deos7&{_6PMEuQ?o$gV&EL24X{&!-{o3kvs~4}PSI=De^WqmK zUolypMCR7!^f_$)GxOIhCl()=`PJ;RW{;hH-K;wIgxPzhc253e@*~q9o8CM72`9JU zAC|tf^p+)iDel|>U=~kt`2Rk)@P-9zftvaF%xh*EGw01be)<zGj4nml0Y4^w|0SM>g=2gen?Z|VV8R7}Wb z-C<)<2du%#EZ<=dcy;FYum0!i_s12zXZ3sIivD`_yBnf)0aZ1lnJ8vs>1JFlx3@U3 z^vmTZjw||$f~`npE&iTaYc`xdbqo6>+5k-$B&D8%+zrkqG&Wyi!h?8_jCCHs%5fU zYGCr_$y>%1y=C&&aYa8i`SEc@Z=L+)xS}7Q{KST6vd*H7cr`X~$O%h&jI_5DYgV7N z#zp<)J6?3kicf1REhSM*O)4{eAR8=Z)(l996AOUIc^ueGHH z=6*Q$mvKdZF!%7Ny)EkZ=l(P<>i6dUIIie-=N_I5w#$>Y{H$ej19e-`^OsxWik`P@ zth{~VvVZCr>-s8g8?hgmylGs~4^Mt{qXxEZ*dNc0-Lrl)_ty;vghr{&(`q?gsb6U=|D5{mhOgW9teYl3wjsLh_}ZBrjw@=-4#pL2&-TX^HD~Q{MO(AI zaYc>U?zp1O+0KUOw)Oha$(vo#?HiU_-l*5@Makujdfi@>Sl+1D?M0)@8}+)qXk>Y# zUdIvLsMqa9@#W6WOaFm;7_FO<;$*HvHyy51B|Y3aaW3Duys_99wiW%*@^>~Gc3aU8 zE`NJm(GM(tYh2OyFMo4f(f2LiJ+A0`m%lNt=zEs$az({$cSb*7KHiyM|G9JQI{!a0 zm7ADOPe0jV<-2+6imBYZJRg{!bno}?IA_QF^b2R6ICt0FwR7#cXU(n7erNXj+5Rjs zd(6y_X5O>@N?@lt6Q8+c=Q%s)r$4lM*Xp&a?bTYqg*4)R=f!V)lw+|@>$H^5X1cv1666=3R`fsAo=oE$ro2=f4!k01c7j% zh09Rd`7U3m*E4avq+^M2lOu}`1G!eu)tQc1$l5b^xf+>7mEf9fU)~Rek^?LT`7zi8 zI*e2TAQLUh!LE>WD1H4!|D?61l*zCrETP+JzS1>uMy*LIYIcRy3(Hs%()hG}8=Y zk_psULbt_ECRFREnu%CH>f|$9|MnpeqD0i=@q(|d5=dVw*O5vw52Vs*4$=yAAX>tc zRyjb~N_5UWKNiq#Lh18zyOWL>#jX|>)0)J}K(L}HVr$TotXvkwg1-2`Kj(fs7s5_L zSc``NT8IaHG7(gB(Wn$p1@vyL3^a0~dZVISVy&YgGn(raV)NNzqdb7yK1__pv#4p- zi#(v_)l9&!0Mi=whFUl&V&%z%s}UMR7+;?-i6~YzCA1!c7!8)QO(;b+q!^a%0{P&u zSdQSvtovO;fch;S66!dI(^@^8*62V;hf=Mm+3q8%o{3UuDFDI+y$sD)TpLiSr#c^7 zdrVVuI*gwb6%|CIFjx?2qX9v>orSb+h32a0tV^6PfHD#cG3k20tNK+YtCuzBQ?nJw zvtY0sP6Y~8hr`-8tQtdf_88YjAZ7NjP>ZCh-F&r20Wl_d+k~?jMFBkH&iYhGoom;6vn_tNJmA90f8Rh^;7kM z4?*WYJGzj`cMZ})`V};U^dyJLylB-*2~5u?s6n!iM>4QZ>8KH9i)_u!)WSDVPJ{_m zrmQeb_){_gS8To)&VyjpF1O)Ej!bf!H;A@$h5z;vz9YNax+iJ@lqC<2g zq;4i#Sx_?43Q~@x{R~RaxtAsRs>q{(s-+~fVofddMCWTUo$cUs81-cr@3Zbt2WP zB^#w~j({V$^Jp<(iq35r=PoIb=9od$5CMT9jhWkB4LOVRA*yN;+>mMd=oH&$1V#*V zy+Ff1NQu5a(&zx7Q42{k?oBO`sh4yd;z6`Ktjkq7p;mb?iXlR3=!;-BqYLq@Q*2Fb zBeU*JRFP(FHEp0mKa+-#Y*@4WImTbBDnQ4Fq*PKVaW>$uI5&6b`AbJ$iF90PGi587 z=YoAjNTukAVHKP_yrB}*2{x^GBO9d#ZOgQ+*$oW}Z*{Cn-IlWDRs~6SbU^~cd4uZL z3#{4?H0^W}fHg3M`={I+03vCJx@}TIJ7Ns3HyU-HTIq`kqr=)myj|&(5fimjA%(G! zg-O>d0>crYI_PsyH7G&~k^!_7Ivh%>lYq88?DkU>o2iAFJ-PDylU$8x3m(=YoC4;< z&YkdhGZAqp^6QC8ve+=gwv+UP=?CISM`)HO-7n{2G+G}@4X#M*fsRmY*+#=g5Uznb zX$I<680lL@nJ6M08M0?S=-O~TNCS+MipWW1gOMhjDw1kQW^B9@tHWI>7As<*h()w& zh%vWO4qT@CzDCn7Wsr21MapRj%V5oRKC5af(!jWG2~lB!m15H5XIvYYlRW|I%F%|H zEu@kPm8ye4XizGFqLFq8Vt_Ot7r|mUS3~A6cQsHakq;bJ%^U!s z1PBhL@ah1lb|bMWH2YmwBiJ(dy3#j;p?(yU6={eS8iPia)q7l_2p0e#(jel77{!DI zbVDN%1@qN>eGsNvFs9VXX+PV@!UKPE=*z2ve!C(?`hDjOzqE0KOa#SzFGF_op^oHZ z8?8vQ0;w@yIF#t88K%wZm0(bIUP1)?=+qxv8-W5|BU^0%vv9W5X~1wS zA&0bJCSO3LX5Uw-&Aa#Wgia#Qi;YSnl4ooECL|$UlCMPrwv?&#t&X3-Qkjm8X7!-D z!|m;YWzj~5h{tHsZ0Stdjzuf&nh@fKdCICcl4;e7DLTy4qP5_D4HhgjKS;%cSto^w zh7_${bSQID6E7FjQUypg8gi>gz(a}}LUX6ML{&v4>Ce^DaSW!RVO6{4%96p zqvzO$zZY>9o+dr--tH14G?Gp_gJvWa)@{y7T#@m$hRI;p*X#S_8t=Sb7TA1PkM@np zTU{HudKGYTFThekC4pWu4DdmOELLGU>tmZy=WZeehNDKBX+sMiaW%4)BvJKsDCd!g z&nUNe6pRYE01shuh-EAxkl?diKG`!sXx6=pB8W;TMgV>_pz+Z@Oo9HQtkpVPq+0Ei z{K=js<`ZG50NP>y-1}S`q6mb#&Qor(!Z;&uK!j~oE7HyzHRs|g?N9q^RwP{oP-obj zb#LJasdBSfgUOnxI;%y$t=4l*y-M=&Zn_~*fgw#&B__`1G7Wnk7)_i)3W54`Ii8~m za4wf-%cwJ9We}T0Qp3DLMX`Yr%9;hzU>v{J^Rx(5Wl#pQ4d<0VT=e^@xfI4?WxX=+WD`paJGYXk{-ygF>E$2OXH;_(;YI;N^ zg!vD+8l=z2kdY+PZDQ#dSCVWznu?U$d?P~)c&w^dOU+n@i}syFRC$;G@7#&yGv?zn zw@se=kKY;o|6}1LGizthSGdcjuQVrjdC2nlsAx?({S7L<(_kdDPUUeZGCs)vKa%If zL*~*$@to)t%W}Ba{z?2Mx>b9G9I#XAch>zV8S;olkKJ%>Gp7mQKP0KiqomgZ{4jJ_ zR7QVTE{N)w|_UKKqr zRl7JrU=F3q_UuBVqOY?H&8+$U7Ka`(=X#C9@Or~-jzi>daR`S08FA<#neQl!MpSc~ zSvw!oad;q~wo6I5nH2Le5Qm4Qxg?NY$J$+SuFL~cxTRHNqOF@LvWRnrsG#9=s|5PF zO4!Lviau&Cp-x(A=i)srX9tWhf}(rCA`z>k>rBS#DQZGgMY;{x-4506v~V)vZxnZD zE4b7g^em#^%Iv|)(NHKq$Y2m-7*3kjs#1%LABVtt9Exjat+RZ+++q3h(3L!g;d(V6 zYW4$>e@4`KWvM(U>b!>SBeTsq_3YVtMK3vYvQC{Gc_vz5E3NgqnFE4uWZcnRXk=vvbrZ3&4k(AGAZSXdqR6`A_Ho1IjAC1QpL%f1|pp_;XGZ1SQVB*KhFl`?==uNR zdO2R-Ey50;U_aQZV*U=8Gjmeg5TR5tTBZcuuPI@@mMfDm(xu60(4TQKPKsQT4q}uZ za+s~jwv(P1Re*+H>W6y8n1PP9cJ{9q-HGh={r@Qw#fhDlIyd_-UitINMb6FtfpbgW zUVLD&>)g(_=I@&~o!0?|^9Dely=PXP`PEEg`WMr+sh>?%oSXgSi61%YhyS_c-yT0x zsc^fpxkv>-;bB5=h?!U-DZ~PnHcUi|eH6z_K`uo_3mRyORl?X6abAlxTSMn#9_?fc z7uZyefxBHb31$bdhV~qylTvh8sTT9FajK~_6b`GhwuPs7u@+0|Tdv$)@^43bc7Wu9 zMlsN-I0>c`y+{{ExGt+2&O6o=o=-=>WS=71X%ljuupr|&@Ho#7l;C1ltqE;0X<22X zJ{X4b49%DZUh3r)Mwj`d+{J|=88o8fIB?X+f&Ta`Pe-~5c6xT;2sgp1X9tdO6ReCJ z@Bn(Gn_$_q14pUz$jLmD27;qMLPjI z+%2jx60t*OP0_W6#3jQODSD*)z^rEnj&Kvqcy{0jH^KDCfpD2j5*&b3Iut`@5u#q` zP%P5V1#!FWJY5gx61-&ff?CDIs_9?)AB z;CY@MIKoYEk!J^va1#t&2gaXZj&u_YJUeiNo1i~(z{4tM*AaHW9y#FQu=vQPZMo$7 zo@WPy@h>}UfBtpJ^nc*qK|y#wp}|C6TaiJivIrPbl;Q7dmlUZ{XoOy8OO*2oQzGIr6`lXXC;3>|t+0w+f zCmb#e4?k01_8|PfqW@b1kKr1)Y77OjJR3-&CvX99>`}9T1grNm3z9=oR|ItO;r)Na@n4N!+>FuauLy zY+{h5qZvB0sW#RVG#;3j^1$Sc`rfQ#^8EFpMtf|GbQqGDj@5VA!KsF?il&l9rXNx| zKrfv2(G5yz)_V#!w#kFD+g0|8j=l8?GUR+<814%~NlfNoe@2xn5r4G<+dYjT%1pru zbd@Zm6oMi>w!KHHvbI-n?0}9Phy@ujDG&}nj}b7^vaef!&>Vj>5u|N zz=k!pot9xcsVp_;euQRSwz8LZOs#i*OOWxBRwXs4Q*kz};Yvjd#rsw-LAQ&(ykT=C z3o#T(PhsPm`ukzNHD&U81r+yY*8Opcju*rb-U*P6{t(ROtl=Qj6s;nc>as;2f(tTM z=8P0pA{%96?4rB<{IZ>U)1yM4uuDrfOfu)3&<~`xty>7x;#etJLj2KAr7{e6!<7bR zfpm;%jd379)>yDLt9PT>Yji5Dy`1CGdhj4%*sl2c4CPOQOe1Aw6)o(`V3BY)RF0uQ zNf1<^!g35ZP`5uI|NFs{a!jrV55@%haSD_w1%G*{V@fd=O&3jyR#R5H*@gUeJXfYN zfl8A_$2NJW;7L06)`Q2x7n8B=JrX?1-h^W(?AY<}Szv5CTT+kJ_Ogz#^+@$_06w;{ zEs+{`Osz+%hm+>9P5u2y-I{dQjnvq>KTgq)=xO#wm;GC$GNVGDu*<{g_V(wsV@2xb zd3k*%dN@*P$D{Q~^>EfYwnrNYkhe~Alw)!|Qazk4j&1T#kxDxDMv;0XPw6)*eRHG| zj-B;L^>A7=cDW9U)Tm=@JyMV8VarCLwnS>gF|{749u6SJF4y0W)Xgim^-k+XD!%T| zdjC0+A3Y9?)Rn#1Mxi|%dThUqA1hKfFLWO+^!oe%rHMCBOzl{`dIehg@?w2qYR4Pr z!ZTl+)}259^M5Xv=brt-@+nh?*#v=G7Yfd2vAz`O_oM*fFJ{BerHRq+R*?)3<%j_# zBdColsLk?;R-P2QGO6jstT>>_j#l9en~g+m$stvyNm(m3&@df|#vII{q&xCB29yKh zAh-VS5r2&%iEb$Xbyx-MC;6zO%T!su)=vykJI+f(cG$8?fkeGT)_Y};3nn8qn?*-9 z0wWt#8ZMJUEtSFQQXrv~L3Z8NfX$9Zpqnp6>NXQd1PBh3)4_VLHmoypvBlQvIp;xYAau6h z`45<)x2fg8W4M+FN350!)GAR}ycFnn^I5r)0}^Z|${nJXv9!ph(PT#DL;e9^Myz75 zVVkgQGgy7t?X`_MTyMwm02-w2O||^2ZE6|ZIx0P;`m%DQ`m((KDjP@u8WRH{UCPyC z6#+U#E#ugb%x7szKmu|ctL6s7VO5Rf%SF3U?w9jKtlF_F{b1N~*f%%TGPPYTZ(XoF zrfQi#LbXguK{h81(@mLmZirUWMB2H%gdU=n(VA9dlOepEjn(+7-NS2CC)4EpL5yuy z%@Wk@Y6Y!fgQ;AT+f>WscD1~9vUyC^a^y(WGObIXOt3^)kg_mZc9=zSs(Of8#xvGH zigGO69Ds$YkCUWu*&H$hGde_d%?1jcVm$AMb-)~K9+!#Bi*pyeuz1SkVP?PoX9I>i zA$ghn21)&J6K`HAX1SV7Y4&JxBMC(ch{LkIJ~2AU97dBb+$-q`ZGDo><1n8Ibb6dB zXGMU}MH~nwMw2L`mjha}q!WHQ$7poChO)SGP?1dM!z3O8%rvQ~(URCja?Bt`m;3%; z4>3}0hx-Mu{cY}px9P|MWXnn0W2hq!l1Hi|M_7x|42aXTW*+aW)h1&lqS7Hcvh%Gv zU(I&?rE*E;Fu$+ONbMGh4xlXLTww*VID&IR$0sOFcJrVd-L96mp8Y3A|Cwp5WP}ZG|KrXHyYJq`qr#?-!&%mV&A zAPRBZ-M$>SC2-_CZOEMuRV zjm$oN`tq3{&i`TMy4AN#Ub*u5`sd1L628lRQKp^MwfvgmIjfx!)mY@iP z#-L0NrUNB^fwy+L>FIdES=9v?U$Q0)xnc!H)I1Ro0Xbhn8LQ$@6H(Bx62j?F!9VwH z*QjuA1|&6&eUxbI}X3e-#*W=&Lyhl06If}8u~)J8$ZW>%W*Ex0N{ z(H^#1<)8%kXfu@}(k*|SKw@Pkn+Vkv6$r3cM{0CLMqP5LN;36eMI0ivAR0uP97C#l z#aFJ!I0LJhBuVi3Qq;fBHzlFglA8pF&t$V=HjD7hY(D^E2|s2iV1+CO2O$(Kg=2&w z7a@>O*Q@2FPi$1z_*7RrmfeDkPj$6)rK?ftq#c@+NMbm^yCFZ@ZiBc(UzCWqSw>I6 zbSo9gHj>al&ewJv=an35qjtw9$C_0)%0bawlfo$+=yHLWkPXFzIOVJ3xNKM?N(d^| z3g^6C6+&5+$|W;#oje zntV3}#MH%`-O7U73FoPi(a#T5teB6tOJKSevHFC-_<3arSKzK+&en@DJG%JZ9d2!n z&xkg3ZFpcr`;)8Tfxz+gt_EFhqZ3H*z5L}003C(5YR;oTM0{xC2$_EjK zwPQq7RDV7q zd~E`TJ3I?l!^yNJXNy8z07@JcQNzJteHf{h%T}cb#k3h-0omaghwOpP z<#jm@XBD048B;HEHTrFbwW!gf_=J&>t)V$6nE@c5uVF}_&?4b%UgwhjK+4~wr(Wo4 zv~wX}tst8@J(|sAY=RG~V%sjs&=4exAcMBTvZ?hLzG+OmY(be8?+^QgI9O1sT_YVH zNTS@S3%#gFg(ipP^*%I z3!z*G<9u?pYEN#IqsEje1a8L)6q^9ml!ArnX1H1FNUdb76ZSiNlAx`%doY7eUAuh4 zKR)m3s2sxhT#QrU4UO@+7~Kp*LSSqz#-$C$F29Xr7{29eXVF57vz&E#7s)c^Vyxmk zx>4vr299QQGUr$P8_axy2lhrc_m~Iv#-)wj1 z(y3}8iNuN>Ob*s0JRyd-PDl%@Y$e&{d4ik8Hn(#X=rDg#z1Eol%;tBkqHZ^3ae5l-3*&dtX1f{{Hd=(r-1Z4iIkyp+PG%joI z{FmHv^aO!~3WYL3B01e*KU4B;1_`TCJjM~luu(7Ze!HUgocx&cpK~=DeJCEQ>Z&mC zH=K2HuZQ@W056k(4kyw?ATNPjNY(&2%g$fzYV^AZK!kdk2EZt_U|WZs4+Al=iG%r} znk^)2omMe};9Z&FR#aDGkZd)Oe8Ar~EuT=~^@hSHD!yU`?_{V{aS+k9IGCq=rATf0 z+L49_Uf0FJhQ|23uAdlbcwjMJe79Q;oo-asSSpnE%Nac1SNbroisqn~j@1*+>sX}? zQ_hE`)R5;^Uf^mNKAG;7+XJ}H*IF`;LA9b*tnzv<8AO2`)huyES;oW;sxG-XQHAjt zhF7jw={>524-e?cWF(-WR-x60mBP@_8fk4%#iB?C3kN&-ET2YlV1X#w^KSlFzC1Ro z^z6lM?KvElfjAR`&72H^L*|9V2fe^x}T9U)^<{23#^*Pmd!w_Se^kr-is_81HD3izX2f(oV8((*I@SqP zhagbJB&D$UOUN7JmA3p-~TZCcg;+3+;acaYJ|=d%>-80l6Jk)?q@n% z&fETW^*IOx5V&upN?R{r^ZQSCz$(l3sROiXWd>%8$r4R9#}Q0?E12PDLrk{ZiL<>> zq1cn;4$7E`OicrQT+$an2#i;g&W8#Bl18nCfSm_k_Im#wvdIGL}eWVRAX zuswqcC1j*(@cW-Ug6)g}TQvZE2=%~fkP<))=rvf9R0++QmdRXe)5Qr{@gLg6qLXEfeQ?DUJqUF5wGml5Q~jPj+Cr<@Twl zM3`;8Z{r%lX=O*u+}dOhU%-c<00PlsmEYQ^%>I)`V5S!^Fo1yFQdCyAm}B-oX$02t z0u~4$fd*gB2nT_kI07@gfH~dVnOjrI)InfR9Dy~xfcXQ6e;{Yt9t=E++&Y(_CJ1R3gDJ&2S0B3+i(dT?mY*; z|7gdP%JOr!zd`QhRRh2OaUA;U_HD&;P0PNrv!SWzAONvG3m zj@n5rM1w7}q@yJ1NBI3qBUot+SPyRz`2C9`*xVSf9v&3%`xi#A*)d=}JR0El&yQfm zFnUTl@U}i4kmK3|J30^ZB()N3iS|upXE!__a$$u<0kF*DfBx z#>RmipK*X+d)^3^83WeCEqZ?Kq7f`T2CRq6@ci0v1WRpyVSQIvCJIa|8HZ^k=Pzo& zpn#Ey6k-((CYfo8g}PO0`4T3b=_CBwU<6B!0jtO0G>n$`LJENJrdqHCtsc@Hs%c15 zG&M&y9pXC%O$Zt&@N4}MEHMTwC$#}wsDWyxWoY3_vC%6kfk92qc0*3?k-U&Z>sX@^ zhg+(}uh}Em=oqkUI1BP==>`biQzk1CozVfS;SFa$;-(P<5vTy0mrHhtMTm0N&Yw-ySw=7f^ zmgnC;pLcQtUbiDT_rToi=OVMeo_&{-Tl(yo@6Wty2A#fd`mNLa^s!U7PuWvXo&4tH z6_dck-4mA{cKa``T{u%YA->DQw$-W^190%h9~^bow_o!9(-$xL>Ysu0Uw8S(VkhkT zO6M~#fWGjHQ{MmV-~LftyI=(DVdLroU-I(mSFZg1#c!$3-}!IEi~M(6$7qc=_HOz8 zTPokPXFe79ptzPF0ejfGy1-9((w$4!JP~fb=asL&W&iM~_pc4Fn7NV-E=@O6p?lv} zQsQgU4ChqE!`q+3ZeCruyWVi?_g{F^oqI#TCGUK0_GkZbS@-&*j=uDv%JWbE$;%4j zImg7;atFd4Zu{!O{p!Tdd9Oe7=P%8i{M@VG_T<%lr(e19)t7$bUH9xU^Pg_d-fYI# zvIoK)ZUgJW?S6N2r*m)kujifl-PP*c>ux-kF5LXly|>Pu`2F`j{Y%To{4%~K9td~1 zEvySi&)?X64T@m*MtM%4!4PQ;oe-i z>y|g=zp(4C*C)!mZ+L$6+;2W`?L%k&`^E3O@1CR1D7@)o=dSTHfYTHQZDU>a=R;=& zUUyC9_kY>Bw|3c$C;s%U_o=Ua`CrL95^3 zaqLQD@eSeg@A#?1F7yzmT=lr?o?W>8Tk$pSAh<(rW?i@wU-+Aa(6P|_pGf^l{{VRF zo?nMgdT~oT`puUwJoA{p{Oeo36kkgn2zR*ctP6MiPmaCuEi=Eota#h+Pkz~j*L)F3 zVB+Mt(x={y-2{F3)2F{DzLq=??r!+v2)#54`1H5@%n3&i~DSpY)u@m9Y=4 zJnr<1UbN%fwb)2~^VRxI>MO)~|5BZN&FkUP&U25t;Ip55<&MQKKKsYdy2^O_dp`Wm zo1XbXag7;`TpoA_++y8y=4X=M{p^pKYcGDq4L@&v^_#ix=r2QNV-I}>zW#gMjVD*d zHF^Z>VUO+t-x2%bQ}ug~`d97tclJ)+^TvtaU47=sKfdvTzg+O6(@qZm@D@m1qej3U zcIhtgoA2s<yXPkSYxE37&d)TYHz)SaEwD{c@U-_ip zOBaEkd)n9Ed+p8--+s$)o^sOcZBIS-xZ7X$d2uZ=0`{<5cY)9S=8s-} zaqavOu!nuS3w(R}t{+C*U-&Zo=c(=sZZ$q+y>6(SaZLaHcYfxJN1b!!`INYJ-U!&k z&fNtzUU$RZ+1>oF&uYK4{gn^VPrmxPg`2*4)&AGVf8JamUi_=e#IG!`preaW9l@Qc}BoVELuOFwqu zop-ReS3U)_Qm{Z*NU$? z;eJ$H3yy$2?BreGm!I_Kd!BIV2Y+=;|Ed#DJ~{k^mwlye&9B^ly@l z{K+ey@YQ#AzwjFZdgv_W@&xnROJ98ZFaPkZm&Q&J*MJeQhdsRuymkZklew?``7iy& z!TFz{i}$MMoc^-%>!P>6F^TM&=J$2QHU9|M!>-;1Mz6SkfeO9)>GgfzxcZ6*-umsQ zo|M1$!3PK)nMbcU?ZLOl#I-#mU=RCx7Z|+bQw*$3oqxx57lkkQ90tDQqz6v>?|Qa% z-Kkgp)K32UtHrgmN5CGk%(=kd?Ek7<^B>hd{vqYizn;>0 z=11;rtndG4CO7oY%LJ<&&bx|fluSm+0Q%-Gl$OB@(;{Hc*uN4n-ei@NjM0ux6Q#B z(KaVn0-!_YV0o0OQ~(I_uLIVLR=r|5xtuOK%^iT&^FlGI^T00IBD?(xg-|&o6pLne z2XRTt*>Fh?;^_gQ?=A&RH23J!5K3%mH<8+1*^P<~BO8k_HdN{)L8cRIVYON>>%8ws z1qTp_Dm9u9E2K5dw1ZSqp;K5zQdlSJpkboXL4w>PQ%xh56I-T$kCh~8RP^;ENwaIf z-vZG?XYw3~Thb7Qfx`wO_|FJL4+)|U9f%&L<^Sh_c*yian*;ISpmU;POLPWCslen) zt5bKv16WTkc5s@rM;nM>2tI7KG7!zD_S8toZ|UJ&M{DJDD5Dh{&g(BT5lQk!uoUWv z?Yxr4<*FZ$p z^V4mf5l0#A4ml$R{uzO2<$Mjz*J&vER919IJiu@n$VhZj#UQ)u^P|naOd@qPJ1qJS z9f%&5u?GZV@oc>)SI+LX&aO5zttS}l)UyHPwOQ)rH>Yel0s zs3!47V^2zEx=Ozk88habqOVVW(Au6+%w5Gz?()!YyvE$tRVXsbHFwCE1O6E?_kX<# z_3-U>y{mi4JAOcn{*5v2pSfxsXRtRk6St$(NheMLsqH^b8r@h@`x2Bg}jv_dlqPQ zvvIqLD6KtOHKG~Ce2`5ri4^3#po$2FSu|>h2$Ip=X+gq50b`g1;$k>5-lBEA=o|U} zo-(0M?A*H(Tz&8Ao|Sj6>|TE7@|jC-TRMI5&5NfjymtYg|N4Aw$AddQvSYC08FPP` z`}iC;`{UW}?2ei1X3m-Z;`DQ-elvCX)G?E9pF}3U=|COsN8QKHavV4R@E2|vguo!2 z6;ZXnrPJJ6G2>XHw^%zI)+5$JARNw=``PT4dK$+XwZ+=uunw?>*m%8P zeiYWoE!JQV4vEofdn*P1Bi0}g4kolgX6udkQ_W#JKSUc9yhdh9tyq#vZ}CUnM{Kcn zIIKskfglWLN{q5KkL5VlqFbyT4(kzX0T700=iNzbi|6B5i)^uWIIKsk0U!+4D*1N8 zv$YHN;Tz$+dBJyJI0Fa>11Y0bQ4WUhwiS-j8|{Xz6a}`P+#;dw!wxLWVX^*J7{KYK z{${xpIVhaX;kMS$E!GZ)wb>dJ?utD7Xv+4igRC7I>k(@~5JGxC>bGpU0WjlY1 zwZmaOV$GQeLUA%3+xj+Rduz|wcit=j0M01CsX-wijKBvbXITHsfiEA{|MHA|&pd>- zbCR`1d;Kp5YOnw08T-yXSQ`ulf(L4k{&Il!=r7OQ_l#K(T=#r4^_knD!Oc8fqd%Q` zP_KXHzH>J84=8#7g5g~U5Q)0_2N_ZKg%4~eztc{^Xq1a=?O}gwCqD#2aKw4z$ZV?Y zbt2(Y`3zAm<|3Hc#?w(MP4#aq}COcq@7$StRsN6831*pQ4rFJmj-$j)I*q}=rv6|F@kSI3PSj@?_ z&cYomnWy!l!?-|HwZs5JYNw*fsBDU{6d}uPvlL4uw>}oUbYEbj4#r2_)vt1zT?rw$-w-Bk_Cor-f0QmN7)Lqa|U z41sc|)~a@L60P^erok0zeO~j0$7%(?>w$-I?P>|BBIqz!I~lAiLdonh2uFnDL?}W9 z`$i_#7OIv*<{1+}jT@&9_P8E+DAz8AFpBkBjWHD5k~o=dCwgs4b5bv%y)qe=>t#xF zo^Izv%1@8e6VG-%@KCN@1J*3m@)3zaopWMzr0}tieg>~dmpb;xY8%$0WQMy*?076iOTXdcs!c06&F+@|WBXtBBrw-0?J@8Pj zUC~yjpT*5mrx}GxrdiC&I@aO`EUC1s>1dF!A~_L+Foy*O9j71cc0KS=u3d4&%)u5* zhjNKtIv)X1ldbs_&PgZ?DYXVq0zB3f3x<_6jqU5;!hKJ7m!g5g?kpiB2!dg$Byrr9 z;p|i+Po@VLswN`sXr+Og*{#Eky62_{Ky18@xbJ4 zXYnOdA6^s|PoKPM;kOI7PW@`(6%OOyg$qxg|HBkNfBWp+bBj|ip3Kj`X8K9{md)hfqT?2`BblZBQAzG z+!*#i&Wxu625B^UZK($$4b}#F(PSJblX?M5(mvY^$E~H$yBbu!YJeTIQ*8_RKGK6) zc_K}8DB9`M1Ain!4E+Wu4t;Hno`1$jBc=;ki1uS`E#0n!+j$?}s8y;Y!qU#XAQPQJ zLoPP(hC;NY#W%VdlHeq;!s^Xfum$matJrAitxkf)zAwOdjPYxF+#Y+}Beztlhcx!suBRUL zkV^HCRO7MGqx33CB~_KAk_=-e+-p0Dd4#>0z2w_3pZT&&lAZ8Kz69807g&~MAu(V` z76KT|Cc!*oSONqR*6b;%@9l229;G{N&zRhv@9Wzmo&Wj&&j0-Xr%s*oe~eOn$%yx2 zOq6x`BE>|7J<3sVDc55`Hysl$OvzS;;d5ZxU}!nlG&AgxuqG0kqK8MBe{NW+AXAeLi8 zt{F8OSw4l}L!DxZ1^W6l4;2@Wx~1O0%$UJ?s@owfNldh=wwsi4iME8xS~)GaECiE5 z^V*A39x;}xXA^^v$rf^Tkmz%)1thW(%wT4sSZqN(p()nea7LBNJJT#J9NlvPNTVV$$H zIby!t>qd!Gv+jzLHIAn`Ef(adFv(~&ipS`!&rMN9B-`aVg6wHrph8e%rH2nY^)lLR z=yEn36?#sLXcF;2%GjK0BF9T;AR>18YD=l)dP&+C4Izf?6YYE%LtUcL(-ZBIjaXFv z=3AybILhcVPO18riIZsYZ>%%Pb}(AyV9)=AmRWHmUZ9l2s}M zI?ZlR1qo!j5H%{ec(z@}!&0P|j9}bIQ1ENbDG#BZ&XoUZ zt^Z|80SEXP7<~ zO-iiLU}Id?DkDyL`wDWE?oDiy2G+Dy>wX7|*dhtq^Nf;@Y2|DKX z`=%6uW+@1Re5D*|w#p=39)nfXbx;P4%4o+C6I7H5hcR}*(HnnjLctExeH-Wbl3GDq zRL3du!xjWC65>LpAa*U8KmxHL)u0#@-x}vRG=B0p~>ep-d+%xu~-F z7gLI!5FP~kNrmsYN=ojLxkgSl#=%s(r{?6GChCK>)em$VPGaLzQ;KfP&DG>OizLQ% zLyqTUxi_dch}n1h|Mb5Q;a%VQyO|Glxmjqy3`B7&EVyK zF{P+SL}h4)5^mSk*p5hKWTIGtllmy*mgxRy(6=Mq7Ni(`V{@AEiLK^}V4(;Qz;vKd zA){sEiBSManCTJMm0Kob<>}@i;GlK>IHw(3$zlSTvzww`Y}2ib+hDX%oYnfVY7|P2 zZ7yD;Q#Aq9OvrD!lcb%j7A%`}XVl4n%yqEcBAsqks|Wg7S=O*rzZ(x)ikR!UXeE-E z4kPc9$wAqPwkxp?cu&+SMY#~I4&gr5t;Sre>dGmj6=-ytwe4wwIyT#8Av>MO3NY6J z%Ys@Tl&}N^iQNX$z#(_it=C&J=*Cu5BxNjIN1JMBCT-foygYD6YTDGk62yMj03bkY*xbcTm3QggxVW6JS`>8G^_s1b3T}zS3w^ET*pVydF-0JT4(&V{BGp1dvvt#^B5XhfdE{DZ zA*QLSG*ZUVoaNw~79Sx&H0baS&WkD-))P!{W z+DlU&cB>xa$B9ATwcDoBsL926wpfaDSR;op!)B|Wm+LfS$c6mP>64Sgnw?lwtm$@W z)NQ51YCo9Mc+-lsjRr{0!3=FJ5oHp*6E5EzfF~IUQ=t-0-ZP|tr251X}?CW(|k|G zbpSpOhCXpf1fJ>li*pt`W~WguwhEKVvL2?>u8Kyp307}&Xg5qr%p{U}ZRVWKBe3;O zMYlY6_XWkeLZ_p~y=ya%>a?rklsD+$?T&6c0Vo)XhJrz;09S5~LP60`Gu~WE4ZgOs}b!&4>}k zL5XZzKzsQKWV=!86rH|x94BzLiZ1~Aktg&Dgdm~lGsQxxf}0+8CuiiXaid+K+6GiE zDWwRTMd>~*q^ma9>Xglzg@%rFJe%lL!NCz0mE$mUR1V>FtB3=)OPA4Hb9~2ZNsf@PxKd(W}LN%K>`ng<|Lfz2i(fJYG(~i>D60M7(d0KRPq+ zNp~mToi8m7hylZs7BfYfXZkJ zvFwd*(r8-G8~op-^ZP*y^J5k+Y`MYUs5kt%%S}>mjCxkA8j7}{W*iTR<%Cl$LUI~L z24n<955egKSA*D6wfxY_jgVAaOW+QJOerMFrD!dNN^FO9h{m{Q#%f)x*h7@$NQ}X> zsI%Q}k3R(O1cZ@IH9L8bA)U<~DoV~p9Z^zB4bdT}ZiDdWnCT7O3t|}DgT3YEZ;m!E zwm!DsM(ow({gFp%e-Sf{l1e0Sw`2|Q#$&#%Jsd0JB3t~a{C8lLbFN z4ZGVaj*e~q32!HH?$HerQF~3NhFa)bEzm)y-)fx>NthpzaK@s1uUCNA!#?QIXwdVq zo@;q_M#4gUa5CI$;NvnhR>dedKSIrPONHVY0U<^DNEHVCVY2qnmRF3GsUX{mkS!`$ z4fdr<6X#s5*334@Zja$ZK?@X-DwTOWN)oMP*(nvPlp|MrItx{rY`IuN#!(Cub>Jbx z#3T8bMWICkz?rPx9dGNQSAg_C@6 zS{`!V^5)o*_tDJ5wQT8NyZ0(t$HX89;L+MJy{F0(u!m!U$Xst%eA+1HyZfIp3Xjc0 zD?11oTcqq29_P4zMGESNido5Zlc-RRHZpNbO6WD4ZB(-&RVxkqa25p0fKDD}SDT1b zno?slZYGl11P8~QVnRL257c-*d_;^@+329r!(PlBlKPO(CkbwZFkv>-ij#dcVO618 zQ|5TSN##oEBnT+Nlupm}87qf^p#x=qbpQY8{{PYa|J?ik-=Cf%KDz&ZbpOBP{{Icr z^XdQg?*DHbdx<={|9^D<|LFez(f$9U`~NT3z0>rGv&Q}Z&rT^G-Tyzj|6lC*{$;uU zm!^B_*Vz64)FX49`~NPm^}jOr|Cg^kw{iUwJAVoOdGzy00*@r{NCJ-}@X96d@yG8) zy?caD@|y>1lvBDehXG`60;Dxu7rc}8b#6c=?br1WIDbP}j-?ne6w#e{GM0@n^@0=U zBW?~QhgLish~@-2Zx6s-O(rq#hH&CU13XIr_E+JG@ILov9)Hfa7tiyjFFd^Ki1Yc4 z&RlrR_{#u-hrIrPLdYcd;M}{uP^SU1hjsfwuCI#8I<3=6FEXy%Hd&2#o1`^{9WG@ z>$n%aIySmSp7wb*I>+JV)iKvOK)}rD~s{ zfF{G!3}^O|M-?k64iz$%QF~UsAgZML}jSz+vtKu$Jm}Gx1Ed8;&OPRHt}|cbk;lshgb=oucv%i07)J zxGR#ChtL1#x=Idr+hu2*Z%4#NSjR~lgny2Z{t?#Koy{v(p5M8AKi2`sQ3PS=yxQAy94%KD&c3|a{?^}kHaGeGON-pAPN#YB z{I*|w>|Vpe_s$c1mrn34s(*HjZ|X$08rdZeQ{*$=?W)F4-yyEXd&NV#zvKOW;tzb|J-GK_5AV?l-mQZ*3f~>$J*BfLl8)oeaw?YG6WSQks($;p@J|!}xXo**C_w-g^$L562j%3-%f%{*ECYFWoC4de~k+ zk>%_Eo^OQjx~F+k#7^{b_h5};fpc@gudc$r>tWsby2y!r_{O+@FXv%AwRm1VSfiBTF~<9g z=Sq-Q0ld%s=l%YRg^TC8 zi9fifdU#JQo>vamsJ3{F_tfIK5~sJwf9}s1@AscS#-BUcA(Gz3ZI#{E4<1x;e zaG=%Jcq z`~Ca>gKsIm>z?RI@ytrUM%BytEB(sYS#PEP+`pel^WXo|MObG;XJ35$UV3K6cMjGl zqqT$IjX+`pIdFrHcI*SH8e#(00FUkP%u(tq|JdLVzz^eag3 z{r~di=Qei##daM0^XTW11RhD?Vk7|K$liB9j%|rj~WinD(N+?YON@0}V9n@3Az|AXVBMQPUeFaTU zhVI?pnD^d0t+^K4JXmAX@K6^xr)#cFDqYT1|GC8vU-Rw{Yiwf9+~~~ReR;O^DtJN6 zues*s4>|AJ@UT~=pmu^6F+KO{rsH5xJzV3)W5Nab_i`;Ip&80pj@Svd!{|rt!&u_b z40RjU!)FL^qeZRbW^>i@fYnP%J7E!8&}>v(`zYIQapin1&p^p^16X2Kzz@@Hv!A6D zkSxs!+YZ|4A04)0GRv2_VH+N&i}6;nN0Gitu0h_=fysRmzxRyyeEIKhZLYKReJG2Y zYhZJW8h)t_Y>mrdPy5%&oiMSdnWy6u>$q;6F~~Eo7_X8cp3PWroXG=7#b3^XNnP2c zISahvY3EJ)jJMLF7m?GqAl})XC!vR33m`bU@bTO3CeYqda1_xFWt2UP2P3UI&3*Z012_y69#xg%b^yZuYsUw`HKt?vf6{~y2fW0#)VEP=+a&7T)vcZc8AK?S4N z?`&)wARIM}=y9s+RCJcmhDM~9?X_YiRl_K(-I8Mo-fgvMyY2>0u5ZtzXq(*4F8gku z>i6PCSFUDBrPPuvSDY=K)DM1X_V?VA{rEQdv-w4K|0W;Ea-uv zPC8ONT^v#E78lmyff7|)pkU$I7ahOt{K1`{za|U=>9Qx%wdKClku|)C#JVn5PuZDa zw_zq5nLs-P2O)|wsX8iQAhkYyN`j}?{8{Oq`)*$-cIrZ#?5JwDt!0vyJX;9rSh~p* z=|=G^naOnftXuA+10eZufNgb>N>Pe}ayx}`CRT(C6;}(#oJFn+;nUT$yywy)>CU$E z2X}#V!!JH{C$TF&|9ohoB=@1FXQ#Lgyy#)P$e&Fj)58d=M{2?!6e^dpI1#O7%W9+tniQr)$40 zQAudF-75kg0pRKL;4v&545h3hT|6WCg?0kX`eiH{YQlAmq2&uA&mC~_qN)Nd(SrMTwKa9jc&0pQ8= z;86^UhH$!4ob756c%RP+Xtv$H==R2AIi;}Y;N08h*T4?z)>1+^R4 z896KhKfAp8ZMR>g5>l}z5cO7SSl2CAa%MO#0Urj>YW(DBQZ+#_5qJ=;f@%(=AXynx z8A5ShDaG2=iiL&Ec22JhWs0|yMeuws$k1%N{SjWZrBSb&%rYG#Co;*|GEqy00f9OA z;Q2W~LcrBJ7f+{V4QUbh)2sI~?VoM8kLQ`5)pmzuixFF8S;hhichQRC3 zr$x5vtJPkO9wH#N_n#LVyvU(*f!ILW+OrWle%tv2Isc&?ih>=)vmW@f zI|z7@Z{;FTv;LBP+nx82Ts+4itaj&p-W(tAAWq3^W+G2()~c`OBLEit-V=*n!~1K| zz-OORe%q~fa$6!W^hNs4Gm~Mw(R+zl!=-5vywirY#I{4{9u6ySfu=_h3)IwGVWTJW zv4)SvqJhi;^xJOrHQSQ;fWdr3PUd5cGy6sGPVb_Z&By6g&uh2(Gy0=6-OZP8Iyaxaxqaj3Zrr<3yz%bq|LOYwdi~CI;rbh{{l>L_c+I-@ z-fLTXKePAJUSaQDpi#ol95$)z!OKpTGL#_8)Bj@b-(_ z$?ex&`RtYd=}PAcc4cGhr?$$Ci%m4B64_^N0<@n{VzVs`X{@$h5C1mr9oB!A5 zcWmZ2pV;_40B7kBeCMgHXP@1euVN8o(dY1;cP$}<7m&g5op&xF3ojrG!*`xsLIy1$ zgWx;wSV9(BKo)}U94sLVE+7lSciz6342#Yu!=mt=Czg;!7LY~YJNrw>umxloeCP2c zWat7i6u$GeC1eY<1Vi9EZ(TyRKua*7C2v_mwm?fTpe1i!LbgClFrX!GT0*u!OE91% zU%!NGftFxEOWwGc3|*ilDA1CxTSB%#OHiOCZ&*UMKub`dC66s3Tc9N<(300LAzPp& zDA1CxT|%}%OHiOCuUkU4KubVf_B&s*glvJ9pg>E$dI{MAEkS{neAN=N1zLgvExEOr z3|XKh2+)$7OUM>z2?Dg_#uBmxT7m#AxxR#KftDaZORg;;Tc9Nf(2~6+WDB$e0a~)V zglvJ9AV5oYmXIyb5(H?;)g@#Lv;+ZKvb}_CftDaZORg*?gBNHC476lx3E2WIfq|A> zUP87&OJJZSmzIz%&=MGE$>tKW1zG|FE!kK?wm?f@7{2|{C1eY<1P;TuUsyu6Kuf^8 z*X?gwLbgCl;1GQK8<&tR&=NQZ-~PyAvhV^e2}j}E<0WJZv?LsXZ;zIcEzpuM2H$p< zkS)-XFbdxuE+Jc>C1C`rW*p&-CaVqV7ehN-R2Uq1=9_I>Fz8cTQJ=anC|uxvIWx(f$457 zAzLur5SVUb3E6__2EGTkn@h+ROg99kyRn39!E{4ly6cO{LJJm!5LgsyOUM>zNeF03 zbqU!5EeQcF(U*`d(2@}Pf!md>XQc(Nv>}8#sWI}nq&tr@ru8_ZgK}d<2UJ^lTn6RB z;M?UTj29?O2=?CpuWi&eZvO2X@4oh0?794^=vxibD7fYu(Qc)ggl z_f)VZ#+ZHyf*rnzyD6Clw8<(j*IZ$Y4>>tM;>TUP1PS$0tUf4*MKnd#hCDvu0Zn+s zK{d*lQcDBxo;)P9%3gWIJz!IdDluVpKGE+p_0El%2M9ncm+ z>Xo=U(j+3I@)Ke-nRTRYaU_JJiJ=KZop~VY=N5^2 z_8cjJr&GAhlAQ9I5_R)}L`{Zz;>gM8#%hQU)xzRfRXD0PN7ST(Ld7DjHk44ZQ{~6p zsNHL(^Sx*+C%qL^a5el2M)`YAPOyu|y*VIc-+61C_CmYmFn>VZqMSZOkF-!6d9YLKzpr z6Rl1^5Osc$s88P)c&DSJoXRqJAs!Q7Q=+CWNYqqipc<8!N>|dUYy#$)N?U2bb3{$Z zPL&Gw)0!q*c25^t*+DkJCzITu)ne5uU5_Y+rbv-kPr*H{PCXEHZh@%J-E!etGD)U$ zNmYFfi5jfm7bR+OEEpyzNX^itk!A~7Mib-nL`@PMD%KbXo#+UT%VQ^+XmSHOrt_2y zM>E0nU@UiAnR+(acSTRs!UIuf7m0e-uY*lzSe3jR?(Nq~>`h#w2rGTzXB3`oeY~PPRO* ztNC2M$3ljy(I%oQ(~aTT%^p5-`A#4(&NZ6DRAEGQ1)SBoVZj_$Lb?l)NhfMD;bsTN zTayKZeIV+ENswmUlQ@bIvywv6{A)_o7qwQ$Trp+T>!nVjC&WU8)@f;_;Jke`S-}`t z$%Q%rxC*uTX3NMov;7=OmAd9Ql`Pd7mPx^wLtBH%0+M_nYH85|GJ74(vdNUj3KFlq zrbK;FYjr%65JOrxH+IxYFe9^224=(aL`_(zJS_C7R=_T18_78DgsSwoBQfD@JZ~YD zlw2x{NUrQ~*hJKc2cpg_67}rcqC#b|u_U9$6R#yvU)oi<7*y&s)6JB{j+PQl@BM#k^UB8ckL|o+ z>zlwI|1JK2p=^-2$3+;v~u|i5=A-gpe=%S00WC<11ak|h7_tYw5u67~u z8>ti=9BhoIN!{Vj9Vjd&AiAs&%b7V^3jz(Mw4J1y#o%wypW$o z3&iYN>`|Q#Z+ZY=CT{4pgdbMB>5f~>^QC0JlM7d?p}KUW zm2E*!vRE->(+?fB)<1GZqHc+fW1`wv!v)gR-1Y$O4Yf;UUmo1W2R9qwhGhuK42B6W5mBq#z161g0G z)DS9B7gyEDNDLFzFvo>3rxq?rN4lHPvcZhQ>B$7eGPYii6cf2aEZyn4$w=A`kKthz zt;PC6K8c1$%P^TlWz;6K{Ju z^_i!SuY#HNZBUULJl{Uz|0(&*COP(^JEmZVsvEOn)A3^Y$KF~Xps_7rOA5?r? z_#W}TJ&t)lGww-uC*PgV2?%-y3{P6jMkzn@`02i<@#ugCnP8manE^QsYCZuzp)VD^ zvgDILjYiqH|2N3=Uo3m0n|yN|A`ixTI(tkQfcsJ|H$ik^6>?3=@sNg%bR$EBR5 z4&v=$g6vpqIvRtjO155lMf`Ug6xAs4s-N9Jt=)>&@6 z=j=p99vH}T>+Q(rJg83s&tHXw+I?u_Cb#V=O2J%?t|XU zae}zN6Y!g z$Abj-C=5lSxddIOPX!ctw}8*z{Se@D1o*tyH=hUZhW45MBNEvi&qlKq?mh?l4}<=n z@!kLFN8|~IFciUbY1Hq`jNrx1yBc8e&gaFwsBaci3tnJG=X@7{D+i zzA;QQQ=jw=LJ$$xPM7jC12pgEK;Iv@q3>ag}JR5TR z{M`)T2LE)|l`uEn6pzr?AD9Qs4z=WZOT$Yp>4qzQg zdlLfIkt5$MmaE6C%f$m*Ul?wjznk(ffMLGJH-?#Y1OZi}f}@Hw9L)@{a#!&BCPRCH z)#0?qs&G8M2Q@rSEBqwr^x*VO!{ig^Ep4zTqU z{m?htB@av(gRn@pCA2Cte4f9{0zRIizuPyTIg0L;>cJSBP0!{JDBMkg{yjy1%6I>B z6@6hbAfV_8Z-#)Pzsom^rHX#e;pKs%FANaR-;H}1z%bwG8^esE!;6ac^zO#CFNM7E zJ$XLx`K)5k50DGZx;mVUXHm?!e3$Wf0^?fA^MNs)ULK~#blM$m@=5eA?R9%&S+~bx zIoI2>cPT)7w3POgW}Y)`>n;hV=&7gly?3k*&^{LHaYF918~kdS11CHKi=k2~Zo)X&{WoR0AKL|K^o1G_ob2?+%Ym|}!o1G5;X5MCJ-#4@6 zMm~G$e6U8wfb!k5oH;pSi59N+k}8-weAeBHYChoA=F@9#CiyZ;w8+pL;y^vX4_*4grEk9UHJktACc5!cpqo=a#!DZ# zB8uWk0Xj4aMPaMgXJxB+(ZR%OZqL1R zbS30HFHdjBj$fG&EEt4ARPLe|_SLVK#!K%xJC=ogJs8Wv*LOeM0yEqe9R-}>hR&<& z>xbLZ=ef=8>xA2)q;7t=9nNrD)Yl2OMQ`z|xv?+3`-&I}o$R6(W^_AW#`gTdzPpD#?c9s-`Kq+@sNXfEKKBZLaPGzYJoQIkdgpXy?2oE(IT}vD!JbjgqkSYyWgxX# zF69lFPzD8eob82Mv2eVdU#DQ}>UXY>zVzf2!5VMQ>y%*igWw%g1Z%ueAFT04><7WY z6u}z3uJaP?2f^E?2-bMDK3L-g*$;vzrU=$3W4g{e@#^=skG`}&MX*LDzjca)`a$sc z6u}y=-Ro2U^@HGTQv_?ghObl8(+`5TP7$o}dcMxPwI2j;nIc%jmHJ}+@%Fa1Hxe74 z-?;X%YahObUc0&X`MnGy4ytC!z@`7v-R@ROJR?yYaQ6}k0Q zH-G!)kKMd?vwJgs^Ko##@R=L`^Nnx0@u3^Yjhokh>-vw~`hi=YxHPyVTzYczi`T#F zx^D1A~ zxj)}|S-iAAW-5X{Y_T1s!4qyLZwWK+#+R@7Yvp>jZyJPvSoX>VY>$Ba@_-yy-X}fGaVRLp(P-Eo|%~{m!^Ze%1 zKHI&+XS=uhZ1)zQ?cVIO-J5*2`+A@4-Z*JDHf0g7$A)c+QfQKOO0#}yJNlEC#m)V3 zowW1i;!syX^$1QF60>(f@A20x;IG-!{+b>7YxZt`&7PVz6Uc<438k)~vHgBEQzvIE zwf+x?c$cAxjz?l*n5`w!E0E8oNpeYSgQ+HU2}|2^LQ z|K&^HwsC#9^RK}_kA5CW;8m0WC}~W3abR9a<28;(S0A+bOvHeO=EZ@n5diB2KCPXI zVU6R*N&jm^$jwjOu*RA1yu=Mt7rIv|pmoaJt~a_~0M&84CXl+n0;-d0@k=AqUO`FY z(-*g%ADt(mhm1@Mhp@=P5cE`oR#RCc35g2k=3LVW?Vq@Gu*r=Q~T4Ye`RH_zh zKXhbTD`i3L!$V4TuzXS#Y#!p zu_Rs1b*#FYWErGo_Z2q6Ho{TUB5O~4lNTx1?|89tdSwz6SFGuUNkH*^@AvydpHA(U zyQJ}~1L#zM>!%t4<9u~W8lOvgG7rg#1)r(^ydYNWToEWwu}>#bIct-i-=o*UpG7W? z6hr$}yaPuAt$4f8?>{=vdL1w!QlRc;wh5BeMF1GmrF*Jv* z603wdsZJu&I1Dy}4BstQ8%LScAZ0kYwox-1x%gr8sD4!Oxwv~n_iXgaJ$z!LZ`i!p zTVJGH0)3WWjZry^@g~on^KN_sPf}o>}tc_iO>j&y%heMP;7%6A1)yF#rfS6;)n7-ec|P&wm^KuI)R!O>-hM>%kNqnz=irf zzVPxp*9NeLo9JrC%NJgLaspr-|JyaZMEwAG$JziccnS{I25`Ys@b(FSbsUf{cnY3a z8^8rm!T#C+E_ezap8!~+(~Dk+-nKS?3!Z|vP5`W-SQqO7{=&;|nE+Vhj^SeS(!KEV zo7V=g#>?O83y~LIe$(0j*2stF2f){_4PcFn7(W2sI03N6WynSAz}Kw};DV>%4Qm6q z;3;@)0$`1!$BUkV*RKuWf~VkX*9LH*Q?PgC2R7cg@rjKae|V#H{kJcFYUih}{l=B2 zcc`0x25JDldG|A$*RTHA#wRY_yI#5UwVU5@^?lnvymfHxLwo;r8@~Ute`}45fA?w5 z|NG*lZ7+EF(g7|L`Y_o^M^rpsSK3t)fzv6vVf2Ey9k7y}el*IriV>kkP)Q8R z)(EM~wQhwZ;99X&tjg^{3dKXYc&8*5D-}X@aARK^aB8U&Z|b9VrokkP{H!w_-aYG#_vHblvE1XP)_ zk*-Ogg+|{h+WT6nn`sV|x{=K%2_~uOsTmA6Zk}K$C`_d^>Q-^B0E-F@Ypq6`3?fyunxm(x@vt%ec;n&S!Du=<+Vqa3ylC-&3o zNFK1IUZ*MLsh09xjH^!lM6EDh;gszZtnJd|WMP$v{OESIPTpREyLtx-brJt86sMB*SYf>nfU>mig$ zjLl9h*Dbc(QhXd?X|d3d8o`pmAdX-|#qxe8U9DD)wnUe*wHn>*n2DLW*!tTi7$Ug@ zpJlO7KPk|$&VWi;bQn{Je5xSBk`=7coY~T2L#fOkq!_7{2iYgEhjH@invFtF5W)kU~eO%8#X)~R+Q3F$$$$Fwrt0ffX|I@k}_ zp@L}dkNad#H_cS1ngw<#R$P1r!;W);ArT5G`EawV)38ZPwT{RzkQpjl6}}TN3?&@pzMuh=Df9Y>eSZPHn+LqMD={8V3!>dd+TW+PKBl zQ%sxjL}_TxVAv|2V8ELr##YIp92i$zB@43^-paR=bcJg)nrW%cW@W3@*2fi zi@V&$?enYWm-mwsIXHALGXidgqGnl5Ma$tMGCX1&p=nd2s+fxAsM#nTZ9SL-H~?PhyJt?z_8*^t>4)EXKxn5L8Nk`b878V8V)B#;iMrBNJI2~>?y ztuBU-|l1>mS>8lK4y$SiBEO4u~kim*r7CF zi)=<G%Ce(E8DAgXXJ1xeS#rfi&Iz_+M7oBvnSKRoV|F8x%4G<%5QtiNj+gr#uF&T9<7Gakx62ue>u_Bw zl}u)4Qy_oRQ$$6}+n$jsJa5 zel6K1*UA<9yg|rO0SyIMg6A7`hYuvvYL#dc5EG@bf~byk2tS~SNIg&yhiR=Aii&z3 zp{mX}$tSQ#7^W+HB{ zmu=&OK~-8pk(LTv4Bj`IeVt@Mj)G!duhMi^oK0!G{n&|4u7Lq+*%3u-j^jz!Ne9Kz z2qN`Cux>Zn<#B^bN-4ZlOLKJEuJb-Hy#543r081#7_}Mv6sT z!|+@Q9U(TKaS{guM8xe58Lg^n-awKlL7~AaS(4G@fNPnMCYCHxDspkK>jwE~#<{Y< z#rb}ZNtIK&drlv|{scn-O|?dqG}$tmX1^WmWGaYZrwq*P)O&rxHS?g>H^Dbck@7*d zn+tclj!@tY&|$K{cF8v-I|tD?C~3oBWuy-V89Hy; zMO2eAZIltXWU9`=I^kpzc2$>hB5T?E`bd)0K3N948v+yrXA`sQ!{#@g!Vu4{AJ{(1|wPBG!t5!Mbq^lL4?DrD(!H}xK*+w~+NLO?m<%yw{7aJ@V#9VWn z7YQkifW35ZkYw8BeTksE;Dw{nZ#5V}stWDdh>uO;1Opwi4it@inxEiHi+TxVR9Zds?tBR{2m~FSltVqn47C z+$d;pIMs73j0&0uwpPjDxEbKfiFmx3=&^xx&?=E(0;_gwRMt{pw$p2e5W_O|-Eu`3 zB?X4A6p0u!D%-O*JStx88PX_vR06e)rAPX7t8y-T3In&)@jkjmF*=_C5wO1^nQy zu=Dxr|L8ik_uYH-y(5r0;D6h_z02-?&CW0HdCN`H{@M{pD2=@*2>uJDb*uIG;p2ktJo5~0k2>XNo{7tH`Y8Aw7}jqS zEBK(Z^^_Btn4%S>3&ppW}pZBifbNbrV^KZ_Jl&rBd$;CWA*01^G6J5avnVla^Wht2w(wyY+ ziGHsyK7-pR;B&M@-;i_jgJ#(SUj#aO=$uNojUqv**z znN@H+PAGuSuYQ{F$p>1+XV#!(B1fu(5>qoCpMUZbKKTS!@PTJ1EGOnzCMoAh(&O`; zKkk#ydsgw8Rdhxp`D~s7bvivh?|RH9A8Z95WY$$w=0zf&p$R$#_tKC_0KrZ`5$Wh$Y1eEtv4C!f#?K5%qKufgGz#E=Xx zd2;*J@A;HZK1Zwg%o?tojpYp$^RXnd;+WZ%*rjNQt>zi&JB{@cy`|8lh4yD z_=IQWmf~_*F-MRYPj0{RJ8$>N=WrFD*_7`|JS`;!DyMkk`IS$9(ib1&_PYU}H!fLN z!?Wu_I>~D>P9zC!*eI1+-fH?Qzy2q_xLx~^4d#`fZt1s^D`bPs+I)&9;xe1Zac@oj z^-af|U0dQDr{$9zEeal=pZSbWKCxAN zX4hnbPi4uZ0u;&-+&KnO!h43aMl>Jf4}neSOPs`sDM>Dn7HDY+fRXG?`#& z+8a;hb-wrvZbty0rEe>fkzTJBY_8ZO6_b+9d(`dkgg=6 zu-P^T65ooK23#4IZ~{RZQYokFk+w$D2P!JiVXdmwN6~l}A)2*vCdfCk$SBwu2Q*{k zT6&&x-BMP{@0aZ?ovW&}>Q)8~VNkJ&3;G7XL~v?TA4tv=eEFgV0R#mvjqjUT71XMS!N`G82g<; zxkZ`X2EoxOmPpj~_{?F~l{=>}I2~a)O4A%E@`EncI83%$oqVmGF1J(JBQrP9`psPR zXw)$d#6)w@=~si;=&0H&h1IUs$)Tw{MvY5M97>hiSZ&+_z7af~-v<|hG@lz+bV1Km zlI{MmK7(QR-~@x*4Mf6NKA)>K>Fh9Gg2X79EvwyHC?J#xvR}YM+Mp0~>%)UmrcRX- zScP}Xp)Qm~(Sl<+X=gZA^^p<`mP96oN_eMD(TV+z(;ouw4!1fM67>X|FP^(&{PR;7 zxN&9>i3jTg4Yk6NN+6aB#|G72N@qqGSE(n$;9@g~gX52bKqV`)g%ldC@kont#!6Xe z54eWvX4Mk7Uu=zFsfjgm&0!?6A5W!6buC?H4Qn8^sl*^RBZn)$e1bvj$_2GS6o>&= zRfw`&(DA$hSuzSWE1_`&RS_X?>8=KR82c7t^^|ICq<5p35}^AAL>CBJg*(MuB9CF? zENlp&Bqr4p`-4XUM{Rc09)d_}lBoQC$SXwk(B+S%%bqNPo-Epa! z2`CDhPeex9VNPa(?)zt2Y)3xfT+RZj9 zoI5iYmrm~g+nkfFCPx}Iu8f*?mKOt~0$0OH-p$)Q-6aeo)J$dgL|TInP^V@mV7IKK zI!IABNOYX;H+3vLtSSw?ok4Q_rV?`O5iQ&M+yKw0i7s$v&4_M^)vM?9;d@Wyz#i%Pr4}}%uGxy}#m1o6ghsVW zwvQoELsL?CooF;Tq-)swij0H1N=tAPB?3IiOLTKtd+l+8f6&Gb4T z*gR0x+7-P{MzwUZKnEi!G|hy~1EUwz?5r&uH|hPnbqiCl)N zFpk#Egy?)XanP5T7(S3-axluI`>CEPX?#8tXLhGOL7I&q!2xxX#EkTk+EW9)*oc+#K*av5Ma9r(+s#u)Eez!Es+M!lqEdgR*Y(|8?B0BDAKOdPQH8~ zOWCZN%|_%-)`h}^5G2fISdSIa5r*TXD9bb}MK~;H>ecdoJ!Yrd*<8tVHO)!2YqU~_6PmPp>vai?3!IxO#oKy6@%x+uHo0jqBgK`>E|G!5@!)z7izx3FBVO%b)V?<;{z& z4>OZL#Y>B0JC@KcY%V?wiNJt-Zk8CXn|9(BQ9|Ow923F9bHvh;C8 z?$HmOuBo1K)w+wT5n5_6t+FcS_#@M;SbV=91}XWPW0)j!7}T$J1hF5(EGL=aYDWy% zHK8Jj;Hr=i(UPQAI*G$Bmpx>LeHFz)(wd15Ezia04IN~O`Pkqd<1IITbF{gR(XI~2 z_q*J{uXMS2{=SMlFp%e-SSO}<-tuvhMb9$LcA?=VH1tvR`eGK2SCVrN^&l+Gf(hLh z*Thu8xSsO_=qp*^x%CPVKCI=TkuUhu|Npc1CUCCf zXMteZbsw)@fD9qWhPchlJ5r6kt|u3ZOM`)*_J!qj%>@8 zA?y?bFWdxXXPAZr$Ru3JKw!vlPM86P*`0xf3`fWT>?8{ezg^hBe7th`s8YSIPTsIx z{rgqdtMYgJzw3W~-^VIRw#nhD5+jF*z zh0^Sy1JC>V`d!@iyl*PHu41m&=Xt(m&s*V3_OBL-jZIpc+m`kP@QyLb9++C|nmwyL zG_Eo^=1A+>|8_jt-U0m2ijhI!=jU*m2}FEri;*FEjLzY$cq%vo(@ZmG~yK z0_5*$gS=p-Zx>{tITY`Envomj1fMLm&_Bl+j$<52ey40X@i=WQDCU-by|j zf`}LaBRvBrJ0Wki=~G7*skCzVZJ$;4GTm^ftMp=MI5vzTh@)x79=bivXm_zanSFZm zn+ey$S7^?6d9we)JYxqHbI_iK>_B8}9xKCu){ksq}>k5=o=rrdpW?9*Tf^DeBbb(KY{RNsg)e72TkTKBawUVH~&OKN^)JmCO1 zD>UzewudbH;Qh`oaL{MZbSvkV6hoT1uny`PlbMrLI#lx&Iz3HBOA_Sctgw|u(=eq= za1iz0x64tbR4W}O6)X#Lc$zDO(y`k*)JqYWz-{uR8%X-X!#FC}*)&>HOs{OBNGb#J z6qHD-dAsRvaa@KI>VCY=_=TwMZ({|lhB~VM=d*_nT<+lDXEq*poou+j$(?h5rR%e< zk8J<2>s_uV-JjX~z~);w*{wU&cVV>RngE2%gU1wT;CCpJRGb~2(NnIsIp=Jmw>0u$2=*#I!q7lw|(XO`p;wc1X z4EtF$)(S8sz2UWJEauhQnLgI{mg?$o*bml9enIv|8?J{Z5Q%Emv`oL+n0FHtgR`2yh<4M*=(<-@-#-^ zYO6YK$2*%=}T3`?(`cl}x`}BkcABETgo=bQF+yloF;OoL{CYhVt3`RTuh)ZhIcRKsdIC{nV4_5a36G^4S(535g#yU^ zWcylOYM?~ZlC56A81fcG?)=&qLh^;%!*sKlD5ok+oL0MZJP;B(IKk!MNH~`v2s)hZ zM75ymKMqc4WVy^B*GlC2#q?-U6btdn00h}q)Qa&$H3iDcXLH?DWi-lb$KN`kkp!8o zNI%y%%AGcc($OebS;K`2BUxEbsS@f$eJ1BiS1TUxjc=SlB${*@;agmskkm|)WQXRk zZUoypp7Ib?5i2LPMz;jYZ7|{E#*`~bDe1v_xRlftxCt(ZIs}Pp8r4lGyfm_sCNcz- z>-C)J-+k!>LN>)}Qp!d`5+cVk^XzibQK8t(&2-fF5qu zJsK3p3AAV?Ad+vAIgfyA$M2tV#Z=)YUL>V_LNF>tqs!J2KGSH*2%StK!xA10ZJ2#+tMtpV|vL^sgMZ4ijnQ_ho=yM4p6N>ZB@d_kdjbGOtOtM!&yN` zQ{k`-vgiy#tjifcviFk{h*G4|ZuhIHpwIBAKARkp!K&CK zx3(q_l0m0>WWFvZ+8IJsje(Rch(bMU=7!O@QUsAp0%gPbuGiT3iwQ&yB$*PSj#}_3 zs8JPC5;TBPWJCp5XDWzPRAFRwJdJ1sq7FXi-IQHmHh|;7TCpAOrjwp@Eo)hrB&Jxk z0Ay_Tnqt}n?|I9SKzU@)V-gZ2)=1S;kE_jOQV7bN8i_Ues$@oceP1(?OQSS2szLsk z)ZflbAmXub0_~~>Sec>ayw~IN42=}Xw57PP6%@o#sO=4~qa4ZL?uRB2u_O+X6hl2Z z8qoA=BA}H;cGRYaw8RB`IgK{!GDQgKkY((AI$u~N+2 z_JovrT~P&I50~rxOv5z^iN(5)#fKO!XFDkmA7f}7Tyv|CXArEal|&5|QE#_ct!LsR z#C1BM5p64osRj5txT@h|R-#xOb@WoRr~n;V<=`mRQKG3Xr=-};Z=OJ)N~F~6*FA%J zn{Hsmv@h9bn*|oeS;U7}oVR~511bd_Ss6J?xQ?+5Vr1<+-H_em{sS7s!eJKzX zw!dTwAp`|K5enwbQK*U*2Qe#B;AJvPq{Osbft6G#60R_2t>bmQYhomDF6)h!>J^C8 z_;St^lFd-42wTk|i!`;qtRey-RU&MpPEI3958MHTTS!A^`+BvZ^WreT;-L;B)^Gw( zcj!?+h!2pE#x&eNF=nc0Hkf{+RQ3hR&5A!jl*%UAmihsa$4hN+r`uGiCaw@vCx>qb z#!MBBVl|ydEpUKgDE=g{%TWUL=6l7ctifX5SC$b*_5@>vI{T6d2|X%Fu#5~u0ZoPc ze6bNt^@mt3;|ao*5gnihvYfWc^#~?x|IGxVT*yb0zJ%IgLxV`Vq!AhS&BQlMY--BksbBLHP*#kUyV!axdBe_bgOV&VT?Ar0W zZ+!8_g*~6=)_R}EA1C8zH2tRHPx(qCIo334KJad&!F0;GGkQPSfl2^oyXf0XxueC0K%aQftskkI%Jx zd7PxYW`O{An3>wfzn(y7o-7y6wB=Njm3a(7gMHvj^?W*-X{e>RVQ7|>32U5y*WCOB zqMbAW*FW zc?Hf>7n6nB-e^n%ED|eaK7f`LI1<$Scqktk4N_GkRb{hikIu_9Yh;;-qEN;@e0+3& z8v}1D%tu1y24;0Z5g!i@X#-HjT|`UOdQ`y$+$Uuyek2Ym;lsj&s}u@WU!y)2C4Fcy zS8S*O%F8j~U?HAIQN+v?bLF<$5HOnBKbSxyVetHkY162$l}j^fmMmp^p*o@WlChju z!@HHJz_A=28sYn!6NnHZc#;KJH=&RqCoE7s02KU)b(6`aKq$O7)W~DFr&CM#jH6#T z?%dzVNv4>?XrZFFDmvQiS|Jq_eo~TLn)l~oshHFnS_qWN*J165+1PM>>*vuOpC@&R z1nl?!H#X`U8};qLmfQWYL;r!x_1(w6apOC8ziUT1s_p&zOAt8y^IM>;x0T%#1||@A z&=cX9SIR{!k{JJNNJL~*!8CswL;6t_=~F!2DG*t!LNr@aE;}R%vk7|N_9+bSi*U0+ z5|w7DjzmZdPXe4Hu?0RKMWHgB9p>%7W8NA|#HqTBf}?q|(mF$8xqQZk#Y zHZmD-q+29ovRFdFYa2v!>kpjBVCuR=MwI)sP+$r%!OX?95ep9pI=4z>Xf;YP!ECM2 z6J?JSZrA0yUx(ltmadOR^`t+V34}vA96Xw0SU#bU&c~xm-a=qCq&K)XA&{`mXMg7nF8C3RU#`SN?tKlPu9wP4-_twS-hQBkZ=m^BEE)#dNd`9r8qS%l!0T8 z9zc;@S1gOMk(?j~b385bqXeF8l6pBut`HfbCt96WtwvE5s;LOMUL)LU#c}UI#4|;2 zAVwk`q8u#-T5uOw*_{VZWYl$8S=8^%5K5-(Q`(tqf=qFu#Wf~txbxv3V>41-iVRT1 z>nR1o;Ocwm&xBPh)J5}jPmb_NF$9r~m0WV_S5kqkXl9h%^T<^34c};_Xl@09u|0{R7jhp}O=C|AwZoX{y9moIe z_+RaXc7N$uKMvpc`x_s<@y;9kjh7yM{OE^|I!B?yzdQWs;X4oc!{NP_5=)2rO zc>nMBKf3?UeSZI?yMMR$@x343>o`3Wc>Z(GOW=74JTHOgCGfli{=rBfyd}Tb{ctaZ zn}!r`jS>|#I`a3ea9mFI5?WC$*2qyDE5_7*(`P9?uL$}c!-3#&e*$QkQV3HM-EKwm z*JGmMZ5S!BDf>l=(tB3C%lH(%QEt}qLu~?hHkh~$6#GK;MBq%ME3&;fU;C(Z|93N^ZxhNV#vqn2X_)=6%u{asgO$M#D`6PI6 zMWuy7cPgBN=oBzZi*T0d#BniACX`5xDGF6|7-7noBIS_|qas{59nEXa#)P8lkEVbb z2{gii5+;-pgC<>|x=Mi?hzv*^Za`X=Vk9F~ALem0-Jbw%4QGJiKDf^yx`L;UV5w2I z#;|CaE%=yHvFb%kY0yJLqAAxJQ;G+FF$HA7`!L+g`qV_NK@-Rj<(mEu)+ce;6OBeq zwKL=+-Xd-x?3AKs3J6Zv{h*(YWQ|(gLLT%7s`M0~A-^esrgO=v1jBtiPvoOAIRV`L z=_z1JEv4c#7i_aSS0`d4BZUu|s+z0^1gVOLwOj@XT4g8~no`_&*Ay@rphks!&%oLd zf5L)$)lra^K(T%BFw8(&Si4*fg}^IVg~ulp4?YnBZsmHoN0z)=x}MJT3Wes7iKB&K z6smAVuk1_p6i+r4_kmmXj!+#K6TrhMK~m6LEa+xmt>B;{L@3!0Fln`u?KdKPG$q4Q ziEhU|o+6x_!nuBN3P@BrJ%ct1;8@ie-AJ`q)1^smi2j!~!A})BmNst*f^{So!HU*3dMtCSh!eK7% zEm?JMQ1P^qsS)o7H4i-&cqd$y5i?MSBU3=v<5St7yah_LgS+oSG(c8a4_N@GoMa#Z zRTZ3LqJ?yL5Y^fEpd(CVb2KH0)F{+UtBS~L7!}IphNv%2W$`52&UN!~4Np{Be1kOn z^{H>Tzjq24(Jht#sC#lVl@nk!&(;=qlO`r>LhH?Z<)a8036q3~4wzcr;8lv9DQC&}Yws0SLKn_)IY z`;k_&B48bc3Y-3DD>JefgPO|b@X;AyUd%=kNY&G|6mKz>wqT=>i}Z4QFe8Acu|m9Q zVSOJfL(_<}`%N>zaHS`N;ILm!GO0qht@h$R9|<0?!e+kRO*H}~Z()#8it(v8?zE4CQ$RnHWV&!&qeEW3lp`cG+mLbyn+yvTxDf2YxDO&m{!pqhi*ZlQ06hYg zi)dc5(CpAisMF2+nGOymiDa8;>0;B*$WkRd6n)b%9=&P`==HUjW>?Jwtstux2r(sT zDVa%^;Hb%CvfKbKZ<1oksG&h|9cHS@nv_>(w#ldL+n8H)N1eFgmjY6_lO@n?TA)(}jjSQA$@QN~}xL2J5 z4k0#+YreuDTo0Xfd+#GCt%Jt~xi#OhJ^B3J8cm1;a(EV0d-5uNgo1Nd^y|w%9-Pa!f z;qea~tH-ar@o8{|@aWP1b@Zc0y(8b&Z*N(v*LqKFv#)&8eJj`D6_duZN>?Zem1u=Z zEaaC=vzR9tkgGGf0{-*mAK}Pe3*BZPzu_GHaw7Cz^RaFA_7za5TD@KDT20y5Ys>~` z)g@gg(>+k}2

    RWn6u5{=oTKkZ@$TIvbor^vi?Wm1l!g0 zpYN8hRWgg&r@N&fu&OKYHc4CDm1bOBi!m-172d1QxN?Yo!quKafWh1bf>xV+G?7im z5)G*xH5)PfLMvLVHS88k?6|V=*PQ zT1^w}9%?fub_llY#D~U=vXYW3be|yzf;V+Qe=$dI8bhbn{_Qgdc1U^Nfe-bWt2hL{ zC~k)W=i|25d)S%TszdZ=wtCo|3N%k3t#(z)_EgMd6P8t#R_kF|p!;7krS25P8TI`y zo(k0|=)6$(zi28{r@$8pH4t7D>i!F+cH$KM#7_2a&Meen+*lTkxRViIP@& ztRMQUpxrOpdQKm``K2!)}L*Xw~Ji{;0+{$%DP4$+^P>DDJ_ zI&}!TtkbPOyP|yOb-MM5nNA(jpXhY!kMGfR7U|}_YxnLwZ8!tI$h-WZ^WFtkW8ZbH z=oc3cyWeq7+0M8>Q}*t=?a7tOD>dQ-%aEO6zp*-;n@YV~DR!hTcxXE*9OQNV#nfz^ z;+QuZ*C%H|*&zcLnGNJSzlisGU4MD4=oeSQu0NYiWe&MNGaJ{ZXF7KXx~y~8XJ$Hg zh+?91*C!s|W+Pzv?o?k3#i?AM0@3@CN1Vfhj6lhdjiy$Xnu=zakJ*jRTz@_bHV)^V zd8=`=vuNND_(H1*F7{h-Gc(>CZYAb<+f(iOe_ypx-ne;s^Hs+mIDY)b-`)7gjr!5& zj=tmQiw@s=NFDsq!5a>~eE;41!M)$vYwm6DTDyBY-?Q`8+rP0*Z2i&JTen`a`Qgow z`?uYO`@r?1u2*jSTY&8R*HfpxT{qY_vl~ZWe=Al zJq&@)fL_1P7xIG)Vu7S$wFP&7-Khl#_-q1ib4*}f3}+;Ka0Ik+R=d>&5>LBNO+e$7 zPXZcmbxh+TO$On`UD+8IFi2AxB6U<>;G%G9*i6_1eT!ozCXRH59D+eUs)psY1?1J! zE_8u}8jg|$r8SC^M0c^e?b_gek5DiINBmvgN)8tYJ$0%BLSuV)vtvRl z?7=4BGYpREEf9F!X$KG(+ryh26If{v!7vEU4=-V#FfRfil-7D zXb%t^4h#&Yv{*B@bE*L3kKd3tj{2bMbJgI6dMA;KwHtXhmRNJLvAhs?So2b$M6PV6 zs9vZj^~8#aBy^gqHGLj7?SaEFly9U#{uYF?%4rRdyls>0IVL%085}+^k#tuCM=(An z9x-7UDk6wfgjEeQ)cjB|tYJ_I<$Y*bJ*@&#KAV)~nAABNF+B*IqOG_wq>^$Z9;SFt zuGWQ8M4`%5C>dmN_Oy72u7s=F(?_SWt*9pfHPbP*i>>ASCdBJ~xT--d@FKuhl@hHe zs2iTqggzXngc8xJR=Ob{$n}>ieIBE_Lgln#vttk5aLmrsZeoLx5DoMtIv4D#0oc>Z zYpf9fbu4OJis!`?0tdjYWw0r>wCZUYkQqmm?kz-&>D|_#A8p3iW*K~P(Q7Y{DBzGk z>?gBTe$n`zIxPW0=z6>JD>ujai5k+%M<6;|GY*lqHq6C1`L-{z9W)`?8oEB{+?1<8K%)~-O33%bK zcUa;2iwgny^eG#86xujy-RgjQ=C;C$O21i3fQvm?q zz8HAUK$o1&IOTz!9(e@J9Iv~)=IB(kEBDwBTh1Sobxt`zb-WZQI;Oh9p}lyIN)kQ` zPngC)Z^H4qr;0FXDKQACCe(}MJS3>p(^h&aSc+r-$?;O;DaRz|B&Ssh@F+46giFw11!%M+%c`A7QLz-na>#|)k*z8&|C>6T0NNfZYiJg=nErN3XpzLsQT!R1JdI9o> z@6|0mt3PhnOdkJhT84I+qj8yT%T$Wzf#$2EjwAcDrt$h|V_cwgWtG!fIV1-;f3MnB zPExh5@8qQKRc=$AbR0%ooC=GXN_fE2K{P^6s+`Jg&<0{$Liqx6U}TP}K`-gR9`UMT zuH&iB{^Pk4dT ztX_tyu@NniVL?UwEo?=_<2!f0@r8F2b{X$?1YB2{=q?Zc3x$o>_Uj0*s$+bGdBPU; zRSJ-gKY!Ks9OJ^iXSV*(!&jJ~&($$LztFIGnJ-}cd1~!3UFHBod*Y$5xaTbJ;=Rm6 zPtMmj4gy}6>l>f5`ZKe(?L=r4$F&07CGAny; z(`g)LyMyX&m5Lx#!_2~givRx0A4g(UWGE2^qTw1OHxdFBO=qcYx8JE|1x*Te2YEw~ zb#GNxQuL7bMlyaqm92BN+zF_kW3Vk!QMz7GR<76SSOrl!$#E@bjV=84&_Vg*Z`{61 zj{WjQuB#Z^_4#G!e@ef+!fJMvUoJ1jzd$a!f)Ow+fY-R7>&$q%o6RnE!T3wu@>w{d zFXDx4$6q-^8&<8B385V|Am5JaRE04DC7lIDZrg;n4nZs(QFw?C_EN$7Fa7U3sb_mx z?1ahT4KmSI;+d#QhD@j$Es!)tCg==b3ns{t(d|TrWv>?$ehgRRNT9d}-#|)b13{QXeB5Eiay@(n>$*Ni4E|5)h*z1x?e*rus_blo z`UP^pdkomiv5r-0*30Mo&hESP9Qs|@=U+Pk=4+{NZUp(frXOn7JEGn{$(Cy{>C4J! zs%O+Ytu%~}P`Kcqu8h|$KF2fh{Mu70rYneMk2p=|(@NPSj4WMhNZr(6z<0q3fYyv- zBed<8_%M>g0;3k`)3n=gS`|;|aIa8~MRcZ?Ohp-@->qtcoR$X#y;F=zw?K|Vbj1Sn zAqmu~KDQ_F*+U0@H`)J}HjaLO|4rLHcj+JO*6;a&eSswK4KKKx-epg)i05_yT-ai)h>4T4(#3dq>GgySaCtkx#glLwYJNMv*tZB8 ze*pAfG%xO3w1jtlPvNj{(YdpTduJExG-We(zxxh9SnvSwmCK!4t`rYd;-aJqXgn}V z3=PnDAPsWQni8mb2>PsI;b^2(B zi=MP-MXON=%vTn`F_;}j976&favMVz(u z^4mkdbLZ{bcj;Xgdi-0oIiFZ}bFk`MYNg5BJvlEo!Nsc!TagQ10b1iNo%Dl`0qo;U zb$jL#`l(#R%zDH1MshR=R0b)~JS1l*7F5eyCt_FRPU4KWG|Z+!_q8W&H@onFEa0`* zzq8f0-OusdmUnfM*YY*YWkxygYLhD~L#cP{@z@Iy8@f_AI!$RfZJPPey6YeNqbnQe z&7U)@Jl&GcpXUKlulm@$sIPQH2PFNRXm4TRI1!#Lh2_=R>Nv8_x@{aQ1Hn*J(4I(y z2F+liTBT1isIRWcvNq_ONHrsMPa04tTNVQ&OWB%@{ z1Hk3=mmdmv!*}j{!xMKY`#|~~FWz5vxmAwu=s-FIfk?3I(@S@UK=$VhEgwe0aaQ31 zpyfD`?3Q3HX_P}v1|IsV?Sb6Ni;~b$N~sE1VqWgk{&A3>ce!<|{m+X}T>t3L8TzV# zIUkMh9RRL!_thS7r5J0+F1ceWAeh-L&CZ2S+s9VT)%Q--FO6CY?tf2_hK+jd6ujox zeQ-japlCLl7=@|mkf_6jI7Y{s(TJ5s3xzg>M2C@pDL16d;6y7})NQ3(r9%XAlJ_-9 z!ns;ync z)h^TCIbFZzVs2(OVy_7olzJA^FExkd*s{v1&&8Ij+%w%zY*{g#+iT!+KDuNAw)H;O z!N*?vT-8JRSv^jT7&OO*l3vSOFgp=JtppiGistEb2sQLrss)9(W|B8^Jjn4pd_xtt4w{7gO;F-n7y{%#>;`91S(VDRMpyrlNwsS0F z(rgutY1xrrGIJhoul}dEKxe_~$!l zxDWZfP`WGOiy5bm@lWh}%>sDoGAVa@$L3&ugn$EYG{p$@1?f2EV6q`DpS}TLetaHt z03;$-OT+wPeFn#=3CqXtPn^CUP&udnby**dsm$wt1hAje|GI<^$Jo#7e`rg8UjOS- zIUM7^SpSPTB)QXp&B45u*5!LR=3u7(KEOP$|FsDoj#Cj<>Awf4%Mfcc#M*Cli~ zPCZzqe;rUcr~h?X9geBY>%RlApVR-kgbv5p&+A{ar9ZF#b*UPT@n5X}`M1vYl$Seg z+Z@d6e_cL@V-6!)ZQ=?E z9FFPCTgTJ;rz#+2o5vM$GaQq;*ih!Tr?#O;r*)ebFzhQNa5&~=7BUoIjPoI5ZN`S< z+=698dE&GNsLY3qwP_oUshpX|lc!aHeLiFa)@5us#y)QpZ#tE2hn)){Em&~l#I*odCot>zqhHKx8w0HzI<8&?9Ut7 z+Vl*^L{}M^1gOj#+1eZp$5iHx>~#S9ypgR<&v1&)pB0NQqnrPGMD&{J98bAovn9y>wkv|u9zEG_+MEz*hj4t8rB0oVICcHX{|+l zf7t%^?a zHNVK7Yxm(@WB0u~zqA(y=LNrX_?AQB;O`E;@1W%V4cGf!f8}nwp@WnCKi_|5zq9{E zdq1(aruN_c9vJ-p5N=wF15cqfRH8VNq8n5JDnQ$axgpmg=r-arzp;>_wXar zY7bg3b%{;J2p&bqaiuK!cI_fMl->4t{GYP^ajpA>}{f zlbS1-RxjXPq%!QF0#!&#^5Q|!RG=&K!8U*4o?wJHXr(I?@_=rsT(+aHw)1(x>}=f= zj5FYi1mg=V3T9{TTG7wPY+LR-oA=Dg8TV)Aw6lHBn4N(xM~9uAds=sfVxsjOw^Myd zkAhbAvekLi1eM($9baDT?9PDYW)($uVl(b5?`P zDey(ghW(3k$mU;6ZRvW@FXnn|&T4QuMSrIJ%}>qdeTPz8Hp9(X87rsM=j-%s&I))r z1)f;S<|k&#M$n8}5iQVxf>KgkpIp2VnN9}hl%1bx!y)j6%Jwd4BNqq$1i5eFS3~cc zcOyGsZ$3F&P&q_2r&e*MR)?S~)cV>hiujybpO`J09MYevH9HFc`XJUa@(gH5Lg*c? zUm0HR!@EB<3)l{UFH);Ne13KY_nL=iQ&21#pMlpDBCyXJ%80L(pYq@BZv8 zU^_%H(bw(=r*-r&r(pJGxDJZo700Wy-JhPV=^VnZT+{7-Xl6AISBB?;!M~ZQ)gkaj zRs;K%g29JpYITVIOs$(gKU1qi&}Fr5eq^RrhbX3Q_@S9vor0ZL>*miL>_573;(Fo6 z&i}R}>}+kn{~&wgjW;|;zrWo*dgsyCZr|Ga&8;_WJ$l^Q{JqV0Y$n}*>;7SP-o1PL za@Pl4vg?I=zqj{}y|3B*t1u?9#~u+-YTh;RJ4Nj8%e3IWXMd6Scrh` z!wV5EUg6x*N8Q2*smswC!Ie76G@DsOcyJ-Y#n}lUw7M!AZ`Hf?zA6cnl3ql(cM(GW zqO08Exma1sW2oMZX7G3>KIknX+`SOt;;r>9B#l?}Y?6spDqX#lWYUWjo!r|OB3x2R zA4ORaXH=GGYsd)e;MO9CTNff+Qi@g}GD8$;&{!9j>xiCP;?Vt?ttX*%X0y? zFVyiFp4)XHN9SibfS{ZyRh2;j$7o8mT?ACYs&m;u93t-(H9?yg>gJGtzX8&J1K6*XyZa zmdh@D!u2;7B3xYU-?ErNJ`qn0V{E_IVN(U#SVZ_&7b09TWVX?$v`4*4SB*iV+_5iYsbrL`qppSTd= zQoOsgIHBuLFG3hx8gjCnr*m|*lq!n`nk>Z0&Vp9mFS`)o(vT}?72yiqYCR@%NnRBb zi|rZQFS!U|V9C`+Tshf}7NRkRB((}sUpo45zwknYORkmzO^3{KywI&DN6}u7=r6jO z`-?9`xa4Y0JRu|s1HHf`3$iJ<>PxfQ3ob-BA201a*R4`2H_S_U(40%DaEPV}iv>*G z#}^`;_pC8Ov6ZpNTzwGhNIEWKARH`e`Nr{wT(5dC*DMbfR~HF>>CNKh4-s9Tx(H!# z$#@G%CeiCg3vnvW;0;5>%8QzO_-Y6-E{1j*dbQm{*%F~q!eZrB_g7traLF5(8jY!n zLZV1M*&SNMQjui$Ll+`k@`heJmTxzrT$L{w9G&c%i$${CuecE5k~gRs3zG+GY(!$| zWT}rO*hLw>{33))$EcSdTDt!Gg$S2|C@r-Wx)p6_vz=BUlj!n`r3_tvNpC&Dy4I}Z zm!^$gf)!ioK^jfcHu$zDteq4RA5EwoLfD`%|k;tO^6mtBZ(X+>|A#j+V^ z>QZuKBx~J#Q&y*ITYj-nt^8dT^xkTr6)om|-`QtSC-!90voY&(dr+q$WDMpmU5F+5VOfL1J$BfOqj?I-)k$6lC9o!mxA#b_D{Kogj^;@aX*O#HNjxaVlmSzg z>)By>gvHWqQ$(3q8|wzEWXMjr9Ow4lBeAZ6?;;65-;eU;ES;hxjxkLWW02H| z5r~V0ehEz~wOS(*W8jiz*rTwsm6o%+o_m@LxoBsHt9FXkd_LOesSv|G2JGbU`!7s$ zKK*Rk$Ykx)L3^yL@-*%3$=Tml*K!L+{C2UNm8sP7s86c#p+8;i4KWOcsN3OYy5%dI z{#wWv*9BPO20t!T78G_W+9xeeFSb%^udPO)YNaa792Zx8*>oquw2 zm$gI9KOLSAHNjO?uCFiDtSGDq67cUi%mw8$=HtKv7iunv1J6l#vuQ5Gf%yxdFHj_~ zKuOn0J<&BAiKWM`AY_1ceRsfU#<0bTT$QY*34U!?JsF3D3Bu`>_q5nV!2ausTkr|G zv%f9h9xZ-ip$QiF^E~cJ-HP!qZ5}85EHF7qDL!L-3}2GkGgOUwxiR}0)XI~}_UOhR znlxGi{~8NmmTqiU^jNMGaV~M$R`1$yxzdTxLLwmd(7LjiYr-WQTpKRoy5Uo`f^r)p za_J_P#gK>;^VVTpZl?NhIYU!5xhA!Vy57IH40JkdpxRR#0%l5Nvg@yUs~tM*=nhm;%>uu6PJN=3cl zE-Qs&{Yct>lBy8Ha=P2&8Ms2KIi=&zTDn{l%Mj{p)npHSlK+3O5#Bg%9{uM-Z2!mh zUbgf0t%v)oLm{o%PCZ!^SG>-$=M3v%e1A2t?Q@l8&M;)UC(@Kd@dSxFQ zOk=e^g^X#yV;b>HpiBrghC>w*rkiCSWz!l`xiKw(JD?Ugz(YLI=W=u`qoM&Rm+_VO z@{o>`Cx0*A(`Wl`tK@jxye#=AojX+}(>*7AzpYO2veb)x7uxlz7D z7i(G`&1E@`tW_i>L{(~5~+kC~zD`?q#>@ma`w#$z_7Uy~9w)auL-_?5OANVgaTy26K|l1fOX?-Pin~2 zS(<=RMn{Sg)=t4H1ClJ-L|#BNf{LZ`q=4qqj&<^Zj{%*am+55jVD^F134cvGiPHI0 zGn-8|25LG7c?(chNJS?;b>2@f!jQOOtDYzgk%UrjYDBIYwrD?;NVGJg5(G_IgP52c za6+EdQu!p!;b{yf9qZ)#-ve}lT&9!7)9nXJC)hRVgz&bK-fV#z8Wtagg-ycKz%baGwuNfgrA zp(*A^S%`vaK6V6R7gk%Llc*4cB?-^g1t`(3Qlr$crK^mnM+h%XvsOqF^prV_3{v4? zO(Q|8DTGf(Q-Wiil>SeklUHA+lf`?W2TCW`6}zxXFWaaPS-irebEKb&Ra!!Q)f|uV zW)<^VOrgMc4PB<2xn7Q>5{Xo=*-U1uxDpl`1%V3_;C@Vt^66Ymq(NhO$2v*B2(T&9x;c+Uv-j@RTTc%sgNxjmB);Iv9NN_>G6vyBxxLBb=l zlNn`Ni6%kn4IUi?2?UB5O_bsVP6%hiXrP6nT|^-f4J)uDjwJ+~cdV1I{@}R&|Hg|p z4&Sy5Z@nD+F!=(fgW1K?!DmsJWZ~Y{E z*_}z2+JlUT4(jK=;I3eYf%)DIYdzxf>OfAz0K6OqR-EClIAWhvrQ(80wS zeV5;Z*r$E(I^4gX!dCN5MXOTN77!9@X#9CIg9$m@S1uwszlQ&c6pJhEK*NNvztb0HQOk{o?l);mz zo=Jp2Wn_@yF3=f7m(%=hHjvLWOSdB#N$Y3y7*rjY+Vthuxv6#jxb2=MFIhF%p=)9Z ztFPU|;o!Z9HhFjeEd7c38{iNe3g?=1b20gBVQfiR7{Za-Mz@I}w_$%NA=f*ROo+le1k36apE@b9tFGoiL>b)W>{<3(zhv$z zew`ey?J7QOSF!&G?~lC2w4Bze;dtnVd&mdx^*65bF*spaq?<16byy|_{#aon{Sc)Af6%O<)}gUM!7;c zk?w^{*^Dp>P`(~14(d50Q>nH6_r00{7sB;QGKHyex-+V?jZCi{4MqoacEDxYu~O=! zRYAgalvbohHWRE_upfa_ky6!+RtO)}4IyN$AaR|#cSN_#y@Z<9s50hgh3~hAe&^1? zrhDV7H$J$ryRq}(oww|K#rB_Uzi+#_4Q>6$En_RV`S+V2+I)KRp^Xn7zUbg12j6h; z%KcBef6evduFtufuFS?~-M9CDXuq=W+56<)`}WwIzj*V`&9A$;vv<7vTf63NXzT7) ze(Toe?{EI#jc>WZ-gwE;?;d^k(bpYq9RBLz+YjlDr^iPPhrc^33~<3~gT+5h%SiN7 zbu%IMYCSqJlG+^ETTEl;1G1#6t>zk znK?u;k=d3yGXqkgN>z3wmJ(Wtsq}}73H~em&-K_$pbmjA5-8+dJi~W=-L;}$JdSon zX8Ll-{h7X8Uo+E}L(pY?xxRL$FNY{5`f`P5`idHunnX=YD0N$fnrbX2{H@T}!RKZI zbqIWszCug-I{55NpbpWW3H0Fqo(a?;=(0c${%R&rhbSfjJ^1f4ff~I$pU7)gwt*MQ z(U`H=MrMUTH-Bj+P=~-52^97%TIlA#oe9(-`ZIxU{=1n#9fB?kbn{oA1SQp;aNp{; zyLD!iZ;({0!qZwJxtPo7V0H~HK8)J<;B0ITv7e1?uO2`+H_4>k#-N zlZ8U_2MhKh-TnPDwK_z9rdId&&eZATN9-2ABp zOwa%Gc?mo(fd^ayGk145?@io&^A~4<-6_~Y@x71E0=q-_m4SWpS7w3T;jA>jC)@Z* z`~LsM8*kgVSvvmE@oR4U%Nxf>?Ze+Z{OW_Z@Bh*MV|)K}_v5>XogdqI!FG4+*SGwe z-w7%Jz82K}dpQ8Pwy&qZ^xc~`H(cy1K{bc{J=ga3lY8!a8I+-Q9n{V(ATBR+<+k6* z|MSPk-`}%(Hy>Q8p7rz#-|fD+u|MT(-{$O$v2~5rri@LDwzjyyv`}=pjc^#LFo7<;f^6m|r%iCvMZk^-O zXBZsSTYc#iiVhz2YrDWOMgczhmxRc6YSiE7o<(n{s!~D=y`4>=kd`v-x|o?wG$X zy*pg*73;d~P5GN_OD=DGH};A*{EE%lorq)3UU+w~)+^RE;hQoxohvRCna$DQ>GAhJ zbj;U3zS{>9lh4f+SLoq4WpTxDV` z18~akoTpsM=h#!4<0#nruwy>I>aJz;d2ar=LJz?ypXYq$QclM{Q~yd^qUt|$%;_uc znl`8B)>l_(K{(}f>OGh8IQE|E$7~*JuXfDi%kLV%d!{^2R$5o+OgLq6>PMHdX!BNn z_4xZI9dr25-7b*g>NVHe#)ebIR(Q{)uhPce^ThjYE}#64bzCl9gMisZx4As$J+mv+ zwT%&{T+aE-rQD5uMnrAy#2j=ayRzK*KF9_ zy{5m$u9mp7_y1q9@y|AHp5FYb<6k*$9^bt2o*RLqUp{)`@XrsQK78T9zc_eo|EKrE zdq24s-2K3=f9L%>@b-^yV_UzwrEdPeo8P+m748qZN!NdMX)ZTFb3I?`Gx;r+WiQ{2 zoqgi|`)sSv$X5=#MUjKFJCjO z&+u0zwr)}Egcx^KV(YdpPKafeh=sXmXQVI817~7i}W3Ec<+2|~Np4d7e zor&?;jEuf2v1cO8S^e4mZ%{)xTunbejnvkzZk2Y0>g#^3l%^2!WcVI_ZgkMTD? zlejViSJ;<1VSpSnaD`3jb*mtLWd^QU1;mvZxMmf^#td8m>2<3BzcK^YtO9Jzz!moK z4__+->NC-EQE%-Q_+Hdoo1=Gm=9u~ndR1axIP4u(xc*{8Iw!=Cs}chdAFlIdZt<0i z6JoD9PYg_pKC6UR8tYaUfoDVP@v9O8Gu6sXual9z`l`g9jn2OAJh62`x*lu%+N%1`{7nOSqZl3ZUAd| z*sxT<$aXwmV!H;ZmvlNGB)EhSDtZdpaP5{#fD&zaS+PK3b59kBB)JeN-QP4ps>zjz z6wy&NmxH;T;5I1eKUbkG~P%D2BxaU6D-+cVwr9x8IK^hD!oFxn$X%h*rNuQqFFQ;CQpP>lPQl_P z)0M@Cto(#g-4iiYYLKj^MMbPxB&eZ7#=BC_%6l7hzM6`U+JiXPkSj^=Eq0g^YPCU^ zZwn~_NgMLwetF{)Q>21ej9~c&5wa3}s++SaUC&^c?J8si^@6SG_-id6M?$P3=7>Nq&rpG$x0LUPvpT4iG%Qzhd^D0z zbz;>Td8>u89YP}OQU$k?Xug{6FRO0dJBmRO`7> zyGhWLU&sfW1aeD4qdhLKngs!8nhX)|i3>=dm@uj#RIwkA(fzz1i?bXV3oDpVu*xM4 z?rX3&Q?3uH2%Z#M5Pl0~=wVWC^~)V%K$eJDVX4=qiEbevo;0=Qq%8Fu&G9^T%OW~l zNfR5jL6%PIv@9&4Kqq5uNiCKc6toc431Bg-Ca@JGpH8QN8xQ*x4-=?*<#C`*(pyLmh2|D64U|iA zDUZ+smr^Jpq>z?N3M8cARRRSP z#yiiMLJ3jp_AQh3I>OCseBzL01onl^-m+wRulpDU`;0j-N)P zvDW#`qrCTy0eAKKe8J5cTF7L|1RM5|Rup1nTy8OS#U_KeKS^ilig_?IBCAge^~_G@ z?TpF>>+uhtb-P9d4l)h1*P2DKrAKx3$0Q8hEt{o-U)vw2@f9_Y(%x4$4X zrxVmb4%sHN{s~Zinr9K~R3O)iz{cT*zEE=}A?kGM+G1`~>ayn*SxX}4cGrABPrDf# zHAj}%Z`ACt-(IgQ)IH}prcz_s4ZsqMEaubMn6LKRg|yOehyq8OB5W+v@)(bdDfVC* zH|s4T_IaBE8G`B{qwd;(D(5`Qy%ldLnbak8b2##*wg+J&0avqWDp>9$f|mYFnj7;G z@?f6L1-Uti&7>txEZ2rC(GOr26RwYypxp+i?irgWz> zsLYW8#LQ+Eg>`}gJ#4$3RE%rV17kuiUDwg1WM)QI(qB63On)|K8YGQABZZOewFl)w z6)U&08Cd60NJQE z>&6ANlA#gLc`^yn^hlVu!SSX^rkKif7GO;E(ae(jLXb{;vtMsEh;^o0UjmMj-+cXX z@?+^qKA}Q$#gH_CDJ@Ktq<%t$LDIw}Qn)Of z2XoF=S4$?N&0zgKZEl`3EtN)fx|O(r0@cR#AzUwLvPBncZdJCark>GQfEQ}xv<8?0 z;XU4&+Og7?EWXdIA3`_ZoilYaN|?KqDXj|gbixKJT@^-?c_PZ?wgqRdRgjILHd*m5 z_JDC{Hkhr#B*Cdg+QUtMUH8BLyrq>zgqa`{VZm9!a0l^cp&5vMKdl#7v`bl?5?czZ z1BD^T2Vu~cxgJYvlrW)J)M~iC@NItQIny>;irCPeHKVd03uKvh;Y2R9lBC)&?f%>| z5wbRz^rjQ}!ANc-hXJqB!P zRj=PEH)d$GjJf&58dz-0e30Tx)o)Q|)M}@;Yx4N|7ud#^=eqB8C>G?da%p&w;AWYh zIyH4s;JO4=W;522I3gDsFF8 zf(Og2Ke41J|de(9Tc__}uPSV1gdSznuEgzi0YtBFtr<^SL4A%r%`PP|!**Vj( zRi1cZw^9qjQlFoKY|Bg?2UDq=ftl)POaOAmOODeon$KT+d+X@@Jr#MB9cVGfC+Jv7 z+!Aq zy??g%rag7<<$IsF{Z+favHL^2U$YzSHg{jR^QSxS-}#oEFWq_d4zhE1`&aJ$^xn_h z`;Kk$-W&H<_tblTWBarAx9)w?$#0ze(8<@FL?_(I3-A8v-S^-9mb+he_Y3yF;Vydj z-u_FEf9?34$8R|fjvL1h?)=f6pS<(UcfRD#Xuop@zH@x^%SYdL^i>C+e&iidN1t=} zUk?A>wtM)t!!JA>96o+{a8Nt=(82d?fB(VXKbY%zKo z&1NshHT4yfu*;b(%$v%Rnl0A5`_eO<69o0ylX9FVwrYAt$w9(; zF(`&RTElJp=Gm}V=1fl0bT(iGGYwa*c>Nq<o6m4F-wgvdO+_&_tw@`$)`w;D?Poa0)Mv4l zI?J@>GsBcL*O$J{uRp_imMSqCqgsoh-I*JDU0Lhz=0D2iK3|pX%{S+ANmgP<*?iP) zj`Ub}r&rYK4QIIdDjcowsZN-5R}Gpi@e1xs&vA=zCN|t@I8A4Y#9Ql`b2gtm!}%k+ zv#jB^P!CCciq8k!dOubvq56|s#RH(BWF(CaNTanR35h6vUN<&`;3 zsk=fJwMI;Hi3e&nT;GT`>p4#9QnvCV5;~GBcS)y#Ej>-<_O>up{-!#vL#b0?Ng$P>0LZ~a8658v>edCOszcRg4a&$FI zR$M<_Hm=Oc-_6-Q?_Ep|d%mkm=uGPK-xvKKIWEzhx*tqy9NB8t7)xKjt=V|jnOHAg zr2UDM87qOYn5L|(??d=GOvf`atI@2X&`hu(vb8bp=TeQVKGDp&;rrk{;45WakvCs` zj*Dr|;jF&7YNah|VXUjl#{152ZemIeGBW6S-ENCga&;5`+_UDn5pQ%S9D^se)9vD( zw!VqKJZI;dpa5ZJf~=!%Q&k;l-9)ol%-I<$rZe?wI8BG6M8p^C8sO&adJe&jM8Y-d z3o&A|>`E_wHpjKXX02`M@ziKG>MpRTtqtDBx8^uL$^wOPw58o%w7mM2d#jCnReoOk z#m4WSb>sPO)X&6%du=j{qm`uiL@)x0!LE$TU(ea`of=3-%htt)IjN0m6uZ`qdJezS=98gK#9k7u?`RwUSB?unF|zt-Qd8Ao zlk>gN`gv;OALiY#O_J(+ooF^dDqmSQNHP%D4;UNoKgZ3a zhzACMsoxbl46Co7e>Sxo*Kaynf#<_5t|EN6Xf+Nu^+y&zqZSC-m8ITQQ5m;Tbx@rO2A8{f8Z{B4^* zbNu>a_V@*NfB(*}-uX`OP2Rrq*+;*5^t}hoqcoLB9lR;0h2CKaH*c7o;ZJEk8h(1Jc^y!(m#Q6 zia}yQS(*)Oq70{+6*)$=(ZNRJpuvk$#5F?)9l4+~ZV$QpcXNud-9-Axc#ddf*j&qzm-ALu-~QHV|%U^qPuTAZ`G_QEli`|AVRE1*|SzwY%dyQlXP`Q5F)iX zE=?wl;>tG<&fkY)@n%n=U2D-4+(HY~(i+eF%qurt)JXW1HO!E9sf;h&bo*O!8Fs(w z6Wt}-6UhZ+uA03f>ZxLbD#{`>(5EzT!KlJ5xuZr~fBT%GK82iR#4qcua=jS3C9V&r zLZv%OmQ*V(X2y^s+qxG!`2LeQh1s$Cl{gwlOJm^*gPFP_Q)mWmB$b{&3y>rvCMCZd0fV-H%~M_z z7>Fp?LToY9RJ`+F&ne0yjKOthP_%o|us9iDZ35FY0&B~B;B zW@Su9WpD=txvqyTM?pm&(Pjgs@a@BM3I*<(e9zEYpj7>^?E$YHh-<@<+I4CTrm@i5 zeQMA)28y)zft+GkYD^tO7udGM(Y0!&)0;I;-(>1RCe5yz7RG|!K{HrB_{TYgB;%sd zs}8yqT|oQR%$|DjR9^$jdoolJtu(Jadh;-oT4`bn{tf#Ol`GV6wpz*QsGq= zD)vJH!1C+D+VMWD2c*rzKTpVJx`;M~;*% z-8omp?tpVJF0rkoP~e<#)yCPCz?VxT6)|Ex9+``2MzhJJ&EI)RE~8Tg{#S!V(nh>7 zjYho&Ni#CkgX(h0j%UTWX9tUUt3ECixBfV%5Rta**nSg)<<6px2WuMVb5;R&y3wQq z3`;?ZS0-e)zLfTVFsEpxtE{LGh(euiRP}7sU^xUNxkW(LRB%Ot79Cj~E{8~CCLR3i zoPvkheoLa*vE@U4XHr;<%N*q|P{}A!6U!qd*+phZ9iB$VugNL6eqR)Z?a~C;HWko5 zkDBufF`O+7d(u|*G^Q)CKktvK#iP$Vr$92gQW{H;p-q`~6-5UTk5P1q6jXB=x0+>9 zuGgbluRz}Uk(`2|Krg`eBVCUcqS|nq6>tp@wga+M7z-2qC$d|q|K8TY{Ot1tX?=mocXrg#qtLiYrN(U z(ELI93PmMhfnjOq2FrCL~-5oiTL z^^gQu8*#n}b;zAB&*wimPHLGYjH;D(r5vg$L&jQNt&U3whNdH23?18W ze5Q)xNiG7sj|5_XLDZ{sqh(fUYWC2Gv5X5wv!Z5Absb}tCBLA`M{md}>Si`nYXZ9B z7ffqXc3LH_st7}poHRHhsnx1JJM|%LL}FV%mQ&!I!0}`~Sg2!k+^J$y8Eq5JY+l7! zybe5jM!+&=G>tiq?|w&4QT1S8d!u1+q0yVp`nu!$HWS2S4fxO~Nzk`4-Y5oCMA}DR zeNIswAQlB9SY;)Tn$6;H1&Jxj6)UPgXXoT;MMNw{;?}3+ucO={1VSDNBHmUpCMrw>wmzzHUE6{QD{R8!Z5kLOfmf4mm;J5B&M4reKNP|B8b!B9+zLj^_P2vFAU8hUR$}W<&~NcW^wkx*Pm1HX_qXUF{726oCZY;PMbPLP3nTB znXym^r0K>hr8+d>_gRUBy!=@-LkM_lwupiFT!=oqFXRCseIf93GJGLP#urZ*^=OdL6J zS|f*WFD{CMX1#BtEJOF|#jcwW=GOaj86Jm$s?-II6F^NN5-y68UaQDjn=D&`it!vT zX?9`NLJ@5{KYPHg+gbEhh2<2P))8uGi5{uiCa*yh(it+UyIPj@rAZe^b1Od=z%ICa zCxlKI3>zJ%QJDp!DufDR*-88&fj1!yE7i)#oNSxN`GkUa=-W&+%|V}^Uq!s5@)u6X4HH;MK&Cl}3%(ywOBUD~%5snhZPhKCRQWn#Fe` z|K8nP#@H^Qti(g}l``y7WsDw)5%}2Q`+mm?D`leBvttgMlZk!ufwR4!3|-j^UYS2A zU3u_|{K3`Tl(8qZa-(7Dby1V;8Mqo~Pi?t15mKH(E5#G#oT6Zt z>^{at@O0S)n|)Cg)Tt3#o;oSfx*>LjDX+POo)Ddka*DJMRlEf?4!lh2Ehb_dH=zL$ z!>-a;piEql+jDv}#VFi7-pwfz)gD);1udl;QKO-v9T%ykbHXG}kNudV(vX%|=~Ql$ z8~-Jzh!xC|>g1?9Wtv7vRkO*IZ984s1?_J*v+9JP^dUDKm86aL<`j{FfZLw2$FY*F zD+}Ei6)~@4!{xFu4E&%q&%28Qo$yvQi)N~ ziZ!CD1v}_9a9n~$yIS02CM&hOrN z&z-m4$-wQwOYUqOeemcV;P&9PN7&IP9RAwjI}hJ-7#ubZA36BLgC9S5`@t6-3=fJ2 z2m2q||DOFf?_2w?+<(E|@9w>4@2z{;UVHB)dmFnS+sG+~Z@MM!jkm;|S>v8<9|}V#ZU+4NJ@@^; zbD7_@xW9b`SFx78`MM4I{+C_CRX~4{I%i$6I@~|A0zT8e>v3mRz-Mp}7-wi?Xu`VV z#eL$E-0N|*i@0(bgE2%5o6)+R;8Q>OG7g0?n8`$C{et~d&+f#ZZeO`vgs~zAiXyJt zY(DkOaCZyb)@8ZZ^4q+GL#i-Z1y^9?`d#;@Zrl!c|G(W5_y1kSVK9m@8DU(}tN(gS z+@IbO_a~QeD2$@^V1Tb*JKz7~TjKucGOhxn71^+jb?fr|KfH_s>F!|=o-|*#-QE9# zTjGBImbl-$j4Qz?G}3$Wy8hk&e{YHV-CN>*=Q6Gcqa`J&JL{&P`@ell+;3gN!9Y)o z8Bx1pGrIqqmvI%if+?oExT05QUK-ELBlud}uiuj2N3P&dC!w!Iy4?S@TjD-^OWc{E z{Mmk#;Y!7_RA=3Yd;eE1^Sc)J%a?HoTqzIwxP67+f4qzdV^^YCe8&;4Jx zCGJ1m68H0$aA19|lv>N!Uw0|H|8uv*o!N1p8NciK{mf;47+fi8dhD(HSl$2WTjD-= z8Hd8-|5yCGN}{@L9Rn@;md1xP*g&wccCz z8odAZD{`M3_qNM82u2{c&${bQ2lv0}BJPIop1*fX-2Zm>_zN~ZbLWK{_dfgNTTedk z?vLIb9e?CF+j-%g-@5bqJ9m!$(a~Q!{LRDH9UdM0*uAefc=i5o?639@_ujJi;@$7w zO?UTpzH0kdw&U&c*1Na9Z0nOZe`<5G`J#>Q+*oWZ0Nd+-j=y00+pmB6@h2Z2fsc>C z$Iu9PPqub9Uh~EGuE{A{yl7TDJ-yx*_sD5Ilmb*NRZGPpG%jH!v{)=uie>Lf?IwZn zMdRZQ0X==Nb=d_wxugK-#0z;o_y_1?S(Fy zhA~je;+i*d1$Rw*p{n=f_(p*j4aheP5VVfFP9Wwz`2{x$ylCUTN#Hu}I)N4M$=BQ{ z@Rmw`rG(6N0)djh`bL2lP474La2%#dmO%z;)bp0*l_0?VAJ^FK-*yZ{+K^YXlan;1Kia&wY^b zo)m6u%KAp}%I97iuGEvgt|?$c;cpbUu9w#dT*qA}5a=&^qrmlL^g4m-xa$Oh<(9co z;6qB*&z;)bp0>O|{Hws*z z>emTe$6Y57ED7=r0xQt^RDW)PS8>lR5a5pXzHUR&x!VwmxzQ=`-%Ho zM;Y(}__pKM-}~B)KiGKJVR-cFqgNa~dMMx7I%bc4<>);}-*WfuJD>8jSO4(~w!dTh z3pYRa;D-+W{^5)Bc!Rt3(};tE|FrdB>;K;Vj|by}!v3FtDF3(Y8+$*#_jQNgfB1Kw zdnGyZdB9d73LIG};FL=Raj}ocQ$tP0oq5tth%^m)Y@*jA;2m6JPQD_iSPspy)f`WH zz&|AE)Jq_Qi?oZGUXd##F)A_?@c2MWgqq=ZbNAvZi*>A$*QQ6tK%f&mt5z0vX7@w9 z)(^V@L0M8uoK6RBnBK`LFuvZI4E&m2#b;(AOh`j4BG}vqe9^Z~2f|oOKZ;mAl1Oui3~3m9S%u>twghfOyx^gg?r?x-d>D44i?bdX1@O zx9MPEz$Q`WNTsl>6buPO<4uyDhWU_u^s~8)X4S?dbs{1>gY$zFHzqPtO+30g?rU;5 z9YjhY6+tShdhvLF>x`t@BT%2B2x>s5Nu$x!CRGq~p3SKtr^rgv3HqW5`T{%}L`zAsqr#w6X>jJjPvsPP8wP=>88MDaiawL7 z{r&{1Bc(-=b1Hp(sasv(=EHaWHh=KG^HvQpsK~~nxKInaMA9iVs!~XnlCCkDmqt{W z@{-Vv$Yh0VepN2REL2;=BtwQAJ>!aHvf)+&H<(uV6$Rl*d6gDGtb^Pql63D6a|)YY zOc0{VC*^UKDAjzMU_g$TQEM=j5^UaEv1r!d^`C*>5cjmyxp6PR0FFt zt5YXH84X_qID~Mk1?>hDBO&|$Deni1s!~h!y2VPds8yoD6f1E}L((m7X_?VPYNAx7 z-JeQRd@pxysef-5|9-^4dxQhnJ02iyn5!7rqupwOn;cOOCtS99xZAEHh zk~)JKE9I`(*tC{vY`HU@hWbD$l1pO=b()C4r>zwwV}&{y!Se-h4`-y!JPrg? zLoOR&O?0rZ+os_3m{?4=-kMV=W8a1BtRpX?CL0BGBnIND-PSXZOaSf%&5)g_I+8dz zzLyUbHXeo*xNPBrkbo$U@-#7PbxjQI4;t-c(2dh{n)c*=NQyhHT*e4)({rVu6^*G| z9$DFBR+=me^&#GF6{^+FBCNG_svue!x!cGo`hFId)umkl8Om^aL877oa@Y-GYQ{?- z%_WPUW`1jVvBB@*IR%JHvG5-1Ia#&kppb1BXhfU0Vg|t`Mi2^FAd-&WZZYDHoKtiw zom#0A)25?4EYpIE1!5mv*1XDH=7hBJ)NWXXR48O~5NAveBtSC>crGo;r65N!DWZ ze!aN!Pr#Z#dG1yJf81a5R(Y5%hc%5%<^n-=u<2~dSyjbq2g4cD5tf5KxpJ%7VrKFu zzmrq2AW?wNIA+*LR-xo01yX9px-Vnk#cQ&(DvaDt!J5Yka`LavD5|oaKsC3mH53qJ z&>C=ETI|c@uw9`M7z>xxTCE0Kb9cBTcYYwJ08=_kToj#YO3!FHz2&kE(W8#*fZ+}b z$#6JXkRTabU@}J^KAVEo0jnm~3cNB=>?6R%xl~~k2%BZqlG?_a5-9nOjZ;Mcp}wD- zix}Y5vWEm}jG-kN1o$Ydg>x&Q++gZ<>NVI`Lq%0(v5(*Rq@1FkTBX{8UI`Vf5I6|D zitUNYTJUltVpgjN-024;kcd#k#XGs@XjKAQ+A4!3y;}t?iL)YPs9w^Z6rH3ZDXV(5 z>^FxnTf$RhJKv(J%?=*%AO^EDmtETT0%(r1t$AIbx*&8S>ZL-Q%F9gF`DpVbZxuUf zAw;gjj#|Kjm#L|(j zsgc@3+xg)$f7a^7kIy`Q;+OIV7Y}mzgMSZH`p;b1^TEO#eb#r&NU2l}VRX*n%{1cq zA<~v4xo0V8Yh}xHGLK~hJAUbBZvTy~7gphMWL!MO=57Aj{2>G$Ui@mvAO6f|Y*ztg zY3yEomWM520oItCNlYvXAoG|doJI>`st=_?zh4k5Rf3*}Q=X>4Zg$7p*vR=|WA|!9 zKAblMea=??YW}eDoUQzooNM{HTKUWQYwx7>eghVww#D>4iONcjO){kzWLFp{z=t0V z(nPQRzoP=hbk~ds5D2^;}TE%Tia!F_V%=SOeDTG>; zqbgY2FADyPjFOr%iThpXj)B8$b^vRaM$^+F$tW@3W&$IN+jMYV$ALl>u_ zlwOtj(V!qSUAUQ%MW0UXImcljs@^5FsK@x*`QZnPRCdbFv1OSq4hCd_0@+no3lW?c zRx&(pcr}kmP1xt;qP(?n)^jYJd4e^-+bl@rQR`=#Sr|2_@l??2cBR7+Z3k_QGP<+M zkp1#G1+pqH@uukos|AWh<56cm=BjO3kYE|i;yS9>PF)H|KC^p%P;?s|B9Y;&jJOF} z(!F5779e|0W4$qlq|Gj zy+3sr{zS`V4CYxxU>%jfmKrRjtFcNXEl91IS!=u`%Z{2hyG29?F>@!SoI=H619v8p z43u605mX}TkAOeE7mk&9F0RJC7#%N#8p2`wPTq5xplAat%}D}V)fys_U_3|_Y4wH~ zXUW9`WRi_JZQO!u{LcCEPKpaVrfUUap^)>;A7-f=mnzLWN@k!KsUD)0c zav62FVY*UO<56?v3&iE4sq8iFlKybw}yYO~1E`TM9Xz)>y9vJ7c-V;lsY zsUs&FS3tIIBAFCX;AFol*iyWziaU8s6lPEg(@BtJ)tV3bvS^5EQga&8V$|b}30306 zLWyt@w4#v*d9(|b`89If?J(JBY!7IZ<_rU|;8M)ftd!35c2jRSitmvt>G-?y(Vf=l zj1HwmGNC1;`1rKTfgI9!tF5!s<|6B5tz{cjkc#F#?WlZO|8INq*Ef!SV$T5oeEjDx z?FhUfdyBj;iprZm>F{aC72wqcVF=SVdh3q2&tKi;b&tlrt$X9?kI?uX^;^23%LB<y0XdmL1$=*vC2vjRr;&YYna1U}#4wf^l(|0U|&*9t}c0xn|_9s}r;)vtq@?c2M-n zh}CPjgc5Y|ma}Ye^$E?CDyxWtDJ2^+RMH^NWyZ@uKOOUH3#9@HkA}YQ=I{Q|=hu}} zf=Ks(TAcE|?VaT*mF9afP#Zw@x<{{8Y(3=$bOdDjfAk3ep6Eerfxnf(?`H!S#Y%kI zl~2I^}()kmr4PRw1vO5vLS>I5nrN*jMf@*%3Gg6ILR!sdly8gryZ!q67JY9RfVt!%mxlVzfU!QKf_LOg4d;X#>440R;Ka&;o zdDfmkGns2^&$H!^Pp8=bgf-`x2AobK5cBU$wM$D40-E(vFE!UqfM=JJQ>kk+90<&p zq32#|V5C$8kmp=#63$a7Au+NkQn%WVC8W}D9v71K2(mOwOy_jh)X|aY^e6I1z0?@u z1gTTCMbEA<4FMFIs9N9^%kgp(@5J3;&w~TY1f~4;!TgeWpKLGJhi;A)Lf^p|KpY#=s&aY+I{VcICda3L&+9Ec{lBS~OwZ0_`o?GCkErCO&kOy%YZ>R~9UxFpYmn>#~wYW?N%u^EqOjto8sK*v6s zlNqo^C4@2$)ux>yn2Lq124YD_w=}Poh03&~aa2)^)6NVvMsT$vxN5y;g4}Z^Q>12| z0%BstL9c`lsj~4{lV&R%|Cms_-Ew)_^(US<+W5l_e&dHXHhwtIy!W()9>n>7{>ky( zp8-++$=&+hPt2|Rr}+UdWe5D1M{hhDZ2sQSi%zow9)87XMnI4c@IwdhK6u-~n-1m& z(!pOn*xUce{*Uc{&Hi*B-QV4N-`>~n`FnqJ@6PT|@4jt!u}kd!m7QPM`PQA+?Jzr^ z3Ze=A`SurYx3)iP>o>Ok#nv0P`dcsDeCOtuZ@zl-rJ$kL{^a)_4`Gq{9>D64uVwd_ z`awQc4`J`hbHshzdUbp2)x+D`dk^c$@bDfPABDZ^&%^NX5ZC6vmtWI>_zOD6UZ*5h*@ zz&{USb{U6`|x_xK=<@*PFAK3fG zz3|>_@0EKeyFa)4%^QEbo9$A&pS<%+JMY-}TRYs&r*Hq-*5_`2_x6`=ceWqd`t8k^ zZGGR?n>OBadKd9ggM z`*R8b8t?=guF6FT7AEA>=E|Hn>lZzVAL$j_(iBy0k8$1f@8sDP35$c~1raI_t$rL} zv>F3nj)~&R4%}$WEw=2&{IFROg9&*j_m8Q?6BL>@RdUJ>ZIH#>)Z>y=vC9E7;#Xys zt2S85>sy$GVMo2QR@Df!yfJ`eKgzuZiE$q#Vu+q*D|j%BR?Du}j})=J;FSVKNRyf- z`t6!ZPc{EAAKe;T$+!kA>*J!fa%(fCNp_TZgR$FD=q-?9$_+unQb_lB{4hU|fy(J4 zqZr|JRv4-vG{GgxEQn*Mom$Ctdt8|>N=p#AAdbh{-s|%>IITOCVOx_|hP2w!s4{{QL&aCJKo&Rh)aQg*8cspQTC6xt-LmVD%&aqq$1ztn#lnzDF^H2_ zs312uX7`=B3}aDb2JAwV>LJhf=D_{FP{^ERvBy&LU?M9{N#d4LQsBgc@6RbFssV28 zKsi~zzylZciDF?5$}7pySO`K_jA7kTmVT2|u+7{%m{46%@S5nCMzeYq_)tLx6_3E1 zbIGjHG)J1xqf5eo$Mqh$m+KXwfNH^-0Lojtan>FcwDL%yhEx|M&2I7GFeKs%5se0A z$CnS@lDA4Wr#&3F=S>E#*2S|FTGYH~I1h=YwbbQ0r~u=&$Bl)7U|Y?cV&FD?ShA4i zY_ezs&E5)+>rm<>lLFl@L_vWv`=H!c(-Qri51v!76+c`^L#wAZ=k;2W)p3x|6zA(j zxkiP{g5SeEr8fhwbNdI+Dc~?-OA}K-3r@34#cmd|Nwe2>1{2Jb`4ORk3cLvA654L= zkxBHYwO|p=Yds?CKx|1Cy$O_3jxo_iKAH`CW&xu~s#hiOz1$;{kk~qc4_1?4I8xdz zVZsr>zZc|4t%fegX*FZgUZx|H9rTbLH*b~Hs1BN|u!6K0xlSMg>*9FbQKIg$%nG4B zcYJ5ip0!cN-1+95qRX?hK{T3JS8|F@ z$#LpqXC;rzu|^1~Ge@+(*vi~7!kVP5Co>l55zK-u?&R@71P{u+kCc^C#Zh{Y@>Af? zK!bo3C^iX8ZqZ4Vs3|aAX|9;f{DXyPjxyNIY7_&C&1O+FZx7pu&@lwgUDm`-;FSwy zs0X~=E9Cay%3H+%_W;%aZi`gdP-ofzr;YMTVzGppb;dTx+27`Htd`-iW{DH?$o zmK-TuN!qk4vmLA?P<1Ran28O_j1)-7o0HuThPZV5@pFpOKtQk_#{%W(omiZ5+m>tAtegFsWg^M zy7%Fn0>%1-7?Ddt4p;RlzcN}3RY%}H22smpIWcr-R!c#wRK?u;Cpkq$7`8yz2Lgf| zrX4sSoGQiWSU4!OrW8synsPL$6fC~!AzSA?FM|rlJzig>QGXPp%LU{Z>N2TRr2tY| z?Mf7MW|LK0>#nfF^Od38LUi2pT5-rP5qnZS5OMHVU`=kRyQCE z_f$P_J9-ps4l5D3hd%Jhh7WLl~3=)lK>tVc_(0-`^fQFR8B zX<_Q3__ABv%A-pNq?C@44rPMXnFj03cs_ABqiMyRA?B>cxKJnXutjwIesM4NeIy{e zQg4K@FbpKTGz!s15gT;jK~$_0q6x~JjP+(CHe91AZT;fe#~lHgp4yr_y4Y+^7@nxt z4BDdNsa0ghzA7y2KpoYx5fYBmJb^q>j9^98%LN=73$2D8Gz83NGugCNiKJGcGXl<< zJ+(v3{N23h3sEdO<#gVVNgOjfb#uAsu9g+KQ+LN`FkPk~n5qnxe40dy=LK1a0xgO_ zIA3m(MBM2mZm&A-U=^Ka<_0KV?J`Sw09Ta2F7Ey2SvNoxkDvi_N+2d+kJa@~OO`Sa zy)z^^y;53+Wodve(PB8S7C+(a{neJB5|>pd5|o_^PRSI8c2jbPgitRD9@^=4yP%Hd ztjLYq+GZXFSWCK~gjbi)tOdgMumLEzPQ*>%BJ5Dmun4N&gbipRrZCdN4hy-AgsPN0 zjn~_&1<7^HMuVH$T?p~@ezQF=c@gG^Rl3AbD6?ba6pN7=BS2AOr`n%Q=LlUY^-bCt z&ke+x37!Zm?4r7)n69+-wwxk%ond=fNp!th$pWNns%T`nQRF(aMQFIwy5Uf;bY20V7O1RkPL&}iN||a+ z<*7U9j<5z=L0LBF$`e~^HRM86hw#n6mdo(ezHDbJvN-H7%ZS}dii&IcZ4hGFtAfau z1S$ccRk@@{Td&P2rj@7|`C-Ntb#@_(Vh5C617&)qrH)*P8*HIpXo{WcuwPFP-knnz zAP!TTxf-Y*gya1C?{yRq1~x8|eI%$*OjDT*n}pFp z;Ho%I?3ryQm9A5__jmIb$kr6CK*mg+Z6;L%)D&hLrBEyrLIF6P=AzIU6+uhv;=a`k5Wn8%!}RBK%kv~c2|VhX))E= zszo@SAukzoy0YDtK}$N#o3qi%hQvK4?*;|D!;#c0^wK#GLS(0gGZ^+AijNf9YXNQW zJWCn`N-!`tqnx6aRaM0arqx*FDyAGM{Ft=L`e>Sl65B~DoZ$}VM3!}$+P(keSakW{QqPnOI22bdy-B(_wV{*Uv_m2-pq_LQs+s;+vhK*6F+2Sn1ZFM$|C^ zvCFY@N8sW?OJsJ>za-o;(Zw12bF3&KwAsHI^i0HsbUC>*#dz_sN3Si&2KsN z|NoqAdSmaa_psfc+@0>;+j+yzOYVKcz0W)O;0b;A=kG4=?jHZs;}_m}_Z8fBnIy?|<+9tM~phsNzR&eR%7QTYq))-)z=4AKCcVfaRa}&tB=zTjB+I zTe}zj^YLH*R~&&a-hS$dJyCq|Yd+-}PjVE6i=|emkn5$tAIzVxSoXR1m#e#~9$x2e zww{RQ)Kzux)ay@Mad+cVD~bfymevaZ-?|l`cpLDdf6Y@m;0#~jT)xg9>-n83pToNi z4B%vK0L#~T7v5UH%>#Hb1ot!_PCkIz+5ncX^Af*xEAjy}o>KQp;){t-dEv$EGiQ;q z4}#|_#C=&I#^CF!C5*>~D{-)=UC^FVKo?$d;Y}eZgkl3naMsf!-&*ihU&B{D^;#h1 z@rzO_z@vPg;C@lt5Wu}M?+vB()V{aoe$kgwd+K#-V}zEW zY88CC57$!`KcGe}&Q>_J7AR9SR2!r9r0(Z0=efpcwKhg50#z}I;i>g1#S>n^$eEk*Qc93qKk?T1Y^BI|*w&H@K zp(UtV*8KW#z528H(^?7h>0XHkb#gqxpF3aI*D=sKOr2yA(XSIt;%aJ z;?}LmmB4+<+0)OIz%TMBJJ4c|PtdWF$dedn=R((X;DXpHKp0i)*)67CEY-vZOWXj) zfO)_UD9;{>6uTZod#yU?PuW1;sXBDYEXG@*Jmh+CyHSlHs5Az37RJ!buLDEIQi>MK ziiHm0ns#e>&nkxb)C|ZuRgAcRi=41%L`%M@R~sOBzA!Mu5(jdbwI{q@7x=_)puz+* z{9DVpq#}2pngUU$Dq{F0cZDRdp|CC?birBH*HRN6HrAqxBMP@kl|Iv+~67Im|- zTs#Intt`q@)@ge{*QsnJ5oK_3GN`1pGE{Jfz8ICQ3P{Dk^?EXgLuFAYR2U#wWEAp;MlE?q|d6zY5l*^);l(KdOLq}=h5xo*?!0N zS8l(4`)_VPvc0?YOZR^4-aoqcjrKlcZ)f)>K&^p)y!)lQ;_l0LKWFDRcYbQ;dw2fM){kzzW9x}6W2?FKg00QX zU)}uYn_s*6#hbz=y7`$KAKCcO(cE>xM(2xK8#U6oA-`#rr?$*)U z&Zo--MgH*p-K`g2c$Cl4qhEP+ck314&-S$w?C2k!Go4*!`qiJgy9ER4>G`9JYu?dU zeLj%*nyn-2{NatO6uf@8J^Bad4K~lo&l~)e=OT^g68YulBA=hoA>!XyAE|P;L2_z;(E6XYvf&B z$V~$v>vT^$c0JRzYZqjVUC(yy+I6UF*VElbJ~nzIPj(yG1zFR`Q{6^(9qKmnlWrq7 zj^4NfJjF5;uzM%EyH&~5*Q(cAxiw~>!@A>Brv=!V*d*E99k zVTO92!3&Rf4e5dJj)Fe7^t;{5+67s2S>Nm0xa&~Y#>cvid}zImyBk?2#@>mWN4t>g zM~6Jp9YQ|Xh3x&Nzwn*zDc21?<=fq>`#|@UHCOj=x65DKJ+y9=`H${>SnVDh+U23O zQfsZb(85Dq&#iRfUC({1>xTEQXWiquwF2B7&;5HB^1jg_-|V{Onl7Y|=N{~yvOM^d zZ*)EP-tH-DJoojk=dSJ^TIad1bv^fs=09_-fa4S9RfC&wZuqhAY>z?(y7O zJ^qg8zTAbpdvwSHUAMfe3+dyzFLh73V(=+n?0W8<-BZ?h?%%qeyS#g7o#(#L_1tCM zgF`)M?v;aI_}8xIF73j*p8I^)4VSEE-Q&5nOWGaJeXa|+cy!2TyKcFt3+dyz`@5%H zIQW$Nx}LkBd&(Nm{Y%$#=XVdS^W0~;o_k04;Be2a?PTrV`suFc&g;Uvp1Zf(cHjS!4Rgx?XB@53LK%8@s_-UvEwSN4k0KIMxo%k9OTs z>%zNkxuMHi9f@^qA8)gMr0b%}dib93T|0~IgztyDkn-q|4|Tm->O%T>_4=+?i``S! zc=dx_uNJz8)_L{1?)B%_TQk6`dtLtrx?WYg@UB;{?Ovm@o^_8~)-I8C+_Kt*$fHA6 zTHpVtC-0uv_^SEUnYV&J{_#7=J#cYlWyc1Qj=!@yd0>|hdsMYrS{?lq{p^OlrEZQI zXMwS$*D=njT>JbnRuzxxBU&?_fh~d_1)4pBL9hPIK9_-8fPJgq^}ay0P;1rib{hZ` zR{jbls{NI)gU$Aq)>U6!sVdP7is4$MNo6d-Fp^-37!XH>4b_07+H88VAi>3H&gOG| z2uKXJoUtDgc5-$*#)Hxm*h`Lg(OW(P3>QZUt^j5#SQkWbk+V{Q2 zJz~Fu?fv@Q7^~X{rXP*l5qtX4+9&7VsVD7Q$!V{?_zxu~FzW1&lGFYUH#W#-8#LVT z;S{m0;SQn(hZ}aw(Q4X@+Y+h*l*}0*wH>S`a?WHVr!wx2R^R>%H)H{bDZ#2{$k{`t z49RRmuo9+*oayGaP$=(7C5wW`*2K3-zH;3FfszE?dT3j`%=m-evXuTm=9wZl!{$M87gRiDRj=auX4 zG;nkpFK4oa)-$;EKyDe5%$5rvm#?edc2xgqzsb}(wIjI}tCnOypjKtM^)%e2jb;LW z??W4H&{n{`#w$Rzzdg?i+w1_||3MEoA(H3#oZw@OVvaW&tNBdPE8Db0#@ZAN7ChL@ zG8B-p1)&Z|_NV6+iInUOCu^u(Er;kTNIFb->O`{MAPaFvUPWZy0>kmDRiRTj2InLx zo37UU)(TmM!p?1tZ6bsQe9?kLN2;b;MzocQFyW*}>Uph82R-ki>`JuN^Dc``jx$X? za1VOh-aW5RU+U94Ibc$9p z`y1>QyVVNdeSFV#R6B9pgbEuf&^C+%03OVczl|*PD>WflXmQPu**z3 zVa!kv7flj2k>W@m+6M77@ARUe5Dk|v!lg!>Qj8Q~ELh8g%YcI#6lyI}Nn39ssMtjV(E*%7W>;2z2@!^Thw`>k>dU?~Go8p^Zz42=swT*h9 z`1jr=_u?NHZ(ocp9=7n6g~r0R`5(`}XC9qQzxFBvpt3{7gU{ahj zoRCAM2A%8K^t07MqIFpGpoL0O^dix^1Q)rYjOEd^4YesUgW*&|R$S#w!@w*j+qB6M zXp^e32{}t1!R%G5<+|5Ir8q_3G)A?IE5U~gE{`9i_{NP)N??|SHmxuMZ6j+#M7(Hp zY9&Yr80thr0@_%cHZ}sSCswlgEuowy(_}G%m^o9#>$5^lmlY8jm+T3>NiJ4HAXWVzjKULjhdLamI>AuoUYOU#t>w zHQ;1`wL99hJ4T=_Gu4pFL>+MpRsiKp{N-T1kj)d>pk6UT1`~?r(Tp`@%YYJN%<}1N z+S5m%P3ddFtcRd4%vt(nQ)hrg3%%u!faR$v7D?x%<^e%+S5j$Rk*w@ zpN)80&tGRIRgK938ccMDVM4NTE0!ok3dTV1s@i18iYW(UWDS_O1KyeNcMCf z;H^`c1QM!{pjSlosj7onjZCY*wT4Z!ljuV#IX`PW?V7Kh`d1rU> zhuvs4TKz;E@%Ids_;MK7wH>5EILxHd$y)G82zVM0K5I1_QAiY?B4YA+wo{MikTd-h3Hi znQoKOEHDn4#cZ%=m1JUXGJl)QKMomWHo)mXKAqXiXTCO> zZyYkS*1V{UyG`cachbi0yE!mNhgf#C$z0=*4NAo!meDpDIu6;O%o}3a z*(P(2LpCVOhFC`0WXL#VgOX*4Wk;LLvG0V5-8W!hehjhv_BPqu$0X~U216{L+9o@7 z9I`=KFU0aGZL(9wAsdw8LM)%$COdf?vO$R|#PUgPvXl0mtgz<>49ZO*mQQSxoj4|0 z-xL&L`Ghvv3FD9r$~YmG?QJsqIAnveONeE-O$LucHYiDiS+)T(aly9lm7A?&|>HD%NNSa*@`ZIh2oYLi=lz+k@p`p>RzoEDEHx3Z`brKnQSgsC1N2?Sn2 z*jgdYCcV5CF5sZ5v1Dyv&YIhn4R?ybG4@r%gWrBU`?WWIR{X$4`I}~T9CyW|H(sJ1 z=DX`P+rIMaquJG^Ht`s103G6IKlz*AKJ~y&kDYPs;f41;_`X}|n>THG;MdvEYc4Rp zFaEYm-oUOdwu#4hUGETo_4qF~F28s2gqI4h7>+q&<>#Mq|6KRcCw^fm&)@fhryqUK zpV-xfHt`rQ>>Xll-o7|vn7`)K?>zL;`%e2$dE&c|y!!X&{qlE{?;>CL-hDrQ6}vj$ zCLZIJy+iz&*v>Ef)3LAoQ*-u#v)>y1$ro=GO(z^{dH-ksxb3m`UiOy9Uc;`=wTZ`g zY3~qw-+aqEzgm0bwYUEfd+)W!K4ST=Ya82wum8i{o3h>?z30y|%&u-|6OZxQ-XVU| zf4_X<@<$(H7H=ukPZJ-sT|XiI;^rq`nSb;i*tL0MG03jYwu#4haqkdkZwGZYG4o*$ zJbA+EJ->oay6mH;oraaK{p5Qtzw_u@{$n0kJkur~i6`Coi@ zz5ck%&re`KzxNsHf=%Z=_4DJOdgdv1b-GPF##?rWcr|z84M%^?@w?fDBYyqo(8F`@ zS$^}K=hcFD{`OkiF;{+Z!;S3fRGWB=_v{YwNteI*mG62FIcxF9hg@{^v-U6VxaEh> zz54q1ZTcv4+ugP=e+Xh%b#3A?-n2W!M<22A$ik-zFB@*V`0{&+)l>Xup7gs;2C2=@$<@^3GH1 zhu2^G1iL!XCLZH$yF)xZ^XLzsjooqN)@$y+;G##|M<4xv?63RKr?t69Zu#!BAHVwb z8o6@rGz~_#U4{SbCbQ9&sRGrio_TqPPJhkkes#P2$RURs4mdDv*`qw>i#MjA{bJo)hdY$jkz3SHT)U{_-U;gRV6X(c_9y?t7Mk!C-lt22A|31%h zbIZE5EAH)`V;%fQZnuB=(%VkF{gulO%U}6D`Mrmp`Cm7@^;5xne)D>%2|Z$?yAS{h}kjfAM#Z`O+J&t(^3_AN>BR*Vwn*_uvEc zSO4|om9skdwL7iX;9pLMj=l8ZfB*4gpFjOc>5)%={3DgVqu{ zbWc1w``E4C7w@BX{__jRE>2t@dOG7;Ira_T5By$z@mpu*w~#B<^>l+)6FPK%{;>J_ zQ-1g9i>seG@3;Q@-g?o$PJKWB?lb9k{pG`F9{rQ&-+LmtQdv(oXg#4r_h9(36An-O zY5UF>KD+Y5jrCIx`PP45__f-sFMoOKD<24Me)Ywb<8S|^NUoID(+ye%=+K?;!CRks?`sQ}UX#6$|9a|Y z6UtYiBR}%pM;^t+gwEKl87OqW_D~0uRgO&n1bPt&_)ti4O{MTpkuZYJrZ#wOLx1RH-$@H67 zkN)H(+}v-zb_=!*T46tyRO)BA%1B(;fG0GJfTp=#6KPE7E$pL5l(% zy4QUv^YnEOze~9NGw(b6^80o3hSwf>sCfIyPdxp|o4@f=`RY^1mE3x|LCXRix}SUg znY{ePC-1kN_3h8BIR5$2^Ktxn<^=sA51a0K`CFHL^A&O>yPj^)!a#@a&r6^4edET{ z(O=BgAA<6>yFWCu;g}ar48AZ!p31%D+b`Wpu4LBH4O|-N&?#TP|Aec=Ll6Dl^ZxTr zu6j=YB>FSYGxxkWef%$9_xt*lH~)}aN$*A1cX6OYxA1zi{^%>}S6pzI^I?~`;=SkP z*`ICym~N%?%jTiCaF1L~uB6t}4O$-P&^=swE%Cj(jH>e8H-Gt6^b_|yW_tLWlP8r3 z>9;>P{4=Lq^+s|fuKWo`p~1SH)x5VLwDcfcl_wLn_t&>%RArj_#H1t{_ukvU*r=%Ic4(tlJ{G8-1ybU z$rWKe-JnH+4&9%Rsek-|)q8*Zq13ktM)}b%?|R>R8{fVy@JzgM%%!*A`}sL?C9xM> z-(`XhUD1RcR`CDokjw`A4=N8BqXax-d&>`Xuf253Pq;9R{3p2*Ur#q^p`b%&|7P=q zcbq?soOlR58T`?PVEy*5=s%zQz^ACgS6{F_{jN`wD`&2!8?;o=p*!orWAC5;_2kDM zJ@@LHXMU5v{H<5{j+A9bF1bXxzmZ@&D>ZI9O;p1=9g z55NE1t4{pS&%OH9&%)=u@aU`Al^t#3F%}Fu#2!+a_{HO=f9l1P&Q*S(oI5f5h3}s* zb+_er!EfIEi#I;@!y^`EADuV_)c>2kVS(H5m(8<_KbinsI zZtK(=m!8u7YT~Q9`zPPJaM_0J!hcS@wE3ZpKiGWT=G?|xHhf?+Jo#@rc23w-So*}K zQ#ZAiAO`;C{0z^;bLMKA=tD@G9hAhC%oW0ABpo?K!ph-DHkLM)ol$D~3mrsU0fOL8 zy~7)b5fLxeGzN?^9@T0{99sySjy;fQE_XDlvHcO77ZGViX2D9O2C1;ifMvH zOIm&w>mW3L+8mVKtko7|!xzhoE7V+|;9Ez3msK@Hl`&b;0l^v!6 z?N+gxF!P2sf-j@B7|bavp3r zs8yOsG@Gso)smsfVKP^ZAs5`qa>)urF5!V5h$CnT$l6(*pWzR@l42C%y`_v0vrmJIj&xBQd58Gv=M-l zj-XkPn>>~&lst0ToC*~@4h=5x{)9D|@)R5*3JNKE$}=6c`6ynixk^q=A9Odun7*Df zSX`VVFI)Uso3EBvX`|0XIRD%wW&4@E4m8g4UPLAZ$PFXO=v&D)8Pu|c$ zpjp4oQshX@W6JoQlo)KVF;&*r?HN7ht)@$8)ZmmV){I`7y1s*O*3ux%tr@vkgK>ML z0w-J1`4nwL8Dt6c-n$KIo)jXDVleHun%7Lvv zHB@d!)kcsZv+0>|7m-6#1-PCdv*x%83#y^)Ersi{ig@!(!yZSlj9^tgwJ^a?eYk@t zCk-LN>M1!H(ymBht!CBd!nJ5qQc=Ne0TSS-v+1Qvp@>1((T?ZKF0a1CkpYfOM--!_ zsbLkm)Tlx^BoTy(JgA)E!7_TToS~;W8pC`6(~=px$%zrB6dV=!npBHmF~7xIbkl-ppz7QGG!>lVnR;_uZV0Pt%=^u0u z@iNSn>PlEI@Bx!KN#Q9wkR%q&xp+wxBcNCb9)u*P5~xGEtLBUo*X({Y94vpdFC!|F z2EjlD`I?Q)yGn7N%Mr79W8Pd)jmGK~G3RvK(t?tWSHnwC2T?1U&1N)EQ6hrDDq%K9 z$Q_U5VOz3kcbFL}qVQEKli`|bd8ykyaAS^eW-L)ogY;QOa?x?4$hLOLJIO>6<{fAN z!OL7ThdLJj(s8a>ty(=8DVMF1TrOD&XA@NA%=iL&P?SQDQI5%FHQXPG2ch{R=KeTg z8qcTgHe&iJ=MPwLeG|@U1RI%zED(wE&f7E44y`!&sA_9F~jp6rOn^F@o8L?e!B z2sXmuQqv;i%^+pem%>r8Y|aOwgg%su$D(Z1F#pdTUpcsVGb@#YHgm1!M@g^?I43iV zzot*seUJf&D;pzDkdzdvq!;vy7fhZp-gW%r?9n~YiK~P^9WwfTQFFoX%GzjIu|RB! zV$Hr-05X=`e6{R}r}AV?lI9-n+(DrrK~2tIsDw&jl=VnyCFsqu-a;gAa^}^TDPq^a zBOByjq~|`@K^XC}UMX5i6hyVb_E0h}A^e z5NawgoAos*HS7#Z3Ct4qN*TsA*D2t{SCyK*m?njYiLtZUq~PU}+qN^3S51m$)mI>m zh##-md20cld-IZazj_p3^3us_fj4`s43vvy1EP2NV>Di?T5xyKBUE5EnopZ+d_gYQHKgpUQe{Ml1=4{ON^dx}<6Q^kE2r%wL1cwW z26jXs4RV{zVk#P}SA&dROGeaCK(Qoce#54X^@-%@5wd@0wyla8vr&^NTa|4!7m3Re z18ocuNg;>mSvSoq5JyClO~zG-WZ}ijI@b|)dYZL(nq=~Ey)8|f17PE{vxJ8^T$K!p zgpnq_I~ZZB$@*fTgU|z!f8OfU3dR(udZHvfY=qLXxso2QG$=g_X7G5W=J!Ks$D*%; zFj0aHQb|k6(oA3>7_rrIRno`!h=Q1Lv$Wmf_8BsDp@8QXaqv>JKci^0&|pke~WN}43Z=kW!58xez0)#+vwgb1)Cs1pM*URR6@N5d9ZHBU9IlrQRP+VTRN zrKf(>u6z&!{af!Zpv@?1(q~K7Oi)8jk|PJI2iU;A&@5pV>~SXPY#QmaI!zdjB`)k| z8iH(K8Ss836g0#MiXK3vuxu`RllnrG%=#gYhi4uHfwEt&vZ5&=6O77!l%RRposC8V z$(p|@riqv}6>bVuKb!6q~SfeD(@N6(_C_B-rmbIrE)hcSj%$YLoM$oL{_0;J`DLm16I7ii(9rI=+ko&?2 zF+tBKgLbpC?p66z+-T3%b5@cy@nJuaFY7v0jH0R?DmPtzTge~Y$6C#|?SJP_M5s!pZ7H=L&A+>C_NHbXp zQE1GXs^hp{%1Wjv1e)lR)AK*l<`@~d`_!UsCEx5jw|%WtMN9Leg<6~2zVD3Oy%_$H zk-MK6xnfOJE3s^{)g|`yp+TLvb*J_-xylOOTLvaqQL<~Bz(%2TwNc9-~>n-AOv_#ZX5Ice{}k&xbJ+P1O^wAlj^m1u1}X%4o?|?MvUe^U|r6Gh4&u zyN_ElTn-va4%~2Q{yPkp`xwjqCPQ?q&7QHWe|qr$&?xrTqz9W@9qn(G9=zA!3EJvE z3=9g+zh)RPSqv7?VBZ`|CL6Zes*MCk8!?JZS0lkNTp?{z10#rMHVJH3pT6E>{Jn>#{uwRXAf7 zSDRS6nI;O`UEyGy4MzOMY`GeQF$7Ezc7ltls;v}fu{D^UbyPep#wum-=dSHP*4Cx( z+rBX&F8jhuEgson2ri2oc@Wu_btlcqMpJ-kB&YhRctenxxvA^Po$#M2p z?|bia&pX9H5i8@~y<W@rq8kqbIstbsoxMaXy=L+G#ZN7szZlT{LiaDaix=Ox z@GC(4hlTqWE(PlUTjzfVvIpKh_3YH$Q|HZ}KINNw{rp?!UYh*Z$txzKz=-azgYC4{ zdu-P^z5j2otQ1QnPq69@Lmm|=M4(92q9F*HVv(A;;0}l36soyQMW0XbNv~>-jnJkK z2RRJ?5Bxe;9@QhBCZ)wakjBf=iVDSjh})L3(6$=Fmy1D&DS~`jt2cxn`IE5`KkguS zy&n$|kdZ56VXIdSXDn`w@>GgZ#17SxX0xp*3b>UL42B~+pSp@tO){E{?tIXn<;#!{ z=)fQ$H!EPIQ8dA67g|7zN)^EnjXt8YtUC_k(vDSQv~fur0je`=5azMlFm15@e_f@u7zj9ck-~}*pHz==;A;bEu^B>T-UaHP zQyGK`QY0H-Toqe-Q)lsi90b+AALAf8#m$SpT8RRiW}pOBx8Mwhfg(F*pDYEf7E_(k z6xAstRMxDT-Q}6zciQktKoBujSB)O9r3CDYvF8ys4_1F&YSB(ohLmims-BRw5Tg(K zm$4C_85{BG4uS%k(nJGXtj8gf(x4G6TrMHWT0K(Wt05z$q->2aI)yKt*@;Ar5ZslDg|PD)l}MLj z`n)~u4-|{3nzhWt44^EyEtRd*VhS|(#}1;#r)#82MtunrpUp<}YN;9^Op%(!kzw(mqlKv3gmcljD&bMM)AUB^}3n7A8rT z>{ud6O?Ah_F%aD`aSVjFa~)&M6B`|bir@{iNZOQesX|4(VqM8qG1gjSaMFm1s71{R zez*>q-Sphq9fXv|GlHez$k)OBP4i(VZ!B1NK9j769gap$mTEK|W#Ul`-f;fdh<9`m zu9&JwR<4vKktmztY=Qx|1)}*a%Apk#dCeY>_HB^d-$z zb~(+Cf)jBPC{!>LrHEiR$3)hY_n7pCsb|JUJUuqzsSY9@${8gWn?f_JjWT0mv7|ZU zL@eovfjtSB(=J+r_5h|N5XYu_$3}c=Y{WesMAS^Hu*ugHD=D~M3fhc{8CP8l*04(m zYY?q9$-`nLOD1QS?|h*yj)Ayg>^45qK|~zZM8m8w7P}fYIw;6pNfaTA!AnO|Sx!@B z%tt}th*icMGo78N#W4__y~N`nuIpG8;4wOBi5b~swyGt;G)z;4x<3##sBRPD^fu&b z#Z(obbcJ4$#zy4EMr0Rshff?aarwm3&o;^%e>44-rR0V$EiUkg3U+xR(&;7fGthT-vB$=kwgf0Myg~yUMtvxDVwL74U`=OLSqOa|jt~ns|LnS!lY; z5h08dSrVt}9MxOdIJW0jl=2voja5S?Pa;=ylTh3)Me0G;Wlz?U0VW;h#4;nME8aqQ zYbIgAA{9%-ma!mJJr(t3$VeH=ktS;rf~f#c$`yk?OFHEcyakc8px@H~+aSES#ut=8 zud18wxIH%HT~w(;XR<`BZfdfegaoOy*_x{%6@P{{DmlBo?o7fGnhbB1;$a*yI!yxj z&Kb65te1XzDJvPifzQ=|fi{VZ>fxF6zBOj)CL1y#DV#7$}S8qegc4xys=6hjo`ZSq_El$2$eYSr%& zvu;PE67G$M$>Z19fX4D!y_ZZnO>k6SQ!K^;*l!v4_)#fIR`u1az~Fv2WC=oBVK(2W zSQ1UiYKkTtSk+~tQ-Ow?tGIJiIPbO+j)1;y^{bT>wMDIgRET^bi;ynA7r`@PuODVE z*<(Z2g5@h}Fi}fDP1$ci3Ke6sDSC0x*F#PxjC&kxu;NuvmEMY{34PI_Il@>CNXtj4 zLMB+Km;51D)lws^Dx6^4MaAFJ|m}Y)A)j2bds` zHPjeO7WDxoWAh?4OTg|37|W?rAR;tk%>ri*(OYc{KE8z$V z_UG5tkXkJvZb=LwXqfTF%cc&6^Hs0fh$-nHeL4`W}q@5W}kBf*Es<`wVM{U8Wte~YOCzBI%2{j=$13kAwm)&Cn zXLOY*t6E}UDil+bnIM=%2h(sxNE8W{Cw+ksWN_#q*aUBld)zRAlyYSyoy{Q_*BFvup%%+G-?|3P5l)7sJr>7Kh)JEVAB! zJDDsM6W)NI?6G0-&OJ7y%0|Yd1r0ef>}Q!`B*|2%n#2e7v`ivMFcx+DvQZ_+h{9IA z*oSii8kR~Km%Vg?qYAMDvgzb<~|XN1bAPg(wCxelS^y zb8HjGDzZrD>lEs$^Cnrsv80^HI_x;=@&ZncSHSsB#3-29W~6l-HKb|GT}_yv3YBv- z{EQ1=i4@3QZ}>Dvon&~Uk13QBCSN9jWom+%w))X@odS!(pbb;I4NuS_VBvHigk(S( zJxHdeT5UDK2ZL5yfE#qs5U{~zcL`XMp_&bjH5jF!(e8@o^6hh@Ae}5~vSdJlQWKoZ z83JKrne?dQDFMPosZbT%AfINis~Xj9>pR4ReF6A*60!QwmQlAVIcC1O~} zZv}71STMwU8n!?vy~fKs_T}ZZ!fLH;QwNoo4^%8;a!y1kJBJ2I7K>*5sIlM=<9)o0 z=wlLx7tLaVggr@f-d7LQqr5F36NyY#qnwtwIgn@yvlt zDqGVL3z9Gv1=djaCYr%UEkslrVJ??(rgzUU;C%!#WIuSPaR&bZd-Q>f@vzGtauFEC zmCclk2>Fcd=_OElSvQUIn7yWm7Ai!OF$<~kl>&w;S+@z7P=gH*ILVsgFoN>7!6dPE zxZwBYW%ywB@_`yIJiI2lWpmh=afNdEMv$e7VN<=|aA6=Zh7y>$@GP@-Q0DgKW$VH0+)bv9&uv`U=w15r(p^i*r6U#}T->>6Sa@dPeGB;f zpXNU?pP2urxv$TiHFx}mr#F0TLuSLFv-i&mKr8UxnKNcKOn-736z7_{ZHm#oqPt1w zpL}WZBa_6$?B-P+d`D$sq zt&(gQ98O<32=?=(aSRBfaHUF|VpdzFtHh-{Mxa%LA-9V)G9G)~C|A95rqmE>pdOY> z#^nrYsU_J!fX^V1P;oG;ts++9((NPAjuhpMOATYDqDr_Y#-8urT4MoxdjG8svV)5eXiiG6J90C{O zH98Q?(xjbEx_z+UO7mrAwN>IuT)Jh1+J9q|mS9#}#jeDqn@6A>qp$?C+A4b`E`59i z+A+#XFsrQsSmM%6BhZdfRDxM;mBJF2J~jgF7$qf`)mBj~ap}epXvZih!K}8*V~I;2 z9f5X?auUpHt5BA>bi)Wm`o<_G!K}7QW{FE58G&|;QWDH+t9X{U^x+X`$0#JhthUN% ziAx_Efp&~C63l9=pq99F{Rp(g808vQTcx$cr4NokJ4Oi!X0=shOI*5c1llnQNSv?= z%553xr4I}%*EpzL#0jgQycPgoI}ALiI0U}hDyQXNdha+z_y*N_Ty0m=TDp2Ht^tJ= zh}CvAt)=&jLpG?a0T@7QJ-YIAnv$C=jbqo9xnY$OaWrAXd$7vP;G!>svyBST(iDE*_^k;GhBu#Hz7P zcF{OwgUTlmtA;k&h2xM7DxN^BZf}!aFb>(E(h0=s@olp6#~~Y3IDuHzx5?fyPIW-L z$qto@Rd4U+GGg^@ZL;&mBa=g^(}2OZ5*;enq0){(QUHkIAnvA zeOmATQ`0jOi`Q&868!HUzkhh(A0GJs%>zK8D!G?J)u2&!-y?O822V?2rLV51ZEp>p zmK;@E@1?E9)DqJ=t@TemIyU_ks^~^hqw_8Vk7g9jgJY%M4Vj~9*YtkvYoXSPRrV@W znc64pTcK(%-nBY6f0aVjpgYwvY=6ZYt+v4mRog8ABV6>>c`f4$v&L}MB4+%26sYW0 zjg_Luk}fA;pWB<$YT^A>#z{$dJZ%hzeA~7~l7x?M2&r;2!~l)BP^M-GTed~%q`X}L zn++8$Yq7x|gv?r-TxwgPs&HZNHiqF18JQ1z{MK+iRdzw!DmIiJMH$E1x^y7k*jN!- zed4NDtx;_oG+Eg9leni(4Ai={?%gNWioy<>6M$R2R_oR;R4o2H=0se$&E<9>;flc% zR>g87WU@zzs(}f{qjofv%_V?#5$IrRn;4>Zwzi4?ZF0Kc*7cp5^>|KT2%wIE1Z)VB zM>G}Wrc(`0%0mbvC}sy9&pJIR*2o6wDo|f*OZfJTO?{-TKsQ{gvGqTsq}Rugze;1P zzdGKa9`V`Ay{{4rCEZCP>^)n$?V2Z|uK1 z9tMet0_PP&GMRz1Ij=$X8mi8k-(yz!Z3-K3v58PstTQ1A$17x0w1Z@4r`gDBHMhl% z2H*&R`ci1crZy-sUGpk(F`9Oaq{?M&T{;+h(kqG9*mGrZ{n*ntfDhc*V;Mg7{BIo? z)(>xglM%Gfv}Ns(`ZsH{XY9%4vudnVmBn(a`~4lpoZSYr)TIkmkeDN^lk;mm_C}EN z8#vVT?Ert(P-8H(Z0ajc3qEMlhOm!Ag8ra_g({#r4MjA8)Lyophg=Or7Fm(p=0_75 zFs}e4Eh*S|rCI{sZ^^;Rc_A)m^5x12Z3V>EKZ!|`#bUId5Rdtjj(p2UV47k@w?*^e zO1Yq@YHnK?c4}6KQ;QaW>Ih{^n6_23u0pdAF#8M6 zbloc#@~XrJO%7s2Ax;Qf`p%uL{r|+oXD6mloSvQf_SAJ#=_wQVa{u7wZ*G3y=J@8f zZF+Ijmo~j?Q)JVdH~xC#XE$EFk>2?FrJpX{y|i=5v-GOPA1~grSX*=~E-gH^aKnPK z0543X2M-Bz`xl2!(FVcC3f_h(ETaGy5qdHF{yr~f_w zg?n`u3D(w#pn9{Sv-d%ov5VOTqGpLlrw(zoc|F%;CNq317N(499F)F&RE zzu+9*xkEvH;^FzTAJCmM6x1gko)6FKb`AygiHGO6KcyQwSi^nd;rWHv>&_b5QlGB= z+<%^+(}sfj1krO3-l}U31@#G{=kET!t}!f#`rM{_?oQi>wPWiOM9*C|p{oxC^$DWq zF1=Y-8w%=U-E;A0bk(7tK0)*x_hVgUD5y^mJ$KUcy7Ev^pCEc}+iAMeP*9&BdhXb) zt~eCbFNj|AR$XBzs9zAxzgd?b3hEO?&;DYAP8|la_KApRza{FFp`bny@$7plI(aCl zPeeSMd7Dle3hEOP&wBr$%MAtfiHK+Q@7HCAg8D?nvxnWU%M1neiHK)@ca<(Z6x1go zp84u;bg7}BJ`wTESF<{CD5y_FJahH0bje}EN+0LlbKP5p&33JpK0)-%)j!t>!;G`^ z38H68J9LSmpguwLjAvFC9}4OdM9;jIpRKDY$&Kt5Iy~u59-bs3hEO? zPyZsN+c6Z>Cy1VY_$uA$LqUCl=;^OMq&saWs80|*{l(LD(P2T<#}D^h_aZ#3oxM_U zz&!8i`=8bELqUCl=;==fb&;WP@f=r>d~L-0*5y{n7N;CIA``%(>G6@Joz2)&j;#ve)_^Q=5#x@TqsOT=+Xo7?99D)E968GkKaT3 z$%80IS37W7{fLFn2_-6*R6{6YifX=4LIb+PKmlNVI+&AoS7J`4D3p+J-eX5y71>j( zd$@#+@JS&QNC7uEPC$Sjd99uYuHLcR)u}*rh)N`cI@gGWF?J79^rZrJYC zShrth-6WgzvF41PZuZRyBfa%n2#r=DJMWSrvdL}D#X+v5S*W>eK8GcQnG#5xb+dKR z(Q@_ayIm~~a&_;>zyw`R#_xCfL5<#nMyoHRWII#wN~)4DR-*|tQzuM5lbc3BqEe=o)-psgUZB#MNV?_f({{T$Imp$$<0*?r zoQ{ZtXodDc-w-c+S7HmvqC>uwJq(cP{V2D!TTDdeS{9y%CCBGH4&)dwnd*TY+>2(Jaz zBo#8tV$dCQ2H1X?{*JU9r~Edjz8l}pFoQG&xR-?{_lv0VZ&(&91AsLINxT+x`B$6~@(3c3lRKOffh=Uvrs~I6- zCmpWn!#R~|4V_K)v-aY?tOK_ zC^yhk3?rQXpmOyAdn~4HiLjx>xd_o`5)_~W79o{>k41;gf=Bfp32}tcl1J5(%|Mp+ zVJ0L{bj0GQN}6(kG!j>_NVq-O3-5OI8G~Hi`wBxwyuOgjkGk-K%GC$zvFP+gNSS55 zQX}W*jE1O@4U^7}h}rsA7!)hVLro=1B$^H)la=!rmx8N42$Zx`^5vuzd?&Y}ytfew zN7|1?X1A+%403hvD~!Xzc*2x3j2}#Ii_HV}3WFIGuYrw*n&m3vG>U>DNia;f&twlF zL^>TyXc{l(>Qp(H_uDKHtKuy=!gWW$w;W%{*Tqsy20KRN z^42l3N6)=x60~)UOI+A7a$B7O&u@>#96rA<7SkKo&VeCNPtmeMbvmUORsx>7A6RLV=IAkNDWWrre3Py`u2(w{1#B#C%<(JO|7Mu$FecH$t?7gdS z>lhiv>E&}ki`zkq(UDu+y{D+Wb&RCr?D9^~QY)7Dkz3l|SbE3e^4YChxkFrtjNDi^ znBr<))td}PyU}1U#f-4gW-#bM{rH|8B3sAEU>26oYFPs=og3Mj&W`%6Vem$g}) z@t7U-0=susa<_91zsBB|_1!sH_Ccx4>~gc!rpvtVHurSWwQo$G;HXcPj2EzBR4?AY`)d_ANXhv-=&B z%hk3G^aAs@x4~>Q1GSTCqbJHU%N1b5amUdMp^_U$V54iCyp-?3Z*?Q93_&?C3AHdsL2Ndsgy!-G=Q6U#-wZwLH=k@WbgG|eD_ec_BF0MlI?2NXf{{` zAswmp^eAyz2F@b;10=@SYq{=T{fvE?%Xe=8JD9htvxC|C!H9e_}XwQd6lg;PduVt6R@E?evl z1?wP@1b+F9mJQ%CPafHZo=~t3$`Nhn|LbBCi+`Mb5d7~Szkhh(KzaaVvZeRRWE<3- z_Z{3lS+oN(%S{Hu@JzNrouMZew3^IT%qdp@GpCho!_1db72aN{N_L~1HER%}ssyRU zO;V$h-YbiCP+#t8BI3`LN|{_36m8eyVQ1Bg80)ez2TDW!U-sTSz;W%Y6Lq=vrMn?} zLLlk;ie&N{Pg}BN$=M}YwzXN7H(Acy_{x?nTe2lvvMpQ5O}MOKNdmdeB@JX)CcrQu zFs$={VS5ZD4?+eWyqP2rxFjJYKte)>B@dX)ak;vy%3ZEExI z%dnh!JyTD)jcXQ(97eRx1>hf#WP8#umyTrnB1QS%z70JYk!*_=z`aDWftM!OqDZ!G zxqjUiGjx9pX5JZd#&wSL8+o%if>fz$c_|`WS8}Zs%`$f#$!0p8Xlhh6eRg6D`AHfS zJR3k7c^zk=d>WF=-1QNthdo9FB8I^Ov*H`xphM$LauB^9)^CsEUo8wCwT zV1uWY3!0sF{o#kd`P$o?Q&aP{$+4i>Mf&FbIuMNe_ETa zj^i<{UnEN~^mKO?(YfqF6fha!ZSFKA^gy&0cgUcQL-<_MX8Q@WixfeWh*H%aBKy?p(oN}D;nIq{9)Pa8Nw*zi<)xLEp z3shUa^>H(;E*y=HfK?&-3Z6DS<1GZDAdAKg+%*-`jsGE*Y-MzUi3l%LZ5%RSZF&@e7>$KV; zsv5DFrw>Z3*8PYikA2N+$?ph3OtKGcR!+prQW=JXtU(XYg-cThzsJ3F)t3Y4M za?)Qazsa=gAjiP!+x4lre9KFYnaf4X`iV1__)}mm&#`4s%G2N}wrsX7e2T2t3>*{~ z?Sa+!!1U>6Nq1azn`5pvPZi=iuRyiuo6ywRLl^N9QDkz(4 zN-#%Nk_lN%G~9{oj;`hKmRcN(W^Bn+Bh#*f`2S1K481)3U}*27Alv^N_k_JK+x_H~ zjotsW`}Mnp-3N9)v-8VW@SQjByk_Sm+kdzH>sP*K`_XL`#PSR##t&|I8^p$y^$)MV>xvnEaDB3#x$^$?XRUo~?I+j1dM&&5{MA2S z{khdQt(I5!!+#zA)$m(a8Y@uf3!(QP-*kdJ0H0rZ&&qe4_J7CQo%Z*YUp?*D`@cOU zBLJ=H;r&viK~H2*%{%TwC?40cX1rW!>Yiq#i&_;EMaQnh&7jl?h}G=_iE;%Z;7Cc6 zKy7#~twO*-xvuhR-jyiV!$Gt|3UA73DNDFQ=+~pG`pSN})R$dE^SjkTp~A=#(oVvC zLMMmg3~aXYc^*qvp*o+or07d$5Io%WL}`R|@kRtxr)HBX)1X1kfe|P^Aellr(X6&} zxpBO0g}yM8Nc-5(O)JEp%%QDzyA`qYaXIS@1V5l$ebB)BgrCDq*2BY(%p_7Ig+tBB zSVEfdGGgKdsFNdrTaE=%zPiD1oXZFWBf(y;G{VrV8*~p1jHG17stMhtcsrYfS-+J9 zC2HFJgrCcH*)h!HgOc6gW&)I!8AsZ-UoYT!S-=~4g)ES^Ta~Sjg||s*SjR0iB`BOS zm~kSw5``wLeg%SDjuyRU(h^9hhjt{TgrHcCYEGc$Fv&HMmC;NhR<4c;>0uQcC=sFE zOWHbOaghX;9p_or6}oynuC?7f93?{k*GvKuQ=-?mOD!!uY8pYQq!lJ)-6n~;l^gom zJ~U8RBx-Sxw4xqJlt#KlLxXC@#>Sm|IXWEG1x@OBlX@}I28F!F%%~-dBdl*CEAO63 z)cpEHE@z}j&xbNd%0VD9(X@j&uG)GWreaNj;9B*FktnTv;84Pp29eyr?C~SS!CFeC zXd6x5U@55OIfZUTXh%m$yGHbxaD66`ZIs$E3+>UO#afz|?hyq$K`MR75K>*gJSbqs zIA>?MF&+NhgXt*YeoT$TH2#M86M=GGtY)0_=ED-wQ@TCn1(WLE{+&GVT?T zQj3x(Q1~N}?=UddMRK4_wL8YGZh_7abvTCBx(y`E97q&Lu3;xArZ08dmXzQenxN|m zGS5DlK+UuF3uiH1;t}Vp#ZbQkY+J%Ngc?e452`#Hj z;nGY(fMPc1X0U?P(F{M*sUo(CdUAR+wn^SfqGKhAkul%T&?}G6BzTUZs%*Arfl}>w zvdj%D{UDhbr;7vF=rgF$CrloTR|Z*W}cqB4E z4JZ7LnM6Iw_Wgk+mSDVD_A_QF)v@tN3h`7k$bq${CDj6*l#>(@Hf9n z><~}JtASaKrFgexxIC7rgS$c+C*f=q_E!GinS|uVg9I7xnLN_0ju;U9#TXS>6Llw6 zEM_>5HB)X&!D<5`^sbo%*~M#Ednk4KkXx%s&A8Ds3Q;H$kEEn_WnAEtSUDDH6Ae7{ z7c+@O+aF=|c)k%dTCI3;=(iy{(X$Dy6}4;aVI>%lS}z{21kTF8I*`bFZAYpWP{Gbu z%LP3hVNZTT zli=w>QFfRC2=I!wxGvu*Ofn`yres^o#!9S|YG%e7R0|ST=x=8dOr7+zv7pE6uBcbX z`7|qyotTm4v;vZJ^Qb^|wG7x*kCc_np+v!jfqknKy>zNbI-th8P1ET}E~cb=qjZKT zJ20%ZwQ8AJdEmh8<_Q;gKSYKTuAW9=j1mJ{GIcT*lg0=q_A2bakl@l7H|zA(83d7| z3WK4Nmu&D-yynJ&qQz=)*NkLA0m;^Yv5NUXjEaRO5pK>P@GeOUFxnnA8bvsX*-YCK zBH6yDrPz4P!+QNDQb|k*vt*zL5You*X2(q&>QLgimYf8siQbxceJh>yB&mr&@is9k z7=tui3J+%z{d`nbn_i&;S*D})st~WjblWgf!_s8L7BiH?)X{z?m9;`|n@KnjukhJq zmlfFYkSSLyP^rO|ZP=|O+qG!A58QvrDA}a?jg_a*B$!-;6N*sX)=14Pa7{i#7He)p z5ITw@vqCZ%10iIvoeP-IUmr^FXemn-SfR)=%}z;n$K$S+%Qd=IvN9}y!O}Vcg(a|p z6MpARB3bPP996Jcf=xhq5En+oT%e|vk;e?s@x&kbDw?}q>ZOeED`yg@F=Whv#Ft7Q z+Q`P+qY5p?5|L)RM8LMe%SAHF;5jz$d7&RXkjRY&jNIh2n3us)AmOW)MX-d2q8UZA zs$J64eMkzW6p6qfbuGzgD#}Tr zuLH&5Kl_wCFy9A#YDQqDhLyUV=QFTUQiEidN{U$#(kqByl9P11T`%TkOoP)R8}Qa@ z^*|y|Wn~=$_n~e8t6mg|#9~&P%{943Y}nGG}VD=to0W9=-CS%RhISzx2nK+)L|wZ{NGN`%Amh&c}BAot^D>Y)7|#ZL7Tb$<5bq zUfKA84Rrmz>#te+^x7k9&shE8)t82UJN(ef=T^R9<+3(UOV9cz!4~m8>e;=3^-h8< z^3=Tzw$KUne9u$!UCv#Ao$8gk448dUuth$;%Yb!HfGu=LJ{rb*0qa~8Y?0&eGCi~I z0_?8m%sdIUXpoME@m_k?z6-Fs?pf<3*dpF1dUn@f#!0Y6KG#QJi`oXc+GWtZ`AWdzrKPMZxZ6&eW4&iw5av81JQL4_y@O zUV2tL3ATv$QP1uL?6oJr7JAR$23zPDe!g3V`K|{q3bx1{Vi~Z1b5XEGu06|uz2*+E zQ-cJKhVfp&suu-YPyNE|qEol;{Ej>gXPH^dg*#(_`S@nUD!jR{`#x>kiQ_DSH9l}+O@ zuZO3|Ni!-!e3UHt*UOW7{u<|;8$5Ku!L5jJldxQQ-C|kyfd`h9jHr{ zVLss#OSv5%bp7FnL8;O9)C_zxb>w!ua78?EW&pm^44mQ>e2UZVERJ$I<;O!0zDSGc z0CEJv58BSu^ZtQlOO4*4$8Q5t&pm zZHTBOD?2g#S{`PukFEi4EKLW6EWsq*O%1L@g^vz1W>ZOJvUa{Bum~oAgXeS!%|-hh zkqHdi8>(uwpaUl!*=6f-cw*xOlPMPK>0YL4YSAWI)rZ$Wea%+08wdV?&T>Au*tF{h zrAAxRHF2WU=%SeF^Y;&Px5kA*lF*%N;z)A76SKf5^0Q%uM4kvk*2 z?|8SUmLKp1Sj*?15a**WGq~P>FS@>t3lnb`y$Gy{^R}a>=Mk(DhmU zzgL9)(Aq23)>eOO^=+%l>I=i44FAU~@451pE2S&Xzx>h5|KW1~GJfd`mww~Yn=chF zec9ee_I_lqw}}H*XiWzkKUsTkqO(w-Q^S z&G&4+WwQh-|NYs!P>_`_P|lt55F?JzVg16M^~yVS3{o& z{rGltdu8kY*?RZZ*M>s)+Jje5)c*sXIq!Yq_a^tR_{AQ9J0|uh`5ks5!`d`&PKhI{ zl>(pTOW_|nLogmco|MYr%Qq$OVtVy?wv6i*!(2s(vm`C(*n(?XW)UF z?Q^NG7={xI8Ho`p;U7FxaDFlh)d~k(5eqySe&?Bj^OI4|mr{9wPl~zlzdKWKelp~I zfmSLanJI^V;7q~!$snq92}FsKtPuYGGX?LM42dh3=~A(p3V+|3g7b+et6W7Oiwcv^ zg}?Vq!Q)vD57r{3$Qg}Wjg5~A# z_h4(vS1wxpb9&Qp3{XW`(?{)w{uSt&ve0VG$tlTVxm*Il6!Mp!LG#W+uGx}Mky#~G zq{82Irr^A~(McjN)1@j!hQIqv!FhGdkkz7;;ZkBDeDh4fd37rTKYN?N zwT7otOjXGx$!hpJ&lH^38d_%9bdE}ieE6|51@F)rwv;T%MV2dszvE27d9C46C5mEM zkx7SdoMEQo^Hw9B<%nDmf^#1JyUSrOi8j_ zP?981P~o?nDLAiiAhUhCRHUU`KK$l01?LqmpQ+FcL5WH_{HI1cD!3DW^H~ZvKaFBmN%NVMToGz#;oVVkw2-ZW;K@9}rNZBIrr^B7 zr6?{f@ocJ848Q42!Fi_OPFkk&m2$cq{>C!}=M|2l%9U~^S;~vyZ#YwMUg7d0omRvQ zK?vcmKT~jC;XoNIn&6adxfuSsGX>`ruAJlftei|zbogt}6r5MMY&BEh4@=8Giklg7cb_%*kRUTNGs} z{JJv*=QW2@sItTnbe;(ZX9~`14kMB2ic~2vnegOH!FkOgbIBZ$1&Yw65q#?q|v~Wv4BBQjxh!n>F_3r z_f!x7o^OLla7jyLaWO6_-c`x6G&>k+u9;C&X)6#0bI*QneC(kagL1N46$B6sowhR> zcL0*?mq$ILtZ_M`1ow&LS7HA-Ra(mu*3eO%8Zdjm}eJ6zww`*OdNc9I5&!RvOw z)=U_8y0q6CN3SM_{fyNsHf5?*?-bmzWZuD`c5w!onv02O!3;VQ->;A{83xr#P=u)J zeFw=!S%?>qyo6^PWo8)$ubnZtK)?3KZH&~JK12mB(#S=9cC0J$x|WVhoC)g=)bP6~ z9N|k=(_e_ZR1+Kb307=9~Ho;#|jV`f9Hun2wXJ>R6;Os#u8Cg~rA*3|=#1aDl7t zUm#p~83xrEg9}`B{|VMaWyU~N$C+Zfni0l)5=r)}Uc^UoOxKj5L8gKBeM{*NS=G)_ zMdKosJI=4F@W#g~7iVyR;tR_#kY@}oP?6*!l{}VVP@XZkKz)#lRQOniL21U|0(CJ+^B4t3%XYiWG^6QcWZQb_6Ou#V+YYokzJ9iH| zYO$yQ$eoo&_{XGmDR%toi)8ojEWB~&`Hq zwkR@j88mJVZPB%k&=v*oErZ6+p)I=B5!#|Sw`I_nIkZLBIzn3%d$tT(W)5x9wT{s4 zK90?LEIo&Iejr;S7LC;@qh!z7lky*uw^wy=oH`HC~=Wwy`(JAS5@*}_#h=K_|- zSqJF_k|~Sc09E9+>rhmYP}BKT$4$nn$UrbB%~oKU(Sv>BJbul0BlMstm z=v*)2CBrB(ZVcg_n4U<-x-@Oj3dxy;k1MF!OWjKUKj%h%igOomc6tiDVNW3fd}5zq zFSCU!;vT%rFce8lJ>O3CGD8VJujlHxUZ8VvtJ`<8dD11s<~6TTNVw27+sj{z)iaHr z3g+i7z0BwU^$jlG?W;LQge|n9mU^v{crA7_cbRBrO8gK7RZO>c#$GpB5Dex0#d(g9O z&!W5L+zR?Dfz7#1VCsI~8`eAR z#tDa31kk!$hgNLS0=O4bgJ9Ugy!PWV(!hB)Ule;qz28e;M-jhxy%g;b6jp4iw4@JF zKR_KPhu^iS>0K||Bfsj6^F>II8=XR~?+1*iTmxyXs1Y?%5r)S-?ixtu$i&lSvra{o zE(;TxtV#{jbsIQdiPva#gjHx8H=^40U>K)>f9=%t4ehCWKX562DR2*04ZSv))@8W$ zrUaXI{h$npH8nNI%YZD@*C)=@ApbH=%{g}9Nqg>@vjZo}VLUaK;QTTm^GC^3-Mn80 zQdVE7}e%|nQ?ry8{>3j)jpwevd!1POuWR(`&#yiwJXrb7l?Q;(U40(;s+%vo5?aw; zC51xUTPy494{ohrDZb{0go-cfq}k^P%5nDV`T#CPo? z=TwJ$Z$C+R=kr{fGTiuq>G$R%OY?lu&99j9j2-b@y~t_SDV2xLXSzD&xca5j@3rpI zOka5O%co3Ve8e<-kyEcjrn3;^^Erm6>_RU)`2LBdIex{>=U)kJA9CEDYUxFe#SSUX zqDRlC2)I4|k6$zW{-?&$3^#9n85l+BCD844w08TA;KDPu+hgZ>;0fx^1E{I$J^t6_ z>G$8hX%UsOY-==``~D9sx*&(`rXs-zk0!|ZFNR2k1m7tj4Pq7L#(YC)*Xk6hggTIcSpw#u-ZhMBNj|Cd@`;BPCWAL zn@>B?`y=bId0W|Ve8CCkA+v*m_quofsn_O|)Z;%s{r;0@EuuE>=lIA2H@|dB?bxkt z}OfX8F9EUovHR?1r{}#B$Lw=^@KwYv$bPo!)LA z|KI_;5B{g6*}dTAl_|T#(exgfHGI*r>LI(i!guEn;DNdOZ@W{X|Lv1YQ+)o-%TtOc z?zFeIq4=V+*h7lPw=(DRJ1~O3_mxwAzxVS?^Ly6KOH+QwmM*Y@C>D((u^7JS^!AY7 z;a&CI$vrTT|MgQ-X1`Nfn%OgM?g49g$ZU33y-jScM)%Uc(* zoOfz|fh{`eK4f=d zRh-N3U{(C*$EW;$@#RbNd)CdZg{xv=GTcLchpXaTW(TX{=Mqz9Klj~BGkeC(%|)wX zO6=6^p@+l{Z}aC8n=<;@=)w25EY0hIn;Q#P#lnM|_igUB39IT2T|HdiHpLp>i zmdBrH4jw+AcXNI5s#x@n^^oPURdFu6gH`dPKQ(3d?yp>$-Scm*EnXFi-rXOvJFzOx z<#(_u{{3G}`Ta<0S$;d=MCf^-1mESqzZBj1!|<=I{q|~j^W_`=#|EYy*EAAxHQFq!n|mPxT>~A&-YtSwQsf>W>60rbN7zD48c z+!g2oC{96)@sfYFr?Wzy9aP8>)20T4Y_~hdVW+f}3~hu$M@(c_&5Q1|mKZzaP+KNC zZoff~cr=&s+gJn?8qW9Ocyid)({&6x|AeDK@@VyG$?-7N%cCd zW-v*moU3LhUC^?FL8GCJ%_eX8@*I;(`YSge2%1jFvHwA!1di?>-=`k@a`nV+Y1(G@ zmrfN&km#KF^p}(3(_eOf@n{5^W76a;Q%z~kK&v}~JsP#+NZ08Mm2{VGj3<@OFt3gq zXk%Zr$j;D{_(HTEmAq^$sn#d8^dL@Hwiw_#p&VqFzT+4i}-p9<_zzDES( zdAHfMPaJV*;BKNzYHVJqT2yJ&EyzJC+0Qh|iaoUBAjCu!=^Cl~h_OFW(-o#B=Z28Z z_oGQgjwMu(`p8VTM7*eVrj^JNBb?A!%(!a#tWh601|3j+rCzEuTgS$FYJ}JS^oT=s zn5ztdCK_0zmjIRGbk&RFgC3=*jZAh7>Oq$xaTU%B{pfzsQ^wGUc1g~LV3b$OU7?w; zDEff1(owcXvMn-$*QvDJW3CPdtd|7^4Rxk6Y81J2Z#>W8|9HRoI=!;LsW=K z&YSoSQLx%l%^McekhNcrsJ?7R#_en!N>MLY7??Qby-5f!jCplM}&9+&zb3(o7CM%h~ zSZU62SbOgghjc6gjgelFZ*{QR=p_^F)_OiN}xwV$JLNT=eC+xOf=3+J2FK1NOAdSzt6B`^z)Vu@S~ zYel*}q)qQ<6L~(0RvZJwCyRlVXu64bLX~+lJA|}uUEzcdOsK88K;c)JPMYnRfjb%z zsi9vajQO>>{lOy+g?Ny0@R;VX19Hr#>m8-rBFoHZpc0tEV0FCjMJ5ed9yKG|^E}Uf3T{g$csMC?QBiAdXUX4fSx8 z9B@G|#|Ke7rj+ZwL2+Cbn5%@qclmxhAX8SS*XP>x`K|31e#Ak@C)z~CPwH|_<&0`M zDOtQTj8rOYPC$B1cARPqak#=pi2Wp9(uHOVj!$B-Oh?hjR;nIARV>k$CqoTnatY`G zHx`==CoC}z@8A8h(AsHt_811oPu|%NemQYxKlo+$KVSN`mFel&{m*m+j^>n|D5@+$ zhMUV$tx=Ebn0eFe!pS0yk0r-Ldkz@~B`sam+aHvRC_G>!tP-^muhsIIsNl530p5uE zDc6HX?j%#v&}L)IT&;?|yg4ehELma66kFBBIo-Z=>4-zei)29kKu~6tn>tY9HC8V* z(@KPosqJWXFygHWD5B=dvamnIX$Kt#X|=%CB6$VH4YHXB^>TF?f^0+ssT$E{7nM4) zBV5e|PT$Gq$2v{-+x(*eygu~Mlc{k>V`x;Oak^zPtTcVla>`nojf@~7AxsJmZp6jt zm}+J%KNqp~^9EZ^(`_u>auYeHNC}0SnTrl&wwJdlx5*|5*mLyC(2X)zN!RmeUZ|^H zAtwU9KfU2sHQVC+SYq0lp?!^;8dz~h8}zS8AWXO z8Llslz&4|dMQVZ~I+Gly4~lTX^4k$L+4s$)S>r&ov|Y+LV^>e><80R~lza2f4;$Zo z!~xf;JuLwdkkp^RS$vT6V~pv$O^quzCpC#6m99o2($IAFvtGVE98O{~JCr6x!(cA6gpn=>5?}0?ExA&jL`fDbx=x4R!ow&_!a#AKAO{On~;{2LakUmqwet@Lcc~ zDK?+{;vx~`+Kp#S=}bHS@0O-Bd(C-~F!4v8apUQ;`PBD*;QSN#JKhIg#Kq$xZ6CpJ zJZ&}|7vXjGbR2*4G2M+{#F^qEZ6Eon8(%u*PV0BU{pbw+j^pu*1m9nz?IT}u<4b0` zb`idZx^{tZ{EK*oJo2m?R{$rl_%Fig#Nxk5F!=@AJ`%cdc{aPDz3*Ro#T;tp6EwTM z8sNo|1WUU)}0P?`@fA7+e$L#(Ek_(tOc4t!`+WWUlL*4Dv1G~R7 zozAJs2kiVn^E*}Xje+W5en z-a4XAEOKR%Z){FM>DUsWM=RyT%jnr_$~#Qs%WiB;uYBpuD<6*1oz6+Gy0H$Vub(Y_ zOqcIWk?_dMjkN!_oP{9~$*K0GBc?dZ2IP2$$dyfs&gd|aB3z8+lsKVu0P=)*UhAEb1?J}mUsIEepW{n^mo zUvB*=_~ZWP{s`P3fq(WR@XgoW-n%2nagmGN`NnB3mjebO>`pDFmpD7fagnRqTqH6= zA@qdfTIE8fL6;LrC9ZM#z{}_Oa;HCtCYl)-lvLsd!wLv72LXu3O6x2%#&Zy6c035G zRI_ROE@^bRK5NE&xISrmVp2=6ER<=rTWviNPl3>_gVH(&LA(cbXQnZwulb^2ULeTv zT>rjl5Zw7y`{wU*Q>o)s`_2z?yyFV@gB*drJaIveix%;8rtY@rAfA9=Abx84iI#aS zX_`rqtFi%M6!a!GNDK-zE_E3bDv&ly11A%#aJZZxh$%@Uuy;i$7)xPQGWlBVAd%cc2 zvDL0V{aLjgI|4_c1c=dwYY+~B@Q6qpj!&CDS+z&+FrEPG)LLg}-RJJw3}6XtdS^e? z3^;~hMNK~tqcN?g;mJB@MU!nF7V0t{&1!fa)Yn0aLZafA?%E9Sw0zA;jB_Bq+3ZNy znq`@`$1KmOv1+1SK;q*fgeT%f%y&yrQ8Z)5ple4xjm8SiY+k3)R7_3gye5RE1%^P& zxS61YlUd>UN@D%Go^B?Tdx9d0g!X7 zjq*{uo}1i#J>FJ+SkLGPJcz4;V$cmSUo8!TQsa6rh57?e?G)Os%)-fX6G0nPU+rh2 z#ST`$OkGewty0WLUT-xz(IF``T_wjTZoMroyCy)_LH)nI$3yARdsa5rw|{lx_cp$N z!`XPn#_ra8)<3xZ!|TKC&#hDIPrLM?@K>%qzV>66lHpISy?#gD{pmIKa&PA=!@qF( z_t&0t`3J)C>c>OxS^b%n&s}=j>esGHyI-^Y*1aEF`PG%ThQI0ZD_38z_xdZ}v&Zf| z=ki|Y^Sd8kd2r?0)^}_*wxG=~Y`%Z#xI|i-l;r-P3q+GY+NFLgXw~Yc{EGj$!qf#~!2hMoTUiv^Hoxt zY1f=dV1sc>jMUs%!^>jmkWvERW=r?RGGnv|x78?@fu+o)yez%?&Y46!TNQmOB5K7< zGS+G6$#_4&yRDd6s|KaahM>EX;;b~a2ja@LPTp7W`?7B$KhY1o&1 zqbA1N4y_6OAg=J;&LpO!drgCi`{92-!|1h~Zbmg}w^{5+t#RLxMjB?ds2bdL3y9J# z_WN899qR_NH5=Vj(`@wfxE9q?x*5T`*+wf94EhG#k7@mCrD(+z+|i(6Ni;S;IAcY` z7;PNWtP!4x#`R9xh$bm*q#>0e@bDi8TBIIrwM;md^;d4rSOs>miI{qsu(*yDuTYu~ z<9$s*gBDrp@&q4|lbmjFsWGwon}-sLJFZvzsphy`rf50H4RTOTuz7L9b;wqV=;Q}1 z!to?mTK(4Os=l(Hnn*^uE290J4?MN&MLRiWT9xQfkwKk_1ZT9I7T1eZ;Bko<%pm-# zI8;X!#Km%?6$z3x0qlTB<7`2pjR9J0q&j$|+*6#0v9dpt7+~!Ns{3_jV3gg4WN75b z?4X57DW1^f0%fDckzOH4NHxOHOrl%qc1F4x^s*ui!`-?8F>q#*w;b23k$ehi#fN#H zt|qbQYJVnSrV?WT&a+w!PW5vh*(Eb^DlkB#K_IJ8q|f#t0tv=EUs`R=By^Jv21v8B7z-iX}M8*t`f5ktP5aUYCfIT9RYY~JS5>lgxK#^XqP!u{^GhfXrzS1MW zDh1wCcIvuE|9%p2lFPfgoHjhli-zD!FD6YurX=GRmDSPxn2dgU75R>N~a$MAGH*$O!7bh%qskwCIS)mV4O|j@M|iUxC#q0r&K{sW)Tv z_R376Xg2e16L)x@GsitI;kDd!Qt6fJ1wzV*uv--)J=4=cpvcZ_)us!LoXryXUXarb zTE}S+M%Ebja;$-*TB&NLRvlJRzGViSxBAlux|_}q1J^aHL>J;%z6NZAQgb?d#Gpqx z*BOYq>2Q*0jr&QbJ|iVfQXDEu>4|JcK?zgG9FD{`X#n?5w5E1Z-L&0mo6#B*I{fgV zgu=wzM1kYSToOFLL_v^*-1P8#Ef3r-C3M22(=NnW6OCSNA46&~or7@Qp?hbS%$ z1=s+Ts>&GI=L@m~a65RpYw$m8fqGD|)HHMHnLSz(hFUo6WuoiZ{W`>~z zBsvHK{g|Z`I zQfx%?gw>fX0bxTis+MgQYN=7B-L^n}&ZbD#C`;<(c^t~98H)-mESsQ^tv{IcobN_D zq`_k`J66$DYup_vi9svDaYIKO#d5<6q@|Rq;s^fDr)LtyD1`HfBjzwSTACm89e$12q=^5#+{U;Ng@)LK#WNZ zH7Bxv`MYKkRyXcCSs@T2Y+r<;I;d)tp^z+PwW`&epGc2dwHDJ)O{&J_w;f2Nn__|u zMBvbC!LV6_DYn&i1Pn{{Cuyl2pA@}*(iVFqwj^yFYNGCYzS^LJj3whjzF`v|4(jRH!sqV*=AT3zWx7 zj=as6%@$NcK$5j+x>u?s#n`x@Q8XP(jfGJp?XxuyRApzHJ+D-YSVm{ojVc7^vn+T9 zsmJ<5zwQs@s=--Za#9gTDY`ux)guk|KnhkEpBZS7VoyQ4VMa{et8@HgG=<0ZWY)63FsxYb+43%=} zOFuuuVB!&*bn8SvYa^5b;?r_TJXeO8Y#h{dr7d?@mHNp62ZBW}yZaG$e+@ zlHI<>bY#dV#l>Ny$m6w6H^@X--s!ip2)*{AnMBg6C#|wjv9m2gYC42evXXkCXWCYC z1a7|uLL!a;#qkNgip?Ylf0T+;s3cNTivy!1C)9xF>6%l^fM+_qnDRzkO9ZK%i^%3D zW)jGxr5Yw-1LLZ+h71G(MEnd{O2f@652qEn;QP?fBdwgU^}LUpF-Xkbxp{_RYn339X5qp`MRCcuqMu&R79 zst&R07%nu~VzeQ}BRD6wCL*)(H&-@7+A+u{<*zeV_xB9Z`h zF_W7_I{Znv#};if3(AXA&NsG&*F}R1lPRav4R@`jY|L?hHxSK$uah(yo;S55X$RhH>Tn zp~s$-Nw}wr&gKI=X$?|Bs%vXKyKiKfv_5d#g94kW+NzqdV-$r{xGIz9kew=kyP&oo zod2(_hjzbX^SR)U`=9$GaDN2;|26{O@Fj2e=3k8Ci(J*t_kEjlCrE%7tf#pXfRj0p z#(6ks)sD{H@8!k#xhborFN)7UBcJySoJt)(Bfr}h<9A&5iFz@#Hge=HkTgP0&Z?+R;U`D}VTEEk&ScB+w*|(&-J}gp2uA;W$WIPaE zu~b1iQ8SGZnSy|rsLW(|Te~hq)3I?^@yz--Da7fx?t!cYd_jpqBaNDf6ioICB+N)4 z5em5BXJvc2#JC4t2WQ6Px4Y9d@oi5(Ix_|tbE8N@)$QI~+~yN@SOh_Lu8HHmC}+3_ z9Is^(n5+W=$tTLSYgnOC?r@cES4P!-bL5lNiCAUppdzzx`s28Dnv2b;38#QI9f$b; znitQaB4KmgVmnYiK%*(}N(^r**X)7U(?+bw45R}MJ2|qFsU-SQ6ek5FgE)S6*y#|e3zeJ2i39?lJ@^`|Ue9z< zsBYw~W|3(n;}oB6xE|Qt`@?Ik`h-`y6|`EBhl6&DSk7MspLYGATuOgxdrp)~S){;E zob8F8ZF|l*fBk1|7*2W+J}DOTl*g7Q-D*!U=WNfBpZ-&1b8Zuude}VCO)mw#jJZ`R(s z_T6jt8nO0_mET_Zz7=mJ6@Jkwvhv(jHT0LOpIH5c)whIxIsER+-*vfr`PG-7e(B?v ze(utnFFkZAc4=epgL^-^_tkray%+6%e)oO5-@EJWrgopZ^Orlnyz{mleFxv!+y3M2 zpWOcX?Fu;c|DPbQz&p46Eq3e6H$SuaYnzX4wl`nCd1d1x8$Yx0rj6HZ{OgUC_1_1+ z2(Meup0X|bsùKPyPUee^q!LU?!yH&&V$hil!wac%#{EAzRNvlK>rWn}bWbE>d z)_hi_t)0K$`TJ!XZ|?l<&fhNEcx~r%JD*#&@#@ZB@BH<$jl(;i-TCabb!mG#8s~Lz z=QBH>34e8H8OMs*m@ms>edmAd{Ex%NqY0I2mWnK=u!W?mrCP#$sD?!wOteKz#jWg9;?^qJ6SmTml;&|iiA zYT3rm4t+ZG>17)~EA*+*rWgA}({b}e=mu-A0^s&&#mTkNj`e^8*%QoH( zeI)deWgG8={v`A#%QoH)Jsx^|*~VL;KMwuzvW+)Ge-!$oWgBmVJ{9}Io)mO(nt$b>!+`oOIVw}z80>+sEzW~H>TGnvnt zy0wjO%yg>1Kbw8F2VLWwnS3WoZa78ectNxmTkPg`rOs$F57r*^*O80S+?=&>a$m$ zy=>#~>a$j#b!#xr>)`4$SD$(7!hCzcDkEc*WdqeNFr)cQfg9`f_4=}n*Vol`b=k&i z>kq9zv~1(m_1b!E*~a1Z*RH?z)?l31!Sx5%AG~#8rX|#yQlQjXr=HXNNqb&1R^GVs z#$_9?ue@R94a+uOTX}fp;bj}IuDpKb^~*L6ue@&Mb+-oNybi7eE5WS`6O+0lrVLNa z`4&hXNHFt*vHGgjS1sFkef5>AuUxkA+UhG-U$JcC)zz1;-hO;Pk8^nSWvjOz-_L8j z`qI@~kMHO1zqU6X+IZ;x{{Q~||NJfTEyX_X#lrpl|M6r)_xJzDGB4fV{~sHS`}_Z6 zgK>ZVf81!?-~S&Ujr;rmi@qV`lZm7@4E8p%fEE_wU_?k(mO6;d+*;9_MW!;3%jr1-P!q}o#$@<_;z~h zzi-K#pWAG1{Nu)rjs5j^uD^Qir$9BpKLIrWJ{x{>IJENE%D)B|`l5ciH}*GlkR~y- zpQ9`+*?>5r*uXnjwk*P4s~=1Rti$?~sOzB!-mUYr1i}ZemO8_n0;={67@JEL&3rKL z&bct3qJHCl19;Z~9@$J!V2+EHbEYl|Xu%tToMld}UMNlIAUEzZF)XP>>f=aPSsL%Q z_l#%UcmUwU0FDDWJ_xDxZCdM&3hj<8V(lE0%q7YiW2POUiE&DM*l5)n*3vk~vu!@I zed8*Cdhvsy{a9BYr>Jb+?Nu`QP9|>lstFj&N@!=~L@9!U8#qLwNT1FbOGBNRbTc-e zD@48V!YP;OboNo*C33Nzib951940JKY^GmOQB?G>h@FY{C8zGCt0miCn#(Mm%ELEa zFvXxl`vPCd3N3=hGNV+xRtN5c4SVDgf&k+u9;C& zX)6#0b8i+;CO$sO@q9cr|3R6U?v1Yi_*0ejib^HTtAxo~b`;cF85s#|ppByh1?r4& zXv|>JjX_Ix2dc0%{+&*r`i(CKcvF?N^Mw>$hKiYl4|RrIr{#_tqjCz=TVy&d9vx}r zTuy0`NMk@OjdxsSpAT@RD(j-9z#FFJY-eKF86w6`@J@y>y=p@kj|8dF&{>8HhLr}k zJkFfTe%W*ify#1CELv}2@T5-)okq~j^=vfaxTqbGwVq%nM#*}fi;ML-i7(xyvsHG! z`mB26c~dT++gOq`8hA1mFeu6~#=s&S-X!s!I+Akvw!|kSEtSQ^xTJVXbGfCm&z)ib zl|}iakL#IaZ=mTMSq1eR`*OdNc9I5`1i`h%G$V{VUD|7nK@pr`KV$WZO_?gyI|X+v znMqroS}) zX)60{fHzfH-LO!~FZoopREv6Xb66e?>O`^}9Tx*b98CxX5)xSzjkScO@s6wPvjEOi zWd~3qF&dYgTE2{uUKZy%ZqrwL9l>;*Y*oi1g;B*qK<2u!u{6${%06>Cg+OHs6S3FE zt8jD-YQUBBVi%NK%<2(Urv`;gPMsvX9X?jgMC!@qq0Uy>`C={Vjb}``fNp1ME+(P{ zGw4Wsze2`jxWZ*ogsAF$2gyZQh!>E&gl8LNX6bI!cE<>WXYBTB++8awk+9_ zEXi_cDqiH>lC8yCayum43b%oTq|<>ac1T!e(#$Xn1jqo7Ed&A~Fpme!z`Oy5na78D z!)Inbgc)E4-cPQ&T`t+Ga$PEue($CFkFHxf-*bM;Ip_D&Pv>_IMHy5*EJo_cZfxPI z04M!D^|CW*^E*#JAINuAwxbvkqmfVW)g(>UtImEq%abWuN`}LDrOh~4!3d?0eS!${ z3(KFUvd;tZT$Q~l=?x&r^)Ju%G!lX! ziXN-p5AX{Mn)NF0c={TUGEtEG1qA$xJuEEcRDa(2^g7@ko$iBQk%xu3d&);V{`49U z;kv75hsm$V!@?q_oz#1tUIqN8Z5Hz@->@+Mb3M~c-3aCB6}JV`J$&$svnM)#Y)s{+ zmjUN9D)!*l4}ITg-S_`Tm)ND7_ijFPG2Gk{28x1lWN$2n!gZ0Q`xeqNQciN9m$;Gj~ZeMP4;bch2UeE zHd{8DMV{s(6s1@D3YCj1LpIwUfdo-P^fr+D3FqMEB688Q?h(|5J09m!lrcJu1Xe6t z`BH^IbLm(fRBTV+NT201JhR1F~l8@&^`Hx`oXz3O}-H4N}Lg{Maq$J!pT*zd=u$*f`T#J%h5ubLc$OeZ<-@$oFCIb?xAzAvyfcx zImU_HHrh{FGSZCoa2Bp8R(vQxbf(d;1j!sX9co7X|4)jTf$d$s7oT9We9PHF6Zs^#}B+_!ydwPb~q*krcCOcyD0yopLsvfW#zJy^{8d@OrDTj$K(*SjsM0iF-~}GM4M#aVG6?s(Meue5 z>tXCR)`ti>dRvj>qHDiH=iu5xa((=jlMFU+J(p__8fnBv&>UVGb{s0z2Z?orTt1)e zjHAP_A{UD!JaL5|F`rLO8))$eB`1mVwY=MkuP&X*S@^}z&_KLwYlPR02dNfQn z?RW>ALRi5h6$%r@!8okY-BjEiXU74H)B@`f;^VKdD8&ZRlo>Z{rBv3P!}Mas(DRw9bc$J+lP! zPSR!-gid6wuGwo=|QQdiNUc*sTQf~1-@97IVHh26lEg!?t2!J>*KG$QBvAMEK~1P1|%3z zGorRMkfnmQBJ^m`Y_@Es$%Ptia1sp1Q?U{$jyYGaMCa}k3(58IR|UyP=RwxAuv8Iv zv=yudwRG#Y7*4?hL~KMxNTEQ(U8z{3n;FDL`=Rh&qyyxpox67ylI!EInzuvY2;H@# zaV6f^W5#CL;^j;xNI4LJ>GfzamWx>u1Upi&99P+T6fB1B`b#=@-@TArAAeQDFk3fB zq*B$iv4VGpr6w7<-7F}aW5PYDpdwtvNw(v(U@=vm? zqHrQA)sRXw8^N0W)?R_Bq;Gf0bhimoKqLJa(6s^;?3H5mLX3;cR#C~g>)@N5yQ78V z`uHnL#D}>wi?O<>?lHYwt3ecCEhTjs>o(5!!nHA*PbBGZqL#@tu?!urMLE}w9>MMo zcOGB5|GKc{^SUtenFv5%;@ll9)MOu*gH}OxAVuaRx=v#uRv7REk=5vC*2(R`JtaQA zEk%`JIy%635@W(?y_#wEfZT_iyZwda`na4>9;6IUL{e%o$Kx%KQW>Kgs+Qavg4~O- zT31hu*g{0f_i80Yt&6IfC`)121h<^K&O&m1Tu!#l*$B^qw4ME4Gh5sn_swB5gO#C? zUMtr*tp~xZo=?gN+(Cq5SF?B?cc*B@xoa;Z*T>~V`5|uFb%z6q^Hr3*4bufIcRO1f zMvTmDy&Mb2^u5Gwkh;1y-jn0~Dwn``V1i%xd*`mTkX#>^(^hhAMd9*|h>(Kz`dm@( zQ_0);l#>@bd$>_(g^AnoifKY1du+9lBO$Z_fmgaO{GD^xTu82u%OM-tMx|6DqvP(l zt`bPaw94( z!9)ul$yF++>Rdc`JCEDUpp`d=TyN~?9fxVv>$;63`L4hdu8aM`U%Bu9Z(I8K*3DnO z`SmyHo7*=&f8&EUgc}cC|DEekU6-%F2;AjA-1(ZFmv8^u?eE?$ZNDb)?*ktX)B}%g z{l~3OY#Cd(H~;JACpW&Z>1;+fetcsHZtgE#`(|t-N9R2bcc+vbOxDrH{L(1^z#8ee%ZE3SCLK`Tzm0nK-;2EUf+6rZEe&2S+o(w z-~XLS6W;pd&V};526QSPbRha<;6mYE2B*R|E)qU-P;e^ze&@F?lQvQQ{nHvP`Gz-_XvO2g~GiIPKA#z6n?I!i{2YuDBR27RQT{h;a(@>>G|M7;a&#! zg-7r8FU0zJnCLy{LaeWYxMyF4ojH~`ZL@VD*3Z17_sk2izQ%d4H<_p{|Md{@+Schr zU3)3>)_eLzGmUsvbc(_AF?T(@*S)X<-bUOXxVPRjE|l-J&r|v9H_7O|&V|Cg4DJh$ z-fLfo^>+R~_O16?7dG5$UZ)Me`IbTSUh_iXUIzDtfAzh_vGC0;ziklWHLp|Qt3MIF zSHDoU7k^*&Tkq8_?6}uFPi1es$cf&oUMSql;8eJFp>VH#dxTdm6z*kkD!hD=@R@Uf zQ{mU1e>-}wbdm5G2B*S{7Yg@!m{Z{!FS--G_s$E2dl{Sx-@j0}*F~KQpX>p?_Cn!a z2B*RwcZF}Bv*({V7dREZ@uF{z-us#hh5P%bcU;(WuX{NaKJib53x#_b-0%6@?y1MZ z{kNSVuYWofzA}-XzfiiD!>M%TLg`)y<&`d9DBa89zVx@=la8ft`fj;HUKcebedE>9 zd$|jRdl}pp9=#`Ci1l+((R;#$Sbq=0U!-j_ul=W;-gx=7=)LSk!eZ+_#=%*_|x z_*XZ+`$l==JsZ@<*0tZf_K|D(Yp-1Y6Og5^y$-GY)!M&U`?|H*T443_t52`0tFK!5 zcPl@<(piC*|J(AKA&DLAO`yN#2lXm^k|*dqpO=ye?BMnU`m26WueuUhDvNWR#OAPr-3jzFeo(Kr zN=Zrvm2Jqpj2*mr0{s;~s8?B8C7w$pR3RV34sK1LpZ0;yyiYTrq!g1VWHWK>;43E3 zU-pA~mm&^V^GP1(C?NGs6X-AbLA?t}Hp_}4$>mfa^^FtgFZw~f%Rq{gd607)L{no2 zk4>Pz;0N_;Q;JPV>pk8f?ORStG;~AR94qiQh{Q7ULeFcm6bt*UonAx z(hus@rX&}q#W=w+KqX#2f&PRa)T>QgEX|UH0*nAVc-aK{<9<-DHpO{Ch!IIX2B0sU zK>xWP)T>R|I8BqHD9FI*9-csd%n$0-rc|;Z#aLP8Iqcvi6X-wlfzE8SWpI^BRg)P3 zjPQ#m&>!`KdbJ5>lW8HDA!OiSUNnLJQ$MIzo5T#6EM(~<38cPo0{tg`P_H({=u81* zR8?8f*PRLU2YjHgS8AS6WKk4zSp_@To<-u!)b7( zK#XOm7?{_s3H1B@pk5uu@mxY;Nhz7e4mKyy@AHFtby!Z**=(AWBnms&m_WbR59-z7 zB%aNH5I-^vEa%z;`aOP7uMV@kl92_PrNFAbK7oF>AJnVEB-nw(RW3mT%UPR1zsm;- zdA~XXhnOO>`IHP+_0F0>{UNEOz(Z6X-|%pk8f~z{V0RSp+L# zci%OEKJ5qfYEvPfj|mA;5;*Mccmn;14>ai2rbHr@E#OKF2V1VMoaN(VVcRr za0Zl!11j;>3G@&ApxzOdv*|RaP!y={a1dQviS4fZ?%SWe-gD+Kb8;iiEb0}bvtQBr zjk31is9*=!MBeZC$@5MNS4fCyg`+vJPm4^T|GOX5J1vaF6B!PVvB2+#C(z&WgLNRo-)Oz95ltKf)ADTcv=LelJ z@&w1kcuGlt)drqGf5Q*zHS%;kk&lB6`XYfHykG)-#s?bm8aY4_iA*A$0e=7a6X+-W zpmS7EiLn%$0xRqDCeVNE2lc98PKW^~oFOwndmoxW|B)Zmt4(q;PvUtz1>yk@Zcd;- z><69ECLV-`3KEeoU!-HdbLT8XZSqO;4F}OeFFVKALz{6e~JMG7kQe^|>t^~np!OtxpET@-WaB?bedyRkT%KeKZ5d=fR2;b%MvM1;pRJeiB@by0c zxWF;;ED#MM;V`&%*>mOt|IO0U@~2Kje))g@uNnbtO>;E_)7qFiMnihW=CN_4Mp}3f z;>aQ5Fx6a_D5LzC?I!BMgmkPcH$LOh5CjQFIvi8&&DW4WIYS->dI%!Vvu53U!TLlK zM>FJ5bezoK`T5+39&!|cFq|l~%JazwXUNfL2!X<+;i&!jB2b8__cd)kIewNLiy{!5DuU>x`Q&f+kYfl0Whp|T=aawfL>+R8 zB4k&`2@oMp?4o@^j^=Cn5RFm6Oh|;|OnuzPGfHl^GdP~Gjra*rM6^&uM5{#(RDKTg zS*nbIC~!5}x1BVh4XeR~)`653m*kIu+rbl{fG04dDkWg9+DfIHX(qjIje>C;AGQku zW2J1q=VVmIAdO?-)|*d&oQ;(eTnfVWcNwjbH+Xd%ET>vhibIod)Q(lFHQLnc;huU7 zT>qE%fpM+DH!vn)Mbv>I?4woz<#!!22-hvMUaW}C{+XEjb58!&bZT>ZNfAlv18HLMZsp!fh`wK&nh>^?%< zT!f3K6;P-i3d3PLP1q-cv-*FY02wjHbh$!2G-|6kL_!O}QLS2)hxL7&DJBy2Mka`1 zdAruNP9)kVKw33PQ2VQlcC}qNsAlbIqbU z7#S`RX$O0<)e~bmy$P{wC>j};vOGmIr6|20$&4Z(ae}*ctbO_fNZMJk-Y?Qc!8Uiz z7K8G&>Mq#$C2LqbT5RySDwqkJ8Fx=KC-Ao?KwRp9OD+=COvKYsyIiA+;cQADC}DlX zsJa0S2&trwa!Qyzk@%APK*1ghrDma?8P*l5jaKtx)#jjJ*K|~|itgu<9jA^(E$IHJ z60cf=T%R{(0+Su`G2fl^3EyZxt&vr9R}fK|DMxe70XY)kQchzdFxoIPnR1~X4Vk6k ziDIq2?bT}`m+$mGbei#JP8ehKeuWw|f`-`3hp1w#9K>q58a^J&4J272cNrd!RR&T) zJ5ju~zdm6+y-S^De1GgC>2!|lD=j$&fgs+fTvysmfzk^D731>z1SN%&4Z{`TE}C&96Dh+4aG;LAu_-v1U=`1;fB1lLSdl?^%{FH}0 zgdmWT%C@re+x}A?au`8^W|bTB^T~g5hCCF_S5k4G&DSSAUW+cj{x#esfmw$$e%h( z9>$OmoU1kxKC}8^4>_1rD9NTppON{1ha5B}*h*)+-28!j@+>(DcHo#EsH^eC}iS`~T|7S1)b<^|jA}|9$!A%OkM(2t2uU z*q_wMMyG0IKX?w2MeD|+=hmq`uN?Yxomxi9vk+6v7Qv+)SOSG0O$l-K2BYvTr?*$I zMQSM06*6pvVaZz~+c(JwC`CFgV${y(Gxb7akBmm6X-ADRLMS_=xLZapVW!8FQs7f` za=@s)+KdRlJSiR#dp&%tHzOn4C_pws4ReuJuU}9y-sUvoz$!D znp%Hf>uaF2HK@}1j;T7ez@>xTd)BCVHzogInVveeo-8qL0oUmQ*;cPbj7#7{L?*{6 zgOT3z9`U&VOVcZw>DHND^w85ql?~knJqNINJvvG3<1%=ytywmBrG%moC||!1VNfI( z+(o8eB;5V^q{wSU>)LeyLIAVLNLqH$*1#X$;nw4w{A0hRja;^_yvaC&4;1dd?;l-; zFh3J<5r)~QHFTH0ZA|C>K7F=tX=YzD_b2oBY+fBSgrWdmFIsiYo!DtT#NM) zso;j&T76H5A~{{-F1qe@q zy_IN(=>|Oz7>(P^}OnR~7dJxoodh(|;{hqKpeKwmX41j^aQwpztJji9?()zwa(Xpm7iEF8z%7)FlGp zmvooll*Db*Tz6T+6TMJ7RnHS(_uL$`6J|H)XyJC&%o0gbEbAq^Tus!9(761-?vlpi zI>f`3cxz;t{dhtr@n)mfA6d4jvG%BG)G^gS%^r)@;*Om|G^3p_j)1?uH7?*$z6~YA zUFa4gXVo5@zO^?DrmPCVKYC}Pb-L@oHFwbgt6WX30nBMf}YsIb8@UMhvdy3rV9`Qj~;AyCRsy>tRs%`loi9JFnROj_sEP zjsg#FecRTHH$S@h!i|q?JpbBE$cVdKCrg4`c12Wl_ys=m%m~8+S0+|FOD9{ zC!TrLnx^rWNpF3%xr?88=9QOjz?J5Ve&U%|Od4>B1lw1eN&1OrUViBYTxt5~C!TrP zr5kX?Q}EJB11^zs{Hmwm;Y&B*il^Wumu|onPr-{P4Y&l;tDb@vUAh5RJOwYDG~g0h z;;-5hzF^XTOQdbT+LY%{JoEfZH{eQ>fo-O@`iPv4x--y!Xm<+xU8@TAI0V;?p#*bgAb$o z{vTMvmTt6net(NrDntrE=DOLVu{7rA z%n8ZI?R_%WE1QI5B%)NzZYTPKgi?y6xRyxS{r#b$l!=%f=?{$4>w62&N$3E~N$R3G z@m}_OPWi5@T2JCwlo^AoeQi0OmMfebs_*in!JIiE(#)VQQUwQzsibCSGhPQ14$pG$MXUfFsQ&vAJ!S=0;l zg2a!@9l;Vh)|@%P@t~n68r7m*wM&pwC{gu63g+NAPgI0(y1yuQQr7Z{I}{W4fRu6A}hj z(nK{s8jq_js!%Qt8s%ZR5NE3PKpTdmmK+}#r#E>Qo|C`$=U`6gi{@ndMe#W|Cs(ze zB-AjQSI2^0VZ~5e6XF7ymGOCYQfn94OoS{eOf6R(^g%Xs8&nAmh6z@$_DYE3wD;SM zK`5Kod4qZOiuA&B@_(~nPU08M$@GidFS*ck7$2hhG=RNFxKc3Ury!&X&KSzK)1UYMxt7pjUAER{9T9X^iA0dsk}e?7%p^)W7hY{Q6%W$N#@?Y5SuaFI{=xIWPQQe)Q!L_@a!!lP@?N zO)e26rvlM1pToS1kLnf;M8nPxMDsgQ1HneqL1C8*zkl;1)6w>RCxjjmmy0_)E5h%@ z(<9yg>DD*Z~z%m*9EbQ6OexIs4XqR5YmdLvmfXo`Bzb|MWx0J2y zsZ|vkg}Z1krb%^+C!D4eD<*=OaG4trsbaq*RtglCh|PCPbE5OVG#+YBL%^7VE3P13 ze`1#RgLlIhdm`79%=bNh7V!`9JrOwQUay)Plw0U^)Zw`A*l?u;Wnx^Z7m6kMP(>i) zUDE2OW!{9;TN$J^#va)B)P~(uu2}DO2m@>BI$9SSLZcZs%lQzyM>KMFRmONJtt$%f zKdfve5_+#UNN4KVBq>$JY;V|1fxFRGEE21l!D6^f6no@ivH5P-f$ymu4qV^!Exjq< z6ecQlxD76F>$w#PaD8j#!dDQKS{_f3ZZocoPdUNmQ`i zH_#i)*FSgt=(=_t+WDV5zqa$yo#M{!_Fr#*di%rMk8i&z@P)uH1U?W@1Fr{}{(g4r z$t`K?*4Fao?`{6b=GSl5H`z^i;yYa0XrH$Ly{^r`RTzl%;*IxUI^}kyGMNk*8 zu>OX%|GM^bYu~sguRXf@7ptFIegCQeDh2%c$|qOex5BNwZ23QcS^?j;{Ql+Ea&{SA ze%aFBFa0TKrvJ~wN8#1b%Io0ik3^Grpuj(R_z0amn*LUC{OE@tK0+psrjN{zAN|n7 zN1@51>66>zM?d)R5j=S`eSmoU=m#D?g4{>5d6NPkfA}akc{Cjsag6%d!$*6QN7G;D zj~{*i!$-F#kETBoA3yrO$BuSac_0e~*Ofu850D@PMyPbX=uJEk_+IGf&8ynV>kO?D zEk=7pQxV8otGzF2P(NDD1xHX}AF4WuPO(3(lRYNAD+Rs>I=baz98M~0@iL9?LcO*? zRBQRLQmc0O{Q_O!AkKskosW$pB$UPj-vu3gg^S4uW~W9B!pTa+utRKGXvK+AB{iga z;NE-9YLy4AgtlJ+sVr(P=1nf9&>cykbf}B<;%TSK*IHa|#5YAY5>$m|qmYS%RW%}o zG!w!C-w7SP(ZzJD7LLa-UaGIFk$ujCxI-%^ELzMUorVZRXP8ih_Xr-p7uX8b-{W@CFO)*S#O-N=N!48w8aFHa%VXzQxmy=xr zElTaS9IOSt4LW+Qiy5|11n>^F$RLXL$|WLN9BAcCw8Y~Xz1i2TYOyWJ8pR7p;G@vd zqb`QW3)uqGC*xLox1L5+(qff<1dSyX%#^ThW?q;FEITY3S$?7c*)L2veg}rA1dlY$Ai~7t0o1 zSCf#8O2tOIr1W=Fp(a!0jlf5sqgT5aY-kX%eNjjb3;QjFj?n396{&|Ho25Z)OjFA; zTB2Pv^w?MmJOv%S%Ejap`8-*V)?mF286Xm9z!7OUT2m#h>Xd3^6w6>H9xDia90`0F zI(nsx5sF1)93CYZ$a0|IFkH(jtwA9eGk42E6IBd7heoZmO%}rCz=xotSGbshE*Lp# zTrG7Xb<5iC469?9&^W7C??|>t3msThLL)GsMl$d%(9z3XjFF7C<8`=R2uV!EX@E-N zsyWJrWeU`o$Wv0fClvXHT@piGEAY+G(aT&+QL9TzNKpkjBqcPbhnI=Uu%vb)gW9Ov zP3LN(;;=)M6(eD|n3uYkeV!IZY$lef<&*se%vaFd0MTo)RI?q~l~6$jDRRc6=B~{1 zfp3D29(FOoaLI&PQIHG{=Nf5M8z4#(M>5@n95Ztnwwua}Mo}6IkRt`Y5juK_i%InL zeFCkgTVWPt1}x$6CaQN8M?i&UpBBXtn{vQM9~o~&f`KQYqZhlF#y%HUI>A)730PIj zF-6Y8`C+ZsE5H^3(k@cl&mv5tHkP9xdW0?4LZB=M z%SEyp(HmD$DnhdRV=V9u(9sKBOsH+`Cc5z&s}M{rrlzp{lqOYabvGR`@4dQVl|>F5TS;qK#-19NCQW!9h7e?#su?x7lXwM zv2Gh{(S0*!5IB>G$4s)4D4}#X9^rW%E25g5G=oe83EYK_p66of1Wt}Pv&Jin{39MoWB zE;!_dL>AQqqLV6T*ueXsqnj?K7{R4orU3VBC?aVoXJD!tjK@ueX@g?dxnQJ~r((O^ zS}J4(-U}Vwa4{g?cX8BcSwbY#?`NoVq1$SsdEAiq0+@}!f+MY|UGx`&Uh zPZp%<2$|yr>4}Grb|#Nz*Wf!3A8k(_&91@ke(WePy^O)p5DXWpWzL&(D=>cSXzL6m zSn!}$JJ(Bjk{bm^j~#7#DT67hk@04H3Jf1R+BicAcA3!}*VMgXq=CU>N7v3!q9F)t zr>iqL*#dp&Xx$ywq)as1`&f>HA>OE2q#}_HB_q|{Y`;huiSU4LjFW@HxThuq4s^8U zVj2db<1J&jkM!VRz5!`!zsC*Yz4E}(2L)-Djj_A%IGgFwR=|diR$Yw5m8C|gg?3|^ zR;pF8=q@hmQ9|uOH8$QE$hH`%4YNAO3}Y^4#l>XFR)i`;44x}v?Fc7lV5{C6kJv~% zH5dg=3q?T8Tsp=hZ9HH?N6RjTsL~^tVS;kDsbyGHw47!oXzZ6nX%ri0hn-|7LaDo@ z;ciC_^q`|97h^!ifU$^y9;E)d-_DoN|4?+zND|!}qwD zP)8faNlq}k`Gn9;gqz($6pnB_+U^@c5@+Bt1`#OIi)Cv813G+S(w`oxnRqtTEuv5@ zlXIdij;!~@B8q0L$gZ9U_IRUY(FI8#1a#=|j*DT_d5*2)u}GpHZ|s+cGE zssvX~g)NBnJZ>=%OAI$gw@sg@ojt6aYpjo6i85GOfHV9-!4Ntf-q zV%kME(1H%%JeI=X9^sJ^RIx~v}UQzd~3XG$dk3)G>*p^J%7M6HZbp>Y!AQcTwD7Gb2D1lBF~ z`K-|3(^0+>=deteSFJz|Ivltdnd%f+))Zh`Pb5sc-9hAjw_i#u4O(i?Nfq@hnX3i97CJOs%q8yb1s;bEbr*Aq%VvRh zK!+U{bBWtgfdX{cb}^T@&J$3f!@kRCg%PP4@PzUBnP#}2hKlzwNg z0pYR3${9+(qmuw1Sbp`Ay!82{o7l~VZv5GepTBWB#Zhjo({(t-Cjg3Ft_{ST=jrhh3ul>oj zAHDX(we+=@t^e8jC)e+;3+s=p{l(hPfOCQT+8b8?64WC2&@#6AwX1Jl`J0trUHR6P z$_fMO2x6eo^M4+j>b9Hjb$)W&oe!O*^Gj2^^TC_%TzctTNsy~#(14?faaJf7tPU9) z5A#kPv4)9?HjHNL4L!F{miu9aQuJi&dhCoy-y#G%;4Ss^o!-C7!Oqh&baRsm?|kGe zonJQLou|&y`K1xw`S8Q;J`SEa71{pi@q?K&k?p4^V+9AJ)oy*L8bNv35saigQtf!T zp0KOYVK7$BBb``JEy!}o46gj;@#v15%{YXYEB!t@#s<4=rvfr|kDvit?_?5;FxYhr zwT^SJUS4_T7%|dF26rGn-HV8Qp%3zWvhAFM7jq?zz)*@TL4?%LYcPSV{lp0(#!9;; zaIo#3!>AP1i;cvRhO*#u&ZJ$+E2UNzGc>E-4u$yAZo8js7FoKKG>Vx> zeZOj4d-533PluzyS_^_pqQ#{21Sr~-)|8rQBr`0(n?d75C?CkxA0upAgm(K>rO`ucZLQ3tvXM+a z(u9YkSPF67`mU)ogFKe3@H@d1L~Cs8iCkDAxhB$wXi4c9NU06pm`jOW$O0APP|@V= zkYp?mj}eyANg*}4*}*dz(Mp>oBZ|5a$#ly!1uBWO*1!1}Vd^8vEM@6@ z0$j!DiNj{B*J>%D4%xy>c@2qIIHRKIEx9bMzv=|h6bBi)9Aw6d!sK}?<0Km@Or`p{ zl$IW3MkqK&MbsXbHI|=ujOY<#dn97T&M1qwb!XJ71*6GgL{3X1lou?8EY(VBlTLNn zm2Wym7-K3L)AOL*A&v=Fxu2=UD~Yn8=fr~4v~xW)g$(eqL-6dzNzXe$l9D*F9L>jS z(5S}NLXG5jYz|uusFyXe;il2-D|@P`uv^Jv8SVY*Af#fbWR0u)<7^J(gK3u0{l-3l z2P-BMQY=%D&`=qVZGGP{q6PG}5w9ENc(j!!nPj&!?zgHInzpb)iz!2cE=u6FaAeq8 z`QsBrg3h-`xLO_SQBvK_NGO@J#?<~kELs|@GZZW)AugB_TdRL}jA()|&5+9WaHw5H zO}Pt4LWx2_>K3|{K2{>*Xjh7cc8wmJy!LI!hK`5>a{DINZKc{6O2*mn zuH1*JAS&BQRvLv`Iw&Ps$O^)7tr={se(VGxMY3w53`OV>8}HjnG7~eoW~QE!(}PAx zNfgO?tht|}hl6G71d%9>;W#$bv5q2^OikmmWvi=}tErgMY&4@C63(`p`B>W6I2ql= zOH*9?)?*oq*Q5xXSncA)C^pN-G8V5wvHsWzV&N(htKWExSiF41YUdcSc=?B|A3a7a zUiV?`D~}P27kmh;9wQd7^sstjwfMp%9#;PJC94qFWlZfYCkHLtZ#tH}cu|MdPfr%- z*{>4YA31(7^G#y=sbd+7mtk1`XUD8&zF};C==j0R7mV$1IhL__#f9aYXLc#R1p{`T zJWJv>z|Q+;=-_!p0d@|~()m>b*tvU_&aVu>&exrx^UeRi z^PaPGeu@8go;XYAm+^n+&RIIYRR25gK11i5#QUz9%{`_skipI!eNH~xQZ`9n*AZ{K|7+84ktU;g=$j==kihj%tYQ1P2z3qrqU zq7r8FpuFo*!?D|qNhWmLbTl^(#WuBK2W+ZAe)NW31EDzJg9;2{yN+Xl=cW&jLg6q3 zu#@1Ni^ zhKdA}K*(_r{M9%7YzXAc?6`qQ(+TGNqM_zS8^JuN)Y8)!6n3;swqh6>lcJ42B_`nN zP;G~9^@dbfD>kEj&_j2YPxIeDTPPQrH;iYZ<7YnFxo!4j=YO`@lPI$Lp<8Z>*O~P8 z^Pj7#dI2Z$c+~(p}L8ade~r5tte@tQf$zij>_R8EGL*&yNEiqezi*X zxdPM4!KrqI>$Lam5efB^-86|10vnWhiR$J!vsa~T9(*IA=7fCTx%2+l9lqN&vTt&8 zAJ3@CgBuwHjBM`YUdl!;HMyR-o1bHN?yR3XclT%A^$BNY{j@6cd}X`9$GE|Eb9~G^ zy`A4>HIXxB;flR)Gp>$%%yQB}%_W*HU@+G#7x(LPJUbYnvm3FXRo^c}8 zPPhWTXf_LQyw$K%3SGgA^J3#cJ&)4icr(Ml7y8IcrlR062+r3B&HU7v2!_GYMS|c@ zH%cHR&YJ9$z#6)5==&wDUTjzQjjFp0P2akLqwbu|+fYH#|33uF`+YYAqcZKDFMnxf z+WKcQ%Yz|bEHV9Lc6_|*eQU%3x3fOuI9%5Ei>y22b4?3WimC{04sB#Od8oFEEEDfS zWK|N#6v3s+!9oKWXQK`uBtQaoz+|5HaCOxcaE|lAKq`V~ujHKFia*Rs`xtSh-ozJUbl(aE1pQMSTS|#-;9b=+0a^4)A!Dv+|{3 zl6GAWJg^x#olmf(Rx%tyM)5sM7GfY;s#=7SY9&VWOCtz`>W-KiEmk8b7zZ)2*=VmC z+A9re@PICj2K`zgL&wpY(jSJ1Tr-XP+y(&hu*jV|0r0kaeW|f@<8#-)eW$jK1eP~{ zbK^U%)z{Irm6gvgeVtG7&TnwyY*D4e%@+hsdN*sx*TY zV>B>Ua?iaXyRsxyaoj2;h;#`aIt8VoG9I~s*5k*2heq^ipu?(?LEyRz3VN$g@T1HA?xLAzI z4GBu=Rd$>nb$U%uS}95gIgWQCMX8szN6}##1G#Tjgy-OpU6n73N0UmNAA>}4QZ5(g zya3{S;yFue!5V=`F##AR_uLz@D@y|$OEq|~QVMxA z#x=QCDOV6xd0y%>92+OQ!dU2}+H|tl$`QjbjYCnZO|UshlrR}1&<;VEcs&UWlYI^j z*_ExBwo)F~Wi_w%7`QbofV)w$G;b@5kA|$iyF1P|nuE;#i0X1UQ|N{`V^D##Axx(1 zs1Xk}bsTR4!%RO1hwQ3+89n4Pt$MwrCwm+X5e2�FjK}L}xlFO5Y+^-4wc_C9f@`Cwt+&&x&h;zxSiCEgdA*PUhM7G6 z{{KTu+kd%vWBoPDKXf%O`(NgA#Ut?k{?YP=#uvY7w0xQCFt6c?dY$W#x-54)1tDeT z;%5%dgTb@u@+LKL=N)&$bB?N9TNnxjX~k)4u6r$_xAK9>7k0W|?30G&Frv zb#i71P}9fObIzRs#X<+yp^)2znR9RV(D?qb^@CSbo!jvFXVKsw6o@?MEIJy7{Hw~x zc)OM8U=TBC1v6Q+7_x}8UJQl@B!}9)me~#SC00W446QzJev~R2K@(J>)I|6aNNv%8 z)eKEh#4Yg26CwE#R~log+P8tvqm@Q4dCM9`GW7v1r?IqH8*x|{YsZQmx$YoHr#vXc zEg6I7ee0YplRWAAoja=sjiV*k@}AsZzKp(Ko#o9*R5{0%+&_7&WBy0S|3%g@F~~ls zoZc|pqT!DsuBJ_Y=Uvq5rj6p+2&KvA0>JhiZnw{TJ2+KnSL*bs`^CzR!a>*U=6?KX z)Z_7dWrPz>OjfjZ2#$a>?!{`%EVTBRam75WtM7Hlc>w^n(bDw&hFg0)47sCe4^RI2 zC>llG*_pe%_@q6Y_j!N%!{aTul4FuhaGDUw_vLY|7|p=lFb#{8m?!($ZZB?CLlmsQ zRO{KWu#=ByzQxQy8>dCY!rd{Bb|aN)Ur$k1GMCKfv1+Fc zvbCW-@&EUiHaEB5?1#p=-)MbR&z16^YZ{D3-H*a^%{<;h!}$?6FsxoOg0+QldngK& zl^*0OHQdQS;f@`~M;5_F9=zuFt-S_byCty{NSMNoJ5VOnSA}*#h}uY{7Q={cE(=oN zcWv6p+I1a9tLl)?2Aj~J&RI}5p$!o_Rv}^%BdFoJ9whp*hTsd;{EoU^2k8Yj51-iJ zp|^d|nKE*}o6Pf!e69(**#oa?e+0YpJ9mF{#@YLPC=Sh2*!$WS0-R5k)3uJ_3pxp@z0k_%IHjv^#l@TIX_@z3$)di%PecD9z5ohG+(gWC zeL=HT4nf+ejZh_P;KVdJ7pcj`S`#Ellg8=62psiCYK+r^+F)@3OdcfSpxj+}h|4=$riYd2PZcD23w@|Dlt{HHg+_U82)KXapX<7L-> z?fUron|A(e=LdHxJCAPv_V%}Ivw{B+_(b4q0^3`k+G=b)y!n~U{^nyFzq@g;fn57{ zAjcoR{-4*sXPsO7`?Y_v_V~(Z<&Dd~2eSB~Aba0O7JesQ{p`Y|UlPLX+T;AneCHE^ zrWaXP#a~m@VQ|o2a5|gPf(+ksE<^8|?Q3#nGVLS@$+Y9W zTAivDN8T1j&t>Sf>IE2L=Q5m**PV2Eq|}pE+NyCfUDji1lM=lxjGV(T9G>ZNg{b0U z!K^mon#kmuYO3R5xbi>GWjNDiyNGjnod)N>Hg0NyW9nXpe|s*&nJ(L9wQMIq_E(S* zjH#LVw&Z2_H|H{(-aAjmuwsH6>^75caeT{gz&FO4m*Ics*51J{&sXg5j9!}sWuVb9 zvfdgF?eVBedGk)b_MG-b&cxj>ILG008P0V0Vx>XP>2PF*U#3XLDiq!*+TDR#F4m-y zX8_-LUi*U6cWfse9*`E*WC+`lN+!sc-?Y2~xQd+1aQZ^+grV5Y$42&OP|ua{sy&hk z)6>A!(76m}JWa!{Hq-ffte6o}=~k&K&zK#2F2flsp{*uW>lTNVfm+qIYD4iR&RK=d zWjJFcO`~IWJA(nw+qJq?Zj8NVw;DW`;mjmo%y91)+!l8U@L7e!`*Wk&U87KNPwBmh)RRta|r?0z4`Q3-+Ydp zglBq8(>X;Sr>gxdPt^Dxm5g~B_}F>v!)6=}quHbyADX#galmD0JyZ4YTm6c28P2G6 zsv#8zWSrNHO1W!UnJVLDc%CJLJi8w9*KoBKFUt})Hb$hJwacxUCH@WPws6Ky+BL0d zmj~_9AY&x+gIH~5MSk763}=)kDk-PA9w0S z=W>`Hgoz@|;qbz9Im{T(>{X@J=f8dJamaUSW~^men~pp2(7EF}V;!O{WQ+zy6uTT} zvmHjydB^qodDd}uror`=3$5c^{}P_=&t#Atx+|6pwN)F+T&Blq676l@_IVwi_4x%R zo9de+NK}}}##)w2dChlq<8llsF-~+_y{ZoGrjG_?|%P(?J~BsZmz%Z|6%XV;~dATd-1Ys z-@0HJV1{j`n}z`z)7}@t63Lbq*_JKuHcX5xOSX1fvLxFK44ni5VW+!+CIkpc*uoxo z1QPbJWC0#a@(6he1b9FQdE^Iy@FNSqE6Fomt*Wk)CJp@ZUiTmM>F)X-eb2r3oTGcM z?mY*r*lWw5+I!93<4=6~#O%bqc7J_0zk9EpckeuL`!n0rwrA^(&41YZmCgHXym13r z|Iqr`wNI>(tDjpYz5nWcu6NDzTJRX*nDzK7bqwIY%C86KAGt>1q1`1n8E#M3d9~aw zWLlFP$hB2tD|7pR2#+y$BuA1?$z2}oiSr6^{x-mL8(=Cs&vEOR$@)lm&U60Z1_^Rs zH%W0%;vCl(=UQK8vqU<>&5IM8eJz?}ZLsdUI=nyVoAK4Sb1s~J7|`K~prJ03>^j!r zU^yh}{As{<7Vx=9(Ce7*@-6zo`G*1?!&F=(^>xg%-&D>mpZBie_-(h}_pYK4q4?EG z03Ls1^2)(G{M*ohdaXvYo98`-7NEh4(iEZi)oL9&*21z=^86BbvSA{3KiMDhhe?7G z?VfaUoD-XPd4l0IoR%8%`lNigaUL{`C%xoOz+xE2T`aLg$RFw@2X*`E(lN_rbGZE_ zPX|vo%;D~*$3y<0kcTt&?BtH0euWY2o%OiWWC2%Xyw%z^9ynVKNA43Pw`z}(3JJ#VY+r0sB7`E#o z{kda~D{c4QfXT337YW@RGaa;D<^0YX9@szOxat{~g9izmOfU|7%QpEyk}_ zn%ptP{(xcJ|0kBdys|YoaqIHim!Grw>gAtoes}W|M*jbmPprIp^@(f$vi6ycy^Zch zZ0+@Hcdj+p&aSPmetGMOtADWi;;r}WJ;kUTu)OE}CaC=PHlt#|i4QIRkM+#@Ez8~A z_HJ-@Yv-$*`eteUgX=F_mv-K_I@o%l=k=aDJ)hZm$#QJ{n>*a*Z5uyXz5j`CdCo3< zdGCvRZ{K_NN@?Y`oz%{QyplJw{e$gKZU64})3)nYWrFR;Y_D(K-_!KovhrQezj#0B zeVJ!{Mc?}J#;+Z^b6})tJgM?Md_L`$Tj2IHCXL5~`aaJ$UPjA|iPNS?vDvQG`*W=@ zYjm*rf$ty#>v=t)*2mM(P#$);47gG}wLQ;Uu%37P29b$7{?qZlImG>|g**Psv8CT+ z!pW2BEQ_G6Mv>PFlC2${A6vL%+p%KdV2wwSR=tYn>uo{FllG5$-i||D+o66|zi7v{ z!|E4VudCpjC_Tw!Q*en#I`(h=9VWmQH7HZHc`!XD5$97?b1wEhxJhloF;7B z`}tRwzw+>9+;eiolUg#1_1Gkb6^m1`#@UddD25Z*vc;RNWTn=;Ez9L9--J17XnUHM zG;!l+V+0>NGFui)GZ{Mu;Y?(N9f-2xPe2>btG_sb@jO=#r(H-J66Q|9RX}VrR z^KG=6BKqm!0bVmL@5%9WjJ5Oq$v`XN{h=^W?8~&Jw_5E`iHE4t1#1wtr7w zdX2?9QW-6Wr;s5iFjp!xNA`_d&r_^+RN--rniFZVDDsl7AolN$p034vtbT7Zagzc! ztckTjie({9AuF8ylbP4I{z-Fyk3Cv(+8OMjCa>Jg=+!Dk%bhvi247OMip>r@-*$-m zdxyAhSvZ_(lckh~GvHu)u7}I^cA zKJR$Ddb^2Jj`btTA+B%Xw05b~YId>{9G~+=60z@0dH$zE++RDyeaXZfAFp;y+;|Qw z1!AZq$THL$t$rxDKyA3(zoNR_8yP>EAx43dD=p==q!zc0;YJOgW3JV`vr%% z&s+M9Ie6a7X3Rn7C12rFt!~ zHk;zj#uUjV?R&OfGoAd&NtsJ#P&hA)>n&8O$fEsy>Ah(263J$x39O~n6F6!*N@BKt z_E^u;p;=EVr3Xr>taHMk2<2?JrB9gQRBH&;KFRCl+_;_Ui9*@F+siwHnaSEuYLyB$ z#b%kFRLEk(F|NrrR?lMARHCVl)|-sfEG>?tfU#K=HOv znwH>#p3cwP9A` zZ3~7@@|ji*>my>a3=Z=gm^+^`StlI9YPoz?Dixarf|4&4%2auR zG%LD&$d;^Ej6%118bp@r=x`2-J|m-!trbf@G@o}o?gwTw6mphUT4X(kGKE}GZx-z) z`Tz@7ZZ;ZR~u(UfCXH8tj6N-nj ze3g^ONLt{NqzmZc6V@)bB9=thp&^+-^DSzEvo6wPfSzgrJ^Pb18Aupv!C) zWQ$IwYWYzwHrIPulpx#KG|p5aSUL+5@*7V-ER+0!y>dg3tKOU1fBneO1>?i+?(J(X zF`>E~WnL=WcWU5Jo$k8VWu#0!>Lz<47C$7b>M1w-7+ZC}>ZnpG|K6LY$!Nj}x zC}b*l%pJTVZz@?sus=Dem6<^Im$ z6ag2Dl_4Qh{VOgJM}XkLi-T9(4mF;ASe!9-d@uj*@Ny{eNa(8f0@L#nU1&~*%9MsM zq6Tuf4g$(_7C*zb=aJZ?ri9BWN}ON=Ke&4jbhsz?0+CUP0lN2syZ{eT2$svWT9%;$ zgCR>6L=e?etyY+iwX;YlgLFpCSge<7lW3(0XKBCM9?ft?ZiHi*bg^)z=}$)5WD@$jsJR%=;8yf>B%-ny!>tH=j4EQD_~scAE`Z_ zA+PYe!!Pe&Grx<)5(dMe_i^JjkXr5x$bu2UMXre_&#GxfjMbHFSPo)DxpPK^#26%IF&8HwJJxK<+JHH znQh?Ncr4N&n}H%kXd$#2PXf0GFTOKH7A+64QSXdg0duVmg>w1CVKrzDNx@+>9r)d| zxl3Nd?_NlH+)OR5-3!nt&T+u+T%3(C4A;#8lfz$VZr_MDKidC^^XKGAssgg#0GoR1 zsjFP5Jea8Iv9|{Ne&dy4%69t{O{n|?Ji{m{a(5|!r=D7T-4_Xg0>6wHzg_&-740@3 zclmdRzFPs|WrXe9v+dbdJ_hqY4IM3gY)V>z;IiVTZau-vS5s z_x@to;$y zTjp_8zu(f2YvXd-rn+edykOokCCT-RKm0a_>G)p||ecU_p<)SGfDX zcK_BX^y|BS;}rU}-T&zn`qkaPb_)Hw-7h(Xer5NsoI=06`^5$HxGRLaU)=qyQ|Mpp ze%dMY^SgiU6#CiSKXnTI%gErVfC#}p|4+kiv=x{nvhIUje2#4_FAJFV?VLA`IlRFO-PP~{>9e4oj&yo zTlaDb{ruLRQ|RZmPB?{rc5Bxu^uKTIIE8*@YuhRG(_33kp`Y5?TtM+ovDTV(W^C~g0pO4yJ*@m1#m$!e>Db&0DM5j>C_7j{!m$o185W4k~?Z+*k2w5UX zkRk-mk6P?(g4owv>nrPv#IvY z*wxlPzxG=jfsIdWet+}Bn=jnFee;Z`z56B4ZJzggpSg@JKWOEC%U@r9=k~i--m>!4 z?O$1Y{Ps7u9=5i<^^dE6wfg!KBhPc!e`8f$P42vPk1Otv@`mv-HtD*5MLAuuOSRYt8CtIIpF6s4e;Z z`5@aLglZM0lLuLda*bFgl}V~KA}(<%xqG`Qp<;ECN|PDL7Z~#um>#sE%>gao-A<;1 z1u!`f7I9=AO9l$i&W9}tx}gVRsVT_9$jgleSrzK>4iOE++fuX~9n^v?K1>D*vewX- zpKD3<5@Z0IL`DU@+n-klI>YcYY9eDW_%u_>Wpep^zbs1`sTueDjU_P;YaJ%mf*UPg zqSN$`i(@^Omg7NE>ld;^I5L2fZB?M^C2995bv!Fj#MXECh4-NhJ z)^jY0P*f{)>lEhK6(Xajv`l3h_0OOz9&2zFnlEr*X2PnGM69^_mkSABDGrK@MvBEr zyra;=DH=_RB3mOWP^Cb(Q&gBNgxd&Pfz~RP#IWs$M-VbX*(S&PQZmm(;)BMdiwya0 zBEdqbf+mpp3^>`nd_PMfUM{r;GS!XM;o6|W`GSzI=NIK3->qajzJ7WTrVA4$PN#F8 zA6OExG*^)eiFva{6}d2jq1g~P=s1(J ztVo_(u}H*lC>$(AiDDo)PJ~-5CQCRt!NSbh5Ej7$LT(m>QV|kLL!Nh75(2GF=Ww0pJRKujoVBKEklC@RWR2fD`L7^?X@Q(nv>dU zI*1VUcva{`i+Z1mCR1T$^{tjfG~UXI36_LEB$RoXtwhx*KJH}N1wtxNb6zWXl1HC%q^^k^l6U{6< z7K`0nG7+fxk}101&$ENos3kESjInuN$>;QFBSto7Q%W8CLJ40CkI@~t5$<6uT?c8T z=kZnEl8__vxK3wg)F=_F`w*22B3&41ghis2YsX^qW|+eQXskNSEvuG<1k%vb#j)Iv z7bm(PkiDdUcc6M8*oY)$P&$#D_*&U1+E*pdUz-wII*u3cvgYT|^0=9bGldk>ud2;x zm?4mCt){n;y3e1F*T&@9*DQ|mG~CEl;sJC#8DW_+$+x*WqWI7j+pdPEy(u%P;WWt5 zmGH0C77~#-IAl1EhO3$whF}4i^N5tM<#IJ&e#}!6AB$8c9VLqRmliP&!)Bux*Gj@o ze%6a()0&D@WAXlwo8v9M56uU7Qqo76FivjVXmJ!dVZ>(g<#?qf^itKHJkBHIj@;|i zlj*q<<;y89no`>#1z-BCB~fUW@KCi_jf_KJT+$gR$PE)6A8y~ zZuu`PiFrCGRD&g8B?Y9Qp~Y6T%yse_T_VbZSiR>DDwR|N9DyG~8?-5*C8QKS33Pqk zdbm=hGt|gGOA4iize!L^L(Sp&k}_stP~TYJ*s&y{t#%`ULbMOuQ1W-vB{isYnhC90 zi&pAzn677eHKm7eDlk}I^i`!rYSUmxEjQ)>3R2^jsBt7W%r*;t5zYnXp?=P<4Ex<0 zzA9TBx%ybZd-*!uN7!*Fme(WY5<6<>T%wjNP(h-HiCLeo8_X-7k6IGpa=t7F83~K| z$Gv8TfEygqgz$X4-Vb)7S`Y~ZGV^c@ynD8O$&^s5phLB4IH}DlWwiv#CUlDJN;W;l zQo}G^D##KIH-?pJiqu!Wv10}-E!Xo)C8ku-Ivsvi&cdBpGSIJf{Zy!+4SkJ5Hk8O< zqdGlRwk?j1nDrMD6JM#H9ry%}rV>MfQ?ewTAUYNBmgl81RUWWOtos1>sC^|Qg2v+gZX@Cutk~@TA~PHWpz5hb-X#q2E&7Ht(xgo zsZKPMW#i4M+A86ALn_ANTc5W$GBP+z9S~B5ZmK8Mv3S0W#oKdgK2PzC51OXPN}kHk z;}A61`lQ8CA;fSpfKCD+8~z0A6v3U$>a<->a2+lnHHu|IDKN7ZGAZdRi*`(iSc*ku zxi5v8bbILMz=b@X3b%{Jv8HC_a%ec_NPf_oNxT2P;E3yO0n5mB9|L9~^Tq%qwj}XR zsNCz)erQ5WH7M4efXpM@)^{z5e55$y1)aZE>Bf5D(in0nMq(qX8#WI!1 zu!d}X%WApE#h5X|PDk}c@QL7%A|?}b zqtWIfXcSIG{dBTdi=tYAmx3LN{L{++&zE99TOrpHzsM7Akbuc|GV;VsNWRL-7Es4gHbaOenX8K9-m& zCthZ?qkv2lA}h?%KAW6}bv2%)@)|02+3p}W&!)O*R_8Jk8>u13Z&SRkNjV z5%K3hsR?0N(=)yVKFOkdAU9xpRFy4@?V0~X&*Erc?O>xk$|CJj4jm>2lPLMyfhuDDdAB0jD%8Sv&SAXA=Y^Gu`;D&)i^@LG$iWS|+t zmtJUTHKVh&L?{>bk2$&>RU@HPCD>reUYiTUb*(#{H^(796Uqwh9n|7T)|L3Io`}b* zS$|Ju{L!IL$QS0da05(eRisp|M2jO1YFAF@8KR1IeU&y?O2Bjl4lz_S z5D>%tqy$B1A%gQwskrk-Fpxi+G1r>ST8tYulW-btC?c8(6^iMhTBV@zqzuzyEuW=R z#ZrALOIt~+<#-F?5I-Jb(E*2Lpc!1{!=sd*LxR3!GC5&Kvs%L6MN|l1@mUhkpa@S& z?66&vQpH%aIfFylVgt$JVpDEqB?RpYH7=)w8Oi&q{~Oo;PqGZ;zx?|D%~m_Eum7*F z|F5tAudn~Fum7*F|BapS>+ApP>;LQP|7)`T{}*dQUAeygzrOyz$BX?#1Q!bs=j;C$ zTZZUj{jXabF4q4~wm7b@{|_0kYw%xI@BaU7{cqg=ziH{+OMAl;pE=>%ea_C8cVgSW zw)Neu#^$>=@3ld%e_;K@nzs6dRsYJfmj8D7G2WMY{vS^f2>d+%oV)Q-d~azJzYSkn z+T7S&UA=wdt_+OfU?dcX^935S7w1~skg;-go;)_^k4Jd^-^pJ#ek$YePxYEN!h55GxMC@nUCAKE3$ST zpTd8`(2jeF@sq#fYC77>R-8L|DQf8WNVD%u9oJ7YQdopIY1$2sgZ>s1pxW9P)6-*8 zS4F*3_Z5+%FE}rk#u?>q9nA{E$1AyRsMp^$fBwE>C2zbGxuPSHjkVKtjI1*uE;2$& z9Ln{T%m}KA3AjBD42o$%Z%nGO!L%%5`6+U@l0uJ?kJoX{(C#Tk^XK>XB^=nFaNE2Z zf9}zj!iHW?yxe)Kr^`t$mLdars7A*57}h8SmEm|kE5>+KpZR4a5{?VK0)mHmXrb4^ zn^?!|xN2zE{yXF6lRo0OA5UKj8Ty{RtnbQc7<}CYpK1J!GE0?;k*cDyeZSQ0RqHck zUY-iMP%PE<^`<_dz0mjWOQFi~daf86Hq*w>R?xAY55E*N^bB3rbJ9V0?_4kdRwSRD|>8+OnS6Jrl8{X3)ftse1SVbPSvK2lXWk;PvFoxmaWQOEVLir#W z55SS|7%2~2TBg^~4!^(o^VyDde8?p~2#<>}WQ9i;wL=yvE&}DTB})w5iob3A#G;Nh zyz$c6D|*rJ?_!~a6^9TK%YvgW-5!n9NKuoaqLig!jLf!_W{=m?ajMtwU)hVtmLW0y zJM%t6$L#5S{=GjN!Cn6S7(=fs{M$t}l7(J}`1i3on*I$v(9kjY`K$F~#Fjqc-04d` zL*Fa>+eI~#g}(dz`&d0q|9C6?DY52E`IvOr5)Aa9{&(JacNXI%p#GPCoo13+ zI>yxR?nP^oacrFqbCU7sCmTwh-P`Bj2lB9knd2j_7+xdrxG0=)w_@hR;#j@R!1Jg- zGxRzWJW{VmU%JiE>x#hRqWr`{uR{XQu{xT8=V2c)biA!}wE^Bg6Ee~#fWY%`L*FX` zkBibP3w`$mo@4bi1JB7zhMuRc`no=#=ff{O%+T|Sz~iET$U@I60?)CUnt|sb=BVHD z?~Z%))=Q_auuLQHxTrDX(lX7!^T2Nyz4;*4v5pV9^iX@?aZ&MQq2eO&9NUeCZa4j& z@$=?iaIE2dFP$=a@ruAh5Yuce148E!)pW{7gbNL9NuH~G6T;E*3fJ3#8G+~ z>;IEWuU^`F#okRPo^#@s-S_T3YUcwxwe7ENKV|E0x8$vz&0pX6*Nqo%oLPV6x_|BC zYqizSfjWubUU|~;cbA{Jd>`+N!L9sfcus>y{51ZY^IUxBnimwsz_;|{*X&nx($!4H zzG%O^a{wwsBx)Ed+OO5LFP<`V*u}Rr$2wR>e)-u~+m$%CeetB(Kx#`pe#zpff%X-@ zn;nQdFWzcsgHT%&x7u7Ttg=4{o6~AHOZPi3-m=iZ!D92u{y0j5ajZ$@tC&2eGI%C1 zCr~bu%9L_5CdNTS1{^w5OS4M09@nx1>Ec6x22b3jwj697EHpUy;&rnvFu3?&z<3rg zjve><(dG1<{oqz;(2iryvs<2x_jw9tURmKq4rV6Pq5vkDf@ z3d3xRkTSzguOJ7aWUtvztML)6O=I;a3igsN-XCaSI1uMpgF_t1#kzZN@qU2Oa3Iz( z<30y+v0`Q}-WM<#4#YTSy2626ta^b1-DnIYaG;@MvZEcyzJcIoX-i(bkD(1{`oOU^ z3kPzsIvrfRVKJ1ngev>|K>B6Np2?acV4`gp$&)}uC=)w zISwp%-7Id&i+hU!cCNui{U}eS+4LP7t6OhFYYe-lyYoL2cq$Rs!#8{ zxUUb52pjf?60rmrwnH<*CbDWUEy^7&P~o9bAI%mz?Ls(^ z>!2-IkLrY08&ttTp0i=TeUCH3%1WRBZuZC3+1Va)rlpoST02XP3dxb0j-M69fmE6I z29a?kAvEzO$8@j~l?ux}y_~_hY=j4gcu%@ zyR=~@exG^VEMKl&xQYH}!_Dnr0L`Sqcc)B#(y7yK;+63LTllbM8iq&juZr+93LF-> zGGs=;8%`PXD?^b!ql|cBw#Wi~>M9i%!Uohz-P~1W9y~x)dEQ7^d{>U;Ao%++IhL=s zmwq5#fk)l5k_*86f}uDNKh*P@XZc8uh(jqhg5)Dmpq9z#*%D5NKstunK%;3T&FI0z znOOk2$DUVjr%Giygd$KV6~<6?corJXun@sP49oUuE|rT5v!VMR_3^E}XW;CatvF0C7$_nfZBjk#Vs&x;)N zyhD8H5bv<&oj)h*bdu7drB0gH#YgmFx`90Obu=4;g4H}dPiGRM79__+W!M}^(`=yo3RBS*UM0|=G?60;(WW3)P^!@2pm#3v|0lib(#rjpzqHt4)ZWYYs6GFQe?Rfb6BkcB1>6nz(eB4~U$lGs?jv@7u=Amv=kK(39=82Y z+wb3g_I7>y)~$cs`h%@!Zk0ji{_kwQck@>_@y!Qq{QbteHqLDnHf~=3`uaQ8?^w^R z-*4?}Yj0n>bFHv;^Xk`E-?4hfYHsy@D_>iA`^wW+P_ODmyf=9M#`7kR>VZAGOJ7=g z{oN~P&!_MH>-E3<7w>`a%I&M(tGt#IaoZVJkMM$Tzi4UYPmU6b+P`ZpgkBO{xqano zOb3LBFMeTZ<;6$x*eARTo);a-V;3@cUU(#red}_;^MWIJ>_R5b^N-}&|82wAB36jv)JW|AdJirwB_>m%Z!!boZcBIIm-TcQ#iX7Tw zA3bVh68ra#_l?X)j^wcmnXUQokv#sx-R(n1^4Nt;o(~3Bp0w(@id!G<)lc=WJZZ(L zh(R(xO09Up))rH(4;(3C*UJ=n|B)hx8s;AzDRQV`{^3#M61N-X!ZY7@B#&LlY|VR* z1 zok#N6g-o7z9LZyMwFS@HkL0lnnLKYh%0KN!ZPCn*@=v>nK{7q6Eit=)F52?equO#n z$ZX4-kL0mC^@8UuNAlQ(OrGC6lE>Zy3!XO}$zvBXdER&=k3D=Wc;0X%k6p;*dHs<* z_VBUbdEJpbb|I7JcaP+;hmQr%?;Ocv7czNXdnAuNd@Oi=`$!(Ukje8~NAlRi$AafI zNAlQ(OrBRC$zu;63!YaU$zvBXd4BUq9((v$@VxR!9=njq^BYI;M6VJLUU3x90U?v; z(vduN*Iu;e^r<~`6ZrTUuKtKP}%Qso*TUt z@1wk4&zC&!^!};$wHv>1d9{1nGVVP&s%ibygd0ejlIuhLX;Lt!eV0*(|LrJQekduE z#~RaY@3TDbG?5D3>S+*Js-wesMsFgcj%{+e^u~uTiMjND{#Y_SR15R7Sa^5!ZmHr^Sr^rrId0($(IUcsysoO6+V+0>NGFui)Gf1p#|ZQ*ia z6E9CNoQBg?AKYd0aU%*XR?tZc)^bd8GQorc z>V3|_^;N3Z5^J+5-fT>fTykU^GtZw|I3gurQ>E0&D+Y2wCucZw{RfCt~kWoL!IxTt#KZ=S8Mb9*u+V5 zu|p(BB}~OqM1hdGob7o_FJ9mVqF(2z-c*|x8u~1IAcRlNZkGCxCh3J?79|BP&rj3# zZeFn-*U407FvMXg8KD?x+$`8=y}Q=qT6q+bm9j7y5N!y9WqZlBkomX?N3dEhpOs3* zW=5${u)UX;US;BV1O?@cr{hjJ-=QXyz}S1)dy9z^HIh$BBL*%Yr97M)QdygNo|{bd z#EGgm`t>P2$8y75zD>++xcmY}K~rQ3XHc5RGGR(hK1lY$-aNiU^U`he}hCQi@>^(>;$nQ@9M<8_VB*l0a(Gu0E6B3cOzPzpuEr+| z9Xu(}DHT`H>GVJ|iVkrFhq%0jE6I7dJftQJozJEOgfH6KvGh#~S5&4WBFB?64%BfF zQ#0O<`#TE<;N2U{H zUMHmFs3Xmg1Ks={>v2U;Os3aHQ+RgJhdXfE{&vAE7?i1z?VMVKTS}vnNtP%Zt7^iy zX)UGZ2&IgTS`((CFtiPJA5%F_Z%w%jp5_RO64NytwYOpE{R$UO)N$lip>Mec5Y6dqz5Kyvw zCJxlv7?PSP8}5GA<5-nS=0Re2@LiHdkP&`hqLgxoOD=G1T`5XBIQ&t?D>;<1hbhkw zmzKOgIDAQP{eQ3b!2h8=U^<65Elic^oM#AOPV+Ky@a6n@>-_)smv)}L@sri3d%pjF zXm?z1*5w}fwe?H88x($S{ke+AO;R2w=L{uv!ZVa{?2aaQg)&aoqWgp#X#>kJPHV1eaKg@8iw zU?Dclm!!ES)vZ&Hln4&<0Yf{rFTF`{-<;#_b7RJ7L{JA)ri^2VPd&v;Bw_83T;=pt zDwb(b1uj$LQgL|-#id|iBvkQYr9EiqZC=B~blIOXsvl*;s4^iZI^GLJ!WB+UFm*qo z_0fSpI!=ueid+x!kx{5f)5*h6Up?z-mv#(S3_UmGaWi!H%oUAOZ(T|3_9(EBpt}-n z^q##tfDs+)pnl+>haQ_dw9gM6Vpb!5_F|;Np&~86S=e2>yt1LU>;Y zId#Z203)m%IHl?~thu=w0r*^n+1r7DTL&!3rHnui2a&n|jE<8DA(YP!LLu0nP8SDl zIH?GgRJvNOQuRu1TwvK|D>x3{v%M-j>50J4;s+@!lJ^uJ> ztCzNoBlpj{zvr}z(R1Q|6N#h5E@K>njs~uJw=Qwmd+(<`fNrnS9WZn^FIvL_Y(97J zdig!_zkT88YL{R3y$_79I|j{uk2*vV5E7Lk0cI^`2)gUF6#;@j#p?i(Sk!p?9~w2! zh67Zrt`fzwXlV!sbE8>#rb2LL)GPayOfpvWcR&1Ar*PYn``_CE^-BKTt zjF$k|6Nam;9>*wAf2B~DQ~E4Mvh(a2K3Z0VRJ#||bh#DkB)~4@Y#gkoV>3`?qCD*yJ?&-Z`LHGrswP;>Bgx z*|r!yn+#9d(;yn}U;`>%$da0N&u@E9)L2{}Ci66t5VMM0mU&oC;mE9&9j3z&LdHi0 zMo*A4tzXkoc{uX30^#?!8^R=sWlHg36l6`6s|l??p7Cu7Q))cihYy)*9&!!GXu5I! zf8)nXk6-%ulJ`5__j#Y~ZFo<6zUTRn=LMc8dmgp)qmAIsP21nte%IEsw;Efgw|{xN zy#0`^?`*wq>wfE`0S1x^gV`=A(9dzsT=Jz*0;`!c2(A(I2 z(dI90KKjJy#1l^J82p|mdrt!ekNsoTR^TRRoKm~sJ2wI@$jWMB8XETo^hAOr6{k>G z&p}CD;&qZ5+-%id=iq`IL#nkfj%dw#x9*!Eb$TYp$T6=ZxoKMmzh>)!WWKB4Y?Tss zBk^!cD>yvvV}qgY$HGB!CKvnapynH6kse#Z$I&P$5~FlQQo)L2Ip|s<;9A0ONkAdL zSgF;<39J_nb&#ygb&LMy5EY{g)`}>4jIEXYDpmEzH(%#k;&)w3{Ej6NVc{7hfC`sb zOjVk?KgDMdl!ygbMk_|g(+X9ZH~lQhLHWkKie0mNn zFv-dZsKqGdS!PmiRYw zC0^@V;qdi^IiuSXH9AQh1(L(^M=hv(;j{NmQ_m zMn=S8zlN`ucHRnR7}x(e=mBdqvO_-ANXhY#lxOe^HRVV`^yhp?FauXrNiO%fC_h%J zT&{jI>{Lx4M_!gw$!H)JPDDm?NQu)R|AgMcQc{}e_@qp@-r%c5l2ys#=D^}`Awj#ApezZv zfoDoO!g6J-ldkzQt)dLW)drK{`Yd<@H(@_I3HYFLqwmufiuK`|xc0cUt;_oZ)_%d}_clMY`GUfntLr^*y$7!M!1W%m^?(`IXH*<)E67Z%B{V9KC=6RYapVs| zkgq=$_-U({k0_Y}1fwJW&EIt`VeMSa#9T2aR3{S6kz-^Ks1+Lh0!7u@!^V)sbZXR# zvRHABfwTHI|E9&^LgJOKC4OVk4yxUj=zONyRz=M>YPaP?Un#^h$w(lH2Js$9y4H_| zWo9Pb^a)F1=AV`!w!!AGx}RXeL7u$nlxvBTt|e}@ zB!o#g)htbwK3ap3#B?0#6$;5%zd@JM{akV!X38@v01tb!!TqgS{>+6$-!%tmN${0$ zevoQ6xlBJ)%$CAkf>biYb}rC})Cq>~H8md^V)Fzp-OpOz%v?xV>l-%`R?Fv+U^&PR zQ96_sIwQU}gsag&IUZq0+19LE^9zwdogG!#xa_}i!7*nVxK@{;eunnvlNuFE#rlFo0x_)QR;~SlnF|SP4dq6{T0_mWb|H-f!PnIIxEgDU>6TV$_^Jdo zsMOHJU=;6FN|hN~Xcd&@#dxT*cCwM@64hjDT%R;xE<_1heWqglUNs-l(Ws6feYKe# zaoc}xji3vOKXWbdrwa*K!TOV8PKyn(JTheYveM<~G&P$hld}|~4jSdAj?F<<%6o-e zOITk%W~y@z>&wSX8nyLVDBdYHpgAXZg6(N1P;a239#pH8f{$1hsmxMHo-caNSX#M| zu=ax8a6HoD5H(ODr440AQnwJyLAkoWl8JTTijRo5@6aHi+v?rGr=T9#<#?MsODu5K#-YWM$)m)p8dS=Pe1g-%KPT0dN~C7Gr~?7 zfs_L6F5Vw5Bo?vVg~Z~`Nr`Az2qwCnsGLsCGc`;OwdbLUl~8FSqQTlRf>-k6ocC+)W)oHzm!Fucr3&Hz!;x z;G*W569wX~6_5gwo>yR9MEZhOW%y<(#*}lZa-z}Cu^FjI$MS7PC5K&-bD}`ZRG^NL zbtc3`Mo5W6xxSJaK~*sUx5t4&F)iqgNi{Z@mPIT-MckD2bD}`hwF1|82qLZ(xW+>e zHWer*xmbz}n#C z4?)0G0H!muU_S3}lv%1&j8qks1yuvPy=r}i%*#`7106##EqKXko=6gv7F1n(6jvq(+LG z3>Bp;4P#`sr8IlIo{oc~DE`wfDpVd{oAK#yew1qkTvTv$qQE0vE8wDTrV|AoVJhIF z#HgD}nNAeA&9ws8cnBWuT7hdk1P?P6aADckdI(OtR^S>B!9!guaE*uHl&OG=hhFO; zIO$q}Ydi$EnhNCUn7=oS1!rTp$dhxSJQ5>5NsfrVW`9%^6I!ZQ$wy1Q9vnY?jTN}X zRKUfx-D`V2##;Y_@Beq4`05F4_fWof3)(Xt+#CYUkr8%X4Wg zVU;O`AY-O5YgFyY<*r_oU5z7BWIE3fRkZ7?v?K8WOjqC#Lp1{dG2Bl|P=ppDINy|t zr{n1kGUDT0Mw1ZAa)qVL+a6Adb>J+N#a+?Z5X*r+g>zQHAALCjn z#5WqCA*FvIXsBZ}{=<&dS=8a|Kuu!Ca7rZ>(zH^l&>3+oU#pcgAFX3yKkm~5t)yCp zs*}OsLcpLwLQ6aEKYsgYVc?5aUoK&?KNqigyZv+^wC@U}`Cs$}KVW*|F21z$M~;~e zUX;ANF_j1fgB_a0$bC%4UBh3xa261qHHiMuG0{E?y{lqS5bXK|IKDQvr&)T+g~tGb zkU{W1#{^f{<$j7oHx|!%FZhgcxgBu4*D=S@)@V=odbuaBwh7p|@MuE`qoIG`Sc!!V z-rdBj?H`_tUpQknk)n3qbNoiqm8N%;9%bl`0PlRaTiq|a&HmKV zmfN`4ryE^(Bv1j=*IwFrmtz$U@h2DiQo{?60F-9z|9!`l`#j6V{tJ8IHb7;@{&za2 zy28g?>@9$q*FC`~JVi(|@tR_tQs$X+-J zs7xzSi6*zi^A8j$y@ba;(n6id`&r z&fR$7A=Y?OJ8wMxUjD+0_l>8Eq!Mn{K;ZrMU~4$s=ymjP?0;dH!{H*MhMS_g=N^6G zL5A)KcsD!m(}nJrBg26dJ#KOhxxt>h*M$dKw&q6Pg{`?rL*u4A>bd(}cmU7|IKLa6 zu5f-ASr**n6a#VpW@~hpcHZFFW3KJ!Ztb8;sii+&^46^L{M*00{EU_C%KfaH{@zob zfAV}_>wTBc^BbrA*Q3U%e&am5rE&?;m)dW!)xNz1Pa$Em)HE(-;5It z_W!g0+dTD9ZM7p5l@KH{pu)-iv^DMLQ*{)hOH2=GE3u5<-;0DI3M&n6e7tLk$5|58 zm`a8QJ}OeGHY&s5I3JZN`ALwThXQo6UoGg!SW663>{P$`J+3AGn`?=8TN1&p+J-88 zeV7VWef6+^jE`&5JQnNt#@#_JG07_(KZG6%v|DtX!L- zc&Jqwqv1q0(HT78Nv!$8vRP*SgnTbga}qi2RrQ$9_#agNVB+S-BqsElEeNqz}6C> zFsjB=(axOhgwh3hIFJjmY;O!vnK&H8{6XbjcUT;K=OgHT%pYuVv|8khp?XCE6tif^ zgII}GR7Dg(fpBp|G}0AeD&hUYFd9I%KIK}%I`>$0!C{?ytl~1*c!Q%Owh*Q}TCSSv zs+X$cYJYFlmoU`ANbL!O@V;%UT45Ru{1(!RT77 z7p;(UayX2pRGkSRiIE8PhklUwO>NC5aDNnyw&v32zgQeDB!1{x;s=(53y-q$1yo(M z{K>BSY7wc^#57#5HZYnmrz7N?YG#Isw7`wf$)thPH8nQWQgS|rs=3WY^l>4v@LCrV z3$IOqf&`sfw<|Vkia(%q%W$#pi_I{Q;e)HkWoo39<*~+;)ZDstF1YGK;!mu;av||a zO9B?+-58_d{5T+YyEq*kj0RPIz!&wUai%q`=G&o0$j3lreEmC?M5sR%Tj1kCuQe^@ z*r9(?%H-R~5EHYVX?j=y+xn@wzmygGagTM|rRqY$y6xgdV)4ca)q2=8%Je|qBObzI zNH9LbS|Eu=0i^K>F{8W|1woPuHQi_duAjvC&RLXf7t`i{<;gp1Fm%SLsJA*CU>e}L#kwwnw)EiZ%S?wpp91+Wx zxj|O(=S$v@kkJ?@WHNB6S)%ZNp8r2?B|y*u%e0FHg7sz- zp$>w;WegL_h~9`=dSSp>-GN^X6ue>a(T7JL7Z&5Yc1$IjYS|ql!y&;cZzpsvUNeK9 zxX%)FC(GsqDJn3rXg(UjPT|L1xv=0|Td*MN(qB9BuA_T+HZVe&C>#w$I@KQBD??Q+ zKsaq&2}?;|Q!b$8K%o<+JyNx5&)hw(!-d6NV+$86fHw}${|`*PeQM7;_vBW7zjFNw zxqR>P81@Vn}#ULg%*+Qp=*W(CQWD|nN?lrw(w8$PhWd!@`31D4ZVPp@T zJc51I1hBz=BOEI;xn#doHu87?C#qGS>V-Qbp5km>&Zgx=O=y@ghhq;tY6N@r1h9of zp%76+3S!paMkx}usW5?rlp-fryA>r83TA?BELO_(Fduv9q!H{@6Ts#Kq2c%k$uw+r zVW{s{vU$TUL9w<+%;{*pVX|n*B07uuNGkwa#@IJXc4GdGCPm6&p$R(8>t*6%)Wpsa_U|)TBT~ zhci`P3HYcy7NeaM){!aQ$`ONfLJZ^alAy4MmPfGvGy!aKnR0$4#-xx6=D%<*0>A$MDt7?%2#Vl!YX1F=c4 z%0TGX!(zUXXt0NtMzAlN0Jcm6!7nlsODTn9PeW)4Wo@6S#8P@K;Nj35??ZylK=<%8 z%^q4D!M=0?Sjb;#!$wHABN(L-McMY{K_Zzlu(*zz&3re9=JX!UFawYtK9c2m%m_g2q4^Q)izk59%2YgGhf-M` zI8+s5c|MC2`bw)+LacDzwxVjPZM2;T$cV=t+BJfG(e`YQec?y|Bx6=-(wEj0hYvAD zlN1#%S71phkd?zNr;38Xo-8{-_R!7|>Wml*q~kz2THQb?_o!O)p+Fi8UI4AfMOgg}jK@s?@pl z+Kj1#&IrtL0fuZvfJz-4jKBtNz_ubgr4IH-V11Who6?$62kjA+7NrNHG*wT0PBJx#2##pV4D-bx*!9w z2OA^U#ssh~I6dq^a|CNn0PBLo!yc@UVC&m6EZop?*n_nZY;6MAx*w?&gLcBAdH70;{!RTQR>LXZv0$3O99rmC$g4HH~ zb-~kN52_X9 zU(`?7|Ft6klu{mbL@C8ZC)&O>UUbewB#VZUH=8Pfp_?hCxafC7sS!7~Xh!8-bik3c z)&|KUHx9Z|N}&xyGM%2())PV;HypCFW^Fyv+!+S)s9N0?lESFbWBbAhzAXlOoG%tc4p{ka0t>T( zPz7Qh36Q0_gOR=zTD`b$n!B5zHe{h9(+E{ymm#J@S8=+0#11#~Fbi5k&I>=l*)aOfU?E zf}n;?g_)dOCBypmn?58S5@UvpGTo%#Ac;l^sR~e-p_MeAi8~^k2tMrL1}#YmZ`FyC zbi%5lh8-xh(;XUSb@f&Wuf9Y^DoJBWG!*0f_O{U>7 z`StHKaEvRrRILem!#TqVdFTj8c-X87gTsCPoUMkGVbO6%EqB{B0AO}&wc(I*q(?m{ z|2yicpun?-&p+4l%cEpkvd#2RZuiQ z)Mb(seWJgVs32K?zCoJ@n)$q1EQ$}m=Vh>hG{B;Tek5D5i%3agIy{%vd6qAQU=|Qv z6ZKA%ie|h%+Rk-4B?sP5HMmX^t@V>htLr~dw7R8;56)J-V!s&mc5uRpP9%vodUVk9 zuGCgnhCS~k)e(&MN2lhJK7ZYK&!? zCvCiIJH@am{`ICADC9$j3WJ+Ac7pEh;r)SkERp;rEP z<#j8O<$IQ|UJma5{_Yp<_AGsU>7u1`7QeQ5#Ui@!-B_&Cb4iHn-#c9dF!$&-`-c(9AQY@1B0?G(7b!0KWByUw!(ld?v9k z%2lH676N-R3S#A=J&SR|&8j4!4ngN>uYqT~9a2_!qz?ggRs{7g$8US@zkKk%pZae& z=~p*A_13rk?t-5nUoI~H@QmO83;feZf9EWI7391F!1c0^x7`{Z}OG}SwJ=S}q6PQUOBqIdHr9@vzasb!`Ghz&UJKKNC)VE65>;L1{FW!Cc>)xjp&gBO5JKkD1{&Zgdc5RVg1-aq? zunTU)82Eo4GkwZ$uRiy*ORp9_a>j?gf7e<6`+-|u!pwi~8E=2lp?95k3d*m79B=^G z1v_F4{G9Hci;uhT+k@_#uDbk|FQ>ortuyGK{L-BI*sp)Je=1IYbcJ69x!nM;3x32H zIQF)8eCa(e-v8{U-AOo?{qOa+zJB&!fAXkT9(u>?&%gM69|Y5-Muz;!Pn3F*}JB1x!_YbJ^5GjNc&Y^ zd%+WLxc2nZ-steFAa@!7c0p(u1OIXQ>d)SG#S`Z~d$Z>%`{1wd>d!tgbkY43o&Tqg ze(6bn;%50(kP{66yP!0Tfxq^)pM5hU-tze67vFUDZCAc!#|z$h7QLUi`^it}yh9_t z^35Odt031I0CquY7z3Yu|DShVn|}3McK5$|%W3B;Z#Ay|(HXxwbKi~7YoGs2?pd$? z7{3Z~m;qoHw1zS8%l;=;sxO@H>;BvCW{b?djIznyMhViZMw732~Fz%IxQW8jb6`lO#c_S4!qm)?I`=)b4R z(X;Lv)VptZQTUB3<<}m3Lg@wkD##HAfL+iV#=yVkqi-wkd&~o$eB()rL3;nfSHAN@ z^U7b$p7)#gK9_yzZ};EKuY%lP0N4e=VGR7DXFU4q@)w`_yzAapx##3p-f;fi-1iy_ zFS`7h|26%T_gqNte>%Ska(WGcC#N_Z27b>UzjfJe-xvRJ*ZUjvRvD+WF#pxcA)?U;gF~AJ@70l;{57RquH9 zGgtSH?KjXB#`e84zwj^To%;*xdk>`NY5DdGl6QXiO-rY}>6Ps#pLd&2T;4&fK7N)S z`9CHsY{@Yg!~HDyzxR}j&+nA-dk@4u^z!px@T!@>Gk3pvhkxo(cmDJ@%!i28$2}Bo zE0)0+t`+;~)1D;Z-(}zVYKNHnQN9%L+;qz0XX+fbcgI=Zd-4;A)w4IiZOStk!=++p zu^%R9h>HG^)v0r@x|_N7-!J&_je8z=^`Fo9**mOjuP0W|dMMmhOoK6;eC0_W$lY}s z_rR&QToYH#S3mLX_Qj{aIj+S%}X=IXT5j%xsSW=_upQ*{DE8N-+%5->z!Z* ze#{71YxqpeXnkJ&2RdwD7|-@JnBD-b@Pq^Gk2~ zO8A&*?q|IG zm$~NWQ$PR7E1y`q;iS8={Oai=U>B@|G4R}PZv57pKfvDiz2FyfH~#dyUw!3wlEf8v z{God8CAWz`{>=kbe)Y5wunXS782BwE=qa}}Ux!ye_N2EKetFk3F8k8|<`Xa1cVBwr zwJ-h2-amehUw!ll*ah=o4E#OihEva_-+SmYar%K*Tyybv-k<&Pvw!;K$1nchFQL2s z`6(ye$FH6`j&Tc>+gR4y6i`^nu-%YBMbs7}plHSlc+3RUq8k+pFL9mrQa-zW0l#|k2-pQtU<`bN|MW9%y6okv=Y9FS zFWkK6q7VK@XX@Nq^u6|Fm%r|&N5A`nZ{b%TH3D{V$~gvp{i#H_9*|AAg}*IQrs z(dT@O)=thn;fk9ZUFJ)blltv1^Q$L~fL$EpjDi34koU&_yvF~WH@>bJIPbs{9y|YY z`8N2YpL}fVuEhSQzs7nWzq)4x?BbAQ4E*Fj{nkAD-rL_u9J=Gj=da%Xz)SCc!;fBn z{pbGpQU6!=zvZ>H$MLHxBVZTXr(@usLl;`N{O*od44(92&sC3Edimp?tp9dTeBaXl z?urF&`*-YRL;b&{sW(lnT(|67`r@LoFt_UsJ3}+OcGRcdH2bx!U)=v@-#lO1dBGLp z<7T!vtMRWtHzlK3zlijEMI;Uh`A|2BkN#~5c*v2W4PPdT*%2MHNtSAsaK0N z8w0kfD#UtR&cc^dTDT>u-^OAPQKmNRJvwi!I=6Et*C&{ZRsS4L$^CMb)bs+8adB_`U*HL89*D`h$u z>W|jayh?NRq@C(#Y&haaLaLVJ8*o9hIi&$3fu}*>KX^vlF3M|{F~eQtM^cnY=Qu?< z(WElqqChb(b<=vO%f%>;GO^8~tQsE7^mKq;7aH$16C4|B#PSJQ4zR^Esl}UJaC^^+ zq{Vd6UK3?yyC|<+Y(CPWOhu1Vl;gBc;w6%(r*JWv60-RY!A3WYaw%1TiiDO{MB4)g z#Uvjy3n~^Q!fniJs9{e-jYJ7XPjKSeD688=d3{E(M^am+^y3uen9>#M5K6O3SIU>d zBy7|Y9X46a7s4nC6Z8Bm+y8S@imP0jx$PFUxE*#Huv2ql0 zvLs_+yf9D`Es%4#&}blQqg>rC%J90AzeiGACXd(bhl@7Fflcu^*U85l4vqA2D&N>7 z%He8+1Y4I%saKSN)S9Wuny&>TN;IQ6j+XXCa%eD+kA+HEnqD(5%iBd6dL&2rc#X>u z9}uZibD~!@b9^dE0If~N7_UbjI%0q$4WS@}`e6+6RL~%J4H@`yK|LC1hYM6s4zVqz z1tFqWbK+~;vh;jm=d-U69yh(kO73sAkQ*i;UqGt5h&3qCB{c3vAPn(aO2W7_7?zKuN4Q}T-uv`ZsOj&O%831EAnM`D>B&9md zTF7!r@Y;qfZWrbC>82k+gEE{sUc*H~3Ug4hU18%Og}1N6;VG0e#LWg}axm~F0z|J3 z7ovVIo=Cz;q!bWnrwj8Cnx|<#>eExPHvEvqa%Hhb?j=j4gQkd3A{94zHK+!ITe-LXReaKN zd+l(TF2=GtjhhY3i8f>1O`^WVWB8maFI?~*X(Pzm*?RrYy>{Wh?RR?{C6EIkj2Tq4 z9g{VzmEW`}gsv=a=w7}+z>E0R0o!Vmp}Q+=MJ5R*NzQ7 zKRD;|MF~|1Pw4#G9yWYy-OBm(<@T-}R7StYF~$N!zw(0N&kTG8Ljk;YB3u7cu6^^^ z_PfoU00Yl+jt)ExSqcfER#?yHB8pKfb28iuRas~Nm&$Opo2UytM~$@7goisixWLC~ zi5tJ$E9NRywO^95Rwrg<@hrAk7XUr>*++XU4CmEsPi=CToM(x=Pav7RClk-*+O*Le zM18433aPL`z1%Kt^^>ga5y$%J)|Z>i-7VT345C{)8eJD`vo+COyIx%PBf}fM+dfIl zu;qd17LP{P1>I?@=&o&u$BORya+A5cMRXw$-NMo6y5K;qiS9AF*T$~5eJYW|(aj%? zt_yb07SZ*t?O(@;?uK%cxx0CEy&(`?{%CYvP=3}#cf+`Mtnao@cyTzo+|lT|;GAp~ z-L-w_SkYZyZZdbbh;Goil07&-3D*WxkICOW5)-Zf=${dZZ3--dA=&oIpu8;19 z@3z0laX7lv(dfE3@ZT!BYkT^!8tM9SlexP^bkX7a|Kq1#GqvZEl|QUF%l9q!cK>#F zd+FCp=Hky6b+8NYiiO9|-#E|A@7VSJUGUC--RYTo_uP|b-#+_<9d7{&fNz*NYx=d* zX99rZ`?>Tr({n(we%=jJ+YR}8`(??cubCQq;9}L`hD^Qv)P3nSmySJfLD6=@x}L;? zOU53!;Bvd6XHVk6#bXa#kiOmUxF_-8d1DV;u*2ODz9;eEqOk`q7MgCD;gfjaj6HC% zMs&mU-hQEV={19~2QK*IZYbfCc+ekv;DTuGhC4oq2lm(l7mRc_B=bo;=#4#aL0xyl zMxVrk?qLrm=eWC}txw`XXY7HC7ZW%9_U%0&ud(L5UNARqB)9K_H?9@C*?u9FNDbmN zNP0^XCCXa27e-#wK8&})coX^1?U(k?xu!MerHB4)SXv#~wD)bvXWt&)xTZNrkKhj{ z^V<^ZXl+~3-6yH7v97kQupUy|rfm30YBM+1wkg)p+O~S9o20h-y4tqFI$GOST>9;6 zyY!mcxZ}8Zq1)$z8^8Ta(WTcIV-H-Q?sM@vH;D)O*aH`+`&_W?C-FcVd*A|fp9@<4 zBp#?^4_u(`bHU%A!~cT1mN`sbzAQgrF` z#d{V%ws`fTv=~@iT=?$7`xaiXzyoE#KhA%7{w)CZ#OFi=PDJ2D1WrWYLw$>HekrCoQ~V>At1=CM`Sz zRy@DkKSwW2;`7qIOZOfI&UZDT)kt%l7^$V|`wD7Kl*Dbtm(!x6z zUc2zxNej;{T(@xDq=jb}Ru@($Excpl+J$Q;Ej$AfOugoCn70kx!Zi!m906{!YKMyP zYMZNYQbZzC>zdK7rL&jLp0x1JrL&gKnzZoT(qoq%J89wBrGHrZhe-?XSbEITVxy%%^8QJ!#>YnNQ7p>d0@hU7Tk=IrGUQ zz-o&ZaydENPZcn58Mqi--C3-6r!(cF(FEj%~(!?_<$T6lKu?zy`sExZG8 z$nTo8@XXwQ&i&_+$ZZ#mxgX5^;6Y$QmMA-m<2c)C$t|J2E^_k=zgqa!q=k1a{Bq%! zlNR2&@Qa0COj>wu;pYoKpS1Ao!p|0dHfiA<3->JCGil+Oh5uUkuTlR0nO9Ejw&(BN zxd8rg;&UPbf4>NTtjCW&lJ(fd-stxGo$JI6{2=S+rs4*njk#CPakKNfx%hyKt=kd6 zx}@owXFcX>K&G0&3!S8yjZEmM56tXQFAfywFZ!LP#ad~A4i1L(OBJS z%R;@}XiIWiGZjz@++GTRZn0(%fABKkMsmdvV$%WaK+#FDin?D#D@edpXeT1#IW};B z#nP(NMXNC~%Tt_sAmari#?ar9R>YB<^V`&TBz^sGM%`@meemfzn!xKc?!!ldexATg9>=>!2W8oA19+lGZ@C;ZjYCB%&U>%Qj|XD;$g6c z!aRy84}boPKnHx$-0I20+2-Y^Paoe?<#p@4Ek&&S8)uup{txJaCBFGO(rpV^*M&9_ zngKtJ>yEi*INFL}-*_csQc{l>a`7Z#Mv%CiuUKIxqNkw}-DUz(RT43i&LJ6$FnGrR zX%IH{xQ+S$M~@CV;_&?cDZB2N+VjOduiK;V3GUgs^7WNBuQYevv2y;(?&WVUzhk+# z99=$n_YZczclYylr{*uo-uJW6RE4DyDfbW_eqX3HSTp1N4W>=t+lq1r~S)2w)7D_?MJ zaffS*&yOt{W~g3E<0ugtz-=s+FJX;VQ`CYgRFT?MlV|G|Rq^XNAWLLIcuQ^dkZyzTr?`{9H?}ZBXt7aJ`f4!6`|XHS zi;zK9Ob7$TjytT35P94vbh(r*PF?5PV%4?9wPOoCWg->0n-Svaj#z|(xgZCH$xb|h z)EG`F3dJ~4Dh(jIX`g(!CAzQ>UDr_=TWD%8A8^7sLJ73-nor|v0vf{vIM=Vkrf$+_0+t%Zm6a`saBYGP_h8K5! zz_rEu#}=}`5e;;}rW1)rv`SI+F>zdQLTaa65-lGe;0;(!MkJtKIrCE27B3lFh-Gii zNfL%r;G90xEjQ|cU>ou96hmO;g2#x)+oEO!$v(MKcWqH~ZDEWpDn?i_P^3|ghmvZ! z=<~874mogw#4toCI#j3)<)}c_564dZ#bFBuiI9Gz+N$AVH7>*?Pf5ura*3A`jeI#WFhzKfl)z%R7VaTF+uIj)D=pECtyE%kaqe&(E-bz__R59D zSH~7~rw|e{ex>3gDzPSRBVk?eg8?|z>jlbCt4(Ujem96)dUD~Ht}PD#f^<<78&o3D zjijU`uNww!gu?|CWfNYYsv)M3Le-=ik`Fa@z434zlUuyuum#-K86u=*^cd=)?Xck` zePI*xs5uRZfow|ca>RmYDUc!eJi)caKGzoKjx92DAgw3tE=|V$5pSzlZ|iKn*obGT zuvawLR5$G{WE`elR~G&_eE&aV-kRET!=B2XQvvh8vvTh8SC=nehJXU#D|SC~>E}zY zT}mz9zxb}j;^O?mhZd>}kDmV|FgWo!5rGpCI1zyp5jYWn6A?HOfypC~oxOnGLe2*c zZpdyVnI(R}qzIhqaA}KC3i)+sOH+3o6L4)#gJD{wy$#1K36W-o>QYgG5ZXp^J>ch$ zH{k8Z1YDaXVdSAqSnY7kY$qBuJ3=W&e8VGu?wEiZl9MKh3dff+)oj)lO1XAAC9Qwt z&mI$SL&{7W8&n#ZxMF3pwoz9|Y(oq`b4plDw zHT!IO3+;f10<(JNG-Vi1Ap?E zfEzOIh4D%%R!;%x2E0~^)0L9E?un;vJKlhwI40nFhST0o)XEZ8IzwdTY$uk_+2(o= zZ*d2>{UhIcOu!9eO@X9xM%5DOLQYqT?F_S_pM3n7fE&h|?E!B-Cg6sl!RERRsi#!HVZDY;sEL$VH`29!eEl#uH+=u6rf#2JX^zhOzq9<# z-9G@Q`ybq+FTG&qm@4{2(ADI8# z{A=dr=}*r4W^SIJ+4bdJ|GeDWRonIST?;$!-1%0(7dT_5wG-R9XYRXm?_T-(++Yr0 zj?O)L_U_pmXD?emd6t@e?9?A-N;~dZdGn4B?|9)3cE>q0zXr}+xH~odw3#RE*}3P7 z)A#Oq-DFFP(cXui?4(kRWEZ?ioHDE;BMDL3E2ZiVPD(^HXgS_&iC5BER9XDP*dk1L zGhGykbPENo7WO9FGL<7DVz#5OcDytwmxW*?Q?FI+*g|Ybk| zmy1&hKc&T=^SG9HNPD$BSujvRFG1`QtBxhqOB-juThzVq0E0}REL&8 z6X6zt*s@>-k}4wT{Y*-eGvRjG8)ECdKuxI?3uvvX`IgQedzJGB17WG$ksF+xv?~eK zhsJ$oRy4_Ew3_!Ot8r7xX_asOnGZjXuu>Yf{l9OazCU;arjPNT>s-l=l32ho8JEWfh7{<_A%BU^}5q zhF}z*(SqtomT}S&QxghFLbkEuu0M{SBiTyILdYygjNA?8bQx_oa#dM0V>oX6b*bAz ztPYq0_6bZw5d?kNLV^OV0=>B>zCu@ZYIJq%}QM7v&o&8kL!pB z@HP?XTBHdUH94q8iW#3L8O!+M@q7+bu%=_NL^v4iCwGo_6ZM#wXtb<4q6wiS)2b2O z9v#F3&2Tl;kwhwp4RVwM=`pws&AwnJ)G#}=5K z7fZoxxZsVNWta%^Q^CBMz?*T^n>89hHWUkSQ=b^u zp`|h&)$56%NEBBGYNi>(+6--5<({J`WJ64KB$F=s5!yHR;;}^)Nvm{KOm!^*QOQ!b zz=a^kC<``J53;-`MA5NeODGZ;w{v_LrAf__x1jp!b{TTISrk>JC$!9v@k16($-jW+pX_i9?8IfNP#Qj#Ox6=bOhCrAQ=@&0qy0pC$`1V9%+Y}JgWNCvWBq{p36ql(Db*)7U@AE=aj@y zC%`jpe_Kr_JA}v1mTbJ4EaQ=EE7{~cOeUky)5!S26IBk)is&HDdTIfhHZopWGoc1+ z@V;WDR_rFbup~9)rUqR&t^&{0`emkH@Jlh7D8>z~+C=?^fKZ)W!?S z%;705nsiiu#grT@AflvGi>rlXEg0z~nVx}DNwo&C71OAKcD(btu~X4-FK88Va+ikv zSkCs9R2J)4A-L6k1==s*usOFR4`1-3~Hq$wBFJ~i8>vuXaYrK#W0c1_n=sgfipFG`Z*&D z6&%!eWDQR7DanrwM3f5Au^#w^0r|QSFJ;46%Tcf#kO8KrPa0eFphUTw4rGgJy%4Hc z`83!Phy{&0>}1Hkm)3aE-!cO6o@VSAkDzKdFvt!VM#_}&ilfDoR@=fkNt1GcNUUx~ z9iNkJDJ(}fx#=H`>*zF14iY#-3J8M&N&@<+9PpY*Bi(I!?IPLDMALYWRSE^)+_Mi` z;1!%jA{ig2sS(vf=8+0D$fYeK6v}{!0C2^xa%@pS!M!}R5Kz%@*F%;ll_*#~oj}0I86?52y;=wI zXhfvO1ZSqk7FlmUPcud^pUcx~yln=hLdKsi8UCu3m+Dd#4l+HF+83>ZLA#UD~ot^l{yHchqU zR4qHm#}>mQ3q8%(im{YlZbA7}%^$HUro&_yaCuP4ByI59&_&tSSR<&?OVfwzpyK&D zl*;NkxLXxtc|MC2`bw)+LacDzwxVjPZM2<;n)EIF`}jE=n>YA!hDZ#AW}Rro8lHZ( zOYvTC!mavZAwL~!wefH+?N|#R8e2@BEOz0h@pE{6STiO!$rzmSBTOaGP(4%+OOU2P z3T-H+n}WlX^G25u7hgYqjwX@MHi{uX73#!emIqRB)+zRwM4>2x1xByz{7--1zWIVc(@P<;CWxKoELlo$MTJCiBXn3#a0|QUKv_ zxKZgkF_|I#M5Gsq10C>DEAfe8v_TilRsjpjLSBnXqM5NGmewpdXtrA`dBSW$^LHR+ z<{4uP$PN^tfZ)w=4XWO*2~nn{Fzs-W%M*?m<7?F%ql;~ZvX!~-AGXM)(oJs$qrlRz zMhx1B#)N_9P&O#`Jr0yJ2Mm|vno2zvTfBB;p~s?e1mmm{9}*N}P$;%=pPIol1sV(I z;bze6l6mRU=(*prVJ(r^*q`6nu@P z@jI7JIAJsq8Pu$v4c>$5WR8tyNw3k7)Nao4lcthO6@_?N?bAyi96v|h+cwjoN~*4g z2Z}Ed&!sbsYREx6jdl+&vV2Su>bC-VQT3C4;1e`|uV>qP#) zHPJYc|8Gq&9$x;xr72}<<yQssVd1TeagXXf-ORn)3hoGrB-mYCwggcJg3+sHmU;|)sX-j zwK&+$XToX`p=zR+9M&};^P{=|HmaAHp${dz6pgbqPU$R^suy*#1rGE_WlJU~i=e8M z;0Glp+6zdm77PSnJ!G1Gyl9rQWG5Um*m!2p3zie{e4v*B*VlN~Zh%m$4~DwERjAkV zSQ2qE$$?YU6!wS;wSJsJjjJ`r45u=+P@T_r1es42m{@X?P)Fnz&a@>|)Nw1^N*3yf zkEAIyjG_LN9q1rPMuY=W&d(0K$}rT*gQ0G173%dgGnUSUnRqnI(siB~BiA4;V3%|acilu}A> zU^4vv|-Fyp4FZ z)3DoFMS?sfW^`=J%@l}1`i9i&Uxhkk97$u!GJ3;P|s@y)H0d?BnoTP zs)YwbZEO*0@A~Tud@`CX7L%FWBWVTVWR6#>PBc0MQxYmjCnG0}e$hY1i9#Wq01c^UXWcbHAN?{ak$ZSF_j7Vmt2H@#-DVnEBDnD`o=IKbU^W zG&1!a0DsI6d)@5p3@z{e{$4RxsjB^wl(jlBD~o5b?z+nq`x>$o5<;!8p3g-TqgLi* zxEHFj&;Tx#VUTg5F7&_!55OhyaO`zEMzG(T0JckUq8t}-%mcS-xo}k}`14A&4rsf0 ziGl$I)MwH$ClG-NjJgj>g9HkUA=e&`>hFJqg*B0ZXvKIqaao;+OrrZ+^kCA z$^k*=X|I82yB$(ic%%=ptIr$3-nrMpz6EfD$6cR<%IPD+V#C$rd^_ybkDHMK%GL&b_8LjDnx6 zKZ5<*1h9iz&X-oA#k_$_UNVvKl?Bnl)nXFnQBKfmm3+^W^6F`UZm_HN2==QJzy|w` zaIDPalKoQI$m0Q=s8)Td7w(Wan5T3(o0bzbp<%`x5QN?c_A3*>77~R*L=7p3S%Vv3 z@6o2h1QJq;oLuczlt?I;34%;GrCbjKKUsGK`{fB>bAr%t{DWi~wz@FX_bb`FVV9s- z+au<5G~X~;G-MH-NXZX+S!V?MrM<QpOWd2dsOI00-U83Jr>L@jx_wA2MzjjF}=AnWI& zJe?^-Iidw9*jlH;c$*fx+8)7vVFFk|R=K=4U(E4dE+KbYm>8D&m0~ksD+94fu*yK_ z*TZ7IkpL~BHG;ik0@yN*=LTdZmQo7Io`%p8%Gy3t0X)}Qz{8HbXYS9%Vz|9KeeO4XiPOy?p}MM9(aT zQI#uBEUu$wGvCdjIlYH7%z#XFBY-E0838bYnIqWGO#rKvCnoB$tMw7=XZL2E zMyo;P`f@DR(ue`i@ar2`7+mTYbXA)4r8ULjLrl>mMa9b%Sdt24<#5ZXqR~KvEIUDV zwKjtN%mlFBn&n9};|iG%r(48Y9?GPXOBu z#u{kf!38E&_T-9aPzhIaVXr3VVc7IZkp_lV>nL5zHvp_Yg8kG4u)%mK)~p2$yw{F3 z!dN;JkF_IG0gu#?lN*;Qo( z`-ur)g#gBR(j}zTfdibJ>e}t1j9~G0I$h&%m4(2~$!N^eR8n3rCR9hTw@v_C!t%lZ z)S27c3XcWa`5z3X=Hu#B-U$pst;R>BkPY%V!d|rb!44dtpIC!_eUqOMgtJ! z^ZJmG7ePu@krLNETWVDpfqldcSkQ~WMvZN)*IrDmUN{2#cb8yLDyn7HOW&nd%OkLV za|7o0!XcK@&GqjRsnzF>z&`8-4E4gn8d0Sx>te>PUI4)4-5=Us5NBVK59_sZkfjiq zZYWkH&qcaX-dFWxdqT94@Y}TJB<+&ZDuOxnIV0GcCVa+=pbo3V_MG9(LcVvHFI7hAAG#_KlU zVFF4&Yf|Zcj9o2_U_UScY&K#9q9UBeSTR@&P<#fl)Rr?K1GSVrfZ7)71Mjy)jP}(q zcC|Qyeg6co;VM2r(rJj#>T-re8LO*HP@`O7xIxUx_UlP+AX@ZP`kp#iVircQH%EZtRLda0-fDgXd&2~5V6s;9V+$FbV~Ft}P{0>Dq5!g1X~)IjF>+(A#K}a$VC#fajtcN?rH!;< zJ|Bkbh>FR!+DMxUkRUuGqV@f`NGfRz#B?swZ;+;=g)*&X$pGq3i&+isAkiqez_F}5`A(Gq{oPcLU}~eD2-PRglr-?P@39-dS-vA8j5E#KGBRO z^C53V%M8dgiXe{NF_j`eKnVnoQdP+x%y3b;QWsi?ifUndqfE(I80d^6!ay#5?Lc*h)o>% zy9fh?U}#G{NU-?_cAf<(S}EdP6*(ZrdSF$N&2ox``74~9=%&g5rnP3ak-P^onxp>QB! z6ae*1u7)rgsU9#}16md~>v5Y10}UU2D3RA$J$qpIoEw{@xux70N zy?RpD0;6pA8UEwrVF((jB@S)Nl3W$KwV?_#1p35->t7(r{IucJ{4`Uj z%iv3N`0e38J>rT$;FlS>qG z!h!f8A+!Rt!*z|klZpuk5?s1aXu*VRdecIa&I!iDp5{r%u4h}3R6uK{p+s(wD#>OL zj@JwX@#X^l68OsjF@XoY!Dh_MD-zOAVs?jjye8Q-V?iLQQ4qrQ0}({efOduNxFO!hu zCGV4^y4O|Zx@F(4rYD)MKdNt+biUvD&i9@3Ejr8gkFNjP_Lc4A_B~sF333Cz3seVq zVDoR+*Ec`B`7LX|wOQQsZ2aTKFR%O88+L}k|Ftg_p6&g!5_nbu&r0A~34DblFiwL5 z>I(4+W2W>WuG1yF?TdrVKHZp17$q|5OJkbX$ayI`kZq8=PMIi>D6Lpdlzsj(1qaK4BoXpffCm|{VPV$>55K}c6{ z5buMMeuY9h%hvrvg=&qwv{>kL>Se2g)qFc2bTFbFE|AQa1+C&wx0zNjJJb@bK31wU z2kA~TQ0*52vG@>tPB(X8m!egRhN`hxcYt=w394$Dfh;k|w(2mZbJ1?D2b^eSy3DGP zod=zsgLJm-h*0v7p;VWP8YM28=*QU%n+(-ktx^fr67@cs&cN};s#DM}h**_YhM=vu zP|0^SUrhEWQ?=N+yr-kbJfbw1~7mQxwD$$IMPL_)8~ z7UaV~W;}1C*Dun&extu^ITRq(e#9!VmcUj^g+2swGNvi4?gItP{T-4)P$n7+_Iba!>ZBiIT5;3U3YBC^ zBhqcLUWf&Sd^(L%5sZ^Zm0C_4bfP+KmLOyEcO8soLkz%`Cisv?XR19uo@+&+c8g+} z7F#5EDsClknQlaz9ONcNWAtILk}*6f+$spUu5VPV3?evFh%_>KOK0l?GuHLS;BbB* zFTT*h010N>8Mvzwg#m{qGKHaR>49=gWCxW<#4CqE>}9ZCuAoI@@v1}7Fjy1vv$O#+ z9p?>}Oi1h~q}c+{lgdl`%E{Ak_ojs{yT}Hq%X|)`%&MvQMsb8Z-zig(ZdhDaQ*zM&uQlahwl{!fo=hugsnAsWRi>c|EAG@SMdYd)Bal`<%b|h( zAW6n+4K5{tkb!i%gen0{wnU>Dk|Ik(2czf};ARa0sU>T)1P4IAF_NO9Nf?UwBOG59 zJ9Q*QWP)jp*@!vAQLwtv9Ieu7mB=*FVyj|QK(uKMczsa=G*mK^o{@)=b0v~79f+(L zOUBr4v7SNHelp9l9=RBTBI$m&+d-s)AJe)W60V38w)_c)B14F3tzu@Fbb?O~vK_6| z&aplsNd#ERlNU@R-|^+Ude+ll{pj2jA8{yBO)`h}vmJ}9fFn`>X?L|q2qb{DB3NdW z!suvgkdOH}B`q)fnnRK7wNwG=wY-^FB2i%nMZRsu$yi(vLSoo3JN<4sF!J?-u)Oqf zhk`HGv}h_l=v6p&)JKAJ3&hlwOL>2eR61ca!nKkq;PgJ!u{GSG0Di?jlrLJN2GlmQ zoSA6iilXU}=%D2*Ruf28|A_;B}!~Ng9!wT5cz`e8L6pcTC9%XEu!P275=L%~qN zQWTR*h&Zs~6|$M+OeN9=-ik~;EpaRg@)?iBV3|rV8;>~@0gyGXEP$-HD(n}OJWf}v zQ8XSQN~M65P-AkN@1j^ORtX84PP$UYPvlvxQ>uI8MM6kLz2DA`~UmTnp>>l7^noujlRW|MxAl7WUt?_oclXdoSGmvEBU6mv+8s=M|vtAH4O; zTlvk;Z`L+fHwGJf>)*TXUHj3sxTzcPx-AQ*i8f{-HYgMp8n$BUhe2bHdoHZP#tbHoYE%!}MnBujbp+Usl#pdxbw z4V>>GewK<%My$Ci@({qcRb-AZfSK{9smNLsvz70N+-TIU){b#y_nh$~6Mu*;96oP=uNBCa0_y;Mh}kj@X2maZXHG z+#W(OW#@=jm>KG}Q}$Fmv~&%#c>&#?BgkQ9UXG?LYGZ&YJ4eLB`R?s!P1(tBi|&*~ z0KPqC=ZImL8UOT@)vv)W-WF+UMGgUuts>Rg!ysnHIiVu2 z22i$&%n=$fGt})>WO7$v=Vp|yd2C)lx95nbn3)$xMatJ+wT6yW1U?nuaK4ZJS(7i&xJ*z(4!K z(=CA~mT$adT@a*iesJ@acHg=7z2VX^$7cd~)R-1HXzE3y1N_BnO%*uZpa@Q!_WOGM zrCSV~rF_6o@eP;Es)XQP(9WgasFNT;eb+YK;B%BBNAh_*R`MNiI_%fkWQS{@l|wO^ zYe>SSM<2F5Ajj@KH=Uba{}sVJ;VtG^rjs6*=f(iq1n0XvpEPVX)uhX_18#y6Ikx<4 z6K}r(as;(BY3RCmT{^>>5caE%Uskc((Le*uuBO@ESw{|32>ACo4pe8kr%hvgJWh@e z{lwfjYjWD$IpH04*E9oomQ@YYdW~)PZapgkXSW{k*0XGfvzyDA7e&u>&EgXQa0xps zAgx|9?4^CJNFW-i4|1l$_C`#Oj?j69_J>j2xO3O6S{ly{`KF&^2?~;t!2zi4=!dg( z&M&3BECS~>(d+M#2`Y~ZwN6}Ynvoa@=MsFdnU-*nheIOxKCEaybTsJtI+;W@pa#I4 zAG`N8?FbB|4Epor{?iF1 zau!#6SSRBKzCubgh6IzytQoei;U(DSW6Y7^M8uq>KqtaDkNG;oppA2M$A+1%6K9Ro zt$9DcPT)9nrcMN6;e$*z6!$jByisp(#-IYnyne!pvXs_^m6X*R1YjSM%*F56U9n3V zz%Ri}3-Oj{vhc9R^Zp=FkCy~OPNn;CgA7R~9v%duQLmqm)Ck4jXyRpkFhts!sD#F- zP$rORw~RKMEtfN>)X4-X5WF;lPS{<4^wGCRZhQ@Jj(+If)R^2b`mmXRT+`pzm~bLcORR2{AQJp+nT5R{8+V52|upk5_{DP-l%WO6K$+ z69YBth!SjhDTw3lToXqDDD0O>U)@UN&?wqFgpC#x3WIOua^axcW4uy|W&BtbBZK~G zDVB^6h`!FHERJ2iZCiGE z9-n|i|kn2<##neLEqVf*Jsh@V_RN3*3TNap?{Zfzt^60Q}rGDSE=uFoE)d? zyJf!v4oaG!8%-ir4fZ7S z@W6nY1f-e7h>PJ_j0n(?C}OmMJ6;A<+)VlvIFsl^8@UXANF4gD?qPrdjx5Ru?>6YF zk<@40WZGS~{r~sBYazbyk%fi7Tl%x5pICbPlDGlyME8Dk=kD!4+5VC3H*Y5ve{S*J zyY%);xBh(VC%4|Q^MU=fozHG%x9;Ek((Y$Ae_?ND^F5mnZys!XdE;Xn*Ei(7*80D! ze|+D!{+;W!bz*&W@%0NI*?r!AeeuDy-&p&e-S@8IzHEkC}@FTZ4l@0*uLPK6Fw+ZLJwAnq6<;sW_ceOL z?jN~akp&-AA$y?sb)}B?2Yx;`REhyiOJ&2JA`AR@XsZ~Od`N!xcO0%vjrLfcyuVS7 zd76DP=#!)l=2KI?V6W+wve{aq-{R3hw1Vw^*ri~+veqPfa#Kr#gS^it#MBHZH=eEb zJ$NvpvJ^0XF*sE1Z#kYvNw7~A3Mo9^OcpXJCFF?)gROxO=e)FPDrRz&j|U4>HL7OJ zE!F8qMQEoIViP{79maET!N9#~;vkL@i6#?HwP_VPVEtwU@{P@Ff9#Ebj@LnsOFGkPiP*P;~Z$MS0hheAo@Gx2T=4VzGcFd8MlKro4Vwp?Rt z1I)w4+ZDQ_GrdsIxAIjEg_Ma5=wz~rpm*ZMu8gGmPpxp9$b0cGQ)xjWwSyK0;vPxbG`wQU|V+@Tbo{O2;7+>@7g+_)@e6*ZY z;v0?=W6Fzht(e&~OqCv?qY&iFc@mv)u9|Ae@ocUTFtZ>zS%eG$={iMMQ)qe6%z=+F zrQCpz)T@xihuAz*<(lEbAXtroGNJSc;=|3dZ^0?LnnKehZs4tzFhtf!Fcye!$mMj4 zj4AvG1tCj4Z{6%FScVdo94E#UL<_*5uExjut)iJFlECw=I5MIm5usHqQPAXJ+4TBr zL&98suG4dmug;e!Rjp%DwU2n9GAeg1EvvwK%r7IN8gBNBD4CCB$dxaQDUu^gH!7)C zWe}E=F(z9ShY8i2EuemhucaV=GX)n~`9wTRi%TDHDEel<)NQK+sOQh=EY?j?tkkSz z8Cj*8CSKG<0RshRtx%`o7En+2Q&cq-<(rjQUsPfp*%zwt`)t)$!>FRx z0tZ-+y6kv2CyfePLHijagU6TwpJB;1?cu^$Q!`nOOZx(&a4Fd=(9N{5@=K0%)o3Qf z)QC{E<4+iADxNCV6OfgSwvtV%Q;GUX#59y#w*y5M9UV+6T$r=eG9Bwj8)~^^#0gf< zjY?D@Bx(6@Fgz40xn2T6yS|NYb}(e6T!>WYNHxoIwXmvJ6}^i`jR6@8Cjx4MDtG%J zzhj=XgvEC{6eURnrHBoN2foS$zAgls-EKZT015;32eDq8jQIvqO-P#9x@)QP9GXV5 zUOe24WK2%3rfbd2C_^Utm2{VvTd8zs1jkTKFJr53b1-tI562Ti(`4HPG}Bh8c)iuB zF)>~2w}OpIBadf2xeClzeT$?+k!l1bu!SJCh@zUlc2@5xjlNt=S4N#8Qn!o@Qj~mc zG3@Iv{e?@RGzWxVisE7mD-5iN8u#nXW>ktbg9fOpjnX-#UhkM)Uw-v{4u$Qf--JBA zUJw#MK20`MN}B{ta7YSP1AZoq_|ga*ElIGs^0O{Qvl_(T2 zH2FXxV7#OG; zV#~nAx0A%976U|7i5%+LyvmA_iQIlx3D_G7yB8?MnkT}YOyYF zRk5^{^-)6ECwi^Ej}`pp+I|Ga{fv#gjSp33fsersE5lHqNtC^V53we z54Qcw&T=lalTcQwn5i7xs2k;00gg!sQ&a1~t~l&P(_zYpfEXq%$TgN+3WN2aOsU?k zVsei6g$gF91Jx7dY_$>yM@HeaR&G|R(Hhvtu70hx`{F^<-w@#vr%$rN(1C`U6* zq?4m-MJUv+_u~O5P@&rL$`3jeje>;xkW>UGi(vEGVTyINt#OTrHGp|UE*Ev3=~S9v zsbkiiKq5f}J0yQ5nl$yk&r?k#I)cznh+&GtdSV-7k%j8@N;-t1Mto^+jFD{kbsrST zmE^i&7R*37TC|9vPomPHMl2Ml(_Ac9f@x7~M7B2_u6j+K8&rI-M)89pJsbsxN_A+q z8zIgVgbD_GYh*8_S|LQ-UUn#kQ7uM@;e0CtwN#L)AtVY4KJbUgRCMHz^%|r~q%y;7 zk|(#L4p#-a+oWW@Rzm_zlTUg|wQw#%7!)dv+Cn+aw6I}1<%d#&xa|aX3046oigk_Y zjw(=PR3zdw%ZK!8oJdo>I&l0?6lz)@q4+l7^%&;f>e}DC(u!f zabJ*&7Vu=Q+Va%OP(^5Lf15*5_9{NG#pNv+ZuN%5s2L~-)dbg;stOqBR&A6+(*=z$ z81mMa9SZP)B4u^+Nuim=tzriqr5n+*w^R3UJ*iT)B8_x%#6x_lLT>%GLqTy_g-E2c z`UsIs2=kz5qZS1b5Hi{YD7B8PbqPLfwvotQc1)2%irqY!)Wj-{raU4BwS{)t7!gFl z(=MuMzr~sbh-5N&CBJu+Rzk!PzJ8mTKnuTjuI=3`Ss8J<>9ijQKo0>+tp z8x97JmYX2HOh*HO5ZZy+o)%=4Mnb0wYPV93)i_d284Ok*FQKp9-6BiNd%f6Dmg%rLr%m7THc~lv21< z6|S>+570;D|X+!yR!4vowe=q*3WOfV6(aLv%u@Ww)X35hpX>e`GXbz z@;jFPcqz8{{fldWckDInM=kc(0S#-YAozJ4p+iwZu-`_nmhtI#kACn@pu|AK3?r|xeo11*y=I-Nv zW&i$!FcI@H`Q3W#`uO<0H#nQG2XBq;+T}Sikl*BMd{275-{+dm(yx4U{QFnW5Z);t z)W=_SBW^SK&@Coc=kYFfnLO=?c0RwWHm4tXvHkmFZ=adp%Qs>+zlXQ@U75!_+2!|^ zOWXN;uGk#@!mrrBKfE+EpD(|`+I$`}LUVXGyL=uqLg!A+u@U;2U$r^?**}??(|c}2 zZBCCFp*ftRZ%$MD3)=b892=pZdbQ2tPyOP|JihcsbM=8@2@_dpEvHcdHhadW*%R9gFI<8=kTI;S#*u&xni_=yZ+Pm?{9-<JHMWiK{6U+Q_j)F5<0+5)M{d0K1pNAC_*CZNdEv(v(1CyV zW4D8U!;P=E;creMW@GL0=?H|I@Z;t6+|G~f*%P0#JO7T+?a&{;bmQv)yf9&NS8uu9 zo>im4PZ$Of0o343)e zcVm0?rtxOi#Z>XCo6rT6Hc&CK5mZuo8f zZced>H@;_Fwm-NX+WPNXZ{2!u^XE6eapNC08sOXh_4WJKJ_POqesDFk^5ZM9W?(+4|P=PpeZ#a-NHZ$G5Ks0oVf%D_h?(GnlhvoFV5^ zHw^RKCdk(rn;X#e_s-0XvpURkA@IiQw2i_jTkkpluyAZDPFS-^Gcn%g&U4;><8{jA z;F~X!gK`vnJEXKSQL4jyC8H7;mn9it6eTT%tDDK#kPXX$P_Q7W#`OT;;2|LCchAhh zX_I@|>SZhWss@!x1w334MY_nUtpHYvQb=D2b7Vo&m}Vr|C(8hlbP>PlsUXe~cC221 z9Y8z;eSFu3Pzw!Fl*(g9|-+um9^(>V=buyjjbo|EaU+Z%4wu|H-7meew zLaM-&{S@pWJkdbkAnLlGO-kXs8s_3?pqOk!-ICWZuD=FwV5{s~XXapv%FYo4ZCv-e zh*zEp;vC0P^|}ur+A8~&nGq*cc8&vO?K%OFY?XcU%t*&nc8)_KP+2bkvsL!-nZa(a zvM0~2^8`xE*KwO0(DgUX%#EwEa~$3tzK)HDQrLRq`7t`K%1#XBko{9gsnGRD3F_z0EJKlHA}k4oCxDn zuD=}MJ_HQPXlC3gn}nNY<*Zwnhp*oca1H^^aAur&tb6hLWzfED7zZ=AKi^ca&;QGd zUs%}s^|cE4&$B=OW+m{%;Kn_6Jn?(KY9gNatciX8Y@0Ej*gK`1%w&lFSqnCZMw(13 zkZOqKOsvY0jX_Q7bPFWK3mD9Utk5Wk7drGd4t;sAw>r{UJFUR0?wN!*Yu9lCV!q4; z$05$T!8-v_$cus%jMai#&KrzV*`!|WHilBOj6yj>D;rv>;|sT%-eeLKT{dblQ}H|gE-z&J{=*WRz_s13W3C<4U-CmD^6oh#jlkXmd0@Zu=~a zuqo~|%*>Db&kQq5p^TWTvptj=6s%#$(kekeTNVeh(lV)otl`HGLuiW^vSj{_!^{X@ z0_4WUYE`LZi50S6r3kslE8T_|t<;;rj+DyRyk^3TLSimhDd=e;d=NnV*=TH7KQI)f zCy+^9;kj179uFHMq}k?DG&xh;w%zqdAALf-@iN=?yr(^3d(N7`7iN2K|LJhfe+!1; zxb1mbEa;S!50jF6+AvSpo?CmRXUOK2}v2ee|6NH(qL+nr|H*H#JkQ1uo9i`2KC0nse;H)3)z9VFxD9G|!MFIKQ0V zsr}2d<@|uvypwW%XPV2YO8k_}CFHlW{7f~M!LGvftB2jNx6h!%ZVC^9ihCWY!i8A2 zuT;=X3-Z@hs;8#!++3!Sa6X`w!Wz$1gHl@2EJF|Waj_N*Vxl4TET$3!=^Ye;OC1hj zJdlm6NjMlVxtwCQ<=8;dLqRP}l{+;FY2>_z7*fg4YA!+7AAQt5|KGQeS=ju-=4j(@ zH{^}Y^*8T-_kLjScR&Te-Q7oapSSb<>(5(z_u7N2-@f|lm5;7umVbR&U;3S;&eGQ6 zcP!qu@S&Zr-u@xrC-B=_Nzjb@le-#QL*WOQyDr0%S~31YeDA3bP-lFpLMFZR$E5aZ zEM2t|_}h6QpS}$2k1t-ev-aEkKQk%V?T+eK?M(h(xisv`RXd~qS0F9=RXd%(ox1dC zPm8{Lb)2pL;k)MXzL?qN!o3llo>}aa1Bf$s$C>>>*Pp)B=wR;J>HF=u`^@AN5eP)0_TTN$HXz8UWzdS zrjaxAaC)98S6>P6?DahWa;G9J3uO{@g^%MYvs*7iMzNF1v7zgv+|ZHI%Yn^0x!^Y?4wNT2WNsp1+cYhH#+>SGr+c>W!*Yn~izInQP>n zQsPr3o*dyuUEzLZ$~-AoBd&0t=CVsy!vM$LJU#7Ymv*m?D|La*(=%Uo$sSVW>QN1p zIsBVuw#&EXuX^>U2Fe`1O*7-1SmT`b^qmiKd~3v{r%m}?(=tkbSJm{+uNC~-`YxV{>El< z%X?nt$lngw)(59(Uo6b2`_(aIk@zZC3^A0iviHUGxhV>lMk;biZD;8^u<>B zdF;t=n45tMJv}`3YOgZEoJIG9pX*vHO z^ZF;5b6Y>l?Q@pa>2q7Z^FvtuNoqx~C3AGyd)Ars)*ENJS)AXM`jdfq!R}mc^(V=B z!Jdj3Uw0hrEN`k?VtguK56uhqRL1PJ^Mc*^nAzw5l?8NR`}^0G<$J(C&i{F9?Z#zL zghzTuaq(hp#tD~lyVT3HDjnV_Re7fPDcOHI;YQ@#K%I0TIya+=O>n;Z^2rM|n`+X1 z`DBSIZ2W><@%{ECs!p~0C}{YoyxWW=s_xKn8O1Qf9)+pyF?{_XZ>LFSyq$C(HOT{_ zOvDpHJfVQhs5yo@Fgavo=FNBzx?{)XGE|@78oU&ko zi}Vv_r>jdssE|=B*@_OQ!h(t&Xknz>gS_k^AnG3&=>w@8<5ir`(lZrd8+RSl$9RIg zao;*B$nPO-cfyG_W~ zo}9ktvS1UOPy}7MS@LCKV&D2;b*iIw8&hXNrU|$6v@|NzLYOkhO7Vn|;d2BY%HjQt zSHr!TB(6{zN{{;ESum*)N2Ool4mbB8<2R(PRV|J^!0dW}cTx{>u2N{gOq}p+p1LOi z0+@_Pnah>-G{jkcY?Bakxu3ZZr*70wux<4H(O#E|oBq^bFd*ZIWcbijx(%iEn%W<9 zhz1#ntHF?*0EH9B!5}AG@lGm$Juk+g7RLwd)CbQKd<@Jyj#V2JsBJ){bHN;!=mkPJ zOG}bSr6UJET&1G@QLfhxnl2xXI!rAUiNd&lT-6lRRFtf4N3rM48z#z4I%;G8O(`Qd z4*OpNgEuafSl@`vSH)VMifJMuiDftB zW~yzOppHYWZCU=R?pIPnijqX5VkSleLQRQ^0O1ehjY~I;$SK*Jzl)pMh#gKi_nER4qz9FFQiYNcw$sB)%}73q z8lnhFBOM?-!AK)9#8<*xjp-eXD6mx8o4oEsnOI;%k~i+%K!wNA_dFMDMNUoTJG3H7 zvs^Tq#gbB$v`W#a*oN^|q28Mtj;UJ~7+9Ox4aHQuG}nRzL~>*z?2>-R0{SL$Qcc~w z+}55vbsJ{q1UzFcJnxu)Vrle_>abVy7u04_$_$xavZR1L6`vyOvY%!nG^dhD_@F)z ziK7_N+gd|-yn&yb_l{e@QH4$WJNbFruCva~CpiC~JVy7d`+*a^m_J_j_!jAuV%pQf zF84go)}&b|GbN+jqazTTC$KVo=#MI1f22R^BJ3gWJIsyJT`w0BN)0e4x_C$76N3bl z7Yy+*5NM2=IExNiCT8Jbxh;VbIVu|>r<{8n|NmDm{M5q!ll!^7FYSHv-h;dUdG`&w z$j;|?zI!LTv$y?`?MJrJtmlof>_yB11>HVo)xqp2@5f<)$$>lkUJ={yDf`L|}U1|0L zLMdd#MRsCSYsVeC%JORacmm9u1ze9b`O0Mn>$#U@keg~C7lPheveW9zIh(Lh>#Z>Yso;vEA|4}G~(V_uZXm3tkO7hW!=(OxFc(V%h&)6D1f zm=dngpt=w!o0p*l*dWq~)+2IcqpZ${5>_CEC9olgMe1c0 zO($yww$e*PGDLw6QQfrUGa6m_-d;19Yc{A0xGeGz&FIry!d1dean#=ep+kS6M8nKofE_7<_i0bu1 z{mgAzbruSlzw*^LTSO*acO-rXEe`lAcOSKgopOi0S7V}Lv@mEF5^O=S6up+506jTH zZu0oTn=PL5lE0G{2ilbvI9hYx<*?TFO6g>pDTX1I>5IVUSx$>-s|jj-B3#ET!Lq3H zDJdVv?(GKxLLL+W6+%se9MLVNdmH8_=0@epUC!J%IGTW%&x@k|d?_!SRJ?nQLMS;5 zkPVLRFiMSVXJfYkIl8Tap?j)IM^Uak-vN5?77&i2zFrBGh&|EFodNk>oUb~CgP|xf z)Pgj5Aviu4=Xuj`yls|G&=-P3xH!)}g@d9fZqO0BbRjri7w0+Ca4@BwwIUaSgS$BU zr*II^A$mYF?F+%dT%5gAI53JLxsE{@7lMPjIJ?tuV3Cuu=?lR@T%4U#I1q|LrouK) z8q+(|A9#!ty0Q%{8oY4-!DVFflZsB22d`{7Ej@S{nIXD$T1$>f2d`}2 zZ0HmNr^95YHRR}Q@XE%`hEAP+XKTn&%HWmtn+=`1K$)!}N9%%D)^0X*deuDD*=b#K z)GByo^=3n-7t5I%a`Yy6W#wi=@WlL`WBeV(2wqvf+0dy7<=JH9%>3Y$rK5(xmGp^A zm)RO}rgrJdqO-K_U+!|EtOVr{>4Do?GEh---mD68eiRF#0ta&)7;6g5h@jvYd49yr zCmd2_)aF_nId7&?RORT1aB&2h$!0dCMD* zK<22=b$*oL36RH*K<22wbAFh^F%aP`kAlGk+s2pO(PzWSwvNmXBE031n{A%f&T}u{ zr?m-hfiwCyEPe07%5df1tsJa8fBDPHA6@>T7SN9xBt2QAKw4Y{WtB$ z_q}_6xA&R7pWFMcz1p64?`3-%yT82q&Ru;sv5W7%Xy@;Ceskx0K;3}s&fVMpxc%Af zpV@xrwz-XMzi{iHwm!Y}y<6tiH*CFRYhm+qo4>gE1Do0=zlm)=cjNaqeqiI;hPdI| zc**)dtpCyae_Vh2y0#u!f5F-xt^F5}cc8TRVQ;tN7~OtBWhY zxANhY_bz?!(mR%{rQ%X_30=B-@pFs6wD_ULZ(EcXgNug?U)*~8(h}(2@(&%mhgkg4 zWB>c?|NJYEz-w2R9(Z_Z>x;(&ET@Nj;*)E#eZ1MGUI55g09=RZY=7PL`c974zAV(w>*&$x;H_=a<>TlThmY-tUc9=50?NN~J3ARNwf(g& zlSdCvhNMnm^6OnDk4Pq%e67pD(Ic0GuY3OL(!KjjTYm}eRIK9oTSs8~H7@dBoG}Jd zko_*npU+O>b36ZM(>hPMGW~-4aEeU5Zf8I5zB18S+->?#r!|eaYX0Bdhqn!2Jgw%a zE66`_$z4Grt^huFywzJ%>eMD~8+J+lcyepn?xSO3xz`oTZyx97jB%XY;Ely{k1LknaLHY)t6h@+GCRrLu2_E6C7B|Y7rJ8k759}1vAn<)%P+f+j)~f@xaoIdWjBtJJh$+jz& zpLI#5h-J%t6MTGA_&Rfc>aT?he?J`{)1r_@5WuLHUv6V7JEc^e%0|CHYS?lWhHq zJ4iq5lAIpLt$%h$;zRB$w+3nJ|8s@?LHE(*NNoL+E3F?o&dnLqds6f4>Am%ju2??c zk~`Ac`Ukgn``ge7WUWnziX|v`e0KIfrckk3wKI@=nP7|2B3dB3}()vPEvaWK3Z| zaUz`or+gjC7kHC8nUoenv@DUn<1H$HCI2hR$9PeRxWmw_h1gA*F zSa1+PTUga^wFXff)_vT-=Qprif9-#BG18g5g~!|BF5k9d?GP&_^a5WS78p-9g<>s= z#mgovjVNJFbTF!9giq5T7q}0LcT@J)_lSz>HQt--NBfN-!y;;om%2GgT^l$QMlI7! zB^qg^m>}wTD@~5_uv8QY&Y$T8>mH%v)d&fZ86mRspu?3X5FhBHPFSwS#V`^o_zZT0)|Gtn|#)w&ud#(KqW z6t9p=f62eXMHiQk%udgQ0VHAS!9%U-p?vVDBaVbW;Ic3t4rSsEh~$c4Ki(U zGz?YHLdNh0;h07Da`lmi_d;)oVxq z3&06+@s%z`5d|L2Iwwndlrma)HXAbZQfM^Hbg)n^Uokudrl1w!R%G@6bSOOC!Z2R2 zBRemL9dC#siBq7S~3-;hCC#XA}fnyisr})_(`;{$v#gq<53}xP>olr zBcYP1aOq(=o%irewrLC+`Nc0f6s94K&}dprXi9^NRgzpP9PAT%Hk2h|!yZ=1XjoaM z5_s5L!5oT~73&6bP?Ct{WmfZ;DNj*@gCBBWOY~wP zPo59urFOK(GCf8}cOol4<4~w+KA;7nL$=Y4asEy_(CGCEzT0XQ3RafVecdWU*Xt#} zH@*Yqy*END3~;VY3~iGx>_uD(8pYdK=@d{^DC*3Ig*Nn`}`c z!6kmLMnXe0*$m-GKUrb2a>n$SwHBSy!%2DR%?^b^@q>`&BPx(iBCW2c#AP%j;wdMo zWP^#;TDp$P*VtnP?r$2W!5riZJy)SEk7vELrV1()P2^ z(w;++ZF6n5(#g@;AXe6ku?7(~DoM|fM9RQHDN!L}BM(`J8KS@Voeo7Js#$roSP+UC zAE@5YNm+cELt9x-Mb&h+m2Q!xQec$TpvDsJP(%j#Xe=a%*%W8#NfHu9RJu``X34AA<^BD+p?BsGUS1k{%#I5L-d?d~rk-HI`X42z>sbO;l8V}ofESXe5;AYDP=tCEBZFv4)3^lEZdAH(->i9BG&; zFJm1gjcvTm;YuiW+BBKW&=G&JQ1gh$I4XtO@_2SZ{GuWRL?9vyj@$eQL*@F}kn zz#^pO$p!ITpe_}n>Tsk`nOuGdHE9x(H8~P!utRz2^A1JWOoic5vR{n(RXI!ogN_x3 zRvs@CK2MfQb-E)wnU%xYc4O@cryt=gMGe@ZIgHBDUbYt3eRzW+yghPI2*b@@5(%}! z*(?&MLTkUW{g%apDZ}*apH54_8IDq#;y~@F0$RES#uN0U=|xvx7F2D;9@jg~ygY=TPJ^CZnoR zmhu)eQNLD)${ZQ;;X){x2r6N$p48!Tp**07_~MrxicG3vn5|}|-oxl7oVTb!q7s8! zqf}_bb!cm-4`JHZM}>i~v*J>CwP+5Gd4ysO#bl<{9WkmBEks8NI7%ndH6)Xkbs21@ zgpH0vA<|wd(#M+ZK}Cy2(*~2smWZ_B^YbYt?JFXhR4mfn22HCAzwb~8Rg((qt$c#P zbFf#ykrW&9bBRou$aaUrfGBgLW5!S+aU$)jt<%$8bET|kD zk5oK_Bs9=MN5MOOwJUG^*xvgVzI=L^{ulU*BdV5HUm>4~(;z7(y+S~%;4P~Nxo z6Anff>j}AniTVc_1@CDcP-0e7tPV>`e6`Z#`|XlMr4-UP z5GX0v1{IQF^E%9If54%DjXn!P;=o=;@1<&5mM3b72%Yd}%8@#7u&xknzSfniMY*wc z$>FNuEn3xfk}^rEC$B${zEUQ7R5Y> z7|-QKSymhb5wO_S3Dpclwu6A9JL0N~_IO{^hj}U`wxq^7h}84xOk7~nSi02X2RvTp zgCQsfD79pY>3L3Xh}3Fx#0~|VbnwNrC8k9etb+}_~M)r zt9rv_vB<`G#>+*qo)v6Bv2I^A{aUCQ3&*N#yG=FLH^=e+i!WH%dE3SdSN;R|$Fn~` z0&icv@d`V|!8g5d@$ww0l+O1NnKi}1l*(0?F3*u8#|}D~Cpp9T{W+4wTmk{dCj);0 zgQI??kty=A@K9>$u{L9P28k}0L3LlWiZOn#z_dG|Koaf-F1+D z;ceE92W&-rS9fuq*>!P>IHj)BzXnB|J@M5vMZ9%E{0u4JO$O|6)H^S`cBgtrptv1i zI#uruD{w_81n%_tETOw=Q)HZQSw(Q)yNcLQ-`wWV}X~NseF^3q$QzN2lvS zqZHE>C~37IBo~r-WSGgh3(Gf{Lf;o!xai?MdUt%Wb`Tj|}u~_Q`Z{dPkhD zr}hjiX!EX!Cl<8HZ%)sUCY@ilY|&>?{O8hThFGhtpM=W8?yZwt|m)bQPOIXjWyY})RQd9&ZdKpd35+W z!K4!zev9!jeMxE_HPxuejiY~q>qkkpdpCZm*6h_RyE}JMw(L&LjTcAl?oTy0i6F`Z z%|y24&*HqyHA_LjVk#Q}7hqVd=aYg-rZLJk6aHBIj?E1cr*lNGlUDrgZUcg}p4!Pr zBZX3{5T;uBQB2B+Nii@GgG?}I3f@?S_Jwmi2zG1xsZQ_EI81x{1}PKmoJAj?{Wwt= znX?rkL+!5H%Y5&~{o`eRxOlON7-!vYd!MSgnYtgIBc>|sM3}F4{kdRb z*yyDX`0}ufGDc2?g=|eqa@1@aY`g0~^Ty}@rPnU(cs8C~JpliB_UBm%%q#(r3i4I9 z-L_LfUT8Y`6L#CKo<1!F4Pj@^JCTKHiY2!HbYg4fc+;G;t@)O$OK~nfs`o54Z*{cV z6k{`y^5zT^H5tuyWQN@68(^c};9P0+l#W?5>^;e)SyMrtYIoA4KE0_RF%&^;qdnDB z%FzI(lJ&z;xX_AqGGX4Tg9xyLQa_u?*SP!v9Wj)66?~HkV|Q#SjhsL>2Zs%$P%2AZ zd>GDEQ%EPxpM_B>HTkiCeO>clAV{s1GP#GDi zBBeJoK66^Ow$o>#ao6ol%A+?9HWZM6>0RjJ#nz{rn&0gyWM(4lJaCf}ZpvbAPn{E* z^YmYA%zHt2B8%Il%X0+mjdxVHxn09xFJ{l^v(iXSI<<~{QEg*!w11f^L+4DlmM84f zt-arbxw+}&2Tx^gs%{nrS^|8wVmekH7DE}x8_fs?2IVWX)DdaTD5dIxVSv4cdd@b+ zk-_AJ#W;&%J`wAs%8^X2d0;16!3JTBL^z1@>-BC9H(@H1$fHn?@m3nKD8<*SawUa? z`&tS1laMw{!r{7D>qkP6>+Cx8e{jpP^eZ{qC-dl-NS~N$2j_K!;o7rQZfy?=$tg)~?C?F}$Eyb~vfi+%V zjFp$cJJK> zi}`^RiUZlwSsHkv;dp@H8wN3KSlNIklng7U=L$70z{UB41K?FHw+EphdJwX_ou;nq zn20wF1PiCM+z2a-jELARFo;1;Ng#ZBOM2Axjsk9DWW{viL+!E37e zKDGDBycz3SdK?&bCpd*Qv;?fLegy;tnr zvv>F2bM`j&7Iy!B_lvuKy8GGPPwjqk_m_5me)q?BKe+q;-S6K0_TBgHKC%0z-N9~a zx4Qd{yE%|kAiVp!UEeOW`-hPI+7(H3++x(97W8_`v016o6_F*pqsQ4$5wGGs@3Bu7gSAJNeQG#AZ5GZ98s zCD@5?$G73H;al-1@h$lM_&xY$d?S7pz5!o@pNXG_7x5$>#FycAT#w7~B{(0a;|uV) z_#AvDj^Rz%PHa224SNmSiam*K!S2WI!8T(Xv8%8R*c$9i>@=*1C9xp346|c;OpYxf z>mAUs1=w6{4mK0Ru%?ck9osv$b-dQGwd2W-EgkoF+|#kSV`Ilv9UD5)Yk+OWOJE^!5eqbIDo^GuyHD zrna4J+uOFaz1Ft1b4%y_o%eKZ?%YULklfI@rt{3s(>jZt$;IFmSyC=y8`NGv1l zgr1NSO9+1V&hG8q+qz%t-rD_S_m=MayYK1V+`X~;s_qSKPquAoyT9$8w#{uD+pcQc z(6*-S%(m0oifzg1^uOyBN%MwXfB*c~WJjQ*wWWoK5iwwthyo)-1Q;g5zz`7v28kdr zKm>q(!VmNjKA@NI0zHHW=qB7i7vTcVCFTMTB@P81LL34-m^c`C5OEOjK;l5)0mK2o z{fYg7bBH;>{fPa5vx(WjeTjX6`w;s8_a^oR&LU<3_agQJ&Ln06yNPb#3}Obbi|7J& z5}iPTAb^NKK%BsV7=Zyhhz=l)paI*7c3>OP25co-fh|M}u$gEEHW5t~qVLi7!0*s^ zz;DsFz;Dnuz^~EQz^~9(z%S94z@2C(@C)<>@N@Jz@H6xoa0l7}{1kl(`~-ah{1|-< z+>W*b|Aqbq{0Myn{1ANz`~ZCbd>_3Jd=I?`{3rS+a2whNd>6e7dg$Dd<(q= zd=tG1d;`4!d>y?Gd=0$@d=OqAd%ay+={jWpF__9 zpGD6CpFz(6pGHpu|AGDi{5$$P@G0~Z@JaL}@Coz;@Nx7w@NejEz`vru0v|(<0Ut$= z0{?>k0^EYO0RN2s415GV0(=-f415SZ1pE{F6YxRwAn*b70PucvKk$#}kH9~mKLCG^ zeh>T|`W^5-bRY1y=(oUo(Y?TX&^^Gr(cQqi&|Sbg(Vf6M&>g_vpx*#*N4EoSL$?7p zqs_oyqhAAmg?^1H1rT06ZU^4?GW@2Rs*@3p@v%13Vj@4Ll2- z1w0d-2|NRx0sIB}1@LrqI&d{w4ID*U#}mf`7ZZzt zgTx?k5wQq(9B~|QA+ZoRKnwu+1RuyFct9?}1#$=ukWH|GEP@4O5=8gVYG}gH#CegOmvJgA@qz zL+S^}50W9s50WCt50W6r4-zBD4-z5B4-z8C4-z2A52-#NKj?S_`9X^jO+tpbPR(0pk4&|K}RFV4>}4#en{N}`9Vh_$PYRKL4MHT2=aq^5ab6P zh9Ey^K7#z9d8nG7xo9rr=TLMg@DOwe@L+T>@E~*$@IZ7R@BnlGaDTKva1NRS+z;&s zoQ-A!_eJ{x_d)vr_eOgIXQ5fZz0h93nP?`k8+8L`pc%j})CKHBoj?K+K!gwwM>r5e z7_b9%0BMK@Y)9?DHq-`eMXkUV)BpT_mrKQTwg2OTRrX3*m8?~!^? z+di}HvbM#o_q1ACcC?(+GOzi!%_h>w|K{Jy&b5w?7Q>NCeNCNTPF^s&wm8H1Gq~*C z3uN?=Yq5FEkjgCyDeV!fKoRg|85VAYlTU?yMB$+MRL@9SRE@8VP9PBBZ6(gga#274&V4hKSJ8N)1$ zTZa-ET`Cb+YpuAEu$nqQpS=EV?ZN$VzKS`fAV?2$^kg>7E&114NKeOi8JarZpR%X1 z?<9XL!_6Z%Rvn1sGxpufNuRXVOgieXIC^i&j(+?kIkU?FxupnStce&+yL~0FY^{lO zz^XX-=ae0c{%CTS3m%Kf69keP&2BDMwyZT)^0Jilv2DseslYj}+*EYrgnT?G>?uWVYYt|UuBUYA~-y!dbJ@%_=&*hbs zHINd9ZAMWb5*@Pm82JIenOO&{`E4!|`1{U+2qHj$xcJ zt}~`%0bzlw@bVH_mPT!iNQG{%vM5fO<>^2s;wePrz9BE4%?$K7lO+pF%}Vl9QgzO6 z2t>$26o#-#!=pQl;Sf1Nmnw#N7M3r~;3wBAtKl_uzVz>e*MQ65S_SE{zjC*Damp@7 zokB@}hGAxwjr1^!ud|v{cCR_7$wwT&HV6C+TmR*t%OtQ z{MIRlLk)DX#Vt;!n2bJMfSECh^JMXBVbbsQa>{Ch&A^Jt^tr6Y7qBvmbaIqQDt;=X z?77LSU)LLDW3CH5v7sQx8&8|K!ep9Z)r9CVax*Z2NhC5A$1>U6Do{h?4Ql`LRmb>~2`6NroaU|D@s^K+tKK<{6*MQ65 zS|RBYM%h26>~hp7D@BcLRY2^pMPy!bB+kl&rAIWk`hA z3P@Kl%KkoOS2d%IUrY!kLnfo5=p2gJWIUPL?`H>DjI2hcv=qWVy~-z#x)O0o*+P!8 zC8RePWlv4n+r*=6^vwiDnO$zTi-H3IW<1JBI7PyOR;XqU1RNHhKj#jJ2KYLDS|D*6 z^8&Va?eUdxDx>VlDThOiGNa3tD052+6(f^l(2LxF-WDl1>5_Dx#~9|=5^*zUAfwKT zoLmn%$`)7rR7Tkolb3(5H_FD`BYT{2V}h(%<9Ek+!Axc-R`i#dLZ3F13dSs1lgt<+ zs{pV{2yBu{E50RxeO5}=gWx%e;hnNO0$4nN&2uPISD6h}L zFpVeRpuRV@*1!wVNQ+8Ez7Sp-zG$S$)E{Fu$klNuZux()hTbGnuC26xx zlT_MG374g8WaKJm@xn?vD+=z>DSMmnEM|?C7jPEK^YI}eC#a5aH5#AREs%ye@uV$h znq6eCSkJ|KGs)ocvsw>UZtqE`MxbL{&bkY2=H1_;DI%Job=Vhe0<)V_Gmk4cXhEc<3+5(1N zQC42mM_hWJOD|>gW=f){mTptgBU-XFKsn>j#F-3dP+n5Vg4tf4HlvEz$zpaQhA>wU z2&eRc6Lh`u$!~U~9~K6Z>;D@#yQXg-j=M1MCqEDy32>anF}ofaD%XY4UmE_G2H=kY zRL0$Zef>Y1+_Jgu`hSOBuPr4be61sG&XQ}{EzS~tC<5%u#-c6lO0=a*7RbwcHq@eIn;K-S6>W(2H(jLn-$+gMhioGf4& zNQ;UEXE~;+R0Yz~JprjA)Em=Txp9`qCmv^0@v$EkLf6T0N3Q=rwleN2>;D@#nWk^t zamM@7w}EG?>A9gbdPm&#EqZ0p96S1X_JWPMBlxnPeKQo9@sv7A-%JA*O*H3za$z08&j zsZ@EcE}+p_gwEb{-XoaW<)+g0%56SB(g(MBa{Ye;V{ZB`H|&4+a#OjBj(Hw#&s`EWt|36TP`9HP(f6VoY^tFd82-s{ogI#ard6?E>nin?o_LpS^rh;n{ z4Oj<6@wCHg8Q^=JGM!K@%R4xMemZ%olScMrNsm!2m8t0&t4EuTl>}C)LCz@@dj%Xx zDy$aTBJ8-+>SvpHu?$~e3k3O@WIn~|XD72UZAz0=D{?J5zL46wez5h(|yZ#Gj7-r1sdYk+<|D3Lqx)_}wci!Ha?qm_4 z62GbJ=ZAKnJ5dgC@Xzo&@f^;Nye z0l8b>gKfiY;m9}=YJkoM{i@2q=`N8Iu%}biTCj;1{e~X7Xord#|{5#8?FP46;5YiXg)x^VF@tHwY{>le9%8ojTaOBW(aqrhG_f*%YpYYYfg zPGu$`)doXqv)G~aM0cN|92)_n#(*oN%M=R1gfm61wG5ZtWl!A*$YX%!-Mo01USmMv zC|V*VsazMx2h~Y$M7R6)(qkj&uQA|?OVX;WU6HY>B$71t zJdp~bj5w?kW^)#II_4zLr}|lFZ|^>S(Qt1~b5qhTDP>EApxG_(XG>OLx^4%^=AuQz zN7pnDW&L5bJt%SMj7oLH?YGtszv}&{n&z@hSR6>Yir#|5ANTn~w%upeclDk|o3(K9 z@B(Q5!ypPLV(Db8P%s#>fndfHD#z;vUp@MZhL5cAFA&Pzev3CE7bzndO~z%()-}*h zn-yO)d_+xiaXjnw3-o@GKj76Rj2dsWe)EofPgpd3cujM&TuvS^oyisw8q9K|Ag-+6 zoHi>qINbAN?<<*C`LZI1w3s#*T?MU97T5XfHm9|89-AB-KCGs_GGuWTL{dXM;q@dl z8J%A>L3^UNd2o1sO?z!1Vz62*+H#^qE}hPLA`=XrR{Svk;PAYf_R)YZob?o**S_~7uNHSKlEP}G!)8}jNv z!fr|#UA77OPn$J+aQKj#_NH{fn-!O`O0h*~b?S0H>BQ|94Id2ce;B!XYss8fxl{45 z$?lig1e*H&Cr>{9vBL*-vZy;qCH;)iAHl$P6T1dWdn;|0>)7D~Ya5LI#$>k!Z7prY z9fQLM)O0W&bXyc2g*%wDM~o@IPwbkwgZ;z%|LCxCk7JCU1>enKduvO3ck$TaIX||5 zK8>Ey+pPtJ*f+diCu7t>82yMf&hIx54$rRXk|}P~B|>7ME2-6HH8EA(T)#`L*rBw+ z;eBh`=VVEfJK=}~idL;IrS(~46SVI*;FH1OeQMf^O;TYplJdv%LW7&EIBl6=%(Y>2 z4jdfbyQaNUrB~+DBBxv<*GnQ9hq(SNlSYo2eFukU)wI_KwBm3us1OS5s&pwQQ`JA` z$ZJMV=iu;OHSP0psXZPMnfzvV#-vrHvw`~kZ|T^#YjAjGP5ZDhC-xP?(y~IVQ@IOb zrD3A>`)nH=?yhOC^4PUDt>3NoI5On(hg^}IpgoM)88z*_LchS|O#4GppGjp7JCv~r z+T-)d{&&^1&$X)qQJ? z`xN=09yZ8hp>(Jac3CvR2_BXz?vEaBsaz|`=fujRV#~amYb#@b&)IdqX#M8s;pX~3 zCe`O|KW?h~V-}Cet^M(;6*WH|^TE)rFxn10de!opACLL&VbqV?7Ogt9dVI;zRk?eZ zO;V4`E+*e}i;NPvQC`f~O)rhMziM#RDK*D;(hs0_`6kbh-k%Wi7zBb*>?Z_dbInQix>8-Dpd{@D9TFj z$4sv8`hDfrdE4NsVvRqK(d`cwonCz=E6M0(dO`hgX)V|s`QWNTP5X?%CQar|7QLkC zR9iiIiMRgIuC~wDw4Zc>W>@?)@>bGZ(f_E4-!voIxs?#(o3VpvXSVHVUDo_p@;^@S zZ+PZvJGOMSXWpTb_IlqFk3J_WpI~Ujf}(?2%sZG;y300@Q^?@&j7v69_KFj1omfy1 zg#`tTS(|jqtU0eN8gtpKN6w?8nL9{U`8VfnK$YV2?i8miHfvYcErfvi-ZjF zjZMj3`QOF#fI%kBxtZ*&CM+oG%o4J#(yuWGisb%q1-02h*NKxEH&YvQ%Yu0i%fQu% z0|ku$d@#X>+{p3BECIbq7<4eTQ4dQ~@m12;z!zzo6Ly-&803z!Pa0)e7(t0CoG2Qt znv{iEfZsaf>$WLqclNSk)q_P9|e8dB@Vp|6}NV>FC_vxmsU#;X*;C6!$D#C^wt+{q$qm?B>{c zZ`@@hb7Gq;C!>e{Pc0|f@-*c{?36jhQnNv(4V&r8m?Nx7>Ew}daw5+2yyg&}$76&C z;&HCb5g*cPvO-D3EY+3r{2{s2#>?xhMvK)Ho~E40f>BdksMJLVVpdz$<+GCCe@PVMb_ z;VKKfER(78FnC@g*IZ%+5^||VVKJHoO2L$KGIBGS6U$^d;p}$V{V83$wB~8ciC!yW zWF2;Ln<9ocsHWPC<@W=<}2mk*GUp zX46B4gxO#enxtaAb?Voy=$m9t%#-D0_eEJhshmvL{X~*1Im!%^Ev!vDtwF8YMy@Oi zkCzjXo7{6RUy7GA1(Vz&Qu%#uWrWv=X&OB(!O*hAHP{z((f8I9X0c zzmfXM;ajS-043V`;g&7rny!(kTuQJJPY=$S}2o&Xd`jFi&H<#^3 zB}=PeO9a-m%qOuqGp-?SDv*#Gj7Eu2VUSEcCkO3E=EN{rPDa19`^n{G+U_S-i<#+V zI3h*0!y*j1$(?Ds^jGKou`sH(NC6>(bD}Vmy_wbpU91LMM-NjvXXsd@qTjG zUXDq@aEx>95=HcSLB6bJNFr8-s}Lqv#PO^GaxY^!x#g~fo@d2*!6A;EYtdx_337V~ zi$r5}7_?K)$)8uM~aTPvS5#mcaIk{ z(lW1JZFXv1kw|bTE6mVEB9SevbK1z)1byVLtEysq2&J8&w(do@!-Cs6be+{@{XL6$pu) zqbd;At3MbVRe^9+{lQ(Y#(u8Djweglw;Uk~Kq*8!RMwG^)bGVrT`bo?q{KmJSLvG|q1WAH10N8^`MybO30 zekpJPJ_0-v-vB%UUk^MCzl7pC;Cy^7a2|d!a4vok@KF3h;KBGB;6eBW!2R*_DV_(M zgP#lB4?hPu8$TPkFMbwq7JeqhGk`PkUjV!D(}6Sa)xa)%7}$xg0uuO2Ai_@r;`j<6 z4PQ?2RA3u^3a}MF8Q6lCfz5ac*n}4=MBn2D;CFZ)_${6TevM~=U*Q?x7kHXt3ivsm z1b&7mfIILw@KZbn{1}e{x8o7uM|c?cAszz0j|V9RfbZdc;6HI6a2xIgzKeT+@8E9W zo4AW2)ayZS;83pzy^ce@9`qUx^?J~&IMnMwFXK?J2fc(ty&m);4)uD_3pmv4LC@h( zuZJYm>p{=rP_G9)gPXz6)3^!v58McR3O4|s#Pz@@a2@b*Tnl^**HBagAH`L`zu-#X z7F+@RGcE@{jLU!z;ZoqAa0&21Tnu~w7Xkl(3n>bKzr&XR@57G={uW;hycZt?-h(d! z-i;pzybE6lyb~V)-j4Gr@_@JDT;OJ$1N=442L1|X0XN}H;4L@l7UV|?HUX33KyaGRh;^Dx{@gCr1_+h|H@%g|Jd>(KE zJ{PzSKa}Djz_s|nz>D#NfEVEh0x!f50ItFJ2cC=1p|~H#*}!w~eSv4=`vA|v_XeJc z&jOx~??rJYa5dfy9L8q=SK(d2m3SxcG@JmQiX)0R@Dv;amhldXG++sD2Nv-*U;%Fh z=J6I_7H{Xx&dj+V(UIr?#mw4MXT!K9VJRW-*xET8fa1i@D@Hp%#;6m(4-~jdnkdHkM z0bu|ETk#U26nVh>Y%2zWI1C*V=ogTMvY1HdD( z`++^!AAyHqe*n(Meh-|7{SG)6yAOB>_FLe=*uB7muzP?9Vs`@%!0rO>kKGBJgWUm~ zjr|6=FLpa{AM7^Z-q>c~EbQ07y|7;aXJWSkXJDIvUDz$aPV8nNf!zc|*hV0Z-3aW! zZUDAp*8$tG>w&G>S|R*xA6hu(N<~U}pkf$Ibx0hW!HgDt0>XWo$L@C2Sb@BDM|oC z7gPFwkkbE)DE)sNrT-V=aGxOtDE-f;^goZ%|6EG{vnl=0!r^{HFe&}dp!7eT(*OOG z{_mso|1p&Q@5SLhLmW-%|D!1Vzkt&JM^gI#a7zF8;BcQI4x{w{d`kb%qxAn=O8*~1 z>HmW%{eK{({|~_7K11wJ>Hj&D{@;(%|FbFmzYnGV_r~EqL(HP||6Y{-pGoQeZc6{p zp!9zirT;rA{ZCN(AEWet2YxE#gGTB9c1r)ZQTo4?(*G@0{ZBMk^*_->>HqI3{r?T6 z|G%d6|Cf~h--)Lx_R$xV{{NiP|DRF%e+Q-iKcV#h$CUp67p4C{qV)fVl>Yw!4}iV* zDgFN*rT_m)>Hlq%{(qO!|8GUF7(*Ij2{r?=L|DUDw|1*^Sf11+&f2Z{SQYxKu7>y?qxAoy zl>YwWbm(*Ji;`u`3}|NjQ(fxX)){eK&!|2I?m|JRiMzm?Men<)K%3#I>Wru6?! zl>XmH>Hixk{eJ_c|F5U?|88?^#3LJA<%9erT^Db`u}1||6fGu{|hPoe*vZc&&T(JcIV-8<^#2M<|1ZaJXm={5|4*Ux|H(KF{(G6y z|0PQQ7b*Q;p!9!^(*IdX|7R%upQiMGiqii{O8+M){U4+Be-x|g{|Kf3!<7CHQTjhf z>3=_^|9zDH_hME3@1gX+o6`R-O8+mT^#4he{$EPz{}ZvQ{y%}z|4vH(J1G5cr}V#t z(*I`cZ8#1lO8*;w*8e}DTYXI^kS7DB|Misq*HQXkOX+_#rT%l zl>V1c`d>`xe-Wks1(g0@Lh1kGDgD2g(*KJn{eK*#{})pFe}K~eJWBs_DgDo(^go-@ z|4d5%GbsH}r}Te6rT_aV{eKKr)&ISe{y&=1|3^{!e*vZc52y5h52gPPqxAoLO8?KJ z^#7rh{y&7${|8h0{~${LA4uu{11SB!Kc)ZoqxAo5O8@Um>HmEw{l7P*|7TJ9e=kb^ zcT@U*2BrVIDE;3_>3@RK|A^B6IHmtFO80kA?*V94U60zR_W-Tbdw>?|JwS8yJpgL@ z@jXCQ|9^{B_5U}N{{NcN|6fu1|4T~$@1*qq7nJ`0oYMcFQu_ZBO8HqDN{{N8D z{~u8L|9wjTzenl+ZIu3hm(u_5Q2PIEO8>t}>Hjw<{r@_p|6il@|0|UKf0@$%FH!pc zMN0oaPwD@yl>UE?(*Mt5r&hlIKeWl!)V*THcQa1y`ljpT&aXO4E-jSevM2oe5&>m@fuPxNNtu@f{PK&Skt!7Wt8)WnP|CVn$rOoFv^fXejnKAXU zzLS^oUB2n$h68Az;O|rdC^sBH1BI5S5lG8ZbKDDM&XQz%-{I1pzcLwtJ>IfFuOaKrQp01}bk) zUT}K(rbNR5G*Fg$DgneB4xoW@;8O`81_3lu+P#7L-%|-7+He5VoPtQh0ZelW!Vo|M zbh=aEX*hss zPJtT&XkcvjOmhG(2%v#Y52m*W^YTs08V;a=nu?Q`hg`mCsNn#nwH)s9O(#JB4HTK5 z-U69W|G#B@Q|AioJKA34A3y*5`3U^)j{sQ^mi_;h zD3SL1(xG^Otba^y>&F%XkV-Yyj4w`huToCEUbT+h zSHo3Wc@pugFPF66{QMp+S8i3QQ5SK#qQ zdJSBS$H24ZjDi?h>eG-*Yf3h~!NyMPaY0ya*&7L`S;}y)#bii_y@r5QHgjE@NXALz^@6*OqM;?9b$Q!4) z@;K#^s~hlC&a}}x7W|h+2e{1p(wR^qUKwR<7(V2MC7Wn$)KxO&^(C{xN^Z!%|8v`m zaOgaGWdoCO>yDenNq)hj$Yu=XfH&-C`?qNX+?2;<13~Ty;67l)Db4;GpuBt!)k71@2|<9EKi=F z$YrA;xa9g>(TtBg(3NZOP$E9$_61#0|4_nTIZ>hsk1HBXWU>pP(qiSb8SjR~8z6_p zm;&Po#XwXVWRexzEh=v=WN`T%>5`sn6T$_337;S>7s}oWu@pI2cN@cR?qz(hAR(p z=gRtqBEC{3w@W7RKHgpG$IwP!#3rrkRXMD69~SCXCa?Oc+zxzRCWk}r?aHWx9ghh5 zB`yb7F0tpWp{PT`P3s1{p`hQ9S0oK8o-^cOD03-|LZTGV?d~*-*>B9GyUK!{^n_8_;D{^N-)gDv^pwq+E}J&Lg?@% z1Im6!K$&FfiV3&7#O&w$gL1vIZ1AX59GOlO%`=KQjW0)T4C2&CSxmc>tj!oy8eMt^ zzc(oE?-x$#agR+N9WFTl5vle1I;>6IPjp||9qnG&jm>y=#&t8&GXyibJ3i=orR&zN zlRKG&=FSNu{uh;y;U zcsuzm%$e9VSn_`_CV!6L=Ogg*5%~EC{Cos{J_7%pBfx0&we*C-ia<;k)SC1;#ZXD- z=@X>OY+uG#@JCd!1jo~7wkd~1g1C#FXt@Yl#5^i5H?B={#F0J&-{ll5;&Qv&kj^Ml z?v&Qfm6?03~>MNz6v#ke7vTbn9)mC3Ttt#x`+d4q$a53-7} zV4uTp_jt>i7{B@V^j2SUPb^QC6&E=)rH#?%R18ha?~Bp=zVmvUf~g$gC!?3&J$>|(Y#Ke@^QUEYcSDtdewz1X3!U% z37#g&5lh7Zi@__f>vTG?-BK!e`}7=tpD$!$SUeud0aX`HmbuR+F$Jtb9=|V~GFXD* zU@9LFmYIRL->GmW<;AkvO;5!P&9kd5SV5k)WGQ)!;sLYYZILK*PNU11(hCaZU{)^} zk{aEKsLPTOD4OoCwqWL!9J)SiG^kC5ROp%q5GXy3)0IJs zBo#3QOw|TM(t^KaN~X@hbxBr_Uq z+!Wi!RD{DpDc_jvliBq#W5JLWTAE*k7LgK5l_>=Jh7uX6o&1D~!HfmyR)tJxQlyM! zwN7FVs1inXLZ72I{iE8#Ceu4jHi-hMU~6Cc&VH{t2fB~{O0$nEmY~WSj1)U z*aMtoS&-rT8D?3*t`#SwE@xjt#_C*Jx8c$O-9brk?78uCt?4g{;%8aM&1{RMWvlMgQAr0M?<;5%^ zZLvT%+7kSx%i!RKa!R4xWYf9GE!$X*lqbdunDWvAo7e0tdP5#|$&e8Eyg`{V;u(aX zLV8zULCJUItwRwRU+v{cSZXETlH_{|p^zXmphA*Ns{rtyckhD0hXE*Wzrsm#h_=+t75RFdX!<#~a{t?@;a z_JMMw`BrcdEGjKoUx=Z!D!p7m$-~hO7$o^%CPcnhN=Ti0p&(Tb2#jPob;HqB7ezso zC(^3;k)SA7(%I4)UtAi94A_fKp>oLJPtYF zZuSIh#bC;8jzmIq36nV>W+o%53@0OL=2u&I1RP~v5@ZW3PHmRU_2^~1OrOrKD)<9B zuE6ThrWvW6D=$|ony#<5U>NP$3{NSuC_-XeP?P8@I1Nc-RPAzFd7`|}NS?=Wn@3`H z2bzbWMIa^1XoVgN)1r5V{Zf6zp3vn5x{#re9b#BL0j)HqaTZ(!p0Chcy(k2NwnC<4 z^5tdR0cVVp3M#DR))!v6(Cu*u4cW3FF9MP`?pmp6;_ zQXxk!cIfD`gpH0@=KqgsThP?;R>#^7J-Kt=?`Tolg7&TLYum-`O>LXoqTPS(KDT?( zjGZ$!lGWSx>3XOu+eIhqtgY!(6W1TWLqgM8wwEDTt&qv_@*CQZU zePFA^uxmntHM&4PsDXKt?gAwkUQuI%oNw4|_G|LBESllvtxFB{zG2wC%JSs)1jDD+ z#x#1 z(~9bG(w7sdgw|N}NhGO|x@0w3N2M3)D-RbZh-vhJul#!6r1k{Eh1!_Lo|8F!(XNy$ zOHxB2pCl!uHZvxs`~)$LKJXCJr1k{Ex!RaU7s#(3CqpFSjhFM@bXe|^$|7aWXklGF zPT2`!8r?~VX;OQF;Y@8zqYK1WV-n|5@<`I`w}~w7s3~0>vxS*v^#S!`;*Rbl#5DP} zVDf7Utz97Rfn{;bC!Rozh`#AcrSNk~W{&t$xvnGgX%ynT>dxF(_ z)y6bBb=8x#5DRMQSIa8_Rs|OMh{4^H|b`Ag4K9!A4ebd zY9A-vQBboQn;@pq1Bi-gk{t>)Wc9cD&qSjG1bdTi=qFfBtL@|Hkg9#0bXPyk>h=j@ z8gp(+B4_)B zoh`(J#418aw2(9XtB?@2;J?AW_`cX9SOHtm@qEYG9mml=pj|=JwtwHgx!u)1uWd_P zqHT8T?_0gCM9b|hj+Un88=JLFpOH@X_@8%Aqq@Y=$!Ci7R${oXZ4V zOb&PW;ZHVSc+tlXwCsKA@bjjdZ@p>L0XJUvsQq?ntA-D8{x-xm?fvnN1?qj)|K>!E z_`}P0KD+gW4-&S0HoSG|vd66>REj@A873!Rdn5!=pminXQYh8YzYWU#auYB~41xM5R z-h80>aZ~f-($06D)Oydl^s9{zFE^z-wpd5VwSc5y9o-*M%H#tca#soqN2C?2too#_ zFJ(*%q6HsUn{!K8p3Fe1UEIVDV8nC|6Q4L`i& z@CQCR@7~*Z=_kT}dGXG(?+|P{?CyIxU(oLz+1qpNQ?FV_$hCW!UOIVZVZ< zl(GB5EFR074J7?pyF(ExTH^`BP@rgz_6<-GuXrwYvgUW^eZuRBFTP;;7x}G6Eq`X~ z{HW#1)yEI-^Y+(ISx3k3~|w6U({9gh`dQ_%l`! zLuFw_$@!2|&7g;zH`f7_#n7=HV!BOhwtaofqA%Wu1S{w;>D_a%&LUol%p$hCH) zVWl+T3I+noL|Bw@7`&RaHlLP9CGnsj?~x67gC2QQZpqoLa=M~wxW{?GFEIQX`X*P0 zdee*VA7VIiHSyAc&$*H%nepvs4i#EQ$Tf7N;gTjb6mW_i7xL3pc_^Dx8`!N2wdkL4J@=C6D&p$uG>m zY}S0+g?en|`#IB11K0gR{lrD89cjrG_EXMy!a72(c_R&zQ%XgfDCN$^C2V&p<+6EW z!lb#Rk%iNKql7EXi0xJuTNqX_1=T|_>#~&n=*#X(@#8mN>Ue(T^^49uVBxD`?dh92 z&%An?=dfkg5ppdXX;{rq**O`Ff@jy}jZQI3kkSPD!UlUN$_@uYW*NgQj9bYm99=3= zHQfK``gi_>eV9o|)_l=&?cA@}`<;E#VXJx`d4?XaeRa&jn+HdhwXsOU%+b%raJj9v zPx;O{`?_C!%J@|Hi6}>`E3Kb9n|{|T@4S{fzWkm3p37U+BSWJvU2Hy=#Tp#t;{ill_)mdG)z4-f^=811K@PGZ)5eGbAn7x*F<(VX^pmx4K{& z*W7bEIPbo^{m>o7+c)&Ay8q1$)-U#@<=)y`cNRUp^Vaj{ey<)`T5FEYVv!sCNyUP3 z&0Y7EvvrQ}!L#=&FL)qy{vD3%P-yd)uqojpY=Afeto&_^Xq=MrT-)K$O*OPSS%JNAy$apdKdwdR;)c;cd1k{UO<_m;l@zH7e^ zu1%PqTe{*F>lJj@8}jsw{TF@tn~S`HWB&G{dc_o?n@^`iE+(6IkPvg0$Y-e zJ3ewB`Y!8mlmCc>Wv|~(4T*Yhtryn8F- zi32yIzdp5jUmEsm@8jnl{EX)e@q6kKYpppti$%})RB`LL=CmKVKlyUoi}8`zJ}K|s z9sS=uojUbTpWpL|T6V$G>`UZUxFePtbG!i-b0Ct>*vHRo>-+b<>Un$L$1ipiPCiQR z350X5z3x=4{p5%%7r%AVDZO&_h`H7rpIpKv)e|Y<~zq;Y&&9vp` zJUV#e-dBI|ccE26zXiR;QI8nMnBxXZ=G6F^bzZo9=<#{t&t`4$KXvwP;gMVZHjtV- z>yZ^Z|4I9$)BNQo^@yR?9GAu9IH6$TC29_UfipZdfrcYJ)|5r>`TJg0o`gC`t&|KNzG z+J3aC80Ek4#lN=MzrJAU-S0egzU%R|-!J{-p}&PTpTFc|rt`zQ+!F!4*2$kqr==k>~zlWd!t+VX(EL-|);{Ma#} zRqr`Je&o&j%_pnRjNG;Oq)V@SvQIA=98px;k5MqyE6?)^S1DE)K1_VJW5;9G70-Nq#>g$7 zu0Hzg{QuU%o8!x)T7ru z&m2^=(ON$YubVlva``1Izg+X}g(Eyu?oc$8EmguAy?kiWnbS&xNtX|eyTE(& z@}Y48Ay=`5;`vb4H-*JRne3<~dzFaxa5WK!%Vx02=Hsp@)C$QDB;#dgf5fHY7M(7h zgeehagIu2=nRmwAG7~3TRC+n?Qi#9DtBLH{Slm?P^4ZK%)~GEzv~G)w{HQ3$NlJ@y zj@HHsM$}$oAry|fnJ%@PqaZ)&Q%ZSEzCoNR*^KD~JGIBlA8SNvy%1}Oczvf1oPTx>w|DPPn4HUBaKRA@h zTN(KUdd*O-EL9%64shCrGJ9ZvUvDTg(#DjmEZ_hIz$EjfvPK4q^>0H#8`5)a&e+-~<@lNL4(U7NR+*NLH|G{y` z;&7Su#$B-_a`k8VOrP63ph+9}1zE^wlnlgkUbD%-Em(B&-$+kKtX5ehpxhqCQEj|DO}K(bfUtPZlRT~ljH8p$Oxx0?k*q~#5Zs)<_2ZQX&|{N{0$Jc9N*1Yz{;x(-j;yj#sgH5;)GIEL6<_G|N`(z* zJLL8GUAbtb`ckD#)sF=?$WlzlLMP*{n2-D)IhFsF`cF0e26Maqw{ahnNr$_B;i;_O z50UjqhFsB*s}lTZ+pfGT>#7v9;XsVlcy_>Z!ww`9A?bm)& z`*Cf5Ya43)xOKR7-e;ICHtzJ=RJ_d_>up?HyS&w#eVdB6xx0C* zXX{6AbtmVh;_c8{Z*^_$@>Xx|ZYtgm+09!$Tf4l~o7$U-w}W@{R?k+Aw{d6wrsD0O zvEIhDwaZ(*iNVRe4X!`1oyB5cJ!8M&>|u_X8JRa`u48ch0aW`jKki|U`Sp11_ov#A z`Ed_(%tYUK?dMSK$Nad5IcB17y!QK1?Z^DMhdE}VZ@l)isrF-j+`}9*(KlZEeW~_i ze%!+xGtoC*`+cbPV}9H-mCphP*Y8cWA2Tc1Gj^hHJpZ$*?Z^JOhcRYmZftw?`n}o> zmGke1@|1OF1;;r<$C?{lKa&b)jPD-Cm@{;|_T5zbF+c8Mj5$NcwqLP+MmxC)64uke ztX@w86LpiXb6T;!s~SNARvX!~X(|zPRwHO&p0KBZslTa2KvW}Wz-mtebA(fg097Ms zz-mtelZjJ_0Ix>SfYs^U24}^3tQtWBbD7h-4b6)69n}aL*@vbW;jlBNGCd-qb2urF zTR7k<_&mA+w?UwblQm2;WR*{g)h9L>*^|GES+Sm0ji3Rm(>t@dVtsoxf(GVGr*}eT z#rn2t1P#m(P4AoN73*885i~G+GQIEQR;+KSM$o{UzDC}yO@1a;^#8F<=QMR+(%m!T z_8Ii9Uv__I2&% z_Wj!a&~|FulGb-yZ)gp*&TM(2Q{Ad>c!F-lsBYb~r`?KD-KuY354#ni zx>eus1l|wWtsBYCaJVCckqPlh6 zo_1>~)vfvl_OM$gQr)U=c!F-7Ky~ZdJ?)m0>ee-N4eVjJ98|Ziu4}k%w^HlumBvdA z=&HJnsi?*;M$I1mLZz~fa`IOU3)k7Gfb09L8}Qb1t&hyteuXh@F#j#?YiHhLL>5RaUFfg5ETR7m8D(K!=(jO^F z+2N?k#^oh6eI-REWzaJC4kKCIAhFI!MsR$W4_z_k2*%E%Hu9n*vCcqwy!_ws*uX1{ zz&btYv7hYYWmEPz>WpmQRYho>j`YMLJzY9wPorn78+p~Sl31rDogKNQ zxMVsb_>we!^qfWZ32MxxikM z%MD>6;Yzl8|?ye&>50zjLH>e&_q65(%STtOqH+;3d3C1ukct zYC0F_K2o04)E<)x`7iTT$jPwwAsr%!T=sBb1ZTMH68j$ZVQyaHzxrBATwd^E|GGnJ1ua96>wpY4Q|Mwnju>gn^Z z7Djf@&GxlR+jC^w(R6YI?7WlpIk4Hvelf5JQSu$r=U*<2YvX2X+G60HY3|A$VCNo_ z-rZuSjx4W47#@5$dOrW$zB~ElWZ!ItK;VPhK*+7I>rTC0%u7Bt(odB>?Vks)PBFaU zt8MNYH|4NP;1^Fz47-F3yX4e~>;+YwmNKjF1!aBLwM$#`vbJVrokD(hf1mpEw5+|0 z>-Fe8HydG7YInSV441AYKBVrhvQlogbYD_S1CJb(s1iOrag%nshTZe05w7R>N9}$3 zWA4%GH@`E&b!9TfZ>cH2wmcaWRC-AN>bI|=Y;GHgTTQ{Tj-TB#t@$TPzMTGaR1#XG2 z!a3e!6TpS-I$i44Ulifoc*VjvUvyK7aNf1lEpf~}$9dPHcOla0qPNnHkgmRcVWbb; z6eFZ}-2Ii&%(?i-<{atlYxd&L&1ic4+?T%;J-_l@3uAo2O(C3X=NMTtX)f_iJO_Ekq#nxFgQd!y%{d}U$0`!_2Q-n%BvCBDn& zc#ln*7q)9OX@2grlj!;6vlqn~egDTV{lKMb@48mL`ZrFU0r>pht-bW# z#_nf!e|q=TyM^6{cD}Ilp`9Pu(RaReXK(utx8Jj^Y~Q=}@vT>H9c(_i`SwkAFrANr8KyvlJq zzj6IS63yGMyeNplD9}@ygnHsFhd#_rjeUGMV@PsX3om_~GM1eh``9wZ;xLjJ@w!Bv zGDb~}eRLUPD2!kXRnD8IjAf?AK63p+V$VBm2877sB*_t{t4U6ceRvsT2#laax?|F( zkP%a3A6mv33?qn2kEzTlW9g}}4=!m89`wv!@079B)Yu1>F$Td1WYuWvB+U|ih)<3E z`ZC7OBe3KTzdbaj?Y#f`g_NkvuuSrYFQ1zIwFS%qP1ln$$#Bor5Ibz6tDqIEQPC1@ ztJNTz>5M|-wGN`@fQ-l=zHDmtKP+H&3F=Dz@Sjc1{_6D$d0dwvuH+A2IyL(%3!q(s zwvs>mwyD`)UI6V9q?P>Pw@%Ie`vuT0L0QQkCZ}e9X#uoL5LWVsiK*H5Er50jx=Q{K zo0|Q13!q(stdc*BPtE?~0%(_@s^kyRso7sx0PPY)mEU%Vgl6HLpTBrRY5z1$C4C5o z+T701Euein+CC(yM1aw-+Z&vkIH9Sr_by@V+?`tb5S$wO*=3BKx1mZO0#jq}S;p9T z`=9jT!PMBlUDDW@o0at8Kb;!;nPrTfx50ShFm`zrgm<1e9sO{|O^6$Z-!hdxBcSq( zYdkj&Z%n1n2q!$_($0;;e=?OmBTDa#D>^q0Uow?GBlzu%3pqCqzj-QsMoifm*Kckd zzIZBqMyS>qmu+qwzGy0aMg-IuS8Hw@e$!O?jKHEZF4EjM+@DIH5wCN`wV4}-k4&Y{ z2+%oWAN9uJ!&B)qqH)gH1if*1eJXuMh|L*6uHSn2P$+$7OwH|gM&#VBW&ZGm;rL!C zie#BRP5$r&Q?m<%hU_m9@xdQHe`R4v4k3W21YIcFhjb(1C z@Q3$L%`On8u}n+`fB3wq*#!bJmf7y&5AU0rT_CbznTsO);d7^E7YL9zG}L7K}t^{4tH8 zYla4TL(@D?^b)Z?T|o45-j?QBz@H+8V;ls*bNQ#r0IxA|0d5bdT9_UBhEZ3t)h5!$ zn^f8yIl&Er7&2|iwE3sb`-b^snIW8mG&c%!bq7_g+}0#NenXiMNkdP`oK-&HN0}bU zDxiQO9Rk$FEOfvCL@u8td9hQk3@ljh3vnYOW$fxOon%t&Mfj(tRSyYa{)KV#+ak~V zmyMNW7SpHC^Ae{$FCt2t4vsq_?@tYnCX*fYF1Hf{Li~I;;@o z=8bkW>krB1K~T^1Ge!>-8ci7DaX{Ur`r zr|a{qf{l=T=%J-o~n;iwu*6F)C;j5w|4 zz-aemZ|H`kk`W!<$q^U*F3i)8qG9|G7!64PPqz;s;|2t4ATnlZ5{rUQL>9TZ(r-uF# zNB7e)amov)!3I&I9A7*aa>HY@(mAhxClB+ZVvajj(^i9M;7wl>twH+;oTvHTUHqe1 zsCs%yJZq+kq8y@vzM7L`@cDG3L6AxViXNvjGS?=vRyvWX$PnwBV2do2TR!d5le!}m z3X>T_>bYJv1#;ZUggag z(TNc_h9NEGF;(40%2l{6lS;Pwu^I&)LIv|8DmqyN~bk zyASL>O>N%l~N^r5IEiVJm>4> z-~0J z0upr0I*JLL+;rGkrl#Fj&lOuzNN(?PMrzn)ob05iGM#Q2=wTEuFx9F&mS9!#GYP?; z=jXcolDVShODP)};loZkaED;MgT`bt2kKhURua81X)wXpbPKT2?ySVs`dm?U8%DCL z(PED9OdPY!hLnYyjntqzEPLgyT6aj;Y-il&JZI|a$LESl5VWh52bF8--bAQlRvV~H zd?Ty1Z~$v~1+m#iwX{Bni}N}@en*ik$R6xTmeI(z%v80dAyqY@V&fJ|$RO;V? z8rFEO_>*(RX4RCiyr?I!OsSV3a;BGH3i0wpPU5iA0{WvEj#H`{m*-=3^Ka*h4N*j- zq6I*ZpJQRJsA}*w*L_F(0d!%eNKleyiY*DF$dz&8~q$L$;79 zr!pSgf>Qp3t3%1aNK<|Nc6Z!v9AQj(EYc4Wv5Hr7Mn3D;g$f%KVvQ=5(o!bt*{$v* z#}$=X9k>4TyhK4F^gKosn-rL`?1ADb6%(0) zoNB1(D1f_4$x+xWI}V0+)%H8wzy?V=$dYN76y|kYOJL#Dv9dp@S#@iWZ|BTRyPtL| z<4OanFfdTh-%=Lw6~ zZf!i94wD%IpDcAC!E2 z-l#k9T#=%Zaj@i4wHAdalHY8k!>kq{6>F-HNHkOmY0w;ShwpDU*5SlUvGSvAkrX{sC#a8aVi zh|MQ4%2$(h-7EKMl#;Rrv!S~4(Ya#MrUj#HbX+ObE0400V|uP&HZT{W#2h#Y&`wht z*JERM4t0~9D`F+d%?ftDn#Kx~RJW1Rnw0?yBV-jF6jQ?_k+lq?S?RRrV|?q4bH#Y0 zBl@F^DEVX$VJs$&rtKjN>2eWm2|go@vqL045=Pj(jvIe=TQLP;=~S!BiV)vHbz4hl zl#uIq9+K}cvc;+GLOa&e+s*mXvHp8=MHDJQ9i48$87r(`Q%_aG-btVxCo2RE9~`mhV9zHjb%Jc`4o4}XHdH1>(DduPUF@TE+p*-9I_!d0eBP>?S8gkk zSSD@x$zf$k#|4UPd82r>TgaCBJcTisFKcRE!D>=!o^^ZYcV~*h#Hewjs*9^s3&^KO z8b&05{y5RUB-(=whwlj8(f`Bl6jF_eAXc)!(`LJBx~b>guN! z75(k2zrCpFC$D~TQPJPJ`df>N{^r%+TvYTES3fZq4Jwt|Fs+znCq=Z!`CRoxW34=~ z^2DN|?_PQLT+~gK)V5b07{yjDU+wDl39D;wUwiwaqCd6vQ;UkeZS8G~ivHxz@`HbJm5Tzp?Tgivsxg%EuQK{n*OK78U*I%10L! z{m9Bk78U*Q%7+&f{m{yX78U*A$_MA7^|Ctlf_`>1^rg&5FH}xA%=RPOk1Q(s@b<%t zieBHozNqL!+Yc=&`oirOE-Lzh?H4R6`uy$ZFDm-r_U#G&!ghUN`+=R+&tEEsWPYo^ zcKP{hf4cTdYj0Sqt{kk(>$#BE|G!@Txyz4UF0RO1?Jaa`ZS!|F-@4h`eCg)S#%DI( zvEi+Jcq6s(to1)!fA{+2+9$95*tO2J#OmYMHm`pA>Q99+1TVjO<;w40`M{+wUwPA& z@fGsQw}g8JpWFNR-p}sc+@ts%f zykz^YR$mdu6})*{UU~EO!Pb|zKCyLk?U4)KCc!mrbsw&~a=GXVRFR{Hd9+u@O}DR* zBL*#H#|9PyWv(xjcl>*3W(Vn zlI6CN%BIFKZ8GjRO7+3el!$D);`5~e8}AQtyMHr7<>YI{f?epPIG3(U*-k&3$H0lg zx;csr6HwPEjY_3-wlc8yzIj$gO)BI-pdaI)iU(?wvJ#KSD3r9I_84u#Dq8Skk|*eJ z4A}kXtPDC3ODR(*+MDS$7=j;LM*r|BK7`pOc_q{U>ro&_YtpAu#e5Z$T%;$v4AeHW+3xOSreP@^lS}k#L^BM9Wz7;6l!v8Stym}R zUe@$RSj(#P91m&cb}O@b3?08>v@L>->sS^fTeg73`j8cJ`T}K{EoZm{*AfUM1>n2c zsRkI3Y&GUmQr7Gb^CCg?$)M8IaJCGTu~J7InozryXJRT*-F|&Ih@cAw(uqIM>fslg z{ZY6#JPcrnD+&{<=JHSppd_JC3L2{`M|6mNM~5Dw<(uIT}6qFdhv$N z8nt9Up@V(W91X(oSs~5xiJo}r7iJnzy;IDHl!u`4F4qZc+^@-TwH`~ByLJ-EFg>h` zS?Ov8sBW7x4ItTabYHZAns1fuoEL~*M$xlgE9nDdOF%5LFG{sq(*@SHryAg-)5(Ci zqK-!FBr%S$mYT#S0XSxLtC-D~$FZR;>UaU_DXTM7V4$V*_z>inK2{l(M<@cZCO^bw zzNw`WA@M^KOjSpM2Bnb8vrz(3ZgJ4bBNPJlJGfo5Fae6oT_IHS7f_yZ8f?N|`MFum$p9;On&V2{ za!2Dc88hv2WZ^Ostq)6AyBu1;dz{?d7Evpt8biX$T4o5Ynv4dR_$1# zLl=gUQSr1QkxO+*vhD|3AH(Ya87EgZXBv%|Tdx>eKf@q{GSx`*Fl|)nKc#R@V2gA~78MjODB%k+uqJj|>htgd9a$=^zXDK?7X}2dHkm#^A7K>-Q zbgGvHO;YE>d`@(U&(>ls7wc}#%iv;A$KR?xxHq*&*T}P$<-jFUrJXnHtfbdfq2BfzrPFDTW-Bp<`eMOAEo_@(cjX%ys%-9>%7pUl_{61qph!1HeC z7_A0@`}szt*8_%DKWTD1zY45rEBg>L>?bIkX{Osf)yd?NEwrWUd4=uPp>df)^0-Y0 z!@59dzzb(Z&_=DrnXyLEjCBFlby26+vY`pnDRg_a7F}ywK&wS+SX$ipf94tlN~Jq( zCFZ4rY?5eLJnWgZOqLrGfF7%MdtS}O!^Lh$Zr?Z4$XS?YT3Hdz2~elmA-grhQYvF2 zDU^*-ZjcutmTGF@LN{J}&Qt^T*>QzL+g?sV^Na!whKypEtyHz*J9tjVprXyBI0y%BU43il*H}Mnf|$lLm(Um;$5| z#YzT2(;0T7H`8bfK%CS&Ihu=tChVF@(XkPTC)uE4C))8M;HO)K?zj}|uFT)AT7Ig- zbUSiJwLP_Iv%S&Ch$EJ54n@i$6+aNnVOjPSRJr_LW@RX~N=f7M6=sCUa2ICe3C>n3 z6CyWMQ+c^-CX|Bj1aSF1 zag4Or|8k}w4NF3JPMB?DF((<2wk8ka3C~H3vXTw0dwMk%m!UG-2i89~(-1u0OOy=} zRto73oC?hy6EocolCrAJn3#&iczPJfPFvBn+;(=lZXV~$ zMi2x*cUq@^4qZ z7{j^F*lnr#S}SO9cuKU?6v7QV_UdzIrcUww?N~EYn5{GE9L42Jt)3NIBjCRVJ2~WqYeMGj)o(c4ywLXi3Hyb*TG|xYSO1eRnv@5Uf^M;+kB{WyLjNRz|E+>6K+A6YtZ58lIW3Xkmh4j5o*x z=|WcS#Mr7ZmO`?#!N!wQ4QL!R@tzl_N~2ad!}oCma(eZe;qaLnP!NYHiH*6L(FCS} zjZe=tl2XBhk(}8xeG8l{qRj8r+J)~c)X7j`#R6d4GLl_6;qzAcoBg_M+lo5~m$AWVI^MZ13Ur_F+ zW^xxt11u=_?gi!k%}nmXc!4>-3+3LnpuL}5Q0}b@%DrVlxu2NHT^Jqkz=Cr3FDUoC z1?BErQ0}=4%H6x5+;bL`yJtbUZ=B0r6aw(W3(Eb_f^u(KQ0@m8l>31N69%avxn#?jsAzeRx5+4=pJ7!3E_$Fq6CR#Ch)L z7s}23{6e|8pI<09_wx(o=6-&m+}zJEl>3GS+vV8{$~|jAxoZo`U7hm(UHb5)-5=U| z-sLxi|M-Xa^9JhXw?~{+k3X=o3}@%l##xnw&*BVOBK1tfQ?U%&)>M9pgfVBc(Jhff z_AJhj)44w)SjXu@{s(byodG1|)`%GDH1#g>kcjQ-G8hV`2-k6ihU z&s|wY_33kE=pUpjFOiPxAD}CTmk!+%t{ic5eH~pnywwheC7em4RxJt=@9GBwFD(_? z{d}Bpdd)P_y+OEPs7sQGi(}1Z3i*nMKV?@=i#G&1XyNuyCp(IMkgWzu8|na1Azdmt z2Y9s`o)Qw><^&s6kfBsRK;;&f#={Hn3cwp7?VMoOi&RJEicEzYXHgoMOj047Xlfbu zqRAy}&xn&qZfkcFpT(o9N6cK_&6h=v_}Fk|8D{CH&k@0YkdC;_qW{XUGmfaPqJ{IH z9_4%bds<8H0Ei}UXV^528N1bO20!HM>v!R)Rm9YGrr(JNZZBD&sa&r}nF3a$DjnES zq4EJxX-kRlb~T}j4%g#^IQf+QF3025l02yn3RTt>vz0-ZMpFq{DO{QW2UOB&nw$kx zjf!rwyx;*+X-w3HGT9kpPQt-JM^D?jl^poxzLQrD(r%Y-wHM@HjjA5{-S>5G{v7l-|~3}^G%@21PX+@A2G<4GLJ!4e2KV zU^+!dx(!upg?IM7&=Vt{xWh2{s)y&v;pU{vQ;(Vd>aKcE&4rrh=DrIphXo!A8x z3{MOHH*J91(@nGpe;t>3Fqw=vcF&w{Tu{s_E~XlE9q0(jdag3I^1uxtU&&_*hMO0Q zMY5Sk7}I|047kl3dAxW4_4SNB!pt6jBT*Pm;z*EEG9kTgrCzcm(}a7~UK$0uoST;k zHZN7NRF9SldA!xM=|nC&=+*Q#MO5=QSS{ThF2;ZxkE$N=|LuPE(tRs?mv+BnWiLFn z=eIw#&0O`iKDI?{zJK$j8}Hjd*59-KEtlVUd4J_iYj0h9aP>!5@45Est6zG$w+Z&t zOW*r++g#S(qpKU5#)YX^o2WeOwySQfI2b6l9!xNa&e454-5g0B+EV$VI!uROQgNHy zCn${SITH>sb9paZHA(`F4u{<$Tn|taAHnfTlbnQ(ETQ)Ge#hu=sTMmPq)U9QoiAC( z!h)kO%-si%Fi3@BQ5k}MY|yM!Sa4*M^-`=i1YJRn$!y!JA(=qV#DxN|53zN)tWc)b z!4Vc3O=9hAdED;ic_Nn#y)qOF%Ex|j3$_R);n#V#x^1U{! zhs+;LUZ(W{Yn6M+q1r?J>UHg+bBN2YKf=(+cL$|XDqay)Bd%mi86hFZ+C?TngrweM ztwF2X#d!-!A^Vn<%4x%v0Fhzrjasq-$ke!y2)%__C}+v3kpkgl6#^#w`1;86x~^9V z32|tl#+oDM@z*)bJfIF)NS26UR)VRt{q zRhj}1@Ic*$;1<~GR^oMC=(WShQn1$0+**EEF(}&lDKCs53}N3mQ^-w6u!A<=w%)Gxm@{C2u1jPk>`V zAB8;Hz1*mT#~M_JiK`H(R3ye6<2ZX=yYTGo)khd=LkuXk1}Rtax(NspbEcTi3!G(5 zhA?V_Hq}m-bA;qN)qR1^LP3Xh!$?iItqumIO3LcPA#FF*E|j#8!UzQhG25>7+T8VY zrB4?66I`3fQjh9-P4YxLZ2#sFhN_Q_njN{4Fxk3Zk1>GDkWOaQ$P<$60jz*DWBr;J zgDG-9U#XyD5^fafCeBMXlqW#jXxnaoQXgwn0?O-gdSdhf5A9xes+A1h8;+|()@&NN z9#KAlVG}xrK>*<-h;k6+j5V4U#4KKF`-NuT$rcKH47QW_poXzdW3V5mQG$&n$4NQi zw3M_pA@HnW7mJ88EI|5xqpmq>|dP<>Lu$DYy?+xz`4+2~o_c-=LalvLB}m;&Um(tv4- zLNl(52~8TAh*`Gr@%2ne;ljwwg4P_VnUPiOxhF9k8hXBPbG?!` zp47CU3WTBX87f}Edlo_F_JfjNC6ipCqNkHCAow*JRT_5(DV)qpxy+yw2_S1eYv_AB;5!8z9XZ-9f~Rkr=k2@Pn?P61V`$UY~{f$vkjf$n~vB zF4OF0DXx@rg7Hauht>OzFyJE9)yzE9W0G~MVL(u>6W5G*zKzM1i6bY4gj*}{fL|2% z%lV0cb5vF(Y|(HyrzB&2x6@^+!#Hd7atf2m7E0Aj4KCW(sj)x}-DW^qLD#afO7>*N z-IdQC!vGNDa+2T!MQhdj?6}cJ#*+ro^x#~ECPrzLN>wC-PodR)Gn^_CQZOWFvKwX; zP-Re1N1_h{N(xOX4$ib)AEYp`G~%vzi&mNs3}ce*x57PT-8?x@Zfi#vFkx7Y*9$-* z%#}hhD6RV?y*K7WfWaFsYBhsu8xLtQVg=hJkc3&j5>b8{-{12ZWg|!)uki@8RW1c#$Dfj*p$7E@ZTB zhye>p#PWbWuz(Bw(|Gc0HI>6j|Yl zS-w*Z!+66RNJPyJ`haCigYfQS#f$5M>sg`YQp$)-Qml&sQysWs5*pNpuRnyP6vlV>5n_^sytndg&j_TD$H5P=_ zuYrpxeRGiR`rh6S?qa#VBQFt3!!rTWSVmxEPqW5G)?flY>#XNg>pw ztjBQ^lU2yli8*TX3(@)i1DAf{(zVvrFJ1lqFv9;AuP}Q*zjy!cWakfd;@j^FWAt>;4WR--db5-f92NY z+X+cgI1a}#b3`#G-%#iKU3a#!2iL{$GF`52_!&p;lN!M>(usz+ykb56M~~b)eOl-2 z*P-YMrAHsUwI88_kI=0hp$qZFb-ihx{H8l&r|x2WYZ0;+{k zCz^ZSaI-XD7XyoM-8aQ`pSdWmSKYcEHW+-T!N{$R{nK~H=0N5x)a$?ZafZu?ye zqkHhy3nFx9`XrQq0 zrq9O~#`W;62hQ~h2!&CsMz-9OZ@%YyMStQ-6Wq|!ZK~3A@j<5>-71yh0#5D>v`vU1q`ET zrAc>A?na;G6&K=*yyD3(+&6u`abbM--n#F6uYh0_$>F$s@(b&3uecCdN!z=ow-th2^a-Swxe~9 zsYO2W)qL^-WK*B~&j%5*&wl@TZ3LYtFZ9WKB9v#YRA4xz0X?NjT)a~CCl{cb`s5#6 zjnMtUn-@m+;H__r(4Dzbh3Jsvu*S3JwBcPV)rI(`KKYsd6yf__W?_8y-ui|J-AL>h}Wf)FFQ+)xT#Nm`a2_JpZ@&9$nLrI?DKpQL}3JDsPN|aiNP=jcE=|#=+X$$ zr=s`mC;ww%QC#g?*TMz^UkrvHE8F46%FDOb_K&XIj5q!Bw5vXktS(cj_N^;ly`+dk;j5Gs{dro_um56H(g)sqW=WA$`_}GPEosWKaDGYCKTk{g zWil%1SGLY7X)4vdwG);E-)TKEYQ0ko33nHDYQmcq^^0$diu%QO-zh2v9shKj&S2eJ z+owyrtFs~!g}X~T1!h{>d!L9(d+#qDEA99z^K8Dn_tsWe+;LypxbxNrguWCHH#muN ze0~0%!B;YhED))_n=X^ta@HRm8x_&?I-R|r_=^b5&z!gNBFCrlNNF1Xzb38hy>9C( z8{e}geVsq(pE3VT3;ZK#f!9BBlh}m8#$zwp-Um-j9oLUL;&hHUGh?)OGC^%Dx@z)O zQkupS3GhzP)SZyASG*z&$vahI7#J7kl-6YkjfZhp5052RFNq~9PGh9kB^0Hmz*0qs zk3dLI6f~@l5XBn>2(CB7I{~kZf>)>ZXBl=w`={y{1>T)Vj{Nn(aXgpoY_=Teh7;8( z`m&k^gPs}U)ru48c+sYkR3{HBVOkhJ50-)OyU?8Uy#u6YxD!EeCiUhGb{0_@Ch(qS z^nUo*WPjHZ9xWPCwNHd2&On_EL_DiQ=6xGsKe~J}9UqbQ?Xb8gXzi|RD;ME8_d3bU z^zrPp(-hD{;sB+GadpsUle}1F!}G#0aH5b)L(Ng8*mKl)6;#O~poUc9h4!6io$DhCEHJ85EznPBw@G+<>^gS>nNG$qOS5&BCR+7uRwsB}ENi4bR$FzyS|63tsR7I@UJ=Czca#K? zK6F-Au*Zgr)GUP3=Y3wJjauXJGrrs z#`DAb!$MWjG75#qz`j{dwc0|w-fmf3L!zVtSa3n4$B`2(4KnyN68psHe|VW7^k^h3 zeT{M1t4qfnZJEiQt&6D0P@tb=n%+SZSK;w^G;s1#qJ!A+QD=f3l(`!Ykd}dSCwuS| zk6cvc<36>}mB$l4cC0u#Xom(vT+Q5xop}EL#sRbM5)83xJk37=%~Bp3-POw)jN%2VxKaCc|q5Py~)sO+#f&4v`DBr1w&WYQ;sI%{N{kb*j7GM~*C;iWp+8@k%;(#O%;+Z4$0 zH!QhPRB=11TQa4VazPaPb%UOiB|M+CB>}6}F|WeG?KGFq(Fd6-F_E;qr{L}w&`J~{ zxCfJQ{UD#5;HoyP+FUkKWXge2ZKXp_KZIE9nqO4)F#bRK{(t1sJ1$*&U6}XpHCJDD z!6W%6-(T)^eBhjkO{*X>^nIx0Jh-}=q zD`CKEYfwvr`6_I3ivW1u6hJGfdKv&FI;|W`Q%I}>W3cMAot7tvIm@s0Y_8Gvc{7%L z+P&bur2#z6Uhv$d0X)rKaPJhr68-vgd%<&-2Jkd{!97y|OEl}#Jr#fB(g2?3LHG?* z087kn`%iNhJbMaYi4|`DX>Ea5Jn^i{Fvp#d*5dY|27)DEC_%8@a^=J{e%+R+a*)Ei z1;QmY1Mk;QPV}#M;@XAQ<1mDddSvP33HBn^uU=>!gCPuWJ1o02_RbSmE^}ea%p-ab zEE#^fcSi%^WD>X|iLkJ^9`OB>_e(CWO6Q5aWea=i?bdl>ciF{(_PK@ zl>2P^DHQgU`)q63!oHS1+q}E5^Lpg2W_-#u+gP@+ucc<|rwTh0_fgHBdSUq|E??$g z@Q$f?I2sXx(dE>M`C%VCbs&9u|2qG~+M?D@ZP1>!_R=R-FT)YGQzf5qAaj=b%DL(b zm40cae(LOJp{@MazWa64g=~qP*Qb|@=C$vB?a~02pu)I#m+H0ee$CPVme`BB7{Jcn zeV6i=K6+{EUvFt!@b33q`?1~9<tgw=1_{PC@o)%xDg?7V1?zWTDw z&F#OuGP;)7{=k)YZd}=Z)9Syx_Q}it_UhZ#zj5UUuYAM$yH@`1%lXw`-u}+**k*U* zr?x(R`88|Y*59oC^R@ruT4$rb{`;4oxA1ntH1R>8X`*Pi@>E3}_C_&B6y2mc2s7`b zQ7}zIT(ib!)HvcAfxWvi)5r^XGE=WJZQbm*eL9z9z*2n(glX6CQl}{kjl9;4r8%oO z*!h#0hD-^$M!hryHKq>`VjWJ(G=g;~y_n!tQz_)MooG}9o5mV@-i&kU9L^@{Vv6wE5>-Aj1?-=1{f0oFM8+vtj zmepm#;YF^{Dm1|nQ&BiKUTic|4sN0bJxEqvzs+X-db?1B=<(W5%*xQ=Fr|{o4_Y~X zT#j*~JV@y^g+O6vVpU>g2}4;wr-Q@*U;p5=@y)^B*$#(sY)CyCD4wO6@ms2SZKT2aVnjJ3vA8G znD9<|$de7hQ0v+HL@v7nQK=FFIc(5wNhk&2z%6=g%{SYZ^%<&KA(J7oB;T^Eb{Do~ zgdkcVmaxKHB;~RXHrcEo;asac=x)vOBnE_3$|Pk`4>NiJwQ`MN4Z*fDG+4oF!qB%o zEhD8nqg2<=t+i(LP=cUcr97xyOZO&19kbd%W#SuIt%Uq&f6>8F!yhAJRI$E>56z{yRA-TL^f9topAc2rMfWUlK31Yhan zjS*fOmdW8*nZ!kHLZ`!AfQ;E*d-tpir~JNFlCF z!V66$Ny=^g(M)5MB2$GN<)StX_}H+TsHfyu)^CTDNS(}NqVz_kUJJ_gncV6uv(khc z$hq+Lcun+DA|wOcqy;vDQp@1`mj_}VYc%1{EbAbYSUSw!- zhE&yrij7+=A)EY23q8`Z8rFDgmLX~qZ*)X|lo2JL>>-TB#L=`pgdtrnhSxWJMjB_s z+)g85gyHKSo|fTHOkL5KaJN+96JteYC66z3Gx=nge$LXFU|eL=I95)09(nnlGgL{N z7L2mdaivtRJj#Zn(Q^f}fw>SR=D;xfR;MYA>#>mEVqkJh`h;D$B62N4r-i=x}WWDMq2q4QbnL-UzdS&*2whu}^&Tf8krZMo7 zX~O_{ma!qWoCcimW_gLvq$L*%8sW8QNAi6hNW=(W_d92(g0APO9f6efe5z_>1j=oD zDF!j1tdY}HjHlX#c&*a*MQ(e2rqSdWbX1V5Nrq?z`9gS9G-%|&WTwF|dbrz$1Ee1G z{a9g;+xqehm75Nlu9R-TC|N2qECa$yjLi@NsA_d08PeFLO8Ovhx^Z&tOEZl|-ES5A zF~tB4s@XMAXvh{a(aW(ptK!*VYO6lk26?8YhL`YoQLpKp!dP>f@lJuA z6f~OMoh51tl%6Lg1QLV{gHaVV$5%%gfK??l$8|)#m1=tc*sH+?#jd|#Ru3VODafgY zijD%ftCSpt&9dWQXjg5&!wqbZq(e%Qv`Y%e>VKG_3KI(_;@Z zLoQD=D4=3N<#P>-q^R-g&(1W+5#&%{yVtipo-EoOgBj%HtlF-o2QhGzaiOeKGfEYl z1XkZQ)99%6WD*5E&BCw_1WOdbR_TCJ^NLoi)U^RO$QUwM73{&r%cdIsaEJpI(lye9 zme>q4)a6*}Kx|TFL}@7=O!5iZ3G^CO%^{obpJ@zw-9%m}4{SdUmzi*~86NJ~8qADT+WS1LxK2_ZNP7`7jf8L6n%+*rFyYx+>! z__b*nlV&xfy2*=r63di&2_k2D38oM)Pvj&HD=nZuis3k=s&N_En3XZnd?{rkBYfCN z2ksE8chHz@=0II5+Df9Av-1RB(=EV8yDNV_%@Q@KS#@iWZ|BTRyPtL|<4Oa$1M0eKkol@PkT9#d zG9QQ&qsEP@F0N87AfFy-7?A+_<3y{EvXOiew zuzD{ejLNlGMb_G7o=&!%M6+Bh2OUOW1}n2nRDtb?;{Z-bYz?1uT?rC>39Q!W-WVEI z!VY&j9kLt{WZq;iy=hj4DP%>GuhD@Ci48xVvNX-e!4uXmk|FH`MdXLBD+MW$S5~l@ zhSsAS7L;sAaW_LuD8Asr;so|=LQNt$%mmv^UhKItW(-!^GmU~$QBqaP4e9iNiag@e zHEbBZyTzC~ruO>01r3M^5X<)^Zgpka9RVdZ%}J1J$w9(sIDLf7$5gjUDxJWsD@6^` zSODn`c#|AgX9|e~Uv5I_b}Lqa+I}@Bn+n~53P`Ty;aP;UvKd{08w1`WXZcfu1ja}; z!DH&aR%&YX43|_L-RWuBI);rx1`D~>Le;#UWV$QHv=)9aVdK=eDwM`0j%EzR42QKv zF;=4$6SMtf&Q3NO9S16s+@&9%X^fj`QYuzKsaK>EB2qy!9>bJdwvvaPp^@@hyf2I< zKpFxr&C+iMJW(3g?Rc`Js-+kwSlxWS6Pq|vGe;IBB;Fjv%~+;qWVr6mEH!43ttUvY zHcplrE~z{HFdMdL`$S5vbXNIcgl-;3Gh!v~6t|W)~9;D$Jx|a{%+^_*tg9}o#z&09gtGfBDnZ__R zD5xHr8)14OA7<4wOM^zYmC*t}oel_N*shi8fcq6Vt-~9cgu^wx`i?B+j2( zKA+|F?e~4&=Y8jWp0_>4m@JZ*snRLfnz^u$_hdBh9Mm8p-9!U#byDU!W=uE0b7+g8 zIi{M7^1%Pa-T;|VGcAX!mY@_L0H>|9NsTR3TFp>Y?~a4`(x`(2=~7X;SZs$ZxWm>8 z^Y)0&7O=`Sc-2E$js6~92H8B*;ieD82uj}!;4QNaxHT2i;ZokEc*mfPZHqUUH}Oc1 z1X=R9t{_m=U?Hrl7%($zb7tJWaxl#qt7MSUd}>f5P&d(F<7wn<&a{xM1=2ZhO5dc& zPCR4q*Q-e!ZQ}+9$WGo=Tr;TA5)#&^r)&;e<8|Pfia?@(26&|I3eGUcF8Aa{1?%A20dD{r_v=i%h#t(I)j} zz$SItj&k}MO``3#0hktz*-TH<7A;mLOz_UV&Ga)b6KPJ}7=pG(?ZNZmc67_gwfw9t zQH?q3X5JRagOh`S*ZBoM3u2Svc)PnpAr2F;2OrR!x(A$~xsb(m-^N_Bose%}!gLvO zS%GOVeUqFsnf?Gf`BUitPTNb0<1)*}Z**LowFperg(wx`LQylYR;c6~%w#QP0>5{FyZzq4+xTzoH`2b_9sry!OF3hrQI%QZpG>Q^~GS9RlCh)QWguNo_ zgw5#Oa{H1IHo+OqT|+5KBT1dDo(ti;BMcj?gDB4Qejdp(`5 z;3kb++~Ml!y$IJ3+(?Tc5FEwx9VApYZrM00UGh76F5cL3Hl|^o}dv4hTMMsw}-Ld4%x*4 z2l;Z>$C!Lw+{in7C{EiVNTe3DS!Y-&9zA-Cde^Qb;VWO>0lpx5U_6Qe*T{oj+h&RK z42xb=C%S@64Ob-%Z1cf5jv8;z)WH8IS7T9;jB1G)hVlRbP8J|vi?AO@{PF{=cagqM zXwy91NVOU)uv_I?EGz1g;ri3}I2*TDKZw@Ei>XL@M8X!RHLZ)M1L^?kqYHQ0!cxto zDM~q_JkOh|oC|e=dvDy2QipfCLWM#e0S$JOV&DJphvmPAXj zO*Wa(1J`v^q#BGH=|+#tGi=-+AagymiX<}CkQK#oDjeF$xtn}>2be0KwQaIs&b5L% zr;VZ$A*`Eph+z)5OuS&QSMj2QrSfiWhp5wOdzuYcTrA39$!HpPnOYsvzZ1bS1|#Ne z?$A`NT8-fTV$>**rCDA8Gg6R8ol+X5mgZXkZQJLXf7WkhT9k#rt&cE+c&e2n-1-19o2 zmQS(U6u-b+)jUhIPHm3I^pQwzsI5e|znN?GLI)l|WJGbDc?UZrH7d4bR z)>@;T$QztV+zw~sH7CE7sMMO3ir@&Nwsyo`HF^X`sTyO^tzI;Y6&uk=vEuX;cU;}J z+p-nhav6lqth@k6kBaC2r!Bo{Y2(8i+{VuOee0REe_6X}?J2AGtwyy!*1lZ3tl>2p z^=s88)n`@LDSxP}DIc#m0kQ&o8AJp8h5VIrO!j$M4MYTd2ZSttewklh2gJ_(PxeGb zfs&iV(h+n@SP##Qe=-)v;j}9iG9}NM6Rd(HuUuOi?mqMf2D6de>U3bXt51|cD?6jD z92AU3ax14L=h_n`Kph>YK0BCKQUq5U?_sZY>Y8`%@&{&pQ1-8}R)(0sp^eNy{F zcX}}$-AnaJWljWvnjTq95B9f_-0HLnyZS@`v@)`o{x;4{`+Ykt;Mbn;1L`A-DP}y# zOG9!WMuqLh6FzY%JH43p>Cg0)kWBx~lX`T|BTnkr39rZjnAF27UE|sNVvJ3*>Isj6 z>~BPeloz-x9-n0gYz#)%Pq8o0KH&!Y(*9f+-LeiQ_SA85>SISvxWsL(xaS<*j-I*t zlpKui`1@5&C!8W#afGLMn7$?(8^Sg1HcG6YK5u%hw)Sd+Sh ztlqGSXisQ8+Q(?_(-buqsvl6lP>ra*p?ZnRu<;S)FO^~Co0MwBrxnK(u9e@cd}!sx zD}MRMSLAY={6@J<`(LuZm0c}ULT`sskaqdq%dO>|rJpU`vvlLg^?>#pvj6q8edkju~X9-DUMA69&UU8y=esD}0#GBr?1+Sfo8Y zV0#NJ@u?{${;iazNvl2Ra#gdIA~_kc)pYE#J`tY>Yv9i8X|anrFUi&e)i zTLx8G!cDo8-FhM-RdY_y_vEodB26b-H_(v~@ON_RJiwrtHTp9VD#CNYQVj7o znrS#gRRUo;;lt*iP)x_x#KAh79XfeTE3&hI-{yQzMDmzMq*+N7^jWSEHsG}e9Q74- z6_3ki30tkhg!1mYBWP4pTv*oAgFAL1HqNJ9$hk(oAxpYQ)yd zr7Ollvze-Z#F7Zk_?z`Yh2q1GPz3>ge1aECR9%M?wM>Qf0XL6Qn+G=|4R$iuAbCtB z(!{OCpwKFiKFm|h_oC)-g7anaC>4+R{Kg=I*Lyj=&1m(ddZxp{UbSBJhq??w)Qesr z8lDz@rT8h}(ZlIL_*=m)$fW8P>}02119^-5lS+v^rV!b2MR8!oZHo92n5^cT))-}N z*`io(JJ_aeRi~jt7JIHvvR6;I4pVF^2qNLdc^3z~8%m*GZ9*53$5up|K5yLbD_97l zvu!V?(m5gm{Jk0$U)7nz{TXMx8B4PryNO9hb%)7vIm`9h*+$7p+a2YekDsC`7imUq zm88$t2q6y6>1^<91&g+8`DoUY=utISwgr47x(;jGZY)xVZLPYqT#MvlRw~Vg8{H{& zOCFPnG-EpuO)!uRQjicNd^@!P=OAGO->ucbl<1N!(;8sc((~Yz< z*`46dar5&=DH(t-5Z0)-Ji-L-Fxjp5$SQ%ml7&jo3EUAUgZn#f2Cl=Whu(l8CUe|H z6ehF69M0KXVY=?e3v8eR%tX84scCxi4dRrl>U>6bEW>+!ZDWdVC$hZ7UH6CcEV@kw zwh@yZkCX#0n-0dw!-6FT((l%5?wFk`$4R?;GN6g$=Ickf(*y1>S2P!y#2vOI?Da;a z+p)5ClFdXSiTcDy7;o;$V6e!J{PRFf)SOii*NaRPI&9heD7~?usMQ5^~On8FgX0dO4RcwDhPgghWf3bfS#mR6Et}#p)>QK%JGu z;eeHJT1pv^dd@GzTtwIv9${GAF_JfTM4CZAS7gf;0YMvF9RvhRg;|iux{~E}8KM(l zjV`oSPgg7sht4!VcRG1uR1mlT>!b7nQo}O!cC!-L)(LTAsmbNe-u44hQnnQfM{`bT(XS_v+DLFN+=a0XAuxO{xszVvg@ z$Zz@M-ZRJ{l3aW8{4M2B3j^hJKteM2p1x1)NsACWD1VrH59<@VbP;0U&3|UHFZUkW zCw9q##AYVwa_=F1Vo#jEc|JX62PM^V@4kvZ>LY} z&?3YR$}8pG`=dUwCoDqjpj=ST+S>-i(6z?{VsmoN%&={7?>)26%7qeY51+9QG+A-) zZS{#gZV_S!S*E!6{;*H%4;CXf+YrUQ_l!QV$1XzbAgdGi-qZWU9Bar{w3rv!i?OX?55n=~f0l4>$^ogx3NNnb#zkBZ~ePZ%O zh#mC8@7}w-PfWH5v4bA#BYTGdF=h>#e>z^|tR30AtWS1%{u|{Y*8-8fC-=!NNg%tx zYkp+!Ng~|N3)`)di@F7Ol|*?VH2?5`wnyTHqP zWbfiW*!FGCRcwZq4w~R(quqUb0VD}$%$>ed+!hW z#D2O6F+GZ!+G%fR@{Hx)du*TBPZlADp{P+{?KaC4XOHO<`|*Or40TT+XqzH-L7&)< z79oa$tzXsVvP}lkbnk8SiT!XvVi;43@sq|}?!EOsv4LdvCQ*?B5q822*}IHhElh?`iwQey|8JVB@E2G9GU7w#vPy=@a|@ z0>lmsi|XD}_lbRP5n=}g2X*hM`o#Wi5n=}!U%2;_ePRzSNNlF1g?mrYC-&V%h#h1! zamC(>9D>Q!?@T}S54WTT-v|us|F2!zcwpm&8yBp&7nq+n+X35k?Eq`v39p}X1zxKl6G|(dl9eE`;a7NP~{piJ{ zc+V%AImM1wL@rL7|LBo}&dQRx7+A6uj)dUiLM)?^XJ1{4(WG)8tO*oztDH9i+lBWYt`tuK~2Ti|G5$fN+7rAzkXEMhb z(DvvA{GIp>9M6JQMknAo^&2>z0n|q);JNi101B_~(lIgt|EC=X1`|+p@fcNj=gN8D zrsd<;iMG4Mg^OGx%s+)4G#5w3t3MaT?p2Pb01x6!T;R?@G7r;Av2y&m`|T}o88QEe zxZmgL_L$?>g7!oiSm2gIa{o@NW9GQ%2_m+$z_o?sew}V7e_ZtV5T$N`dko3_8C?MK z$3ki4CI^&`<yijO3vRJRvY6m!LiO0x5k@s+=ToiGkc)Djb7;WYK4LZ#h=_b=W>`yvtc)jJc z<=mE1k?uIk6Gt1xa^+=j^WG)?_N~m0SAvXg?!`;!o3@-Hi&vgzaplOiu}%pvQ8r~r zUl(;-oTW&`W@@xEWvAY0vm(`MED7hl7GKJA;6=fFZdXJ;omYszU7wK3?Nb~gw>zh~ zl^?0m;kKpisl~%s9W!GYq>EY1c4OY^>vc0k!rZYKXs0a|wxqCqZpBSys?lPljHz7Y z&#v$P{=8H!pK2GmL{D=mJHi@_?Ql7!6I^WF>M48Ue3arM9^5DB6ST*HvUN+m0TQny zQQbb5qeuGrqbU;yI}tUTWJ|Qtxx976s@?)@4S~ zn5AeBg;2akBq9l$FW13~fr`WE^BTjnzh+1kk`W7&*k^EXR-VtG$lsM85Px5Fu~h!H ztW(m%aA?G8_Ec^9D(noH%BUBpB zBohxMtjP{jp7iMJOV1S{k=th-KY&}^gnqc`DfpE1P)g&V%#Nr1+dP~ds-qFsi zEAI_9!fhBtz4x?$Ba%rd6`T3WHtO)rlAgJI_NB*Q7Wp*(kyJjPeASfnWJ9`0nu%DE zW`s+6&{(*Q8VhYA;z>n{iDn}f>TT!gmYahs&2j0O%VA%7uvQJgF=ypRL@Gzgo`8EsAd{-lcf4;u^({0$TYJumiYZC9rb2{Acoy zZG2(ljT^#-cjL+HKUx3C`tCZre&zaO*S@{>p0$^*rPh$OmDR7TzGd}!tK{kt?SDaG z=xNJ8U;gCsYqWo_{Y!0KYtueK^F0u0;1!yT#-LHFzplPZeX}~MeunB-s?VriuWC0=Dp;wcl$2DkLK#s;q=HvKn2xYiuv{5ZhNOaJN>WLl0`u9NC7843^4UN& zT+v*lxkxHl zt~sPRBo!>vJVEmWsbEO+c+KPY1;sXxXU&D03x|ZO#Uugpf(bR8@+CbqGwF4ylD|a$ z5~*ON{KfJYO9d=;Sxc zZypln=FDdK`o-%PO9ji;FIqn}=jKs{)(@>88nQeOy#9psQ&Vo93SCt`SNYsiEYH!m ztIBJY*GdIzmDeb*kq*9EdG$V+$hkAILbl|$gwic%qEVkb0zmR>lX+nI)$&t> zW96!y$&?Fm5Wuk1auJi6x|NDoD_%Y1Vjftb*i-CD1+OS}6}wWwa>c6@uaXLuDPE~~ zrBpDaIH@=}9J#q2yj5}Qkg%^{w+0KH3KxrVB|aXXj0B>Ue?a~LsbG!#0r>+`!D{*a z^82NNRr2@C-!B!cl)q2@KB-`Z{66`8Qo$?od*$~^1l_M$YpYwR4}mfmP1m(D*3W} zSt?j5Uy?6L1uJB~k^M$0ct!T#vj3I}mdk!E`?XZCO!h0;ucU$@*)L_k9DeP2Ur_c7 z*)R49&yA;{T>b0Szm^JCtlqJDhg9&&>YG;IBo!=QedFpIrGjOvZ&-bUR4}x9`|9oc zBR6+6R$ssR`XS-DTQtp+G*6NWR%oIn4M$jSNh|EQdtKmU(Rjq~UKk-^aXUp@be_W$zbjipta>K6GY zLE-oN=SAjSFGx8`zbvdif{glA4Z>_r`G#MAxule%uny!Lotbmek5u5aqgBn}W~ME$ z(RQqv=e+GiF`GwR8S9RxD+u;HR;MqxpB0|8*?4?eruTQ=yKghMr6yZ4fXy;fzao?IQn+`J`S2~~ndgE55gIe|DYyTwK? z9V2sP|)Eu6aU68Qjv=--?E-wNm1zJB4Xi;7km0dIwf(4=+ixfQ(b24b{ z*a~nb7eS)dQYI76=*)tXPgb&2s~h2sI~mqu?FQ2g*b8JkYmDQe(3Zu3HF_akyk0YP zGjVi_t{o`5?o+ZmHObCec1=KbPmj>PF?X{`=NWON=8D-F1!%ZR8UUr4Yx_d7AbuT& z%Q6iyQ};-Y;!z9d{w73Ze0-Wr4VMTxza)-`)tS~iY|eUjc6*#rCzRo58{OkzlqZl5 z8W^}-ZVMfI-G*oAh_zwjtDRn@s-t6!!(+{xT9g;tR}G8GZZv7=gkxULhjNZ4Rcw@~ zy4A2{vtjxqX?8X6om#qsq6W*BwG-7-4a82DTDT)vGv{doWz?6VwRq9(4!q+3-Z^01b)Ayksg9qu>>7dW&KZ1B^k!@@XUWXy;1hV+VCa^KI$<+9x7@yDgiUZp zbJtLc(nwNgtLH*E?+C*N>-h~PH*b-e`rxAW85KB5ixhf#k(#MX7m^pifOK6tcM&*v z{TQwQdeGn*mx@JY7lqSV*0i-%jVHW}xfZl@_*RVW>9_nf+m68ia}*h^by2w1NYK_= zBxED87VQkSD~`53oymF&hCnybb2ZGha+GUqb&U>x%|gzSUGe=t3~ej{bE*^aqWn4X zKaf2nyHED#vb^lsvd2K*QQ1{bRQ^!;Vdb8(sc3Ps)lT)L zsvoO9s=7^8|NXw4_#f!~AIeEdru)F#Kq?gY8c2l#PXnn?;AbEe3fv3^P}^K{T3|>n z6$hLMjxvLr8@JX9&&O zmTRKIl)DKe&3s`r4{{^$@hF{m$xx+IMJQu=-XlseSUs zr#1hsd9UWhnwaLP>Yu1T2qn~ircSB1RlihyT=mN3pQ#uXs#;clM)?=YveK+nDZZ?@ zUC~fj6dNmFUwO-lu;N(jfct`P%KuJ&queKdqU_(+uav!8_Qz|4EG#>`_SewEYhPXe z(uQ*VOY5&+udfk+L1{nG)1=`Z-6+sqWHAn583P@`b)`za%K3p4w!Rc}XM1=yldL2u z@Drs1X(!z^C!#Ho1i779-5k{5vqq%oNfSkPyd1T-@(d4ChO)lqO8RK=gv$iVHQ`65HwSb|s-L z^lMmpM5=>lg1##4Dj}XkAxPVd@i1ZYr^Ad;Hq(ws&SxyA2yc1iWrG?5Z?U#BEwj#= zD@F2k%0)PJ@er4$T4<@JcXD;VzUtU^H=CZ5)*{#M+8p*ER^yA99)JgDJGcF{@~x4S?}>~6DJ55|L- zKa}%(D*{f2YMg#sk3@>#Hjt221~r^DvH`PHqeo?m;eV>};!ykWEXCf#C|lC7W!f>?-ClN_Ovl4K7Wf zdeVgnY(41kHk@?R<7*j`wvLf0IN^*?bBEG3!4B)x#r+yq&f3{>Bx(_%7syy_t$48! zM(i#JosRC5b_(&WYBx(-3?)L13~E>^;KsAa1lVe#Y;|JcvZ34r>7S`o)|I8)o-F3Y zncERW2FvvQnp$}k;CC>%;hOgDGHY2)% zUti;`LMw!u(g+*tm>LBWRc9L}&HhZpype5s+mXxbojQ<8me{t`OVRBv4CZ>?>UTux zL|WHy=?ccg@=FG7V5kq)JCZJEHv>mmCeP7?ttog7aHZ;LnoB6r*)CR$79p&Ii2WLH zCxwOVdBK1?tj%Pqo@Ko`Pk|4`O>8e^@!PDHM!*`XMs153E5i64C(t$e5kRDIg<+IxGoTFgpiv^uK zRZaU_yLi`yFnE2?Y{Vm&^s@ zgqJbELPJl*jDk_usGD_b{U&h1%5#Nnoz-6M_*ppbkC#1EC2sZGU_WOJg~MSX6fNnz zsbFUL>OqY*WyF}e%N<3FEYhl3YcY!UwtzuQFwN!MA)U{Z@R*AAXr-+FkNp~9E>jI# zGldrBj+kPBcE!qr*jqGfN8&ms;b}OL7TH0ufJqbEuVDvI1Q{O_?6nAQDDQN7i{&ta zRhg(K7|Z)e)E759V3*zKl9vWGTFE?*fr-kq=4h!{#~d&m%{$msJ%_O+KS!dpyJZ&a zt}q4t#h?aXVdFu;&}JC3p<4qIXeqjarhHoP0R_`Q!9;851GEvX`Ji9y*T`Y%c0R-= z@?a7aEZZhSiOM)BcOa25c9M9Zm@1HY6iG69B|E6mL`c5sie$qq?FzFX%oM_HhttMp zq0($tSdf!FS1H{v~@ICNQ2#&A!3H{RvUpqqvixmQ_yvT8kGi`E946) z+U~3HzK*e%$&jJ8&D%D?F^=~aI5_RI*Q_?WULE6ZfwXTS(%m3} zOc4$xwt1&}JG$*5L*{tY)=~d@P$O4st7gPWwzhNiDB`flG=mznA!>CN9elcyuoGp1Yi_4OTo!$e-FBqN zl+bI1?5S!2w`Mv^UmetlrvoL5Kyy~I9L*JaOg8N`bzLUdM3rE)NOrL>k!C@HY$C6G z%%BG4ve=UmLt>jii$P-|iIi;R2=1zKd0$4KcQ~6|EP$H|o_JzqZ%`u~iUrVilF>KO zM!M@S#M^=%Y3h2`ZouL&mdi1xyIeQ$T?eu3AJlL+eO!nx6}>4P5yff-yURmo;~r}+ zgrOjQV7pu@=i7!bYbz^1Fi<3JoX%_$yvyeHw9Pr3>AIb8!=Hm|a7|~&@eC8K5ZeZZ z4pj%*4R;}g1w%SL9_LuhsCNoUILz8}NHp0lge%qr)q&9po$OHL@}~z(IXbl8817lg zP{3s_^#WFw%OwO?zSt{Q0yzi8IDGk1vIo{`#m$2ncE-tK9d0|)sr$m#b}`4s;!)P_ zYtjxb0KP596{uK^uEp}ptw9aEYfjT_t!U;+uOXjk(!LZ?&@;(mr$M& zZn&u!EakX~OLP)Bz8qxHR45VJ?m9d%%F#pg!MxRAuetoqNGuvJn;jYXYX@!UD^#lJ z42Ah{+nZ=cF?*M;kc^ND#Cs82e%qfjAgy#E*)v#Fi9rpJ-reg&6HH2%=~eI&?IA*R z-RUX^n+>jo={nwc#u-K!&S%j+c~C=oPI&FbOH1qZC3vANc)t;=ABp*_?nY0qqfmdA zD3-l?FIkG}I1s6aF_a4#I-LlnEKI(-cFCZIK4f=Cd`z#?j3#-b&_dW;T4)>0R1Kj6 zK|`|!f;04jnQ+gfl@Dr=23-q;HEK9Jg3TYzwvBmn)B?6tVP~b|jCpMCRz^rQeb%T? z^M*lYy9uiDE6G-#*Xfo6DI?$?6$7mNWFkHIIWhi93-lg)I*Y#`Q`dZ6cs3P7* zDHgU;Ida<+unN9*KIylBu7vrRnb76*HGjFRdHJ9XS27)R=giiIz2QukGBJxIr|(g2 zQ`=q&rOF^WX~^r#f*5v8Rl|URJ&VD$w8I&19xY@{4MV-s=2`_$jVtrLn5$$2+Ouxz z5?mnO>NIIWHdvr6JW&q`wvdlZHh}F*GHwy9s9@*)rKU~Dk#Pq&;iQ{XLt}OAybFpf^JwBtu@hl5v&%~h|sWwYn5uQ z)@D(R(p)FL|Lc`^FR9`xol2#AP)(@z z`!|WWha}>@D-rjd0j|h|c-m@Dy4_Y+pd1R;Cc==(ni6phiMYB%TumabDiOy?#8o8X zSc$l@L|kcrD^Oj$#D&YzcrDT(*#L|jTD?zs|i*Gj})BN2DCL|k%!n`;lVClR+V zM|0`z%h6oiz8uZP?aR?z+`b&m#qG<{T+^2a2IDdpcfUm3`z7MuClPm_MBKd+aqpFg zyGJ7KJrZ$uOT@i<;Qy~$TD7YFSpLyv)yNj(_p11!XYB_2j{h;>_-`6n?tV+he+)P= z&G1k%PPPD?W=1`foVKzfLRvdno4Mr+HRA@#Wl=ExF4q| zUp1ZWRc+y(x#+YTb(f2ty=Qbk8u}^02tXd}&NSB(YEd_)v@e&;V_d8rp;|jI>TKHL z2p{V@>v*9-mxFYi^jquRkcq%)fg=itL(j7TGY*%mK}(`(#B8>7-X6_mlF62ftNYwl zp&2>A1@VgAKwo?*Aih(=b=EG3FnY=dQFO~?tHWqOLzm9WPYo^nTMn&c-`|yofHFt@i;-LwmENwo)ePHm7B^;TAf{v}*=?ZqRtHV8`Apg8 z$U8Aq7b-j29kvo`;^AQ42!~oNoGVnzwG3f1Z#BB@P)VnEvaKdpPLqtqOxY~6oDg4h z#jgL9^iGY<*-I}XS$cK!9Fj&^QBimXY=NZx&6T+&mz&b zw-*WUcB*WHg&t0oYME|ri_yhkV_454%`NJ8C%sg)=w&l4(!!C^oY(71Vu6a6brix> z#aQDqNy12V+pVHLP;vNxxl@REb?%0h6I{h`sTr^|y0+P2 zcg(xKzVwPQv!=%8?4=h5(mPyU#qH}Ew>We6E(dRWe#@QOLSOrAgx7dlc+Us*7zyez zz7Lt2c%mkovGh&_b(>gv7YfMsyIOj;;<2{X(~O1MI|g7$XHGZhq(122g)r3v_NuO~ zu8o_jrb0JNbnSX~)!&SlA_aSZPrJ}khi(D`$t_^Dvz1R2!%e@wnJ}~o%gopv;`u+d z^sh@B-`@Dd#@}w70A2uBZ9IPc2kRdI@BZAnZT;f4AFh35?Y6bXnse<*Yck-s|LWEJ zDysbr$lCWxZAQBTJoP@RIj$*cuGCzp{=WJF^()mCwN-tQ>S5J~Rc}^3Ulmn7Qw1r% zqI|3J7>MGoQz{hSS3Cf6*;Eu(#YHO*uY7pr)hqQC$I3HTAo*A1Zv`ovl5(B=LXf!P zpJgAC?a68~yX+F^$Iw4O_d>6OI*<=~3dkGyndLVuU%yN)Z!Y~Buz!Z1Lwgs>Q0Rz$ z>ceYQn|qG~&nQP=y-}ybP`wdB%|?sGfmcQpyclm?tJvK8gGr=;O~zf+ByweQ@3CV@ z7&YtDHm}__iIi{dJ!TB4L(LdTc&+(Kq-=BVf>ETwjG9rZnQ=|=32p9ej3G^^8FBI6 z!el-@?egZ{`WVuPnqfzk%ef|7U)tPT8$%jUv#uWL2$QMDG{4#0TOC8{QIk2;>{=QV zG&TRdxu+dNVyMZKtOSVYB=XmX_B8#^Hx&y@^Q%L9>i(am;vZ>#d1z17|I<_?7|qW& z_mrbe>&>Xiko4l!DG${D+T2r&Ax)@BAFq`gldn^n|J>YLnM7h_!_%BX{%mtkK87@+ zCe+m>>8Zi|>E@nn3~4}3h$Wv8N|QtMlS6xu*jdax@|XSrRrBLRd&~VlP3`kEKRUFx z)c@0zTxcFXw0mR!Pt#-m@0+{N8{1sL|ZveZ-_7Xuh|( zd&3yggc?n*t~EEAdP?(eo4ePKA&scfSdPUkv5}sO0;_pwbGJ8!G@wR9B3I6NCz0RX z+!e-gFyth6Ew4uwtifjZC)wPlt9Z;^#xB#pEA1ciAx_I@ExaLKHuxj{aeD zw>*kO%%}kl2z)g=+1!^mcS~bPFx0vX!*j_=ZrmmhfpW57|M~T2D zR1XKMHD*dx{{80ebz?{{)H*JcwoQG$Pi*d{CXkr96^;0&XnuTi_qk(81B#j2kwVfj z*@cg7?p`~F)T5Zu>j-%!bERrNy19GJC{hP#8VG{4O||}!&E2cVkQj>TZPpSyrB6P* zxtknAq9}%C-5rO0lFtuq?j|OYXp>{PDKmf%Ztlj%kO+z)m8iQinVMSjfz9367!pP? z*bN*UCifef2jJbP=+Wq8>jpX!iV!KM&O>x4*67bfs0hymOEJXXXr|!|RSAUYgb&l) z2k%m1os20bxVx2*$&v4***MlA>psCp<0i_C6WL;wNMTIGk!~0eo#uXcH_~q>jo?Dd zRrP=*>lnC-?y)|+S5El!JRV58D)e@}WY2*#lDH|ac^|wR7V89bxw6~gGy{`vgwg|d zA5Sz`i*46K4GXZ$hKn|;1H53YAQLVj@)i>7baN!zVGRBj@Q%wg&2=H$f{Tq1-mnJH zRwR!4yp<$@1WLNBNpml}ONw=J<}A{YolEl`c-Jr15q$MjK50w1 zEpUMgTVqZ-11=_YXfztftj4xCk_%#{WGUsal&G~n8|>m=0qBJ@Jx!# z+ctNdV@N%Un%Ze^Cp-h`7(-$xY7|(z%`yXNpF|q!o;f)J@s_5E)*cb_$eG@yvS%@M&VRr%IKyI1rlZZ zpW!HeZvK=wd9eZ_H`hqbrw`YAjbzVKWUP5dI9>Au$k4=`tNf98Gx$fD21E1Asgpi( z&^9A?@*Fio7kTVG*{)e5glxXbvh&kn_zIkv~AFjUQ~$3)YIKOXwR59`3E8o zpqn3+%)`W7hY#9iwoX1)q)m{Tj~tGK?HM|5!|M%eh;W9S-S))1IcTm=T@jq>(}8F$KZOGkc$$I=KPpi&OA}lIhQ! zf?^9Z_t#(wu8W@tOu-LGZsLqn5F6D^V^dIf(AGV1a!up`bn^kpJWNbM-9elE8&9r^ zvk1)YG0|VDhTwpeZ;iRsCzPMUDYCp49Gh@cwhjfiB(PlXf%b z#fyoOFICRy!^TvYG36_{6p^;tGj2XRs}Zf)fphZtHP+Sw2fUlKC*`2A&1)IW-I6#c>ydxmVtaWOE-aZ+X=)Da-5i3E=Ru% zB9NGSSz8qg*eo4u$+e}Q-bhC0E7h85#9v4| z3hfL^8UnU_$7kucf%MyOhV&T+l@EsTLIUx#8Pq4XCHP|fwg7E#%Nhne*v&3)IO(Bv zWG~0MVKM@5du{nXHpu}jqAz5fQ6U+|^DR#fw(DC>s!PPSg-TB-H#0iYl+dxAV3&4n zr(4dbv4`oMohtOeqix6%y+Gs~4VkmbgCPUA=X`*1mb(eg-HBIb4VevYII=-+Bwr28fyfSv6#KKU5|A=-GV93b%nO23ek_kA+w#MA+zOqPgz&-+QE#rVS;vFJd=$YPG> zb9@?ZCDXP-$Bp^MPW=}%BD&hsm>TOJm*Z;oJ|IVuq}g#wl*Ej zGWNV#aFiL?(F2d^)hq}~Cg3h60R%JkC>*kLHOmNGpD1M-zEYM8+i)b=^M}(ta>kHZ zNjsmiM(UXcPF0;|8^N>AHi)N|HoE=gu#Io0fH6)6CwOBU25;46c6ZYrgOSWB1Aq_ZTh+itiMjhe$E zU@^?nHK4fHWF;wn8^qW2fms%R^bOfL(`8OK$V}cOXzamat*ILQWziZ#cTc)m}Rl0rDgQooK}BtsEF_XGTFP9 zR)3)SHu%5uKj%l_{0KbiN8lw}yJ00s(tj?fhwttEYt@-XO7rhhL5}Qdnc-5!EbwwS zASm#hYVyEhrjn{QKt}2!mk0k246yXTBX5Qa+Q5D0;Jeoe4=W{}f|=#}|S!QQq78d(=`k9_83S6@EX$C+2h6HUZw_N}-HD};hnSa766 z#fH6Jq1*mIoX8W+8x|M(@mjH*HS1B#z$Li_UAKE{$`aXT%C4 zj*PPnP_W56d9l`Ls@_(UX*9vNzW(y7q*A!zGGOB(Mxf}|O=X!J-C}t_Lv*R`yXXxF z?%%OJtWk zy&6nnAW&i}Ag2fThs(urA9G(Eooq9OSgbM20akojIsLa3_OaGIvcCsQZdCQK)T16vHnrGwPX2qL`L`g?e49LvwG`NUS zmkewb>((m6!EU<&@faKSu#=A0OG&O?PDInrIx5Tx19CZ?XJ&N$=+RqbyCG3szWf>E z>T;%7pF4Fa#{T)AR+r*bJA*De=y?oGq0#U|i>YX_%?VvLcnK3H=Kq2g8#LZ56l>z_ z6JPa2MzY0bjRkRL1`6o%gY=sS9cYbItHA;ST#IEz{WcVDQIzJ0w`AoBNFAsKW=>9# zyR%jUFl+$TXQ}~L!q3!<#zeZ|bB5DSUpEKb47T8`-O#pTHNK8_b6mx1420uEKitsS zr~!-9xl>MZy1G?ob7B>%Ayfv5fS7D6pJmvjzFIbf6E!!&m<$DTGan2#5|Mgr%U{ox z4A>62#l(4>_Y?|wGLAP9fd~=hkZD#tky&a0bp7bj7hbSSifZ5`))6&ukVKq2H6X?i znot8Xq;!T54PJf!mxQPID(~ zvDK%qUb1?@s$Bbs_6OPrwO<7B{qNP@rM+GID(y?P&)4$Wg7z8^?cb&~X`iXRO#3*k zO7knt4>jKcvHw4*xnJ`R%^jNCG_TMc({wc~$OI74I5p4K=rl((hcv62CG}51UVyKu zKdb(b`fl}|>es4IsBcl<0I~#R)p4~~eU%zhKV5x^`U15aOs{PRUcE`tGY{d zyXsY{mx71_ysDtOMio@qR3;Ex;4;T>dkgq(l@`IHJSH8INv6XvQ?pnEhkmHdbDZ-Mv)pOoJ(e~0`I`EBx7fJg>iIV-r$mSLI8xpUNJR zeNFaR5Z&Nz*`2c2%1+2`k=+2|9Ass2nOAm|43j-wc8Tl)nH+iq`T_JH^hM}nAojss z(CyHxpqE0=hj^#}T>}L-e!B6{#@9AJyYZooyEpFKcrA$DaLdLG8?}w>Mts8y;y7R% zPv5v?Bf3{Dh2=tX<`D?d9|1d0n*l8!}4EPp?40;w>}KLV_5FK8G8G$ zT(}>4+pwJbcj)hiW#5I+UBj~DFQLC3mY;qz^wwedf)?~Q!}9tzbmxBgp&wldy=7Q_ z_}S2#hvhH40s8A<`EyTz?iiNe^rz69hUGVW0(#@H{K9WRZ#Y$cC3O3+Tya3JAC}AO z&|eM9-cLZU8{s#JsQ{~&C+xE-f{n3-4R}ae%t053( zqc7**{oK=_-C_CDk3d8D|L)&?9(v{Q-|xBH^P7KT4*FeXI<@W8+ zpAXB|eG&SzVfnfDLa!K>ohj%~hh_W!gI+!?@BAV3vSE4q8PH3I<+UF{e=;m9aZL&t{Y_rDXmd076FGW3FB z`Ie_aHx0`#)IiT4mT!I@bmOpm!v~?~4a<}oIyx-x{0nr$u&lWRx_-a>?SH=l>J7`E zcrzpn%WwM$)E$;zyaaWIeQ`s&%O8Dd(S!d)TzJ5{0_YE49x4mFIO?Q z10TEqb2@O}v6$V4A9$z-vpVpNB4&2r{|qpr11l$DdIw(pB&K!XlRk>69eBTMF{KUP z|LFTMxdUJSM@-6L6%#w~2Y!zUIs99U@4((`Fs=iyC}C^|KCywlwgb<<20N<_-}l^k z*qI&pAqjg;2fpe3*sDA6^*_eW=)l*#8r$B1uYCa9)`73yh)s0hFRjD2cHkYCV_Q0~ zaX+>>hh1#E12?x~n>z5}^RbO>_}-`AfQ@zFTOY(Wbl|Ui5nJDZKfDE7*MZ$Tu(cg{ z?Fwv72d=&qTit=@Ux=-0!}mOM1GcgQKYS&&q67baEw;P^KXfg&tONh>acr~$fB%En z(hhvpPp~B&_>&i4BOUnNw_}StaB={4dI#q2#!l*r&y>cF)Nu~&9rx`>_Hffrnj zozj61xDb0q2Od5gYj)tmO&HUL?|x`G*66@@{SvEp;Oig2Y908}%P~5KkH)GU_{|^2 zs17WB3afPBWe!&Ez^A9r&jE;QQEoRdqdM?aS7S$Z;4j^Zy`%%5a}jn#2Yxe$9o~VBVeGIDte$}#+JQHBVTW|! z)82s{+<_N{*!&KB;CHcuI&fhLc3>O+)pH(pKnK3@CT#x>{6QJpuLIl9VZ$BxjGM7} z9e7a<8|uIZrPyE_zVo>QF|q^y`db*$)rHObr+2&TWAOi)1loP$_p8}ru>-%PyNI>n z-#z#9Vxa>+^LVkV13z^g_FM=4(~q!cJMhC7VSnquFn9d51ONJ3?3oUHUxNLm1K;y* z?9Uze?*GJ|?!b4Pi9OYUf4M*QWCycC(78}`Qze9h_DBOUmve(d26eC3tcA3E?CK9Bvr1Ap#q*zY><Pbm!G7C; zFIt8DrUQTKe(b>x{PB~qU$^}K*g0JTAMQB>{@eFwp9KCXB=Gh(?AX#fHg*gMP-pq9C?G%UfO#ZrE9V%<(wM-?_#+PsPV!K{9s~mhI z)s9wgVEJ)dk~WPQ+n23f?XRvaMJa)I_j%7^_uTar&(CMG`zvNTkp-sJU0?Bcb*s;m zr)A1J7cUHJRceE=jIWPxoe&zUH!Iq9%BgJIxV*BdHlD6>=!LaqHD+}gk`uS`u}*VE z`93%Xk(Ky=!x!btJNXI3BtU#j&A4CLnc=tNWkQ~s>6Y?68pdZF73ftj&{@Qk8|4~2 zzDH-#rp+s3XWdA)nQg2e3ycV&8eL`ix*9KSR~K(*18sb5dD)UWWLsMMf(_&5wq(W9 zP^o!a*01xeW$Qwc z)sQFFNcCvK-Nf?SR+k%$v7!dwg5N{(`RRu7R@K30ob1?)oJH^7e-~%bOnvqR>7PBS zo5ntMqp(!1j$BNCS9LSf+1e@UM%`A^Qce<9FWO{{Y!0_A8eOwxyfVfw-?V!5s4mr- zDse)SlTSC7%|&DBdSSb_YMI3^rWV=;yM3hLZ>UP%R*v3o zuB(l1)Xj>zWu4nxH?n?YY>~~h6bk9s3`0uBC>a**f zf7Nx!w5879f5C>F_pKq+nxIke)fxZrMIw6V&ZPxXFemRkz>bFB|ANlR89?gW2{}WB z&yxWw9aTejyu_--+eKmdI$v11bd@G0V@|ew z?Wk#tZj!dU@M#=w!$$gr`~U4lTihzUl&>^atXi*z%e9P~NE+1@<&lXcBTHqWE-qTM zb-hMe3k7MVyKG^!HCv_%;>y6b8|m^;Tn--r8r{Bi(a5&(a7%5=Vr>yOI!9H5st*4D z;w!tB7l*orZXUX5C>mNZbSQX=zd87UL1C~y*h~JB{4DuKawB;h@g(uz#K#E>q679D zcwpe_fwvEAA2_l9+5Ve)e%pUR?`Ti4bX(~X@bd?~^Lr{i?5>y1wt?cXT)KvF;Cc_ugKK~SXz7^J~K6Z z2w*pvP|f1eGgH@@X?!0v3w3jv1P`5snwc78Up!=HYNj%+bq`^uSV8&lh%IScL7+Ay=w z?k-iT*_efPs!VIM&`zyeeP-HeW4byE?e6lWn&nw&r^@W)S!k!qY?jHYUYlCEVqvz< zHC5GOPtHa?Rmj+%W}}|U3HHb=)VqCD1Suix;n}FCDWTuZMm=@tV}G2DdiQ1oP9B_% zdTQff56wnBP2Rtqje7S10GvE93-xa4_Zfz2Vt<&WPfcw~dX|za)40BWHtMNGWB1KQ zJ*}7AH4E2MiK$siE>E2Vv0u$bJ*`LGI~(=xJql!f_iWVDdep76QBRZh?Xyu&llCn$ z%da+7B*)HNa&6l35&P+E)VoUr`sz)yQBSMm&9hNYZFcM@vrtc4Bx66Gjd~gzV=Fd>Tg95XVj-Wc+Op1E zWj42ra-&vMt282W>-G^#-?P`v6#wa+{Pc7uKl-ZibkEzKo%}P6iWg!1W+<9!%w+w} zMD*3@bzVbVzq+v~G9@J)U02y+t1R;YTMB6F9u?3Y zJMD{BK-5eMsNCGAfc`NGXr_?WDGF#jtf@?7*%VdZ&TZV7Y#&*>V$Gt)W`3f&wCQQ- z_^7yHVq%;h(_nu47ga#ibOnT#AJ0Pp?QvARCm+zRRbMuu)`JNqs*Es;w58@^$b!DKIl4B1OT@r_J6R#=)17eEY6MO6TAo!d3iL`2NM;l+4mdX+E65zpncu-Fo-wZSLAU z7QZpK{pSLF48x9&HkxaBd(~2LN$4zI8oTV&PbLrSKd}E?N%tGrZ?2@n1H*07^^&+f z;sPgJA=&OGW!cn^{uYyBGFQ^L7|)e7Dn@P6Rb$)C^~w5?HSx05**1OM)NcuQo!fQp zTuFCy?U*a+IbG+>mGq-sAB9#udv5Le529-GYAu_v^d+05an9egZnYQ4Qyf*KLppOM z^+<27q(14-l{6rO!DqV`&QT2QQj^iOTeppG7P4qfx@G&iZ0c8{AxXqNb0xid@b0;i z-ZgmFTuFa5_^Y{+-Z^+@n{=MrMr~QES))5(pQ&WJy+80rR{SiohVJrm2^vK%UnsvOXG7T-B{WhT!q4VP zdRyVPxsu*mxOJ|iw-j!fE9uRJo99aU)51^ZN_tb_rn!>-r0|oulK!~xI`k?RZBO@0D~LzHP3hPT(D_z1LE=;#=oh>K44C;P+bU zW_8}0Y;H^9Sb7mGq#2gXT(l;J|@Hm;a0GHeAw$h5qwg?C({s2Miq055?Y5JgsZ! zPeb<&-2$29t{S>{=%bKFE*;W_&VVd(uN*pg=t#&RR~USB@P5c3_r1ZZ2QL{sXYd_^ z*`Pl7>cO>xiw2Jx93I5T$H)iB+sNz5uaK9LJIHsEuOkieHRL+-G_p({P3}h)iN}fm zA$~^OKzxw3S`dtvW~df(a`^~${yy(@cP z(Yvts@Lsa#*`7yw?&6O>i$#reciWof4BRp?u)xW+Wq$Kv|H;w zqkB#FE4xqbKC-)jKiWMHzaPI9{~mrdehGdK{ti6Db^O)%TCia(!jHm-ajf`Q@qyxP z#p{b-DP9VB7ER(zVm+Lrs1U~x`$LY!CkB2!@N>wpcnys7k3oLLHw;(=uy=S%ka`cVgCSOUu zGDlzka`II2)ErG$lBbZTf@uOMHMqyF(^lWgXw{2G!WnVskvWP@z%iq^^cPW07e zjjZJ;J516foug-MC#z&NN6&bUq(~}9x1L2-$V!fm6QBXhIlAHZJU&N{e3(3rJT6BMKZrb*JT^xUK8QSqJSInn&jJO1bdC~B z$)m`la9juww5k06i8(XIk{IC*#nz3-^IAP*%E z&Cy2>CJ!MG$jMIKBZoTCrjPR=Li=ji=^BM%}E%F(+Qkq43o=IEV+?8Yf^ol={y<~5WUh+D!hwRDGi(W^T$Wo4e>ISl#?9S0o43apB=jgcvStN@&`l0hl zjKp&Ey{D4}vXG;1`#RZ0cID_>JmNXxxg34-4aBp=vpM>v6NtYNf6LMA4C1fEUvo5x ziD!ssax^#*+z5ZkQSUb5&%~c|)Vzv#ns_=#jR%RRh^KOtZ@{s`Cv)_*uO*%!p2*QN zhhahSc#fWNF!31iSdLEIL_A77nxk9(O8klVQ;v>bL;R8WV~&oULOen|lB4U-BpxOn z&e64p5`Q56kfW>4CVo%+K1Y}ShWH)vyBr^b+sPkE+)LcM6D5dyhKgJ4@{AmPtW0hvx98{|o*}o9+j8``-y|obPi`T%oO;+)1t{*XQWfKy;b%GeyH60e5x3>&uMZ+_C2q~p zdotn{;+7n}^V`JD#LYQ+`%T18iJ#`^ZDYhu#7#MR<0Ihj{7H`f;CkZ6#E*0IdxsM@ z5;x}Pbw40}MEod6zwtu?{^sb{P9}ap{2)iadJyq_;`=%J#fRW9^?&E+XWs{Vq8oDb zGK{#MxIRZOT1|Y9_+E~F>O|tZ#CLP_yhn-e5Z}qs9Y+)2Ccd4cAO1RV9dTWbzViy= zTg10^MZZaWGe_V43*sBZH*)mf?k2uYd_70sd^qtn;%hnj`e%r1iEBIP1E-9TCTVui zd!r+WYlv%d)VmwDd|%B`{R6~Th_B?RKoVCISLf(!*AiC|SLNuodxs%xFSbi zaW3&$;UPjec3-;zBPxGRHq>A9HTcP*-8>w6U(PzTQH2_iK8)J0J9_ zmFX&7Te?IuraB_q--eo=#OCPnP0M(8V(EzME!jA_V&jS=nqn~PMsw#SuS36=`FlP~ zSf{6+X*v7Qb4!TJMLW5~cXC;+(zLKDUcPL~tJ+-VX)pY}bB80oE^2Xi(a-11-QnkU zBkrm@xhw9*UDdY?YpMrf58v0?JAXyQ-=}UszZX>I%-=!h;)uVO?c@*J$sbjvDRT>i z?e1B6lb`F^J7;L0ecvYrTffPiIXmE7$ZpZs=By9(zMYI!s&r+Q$y+P;%-H1jSN6^r z;^);N>?T3MrOGjbH)T6kGX5pwd@G9xj8Z_sP^btv^c(;^XgPNXVTJ7^T!E?FQ8`iJc6OZpb9r5`7gHAu|ti|iruRH6klc-%!EzuNRtDSw~ zxm}2_-Bij7jv7hBmG)|RTR=0TBbl*wB-6L8tpy|Nx34tWlU!|?WUNa^7`e^Y9+b;o zd2A_{w|yP)_>OPSB*&?zW6wTu$D5|{yS^;xtCp{5@+?Pft}R)&boG`E&eoIMjo#uF zaavKAZjQZ*u|!yJkIu~RUfaFJ?^`^??|+QW#P76rKW@hx5x=`B!$cZ0VzIt1lzwoK4V8qu0@X#>q{*UnnLw4(#slh0oJXKV5a zPDDILpP7lrY0^AuM>>tikw9or>S}IOT{WR^S-esfgUaT*UUOD%Uz^q7BZw;?uiO@e zQJBs+YV){PIktH8ZbLkVFPn+SX>xqYjszyiwj3uX$WbRqwk~nGz#64F*JZZ~=7=+% zu3fHq(#FyHGJomBmMT4>t)v#W8@xSL?p2J4H}fj=YwI&{I8BN#*%43Wa1G;5)cmz8 z8x!lIjoX)27rSfale95NIZA7?I=;LzK4NV-%l6m1kQS|#8?DXZUWJHwGahX5dG$;j zPL<*7c0|z3)x0Z?_Rc$QxAW|+S*y)RyEXUvMb~+V2foqzy>pk@aMu&lle6Lc9U;7a zY?r0PP-l%pGxch*)YHgk&)*SDVQp|H zYqe^XiMTcKrWeUtYe{(KFk~kSx4`6RQ4mL#;hu*REGb(Mfl+sA%kug z3Ka2{*2Nnq>r2hu*deegX!M)Q0|J_e zhEe5Mk?_Y`H=BtYYdZc!>n2sHmwnF+O#I2#&59v=evCf_NN;bWP6N-1i9e0dex_Ps z=)mC>1^;vFLX)aAX)odI4F3yU=otWkP-U7=BFV*{fh&D|KtemQ5{39*TaQ#2ilR*} zwlw^2tqW9*s#0O5DlYykTp&tSU*jzaf3Ed-9ZYd1e6rrf-&|}}n`0Hu#?J<%udhK> z>#WVHBK~i!n+-KIQV#$3)=ikD7$I<>4c^ka+2l-{Gx4{!ZZ;{p%yM4j<8N!-tc0Pj zX83;q(%VN@D`ky$Qx<<+EWK+I>fuegQRhYcS~QGaqVudM)O@DK1$yQ+-OM3gJ3Y|2$t%`z|1)r#xbPF=ILD8@g8SQ@eDSNujA zL)Uu@T|Lb9WPx=eFEKo-?xs}6Ng;fSz!uUv{>dVYjh?6$TCwTiccC{PmV`+)g)b$@ zn%MAd+fW=`(oI+QH5CV-*%;^*{)kX;4)-O&5H|FsmW!x=K{lMN<}> z&`M0xqdD0xJ`Cfy*yL6GbMVTp0(I6`TQd2FDGksUrFXX*pi+8I8=^|@Z9~=4``Qp) zdVd?LmHx90)k_~}Lygi0+YnRwP#bENK0FB#9LIW*nF>+Fv-qz-IE4W%)hI`fQ$vmM zi;$G|*A0+{9+%C47gD3DHY6Q?2NL{{8CN}CHbjl3O)ARpTTz28u*9lX)0A?`b9UXX z3!J8|DHjPWnQ3yS@b9J8Sk3Ni}{y3|#e=bB~KF{#v&)FicB z8Gi!QbzvaUp!xY&RMZr%z^h7`W4p4CKU3_6HmPpDrd6%KW1Gwe9gbS!BW=l5N*`@Q zROy^HR4wgjLv-ofHdHHptPRymA8$jA(s^x&DSe_1HB0ADLWi=MC}f<&ha#1kVU10r zSh92{jcEMysPf0jQr-4k-)kz2A!TlZPwQe5=~P&*#CEFNdbOe8Uqn(lMy)i>KvONb zp(%V-wWXv&b2O`@tXK7fNcGD2rKtTd!^fXRPmnInrm^91oX>>;{)^V&NTs+YtFB}y zl&DpssBFgx&zn{vvW}u!tf!Xisl_!cz4$9MnbNDTZPleUi?!OIAI)bi=y}ba9ie}+ zEu>26Q*DSUUC@HCBABR&KNn#x>_QcRe}!u4!pVCFmIWi#RLxT8iYsWoFUFyu;h#m7 zD$u2i+GW*BpKe3-(#4Zc_VQ602yC;AzZW_bsKnAGZ4#z*X&Y*mE}JAip&8SPRE6z- zmS(zMPBO}k8%DX#*{NOa_+_HSMU={$x>fi6G^y*n zo=5?c7*3{@c|{dPMolX=eo?EJ8+0PqEBH+aIo1+**3j#j%KKTUn2E2l6-m&7z@{`w zku`&6@V6sHSg`RQp`LRfnCB9;BBqgR#gfa%pe(Zx#ths$$WL6qnsS~fM}DZ&TvUrS zBgEf{xO{n-vaS>~W4|t1jY!dS*HTXhOLyl5E?rQu%!&bkV{O{)XI zvZ&Z~I9j{RzEnbuO%@99Iv8e`PYU^n8r_gnZ@%lekoFS z3;c5G%cfBA1d$K3i1wj>N1_onL$_hV*D110_8Fz_Br$#;JdPE!nhO&Xy!O~A$}*9a z)zt7@&EP9$O{@7S#R-XKTRsdZC&fR|l1%-iU$kmAM|l*E&~?M9Ct-zmWz$l8Bb5a! z7E{Br{m{p6L4Cf*u$-Wr7*G#jW`P-aVj?O*!n3ZKHay2Re6}hjb=I)v2~T>L|PPhy_Q)ZN=1O8R9%-S z_DvX`n@VNB3rXu{BgwoeBG6q)J`CY zkyuYt+fX%Lt_4i-UZ90>@kLZ;-1Su+e=mA`AkZ7BC^eyTNBB3;n~O2%#v2ecoY;&Q z%5+_7CQ_5Nq!g~B8Z$5bBown{rbrwB9{Z66lq%QPtuYb$ol)WjOs+ z-`N;aaS*Gv?`NM3pv$&|@b~@w2Nbz0gZrB8pKJI|w#}Ye1(ej3zdNa>-kE!yW>H)i zMm2k$hFN$X5Dl_S5Z7r-6P-|m<;l&h*_jVrJ%}9ENOV}rY77H&l*3juQIbN%7lME_ zDA=M&rj1{SW`thRsfMPK@c8!-dbA|5su$5|Q*q-cbtRXJI711;EHEWOsu(V8Wr40= zfseg#O{X53_!UU$52{}1a*~9$grdvfpF!LlqBnIWJ zR?*Fcpz~qr6zx8*48XoVzmiWag&w8`vs_KWuR(9Xz}WIM;^Y{&7TQ)57DPTz8L4DB zzAn`2HfLLzkhnEH2=VuzDjns=728R&2E-Ydu=R!IozDwZSFb7o>!(HRqZ_cAglD^}f#Djku!WVt36YtEY1z_^DlM1UMkbnuz&2!C z_GCxZq>5Ip!P-RELi|xAjf2>V?s`fiPHZ0bO_C{SU`z1v?}N;+p+pJf4A#+x0F}vn z{4eMcjMs~I!`)#ktu{kh^{Q0UP@5WTJU@x#cBCY_NmDW1h!4W1)TDhWt~XUNleLP$ z#WDWNRxeTvp7JBB?#Y>`NyU3WhQm69kr|1^D~1tCsiJEoutCE57wdPRSHlL&iw#)C z04Zt8K}1U_E7cRLjNb+;8*H9o8mfDd#P zkFlAs+H3@150Tuu?$t6Xu&ZpS(P_qoX~d=>EUTd~(h)e?Z1}K+3}LDLWwcQ0a}`mB z&Y$qbKL83{`ooA652S{P-`eWkmXi9Sr%3`OIdaA~{YF@+MLd2UdVc>X_2QU{xY)I2 zC`jO%DhzehS2+A4L{Y54>h1y5%tKDdD>h|#TI};7*yp~3T4sM1F7jy@S2;DY)T+@i z0$8&`n-s)W73XzfxMX``>9eh_S0H4|VSLusY~P7scmU;qqJ+AvWw2rl@UNi?bZd~B zEQO5&aIiqHN?=B@Orv61w&UAT>rS9oFn)UWJ*G7QwcU%o0)9phZ{JqN-|8p~E|I$Qq#$ z)f~=}G_Pzjsm`cwWa{`=(QL5*Ts>MSRwdrmBS`@pD`zl?%2b$8t5j7tp?#4vD8(0L z{9R}wD5PL>LoWsch|PzU1g2fs>@Vc2QtYVJR0HWW9WaDKe0$yHn@!$HI7L@=&Eun} z<~kJ@e@kmhWK6*W|3tC9|39&-3;ylx&-r8hu>F5&*C9M>x^$?DwgI+XUZH)xuKTb^ z3Gt7kdJW6G?kIu}))SkTB9C_FYMIoXwEDabgntXT$|vOazz>WV{dT-P)~Pz1P0`vU$`RJ%S99ZYz1 z#NdBIGitvsW+oMcZk85*2jVLkmTH(Reh(_XfL;ScCEE%_E4Tq!H4tz6e)h>=Uq9G4 z3|^!vm^y>*I;dv8*Vj5X0 zt^ijT5>t$S1F7GGQ#(uJu$;)U%z9bi2x_Q$zLtSy8*Ue~)W_css<*2k;9o{XE`Ujo zZ`ic}+;xUsrVO{7GInZJTv>s*0GHQUI}9Z;l|%f;tufgI7a=Hek}ZoVrAtghc101) z?r;s`1pI9Do?+3DI985$FGyr?m0MBia@YZd*zs?VcLQxhqV~o@miSL zx-Ywdr>CVaO-@^bM$23 zOC-}z9Nv&+7urEcOYd$A%E#}A1q0Ss&Js#iRGP&ck}oe^}f0C++Y>c@Sv z{lrXL=a%4!1cGBsj1TEPhrl63 z&x6|zUfkboq&hfQwYmu|2dE6}ci)31M@+2&fP~32ny2%2=9^|20!YdlOtokN7Fov4 zz@cM8Sc&DB_(zc>i$WuSsTZDG0FDn%u`@-KlEh$TCxsXfQE?U6wfcG*O1566IJ==L zDgJBpqWu(2acXKNDvl_Fo1T_TaE`*8=7kBTS+IdkoWOGp&njIuDVqb-%wj^hMtc%? z!vbFbq8PksP_ILw7iU@(qA`+G5`0^=$}u?Tb+^nz3&7hBv^?mnQ4EerRzju`-_=DH ze;a!E5IeP_km8++VYyvX zploQI3>KBlR-$^{5dExbb1sX&2imMybVCu^0Ck(#6XGC+z>qM>q)4&+L+{$T+?r~3 zIU+Th37B|1KjEq}XM!0~bZlSKp@Zrw^jtJBC1ma^yu8ANrU1@}q8ejNZ~{Vk!<^=;uAK#$&Ps8Xxo+v1 z$y)X(Di>wo==})laW7R}aFk~;8w)izO3R_H^FpLGBEj+;y>7)`m4^@oh_M4=T&sB^ z{tnc?PSDJ#CP4qHWMXVbu-O3r9F*aMvpfo7aDXROF)L>fWb0b1&N|? zY{JPR7a216`aNAsT~N%>tUYoqEpv&Ce;tW>n3EX=29r{U-sxx#sKIZwl&X*^O}Ami zyexAbY=MCG5XFmST@M7c>1wHJ8(Bl~Qd6X2h^m46$ElK0XZ#u%!zh-I4Asd(Sl4ql z#AhU$V>Q@_YZiYGlwiN|U`nE;7}oOwk^&c}Rk!T&&_8OK8&?7~wqiY@!TJqV#EuWa zsNJLm8tkZLtKl;~7^aasU6ZK_c=;u>!ZvCAqo_*9Bfl_25ri;cVFx0Ht}Ij}O^jSu zr37BFz?B}9L3BV1>;BuJWw4U(R%1qNsm)EX#fHXIv5XbX;! zMxhNGlq?+h`Bvoc1p!41EqOM`3;w6FK(Bx26g;)V^z~@ny zIHY3ncQB4#xRTdg%&F8eH1Y`SSsLKpRzd#{+m?uzi`8LS2Er%W0x7n)ppfu()U#_7@7DpEO#wHtLg06lDEI5QLJpE$0wL z(7x8XvGZrrRKAD15Z}7zC&-y@%vcn}tzuoL=8tt3sM1xFJP*shW5%%IPO>b3=z}CZ z<9oX_U~3-zJN^_Eh=h2sWff3GDheTDkauZ?N*m1@Czr*lo@A^aYo=(6lq~CpA6TG# zQ3G`wFr8QMk07~s!&Vs-=;P@6es)8%VYC~O4cp)#V;l`X58WUfNj76}dWfmUK*$v8 z+#XDT(2wRRU_&vXQ*&yj+p4OUelc0;qR%;CZtiaH|2KBQM*#j#f6iYxfOma!dl#Vu zye5@-OR-o6|4M773Vqu%f;bBd&U7>IjU9R2%rjN1IS!6FzClY`rZWl;8qf$bKQ2dE zivOfl(F6t=%r3{%Hg8tS84V#}Qd(Dc>LM-Znv?22yzUy*Ul#Db=7GkSo029dk=d;A5F_Qf zi2<=VnaEg?#-&XOvTCdNzabvFg)+Dk@b95j;Gqg1hEYI=0wu zcBo;GO~Z1-P9VGtwTGI z=QA};bti*GMHt3b>9?)UgW+aFk)fNT<&y@T5?(jd@Fm5oXcey+gWFNWKY^~2ysq%3 zA;MIgxn}V`;Pe2YVc6ierQK?z>NUjxLtYub4DoiTAZoq>I}_06jk;_~w$Ei6%y4!S zoI_}c!{#V3A<~OW11C&o8&GA(2k{4!KPL4|@~F?Nh*7;F--rf2gFJ#pN!p__abniMkh z!A?e`U~K^pyd0*Yx~VxeGqI9ThMlIQpplX&Jh;kTXgocHUK*Iihfs%sMoB<1q^d84 z4s?OoiNiov1FiIz$w6ImeYXyt z(;HAt=LO9u4t1^`SqxvLJsSUMOF*H_1`ccqfSGv_=d%WFQ;ua;Y}>LS8XJ@bDMVJ> zD8t6l^(`8d50r+bI(`9aB=CFMx@Id`W|$nT-=q@El)z|LLGODHyx^=Zh?^=5QGrlr zY&J|)*AqDf{Ps|nfhwO;k!z@opn%ecR~?c-fURxDqMz`!W<6?3Q6r1oy4t{Rhr%#2 ztEfTjiX7|(!2AC$qz8|qxuzI$qGLD}nC*R&aT$}-k~9cm+hQr2z(W7K6)}5&qE&Sh zLKg$kw}n76A@)GTKaIq*pr(eIYz2~C4s_b~6Wd3fuA&8o23yGxMy?2vglP!T*PlUx zD>2nXOv?CIk>n0%8z~>sPGsve?Sp~9aOI%kxpEi;41Ncy+Pp+(RaZ%ZK$c=Zl)cj1 zCmXsK)~paYXG%Z^MeQ#(G&k}BJJZsttjo0AbRmiWN*ItT6_sn$l}u_vASlol;2=h^ z1PgeGja7L~VXClkG#G}F_@ELQlIxi@mxb`~M0cXdic}N71k;~wpLj9{dVQZcMK3J&reA%M>hRp^(oct)OHmpY*iK6rP zH&Igz!z`tnneGYT8&=ae&Y1T;ef#WI?v?uC{#Q^s0??hdP46{TH zAX=oxs)CNcAN7ubh^X+{4MO$da$gn)eo&KG5x)lt z>na#9QzKDUS+#5hkpY3q)f)KbO;BQ>hk9wVY8Y~i&N~3vVGQR)Tqwj6SQRKg0Dngo zg73(-oV2B1huKhsC3qIHHt%G1D!Qdh+cj323Fk((XO|V*u{h6nRYUR<%}-R*XM>D) ztVE}1zHH-PZ?US%VMEl*RvoMaNZuO6=lX%Ez-kY~&;*`d zAL(*UqiXxCX^9M-#tMW8LXe^Zhpqge2{X%&&??|?HPQ8Y62JrwF=ByKk0c|o0;cNQ zL56<^DdzqtpkkY?f`JXv4jsV6b4Dd0N|~iHJ~hBt3hy{f$g&3PaaAu% zRXAv=f<4L)GOEJq4Oj!fqZpWWo&iy!KY->YlNq5_MC6QjXTS0MpMCyMv^aQykIl~g z^S=u{eCl5$V370Fzvyx+@MY&qcGjT>%vVpf?)Ja|vZp8MN)qkBqO}#_xIn2IT)h!g zZCW!mzRH8`I|1{6tvfXjPXB0@%(`BnaIpUV7;5$7RW*`*t=^DRMaU#ag+sS88(MIN zRJB6~d=yUFK&MuK*%K_<*Jx2Hr*5X2v8*~_Ov8C$Gt?#5X-ckJVN6L3QXc;y>ZUz< z4Sa}PfIo$>K2!803(j;Q^E$|5*p{`(No0jJOr7Ik?OnPYv7F723n{D|1(4I|}6CqO=^YV3)EVVapQ zn0gcz?}x#B2&{G?y2GIwiq+6<$q6DQ@x??C@b@8Jk4k`3v#!dV<;H^MWFiNV$Zq7O zH83G1u(taOkb&7Dwp}ZU;3N}_VDLl?JUYojYpZvhRpv#L*SRvA2^j>X_z>*_;wh(^ z$2u~Xl(mp)EyWi!J)>MpY?+I|p{6$_uUT)hv7uREMKC$oJQxtqsc_-(p@UH%7|fOQWNC`OTEW}fE3&i5}ZnPA6+ndxPTr-9>Hku;SKSon&vF0m99 zi;0}@qKf}JQdJ8$I58?hjgxw$2evO&B=8F=mYs%Pm?;6H_;J=W6PlOtOIn(X_gk^b z3m{xdvv@d^6&oo`7nOik5|*W0h@3a6x)Z=0Qn%m>TMZxo5bDtLQrH8-sa-bU!GCDT zLFsd?&V}`44%oLdSSsYxmW za~v%Ek2fKlJryLVsA5#~$P1OstUA0GTcRxKj#Q7~AS3v_p*?#c245BpS^^grJc`*t z%e>UrRqRtGm1hI!ttIGN$ob#nih}4QA^uk24Z~F*c6Lw0b{9oAe*?XEL12TcixmVG zobskACly0ABk<{HmZQZGzn`ULMKtOmoX)=ry}dgGe*_DWV=#~6C^6$>7{tdxP(P@k zIsLTG%+8g#Mfd!lc>a$)|93q9C!SZO_eLCpwFi1vKMe;&B};Kk8cuhh&9B{iIkfes zk!~EOW|g;E=vUh7k%PD>)UKhhv#fp*Qr_L!XhGss<}LcxbTch)uWRnii+Zwh9JK zpc&>PlNZD&^iz!kAE+vq{x&($^okzLB0)PYz8?lQ_;h)r5xAk2fiWo!0+?oLi_hTn zzXAFUcK;k02d-)rtA${BOamLNgeW4Lmh_*+8V>3J)=06W< zl#&YBD8(9hevyU<;@E?gEWDrxTqxj5kD=iK>(?{3#!$XmV_}Ae3kyuC0w)_3Rq{B? z;evWq5d+OiVoTO|GjJTM2Ei1KG&0KgZ_xt>8L*Zi#vLoKxR)5?s{(vvM{=TA$y!3n5#S|GgbAtBH~A>`;b z7&y2N8!?ay=E(SkAYp7?1l~JM<|XL#nUp13=@V_~_(H;Yg36{orTP$V<|I0JYfYu9 zC@_nvup^4qNQ}Lh#XkWx0(*ND@^BzbhfSu=Cze}!&tysS1BlybnsDq3PR3ghpCE~m z2l)eF=LLdT03j|F8vF#Tompl*%wTzHHFa1#*QJ=}B|WmhWdTt@)foR^>pgM|2^u61 zqD@jhl@$Nb8z;3yYvH>44_H;BNQQVFIKc%8OC(1xT{FqlaB69+3^C5C5qp`$d3Nc~ zlM9c7BNk%4QiF4;(1+u84O{n`HXML!g(-JuG0G)!9nx^1D!+t!DOu44SyKa+&n)mx zU5Ty@z)mq%#mJNt3;Ypq!m z)$;!riT>dbGO?YsWc$#cfImO6mIy1{2WSu=1k?yC|#VBLsI$LKfSCe&9gwwpRt?!1cjh5Uh z18wp>1%kC9%bw#K5Z!DmrLRuzr*RXkcW`hGObQ2OUXTWn%T**Z%WOR}-6W=^$@Cu< z1hv7gf1V8`lp!yhmr5=Ki?Xel+hNai9l?}5QTI#{USQl(fRn$-==%TEPY+~pl&FRX z7E};D3F(TW1bp#{Z>APBFkBlBg-n+uPIkJwhPI!7Cedo|o_0q3M@t};8RGN72!zD- zulLikXzlc~PN~AApEa-6jMbk)*D7VowHcpn5%1~lT(3bU@8>D|>Gg&7oc5&tDJlO5#pcEr1o z%+g?^q{6EjBpncWI130jhclmZA*@%@1CN2EqiGdV8v*MFK(K|F@n9jYs=NoW!VwFP zTn0T79ChGoi^2Ejy5Qvi&+d`n-JEq~4kdM=3K?WmUC7EYU4eW)@ai7HQCK@l;Ak0O z`=wRBtg|#1JCJ6yEW#K7vn}Yd?$qK%VbK*kMgOiTJ)(skvAe{(jt(lUsD;fa5-5SL zgk{61M{t-&@l`!FeH#u#eh-bW0u9z<_`g_(%-`3caB{N22US1QG67VQ1pXTk#{!7< zXLwm*)J%~1G>{XBwuQhJn^qIk4`V%Z1;0vHIfKV9M)K?D{ghU#yqQUm%o;8fML$5f zeNewRKv5wZDzEUB(53V+Zs?(*z)>C)Y%m$i4r~m=0U!vPkp)d-BcJ#5xb%*;8P&`z z72G*q44-ImXe!Yq$UX(Wpl_p!EU^42HMNRRQ)O3)Gba%uk*g-YFIOO(K;l%$Rt0Ip zG{|DL1F^cmkK-tgnZSw@stU(cA?*b?B-nb|U|FuI!&wEEt_GeW@%RtW1M>n1Xcds@ z(o|g$B1=J&!;AYfIKAn_Ld2zzaRV9TLx_Qg7CefQcvW?pjRvGSVkEf&He?yxFnSG+ zYl43!fMc$RamWn~Nn4v)4E_VXWZu%hvo>Fp! zQh464S5-%@nedr4i18Kdum2 zklu?E3?2?iIwF|=X`OcN+CT*phD8tK=U#OWoqdYE21~E<$f}xpVtd!n0dHY~E;RcHa3sT*BoR~Hg4cr}U4LlaVK+QZ1 zhI@#rwi1=|4LI+>I)^^KOSwCxJ_&`bL#%@lL=iMpM`Yrp8A=c(4ICZ;Uh*W4T{s-W z)?5nYBtUjv$`0X>Jg0$=6gpQaszdMqB%?+y%3(N7sfkGl=YvBS+;9@Q^x1aR!6l`} zJ_}j)px{AC*ONq+EY^2;NcMXUYPkif0ttZARE`W-FzGIs?z0AbR;y0SjPA*X?s!f> zgF5h2{DaW{u^!uK1p%M{lS!ZaAh>um2F^2jyuww3EK&jva>g+s9AH2_8^RYdzrqJP z3*pT*2fVltkiiPH#|VvtNg`ja=_Zs|Ae?v%pmTu|+Vt}+n&5(h#IxsuOoU4lqJ!3xZ>qcJ=*4{B)0R0rn@pqIkI z|HOh+{-tlUq}_E$OgpLx-i$!uYi_Kk;Fe`A2eMZ~YCI$eS(P-Mw=y42(u2RxGE>(R zvj4;0m&dzNSBu}9yKgOClbZtXJ%7Is^$#zZbH3+% z&-a}3{mysJBA3u?P6x|zknkD>A${4TU>JX_y*9}z_ySWvt2vMuE~TRep_IvhlFc9` zC{xNeQ@jKcVbOFVmmL3^vNoD*JHZI_^8sP%*DCC;&(-T#BPUnN4M>lGmmm<7 zU&t3rK(Ry_L~j7WjY&~V`Ld%Kle6(dz71UU@~1bcwaSy=t7|b10Yw619s+2mMafXe z!%(sg>ivv=Us2p6D&RAGBb!O*DPZ0Jkcue{{15a?i5SW)%b&3NlDj2j5ABax4k5S^*a)$XP>^uYsBa zQIH*+PRXED69_zPl%x!(;tkjGv0SVW2Ok6#3#&tEV=@WSH^3_cVDHZgn}_o8d_x4y z7UI!ZGST2mvAHiDFmlb|z(MF^wG8;uTtEtfwq}$nVE%xy={OckftVL92r&hcS}Vwv zjG+NO&x2eP(NG3S`vB<3RJ>Lz!-*zhtQ4ds24YJkko6Dpw!p*VK!_U*icv~Jp$I-F zBiUL(7X#%Slhtw=1WvT{3v`!*Km-^lJ|2zfv`{7{C!h>SqX7X2P`qjciR&OizFGs- zpvQj(#?;K#n;^y}En%|K0YaG{@NQ`kGFt$Z(-0{cJ{9JTa3;5lEe zVX~A3#WO(95LB*fKs)DE$V-sJ6J9TT-mDWM0_EY~Xnrc~Ef)&rOVpU;A@#$rMOALYSk>5E_X?0KUWoOM9r zCKzS|I7}jPt&oU`Xf|2N!*a2lE|mFdqMimBds3pT8+#VOO$bml#<%`FG?8v5!P3;@ zJP2oq%Vl};H?5`COx5{#GnWHtvC7bgCM!yqT)e;oCNpxr7MpyotrCZ3c|KMbMIj2p z6$_<0$WnS~*`W?R539DqL%=bi5f^G;!)U~Gbg{v3AM`&O@vAW!> zNYP1f#8%iItK?!J@I=v^dQ{9N3=%lIf%ZEBC>BsF02Kn|&TFN3Ixb*&Ia9(!qf({e z>x$+a(*$4Ei$)!v0?|rIP;9ggynI&Df>84>fYlrsZGupMu@N39E(~%$!Op!&mjWk! zES(1#W;$6{1d-yPnn#Y;HjPNWnZYuNRMD8ugK8tf_?HwChei{LRHcxK@*q?JGd7G> zP*oNDzAn}%@U=!GUqeB9Q9cJrP2j4kAXq5_Qj9_5EvN$n7IRHJibf$2?NX|MXpR&( z-Gd)bPJj$dQF-f##_KQ|g+YB$QEnKF4Vf>9`8X(9paVrA(q(@9dWDCvOi6KLU|H5= zQ3y0@fP{E0DQKG@L94Xg;h@;h1u2}}rg>_Jy0@QM? zfgK76VW9#-sX&DXuxF;gTgW|NkT?FlApsx(<&*#7)JI3wesAs7)9_kxtzqrtnupe0 zyQZ+_)HU-9k1X7U~FTQ>PB-q4f|L|LRgO( zDj+V0%V#l%Wql(eG4?{ZUiU$UB?4M_TN}gFaF|h0${~`rMj?I9()2?mI2EDXWDRHP zMBe8xLnSH{&S48jwlEqEsP4i=zi4%EP&AFX&_oG#SDD(9KAd)A22fyiP<22$-TvbUo>)ASBWs}?SVPPPwECbs8Y{4~g*HYaEV~V|J8i0D zm;&8&#u+i=0j10+Uy$LrXf2G!!*z=xSjWe1Zes}but~zIcttdWDnoEU)GkG{RyZX% zCEk#?N_N8MPk|Q@ZAP;!tGFCV8#96}WF^X3T%XHE!ZaF-m`Ys5Q}$%yfdUEMS}k%+ z(wx<%aS#~W+Za5!!Q`ki;_;jF&0s*EOF~{y^nn%WLlB^|IBAWG8g+K+$ zFbwjKz2*@j~&!ciu{lx16Yv#t{% z<6miGxJyy1;1K+wvMGc_A$KzYM;fj~+E>&Iu-?If3r?5b9i}b#*x#2iaHrdmaKzv$ zZaz+N%|v***2ygU0ww8-pQ2aswyB5M!b8i~fwg!WIn#M7tS_!w>U$54+hW zZFbdhcE;7lpeUmbcFB}KL1BEj9*dYICIwSVu{aVUB5?{!*sPukny*OH!8V4;wq(xn zPC9O912)Q|Yv7@SW?D=gDN$X@=h7cb}6p{pkKn1O!g(GGcnP|=(PdbuI`BXASP8ix4TEvBjDO&=yDvglYZ_gOL=3tR4`%M{C$W^&U#SxEr?b@16S0l-I zB|sN46K19V-}vbBBl8zd|8t@W{&?fx|Njy=f5)E8wBSA$y#yWI$Qu$3TRrD7P)QI4 z6~t=pBHLi2CIe?Qk@2)dMliug#bg}*QYu|gI2A$J=nXpnNVHN;b+J+4Tv1djhD+eH0S9s>Z3ef+$XN6Qt*c93yUZm@ zelu+6(5zdT-$t@ft_RG;SUFE(G4TC25YL$kz6MK)a>7IEEF~i7M=_IVMer7UJ>dqS z06Iq{MxuPl2x1v3pc9(MB%7UR#TP_bCKo0VrwGLjt!XNPZb^Q_>5&NTa5^H9B?ReM zp;ttmik3h%h1O(CptqD(r%W`#*B9}qNTv`M=kzx-X2xQMz;~*!;D-c*-&PLgZDB)@ z+-z{Sm2SIRLGzh%P%1iY=j{QiNFRJIF%(GrtPpf#VaagIvvak zi~#)umI!K>beWd2el01q>`-x`TY54n{D5zM?lEjHDFumAsq% zFyV4Dr2-}fa9Yro45l>Um3+JliTXANHp?Q{Vp-Ae6|JqsNT;zyBhEN3DHdmxale=m zb%LXkv&X0=B^9tJ5;F7N6c;v!q9z!p!Uk74;VRe#r;9J!itZp(m&k&-)~FLWhh@VE zuhr3%2MGrSSB=x@&3LcHZHOiXmD^XQ-ZsSa%ZBKL5*Zss8~JDq;k0H4VfVWVfu*8H z#_3uEP7_>BhO_3lQzF;bOeS}Tbct>%FX2t1Kvq;st};YL%q#A1`jrgPW|2UEWzr^z z=2FNVXJtoKrn6)|g;#mX2tLiyS(>W^{WN4%mVYB-Cj3^8GhgB}owK}=Xt8-*>2s2&9|5QRiJ7_#Q-{=DRlB;j>;Vw7W3P8Ec>k3Lvpdhjw>@%vacJHrs8x{2vgEtKbDL|nzyftkG)VwQs? zrb979)t3rWDt3mM8F{e8Udx&hYGNy|$N@3FmYJCkme^~VnW>vtKfVvl?9~(7vJW4J zVR$3jvDz4HG-x-%>R;AF>R-0(JG~Eb*FAzV+)Bu&^+E2Tvl8;Dy~r2_sn}M2=|)z5 z*|Kk2ALK5!`yp@bgWSb-Kje4zAftLj*Ufh8moBzjziipJr4Mo^+kKIZeULlZ?t{E_ zpJ5zDfs*JuH9)zE+lY3)y|!+Q)*IA7TleYvqU+SaE1~Q9qIVwI`%>4o(YsW<&3u=R zD?hF8nc>!bSYPx`xwomWfR6S>@05HWba5Zjdo43Nv71;wz7Ndo)e}2=-wES_ z;69k|U$5`Y#M%2c9y9|7@d;`5tC6$!9e>aa9K?mgFbr&HF>nyy^EMvDmB}y+tUqW5 zUgsrPchC&Hu1lc2|F0iEb7bv%*B-j&hijs1HZ0t{ATNy0e|_FPe&*ccbJxs4vwxVq zWfq@#aOMLu>!v@u_G{CpPCYR7{;9W2-ZYt;_{YR06U_KeK?~(ykL?@Vs<}_|KFzVC zH;!gT{x>X|ayL2%|5Am99YF7WmfuwzINi<#8Km)QxPH@xx9%#a_#NOg;htytUD7~) zJLNNoUo~7mexdxX+#rHzr?dwU?1Ae;5H)Vyl~oC-)9a{wCfxHZzbiA4-%g1R;#UpV zk6**qU80KLPN${jx93@YS9&17L!N57F8-By_2XB+byrHoZ>Mii^V{<*zbiSA-_8jR zlCK)BAHU#`kx=p5=~~qM_B_k)5(e_CcI2n8yu+YsxPJTshaXq*+vywB{PsNCx~tKR z-W&0Iz@PO#YsLSMERM`qW}*{e@JIiD7ccISrrhr62OBd7?%!Vh1+6MVt{p|(8>)(; zy@5F(1pMVF0;t~wAP8vLu7p%B2RA1T!A8kOc?~>iCQ*by{7%rIGFPIC0;tv(%`h&^ zrmy-<=0bAA4iG8RNuu8^q>6;4Q5TyG@3e;P`eGRo{bhs2>C494mZ)BoLTtL^3X^a( zAUh;LLW#yv#nt_f&{RbL6+jh6Ig$m@_1T^npjAIFDM3T42jO+PT*bl_tj-|33W&>Z z$D;SKrJmy<2o?eXw>!W%E!;?4NJJ~yGWZz+zrB(v03!L@G2!`WP6-j$Do>Yx>Y8_J zxC75t49^7w4?rjdGxMNRTQ(BSrlSh;onuF<(Q;JDfY7#hZ1baHvK_;FfbkZ7ESi6L zs7#vAy!{>u+(89c|Os6&Y%R%@kU~Y&&sTMEG;q?b>sGkulEA{GPi?t96 zVS0eRVn}0v;QbCEX*W_Ct1J--5eEH{7(0*09K40Gz*KBe*bM9JMjluS806K2R+Ze6 z6E8N1LQq0QeXtx85pyLWd3ALXLKs8`>SWQJ-H-`43Ya;|lw4Y;2L$+sJdDK{2MwMY zOd7R1Y-zY4IzXy1Qt;Fq#4sU^?WynBapBmWyy7n3e{zSr?B_~W|GGM?yF~sM+~q31 z?7ziDD>PNU>_F;AXC&kQkQZAaX$Wu_PnSR?v&OOm+OYCj0n;gl*7h2aikt<0QN=WB5T2xRP^VnCR~xO>rJValbr4AMT`?lJ<38Ay^h z7s|#B;e2&bh_YFO2iP6p`qj7rhozB^Z#HFXtlMMDHFGA44Z0}MDUq-Rs#2pN2WV@V zYB->ZrMTIT%ciQ$?UF13R%?V7i^)tVYGcDS!tL_H1)2HZ!|_BnZHcBJ|Egw-szc znfi_$7fkKRD*2~lm()Q1Tehf+`aGa80S(#78kf~Ocpv90=Urj|amb|@%yT&>m^_QU zs1vh`_p^!ke_^Pp><-eZgkb|j!n=L8%hYGR%eLi*Yw37TW;xy68%vfJIT>>M%yPMNwA|38Y4 zoG?-xSqrWC_L}!Bytr`r!b$UY&j;szKX>8Wy4gEsT{F+l?3_7z`g7Ct)ZFV%_ACFGdp_i=&hs8QC#`oidPB3?%ed5>qjQu zMo#zf=5Xg7lJdgH5uN~Lv-oN2XztR-M<0X{{V5RmAM6t3?7GEu4xzkd3tj?c{I_hO zeLmM_=oAbSc4t?k(|Px!&@Kc05%rxP z5q4Wi!p4z4MPQr0Fw{hZ-G$ZYsNdv6pyrXq7G!9zLg$E23`s{?jgI=Rk0C7XfR{6q ztU~9|73Qc;f7LGNsoI>Z(M~0*$ zu0}_FNgoadNe}JiJPw7<4L1%+C%p7Na50E(7qO*QTI;qv@sPEK0UWb`u z!XZyfZac3Sl1_3pI%+*|`U4@8(;Eo4=99Q)C_1{`iE-fA`ZzU6Z95*5&&;~aVV_^v zEv&|%+91ppj^iu=8$l=nw-knAP}m(`jgHzQ+6jlx9PpZ*Higb=}(+C65cvgnWglpktPVRvLO^ID_Y%6yc=YBSjwr$?dl z9RTO3~5((k4OLW$sy^Sxf&g{r3D;bnhV?gw4z*({s0-0&btnl zPLK)sLR8rAr4%|3J~1SnGghOcHh$XWvT!yN>vb!1?tgelI^osmsEv}P8IyyvJ89(< z@#wcdJS3gaYIM}bLee&lvirQj7M-sTckB{&2Unw`HvWL!Z>6lP-__FVyWcV7d;+V` zF{pRLfR|-i2Tglg>+_Xi?bN>-9krdB!LI9}I2W$$hL7I4f5`c8tI<*0sh_erowS?w zE4$&Nx8FV_9p7qn)OJcZeI`fP#)ixao!f>zwR%^hqqd(Q!-Q?1!XDkygIgXNaz38b z=%`N-mayH!d4hppOKw+va!5Mv)##}0H0%!1G)EC^i_Xs2kaS$D(NP};0$#hrI&# z4^H1Oy>mJ^4NXsjn}s{3E}u$Goj!H+qajfJ!ACbkr$TdIJR@-q(Rd?@NDdw1Ao8q*Z*@8 zc>9=J^U4Qooe?#nT#2{lIzsuB7ww}?8WQg9-Eaofh*--^s;D0C#3A8M=!VmyMwpWF z33Z<9Q5(DAbf^(3`f3ezUg=TCcfo0ms6iKIOj1+L-?2l&y?IEuW4hrCs6iVnWaNUH z?$JZS9W^A}kw78q{~Jo#$)6sEG1L$%I%##8%cEyI;dBO&S*%O@oY}BiE>8~$_f$8W zQkgOuvjrsec#jSV_wz0|Js1y-iFmVTya$GayT1!gXTVUzmq^p0tx?dO)?zSP}KHm+8VTi6Ir0Xs<-CKu*yQLcr6fn@@sfNp>9`EKZILv?{ zh&P#~-D=!|5>uj^}*gfST@&L&AM}NVpri;ecL2IhUO3v7_sUg!_~l z4mLKePCTfd$90`>VEe#e9jF5ys;c3*A>n*O!g;&l5DeC)gfvd8>3W8Qa}NpU>V|_c z7;{vDb#+0eqnty+IlAE>3`Xs#svxN6!FItJv>0TxN$zk>4QCw^jvf+@>V^ZgY7D_R zl~7j4<%n9{;%|pVOhJ?d=;B+}0ulLyR=|jSuHYD7s-Eat~ zJyfwso{)OJ+lGYO+6@O|5Qax(yhl&pIV9YcZa4^o&}t|o#MJy5yWsRj3_|Qgs7$Ki z4Bc=DYSe=U9HyRgHZ~+2IwTy@1EA3P)+G$h>SZa5eDhlz84_-3 zNVs=&!$GJKbCZlYsh;oRkZ_xZggaS@|JRN*N7mlFHn#TgHQ!uQS)*Nee&GuXwFSfc zFX#8p+volt_tx3(&+eRsXP%h(&-=#&Gnk3=Ix{VN3S1EjvhVo9YAXNAG3GExceC9m6tdz zc)7sbvc05!xo8LR&;6+Q@#{9c?*z?v?C66}(MO`sl8^u3+wuFyAE0~ncJSB=;ED!4{V419P2Zz!pZV*8 zbKBmxjiX?)Sm)(XE4QPKzvtEshko}rKmPbFTg=EmK4*OCKi0f+``s^$FMN8}+;7kM z&7x`B-nX{#J1zE%|zc~5(KT4Ne^O5f!QQdjLkMM1Kk89(1 zUP5=^kG=o)zwU;&Z~EkuHT^Xw9{-CUJni}AWjWzPf6No_ym9SgzuLC<*fxIWC20r# zAHRC#?vqRpmcIBx_Qt^S?@c;Y#w|fWPZ*`{-3K>^kS^pPMzsT%8s(UyCsyA5c<^-#< z|MqZ?>^L#;^e@l-+hZsH;rajMUcBmtZ*KeI2P=u29=3k}_-jteSDC#>b`PgV_4-U8 zfUCF1_Hfm={aDy=#|Q57ouavQ{D+4Xtj}fnH(fk;-!&h->*BE|0!e1?o4SY7p?X~? zkx8iE*4o20|8eeDetFlX`*J`1=H!o$Ip?8w|N5)@f3s;}>FbS$f1a*4PGa^R(LJ0N z)oXpZGMia(vcE1pahUs~4cL3#gPXou`0|l&wH-41C;F?`-g!^#-^%|u<>7664{vKm z=k;!fW;Awg*FS$N`n#W8|Huu*#u)()*V4+?_u5SAfT3Y zgYp&CXVmuWrhYNGW5eFx@7npk+b;ah`-``|^rzb&_}nF*ePa6${@tDkLN&(G6meEzv}KXW9rcWu{j z;Mzy>;s0#kaooGVfR3&+J{( zJ)8m68Kq{uSYL6t>+U>z=Zkm78y`OMj1OOa+)K9|auYee&h>-epLB@0=Yr3q&S&;6 zbPuOPb%tm%S6SH%tXIG7g-5QG4La`9D{sH*qAN`w_~el8FKseb+6zFGN4hv%R$PjYHJVR<^GV(Jb{!J>>&Gv=<@nvZDi6#@&wcm0=l&yCe&W*Xw!PCW ztLjR_?Xaq2-}1-}r@#M*ht4|T)=M6__s910kKO-${-HD1AtmG3pKdI^o7p?n%?^a> z5YYp=5v|D1WuqV69`LSz_-&8vmO_7-!*tpYowxh@H%`9A@IK4QuA?_FdndbwLyV{n zb~ehA`k>r4gV8G={q4^#cFw|3@=Yiad3!wEn&;-udCnzJJ{(=a1jY?49Ty z4(Kh!3+24}{;oaT+2I?1d-WZsjy(LfOUvJ`a_9Vf_G?pfFRnTAxE~!h`A_5zz=e!= z4+rZ18ohiyu(H2BcdqcY4^St3>Tib{_FQrEBUg}#FCOvsjmfX=96#;02cEdf$Lt;J z8O~5Mr|SHQvb*E15B-Wc^o7aioG<weQHvv;(6 zI1JV59NC7qu_AZJUh$J7!_yb<{ZLYm|NZ-y-+dx~&5w>7y*+u`H~vyHS8l2>dq=v5 zLs6~P8?%Y(SBy4yj(Za`Tl}FrgZJz_^q<#M?zr@Lam%J={n-~U{r;6#pY-%DX3sg@ z!y%{^v*LJk^zd&lI_4ApU)*%;WjEY>l+5fo zyL&ho)uKruEUPaT+uR-XsY@Pt?2|8PaXTy3feG^vA!MHvKX7t)Jg>m+9-DJ$2KM&$#G!-@4`Sx1GoAX{v`?xqfqdxIe5t z_S*9OPwvd0?&10sO>Pf&{};}`TYJUK z#R+}jkCx*V^6*K#6|1xcRmz?m_7CG;rdlrZVv|+9((lM;1SW&FS_mX z-}`&;jKBZeb>?S}-w)ZDtK(u3Zr^xH=N)#PtB1+V({9c*CDj~@B^-ha zxBYYPcR%%)_rK?ZTV}WIDYx}d-(j;I-rf4x13z|NeL-d2#53!>@1K48qxjd({p>gR z3-3E$I&SCR&cAlso>CjXYxYXD-8Oys?led1y&g_x9)}n6>-1czIWA{CA81c^UU-RjAlxqq9pP#gjBR4(uk?;T4ZP$zh)_r%L z*;D8qu3wqb_HcVn`u)>4I@euqdC%_KbmWV_vfT3l@g1LA^E-V1X}NLfNDH$^>KSgu za-HqrGJEd7*D|u>@MooaufO{FKmGk@U%M`L%lLb~e#VJ!-+ubPY4gmUeD`quvi91; z9rX_W{@d5A|KOKy{P`#Rk4+ejXPoZ;()-V!zxW$JIsA+t-So)pw0(pcd3S@TI5K|B_{j7z(<4*+ zXD^)k-qdYVS5JvkhN;QPhbFI`EX;m!^3=)siAN@Gn5a#VnujJ1pN`K4rtPz*%`VLR zbmr4DwHb2e@abPne|CDu%eM!^{(q|@A}lpQJEMHi>m_i5r^HvCNlTpOJ$auu%!y@R zQES6$c(Y=1v(qQEF^Z;Ux#2ZLa}5`s%~u+kBxJNk^?rtHXx*Mtz#L1)7@4vaSj|t{ z7;>=6rvfQ5Z_&A9MGqfv$u#GRY4scaY5OBJ1~nIILd{oj)EteW&90; zO_w0-CaWvq$OWwoskP{rYW||TWY%Z71$~>VhG4ei71+%b_1>zd3O?7dSziT@`eK5~ z04hYsU=vpj3Wy1ta4chxEVm?6VUYD!v4;qvNH)AN0cSmsNAD6tl`N7>*qNm`!Oh*< z#;Dhc2q;iQ7toLx56ONX?}!mx0gBlCF)z+w5>m*suv2nK)9s$GaINBiMUY1v3kG5t zmq8LrsVs`RV@uhpRgAJ!%qm5l`cxz#O`UY~7^>Nb^xVqKf1&-L$HC9v*5V350;1Cr zqRN;SBrJ*z39tL3x_Ds8=*a+mu`qdkMxQftUWl9fkM>ZV;bg77X8!Z-2c7X>?FYBE zAN0J@&)?D}4jB`LR6Qg?e$cm4vX&d6vV~94<%F>gk%=IdBa5N1-yf5qv1RpXilPO^ z`FOR;HVw5UHWxSQn+>?ergBc&?x`(Rq)-ZXAbEalv`xcVDbYSlqfoA)1tFDUG8ru3 zULs&mx#-|IL({&5dz|r51ja`$Zey?k)WcI*LBuf@sWz9Aw5OKL*nNc62qa{cKxgZe zr;!to+T3-mp3ewVDHrjFjY=R$2++DV30v9^ zdO|KmoY|!e1lluc3;BeEG#Sy6$@>#Fe=-Lp5~QFvhor2~x^EeS&{AeG$uE`74cbUVaz3=2^;>9ng@95^Tqa-k z%Q$bME9BgkHik8)FK2UfNNWuuVoW46{&bG_)(B(Jldf7z_7DPePN41j+E}~)FG9)A zl4!>$3@M7Zopq#=jtXH3g_?vR zkY(dh6NOpF=Grs}qqEF8J>K||Efx3J5IAaY^0ipq#RrY0d?Ettu#BDc(fZo7rHx^x z^rb3i_1L_LW)Oz)h|$}KG69DRPQ_DA7#FLQ))@%ZjQn!9YXlE)qJ<>lOHmOu`@Qj; zoRLI>-k#*NkR=g^X)62ceGfJ@b=H_6o+bQTK+L$Vf1Q_jqqJ#7ry z8IXfmHc=rxN#K-BwI!ihD@I&_QY9NEXtTHObCpAAnoSAIa zgOwCks?%|yZgi#6d0)iLS9Jj*khf;m>}+G$y`;V12wTubHsr%NZ5V1K6A6Dk6~XOM znogyikiifH?GH<1avOsR$!5e><+TL?v(E2P?^p}cW(w7%umIdEin=zN0pzJXt7128zdUE_5Z47hSrN(?NY-La8Z5?W+U-l|iXq(IEYxOy-NwMVV$n{6rsTARF_!rx5!JbD`C34i zZ`eb539gyASg7F(=-BZ$wK4LD(}NL)rAQDf+YNBap<`=UIf41;hL<<{aNJ$@yN&fm z9bZ_Uc@8oYoWsi-gJcqJB-tdIwl~U=x*+`;>h74M+ z)J#O0E}y9o6M1bU9WmihT(+ZulI-EMD36n~XSXpj;QW-fq$J*A%rIuVwUBF8Gl`Ox zDH*~UI}g_iS`(hs>Fb%XZOa%m70fp9P@;)Sg>WHWHN?yoZGkhQOHz@wP8r%XQod^4pYU42 zAuumg!m(gb!V-9M$%DE~oZVe>#!@~~Uk^2<@%Oed5~#7|m*RDY;pW){{bw5^V5PH$Xql@MnVMHv5_56F*z^Qwv(TWV5?&$%g2yI8H3ZiLNpKSsRnDQmIGENDLX`G z%I0TkX%w{iHK#~1Vx#J{(QB76=vWi680vXLF5r#Hg5YrRr9y~L*vNpG7L7SG;>wCa zl98rA_cBHlE+?9x<0)ZU(pEiwV=$SoYe@ofqF%&ePc^H!GvHgw)uxuu=3Y!d^L_qQo!dq52F6(r8LQ@ksR3C) zTuCwa%VS^2kCA*LgO72h-;%ynu=FnOZW0_1Oz}}fp z0YjX4uua2Z5z_9o!A_U-p&IZ6+EQ5SUaFWOBvq1A(YRNvHj92&Gmgz4xr_m44RAv& zWF;I*`e26}D#t?w9iK}j8bmH;r!1`C;F=O*82d*XgDtp=NkiJ0)8aLZ6)dK-p>nf?0gduL6vnwhW9ym!VlqnZBZ^zLb5 zdVK1eQ@f{#sqx8gPF^rcO-@bRJ8{7TH8C}Q@Aw7dl*X!=8@+Gzl2QBU+{k?+mrQ?f zc`F$GYS*6d#((@jB!SZ=&RjpCSvmVz-;*(Jwc4Ft+HGZnAopf}{bQOB4Z_#yI$s`@ zzoq%kp%#65QWG77 zuZPt1z2Tsp;W#g20`&J+KdJfPV0<-wzlXDUScYh2ZtZ{fmo=9U#_xRZQTPvdyr7%0 z(?lz)zyCcCX+AI*U%h;InB+*8+hkMHyZ6%(&1Hk}JFiz1`lQ?L_miA2tjKS_Wt-;x zgYi3WVHEtZJ;<7TA#1Bp$^OloG?xy>S6dHj4uKN@ZZgvf{iTyN?;DJ-wjR6J6tq}f zA+M6Ty?@~kno9=btF4Ek2z%I!+ZZJyd;i?GG#7832=%v~j*k?|>_ddl@8^P)Q^CHu z4v!BauIou}2Tmgxu!Sr>CC&7iKMR@*2ji=ik0fwUfCT;NTgp4*(OfhbU#)z!DeQC8 zc8gOH-ZQ_$G#3oUS1X^NFyo}%6Cj)l{MVp@gh@%SeCDa^HRlb+ zS1S)0u-Yh>+2Ujr{Kp^D>>iBYqdYc_-~v8dEA9W8yKd9$8jP=29=nAzn{76yRY|jd z=8}x&y#rmZ+V@Zn&@P!HNKUDuaO;ooEd#0dBzK#GzL1AZC5kD|OFe_6rbb++c5!T4(JvzT$Gm!jQvyMlk#H#BDt z##d_}>Gg92<*`{?tIy47b_~W>yFPay;0+Nb25(v4=3i*e8jP>j-hh*(I3MY5CGkHq zdz@zbV0^Xqdgvf&#m&x;qCHRlW{ajd7+G#oQ z$`huPT2@cr@+VDwp!TYl-{}ZZ3~dPnm7V9-Taw0s)O%8qy^e5za(i&QqPc9K4=BDXl3E##d{vBTRe2^)ls9yvNf=Z_*S7w zwf3@RuODpQEUS2rr=FYAO-%}3=n%rP~wf+%i+V5tF0HtjG zPko@Q$qvR>>mNZo?4Z?dfL01(J(YTgCNmgct$&P#4qCis!mI54Puc&X5eMU|^^b4` zoI!`r8Bj`tJhgPCCOsHmt$+5A+3&XFpnRBuf7ll_sloVa{j<7FRL~vdtjfXf$vs2?l+yWgjpyx{ai4i6puTP7m2?7QzZ|VTuV^~8tcHafj>(lPKkLxud1rM< zu2WyslIzrkRiCXdM{!3mPnp8`GA;|Aima|CwJf>qy^=HZ%Npt@5w({Z7H&8u`$!G} z%iio|zHcg1Kc>_!a#*Y?qUB|;_exIJuSh{ZiKsV+Vc~{jvXA5-u1*Rr%t6>b~VLb$@NV;=_irS6NPdGG%Vb3O!kqS4j4>f1?KxD zYV>2Ob5kG0yc%wODCenI1L?Y@5G|4d;bzq72?Jb>wt1d(VA*@qI1U(0Z=cOU&(_Dp zIAe~RY8iVpo$^>c7HVaK$@fYQd_!G1FQT91I`_pvl2gMC$7G*n$H21ZR$#tgCP6>u zJAKq3=GAcPi%mJ)#M6Q~NqJ)hyVJdrOX|ZsI8SCfmc7$gt9&8SX;Z5{158z9(M{*l za=Io)IX0y(EwF6unO@1k{Q~#4-!+tO8T=0@inVSLK&>Bd79t={{tYbbx+jI|u7aRvDn1tn$mtWK%1X zZKVdN)fG)DFU)p5r=Ot&*Gs$HkcW#Jn6pOv4 zzr0L3(LuV?>IRbTJNYZ{e{cMILjwQ(61Y&_Q<;Y0bJt%!G1KL0`}mIS z8>C7(n--KFKxMfifF2;=hlrGjfR1_bNLTBv<>xClc`@gyR!WhK(CTP1tWQ;_ zzufjq>FT#qZ}D(%*UK&oee-jki%>C5u!X96+*m$WB1x|em$FFRSj;k&reV3iWt;T~=C_l4G<|PDQ$SU1_eJypjgYfVxCFfr8}-xsvroL&KTXZN?Kp zG*EzisH+&ru<=@|4Eh#>+;1@PNTEoP8Bm+eW-42 zI4q`0Ef&DIfX+8$`Dt?WpE*a6bfh%=5$3Vk>PT6ac6xfF*ldJG|b z))LnXNYE>aAlb%i4rXY%RIz~=!5R&E9?*g(2$F65aSxhGL}|*y;aQAxMq|NhQ0I&Z zXpv+y-j$kB?bvbAVSCEcsN!`%E4Hrw$L-C#=iKmmxg7&=J1bdni>%t*+l@h2@fn@0 zwqy&W-Ph7PeMBeXE96&ji7T9|`g%nz)r>>>X4$8Yr?Wo2i)_$lx?H7*xHe}rz&@-H zOgbbwW%ajPmJMa{-HP6Gu2Puk^$J~0mtUSG6hTrE|E^T6MrwN?%}+rr8bIv>~0L6i)~8P&5t)`C7iZIl+{D38P@L z6?}eui548WIAw0@*#X)E+>ZgRcwI%a7u=Y$q`+(S5J)g!v*ChH5InxPi72|<_Cz*U zf%B{}#rSyK9;`uBII9gOK@b_8$x4E_Xp3*I1qp*kG)2t~w+Xb6?&*cs-p(Q|V{F+t zaDHc;3EISN-&1OPr8fbu)alY&5?kM+`mdK)(gUxwg4T4N0#;IY6K~U1+`2TO#U<J!a=E!AgAk`C`+-$B`0B zW;mlx^k6ZgMeke;OR`{yk(|?@^%l@tg9$5sNfBzdf9z2k#Xqj-)_dMr`?~eAf%6lA zG91G)F`SCy-bG2rCTtSpsV0N9JQhZr&bn){7*!4@3b|npyPewn^2W8=VfST$bu}dI z9OxB*zFT15E}3oA9d{%@qrq=I)yHP5*N0?8(E&Zy4J)`a|&Z>-F!w$=&5?&6(?W z^B_y`>^`qT+RoF7q8$g~lv$9n9YX5W5MqI%0b|8Vwf-(eDPuEENSKqrYi1Fzu|7v3 zO!+HuDw~T2{0%Er&I)d+#u8*Lgk&>TDxQs{!Nx+6b}(i$lmqZ)6U-z2Mo@W9KuI@c zsl*Tr9EkC{(?y9Q~V|*dlNfb&k#F2_osVa}UbxtB#WAPRZq(y`FXnBfD zy1b@T2yuW@GON&Puz?mWFt%05~nRGjuV z!d?^GXrQ#7smgGW(4iD{(65w*?XcRRVD5A-7X7t(5n_&SN& z49RZVR&zA!^)M9{xkf!_%Y|0b>S#v7!p)44(7IeHqn>a$O^BGR`)U<7fyh2Kyo6J^ zrA#JF;)+%mUlH~8exmL?SGPpXG7cLT_F6svQlj>~PNF7_Bu~YCY@;6VLvY3e8_BRJ ztsrVV0J%80#5nRn1BtoIjffrrbuR3Fdm`iUm}2pW)gpvqCJTkPL|u4A)XjdP?mS2+ zq9!ShWdrVT5dW7F_3P!U$*kF)VRb$cPWYO<-b=tGHi)kvYE!dnYY_1~Qx&Rqoi%O< zie;U%qKC>Mh$}hDAwN~iFxd(mcPACCmR=Ecqo1fdPj`x_89QO6eJ(}f|3VIgeZ71& zB{dze$IL-m)=hZ=jJb{rTyh0b6OCLbmq^4YOANE>t^QO11Ixqg3gHJ$8p?7g=XIo5qEU_ zIf^XP^Ip=%6+gUt}FL9|@eNndBU8{sMS%pp( zv3N3oCo*Y22a{7+?X1+Bd8rxn`1(0ll=8K|GPmN4o--iq#iotG#lWcZF@VD;`RFv!$pDu7N0!DXezv=Oo%+ zNBMk+^c7Gg(usAlj5Q_)@+Gk!cH%4=%l9~nCvi$MAo3}z+eWM&Jtk|c_AzuMlm_9G z(M!eZ3$xX6Ka~N2@KvQ8O3T38yweQ!rtUzpZB__@Y+7un=c^~fF{FgGdz{sQSbOO} zpUY-ZaPN%f11vk(>`AArZW*w8;)OhE&j##aJ4IinWf6*Am{zOpXv%F%xC7oW-9*Fg zKq3N9-B58{_xkkub^hT0W79PUZ+LSSI4%crbXjaaQe zErN>LX;4Hs>h)eCt%fe{w8->%kBp|^k~>jWy76?lom9jh7~am9mC2XC@gIOpuFsQ+{%+zj z$>fsXNDQxWOv~ge0+7ke=E+2ViE*i9a#8Eaow>Rg-uIZ6$w%G}Wb(i~ndomjE|p9! zYCX9#cO0S9iy_l8`7Kb`!Zre*ShKi2a9?N~(1`eTb9Ui+cd(!#rz7dICy;&yQh+x^haBP;lfOMtWd z^g4L?A#U*!i4(oB7f-JT=Tf*zC6yl2mwQo2!s5Q8H-FvZva4 z7HJg-wUhD!{|+Dnvg0L4nr$y#ZQVU zz_Qm|K9)r;ie)Z15~!5GYod&2Dp`n4LDqt+IYrByr4pI-ldeY3W{1+v5CzL3k;L&j zg5`@f8|HJ$g|a(iFG9eXXz`&-$1*H@VOuy)E!&Sf!iWTgXf+WQ!{tJGnk{n?wF*yF zYq*olHR-T3TWRsOs)C0T(0(Ik4VFW&1b0Dxcr^xC_WhTRWoY=KP9ngyj8A4&))P-9 zs&bbD?r2$h>ck}FIC#YKvs_;)$JLA*DdJ+WA%*%OSIN_-?TuX3?h_hl2?i{C-6dq% zMQNE&R^k;3$ zZZT!uS5lJP2e$0BmyTt~#l5t_e%6P(ljWiY&v&9Y*kru=(y&sTb2Vt()CW7&m0GCCu0 zQnq(!F9{0#NVa4p5RSX1vCQQwlv94DP`7zW57c#ax{-+Jt-(-_fy=&F4wr>uzR@dE z6mZ^KeB2RuVtBai` z-Rs%fwy5YVSKD%1fC89=szop{zwgqq3=3b>zRaD5W2p$0lxlDy3CdV_J)XEUZTIiA zLp5kW4!7b7JR$Gf?NPe}%f;CMhS;kmcQ;szt6^7>>lGw0G2eXYScYDh`FD1`ns%kU z*;uL?R+ApAobpa{inEIyP%9%++ZRe1d#zDP(z!%~bl3_BD=CV3)*)47F2Qyj*dF{#I&)v^m4&3Fy|3e&j!xJ9vu2B^GW)Ks75}OYjmByW)VCTVmR7K*e zIk5o38wnH{#Dz6@wgM{G6!?Z%ErRbsJyje8AL`Z`gQK=4M*9QPvj}$F6#XUxjP!o8yc)63XHyHeT zFz#VgDEMY9noc7#7VtM$Qd)~?A?KR+=kT|wk-RVt!z5@vH3LQ{?Fwc>Lb3}x4-{vc zVfjR%PbVU+Mr<#f>f)lq3yE?#$w?qkQ5%L)IPjn&qj7h`i~vq=t4BoQY`0!$ahbhR z-V^g?-N`%`b!5FQ8un!gsUQYe*-A2WJr%+j1#Z z5wj`*ce2&yuzr;y2rSHnDUxDBU5pa)SP4XpYr-)Rv5D&+E;Z^Ws%w;<AW_n7LX$s9iR*S>e zOv{Y)Brrl)wQ%;)*?-RTB-8pTyX~G>HSG@-$v_552w@tOG$13akYtNiQM8prP%Eum z>L!%tNnG-%4Vi}WY{Q>OPcjW%Z|EyL891_t!1>=m21J0 zBSw}_626QXsZrOE6ZFOg;Rs^XA`J2as0n8ncDov-f(jKAAp1WKIf@kF_JSZ!f3&59 zq;@vM_L{*GDy5aYs}3sZrj%%*4NA}3eL+$}18m9(Ne+lnK-Cj44N{{04EX`v9vjw< zySoRKN=qCtaal zfM8s#2hDa6pQ5;3QqSuE^tJids%qwu*Otl$2Ue#&-Rexw<91P22L@Khl$sk0SL>i= z7j!E+h^8N#^v7VMlQzAlauqvGI9jb#)>UD%M7JUlEq6JIbJ={%#&m+Ty~w0f6=1-@ zoYm#&!KR=@yBP;~;|QedQBO6R?t-#c(>3%Vh*|urFW?=0ru>t4ul`fl&;t-Z?1r9v*bl5KCTXiqA zSB-|*SUY||v2N5G$uxL8?>o*-V6+W6A~RuiMB<>8N!J8%k42zhFO)7sWK5zf!LWpZ z8-BcI+jGWQg1`f6q+3M@FD<02MKPT3?FB1kd=Hd($r5`?w~5-kAa>Y}AdZqY>w!p2 zWhIdrCZ=Q~onp0I55S-{d|OS}J8En%Dx^N?=_!8}IZQ2&-#trTLe{*r!virymw)NO%X=8EiU8}_6^72QP z%9bB~{0~A4!sFkEZ}lF$uCNL`Z=YpQypOP6VqfUX{#bD8iL=<8$K^{LnSoyN%km+Y zTl2VliDNA;yF=2D%j!HXUt&*v^pbm*47uEx$K}xjmbN#$OwM(e+*54G<@!7>j~=`) z9hWb+56h6twRv3DUo%}QUA`!XggfK*GUT!{kIVYg+@<33MZGTH8Ar(>m#cHQJW=)T z5^?#m`y&mxT$#sZefa;S;_^l5GDsU2OWAzkUe?Qjiscy@WR3zA0Kr&wdCMHOS{7sp z6gnVtq!oz;11RbU+ag6KmI_OLE*@vCB+`#MB_UH(hFq36msTtp%a@jx>`PC-^6o3I zx{|)~f-9@LU)=r0-M8)vyD!;&>ds&9{MOD7@3eO!JI~!--ulAU`?vo6R$=Q!TTkBn ztIgloeAlMB8QQ$I@wJUlZ2Z*5LmMyKz&5tm|9EF-`}5oXbNjv9o$Z%zf6w}BEMKzx z)56}udlp)YTMM6E{Lw{a@x{yV(l?eqwDdzu;>z1ruCMGZ|I_ksFCQ**>%sM>ul?ED zFRZTBh?aE{8KWFw#{_RWX;x&-#dU)+JNcp(^ z+wc;v{m>5aP66Ks{w3)kXG@kl&Rzn^C;6}y!No>2%4JJ7w2|*J%lAKR2?i}DFoljV zRgq{MbCLVG45kK2xD+A69ZVIlq_3CYoJqT#>ZU1Mx7W4rJaW{+o~qY_zLcx%^Mq_F zS9GyNEKu#xM70-WstVk3r1DHJNfY7aryaM5tNnPs46?^`OSDVIg-oAOBQR8Jw3vi| z=aT+Pt3=wGFty}3YT?S-XeAo)S!Jl6Zq^g-WVH;3!>G;L&&Md2H<-71bJl3j>R$fM zqZUr!k5g*1UN){a_8U?Vr;5TpC>ZK5k{%l?Qb~cRGxlJ=*4g}*qZZ*BRWFjFt%BP- zSeW-XL{e>IUNvHkctW(BCt(61)R04^mcDq@BA9RoYmux6?a53MOW|3lR}_SFx0JI7 zS)Z>YMj?{SsKr`yNj+-eWg1Q-&;~be{5}FPF;x)DX@@&qZfC1iC7$!=!&N7mHS;8EYBoIum&`QY-B@=rTl+{sOhr1j_Vey+Yl)L;-1o ze83Q>_k>w71TIvPS$ahL+GcR)mE&*(hCr2AqognCvGd?juM!xKS`)ayMrx@HLB(7g z8H#opTU%{~fa9hB`S=PY8$dyvO&@51BO)E$s8PDO(EKcpWoDfoKHcD(EQEvC^=~%yjZ9n(81uTj+;Zspldu}nuDQZ@#Bf4$|n zn^CxwRbdvtbkqX44EV!AZ=Y1DZmcWYNxRf6#B!+56-)T&de~M4IewTXRf4wu=%@wl z_sJxyrn)Yvli>Hum4eIGkAqV8e9x;i?M)Z&a!HOb8f2EUM=g@MMx-VMWD<9J!o+^M z4GV~eYWWLdwitp^{xIBx5D#C5q~-58ZjrLa7#{~)y}huft8KQLwz+*yr-TeYiWK%mmiZ>y+4rZuk zwB;8W#1&{K1AM3x?cx1W3S;V#n6f||x5%`)5?yjvJp`4n$2mII@TFpT+?opUMWMr$ zEBl?0RK$|d(%X(&)E#}bmmt|nFJ_mkt)5hbY<||74yAfH6>rkcd@&ztcmuWY;^&WA zD1DdB4GL=}Ty`?v6;Ge#=3H*Ri7(AiT?!iX=E&#$AMa z#c&Z7{r!HUD~WI&Jna%?rQD_umbe5l`X3wa>lzmZKI@!wY{uT}bONoeAsCYhCj9Nekl4ttTXcBQ5J zk6ZY>8M$X?gnrfLWcH&e+?j0lUAY{{T+bubpbJU(VUWK#AuYe|s6{y(wW3V8Zii(7 zZ=`FWgqts$P1UlsLYvGxJ$Nl;L-&<>*uQox^HQ?iO$!M>jv-aPmCa`|3MSb5`*Aep zNCl}*G+#=(tzD+CEI$Nn?hzT1mr16Nqt&QNk$r2?*|bA&#M#AKK_OXla5&Wu*L)?& zm)iZ%QHxH*>*&YgT(((py8GQstAsVXz5?I1#Y$o|nR9Y&f_BkWzqI=^qbAylV;Etr zM1n$HZ_59C91g+=B3TjZEj`>Hjlp3gMqpGel1b~~{%`^gm&W0&7=bXY zl-r}n_Xp!}1V+F-RccI(19~A9`1L?;RqmLIZ>@lY)yV=3=Rep1WhFB(FuL| zmE*bU%@tf75mFNM4C-Rt97GnAMSH)W7BPR`xgVtL?m{P(<+0^|I@*1Yp45(Z-^Y7R zYbca-wrMHt79u{tx_%OIG%D6Sl1<6oBwkgmja1kfW7E{apBZq^aA>}4?@z$p9D^fB z3`LTH=$p{QcaFmWMTGs~P-0@dZySdLbRj7et3~wV{r)jH9Bj_XMwYFMdbo$i;lR>m zRoyD>(!;%e91g<}qRETh2|ak-I2?*0xV_YiO~~W5<8TPjwu&Tq^rhN&UNZ&z4EGI+&>W+1WA#3rJ4+5dmIXv9c|v}<|@u~IN-$amQab+`s&L7FZm)fg zra`@v0*^Cw*%N3rD!vGAD|x`pke3sy9=?~6vc+bWO&_Q&EQ+>Rgi#Y>tekg=YL%z0 z9@1g=gso^1_La)Mwv_BM($qp)lV1DU&~My$^WMXK&D6Z3H)d+4&ykBWHCh?1Z$VR| zIham5v7Xuiqmte_+jBParqR^dftu0*l|yCVdMrO)mY@a7mmU?fGacRo%A7)wW-SJ7 za|K=iH7-?PNg(juz>WF{VdVvyWqU!Dt?=MST>JI-tFdtp&<%epiA@l1)XTNSrkEFH zQPs-J0)j&bPK$36IabcGvdC(*Msc%kR%Nw#C4A7wqyFeRc@!El?D(KPD7JfoN)kEg z9G89GP5^FC-F@o|DEpM=0 zQ7F9Nwgv=$w$rTi+DxPo_Nav>h|T~=&J*Oc$KVH1cQsVSvyCoHM+BwARq?)^?$+{L zEF2+SnQ$wJ;bq!~utXj$yL@}Sn6-J3Ekf0Lz*(jlEJ5Yr0JzO_(%|kdRX=co3S$GU z8;L8pXNtK5LkHmoOAq%nbNSZijhIWLX?=0#l6V}<<@FQgJDqx3x#Lq?W)$tHvt?)7 z4Qe!Xwrm*O^f>Hi#)Fr%-HkST3SzS+v%O;jbdOP3qy~CqHFB+n3}$+@CCi$oo$Q9R zSc5s!+qCfns0MO6iXk15laX@Xju{{jT-(V_H3LP84z|JlU<|w$^n(r#5<6B8k_cy8 zjt z2c(D$XXy;nbHZJ|T11^Ps3g)=h*%qM6TWmr3P5(QXT?hG9-Odi&iV%j0l{@Bg}YbHn%l&yV5L`~;@I|9@^A&hY*Jv*U1v z@BicP@ZixX8Qa8_`5l3`2PRfWBoIJ|NpIVIK%h<-yDZCeEG|6d=2 zGk*X7z|qVj@4o*Vz5nap;SJya$KK(sFoGGp|Nr8+WDMW`e_#*mSH64q^T6xhzVp?c_v~bW2mk-r zZf-w!>$6)wv=!M}*nIzHaq~MiKD6TJ@A&@9lMZZAVy98|+o8f?xNF95{TnhHUd2SNjPrSaoKdo|xaS z6zn3Ue<>XzBTySuPj-5N31qJtAp81RdWNHejm# z-go#)fDGOI+SL%-52~Cm+Yp0&XS?m~=s~aHqmiNmm!|s;9~`v)7c;F#BOs4k_Xn;2 z`JAnLgVz6KruAsV+JJW8unSsKoB!8D>ys;90zyfvtImmfAHKk0XMpS<&wvcKp(K$F zIh6i{c-sSH|8N#$Bue6$n$)OGAZrbf{rwq{VN@g7oJ6J$kbU(m$gC)d`nwc6IVG9{ zWPf)SWCThgAdgQ^KLhVOYyf2F=HFhG-GNjf+pT9~!7Q)x6)~>k$hEQBFvogWAGH2I zW?D~p2;O&C8?^qLbGB9nt^YqWtw-bGk9%GnwEpXJwyq3X|8Fy`C+2?OPzJ53&A&3y z`eX*-FiOI9DN*w3q!Kuk2FU*MEXcrgghG*icyb1n2gts124t8Gwc0vr*sb^03ml3A zWPfoMWF%@O{avRJo)~6nfb7rDg3O9qtrAV!^+7{{!{PwhpPdC6fm#VhkOaLmWZ+O3 zAp6s^Aj45Bo~)>0NjJ><4tanK-TafQ`4HX9u$e+TRVpOxijvaH>KZo1G#3V4`C}8U zPZSHdj9M|t%c*)N)W9J(K=wyxL584KwC7FMeY$Z44)X(KUp@;m7_}l zhNA>gD&^(LVLmWG_W83Q1CG-;owiGp$g%@upF0aO6kJ7!ow_rr6Tp3k3_yl%e)ej< zTnK@zrX4BBilw+e?$KM}Yv@FTYv3?5=*nkIv_6@PC=3Kh)u@U-rX+Ak50HKOEXW{~ zK+5rys^5+V4$}i^MtZeJDWSFg`%`G2n^O0ObWlUaMJ=uLYyj#6Iannh1{K zRuEdz&_{*@4yggMkDdh?hT=p-?j?FUEe#yT2FN~g7Gx-jdpKa)43GO=#17zsthXArxqfGfKVnghdL0yqjRL7BP z=tRIv;4n1k$_GufJ`o>mLve&{Wxe`vg}`BOfb93qf{a9QIHuHi{RVt<@$D9`<;#}M z)@ESySsP#7_??X(-e_+4R{whS*H?dVwX*71ecH-jZ9H@RZ`Xf&SzP|r<(o?{T6*H* zpDzCL%CD{5T9H;>y7H9euWVgieBtIlEb?2StZ$M_fhrNYVopTA5kE?#Y`s?<;uJ)b*}Nr&ZjNFSuK z@zRY~ngzXo<3Y2aFWKmu1%2^G&n)PRHo9g(@7w5@1%2U0+bpPUqjd~g^<=7&Ga)4N z%|s;FQ1ur!JN2D6m<4U_ym9Frmc0|n&G~zbHC;&*IJpz^*BMv7T8ZhSXBIxS@ENn9 zpIrE~SJj@IV1nK__JANePrQdW+@zo-@ElWv!Fk@^;xr^@7X%h*SURt_tuHN&ISEX zTPOM&uY@@z;NqRFn%&i@v--Wp*3-6<$9jS76WomXoupP$G`^ZK+gECpJ_fC| z#I~Puxl6rPDH@_{`qReh`s%J((6!Yav!F|>8)iWlSJ%ygF08JZ1+}cMngm_>#_Eb$ z(66s9Yw`cfmM2-Z9$I_B@^^rLSS%ni=+*kjAjIgBW&YiYt|XWi3D#66OM>CZ%*Y_b z=we0}rQ;#k5Tfo$2j#R>hI&oN&^s9!G_B)PBZCZ;^EM)b5QG?1P?;VX zR4qds;w+@?XxU>&9b^K-d&pikjx~dLf_C6?0pG*1Yy?l8J2I$NKiEUzq||Qt!x1i( zV&JaqMw2~H7%#Q$s58%c!;N^a;AeXxUiX(NE16c3;h?)|3yOOorPRlIt-1~N)VpvZ z2BCaC(s!AP3>pmm#tl#z@Ku@_7_AIwI3q633~1$ubn(<$ES+waYG3Ilkx^y9Z^|SZ zMD(4bGT?-cYk^xQQFXUd8PHHtw=*YTE3TREsdFOKg>a`8@{-*dP-_TXN>oh?T8B+}ahFYIxO2zWZ>1k=hQ zps1QV8%ih~oMI~{`nX3qeZ&}R&MedC-`b1r&M!!zQoExP7y$WJKalbDhCWR(A^oUjEvo;-VC?d?haI#~&MP>wyIn!$EV zY-DJx+V1B4NgoE**8%^>_63vgZ-eP$SOW|J83@6gd+;8DotI~b=mD)=C_9u;m;v1=|85?7+2Cr>$rTx~^xEVp?qZ)Vi zL?tt<%pKEmoynV$2m~0BGaJ&8<-_0zf#@#gwCOZz&Zcw(EMd(O3sQp&mW-2C00#{R zj2o;WV9q_D8PBl=MZ-Ndn@``vjb?<_V%pMja@d)bHHowmnr)uCF8Z4ici`d?J4kZKy6Dwt0nA$?5e9 zS+Ap4DS%7@=4u^lL%(rDJO4k&^04K~JFnoopWf~5ZtwiZ9c24Aw=-LRx%H~8CvN`4 zrgh_YH?FV$&HC%tpSt$mwHK{^c(uOzB;f1sTmJlVb$Maw=F+{3A6N`8d}*P%unJIJ z%%}Wlb(x~5Q`a)g$b;?_!q6>lzBgR?(aKqg8M)1wAtsLzGwk*>G`K-i@oZrk46VFP_$Oq62vC;&wslJIrHr@`g;#rB^j?9F!61yFl@#Dmd zm^5VL?ToB&R${j^GH#rhQNM>HJ3p}p9?dVKz&1{LeV$@BbeNp)jrhQ$>|E?%i!;?v z(G2!i%*AfxpE)0U`O)0C7>t}yhm11v-!wz)<>w%FPQH~Ny?$0=x5LJljS(}{rXd?| zhu8yWC3ZV9%btVSxn;(T6EkAckd3!9vdlS%oqJ^TI5DGskB#iy#M0x$j2xSX#LoX* zRem&eR$@j^Q}Zvy%a10{O3cW0+6=M8I58ua?jaj*hgkfq#BN7s)LDt$j?7}?#Eh6U zWaI6OEP7UAw==THI5DGs4@Y)>Vh=nTKH3LNILn`6hk;Q!XQJ>29u3XKZgk%{|Ay^> zM}u>*8(rAW$6kIkFeV1r=;C9@D5G1h`NTFCAGSPgVP|3C2NplLii3E5_R4>`vbX!5 zb@xVi~D};T`Dw@F*P~I=(V5_|(StqlOI!OJfxJnM@nimG8zj6^F zk@K`Oq9_H@Z6WH&IVDPthe+7R`XpyxvPNu9>}rGdb`l9*0Pms{<77O(rtWrc{kf~J zT^KN8;Tq{l`(RjTF-~`cb@y}XK9Ps_t!<=LP#Lir6q@B~if`B$nYt$Sog`mW{Qgui zM5^_E927lF_cE?XqD}?kMJU4YB@jSlwZ*xskrG{uCYioF7Vp@*ajKorbt1p^Vhegk zWr(e!=sKw%;M5RaFRItE5`>T%+i)BjFN8wZY3k9pwu}XF1@`N)6nHLgAFX zgzpy;%++d}lU+(pbcEc|pu=C{^?|D^Zy5F=soE0(K^1-ZYR?dTs@%0jWh){{`(BKzM^!&Y7KB_e1beWbU;me{mlXOJ9( z%AGpX#gP5B+vjB|IM9+rl2PlaYe_Cez&51jEMW2dKAkI-5+P^4;)vL)l$TA~ta2$+ z3aIHE-MLx`H@h{~FLOO9){0hLJ-tip@)w4E2q!8{4_l8a2nCfB7~5eIP&66zb!nmz zFXmkCM9j}9A-E8}=J&bm`<`H4vayb2v029|N+Q@wwIL{%fQfny3dh|-uY?mKbk)Vn zg2V1@I~&PNG19F>^zoY;)nOkNpk#rd?q0R^qtwNk&6meRRs z$k$_Ql~CQS>~~|WNE)q>ffDAn5tO^$@H*nbe)DRquhf-Dyvw`YbrCrKC-i*S>VT#1 z+B_STTLm{Y+LAI6p?k7?TMbi;nXpk04f$Zh<5l}UYp>ESpS2muC!*~*4H2cFw_R$u z>Q=Oa+KaMW4y3Mi$+pAoZ`Hh_*lO=b9j>H;d96Ju(j~ICSWS}Yuqb6lWZ#-zHsc3YmP{&P7v!Qs3L5 z-8FsW?$Y~)eW2^zrjv~4on((gQ=NV;?`%hryrbBS=6Y^i&Ub;&!Q}Gtx zAvs2N_41jdEwi6>hjLW5$w^&bz1-r7v8#6q;C$@-8|%U=%r5VT2TDqo88l-fAVm#JkCS*qbL~>3WRftOQuZkXARN zNVc#w-b{!6v<%j61hNUWQkPMnbhi=*y4hn?4_ET3f~^a=g$!14*O_cOoo_%_OT{wN z>2%6;AX4l!`cYLMQM&k5i)AS=&pzD!{VoUYa$p_@hO^n3st46}%`RtxJxx${fI~WhUL!G=n&(7NOhJCQ9h+9fnS-#$jw9+~wdYxBa>8H*J$!f3|gN%f0#4%^%;4Z!T^8+y=Yxg!Nwq4eoyKa^Nlp?sDKR2kvs< zE(h*%z!V2!%h##J>6M@mWOV$jk(U10X!-e>2uIU+4iLJ907sV!<%m6=Z~2%;J*DsA&&@=rcdVU% z5o-C_nFvR7G7fqe@z6o4ot6BZHqTY#?x<7W!=H&PKR_)Q2nE(bUyWpaHAlVWa|r=^ zS!6q2eG18qS^a}2WbBVIVW*?R^&UuFoR9FQW+I%>LXRYu(k-XIDhPR3w^g0w;eVcq zP#=VLbw0xP%|tk%)_fqAOcljaJJDgICBM{|(8Bl5L^z>_^AY~!EVZ7PCarF;S84dN zd?wB~vmIX2PvrG8p=-wIw6`kO%A!b9wO%Kp&k?saYq-$p;clQZbMHSb@0p2kV%jk+ zfsG1wxzS1%%Dt4cqwk>QKg~oqG3`Ww>iQ}XJC{g!ed%yn&}Xf){P;|S6VuLZcf`vD zrp2@>;Zi8k@#_;cS$=dT!ij0;@kd+jel1*5-0@a5%`p1=Tg!i(iEv`t^&^g2uNH8{ z!?L3gCc=qnm*iNb#t300;Bj+hCO(mD$nvh42q&hUgOjpCi7Ga@o)|8< z?RI_kBFhhH{{P=?X<4qk`^vT5-`Ev)Eg%oywe8>77Pc*04{zPS`74{(H@>m)@W%b? zzp{RP?Hg+kuOX|SSxv9}!^#h?Aj=oFT8(4U`#e++Zs@JZk$=U%tv8a`@0 z2JwsYGoc@&Ito&J>1oU)Ix4aOD(>*xzyR(aVU1zFdDB!U!*d=ud?KK7FiOQJ5xr?D zrxnwi*AJfnu%kNcQ!~?>#y(mQ`PGM4G$Dh>x0Bc)+$dSSY5d2MF$jRt+fVv%SL?y( z2#qq_o9@Ap0(%a3fSO&sV!4KmTCgKEo0}tF<(q155KhH-4!2JbjrRV`h?1VetrJ9} zCh*LN9EHQp;~9jFnm&_=jAqd2)?k3@`r(FN`;79<&rg|tOm$TIxWjdTe{_Z#rl2>C zf7%RP15`$5C}xzK-ZYib8M>hf@p`iHL^!)Xv@z0*2`V9$v zxUBVHWQJl!N$*Yf;An;x4wp{Ucyb|_n>OD_jVDlXhl^kUV`^rU-F|+^*)i2=YPJBV zjH#JX+I!PfM%By$u#YYThEaT`v5%?Q%~xwe9$5$sW9dxepRH#4Z7O~9Ray^5)C^Qj znVwJIbPtZyjJtUQU>=pfVRHTX;cLe7zxhf)WmNtem8p?!rm2j`|K@`L`>6aucJ1kD z_)TLUmw#X5{;2$o!mdo?pDq8E&6-*dM$}B}!8yZe#?*|v*#MYFpO~gHD*qb5J|=&om<-d{$K|hR+#i*{QDlT^{Ilghx<5XWe^u+jsQk|#c`+t` z?fm~-%R4Mr9=!7O-Cx?JcK&{+wzIqa)7ze{$F}&bjm?KP@7egD8`<@*ulLrUw)S7w zqN|@?t*(CC%3D_UmVa-V1M&ZFT6*^4Cqa$Br!D;SLd5d70NHJQuHSm;>LSn;YSTnv zuH&%`b>Z|#Xt-TwZru;?-+LzfM<#iQ+h{9!{nks4`);Bv*g5(x+zfW*DmsZA5L;f_hnUP<)l;vpN24&=-&MC$`!qluDo$9*t6^@Uroh8x%~6BXvh z$2YWJ6G!DKrRZ9>paCIj(?l^k^Mp=jlVR05{?-AYtLeIlx^>5NXX(1pk^lOw7XUh% zuA3-ncT8uRt{WZpbGP;Ze$7UksB(9Vf2vh5Jn#cu{~k?5K-W#wz8e>j!7Z)+aDJ-m zhPMLDtv#&|V3;OK<{kGzr|X92eg4+-0d7qdOjOW2#yvw73{U^pZ#@st(Nw`ialK3WjHYpbGbDVgRaOq6*(JiP>gPe@-8&g3$#5bL+WU zAHXn86z@CkgI*Pkj{d@}=V-Wr*)vhgZ+v{GW^eN7J|+E2>(;daA!^e^*+27yPRyQR zeL()!zXNnNT{lr7@R;r_T{pUdxPI#%Ku6Pc6U76M=}gge!^?-xQ=!%&li>eE`EWQJnC&4?0~pynola| zT?9~t@6^NqRKY~O!($S&&7S_=V5ka47Z#fvo0gX@++$h(^W_gL|NG^aFGI_VyI_r^ zd~D&K)#tB#ZRN3*A6aQ!xo5?-^3?4QY`=9ozfEj!Zhdj<=eFLkmEC&5*5c-;Hs81T zDiC?_+>L+Ucx>axHrgA34SeI-pwnZY0FxyJ;9TEW750w=l7Cp7W9OEJ7+;pNXIb?dO|w(8PFgxxU6t2NMO)Z zUpTOAFFh0V{j;DaWciX=&=azJ@hs>G>AYwb^u$cNZx*zErY*g27W9PVZL^>!Bu~zQ zo{+rtOwa@f)-GS*w&5Sw7Lfelyn@E%hc-aUIa=7KogxK|<{`tizjV>;AG`)Bx z>yZ_|SgR+q{eokU+@m_qY*{HVoIPKts$2{yKoPj%JE^5~jw&;A{K7z&MvIgg++*t( zBdygpy~>Q?jL>>>&MGsYs1#uX&8JqGX?qJw$YF~!R5b+$L|aFsygaI^HQW)5QhCKC zyMqN+r=6sZtFN7-%1p&0+tYSB9gesvf;(KGco*?d&Hudns@X@ zOpVbTxj0jUeruYVIds7{b=SiH7=|EiA`RN+ z3cSF!WEFf3f$s)2?v4;1KkZi83J-q7wO^+^C?BC4{xqeiPg5KYQq^Y;H3Va zRz3K<_5WrqI_KV*p#(&NK20^35%(T1)17qL@8Lk!_F}!}N{E3f*7FyvWP)**icq{+ z5xjH&JGZ&)2kT|o$Hh8)uLbRKj)Q8xRSmaWY?$bU?X8quwh9u+mYed1snWJ5TqP_exRB~9Gr7{%S`NE9ix)5&^#0X9##lVVoR`&c=Z2IVuT zYQQTvigxNinYJTxSLV_wG_hlqUCCbkX^!of7} z?(&eQUJ6?kJLN`v9$uB|ihy}BN2WvO(_9|r5H!SKvWtcaxI5Odo4bC}h7PWumL6Hw z%;j63H*zL4n%46}c+Hwi>*HW9=N#DXh>6x{>TKDOo2sU?qn9?{BrA4Ayjc=!U~1l@ zUEydQDTvLQ%=V5?S@#%RLSq`}(a|W^YRCXVwI$1%=AWEl5b*CgXBeXs;)L-8sHWCB zH}u@T9W!9H8Q($+eF*pS-eQa55{*DqRXu*##gUB`9&z~knAaXHV!n(_imCITbzB8s zfp*6bx$gC~n>Lzk6%M!nT1fj-{sYgRTRyn36IhwaCmXJU(`D|gqYeGWjW;~uktNLxyxBQ!2F@8rZ`KSDlV)JbES)lm zj$G~^r%5!Jw{39eAu8H&{~lQD$Dl`wc4=^rX&oP(JMjFuqmeP36H_I1TXO<7o)iA0 z+ILuObR2gEnx1Ud!McM^0>;TyK^Ej1LYE?J0dL8n9(Z#Ey*|&y1fbn(F6x#xz~pH@}t5nlk(dhi~$$mdk;3g{%32eE>o-?*{4@FB~yf%E+8M_2PJ zf4B0`u50I$JHqxqZohH+>09sHx^MG?AphTAZ@e00{r}N*WbMmqKd|;v%kM3`a$)bv z4_IEd@Z~G_EVg!ke(@_y)fImE50`&r`58;Ue{y`%au#T%kB&)NVQ}MZ%M=10eFoJb z1PrqEfTSznCk+L#y9p!HRF^mgzF;R6#0 zL5DFmAD|~XY#sG@IYv0@d~$@4UZgBfIbr_k-zieSpoS3XQ#uZwu*FzTCZteV{G^$h zSk$0U->G8Y7=|Vi^(dq75;-wS6Md*HF(&wckQiq5OH5;e+b7*ZtQbZ(y$tKtb<0Fy zjT;ur;@+tY?*8L02aGwOo1r)`ERZ#o5S)Te?@neYP9hkDw>(N*KVhz_tUZ#cdL7xS zYH#rIR->xJvGw(7~@ih03eBO|AD5L^E` zLC&b}Lvj#^8gDZUpVuKXF}kl!kTdH0DLJbRgOH$$Q75MP{L%f3-i8}4Dc7tME5Ni> zCNhB#m`2zi4kh$U;x+52o8a-HF!9xPFbb9}cw|{5z$ybtp;#@VBLN+9a06A17$)lgxCPw!X zJvofT5F}BQeMWl$0<&TWT&N_odgE}l<*g<13L_P}=}5F5;Zyo6$?X?TkTXJdIywv} z6I6*cN)tP>`Q&U9rb}6e`|U3CpJABU)pG`-(0J${`b}9>hss0u>Qi8UtRmc3c38`rB^JyfBB1xf4}^i zjqhCip~WXJyk%kE@*#lE=<~qMdw^XTO$2F$P#Z?ML(Za3H%;f~KlRaTUcY%&y8{?W zlStS=?w?eX^XmAC`=u1D8zK3k8UL~V#)4VCKW?foeJ*Dl?B79eo%^{1Q&7bmQ! z4fZNkL0^sB^Z;gNj37ogj9N^q#!S$S@6vaJKy6Sx+35w&JIiW%Z*Qp^m zGLjcpUxB-+mFF1E3414MP^_(3RxOqXZhj}AHEIyFvEZ4S)TrrIEyVVND(A~K#9-gq zZhJdv9mKKM+|VGR1Oy|@w*+90@_T7ic# zeKX1_X}Uncq`p0HQ!Df^rfEi*B~4QqYTEU8YGocq=cHj)Nz>Rz=j3ab-Z`k^FrsgU z86!;-n7!W_ea|`F@4Qp1_%J%540B7G?#0m)GWSlc%EPD<7-o|+T_<6R61-EZ_Asgh zhB+loQyEnPt=7Y+5*X%_G>u)S1cOozqe@_uE7CN9*-9|Fj1HBct5*V}tdgdCaij#L zcXp1A&K)f%F)%vs)XF`K>6=kT$NAn?$D6uo`lgkA7}Yn!ypE=+jOv?K^kG!r40Ae~ z#;()1K~0AdeKYj>F->5$zKw2@PV{XM|G)U_maW6Jr-MItKX*CsZ;b)ZCSWV?>m1P(rsya-w9 z;VG-^KZw;>D(vqAU$8mx4XDx5tfN`(c=J45Ncsy!0mb5NT3|vE7XwQe?d}tb*OwH- zaZ#qc&XeR7M^hjN0w;kV;kfr13Ij(vAW%b}xudC~d9LvVkUs(B-h;Kd(r>9r+3WXL z5G7G@s|TWzM2y?cRT_$3b7%eNoUb- zJuNqKz1L~ng+}q*v6-ba$&>q3FsjGH3w}yRzyxZRSP-Z?Fv%zDiZ&1DIJ9d+7&Ff& zUZjRILTfQ?3DowT!{4Uc1_*@=gL1j9tCY_N{IpZd(}6}bmUoF{tCDpq!GI0-`D^J~ zGT{a>vefzGRk#D$iD0$@67f?363e6Zz=2vsOKYnt=GBjJ@7*^~LAHXaTU;MkbBJgtg&?VPCLu)Qp>DYuk662svqMHH!b)JVP>762 zJRoxxPyoZd%%CHqTNySNE!Y*Uw~E*+pz#AJ(<35nD_8AQWI2_M+Um|yD=$P*Ijq*Y zB!AGzcQb6lk@7O0fU6Ycqg5yCt`P;q5la<$zk&r?VzX0sDX}VVGF^%`bm0HL^+Jnx z!Ef1G2f6ltVzaUN(#_qCFKoPTqr2hRc=Gy}*MDaHRqNsP?_B$fwg0yEeQT+;f4BO7 zR)2N%EvpageB0{tR{nA2H&@=d(qHkfKr2sM{`&HtEPrtMk>%GemzJZ;&s<(!`qPh9weg+~`2T<~9c?8>{Yl&_FiR(C(K`(wK`P;Fp)=W{zh zxzpZpO*w+@Ua=rkzBcvM+1qZ(XQc)&@r&22S+~FADHNPBTPEQQD*mtn<9vp?r zg+!KU#r)1T1i5(s>YI;RB)eE5-^+&xG(d=zBaqg`wqUkmYdCqTk|@GgGJn^v;fiYmLBG3X5y?~pLI5s0-I4)a56tQs3EJ%EP_s;p;1=x41!uX61PgCyo$6~RkoY$c311EIW3Bk zAV;_NYdA7ZUbAGkI>NPFDk>>Ue4YTmYIMaLF_Y0EmY4LUXeFMAB*iR2zlNo3Y(|wn zze(>+Ih&bG$EV=O+)cYN?03gAB9l0#ZhK-DpH@Eo>Y#>RRZ|rMC7-A$=bIcQheDxl z*wXH-SkQ?YUD29EBg~au{A!6I>enzhB=(xCknKpSsixQ>lnC7EhThsWrW}%_T<#Bt zgrP`?7f5*-S^i97dZSFAa7a`}f3w5O)}jbJ@kEJVH5l-%e% zzi&dPj0@5dhgD*&b&C>B(k8H$YmI2PiN7XU3-WTH#`k-?b+K5L;BkCF@ z60BU`?5mQO((Zi58z>b$JZ(J0x9Z$t z{K94m;hHbz)p^SWuOQ9i;Ui=EAAMCTmUXe~`UOjQa{D%J=oc(q59t?N)i*9ll?~=v zs#P##tum{y9+#Q$EGSWnpWxMNMq}15$f>#-Zzd$B?dq#q`c=KJ>Y9GR($Am$f~)%l zD>i0hm-Y)*B%fgK>=&%q%7Ihv7^fF&2ZVc4h{)9K`5AqG+cw20>d7L~c zKcS39&0TA~uW7~lA9i8C;XJ$CsVVXbiE!3jELVbwswl+k=)$U4T$J{kyV+uZZ__9R zV&jY$YDnAmHZQ8;nVVg=EvgWzGZv}49+t(N@`gR2l9`P~i@+o@@MJm0p9eLxF{jUy zZMgLLhRD&ZjQ`BB|38r0BB-0@nzR4OSNcrXoWLrH$l>g@)k z)?7KmS9N(xXHo9WWpY!u^lKDDl6bt;QKz%UkY8{2T74OvzUEMcl3hhxdJoWMx5ROebqei=D0~u9SCW zrf`(+bEWZ>tMCU+#? z5Ifp#XV#w4r|5s~I~7bg{n3P_7R+Y(QhTGD$hZZSWJ4)U%5-v>vXu-Ox;&vxE3$U_ zCW1PVSz0W}WKEOJCDkOVzDmZPQ{;2_V^X#i)(RuMT+5Uf$N5uN^lJp#Zf~$7i^t63 zR?weGw5#oW$s|xI)jk0)E-ki8(FPt)i_+NCX9hLIL4Lq4(WR@ry55Axu!cwA708n| zVKA*x3vCjEzafojVyPhQw0;f0JsI};@<~zAVoLJLYN5d-v8Hw6f=}Hrd1U&GASO<_ z6?IFCamb*C$n0~-Y&nlH64vM1_O2tO)#=m)ZMI>lbR1r-*shidBU=1wf23c-SB+LR zf{@Zs5{I=$V<;u98Or#hoVyzP-*cwtKc?F5tVQ8t9_GaEzl3LAX ztHoXmX2L<0mwrsYhDTWC#mZ4ntrd@`WC@Af)D9-wVT0GK)^u|bhaeSJwj@enwS>Li zuiygCY}i8>dmw*Xp&>Peho)NtTRL-$(+?7 zPu5dPVhd~7-d5`!YK6CMZVS9BODf3Eo2|5u4rlvQ|*o z!ncLHOR11KAmLZt!AwzXuR27wda0I`sQnHt{Py zHPMy`kC&Br*I7t7BffS)XX{ksT7`1zfqsoz+Yt`y6Q+2~5;G?{saRJdb`%1xZai)d zc`eqE*6Wn<{F$1S6&=*D>+2;^BAYFX%ZWhDg4e2NJ0AQlo-n7?=0;SMHT#`DXEMim zYrjUN-VTRdl87K&)q35Idbkq`d+ZTjGpsI(nl@P}QV|zjd22q$nCsUl`C7bg*k(_q z({_C$8)+CiytX-+2?{kOo3+Zznv-p<+8f~q8U1)6D_B#_R59x@dxd_nr=?CA)M-H} zXpZONEpt_sH%U#koIk7dYq8t)qOliF!#zeY}@tV??PCJY?Ely0@F1iKshN0q%=_1jHRb#D9_a9YcNwr=eu?Ky@woI8* z3Zzc4o#zos971KetE>rR4r@oD&DS$dF@5kPAHyG^?ecmdiZ5ZLb@F7W6qZ@rF`-tW zc6h>0gC^xL2E`SA$;%nM?nRnrdo=CrMzxM;GoKIHl~r@C?ThQv5lKMW4i+ksyv^Bm zI<2(9D@@quGkQ$rvQ*#-M3M}0+3T2f5f8TN2;@+;Cua{@|c3z70AhG^0nfl1o z{HNy|^PA>=Hh1lu68#C?fkFszALiD$63)w<_i=2TY3zP1g`L9wE&FbEiM@gKL)LEA zR_3eB`2Jlq0bD#QocYtt9W$XBgr5*tqn$MUlj*Cc-!b(E z+|=6tn0mQcn$wB=le1A47X;;GIWMMO7L@rW=b8+ak!7@)dKpmWU5jUAxi+R=8kBj~ z;u)@N)l3>wF9pinYw?UMHP_Tjf-=`yJR?iIHT4pp%()iN$Wnq$y*MaytidzVgkMuH z2FmPf@JuEb^97M=S2dbKY3fBmnQbke;mY{kVyxvD+1YODML?N#a#qaZpp{8P3WYo4 zHrGa!ZS8Hxm5GpLat_Hx$%?mTQ!fmzm?_j=SSFGQMGBcxsaK7r;h1{ogEG@v+J!5V z=0gGFXs)@bcOED+uEjHfNFj-8EV|L$WK-{4P-a+*XJoldrrtTAOuq)tNK=&9HZEg@hObUrdmgC0jY{nmU!b=3+)(>$Dz#U9Tr%}s&<$r%sJ$?ENppWp zJtrtTb1llql6y=&2PoURMrEsJJ(+rTPe)bj$-#A`c| zkV5UnfyWog`4(H(+8w!nnR*6LCRl?q(kvTOPY=rYYf&Z_$>heiI%!?unGTd~S&K56 zNG8kq{mM~>1yfH8%1&R4GO0)=4JLERQMM3MPXo$$Yf&Z<$t0da-I^O|=GLAXS0+NI zP0mBuC|)H9slb)Z6lyPw1u2=%1)Y`L9qBRNt!TX>YWM7PF$lh zS-z>aR9ASm6_jmUi!zBwDov-O`IXH)1C+geEy~DJK1@9YDBG|WWn`HfTYGX`nFyUQ zdGF3D8=6f$8Mv~ZLhXh5L%L53ZEFz=W*%x}Zj?fLVY=GV_3HNS42H9s}?#@uUjFV8(Q zw{PyLxyR-no_lESzPUT+ZlAk(?)tgibC=Au@fH5`-1fP^oMX-~w{1>7$Di9Yw|?%Z zxpi}_xheDpdJVmdoxc5$r~npf)O_G}?{=$bk%K8aMiu^(eU%zlV{ANx-B?d+S`*RyxCFTr<8l-X(ac6Na6U>n%m*m5?Xy@|b^eH42g zo5h}Dy}^2o^)l-j);`u#tjAanvmV0tRNTqBopm$odi)#8C9F29%u2JivjQv!%fQ;k zlC$`%O|12-qgd-$EY=kB4d!dimzmEn_c5PhKE`|)--mG@^G@dN%$u3lGj}sDVYZoN zW}3O38DKh?2Ie-VoXKZyVyekMR`aF~-A;hZy%U?quA~ zxS4T1V>jaxMw?M)q#4^80fvKNU~FT^8GObj#(Kt4jCBka{!Ql%`fK!;>Ce#j(VwC} zMt_+85dA*-o%Gx3H`A}D@1|cuZ_~^4G<`cgKzGm$^lfxGz87Q@eLejs`Z_v`J~jKs z>}#_x&ptD|@Bhx#=RSsiw|a6?dtKlr!*lpbKt&}is)=sCbw(6fNQMb7~K20ab_LEi)1hrSE= zWAqf@kI;7je~6w0`~i9b@cZc7fZs!p1AZ5M3-BrQO~CJ<#{i#1-vE39eI4*|^eEt0 z(boXKfW8X&dGrY2=g?OGKZ_m)dF5x}d^hXJod9|F7reGqUr`T*eN=yt$e z=r+Ke=vKhX(E9=3g>C_SCwkuidw`ds_YUx8z)R3xz>CpM1Kb065qi%6-wn6}-3WLg zx&iP4bUk1fT?g1f*8;ZDHGnO2HDD861=v7W0@l$LfHkxmu!=4Rte{z`wYJlgXD&Tpj0(dSe1D=CQfFV=_459*H0ObMwCTF`btGYSKm(D{HybRM7qoeQW(=K$(Z2vCcHfEpA4R3ks2 z3i$w)$P2g)c>vEwZospU3-C^GXXcEt$=Sw zX8>+M3cwSP9B@670lp1M0gp!#z~hh@@K_`QJO&8?k46H(w<12^QD_U`k?3^5BM=Yp zaC92rVQ4epq3Be=x1du14?&v%S?FXyCOQd_fldUZqm6*G=O0sn%I0(=7~-0YY50oUix z=m@~q(cysqjSd6+6FL;|kLWFce?W%-{vNFZd=1S5{ui19d=(+U-ytsGD~JR5TZ94r z2C)HujaYy$BPQTWhynN_q60pSW&wYRW&nSIXn;RQ(||uiQ-D9^{uA&g+|0Kde20q~35=K;UKeGc&R+-Ctl$9)Fyv)rcvAL9NJ@Ime`03YD~9Pl&Tp8?*_ z{VCw5xjzB?6n7usC%HccypQ`Mz{et`Q8z}vZB2fU5@DB!K!uK~WF z`&GbOxQ_t7kNXwC9`|9ucXPiCcq8{qfH!cz2zWjB3xL;gKM!~<_j7>Pa6b!pHTNOF ztGEvWUdep`@Cxo{0C#im2fUp7X~13FPXX@aeiCE-aBBOsm&Uq)bsp;-tP@x>%wI9T z!TcEWDrSLcX9}2yGX9(KW5#D0?_=y>gc(Z4i3}$93T}>TEBL2F@3}Iz0nv_`BQ-_#50^_c;>w!-dfd!ev{lcSkqHH~$H$cNY2P9*707Ad98;>GJvOFPM*GH?QYr20 zV@f5oN5_|_RyG80qwytrF_~0q@_}YjP{u^rE=Q+V@jp8Pmd{; z&^|S$R7Csam{K9_zA>c&+Pz~+`Luh;OXUjM-D67Sw7bTX%4nY$Q!1r>d`zi?_OUUg zV%nW!N=39g#*_+aA01OFpnYUaDWCRX)(^?cWqexin3=89tHv{_%uilZs-V4hOsSl9 z^O#Z@ZSR;;Dea~)r4rhnF{NVKd&ZQCXzw0VDx}>wrc^+?VN5BXc0GBiL_xc5OsSl9 z?U+&-?V2&AQrgvHN+qi{c=pHnD&b?r6StT$CL_bKO-*{NohYFQ!1hTWK5};wr@^nHeKnJs7Z^u{FpJ0EG-C|qVC$L^;J&v!3Hzp3%g8~P8;9w6N?12OBflaf{>FE<9 zev`eHX_)LbdlS!s(`HA{+R3CT3Z_#O1yf0ig2}|GvrZcA1Uw7SUg~5lmSVfmO<7El z`pA|!#@P%FRnfB;YO11A#+g(_C5)|9Ma7IWsEUdh3aX+)hMcOXfFT=*y3IDTTOV-f zOU9_X8r6=ThR5l0K0#Hq!}&N>(KhE}R7G2yJE@8`Id=?1E$wQhT(RV$-Hx%XPnAcv z?&3SSUZpDf68(2nMSn(rma6Da=uZzsLoKU0s0o-{dAB+1wE0JOQ!}MZKUGl)(??y@ zOI_4MRaDG$Qx|nn7j;q<6)_#uMeWo@ZB#{tOe=L!3sq48(@b5|L|xQKRg}*(P#4ux z7u8V}J)NngD#~Ln+zTeot<#tb_kxL{o0%%A4xGwVQWZUgxs9smCgwY+ik{3oo2uwZ z%(Dif{-)36i)fPhV5g(^*yE$QfV3NF@1ZJs1MS^ZMX#sbNLBPY+6`1iuccj2RrDI# zbyP*Krd>-_^eWmlR7J0(T}@T=3ffgvMR(J#To8@MLN=c&k~d}YYHck(nr|?5#nf)9 zqPwRqrz(2+)Gn%`yQX$h72P>?8O4XkT$Z9>HUs?sGY?F0Ut#?Uhl76(_Q1g&_>b>_ zD>wHz1VQkPoh9VtWO-1NGnOvcu#B7>h}rAaM5GWdRpW_jrkKLHN{M8qu>`>)vUDe$ zpmiy=Zc&t%N@AX@ttQoIbwx?ZnCTeBZcRMKw+4f{gwbB9MgyKq)=!YvFJtc&k)>yi zn6M`V!d6+NjVIfZqyZ@Eq&2yQkDePps>!L%#Z&svpK@vyYzIJ zQU2aw!PPMOGi_ZtQqd+fPD^OyGXPP6II{u{vDmO}_0@XGQf|ZrdfcTaGc<6ne&S?> z3#e;2W4(aSC+KgXZaAJuMjH7#u7i*N-{MgX7i<_D<5P?j5_lo7?hgKIS-U|UUX%3H zAG|Vda3~QYnwp74^5IB66Cs>msyot*)FZ^X9yr(SqCX=}*6PGz97D&Uel${CP?gKX zatnXQxzi&tqIDpYDK;~8qB)>^flpn88`^I^b+SoJT;KiWQdtbeWn> zEVM{M&TOm|73)>))@EAb@mbTtm|xa%?l;NtmX5CO>?GYKMK-BOH*$)M zu9Qy3Q9p_V@>0v~;y!MnO;gW1xzj@(O^lu24 zc)T3LMEO%BpDj z6e)pFuXdWeT7_TZSC#fV3~V+86iS1;*2qfJd{w!osiz|ehhnoisI!Sw&7#uK%xP2- zpR&}Bh;5;!Nzur3WnMw9UUVdzo=Q;ScFNoQjyvx%`;}(BuAnTb@lzfsgaM-IJ9g|^ z*JBf5;7aw9FhKUu^5%sB;mRlvOrAT0nbqe6*_AqwIe`a6;wYX~Z;g7h(GhU@%6yzo*b%e{U9pxq8nNYL z4v*Aj$VP2Ng-lXUI0VuC=l}O~wYHkLm^IY(@`gs&*;0sV3%YW*Ua1=LK3_Mj^SL{z zhSTb9W%WL3(Ub{xMOzX+l`W|XS>#*9`9#GjF{W#&k}+FKD&ww-KuPV^NHl%N4&wcP z(^P$G{>J%@_?~(L&cL6?*Y9uR3OHZoc(Ir8)%v5^pJ(UT$KY%97qB)ne}-oU9LKnq zp`?GGUdQ+OKRVl(J!0nTGtC(mtw%d;`b*P}spoKoWB%m!Ht0^LleI`LLC}M(%IGze zAY;~=%;%KbTOYATM)|d9jg)$8G8I*BZ*9aHndd`mWZYa-+E!g|jnwm@H8S!pDs8Kb zSR?a%XpM~Bi%Q$dBi6_~A6g@$|Dw`1JaJ{|n3R%vKD0*04>q}N*1bi#NQBRTmGod6 zB`Y|;CKFFt_ZGJSZ{3?4xVxgQ8zn2a^{ME~ zlIz>A=4S@YU+LeQ(EM}vrU&X*__jE>NLkk=_eO4SYWYwm)itz6M$1OUT5`l1nXYBl zR^@G@VlA=K+N!#S*2sw6s91}SSR>Olw00n~BGwaY!vU8zViqAR-9+DpZd-l=v(2;M)3;9 zyvhA{?%s3zVPs|F7KbA#?Hv{M=M2=Z@NH4Ogp8tZvijWK(8$~()3s>r02-6q8(eOU zRM*fN8Oa}&wgpD4k?9&*BVz%iV$DBdjZD|j8W~L>6>Gi`Yh=2H*2s7SCtEvruXivA z$#0dU+<=q0@^X7UBaI>B85}l-j69HvHTQ@$GF?M!WbA@ethq+4k?C5rMoK?O#hP=u zHBwzeYh?U{RIE8htVu;uc_dY6jP4;DTEit)~rB{Yq0)5OP}KGVZIfIgMSY8z`-8)|E&k`594zyK8){wbROAGAjLnh)>hk2 zAbWE^j7Kg0P9PG9H@)e&TUjc3M|q=(55<$$$d|s{thPqJzaPd|=zY^ZjFVld1Nks6 zmnw+sw(5uRNnYR)U$$U9+#Aa;LS)pv)uDON{%ySmfyz z%$c$;sH&@j)s{=_uqL}HyQfkWIh(mH=0;QE_wfBuouQL6dCU2b%q*>KX=FBg%=po{ z!j!R?Zdk;dn;o0Yl2Syr$W*4Hg5qKk(R6&xxZOjC2 z<(xQY!|+B=_9%c~>ZMz3ayo}SYS$&JhJ-8~itvSvNJ(#&m7EG!%P0sG^=_BPY0Skn zCF_bUHe;4qmYVqAwu^`C@o%K((jm9gkxYo9&3MVarD%$_`0B1ZldV=;nYPazD7qSg zEt*!zBF7g(&s$~DE}f~>OVv)enW<%>nS7?+A*LY&jSR6%fdYXvQDJhNTKbV=wK%nk zBe?ZiI)d+EfoZw0piPX$2@BeUN2@Q&kR%V6nW^uDlY%QBJs*YWhY{`=Cs>34-b;`l2NKn>Si&FfEPJ1(o zAAVS{Z)vJiVr?U8m&hh5O9?AIF^S?DPEZ;%+ zFNvT;9DpgbxZ&-be|gkiCH7=XnPRxw$m4?@cj(^`a~2&S0$sTg&1YiELJhIjejuS{ z)d_Gwp+*23`Br_*X?cADb2Fz-De+yT@nXVaGFEMT1z!@WdWsr@N72|)(;GX!Zf5`2 z|EmIrQxTOHg9)82D9)QgwMbEvtofADI)3P}!4%Y#G@`VxpiZ~UwX#W9jc?K6TlPxE zdZgQs*+a&XOeyV3weqGdRx$aS$woGQ_FOIib|0{<_%4FmpP9D)BAN8ta(5%_=6tr+nCqW5FK|BG(Hfd3c0 z4+H*R)Wd-P7rhq){$F%62K>J`0{<_L!2gRQ@c*K{81Vn1n=s)2MSC#d|3&Y?fd3c0 z8w37dbR+f>wEubx_ib|0{<_L!2gS`!+`%6N8ta(5%_;`1pZ$ff&Uj> zivj;Hx&{OOUvxDF{J-cb4ETT1l^F2Czo?7>|1TJ$g#rIB zN@BqOixL>{|Dres{J$uM0sk)wW5EB5&c}fN7oCRz|1UZh1O8uh4hH>x?gn&WcL6%FPXIcwj|1AVj{(}SI{_`&9e`%+qktyt zBY;Ni!+-|tLx6hhgMd2h1AtoWc0dhw8=xAy6;Oq}A5e+iGQjr@us6W>0&c@@20RA^GdjQYC-VLb0ZUmHLHw^Il0bV!2YXN20H3PgFP>fv#D8jA;6k=BZ z3b5UPeC%?-E!Zx=)3Kd^JnS;S)3A2|ZpPjTcq(=&;3?Q8fSa(30Z+y*0z3)Z0eB*I zA>c;r0>HOpUBC@k2k-=}4Y(d_0lp1u0v?Yw0FT4!fX8Arz+|DUVVdnt;6$=6W1q%Ycfdv5njQIgy$9#al#k_#O z!90M!#@v7}V=lm#Fel)Pm}7wU0on#=1$-K_0R93q1O6N{9pwN2&wOHl>+>_r2>4UX z0QeJ354aE00sa`%0{#%w0R8||1AZS<0e%lt0)7|U2KW^A4#4kVX9GTgodx)9>`cJN zv8{mL!p;EvCZ+&<43h(X9g_h*ib(;#hDiXwiirUq!9;*x!GwTc!~}p}!1#cl$F=}| z4m%z2vltKXA?!532eHk7pTSNAydOIS@YC2Pz)xW(1AY=a3GhDbM8JEojez%HZwI^^ z+W>eMb^_ohu=Ri+$KD3`G3@vO9yh>a2YAc?j|RLGdn@1_*inEV#f}902zCVEhq1!} zKZG3y_(AN@0lsB`hYWDt0OtWefXxBkjv>I?FfQP&7zgnE7zTI?#s=(RtN}6y$QU3U z@ZH!f;EmV};0+iJ@Oo?-@H%V?@LKji0k2{I1Mq70{{UXa{yX56?7snC!Tu}YZuVaQ zFK532xQqQ~z@6;Z0WV|!H{iS2e*%0b`;UN^vi|^h3H$ee7qedjyomi@fIHZ)0$#}e z9pDA*R{*>0-vV~nzX5Ese+}4TzYN%9zXaG|zX(`o{|d0ieu3cs)6&nLVx7;DvgVjC zGQY&UiCJXon8!0-XFSRHDB~iAk8wJKLH{NF0s6J{B>imsguwf7KK||a34rVH6aOB@ zdHBotiGJ(x6a1dSPwcw{1(6VOxG!+O$i0bMiE{v6#PM-X=P=~ zVoB_5>R=BX?16(lu)GIOo^j$7eG@s? z_ED7k5k=Co@(#VB864?9+9MR@zCuy%VTy8JrYQF%igI71DE9@5 za-Z+ZO(Y`QNKx+X6y-Khlskc<+2xiNxk_5}fFi1psMT=dpUK&r%;~~4&tLWo@q5L#ZQ-!eX19gIJkg3A-*)dEg!vN) z^XvulE7FqdiCR4j+)-3bKTVjv{El&^FX)|3m_B*fa>hoSXH-!tT3F z@cV^v)-T=AI}11Oe8+SxXy_~Xl$M&`o-ie0Z%`Ee7)okT~cT1_z zCi^!-c-Aq(@5d?ocSP?D!oS5Kb{>m1^p7mRd5t5EM)=f}!|%CU+U(QSC(deJ54FF6_zi4qFfu7n)s zP@ye-2?jd&D^N?Vgk{za$vq ztzWwL7|bsTh7?;4rqhz~rX?#)FO3HKY5H8E)ssw%7ftu?K&(}AzSq)f7GX4U2=N>L z^SD-D&=V1+7w0E&^IMr7zIeUi7BI2T;ocVx5!SaKHO~5_$M%G{`K95DF$@o6DcS>X zmW8j0KEZA2oPQENof{^bjIxnwgWsYAgnz5vqDYgv2mYMy~Pj79}Q29^bEP47Vu9D};Z} z3n=?{MDKLMzr`uA^cE#1OE(|**C+p*d|lxdW&S4Nne|D^o*mZXk=>%iWLfS5#|F2k ziI|fqnIpnf8gD+xI585n3JL+;h)4z*}o%tn+X3_os*=w{saF8b8@0* zFemvs!ZZGdD0_BT?_{z$DI!aa95^ohFU^!^%DC!-fBs{7z_M#In|9tYbUJ;CJ#*UXV?-`k`U*mfky0|5&lD5* z!20dy_Ts; z|FREM`+q^ryLfmT-`c+dr;}WPWuK&=jm5zQc;$HFNq+Eq#yfSpPV4s=Yl=0)+ZL%2 zZzMcEPskT2aF%poSRfb3_~bqIikx@>#eeD2Dck-xYP6TwpMJVl?}`aiYcC^yIU?={rmUmGWxxMz*|mP?+X-a8jo$Kxeg9%7f_wFn^9O`)-aB-=)(!jY z5E6AV^5OBTd!x7_{8zVYeyC_2i6ZIpF|jnvzHsrQxFY;lw`*>wXr4^b=#%cSfiFbw zB@{*Pwd~>ydx}e^9J~W@Om9r_k3Ak+1XlDM|*=rotQlEKle60uIRas=ZW8w z{-oR4Tx=$}I`T30T!eUu@MS?MYVVC2Ip1J&L z;upKlvNJkV_dJ<8^0EH(uO228ZNCuzefq`YEjuGabc=4Lzpcxi9euMLLLzU;2h+*Ysp{%ho+d z)P3!Rp}McVV%dey_l4IQGVhiVOOL2~^4OueCr`HQym+W?imb8Z!|k~T+=Mz2+)Ez% zvt{Q+6x5O5d!PFh@en&ucmMtPfWsf}cExd(1R`3hylAdyLx|MaG#7jJ}WlQJnW* zMPE1Doi)t7F#F5dyJjw#vCSMieG2V2w1;TBXkOaer++{F<>_nY8Pn&?{a{{>4&fY+ z=S;qWeGc1=`LK=bKOpt&5$uQA*K+S>pUXa#^#<$fghuYu+`G9KaE;s}Ilr3Y&V6wH z$*D)@Z<)_dd_J7{)Kq+;&;bevW)ifG3a=1+Du7X zg1FZjh?PsRY>xAMzed>5F6kA%AYYs z92B&CzI>qOG4pN3NVXjfsas2zV|n@1o&6fYL{q3LIAVrKqE)y1EpENj7t{tE zQaw&;U+pMVDwQZ*DFiy&*@ycz0%d-?S&+%Hv4|&W&wDx@hfp7K74qqbugq%(0`Z1c zoKloiM(hv#nZ)!)h2Ioyrn`lb!RhpglQyeBYK#cw*|tIub;!-$irFL&w<5M^-g|i8 zgkPzU6vgFeT`m&@6J2k_lvY~0&Ad`CaJV!oQ7KxDM595cPsusHU&Gf*3sen#*Cv(o z%6YM~jR~p(CUBD6vM$GxL&zaM@rA;&6%DSju!|fN? z9f6QcV02j<%1XMTcE{o(XHI1B_zFRHlIKiRn=PLpYi0i8s2LGH5SKp0z%u9@)-PE4 zN(Tk#pM4vScuA(o$%+fvX2#-b z`QkoJPT^9BZ4yJW5Y`Lgd`Zx*Pv>QVnA;F9X{|YVZSJ^!4Vy|~(Mjcwf~6r0`@K$; z-crg1BU=0nR$auZDY#mJZqi>WIlQyAeho{TXYzK8mU60Gi+iN_VJgbJj$e!#GsS?T zpiJtFWm!v}32OPAb^RKymd0lg*=mlo(O2~9+JbyeYZTPICZ|855yoZJRT;5T~kTiHAXW|dDqktnhHkR)q@&x zS6WbX=tD`rP2W_u71Bnoo%2{YbuK=SyXmg5k)H!3^m$dq=;9s-bW-n(cO5Y;f0=u|h5y%D2N(rL3lI#`!W{uH5D2 zy!EcaIUDHLQ2Ny_SGi$s$BGVK$Rp;N^F@<4;#OD+p>WKlkBGzZc1WbJ=eY0h*HAR| zWf8xl(~9z)k|q+xgMcm-Q5JPFxtVWtnUtcGOQmqAR9eRDF*6E0GRVV)r7zz7;4pve z%vQWi7Oo{%7S8nxrFh}eC(C|K)b~RwR|X17U9F|>N~|G2&t@)&%xx{dq;KUa7K^;r z^%k_Tn#Wk0dA;w4BwSdjjoUXRTGfx4exYzxKWKfeg4MK6gO5s~R+smti#&nTH9|`r`^sP?Tw&0o;HOgVU4%qE@c&Yi`yy> z7*+9>E0*@v6f!|s%N*Q4I$4RN&`mi!$x@~5cQ)$bdPasHnV1l|!di#f;ByuOb#)}4 zXwCj~U?Wr%=nFYdwjAgh>vo|npKkFLok~QVHRWAtMKB)aB?~d9JjEE?oZF#dJ1FFL z?Ll3mC~WK9ZD~o-2#JJ6x3I%27Q>c!Eu?gXJHhF{_HDGn3Pn^<@<$vtZ7!*GN8^oh zPM#4On+lsIk`l&^x}c<5^{Q)(m-;nox^7S;>*%V&XvClGN^3E`C7trNv^BB2=1F+7 z3A>;bbedYq*{Az83R1gHP^kt~wWK}~@y7B|Z%LSROT_+oGat?iWwDUA+rdkeIe96gs~y)SqFt3!qt{ld34=rF%`sE`8i}&Q@8cz`sS4lN zb!5A`Mk)|&I&|K4Sy;44gBE^5W_F3qjoi$Y{TlIRDR1#gEO7}hrL~qUR=l+xzeCVe zw9;XVQO1wBW44l@TWQTG7iviTmX0|oDe26TMoZGrl|?*-HKD3%q)J;w6A~u*+6K>> ztIY)a4>ECnBU2H(Q_W&Rj&J2swoLf5j;5Q}>1(lYMJ#L?y)9X{YPQ98pgX3|Kfnjg zgVjlUpnm~7{Ia#9Q0kg3b)s#sG&R*OU(OGNDs8K=;EU;n$*NXcv@}}Y`TnGpcdN1{ zkEe5ZExK}@?-K_z$!Ig|sx;kAb*okOn3@Sq%~)=#jB^k6ZN&6;ol_uB)D0%RU#?4s z>jta59gwP9RRca+0ya;u)|4vvE+g00uhEc1T^*h(nF;B;I5TB6R#AI`fk2)oj>gmK zR?y)vq&iJwQK#e@2Ln+THkIubiBTTlNAiMBSf#f)fA(Dj~VQEq-$^?|C-LDZ%#e=r6jTa9X zWFA*l=kz&vHGxmrP8r*wtXi%LSsl%Q(r@OYH16X~%l-UM_!sd1)524nzc9Xq!@)lX zd*IFN0i4P9kWnUEG1*?L$>)BIuxOJp%ggxVnQX~+1>sDhODV3)(fsC=BO?hJ3SM2l z;mxVKo9ixh&E<5ayfy>={-UZBf{8}N7+k@ktq{rhZMDi&9a%vcG2s?;H)oaAQo$a} z$l7X0R1=HXd_um%Z%Dy?Z3|E$obL3{dNSq>ry7$iF>t(3X|EhN56a% zwPK}WJy9f1cfVl6q67Z|CR@@8Ksd3Vq}jMm5pnSRs!X;idsbHSZWdUBTYR3fP87@J zb4`1@DmO$rsbWj4&|96ty4|I9_33)|hcsKJ)E4!r&2GIymKGH}nvB(HvdEngbsMMg z*XLVawOW_ZmSnn))e&@MeBHb$kZCqW(NfXak+g%ANLrVPd)od;(%}o(jjfWIFJloo zTArHH;Ed_F1jH_9(I>X$<@o!c)+&!@n*L(QlqhC9nr69?54jwQM&06R)}%^lDPA<- zi%M;UoZ9&&cy|&9fA5~@%@g7B+Ea$%l5|?XdErw2ZwQwYV%fjsX>gTT*53vHZ;E1l z2nR;ft2hh%Axc67VS1 z(o!cajwWNq?*8-tTaudN%{DZ1jNYRFL9Y;xpUQL#K(_o|9zS;5xU1x1;HRnm3i z3!?dEEKsU?&7PDFU)F4E>K#`skO{cE0k1YMugK!=%@u{JwP6QwaT-P9ISxPUg?OID zGvvc{;!dz|el;|G#}4BCf8*2#rslizN6tMkXGPDWU1*B?e(sr^?{lo!^VnX@%>F04 z!}=?02lFq?TbM^MKFx5^e@(xbE}Ff2_Vk%g&hThY(Q4Dbn@&y7;)>qvKil_gVA7l- zw1r5A5o@IphmrDMI2jBb+}dLb1l z_!Up9V;b=ywC8xj3F3;XDR)IbQ)j4LB9bdywYGBf{W`SgxCP}Z$}JpKj_-kxhYEJB zZ^ZLUr}i9+Yu|#LPNbZiY(5w%i?5QkMfW9SECrJ#x_gcx8mJYC*5Jb65{}`#ScPR` zmH)bM&(ZzfT}%5%dY3enf7o`iGt}`}^j6$H9t_rEf9bRI&<1`5R%K$CEMeSp6z;?c z^P(xVZSo)(=7hrO9I%X*&C214$*id)5&R zBw`tL7gmh^6}t>pp-Wihzb@P}-|yYEw11>`Nprr3Z70hL$A{P)ZXaS9^~SG0jLB#j z7QK*=Nf`Ga+zE(fvdM#BX)IgG0b<<4-7j69jpC(q1LC4_4`)G_crAXZR$=K#?ZI$e z_$Zq~qRG0*Vl0(WhF?=QZVIlckg}=$yr!f?4aq$$+zE*E0?JN|iS(qb8t3m};)Wp7 z^C=rzCDN0|hwXb9{qCGXTPPb?wL3{W3%_J^+y+GY(<$2+AL&Q8axYFpQtpb-o>{^P zBGU6HJ3%!TrS{Aa%8BuR+T;&8WaEG7+XhV0RR|`MduX^N7v#52Yw$~MN2ywO#y#gRw) zWe>yiShr07Woqu;xj)Qj=Z-;NnCs5-&`$2@+#L5FbQ0$~++XAS_PdxGn_@r8PO_P- zZ?P_8X;_H)E#`GhKI6NL_b|?598G_kehK}A*+*t~&e~=Dz z`oh6?0nQT~JLkr!Gw}0MzJTY@F8=?U{n_B4z6m{W1|6S2v=b5?x7*iH$MUYSN|S1t zjoy)W-W3mi(@7N_pX1c4uAJc0DJdO~cGOk$L)~=r9dxH+E!CE+}iX`LEY?{%TaBs&}eE(Do=FsaF{kMb||&ViMIi zZCby0y{LIVyk66$#p^|_>dNaiZC<=y)Kms7$f`>QULUFlypa zZ!TyuhEz6t#?!EvY7|Dzj)lugvGVv)Gn#olec_v`erW;+5k#h&3!{dj_e(}iXJOP( zth&mm=`M^KiYf-9rm-+;s8$;pHSL8_L$UtqqvnG7Z%%z@!jsbfrs9Qx7RsiBK3lmJ zFQt4Mvq7C(eG-ft$1=%izB=cf3v-O3`c#A9$d1HfI7K(-l7*p0vGU5H#<_T5pi@*a z9O#^j*68}uhc?b)c%W!^e77!5-6ZBO=fZ^1V}FZ%9_uyMyI4msKgK+h@k_=fj1Ba!;b#G|WFzYb)xBwdviBT5>vTF7KV%RqrxVug$d*$kFQH`bIc%-Q$d)Rp7&~;iF*3i0ZQp-m zmV4eZD-tTeO+1 z-Ez-dzoVDF>J7e^E`2V9|C+4soO@8;*_G-RHu8|J?NHIqouvJMI+`<4`+;;c*6--0 zq3}RDn%#GHrMfqzqgj)*A5cd#Cu-mSjy~reMqm4i;rr&yb^0Xj2h=mO6SePu&t&hJ zSsua3Lf&FWkuCsDo~zk=XlpfgAT66-t1&X#cq%QMT4s!Nz41-y#=9?GtFZ&<#=9?C ztFZ&<#=CbcH%4~Mz_1$+#Mp&vHAcqUPho)EeZg9d9mqB7E;mNjNyBbDkd}4UYV1H- z)?RLm%k_u^$BUA(1YthQETWa$u8j8)fY?0~wlvfLQiQvPB$9+0u}8jT&$H7l*v*#2KL z;{AVi>d-09Wz5rOkH-HpHHBve9X^^FbN~-7BRShLJTswIuO=deaH$$kR5QgCo|PaH z$;4wbgGhJU;khYGGY(}2M@F01M-v{MryN&Snng8E-ytz;mDR4>BF!{nA)HyvBe&G? z+^tF3&Sc^5fNhRsJNLgA5?MA&^BX6g6hl;)oasEeC`i;=n(5p(zJHlPGD6wEDl=#` z$5+h^A|0HBcl(=9ji)^+@an7PQ@bP1Qp}>osmDTHzOY@&HA{hPq2ubR-8q}m6fans zxlXRAP;GH;ATn|GC!gA%ETzQ~Pdg#a7Smo$!;)#XlSO$ts%U1jn`1ark#9>m=15sn z0nHX!Dw1j!ONw%}>yH}zt}Quj%qteh>~@K)1wIWHAmcL;t8;)+Mx$&W}E zY$!GA`AmWs6v;@wmLPf}QH+O6#c(u{j^vZ!Qj)l^!#zjvO^dkBtKKz*E2qmj8zw*U z;QE%{HP^-wa@zVOCP<&NM@$?5nF{HP!ib56K%L2ZwMAjwxw&O<7(9NXPVX1nZJCNX z+jdHexpv(uN$Q10JPyLWf7#Soc|D0nntEi zMeKpJAgyvb%kpeMXY>ui$p2yQy8|05uDxwb_Q<-|Hl3yKE_E?ptCyu-^>Wp_77*&a zcabGPfY2c;c^@$)Ktf1>Y=8hEgd{*h4}<_ofCNGap@m*UXy1$^uVvXnhTIV!HZoXyL%zDQ-7CCIv!Y_oOR&&r9yL1|E+RJ+|e zak`?6@d-V##0Cser82t<+G#J|wPUC4H7%%MOd%Z)raV#P`3v)m!Esm;I@tdQprhw! zSl8<6KTye5R&=i~(Jr0+!&TP;UHrO@)?eSte)&&`ZrcE)rV|&Js0ds@`G^;W23c^T zL8pgh;shYs3t)N6by2SsfjE`|8f05B|J%~=%;9oS?1CksNG}U_`GUftSsO56HQ#4G8ilhYocIQ7j-U4EzQy!D(WJKNas`# zi-OTih?C?JHEG3PmIu|YWKOD=d;NiUMpO+(CRx&qB(9xg2UA&YDV28EL@XKn=s?@j z*8TtCzQf^c*uZ35JaFW|F9ztBPTW6n;e>5s-|@eXUpJl|7ma-}cHh_qW7e^K@E7nO z;u%=z!@=pns|F*uaA?=TXNJxkB56k2o;&Ecw}Y<(1sEH7a^zC%^AX?3n&G#He+r`qD2B)APhuBfR%{>U3(OxfGfX3M&(V)Y ze>r-^XmC_MdIRNHG*(d>35Yw zq|L63=W=G5niq#tlEW8_DBFyMc&(69Sp^QMKE^Atl{QB*&GRL4Q+}Un3X0X+`Ieli zz_q1YjDy$CF%~TBsgf)Z&GN&EpkJ#o3Jp$Sj8l`0SSFD=U}`arW!sD; z5(W(o$Jrd8C9l<_R2Hj|3_8NOwA!FEO&RzK30H38raT&XTjPFh#-cvL4He^DX{H=3 zTeDuhMCvD4ZWk+RA$?N@xzjESg>!z3w5@T_+Gea&WFB~!RAsoXSiqIDTm5>U%$8w! zG$oDN^yC`t$Okpwg$@J%I3>X=uj^r_Qc zgD~O=wHbl1%~%aewM3C^BC|qE$z0;|IJ$(jNO-NjphU!%NquaIt>g@HB)nD|hmCWL za-D4|r*x|l*`n4ZlV#;qi!Vs%%q+9e7;_a9oN!1>P6cGP79;kj79)oh7s_P%jM5>7l zuvTT4b&;e&r8SqQifLDb&~sA$HsfHsMxAz2&4Wq1Mvt^>#ETUjj<7N(=jZ(XsZvVd ziRhwsaaI(8(a{s7yei6bN_AFI%GF{VxT`IA(YtnA@Ne6{FM8c=|Nd}WF~<=zC|t?{ zk1vRuV%0QJv3msp$CTI{hs#Ytri5`}QfjlX<_72IZNZ)9s+}!*sQvpQ$I7rNjK_Hn)0B*DE@bt7o2aIpBFu(>%flvGj05Mk8TH;;I?k`8vtmV>uJ6;_$c;kec+Q;T7jsnZF! zFk-GIgPe9%v0H@@!3GcFkj1P>NsvBiI2Fh!)Z!376p3j4y10W=HY<5rBCV+w9Zp%W zJwcvvj!}Ka&cjIW;n-kEiS*jXj)Xg+lV6P}-gi4EG$PE5^x$ z%hiP1G3E0(-O^ahm$n78sz5+dbjdhnR+wXv`$YVFt8c(FbBr>+QW^{UrwY=lN-Xmv ztuTI-U7<1+Wvs}QEuJdC>;is?s@MiFOx8cseE$YQr`@OA!jNRq}vHDb$5c zx>i-07wu1HBjoEa5$#-*y7r>%^i1gd{rUFqi_DQ);_=7YzjxlT-W&doUv~7sjviRl18p!6q-`Q8OP(#qZ6-dEbEK{AxR_h@T3}v>sw%;AI$b$F zE2=cK&%xurZZoP3j+~R12o>e#FvM<2c`8^dXFWzo9>yA!sic;Q)tJ`_viUaPjo;j2 zWS8|yNzG{XyYoq@yjF1*BfNY?6$nX7ureHZm9vmHn~g zRGEy2dDgk3+(m81tfW?`T14Jt)kq{##cDjv605yJJ87%(P3F8q>I?hGoKll%L+sF> z+l*rq7{?N^B7q*SZUBx|L3D9=4&#KhhxbHIVr7-V^Op9ERChjNI$*J7}FVG0vBDD zU?)QSoIS%4a#e1(E#uAvN+O?B!Oz&l+^KX{*Pg3j?qe@vEmiCxiyD59q=H4a&*@C_ zLPTxK%r#1ET2tBJi&*8timTS1IrKBy8bcA^R8^idW_c{_WuFiI-RSA)|V5`XDWlL}>mJG1XK1BJH%0#it0er9VS^ewT_)X57!1VfY#B!DGP};E{=*NdgSO4Z20NnYln-&(&3%ye}!=a zPo}Hsd(!?v`w8tdnt`_8&_9QMHuTNG?+#`sE?b0d%l`vR5m;Nk)K^iWl?H-(qBQ*b zO%B3iQv`g~Oo(uMcqW^i=T1bEQnsd&Pzj4-yWhY`5g{e~#L}nnV4Fi=NK^wxX(Ap= z7n3X)FOnb68{)!BDv~WqDruM0W|Qb*K9PU;iSf+9zP(!8sxkFc7x+Fc4(h2c@bMN0 z^@MYHEAbIwSMe(`!&I50;Z2b>IVV|@Te1}qk438%NHi%YCxy?{NXmFRTQNI^HHG1^0tq=cgo%Y`13y&B{Y zqo=hvv~o_6G^C`a0&nMr!nks`=W$RQeP_a39+JG+SN^0Z*G8&B;ElI7znoDF!hsehKV~a!N zt&n2B*dc^D%%oNgF1Zlxom!z zjV!JV*twdLg-fzsPEXucW>q+PTP&^{xVz2aRO=jCA-ky2%ay5;%#f3Fg*J)H;IZ;7 zIaXLsrV~MB+2YsHzt-l^Sy(XkGa-mas*apT;8ZIWE`!M%mej0{_!KG6@(s0!Rc4A0 zZfJ9;9EL1uEQU#2G#r5WG}EebRpbbly%Iqv=hqaBp@2?W%&PN)k`@QwQWHDUPGc-9 zB*j@vhGVlBTropft?>&|qA)~-Ld+XT!bExWR>mNLuVC99p#(ux_?l2GDUQc@oNz2@ zsaVvUDVN#>GXnCnO0Q3;9$ekh!7b@3MHfpSPjGok79p$!lcbCgh7HPGIHT4m*cvY1 z$hK=^Hf*5Hk+e!-`XsK)5)O5tr7aAz`@x(xLXS>SmdgD3{NT-P4wuN` z&XgU-Y}H@j`)tt)H=g5!qwF9r5@b8H+9`oNqtEkPY1$2K4l8LQr+9i9Oq8Y-xYd4` zJuZ^0gnUi zHV5VOal`G5vwftGYpoA)AL-)`ZPlue^l_#Z2le!Ew4by%sHcw``EiS*kMwb^^&zI7 zK5po5Egb>)MMEup+|UPY4$67Huw7al)U%Iad$u^JC%YQGsLeq+FEOLFe~78)C8oXB z(&46_m-vDfM<02K`7I9Wd5K5g={GO&&Mg(x^Aa=L!_h~U;b=<-^~BQ5c8a<_@)Czy zI;@oQ5`)|N=D4-R(MKlZ_7bF?&v&T3%=^gat7z4VdR}7M+bs_2d5M`=n}c%B;E|1O z4(d6B+nIBz=L{Zrqot#doWZg-2jvXF!&kLADChH~{{ZH6|97uXlmFfS-s&sr$t&AA z1___o8Yo255uKKu$Vuf+Z3U(z;9JdHS-uujio?82rp%Z5RVrd^9QGimzg}$S{~LU5 zfcg885d6=MzyGy80IkecEV43#IW@%Pf=({$cn?B$Ghd!Pw{YuLX1%=Ypy5uZfodQ! z?Q~nTytuz$Bg5L7M6b5^<*BSssk2AJB{JAG4GELA0?U~*cUFBCW;lagIYOtTg6>8}_nXv3^RIhAs89SL60LcuwumwRWI zPELxIbQhhOa3~kjhE!}88=5U>V{)S}YY8~Kd5%0~R14&3UurJ>U*A?{|4oi6|2$N~A{s6h3ocbCHK|S*rmcKsNEt22)NWDDl?m!n z!Yq5+t<21Fws+Fv^75^0wS!~J78LFzKfupwh>$wxb-<_lnliz$806|Kw6Ta15tGoM zRLX=kRzZ;AI3h8z-6v$_Q<|bRDbP9jkr>Cclqu`tEHgaRbZFrkp4}5+&qdh|3)fx~ z_{+l{!Pc?o|5spGh&^8kfUoMxI!h&rFHg@fGk5Jr*JtJkr~TU zj@LkBL#CvFCtzv)UW3=CFSv{OG|AP;QhK)3BoK?E!kUy)LMt9>dNU2^9tdg{qyggXNiWK?bG-OuxoFOdC2`U#CagkTrJBf^Y?CFcoExnf(phdek>UmTMtvfn_ogEX zaaG`mi0v>1bDBfZqY*V7+W*7vUBTdY2L``8!5x2RB0BNi@mt4F89#D-VC<@~z}Wuy zbNID*0AGVWgvGJ7%=eggGONsEMxP)3?r36kn(;d0CPsuY3VsV_fEnyLa{I{1BeIdf z;roWeFgoDFFdKlJK2Cd>b}@}T^!U)s(1}C)3_d^b(7;6#do1~(J^s$1X~`e{5C3C3 z>SVE?$?Hu;Ur-G#nXFo6rqcDz-j3AYiIv0WwxeB^+{&0!?lh&P-n7k`ic7sqGjBQX zcP|6p`5AmFCEJHjJa83PU`$=jVvfFW(Zk1>-8@`YZ!sKVWr5twOPYo z;7BYIam0-9mnojxX`GE_4Z}aT$|=@*86%2+hqJiXz&p+_z=C%=(JH zs`RBvl|GU+m2%6E7wx~cbSO3Et&JCl)4dF7o3?Z)HSzNuO1orBhf-r*+@Z8@Z|P8K zB3d0vo0;oSLn!Dj`3jk$DwNJzA_@v=G=|=p>rjf!TkBA^sC$MEy{&?2PuN4NO8tpv zclrsSsXp|<+{jQAG(R#!@6L@3Mb^b5GxT1cUECP}edztUVWp_QHLOE#&FR#bTuDR4 zAR)^NZOs+e$Ch6w1LvFo`+Ys^cZ$qg8&*O0G8s5~ZZIfHpC62YbLR$wBJ1M87&vdP zKPif6_257HR^ijJNl^O5rk9c-0BSIUf1hd@Bd(cx%2SP z2T>A%|C@j3ux9rGT$BG>2f(;ai(I9tToW%5*J+6;hawidC+-ZL;Y$aafxKEFFMQ2= z_XXiE7S{&dcX^A2h^JXD5AWF4Z^LtUU-7!SUevepaMAgq-q(A>LKQuO+QY_os?cTK zdI!rKE6J55mopbqSM)xX&ZQPOid%}r*tPNPyR4i?gdOhDm;PCZ(!gPr8ZxM8j+!{*P$ ztLxl3TKoKhxUCyt=WcBc!%#|SY4#3X_0`pUZt*WjFTG9cf2#6A`!di5`mCDpYKF$8VuaAQ@lc*iKoEUZ)k^rLHWG?E!FpVQNZG6 zOQNY#w18A25zWr*Y!zaiLrfG?a$Dp0^#l_%YnB3cbpJH}+mdno_$(p2XSD9Vz7b#@ zdSY|4BAszUy&gk=NW%_WEi#BC4qv7aj#NteB#bQ#|0ff0S8OWhv@w&gN+uobRLD{+ zsWTR_AztB#IYz0|=r%`qsgeqA{K1^TXO3}v1(_%k5mM8^xUuke*X_zhi)I3_;BL&i5DiGns{{LHxqYE+&J<5iOVK7PMke)%0yuz zHgVE~eS(~jOo%4dP8>9`_ry*UBjcZrzdQcQ_;ce=jz2Vh*Z6JYH;i9Be#!WS<7bT5 z#xvug@#DwM<0=?efH!{l_{#A;#_{o?v44%dIrbuqGw>LUEqLeH%`o1;6=RLDbH`4F zaR(A(zA@*R9>yP7H^v$}WNhEDUB(#r=lJ{hYxwi{AMxMf_u#j~s081`XYq^ivtV?B z93H_>#H}z&!Ev|%KLTHkufWG~I`(huZ5XxSFWBSQ1K2NN^n&ZKE3r-3`7nw>1xsN8 z%nhR%D6pe24t5x}Keijj#0HojGGB*r2%lm;%KQ!U4(5%_?=vrBZe*U#JcU_c#+WBD z?M#v>VTzb*nFlfVX70os8U1wh-O*P@pBsG==1071^tRC(Mz0>dWc0$(Ge&EpnbFYb z@uTKZ)#x#!ywSr)SB~y6ijNL4{>6Bc@gn0H#$$~87Jsgb~ld&D@R z7&&T$GjiC-{v*4MFh>T4KOBC2_=Vx8h98BQ9q$;varpbgmknXv7e;5ZDv+85sk zG(w2l2j3esf{5B1-wQMXh}sKZ0UCZpt-$vL4IiTRz$ZY%i>Tf4-9Y0cMD2#}3K||n z?TYUL8Yd!Z7kp>XH~~>R<2!-I@rath8KAKqQ7{knAHHben@1T?IO8o`G_!-A+`oDLdhMA2~?XqXU1!-qh_h^Qfa5Ht*k z8o<5)4Lzi=FR;%+Lx-r(vClw*MAT>4r=X!l)Th`dprJw3zp?i~Lyf49v44Sv3Q_;U zJ^~FTqCUbt1PukEKEyr%4LPDdz}^Q98KU09UIz^+qTa>c0SyVF-of4m4FXYbV{d`R zafo^gdlNK{Mbw+v8=!FvqF%@T2^t9hV2<9`Km*|)_8RsoXdwK-A@ zVSfY-gn!r{uqQzS;UD%S_5^4k{KKBW9tRDCe{j$KJ!l~O!+wuF1{w(eu-{>if(F7r z>{09y&_MWyJ%arfG!Xt_zr`K~4TOK#!`MThf$$G|5PJYL5dOgv!2O_s@DIBmyAL!F z{$clF_ksq(KkPTyuR#OhANFhP9?(GehuwqS4H^jlu)DFlKm*|)_ABg{pn>oYo+o|* z8VLWeUto8F2Esq=PVDEPf$$Hz1G^105dL95!)^x+gn!uW*iS(N;UD%>>{ie~_=nww zD1?95t=LaM1K}S$r$7qfANCXMX3#+Rhuw_b1R4ncu$!!H;U7Hz`~Wl%{$W2v6v9932iUcsf$$Hz7W+PEApFC= zk6i;A2>-Bau2>-C}APV6hb_I4h zs3ZKtF2^ndb%cM|W!R;lj_?n=6q^Nggn!t#u}eT5;U9Jhwh7b`{$c-xD1?9Tyfy>s z2>-AdtO4o>|F8yD2X%yhSRLC4>Ina^Z($dJI>JBfV(ffSNBDp@DDo=I|tMe{$b}L3gI7i4t6%EBmBe8#=Z&a2>-BeVrPLm!awXR>`YKc_=lZ= zoet^<|FCZ$3gI6-Rh|aw2>-Ctuv0-D;U9J?_H|H4_=kNRI|bAc{$U%ilR+KfANDmw zA^gKm#;Tx>@DHnD6;Ma`hgGpMs3ZJ?r_~auBmBe4SP|3_{$VAo0O|<;up*WRb%cLd z0n33p!appFWk4O_AC|$=ppNhlo_Ld>j_?mlU@=ff_y=uyA%*Y{i(yevNBDV1v^(OuugB=a(P5e6=I||gB_;(a`B&av>??_Ax>P`Gxho~n0i7^qVH}Ow|2|>My ze?m+E>P`F;V0=(-;vWy=fO-@ExQJ@v9|vQDdK3TH7z@;!_{YMgLA{B8)7V;2Z{pt+ zb_A$5@$U%ia8PgJ-z1`%_;)zA2GpDQw+1^5)SLKs7=00I;@`p8fuP>R zzk?9f#J>Zv)u7(Qztz|(P;cVjDr_aFH}P*Jb^xe1@o#@@KTvPt-+tJ>px(s4eXzYi zy@`K&BdUpidtoa;y@`J-usuP&iGO=ydw_Zq|MtLk2lXcY?S}0F>P`IH6;Vz6+XdSR z)SLLXGd2P0P5j#l8wd3!{!L(Gpx(s4aSR9bCjO0K7^pY#5675bV-x={Y!qy4;vWMW z0UMk62N2c7zY&ZMHa78Z7^8uWP5h%{LttYQ|7h4C*x1CsA#4C_Y~tS_^9!)CiGKsk z&%s87f6OnKpMi}C|CpaLKLHyN{xLsc{u^vW_{aR1`4QNN@Q?W~L?QfRe#HC`Y()46 zzhFNA8xj67KVZHOHX{6EzR!FQY()6Se3$tS*og3t`401Kuo2-O^DX9^&G-MI2L|At z;(`C!@wcM~{=e*jb9SBG9~p359E1iOomQjG$7w3>6ci0OP=30(YfyCMxYUj1mIv{^ zAGf%wih!j$3A1Nai!Q!6RI|Ax z@d}TdhOd)N-TV>12 za+6%Uuwtu3$gr2lSNw`xHZF!SL6drwjVO7;Fp*bHZz@Y!Ql}hdmoc*p+F*iBgyYem zfMsJVg9*NxyHwU4Xrn>S?gwGXdyxym(xSfU50(nKVl!RfQf6f+4d8YnL|-Gb9Po7_ z^Awz;R7Xvc3%3tFs(`1|#&+(=Hlro_tMOita zO=)<_tSE0Y#1rhS*%omck`fcwq6mphepXPyPN)2Gl}amJ+U83)yKfUu)~RSWj58qD8pOPP4S>)R(-ABFI^|n=cyW9eB}7HuFCRFFH4XLOKIu#6T#Eq{}N) zX&AZ$j%dGtk|y{%b08j;@nfP)UXZWJD+zVl0>9rSNTbp7vN*~65o**0KWJ?39SP}6C<=&Xt&YMpqXha z2TvS4V(8o<=g=Vx9%BeR2Ce`JunsUr{y6fzk=)2}Bjdw=8NP1l&GBP~D?@iedxOhb znFpA^WS-A-GY=d6aP&8$8%IwXUCa2C@etz@SV0PZ`11n+3dOdoNWT#^hY#pCq9%TC zlS7mdsj}fxL0X*RX&e!~$8NKVd}&TuZ?{jupg2-xHW$??6~-E|TcX90RGFnE7q`Oa zB}+n>4Km}w&>HMG8O4tJ{^)T56MKG5Qz9({CpRo@&d zTO8D*!Gd;>Vd{wqR8!d$#Okb&5f`f@v8W5A-(%oPbfdd9IYd=wm>+XPu!stD#=iY7vZuCI;F-|js=a{peo@_ zr{p!;9)Z3&{4I`LETxY)2&2XwuuAL|e|XBRQh1_zzbq~=lTu?sXmD4Q=30d}_HLVl zSL0U+Z8(*w6@{X_$8FAfNU_nSw{czWse(-CjVENboI90@zB&HUH^+-D4(id8cRi`$!`sQez7y8i=Y3Zotgr%t|h0#VTG6j~M=Zc1W zC675lAD1!Z}4rX*4Cbwg|YNs3}~u~ey4cq&VQ@F(LwccS7E5W>8)6krLb z;+fi1+*YWGO^kMI{ysQvY3U#wQnNWz)K>f{mfVJb{Xv zt1QI#V6-^IC1r-osw(8%L^Un*`pR~;0^X{o6he`f1rwYS+^|_9hPi&^*cW|seBL+5 zXDevjp@YPWJbG7{d;MVQ^@FkXL#GFI^EzmSwAT-kUOy1ccJy<$`bMke)N?-2+Pe+) z>Kska1Pvq%AKYHK7zyD|^LdughJsNW~?d z-_H{j**bkr;VQ>9yj|K!ZRDn6yr7g!NsC^2wvu*M%EDd9@n5-iYaKd zaN~??TD9s!$C|!54r_7rF&@(`jy_J67ozw7!v_2V6PHZr#vd5>jPEw~;8=KU82=%D z9QHhR3bva0Gp1|wv(d`v9*o-GOw7+$1#&PeXF=rL@=F>52jU4`e|baFv&AviCa z&lQQy`3lh;pvtu=)Rz4j<@U9rLd?tNaYZ6`(jd2YRpo?DK`46~*89^{q_?Im_*L#) zR9lmlO3HF~;dZ^}V@GU}&lhn;d}k(}%XG;PZ3>{e%ej5XZ;#FVGjwNQpe80B#9suH zu1)@SCsA)mS10iWToH!|yQWpn%Lq)vae1>M9m<+S=D>d_}`> zQhOMv%iqRf@U(}4x+3bsaAJEHsLSqe7*v~1fErd!aGC#Jei-Io7hMPO7dZfjHXYyY zaSEc^3u~Kt9KzpvqzS@b-S6+W8n&+UWdj0 zh4XsET#>*{O47A1I5}aH3+h2|MO{xnaOK(Lg!0i!r!K$0DdyVbK%>={2C54rw8@TS zL$LjL`RThobFR3_)>LF-{_D$B1mg#=SUfIYz~YL8vaCNE=^FmvCM(p7rrAf7^={iV zvp_v)nte!FPk+nCh7w1?>3<)5W{A6$67!gYi;=~3O`TB0t05q>Y)6%JY)9musw*X|_hin=<9*DKSA zXA=o!qj^SM_R{kVu1FTCujv*8vbH@8?|enWplJ^Sb@|&k4C?kUP*+5K7*y?Hpf0<= zVeo8Hwl%y>c^LZBpxUH>8qm>*f`-nc(PFbS)h7A2h_WtWkrLV@gQCzXrBJJ0J06u) z3h`8mG(kM=TT_oyR(C#!EvFw|ue2AcO_FWtILQ(r=YpFEC<^VQ)SI)Fos`!r3`jHn zKe%`Rdj8)Cz5P#uZ}~exul~EBCprsypFa;h&M$^u{Tb-VzY==yKL$PaPlaCkEa;j4 zJoLuD82T$8hp&MCiVr|vunM#wI1KvIKL!2g&xStpBIZueL;f!46`z5g@GGJB`(x1K z{nYuZfgRM29@x@ao3o*ytUFuF*eu0z z-=~#@(Nv``sez4Wv<;%6&yL`D;dzUZQ zYVY#pTJ2rF9M$&12ndw=1$qjj`BEV(`ARJj^(JefL{2WrciqbjA4aj{_GL>&pLy4o=!rC3fvu^d6M+;J4k9os5bD3}_HuO)5dgrEnALi{a&Kjj(@bv6_jLxjM&39L)oAxhPt0CNA9Iq z?l(>Uf4Y7Ei!yBRKRf<*^uUfD*wF*q*aK(NW>=x$(HE}i2p-+*O?vq!r;}GN2p-)^ zD*%mhlAd%f5X?nWVd!YZg-_924IbUoYy>*eb^3|&QuUF zOpxK@dI|5^|B~7Von}`e7&uqj0Rz2^x4t|WSRx1e5~~#bQ`aJDy%VA#{|_vp zrpi(6Iyja3*8wl-l)%+eq`EIv_2WjCv>+CVs05c(>j8k@tFTi9umYpl4v za!J|A*Rczh>8zTY6U!Yob|hsICk=tgX`ac<4`gbV$-LP?Od9OO5@^+o|35$*d~JaF z`;id*&yK(UwLNflW%eL+D7$#qjzd{5JKitvP=<1X^@e2Rm$0P4eb-JR6}u~Gtt}dp z+N4!mzQ!_`)ry!gZ4S!JRi#xYkwJI%X}dOP+LGsQ%b9d5$C=K}s=v>_g$cj5@)gmk zLXh%Ccrqu?7xc3E@qpUzO9|~&kw#4N{WZSG9+3-T8BwsLDXEwL!tCYI(baR}Fe#Ke zD~6&&?2lW+UVdC!QJXCWm)RYU>XW)@Ucpsx#bdr$x~;S8aJ<;62to0G;Gn!jMd0xm z#)5$fZ5ALqI%sb5;kVa{K#ktk4??kkjqWa%+`bet4e4~sV*zhMDTH2=@E9u77~lrx z7P&e8ke8Dpg}zBU$H9ZyCZUj?p=7pl_|=+=0+FnuNx(c|l~g*DD)QW=c$Vvv#=}8U z&z33lVxzOHaO;CHLt3xW+1xyDPNWItMWJFU9wp5=B9-)UCM{(H$Dz%|tqH9R=7FMc z_(e^JVerDU2O{jbDBFQOy(aLNhdo7Z?pd&!kjG?KwsjSqAvksJE2)WcZlT8_0wF|rS+&43nCt(nlCs7Z`qi7Er9KT z8j+aa^X^HOP21$0TB$N=EmZjiYn;fNGAbgR4zSWyRz4QV_!1&VvRcvFwi#-~GO}cn zDv~OVUn+=;3(iDpGA?uJ{6*DtFh8j(sFaBc$rqR6Re?8m&Z}gIAVfmyA?RC(Ttq87>H= zagCrXEUE%(rPe$>S+UCak|jR$m$;S`!KSyq{|^}`4@}f24jKR1I63y$vE0~h_-(io zW;ePFI|k+mIGwrs=nqDZWIW37!d!IU1bdJCXymxzCx%1N`~Q6U0Wgc3eCXw&bA~1d ze>rFxco7O){x7!qNV>^n+Tv0nV)Ix$!kZ%kT`xCVN(jX~7Dp>{735tdRu%j%e?eu8 zdbPE@Hk@-NY+WT{o7eTLOvGccYAJu&*d>aJGVwxXEFOzna=C)OE>ZI>gUZ<6Tuox? zDzU2ICG^geQsPb(h+NQI%%!?Y#5Ri-DC^lt3zThR%hqoe(z$TV_=}>c%u8o;#eAQP zRLHt2x_r!q^_vCDNf-1Az(7She_82bu9$1f`_;+5>mJ+8TR03Hu2@JAN=dhZwwcwhvR*Md zs3@E6SDA<_=D=u-k!<$_SvG=7Z1dWM%6cuz`EJ~vF`MdF+4fBRBl=agJyU;jp)wX% z%rnVJX}59ud^c{-m>u4)vhA7rYx-5TJyZX%1ZEyP+ukyA^J+Sm3(qi%-WQh$S+xA^ z!1~RrTE~jT;yQ16mVDu$B7G$l=}X3Sq9Xl(Wu^DJwxA+?|CaPcfuObwW6zjWRHW~> ztn}@v`@YLc@AWadeBEQ4_gOIC`Mq8o<~yp_H47DGd-tns+xsoHd9QwzZBI9@Sf~t& zT5=`KcjNXb+p}L~+cRc+^s8*!#|+v3a|XUPFmcm_Z({ZM3oxR;Wt={C>sV-PPy7Lx z8U1kV&oFl?$^4ADndyQ4{CAJ$Mh|2>$vBfC055}UffJ05+%Xa#*?0Jn;R}Y9!-Mo6 z&<(T?X_wPvL;oDwI3yl?e(;-vYX|-Sm2~~tXRY+$UgSX>R$M5P=`&_;f<(`uf{+Y) ze;Gsiq*G1G`3m7w*`77!rNV@*mHFY%>LYP(RQ^M)V|w(}w;!T^lz&2^7lbFurD#!v0D=BD7N$H%scxt=#`tUdy!xF_{(Q;0567(@ z_uqa*i&egDc+hK~H8lmNQVvVW9LZFqUXemBHu9wQT$NY!8Hg!$CLprN5@d$2h4GgR zqL$z*iW`5iBKn&{?%d5_c>dt{!{=QpnmGHKzwR<_e)Yx8xBTTT`>e4k*itEq*a~6E z5@l;*VL>j*XX|xZdrHc6*WwP3Aza9KEh1?#pfR-sYxlYH$rInwOsOv%zy8w8b*yW` zSSkOl^rNRg&^xF zIdQny?&HhOzu{h(rnz*K`*5@8?L?6=_T)VWd~(~x^-}!g+0RaY**>dp3eMDI5?z4B zD|rZu-k%nw$*fSov(_w{jF3>Kjb0bOWEV+TLVmeb<7-)Wv!50H>g`?l4;^*o9Y6o) z8SklPPacR(L@!J`?kc=>nSEB*6fE|I11`NsKNWDQgjRD-VX_;i@@zX%3B#cEF{w!6 z))`Aq!l7lg1b=Y*ekc9)k&pjy{~Nsc?!$(EKVqY;VBCAjJ#X(p9`nIn&L7!l$)@0H zvcQu$43&I9W0lCol`P*!l2T#79x^BbYH5}iHCBUcNyJwTw*)^FuZ;O`y6JHX-H^S0 z)5Axseb)BgnT1oYd*Q%$Pd0kfhuLSfO~D1plteCcNfH5nIbyNFwUQ*NnNo^h%0~ol zgIi1+-q6PVKe=VU2el79@w1N(`_0bl_6YdvwfQ3`RT)qI|iwE0I*fGu2Yk#`vF2mP&?sV_{LN3{DFJ+QR^-|otz~WievHE zTy3Nj>>R>*ap5nH*S?sUJn!vIM<4w0!z=lxT>r^y8{hxgi=lmXso5UnuOw%WTOdv( z=CL^%ugvONvhS)tI#ji{hRLg5{>FQ2OS8EDx1X#z`{qB@O62UZ3&n|e zY_?G*aVVD*r@UhKSI)bB?MsbYZn%UvSo2`QC_DVo>eF{Us(gpk{`1Y5^K^X zcX`R`{Q5DErVuWC>)Tha;6C=U-8w6O|Fpzor#B-*CD5m#Ty4cU*Aj`MZ&`@V>uGoFGXM z6~~g|Zn*u}^^Toa?|G8rgxgL%mk}D8(4O)1PA6Bg*h>A#t%vZ)S$L!0DUQQq@r#PI zdFh$)=4B5iU%cdwacnw>q}zv*PuqzB|0e{1Nf$dr#bzNHxe=!2)qTUldy6!W;Lv_3!VB9{<*z z`@a6`)Nl6l?Pa?E{Nwyb=ScOZg~nSO$yxqFalPI;TH+3sI*5Zlcy;(9`(EGJG&Fhc zb$>aBl&WG+|K;!t9$0_aw|`I0!i)Bfz7zI(r)h~>_3G6j)f14)`unGyd&+up7T$k%R;Sl9LQCA!yVZ6(TYSW)mmOR+opJCz();gat;uR{bzz&% za{lVLN8TW3;SG0}xFxUCTjDmpvB#sL1J<4T+y?R|t8QdGd@k|FJ05@bo)w=Q@z(DK zu!lb;XW<=or?_6%$}MrFKN~p$_CI5fA3B>WU$OgstM{9H_YZg3W#4)7{>oGT{uKmB zcuU-}5d|2BdiG7k6&D`;4O=yX?O4r^s1&)7&Yp*WF%A zoUC+7x_*K0<~ROT4{9HN*)nt?&-2emE8bb+*^fR(vaJs%u*9>GH*dU`c$lcc+QHl6PI0}i%v$2KPn`Yt@Jr_(^8F2e zd;BVF=7?`htrL=s+l-Gra{1%;|Lw+W$XR$F+$pZtWnN3%1;3SC|G=)hoGJPB>FZ)| z8=jwi;I3=0S|P69e(9?}xLiLuP0qp_-%fG8uIpOjzK=h2`+C93ku~2re(d$@6uUoh znepVuU;lRH%E&R#oVN1iKOH@LXlqq1%JbRcALl&!-NA`l&&$w%eDDXCX^JOY`a?ET z^SX4!#23dnzQz(AJ$p!te^He37QbKk#p)Y|fBwPxn}grsy!P+CnV&y(x9si1-}uIE z8_&1~`_O#!?7=PmMK@V3{^Oop^>^OGhh2D9`1fzzU$wk{g`ptip$rXyZVCN-C^r{tKN9(g}2q*=g3)j zUD_$G*S$we+=oM-jDAi3v-I)b67BucaW}2n?LzM!fwP}J|BLgSx1MugYF*R+fAF&b z?7ESU;D2`f{a@;Vb4s&^BA>L4lcNXm7cGzFZx^Zhq=|c`r(UEK8S^Y@jpNMe?NLq0 z=gm+0Qst~rnTneo#mT5UArKU&t;Q59r-=r;C>Zj+T+)3j-6i`iZ`bWl&Hhd^JhkkHh0{puy&ExKeT#MA!iq~9nJ+7&68G9L^P z@p3Vc3x(xgjykV1SX4zN+o`gdk=H>>=A!qEZka)Qsc0~$D>71N<(c}_B66KSME zht4LJF5#yDy>X{!4?%gfzjXjazD1M0KjbUqib2%a!s5sj;+}c56a6e8Bt4jd5igql z;PXzBOQPo-p3TR2Mhl~Ff1R!3=K=$F7n0wJs4Z4cKA3}U?F$10pryXW2=xDRq(n1( zAtWuDNT_FaKvK&e`F{o^_1 zn`aMZa80LiFWMPm)}oJ*{xB=#P2`)P1!2XLPI-L6h&K`Pq(cZ{5^29T5lQC@>$V)2 zmIOD}l92|sY*oG9wY#>eu)&?mRQWYG%)VHfCL+nWH(*GoqcOAH>tLAy}D@imu zF+A8Udz4@ag~-*ebC{QiQnO*v(p}V_%PieqZ^>Pa`ycMN+Uwh~s}=qGkW$~SmOSkD zb!3HONqw_AZkgS4ZXXjssdGE&mk?^Hmuo-|R1p`ZC4uQ;jw48_l^Uhr7c7JcK_Qxn z>MIpTK2R;ioRSEcm$B1MBJ9hE`~tt!sYvU&Vzo9VjmZ)MF|QfBXpA*dAK~He4Zk_Od4f9}A7;@Xrx)l#rja=|`uJ#mbT`JW zj8hp>1`Zws7lP@LckwGQC3XgOKYlFp6?|xX&G;J=500nDZ=Q&aJv6)z{YCm!^bj4; z9-^H=lhMY89vZr6hy@k?W8lZMzte75^rgZcUMV6zYYaUvYQ6Ofb+m{vf`r0(d!3--;W z!@*NrOk)}}6( z-5CuD_3ZE|?b$_D{QT@1KD9l&sIo4eUBjofw_mCvT2b?cH_UaY(yl0)l~toDDe)=u za#^}-WS8hrFu)r&9SCRi|6}h>;2gQCyU}j--s*+L7-J0B!x%4Pd8AU6s${%SQk7Iv zsY)uTw3?VrT z3R@djmHh0Uz=-_%RV6>WZjs1uersYwI+ZiEp{>=44e5l=+t8L_VnaGfjBRN1KUY@v zKDTm<+DLZdIrTZ5dAT-koG3k5_a%HCM=p}dQ4!wZ4lOoPEt5fE<;!5k{G<=)B%kjH zt#&e%$$-NKV{W2$o#6TE8HGus>m)I4bYpf&(PxKpW9F)=RXG$G(-UjbS*@`(8DDv5 zT6v+GEv>A`71ld^%~p8xS#pK-g0<|(dMc21)OnwIGQH4e>C<-HeDb z4prQa0bXC;!fj^d(6ocgQ-`ol@(ZU9J1?70WvZ69T>iJkis9&JN4LaO+BX*(|9)LU4j3ry9(tH~et5xSXZR%6WZql{l?QAP$iKiNr$609< zxNfU@(OlsLvD$~4qAy*D!wG-Xf1!>#8)nL0BU^m2=P$uH3iV2KFdw&P%~Z5F^uWRe zU+JP1ZtKfCXOpTvN<@CQ=2%$n5ooRMMZtMM>JoFg%1) zHV@Y5GH9*exJU*w#fTU!_$&zG;G&*NiwhL(F1K^Yk@3ExS*Q}hhM0-K1l)ANu`FW4 zgp8#MZa(CTi711Vve2T`zVjJ>Y*N)ned4aUKA|=;UA>`EN=vRd3YQMh6(g7QC&j=r zo~cMpK6{7ZJ2bF7#Xd1VU|qF7q1IC0PM?4=n{+8-;Zd9eYbL^360RitSgpyV;@&*t z@KhjYHP%2$OUd7jW|>BtptIoi7$EXg#tACw^RwMSy%);3Y@S33wv-$&oaVy*bg$KQ zvw>K55NvW-w$e&8+tEQZBc^(ZAWD}rcAE>shZp+ZJX#QYwmM!5_h^q5w>ZGCCCQ_m zoZlu@9b5T zTe?<` zEEoJ#`a&h!u?ya&SGY)^7Q{_npoD0&;C5A=DHjt9I|~)Z;){^d`v2yYGXr&shE5m461H zvwt4H;Y#9gWrw?iTUpuJ-r3l=w0+W#RU<6rgM#tEe_Pg(!$XN?uILg%G+&B)l}XiB zt8e|Ye>9JN|N7!nCa}0tY=S-h+$-_Jl~wMpkzA{iT$@wD)M7PH4zxyB#LvzJxG4$s zxBn*n{yT%3XvzbEN3O(xWY)39fMg_)44FN~8rjT=DAlykJUda~NRqF9Qj+Aa-Z^sz zX7+${MECJ?SE7<)9-1m`m+P+_h0;wnvP~Wtu(g=QbyFJ zR0~Gckn*e)k|QbqlJxsi`)YE{Y3zn8td?90^SLVI zI!j@aM8Eb=Bk8`PCfA(4o_mFnT5{naimJs@pS0w75Iu7tKN`9|`Gh3eC%;=ow7K>6 zBUgMtGQ}V|H`twsG8#nB+-4g|@-wfOB>B0UW(LvO$;D(4r6t8I97IvIsBT%B1%v3B zl8uI^|MOdtWFKKvbTqe52L@5Erj!eV#nq)evn5ATe)x(cFo=32DHjf+ zs9FrVEal7~dZrFX!_@zQOVa)1J5{8c*Wt}qD0S)7LfYr0J5zTf>3;m*CFy>GP?2t4 zceh+|OVTYIL{%f?W$8{0qGxJvG)Vn$K$7nR85Q~FwRg)Em%4mvA@_^qJ5zrn`QCrC zB;St?ROFl2-;GyDNxp@HsA>vlSw49XJyUa|q3Zh|mL&UuUsRE7UUN5IA=D*PJLo(u z*_k>UN%nm;NwW98PDQeLo!xN7sWw=tZs^N`O;&toop(qQU4FUr`^itJ$ThFA8?HE1 z#y=R+guKB^+uoW5Rl1qTgjJsCsE?XC0kJ7Om-Hy$+4eBXIX65@&f zTl)RxpPd=1W~UaTp-Ni+8&~ox2S@vN8-IS6-~Q^>t2dv${$pzoto|PO^VRzKuKW{^ zZ5uA#_C#i7#qdTI-Pn|;rQxt6=n1$YQ9ncA{W#R`#vzwE6}5Lf?$N(nDYt!?B}CXy z;5}yv?=d`EPq-tUth-W8MI!@`yIm1DvBwZ(FAkFZdED7bx`^971W5y$!*~^xcRDN? zC^Cpi-wAV`bGtfe2yQbayzl2pfc54WKHq1Dc+bbjAwOSlr6IPEb{D!CD`)c)`5uFh zct9f_R1nFy!}&nSQHVoqC1YhIUIQjN;st0Ur9=qG;I?>D6heX|Y4)JIZwnJ_i6O;N z*+dPLg^&W+@);yT4xuk_?Lp0yZ&`6ajAm?@sp97%tq@Tfn92?xoMK9CA>H#MJrSt- z)hDWcgPN+97f1*v72$kD)E&L9R83qZRRi@0!gh)oB-|My5{gv--3Yfx)%JRVB5JuF zoVKG-fc9Fg4#v*rUU zW`|cv)h^B+4aaR!o{jTEFG*KCjZDj%{$-f#4oTu|N%rZ7N!>v_;G^aDtm2b*kGo+e*$_s_gTLm2PY(gyE5@A30IA z+PH6<-I0{GPtV);_bg5uWG@sw5z!JW_QE4o4^LG6gqp9OUAIcArsGcFyKpk- zxvo@wwS2XUD>3c_9SOLd;jwWs` z7aS(B;3RyZ5*5yjhQ+~&s$Z+7>e;2dq-qc6aL2e<*h5`Us%Ea1ucqp(zhv>pquCB! zN_4^`8)j<@Rb42X@S3R&QrIG{Zk-DAq)0|7K|uL9%@kerjy2P1Iq3`?AE~;3qU!Hb zQ}ygrR8qAgKm>3Qu*&e)m8!3ruO`hQa~aA)kz%S=w=xyB6SBe!tlHV>qabU5y^`_; z2hL{4;);gJBw-!~m{1Fkqy`15QLiB}QG`Ygq<5m~$JJD=+_(#Sz#J$@IAf9PO4V1( zSGzMkHk=3~T;X=qB@Q{L?`ES5=hZnU6R<_{iEdmlrLbC2D8da3lqdYf;jl=<&LD!J zQPQ3WaGW&R>z=6kHEOC>Ub17NE}o*|!5Dd6srqX9YA0+Cx(mUKwNML2ydGaD-U*e* z%bn9R5P}P)VkvvyftyWf1mnc?01jd$0&O~CY*(=JjeNrlWvs=aYh=}(6IDN^qUyPo zH0=xdX(|!$yRIWuTdvw_ku;lQAXi8tmbEZ^xEBZ`LZ-OTfrL!WZXURP~$wI)i}W>Guc3DDgprYAw06wYcB z7&s!guicydkF)6-1|-EHz~UjuC-oYf}ECHQsKCRb}bxi$;$>28JoHYtKO$)43F zb8GeM=#Y`QTI0#JS)ot2HFUSh_x=rNlgt@yGPkI|j{3>B&nw^B{H@L7gXCsv?`SW*chUIo#@{p^8#6{TcwO*QyYJc+cA=eb?R&_|=ZHb@tzgt0;C_XLRC)11 z21PVM%Iy^y6xIYO*Z(qz*9563BBTjYuApR2K~0cyiZ6pWO^|X9DuV)=Amt=U2KhBX z%28Yfv6>(ii(s@sb5~(xPCiYLibc?xAQg-7YJyZO!lMaNu?TAW%UZCx3~{n59?}G< z@On@aq+)#!Xo6I%?|w~?iuJuz6Qp8&_i2Jutj`XuKfJN3vU9sw50DlgSjB~58X(lH zB3w}eWV5SSc|jATV&!=)5T;V6S89S(#7k;|RK$CQCP<|dUeW}qbc~m4f)*Qu;bBdX zO2b(2Toa_yF=8t#Yadv8O7Jw`X$?HBf$yXSwEDJ9#r3!~K`O4tr3q4TJ)|Z` z#q|)HAeCXysWm1q_S;r1K68s{dFJHM1gVT)xF$$t{Cb&XJ+3;-P=sWUg|lhtV6kY3 z7G$wtuvTU4i&bq9G(jpRm(>KRm|R8^q+)VuO^}MorKI!!2UkA1a`=mf-*xCbTtE2L zgV!Ip54QJzbN@~I^#0!7Z|{B2o`3Hd#@{u5uQ6o2VfS;p$Gg$pn|8jib7kk@om;lQ zy#3zo&A-_E(arqkof}`>_#k*e;GXrrUH@uW!?)>?xN z-vIdodIoIuUsiv1^^w*4SN;Pqf671iKhavhbeqZr1k|FebeMJ|Vj&;v3y+QM{wJCm z@VO+I9uE!PO5;3CYyxj+!Ijn;O2lYiI2`ghCh)ozTxq$HVAvI8e1snaSxNg4_dg+O z!IhK4XfQ;^V}1t}nZRpWaAhCB6J(eSaNrr13B0NWS9)!dqazVA%qLtEcts1Y>`Vzd z#yI%6CwNl#vKCy~tr%xKN;z?dlbUcZX~C5pj1D9pH3QgceEx6L^+-`zSIB~Bp zI)N9|;q%KIa3eAn^~PPia{|w6!Ifr15_m95hCz_a1fJ7^E6s*-BzTUZUH+503tDic z*+d8@PSU|B;h%8NYQdH5?jv2EL>xSuo|wQhT5x5%lTnY?9dNq*z6m_71)pnoZ~wm)evf;?{goQazhHtas>GD$7C(%ZynKNFAo!=B)T z`zy5IO0)4eT)f*I^19;__$4j4(rjE#+#hmN98FK)FV})A&4vs`!h|~rE)-7S4{O1d z-iC`tLvh04!%w7V6 z;5>OEd{hgrv^p}vQ%-j<;r2{a7tw&v?L3FrMAXd%ar(sS!dh^p)w!IsoAY^HVSd7$ z*Mcjp4vdWvXD}9VpA1qVEx6L^XlEoAp@?ws#M=b5;7Y3t`bl@(PvQ8<7{_VBl~(7b z{UlDu*~m%V16pvU)kOk69~&hICpl4_Ukk3ZIyOOtd{NfTo)|5w1y@=f9p)T<7AKjL zc4xHUN~>d>p$Hh~80MtE`!wKlyW7)1&q>{BEx6L^rne<0aIY3zX?4?!eG|Ax3$C=f z=^eBQoYI0Tt!{c~rD@KAgJP?^AAss&eCoipO{MB}bFbuux#PYbTJI-eU%J2N0@7eYpmbR_iH*fvVL;K;@!RNMD58i*!*lZj;ba1f$rTq`?_xD}qzcH{rte82Id#wS*9*eY-QIM^S!ZTIhYKe7Af-T3YuJKxya+WF;; zFYdf!Cwcgp9nF4O?}sZ5lpnc;9N@P&Yhi*kAq9>Ia|lCc)A`%bH*~4=2kO)Ej1?#xNBNxe&~q z;9B`U#ze$mp=m>$JYR16um;&J8^1n6AYQ)AqnZA&!$;hN%O;epPHVlu6RwV`4iBtVB9 z6ZDeZX0YlR;< zgQ_VQGWn`OXQmN^JK3V8+GvMTbi&_4S#k5+7~$>IDXuD5lMQFLok_q@&y!%>Xr9gl zhy7$S+jK*=I#e&maib_B(y4UC&%kh~;emUBQoG->^zjS`CWYPUx+ClhxHwCiPQ&8n zT{6Oh&?z(4bH+;`W{hy8O~Y0mgz1Glv7w{i6&tL7m@ens4bkwKF~Sqd4699lBUkAc zuq0-KBjqL>b!8Z@BUna6tIdI=Bg0@dL>gAd2&xCSg@!j-gR8JP!6eHiPY@kehBb?& z1~r?(0M;$?U7xMTt$cHgaQA3;$HdgBL6H}0_M(L;@mxGdn8dc32+>(8pDmU+5Hs6b z^~s1N*IIM~zj!ZS%B@Im(;iVy29a3-3`yZtr<7OfVs(KP3)P#?)u z@?ybeZPt?%RE9E}+hYV7D&?VepYr9ig0Jr4%PD&=T`oJDrdAvhJDsGD>YG!r;Dpz{ zIz~7L71STiAr2ekbLH3`T695Z03=RFh6B88w#Mp-p(AK2Q22UzjBxY=bg|(Jdoq0? zgSV^@SD^|4qF#$~QPJ0Tm_2U3(PU7jwYniAER-iJ#E6(VVu@ia(|2~PHAkWAtTifp z$IC*#VFdEU&2`UK?4~uV;ryu!45OqgyB`{pLO4>0a7lZ~oU|hryMR`mr5*_H9M&yC z8`yFp-PQymz&0A+bd3;jDw?jL*>1TLa+<5CztRy(rDVR=p;Pq%U$L4q;7L0>6S8Aw zRhwD~NRvCTmn5qsoksALOH{HY=8mZyF;8v2L$T_Q7-2 zJx@97Kqx9N5L_l+F7;Z)K(QS6XW1U(L2YK^t7L?!Y$-=DU&~q?ge=*Y|GdgBk)Fn_L;H)OElL)NsvZdfc(Lb%UK{Ov(x0Y(6w+G4!~FBx4MY5w%dK*JdhAw&WPNdfi%G^ybo0mMwB{ zG7{#Q0NS-C+Lh8kG%{mEv|E7sy?(UpM6A6`p;gJ*sUVg})+vSyw|sQ7f+nhw0K>z( z|6`0umT5QwCzB%W_P4?XA<>N0eH}7}heUrLDmEh_S0Gt#q)n~e&y3AycIq}ZpC2E8 zn4Of3KfHJRVRptc{_xJx2Y8;;TgM+}XBlI}TgD&edAxc2VRlM4M!aeKVV?PGM>R(f zkI?gjek=B>&9JS&R0eb;OUHZ``!HAto3V_)=*j!DX)n3*@i79fr!8)`IbH78tS(o1 zfH=jJBL{^tT{K;=Q??RR9tCU$K#t19KaMeaGhesP+*%wBvR*#v$VytO62!d$H zc(m2EVUVpS<_)hOw?{7m*=ikEvR>$=Jvx)Jpa>?H}2Ral~8 z1A#}1cr#i{bCk;*=b)%ibT?>81@U0IWw>vQ$Oj7%Tf3EP4RW0-8!e>=(R{^+7pP91 z7*@=*D{kt#tm2R{Z+vZx$ob$1*vBENqNxPuAx=bDsw)htLsLEG4kKc&nd!#tCa%t` zeq@ZuwB1lEi34}hi$dLWlmqW074fXY?lECF#`kPMH)K6548vQpXUJr7c&=;X#Zn#a zHl10Eh_oU$mc<%9Cc}Clr>V@g3!Y+++c}nbAPLNa3zew3%(r6ZY!nMPF~MIBluXGE zRL!LQfwb8HG;iMQj%HDCs!lsixpE#24O$H{f_qZda7ZjVSvZs%5Dt{{x;T^9k?xA| zF^fdcQ||_PtyZ9zYWK2c+6D$?q{P>tYPE(95_Ox9aOWx1g&W4*15Tu@p*+LqGX6}U z5DU~OI}Z5*))sj9%|Usnju42rqJdmA72ez&^N3lfB6!(^^#;N|x5I7=@+Ar(%*|d6 z%DcTox`25>?Vyr1zBQhA!F;S)5WqV?9>;LlF`)^>W@FM`$Q)@pF^tX@A)c$lQ8r5A z8w6sVU)jL$9th}m2Tx(eOTP}1> zCSN6KFXP@J%ODog)$(veJCW}c`)+SH8(_BoX3QgKHzgv`PB#G8l5VucQ>5Dyhj@sH zTgvT{xlt?EI}JaPG?@)QIY#(TqeQO54i8Ml(<+ zEoPsX5r$MY=>lV2uO2qMag3k_eIHY&$$~$VwpOYb$Zr6{F`-dv!93R+3W;nTMcFQ9 z39o!nM)(}DI3^J0Lb`w<9@d?LQ6bjkths(RkPlVLwoJk&a!`<5-yfSUne00H1vX)~ z1#Cz&-Sd#`43V}c!SgX_FM)H-N;y{%J$4kA$7UkmkEIhh6j*MJSUXF~e)dLgC;LmLZ9HC!Iu-K|5BAqXB27)Uab-oM`r{iL~g?(YXeW zZ|sZ__L|ijBHY$q)6Ex45uqZAWlz1G%(#N-RJR@}^qHKw?rM?c?d}+X5*SfwwnR@9 zi^I`4jpQ7#GZl8`8<|0V5NRWkXd!D$`HN=5?igWnQHgT8OVIXiBxnmH9h}RRtToE6 zDuZQV9yRx44v^)fli*hW$5>TJ(8+?sMV7+@Y0T3yWmzwgaQH&y0_0;U%u)8`qoIO1 zKL~SU7Uq(r+2_l+KkcsjicB<;swbgvjAn;emcaw=S|nJAIR&s3SRJnstphQ}mL~NX!px3mI<*LDimU^Rvr)Zy3S%O z>u9#?j4x#dPsp|Gj)>pq*+ZrC|Cg-1Yvu5f!{;4*>>#@T&--uNzhm!1doJT&8NbK4 zv-`yExt(9wdF9T^_Pe&tTmRRVfAb$VYnyu;Ke0iqe|kN$zP0w=HJ9P9438UbTmAXf zORKAZ*j4z+J$bNc;O-!;#uuLb0>DXjL@AH&Ot!CS?=c>KwIiP1i{1fy}D%4s@n zJ8XfxDT0qHuCyed+yk_&OVOT9yL!UGZhId$&IO<6hm={`l}Y{<&d&q%Lwa!;59oGR{(v}t0fsgHJ_tf2Hm!P{HR15c(WVMem+5M*qU2&FL?i@bNs3iBhL8jtI23$ z>tJKjVvgnAl{s5KegyE9Z2eif`D)lYruL$Ya6FXxJoD^#c4S3r@lksd=F;&1@R9n| zGj!KyL7&3ZUURv0+?VMe>aK?h{e|zbB#(Q5zSL(Abj!c6&thtCsw9uQGKYQLHCW6+ z?Nt=u6*_>0C{GMeyaJ~7^a&86Eem1PT~BQxl;@5oLa4rS5P(=cpjPagq`F1h!JPp zx|)n8ZV!d6cFgBXXIaIVAsmZzD>D5x-St>RU+o2rWmDVO4hx z7IRR0#{zhqB48o8y%pV+(Dpd9d+*ZB^u*(+K2{;E|8HM;_sZd`5B~k&JqP#ie`WtQ z`#10X^d4jUN8`JV&)q%RMRq>7)7yF8_J7}Iw*GSKEnB;rAKdh9{N=`D8_!?=p7rx< zA7AqsK5ZzhesQ(2x&ugEouBH_eVYR4aVzJG$+U&HTa#V0$3t|)v@MZDAtZ0J=aF#C z(kwJNwjp>cfkBkKtrYAKLcio1dI&$A?h#&2(UDdAjBsQJ{4N51wOTaP4HoKsX9{zX z_5kdRGzV}e?Z-`?dKL>26<-~pZDz39ubbc0w)00A;9~`RLj4YAaiQH%(c&%TZOtlT zVHrA7b6OL_ay04Bx9h18=Ims>oNhj|7Wnwi5eit{aS2q(GidcQB2}T{ShQFV#gdLz zmdmpi#*;IlDXwfyGzFw-&v!DqS!rA1?225n#MF^Zk_A+|J|rAW)(m%(IM|{RG11pR zZ7@F!dFv?d76Yj`(v4saGm7+d%Q7CP9z8-N9^}e-dpe(uvq{#JjZr8cY`VBez|^ut z@P6LYPn4XPBf$i^F+56}Z=*9YHp=GGzD&I3pc9m*zSv~s`c5|;W^ZJTR_oOx81R=& zc2I4@MAYAJWxc$EwD%iG+UIkk*+|as%6gm)kVbhRSR94)Ag`PM{BR;1Sph%EWZMpt zgGA$wN;cCggu(>DR`Ef-TSL07f-T`nfLkoto^KFJlKSgAYqAjFBbjWeRdVwoI9#gc z*l-H2Ci^W9ZgJTmd(rA~gM%3!MY>iypJ8#`^-`FuMQTD|vSD96kk2Q<;~0f}rQs~O z64gev7w~ZL2;Hi7Trp_a4aYq>vIFa8rERjalZ~;-nk894wQE6lH%m4lU%im#;OL;s zM`^rjsS7M+ayA4gZpO38KE%6Y`kQlXve~1Ho7R!Z&fgNyy*Qn-%-PO2vdqlUy_?*~ zGAA<~yV@p$Zm#q8`0$YlaFOisgp1m)pKdNwE4*}c51=kt;R$uMT~XcCXKnAXqYHq( zWO^s`)pihc)7Lh**$J;?aM7c?B?)F~sQN^%ZV4u4cGtwr=4?avLSxR%W;={*W_N-L zNDgqKLe=%XZm#oYmN~jZ$wh5-uX~|0&*kBxmjEua#;m%=*Ik#XF+Y5C`vU3x?XqPCT`}16E`#G8+U2R3JyAD(8myVtXsL#3?)z!3a*=OCrrK6ja^wrkVx}~3`|Jc!U0sUDwqqqi4`x^U&*Esjfl z?_lR-KJ{&Mut(0d#j;RK7PIH>yVVAp@459{tK9^#1xc})6`I9b9{k;Ewc8(kirIs$KHgNUlF!|9>m%n{g%pI?0OVXL(|0zoJa{H3;_Sg5 zTdL~_z}}{(M+*fvQ9(N1ZoT2P_~`+5?$Jy4+&aqwwc0Iiv6k~_vl!%THET8oF|Hs4 zxjS_xgMzbJS9k#Rd}-DqqD{iZW!es*od+xck8|~=&{e6{HRIVH)CEZ)^2XDw{{bpH z*A&I-xJk~P?PDZBOQ?a?k|?SqZ<~AhR6=FMS{t+`fIV`q(-N9evp%Aq!hN?|%;rY{ zoe?&f6>_OgMWQBI(TSZrGO1)ORh8T|jl5s4LLdaG-)fQ&{Ho~VYxRNdd@t0C0Nh9gkRCsZAn(?imJ z2V3RqLAwJja4x$yfC$!5Im?@SOw&mufEygXWxdmvwNPy5nj=9z0&JS$Zr=*WIqHKNF_9Ar#!# z16P_aKq8q(?3p=tsWxL!un5`x9vI41OU_g^Cr}}g>qLAWrtL3*=Qn)`&dfkrb1PS9 z(c!b*MrRE zJ~1^MUKp4QVF!}!CY*_0v;yZ6VNZ*+SmKsW6m|A-6nBtM*v>Cj{o=*Ag@McS2|-hw9IUE@vKSBIKLe?fWn9c$i8sa6pOM6B5Z-9Xun1dB6v5(iYVps zG5JuwP_|JmE;EdI2oGB+KV>&CBuc*Yux9f`W4KtawLBNGfZ%oITd>(#D>WdikSTO{ z*e`a#JFC`k3J334qJ>P>6Y+E^#Q-=9FBa^jx*5*05l5AYS;Dm}4wgGQW=&GnAAM9> z|3fR?mBasjm;(_2pFVi(0NVeH{SWLX_V@RGVXw9K65|()Z!tc!`wzQ6vK!mIdFQux zUbAD}{>$xmZ~L|nwm!A>`Yp%iUvFOCd;r`Cc-scH{wM2iTDPu!Y3+4u_Zt4t@EXHi ztG@^G{GDI7GE}RXJJp80 z!tuxVj>z7tgKSc0$8&TlVn_hqUkhzS4Gw6CSRq;gznAesgXon||W2-Tbk3?LksTXN*Ehkz5O?J^(ZjQbi=* ztCRw@ju*$A7#VM8N--9Wu*F7*wudT>7E~#_8s>^G!yjK5k-eaUte;@KreZi-Xb=q5 zWK*UHgkdEZAD||<>`SJGHn>PM#Tf7F@yB=M^j_~W}qWbe{Jc8S|5{PCS5 zvUlnryToM_{`ig&**kQQUE<;ifBcdW*_Y@byTlz5{`mG0+1t;)C9ur35&ro6i0pYC zWRt-R>N7XJW(S=1)ZB26jKHQwxST;F@tm2BmIcO^%g4dJ7!QB^;t|;w>ma+tB@_Pm z+=%Qs9b}idOTr)DHX?hQ4zf#J9pR5}9g)3N2iYaAiSWlS8j*dG4zf$!_`r`}2*^I9 zCw%O@&77>Z^2ynVgwcr>e*A(F4ILp+=Mnpe#%v^*Oyiaj4IKeIsx)4D{Cq&eYSa;^ zvmlhtdwerM>x#ZP*$Yz*s_`B_Z$wN-NX?X(+NBon@l7LQI^t%g#MJJtc#oeuBBmoq zW=c%$s*3mc#t|_c;V@HTYWGsS$IlrN(-GHlLQM6F$xDxK0K{|!vMh*C@g6^WB#Msc zl&L6cK_}khXN`#I2sxP&Q;RI|9zSzLOh;VFl$hED6YudeM#OXkk4%ZF#fo^34@bmw zM2eguw(#1B_xNB$Oh;JAl$cr!i1&DZL`+8n$ds7cl@Rao-iVlvSdS?&wXhBEv2jF9 zN6f|vG1aRa-s9a7F&)7dQ(_owwxpUvhw`Se_jqSSOh>fEDPjvRV!ZTt8xYeKUa=r* z!h5_m5=BSM#8ecuPzdkw=7^Y%K!_V7|9=1D`)}ND z?)&%Q{nfo++WV2cx9<(We!%U!+dE(1`GcMJ@4S9T+=+oGfj`>*#P$zvzizvXRZG+$N_k? z-d|6xKYRV&wQsKd(pr1%ZEIgxyR`PgwN=Ak?nU-qZ2TJ7iMV2X)R;CtWZc{R!tO8a zeqgt4_`re5@OA@dfDO-E{pRW~ufBbCuzKI>3swy)e>~c5SpAKYU5BUt@n2R04{jLl zxMVPzHV7a{gK_dnuIKbF+-%iGvwq|E$V?^> znaNw_dQJX3d+)&b7Wv}~7FGJqvUrn^Gvd8T7H{%Nhj?$4#hXB6@!lXyaLXbIzFTH9 z`KW`<>t!|*h|K17GNtFsY;;h1LWbO|AM)CB8-|BKJ3mi`%-?4)ewVD&o8%%hN_|{b z#dGCPr&{VYvIX9FS{jv&`q`V3BMW>?R?Bl_xU7~}%XPfrG}Wn@&6@Jq%pR2?&(;rl zm8_O$$&dwVc|C77_11i;8TUn_pX=AwmpH&v}k->@e^tbDxQ!lK0(VBkIHiVi<*|_<%r0T|5raG zEX()L>J7uowfm-AWTti@*4Y zz<=8LHSi{2W#_@2XKerJ_D=(%Pyalvfu}X_v<9Bmz|$IdS_4mOKve^#wM&M@7ep!C-ohxaAZ}@$!(zE*`qtc(!Dm^;|AC>-9t9=UKyHbJoEK&B3CCZ*#qUbJu9G9 zdd~k+TBYawFRoR3uGikDQF?BN!T@WPKCN%y5$@G0J*UAtwMx%v@FiNM=QMcRDE{B@ z-&TxATX%#1JpJ>u2CicbyrF-^A_c9z{n^u?HPsxaXJ^bf9kiwv;|rT1?VvTayEPy% zX*R|;NTuRw#Lvc?zV-z#TM1#^TDV91S=YtpFyVALtVoMSX|`m;=VE;S_Y5OO)$U6v zGzT}+T{98R6k!XMAQ>jrgs@68D1_qfe3ri0YGqwcR_yd*agQ9jtO&iE--nli#%9Ct zzC*#p3j_h-cpy?A_r~fq^~Y^;9B5 ztXLsaRKG8HN@z2h#$A2PNd=>c6b4r>LMpXv^v-vA#$@jdiG2?sGZVz6qfLU<2RNMDlli=0wKsuJwEHn~g%z99v zooRF4EQw;BC~26P!_3~Ho6wW-<(lzS_@3z*j?(9xnJHZY zoSDGj%zQ5=0!Wd+;Ki*<5#*2nlqaqR2LC=Icvs!ksC9sE$JKz;FH*XVg|4O&r}1>4 zQBK=Cf;Cr*h~Rn13WCDkKn=3+H4wU&3)zdgP^Zv(%C06?;0F-hk8r-C*;2~0M8xN= z6fnVz_j{SR^wMHB0fwWd_d?lq!6BfHh^0>^y#6+i20*xH%8oa)v9Om;r%PEZbD_W% z5W%U>)kswzxtfb}u10OFyn53dv$m`GuQ~@g?P{*cWbM+V3ypIRfsS@6XVx|2soT|j zho(2z)YYhUfN#gum~EKk{ua8LJ`9myj*)<}RT>`u^=)OqxDzzrA&(%m(fArB^1HTep`IVKemu=m& z`8C5Y8Lk+b8@`S6>;Hc6m4lD&Z5Y37{D|?bM#1=i@j1JHyZfuV-@n`0Wp;1h`48|a zz)$YHZs*btva`Sa7vR?a(ROw7H#UE8v%eYGymND9o>0bIe2^UKes-;b!Dq?xT##Aj$S>q;NeDtrou!Cq-e0WGkGFq3wUe2 zsI4}j<2BLJbA;3J0Uxz7+Yc^{cxa4pwgaI;H|0(R+f7rmOe0)AXCfJwCy%-8sUpYc zY>hZh59_^WkKakR5D9x!^s^LNF*hQuMu_BVB@4rVaF}5_z!7dbZFeV8{{SU-9v$;o z0`b_=h*ysh?s(G)dAk{;E|?&5JkZX8Cp8^pq|{B+YGN?yNi*OX0zMNEpY{7oBYtoF zD=Ql-qsO2vvm2A6OEx#Y=8iSaFo{b6xlX~OYEf^X98jcvxapZX%RS?CIS54RHFNBc zQ_?#e*MqfGFw3A+uZXxg&x7n|=5{t-w`HmY@G5ULO}kx;z=nq+gl*qAM$jRWuCoc3 zbx?`(a2QFL2l-qs(k~=&Iz>^1qTh^Rv39=R+PZmZ#Q2q6%Mv_pTDm{<@S~3toB&e z>&htQ+Z$UuPo*sF+>kPR2!{#j1|#-ZzSc)b!tA$(AQxR2c5#1{H&uHF2V+DC7OZFp z$)(tOIA-GlL-SIhue$Gq@RZFzYdbnr*`O=6#TN*Kbli0F^FX@Bv+9+H5 zDP;lEwOItN&}=A`qBdx||gxq9VP8XhB&H)mhG_vmA1|oEe7gF($>Pm3%Ea%b9hSyLhS`1X5mLv8s#0 zm3;5jSV$%$PwQ>pVUjMvAZ}?KIIGPG)=IgaGe+X=%h4mqz^2cqTnRfBCL4Db(lVo!w|T zLgrWsnDdZ_9L*}4h7#dQpGRtKv>9fcAWtN>_B&$)5-r4&URxnrp%X+c(Tig&co2*T z+dM_F4f4Wfs34cMCb)DNXlYH5X6yP9FJ2mPZfV4ArwtZ`VVlz(Pmwb%IvODEI#tf1 zI>WmOj87j_&d9FM|9`4R>hr{~&T_nT{(pYuk(I+AIDGKna|e|J!~T!#Qy{|seh_Wn z1#AAF-lcc`bf>@be6YTUwtjc3u=$^xPi}5)T-ms|{_%Be?RVF3!)Fae5PRQTH3CBV ze;(gEa&1ZP9||k`jkA;0=?NXNDX&_bUm|f{_K1|k*$3j(b&E3&!#KZ0BE82~jtGel z3Ha2`&UJRW+S9nCxs~<#CDP;7kDNe+hoq`jb&D{6cDc;cjD;fyV0lquS<%h%WIwUB z%&Uy0BOI`@O03GdSLMpS@9An_kb1OCpD*J#aZ3lw5?cqk!_hL9kWMvNgTZRVftQ_E_AlvXG-IJB4Aj!!z48bF48YUNEBi0k&ES+>p0H3$iubtT2$tIA z65#Q$ZXVj!sJtjS*@8ywYT5J>M^;G)siFzpLdZ5asiJCjz5Ed*5tl44ru+T2Im?^v zVzTAU8Ds9q0yvE6kk*PZ&jXKOCpUNG<8!D~>J#Mk5ms>#orP6IXrE z@fAn+P8jgIYofwnVRF7#9+?1xQ6CEFuED}Sr24SqE06A(un6j|!(tX{iTpq}y#RQO zx+$l-8rt1dc|-Z6o2tG538c7tB1J%VO|_+%-5Zcrk|$EArVdOT-6aVjISapTRVbW= z>I097qdO2B`Odz!rZ$nAb+57~nsZLMqn7|qBTq!@=A_{u zq+8>dCvAnwxb}Q z-|M%|ZN6jkj*Xw+@UMSrJ-7C`wes4U;hlzq)eo*dZ{Ceu{jFMEklI z^Rw>n@%_u>=qd8&EVGB8o0YcznOzo1{zq!j)MY|`j(myD2HolI;%4?(CFhYP*;u z52*SQ8wg!&5&ROVePdu^E%w-#Ra(MRfY}Ks<-JIv`^5M$} z;4=HHwOwsLK{uDFHD0<51L~4BPS)9WwXFo*)KA&q+GXqLUFPRZ8K~_N=w_g8d9(B9 ziRDEvLy{CT)l}V1&@IKp-YgS)n==yKtCu-@o9#U__GSi^n6)?64Fuhs=j|K20Tx=gM5;mf84)EDj_=&r+*`lZYF0P3@SMs@2zx9qch z=F;U0O8RR12D+u6rT_Ta<+}j`XlDP~-Q>gIuNWsF@z^zt1GC@CAZw-HYcrr!(s~XPnOM zpc1oAM|H12H|KeGkhy$*0hfgv1-jSn^IRUj{9?dm*6FD37U-_a)GdtT|3AE9d}8Zf z!-t>Jn*3?d(;E1WY2XdREATeQJ-+s~)=U^KVzt{t#Sq<6+yFYe+bu`|(C*jd7u#(X zXcU4u|L!+zMzn+Fu1Uz(=}5N!s_}T*!E%zLUg$zWOU%XsmpkU@u$0gFMMB7h2SZO9 z_YB~=!_sVqtQk-I!eH1P_Hf_eB6ywklEhOHELRm;E%27S)WH7>5&@*e>_CdgrG!dF zytOAbD_Kbr@H*U7%(l>PpNiDAffDnPx{GrFsC5a6%Tvt(fY?V5J1>lu^R!*PDrRTB zH4J76gh-`HAI7tJ+G)-N2DNO?(-xdjM}&7g_1ALq-dHeCS2`k>D-9}{SS*tzB8bPy zgq$S-!Wu=W?KAh>$RN@vc|~jUBEobivV&qQ8|t!wE**5mag^(l0WsKQy%9&Hj5u|@ zmWxVN2Z6c4E7ol*_qg?K&)iw?&eGZA?<42LPPa8;?w(KbMqpJ#KM*X?}3NASQhp2W-*?F zoBV|eRKJn2?Ts*eG3!}+CWSn;tLEi-dH7AaK@Y*mUA)AN6!Lj{5BQP%z8BWvr8AOK5h5X0qJ^O*mPlfS45<1nQ9yYi*&{245aIH?G~O*SMt! z(o;PdnVTd_+DLWUh2+A@slFGbsN8Z%6iyF0*FM{X>fBaQi)_2iF{OCl1~o%oq&~=k zFkC9+0oh=Dk)A6-*6chbfK)nNYg}$3)$rW0?MlreWFZ8x*zdDQJupkgxG;pf*^0+( zcl7YG)iP|B%NS)R=srHoM7WELFBs?s{V-QfHv_>&1fw~z+@c${5V%_%V!P%>?P8E} z*)GPzkzv*y2sl7yAE}``HAxzf@&#}OUZsBpT9nmj;n}7hAJZ1gf;@+@jXd^3*J0J3 z!CaI47~g4gvS~aTKilI6Ia|$|jX{hn2tn>noynjaPq@MZsOL+w77=X{E-uq{2s$#0 zTssq1_{jOSqU0f#_Lnom$Kr(iXJ!~#APDF{*C4CW!X6=2deN99ZKF=VE%wb2YLzlA zEgToge$yU+C~t{(T54p!UZ*c4%GL&S!QyLHA=V>8Ea$LN>2w~90)U9BgBb}j>t9ky ze-;-dCj&W{<^@c=F!VGA7aZYsO7y^4SBDBWQ^jG2bS8W$GTmxe29_e@gsW*R+{m&a z3k7*o4hvi29HPJN^0d58Kk!t=fEl*8B05uzNL2^%|6t`t8{WFI{#)xmwf^q){5rdi z7{0!K%lg{dUvEFUEo?K}&)@p$*2lJ9w-wsDZS(KJ3x03jOl;l>-tYU>jdyOOKor1# ztiAZ~Gl%avEFEHpn+Kmgc;7+&;K75v{Xg3OvHkAa&+L=?&))l!y$|g@x<~Ipdn?Az z7=GUP9^-3`obg2-c=dVjsmTc_NzTkn;X3MhM6zhB2p^Z1l=u4#AsIkx!KE z)Y~GzEOwfy*8Vuy*9xNY zsYJVH^SNW)Y7_!T)k3E_Y`7hMI*cZoa4psJLHPB>TPyU+loRq#Q5=1d>J$sUuJyA_`}>C4deHX zKg=C~8?TH%%pEBi-#wNVE_I?&kO6>1nwgBLTeBq#Ofh0_UB)ZVqgG*Z9NS^2Ye&mJmIkeU3oXe2{>Wpv6R^^SCpnASHsCc5LbTXE3mP%c6#JC&pp8vbWjv>ZRjyPL0Skqp0WWB+7 zqU;QJtD)%d|6}jX;~dHEI>AikovO<8-Q7OgUEOI{+u4~Jp@`7o?shw*&=H4@P=pjm zcOy%okQBN?SLkZnMSCcZ$DhGA#!S_y24k2RKEOibVTW0m{VX3o%P_M8<}lZ=hUH#! z40ydP7~^4JUnsLGm7xr!R8`#!(E3A_mHB?hd+~n1_u{?p@AnH4%}Bv7){VNmSMg@! z!Hi$+p~bZ?9aD&%e#e{CBt(e?%7`CzM-sz;*Nyx0jW`};TlEluqy?gclH2KH3ZdVQ zkYWcH+gZ9==(xJ2Zbl`8n5H%`IOMMtqq-;uBf)-q{WA+weETs4=N?9MFOt`(j=#@@ zb5*HU@~6peCyRR|(v$Nx+Xn+h5Db0&%f}Saeu#CmD8`a5UF-Eq-M&U)U4=oAYKx6% zDxpV2E=y2YYV~C0h$ej+F7^mj$YklFoF>=`qZ}V)xSnTG zi7#OA0jDZ3Gia6@p=1+8A9s^XW%WNFQ-niqz13{Dd?hbakei;sz^#hiiYo=dJzc8A zX~a`Hs5MY6xb?+jijZ&Ea3MSwPXrjT9S)`&u&4$P3KboO3 zLEwG1B2iQ!+9|?)H0PT?CboDq|8}a7lM&1YGFxwosV%`qXL&$QkRmjun!XU%q zeyNuCqpVTZ9VctQM=vn}GR)FYoaiSqolrB*tGE#M3d11el}Ww<=a~aKo7J~!;{^?w zUQPekV|B4a7H9HCsh@GnMPH}cGP)rmD#uHJOF`?TOgjDcRy z4u;5L2=(me2 z{4s@IBL+pj;|`?Q%#hFX2c=NJ?+S=YJ3+VxS|)o?0*}J&%0P~vIHssISYLuh>twpe zAnsh=-x5TyNk>x|jIN=s3g^LToZ;x2;c;&i&d@$ z)`J6YnTi>*#&y&i)|2u3UBeYsX{D-?{vDz~p{^uHAfKO;y>=y*4Uhx?AQA zS&0LemZ|t+-Y_uggj%3+C0}hrnSzc*IoYG{-Pgc?YoLzmSc3()-rsnEYd7C`tib}? z^+nPVTpS^A?dBU!G=M4|f`~Pe*(#GBcqw;*p#5Z`5UYtK6U0KEgI>DHwgaAO>i&ig zT)X-DV+|Haad3Zw2d>?G-LVD>ge2VGaDr<$Uwf><0znG*H_+hP&DR`jut502{f$4k zc60Y+49G#dujr9#MDhw+g9GVn+IhAZgQH~Oz>{Swh}=@Kmak|i_c!9;+RdF~4HgJ7 zxWAzQ*KS@u)?k4EgZmpCaP8)$V+|GvIk>-J1J`akk2P2z7~%d#6I{EweXPL(F$(uL zen7gpwf4+2U~P0m8GsteOlHR$Z^-vB-}n#t2~5=Z4;N#_rJI|_yzN-*Vcs$C$vlU1 z@_z2-#u~x`y{B^|+yVC9LqtO5=K30IvcG-(Zp-Vm#ob3HRBo+xK_O3yD+H`rALo#E>`*g@sS2Q1R;G(VLFp%BuQB-NTl za-P1uV3B60`5kHZ5Nc4lF|OB zhkiVjW(PC97&83ajoyj#n7W-h!~R^b!o~2N%8jn2Gj?eXPNmsZVYyh^82`VrwdDN1 zjVHiAuYO(~fq$4I@X@!AA{)=J%Ev)@FS~8;#XIM7@xABZc3o!pzy$E>6dXIXAu%CL z=i-xc{_YXoN+%kU02jr0n(TLZBTIEc9b+FV`Vd%XgogzWNVD6UAGZs`K%50kYy!3>!L+xb^c}sUwjZgg zMeP_he-=$^^D<=WXpgD2-O!rKngBN!<~CtmH679_fmoSgeIbcrsXmE;1jk&|A5r5S zRZ$0xcnWEDX}lT*Xw)+xNZYt*iwHxj2U~$gF*3}yj9#ZaK%@O81#u~5$hS*@xW6!z z5_M%5z)ei@SrK-x2^JB8>_zvx-uQs%_Jl0eOh&{=fGxxMs5e?E7hqC=M5-G|l)C^{uGbN$o+TRiegq)~wP+RF&nA6vi^Ji&#p!zK0UGHv zl2S>^V{WDzis%iw-emkrH6#|9kSInyU5}D#xKTdW?F0z{pm53osLr_M79Agy@|cp6k6-3KnhlW9~l{fQYr=lk!Zhv>A7aR9)1 z4#EWXpKJEU03@YvU_v913~+Eh9r9E`_(%mC4AQi_kqgxdHKkn)#)=4-YoHJFdl`$_ zPe$K@r+OAxI*;w22g)3elF98%aDe%X+mQnh`1t}4aHnG386bQIz);^a{>@=tSKEhm zy>qDcwRXpxlEJg6p+W#0HVba+fAjHCqu6W_miGY&nH4}tiK;}ex zpADjU5*==-kV!iK@%XPWq>Qg8aYtso5=0oQd@EXS+g-&CS zrr-j1xi`6o*dv!2KycQgrnH)EOtx7&OpBdZUfZ-ANf&wmeB>Dhf(_rGR%3?JFcFQr z6+=%*-h2w&>Gs@Z-K_t6X6>7I1y{2!I!zT4w4cR>VZSF5BAM*hF*P-$i7mLja?Bdx0@S)6QL47uu_sz5*D}IFfI+FEU$#XrFAyXsO4p z%NpD_D(xc8VJO91&BhIAr;q5=# z{&(9S-i`sBz?ZguXzN2;)Yh9ee`oWh&EaNf^K~1)vGIKy-3|Z7&iWVEzk9v4{@(TN zwO?8LuC@Bwd)C%he_{1AtL0Vqs$=D6S4Jy^71zq9<2M}t*6}gN`yD}mEBMRhpIiRd z%P%am%kJgZEd3Es)bhDH(kAN3x!n+oKous@h)wLwOb74GJvJ(>kd8e!ngO-8vq}f- zMW;bb*~do3aVzAs)nxvS@haea|AV8#3IcddGss*Wsgnv}Zq(&%$=Rl6Rp|Mvqx_kc z80zwLf*padwtVjDNIBCIMP2T;s`af!^PJCK9p%onL{Jxomj(&Hm8X1lls(hZ1-e$N zR6=!YOX=;S_k$V_dsV%9Ja7{Yj)y~haFiJj$Mi@ES4ZD4DF#$5bGbMGVYePBI#X9i z&!1`OL0xdZ#>>Klyz!Idua2HO(-I6ZR7gZWZOV~FgzQSJLB+3FliKjPwy*o z$gL$?jh+GkazP&)QV(EXf$gd(9Lfe&FAEUENYELBM`@ELNVQ^q0PB*g1s~rGHbTOn znrQUUT%q3QTXlb_C;OS`AXJRDol$s{GHJwSB1c8>R1AjE8YV@9!)_;5kW+0TThB3M zMh^BGLWTFYx;-Zik9d;??sPc#piAVnCRVJf3QZ+EanVzD^#fcEi8V?9x>SZ(FPUeZ z6g=Wg8dT_(T>V~Os01*irLah++gptkeHFrSw_@wi?W-Q{7bI>en3c*Kt7WGXb8Ml%_m4#(T6k}9{v z24e(8oYUidm)1&ip+u(A%pJ6yVR)1q)=wik%B;amWXC4=ESnaJfqY(R!?HHMrT<4Ub|DH7`X27_w0=M2E3s7Vu~8X6TY zF^thijC>+c>a_iWr!O?h;CaO;L+r+7K_T*Vk#zdu5o6Nyl7X5VmIE5%mN2Bz7kUx7 zriTfVPNdSIG?wh^At(|iR03f>doUsnQ9?&Dj#&d7am1S8c#djQ6mKc6@5q=0cdOxLCU+TU5|)Y@L1etOd1fIQh{ACxJ`>| zQX7pGs=Ryp3#mv>FFe9c8t*_XVkJ70=MfD&d;&HC{a^*^t8U2+;u0Y;M0O55 za3I&mogR4fK9j~L*J)R#@5wboo&YMN49@bbPe~&QE#p=|A;XN3aTnZ(CgM&vJbK2Y zNydVL3x`~hZZL*c(-<0yxV;g6Sg2Q0Mp5sTGCdZ`Fmcw$Ix%?kUX!LPaE;QCC#txl zLlEsM=ZgrR)MUEQO^|7iOoRglpY`e$Lvf<;=xLKCgXIxTX{&rzPsAa&D^;W-y?|b< zXyDy~EDzgZq(G6GD5Faz%~K{#zg@3*X))Q>`)(x3^bTrauyOafeB~xMYmxd5O)q%@ za4S(xIbHDRNt0#)sG<{wNAEFd762DIA$a67X%>J3I*;Iy*Q8k>aj^3c9(l%c<_G;b z58#p8q*(x_=X?SlVJ6K2z&hu<;Sp-mEC7Xbz6%~9Cd~riHRt2-$TgNTKitfD6&{U| zz6&I}b-oiG0SGy;pZW9FI^O}0j!c>bU|`P2;L)K;vjA|*`6xU(FliQmS~;)4qbE$7 z1;A0xN8r)ENwYwbRp;9u9=&_z8DKv)5Gd!v50BnuHn6+UalY-r(c|XUb^09J`If7r z$s%f)Es)6h(ACj9CpM3m?SX*v!KS*swOFQt5^G#Prk4?6;J@Ih9 z@#^T&GcD~PHqO^RIJz<(mZ_69=Nqn$9y!Cr4%*^;-PO_C&$P6Ir8r-Eb@cF=mUfU5 z=WDKx-ezriE*!+Udv)~IGcE0)9L}Aqqqm%CX$Qk_UcNed^O=@*5DMp|tD}b|TiU`O zoX)EtYWB1uI%b>4(Ybwf^nkVHxqu1h*45FQ&a||HLpayASG!A3g2aETPp_`7{PfDF zSDsyY)bWRo&+Y!F-H-34cHg@5rJa{{jGd=;Rxbb4e8~(r9{2TA;^H&__pWJ%f zOG_(1X*mOU^}qjcMqtL=011#Ruk}nYM9LBX5z3BVm`ON!j$^*%2L+M}Cjy~3v-hGVI4$nmo?39SM}Iv$mTc#2Fqer6`&WZnQ%Q_>&e!(s4_BRM_~ z=Bw|n=IjiKAkUVik6bdIVv|%V99vnMNjSO6jR#dug3DDpEXN!(oe??-05b`rfp9t~ z1c*S?@z*mYT4$;h<4Yi4;?Ef{c)kznn>E?V=Q> z(&>mCpjgM(W)fP*R1^e`3rE-h?f8qCgjVZkaViN$H5!l{U!6&4wSIqCqLV~G0!H=c zvj|;QGbUmQkS8S?#zT%jn@MOjW15k8zh9=4tmFTiNoX}=5HKC%1c^x|9slo4LaQ|r zV5<7l1VJVoe>#)UI&%^UUZ$eyWR!J$WhS9@<}h)FrQ%_pPdWadnS|DvLy;h=m?T4y znB&Vc39U1SlzCE&;sh=^{$wVhb>{dZRD_{|5*~K^@k~PN%n48um!gxL7<2sBS@v%g z$OnKK2W2)yR%Xcq$?#4ut4#Y3CZ#|yoCNtw9Dg{I&^qCm0K)|%bTSoo{LxH8>x84^ zNRkBSvJC0?gPDZZ35WCk6b^EOhLeuppGjz)a8ZUMgQ+NRbB-^~B(zRAiSWlso(f2W zx9GQU@Dkk{Z!QP>oW2xr~%OS_F%_Ou=IGGlsejcnh3C9;_5?Uu5Slfh10QUzXj{kEep>@L1oL>x) z35nwzzdDo9I&s2bf|dg~#R!i7;#rm6VYf53II5~%)aseM+SL^GVQf{h-BDZUB!W+; zQy}}8@U|Ihx_#E^7E47%fs#Ue!12p739ZvDNZ?5!n34m2$FIyJv`)8(U*_2e9g1^~ zUz$m1oo;C&fCuTMK*SxtXx{&?F8|chr9a;K)wM5xA71_Z9UXy>u8h297TJ%#>ohU` zJyb|Jn;0K8GZ>$nMb&QwmIYx%K{1i#c-YMfiFBz(REuOwYqk_E1(KOxy%?$OfTkb-X6kBoOi( zJQ!$^*V>J0c2LjOR1=qO$J=Co*mLCd;Pt(x%1+ZZYHagRrM4ATV-o;C4k zXE3`q2NY*8G3JiZlywFrcpH6YJ2_2?^RUCWxy``50eP4$z6+@~h62-s_uC~U3etjw z$yhx(jPQKUXFWuMIJ6Fe5r|w9;}Tv zvSgr}_i`+m(#mm_&GKE48B0}EN;Mz4msBicrSDV=N?sqM?DGjB-NTGSIT^`3-L)JjmL8$QY`Ne zjNB1Oh?mILQK98-SIM&9j6g7_1bc0|*b=KL%SOYTo&@yx?gSpHkrL#Zs0t{&vgT~lhf4hF5Q5~hq=9g{9KqPm-Gf`8KiD+xAz{dioy8+vU@#0uziyN@r4S1&Az&TcG{>oxR$0fqdM?RUq z{1sYD$09-DhBm;2n#<>{u4#U{K6 zVm`NXZ6a8t`t>0n6TEmU!ep!JkfbRk#v6SAQ-j+$Mm9vHC19z>-B)jkhZ)je2^>LQ z8XGcXI}Fu)imyQ+TC?D)l5_(v3=q7&pD#AEQAo`ty_z5!{ZN2{`Yai9flQ8g17z_V z(jc7~4zcrsxPMEd`##&mE3>fNtxZh%{rhPX=RT=>vWep` zp>u8G2=@9(f4Cd=9c3#>QpA`@HM9=`H=wxG65JvH>BTiX=)wHxU9S`S?vQ#E#A!(J zG&PwD;bJQ8D!U6xKdX29ke`aw-TNMo6iE1VDaN-_2Dg9IH6*mB4&$Bj5d=2zsGh@u zWTNOPR&s0@KoHLfUVIc(y0Vm|@q(^{{Qv8}wDi8^H!L}S#Q8DjbIwEO()KTHfA@BO zo7#Ts)|a<_eCtzN`7QU>=H{;hME^H#CO7v0iod^3t-p2c%WFS=sl4{7wfvfUZFBXD ztKWa=y{jKuO{_k;@@FgmapmSpdF82dg}Dt!OUuHJyLSevszov zksQ>l*4D!#2RW;?^~A^#nbq2QD(MK%YHdA=bcANLwl1-b;H=hG=i?w|wVryGff=o# zsi)(g)p}~Y@mZ~>M(};JT2GD1GqYMxjmdjwwVoQ2r|nx$AA&lbn$>zrlP71jp3>w! zGg^D6W~_Hs>#6Z}&uTq2W6@cyr=E^$R_m!Tfo8Ry8k55rtv%MO6vq>@T2IaI{aLN2 zywAI5wVtwutFu~9JF3e$gI{=w(;<+ z)>F3emi?7y?JkSXPDq#Up&6XrQ+717^^_ewFpKw;9ldc@>$7S@kmL2UT2I;0YiG5d zvZLKut*4&*#66yJ*)N9bKjit+^s|MrWu^Espq~itM$}#Uz^o>>bbAX zYCZMbmuI!McG9tAKO!FMV83lfTkBBIvM}p#yxE?w+d3EzE-g78|A**lU+wc^BVca@ z9+!1eIrv$vr!0({)q2XplCxS*c~*8->nYEgnALj9v&Lt&wi>%5HqQUI>{;6W!rFI% ze_s8}Hv%6mj-E7+5eU$j@qjwZA&-d+4$nO4@?M$wd zS(i1FB4msdI%;ANkI|r<%dp*Zd4dsvlv9ctWy4XQQt4Ai5iaGyrZcwKh2Ma4zDW_F zVpBhu%Rw5=$`~lW=Ie->nvM!+IZz|qN5+1&3*$-vqkE=^b8aCfM$Y@P$+TQ^lyv|~ zY8h3K2m~C(pSkLIWsY_4C}7@eG>lwTQ^tpvw{I=(q}H;(_%}5__twH|X8`wbLhD6b z9^0F|eJ*(vs{5;TKPXKm@JPAmhueihwuSIbFVRe;Ih_NQfT9e2T)F&iZt{d=HEJ;X zG=*_cs8VuAB`?&2!;0)m3>d0hD(5R&I9RWG1;4);VS|Yp(;Y_o^(Nxm7Y13O<^z$? zpiG*NZgFVY7uHe-ag-f|(NvM%9B6k5du8KTn+!m9Nl0 zuP47c)9>3!+D`s`i_6BXiDYw&&SzrZvRiKceRcQs)8mjv=AWZZZCPgc1CkCCL4LyqzeAIg*!}Y3c5P4;_`S=3<_3%*XH$+rd2;uQX%0 zOu?;MB8>V=1_TSR-|dy!nvra#dx<6$!Lx*rq9ce7h1CKBp&&nJ)g5N}9@_MYIadIm z&c-=?q2)EZK6aPBDR*hNyx+f_8FZn${5!cbo4sv%d3J-Q%thSgE3K{gX{K2~64sMv_ zEW&8NMl)r%qNLjH-tiXtZg!VpG_sHE_d|5kt)>9rCP$@O-8ushjxuT1ABTLZE6Nq> zN5kAc8HGJ1kOxPmTym-uz@lZ0t2WyC#=h)LfFz?*+FPjz!~T3YG4RUc@qF-u=KKFU zmcDUm_gi=0x$~nt%;n#_+`7DW>E@+<=Z`tr?cdw(ZEu76f3eN4Z2qIoH*I|H##8G* zx1L-3^RR$zhrCX3`XnMZ+dSXhAtlBCR|!1H4T7Rc7JNS2n1 z6<}X_ZuMz|wack}PI8uulij}mrDs>4X6EWks527}-UI-ODpeEGX=ro>}~? zFuOrI9mad*q(B?%RY7S;o7q=(HC*Q z2N3?VFHsk9w>?w1SoiWvOXx}nBCFs=`h~Q1bJ#fVfQdWw%5LG z>B`a%FKw9F{2$oVq|J&xx&37&R@CLp9>+2s_e`xioov-cuFFVNPKfPSP zd~oS|FO>m;e{cDF98WktxAG$^=-O*nzj^lq3%&T?OXK>wS>hMuAICfpNcitTLGCY? zWTb*2o1HoMJL-m2w$y4`T3n&kPCyFvTYNv;i^aqBxU~T5rEhuBu?~(XOnG>!*`y;W zq8te^zK&8PvhD;`?+NbufR^ey-1Db$(tsD-(Y;#5tAs*aF<7fKXqHtXNq4zkBIU3^ zX4!}ubMZ#5??sB4s&Yj#3fXu8ueJ<6Umm0ay|_iomCv5)5a=YhQa2mUCOfTzR)xmc zV&Nbd4Ut7}$d}bvzTj?3I6Z9i_8_BE9SlW6L>S}GVcltC~K=ZjE2-EL*I zfzN03*(=#jJ;{Rv^_5OGohn7sq_qsew&PR>LiP!IJQnd(p=P?>(KyNAZy{IbfwZ0 z`;{;os$^BQ9s&u9T9$EGeb1>5VW|~xQC$UVs0S`cN^}ZZIqeUo%U!u%*OM7C7p(^n zUP1-rY(&CWUD|U*F_Fkwa)~a2#JZ!|`L6@tR zE5f8JGDueRRypHmLTJftBzr-b@IY6Bnwson10k8B0%W|BOjr-jR(2EX2y$$O6Y8Dp zr6(~@<6m7d5`B@1P=gTL>o!Gxpk0neZt3LDC+jqVsyc34|_gP3w{!YcKo<6_N%ut>+H$= zIdx=-2t|EjG#j&4qP`=}a%;zUl(p2w!aHckS^>#e{C@q!k1ww3H z&nt~~J6@nIN?iKHRF7;$+?gIQig^=qx20N`UAb7?$E~v+wPUvDo5Weq+ntYDyMy>p z%neFK8k|MhI}Zr)q8LWUh=_H|Ge>tmIwj7I*{L`Wm=W20yj-*>G>5o)u_jXX{~r%kxM?LmFr7;9op+c zx>czQ)ecI0dq}pUrCy`yF^2h+k(1(b1&1<_9P77IS0ZwA2yS+pY9$fx^xK`Zwe-`< ze|R6*@Qk}K85wfi2Y52x;rO{sLdwU#aenHw6W(wpPxLfM=b9oyWP=LiE%HTtI819O znF}1yEFR4FQnHHfm1-);@T(`eR-)2Mp~D>GFZ*&tXjrcEXpJnn6u1{sh@N(JDr%)cn_YQT+^NSx?t2WnM;WIhd4ReXs`B03-RZ=X#(VIT%2%d-o zCT9FZ%1vr|htthBH#U#RY_77WrD~pTIpJ{&s9vTprVHnEA_enC*$C%E12b~mL8BBX z!)5&nFMyQJsbV1{XA49=*JP}2czFkG)Be`Cd1L&)S@_Ac+CU>rAx8E0d)097RCbMZ1&**y1x`7Z}j* zu5rY67I81YI1Icj+5zLZnz7#;!i+no58!-YnDc>bYJA6&YVk(1-!Hk@T6W(}42v{~ zya7z^1{hZ}xOiBA2_7FtJmI{nS);so#hcT>nNX~|-&5NVh$zB~-MSDCX?%0PZFKsf zvOmD)l18l+gG8lRce%KfEWXDs zk0OlD_C0o&GZqV4EH-M&_xwGCD$j~dnv5oCF~uVRR+3~XUrfU;SCQ07xkx5Bl$V2% z4yv$iuuspzRQYx^$>P4}l>scjGQP*oBJRcapkAMCqPhr#k!4p8ys&opnhPmMnS#ID z$C+xl-Z-kM`#!@T83cV)p1%7Wu134hXjG?}YNvU&AcT2J7;5EaJueDHP0I(pg|ejk=!61!C;8EG}Epz<_Y>q0_&&`aP#I7b~pdbdkNjgV)~9Ij^9f$u4fo-%!p zHM-Q3;tGMO!@AeAgE_2wsS9^OpEWi z6Wlo+UHZ!GckULPcbm_SUa4)U~!F)cuZ4QLBD_JTRfTXyXAS?T+L8I49A%sdG zxr)-`48Ec(GI10o?tYsSYv_eyLuoZy@sOcPM1T=INrW#O`*F&J3>2w4>}J(&2&;H~ zieHN~%7vl^?*|Ee7!K7WaR1##Dv=oMp^{KP5KO}u6PP!;6mL4Z+>Poyz!Nd zf4<>g|F7$xUVnP+OKYE4^RE8(>c>`*m0w@EzH;dJFOF|?yleRvmS0%jTlyIw;LL}= z^_Eo!t6u)_l|(T{luISjkBf9`V1((x#POhcE4WB;Y5vxm=VrS=EouJNLu0lt&Y|r> zY#*GP?E=-Z`CAW+*?!~w*7i+fw%6uho0-2BHh=4l^Ru15lsA9t4P&++y5HKqer~o4 z6cOidy>86*gZEq8*N)kKU=Fqmlp^PEy=Khzg*n(RP_3N5wL4}zoP%w7{(|QGt(`I3 z!5nNCsCmxcx;$pvpM&iJWzqRtm&R;+bFf{Yayoy@IcD3v(yfQYdL_UW_*|Qd(XEd4 zGH#LL>in(kF=WlI|*>>h&yFiI{{?_K0ZF>&33siCEZ*7d(w&q~FK%sa3 z*7}%ja}Kr()Pm=4t&Q0>=3u)(xp@B8>X@y5rJOVfwOxry{b<^vIy#ylqB5Mv%sjymIB%*A8s{`$#LdrX~~ zM}sM9YSaSM?B#EmgZfUKtX=RUHENETJ--MuyAVKX)EF~+ZV_g7fpFAFA2WM)5oUI= zXVj=ZW+pGd%r;Do8r8JwjZsUtEuNQ@3P|92gTh`MjM^(TKxh!6M0lJ4xMZJyd z|1G_C$@zU7Ppm!&egO5O{N~x?WEnZ1-KbnV7?!g~GXcbT^`qd~W#nu}(sJ*iW?gig z;07L+EUHAT+Dv&{x~dZ10p_X|`{iMx-Ow0DRO`~kIpKUL?4q&+Z-p=3vrNmW-Ek#1 z^Av0HhBEIj;GEnlXd#uO`LaY=ab+gK#r31C*(6P>`&B;*F`2zw^`p*H{M@W_wv*G; z?k?*``9LZm^Gf#IyddD>o^eMD;FdF=;P94?Gtr>jb+=#rYlus5U)}w{l^h+6?=1`whNE4H_B|8)V)6Upn?I^|6};LVgcT@E%#}uI-1Tv-9NcsRll7x)t?>T2fpf~popZKs z{>oxR=hu&Vg^Xxyw|7xLYHE&~)8%$1#(SwBW&5P=!6pDtmn|fqrbAjK5GymRFC_CK|XIF$|m$^C7ak!w8SErRZ2|6E2|zqK9^05yAH19 zhNJgwvh0g1pJ-3uk?hvxi`RC`+GsxTPQRYf^5*S{8y>sYa~I2Vs;o_3H@ylHu+21k zjx(7Yxf!?FlOTaW+3W3;xwbBWh_PZx@HfPPPZ3BYDu7Gla>q*ziU}r^%Li>h0yVYW z1{Il1Yn)v3JFg5fAgAXC<`j#l)vEqTPj6N8rY7KB=hUfo)PCm-LdmSEI*m{|cSW|{ zs@{wD@j^b+K+au}Jpx_`c_39wxF&1*!K-JjE&Jl7D=WGK7nh;$$bU!!d+bXq1N*my`IUbAl|W%(F^9hsOliaW=U^%-ZOqxHJ{qKZpbWbH?afFWbjx# zmdP9-Q_)%`i*uh`$Uw1bH7*sCWuYWO`+6*&Mj}lqT~)a1ex8kEg}kenh3hA9j`Q|& z6G~<~YNn!&C-?gVHN;rSc#3t<*!Cue{FW}x&YeM`^hFL3o_Jx_&Y zy;K}R@w||x8X+GUYC~j1a`|KFs21$huPl9ZY4@YMui3e|^T_4zxeQ(UkxTD${vXc7_OEWgf9nsn8k=7OmH#$3 zKDqJW`e)Z4U;7-m>HqQ7$jZ;Jh@iG#Y56P5{pBUV;+{Unb>#B%FRra_E^V58Hf^el z`z8;w#_#}FZVy8D+@e3%Yz~J_H#jtIHnd7FDwWjkp`HxLF+GZvIEw3qI{}q0!;LJZ zAaR0Hpa1kG1{={`esr3$;1#zz~s&L?zU8 z2xTFVL9!JTR5}z#F_8GM35CNz?seljG?(1TfZmDz#+4A<(^?=xO2~I~B77tq*ion22p(1sQ5W9Fi9pBi?gr6eHLJ5OzwR@dVIO#{0CIKX`oTPMy_1n-6LP&} zm=Fggu^g$xJtl?6eMr}*3W%=9n+C;5$!IfJYy?ZX*sKV7G~%oFy)_`WZd`w2F1eGz zUlVfky%-#Bv%KKThC@oTT!C6;_%KeyawVu#s`P8_Cg+ik`f!sdD4LL{K}5rpTQjcj z&n0&(E_1s-=rrVBp27jHhy%3xqfx z)%rXQ_#yQ*z@`O(vN`9`GG$I-}L8_vMJt`gZAMvt6VK^U- z51L1jh%`*+QLbJ_Tc#Ze#`UXn$+huU)FBke7w~wA5`(pnKM5=O!XRD}lX+L5*Tj!P zWu#U=^!pPPRtxw2xwMY=%;y_5uD^3Gxi(|{C^$iR+8(%$ z#Vh$i1(WrHHzyrI?I0ILvh^zCZt~G#79vdl>@luCHjmt?nvy5$5#-?$P(D?!(Uqu- zqC*ufFv1~OkH>TuQXqWo2;`DOIj<|+$hsM+ln%66b3J;(xc=x|a&7#Tn=xwj64Xql z5?S1JgllrUT{KiBm<_pFNK}oud{9j-7mb$&$x<4)N}=xySF3ESDWr#op)Ls!>rybE1#%xXu0JxDTpNEy$mo$z zM+Cv!=tfwU@t`d?Y4rN#3fqOS!C)9|;T|m_5Nr}nd5vx-jc}%ae#E%`_PONR_^Tw< ztZ{0yIP|75CJncbI!&Q(BwNv%k3$KyRjPQIF5h8YYNMz7yDH|>a^^TcWL$rEF1a@T z%3bFX${&r@n_M_fmrH5%5NWF=-A#cv@gl<`>Yz@zNnGHHu6!Vn3I`LBLm>C{#`U+& zCD+DZNhQiliLP9=iKrMEHmX^d=84C`)tuX<(dvL2R8u%yEAlC~0#*x1Jse`@x)0>O zbuPI!{;E`z+f6B7jSb}#ED4O!)>6$lj~9{^&yn7c4^x_})q=XNMyQA)oUc+Tq|I^O zHLky9F1a@T%AIU`we(?9KJ*o1hiV}_P+QGIr9NQLUYi6dq0%Kh+6xqmA}>+_S6(r) zh`Am)jq7ioORkN-N<@$fCwh1^;gb}_RcHwUDaDa;q7z3;N>oYfk!&E3Hfe#QJ)UBT z#bPZNkh^MJe`p@LQ?)x!{8gh8lnd^n*lPOJW~~sbRE^_%tPIw2Ms+ zWTzSwf&sbrQJI3`l%_Y~yUu6Tm-0-m0qCyR<*^(u(kv5|7Xq*m% zfvxll6$Ix+w&?~hs)7dhL%^e@f&G5<*Np3LoJ+2azv|I?LK>7AuGLBus(CmO35PJ% z0}rrPxzuGCl&j_gYN=1aowVOqLQyWC4*Iz1KO7Dqy-=^E z3q=305%(g=-Y`K6F<&#Db4fZP9`&JUB|HepL9p(B_4CH{*Uu%_#$RD6A&#J-a4oBK z{GptPgaxdu_J{@nUP#1Nx~yWwvR6_)EDH{qF=61xIdfk92lM{_p`~j}yM>+qxbyhs z4_{ur^a1BrosI3E-G2AhH*Wsw=HnZmT>tVqwf5Q7uYq^|&#gS*FqXftd=-$~kI##H zqxSC7vU@9%iJg5B8#Q@9|Q^(|Qn%~@S&8_Jpqo%3p)l*F!dkX+WPc%I~1HD*B zhpAosznk9)ac&(S9yNBCwoY{1GN1b10$|hI)u37`IPbk!MN>D1AAkSe+#0@dq?@Do zuG^y++}zk(0JwT*6vy{G7egU8O!Wd4^E>pOiKFXDcRoHVeDMvV`kBWY++5#V0Q!1I zv06@TU98u-sS*Au^V|Q-Oue2M)l9vfxjkUdZ?5evfSo?mGX{bp?6SKnr8diBPHeNV1`$A0IDQPI@v zjD0VFh(6Kl9Q(dd$FY51`GBe8V^zCxwC=}%eHTo9&)D|@Q0o(YC+zz|J;(O_w%;)I ze0ynbJ>NM}O+C-p_X61M6Ftw^_l25{=j20QH#L3BC+61lkx~B4%rxzL0XX=AGjnX; z55!F!ANaR(>-g|UvD)_n(DD-%Pwe}`VKjAnea`%T!-I2c=osa|C_Xc>v2gC$0nP6W z+3_yoLNuGx)Mew>%PKY`8v z*5=w;aMP{^)QJvfrul`#F&>RCn%~>qS$e!M%7DQ@?(CRNmy1n#z&pb*-Z@<;eC%1* zho@uwGRHuk5Er%cUOB)fTQeb0RH&7q2el}LswkV%vnAcTXt?Pm#>9e6!d5Tz=_bJhP|&8_xYae8RFlB9a})tksr#Hv z&()e2=R;f`6NPs!0x}(QO9h1Cj)MK7B=T9t)n$WPK7=v7ppUI} z^5s|o!En@5z56P&HL|CsyooLnE4d{qH{>B1G6+y3+{6tUbrakM(ixf{Hh^h93qTxq zeVovWuqL#!oxu0c_rNoK&#WMcNvmBfsO%MR3};2y?QX!Nob1J79`pj=^Rmgb_@2{^ zqiJG)3qI$L0wyZ6VdSdXu@&A%51Roo)2R4%X99?9YJLV>duQKgw`)FiCuh;v^SBxp z;$Pc4vPQK7ehuAr3C9di-t+3&nhO4i znSVX!)%2L%>EF)X4grCuEfjO3rGZ$EOtz|JO<9w?j{|qdzch6`#`Rm2Ja%NKijV1Y z+4^l(6-}w!{u!h+&MIc_#*MUkSL>MF84tj1cWH)y-?h7(vFP0O&J1?@Y~y?d*^BOX zz3~Cj?Fm_`nT&{$09%IhQE#+TF2JM!iBva`%E@;hRfOdBBWyyB2w4|Qw;E`SKFZQL zYLI|*#bsz%Goqq`4~`XDE~4dS+Wwk{$ws|Mx(K&Py;EvOT%CP)Ac3ZnP0ZgXuug{} zdUNf}%&s5f{~uk7FYSJI_dPrR)6O%OfBCX<>9;RsoPX&oZ~yi7r?(&7`s~)jn?JP4 zZ2Z*5lj~ntudMz0T4QZ}_0y}bTlwV5yB)V2N6SCC%q@KZFj-ouvdes^T~@d zSe-fFm^dPv#6*veEB6-2PIm3a(TN7{xt|&oYG^piXG1d5gg8y5Jmr4OojKr3B3nZ9 z$&y+PN8+hMTjeVHjYFWpGnZATZ*C2yjd^c@)M>>V2SDNxkm#LT;+>sMdvAekYTAt_ zfFuM+^2{x1(s}PKkYepc=Z$^yQ9iFa-E#{n=*jE!F)Dh2)NIO)cbnRPu48j+ zb9|Gqw?L-0Yd79yN&yChUW{z8GoVTE*|7mFkbUjijmJ+kK<=joWfULad8R~!&<=+2 z6+F{o*m8`Cg{iPAWwUra0oM}n5W{-?z+!7}Y zXaT^IcH*{1&;p<%U_g83qXY(YG`FzX1~hpqavW9=%MSGl*4LZ9kj|i> zu1`$E=@zKyQ^*i_pU!agI%L$EzACHSc+AuWbp3E{ZB7hm0dP?9#-k^X^5Ehd`V#}1 zc$5o3K)~=`IT_yhMa>@G$&0%2@GgL#xOU?aQybtp7S`r8RCdxq9(#@jutmy^x1T)V z`87Cszzcvdv>OitDW(O@FXfB{Er6c6cH?a)WBSD0lj-CM-={IXcH^xl8tmUs4dO(U zI?z)2Qj>^ATCwaw+U50{F4}W|R+vn7=tec7rHecRyzyIr2BtTD_uLwsX{6XpXFh(Lykd z9yC)G#gmo_P)w+(QFoyzfdrSNe&Y>51Ji&Wn_Gi(3}^wcRq@8_fke}Q9-Uj_gaIu8 zlhSUy4oET$=*rxZ&KS@FP$ytOuQeYfFrY`~7B<^}tf;X?@Jz~$*O=OXuD^Y5ZB7hm z0Z7vGH+IL5l65|O@nPlkcILvf=56rOj%_@&wEN4u-?w{hm*0KY&R_5R;$~>`EgN6i z_{ojYhQ2{;JhbyYJ0ILh?p(e6SC_wV`MWQ_a5-`L9hbg->E|x}t4o7Ru}hCRzvlcI z=XW`KPR4m<`>WeOz5Us3W1HH3`_`Xr{pYREY_+!{TW{O^)6M_1`7bt`e{V0?U+vhd zBk<}7ygCA}j=u_9A!@*!DB&I||CdgVvZB{MWcXz&P=hO2x{mz|loww;{ zcRn?5)9={%mU)|gX6KXhHvRUUPt4o&U+jE*-ln&9zWJo7kj(cwgJz^_^yN^u6yq%8 zy7kajYTl*~Zt?RrePD|_X`1Ln4*lf7XkfngG6US4MdvXRL;$(w9sCS=Gm zNyyG*k{2HE5+DhTWZ%MWU*BuHa%n0rb@``gpY#3BcfRkOPx^G0f3^FrMM(?0?_8Ah zYj&SFC5_t+l&Cc#rryDve)l9lwOZ@cI=v`qWIetpX?Q)hC@HxfU6hnqr%p*@jYM5d zh{dQxSE#7d$bM>Cs~=na<3&mTaP^NCCH;fdk1b02`>TJrDCz%L{ewkGe{c2o7bX4O z)&H?5>F=!m-lC+xz52V0l74jccTP#8$qZLfl{VWE6AGm@rdMmjv*BHo^q~!KQPSfL zU{TTsH%=BMedWfDMM+<=;aQaQfsKb2C4KqE@uH+h8xJl@dVS-SBT~7Yua%=_4cEGQ zqk-o*ZOXRxKDhT6i;{j|@6Q({egEE{ElT<$dw;qp=@0My$)cn`w0G(<=4;>w_x@;D z|9^F3W$$-){(94P{_!vH?=9LB@$Hh+ZdF>nzQldh{9B)?0?%%+;H*Nb-Wkmg?!Ugo zC6OBvwanGk{WHS4mLFh=7_GUfbnXo1M~F{f*STDi5#sY>w9brhFGlMQW3=ux7Cr>Q z=T?!|;%vNvB5rxLvws&@T`Q4-2@9O~&vv3%0fYv`SKR`3TggUKZkA1LJ# z8MBRbs_j|=t;hVCo)zmh?1-Pw$K!le6*##U(lAVqSxzs7ct`G)brROIjdGhQw`&n! zUI6mL-%6*J3 z>ZgM{pC-pK0=E^_a;)Kg_fv=~h^*R{F7{62GVebpZN=x3eyO^kobgu<)}2~j&>uLr zf_grlYllpyhdPsEdMpYX8C;5>=|n98h!^jw(UY{P_Cz&D6oZwhs`ia$ zH;_1~q%4$5)bvWflxcFunP^(YPNW!sEEenNPTjG*uUDUlx|VmVu)56gdU=*NqjAFw zTbf}VqYp=4ChHjHT}-q$8SeO@M5bFy{{Hi#?apjocjw#=1zxbd$(?TYfZu;^9)#J9 zO677Z(Ud7d$_Q!HU)Fu7>GfxL*senesKQ(~C$u^}?v6Km%Zy3MsuGbaU^NfvP)xRZ zp>Pf9N8(yDP7p=8n8m}at~eE;!ID3a^J)Dy9;~MrHW~*`ip?6x3jk*rT$RQ_Ho}Lh zz6DdR+^IX8{Vn%DL2W;hlHS(YJ-5KX{7;HAn+7zU%1cJUY{;dCTB|r)0P=a=W&meg z`phmD3#ALrTKD0%|Dj_pSxv=(1<&zcdKM{vK8IAQt?5CvioL9JEcqE)(Eq}p`)TNs0I8I|C>Gj;? zI!Cg}tgxk0f~FuYh{vCVSSpNu1aY?b!j#x}IOnzU*<%i?w)LQD)*zrJ;udlof$*L| z>+rFd4uK680dMztn|w{U-T$QK1kgMC_oYqqrFN+_H1*N8o1Mk!3xIB}z}XfL=M~Oy zg&w8xo_k!5w$ySR&KlqJ5|3_EeeWi5ey3-fneY^VZ zLpz_?dH?pOcI=(N_788jx8beNZvE)i>$gIi|G4>~&9B>hWOIGvmp0zIk=S_l`hQ#h z_H}Ok1#7>*_U^UP+ACN8diDEPtySO3r&fMssY{2~+@g0M-Q3vS+1Xv2ee?u?=`yFQ z#@qh#n^!hp{?#k%>4$H{NBq01=kiaND;@K1UVS8VE4KUS#>S5Ox2OQ(Z+~P6AG#Ht z%R3EzoA=>c)EvS6p~=v1_gQQ#_|~&(p<9tT;Qr8L1gJZ}ZSc^o@Lb+$@G&nrmv=JA z>#4ZJT;54X{Zw9jF7I^K`B-LbF7I>^-`l(o-$Lgu)cHGf`?;?R-9qMqPY*B#f9+iG z=^Zw?ON`L1N9KZ04{#eibnD@Hyk}Y-jCo%(m-qY~_EmFv&)?avp38gQ?tJ*xtL7;? zIK4Z^%HDqAM(9>(F8K5Sw-xsAt>9en>1Xgs@N(!@U@rLd0Jp&(x#b^$ub$^c{lRJ5 z8iTKGJ^!K5EqE^a^ax{g-(2)*>zqK}yo!ZxL37ckN4Smt@Gb9L#ZQ~(7=7#cuMXV; z=YmfUFa`(af=`?07~Glp!?!&11_(^s*?54hX9bQxtz^5K# z!=-y@9{8C)1-HRNw~puXPEYEa2M>Q>+R2O+xBh|9tyj*)p5`B8zhW-- zw3B)&_5*XVr}=MVKYZ)ua~EvdM~$(E3wAUYe0qS};IFxLeeT4kebg9y{TE(y>t%EK zr@hPszq4rf&y{@||F-O*Ti53DPGjHZea)?x&Xs%GKRs3Mm(0bU<{x9fcrNy|dz!$0 zf9TeW=3-Ct-^PCA)(c12=btNqY0omoUfX=uM?$y0axVJx2xIiGn1_C*rNA`$3+ADp z8R0hiYi~W@MPJ=HfA0Qi=QM%7aplF2hi=_B7k+w(+wc$HdfsUA=e`*E{nP$w48Q%{ zYUtK;=YmfUFb03lT<~cJH3r{$)~iCdo;??QdVn$bv*v@{8~n;9yt2QduI&8!+K0E_xuvYWYh&%o>WaEw+4CL#+QAb`y_sJ` z{AO_DrnE*oIiZ5L+ZMbeAo8IW6(0WE7Q>;wjGMj$j&!gp(xEeql@DiIa#%M+KHH>|G&02S4lyEWSPFBMB#SD7m(eANcCmUi zKg4pzhO2qjm4Q?luWTiG7)}^YHGpu_$^awUe9fWt!ipTp_8i)9!Pjs)aP@;u17N9V z_1b!_J-z6x%RDhoyvSn|B_S|ZgyGH!t(1GuuEP~FxzfrJ3-sb;Xa1*1HO#Y@B$AK5Gjl0y6LgOUfxmJJMKc2UDhrO zr?W+@>6bXNp2uZF;$gmQa5Gefhhv1Ngvs$P9nnE9?lckVBWf^Zz@`9EW;$x5s?h-G zuSd*^x%RfZPi2YAQdva7IMwtqS&}TOw2|&d8J?47s0=A&yoG*-Won&vq0wY|u%`#I zeVQs(bT(C1Iz5?fc2XTMlSr9s|9Zu#7JQMfyZ74}4cvXZjHzCdFC%oc=*_AL4vvZu z6chTSEd%z9U4~XzEf2R#bu)}*On=?>3WnNmHCsd#^-Jk;FRohwRD^gg(=pe+`7Tsh z=CV{K)3oaIWyla%il8dj2g^AqHA`j1Xo|=e%i+A9&*xch+7fi9K{g&UnT8%@tZGy7 zhAAwK2fB{?fBW61GWxPkOrm4tF&ZdFQr%LRL{ix-8PzK@REEcEpwm~>o5BluO$4Q) zWOQwoil`)s4vItXUA~MKvRyDzgIEEvATyEwrOHy2x3F&+)h?+$` zR&$DFE3u^2_2pUtJnm_mu|%~I$Q0D7^CbtI1uLhk6fR8vijn*M^czn3+R0I6sgSYR0{@~I5U)_IVU)lHXZ|wcIy?5_fdynkx zU-`W&|M5!iO61COcmH_z2X?=9m)d>a&Y$f3;Lc+^(VhFY|8)C@w!dzh-hScM|J?fE z*4J;Px2|n|V)Lgr-?sVa=0h8Qzwyf(-?gD^1U9zTe{21{>-PF<9Wn0x-P6FAPXoXp zwVPU9b;3GU*RG}bcrs3Bq%_gd{Ku#qPZLO_cJpu;FF-x9D7%QTy{a(Dmso6GS z%wT!RjX}a;al%)e&q&uYn%>IxyQLCVq?<*O=hj*`bsqp<6A-wGMxVVg1G+I%dc-R26GWyi$=mwkFo9nQ4Z@&)*nOV+JNF zYXWU+sSagPyv}RW*}9vDpS>}Nj2Wuo6cf&slvbsgX;-;$j5>qx=^KOam?6u^b){+I zT8}nUGM#DCXBhtG#(*3%wDe-C-<6ZOeyh!x^?HRn!|<0k2E>@b%Cu#?sa4FjP05Jd z#ilpM=HZ{+7~o@uc9)=eA;Zc2cD$F(GyUipgpb`AU}FXh35)4oy4^4pJdN57{>;`m z{Jk3kbj+YISf^)E&dqBc&l(s}md_yk_ZtJmWf(?VT`T3QijZZjj%-%4cr1cVC)I5q z{w6SZt;29?L^djQdP&o1cwb|~*-Vv5Pa$lttsVaIjlm;hq4-L!pUA~CrMeg{t2n8| zr&u-*e;yb-JjRHVsA6`bmVwqu=XSLpPEIkn7(WFJUNdIl(pEelZ=vO^RE}gbXn5M{ zT$T^q7<|=OEJ2L7Qhg&DW9bY_67_0g3T5-~Nnr5mF@}^N*-o6*x_JVN((P(BKgHnq zkHh!f7`$qTvFkd6xzz@?4&MU|LSu}4!b(@$oTBkU6eXki(zH-6#uLCGIA%eUOfOb9 zI+7LcQpK=SsBwl^-vSH*W0riPqZW%+gTqn{Hk!g3-5D(32n_sVmU^?KBN3g|8g+s+ zbgVwDU{|o4zyKbzFj7=7oC~+AEykjWJ}TK$DDIkkD=_elS&Ax3N%dN|q(|EEnx!RL zQ!Fk@8yG-imL5~aM3hdJ`mIJsi>8s>3>Fg@c*iVgEFZN^l2O!n1{WAfM`y4ozyKVx zh-$*_bQCjRuPRctU+ha$Ebd$^U;vC+D51+~Ju;H0w3WCeYYAkE#hptY7@Uk*M5mm8 zteKXi6MxUSdSuP6;3~r2BG7=VVQ7G4FlJ$y$SH#{7lsGW(j9GfTLfLVrQB5{1 z)rfclpI%CLE}0vHha8sEL#>0^cr}JLTU@bWcvKGOjnoZjx zcjn+ceA$h`%f}3Ottpq0WZCI{E+@=1El*b$**tvFjlt2FArpx>Q%$hEil#D=G*UZz zBs}kAaQ$=^jz@`=nwUU#8=0=yVG`k5qCBP8t+m5voD5zz=7_4C*ultD&6Fj}wiD&p zS&og9!Tn_W*2T8 zeBxyA(lJMpqq>NwVzpvj%ofE?_^hQL{LhoYOU4|DsH(H|TB;qdm^rqd7N;Ldw$={* z;KtxZ4#(-PO=f$UcC1*hRoPrQ%p>%)RGSCC?L3>_Sp50)Wbop#Ky0HUH}RfRpPfUq z`9!O9_IdK)qc0u2a2FbGL1*zmXhQYFNb$|}gI|5=;43FcCPO%eBpYi7KYud#iZO0Y zDRPuV6_r*_OV`UKT$~uyjjeJN@u6#j7n~02Jo$o?u^FehYX={E>EQVjz>^Uaw}GF0 z$>6?S@V5A&37p#$-*+;2-dHrYmQI)|j*1q;G-)GNn|==5T0i)nlfiSx9Idd$7EM8` zbhAvN&Xl+_r`P(ycb*KMGv>fq61B8?xznR6u7(cZbMSiy`2J&i zf4=vUD{tBT+1&?sr0rkbzPk0VH-Bq0xADI=9$x>A_50V}zqYlSa~QrJrp&k)sikWE^2F%JlbU4-A-dWB0 z{5~+$rxIsg7SGY#Q|#^n&7GlJOvu@q^M~H_?8mR{mTqr4BHCckbk@%6&wc*V)JoYwc5->%JlxEksddwh3%qH`XJ=<#f>>TiH> z&@VGpe>PVx*>WYEk@JB>!gN0JRQsIXN6ypkbo}a~bc>05j&_5Nc6Y|wRjoLmG=-3# z2{lQ=_5hCfeWA@pa)N}Ufp(#vmi(=Hv_?sHtlhZ}qNm#39&5Lg#tc80$5hBKz>wAD zJCVF$hH)yE=@t|B9PRo>_5Yr?vj6zb zeH-7s`n)gIYyCg3alc{niKbg9=o{}_y`HNli%}Zk7!dRsus{?2c9SXRIhz-2#W+>S z6CI1!GH9dSng4(>RS_`gmQtEgO~pU6BA}0NE8cFW%WD`6btJ`hp6nv&V7_2Q=oDwi zV+}o!H?m1OGpx{a-B-|JzO1iMFk|6_%_*gPBWJ~1L=IuiMAxe}vMiY_X!WAUc1l$^ z1ppU+G@D&P5NtMC(9_0cwGLPGk`#+31BqOq>g{4|5n?RG)8MN>DoDwx;Q&+hV$L=_ zamAYK`aJh}(`A@nM{x4Eavhn=HCZ5Jh9FLvqdV_3KdYF{ZQS!Kj`*Pv91J<4&FmYW zHzlHMWSV#`3ah~;9Y@SOSqd5vz!t$|_c+L;9cm)KpGXNWUv<=yp$xQu{K$(`xKP44+tXteaw&@X%$ z4DgErxaGy}yzTm(+Fj5W2%Kkky=Jhhu{`8uqDYQ0;@+_L#Eh67umWYUegka8TUeyc zIbRO%yhf$BblfS6nFt>5NqAK21jQJlW(_MJC6YD6xrsVl!Qy-OZ?! zcmLCWpe=hMP912Iovyx2N7?+6xMvQ-dnIuluX87D%kR|P1mM8AwF(gerWwA}V6#R$ zox!v5BnKFY?y(N0jZnmQ5^q)V5{L<5_RgD-HwCAhUR)(oM8s)xq5*ilUh-lYQVe

    _kB$BhLjomL+ zN`y-mUB1mDLPddZutSG+{f&a zxKGnF@IR#_ZqRjhU#gP0XZ)3eb*Gl+4+enq4tieoQ%n`@H54)6PuiU}9HjLsnj-{A z02EX;(UvcgDR#*?eaBToRRZz~$2J{>x+-!E+Q(UyOtKBijwxKD&zAE}np~t;Lnur$ za9=n_88*_(XX4FcC*PsrlPxY=GkuW?j-%m7L{S@Fv5YNJjM@*mQ+F)S?f-XWZF}YL ziG#r2f4!pZZf<|~R%qil*A3@CzWc6tYhNwj6=MAVT8KBxTaVt9UUE)7L}#x|eaXFx z>Nwo(MF~J};0ECQ4_tfWJS|UNMY{I`ca@eeE5-0jlP*2h@*C!9dHQ<#uF~>lrPvW) zk__vymLHp^<>_ncyGqNKRbb*vQ8f0vRVxbjTSh5WvtTS( zuz4EG8?92PX2LWJcpAw-tQrS3up3c*&E%w(d-JqB8N_qfY5B7JGLZ$79Fxju7RY9r zNj6N9X$_cF#sC2@}BX%=R64}v5R)K^H0QJ)v7DMUABrJFQfk$SQ=spamQ#jV%f z6koD>&d1!R?QwZjifoy(XHdfVpyQNZn^AHtUlsXOMULp5VOg^3bMu!`Lh)(s)Ko)p9?U-TEDIUD&*h1a|20Ry_U5++zT%vaF_ z7tSZS8r{s0J|~%VR@E0o?Ld-~ee5YKImBuh8NR?af@nx~>T#%QQA9OEmsqhQslwz& z?hL>Guf1bs|1CRT*nETYk9&XjH1OrrfYYNX>0Sl9J({dOj?yyVYZnlfk<6eOuLu0T zOrMQf0VHfDMFDPSgD4PyJM&|Vra~{=RvxoLFCggbRq%|<-5Fm^XZL8Do42!A$innX z>vYJ%o%U!tGwx%KUY?d7O-`18F9@A?`wEw2Di2pP^sFq1Ee4 zgtpC(`I_0c$!T&_Fl&WkNfGsO!7RI9*L718b;Y!suNnr%4R_;=h$P5+)-3K{rdz5z z@G?!;hV)I;gAxG&)n%!pupEvv2GXZxEDXi{Olf4p%D#c!9 zR$SBU?>KYQ^IQ$g`miyvPGYlE&`Ldb9WRP@bVs^v)8~F^w5U>^rC{pmpDIdYY6ikNGP^8i_N?xW-2Pki*MkmS{ zIBMssp-NV4+hOShccP%&BC zuk|kkemTr)iD=c?fqu){<ty<3oZXo~zo*Fn zzbJrvggxznegFW$=YIe1q>xg$qnmiMT$WAxSVaXM&DvZkn{bXoJPPvRBGt(`Mab{m z?s@`Fq704u)qc=>OvK8^R;e8;QE>#;7?|`|@qDl21epT?o>8SrG+r{>iF`NN7c!+X zQgQB8nn5oJwl%74(@w!1F_tNzi?qb`52xKhL~fB4wJKODS%|8x7j zz0d4*uKewl*6ycw?VV5V)VDveZEpQPTgK*JZE72TxuLFqe7&;v=WC_aKV6kp{@7tZ z|L+Y?zHy^eN?l(f>F9c))D+4wF&heqNQq2K-C!tD&H06(w}}_UR;LU=0*IB8J-;wN zbLS0DzF}zr{HbnF?GQ$YN`uip!Luv@#B4-f`F~88YR8t9HU?W-Ot}l@bwTJ*;yR-n8yacabT7XMl zg8ond+n0k)e?HU7O9{;K7qk!)411jlWuq}&AE+T-nv;dd91m@BLT=Ejsh5{_HvoCuIjHLy*z0O`dXRSQAky=~3mdIIQIaV|~B$vUIq-;!= z*4+E#{>v66{r~n~z9{LZ_m37O{nY;TQ&L(}vzb;aL6lSy$u&7)Dztu;U*#7i&8}t_ zCFNGRMM*QOnMFz2RrX4Hb#VUm&%Ixs1`Ui>D~VM&!>QHH8%^A9k@ehkP0`H5DdRTWmdPWp%7$#AJliJB{3QDgU+aU3=eX)WlrUf4}y9 zi;})~?Rys`{da5seo@l*ti5+p(s!@@yHipTBV-&=#irE}B(cI3r(DL$e_HuZi<17- z%1&a2Nos$ zz{>kiN%?TBix`g=2mm3q;sDK{@?xkPf437r-Vx!tKu}D zj!}us)DrAbdyg+l8rl2$MM=YZHy0%(_XdlS5_@l6loa24)1su<-q$TkitfE}FTV1! zIcMJKXmj`J->>}rqNJZ&`P`zU|G4sxi;{kR<@2Ycb1ijc=fQ)c)hFl7{NB%ZjRsB^ zc3zu+gFiU<=;324AH8ejzC1w|wSs?G`G-ZVU~}hWQPPc_8;8HSa=hSl79Dkc$8$Ba zl3j4rCCC;PU~30(M*aRJM*gyry5IlN&A(f@`rB83@akKx{wt>g;4=EOE$ItPh^2ll_P|Iz&)*#Cz8!hT@?>fT@M{q){B3& zh3$aTTi`FYetPRWw#==tb8ql>Usgrj1HY$%dm6Z>fqxkqI2uIPQ@itfTpkT5mu_xj z%cDWWrJLK$@@Not>E<@EJQ|QL-Q3=lM+3s8o7=MTXn?zPb30WY4KSB(ZiC9B0qWAt z?MrzyK!&*U+EE@2UhC4$?Lv7pc*Lcf+kEn9@UTlax98;1;59DY+*XrEgRgSw=60Ao z8ob)2o7-6OXz(hRZf-xxqd~}}o7+b6Xb^Pi=Jt*}8U$Rrxh*4)27Z@rZlB1b0qoMv zZ4P-f@VRtzdqN%!AeU}#BgmtH*QJ}=|M6%5x^#1!J{}E#(Rgz@Jsu5CT)MgK9ghY# zT)MeE9ghYcmu_w&$D_eRF5TSjjYorHmu_y$#-qW5F5TQdjYor5x^#1!Gae0IF&uAR zN5-SU11{a%HjGDum%DUxdoLahj$FFA4Hu6F*Il}~eHM=fFLUYUwpTnF-0#xO?W%Y* zxaQK$ZKilMc&SS_w}ax*;3Y2I+{TGVgBQDWbNeM84PNBZ&25u-GszcrE?D4JQ`ea>E`wi zJR0n}baS(h@74dG^}z3>1!iui1uhY)IbYq2CZW$Pf4oGvoSW#jOu*QXeu+TS*=d1G zgjCMTMw*e0HiH12%EF zm^Mv`ZR@?170ngE~Sz5}b4Y8s%m5v%PF~%3{MB#d=i>pjS4mXnvrUklFAEgDdx6=Zb z*j<-rh%*ZV-}Q#LjDRyLS$r`o?@U_Y(`0~O6yVE~78n45bNg=80$x*aDnrqJ=%f#a zHrOQ>5jVFn$0`Oˁ!4U*a&+g&;f0SIWN;8-jIqdtSlvlUGWfHK>WPZ&|c zVlrNan`~3!QnhSUrVJ%TCy1s$sVTA)DW2qtxKd|Zc_fCktBlfam5bP7X@Tz49lLYt z|EFEMbL;;vv6(K`u|HjgZXjeClosT;G5znXCj z&#oKuG&znL<#U`V%BpSYVy`9|CHH7Pr>h+7dfzWq{r@xm%E6jdCdVyq>B@0Y!5`Rv ze*S+hk)&%8wOKo{e8JXny%!_95$KpveC^{3PFB*sFb`7(BkRB&TVA5kW>TqU7ZwGu z38x69q}7smNyw31u70eBTX{ZL?j;E?4u>)#-Gdpq#n&057fYCtt}I7{dX}qql)Bjt zu%#ZU17c{w{C{`qj^(-a{~y{`SGK=!_1CYy>uS~c{=aqjk;C^KT89rG?jQW_!TS!{ z2gJd%_y5TG>i^h&Z2tv&f3f|}y$|dS_EOF_|G&QSlUHszU;K}DKehX_yKmqAwQY6x z(cR;n&pKcHzimg_0cX8k-}`U(G;mJ?_cU-%1NSuWFGd64dTH%?J?}vyJ-RPOe0@RB zH)}qTLCHWFP6gViVAs>WOdZ#v{uoQ`eSE|bX$53E0Msi$+#45JHCbr^eK`{M*7xD6C@onW@^qsg9B<}+9*7q`fAh1&YV5l0zeJ=r>KA)ZE`un7!8 zEDXh>xX*&3jhy6EXeed9X2k0OR)1>5A+l9aBeG>&$p&aBT&g$vm>4a{kwTaDgt1V` zpAIF%MiE7}zGuXdhf=kmm+6(7mAYOHM%J@%-Ic8Zg+B)&|*Q_CcIB5j5y-qV#-dG(qbAf znpLb6=yqv4*DRNqn2#(}Nx9)i)m+k34(|NUh$Ggpjb_o0D+H87(pgjlfSkn$iHes= zq*GW&6PkrHS1DkP9C1XjBv$p}K;OvKoHVj< zuvZH;a~X#&9d{a$d6jy(9sw-pg@4-^afDR}5cyVsEXm=PH_?OgMvrf&I5Q(rL^&(^ z3LzxeAyi)5I`s@;0Y$U5m=Tf!=_b-ctHlb5!D_zUY3W!O^|o0w8x?q82HE`2BMhQa zW>QtD5hL>r#Lh^?N~l`lO@kH%g~W5&Agkfcyi)8^>#rPfpjA(Y2(_6q07h^wmt)H< z3J<`gShHLR%36ktB%GHwkxX|se|*G&B*1djs8LErgZmw`8qRUiu%>hyQXn7{Djr`7 zr?YTAg$f(nryQuHmVlzJVRVBF>sl5~qn=7haiJv^0VU2B5aDVp`>(^JZ!BCL+{| zF*RgsIh?QOl`a4Q{#ddu`-GjhjyM96RRyVz;RU6<)=d>sMm?8IM9W+zO-Je>K1ci1 zRtXhj_Uh?Igl(riGtm#3KHQ?zu;%j?YdR}ONG;lIghjpQH~e-ZlVhZne>=kP1u)*bAbT8NK#E13NTf35s;^6$aZkk;_aqREXsJ9U`T-->;_c1nk2pM`W|i*7 z$_YUcDlMw5>r|v6wp%$sV{+wqq+jbsSgj<-fQ`=%Ijnx-M2y9<;hya8kYqyB@<EbhDipC2$e48!tvmUfd$>U{}ID zZALCJN#5dsBw?<-W5m&eGbV)RJWvD(WdWS6bt2US;8UZ?s4b(>mh%umljRn|QR_c6 z;^>A64onw)P%54Ra(Ka#l7(_ASKx6~4<;(}Q;%bPqBI z`}-f?|DOG?-S_SN)!uvezHaZU_nvd*6IXuq%FQcZ*# z&CbDsuHD?9#+nrK6l?E7tjQ2H_ty9vEH82M%7x=h2C7{!4tVY6?zGI4(>;}W=R&N> z(6JgK%*nKv)Qn(o|t4p#8m&5dcSN!6ZWz4{`o z&?Lk8`U!!=%~xGG&SH8F0oQJZreuaEr+X@M@FJ|>q>-F2a}d6EGcb)c*>LI<%YPx( zq>)^J<=8SjjdiBRKXKD{A=c#Pz5vUyWoQ~}QYEMJ@?MBFxhXHe@?N_MPGe1~8v%|P6Hw0=JAE&Od7!X zYY_+%Hy^xkoXO37!8n0yH(xnD_sJz1&i&fWS4`7Ps`gYr4_qkEqybzYkN?`umrr5& zCf)6+JV(}*#_#K9hfe;u3oq5j;+Kh;N zppmfolbq>|*A_Jc3GluoA3ScAa|+VXoUUZDR$Ie*jO)6o)8OSG_}B?M*N+ld3TNyO8&EFO!CEioh>2kN1y zUh(Gnl&u=IZYGsWkQBxSqe?GtI3KV2HOk>3ai_DJE6-$;w6iUnhEsP!$DB@`ylZ)G zr_Lp|>*ZPAjFwGq6y;PHru(0!tYdbk&Zo&b?zB_qna%62)a|Z*?p7Sm+ip5N0iFHv zI(OQ`|8u)R;5Zi2-9FBcYtpYqME1M}yw zoy{)26q|5=8J@OctEm)9?NY7jcBFjW9b`%_-WA22(=sVE zL;7VWGx8R% zo4HyTKdI!%lvk#zUT=&(ZgnhwhRUe%6w*s^B_#|=U=nTAtb~-MR1}H(dWf_4T!%Gz zw76#V*c03r8{jT6>pQfoe@X+~pksgkbb~O8j_EmixNQ%k$I97#TxZ(kg~l_}KggLn z*wP5LL?AW6Nzin{qO`P+*BK(#1Jzm{@CD(Ls;(|su-sR0D+h(R%U4CyV<1kxnp*kdB1Hy9`p&5Qm_F_Hv`km^$a!!EUI zdADot3)+jqe2FYNr%?$7Rg+xF(xZ*IN&s&+MW_1|on>u+99uD^Ke zHCua|zq9#$Yk#%&6KikTY;9tj&)oRKjVG^uc=c_oU%mR^%4b%7ag-n62{dBdm`@>6fjAARIrs|1# zVs8V6Fm9(XP1gqMJ4I4fNt_F8UF+0m}{2wE!mcY0*f&9v=2Wu;sA1}9X4oEF+#E@33`2Cy8tsG z7D!aE03eaLB^OhCH`S)MesyV%Us;;tmxmnDQmMp85h-m#ls{S3n^-TN_B(wO4R4`p zb7Iz)EtESv)ogZlxurQWOLMRzjv~i2y?Vcw?UuS#1!U^IV1wz~RxOm~>fKT13)(cm#<(P0 z!$K@n@okPGaHCp2=S?DtR1U||1Qc@$8o;GWIg4dMvr;r0%@j$qm6+KM+B?t)V+juL zh(l4*Tqsm6J8^pq+KgLxE|O}CIoO~%FjtZ^4DJ%Cq7y8Z+7?C}a$T-=>2R6GnHtP- zb^`M$-B^|ZvSFs-#DF)=M9lA{O&nQ!Vrh==TAJfKPdPGZSEbXjfUMUo4UbkLW+x{` zdMrp)n*FLCi6~&BV`k{!%KJwgA`V9bt)AkmI`50H#O2tq#AAV;Ni|~?UnT5q5h*}& zBDc-eQA~SO0Ogb(m9l-Qm8;r^W%CWIBXd^&8!Ff!%vcrrxmeAF2 z9T2NO_q0#!9^fTvV7TTnfE7Dc&0Anmq>@Sr1sP<;f-eB2H6z=jOa!44@dT)LW1Xv` zxcO*bt=6ioNHLZ!#LSx9^~7KhE(%nj%o(X(Cnc-ADxi71FKmw<;i67+LXeb>O)@Vu z{f%C|hO;rcPS;Cz7?ydZ0+KoJNm=MB+B1J)v=f)$`1z$des09k>*dq_VA`9HS{6_G z`k7dOrdZkOr&Y~+xp+inkXokSwEd}P@Jn-Km*(JBR#rcKiOYL$sDB0;810fgk!(sf z?@5MmJOd%AERnPgr(Kh#c+?_7Hez5%;^KtjF<)@{a<>G>=;bcD1jk#KeL!Bnviivt z+XJKw$| zZ~yw*_3d|USGR-PTU#I5de4@%_3+mI=I?I4Z?nCLZ$4||V;fJd{Nm^az`Yk3-7Adl z6V|fsEy9lX>aX9~$`2PKl`GcL@rYO8#6E6dc!EiQrJRqhqP?)H7Bgu+AbZ_cwYYZh z@*zjkQjKs<5xKspcc^Zzo@UdTqN1i5r9~T13+VL_eMUY3H9 zLb(bzh(fhe*BOm!cEN;Kk_(1GG|IIU(;>F6jxdyJM(#DqY85hIQYKW1j>!b)tMT2g zW<`9>2E=8MZY>p-_D|f=qh# zBO{DTzGqgOomwT1r`SdziugQ9)lS!KzmX1CZM4#F*78*(r%0>+<&?u4@wcs<;$%Lx zY^Z@Te!)u@5?!Ac6X6bF6eA%$iXkDNu=#IB9Hpd?rCV_*Vgt#5sTD&`U7uu?m(`fs z^?4Y&Ud9^+(GHWr^-(TZO3YGSJe^j25K5#i-T+!To1ltFMPd<@1&ayiGZ&9G>k6>( zBO{D_t%IrMgdb;!exd2}2z{(knWF($|MQ!_kjW~n`h;bE2V=Yq; zl|;2IR=vH7>?9q+AuZ1(5Cv7$IBaS5-ZMrVJi~<9UOMgV<=7S&346JUQ6_CRl`2}E zhHQ8-%z#5^1=Kc1HylM?3`=B-#*01*6^TGo@!S4OXoDV9JwIhn`XdN5v= z2>}tx6)Rk`NQ=zI@?g0^QQ2%qZ4kujpN}xoxf0CREmg&w?zNJ{+#0VxeHl-AEhp*Euhmp*CK6 z%26`o{$duS7=bg}sW?oE9S`fry1{Cxn<%6^iDE;9l6X3`_ID!=CXJ`?ct_wvf&evJ z5r55+JLy8WEbz|v*Py3Gds{+EQ&no?7e^d4-D&Gs%$w{5D&8iXk;6sC3|b_@l)Oxm z=@n~!F&NLQ$!?U3k5xUnG#{@JP$L#mtSrMRExgrP8RgBTqFGJCvmH-DbBw{1 z8zCo*xz|-8v6}G2qZL|ao3!Fl6wcoH>JbLj?C^~O?aidLbeU~{1V$PHr2A`8nid*l zP_*h?ERL93Wp{LUm7=QcTB{5MdMD+25z1QmTF##k0(iejM!bol5vcTa*=x{*u=}!8 zjHD+|m}4+DN@t=}S?Bmh zw`<7Bq-1D#uEmy>rmWLgAQWKDE2lOXhFD+0k7-Cb(yGTy6b*%R$`)h3fK}@#NPu!O zqJxcKyQHoE*$4y4C-bp3M%ubZiTec1rulXNCMZ7G;DT17(@?r8jYZ-hxA*lU4qL}P zO^*=L4y8gMM?lS-L8vJyg5@~WTZOeUju#*xR$w626DZ)R)ent0g5e(E!PEek zNK~W!@j6O8syZ)Qf!Q z%IRs~jbKobM7Wd=XGMw86Uhh=j5Rub+N-lwxdJ8`(K%j>qPG4mBMeWCbc(Gqv}HD` zP*_DfY{(Cm{DP;h_2ZJ>3=0|pVrD*=TK)dvLt?TUV$(VWJE`z_q?-c+K_NhsC=PT5 zjWAh5P%(WNW0eZPg;pfn%g6W163>BcFAyg5HMXw-GEr^Vk zm0B6DYXOafOLco=RKX!>w>(&-jix-I78X~_y($}Q^!1vh<`Y1oR+AguWI92UwLY-5 zI-18494jM^dX4RN8Rx58hA|QnqI(mVM@{0sEZ2{yP{lb|_&iym2#s4Gefmh&bjHp2%o5wj)mFwe4dP{mm1Vuq$hGCl~be2jgVKqx- z-&5IYW(bv3WnZgO*@l^6K(>p5EZvMfa1~`!*#rgAi=bS@1yBJQyvPNSD~h<_t_c24 zRdo;DonETOR`30Nr~jA_bie00&pGFP&pCC@d)^m(*fXI*yJBn?rEl?dR0xQ0mO_ZHU{YUe46!VdQMd{Yq+e}>BbcQuh%N>uYGY}rDn1kU&yV=gD zlkrY7A@%bma?wCnU({3*%38FDMe!^`Aa$Lqn9Jy!Dbko%`LPZO*+XrU^z6A3&N!lK zmeoluR%}`es$esOc%5~>3$_@Ip-Q3R*Oz?BXu6n{@{tb$U?YoCR5_fg#_19YyW@#Q z&g7?^N`J)xJA%a)rLs3tYHir7_%g5TnT)xi1XreT)DyuCNWIkzbTm1i*4iMu>Sl;k zC-g-Jr1p?ie#a0XvpyT6u5@fzwS@+XMQ6~TgHXCtOMu`kVRH)YVzEw}$Y@luF}@jT zUz;R=zh1YQ?Kt2f)>RXT$B`l+Yr$(&YC0uK-Dq?Q5eqH-FTMm;_jkg%P91KP-B8@? zOSrSSPN7rRxJX6|?pETCE>rUryrg${_go2GyPCEHDmGh+j<>v;Mz+%O8E`*T$RyM% zI*x^Lt8gk8`YtVh zFF3|HcrbWiAE5tBN^xTzY+yP#Stub)H>1jQ!PeI5_S?%jFN|86g)kBbVEIz3V};Pj z=(kw^|KlY0N+!;qIBNXL@#DuH9P^GoF`6Iw;RrP{JUl!6e#L!?ljRS|V?(bFWrz9( zo8X)NrU9t`^Zi)gbA9c;_sc#n^Gbg!txMkpVE!3DTc)=S27^;$l1*w5fJOyr^*SA- zfg4C8W?k}{bwhZqd=KSQS)ZO$_XnJ&w@q&a=-P$o5Tw!5WWF6ZQ1pdyfOk!A0r)3w zTfklmX>_JsCtyBMeBQNXg=gFF!uL0AZNQp|JmzZc z%{{PXItFT4sAdohY1Bj+#bO6q%R)7a0`!Gy2I(PmDFtdRD3t+D?o`E`Yk~7AZ^xGP(rc$`SxNsE+7IWjRh|xk`dR&p*8?e+*A(%o z)lgZN@FLx2!LcmXoS_urj^Lob=eY` zcDn+wKMof(ZA&<%_0@7pDh4)C1}6~|T;Dr7nVk-RdRU!=V$GbU-D<~x6N0vt&e5QM z9U%jjtU0{F2<>rGB^WD516~voT+c%7+^v}QvqK2fGonl*omeud-gY4lpz3Yo1u3nlS2p#)b`{l`7EAZlW1>#1m;# zNYzYsLuoxzcU7F}wX+6^P+1SCpH*4h9mg3HxKXClwMy9Rvsi+4qXr`aI1^Q-2)Jg^ zHo{tehES>m*T0;~x|5AYc|qpx|QSO%RVOZ6(}Ur{H>) zsH~G6LZGs(kfp3wW0t&DpU`P*?O4l!m8*8E(;bLJZBbpQUA3tkb|+Sh2(C)3$~sss zpxNQFGoLBC18_)1ge_RR(AJ^FXuZ=6xpTe-$rLD^qmgL@x@b@^mwA=7vvmNKE!6xp z(NVjDWX)ipJ7JeyLp$RX=Co@F)RHgd3Vu71h`LJV?Ai&eR#4gAm%HU@8>pXE*}Cv( zlY;ACPGu2LAFHwj#A?%+A~Y5?fKdhmvTduuOlgY=!SyXv zSu3cARarwPp4H{tNdnd8!GKxSjHQhfRc&x=qem>5%4u^pl(5>8YMTVtvqWVr><|K# zHDVRFCfSBlbsKDfFvw#HqCsOistx3m`ZTG9lT1>d(QET@;Z?1!vJ0Kp`B~P?askZ_ z`H+~#q|!K=hz}}7b&$`YH#pF4rc#VB*=j47Yz0+Gk3VP(3U2ni%9_|ZfXXIygf^e1 zNfg|E`9YR}NY>?OS*qD$UayTh3tif-&A6ONucw+=`#su9D!XtYncEnA5Y*4AEbv(^ z8r<;^U2W^NAx}$Lw%4qPQDZFBqi%hoUCUQ9+gZfyNrJ=a4s>e_U zOZzP~eX?lu8SI)?CqQQ0rZiEaRmn_M=M7_URd9U^RrV}U5392JAaJ(>=jldgNbTw( zv2YksxoJ~TgU7mzj?NP#R%|qLzJ$vlxSk~{dnP-CKxNnUG4J9bTt1+Qe(4K4eEJNQ z3uyMbK5v}kGOx0yvvmNKUDwwyi`%>ZTS~joDmj(q-~YcX8GTk!9l&KT|1&<|57u$8 z2mX0I@S*K{4;j*G@k=9$O>oay!UlEEueNV|=S60NVpe#NsZ(i}JP7MWV&6C|c@oK6 zJ^P2A$GP)Q!q_KA;$L~eDYBp=Awg8Zt+`!$!LnzP9ab( z5);xcvzM_t@&0B7I5)em##bh76F8fT?!c2OM_I%mUv1B7|YsTOWcAfEd zH|?%hDyCR4+CoTe3$>?=2cB=q6EDFr15rriT^Z5}V+~-z1!o*@H_&Dygyp9obCE#( z+NO6hHq~rtW8HjAS9jOK2t^dMMMFCTmx3Pfk~V`lgUvkf`pe+L5}I;N-?8K3^xkoH zG5YZFU@_{k^$oq|A#5>%jO^>=Ri3hf>NOhB^Sv6j=-hTWtMFP&+C(em^we=xDwmrI z7tF1CMFr8|nz|BlPDUwzrJ;(sqCW7#l(ULO`qbM>R)LnRDgPjg{|B^f;j*^>1BsoG zYD}t>2lK*8>Md$=0U!%zkp0_tXyF&i{8%lWg!Z%QN#5G4T`Sr0e(Xid3M)Oo=CH~n zcF{$gDwjRvDz*ytT9C+CJNa0uoAc?z8Ol%!`}KuFH{(&IyWq87ARDw=3jT?O)d zL=ErS%jb%#(EMPr(P0*i9aD;$VS;+ofR5PTIoVNg^L8l>;Wa=)#u^?zCeE`~82aHlk19u=8QDIvarm3Vwc+!%qw;&@m|Qw^+mL_o z4}&)hS_ghUaP@!@`1W7Ye`?>0eIM;J$$lgIlq@YfO8SI!r*um45&#$TE0?zv@2XsX;~U%G{EOnd_ot5c{Q8$)-W;&b?&vpxp(s^L6iMLe#)1*xW&<9k zdf5$b*GZ?2+AiCC;^fbN>;2t%cO0p?;q}+HeBy#T4&9nK>&)5n`n3S2*C&84zsm%LsS7-_+ZVNJ#mjyk?v+2xZu{oO z3rZiho$%nRN4=l^rR~P{QH5=19rniazuLF=^@?YUG(W>Xw%$ zpZnP7-oB68^zrCyv!4V*85I5`f(JGq@pNkvx#G;9yb`ls_Q-GpG2Bq9Uwze|?tAbV z&uI38+2}080eee%EE0_u!}$i0q_MO%X7@Xljj*n$&imTRxKR~%Vx%VMiJEv|P5fiQ z$SbbNZEt^l>kIf5$NfC?6K@JW=DI&3-ynbSQDq=H+u(rH=2o?x#%%h0h^n>HaUF~p z)izS8HfDlNGGbEU2x$mf!gejf1J{l^>CvwvuWc&cI`rOGKX>Lcv9Ijg_uU8oNAkTJ zkA6dP^^x`HEX@Ina6#aK51D;p;P6r=_LlF)lMgT5eAe}b@}J-EJ@C~_22H0te$n~g zj?PjXun6A*9{8M3J-_MU!{BZ0y)&clKK&QEKcJt>+*-QlY{GLgX8r1&e~r%8Ibh(1 zDAb972Y&YX(7vBP(%k&O1+nW-y5>#e&L2O2$kkOkf5~glUTHmJ!!yy@8V4-GYk&v- zQSnF5AEm}V_`r)p8}5m{^?_sF_x4qfo7^LtAK0ED{pz!#vsDgQgpU9Z9DcR`o5Xcj ze&+YDK6!)wtm$okyuB&ChWYm=PrGd2YiB%johmw8;Ta3R@aKVl{O92xopbiv7hHVW zqZ>N2`)>O3hqm_LcKQcCc?WvY*?-<#{dsh@%mItI<>!I_?doUGy7ncJd9!dgYro|1q=e z%+F5$`Tp%UDSzRnKi>L9bhgL=i@2xffi03t?u~x!rDv?~dupQm;=qN!`RuLGzw1sa z-t(?UANt`v)3--w3mmYBi+LXSklECxocq+*TMs<_wTqvZZuyVsEoZj(JXJa8w;xM9 z{K95gbT-cci@1j8fxmjlEkBQ%FW zJ|bh%6#_1zYbMfI8H<3QXvCj#;B8|f%AnS6Grubhc=VM=p8P{}Hp2m1;;_|k^_s#MrM2i_YYn_!>vYRj6KPIk8OT7GtOXaP z#UjMq?55cHh(RlH&i--a-ygZ@hyC9+UMPKOEBW>r4atvBc<=|o??q=x4j67=ajQMi zjzk(xlT{njd24Y(?_}jwNkO8 zUj6ca-;vqZz2(eL{r=WRqq77DoK&TYhGq;0uP;i0P}bgY_~C}L87OLMExo;lWV}>W zhjt9{RFMb%n|;%7en0zd|IKF}G58Jo%b&meJJ%FyKP_0#`_@nY=Zyc@_CRzt%>j3` z3AZb3ZM&3pQ`YQiCrau_U8z*r{0Y4}6HG*m!Cb45*7i#R($bN^~A%)|F%VE&*6YYctr5Pzdin?pB(xSbLPw6 z`km$3mp*r>SH1m~(+)YIacoHW=7Z+yA<@}w9Iy!g2OjwMAN$txf8AcWar?-{?Nj>S zZP|DD5C8Pg&mOz~S8qP_v$yZRtQMW!$^naTX5fLX-+Rw;$?OkrKKANU??*5F?cL=UH(mMosTW6Q6CAJzp9dcJ>)(9k z*EfIfo)33lIP}+l!9P9fZ~o!5>B(Evw`|_ z0BwGF(+_UG_se@XGCv{i`;+%vus@D+z#@DXc;M%rz4w#8`X}!~KJk?keb=NUpLvwX z{%v&B_14L+KJ~ND`~i*5MmS&*jtD&PTR)GVcgyr2?>oPZZhh=KzK`tO^V_1O{MoyA zd7pdc#4Epj3wQr7osf)NC4XQ4N5Oymzx@N^k#5}|k5q)EtnKWw@F0JGoZr7A9x0>) zN09=Ko|c@FfvYv)E~t8Gpvx6A)NyjW(SwG+W9!)LnO|6b?dhw47*o)2Xx{E0{I#WXe)f zBPgD-c{Cj=jl(&g2k!>K>%?{y^STo`jn-m~O-5=#6_x-GR&V!c@2X2crZk}HE=*Fx5m{~k3K=wi3@{CpWA;BE~pW#9mk zyIY$#RvqBidy~EBl7q!w&hQ0XRI7Db)r#32Qj{)iEogOQBb;s3Ftmi&k!GyXjZL9- zv#n~a7OX)EGCNZwxDW*#Ry_RYh%7X=cd|Pe%1j9|dY7h_ z_ZYylUyC+`(0HuIq`NgU!$7dfLqw-gcX%ol&g6Y+5D>#tiZ)vI9Kxs*9{*I()as1@ zyiCA*w&`qS?#3*}8sg=QbbY-`g&HF3UiE*^5XG#n?ETUbLtOdtR_r=*fCjjqz>+)d zdAnm*(|@4dfk$erH`B7KoX=a`_-a$b8zqcZP~RIRQQfW znw>QAX}pxgOL?3nzYrVeocp+6vK)CnOGxqy@H@o0@+rJB4^_;ki}U}@-zm=jU%t}J zbD3+n4w#>A_NV*cfPw7#=C#op)ke^iWv838S6vFF%SbCyi!rL0jj%zuOON{s`M4`F zl{L@-UsYF+Wwo_6Hr1;v^I><`)V3E~E?1B-fQThiZmo;KYbB>vuQpeGHF(Nk>r7%U z+7!^j2@h&@VJ5AkXo`PE2qq<^?EJfyfDy#K#he%8>PL#DxJ2h#&D4j}!X z>zDQ+vO8q&mhJ+D|M&dtK4qF1l7aUWBsum`vF}Cqe|kYZo_XMTq2QOsVq|w8GM#4m zj)~&SJCTcdt|~@$_j{&OfRk1nr-d_*nD@hCWOpAnjRR&=EVC;GKWE&(rJ=<~g_XEgE_eZWFslr{nB! zZUYVbH^H@uoM=RNTM*MRmJZv`mkFjLe)R%c9A)c4Ks}cVp7+;xj;c*Z00mBqFA+>( z87&s!z>%9qL6w{qUo5!F1zIe^rK2z%233IxOfq(n;HnmBv4{x_XmN-gPF9OQB)B$_ zS}ejlgqRMpblApUD434;6$NN6WL}MsSdqGv47Iy?!wMdIaI3NHm_OQdrYH?d|ZL4eX!uOZl zrcZ?50x|7o>9AVd5=>`Ki$!=b04;X0^{`sZtbOXV(nIux+gt9|NleIw+O!i;;Iz0Q zn8GqzEaIjyH|+pba#~Cau5y7Ei@2aEOxrQ(UA+}*G>!$o-umo;IMSd@aqH54fPM)GSC>aDz1?Haqy|}vj)rqhfF*< zUhDtwu|tRS{kQjb<$oG=$-h4S$ZUgM4GgSgrxm zZrju5GCll7zK(pp6vOInTN=rM`~new&DjX$LotSgntF>j*3naKGSD!Z3+bWv@^zp! z;Ms#D<9Q~8U^7@G~SR! zRnr7~0n}!}D7cfXdfYXv-9}XgrByW^=Idyh5fG->PU)&HRU2j^hL{;wI@(!Zt6Z>k zyXi_ZP|0YVb&s>J!7!MvF%dTNfYpYRllAyoyoqj3;C8E;`)0&Ck0@wTa-PdC>k+nBiuHBC&x z(xDi2te{ikC8s(^`m1R}#aVS*(P*x$s_Kfext0@DGH)!Bu7uOtD4F1LDTz6>sic{X zFrh}MNW^N0wv@F|sj7?*o8L_)5)kGut2COn)2j{oNz&gbW36}wg+Znzi_!x5TUF9! zvqJp=zU9eSJL7A&>>gFpo+{>b8U|!o44G{WZAN2mcY`sFv6Hvh>p4^ZV|bJ$G%=gpNc z`GdZUF9j|-!s#HT_a{^4nyIAAyJ%yCYG=KUb}Z}jYh11VZF~vD95d!^q`N?&4oA#F z*ScD>4tNE0vo^cG0uHmim5{4#b`(whe5C76Hy;f)Bb14-XH;%WlZ34XRlQ}%)S-+o zZOV|EcugPj;JJ`f#wRQ5be&Coz@{+OzrHbj#ybPe?|9IM#k%}`Vs#>s35bs4iYLn`DCbqb!ihnCJ6EtPAeT6(Ia zGNaYHF$r7TSzF$mj@R=b`(&`BRl8}(93oWVh=k9M*{RkNt&Y}!d84Lok-{{IHl~h5 zU6q!G36pfNRwy8$jumm2ypkJuR%MOfgWFnYoYK>ttP8{lHmjXft&mDWj%XZFf*aj( zIuG*6IHd?*q7-)|5H#xQlr)KCTW>CtWZtRmdfR>^#(<>EZib?;jyZv*rR}*A5U%oo zODR8agMqs~U)Ipdhp=Ecm@0Za>Y6#}DkGJqHcrv~7QRH@34@f89cQOqc61mFYl0-_ z8Ib)l5W#gFpDy7eP-n7?*zK)8K2marthkb8jWLPoDVw!xM}3qw(bYDpR>BB5;gUM* zaVIJi4(Dm<-FzLmO9=wnS6r>U!9i!5M!JD1i`Gs;?Q=T1HjunZF<8{7{iqq>Xha1&QFDFnH*)knVD8vyw=LW&gZE-u5ZJ0e!UgO9+NdiXgS)V={&bAZ5 z5k6BwCj}BPG<|r)=&@!xZeQA&rjt&!(U$aha%jhEQXwE5bX)18og-i2>gc9Bh!1aQ z8(2A?bYx2ANQQ(Wgn@utAPXd3LYpMGoG6!*<<P^VCe>EL(}Pu095CBAn_gHj)%^6_{%8ZuSN zIgs6_rqY;9N}^sawc}p5j<$PDW>-x$J~v=IT*+`)lE*L1)$QGWJ8i8$=a@)v+8J9k8M=~JsMx^L-1nQ_A_P}kNB;08@ikS_= ze3>$`P-+BNo`#H5Zd+G8Lx1S_p~gXup{; zS{-GOGbvoiI_3Nbc5r30faKDMA%KJshX!n}YT}>NJMFV<}WdhRVP4ex0 z9o}L*-G%(>YC7Udcmg#L&gbyJZN1_S>x zBWw#)3|42UVet6$p)PJTY10{1y{R;r>;$35RYSkv`>H_1j7FWhL6%_M zV+R9G`ol3WSA%xp>PWmw552^Qta@@(ynrJ>r1c(IIlGN+99E(`sG7lxb=Fnp)THw)*^LxE@Md4Gl0HP7{U%t3&)u z-Hl_`oEtp*NZHlqa+OXu(xGImobcO<(PFhky0YbZfeD%tuyg1vz8%J@E1nPK3lwIq zgqjXuBMmBEO2U-J4yL(oM^{K;t(dY!RfqJWFC9=jy6Q?B3CFBRHdh1rkP;+tPx7lv z?HF41*LAUKt&C>~rl>CK>7jm}l|ESjX#{Kutc_G4PuZi_S*naylf(VGOuX&ES`G%% zH{(s%G&r}O=d`7Ex`fghC)x@rg9)5A=b~vV2*ueghm|1=)_L1bkV}L%4Zbk;`~kJ2 z(^0hprb4vf^hH#79tvnPv^v-Xf&5@BAA}6$R?bRi!kw%;JvPnjRk;CKWBxWxxKJ}< zY}a)ukH-QN(Tv|lI4yN3ALx*3b1v1%j!p3;aC_XDj0G4q>8Y#0%A+;nE(k8zbAAI# zl+r#YX>1xygbU6N-pcn?Ezwpt5z6eYs5Lqd9xZz_p?HF9Sc-O)D`|G+%nn1!Vvp6F zgCCukFi6y*KBjn9=}H4L#1XXyWL+ZCgcFT4Z7MH;>72nr0wm(fHdN|LRGYVZ2l)?{ zPNiASIT9vEt(x>`oF->P6|%#aIi$o5ad$alR~CROtHCBKILIdk=`fvyCF(93!=<=z_cs zY58~gmXl;6QO&sR;RI7@Dr<=ZWh7{i!EVv1EiO$k6-8}?9-{J{>ewGa$Nq0kAKPKf zw2x?bolW3{>qc^gQh-htGd8l*wpcpWJd=$EVl7`6#C4MYbgVxoIZAeqWMU7v|NqqZ z*<*K(Sx0|3S{iv(arfZw6tD57RCIz-3>1zw1 zA#Bjv`Osf023QbWbMIN}z#WqQ*tI2g;1<@jQaA#^HSq`7+paAFWHvzU{x+->hCwj$ z!V^`Xb6J-9ws&;ypPf#D9jd{q1QTW?Vx`Dy&jn#<|MBtKhmVyUxdONBwK$-_j-iMP zalx0)3rDkjEe2|0HC{xF1;I7->OOfbIoG)&{0aou_s%85?y+kLP!X%8BJS`7SG4LJ zxhP=c{2aMkdTp8|gd8r$Cr&J(Rh*F6zQM0eu^d>v5+3bAFb97A$zHn^)WPbmh-+}c zSIvuc7X>x3x+}twKyXdHx(kfeyMn=r|20t1Bs&&BK}B583ub>n3VIGp2q>rscQvtu z)>P1KEC(>8BHYsibKn&eSR$8j1vFGdxBYTt&u5A7lR(D0X z5D4b8Uw668D?oSG^+mQ|*92a7*^Dc!?uu|45PW;TobIyOSN`#-9a#VWBP4f8CIaJM z9Dny%9r*qqH?n>B+rwJL74qN8v7vj1ju_lE@B)bOcV*wJeX#7~(zm1uP~czlv-^a- zBNGxSsSQdb3b}M(V4J*8%zkm1XEJLq^?lOvM_#$h!Tx*q7lit2?>=_#Fw6ABcbLk= zY|9pD=h_@)EW6t;=KlSuV2(%cRZK_}a~u_HZ;II@%~9m7bZv?(x7#>|pI-4v)Izr) zb{Dz(u)Xp{y@<#M#Vpl}dl7R7TusV?IlPVlQlt~srHSYUS&lcI%KiI^1wm4K-h=Eu zW$zHn>+E+%cRrp2-LJi_53uY$eg^yRC%!Luq&MvyWcg0L!?$0|3x_$r>`a)X zm=Z~^&9k3nc)gwd_lADKJWtp=!17#V-y-(Zb37NWnQJrU?E7Pw||jl zZj;Nz>}o}7CTHI>_pltVn-t9P=)HYQ?OVhibB^MieXnUeEVrv(WdFVTBEbv~-z#H# z@#J~EMC8MpM1)D(--|SjXUVlU7Q-yP4|7WS;cvWS;Jy3R-S6ECg5gL>RZVz`K zKD%ql(8@Q72>T{>Xyq)u3twUX{m`dY?Z5ZV?p)M=h zrM6*q$D%&l2B;;42c8q#XB@E|kFmsdeo64a9yWU(OLG6thFz0Id{yozSx%AE+IGRM z$=%0Uj@zMCJMWa)?JTcFYqE$h%{gAntjU3_&sxZ_8D8RUs>9gboduDQ*=-89CNm#o zIkwIb+>e`PyDZ;DYqAJ$_c^``*5oyLa%(b$u{`PH1@k;%w!`vVv?h!A(w*bEXiZ*| zDYqt9!z|NUMljQ3XWNT3lU&u(X*|kYqE%M=Q)aVYx0`i z$a2eZ_MKM>X1HOt$@b#@HJP1+M1&*U-;4a3yk_7MZW6*bvsCftS0cGE^9BE5Fv~0& zYIYJ5aXhe}BsU4I$&s6c&fz``&Y4*-2`&6y;hr((pbygT-%vF7OSMyZisk5qsM+0p9s94V^Ri z!$EA|xq)c^GyS2y@Amm*kIP)rZ%A#D|NPe+1-w)1u06T|k_^f>iHIJ)z84a^_CVqp ztm_@ku02|wfrvooVm_qT{)*bQM!skX zJySdb5vXDg(AGZY*|q0Xo`HyKTrme@LKrA{1|lp>%(<5k2B+{0MBFNgIX)6%jgveB z5!Zxb4upg-*vvB!@eFx=uTOXFIhki5;s8m^5sr{PIEiNQlA|Lzyk2gmUYM4Z5gITR7XU?a~! z#8HcwV+A1$Hi%~+;@eCJgJb7LLHO9NcOM+XGZ1lLv%c5$yY?K-GZ1k!vq{9Uh>%hE zz+4{)&ttH@SNps69K|ybaf~A7AVWwW9LX~faXhlVcl^8dyq{+v;%Gt47yjCp@4NQA zk7pp_++cmL|99;V&md(hV(!WaYlGddsNnQjM9PrO+GpT-=IyjQrU|ewO%EyeD_x9DRAg!th zo|yR-h_J^*wi&!%7Ico_YkPuk(nHDp*EI{XGjmnhX3ph+&J_ZreFvx~yZq@hNdTlB zNeTh#q2Zc22b2zuoFkz0eCMzk6pcAgAb>9E9M{Y?4s4r9uw~ORx@NX=U|Yq2t(1(> zHM4~S+ad;RrBsZr8H@wN#DJ~zqQEti;J^}Mz*b7bXr74!Fw;nUZKw2=lQ5n>69XXH zk(dCG-V}_knJCwusDRS*3I!PiDijGu$` z3xE}I28+)4I9T7>AJUsd9BZO8UJlkPfUSs=LUhK%!FmM1rgJ5?sbIp?nPeM}wjC8T zTn~0To=lvsXcGE(fzgAM_8`H0!+_4XIas#+GC&e%Cvy8u`bj|0#d8wYC>ptm9p0MQwQgGB_us+v_px|6|LkgcXH zp}AHnfvCFS2(o=mM4?gw9wwSgQb75r>87jD>@>2!IuF0*KC-Iasp* zSP{p1=!}VjH3@(faaM=Ue2|0vpa57Aj#uc+*&OWIYk$Xyah5`7&f;Lt5&$dWunwI$ zlY>1|0IZ19Cv@fv4)zQIup*9>(3#UY*wY2TiZ~-dXHMf_PZI!}DMNM~#ci0jt_tT# z7hGuj^hqU}iwAQM5z3JzyTc#J&}6uX&KNmZqX1YD=PBrnfrB*&fE97{g3iDkEGz(4 z#K{Lbqvv4t0$@cPXP`4W4pt`sR>UbFIstJAfH2kw-dCHual?{*NKFWNWRCy{-B;G&6r z6L(HrG_iF;J0Tx`Vf>!)OUAd3tH%e&o*%n=?4mJjY-+53^arDNj$Sw#A61UZM)nP0 z15*S2{Xgixv;V^Wc)zk=*0*ou_L1Eq(UHj!$?*4wKQp{*7#%)Y@wVb=#chfmijd+& z`CsMVliwoWe!xcq2MZqTf#rH&bAM1K+t3ZC-4zRFu?3vYDq+e83roH*$Gl@+p`xxa zmrzm1m_w+jZOkTA)H-GrDrz1x&xzK9u8hqB680h1fV&h7EcII)HH;dBimFCcLPe)W zr-X{0I(n*5QRS#osOTx9rwA3D9Gw&@x_NZ7P|=e|PZlbA(&$Mi^#`TW4c(jtMEb;A zWYp&Jczi8$cZt4k-Yk*Go>)%d5B~085B#6&fjI+VEOoM4^;a^nVuA`1ZPOC1S9la2 zp`vbuTd1f@F+ZWNNx-Ra3Pp7&=I8!3QSA!5P*j^@ewkPk6;U8UQLT#k6=h9Si^3ul z)vTCbeCT)}=L~eoc-+%u@NReM0k1qQPtT#QDVma}go@&FT&U=|@^giXCgn+?qUXrZ z5h}V(zD=m;R{2(;qFdx!=0vM#(3G>c%2sOxvA7#4&l1~`Unsv&sOSaq3xta9mhToS zdcOR8p`yFwyM&7FlOigx53p`vYhTc~JD-V!R> zlsAQnGIB~p`w(W5-M7k*M*AK#0tF{0 z+57)vB)DW^*ZAAxJI4Mxwte(3qn(jIjkJdUFw7`^r=aD(k=KWQJyae1^?_pl zYyJ7YpY~;CKar8rSEOml%b@tmKRfR_x*s^_i}7+^UvJ=@cYQ!K0TGu0YhNwwyz40O z1Y%7P(Y@7<6)ie#E1jcD8KWa)z>+nGHyEKkZmI-h5ceK6^;XCg-f+HZ}I%s{pv3K6}UeN?Z+*quA|FQF~_lPDS;(|vAfx|fh zBJQ}>*BN~0U5AM#u+Bs9ZqWqRc?b^W2#B~3T<;-xmuLd(JOqb`Ca}&!Fu@TJ0lMBp zFfN+FIuF4ZM?l0i@A^K;*m>6|M?l2w;`;hO@4Rb7GyxG8U~6Be?YwJPGyxI!g+d4@ zI07QBOxM@_dgono(FE3c2!=!xSmz-aG|7fGyAsbmBl**N-=l2nc#%3ZO?i#1C)-RI#-h)573#rQh9rCgfy zkW3_v8d_=Gi#1SR(dITbqqTC*(`os=784vYG#ZGM&zjhQ>SdF=SWP>sO?%woO+t0D zl7n>xKT3JSv?&*8Ds8koS#=hrHogvH*R9eaAlV?Pr(s_(=b%FoDA>_<+NDz6R=S9YO%H<+X{C|Ep0QHG{zj}dc~V`T8wH`X|ZMq!d=KpPv=`M zJnv;6-;QPsB&qT+(NfE!a%8<(FQ&0Y-7esRR-tX-cA@1a+GQtN2{Bpe-?%zNq!f|9 z&Xs8C=!iaQ$HHg`23LE^X1frm>eb$A!BnZo^}deM1uJott`dNxFYxUUo{~!XELVc* z+BD^`#vf#S9>mjhdE@#(5py>)emszcBPo@J4q2lCgvgoZk|4HQDku!+;BYJ&(|X+z zoh{rl$9&DAJ5~(a@~KFz5K7e(nA5H5L;02qPZuTSbA-0D8o#xwPd0KveVz=u6Nn?2 zj=1Z3tIgm`Rl6yINVh%j5scTf)XfzI4;HC%7 z1Rw)eNZPGg*~=WOww<V%?23!LpBB~LMG>{cR$s`N&# zL^B^yR%sGNOSqcJ(Rn7SulY^BSfg&Vdb0Xr&Y}0ZiKZjrG|6Oq%k_>@B-|mOb~Ech ztC2*=-|}m5b(Kz}3Z;}5O}H&_Z8+!5n4}-)N_2%Mn3D2IlUfL7i`r_mvlK~JA)Oro z*#u2>Sg%osOG%T)-8JX1IMXg@A_?i$d^;*2Gora{fJwh2nq%Nbmke4>ey`sfu-XGP zq-3j=a+z+&W~=s9cvixb2lnAyiB`^8GDGGZ&?}wRtaL;EvZZXPz)Dq_wiN?WO~;JT z@hFn@8l*?^CC~___9cRqDABS6L30vF&C*CDJ61aq4&&}FlG9eSu2h*cNe|&m)FXj3 zm<6mU8&xdCRH1fRk61JvTvdxUGc{+tnj+|G*hBja(lM??v)icHjNK4bPa#d07N!VS zhRM_mR#>O^y2zZ@h0#?{2rt+X=?GV%9ZB0=zJN1e4XGQjuG{h$3*9N0@MDQc%uw~E ztOQ|UG`>lDQ3i1(I);3`RU;Z85m~TU>sZ0F1yizViZUUDX{EdR1le_2(m98xl$FZ) zc9er2oHPYI6j}0TYw2RDVKO%TsIAn@xwOVeEr2t0uAwXYywX9w9kCb+7jrR-*=CN? z6_;J>@!E1AOD4z;7%*olsN0dTdi{|$Rh4~^E72tcXL*%;nlB+d1FK{%HKXuEq>_(w zIa@`f70vPmglGITajoe1(&4l@Vy9|`nktMpQA-qNKqe}sCk7LhI$GAy)~-tFu46&T zNBAlXum_9A8=e?QDx4x?wxE%yj` zP*b8AaWa{tyHj;JTNDN7nUq%-F@!4)LYrmy0%|N@SUYQt9t7$(9Xu)A3l!7@(M< zp&ARB^h{bEgM-KN1)Pz3Ihb-sG6YhnYhiaw7w{Kq z(N@ujT0Eo%Px&eir822;(|L{)lZm#}u(hqV>Pl2JnhuvE1`OA$4T-8gpwFTp!y=Sc zd#Gl#C3%6Xf=L|wLHW$E-R5`Noo+V~psMz2%upHoFA1Aa z?EkeN-c*HW?VQU1+{6kRlQmIPhaLHg-e-c*sj$#OF6YMs?^pq)lNng)oGNiVj}KVqbk{zT#0rx#xx8?XV_Uc zdG)zm$C=aGO!15+=mmSWlFs2txGNbXK$V@c3%C+ZJ?YUIlx`GBtD7}@w_ymvKFCxq z5fN)G;59TLOF7hK0<l%PNObMP7EH8)yfBl4RfN52 z>n0odCPP!MPB2x@g#ulWFPsd@++2xv(L`E1xvp001iQ^7Vs|<1jMJzh;C8YKBe6i+ zV6!UYWqrmVn@dvf*Ak|R)@WBH^jJ4(si!k-ibCy0oJwnIWo;*vQ0Wq0ODgY_&29Cd zfIC2G@>L(Ks;6Rh+#D_9F;mcOh!7e~-_#&lwY>|$o}x)+8}7S9VwIgQne+EoPYjMf zKYsW4MdR4`)Oi2c4?sS^3&-MP$}!pKzR}x9caKI#Cr2eC-yiwR$S!bZa5Bga`1J5? z!#jpU!zapp^5cg7)c@W7oBO+c7xcyYPU(}%o|S!8cD~}Titj0IQEXQP6vxZ|BL9y3 zCV6M*iJ?ypwT3)H8wUR{`0c?@3^IeR!DE&ig}%Q@?_BQp%l}TC^zs-7fBVnsfjPe$ z0gmc60dP%G<>Y>y0!mJ$^U=c zzl9Fx=Fxd?#x=X>Pe%CiG9*d|135Czgo{+cdqFm>``1QRMf4wM5w4saj{TQ zr{W@^q7KD}go@e~^X{>0_JB<>?;g9RD598mk6lyLs+f0=T~pMen0Jp|Q`D>w>>hie z{Gqwd65vbx6``UR$RC^&UDNYBEx$^*=tqT$rsP)&6~*OO2o*h7{t=;~N%@C`ik>6C zT&UIpLyr3l-fize~92okB&s@;ii!-Y!(MBmb;$ z(a#7KZOcC`T=X`fqAmHY!bNWpD%zCaEL`*^p`wiZQ$j@>@=ppCrR6sY6{X~#5Gq=i ze_W_&P5$o!o$#lG`v0FQ)c;?33fOO+ENIWk`v3P!Z;*`Aii-zF!G9e5`QOt6mlkKw z9U_DHm1*hv#vk3ju~99Om1LgSc-pag2tMW5jdX(|@p7_C5hRtbWWn#X5Ml=F41uuc|J=>(ha;pF8Cc+K8082p^Eu*asI#gz?<{`m#;MQ4YoNPI6>AL zIq*MPK#tQ54jBAm3#%=BEk&{eezB45_bQdo=GjU%ma2e$u1YsfV?%+y^EcqmLp4b% z$tm=FP(zIx+_;0{Ih@Xs{8-UBT%*&oNF9GYJK1j9je%}N=l8Z*e-52Hq^V?M6~yK? zBwAKWD4cN`jfsuyn9=!c1#d9a9MBRXtR9erMyFF7Ps5Xb!c=AJhT{{o&Q!|KAPPnmZ7o&xWFe%X3pRbsUsZeC0TbMa=R&%YLxrW=Y9fQUJO-<^psjaP zhL|o<()fr9IP~4PgVUi3%ZOyH9T;?$T&C$ecJy7`oK3RR&E*}b7^7X=h=fizhQD{Z zVHKih`ViqUxZL!?Cq-tp=U*^!@RifKe2rBRHq{qPgvisi65g4!`HiflDm5y#5g4#0 zsWzx#l}f4C!>lN8s7Zz@0e}iqDzWM?Z&4cX-)mTum>KUtEl;I|StiR`UbWTMVpf>& zIt`FDYs}R_92d&ttb43Ev)xQa!urXU)o+N`tE~gJyrxR$LuhTHhUju|0M6KIx>g;o z!H`>(b?6Hvy{3?};vHMTZl?;-e45fDR3(-Qx*>z3Suw#-nyyB0DqNwn&8bPRxn*;y zEP##x%VV1kEbpTH>^ZFEU6z!JSzg!A^59;}TgH~yUoBZ&`9x&@BJ1D`63pZ@NU+J; zhmPgJF{+ANt`{1GdTpa%t2sbrR~vO2cDuE#vICbATf*55r66CWVpM7B<{VU3Bl)(m z6!JD4gsIf_O?KU=A>lk=W!IR|m^q}LM4?+Y|y<;kqdEl}CHrUPZaL@~QfP6l`P zUA|o^Ca>Ju&df{J89nIdMtg3TyHT%(*l}BSpR4ax|BajU!FDI#Ldr&GNbTw(v2Yks zxoJ~TgU7mzj?NP#R%|qLzJ$xb0&hx^jYP(cM~yIUB5k2`+hFt;;`)R^)3By+rj=1? z6KV??=;#xiyvB!jt3_SAS~uFN9;kxWe0h_bvSId2(rRkEli^B5#gyyDQgHcwu?VN} zP1whlTDT=nRS9m(hLUvS*fPmcQL{p2I{VHX(;(Xo zPSxj^EY@~p{x@V0?Ni6u0t(KVSVAGofM#^Y97 zph#7l9`6AgVeg-w1uraYPRXX7>B{vu1oe+N&mL~seWnS z(|tGhwfdZWN6LOB`|89~6E{sXCma*+AAf!PE92LUljHEX0>l8kV{F%0aO}9z-;X{v zx_7iZdiLnMMt(f*%!2|Un$x$GJlDT8GS z>GRS%q`Ra+>2Z?ZOCD306-UeeCVyG}$f6JRzJGZd!bNlr{?9+D2b6u=WGie^jjAQ# zZe({(_HB`_AiXe^C;Pn^Uj^B)Fck$WEW2Yh>7~%J+gFobdYB;l>}t|W=ULfjR+H8( z?U7HfCJil>zHK#W?NaGmSCd|t+JfuBTUM3^9?na#f3})*Pb*(tO}eL*KV40_r7H?a zaW(0lasThtq?dMy?1j~&d&d2Tt4a5a`}3=e%~I$ss~~H8dgQ-WlkVw}AFL+b(<9HV zCcTulY~O0qJw5X5YSKNH@cq@Kdo1CZRirgN+Wquu(mmS!)N0Z_+Wp%QW zGw$D9O}b~?A6rejXWYNOnsm>&Kf0Q9&$$2RYSKO9{>W<5J>&kh)ufjWj_j+eN%xHV z!>f$@QsKlZ$f}-ke`q!7o^k)mYSKO9{@`lTJ>&i#t4a5a`(AS1K-0Y#E9vAjPa^6FaNSpo-cYGf~i zk@+Cr55!89rpINjvaSO$C`Trp70iwWacdfnVvVXb%`7(n<@(tiIzUi|J(spP9fK_U z%;yt4$~k$Gwt)6JfvVMsSUmMs6hrV(yOCg)>wt1Ym!+5)Mw2e0ggTR?+rm0Cn@4ek z>b@+F*z(qH(`@Ut(pjdx9Z;@ksYWg4ihtPf5BhCywp3%v>4nZI;vg+#4Y6a}fC~v`JvK4pZ*Pk(_ymWl^uo*KIlcS%1FI+#{EJEDWRH z!`=Qs#92q&wFs83_L+4egXa2Vrq;3(*`B{+Z6S?L!NDk&^Wex_DMPjpTcHoq<|YFz zvX*1WZ5x@x623lIkL6C9qy9+OX=dI!aQ+C*^~grSV+psTwE|jC;Q2ChU>pR^^#~G+ zAQ;|_Rqe^F)j`-8rE;zunQOGmfgx6P`|7y}iX$H8oy{>LbEWQ}j^N%xG*@-x`uM=k zFvk^-%tgC+jw}*&Gt%-PQHwRmkUM8&&Rgyv39?9{$S~aqyGiC~cN7VKpjWLIeP%a9EDcK~2UbTR8bLe>By3CP9Hj_DjPr$& zT&GiQcftdg9l>J08tKIua$LzsuGuYz%Vn{Gi z_xJr4Z_nWwCaNg2yB|6#)f_dulWl9m70LUYd9>SO25}iQ*X?@-^|mXS$3h8fvX`$i zJBk>Z>vj+V4R$kmH%4Y79%qwz{^UG7l54r`K5UrFqj@Zc)Vn>_;68377q+w_`C;0T z^)+gEkFXb*?%g?Z59xTESOm@48`c2oce2jghe5GUHC-Xy9vANS#hpYbZ(-g5w_vFq zZ?zO@M2fab#Y_^}P?dSY;~q0IXCne$!sGV_@_~*$9_le$_B?2gAQOH+7Eii5!Qimw z%i05sAh;Z8jwoOm(wy~2kVA-e}DSO?%UfjpLCLb^o3&dm2 zM!8ujm*c_apocOCnF!nk{C<)`5^3&uxxl+C$KT*C-ew6%B`9X4-TqvKFy(Iga>^a%*WxtX=EqfBISNOK< z>#~PsUy?l_yI1xp*(X3w!w<-Amc2)|NA@n++hteEE|Xm>>&x1*nyer@UzU_bWT?y~ zv&c@D>19gUDYALlF|xyC2g#%|zKjF*M|@8DJLxZ^Pf4GUep~vO^dafzrJt34N_soU zkNAG+P0~HmYo%99uaI6Uy--R@o6?FjCp}M!NkdYv)FC}fdYV)#U6P(G-7Y;!dWiG@ z>73*xsTl0I_^jkNl4m4OO1>|7T=IzI%aR8q_ek!Pd<{%O#CD9 zcg5cjKP>(tcq4PS_zv-HAZO!y#W#qr6Te-2rT8-O#p1rWEv|_R;`7BxaYT%YU1E#) zbg^En6rUoV7at=&Oni`7D&~th;4RK`qTh*rA$m&mgy`F%$3zc_J}>&L=u@KGMIQz^ z9&Zxu5nU^~T66_?!*ijC6g5Q^QBHK82or_CPR@4iJe& zbHbN|&x3s?e<1c%1ME;lV<=P$--cyeRk!*tzmof}aU~B>1l2 z8-j-gUliOgxLa_C;5NanAp7JEg6jlt7hEa0OmMNFFK7#Df`Z_DK~fMApaPe`A~;>3 z7bpd%2<8RH2o4h*B#;XD0uKKL{&Qe2%wO=I;y=OvHvcjHL;TP4Kg<6V|91X|`R@mL zEcfuQlQ}a&coya?mcLeWXo}4G-&CIn+hs)4N<*s6j53pK!-<;-vof;~4f1)&ze4^6^3RaZLH-Hy zS;#*^{sHp$kiUccE#z+?e+~I7$X`PK0`ljO&pB}$R9#J z0r>;S??ZkM^1G1Vf&4b)w;&&f{3hf#Aioaz804dnk3fD6@?prYLOulf7053`ehKo6 zkY9lOJmdqApM!i5@;=DVLf#8`59DVc?}q#|Q#Uizr-3;Q|Un3Ihs#3Ox!Wg)W5-1%g7GLW@F^0#2bpp-!Pjp-Q1b zp-iDfp-7=XAx|MkAxj}cAx+_Y3MmTbQ8<^vITX&Okfe~HfKiB1h*OAA2vZ1A2vP`8 z@KZo3_$YWOcqkwg+!S0CoD}R792Be+EELQX&Z2N8g`E`6pl~{c9TZNZV4`58V4$F< zprfFrprN3qprW9puuNfz!XkxJDJ)Ppg~A&soJ`>)3MW!Hfx@rQQIJxQP!LlPQ4mrP zP~cPGQJAAJOJQbqmLr1s|7MQnNUsq7gLg6bW9#>Sr3PkKHjDJ~bNA#%nOctL&Zi<8 zGL}GX*;?1$NF?gsJlLm6ik>+MwC0L#%kI5Y^fGPORsQE~pC8bS0738tv99*jH~DvRR9RS~2M}s-j^c zop*JzNT64U<3@X=P_p%xGoC=uHd)3!Ds(F2-bH(|)Bw}GMZ=w+qLWNA!0>LTR)%RS z3+Wn>r@KN5Z=^E$Qo2@1;RPyZ*4{O-TD4ZU|AD6l18-elN~O%GA5EneHS3pGzHDN{ zj5@l2W!c&EfsISsO@yqIz&q!1tn93n!sb#W7){#-XfkalDy`}$qYg|UNvC!^Ym*Y! z5(H66pEgPd^s;N~a~(}Ig7#)3qEwgm<^yC6^C`9yhr@j2=f>$MwJI%@jdOjr6VQYGCyTM3-<@n_^x944w zGqrMdpK+&sZO5I;WY};1zd*AU)98#@S6r=*`wIbOJTSD{{l%!K6gHwoGG@%Hlxn}< zP9<3eZtnLgM`dyX;cb-jc@Wcev0>8I1sR?z30*y5@u|{{jJ-K%RGZF)Jdp?6oq`OJ zo#lQ*=WC~tx+**9RxDcQvTx{jgv%~fKTf8}LLrFd2QhH38a7)d%REbkPUZg-*f?>{ zQyjtJ;G5=yqOXW<6cO@^fb-PMGoqux*V``%ZxFVG4&jl4Kg_*u_7}7F&R#QH1R4BJ6MR9iNAW>0 zUmzkz#K(%Bn^ACo#{D$+YHpSbHaF&9&#&_>{6oRl&(HC$<5hWQ@m@dknVGjs-m7?n z{3ZD}<+sjVJ6D$UBwooj@ICvh;+w?X%^qCWA0KVl5RI7qAzK^myTK-)PRqdDWLz3R z62miT(Sa9tWA$Oo(HmszmS~RXJ)oVHxr$CY38L#MgKnY_en*cpLu># z(Lc}pc~a45XP%u@^tUs=rA5=_jxASp+Oj<|<^Ob2(Wm%NjYMPCuocbM zTjeT}EQdP%_P$e+)TY^kXAh=D&30SShQwO+K9(GqYhl*3qzAYU&<|W&MQzlSxINcTl{2xs! z`a}K?Y0*t9>y7+3jznYrlFwZB7CeK9Kbfg}oXnYZU}i&OQqelEPCKw^!OpHdds!VMepU(Tdr*`dJmW0#A|cW&v5BY zyfzoTn@exvwYli0x%4Jpn~Q#mOK;*8b;caskaG|S)-4$)VYe|SsEQOK1>FZW6_tzR zlZwhjGR3zzvGrG+SLbiqfm%0^@H(-MyWz1#Dxo3s{3`vpl-`*ux~b^Lr1Z{Qn_mrG zQWx#Oren(~bxtbkkUAz6wM*@jirS>MNky$v>!hL(PAR?X-R7d7 zl+wH2#iHd*JL0ZpLzzMwFJ^+w1$B~RCC83@-TboA;dLezC3wW7qHSJ#QqdN#HK}Nm z*PK)o=i#&{nroFhxUD+y;lZ3Q((y6wU**4+|9T`E-Lw;aQcgF)=Aw7V>5T_B7yX2s z-gt0x(c9(p#)F%Seq6qGdfPOtkIC0gZ<~sKR8DU^7~S;V zUqp*;dVbw0-8re~8PYQ*72P4-L5psBb~!|J$fTmL7rlN`(St?w9sK5J!q^!$27X<)|xlHz2=cEwR(zW)IVv0@H9y+1F17VI7H zjQmM3*Z*<(Bl0iH9{@A`@05Q`{z3W8V4nZ=@@wQ*$u9@9{D&a(Kto;vbNtVd$K(My z0%rK{lpE!0`Ke%j|8epo;V0QnE>_ypMWPbp2`+p|;k?gy&Z-ANoUzFW1yIXb# znAd-+?7gxZWY_%fdBm|Mg&A|En^qf5Ps*CAL)qTQ#s%1OIDkfSNTr z(}N@-yO1470WDT+kS%EA=mLQ9e1;{*P4l)awflNc551E2I5As~d zb0E)#OhP6gF~~S%3^EECfeb^2AcK$rNIxVB>4Wq_dLR)3;F3F&~eL)sv%kQPWY z0dUk`aO~4f1)& zze4^6^3RaZLH-HyS;#*^{sHp$kiUccE#z+?e+~I7$X`PK0`ljO&pB}$R9#J0r>;S??ZkM^1G1Vf&4b)w;&&f{3hf#Aioaz804dnk3fD6@*&8t zKzW%0P=H?_e0(X`B}(&A@70w4CLLApN9Mts*7RdKQ{wL(kkne+hFXVqf-URs`$QvR59r6aq zRmeS%?}oe{ayR64kne)L7V@2t*Fe4l^6ijsgS;B@t&mqiz6J71$Tve?0r@7#%OO`F zFN53#c`4+-LB0|463B}oFM_-f@&d>qWDT+k zS%EA=mLQ9e1;{*P4l)awflNc551E2I5As~db0E)#OhP6gF~~S%3^EECh73UlAp?+p zNEFfs>4o$_B9LxK7o-!?0cnS{L0Ta#kY>oUAkT!{33&$O>5w}hPlGf;8X*mkdPp6l z7E%MLhEzc+A(tVSAQvG|g%AdXs|loDgB8wC3#Fz5WgV4R(z0X zm*{ZeM}$uB6<^PPm|x(12YjRF&b@6;Hv5*@6K1ZPIfZ*a7X>E%>Ay>M2l+F>lk9nk zcbhHjur#Z!LW8sfGfvzW^)TL-(wn4@=2Y&TT&h*BlY{dU=Ruw&kGWz_stT!oI@WhJ zBH>nWIPexQqM=Ucu}W9pFvrQBrenl+2Y?4>%JLG=#2&Ecpv_~gKnC%dqo_)yt?Lp# z`;hFQg=Cb7gUKOYcPeS`HRIKYrA_vVyZxZVWl$nAafvTax*_IOolaP_NZNAcmabUK zXFW*3;KvLVb>AA!XwYV9u^q1{gGP5QSl*3-k~E+s_rxVN@gCS`y#}_2uMzXwI9XEn zd;NITs?+qcC8Aa^xkAQds_F6tqlDMft{IZ`Sil$2{+ndCk7~+u^Agv@h0#s9pQ(V% z?Lg+Ei&^_19>yyAwn}Hz=g^8ih86NeuIP;9k_NvYZ(#b$Fj^a$v%9^NHz0K9#NLcH zMxNKEsvd1jn@Zsxr=N(H^2s*V^mgr-IgI=J>T)|4b_7z@R%Vg7WVeSZ1$1A>wxlP3 zkZo(y<#Pq>RU%PTtknCw4&Ba=z)XOC7%3(Mf@l=!TOxK|C2Z~**E?^C6@!f9V z0X2YZ6ML}107`qXbl2RA_lRt>5w@UKYp`WfCv$;xH@29`8Jbp2C#>-oa-cSLyFiK5 z0J2V8;<^D8>{(reDr426A-gl@4rFphM=|ATH;3Uo8ZNbolFLDw27_9V>Rzy((!fYxD`7rrD48T**4_w7ETjXv`7Qh5EQd>2kW0)o6CNo$>~Re%8d^ z&;ux3cb5xQPrwjT=EBxwztY!5s1 zrpe=crJV_`|L0!JkuD2=z&jTFvGx1^SOaf5dQXX3|9kjVGVZ(;GuZq#pE;atYpgnh zt%>&RgJIof&Rep{g4UQb+bT#~lZ@u>qA)!Sf{upgzNLCxO0c{Q``oQ>^J>HZlLl6^G&0X z7=#h0ns8(7q}!>^MXcposp5Cq6EUP_DZ+r(Eit5425q>kFK4dB30&=N=8;&f7{vy; znn$NzP!nb&W?HDat9nu!K{E-z)()2!jT()h)=5p1ih=Tf-8#uJ=Rr!6u}*VuM*+vC zUY2Rx!6c|x^1uujanwfT#>$h=@eWayo?y4pm$cR2yU%wuylSI={n2a zq{>|~*ur_6rms>ZTY+ZWAM*!V5p#PX5!87i!(>vaBQ1U`qAd@*M5VA`=`~#~7hy?g zi`ETRD^lx&jeCl4eegxxc|#|O*_?@fG}>{SZJM~w*Tiya1atZG22D#;EN}B~Fnm1P-ui&ls<2KV3IG_VezL`D{-4%oyYA`!jo?@PA|QZZwME zibCMS*psuyNG)34@Z6*G*Bo&ts%dKnU^9V|EmkJ7$$YP^Z47D_RXn3CS_v{~2_?K> zE#bHUI%a8OY1!;)G)ZT$WHB~cIMzd4p;$og?qn^AW>I6%gi1P1XMncrWnCBTn7fYJ zvO61%Hcjb>ua`_DB4(ni)-F^V_M|&s@w@yuxzQs24VDxeKxRi{bemj2^;$i({FHS89cU6BkdD2NEVj%=3(`;2RMFPrqSE0{=v9Sa zB5DejdX=osuQUIzzkqJf;JE>H)hm@@U)-=m*E#{MuZlz#a=qoYMn&e;WI3h`yCTh6 zDG=3$D&epvuP=p%3v`FhH^7Y!Wp(eDpN$ilOnud3oSN!+f=Czh)EQhSrlMx*-W z3?JjuwLK^rT&}Dw9n9$PR;=WU8#RGs-0u(9kzw9!G}sC{C28zBDmXzJ0_wIU zP;v!Sy0pozUe;vnQ8#HQmy9{mnl-fw<)CeYRm9Zw(6=W)x?!zz=cnkVuWV3Fd&8>T zWW4FXfZJ%iO?|&P<^ACQ$s62-T-<3eSmNb)8Y{-t8nTZ>6N5fUVhyaetTfm%+Myvc zC}WPq|K=N9DrMHR16nn#)x*k2Bc}G|oC`Xv7gdr@Z<`E9T(%bORtAz*vVmpO{l2oH zwATub66(;XJ*AKh3s~zu)T{|R;<~Im)J;3oP37{1bhp+4w>ngUAuti6(HR+}zkfE;^*lJJqmI)5v`^g7+}>mCZ|y)C z6u4I+OF3H3rfaEmt(>OHT5mf|raNis#!Cl%9Qng=a+FnRuj5cZlcwHQ!9T0zY<2DL z(Sz06|CJkLxkH5mjj!ErX@GO=N3vHdcauhC((E^P6KOPBdMMcBYN2I7mCH3#(i-zb zJV9I^SLHnIY@?3^TQ#sxjs}0l4_Y{x%v!Xq1;p2CcbdKqKGb%cJySG5w2dZHJ1{U5 z3jxwq_9FFY%;pPtv+ytSw;+ZZle+GWE=7Yp`-77U$w z&?5I#VeX~@cYca7_v)Q)Hq6~L&FSXcwJ!4V06Ba6{Imubr;pKhkblyNV^la%DmTGs z*iKDypd87SiDoT5pvV07{hd-M(a}Inb=x&ypwX?>s8hvE6*Pqtviawg+kj{R*@OfukH-h!D2mFt9ZeTtyaTc??)}BjLy->cbd9%V>y}* zI2W`vum#m1Y^&p*lDcI^7hHxqp;5Pu9a5XAxF#KWAapSDR?2%S)W~~tiaSkrebq*u z&cuE_xxrYX{)sUx0{a^oj5#{+oV_+ERKUh- zc$rZuwbY};hMnC?wv>5eplBXgVoHt5=PQNsB@I$5Iips;wSaljiEPq`X`LS86<=ob zHoGU|jhR)+P_TrQdlt7bNa#wAkk$|nTH3j`Gq+48vaxDfZ;mV*O%Y8^gDkfP*_x|t ztOm^qM^0ZdSi>rn(^Md0oq`AHS|&=CtD!=tE;GSBW%xdCkvnfuR_aB(f#wQkT_u3z zhL&75Sx-j6FBbJ@T&Rj3Q##w*A_{tK^ zzWnNF-JRqnUvM_;9}C)WBe-m_m$D5nktrBjnTnz8On4WfPNG&S)%9v~v>9>vLZ}w@ zkM*~pjMs1f>t1EBo*Jh33=1DS#=V3$>Q&TB_-UX2>#=gu0ZL*<{rLX1e>MxrWtN#`|qY){Se7gBD&<>dIYxu>>Yas`c%` zfV6F#0GE3H4{$!pQ9J_n_Nys&D8%ybfnE4|a)bep+~iFfKe%@Mpp21@9DO z1S+s++N1m%L6*PM`4X_V*#~%io|AXT+|ytmvR!kbxoxw5n7wcI>e+K=Po4Su%tJHp zo+-^3X87E1ac|}lTnqO=&Xd5w(Qj#`ICrjSX5+gHgHFddLCYUnS}Dw(E8MSwhN*Dx z(n@|@32LUogG(#9aSAG?0w^InPGS851nMqGe$UcMW=ss)^$ED4!Y7wj(&H4E&x~@Q zaQ+yVw9M~cltOBpf}R=l9ZM_cjdO|BaJMe4oI9=rEwhB%mR8Ogr@(4yP{P^c6j&{N z%hF16jDm(0^i4}EiE#?7p!Y1TVB-{6LBD%xB|c7p74$ooR$}86SV3R2v=SYs!0O&t zEv-bxDX_ZtTb5SBV-(b^?tR(PN@$z{t9$QSS_zI*U^U#eODlnK3as&P@zRQaoC2$* z$)y!^oC2$*yGtv+aSE)K#+O#S;}lpe-B?=jj8kB>bZKb?8K=N%>EhCgdyImL)o{(F z71uZgR@cZZtvJUiuo^D8wBi`2!0H-_r4{=)1y)OkmsV`!6j&`CT3WG=Q((2UcWK2k zPJz|Zo~0G@I0aTqJC;_?8mGW&>B!Q`nPX1(N>;;}mR5F-DM88V8atO(&KOq$tEDwd zE2obufz{H$r5)o+V72t}(#mP$N?^6Lc4@^lt^`&~zhP;`IIaX%O9Ph-;}lpe4NB0D zQ((39$xAD`{l}m;vD)Ftr4{Y|OVFEGEq(mbie_91tcC+6sK+U=8V)F^#woDc0VpWP zDX`iBC@hZ)n$-?RFRd(%Q((2jAxkTZV_Y(_Mx0`4<3*|(QgwvSU_wzTZKODo&PDKJ}F_SL18ou8K=N(Y1y4iD@Tu0V6`-Oq&aGw0;{FL1ILl$6j&_{o*#}Fr@(6I_bja( zK1M;$YU%5jR}Pyym%E>*UA>;!(la1W9(baq%&tuYsN27r;L4Zx$K_KL>lL+rf_L?*sdrbHQ%q zDV}`pUa*t+k+ToY4rWiD`OeJcGdhs(|D9Y5=Z~PEDSzm$v*v=rbGLPCnc%RW3t+=8 ziQ=tRG0J!V+|MfkJ+rf-yUv6Ln-MZ+>r{(4Nw{<7T;E!s&>*wtpu2WLgJj?Cizgz{ z+>rDRVo7v}OlXkVv(Q~(Yk zqq_{y;HDXH_tUYp%#$j*OAieu(%x=xSVufzUnK0Xw<`Y0Jk+uqMF$OL!A2uJZ@Ah| z+O3F>tW=T{8f4d@78=Yu?e>lXi(=iR&0VkNTeZmyvfi+uyEM>X(%%eK$Y`k=PaqZt zlFUySvW6Y98XB|&(1E8_?`4}#uwjVPk#tVzAvX%FHbTC&812HE{^2^!3l zE$(cq;mp@uq_@$m`nwYvWH-tpG&n#D*0jYJ$Pc^iUKI(Mu?Y>b8|74J(BerV1nxjf zWpBimK(jUGCLjA5dTMr~EI@bb#c zlj^|X@)yaHx0$Q@&69*lcB*#*6QuL zEzx$z(JGc~#q?l857~`!5;Qo7m)wMxD55cEwqf_SZ4-8E6}wSRga&(7!dCHEI^Afd zT~C7kJz*=e-Z`MVPJjl(P0T*%40@$V#$4zOVwUz~AuHL9G7k-wJB5lDE7{Zj5aO+7 z3b6@8RKqp(B?Bt*~ zY8a8AyN-kgYsGw}lkL?8JwMn{t7k7xc5=`g+5PYcXfP0U_s!|DwUKG1W0`c%IoUNu zZ)CUf;n1KTDc8cSMv(LtaV*>lrQ?iH_E(4ORz3_G47Yo^j&0CEY*9-r>F?$plNn?^ zwxhca9fcelL`YAj)u@EAf-`RRO<0Ep=7s*yU5CtRgtBd_^{oXbD;&D(^`j%}s~sLe zcO49!Xp}NhH<(Xe>SEPUtB|*)Cya{uAb;qt*Nuu=-ypOT2aS%buUUBHkX;APset3_ zA9rc*4j9#WBjm@+HcPsRq8(q|(}+yy-TI=aFQ@1(1vJ=oHp3Y_Q7!gksK15gqm$kK z=~++Y=q@=lxM?=J{d6@FPd@W;8(C zKp$E^D_-*9oxq!OXC(T4z1h#SLFVfd$%hUB>cF;+Rv%41t8aDjQVGEp)Z0Xpy~1(j@Pvt3^p4+NWCnldhKKeZARFwJ>joB{$R6 zvex``wY*3dnck?#=jjSqJIL%8TGrK0@&X;*n##K9{!V+ac8K=iZ?p&JZ{)#qvpb`??ZMh1+JisSg{1ba*_XS<24Vh*Ryl8Sm1pU$aW1X0LD%>r?fN;i z$U4{mK-bdQ^r3ya#_#E_k=)mtjk|_{d103Pj;@vjtxi|VZ|Nejjf(swT>)zcSq;Ow z2a)`mj&4n5U39;pJy<(Ld+;l|kodlF?QIx#A-|wiVw0=XPL)&=kc9l+WlI)Lxd0iYWN@J%{^wS!Xz@NGH(hWfYwzC{PHrm`-8@6Z9P z9ijtxoG!#i2f!M>>}LK3t>T?r;-uB-YI&HB(Xp@9adrPyTE#xO%0qOu*l3jvs^u$mwODD9b=C4^x>_vs zp?#|5OLVoE_w{DdYWX5vEoaf{bhUheF7nKcioBmLa_!*w?#{YhdyuX_hWg7@&dwYm zd4R5*HI;Rh^LaXewL^3OpQ8iVxlsV0p#xYuIAs8zr2}B7j|<>lI)F8mbphN*2e5XC z4&WX-fHO7<;7&S#wS!Xz@M$^#hWfYwK1Bzxrm`-8yXgSd4$%SJMF(*DMge@B4q)xz zlmXm92f$Dt7r-az0M=C21@K8afVD$(0JqbH?4SdneH>TF$EffBD$X4o#Rn82`QO2w zd_J(d-h04z|36CiNUf6JNZuvcDgL>5kJu;Xh&~}Ih)x#%RCt}xEqGDzQ82fEp8o^> zHT)?5P~P`=t32D>Z|AO?J9G9Iv+tM<&%S=U)x3kYx*WE&Jl=pY+g}_iLybW=8j3cH^^iTX>V{??o4{Mbme+gS4#b!w zoTW^vHE4&kEqxsA_YHkx)nZ1}hGr_>$AYdzWYq=DK01N78BL|1h*>)6n65#_%(1jP zrtP6Uq?pL)ZF-kU9m?keb~S3#79*=pX!el_ydAjWMtdjLGX+v@d#Twf8x|9IAH3cJ zt2tAIwx3UVI%v8R4R@=NRR=WtwF$iKINd$e)+D@%_F}&g=?uDvi^SDt-LN}IyT}aY zuGLi$bF0;I?^w0Z&1izMuWso+MxD>+f=L#cq7+!0XFiSq?stn76tXGxKYV|3LD>?LbivCbB=G8fLEq7$~OlbB~6PWdZob%u% zaL8FnDr2O}gX8gty+Rtpfu=9x%*R4HZAFVVJVP5WyAztdYXY;)T4=Fb#)`U@r&Ke7 zCFU(lURAE_>NQQ?VW*U+zZ+(2q2kih0QFM>|$e zhuWVUrwzUZY1ENI*;Qe@G2gMe18U#4ZJ+O|@M-Ghvh`Z6M&&kgH=_#5 zZehQ(->VE9S601&Cdw$6YEJExg%#{dv81Z!%NXSaYlzp`rIoVo0;s5Ua9x ztQvqJjqJur46)omR`ozTDEs#bwAaopAW@at*iNQfWk%&7t2$`xhG~rHv}!}g-ig~- z#NLi*TD6|Ym-_*hF%2}fXBuM~ty*U;4Fb$h^2n+h8hiIN#?+v9)~i(q^V_s# zRRxThW!G=p^t;Y{4X=$-imZzccKRKwN?=4I+dY91J*x*HtIIH+>n6}%>jPS&R;>;f zYp&orU#J%#$m$X__O5A+8MJCut{zD;zi%R|i_qA$(-;H2Q&|tROU$zdvU(~s_Ri^y zsZ2w+pEa8YSzUm}u9?P|POCDSEy*f#;!R}r6ksebd&lPc_sj=Nl}K*Taa1ary3Uca zm$N=k&K}h^;{ATL>C$%EnrOt_Zv>s-mhlbH?As?W>#LQM(HK?@mpi!>k<4o1PQQDx z6V_E#WwgJDnUt72nO6tF8vDrV$0~YI^u&`pxM)A3XOiG-LRB; zNsHGva1?z+Wc5U7_N^0`onmLJ$m$8u>{Sz(onjlS$m%>a`<4mJPO)QEWc7Gx_R0y& zPONG(BCE$ivu~Qf>=auxMOKf6W-p(>>=gSk zMOKf2W>+RKJ4F_?$m-G1>}3;}og!OOWc4U$cGm=Er^s9+=HeU+=TVOQLHP}{K6yv( zlb;}nbC2aZWiN3am3?RS1ldQ0&Y2b2n`9}OQFegvSm`gN4@mb2ZjloFA9MaH^+=EB zzDx3V$>TF05j;90lYB(7A~_Fa2T*{w2KS4v7YM{Hv0J>IUlhF{`Udx2(FeIt^Y0Q} zDmq(miAXDw&*;D`fcyAw6Ydt`n@qakUVYMRaAq6XaA1w9HC%{fh%oADcZzG*NAQ9H zN^E+W-_#P(IgPr~a<7E;I%zYCHJp}YqhoI8L)|v>4ugC7NG_Ymc3gpS+A}0P#Wq%B zu2JBw%$&c_Q+!TnqYb6*;moM zEmK=H@ISl;;K&%53q*L(Y9et*qlyI`2=l!Gw+Q8itsqixSVOh2-4^iFf&u1~NA7te zdl`((2l0L&R;n~TE_0PRO^+Lea%9q3!R%NNx2EwZb?sozUgY|rTtAyb2MFr0=h7CZ zV~}Od6z6&cf1Tot)(KRtPQ>D=x1tz=huVz<^GskL#p|*ZGs9@oMU+ryl5|_bOu4g0 zafIr=ERNXn)^5{m>$TF%ryTBSP_AdGMlIl__ORg}^xIyre=XCVPX4PY;vg+#4Y6a< zaL^M?)Co_PIh~xVgynWK^++!0Fqhh;WYrn<+WO3LPZ`DAwYKs&?r#+0)kHg(>oZq1 za3v!-0t=MffngrQyks{$95SDTInRyc@JP^Ha`x&rThwm#bTZ6-#rf-rvqA2*VaZA) zvguwv2D+XX?OBEl{{$z@21VP3sMXy`qM2qj>FIV0p^}v$HZq17mo#++ZzJz^wh9%5P4>+*G54u964@3SuA z$G|Z9J>2aNM4WZRU5jApYM)srGH9+(W@;@vk?r|ARog;bXgPm?=6YnK;IV|;(OLnmC-8ik+2Id@ z=6VE)MGy?{#;W#Y*6JXbFT^-kj?6XM<-icDx_$Lr1jP{#^FHpFk-1WLP)BetwT{7& z>*E7E!#l2MWG>ppb7YaIn~|0WiCV18ZhX$joVVOT5@eA?kzu+Kc9YCe#(Ce!oV(=9 z6oSJ<2Ej9dTyMa9N4Pu^GY>Mj-%%v|fnK#<^qJiZ@6xbTa$t27q7lTC02@xEbB+>o z?lkA~Be_ne+U|r0E<1w7d^OU`eBaC!kK~%&GU*Q5?4?1sxbnMb=lM%{5PgXX$@&!FCR zCG%J)VNLe(Rc1#S)k(L55NNQQ$-6N!8}T@s%=04WtFYK^%Wd~z!(1NCV>zVW?Xd>; zu_HO~O)!!lrX5*dqlWhgJ98xo=gyIPNXO&EB52Ouum-@A6W00oFeuikrYoe|nt(GELY~$ZKqmhkDGG9bLQ$ zGU4}Q@uaI03=V6)teyEnnez{5jwoOm(wy~2k=HnZ6tT=`G4#Ae>60fQxz zJ^yb#|F6C6*!upzpRqN1>$>&*e?Qf;_0L;1ut^PUJ^ycg{|EEww!Z&wegCIsV)VDZ z|F3(j+xq^$_5GiE*|GKgfBh@Kt?&Pv+*7?OhpGI33xYk={(m{Wmh<_u#TYQB$CY~2PE4p8> zM|8F59MLJl=Y?MsUMqN5n0ZxP+%mFN16wt)RRdc!uvG*9hiX7Io1d8<=5(fDpf%|G z{4Qf@7$Qy9W)g|!oQ*|&E1&Jq9-dQM`e%} zy9ISS+d3Rk_6^;rF>XmOy81=5SFbpR!)${LGzw~W%j=#Ul`(84`YO`iws;yUwa%N4 z)qRB!mH!UK7ewRvFHx;V(dMKW;HQRbDR_kZds+c9> zPuekkc4&_l0?DLXe8otkmQ9sVbE^}qsdc4d2#dx|W-{EentTo?svT5nah(ARrZY{S z7$0dAlI@_e9V}bARd3TZ^yj>lYDn2II3t)h3i7chHMwNpVZ{38*^iAh^8KU**OKOL z#X4wrRq={$FPlQ!ouMPowNqtNHIF3Rwoc7#sMk_SmnM_4 z5YcX^6RPIoO}nO6bhI=0%y3jjb}%SLogH;3W^D|yxS?KDhKK#KD_<}x4XF;A$P{&X zYl+Ye=Qi+5%>8~r*>b~2j3HtMS|Dc}iVTFjH?W|fCty+H%&m#_-cJ^DeM!a7@ z`#q;unQ~^TWu3a~LmeTDqoXOPE&X9It~L$IR%feZp4&Onzy<{9lC@5tnf%$BWWvn3^sqpsj}2@BHPJ^Qt^7i@2JMAcrA|< z>#a%`>?Ln-^KYOvj1DwyN#%6pKvT^InpvOA*Bf;4DBiCG30!4Iz?5}s+m2~xelXIA z#kvJwsa>~M^;XQR&Xv^tRw|1Qt^Sl(RVua1c2BfMBn|Z__r{S%q-pI&>?y3Ai@V~( zbf^thkwk5ny_-VX0W2_35p93R7wgxGbN7rif<)R4W^u<79ZN!;RtAR!Rca6@lp`%e zUS&Xi%|hS?uP3us0~dc)aZTklV0EKPGwS4Id^wB0ZIT>W?v^{bKI z?2{u6Ulq#^wSE0- zTk|zSNZ;bBn&*&_h6P!)*0X+B&f|+C&X9g!P9%|@7LS+=&VJ4mFLgok3%ko9wx|T0M!81~dOuTEn6?SS^LBFQJZ9bR|4*%sVl6rPI-RD&BNGm@A^i#YU-^ zvGW#38aj1AT`3_2jiU}?$doK$P}xRZrRl;3sfeqgB)uXhoa`uc4pL=R8?MENL-{o`xEOVL28gqiu7}Sx|bcWZqyiDqB5cp`|UL zS#u$;#+v$&H(d(3XM(hbo74v!YLG?Dmon>Y?NHf1)T`~@Qdb)uXopc-1#Ok;X>T;h z`PxW>On4o3HQ2bv*a{osV3|1?iYVp%}8UHY8#i_+_*74A*kHrFPQC66^?Y znIy&&%{?K!TXM4a1@Xhew+hb}YJ_~j_XHmnTp|bwPT)V!|0@4Rev5D8A1eES>`Ssw zfJ}sMk@aL*nP0X;c9QIM(tk*QE&Y!4LFq@OtI{`t{RUDX|A0Yyyi@@)5d2*7P04+d z4@q`Qc1du_*%GHjBRN(g5&u>EQ}LtX&xmgkzf*jPxGIi|t>R_zkz#@9Pof`-zACy) z^ghwsL_JYTetW2Cj~(?P-P#qlxH8pA%e)h0=F zoHQ$edT@-iabMdYkMe(vk!Ec?A-Q3kG|TqxanhQ7q6D5?GftXSgKr%p&D#4=a>Y1l zmfyR^Ni+A&kz6=Vn$=H+J$pZ+n6)1e=d1e{W^K*Gd2oy{vqCucjuB?|5zd`sgqhXB z`MB`soKq)#A|6w9DyGkp1NZL+V}owBiF^1MVWwrSVvI1ecW@6FBh0Ki?#vkBeX9=m zEEprq>`0uyj}hK?Bm&EvF~ZD&$a!vzFmoJo{Ono*E;(Z=)!coF~Qzvl``lV}$o@6kz$AVDabEla7=vxqs0b*#9Zcs9|;q$zxMV zKQvC7Ijkk0-@mlM#2kH+`^HH#N0j95anj6DBKhPvX_ns~n@ajaW29LpB+2{7NNZS8 z`o~E#PaG1@IB8a$xW-Ad`iX6vG^+;9Ni(k(lBIFdtcE{zoHVQ9PaY@DYWNezN$*?V;GtyOIB8bHA2Uvx z)$m7*X?Uh-&PT_XRN~6-M5p{%HoX|pzLU&00^&xPiOZcU^tg=c1pmC3j7V@COYX3 z6)LO=_&=#?en6E&6@$0S#WF0lw$CEYy~k2Sz73p9!RlN~aIMi9wR)gl0vVq2R6Sp? zohYT%Iz43%wv%tldtzOq7d_ zbf?><4KW;{Z0Est20C!QOsD)gGftHeAqcxhmDL!nVSUXPbGmg_y?Y_w%^)$WD%xl| zT?S_bRcSkQy;tR3NawVPWh-G^=r|EfKg7#v)3Bx;G~#%$gtiCmS}p3T+lt#SqWtML zD39_~UjyB#nBO4ui!PeI{GvU$FdzIk&093w`6&|ZZk_}7g6(*>Q!B$xRY=!}JXJ@a zm!F-|yG;`wfHNGRTnyldI?Z>hRWHnoWq5yvcp2yUUs>g^^VU|^Tzb9NGpJ~jbL(t zt;ZghU;w^}x2Hi}ZLT_$J5AF}GX5sJ+GyBUo7cXLr${>u+nCjhr|l$+fyv%Yj_P_- z5BLB6RdWMJK>q_*A+QMq?7C~`0^QnJer>LV;I@OhAKCc)YStI^6`MV$tb}u2wc42; z*tqExOY#vf6@Sa*#YXj-|S{;WzF_p#B1op)Ej#lbx7FcG z24C^GvtoTy2MOhJ(R4c4s?|C;hE+8cM^V!$bxK8dv{*IxDwTRK7qTD)Z`jl`#AB|Q zQkgB}9eJW%0u$Z}p|X=y1tPJm-`tO9OqR*+K&jBdxD#__oI^No<|rb`%KLU8?S`ZX0ZDV`IC=1wP7h<(;Wv#&$|d-6<(`q>k}ePGzRfR4SD^QqnLU z2HNcSz!%hXGYwuF*ne$~9j>+EU-)g7djW$v&9!SUV}3BZEQk5I;eRQqB$aOUVK;oE znNRgvneRosh!^pWhdSDpE(@ zf*5b|F{~*eOd=LieQCd^;wUu?hmujS3aTjigf|0sZKC(8S8@BaQde)aXjjvS(fP?6 zOb>p0>)ST2-IdO*(ddW{tSz5GnN%{x(K(w9AwjydT9uHk?oui!*K6UvT)FAXT3~!SP(KeAVtx`|4F!uuu%doT;Fw zOeSBt^w|*(kz&oA^e~xDN;fKX3ez|?70OZ&(~#w^t!a~s_B!hI`?Ts2h1v|++V**% z2xq(LvqtSsTay(1G@Omc%dpIHKCV-~S}PSxoIS@Hbq5%@d4ISv=@M}D!tZRp zYYnnJ`|4e!z-T;31YMI${+umo^*IV21or6>zF2gMXw;LZN_>%wm&;PaS*#v)m{>cd z+nYwlR>R1w?$0t!Mo{Hyns1;usFH}gzTlO*>dt?xPCrGo@!K*S%vr#(= zGmJH1BLpkrs^DIvrq-$$Wk;}f>gv)9Y4<-rhC{iWVQMAa=AoTgJQb|@XwBxzbp64q zqmfC9js{UjAiV3Nj*?tis)iUoD_7%9s#Al@V%+eA>Iq*m2id~7*A)?bc!$f9(A8j0 z2s3VfNsT+>F`?D1PF`cYofzRDP`JyJ_7#zoL(ckD>sQ^yXxmr!rwbhKv6ppz>UQX@H;!<~ zxQtvUs_8KmYu1XT3~S@VR)g>;4r{=j=g?foz%z6`2j&Y=BJ-_^i*T#FT`6{ikOEH~j z7H>ynGHkSCOsxuhu6+WG$Skka@-$+_j&!ufCW0Z{2|<1w4M-5LHxP-{JIP4R>d6Z> zq^$brx}MK@uL^uSRP7W40kvJ}<^w*VF@eKwXM}@K)ANZ+R?KuGxXp%E>;y|f$w zt8}ZP8f>|cvUByUqds}N{=qXN98x*X!SXgdu6a0jBy5Pz8WyM*!f-WBa4gK)fJbB` ztieqFNDcdxMheZOxeQN%{gxhWgrp#>Mg?&Po&L=UI|O%M5ekqI0&e|dz1KkQW;U62SB;{{KsS|{>tgsm1~)EAZV z)wp2G#)L{*&I;wDnh-6)Wroi=3p~l9Zn>s+9fB_-c`|L*>UP*D_J{zHI#7s z+jJ$Sdjew7E9t(;`{(yvBOE}|VW@_t#4ue;gfuk5DI(-z0}+AdD5hX93P4wekzmKl z9N`Jpu!YnpAF7k?LcZC-7*Elo<(pN(9hE9F;j2k<$Ld!Vpda$_V3tqTH9n?=6v1Pk z)RUXvd%3as&bv4zKHK!_6c`N$InbmCzpm6tI+AO^k#HCCGE_Djt{C-FT_vN@hK$97 zMAUmEIwDX+!SpWMt>NHEA`AhQQgcXowUJchG?6ViK*kiOOUb6L7Too4C6UZl+v#*8 zq}1D!m%m?l{|JX*g$~$5a0;SYF5VUZ0YF@{8kLJa5XsTCB2<-#**Jkxy3kRxVzue8 zhGnBlt_3nE5-p_w+juKl%T+r$h{qADQBEcOa16Rya2dtCluDP4R4{7DO2jiEnisqy z9NZN*FjD^-BX zAcM6cV=0!cwVED1j23ETQOPGe)vKjyMb|2Z!Eudzp)MA?lUMIw`u-6P393$q(Rjkq z$$~HimCQ7)uCA+rH{BjS<&KGE~QShVzU^bXjQ;Vkrdi4z~PWCQ<1i6 z_2zTHY9mDR0*JG@3W7Ltd^8kI6zyEh6QR|~oE?|G1B{$|Xq&Zqg7~8~(z7p5NPg)p zBVI6Rno4(?P9IawSYb(klTs_yMj_FT+eD2HM#QlBYe}b;Li0@4_tWd!LJ;6Eq`Qro8=|@KeYeaz3<xnJv=EpW^(9k{ek)E{dZ*i|Yls67nNX&iChI_O|mhSqK z`}?6^zgW_engI=-)rK5C$~-PmNe0~ z(jSrIZZ^H@^(VK^NIEO)`AQ_6bRrs&bY80TGtUcrUPkyKb?3g(J?)rUl|j)pP#9>x5w)je_lOu5odE-; ze)_u8c2A|R)&BuGhkc)yCVtfSd2c+vb!q_k=z8?mss`|UQGhp*QXjzp2Q@@pF1EuH ztHkARaqP@CK_zWKBP0D*D6aro^k16pTXzvZ;vXWYi&&6NSH$2c%JbdxXULg1;D zVJu!3j4RZ0$`3~|1g8s~@Z{Nj`m|;uVN&wjRz_k~2?6>?>+^{_ym57DmgV{S2O}{A zmMXDyVq&xohsxYHx~EmMgQosj9fc|VGd`;fl+7wXech?rr|vrapP(*=V=zVA)8)zJ zouj_bdjkxy3V`3!KdX0s<}e0_LI_j|>xIb#cq1(4y^%jv`W)CeyuR`vxFrtcPRjsN zPyal*GNc9i2cs|qiRKj#H!C?Q4gsFetzcLc5O335SJI(d@B+G~I zUG7*rL;o}yQP|pYO2rzUS7g!e3zMNFSwh;5Xci4>1*@JYK^*0kxFdM=cOR!t;{omZ)-rU^Y*w})h{m=Hxo15_dGiOC)|Bud!=>8v{ z6|wElZ*OdF;`^UJ`-Ry5UuQ*f|If~f)c&9Mi(5M|2|+)<_b*#Jo0~h9jm>BO+*uO* zYo+MM<}G7>ekh&)*XqPQUvKcQ)&Fdt8^|*o8{o3Sv-4mV*4wo0|7BmaHa3y{PoD`K zwEtHFf!l*^Fa#l{$e9ujMDE!?ca{YIS}AIZ+%1lU^F!(UzgBPae7(WHR{zr*;KT7e zv#|j#D?B?7c40j*E&hLJa)#{x-C2>?|Hrc;wf`^u;>H#V?SI2gZ)!EC(`#o%WdEDa zis=5^&x+XoyZXhA-QH&PvESU@-Q3*W=zkxKmA883RlYaJYK^xxULJ2}{P)c*8)EsF z-c!rQ)+TEC`rcEsj9DHZmT}8B49kS&8;51ma&1_qEN>f@ZI*8ulsC3<2)6vY;S<#I z#ITH6-aahjmUj%xgyo&XGHH3&uuNILd04ht-aRO9f(4D`J;MjE<-Nl)VtL=Nj9T75 zEMt~$8I}plw+_pc<#brKS)Lq}H@6VTa((y!wtU;Lj96|AUqWHaQ)3Tq4$G+J+lOV$ z@*Tr6Zu!8lOj!Q?uuNJ$I4o0^4-LyU%XbdSn_C!U`L5vu*z(=OGGh4;!!m05zlLSZ z@*js~-16aJnX-IjShiWdXHeeU!XeA|4j;gl?;Dm8%l8kSLLX8FXhOjv$$SSBt1by%h> zKQ%1dEI&OcZ*EbLVfp((8HfmC`G?^H)bdZmGG_VbVHvl4{TSOGAC^hWHxA1{a}LWk z%fB0xfn-3ICx#DT%R7c;#PZHz8MS=#plt5{U$T+dxRSj5;mglCG%o%AB?q`e@3|Lh z2fuz`wLES4n*De0ePJ)Q`_bL!?7V+xce}lPY3qYquiO0YO>*PEfr_&~+RfKo+1Nx+ z(2b3QgU#Jt1~~SU#=jxPoc-!pc!HM~O(J{|4Vf;|sJOHfTd_|9~CL(8SowjmL zvvmr3kE;hS?cwmFV_coGc@}Tp*W+pr;()7DtY6aA9+a+yq|F9)RbTDbvF84DP%SOz%43St8U@lP2)2Y`n!EI031Ok}oRV192uFh=Al z>nlL!WfREk9}(yXnelNjKD5_BW*?yO%s(YfWHwxjka^_hOU*I%$`N7qW>|;MvwW!; zbkBtT4?kdj|KnFKg8uk@H@^yi=iYDz{$b1RP}cBy2Fw9A)3%4@OxiVZ`kQO!_usy4 zo*a&AMNr$<@lLe?3up^_r5>AGJaoi^D9mKhC}ROFgp$X1Lm1l*Bo)ChI{7E zUfcWr;L4ai@8*kux*K2y)ZN3D5n}V)5(bD3G}2O0Ffsak!TkQy=dO&`?#&khiqT`* zYX3;7Dt-AMvjNRk4ogkNJ>~E}^CnWCbB>AUl-_yd<_iEG6Ut%gt@Er}3caTsKJ#J| z`e(jj5p*4^L8OmWPWn_QoPlz9zKP{b);#mGpEkdLeq3C~C&`{3di(`9pJO6+PHVABF&*cVNOH!UMvfTstDjTSl1sLQUEYdN)L=d$i98~2Z?K2!91dhDk&CTc%( zX-tz$qxSrpS0-q-ep?USdGzKiH;K13@n>d+c90W z7mTz$z;;}Dwmw9xl9l!4yzHsoAODnz*H0uC;`Q2_hbCU3BUMsM&xwlV%JpQI6U5mNCFIVsq18%`=*N z@M-4`3b30#pO|rgFtfg6ZvF9L(bGvY>)U(v&rRgLmycG$-~TiX$N`V2JMTo`XnLzY zeJMKEiuwQ)&)Df{l^@pB*UdE2U0 zfKlrLyLGyio+j4?b~|Jw&I6k_nnq;29bovnz;1_U*t)=OhiB+{VDpGHBIE7!3|SZ0 z?eq*j4{RRZqn_P5u!o+8`YK~;KRs-H?(T89ujfNgpDb;Cp5yZ>t-o$*>+>9%m$zPe z+Io%+bGP~(V#16v*ja67{aUm zyB(gr@;tD4M7lGh@oUxvb~`=$>PcX;IWp?mtpj`O(?>ht#<6uax9d2CFW>vV_32lv zjldnf5&NxA-?u&ji55thX|xk%2aXzIp$?7&JP|6L(L5{{uLr>84rIE*huIjm3I(qA z5Lja$zm9MFDhRxMZ3ONx3tqN90(X)H{rLax#+A2T`7&?=z-ta~9>SM?=2GV3A6yJx zc;-U;!j~Pq_h8d<-SV~jAKi}uPk?vrJ#Y7WcfC9Rd*|BD5y<@K*!t|&<6AG<{Lm(~ z@mm|+@ooR&a_V|>uNNwE@h!{u0E6R@{xRlI`tf;-oy_$HXd47dtGvV?Q6ht^qh8_UhuBR)#Y=6&tlbZ|jt~UIp!&L*b#7+h1=eJid8dHCgm9 zcQL&(h=tKV=T4%`)5K9X=WPH|*DEFtpqr_caadKF9=@&&Sd?786q-7?9O6EA^N_n< zHgN~Cw0iqfvNXMEA*OyxT`vuYv^t;(BF#0lWUd!M+vYf1z3rLfY_1I~cfBwmQ(|Q{ z+!C3Z*YiC<+(rIQ0ybFGUI0Q6V?;?dJe!fRZe_mxC>Oy++Etk*JS|5R5`Jg z;mlAubH{V$x&+!cheCAa_SYKm9SjC+U1Mony#6qNf*!5|<(!OQtFMCd*WUzSod8&&yFZv7GKu%_ zI-u_6U`m_VfWfqS$ERkD+Z;?O6N9x#e=CD2380vRY4v{I`d~@`Sms~~E>Au%t(n%y zZ2&YAZxRm-rqw~MIhfYS1oGJRxQPuIOlxDaNHeXG^+>!PGcj0;^n<~)Mi!jn^(cU1 z4yM)nd2TSRk(1%E>yZIX*_AcZofS}X|G%^Os~eXdv;4#EYe3=I&%bI4d=vjv8{|pJ zjTJ}l{WZL%rZQ<|t5Pq$9!PQ2D5|p3%P=xCO$l&c3#cf>M)@+4PdjcV@|3t_9?^6)%T)zRW^^-7_7G@TDmM2WU)L7 z!2o@4=*?alzgbderZ1YH|IlyIVW%dl!Ov#boxoF`nZ}&An#kp4btq{SQfAa>kRYFsa$&hhBStm z`D>BZMQr3ShuJ8x>3!>%Q%n-Bvytp6wRHuW3Fr{oN#ul7$OmO>ayoci44stSHmTt) z1aqlSDP@m&xq>U?4XCY#D;Si*F(&MQk*2_qd{+nt@`}_+r?hxWPMm1677x($?A(7F zPqoac@?94<*BNumSN$tbl`s(6aZy?*bK?@c)Zf?S)r2}2Q^%`|%uL}1`B~hOnhw}% z_Amt$FkMK_CTs~?G9IPdK_(vWO*!K!BF`qaLb3N~N+j18vCii z_|=L&cOejHw$Y{(b`%Ou;vQOhg26S$?tW0iKB$=0K_ zl7aggC%%9yfH@S%r-$W+gNw9k8kUj*443jy^o*=z{RFtpVFKJW7EHG`shZ>dRukZo z5D?a>Me7=t>)!BK$SnvnlhKV!OMkC7l~8p`YSvwv-pz^6Ofz_!3MLm3C`ZwmidJon z;|gs9S(Q3bQmT86mdn?6IXNlfsM%;Fn_pt z>3c3caPgBD-*VwEFMP{|S08-xpm6Xi%O6|5&62ddVE^a#ziFS@`|G_YfhWNGK^(xl zcP?%J;`TeX@89~ntsmVowqCjUA2weBn!Gcg$8NlN*Gvq!PV$8#knXOX;_VHA$JG{3 z^MaShjG*Dmr?P~T@Ys-uEQc8+=*05N6E!?`tY%)>O3(E^6jWN~Vsr<&3`(6dLhp zvwStG8z5y`r{QzP?R+}alwA{svBz$_sgJ=L{W@y6y++WffovhVwXMb)ou<#zsv{W;6tI>JF z;PI*|fJB)ntH|wSJMD!l{u8@M>Edci*9BFmrX~KJ$SpVL*K zyzD04lbmP2{djjtVy>8Z(RNSnF-YRj81-?;xgC+4;_f;KPsW)*0*325`$8bO;E zqp}J#Y69B4w#K2&jvHA88aV}Rc3UH8w;qG?*bSn;(ptk*duMG==KgRD&r;x73fxT!yyN((NA@{Rcz@?$q=x3H#pOy5n2xJuSUl#>BFr zHN86GtF%w_bjRnT?t2uRCXa)d7gjp~f~kvQw(UDXJoQ33D!~vRXnIA=$!aBcBbyCs z4LA{2vrxhh%n}cqC)N`u`(G5869>%WPGpnE%u6qBGAoRox(jMmwIWdelK%`$ndvpG z#JRng>6vWS8n-tz>#}Yd;UBqg-u*G-XLG$4?teMRBVbyedv!CCoD-T#1GE8w--edv zNA4TALaIVV1`l!b>ENex{q8k4dN%Fe4=MwIqc=pet3^>zGJ;YROytMg7Ft3>(0f0c zIQH?GK(cQj7$M%PpB2=zrpiUJd{*xJDV+VQsJ4m?vpYQkoY@Xg0H`;ddJTi8519k( zoEgHTdk>es$c@R+JRvq~%rbMFadDAhiLj9fs6Nw{3k>E$JV(3Z$Ie`gD{_&%UuDr+ zOAPD9lenRErBc+U3igE7H?_<)Wef(x$UjHOge1BOx__VBpl}?ibx=)!qg^dQS8}a0za<>}%!an#y#%Svb~E(Ta$1 zjv4mE`@r>KnT9z8Mk{vE^+z9lV*9DCsV?9B>T~LH_PXxQsZ0Dzpe~o_vb&<9O_&q9 ztZ%q92kp4A?2DwuW>`%DB2uxgDMDwUfbN^8Ax)h7S|iic6)=|6rlOd-<4n9wQCcG2 zD=SaX)I2pXdvM;C8bDxEZ_ZQ$Nf z=hOf;tp;XD=?o$2`>ub9gs3OCEwCH_cTt;495(UjVN^Be)72LU$t z{&eH=>n~qA{Hw#CJpAxs>(F_4e0X^2&o2G=rFUFPUizAgU)cQNi~sfFdoRBE;%hJb z{gu*%pWpb?3r}u-)dlGSa$)n}R}VgLpd8$PV6ps`<-4!!S?Zf13vIb_<=glFVE=pf zJNxea=kNXD-Vg3Qw#V+h_zHF9le>Si`?1}(?M8R6?r!b;$@$7uX_Fr!Q z&h{s^|Ks+%x8Jnw*naNj<6Hk@>-)DJ+46!*%s)4=pSt`T8_2{@Oa2LO%Uegduc>*s zii~Dzj#fiVV~F8Rvu-l)7bD$vCL`%?gwPPJoT4ws`ZfG&BhtYPgTbj7-nDiVyREIr zcpdFz6t#p$05t~bjNldi}7vIDMmy1rzHv`I)iRQ5G9vAUOO zV75R*O>KVtpoSim93dy`Z`NC`Mpe@*L?c>rL~4}B1M`a5P|NiqQL<&Qt$4qN4|lRw zn_&|uEhjoET_^1+R>9g&Y=#jepl5j2m*hn%8?SHwX}^Y>kr*#p$Fqt@jCa6nkb?`U zBrZe?G0`TgP%^`dQZuSH;@bnP7-y*^b!}FEj;lC`S}dEZvB{h_W}tPkyzi=Dfnqd?> z{7Ank8-->>7PmtjldU9$OgJZ#{+un2Cf%*9i(&JQmaUR$7h>$rU-g@sa!5G*aR0%S zTf*UY^&d>xqz=Ec|6s}!;P6AQ*!BW+7iAha{9qq*%&EKiP_`>5PPAa-x+yE$)(c3h zP6~}eIOepG5{x2cwTfMOa8LtNnOcA?)tP2AUavMhR5{iS6|Et>m~jhRz9J27PjI{ZK%9PD(Jquzklu|*PT!W}HyJtZjDiN$hxzoFJ>8`v6( zE-E9Z_M`o#rcU1vufKHr4S*g{rc%#mrKtATe~RRNBPuwRY9K2KUHJN+o_tr;N|AP9(-ZNw^- zddsKfEq~dsVc2|uD4n6B4C>9MRa7bJx+Cb0#>8gY&a(n5dILVeND0{XU-fG=^jv|3 zLJ=CxvGs(#Sn-#%bSaUy*ZtXYz2?xpQqXNJmcr!=|1hYL>3AZpvR0JaVbLm;!u~)d zm2}o28VzZD5=BU|QP*2Ur@Hx)evMk*;e?WICTs9=Fc=M1WT}l5J!RaUs?}?$I6`GY zo}5)8*o|w08mJAg#{3+?rJzPesx*AwD#5sknvd$o=@TdCRLBoDp@NA>mjGD7?4|0jY+$L4%sI8n2ruz_-r4B%mpL4 zCT9~8Z9*s)kT~MVhZ~UBkeUeu^C!IVCYcSvws3iOqhCWz){-jZ$#jW86L(5v0#2sN zYQ+mecPcI}8}p>uP{w1ockBC~=-0>wv~oh|xVZPI+G*)Ue`&EeLz9qO;O+6;nCC+bAR`PsZbhtX7nh+eDhoRtbpP`kQ`@1Z&tr zYLpMvNp~UN>|l(i=+W}cs^E@F6`AnWB)Mbtt4e)y&~u(ocNrn?O$GE?NfF@+?!)UH z#4fdcRW(X?65-Bqy9@ST)CCOMmN$>^c&i-DMeTmN>ZX$^dk_huL>ajJ2NkU0E2kUH zLWqjw+*}4D`bb4v0S}g}s0E+y)2#w$3)@}pa+lZhF2Yax$wRJiDNR%1>$4!MD3 zK>ku9&}4ysQ>7Jk2X)!mbad(6FAQqPF}Iu~9JRW)<7yOTf4t~J%L0`w%3=u4I2tO5 zHo{O}8QOeiP$T2y%UmR=RHBk0#^W7uL3+mL2|7A{zpdtL$nJEOXd;FH?LF475$^bt zn#a+wXV6BZCUTVCjU@GSwoOOMcuuoB^WJO>F5-5|^0_{7!+D+d1Y&7x&`G#r&Su8o zDhV;_%6j#dIj4H#1}9fi-H?&$R|t8kTBV$)u>>W#VVnmFB&pOZ!K}wEMtn?5k5s%} zI@iuIFYn_Nq|5Cr-K465uUv6JkwmAi5Y9rb?JRYaShb9>pgQeo(_oASt9yUfui?uIStZ0`ShVKFjBu&ZuET9w)M*rRXEl2~5O#5J z)}O_y`&)w=UfeH0l;KE~oS~xAp0#%Cc{W;)H8dbrwA1xSm}(VctpH(go3HNIa2tMi)5>a|PBB(hZAF+Z#kf??X{|OOXVfow@=~$H ziH6nK9E=fnhN-hwXSv+A!@AcEvvM~`+9TG2 zhu$6V!`=@2i?sl76Ku;OT}Kg4@f5;NO-*o#YM^b0yzW>{XGylcN%djaVUJgmlg=c> z!$~U0wjE6rTuI+@s5K?l^aYVXmq!9ANb_3$xi{D2w%%MXP0jVfS8YECb_irfQ!?$T zr*G-4An@s>)?j?&93%5w##VwdHXOFeWW`Z33Z7b5gCj)J7trnDf z9DUDo4{pf?Y-YUX?SzWiz=`k7BSo9e9@|MV=1;*yI@dTZdQRZ>i9%+vFpIn46HSn0 zhu;CSXg_jt%%VGrZJyf0-qH#2T!YOyL*-b!)J(8vMiKK6b(VRuw}~Dd&}M*r8JsDf zH`3t{G%GfF+yqZy2olXJ9&T*X8dyHZJqz%#ASA_dF<Yl^Ixik7{$4isvDAE}o zD`1>|39R5V5CLX8;D!Z!SbFxVGu=JB7vfg@Rpv>=Navfz!Ut;<-k5Fv_2>XwaiE#4mVU}+=?w^~KT zj6FXnn ze#_=BfgcxqzVSKNT9${eiC167?9Op_Iv0j*(tvf+?G(~(r4Sb+By3HuyH~CYZd+G! zRMzTNS0$aH zuyhXw(u3iN;G#R84@d2V6cSWKlr>>>*hCK&Xd5r3qW}j-5RNIdk#JK;qExoUDn2&i z6npQcRq!r?7bFkgNk^R?sTB97q?*l5xuCYwXa|Euw+2I<6kPIzGq{q=XZ22=;~2W? za`@d`z~_p`V1#@f1pb4=vaY=R_~k*lasT~vy`zfv-|vaV!Y9z{Fbw2;{M-{C20Upl zODyoc7C6aBN(%huEa%On2#7JZb5!tQ&|?AwVYBc-K}d8udh{ zV{6$$MMp_(DXkpj!^&tQZ&afJJMV}jc{jf@PlP`NJZUWB$@r1-J;#&Mo#KfL7vp$? zi&ZoathqfnsIUdpL=Y&68z101Xri5QO8 z5Gse6tmcWOQXYe6}dZV0YOJgO>f5v(86iPQ|9I4C9J z%ND$X*e#cU6FGzA@nT*t2f6`g!PQOBdPb!^IUOy)Ts4^XL<5PSI~ZA+Cog;r;7N5E zPsUHr?>U~_srkgCR3jA*Nhk0i%W$1kJnhRyXU*|;N0JGvWj_~{kg&tY5S=)sh@_`v zr6L7SRtvZDanf3imz8e5>W_2wP>^O=*UCIOcm?2zx{N2|SDV~(Jh@ZziK|c$%WXkU zM9M+c*A4PwEr!gL6D#m<1E=f72;+xbtdL3D_(oSG7^tAC(Q*YuU7?jm#Y+|A`6}y( z@@}`2W&Nx2tWiH($2# znT_hi5yB;I`#UFodiI~sQs7w%+%pt-;M&*hY%t82;eR~jyJwbN^z4Cam(N#6$9K{b z)u*35aP6zlSI4G;A;zFjJyWk8?QT5$_?3AllH<6TGybaYy?l1GD2c1rUa=71a~|I#CLM9G7sO$_vYb? zJaFx06ZnoFuTA1R^-R6?(uMd=uH)z7JNe!`eBlSKebofMGL0Tzknx^>NNl zR-byNUVHJv?oKX|=XH1Ty?Nb*)N3zV1aNBoHwWOX~}& z1q<;VS3wIj3NiWKJbbAKu6@OMe9;B^clw!n?fDDw9T(6=_)fkz4`1?uYtNg&cf6|; z^hKwhsn@=IA->}Rx(MIN_vYbCJaFyHCh#5a>Lk8X&(v$rU5M|vfG)y!^1XTZ;tyPV z&IG>WU7f^t>X~}&%0hf6C!ROXp6Q6m_vYb?frX4|Q|y1#YljP=kL#mF)S7&69&|8U zU7DcQIO3DkntG;QySNbF$ysO~wI<)2hcBXDyRZ=847+YV-`N|cHiZ9_nyCCuy?^)eZ~cYl$phDzDxexJEegK*tj#hBiBYeVS7rlIX$?y zfjT0trt0^!<&zfSkQ`{L$g@<4ApOdjGy8nsy}JijagAO5~_l*R6z>{8*Ig| z#-?2em*4wGPENO`%iI$0eDKCfH)fH&O9b!Ijaf4hKNop$cGKJ3!Fe0rfjCOcza6N| zMiV)u%dv#pRds}m#|c<)sj%$w7M(&PA7z;Yn<`h`QRl7S4ir2N9;Y?ZUo9D>gqH>9 z{oS_nxa2G;ai&_<%ZUo)D}?g8PqBp?^+=d0ppljsM^3!01k%iVu>#Ugr1_SkXrp|- zVgt69g3gs3k$YV?Z_K*6VXA@g8?)w0#GO+EW?sArH88`&c7_A&>>IPbNJ4arH)c(b zanqUf%sKydZp@m?soRkWgoNih=T}9MP+~^JkL4sLLRReszFTN&cqAq@v9QTAr5QZ4A-a8**t|U@M0e) z*;>x$6ChR$aail>*Sp5))k9oo#sh)NJTwSgUP-w1map8#yxp9wBXqr(k5Fwn;FG8l zIo?lnz-1CWm9sEN^-SrSX^khAXLFM&uXd;DysH6w`*R@Jqib_}X|1yj9%rhO1f@KN z->ILtSq4fJPGqOI6bgF0T{}}F9AwvBNyjSEaknI-(30Lrr|VkHnSe1)(-Q=^xucbI zN>H-nc51xiWL5-H-~a#OMtEUZGe*8kr@+bRm-TU&L4{txX`D@_kJN5a7{M#;A zwjREEP2AYn`qnqjK4d(XInF#BVfdKG;f|!i-7xn~=m5svDc2l^hr{t87b|OUG}Z`5 z-7Z(8Jj<=nK|>m3jlj)F6pt!anhw!+EE5rY7(%NBrtQ}v4c3_#J5rge0#Y9gN&PKz zNPRAAo=NIxFr9F75!&y%he(ayF^?NZ+igYy#NFv|TxA0%_H@=xu!R&hansH5~=U;I2SQOkvy+rDJJcb02dTU7Pcg-X9xRuf*^(e~Wo+9;~($#k- zGz5@(b4coU&Lj1>@wGQC4uco&DN^4lU43^VJ^-mVhNOPSJW@{@HAXQB_Y|q`l&-!z zfgHX2|299rap6;YIq>J%&%ap;Jkh?nW7=}YFSNqJQ6Z(Lg<5gKta#^aIg67Z;q&s- zf`i)e=175e!XmXvt{$yER%9GzNAxyDFViypy-AEwX})gILdM zeC|@2MaIW*5PA|4VvggKT~z}fIOaHUwl#P6q)?`d9>ypg;|X6EO(TY{0_-D8($eAc z@(52*jtmi~Eyj0bHWCXIYrHQ30=kmHG^ABT4Lu1`jZWR}2y03nTshc-IdSqi;~pt+ zJUV{4%ooYNHUp76f1%Y}iMSn`837R#pxMj=Y@}AuoF2GZ$TH1H(P*5mCZ$9KcTot-HE9@JBp=i4T&dV}xr`I1GXSBD zcndA0-N!9eW|LljIaY}zeWf;Bs8E8tXhiKnzZ}$Sfe;wFV2GM_m!@jodjqZ%GY_$+ z60qTB*Uby9Zf={pXZ%8|xdZsl>7E&xm&U9;|7sP(dEIkYG$=6lEV$6>?r3cj=7jDU z-X~Qz#qUd?bA|}qg$u3b3c&5C8XTT~p;eXlw`+PS8LTAQA^zB7qdZxpTf>gYEL3CC za4RGbRWxBs76!M^&a+yX8=G@Rt5L2;qFNUrT51K|brNlW)FDkff##e9RJ3D3pXdk) zIBz&h;bc{!*^uN-cqtbp@J5LSmxq)=XluSHq{zkzRbh+>CdcJOOQlzHSuwl*=%epA zzPV+pn(@@QbI0qQQ#I(c%gVn6RkK77%<~AGSc9y;?wQa7=TgLeX*9vo)VR}rJkO@a z1xn(MrN*5r#J8g^F^rm<6O#y6n08h7_>&wHcJNA5jTil{f^n=jJ14=Aj@G-mU=ZBH zkKOuJqsU2_wE0$-q1F#Jaq-V{I{1sbU6!d0Qix^ zHyys@(nl^;E|(8Skxbh8q}7&AKsB~z zkS?7<#FiBB6zq83?npH&ay(SM8r2P{;p{Yg&bXaVhnli$V!t{_CEa(T9YqM^<0qdz zUyI6SHpZK*79d-YAKq` zxf3+oncd%Mc=D6m0Nx4cALo|&YLsd*OrudPDM111+C8M2wg*~DH&>3~bdDgBv2HcZ ztlYoha;Pppxix^Jq(LGyJnlBSF4AT=__9sRQa%*qU9+_kQlxE5;bFQIiiw)Ia=%7! z@=tC8I4I~(%uZOVSvjltNF%Nb-AKyYA)-tuB!s25rUW`VQjFSb{y>-#+tw}=Q9IFiB(CKv5oYBcjYE3;;Pf#7>Xi>~ z9MzpN;rF_HRJ!giWHYsn9u;duBG_(|ZK`aieF&*#673}CNv+(efe_60v6*uG0LZ*` zfJ_!Gfy6KNhQQ-$i>Gq}Ip z*fTwatiqm2_lO5in`dMke>6aM9*l7^HqOX6?hde|{v;W(@o>oj8S7_c9JhThX}3?t zM^ANAlE9DydCWo)%t=`@Nvz?g;6jjKLDI7v7V~tXq9aDrT$|@??M|D%sur>!yO%dx zXeP^%T26Kf6C|HAmrvgWQaP;w_~u*=@-`K{RLcR9^m+JTB&{`!0_u`7MiVV&C>4*y z17g65@E|AC%J65*t*07*XNrOqu~!|MuSVwygU73?fPi!otRlCQ?X(xJ_-k0Z8O|p{ zq?TG4-iX)v(<*>t&UrGFOYl`iEhO!-KhJ7NHqMU2SUJ?UbkR!m$#w>2eVZ5ESxyyh<`RJ7u)*=4v`mZX+H5EnHh#nZ)bwjAyH>MM8qO!Acp z$tQv2qhPt+otFG5U3h+E&Y57x#mMZIc3PeQNl+My#BxQ?AHcr^Pd6Go_i9;y9d3rZ0oBofgiN%@h*z+dbo4ev*tBhQeMp5DiQy zkgM?2Y_SsO#{1eQWA2QMQw`3CF-}H#K*n>|l?>Q0)#P~`M`WCuFKnmMfQ;v^6gFUvOcCPhyy{c)-ng#m(TO^Ji`lG+)zq`O&Gtzsoo4pX-n2)IkC;CBm)gS4({RM) zDM1xa-@M25X1B97a)ag0xaPL<)wznSHxcEJL{{;R#`+1fEw?%=SZ&}z#}Cr>Ey_o~e0a~y;5RG4+)S@E9qdgk%0fSJmfgir4G+h&E{Be%PLhox zWoNQ(B+_M@lCWGh0d|$V+Z#1MX_uY*-L#l5z96|LTy!>r!MvAXT%x8&eB~@s z46zQlE7Mw~#5Ef*NU=^Pq!ZL#PCAP=HIi~AG*_gGbv+5YuHXfGC8`E^MmvFX$#@}c zhgt#3U2-^P9CP=gbFC|DptryJ=ANm6-unp9K#O$19n?T(lC3kQnQ?|+M5ck1roAZQ z`$h-k*ys8aF~%+#L3`Dg)%kAJC3~IXNyLpf9F>AU zAh`mCVy)&jny_J)6r)(RGi=t0_$pWnWWymkL0i$KQuIvC_C)07?kLUiE$lpIE|P8| z%`rs)Rd$=x-CDD;$)1MLkTYeuWBTJPQ)6dpc4>0r+npQx+s!4CGSAg^LQ>>}l;{NO zWu6rEW;E{=y2oW0h9hhw-U_2gD-vr~9Fc}xl+JP(FC5h{YW99i7vI-Mwq7V|CJoJv zcF(VH!J2c$)!IGoqr2j=c71=@b2m@S)LLt66}FIXz*^U(DXfZBJW0CR#(Z9-By`H1 z98CvgBir^fN*LzTs*s2$%fO2};^(DQq3LLKi%ia&&YdWL_kHl& z9=r5Ik(a#UeII!~9KjM+VIWo&P_3Zyx71b@l(x;_-OK zmL^b^6iQ(rl+fVDw!FwqLn7PqzFU?pvpDi9@4F>CEMX68DP$QKpe&_?QXr*3*&CpR z04?Nc%hoke+Oo7XKxv^+_{moenRqg0roX4(@AJIAr&AHv?Z z-u}1kFC9*O?fM6U!v;$?}(E@xyL7`k8Hqy;zo<_th&;zVBo^Ijsc34oAeK zj42Q{MuiQugMYK?xP;F0UM-d|a> z;=GSYSP1>(bF+%UmX{ans?JKaM+?EX(hUn9Z0Ket9lN2_Bz@U+~V_$^j?8^yNPv``A*w z=Ek@2hsV$T+GrDf`C!v&-?*Ec{t6J*6CMb@>X3#PsTkeeyM>4<4AY6D~8`4X*_WADdN*#8WFv8 zMQloLd(8L2{m-7bBD3w3HA>=_{PNUSo*xN*^zOzxw>-J^&yf#b zeCjvHao8UR46)a)@xD$@F9X60$Yv3Qzkb@lw=6d+T(&n7({HXnO}lGU)skf&6kXE% zzT@3*-Fpl<{beA$fV34sc=06hHgmj{L*pvB9HERw{gU8H!JpAa@!xhTUOZ5 zJCmF~76>mOKSdB;o!)re^5u&hh3oaMob%zs?|o5p{~vz({^wp9{pV}-cdq*eIlUAJ zFCZaB5Z*ZLJ7J^vQRUiy3~apo`9pVI{p*9DDN(05nRB>pzaBf| z+<#77_pcWZzUFJLizn8T(?}7Q!rL1U?Eh@}Zy&$0{~_56e>io+qK7l#KYaAG za_!r&>6u_TV439Pri4An}3Ud-17DzIv2M`r%Q3 zSvh@#2;nbCv$Hn@d(laU7AC$>chw&`dZYBUmv%h+?Bd&V_nt|AbAN85FKHWJIej>= zpPi*4*sr>n{KsW?z3~_BUx_d8_=^3qH+FuMdirlmC2LXS7;>qWZjxYK0UEW9A-_Na_K1{Hm^^!-hH|pL# z{L+5}Zg}MO7k*Ft$bAKNwR!F37b+fndEnu~Q?AMl&gn1g-JNq~WC(I&`<>?d`q0Vo zpWRYCq{_az_=|6@KJ}DexgI+&{`3#dzYD!`<@BL~JF{LI3HC3lZe8)(zfOC`aru)U zM=bbZ|9s>_{~^a;v?FoHRcHL;6#X$Prw}3Pdv%=nMwQD~5=oW;%bo|jH4*%Va zYi~dOUSi#v2M>P0K~66Qr|kkVD+I@$JM|LFx(`2i_q{uB*PQ*^iypl&`jYZ(+qTOG zM9Sbq<*7yF^ua)Q0VxxL@DX2#9re^BY|%peg8KHaCx0C}{gJ=C{P7d-oKA+(b?v2!t1q z3?T@wwLC|jpFXF2a-DnU!egY*{t10!mnE(G%eq(A5D(#hyq=ss00=K2^+6C;8NP12 zZ@)vdb@HtzuRrJV{a1Ww{iidZ-e|ezsGTo9`4bg6{W&1KfIJ34_}j#3_Z_|SkJn$f z`A6SA^4gQ1JNB^V+ZXztY|`E%SKhn-Po|yI`|mYL=1g7?-C z;qP8`*@-tU`QAadK0WoVtG==7Scl6wz29DPb0#JTa>vUJ6>fj$jpwuzAAGdx-fg8z zemSP9iw1u5wI9Ft@_#*MTRFW*IOk`5sVLaLyG63?-q`RC(^=1)O8?zo``0tydghlT z{7dGJo0Y};Zu!N^=?TGp)|VTC{TaX6c~Ip7STp!+{pTkR72f&58L9NS2VcB%nK^i} zF_in$%4t}zpZN-U#+(~D`q*v18?p@_cdnXz@6YQNZTJ2WegOV^I(Xk-|9OS>FZVmA zp}o6ft^@Qir>{M(n#KF)RJ=f>|%Uy3|=_WfTB?znr!6UvWH{ppR3gH}#Uf&J{{0l|Lb z&C6aXpIN;!?)dokFJFDxkw3qA@ziZSq9hRfa=YMbvwsKk`xHHEEqJ7V6ul-|t?dFN+l^cHb`*&Xs9ZLV8 z{^HNP-berWvVRIr~TqTlw$X`?5<|J;qQ|7Ph8QT?Jb4|;1|XwAJpeh7YU z*T_Y_Z%`|z#e)4D5&gO+>hI&Pp6q;Zzw$572>kNq-`$xWQ{HjL74H|`ajd?&?Xgae zh-~}?y6>w>p(}1YJ~&8Uaso198ZzG{es^f2=G#Xu4c@tBb2~z#O zcon2Vf7?Z0rxhO`V>cgqeERp7Kl}1aYd`woRqsX)JMZhmx(i-iIXx^K)f{u?ca7hj zliVMlz94>F^Y_0oCbHb0zq{<}pMM}fXVHqcH~;pP&h!752Yx*;v3WuVKMS|uMbK8L z2B~B(%9669q<@fJC_QNWh4D?}UmJUT%rE(~T`Jt8WJ z4jjIJ_|)MQLyrxeIb<4qeQ?X5ffxP1{4YLrxOj+9C`(Ff3(OH-NcwSn>Wkf0s`Ji@ z77|I8Ea0AW)zo3)AwCTkkyhr;=G~JuvyWum(9{=rg=6r(3cZOlv-5%Xaf7;I>d=mI zf8V-?JUdwzn>vKIMtJM;+($MuX>kcUE528!KHu@+B%ic2w?_Ba=15zb8I?M(ZFYar zH1~b|s;R|%qn#n?(`erzSuu5Rx8wRK%-M0Fse?M!{DAhc{iEV7!afSS!{|Ql zCx*M-8J|1RyZ;!@Jf^+7bFQb#(9}@3J3qbAJ>5AgfG?T-ytra&u-h4Zl)IfVJEeTp z)BtbYxu*27o;et^ZUicuPZtmIN#{OkY|h;3{=GSKg!b;uxgJW%&8KzRKS`kd?~+3m7ZWe(ZrrGJv%B|S^(lO8_) zm+||@&mRwsA3gTY*u!IAAG~Djq_Jg^_a%=@HcH|W<><$wKOLPO%?zG6w0=}KIwJnL z_&RY(yizP3d2!?>(b|#5h;d}Up(Uc<4Zbb9b%@;awZY)Bf%gX<-}C>ueo_t$4F7Si zKmK3)y>d7?v}6IVlA*1F1uhSGI>`tYg_oo9g=(gp#t@a8jyUv~6l zzTGHSe6kh50*mR1L_JetTn=TvYQ)11PN^j#N08tw6>ts#F!ni_Qicgm(!Raf`?7Q=-Yb)oux)W+qyyel6h=3rViXZ?%i>^hty87+>(TLc*TTw`EP9es zsIvuQ%~{~$@t{ua!n8(LC|4>o!KyVy@Z}9O?AS8FB2sjyqMR;PG3$)#h}wzy2%kPq zBHm0?8&4v-Xd|jkX^~7AgQ8sv4O(iaG6gbePUhvVP}-PsH!_^lX>}Q^wxHf__Y)-2 za#wIs4p)-0l@EoeRGy1yEsZCQg+7-na zO)`>Ut3kV;)jCs!aK*@Rf!gTdf(2TnSSnS|dGkorPxQ-)5+BhE7MgOy$dpvhYBm%w22(yc>M^!l&2+?#6@#uoJ*3AKDNWg(%Spf4wI~Fm z8LK}~4>X-rjZ~XCy5LK;qMmS@Fs0@0lvdS zc%)rva|%q~^fY{9s|AZvL$CBl8wQOL@uiCSOqZzp4TyW*mZ_7WY@V{}dO zxM0DwtX?#Lkxa=HDu*g;q)|tTAtj$aN2ayPoH<+fmW`;|6v~Mn?ONyp9(~dzS6GVr zti7p9sBzk8^3>y224O26gFR$WMgkhP!VXOd7E!Gy-%5Cm`CPWtC@6~lgeF(>E>2i*Sx#Xe1AqU3oOtU{EYaG_{Uw)t)ZLQgOX1X+iJ?QP2@C zYT#DEg0yP5tVhnO>}t8QjjIZ6JWyfO27S~`W)oq=msPZo98Gb)k*^3Agdxe=i(Zt( ztW2?n?zDlD3iAW=(k@P=&>Jv~DO17ASdHeZp;YT- zxi2dZ*qpXVfl4du1cNAs&J^14_;Wgyuat2X<49Rw&Kj~Bs*+6VGl(}FvzWE02_Z;p zCGT>HWrBq>AEq1priX;ytyRh$2A3!Losu&D|g7h(zKV3B0B z7|7+5(O4m=cenUrFvg-$qsugKD_aR`R0tVTnTke*_+z0B2d+vwJw>Chu4V&-L(Uk2 z974oQ89sCwkVqz`r78y0=Qh|zjuR|wN@bAWZK<5Z)g?TTF8a)7RY@7ICR|0Oxo&Y= zsz|fWS}CnsBwP(Jt1n|WdZQ>_i6|PrluJn^;{{yfCu4RKk%>msh^JDo>P(7~^mL&O zlb=er2n(smZs+N*KK3( zcP*m)5`H<$DBTJw7}vNWIZJ{>?9Bk;R8rw+F32i4l{=WtxdwjQwFui>A%{-cu)A?f zv1#&DaYPq1P@X8E4pbFs+*!&eEo>m}8ky)?xB?0Tk;1e&rWLA2wPBmZOnH2C+R!FK zIk^+BXgqC(F!^$Ww{JKl&|F-0(2;D4Xis#{s7zop}Rb~ByvhRlU@u;!?jN%fdcu+RkB`cNtC%~o_y zeI-y<1p^)`>q*<}acvPz^BINpES)F>HLU2_u7z1i@*9d`7^ktPvITuEt5&76g#Z_@ zspOQ6%DL2Aws;YEGnN%}LWys->Yw@zXrlv}wqq`P_)8~|{-6kWQcW5KFc&!~|3S7-b zE6t{Cz3$s;MsO@_3BU zA=ID=Sm;|Em$i8se4Zy`k7}Z(U?ot>>hsQk3k}=&^%wp)L)P>pcziW}zXbSB# ze`zUIxDdbUELzX#3|uOy@<(y6!%F&!;X3bek`=w!wTKjzmO_rmmcwlu@3lM6unK*v zLL0O8yr0oiiI_Z9i2KmA_$I-EixfFZrD%I7e!F26X9;lHlD-i^RKq1d9*89xV#xHFKLn zbS-mL6{ADhww5op>15JD7O$xTH6j_zCOt});v)to5@wXXp!JxQm1@{fOIeb1FrJSE zOR7{rqYfpS`Dk9Hi%0k?YrGNniG&EJ&!UE!m5rOyrd(cCcPSMHpFC~za|t{dYa4tf z3tp)vtzImc8$D6*Dp~f1NO{tgt|)A6Z?l00F$k}qFG$pe` z(_l3XbD6)Db|no}lNNJVOc7N|fj8u?Tx~e47)}l@(I*s*Ohp|?^TmwOda{7A_Cy0W zwex06Egr3?Rje(@>e^L@JAMlPOt5_c&jtTuKichFbJSk;I5jquuQ#4owxR}$+l$ud zlHC(1lToG?E6d${iaUqORO15!gHyw+2c!z=Qt1)W#nMGmiF9E6z42Y+ua58J-~8_w z-#)%={GRcx<6FkB9iJM%bbS5zx^Zs2JkE@-84r(p$F1Y5$I)@c_|ows#utw-8kdX@ zjJ-FuYwXprontSI?HJoWwr%X5v8`iU#;)bp4qQ66er(+sH&z~F#@6sF3A|(0vDIVf zm||?{*b!rk#}VBiSn1BDq#FCAn0xUb0TYNy-vN zvPKe?cqLZJY6&V)NR~>DkSvxgl1TUph3}2-8hv$i=jaQgJ4UyUZX3O4bSq!A@Y>OJ zqf?`oj; z#oNU9h_{Njh_4k-i7(|>I;<0O;}1$eNMxhL=w@!@O#6%hF=}tIsC%#j^XY6T8w*!w+?R^zIJ$O_|oC^ z!|R5*;qovuyk>aiauby~sV%Nm06FVnfnAkC~onH%b&&1Y=Efd#HOif%mv3_FR z1UFHhU?$c~geSZc)``^<=!9Zo>BJEeizgOMNG1mO%8|R^SK*!T3-AtjJG>3P2i^*A zfv<(9;7j54@H&`-%P<44fy1yDw!*7n6js1X;UnP1@FG|O4?yqnRVQDCc0w;e+o2uM zHs~Hc3uOy*Ei?sP3ay9MK^#{Z!L z*$c8AvhA{MvU_A(Wm{y|%BEzO%GS%)$v9bA#>m#l!ZNSSDqAf>WeV9+*%7kEvPCk9 zY(V;+beHs1=}zej(jC(6(rwavq+6w1q}NKPq?bz9OV>#`X<5oh*GR)suhc4CEk$>S zfYIThA(;wR11ezzPywp|m%~cHufPgGIlLV3IQT1o%V0U+m*L|8kA;^3E``4gcno|j z;FsW~fJei}04{;Q1b7sDG~ki&62M9LD8M7&BLNSGCjq|*9|3q6d^q42;4cCm3Lge| z2>b=W&%=iTE`|>QJQ)5w;6dze_%8Ghz`sF% z2Yd&57w~Q9Z-8$>?*Q(C-Uj?D^cLWo&@RBgKz{}NGxR3l8_-_>Ux)q-_!{&E;H%K< zfPaEs1NsL_hF$@D3Hm+YPUvO8-$5?{{ubH^_#5bVfWL-* z3-~MOH-Nu{ehv5{^eey@pkD$$54{NZ9P|RxDnb2xB=P#csX=A z;APNdfEPpG=v(8Yk~Ll*(Ahb{y>54r>L3hI3#|s!K&t@N(D7Za z1XMv^15`pQ02Rv19gYZLu55Nxs-VZ+j zcprQ};JxsDfcL=n0{#HL2k>tA2Y`3McLUxDgZL%;J`CcQ><$>jFWL8C5Wi$wVGzG$ zx5FTQ$!>!|{F2=YgZL%;E_^G{xdr|%;CJ9#0B?rB1Gojg8So}}3*fimn*eWwzYX{; z_(s4R;BNt558nWI9eh3DweWR---NFPyaxUz;MMRofLFm+15U$N0bU7D16~1N2{;8` z0k|2S0^9^|2HXg50^9(D_$9j>2JuUF84Ti=>>DtMU$RSK5Wi$!he7<3T>^vnCA$~~ z@k@3Qd@<0u5WWcT0{B9}^Wh5s*Td%no(Hc7JQqF>@ErJDz_a0V0MCNY20Rl!3-Ap1 zOu%*U8Gxt5>i|!KPX{~|J`L~`_*B5P@F{>Tcr9QXZUb^~3y_65z$VNBHsB^;9R~4B zR)azOl2u_4zho5{#4lMH2JuT)f7 z8fE}fa2hZPrvMXh5-<)Y0Ap|*kcMM`YhW5M3af^ z3XTAVVG1w=hXF}A1Q>)#KmrEwOBR4Z{F3=$5Wi$T7{o7`7Y6Z5=7B-{lDT0Jzho{L z#4ni>b^#p+>;$yK4nP}h2eiUAKr?Iw#9<4d2{r>7VI0r^n*cG`2)G(H0Iq^Dz~kZ7 zfGgoufM0`;2V4QK1pF%eH9$SQ0uY723aEqifLa&@)WAAGHLL|hV9mJni2>uFY)EpU zgp?dL`sV2UqvwtWMvstg5Wm4c^`6B)-yX(4$=)?`_QX&5Jpz&wD)?XUlki43Hdr6= ziC!D|qUcW18KTdN+{3R7pEhh8{@(BbuoC)c=%u07&}~Deq5TGL9{la#D(P3o9~=K* z{2SxT#{NEjGM^=I$=H|1PLRAcD3e_wc}SL!(z4~!51}WdPe2yRQ z=PW7jV|w=k*ME>I(Mh7AeeX7@LuMv5buy6X7k)7?GNcxrG%}ZoYGxWt$7HZ@$?KO6 zh~DjIv{yCo8AdPlGg9>?n{`|ITfZZk1_v?yxrHe-psA8Yj5>6>h0{V z(XPJE^|l0@`)gm5-gbb=oBdo<&dlMSF+P9kYcz8O)(oRR_j69!YXqEoqpwM?bHL>F zzOKzo$DHBXYkiGoCLPW&dbO`}y)6Of{?yl`w;f>e$G)!3%$S?u+8_EF^(NeQ&$n0l zI@jA0aPIegO?uk_CNC>QCx`~;h&%f_Lr>|~hGL)Rzzh8jU(tJ}br1i=zU}lHfn#~T zugN~ofM@%<)_VqYU3;!?JH1B0wO{l#+2>&XOiK4P+UIcoS6`FfYXWfXnZ8}n8+5x} zu%mA~y+)v&r~9_k8$Y}4{IqX7y+)v&r~0mx!ZDg>oQSU_P8V&SwZdOa+cs}lH(%TL&`KYgJy%Vn6)&J^i)H{2+Mj!TduGa`S z_d#EiUgvVaH_7$i`M*F!o>u3bmMkIZWdS_|ZXr!-EZ#3u{ z@lOkbiUoGU|I;5=iB1@q>&|1}19N)6o0VRZ&I~xU-&r>6X$Q=i-}M{&y;hCRxWU`m zx1D`WG|_MSw$pns-HG;_zU}lHf#dmA-*)zWE{lHMx1C-i(9SRW4QH=~a5$%R*8h(T z zs(dBmOfk(@ute1QP94&o61AOuOZ(Kt)bi^(ls$DZ!QzrqwWqM?%?WJFV?q z1#;Hmb&7-TvsUWt2KZ0zvotstF=@gjy+@@;q}BNn9r7nx!hs|_aivbvX!tD7bR^5t z1(p`J@7-Pg6qL1{SvR%g%e>1@&)!R=W8PC7Tv(U`j&ZI%gN4rPY!aU2cWKh_&(WYV zc){R8+Kw2fh@`5HMuAf~Es8*_Tx&;Av&)`{=n@WP5|1khtyQV!wAECVDEdSB0-t-W z2s`LFO|=6ROHrHS48csul%xt2i(#|3Cg=AcmrP4XeacmrG{!zz+rNOlPxw-|``A!G zui3i>?i|Ghdc#7er+Us65+5p z3cy9( z`1E4J=cyoAnIM+qmOx2gR21##a-*TM0|MAv;6HF=J!hb>_^H#cyH9(B|9S6n!3!9i zPbrw&X(m%E=jm3FE+jicGnaP9Bf3e~>1wo5&GVluHuCvX`Co$J2mbNDJYiM@*}Vd7 z&NH+lyst^zxg_rSTeOy}Hj~w8y~ek)M^mHkM8ADTnV^HuGdIwfN<-#IuS==QCM`~y z^q86TT-e(tC;0x#-VF^Db%Dl8!Kqg zX4uE1Kax%-_i^Z_o_a1lJwA#k>5GoyFGVw-LxfAwteG<3E0L0q2K(&Q1Hc26x8|8u{qt3snw^B>tqgxlBEZf1ld z6f|zkG?lKZGaV116`d|y*IS%9!nw>tdK*3j(-f6)k_abv@2;}CQ&6NYi-1Oyjcry_{p+9Zo$*=4#`GHAz^J2b(B$82(h z_@d%obHi${XFd8h6Gz&5ZJRDRVq`U;O)uxvY5p%^R|55A<7y4T;%TEJYigP8CjN5M ziD`{u$D3q=cX2I0$(Brybtc(`$MTbGR_DxK2tMCQrr;;pr<{GA*uRgnZ(kE==GnKf zD_bXK|Hn_QSr>-cJ=rl5)VZGZ4`vIzF-_Y;MmCt^SEKoq)^awl!(uJ{GTagl87hhlfl=O4A>UqBch<9P zPQNTq#FlAQCi^n$@<J-EP=uZS$ql`ZXRjW*?qm~xpB_3$f>+)6*cRXkO&Rdp)T$+h%~oTpYz zR_ZBZyIv&96_1%{)eU}@a23&P6$`cNShZSk1SvFL^BMPf@_n}c|3CG0$G`wsjj@pR z|J?Sit)KzXFT@{paTL1sL;M9L~{l80S;FGn|Nq(g|0Nju+4}#_*8hLD{{OS}|DUb@?>=$r6AM}Y z58XZ>A1DpTPJq8F6HC7*JxMAV{QUU$$D`xYvAf6UF`48C6NAu*BnCw$NC_m{I(qMD zVsv8QN8$&>Y4QFe5Ay5SKL>|I4-J&yp}|Lo7K<{X1BSN^7Kii02M;|mR1hV7L!Ufj z6P^Dp;nFIPsick88J{+j^f@Tp>Wc>*6|KpPx?`nwEK#M@td?@M*vX*3?2I@Yh7=i3 zSxLfR3G^gD4V@y0vX*+>7)mvp*|0pqMmS5DFE67vR<%CN=+8&JsdP*2Nu}B?cY4xS zEI6wMj7!?;32Tk!La83n!5xBVU60spALX&f!+kd`URBsX?0cL3JY% z_16rR(j@6lRG50nlxw+4zJ!aYyLv>2)(fJoB8F0?WJni6TQMz1WuqulD(MV*ueMQ; z%iXLx<0+@y*0M`IX$(|wccqOb+jOL8$<n?vv>}?%H2LCIac?>9vbxw-qZBN0SzoQB#O*CBZmi;A^<=C_G)Vpxr=9S|;#8#W z&-aR6+!e(QA$~{RBwuffuv$aLTDgrFGz1-Mr_B~mk#6wYbNcO^!J5M+BXqu=uu&vm zM#ow+hKPu%SM;F(fASA5X|*$?uNkaj2}?@hFgBAq6RN1NXsV)4x#JjLk|tc%`kgf6 zw@um`HLjAxjrB}|_U1zsrraYM>}ux2RthIbdn>P^qMX)HNG3Bjr0wE7c&Jgdm>i92 zHPSLrM0_$H2>L9AvaJ>{8ZeUZSW7*c%e!NrU=&3!<)q_EjTcL?Dvzq%wm3{~k3m=Q z$4zLxkjdDv2<|IQnu zn6;hB*_-BCmGo-!wwhX%!6w6T+QCI?-exSqU>1iX!S#xsBZzXAnxz<(<0Yop!dzBw zo5t|WYniCq{H^0p%@NVvzVgNqT695QL45QWNETp3ws%xx19`!oy`W_Zu@#f z2T$vY25O{>R^zdDu$c(33MS2O8kUKe4P}d>YGx~DGnPYW)~e)elX$D;F69$OXE0&5 zw!?(U+barnMSTIjoFGSg^)*(Vuty6nU(lC~R1kIA-IOcTSkxA+Qt?PB&Q7*Vgb!mZ zY{6s-;zmb3=Ij+cvn%Ri8njc%mu2(GH3~yeLuu<8LyIrmqcNntB#UVM$(X?r%@)&> zxojh8bXW@%O(jz}UiMMFqDDVIpT?H3Hnyfv`Dl;J8+Cj69Xj29tw&x_X3K4p6Kk6- zc)(Ce+b}$$m~6X^wtB=JE9WAPMkD2Cdc#TAPdws{IGg!|oQbHISj>=+#~H@jjPqsv zO8QolN%_?U*3R!b8lU8vv3v!u#aU0?Sg{xDDRZx%hjc|Ttt+hcD-8*|J<6tOrPH2t zn>`L&2+21ISF@7S(Up8i&3Mw2HNNPckMgvt4JKF)n`+Kk2X7T7KC7hCHGi-cvv^vq zM3Ttb<+cDzA(LzC z)D_jDiDpozLe*J^*BPl6vrQ|4*oa6%RSy@d&YUKuh;V);YRydsQdFG|GDdT$=1y5t zL9);*sue^Vg;bMk8>|6MBkvASOwmpx?0iW=BwBM7O}NcP_&JhF*s?L(WV{x_!zmIc zxkNMLZlvapO<~T-G z)c7VdDIaGoxUqO3V6l>}Y^qmu;HIu!O|g2@oZ(+pO&ZgrAry-H?IkN7XG_j_@1^8;;o$4>DB^G1oVj*5 z*ya?CXsMt>t13C^4mh!BSe`SPtMN<@b#boAm^%;-5Ol)hVr*5a>}p$khl%QnqL@cz za@WeOa4D*4xpKZDQbQ^ZhDxV=R?O(qIvPleX>&Hkq|a5&1-yB_9$Kta#af~ItoXcE zIQqH@PZ&IAf2OHJ&1y1JZTT8#%oneiTk%35pUi54HB3`Y*}Sz$I%N0oGtTaDdzx8C z2+w+YJe?36t+gpLO6e77vK9#I%YhKVM)}DV&9wB+b{NC6Nn42xXrr-kd>-NXjIJo7 zEGFz_XN}`1BBG;eO4^=a8NAFg&U7gP(ToEh5}`VujiY`sFFhAP-PQogz#q~j#SfY zBXtpz#n>Qdw3KVaC!0lcxR^5rQbep4a678?bkE@rt(drFAn?h<)PK(pf}dPDsh86V zg@Fo`i-nXk<s3*x_CRQbE zxwt=;s+Q6*AK4>1xLpu!v{F_=kJ!|TN-jld8ydvqSCQFxjLNAE!Df_ehbl%f%Po>+h0#tXV;t?FTud^qu3=$aQ%RWFrkU1T zuuP#)$s2jmaM_Y@#7e9=NEjJ!qA=?=;!pf=2mgD9e1-~N|8L+}zGB?O(-T)sWcj*o zV)z&E^?bFqRqzD#8|W6O1zDhjWUt8Xkewm($PSafA-z|6oSwINUJfr zRUwzrUTtgm1aWtQFaPhfv<#Y*2gw2RU&6sjZrQF@H*ciJvPvx>1wVUNaxnSAG zu(@Ldrz|>4vE25l^EOA_9dvoac7APK&Vz*b>i1X^(FXY4ACf6LqA3mCD_EF<;et_9 z_oR?YG}LtS+sT+iwweP`dmJfsCW&cH{E9UMGnQS0ZweM@I#JW=s_{0-S?y@ht>wZI zW!&u7^Bb$AS+gmPR@BR!%D5poq!%o7bTjHe2uH-Euo|`bOae0%D3{4-u?E%tXxaCA0nbOa5pvP)^#8Y^C3JejQevn{Je9&Gy6g9i#0 z2{2h;_E59*UHrlepf7;a3D2nKJUr+2CoDb#zv6{s}!+H!N7V* zJ&u-YHZR{%CR+}zgqt4Dnn)B35wopM4I+X?CLT#CYM9CH4#z?)#`l|D-qITUO^Ys> zi)t$kWh<8O`J&PI;I{;eP{C0`f(&0VKf&+5=^)xz-K1jk&4k=S%4=o0IZcx7po8(1 z2Ay3CyS7wq1j7uGbW}`+CTGH&;Rxn3DJX-v+LD(DtVk1CzQF+@{n1Ef4CO|xVjzs1 zUu9QPzG6bf@{bycq|a5$hKaNswcut#Z^Q=I32g-JCN-ZQn<$4PwAbTKIE^W}kBU|G z)q>L*H>48Y78~JHzzOxBa9$aMR(;6Pu4>gD8)Y)8-Sz^8neuck(9+o5)m$ZC%7?sJ zug;bpTqU&OSLXAkXueHFHT;_?H=D1hr$9cKcIqXQ5^A`O8I(T%#f~PDYCl2$_aPTO{VSr}(#A=BhQ8jfXHx zO4AP7(WW`;kLFm6f05uyl**Z8X{bBqI+Qf}YIw^?RonbGQtgz-#=mGmNK|ERwKPrs z^+VRcri~_Tdhjm6D~(5;#L`7iz3OT-OEp5lxHaXvvDNS!(3mP8V$mvt1}ph0I#dxX z)QC%|u|#ZEF0Lf=$#jv%O|_O&t1OfpHGLMrSvmin3Tp)v15;p{8&&CI%u~tp?*bhO zo6Bf2q%4fNo?#UBXes7ylB0ClSFRu`FJUp#=*Z&@e|Dgp^y4eZPH?^qzFr)U~5q!L`#=* ziou@%rwlD)y~xNFMstpJ=ap%9IOhl!!bZ1A=`QO0fk1$N!H`urqu%%l;J6Jt33hOOOn^|WYTq0Q13;D3OrET&Xp=i0XI*`LM zHZPivwOmzx2Q0)`i&QlpT8ETf1H%41hI&G$$~aGD}&mcrfiG2+8TbV z9jo13BpH-J(V>@wHZt0pqE@PMd=-9sgYx4{Y$N}S)Ez^o0nF$*SiRKOqdAk9cp zQ$wmT&dr3IEsd(CXd5{XX3^zxA*(%C(bhQ&P6{r~fA_JW=LC!SZ^$&b5?Cx?v!uc9 z_A&F{ENSo+p^f=(OEfenSj>Nup}~W@7W3X@Xy6*bV*Z;94RVn9gn`N7s|QBj9oaat zg0D7T73uiu@mEh=Fkzp7;XB|J(8tgZp}6eNvW9Fwz7qOD?NxCg=;+G2#? z!}h>=t)J}f_Bv}D&{<28I!j5vwxPvKFNX%^wf=e5MzZJzuH#w9e3ty|6>D8{VL8|d z8(6WA-|BqelPU~zZbtiWY^};{Xo?Yj|60DwX4ZTW)^!JpeorZ5GSFqWyY4DDibT)X z!E<#XGUIbVs?&v`4GsQ4I(@KvVZCOq4Qc0nAVL?Nw4u%`BQurf+K6`E$}{)3jc=&& z`m1K@Yt<^W8lfw_?;7W=&tz(KK64@3%+xZmOg>X@@e?cYpPpRxjh_?ykYi3d@RKn> z5jCRC5T1Nw#+Y?#5lrq?YzU0v-orgd35S_W$~!upp*~_-0gg_ zguABSxF2udIdyb1<%P3!PCtc{gW9;LW6Pi6=**fvpJLk^6*S5j-Sf#QO>La$YU^fI zu$lX~W@*pzt8?V_jd1T!@q2fx>6ks#n}IaBSCOar57o>?=DUZA+6e7Fq}h?`)7sgy zrS+^~?i`YSqipvu&MeE;9g11;XRk20a-)>D1*gzVw?D;p&na}q;Kp%r5=1&a-N)ls zn^1W)tgbsyCF9n*Sam(6CakSM+(=-Uk0X70uEk-KMJ5)s#ceUB5zhH5sicXXaUOK~ zZ0|X-rvql44;@zu8^?GbI)TJy;4(I}Y2<7BSV^}(Qm(TMVopTa29k-QMSgELXWVI0 zk-?&3-agC@DanlzzP(PT$yJ7uv23qkX)Q}?N<~^_(;%*Vk_&SYo3h}jsJVtW9rEbP z)V$l<)4A!5qkKD^0Gw2$LS!jlWWt7I!kTUK??0VN{xzO9YOqGy&U)J8Pm@lKSyQpi zyPcV#_zY;HxHF7=1asva-axG6w`3yIwG&^B{I?4B?5#*vOc{Gpfget9Dk=C^QkFnKgkuvH96WhR$MG#$=hysV-~EaQ#k z0&RP3-iI!Pu=++(Pbkpwp#X_xvG(jxFe^~)eTL3)Xfx;I?nA3@9PVjWi>eV7k;+)J zJ+oj^`R_G5^G0EJv((0+-4SS3Nchxt_DsN87mCj5x^m;-?kF;|>24I6>lpT&==zNV zyCc}F56|secN90A-rJpOHKMe+oPpj1zG;9r#bJi`%m|oBfPse zJN5rm0~ZfWJTh_igc<%TzhfH)y$)RteNFZU*-f&HY?<^!=})Ci>EZl7fR*t>#vUEZ zj~ys^NRpN;;#UI1My29A#V3iyBe(N=*-1qYi7KKc!_N<2Fzg)qc<9cd%+Qj-=Latv zTruzp-^i!^BQ_r(8uBHj8z%GVWVRk>^DWj>vW59dGp(Kpx+JfXvvw|!v8KxfbB2Gd z5XnT<5h~@@2jiUIQFUaZ2D!RnP{b%=^XGu<<@1nL1Z#Z8UCLhPu(ljyY$27!t7uY* zI@k2Z>3}L|#-gQ$ErR86V)On$_Of}%+8Wjv&L+YEq{%TJ#Ba2k14>5YN#}|N)Se0F z6IpFoiw22Y$*_4pUKW#nW6~S6aTuL37Go8fjM1#6(c1~AmM`#CtVd@ii-5Ur-@J$y z@kuY8he-G7MW@uM)#|*nfctxzF>Iay9({ctVyH&&Xy!5n;E{DR%!|0Bm&`{*9gWfP zTq={#Clk?`nTlO02uNN0|6j_!SqA#)qIpPt(oY&b%mo4!57pC8D>h4cF+_UdJjBqM zp{+Y)v6@tc?^i1zYsA*tyYHY-dt}^xP7w(h^=3Um`bfy zR8#I*S?l)Aqd@Gug^3~haKdhm&pJ#o5Ic7vVoEiF#ws3b??uYKc?5`^vk)=!;XuVt_a?S2ahsY6R$T~TE=#L|Bl#?@rV~1T zgVrWtQ~UK1ufu_ss7T6TP!OAkf$UlHkfpN9YCY=o#A}v7G@nak9C8c^xzJRWaits- z>WZ4eIAv2uTP(482*{o}57|(O2-R4;(rPpAW?sR>jg*VxR9SmGidhqR)@X}s3wf&LqwU-T@%H zZXU96R}mqs_Hy3oa8b2JR+-D0khVreS=x;_tyKBECY4_q$YqozV$a*!IAbZ+8WOW5sx|uF!34Uoo zNKLTDwAIKcOZ-OHxdiK|8uE6ny_O4Qu@FvdIu*#C`sul-OBRsXN^Cj>$euC}SxQmn zPwi5Xf0J8I%3UQ}*@#3F)esskRY_D?&zWl)JV$I=3uM>MLpF=3u`-@4yKw$> zw<-|Rl^D6ZiCJC6oQtk1@NAK82cn@|BM>JxwSjDV(o438rXyAiKqK$|#T zua~lUH?80oB^!_B3|e_9mW<`?IyP9P+kVQ{(gtvkhxR78 zgi+PNLuGF}YgJ%sVp9{yHs|3jqww1uWrSZP(y&zW&Q?{G;TPRFoL)4}$=eElu@wmV zgXMI=&v!xt$TsHTZOxR_I&5Y;8mU>BShUir2I3WM$jxy&4$T=byGmD%hPbfL%FnVo zkgdU6Mq_yf*{$$dNtK24an%GnWvbA}5+p>mG zyiT%uZ?uXt8Raul=z1{F;HN_s$X4g!ZQWvH-KH|X2B<3M zibTEDuv?oY6{cx7TC}wp3)}JqC4p5c729!}Dxx6-kyiHcUELCN=IuYY<2Y_p8GO7v zzmKojRN_DGla}`N@$MaocFwO~S8OT*>*7AX+}rczEq`s+5Fy&k_FPU^Y$^cjLSO5- zzE31JT#Un}9o#mW9ISMC(AJ4y*{1i>GPaEdAINV-tYak z_j}(Z$gXghmbg+Jk}WPlc7=np#FfI3Y+(tqE1ZAbdqoEBPUI zejfIjM^*P;5r^2~j6;Sed4q)avrlG>{r_!izp!@PKHfbV9Nl^N-G|RSs9S&2dVasZ z_n|${6KcGqu*Gse(UGo@&V)LTmNi!*)Gq-wUBYv0wiUpBwb6Q9fl8ztj4<)^Q5H%#dctfX_3oFdT69;>XwP`V=dpvCX{Eqfn zGL9egoyr)Ql5vG;eNx7SdK}95W}6}7x1T>t#wk6%Xuuq=?Vajz&#=u4D=ZOCMI0Tq zE>@!<+Z%t=`1#m(oh9Lv7GE?7ny-nZxE@C}Yy6|a(bxxkw^7SVk$z<>#!wh#;wZ1=+S)$^hT4psX{Q|+6< zEO5NGd&+Eg#B5yU!&UJqv(te$-H}6Pf4G<#FeB7tMjJQG=&Yrxsi{+^gu}i}ebHc_ zdE_iJ8o%2x%&0!Nf4X1CfyNs?-mg>ahy9xRq`^L&y$Q{;u(p?Tu-C+*J2R0_?lmOEGD}N(`@lD0nGfz-R*5QD(K3g=;8p)*$OyNVmhnXcuX)X}!ba;<#M42% zP>qIc_X)$FpIlnPoq-2varX3o<{oZRchk`WL({XvhA57|IZKprpEVpJI|H{#lp9mX znH$1Iq6}^8PTdgYuFbPV8IL9yO(77F#I!UR2O9UWV`Fk!6GOAP^BWBIcYb!vp-oN1 zrGb5F3YqfSiyAv*lU;_{rKu@o>cDK$`lg#UWb^99%)rv%Rup!+yQR zWw5{Xm1p(qxB`uSHTM7Mwcl7f{`m2`k6(Ap9N&8M1rYWBnj`Y)_QNk7e(LZC4zW_<*J-_N=YnDYH?17{Ja;{rC_y4bZss{MARZ@{RL=n zuYprZ`7}7{<3%!HpoJhp^ENiOC&=MjRj!^KMxJ=F>rzv4GxQK zCFTQ9^4z6Z#@UsjXrw9mV%?gJC>8{VTZk70kay4eK4{=KaC$bFjuw3_N1v6mo?5rp zc5;7?y1K;uMQK9|SE)|qtD+|ZxAIIx=` zol+ZanB$$TPh1>ant=4WJdRA8!^WdU3?E?qiHn1mPe6KH9!O&4=9HR${Nlhi0qJ&m z>>3$SGG;liT^!s!0qJtN{RqKja%RXMxovPV)ILbtHKTKW?6$#O!>=7PJLgAVFnHOp zbD&wB^X`j-J13-oTy9^K?=+<;DQ+2D9Cpr>U$y@5#lcI*i8%c(x2GYs+i^3IAG|oY zV*=9Wasvku2o=nbKX7qyVFJ?Ya)Vp{NXeY@$NK#j2QQg`^tjy4WGg3`;}@*ocX4pu z4Cx?>J;j`#$NE2R8Jsg3>zmrQS>JWb;P&BHQyUEHJ1-7iJVD>-a)Z2@a!NM0?j5%b zUNj^!HK|*__u}A%6GZGTmp>gAm?^t>`^CX+{9K2ux(&ut{ z36PM^98GF{+r`1H6ObV0$%Enu$Cw(D^ZQUTUTj9X9@d|r z+zAH*o_j+?w8W}p%ir{2)+^B9IYT}@dl%dnCrbpw*#*TFbrv)Tgrw*;FR!pJUsaa_ zS|Q#mQlj-O(BRny&I%EB*6)M{&oXdU2%oe5cWCfT180TUIO})3c<_u37#QwZfp6At zeevMyh7e~3qFKN7j=}Lp7|@s>WoG@>i-V(a3xLjw`?9|2mcilB+^46O$1V;ICWxFB zWMzHh#esDK@~j9d>l-c(_Dztp!=tRITjV zcR^MSy4o>tR=Az9HgCDw9y+DzC2RfS)z;WxKRal{s$RURO+cP?Zf&hXSDS{2E5uJ& zE6~-3fwMvsgS7-*ts6KigeX`Q=&EYqtZ-^)m7%LO180Tc0&5YvS~YN1IMA{dE?%vS zYxv9)CaZMuYIz*;%mW~6{^Hfr1msyq3RdxOC%AUc`hTn)|Nik4$DQNI@tsHSIqDz1 z^2l=dtB3#XkO9#EA3wNy@alu})=yjCX)Rdq+W*4-5AWCZ9ecICC-%MsA_Qu?Pwaka z_pY749%cgA2mAcjZ3nk4TfeaNx~(AC-T&6+cWfp%Z{7Ix#yd9(8+Td$+VX^@V=JLqN=8cgjCrRxsyhq{(l@2N|1vR-yD%x} zByMONH&L#%ll`tFp}BA?7fhwh8>>lNd>j{}ks>05<3yM#mxA$j)tuXR5;r)Gi&9LX zMNy~_L6aOv|H7Dev6HyiIIh@9gG9n&mtkZLq%G}sTBc3VByM0FcVVv6NnCUsmuH9) z+7VN7QPj1v0Ahvb@2HqT11jJe=>M z=8f=@Cdt(Gyjn~Ks1RB<$*j~mN=q7zUs0%K zje~mlvVWY0hKPDEs-~%8EsiG>B__564gY1|bsB~rNfp!mNSaPa4FNAGfuL+Ys+eqp z_d2f820DnyYL~26kw&noV|_fj1b_IlXPm~unlZEV;J@s?PQz%!!nD!jZP+cW*;jGp z{>!d$UScE|*Xek<%VQ`R?vzUAv%GO${>#pBTnPt}=fO0WZu0@6T9z6WlfEW#9pkus zq#i4EIJun4kVuf^C3Cj%NnB_g7l|@Gs!`R&I^HF6c#t=jdl|>w{p+<|LqG84mtUu0 z^fe=rpqg>6SkeTFjPwyvGs!-?pDtaeVf0g@B~{>1C0=eeuwb=(uP{g+=d zj;pi^B|0r*LL^4gBZx?fm$nHfTIPpP|Qcv zSU5l5&%J0I7Zh&-dWytT3c@BrF}OB=E`Q-TE+w_lOt9MowQzJMg4ZJEqx^Aw`!C-% zj!P<>prb*yrsl=ErbPaFt-8<`SH0^1`Kqg`7)ps>uu26Bc3~L7zDk_T0inh6SHX zaLQ~Jt~@N*T!K#1Lgn1T<%k8HOK@r|FI=No;JE~+dU@f(#d7~#f>XV`a5ZDO@9^UK z<6jHEdy~gf8eldbVAp3)PoD8el%d}eRSgss1yvm>A=xe?sAArHzi6JhOg->h{6x!g z?_7dYesJNc)^ZQL`JnTR`Peg&S#sFoi5YqrcV_X=r7$(UP3KFrc;+^0%F``e>ROz0 zMVj(-(^0q<$6N|iu5AVdXfA~*Uj~i)j*((7y=wD8=*+H}a$Zw$SQgt{y^b>+_U*#d zwwAl*jgHBlcKFXN933y7L(V(ZzKDQg`2eg4t-78Og(7E;_Ik-Ya#O8cNbcMmauccD zhRVb7M5@*&K}kLqDS{**ggF}QcHn;gVJR2%Uk7N8{dpZz9lY=u(!$RrIMv0A31;RI zgr@xc!h=dndM?4qAp*8HiwUOY5}fjl3y(uB$+-lleB)w*oKgSp6_y`g+k7R6y#MgV zcWykeao+M5mLEU1HKufKTh3u`|*cLZ(x+T5>ie)(Tr z1HR1%&z&)?dhO;B1&Y&i81U(2*zzlL5zQk9W2s~k;}U$(@`1UC=EVe0fm~9F3=xZ3 zet9mUc?}UpGpP^?BWTF-OLGy;o|lRv=^zo!#N(DWG=Y;HV9#1i2B z4vt&?elDWf+@f@x$WSRZf>{1`E~44oVi`0VO(Id4wfxOoM6KWmJ}029R{Y#4`wmOq+{XdaCeok<`FPsLcvAI?QIk46Rr z_r+5gIu^71!CXYM145t=0 zs?EIARBA==cc&Y&Km5>nq1}?RQU~Pt*T5eic;NhniL=$=S@Ze3Z-3}Ki0O5>JpkrB z66fkV<3r=1_N`osrA{I}7z@j+yXf%+I(#jeYj#fRWrsJXoqzbjyKgsWj*s(MH2p%6 z(CLZ-`w^_@^ZPReJ}zEzg2FVHgk;ex2sBj>r#zm9p9M4l(ew4D)R`3gdWP6DpRJhM z5mg{mOc&z`DixHf3Gb!SiCf1{B93CYmxo!n8|Vj5B6O@|C zs#ZtpK0l*B3_ZJKQLVK=*8sGK&X1BJ_0^ z6RJbW&MH=8fC?R+s#@yjH?d5{0Fo!lB&V1SDBdJV38cddwjxw++qqz#GB;0B@ zPYIa>F=&HA-EKEn3kcar9##!$iEg)$@kCIp4i)4}QUK7nlq~q%C9II{DqPV6F{!HF zw|967(pZ-gl~Xwi<*apn-1VVyu>Gn|9n51hkdqa7dD*L!PvWG)>3Vy5Jd z!LUSz^7-|OLuY4#z1()Z|NhP~fsq1c3Q`?ls z&${GxSp-LMI93;;4aF6X>YfBol|j#efwPRuTfNOsmp5=O^ajR=T59Z{+5Z!6WTrPb z(?)NM_?5kZ$LIF@&+-OMrwDOi%;BUMijz*p8}XN`<+3VzE(O?FjM9=#_!4dJbPK+x z><#KIS`l242$#ndM>Wp!SfJEL*%(~sVyR0_+RcmLgesM1i$bw%uLU|-t>s7fB;gZj z@E)d?%Vku#?}MzWk}`W#f_1O?7%u{Rht{o#in6$j7UaXN)J-LW@g)}1;0)po0* z4Ckg+vn>sKetNNBcLRSg%ST_I%_r@}=)>z%iv#>_nmKRe?=k>anmun6*cxB1J}JfQ zd0-Xy0t6Z#Xiti27~?3Ub!Z(EHa*k9J(ac9Q*kuoTcPWE7@q}UUN0r!5?iGr87=SQ z%a;P3X1W2>v1H5_3Ul&_Gs-!8xin-yb8Y2)`k9Q3Y38I3Y}xAp{cz@dJZk3ngbW$o z!|_s`jK`IZRFq)4cLIZz%Sps1Bv`&)?3UC7>qbN}8I`q!?2Cdb0Ab0Wigr#K2~~Oz%YC1;2Fj+d+Y=Zvnkzt1;fk*hS^LTUa1!i z-Q8GlE*LVVu{m02E;lZhcL?+H@QphQ2#CJGGTZ=U#u{UMV?S#ctTwd3Bz}H!h)?yh zA@)~&En&zp?v7^c0u1Y1b)@1fm)+wJZA%g)b&Y<|A&zu3OI&2Qhj z^#@y5w|G$X?|qx!xcLfD72qMuzgc?g|FW*HeF=~}`}3M>>ZT;ItE2&0C6&kGqAIVs zRvQYiLQauY@`5am1$@o5>dFGFkT7H!0V+cQR>+XDN|uvlx}dzW04thz3x6U!cIfVB;up@u zpISZ6z^AWCn=Fw3`eDPFx%p=#v7bpUzbv_Bq`U8$IBMPa3G+}D%ZYQ}wcMHb%kdY6 z_|si6Bg6g-b=-ID8_vXEj(>K9KYrkS+Wuwm>1(f^?2fZi4_)w%A8?-<+thM^zV_g< z@Ay5<+4v8fiN73weuO{8?7~C{#aaAcb1gG;J}abaS|#Pu z;+*lXxt3m8fEDs8Eh9i`WdT-5AhnDD$)Nx%WWQP^8`Uxba4QS&GOGabBp}|eJwmO zHJ*8yx!jQ@eT`n0+|;4KjH4O)8Z|-g%!BD=r6ZRmw~Tc6T_Z;G@c2>taME4w;OV|= zp)>K95mTMnuR*ue{}|788J z_4}=_u_moAw{Gu$djCE9kMGO-uiC$5@9#iuz<2KT_u_jedmFo-+WpDh$96@_35fWA z*3Mt<{PNCVN86!xUb6N@OMLsw+rPK{W805zzk1uVeYEujkQeaER&5L4x_$FsHh*XH z2RFZIGY#_o?{56@#(OuuW23ZzY}{)3d&`F`?*i`vKDGXn>yNFA>#qP}PJI@AZ$`KX zaw0Cy7i1AE53N7XjC=F{YeV%dOsQtkmQ-`@@ZD>7E~8eEdwu;}E1*Sbjf&7wDcZtP zG2NVY=HVe*t&@pF(kVXFV%dHrEMiep`1(4vBpkUU95I4xl#zZe6e1%DCZ+dkjij0H zUo8pu#SvT~o$J72gd^oXuBWiNIV0HG4-D0!iM^nf7CC}cXd?()`m+}>vl)z{=xI>@tz%H?ji)$bJo=3GxNTn_BE5!Y0;)4`Eq zH5kq(J7OVf&eFI3fm1kKkH@7#lkc+`kb@|snWf7t371|HE;WK9<#?=LO(ATkLdfkv zI$`3w_K6`}17r&;v7xHcRP>U-g=Mo*S@xEM+dYK?Erk^cE#mQ9or6KV&5wjDDJ2#ugmn##CXz)e zI%QIy9l^yxBp;nB1-c@xR%#yZs5&i&ND}U9H=%C_=Sgs5CLeYy@k+JA&(om4I4T5E_e62q8=6 ze5Y&wK4e>Kwe@74>lXS9Arn+a=$n)rIR%s8F4jeGvLN6@y^VHFV523*;#2~zk#us7 zBSa!6x6|cVqaXzY6FG}>1Xt(=@?tSiP*q@`a9=Qcg7uG`!nJU^Ld(fwKcYv1?ZVVh zd*%?X+9`4&Oe^4xKsm~Coq{<-=KAAH!o7K@mZ}ym#Pc)(cgmG`9IlCqsU3k4IkgDx z(7Q!hs8p$3uEI4;VC$gJ4Oj?Q7s6c~!4>*MOsOF-jyL%Z3%936(6^7^K&TwevuL#t zXtNsG?M|s>H2qa0O$v$1QcEdNp+FU@m=oKr{rQMqSnJjWq9XRn(RQR3=$RA#t^Mhe zaDOs_L)bbl5Q>n;+Tfv9A)=f0a?g@*{$Y1iRH1@tDIt;3n;=ggNjFXOHizUE=ea?a zgoD@CEQw_n%r{|fYG7FnjN<>FSUY^y-gj-?wf@AicsJqxvo!E%YjDes#0rnK*Pq@S z8PmH2BaUZQ=rLr(K$!w-&aYBh!wAe-&A3SbI!ydK86#0vpKkXa#N*V)Yp?DBeCJ|j?c zX6#$oUT?Sau|%L6{FEc(6DisoXzKUGx zcv3M>yWt>wR4k#zlmHJe6}dZX`oj-zyrDdJo-yto7uQ#@_Ql8V%Z@wetZ}!(O~lhP zj#eFrrg1c02Kzs0B(=dqmC;j_v!2XMjm3LzhwOI4bceVg`$a)$SB!T*cJQ}h9O8)J z>C2t*8yd~@CF2^)u7fC@a`rnfX$e}Yn=Az<#0%7*7xuJV^RV+>P67 z_W}+x-AxZ9cj($%R5$KOXue9Jt_568A)|NVxke91Vkam0G>4R3`SeqNHst5=Oxp!P zK_-y#)uBr=9WIC>O_$0Us1ptm&Q>o>r>lftCi-b6j-hPO9kv(olj_N(Xh?<_CZx8I zp4LuBQW6wvlc*4fmwPs3G#$8`M>~V(4yXCv`qO*gxvaZ!e5LNjm_?_i;Z^*VLBQlb zW=B7Nji%rk!M#Aj<(~MtjnOdoiJvjOE*Mcu`zt`|sZR7RpXbacI>l1tL}?WHc0Cnx zBB6>-I&_ZLS+N(_6rE_s$gn^3D)ULUA~n|w`N+i)z>%`$<^iqNER(yU0h%@xw z^B?RPO$Q_Io0Y+HjFI=o?D{Ip)TeKvho(o~jIlIhbco<#5O^I5djCIRfjVRf9)N0! zI(#SUGUOOaq|SX%V7yes8msNh@;ZLYj7p6Lu?sW%lIX%Q!RTdxSgoFDe{`$8-9ooNy7|!dJGRyB`?jr{&)fR_tsely{_gmnSxbuGwJBNY&_wK)OKed0yF0}U#d%w8%_$IOW znYDl3dvNb$_g{ewKf3#CyKh;4U{_qY(}s+qi#^F_^KeZiovtQ9iI1wB&eiI62MpWt zjcTQv4pVLor(uMw+mAjr!dSi%{n0Z<7&RhHRO2nRP!=0l5lYa>JS|CyjGUB9j4su4 zo)Q!8GvLzZ=qE-P&J0Dy16|VAfQty(%~T++VYAtTk&Fi_uo=$J%6dt3N8Iq?YepE$ zmxMn2n-PYqQH3e+QZl4=@N_7gbv2}#>~)tSiDo#i!E&@?3u;tWCCXdM2t$%G8MQ#+ z@l3ncvDGt~ra!OIJ{)yJDCuBx$wV;kaW$2mdK4L9R9mh>RWCFqb-G)0y)2KI} zEJT8N0&Reo(>)17to!QrxX?+UNpBmd)kdeh`C21Ub!dUU-9|Nd)}oFu!i{W^4`fIf zl55}rYDf?r4Ts&COr~9^w?iiiQJ0S|W${4q;NM0Vm6(ev1~REKo_B^r5m#0aHC)aR zP}0o_N+p--*hpuI5?J-%qazHTm*$h{7MwabZv}ASU~e%q5y_H+aS??U9CESMP*4_~SuN4R&X0Jcg;FKxQY`NYGB;pM?+kunq7}nOsz9%E~NmP_Wbfvg?YKgueTaTV7 z$xIP-q#|y}TaJW6dR!qpDX3P#^l(h`MJkZ4>ihIasw5K(VqSluP^}irPE~LP0tLwH zt9lw_EY^i`;Sk6x#uj2)+4}eh1EwoIuPcMN`(-z0Yx_`}LV0a+S_hAyech50s$ii& zP|wQTQE!Bim+iGEXoU#5qu>a{5pmS}I2B+bh_?|F1HM*OQ3#Oql2vye9ASvH7Vk@A zkV8@EyqG5vTqkG`=Xp25mNIeyDYooTE$LM7@b;}E41u&UXuL-1iZ7%UI)1Ii+sc7n z24aX%KUhun>=AJ9Uek%p+80kT@HCN4Mca@>H|)S7oqmCIc}ut)@g?#Vks&01#|P(0 zZ*lvFPcdwr5+;k7FN(#mkRxf=f(n($h5AaF>;$!DGU}-in#-ZUyKfm`WSD@KD7C~8 z&A0QmtO!<~DMk<2dGN@;8VNhn6`~nJiscTw`_>UgvL0);6~-3f60+!TQ#FSK2gw>$ zs-(OcopdsMjtOV8%>uXg(h)|0j|TlhMy`N|b;($g4l8N38YH1OoJ_a7)s__G(NYH~ zcZtof8(|S82bSn&nn+vE`Wr59u1V#vHYU_0 zl~1;uZhNpP##FHlZb>uT?hl<}xT2kwr{9KMDX8JKiJ_p+(`70?+#c{LOj0cOJ>C{d zYl>^_3nPp+qA~eanCN(AhQj1hC8}l1g`m{yqG&Imw2R4TN@x0Dr?Y2STif{ag%k2k zfSVe)se%8eH8AoFRGPv-J!ZNnR~p4GN%(0fnrhpVf(MS4azZ*wNo5<#RwLSxeS|@l zZT+B|uq#dQQd!CN1-Vv%9gbjJ31ljEIYGd&m@C%;hj`oX8DWG{KCG40{n2a_lj{w| zYs;wJ03swym8|CKN?EGK2rVw&E+78ZXpw>Qxk|rB6u`m0lC3vf7-h>hIZQ0|nkg|N zIi#@NRn`(}?vM!~Ar8=nMT}P)K&tEs3nEg2qBa&a;Rp5I>DwR#3z+#9LDS$FBxHUGIS7gfHI!GM%r)7`0AS4rSm_UCq)C z0wy?n4EOR#%qIBKRF%VRC9vjzKx5eL=Z`R|Z7r5`lj(}$aN;$8J?0b?r4xe@JkXA} zf(d&d5=^Hn0T;LX-KQ9i2rUHlM7rtG=pq^R+YpFo)=RmpE0j!QHkTK!iV7Chi1pwI zqmmAR+_OotS8-&rT@O-~B!zJH!`%p$YS%z2>vprr_mE6NTt8hbRa}Cd_9t@@ALR1( z8K&sdl^D;)*<6{Xp*oT!t5~h$OgFX7Qy!&!%I|kWEvf4DmK;tV4}>|ghvz9#0<|N7 zw!kt)fVi}(?VK*c6{V8m8&pulWh$<^_o;Z)Dl1Z!=OL8c_`K%;gj zgx3KhJ)+mGhI(zD@5F>kq2;JVIl7eGt+mr)Ii?vBD@u- zNK=_wQ}8O<`cLfdy?O1GYadwK-nT~g?%Mpc^+g-swExBZ_il#P`TaK>KFjjI_mle< zHkh?9t^e(D=@7I0>=v^ArsM66C%~Tm(W7_nef{pg?EdQ3_gfN2A6x&0t*=|ZbL*3q z^BcEqd~WmIo6p_-&fVN*f7gEO+xd^3-`u&nBOmHJo}JC@k8FSMR%^Sq_x`mHY`D$s*mdaxx#`n9Tu>$8l`_{*xRqU#6 z6c5(=5hZT-*WwUg&qTwi0B0F_&=@!eWcj!|T#NC^xP;rtV6fh0!I^EKs7YF^p9zEY zv`iM0+J0xm1H`T2nBv9dra!7CKt|MTDQa(}$a<`XHVdR5c4(@UZa3}3dU%8()N19h zMkT8R zh#aCb&VHSinh}Zb#&DADs?H)#<>Rh^ksA-K=0lDg+sSNIM;HsU&r7=lx`|8dlEN4A}TV--#gIO8_XWpA(`6HnjkYLDax&m3No=RgS9}qlR&ipHfaF9Yq)XvljCSN3@o+oywUoD8bs`AvY(iA0 z5(G<%Xcgs)?LwYb7?_Kzn5)#s_8%Ex*aK{`A6Fu@Ue9VNZwv-G2m317=Ny5eE6Rg- z!Mx`2D?P8aF*>x15%H#jXt3#oh6fXf%Jx$=48v_vX9ll#@DPzy9ge(`2Wh@G|9R97 zJ&ww8GEythIw#Rh(HAZl*~q#ykE^Mo;A^F`YDq>KE`4`&i~-gZT(csmSR}6_d<2Aw z*V^q&qFYlnyWXWcAf1!1D=2JCbM5^7s2z+g(W0xRyhy|{RfUx}KAkPQWYS4h={nuQ z;%Yuos_LB{w)3fxR9c`5CU-^X#T|-<)sSen9FK;pA*U?Me3CQ{YDL(uFl zo6}iV$Y_BQoo!iCl%`l|dm|wYgi#!Q%cvd6K)IUpg^;)%N@U_ip~eWIsw+Y0Qn_Aj z#&o3z3MV(pQN6hHrV$3)^z{9Rr(U961-?VX13XboJMs}pbtV!aN+V0brkwF+TVcy+ z6%(hbnSR3=@B6Dzxsp_Epc(*=c{-jxNXeF#b7)tD65XuJnGUc0>xc&>Wl3Ggc9UKS z(_!2Z?A7Fu%ic_av&*npu98YA6v(m-mE9SwN#bN(&!cW#ge9?{<$P4C-3@C9?8t?> zWW~o7{7M0d;aU-cH`x)7pwM-3@g!5IM1yXzFQstVAFP(^WUG~A!C3dooj{W!$TqP# zI`RSMzzv>HWV5woLbF3!Hj0GvHgAtA#4*Ocm7wAtt=kv6e0I<$+UZBY4HE`zRi zNT^tAuw1rNM6)VV3WpUsM~5XnjU-SMD{hZ`NgS#AvRWCeP71yS210$mi1C z2vL<q4wKFqx@^_-;_9+e{K1iAF-Tyl-uEfD?Bna!l8w6{687t%`B*8pNl#&}^j+;wfy!jYmfqgcHuXJArz? zQ|3xtl?LZ?pmG@265K?+=yQf5@l1peVj%W==Q~FjYQb4>>Pa=}rm=1$Nx_YPjS0DG z6^*Je2_MoAowl4tAsY|>wNH^tT zllF#k4Oqf`h=Z{^w7pB#*F7a3v9#uMyQ6GP1LxMxWTS|NOZBFL(N#{b2g`+W#+|aI z3s6M%t;3_1$IC%Mqjb1JNJY)p0MDMJvMy#5g?=;ZD7Dn2BP;kCT*#+e{%V8~?gfiA zyUhm`X?5r>M19~>3>5)O#W36Qq={t*U^#S@{H4XYf=bx^H8?$FK?b3VNiVqX7Nxypk|ahIOP?*jb^D#b8SypsSzTLgqSE7 zOgbZ~WppGOBeKmzHP$L8L>VS!vQ{Y)el(PBL*Ag|3c~hc*PZZ05?zux`px{@nCen634nNIMU3d>4Y;H^0yogC=0^^7#nIpE2Vp!*6w*^ zZ`z572M->0+{4j0QVKA1ItK+fu1r{O8DX?qtm3b-L7wjy zYhlSzkdjTLDP_aHAVc!*SX!>zl)SybmaX4E!pM6hpB+SOwIHb8>3RD#glFX_(-6SQ z2kY0Be%hJfD-m85t-C{vxHp)Bt2S^pkP$+DJ`~5)o=UpG5l@PaD7FY+2Fnj$K#WP@ z-N!~ec(GkWMQ~5!K(JaQX3Hs|G~rcIr_gV(`2<#X^%8NvJwmNUruCDxUjn3tpW+qUj)lF5IoA$9cP`2iU06`iTCO1^ zbApbyO{p>_Dz@FIMd6;xgnYp_mFKW<~{zG={@K2ZKH2viAdKPgn zGcvvU$_Zd}$zb#+OEY@%*|v6rfx`tZq6$o(HEH1+uG|F}I1L7WyflOHho$2zoc=U8 zVHQm+93L-B%0+`ZCaoQQ`doxmD)B~*?a)Ov9dE=0b6%&jOwT!{ zxluk(CeX?2u4)PnbXoB5h%Uw3>G70iq*hLq;1mX|_?*GVu;NcFt|f7n z6;FA~3+=wE!zYhz5MoBt$SCP!$j>H`_eh2(&2KMaWlP1XvCS&c~K+<=DVs zaVe?BeH{#IwbW9D5$t^=e2W2`ECE0%I| zxZV$X{W_W~`@u68(kTXg{!S_%>-sa`T?lyfCDz5#mD>QT;TZkM(yf{pqbt)tx$#rID}gPx1mG$ou!*L*C9-l$QU|=6^gcR+qVW4-blGm4Z< z`n@w!>v+xXeqq6wS6z7y;Cks0tX}7E9UGYw7BpNgyyD8U4Z#s}|q*WOqvXnNNlE6rCAmWV9>KFi0D{^~>j=smW)l-Wo5c zGQOPj_|7T-lfLqG*Bi8$>yx^2jaP6V>1w)i%PX!N8>Ee1oJ0D0FHUV&UUlUNpbzb6 z4*IwqjkkU%6j%rx8UxK(2)u7`j)6%P-)QW(M;SGCg~~7WD+fS?;X>elE-k{0S$Ty@ zC-N05U^$$X-@7!+F-5LW(W7)_AFvuul|Q#MtBKibh1wNhw%9Ye(^$^_?9$xk&lctk z3ajLc*$2CIX!nh4$G?C4#PMUt`Q!VJpL_H-N56jbj-x)P1bFw+*5RiQf9CMp4wXaX z@C66|c<|wa?>~6$LHYnX*th4` z%ymVqk1hxH&68kur#Fa)gXa9}*4Hlwc6m9l*G+)={7#UAfsC8$99SP&4(yvI!F*14 zm4kN0<%N8Dw?a=j zhg+5dd){(j&z%JGKweeD15?i7Im>}PdpR&r=4#A2xIB<2+7UyNxlf9JbOTzg_46kHi=!kJ)S9_Ihw z2(KOJyI|G)f4g(?tsvI`4aBzfXCUvCdbC@?hx+0ly}PApfD>P?ZXeh{-MDO zjFIvN4vdtkYm*sQN6U^B@8Xf-0V*|(6vM7fMmuflQ(_u@Gj4^$YoOC)$2V?XrmmUJ zHqDg=BVayORb(DN06CrydbS0cjCXXUHEw-IIf4o6TWGhdnqQx*BBHyjb2!L>(703$ z*z?71Bz2M}$vg+jb%awnsU$eV4meY{XKI;nAt-8fHR$U#tF23<+XZ7@hV`_yo+z?Z zk_qcj6P=NC|KW!peRy!|uEaj#e9XCiVTFsZ#TPc?c3KcB+R&u~50X`D*_;GwvK6vw z!5C{56_j(3RjqZ;ljD4*>-PYI9yhWxT%E~!SF3jedUlme=SIh`QhI21L7o*Z1BV3W z%zs`V*dqg>Mj&Ts_qrD~hW~^ek9pClTT-K`rjaq~0OO1CLA7cLf*a#?CeN8OSE`Y| zR8KQ)p-dF~922OVggyRXP4GrRW;Y08B&VM&=F|d+I+~x6{0#S>tkr{UKi>${Td`uK z#OK|*=HiQ`Fzm{~%^(cJ(Y~)7?KE29R_G+3^tKSxkAgb3&ZMglQi>%fi=6~%xRYT( zX7VnEhED2%8L9U{+VOAM8a&^~vHzy$tzTHd5SAH#EwwCxv?a#+1kCNj&IW1mt68-P zrg5cK1b+wHA@_8S{jdI*S+#bKX_i`j(3wnk`bz#QHRCfPZp=5*2AgpwHbW5L62Ynz zl%5%y?)AZH^xs!8|G(F1H{8L@(S{J|0LUN~zC`+Hn(X>KC=?9_I}FAqdTt0Kp*)sS zG$L12?ej;Q5yS$F?#5b6$VT$8I&@Pl5CPfF7bMP!)?BSb0;P->egj=D|G&|6BmY0x z>D;ngTRXmX{OaSwqxT(^k8VBu;9>Xh;=#uczWKlf?)bmM3h)2z{`c%Nd;h-o?!5>0 z4t9SYAl&@i)WA&*+|UmzgG0*CRO| zC;56FHeY^QRw73Ww20wBx?ajxGL;O4qo%8s?Ul&kN>`3@;ZBQ+r&KCd?C2(Pd(q7Y zS<6|C9X}NswzdM}2{A*o!$?|7#&9%ZzPws{&uRoy$$p8GqM>FVEfso+T$M5jxc1Yl z5#)NbN~Ej(WKi#kRfLdS`ysFx(H4d1W>`Ejdslv{?uH8<3aO7 zf=Y`DM9`KRE(bX*5olGWboj(vf|Dlb@SkgR8w_}z)hdFLCwaMA)JkRvugxWB-Ulr~ z@WXd>j3!D_DObx$y{MVs z_su0Z{*YlvFhuYP2|>G^dKe@yZQ#j(nc%zT5p+)(wxEY#kkb?^gW69xjEbE3_IvG} za|uq`A#jC}Mzo%+pn8?c!(2sgrU|oz@0d$)%4`~Kf!CXOt)ByVPIWkvF-MoIeeYa? zQ)WZ9wQfVg(3X-HqLoxbHpf$}eb3WP@a=O6PTApN3BP+T!6`cwQXNnxNC*@vnO+Z1 zMDkN+^R}m(;9xGnDZ^eY;ddGL|F^7t-`cTq^#2`&4?lC*IQV7jm#yyox9@#o?^U~( zcei)$-~Pd^zuc;A{segKuUr1j;#{u)gsmF1fOfo9x&qZz@928YGA67#_|!Ci`FoLw1;Mw*f@ z)~(ryVgcMV3m`2x+QiNh&uq8L!ktTmLWN`;)aFnSHW30vAIasLVedbBvggDwU{h;r z7yRQMa(Fy`<5pn8zc2jm93{2TOt9NbbV1(p2wsa6W(f6&|Ixel*y|R3dXMjQc|hcn z#LCTv3FQop_{{5zW#N_+a<(1&-2F#iHm14p-ZN`we1*2~yK|(%2|5~NYieGsYg(jW zUa=a33ux}nY0Wu3F8BY>-kX5Aa#iP|EveP5p<4~XAq>X$vD-FwyL~j2CL0f>QfVkj zC6%O-RDy%hJkPU?eK7QKfG{SI*SN_g;bwq@#7=TUE@4VYNbPkHD@CAfQH?5;!MZ=`bknCmTkna+GWSIK9=gMrs4}&U{;a=LjQH{7(_CX1xYjq=Ovl3z|3+y&Cr0_k3R zB3%(zmvE%J%$jGut-dgUmF_hs(iL%6!~*F~-5#(+x-;d3!JRK%vlR^Fo^{d;5m!ts zYKFjJwV7XT_5BgF)E6|v^5CrxT}{Ui12a1~&WSWY22(NYukepBECSE%jhE?wUvAE= z6id*W%MU%14z&O*7=#Z#nTVV-PF-BT z;pqsP`L;S&h87Mz|2N7$vbOWVo!9O}cAlyEq~^CYCC&NmZ*9M0`+;p_TLDh||MFIJ z>uU9<)W5ARtF@}fRez?ssIse+$`2`jMHy2*OYxZEcN7(cZu5!FcW%CD6Wvs8e0bw^ z8`Q=%@_(1VMP8Nb*T22~uJs4k9qVe@M`W*;#bwt@KP`Q$vAODCrDT>wHVT!MWp;tZTEYjp;PtKp-5)e376J0IoBnTZ>4!^BkJg>-Q3v-x&^*mP<8;RK19lJ7lLx zG4M2`!|issaLc7CL#kRt${ml=q+nyh9rM9;wq}&k!O=@#tE-!{su+ws> z%#bP;k#f6!niRpP5a|hqVgPW7A>CX+s^go47x9KrmnTSqT8j+n#v)R_A)-MFL7hP? z5Vl+@Fr@NDqhtbbS#i-w@%jjfBBdqfTH=IfhiWh?H+lK5xj4 zl0E{nTP|f8(gnM{W*y&}D7($?4r2%fNHYxSf?aWN%$c|9;0iiJ4#MvY07FbOq-%=+ z^Q{SSph1|RD3`}_DMgb?f3%2{Z%tt$8VxvuaU8Z>N;0HBTtv#ZCemYz$3kA*3jn`> zA^pzO%cbWtq~Bje%D1LSkaPrn zUb`Q}rV9+|_ZE@zttlD|p;XLob44tdp2v`WcM&Pyn&3DJHb@B@=CfRSE<^gAMWlRd zLcw8&2ug9-b<@~z1Up8Ad>Fy@U~E=3s9Z_Xz*S^0WJ?Qk%Pl5qr> z1j&$oV-YFensCw=i(-@`j94y(8PcyWBITQe%N7q4E|0?x+8Sa=zqW{!ZxRtZ0;8T_ z)b6)j3NoZ$T|~;)s~2^+<5a+Ii(4)c4Cz-Ek@EG5M%|b*fH?eK%cTHA`sGEWGkV2H zI~fgQ9?(`lL;9sfqlxFD@Xp@S_A03sXTSPC9}4`xw$MEF$IW zHRkdAsHmTGgXr&NNI$=bly6OLnfpXjo>1P*_@}mTZ;33fOcwu0}E{60oi%9v_6!rU@WEdp^Kuk`C z^wWz-`PLM4#ZWhl_$i=Q2SfVri%9v_gxC>0SBTpGBm6YXbdL1PR6> zPSA0{4C%iuBIR3?2Z_WYE;||Y>n`29E=BjGpIEkJfRI7AKjaQmiJ(7%2AT8!(#O`e z-k|uUjVv-p%Gc9J$ap8T=#%bT*Ji2! zho877?jyd4F`aYTEm*E>D1{o@y%MH1od-|G5B4hdLCb9~nQ>bk_Sx&~iTfodA1wyG z)xbMkCG3RgPX_Ar9= zYEY`GY+EC-fP3z0hMy1=D6FbFY6=chsmU_S;nA9blA;V z>jYMG)`I8T-eA>|PwUEF?-1z>8ZDhq5839P$^?hD|mK2lb?Sm_@%0&Yc>hPJ+JC3C$GFLzVmy|E(-{< zo!OJVdIK;%PRv=bi^v~U(NgE>ijJ7vViQ;A26tVzI;t6m?UBtE#3JzuLiN0rAnvR* z(;nj<4qm%(`1*OjS7%JK!F|dexufR^d%p5d|2CD|uKUR}jpu_WA#ma|bk8;AP(sJN zw05`-FsI7{B{Ic!qmmrflGQAI*mjZT83*enJIPj}+p2()wQi+CyUOgrZoYcwKo(wnY2 zna|E#W&~Nl6&>lLXVIO}D{cfg#LT$eT=y4tmy&fl9-Lgnz+G23cD}g^5as0yjNrJ2 zi8Ii>!;~}7Cc<+eddD`=>Q=IC`s3=*F(-m?R}>}~xDia$^+?QxP|JEfa4{8a##~=L zZUgO;jc%$^1fdX=newe!jnfV0b%Q^tNO<}pTIhje4RBqMxriwg>w?QSbm<^&#(R6! z(w@89ABH-}$nA92EzWd}3`Dyl$~gc#OTn07gxH~6$(r%@Ecu8fK1i9ocJKLez>&*0 zirq*k5TiPVdER&b{q*_&Ti1VoP4VQ-k8VD^8Q#2V;}aVXZzML>zi9|2k!tpXG@}fvpTE3UG+uPqpAzaAA)=TU#|2i zRf-QQ8vmuHk!OuLlfan-ezp>@$-yHu(sM|tiQ>%>oE;_OHD|e#;-{{&kKoMSGNZWd zIdbsQi*OeCZnoV?B-80^yRFza$-%o?!imR|PQ|k)brK#gK{i5EvRoKM?3E7x05S`k z`?QSY-OmSgUI29pBp#21=Z~XxdhR%i9{fq(cdG2U9Vf&7U3bXA8$QC(ACC>%lJ`!O zEwJIU%9i~3L|+0~jP)gX=RI=5Dgtzxo>&yYZElHVrI>6>-h;}CTW%5FXof1=KDiK% z5#3){<-|UX0(aCKB)D|Qks6h%-d>S^<2h?Joq9FZ?$m49_3uvXO&}%L-qyc2F$jUg z%k0gddty=#o%if-?_J$-h5|&;r?? z2vq)^NrVwzur)-%+fQ@>q(u=yc%5WM>L71g^`FIUA>Hz~iRe4p|)%I7O@Q+!XcrubXM zuPQP>+sVRN?Pn4=lfan-&LnUqf&afqKqt#ecdbKh!#A|Tj$R+h4qV=zy*1KWw6S<| z;B!|aX{SEdLeZMH+b7k3%TU1Re#k#;q6VinFrX^cRf_@&Jbgnp zQ45tT*+iV@_6s(0 zU;GsKH26Vt~uBce|PFq4;P(WsE*!W*^)wgElF}-;F5{ zyq8EsGfpy+A>nSOSPO@UTB}?}TupBWH5am^vz8tjD^*zXTVo2iG<38hNI$$k$_&UT z$(_@I%_?b_?!ceHC&7+RZZ|saIvW-AA zvY++kn_BlMP#QV9`54s;ILJXmU-OzMuQqG;1R+niEd9ZlBGao^yrj{aG4AJ4U)}2i ztsO=Kx}LRPNO;|5Z8DU`>$*|1FMa=n!W8umJvn3D;W2jm#;&7b*thz!wwB!l<4t>9 zpVOkqV`e6;cTZ9O-SE&lYR53<-ya< zaTRH&-$Cds*^UcyMlFs)qT}*e1~C)TZ@EBSF+5DQdnT(k=vKPM6sdfM!V~y@Cgw7S zgDnRU3GBCZ5qJP@{sMMrvBiCXUJQw1ZyNY%Zmf z;het`9u5O~XFOP~6q1p~elHdSl7sTrM&7MJ#=?zasa|1Jt-A;Uyd8o2bS8IFZz&C| zx>Ug8A)6k|lISP#RE4~1TtyVN8f#D^)v=gCP#=*=tN`*$df?VnT_9+;8S1H4GL@o; z2&|}%DI)!X&enB~aFb=fQGpDjPC1jp3Wi~vuoV2-lnsySeAWm76NKNOliPVaVwl9dE-CXg1;eph+5CW`EQ~ zz?N1oZ|dtZ;M`EUQBRl33f6LieS5@Zz}ta>J5nPZ16`$zx^*2ABce&WK4Wh6d+p4= zm1t|6y@@?>sv!8hq z_{G%GF33hozv`TFcj^WqW?6bgYV7elKy%U)4SJA{#LM#GE=|qk`Ed6hNGk*UC*3%7)(?&XGF?v3&`YTc+TGD0@}mnsqfYe=j*mTOHNlw=ln5q>b$RD zNAl@3yjRj&Y%OcUo^oc+H|!{z9m-9d>Vl{L=vV*l{Q4ZIsfKvXRFdnvq~4s<-NraK*@-1TT9mO)I0 zlFo+PI!5~-zvpTvjX|`PNx>sLdOp|b5MFN)pZ7@0{r7_mF@tz`HtS(ihjk|yxE-vxPMdcA=>f@%mYHbV?GzGIedJW=oQLCQKOmvowxZ9u9~4>W znB!K>Bql?pdasKV1MRlAHmKqL9uaCeJy6-?(pvKkB9({D=KP-4T=%8AZKwlwZ!AyXAt)n=l{=W z{HN%Nr!OM0ZdSI!^^&`jE)9A(R;!t+Zph@-d1G~bER?NuQ@U0yXU2RWY0& z-hss|CTlR0Hd9eUsR-A*`aa$Z`P;!tJVvBOgCH{3&6xdv#b|BkA9j9or?=zUd4}do zn)hgaS(DM2G>Ywi-hLan1u(FE{nod({&wr(t;*KjTTfCyrv4x5htx6kt*RfYKB9WF zs-tqKo~HbQ@?FYTDPN$}E7ukOq<9NpdiHZBfinr5N#INZXA(G*z?lT5B(N{Lu;9K# z{Y)yk9yV@ls3s`T}HvgarTHR}JY`ww8NL79veGkp zfAX@@Gn#zT5>4`fY%k$#nDNPFdY>(=S;l)t?@N))=zVJ$?-{+TmzAE;q-t5|8QmyV zns=?;zV@*-Oj)~R{QCObKp)n9dZ71%`$u`Lw*l%F7EFaQ>g~XUn5!}j6hlgev^Gm z8S+Dm0EXUW3}>#+KiBMJE3m5)Dz}=bqgp|lGiKvi%8(yM1Te&wGMq7NJdIIRcPd|M zCq0;dWJ|aCGIXCdL)TJ<{OERWHA3f7hWw6Cfa^JyJoIgz;n%OVih)tA?DkbNVZ4rc zNS+99TgGlCGL&-dLd1#N2jyNfFmMc_{Fit4ma&`mI`BfZKZ&3jJ6ue58ht9A%#$e| zx?7e>&p6ZCY`j#UBH-2aOg$g5dt31GSXljM-30 zZ9 zFx@?Y=#_P%XvYyFW8JvjT`gyu{C9|Dy>8!uI!a>U4~D`pSqq)7X9shzfg}iONDf4lp3Y^q2b=sW_Zt1hBG7A z)d=ri%5XX~fIiPn4aoA);qvEu1#cr$_QU)p-vuUzZ@%ma;!lNeS(B%p6{nY)&LOZf z85{~VXfilDg+pJ8@;>l$$XA1v8%>_|0# zq^W7H0ek*0-*#+0vGqq=DfK_AU#@njo>2YK&IeU3)m_RTDE~(JkP=lquJ}X63l(?n z{HkJO^ZlFM%^Np9wei{wpZq`N?~oVeH?IHV`fJzWwKwcIG{3EhXg0RrzMb2C>egRx z^|o%(JRwN*Ti3|mBB?01MR?nmx?Y2M1 zzo!a5u**>5GtgU`X-gkg3!89{I!SiJz5+U(XeTYN?opjm93zP7HZ5k7I z5H`lN^7(b&0lWm$%ustJKltF@X-r&Ek1+*E_W({7ky@&mjHHr%XP4J5-@!dgFwKl= zR`P=n?w-cP74;aCA1hWu%^K13=IgOgGnkCdV7hAwrkRo3N`COcbEYwIMLot8YV|W6 z`>=!ANn0fD?`9n{nC@JRX(}n1>Bj}|yJHEznXy$MG7R@Tsb;MdishV9*f-?iyZgW@ zyI@%0gHy@M48ExgiWm{-4=gg&z;QaR?#Du!CA#~-w4_$9+m36U5xoAuI92P^<{Mk< z3=w<)P3d#$O0zLht&k$!X?Lp7jg^DVT-KiC$;)?OSR#L}b2G?S^1J&$zeEyT#~e#y zM!I(&=$0_xdeH&~`UCB>D7j-nR+MvUJ-?(@uH?tHa-EOisXw@Vx>l~z*;;3a?m7TF zRJujR$_2^P%AE@_Ea8K_X#?PjXIyEh7WCJPc3ZN6;~ihsQwz@+z^zLRfD4(yu#z8q zaLY6%ZZpQ1y3R%@Rj11NK?L_Vad1n-45nu<#$@7(fx)zrAAE50G$wAd#+X)K%m_vm z_8)g-;XAl#NxNo(-AYXG!Huih754XD+bd_e(=E!Q<$c< zlgF|Sy}{RdOL$@B|rG!+G$MOW{okGf?a!dP;!jicE2apw`0T%rfZg9nhD-3 z`N0Rzn#RO!))r+6Dl6riZ`!b~|9`S7`16(W^Wx`k zPzM*TU68EB)fjDvK06GqQW^9py+fOfTDxv1^Y3=jX$5N`b;xgr`$!4y`@(o5?u>LZ z&Pp{IjSSt+P9;m!`#w9`r}UMw+nK4Pim=sfM?HY&$R7pe5t}Zpj|^kPs#S1O+Xx4d`aCHI{V6yD6)O6&8`ySjRP-eC`}web zQ1lwpwr;3l9}uyIo-&&H94Wtam6W-C&Xp!*d~`}uM(Wm3ksA4{j{Qif-B0S==4^$S zBV~KpmQ7fjL%iiK+DLGHSP?D}(UdEkFJ+z4n#rci@5jL8I8w87ZTV%Zr2Ny0$(Lix zc@*HCp+P)eE3`=iHi*=SPL}M?kuuV0>PU=CM2E#;!`jGFU2Dri5qWThnkj~N zt-VnZZB4rItCID3Ih zPk0E9lrOE4GJnD03L0Y=eWw*M?CoLD0Y;}`n<1mca!I6a#L>C7T#QGvMci1nG_@9@ z94`9ZzF5+Zc0y)L!(i*U>_t07nyTJth8r;sR!Nz=GI7P(@@Yj32k!8fGo>s^nZdZc zN{|^eGHlPWWu#T0j03oqOD5gzXba0xo&wZ~?xQwqIG0X@7r!{U@!W&t!dc%>ZMZCc+6Vcoi>91$W@?vlRr<`N z!|5nd`p5{pKmn$XZZK`7r=7-Ls){qyLZVU_M8Oo&8;QrV&PJPXTJ$ldELKhHE45VB zq0Pjwird!0og-r|M4XfD)^FD# zDI`urV!=3xQr)0qZqFIq560@%crIftb!msOPwM9m=}oz0&}+;G zefChRW56OrECOR?NM|!va!@sj2eXb?(5my-z>r?*yaGjbT8?S28+=7$g{nmKNqr|5 z0~aO7Yx80mQnTWoV5*VAJHu)-oDFwFdQaJ9PMPsq-3#6QSDK{{UvVM>Glx^MGDo419=Rdh)#&+4?xCmL*pIvBZGpgc zE0)FF0YkXkXc^=2RFJ6qEUv+RyXLfk$3gb>7L&ad%vLZv2>*9qc_PD4OJw$pK5pga^WlJ#L&K4o?f`_Z!ADJzgY_^xo_`?IRW~mL~xdc=P z;UBs3M0R>hjlpm@N)AE}YclGj44DvK^*JqbhO)4!<}0T3SP=zFG~FDGn zzFM#o)MxD3nmz09RJs<*U>gnVy=JmD%C``0It@g2FFpT{OJBdXZe72AeN*;P+3RHw z$kH;c^y|_;+WGv>@9w;Er?unXxmxpi%|B@VRCAy~HP6;OdHad&zubP!Hn9zDKWkgM z_2I4mx%C@csjVBfHq@V2e@Oku>er~N>O0h%s;{a3Q1v?1P!&<#p!}Nhnz5!AdLKNlPwvGkw<=lk-IfJ#wcYmPDnTy*I0~`Pb@|Wi!mh#BY)FD{UetA zNC>%QMh#IG)Wa+}3o61I&KuZ2b05;!BG@V?AIEpwFUUi+%t$rNN|i@`EY+!_^kb=p zSgB4(I8r6q224J&4G6N898Qa6StS9Mi%{0hdvYe&g*x=;$jrHhYtAF*p>Jhg3$wyB0C|mvH z6I;EVW%sIi?CxdRO+Iqi*;sayPb@q5D!KIBj#T|h`i<)Z^YR0-bq`y|E9cem99Bt_ zkIOxQ@4H)fvlhoAKQU5n&>c(A%C>4k!fDkVtT9eL%`wKi*oIGvunoVHZR0EEwb8a@A#D1C* z%WbS!E>5FaAQt{M?A9JDmIqjJRxG!&ZM=V)>xo#n-N9Hax3DBH6i)JNRxJ0iBy+@a zGh1Y|xX4YcvnG$*fmz~h zXCakn4qKsg(-g%A1M|RAw!xU*+(<+RF~UWiTX`xNyaQP;XKRUK2E2@7GFs{9dFH(s zNk3r2du;9c-JuISaMg+Qt${=fEi{@%^FCD{fFqERQYF!b2HAwCgC{#=s9V0CejAAQ zGK_czf?kwb*=G7x7tUim3!)kK)Dqog&{XMqt&^>Fn8(A$Lz2sTP|wOdEVJxEJ^Epu z*@xH!J+U_r_`Jt}CXHJ1DwY+p>K8uPNmx3)p4Z*zCH=-`8S;gQy*L>PRmwRmAIDAk zLcUoH^g2}6OO7Axyo~qQtRz}!rOgdrt1z_a`;NAuZgs*CGSrpqfo`vovl{o})wtO@ zY8E?0I+PkVblPOIF$%bX4x&XmqG+<-?x$TbAELu60XR@`3VV-@Y5M*5%Yf(IJn_7g zp){Z(woWo)to0&rBZZ@0l`JN}1fgsOY*=Av$#~l?- zjP$fT&+?P>ym`Pc+w)8YGcbv{Pl360b$!<6*Oo1nbf8{BGoy}{auIl{U_!K}9%=Fo zTG~2A*oNTgLE5e^!}C0NG~md%;#j&x^qYgc(^c&m<348`wlthLRskEy*@ zE8X<5vEMi`b`i1p^tu_t=^Fb_sVlf#W1orCwB`J?jNQPzf<4#Rz4b^lM(K^17mFJk z@pjMGby}>Mz9+p$_S5HMMjX#*)2Ui42_Ck-Ok>w};6|rRpw?*ld}W9YtbSXKbVUYO zq23SIT{<12%h>m7F27}%aSi)r{rO_k)$x=dxUpvn3{0>ovzN&?L$(lvlPKva#=AQE zoOk}_89Qh?Fm`(Wzq{63+i7YZ)ATf|?O)$EZT;0&Q2h}#uKJvcQhr(a66KQ>uU2et z{^90r8z0yxZAj%Wm!DgI?|MY`gzRCNMfwq`NAiT^brK7BMxeK3;>7usI7Bw2gkhUd zzxv$DTczf#hD=^5H|vZLWC(?teu}eeLl0Cg9NIt)=Rpmx5?sT{Qp^c|Jp^f6IebL$ z_)~{4;D56y{^P#rgtrzln3ImWjemP-;_zNpI>NDY*NRSRSR|d`;XR;+b30D;D_2X0 zv#+#kF(lYmRuc$EBFuu>gObC$X_;Iggm4wIjAh|2*V0-&X_*c(SW->Di+>Y#_V6x% z;U1tD0WD)O%&yX%)MJ1kT@(wH1Dw{#^w@-2Ed>0 zQ+`n}|M?!NhYZ$O26OOljRFBqPpb$7xQ7H+v2?raG6EcRsG~7}W?mAEL1TAnP(Es)FJb8f?8;R7h¥R!pW?xt>4V z2Q{7tH9jb~Mvh~6JaM}&r8V?ZRLqw2^t%Q}-Vfyxt!g@v45tl9DQU+rzc-7b#lzb` zO$JcYiv-s+zT-4PcR3Glq zs=Sa@Uld%~a%bUgWHO_HTsi8o7&-%o!(8xm`;9>{Z^Vo(@9FW<}8N?{MFk3!)-l6N5mU7hk|%3wCovQ;n>)-Fd&a1_0G zcne(%aG?8FuYD|XpqX5Q-R=nL#f#>cEfU+e#-rM%uh};{S{8k}m+`s^He=oft|Y2v zNq_y~;j>u;FFZ{Il(qrxZ3RhO&PXG>@2k5Th#_H5Qv1PhD{6_=TM>({Q+BkCXt{oP zGeAH)(0zgt%yFQS0ZekxY{Q4@Fq1x%GS>_Hcn@~@YGq%trA14%4{uAo(EpVcG$8`lA{s&h8p3;daR{-Q3^4n8#Q4|SoD54_G+2@$zMf0zk%`b1wB_7mWBE5DD5`blu{+2oGCK(Y*E7=cVyZ= zN7L)biz-dx}tP)S|`_0aAdy-L1Bkgmvz~ShxEY) zRk7M~c26B|RV;X;)G6qlZX{?o58yf)ija0^5OaG4`w6K8I(gwLopAHY>GA&+rIXX@ zPn^L>3h9{Aq@x+PXR7I_F`BEAa}N61ogQ>Qi)6e5pR1xHz20tn1Q#tt)mWzt?s^cb z5+%&6Gc{fHe4U6!LzLa-g%QCzIsYl3llxZbgge{6(sXiq=T-aFIO5Z$T*M&jbim-T z5C>Mt&(n#wrYkqW?LGEHyqwgfQ|aoU9U?usPDeW^58a)CA>2;8_2~dOE*b-v)zv^Jqg6WLP9U#1ot)NzShQkHmOA!$wNmrD8ZcG1nDP0SLe?5-Z8c_h8@fg^ zWe=F_sNP4^FfvzbX@k|i&X&g-`C0Mgv=j0beWgrk-i2^>Q=^(CY7HY|NwlLrBwp=it+l$*7t-#x z0%lLit1IgUt|HV45Ou_k;2|FzMg_;N&wSu_fldaibi!Xxx}tP)TKx$c(FI7H2$vhW zP`DEaIUSB*Svx0oA+WZg-4E)z!H_Xj*@t02Y=aWPBnIi>S^*t+8o`dwo=*=mWulHo z!Y;%aj62L{6g$$h0lFd&#;{mef>x3OpiFD5EPvBzT zkcRQbey$LM8?9@Uk!$Kf$y#aAcS^h`E_M zh#}>wn^Jupio4LN3Ec5-v$yLy%F_$AZ1Di0$HJas*V)h7>X)CX<8d}@g+e^yFKVN~ zVOtjq8sV_JRkEE=Q-ORtS!`so2$mXjJbgzQF&nXXNDEt?q}D&|XH0EV&fjl98F;T; z>KoCbDd5qbhx^yxPiGEwYjmPqmQMQznf)_qX>`-+?B~wW-o)=jEbFdbW90eW!t7`3 zJiimM5X<=3-s&T13V`RnaDxg*6eV=800KM!ZriIs3j=*A(~T5b1|ODz*T> zqni%=?xp3UTWP<0by^~(mpQfm&S076cVf=_0R4&Cp_uPSVmm-y9yWLP=4aG{(RVx0 z`M&oF&$|Z6!CEb2Dl}~(lg%Z&75dDP7RWT)1Lu#%4B1V^TC$o2AC2@^_D@soX4sv4 zo89#U#Gt~H5^4HCR56{bB$Ab4lCF!(JIS0{V*W^D9OL1E93N9U0nX5;l5{#c^UrcI zU7q~AmCYp6bnBQe#age}p_{`9V3Lq)02TcNyLpq*LN|YIxQUEN8<@sJ`A9vL+(Y3M zIkaK!JzqJl&jcD;Nb3yxA~rnJ>{>26++cxjx!OZhP`T-9`-Zu4^Za?z*2Ci!6Ily1 z3Yk_Ihr!+QshHhb(&ejVOBy$OkXXBP9y0nN3L>+JznpV5Fs~VjHH;{NA_B9|>86jv z%`KB~Bcks*y>8w-FWmgpyKS-kzKC%1Q|h=^ha3L>DDbtjaP!j*H#%tHtRm%w8%P$f zqHS>Aanx(M8bzWGnVKP})lC)(sj|Tu$ZIQws`>H{yLc**RF!O3l6_N`>YeWy99dJK z4GymCk+os6rAORQFR4n(&nZXXuD<`K&~Ewma@UiSO4J?UGeH%s0j@vMCUun_W7KD;O=2tsk3U~SMskTvE@ z;wfH&^>GQk6@o0Ws>22IO00edynOh;bZrZHoz!N5AhWwnwd$8q+x@)S7V$c%%?v>% z#7QO5%c$*zyxJD=I;qVBLB>j`RxOFH?c(8m@@y99E#Qxt2$vTHFDf--28$(Y1&>$k zS4gCm45uuRxtK7uNBIHV?Gn4guszU^)=kzxCKK2vV%4(8Q`#Rj;wd-kk3*(Z+f>Ic~{=PY0Bp^$*U$D1xp<&*W79xWlD+A$NcPsk+J~jvyUHL}UQPem0 z#dmZzAo?L%FgHbn_R&>bTX!FBhP?VvM8AcE`esro|huq}7eaeUc< zpyR-^@RZi-4EEe%OH4(Jj#LS$s41qRMdy!JSMZI+z-WfM z4a^*tInl?WQ%$Scmk(>x-HvV%uanvqouLy_Ta{PaB3|6u=AYgWQd?!Fw)wnHYP)

    cSRg-5Q3mEMj(Rgu81QK zLJ;J~2t;t*6>-Wz2!h-gfe5auYe%2@fK8eOE+o{_2&Gi-(yp0ukpbc14^=SiSUn@i09`AcE`ZZnj=LOpOtU zknHZMZaZB(OpXzV*hD_v1^A1HFBl^b(XQQ7U6Z?bm>44vu{$hgDSx#ks-&SceC@pw zO+|B$=I10IlzmqA4%sVY37K9dmws$}W9#EvZ`-=K6_otC^!KC>Y-%<>rRXW#;FQ3h zY<_j~eX7@}(wa9Y{=4EM%3Bma05=6hR3?>5`3dEto3Gz2Y(7W*0rjt`8)`)TRMi($ z@7{Rn#&b7r-;l^ZDu0XoJ~<9<5%|{n->g5pUR}R;{VAHimb4`f$(qRtK^85Uzxx{IZgFN!}$Cg+gUh*@PC}RKY4({q42A zV+Ae$#{}`J*2&~`d0l8>sk|nyO$s9+1kYBR#WEHz20MdRdrB61t3q~_?5auORf1DX z-Y0pV(84OoUr7E!Xkn$~&n15@w6H?*Udelf7T%P+NAez_g*U)M8t-NcN2BpbW9ZGs zbHiLCS_FBFo)8axGVN`EB% zkJif+AT=wZiUlI+)6}>iHBA;Wbcw zt@>J)@#+97Z_1lO3$M!?@`hrf6Nk&44LOKmx0`onZH@sy-YQ51IpJI&;c}&3!;?`@ zu;wkNTRg~e^%vD&6k2#){RQG@|L_cDZHwIo9Y|YH%?3=O8Kw>+AG>{dj>|`Udq4LJO~}uUB8s7LKk8bW-Wpq+gqO>&n7km40=?c$H^wDB_B^ z(86*BrJ#fsURT5vF`$IstA5 z?D%*dDA#;dGZFO4!t0u^XucwpvrO}4&6kB1mTJDF`O?JpS7FwCQ8Nk7(R^&=k9ot{ zX407}+8qV{O;mEt`!tim;>yD7n!nIY28$~T%QSzknG6t9kFl_E%xnyhkH= zthg?Dr{tXzDX$ti%Orm$`7@z~rC>|>9h1VVqQ-`LSG_xF->PY#>Q2?2YiknCWgGW@ zIl{B~|LkJH+5A6QDb(AYJLtoMXY>CPS)9%PK`QF8Wt`3bxzaeB|8q>^Z2r$RjkEbb z$2897{~TeQ&HuT=IGg{UbgpOf|2j8>p3VO`j&e5t=Lq9${{LgGNzmv2pT71dYdbN` zqnfqtz}ElVdWyQK`a9JP%KH@mOJUeN+LUZ0<$o!E*7}=dKavflk4p!V?@K5^^0WJS z@VcY&&YC29GqJX&R7m9V3ks>24BM%jS63(eO2PGdpZWSW^ZSn%U3SRLwxs9H4_;7 zOzvnk+iruS>8q0{)ij9DA27eaaWDO*c4FS6+M}ZauX5F{#W=2`l7|NSNRp4`pNy+MtU?1NVyWdRD1k9_P&nlrYVr zJicC_y&By*jq4w6%{j<<4e9vsKA+m0^KxTd^SM6x-HYmVQp z#K`#Vd!I$i@y|cJNRIsL#va@~dLE5;%5O!arn7iC9=sAK zoIXM(CM}-D$-N0-b(T2e%kOxIM*3&35RCM?qxh6%(!MMr(OslvGG6`mERFH)?-7jg znj?xoE)9O$Q+n`TWNqtEp^jb(%DT z@vSeRG5+4woFK#<^-qFOgvK=$ghXWdv$*C2p_Ok|GC}Cg1{&jUetwZ0`7aVbc-s*< zgIC0vfW>%MUbDd9ebbLv#cPeO#I6=VT9n z=Zsu;k3uszMVvF3#ko?R49hGxgswY%k<+qe=#Wxhq&980NH@>n_m48WIUjM>+QT91mUiw+-P`V{~lSIGve!zhD zQ+jk~9USyO8A&Y~hjL5I9}BJ?n3f*Zh^=hVz)nbI+hQwQG*}Z-*%rUD`9eAoD9#1{+=%#1@A`U4E z0kFXU5HTPZGjtOIKrR}aLOu>hz4-VDp+Fxh+ulk zDtP3gXaJ|Hf=3=;0En2Do$mSL2Ohbf0U%dgL(u?ESp@^p08Uv2eFlJtsh*f;#a17}f8ddxXaFK^Aru0j zD;mJ59{zsdkq!eu#O(F7y`U``z$tq{OEiE}_JSq@K*YrSw7sAq8o;S~0X_fMuf1Vy z=dC+I&G$9Gsqt+;vHhlP*Vfm!9^OLKPpIFjex7_xJBq~DPKmNX_+gY(Bt$?n=` zK_y~-u%idor39f-pYvop2;~^G!h>WV_d9F;9B%>fc!p@yLMBtVT5NWCCGI@BA22g$ zR05b;XEsT(qZcwzlme(H8#IswGC@QGkMYl>Vn_Ee%oHM-8M0lt8RxYNI~p;}HpMVA zLnb}dB~k7kKfMl%9Ss>~8)BH5Ad@aj6k5$0WCMnoTnsZKWYV@Vf03GD)@PWlpTmk| zvX3~3T0N6)wA=n1|4j3-Er93FjaHkRr1%$0Vn;oOnM4dT9b~iw;=NXuhwSd7E?{QW z2w`T`C92&0ZDzd~JL)iK)=mS>okwlJ%&`3<0nDu2Wq%e*i-xlOLjlwiGdEZvqp6wg zc0)YDU`I`c*?)>*W`T@G4{mqy9UMDqFwA}+hM5^Mf@jA4rTh%CI>YSyVwjmAqao5s zW%-Bvu%jBo?0X`a>D~EZvC2bs=TQ|fGi-l%_06Wc7Vd&GtG)^Y<=X@j5-u6 zL<=)e$_$hz1TeFhIm9fK5(DLN0o2D(7F`639Tgd7-x9%W;XSX|QGsFhO)<?qDK`;<6l^EY^~BZ^`6Niob8?Wtf#F^1Xy5yNcJ_DlFE3YcZL|84ae zyqFD`@KJA%^Wg3t%f^MKydB zWY~UO09z5Ozu_Z-Vf(Swt1n^_O2S70hVB1ey|O4K1tfgrXW0Ie0Jb7lD#J%O!}cEq z2tmZ^TlmPwu>Gh2v?5lNRA5g2ahZEmLtdVYibNkcV|G53v+kd+KmhCrezkK_??aKCr zZG8KlZSD5C?VsD0fwu%cv-M9~e*;bxycMh`{F3VTRBu$hQuTnUu6lur05{7SRJ*F5 zS8XW24PF}fxbg$acPihe{59pPloyo^Wl|YbB1%Ykqw=ZBO~rQjmH2_~ORDZv5TGdo~^cFB!ac9Et=i4oSP@pvTnVk5wqjg4VdaRG zx#d4C-@E*SVd%f z4}X*H3{ZOH>MC%W|LBt|obGf`dcV%@)ulk`lK1`n`?@43J@Vm?>1a@T}_fg#%C_QpvO7}cadgN4GcLFF~^4>=- z*X;tONB+;LV4gX?PyXdf-E%?d{rbnR>i!Ls9(l)_?l@3-bn~F} z$neFwIkjlUe=q(6-7F|QGH|MH29zF&iMnY}dL&xYO@Y!QorgpJfd0`ZU;KOM@6g}- zq~ihT5$KUVX)}PcYJcmKrUvvd^l+avybt;x=zsd;$yY)TK@auGla2&=ZvNUQ*B*lY z0{x{=9{*182K>)`^4S9PC+JUo@>#RcAE7_?$s=9{7QsLC$yEmWJ@orNx%?jJchK+p znyzJiLU%)V_sQ=Dp`SuO?UOexLU%!T^~rBNANmROlRkO? zj)U%m?(CDFPeOMO9=v#e~`3Lk(=$o75kD(i& z8wSa*LSNk^?}WYrePxrp1G*l%zE3881$`O%@+Ns3bPaUPCiw&Ci_jN0$y=c>Kws#S z;hUk)L!a-H{tomx=yQG2>x4cFeYQ`!{~ZKu{!E`d?N89v(A9nNB@EamUezZJ2y`WM zWuH7rfj$j=dXs!P^hxNGn`9dL1O(1_YLm=8C!Gm>9Qt^Vyzl1QbOb2fU+?=G4fd#b zpZqeV3xd-7^^4!q1wiSMpSwZl2c<_|{U)6clpcBIOLZ71J@SgHbzV?<YlpZO)PUirnNAefx?4a~W z?yEW*C_R!rNoNJ6M>2Vx1(Y61e@ABqrAJ2BbtX`HWauwC7?d6vyhCRMrAPX1(HTJL zlJ^{KhCT*;Y?FKe^kL}3o8jV`{c|~(EFhG^~j$;LW2*X@9mQhe*$_B^qxNXm%Y%tp?CMmKl~AT7xb<^`MWT5 z06Neoe|-w{PUxL|@|PTVTzW^Jyt@Eh3|-tO?>ZKGJM{KG`JPk#Pp z=#9`D`{Y&6hu#3ap-*0MBZ$p@VW0ds23-JM&?i4~E_fPzeV@GiT-ywnr$XmL z=l99?z8E?WIt>*{p|ktsd2Q&`(5w68Ykv;C3VKzaeD%kmS3=eUg7B)Ph=llKUE@KuVuvPKKILvq#?dr?4&#O3yWZcbhH- zN{_tnOkEU|9(ng=Itr8?dFT1M2q-=BM=#Tnp!CQekh(A^J@S@6>O#7Z`u%@y`tGR} zW%1YZ0{G+5zyD(zIB(&AVUZ=zp1nYs-p*UMgTKBSkvP8I64OOBk^Ko)qgBoGEZ1u+ zBc!ETj;$5N0{F!QZk3NpF}sbkRZmSO`E9(c+eo3kB_(Y)g2LA1*9X1Uv!O*+1o~6e zI_bS06_u65Itbk#cPnkFh*29j{0=KBuw>a$!@98(`-wx8Nwj#VU z7Q-{1sDr_B?kKx%E5Rwrm$dlusPA#-b~X~$R4Y<$TXr|Y!AvogNd@b{FqyP4hC(N4 zqHrR~6n*Y`&FQy^4i_)9EqpknbOaX@Agoj&A1rg39Od%TxF=OB_2%WYaG&uBnga#VY}BEbD$Q<5ny;%NQjZJrR{HVB7f;QKsG3 z(_`7T(Q2{){D!mi^TMBg!gjJoZOj==qt2xVE7-gbNw=OW=U# z%^x^bO|E|7QPbNQnuD9;mZeZlDm`-LgGa4=PxctgZq^#`dHnX2?Bz1)YP&{dtJVb3 zYS~)$vJ>&cwn{qDqDd^dF~&nyyj>&N;+VsinN`!9nrwJe z)qKF}m0EC>X>bN6thD3nF+^PF+=#VU$T*lBX^b1z?T_tk8-ffv8d*0Z~fiZ2_9)gpzsDUrt?w0ue6OkoC)W5uM4OnXhobM(i7ZW#?f#@Vsee*ZUPN8%!EXmD@b87M+995 zj;KEWL(8*Ms~4@V>At6Xt?oqdviv+~_sTUZ36P5DRp6&X{|;&3kOmHE;E)CmY2c6s z4r$q{ z-e$~FSWZImvR&Tg7VEh@63)BwgWHOdwt5b+Ez53)0gFlsCTewv1Z7Zs!fT` zRN8KDFi`4b*o3uIa~HC^q8OHsQz0r*bdpglf{6YJ`jp(OUzj3+ogCi3(gHqZS#1Il z$Zx&=dhlDXgCiuE@V%!jOgT2{cG%Ox(EIW2)rZ6Q-rfEBEF-R+ZnHjPa_`Ao`#$!9 z?ZK}?ZteRE`?8L@BD=l%a3itzq=hLEtn~TMUb7E|yW4pEeDK?KgTlF4vB>61dr#Di z?77>yEO+n=SA#9}CI}ay-f zx)14EIxjc|_}}1gMFiTl@>}pW;EgM1EFHJ=SKx&H>vyu?RQ~MZcNhP2v9kEGMcu-k z3m;iHYr((p>>c;-xN66_;6(k}{2%78o4;uO<@4)vf1kT??p<@`xfjjtoV{)KgW&u+ zI{VC-pUqq`bN0$9%MUMqWBDD++_G(X$I|zgE?bh9oYTJsX~-{}K3$b!`sAsHfQUC7 zY{AhzYx*+><2m&E(@+D3Igqk^c6#)8ZJTj0%Cow#&HP*gOpOB!PMsTopKSnU{xJ?P zcqM-WaJL5d`#8Yh#rX}uPc^_JV*rM&pCxAQ(g1%O2N;~SHd^?J26%WJVDJX^2H?jU z;D5#etXtN*nL9PWL*oFJt$;f;z+cA!2BTo z4lp#*w`qVsjspyh^ba(^A4UVLj-he5RRjEf9AIdqzpnwtCGl_!4csjn;J4#i7#g_m zX@K910}PGBcQwGT#{q`M;bsl+;5fk0IDAI~{AwIvXdG_R01u1-JSsTChVgw{1N?Fv zU}zj})Bxk+C_gIZ!A1+;(g635Yhh^MzNrE38wVH~xNm5Hd&dEWJnrin;GS`Sp^?5p z1N?j(U}&VjrU8C74lp#*U)2D2j{^*i^j9>%PsadkLv!o(8sM&RfT5B8vIh9cIKYrk zU#9_nJPt54(qCGen?Yx0?%X_mVjh%YV_!ORtyb$D<7yo;hif#z?c)GLqx(e-@S|~n zA^Z7)Cikraau4=RllwCpwa%`LuGIz))%qI^usjYhB=@g1z|uItkX1gY0d|f93|Zx` zG{E9Gz>rlwpaB-f0E~mfyD>ZbQUmN52N*KrUub~&aeyJKyk7&%jROph+=fW^X@J>r zfFXmsR|Cw90}PGyJsW_TTSfz{wxNN$S_6D<9AIeRuF?SC83z~|xGOclxEvkUp>g=M z1{jx4#yT|8|E&RT93zr-Xy87j0lqa3Ff?#iXn=2y0}Kt^CpEw~#sP)~?i1iS!MLqw z1+9gzk85Gbr$4R%ZWsp`GL4UEfUk`M44KA9HNaQL0ftQDBN|{_gn!GBY5bQ4xPD9v zmLb#lum&K}YO-$Nb6DS8Qw44>3Ol z8aTIpz^opfx}Y)r#Kx$9^chC00UV4Od34G?>gd!qVw$KY=e85ld4qo&!A(Y=Vcd>@ z>S4bvXBamVhN$8U9+^654jvV7(6fv7t@DwJ>b|qvI7-(SKs~@$X=M0f_EAR`)uZ*p zXZ0U-`dO{=@f}&T89)F$8-R~IDHVp%-SH&fUbn@foI8P{e$+1RPMR!!n>U~0=~yac z5$O^Zk-JC2!2v<0-l(ziM$fj~A{c#S(ORj=05-2D5;v0zCzuq^2X) zEN{#*IqZusI^Jk7Y@BXYvFRL}XBxbEu|}OF`^WXqI-#4jM`Q#R{Af{s?f$Pv^?Reb z&EH0zh6D|6a%34%5{n=qcADXfj4J$K-At3IGwSUi%Z7}aJX?g+CW1}DH}Dxoy~eZg zXStXy_x=W_p_#1OyNzFpVzXFRyR%Vw?`nm5e)&n9ZY;L(cZISRGix{2jW(aDDkEXK zx$g1d!S$?-kFHaOPQ&dY6ZNX%G3_=#?t_^rq#d>}*}Ms%ITp#~BkQu)CfEEfCYEeq zhPX2dhgccMBD=XvxEc-G;H1mU$GpKp3WrlQnz0!e(O;_OQ?Y8$W2z+^7ID;_grO7C zHiO!AFx{NnJOFPj^X=*FIPDWR-B|zG=|){ehprNAXTBP-3~%1?c?u?u5%*m{7js3q z!ZYoz&!3=rsvb5N9l(L%w9#&~84P->ed8{I=BVjLjR)=|Hh5m0QhFW*_tZ81&cVd< z6{za*F7#%HY3$vEa4tYc@q(*Bh)5xvDHj`Uv5})Q9ln)uM8a8fst8AYEKalF`VEjl zy$P`0g`SjC5pS6Y@o=$OY}u-;FXP7~bEE10Mh=M5F8^`;ngvBq%C zm@$wxA62$wm13S~qGpC&N3npliAjiHcaOLYG{W=Lt^?0Ir+C1qdfw~O)7#mmK5?FB z8TPypzBIx+w5*U%(XzVXkl@y*T$DC0hniJ6HYAcB>*nh4Vms&*#)CVB+RWoNb`aMa zz(?+C(YTOvb+UMypejtUA(w-2xL!j-?XW<`Br8@ytbWR9qdSe43CSVKXLt!ZN^)_S0wfy zt4c6n#_+h%`x8r`nem?e$LwG?X3K2^%s=?i!g2ODxXf(WpRoVgpncW;wh>u>;`WCv zk==hZXiv4oZG_yPxV=rAEbKoDw4?42wh@DW;&ukD^X#4bpQR2V*eq-#F8{<$J+`54 z{Z7X0KT?$ibbA}Y{3n)0GuiO|XYTNlyH=;x?51=#>`2-*a6U|%Bf~LY3t@^bF;ivY z8AkCo23XWD(CWGL^v1)k>x}(JY?(pECJQ)t#QKqw`7?Tx`Jl!o6m*<34K8-w$^3B8 zjls!$Y@&gQyD{uSr|&-uv@$rE8^>iJn7EZq$2nvF>eiuv&7)HiOkDe*yS!?@4%9z5 znH$GtDVVta$M%t}pN13rAyoq4BfZc>#&M|)CYGQ#nXjnc2e!V1yO~qWLIT0P?J8>G zOW=(}&Q|qN&8W{Daiqe5fL%(a@&Tve;O`B_+27zQ)t$_jLHnxxZ6hJV#O)7TBD;SH zw5M9)HZmnl+}@^57WVH1?WmLaHooFb+|HnNg2{YQ9YQdfZ{tJ9#7#Z6p>F*I$n0NG zWdYsZ##_^gWzkG_Y38R>r%l~4HT$wfGj!RG&(GaE{k7>oL(S}<`xd&e7gJTP^~!mZQGb0;qxv(rC+JDPo;#@-Y? zv{-gg`DjL_h(N8rHJMpg_KP(3I)f0j=yj&VI#OdJjhZ;R_BV9dv&n273m2I{M-CKf zG#?wyexb(Rp|Szdm2!E)KA)6Biezat`|CCK2A?52tz4MyG@67|tro`A{#A{=*+TI| zG8)Tun!$EFMRc}a$?A6XdW~IiI>{s&E|FeQLOSjWx^;KaWj||HoLb+8adl-6w`f1+ zZ+l3kSs{ZdLLS}Ky2dV7%g#;)!~CHrkx<%VH9neMoc-405T=dfWvX4R_!AznOp_k} z)+BV@t`_I6dK|TPs-!>faYc9vb%z5y8E%i3{pDI$+Znpal2R)i=Sr0ThLpz`a6)5m zro1H@rAf)fNHNuvnbEbA8hb07Bieq#gXS2g*VE39^Cn{clE=~2dbsTJ`keux8l(ME zq$Vau3**&<>0r)6rV~qHGDmn*O`mhjOyKNxH6q|y$?s-ejSAvcs!Vot?Jw5Y6=yl- zL`tEK7;3j_!7R7+T4i@sZ9CQMl)~HqxQ9L=eLpRZl1zVq0V8 znd3F~Ita@c6{7`rsf0A;Y^0nRE&Fr3wFk3VCtWI`jEuG_GR@aVvp=WHo-cL^yx^j9 z*{ZAHu0?C3*`J}YcN(rLD@h?SpC~75WUhsbW`{KPX1!jn)u}dy`sp|hPOwO$*;h1n zg=@40zZ6uj{(5pPsf~_iU+&5tC550HX{F?BA`rx>p)8MGy)QxEeH^1IHK#EiqXmMhbGe#@$jRUnm^)3Vt6SrI~VT zv>;t;X*Dx^6_v9}sP3xz#d>~g2479=sq4FybD3t<@6NYyN=}8VO+GhLl&;sc=p^1^ zWX_jDVxefmi;P}>r+%+ht~5pP@IuvMe4Y@J5?dTSeRbVJk(+eXPfLw>8BJwf9@#xo zl&K55m5a4vxI>qHxR8qyl0Ptd>+o+~?pQ<0H4CzU_^Sbb%;_3!S!Z^AC|Ioflg&bt z@OK!I@-;`#j8pILR_^2QOrGo{Gkz%(&MEEUNXgf`+(%YT~ZTUEP(%Uk`Zc zD3L=&R~Y0fAG3}gt#Q}OOsVM$U{VNAc;yJ|9KFkwyWDO$9Fzm0V3-Zp(L_WUJ)SS0 znwmLv8ynt3tp8tYU}=V%!lqA}0%=Z;2U(H612Xx&8lq=Tn>l*=x6?OFzi+xV9iBdE z=IWU@uim!$iPiH#X1~RiyFfm_H?MFYtKHGdzXiGM-nZNW8SGA4o&tI6KCyH@c>1?5 z9R@P`UAyyQkjD?(c|6G8_npPd7hk==Eu6M+^p4-|xB=wfYwZYwtb0@Qw}D)H=YvQA z_W8r+?gx4GE}pA^Y3{a` z6KBbpU6dFJNkzAlsSrdR7Xsqe2^%_ZLcVh+>#r*G$NF_Jn+|p1Eu4 zq)Em=tFJ<%?Rs0`q8TJ#Z^xZm7bBf+LOy8W&C}c@?H=O!=QN;=M-3+{Zrl?=0}&hz zDnpz0h1(|N`@w{Kw@%3S{R#PQnUL>$6Y_m`LcW_PA>UOK@?AM0-=`T(QkDHJ$KOtXkLcYw@)b!E+yH==wjkKEn4*J}4_=J3i zO~|*p1iJT(9iN$6eTD8JU1Q-(AYrkv@RF5Ztgy>JT|Q&!hfA@YH}Aw3zrKh9rbGV@ zY2c6s4r$RGdoz8T%1|UiT+@l@rO}^E#+hNHCw(QC;X*kvYfGd7!u}+ zRfn}4w5HPbwGAp zc!JNsEEo6L(=iXu7?4R^pXTe3uWI?;}k3+e4uzr7vJ1u6=S zr`kDxHJY@GIl^Qv7ZnjjtOvdZm<*dmv8pJb< zOm(83ijUs9k)D3urVPB#*6~%dov>1O5SYzjVV!uKvrurH3o}hyA(2Z`KD%ts;A>pn zoAE|rSI8VIaxPAfC*Taxjs)bg!x6Hk^q844*_&-4;vjd?1u26UoFyXbj`+n09US_G zo!pe6Oe=9g;4&2hg=9Kyk3U^CyEBn^G!Tcajy&wocM!J_uzBrkC7yBxC2y;M$Q6n~ z8+K2J%Lk+3mg1$-7BU!UH;h;U#57P0yJ{qx%!b@evKW*@z6@F0`jLJiw<$w0$R%wd zj^<&5C{bm+qjwkNiZw>IS<)TwWktrwH1v@~I=kl1iF}RYWjg7I`wdtrQnEUttwJu$ zdaA_|>&pZ=chwiRy1}`Bu@!8|tqzDoA$0P54ITPwId}f13&Kdl&{D| zI%jc9cFdOua8%l1H@WCaC_&fpH8MdEUaB35z-^iWUG~DUf{^#f6iH?(`dUtaT`s}m za2AZ^U0yHU4q~}DUFFD9IMN(CYc==6O&M}TCCh|Mu51t!iq1xkDxy)ok}|v6_dy7CRisvKB9I&ZT|D6yw_!!pp^Y zw9yEmY|(?z_1Ms9vh_h3Vu5PKYPUq}k!ao(jf;7ZI;>*HIv{yv(CI5s3TNu%EGcr$ zE3@%fq6C*pH5j#VBA3e>qDsRO4B){asf0Y1N)0h9K1xpR%899XoAL*}L_HResRF(A zY|6~8Mj|xr|IOPt&f1!7fPcKpAd#s%9G!DpXjzk}+kiX8W3_%{ZJyAz4ia z6Qo${_(Y$nVhkcqAFkAC+JrQ%{umYXq&nGM)WIHd=PwpXj{TW7Zpb(m9k51 zH{9TCT)N>wgK4Fim27RlEfVvsv9#S{G;!5p6^X?PR$M8R*qF_ofTNA55c11rhs8^< z0e`ev-gVG(REylE44FhVlnEFy+!u8toI`=lHaS4Bh=7@$_C`3;QGzvC3Nk@uEncb?1Vz#`|OB>z#Qajj`?MZLk5w1j{RiBt{`*zV;CIF6Fg`1fahj;^l+}4u? z3*X+9A&x0A%_dYcYYbgdivlGJ@Jn*|nCV6EIj%a8;eP$~J=JFgX%4 zlWkMG#q(7pR5oPFwuU23Rl-nbyJSmL(_YE1>^kVV6w9U> zNPSAFlrjao4^3u^flLk)J6JH0;UZGQV6_z31Xaxop=P1H*0$S%sW@E^CR%J5WVG;* zj$DPSh%A<{*_yzEnS9=o3viBLc^BKJsCdJbexBm)Z81F zZ(4ro(nptEJAb&dx^w5^I~HHKaM{9XJ3hVxUA=hqB;BPtA9NiwxALA96O@_L&EGz+ z%>8~En)>+c^OirdOf22Mv}ftKso2K0aPF0}hh7#u)f?nP?H|&>b~PaEGk_aM|GL}2 zK)%BUWn}g}vwP#I?)52zyCMgZHGNBr*4qs~)|6nx(rL6AteK&GykLwX)oRsu@T9=q z3v}9zr?aQ`nbbzmBiEjqE7$71HfA5(?dX$tPq(no2-^5pT!Aj^+tqF9Nxz>5?K@so z+!;qc4esY@1Nq$UKt6eQ^nk_wo7xDl*ha_POciakGfwLrsf3}5R~4J5W{0!QToB`& zmV(ojYzS;DBpp0y{kVIjLsz8LjtcvZ12U*nL&;=DDkxjQQKR1qQyH7Y>4_$S;bIwQ zY6i3QOQ(`ry6G{5o zRtgP;m~gI@bHNQXQ3-cSm;o_Q+@HZavxR+XNC&m0ZRAg%xJQF6oek|%qdKVGFS}W5 zz~}MXQ?i%Kq^s>3m91J6M5|?M*~?DE58Eo~M2jY|@NBr{gG6?4E5mJOpY;Wig0?)zg4WZ&{GP!)*T*Ngwq z#u9e49#Ial`+v0t&YwMCUm!{5jW68!M9d6Z!j!9#IY*@ju)xF7OHV&xQ-E=y zRzMifvrdpX2DVs1aNHA|2toQSu88F4*WE!sg}Fiz$(bbxTdL-9M|L;inlXV}EsV8r z!k#lmgh&0Cw7Dg?xwfcnjlX)(Ku9X!;0Xe@hOtpmg9!q(#$~QpQT3ys6oc$DB9G0M}$oGR+3ZH!yCROudPh*u+sjid%rA06UV#_W|mpR{xm?pDzWJ}8yo zy1#9W;=#yz#ac7CYK+Ga7aTtT}<*t{Agw;`%Q zXIWK?XAQ?Q-st+CJ?9>Fz@|d+hiqIOo?c|{34<7q_K9&L_0RZ(!D3Kl8tD`6ddn4U8t8T% zOC9fGnnYfKi+!TwyVQt4Ffj=EQQ+&*E%TTOeQU88Bn;wu-Cd?;CTcZ`WD zW;MB6q^*PJ?YW2$pm0kd5cE*dxHH5VJT;4u6n5_pVg`Gz8jTq;QI~A=&`1l-COnfx z*EgzN2R^aqfOU~2&o*6Pntoz8m1mvMjfe+)GSBcb3-qkc?P*C&XIPHm^Jyus&U%|! zPc}k#slz#PIW+)3I>*c6WZpkU|^BoeDED9+(_5NGkm+gyX}o;OSZgJFc<20*>54s#iA?A+Wj%k z%dt-Bv0YQ^+F+s`qpGoFG!wJ1>qG!^wJ8kdi%F|<9j+juddO1eT~y3My4PVd*mB{v zhQXT%rLsJW*E_{#GFi%^N!rVbvMQPWRs zthz1Ble+Yeax1fAczJsAeP$aW)81Je#ICk%oQe=_w24^Du`Hf5CU5@7P20} zibfGqmIIwq)fvMde^#q%)+<#^*%Ak4Z-dDb#X20mzHVW|VK|({^Tx2P#+cA}ne`M+ zNfS5$gf@br*UDhlvXieyj3#t9?q;%CzK&$g=8BmjY_uD!%j$~o*qcLx+I8TFOVdYA z*{3dE9;=;qdUwZ%D$^ko3F%;2f ziWu#)NiGuuBADf(XvN%)QVs(}#d z(^83ZR?Ujm;^GNiV1|j+ct%1)ImMX@l}M-9Xe6U8QIZWxE7+_eL3@iSk`dXg0T+JX zQx1}{8}dlA0;2MiEK17}qT6iRY38W}rcVb9%-FuaEAw(X4pt3|aaR|mTIF+YQ1nCrNZCX2&n4!6htCclITWPb2#x>y7Lvxo+IUdI- zPEB?D*lv*$5olN(k&aw*`@j*sMuKIP5aG?ZF{>kOalFf38 zw}TrDo(Lpz2D=hv`Cz=fEWDFbO^O0rm^w%xed z78X&As=5n?-2sQ}Go-BD;aKV_C*sRE6~ff11ld}MO|>FkKiQH@1#cinnccOB1$PNK z49<|vE^uDh@&2iwKQ5E|Uj*t}4%(_Cp<0p-MonSNQHNn;8bO#eT$0>!iEhW_M%&8S zXp=LmjpNE0U0^IFXR4l9*i(+y>S@7tQ~icMxa-Mhr04LVW=ZHTF-nNa;gvINHn?42Zo`b{mcQj$|dkBN!PhdIFKGt)id_ zEy*lb?T*aL}13zwY;oWWXt-X6<4ig{GIxWVdvcV{)!%jS>x3!5z zI9ZO_Yivah*Q(88s0W<=UKi+UWXm{L1`9FFy0T)GjHR0eS6XtGaJ(4I1aVfOol%zy zEp(N0ei!Hz>}gxELGo2&EuALw)ls7aOB3BYQ`KLqZbV~1NNri^oN@zrzgYN-1TCk0#b)5inH6txih=K zN+VX3jXXjWGQ6Op1j9}b6uQkEhToV{pvrTGeYx1{fy-0Znspp-Y|O-fvfTY_jS z6b~R(x1PYvA} zbk*{H2Lj6yOE+qUu3JsxU0{~YrF?m}k4uDzB32MMR%lrKQVR)UVP_!>E@3I90Fy-O zT?3eYeHWM{Oi?&twP2zzE)!WUS`_1*I1*NX6Nj=DDq8CpgC<_cb+^{@UAKzRvfW|{ zlcF4`rcCi_!G)J9wIUi6VoJqR&X<#xgr3eDLtNKZr;%=pRN2GG0R`cGY(chH6GD?M zE7^23->HE`vM$BpG-I#EFnf20Jo8`=$cinSgYjEp6?3Xp_Z4u5Ac}q_D0(D{E1KLi z3+{%<4SR3baak9Lw><^Xptmc7Z^mTXhQPUX~JjVz$DCr zP_`wr)00pykH!3?Cy%vq#&pNgwbi*@U0|S7L`W}|b4$r$g{*`bTeVgy2V)H@UKGIr zdnZPOW1hGv)U}+MV>BSRC&1-e#!}Q_GW#2Lw>8x%rA(e+jVsiGHY`&`Sg+j)69%~3 z$DQk4AX&3^98PPc6g1~SFlWl0@`1}qxezOvqERd$`%9i0xG$BicOM^SZtMbWo&+N3 zsRWy3Fh>fbP^m5XEp$*$1VcrAC{+eeGIn=1!ghg+7pPm6r)Iyx5*Sf0r?3m`AvdD9N{E6-iAu^FZ{)nZz$D^rpSR9~aODc_MwKMYmwnzGpD7{^ zpBoK;Iy*@L@VlJGEF5ps>hEqtf%JMB8Q5IMOGqmeMFG~I$D@T7(Lt18G-<)36*&VF zT*k{avnJ9S0#_?=WTOpuqYW%#$$DdjFy-WuFu`X{xY|}5Pi(XW>b8_p6p+C~ctYhI zxYd-jfwn+a$s8JQ%Nx8Y5%9tmE{Db2Dar*b)6zo3Jj3|pM9rU11~_v)p+}vLXdX-{ zDHCgD4Vj|V;6o&!>Sy*<{ho2E-kNJ7;EB0het*d4eOjp+oB)49RPFW@2{=~?CoGnb zz?pO2dL?SmhUL+!M!acrO+lj_yTHoXcBS3qK{nf(mnwLj`n*jd?2V*Mp$#Py*--Tn zeO14EjH>O9EiXe|bTk?95;3pmX{2g%=!vpwl&Ih|VTlX8Gi2f{gxBFA@SM|Hbse0>?NIDS0lZjBw z7kOH#`iZh?jB~|`l9`BmJeeXcL^^&V5gVmyBprp*ohU_!N;6%n6K#vWZKOK5FH_(t zGS4aP9936H#TX5d>I8ClU)2Z3soJ*1s(nsR)a#}E-sICs)kNnBnn1ku7Uik9Q=XiR zd&;&P##th9^C+t>8e|sM+vCkfT(Qd%?KTrJtHB(NB|>gHYVzcmd{xrR#&&AMst@a{ z`kmucJ(x67{h9*4b%!V~Mh4X8pDxRynJ7I$sz!Nd296g)F^iwUz;%X>N6r~WEsM?q zRZdx&Y*q5QqLQ^L!{Dtzr%`X9C8sqT$+v@i#pYy8iC9!ot$MYu>UWG&wPlM{qf|WR z_D1}^;L}RgPuN_Y11}ZK=BUSEWHDzk)-mR36FI`Fof$ugrZRbNich6d6db7}d51Ia z4oF0ew-c38orA&6`H0)r+OTR}U)2|nQ}y71Z`G>FM1o4Xok7I)v{LmGHCKBh@rN<5SywLD!ZF?$3kDl<2PxV~0R&Po`J{+nAWMGm^{$FZ z1=XsdzN+6oPSt})3{+JoLbx{;Mal5fYFWgesJR+dgp><6WH7SDdOZjk2T4Kr!YEY- zMPs>Ihe>xjRc4IYOjc;gR4kCM*A1<5+gERyqH@*;XTy<=WpSmi>iy$XJ$NHRRdocT zQ6J(8C7(|FYBP9;@x<(_QQpFo>h6@liy^FnB+GUiK}#c4?Gh_lgFw4|riLY!af47I zD2$V$Kb9A3hKQgTodthIpDQ))z{q*X)R!ghos(s--tIu8Cp?gsGeQ@i)s(YCZf^J{<4)h^Nfl%m( zmAh9yvGU3leC62XUo2m>e9q#`GPS&G={HMXSh`>-xpd;rKY?%l7wt^%)GwMB|F-yz z#fum7PtW4xKBlQoN>=na)2T0>x=M2$A{`34#(WSy8((X$<;nhq!A2A^_cE~*s`%Q$#q z_CWy+0Wz-ojE1V7gsHlb?L@LxtKMrt1-PJCGNzn`Xz5dRnltGQ&~r6Jqtc>^1;LGj zQy!QR%}caRUxft{P8lK$9`TTN@Ug*db{deCPi-JfdI}`aVX_`;)i1{pPY1UU9g8hk zmpyG4fiWdL9;201B*4oH=W1;z?F=H;t=@1mPt@cpQiLmFB3`USJ#hn(Y&cW6zUFCm zOn$>cyNht72;OA&@gm<6jNTw6xqOybqm`tSt+E$$@Fq(OH-*}U^4u|oS$292Zu%Xa zY_nPd;jK~;F`tIxnWmBK1dRStwS<{{ITIX>Wc_WEvY<6lPx$ym%?!e_Va9?!jWtvL zLR$*rb{lT>gc76))fb%+Pc9ytDQSpG#S;oOoW@XAvD)kT3~tiHZYrF%AP$U6Tixzz zA`+JyG=t2ZuOTGC-7ZAyR#%J!@9lC?J?C*bVHcg1!)~};Ofz~8sL2Ya4Kvqi2$41U zE#O;v(&Y?ULsqxbUuq{rDq=9zlc7?^V9ppNEPxd$W#LB}La30AaKH>ch$=yoHIsAJ z60{BB%`p%XQ!vA2%7z4xAQcdjGbeTtN!i}vExt}Pnu{4-l8}u0;7;7tHaY^}OQD5! zCX)7mgs1VD^E3o6+hb&@gZW$auz_s1VHevpaB@+|c8YF46+$wFa1ag49KPf5Z6glT z5QTi+*GV9H&f=woFb&fCrAo;>BP7~|T8v1Q4eqp2YzQUrSI$w(1F*<%l)B#EN93Ct(anBNg4=n#kmAtvP{ z+QA)c9tUcXW`3?A!ff0a(AUI_wHgcr4H+^Ho2y~FN6({So1e7Cut+$Kl?rHK{tnHd zLJb*MxBFbVs?~fM*Lq3nPHdk`t;w~atf-423&HEDiOqx!) zN~Mex%LTAp&eKS`VZ~5@DKulZ$O|WT5pIjuRVrllbypj6xYI(^ROjO!jBL0KoIX|- z>+u=_=I;i&glY(%V5{>2mbCb2xRfcOgZcH3X7~ zl{~0Br)NqgSDeZTwX#&Q+X97{#o)>3j2N1;du@1u#CKkD)T|LKn**l?T4N2gW!S|$ERd3gR7<{Xz{jgtC6`UxA zJV|feY_XPsJ4i??&RBB^y(3bV9A-~Yw3TgwD@GY&OP8zP|4mb!snt)eW>$~TU9a1# zvp^3(Z-E>j%inn`r!Ieg`4!9hr5`W7Y3a0`zuWoAotd2{EZ)8N){E8^eI!{1I-NliytU} zv;V<0Mf8~5Q7c;WcFcZ0ABU`VukQtG;@0=;zaNBFtXdE+ssq45Lxjf&}NHc;;fWs8IN`w|hGwUTx@aX*5h zaJG(%VXwm;G1U9s85zJI;9H zCG+tE>_)|W!Y(6WQ!L?XXBsA0w51z*PsdZYG`v_m87Nq4^{gL*Ws>ZkLfR*tGWEzU z^eG2Ex95YudH9P*8HIa)dS2``@4NSN|FO2`BgkC*KyIUA4=*Elwo#1O5s|T!B1uz= zGloMo)*fkzdS4+AZpVXNALFWZ4>&&S^eN*vFY#V`V)^2mv#(%o`NMymb$#`{cOSp! zqn|0Bbj;Ex;|H=E6}R7j!9vTG6+_)!|2Ace!Lvch&G7~cUAGt zyOv+Ns=wX5>mMKb-8Y|q(o3KD-v>VSl3R>-tbKh|eC7S8{V8}LGe?5K>)pV!n9Ww3 zKm_vD0k2cve`<#mU;OVo@3x$AtMnV)Tj~#<7>l1Nf@b3na_^qM_ndG1ZRK~T9bo1h zpgM;=Ee!dsR>j+X@RQxUZ@+l$MCrzp&-wL*2k!jr`(6>cp?Gr|d-=)8Z`SL`ftSzy zyIO@UE$7G7ansQ!|JTprf7$!Sciec&;g{I1TRraY?|oS|GyC;tJy15h(NjVWq_sM% zppL=UiyQ8x$#M3F=_|jCU9*=z>WaHAzWV1U?Rog)N4?ax=Q(>mf9Z+OJ>~rBX$Q{K zx&n^sjj7@_XMXwPZ?WHS&0&u`@`fE(L(=ZI92dFkwI6$q_~7g#x9{Ek+2DaQ9<5x1 z*=mgn#cE@ua{pTY%KN`a-FRKxd)pnaFcqeM7Wm6UD=WsG)Tuvtul&|Of9)j){$tLh zYG!CA(Rw7G_$uE$Yqvi0Bjpx*;e!_(pKhP<%4b&BuenM3=5^ou)y2mK51jream;qJ z)l60hIzCdI&rR>i{o(#I&!?_EaMTaKIsc4TRxW#Y>i+W|{^EV_*mZkteJXe$HB_C6 zZ6MXu$m;Gq?E2;#z8L!ad%y6PTVAtt+EuT<^5rkP{}tap?{l}k@P+d8ue~^UAo=L( zY-TH*Y{by5k;b&nuHR>>?tkB(Z@ufM&rLfX^7vl2m@mKSzrGR={`TShUwBXYH^Bq+ zqpP!;twxsQYNHMNtlG&}yy2Ex|L!^eh?oE6we`il)k|>GMV|=ll@5II)o;1xlfeUt zM^|StTMe>5RNNXYT9fW`SA>XHp7y{S?mGTEd-mLO-p#FB&TqcloBnG4kH?-zvr7T-RU~pJ@?#mZ{2&&xz|4X-^pvfdiV7W`-8R6XI~1RdEt+b z{ePc6=f}P&<4RPtcWC&Sx$GCe{nA@LhkfY$Uo;=MB4GRcn?5&wMSFi|KXvx`i|_u? zWo6?^M5Lh|K4yM${KL!F{qf0j?myw^_RD3Dkjl@ZpZwGP?|=F;Z-4IipY5N&@8XrP zIA%0M$Bh4n$$K9x{qdRK-B$bkT;%q}A1aQ!G9&-VtByT%>~GHe$=MHhRze#!qk(lu zAjLL_ftvY`SB_8r#vXg{_0KeZdENb2oObt>E9e`(@)UX0pQCR+2c(Cw_|6X`Moqy=`yBxne_+MA;Kj}xm-|t!RZzRVjJiC1s zNB?!yq8S|xEk1YJhr%E9ruTjOf!BTHrZ?a3{@1@v{PxAyF|T{*&eufl^Q`zbk~6?M z7{mRA{u{+ba#s86K8XJIL&&`!_^R&Mcbsy0uodqcenOU}OL7!|1<%`0B4-WO()Nc?kd3d#+U9dC`}i_T6;Vbn@il)s=w&JDBTbC91Wjy>_x9Vfq2o+p1x4EGWH*-v;Lg<$@~5ixbVBR-+$-q3s&4h zS8j;I>gh`L+h%V4^p{S(=e<8W@Ekh#$mR2jgTK1tq4N@&Hg(6v^3tOhthhw{P0K@( z|1Ed?>WdFu@vhkAkNJ{c(!c%e->JTCG2dQaxcHk_U9bJ^3}}vXV{;%_r?I;7@%%vT zeCDc%?+f-zA5ecc=DV+Y*U$em@V5Q8oPOV9_gr@Iiyr?>>-Gy)9HKaz7Nw#%ciflr zee7$d-@I9N)z`3(-F#Di@vomfKFO?n<&pc)-+b^V8qbP-qd009MiXAlH`s;z=4WsJ z>nYY(r~@bbrnvh%XOPd~k00}y_nk}L@X$|Qrd6gd@T}N2kkg_tj3ml7hjC!*KK;!v zfBoCX+e3+$JJhk$&VO`D^X!+%Cr)yfymQR2zyH+NJS*0X?f1X#ytw$S$3OAKXJRWRVRaVpC-gyw#|kHy zdICZa0@cPks#4Tk!6F$1Hc)AOvyWCbylbC*|2O6?{>0B?D@JkI7qJ9d@v0sT;CMh4Oq7dmcR|&urN9LT{uHdwH1#Ew ztwPyWb6>x#{^kRPbDwx>-$g$^@g~z7u6yj)#C=Nio#$4*@!qGCKRbRpwsNs3aY${c zg?trL21U%SaIWF!^g%S@)E$@G< zWB%?}9=_mBm)-EE)1Us+Ol+fJP! zPf*?c%%RUbbnrC8_fGl3G4DHT>4Cpod*Ivm$5t*72&+iWg5V+{J zpPi&I##YW32#-)0STwgi5B>Pq$+-LSKfLBQ-=3<6{_|Ad)y@Zge`xu}AN)D}!Q*@` z`^tGrJ6H!I5Do24(#^>3kPT`|>T>ajw_vzmJrA4|ef-zwJ@NRt-&Vh2cPIRZOHb6jH|kl@Z6r6W zXqKq^XTJRrWBlUj*FE~ysY8!kcKw+@In(^w;De|B>ehFh|K;p=&%1$t{~wb-BAfa4 z1POlZ{QIZXz%|^;d3+tP>yPcJ12$}=9JxARy3K`FdQ0y*tw7YbK`a4N**4b^H&jkM!|s9)#ZzNq0auhhpAFEV*u zCfKoLmo#lhE5P_|CXLk_F9y6=)C1WyZZ~9Zd#%18s2d^_QXIKx%-^Xt+mS9BGq~Q` zuo1zRumL&BIZ##+l&V>;-o?)=>(2(3r_w+=!_YD_i9j+B7GR!wtKx7FY=&0jS)&>a z5iPbu2bRFib=4@*@cAQ&=GH}>1VMUhI$>!)ZmjasHcL#f>ah9L>ZCDRP6h&Qa3fC3 zOH<^m;oO--ve*$d;YIFhK;`G1@WN3g^G2T)=SFuy3`kp zXP@uktC*%6Oo?dnL#($@)!)hItO>vmqZ*SbQP~akso+&THEB?K4gg)kYfkvGT8EC8 ze6Xt-HUQS$$>^>|i$J_g1MA1>;)p(+tzu?IV zlw8?j362B|RD`Qz@wl34+k=))CRDSK4sXI_lv2@3!#92Z{x>YHpuDSjYkPyM8NA9k za;^sY=X5pO_<dM|C z%vU8`bF7!|>E20F!OFY`(eY2af7;M)f!zADfy*Jla2+DcSLdU$SSgKx+d6sSdufa1 zo`pa1GK%u_2`M_Vb-@Mv48|{kgkOtPwz&E`DBXu;`PKcWxke*l~QHIuvqe%6G?*g28=1NH)rb*uDlUsjho;`!j1S+ zTIXTA%NP}ExcqtBqFMvhV!<{N3M}R;Mk3+%+2a1tV#@5oAy-&WBo~bos8K@Zoeel{ zs&#N91m>_jGI8I(_WaLsK7|+W$}cQoYl#9S!VgU`GQx8radmKe7hI zz(DDV1PfmlF{CFFEPPDFke*1e@EQ@LEIq3H2O`D@QRUwfF{H zi2p^zkRBsGcaDfLLX7x-i5O9rNms#=UW_G8`k37UN+KA{X3SU(KpnT6<}6`@C0w%; z4WqJ~NgzF{e8wyCkRDY&W)?A|N0m>!OT>^KRW5sjh#@_!d^K%GdRY1FM@9`RKV1|` zdRX~Xs*52#th}4p;gTFyKI;^Dj1X3iix?w>l|S6Y2pOBLXbLs7%wc;QMES!+77Y{~ z8GX$FV*Sy&PZzP*Kw0jp{1+mI^g#5nSrJ2eAo^5P#26tE{eMIZ>4E4o*9{ItpYw_V z+?xW?=YA?a*c53r_bu_krkJ0(Z;B5#1)|S=Lwv9)5Pk0JVlzevME|TvYE$^=+}Feh zn*!12zAEyN9*92sNs-j12!Oe-hz~Y}(#~~fD0R_bvG{5stmKKc5tkm+Br!Pyq_3QC zdaOE|LDh=YgH5xc6rGQWq&9{0&3#D}SB;XjJeRDhV_Z1|sv5SFv@M%=&?L&Ktz-f& zTkFYGES71g$6^A8rc)@`G{!8>vfd8TQhCcbYUs2zEpNW;ceq0>6;n;-jXrH9J0=$~ z+JxB+@?OAsk0H#4SRJhOmLiN11o{vrL}xK)G^Qw@9X8Fv!o zidoy?3}N(!)FDHlkYq~Q9O?F`a7%Rd`yvL%S<>Ku%29)o1w7e^MY6t(C0bLbU9n8q z=z*&si_WX@SAEr~pYrGbFO^**+jYaPGZsF+aLN4N=HE51o%_OEV)pm5*UX+i^Tiq4 z^cSWrQ=gkMO+GMr@x*5)E*$^#`1#6DDt%)QjnQM1iklQF`Df)}*)IT@!~S>4y;>y{ z4DLlXnfO|axpr7A*lu602#S0}eVuCC91GB(fNo!CMd4nJ5=7&#k%EQ=y{$oLhee7> zVWu9H*|0b=Da@8eW%hj3c+Li9LxcU+)%bjvsYYe?eERI{QJFoTK3m+#Y?zYP)%bi` zcGjrOo=?kOv60y@xz}5E_{^@j_vK202<~l5k8<4GmL6ZYyObjebhcG$xBOLSqGips z`@G5(_ntYj03$^8NfF?Tkp&nb5K@W&FB1d^NRJ2IJ3_#u6an^*EWi=&f|rggz!C0( zJ)#Xrx9lU`1-nNU;0SlY=_3npguCE0L4Xm69_cPPbz}jKa2LEp5MYF{z4wR;5WDvj zr5ysx$1P%;59_*m;@%f4?fklQE5fT;kVE63w~sR*?mc+~KEonMr0_Xu1U|zeq_*c% zxc5aHRsus+v$hh@3=1ZZ!tBIRnLVGDoiHl1Vc}BSx9pO8j~8EyHoX|H>+SH3;gWlg z+m`y_E9#PakKLB~;cKRF?=c&@VOZeix}u(sfgL?6v%}Y0;ohT0Wp?;#+&TY0>@ABO z#2pRnXkbSJI~v&0z(2YMgoA+*3JETJPQ)1D{Qo`?V}$enD@2T)^Z)H@#O<8_zo6&; z;&GmK=lp;BvT{4;|B?&Q?VSHhE=2ctIscFBoc|9z{~wdRMK*i)_*cM>|NVb&(5+lJ zX%AvIoH)JZrt9{5XP58kmbm~qDRhG&K%PgFZV*^GU2Wh*gR11f14ySsKgA&C5jt8M#>$=q5+Hng<~70`eK!# zstjs@U1Un_#L7^?=Vbl)ZpI!_0WAZ~+H-L_ZS?tl>9k6>DQ<^nhcR?My_SP?NpEuJ zu$?P_)|)SHp*b_iz)!SkN4}VZ8!+37c6?zS+rY9`T8;bmTLNj+T6fw>R1`|C0F&fc1rShFt8V-_d)mX zX;QfgMl%hNk<9CVy$*mlkbCp*HGIc3gIc$`j8jDtoj04Hdl2Ctp#bDVoWgD(i+)d!eaK6ZSEzeVobP#gYseb@4xny zD;MyIuQy)OllVGJ4^q4*T`dye0&pBOL$BBHZ6D-44O=I^4%?aTGpC*U9=tvAZg^Yo zqIDSZsM=gX)ka6iS2sFSPN>!bcgMyZnN}!N55vB)HlIRmR(|C1HoH1k4N58nk4K*S zn(MO+pJA%fsx>G`Jk_Q(h{m8+tMo{3wB~Aoj9$03=6cqWU$@U@-6|J$KYkMu(kklAv2UdtQq$+TV1rv=y>Q*Ae87xOJz zU5OaL?}wdnJIs6Vv(VwzLeUx2ye}EJ+!<*y21JHB<1K2TELvM^(64bgbgVmR4d-Ju zy9RER&{~yhFI8Jegk;pExFg90Kxuh{Ycb<9v&dpH*rZ6OJ?zoe+lFvL8)~MiH9Opa zm!Lw6H0LB0DCjo5LDWiEmS}dDjbM^kKcXD!EmOtWYirsKbuhLxdB9@TV)^w+ zpT{Z_hlMQktqMl3UA4u+c36BsA5V9|%LKrUgb5>Q*BL>f_;94)Y6Y@oaKn5e z7qW#31W8yltce7#T*CCa)p{G$qSgYl80x5otQyO)Xbfe8nxfWR%Q4}^VzgZ$?MWYL z%~rEztc>S%UTe@9G_k<|n>VwmaNb1*6RB1a#_TpnKANF2t|pT0c&UKN95?aHaLa&# zO=AB4SyNZYc71o(?Yp>Lu3e`r{C4593vXS(7xW8L^Zzw}&-_*Mq4~XYPlBxfo90S$ z7tb9t`}5iRL2f{5_UxHwXTCLa+YCM9m^pd+vFU%Cz8>5U08dX${b1^DKy>Hdjs|u# zu%m$;4eV%OM*}+=*rb8Qu}kEG%V|NqiL3nU+=6OspJEtnZze4dEZ-8kH@#MX-nA9& z-h@N}dVVYD-b7phdUh-5{zl5@wu0{6$QNkOYz5u>hA2Q!Zw1}IV_RLn${f(48y%qGPzIkdZ=uLg|=2xsA?>&zq1()yH zaJ~r{%m0Ug4STr z@TNL&uRe){+gbCHC7En2mJ^FjM>(U1z|GQ`psr=2!ezRZh-xdYRxAv{E&!3Co3er+ zy(jtWmGn!MLInqrHTBn9v)9ipvAXL?{}xdG#1V2XY;| z#a03LX4lmveHOO({K-rW;joS`Zi(es3w+pPf(l~ppeI~oV}7U4r#3U~d^xv7XrmN{ zKyh!|u8BrD%9kkT@dT~baIJ=mtuuaW0rpgjfrQrtg^JNyHtRqsHEGL74G@M6I;_}( z<%l%R$4RWY7{2NI_rK1t;+Q0Y**CW3dq=oSZTspHy%B_KC#C~VLiKzI;XwVTd_EhT zbmX*Tt!%dxxtw~jTsODFNjw7ASre#?pNErpoXF6?2>`3Lt&<_Z7+p~An&K;rXg=h>T7#nZhy~Z#k z{$R^Ntk&{DPMZet2=#r0LDoChOL9S*yxD5N$FLBRfj)87SMebvgI1VpPHmvCZfLxo zW+zduw7nQ^&@W~kbVvhZQQFw_w~03Cv$>&iGasvS-Sy*o7~D3#dgw}X$Nss-;8p-I0B06nFLTSyvW3LL8S_uf|GV5gfBnWP|7$h=k=cov z?}F3*+OBWRTsSi~{iEqSr`c)C^f6+M|6PSC_tZ&~znuKYqnA;euLAW-4p|SC?3Q6zf<}UDcg{ zTpD8w9{XVz10uCG+EcRy>UCp|t4DPXgFmhh8j%K@)3-Eu)SJlGs@Xzu_R}IpB!Otv zE;>c4Otr9?4XGSxv|-h-VKP3rH|h&Ft!xLD-`K@48>%*&*%r6ASck)%#nF<{ z!PRXE50=E-C2&ba(HhpXZnbwxOa(*3ag9HaNEtNFkQZFzp0At3SsV%$Ax9)uPKHTU zkZ7>2n5N*J6thavP|Ff?K)z-m?~GNVHcnG2TAdm?>I%j&ixxLDT5Jr7&RprU*%l>FmL%3qju@8wD0laFk6b%&*Qnwm> zurmvH9IhC~`cQKtr87B^x=yd7a^XyFUM&BK`b?Qu zhIuXJP%F_P)s;r4TJevc(x% zF>NTNqQf*(%Z3O;-594l2C{4|=d#u;lU06R#BhT`yWRpzgzCw%y;`Yh>jsyZ| z9S7cww0v|c+2)djw<%u`F- z^=fri-;7g;#^WZVMBdZR=bCubZLsMBF)B(KC(>OWIv16wSrXh*+z$J+_PE1aNYvC+ zs<7BVAr=XAjI5_@w4$w<-CYcA)ozaJT5c0$ZfOHm2-VeWR<+vHM$u->qKavog&=E) zrJds!ix_sZ%}yeky48$yOpYMTBHoC%l#e7hPd=$;O<^bP$)^2~$*X+5h+z#fMcQss znHkJl=c);=Ri-t1H(S@CMyj9ucDOt)dH_d6|e|V$cx7avTRc!n844E@o0CF0D5jnr0SP8&t(wM^%i{ z9WWqf38+#LSmiENTv05HddjJEHe|=(G|r_UJ6)VBiab!(UW3$a zn-waxs}?K+2BmCo%>_p4f zY%}7lj{Q-TLzgaZruJbWw-4>z#24AzC&~Z~6!3lGL+wBT?-to<2C=(KWY@d*F0#8r zXcf{<aAvww`FtEDuc$H;f(V}5reCmtm$yb$)(c8 zxZPm)kR+UkG_=?5inaz54pxVNA zl2F=FF;&d198~or)k%d|(;jIQD!zQ&7;vTVh$+TpbE=fXmZp4JOHm*4m;)t)KgHQY z9S)v6LzJokcXVx|J6EZ77z5Hwr$Y6#2Mr{BQ78#qk;Q-&OO|LORdtTvAY#-IM?J^V zW<~|O;(1>>7L6d8sEJSJ&=8ec+@;bM z>sGF+wK!~C7&1k)re-~)PRbo3Mu{lI$~su1K^u^v?lHPsO|Lfr2cpqrB##;Vonq3? z*6AdS%$+RMX-A6AGUG7YF*=h-Y25(_lCWz6zFHmz>e6v7e>3C2Tcvox72PMYDB2ku zHltovmqyJRQ?+PzqRv9fAE%-$Qjc_82F~Fn5~jLyLae%uP^MPar3}XZ&tLhjPs}Q?QpJsT2z#)>+Y&AUPH4 zPp}-E@Yo9;wa)4&>ucp2rT2DhAhAf-X{pL(IvF#wt%=`>EhlZ5GsMDeG+&R$l0h<< zVNE4Rlu1RrByQ7qf+`JIV;b~X>(p%`1`&Xw-aKbc(;AEb>zP;#*IHl=qxO{aF``Z6 z!&EWp4Oau+DRGKIGD!<$_0z#b2aN?{85lOFl7)oFT!BD7OPtBlWj(^iIN;jH#Ih1d zCLC<)qh*hVGjdHP8`J3``cyKhExT%n+Yo1pft;@taniw-;ucY=Ob8=^Ho$eefk+{h zArZYBrNHe6&a9EC8MUyd!`RC;n==YeiuLA^j1eM@B|Mr0>i~>O8iIyuD(s^wjWkBr zgPnAQHl*7=6YN1IyHaYulShv`;}YDJQTV+WY1AY|>=81+WK-g5lJZBxZi7k)UxuzKjWz75?jL}vnoiVe1ZN`-2 z{OPt;SIV^9VKq!Ny~u)Cl^lsxIa-%#7xmgy#%O1(h9DW>Y+!v_@J6dPy~`L-weuW7tK~lp|AV+x_V*SStD9cpDTV_f`n24S^`N(@NS|HrKsl0Eq;xd1J&- zDRHR3WY89iDm$F5`)MbP;AE}g<7z=`i3s?eL1kRz5!Q!HnhL^&U!MH7_VTou|YevH?GIqO&;n!mh#NNX4Sd26#wP`g}YSoh*tW~jAhaC%QTuhNd zl5{;fet8!|=L2iWdV^NE!qqHl1#2snrerTyeNZaiY)7NTrYf${J3P6m2Sp5TxZJd1 zb$?YGR2RZ9)=Gxe{)pSb#r2I)E^V=f1I}dCqrts1_AZ7cgvYBU0yP$Lss?K%gK8M5 z=TcC?63{?K6Poo^-1?G{FwXi#3=gJ?m*V+M#Q`qZ3)pK63FXWdj}=q3^(r!&;aHrF zWRa>iIrB9U!(p<5bW&1hbR{*GfEnsIn?(avGGm<8TC8O1B^As0T|~^HR^BRN*em65 zGQ>qPUV8=!#Oi@y)}RL+q-D57wTOBRp=QkMjF(97n7GJ5Y$(-AReL+v)eb8i*ht}W)!XhY=U>XhyJnN>yEHAqSh-2k!+ZFcpgegL%Li(Sf-~f6$79w` z8ReKl&b#m#5u?*Euqu@;5cSw?}#KjVprnAPHoD1T@8OgTH zc~!lssb>8#FW2xy$QWeDEUI)P1Kvc_DVE8qEitO(^e#aB`Tyy%x65{Qb{)I$>V*^L z-#3rUeR%HV+1JkeCy434dirHk@0vP)^2d{n$*GB}$NxIMe_W<~mGZQ)w~y%+-&2$n z3i+$#rvU;-`riS?LGPp@c;>#KY++)zo1o1}8`p?%Sj*?PSNvq(5luJ2Pu%kwc6Qmj zX~dtbtmR$7dRVU^g84?hk8J9ohZkZV2r({M2=UBoowyFxtD7!A)t`zEn&<}9q4{8N zLAKjPMX{FI7OYh9Y^~Y!lkPrQ2BdGU^U%OLB#>eoL?6-`1Z049FoydJxju;8K^Nad zDr8SUx5A3 z|3xy7t?j?}?P!DjLwZ;T#pn#->CL? zc1LM{pLU0a!>pOuPzYdRM!=u3ZYxP0JeL=O_kw?r zEQHi?df>Q&sL)RYG5`DR&owuD!4d8>8RhqXJ6j}UDz%5nCAqYml?3mZw^u7!VQnp{75ow z7~UhCj*oJZ4%Q8@*t@d%-$@=Zo1FFY>fPEw2#9deJRzI^tz;1fc<~WV*7FCofaMa; z@;8!M_W1J={6yiPM(ldo{9}??9d6ejm^-NEyK^6C*rSrUy>N~OKe)t;!|VAG$>NMY z7QxhZj-X_)`g(Z#>K$9X{Yki7V_4k?G5&v6Hv8n{6Jt-yXOHx2{Qn_(|3a;pKo}wR z`m}r$Ke@e$^aa@p1tA7S5o(WYkgcS^o1TEfA;GZyb2cBB)Ej2hA7l#)0DRc*)XFLP1whSe<#*$9FCLEPmGSLz|OsnHsVLaWk&)dCO@*bBxP4puWfEkXfPWAnxaN z+=_A17|gzTHz+0q4roC^!L+UC%}iMCZ10sMN62c1Maa@Y+6BTLa!l5gErs;8ye3dg zsq2~?U1GE#BEw%%$2FEpBO2NiAxmOKBF$I7>X)0hz}^tFK-9?@G0F_~!Le8dYuT9| zj8%JC3qf0tq+PQ{-W&(nnxG|~+ZI7}8f0Kl9bf8AC+gkWBh4zs-b$OLOy02QO&aTN z4wS80WEU|_Vu|wLdaK*#%pjS%FI6h4X&0I2Yg7&ha^0!!DXF?Auel9j`$GptzYkoE zyA`p?t^I=7#l4O}s1zB&D>hV9vE?jAmP9B@M~1bW#u>3UeSWyjCQUJmiCTh#ns^4z zVNL?}!4P3frGiDQk_^yE(hyu?Qvp)`x;^nVcxmXzcDlqnk+29cz z`+N>rUq~r${ct*iWs4+LDiL_vvdtK1a``wEFtqg`9HPDGH08V|Zk@%p zK#yecSDxoZsUaRNhTzjXnja^6xUAWO0hG~j7hQ6rsodG9#!1XLZw)E!%=jxF> ze^m}0Z)ao5)#ylD)W4Mv_(Mh*K7He;0%8+HdmiIkgW5;>gEcKl{jy=lq8>Q+(f zY1QIJoYj<4wNMjNJCF`;Qrk#;(c-8y2pe3~qm0JxqC7+*frQHXVzh1^uqptx1SeK5 zn)HI%UjsbA*5mNV`2iiDL^gcL;RA-;)}rkUyk;H)1)X_&9;tMuutpf*k+-w~LC^Y8yJlf6c^>pi9?Eu=B)uVhN*a3JnLd;Ow83%b z2#pwMXG4#x_8r2y-Ei9r)D4$)yFohr!|aB1w;QB0lHLuMcDq43?a{g+eP~~o!>y9L zVIYU02g>B3S9Uw%7RekAt0S00mk5l4v+p{Rj8+Fc1mcF&Aq(5g^t^xZJ0irP6yOU^ zsb}9QneTw(jWs^Q4ypGYN&-H-<9df=KBG9U^q~Zx&yV?=CDR{h3PZ;{m|Nm}7lFBD zMKX)6=a$h=sFR0cybPe3H%XR3>Uhl^iV9ss%)Wj5z3%6AJbhsHP()DWD6FKuu9)#ssYj3e05xC1c315{A4U$_6^3dCjGFRNg9W2y!00ZXRnv6ovuajy8wd@lh(q+ zG$1?-E_uqO|$c4Rv*t&3=VNmoGQ3>1P@7mYR$ z>-NK5YA67IwMyTa!V6z)WP|`ap~^?!4&t6sq6-BAYx{WT@O$g$Y@Xxz9p2t;_rFs} z!vI0R7Ax3n1s5*~yVR=(!)q0f#2X;N4PN{i;)a?fd=my89j_icZ*g+0)xroLEHd!u z!_Degbt#gyB+Bh(HX863qi!PXSj?6fED^~;h8+kUXELfMZA~jN%Rr)k(GtkhbQ{FqtKL z9WPK`hbK~08{w$GvAAd%PzY#qn1axBzCIAWV&wtK$v^qeAdLFU+{p7z}&~p@HKrLtCwW)B?3E&RQIV zlQgrHq`Alzf}l*0%M}RGOW{SeExT9@9e$Nb+>vE`RwP+BM;x@i##P$3XshT$a2?uk z<4LR8P|HE_c&-^N(|(X{8_d-*2~&%8Iu@xSR)`pav6vxbjwH}Lnem0uwxO(%SW%B} zIv97?6jv<#xVtVZA0;yP$c;OA(6}4not8J;QQq|jPiUjgh z`JQ1(k^MU)02o~6$Oq#IxPA8@3Lz2V-KX7`ivf2pZfLha4p8fY51d;8!*z&3mo%OU}#HBNz_21c3f3*%yM0W+6facZkns7= zVWRcNCA4oI1CpKh;#JA4Q`39Vfz_~RqDi0AB)6AinwP-)eg4yz?77x;Qw@Oj&5AzW zQ+v^Y#kM5#7Ife~hv)77pRIUbd9xhwM%Q}8IyJc$9aPI})Lxk6c{?h?=kK~Nwtj2SFQx&LE=~fHfx=l7*Mof zO+9=?*6k&k;AJo!Equ1O)XtujD|qc(w62};HSIvdDm!$g*}RC{Ub1msrt=@?KQFvm zs*Vnvyt1E{a%o*k<+_x^%0qOeTziXcZZDmZmkWKA|2&tGEZr$9uj;d~v32Q&RhBp` z3mfAlGpL2n4@j2pgq6#A`PNKq-;`oqzF{>dMzlBuFB8NFpU+5^?6{TAu!&@7kX2W( z?nJU}y%t^~^)15ZbCTtnTxkQ1L2IMJ3MLYy_i`*DT)8VrmCh55ba(zp^Q*`05B~)R zde;5CSc{sT0lFzT>s2cpFUbZ6iu7LY)GNvA9JcMn4uYq8`SCpam%ni<_6JT`Y4zFm zy7wF!movz=rOCAS8D6HBy>VDi^~YpfzQXeASu<_kdv31qvC)>dl|=BKr{Ba&vU_?< z?d(}$cQ{krYp?=@42mHg7-YB%uDv-Z%EY<@SZd;yWsmpV zRyx6Zo*?+B6RMJ>J7uNWXJPB!b7;8FVOf|U*|8V#+B^1_lI1&LrNPU$W@5eGbLj0l zBU+r`J$GTeWV^mAS+e6+>chO}(CEvqU|sLIZI|*A&AyZWoO4N*>-d!#&=|47XN_

    )kAzolGfuT*%sHn@=y4m`VZ4R9#i%E&vEhyTnk#~%LczYgo*{&)QYdsoW5yc-9CKh0nv!m?X=2#!88ovuEi)Hp)JFSI!MD0ozK-JDG={;HR;N zpFCNrr2P9oB)eX=>#kj)g}*GkXTiH5oB!8&a{i>bugzUKXPkXv_WoIJc5&t>GjEx( zOg}Y!_jGCcw5gv=-8_|=nxFjaWOMS2i62h9al$ttAOGz5731eC|Dt@4(mVF#*t^CY zia#iBQkdnB$#0ZjDElQ~v+cjgvR$bN60=|2Z8SMDWE3wt%&ub7+XVM8^o3pQMWxqx zk1X2+vJXli+bp#!hLADArZjdm4`KO;DHv?xd4p4H3OcfpYAREpS`A3UWFyN~f$SF~ zkk!S)w82AIA&RbZfe4A~^RUgM(^)7y7;;*evPo~pEss?svr5DaU;NG^=n*oq#D$EiT8=7%^J2}hRA0@=?= zAnVR>4vQ+RGdiNRLJC|E;G*d~tiqEdrLt+QH8$+);5tN`D&-=}CV}j~OCXD@(%xj= z8`I_DnOM+lG2l*nvSdhwYTl+**GZufn-M|1?WP8hH40=OkU$ort$ExUFqqWYJh)~G zB4XBrs~L{AG-%3TZV|R}&dI4f(V{ULS-w~x`&kKOW2B+tqiUc8px)@oYjY)i#)WYi zeb5<%nM^un(fzZ#XtrmMFj)V z3zyFYWYO7AY`-TsxcL1=%P1gl-t5Pv5Exh?|Dt6?pztv%6b6>jzi`Rn)B@kHMywa&*y*^T=>iu;T&Rf<2V$keIBoNzNtKYQ@3!;2j0`{sXFbeB+ zfd&_J^o=stG9(cD*HMTepb%`Z=1asj5z`99?i-O9M1TuXQk#fr1Y#c=g_r@>Ym-dW z+FPT8-+6W|s|8~BjzVl$wOiNnl0fW(qYxXm?S9enIe^%Cv-fPj`88xi`l4l(K;Z*Y zC=A?IzHs?$Kmnb7zZ43CHiuoyi-JbpCxO74Ne^BQ1Y#0WLD#nuhi!Gb zmR}|ilaTbePHfn|q-%MvKukjR<_2N|Yu}!?{8B(nQaa^;{lD{;_XyY$66p4B-m`Np z?-m*&AvtcnA;ZeIp0|9uAc}-MxB>fPuI1ANQ6wa{t&1`&XU(;Isz6La#@afuVS8n+ z<(CM=B&4OS5gWRtJj1Y126JS*mZ5D}P%4XQ`l1$)Ax5`lS46si1$7KP46P3Hg&!LH{WKlT^^hm&ZzFcP3Mc_Fq3WbI;5w)onpPFe91j`)7V7Re<-+{8B3D-7~+C z3VPSf!%{)-ocXy_&^u=SM=I#;Ge46G`res`x}apo8KNpCqut&}u|Xr_?DJV;*N?qT zD(H1%H%JA2^VnOZg1%|&EmA?>ICi~M&}+x8>w-o~W=|?;E|V4}*mQARtxp4!FPc18 zIw&d~6p;#g;-o=3s9rj#PAcdLldyD9NIIxiD(LZ(8tI_wo%jEL_665Y`*t+2qk$a_ z>}X&|1OEUT5Z5LZ&cijiPKmyd>EowM1wD2A)GjESEm^8H z%H*;7-6m(A=#L+nuuNE_f|@7HQbA1*%lO4bXVxxgCdIV3VnM}gQB}a-yT%2=&sPW2h}OMEA;I_VMTX^zC9?U z=&sPW2h}RNEA;I_HHz*EeS1)~LM^ppmlR7@E_sJ8O)Y>o@Jk6y1%T3cVfZql)gv&i0^>D7srP+k^gE(cOaC9`sj= z?iNfXpNTrN&P<+36hn2AuwZ@7n*Y@Nr@9*07W9+zpOgyviTO`R1^xK^$EAXPZ2n_X zK|ebG(Jm;-S@3MmLRPC>n=*DVQ=bOrWOFi!ptH}-J|`9Q+1Y2Mg8psxZ&E@3I{R0t zpnsYD%PJ^s_mn*OSgzx5`E3l@zppa!;KYMcLBBBZ1*xE)pZL5`|8M?S*{;;WyXPMT zzwG?m(ZG%db~Lb~fgKI(XkbSJI~v&0z>WrXH1Pi)4IGF%r>5jnQzyRqpir7j z$!A(k+^3Ywl``2Gm|C4_H*1wLSt3h@YNddc4Q#fEu}p@klemG%u~a*&yiYkMlgpbt zni8NX`3pw*FJoge*<|+`MB%$aCY!>k29c#p0)TQHK$DGXSwIEQ)1_(_E9I+An*WQO z$M4o#5g-7*lB*FY;0S&_rqjYIwS0-{v`YliGP&yWmW%x4S5X1NKYc}2#?h&Ii4e+C{ywU!GteB zZrDCGH4XaX>Qt}T_lja4hq6?u(PD8HFQN?;uITb)S$QwdLKGXoZBRM?9Xvb+=Tob( z8kq&D)zxaFjF%>OlHkz^ISsDXwX3;&$)L$L^LUjg7m+3)3MA{1n+IQ$6?_3a{$~Rf?p;4 zdm|_oHVuZzNy{8>N;<7xHm%MU5w?Z3)yms=wsOtH9fBU@N9zn~{zeFJdx8$lP)s|c zyc)88tyLEdI9{nWuu_R_ zBLzK)7c*3itdu#bMCeF^@?AX7aiQ48{GDLD$;V}iasE?&YMmiqaMDoDY6@H_Z(vY1 zTh&%DGKX<&rK-H0ADz?sW+%sC3{zL$!GBjEi1JQAQlU_*!|xKhOwNz~(LVa|*1yXX zs784g-)Ol``99$%qI|#blThBndjL5GB;z|gqb`(l)k*~ztFBNh;GGPf?UXwV_{TEU zOdjLVHp|d>hixn0%~KcyjwT3X29^c7`$&DrPRwRnRW92h^;Dx#Yh^i|fj~7R-$j6F z*98cC`BnilK0+T#uC3>F!rK%Meu4BUZUC+-x7+|f-kQ!CcV3Y)2Ca@8_ZoOmzL zG;a@iqExCL1#|(h75vAzFi7RpQMZY>;#Z#bF$IdjT3NDQsp8pMvsopJvv&z(K%axg z9Nl1Wf-8_63a=J3Exb^l;O&CPEE06KO>zyI!&;SUu2LnBdS4HwfIil2RDkfnw2oOX z=@0SrJzSu=l^a!gR8@F|yTC*LprK`sXwt%5ei zCliEFJx3V*3fUy^1Gj*ufXFeGT(QowOr=w8XW1H^EtdH%FfCt0dq{9gAjy3 zwQL78fbTuFO|!*Jy;5j$WUhsk+Y`47=(5FnzChz`nk*EcvW~f`36=gOxz>3j-OA!ue|=I z84}O5Yb4eIo`KgVTOqkhG20~ROtV=jHs~C$YF?jbwVJhjhsH}-yQ)?fwFNu}(G99o zB-t9p8ahC)piM}%$Yhnb@J$$RRU0Lo*9o93pJMCvX4X(*fLrAmb&9nD+sJl6ZH*dH zY_{?2#Crrrucs_i(_m^o=>`*+2DC6FQ)tu^H;PocejnUn0YMPX;Y3||9pE)R4Zgxe zp+!^5>%sTA30)PZ@Qm^;!b2E>^z~x3hAZDHJT$aRB#tR>5FY9v2w^j&R#W*l;kzD! z&~~O>uPbi^-zR5v1|5W=O{!UJDp$Z?re+WX)@W*Yrj=9P4E`XK!xPtu;wiPTPIv4w zKUb{&ADaM<42(Lxav8wN<*4$IfTNqZUc`}Wm2VfGK+2oC5bqEm^b>FCLa3GR6#inE zc&qRinH-t8L3}m_R*|Xelt6PlLkXWlXPjQ-H6T#!bmDCyPhoWt1Pwi6?jLv$U>+^zaHhacR-Hc2h}YK<``|tOJNTYCIzwX3PJ^UKqFJO1?Lv9>9fIWvP8n2K0@~)4 z$$}wy{4x-Y7h}VRY7#mbTJpW;9u*Ty0|QGORqvH%2MU6$b?x z-Dv_4QYae4UfyB=m#sghN_9O(H)sNo1BP}?3F{Q;Y`)W=ik&RW=34CR+t)=0>~a)6 z0U85%D~>8Kg$`Hm)QTN~CdyP}7PN&Q$im0kv~byVz~%B&f|O6mfa3zB@VWM4AvAfD zF8fp~S2S~z@8ER@Ok=VFs!>v41^-S#gR*0)om``x!HN~UK@b#HZv6Ag%$r?pfag3@LCRPLj*KlV3 zEoADXw_FB%od!~aCGN@3b!WpKlN*!Gh}6{+*9wGJ7r-#0h9Gs5C{|eI8v(-fv<3o2 zq$*g8R=x>*pPSHfjcT^4e6v6eRbI{ioUfM}88XM|TRE~^R~`^#*3Q0KAic^-`Dy`0 zqkN6*+J;U}bgt?(08zE1e5Q(hzd)GJ>v{4^-vApAs>*Mgs*n&s?k*2(g&@?XRT z2(I#it4Hu$Kw}$yoJ_8pc&$K9CfDn)?)KqeYY^49`I$^^n7F1(1DSZeC>!ro_keEL zusEL`z#pA>gNQ3SM}TsKo#X0DeUK;3kDVj;aWpW2JO|9V32eQVt6|K<0YQZ_C8z*P zn1DXFcTih`^t{(z%4?G!9%~;`+>KQxSBO7#X-r@;CMsx_6*@f~vXCbx9d8~dzLc1#m%ku|o-)Cdrx#%62a zW#cMQ-_uNm$r5BXhk>C8#>^sD)WOYWM_tUSb-H>slR>c-k*%VcItI3e*h-VcN(@Wj z^#-iGipMynUCWlSa;4lUuyn3XH%in5uW|lSC)cTHDh&->sS_PTR#W1+C}n4xCgsy| z`AaTVOf6{O)3p!`q4Fv4jw(}RF_|Jy%f`TlkWA4u$P^uJardMgyq&+;q?k0EuGN8Z zCV)8FmW_c;Cjd;y6s4+6QPs=Fm||f@{{Lh_*~2pV2X;;DLUt7t|EYM7qA_ooKW6Ty zbN9@>YR)Blc-J=;Zd<4=T)6Pvg}Eu`lzQsK$!8}Yp8VS6zs|jA_7}4sp1f)Dz+`^* z>e;}ge^NjBlG#&d9-H~Z%r!HyN#(?&6aO*s;faG2+(c?-@ATu-pPhd5^ed;&ntFQb z!Kt@RW#;dkc=`B~-c5k=P3U=RvJ5ZY)bhRu<|11m&={<6P2JS)Y!Mi-l_P3 z;tmC)Fe{GUb+d#mie3=S7@^n;w^c<=F@|`W5Y-NAa88}^WhkS6_7PJS)4QR}=pL zzl)8aIdGpRtAsm(( zjT3HDs#Qel@p=hd#FH~v4CPY9?(}k{vbCA#)F-?|#Blo^WWH>GVbE~`Rem8Tps zXX_!D!6{9-PKPmf%HK5js`GCh8RIP@V_Yv{xH4F#D_w!husY>U7Yojip;H2mSX0q98leEf zQW4gpZv_?8B8I)?wlgYk&C#YJ)e1^!yoE?GNm^7@T5SotaYqiP3c+kk)l&RmWQ^~R zjPajcjI_1sRauHf&ZKEL?HVt^c;Enx+A%cd$a;fF*{CyR%Q$Nt@AkP}I!3q8N5HuL zC1V#T_Ua7jS}ENdsVQvHp7_cwpI)-%)6|wvlP?;B0N+jJ>`zP#dk`P?ASOQ8RL%KB z&#(vXVGms5gWk(oL`4{eJBAnJCO%Ma;&JM*2QL}+ z;1tn1Z5$DH!`XN?XZP!Ma23*2>kW0TtmSmsQm&fULycP6p)G2&^OuZ_krFY??UKPC zCv_&h*JC4nEdyoK8Uhx$q3g7bMKzSDr8=gND(^5(KRPnTBO_z{TEsAM0XJ3mfFNIv z$QqlFHdu1x1D0AP9*kE#oJsApN9ql)!P``vH8RF4x)>UV6DJauggT~4AU-c=W}u2G z&zdXsQiO5&w4P2(>q)9>&ZAa&NXHnR$IC^jbU`Z-3#QS6I*sZy=>Nywn+G_uUG>4e z@7?1C@7tSk+s~#gx>So)Wg9=2q>@T|mG;tGL`f=@R4P?TRob5i^T=j!unl3_Z~O+Z zoiAWNZ)0O@lZ3DZW5SXU7Kebrm@PPjID`=R@&UP}E_JKbQmfSS#s+__Ke}gH_ulin z=bn4+z2}~Lj^M1hrB0a!W)6GIk@5N|ACkoc)Da|ie|PR0Uo>m@g-#S!TWY7KB^~9k z+zY0P!A!_b^kd~DgcqU}zZfm%1JvTT&t2n*xodpeaSgPdxBGlhzS>m~ueaL6+$!%X z!SO=dnJ*KxLxPnA6hm~Wu=LrvYy8Y{4FnFewMMB#rWQBvG<0t2?@e_j@XeR|bl$JMFSGvhWtH?H+K{)MhA~l|FIsG6`HPi5A zE4Y^wpLb*K8rSEp@xXD7cD|Y;Dz-w$N9#15cgAX{tJ83c?ihhYaXIgU3+-~J!ZFn1 z_s(78pUqw4dv^cx0zBVVd_V*9`4yeUn;_F78!c9YAxhW11lLk1h;sSqGGS8+f}3$w z@WP880H>tO&_H{ccBe^oDZ$n8SBox;rbWTS7PzX^(JM@)%0-KjPT22sKrj3s$335; z#($r?#-ExstRLQvb;&&a_{uSjdZ}ALS}~0FH_R zBWm?Udvn*gHg}C@0SUR>2wLaW7mfS><;5>AZ2j_D3H-YHeBE;3ZQZBB>nzK^>$#gJ zkvx%U(NAaRk$U__Q>_Y0u2KXEgB=bdfvw)u1iqG2wW6Sbn*i{41anTvtorbc697}w z?0^iSkKfSCAPBqzv~z=^<+dGV6j4wMN~lnAQgAXO_m~JxR`eE0wgPyAFFSiG43cqz zRH~ySrbR0qBau7+ehq@VqIjf0xu*!l2$G1hX;NmXxZ3vAg>qAan4qJ3SUpE#T6EPh z5>nie@?(%)aR=-%d(5rI)P5@s<4qaPcI^(lZZH#8Sa2V(psZ`7!;DVIcNtJON+$D4m^>U4H$$>ltg^PlcA@Gh#b1Vz(`Q$Eu z!VOJTYsX`MW45dq6~LuA5DQ~0r==zt&#-0GGX3O0wa0Hv00LSAG6GUK-?#}157RYc z;NKR~H4jopn|(MD$f+~JKy%47vj_y^2BwK=X5m8>&FW&&uT}l=kQj+)#lvvC6C-It zg}Ol>@Jb>bRp8gATt3ZgH3ZS_2%<%Ohxw|nOgm(ZOVoniP_bS1M>Fnvu>v)tE}pA$ z@q*j!Rak$cq&F~k1PY~VZMdcR(|xzpi-bbnX0M&e#7Wj#g6BckZ{B>H_*BTyJ@0Is z>Yk~=l4r}4Row%h&+0rngZ{oMhO_EzT9{}cu$R}MGjz{=qdBH~P7^g5)9-7bb4~yl zimTqr%ay_zRfFDxs+l?g?m^XnHtqIlswOBmVh)$)aFUt6zYxpD8V3X?OTnZ);7zdU zQqawdt*k4>Fqc;~2W1*=W%8wh+OKhVy9LLi*;d2M3{cy(VQ#Ra9~Xs8)?^ zX+$T(`x5qgrCf0Oyv0fg_2&7 zQQ#F-seq>gIUWRBF=&mp&A0g`Z*0i00mfrIV<7b=-xNmvHmINHm0?rWN})RZH`s;o z1*3O^LKUT5X&T)bG(Oy!8K;EH?#wPaDqMDF2IEf1z%+4Z)``>^Fm@lZY& zyY`m7zuar?eq~qQ`7b-g?JsUWvh|;~5}W^FGqmx!4G$=M^||7}6$h?3@Kxl%6H9A? zz#8e}I}x9&&%_E@z2>I+-At>D>RlSV%^(=yf11d-3vEZ*lUDrI{Z1r9^>Z<-O!~d) zg1=)6WL2fAk^+Hp$)K&sOI|r|^Qj$;yY41^e5@C1YiijObLm=oJRsPv=M)35arwhR zUmtI>U8XCOI9b(;Ob($vOq@O3qYbxgJQASkb^DP>i}= zVm=yHAV)n))vzp*Y>?M8KC-5i_*7f+G}%C_RT_^4wDohR7!o-xQm+PN9CLfa4qK{i zFTq~66z_072OukJPqRBwH@9Thl_;~ud<@b+ZNIDsgF4&#EUfA1iru|V$i%8LOAQkFV zkTlJ^%aoK0_`02a+GXcM7>kK{A*My`tk;$8QVg3&+DNp}>T#h~E(u;71Tm7kUJ7I? z$$CB?l|y+tU1P-Y=x~eL2nIUehrM`L!E5Pw9{3tZn1WvCK#!@kRFR098)jrFLxt4+ zcsrT&p}Aba>jS1Uq!gC2S^y`ilsm|Jc|tAc6HrH}wdGLZx~raO_=TFD_GbN!L_rKh z$LwtDe?5wUw>L|D8uEKwHmn^>5gAeGVw4ApB5~LoEce@FG@Y>3I{SS>1-*+^BrWSw zVr@EGmz!Ef&N(&M$NBC39wz0powTi^sUSmXl2*H#;1jx$niNmf$K!*q-8#h(M+L+g z$4Y#pp3FE{##6vU(Jn`{o2`&b=$0T_sM*uih@ah8o6cZ>u#ug(gm*oyV70?^d_`M3 z#CpUo!y#oUfCoEJ!531m_h}%g{bZKb%Mo9;?RUk;_^@?&iUGLE5Rh24!}+)#3@CIw z!GwggsHy&Fn6*VQuM44)h>{CX`#nN0kTu!X;>9`{>_J2-;l*99c9sfpj+`v;Od#kB z74mo+zn+kVV6;b4vQ`&+Jq;M2#xN}WpHmF2NEs=$s))PZ5ju$mA91-0B$VZ#N<5Ue zMX5~6Qxdg8+|BOWAQ2B#b9~3vMr2358tyCAoK{Gc^9=Bw!V`g_Kgp#%dCkRMkA|z2 zRHD_2QbNT`W|~QE48!8VDTZb1@NBM5huNea3ThxdZY{-x6XE>edJ~f;S zj$zmt8H;AM-&E*?BkhhzXxr{frfVvimo-}V7nlW_-bVD^zec z*)U&F5k-$OMc`^IwdhhNZnxV)JkCbTt)zvy**WEdo~{K@F(uW=Xf&>ew7g27zJNol z_zGO76iTLiRp4Re@uSs!XUXleV^J=}b)96D1lDjgYD>Tv1C#YWsoR}QxJOWO6L>jZ zr^{T7DkUp{L|sbPqAg}TE9UCkPcdjpFRA4jUxwyop%L#&RHIr=BQRbW#IsTK3dEhN$I@fAGe$S4^VL8X+9_mPB)0d@M_c|12YUkTL_i32qCKo%PNG~R!l?T`FL?gc zax(=u9m3#sb=ehsv??z_Pu>h>*li!Z>gI~xv6xY zX)^X)SP4aYLD^OIl5(sWK!YJyFnPT;Gb;Xrrx+TpezO&J>v^y{6Lz*v85AQd*o?FXl-8LuH@Ic502lxc(tGL=Q+CjPnNe>p;ct^}rEO))p zZA7?Wu_?BrY_VJub!lu~EPdxG1_h4BP!Dd)Wzc32x0fT1Fz^5b*|F&!Fn%XUlI^Bg zqTB0|`+irq;>X)=aBQPfkyy6g?3592G1qA!l^m{Mv1F(a^1!L20$raO$8RTbilGL% zJ;_MCKx8P<-V?(jQt|iuTDMxUHyD+1NUGFMA97kcyx*?SAqFr*Y|AQ@uCo0|qwj36 zqyzQgK}`%*>smd5f-4@KyB_q@Trk|P#ynb}Q|Wa&aEh0(jY=%s3b!z zpzJ}yY4*CzC#h~z?aY``v)N=lTQ9cp zyxWV_lLTT@8)5&xJwaPtV=b(kN2eIdiFP+tiu*AXR)khIpUJ3biR{~A2;)iy*-kWH zPEgJ+*H`!3K%;trW0BU#Q|$oa)QW*ptJIJ@PRtPw2<31ftE-_HFZrnJ9<`FCG_u$O zj;-NrkQc_&fG_{;DTY!xZ|hMtrh(F7%u&|*O}-;a>8PukW%BW`Pl{2TT1YxoZol5Q zHF2(+>ID5-bUg_%lxksKyz}_}|1%5QPp|&Q;%B~! z8~Q6!_r!s>z3eGw17V-oeMfk6LW)-_hHbUglq-WPS@1+f7l0WIJbG{DN<0XuVA$B3 zT5IsfNw@kAVD3J1e|kR4tYN1GF+|F_6HDjAm$-b3C0TH~;qfLT_JK>jl15?;NOlxZ zPbA9a>v)&%N79~@*!H03ZsjfBkk5530u17#y_j~f06IT<`8Jm+@t6ee99hE#4!&b; z@&zdB*Rypv+DiJBUY4jh6CIU-h-Sfk7=tmck@ZFksZhwJniu<{m+3}C7sCMQ8H7?8 z*e{Q7w{yT&-4?X7cPj`AAxEPDpQug~vH}6BHoZR~x z2q#){4|p9?g@C`}JxCEP@bKr*3A&w+hbkN%Zw0fj{dY=V^I+r1HAg#Wk1xGE)!S!;moAEL%9YsjsxG`?;y?OKP;isa;J=m>p zKD`G6@9r@>6#%=y5!o`V(^k}lHFqEE;(IWQ^X1Cj$GAf<^yTFFHR2gncLtP%CWYA+ z93}mpFqa1bENY^57$#HcOgi6dHF{A2ZX*&k*}v!S`Q6%yHhO$`X*Sd|XRo^~UxtBM z{mfp-8FO>=qU;#)$gP@jsW^Hn^D_ZO$M#ym33x+5-ZHP=V#eq*fIM zOMs!r2AIzILQPW_MvX}t*>pcMzni?0ATT=BK_TTV(=zI=*p+mlso;7>j}hPzQ>^JF zL4@!gT&j3!A=t&!vE-$1+$eWGn^h`kwn383BH?J&ip8?G=c{x?d(t~Z!)vECMV$yW^5vTdS@mxG7k&(MRoJ0asGeR!aEnPz5kkL@AG@o?qBb|Yxk8qpW5+n|JJs;y|i^}>ouGIZd2ae z+j!pwy8g54*|q<+_HAp=UH!pTV&xZBa?5|V{JqN#@HW4?_&1B+z4$E9)V+MZ^=^6v z-1F_rX9fp%bI~uqowg%X6Y_<31YBc>{V))Am*8um=ncNz8ICnxrse|*L|#c z&qY2s86*S;VIzYP4?Vt?&gR?0cU?1E2cvRHMm>C&oZ0&2IV0VD^O<2l2o!TPgjQ?Z z9pscz_aa~qU4qyp`6k_c)4as)g^k~Qme@3DI%VU%5PRdi#O`Ix-f#(Gmmag%pCvYp zNvCYQm!1)qAa?1V;b)0W!+oY_mnP;sOKh6K?}QjU?ZwAz^OJPfF)y)c5A>`M!{#M6 z&B`&G*u!_xf#yAb_CMv@H0y(v){&X5U!GCM`Tx}m$%Sk0yXM;aGZ5#`z58ps;?7^~ zynW|&+rPNo-+txRFKj)w`Tv2aeGhDWV#5RC{{7>1WbL=s{^8n<)t_Ijto+f6u>5<= z{L=3%J-Yati`>HJL1S;d{i;>NTX)}I491(8G}mH|lNBTf$qEAPP)+9Ao<7cz;lLnS zL0I2sb=hMB0YZpsF=SIZq!;IT4aLPc?rkd$KAo;Goq81M21>vY&dA+BG9@?2-4In{ z;i5dY)W7xi{xO0%THO!kyR^p^$4EL^m$Fp5A80nqsIADADmE`n)!Xq%zlh;*Ru-)w zc;zvIIa=K>+Hzs898O_kI-g?Q9%s?-6~z!Hr`CbF{k8 zH>q-wVw^6h0HS>t5g|oo0(v~zu@hORyCZmOqQ7ekC97o83J4TlrkdYv*d7tjzXE(h7nHju8|<76B?(v{m((Vk3l{Vn&?L zfQa)RUnpoqoKJ&@^B%FV-%-4XC*hzpUmp#~rBI6I5nqJ0*_#cYKW%sB>5Ru1Kez*+FNhG>=?m3X1zHqa8?k!^ccY$tv<7Mz1;1Wtg-~qi6@4=LH3hKurzMA z+b8f`4IWWwgL{hEMybT@7q1d;02C(H-zS_oy>y9YbGKi#O0ZL#J?YLgJLPQP$=fen zeZv6wM2P!@10c6wh9NIleS^XB$qnoiSgv-7W|`a1UnLNu*-7;$SR7|_;E~(UTV;(V zXL9qB&40rxFopue7h6LongM5{uik#{Dll1}qPJjDyu9tTdiy!&sogirTlMy{$Ee*; z16B3*v&N|1Poq)p_O-JrX^^4G1>QX5M<$zbslQLyCry=^mZIwIy|MlnIiMy|7)m}m z&2lx{zLL1TYtDCMlB&CGCla@J%x2+9dg!vvGPk!)H8P2}`@}gD)NBSka(nBn8kx$> zOE$l0s*!WpP88QvyU=X()!Q3qg=K16r_`pIPpnW|AEP#{ty5~#tPEDDt&LHe*48Pt zX(xXx)K656wM$wN9=W}8EEYI1qBaJbmeb~^Sr%rWf5W$zR}o`voY4GvuVX9i zEt&1%=ir_{!Ow2*k=u)BC*4#p%S(CfJaT)%%U)uQKM*Utc|F7y^ao~yrR~)$Fz!e9sIPlfr0KOu;XlbA5 zL3QxBn)J3iZlOzu+8(VB;ZP>q=!X5ZRKW+AG>TF}txobdBJ19l&5G?xDAq|8Q=Tdj zBhUy&mufkmkvy4;`Uc69<8)D_!XSC_amC&Leq52n!*R%!D1pe9zQUw|VLPb@oAnfWOmWj`R>b0c zBCTc^so@L}jd&eS#-p4R75#yNL&3!)g*GL=SW9A^vB)eFkB-Mzj z1YQ|QwRkt~hi#mntA?7nht`1f+=xIv}Rl@g+qc8p%p-XD;mRxIG1Z$eceO zR^;$>w%$oI4UldR%O@*cq^iMU#V(*_!JZBVZ4Rm)(NRt{`}nG1#YBh=+0cl`&C5YY z)?WY*f6KM5?&s}ZkJM~NVaZF2l8oZV71#fbSrMkZa|h844l z-B+t7q&f(j2znHe%N>a5HF&oakYip z2QR&dRu;Ik#^Ih#QF5W8o+Uw4m|-9P%B$}{9R%o~rTqYyi(0sA%?r;o4i@mmA@sh_- zcM!9p4xaoY&Yo9;^L$$BJM;n+DvQYurK52@AtWUsYVUAtpkr2C`E9de%@Ot58(PMt zmwW=BEXet!3YX#-nl85GoD4e)*<`<8XcgFFiYu=(D@wRar@>PT(NmS-X4nq7SexjN zb&Hgc@4Hx!k7{TF&5%h`1FSz{Rut+?I%N~WVgvH`8L97Wi5afVc$3{atb`m~2ojvZ z7#eJu6_>xmtXOQTGRTtoD2D#5Fal@RC~W+3c&K4Pbxan zpR>Hz9kCH%!KHZ|WTfHA!X=0T34yBSa4ctr6}`DYJsQINcn0ESdoSnm zMHpP?)j%{}Pvy#bzHW;&{PC_iJ9c0vP{@n>L=gh1?9jN_Rs! zkcgbPhN;7jA)a;`~-@ovW9?r1cCoQWhjkQIV} zVy4R30cp+$D?nZ>a*}&@sW?uvsxz z^@frxRIRam0A?B?xrz9LkUIiW@cyM+mmZ2(yb@{b5DAtNMr(2cc5Ut%qve z0(jq!hDnDzoCfKC%SnwcCHhcos8%-*%!*kh2qggaWvI&+bq!6Z(a}+=b zoLnpCC^?Wy#uU}vM~4+LTf@?j(<#HkAMlI7DQv7j~Vx|st+cl?CD)5O?R8I7I4D8FsYI2!R z)omaEGY|(OaU*fzLw3V^dZM`;y!IM>%?G>vv4|T@FesPK#-z%D2W{3m40wN}Ik=`nJFt@n zcG}Z{dc*7s7rY?6Z#1VqXB>SPIrFk_@X&2$mWR2%9s zRg?>@Mja}H7%89;{StwS1tOqeuA-OW-FQC|rfU8KORIm?DR+T7|CF2sSKK%k*Bvpse)Hx(e%4bl zLk)bV`>Yz6I18USHGrR21IC-%u~};TX>1Y|KrS2*02_SqHJC&Lk|2tUUQvx$-8bw8 zjsYU|!W1m$WPEh)7|-@+%pHTw)HyM6r0!`>z_6jdC(a2)(Ritsa=41H6m8|J7^aj2 zInRbNhhcDlC*7`kH&avc2&rFsPDlq`x>;>R8*D_t8hsB4KIaT6RV538i^>F(@Tf|! zR4yT%N(aH>5>l-kLP5prq{(JTb1=yR2`wo7P_iaLIQzlKE_Xm!tQ*d>LCNlfh$FU(@_%h^4Zd{Jd7?$!&phAEUO(e z*U$ye<)=Ag15qE{8eB!TQuXHRhf*`jnYn^O3_>8B(K+X5f}v!_G`101=iL3jF+yY6 z!=_&5AD1;0|3%9->a-7{yG&bgkGp>^Oq8E3D(+adejsOKu?7tVbx*3FK@S3TcdXnE z`1B~2Mnw-ANouGQ;qi>CnNk`DT-A%0+P!qwD?~yiEaFo8VB4ffk+37>oRk*e`2GL# z@~0Md{$%41z^|*%6$h?3@OPdAAjwf~JjoF{%`tVh1Mk?G*vM5k=}b&qoa6|+x}6*v zVr=1SIRi{W!9WCCMD#~oL`g`~P)15t=&TsCAA%s`Y|$I#v!V96M2k~HU5t?s5Pw5g zk)*wifsnxw5!_8e;3VEvt5Cn1cZJHngM3?c$J@?fP_*;Q!|*tF|7eb#7%mFLHZzh3 zV8G}WY03e4G?o4E)H%A-nVsb5Tnkr8j=%xy8B20BbrKunJ89QF9K{PZA)rmWVfUDL zPO2R?GpR_mD0dqe)gX&rJbVC!Bh7;#4L7(B1yZgBsy;p(E`pS#hKx-RXl)z0KhLN+=8>@}CcSdERy~=sGwjEkE_xxCwFUw0`E)C3N0)FcLfV z{-{f1LwbQOyDuu*pgE??j)RODLOW{x_$q0!2@%c!5Njqx%)orVOo-={C!lra=wfBf zxwIN^I0otCCaM9>FHrrw3T0Ax_1pB00(X=#0^rf(!)7?q2Xcv5Pk0S|8 z)T=5L$vR@BH8o&#ePBX-blikEbrwEjY5<>blQ3bHPMAcKO^9Em zNpx8g;yD>N#ARqZx)&4T)RDTEr4hrSsY@e==wU}JPAXlxQTL<1d?qX*xFA&=!3JHd z9D<|}N#!seicw$_dC55u3ua&@7}O|dlnT~Dhis&8ue7Sg3IZ~j%P~5Yk7%`WIP7&+ z5e9F#a10BV9Oa@fQNX(qsV(t&Oz`0$yIl;G`K+o(^HukRc+eA;Mx*QI`G4`t3tPXu zRsz4SK3}&Scw6_Wl%f6J_1uvQlxb0Xp0NuQ{Nfzk$h2orU{a5o1Y=2o*7{0IPgFWu zLAO&FRPZ)p5wzDopny*vk!A=dc$goijRW1882$I>s`ea-iLtcCdePQctfR%=WKWm{RfnT7fCtW(ByiP9maN@zLlFAhiZ%_l?ZEx-Ggq@K}0fkB-Y^s zdrft~aVJB#T4-JZY1&Y}LZw3f$??G__Q4tt=0hpUWX zzZfuX7+`sxnAg*0#67Gs2!S|_Ta$@9FQf{5%e?7kONj)^cB~msR$ClbtsTTmWI|*7 zd66O=1d`P`{PL>|-fb1T!2=8l3$11&?m^s;oFqia7iJC}QL#zIT2UolCLE3|RYIdJ zs9Q*Chv`BJc!E2k1!n+-^h`S>6DfBrTA|9>aMd*-ob35kX3%wT027}|8oKA5jS<~5 z?Y-qQr+Y58SN*+J3}-PxAJo3 zI77%#)fo5BmsT~?CcwR@8r(5Gp*60!@`_%|NQ^`G7PE&Pp91M(QRL!!n4qHRXuHRf zaM{IaU~YqsUFMKSN3+>p#!YiAcctZ$BQ+7?yG*C3mXbw(IT4R4AeO!$9ULBn6V(g9y&uh6j#1#W++gXaU`AP1P7(zj+gw#uJ9B`JUd0s+lIQ z&z!2c*w*v+R@KbV1Jm5_#`Yld?|a7dz(uCA0u^2*Ztm%NK# zSiHIT642aN>%-l?wzkARxXW(;%h?7yGp*0DS?wfv!c@Cb?)DyN8=0%^QAKr1VcN~? zqqlbfg+oB$M=hg2*^%g{?N1A)RJy%maEC(XX#d!FK9Wx5oR`dmC{o<*ZF3l`cf|TI zY?;Ggz3q92VbdIj&)lbB*f58|di$3ghIMlotP`?63~S~vSZ{mYVOTYX;nVkN7*@<- zu-^V9hhft#lg}iflUq6hbzqFvW$BSXLdHF~@Z`4wx(@-p{==5(p0{qD(eD#O^F8|HO+d$3 zKR#iZ&OGZ!?#Z`+_Kg+rC<;^E8lG5u@^NE`9%Z-hS|(x2f<+&fQ#MYErN=$l zH!y&1e#|lktMm2ICwt})eRTF=)7+p;7^11x-qMp@13Osat+#(}E}f4obw2tW_hiRp zl6693OqzNM&OO-%Z5wiCz3qu|Hua3~=#wpT7(Q~JhM{Q=gZ1_=ISjfv4Au!*9|p}F z2J3ClI}8nT7(RTThM{f_gZ1_=ISi^f4Au!*9|px72J3ClHw?KaYbJ#cSssRo6sE?P zEIlck1M#$F3YQv))|1r%A$I$N&m^IfB_2XCr>{w+*>g+01n3$|{0A)4y>7z~;QqVfM*GctVR3cIv) zJr)nOTW*l-Ey%K3(A|wR#s+lj6cTJ(Xf2doyozIEMd(N}K0(rVEgTQ|(29*WFp zjSO4qx61CMmpRw1Pi=k5YSACr`Vp%|Ke_cut3`i!>xZot{lwNMtQP(F*2m4FH4xcG zYOAeOsMhn-5+5EL*hOlQvRc%$=&@SVz38@D)V1icT9jNQ&7w00_A^_bv0C)gTc18I z8c%xq)sojG)?03n*S;GY&)&5C_U*Tutwz;~T=z&J5W5OwIcEHoF}|)mwepnJqVHLG zkJX~LS8iJ^`sB)!R*SxS<=s|`9<3Z%EqZI^mRWSB5PBEAW>Kb8DphN7DxVfbN>Wt+ z7++UkwfZWnMQ^MQ^}x(lU$Ht=Z!-t>`&PcsDhKYY+!=D9ljqugKigwuT6W9L-dML* zdMiDvMY}6qt3^92ong^gdceQvw_225q^%b9E&7g&vb|7J6hX38B^gSRQC%2U#j8)Q zn({ik=(|^k@;Y;1ZL2mD>&&7Ds|Qw#zGn3`$3^2Vs;)-D**FQMFoBSyZeRtu5A$i^giR&ds zOsxA``(~>%HQ}2V-)yz$LyHd$;{PrF%EI>3t6v1au0DVNIq3ExaSRdD+}F zRs+M}70wuSghK>9E!SE3-* zrR;Fhb&8{Vg~M6}^T?4{rk!#%dc#NaXWlMOOc4cOk3LJ!<$%>8lFQk!b5~AA$%zi~ z`7%@D3J3LUDdR|EO5a|taa1cFAr22XrrOH)(ynr_$vdj{LI+N&g2?vsi_#HBmO0=TBZXEYyJo&tvkEB{MoTQ-x*}-MR zjKA5JcwZ$-C3}HGuf|q+D(Gv6%ArglDHqDVP$5nCBEqC-YBz7b-TPF=aEHBhc9KG=A^VDm)F)6=#^`nD{`7n4iK6CPtk#sqcN;;{0-oxVdDg*_a zUY?*F2Zf}sP=Qi`CK++05j_oJ01g!Km^6>x?-(NeHF)3y-tq8-Cg?F{#7xTbM(Dc1 zKPyVR0%GZbI1AwK14rCkQ7G{(*(iMSH}LPrZ&;4rc;cKpP-6O6gM*htBYzvfs8$Nq zA*AD41Cr*g{rTG+@GXc>2_Ty#&aimwJ2SH7fR0)G_!at)U{lbW-=vp@|24}L3@3<6 zftPcb zE-#s(`LYLR-2r<%7VC5ZjwT$}LRmR1xRG+u8!bsaCK^dW#Sq^0M7(}~JYmh_7B#!B z2_TedLC|x<=V>=iTvVnjM(~Cz_EdTUVV^+W`E0PSJC}jaite5e?GMCj&8bQ*FO+$? zlv54)x*@ATpvt4s*(Qbq!V#e4FWSyQ4%~=YdVYU6yj`7o>p8X#r1&61cm+oa(`yb- z$;VfeqNmY?_$Vbdl2RL3o5@m4;cX>k+wcYJxWxLck7f#~S| z{P@ydFIVb@b-HBGndf{G)%8qxMRn}|zq$K@g=-(V#$8+8`^4U3 zdt1Ao-u*W4r>oBu2d+49#epjhTyfxv16Lfl;=mOLt~hYTf&V{nKyK%8;d9^$tk?p!x5sbn=i-V#P53%)KbB2JXkRr&rtn%? z@`Qb5E{LLns;6W91SEAtQ2yr4n^CoO=1sP}{@}stQpT)?I5EV5 zT|_`uWJr`2mRC|8!Y?8@^@bwxe!HBjBxprTz*957?&AVjOtCbDk!;15!eJB4&!R0 z*){)r!(n$CM#xFA^zjLarAC57TlHp`FTdQ`z+v|#c+VLU7hNEYI!r)v$LkjCbV}y~ z2bqu}IkN|}qkdTH*Mhxvu|_fVlvLqeY8G7a4=yjK-znT5nuBgl^KcmLN`jEHdN`}& zhe}&jJROfWUJDXcx0fa=WD&zDOiVcAB_R|ZI#HanUF$l?fp#h5^@zcI%H!yBF^YhA zo-ya^;whe{&}|wxhCVnoTMhbL?!FF{E4*;%3Fl$X9_Q-^35TOWsst-&bb-gqDC#`$HxgZkn=7W9 zDLv>fsBplJvpxHyBt7S&Dgt&wIJgmf`BM*Xyn%gU_Z>cPBRFv{Vnu;XxD`}O&0LAE z$RM!h>mO)pEx};Xgv4ODkr-@5>MrBo6HP`uxa?~ZYUKbsXk_hSl)|AFPs25wiXf*4OWW7nL95~d2Q6kbrB`&Es ztKnW;NNH53h!zj@l9oqmjAv3ljDd#8o@$7RCyV>7AsqLvA?T>j8&6aeA(oALm^cH} ztB+5I0+~|8k%xjsPawdAIzc^5gM>j~c39F7#?tY3PeVYk5srSchB)?sJ*y!m4(XMK zICX0MyWuE1uZAETzIK9g1TcjpYKK`h3j6vfL8Aqt9`M9zM@C|Oyc%@`Mc-i>vX@$I zPrgfea3>6D6+goGyxEqUOA|E1x5LF=3$mx|lbn=}@Bd%4@Zp7PrM=JYy=M2@cUN|b z+h5u)ZGCyGy!nfp2OIxn{R`{Z+CN(T;_91L-n0CbWqRq!#s9e&0ad>0pC?{^=jGQH zHiZY-g@w(H&DGUMH}k=r((XW259@rgId=9l%u;{;S>xybI+)^BNQ}nDGj@tkT)*?OYYU6Q zYX=Ay0m9AoeWG0n#X6~C%2Op`AQKrzmufkmy$E4k)K`|B_ClO4s#Lg&>n23gv-rA4 zX%hARjv6XESxxK>Dk{X8ip1Z?lYd z|IUjIyoaZFSN4PXF72_!F_KQ!r7YF%2b#??YAbT3ip>jC^>#c0yzFr}D~l%H)6gBW zajqB$|Kke>KmXD)&R5@gk%1FA#kss+wB^EFIh?}8bUwwpJ5RUIqJ$f8|kuv@fRO6e*XKpG8~wT*_FFWPto+a2m)_f6ReKdflDp86eM0_KxC3JP8M- zfu%1XmqIC;M|=_1W^Xop{J2Kg4;v7u&_fiJV z_kG4P&R5@gu7UH+WS7&pPBKU`?e67U2&_1BEt|LRZDK7So#KKbrZo!z8rE5sH3$F9 zWS@z2FxlVzfPwVRzp#w-fjiGJke-?BURfu@g;pholM0VXF%IdL?Mze?@K{%|`Na}= zxx~js-mOt5Vmg@YGcgV(`*+_oFn;%!EMt86ooA0v_MVLlF(TfC`)oPq0Fmo^NtG$b zp_&6F>>j!h2v?cB;#65?IOyhNp9#^x_V&*iKmX}tmJz<-&a(g)+0)6sv3=rOj-jZ- zS&IZqjj<_v!n@-sJ~O}AwgIY}8vM*ofrgK#0ekD6YXBK>f{fbOntT~KFPbd_NAuT> zpDlUBCwFvfHaLg(?(6{sZ+OXY;iEQy(QM+~rD-sl9Sd^PpsIbx;OAdoOnY~X_QpOj z@s86n?G1yf`hMeQ{ka#>e(Rl`G0uZ6>7={Rc{xAWe9C`rAgJ*dA$a1YceVipc0>)W zpNb`fIvj~wrO}!!mS>dDOxYY@lHP1!l77&tY(9Qx3&2B8bPomg!^Ehid>}t3>=~$- znMei;Vd+fB&4eXyCbGej#}6c@aMLP}o_O({ z4FlzwC2tfZB7op(*~w!#ve3;$H(2N%`&)x=xt3*gFTJxqCSl7b5;isXtjWXqBy6y} zJ(4i+J@TuT@jd^}nt|^`#EiY{3HPSOrJYma28$Z^8Uq>kQLD&|`~O!g{lUU6xAQ-C zq@9&(FWP>3`=PDh-TEh63!CrXeB;J1gP43TxYpbIS9{6bFRy=kJ-+r+Ynj!rtbW_- zOF``a>&rj7>|gqWrNrWw7r%Y+ISU`X-$sEY=l!N|)&AEV2l!j*H30;8UYN&RG}}UC z&d=i=UF}v1&;ao?=CM-DKJ!i0^TMqppmzx9&0%G*Oz%`WL?7{K3(8F)a~=Qz__#xoQf7SJ%nW{&gj>|^)^ zV#5G>hS^W(Enq!#d<}aKx_~^;Xu>Kif(zhNmP>gnR znj?c{6vy)R@U1tEu@5Ft6wlPWG&wGB5wXgqQpreP?YUD-vrrw6C2WevDf`N|zS)#E z{M;kp>sFgPCvA=qV1u*gF^k?f#v<4O+^XcbQj?#j(qJdBNx6L-PhGYH7fTv4)~vxs;TC2tfW02nWk@L{`6Ee) zub>Q4((PW)90V&z-Gp<*1J zfdl!nti#WV>Nzu;!Q;ZMz?eXx)9e_QDW4aphi~}-l~KiLP?>0luuSE2WYv;W?Z`}UaHnsJIt=iV+=36$Ov+?xC>(_sM-M{ubYn`?A z)&A=9SH5HA70aJq_APy3skiti;Pn4u(9C`O)Q*fDrJToCd1Jk1=<<1lx<(kw}#Oqt@Nu|TMr2bWi4gJ(|M^Wr5O8QV%jys|ce z&2bKQziuy)y?Cuir!wJwMbd2kW;bk0(PY%$Y)7N0NLr?MDrn-7v0XF-4OQZ;PBScr zi;}D7?X;D;8Ytz{kqFR6xl}R{W}Iy#+mxlAYMIU%k=r~94OG&?z7mfmnJA`aayco} zh`FL13)gwRj@64Lf6NwBn?xA6TJbfqV42iK!smoWb&jEu_@khK1$4U?%mgWKIvo|IgK;=%M?pG~Snk1Ywpl1jt z+^Tou6|v?CJIYSlPL;eI^HkRn*sN*xI%* zn($^gS5-j0HBZZy!Qv^AR6~I>sn+U@D^f-+(-{*^FCg{cqd+)A@lpz5(!N?oj|&Xd ztAqlm4NitLnSMH$?0R7sM9z1Ly322w)WyO%zFzW2lz|1Xm!xd*Xs6 zq)o55YWa$+W?Q9tO^G@h;7T(cW1Ue3>c-r7+iw}mB>8m#dWQURnJQYMu@vcr3Oe8E z@+B?k#Kn4B&&pa5!xL_I*58D|lu25qH!8m*pffN-_-d|lS(6pi52dw8nu&O%B$0I22Pa~vdI%D$tW^*`OP2TB|0-9D-(TbNVLsdP*$pXrS!;N-}))=JR zs1{@~1N8VaGo0gQ2>$3H0}EiUClTY4ufsYgZ~p!r;wNs08&DoXx||tOB`0X-QHgtSv>eVaxPJMdpow zjv+DxQ&gRvs#6K!4aylo{ROm72U2`HLb$ao=JMkW%z;(JcwV$jXG~7s+;MC-FkWx6Zfo>R*KQutx z4H5{$i)J{a4%X*nY{@C%5)DKfM{+_>B#1{Y&eu z^{uscuR*KtT}4(tw~}1`ndQfperbtc{ENkZu=sq?$o>BmkJNP|Rj;tH2hDjHoadY= z9VvheVvsqse8d<%5AM%#;H)3j0I`P*V&7z$*!jh7o|9+!NCqSi4U#s?Bu}?U^Bg** zqw0{Aud_^Qbh_=&aq@g(=STw79()vZ?7%X$i&i}+G=4Um=XhE?su*w#K7XTSILCXX zc}||7+vQ=mUu$_Ae{r|R7yJD=4ypAc5x^Np^lL1`nIO?~oJGq=B|vf@(GOZCIU><> z97Cle0gxIfhgVxBbw;9P>t|6gw8i1jzREJSi-&gnw75UVfwg#4FyI&xy>A(gDbaJB zMe9d-KxQC8H!L4ACP>g6N7M2V4@eFq=(=T+BN8;nsZ=`p7C>qsK@V6abw+~bIGO?p z${9mu=)_l8rgo78oj*LzbABxzJ!Zf$BJ>&uuu_21>dqXjm#tDk4&i3LFYp|R`7WTF*gD@rF z)DP>NyTODrcnMSVZVTUX6geKa zg}rsl1kTLS6B5(J=Q#e~PcLlWT7S{fPv8Fo{1t^O4m`sg0Euz(r-^YcVUnAe?eHEF z;}Aynv(XqX$7<|cl9aQdT+RW%IE>3wMGLBphBFEs6#9iMFA~kXh{lLgv?TlDT-Beg zwU`E;N+%ePQ`TT1bAW@`dS}D;O>_AjBhhjOn1q6n4ipjn5f@Ps(lnHjk`+2D#_Wf_ zY@025qkJ~h2Jv~uNKEtC8Y6+kJXBGI4-(E^sa-08MC*03iMnCVbEuZW4!T8%^0wA%!s=; zu%-k4jfKuCN}f<(NiaSKZEv?bOy)qyG}un5Lu51^582}$9rx;S7WZdcOi`CPvFfTP zRf?r`RrlwUsv2^p+Qn`|P2~~0pGnsm1$XDhAZ1!hF~al=Lf(OR$ZzXD#T$te-}T&) zIHA)dXZGSemN*eJSUn-29%1CvnjYQl>txbT^K%@VfCA!g=pYo0y^Z-Qu80U?=Rj_M ztgBX`el_n3m3;^Kw(5?zox@m~=fm}#%)k4lXvXH=`Hkeg&cn$smd?GfH^_`GIun3^ z*mEE<7D%&rCXS~ei|6Bbp3y}{+h^lWKNv3ptjFR61*02tG# z-pb3B=ah0LB6w1sG-~g(v;=ZUI7=~&N{f+L9>Y0=@D~m$Ved!w-m@p}1@@4=7w-PS?&o$tzI(J=*!Aun?tEqES9U(Lb89ERbGY-I?f%pz9&HuUi$<23zmj&FWYjb(y4>x{( z;{zMty%F2MHeS5`zt_LG{=W5pxL#TJuCK3sW$oi@-@DdZORW)WH&%ad^|PxVT7BDU zV-;R~-pc>G^7)mYSb1XUUoQQ?(p#2>#PITOMACLws#MDX4-C&rO5 zHRpNoh*{+atXFyX#_G~TfH&W7Ryn^V-O0WQucsWO35^~^4fVeB8ME(S zG^?9^|FlW?ccv5ml*!QFHj9if^hZq2eZlHlbcb*48TG<);Q=9d@uX54pY z=L2SM{)1WF?9C6Et^RvUt&ZPa?);$H-(NDTPw4NzF#G$9W|5Kp{y%1af5H6fOn?8m ziR|YsAsfFF+j+m)-+yOTH~afO6Y$TOf1UvNy(Zw#nMFnb|9}bjXU(t90RDax@Mq00 zFB+ipjs!d3X9E5iv$_fRjtThRntz@E_$d?cPn$(X0Kdls{8Q#v|ChZtk9Q@j>c!8= zna9hZOae_eR~op4Td7o4l4h(SHKc~5rlcy}5HeKec}}oF#e*m^`4GJJQ5={=hGr5( zkV#||K@bt2Pd-18ru#jAD5CIFKfm|e$;s)?spQm6nv3-BeeSvcoO^10_gZ_cwdcLp zIwSCviNGJ7d~_Cg#`eEH5%?n$_(b5>P6Yn&ie$7PSKbeqB3H)mlfj>0)bVlG; zPXzwp3v#|cY^bGiLjPHJ}wnX28ZCK8-} znn>`9i3EQ%Pl8{bNO1ab0SSI(BEczSN`jY7Bsl#vk>I5h3En?Xf)`IDIQ_VQ1TUFL za0;1{;Fl&6oPL@}@QV`(-ZxKzpPxu@`f&jXeqkcPDP&55qlpBkpC%H#Xd=NM&XeHh zCK9}N@^JwPes%(R&*G35PTJ19Cy+U9=LM5`y=y`;Rj=D86?o_5)0wvOGn2M+=UHjy zj^5bJ7Tf;wCpvq_1U}K(aKiHK^H_E#3OM~ZqqCX75&Qi~?M@+6wHr(%IQ=w{pf{1= zZSy2(P9!+}xPS!hi3F#RDG53g2~IyvBxp?}c# z6A5Y)3I1T71eJ*dZ<%~tK!WlF^81TJN)yk0^8_--bBmLDy=g)+RjJn zWS{wmNF@~*rIMOHzql_o(n;bIhf}cyccx8mOT-ivulCtSokN(EAr`%Ut60GJ2Ae5d zhEZ0pURDJqI66-{yKTJS@zGLyhBg)p?o3xMw~eS&%9Q&JG}yp1v|qdgiBkI#&%zC} z7X+u6p%Rl6#f*Gfi`jQdw%zWfj%~+B-DggN=7%yK?aR&{$juEsJQC-sN9ZWX9Xr}U zKHW?nW$>AI#B_3W3_dQs0e-rjK9w=-hdd9j_T_#jUCMPzNy3QJh6neL!aRro$ye>8iJ$50j0- zvlWojFXutn?0Mm%?`Quo?-2aloKe&JKlhq>?&f_vD^sk$C`FBeVDXn_HOm+4{IE1= z6#|}gFw*XXRqV1!dzB@!%K3m+PJQIwVR&x3#)a-$%xN6cG9Q!q79a78 zL^qo&qDG`nCt1h}hAx*hjUbv#B}|3;1OXGO9@TVjx~E2R$YsHU>Jl;ca?DHVJJO>A z>R-Ej-vRZxcY_z{7|tZoJ@;}9>U==G-~jpnzY+H%IRgh%jd^47-0o&{M+81;2kAlP zvTEriPhM>$z4>x63=13`K)EWSr`1cBhwx>g*N(I*L~C*d&elCnFJVR<%+f>h)cAYO zQpX3}^2chjV%h$5MnB-TAT$zgLO#2A1Ykfh z9-F}(!;uKjziH7x?>4MWAK*?SeSz z%ApAgIk(##3o`%8P%J&*>*2zsGEqe9rNU(`gRzlJho}uHt5q2m_*RUR@Ik1~%0k3q zyhJOQK~0#i_y$ObG5Ao!TP=0N6w$o|k}Cy*Lb_|sOX)d#j(=M9rd2N|K`64s*tLPd zZo2xL3^30do&P_0>**^Oe&+B8hwZJ89Q@NkdF$!>-v(I#zrJ_d?pJrUoiBj*|DW2X zeoQOV>uj%U;JOB`Yv8&Du4~`{rh%VxZCaKLGWi4sP+c4ZaJt(gtv)Msspb&!Lq@#O z7r9C_&A{1aFjy5jJ^iA;>bJ}$kq&kn5=krmLa`v^1Ic`(ZWK918pNW(VI|P-6m#N@ zTB+QL4*Qlxro?s%gh;gquRZABC&S`dW|v9!diD3!`)|Bg2He{f`yCUKN@Xv?K)7#c zRRjw6<-$xeSLZ^sC*(yVo+w|`FETX|OV?vzvkHZ!pfT`_}bOpnoX3?jvg zq%R@TJ(5bdS{IS5fJAvRN>ys^B9epUnonZ8)}S(U*9}HTYuRSHYXsbKUA(c<60>SX z;P7s#tb#q{bipRW>bs_7F#WN3y%&o#2I)4M(hGfWsR`!_9t+7^o*2)&v0zT&YRg zWLSIalnk063vNWB$Yhzv;#|5l)bo{oi%WWFs@QIa;UdlEie5c4xlW0+666&zDD+Z-O@^(*`(%KFbXm_= z-Ke5VrB(>UOwChfU(B>Xf@&;mc9E*D9876GuX$0>-3-=inWTq|Cb)RZs%{8p#++s z^oCk12Fije`VEfA^ptcy+6wqQ_29*!r`ivyF)EnL*JUPcTHxw`*i-Juv|6}<#%lFS zGMnj?JQb{UV?`^ILR=;*S*jzqLj8W-riabj?w1V7L|1E=@q8&79Z+~2H{!vFg2AF^ z2vwSv*~s~aoYz~6^eXxSGCcQw$-u&R8IGlW)o_=urxS<}&X7HyXyOV|O1C?ZVJZ?< zH(=Dah#sCZB|{NKE5jVf(_L%yV{)gRXcv4$KWo8CA{gP>s9ZJLz+bK2#a^B(69|!) zI}++8+&mJlkQK7(b3>IDU5Z5E0TFB&(O^#W-xyDZOKpvdT1t*E`q@s3wb^1_n35r0 z^)Qu^R;(LC4zvQPobN$_K_f{PLuDl&Ff%E4wvv|pxrezg*%LxFV?smH$yPb#T&-wah#_PD$o-?&ePe!SW@VQ3gO=}r~y zGio2J1v+t%ja7vQ?rJq5WLj}tNes-3GFqvM{hCy5SSZ!?(`?a0^~+tv=nzf6H{vZt zLLI}0_Ptr?Ms>i&%y1>m6mV54MM#aX>0$Mw_sP&p1Oh2fEtZT(!BRG;rZSy+$zOoE zZa|`vIXJ`7CfaQ3Wa}c#G{dbTk1z<_DB`tf#b>}+vSbhvr55?(ppmL}tzKl{%d~FP ztY$LTDKNEo%?dO0u*}(H*u42Z8Cvwv^!f&L6HX;##Ua*8Lqa~M_kt}%ZE2QXw8EaA zDTjLa#YVO=sNh4O>SDczCi}HG3NdQBmQQM3IR&QhpFHLlL&$A`~%I%<$e8TP}wS@{Osm+8?&tR6AX+ zHe*$x*0tN>9i#LAr>^|g%7u4>JN}--Zyvtv5Igw$gO?n5_P@6OqW!1r{ng$J_HN$& z!tP-Ark&63w09n}{b$>??MG~VbgQ`a(9I8STAPO(@7s9Z#?Jb?)^A;3UwhkH)b+nz zZ+6M6|GxTrtHR2^g2M0jk9Pc&b(d@%-g0A)Y*9s?Xm|8fvl7o0TXxsJ7>0PSkBli< zcd{Dv4Wc0h9}X(bc%@3#L4sfrM^ue2do#sWOFOU?6+VK;|#bOH-oAmmtgK#+!N(-3?D;1j(i~96> zkWpiBz#mf>M)HU2WDP4K+VRcfV$lVPjq4;k=q7W5R!h2lIoTV@#&ojD=`pTaf+{6O zfZeoWL_3+DcKn2KvB=eM4eB*zpkVya)ELZ{E4tGeyy9uVvN4F{ zwBw%`7rQ{L$Yrt>XvYs77rQ__$YqieXvYs37rQ|0$7OO5XvYs87rQ`^$7SL*YR3;6 z7rQ{P#${3mXvY`E#XjchJ4_b?i|=h|$A{x$AH6`aT!vTMM$GI)hAqnifhORB2Tq5& zLZxhWsiEJ*#SmOdAt@2e7!St9E)Ys`ncN22@&35j1;Rxxlfpnd-WwOYKxoKiG8SmZ zyW?UPhyl4wb^`5qXI$(8u^g94G@u=CkBfcS)dT!pO!o&^Fp+VWT1wR&uoBfKlr1*8 zH6v9YP>jGAhbb$D6`NEJOq;jH#V!yMa+#C`+VSSN*agBqE|Zx+JKh)8nV5wqaWkKJ$1L z6c$({p5w)UKT(-}f;hg>_srv!aRK`aNHh1MW$2CDR|D_(?QmNf8YI^-S_XlcDK8$?;^Vo-TCg$ z7kBR5dBsk3N8EYJ&ffMt+n)xn{eN*gyB*xVxV;843w(6zx3^xn_1vvzY(0AOKQ_O% z`A3_-4)P2}H_^?X*!Y)?zudTUB-(G*g`fcmCtUqGy-`2jo z_O7*8u2t8BwI{D_yT0Z6Q`hghe%AF|*VA2(TK&Q5S61J%`m3w;RcZAptE0*~{A}Ik z@bUVm>l(POf$JK$u7T?sxUPXEHSoCgo1e6LG8cgmFXX|z(_xz6z|&?3;Ijm`%n*3a z5(H)lptA&iI{_LvOW>O!@Si2XW(a&|3D6k=>?{E?Lx7$o@Xin*X9?gL0-Gj1GXyqG zLNf$5OS?rpcRU z2yB{s!VG~;lRrH}VAJG9JAv1x$;ZzS*fe?541rCPH_i~)H2Jt00-Gjpm?5xf^06}n zHcdWehQOxDN6!%0H2G6A1U5}R%1!{=H2KIG0-GiuF+*U}jp6L z?o5rmkWG`LCGpd@AG31d?=Sob$N~5>7jC_9%Y{dRw*Y^A_=ktTcGx~t58=ZHgI586 zaq#womu)|D`!NTl1McAFgN^-f?0zt@L)G5^#IXM7scwNXF; zJ4NR#1qcE%H3=G*(LPH7f+0*zw8j*zGZcsy+;P8eT|K5~+9)7+sw!iO##sv3J3TQT zQ`FB;coCn+UPjGE0ZlLX#${B`P{0T>J!u?MRAwmbbt#{r@c1EkYU(;JqjZ)6gE2cr z@eBnN@Iq4)=y4f^vlM{3eyN}6dDInNq_p!)KcRHnYzaI{SX9%X4j0mI|f+?$v2*eqJsj7|$ z1UtdBe~k$E8G@;XFd_)e5KQTNM8M4uOqqQ|z|Ig%wfPYNGea3JhQQ3<^(+%{mB-huoFrh6zH1LoGLDA=l7+PjwNc28n~*U<(*kb- zM$tpgi|#R7?kI4@X><+`_&~EST%r!cJoqU5jjCb1!TG?N4V0DofccKj@|^q+Ct;p+?@q#7Y3hYHU9;V5*7% z<#T+A?FwEHPG%e{(bnNqtot7_?y5b`Ow3|?k8>JUv*I0nskCzUN!Pae?QyHTwcu_5LE2}kKBy~}^u7Sgn$?vhFB8a`;mjog;+vOw9P$;+K) zD;UJ68bP#jy;`1Y2z1|D!{Qj(Q+g?1n2|_oPI$#RVSPqb2T>Pqxg$4=|#F}LO2 z&;Fy9Z6~tHLV27mwcYGkBQwqGRQ&gLDx<33H@}-vWnjvDr!n=wpMIEtfmd;sxxPte z8+t!}2|*&wYN*KjihVOM#LD$jL%gJ0B2ji-cM@?hNE9;29-d9vc~s%A7} zHX@gO5EV_8g_^`E7A0kg%;jbohf$^5i^EF8*RKYf`2bQcHL(g*j|KDOoD8J5-#%LZ zKYk^@a^bZXe1~5=Bo4lEP~QLd{a5dA>|NQry!)Zu*v_YRZrlFu_RoU6d@tR)Ve`G4 zk8r1cN4t7~_!_15-WZ*Vq36?oGn-+W;0{q5BshMz{<9Ib<` zi%Yx`uU+u~F3$p7zIOE|(&vWRT;>6J?g|7rUIrZhW?_!^rF^)!#GCTMmCJyW7jXLO z!knhVi(Xvfq5044U3u!rT+gxg{(50%lav~B!?gOK=}@zy#r?%49;m0UxJS}}s()o+ zX->9vmU*vUxbl>Vg8%A9rd6bJ^<+QOGqY7ITZn~iQ`(91Os73t<`H}C$|b;S znAdDP*k#_g=dL^%a2z@6mlozY<*0C{-d78`3MFEBAL|KbYlhF=fGaW_hm@qK(6V37 z8Wci6c;U*ECVKqKg*lyZRKA2Torb&X77>j$Orm5VbcO1;4OETh@R|_HkDr z=_gDo{TDYDYn5ib((B1`JC|>z3qyO{p^2wDSK6M|Y?){HwJSdjq#610=NFb{jxUGv zF{~3dHQe0-A$e1UaxLD;)+^O8)<6QDm?Hba5vZ?_{w|lhauIMG`SPDH%yG(>O{0Wl zSu~2%P`Z`u^^@5q@AK#C-B!HZ*EM6cMf& zMGLUEsNiWeN_tyn?aJc-myz>)`syJ8&L6S+eKi|^LR|!t5FZ@kDwx;95c(j}Y*@*p z?oS7rbTEKaB4{ah;)O=LQR~7t?pie@$G%{pE(PUQ&kj*$saQ+60 zp@p29$^qwj%*b4zBmA?4nK?PnY;MM7f(E3oJbEMzsQM=smS*BSXjESa8 zDK3NM5n4b|s*sTtj_OnhjLA@1OON?$SAGg`89C3NUj4cM`OYJ^%9Ok9HUp)M;I8&? z+Mi4|5r0ng)7>hTjKvC(UOnvXhq6fS%A)|sk@I|fVUAPI!>9TzWfnu8gHZ$$kox zIT`7s5>eV?R;YMc9^iQG%EJJck@I}`>Zg$BJ5R4ldnpYIHklz3Drbc#oAff0?Dn)9 z3>WkGQmj#yTToIC>bWaF2{?|N=T8>qIORM>Bh9yml@fTk6yjov5346gLJZ--Ae?TC z)d&;t3tk!y*K%Ok`U${kGiT?S8Mf$h3m4O!g5t-kNwlb#-hSQ_2{$dj(yzL? zToxPxCDmM#YzEU;9x{>!RQ-<^mS*BS12-;2a{e|vl=BI!+8+$`S|qPR6%0&3C^j7u zOOaI7SCb?9bFMsiY)X0W16SWcn>f#uDNlFIk@M1uWv@JFREwZ@iSj2pb=dW~IGYG; z_rSgK%+2!rt}nWN-PL!AuAg}2%CqhJ25(r6 ztv+_;A0~fZokL+qIRAh3vh(+azpno7N?>8)X+rHP{fuVk_8D^;WIT`b*j{TyTfcLW0g0!Kx@@ z1mlfrDtH21ee9FhIZ#K;n>a})0`4?S4+G;Z-$|{0;f8hZh;(|fHU0fX6B#@dT~axr zJ_snWIL1f?f-OSXfu?11e6QNT10W|$n)W9w^Q7P{c2e+pjDO~**Pk_F3?;zPXTq9V z)lai$ChQ?G>q)R2$f}XVqG=*61w(RMi4_#II4mely&!VI_>*luR?A{16|Z&qx@h(ZPAJBt@%Iikgss-WRa6M{xVV|$vYIdfIFy$(q;1W7gBq#n*%0fa4djbYB%|x9^Trbm!!3ZDY^o}nT_5@Dtxc%BI@ebC%|YlJ`5&voGCOL#d6wHK|^S3027&h zs8&_+K`b)3)E|OGIR1%NpX3AG?&?KU@0O&jO7M7%!NHD_1V_ABDFmgYGSUu};_Xg8 z>{qgkoR~B#biz>W3M^t&s`*ee)O081K!l)~(lFZ0(HQUJeM*Ax(>UzuPVDdcvr`~6 zP;-@xr%734C*DO`AhLKjSk?m_*;h&VJMkgY^0QdPn7HcBBPKwlVT6-aPl2Io8s*U; z2DC(oFUEw9n1RtsS7oH8MRmyGq`Thums6mnF_wXmjTSec+Xai)3n6bQ={E{iI_yc1 z?dm{`BHmDG;&H3rI0YuCXh~M2az)Mb(z-tIR?tL&z|9v- zfRO=j)LF7kB)n~<0U~BMhlpQ$WEv?l_~^_SYuXh~%0$D`;`I zL}9c>brL3h6$fRl)c5waJg1dP zz6rn0=T3kE)$>~{Rc~i3o{0BKAe%s=%+#oykMdQ@8pDbGas@E!PgJx2D-$3OdcJ(c zheQdYJ*c=9)h$#MGTl&+PNv|gG}>yCj|2CYCY|c$zfORmR5Dc#iwL4NIHVUTw)-tl zA5S-NkWe92Cv+h~%bpaULQaAIc?#rEJ_-4Iq*G*DJ|R3z zm9k7@0$h941jwoBVWk;vWXt`$KjFuaXsLN7GUrT%=#vQ*ik5-lU0*66V6h@x)gZ;&Do%{P+MN_UeN;7F zFuby|di(U8{`&9#g&LSxRgZ8)-#3Jsk!p>~ieiaj`Vs+2ImYNjkO)YG&@0A8Z@GNB zocZ$!u+>S59f{WRgJR58!PBNuVNbdEyYEZ9B znx0XH*-6iO`1}cwueOx|((47mAqF~h56VQKU$)SecaSJoi>M;dxgiv^l9kgsb`I7b zwGvwS!<7r4zVNHy?f=EYe+2LTUwFuZH~(Kd_`QSjLEvC>|4aM7yufJqnUVrS`H`eZ4yM2vbd#LNbgIfeum(R7b z`e_iY@7C3eEB^?DI`;=TYOO!x87uRTnlaybe@Bj*4lwK-7=|1*9AL;fFa$ZOJHWi> zz`V#&%>f3V1A~#Hssqe(4$OlbRnCW@f%!1-R_myIJ`98A!!QUrDxD8Q{qtd{A2}*I zz|Ju@>O+nS4zP2~4Q}`!<=3Bqp5t67Li1rV%5sWEauGEaBG@PsMvih0oX~UXg+fPJ z2Top@24~!6h!OCJu1isx(pe5VvK%0A$ds5cr;36RrD73EiAto39AzAYJI6Cn&-!;) z)}f=c12>4k36)b6gVb4xqvNbeiv)6%I-eVIjzb{<=qPzUC&W)l6iED~(@}v2i)TI@ zF~ElY(eoTQo#PdVZ|nP`dLpVD{%lZ93Y5p;3{i|sTfVlK|G>?B>)gC<^b_3AzTaNoT+gJm<6Z?6_+XKDCz)# zgE*r_MMWgyxI_}X8DYW%bfh~#Je&ZuD~63Q%h5VZaUhWlsEg(R0f~rM9w69gm{5Rt z6svJT=tw;u0-i~#0v9oKMrMHva3mkq6ahL?93XyNF_<_hg-p)C>7W@61Ir>u^7)Mb zBxwTsqi*ozH30HP!wHYymhD=(r%pc!L%QTE~@mI2IF0=ty*cfYY^z5RHgxTnwpE zMZ|-86gZCH0D(;dmqb}MB!QAd3J>BS-5zwrJ3u@J=te=prLf9ILu4!{g>)To2{}L@ zLlya;pqlZx!i9N3rd5SSj=1yf<2h%<^Z3Ec2iS)_pPMHjazczOnm~hOM}php5XxTiK@yA zxMVV5G!3c}!7`kI96kN~`atJ&ArP1*J=XmYnWKoFTQ zXqi`JDyHM1h!l;7SS>B9+@r?!-@FZRUipCru5ETwFv>xW7LYN7S<}56$AxG%BipB!- zCoJIa$PwZIo8N_i+dIgS*8w))tXKdnc3=nC{HZJ!K#?Pl18n|c2Md734&(rvzu3V7 zV6k)A0XBb1iv_@9=cx{``7IXStD8wQ8*1 zu=1^yxB1tfbEE6ob0;niq5#+;Ry8EfT1|6a6&91a4sh&22pNoKwJZBGg4F;#r0N4j?+8dqdJO$ z%7JN#9HLx*;0T=F(H;R!iU@OBh%_nJTO5I2o2ab9sI(krBD(AM9f8vqJ4PHyI;h~X zX|lZQ%}zj{og**Fkw`Ggs&N-M1wOZH@cHZsq>X!li#*J+vxf zud;#)*7qV1*Y!7!K)bK9VmLUv&i9U5-?FLln(;EaU=*Yv0__ff;o8_7>h+*UvdQ6{f5!OvLObJ-Wk^y9f5Yg zkrl8hsjG1#=mK|Z&Ns%H2JB*g_#=f{}6Jhu9rXkA#8@sfCD`>r;+EyTj3B z+=$^69l~9obOhQRjtlXGA;}@h99^wB-x!F!qf(WyE-N9OkzAi}1lk>ri7TW+@=RQE z{i!3+u2hj^qQMwRk)rG4jzGJ^nL*7k1w-X&*T)=zc83cK0?w-{72;hVbuz!5p@EwO z6E$NznRepmwd<4?!Wx)tkfEsSBaT43PUEr>$9ajM4A+Mpfp(qpV6Rh;M>v*qfx8^% zo8N1%qRD{G8a7UbWY>orfp(o5v6u=rF=di>eb5nTZ^SYlQh-)P(scc?BhcQ639yf) zP^8Hzt`9f?VS8(&B(R$;iwsV?{>Tw%Z;iZQY6KAnTP?2lI|A*kQ39is8Z!k&ae+IT z=bHnzdkzI$m55Rl7jeDM5oq_ENJNW=M6k^(yTE-#2aa~n5yFBT3NxCfyWZmnw0lm7 z0}lf^j%Fp-yB&dc&*9?WTtdVt+;qLm5oq@uf(@}kC}`r03*0+(sH5F;$dCz6oD|iN zTz5LPe>+2s1kE@YmuW6FTL15@{LYGN>+m-YkiF0E=676M$D7#t7uG7^&p-a3pU-SQ z=NHm9oUt zN-`E5a7?>w$+ezH1$!o3E^$nzoGRb}4ot5>nM1Pz=t;tphNA;x^qqooGDCM#Fwz!_ zcpsdV0cE#u1laOJ#a6fY)YaxTM`dt+B+=%NWn zhw0wP%{kB%M@k*xUQhhRauO~nKYL09lLmxuTUsZISXaTXr?(RH4Qmi%2#691d+;t7g8R4 z+L!ORnwITT*$0%Ci`PiYB+?6)`e?i&cH}}EJQ&vzfv(NdvJUQdhr#1&GN^Z;FfX=M zJy(Sye48fgL19qlJ^3Qi5fh=nv@ie0)wFD%_dlSte69Lr+U$o(6x>sY4pQZ~k)k9% z8SBl{vZt7nypR+iYC~@!%_j${5eQXS5rpyzWm^(>{)e^TPPj$;rnP+KYFf50k{(c6 zzGnTh=+$ERwh%?pC|w?A3>4PNbNgjC1vMIO8i}&vP`C2>fKB^Th-CCD{aP>-^h8A* zyv0cO{PpP`en)S;{Sc7|=IIV=ilBlY9MJ(-^yCh)-RkdS!_&gKS zhMiQL)%1K>Cqw=Q-Q^RtKA3KU%P%#gh)iqwwO7-!efj->((*Oym+O=Sf!8qLF|HEq z_hlohWh?V~3_Pep78QsU@DAI|TyM4FWm|pn3W)%N# zcg4SQSl<8Y9<_7l_MqY|cp50%`#~AFa^tXtC8xdN`j|p1Y7Q5mqA57)VX7KAVuUB>HmE^6TBYz>kiwN%P# z*Q%XR3Y<1_n8v0%>1tWhp}MA!^V$iigj0A1$(P|0r+CuUVS!KP+&K;I7X&m-bksTp z`oTJEqFApcfhc^jkV$LgqBi-~_bgzOt*hDOfjjF5?c`dGE)3ewsQq}nns4deP(NCg z+m=3WbRpXfj~>z!u|Z)_57aHQ6KMKPIVaax%#b>P?y!(;$}`=IOAy zuuZ;$0Gn)H%_a}rH3G27wOS*vQc+?v9vuV-3&#Swq|@M~#N2iQZYD>iKshG+iCU`6 zR}HM0=Ogt#Vsw0z;O^G=aIxvr5`|)}7Ntl$$SW}_ys%Ba{dd478&|W*19z@#qStJ$ z8|;T;)x4EqqHdvD3mDZP5;c%H>oy$atDdZx52$W%g#_jj3RbJ+iI$so=OTCqgsotb ztz5EAC3v|e1ZggAMkpe_uuZ=CCSa5GtJ&m%J2M70xmJA_)fy-?IO@>|Zw3lwlVlB* zf^*wRAs({|5(tb%6qndDlr zW+-d&d88e42Z?}|O((Rt*TO6)Rlpz)ukj(B(qmePUwl0I@OyzxTvxNn19yD_Y;w)! zU35W$GxbOegfOb)kqpzWYtYFtqJMX8j>+f4A0J zd-LH>uRU{Z$Mt2`>s&<_y82o0R{u4t$*tFJ|JC+yZdo(_^GYbmU~~?f8^db_ujEL+zakKWcTlO-wa+1Jaczv^9P$B2N43y zoiFdaZl}0|ZeHB@&dT>!KDLwCaqo;SCtURhS%1#PdA9?6c6R~`<$FH<0@oKDLH0HB z2=t*3xc0MNJJ^a|IT96|Qw;t2GH&$!;`2(qtdMxYme z)Aa^N&>Tg4`^B$yz1|UIpQVpDRT$UrIfCpP(<4yn(Dl2HAp6{N1QI^u`W;7*eR4Dc zg>tUnb_Cf+?-A${=6anYXpTpG`)1AcTTUR)Jmc>DZ`W@+g64SnxA$J}I(7ujG45Ue z{8-l=j-WYibk}!wT)*K6n&T07eensdD~_N!9&y*_f8hFcN6;LPxa*yN>3XdrXpTqR zb?0MTuW`xE=FKXL@k@x8l0`W4svoj~4s9m^LV?s~Hm2%6Uj z*@WvAP9X0b(A^K}xZdUnn&T07AG%#{bp*}P-nakxS*|~D1kKUjx4-jr*IOJxb6W1V zzw`#z?}KJ^X~CxCRQ}8Zrjc*I`(3WjIfCZY8@OnOgy#r3C-pgAVJ=eFN*ecTZ=PZ9JJT_1A<%~J%phW$}T&^$%pP1i@@ z^;jO?8&m1e=^|J4|e$B~Bb1?7z$EQBci4Qbq3Uyci zb*}&B2%2N1ySlG;{i-8qj+O3u?gw43bOg<@(p}H{vg=h&YM7(%_dH_znNE;7P4+zx zdGRxxAamN@d)8k3bf?-NbM$uiZ<4O}I)dit?e1Uuj_W;+pgDTGyZ;u~yB$IEygUC9 z*Sj1+bF_VT_8(pEbOg=O_ub-8y6$uY&FLcVW`4`{4kyseQy$O@-TlNDx!yiH|6gBy z*vkHkw;#U#I`GT&AE1F>+_kUJOz*yN`ql{Hq@>&o!7vwbBZ=na5*|H@z3^#_F+6}uVkYP>2F#|9;<}9i(Gt8~ zsn;W5aCINQ>GqL4o$5%UasJ@v&!#^gSN-3PB z6eEwdHN^We)%=(m;JpHjys+L$l?&-J?G6Dlj}!I&Xxbg(hej=XZo31i8PGgP&mp!e zQm2cG7D)>d$tAH|szef&rljI&qtNYNu0!{`-4SS-mz5wmu`fsJB^(cUjYv1D^^HL_ zAQSCSge_&emq;~_S&d+>-0bpf-``ato?JRrK-+q`kjF`^sd{=?CJS{WDR@bnm;QH7 zyBk#N~T2iM4-cEs5^!%IV z-_o2j7~b+Y5Ug(8MUSwFjFnAx%56}_XtpxWX?{}RB&}67Ss9Id=;-U@PiN{qF7Cc> zbH`S|@IH$4Xw)vGljTIRTu6?1O_!bQCfmv8_>UtQPvo%)c`^XsC-@kjN>)#cDizYD z)89{06`lUCQ0*4R1Nm41r^CD#F!~X8H!y;YI<&dn4M>o|4t2F|GW`x;GYkD3;ZenQ z3CecGd_2@?`msO5c8x=wm8g=MHR z%m#{G5O5MMMm#19o;;%AeE5<-mZqcrRHTy@`&=+>QDQ8fG74#O!C`(>_0cfjy`wlD z<_D`6muPd>uDgMM*zRUDirP}gFE;_7GYp?3bo()N94E|M`9dAEjhjXT@<@pkpnz(pTpl&o(-sAvs_Xm#eBu5I6m@?m*d7S>0QlUN2M2p~ zSh(a@n`N<`3^&rbjvl20`H+`p(m4VvrmRMx&7y-Sn9+hl?#~1}Sqr4geIpy^vN2!I z?RJo)(dOjUdy?@<+4`4xtd=2zX1IFpBLQOUAHXx;u1-h zm&sCm_0+^KxU;jg081n=UPOTQ(gG}zHhK{OT4Mp0DD+yJpt-aF*VqJ&u>hTR(nl3T z&1R;C6lgOKm0E&Xwh&Vk8(r{lCgROEN+2VUF{oX<#sbvG0xXfIdYN>~SI@Ehf;($V z3$R2c=0yakE-k2w*KOz!JIC7ZD&c7GQ~F@YmV|>7@m@#wJKDEx5`|uC6FhHe0j{wL5@P|DXxkUBu>j8<3$VoT+O^#pc)^{|Sz3T4HXyIQH}Qfy zZ(CY`B{udK5#ZLb081R(U27A>mlog}o4{OJfNN|5V=TZDgaFAL)Hb}m;(Ffj;!U*CWGy11Iy|C#-e>pT08*!$Mrd-i^Q?PxEu_t>q^@BS~>&AT7i zeaV`-8(sa^t&Pnut^CL4?`+mKpRu{ORoL>K{maUyH@>#<`y2fYdgCY8zX9GLyzuFcyy%b#!QTAtdF^4z+4ARke7Ph&8>E zyz|B}B2BSgDN^>kqn?H*nG;irnoE>HnE80Nkjuf1P!$@MdkMv-`gdM`!Xsa-_`QK% zshnw;p-K}3_Di-wZr>WNq?-wMM?+}6+~;$witm5_1kr?0Jj%4Amzd0u4ahzzmS&ig za;X81$~!^y5`vLYvzpdIo=s;H;uFS-4Gwq#NmstP54!jz=4tPI2XR3Sec7DLpK z?)mW2LFELY=mLj!Xx|`flvFyWKpj&|d7|NlCs>U@G!D*tAvz!6{rV2#`p~j>lTYeV zu9kr^Gu=zkrtU9Vfp90yba1Nf(eq7^_c%X{q$8b3kSPXt{A)MfFCOV&rwg{Xhjg9t zL*Yy<+ob#&ohTy1NLC9gy*^wML-ABzNN~GPUVGpDe=ZR#&2e&Iq!lP%>rQHXgXjc$=l)GFn`}+u6m83+>V{vFE2bG1IspC|Uw0fCZ zC0c3dJKqG2;x#`K7yqFOJ*fw&KuQ^D4#i;u^@516P@R<i5qHbJVU5%h+`Ly@bOhm7BK`SO~zb1_v?6}=f$BCxM$QQ2Zx z6b2eo8(M>g+Cyu#N;04I%QY{Wz)v`oymX_~Y7vqm!nH=MjVVbIhLbpI$vm4GbjY-* zWRXETBkumn2_j$4WFvuU!BtMkEX+wSQ&*Ys#J^1K7IWcPY?;d)QUt1Of@qFEX_t}!=H5L!omPC0{N;~X=O_# zT&o}ivYneCycCu(i$$U@4!uU8pAYj-x^0gZ8!LGovw&w2p!pYGoptBnEOO&WH1Htd9inSX9+J{%l6{_B_FqKdScqu8SuK zHN*$$Qn)L&+c<8d6rVWs^i2r0ihL*D7Nss+hjkC1WLm2yx=Jfnz$|LDcE_z~2%9Xo z1-wz{1mYqtvOH=Hg(iYBbpsag)sLU>klRL<$w%czMeTLjVYHm7)=;6ruoaC3N7=zj zvfm;KA$Ke7`v2H_^LWRpx_|sm?tXh06c9wj85kBBdT*2NFv6rwx1?>7wn^FumM-a% zCSB8&9q%ZvETRw4E8@N{UsOayR0IS>R8$01Ku|;k4KiV8GnLCZfcv*C0itCazqe1}5@S_dCXIN< z8I{BJG9E8bEP@%l^=6<|Mm?BbFO^DEI)RRSRHKO7(2`V07|}?cAnY->*UgnlDM#sJ zRMXt@3Kdw-M=j-GV3h1rI6!=fm!m6IHqk<VHq_`V@${4r-0nK}C`Z zMuiIba#>+hQ3D<~8j>l{w73$%gGD`6E(Io^?Nh+Ha0M--S&o(kM;^qVH7s6+fn8k7 zQwpQTG7^OaV=>U;d|NUaMG_a2WD_w;b}%n>sB9MFVHq@Rj!O1Ul9m+<5fC^@2*<{s z(kMLP8YnYNWL=6cSuY2hW!y_zLk%;@x@9L(jYLB=d(&t)+Qz>zG{x6@6v>V+O(`L~ z5;bL-^`-&$M(Q{gw5Ld8xr|t`h8e}(oZ#`Q$2{7JpNc2DH`mRQa)OVn{(E->>=`vNYOWn^ksmbhA{*( zK})AwBTS(;)=g#|n zpRgvUKn-ZAZIn}B-zdguh%;R^sArf{@Qrf>hB$EEC`aHQLJNUHdu9hV5B-9kB0^%5@z66H*Cc!2X3Hl~#+f7xTGcU{|n287=vx zSUa7mI6d-?qy&}&!M3be>*bi0XnD(thA$E{#_G&V4(u}UJF@odE+Z zfR#|r;ZBEId^GE^rnzFZfcQNO+-6Na-tVVVo=9hhx4o2d+fAWtBF#9tERCX_aB>MD zL;k@6yLz|Y52-BwEu?Y?pWA=ajqQIs-E0_AIm~=Da7g99XyW+agjB-+%#g};^A(u! zUSde4)od`?0QrWVSL88kP6Exq-N_KElBp5F=}YX0kjmbz1JB!?@0L{0drN%F{(WUS=!4$6-t#u_ zr476zRTb1UwtvrKwf7`%sGVL)YM}nnrrvW`)u^O~`)``Rux{|41N8q}oUytX`sYRN zTa5<8rh^?d*D$aByp{^p(C;*>yNF#7hymhGul+v19+^@UofsJ9F8V%3ll zD~(b#S#Dz>O7h=$S#vd}KJ)TZD=%^(R-EPQk!IREVY%Di*#iMdPXY_ku|zG+sk`=RbE-O)35%#>#i zo4#pUn4X>b{1iX6W%6T_2N;ird(OUqD1Z0h{w;NqB-O6fWxUunF9?E+?e8xqyrWb z1-aXZ*-}5@9f$n)*0VmzJ@TMO=){dl!ax)s>+iAQKiP?(jpwW~y^cfp}cKK5medjRK?6s6l zeTNnQ==fdP<9#2u?0oMJnvcw}D=+IYHbpxMY!zz;GgVFntQA_25l4b+*I~QW!9Vtjf?Mn>$#0H4`x>m z>M^!Nkg7gaPDX0Ybi!d3%r?9g%e3NNhHzq?LIR|Q#?hu3MXIGf<6pn-lYhMP^})Md zojdBmU)@|uym;QL9TzUxk2o!Q+UWeQ2X0_j7J7^eQpbq6TX~FaBa-av_$}6o$Ie^J zRt3$A7Jte~aYTSInLOD(Yd@1na;X829^OY{j)|lO6sYsE9r(Me+JKJ8XpuAp}XGp z^Z13tQPlbVKQOLBXztdL-(7sZ`__9y4;}j_c4e-|IFu+B3js?Bulk!|B4HHG=|INe z3c1;aS80%8gvr<%6bOB2(0z?vAOF=QcYo}-YoGRhmDwI+(m)ukPA-(uCyFj>o5e$Drww8=N(W?FaSAa?5;_>-cA^5?V)O``>N9@$nMc2L&CJ4YPrU0V4}NHD_Ns!T{l;IO zWq9t8tw(W(o_asWu1xh9CycgYo0Q^s%Ack{DfDX4Yqm0FmnY02b{iM+c!D;v>?~PC zQ=f6+yyI{G^?AqM{Ii|4)Th4x$ER@YmB(K9{H>QBo;v&FOFs6*sqD&Rk1@|Uz3oOK z>yMNYdTW+8QW=hMR(%ZTD;5lL!Q)5t?obr;18w#h3%lNY=X2NSPd?($*B|uL=kKLX zyq!)S|1RH$4*%*+haUQ)FaMNXndmVN@hERIWY9*rQOtr)(`&cj^BObk;r>q^Se8L`t~<|>AxTNeTH2b z?`y2L)A5+a*(t*rv>lc~gU@K7lNYOcpVA8GSubds1I9CH%4(w``#b7?-g)8O?~RXc z|A3{RjmO1kCFCGuG*V6^%I0!9;|u9SCB+`VsBl6?wNdfj?U9|o&Bg!v z=o>zS+LtXK{>aB4`>*)6*IfF6hy1gDxax{Yb_MD&ZWB@PUXbJb*_>3#ws4=_4^v#- zz$PpxB_V4j-}Ue% zSNw)u+0tW7l1+@WD`7=u5;=}X;V8|^;NhNBWPweZGD&~Q=ZI7J8u01f6U6r9=<9zH zx=uJ^cV^d*lvj3}?&AGdGdI0Px?yVa_m5nA3cE7WW6UFgl+{%9o2%w_(i65htPQd5 z4@QD`S;)CrP(#2SG~}A8vQM*dHg)9h-tz6oZmLS@8z;Z===Gluo347pGDAIoY2cnC zkNnVE+3tBg#z?)~3g@#j7O>>3Uf7i>MXmY@BGU+O2qgj`H*KRcQOx8pXrtnRCo1>} z$IagT{wL4D&fWdzKYjWm&Wz&v;L8qOIOv@63zNIp?zuh2M8V{#SM&{iz!Y&uV~&Dg za?4mGrHpr zI^n8!eZz7T+dZepxabdOSR=)Ge7eFcEx4%7^qfE zlB@Z07BwQl1W(xOLevctHVUUmh37TKBjjbLoVff`dEwT#WIl7^nWN{tBY)Xfzx>Nv zfB4yj?Z5xDa2eY@tH;=tEEkM=2jdR-+^s+}oNDrXgd;?ITX2>;7L4JmRWYqpO?g{? zRJ`nl^g|Cv4!L>!mb+eFn*cF!AAJ7!3qN}58|kZl^Nybs9$8|$ogQN{$64xREQ0E5 zd54_LOC`RZDa%FDQZdm_Csq^y}|KJeEGtaQyc8~E8$(4PL6K&hs=Q1N_9Ql7f1?T^C=kIQR?M=4{pZd(% z{;gLY{j)E;d7SOGdW?ri8SFE@Jb%HPj`-#KE;R9)A`Yf7hkx8yW+YBO}i?`Iv%`@?SlMKz}RD` zfG~-C$54}-g0)>Ph1wwt(TPPo7Hd}2r_!hlR}ebdrnHsNtMKd2yy)}y-0``yZv6QB zkG%BpPu=G)e&xc`R+Q;4elmU7?JuxhkQWLV50Mkt*Z5n9yzhBO>Cq3iewXxiKmF|C zha#umaCQE}J8KWzdhmnCopvSL1^J$U@erAAea3&>am?+b;m_T6=Z?Y=+rRXOyU#nQ zRJ{Lvy9nO)_{H5v?eAi{AdeF;9wL9M&-f3|=bp5mvhuYP5B_{>+ug<^-_76nkmJ)= zqsQF&<=wfOl59~Hjj&CKNe8&a5ng_rC zJ&(R^=J|Vn#dbm7C15;65?7z`FC(9R@=MDP?htSJ!$ZxFwGO$dJ%2ofo&Vb_j(U?~ zdXg@&U64B&h}I0atomt28@O?!eo_rlve z<7Y(duk$=O_lxFZFUn{9;63w4yz;Cc-SXzf1+(w^HroYxl7O)Y%w%|BZZ_X8PFJSyXUem)~~q!#V6ZOSi`w5-hXpxF(18g=Zj~K zPCwv&K?00H{v%*KMCMoD6plNbe|qSO=RCxp&K^1JwyQpS@mAs+rT2a0J^V3Gf9R?I zKK3tc7vwbp#zW*Y^%=kRM$f`8zC2QY?K>`Ax~nU{`QrB`UbyM7#B0BLOs)AoOD4*8 zLB1kjJjC15KI7)^?n)kf=wstuX5@`e-7}GV!~O1eopkWZ{m;U?e*XjH^MEnPLj;V6 zcrVgtEPj8R;{5ZZXWLbWHQV%I$K7)IH{-Dhf+VQl_$^QX^G z&3$3+%(nPJoNbZA1pVWW3aTP(`GH)RuWZPLTp#)2XZS7#j znQ#Osp;NCm3*d+)x+aE$UKUlJd9T^)sQ8hlkSJPxIV2&rNIWl)j%wU!kH`(Hl$Byp zvy{o3wmAqMua(@vT;3nSV!@Q;SjSUVd15G&vDZyvi=Z$Gb^Bcf++q+dCT9VNQIv45 z7;TVk9**ILZHYWtYc_Kwz9QyQ;JrpCwT@@5ckgX|0WPpMf6bV0hv9U~kPIgCnQE?> zOv4@_Lx+R;KuXp}g0*eFSc@sN-Ci==$ob3Z0*S8UnNfKLMJkgkB{+|z9I!{iXd*5| z%++=^s4thDfmF=rG$jkkgparQwt=InDxpMuHI{I>YSj)GSjQ90&GdHA!P}fAB?srT zc<^LU&bW9^rs+zPuh(d|Q7}plq=P34deOBll5+a%J_l1(+?8w&%O=|EcxF|eX1oqk zKV-oeuQ(e@iZ>xmA0P05u((vH(~8H_RS#k*BjrL2-PY!EzM8+54YHn8(dDmV;dMNd zD$gMfI=3~QHB2NNN}WkoWj-s`*8lDamFE!WZ`;D36f4X{jz$!y*h%ob>tWrCej+sRT&s<7ULIn_iI8MV+}zYFw#tVE4An_x%S zjvjQ{E+!&Lo5`AwTWXzb3$54bZBkYcl~hw|H_4JO+H_^ty0B-N-Sfs^U?XEVTp?p? z)ZRuhz80FuJB!Imf=woxp8A?A=w(LLE{HYKO_R)_0grlSBZ)3kB5kes!i|W}6teRa zLD)($K0`PR+rUgNg)2q6;m?+$;YujMt>YO{d7^Q~oldsP8I+HkXvSsYrM&Ef@s{MZ z*6kpL11X{z587gbrEL`3jx>m5r6`6${6A1*?H$w0VU=gSV`}huYt+F;^>&^!R05Tz z)m^nAsYcrG%{k0j2k6Qr3k+G>7Opna3TO|EV~m64$-3ZPqo-}zH%c4-Hqb0KqX|Tw zMM#NXbKN$0&r0CEpWU|X{RdJoP)&&>&Xv}Y?pY7KM@lby)H{KTAZpBn+4S+KNdegk zxu7M`G$|QV#M=(1oDmeITi_(J)o!8Nin%1?OZk$ydbAL#WCSd+MvQIQy{9@>ppG#q zqMh~X>=}=t-HB^YQVSSKurJ^b``3ur_7BDWd!*K7Y?Ovq?Pmc}4Fx|{4z4A&@9`f! zO6Rh3)CV3Hrg*==BM2&4XQyppkU3DwV~#{Q7*ql>QH<=tMtymG+OlI5rMF8Zsn2DP zuX|Fq)j=e7vJ@+1!5AYeL4on?af!--UPjg5g3gJNbX#$YqER-x)2@6fK%`xasmQfj zg|gSAREK2$9?Q&F?k zWry8@9=2!bhQOePgiEl|&alVm+Qt(U-l#fxZ@^2r3bh!ve!22Gm8ZiX%Gpve2;ykX zB88_rj&zEbc?Nt~hD>@BEqjcuN3c#KwvEaLF)^3(InXFkZPz`-dXL(v^0X>^6!UkBfMr^&WH1njn9dp_ zp*&Ay8kCQ<@|ee2k0flRLOr#uQFKR(1!p)DN!G$aqN-%q{YEFNJP|{|iebTWC?WaT zwhgyrFv8)ZGC?`&^SjJ$tG>xr=$g|iG26sY9TQrjyW_)1BH6@ndL2)@%F~|Arenc) zP@j%^t&va_^!cOoHJC+PSzn76oi+y_pc+w>BMjS!R5Zv1!8?aktOk~>USD|~&(nK6 z2S0M!cQohR?EJ`UMjskkIBNc(`D^Bv=5Cq`%+1ffclHgV59z)OcK)xK`OHjd`Y+R8 zn+{GdOkKTj@sxh@>yzP$$0phnQ{&z76UV+fMvYcS$Du2r9b0bN5*T@Sq_)?+$n5;6 zwATy%^W}dm@E;587Yh`Z4dWwcT(Gc7F;&~14{P;tHRyF)H?I6@gT-Z+DsXaRMpk&w zhw$bC*I&0a?4!ex5XDfKpLThCE)d_#iObktg$;Ih)^&aPwuULtT;0z^m|)n&1aOw+ zP`0@2+>0r#|FC%3+nud#KC!VW0Y7osvA4T3tnH$+iOJVpx0Mct$tW5PhN2#)&&7FY zj=+n{=-%$mu(pxTX2RB9x5?cZEaLL6z24p0ema{8TzlPC+7WfrM2N;X2F0l$SeNsd zxP01Pg$+0=4QsF48jJdTE~k@;qB!kvl5UFOgW~e3dlfd|s5Gp-ZmYxZ=4mexNXxqG zHXbD4y5jQ5dlk4=>y7VjeU1Cg1R8cO+s8&+u3hms8YF}9xZb?yJ@wZ7d;6}qY}=c@ zwzZw5Hqm$8b(`pGGw)osuF-eTHd$-+UH4sa*|JxC*E-i``mVihGkr}vm(6SR-E+cN ztM9t+ip!?G>bvJIdhwc*>{Z{j^T}rRU3=YT z`XW1*;Whg1`O~b`cinej%|Y##b%(}e=kn4Tb@sIUT6NZaS6p7)tN8|Wf7{yYHZvb^ zfBhPL_q6<4eb;?gTs~=U`mXCnzRAzM?z&C%wc3lzJN7DY&&g{q{$O2u-DU#8gnZ&& z1@1Y+>`ma>>oyZ;-MReQHNVK7kHxjW$hz-#E}yW5e9y74mVDiJ#pUDodb@j0PVxn+UYS{rvy0jLcp%_1E#UwtQuOdjEfT{ZlOfQm^W( zsaHe9A?^E6Xie&snnbrp>J`$; zjv2ElA;gLPjyKqJiLRHNI$5s6Qmqj)r#bKrBw>r@tp>s9OqOFw8|rcM>GqD2(bj57 zM9C51!hWj7U>(o`vDyUbYc?2lAXTQ4W2?UC5+6g+dbUxz-TYPAo7dDC_c^F6Ly6U7ri!b*5wpT5=_!&8FlbDT82aQ8V38bWUR(f z_)?25hLdb^lP=6Eoxnf4Hu!2QmDvMJIWr#)A9uoi7nP@rAnt$ zB&?yTBxdL;;@Ppogj2{%@4V^di@ZD0XvJzJtBICuE`yIYEd@JDKI#;W*@P`%Vxlry zPLMIHwMqy!x8Ou~NS1P=4l}{Dk|Zcwz}i4!T-_BFGs!aBjyVSE|JS>9E&rbwxn*SG zu7xWW(hG+97w2!EzkL48`IF{en7d`}lDX68PMH0}?AK@CJ{z6gu6tT{qwXSIM7MS3 z$(ieCmS?D$Bc~sq{=)S6)Bfqhr+zW@xv8_Kyidp2%##iB79&DiaT74exXC5xc@>VGqtf`$NF0xs1Y<=p zra{3X3z!NM_Bto}+}>g}TB|0Ccsz%4VLCfI1+Pr2G^Td6(NtV5m*8mn3TcPg>&*%n zor)F2W-U+U3o$YsYzBN*i(z&WUYSy9LVlqXDurN$A&DG|A)-Jx3cO?^Z8gd0A~CRC zA#qclHwA066Y$EUN@MhkelM>yEq+h5A#(_bF%jx=r{Zyt1Y_l^redP%N3xRFTbLb( zS0+>%Z-6DMV%E;_h76mh>SkUH%0@)A(e?sSwX~Fwq>nYMHG>zO9fMcKRT?ENSfVYV zofN910VGK3eN@WkWiwLM;BhC2w#$e6kXR&GiKDZl@XDAJ&}B~h z?3hT4O;ZCj-maBqx4ifQD2M9)4uLf=Pud&+Z%9jRjPPaxx;4FLJ1PwoW>QQf%cLR!7buM+;2dFL;haSuLqn(w#EE+v z?iS%h;{k*21-RQ*X-sG(NBH$=3g$93tIGw80tjvlyPY}AYjEmum?$!Z2B>a;7Ie?U z-IhuNBOQHMtkfK-P}<6-skDqY6~*Nb1WZm+&J?+hY(Wcl0uPsT&%xcMO4AmESge#G zJmFZq*wB|_v8p{?^V>X5Gwj3>I>|=dX^W|vZ!rg{S1ERn1LL(WiAbvMt$zgoZ zAN82ib~=S{PPQ(W5(!%?3o^64jP4I`x31D~BpPm*3?+pr8YP>7s>Yh>R63in+w?H& z1Z_0(?G($@#2iZNeh+sQm1c+#ecdx~x2DoqQ+ijLv?+4ZCnGtxEyI`M)<7$TxIoR| zNSyX!S;`kPRSYFY_up{0s?vxA3+mnAV!UGDaLMAVC5xRf83vtzv!t)7w}U!9LNgyJ z*mAV)X}DWaX=sHvTjQeSs&_JmOb5uo2+||x0@jGLA%L+utjg9%Ylr@#@2qEo=yKQz#*;XAl+7j7P6P5L;xYKW!%@vE&p!+r41*tE@F#cR`8@c+L%8Cr0-$+yyBpU_KbU1dQ$>xC>HDz_Whv>Mptm;Vwua0l&}S zgPg}We!1B{))3zF!51b11LW{6rNx*x(_ zkc|OtAEJzi?gwxeWLrSnho}&uy9e%qYzknv!Ha+Bz7KapD$NjeJ#^oLyR=F(L>iOs zyKt9MX@;oAp}QOIk}Ay*g*J5GfxAJKW{4UZy1U?RK&2U?JcjPuaM!QW3{m+)cPHE> zRGJ}DwRCsDU0kIZqF#kc<5OvdC`F;W9qxKnnjxx8=)MJaLCy;_OaTeqZE)AE(hN}> zLU$|Nb@g-`JO*EP3*5z2nju~@sx(fOW{5LsmByjc3~_v>`=0-GlDZ2inTNq>IkG z(m5^&^wcn`oMBcs`%X?h?@C8sgCW*T!>pAC(E#mhFhqjvFsrD2C)1vH<*vR4L#)k) zS(6Q-!D)RBhFIwhv%(uhgH!t&46$w;X1zFw2CwUDFvN;-n3d$d6QIw#a%W$IA=anE ztV;*c;FP`wL#%d(S>+C*!O49MhFCifvt}Md1AAYCAy(DHtfu#!w0_=|w!Q{KtkH*A zn-8LaRT~F`S3DeMr9X%Umc9l<>>q~NHw>bIxv#+xJBwj<6#FhTaNdltRZGl&LAUxOj`P{Zt<2GIcSYcRx)Y?z(cAQ~9@8Vs>l8)lET@A4Gq zUAffPV2GXHFgw0MG+69wFvLD`nEm4*8tD5P46!R6W;Z&B1}F737-Ek*%-(ho4R-W3 z7-IK3%&vFeMNQ7T^2ELdL+qu8*+UPa!E5^(46!pGW=B4V1}F427-FA4%>I554UX?? zFhtqOVNMGM(cn1E27{j`40DdK@9>zJxvNHINAe?ER_4DmlGmA_?@r{#&12Bi-ILwf z?93MM_5S%>2#+nNpa6!pVo0kIL)?Z0XRrG_JzPt8?42}LGLabCbc$#*5DAuf52(iN zk>mu&wtXH&66jJhfVrA6L@M|^X(^dU?LN#!0G^$g46b)N;1t5Pqw4pVAxnGQ^(10a z0wUTXBQ8LTX9AIIE7(Ds{z%FY6w9?F63it%xkk!JTSzS144^$3NKb}0WKMaQEERU- zVn|R*83U@Uc7W^20=&&49|k(O%>i5rGy@$xN61Pv;YlkIgzjC-7s0iNIR}b&x52e} z#81QKplC#*))2}|||7bEtp(=4QnMfrX9N%h?359gi2rgFQvfq-5R-CL{;Pk<~4+ZU} zp>G~|CL{Nk$vm)0h{l=;H)9SLQzGv%86uI?2BXYrbksb?a1934@IszM%2UFmx*-DsH0v$sbwzFP78N$(`l_N_rTFS?iM1&!M$?iDd zOlBbW*C=xb&73!l`MqX$pqLYqg;s`_HZqw*Y~xf!&l%Hko2XO}b24jif+#AN2a|KRKp{vG4|%Aen$M5Dpet3`WwRN0j<>{m1Da)2GP- zYLQb?q?JsT6az&QsjN|nHz^Os*9jXPw#OSqlipeeCcEnZG}-=IWMCzcMbc5a&9}iq zrW2IQ`7j4!$<~<+O>wSjxZE~?jyzGlQSgWI@pvU4NC)+1E9gAuZKT?@B9qjcI>2P# zKJZKy++U9A3W3I1N+2Q!;IP})=AC>kfcQ2r85S_|ZKnlyhv;TCXihV-r)tkQt??pK zOlM4b&Tc?((6+;x2m+IR=YTVrA-KOB(;buo(Q=J1nks37yT)@NOTd6`U@}xeyje#c zCV0Y{iH1B(s8CA4R(IOqDu+^F3e0mY*+|$U%}}!h-FCp43=ZzMCuYGE=r{CGc5X|ZDY7t47R`%^X`$6 zEyn$`_4@w}mjiB;8TQ{?ri4;r8SIDgFcXO(OdhVq7~2M;46QdBB_wI&+$MiGYcZMA z5*EQw)Y$;je?7u@NEczEMGMub0>`{XegCf;$&Spwd+tQt*JdJ9zn(mI;^485jlOQn zJ>b9h*XP3Q?yXbM8QYdqBO}l!c5PTa4Ac}|QLe%oPmMev|rkAdt%Here+MS26=1R$md{+*B5z9(BTNuoTmf!c?qi*M;Zn) zgO-4)U$biJk8ft`J!`F~g(P?)8gN8nz7TaFnR@>%ICE@(D^z(HuLoVZ7RBK))>YfU zeXuHl!yrB!!~;A18QO|daV$lIgZ@^8DU~w8U@;N4TbNe0l%rKsAGK=gk8Nh^JkBE7Tajm<%|_$NjT1PK0DPEI0`W| zseq+Jx~l$YSv}SxY@{W@mzr&lVsJhx77ia!rruu*Tz8-y zVsT5#jdmEs1&+f)P0qW~YR$pGoV^vtBaUV>QchuUUlcZHBD67P2xFzP-;zruTio(+4|F6oVAMZv=rEe<%VkAm{-Y!T~?>LXT7{h`fFy|xfm5VR=-&kA?Mc_5j3znv*# zAtV*eS|Z+5Jyn!~jG-I}IX4>B&JdYI)25t+6nr5#RqW)6L{^`TAg#RLoO0B|WvoR< z%Sg;@>5b}FuA2ITo0)oTO)Rg2kAUiyZt?({tHG$=UyntPxs?vLc)pmcF-E+_7QwkP z)7)SM;#4XygEMiioo|=zWg%9#S8XvmOP9QsXs~W?bn=;6uHxYhXm3;>zG~_ZY-Z{` z>k_JSHH$e>7ZKtx@<1BZ`)#iT`UVHRX_#TkBFWjAAmavE)Eka!Q012m+9i&*IZ8>1 zC`K*S96^;^poNAN^XnTWf)uOPNIWlQ)w%kxRa3uzGgGf!SNmOVA0MMIQ0M)C*}}xk z`)ytAYLOf`^GPghH2?dK2y>;~~R!x1)W~N@du6A)g+(S9tr2l|2_5PYbuv$Ch2D{*h!eS+2~NbSROMm7TM3G(UQ9V-!^j5$ihVn z2hCqGzjf|oa|Y0_ZMjV*YZH_)AildKyufvbv3yOnZq%H&Vq7Lbi<1$m$J!Hjl}76mM^DiD_zX7 z#!`AI&zSQWk3+PT!j(m$FP9uh?%GGBCfA&iSudLgO;&_3=SqXQEtOv+O{fARiySGq zeTav%P!6{hHrkiih#$PttAcU${1f%v!GYwiJ@V4z+QTMO0h1J~i7^$Nol=vtCV4?2 zD9%d6B*E2b)@(7wS}5m03lx`PE_=Bq0^~kk-yIl8?%L;+CReD5^>)I>B^Q@UHaQ*R z8ckzru_o7QhJ>~4bNXy(0rzKW1~LGuk=tayTM4MY@6YSI{R7EeJ85fjnTXYCq$?f9 zU7^}3*@^_MVVq1EOMDE^XW_Dew{)sRGbArA%1v{jNGVO;3FJOp-%Si8ckRi5CKnNv zT-4DDyBEVtF4);DhZ{s*sPgryA9jVE3c`cZOE$cesPVLssvrbo;MCvu{`ziwAi0~& zSH_61!_+88*xaa-1m-M@e^^ufE$i zh+KG+c|(d-K?;10?R1=O-e7UVRmtHavyOn^O1UUGkPQ0G9=i+9XG+bovjB@p!BGct zzhB?&9Z2pb^JHw%kZ$1NcF5rH<6Nc~pwcm9F_vC}{eD-&OAs2zc(VW>0ha;tq zT%^?T4CHzSlDo;g5%rdBGD&zEbb+q56P~)UWJgKL(V)`KqLOel%WgdD&zTKIj)pyp zb`F$}Sp;(LtnYRYBzKc}atZb|V>Yv=EEX+N#Ft0=tzTRS`t>y$UyGR_1)M&ayOZ;TCu9k$qsX*X3Z=jb~0KFnBm1_vQvo23Ebyy z`NBe_p2j_d7#&S%D zNV)TbV=uM|lHjg9c4y1x2kefOyY$=zh0L`xl$ z&&1FyMp=#dtj%pV%Z9o$$4MbDO@=I4P=P7!tCM~U7b^Oawm{S_sN)&PeceEEH<_<8 z0Rv_ACb)c!gf)zWKaxPm?pGP08@7_6( z+)d`Is4d_Sken!j2}*Ia0x`Uy=-D_6-9i4H{mu= zokm_A&sW!X+Xs@n$$XXNUGAt%7Zq>AXSF&xQ#Bhjkj+Rk2h%OMZ6LBz&gLr!;U&gM zfzop(pORD8?N`)y+Xj-m$$aIl@EB$1@pi6U?x0A>4Nm>Q-XKydmJk_Vil)3BbEK6H zS%B~QK$l25DaR~*xz>T?ZZcnS`F0EwBki)W=E7}6y^^olBV8xVogi+cJ>cP3EhpuY!Rl$%{^h;P1pj4!>uq6`3PVaRey8cXFTTcePT+odaJ&q-=oCVW|RPcB@ zqy@w6qO~O1twsX3Hp11MF+(bimPJX}O=eZ@Tk5+_1KjWW);XHoW(tL!7ORnpl|Ru(P;rvm3LC=24knG5WKf5EX}TUcD!zHr3CK?@V0UcfW+kIz3i z|HJt^=f6IG-TbHKKLAb;-Z{TK->}x#(PQ4x2l9&MvuHpPQQf3#do% z#O%Yf_s-rmdowth_{{8wX0MvPWcE$7XU|q=bF*)ljm+Y+yJju3C(Rx`d-&}9?3nIP zpnkz)x}WOq0jC%@>As-*gzg&MdvtHtU8rm6in_Fp*HJpRZl?~>ouE5PcZhCAH!|}a zsCV!PIPti9=9ZZoW`A$BW4bonV5cY z`WbLK^5FCjr|+Ep`t)_vpPK%_^cBe5ElZO(mzIQ^6^0>f|ZI)NxZsPQ4tQ zy8LDG50g)T`V03?-Zgpi_}GJ>9>ksCJm|WyPmO(G?255>jxCRM#>!ww{pa%^3uqRY9vy?g zY1^ZtCJp^`)Tp7Kj3OF(U=&_O1`XXmx}>2WjxK8G2cvoo-7|WUhQ2qtV-=mKp}R+4 ztD*0Vo}i(-MvvFfw?~g#MaOFB&e81}x?}Vh4c$I^w1#dQ-L{IhYUq~H*J$XQqercx zS8M3z(IYkV_0d>$GyP>~oXc_v8hTaJMkA^OUUewU}(4RGQ9`q*-oeTX@L+3y* ztfJ>NbQbj7DtcB!4d@RVDnq|tMbBuc1pT*$^3c;7N_(M8hSZ&pN3|kA8SYl-K(K7=tmlYpda?p^M8bXprIF_ zdo=Vk^!-)zJq>Ybq#$I`r0bGNkbomzPgHT)X@8&uW0CM z=mrg40eyKDU9X`_pf73YV(2;zy#@N>D*A$kE`mOAiVeNIDXL7&x7 z3HpqNiqNMuBtxIlkOX~FLj~v)8afmDxQ00BV;TxTAJvc>`iO>J2YpyWR_H?-ItlvV zD*Awij)vZ^p{>w08hR!4J`EiVUA>CltDyyGMMFBMtD!OIsy=$|Md(TmJq2B%p+}(q z($G(!_h{$`(7QEs7j(IXZh_vVq3fZ`R?(#z`XqG8D!N!hAB5hip(~(wXy`KN?HYPJ z^fnE>33{uBE`Z*mp$>GBhFZ{@HPnFKq@g;rTSGNySwmImjT(}m3pI2mbipb*UqfN& zJPrAwb2a3I&e71x(AgS7ptCgeTBxI;tx#J-he0h39RxKsGz~R01VQyadiJl7qM^S) zwN+Hr&~s2lL(f8G4LuE&H1r#&sG-LoSwjy)l7@Z)6*TlCNL)pE4Sg5NY3Ob!tD)~e z84Z045;Sxhl-AIVP)b8zfsz{fJe1JTwa}RwdLIp$5dPqKJm-P*_6>6w*))qBT^7C=Hb%auo$NRDuE;DnkBML}*Bc za1BY2PeU2VtD!jL(GU;0SCLCY5eU-|2{|={Lk7k?0xiy|AA~8dLFWB=o!eOp{F3Th8~AZ8hQjWYUpPWqM`dC zSVKR83>vxQlO(XBthF&(N+x!&}&xFQ5t#!^y*b~q=uro|Jkw}=oSE-Qrds(lrlu|j(r!SSkv{-0vcm%*s{pFQKyt4ig@&z_6<>V zvv>UvB|0{4@;8ivLv>0)9o}SyNf*3Myig0&5{$AYi$&GHCKS^`Rc&&zabU~(gFTfh zRVpe%Q2}IU9sTd^lwz}4H*Xc@1~01H-l)Jcp?W)=Pk>$)v_TFqWk!s#oon}?p5ky6tHW!mF5~MZOWi&ikrECDkw0?m^&Qib|iMc9X?~tlqO({dS}K?y>v(cN=yg0|NhkMM_PW%cl}j zJR#*1s;oVECmIPQp-wL5o{oKe)+kw15{j_L|MlpTiQ?Z_74oUV-!N!?mPo00*SjcR zY~&U7c6!XyLRraye;9#s<*Lw#_ZWl!c)dDl_N5erHu#UD)fuc*&1dtfkfWs%P(N3y zDJQGpbASIX4t|XW7!o}T!e*<%U@}^5pe@*@oe7s}X{w-PB-f6RpP>rL_6{=_MJ2>j z&Za|Vf2-lj7Gf@^wPASa-9hcOoQTI7m}GC%ld?thndpoQG~cQ3z{Oab3!1B>qwY4N zNY?K}+MJSK%9cE=fuJlzr(uaX=`gTi#!#?EZ5d0_=r?;!S(HBdoZc8J*7Dh+kOo#c z>*z#Q*x=UHhG>m$v-g;9Z`r?9uLhlN%>UczMja5nA+qWquRnU2@>v^o4wMBMv>Z}L z&z{Z7{zVf2Dbdz^{^$);TfyU9y|nMB@qTySVqKEd;qW() zQpfu~JZj+i3b1;~o(J1EZHZP3rYdPE=&Pcn-&y3^0dFVP3I<4T!<~J&_P zx@oVy$HWSkAf1Rmaq!{|?-1me#sxHD{o(-F?i zIk>bXY4fFW3SEL*Zfk&cGME8zNzzxdt6q^}q0xTYFmUMurQfMrZe(8hVYy$LI zd)4P7>0+kVWWy9=G85UluTwO6%I$nJl;YbQ1&fxlWIzR!HBd$mOSG^=s#J4ijdC5q zW93vR>0OeR{7s*Xlg{iwj~VsWwf=vHkN8FwPM^PaerArJ{o?Gwy3)+8Gq0YOr|y_K ze)5eI_fNp%7mWRQ%shH2^egDJEf2eSKmxeb1wySllp?OV?=Rl~@V`j{I}sfk#&oM@$+u}o0_1)0b-zkSc+Pq|#x zcjvVO>$`nLuj=~`9zR53bxq$rJbs^g_B`J9K2=Zq;Cddna?+X+Fgi?8cTLZK@c94B z-n)QFl2!$xRoT_u^=?q`3BrsG2yN04k?&cCAu?W>kr5dg8Sj80$&Ad5$jpd*zkxv% zdgxKqd#^L)N-HXeh^x*hx~uC|bP;^Bq9}V;clW9n9AtIza#uc|`$tw+S65V4Wn@## zG}n&#zW!>e&g(z_IsfyV|16Yr=JAoCD{1s&zBT&d8&_OOkIbA)Jkx$t(wiQ?P{x_Z zhi0}Kdc?Pk-*Dp>NcSz_i*LN#qs0r$lm$Dg#d^!> zww=+4=liDBGiuYO$9HD>d+94(8DDxrihB(52E_tj|0Y+)Bfh@GWAry=oa5^Y<(>KZ zeNk85Zxl|_sW+Gw%t7dfCH?vn(!s%Dmd9+sD)}WID_yGC+mco~^Y!Qdp)2Fpyui0M zzv0GZudgprTkfWaH+_Af8eQ3Tf5QFu(w1)t@4NAht`=W@Yc;oVVVP2Px3t*nf%CV; z8~2&BUtDp8ym&%p^6?8C;OzIiLLPDUCEokLDdZeyUwGU#bM_V4l{fI(`AVH5@8ui! zx$+)y_9ZSDZ_0a{voDm>b@osF6opWS+V>(!f|-u&*(#Kxa)eEUXt{ZH1f ztzW(P$%}8j_=*d^f8k9RUIHS_8V*6wXZ+-;d9QpuL}HfzzRHf^%qu+)onof z>_2g@zZT#tTc5wL*d)4{zMAgI8G`6_rwZ#CaF?mhE$;PZWdGmA<@S~-;4SWTXJr58 zKGDFKK{^RC^2v^A;bw8-m3E05-{M|pM)q?)$SzR^T-x>WS6KEF7CBvWdGU+ z*(HjJi+jx(*}w8Zc8Pl9;+``j``>+#U7{qpxM$DEe%6P9Em5^x+-uCp{-qDHOB6O2 z_v$mUpYcIlD331gSu?VK;e+fF71YH&b4K>lKFBUnWL?~=&d7es z2iYZRii&%c8QK5G2iYY`wTpY@8QDKyTz_(zYVP8mF(dnDKFBUnR8-t6&B*?#53);C z3KjQ?GqV4;53);?{}lJ$FeCfle2`tDvb?xgn34Uji_fW-DLyamy?#dazxW`#MEy^3 z@8KEQKk-3!iK3q3-s@&$|JVoFB`S4_d#|05{m(wg>JpZk42vcswu-@+2_6xtDiYRB zOG>)gfSd-!AVD&(f=WjtaZjI-{l6BUEH85?eJY4g0UZ zDefsVvVY`*>=LKq;+{Ms`-eWrE^(4A?nyJUpICf2yv*sdxF^oY{(%p&OB@x8d%}$D z@B1LT#EGuBm!FaSxDT>R)V~z>_!-&X^Felr(^7FSHzWHWeUM$^gj3w(W@JCMxcuNU zr;_4cc1HGheUM$^AW__7XJmiJ2iYag`@}tFM)tRTkX_<=LIoApd^_TEDgeGXI|h+5T?;8UA0f z`H1|IXj-e0=8vJ8#>0-A;ICd;8C~e{uVJw(V_v`vqH{-}>#X_invu zOWgYAt+mZR-u&R^cWjn7U$yzXjn8g;c;g2)h8x+9m#+WQ`X8?UBw%pwyQhJB8n~x{ zdm6Z>fqNRbr-36Hc-c8VaCtxYABn~ya4f21^0lewbbRBnqi|t37BL6irsRdYeiSYQ z$HLs08sxli?>GvFz_F0p8~0LPxJQq{A;a9TRPn-HI|>KGF&O3b;<0wT{U{s+#~`tf zw^c8_x4q;Xd#IOa0->08=csf@V&mJ6P>aUlXncT`w2YV9w;qLy!O@sw2@dUrd+SlS zC>)KZlkJ@Mu43a`j=@Dzc*AOY>AmGBTm+7Wn_9-Hd*L2A3Kxc>p){IO%U-xQAB79S zQKZLr3Cj!jrlW8O9EEMFD=1#LHy(q7gs#)~-Xv{IkHW!lBr)bk^f5h8j={xi<|sva z`HqjmLBRfdL)3iDaI{Qe#<9`BJ_-lJ;m9aCX?oK`HyTI0 zJOaTSGx1m-@bdZ*YT*PN22~_-Xwuu-+EKVT97cNW!KmkjvyQ>RV)XfrFo26|#GpC!%2z%L7lA{uYO_aD*D@jW-;n2En1wh;q8ee7tZ3E|h>n z$P~P?c}&Z%KMEIzLvW)w)tw`JA!y^_qj2EaacF8cI>&tcbw}Z%Fak2#lHxHNeeEp& zKOn4ZkJf+g{BM9C?tS+(@WpB1TK8RVo@Fj}RxdAc9Jlxs&Xc+5X70>;pa6l7=TF^# zd5IIe`40#zOTd$xwI81A&COaE9_Jz~dOYBGnBgq->bA!N_L(opT*Nt=A|);@GaN;e zgifYXqD$yYleIWs=sQg0N`yg$=93b+YE?W1;iT3Qx?{VW3dfsatE9&xj)jHfus#t} zR|G1TieOsnj?)XYguZc;>YTE=X<8O6 zC=IJw>o>G`TFHvB4wwZqSYk8=#ezVFe21RGxUa0mS=0Bk7T3VJ=FvE(|Z;jiao~)Cj!1}%<^sn@z zET-qUPF3z2w2fi-sD-u}T#eBoET?HIa(O%zDr$qYm6bcy*F97vG(D<{0-8oc_33sJYqt_eT+A zHXipuo~=`8b-|M^wWbOF5{kl!Ogev3TOuBLemedvg4&hk zS;)uV&!fNJ85<8=y?XS0iL3Ucr?WbysZEC{W%%Tz)5Nq~s_KgB^vuJ_x#oZnaQC;R zR(bYpMIWSmy|N9u^>&YT?cjJG@}16BFbstO!MSP27I`ISM|n{y4bN_OBC$ve_V<`h!fm?iz~|pJb!V&Z z`0CYVE92zl4rSKsM(O28iH0orh#7M$g`6)|@rC7U14yFB}!hO`G*dKH&`%eKK z9gk*jy;RCbBmyTMC>A(SdC#3t5L4)pjyj zyTXi+iX?+ltEH=z%rHYE1bX!phqp#s%+`J2<^12;{K3t^CcXKhjn8lV2Dtg}fp_m;xc)cmA6>rzNZZ2Mz=LEpdqyM94`bo+Pwg1&G2xBY_t$o6mf1%2=KZ~6s&&-QQJ1nrDT zr>Eu&+oU>eVN~|s8*YBXrtTN?g`1jR&=+i~enG!}Q}GM>b(^wZ(68N;{DOYXrsx;+ zt2YI|pkK9__Y3;`P5vfmtELlOv^;4iC05TAE8duewa3=J%P;8lweR!``i`}C`UQP- z?K}K}UR(Qizo2hld(1EB+t#l81^u?QciaSRf-n&@SH-GknQP_QvNtwj_l3Kc{DQt< zch@iI*YEE51^v3+ZNH#jySwEV^lNrE{eph=?#2PAlxmH;6{<4MI(0#AcRW)ZDBX5T zuZuw+^-J~Pi`V>vzTMsbfAhuG3aI;c<5FY&{H4UDi@Tp#ukF5Pw{!7_b|2c^-T9N9 zAK#hmP&?1x{$CzV1nYzs;%SjdFZ6TDwY72}8&c*6{X<3_lW=nH*EDq4z;!YZ#j zp)ytKI~BPUu9lcqI$6lodi7qQFvF0#JmRFQ@qvSbr^Uy%ArqBIwVE=a@1V;nSG2lqfIqD5$=yN7OXM~D-2UZN{W;TM+@~V6i*DRDA~v<#>)3W=PIk0 zrAjhUom9s0c$sM`p@|X;M?yt`BhmxG$s3~yQP9O{l?&F{%sz`ko$m*8{d$`jhFdX9 zQ^lrPWlS1_tzJuTRJH~UaiIN9di9s~F+$~dG)CvG@t^@UXvHz?OhV`(Iva`_i5ya> z7Q(6EwAz}YD}Ob^5Qkl}nZVK=-l;YCM5aZcL@ALB)nX1RYjg-6iUTNRO@eWB^*`)m zRQh==RK>H?2{wp9Y_}W2tjHkCbXnTTMbhnDOsR3>fwuDK3`3}3LNeYRg{vY_Hw%3(s&|qxU>3QS)XigM zRYQWE`Vj4u%vEt8qlD9)QNO`SNW3bKQgw;6oADqS?iK6PoTwDbcu^p51;^0UU)jfi za9*AYHI5mz3q2*4M?rarX^Wmp2yTXRx@}C#y=bB~iS<^l%`oznY%1F~%Q!Ql8)muG zVq+Ocot7}2D%x6#OilQzI?N%VL45TcGYo!0+e}PKX>A#5o7r@mlK4twoS;yFo+g-K z12toUJW=u-y83(j7_Ce&M3m|sD{QD-oYH&oLTV@(id9Xf($kb^mz!0Kt`SpcH>67(($cd0;qGqs}G9iVV%6Qypr^5YiND|R;lc$UTIx|Gx)F-Waf7CP?oEN$U z8Vv`5GvqtHsFSDa;KA={zp2+z)iPeS&%#7f(P3M)lyq^>3w5zKSfi`=lER;o-k)=S06fuoeO z;aH*)_E*B#iKD*DNHwa?pFv$lK1B|IvCv_p-o!ZTIP3Iu4*(`U*xje^`%_+2s?qdk! z5ou$TGK#7Wuv3_7nbC11A{w1wtm2qF#2~6r4rjF1$|rAPpcI=`jO?h`H$zdo)yNQy zXbpBUa5R#OR|hTDlt6KlX4zPc?PG9ilut24SRHiKP&{5qAo4V5OIWigmf|3rpb+g% z%`rL{@B!r}23D=BBeqdZadwa@ie@QTND(Yt%~hK1_%vCum~>&-RCT&6?dn zCrGH-B;?zf+9ZQGWosxr66ck_m+kNE{W-UF87{x?`Z3h#Dg5hvv zL#LroDwC+Cg;+^W<+D1}&TIGYBP0|xmoz6PKc6*?x4AHXIX9 zcI>R3E9e&64Hq-K(Fj`;n5Sz}aMHB9;v~{rC1=XbDg%q`3d4*NnHVF@Hd>lIWrwvA z(hbQ|KGJRTnZYTIGR$#xKOl&Ma#;|R_TwGv#`yKOR}<50?6 zea$|G$e~Kv9alS3lm#mdm`OWww20E#TwNKp>(dEjRqW_M?yX*T_y1cfKd`ddSJdhcWR6b%*l9>~dmWE_)BgDGACaf~!1!za=x&UOL+GHM1@^nL*$n`$V z+qzb*qcd2&31E?kRZhth0IO2@k*+!pt7gXG92JdNP^sH=P*8}dgQiM6qC_$=HwGC+ zTQCxPB?SJ110PtVW$!b_ca@gKGtx4a4RLZr5vKX5K=kS&#rK3zc#f8FAqYd6y44|c zT2wGP7L2PV*SB)RUMO8Knq(^EG~&UER8T=Jnp;}_u|-;jJja7~m6q`{(lU`urzcq` zKh-+AO@ax4b;_hMN6T2egGf}hP!4saSTo;+q!ddu%toJRl#N(RE9Fd9hj6)5A#S(j z_b<>glDK=dJmJpDmdSKj7V{km(W{{58j%$uNnx&*Q6mzIP3o3TgqUtAiKM1|zSt~- zg;h<;4x3c5L(>@qCdR07yDk6dA}z=7W-XtUEmMhLvDTZEM6;VGvQsYAmKixc$Ch!l zE|h9LCf7|?%QddRHAqdY5Oik8v z$DE*h67*|S<>V0~N>VXV@$QsUI?XXSD))0j6d<8@&;PIabE+kz$- zSAd6RqtI&#RHaA8paP{JrP2s3IDEHtyOw`sk(MKOvn`+1xQw?3MTk_=P;Xix#`$6` zJI%^-S8}#4L7Y@+H=FV>QSX!nX0TV%qoR_8N@*Hyr_vM*>s3t}-9BQxXOWh@<(uv* zTRyXKSr-HXticJnr#D(q$$_i7G;dr+1v8w+B)FwiyIn2P!=n^fn-CCOASU5xF)yU@ z`ly3)ts;55mVbDWmP2>3Ewg7eF5^~T7yDKz)tU;x7A4t&ICZWqhf^e?B=Z(sr}#-2 z$yhBZPZ!8>1d|k@Q?A8X+wNG-knG$(V*Jn|E#Jj^zscF{{p@C`DW@d~&ZtP6;T1R| z*|RS?wVoQ!X`ipc;VX5oMTr1>&o~`bp-I-U;NhMh1b5V_@dSMyZgj5 zwjtWza4)gBZ;UwSq=N;$UQ~FqoU%vzn|HFVfm1+ZNAL z2hwb()EA5(IP1<~=B-^P3%d2G(;X=uddBp;^dX8lWzT9-AZU%ljhG0V;WAa$WG5Tc zC(xW_BsPswu^~8algOA>0cYxIqZXGOyo{mFw9FtivC=hqy26cd^Kcmn{AXo#WhL;} zr`vS=89Pc@+NRC%S=qD%TNxR#I*gqN#0D~Klg zPFfT5JuyKD8JlqMbQ^af2KAKFYG+wK z432HZamSeyySfVBJ`MjV(DdsUXgYouZ}-Dzw%x~C2)K@F3yL+aG--r!$f7RIvuTuy z$A?Lg#X?q$)oIef2__B687?cf6~%yvi8xNR(P9tJ-M*~+BcSQs1)7fC#S8e*nJwT6 zsVhqMNKvF%9*0>mL`U-E+@)n&f$Iq)+3aZy*9>wjDoa|dRpV=#R)BKgb-*|-_3WN) zBDdT0Z?CL8=RYsdboB0Bdl<->tvyImEvm`B;7DObmP0(=uXFIc!&9vNz@Ndb3z>QbQ$NsFEMZBhT1Udg*P8F=5Z@#E?u9l}0bB z$?;JM*M+QvMkQ$8={F{)YG(VVj*4G`)S0 zez*Jn-{wkuW!K*RVj%RNg2vnF3qwY1J}ROJ$96VGKem$8?Np zW}FI>L%|U|i}EdzSCkx;*R$xYy?n0HwO@SY#o0>#fd|muq-i|xKuQ#ISD{yi!=UEl zr{DQ*&?jHHpiiK?`2KTrcK4rFH*3=bOZ1>jlLYripoFs%pR-&?ovuR?Ty($`z`)t~yG%nI8_{96 z0)Z&fR(jMDbTz|BlE5T=_sQ_RpiiE=s88j_sJXn z0QAXo7WK(pya67a*#!vutLUF^! zr$GrFkK=`Yvm58TQ9}?j5IJhed5O%k49?|z_sQ#j5%kHWMSXG?-`kGBNx<1e;HBD1 z8L>t=ksMW;Hq6!{Bttglja~47g-wny(K1i=q%mE~HM_JtmGLNhW{01>u_75gMJ`7;$bux8@g%9%Kn?oKt8`CVF%H1n*{OIE-<{o`Nw|K1SyXxRDXE%vh9K zt!g&gEDDLJH14**e4>M=M3bVH&ify`uD$~F$@Zc?xr=X*M`w0}jO!FzH(_;>jTR@7 zW3DFtGY}3}%h+VCPPdKPJc9i57?!IJ3 zr+Y0jYteJHH@KADlflV_6j-{onDzOSl5CYH0Z$+;?G;6}wx`q^$3g8YC>Vp|z?>dDG^g++*nk^yoau#X69F!dp;ER~t9GqssSHzz80bFZ`>+CM=N>ev)Y3{h?pD`-)AXo9V0Ro}3|wUU#Z^>v6fgkY;p} zkXVqXDXUmsmT)DnfF?X}*n~-?qG_#ah7vSTWGL9yv~Hr@W8xGRfiW84OR`9oqv39C z(mF{r?{>;BoWCaMTo#v6QOICAp2uiH&2S=ULgKIqP%aZOIXkJOqZD1rI#ohf9G>rt z^g)j&LzR%7M5c7H5v&s@iRRsX_=USqG%#k6PD%ZcWWZ`F8olCU_U? zCpEzf=dTGu8oaBX&q+C);Ry;{RH>S4NU_5v;C3XQ$lGz1jUW}G1QIqm6g)80Xt;)t1U?hb9^mAT>5fI+_uSj+3=cCGC`YkuI+J740NA z?{Z{u6ZR*#xsAZCMQ-*3nBdq1BE__NmYj;yq{Ri}9XdV{%bE5_W%Ws!?uc5vSUT1k5RXXioWJv)N#yExOB$giJA1kCfSJlE{v5iAimO5)>p6SMn5V>eX?xT^yH3 zTD4iJ))a5WKi4NTHJv6fOj1#rPhuRIlk_ZT0(95}qSS7Lf=P*F)8v>P3C2j*MI}>D zOe$D=8pS2O+pU-T-Eb{_l4#yx(=XiO5=d?nk_-s`OJy`gQZuBkr?VM`22Hqn*n}l& zot-3_&yfl8!ue~$yaHeD{{IE%iYwanV7mIgyFrS3GMuJ?;^VKoP~l&;v$9MD!o~jVywWYW^hyI%qUu+i$6pJ0JOp?w zQO{{{EuEV@ju&gGJgx(NR{_5z3ex!I=Z?08?!d0}%*QprCj|H;a^)6yZzk4c;vFKE z4X2IpG)2qBVK#wv^++-u??j@Jnkkfg)cCsDu67F)+AG+VVa4vfb?0%_?WTvHe|d?T zTE3gKUw?7_s2@ERLhmX72-LeN&=O0<#Y?-JZ9h$**s+zRD^T(At}74d3l7SJVwjSz z2EiUZHj(K(I-NLO-DzU&F5$3fSP)4&RIc}WzU4Wv;|}2A+Og8ZwG`9MB&#-57~wLW z>sR}%TtMX@sue7aoLXX{(sFWBbn#m76gzGMey$yd9HVaY@s5m_iv1RA=VFumAScV5 zLbRGGyFZLo9iiFohf8VS{BF177U1LBaX81+`%$n@4u(6^?6A^CdDe0ISVha(l_?H( zV`xPlOF=|7eDm?xantQ4V8=@=&waBx-E6$8`F*o-TzNp#bqPyNhDDPRTg70^lX)Y?gSDaPivv6tigney-W*!31B>qS&+z zSJ8>!fOmLQc3P`XV{HyiJ+Xy0rsS013^Nk+{O zq@pmUWp~UmolY=tP8mRQF^cBurJ`EMR2o5ycetK!dG^i5eB8XL-X)%;T)f}8X*T8n z)DbKc4WrWQIgeKXC)YrAgf(K}CKKsKCM8OUCu76EM$$0cDm)8 z)4ch)0{FQGiVWIgp=LTb8?|C7n5eeYSg?aQJfcJSBAX}qICvTn4<{kiH^18rw0u*8 znMRlzbb`Ym8x~1bOExNDkdZS8su4jp(H28nXv=Bml36S4n~%pp4ZvzmLW%d4N!yNVFiKZJGD8_Kd8A}=(~kd;Rv|@4~Jq2x9PLOz&BP73Ii`;s*6WRk5{{8U*~#t_}TF4 zCFX+pUcKzFa9IC(c8qj<>irUVxOWQjdiWeco{hr5iC7#yu|hY|kLo=hY7Q$F8Eq)k-xrU~h&mG4{O&XoZbi4V~`c0@;&St%Eajjyt8tzst}J(sIhlWB4w8AUA< z<81JDm@ycL=GAy(Qe+|_y@$p!B^hE`wAPX;aM>nVuGAG#Qkd8ioNG6eye~&|n+_cD zTK`>daUJp72dihh`ts|Dp)X5ETxR}%7>q$4M?74UK1&XGO8~b#b$d{akn?iBDWeg0Rinp}Z9s8X{)0Wmv8Eq%3Ry7Xk%Gyu_&o&RSI#=)GO~$gvwTdBB zr6f&Dd8F20*{q7>+QmUN7HwQ96Sz6%ZTV__I#%iv#J)1gC;d%;ZqtF^y`%B2M_j-A zE@Smh7xR}=@q!o)9j8j5dP|vA?L{zC$52w3wq=tm*dDIYOEY#E7?!Wv+ zPiV^$@c7Z~d#-26lLoy?cfS<;Nh`-=lOb3RLC|ci4m}v$2#3$ocvt}|7zM%2n_qi%yTE1gh@>Wa0hlx=mXo~k7{_Kt@ewHA*sTpZ6gCPpnn zFo{}E?=~W6T-8awR#y78eqUBfhVm=jLGJ z^BcYOzgh2G{2v!v7yjykbN;jE8*86gtDXCcb7tUEfy(NiuNo_V3eZpf9(n5dYn2MW z%ni%g-jY1>)bnNnEOGU6ws$9wJoS}J3vi~FC67Gy6*B>rxDh(r+nPt7dhSesC9Y@A z_8#Jqr=GL40B3sf@W@k_mKGpn>PZT7Wa|f^$m?aK>E_mJ9{(cU+<_ zZS8(+_d9p74{#rRY-3;W z^^Fq+_hxGz3VtsC>c`d>HryMpbcjD|_{D|7dj&WUKJnOvg}7ek2e=PDc79>Qy;tdn zg1Zf0Ti9^#f$Aattl{St3hx!*K={OCfrYqU<_EYBKDN53;m4}d9SZI?d}UF?5pT-= zA->!2>u*^k{4oIzgil<5WFfAX`2p^O*WbLb;oc1z5Z5dG0q%p>Ckq?y9a|0s2R(j$ys*c;qS4R0+J-YdX?@QLfqg}7ek2e=PjcNR7L*rOv40T@LmB9gilqS4L24F?-k%c z_{8HN1KbC%7Z*0%JGLAO?l$}l3mfhoUk>qS4KFMd-YdX?@QLfMUx@2vet`Sn z^@kTW+&i`$3hw&&>t^_;`M7s{ImF+1;V&kxzjmSUUI7k-Ph8g*;(D1M;J*60wy4LB zrOF)&KI<`c5%XgYavv~HTvrz2dPl|s+*ern6uU-3ZVI=7Jd$4FHS`%gG? z#pn6mGww&rI-li zgvnAMwV%2QVnXQ#952JAn2ldWJ8?ZOBlTgwq%}yW)(s^%xYcb+SFKP#2Dh+OIR~dI z@Sq`Iy^`hx2Q_^JXYM!M&B5s2I5(T-JF91V`|Ed_kA1PjjXR6Z;&*RFFTeKwWv+4k zryxEexJ7v6w`qF9 zQAtedS$M)GQzY&u^J%~7!0X;#yAg1`?s{Q$8B<+6b@}zW=y9)`<41EAsQb?bxR&-T zOsO-LLy)&nYq{b6_lI3MW)iiQTlC((qIIM57dVB>=1z?rWS_d}-E*fHcF-s5btK9| z5u<}fD7GAqH?W*T^CQwp#h6ltErrx>Y&gC1eA^6BgykYlX2T^TWDlD?N<~q#RvJqg z9c8;5k%tjjtz@|?4%ZliH-d_E6tUt}hEoP51?FKnSx&Y>lnogf2AO~dJM+z?zZI?9 z^qcYjYk|jCwnyt@|wJLS$h-U!XjJzXLc zdj9dlvIIQaruk1VEPO)N9Um7TJ$Mf`XM^{$PcO`Ssn#X*rCRsQ6Z%f4f_Nf+;%;Lc zg-YqGLvh%sDx%a?`$eRkp^RkSWO1q5F4{tRoQg+m)o$GJx@Sa`CkPUerd2F$i7oA_ zFqD!i+*kF1lqp%&F`5f288+SuPZX|Rrz((I%a4*qtmcH8aB&pjEK|A)iul;ANS@^* z<13?(uZ-_Fz-%GGwyWVbIf?3!F5j@flo}czA|R{ zu#rd-iAHJA3Ms=X-IzetCY|qR60i!JLkF8S#O4GE-?6c0@feXML@}erm~4)6Mh$2} z#OYQKZqliWn2b+Y0uO6p+zdxZyi^ODR>np;VF_2oC>d^;EQyC~ut5lQxHLP6$$BxC z^tZKko9=F{-+1GqYwX`~ZuRmK*&t`O$PdBC7y0*JUgAvA9aopxp3LBvJ-9ozbzdTD zW8T*N_DAttfu4nJ$`KL3HU;E77iQb|+uhH{b3m^6cRJTZ5>Yp?dG5C33W3p+BGr-y zjj4sz*s;|O#c~y695zfz7V_Pm!%a{Q>;Ue3u89=8R;Lu{R>R$KSII%GmYkjPh7liC z@sS3_!;^ZMMHI<~uGX)HSh_lJ$oRmv6Nwny6H|R8f2C8Nkf?^ybgqaAp)i!i{cStk zrUUnT^!$wrGxz)I)w6vQ(X+xk=>jVReObES5_f&`+;7ec0MCvS&JhH-V$UjqHvwjo z%E66R1jzR#-2aCo(Gw55N?3y`j>pMHGMR{o_0d?;`+ZdkD>h1V6(%2v$ZfhW+eI;b z$Mc{#Ni|yoyHry2MhdPsjqKzqJkdgyqnAY(56cZ)v#F`$LAY=9b5Mg>>lD|Yfq zUT+&tqe`{7L`Rkq4pT;mMzq#X`Fd_74!KRgx&PlFK>$LPhWiG!dI@n z|J;MC9|ylatM6OQw_jNgJbd5VjFpwZ&z^9O340?mSVl;LS~fh5gEHY`4H*wLgoKJp zVvwe0Klh4MVp_&qVGc(}SRDl=8nbPk0&fUYwo_E(Nt){06}~lMablz)b|X!d?NSX* zq;a>RB|6oXFJU`Av$DdZHs-Jhr9<+hWb0}P&t{6sXim9Q46d8m5R^?MThma@$P_1n znxLH&>p*R~+RJLiaI}XExE>iBwScN$aZA-7JW18w)u)ChxwJ@Q+TEqkJ}lavuJg|q6|8Mg?UfMpiV)H=o}-ZouT zzx<`QlU1Kt5nw#eiO6W6lX=cSWKeLHjT*2y$EtB^0$w=K z;EY8n;&f2bQ=(+q5=O^6ZMtA5!nRX=r-s=Wc1BCjYpDz9hJyGqq(RGnD80VL~*M zPNbhe>U?yh%WND*1hoS5y-_kUv+9@JQuR-rpz34Mxao8T)a~Wdta=w&^@uw&tG2pX znxk7eocJ77qfXZ$2`)Nd3auJH?ZcF!_e|C7#AB&$s4$L4NpeiYd*dcN zTY+4@rRtwNN!8xqWkFXn3@DAvB=0I!pOsaU2~Ah&5$(toR!(SijScH{c8;o(k!)R` zR`5P#CzB8;qufu$(-U^64bxqwA&!mcuv>v7(r%?^s{Y1Xs{VkdMep3HTqmmAt9}%*K|DHXKZ7Rsxhl7>SQlwr#M(Q#R(RQ8#X=-O6YhT zFZ7$;INyyLf|!BGnX2!H1YBZ;B0sUlqzb1fr;#$ymK*+X**`Y`MKEbeT7~`C3 zQ<*wZw2{crLhG&Dd%J+!%N_>YIN#h(E!F^gH|Up_sAOH<+i}Hl8=v(p&VKOF4VZzA4w~-QnHDUtXdrcYALWi1U!!9U0%^96utx z95aGMnTz+GQrbZ*GG5hNg%~?F5F&x)!~!VbZsyWjxC6B$Yj0z}XI6G;-z;zQw0cIi zHG-}eI*>x!QLI;LP6bW?VUH%49t9HwuT@4hu@SfU`rbNVHS@HzZ&pVndpGcxm#A=ld5Kcupv%?{x{UZz$-aa<^}TaIoSD7izQwuy*6?yY-KS}xJW@kx zD%+^~*vn zOXdi(678#ru1GNoT8~tdRFfTd+6p+8BJqAoEVEG#ee3oUZvot9lk97Jb35G!yeBKm zRF^M5@rWyq>-rD*7U!nxhgGnJj@5fmB-b|BSck7TNV2F*25=%qr1RDgire?j){gbjT0P1bSPn!oN#QVM)}MGI zU^R2bSNUdj#DWNt?x4L16PAORFplshF@85U`qAP|P>0BNnv8!{ESz25y(R78Lc(?Q{$Bo)|3i_;~q= zzAKJvK@s2L+_a!2o)~XG(F1H|78G6_?ss!NJ;#EUc>mjaq6=8gEGXogpZ zCpv)D%z_Z#td3fc|ChRf1-0ESb1ew=&FypxI<{3@;(g`){QvjdtpB(Ap2eH^dl1jC z20%X2&Ou!65>C5#Jn+Oyxsfn)qv7Xd0e&%Yxk6hdCjH6C5=&^cKarE&2kaIoI%2hY z{RiT42y#Q~7Dudm$4wXO_GI}xjmteo_x8ZRFKs>&97e(?*8fZNxK^B`#Xe&iY82O1 zJVFd3Xt;_-DxGO#%ExuuilPYvhweBYscWD^eVitxeTFF=aKTvZFG<`oX zaQh%IaEa++@kr`1_7hV-<|Nqrn@@}419)y>5nfO z+*HD6JTMSOpzw*=AR?U2kE``@Jyx@fb~mpHbf2$J5ke`87&=L6l5rdMPg~AjjnYun8p-v){wbC@5&yp3` zbV9PwqvBON-pj^@rCzB-`^yHIHQf!6@88&R9nr1-w}f?_og>aIuY1=!;xYodlc*=h z>u?3@jtjSk$3H_3cuN4+@1E)Ue{mQTfju!YUTcTM=;UfXQ*qJ*GE_7pbyg@}CA37h z)X0SN!Jr%l)vorS<`{vy`yns)Ac*I{@)VD zbas9>w*c-E`Cw-=p)ETjdi?12?b);BNsFJD@$TDz%iVio<{5or2GsdIakv4u%EJLK z%bI|7C)rN7<7nk9k-st!bGdLvuGGdVuVW>ln!EESW)e)ho)c{ajnwe)*p^i?O~aFh z+PqR8YhscjEZrVdM+`^84atZMiY;&jXPI1&?N`RI7DWb7rlV;ldSwcRq&^xaaLwN| z54lYT!_DUUudhT`K6>u_N6$Zcp4$A#=Cw_7^TN)Dciy&x?_Aja$o92ua_!=|kDhz< z+{JTL;G=;@161JR>PJ@}U8Pnpu0Oi_tLxPIMez3jqZg@*7cYEt_idLxbm?0zz2?%& z%16&%yzuA+>cYjfkFH%`OK)G;`pDL`EpqF^#z!`;ZIBxm)_;8$-(6dIbp4|%=c$#~ zd@7^GQl(G0nyQ{T~VZ*y;G*YnCLXW%4+trSmTS?|pUP{ios| zdz~H!IinoUQYh)-|LAuGe)M#FZ>V900nU}wGx?eRe&ja;?>imednAB!43m^`Je_m- zU;FdGkDQM0jlaucnXI5_On%n>YkPtBo{sO0bjsyKon~c_=I!!#sK9$p$M@PF!!bO~ zQKBfh_>JAb51)?jwLdy7qhvZwrm`+R^P7PmI!~QSA?mdN6qPg_=cJS?!DrBJ;6o3d zf9-weS5Jub@+u&oWz#^1Y?1}&A51w9oytApeZ)JXqNJK;LBP7p{WWh6{POAeUiK(W zWYZZmuhIbj>A#Hx{{5-=u-E?%(-Hvwr=NUb;EB`mz5aI?YwzN}^WA~(IUV1t{lftA z8UE)3kDrPk@%j%Xp=wqkMcf@fKXc{XfnPcu-)sMTo+on>2>2yj{N3%q|9LvT*Z<^f zMnhSGl3e@$%$gAR#Z&RaUinE(R}~Z|X%yf;{ryQLg_yJ^n!8XHLiW+8?Fou{@Tc^clYOp}t<7NcZB!EnrV zpQo$;J@C}&_+J0l2vOA-RmiyG@6+t_15cig@3nt0e4-gaz-f2>d7Aoxz)ziuAM)yt zWH=Ed1!A=9;(yb(1%7gOcWw36U(5pHUpDTk>OWS802PxGim?LaPG6t?>y5zoosRD{ z-@~*{7ytKO8u-_z<9p5bFjvvV|J^SHzV~!|ulXJ(;JNrueLS#tDt^>!ekqnyNK_%S zDS-c(@pA&db~?USeoV-yEUW2?JAHhnzY_TH>G)pxX@*uYRnO;V!|&^UDDWRn$M?FQ zAf=NEmZo^ueLh2eQ{Y!m$M;S@GAZQpG@}zU{+Jf{l~eH%FaNYIrd1Zj(b@8mHUmF+ zI=+{GGDW8tn$S>}|6g2*2Ht%-zIXb=XibnZc|n~mUtT5#e&E!_hnKyGfx(B+dD30I zfAHM3SDniJSosqouVDn0r*on!{nmQ}H%`a*PF@Vg2t+EAmu9Ws`scv+pN{XHyl6d@ zPcur6amD}i=e{BEE-?0e%d`5+`ZQazh?c_r(%0uM@2QTanY0v z=3;-vwFghtUBru?0}1I`M&Y?6K!5tzwgW$ZI=mng6WimtA^zQFgc?(Tn=T_1oVDxxAFKKR{Kx|t!}GhM~Du)qIr zn$M>hm~+4PTj$<$>(;sVoHIT3!eHGd4c-xnExj zy1Ybx<73^mbh@TpiE|I_D*W_jf9_Z3Tfi+?{p=q*zgOmd6SROkyZW1My?ZnFph3kS z874FLThDl`_4hv+>$&?M>od-kJ0iHx56g5pm#@}#9&F2BJT@Bo8Xs%0p)F-~E$?}3 zH1xqA9u3QT9~%vQO}CDQ<$aGSR3F4dp_X@VDzsf=t7MB2k+uMLT0Ld3vqH_U*;J^` z#>Y~q(?9(^Wq!l1!cTv0nXlO#FP-cUkC*wio8zUk>DKWwzixB9bV5wV%lzt#EVzGc zH+q|uT4JjXF3=WBje1EjUyE4qbI#GhQ$Ezbsb!O(8@%{hPXWlonXS8R}+6UGyT>ZvsedUgo z_R7-oJC;35A6~j>@!uA?g+DGx^LNf)0pkC!wVr1AxaADx?Q<_PJ#4ym_V=^b z&+Z2xf0m!C77xf)7QDh|)&-CY9>E&n8e4P+kwGH}(SA2IXz(&jNp8&3@-#hIBd-Wh zY;TP2=Y&BxigCSKLISBK0^951SX0lt#r0h}AdR`K&aCG^j_y_;!fgdMjV9DXf>&u%Va3o10v{SKj_09?@Cd;$wEaL?sb@g%c;f+Nl?@b zWirElu~=zGoF2(33AFz3%fpiuD7Lks*c6|x*2EGO@kmKOkqwlJEa7es+9{hP!60*0 z40e@;aw%5u#B}EqIJRP^&jgwWnt;ni;Gxsm9Frrs9780LhC;G&3iXhVn#V>5G64i= zA<_%?Q8-Xc+I4f;YG&+##)gs&B$d$J-(e+l2L#X$1fI)4J_+6Z9J$d2!^v+9r-4)v zy1P2|a87$^O9$#>F5s~Z-NQo&6w7TW)!M4{RXCgKk4P_5)9!U;r{N+aO~gXTzU~40oUi zxQr*Afs_uqxg7a1+&pk8=xJ=W1~NJ5?rA%-y#(|(-a{Bj=b*d4BhB{W4GkK|R6#$14U4Py*(V?1RHq;AliMqs-slL!5cr;LI84Z8a~a>`JkpRv&y zNZp{jpQ$NBj)w_M83S1vba!>kDYGRpMCm|o%mqBQfz%DUxol1u0`xJSG6r%r>>MY2 z)PR2WKo;~jo-zg!Ht6o}$SIQn{fwuKfm{u``|G7}ew=H4^8KHm*(2|< zti5yXoYj9?b*+4L<qR&T@<c-avRdXvxq z&p93ObUGqHu0TJ)NxpEIbM&2TjJ|65P!@Csaw;tUL3d}z9EvU9{YG35%%RMf%__V6 zd);g%_G`qcz#YOtFCan2@*}!mEQc-1h-(3;H@%_W-|7BDJGOeKzy8+^I3UDB7+?dE zkt{!~o6Xj@oe?(!cPO=?>W6gqc!a7AI2h`OlAtG$ie&jg-90_dY796Tz_2DZhV{3) zJ3IEUPJ0224p<_1S(I3#wX2WU>xEQ!Y@r|MU%@eU21C9ou-q?nEf35q2?bzy_ z_5~QpK`9=J0ybl-`IT-q+gObOrvrB=vZ3nxb@zCL)fjL$)DMM0Ph+e3rS6^{XEg>~ z4q#Z3jbXh{cW1{Q)@h%Bk^Ga)q0pEOc=TWBX0!ROF_3<8`4GJE&_CC0HphPGX}^Gx z?2}*&f*W*x_UEEwz&TMr6aZ|-)9+s0Y>xB|7;!#`hh75u8k_M?b@#Q685?jyaEC4e zJ&yMo_vr5N2s1X|eyAUM@rE9MqPwTZnXv&E1eih>g3iWU#vkkM?ASAU+DBl-O~D+x zV9W+6`EK28Hq6+7142CXBGAit^4z8S!*ZL+W56B39XcQMIG#Lr>hAG~$z#9~Q9tAd zJ&ldAToK|S z59nn)dA_gvb9bA`W56-N9dd&n$CKy3b@zD0td4s@5GsvYZgv2zi0NuBfsJQ-~BTvPx`dYEMx{|?9Ssk zm4)nRDfe94RtGjP%d{jc2E0a^yaA8b_F2dbnA{V~LiQx34&1tKJvCX#ygtvk0P41N zf*a*{Ziw}K6|c9lKQXN3-N}9KT&Wp|qOD%6n)mW>)~m``AdWxbEMyH|E0{paVy~B~ z;F$`Ohe)?mZMmv68l^n@LxW+hLJUH=G#<8>?Wi3K=J&h0inCVUw~udSLnPW1eFM4X z6M3rYNV(ur*Bw$%zigaQvD;GWTnm(YJ^!?qpI+kFVn-S0zj1r*b{FJxj30fHh3tgQ zEMx{|*X|kOwsnI4*Bhb{f`+n?JxK=m*lT=jcTav6GOxpaRJ&tqjY5|QN86=gbAWXf zPuD%99lijVwEO75K=FxzD&vpWTNIe+V4ja>d8yNFv*YRg^3xxG%ToJaY{ltzgD}xj z5vv45?TMMx{zNm>sJXbjRQJS)NJz@CjV$6)!j3ZEOP8HULGq$8v>J!~AQ=PXbh@ct zI?CG}MQ4rPZtxdtDR^pyp6j30fHg>27bS;*1}zT*j?A)8Acrh2Rt zz?02p2WIJXgB^yPVK~5&{hZB7GP`DXkbk@S+)w%o&FCA^Ko+tm$&z-?LN>j71QYOh z7P2QZ3z@^?fR0*)yhL_AqDa)(LWoY2C>cUy9Z#!KiTgTGs_nwtH9OxW5OliX z!?Z!+XKY`jMMYo)Z-lG+=&CU61nnt%GheEtLrp%NcOWFb-^mvIjy4?irs90HUJesM zED}KqDqSnn`3%j7bhpg4Kp_$s>^gM3=Esjde$DqrYuammFl*%FW%rgF-~V*E8NWrf zLiKEqZE0JXm!59q*mk@9@Z_s+I(^}jv~Wy1S3t$5@f&IULLL*T)D@{w8njAHcKpA` zkq0)X-Qfo-hO_7Z+cvx1=CymqqNQG`Q)(Bx?FNu+bsLTGbf~aWrw0Dpxv4ItQ|gSN zUOHBw_8&!o9gMM$k_yVcWxWCo#daNJp&qxIdcQvQcSVJ5_t^H-F4OI_A2rRdqC&$; zngA8gIx?vABZO)y-Yfe9IW)WvY_{_bs#F-Z1%!r~Cv17yXar|fAzdvODm`w9v=Tj6 zhiOnQ*y~IX0xR>;PCv&J!;TnF?w1nb5{)5BRcazkR81jyA=!Y`ZV2-vI(EqIA2Ml| zs&j)qeso}Y4S3b3}wp?Ah1=RhM7QVc2>U;~F`JZNah50|s zp1C)e?lQS%-!gL#Xz;k7t438c(Z{bG{9p6YJgiNXR;7_fJ{v_un6l(XN6 zI`Rg`tzX$Y^Ry_u==Cf8WR)jpOvWELe|_DUS0- zs{a|}I6r3hS2Nmg>1Z6cteIzxLQ{5QHFBuB!STp5*_~H+ZRLFL8DpmJJ5x8)^G5KP z>El*T1BbC2Ot)D%cN~3f<$TxeW4`Y`T1|Gx7R|1Yf@8i%yfw{6Zj^8EHS*TfR?c_4 zV9fKKG2MDSYZRE80dwmHj)OONK5pgQQKQ<*c|fzL8}8Q4^t2Jkjr_RxyUEB&@dne4 z_xp|%-P#WA&{gr?5OtTF;hOQm+AQX|M@eenLSsm{b}v8)jzDheC3gq!Sch) z>e7Qt-NgqMm4#m~$n*Eli`M(BP0PXubtf2-HL1G>w3PFW}uCg6A71u**Yms$l}VJOt0v2p9m}?IAeD zFo9hjg6C=k3^)yTH+28Y%Q~P zHt-63XZ5dEU%r}NJrj5bzP0j~D}$BP${D~*@Qvm7Evw6k<-PM?nt#WTX^L}l&s|fLPKn&N)(!DB?P8Dg}VH+l~XoQ z`33>X_hI31Y1d|=}06(bYw7Vn^SsS{N>U1cZh^VI=SZcf(%Fg-dqr&sh3b2EBHe`ormUeO0lkLVTsjp-r1 zqW7B~&@1{&({DCJcYMbd7G9%QlwP<-uV{YZ)p|v#g;!1D|8JRDea7OuEH9kB9tQ{+ z@uq6>20XRf=O-{ia2$^`70!A1+R&IzTNInc`Z#|5^quuOZlJI&pu%H?HceN58n$IE6jc-w8Q9-Z?3q9=7|;`^s6F~5Ec%(7VbKOA z)6Vlluid$}vpWOFBD-TpMh$Vwj*h(6_7qvt&SBA8Z1H$lG+2Y|M+R|1U@_2QUXLEc zIUe387@X+@ai#X7EjMY@=Z`>jXFBR0RAU?#D99})>!A2hx>Xxy(H`gXRuW=?#GiOr zbgu41)L=g!F1fQ9WG|5dndoK1PTmuwa~?7+`5pUeL27@GYbYh0+2?GjJ#1fl=&$>N z6ryI+r7YV~_obb6Hztx)+%cqTAzfk7+M|!dqT8cqXs>yBc6Se7^)5Hi|8%(-zeTr% zMH^YKws{Rt?rc9r3&*7M9m1kPm2E5^$fq*-2pmJG2u`8#G2cH|Nc5Jq>VIH$4#<1d zWCSjkgN%|>59K4NVdx6Jmn-Wq7b&vp7k`H^Tl&i^bEPR*Uu9=_kCS^>=tUAPBDg`wEyp8V~jYh`TMPnu8cp%R0Rg zhD7ReD)84qu)~PI#i+Up+C6}l5CHirWAAKYK z-!^me%$^VL$*lc;?Jw7os}HWeXEnO=tCe@Gz@Q4i+m~Ow^pmBxEcq9IwD|f(*TR1- zT(hu${@>@Xnm^b2FVgtKeh-XL0tD`GfRt0#yMonji(;aclqTUQOVNPE%Mt$3PfMJ?OiXrb#Ui*yo? z+vJ9?OW3ouB$S|uQC@?s>VR!wL=g|i?Zrw0EvFPuqc;S}Q%D6W=Mq=72qAQy#sgLGVdizl+_LClZ4thk;&>{Q>vZaNE=-|zVwBTh86B`*1&eeX@RcHL)P`HHIIShs>1AcRdy$ zit(bzLUISHw2_`Cl);HY6s~fY*38} zm7qHs<=7C;&;qY2^>kgJF>rK}jCnmk2^kIcWjbIp30BOA8Am6Fp*1q({05QTe z*h_T4b}FnV-)9G9wvE}djB1Oe8DEsFHrn<`xP%UnSUl#WiIiLfuqh4pVjZxU&m9QZ zSS&}7bfz&#^om1@FF8v2JmF|nOPRK#B;+Va7Mv4HnY@YoKN-F?xvxo6-Smpf(9H zx=4df>X={#G8Yk}7i+MIos*atNi;-^F4SOA9k2$fh7h9*G}yQfSOY11h|!BQ*q9Dj z1Jyu?(fJx|R0phqVxz>!ufaxiz#2$tLyUYHY*@zxGf*j%7_P#j|Us6PNydiCFyhVshcO)y4uc|AqMrP4BRN+Ul`pdkCzo zuGd^l&@JNmmYfgx*b-F_RhSm0z)D@p58df*qhJdX(YmK&=k`Wp1%eI2{hpM}N{k#K zr!NwkDu+45JhoP_T@J{pmJ6npE|CuT(jc>P%9Bq95ux2J;Bu&g)?BgmMxm3T+_{K4 z414`5ObULc?2AK8*yrz2l}z7HyVC(4BN&Og z(B13gV47;%RCl264*0!3m2VCQ0mVj&`F@P6CMt2RXJ2=B+SOXpj%5N+HK|a+dK2ZP z0#9*41uYAT47X#IRK!;2)52b)7S7ca4lf6(X1f(>mWES~h{Z<^b7;hwQY<`xdUP-? zHpN;QB^qd?$H`Q;<&HIqu^=RuBi&R@Tu*p{{Yb@5!p>|8R3t7o%W}Gc#ECW(s(^yU z0@th|i5Trnq;q?bWGxJ*YihGakuAQH&nKrinC(Y!aD_8&(Z@8?!Mf}o_B}awAlvOl z+L>55uA+^YyROm+Uns4t4@8Mp2avCjBm+aCU2xL9uw06)CN3&Xel5JOMH;1bd$8(quAHEe8bgp z)LpSgv|A=SIO}mVl(sS;0zDs@;)+F5EK1-jomPB%OQnp87b>Yhqdw?mq7tu6aj-o9 zFo!DUj}I|tRuG&{zUYMGwYr$}dc*P{g4aC*LXkr)Ov=S`__{k&43Pq!Ni~|;ipu12 z1E}dP#-k~CfTDgm-eDt!vZw1*OZL6FWH^Vl=uA&V!2zN`N2mRnmd_mK08++wLPEvX zBkW}_Z}(@Ly+&84u}m(TXX4zziKM}2b12kP*6T!w^=52hSS2yF8c>*F33?4s07Ct3UOiuDnXXKOYbRH-CA#o^+^YA86InN%DuI?IsURm*CJDsZUK3=Z;G zFO1ZCy|PX4(mo18))|P0I=)EV=O~p!p;kK^t+cZ7xLYYXlTlWVwtMY(lU5r{-nX|J z&)0KOxr{XuQWZuiLWZC&*>|Po6oL+mf?ec#M6`nkvO&?F>h{$H>Skhlu^=Vz)piI@adfy^ zZq!RtYB0?l=0H|ksF)ZGy@PDk=I}EKSFD+nA%V^|+U*F}fCoY#9Jf`J+iK*{Bq-;dC40aru^yhSg0ZN^RjSn!0(ln=PVoBi3SDw= z9ifxP+ms?@!?GKVHM-fXv!2|`Qkja{8OYIIw_gbNYN_eh#X|Hjhb)tB#RC~>m}WVI z^&!C9$@HakoO6Y1;X<@n=oZ|G8b3spb;wcii78iP2$QlvRstR-gGTx&<%hZhu~ixj zJyI1fqajf9Ou zdg->GO{smDfV0!fBym^`_#lkBF+4J$czYs?iF~(*)q`AI%D9WArkmwk?Ls|P zVFt)LE@$jisj6i24uKadQUUneok*SI!`6GAbAs&Zgn2Vis15nnf! z2#Nk|O$r3cLPBzQl@RGJ$g#b7PAn(6AfIAFnRqLcPfYJ7XUW4H!rr#r^?8V_%Z^c8 zG@nO{0mu{Tm&lf<*(~H5C9acoOH?ti-U-+8l{l*saqlqP9%Q9zFB1t8)kHR;$W1;D zv&C*PLsP;~*jp$yI+Ykr7I-+Sz!XuK=3quv@1I>TxcU44rTF2oU{yseF~~3~P`+d) z+p^mot)v5tdu2!=Rn*O5b-1TWQb$=2r)9_%@)x1H!{6+Hnl2GeW$(xsnJ=1#y5(zcm*>aF_$ZS2OLZPfVyN-Qfo&-v_zJ( z0X&L1+Xc)9wjXS_0rEH>Sf|muc1`?JY9;2!roFr zO`>!tAI~(hD37P8>7Bim12mrUFTJJJ|Cou)e09dUHg}rk4$F;}zV)I#K1+BmV}6e1 zndTpvKD+0e=8v21TYdV<5BKmZA6vO>1zkCL`6tUa&%Mf=SiX80Uw;15FP1*D8d`eo z5;b?r(wU3DS^UD{n-?!x+_&&M5b^)^1!m@}3(kd^`ERa%Xy%dm(Vo$~Fz+{CK0j~$ z-fGYKeyePHohffRYxaTJf1JB#_ARrQ&Yov{33AM_ruDSl4+{-4KozeBo>{!7aBYE!;t#OD0N%uq$4tu?Q!(YMorDTj_RE8E?MajqnYH zVoQDrS^m8PgJmgE}-*;*o2914ivZrx~sI+SvU?Pl%Nz+DTJeZx=@bvW^`rw|D?xQJUAwxM_~ zgDRZmyh$H%9-)d*pv)s3ysr$Sj(%KlqV71>RKQNF%nYPZ4sH4UgQVqy8!bXMJk@MV zF5V}nqIo<5?xn<>T+ZDn2t?6M#@x6sfi$BQoTt~dQo7c?Yu zbZ&k|Yk@%?q8R4m#Yimx(OjZ}hB2Rw^!H0jMD;|Xa+(ezl}yLw$CuBUw9tR3y!;HU zMT$>XYhsCtc%-Bs?BPpAmTmXu+i478jsQFG;*dkKV4)wnE4HCQb4WO%kw~l`X!_Nx$DI%}tu|p-EE_Exb-N?q4%)k6 z@Mh#AWgmx@{OthPSSc)wbJ<$mCv=N3F>g0Ly7A;4!d8^K6eT(u*%l6py(DaqgK@^| zO=pp*1q*2rPx{&&GO&2cMhmCZN~WC(8MT2+Frj86>j)tBhTJW%h*Yo>c$jCgZZVXw zFK}9ml#;1;y90=dckuyOA_h6Wn`1mdp>CsHc%(W=wHTH*B;Z`F=>k zi-AN$u8TgBbak;_pwWiaEFA2m+_@O(L1JM4E?92Yl$-Wb`E-yQfDK>8HB`YjSqtVn zE*z_O&=y}MoiQj_9>N}PNPs6@xMc-vWMC!Wi1Zx6INc@VzH}z+>5ctKjU3UWpl&4y z_Jy=%vXZ2oDLRX=JXUk0Rn%dR%00Iz&`D11h)yDz1&%9T@%ku-n6&yPeb@^wPYZ1X zS+UKEVULX~=3UXGy9Ek^6=9iBdy$M<&BPHkHv7iOW7vH`DOe2R9JoM~@#Dp~$~zk6 zYM;sz4XNc-`7-7kW}~uzT%>h@4QoS^P*V{R5pku#6x|`4Bksxf(pbNeFDA29$PE!h z7LNF=^rXe0QzVlrh&(^D6XDZOCDz$u-%FvcyOj`7->=5UK z*f3g`y;-hJvP>cyYI}?AL7{7lq%uLu+jA?PoZZAsTBt*YOf)H3bi;DNS@+9AzQVZ( z2~h@0Dg~D7ER~A4d%R4~o;hhD4QzZO;cWWQD9RK<&VJAVya@GtDFr8eowDQ!Wu4jL z5H0)W@0+w}h7%dTBh5;|hC=)E8Cil;Sx33fh*UXHraa&tUNccsRmDDc#-xQ1Y;+Yo zkp)FnaU}(XO65T@0!lCnEo|sx0~MFcn~n675G|PBGHFpKYqk(CcawOq=4A?Qdm4w! zxj`mfb;KyYP4<=>Lb8pzqoP@ww5SbGn_5syuBs#}cC=QgWx9TMkDwDBS20`dR%`Bh zB#%d(fw`}2v?$f-YNHY(IeUXlbUWnG8Og9rHC}cU{bao1;i+J<-0B9oS$Wc;(odol zmk+j4VX>n&!gUz(!Fc*&#%l@9kqHG-FA4 zhh1%FtVrdV0NpPWV%^_!Wth&u?T^t?yOxcrSaRX{lNQM`+3WEksh*L#UbUYsVVzDZ zge+ul+u=~Vl`JSSkENa|$m$qDCa0dZfoVG^Q zhecZ@>qfI;D9RVrZXo1mpfI6Kc6=CE_oakNs<;4^BK>ZPl~a{cJQ?*RNqIQTp%Ey_ z0f2r#W`QQ8N_mDA2QY!em^XmX zWX-2YJ+(E&h9N(Ncu3eAS6E0=K!sONIVZ;b0^wU+nY4hQ&QL|F zN`f1(U0=9wqrK%K(n9g9Js4QNVKPgQXfmYwopL4bs`CQLaOr^~oGP}W4)9&tP~vt^ zWdQDrhU~uOt0!ab?G-Adqg%+>s#U&Y?*!C(w&e}0K0aLWNda%l1vaFGP@(5q{=}pY zh_=bGxXaVd)iaT%Pz&c{PH@?^7jw1fVpAzZ!W}AX_mpDX^2a9e|L>hyQ5RolJz@5} z`kwx{)RU%xYlYFtn<2*rvPSQG17|7*a9jsd>1-3~zilOE1Gjg^VL(Q5SZ^P?Z{QZv zcBNF06>_{|g`)8j>=?YirFNDU{&Ym}{*EEXTM>?j96!k+$6lZJsDZ%{(rJ%l$ zX|xiHLwZQhduj zgfktyOXUJhUys>WK+^kiz&C7H68i_qj<>=>V56rix5)U>Ct=4YZH65inD4u1dEVU( zJARU^;|YcxZ=JkPV%V|A2^}?&4&DKmRG588rk%oMknF}0Q|d_%xF}LV_92aaziW?q zBay_wjNYbFuleT=unWHWM!fM{opM!o*_fW^5u+bl%vm4a9Gi}gAOZofcOYoC(B zdwzi=lO)*>1_Sn}Hw`YzVa-IlV;{hApEI3Db-rfCj}9jLYb>J^$CLdHC(RnUvc2=& z=c!%oQ4?5swodlb*9~09)^@c<5?O%7%}5T9?RUWjuAy)D8rxc@Z~PcfK_aVdh=4R& zO0CppE848yjQ=0+#{P`)|B%al)csz++%1c2EJ9U%@*trwOs>NRdh}5Yb$d5hOlO$0Dhuw+s;=0^ChG z_vQ8pC0KMfz_Dtu=4T~vc{NAV+0On%(-r1=0ex?s@uLGnTr%G|bN1}TGp63`#k21* zoizI&dv4!z?Vi-yuhwo}yL|0MYcs3wU9HTm%zfMZ*K?V?gFHj zf3^I{<;#}OUwRZ|{11Q(0aq`sFMMg?+9&Pn_n6J_gCxVV3F)OA7}9`(f+USz@jW6kPp%8b7o z>fiy2+S4rBHfN2__D9Z|#q)M`eB6&4AD~(P=B&}lZ8B>X&z+f>`@sL|b>cX=;~MyX zT?3ozP8XJ0QN&tRN}$_#r5~7D1-7Y-Jb(7)x}&r4vEB6TGZ8zbe z>38k?V~g@`CQv6*H|_ijHsA9)n{Iv2 z&!4mTLemK`d7;gpxoIm(K3$8YhipEd=yglOAwD&=wy_oKb(;#++4xwt0{N!j(bl)^ zD!gmzfvj)Y950>h5097it()Vev+36HvR=PAUOFKr<7IvG=6Hz&9;ijsW+;?HV6;=7 zt|hbGc)fR1p*kBMd%UL2*ZLQm3e^dJghJo9sZgCww^8W(Hx;TAVnd2$V({tlQvGd@rBBE7dUCdZ{JEwd{IE9935>rL^|#!r z%yZr2Zz}x=6J4IQGCbDv*2fA#gNmX&{B`N+!UEBMO#@TNYotc=6)?#kqxlUHI_AD;BVY z)8~IX|JC^ss0SFFf0p&0neWW}Y0tmx`76^eO#fiI9#r)3n^tDOzh}57x#zUC->iLk z?a;Ec3@@L!^wXu!F1>Y$vo3;2gpXNXX~|h$VE%*oTjuwfyXGj!E_ff<$}4kWK>6rD z=UMPsP`F8G*VroAVnn1Zz@1i4nf^w!T3%&&m0sa_%d0J~p4(@^7st>7hahAwK4@`W zLJo-3bkx<%n`hpU z<6OBTatMf~qszH`wKhd#-uf}?$MgzYt$$qo60)hlea{$t9Y&Jb)dPuMEoaq761A2u`reB$UwV~)8AJBB4>AnqMp-+Um zv67NbN%>AaqtvTYnp*%NU5_m*I|9_wM+Bb5~rkiM!JTU9o=8`aQkE3)XL0zol1r(fV!cxAh7yS-)fb&Zh8AuLXJPQvr%-%Z#59<}Sm>)84TK0}K%;pEp59&ppGym57 zTfM?2^8@AwHr2e-1Dbzh{>>)vPFn!$%-d$(rdQZ9bN$TqdWFq1Z=HFoUg5c!>t?Rg zD{Pv1%gkF=9W%gTs^@1DcBfa*;?l)S7wcVJOqXAN4%lVsS%@yXY?FbS?+Xz;ER!PZ8uZnV84!UBJ(>4FRY|j(t3sGSFjaKudo#q1yAV}wyY#ql6r;BD~XkaUg0?q zp^WMkHm$^0;+reLPO4jpt;B$76!e&fahXlZI~ml{(j`lmYzpu64qmW+)A~)l!t>T| zSRXT#osg~8uUj88l%0ev)~{I~GnAc#&DPtkx(&r@{;K(_n-93tN?qVY3f64qMy}~B*7tLSXoOCxF`g}dwK%$|rTCC-WgQse+ zr|N)pM`A4{>gz%mSG=V5e*iUy`h#d*WqBXy*#jr3)E9YZ~gU{1opQi)1o^|sv zPsJTXU1|Xvx=U(FMIynx*BhZ+y&_d|l|bfpv0KOjC7hzcp0c-G2REqYR)lRtdx@4L zVBM)tsC8l>coVH+Kr7DoS+ZKp}&(&a`s{__m zqRK6Ar6+W=u)XQU8eLGhpvbklXh|9n4am-b%xiuSAM3Ld2cM(CK1TLG6l zT1B!(G_iRrL>-CxLxEh?-*xeQGz}85gcFDm_t<%l z3kC`Q*-j^0Yvp^`px~h6V91`O!9Hv6AXSA+K~NcR*lnu<$h?rBo?tGi+Gn6{#vP4y zTs<138Bm7}H#Ep0t!UpD6_PAB0I{Z>ln5r+Gd0*}>VPde>IJ8>-feh$Ai-lJZ^s?k zc!lAEHZqLUO&1j}R0aehihW*^T0U3<eUqD;2sTjkB$k}Yc~i! zinORgC6ce#I?Yhcf${z!C?ZK#axO(IrBzn+@C*c2ur&>KO-Esrdej{a$_X$@OD%7# z<}C4jEM8}^V7mzUiDu8+;2Lz^-Kf@G#KBb!c2x(gkyqdU`;w;jnuL^IIF?V>VP#; z%jV#W20NppFp7~{HaA=`hFyMC2dt4=HaA?Z!Tw1HtdUwaH(aK{{!s_4ky&v6YL*T8WN9M`~;q=9Q*J~~AUgq)jQ zH&DfB=lAwhAmq`~d$_kLHhj8~QbJlFq=9&4P2NCef$ak!4OC{@E)bIIvP!2|Vi;CY zrb8f~q`2kBQ2@b@AQGoIz7P1g@W*lZ>Yp$C5wg2IM-94n`$SOMf%75ZJl9hwDa?WT zy7@K&+?gaOffD!Nyv&t-j9rO6;W%VpwJrI{eJpBsdF2A)6rmUls)(o6o=+($P!!8I z392*{ydFos5hqg&g{k}987|3Hn<1q|j1#2?d`N+9chw9U?2s&5))#cIJ^CaL`MH~M z$Oh)r?wOxsyEtS6d48T6^D|JvY&-MYdVBRLGQw?$JSB0+ey{zgb;wC2EUHPYR%StM z=xSGuOFp>62hyAePGLR_3nwU5gcE^s)b)f7F*C^f1>ZiO1I^Y;9F996!P6Tyea%X2 zUy1a{`#^RxkYAAK$u)?|bSPi9P0OjO+?Bmu#Z!TX4myxeqB4?_>ZH(f7Pyof)|Yo? z{OG_Cuj!1QGd9H6cW2iP%&y%tMEn1hhG=A^7$?T*3?vrhR62d*X8hp`=ueRWK1N_n zs^03=JM80LfUZC53()EFdXH*%>7JNwIE8E|mWHELIfsZfvVxL+zpEVRx8iEJ3_&%~ z8Ed%dCv11|aJRD zFt~KWzyRvHfs+9}3@({4Fo3#l;1EF%gNr8&44|$XIB(Fy;H47=22j@x98Kt9P?#_< zfVytr6hjXKdcwfKA;!9alZl;u@mCzmPZ$_LT{m!up@#uAVPF7t-N1Q=9tPxufdSNY z14koz8001l44|$XI7QLJfS53F$GcJ#Z?aHPhI<{R;Bu<50!T2&p-g7jFBU89h|?oE zC4mmC8>sER^Y-_OL)i%f1LrO41`bhnK8?8IP-eow0P4Dd^ABr#M%^6N2vRXmBwkK6t4M)r;<&69K-q;b#6pdra~LlP zHXd2`GB%`w`8v&Vtm&+Uebs_k^d|d^6Y;}oqKHRBAn!NrlI>z{FTucxZY-EM*`Sd)UzF_>sImYnWl*I60~Ko|4vVQaKZ4V&S|Mx;a(Uwm~(n6hU_puJB>a1fPL?;Xk%T_`Nf&VOhgvp#5jmo;ShndPk(zxfB|*O*^8_r1AS%$;Gn z-6WfyJ^T6D>g*~2{xkfLql?V!$%%CX7h^$cNvGR`iub+D%w|q_2)*0)&wqJt>J7jD z#d~i2llhOI{K(1Qo_*Hc50ax7YrqDs#!i6OKAGBo!mr6+iHomMPk!gEKRxG;yFQwl zO+EzO{gTt}{@$Ipk)sPWU;~$9C&0BsNuT-0QTZKjeEPNUzV_?B`N0nb|8(+$bJC6L zAN{-LwU3gc3p8K@*JCHZXT0IHU;4)fhqqmQ{q?PG|D1RH@Y@gkaBh%nKC+Ve^lOu! zdoDS8kp^twg6stNbDz$>K|Y(lA$;izng@N|^Dk0Nw?3@A|^u`_Fmj&8=%zUhgs(|KaNRH)cQn>leJ?oExvcZ*=bS&Ll@34cLG`Yy$kYZ@uhM`PrAeYUYaX z_kLOS-}t6?KU}-dv+%F0X!G?^Oa7ze$gKezaGp(o@B61~Zh7N>zv=w`2kyS(ybpfj zmJ6;=CoVnrgx7ug@9#XP@~pt`$dO9}HsC*-0H0=l^z27Y{qK*cy_@#mb0I4H=Ycm| z6?yl~KmX{5Z~M~;C&m`Y5u^bdaEncV?>zID|M(rp(|#bG9tqqf|Df+Wr5wHg6KDMB zx~t#iom==NP=!+iHsI}=0EhnWL*Z9-Z-!s@>hyzeylek;hhFibubp`ZeD%wvH=TX$ zp&$GwIdW*g2Ao(E;5$C`wdcK4y62`FKF8kjrGx){YvbN$KjU@TTZA9=KC-`d<|zYm zWY>TV_@*Ynr+g{(H@*w$cRimx!|M0JG{Al5l&TrrK;B_A({}ONBdwqi(*)(7SZl?(_^TqG2 zuYB@AmQUUI>Qw3^wSUTIzvJ(`8Ghr!{XhDh$qG#RJPp`@=V=0b=l;L^SL78h{Kk)N zvt9C)@Hbj#|LmIHe|?vGPvDHQw=H|_$MF#)tArQbJo9M-}_S~_-Npr zr(FNEd*AevTZv06H-q)~91Ylj`)30DhcjF+`1+NBzmLrSt@k#I=eyT`>p97X=T127 zgI{>*)fe~r*N~$ZXut-%IuqdQ)`=5-ioE)lzcVL3FyFiI?#2h+^cM#{c>S&K`S7jh z=YD>Dn;f030UK}uO@Q(9pFOht$D99c@#)#m1)$jvJooiq|BGL)fC{Ci>)wCb?a%uS zIXX)NHsA%C0N?e_4?l15-QR!Ym7n{!lkaRSRKD<;D?j+OZ$1Cf-c7gM`>qdPcO^MG zQv){O+?fD-pzmLE&4K6MnE0*hO5vgFfBe~xc<-$*eC64vz3{o$-EuAWK613K0UPky zOn@&u^Az0oOc^pC_|m0kmT#rEi`itwc`{vY>NFlgT8xeF6LaubIy$-!r*q`Q7td&bjBF^ZVVn?Qf6y7f*u~(^!gWSN#0s zO`Gn%{v6*8ckJhUSN>Mk;0drLWGaA&Pu#^uJWrahxr|6_!l?AifJsIv@3q-#XXVBA3px{cYpk! zpTpn(_u9Arw*K8!!S%ax-uvpe&0Ya2-T*76vC7h}_#Vu3%`4x#Idb3ZUQO)j@13yw zLe*o}O`Q9gn?CdP`#HMJRsZ5CuwohuE$xbf&%gQZt2W(fdUf|>)3;uEQt`kkuRd|7 z_l&HT}woi_cAH}OvX_RI@^ zIM%;-0<4(EB1XGn^>>@Ud+q0M{`HTa`rAK$eb2*+)J@_qe*2q}OaA?p$ZmAUPo)0E zx5J8QtWC5lo_zBwH@_hJdBgXm8+iF2eB;vZekQWzfs^-|evz@BciXN{KjmLM9#%|a zA){Tf`&Qd|yaS@UM8WwVeq{0bzutT`j=le%KYwEVpHIE`>-I1H$-lTBR!n0pqh0a3 z_q_K{$G`Wy$7>D!Ps!*u&ibo=a{0xAfBg14czx>TxBmNk{EKgc71LOvXjgpZyF0ctos{^N(vl){imW`xTN|RP+0d=-QIgnqY2C=#Q%^-DMD@?cy`f3D| zRBUNg!{OC|vgWwWn`qcus=C!1ZQ8Vjas;y{BZg!xDt5^n+C<%^f^A^14WnBcHTV)v zk2)E`?AfT;Mzz(n1z=mCZfQE~0W|1sb#vOT@#K>>!fPhoVt3MnC1Bog9^}QO$(YgK z412UyRV|V%h+K`Ds+nopJT`GAnfHjzrd+TgBQXbVRN{V9*x?DQNR2t<@mbuSpk0K? z-z@}xAb@ZwyJ2>Nov563ma5#$N6$LT==D11h3}R~n7OMCzS9eMa?%i8uCl^jiLHDz*H zK(r5)wk$76(;{7>PFgiqkIrau`wS!V1oW?*J;%Fb9s1hZ-(TK{5Qs08y4}3mIoAHLBX(;+Y%+kDNE^Pr@r5fo{vgJsS zI(JB;Gq}x85F~g+JwZMNcyhupp7eU6K9YE1Z;gp3S`a%q7n1v1WE1yDlU}i}?Dm!W z@I<3`C5@%*A#TtX4WD~rX^L|f>~K7RbI7ErLlPo?vZ)&y9vOPO!-tRKdcUbpE-6i>!B#@oc+K(b=>OQn8& zwiOFXF>ki-7(b^K7K?LwiQi~z`G};iX^bi&5__!lJaE?{ zvLp=wZ>t&!8@;|{)(1if7WFmD0@=ukf+~lJl%ypI(&#pL^e%_N=^I&3_CF1H^44KI z!H#5}jBPwAXzjw7B`8#h-MU7@74&=Kxqh}wrSk|0tul~ORgI-sLLVgKPPwz1!UArS z$mW!Z%1Y9puYs`kz~{L}uXC$hp^(usGEerM1bA}XFrFO2&y0<+Ju@0y4R?@C+jQkh zJSUoS=zza7ov+_`l5;L($YfzIA$B_|5}h}p&PgjuEbWrj!+x7Wgn5F5$W^YZr7F^A zQHO&btKB-fo?P`ZJpa#@F(xy-(*PX(9PNRlJutKfE=?_-LY-E7-vV>I8^czf+Exm| z@uza7MBupvvJ@jqseA$)loHE8=*|mH#jBO%0#PIKl`{D8+&*#7`<(b{hyCb1?KO16 z8dac7~Z0hA< zPcWJ*f+rYkR@DqxaHXzch}5&fl+rmX5!NO3d7<5GJM4p^b!(zg3Y46#WGEMtM&^V0 zgxcuL)+NzEE9TK!T4qH}mGj_vV@)2I&4h_yU6rr8Dr%oZlyR8jzz3&0gPEmysnYEX z$ecLdw5vusDB8XC5&?J#{ADbZD>yi$3wn^2$OAia#hVsSqU^|r=b3bh4&TGtV@K9> zbt3-XU`PJPu2E>b)O^G3Nbl3#U^~(|4?ZSKB0n-LiAW)&1hnsb!mf2FTa{))pGc{V zQFq;BOqUdzlFH*%6bOIFIO{dXwfaiaCej_YC2@$N#jvv2#0t3x5wjSIb*sc4O$7Z` zZ9-_P8AZWdNo4iZ(&lKXo^M%$wPZ*YQ#TwEt%Hy?We%Y%Q^m)7TTL>3}vF_)lr}lPyLa&R|9!O?xVH;JCBAY_sM=I?P}Q+l$VHqQau9D$R!R; zZOcg$c8{tgbK0$Vr7jVP3PHtouV(4CzOmMA^**1hm9KaCY{sn0=L}`w@?lQrAuOUu zMC;9@lwNV6oEN%5n4h?nT*#}eN_kze(W+M`$%eU*Qy2B6x~^q#_HbavDU@LLsueT( zQ{|#AqDx?vM$(ihXcjW5w8T}Fid~klLho6yCboOkOSg6B8stiPH;1j2uXkhLj4K$) zyRAkn>ozWHbS&A~PUSW@rtw^jClGJ4(TeH61t6I9Pt!t2rjd)ww zs@ux@>uSx3y{;@>=UtmTVKLy3t{%qav;Q;4+ zbizoC*Tccq{ccDAjWcrNbw9Lqzl$~jG|q$#_q?-pzmqlrG>*}YLVyDjK;tywc)cHO z-EXH&z?fNJqfNk=Szv_(&^UKFZWdT*6EJ2Lm}wI*W)_$r0W>NdHw%ok2^cdA43GdC zM`_3FgKX=5JtTm}$=DehFXV;~&0F{DXcIsqxWXs|XlWBb!^!k01ZW@uG|mu@*8|$t z{c73-&^R+Y-1*nm{VLi7&^Ts13IUrT0W?lqkJka)*8RYN6POdyAz;ib0FIt$5-?^K z0EbY302=4g$ISxZIEp3#V`c$xFh!GqF|zwe$>3lKoVbHI3a4sG2J9A(iYfX2qaQAR`HP>Uu3W1SC7PVHfCVQ*$k?V8#- zwQZ^~RhUXnoj(C(#Gdd(gYk zedu2FMsyFl3*CusLmOxTO`_+cLDY#F(aop=6{4Ha4d{CG7*v2Vz`DXq$U)?J1>*^AtW>_K)RJCSWj11TU$s#a@%ABcq&XzomCcy^5dciRQfq=n(4XmUb1kn@@@So&A$iIhw z7k?k{rFbKM4}TYbCx08i!7uQW{PX!izLRg{Z{{obLjETH2L5{fF?<1^!F!GO67L}I zdENoule`Cc_werG?c?p`-N@U++r`_-+s13~3cMumd|r^}SJ;;3?#Bq3%`ylro?p@q{+`ZfzxqG;~xI4MqxD9TBo8+F)4RW1aBX=`b z!4+~haW`<+bC2N)xD3u~oR>HUfw#v4oF_RCa_-^W#o5Q%%ej%WhqH^ble3M};1oDX z&iR}m$H_5rHggmlA!ie317|(w7>EK&^o`Sdrgu&6 zoZdFwm@c4SLchdkvsmbz=$#P1h<*{`9q1hpZ%1#3xF6jQaUZ%5;up{_K)emT4dSin ztq^ZPZ-IC-dNah&qo0TPIrMW7_o93G?CydW{N*d?S0LVn-Uac?=$9e>ODF?9@mch< zu>H@VpMm&k^wSVOg?<5h&Q1(LA(*Y5#kN#4G=$$ejMWU==Bg6(M5=)QFGDYbcqw`*#7odi zAnrhSK)e{e7~*zxJH&11Hi#FY7eU;LZiRRudLhL3qVI)x0eS(%7TSW?M4J#BXaiy$ ztwXG#HHcNT3bBG#AePZG#1dM9SVW5u3upl%iINcWXdYq?%|XnfS%?`l12K)JA*Rq2 z#3Y)8m_QQ{<7gZrff5j7XbfT$jY5o|5r{a7Lp&cnAL0VK0P#KOdmwH>w?I4(JrCl! z=(!NXXc%G$4M7Z|L5Kk~0MU>7A^K1sL@(-v=s`UY-KZO)3w1$sqE3ho)B(|s+9BFd z8$>H=g=j%75Y4C=q6sxYG@?d`2GjsikLn@nP#r`qs)eXQH4xRP8lnnSLEMaPhIkHo z4#fXJ{{!OL=-Cke9sPHR|Azh>#Iw+|ASzKMLd#8c5zA#OxBLfn9EfOrae3dEDqlOdjjo&@nk^hAg!peI0lJNkBr z$D_wXT#v4Y_%`%y5Z{Wv729_|$V8bC87PB7 zNE|3>~B;;)cjLHs51 zONa-NgAiXtUWE7y$ z+z;_S#-ET@b&Fd>P`GkS{^J6S))O7m+VQyaTxd;_b-o5cebdA?`!=LHq*p z1&FsHw?VuWxfS9q$Sn|WMs9}qdF1mDKZkq{;$CDg#Lpt1h4>ldGY~(Gd>Z1XkWWGU zB=Sj!pFlnV@h0RZh&LiPLc9UF0piDzk3+m3xgO#ovIy~G$j2al6!|E`k02j`xChw- zaW}FX;&sS%5U)k9h4^9Q!w^4&d9tKrVpTLRt`;NE2cMX+W$ab%-^j2C<4%Ay$wI#4=Kb zSVBq=i%1b-0VzNv5t6}R9_Y?7kK+G7JwWjneFTN}3w;=c_6vOoh4u?PQA7KMK7c~| zh2D=s`-R?zLi>f@i$eQ_eg}p23;i|IT=P=*SK;JJof%8d_IQr@Iz~$V<)2QQWSH6u&XQ6ud^83o; zYHOeIR`xrtMq{;-Ix9kFy$0>7%Pnx?S9A4`b6Xfx3T<4>l<`dy$>Pa=n-(L zSt4R9LX=7YeM3&DQayXPCr=e3>6#%*4b53!-DNSF>$X{c6EFEI(t0BwsMtzsnH{r~ zNNYMA&BsN)z~P@4%glzeN@-G;&`X46jYp|zcpTo8LSe0u!YpBy*#cxq?G-8Us?VZG zC#5dT?~^LT+JK=}mBp$Br?u#?1E(`JXH6Y7ChdAdTs2x~U@>*;;D{0k4SXu4h*W4` z8qzgBMO6H+R75(KqK~79r9*XPN_zhV3b;f7b>y#5jbu~N6?*sjh6WbP#fmlkK4`Kw zM?M^I$1Rwyi02(yrJs;0M1~9|4$2HZ5FS`)%NyhlZ4Ml<2EA8KCGvQsS|TV%9T)T- zJ*R#mVwhAaQl5n`IASW2x?`6^;-B@#)e%EcTV%D85V=$V zoloJdikr%`1z7H;sHQGzWjT@4uJx9jR<%1lpO6SuI$bK_Uu)(e8qjk>JDA&JGO!b59Nqb z2VhfCJiCBrQ#eKZ^19m|BjAq|jcs~*h-{zLJw!MxAHgZ#4=|ZYMKj$r7}D`5b$9Se z=h!*8SE}+DQK%%rB{4|9j>9}~=|lNH>3b|(5)_H0^(J1e$;+i+Oy!k1DrJLB+R`{f z`m$K6H@HexvnErs2@l)cU?p!gp3-~$j$FPG48@|dcs(7LWt&F7t<_RUXH7&ff;W`4 zRxJ|(+xAW6f}?5AzeU9fIfEja+7?tP^5^{Z?ZD0sM&YpGJU=H%MyPpLH_mBXyDI^(UfCS^@} z1GD~gqZ*O4eZ;7(CuK~$exoa-~Em78nMx3OPTUu>x z64y36cQ)3O3>|i^)XHlc9j1=;*hr_T!A$Kvd`dADj9sS<8NY~YtAJ$6HfFLdF{W>% zwEiZR&6!$OnY?IFhUEo$MU_g+tWk|KV6pim5<_BS+kZ%@64zEbT+r#wMG(QrD)G;0 zJPoNUp=$~KK7YxV5bITrIcGu_(TlRyxH&%h7_*WK*G+b~K;zKo@MDZ4aiQ070o4Jy z>m~p_l)%$C0XZ@~s|B98PS9Z~jpKeJx3`R?*YP_nrE#Wh465E)61djnRq<(LKNq2!px$(@nu=_kib z&>m%)#`@l;bN|cbm;u_MhCUiEO-8=2m2!OTMIDyXSXUmosljsGyK+Kx0DS+?0z3Q9 z5EQsOK;r1<|85U}CD?a%mteXCvBUSTud9gX zYZ_(4)3Dj93XQ|9s9ViR2a!nILOQ~f_j#*fPtoU28Pf?|X!JWAky=0;PU#Z`t1w^0 zHL_+*o%c4=T64`<&-irymR~!Pe|>oCU!w4p^N_eTo`|evl5lFv&Z}}u*p_sCLec$rQr14hK67xpvc3Uf| z69!~Ol~XwRbUSt-hLWljn!K957g|gUfD4XVU#W-$OI<3}Z(uOta zikeeYu{X-$q^e~~Hx=`xXh5nc7wT1yr4aBHy%~8*RPq`tVqG{9vlV6)^FFIZE0U>3 z6n1dyK-e#rExv;i_A8sr85&*~hCltU6n4d0PT*yt)x!Sz2a}+&7|?$Rqp?KPZ&%%+ zsV+4vEPsmbyBSqi9%Q~mb4k` zHd`^S)oEK5oj+$ZT9lEbCEY01q|(FRM3*<_Pixhit_W2bs>T z_X*!X;~+D5kI%l`y*NW@?o}e@%vc{sS8A@3GQoW(hr)iEOXGo!l1bY8VQD;g_Sak* zPs;r?xBW!dtF>d=1KazI->bFl#)pJ;v6!5Oki~1&t1Mcx*Y9S}iICS|wrRt5pS0!E`f^5* ztrYbizFnx)bCrBF9H={Ovq686EZW3UqZw?I&AE#6gxpZBT14}?RzsQ7s`C|vMPY1u z=l!XqP93mRWDT8O7gCaPOfM_MQYBTa7ArV|BfkHqZk>An51w`rhLb=2;O#ufG6A_7h()vJA}bG-{EkWOpbuTq=3PO z-|h6O?{22$7Z`$WOaxF9o5^H@OcXHK@VlK}_1(?1`~rgwqA3Wz!{IWSTmgf{6!Gtc zX^@ogALu5f{0F;98ULYfQqF(4n^XuMfk~ zm!;dk4E|;un@eShSTt45H>$aMoFK}@Y>oFQY=!xjbRIa$ZN+nOlB`rK#bk`g@gC}s zS1uGlu(fKXnlA$?)3sV1ukr5fTx^ga=oDTh%gI_gS|{@9YPHOJyi;pAmjSylo2^zU zMP{kXVBXmy7jsiCvZkbsUoS75{WDFxWDsLh$vDXaZRm3i;nS zB#__iCNciq-K2znPd6zOdd zt8Iw~{jyx*mkaZl7TMH_b*WMMR_32A(cPqST)AR-Sh^EV$#s?ZEd5WzH{8bA%ftN@-X8 zvD-W1&jJ~NRqqQ>_5m~6Sq_OhDBKox)&PKV-jQ%%nz#~7B9$J0M@QZ$nYKYQKYM#y^jXlrul`;x?rjk_-@aPJSIF?te>xdJKP{8T+dJ||b$`cJ zcdSucPS^T`qn^f>W-AyX_pJ^w7HRvZ46wfiijY}*P3qG-wmIyw%l$HuU zwUm|#KHEvR*?h{4%hh|iGPh@RZ3Y`}SjONUmO;+;<8zm%%UHicp>OE$ceVO1Q`rue zS1ULCWygPP=LsezfJMJ757;XfcMbZ??v?EA81xQbJO3?X?{3)9z<93Ua)+;-9~mr( zkiVzP91;Jcog{}c<)`d{-MYevuV6Y;%wO!5mGZCeCYOW&FjJ-w>|SD&Qt**Zn#qz2 z1s_|wAQF6hDSe3h9d7Pk&ds*Ew)xmjx4Eg_O~*j@VdDxtJ$v^^hIusyA(wY#wsQr5 zfnZEFQWja!<>2g9eL2{7WIn`5P-+DSSMqb^e+-sb%D?&$BX)I@a{e{lq=Nt9Zc@p= zuAS^!@~a^mR&acUB?k-@3$9w?l|=A?rL&r*rt43n`86vwLh%!zhxjszTP5@W#d;&0i=X9BAZ>R@3Tq*93xREST4Cww*QP6p=`_OIAq zW?#&9u;0!)$hvd-5#VDjIxSrH*L4rByLR3A;1s}XQ}<0>1AGIXiT)XQBKRPB9{Mii zW#n7P)kp|Aee(B{-<-T+(m#3H#BV3QHu1g*-^8hc-wD1hxLn{BoWlP#|112<_%8m* zykGLZ#M=S9)1Acq75B^BOSmrX$(&yUKXR9H+^iieC+j5ULFS#ziu7BjTL9W|7_%MXCg2n}oB4yH+FO-W|}) z+||F@#o$g4qHI+mNZGo9?ey*@?6fmAu$|sQupMMzJH0POJMEwY+vzO?+nF3xMA4Ty z+JQxS_bS6yG-A%VYjUSI7Eb$S7?%x_f!;DyD~wAA5hm}AO#=y24=mEJ6|QPvk-p5_ zJg`V#zWm3)BK=f=d(OZjeOdYM1B>)!<=F#^^uAbxoqX1yBC@`G`L{vhFH`jF-RX$G za$q~Xg&=8)f$jA2ztfIEFv9Pq_5CO2Hs7t&~)xbJg}YKLXb3U zU^~5%)@esHu$|sQupQyRB7MbkeqfQl;yE|SIQ8mt`(_xI3^Go=WvF{$>=;Ctw67Ap zdr&t^d*!*)&1Vj5r?(L7=GlSm^vZLmoihfu(_09(^R9tK`YOSufu!|zbceKe4s55l z5F~A8U^~6a)@kSTf$j7bg6+IxV3EGccIrUVdiAD5+GzvZ=`93F+c>bDUS;dFvteL6 zy@g;qCl73=S3^7PoHDST-a@dQlLo1EFLT<>FfJaX*1ct@Rv6m{5hm%Y)+Y`u(pN)I z7}RHyUS;d_+1m%U(_0Ak+4_O)^lE6Qo#O|#(_09(^R|IS`fBKL14--UO^39%4s55l z5G3u`f$j8aXs4aG3~Z;j5Nzj71Ka7<&`vvVo@5`-@GzcYv+tffYwC(AF7WJsGrARZ zp)<&<$iv9>><_WyY~{M_x^vdOdFtm=ckyuE)x57UZ)aZ04Duw*GbesF@g?xIf8NCW z1Y7W3!RG|;6*vT^^8W<9+b{A-zM6j=?;ztT#y_T?nf?sxH>`VDA7aH>N*2oe@pNn2 zHoalpAJ^T#?jykKzXo~hA6d$?rO%zW;p+)LTBT-O9u zbta+H`F-YQK2h}+Rb)WhQ#IoQj1P2cPFD$y#h44>AtJ8R6f2H6)~)U9`zfTFzTc+ZWRS-^XO_r$1~CqVdu*@7VMlG>qSY|eNJ9mjC!!ol#ES7CuLoh5e_ag2RPYPaVKpT#! zBaxuRO6J3sUiswwh11o)%&^RS&YwAd?)KcU%sdbr=Cx5Xb2+bax|%(d3=ZcN&MTwV z%;x-w^QTcWvp6qvUha1Dup8w3k@LrH#lxlmJjRC^A09O`m+>LSheplJVO+zwX4K4V z#s?W695pkGv5T>5O2jy0(6D#tTDRI^L(jzId6VajYF$`cx4sL^vPLmP7hc$>oS*mWFFx>#(8Yi%#)l)IggH-d4lr@=aEq}3pfvR9v(F_pYsss(6k}# zE8EC=IYoM0Zj+fv^xWVCn6JCKHK&JU7J$%P&QUY-Q4ot|)XY2-1Z){KGZzI>T}I8! zK|$!3Q8Tkq5FcjL%q$cHk?C51VO@uUKr=uA!iSV#oT)C!!xUeC z@9|MH^LdZ)mK0@J%{<r zoOxJg4(EQ({iD{*=G@1*Z`8~z&b^#_yFxdVjPG#1)2(>eOkje0Irs8WGYhzvaWA9x z|F35_8Pn&jyM5i{R2W24TZd#pY`v2wYJ&U0ll(T`4|o{&{haS}gzPK9&Hz306O7kD zfi*tcXBOkr3??z}0-KfuOfGkefHRYKco$rHvv471wM6V?S+>>+7&QK}IN~d7q8XVw zDH6}A91*LHT;-c#quLTSmr1=nnX-DdA$v62>t&3?%w2U*b}aQRKY5L}!dUN$-uBZM z35rhfAv)PJiJ+|IY6>$(2dQ<5^7(jGjMYtcOUq@bD^e+&1utslg?iEG@pb52?%QEF zrEH2u>=f!-{MnHy-LM#=D1GN4N?9{mn=-K=)`fy0mnyDQVsgwREs`=~&XBI!6X}4~ zSE;0g34OpKT;c6wIE^fdy7_7N{mjTTp1c^HW=wQwoS?WqBQHA)6;e#3(*;R^2u7_I zqFG5dy*^!oup~8>YDAp21YCYsJkueueVlkWi4<+Kf26+8G>=T-hQ&xvFA8Q%D(`}M zfh5Gmia1u%)nr@w(b&6s4D{yw9=+ksxUB@~Cf%Zn&tXaW zwOE>P)iU9%!l5W<#X5(U5NjM}Nh&O{mplEq+#T4)p_K9{8sEN|`aa=PBU5_j;sQnK zIfwLyJ0rHHt;TSrF0&efwOjyegwx;*Nvmj3J1jnVxFpHrB`cQ58jW3{T|lXnoG)j4F)& zc(x=o=y6L^XEK|xkk?*Z=+e2uat@UMXgS~X0gBQ$e`{n)H!Pkg%Q=1Z$TXh3cy6!d3@N;R zv#Ke~X6;39v8eZEB*BevN$P{i^ z4EOXR4_KwSj40z*Sj*y6(<6#Cgb{lr6)z?8k(kIp#9bbH$`;Mdd6zW4eU@~%!$+v= z>`N#b*}q$h#@=^L+s|AKQ8cd5bsDZkJDl#L>!wh{muc#ajzleKEyOYoRgcl8bp6GD zJV(*XNUTL~?>j3nOA1o-uF!QFu1Qzx`cOJ4U4QYfw^84JKW=0?PhSjBbgq~s(Qs|r zp>u_<52cjS^%q~8p}zmTb!196Ec$z-ltsrg>PlT7N+YG~FaF61zwaKI#*-I)y}C}r zS!##Gj;;?Sk)rMQ2dM8qY#N!unMH4pTu#o=@U^;HE{FP;n4~Ct@jdYSfwkydHGAK% z=;`4$Z-$1i$Ccb3>ad2gNiY8NQ`Gm*-ntfzy)T)8O>$E-uCPfo{6BVR>|>LL(hF_U z4|h`Z{`kJN=5EQ^&J{L^hX2V9 zohxk8P)eaq`ksrT^gwfDN;fPzdZd&!L&L8cO(}&o>DiYl8lTIKN+b0D$M`A(`Jmu< z&MuJnU-JR}NM^hKNNA`&ZMRue;FrGsNcx5+q9KTslMFfzzhHhE3fbQu2@MN0x<8Vk z`a{t^+!Hvy zmU1iL^Oal*dnQmP0%51vZqAAmNmIb(n9US1X;$epsY_&Cs?!)1PIpZytN7f6HyJCN z%Cjbs-Ks2y}-7DAil;Cu~d_-aF~&)$l%q{41hvN@PN8O^0Dpc`P1j%8NFwU0zb+$&!|oO6|C% z6^ME2#+;%R^T=&gxmOeh@vkVmcNl?zqPdb*rU|NTN{13l&nkm9VZI5LOoNSV)n3y# zv!0?ckv0YD23gop5!;0|tyUd#$XwZCO{Gw(%OO)i6R|6j(c)~a6?U7Y*?HAS;ke+f z14RT;H{V4mA{BL$hIEZj5vBdyeI4Q_F6e$DkZ^)bC`M; zt4j-VU*1@YgRooHXiYRLvj?r(h$LM1<#c{yB~bSoLusqfm#rA}x@OT~$V6n?vK%vm zc(^fV(LOs%29#2(p_Q7oIqiu+Qmz^)AeV%?b)a`3>gFa&?>+#cZqg8@@#&qUpWe~% zOG~kz&QnhR0gdj6*fXFlf4ch@Y0~hho2v%Hqfs}3)p-M>Zb~G;_r+S~M%yT4MDyZ^ ztDMgXWwYXN(jpd%Yt2aBA26#$K7TW2P+AhgKsIyO<|dT3#6_)8K@qgq?8&mGWU*l? zIY!Ex^O0QGpGipzxWs0Q;SGg7ugc0mRBtKRcAhZhLyd^s;8aM1c+{f~U|Ltkm`SIc zO{rtVVV=5mU~Z`M{|MtI#=4JANsz}UGZS3?4Ll|18Fr4v2S5MoKNrV%TP}&7verp# zN$(3vn^x5bVU1b{(+lx{vT8EGbQw1&TQMyuacEU_ObTw~_2i@Jut6g9dZRFJI1ln-(qzo&Z-zY@vAS#! zd&(lhp>`YasDU&a#Rej0)fR=$OkC{+J|lgFY%3j$CrA+{f430)iCA|I>*d}TE=N)? z+goGmWxYLX3l_@ZyrdWt>dPR&x6CF~_333*1v4hpDUl^?k;j8>gU_8Iagkgf6Y2`? zXr>WQ1sXZAMd_`(>&trii^F=k_chTG)ywH|^|I7v&f4m>mN{w3Rf4!7R`Fu4e!ZN+ ztS*t%o)%+4d8kJERgQqf>N9!FN;2MzhRfNo2zYI7MkI5pWxagIuwL$6usNc7**m6Q z)>YimbOBg5v&ZKRVZM~89P%mp^>VdVBe94$pqE%Z37JG1%WAz^l}b~algg`JpTVKC zNL*=|vsGFy%eN2f<=)B75!K7%vdc!j*CtjQ+*I@pvDq2K%q~~6ZNpdEWp$>M4dfhn z6t4xsjhX|mI6$OdbGo>Yv&y2uqQ;-g1m^51lS<)QCd>PW^>Xh?z$2-bZR4`bn!41D zg=H3TEbkHdnjl(%iO`4o$g(EwuqjL)wN+KMcr->a7^`!!Sgs`U>##Xj%x1?_)aIl{ zMHXA$%ln4)a_`~EBdV9jW|x&Ft0!#1g|1dSYZke@qB*NJ(WjTyi7J@2>ZR4Pub~Uf zHFK?IrW_Tz6d_@@?rh+uxk%OMm$xKR<8q7fg^O|CxtHLlFxPlB^@fRZdvZl*BG+={ z)|y0y(|Ev@h#9l>_C!se4q^UQv`$U(Okg5PO(dnYNY0+6CXC@ADYS?S9>QZ6+Vo0i zG^f#GuE{bLIk(^!_3!1YgOIYbqDq6WYp&^UcEE#OBqD|)UIYLjG7Rb6?PiQfd zI~EtsDKv?tdED7U1bLMQq9aIu zXppwKM5S1hNtBhiJ(0&3f;N@WsjpisHk%GhDs0-;^2zdT!+QA$o?x`>W1C<|wRkJk zbOr1hM@AX9X9}u#s-IC-=?%fADC#I%aZR$BRqB!;&{6=aJ3Y~=&>fG==}n}`?~&(9 zx#iQiTZi@X5j??Y*~c}(&@03iVa06oRQwU2$QKQe#h|F~gI~NF2owlyU0tY&aj8l| zR7w&>ELl~^-I0`{;>YY(1y=K6jr{UnzJ+@Kzky+6OkcI`@9T)Ee@x}4nCNxrX~>6= zGba}(=O^x+a0y-zc=>zyBHm3r3HKqcADoLgoqacZp7m{3fcXN`%lIOwfcnIC3;8S% z6q8`AD{hRW)TU@LZy>V9nl)d@II2B6=Q`UP+WVPi=5W1QYt1TbdAYR`PvjT;NpH4B zdOWU{!&NhS{MDSLVFJb%o)(Ts!Toezi2CRr%t|swCa8IJH@o z-m1%(Wx}*fq5+#fiAu$j%m-_ZW>^$53ukJjtT%1c5a@wTGkyf@;CDI@B zNy;SdtP`Xx9Tg|Gqr2Zt^}`m>59G$CJ+8skHYd@lmg*U^TNUiz4+CR2V!fl|jHD26 zM8#FN)M%f;-*iRH#QW;jEzV%69K$pCo_fJbM5Y8tjmu~PM_{t5T(Kx z27K?!YdWhb8x*=k9&;?3R?qqMu24=AFo$DJudWbGDU!R-1WnFUl8_s@$)$G>%%kJ zlG|ZzXq*0;zNQJq#D;KHCIjm%Qj5r;@`=T^Ils!}2>K<83}EdUUDjqsZf!7YduNw3 zxH)TgsJuCk(SWDj7L%)4X{O4ud{JkeGsHvMsD;dwA|QN&QxV(!E{YuL=F=mS)6qDS zq-kzMtr$_xX){uLH5N=1a#f>C>CkBjpQtjIXn3ljti6^j1C84R+JPFE`k!bV4K1Rx zS0AwDon5vhhaVtWv9W56=_uCgYiK5Alp57VD=-asUT#Wac)H;*r@%s6X+f;?=4$e6 zA{z=yv+16OcQMu_Mkc3^#_8v(byeOkT#!^_vYaJWj|iofa8(o`LpGOru9#I+ zy+TjYCbou>yWattgc=tgxyfZ3N8^lEdiM-y3ThlNa#Jfbj>bVNz}nNgtc{J_+F*_A z-A+p9I8|)-sT4Vs#zjXar=xK+jzn!&?B3YEZDL(yWQ`km+pEqS(K-J_=NuAX*@iC5 z@YjoW8t0X`EC_?mh`L3040+ zBR9EB^=TY3O7A|g+m%~JZfb?<(>N&v13ZP@>j61XqC+E-(?_CdoX1J-ej8{K zN_24KCYMPxjiWT_-ERd=LE{h@xv3QrP2)TaVC`{T*7`?oZLmc5zBHn9>Ls@OSc)7< zqJ1Ni(~)QzCs(!~yZbF|eiG}v!$Y}tBzhS?X&gbJ6IB+l4#}RrhD^+UwafIPJyjIn4bcfd)yTeP0Da6#-j=md88zQns zD$3Pj&~256qeQKxChcL7CFqx90e{hXTxr6ja8pETTaOw@>cr3@tKToB0F zONR2QAf8r6&AO~(&gHeK3+}45Wzp8OhM3gmvbf2n)?$p?MnA{&1{sd_glOW_`Iz zR_IF^)5c24q;$FD!IY|y@F<9U&}WSjVRfs5s~lzv;k7I3U{g{tl+)@IIW?h|#mh>y zNG=bkTp$WMxG4~J798jxVn7TpI2BJ23s53OQV?TRB38g#2U>S&YH^Ozy7w(G>4@ss zv`(g6y^Xt%9#KE_46N^x!R9auC1Z`4NflTMzez_Ja*%XA^Hjm>D$$TbK zu-Ee%h3xPT?O{^=tY%)QGm+km##Ru9wHbBFGn;ex^;L;qCoI(K-mKSEk=25VMn12` zm64=GO4yo3^K8Vdta|k3h(VW5)g!4gfko%%?S+zdq%#Xr>ehjZ<_ z7`-u@%%P&#KO+gYDk6EhER;G6QJcKzB664-4>YSHrCeunCu+;&=zjS%tdcVk&oHMl7z7T3^?FWd1?|NS@Gq?S%uJ9 z3e{uLSR$@e%!&M(l3bW-M*Y5WItffqA4NU%eB7(7L!B;A1%}UmdzZ3TRr zhE3sszUT=l4ts8AH8ku|(opl2T@HP|jupguMWv*38O-5EjtFL4D&4G07w~AvcpOMy zQf4Rzaxz!65jSS-Cd}@cwT4MeQ0Hv5jB!cOCp0^=qs{HQw_YL&Sv*RV7s?eJbUt7M zo#hWOp0TOt|5F$^WBQ_X|5$h7)ZeBqK>va^k=Ky=sZ1 z9)*AcZ31X4$&W$+Nt*x~>(!$Wkf%++m}MXb383MqZrn1ErA@$?S&*Skz?fN(h6K=f zuQhHKq-YZ`W)>uA6EJ2LBp?AajLW!L5T{MRm{~y3CSc4gh(Q8qR61@JL}?Q+W)?&s z0W{vb&WxD=91=joh2ad1_ld)w;J5BSpEdzBR=Y+aV1YIPV|`Jvb^m)H0W{tSk9X;E z>;5gY2^cdA&ZAAhm|1WxB!GrwkDCQy+60W51tHo5jF|;NNC1sW$IXHOZ34#30zV{x z#;d^bE~akX?}G%;*D|SH)tA z1;1ZPO9g*eO3MX*?4%iNg;*qbdFcu!_){m%WP^AEvoCS@OeUYf9O0Y6!SiGulwdKL zEO5&s0JmHr$`#OU?YxYMrDzAB8Xl9$V=zbfzSO&Ys?Ccl)(eCia+XALu4A z{(p9pV*dBLNeTbIx=AVj2i>HM|HE!lF8C2lf+jgs#0dE_%Q?s%fsbuovsjpt|D$%< zHfKQ>EMqByP5sm6@+jYco?{6-QK^ALe32HOk47@hLV}F&{u3~Tx|k>@GdZ%6tdWsY zs#%F8D=C8az0So{mQ0qwRWg~1SCg?+8pPe;9q3$4XX24+G!Z3|^?C*`muh)D$$Pea z@sLEF{mgQSVvitK%Vm#oY$*-iFyer+RHKlJlpC>DF~<9ThlFyfNEFk>Y9o^=7ozD5 zS!(5ZKj>VnHt=$?Nyh5&Xpz7Rl}fQ$=KWXaVl9%X)|$ygjI0o)Y%ZNiW%9fq!i$$$ zx1$}o7Vv>w3$Z1+eq?mH?iimH0)NQJob$-&at&%)CH6qBMGCBj4kiD)?POc7&wk|aCVM?E1RnW zH!#^qyo#4vWWAn?@}BBkOympMS|L|%<wifRw-q9-)$FMrU{Sy&om*I$`Y|?s+w<9bM-hul#AIK@6ir<%?6&&6^o60HkQxD za00JKaNZN`i;PPP+t+m@+BpFvTCC_wwBbikqE|>M95ma%tPtWA|E#zU?8wsa32Iy= zt;^(hg)p!9r!D23pFoXlxmeEr5g0dE5)4>GYHVeIp_TvRZc@U3zMGWsf6`6L1wVyJ zP!UTiMY4RF^85SgMpQ@P6D8iwGVmr%I(pw3^JP z;;m#eo{R8)*0~tZkkMK)RjNc&g>pR|N!Ai1?-!kmY2eL_OqFxlMlDt^#DH~=mU%z# zT+F5-wGxo^SS6mMQbaRVM zNbqXc%5&ko6#U;i+XO!Wa=}!!Eob~!J3~JYZ_m+QT|Gwwd0@ZpICuMWTdOZWeNwnV zA`^ABdh@~M;}npQb`zt+A$0~-Et`OWs_6a)^jLaGo50W__+_UPm@FA4_|=lii3R_? zl$HyAy_8l6ezR0SAs761>4H-5yQK@urr(|U*TLoE)LJUJ`uDbSg1cTe=Q^ZoBmcN; zN-aNlcxo_-CKxCq#aQ>)BvbG>BgOw1FUgg1Sga=y33GAkqu}y*J{OBPmqb}dki!wo zHZ~^>`XC-uS3EhNB;fapUD`_8(bq|X+HbIU6XuFflPV|@nYbg95=lhra?7Pn=W(!4 zxE7E^QZ`}CgQ-Z?(??#HjnAQBp=sBK#bKe|7D~Hoo_yHqYLWeXwCH^vd$eWtTZK|# zB^{6$D(a9iqg<%E<)M}oZ0NKXXg|><$ED zs)(VVuQ8QpP9d2SN5mdy&1b|i78RC~drSsfppH8;;X-mjYi|n6ZjUG7B3TEHz%Cn? zVruO*LaUbG0bv?%#o{3HUn`Q8^pP^HNncXx#Q2;JLgJHVx=9eLpGF7E`XUER6jenfHH9uN)F~}sr?;XdEG9_ScaOj>8<)Q+t*DCYO-*Z{;lTs?w8QHL zyGHx@n^HSW*jzoCpG&HWz~8IYEK$adiJ&H=%*Tx?rMh4a)eFwLkSvLSls$I@cG^o$B(3gDMOx7VDSPUO>oRO?E|4`+ zk2f7w#WL=AC=;tWGRcrL*l%8@E#`vZxY-c2guTg>-`>Qs&Y%iUI5w&>c=8T!S zmMG?q8bO!+CxgKhkLNk!|4c1Mo|KKxmL+voy(H((#w4Mv8n*{XB@yW7oLJM)kqxh+ zm<`KSf$INb@7=>($ErKgsyg*9RaL*B8`40Zrhtai?ju>2AI+;}Tb5*7mTg&A~wuA;cLp?C2$853?i6dKwv(yTwS*Q!;b26Q#dJ3AAGCm} zu}h}DQf*r2Y?_LAJe3VI=_HeQQknXsR!_l}mWM+E4R^;9F-+#Gv2iRtH?@Bw%fl=$ zwG}1Z_tiQDO?Gq(S$9hC!v#8wv?)-G0m+s{et_$yc3m>{Z(V6r&n7C3ve87CB~vN> zNoDGjT6K|Gjw3+*3nJYGUs=J0CQ{Dw^JXfXlU;OxM_NKzRYj~Dbd#ePOu$Hv7`fc} zBu6E)gAUHN@}WahqnAwmKd)r!*({l1I*o^DIz+{uOr}1u&3drX%cgpj0@WIGU{vzy z3e0Baj%pVbati)rC0rvDBbPH$Y4OQ$E;PgfyufuzRWH+MbSlk$sB<`hI4_y{{Z}&e zY}QITo1#J9e)<`ekx2w!-R;FPbyQN z)Lb3%6H>kF7SrBAA(-W2JnrV`h8b2_3S6Ffw0K&ktCOSa!nv7HCKlieI*&-lnNuI!mT8ltkBMR zIsbzi-Y-NA(fIF=)M_aA=rrW`^#Tlg(4Ue45U(T=vk^=P#N1e_X-T$ZRIQ zXducFR3yninMU>S#BMx-p&-a`jTAVnU*vm^VkVR-BJ<{Izs3)0cnRFh_7ho>6RA;k zTvgdoRIAd7h`$rYLAgN{t09DZI9K0s$<*(?lBs<&8K;S)7$C@GIG%bEO(39x)(O?V zAxn6Aghg1P+bso#y<)mFDxvd|rsJ}j9}bJ|4#wv^X{MlIqwsOC{;35RcMNCR3l-7ABa@i%rp!9=Aq$4y&Y4zMJH` zbGD^{P@+PqSw}QAs8*{%S;ELbx}=WkY9>*xSDHdO=P$@Gn?&$KQ{R5c)W2~hQ_m)> z!Bc)74=^10WZEqbPwf6L;2N;?A(m}2TB6z$3TfUI4b3~63-EBx$GY)ofZ#QljW9iq zmlZf3>4gGqB06eBkV?&^F}bjB(9-w+2g`3<+JDPFy!YX~H|{-e_XE2E^fl;p(6e^l zv%_wGY5Rfg8@K-37P?{v{pU5$>L0DjXMT9*VNeU;_g8Yu z-(G&>^7EEHuq3R5^S-y9)Y-xRNzaj9^OViI&n~S!i`bk$?A_Sx&OOXTV_`na(aGiJ z&wT#I|8c1rOdN!q-g54inAE%TPP{u(ig(^grBdrqnVxFEPy4eNS-@8cHrTyvbB*`+Ea3bAN97UNl6&u_onqMrR@^tfgh4=Fqw z#u+>z9FBwceZiuhz2o%Et_MRrla2;LEJ^C^`HiGSJ-f&0Ir4@Mr+B{~nC);Jyeno= z4|JTK+1C>lVrU-RbcPS@cJY@j>e)F?&+L4VN+kVUC>T#4+O6?@f<-->$LX1!4?>A#lJL_YXf7)1sc$!(~G=%-i;oCfRdL0@B0&+>74X8n`|8zF)TGU3Da-1tit^(-ByXVyCbS8jj@UTGWdj9^`E$X@dI6bp|8c77GM3VPs zvbvtf7>j!DJ5JB6pQb}>B16WK@x%6f^?erg+kkwrbDX*2^a zAqh4}==1fXZ~aA!dWOg8ne|f~PfL@JI6bp|nvRn3cr?KCL0!-A z28(+7$LX2%QyPzhB36+=R9_!FTKJqLJ?;m!Rj~iL!rtddO(rHGu<0Z?Fr=ghd&dEs zbv!Uz2_Z2?k@^e&QP$!M|H1AtdXALY0qf;(Af5@Q)4Cqa;spGldYqnF2am*)k#q#d z{i3eNb;gpx_5J^?ApU>$je@`JPR3 z<9!?9^)IfAYhPP?-P(<-KehVwGrw`>`70j)H2}Y~oL%}S@Q_9SUbXe`_3IKS=CX8v zk+jmKyggDA6Kts-i4xsz$R}zF0fPk?Rq;8Lq7;bxtKQ<-STQU%>4IGA<|AQt>@Rj_ z&N8hOWpK{U9r=f^)3NL>9aK52M!QRxKjIo>*)f(M9p@IKIY9_VWi9UThgGg^$U4|C;8x|npJ8VKYpJLpg`A1#;BNYBlpYRPL^-=)N( zUHRcXpa%tdSk#@$g=*m(S;0|Jj+P=J*6T?7z~{N-z)ez}xacQJ$)3xXu&l>i<^^=; zuF;*If#@6v;W*clvylu2tDIO>T&x;LW4%_lh6RK;Q4=V^AHyxHTHKwp?~44xkd6g3 z+eX<;%UGu4xB2i6(4h|*8)Y(&&jo4t>(L{ZM^jGCeWw*i;c>cmi3waWdrEZ{l!MnOUrs@{AJzfP8;(Q= zX)(Db1o>XC3dsUauZIrCEjOFq5(S`er2Ge*d7tXaLw9`XN zD$#cbk!aqh2{jP8rKp)M$OjV6Rd9hz$d(;uVS|sX(ON?rLJwfk z$FGf4qL#7D8^2wkPanTF@@87rXCA*Q(4)@)Hj+bH)?*&O+hEUr=h~Z>Hba{?ZG3g( z{Tn~K@rn)S#_IZ?tiOGIydGVD=GxcSKD74c{ZH<{ZNI-y?LU3*F>p@sroFH>uiN^<)^BV* zw3Xeuee(yKpV<7x&EBHpg45qlEpTdqQw#i`XaUEWyI1C)fP=DXGsmCH57^@F+v0B6 z;x5|a7Hx5}i?}b}j5>YG%p&gDz0NYRfE%8@?OrBraldpC_w4d*`NfO4XYbjTeT%qf zPrR31i?}bp^8yxhF5;d&ja(HzPxt;_tzJ3&-TvpR~B(!?hWACUs}XH+n39qU&KAzm&<>( zhSdpIO9xq%S|cfZKUw9{l7Y?jwEq(M8-x`tsw8xR3PZhZb=k>C5*o z;y%)s?_R`xq%VJc5%&>$|JMcFM-Hf#|G!1tN9_Gei@1;2`@byWK4S0xw21qNy)P`{ zK4R~mTf}|D-al)L`!%+>@3qCP+TyO-;?CLP<`;1vnNJ?l@Bhy({hWUPf3WxSd*^rG zy6b{I3T1X4+qrxD_{4FU zogjni#Wv7{0!>e|tjT;}W+%`9bZg7#)|)IVTimU)GvDO3BV|b5^po7-MGSH%X&;Gy0du)Ut05eO5nfEMT;Vw4w>_+6kMu0-=V#zST zu4OcHyvIfuf_$+EH0s_1S=MOs9vcA&)r$q7N%x)|%bG6p9veXjpj-1sw{BZjwzyko zcQ6Mw0u_oEU!kJ`P2aMN#_%2+Aqb$+Iit~=mfdGzqmOLF>=d#FaDLK&v+<*XV;70(15YL^FzyM=8R<30KGWCM=wxwY4-OG>Uje_uX@AUh7qIQeRSfUGT@3-3@r#s??wPQ*YFkz|Xf{7Rq!h z;8k0#=2XwE$4_XwKvz-#1pQg_uV_KVteGoXU8>kpdH^C2yJxDhFR7s5jG{lsb;tmk z*K65(?tS3FC8^!3fTtjUK5Tclm+$FKJ+#E(f8^@YNY|sQX{lAYdZ_hMF}^$XLUK?1 zmg`C?0GB&7CxU@Ghc48Md10puh*vpcx?gv!#*~!@$ZbpJPjSOV<$$ zQ4H0S_(44`g_Ce0fMecLi}h67J(i9(^DtFpqVlldWt_=Gv&l7mVXcXsR}nQJ^v+dc z@lw|5DGhod21iGwZnhiMz%y>WPsgJvIwDCQYhXCZ(wwH>ci)-UB_~dOu>TCQZ0Fr^ z{PaCD*fHHg^9H-?=wQFqPI#E!#dfOa9`f6GliWJT^=`Ejmv{Jp8AqMI)O9FkN2SFf zUmt}iR_<4#?NXh^=eXSQjyk(0(AAh7b*j8uskZeYsJ~xzj#9PTZsbQwzIiyKfOVq~ zOwpl8lB5!Tk|3!hrL*Px`CeYlX=(!qD_Wyr_EtAwIL2GAG`q|LeYIYVd0bwAWUg0} zWvb|-sX`{{%|zIas|WkT)oeaFt|pzah@YXyg#g3*+v0$~YOkh~9kk;LqaIKouP4Nl zDJNNsyJDfSNb|)`SixiwFVOky2v;c_?U5;eD4E7JS1GMzy#<=3vMDXfI*Ed>9O_8k zftKQie%X>&>rL0adToN}Uj2p}m+cHy+g`0to^!n#{c(7;je6Vjz1r@yFyqxn7so#$ z51+leob~WIK!B%%SDOl62WE%WXXRrYR)6-@I-(16`L5)MJ}tnUuhd1{<4-MM!{@zTpV%}0m>lpD0R0Wy)(VYk@iM>DyX!~j zcc{nZyprF6+5^QTjwoPNK1N0=KJsPjfy6)_M`THEII{(nPSu=2kS-DobnuX?S=MS1 zw9Pk5es<_>Bx8+pMK(_qMnzZL+aNGBLX+8YTlC@Qn7Z8PDFZ5|B=f=a2p5JCESbX2 zGoD7=8I5aVH(!aAqpH+97YtbQJH6?^@AUZp+cx2){g3ZY_G|mz{q?<1?fv3jd+#NC z(C%k;-@e=54enkCJqrCAbT1T!Zrb_c&bvWue|+be+mCI(Zxh~rXgj@qu=UNY4{rVJ z*2}kEu=$kVKrUf{LECM&lRIGS}A%}ccjp<2kkWlB+Frg&S2%@R3gWRN`+CojAwJM1W&lKjcPN1goh2v zqxsTB)~VoS5+9~xt=&%>5^lCJD#Zhhdab23VI^6@>Rq<(87h^I<{!31kQ&X;)C%6> z`h}@PrXEl+2Nz-*-hx)~;`LPD9jU2dxm_q1eNKr)2`$z1q!@AO*QOF_3LDl~$G{nO zIih32*&GvGr{ePaa<5;lTA=-F1FE^8FukA?G92^~`vrSP(dZ`Ku zul$Fp1e>gToQYO7sua<-uU+%kFtH;^zA75e6v&X<8$?rSs?%VY_1&pNw2{ecOx>UI z#oUMkocb}34>FgeR9sk0d9)-JeQC2B?%%xOzj%6`GPSHlkoWpad{a zRU@cDCxbcBwBQUX0eEB0kRY5=lvM(Wo@>CRB94lmAEK#3u*wLsAI(+^Zn5L{CSrbi zZR+PZ4NBdJz}+Om)gm}qNrXbq7E)A8jDm!EWQ?`gIGC#DvM!YE^i)p}ZteX1RDy7D?5IPaBpe@#jiy>{%Q)(c<+Bl= zCS^iM#2XLC*qr07apNNfzVhLlgk|U9#h>o~3 zofGjute?vXC6Z5U(TNIP9@?5pc=N-kujmszalb#13ghQA*#pb)z!OO1 zaZpt+Q*e*4E|cuh+pC5|PR2T*Of3?FMZSnJ**aNDqv1Y>MiDWStMdbR=#25TInLd1h;;TSDC-Sx^yP;>4K zO@>4^k(d4Blu&m>i4Mk&E2&_jzWm<|2})o)V5cib`Knw3RRCMmVB{6#LEi5!tHp3H zUM?{KtwVR0m8pbB2@_h5!ePGF1b>&^u2C(T9C?O`a427m3!$bADi!qFY1fv2DuHoh zpU}$0YaLJ2*Wubuo@2!tE7RekTk*K`y3^fss)f=dbgmKSUYDR;&M`lS?Zqwlq5=OB& zl8)4P$UW78Hd6tDP*9PpdDNJ;-$GR@Dwffw{`~T&qE+{i=^A$EuoY^=r|_*dN5Td;%qSUy7mQ{`7ezouL~o zf7kBEc3)mQ4C+CRXf0vGEXQt4v?Pu=iE}hDEXRihRjU4b20R38^s`RTMq{Vz4_oKZQ zaKkWmMZaU2Qn9NfWF$a#-63t5z&t`TC}&ByU5M2Rg`QFfj?1Zn7@OW?T+OvWz3&M! zRHT7rz@ll06(dR`(e#zky4RaxoMbe^79_EVVf~;t;0?C}k@Mb6+E2xMGLM}bKk8%;S(IiywXyt#e5@|Z&dR-c=NNTaW#j3)GV7R)@n(hnFH!ENCqZ zM{m0Rn(t3shp+k3vYlx3lbdeb|Jv#1?#GA7B?oz#y#=`bfSKuIe(>AB(JRoh!+9jq7D!Nt50?d1K{*q_hUTD@_Ypzb+vIfUJral&-3*n`LaLN#?K}F z;5bbwyEN<^pXHrlZ>bW;n!WRlXsj53sm#^K-86@3blf|Cz9-B5d^mv)3ND-)%56AP z!t+j_CGx*qvtw31_4mNqf|8F+Uio(zuLyVsIfgj5T5@S8~)5tcJCM zrW^PF>6JfS-gx`QYc`aP^BdQ#e|!B?>+e|~g0wMDUHQ}X?D|XBZ(jQgkOT0)t~J)! zHP_m;tDjtb&;D2Te|i6B_SJoM|C+t;?!9mC7xrGgm)rC2o!Nb4_g8n7-NdeY_j>44 z&^w|32)zt?3G{4eY3H|h{-2%K?NB@CcCOj}yX{}wet0{x{le{Aw*F=7(XIcoRoIGb zL7QLO{M6<2+c(~D6{i>LgO%DY@eiaP; z?o)yI?#j;JUG}*E^oa4)$1FW{_9Iy6cWzxPu7q~?pT4$o9yLU(1#7>Uq7n+_YG5g&G_juQ~chN0qt)sL7N>v(1(nNf7K8-f29ijwt@RA z#*cGwf6&1FWrO7jKZ8DCJo-zyJk!a4ny3S7FMTlq1%l= zABDSn#=vy$D5ezy(+jL%I+Av5ciB+>{3|P8GL+wTL^<@sY3D-EGk&_PbAMnUd#)v9 zv)g9qUkqzK#}GF<_xr|E4=g=(_UjJldxrS4uTK1*jRrryGr!DHzH8X>tnuul9SMEM zu;ncVi)q|_+j#V|jGr#EH{Fd?5n=L){$lU%9Mtgt4 z5TDcDZyN1=y1`;CJ0KF#>)vi5%6Kz5TQWJl)quNm#V(GWM<`}YRmry75r1NiR@ z#ZR%M_{bdpn4#f@D{J^$!z9-mEGCnD)iB9*#!r`-N9Ul_JrGk%=YTc0&-xoWVO zZ29NLqt6&WU1rPA7_e829~Zvcj(n2~{h49QWkcMs<)@9OE?Ii&k=goBr|sSOp&>q} zy?-)o@6Hbl7E^ow_q4q`|6=@fS$jVjy)1bX3mz{6d42Syv|_b`z11w@P32 z9Z^B6H{z9eVCd^g2$KmS;R@nSdaI%H0^cbG172q{N^zs+h!`fN4#}U>3MrI6uZ6U_ zR>3+#45^mSm1>#5I9pF=hsE<`)QMY4!0>p}RSA4{UzEDJ$1_~q3i2>y`hy-ZBqaZU zu9y56MafbyQ55U7LAM&O)3;cRi2Rx=e>~-baT^Sv%p`rds+mL|7Egki z#EE&XIFl3-u};ND3`TOF3kr<4pDK=+Si9{W3g`UsWU{05nVwuwI$rUrXOh6V5>{ba0;k3^)I z9yUgqEZK{XRVq^Px6hGPLaE{HbV0;LZ<|Om1YT<)Y{J(ORVC(eIti~EX?liXs)!{* zaVa(M)Qe#@tR)N15pFHOR&Tm-|G&Q(TH3#N?>l?F-EZtF&{v`I&R_5R!?|kn#AieuiLzajcAHk{(`xJ8gHZ z-0z^C2!A#kqif*1d!Zs12idM zzyOjrP_9f<0Uv;cRYyMIi~B=leVkV1kUQduc#=84H-tNpK7SB)jl~kl@;-MsLN{=Q zkmXjX*HDr^XEq;0$I%E<^^ej%juFoawMHrsPP3sf7z}}4Jw(i4SQ(fwz#~p>mJl^4 z1+;A6)m4T>pJ?ztE#YYvyjpflcDyqf)cakF;2+z(ue@*gYVK63DgN zuAdFWLajCcoeCAkwPv(mt+nIsp@dq2Sd3}6)EY`w{R7VDMkKfB=+wv2VxvcjbsPz_vfjj5Qs!fYbf()* zr`l{IPx%`&7?w?bk<10t^#sUu*5pcqBF~S|CYB3RvA`%2@pZyIiOd$cWTu$RlU3*pA5s zv64uVeo^ta({jV@>%l^}kA#~2RNi0do~1Hnl`QtdfkKk*jlkW)?3mnq>Z_Iy=gH** zEH0}!6-{Lwv7kGY3TnAQqA3))P=QwaQ8kyQ`9VTzieq+gkSPxE)VQDUP(zjpb;|XA zOe&X3Ey3q3L44C#?0#pD>gpGqp}bhn*t zkOcHVU~#VRf?GXaK^tB;u8M|B?Oo?*GdETle3*|Iq%u`|bVGer7+qf7iZi z|F->`_pjMs1?L2h?L7*z1b%q${d@1&d)wYy_TI4f%DvuRW$)#C@x9=lcklMSTlQ|) z+uZ&B?$>v}u>0xVkL`YN_uU|u;AHnryASRTcN@E}*iG$HyD!>>cMo=N+J$zPpl?B6 zhCT~@0(t~`FZ3(WTcI~Y4}pw>ZKwogpeS?~wqLQG+NQQ&v<+_`Y~QpEZ7*$oYwOEfpWXVz)+1Z*-TIZSw{E?8>!GcC zx7u5!t;|+*>#i-=)@@riZ(Xys3Njo%w)rUdO5wws@85jK=G!*kviXM1S8nz;E1NIh zjBf@vy_>gh-m-ba=H|xtH@?2{g^f>dd~D-`8}9~RSWG|-#RoTr8;y-uY@{}*jTZsK zpZ+_wz^MgJEpTdqe-{?mIkUF1+* zM7vEiW};mt8a2^Q6OEW?*hHT<(RY|=hl#%3M4vO!FEG*1H_^A5=;xW}=bGr}nCJr& z{cIC`tBF2qqHi(L&oa@^G||s6(KnmupD@u+H_=Zs(Kngs8%^|6P4rVt^bIEZdJ}z} ziN4lEUt^;8P4u3L-ZjyXiQX~M+a`L;L~okt4HLa?qSs9Hs);^hqE}4xvWZ@Lf?oci ziT;6!{udMdeG~mX6aCL7`nx9jJ0|+uCiO#6*ACME|~t{yh`@yC(V(6a70T`a^pB|Ff3zOZyM({b27tuw%am`e$ea z_Sl2%f7;f-uDHAT&CL$j$F|o0e!aQ&*jjz{tE<&BUp^zRd~rou{=#x`>92wKmH*xU zu4`Ap-N1p3dfq3Sap?YcU2_P)Mg{K!8x^sSuLXVoyY_7Zuu(DE3V=P^0BqFhwgO=H z5P*%6=5{J`TLA#s25`b%uwxs*33tKvApjdA`=q;I%Qk=$?t)F*08Y3IHVy&Uc<4!Y z!Mbe#C)@>VhXC^7MvTQ%O^zb-wPb`&jC!ML*R6DjsDmtv!D6IUR|_D+eB66*!T?qe z0rb^IszwotkS&oJkkYyttRQU7H^$W#$i?jHw({|T)NlJm{Qd7*vJJpS33>|v z-uZxS0CL7XPK@9>#kPV8L{Z8b3hszvk#T|?c&k+=lJ5k;?Nf&WF#`TbHr#E6VwGw% z6$EK08?$$z_rLT0LjWO%Ho%oMoC7yq?q;MkD8N`Gt09G!$Rxs!PQ$CEV6MWTQSqR` z_~aZ)h#4_~mwi5`$4O(ImRoX=H7zFBgdpGRRbe?KL|`ib?z0WxguCEg+W=0u3+_1t zU}I#TbQg?m132L>7}*AJ!d);t1YqN#C*1`D+W=0u3;KruZ20!U2?Nj$0nm6V7tXaL zw9`XND$#cbk!aqh2{k%IDQc#x=9O@+f(u+iKG_fV?tf>`HUJynu+4z|AYMt`}zIb_rAUNp}n8k%j`V|+};1?re||y;}199vQgZ)WBt49kL*6Q z{>EK?_g3g@(EFj+LjrWL^NpR~+WG07%+7PRzq$Q^?Kfj#IJLm31x_shEZ|zbdu`b6I6Bj4>_77hu+QzjN!6#>uOua9TbX-p+z&efM)QDVNx1B*oF_u~DF zMCSM5eTziq_aL)KWPVpf7m3W~Hk(>3vPfjUkA)X>%Y0v<7bx-0?-pv2$b2p2B9Zw$ z8CoPVzbA=BBJ+DPxJYEaCk7UY%C&oEpTdqQwyA0;M4-A7C5!Q zsRd3gaB6{53;YNy@S+FrTfO`2+WfOm&ymCLKv?AXNRrFMrl)!@dhp%_;zy3biKG}H z$YeO4dR+XTMdGuY?`R;(5mY3}laGs!7m3eq!^2rBNk@fvlzv=%v`Bn*Lyo8XJRV>; zGWodpaFO`z)|`oQG{J)qr{Lq_gGJ)An{+YgXX#WpAYKyhFA|^Ku2V^p3nf@G&OfeQ zTO>ZaaSw!uB%TmOD)P8^Z;|-yIZ!Gg2yrr*jp0*qegD6@{2NQV*Khs8+8uy6{rB&{ z0SS`2yOlqS{E6{TLw%B4aem8eEWO3|kkR0++nN(@7#a64Epuzr?I^l*kv z7YjZ*m`Q>{%KmnjjbK;}H}|3;p4Y+3Z9zIa*|9n3KQhGo&+9=FGLrRjzeGx*R;NR9Le zIC{q$2@9@rB^;{CWhY-4XH+av>qKd6;JNzZ%)ZgcACIe*1T0HqbrdNua9B+Em2^yO zw9B$w_e*7Ou}yZfMc4V^s3L`tkjv=;RhavODwbAKg?hN+N|qH&>sI37Eaa`9+2= zg+k#_INEESPox|5dN><)XA@ZxiT3LPItWIYU{uH}QMBl0EP9>Zbl`QbsZL&~d)@1E z%XXHiC-;&^k9yr4Kbqqi)1q!ardR5rGxjC5TWu-&`qZW4I7HM^_5ZeduyHtY9$(QS z;2Fo%OwrX`^#7HPR*Qy%GFnRU2~9j#jg<2ZQHXkbC0N#+V$eD4@qu))9UO=ARa8syc`O+es)J;_ z7A-QxbfpKE2{#E3`o6*-e?C+j6_R|w+KSc*!KUn1ZrbWJQSmgV=C z=b@d{&Mn)IZNC@f(vNT740ikP-g+g-k$=RVSEtKQYsGoLs!Ia4{~I3z5MbiaLrUYEhL zNi`tgaKY)RDr!7muF%!Cmcf$oek&AiM``!KgL%guPZNex$Rwr9IMbkl5dwD{2`AS> zq;az7b~{LnXgcKqRzOQwZ0N}LG8NPTLmV>UbeVB?=nGV=Q?h-y8GMH96&tbmDFa@Q-xd_j!oa7k`KEHYtr8E_{Y?-jd(Bkl_Z zQz)KD6Y*{{A0X-#N40>5)Cl4$@=>DVXhAV#64PZwSKBX;99NWxQD>xxi4a1v{e4D5NQ9a+iur1()pT1+t)QQiv2QOgKJNdH0a2sARg-slreMnM8CM zETZ(1BiCR+VBQoqc8nT+->@NJU38RdC^ak@4asAekn&9)3L}%SE|cRFTJVvB0F7nS zF^a6G@wB%o)Pf9I5hE^_)anY!aSE;tWQay4v@SCoz%I^H@o+*~K$!@Sg}8xVi1q6c z0TfTJ7Wj58S?GvaIs!u!GNE*tv40c`pk8#CtI~Pc(+rK%a2KiM$OJLKqr;$*iUr^< zDQ254D1=N%UB)4hqjcRDV+FAtqS%P9jI$Su7knY99d-DEYPso>I1wU{Nl2H8 zdTI*kOUP0e8MPa6G_ApzEYniG&3bi|@q`B5EbR-?Ob;zWL1aScGD0(m!&${0tfu`A z6>pAPF}AL;p}tFLlvGC+b&8ALC&ADIMn8Hx|}K!&QoXv|>4c2U9I{y_n&6umsE3RJN{#@lEk z(8ePi58=qfugf_6a6gOkjZ#yMH1S9-G<3soztoAR8lzA_0yhd#G6^PxaM}yqg-mc= zCMWT!ajVl7dTx!b0t}&C!IuL0?OGKk zf;7|sk%myK_}d&3B;}Ba@ftGQD&PbN1+jCj%?4ZlyAyVWw4Kd)^{E2&r9X zUma0=2`d+eH1r~5@?u@4q~s7v6*;Z!9^#0cq*Ah!sF&O=nBa&UOVIs6%Gr!-RG)xe zh)iCj%jCkTVW^Bad&8s_EVv{6kkjEHvZ+*+Ob!dfx{Ua^vf^-3J{YV!Y2i$6 zK!sw1j4MbbI6CC51o^C1EO5>Ns1KejR)=Mrh=@&ufPBd0PFWt>O zYFyUfafymLvb9*ck}8A*A(-*dPSpi@k%>>2QM3xDdAL%L8hTwyeS{48VJRCYd{nqU z##%#6#ri|C>2AamkO!G~58IRGl0%^x76v&MRNxV_RB7lh0vQL?)4us~&TbFv1YdK2NrAFXTcdm@ZSSWMg8d&dK?Dq>ZWd zjF4o4V+TzUAg;xWm2e9)0o3q_I7W&2tY4yLOO znt>2x;?!ldGSUDw&Nzy7F$7GrXunxW4M>MC(WAl*gqCXsiL1pGaRfs!GC_11u2U8n ztwi-HUaKYRoFhU9XtMk@tF=aC7l%M9goDcBXzRyZZmwWvT1 zhl7#Q4rR*;N6cN}$wEXSGxc_WD?)c5lk>Vv3XM0qsc4*U_x)*{P=ljThlo@HVmVBB z;AW{pxms)m?bHG-$bn4m&}9n#W+E@UB3U#?Hs!>qUlqc&QO-N`_oGgpiw%c;j!x7) zojwEIj!Yc7Or5GUg6W{@MO%?-0<0nHptLniI|wJ~=d_j@FV)?RPM53GJai73+^);e z1i|#9h{{Fec5SSY1d>I9c~%Gpof%EWz~oxvLSuU9Gc?W?Oo-C?4hn8}M z_{K0IQydQ6icFrZ%hdf*)kAP8SAysZe2#aF2);a0J;hF=8>kP(0rXOsRls>LxWs}hn|H@Zqa2hl2*Euw?}GXf-Th}QKH)o z`9w`2V8TyR6`w;XN`bh)>McUgL?+MDWvU!jqunLUA8`$`>=?@r@&khOXFMKKaP@OS z-j(N5>6Vt|Wat^l5Awndc%Qrl2{zK}~T4JZ!9lVwzfq zZbl~0&}CeCp`dt5{kp~l;HHOdfG9t(bW^lQzCB7cP`C&x{rMQq=Y^r4KqfcqGVP(G zL!o>|i_!IAu3SkMlMWON&wANGhl=@Vxr|17ZWdKbUg+t_d=D>y33 z(NZMDdL3!sFyAEyZj$Q6ML$tW_FTRM^fYAhbX~?X5S;@d9OqhcHj=?$l@qIqi&f)j ztk>$+uz(OJY62zrV>omZGI^RVM}M0Q=uJw z|Nl&V|9?N&`H#V#zYli%D%j^MV257=d;2oj)fd2i{wKjsJ`48nX|Q`wf_-}o?AXI# zuO70xOSiO@zbd8&ElYu$V#%#QuV?#y}60~ha~BCzr9 zKcKC@yR0C%Z;HUiO2Wqa~4MJoujDFPd-(1W`5)u$B%Jp+ODr`lP|T0zjABCxSSwzD#}fGv(K+P#44O74Ej$!?(yCDSjwuJpCaY$gl+lWh5d6W~EXqirnrk6*eOcw|lL_Hy}R zQR;Sqg+F~&MjA^F02!-6)J>Xt2N{@xf2o;!@eV`EKjS=HjfGn>amA3XUB4jm5| zJT}hPE#tZ5F}E9ywy_O3eo1G^V`j6j=#6e(I0x{&6g1k#rob|uO10Z-siR!K+N~C< zjcRWsU3dY|cOK}ou?4WK?^0TZu6*J7K#x8X*jVUW)-yAXZ!@~o#){sus>S1YHY3h- z96wLT1DbB*zQZz}={Rm)crMVP4;veMh~w8n#xOoQY`|pkbi>FtQXU-t+PCC4vx$|b zMt+)MWEbnkv1~@`W1#9^I*uQK^+|C6OM_Mk?u`vnD0_ z^=!k-dbi*?0@531l{Dm~H^z zQr8-QTq8#vsi(csE?OONNNETcVufm1ttf>OLQ3TX7ppSGrXvMxi;frW(u76nBgkHxi7Fae5+sECLNiUNv=m{2i*=;I+~RQ$o` zf2(Jft*Pnhu387!_r4v!Z@*)6>fCeBJ?Gq8w@y&-eOfj@%WgY(-N7qkY-oSD74g2Z zH5|PnSOwPxviG0ff|XXXTeg*>06&7QD7ZwB%}+O5ITDN~*ouNH1KHz=ZRIUs9Klu; zTnxw_N4!BS*Z+HJV7fkj)9|B%PwnR~iU-(JDgjW<#JZ=N$%cBLRW*c6YBeas(XCXV zMZK}rOqx{87POM?XscSPr*RMDlkA~FJ&^vT_M~pQQ>-MPt3-1ZP^{$q<(g#sP|aiw z-3tLbuT3=*lip|)R-tQszN|~wQbSPyBBA%vUzT#&BbP*KTAsjgalXIG) zad@ArnY4^0G>d?i-6>nL)I@?H<~OH{6N!k!v@KQEK^nYP#e4;`AKR9I(P%W3Y@}^1 z&YYzgh)X0BK36(vC_{|H>>*6SxGNjs)izKrP!M4?S5C%rQK2m5`Nsz1(Zxy`fEoi( zJiX4`nL4Oua)ql63Ce!;{NToZI$EXdMvF<%u}TS~b}Z&g5s)^Nivwuvn2`C=*>(UPvhZ^XiosGHF2BNH9WIgQT+hdNatGoe^sm zW{nMsD9+nrrbLV6OL)NR$Bb}LZ!NgoiRBufg8Bz5)lyW@`}2>@TJavvE{ZkgV{hG@+4|5gDnv&>xghU&1?P9Z6N1_ ziR5i?J{zjIV`bU~ffR&n*%UI(*K7Q?LNFEc<5YHDU#g`c9?#n#Sq8Z>^-{Eu34)vJ z#d0NBi3&Vz(K%C2G*GD&(bxc&jbxj9f!SS-YNh>%HcQfhbqqx+JH>fzL z^dv7e$eqiNV+;5l#2>q`2{ zI!tda@4H`!lTL@;+qA&jkfa@?loFVo^b@{BTK|aKaOxn1xc5eGFEhy^n@yv>4Ru zZqUv^ICbPlb-`s{LUM{cl^y` zh0%vb4}-*=W}^LTW- zJ~uG2!sCSCmz0E7tgyIz;oYA_fwwE*&c8Rw=5SNHCXwQ(l1$0fQtTUm-LduU-w=4b zc++}3N~1J(Y;9Ks9#`4Af?$mm9=q6jUw%8b-r6Mad%+vm<5wDjuwzTRBJjJ)))fSM ztne$c^}c*|Y`wNy;Io0s=JSMhS>RLT?Nv&GMOOG+W$S%8?bv$xMuF4H7iDw0sa=w2 zsiOQ>g<9IN^}_E29*b|2&EwJSqSV$EgrBUixMJ&l#VGKW?d+L3ST=`S+XaaoO)80A zSz)n@9re9an-usBAKLl%(slSrzUoi^2k&({7>%JYW(aWvvq{NOUlzcW>VR2u4(}cjO z^}^1-KU+tl-F#C*vg)Hs?u1vbdf%&#j&C|MCh&OX57yyPE4>y3z9}w|>4=gWuJvWw zm*0+WvUJX(r%$ZMuk>bS61@LU4cs&^yL(1I_2^_`Vn~%8{@&2osAb@$u@5T!*!(Ks zUlJKTcPV_#&^i+`SR`MPNITTeg=cd}uPi!GExS}GAj61A|7HoENbItg{g(00Z^J%%vC z#odfG;nz}`FevW^#%hw@&RBr9Ar)W%4-SWiMhrf$tH;@CkT0X2biZDr3=uAilq@luX)&kGMPm8|uSKoJ6As=Kq-c?pztm64(hIo# zO3JAX(q$WkvS}&-nlY)pHp+vi8)aWB-$j>gNsGfmo5`##hZjTIhPhsHGj!6Qc9RA= z60(#r9ZlM>yq))pJ;pu#q$~~o-JhiFvTu--?E!57!J|yF(qPd@Cfsru=vFts43*q! zHlK}@u&m8rb0RSf?1W9q?r4^igfWzGdeRj`#Ua+^doKx&zHKRZ%;0*v-)pt` zEpJH5Hoxq88*d;5%2>uDXr|uT+z>~~ujO0ey09@~VDlC>UqdaFr82@M8)&M_k(O1* z*$f#x4b_8bN`rN9@oZR|$%V2Gb;O5fFn1;f*<&eRy2BIpxpAha2C#=jWL zv87Nc?+=EPM9LO_W$!zypCU_dn)W9}rZ!BG(|WRHC89Y;(UsI|7*N*HiTJy@3$r>{ z&a(A-AxX46XduHzl5{?t%EB3EB&#uKQkbu)V{$pFA>R7#?k8nwB>es)Wpbkq7@!nl zK&>HM9H}E2D$Uz9+DMA-B4ztR#Fd7O*>Hw!ghKg>+D#MTxURBTcO&*dl`zwcWwDmi zFPI8qck#u3QkK2}?pIRYsIAYQPx0D_k8TE<;6UD@vV|n$RdL+FCJK=sMmwyB_1Lo*D?aRfoBFk#SYXmYUI6cr+05;84!w zb>;nZCh4yPS|H%RKz-(svi7ETU?c+y|?i_wWb>843;Kv?+mmYN4N#o$f zeDg8z$U27;-G016Epnq)2$W03nv~kp@)rXcN1Z~*Y{U`nVh1*JQOhB*#fYxNnR1?z znnOuA9Zy%_bR=xb`$O(H836T^!P^m(w}Q>rW4HM+&npKNceIq*G{1?8R!%z&DuYJn zr=5nftb3dKRJ~rfqx$Y)U`2^Mjj&}!fY6sGeaQVwy^Bmy~PugT5t25WLc zzcty9UzM60_Ns(f7J|ugGsMPA9++ym&;sw|yT0~ADJGOGJKZH~ERu9B(m5;B&=Gje zfrS!=6lg&Zj9XhlG@UO&dBW*ut=0hHp=GbhN52Wyjesm0TaRO2hL3Hz;OK*$9g9`kfy6`^n|Iz?vM_ zZ%y{&E6XOeQDQLV8W&=3Mdr0}dD~o_NIkw-L?8i42%?-P;KmrT{gU>85 zUk;@6>QGh_3vk``6NriHF&n4Jdt;@N-%z$25pZ6E)tW`PsW%2a4C)Egi;ycHbU=Bh z%Sq!dgk?F|zAHTo)?{a|27&v|grZO35ayi75YG?ST3F_En~il7k##zPJabD)&$HE{}i{HA_uvLD}u zG|7$H;}LK}n&UB!(-1yw!AW~H1efW2(rm8&s09^qqnWZoP$m|38G;LSl6R#@otrh( zomxvSLB+9X3L`@cO>nb^1z5~SLUzb6drj~-Sd(M=t;v3TchV#`YCmyU;b^*S^z-I= z$R1!Q4l;7;ZhO3qM|mt2_hobe))WW?BOJ)ui$H}%eL5Yp_;lrld(lAVim18;-WgFG zb>greLS(PWyis`nUl{oOz}&5Kp4pdXKQc?s{B7ogGwAf6rgu-5whSAgiEOE!z%v$QEUgRgm+;wnH7Vh5kh@ zmE`HL?O=y&zJJ+1CFwM5`@9a>oD8xG%0;s6#SYo546+ImRoM31J7hC5$STMyVcX|+ z$jYd?siaW^+dij5HYI~w1&JGM`)wVvNf`>Jp#1~e4s^&S`d7A5k`p1k|8E`m+`!z; zbJXl#XWO&3ndfHSGh?3q#q{OV3sc{pI)Cce$%iKklZQ>*I}x3j9{=okV0>`wmND1p z-$rj3wU7LE)7;a?740qO(%Ky`uYxS?+j6^9NV{K{Z#a0U>575;dzqeYK!Drvk8 zN=s-B#()0$kVoHf>v{i}`qeMFd=-B36F%Pkw?2`F54=#ye`mMmH}~cr zNZx(z3q084Ix?1jKeJ5upX__Sp1ksHbB})Km?t9j;>evJn|z8n;?IBk@~`>ZzEJ*l ziwC=$Kv=;I%`)NZP9J~pGmjlK{p}w;$^GEQ?YFYNrynXk_0o^7`NI*om-*Qu5B52M zu!1|9Wx~N-qc44exaav}rlLEkKkq_zIgoSiy5?t>e~%gd$zMlroa4bxClFR}OS4S4 z^d>TN>%lv2oxFMXGv`K}b7$>564k^X4V=E?(zn@eco3-D+dG66+|w)*e)e?j(Faq1 zeeWSp9{KFYe>CyC%nd(&qI~m*Z@d4#b1zTAv$ykLw-Y#4a8t8P_)m`vUGdy)KfGd= zeI&hL`oWX2OJDkbKP_H%R_vwrV}{VRm-1l069_A~t63)e#eYV3p7;2Xzn?zDI{Wi~ z+~gX(_pejP3Fo4h7=MoI{wut|gB?#Gtl+k0nJ|9y6Waem?x~)7^$`cZc+#&AKIpX1 z?!5M=H(WnOzWk5uM{hcV2Ya4CSiybGGU0EGoICsVFWh+T&yUsq@R%bD-)zCr7tS=@ z!X9zQ%||p(o;ZU=_$%MN?d1pF@~t1+uYT|?k8hqh{o{Y( z+ujag1-CZKgs*8tXWYO2OKbAV!4G@`dGiOYXC4yVfj>8Y#*a_C?h^-Ja~a>}JA@VV zja??Z37Wp=%W3qMliz*%<%jUqnGat#ea5c4XRF`5{-s|$F*);YzRh(AE4aB?Cj8(d z5C8U*?RSr!TpfJd*;^s)t+i)90L}dFyU4aH-+%Ir#~j7C*$!a^cQ?y~Pd?1pgQ>{C1{Z;LZ)318?G5E;m z-0vRN4V?15r=DwobA@}Eu>2my%Y={p`e7&h#&{PTJb3e;kND-3vwFnv2a0#8{`mYy zXAeDo{&Aqkt`1=Zw>Zm$Z#ZZ0#-|@3>|bf$^}@g*cLrmwPk(ubcHrWRFZGU1PQ3hQ zzD;!qE4as5CY(F`H-EXP@Lq4~y!U+~y6Gohcx33t;d4IyuTQylu)7Yv@u*FFo9qx) zaFer4_?m-%`@H=>o*LZdcYXh*&4*re%1fnNGm}o=eRscfHEErHnQuFn3Cq9BStk76 z$1iC8;l6wC-}IZ0|NiesJ(nB%-Hnr{LQg~YT|D&FD+|SE`8LrZtl&0hneanHzy8*} zhmW#p^KTBa z{CILJvGuSYo(hCIyyL0A|M!!P}8rS z-TTMWZ+`LAM-4yw$K7A#+enA7f?J(s!cTql{U7{7{=Ls!@_6pi>;CJ*?|$*byFPa7 z!S1?1zU9HcKgB(;{pY{+z2{#$-}sGp zUii;%e&NGk_~bA6Hryes;AUr;@afzC<7MB&FDK6a+0LoTP1Ik0ec{(XG5WQeuDIoI zgKzkL_E+EM+x8A&1$R5kg#R#Vyg%f^cOG%__R1$uIQD~=e&Vch_CpW-V86bKUYu64E2U}Z@pLy3Ld3)=pp3AF$IQ@j`c{lKFs6$vmgVANe zPw)Cu`TX}hQ(AcXZxdhJs@rwLpWWDl58nTb=2`8I*e8uw^6j%bgcaQJEEE2h?|nZ_ zd}`O19@u)N_q#Km<9~GA*so#e%Da5mo%^unbMCx3^V~tR z&(7XAd;M%-7MLXJZOtDj2 zr-mn=1T_P%nv6`ob@I@O=O-SRXiroooD*BdUmpMd_-$Z-1Ahl3a6keFByd0i2PAMn z0tX}@lE8^7dT7gXD-W&FV%Dm)sj9VDl0-y}|9lOeCT-8xc`4q{R^yqpYIU{*Q)}ow zDZ$fNEFKp}xux`ex*E@@v6zdYTq7mL`^joN(2BuSi?;GkDc+A)nqUO>yvL?m*-fBFv#%u{T+^IEqkFUl9-80OQ2%Tl5^uD_W&lC?qC7TrQvDJ7+ z(2{~~6tg)g-gj2x88l{tClU5VrFf67#?xxdx+KT>98$b*ug248%-TTG<5{zAkF3U1 zYs?zf(OeS=IR33Qc&t%b)Z6o(M0ORH)-wL!YAl1!YlWHBvIAJ(SUsjeV={UPPqMX&np!>n_0@QKjmZ$N zme%V01FP|L8k64DK&oqO_iL;1v>KDnS8kM1>9}8AgQu;zYoXQi0Q}v*8c(A!X&^cf zTr-dRR^zEPCN*2J7o@$a#=o)(&uG>d%~e~-Pe}Q@cQu|#V>D$GOcs~oeR(yWQDZb> z)`Gh!#k*$>o&mL|69p;WmsaB$G)8^SPh{8lzPnfB=`}_j2BE%)l-?ItBb245>*VyjoSL3O{u2ins!)t8! zb8GO7^+uw$#z%g36`sMOF&MHi7hR*LcdWtFJ6e@UOe&Yptj05I47v)aD6~coZeN3^ z4ONhWv}@}4r-kqT!-H=gn109jTZTUjfCGOk5&$hq9D7=nD7bd%A55~UMM;mwdHQZG zN)&|62#qn6G#u^VD`=S7y+w(FaG`FEht?Jo71$E@66;$ERe0dvZvfbZ5zveXT&lgc zElNygaQ*@l>e?%)6r$o40!L9z%7n&UMvsSS`C)U(9(VeSB_~h1gDxAWOO|b7``n@= zVa(X`u5emgve?jq)!;2_JvzM5bW*tlSDTM!h`gy~WlP)i#zI6_jJ8~Px2eX3VV14q z8Qzlxd{wPsIBP0p2yLjAb64f|3KFIcT9jN~X~TjczOy>Gu`P<_8lvuhr6DTu6z2jB zlk^JOTW$JvGQd|D5SoA$s-aABx#88GW}14y^Q+ey$Yj!*h2~LR?T#v;5ehWsafItJ zug3+tW*QI^qqSsFUCqTsY951~^}g`yqp;kkqm%IstjGAd;Xt3*v6lsT$21nykmx3{s$;K&qIj+d!ux z8O=0>sRO&)nQq$!yL)$daAR9`%e6agH@j01iMcTwn$jInVn^a?=&zF{Rm;)Ra@WXp zDmATXS`JK3qgHoRMR;~G5{(6`8KE<-&|o%N16^{L39gi@UTMl_5dQ1RPpjtLA-DIx zb=_ORfO|2*!CXF<1T8KXgPCMd7)wlhxyN7UM}fy>ek4qG8cvFS>pUL{=Jt?EC&TGI zFxb+AVPSS1q-3s^?DVSa2w;!9(SdcX&X{e@d|~k1|P8)m&K-@q7 zcARxNKM_6mT>6dUhJh{ds)2TRtMM}Gj)wH{g^DeJ`JE2LpCEB}qr@2-XrhGhkz!K4 zj+W`AwL#?KZ4SE}Ox?+(8v)pcleJVzdJiw0YrPanG7dE0vT_iHaD)%GGds3k&UL16 z*!aM}mPICqc`ZoYh8Y4pg(7Jl;>}ra%1$B)KcUx0^JTB6#i~>F_&TiVR+r1tTIAww zjv_Ezarii2HDJfG>1a`Ut(A!eYtbFH%h?r?pE0ndQCI63Q^Le}c`xWd@xye zB4=Ih*=quJU%>BE>%%60+TjS%ymzsZYFJyiTCFcwG_8!KTnwi3mI|B=%Vkh5UjK;* z>%m(Ij`yR89W>7HI7ytq2ntv+y>|)=R&_0)<1&$KiO50(s19UQt80;>o6R&Gp{Uav zbRr(!Yr}c-daOySlgY&c$^Hco=?n)DU<$^js^)KTftOCwBF}IIH zXcV&fFdG4?;5xw+@V!$|kbf4CO;zPAg+Yd|(Rj-YF=EykBn$GLIQr zDW1G3I08=6OZXg4tAm3)0hiZH+Q1a-d#9ivVt&0T$UJ7OX(!tM21!^2gmOE1H^~G@ zoB=gkA(-O@zhv7x1qBzy>rFv=e2|MLZwlRRYL?^wFIF1=fA;j@Q-7R#XzIqP#*};N zq^W_)Cnj&7+&OvPq&8vGH5RFWx_|4GwU1KmrFOa6keF zByd0i2PE)%NI-~F+}x_#iqU-Dm5X`P{t_K&NaGYIAene)%fvfNCf=Dc@y?Km_g0yB zr_01UO(vd2CZ1U)o@p7c>~kkDcPr}iIcwEmt3^v*CC7a-@w_tec$s*dOgvU59wQUa zBNLC7iRWI%E7Ew(Ve@1d*zP7W^qP3?>0@Q$y;&yS5i;=(mx*_nOuRSA#CxMmyhCN; z9U>F&;AOl#OG62y4w|=eu#KpM*5vp!Wa8Cj;?-p0Rb}E;Wa5=&;+16L6=mWTWa8yz z;^k!GWo6=JWa6b|;-zHbC1v6zWa7n_c;dw23(nS zCW8sgxog1HHk%>g+?Mc{GT}nTpUdfpBI!%b+wmmMyBMEt+qN23@23IBh+oANcvD}?IpiRsk>w6ifIT2_yvU2&O?`uf`W3h=jjrkEZmTiG)F1oIOq$n!&mQM3f;8qzt>05Siu! zbZy>XkJ_@J$z7!2s~HUeBx214D5x$cg}^LK9Vq)1Xd4xj{pzDY3IW8$ELDA~LAdQ1 z2t+HUaicnA)Z5b9dbCnu5;bOt`*)xx;3Zk7Y_~#1SN#Fot7VKT8!CJAtfQ{AQNNp{6>Mp^9v==wQ=&UqlxV z*C^N!!7TO^ovQ0=X`8j27u02o%WQz4g{X^TdA}#qbk-JI-c(Q%^!Zp#E*A8ZG{H3I z)fK_SyKi9%%0lm34qY(mD%b0Ye4v<0gj20hOImzDm`C63Oe2j1=-8xaJL?+f-z|qu zBZU6Hik7I>YJ^ir*KMbscQNHk#hiB)$Z9#{NmabC*<5y)t4YS6^FS>mD9{-wYsy5MB$(9S|IEEGANGpI+r3gcAD}%)UVy2RHS?B96F3$U_Fs9L~k#NxNC#||P=3|<) ziw1_8uN#78-mjAv=WAY>dMR4S1jEttVka*`;Ax8zEfZ|n(?&YBtQk~N!M^{e$yK&& zQVUMGt1TP4*|L%>H$Yf%KCIGo);00}3Q~u=+48Fz|1bSwF124-$I)&d#sxw>q@nF-W_Y$s-?O**wA=O zmYB7?&wjyS_G2!6x$N=T4OSf+cEhB>nluLkY`Ij;Su0S|9rYr{D9AVTnc0vn8Pa&e zq_!Ha5wP9tZ;)8VU(vd>E_1+T#G7H1@bdP!oE^F|bzs$#Lth$j3_LwB^YYAhXFfJ_ z{>c5~j`6pQ{c-H;W4p)lWA?ElMt?v1KeOMPy=C^ofu|?R;G6yjCh^JY^l?)!PJMgo zLsRuB=hWuOzfHU_dCbI7<9{B1aN^;4ITP zWtbis+8n6`axpxgv!@%GQpxV)(vlarsn1M(W*If=u8=Ob+ZA_(tr$rK3et)(6Y+_- zT-4Y^OfG74A}SX(G7*uB8lDKtMGZ}abG{>f*$rT-3KuynXn&f%$%F zvhv?os1hYtG3g41@Gu$2@Kjw|GHCE;gFlmt`sCn~a#4Re_*1#4KNk5*7|H_OG7i5gQ6x)NA=O?{=|h2aIcs9T4(%4Oh}gTIuE`isF|$VL76 z;Lk^%AJ`#NU0xlu6<)obNWIc5qP1!x;HV`iM&gJjj+i(?F6!YEhcBZ>sy3n;tdUkG z<_WP5cUgL%AKWy!33$~Xy*g8SU8%|y)?pKetq7~{UU1vgZE{gRG4%<#OK{@giE>d- z7(78P>hXie%SGKXxJ54Naf8RnMLl-#Sh=X12RARHM*Dh~`QdrFsHY5{A{X_f;gjT| zo;bX+TK#!FVR+AK_0_D;PJMQnf&K@QJErcCi~6~#&&i$E$-^hhMcp#IqAvXJ%T$XF3l=Cqa!k4D#biP{Tpt$5bFyowY z%0(q+2)U@(3?>&9nL)r3ylQ6H-xJwg^;1)yl8gGusZTDWMxEu7 z70#8=JgBBa#nTS2WVKY7YDKR7QBf5tmnxz{v8}^^f}ATQsZDYiI8`wNY56^2#yaXpJW?+Mt;G_-%3fd@b^b(w?n1Kymf)f-o zu)#}k{4xXboy$fq!4|~~Z155srvg3@9MF(MzydF#{XC1e-bxD99e(vcU`- z(_uhC-tQI#&A9pp>TJK`XvGX{Xr$2gJC0J!fP$`JawKqMhXDo6)3$7AgyQx)-lCX+ z4PJsbD`sGWm*9vF0}3>IqnF@t#SCol5*((Ofel`QH+2|LKy;&*;EjqI*x)5Nw8MY` z+uqoi$?bO>(qTYBY?_kjuKsas+wVA7F#`(1(fTtWy#H?+@D9us=2W2P-|;j5Idkgt z2c|Vs-2HX?4Hn#e|X$D_N_7B=tHCGk!way9)92OR@MC~YUtjf69&IBxNYF8 zfWYd%#4h*P5WQ_CIxzd5{xy(RO2!Ih78Xm@Oth?|C3pKqJCR z=ttWMh_rxx2d1VG>-ywTQk)Y4)I`Vkv&VhQ}$@RmsVOo zOi5$;$S#M#j>$Yy87(iF6r-*MzF7>ol{C^~z&Y)k8+LJB(2Z&7e313ZBp@CQ75 zboMgY{E75Ufpu?Da0?35#kNOXmikv3lj8~2)N0$Jp#EE7mvwm=2WBso%}$qPY?xcP zu~}HC#{Go|4Jvus8dkEH(Hd}r)gIM(Jk13>+sww%hB~neT3(rf*-K;(DOwrAX zEm_&9GnY-oLB3BU69m1lsujP3;xxs$53i|n*0LTf?OA)4_F~zi_4EW~ve{Yj1en!0fC856t$UX2!dHuT1^lFToetRP=<5Zt*@;)9<;r3( z45H@CTNHdbDC{~74AwaWoG*K@E~c-*D<^hYz>uBec1!km0WmvPbJ}1Jas~v$JCQ3NA6hv<-WvUF(0HCTla3 z)*jrV;Qq6)OApxTXmnLJJ6$wd!F^w1mktcs(ddfoAw?Rk;9e`WOAChTIES+Ap;l?M zf*TyLw3g4|q+&wV$p7A4wuK~dSnm>?_MD58(QlQKIWiH7_ z5>hz3 zvv@iGe4Hp^ZWEL!G_s7D2F(-frb?VqJ2gHn8aCzelC4?AT23;8=Bz1KE(yC$xHV}E zQw47|X7k02^Yh#EEz5k#1IK0NDGE~;vJ6Msq+r!o9R{)tgWgci6se`Fj7^<2xiZyg zDzzBTRf%As$!2uwuq)@ckc`EwR&3Sl6r}*^huyLaUu~;aZ~@-ks+GD0Nzse7d$nLO zuS1%D7e+v;>TBM0(`?kNpD^t9>k!7G%g>VroUSB%0W6d*%-0A<+~z}Iv%e6^)_J|g zqt)+wVjw$@ZPS`s9y*e$;7v3^l(VH+lj3lY>}LrkwApAVPtoAe7#y~HuTySmB|>fX2dS9PK7XaV2GgWrc*FPq3fms?bIs ztu#GUq~dJx=AsT)7c}a9?{@1BF-AR8wEM9{QmqgAqp4zz$oMRn5vxG0EM_D;ep}q1 zBIbRV*QS{VWsOSnVNZ-eUS29DGI%TzOvC$_0y=0Aj-{XQ)8)my!-BM?8a<3 zqo2=NbBr6N*s9yEPBqG~ZXQYN{Z%kTVp}Dh$j>7&q>h`y4T~jOG(pxv$X2(R%qBcJ*S^LY3|6mgXV^3Uz~k@_LsBYpM7Zdp4r=HKLVl#uA04g zwlbTTJ!jTEYo9gGZk^pUd+6*q$R_yX%ri4j%seu4-^}M`ZkcJ%Tr;yY)0oN3oHxVH zpfhisQO|6dIecb%W&m^^e0KWD>BpuYn7(WJw&@$EubtjGeZh2LIx_8>c21u?t)D(= z`pD^nriVdp!t+zVocccKK5);}?Nc9_x_;`asf(v7Q;Df_KraIOlzD3F)TXIJr^Y8= zp8VtFGoUlUBa`<{es1!X$@b(mlS`A0$qeXIz)qr*Z=F<6ZkarMa(Z$AWI8-M@#Mr~ z6Aw(>HF4X-jT6^S?3}m&^e~7__$HhaXHV!SPMSD!;-HCP(9z)e@n4RAfBd2Gd&X}c z|H%0D<5!JeJYE@3jGr^^9=DI1$G47e8b5S=eC*}1KaM>!_QcpDWA}}HZtRw^_SiLJ zOJj|(3=sH%zXK9DAb|rCI3R)l(-N3cjSLND-}T%z10zc!_!1GkLj-RZ!7ULS5W!v% z%!*)I1XCiI5WyA^Y!bmb5v&oxTSf3GBKSlRe1Zr*P6Tff!N-W;H;dpSMDUwL@F61j zU=chgf@ehVvzbJzLDuQ1S!G99Le-y#L z6T#1m;QtoE&x+t*i{NKO@KYlAmm>IQBKS!W{9_UPLlOKv5&W14{C2p0K`VUgb$zF34K@*Be!h<|75y&qpTi5H-?xh3lSJ_GBKVCWSmXnT-yr&Z zRs>Ir;7JiYE`mozuu24r{GRGxdw*An{GRG1(eEO^r~13-cah&y{YCWqpG5Ekzl(gG>L;S#MSf2u@_VWuh;SYk!6F~1dQ|lL zBO>^lB3R@PRo@W({&f*7@{_9jMZe!Gg1;<+MZQt>S<&wz->CYO==a-1u*f&6ZV~9<82$9zgQK>&cg=BgN6!9g_T#g~S@X=lX1+0V^$b6A)bumcpO`KU zJwNo>q1F&I_?@Bg!E*<%8=V~f-N<)Gt{+((~ei9&#dngbBDI)rq|xG)pxHW?TW@!#EgLbwaNPO;V}C zsMG6#oE+sSRSTv7lb=8)p0!^T(blyN+PI-3!tiFdp;;# zbDP!8*z6pMc~zTxJ}5N^8%aYxC+nhZs!cr~lx~R*p2I00?%)yCF#`ibgFO`Ez<>I` zE`c7JV^&Lb)$Q@4Uat+c*;K}!4@#|r#~p~z%KCf`m7(W@Qe6c{z5wJxan7aE_k2)l z9h8H$JH0rKdsMm}51K4ehvS63sMSmP-Ly*E<3Z4sK;q!s4w@n%&W_tunjR0DEK*-( z4UkTVcj73nQulmN>WMLmw!3(UwX&*hy&g15*AaK(KASJ#@gS-<^n6e%RffZ?UK@tc zyy~Ey4@%dO2M*bTI|DGHn(O(XRH_if`R#TW2e$p0UK@bak$Wf-vk??&C3C&TGf0=8 z@IekAMtZ$&)ojlPrHg@ZkdN{LtwU7PJs*@VKkcC%ZjK4~VAWL52c^eFo+A7Z2_qb> zn(X7zmue2@#$m+9;#RLp zCB*+v4a5g#|8wS)sjp13m3%Q zGIcqf5Wz5Z9CRUq8h&k~=GQvZL9eBXAsx6JL@cd{J+F7!p}H*v)k&5r1Q5O&K{DB( zmv3T7B@?Cdb1a!5-hLP(+(*3m|nJzgiF&KTOx7^ue(M!2|}u_pXl zDihX`!dOkx+ZhYcHlzX!;K5)EZ8g*7u$nge)tS1x5yTckRg^ci^aiw^2ckav6;Xe49Z~C~ChBmy zSQ@w6aDG1$b&J|4Q6upx?~Pb}NevT5GmJBnDF?IlE~2*iq8bw)ZRE*?c%@h~>MfNR z1seV=`bY?MJL&{MlCU;mChe`bAnLPT5%t6CiCXH^0<r!GYy5o-t)h zSx6A|nXicYq4h*9-3ACh=Hmix48`^h%_)_PVuD(*+HXK3Xa1&5=aaZ1RMX<_fQ+oMvr}(>8J)QJ?XOs2^NU)Y6U9kKjCy z25dOE!`N?pHE7ziL83+-2#r{DY(`&+IvjyeCXgny>Mo+TdpNYJra81oFvc(jBMTla zr>h|lN!K)4BJ0&hOmxl}%9ZQ1pw(}EMbzI|Pt?+F(dYFt41&@&Vfg)JTWoFEwulrQ z9*vd8(_E8JRWx)a;E$!CE~18fh^HR#WwWG@Xz1OD(U4GwO1Uf)Q>Tj%8ltsc17V7W zEmf$a)u+EA>aVXSYU$yOhH#G+;Ypi+zY_IE`Dz=bbMXc*)AAS@xSV1Uy3FYGT|{kX z7Bp%il_YeoEo z$8Pgup8ZPH8|ABED~2^GwWsAT1~QI1g^<~ZqpPnr7quJ`Ta4&RoGIrisX3H{)A4i# zPDeoQoj>G`lL57zFGk^xR$E>X_1D%B^_oH^;Jz4f(If&n_9Ia@T^ps<4ouIpo+76W z#6W9Cm|_f8rmLu-u)$<%rV;^I=PZS+TC`Q6gSkk}N+&t0mP3L?94E9IxB-PbT5Wzs z)L&gs)Y6x2!s%zN)&SwzpSH!OYooLpu`C3WAT|ng|xx zMC>eDwP9%76Ke&-8CN`NC^_@$rpX)RBA!4Hs}@Q(sQZr1vCdDp014v%9YSk6&{;Qfi0_5;I>TOX{DPxyBX`BxZu#)YmXE)1uL6 z1c%n$-Bk3hx}VostDHLLDssgZVzwpYnp{pzur)7IiKx|OKm+&;S-zHF`=lVMjj z4m(X+uISaq_+l&KDaUeA+Ue%QTp?@M#~G}y&e}mDxXzl;&&S>1>!>GI)6`2Ad!ycL zBwUEr6pCRWCL~_;$;$)pOkJqIP;0w8+kA6SNrkeFb2s{KdEiRijj)TZX@{wnP^y()8KD<-Q61I~%=PYtaBsx>{Z{ZS#k$Xa-i}+_qd^>n!-W`Bb2s zo)3AfStC)k+EY>RwH|}Q`)qlpu+QLPij}Ix<@QHng_Msn*&s&&VWSlgjLGN2b>3M< z3^Z0~SnVZODy&f#Oxa2}?ce5r{W>%1j{4^3jm=Cbs&lB_(YU!RuO+%gm^!e$%aU!E zV0l+A4sLAc5xEC!-CE1*VoSnL66;v5H26AMN5>#*(NZ~?&k2U07x?IKlrIVYU$*iJ zN5nm+T0s^27{$M#>}HGJWC9~}Rd!O%(gaj>8lvFjnaFX~I!>nxIfFG01>#K2Uaq#% z`gv#4-U7!+KxAJ|waFr6v-pzEOe8%Yz*3Zpj1oS&S~7%NMMEk>Ma;CLK>0Nx!dqhb zavlKy>>QYWXnJY- z?5UTh?wTr0Z3S8LH%^{2dHBTlC$0h6@8je5jbAWs9(!Tzc96S%(&#gwMnGjw4e9{g zG8>uQH1p)l^&p@BpwSyg&lx>@Y$;AhAtj55B_oR zmcjD|4hC*O_x{Qy>ISL#{lG$=t)PO zd<;N7@t$<#*@eLXEyVyG!#ehEGDyGVI|!GJM?p}R4RbSI+=}vu3tEcyq#>0t?xzqJ z#*j`QZ}+@KU7beB{}cK?5y?d-Ag8YZqi?FptM)<4_871W-^o$_g)qdhw!{ zw?=vsv=r>ci(dMAOFQih=JN427GkV!!tbF_3|cy`Ck^TPI58{lfowiI>p%qv_G28h zwAhn|)Rv$?z>eZpyWfR+JS@mD@;Kel(%XB|kglw-IIn?9*%&M^b$UY)9TX&>dZQ{b}*40){(v_$o!A=P;wFd8?+xfqnkZC;qdIS%ka_M{=z zcOOF%u#@ozz}ta~aMBLOgF;KrUNkh)xJ43k!gd?RQ9h3JI$)Tgojklm02=F^8%!E` z{!wFwmhhguNNo=#d9R(t9E1l1VptIvWK@9pU_EI_?T*AL8ny)-q{HQ6IhIE4FbOT8 zJ!wdF6>|n0Fh?RJ#rtd^=gjW)c%dbvCk?5t23(XK1ZOjZEnxT9aM(wGz~LpR z7cXk5j&$cmZG@K2?#YXEjcKdTL4&W*b}t9vZi)}M0(NNWte!Na>*M!f7;Lj~J_x6+ zHW!Em4^Yt3nLTMp*T?E~1Zb4-dpRHB2m~;SaN^L?89ix8*T?R{5eMhOc&7`&9dN*b zqAa}hRzPFDH8xtLYtvP(01a9?w-+yK;%0d(!SQ~On+hU@Jx&rQFapTroSrnKHVpJ0 zEDD4AU3Ra-1~Q1^Rt-G zjaeC=JwO8;@%N-5)e#s%Sf2Ed)&NA=tb`u~npmMFUr!oR9l@O706@BYE);Wcs2#&S z5RiwrCk?5NAPh=|*iS0>4y4T=@!^+tZ@OjPaB_IZzTOkx)VgQZx^c~zc zN6M|1=qzcyZ$LiB04089#pP zOJmsR-J|Z2=SJQ({Dt1Z)FqM3O81Plm8Z8XMme^XFBbgnv{S!eyA=cTPrnDE@Fs<8(4Vv!2&m6>Zi-*W~KQ`B)6zROMa#rGd3Vi+=H_5D4mWN^JC^guOzELKINNZFp@ZU=DM_&IPLB5TmW< zwQ5f|*@!_f;m>M-0OnQ%V3y5I7wajaRa2yFQWxWDr`8!{lJ0m_-HM>jg+Pkp6Ku%g z_1b+VOk2{&ch7eWym)SB}1)& zWQ`d&o!mVGMiQ*YD0`$;)}y3X=+y4%m1Q-^9<2AWN_Sf&twO`Qrvz@m)b+BtS+O2} zAdD;+E6$p$9lkP}nmkUV6L@19&)N}X)$5iO;HKB2&B zCRW6&1@54p4223M&PJCQoVJx3-MxHo@k$RON?L#BcaH;Z1h=7)%}p1pF^B86#6q#G z$%kD$*(f>L7_UQu_Jze#DD3AJ1GQ=x1pg*87#ZI^wjye^>=8wxR?zq}xqB3hBv{Qh z*(0s88YO){r*@C5EbFPV2kX78(zBtGwx8kM!vZ&8>I<^DS+N=gT|e`?RbUiBdA7>7 znl8$tpyy|N_YfFSaJlocM-(ZKf-auGPX9l9?;h?rcGZcV_q(c+kQb0ix|_;_q)pqF zdLW!-T-(<25tf5c0V4qr0kns?L!*6=>$G^T+Axs=e0w?X}n5lJ?%~ zw>DP;`tr!-63-maQ!naq3UgZT+*rB#TO*^nfvaQG>*i^ks+`AY1(LB{6o7SU~(AIp# zyfd<`8%*OEH8zz;gV5r9#k?c3#cQT38-`o4--cRg}_FlbrargIke|C4Wi|syp^Bp^%+4=dM2X@GvZ`%Ic zMSJ@f!`*_~_8nV)zV*Sa@7roz`oyKjZsmqSYFpmATN; z?^;QubiLTtXU#U`brCwyM_^ZAnTgQ`%L=N={;&*&XTalfa;LYHp#7xarKf48W{xJU zUdIrJmgGAmO`~n6qOiSU5@FSPP+@m|X(iFCYb|d$)Qba>NWdA1PO2hXrH#QbSI*k` zyq?T6YM{weoX8VtPeIfsgZp`;AOU!$G9xy>Zz)k} zO#ow3z=#4QOLHO8FbrRN)WA1wcO7m4(60P!>$P9 zeQ3s)b3Qh#wY31wgxkwK%O1$qQGY2>vRNmk^UjR#4(iQvEtjIPV$SUoJQNsbSW_@xByd`( z|Nf;!HYXsgi%yZIGb;|LS)U=WA_xe%G&Y?=eqT-x3xR?ZGs-@(l*rHxrq&#Td>R?l z02|Ts7$N5zF~JP5EFK<6xV=mw+RJwVn7*IOvZDlv2VFtQl%l=3kx6C3b{^N z7lWLm=J=8B;bW?&Zhs+dc z(Vdc+lwnoe{18Bc#K6eH(|U3XTiHkjZLo$Z(@X(J+*W(k)5(5C^=gfrK5N5t5rG{B zsaZmD?}wHWX)RMMIUe1xc{tT_vZZ8BtOKH(EF!F;)YP_IDS3UPU*WfYE4eLiUPjW2 z*xV0}EZ3MOKVkdT21?4A)YO4^F+0sB zol!n{anXyy6!App!JkOH?#_GG!VL&qRnirUawDU!Z1m;tKH4lEG2wC3(6y+ zEC2&NJ+={BsGD7@j&#*ASmtsf;~R)qv5Jz8 zr5ni-T5(G0F&h9NlRH{HJ$x*aGP{&qosChlrj$zLK&z)gOBO0sQK49XYUJ@0fn|vu zZE0iNQamsWJ5SFG!@&r|Qz=2g_4cR_>O zU|oX;L^Y>$rc}6GI6xQ#4#Y@Vz^1nTZYklWS_Hf+ zw6!cvGUZtnXwhuG==VXQATym&D}~ZU3Cr*^7>Bo(Cc5gt!xOI8?+?38#-Zg=nJ1bS z*#U}%GtEk3%?m5K){sea>%pZ&r{iZceU=}Z%|x;V*V+h)l~r*}Wt-iGl~0%0aiTvG z(m1?*yp)hK0#X{_UdL!+sD_$uqN6J~Wa|mM&948eV64Sunh6vbRI|-idRWxB0%4g7 z)eRpXCD#<0LQbtTn!ArJCF(Vi0sNK>xq#D|&}ze>ilv$I81G1e&1<=l5L`CW~DoDQu0(rLWYeP-(Gj+YF?f={5aFh#DH-UOYND}4e z`vrMYE~Om0pR!YS5-SwCvS6Tzj4aix=F$X_Mqw}=%)ElBgITCFZKdiu)=vv9U#%NR zlMhmWA+-XY-Tcj^1Rh9qwmw7H8bjoVGX~MkesfAFgPCS9eVmtZN*T6h;bU>{p``>S zmvk^N6I?K&>WDw+>taLDF)HM%GMR8}O;hDXTe3xE(_cyyCi#w}+k?Evk{PAxN>m$F zA-OdO9KKu&GGyOqjF8Es?(PjQzH94)@!rO6x4!Lxr-ug?(T7 zMK{5rMU;k;47w9G5w!6NGVKf+?gcNz{l%vT_P=iwOJDH`#A0E0isoyu(46L)SfcMu z8652g9F^_kHK{~Mb~{s}_WpP&QK!m75pn>(VzDYUB;j^z=x3@*YLY7lP_}GZfZJ23 z3{LG_n4kaOwefu$mma?KZ5Ka$k-PBg7hZYtpHGP6KROPM_m5^r&pLe9;e7|caDeas z(Z03!nZ4QGb9djm`=XtHy@PLmV7t8a*{$a0Uu=Hw=JP@&U(?TnhmSyea(LLhaXC>) z`YnLhdxldn;7S{tWRgk0H>z^_6zj;OD3N$g2x(}9T(~J8?R@{tT{vLNb?}NIJ9wgmz+F1Bh(r+D)i%I8&?PjB&&t z+0@y?xqiz)dL%i=5_UVrMS#dymix%YBlm>nA+>CVfK+#TAR&zBg7|B>xoJ6~QA+L_On7zh86?X1n0m#>CX zAG*XOTMF;-&FEBqBBZ23VkFd>;q{m)*!LS6lWW!W9vAL+L^gHyaIW7gkRJKgIhL^7 zF^>KtV_D7@`H`2chA_sVf8_acbqK?G^3s)KGvQWANi8$^7Qq39@M@RWEoa4PFWpmT zc7}0-ZlR|dI#zVxaPogWZWI`gyd=ajUnF82^hd^Wqj~b;m6Kx}_eZvO)yc{umqR=A zc@pEOKeC;*dGal*;hZQ!ujq<^Rpw-3NQ$LNm~?O2SgX$_=#0waor;M!G#jaRBbz#V zo~+*_kRG{fjwS4NjKluOSe|&U+!@-KhfIv){mAp=Mj>-YXm7r>#yI4UZ0~Bwg!A%6 ztAQj?l=8`xQ?DrYa5l!#R4Y}K7_nGPH^vH-n%cuGkXI_jW-jvK?wJF5>IQ-F$O~6a zj&ZOb8Ox2F{7+U+j&Zyn+1^zrE04S&v@@TVF^={l+gY2J-@F>mQ8N){43)u@=;C~) zV)814&8U($Y8CRJ-vfZ`NbV9Ds+&wkHg)!JuHPJx9{Hv@may9~4)-HtSd?pm`9RZqqh{G~TJ@sjywI7s>6}7Tzg}{ zc;vaErTN+z;~+n>r5jD#=Y;kaYok}r$>TQcha05O$QkD+z^J42JqxzRtXwVx1dIfh z9&}|Tvb}4k?Xy=NjB%77+0M1ocK-dpz1`h72@bvx{^$13Z4cb`z^(Pb_doM7e$FTJ z!}n}nzNL$ER6d#fX+D{|F2`6{<_s%wm~a;DW88OIw2#XxcZyjm#Gijaxvi&4APMG^H|3R*I|}Qn zPB68y^?+}2K9AWQd63t_LXjC_I;2@f9_41B z5qYInU0)7sdl6o)Cx!f-ue{?s!Z|#(7sO$!RHCad0dsFJ+O@;$Mso(G`Cmk#1bqr* zqalIODI`~C#C0fc7A)%-hwEsU>TWfA+EeN4Yv=TOuX~J}FUoJ*-n_MMsZkf@+&{#k z{53DKFPlvNKIaw-%M-L?nGoAKYvvsOQ(g;`H(v{j z9Mc7+xs2UOP6S=+6&QAiL9_?7vP!A%lFqEs#5(qHT6yXmd!|$luZ)eox>Cj`GLuv~ zr9xF3rBT63p*Rd@MUeGXx|Yr}Xu(HE4AS%cfRWUE*0+PYH5D@zS!%jr(JC_Cl2iRz zr&|o}`tCUm+Sr^Ep~<~)!Ft_Yk|MV~H~eD?3TY5QAY>g2yBBs5mj7?gIsJ8yFD|oH41mT}>Ji-6q~r>f)b3C1ddJlZ{)OUZYx89%uRkg87IvSp^M@Bdx$~Z#*-mMj zzS#%Y@f&xZd$WJL`uB@l52gweOjy&>({ms-uICyZjZgq%z+`GcInbtPC9RN*G;T=B zMIpAt@;0=UbWT?yef8adtoDv#PYhJ zsb$)pI;tTqjQIS>N_9IGk5~JUGi>&I*$kX4WJJl9W+m;L^2>h2kjO11#-JY-p>u)o za8ZVQ-AQBB?{EUm^r1cO7W#M-pW}rT5pyr z(`1dItFT4%G`13F>dRHUh>+`4B-hJglBF1U_H-6`8JR&Ese`71Q#PZ%_jC1mDY#hDi zDdl@ud7+wZk5V|)BF7Ahj37s;7rb6>l!6;!YJu$qD%@x{l7r;l>z2NXAu)?BFxDlzF1$=A_(gm=Ps`6WJSxK~kd?#lw@8%LUA)zD_;SI9zdNA7;`ZPD~apBPp zDw@_rYwj$wn@rb|WsU}gWPK%8hfhc>=SlWmoaVfUMGAF?_ zdqa`Kt6>@e6@+y!og!#x3Bs#3fV)n|1t zt27dBA&bf#wvzQpJP~FpHN%oteg*YWmUD=sM`KGYquq>QV;SwH!Z8KCP@Yz_F-hm0 zQ9<*AEHk$4R?8C+BR5vj;&9l>XQ-W@TcU~~@!r@H%N#hfp9S-6v(lDebe8o8aB>RN zOdLuFwLU6#1vlXi>|)(0)w;XO41+il|7wX!Mwkh%=d%5z0jKS8qC*RMnn?6&NKHe_ z1KTiwt~l-I2K@H##FqHau_cxnZDx=TODKQ>!-GAR=Q_OS>l4FnNC}itZQ9eu>dc|* zT@6k?dzpt4M`D@R6-Q!l{Kk!kzdld#uSM(9yT)wBc8aQ?*PBu?V}PWVD3;;rXxNyb zvf3pyS7ExKBXi)hes3XRC8=y#?0O{SXWNqB?0Il`MAsD^NU%&Iox=*8#)PG&!z!20 z`M*z||CFA!rYY9_nrcrFDTkr?Du@Wq6dZRLI6>IGT)n81HdM(Z0Vel6W7&@w68gz| zpAsr7Wd#+t5SVDs@aqFzWU_-QQLN^L9x=o%s-LYi2ji~V?rP8dwxzFPNF*-&^wTy< z;*9ynV@Jtvgi-vluO*V7F0#u=jPd;;uOtAU$(3_pqTi`>6w*mce$GHC7Ue*$<1-Tm z>Mak1If|CU{Mu5FZ--2PXofC>zK3Z^avUgbUdK(cM5>k12+=ORIW{U~ zDG_63_}$nNpIS=9n6>6oBF2}ncoYW4EB1e6W1f#c|GK}lckz?+FZql2T|Bw)dl!Cs z{?&frStp-9dGG#zn18vSJpcH!`#*C0{v}tyLF(x9M;|zP%kpb~xrea-)x%dD{O!T7 zAN=!!+xH2cF4OKdj@ur%?Sb1Kxb1;|JRVp+%&mc7)~gyPx2xG<)8pV-GbOe8ES(oZ zW{?Ei&*)hx-9=gA_`^$y=-a_7Dwr4>mdB-~jp&S<$4kZs3s-2OWr)p;igIZ`@b3{P}BlN~B!wc!H2qN2F1ZzAd=VY$OB>No8Ft^fkchEgx%pd5iRe2Y2OnHYME_dZTkVgHBY&+N{Mph* z^xel5NrY)RKoo|wKnTIR<7n`dQir%c|0^0mA%z2QS`mU?Jq7- zMgR6Vf|nA}zlaW=x0Hy!m$Wda6gI+!0n3ntjjiHK*g@2xPoqbG4%uefM!|b%qmtFL86Vx7%>gn#neV zbk+qsLU{-_u|aOEq|sqO_lj^MC3zz%J<3;-n}4zFxj3RnmA=ymO9_BIqfKhrlGU#z zrf|B)6=&6cmYgNvVG7L#n{QZ2*eS{fq5iZ_r>e9pwMlGJ0Blgrd-kLZrHmXs$>lAu z5$wHcDN(QSRR*#e?e?%Ssuh?fo32q_|Q( zNa^E>t~!&hFJLyR6S_)5eov@Yn`!~!9z zbUGl*U?R;^jmAu@@DnVZR?uNqaSO!Je_cutQ+Lv}N_wf869;y^(Jr_>R-scA(NAP0 zRs&j2SoAutjJrqouOxD`Rdw6|NYhf%pq#!p0;Wtz{!>iOB*)+q>9#$S*~En3uFvoP zCpP}o#-*RRRJ-_h7vFu6y6`s_e)K}=gs+eAF z3I+iRg=hM+#8fZjC;FW3XWG`yAygwVWtK!`$>6yw#6m4`r;MYhhc)=dxK$(!O&;m% zL)NaFgqX(zgn(KCUBW{`Lm?N^`V}c$ZB{@~wp_6`kFGjYTw zspKd#4$o16id4g(9#qSP=PXySg<33v+F4;rBR~$Av6-pX_Qz7(CmSuwa&m??m^QLl;I@d6agXwVE_Vf4s6`^E6^$aC zY?+zjKqw>}*qB)~qm3EN2Ti@_Bw9eaNtsA#EVNkeN@1b45J7FR(P}o5U^m;;Fs#E0 zV%r-|3bS%}MbXZuW_sQoNt%`SP$NVOFVw;j)RuV6vobO=ol(%l?wW9>5Og55o8tjT zEOm9fY6CiA!7iuFE8@b$$PAtM*$uHDK zC@2!AHVAhmw@@2_c;a#lCJ^pQcA+*x`Hwh+AKaD9LT!XvA946SxGT^?ZG@^GaSC*B zSHOkZ2<18A&~k8BfQ8x!^*AoaVB+Aeq!(%<)ZU0wMuWSOTBwasL?aG`26rX7P#d9A z#^o4H8Qhh77HT7u#fU?U!Cm>bh1v)OFyhp*;I1SVY9my#h{I>WUAcRqHbUu&I20D# zl~*j(MyO8_hpB?Q^74h+2(>8Ul$GGFeCtAOgrX9cV^BmCFmY5h^*vDaycI`Id#+2o)LP zqyuwT?pmmgP+B2QLfhi}{~*}7^q(&M)TP-a>e3w-|Hs8&yZGou&Uidc` z-gcpVA$?)*>f!iLq?SUseaQ7~|xwUm?m2FXBv3MD2 zj;L0km3(n+FL-}kGGGvqc+|--a6ij z+IahT^Z3aDJP-5M@y7AS6V5oV@zGb0o*X~4%Sc_nA{8ojV>tCtY|R-5`#bwjI^(>? z`}^DbPdekg#(Vo)`%gOKyvDoxoBK~XA1f?`{3m)=x!kyu0<2TR$1K@y^z}x85DK@%GkFZ2d&k##>wO+IrU$&N#1w zTR*<_<4-6KkFCQ?8w1I+gJxG!_|BR$4mUo$@!_bA4>mru@uAz_|4+P<-2U%v58U>^ zZ4cb`z-*edx7yBi4yS zN1m+1CB3dWBNQ`qWK>_vtm7O`C?Cb8j84dL*4I%k#GPz); zoy)d+a$g$jUZ^7(>Xv=kToBa0?yeR8wka8NI&NtGudBbhuHOrFSO4vXOs1j1t1Uu; zVaptJCA}@_18ENaTHQ z#Uvm0^;%L@AT;!KBvsAlZceINluNV$)h9+(Z}dnHuT-i^j}h@f1;ctZp7%s|d zgvgT<H6wl>ybFySra#eY`LWXq8X($se1$Wh<>atuqJd*U4YyPQ||+U1uzpA(#tP#*?cm8vtXyliDxrV4J2uV6Ad%#vVFj+l`3Mo0#Dgl zt7XeQ^7`J`0O%0Z_$sasxJ$gq=Q06J4j@%lm|B%cO54Y+S~{9HFR zd<7vghE5nDN{f(C#pv!mqO4+&R5SxV&33T-un@wTyYL(W{dw2JV1G;TF=6pW`_icxOq#&67V6|7C{8$a^skP_PB>)ix69iz(lF1UB zEb;e*{5f@xl-*27n}cEi1_$6%UIlWljSS#!p18*zcl@l=k@ymZB?mRSnIc;wzXri2 z$p!1fra$burT(l2v#%)2P9X<$Suxw7dAB}kB^A~-8p%qsXis&a5Uq^Hs_Twy>rz*C z+fTfE+!ZIKAY5;@=imP?+K@Ibz2V|lE`IlgFI{-u$rn$i$6q)Oj{fFoeE2^P-GjeA z814V%zP0z~d&chPceR~A+Zk+sZd=*vJRjN05QsVUyf0*>-_Ss54^7!8^E9r z4T+FUs!#!g7BhpSKnhU2(8y+S5%k-l1WG(7SmRbbLY>@0Af@& zztuwk#|ChVhv3z*0o>vtxPJj4M$Pv)nXTu4%|GzI`(gu#u~Q!fz^h^dxTV$GA9&xr z3ji_lkZvt$i@b6i-#by1mFx) z9nuw@0w^c^mGa7G{;^OqxsZGX_Wn>S#ZHyJp@o}0JnGuzy*L9$AY(ZWA%ae z0Sf>zHn8G+-$r>KNXG^cqp)=p0IApjZt45Lg^M2v3w`JtmtGKZ{@;D!_rq-edrv-p z^4JM^{H5df9KYuHGPCfM9+plGB zyfc&oQfr%FcXFCO7G@J!Mmm(&!9;QWNSS)*^*7a*&9Cn?-HDO~r65stf}*t{Rv#Os zHH*|kuRB8@wAT7gq(b+-N(GS^MBt}Qk#4TFpdOl?p$}MVeP`Ei*wm~$p=s5nJ#XTB zYb~gUrf29o<-ehh*L$I&ZnrrZ_DL;RPp+gMn#9r9qKP5mcSTj{2CgQV!}W|O>Y?D~ z`f~ZT-rPx-ds>rnH8vn@LexdgTx&r+hI#xW+AM$u*JynQ$Xnb>h znf1fCI|aHoQa!7vO(~1*QJhDe!snf#?-ahV-eG5xUbQhB`?NJ0Df}sY?iu<{#f#1P zy`rZnmhTchxHmzJQ~I1U^rhEY-|0_KN^Ej`Z|0Lzp+(QiYb~gUMmN_7o$?<7<5F!} z^Bqeq)(UP&fp^r^Dy<$YtPVkDqbS! zYn`F*RJ;@_t+H!jv^pTUVw3Lbg;V&NXXrZ>FA?-LZmutNYCR>I6JDE!B(RFFl6FsH z;8Xa-GxVKW4NkcPjoO=&PNf?^OJ8U4$GH z1>W(nk)jqAX?n_k>>2t_#a{${)tl=}o_hWUh+7<13f`FSI7TZxZ9W};%o+Mljc?s< zpi^%es9Xo`dsCs{oa!%qhQ3qdG4T3BvZ109Q!A5Nr!Uk`>7&ljcWS;3EyJ;mAYfTf zQQhufcKUsiJVW29`4&N6<)->F`BUQo7fPKz-_uG+S#v2@;=NP-EuW$9RDX-6*XzU8 zKGF}bP+B#rw@#!@Jw%+L?^J)eQYjn*rY;T&ELW<*=KA{|{7`8dWnbNH!!$#;+llL5 z`M@*?L3dVU4{2f(hnq+N&EO`}Kh;P4=9qG))_`;Mg$K_kPaHl|4-IdsI}4oBeeUxT zeh9n1zt=9I%l?)Jyc8f9Yg)ao?Yd0Aa;m?bn`26!`X)N}HYD|sbcViDF?X)MdH(-} zjlssn|8N|#&mMeY|3~)Jt>4^*HXl3M+!$PV=ZW%!cfZE}zA?4)PumGFG{UT=X+5e|?c98er>w{T(H!*xRvg(-o1y3ASuRYZekTGt4?r*{T$ zo+zS~Q0J^^gzb?+s+$(3jrqS9!)p~dHtwWBj~c=ghC@Tya-ZUqNmxiIAh|9`7j3tb zrqvFv`dxs{FhwPx;DwF!!bYhEb#Y#))(dJgO@~zO)ZEsT5*M~Y-O#XA4>2I#kTw;E zo5AV6)=MyW%@Fuy+m;!&O+#w0BuvA$2u#YjVsVJYaoG%gn3Tz1O>}kTATa*0L)>UPx{l$}OJIpUbG(Epi#k zJ6uQ_BZ_>sR!mVGP=;#QV!@bRA7wBhriS@$!E~D>VCk1Le%iSVq;6G|p-R31piom_r(L6l)rLJ^{dk?24z^(R#{8{ zWL^+zfQ@t;;0>azKnxQ-fy|*PPQ!+?S-*?uu+X9)k-$?NoGg$sG%NIBjUoeU>;HWk zy6jfHWS}hAnnE?Im>iSEGD4(zs4~5w%Xoq8bXp+nhvm5NE|vu>0O}{9*0@({1_2Rp zOh-v)ffhc0$@rtEoy)*mw*2E%vL^^(b*Prt8WMrfuzd=-k;`CrsA~OoTdd+uVcL|v zvYAW37%k>CmK&r@R`R=HRkN@p+9;fupMBc747g?QWhF?h&w8zz*y8GZbIRf=h)dpZ z`3JgUxm3wb`&d5<y}DuHVU+#B)6Qieeal{DEw3@8 z6B4e9T$-J+0zY8}%^R#`FjK&-HsB5?Ng>w<+gL%Z)P@Z`U1*OgJ~^lkl8qh%q(r_O zy6jJ%hAz8R%YTtb_KaSsk!)2Nz)q9Hl*Zw~4O~|AWUiLi(&AXdP&mA&Knoy{sL~MU z0;PoO7_z5VoDof9rO;)cd)m1SxOMMkP@~HeLlWf!ap;*GTDDr|jlMApG$u11M}(i2z$8;w#Qa?(YH@@C-^^AG3u|DV5sZCv`!iyyf7j0@_?FP%L5 zSP#$qzv<9F`1rx)eQ)nS?!9dH%{!mj$!))5>(g8JZoX~f&q9IgejdEz@#kLJ*py$& zZfqPLZ0_#9_FyYcZKYMB^)h26JQz8rB-YlZ@dJPQ?2pfXKPP$P`vh5FeZTR+7e4-+ zOB;tPjE8fKaq2X!ATCq&&WAY1_RPzR-*1eJ@b1T-J@?}MSG{-=r{>cN;^~z1^Y5XY z%#rRF7QcV$CV129JHp|pf5Y0S@5L!5b^WNHdtA3SN4Ndf#qTfP1S7D%OaI{Ak3Vb8 zr@L_~Q{CLB=i=QBk$&zg@15h_c+bu7uAc%tc=_>X&hg%JH3oL#l)PHu9a;mk?TmHV zz4Kh0J9C_0ddcGVe~FCqC67O2juW_wb30BAtrgB|sTIygx;@AEx8?coFY=L*-tqXQ zwJ6<+QQzQd3Z) z(|9sPp=qDTEjYJ=xLTXe9q>7}zxvzx@4sduBfR|hg|)amj#Dn|bX=aB1$jJ2_-7Xv zzdv~soHtsNPS!?yFHY{+^`m|6k^g*6`rK#dtLNuFd$V=()XlbVO*)=qyk?!>jZ@5Q zh4BXK{JD4+YtpBGe2(`s-+L3E0_!J3;hJ=`HugJls-&GB`*U$F)}%jpaE|j2gUC2v z^7!E#=XGmRjD)8Z&g<5sbCE9Aq~Ck@9O-}gmywa)@%X`7ly1dIw~861i#6$Y-Z01b zJ70*5@r94?udhil60%khpL}0B=kClL+b6$c@%!f@BfR_Zy|sCC6sN-63gQjs(Yb5W z(H!YVzkmMw*MytkJ-s$vetdUr9v#LhUAMw}&9nVnoQHFSzv?c2e>pPFJ09Ozi_X0` zweYTw&U23-7i;JT?K#E|9o-D$`blZHhHkHo{cfDXdN&{YbMY?L(0{#%fPZtdcl-4E zNo=@=Zmo^|PMo}^(_?=w&czz~f4*++*Z05mW_~?|^CgdO&T(G1hQ`RdSmC^G4Luj> zVh#Pd#T&^lyf8A-J09Oyi_)z)2^uk@bg_oMXEFBgx!IHc^r*k^u{W%*p)pc8RuETf z=(%p3kMvJNi{C#O)r<4*{}&t{Y+U-4OAlOn)x|Ge{LsbMUwl=_1MtfiUVq`+Prh{W zp_2zr?myW${?+3*h9w+!j{e=zn~(6LgTsG+_Dd*HSQZhPRi z2mX_6k9`q8TOj%CFM4axBuBYHg;d~nvK1! zt-WVGdf%I>*C}X;Fm0Fg!E8!8{6O&G_3W(c6zo6eNA7(SvruptA~*~YoSXi5je@;r zz3ARI(btKeo9%d2e0TQ}a_^g{>%`AZZd?~9uM?ZS6AmRq_4(=T+`|)2kQa zPb?X3p&LxAR->YrbzIHh2BpPP_))qXf-@4xR&_g!b73@;Q| zld^3TMwkQCXI&f|VS&Cep1`=)@9b!iwQ5$%xgU|VFoaklOzYsDfJxYn;A(I2i3r4fJwOzp8Vue0+$7= zp7X;v^Q@sxOUYTGlr>ug(${Bwjf3ERCez?%Rtr7pE+qn_tUzrCY_d%< zY@R*2Gq%JxFHym*YOCTSn5!pL5^N$aCHh$iCB&S==WHf$)CsHAVKT)Zzj-O)wV-T! zsH5XqKiQwPvsAId;D$asb>=A|;k z+hA+r5ek(zOtUPJCT7*u0Z?qn!=dAjbIqNvEG4KZrkKe@&c~9hln5$#hjX*VES;{W zltLaYrE^?5RqjYBcPG1&Fx4{Dayyy}o1#Hg5g`lDl9h_q&UfiLF|q`L5c5?wrR@IV zQi4S5oM;hVgC>${#US(LaNwgi#sCgRKRpGK)Hg$!_Bc;YTmBnNbgOkbx;xY`EwUXPvyppIp zX=21SI2Xsbg2m266Gibv6=>qNkCzC+N~e2+UP~Z$)RhDPw;{i1&wPy^PAn1&+UBf` zdJ;s~N)9M7^(I3|HN#i-R#AeQKG37tDVDVee_X5Axw0jc@=};L4&rpxp=ey|l`(Px zZhk7cEpJ|~!A=tDCdSiB2Zz#Ggqfl2P%Z!fI{_<<-6(qso8~9EVF!J~5)@oT>fX3n zV@(nA+72+|v~n-w3=_03^;M71xY~?Kh6LvD;a@E!az+8A($HYs?Q-d2)+7cRm7qs% z4=ZN?H*1gjU?V?;hrF`6y_Cq-dTo|;*hH0;&{DRa!W*e#VmKvzH`m04eATXTBf@M? ziT%}ZfEf}d6-zG%L52~h##H7Lh6r`ZUQr*rEm+9}geo;fhu`?j!UpS`20iKLQaZv+ z+QsP%o673c9cei2vTPqrWd^WcS3Eq;A3bm3DmF~X$_BGGE(ieLVOX^zx=Fl(^eBXf zM!J_zX2)u_;dEQX^Oq*Z07!+`Yl}_44d>04U8@OQhi3}(2&k0BkXIZXb)7yZ67bf4 zUrKnI=ZgI>(ZZA&oVE*vuCCfF><$W+fYpU0m&wv@OIHWhp1zXk@p2!-IJTM_jl-OR z0XZPYjHiN}C*}~gm*X>DkgKqHch_1k#)gS?3gi=Ag ziAkn#H8^Z9B^(VdPiD2EPBilU1`eoZsbBG-Hjtnjtum0y3gE2XZ?cB9{f$eBQ7tJ- zeYf67X+x`Lw^F2(MhD6e8}n1Y%#{#}=!8DQXl-k`F0fXLY?5@*C{yLhm=Icx6yGfi zX4+{K2kj(28U|D$S?D99?#@e>Hnf%m<{@FpoH-R-pv19kc) zO9$xwGnNucyUpf>a?Le@Ttcl@9hMzc`4WPFK0oSrjO1|G=Wqb9sLkJAN;I7m!YhDQ zw=F#{O9NjTrl(3z9A}}t*5VmhEer-EWNYs6GnW!|TTk;$x-?0&vomkNT79UCjj3V> zBnlFm^}1k5=@oE~368$Hl#r`Uff|!Q$xT+Xme$F$S+qJ7dxOeg0w!o`)Gs;)>f=~$ z`$v}&l0Zy~wVDb71+-?1MsnP6QKADY-8x*Tr#j4tb;W`=Qj^O8cfPqiDV#2+yDGZ0xO4XoZM{Kg~t*B6{cu{9;ilD$fZnl zmh4oE5GG)g0YYtk(^5jDX}mtI)zZ^OeVi(V6Gt)nm|JJrj-7CNMjAm3C|^)g+RnEw zC3t1zsd$qG+-N{>)OMj>18sCl-A@NA`PY z6K?hu8#ist#)L{M)prO&5Q2e&qMLuRl&CdiNF3RN0TS4m@{9z{F>EkoGBxr1Ot+oT zA(|K$9D-yw-m{coecaA-kN}a{PCHR_x~yxDMI1K^QoC*SCM6%vjQDhcBX*YW7%VGW zWTqpe2)fBzK1)KF6eQ{yBwRM-3Dd3xOvjOcYO%&{ePL;Xt`*rL88pRwvk7?PQpU7< zL9LDVo1)+9FPAAq&4a}( zIkFYLmhFH`i3;Y!xhC8j5pX5b9(1tkn9cSPCdpz9B$Dv7t@`4u%ksp|zgS9?2TmTT z*yZr;4Ic2p0OnfAFwv-qq=EuYrKH*d&8y?$IN11SO9{dYM5j%qIaaIn>rM(qFrx|R z8M91LO(%S5v`2h}ge^@ueEUjb=$qqyGCRGeKk5L2-7*^)rkO3yI#?;qbd0Pr(|c)y zLYs$|R}$4kh2l~jvsdU1>yBZ z=C`At4P`-8x#$4;-k2?F^#9{nB_;u?mHJUME5hdN6C7 zfeL2{GGEF$x$q5Yd- zmZ*v?dsac}W*IK$Q?&|NsjWgjwrd?P*2Wto|%GqjI&tsOB+$5L)0BumN_A6artjdFd)78gWrsR&&#D%*S zsA{Ba)vQ{|SaZZZepV{s<;>6KTNKTyX!@O9{y|H7aA39Wtm_Bwz^jsDAjg!UVtM`Aj{{ z_j7(V$Z0-%@+&I|$(m)RL?z{pQ^`q2sft?AN}^7pTN^^6WpYs7B|W5-=^j70lxTIw zCek$qgLDBknOdSVC^d+zg#glQ#P&GU2B%y-pRBX|QGF#*>}6_gw+UC$hNtnVSpwkf zF4(H19Nw#zx-K`-ICwH1=MLXGKmWgTPi{e*Ke2g#sN(P8=fMjP zi+eKr($_xcvNDEPOdKn2sVa9sB@z0JXp$J-RH<@x;_#VCNp*0cHyR}GJa=R1y1O7Z zxjF%^U)7%9!;&5r=6Eh_T!t#9SDS)CfI>MAnkA-sAwSV&$OWct-5f$S5>sYLRF(`r zGM?qdj|VS4%r83qyvyyn8Gbk70YX46fiB^i01G)o92O~DZB{@qO!^ROGcGv{l=JDi zqI4R#mhvK(OXqc2@@3;;E(COc*z1IdPX#ScGh)*YnS?5NE7Pq^skBE|J9$jD$wa>B z^bJt1HzNZ&eIHMLxE|WOC$vXqhbEq971>QqZM_Nz<7A^nSx(N-2Gd3s3)~iw#|X*; zx{>W&y-%k+Tnp_0p&ijE!pWAIDGr1}!hwyMMKju%!FU`v`uLAkP z!_2DFi9*tE0leNboQeTg+SnwMO!~c1mD8tKMa-NA@XrzlAnwzke3)xm7d~F^(k{j91S{zqaL+S|}l=cWV+en(AtEvH!q%4*3 za@}NrId0l_rX{`y^}7=yvZ=G=57INNix$&r#I)1*`%RF!dZ)+>>T){quL=B|)N zx7f%MX$Opq=SFdih4$ugY>w6XI9u-(OOqUEYSYw&pY&Q)G+!$FGsv1i4&+a8BcoT@ z$o8&{V>GlgkK?vbuT4awECy|6Hfc9orQW2_kql&vad@9h`?4T9WN$WWSds0l#c^>p zr25b$CfO2;!eJ_Nej=o#LSiJ;nqf>&!M@+nm|Ux__c$P2n$A7&i8Q1)ez8F2c*3s7 z$ODRu=gByRSDnrj-DycJGx-+50flfS^t$D&IPIl->dekCZqP0CR71y#&U`&O=PrMw zPOsCME#vsrA)a|0$0*hm8PAR4`2Nt|JdR^jCW>tD+Bm*1v@?(67}+S1?X1P|t5!oA zqs~oaQ)jQo>v=Gyi;$!JV z0&Q{J0Iy9t6HQ6w>wPnkF6EQw=ftkU_(ow<2<^?ormME-=18k6l2)-HM{9^7LB}jX zlu%{T%0UtX;zcpZ^d>RvT@9P?B{4sDUih^Wqgu-O1xTXI&r{TN%lVmGIXXt^kjQv$ zjMtY-eqLLaQN-QFS4*sWa#2sqBQg1CBN)8>Nj; zZCv_P$m#!S=PrKa;+3!-KxEU6Uq#P zt)vA?Is^z%N@$@BVN1)>u#^PK61EZwEun=HXo1jDNc*GZe|@Ed#Pi-fdw8Vjzccyd z^PREI)j7KNo_qDZy7wHgv*7mS8|N>6;BvxK4l{?&7NLAr`R>#GeiU7Q?|%2S<=!); z-211Ld*9*P=h6>+!e8!RmR^~tN)d@^hLk2R>9vqDu>)x3Ra45na!R@1pHlAkrj)yO zO1W29a+wBQXvV{8$ZX-^ep@L_KJV(3aw}8HAydjZrj%QrQf_HVxy8lwA?cUvN#{$S zuYogb!0LbdWr$e6dFk4@iIMxvdyRjw?x#n`qZC`>@&=ovI$dqTf7dRXQtmlZ%3V68 zoIIsmc1k&EN;wgH;kt158vDQft5tu)P_)EzaVi^U!%8t)<|g{fqTOHim9zW7zH)Z^ z?kguwX&*nO96O~PGo@VGlIwPBoi5l}7Kuv&UGF8D6ZPltPATV^QVyF^&ON1^Yf3ri zlyc~laywJXZCi3Jv(k$(;YfLCmQx9_H94*=+VlCoa*I>m@6eQT^Ha*%J~hQ_6jDO1UpgDfjs)byZhdc zoAQ44_?#-KaxdR)nPjQiX3NpZ^9F~HTld>n?y4!}uAEZtc~i<=G5-F4WWG7Kdd>3B z7oH0Ka{kY+vIbt{z5cQ#l~G zj@o;Gfiolsg*WI9TPk?8qxMSYFIM`-=5(6PhE-2T_pjG+0YlY66^`$>#~1Z8BK?d@ z6`Fb%R52)a#nJl=7~iC2=IfQh(JO&WQEhe0;I{zyJA7!Pt8~YfUplS^_>Wvo9ga(l zZ`tfrw2D6dSG`$K>gA?kEl>U8TFy{AJ++gAE$j67z$OBB0Q~QBg5qVxD68Wpzv8G- zR$LzM_)axntR9e$VJ1;SQ4{M_6s3pB`Ffe_@6d*m@zR*kYQ$M4ghG>zil!@Fvt!?A z!jSR3)uPMeaR#2QhmVD8#7ON+g}7h9${9w&JF#>k9+hZO)6>zWZwKU* z^mtP7n&xfSD_oHqnSD_u!L}MD(n!*%2+j`FK;&yxG1Q{3tKj{kSB|UD)W;U7<8BXn zwx;g>=pD6HR|=|;ySIXmUNl1lm z>TxHTC4DcHpJCm7ch8;SL&0vIYb|JxO}W3N^9JS z$38rkC{_&6mxj91fJ022O~ld?PcS4Nl42oB;4+D^n9@_aN+)M_>Y%W0*6a88cP|jw z$KUrFYIZaFT*n1B>h}Xnb{!XXcKigH5Ve6TqhQpIvtJEWYk_vs7#u4FYN?u1NauOh zM8g5&oW}(>#>=j}%2ew+sJY_|C*COnCLU z!FQdHJ^vG4J;SG`-MxC&VPRrEKJ!c8*kgVL9zGe}Y|_JbF$8!zm>fG`Lb<=gj^C&D zaoF*_&(#r~K6Knec6G#zz#1OP$2wYBu7*rcJ}(|Cw~bb`*VD_4;N_c6xs(|tDoHOr z>a{Tf)6BBjaA$g20DZK?@+{OG_DU)a4~9y{Vo&G}iv;vJkLV*fiG zu~T*C6NcCca%w_vlr>#<7wbwWpEbMLXp(ofH7VXh^`L?6Hoe~WUTXXgIpBQ;jAz(g zUW~C{t>2-(z}T~Q^}A%RrZM%T>5}R~OqVh!+ah*E#xHx*RK7`@b)SJHRM%*Pff)zf zS(utM2G`qYXXZxDE^Rf~~QHg*i0jN}@6yY3tn zKw9@U#Z)=kU+~ebP(=xvC6Fu;XEMR*CcyEd1HW5a{n6ZGkEnC&f4-hr`^H*lZDHZr zN4{`GT|Ke-sFgoic_wl@k^^V|PdYB#c;&{Em;ZG6SxcW^DlYzX@g<9oUpPH~>-u}w z!)sq$%g+noM&F+6PD7qof3)z7Y2?n!oUeiNHSqs~8mOM27v?T|{?cBYW^&`;v~rsg zOH+{qo0a8EEJY=lD4QXxClY5<1{NkBxMs13vNPm1=~yBX4#oJS#M5Fl5@DHypr4?| z$~N}8$>a;@v~rt@-URy;QD?x3qOP3N4_=edEuYh0J#qQjo(`Mb6TgSTGvqcCv7nFv z6gJF7$?@MJ*%)6vkvprh$-U@%DVvnr#AP{5#)IK(ip*p?Dbm$FH@O*Y0vm^i~r6vd`PaVnLOfwD`_s%***az^@`O8mx%>F&@>Wk|&iZtdKD3v@Nx98Plw%2!B{?=Jk*P$G4H8oI1anqpQ~H`~Qf@OU zCuot9qKROX$1`$D6zNd)MEb1CCVgnHC!3Vp1R--0LYih}I?f6i8jtV-=p?DLDx377 zy_8MLZDu7rNi&R;OoWplnRWyhgLw5s@~p}xkBaQ2Y*KDhO2ybT&Zk&Xh=;^TGS2fX zc%6FWii|~hG8~I%a4>_5XH_=k+wvJ{d^QP|q<}6`NCh)s zY7LWIEMk5Cm*-ZW=6LqPC+Fo!*Es+0bFG1EYS$~{70p)^<`4GR*TkoV@fV!kJ}tPM zyRB%>a0+X@Bsfb_qTN3g%y0~Cw-w6!AD)?9qulo(;<&)Rp8?PClX0p0e+D$cH+Ac) z`Ce{4>5*VD6VNvJb$VPjS5!-iSs!l&K*7E5+j()tz55gFZ;77qReQ_A1D+e+?ibf~ z{b~Ue{z9TKem$yyEdn{EUQx!z-~Zm$r%CwZ*hb5q2_vn&2fr{Jmd`8W%?I$WwMwCO z@87}45~VPHI(Vg`_bPA?1FV31cR09#qF>zy6esF;jURvag=qXcO^AwvgvZF&p2avQSv5_1yaXanAioO5(B-Cpbp*1k`JA3As0US_ybDk z`pe-5ls5R1sR_lfA-bD!P6at*MRG9c4?+Aexx{QGy=9)rQ#v8lS|PTu|53|nha2>l z9d0hWw~O58AYg0KEaTPq1`}Tzci%toAPpQ-?H|}}QXY>$XWc*Gy0@zC-Oh66exa$y zDsZcF{A>w^9Os^&Pb8P_E`&=O?=pq15cN1~dD`^l^U+9ACVe2B;|@k=WI=K_m~-~J zV!WjILsXjH!Fr11u4e*~N^H2}t~CZOv1#zfifB5TP=={YrYO}Ump2H$p%$W?Kd$7H z`6|u_y?Ct1nOA02qz$;$P)vDdvgH$O{PJut6x2F~=l zUHoVl&#;b8{|`;6@KZn7r)E@|a620Y=UlMqsoOl;KD#rg>TyfmJH?E*xc%y;7&PE= z?zxFBgpY*08tV1CgU&I$$&}52X@;Vlh>oZc33}QHZ*q)I@CJU)oqZ@_6sGpu$NIet zm2L+#2^)v1B%{37s277>%)b*%fiLhfDrK835ALgK0tinXtLI(E3^dO(Xuh(uqsW-# zW=z~2WYc0VG~GA)@uP#z{-X5t&mDL6mv7C_l4f!LO_mc=%$}Q9{bzRe{{@>@Yh<|| z7Q!9ByGd0TLLJn=>lqr`inxd%Yc@OSV$EA`8*DAc@Ad|~%iA94==a|&vu_s2v3KqJ z9(=}H9Q4WiZDQTU5b)8y-|WfaQY+*DI^2gdcwPPr7fpPVm!?so`&!&2KH8ga)Tq$xV zD#v@R1|ZcQ466=trBN*>n!O;1yB8LQgG`fP42h0`@YHe74v{&=XM|8yiixfr5Inae z2rYq1VHgeuh_OBUY-_b2eYb6`!9>Rgf{9~Hq$lz@erp)vOT&(@(5@Sy+92Bpm#e%p zMCHc&1P)vA|E`$>Xa6q%XZ|3$TP>c0e>*-wMB`j zSNvP2a;3>S*%|5VHm14;sqNwp zqf$=US5Nd^6&2|1nJ0~@^iD24+IqK5z%uHUs2EBXsc@^=xMN*eq?>HJ zT%#BxuU1?IC)@IOdcqLsZJ8%KQ|X;tU9|Pqw$YwXEW4Q~CmE8z$B>vOq`I3O)f*&A zEaS!dAPS>ePY)vXM%~PgVyG~FUZA%E08atEP4i@HD!r4-fVSRFYDWnrYsOGCgJD-AK{ z11{z+)Fn@ci8ahFR*7btni%%)gy=w>N`nqimB23iI>%?+6wsSDPd28}>zVxeZtL~< zlwLpU-f64DfR^_PzG7({!#a*P{bf;&8O7{S_hEda6eIcxvQTW-^z8=FtC%P2Q13qD zhIfx~Lt<#T-KMi*u#l1)&7eY#n3@_>w4f^xr-J@eIZk>jwHTN8dqRbbcjO5-L`!dN zs^{Hf+#q<2E|17jMH>WC39gAZ(!SVsmC{4o4BK_LTV6TrA?dPzOf6mZxc7&mQAOInhANa>`$FQEYs~)A3l}Q%I$`nkMuM9uuwg>pedhzd0fl zN(ViO?f5{b0KH-Jq&Ah_$qyrTe~x&EStApus5MtUL^75|ulqN3DcR zLF#arE0x&JIsJ*Qs#EFRW4uZ=W3iD-ZP$XMGDoB+fv=6ascKs*$A(0< zGKz9NtrboODOhcW8U|fqO5_-SB4N?mX z*@VmKFM~xtcb|`%9)fkZiVE#bB{SoBAz+>?Or>{^al>Cp`@oV$(M-sFPfc@S!$2&O zN_hx1pYc1zN+P6*>88l#lZ?}Y8DRQ~dI(Ezej2^l9^+(ipr(_-Z7mhwF?>bOc19<9 zX*HRym|h~Q$3Ya)9_@)V8$vje>gqVvhz=s-e&sh$DpTp*W4x-6(O}(8l`^i%D9|i| z@8C{9rh@(AM2~TKF@D&pa z7$^I_R3X&jw_9peOf+R~&>0&2lwp*6X4l#D6;yU-6pn>)zHB73+a20)XT#(74WRcq zQ|aAfyy9C!Ljxh%{l#+4)PzwexZ^FU)n+g$6=NYCM2BfRc`>HNvmN(94GtP!CNi$) z-!M;JI+fl%#w(7aL8h5_QzJXBOu1EH+fIz!=7k|;?sRdyEsFi3tX4DLh_}WOgdOd-SVtgUfcfo*%8Z>mF{qn;llez7&PlBrHwod~uZC37CYJ z3MJ7tCY1@V=H|xj`$+SoG?m^x`W2%nQa9Yzf~a8dPF8GEB|K%0Mxv|REbCly81%Y1 zq92PevM#GJCXYE|nVkxQ_lus%$SqDeSDubL-?sr2sAuVk0%7I-Yoczac6o#YdBqRgf`dIu$xQB{=K zPOR+Xj7Xr>X7wJaxRgwJ%tKeqll)YA_m~H>nPjIRv9IC!lo)qA*0w0 z8-oDVE9pVMSoE~bYR%sjQ|W^1DAZDKqvj0^9 z`!6kj3uO7bbLqK@Utg3L?pP3xeCbFAaR20?PalfU-!>lwh5v{D96$B=1yyC1FG5FM zNx}RBu&1;T$uL{stt3g;2*c0iGg8*Mbk40pY#mxy!?g=~!C#cBwgq%r-ZO7-H+Ts5b~Mr1e_t@i66#YYd#PE!Yuwk(+8 zt_Pb(;`pfxEemGIPI0iABaWYXlx4vTsUj{K^ihoUm3^!zh;EwVJyEtF6u|)~O02Qb zN}g|3Gx>H_j#23TA^(q``gO~K8SZ+~C^$@p+ps<@l%hqESMob#=+*cIDYC8mIX6p%sAMr7spRM+_GSX zTp9FXpW3i2m?6W(!Dgp8ernyaV1}F* z2b;Iz_^CC^f*G=5Ty&u6ERLUAwJez7t_PdI;`pf*yB|y+BK=^)n;$=gSQgBXGvi?M zSsXv*uq>D%OUA)wxHx`l*|K1Uycq|Z`{MYiCA%L?pHbtY15Jr>{M4dl!3=jj*d!Ro zPc2v$%n-i+U^hG*KXt^iV1~RK2b&Y)_^HE|1v6yYIM^&1$4?!yESMqp#=&l+K7MN6 zvS5a+90$9%^7yGa%YqqpY#!`BvoZfi=KOQ(_0>C8!j99+k6XOv$d3bQK?fqnMyFoogh#1*shp z8Z{B5gpT2Urb&kgAtHvtA$saIDSaQb3A$gKOs-)*l-h(I9Z;Ku`kstS_EPSm&N_LJ zB61K@quE{jBveh&{umoffGzuMh;#$5ZWKYDK&KFKZ81eU>B7g^TnAlmk!fZloRm#S zEES!)P3T_(ZL+gpn@q0nKBU^DdO&Rw4a5?xOerWcD%ImohI4Ku!rCrv64Xqp^D=11V|3ahU3)KZ9=>rw8{29ZF0sp zm4{H9jFJb{CWK}RVlgOHoNR%tvx$1$P#XPR+9V{YPM@F-n)H^|ZTGxbpj%3U*qk|u z#>sTFPf;`-ah3fbA(hsQCQ=*|4}*M~Q@6>*F92<_wO^Y&gy;Ct!OiiWurkUH88I13 z)dHntt%a8=yNz9Vl#qtbLdr-eq4KaEh?HsC6kYu^TQJc~DZdqsHDdznYjhfTQ)VO? zr;@?2Gyk+9aIe!C@PQC)*V3EyNsAHo5CZ&jxMsTl=-iLwJrK(FZo4w4xbjkcrp$p-}BQ znYx@URq16?V<_oP2_BIhWxQa^Wt9Ub16Vhc|Lgb0 z|JTn#`OjbarEB0tzU$TT396S}F@La&>rEzUX27oj2NA2_R4>-B_t3A5t z#C+a8!wZ9{wa@Y{C)d`Rv0%4XKBn*;&&b44e?&*SUM!JBYnke>*+2Jlx|u{@Fcc<{ z&6-+Si8%dRIP+w;QSWjp!-twOskaJUTvoynt>ZhE?+Csj z>rTXEZ@4x(Ruz-eg%`f}(RVv2HN&yI@!`kJ^+kC25ybzWVby--lfL6r5W9x+*fl`$ z3@g{;<9xHM9YgsU*5-E)|1rboh232DS9*Sb@?B%{z~fZs_jilnGrrMT&+ngm{QomT z2#s}}>-l}R58E?pMxE6{)tv?t8SM<^Oe)`rkm|8EO9wk)wS(%%RF~oh<3(qH^5=Yj zlCD%y$=3}hd%=FRS1$5l$889!DJPIaW&Iezr9S1(*&fDy+O)e@&pIqjMCI7!Br)j4_dnA7AVuid#SkzzDbc$IXeD z+z_ZhKJ2d*Nq4m_kb1IXfD>|w1V|L%Cz2jM6_wdmkr4aU!VXWX;aFwh3k<~4iBrcQ}+%)9GTiV`<=g^sf>yp zAJc7zhQ6rV>BIXRl~0h(4i)DVyx!_ZGVxl|=O-dE>yk=EH{mX1w0@XH385d0mNT3N zzHz3r{b+>tkw&>Aho_1l03RLr9RfJ*Q;_O z+S}JgYvkImuYPOwrq!!gldF$g`QFOSD=%E(R*oWfBOgXyhRDdpj-NO_?)ZJj<&N#; zpDllC`8sgB^MB6Q!1)?DUjyfB;Cv07uYv#bHQ+p~9=a%CL`PY-Zy4>k@`_j;dBF~n zL|Nne5d&ilPUWj=%Gu8{@jmW&t5u@mO+}sUjubRXgsSj`dY*5(N~{MHi@kcT?(!96 zel#kW1!h^DxkPd15(TS7)gLs-ej%)dYjszbcA`OMON(U&#fX|81(`4rX?Ii-1Rk%g zduA?y&0NB5l~7x0QP@&xt&PS;>FOwIYErhBj`D++O9$~y6R7H=G8kpnmO?X^2+mvr zw@MUy&3c0OB#NFbEkZXFw4%9&V%pgY^vk(K%!ev$A=z|}+Wp0w?k(XJ6GN)xX+|lJ z+4q>ymU}Bet6<^DiznM**;PU#<`!Ku7QZ=ji92U5@eQj)fdD%Loqn}T#tG3ME$6x< z?H@?qP`^V0^Q0t~HG19vnu-0|3uZ3yJ2RJfzEvWx6|>1)a;qqjJ}%RVGD3RGa0>W9 ziw!{XK7UZA(>)0nDfCh0nM-Ihmnc~!WSw&52EKed>I-2qmcSW~2^0i(x{~u2RJSi@ z=Da1ZyCUI_vY$it_?x3rr6H(MwY~*X0*6XTvoc5|NIKMs@qu_e!wK$TvK?9eimhY% z5`Qyui7(qFwkXp@seC8x$Fk18Ak-y;qtK+TR0sK%Cm+tnM+OKUQ1{P2bLJAyuu4cR zbkJ+~eAR-&l~Q$XI83|1=HEuWtO%{Go+K5`2&M&ETI>%;5-;P4QLl~@&U7XmbT{)IC7;hS0{)1%&0ONfnM>I76EAk7DNmITumKGu%|KnH zyxvO}(R$CzE2D70)1)MK+==(GrLS5#W+-vT%q48!;N2xxtD9&=)9p5{4ca_NC72Su z3=X2eY2LoG$kS4x)htwNkNnn{|L-h)8}R?-*z%*6zWsnzfY!RW{;c)YwZ8>X0bjA! zTf1y6y!P_QSAM?onU(jfymqCs;#*ljzKFaLX(G==Rvce-ywzbk zLXL+me`EO_%UArLe*rpQ0q1Mrd<~qhf%7$Rz6Kur8h|en-kyk3?I@lIliI*%46;Um zO-r3EveQaq;AD*_)iwLIR*~!Dk4em2f||KR9G39K(h}Mi24v8g<=Yj101aJH;qo}yvPsaRn3ndG{C+VfyJSV) zk~Dr}Y334(GnZJfN)T$=6O4(y&d{va-2Q5uDwWuHT4k90Xk=!)S-p|xY0V#F=00JS zh~h;Zo8`w+u5s>VcF1bi?103{>R!E{KM`{^6NRd}!tpAGAu~!-hc) zcW|#PYfju${hDdSz!FuoS?yOEALUyO-dWz-0*T^|eA2RN zh7z~UT;f*C-1`TXw4UP|)`j~=V6-lLeXk2&vy^+Z7D*Xezd=@MAMNTEeeoe1araSm zt82t!RnZB;kcRn+j4eKK<`Pesxy0iwbMJo=%6g8!wJzNM=!bRTlY3pb&AM>^g9%oN zTP>?l+{Na@al$85iZQ7>5LG@eW>GR%9AF@YJhSCRRXm@kM)cedXD;!BnM-`%GWY&5 zQY~{ozSo71Svs~-ekQDMHPjpkdE9De)s9=<8b))$LeX6osSyZ5j3&dl6N@c{XBnWR;a_{VThO%_54fYir zbE@EcCQDSrVifm3+&$j^|HL_EZsYj+f3IJ;_OrDsR{wK#xN^@*ANdJlI)3ahmj89R zy>$0dbMXg@jfH<%s2%yt_4r_=0>5w}At@*;-KY-$U|GDZW{RjxochS}i5s->{ z&PjH%tXc97aHp0bIZb71MQ;<0u#sFU66^IjS0inby$IL<)XG$Qe7PoOmm?Z?&0XIQ;W&;km z3AC9FIN&BIK?7#E>A}X}y6Pr%W&;km35w8w89LU%#sItOrUEn|H$w<84-wN1(%&V_ zER!nMs6@mtqJgZbN6<(xMwSCxdO-!D^DF+s{&}9Rx+y=i0TtF$jd_cnAcghi)X1as zm41W>N}OTGBgvkx`~-BmYbXExw~ zn?Rb`fCFv<5gIVVO%J*WgqaOE;3nXq0WLv~vFiRA|1D=lAnGHDL={Pg9 z0S7!CGtht;4s;&u>6EK(O3!S-3@MTKKOO_V|If!4PS0)JvvK#vT^o09+_7={#%&w7 zY}~wYdgI288#b=nxEACFyn17>(b_0)T)rV~q&LWoOTbA1bmQWU&5a8;F4$P!m|MSR z{qFU<*6&=uWBvB^+tzPczj^)i`i<*1fSiKYu3xi$_4;7FwO(God|g^kuan?(fqxxc zzj%Ff{lfJN)|c1kK<0tF*X~-obM20`+t+SeyJhX>wbNkN!3}HItzElz&Dzy#gSFOL zdF}EwX)O&-BwVuQUqja}UfW!|aP5M%<+ZuhdsgpWy$kG3xMTJ9)!SBY0XYm$uim(N z!|HXb*REaz&MpjATdU>O%U7k<^eVY}$*O-9UA=g9bM?a23s#p`=fH}`-79yk+_`ec z%IzTc;Vs~l!|9b9S8iCjZsppQYgVpa8LYHc$}5+zNGs_Ta^;d0{|dTt@yh1Pg)0}V zEU(NV_ki;dcOiFzoQbz1w;{J6HzTK!8<883>yT@aYmlpv0n$Rs$mNKHq!ALi1o0y% zaxt=rT!>tNEF*J{dmMLzYzucf?r_}hxXp2k<7UTc$Bm8~94*Inj%yv)IIeaK9A$9E zLUN=Xq~j8Y-+?+Vc5FH>bX?$A2H6YmS-yMuuH`$I?^wQl`L^X-mTz7@y?o>H4d67! zwaeFl{EdU<)^d6I@?~i`y-Y4&vg}_*moHx4T)uGmg5~Aqxutu+8qZxI&%+%{w=dnY zblcL+OQ)A^T)JWDI*|AAnx(6k21~7_^3vr?(o%YfT)Je*zl1JbytKJ=;nD?5%S&^M z_blGMc-P{ci+3#EzIfZ>EsHlVo?g5Wc2F};O18QLD@S#Ji zpF;iv;wO=NAl`=j6ymMOPau8*`FDsPM}7?PW5~Zj{3!CT5I=(a2;wcs-4H*F{1D=Y zkRL$&Ao6{PA3**E;$I;D4Dn{7*Tkh>th7x@mvcO(A<@h0Tk5Z{G-3*u?y zA0hr3@(&Q-fqWC<+mSmV{weYeh;Kvw9^#G2-$8sU@^y%RjC>8^A0dAW@y*CrA-)N@ z1L7NzuRweQ@;4A)k9-;8>yR%&ydL=?#8b!@ApRlpd5G5`w?ljl@;QjFMm`JiRmf)` zz7qK~#NS8$8sfFcry#xp`6R^OMQ(%ma^zNsFGW59@g>M#L3}atafsI-AA|Tp$?%67pJzB613%fczmu4!I5@i@XM62KfVs4DxD-Y2;N9lgKL}(#Y>aOd!7pkwUJ8 z7)M?KkwktMVib9~jW2^3L0$?mjJyP52zfEYAaV^v9C;DMXCp6!_$=fk#AhNWAU*?m z0mR=%eg~o(c|Jrxay3LBavY);xeB5Oxe_9VJP)E189_vmA;cYI0C5}XL)=1o5TAxz z0r41OLVPM>K)e{~Kzs_)hWKQp1@SkLCd7-7F2vtJbcjch2E->Jb%>it4dN4#D#Rxs z6^M^V$`BumXb>NRlpsDDQ6XN46d^teDL{NAqCk8Eayi6@BRPlArh!B^N=RjOS1c-|W4{-tEARa+jh=-93#6t)JaUMyJapmVo z3gUkwNr?Z2&=7xyBq07jgo1bv5`*|tgoOALgn;<(NEG6akqE?pL*fvBgoGg8jRYb7 z5Wyk-0J#L>_mO8q{1+q)@t=`rLHr)_Oo-n_o&oVL+Bg%Cf8JPP6mkY9&*GxA7???)a1 z@qNgzL3}Ura2p>6@!iM;Hf}(?30a5uE@TbjX=D}RJCPNLZ$}Ut9T5KnS+;Qr;*H3n zjSCRpiX4IX$H-xbZ$S=0{3B!@;v11Uh&MQXZsUJLe7)npZ2TF-*E#+l#OodZ3Gua# z|A2VPaSz1n96yEl2accE`0o&3?f5aoS2_L-;wv5h3i0Z;h ze2(K*h?hD(0a14R6~wIL;}Av1#~|{KkJ|VVh@9gVh^*r;A!ZyOhR8TR1Tp3KAVk{n z0f?02&uzRJV%+h5h%v|eAd-&vLL?mTff#lC1;nu9-8S9?G30m`#GvE!dH(+p=l{Q8 z|E&j&|KA2y0M7IO2k@@*|2SU*4}}^SkK3z%ZS((6+5G>LHvhlP=Kr_a{Qs|P{{L~C z|9{Np{~xva|3_^8|CcuZ|FF&fKVIY z{6A~+f63~(3|f zTL;i#x!PP{qRs!GVDtaS+5G>pHvfN&&Ho>5^ZyHN{{QPXum3fB z9pI7nI>00Bb%2N4>i`eCZyn${(4KXB9bnB~2UxY&0aomF0OY=P00GJ`+5CUe=Kl*e z|36~$|HC%_KV&P|84XC|FZf2&usqxpEj@m4|^Tp9(x_&r}jF)PwaJo zf4^@XAUw8b57_{6E|L|9dw7|E|sd@3Q&-cWnOu zPd5Mmmd*eF(dPgEVDtZP+Wh|;Hvj*7oB#iv&Huk{^Z&oK`TtjK{(pzf|G#4M|1aD8 z|4TOi|Dw(RzhLwK&)fX}b2k6~tj+&FWApz{+x-8pZT|mBoB!Wt^Z#3I{{IP^|9{-( z{~xpY|3_{9{}G%2|E10UKWy{=583?xgEs&F3!Cr%xxEf>v%L=Rew+Wl53%_Fdu{&z z9>n7R@3#5>O*a33m(BlA+x-8}Z2tca#Nz*NxB35{A{PJu6Py3P4YBzDjW+*(tIhxa z*yjIlvHAa-ZT|lzoBzMjVe$VPZ2tcShsFP2Z}b1x+5G={oBzMo=Kp_a^Z)B?{{I?> z#sB}n=Krs@`TwhI{{Kpw|NoxN|F57Hb?p+74`2S07{{Iq(#s6Py^Z#pX z{{JGI|G&`Y|0itz{{n}_|9{8k|IfGi|J64CKW_8?D{cP&Jcq^qudw<5$mah;oBt1N z{@=Ixf6wOsrp^DmHvezi{J&-Mf8FN)b({a!92Wnt+Wfy_^Z&BV|23Qct2X~vZ2o__ z&Hr;Y|9`H-;{TV~{Qo&N|G(7c|FX^hC7b_?HvbnK7XRmM{?FO`pSAga#^(QNoByY5 z{-3n@JZ-N7BplW{0A;TO#O-x}n7s}_-nR~5@&Aa!;{Rcr|A!o>t@!`%n_GK?<8=$$ z^Y42oyyKp?VZRyx(NZh-MoXPx6W#uM6CapIz&VZ%y=yS?z;4k}XV^;ygNx6woeRp( zuv2UIu*I_&I8(ILeYf3??Kf1tsCLGIPWMQd>AwHDFTWo}$E6BQy{qb7Q0$7M|HIKz z1Ma{$j&)fv%Blek0+Etk9`E>(dKsdUNw!>CIiav}qFxzQSy6Wu8xh=3YO=`?Y#(dU zatUvqd$d&Tn4ahtWR(?Ur`ZUb1vE{lr9sOX%mjsU(aobJsX^xp!mygq1KCm?+mZPE zDBkFsonE_}z=H|DC&47s2@wrM!f4*@2~8I*b^PccTIw~m>&s)mdqrXXVAC>B>v#U$ z{O&>bqgfqsBIV%Eog zIUXdzv*=yxhK=oUAoFcqX{g|$XMENE=>g9TZ}*F9yG9}h3V$I{Q1qr=DJb=vQm-gu zUH8AYm2Lq3IJVKUXTnIMt8~@-{RYeD75&~@)hdPBy?^hhMWrx)Iw)l4ECJ6tzIjn? zb<5z=IAnRNqxN#}#^8rb$AhM&rKS$S4@bu%*tupW@O$^pT2gE$`$~*OWt7TDak10L zOFW(ux@ydw?GG8PM+{mSe&{+Di=6Xt|m3|GJYiEPQNX+y!e0sY=8)8zaB;roKffwb_blC~Uk3J5H z-nm|b(|mjWU^ib+JKXqw+2Ll~Az&A|&p|FdI?FEV>2rcSNCU@IIYA;g!3lJR4$62k z6r{3gA{88a!dY{IxbCg0y}fDbvFexbI-eJM{H~Kc+B@b_v%{TXUL!)OUf4-$s60{w z8C)2-+u=q&olZ33`mvH9J7=$}44PU8546OpQ65pOzpJ$bbR_nZyuVrNw%Tg27-*^O zsuLq)A*nf{OH@S81bNOi2$gn(M78NF`Ra6lBfD}-rez-OB!VDy(Rko8b)%x`Y7vY~ zR~%I|H3x@mr^kn)m^LjIw~09 z$35--xilUm&wwgH01u=5C!k{KP}wKlFh^FFqtLyzS6$E^*4hI0nc7vTl?DD+ZOt3{k6!-3mi{dd&KHDSKqmM3kdw;Uy$IH&8pD%r8>9tE$uz!4U@ym;EK62%LTz7v7 za0eLppk0gP^hV7qb}EUWER;oG0gd_6Mk1e;^4@aRugbkTR?jr?{^65WiApKor^Sef z*3e)SB$-QkJ-Hmg4w^)jRU7T9*WE0pLE6Wy?EJr4CA5Z{jX9ZeRwO(<3B<7Jp?RtA z7upyd9TrL=7Yaxq^kPYiEnCrt+NH*@fse`kTb)tFh=kmDD-;aH!&u85??{D!JEsb+Ch6Kj=l{#HN@1(c zT0|&?%Yu(~6KcCN3Muhik<1O~Eg}?9y_rCoXyk+V()X+qvYXNUuH;bdmi+xr#^-Ff zV|s)m27y{oKvi$AMhZm7Q?8;%tk9Kh3HRm)?gr-x=)4oBv1m8n#ylv+$fb%n((6Q$ z#(2qFC-6fbwRG^=Tp%pf2%j16(;d+)cD4pXp;L|(GaQ&QB7}!;JJ~c!VhcH|1jmNU zbeBn+eTH-K1uvP>(8e%=W{g~Gi+6j{T^)-DWq-MLq;8c+X9=e0$vK5$#T*s&c3`9z z`l+;x>QPrPL=Ea@*JTzkt;;Oh?U7F7S(PT*QnJ?1i51zE022UfWFtcsrOQDuZzqH_ z7wP)6CDGE6>~-jzfn#~P?}>2)f%V+(WWAR0dR%>O)X+nPye_zPchop^*O70|KW)D* zeg3*t16J+UdNrrZ8Es}#m8^j!OLZ#9Muh^AaH=l9774e)?S>g7f+J?l3S8YuXY^6R z=SdWBu2k_w+yy^gY;jVzPBfw}Jj;>FR=?MB2R+%33cp@JfW>|s4rAR~6TQSaH3tL)V z^Opx{j`S*cyA@A|u$7luCFHFjIiyDliH)*#8Mu#<3`Tt=598|vvqn3EaqVy|?xon+ zy1Ce%bIn#)mX7RJD#pe!r)Utom(uyNCpx0ZZd(opuwK#a_O;bQ%u5$gbp7JxSD%xP zPT55iMm=|>&j;zW*X(#*d6!qz{d~KvZH3gjndo#=m^N%+^WTRthCA8_B$f{O6%cEP z=Y?Lh6B@V*0@X!5BJD|S<(V>*t|YhEc(!FVhgK{xo-*l8;UhFvW^#3^QcpLSrb+98 za0Ts$h5B&RMZI`);X77|l2LKjqK!eN!`1qklxp;qfjdi4ZETcxSJ1ka=G@MqpC+2i z-?ofW`9QDY75t;JD-aD0f(qZ13sF6oc1yurb1;l*&SIdA8Cc=rmWq58yykY%IuT53 zIF&Jru7=O3WKvQl*vxGOI@tuxnR+7Epbs6fO32MpXb_ScJk|{Oq;9@|XUowfmk*m# z%q7zbS#`TYl3yLt$nAECI)-hPjZ~B`5k^jM8KtzG?MkMZFt<3p7!PFm_&~&+EWhww zyF{^rYtBKM9;IomFlhO)P_Re)GcuZr(JEM)D`&O>d^@T}maer*P%b&&0W%0eQAS=f zjATK>C86GE`J82+f}2_*TQNm5sxvE>IG#JF&ejjlK^mIX?sUkEN%0t|2U`**$(~U# z7%mCHp1T$0d4DN9in!XzWFc8YtcY5jNS#)KW=L`Soz8N{hnC`@RHT;j47$}S8E0aR zbTre+P-FlHCkL6i;yIW$QxU6r6D40b*!7bcSJ;_NDYa3xK}fNFCff#F2ut?6qha0X zHFPhA2zC=vOee0y@O{Q}!q`gU)r2<^ zu4W%=sSuKEu-S?l9X=f8LsE~)nyTd0{A5U%Lg{pd^pS2a5eKi)hdyVOU~wbtOynXd za@1)A(@}RJnhAJO0%r!{&=!?M6Tz$;R0qN~}MT&G`?JZ6wh-RhX zWmIoT;i8FDD3I!D?JTSH9ycKvkB3dH1YRakOr{LNI~?+)=EFkGRxTd$ zdqzFO@zjmao|}%0H!RVj&n%~@vMMxsqEumNrk&;0+yE`d*hp6D@sVt43tiBxCr|0F zESbkrv6QQ(r!bN3cXEMIE$be|hee5ty4_80h{}z$=8@M~C1P}l&5hh!?zEx{SaB6qz0JV^6VJ8Zq}c4869lfyVl3z5;FZ{VZ4?Lpe7iE7rJ*W#=? zR0P~-l*lBrxM}GiI|Zy8paxW=7h@}7f*iK%oszGdQKKBsq`KiunHYs4W?nmFg?8%@ z$plUZGIF6B59S4@Q4cVt=^GaNynl#=n4+xon|^^e1IC0y@Z=mJR8%M18 zksT}V=P)tbj*5L(xH@3N^Vf@;KdZQ07}($<5qfm==Z z8hocqOO)DgR82zgwz*is8CJs{u)?46_c3>rIpVV(qo&2m1d6-A`||@?*J3Wj4yD=vS%(j_l>#vpRV6EzqGuz^zEe&fs^pJA*aD< z`D^FC0d`ov8k}Fhe5tV%S$h29zb}4f{;l)J7vHjYrUCgG1jr^p}Ufa`>I#-2H`zUvxNk*mrni zUYy?n(FRV>f9}w04mA%E>$iZI0@r|e0WEOu|7vi~A6;7p=lO5mZ8BWn1n~lH-(PO- zDTkRu`v*ZdoPcuKR-vKLNqSW6b+v4_Q?E<}Pdao5l*=bsiRF}HiY~`G*%mz_C*adZiL}RsVXX1W`u9#A8Wa&+@gHg8|?iT8LN7NI|-o*XpPeHkC zBV)$I${^ZO+7UI(B;*OXLw7?tg{6`dOGs&=8OyVsd?Yz3_Zmwt6HayVcum*Ak^rAC zYwV=lBc_ylxFxsmbG+8Fud8P`x?ZVu6@f3)LM%L~SD#X@Vd>qsecv*r+#gLT_vR_( z-UQ{8Q6`>@rehg4iW{{=EI~~??|j;l>yvmnVMc07k0bC0uSF<+3ALQtv4 zEA>#sC{FtM6Kt_~%BYy}R5I18Qevb!8PW97mR4bqY7Q%jgg$7Dv_xF#PS&z!$t4Dz zcB`GJtbh^)x~DvJ-metWj51>fK^23rI(ZG(sXLPxh+$$H4pTFK5-WtfXpPE+(iMEhr62 zX8w%P{&DvG_LF<8C6}do)mB~33_;BFQ7tn(!#ggp)$N~(m{J0@4G4*5`jvb)UvcuKi{v*fyh#8la|CUoJB-j%M@;2}lp@%mny42zBlyZh8myA_Iy@FWQ3Voxd=v8j=e%3_3 zZxZvvG`ycuNg4fqxhc1+9%G&3;*wz=7)ciZr4*!RlW79R8`OWMv|D;6hd3tF+b+__6@$5kMrG(m??np z+J&@V*V*1kzfApd%{rlrA7^xD-p1pQP&c!k@+!=0j#~m3lHnv^~GqofX(mvy;t`0|1C|tdSdm+3bJ(1 zVrgM&-_dDx=KoC1%z41C|G_`cD$HH>Z0WI6C%O3K9lbD3hWs7D)oBZ^FeLK6Rumun zyD8$nULm9-iICq4s(y=PsCpLXS_QmP6?v{3#m$OL>lPWpEWuULqIjW_FZ+Gb5JmvZ zo=^qf4+fl)i|c0ke-A+ljfY!#7h;mCzms4vP&QTsS4KKi&u_&U2`1HgGw-5GdA!t? zVA_)iDHaKKaX=leK^>8lTf$i-nGTc$7gYfz`gL`|c3rLk^ zX;$dcxYNx*&B1rGHSk@RyHtp9-E2ApOMEOspxt7)=!Jb?BTK1y<0#ecfdo57HI@Yl zOPiRPuIjj7ce=b6L*ReTsn6`z%g5*U=wD0!r|RX<9`$mNf{Cu_NU~-kQFZEhXDT9= zx9eqpB@n1pf^Kh-L@OB%tEW_yjrs zWWDT4>{T!0^)404!dWOx3TmpIu4UtNPv6FtgJCvJNxc{shun~<@(6AQgox(LwNhT8 z=Z>fms_vD&?V6W6-j+YPTQ47+dEhvrC9jO8(#9TIQDH!HOQ{IXmZ3K_E%UG zT%iPM_N?PP#$CJgvd8<++siJhw@0=dbf+0L>n~`jqM#Mp39Td~GuwF#a7!qYEynN+ zk!4C{j_SH9$&6nt*YdrNED>pkBceHa&O%Cx9B<2a?$*o4=O_DT^BB}#tue4dg@Cd` ztg3@s0I?LGDfsjFcD4*|CZdP~&co?sn~k_M!tW}3(@~t{EitKO>!nO##_I;|Yv z%b(b-myv&FTi&auUx;eUp#UQCkSg{H#dsv!%U6(X{BkH7HhdT?IuL&}#yOBW#p9WP z7>YoiqSWTwgcu^Ydbkvl+2ecpj@^3s_*8WNY_?49mB)yxEqLP7$XMFq((A#D^ZLYR*zl>%rFYADC~<&W>y%m2i0C9XYt zD+xJUVmR83^PRNFht&j=3W&V5jV%WfQJ2Fi)f^>%1H9PqUhhND2EYw(TMY!0_IRns(hH?yHY>SKN;-I|iDi;}rL&Dr(w-`@bh@ zav@%Ce-yS=@YNN(P)*mGyq}3zyz68!ptlJL$25*L(oTOPl59rgx@P)O@{~nQA&w{) ztx6J=#7sca;(Qzq%jq6MW3D`=5N>~#WJxDSz!dF^1n^3vT3Yu|T~UpwKCjAW8-gE; zu3I&(UU%hLyg?;1b^lnRra{wzsOfo;qm{m~Uw0Oe>5rf6Zuj(UCmK8CKAEw*wi^4v zH5-|0`ewLhBh}8}`!;d|Z66-PMk1o^bh+C|*g-RnS0YkUOsGclirN&3SfUq@MlX#95xj0lT(iwt?Q>jyz7iPiI3kw(4bv2D@nx05uwFm_{vq9OX3V{}u zKZJ;F*6<72j2t2;Z(1uDtRLLd#;u;r7P`f>v~H@YN&_whvazP&4OBv*v9z?unhx$t zRzz?E@^2vm4zA?`Kxd~QL>A8BRfX=h%Y01j=Fm={9e06EzfPx}j?$X9(FQIE%;nTk zZ7`RelWR7zCfZix_LZjFKDeCQ&1E~0WcqX2Zio&WL4CHL%QmtbZa0^QGWWjiA3uNh zNJ%{U7CJhA$I{rRVtyv!j|xmQlZ*z^nNXGv^?`QO*n7Yic8OSVlA|VgJJP4>7o0BC z2bgU8gtjtmR4%Uv+lo}6DKCgE9012Q_pw;{6q6S}{`+GWD_u}^g(g18TTm$22WJzLi1`b$Ybxbit zy1LG*eAj1{;X^_-9}DNRi4Imoon^e!D(L7s35U#`tB4=R)nvDm<_Jp45HRl3;+CVV^BG4fk|M8O zK3oH}oqrjq?cc_(ZTMU}$pZ!%UDd9of)p=%lMG+&xx^*}cHLpE97`4n32v3-a@mk7 zEpH;yY`CKJ!&N}%$^FK^>wqA}TXe4)Bhn<34r7fi_lGz8dFv!xv!G(0b4T=sYNabZ z*(jS8gM`}tircHSeRL9K@-W8-$)u(g4p;g;(692wv3p<`pk&RRO&LMn59M9na?MDJ z#Zt6fZ)M!6WLpo%>hY+@^19KU*IR{3hc!^mz=Ga6cFT^lAQzXVqE4vcpj~OxDfd#6 z$K$}=SkgUq#jm5RRarp$JkY!U_rfR#}_P@oeUY*U1TZ<)m=#{ z$%KPh!`sd2aiX5>bQlPAgdku+<$nM5E$HoI*S50-jcz)uK|M1gB4{U-Vk9g@I}&=7 zA~n1ThB1{9oDOHaAqU$omS^?C;nK0;eB0P`jtytCgL{;k>W74mA=NmS+Z(B7gSgg; z(N+gUm0;@>!xmD#A`P0Y9BcMl4@ik{+KQ*OP8JKs$Tqmlpl z=(!;lrV3bDi-V|tDhJhAGAnzuk|gGZWX>-udi`(_&>2jew~S3^n~4+4Vo6hK@{Xi0 z6k=dM&0)!ARSaTU$x+Vx!v^8>No*V|IrP$D8B{s2=r@mD<#84*AoT!`r_+**bsOE9 zK!=@pq8N9pjzfM{P&|Ln|E{+Bc3}+s;Efx{g>=T5dYbx0GfiDd!QzVFt5N zAs)ts5*bT&4A#?$lnk6JG|EunaK2Bc@1Wi=Hl1S@P1MX(7qKkNjWX_@qgx61Iu+U7 zguA&)O?9W@VWkIQvRGxWMh}a_X3MLuKOo~6W`;9trj-pvtD4-XoPtHeCm!s(d~3OM zEz0|FD;RZWIVyr{oS#Fhyc#0$l0YB`xLLrbdJLpm%38;QkhHhtP4gTNu9UHEFvkKs z)z%1JONYyOwyvh~4IvvPAi(|dW87c&KykOhXRR0LQb8DnJGpXHguK;okAs@dQVypv z%OCHAY77;Gnz)dyBEZ~o$IR`u4-|JBeCmgv4(JR#$!o@@vyCTl@OYQgd{lxL>fuO& z@aZ0j=#nmzO6GlGxgH1dT%%Y;N>6;7MLRc9q9@lDJo`;XYcCo6c^%l7zVy zl*y91fmXG0H{%Uk&1}>cj*=!UsfV8isv3Bbjj^jb!ISXSkRQs{JfSida2Wn-Pp1-E zD-muZwF-y}l!aXaSqny5T(YJN|^%DYPv z%EAU8kt%qy4p)M8KaplZvxQ^LJ_1Cm^^^)O-fO%N3xs1?c~sW3xr*Dl+3pedv*rju z?JUez3RQ!s`iWe)9#lE7=vR(i<#876&-<8yS=LJ= z;j9=zEJJjhNZrk(;83$C<30tcgBV;PDFVXm9Ogh(1B*U9c2y@>G>F$Dh&=67j3gB- zwd+JhO{r?t6=E>7L+edyJfX!a*W$$Mrm&bsv*r% z$%;axUP_p#1X*FD>1rF@2wQL$rB1YPnCa8$TlCAvrn70$Hck((KJ#!I)B{XNUcF)W z)0LCIm)JPOv9rGnh9z}uSYEdK-qKhzZp&)a#-?@sFg@(9iPe{mO=p`KXJbdLfB)Y# zaofc7OQxSP_4Ao$PrZ7Iocz<|J0`P}3p4QirTc!q@3qt4o%!NyYaie?ck5hX?)2F^ z_usrfzyD#YcdWKn&s+KO$~7y# zZT>Iwcg|n8?H0i){xUgp84Y9sO;c`~4yVHfIej2nvMc9AogkX$h}Szn^YK`=8f$>l zMU_ja1tC20jp_C#Ac(O%3g+US9q;9+T_ZPfG4h!A?SdL(-fYOOD)A@+A50^(NREcC%`Q?@DFfQ zp;0pRkk%!2xP)*3GBrhS zH=_lAp%dJqtSs#Sh{9XUhS4f54qTX(gL%@-hj~6d%Fx&W(CgdMWGo&~0wS4?l9Z-~ z)2-^Le0sV;%{-N!Jju4$7Cb)>wy4VfY+W>>YBeNs%m_ip4*bw9DHg_X4XsVUp|62P z-NQ)r2eP}1UF|X49k97W529Oou+Rz;EwN1o(#2Gs2`8JQDpAkg!nt>X8(o(l*p(f! z6WkU@%~WI}(e+~$qpozjhMyi)v%@>Vjjpi{>gCd%;7)XXz)*ID3+*jjaBtxP zdkg2^Tezp}E!>lLg4^;G!n&BqWU)#@E3hddk7e``|N4ZT;7)uB0Yljp?n!$K_r#sx zwmj4r9%C}BQVeIsVyKXB#77sn=uU83-Yj>8yJTtaImY|;)9B{ zCBTSxacXS>pSW{k;a3YEotNgFAkKem_CM#IH2s6wx6HO@L$jw%+&TBUnQza(eCAy< zSMKxfM<-vj?^SbO+&?#S#?((%?_GWUes#6Jdg;vCEM7bLk;x0EpS8$OpFQ<~scS&a z(9ceBb4&Zm`~JFc{laDa|IdGNVSWDo-M0t^krK5)-0)&)23$05^K6cFbTPKrHM~-z z(hO=H!2*9)StkfrYOS6-EFp1Fh4zSWTahcgP-Hy4mPhPLwNkt1M0FhlH!oB_B8ok7 z@gai}6))vUV|uWm%Q)*agrJiU*jh5*WA!#!N=pT*4M$QrvC(YKhlVA9_$Ag!mm^BO z8ZNsUj%+!HWQ8K-W2H`tFo<$4;6PddC@-wsFl3dGP`Jw%{gNiid9Lg0Xd3B-ty-D) zg12P}u9|)+#z8QL&R;sL!_&0d?qsl5jax~dQZIx>Uo;MPUBz17d8kOYD>cklR0*2W z7cUr=aEfsgck8)Spiq>J0NU!AoI4DzXLQ^iSI+C?$~hH;$E+I4{QKv0yHO>Fbr8vD zCD@Y?55kF&gx>Th9T%yWop227`0A|a%QIp%E9XUR-;akS@M@q`GAcEsf9rJhy5$}3`<~HKbq~fLNQFsIh+O238K{q9S~eN;Py#%e_Sy- zPTVUb+w|fS2PHHfPiE6R7E#x`P$ozgobfi3Uw07|4I?!kA{i+pRN%5VtS$UtScl&o z?P0|Zt;8yFx7AJgh@R1@)QMiXD91cLEZIwTy;_{{(+k6xom!;_yTYAvhyx#ciDe_8d!rC^dLNEShGW##+BI?#MYEK0f{YAOYlD2LT)U>-C{GQFM!-p0d{ z3(fV6gxvMd|7O^Z7Hb$~cPW_lw!r;bHHW2g7SrrR>6$M`(XnPPuhC&oH|BvBUp_2> zUixk_3Lsx66}dH1kHINS<)gj%o&qRe`# znHY*oYSm2&dBzbarUkxf1mT#!73`rCZyJ^eAdLd0lq`rsTvFE{3oV6lG$?moP0$UG zN2-C4&?&to$F<2f4omo92!x?z8a^tI!dWb0XgLz%L1Gq9naa4EmRDsGP)klj=*(9K z;bt{owuH1iAu0+a`rSYXP1W+rD2obgy`d#hsLAT2t0uOrq#qep;Z_4(e>ISsX^5mx$-y8Hv_SidKA+3$in>9564`;PElN@O5Nn!cND1==f&iFi zfL*$3SVx@JOC=%eXvzxfFuQRTNAe;ZGD@+gpT+1bX*xnyhSX*M51nwf%Otx!hYQs`jE@%4N)K8Zx^^uI!6mDa%N5ZYQi<`khTKlUiKavE zWTXJuj6x;4YIwYQ2wFNgtRq%86uz35a=nz>=Yy*O59x(6sVWh7_HvP|g}SPIk%&4v z`r@aDB@zXiHbl(<`?GSPmrqJ9+10DI++vsG!xT{$I3!0!+~G31_`yL5wU! z0Yw(4h9#OR&ZH4mtrjU(M5?(?GwOGSeJ)=aj^f}ng7LcBu#b@_|B7Q!q7n_qpav{R zNq>V+)Zr9ekw6%icuMrsC{_xT#au>8f{a2AY;to(`3R;XIeHz3nl~$ELv0ZkGGu$58-aK(g+e-JY#ZfjUfVL*DZ(y4p$ofjFB~& zEDT{oQf>xhfD=1xIw&idY^|)-Txgc^=R0MKDpd-Q7#d={#!Xy73e?HB2{CaaE(bvxjEMIL?unRADSZQy701 z7ut?$wdV75Be){)q{+^x!#W%c%(+t~L@mHAFK-D=LZVTV^B^v(=M@ySmB*r7+tsOK zv+SUR)`f|tr8shWGn{ccd8ezYKwK19dbE-1#S<8#5PB-ECtJ|$d7CAGw;-^3K!*LD zVm#Nydw~$^slav4h_%gdJBreE4RI(bWO5kNUh8;izT>eHr0PYx4!>6pS?PvVVN`D? z60f30t5NF);yEp(Puw=FqpfjC(O0R;z~!bSDAVl}J5`8l2cw-_Em}0YMxe@w6bL#x z`=QMev@7Zn;$~8Ew!)s87d11!PP`Uqmt1I4HY;p50ef*g9M`5d>oDUTAr^;ZD;D6$ z7*VMeD{+yv619jIr1tET47$a*B(z-9rx%BHG#n6uSVcvtONDMQ5is-dh*$%uU>F`W ztkwhpnc%!sEQ4_KzF~>F5)XsNM3AOZkX&Q~8i;>irMm8TSF0pox)?VI1TMx=2AzEI zutZg2Ly1B))4~%znehc1b+cKIc_=hiCmg+Ks>sPUa`>#i^ZhRzwdFwIy5N5qYP^XQC0@=jWAiSquxlbKoK4` zok@f`)w1lfV6RC|4^z8nB{COlt5m{Sv1);c8{8&!oAq3k%_kdND=9=dh3w>vKr#r; zyna|mF(ik3i9C21QxPK=)Vo;REIJ%GXhy~KiHML7whBn5o>OMNG%O*dIsql;^CrDs z$YZoxFe!?>5;a7(9?t~`7j9v>c&W;F$f;X5OF)LuwBitytw%f#4=WPk9(eEvRm11c zl$*&eZnD6Z6-JmDrj^h{HX3jSU6LV&5W`QIV0kYvI-X4SG}Yy^0u3XR(cugdL8dnC zDo5u8u^Nju;u@pt3vNP|`vb~5h{ox@ikO$V_S`I_*f|O&p zyFlb}d0a$T$xs4-XHoFU5eaO2RHfPHN!u@xprO8uFEC^Zn=x?p1Wu^ z(qJ;i)c_9iMfCIt=kZkJbQae04gccmupMR$m0LM~yNN08hSBnM;6`34nz~DbEZyy7;MS&H%nG(BgIG$B|tjp2#1~U1gA9Vtu3FMA; ztI15sp^BX*vjGiYKRgC37UoG->p@x<3sk*0uNE_ovNr`$k-9&lS8H<8o9sdPc$}1& zjY9+2e?Jfp2TFP;R<8ycVZ*|DcydeMLWqlM#a$7u+-zV#-0d*&^$K9f)(5cf9|M+XR!TV8i?&;x zW>sg)3L{2%F&~M0%NcjrQY$TiDPb;+$qCHHg#*~z#())@WZcQ?NiH7fHp_&h@|4!8 zJDb^pZuTN9*7N%kW=#sQG0+JY3}D|k1}xe$ylsagoMhwSx(RWJ05WT~bE!r<-40q- zor!_SN6l!x9f&d;j~~FkcMMo7+%ENSJQ%OKxFY5>>y8kQlo^+vVOwSei;H1G!P?<= zq73Zz`~mEHb{C_pbrcZMQ1|4_674C=G$CLST4ITYClp4hyxz+f3<0q`0@($faNYp+ z-DAMgnS3o3kGKR@?1ieq5D4J!lrsnk-rDhUInShcZ^4BpV!gP=Y&>oNd+QjmISNGV zbRbsBE9yPnNKqj-*tjV>bY}#^135G1j53ud?_e>V*;pIE-ZBPk6D~(Nqu>N95GmUT z`DAZ28bqAHQZ>^PiAUQ(u+IhNH{`^ajdKUEV`#YSNVb@bg9F$x^jdbrS=*(oJ31<6=M-O1f5HQ)%A~73}8o-VrL|U_96NF!X5+L0>=<$xJ4zg8@uw&>o)@(>L`tScU zlaHQQe*XMpX5Ik)@!-#94}cIQjbkB7Y#8hAa+(u^h&fLhG0(eQh!Puy*bj_h#}o$N zw_%an2OhF90k;c0v{lX0r*(V~qFse?{R02<$bZ`q`G<8eVikZ6XJ6E4h3X$pRSQ1|voqc2h%abO+HvoDHxWJmS58Dbp5iiK93!!U{;sP02Tj1g5$IcCy_K(&hXJpS1@A)u z=Law}kk@(g&_I4V5M#^KdYVtU#Y`$r*Be;|OEi4-o~Or4c(s@IxeCdmw+UkLoZ`Hz z)&hZqhg%N^X-lBWJ%u(hnkVa&(sjL?40W^MD9bQwDvC;Zjs%nKP~GcJDLUN~Vx$tJ zK$;pdnxrwXhoff`1F>j_Un+cfB-eXZ^Ex^Ul59ytA>k*_#KN z-6!J6JUU)7{12K*t)^@YJ^o2VR`7|Jy$8<%pr-7b?x1Y0AQ#0}#RN04|16T*a?Kn9 zZc1WnVpRrzboc+d`KuG!eaSfh?(cuw_St&EH^5a79i+k*D><=J7W;J_k3Ed>H~3|r zEwXw|sn(>xy5J-ELs3+yfY9Cq7KU0OIezLP0nf!rrjC^tb+*Y4(=KgeeM>4=aB@!&~?`I8Jo3+evXb_~Qh zM&RACeHEzdROA1-y$}kDZ#(O<`5t%>k~Ps2Cw(-530=|!YP zXjJo*AM)iwvy1t(C~aoe*L!d#lM!@XSdSiZ)(MzV>(xxV)Fg%-@7Z(Tp7YGUXy%(UmrTEM>eo}T$=6R#fC4A^x%#4wi}z1V%1@>z zCKl!=XJ;>)pS5!wa?%(Oy9XtkovdE>|2|{2|M~yC{3O4nXpG$t-TK_F?DjvOdF9C%j;hSAUfZ~+-=l~A-lH>ij%+q5{%twk+`T7f z`V6~J|Fieqlkwcrljm)$_j$s9=Q(ZX3}~Bm!|qI{`y89s^*`G;j?MI(jSKrshhsjl z(J4E}N}EiF-f?%1Q+;;TmHub#v14;Qd*gyFb~*kL)roewYedLCw{m6hdEeL!&)InV zNH5OWIqo{K7k3Q>IM-))xjFd!<;ggXO51=pI=|2H1aD;H>}-?c*7fS{0p$kX=&3}X z?`3l*<9niZ4uZg3Kd;aCgt>0xP;Qg&adUmwejUv9%pHB6!VP2dJa6N1eV!-GbsMLA zn>>$yN7%hb2Xmb{+-J(2bRKQ#(Q`J|Myzzo&cUHgD;>;rTI+M9?;5)|&)zt9%UmD- z5OR~^<}+y50q=98%7f33j?M6#je{e-IB(}b^Tb}B97ulMs~^Ih9GyU+IopK0Sjc$4pOK66)|1E1--vd$*eGF=3^xl1NKH96av4NrV(_Th_<-+y%9YbGz+ukQEl-#7E!)h{i+aPBBD^6=&VntAln!O5pD-#mTa@-vsIIa5EI`q0$#r!rILP5ye{=O;fl`QrWeEZ%;SZG_EVFJ^cD z&hV=P9PDCN-d!s2UZt~w@#|uo=3bnd!lCR`la*2zgIW}DBXap{JgSf|sEL}TbrDQxkx(jJQ0w1ECwOX+S zyR0Zh^l02d%Vm-f8zFDqYDCiP(qo1tI+}~aC{-rI?vj}a=PHPq;u|7ON;xvfi@3m5 zvUsWx$+I(;3`omBx#Rlr{8}#wW^7EbQ~R<8>=nf}1E0`D;CT?h%_MyunsI-#|GtFR4Ah$YAko z4k%J%9-@=zT6oe;D2Sp~DP^uZEMd@EAW5r?(v<|uk7m*h4#fBck?O+H8W{~a2rJeM z;k-#(3;Q=qxS?vVk+1rLUA5SXx#g_i6gnNN6H1%iLMY^oCIgxWh|z_84;_|h;7uwZ zu`V{5Rg2-ImeCV!f1*rVX3oh;3Kg!BSxdCMvNb={D=7^}ixlLfs?iQ-rj>!a?qCs* zVcxW!s6>)PQLEBgTGbti=Dz7+9rct4{2><#8=BBeyB&rRD>nToMK^g6JQ+mkg?z=7 z7j2}qsk?_IYR+yC@0xiBXTcGxUWxE}LhN}+vYk?WVJsGFcQZ+%4YBORUxy{CsVq^1 zS{|atCMpe|=}nhgD3ot`ogO*fh%k{9R7r9aN7K`pVF{pOOBcg9-_Wbz9nr$FShN`_ z;?-z4E=TGWHG{-Jq)S4gnp5WvO9+S;3PZiTUiY$WA>LLM$`ux3b+02*tHnGBLqJAC zrQDRh@Tg%4UiC3pD^&JF#aIh2J4H`~Co}$hBf;~CkkA7;IOW7hknnHri^CFB-krs> z`D92S(tIxN%jBUv+zV--ZVd5rZ8_4eps?YMT61^JzI~#wvj_N}|8>a9nZ*k^JY0Z1 zdQKsgOdCx@k%+cC=)$ z-K9#9qovoOX585=da$X(!xClFYdI?w2CC-D5f+N~fJg6d!;2pEuL13Lu6S(Gu1z-nL_bg^7fAc@@jRz6_ zuj+xJnRMbozf;Epl3LKaC`(b9K+F`>dOk+AojU4G)|Gn3WvMlK-{!O(KTX0CKd3{| z5}A-uZYT1QQa9m5YDz^kY8?kG1zZh(&6fmOOE?1I`FM2ZJp-+_qPD@Ah*nP-<(^gu zK&3Y6YLTv3OewNuFP<{o9MEQm&!{(zR;-yOeWcY?$ZC!Re{48PpY)v-E@ zG<|qJZIDR8-R2}d5-Ct2wh>K0^TS!IC?MJk)RfJt8Hy>{L^4T}5x0^L3BGUGutc$x zYA6iE5v~$O&>=k@ZKG=Imj61cBW2$N#dWx^!hP@`p1 z$9;?`MSVqxP_(L?&XwzK*gy4}VTpp*a%n`lkykZ?8Rb&k#0Sm0_U7|VAvHIsehkW?zC=CKeQg0q!YH`mYAYQ|V2k&4Jzww))5y0eDkahw#j zX>?cx&G<7}hu}^}h-}5*1j_yrWpo@34<3$K!F<+7Beev`N5vwS4Xa4fg3C&?Su2o6 zGLTt^{K>oy=afo^WPMZ-t2l{#xT}FYxwH9U3Bn(1nAIXjT9rUe?pjuh@p&^UidQqa zc%oZ~#Y2A5Y{*4u>U+ZyIMeAkctYs8Dy^Ou76}j#rd{r$twz%zb*WjZIipd!S)uf) zVdNaeC%{glmjgkf47V&hn!FGtV6ITi-JJzGE|La+BIzQb;CNm#df6_cKKv8 z*dzT&R~3YIFjWgDJ(&j7%Y{Q8TI(hoWsaRWV_3qiDrqzz=WAu8>d*6e9|*KsECmwf zY%@>7F_+E#E|1+lAa$&fjWFJF2g2=SJ%WfKF(2^2AR>%6*UCygwvvW4 zhkx>#VI5AH))=B*5wb83F2TX!P8Y8?Q@JM3RPxoNm%>9ezcZTQ$r*C9MAlctGp;HT z>41UVumCj8ta1rb1`S43h8m+EFDEa2qZN+~3V0Y+7n{ z1*bB7U|7QFq=P9~3I&o$-T^gArAnoiZ#k)^m#AP}#e=|UkW06oqoK+F1Wv@J&+Xs; zKQM9G#Qw(qrPV7}AF}f16=eBM%a2%k-s0~UU%EKGaCpHn|H1i-_T9P9JNKD6arTe1 z*UtQ5=0!7W)3;2Ar`|X9coW3y2}*gi8S zN+3ISbIpdvTR#GUx~{K z$FAwbX>22h=h6`esH{JYxv?wT*`h~}((K5~^&=N;4lBF+m6FZ*wau{F$lxg*S?{+W z3@bBs`!^kjjRc;`kqbdBed9@w-LB(|$A&h&eB=U9Q-2bs#;)lEFMkjW^t})g#VD!R{oeM>7`=Wg8cH%16!xHT7*dK6Xtf*s_hwIAB;0ZVoFk zc4a%;^5`ubJ2!5mBM16yK+|JmvpHtV<6oKqjXr0y(b2JU`t8{0Ef-SkT#x~D9=k~= z@<7qCk%$w}IeU{1{y@>8!X7$?i{*^swkj>6iu7uDupTF&hNop!B$yCWVRyluLTMJz zIct+n_<^EhBh{yVfe@z}TxQ$F%&P*dOW z1jnxF1jl3JMhh6$M{N#kVC>3vo`YLfU^ZecNJq}-vjI)_kIiP&@z}U|0vi3u%|<_E zckRkJ4^epZR)?KiCPV#yW@7omg(uB^ax$~^CH}$RoJtP>@nq|mc+$p{*xm3(q6hR< zuuandnW%HRQIL&tO#xaR*o!!~Q#0AH)4qhq&ZV#IqXF2Me%nO@INr02YM^$#wAC-L zdmMmKkI^qB=`|Bb96?83F%&=!{&lh|HCqq{YR3n~lb|F2cf^yh%YAYgm*J6F4C87T z8BS%$juPvHLzAR;_K3IsVTR;DR3@e@Ilx@ zirz*LcEQ0?i>h`JxR&ol*7Ff7%V)g>MeeFvNs5wXEm-q7Va)@VO4+=c^j1Ap**}(y zYtZzec+xy3p0qLN_Rifv|5JCft$5OozZzRS`M=~ih6|Nl#FJcrU^yXzhY}2yii7ai zQG)7wpa)7kxy4t3x=v9%2|H2mNh6aaIJXDO7+ixX`C1jfEV)|s^$^I-M>oPjQ4a;( zuoxh?cB_e;@~q?a6xYdkh&D`fj2u(I{0pmx=(;Nk!q$e>pdV(TM8;jitzHFEGVXGB zT|cxQk7j9a7;Z+j3?T|WOQ@M;B?9aMubCY(WZ|&KvgMC|P-8S=Ro0&0OxBi13se@=fZ4i^7toP;k z2a&-u7p!4S-RS@K*bmzNNF95`IKjUkIDI}0`QUy8fNhn+L8akum{}3R>jevj=ooC_ zr6iTG$_X@8@*#(WP~07S$712nsrywnClXhM(c7MkiS<&{pdAk)YjBYjdgLVRw^sUTxJS-cR%eP+v+qg}?-3oa4 z6d9eUyWR!w7ubF6JNn`{n1{!$eaCu)rK2=P$7nV~5b0zvMkG?%-5+5dz;Bn?dNMDt z9qChigt>e!#0{8iJFA$^xGNi9hsMv?hptGnC0!6Y(rx4A6>3M^z+mt@G=q?!#HR zRzyKqppLK9mho=TXcc1&5$vKdkkCULGq_g2>6`lh$4-b7`=7b`m(^#i{AuNibY z?C}uH4+z*ef!XUJ*k_x-9uL8sZ325d1hWGIHbD1!2xe>(*yACX9uQE*zns0-o^WbF zAeXL1c^_^CqwXw6MR1Mtb7+-ULnK}j2n3P!CZFmtkZLJ*1k}5~)?9V#q-_E=wgAQ< zFkzd(p6=dWb;~mc1Z$3<%gbg57Hyuxu09;~{99f1VcB~<1g32Q zdprb9+XVJ_2#f&%8=!kV1P$8+_IL>D0|GXVO!ju8_o`d;0Rcw4x)ioEPJGisOSZ4&B=r*!}bWizePZvHvamsny@C9$gKt{AlGRE9mmK zmY=)qSh{!Vilu{#pI)plK62s13-ZG9{H^oZ`N@56-ItjA?cBy(X!ggmFP-(xe0%13 zGZ#;PY5L0PbEoc_YD}FmdHZB>as>cC06!c^JtIFVzLo=$b}GlIZNW#-Qcn*N?UJXA zvmIXO2HMF)BoP4Wi-=)R&1Ok-xjlb*ndtqE^zYZl{CD5C{EqMZ=)eu1D7xl;b(*g1iKluNhvo&ZMj&Of;3!%hGb*I0xLFphA+S(5 z@PYGw_OJ^r=oc^f-0YRr7kzL2nipC>{O>!za7O40KfU&y97sRY2Uf)jSFqHmv!j&6 zAT2s`ipLr8hTIhD!OAXC(sFdEW++5d9|B+ZHrAT|)R)gVP+EKRmw$iBy;ptwd*8a` zG{+Gm@qeD%`}X6X&w&IqePCH7o9ir< zKl7#+-gMJdKRpb+HZl8_zoIjdH&07T`Afc9{7BF9uG=_}gk}IdegefI@TKQH_lnl( zPkG%}XXM{KVO6B>_|xNGcl&qVzW;{zU;dY~Uwic@IFN>>4{Rfa;t=?@|8U=PZ}zS~ zf2R1vd%yPMC(O3LHT_xX!)N)pnArVnf*iQ*9WA?yoN@n65}z~rlL ze#Lda`O6F5Z6dv=JpJ0YbpG`|nm%J2sT7C6(I35fsdMQSFMBC_M*FSrdTZz%?Pose zH&>pYIy8CV?OzFAe182AgjH*oKy92z=qE#WU{y)Q5jkIr5x?S9_i(y!n%dZae(Fr=9zX zA3W^Y!JKClhp&=6St_|liu{mf0qEeDTYCl&78sK{S++})bFdq4GFYdU$9 z18Hgcz&0d6L*QRs^2ghM{+uTTKJn&noc^6_UQ_$)=imP8e_L9*in@JF8dox8HBBGb zMq0%o@SQ6!T7A*&?|t|s*M4vDa{vEn1m1q1^Gf#z;ObYu@?Wd3gfHSYz@`-dwxPrs z0{h!P4Bh-&F?{Rl<^O_y@jczEmS1@0bDw|S7qQwmz9LX2H0c*z&6x1L*QFG*KqG^ z{la}_@wI=x;+FZP>-ZP@9QPTY`p}`+=dQc%i!`?ZHje;sw-zO%)wHP>HK~CqPA1M3 z;ThH+}dG z$lagqyo}obn?e9s9A7yz1Xee=Z~W^$SG@HN-=2K#3;%R;G<99Wc;93GbLr#vy;XST zOU`;2w*fYL0B|Zu@v=9`@YSA6Y(fZ#c%hYJ$s!@at&&_W8&aj^O+=avS9Ay*I+wfR z)-Pr&zg0hr{_z>#yXIxzy5O@vd(M5Yzwy(xpM2*h@5pl-U=s%b+mN~pfj^Fajep5~ zZ-3%xhrRE3{(ovP@sY!?{`T$n?*E17ckJaayfn%k1)DPf*oL}g2>ksUw1vwNz4qN# z-}=?7PCNMK@82K#<9!c5LY(`~C;ag4KmM%B9R-^%0N93@WeEJA9C_|#7v8_}()aPV zV)uOD_qEWgO3?TJ{Ttu=`c=g*edk+0<&J{Q6aZ{PxiSR4=k8}LKK?!NkKOl`I|5(W zxE}fSCl-$U@iTu|y6_c^Pdw>~zkd>U6l{_JV5nziLE&SJX*m>ipzx1_NfAU{n{M?s*H~I0?&VOi(Bysw)zvhmD zO$`7Xq+C21j2S&Q#KKeoD{FCgoK`ug#*$eX$jT)#FC=q*S=nSf|BY8b2er>_JpAil z4n03`zhnMOUuZ4MKYQbI?tIndfBi^rkvj@DD*&*KHW79pLJT~s%vkWedmqOXaw$f-jBGWU=soW+c=aM0!z>LX9_QEe8=^U3%>G{%fh~2 z-hcglN3LP3;V0bw)7xM4=Nq}BU~>Tg+t_L!0)Oac$IY+%!QF?YrAwg?e)O9c{<8F! zCx81~=g0q)`R~J5yzcfecNAe)|ykdvE(GelheB>dM5?zuc94#-058Z@u#NS9Iz^kvjzbH0lSBbep$kp82RrXyQY`4LIzUmVnyEF3Z)bjf*?kIRU2EaDAZXXg?j0KJ?Hjozu&(7U%ypcK6>D1>Gjpq6z*ts0BmEEVhDVezxsx^JnJ*C@6N;L zPrtUVzw&9nAb*^(9(TqctQ#(T$_0<(j#dW1HuedIz`sB1kkWcwTfOU*Q@20z?00|R zcj}XT#_8{Sf$;h}G4@%H%PoTU^Rwo&6Z=20zqo(j>RqcZTTQN>zVhCc`U<>!|MJH` zY`@Ub_m|$f)L6QB@mGsCEb@zIE&OodO$*w>>iiexkAk8P{yf+N5B9)=J@8-;JlF#d z_P~QZu-OCtSs;;}JRtIUDNAtyu@Wm+f>^U)TykRG(UZoqxWYXP)R^NV4DYmCPnZSr z)qbg1qFHXm5>%qC5!;oSB{4;?8?gkW5>$W0g>w@^lmvGuXU*W_9M6Z?ZO ze#zqpVK(q|FbbEAUwFq+fJUzi&w4KDkHh%ze;-N|@s81`H{x2p*Nt{?rLJw?p?|aOnV%dRiZM$bKNK@R z#lY~dgW(^u@a9nD_53AOpq?ssF4hfY=G(^x)E-Jdr>4UNIej42rYq+}oj}dxaQS>A zEuQ(xu>lXWO2_XrXVTnVCZC;$jS$J_a;v~F0!%Kn_mN+4RA&@vE1ZU$p zz)KvK-~<_9-_aFgCd zHa@=bwvF`0Q`SGd{?7HA)-QMbz2g@hO^0{wYisXad(9fYwz2w$t8ZNuR-eA|DWHJu zCwzw6zG;2kIsG~2OOUYr)jZ|FPpxdc=*E?6&&0QH+F0LqZm+H#!yB?cKDXL@+ZS$y z+#CDR@R`_2!VeX2R(SXfdm?Yh=91@lnG<>KUvQq|rBCFwkA>%WsS|na-TpaV@@zWf+q^M zJC{v(;6&l}0?rB#pTSP#J-+wxpU8Whhh2LHJ+abu&$d2WbjIwi za?avwYioN~-?rUf*<4u(pE-3>H4dqA*7Vn&dFhGOum{RHE#MsCsuP7D7cF3}vjMb! z$%zHnH8`jJ3r^JD?sVsCe_i;@3r`eoZ-leL!)LBMk@vV!?3yz#o+<9!KDP1gyyp~m z+#f!3#fh?y3-$1s%TMIB8|`eJuQ_wsiF(^3^?bdrJ@b6XzkU3a^w1;F=lHiSiHFZT z??mP83Y=9weCD|)@*WrN;WN)UiPwL8H}mWhdF@@rS)H#v^URYB=C_B};|zA~nP<#e z?0AbGN(O$efQ@|+BA1@1{c-UbK6A;5ymsx+*8G|?&pOG~4n3WHPVs4oJpDx3_7FQO zJACG8C-NTGEnIWvsVC}f56$!RPG5WG;u9Op9-im;VS_#8MCI)YoU8anC-T~bpXI&w z%sy0nPHZ1av3XAM&8IyOKC^d{@IwWh6@JZ`-IJ_-=zYL*!X4@9{r@Xg(kuIS?(grt zYww!fkM3UY`c2o1kl#nJosaCO+h5tPY_B={PGsxnwyxX!_-16|!HxFD%KB~V+u*kU zrE70qyL|P%tL)0>Kq2tw2exm|Z)#u%cjd|@?jJAwbp5=ommp^E0`=Q-Ko@j(Wtl6> z3+rP?u$=RE z;eg|$fvBnDEOGaFVNvrP=g|0fyD-%Sn0JZm(hKV{+p`1}P90QuiL28Kztujs!iRptrywA*PQW9l+X+#g@KPUpJ}1>{VFZi(yU3(K*&3^_F$aG518m@h19zRMh% zzur!qZNkOxq%W+?V?BNJpu$VsPe1u#okgC9_S@&Y&D$eD7dTTXfDD4Fm3ODY1d&B? zAxEaHrV=IVRku;XsczgGRz;4iy3K`kIUFO}?KqG(_1`6Kx-Ts6eE+>3$eH@@5*ObW zmSgkZ>kgW8iL3Ani<sYitI3uH;wEi?*1<<@BGLP z0XfsiUgA@Lh2_{HJ9yBXOMD!#u&B9_eRz$3dtj;ysP+=y6D+LDEV8Hf|H+kmRyLjc zf3W|yeR2Oedw;w4OM9KY(C)W)KfL?a-R$nOU4P^HMOS<6*SDUvHd?!GZFBXbtM6F7 zVdLYgS8Uw0ahtQXapl%Go!Hiytq-jH!WWCWwUNin)yTc8u_8CKHRx>Ad6exr}zt0k2%h z`4~y$a*oU9GTPfQm6n-&KFUO+j_1#1w0ll2M$u9vD@l~&d2<=Edg5%k97(*WW{!fY$_yjHzv8dx&a~OkR zyQ*@A%4C7xFrwq)d5j@@XGO;&LM9TAQfYxe5{hTt3FgdPULtom6x7BN7$hbgN$XPJYL8#gg%eW-d#|c6cbM+r95Am*8*XC zZ^7~ck<8)QNY-)bTt<5n#Bz9sjYQHp(Q(OKMtg6O&8HK2fyohq;~8@q?Y#vaPsiyr zlgP74Tt>SC#Cai=B6Ccp;8>r_Xm3Y6!3s2{!Lq$q*}2?EyseJ-Qj0T?a=a^oa`x@+H>%V-Z|CXvijc%I^!wQtU4v^zjF6PFSR zxsZ>oePb@8-2q};Oo~c{bedlKx4DdV2grzQBp+pIA-DFga~bUpkOQ-hbc`#I^xC6y z8SM^$OG=I%)r&pJGT;=5b zG+X|fV>2@PkIG-O#D*r!U$ewcAf#VnV;0gcvFmwszMCa>o)^z|^Ib{VX0!%IPn%k$ zY*_7vGHxkNZJM9z$nWTaXmCn8H*wp4AnVQJM@C#vCwcURfu!` zn4hW*6Sx+Yf;<-MO$2}17xeK}vmMN}$!398BLTvQ#r&sx)q>b###q%8l+ux5U6mPP znz#+r>HJU2fsv*OJhie9!@z1rDT*VOp0*!4N=FN|(>U!bM9>CX2vKfUzMumS|Q=r$~K>hOkyE8C@)K+=1z5 zu2qDI(5#*UyzJ$d-2nQLaT_|=!Liu61CVKJL%L#J8&9h+{l)oNN*^Pg{;}OJH4Jr@ zlt^oVq_ERp9<#S|=+I%xc1V-ixy2zZ7B17K?X0*bGBx7KxvuOp#4BEN(=@5s#+}3I zpzxMUKvJ_jJ9N_JzKii_9thv3r@3y zosh3=s?mk^$bL_7n!oVK9$C}$^myE6hWT_RnDl4m2p#lR^I7oCdLiiPk5kD6CN{={ zw7)(J`3uNHR&SL~dzpwB@)?H7CB)$bA9M(n9BL6i6^@KcY&L85PZg+^#f?jzw71vw zv}m;D_e8o)Gg@j-{Qgcj*a~IMcw~YZq4wz#K3pif;I!%>5Z}ulUj=PBR8xIHr5M#*cmI-XD_gjL0MsTt1^emTkOC!|tcEy3FgQCtstobwAT_Mj33ysZw`##u|>-;J0LX*TipGbgXi9#|W z;w&x3C6dTY3!1wO^8tbrI+bH)wnr*`frGx^>+=PHCPxnXBa1dBg(?MJGNy;7FDPSa zw#=hZA+IFCa;6qNJ+R1Fsb_T93m)_Z-WQG3*wZ<^ndpy(nMB)Q{dF>wj8lbJULEKf z-&g9v=19!qMka2`(a>0uFzys#mS{sH+j%u1%ctwI;0gL0gcYF*bY!7r5Ue`rd_ex} zS52dCnm>C9SAF85E_is*pN^8yIkbV+f?24?FMqGu-cN z{%pU;j~(mmB`!G__mzRg(F&ODN@GDjRU?9(7@NS#Y0|2*4Y}&iXffu(GwtT;eM8aw z8lB{ZwLI6ShWa4hlM9|W5#y2x1+o8^LN>bmevI+>x-~Q@M7TQEI%3sPdx>~ zhx>~wmU$KY+`#?VM$ojsIe)-?f_6WT+CqHXrqVHk-k}TF({?>}rZ6WB_OTmslVrV- zY06ctq!31L(rVGIU?e%|@#Z8aHE`XN*9!_ZET`-Fj>gaxqlIN#Jwf#+DUHo|gk2*UeK=$&JyCj5Z+*2`CyY5xn1 zZ?N@IG+V`zR#h66qFI@)pKP0-pp}m72C(l&H?ZuE(fQrLp~)Pm*%DjwM@%HoANY

    4Jucz~;)zpI}t2`B8PG>9PWL)l4O4(t%b}B$q-C-{Z*3!V)Y=QN7`DafR znxw`jv$?#|vII<@Fs7$7JwWg_yEQ@2bXBTJX%%A_uV;e(HW^m4)Yy#q(XvE0yP;$# zLn$VeMbREkR741#*TEN9q|6)GiG-v~+t9IY5PR$?jA6D+WryBn^JW})jcNFmS~I5_lVH#g{VBcS33ceC*=kTDRsmNW`7~`3rnT{_Gmbzpn9|e! zG$FO9XhCUuaM@z`Nl+ToDZ%P`bGec`uMV}e?l{Yljg1UGnI@_9P@-E{EEv<#m_kR> zRLWCqd8MlCt>p_<)Y|;#nG8Q)EBnBNdP%kHPi_TPW{Tyi>4Z$H z1n#p4!#hkAN+cQ>rYD|86b+|`J<=P`D(N7hup8t72Qw5IKOM*uc#8KXm{L(~C=@9V zN`q2`GI1eP!JF8ipGd6UGULdCXeL>?8fHbAWh%IkAC7RWFzyQhBhP4jvpMVwX)=ha z8^sw%Cdr0Fy>T>?jW!ab8~9Xg)T~f^B$~>X8g+pY>Oz2GYOLz`@T~4>Pqw6GqdqI! zD#X!zW`G8(12P*gW)0cuf^_U|UFpYK-llc^OoZSrfPSi^hgecpjd-cdv?k#4TB({g z`>#Q1V7)A5AL8a4Z zq)p0MnMhQI5hK~8g9$E}QTTSCSS@66O(tdxi~>&AaJe7Gus|~vO8Mw0-$}H24Go*4 zehAgV4V2`%BMbljnNDcT2(2cm6_Va z7^|FDQ~iNrgegq8o}NtWIS!++4!ir_t(lM!PK8;W^pygUgc%H|ohBb9xe_z<#&BPe z>UwozobKc?45THUaSWJHKbPov5-CIK=Xlab5mLJ_Zs1tb6Xk+2FqQXMT{I@)PUe8a zt9U92Y!GfLk``bMVi+r>jZvpNE-Re~S+zvNlMhh?(pnd096e3I3dLMW@{Yoizmja% zd{v6`rh>H^$+riYdM4eOaJ6PhUj5X}uewP*oC#}6Xu=fw9x_>@V=C8(x60#g$TGmv z5ToOa%8!QnfviA;Qle6^$aXSQtLMw(IJ@=9nNsa> zTI1X95nYugdNZD>MdO`(ffaQg9Moanj=x7Iy|HSPPdGn4<0$1j$x__QB-kv0SHnY} zKWN2y4R133NT?64T@1Od&*T(t=zNFsL(BG}GZ~{ujFc<`ZO6(MA1vpk0h>4CMhxxt zeWntzcy1IDd_*%Pt{xaytxMTRYZUb=Tt$+ic~7Wmj^lOTc+&9)@uc1cJ0T@h4fw%F zm@^p$SCxaws?_j#T4tu6n79euY6nDNtO-P%Ra=Tz>ATbUqPlU}jAN1{;zKQ!Pm5%a zp>;a#s}OQY3S}+JAGRg|H_H~r-bqZdoL`!8)TLpr*{qh@o(vA&=B8rBj-M!{bJ=*# z7YJ8;vC0G|n(-hVa=rw4?)Prg4!gw(kCjRVnFtF00pF$vpvP*BtD|086yn2JxbDYg zpA-@I{u0()^ZRv5@R(pzyDK&(ct06O+YD2QhtMEF)(3^Cw;HGWVmQxk$ukbrVx@q| z=8^?Xj7Qw<)<|YBHmmR|-qceG!<)+}sbq0r;@hbMj>4c@XqhM<4)Z-pE*Nx=@-ZE@ zBGtO;n7|Se<;zD=bR@34>43xI!K!R$Sn(7qT%gpIhlHNjD*?t+?<#1f-mG&a4_^)S z$h9xbI6QndhRH%%vv?J)`xSp0Z}_!BWo(?vWlE;s(i5^2BfM#0CjvRj8H1-%q^<>e zQn;I?xjbIODiOMs7$k$ry&)g+j<26TW^m zSS|J`k}D1esu>)|f@zW6zSi~XmGXCb9GhuH(|Khus)RXwFe)08o@HX9C*IWk!A7{< zK^38Xq)Cp}$d_JO#6je%OQ+-dT*6s(fOqfItIzOG*4Ora_QKkOY$@Gw%`as_=|Hnl>19fxK8jj0 z8$xSbr-ly--SH&Ztd`BakIb|x^J2Wy4ur6D2&}uK*<7ol=g@YDnP5tai?DiTSVbpg ztROhD`=S3V-<6`#a$kx0!?;>)bYsm99##feEZHuY!A7=d;eJg4%QMv@?|o+f&I_|u zS*vCyp|tGH_!3pHphAmrFj8891QtjLfvlk6Y{Z|dk!Aw<28h`2c+^h6|9_G59V`1E z11|v9_Cx!dd!O3-*}eAOwR^7J&&+lNb}x24LPO+VZR zo4=Zc)r0*uT`C4dqKF3xqh0V=o?uHSf<8P|f9h;yr!3BK@wb? zZhk{zbnf}4Em$Cu{#*wP#yJ>c*$0lnA?b(3*-QqT4Kgd%f7tzh8U-k>2TdqhD{ z!mb$Le|-u+hA&qcw7u;YCMz7`tuY=Da~;o_i~cqGoOBDX^z>}pt6LFTb7n_;E4Kbpin^`pK;Y$KhqC72kuxc39 znwaf`RAZQ~I}W^k362AAUxMSub{x+K=l_pxz*G94-o1P4Bd%BP-?{U?=}my$i#K1f zvA=qs>)op_UHjtNdv`yx_C|0pQQEuK`QbHj<+b;z-22y*w(>;diZo^4agX!+(Ni-`f z>+imx-55;@VP3S_F*>*?>Vd>iuW4eC$osPWY%>`HM-v#$Yw=iiPIO2lo)eT z)u@rwQ1lcyPVH)pP)#e~&^idwgt8Kb;^LZ=61a1Uarh4s~Y;rW=>LbK6u z+$1p5%OsV2nryIQxq=Efuh*EipYJilLcCvy32JC2q7+n{F-45vv^z$-yQ~x}R43`s zDAwq@tub21$Bi&6Nl8oJn4P<7VnJ8CU9~2cxMpYw4sGQeiql1{*=?z6)=EVygE*$8 zivqbW&SWG=BipPE)mRaw+8FU_**m^+Y2rdOWO@4Bx<)6V_&fhZgh2yQK!O zSeI5QPdyir>&ztYA7T{E5q=S^_t09YV7bAm#It8I*uqc$GFJ=ML=UM3y!~J<-ZvUjs-&k=^3XTx zo2e)!Y+Wr`G1dkuaCBc2w0-&*=T?Zag;dd_kC_o5}0Jy!rPNt9~HL``Zsw(MX7*KNJ$~9 zLIz55pji)kluk<*xlE4=H^&l@Ad=&}y!l(RdSD@NgIZ6w5UhxjwPM3@XVj=Ay^XZE zrS5!b*_tCWA*mue<_2P=kk8V&NiHSO5ueeib*g=VOpBQU!x}{#4Fol7=Z|I_$;Kqr z4pfqu6L`2pq>DaP~bHs-NvHA&hO4RGF2{03j&*t4-!!uPb)Kgxs$+Y#Lzdhp^B}`d2B3j=vtavIS$n{Q`^^MDVD2nMdBN1)r z{scZs6zkjHfE-|i%qrDhblhsC0%HQaM-lJ(>+XuhN2qp&N2`hukZXgOm9#c4nQ{2U z5|u~0MQ$8Yd-*or8#Xfqqn;iV%a&&%`E zfWt|=lx(C&Y{OfwrFA+qrpJ+>;MhIjh=|QHJ|0vQB3p_^HLcR{2QpzJEKYbyQk9A? z)a#e=tlQl3%sAYnpVS+*o*D=blHfH+AD`+JNHIXyw7lBGtN~l@r$X+W-?Mq(ik2MA zG#HK^O#EJw6oweDF~e%YZMwr_(ZdEhp1x56uczsW?GHj3S|=DSgDY*(B9>s%OiJ`~ zz2Mjrhz;r)k@5JNeJ0>1L_Q;%>mQnNEHPI)7(jv}QAg`+ILs(5D(8zf@>ruIRQc{C z+aWvMd_*HWBB{4a!gd~c{j$zG)2dH*(Hv>&?iyW_^8HpxR0L1l7^MYING8+%Ku9Uz zEgehhJG1l63I>+1A+8<`mf9UI} zux?tYg2|~r+H3OhL5LrE2*INAK7EvA^QjnJnY6=P-XoX&J0IQq)P>o~3Mq!>lj=Fb zZ8B=LMW@}3+L$L2;8asHs$8s#F~j;G93Sp{Vx|=~Y()nnv>!4swo>b*61k3E7DcQj zwyji6Pvibz+35Ls*>oKAB~-&2M(gcj-K=Ob#x>A^nHofj9q@rPHp!Pu#c|M^m-`sz z+4`xa{pvL{jz~mq#1mv7Bew=AUsxQMnn^N_MSzWxYH#T2csulDjOrbHY7?iNZ( zDKs1C7mIa?>@sbE$hEVh6eo!hI+5W=6)e^=1LE56%{aobI7`*?5xi3=lzm=F?|A!? z5FXJ1)bHT~T`ShZtC2<^z&d_;#CzNB@MmAczG=A`RQgckVFH%Pm__XUh*W` zWKJI|;=JADU6&8)res>Lg-c|1D?FzUc#WqK|mMIkV zQMTa*w?uwzrd2rFQVU&GP`ny91eeKMiWbJ1CaTuQ;YctSmJ_8;s)_P3dE*)TAG;u} zv_MDfw%n!{D@XBex{232bSYX>gHdggi&#dJXohOZSc+v;=fz+lvgFn4>G{9csjTcj zxPQ<7E&DfvHThHa{$cO;_TC9{0I++P@BZuVCw6~v_w~DlUEl5w$OP~{*V|n+SH$&f zk#`}lL2}4x#JTg(&b>Q7x?}9%JI@3+`~PtJXST;qWn0*O$@Z%A&z-;SeAAPQ zq$e%#qy?U|z>^ku(gIIf;J=LpUhELoE^lWCWujNt&4I<1@$x9zv^^Q)sLtpVqREe} zWPy#3@pfq>SvI=0w=PKcmIdkFydd3~8J(D+@K%2)_7jbg!M2FBO~2J&TafPEGdl3e zYOf||li(pKiXDv&$yVOl^)ougBwJNMt_~ZenAC|2q@C{l3)1~Aq%$Y2f)Z^f`94o5 zU>06yZRM?eWI?(=I-t{AL$RTiMuQ2L8;+U0t-RF_EJ*kJFLDU0mz!mLU=Fz|6H}Y@ zcwZRKsuiYw6RaRjgPM3>YIyp!i_WNQyCNcXv!SScREyH={%(-j%#BVFBAUTHBh zWihgwp>nMeH?l^PhM_bkl}4A0*nNK0JEI$@4Z>_25spm8BWzV;Y|5<~P;9H$ALh#9 zs50T%DjS!TiH+{ni;=x*F|x@_xt@lP$WerjRTP?S_oE}5(3SVh=&B`=RoPOtFV)kX zibg0lI>#jo(mi8FR~ZpyRVON@38Dv|C=pwEYrk|r*Q2=xSIy~@T4lg zbD)b1X;tVBQBMitphDZ|)~;KS?%D&wGrD9q9!XWTo>7T2M3ZbJZ2GOHXLJc`)K=JrImn|*y-X6(#2+U5ur^h ztf7?2emo*ol0$oWf3P6khhcLxOr=3uc_k|kyE&1Maa~(^Td=r>u7MrYVPiZ@nyfS= z^|6i2@xle^uAI>+B01_=6Hzgv;G=^vd8m13=4+ftCoEWAYWn^^vZAl-{rK)nk@xRp zo&T`)+Rdk~-|e_&^^@S&Pt?!r^&jS(Yd2l~L+Z-P+Mgct&0Z(`E6O5kDY5?vjrs1QgJI(WAl%J4xMOd3;ZQBP$eRoTZkR5US_HBRKAEKoL9 zD`bEUv=}IOyz!)%K*8=H*fRC{$~6Jf7FvMThgHi=Zd}L^?sBq@2byXlJ1UjTo+l;5 zRUPdWhbfvU^y48R6D|%~es{1fn84KW$4vd{<4kRTeJ?FXQ>k1eg@c57-$SOZJwc`h zo1i&gSrW#DF=);S-)tM1yypm0<7%GlX4+#9ty_{C)j05Vc(G$7%Dfu``;S9rppLDU zSaSO((A2TVO#P|jOl{BGL4gD#d|UvTroN|4oqd8#O_Dyo4t9H~azJjhqdXZibEy9) zQ?oHNf$?#=H_B(KD0tY|TO$gxZfWJ5(9?QtW>9GQ%!lFDJI1Z|<$$t--+LL^uyqe;% zJX0n(2BQ)!@K9NOR4Fu$GIgXjj-|5hJX$V>YQ2sRE9su7G}6chS&BqCsgUYrGEGs` zf=Sqc(2tq=6UUj_o=yq83nS%mf{NkaQ>K2Ryqfm zi$*h+S4&3Hw4%~xj6rFw$q^k-s$Pj?MM>J4aGa?h|3;q2!^XgL zk{4pU{Jmsq{)w7_P<$gHwXH(S-zj@2Yx*o!!h;`SYP{!T^oZe&7vjN+l%~aW-Bi3m zs_Y@#X%z&6ksmeDcsM&u52vOkA2apGk2AGBAwwpe6oqt5q`#N?Y7hTJ%|IwD5o;ud ziAk&)Z}udGF88v&qb7T};o}4^#@A4v6qE;M4o~L&oCr=|!{9wLwO}<0Uawakm;7$n zR}+t!`eVnL+MXVdE~KOBXq+y5Z<+du>Z{3KS_R+pm+4qW%*sB|@LIAMI?Aj2gMsNQ zp?S*BW%Pg_D>n%6G7BCZfXQAzE7IV|-!IexsR?-N4|sL-F;jo^I8)p6$t9w6j?71i z==aiO&%-}aeKkX66KI4^6CjB#8cRvOm>`U2%bmwBVIuWzBi~FasxrtIMuVi%OHzU_ zR_hd-TtJmN(Ok2hck>2|hv2XndCb&*e4MH6t3{d2N-`&9;@}4F_skN;^MtJyqeCq1 zD+J|eEi+5V;b7wtRuSN0y=dt~q7y@&Q5+)E={U{a$F#vv<|rWqX(IU9{)iTiJbd_mSO)cOTk) zaQA`T`*-i#y?6H>@cO`AyLar~x_is+>vl)G?cM6`&AZZWYM0r)emAu1*}ZD_vfWE} zFWPnPuDBj`J>q&8yi@R?>jBsOuKQf~y6$n^?Yhf#hwE0?Ew0zOMy|H2>blt_xl%61 zb-gR(^0=;YUFN#fb&<>IT0tH~9zh;P9zq^O9zgC#?nCZH?m_NG?n3TBZbfcEUWbg3 zHc~}yMkMg|0fStRgb)vM6>=GJDRL3wL{@el-Fal^VQ>rL!JP+o?%%l&+=jSk=kA@m zcJA1@6}*!0x}DKZd#Ac{^NzHW+F^FC-wEw_cCOmFZ0FLQi*}qlE8CB5KeGMs_Cwnb zZa=VnKX^~!-tBv~@7}&^`;P5fw{O{g-S%j^y=!nKH$9Hd7txM=RMB5op(9!aNg>?#rZnt$k}#Qoi{rrXUfSq zuXl!=9_Llg%bb@wFLFAaD_f6lJ+k%i)Ie z>$XN)?XBw8&0Eq|YKz&rek-))*}7`$vaL(EF4}T#t!zHJ`N-zOn-6V1xcR{5{hRk~ z-n)6v=H1}k$2&G}-MnSS2iBq zcx2<@jfXZK+<0K){*C)K?%lX&>Tu3tS4s zD_l22yxdiUc$w?_AU@A^6U67bZiM(8*9{P#?J7WgmP>~COqT@lQdb`0C9WLAXShU& zPj_V@KFyVZ_*7RK;>9ij;!|8Hh!?q%5cgd?#64F6;;xH>=yJs&BCZ(39Ty96+r>b1 zx@d@7E(+qNi-fr0A|S52q7WUf2*foP4sq3WJ;W8)byM8?w(DAm|L(d5;T>+<_bgnS62w)qpl#tue$;e|HXwt{F=)T@vANr;#XWgh+lSjA^x+=1Mv~p zX^8*ix*Fm?y4(=|pX+51zvMav@r$mPLi~d3B@q9?brr;iT`z|Ce_bzv_<7e0A^yGV zN{D~wdI7}0bzK4RZ(Nr{{A<@`5Fc_qAL8d+&x80^uIEDhtm`=t|I+nrh=1XF7R1lE zo(b{KU6(?9&~*vKKXW|;;-9*n4)N2jr$PLb>!}d`#C0*mPr9B0@e{6#AU@#Qhxl>V z9>kBib|HS$<%0OfE(GF7Tssi|$h8ge4_!`(_q(0mwF>d~Tq_XoL%t31{m8#V{9WW*5Pt{xCdA)Hz5((7ApZvOeaOE;d@u4S#Cwsi zL;T;!zd-ygsYsf!Bd^hs{ApR=y zC5XR*d=cU=BVT~{OUOS!{6*wph<79Z7ve7&35Y+6JOJ@l;e_*UePA-)Cq z2*fude+2Oi@`n)LgxnAD7UT~gz7hE_#5W)xg80M82O<6t@&SlHi2Odp*CW3N@duFm zAifTHKg91xei!0vk>7#%8sxVjz8d*I5MPD758?!QFT^o&FT@e@zab8h--0+m{ujhP zc++~?uk?`LglHkZ0kMnxI>Zig55zX|YYvPkzatQBmWa(1$h@l4f%P9W#s1|s>shmEFnJwQ98Bfcy`L1>~I&W#kTs5^_7lJaQYv9P;B3MdTe2v&fG@%ph-vm_~jSqJZ2A zF@^jH#3b@Ih&=LEhzaB^5IN+{5aY-hh%w|%5Lx6Fhz#;Zh&1vBh!pa}5J}{RAQH$A zLX0A>hZsSA03wdO4&wF5_d~o6c`d|ik=H=H26;8aS0b;1_zGkK@#V-EVi*}g3?V~^ zL1X|ifb=0^NDrbPu^^&I7ord8K=dMQh#tg*cp7Ozyc%gjbR!LjFGK1OPa!pkFGUQ9 zFF~phuR?T)FGea5Uxa87Ux<_;UWup>uRux=FGm!JmmxPpd_Gcy_&ntMAU+$p3F5Pm z8zDXuxdGy(NCDy{hz#-Rhy?LzNFL%-ksQQ}5fS23kSxS~Bm)sa(hzqL0pd22g6Kq& z5VsH>;wF-SxPfpGSCIG=cfXCqApSeZGB^GIw-5&6HxU}*HxLTqzab>Ve?^D`j2>X* z0C9+4N3MtXFUWNezlK~3@vF!+5Wj-F65^MUS3vw{6uo1`ZH|_)mx* z;y)rN#Q%r*Abtt)Li{4)f%paFG{nD0u7>z`h#TVHA}@pZH^?c7e~r8p;zP(wAbt+H z3gTZOFNXM8U1K++^-YRbeww#-v+5DBwD>h%dncDd3#)miV*b8mcHeR{0xBk$cYyCIY ze`r0oewE|j9DnTiDX@xv&+aXbwj;9k`Q01AI(;3i%6|o{!LzQbkZ*$Z_0NOVG>2Ta z^Hs1?z7wpAiJeQgzXaC2?*J>@S8iYA{5$8pj;F2N>-;(A+nl5Sc2oZ+E4+XO+>V>p zu2-y{X7$`&x6k7Zdp#as(HruD)J*nUID;8=%WT;x-67O|6T;Cyzif}4!(Mi8sj{tk z)b@)4j_&c)!La?P%F&reZ9f8VwC7RVw^kkIJnBPwx0a%A&Y=$3kC`2fdDMq&QlCQ| zu%D|sYV)WMm2J$UK4ih_JnBO}sL!Fs4(VN)M}0_dZ65U@KPk_nwx4!5)Opm0$}TNM zt<0l7q{+?ms1Mn@IFI^}e}CUn)HlteKGav;IEUJQs4Z_;in=h5+I}AEkmpe!vbQvk z`cO3FpYAxldVN=EmCa_c+%Ib{pZ4UXr#VioU0*UwFK;Tnt}(jW?CPsG9`(z@DaQ?~ zM;{D@?OjHE30igz?c>AF7v`3rjm@F8H!8gZEjfqQ-k{79wA370dl+9khxYMq;tLZ? z&_7QJgCJ%qBclF9B@5_T!>D>wD z5mCNO8g&i>@2%!MEiqBh0`;n@l;l(q*K~8~!;#OD8KF z2Htg53@X=4*ZRd^$(<}Jo^we-&(A6vE0C$4YUSOjFhOKdT*#3r@Tz8%tXJJe38%Vo zZ&(#Mvg$Tr0mml#JwHS76O{Pe?w->cpPvZ!h)k}4!j=jaFBAwS79r#$lTK&j1e@kk z(}E6Vkv%^N>|+U7&*uMyN#7UAj_f~5>7!G@g4CrzkJ;aj%qQ&!k2+2@sVY`gvtY7M ztE%|0z%;*Uw&(M=y$OS?oT9NwQe1@_pT6Qd-HcyL-_BD{~YrR}-<+s4E6WOtnkr${pjFs9v^Putjy%uzF^9 zTpSpdQ8F4vZ#?zX??8mxhj4p@AE5|veGmj)ko@-ifStBqyQ>WzOW1ulV9y2HG5Mt0r;2#)0k=ZOATY@#AU7BtAgM8zr%6o^@NU#JIte2h(xI= z54E=cbkY>-8EH}hN#&S9n32&`L9SH`;9Ws7M_zE;bx(EKw6Cwzd0irEHOW7yc$?Yi zDU0a(I?Z_LEPO^zwZF8t~iw!XGZG5*Psc&iB*&OcZeafK!&|nC>!Q+AX-lx39vQ|<0jUI3k4~U7! zCh@PjqIvanXui6oG^d&1vFTqAe)YI|!{W~U?Z~ke2s}rjD&UCBP?bhe0nbrPl|R4i z*#sK?a;oD@ADfc18S^>CA%96}oh__LGLsrXq=|Q8B#|*opLVR4UIq=6Dl;&o2f`580@7+LR0I~6H261v230mQT~d> zl4gp?r|6PYABOpMNXdD_3(mKvRR`V8bp3w?SoZJVwtqD^_jm99;cm$FF;~U4hP)kF z-MM||^!EF=Mdt^dH*bA>OWXX`=36&ku<`bdtJi;W9d&%fal_iz*D9;uTz$vtmH*j$ z2l{Q}O-(>|SFWV{0iV}QQ>GA2#`FH38AQdru7ji^J)_gKP_Gef(BbirGxW=?h&mjn zOewCnGITCmW#X zS%Rv7H<-htw4c%2Loezpg%*}{I4rf>hCtqFAg>6X2#?0mSgkDtN8x12q$ebbwfjt` z*77#=aXXl^+-TcUWNTr0kB#t_e%s){f<^EsEnm#l268wqs1uWj52^u!2KnCQab%E6 zGRcTx=}foFFD~c&uILALZ|eh5FTUyNSLX9V6+9H&C&z6xk)lD)>s+WL2kUV^_{iQ} z8IF3aoaVWnp$r$U(rjPgm~IxcZ;?$6pxoA*>H|z%QyRJQu*Q0)uc}1BOP)nN;AMgY z#)kr=;ZxLlQK+`MdYm;E)<^AXN>6LvW&s&fx6u}V=KgXp@t7nfE~|AEx?&Ji)&8c@ zfrB39HeDcZ>NY9Pqgw2EXfeTFV-nS~!+fha(Mgb@f{=$jBiU;=hm7g#cJ+njo$ods zAZHqQDMO((RLlu+Mv2f8njB(PQO$aHmxYhh9I-NrsT&)mkG>I14yQu%~f zpLptHGSUpv`C3JC8^N@PXWF=AHd|UMn3{M>9G6fR)@SB6AXZujl}>s5KEBWSn4DpC z$P%ghdZQ|D&q5S1x%`sHBpS2ahmb`y*m-61)lQ%84f^DAvXKbTfp zzpZ)D-Wjcq=H!d0R+8ZNBQ<2%|0kTi{$ z7#5?W-5%@hj>vAU(+ZAgLeqVbn33!nRb4TZNRO22#H4I3Ea|9-sRMb_h>2T`DxH|H zgZ{{Bnxdg+6($cdy>LN8!l+@b(U-GT(pS$G<%Q*)A2GE93o2HE^XAP|j*E;fBd*r+ z;H`|d*D93F-h>g!2_8=NS}GyN7Pp`+VhkW^8Zn_sH#l&+sg%f3?Vcy+myJqkP)v6Q znL%_oY-eKw4J4y(4ki7@825Nk&jXuIT_lh4_*=DV4jj ze;AMxEh|@Z)fx|$=%ql7 zPfU0%mRMNc`4OWYSWp3bS7@P?)B>Q(1Mig8QsYr37w1(ej?&>eH*_0jUUH|I^x|@C z5mN%9rV*2l@_8+o%I2FBpJI9ZZWOH>u~a3lf&&}PFuH1^A51b<$Hy*QrMVH~x1S77 z4bZ$zIcU!D%R%Mz%Yn~ zCc$9SvvB=q5vtu*1Tv;>^e&jCnq$=w6 zEL^Aa-RAp%oaxkLVvWL4!|-MkoasxXqn@@e#iz31>6C78Tr|0OyD%DMJs~%-xE!0? z+%z>Ca2wAAJm=sKjlgM>N~Q%jcngkC)M*PePg;__ZZ8K$&4EwMi{`>rn(sD8&uDKv z+l&^-AfUHuxJMp!)mF}j1({|cO4s`_VK`!2{~vpA0_VC_-H*?{`q?aDhmaSBP`K>cy+jnhu+XCC)+Mn>|yt9{=?=AH<67mZQQUlvXNtGR3oFfKhlD@-QmdX@T{1x*Z54V)h~lgEd(77 zO!Lp~$Za8>$Thq|9V8?6dJHdkv1{@{7YRsJ+KECNql0~{M6tbQlG`1O+z#eI8xMvV zqVIFH%Q-}0lwadc#hJIzFvntO}c<{yZw>d{v0UR2a>FWr3S&WLPAR$iDDog>i3l*su!9%nxOHJp)%ZV zcjUG^hue1E=?@Q>VKIyhc{$&0B1Mo4%3DE`Ufs`YZ5H7GnQX*Erzmb$8@biyaO+G7 zc&pkc+%i;+g)2qAp2eK8kZP!1A4eA}YNR2usuw|>pxt&xZaZ_hEeD&$&Y+IuQW)z| zMA&<4Ucpa=Y!3+xF~vkDYM7=a1FAX{V6@sjgfC?M31) zKck@4SPJiW60vBp+ecuyl>_azHFDdU!|kw)g7=*Ez+mEG45XXXeSjA=k}i`?r>89n z{qhjfxENHo5QVz2{>-II&wS5 zOq`YZHg2~va$A`Lt%bQYZdV++73XkkVTz2~EsxxmXMg$cSeO*!c7>7KIVKc#01>7_ zYQ-Yv45maHE|mzbA^A8kx!#1>VLVxhs)Z&V^?CbX1S^f)mgYcfVakl#Esoq4=WuIb zevI3_apd;KIow*92IF=MBe#V)+*+9R;&yKsxxHZyw-#o&xZUeVZm*xit%ZpyZuc1@ zx6hdUJ-lOKYKq%^`pE6m=Wv?@@3U+)?BdhKA)y3_cq|T!IfSYE2c#sGI5z7oyU2KC z7;SL7`H|cF9BwU4TyeYn$SpsITMM&P+-`219jofl`ptUe7#O>O?_%=>V= z$&uSRW9|k=w)^XwmtnYPj9_$ZdS~^o5mh_^mUsg^wek zZ2O+=pKX6_`zT=0dn1?wc%kiPn_|1c76+Vpu$gHZU#1y&Bty&cyk>v+I?-~3mc!v-Dp} z|GM<$rOz&XaOoXOuUmTg((OxqFh{~KMV6ktbnVikmh6lFz4#r+cOC!W_)Eu!9Pe6P zT3S?I{LQD>0m0%`_LT$G+jczK@#sUZU3k3XfsO}G)NefhOO6LP9&o7M@leP8K`zS!7~gpI z*8oxF{SMVLxwhz_H?>V*{TZgLf z8;(uK=AoK-3ZTc_I8;-=>R5NIAF7F$Io2F&hidd|j#USs)EHOzH^RT`SaGZzs!ydI z%Z}wk)yF!P97~5P`Vq&XWARXRKi;tbXgMdY&cDe1-#{kn6VBWJ3&=!$oM8XIKql&A z{?YzpAQSbf?|_-XA04WXdZ+z|Kqg1;|8n~efJ{`|7XcOMe;%sqpSJ%8kjc^IP5bwO z9H?LaFQ5J2flSnIebW9tAQSZ)>-PTxWTJlc_4a=QGEx8OU+munGEu+ubwEJ+okR6E zL-v0KGCBInFWSEiWTM{n9s9R{Ow^Y@2auk8^HBY@o9*8KGCBJEg8l12ChF~HKz`J( z9jebh4`@&ScR(g;M6-Vh$V4TdZ~r2YiRyi${cnLx zRP;Lg7l2Gu*DdzX1DUAMm4M>)bBC()k@mjVKm4KLavR|2=E}IFO0@T_>Qk{n(-UtuNX?3S@HhD|gyI z0%W58<7@061~O5_#ZHo$_( z>kieJWPdG?$x%OVe+`g{dfg57R|A=-Px%-iM}F0ziuUcl1!Qv6b+i39flSo%&#>ib`p!f3o~PUI05UoHp%>b32QpDVpxK`XWTL*eWq&S^iTa0I_U8bZsPDMR{%jx< z^=&_}-v(r&zIhdpUO($leFJL070Bf1Z(;UZfK1fAp8c6XChG2Y?KcCNs4xGN{U#t2 z^`-x49|D=EFMgAK0A!-RKm%0U#-Vyg(B20!IeN=S>^hK%I=sr>12R$dyX{>d6IFYa zT>~;vTi>#GfJ{{7yC6e&`%o3G00iHyL$&ZYy9#7-a{f8?CXk7meS=*AGEuqTvCBXv zYBFwb0GX&v*=hsr)%9q*2xOw3ci787 zChEEWV;6u-)U%|$1Z1LK3EPW6ChEhV2FT2BJX9a@T6@7>0LjfxFK621mHoc|-**Qt zy#te0_s!^EI3DYG?4cUI!m;DnIaGtN<1voM9IF1L<7&s%hw62I))Y~>bTvUSbo_clKW80c@vg~azqWSI+H2O@YuBxvS^e9koa580Zv^oGLC46*pwU3zKL+Tsn1*G~WKIDIL%;5zQ_m;W2Q zEH3SY3mtD9P1T2TGTP4h-C0+Tj-d#i7uzb5fFf{|LD(b~6Ec*oJ+dH&HD{;Nt;$7z zc}Vzhx~X{kd2J}sP7e2^gB=1cW)c-96cy}ujLVpRZPeZwmofi_277BJnEw>zK;dCl(4~#7&l<&*+M6(ycu>cn2yFxt|LozX` zouYjyxvhb8h@mzlM};$@%VYs_0hV>}fkLL77FjOliJiJlo{CSy!Oa7oAYjkxX zx%QT^1(S()>Q!Gh4^=W!qKD?|F~#kwV@Y3w?s$wYD?54Jt3hGhzBaZfx%`73rP5Wt zi(o9F7@=6H(9b4%5>+ZEa_uthL48qQz@y23 zS!jJ@Y*FOO=`Q4nr923PBtxt=fXhp@#5jx7qc5Zgexa6;|)kP={zi0foXXFL8N ziMdosZDsTz-Hj($Z2LjbvbYG=+-W_77X9)-i*{hXhbko&4oOA5Q;(5wX+WyM91_W9 zGXj3&xQIOAX?wZ~3pQQIUT2*(ND#eg_(U{O5?qZ!9Km||PA;Ie_^r<#SU4j-ITaG( zlm`U!t0fhVM*zz}GXYho0^|$mL|9H~H5Y>{9W-`cjFdQt#@$U_@I^pWUzU;}Hx)A~ z*|IYciGUcEWJgp9ys=Rnm%+!SHbu9SIvej~yv_o{$JIfC7D0ZbRHH9Bi$uF$C+Y46+(2D zg2__POP0hkf{`T48D&Zv^!+^=^jN5w!)+^Li)0(@q&7U&aF=MeJ%t?K04a|7fZrA4 z)v_BOh_Di7p^h+QR&PD92(Z0~Q;X&Yoj^Gjh=+TqsFjOSDwlBy9`uQT;)v zo5vCnr_MJd3_^`;m&gWUV6zI|EDJial^t6!@uoM`CJLFPQ_#w#ids{9xi0L3Y8oEO zXR<}zUy}r#Z)~(>{NPZMuW?S`F+vPYI-A6yv2tQOVUrX!&o(O*f1pJ~r)j<^{Uk zgNpP}q`X2h<*jxiGPv_?PZRHElrEsSIH(SyPk2DAc8ulJdN4}nv8X7h{$Z-7L~s@g zmQ!_4)YTu*uJp!J$DjhbBJ_O~P8MqEoKpy}g=pP7?Df1n2=?NYG!u3eLqwg4@{7=c zh1*Ccn>bAcgGi_ewm!MbBp>N;zFe;+Pt`&91(g0KH4j!y>gF6aendDV+*{itF7n&$!B3EQfi45IiT}IDx#x7aVS;G zUIx`l5nrf5Z5@o$J{l6`9!!g6w(Ux1+`&O4*^7lWwpka7y%^kbwTO7A$Z6O*J}$$n z$c$G}+T9@*>tbBrz=U>fP>QtpB*&CR3v!JStAJzx zFM7S5w%6~jq^Lqkl|c}FC*Q76jcs9U;qn)qiY$Q~oprxQ?>6~DmJDgVbPjJr)pS9F zQf0X~0KvS-dhoykmt9%G+l=dUODG`GcsbDbGCa;A4WZkEa$wh&rJbZ!)irB z>Un!^Be?j^kp&5o+gD^x?_`ZqAVGv!A>V3b1}-F&QUVMN<7z!m6ZrtcA&VayTj(V( zCC77KIX?BE?eNYsPjK`azb$EtxIw*9693x=#~WzmPTYS8bZ13oUx zQ7{X3yWo|Uiuq++ZY%C|e%Rky99#6{OrKKxjVhDVC=VIVh1*aeKTPNW2`yK^?z&i> z&7{5VoVJ=duwcu)*WW zG)gnkXcFXMM_U~n=~F3 z7mL7(NL7u2n=f)1cvxxTG8l}YqBE2=G6unHdBb4Og7b|)F;9aT;b_BO;S$Yr;c^ZU_CmUdIZ`xhp*X?5qIU*FP(7>(p z{)DQiwGz$AG^tnH*hvv-H`c?R_pSqp;(O%;69Fk$Rb7WFo^vfV~d#3XXrwa zZUsZ3KtSnaqs|%m3W+ zj%!y$F==Hx;^}S`yyYF|MgT0tU+q{3s+t`^bIUuPU>SggfTno>Jl-+@3sFDw0Qe=# z04&5_%>&?ZBLEia^>lXw-tvxXECV>@Ht<->08Y6L>{teH%5C5=BLEg68&A6pTx}V^ zDObT+%K%Qf3a%Ofun+}$+Ewsq%K%Qf3a+#a;FPQ2Q6m5r2A9*Wf=5~gaLQHi2+IIY zxe6XW0${=EX;;C+ECV>@DtPD!fQ3Nl)7_kY%R3%20$?Fz+DZuU?8x6+-tl0|04xLx z&ja8=mH}9Zt)2(K6(ax^;=oV43La<~z$sV311tkL~LKIMppkMQ?Z@Wd~fTe;L-dBgwtyJ@0HLR&Lk2D$y7)YO>65U1o|>z@%v~i87}% z!vPuq3|xkE;WA=9Os$0_US$jy6{set5SHR&OdLw}u#n;8y`@61Nhec~AHN*D>BJlJ zo_DqcHuG#4H@aykf^&f6hpFBb*n9@k?06MGS(nK z_Ptc96>T$y63zxM2XC6ZW8U-5wm7{E*43-3u2E&YYy_0~FT)Kkqk7>og53%uL1n(* zcEhDKpXf4a874b$$(gFV>6W)HVQIBS>jPN09K2~xxOvYzs~bTk%LCSxp&lIo3Y=kN z{>yNI%cxwq47eX7{lf%UchUgi#n1qYlZa-7%c9{+)TAa?2nf0-OJti=<1+A$B#@i; zyt4tEEV2Vu@2E9`^=lxsWbVsA&AuUCxC{$S7ng%KXWu}l&znt{arrKedT$HfULIY> z!_1M&0uRXLE@L|3%jM3SX8oDX_AutuJ$|f0(c7-OT{x%@bYaH=-^9EC=RI%sg_!jb zqT_)~m}0pAl}KeH{&0qjgCblzt7cRj9HNxef&H2ngr_Y^Mh3NfGE`3p_T!fMdG@Gf`WG|}Vi%Q&e?feX3rcK$+> zvM`S}FTi=vn|&c>eI%nqii+nl0S-_36Eq0*3RB<`au+Va!Wa1Epw0QLK&H=|Jy&I3 zLX`3Y{ZUqPNqgGxC9H|@Rz%UIX{z* zc$?z}u;cl?VBgIn&wS#{@XX_FpR?Uz^KAd~_RF>d8|SvZxBi9o=dWX1uV4G>+AG$A ztKZwotiE=YU0qyx`$}QOzWlze_04y0O3U)nVEGZ74+s19Z(jN{;P}3u|6kmJ#LD%_ zrKc4&Gfgx2=W0?#J1(MVp2wCmY?Jrl2&`OwrEOSc_3JZ>HZ zb02vZn(gpemmq{@A&Wrl_* z21-n~6v}3&>8Y2Vd4fZ)@5ongFzT=Ri%g1QVvTwz)l;TF-j{AZ(czJAo&X)IbZI(J z1RrMU0iTWW)3toli4LbnyR(9xYcWH?h{bAJtlKs8?zF?0qs`3Ox~0xZ4v);Q&7L<~YMFtMJmF{Ksc@|e z_6^iq=|Cgbr)tv|`?!Cmou_w7Z(opIZt(GBUgwfF32_s}={ZsCCPV5&)TFT5+$xF(K4v(~z*$(BC z9J-Hunu=ftMmLm8=X!a9QW|1gp1R_t#)%G()JQ(wV(WUk*2wY^e<+d{r*ji5)lYPI zq)ui#lumSbq*DBe*nn;;eW67fREZ4b$>~e3o#^mLrOb9%J<;KjE}W0|rBbQYNe7ah zR3sVb7}E$VCptXxiay(+cv3&P^om`+vvH6@yiDLyvD@nNLa~}IO#!_6Bz!L4)Y)g- zT~Wrt=F&!|9SkXQpq-jJW2tnaLu~5wIUE*GbaFTc95Ss<53^H+gVJ}FId2E(+&v8j&EyNL-*^RA4(u~fx zFh_W`nOk|3IP@@W>eH{bk`!j-U@EDsIfr(9a^{)YcTD`6*er`3a9^)J{+-u*d;?qULBO9%(S8*qJ(dTE?Qoa%= zB9&YNJ)cvdd!L-S($hIwEtj$g+3u=2zD*f8T6dR=IXVX^!z$7Wr68V?BUG_cl6(F^ zE!p>-uXaK*63q0-aFO<Hd7#Qb(6QO3u8$JW0-Mp|zi0zeA7f zcNQYjPNyGP?TAzT=;Hh{CLlAJ@p#0!diI?8Fab(k~9BIlQa8#9u%~W zW0N!Y+gvW#luBCJ=$5KVpKfKsRV0^#lDQo0mjMNdE3PI~Aym2d$(fBzE8vL+2+aqV z2X4a+a-bn$Y!Hmskt`R_bZH|6iPfBBG{tCR=ni#4HL4i&V983X6)*YIQt@0URKxob zKL|MU&^e{)gNgZa!JC&3hMU_v`%9z8e0%Y918?Vj%p*VPaC7Wq*vfciI+z+h#%aQS zP6NldaIGe_%xAFqKsJGtC9MVME)Ej1UwzzKp?TUkgLgWkSp^y0YpT|L5{5yLiDP`M0Fun>VyDnG6TQPISjIuzJ_K_^FgWGd zURQPb)N(iN2RVsQHOwh|tyU?fy{8KFTa}~VB(%=Kkh_%iMZbdiBVzaHs+HHTN3Ro>X zHCc`r@F0)R*Xztn2fglj(*B~^>z-d&wDO8N`_VOTuX7*mb;tChV|vCoOW)6_XN;cW zpgn1lI%;-^iDQIFwf53`ai=hw;*3slp+Q@3>|VFm>~_aCcF|{0MqCNu#bm$Sg53du z1&cKCR*OenM2rozU1+Iw4wC?(($ywpO{zAF-CjpDyVFh6z0UlTSazW(?0ZtN5HI7Z ziB=6^(R7lF`+c2$6)k$&KE*vu^RaX)-iWrKSVn1eVY$>cs`+jp)@xwNLB0)3F{lkF z&XW0BF`kqf&2u%*AUp1ahoU-FoWQSsrirK5G!cpBhulq97pvk_KQ}KOGziA3z|mjX_@|B1`j^*l06hBn)z7VFR_61&5#g8q9fbq}y=hnNQyb34=Hg+teVzUzOIy*k~*1MlHMqnX|cE>_0ws{cv#t19~ z!R}ayG@Tt#dF$QS7=Z<^Rx+{8gTOmRK+eyfxMLw{+dK$7V+0nw?pVn0HV*=HjKG4| zox=R7;O0Sq93YtA>yCv`;dv0a#|SKV-La5hZXN`#F#@s9XlQ{#0aY4Sh+x%hQAh=P zZErT;DW(Ax6IhRP7mBV}y^rizNE$agHu%=N;V}XWUajPpn+E}OfIupeAkSzfNr#Jm zGUtx>(OO#IFq$R{NKs7yV##Wu=BZYN^p1si>v<5IA0x2fb;m;1x_J;h@!%dnAoY4L z>I!EhtsN`@Zm+IUqpGbyp#jpGCwr(nN#{X^*Fv;vB@^B3fZkj0c8(EPh@alEkhE?d z1lNucSn#@IA-~-`2+kec12jSgI$a9x7{N}x-SiqHAy!@fh!pQgwQ5l+`n$ngg8)2x zI~G#m&4b_xV+0nw?pVm0HxGixj}ch#YK60D9t6KMMqnWT{&bn3ZoT_)V+0oB*H4!U z>ejoj86&U|dcI?Uux55N_N{k6c8tJ+*BuLdE%PAQ86&XZb;kl-$vg-iGe%&+>y8Ds zj(HGVJw{-`>*;cU+e73b?%c{O{BY|%j!$o2X?rNh^!Luq z>od}VKOgQr#;-uwfBwbATqDsCX zuw)S_l#RTb>^3%EKC-}2e--ZR0YUn8Qc1kE7lUNT%BTLgKQ=^RD4FSUS+mF zI=0}tcq&rqO5sv-K*kF~RBgnO?0}9snWh*FVQvYcf)!HNs~c|^TQCG$&Z>SuOI)uy z0e5jF2_aa!ns?WU1nkEBJ|WhmJu)uh8*g1sEIeeoj)4BqW%tq!c5yHg;zCe#>m}n7 z+^taFFR6Z^&?TjKjLM0GiW6?XT7~m|tyHQ?hNqY)4tmv%Zyi|p`l8c_gdx@)krG9sWRj8Ops|@8Tfl0hIEeCH8jU9bjW$Y2-dKrD5_GeL`dtW> zDTixphe)c8r7;6128VNzLZ;Z{E3(s7$c6kVeo)B{>v%_M)$u4-NcUKx>r0oBjUR45 zd%k=X$K0F$=l(RR4_`E^c469;8L+&evs}K|kZ2OBA$XZh;zgmDVO+UP1(DGf{(4+Sq#Q>ADAB3W5td5z z8VnLtS}`G29Y$1SNR^}{HjE5h5rtZM#DRskl#Z90DaZpsf=A0r7#yW5X|Wa^)T&xJ zO?yM0L`F(W_-f_A0*y$y7LhZ~XGtXHBC}zpo@+r;zJ9LbbL&`Qi0e@W5BmD+<8}qI zBdBB`jrM83JJ5F}RJRXpg@O>|ANYJ)hHz$FT0N?bhy9|jU*z+8D&W%*K>!&dU?$cH@gSTGS^x8~ zg-<72ZfFn+y5)gN6ifXK9I6)aX2^@t5XDA7&Jfn`OZff4)p0%ztb+{E0Zvz&tzplL zX@xx7E<}9^F!Al?^Uf~eEjDBVFwU#g$}2}@U?AB-K^q7z{(N5Y9%tA;>?jvD>ARtlHv?Eeg4g$BowTY$xCf zHA77yX~eGQF@jz1xWoXMIIT)BR1kx`8Y`Y-B6uLt07MWPdNLYcoT56 ztcJ!GDG%$1*)Ca?MG(>xcSFuv1yoe+}QP!-n2blMg0rF3lhL1T+J8q?zOcBku% zq*TEd?DkqtSSA%U=FVre0eBc_&MfQ+N2_aJ8Cx(ZMGF<%an+TC>M5ky=TlIeqr*nl z(_yOxBA7~0yc&he%=%Zy7SW;;Y}2Wgnqf%*wCAl}#a#^$(O}Uz@Hi{!lA)J%PQX+b zzwzX;MUX872QUQ>DxG9MQTLGXFpNrKCo&AXI|-NS&$7NwGBnH!3-^qhZYUFm;YyDW z1Y^bEu+VjSO0d(byL#m=FE#0kR&S=oj*pj<<@Z)rh?)T9wrh2_mI*WUP%rV zjbO0REVX5)Zp0&EhJcnoKL$lMl@!k!T!|q|SR&I-Bn7Vr=e(|-ky9H!#>;9M25d;G zFiT$?TMTklv_==ooLd^`ECKfdMz8@HqKEec19^xFQC+7O4wMXhD>}A7ok~rq3EnD; zIIBfhkt^_Q5TlJ+E|EsnMp;r>A4<>?q%L*G76wRbQb@6skuNl|q?e3VDoD)@cNsF0 z?$p$iome1XCkh}7F%9>F%BHZBA6dNI~Vr9*=< z6V{sU9HJ_{TGU@`4mhlh<^An&CZ-ZKKfm_xV+-2Xz|y615@9Hu$@cL|nMZtjlS~yd z0^gCcTG820rh{z)+I+n2=6jX73iEYc!A8nk6x)MXFXLl5Tp=K+b?#hiH4t3o`(7@a6r z$+*cP;Vg7Y;G?UM*Lwm)WF(nlwx8yxFO)47V)^tjs%+cP0vKBgU<1LO?hgy4fF5_1 z{8b}f7z9zd)TdJrB5Dk_eLqly#SC?$k0sy5fr#UerB93x9Q}&=(D?J};vH6Fi}#HW zT)b&xeBe#v`2W|N@&7MayJGS6mT$y;20#5faA#wGV-j|3Va9OwJmgeFlFtld>2}1T zQcyb}R$XdVLG*~*>w%6&B!Te#JlhFmaj4|-O3hB9SgBD`tCvIB1Yl{5w4#)!??thp z*Q<X2ZPcJ46MIrBH!;XBD)BnMbsB6MZg7s*N|m=SwrJsao->X^<3i6~M@^sq#o4a^l2HoEk2*zx)#?ASt| zJGJ2k_5Gy7&GnNea`EmTE8~^vV6L#^pV7cESJ?4!EKBAH^wSDEJ~CE;vhHQrF^WKL z4=_JA5)%s2#mxDks>j$;8x_;)xonfkvE?>gKBs5feAAzfpT@a9(7Ae zB~xaCtz0t|cGnSUSdv-{;!TFKa2_rzoYt=ns%1u2(%x`Wt5jWs#Q7|Bp3(W9Mx;Xt zLR2%Ya*M8?bB7FQzOdubrGs8~$Lju?+3Q~Pki{JfOJcJZnWi4{m)+Bc9oOqDEKHeA z%}T)Z=zObNiH_{)v#@q|+)F`C(zVV)U+$?uBgPImvMPg&3wEkG*BT?%(_XptT6Yw zBlh7=8s=)Ql$b#pY)5oMoq?wqiljQdibv(^t`zOplLNPqbAuGasZKWJOH_Mmny-7Z zIVnp;L)>{>jGu1=dS$T4li;&VWX?@~=B1D0|5p#=|L1?lJ-x@D_h?5v=7Dz1BkK70 z|DVw#>fXlxA9;#g+}MA*@&BmHg_z$w$3As3dOy{n$f!a@a>_X@689k_(PhAzPbNdU zyl}97E|=Fbc&Q9tu7Hz!6aU{UvP)Coq4_15`toUdZlE?Uupv64ccXZ&gx3yD^}v0l_#35=!u8>oVD4ZwkwOui@)}jJI&u;eeGp7 zNTCZ1Lx-XwGfB*drb7Q=6Kj2u{JixeV*1DB&>H~=5!D}`|6WbT=Fn@pX z)pH}fcHeK_#p^EI#nl}PvDpU@k3OSkhXAjdNI#F8zyJ1@%i+CvVs6wvclPlk{m4Qj z_W|CE`q4~&uTekxE5pS3H{{$nui1avk$#kzKi>NQ=Oz7UCel$q`ZQ!B{mcvIMtar$ zbw~PMdeiH_^TOjppw^der4GUOZKQ@ceo5RKHoX5_NxovDyP{tc9W=h?+J# z0DH~E_|99*-|xC{Zlu@lKgmRT$;fFTI{yIaF(c>9+8>RaZ;zXJf6tg3@6Nu@#Cypb zh=r*81H2cFoHKEbM$R{Xz{L5MFU^hfntg1l1uR>M_&>mT$;deq>1gD9<7-W%Z+hq4 zNUz%Wnn*8s1F;ape}MF0uPXLEQY2?r1lM$VZKO>D3Ik@@>|PoEp%Rr~1FUEJ8Qun*zV zyEyZcZZvY<6)`d1^?xse@dyV$7&(zCjO$kRKUl;#8aaRc`zFShRWFBe`s)z9xx1(O z&6<@R5(gMB898U(&C$sDl2@2WU;2!>kzTv+GLc>~a$4B)aDeofk#i>A(a8DY(U-=r z)aS;#vk#kiFBv&4?2tIXd(p@_6X$5;e8H%%zwpkvabB|zO|<|pa<<^CJKv0p9cDy=wowiS&|@ z)53zv0n&q!b0)^o$oZVEiSfDLofo6I{=c<=E!bt-=eCK>w{JXh?ar0&E#J8KN$|%L zex7r`=RIZP`sWH~A01r1Oj+o3-(?8g;9Z8!yFySg>+4bE=-*nA^bN~|io^-LAE@Jf zhKsfGWTsapWu=(S3@EZIi%GT55Wzn0k{c9RmP-=uqlh34d=7a9oFDMR0th0`na2sJ znj}NLk_+iFb=-(^L0}uHFkID$4e@@I6X00ArIlQlt44mF1ZY>-$|sB65mKLXtG@LX_Pz31Splr@O?ECqj{VOlIbMu z@;(s)0w=_deAT(DVq*76Pr|i9Q+U#o!W@@44?PhPM<<4s(+b(~RRZ2Z)730j5NFm0 zxgxbt##@XfRXh@mQ8doxBJ z6bROelKe4s62PFMrf^_mj6@IFa9-xhQc@M_N>GlL6&Qup+)Gm}r(n8hUE%Xy4k?+x1GUY4iO~mPKCDlYUMe%et8TZd!Cm*(fI>Bbu$@DDQ zFI1hJ*7Jl=>ls%$2Z(+K;F&dyf!zVAEObnr;6gSCHiHcyva02rZZXPAg+g0m$}uPE z&1L#wT^MN1bjgVflPb#x5{ZmIfzSOs`Pv8m8q|q*R-H`GzWhSf$*Db0g05J@=%zzu z4==X@v0y~axRK-Pgo?NsG)9wMBb%zZIo=P~i9BD`#BwIt6}zplo>yFIMU7=}HI!qR z7@edDX6}CS9v#$)XI7m|&o=!+)yb(nPe5cj=dUC(s5|GvTxnMgNh`;`cllYpnCwDA zy4p^~yUY+u!M;p5UjiGGn%-<&<&(0+cZ*y)j;onuEENUOe}UNCb@JK}s1tNXogmY6 zw|OPDmILb9}!TN~5W!P|uSH#Bstg{UqS_Yot4+!D8GGcI67B8VS2eQHc_5 zDAB0+IU^WKx8z~HT&b#|Kt7kniBJTeyH554piYokb#if>v^gvLi&Q7E(|X2-VltC! zY56A7E<+(LpJhD^gdbBUc-IxK`Xv~wDtjwAmQ1q^wFrAdWhmHUg@#d)vWDWOeO#aI zt5k&c=VHl7jGwzsUjDD3PTaHV@VSZ3ToVc1|pEmjb zKIzzXlpPRY0sNT#W%e8G*8(oU51o0*nP;5&CBO*yr?wZ_a<(173;5pcJGZ&*s{lLT zySJXZmDqYD;0S!@=Cd}Vn-2v{fp6cqc_X}W1>g&O^SZH4tltl?2EJjfyY@7&`~P#R zuUl=cK4oV;fc6_E(2*th)_y#Hv^#*N@QA!L$LEb48ik$Ahu6*iln^-XFh{fZLfs)A7R=M1te05DUC=i zjWSfN!zfv>#grHTdVDgPpo^tYE6f0@RxP92?uPdtZrX&@j^a%;byUk5Q7Vf@p&^pZ z;~kG0N|uvx#FG@DKmn}}vVGfc!21s~ZCoBTjRl}!yWdDBQM{8?xqQRt9u!m6bbk<* z0p&B_K_Q8ZH*CA`{zFZhR6)xZIA=YR&G<6|4bRjw0kP?+>Y-Z8+Xi2ey-qAyWAnk1 zV7m+6e~4+*^|>QdmJbJ_lq7mRAhkGW&@opLYB4dQmkIgONH1StH9x7^UIFhv*tBs{ zrK}gyoJrItalwe5Rnl=;DgmZ|P`IE~hr^Q7jWcbffI_yH!}||1ZE7(r7Yz?m?eY*S z3Il(LX>t8BT+0#!EsB*I!$pXkoW>ZRW_uaDe}!oS!B|RfwgYt9OG$X8(_=#dTlXU& zu+1V)bqi9_8y-m60axnVejVO_plL(XN>=G>0sCHk!?6GJpxJ?@k&?+e_g6`0}D>+>ABY_&4&khl%#5ID+A}nU|y(%4u zmdY596A3@1We0Tvx4jVF-!^U3UPAXGa+{MXHPShd3(~-!1)H2bl|VILF2^cuZ#vMb z#4`qCdjY(^W!e-PGSaOfQFm!5hGebkR$Edd(Gh%PtU-XBoT*eR+GY$ToDJHZ5ASc9 zHtfJisxi8U`q-h?A*fWp*2{`wKOQ4XMA+lYlw1)iBs!s?X1f#K-!N@l;dr)_L*z;? z$W{e^*xz@G!+yJv$Z9<~PlN+0gG*?Pr>#-8JK+6w(}o<@oSjOyDi{6bA>qU6rsD1A zwV^~iIoy*Db_lqbNmQ6nRIuF+@2{CQ;5%2ydH|sY;lg5KF4*wIv*ohSErM@WF#VID zu&TsMn1T8*+wPQnePKi^-`KobeXgalO4F^Ox4|V%UhSQv|6L}0W8>_ z3-7O(HsyY&FpQz4Ql2NgfHNW1B6U6zX%1>emrW<>e#F;6aNd>kr)|%H_m@o@I?N0R zU#e!rqFG4|qFGllp^H9WW7uuU145HDv6_rU$g+yto(=CWjmpUd0_9#Yg7|QuCU+uA z8}}7;G!k#+Rj*suG89%w zhh(Ecle{-TdVGlOS@8aXX#@9Tq<@$Iy;xHM!J(1s)ey}Hmqo*us7Xz(5D;`vmdG}# zhV52(?^e^MAIpJ_Xm92F=Xn^Ay-6+-$>J8g7;k{>?HX+(o(|ZM&jQitg z2+TS7$RXXUkrlDQ1Q52=6zFIqVtB+YsIx zm^Qpd=mkKsQ6;fPAdMlRS7C4@+WkNU(A3yI|vmTGFYKD^gAZ7O*Lys9LtEmbN|qNnH&P%exv z$;ot_iTM>*BbM`(yCmT9soHdSPd9D))eM}2dTqEDmKp@h3JEQ3B#MD}sNYwLs9pe% z+60Y<43)9<;Juz{V}T*r)`j=Fri}$oW19x=X{L<@Zev>q-s_k)7MP1|ZFsM3+F0Nk zwzc5BmT6;w9oVMAd#Y(;fzQ|0g!h`JjRgi?n*#4CBb)g-b8RxbC!01FSZ-|%c&}mF zSYW%g)#1ImX=5Q?(k8)sl4)ZhX3vsK`|ifLmZw9Y2N zd*TS&{5-C22 zwmiI-H*GA$e%N?;k2h^B#9!EQ@LtZeu@DPk%bM%|%d5{?u-&=w-{61u{oHp4?z;o` z-GTe=z|ZIo+KrVp*cKQm%X2Fb|ykxJhxh4LmxYyTv!d_obUgo1MwAMMF z2^tK{fH^1spx8JMrmbB`BxX?kK?P|jhH(Dgr$35v2wlx6-f+Ga>2|xyIU*!gm{NSu zIv)~C1Fe)uNmRNGxv6TCV;RQf_fTTEO;H}LT5rfLi47>-mcNUo++8C{=G2xrB$9J& ze>5+Byy<>xvgzJJn>xMj=Q_IA_kTe5v+!DaT=zQ~)BJPlgvS8+xoo=kc#zBHAq#{7 z*;IfO;2{7r8=lnE zrLK6%0}%P?J^)dl%Pis8{r_Pxpr=ZCK!+POT3kC5q8mh*0S}4uZo;prtUum&F~zpr zPv83#vs|XGim5~v^7GuFN5_XG4Jyy=7w|7 zTrI4O+=lILjxSrZ|JiI}giE#yUGB1{_kUPfb+NMA0;+6dQe_|iX<@gp`cvOs2XIDR z|6_B*xuok`ST<_yu7ScvUH_wV7j{h7x3KC|+g$}kj=KIw<}UK0u5V$ns=m82xs@ND zyQoXLzJ+{~pdBrP(nekXLvxpAvFlq{K`ZYrnQ+Xm|G(yjbI|oIEV{LJ7eO(j7WBc{ z93uxk|CknJVa=|#y8wzDwV)5oUF1bA$ih-z{jOU;&ju3OA1 zGanv*GIwbwx1i~rC|1@V%XdA~gk!d#_s*n!gi}v@;CWn~o;W540 z!wU6>m1R&+Wj9T#?7crN>=u?~>vs(SoKbuI*p4+->016v*{rAjW*fCw- z0!c^hE&~)f>iT~)caax$eG99|^}G6$Tlwy}i@K!iTUdSu?MMfujk^9H&RyEcU4MFW zjg_VC@?AX>j@kA9U~V`EUEjiLIOzJ_@nwtlcg>z5nWyU?;fk=b(r#si9aNb%sj_$e zw6I%P@vq<20dPiL{~dF~xuok`2o-4E)dq!)y8iFaUDz>Q-@;7xX#IcLwXoG#do=jp zeLwfzf&1>jiFe?6(!PB{4{Kpio;}`Q%G2yJY3`3r#^5DQ85AG#B>`}izJ9=1;%k431;n?Zn^I(%mlW^%|C0;8=3?h{Yb(#&ZDIz%P@#-15lPTC|*_e?saX>ZMyyvkjY zXtN~Sk}a>2OK@z-mgIf6?4%)umrF{41|GbZUMNk=mX`>tLY;G zolc@BB>Q?@Mqm=-il;k_o~4HUbOraK@jAlGcsEvRjyOC5SE6x)=qFFJY&scTE-1O# zbntrkZsx7dsk6WNh3l(4@t^+TwczaVQO@o^= zVb~q?1*b)iO;0e5MgBB9AdYy|d0@&qt?N?L6NC`t$PYz&44n6G8r6=61?LShoEvw; zc;6q7QMFb%DG0e9CMrZ+1{roQc_i=;`F!|vO|Ii|r>2D&G{=p^&;$v4D@kJ%8K17b~54{KcRY3_?>z-WSc0!ME5Ad#NNCcMfiUqJD*Wh9Ol=_(9T31_0icDK&r zlF*CcWj&-_df0gcPWY;)+5xsQs5bHg9w$K*pAnWW#`SQoh7(ExN$6CJ?Q2>*m5CYM zdPN&#>Jcn6VnS(AK(d~ym#RsWzgQX?R7t?48nN7G=Xuj-rY)G0!gB-4>%g2~+%|`n z`74R@ub8HeVwNk^%H!$av*n~j&bbYt0>WaHw34I3oQ|Ny~6Q}^a#`#s}Z#uujd9(B7 zj!!v0;`k-Un;nTAV<)upf}Qp4Pi=o_`|j=9c4FJTeTDrG?LTP0&Hftui|re>zp=gF zc9$(|JH7S9)*o#>xb?$ZcWm9Xb^VrY^Y1o4w0ZX?yXo2Z{KlVd{QAbd8@FvJ8!xqf z()wZR&s*PQ?OXlUYb{@eehhj8RDlQ(hu|~LN1X3<{-EcYbN-hZmP{J8!mJz4+V7|G&vK@VrgS zX{W{Uh6BTmQ+&au#S1RoIlpw_8)#?l#!bu1Zm~GNW0^On{>8{Y&g?Y+4(@%&96o!& z>&#rgX}Jmf{QCLN2jA;C({sYN&x<@Y==GebIpN#pmo6floD-hRFCPDfdvS|^Q<%fY zOTib{3(my6y~Z3qZ*P3gcr^d{!k4B_ey)t+{Nf?X;O5F0%!^#0jM)6v{rRPflo6e` zrZ>NMyfPNIi8v$k)^z9ad287DT{}zN^?-_*IlO&oaC+WgkUPappmLEe!*g9W=NAvr zB{kP&V_xI}U6S)#*XNfm(j_scU7KHAQJ45!m(@9Zu1joASfBrV2;rOOgtd8*3kcsd zC#=pdT}1fCIpNCu;_-wRKl3_YJ11P8!{>xwGry~{)Lj?9qB>taho6f>emtoI@K?7B!0cWH3XyutZ7{32ZOM6moSOf90pVB72`A^5E+YK$IbmUb z@p!@qM$sGQY!Vl1Ro z`qJPpn#04F27lq)SkxSTh_PNUHx@ZBa)Gg)KQ|UJzjTqYo;PQPFU9P@aQNK0v9LLO zZmetPcfEP3yB?UMJ!jtDo96IC+I!8sy*JK_T+rUD=k0y%{L)43y=uw6Ypy?V{U})dqpmIJkD+%!ZRkeG;rzJse&^6h zI-liu46L|scSIc5?tXIjy}RGFE9|~_=QBIMzVpVN+|Enw$oA*A-?#nd?OV1_*}rK2 zko|}4@;CMvKxj9TUD;~BJW39K%5IrMA=sJ?-Vl5AsB;d9u!6(un8hAH2F0^9N zcHS`3FzO~@=(}c!E-@~3^F+bxrxM8+7el-2>>$OWV(#Y`@Q4O1N=#D!_zH?G!m zAYX$}EVa4a+h>SMzAK)=>0p2LdJqSsOP;pkcm<7Zm?+?5)X(w#+fyXbckD zw~VULYavu*%uqy^5`9(_wwBlTC)q}>(Tl2VJ*;+!ZfcY&c(5YOwR?Un#x&ifl9Zwl zr5fsS(yWSXc2ME8ZUaPGB}B}lXda_ekogG_FT@Z{>*`cAQ}5-2jm-MTW{7l~7^JC! zs4F=pHz_8&JxR`xA~h&Px(N}4-0XEhMun_VS2yQ%2)!a(=F3^Bk4qqYvM{M2g>j;y zr#rzi66Cpdvy%u4c_xf)+%c;|z~Ta5;Y-LcmXd{TAYLfpZLIFsBfU_jYp5J!_g6$CmLD+#UtlN>gV@%m=Lp;rB=Aa&lRW9F zUu%`oGA**ze%}|Z#)=I=QF+zf)YKBS`SBUT?@4&7TBPC)H$WRo8lD2?LH*S>BaRDc zxZzhb)pn|icH!OH41qKaDwQKCRci~hER_fHB#zXLT)GwS_;amxqgYV}HKY;f*_?9( z6$}#%BQ!`2A_0-6D*a&~Q{$OLCrt-qL8Dk!qH-{s$I+cXHxc8Bo)w0fE~d~GI7$r5 zK@x>KBec@-Rd^Yzls#fQkc<_zBx{*H48{|77)XRYp+tczV1BZj)Tv3p0|IIce5tW- zk|C>nM{5*g8DZ^HvpUALeqX7IR8foK89gl0UA2$Le7c9x*|HK$(<&dxrc^@`c4vMy z9>Ycgt7fY#pMi_IFX6595Mc;%AO`cg;6ocDjqFuYV>K!}KX9`Czi4)I@}Cn8oM_-g z11B0d(ZGoY4%5JFs-*ZsLZUM^xHyAqljJZ&wDXaSLO0!kPFWr)6S&tWYcbLXLq9S@ zd`;K?ubp7c$vHAAR%I2w!CpO-@G1<7u^#RxSfO7#?L4OW*<;&0wU`PHhIY(eO{<`CX zjyE|Hj;nV+zWcUaW%uPf|FrXNa5~_%I~H&<;Cr`)?Q86xu-|7d+h1Y(XWM&hW03Lh z!`AP&roayWzqGu=qFP?L{@L|kUcYVq=Jn0B-(7n%AhY$U!)6^Pzny5{L<7$}4WMT% z8(5B-G;Rhx5sN(KG8N^hHNOFWxN&!{>9(qG#4j=nId6F760BpBfdt>pM*7 z3yy*=Zu_G1Ng_!^L5?u=t~*TV^N)fq?#to`I!omUlHkz0UT;F5cNBDSC!|EBMJXnx zqG9x|+fC?mkAg04RgA}CSTvO)BT@9O+f3-SM?k%cY6{2VX(2)qkt}-G#DqTQDCnY^ z@`9MhqTnQq3O>`s+Qh5^*0ljNvLa#as zx~L{GmrJE+Mj(Kih9>mcM?n{N+zVJFiPKq*kkGpZCiGcHK^J$u}R8#b}96^W2_4q^$X!l^WR=A|YSKMK0oUnBVplOzR! zilJxrO(=F0bkUn~ksO}kk}-@$&%DHh-h32v(VIYSTOkt1A~68H-h|$C6m-#>QVhe< zOfr*a&@+1`^v0v0i{3=wi4;L*I5vl#xz2>X_9*D0HxZnGiLrECAkZ@}HleRM3cBb` z;cO3(oyux^Gzsv6m(I;QC>(Ss4SDpqi3FHLW4&^7wt+hv0M_1Wq8om z=bF&KQP4#V=faURE>Hpmdd{^b)PEFo(XJTB3LpV2MTzK{=a^965zxS*aU_lxxm1+O zf}V4Y3H2TU4K8+dB1*DxJjzEn^vu;J)N=$hu-ISuXeJycsc1BXp1I0|B1b_NU4n_F z7@Fij&FGnDn^5>D=werAxOgs%<-$DZbk8!Or;maz+LcH}MUIp5xd?jZN)!66M?n|8 zDH4l_gqTkzLC?9ugu0J{F4{GfVeuTn(n1zJ<1(SIJPNw#P1$fNk&MLh6sQ$4p{I_3 z`WIayDq>-g<#M?Mdd6u&UvUI9u;>yPmXSy@o?$ZR8HWjd`4P|q2Tth>Ci1y(ngoP* zP3R3rK^I*@NDARZCY2W{^vsS4ec4gaMVH9(Q3>NHIwPQGwoT}_90gr;iF}4lrUfcP z1HyI_`qHDIi(?ZOp?EfmB}L#(HWRvk6m-#>60sC3MRUT`{kKf$OOAprdQ(yYnQpUq z0t0Q`G@;iY1zq%}FmQf8DzGHz>Ki6>?j0CAKB72{m@68XC3OU4_v-H|GpF1_#d(b#K$ot zk1>=yVYrwJ4OrUzyH}(`Al`q=7bme1QN>1Fns4Xn>_DOGjbbi4X6b&tEVM=(NsS~= zy~ff?y`*Cy77TUZo)f77$`d#ud$MD3dYuGuaOm&=M9}JURm_N|DS(YsKqg;v3lj?}W%sM<~9Zb?M(-}39!Zpdm*JUp^t!vEX%(}q6{ndO5 zaG8P3QTQNMFFHe4~qyxH)@OwRQC2~ zpvtP3No9nQFN3YRm3k6S*G40O;ajo}AEGi6>AC~%IO@*UbYAF)v3g;m)Z(?Q2Iuhv zj!TU`$!nQDTbp*wcRce{7Qb9x7S_6aJwkC*4~{m&M&6_K9Ap>oX`j=7!Rc?Z#?pLkPi@Mlh5~Uz8idAu1!2ZYC)!v1oy-^rVUc z>kKa|1%s0%BNC*W(MCCzz=(2_!@#`kr=Ni;yIhkDS}>Ags|x02{YnFI$EjK{Mr96B z8PP}-YAvnpR=vr1-$$iYyy5SMJXIku%4azg@ub8uBJ@VMA9&eMP51xbyzzZ&_C4F@ zZ6C6|#a6Su*5=y!o2_?m-L;k5dimy;H$SrZw$08av3d2zKWzN!#`n2C=K2ZO(8ai( z2Ynj)P3Rt|1o@yX=cCSdId6CJ&g&eXbG+a2zZ_M^O^z#f|1XFZ@ZG!8?x~%x?tFCT z?K{04dgr;@|G51dAX-3i+q=DK|1+96|-nwP&@7F$f;RDC^#tZ*<^7n}bPBichr-4(} z*R3D%3Ul$z^tZdMw^)uUyZE}(^3~%?FW%bnmE%e;zHzpE`MA=HueL2;Jg)TOD{0G@ zjw`+R7TWS}$CX}u@oahWxYCR7nl1l&Oz8ukzARrjuJqz3E6cweSNec;K7U;4#jg;S z&mC9#fOS55Tr0MijV>yrc+8!V#r|&HKd$s5YwPvLm0t8H>vhMKUR1Vq@3_*7{%3vB zaitgi&-&uyN-z4K^@YckUR1aB1;>>>V9DnlW68zBw;pq6_<$v!e_ZJUmb~`3(u*UB z^|{BDK4AB2jw^k@?$0@{^Z~nHbzJEKcE9?#(g*DRtm8@_u=}%*E4^q+>lLrCN=H4h z7xt<=|7aIEu&Ks7fQwvtTv5$I*wS)b z=>xW0Kd$rvTdo~fdeN4a|2VGnqAe}|ezN}mhFc|_5I)hse@_h@GeuY&%}>_Am@#El+lYes!eD~&CpEXTa`@PRu-;eks zG|}y|NY|jc)lM^L&}3Ql;1NCE)it%K7x6wN*2sy{yKH&hgYcp0YI`88ajuof#_(}Z zkIK~?Uo+ZBFR!%v6DC7Vuu#0;l*vqFG)L!3Q4m36dN*Juuexf~e1Mk}MM=^~RWmY7&j7yaQ;wLXy2 z8s({_3({ffXAkD6n9?%ygzCyPx98LAcM5pgbeIS8$^+De`RWfL0&4GR#co+K(>N>T zc3%UG`~dhgIc2AeE-~|T!0$H`8Xua2KA9q70}u^0GCEz(Mv|H+^+Kc5(NSW;c(4pz zu4?H{#^V*5Rrr!KPm`WeTnhBHaSTlV>6 zEGG`7jDI54viVc(cC@PvWDvDG3A44_I3Jjl@qA=l!I#PvYc?IEC_U4E>y^`v_@=@7 z{tDjp^p3KSgWfy!n|`-kY!q5urQFqBr{Y{W^5Or*h{k zWb{4P@22_KzlQzJ7l5aU#1HLv2pU9#y%AE1x3ZH-E_JFk?uS$HQ)4Bc@%a^SvWf5I zPS;4TGP>k`*JP1Xp>ePqBPNknYyzW!Sf`l6_)2OL*2a}o5tW2+KRt<_%BqcWIf^G+ zVzGCs8i2D(7R%>^FgcN`Dc;{K`*M-f9#l__2tWs9H@<$*?CEL~jAO3?nU0HUxzO){ zKM{WrhJD^pVA^!h@9wU>^@?e~d$YX0zk)MezJ7-u*6;qyA84yS5f}Q=g?XO7PLHxd zuiTv)__iZYr7kY)rrrR-u3# zU23?QHhub-@4nS#KIS{?`z!Rh%QxHv|Fgr*E$3Y1sSfhG)h6K68!QYTv%H#Lr-5Tu zc~8^Zpl@71eIQSXD0;75FOKJZ{(2n4Q*1IR5*dbP!+AyubNIB_tdkZ7-8uj_2le`e zy~%SfB_%HwHuM{MpqbmD%glWQ*=nld@G*HwmwnDC!UWSXqcCygmt ztQs)t7kH}2mm?*dDeG--)QFSprT03Tl1d5}ul6g5)*^j+-aAZoj8G++NqGXjxFTcm z7?X;L_2y}bkR~I4>NJg1`l_A|^pmALTaNE{+_H1!_G8<> zuzklixBUY9KiYrI{zkiKf0^wIwh!2T$R^u7w)L$?w%)eY+%Y;`deS%Vz-U z2*APJOgGE`ONaQfzHA0qI9X$4080nwvR*d>EFF@|`r;X2=|Ei87tH`mhu^Zka0Xa9=$7>bGr-cJwye*e z0hSK1WqsZZuyj~0>vLy-rGsf%ublyw4xwdz&J3`0;4JGkGr-c}vaDCn080nSvR*X< zEFBul`s^8C>3~?)XUzahhrzO5IRh*m{K|U846t;_E30b;SUS*^6`BE-4sT_3&HzgX zwX!;9fTcrOS$AiEr2|-5cV>X4!&X_hXMm-HRaxybz|tYAthO0o>A+Ohtr_6R$@GDT z;^qvnbPy`*#@d?Y`TvclAtz0{bPddWY3ZnBR_hF~bTDD7Wd>L}gs^pe23R_9uyt() zSUOy=wr;Wd^w7D8S-m-SYVv zz;zU0agUPab29*R6ku@|t>v@R@Bc4eE3Ub|1NwL9_0F$2Z*zRfG2VT0ceL|`ox%3M zZ1?P+vv+Jy*jiivwAI-B^k!}2QyaSVajR<}}tz`(XCV>@NwSSojFeZU=mFxKntiMs$(Lw}O zb458t3_8JFzFW*B{jF3l?=2KPe0Aj8AMg?~=_iZhp%;Aaon$=jUJw^c6%0kHLk;$` z<3>W%+|{Dd+smoqBo3k7^mSq8CFCU7Yh1Mrijy`H$L)f899wcqjbSnY$fWkx?0cz zsw!ueAwZb~5;0irvExyX3HIv~LQRiy&B8>bQc;PNMtwcrZ#PCv%hTUj4TbaORHo?s%fff4p{$(a`(@X=f&%+O59ww{zI#h{LJ$*xC z)izP6=~bUF@Oy=59u21oF?BzJH5+uVSVY}asvJd1B{E&pLuqfj*MyUkxKN3;3Qa5) z3XagH{}gn`yKh{Xz-8TXa>u)0yE1_lzIZRAf!C}|;Ie-FzvJDnHVLd?*US4k@Q!!C zYGneK*#|-^6S&MifUZp7GW$T#B#uIZ36eo1TL!yb}VmP^R0bx&62acbp7+T(3aC?+xnBO zAK&V1-Q@ZM*N-|Pj%#;6x%-~o@7zu7zG&yuJHO`Ia+1zxIUd`&dndcIzx}!Ghqk|e z``flp*}r7}@cNtAZ&^RJ_Qm!0ZTy0*=Bm2T&3A5&HYxl61~~w|_I2Awr}uUI{Mv`0 zKZf1`wIPS|5)WQ}Xq(6x$WU<56B|kK*kySHZFqT4|4s zW>5%GK|Mle)t>9-l@T|sjJR>-cKnfv;F^7@A|WIf2kjF(DWTMEGpHvv;2N2pNQP76 zp*)1WC|<^{e)$~Hj)7MgQashjkul6$&(sZ%J4NHIoR6f9q!&-+$!b$84r^Ea+{%cb z-S`{Jw=8A2lS1Fb8kl)Ikz+h~Ta)nucxi}4+G?k*|@%IC|Wny!9&OD=NVbn;NEBUowZ$i zo9(TaG$yVxGMqA5ah*;j1tWRAk)B{~S@P86;eh4`6sM(yCRX*~HL=HL^Jy&hUFJO!yDETL6+q7#;Z0Oqjn*&@vfk0176~WIgKmrAxM4)$ zLj@Ma3qqIyqgAc8{*jdtzrQl#_htyU)={Z+-y3r`f~4j}s*!${Mq(cKcoKlUVKwjP z6uMcjMpgU%%7~Y&jJV!JaAPT5$8*D2N@@mSzA%DGm0*;J2y3N&rIjHQV5e~+-@&7( zw-9S! zq!Oz#Vxz)&qcPH33N;5*6*ne>fVUZMv5cF?bak|S#mWfR$_QwVNF$YIzaGLnNNA`R zDN&6?JXE~Kh!Pwj#bj1W*VHh^z`m_JRz|#jWyI~y;eXMinKN+IGp>X@h?X1|16(-L z$Fy(>qYGj+)use((h+4pR%tTwRlBoxtbo{A8Leciq zokCBYD(4KepHW#=?Uri(41DEZtc>`?jMWO0tM6GE@y6-*|LfQIHP`pMu8007wBh_Q zr_1r^Cjgs|`z#f3FTHXZS z8~m^JSFAk*ZnD-oD{WbXo1A6GckU4wT}+OXLy*jlOya$6j#>CBJU=LQ@G#hZl;Or? zD5V*EET^|9f6h}(49lTVZPIVo$7E0Ml~sX{&`Jwa&x)YN)1byTEM4PMCrf+#ibxR3 zpcwImFj=d2BaIFgDh~aTcst(;*3*R^<|`Kig`f=b6rIh1n!KQ&aOi4FJB$(-Pxq8V{*0L;G09l`mT9lT81RV(Z-mrT%3o3irEoH}dEM3{`w9XM* zKMrIC-A6_|jZYFuIp1p#)Wi@6ny+UtO4$e{G`*QF5b`jPBXdo*CZEkr*?^|su{0YQ z9Hj1-_eZ=)O$tfBhOyzC%#PfVe5+Y&Vdaod%0%5tu^O(I+;nTym0D-hpq8ogynavP ziP5;tf}D_fG7IQcl%<`=SEwPuNQU!tCPgt7_M>!O=&1(EWm`$D8}Jk9bgdslJJhhI zo=t%or_OWx(luV-JmIKE8x9Hx9mnJTFh~I!q9@UTMpw!;j_@T*O*tBiM2uirVD+;} zP}9_TZd z&NJC7=c}5O<=Z2rrDn+}L(~petVN^aFpRxg#FqURB_hqR^4htwqAxfjQ0@0jA%5Bbvj{PLqCPKzVc4&-- zRV2`i7BKxRKezX2X%nb}a<5+~%5u5a+m8*9hEIh%jS@xsg3?ft+geSeM&X1UsY$U= zIh=&cN(xOid#$q^sB7A9hD+CV$e>%v`#3TosO^?sV9S9bPOu&{TB-|~ILG4+PmLEt zN}sN>NmV@?12si-(l<-6Xm+~gbuvkp2VWwYOGVmhWYMz5^rcq3pARU zH@frmTiNE1?Bg4KV6b5F1Dw}_8Ml$}b_*Uk8uZ0IIM?v2doBmyxz@>4WvNVZdfOY%j6T zZ?2q+x-mTnc#u{y+!Ba*H0SHLg5FF{Rk2a7uXh?fFQOB5Iy4@|bhUMs0(DKhadYXG zAJUEek#sE`l)*YPou$cepj`D5-nNz|!P{;uJM9y zT#)l35%or@Z6Roc;-wZnp}hX~faz45NL?MbgV~-N?9(YqJ^d^(*JFL@nl9?bE9@Qx zE`)>1rro%#K|*Pv3hMg1=3pi5(XFE$BTgD$EM|g{GlUR*u5B!j>@e{woY; zhevH(^#=cyQEC>SCDRsP7^!jg_&zYWBTpNt1_iY} zP28uWUb!7c{8A&;puWH~(Bz?!YNDF#sSDCL85D;jG@w!AE*<1DxPRQ%PQ_03hY~W> zv(@yiGdAaJeS<&^QL#pAnFi!H&QqkGU)nM&IW); zRMt0L`PQqZW62Ml1`%B^=dpr?vE-;gl0nZm!dS9G?5)Gck`?AVhed#$b?0fn{5k}k zym%~`M}h-X7Ghpq@@Nu#W{f7mfPc#5(9tC3FHvnD_#T=WDYSpm?EB-@q*!XGcuvhm zuv9h_%H=wfuE6*%c{K5*a|DH!V2Q*$y?h1@cBHo8>*!^snaZ(5A}&yYkV-TMr-Mit zpTtI|d~r?i`@2;o+^EVGuvvmuTLC;4my2=Dm#Pf{6=Jz)bMvOpOj|Hr?A(CzI?#{D z?P(yxb3qKxFIcDkcu{z=#i!6yMW^t&<4hx-T|y*c06W0se6P5O#Zix*0|6ZYla-&D zD@As@ly=@S4f}W7Tc150l)u-yzQ4lAvGN4($UzzTdJM|{=|OgdCEj6!?1c-|uhUR_ z5e3j^K*-O+v=f@I%MJh#DR8C7BR|sYFZ}s!WYT zL-GXaOCsFlN?<0#M8lH-4dV7)@;I2saf6w>v0@1?&dUcDF$+z8~I6?mTDv z!R_1Zu-G=Y)o0YQ@r2BY22M0^qJa|)oM_+@G=Oh{m++Q7KBCkl zu2&OAY@5y1bG7E-n`aKbg5Q1Tt2V*MvE^^^)bgRXd;e=T!RLnMp%;GZ+WqNo-vnQ; zmIu$)6-O*14n#EF{iy)B{R-S>AES1E@*FE(A5YR6!|B7Wp|z@&_TdUWj+H00xcz72 zz2}s@*PW+ zTYvDJLYD*|t5DD4YSH=w=M=i+?awRJ`ajPpbV=wzg<5~;oI;np#Y~~rd*%w|I=z00 zh~-%!tV=x8?JP!wnt#E(P@y(`uFz${$0^k3zc}=lCuFN%*4r-z3bSc*do9cUyuEDI zxxJQ!9<-OOHn-QZx0u<>rk=A`ebVS>q*Okx<6|tZM2)BK02b+-LYD*|XD=kMxWU6B z(qNoBZQ0{iyZwQHY`2^<%#!yxZx~DdoMD!P9yE;Qbt`K4!X>%o+s;{a$@*tjwdBs# zDQcruhZl!sqfC;ics~8~R^9gbHiegf#|yV4c$$M?J9y3&mINK=3ITZ0gxlWBdi%xA zaoevx=OIh7Kkp&i{&OC(B=n$%YzKCr{@M%H-niy`!dZ7b?of6g+r4Gy&v!De+qeI4 z{Rb>pZ*#Wy*+0DY#&xIlg&Qy3c&DARy>{zYt#9A*Ti&wyi<|CCm|PtWd4pvOgbB1; z_BmgHRgyhX?R9#dq=(gvP*%$d$T(C5mSd2C;UOBC2uG!S`vD_vxJ$S@I-I2VQpiK- zu|TR1w(AS%ATHv;DBFx75ijEo*7h(i)`)fV2_CN1+nuOhN-Q{}{gvlA_-maOe9BKN z;W1a{J$j&(h}R}baFCY>q5Uo!_oD!dexsUhF!{u=B8SLIfNO;mGMO3|S!$f6 z2T1gE>a@a@y}46eKed-k#fAf7fOVpR(2WuX-&x?W9eRpG%n*Bc7o4px6na&{o#0Vf zEo&+7urIgA^(MoJAOm3{-=#+EzR|%A5I`{>E7r+SIUNMaBqB_xMyN%aO6o+RS4K&} z=!{5-+RLP|c#`b$23tziX)xk0&cSwf&vO{pqPm>(QmsL*v<)s=Ws zbE9N9)1pTah1wU`m_Nf4sR);ju$6iy*ENiWl1Rvn8YvD^6|Jw-IU*PerdoS?HLeeo z;b0Pv@eP%aR_Fyi?7ZhG4jk8(SQ!>N-YnA}=ZF4e&o77JgaYy(_L+dx9|NT}GeN?y z?ni5QJ2H}^T@h{83#lqbRy0(tfU~6rTNX;=Xuak)U^iXMNPF!f+2D;{JkLgf4oX1V ziyUq`&mrW=1f?n;o}?$Z7I&ka2s%zH(e_B>@?#~^NlR!`;k{tT_P$S?^pRk-2m2Fk znG1G=a>IicbK?OL3Q}xR8xjFUZ~8qF&h3qJ8rzHYR6a|~O^`E(Of7J5{PuYcF_Gsq zj0!TtuwE!tsI=T1b}_ESRB&Eq#9ppcABE9!DGTrS(vnEWidZ#LYr;f@AF82XUol{e z;Csoa->b5;+}Cp5ev#V~YBi-f87kc>Qw-C!3D;lXuv7oAH!=&hu$>L{+1Ny^7R@?fn`@u7ZpF*LK!v})l!8J z>LtW>JSXrz^#a>qss&0d_7d_Kk0Py#lBLi> z+8t3_zBY{T!UXrzf+wctORA?CWa@*wlJL=O zo{+OXz+p0ufX&`xb<*Z&4vq^!PpVxLh=O167ePX+x{A6xN~PG&?CEJC1)e9JBuDmD zv7b>E*JYc(ah?N#`rz=+eEoS2A>OYTB_f~| z=v>d6_v)JLu7UP;`h8&FafLS0y-=Vnc3b5l^umN#yF4Bun^O zenue^W1aK~EF$cc_>`DR7nL}tv0bUnO%~4*ZvDe~4&E-3Y@~A)A#CKsVkFP-Lzcsv zg_ft|^Y#7tg3btoERv35`=e0V-^`8as?H7wc*JF~NG@E+r3pi9bf{v@AFL*@z_8K7 zM|-I*C$vc}oTQ>jj*5*+i&1WEmh&7u9?U%!nw^F*mc^7nO*&YfD!Xx|9grou=M6T) za>7>^i}LdZxLa!N<*M~G5y{5G zky^1y^lM>i!4{U=pW*->igB6fg17@c!8dL+=q{KxmC|iIgVg0jzBP!$Dr$gJak#Xf zik1tJQL$iTgJo)j(Vpba%x^7Yi+?`Pq374tii)QE*(%Mb z?ymAXPx&DE*>q{MsEo?O7sG}&yAm$kIs%ucdcVpK^F;&MBc z?oN=Hx86{EdmNr?j-ts?c+wmu+t|eDF6d$7ljk{fyG>Ot>V3S;M%nOiQYXCGYOzC& z@XlDv5ekJNGR}G9IcmSAafQgR%EyuVsEITFykYqJ?E=tSqLUpYBsQyZMxl!*2zZaJ zNYZ!$(sRYj;|OlxmBp8X8!vprx;dv^-;Z^L7+nK1prmJ<3WN7Nfo@*JU?1Oy)j&8} zOa_`dQR#IjquRcYDhzQ$TrF^C|yCo}zyj*SV1!df_6A{vX` z!m@eZ6s-?25^B26?o}$_+;ua-mC{{OWIA!Nvgi&UI?thoxq~Xp zM%`Gp+zbqjT1)Ka@>MBccVnqx0wrnL-)UmarnDdQt8FIQQ%B$l#|1f0UkOIWDat#7 z%kF-UWve-)hEK?hp7-r##<3PFHYVBdXe>AIOj%ptV0qVh4%JxROB4d>o>oWXKp*Z1 zoleVJo74tju}_auAvMaO{!$_}+K)__Y)?o1?tHKrE92ckG)O2sRU$kd8cTu4e_!+X zNTjRrttDJ?{{jvkrk_b7-Wp95Ie#t6u%1d)L|SUI1vi?7#(*&hJe*2og#D@!qvWPh z9i$uiJlYJqLw=!H6Y^<>A^h%fp%ca3xrV5fK``(XPPM!BygsPwVLnyi8)j%hyqu|emJ7NBy-J`G0U`Zt#WJppOX$DE?$5eKvT&@Q_3 zz|IZZ_ibNm7Wn2e&Pi)eG;pGU6Aheb;6wu_8u%~Nz&7>Nyr_=(*$GuK1T|7Gj|byq zC_3pH^>%ckSIUtWP7iu^z`u%CfJIZI|h)1k6`Ih4I z*Y+Z`R>v}x`j|JUE(o2HA1ut3t-{)xM4AW7R6N2^;8%J!3=5L-Jm@s zLERVgxmLa9(NruaOMW%i%u)#@V#IX3rhxbIm6Eqhbc&pu6S=)fD=lCeJ!$~0QAD(V z@Kw)GtgUT6{uB?bPnYPlhlu+!x<+VjF6Sxn<7Bi~3x(o=yqoc*yy$S;NQQ*{R4o)I z5i&^h!Af^nfGc`1HtEtTuCTs#S@aizVjmaWN(H=n6V+CUQ}HSw&uM1DIOKQ9~>d| zdKzwkmwp*IHUXJpVA`)v;2a2kT?o1}oJ0*8T~Xa1s-%V`u>?)}1~tax4zWr*ADE1@ z!B{q5n2gh-5SEY#71lF*m0qbQeR{D$8xiuM@0(p=z2_%6*ip#Z{|5A?`MUoq*ZVg9Q59^yif4S{EkAxJK`Ibnatze%AFFGf5OXPE4gero!_vyp{ z?o@%H{HYMNFA^;^MhEJgzeSfr!)iiIk%SVj;&~5E^@|eH^|kaYl8JSFOMF@+*48$D z>M0&_lW2q^ULM91CGaw?Tj~V!GB4ybrJPS_Ahlh^JMa|4)dKI^52g)Ysx&Ue0^UeK z*KmwOj9@UH4P+BpIN9ne82F@E>Es&8))JplUJ3U9ulu%cU2{F@dcyU%>oM1(u18!C zyB=~q=z73)zw18Ny{>y)cf0O%8LqZVbKU9^T?rTCy4i)gVAl<<>s;5mu5j61YtWP6 z-NEC~W6-0}BhbUpL(qfJ1JM1@ebBwoJ<#3Iosa>d{cF&zkO(Cp2D%wSAsD&=x(>P) zx&pF8YtARZ4ur?SyN5@ek2oK8KIDAR`GE6&=Y7t5o%cBJcHZeUoNcED&LoJ=gp+aJ z>_nZg^9JX2&TE}lIPK0g$CHjH!2X5D9FKx`7Y{ogay;mGz;VChKF7U|dmMK=?sOQA zwnKB=>JS|X2jjTefjVHv4UX#^*E+6n*d1#iCeZui>VD|YR>YdcTwJhAim&SN`| z?mV*d@XkX!4}y0m_wU@dbMMYQAXCDfJH}3XN87n|N8Cy5FgrKzpgZu+4LjHET)T6{ zj(uls`^oJmwjbYqZ2QseN46gZITaqw{`E)EEf&h;!)zSwo8DPQEe!jvy`xlH*27i7xkyPT$cp37m% z=el-Hd97>5l+ST(oAMf$-IQ0mY^J=*wPng@yEaYvEZ2r9uXI^Wd4FaADi-Tpg%I@W6;M;`Pa}Nn)0upM@;#b&__-A7tkM=@)OWUO!;x>_f7fd(C?Y@ zQRsI~`Df6FP5GzL!>0Tb=tHLbW9WmX{3GZCru-Q6epCJ-^gE_}1o~}LeiVA2DgOX^ z$dn&}e#?}<5B;Voe-HW%Q~oaWe@yvd=+{m8F!XDt{1Ei3ru-oEpea89{fa5y5B;(! ze+PQ6DSsP!k15{=z1x%zLBC|m--3S8l)ni*V9MWse!-Og2l{za{yOw?ru;SNXHEI5 z(9f9iLFiqk{1xb(ru=2-ep9{|`e{?X2l^>fz8m^UQ~nb46Q=w{=*La@0Q3%1{sQ!N zQ~o@3pDBM1dYdVK7Wy$${tWb1Q@#uOQB%GX`Vmv!51lpTPeX4p?`qjsVP+iGCexKYQ4y0=_Ecr7RFZ-rR4SE9Wlt*mun58^Ac7(@E<{ltivFJH zlc)ZkPvI#%1Vnr&?gFA9g6sk!p!{y#^b9lUbUHOQGJby3f8@+`=PdW!b8g+Ly61Z! zy#d(?>AxX6AiW;B2GY+VAA|I>$kmWuhkO*$&mbRx^wY>ykhYO4A-xv)Fr=SCu7LED z$cG^P1adi~A4e{ObSH8tq&tugLV6AI0Z2cFY=`t}LHd5A z3h6~i1=0%<5z_Y|Wk@eTN|2t96d^qiDL{HIl81B~l7qB`WFc)L8AuyQ8qzuB(R3TXhrAoU{_NPUPIQZHhHltA7KDUO^0 zsRwxvq;BMNNL|RgA$20}g4BV$6H+^3gw%!@AhjZTNHIhQsRhwOYDP4Wnh-Ul??qIQ zo`EPKeGh^{dOETM>AR6FkiH91K>AMPG)RreBBTc7R7my6DUj-rlOfe2Cqb$~PJ~pA zoB*i`*$k-?c?YB@vI){9WFw?okPVP3kmDge4LJ_dMda;}o{Ag`=_$xDke-Yj4e3e9 zQIMX990}B2-4$_w?X=LWC7A+5d_j> zka0UH4(aUt-yxlu{~M&!^RGcVHUBE4lk=}YIx+uONXO@2hIDNHFOUw;zqCwW zgmh^B1&PkTKL2M(|33dbq<@=#4${}=pM~_*`DY+~W&UYM|2qE^q<@)z64ICE{{-oa z^G`tf!u;cq{(1h7kUl^E7^Kh5{{hlx=O2ainfXUL`2U|CS`Zf&kc*Hb!Cm)z=YBKi zoqcrnyxIAg>t_z1zI6Hoa2L5{@{UPr;>n2*j=wsdA0HdLcI>p#J4fF&a^HwB{MX@Y zz=@7eum7f&-`SktKr!Ud<}{^Z!#Y(gf~gDy7qMn^-f$veRCA%Yf~?nlP6IDQ1w%t? zG;FLjtavy|h7zrsr%BO7%hmN5s7C?Vh0{A5K-EcGhc;U)NnAh^(RxX(jEADW3LeTT z?PXY1`DUt7355Lay33f+n4+qPIW({>okE@5&Kl5h2GAj++rq%;|0_#PxSds?Z%G={ z;5`=x*7rtL48OAi^hhBiqu;{7diJat1R*0z(*#0BMlXedRrL*-7400Don^@uFl-qe z7Y4RvSICrr4k=`0^jH`eWWQI)6oEb|WMp(&7+Bw)AyWW)q>z!(Wno}FT_KZ~rU`_M zjFtrhtLh&z-DT4tesjyl$Y|%V_Mx(Y0D`;g;Fz6R$rdmNGP*1b?3hmc@;fuj^DLvg z!ocI*vv@MMGYvFJnX<0O^_D%`D^moZPs$V-y%h!?*`Aq_TK1rfz6t~D>BaKV5@4D&wTDi(LMnx*H%H=0lR=rT zrBJQ2ne#6)V9SEIU!CTvT1a7!hgwUq>2 zgLC2F(B)>qS5olqP&JMjH3^Ls&!U7rXb@Fc$On)wT)ax@C}PQ+k}+xqsJ%b#4I8*d zULP*xd^PY?mZ>GYf=yS#TZGQUTCohLDiUe8kJGyG7{J9)I~um8)9%NDsKA%JR5qdK z>@fw$1?sG?WpczQ3{TX9nCRSC&zA*XE0S^IxdcfRT<&gcj-fa3AZiJ9E2yyN>M>o# zYDy%COd{zHdiB{Lr}op@T8M81bF^O-#kHah51s~F%*T*l|EG{7g+7s@0p>7M2mXt4%vvs{)nYrVQxuDZ36Bf-VgASpf5E@wBX%sji-Jq-L ztS29;74TFzq0yLmcC*u2_H#|A*&ML5$yUbXi4kT~i>qRksU!vssYY28XvS8i6Q+$R zjt}`T+G4fl%qfv@xw;|#X12azLj?EbL+XH~=?V*&)uIcEL^LN7N?XpBz~f;nL)Bsp z%&jI3oB33fRp|T*g3uVtMX_9Ui9y_m*F_%B#c=R9WdXMf77u_J*+^zNx3_7sX3Iss z*({s6B3Lqn=__AaO=P@OIK&N%~qiTGr@*bCd!oRH#}?T%q92C&L*4q>yNI+b~t$ z!-hl!jV5D_mPq=Xricr8zUuVgT9cC$xp*|3QWO}aH^kDW%}%k+DT`j$Ox2$T8@09`QK z%#@gS+0`E0mZ}ChQ>o$u2nwXzk;HIsr~&Xn(6AEVbh&EDXfCD_coM*(~FIV!K=xD zK)aJQpOezlnB7t(8ABmcWjDvP3AMnA9LWIuFI1-r@h(Rl90bn@IyL1?w?w-dOl%DI z6rvkzd0Ql(u>_N3TnrNKa)s}*VeaTR{6G;=73C{BtWmGil``_aSSV?38htF5Vj`xZ zFKr4Myp~8Io7t=laHO4!=qx4C*|b#z&K(k5>I`Fu2psEG#w?Y56Q?w4R|ef^=R-uC ziikyuEPAty!<_B1VfdyuYzQ!UjYa9IC<9Slg`n|riVilSK4XT?C<_$sV?>Y8j94q) z`et_`tS2lHQ02ohIV-4I5Nx#4;ijwhq*3K+i5>@U_43k~q8kIbdOTWlI{i&w){(Vj zE6pw&Mqk>)217lXBkbjnLNH;T7RCGg02zc<`{J=AYN{}enn@q6a6G_KvLvghGwiDR zL!eRsCrd_`R-tHQn<(kBm5PZdlLd$Go1vP*yd&I&yHw$dg6$rRpyfNuYIBK|2 z%lea~-yg}M4NuEz$k)>5sL@s-GtL37S<}1mL290K+~XjEy1c&>6zn$OQEMZcPP+wd zh)G%vg>b;6tFp>4C=ekc0;5mkm833@X0)+ZL(`QwDWd%`{l;Dr$98h@l%Z z8QyB8Lat_>uaYHGA>3U+)Rf-;j}3ogX#SAt_V^a?j{`sd0v_02X&)suO}V@Zy5f02 z!I!gH7ajUawq@bTdN?0z@z$U_ibd-c!CNWj>W+e{Tppl(YF8&KspVMDu6X)hO;cn% zDU_Oa$a%yDzc1tQVDHXUatz!GEbQA2XwU9cYd(yW>|eV>PWRJDiEGcUJ-Z*zNbX4r5#$H#tLpg)Tn+q7M@$e#rF6liyUv4ZC z5pUk^4g|D8!#;OQ!D*k62=WFyhT$a*7-*R&s+`7P&RSD7En(h}C~;xEub5+-7S7SI z+se62NuRYU*~MsM$*-mpiN#{3Xk1z-dKW5GSzGa$dEb+aWyUvBWByK)?pGfG5Dia`s?zO`Mt&s#l7*jm2Y~a z@I&|D2K|u=d4XlJF(xZ8lJR@WW@=1@DZ^h%Hg@dkGP2V->$ZDhc1!oJne_^(HPiYNbwUzS(oR(YMsaAQmvIHKw|$Wf>VjtuRqL{mB?RY}x5c(Ij1!NU)S+wD%r*^E7u9;`{7UK+a88?D?v z0$%1z!|VHQY|!OK`;RU+TX%)X?gTkg-gRFR(6xMYUIM>G3r9!!8qH>1QNr$Z260b> z!oof*Kmk}}$=*(q#;WBkNUVG{o82>;q)Y1>*);H41w`#*OEkUGs8Is;o>|96vYv3l zVW!>1R2*Q^)p!?fXsG}ME$;MMt0plo@F{RIRh!!Pi+q!>C(HrN-NFL~HD=VC!8%JN zquN>|&=l?7Ornv%eE`N43)dIDg?Lh9^rRWJFOX8T8U@j|#6+!Pv}AB#9?(^zEO4o$ zDsLXB(JUM~$hu2}_Tf_2T^bvfvqfFMi@auK*7eAv9+}a3zx)>2FIV7<`D*WFttyXD# zClm7~Oa;j&6-}W`;i^We!7O3Un%U$K*d@%>FmY}&TC)1I4JYb0M*JSLSId<({fI1$zq}2=@75%@8qk-_XLl7Cya@ zTlmq!4Dw4*w_itW$YG#5;O6;@=Kb><=AN0mW$uc(v*s3OUzxoN)Wq|%y4mrW`$2s` zdB!yJw&{naZ<;=L+C6>r)Z?J?`~y?e)CrR>Ox`y6(Me_!op^oX-ih`^cH&(VGvmJ; zzj3@iZW}*r?2)mX$1WQ4k8K!zX7rZPD@M;6T^xC3w=vV@fhfA?;KZ-KRDdj?`~gjuqfHpwXzZllyC1e}rK>I#vRlvE}jZ zW?j>)BlO#sdMaI;bIbNE9S`(s!){Qo7~c_FwkyWAO5^LzfOb#H!CSVU)`vm0cSjx+ zT>uP={TRA?>_`l!_G9QiUM(@4(vP9L(}=`yazBRdQ<@UPN&OhqJ-l>cKZX_a05qJ? zkD)uAB$GDxVF0V&-V39|@QywVD|%B&44e8eD0@xZ#(oUF+`6G3L$8TDz8^!cc{r{g zL-$EWY0z))$IxpYj_t?LYo?Fs!+`dhxTE_pD0`*oQGFQDUI}z$KZag2eMCQoUNe1o zKZag2eONz+UNe1YKZag2eMmosUK4k4KZahRanR}vdSkDde%tB{V0Y6arx#XV?m-Kf z9t_Cp8uZ2$Gd;g$`+PqQD`pxn%=Oc-Vy36IY@h9?VZ}@XhM9gER?PJ1()Q`qw<>yL z_e_s0jBeRJ)lWtDR3m@gvVF48AXfBulo%%ZY3QD6!}%-91p%Xw|3pj$Cqo)u5 z82oy@elFrK2~Uk|-FPuOG&J&X@4GtviZ+~Mki)< z1DP#w_@qc%%t}^8H6zmBF_ib)ZPmC+Q^C`whL^MgURM$)`3BK4)jcGuBCw(uR}rbW zJym1D!x%5d*Krfnp@KRb0WE6}@xGur6;%;gR*g%#ns^xM07o>xA`!Mn%nas0bxF1suDKb^ zO%-h^j0^b-z}3`lSO0c3S9hnR%j1ezB6J{N-=AEKx7Nwk4y_}M;W|8&$%GjJuPq-0e!nU-R41n&3^&U}e_BvIHdMd9-nMxp0 zJT8qf>EI)ls4<^(n2ey4W3*oM)zLy(38R|a?dk_tb9MJ+fCBw%0xl12-M^xmTqjps zifKPqu>~o!CKxfJ0n(HUVxV;pSZwx=YO~50Hx<(E24x7b8nP12giNtSMRYT2qEs(( zZk;Ao^O9sDB|ujPcf0z5Rb0K|0_u*KY!N3O4*T|_s8)K{%hfhpz;D+^!j?*lC0(r$ zWvsEPUJHm>oeEjQcv@vt6oMu;7`D*`CsV~@Sv5%#QPkTqH5LAN-qr%|7r+7%*zM}y ztm5hwTMv^7bWOy(AxCIGas4I60#?NyG66tn8;R{t z3y9LYUarPy6>WEML7~zh9f>U4vT6OTUQul-`%towOO%~COSI;(CzV!P5-Zta03zQ8 z8u7Yy@suZ!sc0)UxPbU}yZTqFxw?C|>ks&CzEF^kSofo-Rub#C1F_L6W0_7@>|RH- z?n)E2V8q!YsE*dJZH(f zN^w=gP%ry}ej%Fi;aYzpZWNtrKW=drFe(byYGSvmf3cdYyYKqwkTXE~ts(nDG39!{*)1aEblgKHx%Cs}m zR0s2Uq5-2C-|gz3ujcCR2TmrJm2kREfyn-Jx2Po6YX@QtlsIbzCv&(mXfrfv3oYWR zo(l-BrkfU>3+!0xWzdf|1X{lulNNm}ugWKq8b#QMs$3}>rDq6#LAw7}41IlQ;fo6a zW#MWYD>34K|m$Mgw1%_?nLLqeU5*m1HQg<>BYKrB%;6jBZ$TEH7Br-k=9ZH8c! zz`ba&YOpmFQGGmS&Aa_U)NCT#5h(T#1Bf-a_>@?1X8E$C)+&bz8ih6Fu5g8@0i1wm zqoJ09&IJ^yQiaiw?JyMk=*A*hOJJ1@U#N-Bgb6h0?e5jR*$P^_CVUBlF=0{q$rPR` zfHtzevd31`5N3_m8`JnOMK+kVW`I}@}g^y-op9-QVUk|lk} zQcQZO2ADDuiv8UHVq=-CrjpWl)bSQ)%Zj|ZP!ux0GHbLG84Mhu2?mRv65hx;!d9{! zgkm2aKy0Ad@D~VoRcj2i#4_d#)>G9m$JM=rJ&rkaMk=myIc=N*1r?a>02KS%wNX1a zs~p~NIikrXtJVO|njPl4f@{@FF<)4$W@*e}2{uTdi0eRoAlddqu@4O(*4|1f%E=0! zWz2~dW;D6;InZ8OYzb}!Y0`zPWlYIXz9eq*uw>f@#XdNISQ$XAWZMhHJ}`h-8KA6W zn}A|}Gk{nb_^Mtu`+;C$+ioM{lx%c zWq_iRZ6_4_^8v)lz&9n^4k-3#YXe2ffixxCb}06z1BjJ@TuQcWQ0)B!h?N0UO17;~ z>`w*|D+832Y-3RDkJrXI+$;lLlx$m|*!u>sRt8ci*)~J5|2KfOGQdH}wh4;;(Ews) zAc2zY_d>Dv4j@(r;3wHW1B(6O0Agj}dy?(AXWyJC)qw7iv7U=Vr9T`lI?dx zv3IWx^tV|C5GUDw7ZiKf0Agi8YLe}DLa}!aAXWxCCfPPZvELs+tPChjvTcB3?-)R= z48%*at%qX&cM!2fB1OcgbS-Lh``K0jbJtQj!5(UaqfK+o=XH3^g2+3{WdiKMbWrT= z1BjIYW=XcSQ0#33h?Rj$Nwzgm?Dy8LGm!&D*{T1(VQ8*Cec8mJ!#Aw|&H4d}|F8!@ z+40+@ili$++3~s}xOUY6O68Wl%KZ(>l@&>w>w#iQ1x&8;kG-;3{aO;|ph z_tY3nTS*qHHZQ~JT&bv`phexWD->et1YN<8dB$}G_~3kcrIwI1`V2WG>}OO zxiXrwVgb(T=Fw`RUbj|Uq{GAyR1+o*y;F94thC%*4$6*YPIZl#)Hg6Bg_K&AGEa4Ol__hNsvVbHpij!2fx>uh1W zznQ|F9%aOC)nsT37c_$d292775v$cQYNeWLlTm4}7Q}4APr6)K0HXpG(v?o-@}L1b zU0lqjGC}2lWyjLcJ7vemNLeS99n07ku3OgWjVrURM;`UajNWC(|D?>=*Rtak!FwKv z-d)HRZ*tkOPOZ`dZ9U75c@3i}`5iSE)AZ}boL19vX!2GvXU^CiM5}BmMw@|W7X&di%$FrprQ_`VETg2~AuuE~(qSfH58Q3L@ zg=a84u5=kgKE~*Cl2MFB>j^DQl&waS)_~>)40dVgAlN@J-ac9i_75E~Eaz@z?R%fD zOU$YOR)&?qzDW+G58ToI(}8znV14!40rvoz0THz4KKP#mWYxnEPz^7p7yvs1qk2~& zO7r(7*8l5JbXBm6raKxhQ~FRC;CzBtIRH#5L6lN{uP0wE`7CCgAQqM~Nqlk10}Af< zF%c`Kkf8P`eF2}Db;hl9CGBV$6BHe!O)ehS)-a6DmYe2KgHKsnnwHgW4TCpo#j4I? z)l!sB;9OO-<Qeb>~P zQzuRDm}aN9OuagF$K;EX-<`a2^32IoCSIJlZQ{y_trMq=|7HAtr?bEoyVI`#9Z zo2RZ=WY#!^&)_oO% zY(ig_?m`sE!u4ZWQCSOF5`9^^D@7m++n1&LJPu@u_hDIC2nt!EeOL@D>X0Eza{TPR zOx@KEkm;PhEInr?)|aJc4D>gwXQXZIqh)1zE*#5QeOOi&=t7n=0ZzvFs(I6WI&(Pp z;U}&d8mHA`XRgL%=pKKEXXtX}7&!aUJ3&|QNTv;4){moC{108aG!`8j?yaMHD3XfJ zea5k(R&se9AM9h*3LZF)5A>at?o*j?R+yJlY(*Cdt6@_x07&d-)r0_0`kO1obrgvh-R2#~b}vR-oTNE%m-Ey;gzo zT3?o4DLr28%hD@9$18nVdL`Sq*q5bOOpllQvh~&*Oo+TCgum&(QStHLW`VmQC~b)zi%cP4o3->A9MD`kA(3 zOS5bm(NE6`CTJSom!;G;s7~=^4>>>HYuc`1^+D zJ~;I{AP0U9c;J8s4tU^z2mVPO*uJg30lo+s9bVt|)?F_`Rz2F&18jJcoI^(NMTm^| zmGJvAP8IY%3MAuDVy_n=dp7-+4BGP*$Uo`5B=lIBx9o5>hx6SK%J*^3d%6c9W*lgk&f1?^5h?k*_P zxOGXi7;Tnnz^Z4{emABv4A#&C4!!dp^7vivA!WqWy1jHasC&JKlyS!Itw}!_hj)9W zUys*+Z%q>RFyyUx4+-8l=~u-eN9lk&T2`TDyVsv%1*Zlx1ih|8&dZ`p9HyyIjLMu_ zTqOMY=sqW+m$taQwCJ(KZ55YMpfwsXud=92wQ#te%*8c?t6)G&CQivlIJMabP|Nem zf{(XnR7zf_77Cb-SPU7B63IS4TeD;mixBE~N~5es zlYVv0SPAK?4YI+LO^;1u073`={ax?pPm|?ym8Vy@e=6ihmHSf{G;Q>vEPn;U~J3iPe;!k zeaFb%Bc+iehi@HD4$lL@Z^91+Fk-l)JeyUzLdal2t)+S< z$d08cfD|JMmVt8E5&ZJ@pFQJ#kL5FQkGVi|$c__V4}I4D_dC@;y!D$ycYpr%HU+R^ zB*8Lp4m*M`e&~qP&i&{YfAgAf&xMEH9sKSC;GNFh&Bq>Asw`)R-mU&M1<+z7!7`8z zJA!`_JNTB?MW+yVesJw=Hneg0E8~XCFSzuk@85mG*YIys053)oECcJXBlzSm zUVq&DVQ2pSLC5{iefhLMQ{TJkQ1Q`IjyUR`|M`jTn}>Yua}+>~kp#;?JM0KP-1)7W z-tW2RN0;9CRAt+L-Ei!d`58wv@!IJ}zkI>PmtAt#$0>jrBMFv)ci0j9S@JWVyPH4% zT`wc&MgH?N@44R*ez@^w-B+KwJNxy8d-Rq=DS#Ry36_C)*b)41f4uC8=)$(^55M`P z%U^oXV1387ujFrg>b_5i{^oNJJaiUI0o)i#unf$@j$qw0Ctuw7_;vTxHk`b$A$IUN zKSAzW{Pq#YfBdOiZ@>264*tKlQ2;qc5-bDtup{`{k8k|iMJHVJAJ5$T^^<@9@S&&v z{6_Zn>z=#g&>#IFY5!yT>o-vVJ4O;L1NX2a_+a7n%3scX@&CSVGI&0E#rC0BesK7w z=O;f*y#D^%ULkKdn4kc9j3ihF@?l4C;tF>2$xSZ%@++GkIcVbRO)CGs>n_hdbmWlw zyPN*^Yac=>fFC0XmVtfP5&XH|Kl99`Y2u`Zk9qQjzn^wtRUs68y5Y=9Sf@R6igk#CyT0u`#*d8kAHUAg~l_JSAOT2^R9lEuyezcmwo@b_x{*cdmq(4tz&V- zVo6jP&^E-xrm|&Mu4FPYHLc5Aqzn2i83XT4I7S!KvuLyP<|9{7Br|SIb9l;kI^-|*L#p}Kq{M69XKe+nvYrga8 zO9Joyn~A%_yq*2(_CGyHwNHhDWgsGU1pj>0HmSa^^r;i4JkOo|$HvVEJ+SR~@AgxE z^4dvDkDv3iYcHkRr$E6nFcCX~|L3Q-|LBG0#Uta<@1Za6NPXHhwC%woeP3y(m0aQB z8^8Ars(msPtchimdRGWnm0CK2AnDcQe~aiLXw$^$567?mbX~H|Eu=N zYvz9S@4wo*vH5J$_qDlmmGnW|Z~3oj%_IML>!}C-m1>^^1{xv0mp^;UGcO+b zTjCMlm1o>{^QnKdo%i_M?8RrczjyiOYqvE=sP>6aunc6xj^MkVdC%jn-)(JPgH8Y8 zlG}@lhqNt<*DTF=1={^e@y)T8%x!@nU9pvL;o`Uk&FJ;AS=!LzwO#a zwKqb+GTzU01fTcmxw#iMeEx&CZhZQaW1Do(e*RIDLGvm%HOVC zyaoBiAt(M}G%#|@m6~sS`L-*nH(WrqkAZ?^JSgo5{^5h?z4w)`-G18#oumhtqhBlzg2zwxt+t)CtG@F{u4_20Rlo*MdY zCjO<2{%0F*IOaEB-EuP3J_-t!@l2^B_>qr2;{B%b-ba3Vq_WjI{d*67JUAD=_pU<8 ze&KJwFkk=WtyKF+C|Jgml8)fN-4UMp@?9VP*t@>}lTTfjz5St2=8wDav7bd$U-)|V z)mB!xT)O{1a>y~X;6px-jLe@s_wBhOXY(_6%p5+Qow|GK#L0^$eldZLzi;fpv3HJM zHS)(1!|;xw=K(|4&xI$p&t4cB=G6Y7q3NmN@$s!wqnl;yf^u7=GniZU#AsU^zIk*w zcf}KgLHhKXRw@3~b=SUJxS_pOvh<`kEFIY_V_UUsX$O&MZ5u}<>s%uIyk}q=k87W` zFf_ev%`!G<%O-YkdDb>jvdw-e{Cr_x3r}mGDS7dXH@rA52ljf|#FYo|)<%FH zmn_wPQTjAqxr)6j8aZ9KxlK#^J8_^F9Y~yk5aCaWb*efraxSU68nkd<(I2L{u2e$CEHnk#;WbC?^P3)0J zDZ)~37M-9yDveP&i#3_(nst~*o4+jCd*ae+_IB5%fjkPW$Rin>`DJ_e$fGswgn6{_ zCdtlC19uYx*CTR89?7^jShjPoJX+IIm`BHLl`K8}$$>q(p&eY2M>1|UmMvY*qcv@W zd2|$<^}nRS$ek10zOI;@*ev5-YT3j+D@WEo{5JtD zg}HvXay3g=`0}*2SF&`ktGkSwvt>(rtV(OnJ+yS;^ODsE8wR#_bDNOt-RtTuyek4mH_Nz%T()zsRcTF2;i@#Llq{Y8kAW@S(Dq1{ z?zKynaZkEz>GG^x_ZQN_+T+^G<%K7+n3hDFtqXxG0w65e&X( zd03?tQU|dr?JTCvHMYiiCom8bP6e#hX~Zs7U~PCqLv`T8yDlbC6urxM2Dtg)e~qk9ONs7qQO_n$HPun zx#7<;)iNtO;uilPtpMQAL3!8*9qmn0uwVK1VLAJjwGTme1-k@?-LvzEZe_5)`S1iX zp0Z2HA!oPL8_qz+8;)LOoSj{EcVe%(VHhm2yZ4I{7rRGKla6vRB^0F^PU#j(`bI(! zi&>_bXL6kMYbIaFD^#e`s6k6^ogvh{~ET)M8zJhO{Sw%}|c?p+&DXV9yYC z-VjP;J%meLF|dSrpr#VLun*a2*r2D?E|5~zK55A78iFBFSA665v4gyPh?kn_`5F@7RRLSRx zrc-A8JAjBC6-=9 z3t59DVl+0L{<2$d*91eLpgaw_L_{2Zk4{6FjLsz<_k`?5v{Xu&TozwW=cv2t5z2)P zxSDi^-kI7M+!XFwP|gH=5Z z_ya2}dE*MZmldmBdgR6VD%VU|?R@rSoLiUc|2GZYKD1C+m_=I1+vaZsXaDb=OV9p% z);9B-naa%A^mWqDPWl3Y)h9 z9a7lHIQqW!L7zThvjFr-VI$*2{J{G53>yUKk-|pC(XfH_bcM~lG)*9EWSs39SXKY9 z={|DS37a{|9x!wn2e}5eXII$F0v%G=$T)Pr_CcmTVKW2tNns=7B-OzB_6(b8phpTD z87H9z*3%U>Q_?hnu#s_;X<$`-!)C>qwNBVfO7?)E%Q&|*usyrNW&-Gt!bZlyqm64G zk?0dP<3OJjHZl$l4Xkg^uo(k-q_B~3@MmB>U12jSO%n(k87FrJR@FajdhKm4T-Z4R z4LVBBF`c#FQ7q4Ce}lRoymfBJn4QCtMPN2$oXQ#4MV(0IcMdJjxQt^p1CM&o*T=aX z7XVFC&d4|xGq9$WuYb86=L3CG&d4~{GO)fqa%RVQ%N~?*EM;Ild*{rKbAc);XJni! z8CX^S5beG}hfD2_ZIV5}#q0W7Z#iW;0ea#5j@I&^Wt2;-{RniQ@=_Y~%*fM2hLID8 zkRtLPWN!Xf;QqfhkIf%E_uILf=FXjS&mA@U*vN^qU!Q&dtbg|Sk;u$bGygqv=}dU! zvY8X6|2+NO=_{wVPA`HEeczwDW{RCsPQDJF6MSk?nAA^>PyA%!vlE4h(MKa z@%p%Ru(^(OvVGSXx&~YROQpWet1TfKgdTUg~fd zRUv&eP1ssw%bfBm3?-MLNhaLIdIYDN30H}X8uAH3t>IDR$PP!w?F+^W0ehN8shY!G zD1=FCx@HLaia5Z_qykz^T0`XBEzCK;1Nq$Wch}7+2L%1|JPX8omP0=ETyK`YH?Xq+H2VVk32BZx5LYMGrWJfz}vc~Ud% zSq?-;s!X{98F!ot;7TuBcb90DJFoCsM9SBs6iqVcsI{EQqBwd>heId^G<1aLH2$o$ zY__P(c&fx?9nMHx?X0t2vc%H~u2EMR#Hk0+QEqrMF5qFmIzi|uQ&VrRC+uZoDHG6m zNH$t1ndy>U&1f`qp0yLKuA?GV_a*%Ccq?r#l`(I*#yOpqkey5_!gR`shP7NCwZ-f$ ztuZ>j*in%xy8${0I6xq{MJs8D7MezTBUE>Yg2n9T+=;X^Z=~wce2DdSRPg2qt%zu? z5tB_9He})f!G$J#HPj_GRB5}#|bTPZhoB}s7aa12JjA0!PrjRtMivFxyq;g(6*R-c2;36%mQsk0b-BJo8C>Cj}b!x73DwR&$UNE*XxW3G|%n6W_CrN;9aGnY@r{8mRv zYh)=$-7s-`hl6CySw*I#s1@C4NhCG7YP!U-;es|0@|LUNriLl<4Cz&tGNXrdI095T z8+Kt_)6M4@F-7U9npR=adMoO@-Gf_N86#RKRlFu`bSmB9@asvBm1r0ppeMMe=qpsB zN<6Hs=F7Mz8x~NnQXj8lI87Ns<1cqOe9nT!OZq(SOf^mUyw0jJ*s>NVK9UNvyw&R0 zxWXX;~BPeog0yVh4q2PI5#RmK=k>Pwysco{XC>u`8g z<$y`BMYXM(E?H}~Oho~lxWQEcvyoJ11L3I4URD{E;Y?=oPaO_itooGECZ})KG%i0& z>780vvs{b0QJ*Q|ZCXhersP$$Pi+{xd6}aab+V|xq^2B|CSB78L@jGWeTk+hR2;f! zr5W_p@*%xN%g>sZIilH^Gv`RhtUM$7*rwU7EF=?@)c^p4Tzo;3wd7R!x{ZjTBSMG6 z6Cv=F$*OnMY(gxaw32MTRyN@UN778Nj))kJXX`ct7pKs<<<-hs2oNsOWp%M;LKAD5 zwRXSC!_#qImDfA6RXX9**o%aPr8E;4b#zz^8ZV0psU)A%Gq`~bWHf99r=l8%CW8qo zcPkbSIR#t3u{6OFh zGD;z)^2XB*A!LrV8a5qejn&AM+Y62Fc zp;C5wwFZmRNE_Yw%rPAfP0X1Hh!I{aiK>9gnQ9oR6y;YojcITP;80c@DoZZp!mUBg z)bkwRA@)}U<>7wsTwPTZW26;vgSrlO@|g2P{>l%(4<`Lzy*N@Y?s{rOU^ z(PSe*zqM5|(N<1jP84-))Gn&^c^m7~WFrha@&m}>X=dD_(HPOhNUUMg8!IMl9jhko zSvxNzyn&R$fZ0L?d)<|=&feAGXhcMF8C<_&F)?jX_y_~bbE%-ZR$+_HY~JCBaJLO>F@yQ7mq?m|1^bHuiCRIrURL!8R#<99G9WkLAqJr`|o0|!M z76F!|SZ>ubwT_BX#iK%lMWeaQ1)5CMFQghMnod-bS~l(Uq!XsPk_`HjVp=m|?Qj%g zTuF==iFDW?Bn>e>gO&t5uOyVEs5uuXD@$d2vZi#4{=#^&!;vv5^t{vQR+uP&<5g>9 z3XyV{PDkmi!W69e449qOmhz^2J~}$o;YfREwI!w_#Z*?w>MCf-Un&*UnO3H1i&X6O zppkdu25sCItdITsGKb1h2?dnujNQWE+Kk)bR3+09Wr5EZ&|E&2t2!DMGuTdq{KMrA z2d^iyH8tjN`%9sMDQYw`4O=n=jtv?V1zk-Y;?uq+&xtK7J9S2fBgyz<)gWoI*N$3E5J;Ht`4HD`5YG$y?16f%)!)=8Uet&$<9 z)2kAmqCrdORVfvV&u&=epcM|4V62Jd1`#l6-Fm0r6m42erfj`JB{Ei{+lK3$zM@4l z^Q{g?A|J=Isg}i4rwe4#NtHaxYB{10Y1A#oqbs0{wvsVb<7iQwxObT&oA5am4YVeZ z25r?IvE~|;S~%rW=xt4}7T1B)Wm=(hI^i7q4UZ58VI%i*Ib#$NbLrSjPQ-bp|M&9}Y5iHphgirqf=@a{fXc$E@|JDw}cI6WO=`%26<@ z8^~-$bKv_HvyxR&&4~1O4CVcHTQ#oIRPeN^;U%qr*OkOczCpB1b@0Ya>&1#2-*Ex<{p-A9L*60Z&YorozYz`=85*^m@6B3Uj2gfLj_GTt!D+`K&4tLVLK(ER+cFem_*m`#G-8 zrEHFb6Lqw5cG8C_BVq&RG8wB1bDNn6aM|6&7?Y)N44onE}~S&$P^pr$cV=r7gXtB0i*F?j`H$_k!SZimubA@Iz^dP z)W@y<7H3ODA}XUgfu(X`Td0@IvPpB*BR8s4- zMvLCCzcBLDe&;eR-dZP@*>Dt3Q&u8FU_pCb@6Kg3$t+xBn|rTiW`EI#tHMDl&g*q# z*oG!JXCm#gseNeBU9YPYtr}k{Flr-e0EzkBe(19GiZTnW^@cPhZ;*CVX{Cv=nk~Uh z&nPo$6n1|o?<<89@hpQAaYMjXA#-IzR`2&zomHRJ5HyDBNv8rt*)#j0%hqf8x3all zC|ja)S(7UpRRt*{?hQwKBqkQ60z7zWX{lJyI9H15Liv=dT2Xid1$`)hnk*Avn8mkvZp4|UjMyyw2Vks=>pi2Hs5v3z^DH?XB6Tx~fm({cy zv=oUG#YVVTGFGZgBkZ)}wnV~ERfIXOJ(Y7{4aP?{d?3pHyx+MDY@63<`L}DEUR44Q zs#AGixEN#w!W=*V*{}+v-Ca`VlQFdxtth#?Ia`j0C`Tf0)1q~!iOLFK zlkvoU=Q1s^URO*99dc(2D#mVeM)Q=$pSM-HP_HP%!s;qkH5qMbG}dSct-N0sF;gN= zG%aK!LZ%v(m|N+O8APz}D2%+g|G8}4_GNab(Qk=@Q*ab005WLeVLHg#{5_%!OVa{D zDcPcpsnzi)VZnt^rryX?q$}@q`ST`=Iiib$b}!)|NX)oa>aBj;NMW;i_mJ z*AY1_;mYxvV9$F@Oyy7{JrNroDKa)c3(iqBJVuwz#CeIhvl6B1Igq7x(X7st!Sa8t zg#R}-eDu)VWmCtDT@U_o;Ahzb9~f_+B*E2u=!oI(!5J=QJ&k{I>@4g+xQ32W8E5TG42)hKfb z2)vTuij@?wNDCO(z?6|J+E?T$qfw;;>^-q!l9*jg6C%w7UHX#A88U^FOfg%Oj)J+f zj-1E`ttwA3?I*SMea4EZi$=j45R8jUVYQ0T7?WNh92M{IOEA11dM7+E@ zyuQV6gC-*SFPVsPR*D{Ak#Cd)zC{VR+kh@apIv#Ud2{p5&?oQ6C9|)_V9`jy7tW(D z(0;a<30Takq*v!cO%Z`{26>~~r1z4cea<^=3f!&)YejP@$>!}=2ku$Kbq3BthO7>= z#?drH7sCa-7!P2a!I;BsrDobImK^R@A{16rnY2*Q<-NKBIKAmr@^liEuNXAu!LT2t zp-b40nf3_~`;i@%gHgWr0zF{f8H~O1PDX8}6u5F~OM73? z?Q+#_@IPtn`;4u(ubXR~QMGE;sYp{LSjHRar6nQiD0tkKvZ#!cIzyo9z|EQ#PGlYT4=C442CBlKQp-J~|@qEtRd`b*JZykxSQ)LJ5s znw?3TD#a@CY&xEeMnL*|2|`~@7Z{~I!5A%rWt}wiW&FS6B>cbgk=Kyc{LAyTxfkZj zv(JJafKSe(r~f#eoO)y`KKa|pvnT#P_TD_sk+rNB?@ll2CDQ{gAe(Rw10rMSELBMr zDo{zKQdLPMsY>vh4ERrI1)(To1n ztBA@)+|Y}=+_#d$x7$KGL#I|s>FeSnC`LP;qN;sa;w0e^PkWE<^5C*l}SRgDc@X+d|P#7uNJu{gR;#3Q(4@MkO z^2%{`VSxwg%JZd_ zymFjbSm1$3@_b1kuN>103p`Luo-aM*m1AmQfd|sb^JR^^a!f8P@W4fRzC4jvj?)VZ zJTOC^FRSB~V`5=}2d>ET<$JtxoLX4mfkpCsX&|p0;|mKskV~E~@#B@_buZh`ntEOp zp>X%I6mQ zoMu9*C}xYD(y-K~#-iM}ndC{qZSGsCrIJ(4J!89Uo5v_i6)kA@|o zcH=1j_F1F6%7On#8fE6(Mmbk0$qdRz#HbwV$0VIIRsF=E>|l4ObV~iq7;OZR4%JLG zL-?>K#mkgD>uHhLs8}AvGDxu-zH!I+jk88M{0R1C=Dgy#Qw>y(reH;6r?oCeaBbC= z_>+)Ncyi2#N3_bQ&0(n-mnNNAJw8#aNgVeXr!L*CMf6^^3lKgsty4?~noWjuHZlgG-BvGx z*9e2I7buNp$4I|#$8e-ygULF|my(SKcjf1u;mC3Hk?Y9ObBlW>MV2RZyuce~Y1XXM zfUHjFASc8kj2@K6G0`-VQ&=NolTfVDjPs@LjhphXpEb%4 znJ0UP<3`UdAfEuNjJV2{@Ww!D>y=`p4{6d#5H`%lLPID{_b0)8INTIMN-7@~RGZ<* z3EW0IJ(viJys8Ha)Qy|+ubnl@kKj*|=(&B85O9o5^_wgqMm0ecxoAIGlTUhCPEhTj zDrS*XnC}qn*{ohm>X=w!U;wcR^H?a6)!7^e$SIrTeo>^ z-ja0fEB4&cr{zqEZ??qTG@=(| ztkUXLXA`ZKDF%aNCpZhMT#aN6GFv3mI78(V4?aQ*+CYEG<)`Gd;j^EOS<_za+0Q0< zzHk_N76h$#-|*SlpMyC`oi!(q;E$ikxv>kRR6P>{tO8vl3LwdxB&%38ec~r(F^E9H zXf;|a*a8{pWUxppUIfrTX9=Q31?!Au8u?*f|w+j z6XK+Gf}1V26jO8*5v;lpOpYL2K}v{H!)lO?r0OBS3fycakXZ;Ec+pu}PRb>YOS_(v z7rY(JN#d+Ic?2IRM9%F<0S}KV1U1b{lVU|8+IgM>FaIYU^(OLEBsgyNf=$c>{4`k_ zU~|ZZoK(YpH{~G!Z>6M!mLpgsDzZH(yZuuPKalKIl<1FlSl9YMC9BKAd&z> zg%63Mn1G6MBE<1nw}=A*a6krh^O0pDQmA4^J2L^P5^4|+j(h~HtPZVAC7LtGeJvA% zns%P&)n1H%Lb$MXUyxLzmN z0L;mA&zKYV5q!Q8KDYA?g5cOREKAAZtj5zbK}79ZLL8FM(iV{&W zsLBX0>rtN1DWUufn+DlxkC+w5Mg}C7kts%!3a;nmc`pHT@|?5g1bO7HlkmAQI^d;9 zDNSpD41U6ss$QEBFgrVGPOw2C#2a08Ky{OfolV!nDWZ;>M7fLO1jvVyiB+3ft}}#( z1Z$)POwLIJkGtQyF24=T$+w<0Cy(G0s_?meGm&6#B;oDCdV+)L4c{I?$5cfv9ej}Lf%Ygr!WcVGzf6xeoI1X;)vIo^QqH+l5O4wzgFlN*soBQUU?kJJ5*L0-5)Qqk*feS*_Co$#SHMPdz&49L&bS?(XqI7IR^}^`{*$t2KHJuix z+K7tl$e6@?^;m-M6zvk+BO5iUC-qb+i3ka<3)fP1wnkz(xM`NmTqupXf&v1x|Gevq zsV&dBRvv_9f^|IYYmR|ST=#>t43;z6Hz78iU)=Z%d9bP@zI2@V@Mt~o@O6H9yNO*r z5p_oO7E8XgDsHmEb(<^Jx_aVy0=u656uz~eF9e(ND_i$}qq8Z$ro4W>LBBU|%C~SL-firS7`&PgO=f-{O^V4{s`XohQ_JRcFP-u zI* zHML%U%%swdJKl9N#5g(T5>&U%L~2cp&GAZzwTty}WMpO8Of@GUmoo^}Vj3}2YYb!o z<{*O0!6d`96N*a@9)hrXuG0 zW7TMgc!o@NR?bPB9^b*<2BdC$I^1LeSgwHbqwG8}=DvJ=`8m{AU)kk%Tl~M#2bqYu z0ij*SSw+kC(P3DKbel!{a2C?oXcot_Dt|dAvvlZ=mme(FQt%zVe7NX{&?>Td!yaI+qHH+zhnCU+HY+C z<#u=Lb6cIw&u+FhKC{vA{i#n||MYro?NdPf_CNQ%Z+8=b^SvND&ldrC-}`pv20UyK z%=_NwpBvx>f$0G!c#l0vF_OXy>dekg#ZqoYudLVm-nZ@90ChYl&vGybDN*n^3)#7T z+E!?~Hw84MoWN$rGzwCRDiJv$xG`YMvjHBknr;l(oEzW;f_lzVu;JN&bDjd9X9Lc8 z3fAWacxd!|F}U}=Z_Tp-=Q{-tT$vl-!LiO41AN~D_stFP0s-~_RJ|vOr7PT&vj;{K z=MvUHB3Cz}2VUaYfN@Sv&B`3y#3(|lQR5(8GI|{Y(@;neGC{S{CiH4BZHxzs8v|bK z*#Hklq!OXJ(=R5^d&jy_H6ins@cxd!|LE!g2F!pS~IZuH-H^75q zoi7Oez6VBg1H53+Js_p;IrG5bz6XY$4e)TATV1e%nk4Y!awIJFg+V^ z&Qs9$Y`{5BfiXA0L!;+=J9^&(J|8jy@T|c12g3Uw-Y)_Mz!&WP)$S{HAHDOQ zoyYrs)SuY?we2S00PJk7ZN6di=^Nj(@r}Np^JTy(0KfKUpb?Kh&8rc=k3GDgtsVUB z*$j8L+&^5x%Me}9bvtxZfElVeV7gLE@8i?*V3f`lGL|cErQ1e_z&0n2OmA9PETd9CAd0;SXUJU}3a}WAU*D4>pma@DMAhxf56Hqnxpucdf z>Lw5Jz)A=_=(00qjt70twYF0|D7G5y=7srCz4}bY9A|3(+_gDN56Wb^ZL$#(rC>oT z=J?5=IGc%9W<-z$J;T*XGY02#gBov{RR8KX0yT3F`s_U;y~@reYC)cj?3`hm4PoVL z^7bC&f%~v|^%+3r+=Kp)Yn3;6kOz{(_SJ6ys^%W_8P}?A@*odh=tk2rD7`Tt?b$#upkPYK3^hYRiVdWu2(w_R z%LSX}umbh3UIJ?79`q;o^m$vwhny>2b(05qU_t~QbdNJ-jt70pwYJkeXcblCg>6y2`V_|;$AkXJwK+=< z;-(YU$Yn+_%FV0+P8G+^kp%L|=5y&PPR3BN8lfpd3x;M^|LT*0nz;x4;n@evx8H+2 zoc1@bemzh*_n`mnTICHMbJ?Q^G`$X{e9^~Pewt4k&K;_(ne%H0i z8$8Ivab)}I*8x>?5BeR~s&4Wi4+n3+gC6Tlnd3p9bgk`F4}x!b5Atvfre1xFV~*oN z|I4*GOAqpJgxA0NwLs0>gMRz$qnz7&kcacN=GCtOD(4>bTdq~!;6WbFk=j?k8mO9k z&?j80y2*n)oR|O)`YLD291r?U*V<0^pj+O9Je*$udmeq=p5Hk8^L#nCr`xj{2I(b| zcW?bYYZSP`zwn+5FTe1D3($o?;M0Nk2i_Xc19;$x`+v9ptNZV~_{obue(}|7pSt+% z7m@Y7i;r55te4hbw(;8i7w_}?-?YEE_j`Nq+o|q6d*`eCpY{Ke|JXn9)Bbz6|7oMT z@$8MS@_p9#OTJ^@VDAlk^}XlreckTg?0#(b9lNt#ZugnHzMbFQdGF3^w|`^%J=-ta zE^SA)_qIN@_0wB#*=qY}-@WVqwEi2wz}x(M!v^bfSL+@5c-rz=#e zqNYZfgj~;$SLS0U;j$j)vflGt)_bnYde3oL?^|8g`xck=zWM1J?D{>rQPESDRNyJ}CJkBH82iB46aah7bTiBf`Cjd}zx{qr}v zGwT&x*2}xBmvdQ!j>;0|E zdVlS*-e1h?btXJMqAFG)*O!Ma&T7?H#%1m0F6+I_WxbcWtoI#rcRtg;EAFb@=dRjI z*4Eaad-sj&jz?fVw`YGXKEY+Z$GNQcbuQ~Y#$~;)by@Fg=J@~hA6(o2uARqiz8Mtm z{yfq~0KfqcufqX793q~5K6x`jD(WC%PGmT@%7m!yDQFqTPlzf zvjBTIaGt{fJw(aP>w7prKAB*^!}0q`aKKxma2&I41O=WMQtJqu4GLU6I(MXQT(l<` z0-%9bvjS3d-vkVVf#6pV7zjs0D9~`?LabaKwgpWq)q5-=kHcZPayS(!vDxiJiL@Zm z>0DE!gl18I020p`l{;Vqs1ztF7Vd_uL7t^!+^X6ilV5~bqAL_yg%qH#Mh11&o}><+l3fE377kbUx;1|bRMEB0QxW8s69*z z@IeC{1bu&0)mycj)}4b^{|c@<2!c#k4yIo1As`rrP@X zX1#N}?qVmcyNCRN=JkoVvW{-NE`FF+(#Qf-6!SgyHZi^MEe}Jm3x`7UE!vSSt6IG> z(ubf8=WRu^fwL_dT;2*Vsdr%KughQE)bDw7U;5jL&sL!DOEmxl-{`dfSxvd3wI&_YdVvxmG@8zxFaBSVsGA#P?Fu3I|n&a#QiwB5}^lE7q1EteMt zsb~hNj&ijsZpGtqnM4{GU;~-^(CLqPr|V$5alZe9YyWNS;tyZs4*vPzod?W?FI;%r z1v2nAf#X1O|8x6C`_I|?v%NR(p}U{jecdj!^ZPrm+Bx)p(*IKbGq-DH@X{-@%_B7>3j70Pp{Y31EA^i|KYDaW7EfKfp_22*3(5bHyqY# z%A{S-cE-}m>EETO1hR$Y2XZ>k(xVLAlQ4Z?Iz&lufgHEu!El8wjO?K;(E@+%8|Kaa zkc(#RUMdJBx=38hlVWoeAI>xu$MQ^`Dd6csKcy3Wy;+}43Bus7J$>HnyIeGzk19cv zqM_J8XmROorwN!(Bh!#>Q&3MScdb&I7)nGaR-qdFwWrOS{m=K5a#TBM*Q92nHnuvI zUYlPX1O!fWXbhW~EMKo!2(889v4miu6D$^ogF+v~*2 z!=}k4Ei7pZrV*}HGeenbUWp$IBJO;X8XVbqRwM!jh(9h>TT6ymSlJ8$+KE}Dgf0+pD=XQ4r%XW(^)>7>(> zTq2Zfq~%coD!`NIxLsnRx{mSJ?wL3Hb~nvhqY5+15g@5ULy`fPq%Efyya3muG8Y$m zA+sNA*KIQz&j1H|%DmYhxF<)e9fh$v?3n6Pk}h?6t5a6!x4LP$SrKN2KBgwJ$rU-i z+H7&T=7{M=N1^Eyi#4(WSz!2UPo6jXHW$r`N*vjWzl&z0An^!Z6WLZS8#e_gZ&t=?l4!+r8X0A?t=1%t=?z?} zvM}(3C(WC^-$k>{ZgkMgpjEotOiUXPNpP9GQAcNypfRx13NNPAO4rV`R1H}B#Cfyd z=c3s#qUne-gQLCtkVR(k25{g|IG3D2qfDLeCu$BwZl7$1NHAqjm^b^qX9H(0b!!LggCV@u`MRG zUaVmg&3@cW>DkdB$2PfAs#@c(J!anQ_neJX@q#ttuYK*j*&`Rt2C1qP4_QGLMq7M} zwxzC+*IU)HSRu%=)tJqy!6?R<-7*~FuYJwD*|)lA)&qiwzxLJhX207-v!O6jw0mYU zBgCl|Rv(NwqQqz!*9 zFmLwtXJaugc|guXWL^2aF7VZFk=6Yg{zzfq#m>wli<` z)h?R#0Bzx~`RC1k=h@(gOCI1W{I%_Qv#)Z|tOuwHe{E~t>?>U~>j9L)U)!8F`wAD$ zdSL3|uWihmeYuNfJzz=rYrc82FFX4-?~(_+2!CyT-t0?VH0uEy!e3jPH~Sqfn)QGJ z;jdnqH+#iJvmU@4{MGyB&EDstSr7NF{MDDtn|;aIcT!$%xcI9to;Ukq7tMOO4cgmz z`C4r418WyQa`BB9g{@CreB!|$AH3(lJU}jd@xq5Myx{_W;R%6H1%5ct_x-Dn^Sy5U z4}FhY?`{9=_IIxT(AvMO!(08WAKsz2;{H$WJlbF1F8JT(4{rb6#=F-(u=%0Qf7^Wh z=Hs^?w^`hXZ5SJW8xme+*cC}BO_bz9DY>zEnqcUb#711Ugyl(E$ou~B zRKZn*8u&8Avl7kHobMk_5sag&DPpoIi3F$7N`dwL{V9TgQhMdvu|Tjn1`|NONZ%Ju z5sag&2at&plf}d=P13&qd#d2-9XXX_@w|xf3Eua2rwXn@RcH|_Qi%j73ckNRRd6+L zAun-^#F8S;`aXY(AQD;CNF)RISx!t+wC}G^5kw-Zo|9#0 zkyR2&0^bOnD!A%7$z-ZPP+7VlZ0w&V7+!_die#xIQ*=6C+}JuzFtRFGq6-{{3z$N0 zTsTb-Fqhv5Jgp(O1ncdhuO)$Lbas*4@l0q^0?8eTi zf~yVymT-w=i=~3N;XhSy)#VCoK9$a6Y$ma>eVSlsRglal6p2pZqUigVQw3M2qrfSo zBqhmY()Z7&3admMDT9kSp&-dmK1JCr3B^-1pUhz;k;EIP4P|(B z5$5=`Oy)5%Pi}0SD!4i*B&*1PI{bF2Y0(j0>GnILY^~ zYik<`x6k^!_3n;43|lBBqLG2zd2QKb<IdqOw1PfitF^&CPJ=$uRw zV%qoV(*$pMG096TgQxkFEcyQURKeAG#B+tBNQ-HV^L^@6!PV)=NK!tTQD{En`=dqt z|6|qyuiAO+=G)dEv%0nX$NV>lLXKXKLiTX1cJ@KiN)XmA_5^?H9xCoxGI)} z?mRq64u|rEs#U@wN_zkm#VmsKXsKm%tz4;2W2vmcH4-U##?IghL&&@ZMYJH>iu5C2*gp2T`W!D!`$dT(&$R51w{1)Pg*?}JM3(<>#F9b_} zC3^C~AmCea13g3w@IeDWbl}4fg^YsSd|*^gj6#lcg4LujJg8edS% zNP_G&=)q`UHuE`>J6r@#-%0ep5StDQvq~|eq(%{ysipF<%8bHG2wND0)NH?|l4+4{ z_k}P~KP)ETR6LthhN1D0WQj>?94oPa{C28F#(Xc##2hlF0|DCQcqL9PaqmVr~ z)A_Bt@JUg~9&TaI=Yi(oHtD1X+D*~8z@csg)I3Bh>Fg-v)$aE?mXu0`$O}`JV*fUVa=W2b`$z?w;SgvIuBHTFiijX_j-B( zKjAUFV0~MKFFZsW$3nT)Z1tUIu=7B63~`tA7jde7r4oSOvR?sRcba|^i9~OUx22W3 z(JY7~8?TCo(E_8Brj131b(`yF0p&z2Er4Kq3Jw=Dcm9GMON8UId_^wu#d@?p9Z9X0 zk;k>X+#1zOlTs!@T`qKLgr(#fm?n*6I2({{LGpynF597cPG8;->)9-^VY0@ZwJczQ1=}{Jx8Cx%ldf_gx%bYy;*$ z@nZI3;v#bKSr;!|eEh{nUEDtSmxIqAeCFT}4nA@4v4dYY_=$rbIe5px)q^)4yy9SX z&_Adjyx@R4AP=5%03AH@;3)@>IS3s1F8srVzr6723%`5e*FkK-`!Bo~D8Bo1cLeT^ zz}*qJI|6q{;O+?A9f7+eaCZdmj=V{Zil; z{YQPL^oxOC@E;jY>E{DK=RfK>rJoD@tpBL%lzukwe*cl~l-?is8UInoDg8{~r~OB5 zr}R^SpY$KKoYGGQ{=5IE>6HF^;3xb?4X5-If&b<|syn6k1s?DpX-?^Zzam?=|=+p#eei|PU$^? zANC);&?)_J;NAYC7dWMN2Y$$Z^n9oELxFesk4jGIKL_6FKT@31I|D!HKa!o&4+h@h zKa!l%I|6U_ABj%s2Lf;N9|=zBZGrFiAMsA<`vdp;k3a~YFK~b0`}{|oQ~KV(vHys5 zO2>g~{-c6Zx)!+VKgv6$tAYRIKgv0!?+G0Fj~J(P6nLxuDC?Bo8u)JiQN}5Kci=7l zBibo_SK!V5Bg!egIq)X`5$TlP6nLZmDD9Nq7G1zzbtdY)5yW#AS5qvtxMR|H<}KYET+dRgG5 z&i(&W*Mzl;#~1ex-gW?A_`ro120k9B?*GI7tM;F=_g#BW+x?kcYUhu4c>ibo_xS_c z@7jLs)_>W;Hh*oiwehGRSOdE50vEruW|{m1b`O#_j>kX?NPZG<0h3>9Il*~6 z;O861Hqhuej|&9-QsWKI;{j6NIvxQ{j`O$x)h{*Oc!_k$1I`{e&(N7J;5;s%_Df}_ zI?pY!YnMFW^J~YJV-4uK3xxjCnx*r2fa@E_1E9rm9v6W9rIr(%#{-hTaclyOj`O&{ z@-H>s;5;6{{jK9Z(BwFe3wZxh(@oCf0r3x<$8e?#IFAd^|5DlM&a)c6<|Tt^?YQSy z1G?^lD`08O(s?`(2N=g)pv7?>7t{euEhjjShy10DV;yL8oW}*Lz*6H4&f|e*pmp2< znjGhGK{T+`bd&RV;2r?Z({`o{IFAeZfu*ujo#)F+URpbDIo5!#yI?I?TC;Q>4-^K* zaT929oW});!BWc!&f|gIpmE#)8Xf0x!EdnCc!TqJAUtRt*MTO-d0bE)EH&NaJRVpO zfb(e1bOGmaL4>eWcDnQ2vh#RgPpBQMjy0g`F8C9c)-0XJ1DS$xTmxDh=W#)&u+(yb z^LXG{XdG98M#p(vFfA-K-rzhQs25tt6`;v+9v9>bOHDU9j|V;m;5^^%Oc!t-7nBT3 zWv4sOYGk>Wl%=)fvSSVCx(g4g+Qa@ zJT5pNmKtwx9uFiCt>YH}O^)-ppn+Iwy2*JwFhc<6dA>7UzzV8Cz3I^D?mr%ZBLIk*d3F!ck(22y%9@g1BG}D8N&g*-iVmTR`;KjfbVzIuoeL3db7|3Z)QjDbV zf;zLaQ?ZnrttKLJ1kPkekF9PMj?|6J=noq3FoZ;dc>LHYF^Zxs_OOxcB+`e`Dv`QlC_=PH)X`S8n1lH3%n$iG^12XLpanNRoFAr@bY@8Ot!wdaNHo}W9x`HPu;ySCa+u~% z42d?(RC*+3TbFC22rSh`lsF*@k;-L!#vgJiRF4;=6k$)5TJFyCFQlOnjv%oXT_ouE zG@8pGeE+f$<>0i^MIyza5l2!HE>r9i(5TR4o1=7rY)!LlCfON{!tr!Jl*!|%0aEH7 z4y)S4X5z^Z?jj_5+4V(8G<-cI+QXVVzwHJ-DJ0s%Xl+(xlY`lmK7^(%K>R$FF1MO9 zSyVMKTxWH}xbu}aRC|edty0Q$qk?fc)zMJ5-sx2PBSeh$b+E64M(zaYHz+X8AqT6C%!JpF31=S? zT~$9ESymCNP9+7UT9DA>>m*VoiEKpZtNGkzp_I=k6cvic&3u*0w@R^bYEr?6y(noK zCB)@p#_2k6_PzBtuEp0ryypMjz}M~n>Hhn6!uv1VXV+h_|K#=0@BPKz&+NTsPXaLj zf4lxn|Nq$i#oagUzHs;Q+J_I`vGb2RAK5wDsrk1+7J#>J8{6Nyy?^kDt>4-Dq4i(b z8f_(gm$n|g`M)=Rbn``<)aGM1{$%YRe6QYkVB@75g^jQGeaI5EL-SqA4BFvp){+JZUd#5n%|@>mRrN};s*HvTkrTpZL#FLeFJVFZ-#3@Y zGR0UBVf##77gX7#Q=REJLL^6tu2PG~CW)4*QQfKq4{&Aw+ZQ@IPbQ3eW{X!zwg!a=5iRy&JgGCr4g?_t~vS;*g*o1B|k3bY(S?L#Z0CVY_oDphuV!6pAPeho>(@l zylL!l3#;lNsZBE;nVJbkug$uJeh&tm>ToY8sePu00?-SWiRG)+1hV&pg^pC3nlWlF z9Z&UU8KgEX#dF~tTT73#ZPaK`W)RSWcHju{afGs5}^A>yHj?XzQ1<#VVq);b;MmWLdUp7gKT@HS0K>mP~Uv&`7~z zSgXo+TQz2(f+KCaZntof7>B#nc%R0#b_-JLMI~zx5!9p-vB+RL$TdQ~(Ly5EEXcKv z*0k9%spMnGa7=QUxB)sb(cpYI8I`8FL8laxI=gRQNYInwY-BOzVrv%V2sB(53+3KO zkh;O98iUMAbQ}}aD%5E=*59_2h>^iizlurPq%&+;;UUBIl^$$N;=N#;u6A`m2P2dW z18r>m*+POW)s5&78`cLxX@b~LzeI4vkfh9tG!5Z6F$)<{aTXlt)b{TzB+?nYDN%OV z$du)#9P7?X!D3PkadCsHRP>0MYqQNR#EHe)_OC1?QsyMC)$Dd7RjFR?2)QKP7j4rF zD`K++b%|nIoL1m5gCNMx*DfW>G$xcfg`C8V!>B#9;&fd`C;c%c)`?0z7UC^aO@!?f zwz2F-s(5IJ$ap{AG4UKSiGWNIs(mO#n1szxspu$HjbSosbdt=@w=8rdvY}FqE+t}g zD>B0E#tf<#=@?%ORR^;ZETu%U1z|I?kbqk=JIb0_E@GsOmOW7HDAHoaU^w&H za4p>}C-PKqY@%6fce;>(f()W2vh{-2MPW5nwXk|Vt|jc)2n~0qsZ7Nf_8}?;WvuNt zFC?O~8D36VX)z?w*}{+|>%nHf(zFne)=nP?BkrlrV&84enIR89~nF zWJ~3PX{ZpXPtah6?xxGFOgbwKYVfeO_1g=HNX?ii5i3r1y2_*;rE>LpjIPE<4aq9% zCaLxbs*|4(p`o#LFMQS8^h~Oq)`u9lL@7<)oDiWz>`og-4T@CM+N9R z+C(#oU~p=p*~`TG5YP83t$w#Z%rVJqE1y?#6EdzvBiTx`g^hQ&7NZ+%S3^)TTF)j8 zy_QVXX5|q(8ODeh%Ex*`mcmgY3UZ{_+0@#1F8VPpj4X^{s-hax@!qh)jP-~$lE@ra zX8Qv^0SD7#yoD1nY~x3k5{X7XE#;DGR87MJo68c#1R3h8LY?X5D#iq>8a2pl+nnO_ zFC^@0%m^ycUa~YxcH;&-u@h};*q+IfS^~*ThsY?*^`gZ|Ojv*aLc+{N?a+u8nBGLl z^JY?R$$V!P%6Cms=H(Kr+12B#Y2fi9;6?`7`0xGrQ%&? zoHu7hoGC&5h^DOl{8A!XD3+K}HXM_yjhxt=fH@bXE;0ziL&LUHBhd;ag1vY`*;zaX zGp&NuPr-G1Xz)b=ZFSgoJ!uM^W~?E!v~W1zt|?m43`6bE#tRoZng}b5tuQs|1H5dH zO>lgoQiW)3%1x@N(bOJUNMDZ#ol$39T1eF83V5RnCcwK|WLoA*5=l*iFl7{`**>ou z#6U^4?cgxhWc-PRget{zN!e<%TBV!Pxk-yCg~?25l&+%@dS+$GMk?RPDG^cG_{aGR zK&BL`8O`Ce$Hi40vh?JzUoSO-BZ=or@%*Syh6VyFo0FJ|;0qPXBuU$~A=*{bJyac* z)Cyll>tkpX?*o``yguRc9X;6UwHsT1wUCgynTS=Oq#Ts(f%j0cX;j7<;NtDX4AEdo;1ShR|y86;i5H$!xqO!^F6)j}*R{igft0vj2&>L^j^;rBSw_S>b74 zFGR&rNG>tP0O@vGQ#nqItrCS26f=z0_J0a!{HL3>{zv)O;{N~bYk~3ZYquY@{`UVU z{r-2;PmmAxo3CFxdWeKNJ9Es+b-%+vbJBHx_(Ucq58+A9aio`^sgtkoJ!H@E%EZ*? zZ51DO8is9ADyB}7-4(`hrw3!6WOhC zjSz+jN+&jM3`JC@Va-z6UM5^R6xd-W376tbCaRHlo(~qYt!A@0YK@Ddsy$AETMu&7 z3-elQ97~iiRLKyX%QVcy;<->oYOq2DvmtuagX>(1v3hhdE7`TKJVU{I;~+*Bn_9G` zyNhy|cYSf~`0VS~jvm(B`E55RGBJ7CZcex=x%ZOS23%b`K13VGmvikHb#|ez)V1R+ z`xVf2hu4nba46;koSk?Rqt$v9kY1vFSi5X8ddG^*z*XSTh~{TRdcq87kb_;L*-%4Z ztUJDmsU>6JrmzvGyF>DFX4L8olhu4dBDx^w^UzeXGr?wiQ992kk#0uLFp3FlSuV#J zw91&GFr{gz-)%`_IM0tFrb37^II$X^orxI=&AYz1c0{jVJ9==}b6a^K^n9-!AEMQ9 zr`L|RIPXJu?Fi@wAdfIr%NvL%i=vZ^5@l{X1qo48g1?7y?KJt!8DQsdKNse*f=0 z6TX7?|M6HXbi1ccRYVm+njkP%ZxDmVi0-NxjP97JI>@E2w3Bp)SMnee(P4wRlsMkn<(2ObA^x|8Wptctf~uK1F~{cGTDRj1Tih>1B!2_mDD&p(3v+8tX)YL{D033# zKzvyxA4k%YXgQYc6^0=>8-$H%sh3QSgEokbkwObW{~uh}sCR_ z*%-z-wjuC5ve38-(v!{wZ5q?LN?yX{S&=X(Q3X+$eQS#ATCB#+GC~wn;{_$zV6lbZ z`p+)~t!h3v?uTpRa3k0XN>CymEynA`ZdXZ_O@vDH4Y4N`_+)q~xc^N{!9k4zryeC6 z71>6DYz@k2ypEVsW31_^W-0;OY=P|1CRs8T8n=IHDQMR61-TQiwTr_bh-ZvjOtz<_ zKrmA&XBL3YOlpi&DQ?tb7lH@xeEKHl4343L7x7AMc*8+B6oFuPQSHJ@mhHDKY7!^e z6gJC+(TNzYB!Z^MDUj8iLUmj20vCJq<9-I zqfBF>WCxNk;d@ zbv*@gakjE4s2a2TVv8;-ono6pDtgaskBEwb!QNztqwoTRs z)TE*!vIym^0g@WRc?z#|QvIQ*j@#oIwGdqU?WLfW9U-yIK)2!;S09GC!Yo9kkZQHi z9b0Xy7cUlv8YJXm#iii>JC}lL3^TQMrjE)%I@u&6VS2!ctrlL37Rd(E7F%(l(@}?b zYw0IDZ(9o1x^Ou?c{vGRdMO3Nl=VtM%Hbo~d->DXK%ISY|mL@3}76>6BAu zdw`M{4uVH=CXMw`qCCswjO4H^*OJ4S6$!yJdg%ZgA6W`k5SSoKAXH9CYSm1nAxQ}t zf+FPz9;?j+u={Np>9U z1CQ3?h4F+>nMTu!(UovSV?^C-V3FXmkK2*!f^{~@h$t-dNjA+D#$c&HS*uwH8>JY_ zgc%eBd~=0$T@K@F9p*#n1SA?cUK7nB(<8!5Q~e+@?LrW5M+c}P zwL;CX-0oEqGb}3N9l6@Z;}%w(^`Hb+=V$qnvKSoS_EL~*nINxjsytN&Erbsk3UKHqs)q?g*|-MTy8|`icmZ zb0kb$pU1tAEd_I3ESt!Ol@MYRBAP8XbL_|{YAsqWOlQFeV#Iiqtswer`J~uqmVyl0 zC`VI@tgu)eMI-SD1)D}p3#QwnT%#@F!f@CMH9%V8rQktqDVQlxdU95SVvtxN^M(~` zOd^cY3PA2g(HwN{>SFp5UIi5L?P$y_&+4bRGC zX^cd=QawWjC(FUv`{YuPBIuAARcZyyl=A&3$Lh5Z!DP7##v-X^qE!KFvJlBlgQ^X8{e_{nBBkL{kh%O@4ae|-@A9Wc0d(ND+hrGhVONP{>iP&W#Q)qD&ODd1!8gC52 zF*{)5+33@_Wy06Lrk+IiK3jwMW#G4g_C+?GNPkMrD2S*Zm?EPX3;Ke z#*(4^Z(B&{TyWHq8?c#a>PWFmnF%f2>Zq!n$qKc8L=mx8q1q|VXlw5;781dxS!c{P z!bVFWt8dMQ5imGGYcj2wQBE@w>C^-cO(FuyZgm&^7{)U+8pOs$@FI?nGM#Z@oK}N& zmc<*Bf)Z`EX(Tfa)zVpGXVDL%Cy<22hl?l-qGYXZKT{tgxB+98bS=XhL8z2z#tX?f zk;XQkztGX)bFdidQdAp~ z(&0>Xz}J#RzHAN4sjb;ULQ~NSY9`temuz(OvY>@+w9bd7;ZiwVACcXPJ!>Y-Og1R& zr56%vDKbMRf>0InG8`&)R1FW+hw#|eX`)7#t;nb?LG5x(q;~(;QX*JY4Yh|v+Cnj; z!AT8`wZzD%*iuIIUY0S2)kN0nDs3BC{|wkQKkCVKAEA}!J@4CeoCO8q zXhEzF`4+)uD|OUPj6366IRTGCv+)!xivkEHk7VQq<;k7$HB7S2AIM zCYq@;YU8UH5^bs4(8Ixsoa>>i7%XaZvjTQ(q?gP`;BGzLmxpyLC$u6p-y$-<-xkwU zlC4rHtBV-|qttZK2%Ggt8lciLH4RSdkXoi%r3$tFw1tiaM`1I5n6>e6Sv7-&ZZn=q z*l+>G3Sp~00KlY4dD@qvMRxuEg+x7>k$5v29f=dDVCGVpS-BH38a7W3S*(~aLb=go zoXx;BYW;5)6IdzQn4Rg%MaYCus1}((7)rz&NoF>wr4)3G#VRCTE@EW}Tc{`(yX7f5 zp-Z`uRzxz=fM)6z--r~0h1ytEwOZ2>BDBg=l&`apkisd8iS-6N!W7FrwaMvKnQI`! zTGgaQB@5CkLm?~(ArWN#_ZJcbr$*64K0D3AwGm#S`Xw0W)ch!Hje|vQnxi>W400sf zhJ3%UkU;tsQjGVfqYn5So7oMtMT+tyorgyC;z*CpBnCw6Pc$NntpDOdBC16vwQ)Qa zDli?CR?1`^E-SU31vScIUWn9sjZ$eQcas^)_hSo*aJYo0^LDpii6qLn9L0k*rI`+P zvNO0tWVA^d>0`k}d7NN(KE99$^^&z7le6>@95o@nR;sH4MMb;iBxa`5sT7%K3Uzo2 zg1@(Zav>25l1Nk1^@_-uu!@ZBQVxgZ21ks*#@Xtj!+4RQTH>U~to`Le0?y?_r8FLE zrW?7;q)U&*DQ$Op@v4PVL3w5tBG>>AkpRfL`No9=WP{J0nE;XKMAHpOli)!WPN{8K z?T~6XJW+`M$KIRAxssK28GY^YX;#TERVI*Ei=y(>qOOVj|_U<~n%u1xBY(Lb^5l|l7 zWDE@ZId?xA=tfOV$1+W~o{gj_Z@ySEkd2ptnq;?f2e*|@6uiDhu3Ku#>0-M_V>}s% zlV~{N?>AGiPP)>~gkZ2esPbFmizux|INz+hWASpxgXP?&=7U>cj}$Bw!%8{c18-|3 zA-@w_j znF$BHnSn;IJXh`4^-e#o7&KSRdGuVv$MpC$Y0N7eZ}+q|;!89*-6x}kBHHY=w6fkF z7NjKJlvujz36*0?fnT!^+CMZ`23r+G59?f3HC4<@d&N-4-xm7~C?S^`RyElSsQsG2 z>EU*s*!WG`tj8_|m-e%JuiIPOErJRTkJ$c!t>4>v{AO$84>umWZh}?&)6cwg^`ooL zUio3yXIe`#5M8o#u(xv_la%nLSLizEg=lt}R`OP3Xi=_@RP>wnpfL=BT+7;zUR~K7{cb+5pf`252Z96OOqC}OV=xLqZuCf^@|WFqxwpTx zd0^wFZR5EGvWg$du6VqOz_theH8A@9*|{xz)@8%?;+;3Vc;?&!X~+*uJbJL^Y=h!i zDl6A|RYNcvX(|?Vc=U{I=~q8w|Nfg#OtaUSL-%FpE_ZEvFWj(q_1pr<%n$6np4F~< zyq&AIg;&={zdtj#olm{ovF-HUuyf_y0*TNM?7TT`+3}XH*fxIgSMA?l_RVeS?U&m} z;>hD~rm+x2v@IpggZv|6Q^+X+@1T&XrmaClMY36`zJP#hg==_row`MPcC&wSt9mfn6@JrYL{jHOJy9g3yVXdSO~ zdP%aBYy=U~Y?53Q+>b7xkwIM?2u16_(t|iU*2YmBz2@b%jjy>nw~e=5R-AFPK(6@% z6A$9(SQBmAUj5?H?`iYdla^pxGEs&i4z{Cl0)KiQ6uwPDOhdq;Rw%gV8q&Nb} zw}93E?XGDao$x%WABm#{QrsWdds7@8Yv(AAF74WOzH&IXolm`7I}%3=q{lz7^H3Ze zYw0MCe(-{A>C2V5ExrA6^++5okU9Ur(t|iU*2YmB-8J&&T`!;8#@jAeoN=^3+Wi9) z58~)p6K(&!_&Z0xwYe>P)@Auf94(Ni|G>lvadhnciqU(W_9M2vO<|h7&TA0hc~m|U zM+;>8Kd|?vI6BtOQ5-c!GpNGNZRb-jmyX2I0^d;%>^u}l$67jyqiWIiXzhJ-TYCHD z;*mI7;A-80r3Z0ztc{~MDvnkk#mjTsc-!TIGmaLxgLGixK^z@xqV2yI!lU1>oZG@@ zUH*Y1akN0mf&&vL#L=<&yGL;(e8sj`eAYC3o$0$l9OaM1(E`^u4(z=tj*hi+6i58; z*>)z|^V(^z|DU|{vrGH;?mvC+_xI4<4}*dO{{Siyu5SO2ZO_)bw_dpUvCZnngBw4- zaen>h*O9dkt$qKQZ=F%jEU*5|>N8f}yMnns;;JlvbNSWFXFxM2`BT0332QQ_{jhY- zD|QM=u9KxWp6)-XyXso$u?6-#uy zUL1J#=E#59=DptyR6XqlOXp0Ta3ettLuD?Ksr67}DDz>Q10M<#VJgugRI;VkD#HOu zHs@9~4&5mua-JX8@^Y6w#%;*3D6*A(D{4 z5UtK_%Q!~0dmjgM+zE6nP-F3Ij*T`#W=X77OIBAY=nY}AYmWw*eD7m{z6(I#0+kl$ z)^{UFs`owy=&>oYK$XS0^*AW=XnU9dWqj#GG}uRnP$!``!xaX6YNYyX5MrxTwwFK> zzP{IL@%X*6sq?aY@1uY|n=%X3 zSDahl&6Ig0&@-B45ZnxxGJKtlqwO@pwF_1`MArQ(jrHR3dcBY0YBekt!C8U1^*AW= z9riE*$}CVfac)&JDRX2;v~ceuY+HbP7uW}#+m>sT*#|mo$}F&vdHlA}49e^QeKutl z*od54-_4ZS1$u1CEU=L{w;l&&cI;sSlv!XWaBfwzDdSvX7w+A*Z2{e0V9RfATdq-N z3+S*Zv%sF+*<*J=W>97m=(8!az@FRO`fjGo2GCsTIRkXqlv!YV>g=(L{TY;51^R5tEU>*ax4xSxvjX(k zlv!YVXl^|Y%DC)d0+d-`e`ju0vng|QmRYuK0o`6;BWG?~u2E(Q=&&iXz)sBZi8iKF z=AOHNKASQN?4-=C?{-lh%K>HXc@fZIQ)YophvU;#%%IE==(8!az^1|6`fgChUjIL7sj#$v*WSPG zy=3=ac3-sf&pU(dZ*5y!-`whL{{3bb-0g3#e|^2V_O&(b%va7Rs}HQ!SH84Tb$!7l zFMocywDdUupZ;_4%F|b6nP23U>}20{E?#-s!Umk^3(mzWPhHr66MeC~c;zW00~ToX zq=(?#!Uml15IlKgzydGPC;O6n@yZ<|0~UCzJ=xc{i&xGrY`}@Wvt7J$`@#mC=)3a8 zD^D63u)vG{Ne{to3mb64L-53f4LIQ;c*4kl1sXl+A^7fv4LIQ;c>KtK1(s$fyL7sE z<#8he7Fa8t?4sb}mB%h@z=^H|E?#-e!Umk^3hd&QM~@6xVEK5`L-1V-8*suy@SO`A zaKc0IsF48+G4qj*$TiEcj1$L4NVdBSr=+umV5XrRK#e`wJUzqU+0x zSN0Y*;6&H;7q9G&3|L@O;-rUQXJG?QcnG!^HsFMZU~6Q+0*#*R5ZL+up0-h1x>1d9 z?~{AKyw}?MzP+8@Kiz%ZuDKiDedNw(cYbr{#XI!4Mj)sVnApB;>nmGt+j`YjX6xMM z-)+8g^T+JUfY+)5PTvkV{g+b@oO~ggdZc()EH1ntR!QqPS{IYq0jU`R>VDKUnuj;lnr4Yb_PBgKBZtJH%2 zSf(Mu5e086N{P{`1&Z|vWxf-jie+Z`;wY0r+wg&6gqGG%;qi>0E$}J9LIg8MnXx|F z&-H7hoC6scI&#YzCzfb;I+0{A2JXS@!B!T@=6u6oFkHz5O>c`#r?EYb9SFZ(4{FssflO_ilT{byeMUh{+fizIa5W{WD~bm zZXfHYSBe%R5+bBPK~m!jtlyWlKFYNTYX-SB&xAQ?xi>z0<^K1NhEZz>A&HA$p= zW;Vh2OsJT(uuL0a$U&(h3ZXzY-Xbg2==v+hh>AAQ@L>&sKqdiSq2J8;ASwl_^%Vq@ zFUqDYg5+LJ74mD$YT^Kar`(w~1Cl6ohD4VLdGS^#7^1^S%S(5%rGPgtXFSb_#|Ialy29o`z63I{MF%DNIL6RDcog@b!3uhsu&t1QyBrz5}K&D zqCz)Sa#xF0tkzEm5q|-1x9CI&+4|uzA}!?u;jBWTJ=$VAVz1nB_Xn9yEeeVc;F%#o z`OU+kBghcUWUmnhs>%AekY#kWtC5ubsAf5CCL$SWvGd#T#QZ@K9FFrsd za7hMvAG54t<;8l=6ZfG|sw)wL6vWhmOp=Lb5&~hM)wOZHi#BU^n0yyUij3up3KWHy zUN5UENx#oy2}2F!&nW5{ukIzguGfrpu!^3_VQs5`c(gtrq|)6^uxwh{mefnZj8;M8fry%w&87<*>tINj zOUkt<*(tJ#qT%<}WTC>+^(KsFYEmjTem(ai-JoLuk6}Es~);EbxFL zO~sRG`mn)f$-g8&Swbj}eg`TJEAyy9&iYDcV#$Q3ZK<%8|G`*8rtRg(4|} z6s~C0(?NDO#|WzK1!a|bC@Ghzo`)NFbJ{>O`E*k#MTttLfaC)>W)XE!S@#_vq+l?k z$Fx8RO&a(h*eF)XkguLIt)3d{2R&vgn4u*R5%4p=et-yp+=QTZlsh=68eA6E2U5Xb zCfIN;&_F_Yt^>!RUMJn`N^9MP5uJq*ZF~Li-e@oFe`^2r`*-aN`_BO906qb70lZ`{ zx%V`19^hlU|8w_6yNTVWfO7$VwDa1X!4A9gWN<#VZ=aoOhb`xJtHm|Jo<#7yZP%qQAHH@%cqRHm~UKu6=ZV(Lb73^c`y-nP2q7 z^NPNG?L+g6esEsVx2=6(e$n^OEBe;8Kb&9mee;U`&e|W$FZ$kjMc=aa`}2#wXI{}a zuf2O-(fiikHLvKEwRg@d`rB*w&nx<-wcndp^o?u3JFn;)*4}X-niDOVB0@E#-#3PX zN;v44GM0XI>9zBUzIN$X<`w;wrC**`^p}@@X|I8?%4=8FLH9u4?wFE@ z!tcC^XQN6@-H>$yz+QC6_)=E4R&Uv@*jI>biE_Q$QVRpUpvm^1Cnx0eqqgY3R~_<6 zhTm!(atuMH)*(+-p<;xGm^MhP9?z94rlj*Ff4n8YUd0Eta2bqf(h-I8`Z^Gf2n?^s-too^BM{Y@$z*9HfTuf?^uTpkcsuyw@PR{X!w*tquaMR4!0S$T2-w zzu@g-8I_MPXun$w`O@A{h@Y!8v)%P^9rEwHR)>56x}IF*Ax9(c1%N%HGQi;boNjah z(VlPr$q^mnn|e0WZZ2v35JktW)*(Ma-iNRbIqJs{;H_GrTa#_4zQ``Y3~HF!f$Y`W z3NonHaH)JjN?YMhryUTZyi9h7O{Gvp**Yl4P*bG{h1@dPlQcBIq$)v{g$S2WE*R0K zfJyXFj;R~Fez^QTrd@$#e9xLe+aNiKARlKB?v=~x^ zD!w`EklS4!*CBuIwL0VroJ2nU@~Go6;|Fe@_?Yn?CHozV+}^d>zR0!Lk^BNzb|*i^ z7C13{Yjwzv%^_y@CK^QN$ky7`fIG*gjHeGmi>Z5?vh8}LsJ#PgMg6z>i6=ukAv z*7<^$h$kA|0W1VGGhuQeq8IBGi8iEHqqiK0Ar1CJG+oIxRUH)dz%$W7dJr(<7?Wv= zX0;!r{4qKUMLY&9u-y4L6hq26)RT;tb8$`YT+mx3z7_KYg%W{Bd6;Q}k`!|WqTO{6 zh?k7Z-#dEW>n)$`CHZ*+(eocN5EnQ%J}D4y+yl2G`)hnv?(ul-m zxn%U5$SxkZ?UEF`9K%!MrE63GC?%b%q_>A0V53>p(~i46Q~H6j_~-9=u0L!j8~dG z?j2^~6r?BHI`72lfD^wfef1=PrL7{Q0DQez1CXal`e3v#%&EExEoj zWd(vd--W`U1W$&}Puq>GsxQi9)_wsMfQpw z#9$hDpA!gxr7Mh3?XyGNiVGz#J8X4}9=2AbYG%pH`?+w{V!*LDpu;oL5lQ+=RJxi- z1#6OrRZCvR)-_0sD+fL1!~@m>;e^!fvm?@D@QT| zAZy}~fuMA&@8)2*mWCvPgVSY`Nk;mvuA)_W60DRpZHwS59X#NpCAu$|9p-Iqd3SwBasi;urx73fUMA zE2WT%XL5{Bj8d?GL&3NVj$>AEg^63XtAF6Ss~?!gYUfKxjO7yvA`?mF9!{=iPKwn; zx+A1jOBB;+G7K|Rl<$>26SpFO!zgW}UA~QXWe9{ZTBO?y6n2 z>cMSS=dZi^%hOzqI9N?Y;$jkyr&;mgN&8}hiL6d% z;>BVPBsi?FFjXQ^Z`13mK)zhB9FZ(5Y6)Di>Os9ZDm!xZ3$MHSOVeBpJ6tW2v=HP8 zPz?Oxb9LBT66r_`8&t>&*vSP)?5jE&$5<1Tqh~rAoeN8JmoQRVlNz!5 z`>(tD3sYR}4>(vYF>IDe#fVJ&VdQELcT%h-Yebwxw4_=_BsExI#b!Vju?emY^@(O* zz}jjK#l>E*(SuSH-!G{USIFm5R+S)7EQzYkXgkE)bM^DDyZW!ExEh5Vt`5b*Q7#x` zY35<%YA?uO-`vviR>g-gX=`uT!p5xv7At= zXV`&9O=R&xT`vy%aw(Q_yCaHw2$z$k$cWY1>#qLXG*|l_6G(*NL=qQCHuA7?^+`Pz ziLx4lauTgIz2yd-6tiW#NKJYJ0k@Sf#O*D5QwcL2@hCCeQ%6%#iV>|sLdmsi`7k?x zy2(_k$~W!v|N9%^rTrK0eQVFy{raxD^QE2g_Gh=hf9um*!sbUeG0u@nS`0BbuPdLL78wrmeAro zPhL!eYryfQA{7WW6C?{vSW8;jVMS~<6X~-lGiDKZTxzl%(3du@wj4IB{Ot`J1TjIy zTD^D)HF%~_YI^l9mnh5KR-+fGWK%Hgju$YVtM$_7f}{*e$y&Nwu-Z&2WY#OHn9Zmp z1HP~5oQe8%y_ObRg_c-7TcNY{bgNk8)Oax>YKf}z2=20Z!-iDE@6H4$u*E>TDJzQv z8quNQE|fCvL{*W&<^Fn{^2$V~7d_Wyz-JvVlJCKVn3ij1*@&1x6G3ppmt+bJG9@GK z%)sA32_JJdSKt&wh-TzuHJI&Jdc}yt539lr8z+ADWp>})An ztkn9sa-wSx-2|1udk!16f8>S@oD~)WM#|9Ag_@Yd;Vhriyk)9rXmX~AREqItG?-7+ zyN&L-oQEl)h#vQKe1jsD!h)#T=*3xqrCV~1%MY za3!B}*sycoO*RDT{(69;{Dh2B9V?KnisPrTMsSi17CKZflk170swFzh123PsVMEL^m6R9iK%sC)XL8+kgJUas#ha9< zDiv#1V31}6E=Gi5^<2MWz;sUM!N*=Ob(T_fkg5d1y->Z`9}r}YlQSK6RiQzBZ|Q8X zIT)%Ut0WV8sTFPn!&QeL*8lv54G}Ptq!j_qWl95|J5d|LZ7rZwTAfmekOEp+fIa>; zE!UIGxk6B?q3*;G4K}z$A|KD;{g{V`A{CQ}doUs!#zT@)ifd%Bdsa~87&pKPwbQB& z1|6Ko9X2d|<0cyd^q`f{kYR5)U~&aXQsGoviiIluI!Q)KITq@r{LC;`>YmGUbs$*7 zQjBI`QXm^^H>e@10ynj>bey(iu1qUf1`2kXXR~#S>hm$HQSBK-NR6cjhivc!*2*_* zAbohvE5{Uy)jVj4h!DLtYIvIDAgohKxzKBuW5X2N3#W*4Sw94c1uyP1VzqREVZ=aoO<9tp$A4OgA%@A zqsqh!ITE4k5=T|rdd*0Od*zW*0C>H`PVh0){P%~G~z`b13U8yt-$iS_7 zN=Hcx0~w;Q7LKw=UGwC^ITP7=;uukjW(mmD;Ji1dnBfM*n@NI+wmBLN1p-OEp>uwf z&1XtVb@|W6i20WQ8$UM6wU?+=f`%K1Ei@BP7cio2nTdg?#(KksJcP}p3`>3`g1~XP z`K+-Dxyq6~kcElNH7y~bC{eH?MuhI5C4tOosbEnU=EOn2T=77zl>-EAnIh+b2yff( z>3b>|RA^weYP6DIG8H7!BXZHEk{VKNX8He&5hb=SmoW$$n$o-fGgieyhZq{NvMpd_1tiU0SiUdR5Y%W#s`nXmiUS0kA7$L^fRklnSe8Zzt z5fiKAhpHFqR}@GtTNL5T<@0nq)%Fj&>*I`CiA;o!)dC7Eo1S>PA}Z}xnf7+wd5@PR zc<+$LHMZze$Yuds`p{TM+AVi7{<5C~Yph{j2xsVgwbf>-nvrjHdSx7K1Qj^cXqK(D zmyQvszD24bO0@i0EmY3JRI}>u`+P#F?M7v-%7bKH*;E;cNzK)tKR~2QX`z}f2@Iij zGcqm4{293BcT0E@CyWxvvq<)1Aw17O+u9hBgu<;Ps|Sk#EJUZO9j@=^147Ivcvy)} zz|lm5Z_-AfgAv=08zT}%FI?@adKPOZa|EZA5zd-=bK781c^n9#kdE7>(y8fyI zM3lhjW(Y~w2H{dU8SY_Qh0`%T5Ew9L{_j`^9d`4q)@dOPtj$KUJk^eeqn>Kor{)wP@5@Ae zJQgRkNWQ!DyJJMO*~o)anna`<_trwB(&K%7($n&C>9pRGxF($HbQ={yAM#t@ae&bK zc?#EJ8UL_UYFH9cXF3&vpu>TS`Yp0Z6*;D2QXL*f zd0ewfE8jdoWP+e#bYs}m*@32T4T`FTE2580fW3s+Fw^z*LajaYD+} zux`t=^dUD42Smc3388dX_O`l2uH`SLn{+@Cu!DSl38Gq$n!ZvYpfD|WAfGR39I51J z$Qy|T>d_Qh%tVrG4r(Q(r9T<#2(|q_x>V2i5@et%$LLVk2(YyRmx6=+QdRVo+)@zY zicN#rTsuH0Xt)H&aG`{$D4XFt-C7&0=uwrn22iK#Lolrv2d5QPeCyl+BGc%HTYMgm zQ|)>a%VdxqXM(csAuAOMnyfD!&G?BZ6%>@U+s9LTK+GnB)j_R8m64&sm)n_PwoEsR?sM8LRs`X(XoA+yQ(*Y>pE$Ccqq%BIE{FgA-N^h9Yg#2ex~es%11i z0+R~R!$udyM7YVUy?SgFltaLBUJc64jtp`px~ust5@CFLsYB;OZNW0Bq*Y1MgcV(W z{n%9y7HkFcLB8tCHIe~7Pg@OGEmrzsR#00yYBh=+HcUl8f>uz_5ZwP?{*|TepWJxd z>Q8|GIQ@CJdEoB)<hRSl1Ohid7nI0}z5vnK?It`N-V1x2_{dL*P z_;VNPtZ+W!sW4?6DX5ibrA;fL2*Qctc{mi$rqkiMZl903KE99}ymldXff{8ecl#VW zO7=SzslaU0aFI$$Bl!g~Tui=@yFe|mN%sIJ+=B((xcNTdLv)LNf+63I3%MQ`ntm~` zRx07u3Z^y7yvG~h5+SbU4R(elAIL-=8ybZRCZ6z;f_Z_y^^1XWOXibmMMxOMSZgRW zD2+gC$*A1%`pS3;r#b}5ut*>cz6Fz56(l_ORq?i7^w$+3CB!de>jOc@y1lNJ6^(8r zp%%$nCTPubF>uuN@r7Lc+J)Q&*mZJ&i2jER#04tKO$x;8_hGyCyzp%Z!0QHlTQ1~! zAdh$IgR_nS#F9V|AZ9U{ybk#c>|qmYolSE%L$=GHS(V4{}s^g@E# z0i_oP-N7IpZwGU{pYQgIwQ#M`PnCGNEcL6ocq-)g2O{7DH3n8`=S!V797!R>`STb0 z7XrP$PgkvUEF6f%!}xp;K)dT8?p~!_ex4n7KVDj1+r(!pPy`)_GZ;)f7P_lbB5~}NFeRRWrI3UFpOr?n+G-c2%9 zQWn4gu?vG3I06$4c*~fQ45MtA^!qy{0q;e)Hk}KynWz<2&Z~OGNDElZ3KZi^vdfU?46V80`%$v@<w;?9+wA9T5wzqS0HL!|gw7?;rQxz4xPg z*}W(4er@-yyRX=J!Y;S__@%dPls2Bd{!i<_zy9Ovx$Q5nKV|LfYi|eV2VM?p2tIM^ zfvvY}y<#f~DhGUJ^R1hgHdC9oZ+vy*?Q54dj?F}ftiIrkYjS8|9_N+GW$j0=e)dOP z|1=8(IWLshpx3|F^}tM!^CG91U0oC%ub zh@X45>kBhM&bz&~rvLGiuD_lMa^B~&LH9hu_4yefG~m2%X@g$Vbp6##kaLA@gI>Mn z`rJ&AbF#HTFWPnepP8UZjIbW(`s_^5Bt{gU;`+;(ph=8)Vczu@GeMIWLA=5B=QBZ* z7!mv#*JoyeCNbi>zvlY0nV?CGcJ37Wu& z2R`$2u0NRxn!tz$KK)qNAI}6$V8jETe5dPEGeHv=@xaHLu20ScO<=?WAO2siPs{{O zV8jC-c#`YmGeHv=@xULTu8-|}amlyv4(frXt|4~~H|+L6Z~(lRc=9j?qtFBzeI-^{ zS(#xT=AF=|uiSQK#WfQ&p-)#I{F9aCnV<>gUH!j*zp^w7?@Z7H!e0Gc-}TRTtUmY5 z=JUF(K~*huyN<11Fn7PP^$mDcI`E-*}g@?pb~QRKx_xeSiPVRsvEH6C7~$&tB;I$5|kFLWi#Y>7!iVoC%r` z0#`r&4c9-+1WjP{)enBe_4hMD6F7bK{jYI-VSKs{=*Wb_jLlI!Z$#)C`GUcPf_{r>g4*6&<<5Y*kdYwgZ651zUI%w4B z53b(7de`cmD-W*RzjD{govsI6_q*suSd(YeX@g08WQQM!|zGqw7e!|vYfNFuot#g}S*nHh)W77=`xb>ei z4=%m**3tiV8*eY=XX1~%yiu_rp9;oeWES8@tNPh^C;#YNI+6_YJQ26?_5y!4-pM~A z#pNqE2g&iJ{JIM2ra=4)LaoQ*#cKi6{{+W6o2A=d|Hq!bawP$V>Z{@Huqv+>UHXGtL(7ipeO+WPHn@TH{{ z`rrN*aN5GD2mY&jU{)lem~$pbMIsCpNrH45_Dp1NYtP0zXM#{LkqU*=Omy@lw0F2? zh~e>UT;O2rp&HNCG^CY`ncDpN)5V zE=iJpw5#HEc|Ks&vSg)gy3=IuA^W6uHmN&L@q(AJinB83LepT zyaaja!X%rHv~;X00pz)28L-JLK?8s# zn=@dOS%L-tOEzY}CbI+u2X`;6PlsVsSb_nTtWAdnCbI+$0G6DY4#TFf1OqHtodKJ| z5)80pWd>}@aARP&T{B=)$b|uNEzf{W8E#M~_tMe~*c5VMUgYk(X27NlH|9a^zBmIm zgVZ=aoO1hQn-swJzd6!eC0%MFG` z4YG6@q~z-=ycKOlMO9Sv`5HWO;FW!OaNK2zXQ?cx(NHx6vyrA^(K#DDI?DK{!DDl% zO&R4Qr9iiqzia6^OJ7~u{TEky_mk_tu=2$9zuEnb-Me>Rux{=8clUO_u=XcA_pj|Q zeRb!aou=zocc`5wE&uuY)7Bnb{?YAkZGUw8b=xlmr~2XTt*y_kp<8d?`l&6|_2G@j zY=vECww~Z}uV|n$z=t+pyY^d~FW$^7%bOQA*Ejxhh1ht@#*c4Q7n(+%w!*v4nmU=) zl~Af86uT)l)-kS^xR|4@V_N7M--Cf|ao2N}?^{aD(cIi3BfY~^DW->VAu+w59YWJ`A^rp0mBj zj^qYxB~%D933=G-SPGrQvraj6EGO2Y-FUakW~h=*#5-Z<6zHmszB&xfM!XeTnJL+pmb!WTK@Dp z_wO;29#@KO`9tG=BsHegq{DKk*TTbAyHIlW;}1r1R!~U?@dh{4xk0}ZFV$41+`HzK zd*}N57MNMgpsCVCHl5;QX-VtGO3w0K%Wog)?G-hl%#(PnQ_Cq-Iv(yj?YnQ|&IR=L z+EIeZ4$CqtXUnwKvue)vUO&=1z=zoq8;>x(Hp!JZx$k^pE&tlylNZoC?1kELv&rco zcf8c$%YA2QsO6s?=^b{niCQ!kF4pT+lC_XS zY^;zIh)r@ipfzs%xDr5^v-|3azQFDE>sJ4l2$>NMqo7Rnrb1As|_;GW!qS>ay zl|GZ|<`QjFb(XlgW87R;V)}({EtXle9)k&)rR+E}?-8mIJ za@3sFHM0VpZT2z>kt>DBZrI_xrMHZm(>wLVFsnovP*06*(y_Ed?zVArwcbFZoAFAz ztdQk?b3i!6mjC-$F5PRE&1zS}>2{1x2Sd)lSbEi1E?zfi$*h_zJuK*{sOg+omi}UF zUbMy(EBsI@(#>L`Yz>?>%Aj#`_@LP2$SSBGX(?Kj!JVs@r5_u~nS&5nNekeD5SiTx?x`*FShuU)~sX z!^~)@a!Iuk(Y6#W>RQ6NT*JoAC6jEQ9Zk+Gqqu+Hi6G2`a&G#<@$LV84y8A@h*mZP^zZyYt(>&FIK zCz6x1Nhy@GWW~vqtxF zTVu7f5u>sKTPE~Sg5{_q50}R-EiM1~$-ZTrw)U{`z&QW^qnCCrZ9Hc67nUFWurci0 zYRD_kT&AvNAvXeEIx0$$P&`_fQGbTwgno?m6rdKvhI<(@kVbmBe6ky)nrDyQmYFl< zzQ;Q$32TkTvhm0u)h-XQLTL~rnU-Lc;hGo@$7H!uV+1;!Rnr&~7-bJWw?MIgQPNfj z#!{x<4#m=Fw2oIgy(C#mHi8IgHc2jOWUJ~h0KuR?W*+SHd6lJy?Wax|z-t47dv4b()u$2=EYGhFD z#EA12TOER%N8VmCgwK&PG)dax#hTvQ-WX#jk-|9MO`NPjH+N<*bu&W@_d;h%U-iB>5Yg7 z!{^FEKI*#tm^UwnN00fz^0@_K?&PK$=-)cs*iX^X6X|-I{^#GZ&!_dsy{D>n(SU0Y zaL+Y&jS2<;f#=)(A6E}h8caiKnXC%A)Swbg)YJYzDw&oeIY{d&oRMbIU8;XUz-5AI zB+9DC#N!DlP~$7bk`j(o+TIJDRF4YHS8l}ax}EH*a_}De*5pXKd}AU?Tg#l zZD@OA>$6+Gv-J~OwXNrEJ$Cc!n}4|Zi<|w;#OBjCmo`4V@dlva^ykzArye-od=O@#R4<2q0tSw=I9CV!DOgBDoL=NipqJ8Jlyp8uCk^@)x(Ljz4wVmJV zH~#P`E6fc(qkdm&P?ismh9|J`K8F+%ZYFDokEjEwKR7Jq^NK+tB_Cn{kb3W7%X~2E z7h}n~^JvP(?;n;!VANObb@U^id(UAxFN`8wpXwzJ>4l(;cORDXz^FId?RTBW*f&N; zsIHF~3gkRy!Ym%{2Wan|huec;6vp{n;YdI3KjamEz(d7b?ZaAuSAOqMYksiNg7rd0 zDdz0O?;e&zVLz(XQkwIW$i_Pk%lToyKN@TuIqJDFI=FScw|*e!i-eV0%c=Kmhuiak zBsjVh)0`&>Hr{$z4uSpNC?3f=54&vq&S5zZ?1xpVlX9LZ+IY(mIVjc9j3e{Zn-9yu zurJW(E&7Q6?mHrf)vH$INZejIEC+!ZziWm|N7(n=(&sce69`_$%~&hV>W zUH$%*zXLh^9`Aay>nY1`UrsIk6=-JBpTa%gwcy^ zF67fF8_1Ys!_8OxDq8TA2tIoD*h@19)iB3t<+OXA3v}EGba-2_497NEJ;--xkbAVz zj>J%SSYkR+is&TU8A_&t!5TP9);xRcJ(>d@N52)z_k1tVcLC@l1@OR$BTX)WcA!+K zlOLj`o{)}e)SzSdy|GR&TTap5kW!K6)_48WShsr5bATQ%&=cW9rGaU*dx4>vQ``Br zqDq}^&5%+r9;O$3*SS&#wlKwwVG%_N`+cdn zg5@QaRr%zQs}<|D;jlr6hopt#%G|2PTa8op`vN`(w`~U$?g`lTfUfh2TG7bYJbt6x zP14Dlkf})ESXR0Tjz$T+NVKh1F?ZNkhuibJ_!@06pu?sO(V{q+=%%!RPYV})ZHyVH z-Cn$eHj6lo35e=%X8oaXP*m`<$KIBjK^qk4vuWcuMJUz=iTcBlY9HQy=cq* zCrfcW-HZ8j5m!6y5@E)DiHKE4-Ee_V^i0DP>Ga%s9JGP#VFI)gfN zHtHEbDmW6LOL?uEqew#}A%7uSoxAJTX!C5K!=?@93o0~@gX=^BIB!5p4*;lw0Q>5 zVbdnjs%uoXR!9$l?`GOO4d@xoHV6(D2pPW4 z#?f{f;o1eO93ty}mBxDUc)i|7akUy2i@Z;nTaSY_Pql{$(59w&J9;coq_tXbP(>nS zGR8G4fuYCU?3k1Us<)I2ni;E?<@viblQu{03y)`;r`YxY7sHsWV$#s()j5O5hG-Rp zCiKGKU-gjTTRJc0RtDevJHD{u=CML99MvP)BH!so z+r>7U^;0Q2jpp6C3vNym+yG<@+-9kpqG!0xqkz1D+bnhO=LLC}y3O@K z&cJP!y4UT3oH4g~-?;VCt;cM>b2Gg0?;HI3m)4)XzO(kKwTB*m?Q!bZ$BxO)?>aTd4;(LX zT)+COt2Y5A%lzH3eg08v4uF)l^OuJw2AQM(2eX8nmKY-&sM3fh=x-W&v`jTVhxwn^Vbiw+0}MFIJY*VL35gG@no^lPxw4&Ny@4oJyJ|it#H_H zAzaRr_q(!EuBMiuh>!-Ie&mHt|L>EBCNCN5^eE&1fqeC2??IRB zRnR)6TBMz*tFpUZ!1;8g9}r_|l7u_f`9}b4o&q}kfw{Fg_&wybmMul?cF1jI;anTV zlkTF*Lv}hJ=*2rOn`pP93S>9)k#?a|JpXVY@hp(|7jsLzIF3(at!|kk)D)+NQYj8a zc`Dv%`%0O-x6!JxKn!DIP0}mGz*&y-4+D}=AnE;cOS*Id`G*d)0bPIJ+}d35Wm$_tD#RwGwkK)@8VVPnvUnsL zF_Kj|+i7WrkMYJywuh? zADtvhMk%V3iXN|JjBt|ci9(=XA-lFwWQ*so0TKru`|i0VUgEJX1g7D9&vaL_8KX;T z@f1@=)1kZ;$;*CTLtN=7f)~9mJCP`zzZysycJnW3K|j2D9%w=N2~GW5?gDPgfu=C!t=^3}%v?9nlL^ z52;gR$a2Swc!ie)JYOOLByN{F>iH`N+JLUVV{UCOc&slZ*9vmoZucM~<>}kGlpDhg zD;zQu4QqB2u1+=>@%3`0Ow&4l1&}gup105aiFtza$ly&5Mg8$sC8I*_a?nGiXeDD2 zo;>hjlxH&)$#(O)*sC^+=T88M1Lt|$+!8Nwo^T^l5pqP7aYZX24L`$$zzcf-%GSN< zSehfl9HSKT1eb^wsnYp9AZg${e>S(I2RKhEYb$g;t}?|EZpVshCR&okrb_iKqrrEX z1bCILGZ+~nB`W3*K-Eyx z)AOU1c8#DTIONHz>iM04HlXWonOmC+&a*&J!aBbVqzs(r&2xYFp5QzSgeZ#Vw}8Zf z^So(piI+If0>O#W`Ar~c;5=`fTharZXMq3(aGs5Uxq$Qh>D##PJPQOX?%2NT z*#mO{=Xv$q!e%?qfi=$p0S)!8+Xvc!uD@z-Z7w*^0%3~5`~TJov9f3Gd~(~r`G$>0 zuD$Tscb(5%{Rp@?CpkU{NWko7!#4Lw{c{5j{$gn>U0X+Ls z1+@2hVHFX2nE~Ao6%#?)&LLQfHK=Zk4}g1_0?QY>T%YPhc@>TEwPp_C^juJHt7yU< z^A|fTHI#u2WrUKr8cY}CDX%UeoT;MhKvs_xhq3@~+>ld1hmerqL#98@TXngw7nn-2 zkuDR%wsIA;MQ}YI5_)pV$1(w{!%r->ZmjrXhFSLf!K9a4qESLhhpO;pGZt zU99vCBc3x5_z#Y24fDpEZ{)PrEw@n3URAy2mJrX!&q8M~3{-;z<-mbWVu#se{L|r4 zvI*B;l1=EETlMD?vcK364O2-WHB1wSzS4W3O{g{nwJ;A;>$+kvtfwI<5QEeCHk%A} zaf2*ZaoQ#DdD9gS7p;rc)91FyyWat9!X0Ll@%71(vdOZ>35~lGftsG61yoB5LK3a{ z)C@LZoOr{AD=$Qe0L+(Zqu(#6K9dq;78k=omp`WassN#l{;M4P0AXf7v%P+Wn7gDUMoEfg<6 zxNPS`Ql}H{@UdJG@t|@rLs4#Y!W$+|DaqbF>utBQcYEz2Fs;(GI32Dh=l@cXM%0h9lF_rMAn`YB2B9s;3 z67hII2&Lq?ZStl!0-Hn*v&ndc=ObkkV@cyAgzKq(UQUWKpGr%;L=eV#s5xQo@}nRv zN+?3~3UtAPlATJoSoK5+uHENgwozeZzr-{HtyIMdQWnLA6G18-2xR8A$?Ja~*n~OE zCgZElBW06iJx>C7Moou}XdvAYjaC(;8?xl;XpQ#3XS|oAwOEU1HLD2HY0@~MOs}_U`fyKyCCrQvs3ntaoNi}ajhZeNyA|9;wA~_3r0EpM zLL2diV_uNwh@IOeulWODlh7eHag#^)8ILUO{e;#7Q5vfx%{(TVUWF4aQWGb>pL7FO zmnZ6`?4d;4YuHdKnCPk|6jQQts#6REJVerCTH(4cVM#2PVN;QS5S`m5uigYU2_9yX z@oXeVXq+IiWsQ?yt;hLgREB6)GLr37RgNb5)`aJYk4}SFW}gwF5Dv8r5ev4wQm9O3 z?W6~63z`b|WBmzhf4}yuwZ|O) z=<&|+8;^bHSmW3uoF8yjoYy+u>(CunuD)YcUEKk!m;dAM-aY0>XeS z=U}@)Oq9QSeaQA9bFf_?{K?%#E&02T4cT6GczEmd z0>MfCZs(Bgm22e9Bdbe7V`Vg z8M56yJgBtDsU?2@*+aHFbFf_?{>Sg%K4iN+2ipZgef<8jhHSUyV7tJ%BYyw3A=}M4 z*e(#G9-|r6DuFb)AfdCu7-x;z!J_p+c&iwHE z?IGJ^bFf_?GRE)QL$=Pt11O7}*5UVCL$;1N*e($3;`f_FwySfnT_CK*?>B~QSLR^5 zKzNGZuMgPn{r4Pf7l=pk`?Vq4pB#P=Vv&4Z%1;BBwJ3 z`TuU(fLBg@^u)_g7$E!K+TK6xy#nO^d-C4S?x#TJzxJ+w_bQP0@AW&++F>@}o$I#0 zxc%ns=WWNgAGP(ht#@p_a7)-az4@P;@7w&<&8KhPyzzsLzub7)hPwQh{>vOMBXAjk z%LrUX;4%W25%`560E07si{xe4%@6PB1Y4?4EVSE3#wB;;ZZMPXK%}qXPEep^YBpXd z_OMt1q;lYjTk+8y0XZ&5K|wc{E+cv_=gqrRT@STY9qm|TudNr7SOuh_N+as=m)>^= zEzq@Q*i5Syh6e*ZtxDBBZUj!5Sqh2ty}F#Sy9Sb$)?R$!jz}EkTfvUf!E0(j3gZ&a z#caj^F<`|f2ywNnVC_JrYFVp3$f>QK?)Y4=8ftqplpJ6iYTE0Uf|3UY*@S42C|YFm zG*7#vXp=5e8zT{vo6WeBus6>z*-lY`qm0JUu}D}%%{I@r>t&BpH$_a371z#;?jSXg zRHGL(N@3dvby$G{rEIez@9SnN^?*{wJw}{zw<}F=XMJ^ahbLn;)1h>?oGn__NZbt6 zQCnqd7T#<(s7AF!DkZ|#jKx~z%|9C5;g0ylV62B*8ceY%FA4_&E|GLAE`+Xt%qR6~ z-NFbjf_0_!FOTlPn`uz|NKWxd4=CX1YIlve<%U_ypsMwZo9nq4k4u5@Y8+et&gc%b z%VbN8-i~IIaxNX~g(EB|85(BlTvKUPx*$_vu4uc{G!#DGAKd{<#bz?y%mK|nzC7Mw zT;WtlLZf|u!ekmuAq1&t+i3QyO~=zlcR+=lfQPKQ(Mg$otx;;34HjxsChjS=$wZvs zbEqKt*m$|Td*kR1SFBX?=PNE-FxfCdlsYC|Y-MWhm@EZAnayTak>g0DCa?kL_b=Q* zX2K1!QqKwbV889E^C__>r*k<^SW79nWG9@CV`3^1&8Al$HoT+VZ&hispKR1~fi{DB z8hNH*7wU%JgS%Nq(^*DtTG6Cp1Ov{WjPB^Ry;Zo1yTl&rv2_oW4_b6T+bXr~W`pS^ zausmYu#ecRme_p3=#Gxam6K?*Nyf^Vus6_Xlxl$-gXhgk*o=0=Az$7Pw|%hECyu{j zbVtMFjZmN}%K2cXq_--59gZ1fpK4e_K8-akCEn32x1plNoiC2=s0&S`K?+q?BLGOz zV6aGPK0g-{sBDwTXVYYhkpo$u$w4dkj_#<4m{BjK1Rm_V6fvoiY|?R|7G`WRrlym4 zw&wL@M=B930b)y4N=9jEzIY%G*aW zN^zIf)Og-hU|VmNY{6ZNQJmPSlm2MYUDiWxHj;0Lq%gH%U$`R_jkly;v29ubv7G30 z^}eT`pxjuHh%}1Pa;;J0F{YnyN}Iva9lEO!(tB9nD@IhJtf+!q(~D)k4aJi2dO?a> zVG85ig(z9x;74~9{W_JT;z2)VmqUn)4CiaIyWbN$Q9;i3dwhqa;*zfjmD0z4Z*)hY zT<&IybX95Ad`8>VO`woFgNbxL%KBP4ldhNCU5#N?&RT!e=nk!0^@vs_-iAAz($2?9 zw7bssIX9&S{ACTPaYiJr;Sn}Vth{@4M;`5`T)|2w>uv?8e4Z=k3z1aJW2Q7TA>-|I zv74&M3Ady`&i9Y*P|~VUgM5sj<+@5tVs%Z$npDQq2AMNEZCW9NRH;&_n?7mlgQGiS z1EKs<8BT>tmaEkDcfw>zD;ZtcM$Kq7-L_a=%ww3hsBYalx+BBJt7$ap>oH=mVXP=t22>yMy3vGTpq9b$nAN5PQoM4=EkbjivHEkqd3ovk+MRM+T4 z^4&_9uO{u{M&rUA`4%Jg?+AB1xOc0H-v~H&~s@{rw3spW6D)`x0y4$ceFZ4r9)$~5^Hz~=2pa^2*izuHg z$&o@k9u_>ZKj^c0g|N!Dy7Tan41tpnAzljwq*$2I$znX4E_i!nT=nZphN{I4Dn-P6 z8OtoMzJ7E^QmJA-m}$XD$=%{)78C(afgCzySuVqtz{ZtkwA9N4TCtAvo(p#n9k3-n z*hsrdlt0=Rit&V3O~`0Gmn}e4OX;>LkJQqNnVVnQtZm%8@!XA#^YpQjCu2 zc5Pybv7;&Wz+< zJy(Cn_Yhu?>%-tAoa=Zz3Rj>(o@7uNm=I%_2Q$M=DAQ=2v1-PdBta2<*haJ@mJ%dJ z19IPCpSx!qk5Mp58x7p`T%+=rVb08PwhJ&c>htp=b!WbQ_5irxR!P=nK za3m208^Ssq4dgl3938?2H3 zblBrj$sm|JW2s`fQ}wh8=DLs;2MkK>WZfmao zPMNRHD00IHTew$+ajb2bOo{8rXB4a>>Q+OA<#s%!^zD2hAV410(?829xyl8(n{)Md z%6uh3y+mJtqYIgU+Hc?$UQ}tlxiiMXFR3a*+9M)i%~UT^MKSXw~XLfsaq>~+RaqIRxuSTdNwwa zyFO2U4`dFz;Lov=7-~`sZ17el+C5XSQ*h8a6G8iSqF>}0uLMOvMR2|E>E{{ns^}&$ zR2d`#Xjp)t3?m_I1X?6%wP5Hfiwddg? z7F0-w&kD7MY1kRr9?5mi)!!-e2FQ~Np0a^B&or3=o$1$#@f@Z_JGfVrC17N z=&4A_ZFt=gMa@(7xL>Fz&z_+N{RMJY=8`*Qo~){DBA;sLRWiwC>Q>Ivsk7BHq?Lk+ zUb7nWl9@mxQ9@vdCF>yVa80PxgM;yV&c5?GbIF}DZ%|?)AreFfr8FuOfZ|Dr6oY5L zv7TmE6(!Y|%e3%%jwP^A+N!c$GNgcy5kT&p_MOk3OYW3;(gGP$dS<>;RDDb^9H=0* za2bomD;e8M$tajGm3A$n$5Q2XAPvVQZ?_Z<4AxiAx9_}t9=Qk30^c`ZDXCnwDUw*6 zF~zj6OZwsv7vt#|M3_)bA{loFyqZgaVBw5m$n|hluSExPf62b{S#!yqGH;wsoUv?^ z3kyxfS3KK?_&7+_r=6`}!dZ~+7#eV7zG8!dQoCDu7(*Vu9<@AFum4pyuNIL)ntEQ!%V!vHCuf!ycV zclL&IXTO(uJk!RnuK|maluwYhfJ-@#QmILXjZ&PJAgq#D41*$eWh-5e;9}PU7J=?w zh#1IywtZ)JuKrH(XD;5)1c`3VE>^*3A8)0l3MuqVxLau%)h2f~U+g7uP}dxW%ov(6 zYG<3#N^2nZcKgoGT>YIge&e2m$`|8$In&G4BgFMC)F;HyFP_ZhM~o9yqgdVI1jaqLP=EJ!(jkcvwYS0hMT-iQ;XuU5L>n;iXDc zgzKfsy_A7;v4|;zhW5Q}5dYs*5#IU;E|TPF&`e3A)%Ku z)NUzmLOKL>vr7|Q_39%KZ#uUJpRt0l65jzF|1dzS<$U)Xy z8f1RURv=moL(M*@qMlSrT%QxD36O#Bc4;V<)@s$N&?b#~&Mvx|IoZvNUMLr4;93ae z9)RV7l9S8D>vPeKy-3`ImM+ZcN$7en9F=ZNC+FnjETy; zN0ya*x>*+TR4t2@%LOunHoNgG&Q|Los1G3f(<2DkvZjBU?Jx%COI3AN;~6W7CiO5< zoB)~MrrJ5Z1}8u;u~+V~DNxKuuclBYT*-9Z77|lJq#}gnSV{(vz2V3~29km;DLF_m zPYFFd5)+%vysryhH1oYYI%z3O6&p&otGerWI_nYGoQ?Mi-C!?R2RmTg)riZ8)Ok^02tu|jFZ0uWh4#X^6y8?TEti_1c@>`Y#Pr(fjzQ&>W~-f$gDEN0EA>l_ zSTAaWNN>bjXDENu)4&ZlPz2NepBz2NmX^}YFDp_-@(6tk)Rn+>9!(E6N=+D;K02X? zz-F2#-zk+!{-TBleL}v`D>pKULRqhfg{(JM1lw3(3V7KcJHYGo=J6}R|F8Vu-iLO7 zedlT0r`Mme`Sp#ru7!q||KEAtzTDHx2wXtctXO^sV0+q8gG~hqc$c&`rrBOnVB9KA_0veet!9!PF*I?Zmxe{M zn+3_&Ivu{I`m^B-j2HL>DnTi~Rg1cHHJA1KM7J>b0JjfdJ9p$@>n&}u7vQ=w0`+sD zypgDS3+Z&;AFWP$arI`1IP0!+Rx1e+Jy;7_6e1b<8p4*tM#2VPf&CG?C<$`h3%u>E z0JbL|IoP^O8>D^~waj&3-*(!9-QAwnowONn|{t1?KmLMw7QJ!x(W5=7g} zvp!V!urfne{DEi~5+X71!Cn+qDA41(p&F$${c+%JUj?vz;K;#-vdelTJUGb?9sdkq zd%=-|4PM@-UoOO#IXzP5AkoJqnR2(8nVkEsMABq}U@N{VQ_mq_Q(_~Y0?BR5aTZVH zb7V7&CRktC@(U91wvVl>oQNDb*r25a8GIc)iOB84VU3=&0Pz_ymI$Z3 zQOb^_d?6Gh#joV^l?E&XKf^&zvVc#vUk@mw*Ap9ebcf2 z@-?^P*s;HL8Y_4I;$H?<3%__B{{ODQ^YUw;lEV#-$!9J=x#F=i7e{DE0=~RhvF=ZW zgnFV}gJe)yP7=dLJ~b)4?rq~3RL|tNT34*q$(F5jL1uau?87H)m#D>pdL`uVDsjFW zPgwV7;&lA$VJMI9R69~AFDtuwNKQvg3sHh}SgHzGqDtFZdJ@Wh5gbIYDUvpzY`0tL zR}=0GIF-e6Js;oA@OrnEjlhX4(aa}{m!SM_hoL;aul7ixysTKEUl27OwpdUpnnh%Z zshUVW4NX|lc}sOJp2~r}qzO&yXgVFrhoMlv66E84IMeL1TDcrR{1GoWn|2AxpFIra z@!fz&3gu-j0O=%>$mH9pQl}}vc_U)7SUNTFlf0VkCBu-HM=MnbH$YuL7|rtFjaK6` zQE#q<7%9syv#E*z0zVgLjDI-{Gl!u(zSHwap}edaBcN7N zX|pa@Oo}ljBqfk+Oq{g5^h-jrSSl94VLT|O)g^bjsxx-W6-!#4bP}R`kSv4b3w$$- zUHbI<^kFEE@8dmEC@<@Cm`}!IX{-)n;t|1sDJ9_brOK1uw0$Jz3MSIkcwNlrOoiog zM3Qdtl?GvYz(KsQ9S;yGGTkYJU6-Q#&xfHretPFfp}eel*@xMQD%;VjVUQCeTvyB8 zh+3KWiJKu9)C^=}{wP_~+gZF2aG9Yz6Y+-9MOiS)y=GbQ;vyycJD1GMpE?BP181|2 z5X#unwmk%zo?IZ&*XVpEgOF(64?@fIL|--wUYkl3l3JOuhz2c#OPd3FiP|8yA2<0rF@6w1r;eaYdOQ|K!n=VbN(B#WBGudpf z8z}L?NFA;gvM8t+0BcpR*~q1ZP(DZFP$x_YX6e#3#wQ2w|F=8*De@X zFi&tNpc7Z^eRuEw?7eO8uDxgNJ!9{wd#Cn}?S66huXg`v_m_98yYXFY_u8F%cRscA zuARGgp1YIV@o)U{Ms*{;foxo}e$Vv*o?e>?7W__x+TUc%38{?+E2Ht*hi(WbqrY_gl==3_Rm-1x7JFK>Ke{J$pN5%8pD2f3Y{p$Y6fGlL2_UDK%u>3rf0D&f3&2G!$y!VIe0`S=-B z+q)2R^Ye8hAr3^^Y@oeFuJ51Ta2 z&TD2+VdvE|sEG5bsZ@|;!+GUYDgow-E2dIG1$gI)8C1f#H-m~hcV|%D&Yc-l)VV!_ ziaEDtP+{lh3@YN>m`)`~=lXOi2|3rMQwfjr_zWuHJT`;sb~_+ zcp?q&AW_%$foq6s%@VJ2=3ZDcoB2$9=wRUcE*Lnkn745&PT7RN;5k^>q!Xge}wb@ zTU91Kvq`cxo#v@dr@5`^G`upMhMLo9urZwml^^aTr}W|askafNKAlFCrqeuy=`?q7 zIt|ZHr(xQ38mdmGA-U-^SeZ_Po;ejV;*nEt^N`u;T!H!2G#BO@(e2O_>UP>%<-ie zRMhds8C1mag&9=X@%b54$no#fy$Zi|`kff*_}mOC;rO>1RFC7cgZzKnE5(()m+d}b z`@LKI#+TQhy>|7n*EpZF`f+f1S--!zX>yPI+XM$5`pMZf_iy%WFM5T!vN~i z{ZM~r8r0((DMUu}rrBha3Lhn?m$q#&V6`#^=Td^Y$Rmkz7Jyp6AL{>{2KD&9)Gj~3L+ zdMx_GP`;86Wgs~oLu*LXfXjSv;vS)v#wW=#BHCp-K-DD|Ct#>6&f$C$4Y`_)iqScwOd@X~k78`t@LJH-)D;7*vgfu&0 zqY&kzqFS(M@ji>^J#NZHWzcA~2sO(MCE2g>z7*^)lVX;l!A2qA)rI?^{?;_8$9J#z zyz!LJpJv12QG$AD`&fc3loy0(sw}r;Bis=(wKnUU6v?BShF&gYvtm3b2|Y<`F;&7v z(VR$@6MPA-CX}|SWTP3sST7Es*6xS;o712k-@@W!6I3S3#}d9H1@+Q4vv7)>VM!hl z&_i)ON(Hz#fYa8+7TS*}GoWYI;G!+Jo6{H?0!0mqN}RSj|5scgd?AyO512zCB` zsJ}4{>hXOnw2)@F3_}aSBL(%c=ITHZq{j&0l91@Bl_Z@s@@+rD6vP zmZN*bZnz(gcDo zZNo)KNbn)kALp&Q+}8_CCD}-qi3xMHk7WW@hchi##5UVKzgH36OhfLtg}UYI;abSu zh1^Bm!^;)Q8od8MeC7Pg348A=d)eK8*bVQzX9wMW&Gz+MzYgN({f!r_|7hJ>d)D!9 z9aoS2qx171=l@?jp0fI;)yILG9^Bs@n-5=GRc}2hKH}zVcJMVin;nl3yWsJct0~Sx zjE~ETR>^6Nre3PucIx8ar*66Nlwp;sr=ED@Db=dBv}y}ny6yJc@7R3UNSBAt&}Do{ zJVloSj(bu5oVr{$(&b?@bQxbWPSa%^zlC*q=t!6AX6Q1$sG6qBxXz2}vUTlH7wMrh zbQxcDOw(mt=jpnbn$p%LILt#vz+C%tRc5mD)~;C}45a5h`XN78WlGg{sWlGGsanG- zluT_fu1-~z%HWTxZJJ=jDQ%^tG_rQXym0MyaB;+>0j`{yW~*_Vo*InTt4Hu(vxFXx z^Plbuli}aGYUGz!&(LLji8igT2hL*F5@z$`Q`ZckhXc~ z3OghCcYpRCAKIq@!n2TW&X9z$V%4w?1|74wl%JGpZYtvO32Y#9=y{GT~?XDkG|9j&0?`~hS^}@{`gW7+eU;p;Hw)RhJ?D0Pb6#;H>zTSDt@tdo! zTz%-ui^dI;guQ9;L@V6ue>czo0^2+JLt=tviAhA!h?Fin^7VZW#@HRQtD z2>rP#^Yd975%Bm_G+Z0OBlwXe^mv^AqVR7*BVFJbx{Qy)X|6Cn*Db8e*^w^L3|+=u zf0{1ilii}abkB_35y6`l4{LfXHxrgn~^f8ct z#!WgLu8$kRf723rJkEbn_%|Or(&cf}bwS3boXM_muxRbVx;$p2%VTHgGT(v=oX;6c z611=`r$@RxW`-`~GN$43Gfy3l9_ey=hA#6x@jc|vJawEJ>GJ3qx{UXC8ZJMx@17j# za%zSy^F8oA2lo0d>@PQtbU8UgmvQW;;BsJx=EAz%Fw*758M=&5+tYLz_p600uD$Tscb(5%{Rp@?tZ^rA z2ppf{J_q)gvwo^8A>Fow(2ydMb~rfvyQu`pe%^0kF+bJu85BrA%+=(eVCREoMG=Kw zIM_0^gw=1LS9_7Cgd^rZ@D@AJYS)nP9)p!=a>B1qdg1Z#oH=^%M_ z0Nl$ISiach`k=TKuO9WCKUATDO zV{V)Lods-?I?N{HuM9`YCP(~?!^MLkbKB%^UJY!LJj^EJuTDqGCQEysJSc}DE}kHn z+a@2r0oWvQm`%n%O&lqkEbDpl;GCMcc+_NWn|#nS$p5#!^6C}G?%w|HleYhHtG>Ct z{s(JMIrdM^Cb)c5ezTK75(ffTN5^f7ouw_;0}Lcq7$0wP5XZ|2Ibfn`b7IwGmTvSb zr0SAN?s_PI8CWZe%YC81wK*>5SD?9Q&A4;2Bc6PTYw6wSX6fr9u$dZ1;gm&nOL3)m!LFjvO=U0kfXM&=5q;9HRdA9Up@@wqx&iX zP+r!n$b(ZH@FL1zIt=Bb`*Kn2EbZkYM4?JmW-)6 zfIyw8Qj1gQ@Pt#QUaXf+6{Uh%DO4(Cr&g~BRNM%o?F^BOc$kXhr=z4h)yNCdr6_;# zFqDt(3jjcQS@Uurj4QnCN+5y0kl{=@fP*5B6O*TlxL?S^)m(@zgD_T>i8oX=j7O@u zM%)W!69nfos>!myRRU*uFU}ZWI1J_SsO!;L$pMs?H!nA3e=QxOy{ID4NZypg^29;_ zR43G_`}#RejFK`!wCF^uibUWlE^38v(XgpP(vs~4tR@CVER18~!r@PME=U@Z*tK#f2gQsc26P*Y@h&$Cr9xW9YYxJwlE@iyhU*W*vIcD^ zVxpL~aDEZ(f(@wy#LR_o(GR4UKZhZWC%G%SOuS~^vJY4@^0SrJ`IKCXK zOp9&HLlE;Jvm>WK3_D005+d}~&LF-W2J!Jhd>e1)D%{WjvQX=*ytfn$>F%7tLQJ{U zC=>^6nTm>-)m#M>y2&tRm=-iOVPGj!k934a5Dqpv{$$8U`?XF{hx(T8qEK(hRx(V| zSG*r$YViKQyHZ-&_~=e?{rs8^_V_y-A6tF-N@*M0THX7b-Mg0b3E;@pX6!HRT_#wx z1u^fE$tV|ZTa*ZlXPNzi&Gd)<1HKU48@0nykXIWcBI`Nch zL&-wUMTf1hkn&U--~s4(?-7J-Sx+;XKqF9#VN)$32WDDTwCWjHovL$gY5tD4 z)-gERITiz=J9ZUpClsB=KM$>Z6l*hG99v^L^@QDq}*ya5OgJxG};bQY@!WUA{7zKdlJ=XuG;K?qN(pbf{-n1{U5B+ zN!XiU%ITh9wBUp(O9f+64Q(nDOm^fBZbn8L3>U8I3o*g?uTNO;o+9E*fFlbU3HuhQCozir&5wDq9Tz+1rj9WJmrj z+wp@V2-&g*X0Y~PyfQoPIckupy`{yA=rAVm$$A=*3yP@+xD@V9O`OXp758|0hAs!t zXd~xELj6`kspi|>L@CX+t3jpCFcAa}bSV`~|8GBPkb!}@v?|aK%BNh%kB%T@%UYK` zINxO*_YU^|@2tIU+IfikLN!sl%_XE!F*fdfK8H(D3F!J?2i*a=lx zIqB_Idai=jDb*tFL|v8L^#abPEB$~NQYA0v()cJ z8|jkJrYd8cF0sY|W}q9*i`~dqnP^X8;sMAbhc#EjQz$k?#*n0Pj-ZtUqL=EVyBu}P zSz=z4=WRDO*oaj_c}h|5W+S2Tl}BxgtT0@Xw66dAJIF(Y2#B>~TuhyaP(C18!o z9LMAE^R_$5mrYyfcQwtIV_P{|6jPI|F-0h_M*U(p3MlUH<(gfjnW)u+>1=`%OH|As zfU2RWr{_m2?HWNxaLAKaC#!cbEy29)P6ndhqE}&4VU{Z!bjq%T=IsU$3c69d*bP;H zJ4D$<5TUZ^ z?hnRm0h6wodZFK=Y4F^bu7yQYELZ3520;RAlrMGzaa(D^2L#HnBamLO}C3JzHOv~k?{Q8@C@GnkFCCOW%ugM-#-2X z@W9y` zqTm&JO-e8XpH_7j+q7Qu)B|SJZg2@bDF<@uu>Ar9?9{D5K&_#*e`*B>3|hU~AT|A| z%9(~`Rn82SkN{OvYYx;ITpHnY>Tp~KH#D^>fN;QX-eEns>yi=_!?F}Z1qMuNcFruR zrv~})^IEN?gKHT0d)Vn_O94sufl&s%82%$$)_Q}x1}v>csR-&@fPa~m3cgoc&DJdh zsJ(uXmIXDS+gc+#v@DE!APB=C%ncC(htQR#4j>rh?r-V}g1ZNt#7>jR`@>dKy%`eC z92!dcsvUQ!3s-}jT9)13n++KE-E7BHX{d6u1ophN?8XH~V~PxTYk366K{^&C8_QGH zKt{-;M9)h0&0!xCuC|CcX|;H7T9vy25>d=vvQU=Oj%6pXAa(Ho|<4B(|B}Ws%Ma75r=aj)rF#NMzQp@-K-O%!i zI_TYyav^aa=<9&_ebWsJ{(car8yrDkAYtNkqc@YDN}=TU)tg!?o(rW)a=S~oy@eWr z$zG(E%T;NDDCec{Pe0v|Ju9e60!%Rp*5<+M|5;BDOGLPmbaN?QZwES_uCHbbTt+5i zwV-8WLglkPp~yt>ilmB}P8HV4nCy%3xx~$;4Dou7^HasSrkg?6!E|%3y&oPv=6kEB z7Z`KPn{M!*b-KCrJ{P%vfPi(vBJU1 zg)x6>@+%|Z8R$GQ1?_z!3b_xi=@0O^=PSVKr+v_4I5L=cCXT#zI2iMsEtDHkaDV}_ z0~TVP#k^_>3d139&Z6UDu157Vxm9O>>Ic0M4%RW+pom~0<1c&7X5VhY6+adeI#C^p zwCHpfR+N|)BP$WBsE6CNLJ{r+{6U5VSGAZLHKBU9OsA?nYt9G#pzC1d z-C5cX4MyIJva5@@>+(h(c5viP7)ujIhYD8fqk>DnfYC7&2^Nmck~LgkVgrvKYGf_8 z@Vy<}ci*5GJOm!Z#|+{@r#icvZ6`89rb8IM9vz5-8i%Y7_axQK^!j}%Rjx9+zfe(W zC0PEcoxM~)TdKp7WR$&FTkC{zxt2(stzjnFj#Pqv0jFrFN>&O8V~A)<#bhh&)!csI z-dqI}8@VJe=V+4-xTyLQZ-C+}Rb{rT;;ZohC_-hSft%GSp> z{>R4N`e)bQxc>aDS8esTxOHLurma((_iTQ6^X|>YCcXLajc;tcf8#edjI|#EL6?7* z5x9)NWdtrGa2bImBj9r0>bNhs43C$J9s4Hk=v>f{M}*CYCRr2 zamE!)J1J|)iZsRt^8kdK_f`9NVAhcJl$ zKXK*FD<@ukg4_Gv-pluVyIxbVw`{uX4`5k}By!*$(es#zgYjw*Up{6)BluB_Z z%2V-9+gHluy^U6l1zZ^uYm#0e77dRWf9}(H&tG1J|M|y5pZR{&`K@=|^uJmU|N8I0 z|5Jba+ub+)ZJKzauwNN6u98?mPsSr6nJ*U;^@=RH&}LFd(M&4nhfT$mvx0)3?=VGd z#Q1mq<%T=2{_xZOF@ML_tF)K=*`GcndChN}J0ZzGc*h!#ee1D%!oE3VyuiN25#x3v z@}XD6uIB#fcbmQ|b3gg(-(^09J@qM{|C0~@%ESKY zzqg<9%O`KU<3q17z98%yL&jtes`RF83GD=aMRP=Z)V}z4jPZRH_}nhxc)NM82y8sbVv z5xnSi*@?u6@sBEjPkj0hzVe)Ze8~;Zc+#o-XQD4ozMV_CG0d>SAw$uyW;fyL zWP=f3FIUPmM~pYxrQ7d%{d--Hj=bv|l{0Vq+$|fAa^L;AkHmj;;)6Fm@%)Q!6ZZ2% z#&WxkqNqRKs$^8iT@HGv6s=?|!jlKCg7R#pBH3fvwy99?5*0T|Nalx zzIN=+QvZK^{nPJuwm$NV!kI4ad_aEKFI-mI(`;^x_WH0`&A?!DQxAPbAGb_S= zZpb*5wH3M^SD9i7w_`;$6D`SNQ>FTr(crsG0?j1r3`T}Xxj14R`O#h1zVg$L-FW-k zzVyhazwNi>zg+p~3%?q>D*l<4@wk@+zxP|hzA|LIz~FlYx-T8pHM&fgRlSb z%dd0@``IC5v=$L!oW)hWi3XFFLqClv#F*v()eF~{bj_b zc3*zY&5xmjzxsP0{^rv^jl6N~sy7>a@!YZNqRfYd{bvjr(>w$dQOt_PJq;-3Yh?Q# z)#lUTO0d^xxzR|&&g4@J>MIwd5#xtE3IEo6ZX$m2)xW#(wg3I(Prn`C_$~9bfA+^8 ze*G6d6WV|OOYaf(ZyhoYC46eH-KyJU-;~XIwq7=+MpJJ|slHZ>_23SLb&E))-C!@w z$JejC{KG#o-udY-6y*2*Q8@GZ|I6N+z`1T!_v173=DpcGGZbinUrNXeL$--Cl5Kej zWU(#pmTcLwY>5OTTe22QmN!|pAuptm5Rw3)WqB|BCzQ27Qwa0{DNxoxLP;rprMD29)If2l9!asGR#xsoGF*ZhUPQm>t0wgz z`;tHI#a)tHDY%1hAYUCi$m3t}vTow>uf3$Q{2}qj_r3Yu-~5*!-SCm~?|$GlZ-3JF z&iN>HC9z2k6uUclAkhf>lW>&ldRkqw<;$iEO?$LiPrDp;3`(_Cty*VrZK(JM|8?@B z*I#)t_^EdWu4hE+I86rP@)d zYzHzFiiJvg#Cs;=^!6pLU#g?PRv*9V<_i)Z{wDU8`!)}~dMH zov*h2^P9d(Y~}}wEnIZoQhdt?Kl<(ZyI=k3A3y0O-vOWa@n`-0x8L)XTIBLuZl8_O zufO)iPb4;pf#R&lVXa2h9Zq_SUKa%^jHqAwV6~*Xash& zaMt(icTwN{@E8A$x{cVx2a4044#g0lN@eVoir5C)eoe^MJYoG*EX;W8eh=yrWCsOP zouPwV@qvF*udcCAd)={Twols5jUW5O$M5}w_IFp_|Fv^pTM1SkBsQ}H#m;<_&ZL@! zND7Oq38zj4`2UZ75^{%gD*e%+Y5KSgDWgl-}cds zjX%5RW0ySm#W$_K|CdVPRm5gypxDCo?ybT5P~-A{eaZ*_{aY_4&icUnJh$2o-QxZD z$DjNQ_>~lU{jYxVM2gtN28vTo(%y*0U2Z+bBomRVe7F#E^Z==&b4`*(xkW|7?R*$; z)3M=1|IE(iZ~c?*58iVwEWGJOxqJWF{)`X4?ELc2@6aza7p}gPzmeEX4-|W$KsE07 z_Zt{qp-M+fJ9S@=*hB}4 zlSn($?0e%~GOg5PGSot1;aI8{j0N>zEYV$6Yds<2?nx=%&?+weH1^Ib7C!O9@7-RR zz4K#-&$<6k&RKi;`!9df`=9@Yv+j8FGrvY`A_K)1F12qV{dJc*&Xedp7+IldJ_pZw?Kv0s1ZzwQQ{KfhM}+`q?u_V(M~c8NPo0lJd%Tu~k_R?p3 z@M+!>2SE(>1 zgd_rhSMdOjyy6mIfF4>!?!|Y%)-G;*#~gVvZ}Sa3^mubNB>Lf7M$I5=OzprKWK{RJD#pGe>M zkH35yy7IO=o;qK6|0`d2$;EfQ;hsC6@dD_z!4t%0bfDP6RsOBPyFvcp|M$*kpU3ntQIYlux2aPC^gW6JCcp(JViT2c!w^1;$xdXXjQ-M zyFB>G6SkM!bM?vfJ8ydJTfg{=r<{xb$8S#L?Vpf<|ED*d_lG{< z#;SvT!>e!o$%7xb;o_g%CQ%XRSx4WcPeSlOv4v~aTZ;eartjSU^Z)qHC*F1O^?$RG ze1N=l{Rh{j)kAlEf6le^VEI#$vHm}M>h7u4V=KQ}=`TO9+_C-4)?E7WQf=}6MRDPK z3zhkA&zJP`{pTF{mm}2SuN)?3zc8CQ^w~q`%zZQQ>3gTcy72G#=i1w}!z_!1+8lq)}&?-C|d|DEJ)WZ ztT87CRbG2rZ3}?~t!ow*qh=7uTL>&@U9+%iHG@FfLSRAbnuX=983ffW1Qyn|YZjKP zlcQ3vy-nOgU_tAeg>|hN1i}^q3tHDKEP%}*;I|N1(7I+}rECTPw}rsMGI`Cy%6D>D z@3ps8wh&m*x@KXCYz6_lg}{Q=H4AHLGYFV11QxWeSy*(NK~Ua8U}43*W?>P1bYfie zwYQbF5LnQD^fd#E=7PbP+Ab9o`0t@>9`x|n7?QPT+ z0t*`k`x|^}-2Z?2yk~0lp4FGF=JebD-&%S5N@K;l{F~)ZF28gcpZ6@EW&4)x?Y4%^ zyY!o-PcFT5312#E@tccp)9(a$79L#q#KJ8LS^bXxH|F0uU;iC%1ROMf&;tiOaL@w> zJ#f$i2R-m;_JCJEjhL~rXSQ`3vGO&KE~xWo^e~Q)Q&1zVHQ_=Dufg3IUjS4xA7fNs zg5woFQMVTy-Q4g_KzrC=x1S3_8ja@Nng}`UazVjb5bqYaY!b)Gq~8HysNF4*^6;G7 zMhyiuDy7n$f~)CC2defyTtrx(yVFIpWQc1+csVS23q+k%?8B3u>9wJt3Z;aWKjTCL zc~ApeoZAoQTBwUe9XcIaQLVDcF2Tn0t;Fyc?r?Z0*p|Ke!G7MI%hV#ynA8N@EQt4g zUVD~g3R$-ZAg&RK$CODI($JW5=hJCDGaZM9NBKM&MR`1 z7DLSnE@6DPp<)|GS5RH6-0nJIgMA z#i8HOaPtK;9~9g)3YD7mJ`oF+gE!x0PB&ZXwZcQt2{-eHD9E|7x`Et+}pn2werEC zpcwXb5l1G@I34;0u~tA1MBTwY*AF)Oc^b4g;tkO6>UWdL;i=^zaVS_3%Z-ee0hll= z+C4H_=5?z02IpuvnFdwBt4`9ZFrb=C4Gm!Kd)tDcN~;7o1x)nS(^;fSC2@dOJyh3` zt;BJljJfUJM4s=Zh8MezJUA3&#dxRNWsC6?s)`Pck?SOfRwN(ii1g)#)^vdalagi8 zGZdVqhk_*>O$kK_ZD4_F!wvTHS+7WuB-k#pZUF3ZP_?gTLFO2Ec=|of3rHMi?<)Zpni3-F(Jz_uTO7r;+sj(fWedx$@{h=Vy5qg@G3QANw-e~4~YSoXX zT)sFTjHY{l8|1)DyXqaEV|2jI_xk|2}e3zS|k{EBy4Yr zxm*Uyc89k&mxm4Fo|H~nijgIO>cdi-a>p5e5#`H0zpK~)Imqe7(UYB8m)34nY6cBq9qys$I3WgLE zQ#icf)3a-06qv4xH8Fxq7_mm@ty z4#k~JwGpTV*zj-|XJSLabi-+feMP4L`s8+8s6{+_e>w#v+yz}Fmp@AC6JUlRbhTUV zxcG`~!BhwCr1-3)RT@gm!3Ps!A1Y-salbe1P=maFp}?IceXuaR&@}V3p&;5;Dn-o~ z)tXE(6iKuK0_Ejg2`-%Xo-(%-fdZ>53!LQ2jNM@VDyO5)!DZlBdlD%lCpw?>oZ95UcLpa$%>J7YC za}aW;##LCX$c=|pIn}h=eBfLQmqu#=L)tOO>4FR%#j)f^v1*8|Z1CX_neMw? zB|`s@bA}oHzTkS_SIAa7bj+XX^c^6i=(K`{#heDqA?Ma_edbF+q3!iMyjr- zX4y`zFY=CPDatsVKBwfW5OP*g0XYXTZYGlLK^yfj=hv?~N1gFF5mvehPmk%+Bm;ZQ z(T)useT#Ms-KHJ0;B)&o-2lJsbTfu3$=ZB1hFEDl?bu^9ar`#4V?JXRdbDWAcFk9M zTc=4orjy<|4NAeWp6BQ=r2ymzmDO6Vfkdc=E6pkWNHgEb#+Yb10aRie9u`X?U7ca? zG(E5502)*z7E=oemC<4WAFdMNz(yoQhw_d{PX+jVK2`wSjbucEN_B|fgOxh!R@yFC zrrwTqV!Xec0s9-#j5ETpqD*5#+#K!L;L*2e$B=E>v3)(}GWnu+_;!2V7+)IW9ph=o z9wYBKUD~l-#%oy4V?#Rz>D-=svRkI|(WHn(!WnX%60^~4txD(n-H24}bxQe~E7nSc zlscZ!G)?FEI!$9Ibe4NK9S%zwFQ196qq%;71t=|+R(l1e)QYv%-6F~Y;h2iz5l9ck zWE^NaLSiK-RSK2;nRSORt7ZZrO7Uj+d;+O^69jCEc5LwIy0O3b@Mh95_FJAby=LL` zb^kc_yt|G4e*uZLg=6AzU%_LDW_1fX^;>u%m%O=e;Zg2_(Qn}~Ky*e;eZj6%&kn45 z+Yuoh-=oO?=l1yAdJALUI=I3+@B)lia#4}cQ$jGVIFJeEsvN+&Nyw4NdzE0uP34<~ z*l9bWs+1~TlBs7pu(Q?;cnN4W^5DgHm>(RD{O*8fkNqNi4`U3qZjn&n?DKhO3H zTW{%sC2jG)7uAKIEwtu;GT)f{@tktxM@Qtt_aCm#{$N%(^u0se%y(wk>2FV$r@pC+ z@BMS^q#ne1c6k-v;zTSH~c2W->u?WDz zH9<2y5F7xoa{F!msJ#XdJD;F+*XgOi~whdqU?$Yy@78c*I`1pl4FI+l*&-`=eK08-F z^1zYnkDPt@WrvT>zIQfw=wA*cXTCnuo|&G$ZF*5xvfn@AaeQtDb1t(}tA98-{CiBM zB`Y!Z*V&F|_14b6T5s)%rd!)OoEe{(#p@j9eY6sB&m7MfaEyn4f@wHgTk%%H>g&fb zy_qYFW*$E|w0hXbyZ7(bEX3UN$J2U~8%C3VV7kej1bkLP_Qm5Vy{Ul(J!^eqJ#RXJ|nd0`lv}lX^1)3tFBWl)bkF zS%}u>k082iLK^7wLbqk6aLuOdeqUqKi*@8wn zbFD=AGsmL_9K(VZOv4#kkcCkG`tgX~%)o-?Cx>(IZ9x{|{Q2Wyy~%+E&6#fU6brHt z&@Ubj=}iqR=!og29%exnV)u0mf{h_FEa$NhSf0}Gll-Q+12WMMO0JbtC#)WCwKO*i#03$n27 zt6R_&#*i5nG-bNAN4B6{-$52OVVUEX8*p?BTKSD>I717vuz_4Zewp5kVL>YoPTok| z+k!0Y?()Zc_o$GXo3y`Q)|l-WFtGTaG{O(VHAt&;zEM zJjH@6Y(I&|-Fj043;G|^O+CzlENrys76chXW?0aFn{Mq9EofIF7Au=M%yE|i$FQLP zG7V>FK^C@b>c^dWGXo3y+2p1B-WFtGKgC%8duMK+UcGzug{zU(rImYEZdpmMtS*0k z`8CVL^8c}Y&9-TyZO>Tx{?cD9HI|^opD+IH;&qFm#f62xUwFkrZs95OU!8ybJUf5k z+`rGgYfhc>9r^W?5hsncFp8fpnOJ}3APdfC^hh8^x`=O`KeB)4Y zzqbkw^!+~ez?C;oFI;_ic6xq#TF>ldF8kKB{IVb1I(7KGtEOgV7SF!Hck`izt7m8D zjUThg!RW7iVW8mhn`eyX7v|@U=38yH)wWMc6?RJVM+#Gy-8?(gu<($ENvX+pG#ny) zH&2~{-dqQ4qxao(?J2V6&~M9La?`PeD-IjIHo$E+f*0(gUvWD?XZ|H{(=`jqNH+uE z))Z);^u*h(25)-)LStJ=al6B}wM^>yZ7qSDp0_s?vq~<%slPRF3wvAM-u-cSHR!vk zcS`S%AGuKl+fU%TsXI}2G)c^ktTst@R~ncd+0I1S(Iikivg$qN=cwwUKUWR%3P5FtyM-6iX zI5DyDQ3X4|a}#Cvvh2%m!Y2YBwd@h#*@=yhD%b&@nJBxLYg~B~W&nSLm4e4N>KZ%1 zw^j-_r6)E&s$vIvYNG63-gWs+=%ha1wRy1vePHFtq{eqC*amL!|2=13o0@;~ytewo z)i14nc=fK;cdfpC^^L2yu3ob$uI5*Rs~f9ltBCEZx%8T)7cMDF&t8fwIhUTcw6yp?i{D!Ohs6&p{>9>- zEnd4=UCb{A7dIBqTAW+>>B83+KE3e1g*Ps|eE#zb+Cp(5vT**w-28WcA6LEy&veiO z2R(4m0|z~D&;!4RJ+PIecxz8`((d5W7f!7`X#;QRGfy~l;f$n|X^B@_?MuD-@?z=U zrycUowDntS#a5ecvqeT>Tg95vE|z(^#<7R*pYZpuPWby5pL7U3RsWXS*5QBOqx@@o zlz(YA_JtRZDJzUA!^V`w#*|$-rVQ9!HXj*NcHWq>C+#kq`_D0D-yBo+!7*ijHKuG# zAIx>f)XR)13+^sETpv@$j47kXl-b6VE$%Lxy?IR83&)f_Z%o zQ}*^TW!^Dm;Fz+fjVT*5<}+i)a^~c2*_kopJM)?`^?LE$ZX8oK#$IN|jPDFG zM)nC~%EpZE%+&6B)BiK3>_=nDzCNbx%VWyMjQR9O$JD!XOxatPabvX_o2d(oJ( zF?KrL7*mfQQ}&!OW%!t~AAPFD@DX2`n!D(V>l*E1PtUQRE_{ zGVGXvw;|l-PIR9Uo;-*$!wS3XihZphr@6R1O z@`Ty99=dG$ANAt>`uP*?`tJP9)kj}co|-ze*HJDoxx7f}I2O+!u)j+>x*gIH z2I#y`jYS5(x9Etk&%lZ+1^cz2=+_V&ty2*~Wg?PH=ZRh{(w10E(GWPKkq)UEi!f42 z@cUwLD52}@ld@hv7y!!-qE|5f4g(w(39BWCtBr{MZVC_SvhfOnb6vFW*OEA=A!5B% za-e)E!gtC}%$SH`tVnW2mVH{iA?s|QV$9hocryu4 zTqfag(&x@L8G`gHdRJ=?b@lZ4Q}f;1waHLE8pxw$8as2kx(uc8ed_8+%!dWZMiwW+ z35VBJ%2U~L7#q{out*6g0ha4wx})ign?Xd#Rkd!7z~i-89IN?tI$L*%Qat%AW^{Gu zp{|}9-_^T!x6@$^Mf^E99z1ipTDM?$zq-2KqTqTi6%IIQBJ5x!Dn`lGFQ&&f+TtZSDbR--jYlI7{g;h4Ejp=GQ?^I#c?+c4SvD+2;HOyTI5G@4l z1@UeH=ezA<0>p~m7Lyrtb^D>NesHg@9zFVqp%|PZ{CEIAle#*!UsetCbu7oXQeg_q zNu*ln2lA=DGN!A8zE%wDKq6ADrPu^j=fsYzicwk7iF+|uMfBs{hTNl@Q~?~!)vbrR z`qz7P_2@xQobYF3fk1}Hok?Ad?$=xm^KdpwG_YzNpbAt&&j}zh*>P4K=s+%@Q7EDH zZob~|wN<*Ck42C$!+2GDo=rt~ISO~_G~P`Q=IZ7{UHz-Qx_b0n49{nfJQgM3GwQX7 z?$;LxOypS{R1iItHsYYjcujIJIbe)c2ZRQi&X#gY4P#l2<-<`X21NUE1W&_Yq18p$ zYBl7569Kv~uQSN~$K zt{y!!jfOIbWQGXF&aBtse$Cb4i~}n$ovfg>2#^sI65`5bTVt#`ST6Q5F(80Dt49xr^Kk?Z`;*Z?_KdRX{hF(T z6qL%j8mTOmAVknlhXTQDH8-xSeJ*XVhzP-&TvEvYgFH4N8{$afx(f2_KTmlViFX zCW#CuaFt*?2$a|c>CV;oxZ1X-GKwdc0sKLLqRNU3Z^c}gVyyq4JoT)pRbu5G%MUJJ zWxLC^vQ$}o@8S~|>hqtRKYOlre(4XN|0CnyYp}gcV)T;Pbr;{hygD__ zUNJy8Z6G|nW+Bda&k&2r!LWx7q;=Z(w-w$C@93A^b!)fV47?jVcxSC-SsLQqiSU_> zbJjp8&kg?lqG_Dx-M(btbnf6hWF>$35a+4kL6ealGBApu@ozO{8tFN=FB(XPWxbMNz(mw%grKd!$*7BfI>_LbUe~=fnJH zBGQ2$rC(_v#s05pr03jzc$Xhp$ml*qI`pH77zcio{Jwz^d5vj|XWu?M>PHr0$cGSz zel!uHfi3!%#=o&S(+DrV{m?EyvJi7VggC~JL@APGbi*u{$*x#QR<-(L|gBKk{V^oX=c0jq^M`f^qbHWFh8#i1T57G!f~* zkDyN)NZnsDjr5$`^|1Pp_mPEY_#x7vA5FwK@FVAi21e(trZJwqdF`klS%{<`LLB((~AyZp!kiNO%>DSk8& z=fIC_|6<@=xnhrg-Hr3SO>LJSS>Qhy;(VANO+-5Iqq)x*NEd!+7OAoRpPe~AWxH|y zH%A_?|8nqi&;w^c58Tk%%x>o)voNhsrgwRGHYB%^hi7csLug!r7z?xa_%w!A6^x`Y z+*l!J+u^dcTy5BQX4^k z8q!m`oOVVSfz7}uClJwkSA%k>Kb6Vkv-z;`PwQ%)(zYj~OmiD;wc;F3b4(66*Z zTEC+Pcs>;LhEh&J7; zt(D`A5^?%@$a)*)c0_651neoHVS+2I>u3JCbva8ZP%i=ap_0c}j<#_&1#dX& zwVWy_Ri#l;`wp^#u;GZ1FYsy|^)oyTavc;2n#)6GJo;81GHg2!nT7A($sAA*f6YL9 zX4iG@HuiJZEPN>%)^3HAdr;p3;rIC0n1xU7)5}9P@nhF`j^92f@9u?*@eX^;^N>N1 z5Ay8gh{0M}(X+58qSYwsm01o(akd5cTd1$jvVDqnx{$n(_F*|JciL~AMk$ZA01^oy zdM&1vH>y9n4;gsY3P>{G{L=*t8e653K>D&z7AIt zm?YOKhtj!hAwv80G`J?-I>w{xj(9_RGd=j;Q>WJ~Sl9kJqT_$1BU+g$#=J3i7Q&-% z&Bw?A9|B1xuck$^7_S0nj+ zmlIl;v*O5+&0bk*cJKz0S`TilSD7%1xVd22<;h~5m{LFiC}7U-j7J~l|2zB01yieq z)!CIhSFT!_Up~3~++~}74}h^fZs~(d{L&K_Ke8w-K6&Bph5EuD&)+-WUcF=foVokv zx^rtsK6~W(y70lzK@S}Cz(Efj^uX_J4`A0n{*a!vO%P+8oZ``JT8$NZsj}VQ?7P?& zt5vEAS%J!SJRdIx{WaR99L4gp$VxAI-l*o|bk)t&1`advqcI&LqP6I%I%+_mi zC0R*&6jbyho=z~~@8n7<7G^x5NJr?Ek5YL*T8~OXzn5(01ft`YcjPXM$ z4|lX6+l?kWaI+710sT687m3M)p3=I~425b*Rm)Azt%6;@YUs3(;bih{HthNpL#Kti zXp=7=Vb?DkIxXDGnSA{QyWVE#v~WjZGDkah{i30BU)S5%^$Uhh3(Uup36`Ws_l`3XhfrxUHK$!;LR^Vz6Q7{I<+sH2a zoHKNWtFpVPphzFF#~UdSU_G)F4Z6@^m8Yn*=8JUrA|GpndlYw6l}cE@%cKi^wSy{+ z2AA8V^N67{B-e^en(_LWAc)~SQdJxgf4Z3r*Ij791*M9vG;Ei$QmE=WTE{yK+^%v; zrxj!TOgFgOj}99;9nqAY;Myfs)KG>i2ciMZ&i1uNF`aLz5(P(t*)E=LA?`*icC^zC zA$bzdG5t=mM^I=#z6rxMH2`!ex(0GJV z{6~ZS2%YI=I!%R#Gl5n!AKj(%kfGC6tb(3+4skU4bRiptyb%g(aa}J$C>g%#hJrX= zZ702rP^Ws7M}u0g9@FDSDVUIhp$fB0=Zv9KYLrvyLe*0ivu(S_-SxDPWQ`0tA`uF0 zq>Bx2BBXgsv5Z>7jUKqK7LLj6d3m(J%KI=h@B z1uO1io5$P&izUcV6f5Tv4L2P2q9vtYVw$8&bCXCAI>K zrgrJ<8ajihBaZ~*%{~O=oG23#s!1q`i3Cu~Ar$Mb@@2Y2ri*@-Q;wEX;jEU-R=ZRv z7)LsyNbS<889IyES}gC2Xu%jn5ok10CH#a}&WkV-;mL%{#VW0QrjG-n#vNrNp-?+q z$Q4uyj>%QFA??!HF?8AkK!!!**k)C-r`WS&k&GWx^E3GY!V zRnLf>9AC|o@jy6EvAdsL9pH@P-EaleM?0g!C3m6|cG&G93de;=rd#S(InbA_)a;I^ z4>*dJ~t7;gIYXV-*Msou;5v1T@o1)JSnI_rkcSSW;al3+8N;Ob&ujf6l7gy~cs zh8+c!gTP*0^f-N70w%pj8*NtdN2P4F*$9PYDcIO;pEW}#-m7|&wW7b?@iutYA4a&e zgU%+xNJ+JOVYOxtdQvFkgw$sBXh{gf%8gotm7<+wtzJ#3qd2c$GIUzF=QR0H#^C?6 z&tEgO`bC}d@7h&-^}>}0S3bY;)|G2kGArjV|7!Vj%Wqk}W*J{zU!Jl3lkHC1^)|}3 zVVhn0r=@r3%zcF=`_jzfmlyBQa{>^H>x)whUtD&VPRXt-9)ipMxGa z=z)VCIOu_c9ysWMgC5xFfs1CbnVF+Br6CCtF2plZHX&kYess+|cgfr(W~}IQ8 zJa@6#s_S#>W~*K_cahnu7tUR1w(13Q7nrSj{@nRytDZM^-mq%ELu3dVu1I9B7r|2u zJo4!|^QSX^YPRYtW?o^o>a8=knyvcsnU|Zb`m&jqnXUTLnU|Waddti$W~<&jbF; zal~P^DtH7OR?Q~RL^e`LN0eA8+bo695hq#dFZInnZm|_?YfC@YZv@=9gfE@5c>m&Ei!WMCFaGhu_ZChrykH@@@Z|Y# z&)+%!{Q2no6X*VQ?hgHa0KD_6KltsS2M&7Rpa%|m;GhS7-+BPtL=LetR_;dFJ`HXr z4Vi+U?qO9=h;qV>elHO$@n}6435N+3#@Zw$7Mw~2)l@8mv}}I|ZYB(wXi02>P^s#G zkaE%8uQZZGg#;p)KUpfO**wd}d$E`%)%dE{_9<{PZpes4P>D$%J6k014vqsMO02}C zW>4p8z+*h795a0ue1*L#RxkEBEXbRuk%p4%R4B?nVW|b8XFR*GP$jZ@Y6KJ;Uh%OM9TrFRKNF`&okAs^*L#F0)RrpLgk@qoG4r|C1X?M0V zL^d4HMqp5)?Ik5bz_=FYT(*yan*l?Hsl}5$I-U*z9xBG#o1O~S%Ev{Ltd_iC8guYM z!{@Fh-Bi(Q`zW~SH)JR_36udOLv}n#CDf$RkiQ5eiU~1Oql#La6@y_9SC$1@&DlNz zZeD4~ShyEs`!Kk9g(2gud0DaxkM!v6=~Mh z>SA`S7)b+gLWYcmyGpi`;HJxv z3F83hO}dm++S3HG!Dg}VDXVxcCP#YBw%eI#ss$z+cLu8!()QQjrqhs#V!?8+({8A~ zzC=llVxuaN%@*G#vwfCJ^+3(<(ytQ8ohIV4y&K$g7&2u7)Vx)@*Bh;pup7!ItKK4s zq7s(rr}&b<_xoZT?uRvxziN9IxCt6E7ATf%cY>RMA!FfgmF=D2=7u3-fp^IE4si1l zL&gFJknOL)O}imufsMy@2e^5$A!C6a$M%=t=DHzc;Vzi%?cnA`hKvQy7~9*x%?k|~ z3;Zp%w}P7&7%~=^RBV3%Zk}(*SfDnsy#?Gn&ycZj25);axcLl2#sc4m?M>k3njvF> zd&BldaP#SgjD>J%+Z&Aa|KgN)YDKbrWeHxmbN-Ky+&KHQL(iSQPcM9gpBFyvdS?FW z7nP4bEp&K1*T8P#^D~D5^NgoE-{FV{9iB@7{eNJZZA#~?pM$dPOE2}edbRSUmqu|s zy#ZX}av7WtuX&t4Cgn#Slij@kXU>?M@ofJ)DH2SN$*UB7OiGi+WRyz%j2e^uBISK_ z2-J62=S`2vpB~Z2gq}1eqfDY_(wLm#`2%*6E|?yZ=Pm1F^4v*dGD^;VMvcjtP07B) zV{dv)w5RA}Qk*m8bRCRa}ylTn7+GiglD@Eiv_i6KmnN$5^}Os<+VCZh!MXVjSN*Zbtr zp^e|++BZEWffM?eJbTiZj55%kQDd@S?~_L-Yr;;d3DaY8){h4Kf75TAT6)F&Q)X|~ ze>wO$(|X`~VKcu?$Yo)#YH}Rr!#Rrfro!{>rbaQiL{68GYa%1CDQ=(L#92C|vQlg7 z1YtUv$n&;1z&dQRB`7t6UBqRm9Z-ZRP2>NM-?h@I>PEhv9>z!V=n`_doo>gTd?bl- zMQ{cLcQDw^k??xOod+`6ypnK77{KRbqU#8flpQhwipXj60Ci=}1Je$m+bzLR18}t^ zDHv!)9ZC%dqcSg+Lw%tZ&(-}{Bni874Gk}?`_qB0AULucdAS{GPy&Z#_->tWOBsL6 z6G|ztDMGHnqi+#%5!-}Z7B*QY2RfNGcJFRuKiA4OtYKwq7I^cv>gUq2K-bd;n(3MX zR}iEciFDBbfO3(7ns%k3u2T~!#3@Ps@q}C!_LfhNkn1txm^?!Wx!ev11nC1gmKhIp z_F@fZ2Z}@jnGIIT^<6Yu3%CO1a$17wBHpZSw7e}&l%1{9c0?b>dN$nEbd}2avQ*kp z*Aq@i=~zT*2f7)etvS=xem#=!!b}b6E3R0%S%O0ySqoJ1T-PU{J&%v8DIGm5v%oBEwc_RWiP24VKo2sBfQl?vNgS$hiy|e;vaZ zugmG`cK$Z@-TJT^WrMw0DAQ7rmIT@Wf2!^+`sP8QfpH7Z2*_5OZnLM94dhF-@c};g zwJMaW+rKwihAtaVH~5QC>j-UwLVC~v+XT2yy`e{s|IZD$T}BIIr<<-;chpO*ay_dO zX=wxOvkj%r5UfkqC|6xj5^PtFq=j`F4xRRN1N(Hwi%`2D`r%ke;Au1u!g4>Rxw$x+ z43_)ubT1?%VOmlVGUK9Z$(&kq)`)bWQU-EzDN+e!g?iXg$p;uM9`FIdaI0(bd)s*Q zA^+d(^dC*xZkT`4?CbPj4u13=c=6%QXK$}UEL?ZlwTHt(> z&ZL@!ND7Oq38_JMB(Bo*S$xENm7G>RY%xGkz6f;g-+1 zNqfu$r90=TA3=}N*1;GczYAJmw+D3XNei6!*F>z|Zbu<5rE0Z4*A@$%Os!Wg3HoOV z(uKGVRVTxg1}mpMJ-G#corL&G&E;S-c(+BzNH9d=2{aPzQ0<nxZEiJo?rugxX$(Sg@}BbHwqq zz-PK6TA3*<(E>k44){A>g}6bd@!d4`v%r<>2ErLntyE3dY6&6)3Y=OkRAsddXTY9Q z^~%Yv!nxKvsncJD#FBv&E4lP6ZpTL?)CXP(f4@0du?l#-kgr`Oaox@S5*U zTe-r$f71={+fFxQ4#dv2a#@63Ue$;q`7SjF#48@n9w7GM9H_@aV(! z|I=sBnp%C^YG&n^D{opsmj8SCb<0uPf7))d1()t$deze9i{D+mbv;AkC;$vA&?wDam7IGhbL!Euo7#IpSXOW_CqGrS_ntS zPh2)o`?n_4T8LQ3Pdsa&_JbzWS_o>#Pdsy=_5&u=T8MkcPh2`s`+gH@EriD7Cwv37 ze=|APg_Y=e{DgO)_AV1@Ed_e8mK)vIhBZ&V1N9CbD;LGO{lfN2Y{b&4Aj2cgjx%f0r&}Up!QuR)LLK_z)t`J zwRf6OYk_zGKd~`T`_9RkTC8vr;3qB_sC|bCwHD|L@Duid+P^ZP)&j!;e&XVR+B;0B zwLpS^pI9HL{Yw*SE$}4ZCoUSOef#9yDJxV8_=yV#YTsr;t%Z{^ga3bZJ~XxZ{FR@t zsLTJkEZe?sW0$_MboJstE#?+Jw}8xlO8@i0&p{6y^uR$69Q43J4;=KsK@U8<2dWq@u?s!myK1u#Q@~A6qNMD`~CT3h1ey zs93n4i5F``t_#ba8kw<2Ty9DR>NSb=7XzWALIvwaWz=8JW+PDo5i7z-kk+BsoPr?< z<m3XcmQb?Zj-JS?t%wpz z1N}hMQ=I-2oJ)d zmTRqv-DId->xwEaQWg?0LDsv)$IwY#>x`N|c~VLKw|f zP`N*XVLH5n0jBExL@-;6`{O<^lfZ(0XOkmZ1wHj)8|wD7jz&XiJ(GQyTVp-l9OtaY ze7-1M3lol@l!v4S#-q(X-q1s6@&Q&q^bT?L5_VMXaJguxRf#m_x zE;86c@*NKpisik{oVT9RntT_lX8lPpx7JK&YW<`KN|yb2Meu~2Wv`#9)~P!K5P7ua&xh-sY%U$@Qc0qm4vnT$T)KV-gE!Ix>_NekOQpnMTu$h{?xIU@ zre6)D%WO78$Gh=fIxNImYhHV=rj|+!Mjqn-YhnAd^p4?rD#0T zLXHyt5X`}Gq9Jz+RL?JE^P~1~;|>OhQ)6NqOXhMdsEoRWP9cgAaX|>j> zS+P_rvAxkWh%?*{21g6!qg~jE7yXqGR`BsiqZ4fBd|{ufRmr=3F}5EOL8+A2)?6Cl zaM#puv0m%uaFxN@?SvyAY&&H-s(NB=JDF}oD8U={b4MdulF2Eso<@_%DO>^WkEW1Y z{K6?1qCsaV?9X8mM9^haN~NQKH__DGh(pSX3&mN>g-3gn*Xyxk1)V+8fn= zvMjYDWYrG_70Ropv}5^767{sAp0FzC8Pw(RwIC=;GF7UmCpgRrJ{M*eVg+C= zS)nnV<`?%z5Ij}MC#VGCv=@_1w+gkRDM;s)#hrBx%4Q4YqnVTKa%F1Ek zRb5YY+ z#@Vk&Z_lt_3kRmA+dI{M-FiEQ{aQFdHQmmL{X)axv~a3wx+$yn>oTzDk8R;7)-;wa z`>h{$>TO(j;gpqg*~v$SL-QTP3$iUq(SrwhrPaRFtJA;A#~pf;hLu`4vo+o1I4cG9 z_6#evaF}bly;H3e(AzPr)WYem>2^k}bVF~-cxhTV@ipDlBU|Yx5ej|$5(A6=*cOh7 zO=H=zQu(-DZ^JMd3ykZNPf3SXI?iM+*4r~o#=>E;>Gn=FnRUG#!(=R+E}L#=#AGfq zhDkRW3n$K|n|dUZ(f3qF-c$7P3k@v#V_P_;HjQQ5WG>L#Fige*LH*=Y+D9;%^Y!)& zld*7^ZMwZvP3Ank9m8ZSoNk+LXT)TlVGNUQG8Rs}O*i$(CNoMPN*`Y{u;`C%;h5Ys zmTi-Hy55FiG8Xv!C!Z2Og2|k#w`Z7)g~N2y?VV~eNA-3Lld*8RZn~WjlR3v2Cf#H# zoVc5A>XA)mluMI7{xk!N{@50d=}lwVHkq^aHVl*5*Ld(pFqx<7?HMLx;V^&l)qr7p zJ0`Qm|M%{xn)58i56s3 z&DEoEh1cp-nvK<~8yoQepX*h~u)u|@9&p{K)LTHtb^0-iO;?Z3<2gv( zP~GH4*%S4_EuvReHfpUUa5a|Edsb)N;Mii@puPbiec#aK0tS!16%zfN?T}~-#x(goW7hA+ z5;HyKeq_}VErdirMxHb|Bzl)G9uPC>4)GWgGkF|(!nD1no46;?p%YQiL3Ypu1aYN zoDNV2`tVIz1IO0iiaN=h&W}9uwgW-ep3J9cEk?8(5==zGH0*3s00$!txYTM>X(lNq zXsp&j-7!{S)YIo2>SeGT1*x2dRoi`PJ*_hmdxLFPJ6GV!kTVW>oL-$5O-Z$5(T+cc z2}ro@O_xwT-=2VAmrR|q+mu9GrelSk2-K9XwLJDN7p@1=i9!@@Vu84r}uY3 zY4%0$+UhH)q1`c@|eA~N>_f)5NbosXR4?Jj3y3zQ0UW+v=nJ(#R za)1c-YwdI^s?)D!kygAav>~xlQM%50c>VN^9q&L@h=Ji|-{+93Y(>9;(Q7DDE)on; zKtt2I9AEfG9dT_=Yd7WP1t85e4WbUgDN!$w1u0FJWl`R;OETh0i6%FyuIw$*Nxzj*~pmm$1V4vu}n-B_GXRg$jZL* z_~pBWjp=bzDyQyUj?Rz41X^h)5vWeL9?#y(jv89_q_e#)-skxiqi)c=TWbdtPof@V zatKPhDjTGap~Nt7`umG+2#Yk_fB@DoVx5SCop^^uU{|Rjie4?DCr951;t1x;x2Y_G zca!x}st%wIw}M9tzH~k&wY}>E&pM=dpHQ$|$OG0$5jMB%Fdltt*+FkFJ1p4yzImQ& zpVRz2M&5C{%Z^>f`xq`e+`heO$J_$eDPS$F9Hl5BSV?lJU_X+Rcv)sKkJfh={N4z< zo|4n}Z$UfOiUhm=kG(g6cOTK*@ayX!^Q>-BxVVQG5EmPu?e=<{yQ-~f-k@}_8(sa$FY<6ZyST-#Bm&) z#Q0yW*K?KBhX&0eClVjtXI85EeZTenRaIA2SN#fU9FJIAH$O^JSfUxvW?Z)8sL$?- z`E7y_ZMwmBYkLm01yj~|Ak8o-YcLW(+X1HU5*qH1&*|)nh@;NB2-LwNN*Ai7u(0ve$8Y zZv>8_b#38L&ys1T8+|rZ50>lTI8U^esNMej-|guXs&t<5*Dmy&sRkKhM(O(TvDeBM zsZzt|X8AzDUhmub9=EeB9uG&>sP(v%$vChueQdFicywUlctHS%VN(H~;fy zb>piWWz%1nitB&Ao?rXZwe0Gjtfp7~Bgn4)dGfiO2^cem%0&!uN3FxM@fPd=9#6PRHS>%8kga!g=`O{MeQJ^AEwkIhVAu3ILb zeC}js0(0G}`s8zO9ut^hpY6O|aA{@&b9TYE&P-s=E_lTz>L7VN76#t;(~ko$5~ppL{McGl99zMxT6+pP9g1*S(*7EL|iOy*xQzspRS5BER0&+NZw?+-yNfRnwI-Cx{&V)v2V z&7EJ|d1mLq?azU1%g<7Qr2e|_Cv_nUt(MUd$_66v$ z)TlulvXoPdH*f0NdgGfnz;RULnNN3BOy5!MaKH!Ix6rwocR>Z(dc(sT;IOB0_Sa8? zwYL!81~`{$JnQ7Uw=JIwXg7n2(|yKUkGye%@y?R%*6YQ$9>q35KoaB3rv@3>@?Wbp zp?1rM)mmh|()Wk+WgLzHH>K9B{n|CR8qBO!Ypfo4EA!gtuDR8q`5SIs`}J#XHJEkM zt!uw=&8-GaoVs=Gv)2R-wj#}9B$=YpbUr|p`hKX^P7!qVJFf|9FtethR#?#0chA*4 z97k&PnQQ(s$o_`ER=?w#zYJ!b^w;V;uldWMiBo^AzT;XmG@9+!X1Mz8*P5Zh{8O9Z z>btJh8Y;@g^Tk4s?I$CJK)Ge0)~xrgxz%81ty*KV!fst3&DA^{7iYbDt=1T1f1}o{ z_pj9&gIOnQ&H7+Q;qzS(A@3xaPG{Tg^`U(J|J<5Ij{j#l_{_m$=C7IGV}9-aFYF6@ z|7Gu)y)(N%y&K*6^PRWtykz^6+vL_CZw&2ej|va+)Jk#B4evCMrZ z70AB*O?n=M2RCRW6!P&X8i=k|-}UEL{={l?U$lV#S)j1&5tK^$HW2kwRg5*H9?1Vuy{hsqno ziR6#EI$Qcp)4tCi*cV2A)K+ZhPNv5Qovjf!ir1k+DE15e)?Z&Y%6)`wZel z^>E;R2jP5i#80z?+#meeU+oygiRj_L{Q(_f16Z79C{)Jze_w47C#;79JsB+`>Wc(8 z$}f&_KL0g?IJ6!P+#irJg2kB#5taL$Kl`yS7{m$b;lTZYAID+{BZ_2D#`)wc4dMj# zaNz!cV-krd5tG7*jPtJVF^EIy;lTYtJm`}IHWUtxb$i#}7{m$a;8@^x8VCdp%Hm}@MmxSltJBaJsh|{ zNW@r76e2#7mvJ8Z9fLTS9uC|eAQBeGqKuECWgPOvAP%aB1NR3Q6Xba;&Plk8gFj*r z$FGM2_Xko?pnaSeh>iVo{E$H$pB@g}AJBd=LHZG&pOnjU^q@f;L=Ok<4}z>uzync{ zWn~=me>I5ns2&d79}ol@l>7)4O2{~SzibfajoLV_i6EO8io_CFT=I|G=}ycb&KvY_ z;Qk;Sr-cM1;+!9p=THA8YY^uVJsh~5l0J!J$RHk({qv`PjEqCQE+`I8?si1V->4%~jQM1UfqemoMAasK#^4B^Q0 z|9dySymEN%;KvS7^KYAT`+v9p%>KQ5AKP>8{?abH^B;GbJ1Zdi|9ywwx%H!4uiyN& zO$j7lewGR>6<8{;RNx81mM zbhXP(O+a|4qXr14QvPJ1_*pu)dFG=pMH3Kat_BFFQv(DgENcS7wNDKYPNxP4YH3Lm z5FVMU0mA9j06|Yv)Bz&L4=f2?{JX5s}j6tR@&-5!ArobZ00rOhf`K9TIpn z5k+G>xcIboIio2h+&8L$!RgLSeGa;v*3#v)X6fLC5j8+K-5DR^V*|06;E$8x04|Xt zPWx(?Q(C2)*tw+wHjz%HbPjjva#E{w4tV)O4G>Ouh6$l;AR_R74q<)Kpco62f!gJk zR_VNO^;Q9!NT*UduX7y#f9(S+`_F9u!{*)KAIr~Dfdx?DO7Fw@YjMbCIIXLHcMudH zMh~nlQ4Ks!JDTx;kg&VlQH%vw=XRM`-f3xy z1DD7i`w(Zr>J*2qg56uW{fIEQi|u$DVLXZX5~Yxz5iWS$AxUgjnMQe#Oh{31ORu#s zETBS$PxYE!R4QFab{UZlRBUV*t2FYdau%T&rrEPZ>3}%oss$fo<+sJw9BdCk1VA@6Z6@7mSc`3;{HG<9E*?A zwpKWu!{S0N>h(&!RJT}^i2QAbyDSHCp5tyL#mBHV__rfDN?(#WQ#n zIhKUDuiFoYFJ!zyo4b@J<5mhW_SBJ!4jLl3)haU{?{!z_d#|F=hG_do*$`)lpfRl> z-q?vQ_X{^)sqs8D!0QC$cK7_>YUKrEIkgAig?75mw{jjF5&T5T9bp^!qzmD@7aSPn z9$1|judU=v)gtcD=|x=f9a70ox0RJIk9r)wbwmCto3S~aR`0{flSl@s%OwuwYc{8! zjym(Dv_neK7xKlh(B?1LeI>qhfr&@htfLq~-PHlsO2^U>OelLJHhV76EE6G?wYUS_ z*swoLIa)qfDcN&`;wi+~0}$Lu;&Ov_C!hbvR-awjCbti_esAl;Te+>H&A-|Fna!s+ z0~^K7yEZJQpWFDirvG6Inci-C;rf@>KMHpHAFuuG#`muM?Aqnk-#Yy4;rkBXba?*Y zFAsj|;H?LNgL};X!TiJKw)yqujs4%)fB*iQ_g}mBU)FZke5=py{qo-X_i}r#z0KX< z+x^&Xf0x{S>CRv5{LIcfcP{N*-2R8{-`@Tg+fOaP>IVGd9H((^r#iAk+2*%ghTM#GStwD%FoQW`4Fu-8zF~n5ALDgbCJ(P5$ zXCcPYew+#U!`ymQlQX;uDTE1zjADTVvL4aQ2(Kn$G2Sn5d_2gmhqW@=;8#8Z#U;d; z6i0-0S~DZO3eN`_42{tQ9$F7+W`ySes9%VIgA5`YUJq(!gx3_q92Y=oJnj#!Q<@p! zbwu3n;{tJk^2gQ#ni=7DBoY;k#n3n+QtPBpxCp0s{Yupqw1O=nl9uk(6fZ77W8L@^-*B5kc~urK@x5e`|ns9ZC zG7*f3AWYD=j%a3tU#9S6g1{vqD*4ynsF@M2Zaj#JEr4xq5qAAi&5W=zVU7^U2tr24 z^*3l{gtZ|VQNYP)jAYjz(aZ?fni$3=@HjY#;$Qz3EoZ>_M2_>ZEXpEyQwwimTR0v_ zV2Fs2K7Rf6ni=74it-0TzEBKB>2k=!X0gMJ)qc7PqU6bjpu8H)x2$~EpG6-=vCB4lFmsZ5ZNQ9)R zD046^{X}tn0R|WOL`Vvor=_3ZLm)mW#3e9{+MkriYJus5qfv>7vT;1}f`?3gS)OG3 zM6SY**x)|Q7a)VcI(S1IahpWYIHtbIg`by|I=DbkX)EWQCJ{gw%10A%@M@C?#+X;R z!LthtW2cQqy)c}FaB)6P2(-BJltzC!vHhLmD3T12K#jL*!NGkHc~9Q+_qx<%XB+4ES>X;csx2lFvp5RXz3^o&NKCbm>XaEuLvXdgumwX(rz z1mdV?CSS{}!~nm7VR?jg3p& zo9BzAK)xC&gxP2}a59XfkC4{Lk!@%DxpJ^tcczcH1a;y^YDq_334wC@YV<6V2vJg% zYL*gGiI0>rA`T5DHo?h^uZF%L|E0lA@NIe2aillBf9iEt&&wR&c!T4{c>zfdnX(&` z>}Yc|4Ej@ra3fCOI3GrtjwqD`dp#HKcNvUsD;y`+y7l>P$V|x>$$tZ%o!*a6?w5V) z{MA>Z|8up7t9&*Uy)De?3rfA7xK?uE%i}Oe*E5N-Kf$wQ98) zjaN?c!;&CME}LUY_#1tu>oP~5968{t{-+Hqdgkg`sFtps7pc+-R>N&>0&SNHp^-OK z!rh2i^-F9UvsD5W&p@izJ^6-HoUxX!$?V?vujIdvzQ{1g`>vijT-i}t(vIAc&WAgk zH{TjDez(0So}}G{V4gubfdE>!g;VYfdUPVD8tJkn$j23mr>jT$GRoXue<1&T#JIoD zUcFyd@xdGZ-a0>MWW7g8i^V6hjaoGq%7xtZa0#K^J%14C#Zz4d&$uz7R~)9VtJrQQ z^Vzdf0tq+BN^vv{wcE*?zGR>@m%>Lj&*BtI zpw>$AC{-tTl8B|Nx&Sz>=Xqcp;n8hsxvPUg< zF5ga2B~dJ=Jb03^6^c!-s~mDkC)c`o<*}*Daoi!Dlfz$p_1upb=6K)Lm%<&A1ojW4 zEp$xmxkGiVl?-FZi8FW-EX8WlNxzdV`EhqXT`eZNP=};TQRa5vC*{BQ?;B=#_tlrk zDqio9-UtRC-l3&I54{N9>-)0GGpNHtSE(ofMu(uuz9IZtVAtpOW`}4m9?o@q!0DZ>N%?vB9+KEsac(YNKD%;~^CL7F1D3`Sz#X~k{-gnexn90_y%X8eicYZ|X zx%>TwdA|DUJu=T*TK5chLMc3N4tlBY@^S0llmi}ob?XC$nVz}&BAMw;gW|PWZlqF} zDy>^r&2j5qe^TbS{s)FR-gotdaOfICVXod@@ z6pB;Yk?xK*xgCAwpZuZx_g!C}&Uf+<7F1~edQf5k^erU zvP?9E=c}*YHPMb{xY9`Bc}qLeWh%F$uRQ%_`R`?wIy!~vnX88r?P!M0%nDPb9qDqE z+tF9vCI{*G%G-X{sMqEB|BE;N+sa}6aO2>^2S?`5nmzlU-_Py+?cTTT-M{^8p_19!>d0bsv zOVC@~l!pt|XIa4KjpmhERy7UtnH)f6pN#>$V}Liqs-$7O8v~&Hvkbto1DqLF6%FIu zJb(c%jVdmkVOh{H)aeLh`jBFBT~!t!`D{ey1ByPw8lYi5r^9wo=&(}g85YR&*TkmQ zW4(2|(|Sw;K5{*tVKK}wpK0|t1n}f~Ji}s^VZ58`aS-6h^>~KGD8o2VJ*E_w&aj$f z80vQFaX{t+iax{Akzqck^|F>L-*h>7zuGG`_>CHkA_LJj=!rJml1zBJ0b=~`c9QUpGHBfqL6N?{g!E}H z+>i*qn;|OIWP%?Ch)yNT)cZ7w^+j8y93V24R1xt8P%H>+j>H5=@Ap~LHhw6`zoV8s zL*U(%&C#Jt$RNUDATO1px%?nJLi!;tKa5n>u%%Km!Q?Y!r7y7ngTPCojUT1^GJXK? zm+_swR5e_YaUxc{#@Tr>hUQ%oUhbs)w3|lcv{W^1tJG3e+>#NfN`UUx77bAC-8V zVVQEWl1QZ^c2_Fq_c8VBkxtkCK~H6Pci2LyjLu1Av?E2TlvA|$S`6=Tg$1XS;*cpS zBYZG=;_Y@kmSnI#a4{I$1HriaCzyu}B5gW`j(k+KRH;~ScmF1pzpke;yqRv1R1S~k zq%zUAb7ij(BZ z3cXInP1t-ltNizRD#LpZ7fR*QoKz-#&Ro%&h!w3(x<*xazG2S_@o6d-Q=_!6SVHZi zN+fzBT3uP*PPUHNM8l1T+_hLcl*+i`T}LXDzo{<&Z#|Xat(ps^^1S+G%tr*Q-d++L zv^iVA6^hr}Szer?vOh&w$)+dA8T8FQC%dJ>3Q`S7=^`bm@CXRdI@$gjE}6jRJ=SzWh4Q{9hpwQMGR6cnr;t+zf2cN=32dR{#S>I8xjZadRYG|(`f6LaySCgNr7wwwFqztdA0o}MpM zU7lBuLEucBquXM<&}6F8kjFaBSlKZ}W#0%7WYTO2M2a{OL>Gb7T>f0a)vi|k#Z)fA zxKkaEUy@QDF?MrZ{+gc3@R^5&Qh9D8IlP(n52R$VQw>-;)-d6X$AoEh8ENAwE}e_9 z?h;PZv9J&peMezeQ?!5s(=|@2IM9A7;YwI3{^ox9e~jb*fi3-e@7;c>={?{d%g>@I z@b=<|%hv*O&2??>*3c+kITX~?PL|;!W~bP`J9&RYN$m zi0*DHIfGufI*%RhW#wEqhpW52fLv4TK5qfJtX8+hZU@Df8jwrs+q-RM;Hn2(&Rmx* zN2%jIJWm}YuwkxHavp!5q7s9zN&o${V6>+8OQNq&kUa)rS4C?ZhV|WFv*ao~G#0Qfl z?AGK6DcQ_8Itti8e=@E%R;SxyaX3zkej1RgbS)s)To;*dt?c8lFHAlh?_S~Kf_w^^nhG5tdCEN12nnxF4MX>9+2+tGdY7^qn%uWmUG?QXr~bQrUc}2 zxgB0WZE8bAtF=mk$q2P(t=l{nTb_K*R}7`yX*z3ZqP9v79NbTmkxDgax$TA+PVj_Z zXtvlM;YLO6uM?2NQ$6nixjZh5!=l#i+M#$cMaEqvXDoyE?M2*{7c=EnHX0`QVI>{!@{WGh z5pD)8*lo8vSKaII1xCoAf;*3c{xo@^(C;BZAwn?1FjFWIg$#q@I9{%xQnlrci6|Yy z)4poO>d>e>ds{gYP(2X4-=b$zUeUU|x@*2Owy7py;68JyZU0L2h^7nO>dcu`|DR z=bF~;p7#gZtWQK}+K-=8UYOUC)hzhYDgUZ`bxXO&=5G8p_1OvlZ=^`eks6BWWaVVMYXq`=8oqh_ zrOfe^4@9!vNp`X~eK+QoO4hEKDi_n`YyWO#Gs(1Ex^bdntyk>G#Tg5b$u>F#@E`JM zcs$P-8wOvDrx~XxO(YT&e;tSv~gGxJZ^qTgg6!Is9o?f6(-WcggKsDE7FQ4(ba-+mB&f?x!y-{$8$yzd66lzteU2|8{ z)e4IplciL$+iJ1_7nZ2Bk8|0Ys~pWXk& z{&(zm_MQ7L+57vwFYW!}-sQdgUTE*Zy}NgRd-tbyzk7GEo7_FOy8)v6{p!vq!3~1k zj(6w9Aj01-Y=7VOGuzqiFo-tz*IU28_1xCGxB6S2t(R}DZ2sZq)y;QolA9Mm)WN?5 zl$M{R0!sy!3M>`4uD}~M)?RaIZU5Jlr~S3;xcTafLBW9SUs1B3E*zRa ztXTbJ$m(f%^|oZ2KctlRmz3<210eHvE9Lz~CCN?Y{h(6bPb)v&JhU?ZbA{QzftVRA z?+29f{(_QSDetonY^*)_;M)GrD_>9DyK8>GQmmg-QcWq=cPYjCStZF$#rkJTu|B2z zbW5@RsZy+;QGUEu!KN1rUV$~gPbt>FR*nVQ+ z{}YAqk10QD6NdL5n4eJye^SY=5PpY3_+KhtPa%9oA^Zs?$xVdct`PoF<)>Q+UseeJ zi1MR0VfZz^`P&r2|3b;G5Pn)A{KLxEQwTq$5dI-0$xVdcrV#$P^3yGZ->MM)LFGqn z!V`7)Nrmvol)ROiSXkJ;qOy^x`psaA^g3{k24Y;Duh3( zWLF3e6v7`-zMew3uMqwoCCN>MdkW#J%1^ft?ka>oto*1+cp{9OxuX#Nkdj>?+*SyG zxAOHA!YzgH2bCl@5pF7k|GDzhErc5i;SVT3Y7>SxubJx#;b)cX3gMbU`2EV)QwUcT z!r!GNxruN^A^gvjpKc*sRtW!7v+pO?X1#tU~x*N_K^CMj`x8?#iSQ+b@Y|K2ZXx`w3gOGj zkJ^OcjXvf#DTKdG$*vHVyNA)e3$v5y`SIZw!ggfjTek z{2yz-_l?_FT39NuND4fizp}c$cIoWp^vcTm-t=W&k4B*G1=_OIkIZXh7>W<#b_!3` zL>q0X)PxFO4b=iuHir2#{VIq69|k_pNH%s5o%;FR8S|;%@q(J_1oWiUFsbbsV(p`{6;YN1(Lcy zx6S?-Wl2;iKUWD`!kolNSj8T%)Tc=8r|}jDg_b@Ni>@Y#IZBSsiBlT!xo|HWPN7Ll zZGd!Kok8*Bh>@j!^19U9YEr{zR&E6BUnHp`bF0;{NQjltP@>$HFl#&+FO=i9sXP9U z%3T-jw21QP_oviq)RJQ(p~8{JZMQ{F_)=+@ zp&}V7P_)G93e5*wm=x+t=}d~rjHQ0db*VSiYBhX%<;JBD3nlfucBNwjG>cIZRjXi0 z0&k`gWg8!#)>k8m=25fcZGnqmJH#mIayA0avfXb_w23P3$s%E^rQ_<=Z6NCXxUYWv zy3`wLQp3}iXiOv%2|wxgFOt-AJ7z&5_GC?>gc4N_b5<@R6#NO?Go@A|Cul3+s9{JG zghI~=LXXe#l;~sKsNEVS)9wgQ*pv1y>F9&4>7WA{U62$yutfg8}pXG${g#D#jtc-SwRirM@|LA) zsbPBU`j^+6AoKFGRN(&_1zvx}v46>I+Fsed2b38M+Ks(^z4Xt%_MZ^;$lASPH(z>c zL(VTtrZ(uG2x5NTmO5{7ocHBMVw|sGWiEhO%BWxLHT_7}PnR5{?s23~S90}^rnw83Q){?Wr#=8uA7Fq)~j49+WJkt=t|CNIdNHrUIj4G9`|sadYr6>)3t^d zZ|5_~2<91<9Tl6D>XF9-d$DBqW}O8>Oy)hgi`gs-aDTgd<#k4ICqhwNt9$KCipQK- zX2euo;;;&C>*#{J4NFBfT=h55Bpr&xQt3v`#}|@RTCg(?*RhQI1<#cW`QiVkWJu-YA?j#G?e`_)q4mbTHEyvK`N z1h>Sb?LCGr4aA_eZfyeG&v&o9)(|c{QB`nzoj5(nb>qaaDO75?PK6NLV%CwewHnDV z3Sw_K*k*xp4P91GD^$Y7RKeMkUGy8>E3X;j?r3*QHh3zb;Kut>)bCEzI7i20&tL3` z_GG;q!CbipNrR2qLOJilJ9bwgPWT&MU$@hA43jp1`|I5+uQr6M(gdruey}yP2)>X* zs`_o-QZf<wzl|Zzpo8jkDT(m~Y`#dKA&U7VnFT z*m6G<2!P9if}t43w>ptXyck8R_GY0lWD6JL5>nzvDI()@v?Jv~Hzn6AfP3CZc_%I} zRdC&`*Hgp$yp!~g&`1Pn@^)_^Qev}dn@}kcj$&TCXbpfFCKhhDY@V>&$=YO{Ki9qT zDnsQ}>2d~CJRL7rOOCE1dl4If8!|?Iq02HL{L4j=Z`KRNw6pGU@L*_ZZN)-uYm;-! z_3E?TE9VTA7jFAXc|GMoK3S&=O|**V7z^I6Q%o(3Fu87dR4+wyyf5o;J1b!KES2DB zS`eb6ND<(ErhDbAAzYPq6io|gCQThX%7aL#RqgcoK#%YCqP~lX1|KM11vuhPFuUu&}YH3BcF9ZyRt&A97_8OPG^D^ypf*8$FwdEBCQ0#{e|w8ml^V}O21)s z(7qxV8g%$ko2olT#ndt3?hbt!A~tlU`W#9+ZP^xD$dfIG$rAZ2njQ~V0q#o;l~?7t z-)(ojC?+IQNW3zraC|tAMiP-spip%4bsLk7*D|3>*G&wBi#WB6myQ>GkVg4ZaX+EE)t)|QAwFO$4 z8O-g4gs(KR1dVRxs7|8^DC0pEA4b(d|w36#NoF~CfAJnq7g_CU2 zw<4^sE}UeGzNKJ&W#J@S^sNCPl;xs5G5d3SViI@c7601T7f-VAV!uCL``-&E*P zel+K;jne6YIP8L#?v$gS$Q4KOAY)_UBkmmGr;<-5@q){BKAxzo9N~{HZ;wJIVWR7JUK_dzP`DDlFe)M zkH+c&8{d<-FfUaaT?TD9lH9bn|0wTD6JfCfcHBlCrJ4t?_4!82)pw4%0@0_$Fy?k3 z<^0gXG=OAV^85eY70=2+W&ewN=*}m%U$Oa&>2KB_Tm5zLrN-ym?s|J|8^kj|dq1YO z?Py{*?jlY87km%yjg8CKW1B~_t;(4TXTsUe!w)0vVJ-de!z9N=k1ZD+4*AZ2To=@O)cO4acz@%xf?=P0^gjWg-@?#wf%yvONXC5eBa@l z4$mL_<-t!Ky!9Y(aF6*vn19&Z266v3_CLS>?EYi>5AFT+*88`9bL;%xukL+dudwIY z+Xm+ZKE6BLrFLJw^VOZ7-Ff%U$&O`bb^CX>e{}oXG?oySKP?qlDzH>wslZZ!g;v00 zx^xy?+iC2aPeep1oZ>k$vi^D{SwDx7>oLcbY|rGZ6+01{iGp zVZ>CwLDle?nixy_aVF#sbL$>W&hY3A+%3eAQ7n)^*4>&J;q9SVjQ0y19}lwYt_MxX z+0{qoaM?%QMkCvLxKalps0;OW=V6b>Vi6(&B7(6^kXU<-=@F2!-5BO8$#xq`=Zl9a zi437J_GK4L$OEg7R)^_&y&~I%QdWl`LUpg0&Yl!A>laNyU^884b+YZwn1t7U1Z5xv z1|x%k=!X0gM(|c5^bh-3uotnTU^}q&UjoJtg@BcPdVS7kwzo zksDKzPjHtIfkz`E$MdFR6Aih^W}irpP!vuEg(!*KHJRS(nn;g}U`?MF8H6~TlHTTo zODp1HBtp_ulsTA|exkU(0E3HsA|!>))6!4yArPMw;u07}?N7>MwZL@3(Wpd3**G3~ z!9ym$EKjn1B3I!@Y;Z&M1;`+I;RDI2LQ$P2ww6+y=jQQaVN3+1c}0&uLT-7zgt)To8{^5%i2kp(X-H zMsSP`glHc{Hng(AXaweCX(1Ft15K@LaIFeQ1s@@xBq7zbvca`VjFGVri!oxNu9Xe0 zRRJzS5=2nq_=;9Gcx6|N6Co}drLb^SD;r!HSuVtg1POv(l;!vT_ijDBa`-)mZ#ekF zgSQ;K-2AW2;{MmbT;H|#JA19YyLLaZ8{GNw&Xt|>+n?TkZ0qY=AKH3&^NX9k%@=L_ z_(s(9pH1&HJ+%Ir_1wB??R(cAUH$i~Z&`iWLJ#K*liNIFIO#IsQh}ucO9hq+{9mKM zWA|>QFYVm3{elZ8C-SKHJ_0<3QbBOT^92Rrh588aa7zsVE~|oYw>|zMwS_$9=rvPl}0>I4F0H!y$lTBR!xU*3MfSZg`f(=~&xaevCaD7$) zOu7JY(NzGPaBWin)^!2kqN@SGwM_w7(*=Nwt_A?tHU(f+7rC7)f7||jd!O2qcE7g!{$0n;7j_yu2irfgO>O=8;l+brKPVk+Z(Z3szxnCS z$2Pvc@u7`}LB{20slZZ!r2-44z+-mZF_6Pp-|v}-d3M@!K4#NLFxL5dCSKPl2v&fg zJr;Es>-{}8IGuuE(Jup>bNU0!Gcngy5RP>b?C`@|4Pp8-__2#)m3B1d;dVHu8p8Bv zoCPkcf^bg13`P;)vMLB?brB{)h)%Cd zZZF*S6@)YT2*z66e4Iq5nyg=2r8a_g+79@ zUc)`{I#=<-%k>f9(yNrg1N%!sc$q!|+>+D~V1FqHFV#nY^;JWF{iPthL?6Len|6C( ze<=tTbP?>ZzSGO#vBLgR5FXM;fb~^Ffc>Q)JgAQV>#K$U`|A|psmN=$LHGUA?Wc zHqBo!f5codKWyII|Gllh+WOSiJGPnad$zc(`}cor|C#+a?>qK)_x^bAr}o~pm)i60 z9qj&pyPw*9?`~oDk=+-89Rt6x^XyJ#$G>y`_TO#){`OC7ziYv!2unIk1(pgd6<8{; zR6tDuhw0LW;jRCx)p!5Jdsa3s+f0{ChBGC<_&X~b-*yX*-iCh#=jmH;^fHZc-ui&) zjpOnhjd!}kgB->1liy;h-zdQKQv`Zfe2fvEn4Z@VyT>0eAsZSe0Z!??@Kq+nq>)Od zv3Az<2uP!HQb0~|<`hfyjKC?@-KSWpsbov*uQde$mi7q(IpK>QG6g~LZb>+CcHk8E z1ygKJB$aJ$Pf0j&z(7uDo&=vfT5t+}*QCwKq_WMODK;new4d5MoMLk#scds=(&pqL zg;SdclQt)l$~HGA6HcCFkQ2Uq65Kwqss9vwe~QhCq_WM8DK;lcerj`Xip`0nvQ5*2 zCK{@8Q%*z3DPKC7a>_XfIps^Hq?|kfA*Z}|TFR+qfAO@GQ%iKuWYH(jJILTKJ8$yK zMOQuPASZh4*d&5PhR!O;>0fO(y-DsCr=E#`SEx>XweoSR=}mGcX(*2jvhsskDDd1` zLHPkK6gbZq<@>Zy;Q6(J@-ZzGIL{d6`?XNuIktlGy;>-6o-xWtwNT(y1_k9KS}1Uy zG0OL7p-g{7u6$Sv1^r`3iQrzq&g_Y`_>ZQ@1Z$lK-j#&u1IE){!J>cKKl4l6Ko3c4NbO{-N8(aG{O66!SKM0SIeu&GvQiQi8CIUC|cLM_HtrB^$mB z9VER7^kgDqhun}6@{2JU5)3gQbr0dW$nP#RQl zx4&CEa^+h{wb=p}*V)_`E#QCF&8L%_Z@QdR)fXN|-6?^A2(t?8j8q3b(T2B^gm;ud z>y8MD^vA+*(pba{S?NnG;tddZNwo2!biWRq6c~uI0|{p@RSl0Fz~WLCIQ2-d404=} zN(Sv^uQ*OiRnxXgEmg%W8G)(<0;Q@u=ykb^oWR5#@o4QRRPNAKizU+|LZL7fPdJN= za}Z!`b$|Z4#FUQ2&sQD34T&A|k~l%c%Ji_|64^#E!dh$KiZp_sCNbh6Vr<-5j<`Zj zENMrxNwL?dy8K}$X6F*%_@f7$Kn;e-iamIf#6ca2;W76D)nfa+B#tNQgJ31^rny)x zU6Hb^kg0^HNgP6)WYwAJTDxA-(hF45w9j4|I2bCOu?EYc-O`hKL>C-ruX%2gIG`gj zJmFX%iEZ3-C zi8yg$vHJ$0ZYWgGdR%NUkV~Bei9j*a!cV$lt2ij*&3G>zzDZ(2M`HLT@B&F}nU}9Yc=f*5!P6uShu7Tu}W}SUmWa9;ixxN02lqaJXX3( zA+4^xhg}EG5}v_N1kptxHJ3kEaJ8#de=(IyFz!^xe11VYTR0EcdHB5Np zF=5(-9ckk!E}e_9?h;PZv9J&peMezeQ?!7{Ry9tlIM9A7;YwI3K3@-oViHOQMSsY! zPyXP|z$ZN&pG@qWULc=<>1d;!vV{vNqR>m*8CL|&_v!IK1zi0s&wj|5Gfl|TOJ6~> zg)3=0Jzf~0PiJ9ZTI(#0XHJ;I8Mk>0F*0N4YgCpV6)J+gJY^BZ$9T9zBiE~xy75fb zQ%~2~m@SZMM9hscUb=&Ln7YKWI7xAMfH3TnFTD`>q^swX=WQxAd*8L_ z7nkA;jwZwa9WmZczVP3GPda)&dEO=lx8aiozZ)saL@*+PfGdIxx07G{Jn%_d&nM5@ zOyYKYGOxbt&Ro7kQgMl7SSf1!KAF#c7w}0-&nM5@)Z=!1GOuU+ow?kPCldrN2~o*! zxSjmcp8%gU^?dTY%~fv0Cv%Jce`jtFw4u>y3p!{Z-(ThMrI0UF3^2 z?wZ@nt2=Ykc9Uus|;8$IPxy>2x%vIhbBMLYfjgiLN$-jD3 zp8xNxKeA%_`1&IU-(`Mi@8@^9?JsXVx%tA?FRbL}_$GVfN9Jg3X=u9H z+NHCX(<>|M=IMLEJXZKIbEEHEK5)3vNl838={v0TYOP1te0=VtluKKNViNO(JYOktX>r(HlNew?eZghzYCH1`KC3ogI0;Jx(F7=Dlq=sMT-&l!RB&ipC`3gwA zb6x5esz^N%yWz${bq5>w!+JPH`cNiNa{sj00dHRU6=X= zYEr{1M>m#37D?*)T@!ytF7O6YZ(f)BE;Xs)h0`0WfeR(|yso^%?#T7>KUK=&&|mGm-pYh@7epKy(@c1yT7yh`0lx#&w^-x_iq2(c6NJz>nFCPt<}wsZ$>u$ zcH_evsOc-F_nExwf4u&V^^0r2yY`l~SFL_-wYmC|m7ib9ozBy?KDGe!wIzk60!sy! z3M>^^DzH>wLIJ^aDXnrkX7Y*x_4r2<`3~Y8ZxR=P%YY3@-+17>0NC-=k%C z;vNDG!|=@OMVf|T8XAV-+4BoE4a2E548x=9yEP5NsWlA4Gou%18irHPVE8Ug!!uMj zIO(CL;TftMZ1_OaFuY|`LkgIKSl*UASVZ>j3vCo0@jvdZ}&~R%}Dd?nKys zb-S=)CQZArm(}gUimhwfg}tn97glVI2kl~K`cjB{BDTM}UAVo;^Z%D`et6~Z2M@ys z|NY=Y2Y&NknBQym?ElgJmHi`d1K{I(=XO84+t|H#=jV2^JNqDxpR~QY_3^F9=HG&B z%g<7Qr2_}Qyr7*1-{GCVQi zp24t1)3EVz(8MJU$C`%WVW~RfiMtyvY8r;er85{l(liVYOf?L{qrBH?8iq%vGZ?<0 zX&4@wY8Zyy@sOrrxYlbJhTZX?reV0&YZ!*z@mejz6LGI+F#H-#!?3G048wkYwWeWM zI1R(FpC8aP4Aams4Ey=KreT;cTcDrp1989tfpb(wZaDX zuxB(48?O~Mn8W>=hK<(>8{Ajir)k)@^K5Y8Ua4hx;yMWp=fQo&D>Mzm!f6^^DzH>wQ5AS<=UX;c)0fUBUavm%pI8K){w{HHUA3C6rn0Seq1br*+>L+B zw~WbmqRF?BrR%j$w$=e(J^sWKPwhN>%H>-$xWLm8H7;j!ezW#dXTXt-%FeB~jx7PqAm^(r|9c z?da)Oxt>=tr#JUOO=;RSfbvy;w}1Vg6<#Pf_rw$O{Qq?;@0t zD&e(ymf_Z<#u;9%5?-Tc8P-qDGTi?Quhz2+>!)IQ;*EgtfSzSoKQ+s+KZNsomSO$W zEW@e^uhO#&>!)TJE}L);Sk`QV@Y7JuGF&#{tR82hmf^AqXLKx2EaFe+Jo#QgxL?n* zvFFp|UDU#TdY0kZq{exoZoE>@va#0f_D<{53dX{1R)GWjP5bo8pY^)7V-X$%(SjY0j$W@Iq><{4{ zJPK8u04r0 z9m8-D)eOV6C$Xwy*kH?>I4LlG|8M^GmBWu6h7SJv-~$J5F#ieo&*f*Sz*2#w0!sy! z3M>^^DzH>wslZZ!r2pV?E9WEl;dv3m0`P-}asg;Yde~iBodZ7ZyC$TNf5=-rfE8uH3tU zg7|-G(f$sZY4QFJ{!TsO->p#u^^D)3EHKrp2*t*Y-=nmFS( zy>Fh}9RQB(X&b(^Gq+gO48tq?;K-h~VT11&CiVt^BYWD0VJ2#f;b8+fvZrnM)-jK& z6cfh=!I3>}!|;$th4IA30B~ea+pxiR3?A4QFV!;)Yp2E-_Qgx|3>zC4P3{T+@&7cX zm>5W@F@}8s;{RzIhTqSq8HRlU;{RzIHrVnewgiCqf7*s&Z>un#*bxBY|7jbBy{%># zE*Xgbr)}6^%bVB_0OJ46$1q$n5dTlxFkFq)q=3_b_Vr&!?QP=y|7rZcA6Pm3_+jMWZx23vfSSKzexKR9|Hu39*uS{3tRetmK4 zRK?~6aR4<(v4#!9GX)R_P}{JvVZh{T5fBGZ+pw{5z~oyI5C?EBh9~Co$2tR|+uhj< z0(xr>v*0c+kPsQ%Uo?eqq1-|WfZy?^JX3MzkS8pKR2Jt4f4Z~fFnqgQn5N}f3Fsz-LVYp-<-lVo+gHKTlTrv=E zaz2LPlAYD*HDMytm2f-Zr{cOi|2MA`SGGUDS=jpS4X0^s?Kf9Hv{F3y4f6;09Q*m* zRjq;948J{XHC;}xEzr%3!@1oJ?QaC)mI@cII+|=5>#$Y6m8&p8P}ZST3pXU2vydVA zp~Rv`jZP{lR&rGrRY~Dkq1Fiz13xP9IKwjKWF?VGN9?Xt%n~V5$-<*XJ7o(O zQbeJbwll5>n(xy{y-%Z0xRSQhV;SfKkiqZkrfklV#3J4R z(YAACuMp)5LI`Wr5C`Iuh_%q` zRNREk*RQYNwSbb%>sDLR=gbwYiCEFvq-#`#=NtB{5T7PlF*Qp2iY3%OszjnEqSck< z?PTkSO*Gti$X$!IL#d1_-gTrh`TF|d0!qfssg_|rB4G9QlGvcl*#fRmyxz|8;uOjJ zDZ)xNJvk;{ECaD34JOwYoI-z4NXO__I2Vu9vGP)mpcfZXoq_ z^8!jXuUbanOq`?JV!Y5~s?w0hI?Y(wF-0=p2oGe^YzfccCxYlAkebV%E4bR#s=t`Z zB^YY&9r|YC5xSEz|ygX32!_m zOp^?0<0&qki?Qw!PSdfl5Egw$VOLYM2w|eeNfig$PbFLlE5(DpZ0DOI*(gKsqm17& z2yuZv&6Nl8SVkAo+sGgzAT#Ps_x4#ATG8<0w| zo)`1GeVKa79i)4sZX;C7Igcpu$meg47!FAGl5dJ+VC>}R&?RIL;V>W(m7}@*AUs0) zAud0n=hZO%bTw3pS%6zwQq|Fs$^(kEOsi%7I!EwL$;q;%B-&_DLOqS46<+WUx^ku$If6zk0 z!Dy8ZwhG>|bVAg`O25;=ij4@#yQ;M;%lZADY%l`)vKN0-B-5^Cb9-X)wTw_6rb`_z zE6~4Vpr|I+;9=Dzv$=GFaQ+5e9H=>9!>U)uYgz0BT& zyZ?FjCw2$BkL<4P{OZm-cbJ_QZ~wveN4B%u4{iO`)^mryeE98$k;4}q{M&=8Tf?n4 zY^`s8X7lpq8#hfGp9Q9upQQpz1(pgd70^|{bA>alT{^qodNkSYWZRu1>ygc3^;#_! z+ldt%-m<_8(-9G+aEj;1i02AB&f1*5^y{(1OT;A3#iAk+2*y2EV&klhYp50-R3Xkp z7%UiIFy9r%wAMVkUVrpNVQjIcTiIkMd!%QCrN~5-MxvyIT#1_2>Sxz$*YSOX}rIK~x9lXS{I7xAMfbd*F$61pq zStm|QhC(q3C4-_Lp!&yIA5+OX5eOP*a5NzX=m^N_8)rRH%L>2wA*uhLy*B}O9lOdz z&vfrS^QlT8^HAsJn5>3dO9Dh+hAe5%hssP1m$Nyr*;ETynsMFgv9~FuNy&&GoU`V`!h(cfO44>0-p80 z5wtJ^I<-}pWdtl!$QDz;{bM6&eiqd2{;5Pd6VEU(Ohm6oM^J7S)a^}i5-^lX^GqRn zJu-r_v!HHo!l*c3NFi_@&=ej)bF-jsZ^|+}RwyJeFr&K;w$(td&dh>Nc~c5a7UOKO zh(@mmM^Jhe)a?=klMx^av>)(72#lcFS&1saa6BH?akVD9~6Q1~k291jT1T-QEPH znRJrDhyotHe(wm1&4RkU2}7842E4LxK+~&7(Bv$r+nWk0f<>VW76%>7t42_C7S!!c zd=AaAprK=crdN)j$PDPznY?5=PUqt3Gzxl&^a!e)4~NZY>NablholfL??L>E!P44XV#j_d1qBi-nzyCd8 zleLR%;vQh#Q8u}=7ckGJVhh{kZ@&8WyT1L=3v4nqal39+y<*zvg=r+`FdW(~ zAyF(1nFIq5;;C$1)4XV;Yn&|}ENqi6|0b}>!x!1a9SnLW+2jt7Ry!N3E^L#(cptFI zTQ9PSdt`e@+2nTZR6jG}aA%W%g>CYMDzM2z7um!;Ww@hka=XS|&rH1C+3adzoBZ+5 z0-FpjvWa^xb|=~74iD%%o3<`&lh5^lP5Kwv#645Jqik}!`mSdt{OxQ~zpza{^R8k1 z|7)(CtZk>Z@4oWj(XSnS&(Q-%Cx>4={Heo75AnlqKKSgxj~tjFCg9Tkr}n>Z|1JBk z-uufd-+Se^ubk|Ca_{jyWiPt7z5DySAK2~hrgmSr^XEGs+xf1Y(vEj$ef!hf|6%*> zbCwWRKdl&8F|cA_#lVVzJDP#$)`Q0zr?Cl(M`Ub1Zvt!@O?N;BUZCUIcrnl5_|7@vQ@X=Ek;`EyL&8_4 z#Y0hd=|YZ&afIM=xy#eyftXtyqp3KZ5;!>jtaHStbWd?9nx$cw#V(yAKBc<=74tB~ zv-$kiwJpMBCVyZ`9!jN9EM3fE&|TB=zVMVh%7ZY7JWoT(qjTi_A@|erC{Ixsk;>AC z=gCh!ZXBmkK3~WPuzMoJ=$H%fed^ z%tX1X6Cjq$bIBsX_vXReOO7cvg<&`b$Z+Pt+^b?Nlw$=1BlDT=JeYgohRGBel*J1I zYtMtZ*A`O6OdQV^i)7rI2Xl9@91g*0gvchL&ODgAdyS{yc!q>HgfRnK{EKIp&ApyJ z=@k9P%VT_3=eUQv9wbh)X*8Rn;Ctpg)Kpjig(5jTL&Q^9YaYy9TM~)qh$4s_!rSv; zZm%M<#dxv^V@aVo59anNp23(*4x#yiF%RbUDx9UTWHK$Vd2Jrd9ooTj$qbv#rVvt} z2Xi}Pj?K_~5+ib4W4QmnxAwZVy zybXdsAA5LhZS$UgNC4pOv>DSQ_0f@buUw34!*clo zSr>}rAW;YSI$9SCbgit%Y~DXQ4&R`VN;2(NIGWQ!%)kn8x)~nH>cT)QRBXP;_#~aH zc@koi7;wcrQEGOI@s1BRG*#gmN~PYx`u=LMDHR=m8uEwn^xNKg{hPKyy>T!L#M6yXH_!3Ds(#*4 zz<9HY+U0JHtYsQy5sVRAyoU?~r~oVt8jWV6ovrq5KR6ZF6lvpawd>E`29bS_Nr3;C zod0f&2Hbu6*`WCuqJ6-RebY_*Kjp(+fa+)6p!&tLsCK^x&!!o76xFwDulSic6b7ii z`v%o7nnCqcfW+CZ{~bj2ogOLys6Mb%Y?H(ze?Q`8pRNvv7gMjM&8&p4M7S-<8;#oNB9Yyu+^6F~;S&e_H#Fx%kFps%sV$T@K=Pg_&!NSzc98e7( zrS}}(kD6jKHmJZxkbH_F?Y3^`?;94%zh zn2_WAtrn*#L8cD!U}~V% zHfWrS_{>rzXqgEl7N)vN%}Sd}D`omx?E>Gcdcs~xZrS0RC?+qU*gf;QgDCFZE{X+C zz{@O3P%K|(n!Hy`1ijL^UQGGhrs?%XGe*OJ@_qk+$DCp^K{cpQ$Yee2z{mA{iHg@o zZ=yK!wl`dU!(*|R?#zltc!oIbpS!e+kzKh^F?q24aJ%>u3#Lv5QasB_VK#vK(;CX~ zqaMT63&0+Ksl-LlPDKo|WITpcL&i)_U6m{V-L>k zXI=3Z&k&gdYWZ&VcI^iZc&-8>AzEx&Pvrb(7+0*pe{1_gm$eK1gko*1@U zmQ5NaNbD%*;(=(3@+4UU=w+sFbh1}2=mFe2@6Qy$U{h?E_j!qph(_pkd2qQ;3?w9F zNtn|Ox4gDCdjE{cmFhj+E16xc$(#|%I!Zw!io z^pMk|F(Ofn2sNwM*WrA!W`LxnzFv>0hQ&x6^#Afn|gq1wi=+)WhI!}Y58ieVKiJs+iTiAX6&L8eP0V4j7xBqDS{g=PK-Mj5GpH+b?238EL7+5i|V&Jwh@Wx9I z9$#8`G3_kP;xN-nTHM?`#h({OJ1@3>oT+7VV?N$JLq7`{0(g*BWOIE!es&zp5dSw{ z-CUcCpA+0M#DDGM8-G6^?~cJdOFuBgfB82z{?2ph4ab*e&YsU=`GNe0TVHwoCI0x* ztVKEZWbsDu@euoHRIg2+QqF?j=JtSXPGcCHLje4#Pu`c9Tl<_uxWn52;Mp60Jsw-#w97^fmWf zH7$@YTQ3=%iri6~HSm2P8V)@C&{fg4l#=YoP0IvdK5+FU8;XNUGNOP6Qz}m-Ywf0D zaj;#nEs}{8%XYG^nFU#lwUDcz;FAPc`K$_U+v$7jxyZmS=fc5Y-N3QBCd4M5 z3VQrL)K}?i;by5=@5SV7F4YKdSVc}(1elfm9xme#)#4tTrbh)>{gwGjZEzefkf}YpwI91hkXF9h^8VBec+*?4*pPhSW*OS)KdAP*eQ^9t{mqC z(L%^tIuvB<#ppnb$*iOfbV{@2;7Dattu;lBpmNvW9n$G#bB8PboJpOg?4kO5#V!JwOEtSdGv=L-n?!-(Hu6!ciQVGOL*7C zj6UgE1Zq8V?amhq zhKD`#x$O=dsx01HR^@OdI*2xNNm^;=0{I556v!BplA7UEEY>ZWy@cO<+U>4_)^a&P zuBG$2bfhdNm;MoAmHgRBg;*B1^NC>-#m;A*}d)VyJ@ znd{$cO6383EfMc7)ZGjpefXMpp3p|G`TqLJ5^e7GbvNFBXm>Mwi%yM)m+7z0c@68b zUIqhMQ1Un66+EuoRMZw|8&`)fcy6ziD@bJeDJBP13`1--e4(bPcAm`sZ_OboD_dd1vul9nwU)%lgZhQB>ov-cu{LX{h zf4BW{09<`m46GPfF|cA_#lVVz6$4M7fzT!Tz&(Lyn82aUAylReQk+H zzp&)n|0&*#H@=JF*fUlJu&OTuv^W#@vRfHE{Z?btQ+5XYF5q8@|k4O!kcNBtuuqx z#b(|4;Hg;`1kW>TIN;v--T4m;XfMddF}Gatr4mF^WU1uw-Mp6@^an$sogY2*lm&}C z^OT(*IrWqUq0^qS^Py8uS+Iz)r|kUjsf|R{f-8+AEOi|wlgc>@*yz&FoZ4tX@H`s@ z0`3m`(nn7TU9k8wLNER7DWMBOrwP6EbEkwZSj3pnOFw-|C<{>x+#!2-Ij1G#eP{tf zcV54-_L;R8uKf^rYhV8G(Gy2+K6>fl&mVr<;r$2y?cm=W#P?EzzK; z`~ZcuOz-@rJtpV!UF(lrki~GGB3nN;@i&2LF?EMi8U;QTM^l1KonPxbe~SceZo$LM z#(U}AU%vdoc?zt4TQRU=V8y_SffWPaUyUqh6h1shcU@ z`uN(~{=b@d2~kwKMw=`;a!-cg93`;l*FW}>9Y08n{2LQ!x~C*1wu}^Xs|NBySBx1$ z_wmN@`l|r+Cr+U_#;1uv1I%wyUA5os4(8Q*Q#`+lo9%Y#dx4I>H_p4AAc3KIwtD_05Z=PSkJQ{+bo4fF6)E~Pd zfGYA69Z{UJok5FG41}?pd7g*Pui|DMH)+rV{^Z2UNTFe%veZzD8riSgI1)dKS`1RufnZPZ9EVmj$+P16UjKcW?m0L!EU=sNL$y~C}(s~AldV(nx z(itLGn6V{0J(s-lEHIY*D2O6>1yG~wG^^=&uES|nQs)g>oiR@uk0muQ!}*VsVQe?v z;Nno78Z@bXual9K8OWO-U^@Yp8NO#SRC9!6y@(WPBOw}GvDrw7=QA*Gs1}O2S2DK$ z5KIT2np9z1DU+n_vWyoUiA^V4Gp17WstEXR9nE{am);GkU?wzJB19IO$+*z8ppMMg zof-3{n`zj64Or0mt#29b@UI`6*+M=EV$FkWT+hjhQ2K!{Ylj%we zGmxf2^Ga7Mr;6<~jht`i3G_iQi)Sa~HkC4mOL$qaiw&k-mEjpKatpcNv9@;blM`li z!tAv~T$B2ZtW7oH{tPFb=K%hgdpfZ72$%-^J7^divVPp?v9OJcW|nLfEtN6V>iLg1 z-4JcyM$ex-X`@j?kUW>_H)}TAhKvGyeknIU=^D5-{L=oUfO^$b@<|fzHJo;~hxQ6H zCWW^Y@Dsqoe>R~?ty8L9aqIz=A@v%WuFhB(n@3eB;&!6#uYoG=n&_y=H7*Ma2?WvO zibmcjk=BvAZWYQqy0#oT){Wj*cN=L@RT!K>=R4Mog>e7P zD?Q_daNtar-!h>>>|&6Tv5|)8ut_K(aWmrI##C&8{_^e@#)gq#>&IfDED2;jE@~h# za{@6dGu9Z+9%wL(|9{1{w088VqknT$KZ+h*KK$LoA2@6uzVYzj;13Tzc+fpS4xYXL z+5I2ee|SH=|NOoGZ|@^}Z{MT$Ub_3m-Jje2PLKicxA3@AiM+{^a(1 zKyjhXS3Rm&_> zh*bPZDmBP74&O9J6i70e>obhMU(DG)ED0{s)vKtPqv>+X)2ZnGL_!b6;x%7m>&M0j zuGY4cBu@v-SgO~JClW*?PIo$fQSs<$xZ|Np5u&DHq)zO9#~6`oX?~9KV?9qPGO$$I zsD>oeCg5@qkBdmel7d*R5>HxGcmHsLK%j21Qh*&TnyFS09^$LDRM6V8LzKrIbo^f(Ly9{$2)k2Ui;)2K|9G- zj1A>OnMk?hfjbgyw(LSO*^G;&l2Pel4is*4{y2+oy?=s`jZCsQ z3nzNfy4<5mOqVKm#6T8i=*{97kyXl831n-rDkhU+6gFSwXNh8`qAyW2}S+ZYlhQ(+`P_x~wpBp36r~^fLsM5xvbhxCJ(X35JstL+V zqg1Gnf%+xIDGVfqa$OZ-g+$#`WD1!Y66mBcrIjZ+#fI(f z+J`2Hd_#isT@TghBDzuO#6T=bzo!PoI_1fvYD6Vx5sF7qICJMYV+3w>3!M_-XXyr0 z&2@Z80@e8dNOP1T2)=_x#a5e3XCR|d+f^rsy3@3)-bnviwOtP6TSBuKruYavD5IF4 zD(jKjK&$w50^xVQIzePTX@d2Yo0WK_o=aCVSez|&B!bX*qmUwCIH06ue=|G4xArE8 zOo$^BxfH>s@T$fYI=!A=$z~k%+`1Ib-jmV?Esb&UlYlhF0*We1{t63o9lP6G9MNf^7l7!-e>s`)VZ;$JMh1MXQ z$Td??D&}L-bOH)=YI(Ehn79?{^}9}23}vrzEwr}#+A$&lb8$T8WxIv-YQF?SE z2W5QR{tt{1(PZE2NvIKyqUwp1o(A;?#WKtc8u6?o=MXW~OZBs;iks^{J4S@`Cebs< zW?K%yP87xJKDehj!6Ine(LgrP$foRKB-9I3z3X3}AZj*Vx9AMVnIVY`t4HAeVNQo(v~fGt5)gf7MuqU|IsE{4W}X0;HG;qo9^_i5!q zckeS31eOStQHZ0pP+pWsjml!)Xu8u46MijMuhe{5Qp~3qz1-NkGDdissF5H^qGfdI zfutJm=3-%=9Ogak#vmhUR+4W(zJwt}>GjWz5ZOUi#!O8?oKW3@3v##s?o%(eyEU$k z+JSyn&!CL#3>=}?*!qZfQwAwzda)d%;US8V1*lg9=kWOmfyQBPk8dDGP8SO>FEteu zgt=74RX7%2sAkBBiA7LbFI322BxZ&}7++}!B{mS})L4-AsCd!4_1-bUuD9YXhe`yc z4p=v`(H=zCdbqGZM;MWD@r_wvWab+ntgUXN@F36O~k z+^(Scsf0((!ofT@z|=quyu1nmTEmeBw*^lSA-Mx(11&M+t5K{SYmf<$q(}^fC^O<| zV%|1wl+C2THM{$d4Cnve?QdN>`u(FHKYGW}TfhpyU59@HZUK1wuyu$X-UBiL{O5xY z9DM6R@xXhqz5kj0kM94AeP#dt{pam{Y43mE`*(Z2z3kpA_ttiQfA`0C-?96a-RSOJ zJAbnCOFNJ6v_Oo&J=0wh1FzVmH;xM^hRZpW z?%Q@PDaXMkpKIf2n^;gTv!GmhLAlgOF0XJuQ11U& zQ0{jYl>0AZxdKn5dWfUONw|_q*lBn0_Qs1BlzZWVa?f8-?zs!fee;5H&mPNhYPrMW zjp0UL$EcNQcU17+3l@}{JjaFQJ#WGCCeM3edCys}ya_KCmiJBL@+gIX)J!$qXDOkq zr;}Y*d%I*VDA!p~uDzgKYe6|arrMU>zDrgQe*22Tk7VYY}PjZctc+QJO}{!+?_p`dE_Ho zTe3`_sCB%iQy?fMYvZLpC3&FEASj!1x7tYSMo99og;Xg4HN~KDf|ctXU#Q&hhX|<@ z9#q>IusrTf(om*cvU3GlPW5rTt2LEI^q4MWQi7gp*9$^DPt_|t>gr4%`N-zd3Xm2X zt91r+A`(#fk`z(WH5#&T95(!9IZ}Z=CBEE}8s2oCTSS44r4^{>69%qDJaQ?|IxOo& zgnBilbqZ*{sq5`5rd>0`Yo6$eD;=!?kEnFpOxe72+3BU~Opy zZm$WRdUR<8Zm$WRdSs-)5}tjVP4LviODk}jP4L#G6}ZhNcxa@+5{2Gw6AYGC;5M6} zKT=?cwtaG&73hr=AOhZavWqndfrFqvW02tpr7e*YTkj|(Zr8D z)m>TvLC$z3FTwCmBx4{Qkwl0*9TtoqTq<~yfGq!_;jgqhI1VT)>Cb3?d zt04Vs*JDQ=3ovV&m9$gso>ZEtOkYf85pP7mWltwkt(k>Xo#z|nC@Ru^MQz8zjZ9c5 zMHf-PT3UhIYyxv>1#YtmIwJ*^@a)@dg7(r1+-4KBmR8_4o4^<;utcG^+XT&}6}ZhN z&_@a^(Y8-+vjW;kfmq+$BKk21b-D?;hh-eNGw?>eMWNjza6K<2{Z$am)6CdiZkZdk zF20-Kk*69i8~b*VL(}0f!l0?+DL&V?zXzU zCK&GjA8jLRM{hm+`k{UBe*#0%}^Xju=V8y_SffWNQ z238EL7`T;zZ`-&GR@k>UPQ2}4#WKkr2q)&fS|y)~#4|E&>bULib|QhMB=xjP)aNe~ z(Gyz$p@^B7<)JEA&7(J}a!e5$CMH#5qQG=Z6|JPJe5;iM%Nk`eSIn_ug(4KZ!?%b5 z=2~OF{Cj6QG;{S@wihx?+d#uP5iHh6RKw3WRg(LD|1lecXmXBR&kIVbrITW@-qfnON{QE{e3(l3l3;?~iHj`&cRt`5CCzwAj zdUQ=~VNN|BcC02}DkM?U=890;TjNTmH&+2ykC8o3@VHFndIQUXElF#33eAk+xOBMu zfioS{qPHx;cB1LUN=+q~%XzGnjFWi2Xg2B!Arq1Z>v|vvGEbVOAr1m5mdk2#gUnPJ zGKZnvh+aq*BALDu6!KcIA|j8D;Pr+SyxpyOOI2{Fqr-RMTA@}AW8MZh z!;x#~jZE1~Dp3nL23Hn?Yj}I{WU4AA@s^!%=YiPxzB3(^e!k(MO?nVZqj0vWGR+tR ziynfF;1x9{4?0NNAWVuih?9uuq^Yt(56Vck>}Z-mL7lYO%U6<$7%GN)yxtCJB;BKF z;`qYz${TOGNr!;H-iGsNOU?Aa`45Q6*5gDv)yGWBI~X|igi&WOm{#oQNhX+Yg zjC84}6vO&J@Z=H|FB36vQPlixvQ8^{5UfK=$Ht)6>~fl1?m2@@zT3y#0Yq0iXF61+ zMv;Q;WS(fGvO%<$k2aG&y(r73FOw$>pO|VO5zb7Mp_72FGd4T$^wppW2hpNJH<5Cq zMo4}%$Q1FiKIn(!el6)iYR4LsWF-*#O~i|4o=GMs+2xZPAG}!yk*^qeg3{_pvY2Y( zgs&>Hc_@aUDJ#usEJ#$KfU^&A1J`ONhgzzT21OTp7H%;rQ?TWr6HX9?ez?<?dUy6R}X*m z5IOkB0k;3&_Un6puxIZ6(XO+*2O{~mZ++db{D08x3)gmP+NRNTo7EEyxbvVb6R1^r{LMpkh7W(w!s?8(eJv9W1s?ms)5m_v#bGh$ zdbHR5=&mX4$r6E7t;gR4)ERm7ix*bs9FJZia7cOl0Z`@0qhGXel{b0x5&<&x#|2Q; z$fIAla8^b(;Uz_k3SX0=HmrW&8uDo zs(IeUd;1fIKKG``lO=*aT94;Jts}pE?!vX6gfnF%6I}xAUY!y--f1{1`3_pyay<8y9`r~Gs@$x5nh$vZRG)88v zG}$R4yXq|>(eVDtp`b>gvF4;-!Yb!c81XdMPcRmVhfs!$G6-E5oeYL!~n zj2g$>MK>7k?cCT~i#BlG#u7=JW-TAu^S2omYS^Sn*KNfKaw(~aclwHzDG5E}xYspsD$6GI6t1yFvC(s<>dv0g z0FsAEsSs0AJ90tvBK>OL&!mGS(WM3`5Aq8lW}t1ueGcbN7)h=OcooK#0!6A=O;=>y z4yBA9l)i8{sQPK5;Vo++EW@XpRV3Td@#6s&uayU!fSC%J6dXM6PPo1s zxJd_3j4C?4U@?=dXS^W<)59&jrG|+C>W7@rpzG0fi_a&V?ny@AGM%nR!DX9j!K^*- z#j~|k!b(exa$9Ovn<3O9u_{-~G>)@~M2V7<7UStwvEH{3_wm=u;WHhosE9L8upd*} zg*;DJ>9P~oFwPcOqFOLQV{SG9SxxiXP8|F<)9RizPfl=h9jsl?AEe z8gdX$r?UNqIcOZ44o^|Vf`C~xJemLl} zy%{Z>m;17kQyV;yGqS`KErTuE?j$qVOI4JS_Qs!vmsa1Nt_ z1meZ1fTJsHH|dGd7M9X{c&%QRbM_P?)kXGUNSOyhUK0-$f zAxEmsPD!-d^q|CAky4+G2Q$%=rUKy<3nwit8YJvov{-K7wScZtVWykvHu^}~U&~OU zT@-5zgo}ObOb4-K=>A5J;cYXWGFwTLZqb8mR_+y@Vj$N_cT$xy*x%P6=tPq8IIgl? z2V=3Q=#gk56zfRPz$avqNpA)7dniG`yb@=q-8c2XdIJhAx%3%1MfPr)MOCK-BoY?&T#(kzw-IDqu)7t-;r~K z9lhZ2&kuj$@Ous`hvDsK9qxi#0Dt1(`hjrp+Jg;n7vP8XAKlOGzhdui_I`Ws2lsm0 z_wPY_yH`HH`-R{&eRPJO6&?;T?V_v~&0N7r_aFAK8BQcJmn- zd#nmwF|cA_#lVVz6$2{<&Sk)}`QXO=;$nN$o58X4V%wgZft$;|Xt!|F|B_8;{eIh& zJ082$lHFPKHeR&(hV}c+R==i+raNl%xg)IJmkdK2mf#*$zHBqSe!rx2hCA`28e##D zOJ;9c#_yKdo|f^sWp<}z!a=vp&a}*wK3Aq>LQ!`)m#1Vvf^gS!J!@KKN}o&9GE@3& zooo`fa_A3C3Eed%r^``{d*sp|Hp9?s`=*LL?yHxuCDM6)#X zjyL$-_bbBTP2)x@@%qNQlNW|`Z2HcHUDy(?y?T>hzaLyYFdbYFb6NMr_ijEgOVlmf znj;%=mD`*n>%Q&Im?IlAm^#wP@7qebMYsc zoRa-+<88iXp0wM1oA=I>b{lZ>)$^p?Cft10JZZNPH(xnV+HIQ6SIm=k6S(>E;ru^v zdO@aD+X2!tQc4^@XRqVe&sJX!4J(DAWX$I zpDqWC%f4`06L`ty8;6r9&u9idH4Uo? z$&TE#Oz`CcSC`4*dKDDBOxD}04+21|BRjWLxWqZ8Q2|!JB?DEjPRWAi)yG+#nmD2Mn|ow!%D7dw~W=#ek%cMIC2NX4Ff4(2psbb z27Uf;C=>>8MKl$8Y%5a{{h{!%q@=G8`*V6<&-FA&q#~@EX>({sPj+DWTD_ydA%?KB zB3FpeSBEw;)ml@u?9QYhmm)(ykSqRB$QQd$EKwE67}n7XOTo0M#cLHwm2!nlS_k25 zhC!j#j5k@WWs}i-&zCEJD792Qr7#?Q&9dUH0ItT&Jlqn?^{N{7JF}w1A;?EDyrE6iPR=-{_44#kf2iuGhv0sqS;foG)>L{em_MeKX^ORbF|;7@ zxzr#R2kI=)oQ97Mn$vLpzrXV*Ye$b9{_Wx5;Ohs@{#W-qdtcgX?*9319sIERtQc4^ zuwr1vz>0wt11knr4E&R1;L-2jyfnO{VVN9+%OnrHIFa3>-@h@|V2PB3%j6VXM1%FQ z2FqN6yF{YFMKo9&Yp_JA%cK@uK!f)_I@aJDb_eUD?|o#fL9ZppJfhbd=X0&5Ud>c9 z5reElM8rv?A%`p5G+K!uAS--dS|-on#micczW3p=21{fwJXs>`;368lb*#Y>rQY6) zXdiv=Lt_n=$Z>dkcM%_b?_jLK5_^-kcVpqv_x8scEU`s+dly(AeQ$59!4mlhPnK8< zx_H%VIRD?=`^B}x_Z++${Acx9F|cA_#lVVz6$2{ZMEyq+unHa z_%R6#<6pLDk`Ic9Q)nvBViW;GJ>K7Qct7eD3o#o{j{Y{qWNc7@jUWX>-9#M%@iSyg zNapQIQq#pkzK?@EY%*f@@s@8r9(+)qyb~DaqBc zxwyjnNlgk6!@BxJVN{o0_J`_14(Nd89hu8F%P?s($vl_Ow?gSUHFR;F1Ec~ZP5o@ftfwq^jTpL>Jqg(ERBlx4Df z5>Ipg$W#M5f)-JSdK8NQYExB$t@X$O)XlK90a3qAR3`{MQ)j%uoEUVYK&vm!@R zG?KZ3h&%`m7jOF|cA_#lVVz6$Agg8SrjxJh-vhIUT57bPwJpI|j5DJsz-` z8v|OWHC!|l++^#%A+IU8lL=E<|R>TI#bN%N_W- zNsR&8EWlK9EOHEJ%m7U1yduVc`Ygay+N;bMpv(eHWxh&}0kv6xspMCwF`zmNFqQub z9|PoBfT<&}*cecm1(>o!atx4W0jAPsp<_UK1^}GQx-~|56Bz@<836y3rQtE)Ei(YV zDZ?ekfYL0$6s_?w;LWoDQ-*`afCpy*rtEP481SZ90Cx(y%{Pt#56l8g+2LEp0AUtj z%F=Hb1Na#L@08(QKL!+M0DM!1d)*jNm;vxkHK6;(fcz}Ll%->105=OTW$EY`z|I0p zSvoQXL2CY^Pab{O5qI>W z!#_Iwk;C?3^x*Fge)Ztp2kgNM_dmD)!Tr{LWbg0xKC$<%z1-dlc0arOfn8%ayz@VI zer4yKJIv1Ww?6}B{j2=`|GY0wt15cBI=$8EajT5n-FcV5Im-5KA zCxi=>k;pf2!?9SBRpa>zp04?Qq+o&@uf|C{P(9Rk2Sc13iuH zHDdmLkz_-0xRVsp#(>eFF>v>R3DBmXc3r^5MpwnXcD!6;y-2s<`{V8H%@^(CuE&~ieu5l0OIQy zjuw;Ia!F0M`D(~7mzj1kN)`eXJ_fE!r$8d(D;U{)Cf+yIv{WyUjb6)V3Khc|U>q`l zVwBaCU@p}i0}ubVQ(&;EIsJCN&`?0u86O`jlubSERmzD>n1Z582|@$@ioad2je%=_ zH34=+QsK&?uV4lTbS*&zG92P5re#THdRVt=7c|(%71GhJcMRP6+X=8mdHp0IRg+jN zD7K;o*EQq)!2s+S$1+Z}jVG7^4pSCWn5eq_?WaIrAs{pH1_ItT}nv{y^ zKr+_IbUmt3D0&!-nRw3O+in2!T0^Pe7T6`${Z?P>cB<8q-S=v8AsJM0qz<-e!TopX zi5)k8cLFr>aF$1+WT*fuy3Z4@RfUwd3vQze*wJQAhO~|%ktvTZ@#AN_^r{I^M?FHl z25a$XKb$WnDey=sVbGxxTq~Opy52xJK@1!tHXW>Y);q;UBnp`e0Aq_6nLM6GY)kO;RHLA%_ zmS7-OVAtFtOaP>q=`OPl#GVR7HW=>t!oH^25VuqM|z7C9x{aZ#N{@AZUU?d znXE%1l-H109L@KuX;nybajMNCWZ8z(UZIw^8i{y$0^F=kfU?3B0tKGQ2RKZ&+mIBm z^vZlI(yjHS7TBTgi#jDuB*MreggmL=xDRA!;2;p2+jhFaJ0dAr+ zWS8N(x9Rm@=>+d^VWpW5k*Wq;!3prn7bifmhp|ZkBYGJ)2)6~((+y^EyLRGq*-15 z(Fw5Rux(yY8u@%5h6Jc6AXz5tCwv31-jVrsEjqvh0VCT5cVmvS8eKV>00qtRhJ+}u7TaA;w4owE=fEq0|39*)<+EuI;i1dP!8ZR{_K;HCvyeKV2Jcg)Z)lMN1D)JPg z1rXXGecgDWZHF_Ngiw*jHSRrU0_2ih#a~m~rLYr8ODd^?d;gq_Kdjkw&hqHq9w8{@ ze2>kXvigG)AT1iG9kYeLEt{c6#BL?ar7#DVpa`tC1h46hG;46g4`mzU8h4+X0J8}t z+HUhye^8V{sSX}4S`~wq;&nesGI9|vczSFsQinPdU%mXq1W3w6LX_qf# zpw?HFq>+hMJT;Kc!p^HoROInV*RgrpJ(9_?YBcCZROS7dM7NA0-4q>fTLcsqd0y-% zD+u?L4H1JzS0OIPw{-Dh94g_wO48{7zN$a@s z=~4Xu(JR-EK6=yw|5<%j46GPfF|cA_#lVVz6$2{3QY11YxsC12DBa!w0wt z11kppQ5ayKeD2l;Eg!x7;-rSlB(-Fpe9oxYlLd-hB2y*%;Y*d=mGvQIvHRP6Br#V(O3l6~@7qhjw_pjgskW3|qJPDBDKUy>q9x<*45j>CqZ zEJrG^r^J_AQp20hbL^9MkBWWi#fcS9mPjheK6%%u*q1C&>=KzG*(Z-i#lCofVwXt& z$Ub>ED)vPS6uU%D$Km{cYyIPE`|sHP`%7W)$LjM>i-GIyC%VJ?P~LU-`ZCUcu@{|x zA4-BZ$C7*he|v8N=15hpk9SY+S({B(VHifXSvvbNAW&7QRHc$srLtBc5VBO3&f3|N zVbD>gMHCUQPVcCw7u=A)hzu(3qJnx+ala~8P+@xX^1rCyb>Vj^8DyyLNmY+?0Us;) zd`~-PdC&LOIp5j8Q{y7UOUdq|b?#o#Hm543!e+>~Ihn1niCVtheUs$*r@FWYverJT za*VI0=(Spl5oEaTt0~Q#&6u{i0+boqhK4<*(3CM$q*FGctv3Xbw5d@mmy5cLE1f#_ zS5r)y0!~8yZB)1wcHwo1jnq4qj5BV7&_LMGGFu=AS}IelH(>9?yjh=K&jis}!QDt0 z2+SRc)om6^AM#T1aFtGm@}61~vLk6RucmZW2d}2Q8C_{`jqWDv;PG`$e3V9~c{v*0 z26~b88{GzaxAh(^I!;gL9b=ng3vuu?n84F{N6DLOpM9Us6YO(g~}w8?NY>on!$-sUGC=ZwCAh-5(`cbn@#C@(rD;tX_N{eUV3`UgeGDZ48nD(+Kd%EVi~Ct;QVqih-j;xdZm1Y(OOR?4nslCWSF$-xFcZ_4tPB!3=7vbqxG@I~a@)%W zleuIk!=YxNQfc97V-U}vxWO8?hPBd~1F1_&T~5+Yl}Plnrqs1MjOq9p*Je?n&5a zrJe^b4Zl1M@b3lS9;hwXr|rM^Zz3I33N5(5$g5(5$g5(5$g5(5$g5(5$g5(5$g5(BS5 z2Hg7whpqz82=q?k39Tjw-%D`s8|daI?mUr*{H}lf5#+_S07mY;r_>e`fq1F_t9V6aZfY>y<^~0&)xT$TU*OFoHvo4rGhK9 zZpIt%KhVqgCeIyPuKwHZKltu9{QiXbUz{KL!71TiFTVegxo34ZHBMhG23M-xj5pwc zpqKGA^Us|)(>Qd^p|_v-^zRRR_0Z_~6Yu)yNm+dEo8jAT-hBK1;7X;N@dkVl^fER* z^4B+h_F?(`zf3%H$@OZY4Y_acA(aAl^O@dmsQ^fG>6?*2c0 z%rJ8Ohi*+jbnWM!FliLOd+Nqbe>n5L!K)5IAAjow!Ig41;|<(n?q#gsGkE6UZ?x}% z9>{-g&!H_J_{hT_dZ6>6zxllRuQ0zk;h8JB_x~;!PYkHPtzK47t1aqL)g!9gRAtp= zDuwbV$~%+|rA>LF;!(w&ifa{w;xzeh7m*VcOp5B#I4BF{N|NxEw>*_EJ<^RwSF%`rWRQ&h14sWu1rg`0YrfVs1xEE@wH? zYWX#m9X5^XBfS(Aq#dtBkf`1W-mh|j=kWB29d5gg(I(lD&8iD}U{9vnF6T_>@V9#@ z%Hfu_fmUE=QL8h$V!?ttQb4as2|A_@~37`Ma`EL3)k{@dXc6t2}2tf?GY@P^{H za>c|1^DXcQUJ&+E5tK0{I%pu}i9{UokM~mK6VVI_&D7|;&X`GinLx-2HGDIO)osT} zLwhWMJlwVLTBN6&Q-=cRMIt_^J;*+Fps5Ha&Pah0#iTX4)+`g1&ZfHj1b6a3IAR0!d5K+)TM`vAn;*hU%I^AzEn4@9m|C2l5fK zGn}!S?Vh5==E&D9F4E-f#H^4$kd+yb zwVg$8XIkWF5xLB z+_<~hu9pI()-+u$X;Gq$qeum13`17U5IeZF6D))ZRcG3fx2I>2im&O5>Y$j$U9K7- z7hc>EkB5DzHaTsrx+A?p1%vgJJ5wp3ITOM_x@1Pznu#XJ4njp;+DxXBMV*0sHDWFV z2EW%!;V&a~KN@AS$q*g_-ghe1a3Wx1(0Y`pcoL18>7{Vzm_)}wX-kO!5q5dXK4#h)hH+cdFyl34b(OlsnRYhn zCWIbu^-{QO;Yu4qBN~?-D{c$soOSn%#iqe&mz@l16Gg9kiYQHaT98|ISuX{KSxs6` z1q-GyLn6|ON0E{%qs4Z_le(}in#2q#!r`s!i}ljPwq6Pp4q3By);V4A<9GMoNknhax1w1`Ok*zCJ3&oVn~7pH+Jsy!ThUJ< znkp8K6b!2O9!^2*Kne*8PdeQp0`r>MP&h&~4JGhm9o9-(Lz$Yl7{n5Ap(^`sFNL*4 zFj;SQx)Onl-Uc2i=W7)vA2nBWSlFTMw2jm8NHesfSt*TqdMV7FNCjs~&H!R2J8&)v zvvFV5tv8?nW0ivIRvTW(X5+r;v}1g(m%JwB? zs=lOJR8>??)p^Q4D<4sQRC&GfN~J-mRQy`;ImMi!q`(!M<t{U zz3JRElVZF!Kf?wa6iz2&F`9R5aggZ7OVx27k?0f~7+oxdOD<1{7jNMHUc72Nm-E7r zd>{b_gDJNg<;T0V7cbXHhI4F{rtAnr=Q_DIKi-qQc&Y5Pzvywt{S_O7&rnQ?AMXRb zcuBNfDC84?#x!`2x)E&WdGQ9x^M~lcb5n^V8McS)?N%XKc0xIDQ8Qmk#PXR^wYJM* z(P+xO>?p!jF2-wVC)sKRZ8LR-7j0l)FFP=T50v9^D({Npp@Jt}<;BzYvcqx#w3wX1 zDp@vPYsQfbFWTU)UObz_7n>pBOrC0H*h8<- zMXSJ`WV#lpW~r3J8OUWyylBNPDw~dHvhWOrW%8v+r&KQSxf@vMWtT=89cMX?HVgHJ ztpxkyyzI2SXvugW7bYALn544)x{Kg*V(OtrrZeFnS#Bmv>7dPtBRO7fgP-Ze3we{x zT6+d>)LRjIBSZT5@y2`cJT*7v@la_h0Xy7eq+I03Q}>GJs#ajMSiwvACPJojc79Dx z^rAT{)J&F&#fgMF?I5O`2roNJFB;+T2h!P0!`F;a8AqrifC}mTToJpvDK*kV1E1lXb*dk2Tz|FXfHJoGeCyZ6D#n$XdWz^`#j;Cu}df>82}FCIVr~SxwWm%GyTz z*IwyDY06&*27rd0k?B-1N%CL|y=Z}Az-4z~wR9|)Y~rO1t$7Fr++$3@K0b5-q4MgXfS1WC($CwOqHzFT8%Z0?15gq zY6xXgK{VJ(HnL0PHk%7}2pn&;O9g%_8@!?i>kH6Wq3o#o5Eu&jDV*Q9clNUL##5PA zBAOH z*2njQy3g4N@?rndi#MHgAxXHIbH=h@1;A_P7jN+Oy?9yJ zk(eoBY#wj8isc-&hP^v`@mQFlOQ{l-YLRfQQ?2vs)WBDy`Tx-dBGSK03`h(}3`h(} z3`h*TWDIl%F2eJFM>pO}KmUKChkM-S|L*QM@|8OOxAwFZod0LKTKsFw|3BU3gcY3s zkM-gS&i_?C?p~Sm|GT?N2+#jLT_uj&{Qvr1cE@Y}|Ce5NFY)~Udp+(1=l^f+#uJ|Z zf3+7+aQ=U&tL<@{|8MKT3eNv8@4+6g`G2DaD>(oEWDoYAeE$FEuC{T(`Tupjc!Kl) zkM-gS&i}tG&HrDrk&Of-F(5G@F(5G@F(5H;GzPi@7is>_+aP+G_Wz~%|Jwb3Y5u>? z{=YQ;U%US=&Hw+A{eNlxzux};ai9Np{r?YsVnBJ)#KOqM;1B8d3TFVkl)C%*mr^(2 z%R|^zrO203&Bpa!O5K3-5ALbats8K?(T%?WA0X>LS-g<~Zs?^{I^-fE1!8786qycE z)5)-%H+JL7UuBC)w!%GfdPPi}Yf&y$qExK0#TrPqd&|rFQmWoy)^KkiulrJ}#^5kP zacbJlS}OJ!xR7P?6sF7?XS$I@YsMtFYe!YQor-tHOds=0sSdvl+;=Rxyd@*S(2b&b zYRW&YE7q~32F=q3bKSuBHBE*znOweth04`uveUS`ZDO-_Z6X{FR;o=j<_V`mtTSkZ zu(EH;>V?u<_HZw^){ETRt>DFHj^|jKjn=B*Pp#3c(HIQcuIk{W)Ejau9j@KooEY4? zfy#9J+MQv&cK4FEqm4F1-iyQKQj@Jj^K667uX%I$C2C3aDqG<`@NV&K5!42ardw6G zw9zD+0=F}{cBui?2k27l)|lN}3Ol>+n-^mR76cl((8K@qBE7rWy%4tUE93z1g%}CW zXk@{G@MtWbjd6uPGVfR;R*P}Q#n>%luROd$_MR$#q2S&0@mTTsta8~z?)ksL1OAU56z>1-L3F*|YO(@^bBox(IGwY z8eY^g;ZTVPdl`e>?Xorl27N(WYniZy-I%vR+d95&7LrIAVwThtxD!mn;>-=FI`<%Y zW2N2g=G%i?H_+yepSjWhqvnR|MY~nx?+xU28|(Mi>4pnzArAUckTciNqFK&nZxPn{ z_;kTN@M2h&)ojbMEr(e>^=(CCxLWgg zrc4b_fHi22xw2P7#Wvp5*|J5AH>77#+#jp^a086E+Ra+Hj0WAlreTW2EB1tyXy6HQ zs_bN^nkLJXtx45dZf8D`O{5`BGE7*qp2D_DxaCH`Ae*C5U{uSg4l4V|z`)=DcL-qM zghT58RsUZ7JM|OlU#kC8{R8#G>IcZ2zN7etXey@5_{Vw&}ftP?cs;^Zy z)Mel=FsZ&;&8WR9x@&&od~zYBOz_<;OY`Mms{^0&!n*6m%T%FqwIRwb+VSMBFoECvTI}!nO{cAa2YJSRA!QCWEab}%FdRZCOc84kd1aL z!7F_2Ln@xcfW&~rfW&~rfW*M@Vc>)(R(6BY-=ix{Ab-441@c=fB_Kbsk^}N%D`_C_ zSOGqrPq=+03gr7&UI*lDDQ}~r)T|5xdEp8m;)Ko1*8+LYatp}Qmg_*CygUP>dbt3kd^ro`$TAD$;BstWVDh=; ztAPCDau~>`mIFYpE_;Fe%`yq(W6K1Pk1pGR{P8jj5^Ou0vTTd-(~XZB@d8SF1dlEmNQ5~KUDS6i0$tP}UDN`3$Kp;P-?Iq5k$Pnjd?WSZBG5>^uy`(z?_N9`$bVZr z6UbW@fnMsjFM=pw_A1xCm;U`r1WM>(uQ<@V(Tn#Z5rYEW94b!UCt= z^a7_{Y5{yT1<;OUgA3rR$s-G#j-CbZ)zsty z_-bm`0{Cibe1X%{w!mq6=>n&zb-@OtY2gYWwF{R5xqX4t@xld8$1MvQAkSIY4&>Pj z7Xo?40;k=n3*dXHPg(%2NjQzK-JR=KqJ+EFM#i* z`t3ZZfvSV^Z6JR$-vII#^Hm^!HV?j+>c{h-9;$vYKMmyf<~fc2V;+1p)r0dfARm|q zZA$gUd7zQ%-g%&r>YjPfrd0pmJg^U{yXKuhes~@O@`LlhKB(R|4>VG(%ma;7OY>GB z7v@bs?w{8IdGkE@YN{LOF9uRGe?E}c&Tj#7_dNJss?PiwK+epc3S?paBp_4sqd;Cg zKLliGjw`u)4txcbW3C7!GRKwqvbhwHyXN9Rn&++t(l8eRQai_$cKaMx+KcB%ATOK) z?L~F|9B419^X9nHo-@al_SJK*1@g2xuC%Aj8G$@u4%9J~Vr~bJ<8v1QIXw3oPzRow z1GP)}r#ZmC^4Yo5f&Akf;9vQNIl#a2sX4&E@^^DGAP>!r0r|unD7o^N`$3ITKDr;& zDCN)gH-Y@ge!#!-q5YsPDZjrT)FtJ^`vLRHZ|zS2`L+F^E-Amd|8+oqVSfn7&+Z4c zNO{kGz`62c`#~*I-nria~* zKw>~*;ALb$I{*I)^8Y8D|Ci4H19zO#{r@9| z7}zS?Oy!(~k}XI5(c5M^Z6^}X zkl;#*-5w-ubS)g2X7r^L*{sv3yP96w3=pRP;x|Mi{{7N!b?XMMYG;?u0Z4j)^mWll zN8T^o$n{ii{=aE_WkAscLi$MzNDN2}NDN2}NDN2}yxa`DIk>WiyNP>~b#NnNL*YRa z?;>P^lOTtIExe<5A{wR5Knf1M6&t3Kvykmcq%z2X)4(nL&*qcm73rN21 zwc{C&ucOwpK4qVN@;@b6V1591j#s!Gy6I_tB1HzIKdjoFo=<5)L}) z%H--S8O*iq4ZYn^45p?vmFi3*0L@T_cra)##Arthc){ry{pLDZG?EoM?n*GQg-W>1 zImT}AI@&(WZ!<&Ut{ro#gKNic9$C4TYuRr*b#Nnduno6t&Z)w>E!*^RwCoM|@mRlQ zZ`3sKTK3urFjtFT244Z|6a+Y`sAgio^-Q-`KkpQS^EoBZVE>VZXfzlMM{9@`i-ihl zZT^(47-C^kn<^KKg)Gjx@toFDs@l?lI?)Ivlb!0Z-~TrhQ<)C!wKYo(&s1dEOxmZN zQ&UsbR->M>)J%=Ard2|`CJj_dM7%+r51TGoGNk}ka>sIXh;HgV?y!@wVx5+@!RV1h zNm$jLf#K!hR}DQh^oAkhrh}Uv-1Om1Hx51o zvizs}?Y)#5dF}WciFIALn6z5p94wJ2)r+-g5;$DivI~qN+GQ4aV|pF%@&r7Hl|b!{ za^9t)`E0ee<*Gf~xw;Enza@bCq!%J{|IW2J?%%p@nT`y6-e1(=PMC{Z%NE#tw)mnv zUWit$0*_??YtNp&my&&oud%$si+{Fz__gDc>vC^2S$FR_+tnxB0>Yhr!p*z)I0z7) zS~q`#LAQJDC&|Y;?P$IfOGXb5O1A8(Rq8BY+Tl2QR(jLMvcmghrQcXqSf8vk8_H_$ zkR4Q|z0CS->CEe7mMtdo^&}ewZJ=IE#y}%^5o_DG9F5KwSMYqckOi*Cv>HHj9OVk} zs-qObuudV40mIf0V^C$28< zhp~}H*XNsG)(_)2)vMPY9Yb$DK3qjEWHZqZb!Qiy?eyCubf`E#ts z`G9(%mpiftd%l5l9oFO}T;`RdWDZCY>OR?WhAjX*;7{#^{*~(+*#w&{_qyOUUF^~N z)T_Eh+SR8>7VFDVB(;?*Qh9xrETCIIR+hl%gQofr|Iim&n|&M4(H4 zsY`T;33SHmk0kXL;|r$p`EE1M#u{LR*BfA5V(gRn2wT2>Vnd(AM;N#D6YKjVK4Orx zeqvpp#7A@w>nGOsNqmI8TsQIFts3zC-+0FQZNv+n4l@YB8qv*&ZB<{^AX+3I$ZAAQNi0-rFS+pOiNTDgUuD>{2fY9! zzIuJ0O}tK_SgJ*jn5zEW7J?p+oBt20p#kte=_fHDF(5G@F(5G@F(5G@F(5G@F(5G@ zF(5G@F>o{n_Fn4Y)^yfi>N9DqE=dMG$=PpH<6^b0k>{X(@+pHPkYihiN`%ln0DE<1WCI2`fXexbTc z`-N&Q=@V+P?&=q6vGxl!Tl$5X%>6=*rhcIYW4}ldog_6apxHT^;@ zQ~g5CJNtzick~N2Z0{GU+tx2sdvU)|%|(4eP1Xzhg<3A?7ivDgU#RId{X&gf`-K|L z>ldou(kIlYKeu0~Zgam-?K%BIHD~t;HCSKWFVu2Yzfkj;{X$J=^b0kf-Y-;tTE9@; zsr^E=uj&)3x1Q23)N*paQ1eOsLQN<33pJk5FVryEFI2DY7phbB3)L$7g=!RiLUmSo zzfg;;U#NMaU#MxkU#NDhU#MoZPpH;9(l699+%MET)GySy=`1jUnpk%PrPmsi0}l*r z9k_ZxtpV@yU88&!yqb5R;tL8!{*?R%`DWPxnP=jOi8oA~Hh$MQKKAHXYix4#j?pVd zel$`Z86SS{ux04sp$vGo;OfEMgC}j8-=tB`56%o)2A%`ig8$fk#__Ap9T+(W9bf@*k#9~*EVw_!26&n#4EE3eQ9;}3f=5_pc_3Vp-*q6*FKGYR2XcsPuca)(1yz~SX^ zW@Vyq)bq!c9IHT~ty*58okYME2>A)8k1O=&?-5mKcArHEhwnbbX%8R>K~N5i!+C2? zB#uD<$715?V@Dv$hvBr-$>Ci4Wsx{~AsizQ2lWL5Hq1*nTpUhdLL`n(2*<#~p+g~G z*ao{Ggu|KoFOfJ}AsjsqCq%iifFGqj-O4LFOC*j)2uH`m@!|~XfDkIo)q`LCc9%$; zDIpwwy+&*Z>2pJVl;iDJ4}MrA&Q2j5e!WJ6ejLHP5zN8ieBx=5I6DMz%zPu}BO@V< z_Cik1i2dp<10r#@3*qpMm>qFqw2QC@{2a~=cZkHIxKaU?A6Nz)40FH@o4*~($>+slv zA+BCO{w-D{&K4mYzCExZc#wA4LRg5yxuqr&=UgEizCEzvq!XoWA)MrJGTTMsY!<@d z+XI&!XY4-2=7TvL=O0DloFjz8w+A*)h+;@T6{0zu9djaa&KAPq+XH793DQmo^>Y2s z<0pPvB+jdaaQODXLBkkBGXVz}_Q2`#QBFMK zp*Z{Y*gKyTiF29|4&NS-P?&Mh5NYe`Q@Khc&Z$B;e0$*WxP3l^^oP6p;G-gOUL}OX zw+93nq^SVo?~dOd+in(x17@?C<2Ix1laW$&&A^ty3kHVfhNd=sO+A0q6##7f7vs%w z_1NuWmyiB%v@|+4@}3d%@P7=a2X7v{U{iGPnW5*@nt{)As|W+3flXf<@-c^c6MJ|2#0UZd;td>LL3;)nVEz33q|5wD}=+Z z#<-gb*`bh~Z+qTLw8nVBbUzgZ;Cl|ndtv*~n&{V*JHdb#1i6CZp~B+ecI z924Jc;w~Q!MxPFf8{Qo3tct{G3E}X&&!FAs@e-~G+HF5bSR_tU0B3Et#z1z);SPm7 zoIY3V5sA|fz%lXb8w3TyHX9Yd0~`)i5s6b5z%lac0TzH55)27!UEa36Mh-~4pGcgF5Dwq&M8K-5pGE^1$J>)X zSQd#hBZR{rwnGF;I-HP~;q2R!&lpAGl!b8k{g21xbA+8Cy4y~l{HoaRQxd}A_dnh+ z;_(Dw55#?+C%^auk@^&caQODX4tfRBNjq%Z^2d{(ZivJw2;uPCX^kMiXnai4zc@k0tGbi8@L^bFd@ihCm^nXD~PB9viqim zaQF?}K{#QCqzRJSiFxwf|1Ak{4)yN;R>!r?bOEaFAHVC^x| z#mRg@q&{gO9KIoOBUlg(fz{z|MNR!yBu+{Qhu^@%sGkYD7+-h2_DT0CB5_zD9DW0* zTrSAz^VtY)z3EBpR*^VK0USNQ9@t%!pC)Vw!3|`eyzosTaS}o}{CW_fnF!=0VH?BY zOx8u>#D#G9^}rc+FoB52fphC7PpWo^#EA*vtf>bW5e(uHgy;_Z{{PEG;#?zy!>egtYCjXE5Cs|EigDZHaf5{ zdh7W6$EU}&W6zF#er$HkJ$CZwPe*SX&5mkDo*DVv$i5NR$VtOL8Ghezboi{H$A&&U zGy?#|{McE61uC^H6tF#g@rL+#KR3JR!@6QB+dx}IN&H` zzI5cnshAU{eX!GM=M3+`XFn?vM@0skBS7n zKD&=wNIv+V;${r;R}JkDQ~#wn5(EG846w5TyCIe}j|h3}H0g!{HiA2qu{tMmnQ~TO zm%?o28yN>l+o&Mqhq=bT`c9F%2eYFBM#juHGGO`87bJoh#O+=mykF$*+w6!C4!=`# zhC&qL3=@8Cba(K;^&*Ytun-P^wCW^jj|UCnz6ghN_a{W+3<=@zN8c#!3V9({faG?S z58eZb#MvZ-!yk2p?1UetQ5xks&x0R*i%6V7Asqhb8*)2n4Dz68xAVO710rz-1aQ{u z`iI>lf!&`Z2jW0WUvVdk*ehtS_uhZoXB988G_v)x9&DdF`0M458jv*oh z2i#s4#LZ(5{^&H3I0_*gz6J6JQJSD2!qvrjMCAI+tXv3(&zld&-5Bl-M7kSU-zkaI zM<#^Bw{!^X0jEF$Zb#Pwed~6SI1@rRe9M5iX^3(8DVpo?4nBB}NStvY9KNNq2LlAj z5cXhqW8ndD``GO4m;lb2bBN$Ti`$0zQKrk=m&Kg`XJ-ZW<;_<9dXqoob%$s?XyevR zR-31b2tzR44W?CiK@^S>n7Nb3 z)dT8zfg1pa_y1p7t0=oD+(CYZ}J|}^=0<_Q9J>YX-AtCa^3T$&|b^P%@$pj;j zaF}GcgLnt-{-#JAfo%b;jz7OdTvW(OyL}KhFF%lfMkJ2FHiuTnpI2ZI8HQ29<>BTp z2l8r>I0Ey3t&ZRDoKD7P_d;;@JobU8$Q9MS0`q^ZZjB*fP#E_G>|Sm~=s;BDd4|0L z^M9?5zw$(eaXR3(Lm0QMcEI{(k-Q1a|Fv3v`#~`b50OZ?J6}F9_JBwnf%(5y%imT* zJk9{+pxg+zVt3%|$3@}@%>T7ozTJVSPzVk|;CwE?x%Yp?%`e%#0`q^Zc1=5_&>-mn z7wWhb;(MQfMd~B4vaZ$g?GEFi!xZU-Q0^e>y`N2s#1U9o*J{_;1GmG0VYIJ%ujSs` zMV2!_Z@#Ux1j`5HCwGG4(Agv6n%=Bg2#4RLFeFCf9@G))o@{vT zhazzVoK{%Yj2Rii9}0UT;N&&XXZ4vcio_9c31C?}gz@{q`B&iQfHT3X2U8+(1g_9q z)^x=#HxZ=6Fk-iHIKQ|>B#ywo+HMPl z9d3&2B3HjF@(TT|z?E~$nl1$=z^(!fPSS8k7+1gXZjt&3Tm`qRG4TPG+Q+Mp@rQMLZL8(!(DyuzFVX|0#}VK8vfA2zF@h< zJ+DrMZG@cwo-a7f4mo3@7&Oi9>dnG?7@8eAjn=BwXmnAnMQdJjRD=o86pcC^GOHxzb-ecZYH z)!&Id*&yKO#7|NteKZb2#?om3qaxSzWgs+B6$<= zKVw-lFGmp6V|Ta+Pxlz=qi++5Bj6XtvSz;Qa(N>*9C!ifcAh`IS|pBu9~H}*d7p;{ z4o)bDc5?Hv)rY<<5=X$liDk`vEQp6NdxY^sy2kk{BInDq0{%@bTK>8T>BktHf^dpE zXR!L2zlzjHz`uz_%kKnmI?UK{59rxAocpd2i6h{5#G>V|n|M)Q$R2PJU4KWb_lP@5 z!_Er$1F>lM>m~%^bippKt$WdH^{$VL)JMQiheg9*XTT{q93oI#usgWStcQXc`o78cE#`UdWNx Date: Tue, 29 Oct 2019 10:24:02 +0000 Subject: [PATCH 0336/1623] Make room directory search case insensitive --- synapse/storage/data_stores/main/room.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/synapse/storage/data_stores/main/room.py b/synapse/storage/data_stores/main/room.py index 4428e5c55d..67bb1b6f60 100644 --- a/synapse/storage/data_stores/main/room.py +++ b/synapse/storage/data_stores/main/room.py @@ -201,13 +201,17 @@ class RoomWorkerStore(SQLBaseStore): where_clauses.append( """ ( - name LIKE ? - OR topic LIKE ? - OR canonical_alias LIKE ? + LOWER(name) LIKE ? + OR LOWER(topic) LIKE ? + OR LOWER(canonical_alias) LIKE ? ) """ ) - query_args += [search_term, search_term, search_term] + query_args += [ + search_term.lower(), + search_term.lower(), + search_term.lower(), + ] where_clause = "" if where_clauses: From b7a0ea686ff6d2fb854603c5ebaad32daf1720c5 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 29 Oct 2019 10:25:25 +0000 Subject: [PATCH 0337/1623] Newsfile --- changelog.d/6286.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6286.bugfix diff --git a/changelog.d/6286.bugfix b/changelog.d/6286.bugfix new file mode 100644 index 0000000000..a4bebec1c7 --- /dev/null +++ b/changelog.d/6286.bugfix @@ -0,0 +1 @@ +Fix bug where room directory search was case sensitive. From c28a30da840b9dc291569e8a8062623a2c99b28b Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 29 Oct 2019 10:37:50 +0000 Subject: [PATCH 0338/1623] Pin black version --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 3cd2c5e633..52d35c50a8 100644 --- a/tox.ini +++ b/tox.ini @@ -114,7 +114,7 @@ skip_install = True basepython = python3.6 deps = flake8 - black + black==19.3b0 commands = python -m black --check --diff . /bin/sh -c "flake8 synapse tests scripts scripts-dev scripts/hash_password scripts/register_new_matrix_user scripts/synapse_port_db synctl {env:PEP8SUFFIX:}" From 9d8dc85df2de80626d603770f122ae30a485161f Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 29 Oct 2019 10:50:42 +0000 Subject: [PATCH 0339/1623] Add comment as to why we're pinning black in tests --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 52d35c50a8..e3a53f340a 100644 --- a/tox.ini +++ b/tox.ini @@ -114,7 +114,7 @@ skip_install = True basepython = python3.6 deps = flake8 - black==19.3b0 + black==19.3b0 # We pin so that our tests don't start failing on new releases of black. commands = python -m black --check --diff . /bin/sh -c "flake8 synapse tests scripts scripts-dev scripts/hash_password scripts/register_new_matrix_user scripts/synapse_port_db synctl {env:PEP8SUFFIX:}" From e6c7e239ef70c851f21d39652ddedddfddf5f251 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 29 Oct 2019 11:48:24 +0000 Subject: [PATCH 0340/1623] Update docstring --- synapse/util/caches/descriptors.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/synapse/util/caches/descriptors.py b/synapse/util/caches/descriptors.py index 5a8da449b2..0e8da27f53 100644 --- a/synapse/util/caches/descriptors.py +++ b/synapse/util/caches/descriptors.py @@ -482,9 +482,8 @@ class CacheListDescriptor(_CacheDescriptorBase): Given a list of keys it looks in the cache to find any hits, then passes the list of missing keys to the wrapped function. - Once wrapped, the function returns either a Deferred which resolves to - the list of results, or (if all results were cached), just the list of - results. + Once wrapped, the function returns a Deferred which resolves to the list + of results. """ def __init__( From e577a4b2ad01233ed70b3ab0a9e52aab42e88231 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 29 Oct 2019 13:00:51 +0000 Subject: [PATCH 0341/1623] Port replication http server endpoints to async/await --- synapse/replication/http/_base.py | 6 +++--- synapse/replication/http/federation.py | 24 +++++++++--------------- synapse/replication/http/login.py | 7 ++----- synapse/replication/http/membership.py | 14 +++++--------- synapse/replication/http/register.py | 12 ++++-------- synapse/replication/http/send_event.py | 7 +++---- 6 files changed, 26 insertions(+), 44 deletions(-) diff --git a/synapse/replication/http/_base.py b/synapse/replication/http/_base.py index 03560c1f0e..9be37cd998 100644 --- a/synapse/replication/http/_base.py +++ b/synapse/replication/http/_base.py @@ -110,14 +110,14 @@ class ReplicationEndpoint(object): return {} @abc.abstractmethod - def _handle_request(self, request, **kwargs): + async def _handle_request(self, request, **kwargs): """Handle incoming request. This is called with the request object and PATH_ARGS. Returns: - Deferred[dict]: A JSON serialisable dict to be used as response - body of request. + tuple[int, dict]: HTTP status code and a JSON serialisable dict + to be used as response body of request. """ pass diff --git a/synapse/replication/http/federation.py b/synapse/replication/http/federation.py index 2f16955954..9af4e7e173 100644 --- a/synapse/replication/http/federation.py +++ b/synapse/replication/http/federation.py @@ -82,8 +82,7 @@ class ReplicationFederationSendEventsRestServlet(ReplicationEndpoint): return payload - @defer.inlineCallbacks - def _handle_request(self, request): + async def _handle_request(self, request): with Measure(self.clock, "repl_fed_send_events_parse"): content = parse_json_object_from_request(request) @@ -101,15 +100,13 @@ class ReplicationFederationSendEventsRestServlet(ReplicationEndpoint): EventType = event_type_from_format_version(format_ver) event = EventType(event_dict, internal_metadata, rejected_reason) - context = yield EventContext.deserialize( - self.store, event_payload["context"] - ) + context = EventContext.deserialize(self.store, event_payload["context"]) event_and_contexts.append((event, context)) logger.info("Got %d events from federation", len(event_and_contexts)) - yield self.federation_handler.persist_events_and_notify( + await self.federation_handler.persist_events_and_notify( event_and_contexts, backfilled ) @@ -144,8 +141,7 @@ class ReplicationFederationSendEduRestServlet(ReplicationEndpoint): def _serialize_payload(edu_type, origin, content): return {"origin": origin, "content": content} - @defer.inlineCallbacks - def _handle_request(self, request, edu_type): + async def _handle_request(self, request, edu_type): with Measure(self.clock, "repl_fed_send_edu_parse"): content = parse_json_object_from_request(request) @@ -154,7 +150,7 @@ class ReplicationFederationSendEduRestServlet(ReplicationEndpoint): logger.info("Got %r edu from %s", edu_type, origin) - result = yield self.registry.on_edu(edu_type, origin, edu_content) + result = await self.registry.on_edu(edu_type, origin, edu_content) return 200, result @@ -193,8 +189,7 @@ class ReplicationGetQueryRestServlet(ReplicationEndpoint): """ return {"args": args} - @defer.inlineCallbacks - def _handle_request(self, request, query_type): + async def _handle_request(self, request, query_type): with Measure(self.clock, "repl_fed_query_parse"): content = parse_json_object_from_request(request) @@ -202,7 +197,7 @@ class ReplicationGetQueryRestServlet(ReplicationEndpoint): logger.info("Got %r query", query_type) - result = yield self.registry.on_query(query_type, args) + result = await self.registry.on_query(query_type, args) return 200, result @@ -234,9 +229,8 @@ class ReplicationCleanRoomRestServlet(ReplicationEndpoint): """ return {} - @defer.inlineCallbacks - def _handle_request(self, request, room_id): - yield self.store.clean_room_for_join(room_id) + async def _handle_request(self, request, room_id): + await self.store.clean_room_for_join(room_id) return 200, {} diff --git a/synapse/replication/http/login.py b/synapse/replication/http/login.py index 786f5232b2..798b9d3af5 100644 --- a/synapse/replication/http/login.py +++ b/synapse/replication/http/login.py @@ -15,8 +15,6 @@ import logging -from twisted.internet import defer - from synapse.http.servlet import parse_json_object_from_request from synapse.replication.http._base import ReplicationEndpoint @@ -52,15 +50,14 @@ class RegisterDeviceReplicationServlet(ReplicationEndpoint): "is_guest": is_guest, } - @defer.inlineCallbacks - def _handle_request(self, request, user_id): + async def _handle_request(self, request, user_id): content = parse_json_object_from_request(request) device_id = content["device_id"] initial_display_name = content["initial_display_name"] is_guest = content["is_guest"] - device_id, access_token = yield self.registration_handler.register_device( + device_id, access_token = await self.registration_handler.register_device( user_id, device_id, initial_display_name, is_guest ) diff --git a/synapse/replication/http/membership.py b/synapse/replication/http/membership.py index b9ce3477ad..b5f5f13a62 100644 --- a/synapse/replication/http/membership.py +++ b/synapse/replication/http/membership.py @@ -15,8 +15,6 @@ import logging -from twisted.internet import defer - from synapse.http.servlet import parse_json_object_from_request from synapse.replication.http._base import ReplicationEndpoint from synapse.types import Requester, UserID @@ -65,8 +63,7 @@ class ReplicationRemoteJoinRestServlet(ReplicationEndpoint): "content": content, } - @defer.inlineCallbacks - def _handle_request(self, request, room_id, user_id): + async def _handle_request(self, request, room_id, user_id): content = parse_json_object_from_request(request) remote_room_hosts = content["remote_room_hosts"] @@ -79,7 +76,7 @@ class ReplicationRemoteJoinRestServlet(ReplicationEndpoint): logger.info("remote_join: %s into room: %s", user_id, room_id) - yield self.federation_handler.do_invite_join( + await self.federation_handler.do_invite_join( remote_room_hosts, room_id, user_id, event_content ) @@ -123,8 +120,7 @@ class ReplicationRemoteRejectInviteRestServlet(ReplicationEndpoint): "remote_room_hosts": remote_room_hosts, } - @defer.inlineCallbacks - def _handle_request(self, request, room_id, user_id): + async def _handle_request(self, request, room_id, user_id): content = parse_json_object_from_request(request) remote_room_hosts = content["remote_room_hosts"] @@ -137,7 +133,7 @@ class ReplicationRemoteRejectInviteRestServlet(ReplicationEndpoint): logger.info("remote_reject_invite: %s out of room: %s", user_id, room_id) try: - event = yield self.federation_handler.do_remotely_reject_invite( + event = await self.federation_handler.do_remotely_reject_invite( remote_room_hosts, room_id, user_id ) ret = event.get_pdu_json() @@ -150,7 +146,7 @@ class ReplicationRemoteRejectInviteRestServlet(ReplicationEndpoint): # logger.warn("Failed to reject invite: %s", e) - yield self.store.locally_reject_invite(user_id, room_id) + await self.store.locally_reject_invite(user_id, room_id) ret = {} return 200, ret diff --git a/synapse/replication/http/register.py b/synapse/replication/http/register.py index 38260256cf..915cfb9430 100644 --- a/synapse/replication/http/register.py +++ b/synapse/replication/http/register.py @@ -15,8 +15,6 @@ import logging -from twisted.internet import defer - from synapse.http.servlet import parse_json_object_from_request from synapse.replication.http._base import ReplicationEndpoint @@ -74,11 +72,10 @@ class ReplicationRegisterServlet(ReplicationEndpoint): "address": address, } - @defer.inlineCallbacks - def _handle_request(self, request, user_id): + async def _handle_request(self, request, user_id): content = parse_json_object_from_request(request) - yield self.registration_handler.register_with_store( + await self.registration_handler.register_with_store( user_id=user_id, password_hash=content["password_hash"], was_guest=content["was_guest"], @@ -117,14 +114,13 @@ class ReplicationPostRegisterActionsServlet(ReplicationEndpoint): """ return {"auth_result": auth_result, "access_token": access_token} - @defer.inlineCallbacks - def _handle_request(self, request, user_id): + async def _handle_request(self, request, user_id): content = parse_json_object_from_request(request) auth_result = content["auth_result"] access_token = content["access_token"] - yield self.registration_handler.post_registration_actions( + await self.registration_handler.post_registration_actions( user_id=user_id, auth_result=auth_result, access_token=access_token ) diff --git a/synapse/replication/http/send_event.py b/synapse/replication/http/send_event.py index adb9b2f7f4..9bafd60b14 100644 --- a/synapse/replication/http/send_event.py +++ b/synapse/replication/http/send_event.py @@ -87,8 +87,7 @@ class ReplicationSendEventRestServlet(ReplicationEndpoint): return payload - @defer.inlineCallbacks - def _handle_request(self, request, event_id): + async def _handle_request(self, request, event_id): with Measure(self.clock, "repl_send_event_parse"): content = parse_json_object_from_request(request) @@ -101,7 +100,7 @@ class ReplicationSendEventRestServlet(ReplicationEndpoint): event = EventType(event_dict, internal_metadata, rejected_reason) requester = Requester.deserialize(self.store, content["requester"]) - context = yield EventContext.deserialize(self.store, content["context"]) + context = EventContext.deserialize(self.store, content["context"]) ratelimit = content["ratelimit"] extra_users = [UserID.from_string(u) for u in content["extra_users"]] @@ -113,7 +112,7 @@ class ReplicationSendEventRestServlet(ReplicationEndpoint): "Got event to send with ID: %s into room: %s", event.event_id, event.room_id ) - yield self.event_creation_handler.persist_and_notify_client_event( + await self.event_creation_handler.persist_and_notify_client_event( requester, event, context, ratelimit=ratelimit, extra_users=extra_users ) From 1a7ed371490e8916ca6ce31949e3814360feacf2 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 29 Oct 2019 13:01:50 +0000 Subject: [PATCH 0342/1623] Newsfile --- changelog.d/6274.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6274.misc diff --git a/changelog.d/6274.misc b/changelog.d/6274.misc new file mode 100644 index 0000000000..eb4966124f --- /dev/null +++ b/changelog.d/6274.misc @@ -0,0 +1 @@ +Port replication http server endpoints to async/await. From 9be41bc12159c52e4a700950569d24b0d9a3491b Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 29 Oct 2019 13:09:24 +0000 Subject: [PATCH 0343/1623] Port room rest handlers to async/await --- synapse/rest/client/v1/room.py | 166 ++++++++++++++------------------- 1 file changed, 72 insertions(+), 94 deletions(-) diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index 9c1d41421c..86bbcc0eea 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -21,8 +21,6 @@ from six.moves.urllib import parse as urlparse from canonicaljson import json -from twisted.internet import defer - from synapse.api.constants import EventTypes, Membership from synapse.api.errors import ( AuthError, @@ -85,11 +83,10 @@ class RoomCreateRestServlet(TransactionRestServlet): set_tag("txn_id", txn_id) return self.txns.fetch_or_execute_request(request, self.on_POST, request) - @defer.inlineCallbacks - def on_POST(self, request): - requester = yield self.auth.get_user_by_req(request) + async def on_POST(self, request): + requester = await self.auth.get_user_by_req(request) - info = yield self._room_creation_handler.create_room( + info = await self._room_creation_handler.create_room( requester, self.get_room_config(request) ) @@ -154,15 +151,14 @@ class RoomStateEventRestServlet(TransactionRestServlet): def on_PUT_no_state_key(self, request, room_id, event_type): return self.on_PUT(request, room_id, event_type, "") - @defer.inlineCallbacks - def on_GET(self, request, room_id, event_type, state_key): - requester = yield self.auth.get_user_by_req(request, allow_guest=True) + async def on_GET(self, request, room_id, event_type, state_key): + requester = await self.auth.get_user_by_req(request, allow_guest=True) format = parse_string( request, "format", default="content", allowed_values=["content", "event"] ) msg_handler = self.message_handler - data = yield msg_handler.get_room_data( + data = await msg_handler.get_room_data( user_id=requester.user.to_string(), room_id=room_id, event_type=event_type, @@ -179,9 +175,8 @@ class RoomStateEventRestServlet(TransactionRestServlet): elif format == "content": return 200, data.get_dict()["content"] - @defer.inlineCallbacks - def on_PUT(self, request, room_id, event_type, state_key, txn_id=None): - requester = yield self.auth.get_user_by_req(request) + async def on_PUT(self, request, room_id, event_type, state_key, txn_id=None): + requester = await self.auth.get_user_by_req(request) if txn_id: set_tag("txn_id", txn_id) @@ -200,7 +195,7 @@ class RoomStateEventRestServlet(TransactionRestServlet): if event_type == EventTypes.Member: membership = content.get("membership", None) - event = yield self.room_member_handler.update_membership( + event = await self.room_member_handler.update_membership( requester, target=UserID.from_string(state_key), room_id=room_id, @@ -208,7 +203,7 @@ class RoomStateEventRestServlet(TransactionRestServlet): content=content, ) else: - event = yield self.event_creation_handler.create_and_send_nonmember_event( + event = await self.event_creation_handler.create_and_send_nonmember_event( requester, event_dict, txn_id=txn_id ) @@ -231,9 +226,8 @@ class RoomSendEventRestServlet(TransactionRestServlet): PATTERNS = "/rooms/(?P[^/]*)/send/(?P[^/]*)" register_txn_path(self, PATTERNS, http_server, with_get=True) - @defer.inlineCallbacks - def on_POST(self, request, room_id, event_type, txn_id=None): - requester = yield self.auth.get_user_by_req(request, allow_guest=True) + async def on_POST(self, request, room_id, event_type, txn_id=None): + requester = await self.auth.get_user_by_req(request, allow_guest=True) content = parse_json_object_from_request(request) event_dict = { @@ -246,7 +240,7 @@ class RoomSendEventRestServlet(TransactionRestServlet): if b"ts" in request.args and requester.app_service: event_dict["origin_server_ts"] = parse_integer(request, "ts", 0) - event = yield self.event_creation_handler.create_and_send_nonmember_event( + event = await self.event_creation_handler.create_and_send_nonmember_event( requester, event_dict, txn_id=txn_id ) @@ -276,9 +270,8 @@ class JoinRoomAliasServlet(TransactionRestServlet): PATTERNS = "/join/(?P[^/]*)" register_txn_path(self, PATTERNS, http_server) - @defer.inlineCallbacks - def on_POST(self, request, room_identifier, txn_id=None): - requester = yield self.auth.get_user_by_req(request, allow_guest=True) + async def on_POST(self, request, room_identifier, txn_id=None): + requester = await self.auth.get_user_by_req(request, allow_guest=True) try: content = parse_json_object_from_request(request) @@ -298,14 +291,14 @@ class JoinRoomAliasServlet(TransactionRestServlet): elif RoomAlias.is_valid(room_identifier): handler = self.room_member_handler room_alias = RoomAlias.from_string(room_identifier) - room_id, remote_room_hosts = yield handler.lookup_room_alias(room_alias) + room_id, remote_room_hosts = await handler.lookup_room_alias(room_alias) room_id = room_id.to_string() else: raise SynapseError( 400, "%s was not legal room ID or room alias" % (room_identifier,) ) - yield self.room_member_handler.update_membership( + await self.room_member_handler.update_membership( requester=requester, target=requester.user, room_id=room_id, @@ -335,12 +328,11 @@ class PublicRoomListRestServlet(TransactionRestServlet): self.hs = hs self.auth = hs.get_auth() - @defer.inlineCallbacks - def on_GET(self, request): + async def on_GET(self, request): server = parse_string(request, "server", default=None) try: - yield self.auth.get_user_by_req(request, allow_guest=True) + await self.auth.get_user_by_req(request, allow_guest=True) except InvalidClientCredentialsError as e: # Option to allow servers to require auth when accessing # /publicRooms via CS API. This is especially helpful in private @@ -367,19 +359,18 @@ class PublicRoomListRestServlet(TransactionRestServlet): handler = self.hs.get_room_list_handler() if server: - data = yield handler.get_remote_public_room_list( + data = await handler.get_remote_public_room_list( server, limit=limit, since_token=since_token ) else: - data = yield handler.get_local_public_room_list( + data = await handler.get_local_public_room_list( limit=limit, since_token=since_token ) return 200, data - @defer.inlineCallbacks - def on_POST(self, request): - yield self.auth.get_user_by_req(request, allow_guest=True) + async def on_POST(self, request): + await self.auth.get_user_by_req(request, allow_guest=True) server = parse_string(request, "server", default=None) content = parse_json_object_from_request(request) @@ -408,7 +399,7 @@ class PublicRoomListRestServlet(TransactionRestServlet): handler = self.hs.get_room_list_handler() if server: - data = yield handler.get_remote_public_room_list( + data = await handler.get_remote_public_room_list( server, limit=limit, since_token=since_token, @@ -417,7 +408,7 @@ class PublicRoomListRestServlet(TransactionRestServlet): third_party_instance_id=third_party_instance_id, ) else: - data = yield handler.get_local_public_room_list( + data = await handler.get_local_public_room_list( limit=limit, since_token=since_token, search_filter=search_filter, @@ -436,10 +427,9 @@ class RoomMemberListRestServlet(RestServlet): self.message_handler = hs.get_message_handler() self.auth = hs.get_auth() - @defer.inlineCallbacks - def on_GET(self, request, room_id): + async def on_GET(self, request, room_id): # TODO support Pagination stream API (limit/tokens) - requester = yield self.auth.get_user_by_req(request) + requester = await self.auth.get_user_by_req(request) handler = self.message_handler # request the state as of a given event, as identified by a stream token, @@ -459,7 +449,7 @@ class RoomMemberListRestServlet(RestServlet): membership = parse_string(request, "membership") not_membership = parse_string(request, "not_membership") - events = yield handler.get_state_events( + events = await handler.get_state_events( room_id=room_id, user_id=requester.user.to_string(), at_token=at_token, @@ -488,11 +478,10 @@ class JoinedRoomMemberListRestServlet(RestServlet): self.message_handler = hs.get_message_handler() self.auth = hs.get_auth() - @defer.inlineCallbacks - def on_GET(self, request, room_id): - requester = yield self.auth.get_user_by_req(request) + async def on_GET(self, request, room_id): + requester = await self.auth.get_user_by_req(request) - users_with_profile = yield self.message_handler.get_joined_members( + users_with_profile = await self.message_handler.get_joined_members( requester, room_id ) @@ -508,9 +497,8 @@ class RoomMessageListRestServlet(RestServlet): self.pagination_handler = hs.get_pagination_handler() self.auth = hs.get_auth() - @defer.inlineCallbacks - def on_GET(self, request, room_id): - requester = yield self.auth.get_user_by_req(request, allow_guest=True) + async def on_GET(self, request, room_id): + requester = await self.auth.get_user_by_req(request, allow_guest=True) pagination_config = PaginationConfig.from_request(request, default_limit=10) as_client_event = b"raw" not in request.args filter_bytes = parse_string(request, b"filter", encoding=None) @@ -521,7 +509,7 @@ class RoomMessageListRestServlet(RestServlet): as_client_event = False else: event_filter = None - msgs = yield self.pagination_handler.get_messages( + msgs = await self.pagination_handler.get_messages( room_id=room_id, requester=requester, pagin_config=pagination_config, @@ -541,11 +529,10 @@ class RoomStateRestServlet(RestServlet): self.message_handler = hs.get_message_handler() self.auth = hs.get_auth() - @defer.inlineCallbacks - def on_GET(self, request, room_id): - requester = yield self.auth.get_user_by_req(request, allow_guest=True) + async def on_GET(self, request, room_id): + requester = await self.auth.get_user_by_req(request, allow_guest=True) # Get all the current state for this room - events = yield self.message_handler.get_state_events( + events = await self.message_handler.get_state_events( room_id=room_id, user_id=requester.user.to_string(), is_guest=requester.is_guest, @@ -562,11 +549,10 @@ class RoomInitialSyncRestServlet(RestServlet): self.initial_sync_handler = hs.get_initial_sync_handler() self.auth = hs.get_auth() - @defer.inlineCallbacks - def on_GET(self, request, room_id): - requester = yield self.auth.get_user_by_req(request, allow_guest=True) + async def on_GET(self, request, room_id): + requester = await self.auth.get_user_by_req(request, allow_guest=True) pagination_config = PaginationConfig.from_request(request) - content = yield self.initial_sync_handler.room_initial_sync( + content = await self.initial_sync_handler.room_initial_sync( room_id=room_id, requester=requester, pagin_config=pagination_config ) return 200, content @@ -584,11 +570,10 @@ class RoomEventServlet(RestServlet): self._event_serializer = hs.get_event_client_serializer() self.auth = hs.get_auth() - @defer.inlineCallbacks - def on_GET(self, request, room_id, event_id): - requester = yield self.auth.get_user_by_req(request, allow_guest=True) + async def on_GET(self, request, room_id, event_id): + requester = await self.auth.get_user_by_req(request, allow_guest=True) try: - event = yield self.event_handler.get_event( + event = await self.event_handler.get_event( requester.user, room_id, event_id ) except AuthError: @@ -599,7 +584,7 @@ class RoomEventServlet(RestServlet): time_now = self.clock.time_msec() if event: - event = yield self._event_serializer.serialize_event(event, time_now) + event = await self._event_serializer.serialize_event(event, time_now) return 200, event return SynapseError(404, "Event not found.", errcode=Codes.NOT_FOUND) @@ -617,9 +602,8 @@ class RoomEventContextServlet(RestServlet): self._event_serializer = hs.get_event_client_serializer() self.auth = hs.get_auth() - @defer.inlineCallbacks - def on_GET(self, request, room_id, event_id): - requester = yield self.auth.get_user_by_req(request, allow_guest=True) + async def on_GET(self, request, room_id, event_id): + requester = await self.auth.get_user_by_req(request, allow_guest=True) limit = parse_integer(request, "limit", default=10) @@ -631,7 +615,7 @@ class RoomEventContextServlet(RestServlet): else: event_filter = None - results = yield self.room_context_handler.get_event_context( + results = await self.room_context_handler.get_event_context( requester.user, room_id, event_id, limit, event_filter ) @@ -639,16 +623,16 @@ class RoomEventContextServlet(RestServlet): raise SynapseError(404, "Event not found.", errcode=Codes.NOT_FOUND) time_now = self.clock.time_msec() - results["events_before"] = yield self._event_serializer.serialize_events( + results["events_before"] = await self._event_serializer.serialize_events( results["events_before"], time_now ) - results["event"] = yield self._event_serializer.serialize_event( + results["event"] = await self._event_serializer.serialize_event( results["event"], time_now ) - results["events_after"] = yield self._event_serializer.serialize_events( + results["events_after"] = await self._event_serializer.serialize_events( results["events_after"], time_now ) - results["state"] = yield self._event_serializer.serialize_events( + results["state"] = await self._event_serializer.serialize_events( results["state"], time_now ) @@ -665,11 +649,10 @@ class RoomForgetRestServlet(TransactionRestServlet): PATTERNS = "/rooms/(?P[^/]*)/forget" register_txn_path(self, PATTERNS, http_server) - @defer.inlineCallbacks - def on_POST(self, request, room_id, txn_id=None): - requester = yield self.auth.get_user_by_req(request, allow_guest=False) + async def on_POST(self, request, room_id, txn_id=None): + requester = await self.auth.get_user_by_req(request, allow_guest=False) - yield self.room_member_handler.forget(user=requester.user, room_id=room_id) + await self.room_member_handler.forget(user=requester.user, room_id=room_id) return 200, {} @@ -696,9 +679,8 @@ class RoomMembershipRestServlet(TransactionRestServlet): ) register_txn_path(self, PATTERNS, http_server) - @defer.inlineCallbacks - def on_POST(self, request, room_id, membership_action, txn_id=None): - requester = yield self.auth.get_user_by_req(request, allow_guest=True) + async def on_POST(self, request, room_id, membership_action, txn_id=None): + requester = await self.auth.get_user_by_req(request, allow_guest=True) if requester.is_guest and membership_action not in { Membership.JOIN, @@ -714,7 +696,7 @@ class RoomMembershipRestServlet(TransactionRestServlet): content = {} if membership_action == "invite" and self._has_3pid_invite_keys(content): - yield self.room_member_handler.do_3pid_invite( + await self.room_member_handler.do_3pid_invite( room_id, requester.user, content["medium"], @@ -735,7 +717,7 @@ class RoomMembershipRestServlet(TransactionRestServlet): if "reason" in content and membership_action in ["kick", "ban"]: event_content = {"reason": content["reason"]} - yield self.room_member_handler.update_membership( + await self.room_member_handler.update_membership( requester=requester, target=target, room_id=room_id, @@ -777,12 +759,11 @@ class RoomRedactEventRestServlet(TransactionRestServlet): PATTERNS = "/rooms/(?P[^/]*)/redact/(?P[^/]*)" register_txn_path(self, PATTERNS, http_server) - @defer.inlineCallbacks - def on_POST(self, request, room_id, event_id, txn_id=None): - requester = yield self.auth.get_user_by_req(request) + async def on_POST(self, request, room_id, event_id, txn_id=None): + requester = await self.auth.get_user_by_req(request) content = parse_json_object_from_request(request) - event = yield self.event_creation_handler.create_and_send_nonmember_event( + event = await self.event_creation_handler.create_and_send_nonmember_event( requester, { "type": EventTypes.Redaction, @@ -816,29 +797,28 @@ class RoomTypingRestServlet(RestServlet): self.typing_handler = hs.get_typing_handler() self.auth = hs.get_auth() - @defer.inlineCallbacks - def on_PUT(self, request, room_id, user_id): - requester = yield self.auth.get_user_by_req(request) + async def on_PUT(self, request, room_id, user_id): + requester = await self.auth.get_user_by_req(request) room_id = urlparse.unquote(room_id) target_user = UserID.from_string(urlparse.unquote(user_id)) content = parse_json_object_from_request(request) - yield self.presence_handler.bump_presence_active_time(requester.user) + await self.presence_handler.bump_presence_active_time(requester.user) # Limit timeout to stop people from setting silly typing timeouts. timeout = min(content.get("timeout", 30000), 120000) if content["typing"]: - yield self.typing_handler.started_typing( + await self.typing_handler.started_typing( target_user=target_user, auth_user=requester.user, room_id=room_id, timeout=timeout, ) else: - yield self.typing_handler.stopped_typing( + await self.typing_handler.stopped_typing( target_user=target_user, auth_user=requester.user, room_id=room_id ) @@ -853,14 +833,13 @@ class SearchRestServlet(RestServlet): self.handlers = hs.get_handlers() self.auth = hs.get_auth() - @defer.inlineCallbacks - def on_POST(self, request): - requester = yield self.auth.get_user_by_req(request) + async def on_POST(self, request): + requester = await self.auth.get_user_by_req(request) content = parse_json_object_from_request(request) batch = parse_string(request, "next_batch") - results = yield self.handlers.search_handler.search( + results = await self.handlers.search_handler.search( requester.user, content, batch ) @@ -875,11 +854,10 @@ class JoinedRoomsRestServlet(RestServlet): self.store = hs.get_datastore() self.auth = hs.get_auth() - @defer.inlineCallbacks - def on_GET(self, request): - requester = yield self.auth.get_user_by_req(request, allow_guest=True) + async def on_GET(self, request): + requester = await self.auth.get_user_by_req(request, allow_guest=True) - room_ids = yield self.store.get_rooms_for_user(requester.user.to_string()) + room_ids = await self.store.get_rooms_for_user(requester.user.to_string()) return 200, {"joined_rooms": list(room_ids)} From 387324688ee8f4314e43d1a1804df11197be977c Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 29 Oct 2019 13:10:45 +0000 Subject: [PATCH 0344/1623] Newsfile --- changelog.d/6275.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6275.misc diff --git a/changelog.d/6275.misc b/changelog.d/6275.misc new file mode 100644 index 0000000000..f57e2c4adb --- /dev/null +++ b/changelog.d/6275.misc @@ -0,0 +1 @@ +Port room rest handlers to async/await. From ce85169792b1cc02c857307563c84bb86b5ede3e Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 29 Oct 2019 13:15:38 +0000 Subject: [PATCH 0345/1623] Update UPGRADE.rst fix typo --- UPGRADE.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UPGRADE.rst b/UPGRADE.rst index 9562114d59..bed84e0847 100644 --- a/UPGRADE.rst +++ b/UPGRADE.rst @@ -2,7 +2,7 @@ Upgrading Synapse ================= Before upgrading check if any special steps are required to upgrade from the -what you currently have installed to current version of Synapse. The extra +version you currently have installed to current version of Synapse. The extra instructions that may be required are listed later in this document. * If Synapse was installed using `prebuilt packages From 540b0b90418a52b7b62f09889fa09fcab40bee9b Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 29 Oct 2019 13:16:01 +0000 Subject: [PATCH 0346/1623] Update UPGRADE.rst another typo --- UPGRADE.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UPGRADE.rst b/UPGRADE.rst index bed84e0847..db12f125dc 100644 --- a/UPGRADE.rst +++ b/UPGRADE.rst @@ -2,7 +2,7 @@ Upgrading Synapse ================= Before upgrading check if any special steps are required to upgrade from the -version you currently have installed to current version of Synapse. The extra +version you currently have installed to the current version of Synapse. The extra instructions that may be required are listed later in this document. * If Synapse was installed using `prebuilt packages From 20ebd2497369b63cc4555bf5e4c52306196439a6 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 29 Oct 2019 14:04:02 +0000 Subject: [PATCH 0347/1623] Fix changelog name --- changelog.d/{6286.bugfix => 6268.bugfix} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/{6286.bugfix => 6268.bugfix} (100%) diff --git a/changelog.d/6286.bugfix b/changelog.d/6268.bugfix similarity index 100% rename from changelog.d/6286.bugfix rename to changelog.d/6268.bugfix From 3f33879be4697666a304a472d090d4def0c671d0 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 29 Oct 2019 14:05:32 +0000 Subject: [PATCH 0348/1623] Port federation_server to async/await --- synapse/federation/federation_server.py | 205 ++++++++++-------------- tests/handlers/test_typing.py | 3 + 2 files changed, 90 insertions(+), 118 deletions(-) diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index 5fc7c1d67b..15c1fa0a51 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -21,7 +21,6 @@ from six import iteritems from canonicaljson import json from prometheus_client import Counter -from twisted.internet import defer from twisted.internet.abstract import isIPAddress from twisted.python import failure @@ -86,14 +85,12 @@ class FederationServer(FederationBase): # come in waves. self._state_resp_cache = ResponseCache(hs, "state_resp", timeout_ms=30000) - @defer.inlineCallbacks - @log_function - def on_backfill_request(self, origin, room_id, versions, limit): - with (yield self._server_linearizer.queue((origin, room_id))): + async def on_backfill_request(self, origin, room_id, versions, limit): + with (await self._server_linearizer.queue((origin, room_id))): origin_host, _ = parse_server_name(origin) - yield self.check_server_matches_acl(origin_host, room_id) + await self.check_server_matches_acl(origin_host, room_id) - pdus = yield self.handler.on_backfill_request( + pdus = await self.handler.on_backfill_request( origin, room_id, versions, limit ) @@ -101,9 +98,7 @@ class FederationServer(FederationBase): return 200, res - @defer.inlineCallbacks - @log_function - def on_incoming_transaction(self, origin, transaction_data): + async def on_incoming_transaction(self, origin, transaction_data): # keep this as early as possible to make the calculated origin ts as # accurate as possible. request_time = self._clock.time_msec() @@ -118,18 +113,17 @@ class FederationServer(FederationBase): # use a linearizer to ensure that we don't process the same transaction # multiple times in parallel. with ( - yield self._transaction_linearizer.queue( + await self._transaction_linearizer.queue( (origin, transaction.transaction_id) ) ): - result = yield self._handle_incoming_transaction( + result = await self._handle_incoming_transaction( origin, transaction, request_time ) return result - @defer.inlineCallbacks - def _handle_incoming_transaction(self, origin, transaction, request_time): + async def _handle_incoming_transaction(self, origin, transaction, request_time): """ Process an incoming transaction and return the HTTP response Args: @@ -140,7 +134,7 @@ class FederationServer(FederationBase): Returns: Deferred[(int, object)]: http response code and body """ - response = yield self.transaction_actions.have_responded(origin, transaction) + response = await self.transaction_actions.have_responded(origin, transaction) if response: logger.debug( @@ -159,7 +153,7 @@ class FederationServer(FederationBase): logger.info("Transaction PDU or EDU count too large. Returning 400") response = {} - yield self.transaction_actions.set_response( + await self.transaction_actions.set_response( origin, transaction, 400, response ) return 400, response @@ -195,7 +189,7 @@ class FederationServer(FederationBase): continue try: - room_version = yield self.store.get_room_version(room_id) + room_version = await self.store.get_room_version(room_id) except NotFoundError: logger.info("Ignoring PDU for unknown room_id: %s", room_id) continue @@ -221,11 +215,10 @@ class FederationServer(FederationBase): # require callouts to other servers to fetch missing events), but # impose a limit to avoid going too crazy with ram/cpu. - @defer.inlineCallbacks - def process_pdus_for_room(room_id): + async def process_pdus_for_room(room_id): logger.debug("Processing PDUs for %s", room_id) try: - yield self.check_server_matches_acl(origin_host, room_id) + await self.check_server_matches_acl(origin_host, room_id) except AuthError as e: logger.warn("Ignoring PDUs for room %s from banned server", room_id) for pdu in pdus_by_room[room_id]: @@ -237,7 +230,7 @@ class FederationServer(FederationBase): event_id = pdu.event_id with nested_logging_context(event_id): try: - yield self._handle_received_pdu(origin, pdu) + await self._handle_received_pdu(origin, pdu) pdu_results[event_id] = {} except FederationError as e: logger.warn("Error handling PDU %s: %s", event_id, e) @@ -251,36 +244,33 @@ class FederationServer(FederationBase): exc_info=(f.type, f.value, f.getTracebackObject()), ) - yield concurrently_execute( + await concurrently_execute( process_pdus_for_room, pdus_by_room.keys(), TRANSACTION_CONCURRENCY_LIMIT ) if hasattr(transaction, "edus"): for edu in (Edu(**x) for x in transaction.edus): - yield self.received_edu(origin, edu.edu_type, edu.content) + await self.received_edu(origin, edu.edu_type, edu.content) response = {"pdus": pdu_results} logger.debug("Returning: %s", str(response)) - yield self.transaction_actions.set_response(origin, transaction, 200, response) + await self.transaction_actions.set_response(origin, transaction, 200, response) return 200, response - @defer.inlineCallbacks - def received_edu(self, origin, edu_type, content): + async def received_edu(self, origin, edu_type, content): received_edus_counter.inc() - yield self.registry.on_edu(edu_type, origin, content) + await self.registry.on_edu(edu_type, origin, content) - @defer.inlineCallbacks - @log_function - def on_context_state_request(self, origin, room_id, event_id): + async def on_context_state_request(self, origin, room_id, event_id): if not event_id: raise NotImplementedError("Specify an event") origin_host, _ = parse_server_name(origin) - yield self.check_server_matches_acl(origin_host, room_id) + await self.check_server_matches_acl(origin_host, room_id) - in_room = yield self.auth.check_host_in_room(room_id, origin) + in_room = await self.auth.check_host_in_room(room_id, origin) if not in_room: raise AuthError(403, "Host not in room.") @@ -289,8 +279,8 @@ class FederationServer(FederationBase): # in the cache so we could return it without waiting for the linearizer # - but that's non-trivial to get right, and anyway somewhat defeats # the point of the linearizer. - with (yield self._server_linearizer.queue((origin, room_id))): - resp = yield self._state_resp_cache.wrap( + with (await self._server_linearizer.queue((origin, room_id))): + resp = await self._state_resp_cache.wrap( (room_id, event_id), self._on_context_state_request_compute, room_id, @@ -299,65 +289,58 @@ class FederationServer(FederationBase): return 200, resp - @defer.inlineCallbacks - def on_state_ids_request(self, origin, room_id, event_id): + async def on_state_ids_request(self, origin, room_id, event_id): if not event_id: raise NotImplementedError("Specify an event") origin_host, _ = parse_server_name(origin) - yield self.check_server_matches_acl(origin_host, room_id) + await self.check_server_matches_acl(origin_host, room_id) - in_room = yield self.auth.check_host_in_room(room_id, origin) + in_room = await self.auth.check_host_in_room(room_id, origin) if not in_room: raise AuthError(403, "Host not in room.") - state_ids = yield self.handler.get_state_ids_for_pdu(room_id, event_id) - auth_chain_ids = yield self.store.get_auth_chain_ids(state_ids) + state_ids = await self.handler.get_state_ids_for_pdu(room_id, event_id) + auth_chain_ids = await self.store.get_auth_chain_ids(state_ids) return 200, {"pdu_ids": state_ids, "auth_chain_ids": auth_chain_ids} - @defer.inlineCallbacks - def _on_context_state_request_compute(self, room_id, event_id): - pdus = yield self.handler.get_state_for_pdu(room_id, event_id) - auth_chain = yield self.store.get_auth_chain([pdu.event_id for pdu in pdus]) + async def _on_context_state_request_compute(self, room_id, event_id): + pdus = await self.handler.get_state_for_pdu(room_id, event_id) + auth_chain = await self.store.get_auth_chain([pdu.event_id for pdu in pdus]) return { "pdus": [pdu.get_pdu_json() for pdu in pdus], "auth_chain": [pdu.get_pdu_json() for pdu in auth_chain], } - @defer.inlineCallbacks - @log_function - def on_pdu_request(self, origin, event_id): - pdu = yield self.handler.get_persisted_pdu(origin, event_id) + async def on_pdu_request(self, origin, event_id): + pdu = await self.handler.get_persisted_pdu(origin, event_id) if pdu: return 200, self._transaction_from_pdus([pdu]).get_dict() else: return 404, "" - @defer.inlineCallbacks - def on_query_request(self, query_type, args): + async def on_query_request(self, query_type, args): received_queries_counter.labels(query_type).inc() - resp = yield self.registry.on_query(query_type, args) + resp = await self.registry.on_query(query_type, args) return 200, resp - @defer.inlineCallbacks - def on_make_join_request(self, origin, room_id, user_id, supported_versions): + async def on_make_join_request(self, origin, room_id, user_id, supported_versions): origin_host, _ = parse_server_name(origin) - yield self.check_server_matches_acl(origin_host, room_id) + await self.check_server_matches_acl(origin_host, room_id) - room_version = yield self.store.get_room_version(room_id) + room_version = await self.store.get_room_version(room_id) if room_version not in supported_versions: logger.warn("Room version %s not in %s", room_version, supported_versions) raise IncompatibleRoomVersionError(room_version=room_version) - pdu = yield self.handler.on_make_join_request(origin, room_id, user_id) + pdu = await self.handler.on_make_join_request(origin, room_id, user_id) time_now = self._clock.time_msec() return {"event": pdu.get_pdu_json(time_now), "room_version": room_version} - @defer.inlineCallbacks - def on_invite_request(self, origin, content, room_version): + async def on_invite_request(self, origin, content, room_version): if room_version not in KNOWN_ROOM_VERSIONS: raise SynapseError( 400, @@ -369,28 +352,27 @@ class FederationServer(FederationBase): pdu = event_from_pdu_json(content, format_ver) origin_host, _ = parse_server_name(origin) - yield self.check_server_matches_acl(origin_host, pdu.room_id) - pdu = yield self._check_sigs_and_hash(room_version, pdu) - ret_pdu = yield self.handler.on_invite_request(origin, pdu) + await self.check_server_matches_acl(origin_host, pdu.room_id) + pdu = await self._check_sigs_and_hash(room_version, pdu) + ret_pdu = await self.handler.on_invite_request(origin, pdu) time_now = self._clock.time_msec() return {"event": ret_pdu.get_pdu_json(time_now)} - @defer.inlineCallbacks - def on_send_join_request(self, origin, content, room_id): + async def on_send_join_request(self, origin, content, room_id): logger.debug("on_send_join_request: content: %s", content) - room_version = yield self.store.get_room_version(room_id) + room_version = await self.store.get_room_version(room_id) format_ver = room_version_to_event_format(room_version) pdu = event_from_pdu_json(content, format_ver) origin_host, _ = parse_server_name(origin) - yield self.check_server_matches_acl(origin_host, pdu.room_id) + await self.check_server_matches_acl(origin_host, pdu.room_id) logger.debug("on_send_join_request: pdu sigs: %s", pdu.signatures) - pdu = yield self._check_sigs_and_hash(room_version, pdu) + pdu = await self._check_sigs_and_hash(room_version, pdu) - res_pdus = yield self.handler.on_send_join_request(origin, pdu) + res_pdus = await self.handler.on_send_join_request(origin, pdu) time_now = self._clock.time_msec() return ( 200, @@ -402,48 +384,44 @@ class FederationServer(FederationBase): }, ) - @defer.inlineCallbacks - def on_make_leave_request(self, origin, room_id, user_id): + async def on_make_leave_request(self, origin, room_id, user_id): origin_host, _ = parse_server_name(origin) - yield self.check_server_matches_acl(origin_host, room_id) - pdu = yield self.handler.on_make_leave_request(origin, room_id, user_id) + await self.check_server_matches_acl(origin_host, room_id) + pdu = await self.handler.on_make_leave_request(origin, room_id, user_id) - room_version = yield self.store.get_room_version(room_id) + room_version = await self.store.get_room_version(room_id) time_now = self._clock.time_msec() return {"event": pdu.get_pdu_json(time_now), "room_version": room_version} - @defer.inlineCallbacks - def on_send_leave_request(self, origin, content, room_id): + async def on_send_leave_request(self, origin, content, room_id): logger.debug("on_send_leave_request: content: %s", content) - room_version = yield self.store.get_room_version(room_id) + room_version = await self.store.get_room_version(room_id) format_ver = room_version_to_event_format(room_version) pdu = event_from_pdu_json(content, format_ver) origin_host, _ = parse_server_name(origin) - yield self.check_server_matches_acl(origin_host, pdu.room_id) + await self.check_server_matches_acl(origin_host, pdu.room_id) logger.debug("on_send_leave_request: pdu sigs: %s", pdu.signatures) - pdu = yield self._check_sigs_and_hash(room_version, pdu) + pdu = await self._check_sigs_and_hash(room_version, pdu) - yield self.handler.on_send_leave_request(origin, pdu) + await self.handler.on_send_leave_request(origin, pdu) return 200, {} - @defer.inlineCallbacks - def on_event_auth(self, origin, room_id, event_id): - with (yield self._server_linearizer.queue((origin, room_id))): + async def on_event_auth(self, origin, room_id, event_id): + with (await self._server_linearizer.queue((origin, room_id))): origin_host, _ = parse_server_name(origin) - yield self.check_server_matches_acl(origin_host, room_id) + await self.check_server_matches_acl(origin_host, room_id) time_now = self._clock.time_msec() - auth_pdus = yield self.handler.on_event_auth(event_id) + auth_pdus = await self.handler.on_event_auth(event_id) res = {"auth_chain": [a.get_pdu_json(time_now) for a in auth_pdus]} return 200, res - @defer.inlineCallbacks - def on_query_auth_request(self, origin, content, room_id, event_id): + async def on_query_auth_request(self, origin, content, room_id, event_id): """ Content is a dict with keys:: auth_chain (list): A list of events that give the auth chain. @@ -462,22 +440,22 @@ class FederationServer(FederationBase): Returns: Deferred: Results in `dict` with the same format as `content` """ - with (yield self._server_linearizer.queue((origin, room_id))): + with (await self._server_linearizer.queue((origin, room_id))): origin_host, _ = parse_server_name(origin) - yield self.check_server_matches_acl(origin_host, room_id) + await self.check_server_matches_acl(origin_host, room_id) - room_version = yield self.store.get_room_version(room_id) + room_version = await self.store.get_room_version(room_id) format_ver = room_version_to_event_format(room_version) auth_chain = [ event_from_pdu_json(e, format_ver) for e in content["auth_chain"] ] - signed_auth = yield self._check_sigs_and_hash_and_fetch( + signed_auth = await self._check_sigs_and_hash_and_fetch( origin, auth_chain, outlier=True, room_version=room_version ) - ret = yield self.handler.on_query_auth( + ret = await self.handler.on_query_auth( origin, event_id, room_id, @@ -503,16 +481,14 @@ class FederationServer(FederationBase): return self.on_query_request("user_devices", user_id) @trace - @defer.inlineCallbacks - @log_function - def on_claim_client_keys(self, origin, content): + async def on_claim_client_keys(self, origin, content): query = [] for user_id, device_keys in content.get("one_time_keys", {}).items(): for device_id, algorithm in device_keys.items(): query.append((user_id, device_id, algorithm)) log_kv({"message": "Claiming one time keys.", "user, device pairs": query}) - results = yield self.store.claim_e2e_one_time_keys(query) + results = await self.store.claim_e2e_one_time_keys(query) json_result = {} for user_id, device_keys in results.items(): @@ -536,14 +512,12 @@ class FederationServer(FederationBase): return {"one_time_keys": json_result} - @defer.inlineCallbacks - @log_function - def on_get_missing_events( + async def on_get_missing_events( self, origin, room_id, earliest_events, latest_events, limit ): - with (yield self._server_linearizer.queue((origin, room_id))): + with (await self._server_linearizer.queue((origin, room_id))): origin_host, _ = parse_server_name(origin) - yield self.check_server_matches_acl(origin_host, room_id) + await self.check_server_matches_acl(origin_host, room_id) logger.info( "on_get_missing_events: earliest_events: %r, latest_events: %r," @@ -553,7 +527,7 @@ class FederationServer(FederationBase): limit, ) - missing_events = yield self.handler.on_get_missing_events( + missing_events = await self.handler.on_get_missing_events( origin, room_id, earliest_events, latest_events, limit ) @@ -586,8 +560,7 @@ class FederationServer(FederationBase): destination=None, ) - @defer.inlineCallbacks - def _handle_received_pdu(self, origin, pdu): + async def _handle_received_pdu(self, origin, pdu): """ Process a PDU received in a federation /send/ transaction. If the event is invalid, then this method throws a FederationError. @@ -640,37 +613,34 @@ class FederationServer(FederationBase): logger.info("Accepting join PDU %s from %s", pdu.event_id, origin) # We've already checked that we know the room version by this point - room_version = yield self.store.get_room_version(pdu.room_id) + room_version = await self.store.get_room_version(pdu.room_id) # Check signature. try: - pdu = yield self._check_sigs_and_hash(room_version, pdu) + pdu = await self._check_sigs_and_hash(room_version, pdu) except SynapseError as e: raise FederationError("ERROR", e.code, e.msg, affected=pdu.event_id) - yield self.handler.on_receive_pdu(origin, pdu, sent_to_us_directly=True) + await self.handler.on_receive_pdu(origin, pdu, sent_to_us_directly=True) def __str__(self): return "" % self.server_name - @defer.inlineCallbacks - def exchange_third_party_invite( + async def exchange_third_party_invite( self, sender_user_id, target_user_id, room_id, signed ): - ret = yield self.handler.exchange_third_party_invite( + ret = await self.handler.exchange_third_party_invite( sender_user_id, target_user_id, room_id, signed ) return ret - @defer.inlineCallbacks - def on_exchange_third_party_invite_request(self, room_id, event_dict): - ret = yield self.handler.on_exchange_third_party_invite_request( + async def on_exchange_third_party_invite_request(self, room_id, event_dict): + ret = await self.handler.on_exchange_third_party_invite_request( room_id, event_dict ) return ret - @defer.inlineCallbacks - def check_server_matches_acl(self, server_name, room_id): + async def check_server_matches_acl(self, server_name, room_id): """Check if the given server is allowed by the server ACLs in the room Args: @@ -680,13 +650,13 @@ class FederationServer(FederationBase): Raises: AuthError if the server does not match the ACL """ - state_ids = yield self.store.get_current_state_ids(room_id) + state_ids = await self.store.get_current_state_ids(room_id) acl_event_id = state_ids.get((EventTypes.ServerACL, "")) if not acl_event_id: return - acl_event = yield self.store.get_event(acl_event_id) + acl_event = await self.store.get_event(acl_event_id) if server_matches_acl_event(server_name, acl_event): return @@ -799,15 +769,14 @@ class FederationHandlerRegistry(object): self.query_handlers[query_type] = handler - @defer.inlineCallbacks - def on_edu(self, edu_type, origin, content): + async def on_edu(self, edu_type, origin, content): handler = self.edu_handlers.get(edu_type) if not handler: logger.warn("No handler registered for EDU type %s", edu_type) with start_active_span_from_edu(content, "handle_edu"): try: - yield handler(origin, content) + await handler(origin, content) except SynapseError as e: logger.info("Failed to handle edu %r: %r", edu_type, e) except Exception: diff --git a/tests/handlers/test_typing.py b/tests/handlers/test_typing.py index 67f1013051..f360c8e965 100644 --- a/tests/handlers/test_typing.py +++ b/tests/handlers/test_typing.py @@ -144,6 +144,9 @@ class TypingNotificationsTestCase(unittest.HomeserverTestCase): self.datastore.get_to_device_stream_token = lambda: 0 self.datastore.get_new_device_msgs_for_remote = lambda *args, **kargs: ([], 0) self.datastore.delete_device_msgs_for_remote = lambda *args, **kargs: None + self.datastore.set_received_txn_response = lambda *args, **kwargs: defer.succeed( + None + ) def test_started_typing_local(self): self.room_members = [U_APPLE, U_BANANA] From 8086c1d89e23c89729eca45c23c4b5d201f96534 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 29 Oct 2019 14:26:19 +0000 Subject: [PATCH 0349/1623] update ugrade notes --- UPGRADE.rst | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/UPGRADE.rst b/UPGRADE.rst index db12f125dc..5ebf16a73e 100644 --- a/UPGRADE.rst +++ b/UPGRADE.rst @@ -29,7 +29,7 @@ instructions that may be required are listed later in this document. running: .. code:: bash - + git pull pip install --upgrade . @@ -75,6 +75,16 @@ for example: wget https://packages.matrix.org/debian/pool/main/m/matrix-synapse-py3/matrix-synapse-py3_1.3.0+stretch1_amd64.deb dpkg -i matrix-synapse-py3_1.3.0+stretch1_amd64.deb + +Upgrading to v1.5.0 +=================== + +This release includes a database migration which may take several minutes to +complete if there are a large number (more than a million or so) of entries in +the ``devices`` table. This is only likely to a be a problem on very large +installations. + + Upgrading to v1.4.0 =================== From fec7d88645191778db66d8873f9cdf0a0287bc53 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 29 Oct 2019 14:27:18 +0000 Subject: [PATCH 0350/1623] Newsfile --- changelog.d/6276.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6276.misc diff --git a/changelog.d/6276.misc b/changelog.d/6276.misc new file mode 100644 index 0000000000..5f5144a9ee --- /dev/null +++ b/changelog.d/6276.misc @@ -0,0 +1 @@ +Port `federation_server.py` to async/await. From 9ffcf0f7ba72f16e366f04db6384a9233b1808cb Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 29 Oct 2019 14:28:54 +0000 Subject: [PATCH 0351/1623] 1.5.0 --- CHANGES.md | 25 ++++++++++++++++++------- changelog.d/6268.bugfix | 1 - debian/changelog | 6 ++++++ synapse/__init__.py | 2 +- 4 files changed, 25 insertions(+), 9 deletions(-) delete mode 100644 changelog.d/6268.bugfix diff --git a/CHANGES.md b/CHANGES.md index c59b139eae..6faa4b8dce 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,17 @@ +Synapse 1.5.0 (2019-10-29) +========================== + +Security updates +---------------- + +This release includes a security fix ([\#6262](https://github.com/matrix-org/synapse/issues/6262), below). Administrators are encouraged to upgrade as soon as possible. + +Bugfixes +-------- + +- Fix bug where room directory search was case sensitive. ([\#6268](https://github.com/matrix-org/synapse/issues/6268)) + + Synapse 1.5.0rc2 (2019-10-28) ============================= @@ -19,13 +33,6 @@ Internal Changes Synapse 1.5.0rc1 (2019-10-24) ========================== -This release includes a database migration step **which may take a long time to complete**: - -- Allow devices to be marked as hidden, for use by features such as cross-signing. - This adds a new field with a default value to the devices field in the database, - and so the database upgrade may take a long time depending on how many devices - are in the database. ([\#5759](https://github.com/matrix-org/synapse/issues/5759)) - Features -------- @@ -69,6 +76,10 @@ Internal Changes ---------------- - Update `user_filters` table to have a unique index, and non-null columns. Thanks to @pik for contributing this. ([\#1172](https://github.com/matrix-org/synapse/issues/1172), [\#6175](https://github.com/matrix-org/synapse/issues/6175), [\#6184](https://github.com/matrix-org/synapse/issues/6184)) +- Allow devices to be marked as hidden, for use by features such as cross-signing. + This adds a new field with a default value to the devices field in the database, + and so the database upgrade may take a long time depending on how many devices + are in the database. ([\#5759](https://github.com/matrix-org/synapse/issues/5759)) - Move lookup-related functions from RoomMemberHandler to IdentityHandler. ([\#5978](https://github.com/matrix-org/synapse/issues/5978)) - Improve performance of the public room list directory. ([\#6019](https://github.com/matrix-org/synapse/issues/6019), [\#6152](https://github.com/matrix-org/synapse/issues/6152), [\#6153](https://github.com/matrix-org/synapse/issues/6153), [\#6154](https://github.com/matrix-org/synapse/issues/6154)) - Edit header dicts docstrings in `SimpleHttpClient` to note that `str` or `bytes` can be passed as header keys. ([\#6077](https://github.com/matrix-org/synapse/issues/6077)) diff --git a/changelog.d/6268.bugfix b/changelog.d/6268.bugfix deleted file mode 100644 index a4bebec1c7..0000000000 --- a/changelog.d/6268.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix bug where room directory search was case sensitive. diff --git a/debian/changelog b/debian/changelog index 02f2b508c2..acda7e5c63 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.5.0) stable; urgency=medium + + * New synapse release 1.5.0. + + -- Synapse Packaging team Tue, 29 Oct 2019 14:28:41 +0000 + matrix-synapse-py3 (1.4.1) stable; urgency=medium * New synapse release 1.4.1. diff --git a/synapse/__init__.py b/synapse/__init__.py index d0f92ffbf3..8587ffa76f 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -36,7 +36,7 @@ try: except ImportError: pass -__version__ = "1.5.0rc2" +__version__ = "1.5.0" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 09a135b0391a7a65c24c8c7b046cb6573dee3947 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 29 Oct 2019 15:02:23 +0000 Subject: [PATCH 0352/1623] Make concurrently_execute work with async/await --- synapse/util/async_helpers.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/synapse/util/async_helpers.py b/synapse/util/async_helpers.py index 804dbca443..7659eaeb42 100644 --- a/synapse/util/async_helpers.py +++ b/synapse/util/async_helpers.py @@ -138,7 +138,7 @@ def concurrently_execute(func, args, limit): the number of concurrent executions. Args: - func (func): Function to execute, should return a deferred. + func (func): Function to execute, should return a deferred or coroutine. args (list): List of arguments to pass to func, each invocation of func gets a signle argument. limit (int): Maximum number of conccurent executions. @@ -148,11 +148,10 @@ def concurrently_execute(func, args, limit): """ it = iter(args) - @defer.inlineCallbacks - def _concurrently_execute_inner(): + async def _concurrently_execute_inner(): try: while True: - yield func(next(it)) + await maybe_awaitable(func(next(it))) except StopIteration: pass From 2c35ffead257171d195f228bafd0d65b917e2165 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 29 Oct 2019 15:08:22 +0000 Subject: [PATCH 0353/1623] Port receipt and read markers to async/wait --- synapse/federation/send_queue.py | 4 ++- synapse/handlers/read_marker.py | 13 +++----- synapse/handlers/receipts.py | 37 +++++++-------------- synapse/rest/client/v2_alpha/read_marker.py | 13 +++----- synapse/rest/client/v2_alpha/receipts.py | 11 +++--- synapse/storage/data_stores/main/events.py | 7 ++-- 6 files changed, 32 insertions(+), 53 deletions(-) diff --git a/synapse/federation/send_queue.py b/synapse/federation/send_queue.py index 454456a52d..ced4925a98 100644 --- a/synapse/federation/send_queue.py +++ b/synapse/federation/send_queue.py @@ -36,6 +36,8 @@ from six import iteritems from sortedcontainers import SortedDict +from twisted.internet import defer + from synapse.metrics import LaterGauge from synapse.storage.presence import UserPresenceState from synapse.util.metrics import Measure @@ -212,7 +214,7 @@ class FederationRemoteSendQueue(object): receipt (synapse.types.ReadReceipt): """ # nothing to do here: the replication listener will handle it. - pass + return defer.succeed(None) def send_presence(self, states): """As per FederationSender diff --git a/synapse/handlers/read_marker.py b/synapse/handlers/read_marker.py index 3e4d8c93a4..e3b528d271 100644 --- a/synapse/handlers/read_marker.py +++ b/synapse/handlers/read_marker.py @@ -15,8 +15,6 @@ import logging -from twisted.internet import defer - from synapse.util.async_helpers import Linearizer from ._base import BaseHandler @@ -32,8 +30,7 @@ class ReadMarkerHandler(BaseHandler): self.read_marker_linearizer = Linearizer(name="read_marker") self.notifier = hs.get_notifier() - @defer.inlineCallbacks - def received_client_read_marker(self, room_id, user_id, event_id): + async def received_client_read_marker(self, room_id, user_id, event_id): """Updates the read marker for a given user in a given room if the event ID given is ahead in the stream relative to the current read marker. @@ -41,8 +38,8 @@ class ReadMarkerHandler(BaseHandler): the read marker has changed. """ - with (yield self.read_marker_linearizer.queue((room_id, user_id))): - existing_read_marker = yield self.store.get_account_data_for_room_and_type( + with await self.read_marker_linearizer.queue((room_id, user_id)): + existing_read_marker = await self.store.get_account_data_for_room_and_type( user_id, room_id, "m.fully_read" ) @@ -50,13 +47,13 @@ class ReadMarkerHandler(BaseHandler): if existing_read_marker: # Only update if the new marker is ahead in the stream - should_update = yield self.store.is_event_after( + should_update = await self.store.is_event_after( event_id, existing_read_marker["event_id"] ) if should_update: content = {"event_id": event_id} - max_id = yield self.store.add_account_data_to_room( + max_id = await self.store.add_account_data_to_room( user_id, room_id, "m.fully_read", content ) self.notifier.on_new_event("account_data_key", max_id, users=[user_id]) diff --git a/synapse/handlers/receipts.py b/synapse/handlers/receipts.py index 6854c751a6..9283c039e3 100644 --- a/synapse/handlers/receipts.py +++ b/synapse/handlers/receipts.py @@ -18,6 +18,7 @@ from twisted.internet import defer from synapse.handlers._base import BaseHandler from synapse.types import ReadReceipt, get_domain_from_id +from synapse.util.async_helpers import maybe_awaitable logger = logging.getLogger(__name__) @@ -36,8 +37,7 @@ class ReceiptsHandler(BaseHandler): self.clock = self.hs.get_clock() self.state = hs.get_state_handler() - @defer.inlineCallbacks - def _received_remote_receipt(self, origin, content): + async def _received_remote_receipt(self, origin, content): """Called when we receive an EDU of type m.receipt from a remote HS. """ receipts = [] @@ -62,17 +62,16 @@ class ReceiptsHandler(BaseHandler): ) ) - yield self._handle_new_receipts(receipts) + await self._handle_new_receipts(receipts) - @defer.inlineCallbacks - def _handle_new_receipts(self, receipts): + async def _handle_new_receipts(self, receipts): """Takes a list of receipts, stores them and informs the notifier. """ min_batch_id = None max_batch_id = None for receipt in receipts: - res = yield self.store.insert_receipt( + res = await self.store.insert_receipt( receipt.room_id, receipt.receipt_type, receipt.user_id, @@ -99,14 +98,15 @@ class ReceiptsHandler(BaseHandler): self.notifier.on_new_event("receipt_key", max_batch_id, rooms=affected_room_ids) # Note that the min here shouldn't be relied upon to be accurate. - yield self.hs.get_pusherpool().on_new_receipts( - min_batch_id, max_batch_id, affected_room_ids + await maybe_awaitable( + self.hs.get_pusherpool().on_new_receipts( + min_batch_id, max_batch_id, affected_room_ids + ) ) return True - @defer.inlineCallbacks - def received_client_receipt(self, room_id, receipt_type, user_id, event_id): + async def received_client_receipt(self, room_id, receipt_type, user_id, event_id): """Called when a client tells us a local user has read up to the given event_id in the room. """ @@ -118,24 +118,11 @@ class ReceiptsHandler(BaseHandler): data={"ts": int(self.clock.time_msec())}, ) - is_new = yield self._handle_new_receipts([receipt]) + is_new = await self._handle_new_receipts([receipt]) if not is_new: return - yield self.federation.send_read_receipt(receipt) - - @defer.inlineCallbacks - def get_receipts_for_room(self, room_id, to_key): - """Gets all receipts for a room, upto the given key. - """ - result = yield self.store.get_linearized_receipts_for_room( - room_id, to_key=to_key - ) - - if not result: - return [] - - return result + await self.federation.send_read_receipt(receipt) class ReceiptEventSource(object): diff --git a/synapse/rest/client/v2_alpha/read_marker.py b/synapse/rest/client/v2_alpha/read_marker.py index b3bf8567e1..67cbc37312 100644 --- a/synapse/rest/client/v2_alpha/read_marker.py +++ b/synapse/rest/client/v2_alpha/read_marker.py @@ -15,8 +15,6 @@ import logging -from twisted.internet import defer - from synapse.http.servlet import RestServlet, parse_json_object_from_request from ._base import client_patterns @@ -34,17 +32,16 @@ class ReadMarkerRestServlet(RestServlet): self.read_marker_handler = hs.get_read_marker_handler() self.presence_handler = hs.get_presence_handler() - @defer.inlineCallbacks - def on_POST(self, request, room_id): - requester = yield self.auth.get_user_by_req(request) + async def on_POST(self, request, room_id): + requester = await self.auth.get_user_by_req(request) - yield self.presence_handler.bump_presence_active_time(requester.user) + await self.presence_handler.bump_presence_active_time(requester.user) body = parse_json_object_from_request(request) read_event_id = body.get("m.read", None) if read_event_id: - yield self.receipts_handler.received_client_receipt( + await self.receipts_handler.received_client_receipt( room_id, "m.read", user_id=requester.user.to_string(), @@ -53,7 +50,7 @@ class ReadMarkerRestServlet(RestServlet): read_marker_event_id = body.get("m.fully_read", None) if read_marker_event_id: - yield self.read_marker_handler.received_client_read_marker( + await self.read_marker_handler.received_client_read_marker( room_id, user_id=requester.user.to_string(), event_id=read_marker_event_id, diff --git a/synapse/rest/client/v2_alpha/receipts.py b/synapse/rest/client/v2_alpha/receipts.py index 0dab03d227..92555bd4a9 100644 --- a/synapse/rest/client/v2_alpha/receipts.py +++ b/synapse/rest/client/v2_alpha/receipts.py @@ -15,8 +15,6 @@ import logging -from twisted.internet import defer - from synapse.api.errors import SynapseError from synapse.http.servlet import RestServlet @@ -39,16 +37,15 @@ class ReceiptRestServlet(RestServlet): self.receipts_handler = hs.get_receipts_handler() self.presence_handler = hs.get_presence_handler() - @defer.inlineCallbacks - def on_POST(self, request, room_id, receipt_type, event_id): - requester = yield self.auth.get_user_by_req(request) + async def on_POST(self, request, room_id, receipt_type, event_id): + requester = await self.auth.get_user_by_req(request) if receipt_type != "m.read": raise SynapseError(400, "Receipt type must be 'm.read'") - yield self.presence_handler.bump_presence_active_time(requester.user) + await self.presence_handler.bump_presence_active_time(requester.user) - yield self.receipts_handler.received_client_receipt( + await self.receipts_handler.received_client_receipt( room_id, receipt_type, user_id=requester.user.to_string(), event_id=event_id ) diff --git a/synapse/storage/data_stores/main/events.py b/synapse/storage/data_stores/main/events.py index 03b5111c5d..067e77ae00 100644 --- a/synapse/storage/data_stores/main/events.py +++ b/synapse/storage/data_stores/main/events.py @@ -2439,12 +2439,11 @@ class EventsStore( logger.info("[purge] done") - @defer.inlineCallbacks - def is_event_after(self, event_id1, event_id2): + async def is_event_after(self, event_id1, event_id2): """Returns True if event_id1 is after event_id2 in the stream """ - to_1, so_1 = yield self._get_event_ordering(event_id1) - to_2, so_2 = yield self._get_event_ordering(event_id2) + to_1, so_1 = await self._get_event_ordering(event_id1) + to_2, so_2 = await self._get_event_ordering(event_id2) return (to_1, so_1) > (to_2, so_2) @cachedInlineCallbacks(max_entries=5000) From 7dd7a385f9c1eca2369840240a49263668027cde Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 29 Oct 2019 15:09:48 +0000 Subject: [PATCH 0354/1623] Newsfile --- changelog.d/6280.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6280.misc diff --git a/changelog.d/6280.misc b/changelog.d/6280.misc new file mode 100644 index 0000000000..96a0eb21b2 --- /dev/null +++ b/changelog.d/6280.misc @@ -0,0 +1 @@ +Port receipt and read markers to async/wait. From d79151921ac6b1770533eef098f78db77ea6d528 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 29 Oct 2019 15:39:44 +0000 Subject: [PATCH 0355/1623] Fix CI for synapse_port_db (#6276) * Don't use a virtualenv * Generate the server's signing key to allow it to start * Add signing key paths to CI configuration files * Use a Python script to create the postgresql database * Improve logging --- .buildkite/postgres-config.yaml | 2 ++ .buildkite/scripts/create_postgres_db.py | 36 ++++++++++++++++++++++ .buildkite/scripts/test_synapse_port_db.sh | 15 ++++++--- .buildkite/sqlite-config.yaml | 2 ++ changelog.d/6276.misc | 1 + 5 files changed, 52 insertions(+), 4 deletions(-) create mode 100755 .buildkite/scripts/create_postgres_db.py create mode 100644 changelog.d/6276.misc diff --git a/.buildkite/postgres-config.yaml b/.buildkite/postgres-config.yaml index 23db43fac9..a35fec394d 100644 --- a/.buildkite/postgres-config.yaml +++ b/.buildkite/postgres-config.yaml @@ -3,6 +3,8 @@ # CI's Docker setup at the point where this file is considered. server_name: "test" +signing_key_path: "/src/.buildkite/test.signing.key" + report_stats: false database: diff --git a/.buildkite/scripts/create_postgres_db.py b/.buildkite/scripts/create_postgres_db.py new file mode 100755 index 0000000000..df6082b0ac --- /dev/null +++ b/.buildkite/scripts/create_postgres_db.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright 2019 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +from synapse.storage.engines import create_engine + +logger = logging.getLogger("create_postgres_db") + +if __name__ == "__main__": + # Create a PostgresEngine. + db_engine = create_engine({"name": "psycopg2", "args": {}}) + + # Connect to postgres to create the base database. + # We use "postgres" as a database because it's bound to exist and the "synapse" one + # doesn't exist yet. + db_conn = db_engine.module.connect( + user="postgres", host="postgres", password="postgres", dbname="postgres" + ) + db_conn.autocommit = True + cur = db_conn.cursor() + cur.execute("CREATE DATABASE synapse;") + cur.close() + db_conn.close() diff --git a/.buildkite/scripts/test_synapse_port_db.sh b/.buildkite/scripts/test_synapse_port_db.sh index 7defd47bc6..9ed2177635 100755 --- a/.buildkite/scripts/test_synapse_port_db.sh +++ b/.buildkite/scripts/test_synapse_port_db.sh @@ -9,9 +9,7 @@ set -xe cd `dirname $0`/../.. -# Create a virtualenv and use it. -virtualenv env -source env/bin/activate +echo "--- Install dependencies" # Install dependencies for this test. pip install psycopg2 coverage coverage-enable-subprocess @@ -19,11 +17,20 @@ pip install psycopg2 coverage coverage-enable-subprocess # Install Synapse itself. This won't update any libraries. pip install -e . +echo "--- Generate the signing key" + +# Generate the server's signing key. +python -m synapse.app.homeserver --generate-keys -c .buildkite/sqlite-config.yaml + +echo "--- Prepare the databases" + # Make sure the SQLite3 database is using the latest schema and has no pending background update. scripts-dev/update_database --database-config .buildkite/sqlite-config.yaml # Create the PostgreSQL database. -PGPASSWORD=postgres createdb -h postgres -U postgres synapse +./.buildkite/scripts/create_postgres_db.py + +echo "+++ Run synapse_port_db" # Run the script coverage run scripts/synapse_port_db --sqlite-database .buildkite/test_db.db --postgres-config .buildkite/postgres-config.yaml diff --git a/.buildkite/sqlite-config.yaml b/.buildkite/sqlite-config.yaml index 56503cc4ce..635b921764 100644 --- a/.buildkite/sqlite-config.yaml +++ b/.buildkite/sqlite-config.yaml @@ -3,6 +3,8 @@ # schema and run background updates on it. server_name: "test" +signing_key_path: "/src/.buildkite/test.signing.key" + report_stats: false database: diff --git a/changelog.d/6276.misc b/changelog.d/6276.misc new file mode 100644 index 0000000000..4a4428251e --- /dev/null +++ b/changelog.d/6276.misc @@ -0,0 +1 @@ +Add a CI job to test the `synapse_port_db` script. From a287f1e80475e0c33743ed2990fd9d72572bda68 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 29 Oct 2019 16:36:46 +0000 Subject: [PATCH 0356/1623] Don't return coroutines --- synapse/federation/federation_server.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index 15c1fa0a51..7c331753ad 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -809,7 +809,7 @@ class ReplicationFederationHandlerRegistry(FederationHandlerRegistry): super(ReplicationFederationHandlerRegistry, self).__init__() - def on_edu(self, edu_type, origin, content): + async def on_edu(self, edu_type, origin, content): """Overrides FederationHandlerRegistry """ if not self.config.use_presence and edu_type == "m.presence": @@ -817,17 +817,17 @@ class ReplicationFederationHandlerRegistry(FederationHandlerRegistry): handler = self.edu_handlers.get(edu_type) if handler: - return super(ReplicationFederationHandlerRegistry, self).on_edu( + return await super(ReplicationFederationHandlerRegistry, self).on_edu( edu_type, origin, content ) - return self._send_edu(edu_type=edu_type, origin=origin, content=content) + return await self._send_edu(edu_type=edu_type, origin=origin, content=content) - def on_query(self, query_type, args): + async def on_query(self, query_type, args): """Overrides FederationHandlerRegistry """ handler = self.query_handlers.get(query_type) if handler: - return handler(args) + return await handler(args) - return self._get_query_client(query_type=query_type, args=args) + return await self._get_query_client(query_type=query_type, args=args) From 47f767269c51773a1cb24e65f958a891e00c7c06 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 29 Oct 2019 16:56:22 +0000 Subject: [PATCH 0357/1623] Add database table for keeping track of labels on events --- .../main/schema/delta/56/event_labels.sql | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 synapse/storage/data_stores/main/schema/delta/56/event_labels.sql diff --git a/synapse/storage/data_stores/main/schema/delta/56/event_labels.sql b/synapse/storage/data_stores/main/schema/delta/56/event_labels.sql new file mode 100644 index 0000000000..323d797419 --- /dev/null +++ b/synapse/storage/data_stores/main/schema/delta/56/event_labels.sql @@ -0,0 +1,20 @@ +/* Copyright 2019 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +CREATE TABLE IF NOT EXISTS event_labels ( + event_id TEXT, + label TEXT, + PRIMARY KEY(event_id, label) +); \ No newline at end of file From 213d7eb22739c66ca3af0c33be4377906f35b426 Mon Sep 17 00:00:00 2001 From: Anton Lazarev Date: Wed, 30 Oct 2019 00:30:04 -0700 Subject: [PATCH 0358/1623] Clarify environment variable usage when running in Docker (#6181) --- docker/start.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docker/start.py b/docker/start.py index e41ea20e70..6e1cb807a1 100755 --- a/docker/start.py +++ b/docker/start.py @@ -217,8 +217,9 @@ def main(args, environ): # backwards-compatibility generate-a-config-on-the-fly mode if "SYNAPSE_CONFIG_PATH" in environ: error( - "SYNAPSE_SERVER_NAME and SYNAPSE_CONFIG_PATH are mutually exclusive " - "except in `generate` or `migrate_config` mode." + "SYNAPSE_SERVER_NAME can only be combined with SYNAPSE_CONFIG_PATH " + "in `generate` or `migrate_config` mode. To start synapse using a " + "config file, unset the SYNAPSE_SERVER_NAME environment variable." ) config_path = "/compiled/homeserver.yaml" From b39ca49db167e814d7848c6a4872c8b65ec03ff1 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Wed, 30 Oct 2019 11:00:15 +0000 Subject: [PATCH 0359/1623] Handle FileNotFound error in checking git repository version (#6284) --- changelog.d/6284.bugfix | 1 + synapse/util/versionstring.py | 10 ++++++---- 2 files changed, 7 insertions(+), 4 deletions(-) create mode 100644 changelog.d/6284.bugfix diff --git a/changelog.d/6284.bugfix b/changelog.d/6284.bugfix new file mode 100644 index 0000000000..cf15053d2d --- /dev/null +++ b/changelog.d/6284.bugfix @@ -0,0 +1 @@ +Prevent errors from appearing on Synapse startup if `git` is not installed. \ No newline at end of file diff --git a/synapse/util/versionstring.py b/synapse/util/versionstring.py index fa404b9d75..ab7d03af3a 100644 --- a/synapse/util/versionstring.py +++ b/synapse/util/versionstring.py @@ -42,6 +42,7 @@ def get_version_string(module): try: null = open(os.devnull, "w") cwd = os.path.dirname(os.path.abspath(module.__file__)) + try: git_branch = ( subprocess.check_output( @@ -51,7 +52,8 @@ def get_version_string(module): .decode("ascii") ) git_branch = "b=" + git_branch - except subprocess.CalledProcessError: + except (subprocess.CalledProcessError, FileNotFoundError): + # FileNotFoundError can arise when git is not installed git_branch = "" try: @@ -63,7 +65,7 @@ def get_version_string(module): .decode("ascii") ) git_tag = "t=" + git_tag - except subprocess.CalledProcessError: + except (subprocess.CalledProcessError, FileNotFoundError): git_tag = "" try: @@ -74,7 +76,7 @@ def get_version_string(module): .strip() .decode("ascii") ) - except subprocess.CalledProcessError: + except (subprocess.CalledProcessError, FileNotFoundError): git_commit = "" try: @@ -89,7 +91,7 @@ def get_version_string(module): ) git_dirty = "dirty" if is_dirty else "" - except subprocess.CalledProcessError: + except (subprocess.CalledProcessError, FileNotFoundError): git_dirty = "" if git_branch or git_tag or git_commit or git_dirty: From 9178ac1b6a79727aac3859a2ff2ac91a27da5bd4 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Wed, 30 Oct 2019 11:07:18 +0000 Subject: [PATCH 0360/1623] Remove redundant arguments to CI's flake8 (#6277) --- changelog.d/6277.misc | 1 + tox.ini | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/6277.misc diff --git a/changelog.d/6277.misc b/changelog.d/6277.misc new file mode 100644 index 0000000000..490713577f --- /dev/null +++ b/changelog.d/6277.misc @@ -0,0 +1 @@ +Remove redundant CLI parameters on CI's `flake8` step. \ No newline at end of file diff --git a/tox.ini b/tox.ini index e3a53f340a..b381fbe06d 100644 --- a/tox.ini +++ b/tox.ini @@ -117,7 +117,7 @@ deps = black==19.3b0 # We pin so that our tests don't start failing on new releases of black. commands = python -m black --check --diff . - /bin/sh -c "flake8 synapse tests scripts scripts-dev scripts/hash_password scripts/register_new_matrix_user scripts/synapse_port_db synctl {env:PEP8SUFFIX:}" + /bin/sh -c "flake8 synapse tests scripts scripts-dev synctl {env:PEP8SUFFIX:}" {toxinidir}/scripts-dev/config-lint.sh [testenv:check_isort] From 46c12918add132d8d0cbb808b499c815e2745f72 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Wed, 30 Oct 2019 11:07:42 +0000 Subject: [PATCH 0361/1623] Fix typo in domain name in account_threepid_delegates config option (#6273) --- changelog.d/6273.doc | 1 + docs/sample_config.yaml | 2 +- synapse/config/registration.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changelog.d/6273.doc diff --git a/changelog.d/6273.doc b/changelog.d/6273.doc new file mode 100644 index 0000000000..21a41d987d --- /dev/null +++ b/changelog.d/6273.doc @@ -0,0 +1 @@ +Fix a small typo in `account_threepid_delegates` configuration option. \ No newline at end of file diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 6c81c0db75..d2f4aff826 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -955,7 +955,7 @@ uploads_path: "DATADIR/uploads" # If a delegate is specified, the config option public_baseurl must also be filled out. # account_threepid_delegates: - #email: https://example.com # Delegate email sending to example.org + #email: https://example.com # Delegate email sending to example.com #msisdn: http://localhost:8090 # Delegate SMS sending to this local process # Users who register on this homeserver will automatically be joined diff --git a/synapse/config/registration.py b/synapse/config/registration.py index ab41623b2b..1f6dac69da 100644 --- a/synapse/config/registration.py +++ b/synapse/config/registration.py @@ -300,7 +300,7 @@ class RegistrationConfig(Config): # If a delegate is specified, the config option public_baseurl must also be filled out. # account_threepid_delegates: - #email: https://example.com # Delegate email sending to example.org + #email: https://example.com # Delegate email sending to example.com #msisdn: http://localhost:8090 # Delegate SMS sending to this local process # Users who register on this homeserver will automatically be joined From 7955abeaac54e97332bd42186299e648bb3ace6c Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Wed, 30 Oct 2019 11:16:19 +0000 Subject: [PATCH 0362/1623] Fix small typo in comment (#6269) --- changelog.d/6269.misc | 1 + synapse/federation/federation_server.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/6269.misc diff --git a/changelog.d/6269.misc b/changelog.d/6269.misc new file mode 100644 index 0000000000..9fd333cc89 --- /dev/null +++ b/changelog.d/6269.misc @@ -0,0 +1 @@ +Fix incorrect comment regarding the functionality of an `if` statement. \ No newline at end of file diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index 7c331753ad..d5a19764d2 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -145,7 +145,7 @@ class FederationServer(FederationBase): logger.debug("[%s] Transaction is new", transaction.transaction_id) - # Reject if PDU count > 50 and EDU count > 100 + # Reject if PDU count > 50 or EDU count > 100 if len(transaction.pdus) > 50 or ( hasattr(transaction, "edus") and len(transaction.edus) > 100 ): From 2cab02f9d123924a6ccbf8e59b7e973f3c0a3d26 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Wed, 30 Oct 2019 11:17:14 +0000 Subject: [PATCH 0363/1623] Update CI to run isort on scripts and scripts-dev (#6270) --- changelog.d/6270.misc | 1 + scripts-dev/update_database | 3 +-- tox.ini | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 changelog.d/6270.misc diff --git a/changelog.d/6270.misc b/changelog.d/6270.misc new file mode 100644 index 0000000000..d1c5811323 --- /dev/null +++ b/changelog.d/6270.misc @@ -0,0 +1 @@ +Update CI to run `isort` over the `scripts` and `scripts-dev` directories. \ No newline at end of file diff --git a/scripts-dev/update_database b/scripts-dev/update_database index 10166583e1..27a1ad1e7e 100755 --- a/scripts-dev/update_database +++ b/scripts-dev/update_database @@ -25,8 +25,8 @@ from twisted.internet import defer, reactor from synapse.config.homeserver import HomeServerConfig from synapse.metrics.background_process_metrics import run_as_background_process from synapse.server import HomeServer -from synapse.storage.engines import create_engine from synapse.storage import DataStore +from synapse.storage.engines import create_engine from synapse.storage.prepare_database import prepare_database logger = logging.getLogger("update_database") @@ -122,4 +122,3 @@ if __name__ == "__main__": )) reactor.run() - diff --git a/tox.ini b/tox.ini index b381fbe06d..50b6afe611 100644 --- a/tox.ini +++ b/tox.ini @@ -123,7 +123,7 @@ commands = [testenv:check_isort] skip_install = True deps = isort -commands = /bin/sh -c "isort -c -df -sp setup.cfg -rc synapse tests" +commands = /bin/sh -c "isort -c -df -sp setup.cfg -rc synapse tests scripts-dev scripts" [testenv:check-newsfragment] skip_install = True From a2276d4d3ca72896582ef24d01fdff6c01e38689 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Wed, 30 Oct 2019 11:28:48 +0000 Subject: [PATCH 0364/1623] Fix log line that was printing undefined value (#6278) --- changelog.d/6278.bugfix | 1 + synapse/handlers/federation.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/6278.bugfix diff --git a/changelog.d/6278.bugfix b/changelog.d/6278.bugfix new file mode 100644 index 0000000000..c107270461 --- /dev/null +++ b/changelog.d/6278.bugfix @@ -0,0 +1 @@ +Fix exception when remote servers attempt to join a room that they're not allowed to join. diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 488058fe68..2da520e6e8 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1250,7 +1250,7 @@ class FederationHandler(BaseHandler): builder=builder ) except AuthError as e: - logger.warn("Failed to create join %r because %s", event, e) + logger.warn("Failed to create join to %s because %s", room_id, e) raise e event_allowed = yield self.third_party_event_rules.check_event_allowed( From 326b3dace77aeb36e516ea9b04ba1baa171bcb47 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 30 Oct 2019 11:35:46 +0000 Subject: [PATCH 0365/1623] Make ObservableDeferred.observe() always return deferred. This makes it easier to use in an async/await world. Also fixes a bug where cache descriptors would occaisonally return a raw value rather than a deferred. --- synapse/util/async_helpers.py | 7 ++----- tests/storage/test__base.py | 2 +- tests/util/caches/test_descriptors.py | 4 ++-- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/synapse/util/async_helpers.py b/synapse/util/async_helpers.py index 7659eaeb42..fd75ba27ad 100644 --- a/synapse/util/async_helpers.py +++ b/synapse/util/async_helpers.py @@ -86,11 +86,8 @@ class ObservableDeferred(object): deferred.addCallbacks(callback, errback) - def observe(self): + def observe(self) -> defer.Deferred: """Observe the underlying deferred. - - Can return either a deferred if the underlying deferred is still pending - (or has failed), or the actual value. Callers may need to use maybeDeferred. """ if not self._result: d = defer.Deferred() @@ -105,7 +102,7 @@ class ObservableDeferred(object): return d else: success, res = self._result - return res if success else defer.fail(res) + return defer.succeed(res) if success else defer.fail(res) def observers(self): return self._observers diff --git a/tests/storage/test__base.py b/tests/storage/test__base.py index dd49a14524..9b81b536f5 100644 --- a/tests/storage/test__base.py +++ b/tests/storage/test__base.py @@ -197,7 +197,7 @@ class CacheDecoratorTestCase(unittest.TestCase): a.func.prefill(("foo",), ObservableDeferred(d)) - self.assertEquals(a.func("foo"), d.result) + self.assertEquals(a.func("foo").result, d.result) self.assertEquals(callcount[0], 0) @defer.inlineCallbacks diff --git a/tests/util/caches/test_descriptors.py b/tests/util/caches/test_descriptors.py index f907903511..39e360fe24 100644 --- a/tests/util/caches/test_descriptors.py +++ b/tests/util/caches/test_descriptors.py @@ -310,14 +310,14 @@ class DescriptorTestCase(unittest.TestCase): obj.mock.return_value = ["spam", "eggs"] r = obj.fn(1, 2) - self.assertEqual(r, ["spam", "eggs"]) + self.assertEqual(r.result, ["spam", "eggs"]) obj.mock.assert_called_once_with(1, 2) obj.mock.reset_mock() # a call with different params should call the mock again obj.mock.return_value = ["chips"] r = obj.fn(1, 3) - self.assertEqual(r, ["chips"]) + self.assertEqual(r.result, ["chips"]) obj.mock.assert_called_once_with(1, 3) obj.mock.reset_mock() From 1de28183cb92a2967d527c175123251514f58e69 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 30 Oct 2019 11:37:56 +0000 Subject: [PATCH 0366/1623] Newsfile --- changelog.d/6291.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6291.misc diff --git a/changelog.d/6291.misc b/changelog.d/6291.misc new file mode 100644 index 0000000000..7b1bb4b679 --- /dev/null +++ b/changelog.d/6291.misc @@ -0,0 +1 @@ +Change cache descriptors to always return deferreds. From 7e179599848a1005f753a1ab58953107fc2540df Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Wed, 30 Oct 2019 11:37:56 +0000 Subject: [PATCH 0367/1623] Update email section of INSTALL.md about account_threepid_delegates (#6272) --- INSTALL.md | 16 +++++++++------- changelog.d/6272.doc | 1 + 2 files changed, 10 insertions(+), 7 deletions(-) create mode 100644 changelog.d/6272.doc diff --git a/INSTALL.md b/INSTALL.md index 69e423923b..e7b429c05d 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -413,16 +413,18 @@ For a more detailed guide to configuring your server for federation, see ## Email -It is desirable for Synapse to have the capability to send email. For example, -this is required to support the 'password reset' feature. +It is desirable for Synapse to have the capability to send email. This allows +Synapse to send password reset emails, send verifications when an email address +is added to a user's account, and send email notifications to users when they +receive new messages. To configure an SMTP server for Synapse, modify the configuration section -headed ``email``, and be sure to have at least the ``smtp_host``, ``smtp_port`` -and ``notif_from`` fields filled out. You may also need to set ``smtp_user``, -``smtp_pass``, and ``require_transport_security``. +headed `email`, and be sure to have at least the `smtp_host`, `smtp_port` +and `notif_from` fields filled out. You may also need to set `smtp_user`, +`smtp_pass`, and `require_transport_security`. -If Synapse is not configured with an SMTP server, password reset via email will - be disabled by default. +If email is not configured, password reset, registration and notifications via +email will be disabled. ## Registering a user diff --git a/changelog.d/6272.doc b/changelog.d/6272.doc new file mode 100644 index 0000000000..232180bcdc --- /dev/null +++ b/changelog.d/6272.doc @@ -0,0 +1 @@ +Update `INSTALL.md` Email section to talk about `account_threepid_delegates`. \ No newline at end of file From 6e677403b7c71511374b1532b56e0c411840f87b Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 30 Oct 2019 11:52:04 +0000 Subject: [PATCH 0368/1623] Clarify docstring --- synapse/util/async_helpers.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/synapse/util/async_helpers.py b/synapse/util/async_helpers.py index fd75ba27ad..b60a604474 100644 --- a/synapse/util/async_helpers.py +++ b/synapse/util/async_helpers.py @@ -88,6 +88,10 @@ class ObservableDeferred(object): def observe(self) -> defer.Deferred: """Observe the underlying deferred. + + This returns a brand new deferred that is resolved when the underlying + deferred is resolved. Interacting with the returned deferred does not + effect the underdlying deferred. """ if not self._result: d = defer.Deferred() From 9677613e9cc52af4a31e5f706ad0bec14ca645de Mon Sep 17 00:00:00 2001 From: Yash Jipkate <34203227+YashJipkate@users.noreply.github.com> Date: Wed, 30 Oct 2019 18:00:20 +0530 Subject: [PATCH 0369/1623] Modify doc to update Google ReCaptcha terms (#6257) --- changelog.d/6257.doc | 1 + docs/CAPTCHA_SETUP.md | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 changelog.d/6257.doc diff --git a/changelog.d/6257.doc b/changelog.d/6257.doc new file mode 100644 index 0000000000..e985afde0e --- /dev/null +++ b/changelog.d/6257.doc @@ -0,0 +1 @@ +Modify CAPTCHA_SETUP.md to update the terms `private key` and `public key` to `secret key` and `site key` respectively. Contributed by Yash Jipkate. diff --git a/docs/CAPTCHA_SETUP.md b/docs/CAPTCHA_SETUP.md index 5f9057530b..331e5d059a 100644 --- a/docs/CAPTCHA_SETUP.md +++ b/docs/CAPTCHA_SETUP.md @@ -4,7 +4,7 @@ The captcha mechanism used is Google's ReCaptcha. This requires API keys from Go ## Getting keys -Requires a public/private key pair from: +Requires a site/secret key pair from: @@ -15,8 +15,8 @@ Must be a reCAPTCHA v2 key using the "I'm not a robot" Checkbox option The keys are a config option on the home server config. If they are not visible, you can generate them via `--generate-config`. Set the following value: - recaptcha_public_key: YOUR_PUBLIC_KEY - recaptcha_private_key: YOUR_PRIVATE_KEY + recaptcha_public_key: YOUR_SITE_KEY + recaptcha_private_key: YOUR_SECRET_KEY In addition, you MUST enable captchas via: From a8d16f6c00d5adb204af5fa73ffaea40eea4b632 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 30 Oct 2019 13:33:38 +0000 Subject: [PATCH 0370/1623] Review comments --- synapse/server.py | 5 ++--- synapse/storage/__init__.py | 4 ++-- synapse/storage/data_stores/main/events.py | 19 ++++++++++++++----- synapse/storage/persist_events.py | 13 ++++++++++--- tests/crypto/test_keyring.py | 4 ++-- tests/test_federation.py | 11 +++++++---- 6 files changed, 37 insertions(+), 19 deletions(-) diff --git a/synapse/server.py b/synapse/server.py index 54a7f4aa5f..0b81af646c 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -226,7 +226,6 @@ class HomeServer(object): self.admin_redaction_ratelimiter = Ratelimiter() self.registration_ratelimiter = Ratelimiter() - self.datastore = None self.datastores = None # Other kwargs are explicit dependencies @@ -236,8 +235,8 @@ class HomeServer(object): def setup(self): logger.info("Setting up.") with self.get_db_conn() as conn: - self.datastore = self.DATASTORE_CLASS(conn, self) - self.datastores = DataStores(self.datastore, conn, self) + datastore = self.DATASTORE_CLASS(conn, self) + self.datastores = DataStores(datastore, conn, self) conn.commit() logger.info("Finished setting up.") diff --git a/synapse/storage/__init__.py b/synapse/storage/__init__.py index a58187a76f..a6429d17ed 100644 --- a/synapse/storage/__init__.py +++ b/synapse/storage/__init__.py @@ -29,7 +29,7 @@ stored in `synapse.storage.schema`. from synapse.storage.data_stores import DataStores from synapse.storage.data_stores.main import DataStore -from synapse.storage.persist_events import EventsPersistenceStore +from synapse.storage.persist_events import EventsPersistenceStorage __all__ = ["DataStores", "DataStore"] @@ -44,7 +44,7 @@ class Storage(object): # interfaces. self.main = stores.main - self.persistence = EventsPersistenceStore(hs, stores) + self.persistence = EventsPersistenceStorage(hs, stores) def are_all_users_on_domain(txn, database_engine, domain): diff --git a/synapse/storage/data_stores/main/events.py b/synapse/storage/data_stores/main/events.py index 6304531cd5..813f34528c 100644 --- a/synapse/storage/data_stores/main/events.py +++ b/synapse/storage/data_stores/main/events.py @@ -146,7 +146,7 @@ class EventsStore( @_retry_on_integrity_error @defer.inlineCallbacks - def _persist_events( + def _persist_events_and_state_updates( self, events_and_contexts, current_state_for_room, @@ -155,18 +155,27 @@ class EventsStore( backfilled=False, delete_existing=False, ): - """Persist events to db + """Persist a set of events alongside updates to the current state and + forward extremities tables. Args: events_and_contexts (list[(EventBase, EventContext)]): - backfilled (bool): + current_state_for_room (dict[str, dict]): Map from room_id to the + current state of the room based on forward extremities + state_delta_for_room (dict[str, tuple]): Map from room_id to tuple + of `(to_delete, to_insert)` where to_delete is a list + of type/state keys to remove from current state, and to_insert + is a map (type,key)->event_id giving the state delta in each + room. + new_forward_extremities (dict[str, list[str]]): Map from room_id + to list of event IDs that are the new forward extremities of + the room. + backfilled (bool) delete_existing (bool): Returns: Deferred: resolves when the events have been persisted """ - if not events_and_contexts: - return # We want to calculate the stream orderings as late as possible, as # we only notify after all events with a lesser stream ordering have diff --git a/synapse/storage/persist_events.py b/synapse/storage/persist_events.py index 9a63953d4d..cf66225574 100644 --- a/synapse/storage/persist_events.py +++ b/synapse/storage/persist_events.py @@ -171,7 +171,13 @@ class _EventPeristenceQueue(object): pass -class EventsPersistenceStore(object): +class EventsPersistenceStorage(object): + """High level interface for handling persisting newly received events. + + Takes care of batching up events by room, and calculating the necessary + current state and forward extremity changes. + """ + def __init__(self, hs, stores: DataStores): # We ultimately want to split out the state store from the main store, # so we use separate variables here even though they point to the same @@ -257,7 +263,8 @@ class EventsPersistenceStore(object): def _persist_events( self, events_and_contexts, backfilled=False, delete_existing=False ): - """Persist events to db + """Calculates the change to current state and forward extremities, and + persists the given events and with those updates. Args: events_and_contexts (list[(EventBase, EventContext)]): @@ -399,7 +406,7 @@ class EventsPersistenceStore(object): if current_state is not None: current_state_for_room[room_id] = current_state - yield self.main_store._persist_events( + yield self.main_store._persist_events_and_state_updates( chunk, current_state_for_room=current_state_for_room, state_delta_for_room=state_delta_for_room, diff --git a/tests/crypto/test_keyring.py b/tests/crypto/test_keyring.py index c4f0bbd3dd..8efd39c7f7 100644 --- a/tests/crypto/test_keyring.py +++ b/tests/crypto/test_keyring.py @@ -178,7 +178,7 @@ class KeyringTestCase(unittest.HomeserverTestCase): kr = keyring.Keyring(self.hs) key1 = signedjson.key.generate_signing_key(1) - r = self.hs.datastore.store_server_verify_keys( + r = self.hs.get_datastore().store_server_verify_keys( "server9", time.time() * 1000, [("server9", get_key_id(key1), FetchKeyResult(get_verify_key(key1), 1000))], @@ -209,7 +209,7 @@ class KeyringTestCase(unittest.HomeserverTestCase): ) key1 = signedjson.key.generate_signing_key(1) - r = self.hs.datastore.store_server_verify_keys( + r = self.hs.get_datastore().store_server_verify_keys( "server9", time.time() * 1000, [("server9", get_key_id(key1), FetchKeyResult(get_verify_key(key1), None))], diff --git a/tests/test_federation.py b/tests/test_federation.py index a73f18f88e..d1acb16f30 100644 --- a/tests/test_federation.py +++ b/tests/test_federation.py @@ -36,7 +36,8 @@ class MessageAcceptTests(unittest.TestCase): # Figure out what the most recent event is most_recent = self.successResultOf( maybeDeferred( - self.homeserver.datastore.get_latest_event_ids_in_room, self.room_id + self.homeserver.get_datastore().get_latest_event_ids_in_room, + self.room_id, ) )[0] @@ -75,7 +76,8 @@ class MessageAcceptTests(unittest.TestCase): self.assertEqual( self.successResultOf( maybeDeferred( - self.homeserver.datastore.get_latest_event_ids_in_room, self.room_id + self.homeserver.get_datastore().get_latest_event_ids_in_room, + self.room_id, ) )[0], "$join:test.serv", @@ -97,7 +99,8 @@ class MessageAcceptTests(unittest.TestCase): # Figure out what the most recent event is most_recent = self.successResultOf( maybeDeferred( - self.homeserver.datastore.get_latest_event_ids_in_room, self.room_id + self.homeserver.get_datastore().get_latest_event_ids_in_room, + self.room_id, ) )[0] @@ -137,6 +140,6 @@ class MessageAcceptTests(unittest.TestCase): # Make sure the invalid event isn't there extrem = maybeDeferred( - self.homeserver.datastore.get_latest_event_ids_in_room, self.room_id + self.homeserver.get_datastore().get_latest_event_ids_in_room, self.room_id ) self.assertEqual(self.successResultOf(extrem)[0], "$join:test.serv") From d78b1e339dd813214d8a8316c38a3be31ad8f132 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 30 Oct 2019 10:01:53 -0400 Subject: [PATCH 0371/1623] apply changes as a result of PR review --- synapse/handlers/e2e_keys.py | 22 +++--- synapse/storage/data_stores/main/devices.py | 79 ++++++++++----------- 2 files changed, 46 insertions(+), 55 deletions(-) diff --git a/synapse/handlers/e2e_keys.py b/synapse/handlers/e2e_keys.py index 4ab75a351e..0f320b3764 100644 --- a/synapse/handlers/e2e_keys.py +++ b/synapse/handlers/e2e_keys.py @@ -1072,7 +1072,7 @@ class SignatureListItem: class SigningKeyEduUpdater(object): - "Handles incoming signing key updates from federation and updates the DB" + """Handles incoming signing key updates from federation and updates the DB""" def __init__(self, hs, e2e_keys_handler): self.store = hs.get_datastore() @@ -1111,7 +1111,6 @@ class SigningKeyEduUpdater(object): self_signing_key = edu_content.pop("self_signing_key", None) if get_domain_from_id(user_id) != origin: - # TODO: Raise? logger.warning("Got signing key update edu for %r from %r", user_id, origin) return @@ -1122,7 +1121,7 @@ class SigningKeyEduUpdater(object): return self._pending_updates.setdefault(user_id, []).append( - (master_key, self_signing_key, edu_content) + (master_key, self_signing_key) ) yield self._handle_signing_key_updates(user_id) @@ -1147,22 +1146,21 @@ class SigningKeyEduUpdater(object): logger.info("pending updates: %r", pending_updates) - for master_key, self_signing_key, edu_content in pending_updates: + for master_key, self_signing_key in pending_updates: if master_key: yield self.store.set_e2e_cross_signing_key( user_id, "master", master_key ) - device_id = get_verify_key_from_cross_signing_key(master_key)[ - 1 - ].version - device_ids.append(device_id) + _, verify_key = get_verify_key_from_cross_signing_key(master_key) + # verify_key is a VerifyKey from signedjson, which uses + # .version to denote the portion of the key ID after the + # algorithm and colon, which is the device ID + device_ids.append(verify_key.version) if self_signing_key: yield self.store.set_e2e_cross_signing_key( user_id, "self_signing", self_signing_key ) - device_id = get_verify_key_from_cross_signing_key(self_signing_key)[ - 1 - ].version - device_ids.append(device_id) + _, verify_key = get_verify_key_from_cross_signing_key(self_signing_key) + device_ids.append(verify_key.version) yield device_handler.notify_device_update(user_id, device_ids) diff --git a/synapse/storage/data_stores/main/devices.py b/synapse/storage/data_stores/main/devices.py index 6ac165068e..0b12bc58c4 100644 --- a/synapse/storage/data_stores/main/devices.py +++ b/synapse/storage/data_stores/main/devices.py @@ -92,8 +92,12 @@ class DeviceWorkerStore(SQLBaseStore): @trace @defer.inlineCallbacks def get_devices_by_remote(self, destination, from_stream_id, limit): - """Get stream of updates to send to remote servers + """Get a stream of device updates to send to the given remote server. + Args: + destination (str): The host the device updates are intended for + from_stream_id (int): The minimum stream_id to filter updates by, exclusive + limit (int): Maximum number of device updates to return Returns: Deferred[tuple[int, list[tuple[string,dict]]]]: current stream id (ie, the stream id of the last update included in the @@ -131,7 +135,8 @@ class DeviceWorkerStore(SQLBaseStore): if not updates: return now_stream_id, [] - # get the cross-signing keys of the users the list + # get the cross-signing keys of the users in the list, so that we can + # determine which of the device changes were cross-signing keys users = set(r[0] for r in updates) master_key_by_user = {} self_signing_key_by_user = {} @@ -141,9 +146,12 @@ class DeviceWorkerStore(SQLBaseStore): key_id, verify_key = get_verify_key_from_cross_signing_key( cross_signing_key ) + # verify_key is a VerifyKey from signedjson, which uses + # .version to denote the portion of the key ID after the + # algorithm and colon, which is the device ID master_key_by_user[user] = { "key_info": cross_signing_key, - "pubkey": verify_key.version, + "device_id": verify_key.version, } cross_signing_key = yield self.get_e2e_cross_signing_key( @@ -155,7 +163,7 @@ class DeviceWorkerStore(SQLBaseStore): ) self_signing_key_by_user[user] = { "key_info": cross_signing_key, - "pubkey": verify_key.version, + "device_id": verify_key.version, } # if we have exceeded the limit, we need to exclude any results with the @@ -182,69 +190,54 @@ class DeviceWorkerStore(SQLBaseStore): # context which created the Edu. query_map = {} - for update in updates: - if stream_id_cutoff is not None and update[2] >= stream_id_cutoff: + cross_signing_keys_by_user = {} + for user_id, device_id, update_stream_id, update_context in updates: + if stream_id_cutoff is not None and update_stream_id >= stream_id_cutoff: # Stop processing updates break - # skip over cross-signing keys if ( - update[0] in master_key_by_user - and update[1] == master_key_by_user[update[0]]["pubkey"] - ) or ( - update[0] in master_key_by_user - and update[1] == self_signing_key_by_user[update[0]]["pubkey"] + user_id in master_key_by_user + and device_id == master_key_by_user[user_id]["device_id"] ): - continue - - key = (update[0], update[1]) - - update_context = update[3] - update_stream_id = update[2] - - previous_update_stream_id, _ = query_map.get(key, (0, None)) - - if update_stream_id > previous_update_stream_id: - query_map[key] = (update_stream_id, update_context) - - # If we didn't find any updates with a stream_id lower than the cutoff, it - # means that there are more than limit updates all of which have the same - # steam_id. - - # figure out which cross-signing keys were changed by intersecting the - # update list with the master/self-signing key by user maps - cross_signing_keys_by_user = {} - for user_id, device_id, stream, _opentracing_context in updates: - if device_id == master_key_by_user.get(user_id, {}).get("pubkey", None): result = cross_signing_keys_by_user.setdefault(user_id, {}) result["master_key"] = master_key_by_user[user_id]["key_info"] - elif device_id == self_signing_key_by_user.get(user_id, {}).get( - "pubkey", None + elif ( + user_id in master_key_by_user + and device_id == self_signing_key_by_user[user_id]["device_id"] ): result = cross_signing_keys_by_user.setdefault(user_id, {}) result["self_signing_key"] = self_signing_key_by_user[user_id][ "key_info" ] + else: + key = (user_id, device_id) - cross_signing_results = [] + previous_update_stream_id, _ = query_map.get(key, (0, None)) - # add the updated cross-signing keys to the results list - for user_id, result in iteritems(cross_signing_keys_by_user): - result["user_id"] = user_id - # FIXME: switch to m.signing_key_update when MSC1756 is merged into the spec - cross_signing_results.append(("org.matrix.signing_key_update", result)) + if update_stream_id > previous_update_stream_id: + query_map[key] = (update_stream_id, update_context) + + # If we didn't find any updates with a stream_id lower than the cutoff, it + # means that there are more than limit updates all of which have the same + # steam_id. # That should only happen if a client is spamming the server with new # devices, in which case E2E isn't going to work well anyway. We'll just # skip that stream_id and return an empty list, and continue with the next # stream_id next time. - if not query_map and not cross_signing_results: + if not query_map and not cross_signing_keys_by_user: return stream_id_cutoff, [] results = yield self._get_device_update_edus_by_remote( destination, from_stream_id, query_map ) - results.extend(cross_signing_results) + + # add the updated cross-signing keys to the results list + for user_id, result in iteritems(cross_signing_keys_by_user): + result["user_id"] = user_id + # FIXME: switch to m.signing_key_update when MSC1756 is merged into the spec + results.append(("org.matrix.signing_key_update", result)) return now_stream_id, results From bc32f102cd1144923581771b0cc84ead4d99cefb Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 30 Oct 2019 10:07:36 -0400 Subject: [PATCH 0372/1623] black --- synapse/handlers/e2e_keys.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/synapse/handlers/e2e_keys.py b/synapse/handlers/e2e_keys.py index 0f320b3764..1ab471b3be 100644 --- a/synapse/handlers/e2e_keys.py +++ b/synapse/handlers/e2e_keys.py @@ -1160,7 +1160,9 @@ class SigningKeyEduUpdater(object): yield self.store.set_e2e_cross_signing_key( user_id, "self_signing", self_signing_key ) - _, verify_key = get_verify_key_from_cross_signing_key(self_signing_key) + _, verify_key = get_verify_key_from_cross_signing_key( + self_signing_key + ) device_ids.append(verify_key.version) yield device_handler.notify_device_update(user_id, device_ids) From fa0dcbc8fa396fa78aabc524728e08fd84b70a0c Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 29 Oct 2019 18:35:49 +0000 Subject: [PATCH 0373/1623] Store labels for new events --- synapse/api/constants.py | 3 +++ synapse/storage/data_stores/main/events.py | 20 +++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/synapse/api/constants.py b/synapse/api/constants.py index 312196675e..999ec02fd9 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -138,3 +138,6 @@ class LimitBlockingTypes(object): MONTHLY_ACTIVE_USER = "monthly_active_user" HS_DISABLED = "hs_disabled" + + +LabelsField = "org.matrix.labels" diff --git a/synapse/storage/data_stores/main/events.py b/synapse/storage/data_stores/main/events.py index 03b5111c5d..f80b5f1a3f 100644 --- a/synapse/storage/data_stores/main/events.py +++ b/synapse/storage/data_stores/main/events.py @@ -29,7 +29,7 @@ from prometheus_client import Counter, Histogram from twisted.internet import defer import synapse.metrics -from synapse.api.constants import EventTypes +from synapse.api.constants import EventTypes, LabelsField from synapse.api.errors import SynapseError from synapse.events import EventBase # noqa: F401 from synapse.events.snapshot import EventContext # noqa: F401 @@ -1490,6 +1490,11 @@ class EventsStore( self._handle_event_relations(txn, event) + # Store the labels for this event. + labels = event.content.get(LabelsField) + if labels: + self.insert_labels_for_event_txn(txn, event.event_id, labels) + # Insert into the room_memberships table. self._store_room_members_txn( txn, @@ -2477,6 +2482,19 @@ class EventsStore( get_all_updated_current_state_deltas_txn, ) + def insert_labels_for_event_txn(self, txn, event_id, labels): + return self._simple_insert_many_txn( + txn=txn, + table="event_labels", + values=[ + { + "event_id": event_id, + "label": label, + } + for label in labels + ], + ) + AllNewEventsResult = namedtuple( "AllNewEventsResult", From 5db03535d587f9a3dc460ad9c015ab6a35ff5730 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 30 Oct 2019 14:07:48 +0000 Subject: [PATCH 0374/1623] Add StateGroupStorage interface --- synapse/storage/__init__.py | 2 + synapse/storage/persist_events.py | 2 +- synapse/storage/state.py | 232 ++++++++++++++++++++++++++++++ 3 files changed, 235 insertions(+), 1 deletion(-) diff --git a/synapse/storage/__init__.py b/synapse/storage/__init__.py index a6429d17ed..0a1a8cc1e5 100644 --- a/synapse/storage/__init__.py +++ b/synapse/storage/__init__.py @@ -30,6 +30,7 @@ stored in `synapse.storage.schema`. from synapse.storage.data_stores import DataStores from synapse.storage.data_stores.main import DataStore from synapse.storage.persist_events import EventsPersistenceStorage +from synapse.storage.state import StateGroupStorage __all__ = ["DataStores", "DataStore"] @@ -45,6 +46,7 @@ class Storage(object): self.main = stores.main self.persistence = EventsPersistenceStorage(hs, stores) + self.state = StateGroupStorage(hs, stores) def are_all_users_on_domain(txn, database_engine, domain): diff --git a/synapse/storage/persist_events.py b/synapse/storage/persist_events.py index cf66225574..61dfca8fdc 100644 --- a/synapse/storage/persist_events.py +++ b/synapse/storage/persist_events.py @@ -550,7 +550,7 @@ class EventsPersistenceStorage(object): if missing_event_ids: # Now pull out the state groups for any missing events from DB - event_to_groups = yield self.state_store._get_state_group_for_events( + event_to_groups = yield self.main_store._get_state_group_for_events( missing_event_ids ) event_id_to_state_group.update(event_to_groups) diff --git a/synapse/storage/state.py b/synapse/storage/state.py index a2df8fa827..b382a06dcc 100644 --- a/synapse/storage/state.py +++ b/synapse/storage/state.py @@ -19,6 +19,8 @@ from six import iteritems, itervalues import attr +from twisted.internet import defer + from synapse.api.constants import EventTypes logger = logging.getLogger(__name__) @@ -322,3 +324,233 @@ class StateFilter(object): ) return member_filter, non_member_filter + + +class StateGroupStorage(object): + """High level interface to fetching state for event. + """ + + def __init__(self, hs, stores): + self.stores = stores + + def get_state_group_delta(self, state_group): + """Given a state group try to return a previous group and a delta between + the old and the new. + + Returns: + (prev_group, delta_ids), where both may be None. + """ + + return self.stores.main.get_state_group_delta(state_group) + + @defer.inlineCallbacks + def get_state_groups_ids(self, _room_id, event_ids): + """Get the event IDs of all the state for the state groups for the given events + + Args: + _room_id (str): id of the room for these events + event_ids (iterable[str]): ids of the events + + Returns: + Deferred[dict[int, dict[tuple[str, str], str]]]: + dict of state_group_id -> (dict of (type, state_key) -> event id) + """ + if not event_ids: + return {} + + event_to_groups = yield self.stores.main._get_state_group_for_events(event_ids) + + groups = set(itervalues(event_to_groups)) + group_to_state = yield self.stores.main._get_state_for_groups(groups) + + return group_to_state + + @defer.inlineCallbacks + def get_state_ids_for_group(self, state_group): + """Get the event IDs of all the state in the given state group + + Args: + state_group (int) + + Returns: + Deferred[dict]: Resolves to a map of (type, state_key) -> event_id + """ + group_to_state = yield self._get_state_for_groups((state_group,)) + + return group_to_state[state_group] + + @defer.inlineCallbacks + def get_state_groups(self, room_id, event_ids): + """ Get the state groups for the given list of event_ids + Returns: + Deferred[dict[int, list[EventBase]]]: + dict of state_group_id -> list of state events. + """ + if not event_ids: + return {} + + group_to_ids = yield self.get_state_groups_ids(room_id, event_ids) + + state_event_map = yield self.stores.main.get_events( + [ + ev_id + for group_ids in itervalues(group_to_ids) + for ev_id in itervalues(group_ids) + ], + get_prev_content=False, + ) + + return { + group: [ + state_event_map[v] + for v in itervalues(event_id_map) + if v in state_event_map + ] + for group, event_id_map in iteritems(group_to_ids) + } + + def _get_state_groups_from_groups(self, groups, state_filter): + """Returns the state groups for a given set of groups, filtering on + types of state events. + + Args: + groups(list[int]): list of state group IDs to query + state_filter (StateFilter): The state filter used to fetch state + from the database. + Returns: + Deferred[dict[int, dict[tuple[str, str], str]]]: + dict of state_group_id -> (dict of (type, state_key) -> event id) + """ + + return self.stores.main._get_state_groups_from_groups(groups, state_filter) + + @defer.inlineCallbacks + def get_state_for_events(self, event_ids, state_filter=StateFilter.all()): + """Given a list of event_ids and type tuples, return a list of state + dicts for each event. + Args: + event_ids (list[string]) + state_filter (StateFilter): The state filter used to fetch state + from the database. + Returns: + deferred: A dict of (event_id) -> (type, state_key) -> [state_events] + """ + event_to_groups = yield self.stores.main._get_state_group_for_events(event_ids) + + groups = set(itervalues(event_to_groups)) + group_to_state = yield self.stores.main._get_state_for_groups( + groups, state_filter + ) + + state_event_map = yield self.stores.main.get_events( + [ev_id for sd in itervalues(group_to_state) for ev_id in itervalues(sd)], + get_prev_content=False, + ) + + event_to_state = { + event_id: { + k: state_event_map[v] + for k, v in iteritems(group_to_state[group]) + if v in state_event_map + } + for event_id, group in iteritems(event_to_groups) + } + + return {event: event_to_state[event] for event in event_ids} + + @defer.inlineCallbacks + def get_state_ids_for_events(self, event_ids, state_filter=StateFilter.all()): + """ + Get the state dicts corresponding to a list of events, containing the event_ids + of the state events (as opposed to the events themselves) + + Args: + event_ids(list(str)): events whose state should be returned + state_filter (StateFilter): The state filter used to fetch state + from the database. + + Returns: + A deferred dict from event_id -> (type, state_key) -> event_id + """ + event_to_groups = yield self.stores.main._get_state_group_for_events(event_ids) + + groups = set(itervalues(event_to_groups)) + group_to_state = yield self.stores.main._get_state_for_groups( + groups, state_filter + ) + + event_to_state = { + event_id: group_to_state[group] + for event_id, group in iteritems(event_to_groups) + } + + return {event: event_to_state[event] for event in event_ids} + + @defer.inlineCallbacks + def get_state_for_event(self, event_id, state_filter=StateFilter.all()): + """ + Get the state dict corresponding to a particular event + + Args: + event_id(str): event whose state should be returned + state_filter (StateFilter): The state filter used to fetch state + from the database. + + Returns: + A deferred dict from (type, state_key) -> state_event + """ + state_map = yield self.get_state_for_events([event_id], state_filter) + return state_map[event_id] + + @defer.inlineCallbacks + def get_state_ids_for_event(self, event_id, state_filter=StateFilter.all()): + """ + Get the state dict corresponding to a particular event + + Args: + event_id(str): event whose state should be returned + state_filter (StateFilter): The state filter used to fetch state + from the database. + + Returns: + A deferred dict from (type, state_key) -> state_event + """ + state_map = yield self.get_state_ids_for_events([event_id], state_filter) + return state_map[event_id] + + def _get_state_for_groups(self, groups, state_filter=StateFilter.all()): + """Gets the state at each of a list of state groups, optionally + filtering by type/state_key + + Args: + groups (iterable[int]): list of state groups for which we want + to get the state. + state_filter (StateFilter): The state filter used to fetch state + from the database. + Returns: + Deferred[dict[int, dict[tuple[str, str], str]]]: + dict of state_group_id -> (dict of (type, state_key) -> event id) + """ + return self.stores.main._get_state_for_groups(groups, state_filter) + + def store_state_group( + self, event_id, room_id, prev_group, delta_ids, current_state_ids + ): + """Store a new set of state, returning a newly assigned state group. + + Args: + event_id (str): The event ID for which the state was calculated + room_id (str) + prev_group (int|None): A previous state group for the room, optional. + delta_ids (dict|None): The delta between state at `prev_group` and + `current_state_ids`, if `prev_group` was given. Same format as + `current_state_ids`. + current_state_ids (dict): The state to store. Map of (type, state_key) + to event_id. + + Returns: + Deferred[int]: The state group ID + """ + return self.stores.main.store_state_group( + event_id, room_id, prev_group, delta_ids, current_state_ids + ) From 69f0054ce675bd9d35104c39af9fae9a908b7f33 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 23 Oct 2019 17:25:54 +0100 Subject: [PATCH 0375/1623] Port to use state storage --- synapse/handlers/admin.py | 7 +- synapse/handlers/device.py | 3 +- synapse/handlers/events.py | 6 +- synapse/handlers/federation.py | 19 ++-- synapse/handlers/initial_sync.py | 14 +-- synapse/handlers/message.py | 10 ++- synapse/handlers/pagination.py | 6 +- synapse/handlers/room.py | 6 +- synapse/handlers/search.py | 12 +-- synapse/handlers/sync.py | 20 +++-- synapse/notifier.py | 6 +- synapse/push/httppusher.py | 3 +- synapse/push/mailer.py | 3 +- synapse/push/push_tools.py | 9 +- synapse/state/__init__.py | 13 +-- synapse/visibility.py | 30 ++++--- tests/storage/test_state.py | 150 +++++++++++++++++++++---------- tests/test_state.py | 3 + tests/test_visibility.py | 11 ++- 19 files changed, 216 insertions(+), 115 deletions(-) diff --git a/synapse/handlers/admin.py b/synapse/handlers/admin.py index 1a87b58838..6407d56f8e 100644 --- a/synapse/handlers/admin.py +++ b/synapse/handlers/admin.py @@ -30,6 +30,9 @@ class AdminHandler(BaseHandler): def __init__(self, hs): super(AdminHandler, self).__init__(hs) + self.storage = hs.get_storage() + self.state_store = self.storage.state + @defer.inlineCallbacks def get_whois(self, user): connections = [] @@ -205,7 +208,7 @@ class AdminHandler(BaseHandler): from_key = events[-1].internal_metadata.after - events = yield filter_events_for_client(self.store, user_id, events) + events = yield filter_events_for_client(self.storage, user_id, events) writer.write_events(room_id, events) @@ -241,7 +244,7 @@ class AdminHandler(BaseHandler): for event_id in extremities: if not event_to_unseen_prevs[event_id]: continue - state = yield self.store.get_state_for_event(event_id) + state = yield self.state_store.get_state_for_event(event_id) writer.write_state(room_id, event_id, state) return writer.finished() diff --git a/synapse/handlers/device.py b/synapse/handlers/device.py index 5f23ee4488..b3fd7e6249 100644 --- a/synapse/handlers/device.py +++ b/synapse/handlers/device.py @@ -46,6 +46,7 @@ class DeviceWorkerHandler(BaseHandler): self.hs = hs self.state = hs.get_state_handler() + self.state_store = hs.get_storage().state self._auth_handler = hs.get_auth_handler() @trace @@ -178,7 +179,7 @@ class DeviceWorkerHandler(BaseHandler): continue # mapping from event_id -> state_dict - prev_state_ids = yield self.store.get_state_ids_for_events(event_ids) + prev_state_ids = yield self.state_store.get_state_ids_for_events(event_ids) # Check if we've joined the room? If so we just blindly add all the users to # the "possibly changed" users. diff --git a/synapse/handlers/events.py b/synapse/handlers/events.py index 5e748687e3..45fe13c62f 100644 --- a/synapse/handlers/events.py +++ b/synapse/handlers/events.py @@ -147,6 +147,10 @@ class EventStreamHandler(BaseHandler): class EventHandler(BaseHandler): + def __init__(self, hs): + super(EventHandler, self).__init__(hs) + self.storage = hs.get_storage() + @defer.inlineCallbacks def get_event(self, user, room_id, event_id): """Retrieve a single specified event. @@ -172,7 +176,7 @@ class EventHandler(BaseHandler): is_peeking = user.to_string() not in users filtered = yield filter_events_for_client( - self.store, user.to_string(), [event], is_peeking=is_peeking + self.storage, user.to_string(), [event], is_peeking=is_peeking ) if not filtered: diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 08276fdebf..4d9e33346d 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -110,6 +110,7 @@ class FederationHandler(BaseHandler): self.store = hs.get_datastore() self.storage = hs.get_storage() + self.state_store = self.storage.state self.federation_client = hs.get_federation_client() self.state_handler = hs.get_state_handler() self.server_name = hs.hostname @@ -325,7 +326,7 @@ class FederationHandler(BaseHandler): event_map = {event_id: pdu} try: # Get the state of the events we know about - ours = yield self.store.get_state_groups_ids(room_id, seen) + ours = yield self.state_store.get_state_groups_ids(room_id, seen) # state_maps is a list of mappings from (type, state_key) to event_id state_maps = list( @@ -889,7 +890,7 @@ class FederationHandler(BaseHandler): # We set `check_history_visibility_only` as we might otherwise get false # positives from users having been erased. filtered_extremities = yield filter_events_for_server( - self.store, + self.storage, self.server_name, list(extremities_events.values()), redact=False, @@ -1550,7 +1551,7 @@ class FederationHandler(BaseHandler): event_id, allow_none=False, check_room_id=room_id ) - state_groups = yield self.store.get_state_groups(room_id, [event_id]) + state_groups = yield self.state_store.get_state_groups(room_id, [event_id]) if state_groups: _, state = list(iteritems(state_groups)).pop() @@ -1579,7 +1580,7 @@ class FederationHandler(BaseHandler): event_id, allow_none=False, check_room_id=room_id ) - state_groups = yield self.store.get_state_groups_ids(room_id, [event_id]) + state_groups = yield self.state_store.get_state_groups_ids(room_id, [event_id]) if state_groups: _, state = list(state_groups.items()).pop() @@ -1607,7 +1608,7 @@ class FederationHandler(BaseHandler): events = yield self.store.get_backfill_events(room_id, pdu_list, limit) - events = yield filter_events_for_server(self.store, origin, events) + events = yield filter_events_for_server(self.storage, origin, events) return events @@ -1637,7 +1638,7 @@ class FederationHandler(BaseHandler): if not in_room: raise AuthError(403, "Host not in room.") - events = yield filter_events_for_server(self.store, origin, [event]) + events = yield filter_events_for_server(self.storage, origin, [event]) event = events[0] return event else: @@ -1903,7 +1904,7 @@ class FederationHandler(BaseHandler): # given state at the event. This should correctly handle cases # like bans, especially with state res v2. - state_sets = yield self.store.get_state_groups( + state_sets = yield self.state_store.get_state_groups( event.room_id, extrem_ids ) state_sets = list(state_sets.values()) @@ -1994,7 +1995,7 @@ class FederationHandler(BaseHandler): ) missing_events = yield filter_events_for_server( - self.store, origin, missing_events + self.storage, origin, missing_events ) return missing_events @@ -2235,7 +2236,7 @@ class FederationHandler(BaseHandler): # create a new state group as a delta from the existing one. prev_group = context.state_group - state_group = yield self.store.store_state_group( + state_group = yield self.state_store.store_state_group( event.event_id, event.room_id, prev_group=prev_group, diff --git a/synapse/handlers/initial_sync.py b/synapse/handlers/initial_sync.py index f991efeee3..49c9e031f9 100644 --- a/synapse/handlers/initial_sync.py +++ b/synapse/handlers/initial_sync.py @@ -43,6 +43,8 @@ class InitialSyncHandler(BaseHandler): self.validator = EventValidator() self.snapshot_cache = SnapshotCache() self._event_serializer = hs.get_event_client_serializer() + self.storage = hs.get_storage() + self.state_store = self.storage.state def snapshot_all_rooms( self, @@ -169,7 +171,7 @@ class InitialSyncHandler(BaseHandler): elif event.membership == Membership.LEAVE: room_end_token = "s%d" % (event.stream_ordering,) deferred_room_state = run_in_background( - self.store.get_state_for_events, [event.event_id] + self.state_store.get_state_for_events, [event.event_id] ) deferred_room_state.addCallback( lambda states: states[event.event_id] @@ -189,7 +191,9 @@ class InitialSyncHandler(BaseHandler): ) ).addErrback(unwrapFirstError) - messages = yield filter_events_for_client(self.store, user_id, messages) + messages = yield filter_events_for_client( + self.storage, user_id, messages + ) start_token = now_token.copy_and_replace("room_key", token) end_token = now_token.copy_and_replace("room_key", room_end_token) @@ -307,7 +311,7 @@ class InitialSyncHandler(BaseHandler): def _room_initial_sync_parted( self, user_id, room_id, pagin_config, membership, member_event_id, is_peeking ): - room_state = yield self.store.get_state_for_events([member_event_id]) + room_state = yield self.state_store.get_state_for_events([member_event_id]) room_state = room_state[member_event_id] @@ -322,7 +326,7 @@ class InitialSyncHandler(BaseHandler): ) messages = yield filter_events_for_client( - self.store, user_id, messages, is_peeking=is_peeking + self.storage, user_id, messages, is_peeking=is_peeking ) start_token = StreamToken.START.copy_and_replace("room_key", token) @@ -414,7 +418,7 @@ class InitialSyncHandler(BaseHandler): ) messages = yield filter_events_for_client( - self.store, user_id, messages, is_peeking=is_peeking + self.storage, user_id, messages, is_peeking=is_peeking ) start_token = now_token.copy_and_replace("room_key", token) diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index 7908a2d52c..6e2a360262 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -59,6 +59,8 @@ class MessageHandler(object): self.clock = hs.get_clock() self.state = hs.get_state_handler() self.store = hs.get_datastore() + self.storage = hs.get_storage() + self.state_store = self.storage.state self._event_serializer = hs.get_event_client_serializer() @defer.inlineCallbacks @@ -82,7 +84,7 @@ class MessageHandler(object): data = yield self.state.get_current_state(room_id, event_type, state_key) elif membership == Membership.LEAVE: key = (event_type, state_key) - room_state = yield self.store.get_state_for_events( + room_state = yield self.state_store.get_state_for_events( [membership_event_id], StateFilter.from_types([key]) ) data = room_state[membership_event_id].get(key) @@ -135,12 +137,12 @@ class MessageHandler(object): raise NotFoundError("Can't find event for token %s" % (at_token,)) visible_events = yield filter_events_for_client( - self.store, user_id, last_events + self.storage, user_id, last_events ) event = last_events[0] if visible_events: - room_state = yield self.store.get_state_for_events( + room_state = yield self.state_store.get_state_for_events( [event.event_id], state_filter=state_filter ) room_state = room_state[event.event_id] @@ -161,7 +163,7 @@ class MessageHandler(object): ) room_state = yield self.store.get_events(state_ids.values()) elif membership == Membership.LEAVE: - room_state = yield self.store.get_state_for_events( + room_state = yield self.state_store.get_state_for_events( [membership_event_id], state_filter=state_filter ) room_state = room_state[membership_event_id] diff --git a/synapse/handlers/pagination.py b/synapse/handlers/pagination.py index 5744f4579d..b7185fe7a0 100644 --- a/synapse/handlers/pagination.py +++ b/synapse/handlers/pagination.py @@ -69,6 +69,8 @@ class PaginationHandler(object): self.hs = hs self.auth = hs.get_auth() self.store = hs.get_datastore() + self.storage = hs.get_storage() + self.state_store = self.storage.state self.clock = hs.get_clock() self._server_name = hs.hostname @@ -255,7 +257,7 @@ class PaginationHandler(object): events = event_filter.filter(events) events = yield filter_events_for_client( - self.store, user_id, events, is_peeking=(member_event_id is None) + self.storage, user_id, events, is_peeking=(member_event_id is None) ) if not events: @@ -274,7 +276,7 @@ class PaginationHandler(object): (EventTypes.Member, event.sender) for event in events ) - state_ids = yield self.store.get_state_ids_for_event( + state_ids = yield self.state_store.get_state_ids_for_event( events[0].event_id, state_filter=state_filter ) diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 2816bd8f87..84bad39815 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -822,6 +822,8 @@ class RoomContextHandler(object): def __init__(self, hs): self.hs = hs self.store = hs.get_datastore() + self.storage = hs.get_storage() + self.state_store = self.storage.state @defer.inlineCallbacks def get_event_context(self, user, room_id, event_id, limit, event_filter): @@ -848,7 +850,7 @@ class RoomContextHandler(object): def filter_evts(events): return filter_events_for_client( - self.store, user.to_string(), events, is_peeking=is_peeking + self.storage, user.to_string(), events, is_peeking=is_peeking ) event = yield self.store.get_event( @@ -890,7 +892,7 @@ class RoomContextHandler(object): # first? Shouldn't we be consistent with /sync? # https://github.com/matrix-org/matrix-doc/issues/687 - state = yield self.store.get_state_for_events( + state = yield self.state_store.get_state_for_events( [last_event_id], state_filter=state_filter ) results["state"] = list(state[last_event_id].values()) diff --git a/synapse/handlers/search.py b/synapse/handlers/search.py index cd5e90bacb..f4d8a60774 100644 --- a/synapse/handlers/search.py +++ b/synapse/handlers/search.py @@ -35,6 +35,8 @@ class SearchHandler(BaseHandler): def __init__(self, hs): super(SearchHandler, self).__init__(hs) self._event_serializer = hs.get_event_client_serializer() + self.storage = hs.get_storage() + self.state_store = self.storage.state @defer.inlineCallbacks def get_old_rooms_from_upgraded_room(self, room_id): @@ -221,7 +223,7 @@ class SearchHandler(BaseHandler): filtered_events = search_filter.filter([r["event"] for r in results]) events = yield filter_events_for_client( - self.store, user.to_string(), filtered_events + self.storage, user.to_string(), filtered_events ) events.sort(key=lambda e: -rank_map[e.event_id]) @@ -271,7 +273,7 @@ class SearchHandler(BaseHandler): filtered_events = search_filter.filter([r["event"] for r in results]) events = yield filter_events_for_client( - self.store, user.to_string(), filtered_events + self.storage, user.to_string(), filtered_events ) room_events.extend(events) @@ -340,11 +342,11 @@ class SearchHandler(BaseHandler): ) res["events_before"] = yield filter_events_for_client( - self.store, user.to_string(), res["events_before"] + self.storage, user.to_string(), res["events_before"] ) res["events_after"] = yield filter_events_for_client( - self.store, user.to_string(), res["events_after"] + self.storage, user.to_string(), res["events_after"] ) res["start"] = now_token.copy_and_replace( @@ -372,7 +374,7 @@ class SearchHandler(BaseHandler): [(EventTypes.Member, sender) for sender in senders] ) - state = yield self.store.get_state_for_event( + state = yield self.state_store.get_state_for_event( last_event_id, state_filter ) diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index d99160e9d7..43a082dcda 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -230,6 +230,8 @@ class SyncHandler(object): self.response_cache = ResponseCache(hs, "sync") self.state = hs.get_state_handler() self.auth = hs.get_auth() + self.storage = hs.get_storage() + self.state_store = self.storage.state # ExpiringCache((User, Device)) -> LruCache(state_key => event_id) self.lazy_loaded_members_cache = ExpiringCache( @@ -417,7 +419,7 @@ class SyncHandler(object): current_state_ids = frozenset(itervalues(current_state_ids)) recents = yield filter_events_for_client( - self.store, + self.storage, sync_config.user.to_string(), recents, always_include_ids=current_state_ids, @@ -470,7 +472,7 @@ class SyncHandler(object): current_state_ids = frozenset(itervalues(current_state_ids)) loaded_recents = yield filter_events_for_client( - self.store, + self.storage, sync_config.user.to_string(), loaded_recents, always_include_ids=current_state_ids, @@ -509,7 +511,7 @@ class SyncHandler(object): Returns: A Deferred map from ((type, state_key)->Event) """ - state_ids = yield self.store.get_state_ids_for_event( + state_ids = yield self.state_store.get_state_ids_for_event( event.event_id, state_filter=state_filter ) if event.is_state(): @@ -580,7 +582,7 @@ class SyncHandler(object): return None last_event = last_events[-1] - state_ids = yield self.store.get_state_ids_for_event( + state_ids = yield self.state_store.get_state_ids_for_event( last_event.event_id, state_filter=StateFilter.from_types( [(EventTypes.Name, ""), (EventTypes.CanonicalAlias, "")] @@ -757,11 +759,11 @@ class SyncHandler(object): if full_state: if batch: - current_state_ids = yield self.store.get_state_ids_for_event( + current_state_ids = yield self.state_store.get_state_ids_for_event( batch.events[-1].event_id, state_filter=state_filter ) - state_ids = yield self.store.get_state_ids_for_event( + state_ids = yield self.state_store.get_state_ids_for_event( batch.events[0].event_id, state_filter=state_filter ) @@ -781,7 +783,7 @@ class SyncHandler(object): ) elif batch.limited: if batch: - state_at_timeline_start = yield self.store.get_state_ids_for_event( + state_at_timeline_start = yield self.state_store.get_state_ids_for_event( batch.events[0].event_id, state_filter=state_filter ) else: @@ -810,7 +812,7 @@ class SyncHandler(object): ) if batch: - current_state_ids = yield self.store.get_state_ids_for_event( + current_state_ids = yield self.state_store.get_state_ids_for_event( batch.events[-1].event_id, state_filter=state_filter ) else: @@ -841,7 +843,7 @@ class SyncHandler(object): # So we fish out all the member events corresponding to the # timeline here, and then dedupe any redundant ones below. - state_ids = yield self.store.get_state_ids_for_event( + state_ids = yield self.state_store.get_state_ids_for_event( batch.events[0].event_id, # we only want members! state_filter=StateFilter.from_types( diff --git a/synapse/notifier.py b/synapse/notifier.py index 4e091314e6..af161a81d7 100644 --- a/synapse/notifier.py +++ b/synapse/notifier.py @@ -159,6 +159,7 @@ class Notifier(object): self.room_to_user_streams = {} self.hs = hs + self.storage = hs.get_storage() self.event_sources = hs.get_event_sources() self.store = hs.get_datastore() self.pending_new_room_events = [] @@ -425,7 +426,10 @@ class Notifier(object): if name == "room": new_events = yield filter_events_for_client( - self.store, user.to_string(), new_events, is_peeking=is_peeking + self.storage, + user.to_string(), + new_events, + is_peeking=is_peeking, ) elif name == "presence": now = self.clock.time_msec() diff --git a/synapse/push/httppusher.py b/synapse/push/httppusher.py index 6299587808..36e26032a1 100644 --- a/synapse/push/httppusher.py +++ b/synapse/push/httppusher.py @@ -64,6 +64,7 @@ class HttpPusher(object): def __init__(self, hs, pusherdict): self.hs = hs self.store = self.hs.get_datastore() + self.storage = self.hs.get_storage() self.clock = self.hs.get_clock() self.state_handler = self.hs.get_state_handler() self.user_id = pusherdict["user_name"] @@ -329,7 +330,7 @@ class HttpPusher(object): return d ctx = yield push_tools.get_context_for_event( - self.store, self.state_handler, event, self.user_id + self.storage, self.state_handler, event, self.user_id ) d = { diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 5b16ab4ae8..1d15a06a58 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -119,6 +119,7 @@ class Mailer(object): self.store = self.hs.get_datastore() self.macaroon_gen = self.hs.get_macaroon_generator() self.state_handler = self.hs.get_state_handler() + self.storage = hs.get_storage() self.app_name = app_name logger.info("Created Mailer for app_name %s" % app_name) @@ -389,7 +390,7 @@ class Mailer(object): } the_events = yield filter_events_for_client( - self.store, user_id, results["events_before"] + self.storage, user_id, results["events_before"] ) the_events.append(notif_event) diff --git a/synapse/push/push_tools.py b/synapse/push/push_tools.py index a54051a726..de5c101a58 100644 --- a/synapse/push/push_tools.py +++ b/synapse/push/push_tools.py @@ -16,6 +16,7 @@ from twisted.internet import defer from synapse.push.presentable_names import calculate_room_name, name_from_member_event +from synapse.storage import Storage @defer.inlineCallbacks @@ -43,22 +44,22 @@ def get_badge_count(store, user_id): @defer.inlineCallbacks -def get_context_for_event(store, state_handler, ev, user_id): +def get_context_for_event(storage: Storage, state_handler, ev, user_id): ctx = {} - room_state_ids = yield store.get_state_ids_for_event(ev.event_id) + room_state_ids = yield storage.state.get_state_ids_for_event(ev.event_id) # we no longer bother setting room_alias, and make room_name the # human-readable name instead, be that m.room.name, an alias or # a list of people in the room name = yield calculate_room_name( - store, room_state_ids, user_id, fallback_to_single_member=False + storage.main, room_state_ids, user_id, fallback_to_single_member=False ) if name: ctx["name"] = name sender_state_event_id = room_state_ids[("m.room.member", ev.sender)] - sender_state_event = yield store.get_event(sender_state_event_id) + sender_state_event = yield storage.main.get_event(sender_state_event_id) ctx["sender_display_name"] = name_from_member_event(sender_state_event) return ctx diff --git a/synapse/state/__init__.py b/synapse/state/__init__.py index dc9f5a9008..4e91eb66fe 100644 --- a/synapse/state/__init__.py +++ b/synapse/state/__init__.py @@ -103,6 +103,7 @@ class StateHandler(object): def __init__(self, hs): self.clock = hs.get_clock() self.store = hs.get_datastore() + self.state_store = hs.get_storage().state self.hs = hs self._state_resolution_handler = hs.get_state_resolution_handler() @@ -271,7 +272,7 @@ class StateHandler(object): else: current_state_ids = prev_state_ids - state_group = yield self.store.store_state_group( + state_group = yield self.state_store.store_state_group( event.event_id, event.room_id, prev_group=None, @@ -321,7 +322,7 @@ class StateHandler(object): delta_ids = dict(entry.delta_ids) delta_ids[key] = event.event_id - state_group = yield self.store.store_state_group( + state_group = yield self.state_store.store_state_group( event.event_id, event.room_id, prev_group=prev_group, @@ -334,7 +335,7 @@ class StateHandler(object): delta_ids = entry.delta_ids if entry.state_group is None: - entry.state_group = yield self.store.store_state_group( + entry.state_group = yield self.state_store.store_state_group( event.event_id, event.room_id, prev_group=entry.prev_group, @@ -376,14 +377,16 @@ class StateHandler(object): # map from state group id to the state in that state group (where # 'state' is a map from state key to event id) # dict[int, dict[(str, str), str]] - state_groups_ids = yield self.store.get_state_groups_ids(room_id, event_ids) + state_groups_ids = yield self.state_store.get_state_groups_ids( + room_id, event_ids + ) if len(state_groups_ids) == 0: return _StateCacheEntry(state={}, state_group=None) elif len(state_groups_ids) == 1: name, state_list = list(state_groups_ids.items()).pop() - prev_group, delta_ids = yield self.store.get_state_group_delta(name) + prev_group, delta_ids = yield self.state_store.get_state_group_delta(name) return _StateCacheEntry( state=state_list, diff --git a/synapse/visibility.py b/synapse/visibility.py index bf0f1eebd8..8c843febd8 100644 --- a/synapse/visibility.py +++ b/synapse/visibility.py @@ -23,6 +23,7 @@ from twisted.internet import defer from synapse.api.constants import EventTypes, Membership from synapse.events.utils import prune_event +from synapse.storage import Storage from synapse.storage.state import StateFilter from synapse.types import get_domain_from_id @@ -43,14 +44,13 @@ MEMBERSHIP_PRIORITY = ( @defer.inlineCallbacks def filter_events_for_client( - store, user_id, events, is_peeking=False, always_include_ids=frozenset() + storage: Storage, user_id, events, is_peeking=False, always_include_ids=frozenset() ): """ Check which events a user is allowed to see Args: - store (synapse.storage.DataStore): our datastore (can also be a worker - store) + storage user_id(str): user id to be checked events(list[synapse.events.EventBase]): sequence of events to be checked is_peeking(bool): should be True if: @@ -68,12 +68,12 @@ def filter_events_for_client( events = list(e for e in events if not e.internal_metadata.is_soft_failed()) types = ((EventTypes.RoomHistoryVisibility, ""), (EventTypes.Member, user_id)) - event_id_to_state = yield store.get_state_for_events( + event_id_to_state = yield storage.state.get_state_for_events( frozenset(e.event_id for e in events), state_filter=StateFilter.from_types(types), ) - ignore_dict_content = yield store.get_global_account_data_by_type_for_user( + ignore_dict_content = yield storage.main.get_global_account_data_by_type_for_user( "m.ignored_user_list", user_id ) @@ -84,7 +84,7 @@ def filter_events_for_client( else [] ) - erased_senders = yield store.are_users_erased((e.sender for e in events)) + erased_senders = yield storage.main.are_users_erased((e.sender for e in events)) def allowed(event): """ @@ -213,13 +213,17 @@ def filter_events_for_client( @defer.inlineCallbacks def filter_events_for_server( - store, server_name, events, redact=True, check_history_visibility_only=False + storage: Storage, + server_name, + events, + redact=True, + check_history_visibility_only=False, ): """Filter a list of events based on whether given server is allowed to see them. Args: - store (DataStore) + storage server_name (str) events (iterable[FrozenEvent]) redact (bool): Whether to return a redacted version of the event, or @@ -274,7 +278,7 @@ def filter_events_for_server( # Lets check to see if all the events have a history visibility # of "shared" or "world_readable". If thats the case then we don't # need to check membership (as we know the server is in the room). - event_to_state_ids = yield store.get_state_ids_for_events( + event_to_state_ids = yield storage.state.get_state_ids_for_events( frozenset(e.event_id for e in events), state_filter=StateFilter.from_types( types=((EventTypes.RoomHistoryVisibility, ""),) @@ -292,14 +296,14 @@ def filter_events_for_server( if not visibility_ids: all_open = True else: - event_map = yield store.get_events(visibility_ids) + event_map = yield storage.main.get_events(visibility_ids) all_open = all( e.content.get("history_visibility") in (None, "shared", "world_readable") for e in itervalues(event_map) ) if not check_history_visibility_only: - erased_senders = yield store.are_users_erased((e.sender for e in events)) + erased_senders = yield storage.main.are_users_erased((e.sender for e in events)) else: # We don't want to check whether users are erased, which is equivalent # to no users having been erased. @@ -328,7 +332,7 @@ def filter_events_for_server( # first, for each event we're wanting to return, get the event_ids # of the history vis and membership state at those events. - event_to_state_ids = yield store.get_state_ids_for_events( + event_to_state_ids = yield storage.state.get_state_ids_for_events( frozenset(e.event_id for e in events), state_filter=StateFilter.from_types( types=((EventTypes.RoomHistoryVisibility, ""), (EventTypes.Member, None)) @@ -358,7 +362,7 @@ def filter_events_for_server( return False return state_key[idx + 1 :] == server_name - event_map = yield store.get_events( + event_map = yield storage.main.get_events( [ e_id for e_id, key in iteritems(event_id_to_state_key) diff --git a/tests/storage/test_state.py b/tests/storage/test_state.py index d573a3e07b..43200654f1 100644 --- a/tests/storage/test_state.py +++ b/tests/storage/test_state.py @@ -35,6 +35,7 @@ class StateStoreTestCase(tests.unittest.TestCase): self.store = hs.get_datastore() self.storage = hs.get_storage() + self.state_datastore = self.store self.event_builder_factory = hs.get_event_builder_factory() self.event_creation_handler = hs.get_event_creation_handler() @@ -83,7 +84,7 @@ class StateStoreTestCase(tests.unittest.TestCase): self.room, self.u_alice, EventTypes.Name, "", {"name": "test room"} ) - state_group_map = yield self.store.get_state_groups_ids( + state_group_map = yield self.storage.state.get_state_groups_ids( self.room, [e2.event_id] ) self.assertEqual(len(state_group_map), 1) @@ -102,7 +103,9 @@ class StateStoreTestCase(tests.unittest.TestCase): self.room, self.u_alice, EventTypes.Name, "", {"name": "test room"} ) - state_group_map = yield self.store.get_state_groups(self.room, [e2.event_id]) + state_group_map = yield self.storage.state.get_state_groups( + self.room, [e2.event_id] + ) self.assertEqual(len(state_group_map), 1) state_list = list(state_group_map.values())[0] @@ -142,7 +145,7 @@ class StateStoreTestCase(tests.unittest.TestCase): ) # check we get the full state as of the final event - state = yield self.store.get_state_for_event(e5.event_id) + state = yield self.storage.state.get_state_for_event(e5.event_id) self.assertIsNotNone(e4) @@ -158,21 +161,21 @@ class StateStoreTestCase(tests.unittest.TestCase): ) # check we can filter to the m.room.name event (with a '' state key) - state = yield self.store.get_state_for_event( + state = yield self.storage.state.get_state_for_event( e5.event_id, StateFilter.from_types([(EventTypes.Name, "")]) ) self.assertStateMapEqual({(e2.type, e2.state_key): e2}, state) # check we can filter to the m.room.name event (with a wildcard None state key) - state = yield self.store.get_state_for_event( + state = yield self.storage.state.get_state_for_event( e5.event_id, StateFilter.from_types([(EventTypes.Name, None)]) ) self.assertStateMapEqual({(e2.type, e2.state_key): e2}, state) # check we can grab the m.room.member events (with a wildcard None state key) - state = yield self.store.get_state_for_event( + state = yield self.storage.state.get_state_for_event( e5.event_id, StateFilter.from_types([(EventTypes.Member, None)]) ) @@ -182,7 +185,7 @@ class StateStoreTestCase(tests.unittest.TestCase): # check we can grab a specific room member without filtering out the # other event types - state = yield self.store.get_state_for_event( + state = yield self.storage.state.get_state_for_event( e5.event_id, state_filter=StateFilter( types={EventTypes.Member: {self.u_alice.to_string()}}, @@ -200,7 +203,7 @@ class StateStoreTestCase(tests.unittest.TestCase): ) # check that we can grab everything except members - state = yield self.store.get_state_for_event( + state = yield self.storage.state.get_state_for_event( e5.event_id, state_filter=StateFilter( types={EventTypes.Member: set()}, include_others=True @@ -216,13 +219,18 @@ class StateStoreTestCase(tests.unittest.TestCase): ####################################################### room_id = self.room.to_string() - group_ids = yield self.store.get_state_groups_ids(room_id, [e5.event_id]) + group_ids = yield self.storage.state.get_state_groups_ids( + room_id, [e5.event_id] + ) group = list(group_ids.keys())[0] # test _get_state_for_group_using_cache correctly filters out members # with types=[] - (state_dict, is_all) = yield self.store._get_state_for_group_using_cache( - self.store._state_group_cache, + ( + state_dict, + is_all, + ) = yield self.state_datastore._get_state_for_group_using_cache( + self.state_datastore._state_group_cache, group, state_filter=StateFilter( types={EventTypes.Member: set()}, include_others=True @@ -238,8 +246,11 @@ class StateStoreTestCase(tests.unittest.TestCase): state_dict, ) - (state_dict, is_all) = yield self.store._get_state_for_group_using_cache( - self.store._state_group_members_cache, + ( + state_dict, + is_all, + ) = yield self.state_datastore._get_state_for_group_using_cache( + self.state_datastore._state_group_members_cache, group, state_filter=StateFilter( types={EventTypes.Member: set()}, include_others=True @@ -251,8 +262,11 @@ class StateStoreTestCase(tests.unittest.TestCase): # test _get_state_for_group_using_cache correctly filters in members # with wildcard types - (state_dict, is_all) = yield self.store._get_state_for_group_using_cache( - self.store._state_group_cache, + ( + state_dict, + is_all, + ) = yield self.state_datastore._get_state_for_group_using_cache( + self.state_datastore._state_group_cache, group, state_filter=StateFilter( types={EventTypes.Member: None}, include_others=True @@ -268,8 +282,11 @@ class StateStoreTestCase(tests.unittest.TestCase): state_dict, ) - (state_dict, is_all) = yield self.store._get_state_for_group_using_cache( - self.store._state_group_members_cache, + ( + state_dict, + is_all, + ) = yield self.state_datastore._get_state_for_group_using_cache( + self.state_datastore._state_group_members_cache, group, state_filter=StateFilter( types={EventTypes.Member: None}, include_others=True @@ -288,8 +305,11 @@ class StateStoreTestCase(tests.unittest.TestCase): # test _get_state_for_group_using_cache correctly filters in members # with specific types - (state_dict, is_all) = yield self.store._get_state_for_group_using_cache( - self.store._state_group_cache, + ( + state_dict, + is_all, + ) = yield self.state_datastore._get_state_for_group_using_cache( + self.state_datastore._state_group_cache, group, state_filter=StateFilter( types={EventTypes.Member: {e5.state_key}}, include_others=True @@ -305,8 +325,11 @@ class StateStoreTestCase(tests.unittest.TestCase): state_dict, ) - (state_dict, is_all) = yield self.store._get_state_for_group_using_cache( - self.store._state_group_members_cache, + ( + state_dict, + is_all, + ) = yield self.state_datastore._get_state_for_group_using_cache( + self.state_datastore._state_group_members_cache, group, state_filter=StateFilter( types={EventTypes.Member: {e5.state_key}}, include_others=True @@ -318,8 +341,11 @@ class StateStoreTestCase(tests.unittest.TestCase): # test _get_state_for_group_using_cache correctly filters in members # with specific types - (state_dict, is_all) = yield self.store._get_state_for_group_using_cache( - self.store._state_group_members_cache, + ( + state_dict, + is_all, + ) = yield self.state_datastore._get_state_for_group_using_cache( + self.state_datastore._state_group_members_cache, group, state_filter=StateFilter( types={EventTypes.Member: {e5.state_key}}, include_others=False @@ -332,9 +358,11 @@ class StateStoreTestCase(tests.unittest.TestCase): ####################################################### # deliberately remove e2 (room name) from the _state_group_cache - (is_all, known_absent, state_dict_ids) = self.store._state_group_cache.get( - group - ) + ( + is_all, + known_absent, + state_dict_ids, + ) = self.state_datastore._state_group_cache.get(group) self.assertEqual(is_all, True) self.assertEqual(known_absent, set()) @@ -347,18 +375,20 @@ class StateStoreTestCase(tests.unittest.TestCase): ) state_dict_ids.pop((e2.type, e2.state_key)) - self.store._state_group_cache.invalidate(group) - self.store._state_group_cache.update( - sequence=self.store._state_group_cache.sequence, + self.state_datastore._state_group_cache.invalidate(group) + self.state_datastore._state_group_cache.update( + sequence=self.state_datastore._state_group_cache.sequence, key=group, value=state_dict_ids, # list fetched keys so it knows it's partial fetched_keys=((e1.type, e1.state_key),), ) - (is_all, known_absent, state_dict_ids) = self.store._state_group_cache.get( - group - ) + ( + is_all, + known_absent, + state_dict_ids, + ) = self.state_datastore._state_group_cache.get(group) self.assertEqual(is_all, False) self.assertEqual(known_absent, set([(e1.type, e1.state_key)])) @@ -370,8 +400,11 @@ class StateStoreTestCase(tests.unittest.TestCase): # test _get_state_for_group_using_cache correctly filters out members # with types=[] room_id = self.room.to_string() - (state_dict, is_all) = yield self.store._get_state_for_group_using_cache( - self.store._state_group_cache, + ( + state_dict, + is_all, + ) = yield self.state_datastore._get_state_for_group_using_cache( + self.state_datastore._state_group_cache, group, state_filter=StateFilter( types={EventTypes.Member: set()}, include_others=True @@ -382,8 +415,11 @@ class StateStoreTestCase(tests.unittest.TestCase): self.assertDictEqual({(e1.type, e1.state_key): e1.event_id}, state_dict) room_id = self.room.to_string() - (state_dict, is_all) = yield self.store._get_state_for_group_using_cache( - self.store._state_group_members_cache, + ( + state_dict, + is_all, + ) = yield self.state_datastore._get_state_for_group_using_cache( + self.state_datastore._state_group_members_cache, group, state_filter=StateFilter( types={EventTypes.Member: set()}, include_others=True @@ -395,8 +431,11 @@ class StateStoreTestCase(tests.unittest.TestCase): # test _get_state_for_group_using_cache correctly filters in members # wildcard types - (state_dict, is_all) = yield self.store._get_state_for_group_using_cache( - self.store._state_group_cache, + ( + state_dict, + is_all, + ) = yield self.state_datastore._get_state_for_group_using_cache( + self.state_datastore._state_group_cache, group, state_filter=StateFilter( types={EventTypes.Member: None}, include_others=True @@ -406,8 +445,11 @@ class StateStoreTestCase(tests.unittest.TestCase): self.assertEqual(is_all, False) self.assertDictEqual({(e1.type, e1.state_key): e1.event_id}, state_dict) - (state_dict, is_all) = yield self.store._get_state_for_group_using_cache( - self.store._state_group_members_cache, + ( + state_dict, + is_all, + ) = yield self.state_datastore._get_state_for_group_using_cache( + self.state_datastore._state_group_members_cache, group, state_filter=StateFilter( types={EventTypes.Member: None}, include_others=True @@ -425,8 +467,11 @@ class StateStoreTestCase(tests.unittest.TestCase): # test _get_state_for_group_using_cache correctly filters in members # with specific types - (state_dict, is_all) = yield self.store._get_state_for_group_using_cache( - self.store._state_group_cache, + ( + state_dict, + is_all, + ) = yield self.state_datastore._get_state_for_group_using_cache( + self.state_datastore._state_group_cache, group, state_filter=StateFilter( types={EventTypes.Member: {e5.state_key}}, include_others=True @@ -436,8 +481,11 @@ class StateStoreTestCase(tests.unittest.TestCase): self.assertEqual(is_all, False) self.assertDictEqual({(e1.type, e1.state_key): e1.event_id}, state_dict) - (state_dict, is_all) = yield self.store._get_state_for_group_using_cache( - self.store._state_group_members_cache, + ( + state_dict, + is_all, + ) = yield self.state_datastore._get_state_for_group_using_cache( + self.state_datastore._state_group_members_cache, group, state_filter=StateFilter( types={EventTypes.Member: {e5.state_key}}, include_others=True @@ -449,8 +497,11 @@ class StateStoreTestCase(tests.unittest.TestCase): # test _get_state_for_group_using_cache correctly filters in members # with specific types - (state_dict, is_all) = yield self.store._get_state_for_group_using_cache( - self.store._state_group_cache, + ( + state_dict, + is_all, + ) = yield self.state_datastore._get_state_for_group_using_cache( + self.state_datastore._state_group_cache, group, state_filter=StateFilter( types={EventTypes.Member: {e5.state_key}}, include_others=False @@ -460,8 +511,11 @@ class StateStoreTestCase(tests.unittest.TestCase): self.assertEqual(is_all, False) self.assertDictEqual({}, state_dict) - (state_dict, is_all) = yield self.store._get_state_for_group_using_cache( - self.store._state_group_members_cache, + ( + state_dict, + is_all, + ) = yield self.state_datastore._get_state_for_group_using_cache( + self.state_datastore._state_group_members_cache, group, state_filter=StateFilter( types={EventTypes.Member: {e5.state_key}}, include_others=False diff --git a/tests/test_state.py b/tests/test_state.py index 610ec9fb46..38246555bd 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -158,10 +158,12 @@ class Graph(object): class StateTestCase(unittest.TestCase): def setUp(self): self.store = StateGroupStore() + storage = Mock(main=self.store, state=self.store) hs = Mock( spec_set=[ "config", "get_datastore", + "get_storage", "get_auth", "get_state_handler", "get_clock", @@ -174,6 +176,7 @@ class StateTestCase(unittest.TestCase): hs.get_clock.return_value = MockClock() hs.get_auth.return_value = Auth(hs) hs.get_state_resolution_handler = lambda: StateResolutionHandler(hs) + hs.get_storage.return_value = storage self.state = StateHandler(hs) self.event_id = 0 diff --git a/tests/test_visibility.py b/tests/test_visibility.py index 6ae1ea9b04..f7381b2885 100644 --- a/tests/test_visibility.py +++ b/tests/test_visibility.py @@ -14,6 +14,8 @@ # limitations under the License. import logging +from mock import Mock + from twisted.internet import defer from twisted.internet.defer import succeed @@ -63,7 +65,7 @@ class FilterEventsForServerTestCase(tests.unittest.TestCase): events_to_filter.append(evt) filtered = yield filter_events_for_server( - self.store, "test_server", events_to_filter + self.storage, "test_server", events_to_filter ) # the result should be 5 redacted events, and 5 unredacted events. @@ -101,7 +103,7 @@ class FilterEventsForServerTestCase(tests.unittest.TestCase): # ... and the filtering happens. filtered = yield filter_events_for_server( - self.store, "test_server", events_to_filter + self.storage, "test_server", events_to_filter ) for i in range(0, len(events_to_filter)): @@ -258,6 +260,11 @@ class FilterEventsForServerTestCase(tests.unittest.TestCase): logger.info("Starting filtering") start = time.time() + + storage = Mock() + storage.main = test_store + storage.state = test_store + filtered = yield filter_events_for_server( test_store, "test_server", events_to_filter ) From d3f694d628a57a7676ffb10ffba1453131a74e12 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 30 Oct 2019 14:53:09 +0000 Subject: [PATCH 0376/1623] Newsfile --- changelog.d/6294.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6294.misc diff --git a/changelog.d/6294.misc b/changelog.d/6294.misc new file mode 100644 index 0000000000..a3e6b8296e --- /dev/null +++ b/changelog.d/6294.misc @@ -0,0 +1 @@ +Split out state storage into separate data store. From 7c8c97e635811609c5a7ae4c0bb94e6573c30753 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 30 Oct 2019 15:12:49 +0000 Subject: [PATCH 0377/1623] Split purge API into events vs state --- synapse/handlers/pagination.py | 7 +- synapse/storage/__init__.py | 2 + synapse/storage/data_stores/main/events.py | 328 ++++++++++----------- synapse/storage/data_stores/main/state.py | 23 ++ synapse/storage/purge_events.py | 117 ++++++++ tests/storage/test_purge.py | 15 +- 6 files changed, 308 insertions(+), 184 deletions(-) create mode 100644 synapse/storage/purge_events.py diff --git a/synapse/handlers/pagination.py b/synapse/handlers/pagination.py index 5744f4579d..9088ba14cd 100644 --- a/synapse/handlers/pagination.py +++ b/synapse/handlers/pagination.py @@ -69,6 +69,7 @@ class PaginationHandler(object): self.hs = hs self.auth = hs.get_auth() self.store = hs.get_datastore() + self.storage = hs.get_storage() self.clock = hs.get_clock() self._server_name = hs.hostname @@ -125,7 +126,9 @@ class PaginationHandler(object): self._purges_in_progress_by_room.add(room_id) try: with (yield self.pagination_lock.write(room_id)): - yield self.store.purge_history(room_id, token, delete_local_events) + yield self.storage.purge_events.purge_history( + room_id, token, delete_local_events + ) logger.info("[purge] complete") self._purges_by_id[purge_id].status = PurgeStatus.STATUS_COMPLETE except Exception: @@ -168,7 +171,7 @@ class PaginationHandler(object): if joined: raise SynapseError(400, "Users are still joined to this room") - await self.store.purge_room(room_id) + await self.storage.purge_events.purge_room(room_id) @defer.inlineCallbacks def get_messages( diff --git a/synapse/storage/__init__.py b/synapse/storage/__init__.py index a6429d17ed..3646ebd007 100644 --- a/synapse/storage/__init__.py +++ b/synapse/storage/__init__.py @@ -30,6 +30,7 @@ stored in `synapse.storage.schema`. from synapse.storage.data_stores import DataStores from synapse.storage.data_stores.main import DataStore from synapse.storage.persist_events import EventsPersistenceStorage +from synapse.storage.purge_events import PurgeEventsStorage __all__ = ["DataStores", "DataStore"] @@ -45,6 +46,7 @@ class Storage(object): self.main = stores.main self.persistence = EventsPersistenceStorage(hs, stores) + self.purge_events = PurgeEventsStorage(hs, stores) def are_all_users_on_domain(txn, database_engine, domain): diff --git a/synapse/storage/data_stores/main/events.py b/synapse/storage/data_stores/main/events.py index 7c3607f308..4eacba8058 100644 --- a/synapse/storage/data_stores/main/events.py +++ b/synapse/storage/data_stores/main/events.py @@ -1368,6 +1368,10 @@ class EventsStore( if True, we will delete local events as well as remote ones (instead of just marking them as outliers and deleting their state groups). + + Returns: + Deferred[set[int]]: The set of state groups that reference deleted + events. """ return self.runInteraction( @@ -1521,60 +1525,6 @@ class EventsStore( "[purge] found %i referenced state groups", len(referenced_state_groups) ) - logger.info("[purge] finding state groups that can be deleted") - - _ = self._find_unreferenced_groups_during_purge(txn, referenced_state_groups) - state_groups_to_delete, remaining_state_groups = _ - - logger.info( - "[purge] found %i state groups to delete", len(state_groups_to_delete) - ) - - logger.info( - "[purge] de-delta-ing %i remaining state groups", - len(remaining_state_groups), - ) - - # Now we turn the state groups that reference to-be-deleted state - # groups to non delta versions. - for sg in remaining_state_groups: - logger.info("[purge] de-delta-ing remaining state group %s", sg) - curr_state = self._get_state_groups_from_groups_txn(txn, [sg]) - curr_state = curr_state[sg] - - self._simple_delete_txn( - txn, table="state_groups_state", keyvalues={"state_group": sg} - ) - - self._simple_delete_txn( - txn, table="state_group_edges", keyvalues={"state_group": sg} - ) - - self._simple_insert_many_txn( - txn, - table="state_groups_state", - values=[ - { - "state_group": sg, - "room_id": room_id, - "type": key[0], - "state_key": key[1], - "event_id": state_id, - } - for key, state_id in iteritems(curr_state) - ], - ) - - logger.info("[purge] removing redundant state groups") - txn.executemany( - "DELETE FROM state_groups_state WHERE state_group = ?", - ((sg,) for sg in state_groups_to_delete), - ) - txn.executemany( - "DELETE FROM state_groups WHERE id = ?", - ((sg,) for sg in state_groups_to_delete), - ) - logger.info("[purge] removing events from event_to_state_groups") txn.execute( "DELETE FROM event_to_state_groups " @@ -1661,87 +1611,7 @@ class EventsStore( logger.info("[purge] done") - def _find_unreferenced_groups_during_purge(self, txn, state_groups): - """Used when purging history to figure out which state groups can be - deleted and which need to be de-delta'ed (due to one of its prev groups - being scheduled for deletion). - - Args: - txn - state_groups (set[int]): Set of state groups referenced by events - that are going to be deleted. - - Returns: - tuple[set[int], set[int]]: The set of state groups that can be - deleted and the set of state groups that need to be de-delta'ed - """ - # Graph of state group -> previous group - graph = {} - - # Set of events that we have found to be referenced by events - referenced_groups = set() - - # Set of state groups we've already seen - state_groups_seen = set(state_groups) - - # Set of state groups to handle next. - next_to_search = set(state_groups) - while next_to_search: - # We bound size of groups we're looking up at once, to stop the - # SQL query getting too big - if len(next_to_search) < 100: - current_search = next_to_search - next_to_search = set() - else: - current_search = set(itertools.islice(next_to_search, 100)) - next_to_search -= current_search - - # Check if state groups are referenced - sql = """ - SELECT DISTINCT state_group FROM event_to_state_groups - LEFT JOIN events_to_purge AS ep USING (event_id) - WHERE ep.event_id IS NULL AND - """ - clause, args = make_in_list_sql_clause( - txn.database_engine, "state_group", current_search - ) - txn.execute(sql + clause, list(args)) - - referenced = set(sg for sg, in txn) - referenced_groups |= referenced - - # We don't continue iterating up the state group graphs for state - # groups that are referenced. - current_search -= referenced - - rows = self._simple_select_many_txn( - txn, - table="state_group_edges", - column="prev_state_group", - iterable=current_search, - keyvalues={}, - retcols=("prev_state_group", "state_group"), - ) - - prevs = set(row["state_group"] for row in rows) - # We don't bother re-handling groups we've already seen - prevs -= state_groups_seen - next_to_search |= prevs - state_groups_seen |= prevs - - for row in rows: - # Note: Each state group can have at most one prev group - graph[row["state_group"]] = row["prev_state_group"] - - to_delete = state_groups_seen - referenced_groups - - to_dedelta = set() - for sg in referenced_groups: - prev_sg = graph.get(sg) - if prev_sg and prev_sg in to_delete: - to_dedelta.add(sg) - - return to_delete, to_dedelta + return referenced_state_groups def purge_room(self, room_id): """Deletes all record of a room @@ -1753,46 +1623,7 @@ class EventsStore( return self.runInteraction("purge_room", self._purge_room_txn, room_id) def _purge_room_txn(self, txn, room_id): - # first we have to delete the state groups states - logger.info("[purge] removing %s from state_groups_state", room_id) - - txn.execute( - """ - DELETE FROM state_groups_state WHERE state_group IN ( - SELECT state_group FROM events JOIN event_to_state_groups USING(event_id) - WHERE events.room_id=? - ) - """, - (room_id,), - ) - - # ... and the state group edges - logger.info("[purge] removing %s from state_group_edges", room_id) - - txn.execute( - """ - DELETE FROM state_group_edges WHERE state_group IN ( - SELECT state_group FROM events JOIN event_to_state_groups USING(event_id) - WHERE events.room_id=? - ) - """, - (room_id,), - ) - - # ... and the state groups - logger.info("[purge] removing %s from state_groups", room_id) - - txn.execute( - """ - DELETE FROM state_groups WHERE id IN ( - SELECT state_group FROM events JOIN event_to_state_groups USING(event_id) - WHERE events.room_id=? - ) - """, - (room_id,), - ) - - # and then tables which lack an index on room_id but have one on event_id + # First delete tables which lack an index on room_id but have one on event_id for table in ( "event_auth", "event_edges", @@ -1881,6 +1712,153 @@ class EventsStore( logger.info("[purge] done") + def purge_unreferenced_state_groups(self, room_id, state_groups_to_delete): + """Deletes no longer referenced state groups and de-deltas any state + groups that reference them. + """ + + return self.runInteraction( + "purge_unreferenced_state_groups", + self._purge_unreferenced_state_groups, + room_id, + state_groups_to_delete, + ) + + def _purge_unreferenced_state_groups(self, txn, room_id, state_groups_to_delete): + logger.info( + "[purge] found %i state groups to delete", len(state_groups_to_delete) + ) + + rows = self._simple_select_many_txn( + txn, + table="state_group_edges", + column="prev_state_group", + iterable=state_groups_to_delete, + keyvalues={}, + retcols=("state_group",), + ) + + remaining_state_groups = set( + row["state_group"] + for row in rows + if row["state_group"] not in state_groups_to_delete + ) + + logger.info( + "[purge] de-delta-ing %i remaining state groups", + len(remaining_state_groups), + ) + + # Now we turn the state groups that reference to-be-deleted state + # groups to non delta versions. + for sg in remaining_state_groups: + logger.info("[purge] de-delta-ing remaining state group %s", sg) + curr_state = self._get_state_groups_from_groups_txn(txn, [sg]) + curr_state = curr_state[sg] + + self._simple_delete_txn( + txn, table="state_groups_state", keyvalues={"state_group": sg} + ) + + self._simple_delete_txn( + txn, table="state_group_edges", keyvalues={"state_group": sg} + ) + + self._simple_insert_many_txn( + txn, + table="state_groups_state", + values=[ + { + "state_group": sg, + "room_id": room_id, + "type": key[0], + "state_key": key[1], + "event_id": state_id, + } + for key, state_id in iteritems(curr_state) + ], + ) + + logger.info("[purge] removing redundant state groups") + txn.executemany( + "DELETE FROM state_groups_state WHERE state_group = ?", + ((sg,) for sg in state_groups_to_delete), + ) + txn.executemany( + "DELETE FROM state_groups WHERE id = ?", + ((sg,) for sg in state_groups_to_delete), + ) + + @defer.inlineCallbacks + def get_previous_state_groups(self, state_groups): + """Fetch the previous groups of the given state groups. + + Args: + state_groups (Iterable[int]) + + Returns: + Deferred[dict[int, int]]: mapping from state group to previous + state group. + """ + + rows = yield self._simple_select_many_batch( + table="state_group_edges", + column="prev_state_group", + iterable=state_groups, + keyvalues={}, + retcols=("prev_state_group", "state_group"), + desc="get_previous_state_groups", + ) + + return {row["state_group"]: row["prev_state_group"] for row in rows} + + def purge_room_state(self, room_id): + """Deletes all record of a room from state tables + + Args: + room_id (str): + """ + + return self.runInteraction( + "purge_room_state", self._purge_room_state_txn, room_id + ) + + def _purge_room_state_txn(self, txn, room_id): + # first we have to delete the state groups states + logger.info("[purge] removing %s from state_groups_state", room_id) + + txn.execute( + """ + DELETE FROM state_groups_state + INNER JOIN state_groups USING (event_id) + WHEREE state_groups.room_id = ? + """, + (room_id,), + ) + + # ... and the state group edges + logger.info("[purge] removing %s from state_group_edges", room_id) + + txn.execute( + """ + DELETE FROM state_group_edges + INNER JOIN state_groups USING (event_id) + WHEREE state_groups.room_id = ? + ) + """, + (room_id,), + ) + + # ... and the state groups + logger.info("[purge] removing %s from state_groups", room_id) + + txn.execute( + """ + DELETE FROM state_groups WHEREE room_id = ? + """, + (room_id,), + ) + async def is_event_after(self, event_id1, event_id2): """Returns True if event_id1 is after event_id2 in the stream """ diff --git a/synapse/storage/data_stores/main/state.py b/synapse/storage/data_stores/main/state.py index 9b2207075b..36be8f0a9d 100644 --- a/synapse/storage/data_stores/main/state.py +++ b/synapse/storage/data_stores/main/state.py @@ -989,6 +989,29 @@ class StateGroupWorkerStore( return self.runInteraction("store_state_group", _store_state_group_txn) + @defer.inlineCallbacks + def get_referenced_state_groups(self, state_groups): + """Check if the state groups are referenced by events. + + Args: + state_groups (Iterable[int]) + + Returns: + Deferred[set[int]]: The subset of state groups that are + referenced. + """ + + rows = yield self._simple_select_many_batch( + table="event_to_state_groups", + column="state_group", + iterable=state_groups, + keyvalues={}, + retcols=("DISTINCT state_group",), + desc="get_referenced_state_groups", + ) + + return set(row["state_group"] for row in rows) + class StateBackgroundUpdateStore( StateGroupBackgroundUpdateStore, BackgroundUpdateStore diff --git a/synapse/storage/purge_events.py b/synapse/storage/purge_events.py new file mode 100644 index 0000000000..dd45df0c88 --- /dev/null +++ b/synapse/storage/purge_events.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import itertools +import logging + +from twisted.internet import defer + +logger = logging.getLogger(__name__) + + +class PurgeEventsStorage(object): + """High level interface for purging rooms and event history. + """ + + def __init__(self, hs, stores): + self.stores = stores + + @defer.inlineCallbacks + def purge_room(self, room_id: str): + """Deletes all record of a room + """ + + yield self.stores.main.purge_room(room_id) + yield self.stores.main.purge_room_state(room_id) + + @defer.inlineCallbacks + def purge_history(self, room_id, token, delete_local_events): + """Deletes room history before a certain point + + Args: + room_id (str): + + token (str): A topological token to delete events before + + delete_local_events (bool): + if True, we will delete local events as well as remote ones + (instead of just marking them as outliers and deleting their + state groups). + """ + state_groups = yield self.stores.main.purge_history( + room_id, token, delete_local_events + ) + + logger.info("[purge] finding state groups that can be deleted") + + sg_to_delete = yield self._find_unreferenced_groups(state_groups) + + yield self.stores.main.purge_unreferenced_state_groups(room_id, sg_to_delete) + + @defer.inlineCallbacks + def _find_unreferenced_groups(self, state_groups): + """Used when purging history to figure out which state groups can be + deleted. + + Args: + state_groups (set[int]): Set of state groups referenced by events + that are going to be deleted. + + Returns: + Deferred[set[int]] The set of state groups that can be deleted. + """ + # Graph of state group -> previous group + graph = {} + + # Set of events that we have found to be referenced by events + referenced_groups = set() + + # Set of state groups we've already seen + state_groups_seen = set(state_groups) + + # Set of state groups to handle next. + next_to_search = set(state_groups) + while next_to_search: + # We bound size of groups we're looking up at once, to stop the + # SQL query getting too big + if len(next_to_search) < 100: + current_search = next_to_search + next_to_search = set() + else: + current_search = set(itertools.islice(next_to_search, 100)) + next_to_search -= current_search + + referenced = yield self.stores.main.get_referenced_state_groups( + current_search + ) + referenced_groups |= referenced + + # We don't continue iterating up the state group graphs for state + # groups that are referenced. + current_search -= referenced + + edges = yield self.stores.main.get_previous_state_groups(current_search) + + prevs = set(edges.values()) + # We don't bother re-handling groups we've already seen + prevs -= state_groups_seen + next_to_search |= prevs + state_groups_seen |= prevs + + graph.update(edges) + + to_delete = state_groups_seen - referenced_groups + + return to_delete diff --git a/tests/storage/test_purge.py b/tests/storage/test_purge.py index f671599cb8..b9fafaa1a6 100644 --- a/tests/storage/test_purge.py +++ b/tests/storage/test_purge.py @@ -40,23 +40,24 @@ class PurgeTests(HomeserverTestCase): third = self.helper.send(self.room_id, body="test3") last = self.helper.send(self.room_id, body="test4") - storage = self.hs.get_datastore() + store = self.hs.get_datastore() + storage = self.hs.get_storage() # Get the topological token - event = storage.get_topological_token_for_event(last["event_id"]) + event = store.get_topological_token_for_event(last["event_id"]) self.pump() event = self.successResultOf(event) # Purge everything before this topological token - purge = storage.purge_history(self.room_id, event, True) + purge = storage.purge_events.purge_history(self.room_id, event, True) self.pump() self.assertEqual(self.successResultOf(purge), None) # Try and get the events - get_first = storage.get_event(first["event_id"]) - get_second = storage.get_event(second["event_id"]) - get_third = storage.get_event(third["event_id"]) - get_last = storage.get_event(last["event_id"]) + get_first = store.get_event(first["event_id"]) + get_second = store.get_event(second["event_id"]) + get_third = store.get_event(third["event_id"]) + get_last = store.get_event(last["event_id"]) self.pump() # 1-3 should fail and last will succeed, meaning that 1-3 are deleted From ecfba89a784db12b48074ade3a44092267ba9cf7 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 30 Oct 2019 15:14:29 +0000 Subject: [PATCH 0378/1623] Newsfile --- changelog.d/6295.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6295.misc diff --git a/changelog.d/6295.misc b/changelog.d/6295.misc new file mode 100644 index 0000000000..a3e6b8296e --- /dev/null +++ b/changelog.d/6295.misc @@ -0,0 +1 @@ +Split out state storage into separate data store. From acd16ad86a8f61ef261fa82960ee3634864db9ed Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 30 Oct 2019 15:56:33 +0000 Subject: [PATCH 0379/1623] Implement filtering --- synapse/api/filtering.py | 13 +++++++++++-- synapse/storage/data_stores/main/stream.py | 9 +++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/synapse/api/filtering.py b/synapse/api/filtering.py index 9f06556bd2..a27029c678 100644 --- a/synapse/api/filtering.py +++ b/synapse/api/filtering.py @@ -20,6 +20,7 @@ from jsonschema import FormatChecker from twisted.internet import defer +from synapse.api.constants import LabelsField from synapse.api.errors import SynapseError from synapse.storage.presence import UserPresenceState from synapse.types import RoomID, UserID @@ -66,6 +67,8 @@ ROOM_EVENT_FILTER_SCHEMA = { "contains_url": {"type": "boolean"}, "lazy_load_members": {"type": "boolean"}, "include_redundant_members": {"type": "boolean"}, + "org.matrix.labels": {"type": "array", "items": {"type": "string"}}, + "org.matrix.not_labels": {"type": "array", "items": {"type": "string"}}, }, } @@ -259,6 +262,9 @@ class Filter(object): self.contains_url = self.filter_json.get("contains_url", None) + self.labels = self.filter_json.get("org.matrix.labels", None) + self.not_labels = self.filter_json.get("org.matrix.not_labels", []) + def filters_all_types(self): return "*" in self.not_types @@ -282,6 +288,7 @@ class Filter(object): room_id = None ev_type = "m.presence" contains_url = False + labels = [] else: sender = event.get("sender", None) if not sender: @@ -300,10 +307,11 @@ class Filter(object): content = event.get("content", {}) # check if there is a string url field in the content for filtering purposes contains_url = isinstance(content.get("url"), text_type) + labels = content.get(LabelsField) - return self.check_fields(room_id, sender, ev_type, contains_url) + return self.check_fields(room_id, sender, ev_type, labels, contains_url) - def check_fields(self, room_id, sender, event_type, contains_url): + def check_fields(self, room_id, sender, event_type, labels, contains_url): """Checks whether the filter matches the given event fields. Returns: @@ -313,6 +321,7 @@ class Filter(object): "rooms": lambda v: room_id == v, "senders": lambda v: sender == v, "types": lambda v: _matches_wildcard(event_type, v), + "labels": lambda v: v in labels, } for name, match_func in literal_keys.items(): diff --git a/synapse/storage/data_stores/main/stream.py b/synapse/storage/data_stores/main/stream.py index 263999dfca..907d7f20ba 100644 --- a/synapse/storage/data_stores/main/stream.py +++ b/synapse/storage/data_stores/main/stream.py @@ -229,6 +229,14 @@ def filter_to_clause(event_filter): clauses.append("contains_url = ?") args.append(event_filter.contains_url) + # We're only applying the "labels" filter on the database query, because applying the + # "not_labels" filter via a SQL query is non-trivial. Instead, we let + # event_filter.check_fields apply it, which is not as efficient but makes the + # implementation simpler. + if event_filter.labels: + clauses.append("(%s)" % " OR ".join("label = ?" for _ in event_filter.labels)) + args.extend(event_filter.labels) + return " AND ".join(clauses), args @@ -866,6 +874,7 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): sql = ( "SELECT event_id, topological_ordering, stream_ordering" " FROM events" + " LEFT JOIN event_labels USING (event_id)" " WHERE outlier = ? AND room_id = ? AND %(bounds)s" " ORDER BY topological_ordering %(order)s," " stream_ordering %(order)s LIMIT ?" From 233b14ebe1d96bd7cc3fcca09663794490186ad2 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 30 Oct 2019 15:58:05 +0000 Subject: [PATCH 0380/1623] Add index on label --- .../storage/data_stores/main/schema/delta/56/event_labels.sql | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/synapse/storage/data_stores/main/schema/delta/56/event_labels.sql b/synapse/storage/data_stores/main/schema/delta/56/event_labels.sql index 323d797419..9550b0adaa 100644 --- a/synapse/storage/data_stores/main/schema/delta/56/event_labels.sql +++ b/synapse/storage/data_stores/main/schema/delta/56/event_labels.sql @@ -17,4 +17,6 @@ CREATE TABLE IF NOT EXISTS event_labels ( event_id TEXT, label TEXT, PRIMARY KEY(event_id, label) -); \ No newline at end of file +); + +CREATE INDEX event_labels_label_idx ON event_labels(label); From e7943f660add8b602ea5225060bd0d74e6440017 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 30 Oct 2019 16:15:04 +0000 Subject: [PATCH 0381/1623] Add unit tests --- synapse/api/filtering.py | 2 +- tests/api/test_filtering.py | 51 +++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/synapse/api/filtering.py b/synapse/api/filtering.py index a27029c678..bd91b9f018 100644 --- a/synapse/api/filtering.py +++ b/synapse/api/filtering.py @@ -307,7 +307,7 @@ class Filter(object): content = event.get("content", {}) # check if there is a string url field in the content for filtering purposes contains_url = isinstance(content.get("url"), text_type) - labels = content.get(LabelsField) + labels = content.get(LabelsField, []) return self.check_fields(room_id, sender, ev_type, labels, contains_url) diff --git a/tests/api/test_filtering.py b/tests/api/test_filtering.py index 6ba623de13..66b3c828db 100644 --- a/tests/api/test_filtering.py +++ b/tests/api/test_filtering.py @@ -19,6 +19,7 @@ import jsonschema from twisted.internet import defer +from synapse.api.constants import LabelsField from synapse.api.errors import SynapseError from synapse.api.filtering import Filter from synapse.events import FrozenEvent @@ -95,6 +96,8 @@ class FilteringTestCase(unittest.TestCase): "types": ["m.room.message"], "not_rooms": ["!726s6s6q:example.com"], "not_senders": ["@spam:example.com"], + "org.matrix.labels": ["#fun"], + "org.matrix.not_labels": ["#work"], }, "ephemeral": { "types": ["m.receipt", "m.typing"], @@ -320,6 +323,54 @@ class FilteringTestCase(unittest.TestCase): ) self.assertFalse(Filter(definition).check(event)) + def test_filter_labels(self): + definition = {"org.matrix.labels": ["#fun"]} + event = MockEvent( + sender="@foo:bar", + type="m.room.message", + room_id="!secretbase:unknown", + content={ + LabelsField: ["#fun"] + }, + ) + + self.assertTrue(Filter(definition).check(event)) + + event = MockEvent( + sender="@foo:bar", + type="m.room.message", + room_id="!secretbase:unknown", + content={ + LabelsField: ["#notfun"] + }, + ) + + self.assertFalse(Filter(definition).check(event)) + + def test_filter_not_labels(self): + definition = {"org.matrix.not_labels": ["#fun"]} + event = MockEvent( + sender="@foo:bar", + type="m.room.message", + room_id="!secretbase:unknown", + content={ + LabelsField: ["#fun"] + }, + ) + + self.assertFalse(Filter(definition).check(event)) + + event = MockEvent( + sender="@foo:bar", + type="m.room.message", + room_id="!secretbase:unknown", + content={ + LabelsField: ["#notfun"] + }, + ) + + self.assertTrue(Filter(definition).check(event)) + @defer.inlineCallbacks def test_filter_presence_match(self): user_filter_json = {"presence": {"types": ["m.*"]}} From 395683add1d569c0fdfd83d279551a3ba926f4d5 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 30 Oct 2019 16:47:37 +0000 Subject: [PATCH 0382/1623] Add integration tests for sync --- tests/rest/client/v1/utils.py | 15 +++- tests/rest/client/v2_alpha/test_sync.py | 112 +++++++++++++++++++++++- 2 files changed, 122 insertions(+), 5 deletions(-) diff --git a/tests/rest/client/v1/utils.py b/tests/rest/client/v1/utils.py index cdded88b7f..8ea0cb05ea 100644 --- a/tests/rest/client/v1/utils.py +++ b/tests/rest/client/v1/utils.py @@ -106,13 +106,22 @@ class RestHelper(object): self.auth_user_id = temp_id def send(self, room_id, body=None, txn_id=None, tok=None, expect_code=200): - if txn_id is None: - txn_id = "m%s" % (str(time.time())) if body is None: body = "body_text_here" - path = "/_matrix/client/r0/rooms/%s/send/m.room.message/%s" % (room_id, txn_id) content = {"msgtype": "m.text", "body": body} + + return self.send_event( + room_id, "m.room.message", content, txn_id, tok, expect_code + ) + + def send_event( + self, room_id, type, content={}, txn_id=None, tok=None, expect_code=200 + ): + if txn_id is None: + txn_id = "m%s" % (str(time.time())) + + path = "/_matrix/client/r0/rooms/%s/send/%s/%s" % (room_id, type, txn_id) if tok: path = path + "?access_token=%s" % tok diff --git a/tests/rest/client/v2_alpha/test_sync.py b/tests/rest/client/v2_alpha/test_sync.py index 71895094bd..0263be010f 100644 --- a/tests/rest/client/v2_alpha/test_sync.py +++ b/tests/rest/client/v2_alpha/test_sync.py @@ -12,9 +12,10 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - +import json from mock import Mock +from synapse.api.constants import EventTypes, LabelsField import synapse.rest.admin from synapse.rest.client.v1 import login, room from synapse.rest.client.v2_alpha import sync @@ -26,7 +27,12 @@ from tests.server import TimedOutException class FilterTestCase(unittest.HomeserverTestCase): user_id = "@apple:test" - servlets = [sync.register_servlets] + servlets = [ + synapse.rest.admin.register_servlets_for_client_rest_resource, + room.register_servlets, + login.register_servlets, + sync.register_servlets, + ] def make_homeserver(self, reactor, clock): @@ -70,6 +76,108 @@ class FilterTestCase(unittest.HomeserverTestCase): ) +class SyncFilterTestCase(unittest.HomeserverTestCase): + servlets = [ + synapse.rest.admin.register_servlets_for_client_rest_resource, + room.register_servlets, + login.register_servlets, + sync.register_servlets, + ] + + def test_sync_filter_labels(self): + sync_filter = json.dumps( + { + "room": { + "timeline": { + "types": [EventTypes.Message], + "org.matrix.labels": ["#fun"], + } + } + } + ) + + events = self._test_sync_filter_labels(sync_filter) + + self.assertEqual(len(events), 2, events) + self.assertEqual(events[0]["content"]["body"], "with label", events[0]) + self.assertEqual(events[1]["content"]["body"], "with label", events[1]) + + def test_sync_filter_not_labels(self): + sync_filter = json.dumps( + { + "room": { + "timeline": { + "types": [EventTypes.Message], + "org.matrix.not_labels": ["#fun"], + } + } + } + ) + + events = self._test_sync_filter_labels(sync_filter) + + self.assertEqual(len(events), 2, events) + self.assertEqual(events[0]["content"]["body"], "without label", events[0]) + self.assertEqual(events[1]["content"]["body"], "with wrong label", events[1]) + + def _test_sync_filter_labels(self, sync_filter): + user_id = self.register_user("kermit", "test") + tok = self.login("kermit", "test") + + room_id = self.helper.create_room_as(user_id, tok=tok) + + self.helper.send_event( + room_id=room_id, + type=EventTypes.Message, + content={ + "msgtype": "m.text", + "body": "with label", + LabelsField: ["#fun"], + }, + tok=tok, + ) + + self.helper.send_event( + room_id=room_id, + type=EventTypes.Message, + content={ + "msgtype": "m.text", + "body": "without label", + }, + tok=tok, + ) + + self.helper.send_event( + room_id=room_id, + type=EventTypes.Message, + content={ + "msgtype": "m.text", + "body": "with wrong label", + LabelsField: ["#work"], + }, + tok=tok, + ) + + self.helper.send_event( + room_id=room_id, + type=EventTypes.Message, + content={ + "msgtype": "m.text", + "body": "with label", + LabelsField: ["#fun"], + }, + tok=tok, + ) + + request, channel = self.make_request( + "GET", "/sync?filter=%s" % sync_filter, access_token=tok + ) + self.render(request) + self.assertEqual(channel.code, 200, channel.result) + + return channel.json_body["rooms"]["join"][room_id]["timeline"]["events"] + + class SyncTypingTests(unittest.HomeserverTestCase): servlets = [ From fe51d6cacf6e1a2da5fc3589d0bc4118342b33dd Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 30 Oct 2019 17:28:41 +0000 Subject: [PATCH 0383/1623] Add more integration testing --- synapse/storage/data_stores/main/stream.py | 2 +- tests/rest/client/v2_alpha/test_sync.py | 45 +++++++++++++++++++--- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/synapse/storage/data_stores/main/stream.py b/synapse/storage/data_stores/main/stream.py index 907d7f20ba..cfa34ba1e7 100644 --- a/synapse/storage/data_stores/main/stream.py +++ b/synapse/storage/data_stores/main/stream.py @@ -872,7 +872,7 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): args.append(int(limit)) sql = ( - "SELECT event_id, topological_ordering, stream_ordering" + "SELECT DISTINCT event_id, topological_ordering, stream_ordering" " FROM events" " LEFT JOIN event_labels USING (event_id)" " WHERE outlier = ? AND room_id = ? AND %(bounds)s" diff --git a/tests/rest/client/v2_alpha/test_sync.py b/tests/rest/client/v2_alpha/test_sync.py index 0263be010f..a1aa7d87bd 100644 --- a/tests/rest/client/v2_alpha/test_sync.py +++ b/tests/rest/client/v2_alpha/test_sync.py @@ -85,6 +85,7 @@ class SyncFilterTestCase(unittest.HomeserverTestCase): ] def test_sync_filter_labels(self): + """Test that we can filter by a label.""" sync_filter = json.dumps( { "room": { @@ -98,11 +99,12 @@ class SyncFilterTestCase(unittest.HomeserverTestCase): events = self._test_sync_filter_labels(sync_filter) - self.assertEqual(len(events), 2, events) - self.assertEqual(events[0]["content"]["body"], "with label", events[0]) - self.assertEqual(events[1]["content"]["body"], "with label", events[1]) + self.assertEqual(len(events), 2, [event["content"] for event in events]) + self.assertEqual(events[0]["content"]["body"], "with right label", events[0]) + self.assertEqual(events[1]["content"]["body"], "with right label", events[1]) def test_sync_filter_not_labels(self): + """Test that we can filter by the absence of a label.""" sync_filter = json.dumps( { "room": { @@ -116,9 +118,29 @@ class SyncFilterTestCase(unittest.HomeserverTestCase): events = self._test_sync_filter_labels(sync_filter) - self.assertEqual(len(events), 2, events) + self.assertEqual(len(events), 3, [event["content"] for event in events]) self.assertEqual(events[0]["content"]["body"], "without label", events[0]) self.assertEqual(events[1]["content"]["body"], "with wrong label", events[1]) + self.assertEqual(events[2]["content"]["body"], "with two wrong labels", events[2]) + + def test_sync_filter_labels_not_labels(self): + """Test that we can filter by both a label and the absence of another label.""" + sync_filter = json.dumps( + { + "room": { + "timeline": { + "types": [EventTypes.Message], + "org.matrix.labels": ["#work"], + "org.matrix.not_labels": ["#notfun"], + } + } + } + ) + + events = self._test_sync_filter_labels(sync_filter) + + self.assertEqual(len(events), 1, [event["content"] for event in events]) + self.assertEqual(events[0]["content"]["body"], "with wrong label", events[0]) def _test_sync_filter_labels(self, sync_filter): user_id = self.register_user("kermit", "test") @@ -131,7 +153,7 @@ class SyncFilterTestCase(unittest.HomeserverTestCase): type=EventTypes.Message, content={ "msgtype": "m.text", - "body": "with label", + "body": "with right label", LabelsField: ["#fun"], }, tok=tok, @@ -163,7 +185,18 @@ class SyncFilterTestCase(unittest.HomeserverTestCase): type=EventTypes.Message, content={ "msgtype": "m.text", - "body": "with label", + "body": "with two wrong labels", + LabelsField: ["#work", "#notfun"], + }, + tok=tok, + ) + + self.helper.send_event( + room_id=room_id, + type=EventTypes.Message, + content={ + "msgtype": "m.text", + "body": "with right label", LabelsField: ["#fun"], }, tok=tok, From d8c9109aeee58950f0fd4d9865836b82aa7aafb6 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 30 Oct 2019 17:48:22 +0000 Subject: [PATCH 0384/1623] Add integration tests for /messages --- tests/rest/client/v1/test_rooms.py | 102 ++++++++++++++++++++++++++++- 1 file changed, 101 insertions(+), 1 deletion(-) diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py index 2f2ca74611..ba2008497e 100644 --- a/tests/rest/client/v1/test_rooms.py +++ b/tests/rest/client/v1/test_rooms.py @@ -24,7 +24,7 @@ from six.moves.urllib import parse as urlparse from twisted.internet import defer import synapse.rest.admin -from synapse.api.constants import Membership +from synapse.api.constants import EventTypes, LabelsField, Membership from synapse.rest.client.v1 import login, profile, room from tests import unittest @@ -811,6 +811,106 @@ class RoomMessageListTestCase(RoomBase): self.assertTrue("chunk" in channel.json_body) self.assertTrue("end" in channel.json_body) + def test_filter_labels(self): + """Test that we can filter by a label.""" + message_filter = json.dumps({ + "types": [EventTypes.Message], + "org.matrix.labels": ["#fun"], + }) + + events = self._test_filter_labels(message_filter) + + self.assertEqual(len(events), 2, [event["content"] for event in events]) + self.assertEqual(events[0]["content"]["body"], "with right label", events[0]) + self.assertEqual(events[1]["content"]["body"], "with right label", events[1]) + + def test_filter_not_labels(self): + """Test that we can filter by the absence of a label.""" + message_filter = json.dumps({ + "types": [EventTypes.Message], + "org.matrix.not_labels": ["#fun"], + }) + + events = self._test_filter_labels(message_filter) + + self.assertEqual(len(events), 3, [event["content"] for event in events]) + self.assertEqual(events[0]["content"]["body"], "without label", events[0]) + self.assertEqual(events[1]["content"]["body"], "with wrong label", events[1]) + self.assertEqual(events[2]["content"]["body"], "with two wrong labels", events[2]) + + def test_filter_labels_not_labels(self): + """Test that we can filter by both a label and the absence of another label.""" + sync_filter = json.dumps({ + "types": [EventTypes.Message], + "org.matrix.labels": ["#work"], + "org.matrix.not_labels": ["#notfun"], + }) + + events = self._test_filter_labels(sync_filter) + + self.assertEqual(len(events), 1, [event["content"] for event in events]) + self.assertEqual(events[0]["content"]["body"], "with wrong label", events[0]) + + def _test_filter_labels(self, message_filter): + self.helper.send_event( + room_id=self.room_id, + type=EventTypes.Message, + content={ + "msgtype": "m.text", + "body": "with right label", + LabelsField: ["#fun"], + } + ) + + self.helper.send_event( + room_id=self.room_id, + type=EventTypes.Message, + content={ + "msgtype": "m.text", + "body": "without label", + } + ) + + self.helper.send_event( + room_id=self.room_id, + type=EventTypes.Message, + content={ + "msgtype": "m.text", + "body": "with wrong label", + LabelsField: ["#work"], + } + ) + + self.helper.send_event( + room_id=self.room_id, + type=EventTypes.Message, + content={ + "msgtype": "m.text", + "body": "with two wrong labels", + LabelsField: ["#work", "#notfun"], + } + ) + + self.helper.send_event( + room_id=self.room_id, + type=EventTypes.Message, + content={ + "msgtype": "m.text", + "body": "with right label", + LabelsField: ["#fun"], + } + ) + + token = "s0_0_0_0_0_0_0_0_0" + request, channel = self.make_request( + "GET", "/rooms/%s/messages?access_token=x&from=%s&filter=%s" % ( + self.room_id, token, message_filter + ) + ) + self.render(request) + + return channel.json_body["chunk"] + class RoomSearchTestCase(unittest.HomeserverTestCase): servlets = [ From 62588eae4a1a0a894f66709a38403a153d78687d Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 30 Oct 2019 17:54:40 +0000 Subject: [PATCH 0385/1623] Changelog --- changelog.d/6301.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6301.feature diff --git a/changelog.d/6301.feature b/changelog.d/6301.feature new file mode 100644 index 0000000000..b7ff3fad3b --- /dev/null +++ b/changelog.d/6301.feature @@ -0,0 +1 @@ +Implement label-based filtering. From dcc069a2e2540862c233a20037e3e59591a42431 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 30 Oct 2019 18:01:56 +0000 Subject: [PATCH 0386/1623] Lint --- synapse/storage/data_stores/main/events.py | 8 +--- tests/api/test_filtering.py | 16 ++----- tests/rest/client/v1/test_rooms.py | 49 +++++++++++----------- tests/rest/client/v2_alpha/test_sync.py | 12 +++--- 4 files changed, 35 insertions(+), 50 deletions(-) diff --git a/synapse/storage/data_stores/main/events.py b/synapse/storage/data_stores/main/events.py index f80b5f1a3f..2b900f1ce1 100644 --- a/synapse/storage/data_stores/main/events.py +++ b/synapse/storage/data_stores/main/events.py @@ -2486,13 +2486,7 @@ class EventsStore( return self._simple_insert_many_txn( txn=txn, table="event_labels", - values=[ - { - "event_id": event_id, - "label": label, - } - for label in labels - ], + values=[{"event_id": event_id, "label": label} for label in labels], ) diff --git a/tests/api/test_filtering.py b/tests/api/test_filtering.py index 66b3c828db..e004ab1ee5 100644 --- a/tests/api/test_filtering.py +++ b/tests/api/test_filtering.py @@ -329,9 +329,7 @@ class FilteringTestCase(unittest.TestCase): sender="@foo:bar", type="m.room.message", room_id="!secretbase:unknown", - content={ - LabelsField: ["#fun"] - }, + content={LabelsField: ["#fun"]}, ) self.assertTrue(Filter(definition).check(event)) @@ -340,9 +338,7 @@ class FilteringTestCase(unittest.TestCase): sender="@foo:bar", type="m.room.message", room_id="!secretbase:unknown", - content={ - LabelsField: ["#notfun"] - }, + content={LabelsField: ["#notfun"]}, ) self.assertFalse(Filter(definition).check(event)) @@ -353,9 +349,7 @@ class FilteringTestCase(unittest.TestCase): sender="@foo:bar", type="m.room.message", room_id="!secretbase:unknown", - content={ - LabelsField: ["#fun"] - }, + content={LabelsField: ["#fun"]}, ) self.assertFalse(Filter(definition).check(event)) @@ -364,9 +358,7 @@ class FilteringTestCase(unittest.TestCase): sender="@foo:bar", type="m.room.message", room_id="!secretbase:unknown", - content={ - LabelsField: ["#notfun"] - }, + content={LabelsField: ["#notfun"]}, ) self.assertTrue(Filter(definition).check(event)) diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py index ba2008497e..188f47bd7d 100644 --- a/tests/rest/client/v1/test_rooms.py +++ b/tests/rest/client/v1/test_rooms.py @@ -813,10 +813,9 @@ class RoomMessageListTestCase(RoomBase): def test_filter_labels(self): """Test that we can filter by a label.""" - message_filter = json.dumps({ - "types": [EventTypes.Message], - "org.matrix.labels": ["#fun"], - }) + message_filter = json.dumps( + {"types": [EventTypes.Message], "org.matrix.labels": ["#fun"]} + ) events = self._test_filter_labels(message_filter) @@ -826,25 +825,28 @@ class RoomMessageListTestCase(RoomBase): def test_filter_not_labels(self): """Test that we can filter by the absence of a label.""" - message_filter = json.dumps({ - "types": [EventTypes.Message], - "org.matrix.not_labels": ["#fun"], - }) + message_filter = json.dumps( + {"types": [EventTypes.Message], "org.matrix.not_labels": ["#fun"]} + ) events = self._test_filter_labels(message_filter) self.assertEqual(len(events), 3, [event["content"] for event in events]) self.assertEqual(events[0]["content"]["body"], "without label", events[0]) self.assertEqual(events[1]["content"]["body"], "with wrong label", events[1]) - self.assertEqual(events[2]["content"]["body"], "with two wrong labels", events[2]) + self.assertEqual( + events[2]["content"]["body"], "with two wrong labels", events[2] + ) def test_filter_labels_not_labels(self): """Test that we can filter by both a label and the absence of another label.""" - sync_filter = json.dumps({ - "types": [EventTypes.Message], - "org.matrix.labels": ["#work"], - "org.matrix.not_labels": ["#notfun"], - }) + sync_filter = json.dumps( + { + "types": [EventTypes.Message], + "org.matrix.labels": ["#work"], + "org.matrix.not_labels": ["#notfun"], + } + ) events = self._test_filter_labels(sync_filter) @@ -859,16 +861,13 @@ class RoomMessageListTestCase(RoomBase): "msgtype": "m.text", "body": "with right label", LabelsField: ["#fun"], - } + }, ) self.helper.send_event( room_id=self.room_id, type=EventTypes.Message, - content={ - "msgtype": "m.text", - "body": "without label", - } + content={"msgtype": "m.text", "body": "without label"}, ) self.helper.send_event( @@ -878,7 +877,7 @@ class RoomMessageListTestCase(RoomBase): "msgtype": "m.text", "body": "with wrong label", LabelsField: ["#work"], - } + }, ) self.helper.send_event( @@ -888,7 +887,7 @@ class RoomMessageListTestCase(RoomBase): "msgtype": "m.text", "body": "with two wrong labels", LabelsField: ["#work", "#notfun"], - } + }, ) self.helper.send_event( @@ -898,14 +897,14 @@ class RoomMessageListTestCase(RoomBase): "msgtype": "m.text", "body": "with right label", LabelsField: ["#fun"], - } + }, ) token = "s0_0_0_0_0_0_0_0_0" request, channel = self.make_request( - "GET", "/rooms/%s/messages?access_token=x&from=%s&filter=%s" % ( - self.room_id, token, message_filter - ) + "GET", + "/rooms/%s/messages?access_token=x&from=%s&filter=%s" + % (self.room_id, token, message_filter), ) self.render(request) diff --git a/tests/rest/client/v2_alpha/test_sync.py b/tests/rest/client/v2_alpha/test_sync.py index a1aa7d87bd..c5c199d412 100644 --- a/tests/rest/client/v2_alpha/test_sync.py +++ b/tests/rest/client/v2_alpha/test_sync.py @@ -13,10 +13,11 @@ # See the License for the specific language governing permissions and # limitations under the License. import json + from mock import Mock -from synapse.api.constants import EventTypes, LabelsField import synapse.rest.admin +from synapse.api.constants import EventTypes, LabelsField from synapse.rest.client.v1 import login, room from synapse.rest.client.v2_alpha import sync @@ -121,7 +122,9 @@ class SyncFilterTestCase(unittest.HomeserverTestCase): self.assertEqual(len(events), 3, [event["content"] for event in events]) self.assertEqual(events[0]["content"]["body"], "without label", events[0]) self.assertEqual(events[1]["content"]["body"], "with wrong label", events[1]) - self.assertEqual(events[2]["content"]["body"], "with two wrong labels", events[2]) + self.assertEqual( + events[2]["content"]["body"], "with two wrong labels", events[2] + ) def test_sync_filter_labels_not_labels(self): """Test that we can filter by both a label and the absence of another label.""" @@ -162,10 +165,7 @@ class SyncFilterTestCase(unittest.HomeserverTestCase): self.helper.send_event( room_id=room_id, type=EventTypes.Message, - content={ - "msgtype": "m.text", - "body": "without label", - }, + content={"msgtype": "m.text", "body": "without label"}, tok=tok, ) From 0467f335847dd096913dcf404ca839f61c38758f Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 30 Oct 2019 18:05:00 +0000 Subject: [PATCH 0387/1623] fix delete_existing for _persist_events (#6300) this is part of _retry_on_integrity_error, so should only be on _persist_events_and_state_updates --- changelog.d/6300.misc | 1 + synapse/storage/data_stores/main/events.py | 2 +- synapse/storage/persist_events.py | 5 +---- 3 files changed, 3 insertions(+), 5 deletions(-) create mode 100644 changelog.d/6300.misc diff --git a/changelog.d/6300.misc b/changelog.d/6300.misc new file mode 100644 index 0000000000..0b3d7a14a1 --- /dev/null +++ b/changelog.d/6300.misc @@ -0,0 +1 @@ +Move `persist_events` out from main data store. diff --git a/synapse/storage/data_stores/main/events.py b/synapse/storage/data_stores/main/events.py index 7c3607f308..a4dab86a13 100644 --- a/synapse/storage/data_stores/main/events.py +++ b/synapse/storage/data_stores/main/events.py @@ -82,7 +82,7 @@ def _retry_on_integrity_error(func): @defer.inlineCallbacks def f(self, *args, **kwargs): try: - res = yield func(self, *args, **kwargs) + res = yield func(self, *args, delete_existing=False, **kwargs) except self.database_engine.module.IntegrityError: logger.exception("IntegrityError, retrying.") res = yield func(self, *args, delete_existing=True, **kwargs) diff --git a/synapse/storage/persist_events.py b/synapse/storage/persist_events.py index cf66225574..931dcb6558 100644 --- a/synapse/storage/persist_events.py +++ b/synapse/storage/persist_events.py @@ -260,9 +260,7 @@ class EventsPersistenceStorage(object): self._event_persist_queue.handle_queue(room_id, persisting_queue) @defer.inlineCallbacks - def _persist_events( - self, events_and_contexts, backfilled=False, delete_existing=False - ): + def _persist_events(self, events_and_contexts, backfilled=False): """Calculates the change to current state and forward extremities, and persists the given events and with those updates. @@ -412,7 +410,6 @@ class EventsPersistenceStorage(object): state_delta_for_room=state_delta_for_room, new_forward_extremeties=new_forward_extremeties, backfilled=backfilled, - delete_existing=delete_existing, ) @defer.inlineCallbacks From bb6cec27a5ac6d5d6d5f67df21610a63745ac0a9 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 30 Oct 2019 14:57:34 -0400 Subject: [PATCH 0388/1623] rename get_devices_by_remote to get_device_updates_by_remote --- synapse/federation/sender/per_destination_queue.py | 4 ++-- synapse/storage/data_stores/main/devices.py | 8 ++++---- tests/handlers/test_typing.py | 4 ++-- tests/storage/test_devices.py | 12 ++++++------ 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/synapse/federation/sender/per_destination_queue.py b/synapse/federation/sender/per_destination_queue.py index d5d4a60c88..6e3012cd41 100644 --- a/synapse/federation/sender/per_destination_queue.py +++ b/synapse/federation/sender/per_destination_queue.py @@ -359,7 +359,7 @@ class PerDestinationQueue(object): last_device_list = self._last_device_list_stream_id # Retrieve list of new device updates to send to the destination - now_stream_id, results = yield self._store.get_devices_by_remote( + now_stream_id, results = yield self._store.get_device_updates_by_remote( self._destination, last_device_list, limit=limit ) edus = [ @@ -372,7 +372,7 @@ class PerDestinationQueue(object): for (edu_type, content) in results ] - assert len(edus) <= limit, "get_devices_by_remote returned too many EDUs" + assert len(edus) <= limit, "get_device_updates_by_remote returned too many EDUs" return (edus, now_stream_id) diff --git a/synapse/storage/data_stores/main/devices.py b/synapse/storage/data_stores/main/devices.py index 0b12bc58c4..717eab4159 100644 --- a/synapse/storage/data_stores/main/devices.py +++ b/synapse/storage/data_stores/main/devices.py @@ -91,7 +91,7 @@ class DeviceWorkerStore(SQLBaseStore): @trace @defer.inlineCallbacks - def get_devices_by_remote(self, destination, from_stream_id, limit): + def get_device_updates_by_remote(self, destination, from_stream_id, limit): """Get a stream of device updates to send to the given remote server. Args: @@ -123,8 +123,8 @@ class DeviceWorkerStore(SQLBaseStore): # stream_id; the rationale being that such a large device list update # is likely an error. updates = yield self.runInteraction( - "get_devices_by_remote", - self._get_devices_by_remote_txn, + "get_device_updates_by_remote", + self._get_device_updates_by_remote_txn, destination, from_stream_id, now_stream_id, @@ -241,7 +241,7 @@ class DeviceWorkerStore(SQLBaseStore): return now_stream_id, results - def _get_devices_by_remote_txn( + def _get_device_updates_by_remote_txn( self, txn, destination, from_stream_id, now_stream_id, limit ): """Return device update information for a given remote destination diff --git a/tests/handlers/test_typing.py b/tests/handlers/test_typing.py index f360c8e965..5ec568f4e6 100644 --- a/tests/handlers/test_typing.py +++ b/tests/handlers/test_typing.py @@ -73,7 +73,7 @@ class TypingNotificationsTestCase(unittest.HomeserverTestCase): "get_received_txn_response", "set_received_txn_response", "get_destination_retry_timings", - "get_devices_by_remote", + "get_device_updates_by_remote", # Bits that user_directory needs "get_user_directory_stream_pos", "get_current_state_deltas", @@ -109,7 +109,7 @@ class TypingNotificationsTestCase(unittest.HomeserverTestCase): retry_timings_res ) - self.datastore.get_devices_by_remote.return_value = (0, []) + self.datastore.get_device_updates_by_remote.return_value = (0, []) def get_received_txn_response(*args): return defer.succeed(None) diff --git a/tests/storage/test_devices.py b/tests/storage/test_devices.py index 039cc79357..6f8d990959 100644 --- a/tests/storage/test_devices.py +++ b/tests/storage/test_devices.py @@ -72,7 +72,7 @@ class DeviceStoreTestCase(tests.unittest.TestCase): ) @defer.inlineCallbacks - def test_get_devices_by_remote(self): + def test_get_device_updates_by_remote(self): device_ids = ["device_id1", "device_id2"] # Add two device updates with a single stream_id @@ -81,7 +81,7 @@ class DeviceStoreTestCase(tests.unittest.TestCase): ) # Get all device updates ever meant for this remote - now_stream_id, device_updates = yield self.store.get_devices_by_remote( + now_stream_id, device_updates = yield self.store.get_device_updates_by_remote( "somehost", -1, limit=100 ) @@ -89,7 +89,7 @@ class DeviceStoreTestCase(tests.unittest.TestCase): self._check_devices_in_updates(device_ids, device_updates) @defer.inlineCallbacks - def test_get_devices_by_remote_limited(self): + def test_get_device_updates_by_remote_limited(self): # Test breaking the update limit in 1, 101, and 1 device_id segments # first add one device @@ -115,20 +115,20 @@ class DeviceStoreTestCase(tests.unittest.TestCase): # # first we should get a single update - now_stream_id, device_updates = yield self.store.get_devices_by_remote( + now_stream_id, device_updates = yield self.store.get_device_updates_by_remote( "someotherhost", -1, limit=100 ) self._check_devices_in_updates(device_ids1, device_updates) # Then we should get an empty list back as the 101 devices broke the limit - now_stream_id, device_updates = yield self.store.get_devices_by_remote( + now_stream_id, device_updates = yield self.store.get_device_updates_by_remote( "someotherhost", now_stream_id, limit=100 ) self.assertEqual(len(device_updates), 0) # The 101 devices should've been cleared, so we should now just get one device # update - now_stream_id, device_updates = yield self.store.get_devices_by_remote( + now_stream_id, device_updates = yield self.store.get_device_updates_by_remote( "someotherhost", now_stream_id, limit=100 ) self._check_devices_in_updates(device_ids3, device_updates) From 998f7fe7d4ddb2dddf4d46a8a420a6fd7e37577c Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 30 Oct 2019 17:22:52 -0400 Subject: [PATCH 0389/1623] make user signatures a separate stream --- synapse/replication/slave/storage/devices.py | 8 +++++-- synapse/replication/tcp/streams/__init__.py | 1 + synapse/replication/tcp/streams/_base.py | 18 ++++++++++++++ synapse/storage/data_stores/main/devices.py | 13 +--------- .../data_stores/main/end_to_end_keys.py | 24 +++++++++++++++++++ 5 files changed, 50 insertions(+), 14 deletions(-) diff --git a/synapse/replication/slave/storage/devices.py b/synapse/replication/slave/storage/devices.py index c9f99a8405..f416d73b2e 100644 --- a/synapse/replication/slave/storage/devices.py +++ b/synapse/replication/slave/storage/devices.py @@ -42,7 +42,9 @@ class SlavedDeviceStore(EndToEndKeyWorkerStore, DeviceWorkerStore, BaseSlavedSto def stream_positions(self): result = super(SlavedDeviceStore, self).stream_positions() - result["device_lists"] = self._device_list_id_gen.get_current_token() + result["user_signature"] = result[ + "device_lists" + ] = self._device_list_id_gen.get_current_token() return result def process_replication_rows(self, stream_name, token, rows): @@ -50,13 +52,15 @@ class SlavedDeviceStore(EndToEndKeyWorkerStore, DeviceWorkerStore, BaseSlavedSto self._device_list_id_gen.advance(token) for row in rows: self._invalidate_caches_for_devices(token, row.user_id, row.destination) + elif stream_name == "user_signature": + for row in rows: + self._user_signature_stream_cache.entity_has_changed(row.user_id, token) return super(SlavedDeviceStore, self).process_replication_rows( stream_name, token, rows ) def _invalidate_caches_for_devices(self, token, user_id, destination): self._device_list_stream_cache.entity_has_changed(user_id, token) - self._user_signature_stream_cache.entity_has_changed(user_id, token) if destination: self._device_list_federation_stream_cache.entity_has_changed( diff --git a/synapse/replication/tcp/streams/__init__.py b/synapse/replication/tcp/streams/__init__.py index 634f636dc9..5f52264e84 100644 --- a/synapse/replication/tcp/streams/__init__.py +++ b/synapse/replication/tcp/streams/__init__.py @@ -45,5 +45,6 @@ STREAMS_MAP = { _base.TagAccountDataStream, _base.AccountDataStream, _base.GroupServerStream, + _base.UserSignatureStream, ) } diff --git a/synapse/replication/tcp/streams/_base.py b/synapse/replication/tcp/streams/_base.py index f03111c259..9e45429d49 100644 --- a/synapse/replication/tcp/streams/_base.py +++ b/synapse/replication/tcp/streams/_base.py @@ -95,6 +95,7 @@ GroupsStreamRow = namedtuple( "GroupsStreamRow", ("group_id", "user_id", "type", "content"), # str # str # str # dict ) +UserSignatureStreamRow = namedtuple("UserSignatureStreamRow", ("user_id")) # str class Stream(object): @@ -438,3 +439,20 @@ class GroupServerStream(Stream): self.update_function = store.get_all_groups_changes super(GroupServerStream, self).__init__(hs) + + +class UserSignatureStream(Stream): + """A user has signed their own device with their user-signing key + """ + + NAME = "user_signature" + _LIMITED = False + ROW_TYPE = UserSignatureStreamRow + + def __init__(self, hs): + store = hs.get_datastore() + + self.current_token = store.get_device_stream_token + self.update_function = store.get_all_user_signature_changes_for_remotes + + super(UserSignatureStream, self).__init__(hs) diff --git a/synapse/storage/data_stores/main/devices.py b/synapse/storage/data_stores/main/devices.py index a96f09ea7b..f7a3542348 100644 --- a/synapse/storage/data_stores/main/devices.py +++ b/synapse/storage/data_stores/main/devices.py @@ -543,20 +543,9 @@ class DeviceWorkerStore(SQLBaseStore): LEFT JOIN device_lists_outbound_pokes USING (stream_id, user_id, device_id) WHERE ? < stream_id AND stream_id <= ? GROUP BY user_id, destination - UNION - SELECT MAX(stream_id) AS stream_id, from_user_id AS user_id, NULL AS destination - FROM user_signature_stream - WHERE ? < stream_id AND stream_id <= ? - GROUP BY user_id """ return self._execute( - "get_all_device_list_changes_for_remotes", - None, - sql, - from_key, - to_key, - from_key, - to_key, + "get_all_device_list_changes_for_remotes", None, sql, from_key, to_key ) @cached(max_entries=10000) diff --git a/synapse/storage/data_stores/main/end_to_end_keys.py b/synapse/storage/data_stores/main/end_to_end_keys.py index a0bc6f2d18..073412a78d 100644 --- a/synapse/storage/data_stores/main/end_to_end_keys.py +++ b/synapse/storage/data_stores/main/end_to_end_keys.py @@ -315,6 +315,30 @@ class EndToEndKeyWorkerStore(SQLBaseStore): from_user_id, ) + def get_all_user_signature_changes_for_remotes(self, from_key, to_key): + """Return a list of changes from the user signature stream to notify remotes. + Note that the user signature stream represents when a user signs their + device with their user-signing key, which is not published to other + users or servers, so no `destination` is needed in the returned + list. However, this is needed to poke workers. + + Args: + from_key (int): the stream ID to start at (exclusive) + to_key (int): the stream ID to end at (inclusive) + + Returns: + Deferred[list[(int,str)]] a list of `(stream_id, user_id)` + """ + sql = """ + SELECT MAX(stream_id) AS stream_id, from_user_id AS user_id + FROM user_signature_stream + WHERE ? < stream_id AND stream_id <= ? + GROUP BY user_id + """ + return self._execute( + "get_all_user_signature_changes_for_remotes", None, sql, from_key, to_key + ) + class EndToEndKeyStore(EndToEndKeyWorkerStore, SQLBaseStore): def set_e2e_device_keys(self, user_id, device_id, time_now, device_keys): From 54fef094b31e0401d6d35efdf7d5d6b0b9e5d51f Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Thu, 31 Oct 2019 10:23:24 +0000 Subject: [PATCH 0390/1623] Remove usage of deprecated logger.warn method from codebase (#6271) Replace every instance of `logger.warn` with `logger.warning` as the former is deprecated. --- changelog.d/6271.misc | 1 + scripts/move_remote_media_to_new_store.py | 2 +- scripts/synapse_port_db | 6 ++-- synapse/api/auth.py | 2 +- synapse/app/__init__.py | 4 ++- synapse/app/appservice.py | 4 +-- synapse/app/client_reader.py | 4 +-- synapse/app/event_creator.py | 4 +-- synapse/app/federation_reader.py | 4 +-- synapse/app/federation_sender.py | 4 +-- synapse/app/frontend_proxy.py | 4 +-- synapse/app/homeserver.py | 6 ++-- synapse/app/media_repository.py | 4 +-- synapse/app/pusher.py | 4 +-- synapse/app/synchrotron.py | 4 +-- synapse/app/user_dir.py | 4 +-- synapse/config/key.py | 4 +-- synapse/config/logger.py | 2 +- synapse/event_auth.py | 2 +- synapse/federation/federation_base.py | 6 ++-- synapse/federation/federation_client.py | 8 +++-- synapse/federation/federation_server.py | 20 ++++++----- .../federation/sender/transaction_manager.py | 4 +-- synapse/federation/transport/server.py | 8 +++-- synapse/groups/attestations.py | 2 +- synapse/groups/groups_server.py | 2 +- synapse/handlers/auth.py | 6 ++-- synapse/handlers/device.py | 4 +-- synapse/handlers/devicemessage.py | 2 +- synapse/handlers/federation.py | 36 ++++++++++--------- synapse/handlers/groups_local.py | 2 +- synapse/handlers/identity.py | 6 ++-- synapse/handlers/message.py | 2 +- synapse/handlers/profile.py | 2 +- synapse/handlers/room.py | 2 +- synapse/http/client.py | 4 +-- synapse/http/federation/srv_resolver.py | 2 +- synapse/http/matrixfederationclient.py | 10 +++--- synapse/http/request_metrics.py | 2 +- synapse/http/server.py | 2 +- synapse/http/servlet.py | 4 +-- synapse/http/site.py | 4 +-- synapse/logging/context.py | 2 +- synapse/push/httppusher.py | 4 +-- synapse/push/push_rule_evaluator.py | 4 +-- synapse/replication/http/_base.py | 2 +- synapse/replication/http/membership.py | 2 +- synapse/replication/tcp/client.py | 2 +- synapse/replication/tcp/protocol.py | 2 +- synapse/rest/admin/__init__.py | 2 +- synapse/rest/client/v1/login.py | 2 +- synapse/rest/client/v2_alpha/account.py | 14 ++++---- synapse/rest/client/v2_alpha/register.py | 10 +++--- synapse/rest/client/v2_alpha/sync.py | 2 +- synapse/rest/media/v1/media_repository.py | 12 ++++--- synapse/rest/media/v1/preview_url_resource.py | 16 ++++----- synapse/rest/media/v1/thumbnail_resource.py | 4 +-- .../resource_limits_server_notices.py | 2 +- synapse/storage/_base.py | 6 ++-- synapse/storage/data_stores/main/pusher.py | 2 +- synapse/storage/data_stores/main/search.py | 2 +- synapse/util/async_helpers.py | 2 +- synapse/util/caches/__init__.py | 2 +- synapse/util/metrics.py | 6 ++-- synapse/util/rlimit.py | 2 +- 65 files changed, 164 insertions(+), 149 deletions(-) create mode 100644 changelog.d/6271.misc diff --git a/changelog.d/6271.misc b/changelog.d/6271.misc new file mode 100644 index 0000000000..2369760272 --- /dev/null +++ b/changelog.d/6271.misc @@ -0,0 +1 @@ +Replace every instance of `logger.warn` method with `logger.warning` as the former is deprecated. \ No newline at end of file diff --git a/scripts/move_remote_media_to_new_store.py b/scripts/move_remote_media_to_new_store.py index 12747c6024..b5b63933ab 100755 --- a/scripts/move_remote_media_to_new_store.py +++ b/scripts/move_remote_media_to_new_store.py @@ -72,7 +72,7 @@ def move_media(origin_server, file_id, src_paths, dest_paths): # check that the original exists original_file = src_paths.remote_media_filepath(origin_server, file_id) if not os.path.exists(original_file): - logger.warn( + logger.warning( "Original for %s/%s (%s) does not exist", origin_server, file_id, diff --git a/scripts/synapse_port_db b/scripts/synapse_port_db index 54faed1e83..0d3321682c 100755 --- a/scripts/synapse_port_db +++ b/scripts/synapse_port_db @@ -157,7 +157,7 @@ class Store( ) except self.database_engine.module.DatabaseError as e: if self.database_engine.is_deadlock(e): - logger.warn("[TXN DEADLOCK] {%s} %d/%d", desc, i, N) + logger.warning("[TXN DEADLOCK] {%s} %d/%d", desc, i, N) if i < N: i += 1 conn.rollback() @@ -432,7 +432,7 @@ class Porter(object): for row in rows: d = dict(zip(headers, row)) if "\0" in d['value']: - logger.warn('dropping search row %s', d) + logger.warning('dropping search row %s', d) else: rows_dict.append(d) @@ -647,7 +647,7 @@ class Porter(object): if isinstance(col, bytes): return bytearray(col) elif isinstance(col, string_types) and "\0" in col: - logger.warn( + logger.warning( "DROPPING ROW: NUL value in table %s col %s: %r", table, headers[j], diff --git a/synapse/api/auth.py b/synapse/api/auth.py index 53f3bb0fa8..5d0b7d2801 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -497,7 +497,7 @@ class Auth(object): token = self.get_access_token_from_request(request) service = self.store.get_app_service_by_token(token) if not service: - logger.warn("Unrecognised appservice access token.") + logger.warning("Unrecognised appservice access token.") raise InvalidClientTokenError() request.authenticated_entity = service.sender return defer.succeed(service) diff --git a/synapse/app/__init__.py b/synapse/app/__init__.py index d877c77834..a01bac2997 100644 --- a/synapse/app/__init__.py +++ b/synapse/app/__init__.py @@ -44,6 +44,8 @@ def check_bind_error(e, address, bind_addresses): bind_addresses (list): Addresses on which the service listens. """ if address == "0.0.0.0" and "::" in bind_addresses: - logger.warn("Failed to listen on 0.0.0.0, continuing because listening on [::]") + logger.warning( + "Failed to listen on 0.0.0.0, continuing because listening on [::]" + ) else: raise e diff --git a/synapse/app/appservice.py b/synapse/app/appservice.py index 767b87d2db..02b900f382 100644 --- a/synapse/app/appservice.py +++ b/synapse/app/appservice.py @@ -94,7 +94,7 @@ class AppserviceServer(HomeServer): ) elif listener["type"] == "metrics": if not self.get_config().enable_metrics: - logger.warn( + logger.warning( ( "Metrics listener configured, but " "enable_metrics is not True!" @@ -103,7 +103,7 @@ class AppserviceServer(HomeServer): else: _base.listen_metrics(listener["bind_addresses"], listener["port"]) else: - logger.warn("Unrecognized listener type: %s", listener["type"]) + logger.warning("Unrecognized listener type: %s", listener["type"]) self.get_tcp_replication().start_replication(self) diff --git a/synapse/app/client_reader.py b/synapse/app/client_reader.py index dbcc414c42..dadb487d5f 100644 --- a/synapse/app/client_reader.py +++ b/synapse/app/client_reader.py @@ -153,7 +153,7 @@ class ClientReaderServer(HomeServer): ) elif listener["type"] == "metrics": if not self.get_config().enable_metrics: - logger.warn( + logger.warning( ( "Metrics listener configured, but " "enable_metrics is not True!" @@ -162,7 +162,7 @@ class ClientReaderServer(HomeServer): else: _base.listen_metrics(listener["bind_addresses"], listener["port"]) else: - logger.warn("Unrecognized listener type: %s", listener["type"]) + logger.warning("Unrecognized listener type: %s", listener["type"]) self.get_tcp_replication().start_replication(self) diff --git a/synapse/app/event_creator.py b/synapse/app/event_creator.py index f20d810ece..d110599a35 100644 --- a/synapse/app/event_creator.py +++ b/synapse/app/event_creator.py @@ -147,7 +147,7 @@ class EventCreatorServer(HomeServer): ) elif listener["type"] == "metrics": if not self.get_config().enable_metrics: - logger.warn( + logger.warning( ( "Metrics listener configured, but " "enable_metrics is not True!" @@ -156,7 +156,7 @@ class EventCreatorServer(HomeServer): else: _base.listen_metrics(listener["bind_addresses"], listener["port"]) else: - logger.warn("Unrecognized listener type: %s", listener["type"]) + logger.warning("Unrecognized listener type: %s", listener["type"]) self.get_tcp_replication().start_replication(self) diff --git a/synapse/app/federation_reader.py b/synapse/app/federation_reader.py index 1ef027a88c..418c086254 100644 --- a/synapse/app/federation_reader.py +++ b/synapse/app/federation_reader.py @@ -132,7 +132,7 @@ class FederationReaderServer(HomeServer): ) elif listener["type"] == "metrics": if not self.get_config().enable_metrics: - logger.warn( + logger.warning( ( "Metrics listener configured, but " "enable_metrics is not True!" @@ -141,7 +141,7 @@ class FederationReaderServer(HomeServer): else: _base.listen_metrics(listener["bind_addresses"], listener["port"]) else: - logger.warn("Unrecognized listener type: %s", listener["type"]) + logger.warning("Unrecognized listener type: %s", listener["type"]) self.get_tcp_replication().start_replication(self) diff --git a/synapse/app/federation_sender.py b/synapse/app/federation_sender.py index 04fbb407af..139221ad34 100644 --- a/synapse/app/federation_sender.py +++ b/synapse/app/federation_sender.py @@ -123,7 +123,7 @@ class FederationSenderServer(HomeServer): ) elif listener["type"] == "metrics": if not self.get_config().enable_metrics: - logger.warn( + logger.warning( ( "Metrics listener configured, but " "enable_metrics is not True!" @@ -132,7 +132,7 @@ class FederationSenderServer(HomeServer): else: _base.listen_metrics(listener["bind_addresses"], listener["port"]) else: - logger.warn("Unrecognized listener type: %s", listener["type"]) + logger.warning("Unrecognized listener type: %s", listener["type"]) self.get_tcp_replication().start_replication(self) diff --git a/synapse/app/frontend_proxy.py b/synapse/app/frontend_proxy.py index 9504bfbc70..e647459d0e 100644 --- a/synapse/app/frontend_proxy.py +++ b/synapse/app/frontend_proxy.py @@ -204,7 +204,7 @@ class FrontendProxyServer(HomeServer): ) elif listener["type"] == "metrics": if not self.get_config().enable_metrics: - logger.warn( + logger.warning( ( "Metrics listener configured, but " "enable_metrics is not True!" @@ -213,7 +213,7 @@ class FrontendProxyServer(HomeServer): else: _base.listen_metrics(listener["bind_addresses"], listener["port"]) else: - logger.warn("Unrecognized listener type: %s", listener["type"]) + logger.warning("Unrecognized listener type: %s", listener["type"]) self.get_tcp_replication().start_replication(self) diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py index eb54f56853..8997c1f9e7 100644 --- a/synapse/app/homeserver.py +++ b/synapse/app/homeserver.py @@ -282,7 +282,7 @@ class SynapseHomeServer(HomeServer): reactor.addSystemEventTrigger("before", "shutdown", s.stopListening) elif listener["type"] == "metrics": if not self.get_config().enable_metrics: - logger.warn( + logger.warning( ( "Metrics listener configured, but " "enable_metrics is not True!" @@ -291,7 +291,7 @@ class SynapseHomeServer(HomeServer): else: _base.listen_metrics(listener["bind_addresses"], listener["port"]) else: - logger.warn("Unrecognized listener type: %s", listener["type"]) + logger.warning("Unrecognized listener type: %s", listener["type"]) def run_startup_checks(self, db_conn, database_engine): all_users_native = are_all_users_on_domain( @@ -569,7 +569,7 @@ def run(hs): hs.config.report_stats_endpoint, stats ) except Exception as e: - logger.warn("Error reporting stats: %s", e) + logger.warning("Error reporting stats: %s", e) def performance_stats_init(): try: diff --git a/synapse/app/media_repository.py b/synapse/app/media_repository.py index 6bc7202f33..2c6dd3ef02 100644 --- a/synapse/app/media_repository.py +++ b/synapse/app/media_repository.py @@ -120,7 +120,7 @@ class MediaRepositoryServer(HomeServer): ) elif listener["type"] == "metrics": if not self.get_config().enable_metrics: - logger.warn( + logger.warning( ( "Metrics listener configured, but " "enable_metrics is not True!" @@ -129,7 +129,7 @@ class MediaRepositoryServer(HomeServer): else: _base.listen_metrics(listener["bind_addresses"], listener["port"]) else: - logger.warn("Unrecognized listener type: %s", listener["type"]) + logger.warning("Unrecognized listener type: %s", listener["type"]) self.get_tcp_replication().start_replication(self) diff --git a/synapse/app/pusher.py b/synapse/app/pusher.py index d84732ee3c..01a5ffc363 100644 --- a/synapse/app/pusher.py +++ b/synapse/app/pusher.py @@ -114,7 +114,7 @@ class PusherServer(HomeServer): ) elif listener["type"] == "metrics": if not self.get_config().enable_metrics: - logger.warn( + logger.warning( ( "Metrics listener configured, but " "enable_metrics is not True!" @@ -123,7 +123,7 @@ class PusherServer(HomeServer): else: _base.listen_metrics(listener["bind_addresses"], listener["port"]) else: - logger.warn("Unrecognized listener type: %s", listener["type"]) + logger.warning("Unrecognized listener type: %s", listener["type"]) self.get_tcp_replication().start_replication(self) diff --git a/synapse/app/synchrotron.py b/synapse/app/synchrotron.py index 6a7e2fa707..b14da09f47 100644 --- a/synapse/app/synchrotron.py +++ b/synapse/app/synchrotron.py @@ -326,7 +326,7 @@ class SynchrotronServer(HomeServer): ) elif listener["type"] == "metrics": if not self.get_config().enable_metrics: - logger.warn( + logger.warning( ( "Metrics listener configured, but " "enable_metrics is not True!" @@ -335,7 +335,7 @@ class SynchrotronServer(HomeServer): else: _base.listen_metrics(listener["bind_addresses"], listener["port"]) else: - logger.warn("Unrecognized listener type: %s", listener["type"]) + logger.warning("Unrecognized listener type: %s", listener["type"]) self.get_tcp_replication().start_replication(self) diff --git a/synapse/app/user_dir.py b/synapse/app/user_dir.py index a5d6dc7915..6cb100319f 100644 --- a/synapse/app/user_dir.py +++ b/synapse/app/user_dir.py @@ -150,7 +150,7 @@ class UserDirectoryServer(HomeServer): ) elif listener["type"] == "metrics": if not self.get_config().enable_metrics: - logger.warn( + logger.warning( ( "Metrics listener configured, but " "enable_metrics is not True!" @@ -159,7 +159,7 @@ class UserDirectoryServer(HomeServer): else: _base.listen_metrics(listener["bind_addresses"], listener["port"]) else: - logger.warn("Unrecognized listener type: %s", listener["type"]) + logger.warning("Unrecognized listener type: %s", listener["type"]) self.get_tcp_replication().start_replication(self) diff --git a/synapse/config/key.py b/synapse/config/key.py index ec5d430afb..52ff1b2621 100644 --- a/synapse/config/key.py +++ b/synapse/config/key.py @@ -125,7 +125,7 @@ class KeyConfig(Config): # if neither trusted_key_servers nor perspectives are given, use the default. if "perspectives" not in config and "trusted_key_servers" not in config: - logger.warn(TRUSTED_KEY_SERVER_NOT_CONFIGURED_WARN) + logger.warning(TRUSTED_KEY_SERVER_NOT_CONFIGURED_WARN) key_servers = [{"server_name": "matrix.org"}] else: key_servers = config.get("trusted_key_servers", []) @@ -156,7 +156,7 @@ class KeyConfig(Config): if not self.macaroon_secret_key: # Unfortunately, there are people out there that don't have this # set. Lets just be "nice" and derive one from their secret key. - logger.warn("Config is missing macaroon_secret_key") + logger.warning("Config is missing macaroon_secret_key") seed = bytes(self.signing_key[0]) self.macaroon_secret_key = hashlib.sha256(seed).digest() diff --git a/synapse/config/logger.py b/synapse/config/logger.py index be92e33f93..2d2c1e54df 100644 --- a/synapse/config/logger.py +++ b/synapse/config/logger.py @@ -182,7 +182,7 @@ def _reload_stdlib_logging(*args, log_config=None): logger = logging.getLogger("") if not log_config: - logger.warn("Reloaded a blank config?") + logger.warning("Reloaded a blank config?") logging.config.dictConfig(log_config) diff --git a/synapse/event_auth.py b/synapse/event_auth.py index e7b722547b..ec3243b27b 100644 --- a/synapse/event_auth.py +++ b/synapse/event_auth.py @@ -77,7 +77,7 @@ def check(room_version, event, auth_events, do_sig_check=True, do_size_check=Tru if auth_events is None: # Oh, we don't know what the state of the room was, so we # are trusting that this is allowed (at least for now) - logger.warn("Trusting event: %s", event.event_id) + logger.warning("Trusting event: %s", event.event_id) return if event.type == EventTypes.Create: diff --git a/synapse/federation/federation_base.py b/synapse/federation/federation_base.py index 223aace0d9..0e22183280 100644 --- a/synapse/federation/federation_base.py +++ b/synapse/federation/federation_base.py @@ -102,7 +102,7 @@ class FederationBase(object): pass if not res: - logger.warn( + logger.warning( "Failed to find copy of %s with valid signature", pdu.event_id ) @@ -173,7 +173,7 @@ class FederationBase(object): return redacted_event if self.spam_checker.check_event_for_spam(pdu): - logger.warn( + logger.warning( "Event contains spam, redacting %s: %s", pdu.event_id, pdu.get_pdu_json(), @@ -185,7 +185,7 @@ class FederationBase(object): def errback(failure, pdu): failure.trap(SynapseError) with PreserveLoggingContext(ctx): - logger.warn( + logger.warning( "Signature check failed for %s: %s", pdu.event_id, failure.getErrorMessage(), diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index f5c1632916..595706d01a 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -522,12 +522,12 @@ class FederationClient(FederationBase): res = yield callback(destination) return res except InvalidResponseError as e: - logger.warn("Failed to %s via %s: %s", description, destination, e) + logger.warning("Failed to %s via %s: %s", description, destination, e) except HttpResponseException as e: if not 500 <= e.code < 600: raise e.to_synapse_error() else: - logger.warn( + logger.warning( "Failed to %s via %s: %i %s", description, destination, @@ -535,7 +535,9 @@ class FederationClient(FederationBase): e.args[0], ) except Exception: - logger.warn("Failed to %s via %s", description, destination, exc_info=1) + logger.warning( + "Failed to %s via %s", description, destination, exc_info=1 + ) raise SynapseError(502, "Failed to %s via any server" % (description,)) diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index d5a19764d2..d942d77a72 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -220,7 +220,7 @@ class FederationServer(FederationBase): try: await self.check_server_matches_acl(origin_host, room_id) except AuthError as e: - logger.warn("Ignoring PDUs for room %s from banned server", room_id) + logger.warning("Ignoring PDUs for room %s from banned server", room_id) for pdu in pdus_by_room[room_id]: event_id = pdu.event_id pdu_results[event_id] = e.error_dict() @@ -233,7 +233,7 @@ class FederationServer(FederationBase): await self._handle_received_pdu(origin, pdu) pdu_results[event_id] = {} except FederationError as e: - logger.warn("Error handling PDU %s: %s", event_id, e) + logger.warning("Error handling PDU %s: %s", event_id, e) pdu_results[event_id] = {"error": str(e)} except Exception as e: f = failure.Failure() @@ -333,7 +333,9 @@ class FederationServer(FederationBase): room_version = await self.store.get_room_version(room_id) if room_version not in supported_versions: - logger.warn("Room version %s not in %s", room_version, supported_versions) + logger.warning( + "Room version %s not in %s", room_version, supported_versions + ) raise IncompatibleRoomVersionError(room_version=room_version) pdu = await self.handler.on_make_join_request(origin, room_id, user_id) @@ -679,7 +681,7 @@ def server_matches_acl_event(server_name, acl_event): # server name is a literal IP allow_ip_literals = acl_event.content.get("allow_ip_literals", True) if not isinstance(allow_ip_literals, bool): - logger.warn("Ignorning non-bool allow_ip_literals flag") + logger.warning("Ignorning non-bool allow_ip_literals flag") allow_ip_literals = True if not allow_ip_literals: # check for ipv6 literals. These start with '['. @@ -693,7 +695,7 @@ def server_matches_acl_event(server_name, acl_event): # next, check the deny list deny = acl_event.content.get("deny", []) if not isinstance(deny, (list, tuple)): - logger.warn("Ignorning non-list deny ACL %s", deny) + logger.warning("Ignorning non-list deny ACL %s", deny) deny = [] for e in deny: if _acl_entry_matches(server_name, e): @@ -703,7 +705,7 @@ def server_matches_acl_event(server_name, acl_event): # then the allow list. allow = acl_event.content.get("allow", []) if not isinstance(allow, (list, tuple)): - logger.warn("Ignorning non-list allow ACL %s", allow) + logger.warning("Ignorning non-list allow ACL %s", allow) allow = [] for e in allow: if _acl_entry_matches(server_name, e): @@ -717,7 +719,7 @@ def server_matches_acl_event(server_name, acl_event): def _acl_entry_matches(server_name, acl_entry): if not isinstance(acl_entry, six.string_types): - logger.warn( + logger.warning( "Ignoring non-str ACL entry '%s' (is %s)", acl_entry, type(acl_entry) ) return False @@ -772,7 +774,7 @@ class FederationHandlerRegistry(object): async def on_edu(self, edu_type, origin, content): handler = self.edu_handlers.get(edu_type) if not handler: - logger.warn("No handler registered for EDU type %s", edu_type) + logger.warning("No handler registered for EDU type %s", edu_type) with start_active_span_from_edu(content, "handle_edu"): try: @@ -785,7 +787,7 @@ class FederationHandlerRegistry(object): def on_query(self, query_type, args): handler = self.query_handlers.get(query_type) if not handler: - logger.warn("No handler registered for query type %s", query_type) + logger.warning("No handler registered for query type %s", query_type) raise NotFoundError("No handler for Query type '%s'" % (query_type,)) return handler(args) diff --git a/synapse/federation/sender/transaction_manager.py b/synapse/federation/sender/transaction_manager.py index 5b6c79c51a..67b3e1ab6e 100644 --- a/synapse/federation/sender/transaction_manager.py +++ b/synapse/federation/sender/transaction_manager.py @@ -146,7 +146,7 @@ class TransactionManager(object): if code == 200: for e_id, r in response.get("pdus", {}).items(): if "error" in r: - logger.warn( + logger.warning( "TX [%s] {%s} Remote returned error for %s: %s", destination, txn_id, @@ -155,7 +155,7 @@ class TransactionManager(object): ) else: for p in pdus: - logger.warn( + logger.warning( "TX [%s] {%s} Failed to send event %s", destination, txn_id, diff --git a/synapse/federation/transport/server.py b/synapse/federation/transport/server.py index 0f16f21c2d..d6c23f22bd 100644 --- a/synapse/federation/transport/server.py +++ b/synapse/federation/transport/server.py @@ -202,7 +202,7 @@ def _parse_auth_header(header_bytes): sig = strip_quotes(param_dict["sig"]) return origin, key, sig except Exception as e: - logger.warn( + logger.warning( "Error parsing auth header '%s': %s", header_bytes.decode("ascii", "replace"), e, @@ -287,10 +287,12 @@ class BaseFederationServlet(object): except NoAuthenticationError: origin = None if self.REQUIRE_AUTH: - logger.warn("authenticate_request failed: missing authentication") + logger.warning( + "authenticate_request failed: missing authentication" + ) raise except Exception as e: - logger.warn("authenticate_request failed: %s", e) + logger.warning("authenticate_request failed: %s", e) raise request_tags = { diff --git a/synapse/groups/attestations.py b/synapse/groups/attestations.py index dfd7ae041b..d950a8b246 100644 --- a/synapse/groups/attestations.py +++ b/synapse/groups/attestations.py @@ -181,7 +181,7 @@ class GroupAttestionRenewer(object): elif not self.is_mine_id(user_id): destination = get_domain_from_id(user_id) else: - logger.warn( + logger.warning( "Incorrectly trying to do attestations for user: %r in %r", user_id, group_id, diff --git a/synapse/groups/groups_server.py b/synapse/groups/groups_server.py index 8f10b6adbb..29e8ffc295 100644 --- a/synapse/groups/groups_server.py +++ b/synapse/groups/groups_server.py @@ -488,7 +488,7 @@ class GroupsServerHandler(object): profile = yield self.profile_handler.get_profile_from_cache(user_id) user_profile.update(profile) except Exception as e: - logger.warn("Error getting profile for %s: %s", user_id, e) + logger.warning("Error getting profile for %s: %s", user_id, e) user_profiles.append(user_profile) return {"chunk": user_profiles, "total_user_count_estimate": len(invited_users)} diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index 333eb30625..7a0f54ca24 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -525,7 +525,7 @@ class AuthHandler(BaseHandler): result = None if not user_infos: - logger.warn("Attempted to login as %s but they do not exist", user_id) + logger.warning("Attempted to login as %s but they do not exist", user_id) elif len(user_infos) == 1: # a single match (possibly not exact) result = user_infos.popitem() @@ -534,7 +534,7 @@ class AuthHandler(BaseHandler): result = (user_id, user_infos[user_id]) else: # multiple matches, none of them exact - logger.warn( + logger.warning( "Attempted to login as %s but it matches more than one user " "inexactly: %r", user_id, @@ -728,7 +728,7 @@ class AuthHandler(BaseHandler): result = yield self.validate_hash(password, password_hash) if not result: - logger.warn("Failed password login for user %s", user_id) + logger.warning("Failed password login for user %s", user_id) return None return user_id diff --git a/synapse/handlers/device.py b/synapse/handlers/device.py index 5f23ee4488..befef2cf3d 100644 --- a/synapse/handlers/device.py +++ b/synapse/handlers/device.py @@ -656,7 +656,7 @@ class DeviceListUpdater(object): except (NotRetryingDestination, RequestSendFailed, HttpResponseException): # TODO: Remember that we are now out of sync and try again # later - logger.warn("Failed to handle device list update for %s", user_id) + logger.warning("Failed to handle device list update for %s", user_id) # We abort on exceptions rather than accepting the update # as otherwise synapse will 'forget' that its device list # is out of date. If we bail then we will retry the resync @@ -694,7 +694,7 @@ class DeviceListUpdater(object): # up on storing the total list of devices and only handle the # delta instead. if len(devices) > 1000: - logger.warn( + logger.warning( "Ignoring device list snapshot for %s as it has >1K devs (%d)", user_id, len(devices), diff --git a/synapse/handlers/devicemessage.py b/synapse/handlers/devicemessage.py index 0043cbea17..73b9e120f5 100644 --- a/synapse/handlers/devicemessage.py +++ b/synapse/handlers/devicemessage.py @@ -52,7 +52,7 @@ class DeviceMessageHandler(object): local_messages = {} sender_user_id = content["sender"] if origin != get_domain_from_id(sender_user_id): - logger.warn( + logger.warning( "Dropping device message from %r with spoofed sender %r", origin, sender_user_id, diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 08276fdebf..f1547e3039 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -181,7 +181,7 @@ class FederationHandler(BaseHandler): try: self._sanity_check_event(pdu) except SynapseError as err: - logger.warn( + logger.warning( "[%s %s] Received event failed sanity checks", room_id, event_id ) raise FederationError("ERROR", err.code, err.msg, affected=pdu.event_id) @@ -302,7 +302,7 @@ class FederationHandler(BaseHandler): # following. if sent_to_us_directly: - logger.warn( + logger.warning( "[%s %s] Rejecting: failed to fetch %d prev events: %s", room_id, event_id, @@ -406,7 +406,7 @@ class FederationHandler(BaseHandler): state = [event_map[e] for e in six.itervalues(state_map)] auth_chain = list(auth_chains) except Exception: - logger.warn( + logger.warning( "[%s %s] Error attempting to resolve state at missing " "prev_events", room_id, @@ -519,7 +519,9 @@ class FederationHandler(BaseHandler): # We failed to get the missing events, but since we need to handle # the case of `get_missing_events` not returning the necessary # events anyway, it is safe to simply log the error and continue. - logger.warn("[%s %s]: Failed to get prev_events: %s", room_id, event_id, e) + logger.warning( + "[%s %s]: Failed to get prev_events: %s", room_id, event_id, e + ) return logger.info( @@ -546,7 +548,7 @@ class FederationHandler(BaseHandler): yield self.on_receive_pdu(origin, ev, sent_to_us_directly=False) except FederationError as e: if e.code == 403: - logger.warn( + logger.warning( "[%s %s] Received prev_event %s failed history check.", room_id, event_id, @@ -1060,7 +1062,7 @@ class FederationHandler(BaseHandler): SynapseError if the event does not pass muster """ if len(ev.prev_event_ids()) > 20: - logger.warn( + logger.warning( "Rejecting event %s which has %i prev_events", ev.event_id, len(ev.prev_event_ids()), @@ -1068,7 +1070,7 @@ class FederationHandler(BaseHandler): raise SynapseError(http_client.BAD_REQUEST, "Too many prev_events") if len(ev.auth_event_ids()) > 10: - logger.warn( + logger.warning( "Rejecting event %s which has %i auth_events", ev.event_id, len(ev.auth_event_ids()), @@ -1204,7 +1206,7 @@ class FederationHandler(BaseHandler): with nested_logging_context(p.event_id): yield self.on_receive_pdu(origin, p, sent_to_us_directly=True) except Exception as e: - logger.warn( + logger.warning( "Error handling queued PDU %s from %s: %s", p.event_id, origin, e ) @@ -1251,7 +1253,7 @@ class FederationHandler(BaseHandler): builder=builder ) except AuthError as e: - logger.warn("Failed to create join to %s because %s", room_id, e) + logger.warning("Failed to create join to %s because %s", room_id, e) raise e event_allowed = yield self.third_party_event_rules.check_event_allowed( @@ -1495,7 +1497,7 @@ class FederationHandler(BaseHandler): room_version, event, context, do_sig_check=False ) except AuthError as e: - logger.warn("Failed to create new leave %r because %s", event, e) + logger.warning("Failed to create new leave %r because %s", event, e) raise e return event @@ -1789,7 +1791,7 @@ class FederationHandler(BaseHandler): # cause SynapseErrors in auth.check. We don't want to give up # the attempt to federate altogether in such cases. - logger.warn("Rejecting %s because %s", e.event_id, err.msg) + logger.warning("Rejecting %s because %s", e.event_id, err.msg) if e == event: raise @@ -1845,7 +1847,9 @@ class FederationHandler(BaseHandler): try: yield self.do_auth(origin, event, context, auth_events=auth_events) except AuthError as e: - logger.warn("[%s %s] Rejecting: %s", event.room_id, event.event_id, e.msg) + logger.warning( + "[%s %s] Rejecting: %s", event.room_id, event.event_id, e.msg + ) context.rejected = RejectedReason.AUTH_ERROR @@ -1939,7 +1943,7 @@ class FederationHandler(BaseHandler): try: event_auth.check(room_version, event, auth_events=current_auth_events) except AuthError as e: - logger.warn("Soft-failing %r because %s", event, e) + logger.warning("Soft-failing %r because %s", event, e) event.internal_metadata.soft_failed = True @defer.inlineCallbacks @@ -2038,7 +2042,7 @@ class FederationHandler(BaseHandler): try: event_auth.check(room_version, event, auth_events=auth_events) except AuthError as e: - logger.warn("Failed auth resolution for %r because %s", event, e) + logger.warning("Failed auth resolution for %r because %s", event, e) raise e @defer.inlineCallbacks @@ -2432,7 +2436,7 @@ class FederationHandler(BaseHandler): try: yield self.auth.check_from_context(room_version, event, context) except AuthError as e: - logger.warn("Denying new third party invite %r because %s", event, e) + logger.warning("Denying new third party invite %r because %s", event, e) raise e yield self._check_signature(event, context) @@ -2488,7 +2492,7 @@ class FederationHandler(BaseHandler): try: yield self.auth.check_from_context(room_version, event, context) except AuthError as e: - logger.warn("Denying third party invite %r because %s", event, e) + logger.warning("Denying third party invite %r because %s", event, e) raise e yield self._check_signature(event, context) diff --git a/synapse/handlers/groups_local.py b/synapse/handlers/groups_local.py index 46eb9ee88b..92fecbfc44 100644 --- a/synapse/handlers/groups_local.py +++ b/synapse/handlers/groups_local.py @@ -392,7 +392,7 @@ class GroupsLocalHandler(object): try: user_profile = yield self.profile_handler.get_profile(user_id) except Exception as e: - logger.warn("No profile for user %s: %s", user_id, e) + logger.warning("No profile for user %s: %s", user_id, e) user_profile = {} return {"state": "invite", "user_profile": user_profile} diff --git a/synapse/handlers/identity.py b/synapse/handlers/identity.py index ba99ddf76d..000fbf090f 100644 --- a/synapse/handlers/identity.py +++ b/synapse/handlers/identity.py @@ -272,7 +272,7 @@ class IdentityHandler(BaseHandler): changed = False if e.code in (400, 404, 501): # The remote server probably doesn't support unbinding (yet) - logger.warn("Received %d response while unbinding threepid", e.code) + logger.warning("Received %d response while unbinding threepid", e.code) else: logger.error("Failed to unbind threepid on identity server: %s", e) raise SynapseError(500, "Failed to contact identity server") @@ -403,7 +403,7 @@ class IdentityHandler(BaseHandler): if self.hs.config.using_identity_server_from_trusted_list: # Warn that a deprecated config option is in use - logger.warn( + logger.warning( 'The config option "trust_identity_server_for_password_resets" ' 'has been replaced by "account_threepid_delegate". ' "Please consult the sample config at docs/sample_config.yaml for " @@ -457,7 +457,7 @@ class IdentityHandler(BaseHandler): if self.hs.config.using_identity_server_from_trusted_list: # Warn that a deprecated config option is in use - logger.warn( + logger.warning( 'The config option "trust_identity_server_for_password_resets" ' 'has been replaced by "account_threepid_delegate". ' "Please consult the sample config at docs/sample_config.yaml for " diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index 7908a2d52c..5698e5fee0 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -688,7 +688,7 @@ class EventCreationHandler(object): try: yield self.auth.check_from_context(room_version, event, context) except AuthError as err: - logger.warn("Denying new event %r because %s", event, err) + logger.warning("Denying new event %r because %s", event, err) raise err # Ensure that we can round trip before trying to persist in db diff --git a/synapse/handlers/profile.py b/synapse/handlers/profile.py index 8690f69d45..22e0a04da4 100644 --- a/synapse/handlers/profile.py +++ b/synapse/handlers/profile.py @@ -275,7 +275,7 @@ class BaseProfileHandler(BaseHandler): ratelimit=False, # Try to hide that these events aren't atomic. ) except Exception as e: - logger.warn( + logger.warning( "Failed to update join event for room %s - %s", room_id, str(e) ) diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 2816bd8f87..445a08f445 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -922,7 +922,7 @@ class RoomEventSource(object): from_token = RoomStreamToken.parse(from_key) if from_token.topological: - logger.warn("Stream has topological part!!!! %r", from_key) + logger.warning("Stream has topological part!!!! %r", from_key) from_key = "s%s" % (from_token.stream,) app_service = self.store.get_app_service_by_user_id(user.to_string()) diff --git a/synapse/http/client.py b/synapse/http/client.py index cdf828a4ff..2df5b383b5 100644 --- a/synapse/http/client.py +++ b/synapse/http/client.py @@ -535,7 +535,7 @@ class SimpleHttpClient(object): b"Content-Length" in resp_headers and int(resp_headers[b"Content-Length"][0]) > max_size ): - logger.warn("Requested URL is too large > %r bytes" % (self.max_size,)) + logger.warning("Requested URL is too large > %r bytes" % (self.max_size,)) raise SynapseError( 502, "Requested file is too large > %r bytes" % (self.max_size,), @@ -543,7 +543,7 @@ class SimpleHttpClient(object): ) if response.code > 299: - logger.warn("Got %d when downloading %s" % (response.code, url)) + logger.warning("Got %d when downloading %s" % (response.code, url)) raise SynapseError(502, "Got error %d" % (response.code,), Codes.UNKNOWN) # TODO: if our Content-Type is HTML or something, just read the first diff --git a/synapse/http/federation/srv_resolver.py b/synapse/http/federation/srv_resolver.py index 3fe4ffb9e5..021b233a7d 100644 --- a/synapse/http/federation/srv_resolver.py +++ b/synapse/http/federation/srv_resolver.py @@ -148,7 +148,7 @@ class SrvResolver(object): # Try something in the cache, else rereaise cache_entry = self._cache.get(service_name, None) if cache_entry: - logger.warn( + logger.warning( "Failed to resolve %r, falling back to cache. %r", service_name, e ) return list(cache_entry) diff --git a/synapse/http/matrixfederationclient.py b/synapse/http/matrixfederationclient.py index 3f7c93ffcb..691380abda 100644 --- a/synapse/http/matrixfederationclient.py +++ b/synapse/http/matrixfederationclient.py @@ -149,7 +149,7 @@ def _handle_json_response(reactor, timeout_sec, request, response): body = yield make_deferred_yieldable(d) except Exception as e: - logger.warn( + logger.warning( "{%s} [%s] Error reading response: %s", request.txn_id, request.destination, @@ -457,7 +457,7 @@ class MatrixFederationHttpClient(object): except Exception as e: # Eh, we're already going to raise an exception so lets # ignore if this fails. - logger.warn( + logger.warning( "{%s} [%s] Failed to get error response: %s %s: %s", request.txn_id, request.destination, @@ -478,7 +478,7 @@ class MatrixFederationHttpClient(object): break except RequestSendFailed as e: - logger.warn( + logger.warning( "{%s} [%s] Request failed: %s %s: %s", request.txn_id, request.destination, @@ -513,7 +513,7 @@ class MatrixFederationHttpClient(object): raise except Exception as e: - logger.warn( + logger.warning( "{%s} [%s] Request failed: %s %s: %s", request.txn_id, request.destination, @@ -889,7 +889,7 @@ class MatrixFederationHttpClient(object): d.addTimeout(self.default_timeout, self.reactor) length = yield make_deferred_yieldable(d) except Exception as e: - logger.warn( + logger.warning( "{%s} [%s] Error reading response: %s", request.txn_id, request.destination, diff --git a/synapse/http/request_metrics.py b/synapse/http/request_metrics.py index 46af27c8f6..58f9cc61c8 100644 --- a/synapse/http/request_metrics.py +++ b/synapse/http/request_metrics.py @@ -170,7 +170,7 @@ class RequestMetrics(object): tag = context.tag if context != self.start_context: - logger.warn( + logger.warning( "Context have unexpectedly changed %r, %r", context, self.start_context, diff --git a/synapse/http/server.py b/synapse/http/server.py index 2ccb210fd6..943d12c907 100644 --- a/synapse/http/server.py +++ b/synapse/http/server.py @@ -454,7 +454,7 @@ def respond_with_json( # the Deferred fires, but since the flag is RIGHT THERE it seems like # a waste. if request._disconnected: - logger.warn( + logger.warning( "Not sending response to request %s, already disconnected.", request ) return diff --git a/synapse/http/servlet.py b/synapse/http/servlet.py index 274c1a6a87..e9a5e46ced 100644 --- a/synapse/http/servlet.py +++ b/synapse/http/servlet.py @@ -219,13 +219,13 @@ def parse_json_value_from_request(request, allow_empty_body=False): try: content_unicode = content_bytes.decode("utf8") except UnicodeDecodeError: - logger.warn("Unable to decode UTF-8") + logger.warning("Unable to decode UTF-8") raise SynapseError(400, "Content not JSON.", errcode=Codes.NOT_JSON) try: content = json.loads(content_unicode) except Exception as e: - logger.warn("Unable to parse JSON: %s", e) + logger.warning("Unable to parse JSON: %s", e) raise SynapseError(400, "Content not JSON.", errcode=Codes.NOT_JSON) return content diff --git a/synapse/http/site.py b/synapse/http/site.py index df5274c177..ff8184a3d0 100644 --- a/synapse/http/site.py +++ b/synapse/http/site.py @@ -199,7 +199,7 @@ class SynapseRequest(Request): # It's useful to log it here so that we can get an idea of when # the client disconnects. with PreserveLoggingContext(self.logcontext): - logger.warn( + logger.warning( "Error processing request %r: %s %s", self, reason.type, reason.value ) @@ -305,7 +305,7 @@ class SynapseRequest(Request): try: self.request_metrics.stop(self.finish_time, self.code, self.sentLength) except Exception as e: - logger.warn("Failed to stop metrics: %r", e) + logger.warning("Failed to stop metrics: %r", e) class XForwardedForRequest(SynapseRequest): diff --git a/synapse/logging/context.py b/synapse/logging/context.py index 370000e377..2c1fb9ddac 100644 --- a/synapse/logging/context.py +++ b/synapse/logging/context.py @@ -294,7 +294,7 @@ class LoggingContext(object): """Enters this logging context into thread local storage""" old_context = self.set_current_context(self) if self.previous_context != old_context: - logger.warn( + logger.warning( "Expected previous context %r, found %r", self.previous_context, old_context, diff --git a/synapse/push/httppusher.py b/synapse/push/httppusher.py index 6299587808..23d3678420 100644 --- a/synapse/push/httppusher.py +++ b/synapse/push/httppusher.py @@ -246,7 +246,7 @@ class HttpPusher(object): # we really only give up so that if the URL gets # fixed, we don't suddenly deliver a load # of old notifications. - logger.warn( + logger.warning( "Giving up on a notification to user %s, " "pushkey %s", self.user_id, self.pushkey, @@ -299,7 +299,7 @@ class HttpPusher(object): if pk != self.pushkey: # for sanity, we only remove the pushkey if it # was the one we actually sent... - logger.warn( + logger.warning( ("Ignoring rejected pushkey %s because we" " didn't send it"), pk, ) diff --git a/synapse/push/push_rule_evaluator.py b/synapse/push/push_rule_evaluator.py index 5ed9147de4..b1587183a8 100644 --- a/synapse/push/push_rule_evaluator.py +++ b/synapse/push/push_rule_evaluator.py @@ -117,7 +117,7 @@ class PushRuleEvaluatorForEvent(object): pattern = UserID.from_string(user_id).localpart if not pattern: - logger.warn("event_match condition with no pattern") + logger.warning("event_match condition with no pattern") return False # XXX: optimisation: cache our pattern regexps @@ -173,7 +173,7 @@ def _glob_matches(glob, value, word_boundary=False): regex_cache[(glob, word_boundary)] = r return r.search(value) except re.error: - logger.warn("Failed to parse glob to regex: %r", glob) + logger.warning("Failed to parse glob to regex: %r", glob) return False diff --git a/synapse/replication/http/_base.py b/synapse/replication/http/_base.py index 9be37cd998..c8056b0c0c 100644 --- a/synapse/replication/http/_base.py +++ b/synapse/replication/http/_base.py @@ -180,7 +180,7 @@ class ReplicationEndpoint(object): if e.code != 504 or not cls.RETRY_ON_TIMEOUT: raise - logger.warn("%s request timed out", cls.NAME) + logger.warning("%s request timed out", cls.NAME) # If we timed out we probably don't need to worry about backing # off too much, but lets just wait a little anyway. diff --git a/synapse/replication/http/membership.py b/synapse/replication/http/membership.py index b5f5f13a62..cc1f249740 100644 --- a/synapse/replication/http/membership.py +++ b/synapse/replication/http/membership.py @@ -144,7 +144,7 @@ class ReplicationRemoteRejectInviteRestServlet(ReplicationEndpoint): # The 'except' clause is very broad, but we need to # capture everything from DNS failures upwards # - logger.warn("Failed to reject invite: %s", e) + logger.warning("Failed to reject invite: %s", e) await self.store.locally_reject_invite(user_id, room_id) ret = {} diff --git a/synapse/replication/tcp/client.py b/synapse/replication/tcp/client.py index a44ceb00e7..563ce0fc53 100644 --- a/synapse/replication/tcp/client.py +++ b/synapse/replication/tcp/client.py @@ -168,7 +168,7 @@ class ReplicationClientHandler(object): if self.connection: self.connection.send_command(cmd) else: - logger.warn("Queuing command as not connected: %r", cmd.NAME) + logger.warning("Queuing command as not connected: %r", cmd.NAME) self.pending_commands.append(cmd) def send_federation_ack(self, token): diff --git a/synapse/replication/tcp/protocol.py b/synapse/replication/tcp/protocol.py index 5ffdf2675d..b64f3f44b5 100644 --- a/synapse/replication/tcp/protocol.py +++ b/synapse/replication/tcp/protocol.py @@ -249,7 +249,7 @@ class BaseReplicationStreamProtocol(LineOnlyReceiver): return handler(cmd) def close(self): - logger.warn("[%s] Closing connection", self.id()) + logger.warning("[%s] Closing connection", self.id()) self.time_we_closed = self.clock.time_msec() self.transport.loseConnection() self.on_connection_closed() diff --git a/synapse/rest/admin/__init__.py b/synapse/rest/admin/__init__.py index 939418ee2b..5c2a2eb593 100644 --- a/synapse/rest/admin/__init__.py +++ b/synapse/rest/admin/__init__.py @@ -286,7 +286,7 @@ class PurgeHistoryRestServlet(RestServlet): room_id, stream_ordering ) if not r: - logger.warn( + logger.warning( "[purge] purging events not possible: No event found " "(received_ts %i => stream_ordering %i)", ts, diff --git a/synapse/rest/client/v1/login.py b/synapse/rest/client/v1/login.py index 8414af08cb..39a5c5e9de 100644 --- a/synapse/rest/client/v1/login.py +++ b/synapse/rest/client/v1/login.py @@ -221,7 +221,7 @@ class LoginRestServlet(RestServlet): medium, address ) if not user_id: - logger.warn( + logger.warning( "unknown 3pid identifier medium %s, address %r", medium, address ) raise LoginError(403, "", errcode=Codes.FORBIDDEN) diff --git a/synapse/rest/client/v2_alpha/account.py b/synapse/rest/client/v2_alpha/account.py index 80cf7126a0..332d7138b1 100644 --- a/synapse/rest/client/v2_alpha/account.py +++ b/synapse/rest/client/v2_alpha/account.py @@ -71,7 +71,7 @@ class EmailPasswordRequestTokenRestServlet(RestServlet): def on_POST(self, request): if self.config.threepid_behaviour_email == ThreepidBehaviour.OFF: if self.config.local_threepid_handling_disabled_due_to_email_config: - logger.warn( + logger.warning( "User password resets have been disabled due to lack of email config" ) raise SynapseError( @@ -162,7 +162,7 @@ class PasswordResetSubmitTokenServlet(RestServlet): ) if self.config.threepid_behaviour_email == ThreepidBehaviour.OFF: if self.config.local_threepid_handling_disabled_due_to_email_config: - logger.warn( + logger.warning( "Password reset emails have been disabled due to lack of an email config" ) raise SynapseError( @@ -183,7 +183,7 @@ class PasswordResetSubmitTokenServlet(RestServlet): # Perform a 302 redirect if next_link is set if next_link: if next_link.startswith("file:///"): - logger.warn( + logger.warning( "Not redirecting to next_link as it is a local file: address" ) else: @@ -350,7 +350,7 @@ class EmailThreepidRequestTokenRestServlet(RestServlet): def on_POST(self, request): if self.config.threepid_behaviour_email == ThreepidBehaviour.OFF: if self.config.local_threepid_handling_disabled_due_to_email_config: - logger.warn( + logger.warning( "Adding emails have been disabled due to lack of an email config" ) raise SynapseError( @@ -441,7 +441,7 @@ class MsisdnThreepidRequestTokenRestServlet(RestServlet): raise SynapseError(400, "MSISDN is already in use", Codes.THREEPID_IN_USE) if not self.hs.config.account_threepid_delegate_msisdn: - logger.warn( + logger.warning( "No upstream msisdn account_threepid_delegate configured on the server to " "handle this request" ) @@ -488,7 +488,7 @@ class AddThreepidEmailSubmitTokenServlet(RestServlet): def on_GET(self, request): if self.config.threepid_behaviour_email == ThreepidBehaviour.OFF: if self.config.local_threepid_handling_disabled_due_to_email_config: - logger.warn( + logger.warning( "Adding emails have been disabled due to lack of an email config" ) raise SynapseError( @@ -515,7 +515,7 @@ class AddThreepidEmailSubmitTokenServlet(RestServlet): # Perform a 302 redirect if next_link is set if next_link: if next_link.startswith("file:///"): - logger.warn( + logger.warning( "Not redirecting to next_link as it is a local file: address" ) else: diff --git a/synapse/rest/client/v2_alpha/register.py b/synapse/rest/client/v2_alpha/register.py index 4f24a124a6..6c7d25d411 100644 --- a/synapse/rest/client/v2_alpha/register.py +++ b/synapse/rest/client/v2_alpha/register.py @@ -106,7 +106,7 @@ class EmailRegisterRequestTokenRestServlet(RestServlet): def on_POST(self, request): if self.hs.config.threepid_behaviour_email == ThreepidBehaviour.OFF: if self.hs.config.local_threepid_handling_disabled_due_to_email_config: - logger.warn( + logger.warning( "Email registration has been disabled due to lack of email config" ) raise SynapseError( @@ -207,7 +207,7 @@ class MsisdnRegisterRequestTokenRestServlet(RestServlet): ) if not self.hs.config.account_threepid_delegate_msisdn: - logger.warn( + logger.warning( "No upstream msisdn account_threepid_delegate configured on the server to " "handle this request" ) @@ -266,7 +266,7 @@ class RegistrationSubmitTokenServlet(RestServlet): ) if self.config.threepid_behaviour_email == ThreepidBehaviour.OFF: if self.config.local_threepid_handling_disabled_due_to_email_config: - logger.warn( + logger.warning( "User registration via email has been disabled due to lack of email config" ) raise SynapseError( @@ -287,7 +287,7 @@ class RegistrationSubmitTokenServlet(RestServlet): # Perform a 302 redirect if next_link is set if next_link: if next_link.startswith("file:///"): - logger.warn( + logger.warning( "Not redirecting to next_link as it is a local file: address" ) else: @@ -480,7 +480,7 @@ class RegisterRestServlet(RestServlet): # a password to work around a client bug where it sent # the 'initial_device_display_name' param alone, wiping out # the original registration params - logger.warn("Ignoring initial_device_display_name without password") + logger.warning("Ignoring initial_device_display_name without password") del body["initial_device_display_name"] session_id = self.auth_handler.get_session_id(body) diff --git a/synapse/rest/client/v2_alpha/sync.py b/synapse/rest/client/v2_alpha/sync.py index 541a6b0e10..ccd8b17b23 100644 --- a/synapse/rest/client/v2_alpha/sync.py +++ b/synapse/rest/client/v2_alpha/sync.py @@ -394,7 +394,7 @@ class SyncRestServlet(RestServlet): # We've had bug reports that events were coming down under the # wrong room. if event.room_id != room.room_id: - logger.warn( + logger.warning( "Event %r is under room %r instead of %r", event.event_id, room.room_id, diff --git a/synapse/rest/media/v1/media_repository.py b/synapse/rest/media/v1/media_repository.py index b972e152a9..bd9186fe50 100644 --- a/synapse/rest/media/v1/media_repository.py +++ b/synapse/rest/media/v1/media_repository.py @@ -363,7 +363,7 @@ class MediaRepository(object): }, ) except RequestSendFailed as e: - logger.warn( + logger.warning( "Request failed fetching remote media %s/%s: %r", server_name, media_id, @@ -372,7 +372,7 @@ class MediaRepository(object): raise SynapseError(502, "Failed to fetch remote media") except HttpResponseException as e: - logger.warn( + logger.warning( "HTTP error fetching remote media %s/%s: %s", server_name, media_id, @@ -383,10 +383,12 @@ class MediaRepository(object): raise SynapseError(502, "Failed to fetch remote media") except SynapseError: - logger.warn("Failed to fetch remote media %s/%s", server_name, media_id) + logger.warning( + "Failed to fetch remote media %s/%s", server_name, media_id + ) raise except NotRetryingDestination: - logger.warn("Not retrying destination %r", server_name) + logger.warning("Not retrying destination %r", server_name) raise SynapseError(502, "Failed to fetch remote media") except Exception: logger.exception( @@ -691,7 +693,7 @@ class MediaRepository(object): try: os.remove(full_path) except OSError as e: - logger.warn("Failed to remove file: %r", full_path) + logger.warning("Failed to remove file: %r", full_path) if e.errno == errno.ENOENT: pass else: diff --git a/synapse/rest/media/v1/preview_url_resource.py b/synapse/rest/media/v1/preview_url_resource.py index 094ebad770..5a25b6b3fc 100644 --- a/synapse/rest/media/v1/preview_url_resource.py +++ b/synapse/rest/media/v1/preview_url_resource.py @@ -136,7 +136,7 @@ class PreviewUrlResource(DirectServeResource): match = False continue if match: - logger.warn("URL %s blocked by url_blacklist entry %s", url, entry) + logger.warning("URL %s blocked by url_blacklist entry %s", url, entry) raise SynapseError( 403, "URL blocked by url pattern blacklist entry", Codes.UNKNOWN ) @@ -208,7 +208,7 @@ class PreviewUrlResource(DirectServeResource): og["og:image:width"] = dims["width"] og["og:image:height"] = dims["height"] else: - logger.warn("Couldn't get dims for %s" % url) + logger.warning("Couldn't get dims for %s" % url) # define our OG response for this media elif _is_html(media_info["media_type"]): @@ -256,7 +256,7 @@ class PreviewUrlResource(DirectServeResource): og["og:image:width"] = dims["width"] og["og:image:height"] = dims["height"] else: - logger.warn("Couldn't get dims for %s", og["og:image"]) + logger.warning("Couldn't get dims for %s", og["og:image"]) og["og:image"] = "mxc://%s/%s" % ( self.server_name, @@ -267,7 +267,7 @@ class PreviewUrlResource(DirectServeResource): else: del og["og:image"] else: - logger.warn("Failed to find any OG data in %s", url) + logger.warning("Failed to find any OG data in %s", url) og = {} logger.debug("Calculated OG for %s as %s", url, og) @@ -319,7 +319,7 @@ class PreviewUrlResource(DirectServeResource): ) except Exception as e: # FIXME: pass through 404s and other error messages nicely - logger.warn("Error downloading %s: %r", url, e) + logger.warning("Error downloading %s: %r", url, e) raise SynapseError( 500, @@ -400,7 +400,7 @@ class PreviewUrlResource(DirectServeResource): except OSError as e: # If the path doesn't exist, meh if e.errno != errno.ENOENT: - logger.warn("Failed to remove media: %r: %s", media_id, e) + logger.warning("Failed to remove media: %r: %s", media_id, e) continue removed_media.append(media_id) @@ -432,7 +432,7 @@ class PreviewUrlResource(DirectServeResource): except OSError as e: # If the path doesn't exist, meh if e.errno != errno.ENOENT: - logger.warn("Failed to remove media: %r: %s", media_id, e) + logger.warning("Failed to remove media: %r: %s", media_id, e) continue try: @@ -448,7 +448,7 @@ class PreviewUrlResource(DirectServeResource): except OSError as e: # If the path doesn't exist, meh if e.errno != errno.ENOENT: - logger.warn("Failed to remove media: %r: %s", media_id, e) + logger.warning("Failed to remove media: %r: %s", media_id, e) continue removed_media.append(media_id) diff --git a/synapse/rest/media/v1/thumbnail_resource.py b/synapse/rest/media/v1/thumbnail_resource.py index 08329884ac..931ce79be8 100644 --- a/synapse/rest/media/v1/thumbnail_resource.py +++ b/synapse/rest/media/v1/thumbnail_resource.py @@ -182,7 +182,7 @@ class ThumbnailResource(DirectServeResource): if file_path: yield respond_with_file(request, desired_type, file_path) else: - logger.warn("Failed to generate thumbnail") + logger.warning("Failed to generate thumbnail") respond_404(request) @defer.inlineCallbacks @@ -245,7 +245,7 @@ class ThumbnailResource(DirectServeResource): if file_path: yield respond_with_file(request, desired_type, file_path) else: - logger.warn("Failed to generate thumbnail") + logger.warning("Failed to generate thumbnail") respond_404(request) @defer.inlineCallbacks diff --git a/synapse/server_notices/resource_limits_server_notices.py b/synapse/server_notices/resource_limits_server_notices.py index c0e7f475c9..9fae2e0afe 100644 --- a/synapse/server_notices/resource_limits_server_notices.py +++ b/synapse/server_notices/resource_limits_server_notices.py @@ -83,7 +83,7 @@ class ResourceLimitsServerNotices(object): room_id = yield self._server_notices_manager.get_notice_room_for_user(user_id) if not room_id: - logger.warn("Failed to get server notices room") + logger.warning("Failed to get server notices room") return yield self._check_and_set_tags(user_id, room_id) diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py index f5906fcd54..1a2b7ebe25 100644 --- a/synapse/storage/_base.py +++ b/synapse/storage/_base.py @@ -494,7 +494,7 @@ class SQLBaseStore(object): exception_callbacks = [] if LoggingContext.current_context() == LoggingContext.sentinel: - logger.warn("Starting db txn '%s' from sentinel context", desc) + logger.warning("Starting db txn '%s' from sentinel context", desc) try: result = yield self.runWithConnection( @@ -532,7 +532,7 @@ class SQLBaseStore(object): """ parent_context = LoggingContext.current_context() if parent_context == LoggingContext.sentinel: - logger.warn( + logger.warning( "Starting db connection from sentinel context: metrics will be lost" ) parent_context = None @@ -719,7 +719,7 @@ class SQLBaseStore(object): raise # presumably we raced with another transaction: let's retry. - logger.warn( + logger.warning( "IntegrityError when upserting into %s; retrying: %s", table, e ) diff --git a/synapse/storage/data_stores/main/pusher.py b/synapse/storage/data_stores/main/pusher.py index f005c1ae0a..d76861cdc0 100644 --- a/synapse/storage/data_stores/main/pusher.py +++ b/synapse/storage/data_stores/main/pusher.py @@ -44,7 +44,7 @@ class PusherWorkerStore(SQLBaseStore): r["data"] = json.loads(dataJson) except Exception as e: - logger.warn( + logger.warning( "Invalid JSON in data for pusher %d: %s, %s", r["id"], dataJson, diff --git a/synapse/storage/data_stores/main/search.py b/synapse/storage/data_stores/main/search.py index 0e08497452..a59b8331e1 100644 --- a/synapse/storage/data_stores/main/search.py +++ b/synapse/storage/data_stores/main/search.py @@ -196,7 +196,7 @@ class SearchBackgroundUpdateStore(BackgroundUpdateStore): " ON event_search USING GIN (vector)" ) except psycopg2.ProgrammingError as e: - logger.warn( + logger.warning( "Ignoring error %r when trying to switch from GIST to GIN", e ) diff --git a/synapse/util/async_helpers.py b/synapse/util/async_helpers.py index b60a604474..5c4de2e69f 100644 --- a/synapse/util/async_helpers.py +++ b/synapse/util/async_helpers.py @@ -309,7 +309,7 @@ class Linearizer(object): ) else: - logger.warn( + logger.warning( "Unexpected exception waiting for linearizer lock %r for key %r", self.name, key, diff --git a/synapse/util/caches/__init__.py b/synapse/util/caches/__init__.py index 43fd65d693..da5077b471 100644 --- a/synapse/util/caches/__init__.py +++ b/synapse/util/caches/__init__.py @@ -107,7 +107,7 @@ def register_cache(cache_type, cache_name, cache, collect_callback=None): if collect_callback: collect_callback() except Exception as e: - logger.warn("Error calculating metrics for %s: %s", cache_name, e) + logger.warning("Error calculating metrics for %s: %s", cache_name, e) raise yield GaugeMetricFamily("__unused", "") diff --git a/synapse/util/metrics.py b/synapse/util/metrics.py index 4b1bcdf23c..3286804322 100644 --- a/synapse/util/metrics.py +++ b/synapse/util/metrics.py @@ -119,7 +119,7 @@ class Measure(object): context = LoggingContext.current_context() if context != self.start_context: - logger.warn( + logger.warning( "Context has unexpectedly changed from '%s' to '%s'. (%r)", self.start_context, context, @@ -128,7 +128,7 @@ class Measure(object): return if not context: - logger.warn("Expected context. (%r)", self.name) + logger.warning("Expected context. (%r)", self.name) return current = context.get_resource_usage() @@ -140,7 +140,7 @@ class Measure(object): block_db_txn_duration.labels(self.name).inc(usage.db_txn_duration_sec) block_db_sched_duration.labels(self.name).inc(usage.db_sched_duration_sec) except ValueError: - logger.warn( + logger.warning( "Failed to save metrics! OLD: %r, NEW: %r", self.start_usage, current ) diff --git a/synapse/util/rlimit.py b/synapse/util/rlimit.py index 6c0f2bb0cf..207cd17c2a 100644 --- a/synapse/util/rlimit.py +++ b/synapse/util/rlimit.py @@ -33,4 +33,4 @@ def change_resource_limit(soft_file_no): resource.RLIMIT_CORE, (resource.RLIM_INFINITY, resource.RLIM_INFINITY) ) except (ValueError, resource.error) as e: - logger.warn("Failed to set file or core limit: %s", e) + logger.warning("Failed to set file or core limit: %s", e) From c6bcd388414c88fff1418de072af60906c001a10 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 31 Oct 2019 11:17:23 +0000 Subject: [PATCH 0391/1623] Fix /purge_room API. It fails trying to clean the `topic` table which was recently removed. --- synapse/storage/data_stores/main/events.py | 1 - 1 file changed, 1 deletion(-) diff --git a/synapse/storage/data_stores/main/events.py b/synapse/storage/data_stores/main/events.py index a4dab86a13..64a8a05279 100644 --- a/synapse/storage/data_stores/main/events.py +++ b/synapse/storage/data_stores/main/events.py @@ -1838,7 +1838,6 @@ class EventsStore( "room_stats_earliest_token", "rooms", "stream_ordering_to_exterm", - "topics", "users_in_public_rooms", "users_who_share_private_rooms", # no useful index, but let's clear them anyway From 97c60ccaa35059a55866304f6850b24e99912036 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 31 Oct 2019 11:30:25 +0000 Subject: [PATCH 0392/1623] Add unit test for /purge_room API --- tests/rest/admin/test_admin.py | 78 ++++++++++++++++++++++++++++++++++ tests/server.py | 6 ++- 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/tests/rest/admin/test_admin.py b/tests/rest/admin/test_admin.py index d3a4f717f7..8e1ca8b738 100644 --- a/tests/rest/admin/test_admin.py +++ b/tests/rest/admin/test_admin.py @@ -561,3 +561,81 @@ class DeleteGroupTestCase(unittest.HomeserverTestCase): self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) return channel.json_body["groups"] + + +class PurgeRoomTestCase(unittest.HomeserverTestCase): + """Test /purge_room admin API. + """ + + servlets = [ + synapse.rest.admin.register_servlets, + login.register_servlets, + room.register_servlets, + ] + + def prepare(self, reactor, clock, hs): + self.store = hs.get_datastore() + + self.admin_user = self.register_user("admin", "pass", admin=True) + self.admin_user_tok = self.login("admin", "pass") + + def test_purge_room(self): + room_id = self.helper.create_room_as(self.admin_user, tok=self.admin_user_tok) + + # All users have to have left the room. + self.helper.leave(room_id, user=self.admin_user, tok=self.admin_user_tok) + + url = "/_synapse/admin/v1/purge_room" + request, channel = self.make_request( + "POST", + url.encode("ascii"), + {"room_id": room_id}, + access_token=self.admin_user_tok, + ) + self.render(request) + + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + + # Test that the following tables have been purged of all rows related to the room. + for table in ( + "current_state_events", + "event_backward_extremities", + "event_forward_extremities", + "event_json", + "event_push_actions", + "event_search", + "events", + "group_rooms", + "public_room_list_stream", + "receipts_graph", + "receipts_linearized", + "room_aliases", + "room_depth", + "room_memberships", + "room_stats_state", + "room_stats_current", + "room_stats_historical", + "room_stats_earliest_token", + "rooms", + "stream_ordering_to_exterm", + "users_in_public_rooms", + "users_who_share_private_rooms", + "appservice_room_list", + "e2e_room_keys", + "event_push_summary", + "pusher_throttle", + "group_summary_rooms", + "local_invites", + "room_account_data", + "room_tags", + ): + count = self.get_success( + self.store._simple_select_one_onecol( + table="events", + keyvalues={"room_id": room_id}, + retcol="COUNT(*)", + desc="test_purge_room", + ) + ) + + self.assertEqual(count, 0, msg="Rows not purged in {}".format(table)) diff --git a/tests/server.py b/tests/server.py index e397ebe8fa..469efb4edb 100644 --- a/tests/server.py +++ b/tests/server.py @@ -161,7 +161,11 @@ def make_request( path = path.encode("ascii") # Decorate it to be the full path, if we're using shorthand - if shorthand and not path.startswith(b"/_matrix"): + if ( + shorthand + and not path.startswith(b"/_matrix") + and not path.startswith(b"/_synapse") + ): path = b"/_matrix/client/r0/" + path path = path.replace(b"//", b"/") From b2ff8c305f66b796ba06e9d481f156badf0f31d8 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 31 Oct 2019 11:32:53 +0000 Subject: [PATCH 0393/1623] Newsfile --- changelog.d/6307.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6307.bugfix diff --git a/changelog.d/6307.bugfix b/changelog.d/6307.bugfix new file mode 100644 index 0000000000..f2917c5053 --- /dev/null +++ b/changelog.d/6307.bugfix @@ -0,0 +1 @@ +Fix `/purge_room` admin API. From 64f2b8c3d8672273bf173cc125e339a297e5a29a Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 31 Oct 2019 15:44:31 +0100 Subject: [PATCH 0394/1623] Apply suggestions from code review Fix docstring Co-Authored-By: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> --- synapse/storage/state.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/synapse/storage/state.py b/synapse/storage/state.py index b382a06dcc..3735846899 100644 --- a/synapse/storage/state.py +++ b/synapse/storage/state.py @@ -338,7 +338,8 @@ class StateGroupStorage(object): the old and the new. Returns: - (prev_group, delta_ids), where both may be None. + Deferred[Tuple[Optional[int], Optional[list[dict[tuple[str, str], str]]]]]): + (prev_group, delta_ids) """ return self.stores.main.get_state_group_delta(state_group) From 3a74c03ffb5532831c8412b52a2d682bdeb9f322 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 31 Oct 2019 09:16:14 -0600 Subject: [PATCH 0395/1623] Expose some homeserver functionality to spam checkers (#6259) * Offer the homeserver instance to the spam checker * Newsfile * Linting * Expose a Spam Checker API instead of passing the homeserver object * Alter changelog * s/hs/api --- changelog.d/6259.misc | 1 + synapse/events/spamcheck.py | 14 +++++++- synapse/spam_checker_api/__init__.py | 51 ++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 changelog.d/6259.misc create mode 100644 synapse/spam_checker_api/__init__.py diff --git a/changelog.d/6259.misc b/changelog.d/6259.misc new file mode 100644 index 0000000000..3ff81b1ac7 --- /dev/null +++ b/changelog.d/6259.misc @@ -0,0 +1 @@ +Expose some homeserver functionality to spam checkers. diff --git a/synapse/events/spamcheck.py b/synapse/events/spamcheck.py index 129771f183..5a907718d6 100644 --- a/synapse/events/spamcheck.py +++ b/synapse/events/spamcheck.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # Copyright 2017 New Vector Ltd +# Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,6 +14,10 @@ # See the License for the specific language governing permissions and # limitations under the License. +import inspect + +from synapse.spam_checker_api import SpamCheckerApi + class SpamChecker(object): def __init__(self, hs): @@ -26,7 +31,14 @@ class SpamChecker(object): pass if module is not None: - self.spam_checker = module(config=config) + # Older spam checkers don't accept the `api` argument, so we + # try and detect support. + spam_args = inspect.getfullargspec(module) + if "api" in spam_args.args: + api = SpamCheckerApi(hs) + self.spam_checker = module(config=config, api=api) + else: + self.spam_checker = module(config=config) def check_event_for_spam(self, event): """Checks if a given event is considered "spammy" by this server. diff --git a/synapse/spam_checker_api/__init__.py b/synapse/spam_checker_api/__init__.py new file mode 100644 index 0000000000..efcc10f808 --- /dev/null +++ b/synapse/spam_checker_api/__init__.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging + +from twisted.internet import defer + +from synapse.storage.state import StateFilter + +logger = logging.getLogger(__name__) + + +class SpamCheckerApi(object): + """A proxy object that gets passed to spam checkers so they can get + access to rooms and other relevant information. + """ + + def __init__(self, hs): + self.hs = hs + + self._store = hs.get_datastore() + + @defer.inlineCallbacks + def get_state_events_in_room(self, room_id, types): + """Gets state events for the given room. + + Args: + room_id (string): The room ID to get state events in. + types (tuple): The event type and state key (using None + to represent 'any') of the room state to acquire. + + Returns: + twisted.internet.defer.Deferred[list(synapse.events.FrozenEvent)]: + The filtered state events in the room. + """ + state_ids = yield self._store.get_filtered_current_state_ids( + room_id=room_id, state_filter=StateFilter.from_types(types) + ) + state = yield self._store.get_events(state_ids.values()) + return state.values() From 8f5bbdb987c92ee3e9b4170cc8ce296d87b1555d Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 31 Oct 2019 15:22:08 +0000 Subject: [PATCH 0396/1623] Fix purge room API --- synapse/storage/data_stores/main/events.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/synapse/storage/data_stores/main/events.py b/synapse/storage/data_stores/main/events.py index 63b09a09e8..a904a71570 100644 --- a/synapse/storage/data_stores/main/events.py +++ b/synapse/storage/data_stores/main/events.py @@ -1829,8 +1829,10 @@ class EventsStore( txn.execute( """ DELETE FROM state_groups_state - INNER JOIN state_groups USING (event_id) - WHEREE state_groups.room_id = ? + WHERE state_group IN ( + SELECT state_group FROM state_groups + WHERE room_id = ? + ) """, (room_id,), ) @@ -1841,8 +1843,9 @@ class EventsStore( txn.execute( """ DELETE FROM state_group_edges - INNER JOIN state_groups USING (event_id) - WHEREE state_groups.room_id = ? + WHERE state_group IN ( + SELECT state_group FROM state_groups + WHERE room_id = ? ) """, (room_id,), @@ -1853,7 +1856,7 @@ class EventsStore( txn.execute( """ - DELETE FROM state_groups WHEREE room_id = ? + DELETE FROM state_groups WHERE room_id = ? """, (room_id,), ) From f91f2a1f92ee2eb5758184ef0df66490592587d1 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 31 Oct 2019 15:25:21 +0000 Subject: [PATCH 0397/1623] Docstrings --- synapse/storage/data_stores/main/events.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/synapse/storage/data_stores/main/events.py b/synapse/storage/data_stores/main/events.py index a904a71570..108b2e8bd5 100644 --- a/synapse/storage/data_stores/main/events.py +++ b/synapse/storage/data_stores/main/events.py @@ -19,6 +19,7 @@ import itertools import logging from collections import Counter as c_counter, OrderedDict, namedtuple from functools import wraps +from typing import Set from six import iteritems, text_type from six.moves import range @@ -1370,8 +1371,8 @@ class EventsStore( state groups). Returns: - Deferred[set[int]]: The set of state groups that reference deleted - events. + Deferred[set[int]]: The set of state groups that are referenced by + deleted events. """ return self.runInteraction( @@ -1711,9 +1712,16 @@ class EventsStore( logger.info("[purge] done") - def purge_unreferenced_state_groups(self, room_id, state_groups_to_delete): + def purge_unreferenced_state_groups( + self, room_id: str, state_groups_to_delete: Set[int] + ) -> defer.Deferred: """Deletes no longer referenced state groups and de-deltas any state groups that reference them. + + Args: + room_id: The room the state groups belong to (must all be in the + same room). + state_groups_to_delete: Set of all state groups to delete. """ return self.runInteraction( From 61be1a29262a9a3553537f257a4187ef355684f5 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 31 Oct 2019 15:39:26 +0000 Subject: [PATCH 0398/1623] Add state_groups.room_id index --- .../schema/delta/56/state_group_room_idx.sql | 17 +++++++++++++++++ synapse/storage/data_stores/main/state.py | 7 +++++++ 2 files changed, 24 insertions(+) create mode 100644 synapse/storage/data_stores/main/schema/delta/56/state_group_room_idx.sql diff --git a/synapse/storage/data_stores/main/schema/delta/56/state_group_room_idx.sql b/synapse/storage/data_stores/main/schema/delta/56/state_group_room_idx.sql new file mode 100644 index 0000000000..7916ef18b2 --- /dev/null +++ b/synapse/storage/data_stores/main/schema/delta/56/state_group_room_idx.sql @@ -0,0 +1,17 @@ +/* Copyright 2019 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +INSERT INTO background_updates (update_name, progress_json) VALUES + ('state_groups_room_id_idx', '{}'); diff --git a/synapse/storage/data_stores/main/state.py b/synapse/storage/data_stores/main/state.py index 36be8f0a9d..bf6de4ca22 100644 --- a/synapse/storage/data_stores/main/state.py +++ b/synapse/storage/data_stores/main/state.py @@ -1021,6 +1021,7 @@ class StateBackgroundUpdateStore( STATE_GROUP_INDEX_UPDATE_NAME = "state_group_state_type_index" CURRENT_STATE_INDEX_UPDATE_NAME = "current_state_members_idx" EVENT_STATE_GROUP_INDEX_UPDATE_NAME = "event_to_state_groups_sg_index" + STATE_GROUPS_ROOM_INDEX_UPDATE_NAME = "state_groups_room_id_idx" def __init__(self, db_conn, hs): super(StateBackgroundUpdateStore, self).__init__(db_conn, hs) @@ -1044,6 +1045,12 @@ class StateBackgroundUpdateStore( table="event_to_state_groups", columns=["state_group"], ) + self.register_background_index_update( + self.STATE_GROUPS_ROOM_INDEX_UPDATE_NAME, + index_name="state_groups_room_id_idx", + table="state_groups", + columns=["room_id"], + ) @defer.inlineCallbacks def _background_deduplicate_state(self, progress, batch_size): From 020add50997f697c7847ac84b86b457ba2f3e32d Mon Sep 17 00:00:00 2001 From: Amber Brown Date: Fri, 1 Nov 2019 02:43:24 +1100 Subject: [PATCH 0399/1623] Update black to 19.10b0 (#6304) * update version of black and also fix the mypy config being overridden --- changelog.d/6304.misc | 1 + contrib/experiments/test_messaging.py | 4 +- mypy.ini | 11 ++++-- .../sender/per_destination_queue.py | 11 +++--- synapse/handlers/account_data.py | 7 ++-- synapse/handlers/appservice.py | 5 ++- synapse/handlers/e2e_keys.py | 37 ++++++++++++------- synapse/handlers/federation.py | 9 +++-- synapse/handlers/initial_sync.py | 4 +- synapse/handlers/message.py | 14 ++++--- synapse/handlers/pagination.py | 13 +++---- synapse/handlers/register.py | 4 +- synapse/handlers/room.py | 29 ++++++++------- synapse/handlers/room_member.py | 35 ++++++++++-------- synapse/handlers/search.py | 12 ++---- synapse/handlers/stats.py | 5 ++- synapse/handlers/sync.py | 16 ++++---- synapse/logging/_structured.py | 2 +- synapse/push/bulk_push_rule_evaluator.py | 7 ++-- synapse/push/emailpusher.py | 14 +++---- synapse/push/httppusher.py | 14 +++---- synapse/push/pusherpool.py | 4 +- synapse/rest/client/v1/login.py | 13 ++++--- synapse/rest/client/v2_alpha/account.py | 4 +- synapse/rest/client/v2_alpha/register.py | 4 +- synapse/rest/key/v2/remote_key_resource.py | 2 +- synapse/server.pyi | 16 ++++---- synapse/storage/data_stores/main/__init__.py | 4 +- .../data_stores/main/event_push_actions.py | 2 +- synapse/storage/data_stores/main/events.py | 8 ++-- .../data_stores/main/events_bg_updates.py | 2 +- .../storage/data_stores/main/group_server.py | 4 +- .../data_stores/main/monthly_active_users.py | 2 +- synapse/storage/data_stores/main/push_rule.py | 2 +- .../storage/data_stores/main/registration.py | 2 +- .../storage/data_stores/main/roommember.py | 2 +- synapse/storage/data_stores/main/search.py | 2 +- synapse/storage/data_stores/main/state.py | 20 +++++----- synapse/storage/data_stores/main/stats.py | 4 +- synapse/storage/util/id_generators.py | 2 +- tox.ini | 4 +- 41 files changed, 191 insertions(+), 166 deletions(-) create mode 100644 changelog.d/6304.misc diff --git a/changelog.d/6304.misc b/changelog.d/6304.misc new file mode 100644 index 0000000000..20372b4f7c --- /dev/null +++ b/changelog.d/6304.misc @@ -0,0 +1 @@ +Update the version of black used to 19.10b0. diff --git a/contrib/experiments/test_messaging.py b/contrib/experiments/test_messaging.py index 6b22400a60..3bbbcfa1b4 100644 --- a/contrib/experiments/test_messaging.py +++ b/contrib/experiments/test_messaging.py @@ -78,7 +78,7 @@ class InputOutput(object): m = re.match("^join (\S+)$", line) if m: # The `sender` wants to join a room. - room_name, = m.groups() + (room_name,) = m.groups() self.print_line("%s joining %s" % (self.user, room_name)) self.server.join_room(room_name, self.user, self.user) # self.print_line("OK.") @@ -105,7 +105,7 @@ class InputOutput(object): m = re.match("^backfill (\S+)$", line) if m: # we want to backfill a room - room_name, = m.groups() + (room_name,) = m.groups() self.print_line("backfill %s" % room_name) self.server.backfill(room_name) return diff --git a/mypy.ini b/mypy.ini index ffadaddc0b..1d77c0ecc8 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,8 +1,11 @@ [mypy] -namespace_packages=True -plugins=mypy_zope:plugin -follow_imports=skip -mypy_path=stubs +namespace_packages = True +plugins = mypy_zope:plugin +follow_imports = normal +check_untyped_defs = True +show_error_codes = True +show_traceback = True +mypy_path = stubs [mypy-zope] ignore_missing_imports = True diff --git a/synapse/federation/sender/per_destination_queue.py b/synapse/federation/sender/per_destination_queue.py index cc75c39476..b754a09d7a 100644 --- a/synapse/federation/sender/per_destination_queue.py +++ b/synapse/federation/sender/per_destination_queue.py @@ -192,15 +192,16 @@ class PerDestinationQueue(object): # We have to keep 2 free slots for presence and rr_edus limit = MAX_EDUS_PER_TRANSACTION - 2 - device_update_edus, dev_list_id = ( - yield self._get_device_update_edus(limit) + device_update_edus, dev_list_id = yield self._get_device_update_edus( + limit ) limit -= len(device_update_edus) - to_device_edus, device_stream_id = ( - yield self._get_to_device_message_edus(limit) - ) + ( + to_device_edus, + device_stream_id, + ) = yield self._get_to_device_message_edus(limit) pending_edus = device_update_edus + to_device_edus diff --git a/synapse/handlers/account_data.py b/synapse/handlers/account_data.py index 38bc67191c..2d7e6df6e4 100644 --- a/synapse/handlers/account_data.py +++ b/synapse/handlers/account_data.py @@ -38,9 +38,10 @@ class AccountDataEventSource(object): {"type": "m.tag", "content": {"tags": room_tags}, "room_id": room_id} ) - account_data, room_account_data = ( - yield self.store.get_updated_account_data_for_user(user_id, last_stream_id) - ) + ( + account_data, + room_account_data, + ) = yield self.store.get_updated_account_data_for_user(user_id, last_stream_id) for account_data_type, content in account_data.items(): results.append({"type": account_data_type, "content": content}) diff --git a/synapse/handlers/appservice.py b/synapse/handlers/appservice.py index 3e9b298154..fe62f78e67 100644 --- a/synapse/handlers/appservice.py +++ b/synapse/handlers/appservice.py @@ -73,7 +73,10 @@ class ApplicationServicesHandler(object): try: limit = 100 while True: - upper_bound, events = yield self.store.get_new_events_for_appservice( + ( + upper_bound, + events, + ) = yield self.store.get_new_events_for_appservice( self.current_max, limit ) diff --git a/synapse/handlers/e2e_keys.py b/synapse/handlers/e2e_keys.py index 5ea54f60be..0449034a4e 100644 --- a/synapse/handlers/e2e_keys.py +++ b/synapse/handlers/e2e_keys.py @@ -119,9 +119,10 @@ class E2eKeysHandler(object): else: query_list.append((user_id, None)) - user_ids_not_in_cache, remote_results = ( - yield self.store.get_user_devices_from_cache(query_list) - ) + ( + user_ids_not_in_cache, + remote_results, + ) = yield self.store.get_user_devices_from_cache(query_list) for user_id, devices in iteritems(remote_results): user_devices = results.setdefault(user_id, {}) for device_id, device in iteritems(devices): @@ -688,17 +689,21 @@ class E2eKeysHandler(object): try: # get our self-signing key to verify the signatures - _, self_signing_key_id, self_signing_verify_key = yield self._get_e2e_cross_signing_verify_key( - user_id, "self_signing" - ) + ( + _, + self_signing_key_id, + self_signing_verify_key, + ) = yield self._get_e2e_cross_signing_verify_key(user_id, "self_signing") # get our master key, since we may have received a signature of it. # We need to fetch it here so that we know what its key ID is, so # that we can check if a signature that was sent is a signature of # the master key or of a device - master_key, _, master_verify_key = yield self._get_e2e_cross_signing_verify_key( - user_id, "master" - ) + ( + master_key, + _, + master_verify_key, + ) = yield self._get_e2e_cross_signing_verify_key(user_id, "master") # fetch our stored devices. This is used to 1. verify # signatures on the master key, and 2. to compare with what @@ -838,9 +843,11 @@ class E2eKeysHandler(object): try: # get our user-signing key to verify the signatures - user_signing_key, user_signing_key_id, user_signing_verify_key = yield self._get_e2e_cross_signing_verify_key( - user_id, "user_signing" - ) + ( + user_signing_key, + user_signing_key_id, + user_signing_verify_key, + ) = yield self._get_e2e_cross_signing_verify_key(user_id, "user_signing") except SynapseError as e: failure = _exception_to_failure(e) for user, devicemap in signatures.items(): @@ -859,7 +866,11 @@ class E2eKeysHandler(object): try: # get the target user's master key, to make sure it matches # what was sent - master_key, master_key_id, _ = yield self._get_e2e_cross_signing_verify_key( + ( + master_key, + master_key_id, + _, + ) = yield self._get_e2e_cross_signing_verify_key( target_user, "master", user_id ) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index d2d9f8c26a..a932d3085f 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -352,10 +352,11 @@ class FederationHandler(BaseHandler): # note that if any of the missing prevs share missing state or # auth events, the requests to fetch those events are deduped # by the get_pdu_cache in federation_client. - remote_state, got_auth_chain = ( - yield self.federation_client.get_state_for_room( - origin, room_id, p - ) + ( + remote_state, + got_auth_chain, + ) = yield self.federation_client.get_state_for_room( + origin, room_id, p ) # we want the state *after* p; get_state_for_room returns the diff --git a/synapse/handlers/initial_sync.py b/synapse/handlers/initial_sync.py index 49c9e031f9..81dce96f4b 100644 --- a/synapse/handlers/initial_sync.py +++ b/synapse/handlers/initial_sync.py @@ -128,8 +128,8 @@ class InitialSyncHandler(BaseHandler): tags_by_room = yield self.store.get_tags_for_user(user_id) - account_data, account_data_by_room = ( - yield self.store.get_account_data_for_user(user_id) + account_data, account_data_by_room = yield self.store.get_account_data_for_user( + user_id ) public_room_ids = yield self.store.get_public_room_ids() diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index 0d546d2487..d682dc2b7a 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -76,9 +76,10 @@ class MessageHandler(object): Raises: SynapseError if something went wrong. """ - membership, membership_event_id = yield self.auth.check_in_room_or_world_readable( - room_id, user_id - ) + ( + membership, + membership_event_id, + ) = yield self.auth.check_in_room_or_world_readable(room_id, user_id) if membership == Membership.JOIN: data = yield self.state.get_current_state(room_id, event_type, state_key) @@ -153,9 +154,10 @@ class MessageHandler(object): % (user_id, room_id, at_token), ) else: - membership, membership_event_id = ( - yield self.auth.check_in_room_or_world_readable(room_id, user_id) - ) + ( + membership, + membership_event_id, + ) = yield self.auth.check_in_room_or_world_readable(room_id, user_id) if membership == Membership.JOIN: state_ids = yield self.store.get_filtered_current_state_ids( diff --git a/synapse/handlers/pagination.py b/synapse/handlers/pagination.py index b7185fe7a0..97f15a1c32 100644 --- a/synapse/handlers/pagination.py +++ b/synapse/handlers/pagination.py @@ -212,9 +212,10 @@ class PaginationHandler(object): source_config = pagin_config.get_source_config("room") with (yield self.pagination_lock.read(room_id)): - membership, member_event_id = yield self.auth.check_in_room_or_world_readable( - room_id, user_id - ) + ( + membership, + member_event_id, + ) = yield self.auth.check_in_room_or_world_readable(room_id, user_id) if source_config.direction == "b": # if we're going backwards, we might need to backfill. This @@ -297,10 +298,8 @@ class PaginationHandler(object): } if state: - chunk["state"] = ( - yield self._event_serializer.serialize_events( - state, time_now, as_client_event=as_client_event - ) + chunk["state"] = yield self._event_serializer.serialize_events( + state, time_now, as_client_event=as_client_event ) return chunk diff --git a/synapse/handlers/register.py b/synapse/handlers/register.py index 53410f120b..cff6b0d375 100644 --- a/synapse/handlers/register.py +++ b/synapse/handlers/register.py @@ -396,8 +396,8 @@ class RegistrationHandler(BaseHandler): room_id = room_identifier elif RoomAlias.is_valid(room_identifier): room_alias = RoomAlias.from_string(room_identifier) - room_id, remote_room_hosts = ( - yield room_member_handler.lookup_room_alias(room_alias) + room_id, remote_room_hosts = yield room_member_handler.lookup_room_alias( + room_alias ) room_id = room_id.to_string() else: diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 650bd28abb..0182e5b432 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -147,21 +147,22 @@ class RoomCreationHandler(BaseHandler): # we create and auth the tombstone event before properly creating the new # room, to check our user has perms in the old room. - tombstone_event, tombstone_context = ( - yield self.event_creation_handler.create_event( - requester, - { - "type": EventTypes.Tombstone, - "state_key": "", - "room_id": old_room_id, - "sender": user_id, - "content": { - "body": "This room has been replaced", - "replacement_room": new_room_id, - }, + ( + tombstone_event, + tombstone_context, + ) = yield self.event_creation_handler.create_event( + requester, + { + "type": EventTypes.Tombstone, + "state_key": "", + "room_id": old_room_id, + "sender": user_id, + "content": { + "body": "This room has been replaced", + "replacement_room": new_room_id, }, - token_id=requester.access_token_id, - ) + }, + token_id=requester.access_token_id, ) old_room_version = yield self.store.get_room_version(old_room_id) yield self.auth.check_from_context( diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index 380e2fad5e..9a940d2c05 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -759,22 +759,25 @@ class RoomMemberHandler(object): if room_avatar_event: room_avatar_url = room_avatar_event.content.get("url", "") - token, public_keys, fallback_public_key, display_name = ( - yield self.identity_handler.ask_id_server_for_third_party_invite( - requester=requester, - id_server=id_server, - medium=medium, - address=address, - room_id=room_id, - inviter_user_id=user.to_string(), - room_alias=canonical_room_alias, - room_avatar_url=room_avatar_url, - room_join_rules=room_join_rules, - room_name=room_name, - inviter_display_name=inviter_display_name, - inviter_avatar_url=inviter_avatar_url, - id_access_token=id_access_token, - ) + ( + token, + public_keys, + fallback_public_key, + display_name, + ) = yield self.identity_handler.ask_id_server_for_third_party_invite( + requester=requester, + id_server=id_server, + medium=medium, + address=address, + room_id=room_id, + inviter_user_id=user.to_string(), + room_alias=canonical_room_alias, + room_avatar_url=room_avatar_url, + room_join_rules=room_join_rules, + room_name=room_name, + inviter_display_name=inviter_display_name, + inviter_avatar_url=inviter_avatar_url, + id_access_token=id_access_token, ) yield self.event_creation_handler.create_and_send_nonmember_event( diff --git a/synapse/handlers/search.py b/synapse/handlers/search.py index f4d8a60774..56ed262a1f 100644 --- a/synapse/handlers/search.py +++ b/synapse/handlers/search.py @@ -396,15 +396,11 @@ class SearchHandler(BaseHandler): time_now = self.clock.time_msec() for context in contexts.values(): - context["events_before"] = ( - yield self._event_serializer.serialize_events( - context["events_before"], time_now - ) + context["events_before"] = yield self._event_serializer.serialize_events( + context["events_before"], time_now ) - context["events_after"] = ( - yield self._event_serializer.serialize_events( - context["events_after"], time_now - ) + context["events_after"] = yield self._event_serializer.serialize_events( + context["events_after"], time_now ) state_results = {} diff --git a/synapse/handlers/stats.py b/synapse/handlers/stats.py index 26bc276692..7f7d56390e 100644 --- a/synapse/handlers/stats.py +++ b/synapse/handlers/stats.py @@ -108,7 +108,10 @@ class StatsHandler(StateDeltasHandler): user_deltas = {} # Then count deltas for total_events and total_event_bytes. - room_count, user_count = yield self.store.get_changes_room_total_events_and_bytes( + ( + room_count, + user_count, + ) = yield self.store.get_changes_room_total_events_and_bytes( self.pos, max_pos ) diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index 43a082dcda..b536d410e5 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -1206,10 +1206,11 @@ class SyncHandler(object): since_token = sync_result_builder.since_token if since_token and not sync_result_builder.full_state: - account_data, account_data_by_room = ( - yield self.store.get_updated_account_data_for_user( - user_id, since_token.account_data_key - ) + ( + account_data, + account_data_by_room, + ) = yield self.store.get_updated_account_data_for_user( + user_id, since_token.account_data_key ) push_rules_changed = yield self.store.have_push_rules_changed_for_user( @@ -1221,9 +1222,10 @@ class SyncHandler(object): sync_config.user ) else: - account_data, account_data_by_room = ( - yield self.store.get_account_data_for_user(sync_config.user.to_string()) - ) + ( + account_data, + account_data_by_room, + ) = yield self.store.get_account_data_for_user(sync_config.user.to_string()) account_data["m.push_rules"] = yield self.push_rules_for_user( sync_config.user diff --git a/synapse/logging/_structured.py b/synapse/logging/_structured.py index 3220e985a9..334ddaf39a 100644 --- a/synapse/logging/_structured.py +++ b/synapse/logging/_structured.py @@ -185,7 +185,7 @@ DEFAULT_LOGGERS = {"synapse": {"level": "INFO"}} def parse_drain_configs( - drains: dict + drains: dict, ) -> typing.Generator[DrainConfiguration, None, None]: """ Parse the drain configurations. diff --git a/synapse/push/bulk_push_rule_evaluator.py b/synapse/push/bulk_push_rule_evaluator.py index 2bbdd11941..1ba7bcd4d8 100644 --- a/synapse/push/bulk_push_rule_evaluator.py +++ b/synapse/push/bulk_push_rule_evaluator.py @@ -149,9 +149,10 @@ class BulkPushRuleEvaluator(object): room_members = yield self.store.get_joined_users_from_context(event, context) - (power_levels, sender_power_level) = ( - yield self._get_power_levels_and_sender_level(event, context) - ) + ( + power_levels, + sender_power_level, + ) = yield self._get_power_levels_and_sender_level(event, context) evaluator = PushRuleEvaluatorForEvent( event, len(room_members), sender_power_level, power_levels diff --git a/synapse/push/emailpusher.py b/synapse/push/emailpusher.py index 42e5b0c0a5..8c818a86bf 100644 --- a/synapse/push/emailpusher.py +++ b/synapse/push/emailpusher.py @@ -234,14 +234,12 @@ class EmailPusher(object): return self.last_stream_ordering = last_stream_ordering - pusher_still_exists = ( - yield self.store.update_pusher_last_stream_ordering_and_success( - self.app_id, - self.email, - self.user_id, - last_stream_ordering, - self.clock.time_msec(), - ) + pusher_still_exists = yield self.store.update_pusher_last_stream_ordering_and_success( + self.app_id, + self.email, + self.user_id, + last_stream_ordering, + self.clock.time_msec(), ) if not pusher_still_exists: # The pusher has been deleted while we were processing, so diff --git a/synapse/push/httppusher.py b/synapse/push/httppusher.py index 9a1bb64887..7dde2ad055 100644 --- a/synapse/push/httppusher.py +++ b/synapse/push/httppusher.py @@ -211,14 +211,12 @@ class HttpPusher(object): http_push_processed_counter.inc() self.backoff_delay = HttpPusher.INITIAL_BACKOFF_SEC self.last_stream_ordering = push_action["stream_ordering"] - pusher_still_exists = ( - yield self.store.update_pusher_last_stream_ordering_and_success( - self.app_id, - self.pushkey, - self.user_id, - self.last_stream_ordering, - self.clock.time_msec(), - ) + pusher_still_exists = yield self.store.update_pusher_last_stream_ordering_and_success( + self.app_id, + self.pushkey, + self.user_id, + self.last_stream_ordering, + self.clock.time_msec(), ) if not pusher_still_exists: # The pusher has been deleted while we were processing, so diff --git a/synapse/push/pusherpool.py b/synapse/push/pusherpool.py index 08e840fdc2..0f6992202d 100644 --- a/synapse/push/pusherpool.py +++ b/synapse/push/pusherpool.py @@ -103,9 +103,7 @@ class PusherPool: # create the pusher setting last_stream_ordering to the current maximum # stream ordering in event_push_actions, so it will process # pushes from this point onwards. - last_stream_ordering = ( - yield self.store.get_latest_push_action_stream_ordering() - ) + last_stream_ordering = yield self.store.get_latest_push_action_stream_ordering() yield self.store.add_pusher( user_id=user_id, diff --git a/synapse/rest/client/v1/login.py b/synapse/rest/client/v1/login.py index 39a5c5e9de..00a7dd6d09 100644 --- a/synapse/rest/client/v1/login.py +++ b/synapse/rest/client/v1/login.py @@ -203,10 +203,11 @@ class LoginRestServlet(RestServlet): address = address.lower() # Check for login providers that support 3pid login types - canonical_user_id, callback_3pid = ( - yield self.auth_handler.check_password_provider_3pid( - medium, address, login_submission["password"] - ) + ( + canonical_user_id, + callback_3pid, + ) = yield self.auth_handler.check_password_provider_3pid( + medium, address, login_submission["password"] ) if canonical_user_id: # Authentication through password provider and 3pid succeeded @@ -280,8 +281,8 @@ class LoginRestServlet(RestServlet): def do_token_login(self, login_submission): token = login_submission["token"] auth_handler = self.auth_handler - user_id = ( - yield auth_handler.validate_short_term_login_token_and_get_user_id(token) + user_id = yield auth_handler.validate_short_term_login_token_and_get_user_id( + token ) result = yield self._register_device_with_callback(user_id, login_submission) diff --git a/synapse/rest/client/v2_alpha/account.py b/synapse/rest/client/v2_alpha/account.py index 332d7138b1..f26eae794c 100644 --- a/synapse/rest/client/v2_alpha/account.py +++ b/synapse/rest/client/v2_alpha/account.py @@ -148,7 +148,7 @@ class PasswordResetSubmitTokenServlet(RestServlet): self.clock = hs.get_clock() self.store = hs.get_datastore() if self.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL: - self.failure_email_template, = load_jinja2_templates( + (self.failure_email_template,) = load_jinja2_templates( self.config.email_template_dir, [self.config.email_password_reset_template_failure_html], ) @@ -479,7 +479,7 @@ class AddThreepidEmailSubmitTokenServlet(RestServlet): self.clock = hs.get_clock() self.store = hs.get_datastore() if self.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL: - self.failure_email_template, = load_jinja2_templates( + (self.failure_email_template,) = load_jinja2_templates( self.config.email_template_dir, [self.config.email_add_threepid_template_failure_html], ) diff --git a/synapse/rest/client/v2_alpha/register.py b/synapse/rest/client/v2_alpha/register.py index 6c7d25d411..91db923814 100644 --- a/synapse/rest/client/v2_alpha/register.py +++ b/synapse/rest/client/v2_alpha/register.py @@ -247,13 +247,13 @@ class RegistrationSubmitTokenServlet(RestServlet): self.store = hs.get_datastore() if self.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL: - self.failure_email_template, = load_jinja2_templates( + (self.failure_email_template,) = load_jinja2_templates( self.config.email_template_dir, [self.config.email_registration_template_failure_html], ) if self.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL: - self.failure_email_template, = load_jinja2_templates( + (self.failure_email_template,) = load_jinja2_templates( self.config.email_template_dir, [self.config.email_registration_template_failure_html], ) diff --git a/synapse/rest/key/v2/remote_key_resource.py b/synapse/rest/key/v2/remote_key_resource.py index 55580bc59e..e7fc3f0431 100644 --- a/synapse/rest/key/v2/remote_key_resource.py +++ b/synapse/rest/key/v2/remote_key_resource.py @@ -102,7 +102,7 @@ class RemoteKey(DirectServeResource): @wrap_json_request_handler async def _async_render_GET(self, request): if len(request.postpath) == 1: - server, = request.postpath + (server,) = request.postpath query = {server.decode("ascii"): {}} elif len(request.postpath) == 2: server, key_id = request.postpath diff --git a/synapse/server.pyi b/synapse/server.pyi index 16f8f6b573..83d1f11283 100644 --- a/synapse/server.pyi +++ b/synapse/server.pyi @@ -39,7 +39,7 @@ class HomeServer(object): def get_state_resolution_handler(self) -> synapse.state.StateResolutionHandler: pass def get_deactivate_account_handler( - self + self, ) -> synapse.handlers.deactivate_account.DeactivateAccountHandler: pass def get_room_creation_handler(self) -> synapse.handlers.room.RoomCreationHandler: @@ -47,32 +47,32 @@ class HomeServer(object): def get_room_member_handler(self) -> synapse.handlers.room_member.RoomMemberHandler: pass def get_event_creation_handler( - self + self, ) -> synapse.handlers.message.EventCreationHandler: pass def get_set_password_handler( - self + self, ) -> synapse.handlers.set_password.SetPasswordHandler: pass def get_federation_sender(self) -> synapse.federation.sender.FederationSender: pass def get_federation_transport_client( - self + self, ) -> synapse.federation.transport.client.TransportLayerClient: pass def get_media_repository_resource( - self + self, ) -> synapse.rest.media.v1.media_repository.MediaRepositoryResource: pass def get_media_repository( - self + self, ) -> synapse.rest.media.v1.media_repository.MediaRepository: pass def get_server_notices_manager( - self + self, ) -> synapse.server_notices.server_notices_manager.ServerNoticesManager: pass def get_server_notices_sender( - self + self, ) -> synapse.server_notices.server_notices_sender.ServerNoticesSender: pass diff --git a/synapse/storage/data_stores/main/__init__.py b/synapse/storage/data_stores/main/__init__.py index b185ba0b3e..60ae01d972 100644 --- a/synapse/storage/data_stores/main/__init__.py +++ b/synapse/storage/data_stores/main/__init__.py @@ -317,7 +317,7 @@ class DataStore( ) u """ txn.execute(sql, (time_from,)) - count, = txn.fetchone() + (count,) = txn.fetchone() return count def count_r30_users(self): @@ -396,7 +396,7 @@ class DataStore( txn.execute(sql, (thirty_days_ago_in_secs, thirty_days_ago_in_secs)) - count, = txn.fetchone() + (count,) = txn.fetchone() results["all"] = count return results diff --git a/synapse/storage/data_stores/main/event_push_actions.py b/synapse/storage/data_stores/main/event_push_actions.py index 22025effbc..04ce21ac66 100644 --- a/synapse/storage/data_stores/main/event_push_actions.py +++ b/synapse/storage/data_stores/main/event_push_actions.py @@ -863,7 +863,7 @@ class EventPushActionsStore(EventPushActionsWorkerStore): ) stream_row = txn.fetchone() if stream_row: - offset_stream_ordering, = stream_row + (offset_stream_ordering,) = stream_row rotate_to_stream_ordering = min( self.stream_ordering_day_ago, offset_stream_ordering ) diff --git a/synapse/storage/data_stores/main/events.py b/synapse/storage/data_stores/main/events.py index 64a8a05279..aafc2007d3 100644 --- a/synapse/storage/data_stores/main/events.py +++ b/synapse/storage/data_stores/main/events.py @@ -1125,7 +1125,7 @@ class EventsStore( AND stream_ordering > ? """ txn.execute(sql, (self.stream_ordering_day_ago,)) - count, = txn.fetchone() + (count,) = txn.fetchone() return count ret = yield self.runInteraction("count_messages", _count_messages) @@ -1146,7 +1146,7 @@ class EventsStore( """ txn.execute(sql, (like_clause, self.stream_ordering_day_ago)) - count, = txn.fetchone() + (count,) = txn.fetchone() return count ret = yield self.runInteraction("count_daily_sent_messages", _count_messages) @@ -1161,7 +1161,7 @@ class EventsStore( AND stream_ordering > ? """ txn.execute(sql, (self.stream_ordering_day_ago,)) - count, = txn.fetchone() + (count,) = txn.fetchone() return count ret = yield self.runInteraction("count_daily_active_rooms", _count) @@ -1646,7 +1646,7 @@ class EventsStore( """, (room_id,), ) - min_depth, = txn.fetchone() + (min_depth,) = txn.fetchone() logger.info("[purge] updating room_depth to %d", min_depth) diff --git a/synapse/storage/data_stores/main/events_bg_updates.py b/synapse/storage/data_stores/main/events_bg_updates.py index 31ea6f917f..51352b9966 100644 --- a/synapse/storage/data_stores/main/events_bg_updates.py +++ b/synapse/storage/data_stores/main/events_bg_updates.py @@ -438,7 +438,7 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): if not rows: return 0 - upper_event_id, = rows[-1] + (upper_event_id,) = rows[-1] # Update the redactions with the received_ts. # diff --git a/synapse/storage/data_stores/main/group_server.py b/synapse/storage/data_stores/main/group_server.py index aeae5a2b28..b3a2771f1b 100644 --- a/synapse/storage/data_stores/main/group_server.py +++ b/synapse/storage/data_stores/main/group_server.py @@ -249,7 +249,7 @@ class GroupServerStore(SQLBaseStore): WHERE group_id = ? AND category_id = ? """ txn.execute(sql, (group_id, category_id)) - order, = txn.fetchone() + (order,) = txn.fetchone() if existing: to_update = {} @@ -509,7 +509,7 @@ class GroupServerStore(SQLBaseStore): WHERE group_id = ? AND role_id = ? """ txn.execute(sql, (group_id, role_id)) - order, = txn.fetchone() + (order,) = txn.fetchone() if existing: to_update = {} diff --git a/synapse/storage/data_stores/main/monthly_active_users.py b/synapse/storage/data_stores/main/monthly_active_users.py index e6ee1e4aaa..b41c3d317a 100644 --- a/synapse/storage/data_stores/main/monthly_active_users.py +++ b/synapse/storage/data_stores/main/monthly_active_users.py @@ -171,7 +171,7 @@ class MonthlyActiveUsersStore(SQLBaseStore): sql = "SELECT COALESCE(count(*), 0) FROM monthly_active_users" txn.execute(sql) - count, = txn.fetchone() + (count,) = txn.fetchone() return count return self.runInteraction("count_users", _count_users) diff --git a/synapse/storage/data_stores/main/push_rule.py b/synapse/storage/data_stores/main/push_rule.py index cd95f1ce60..b520062d84 100644 --- a/synapse/storage/data_stores/main/push_rule.py +++ b/synapse/storage/data_stores/main/push_rule.py @@ -143,7 +143,7 @@ class PushRulesWorkerStore( " WHERE user_id = ? AND ? < stream_id" ) txn.execute(sql, (user_id, last_id)) - count, = txn.fetchone() + (count,) = txn.fetchone() return bool(count) return self.runInteraction( diff --git a/synapse/storage/data_stores/main/registration.py b/synapse/storage/data_stores/main/registration.py index 6c5b29288a..f70d41ecab 100644 --- a/synapse/storage/data_stores/main/registration.py +++ b/synapse/storage/data_stores/main/registration.py @@ -459,7 +459,7 @@ class RegistrationWorkerStore(SQLBaseStore): WHERE appservice_id IS NULL """ ) - count, = txn.fetchone() + (count,) = txn.fetchone() return count ret = yield self.runInteraction("count_users", _count_users) diff --git a/synapse/storage/data_stores/main/roommember.py b/synapse/storage/data_stores/main/roommember.py index bc04bfd7d4..2af24a20b7 100644 --- a/synapse/storage/data_stores/main/roommember.py +++ b/synapse/storage/data_stores/main/roommember.py @@ -927,7 +927,7 @@ class RoomMemberBackgroundUpdateStore(BackgroundUpdateStore): if not row or not row[0]: return processed, True - next_room, = row + (next_room,) = row sql = """ UPDATE current_state_events diff --git a/synapse/storage/data_stores/main/search.py b/synapse/storage/data_stores/main/search.py index a59b8331e1..d1d7c6863d 100644 --- a/synapse/storage/data_stores/main/search.py +++ b/synapse/storage/data_stores/main/search.py @@ -672,7 +672,7 @@ class SearchStore(SearchBackgroundUpdateStore): ) ) txn.execute(query, (value, search_query)) - headline, = txn.fetchall()[0] + (headline,) = txn.fetchall()[0] # Now we need to pick the possible highlights out of the haedline # result. diff --git a/synapse/storage/data_stores/main/state.py b/synapse/storage/data_stores/main/state.py index 9b2207075b..3132848034 100644 --- a/synapse/storage/data_stores/main/state.py +++ b/synapse/storage/data_stores/main/state.py @@ -725,16 +725,18 @@ class StateGroupWorkerStore( member_filter, non_member_filter = state_filter.get_member_split() # Now we look them up in the member and non-member caches - non_member_state, incomplete_groups_nm, = ( - yield self._get_state_for_groups_using_cache( - groups, self._state_group_cache, state_filter=non_member_filter - ) + ( + non_member_state, + incomplete_groups_nm, + ) = yield self._get_state_for_groups_using_cache( + groups, self._state_group_cache, state_filter=non_member_filter ) - member_state, incomplete_groups_m, = ( - yield self._get_state_for_groups_using_cache( - groups, self._state_group_members_cache, state_filter=member_filter - ) + ( + member_state, + incomplete_groups_m, + ) = yield self._get_state_for_groups_using_cache( + groups, self._state_group_members_cache, state_filter=member_filter ) state = dict(non_member_state) @@ -1076,7 +1078,7 @@ class StateBackgroundUpdateStore( " WHERE id < ? AND room_id = ?", (state_group, room_id), ) - prev_group, = txn.fetchone() + (prev_group,) = txn.fetchone() new_last_state_group = state_group if prev_group: diff --git a/synapse/storage/data_stores/main/stats.py b/synapse/storage/data_stores/main/stats.py index 4d59b7833f..45b3de7d56 100644 --- a/synapse/storage/data_stores/main/stats.py +++ b/synapse/storage/data_stores/main/stats.py @@ -773,7 +773,7 @@ class StatsStore(StateDeltasStore): (room_id,), ) - current_state_events_count, = txn.fetchone() + (current_state_events_count,) = txn.fetchone() users_in_room = self.get_users_in_room_txn(txn, room_id) @@ -863,7 +863,7 @@ class StatsStore(StateDeltasStore): """, (user_id,), ) - count, = txn.fetchone() + (count,) = txn.fetchone() return count, pos joined_rooms, pos = yield self.runInteraction( diff --git a/synapse/storage/util/id_generators.py b/synapse/storage/util/id_generators.py index cbb0a4810a..9d851beaa5 100644 --- a/synapse/storage/util/id_generators.py +++ b/synapse/storage/util/id_generators.py @@ -46,7 +46,7 @@ def _load_current_id(db_conn, table, column, step=1): cur.execute("SELECT MAX(%s) FROM %s" % (column, table)) else: cur.execute("SELECT MIN(%s) FROM %s" % (column, table)) - val, = cur.fetchone() + (val,) = cur.fetchone() cur.close() current_id = int(val) if val else step return (max if step > 0 else min)(current_id, step) diff --git a/tox.ini b/tox.ini index 50b6afe611..afe9bc909b 100644 --- a/tox.ini +++ b/tox.ini @@ -114,7 +114,7 @@ skip_install = True basepython = python3.6 deps = flake8 - black==19.3b0 # We pin so that our tests don't start failing on new releases of black. + black==19.10b0 # We pin so that our tests don't start failing on new releases of black. commands = python -m black --check --diff . /bin/sh -c "flake8 synapse tests scripts scripts-dev synctl {env:PEP8SUFFIX:}" @@ -167,6 +167,6 @@ deps = env = MYPYPATH = stubs/ extras = all -commands = mypy --show-traceback --check-untyped-defs --show-error-codes --follow-imports=normal \ +commands = mypy \ synapse/logging/ \ synapse/config/ From fb1a6914cf953ed237235274a9aab62aafec5b4f Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 31 Oct 2019 15:45:48 +0000 Subject: [PATCH 0400/1623] Update log line to lie a little less --- synapse/storage/data_stores/main/events.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/storage/data_stores/main/events.py b/synapse/storage/data_stores/main/events.py index 108b2e8bd5..3a9b6bed45 100644 --- a/synapse/storage/data_stores/main/events.py +++ b/synapse/storage/data_stores/main/events.py @@ -1509,7 +1509,7 @@ class EventsStore( [(room_id, event_id) for event_id, in new_backwards_extrems], ) - logger.info("[purge] finding redundant state groups") + logger.info("[purge] finding state groups referenced by deleted events") # Get all state groups that are referenced by events that are to be # deleted. We then go and check if they are referenced by other events From f7e4a582ef1f1fc14edc86fa677a7d880a5ef01b Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 31 Oct 2019 12:01:00 -0400 Subject: [PATCH 0401/1623] clean up code a bit --- synapse/replication/slave/storage/devices.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/synapse/replication/slave/storage/devices.py b/synapse/replication/slave/storage/devices.py index f416d73b2e..de50748c30 100644 --- a/synapse/replication/slave/storage/devices.py +++ b/synapse/replication/slave/storage/devices.py @@ -15,6 +15,7 @@ from synapse.replication.slave.storage._base import BaseSlavedStore from synapse.replication.slave.storage._slaved_id_tracker import SlavedIdTracker +from synapse.replication.tcp.streams._base import DeviceListsStream, UserSignatureStream from synapse.storage.data_stores.main.devices import DeviceWorkerStore from synapse.storage.data_stores.main.end_to_end_keys import EndToEndKeyWorkerStore from synapse.util.caches.stream_change_cache import StreamChangeCache @@ -42,17 +43,20 @@ class SlavedDeviceStore(EndToEndKeyWorkerStore, DeviceWorkerStore, BaseSlavedSto def stream_positions(self): result = super(SlavedDeviceStore, self).stream_positions() - result["user_signature"] = result[ - "device_lists" - ] = self._device_list_id_gen.get_current_token() + # The user signature stream uses the same stream ID generator as the + # device list stream, so set them both to the device list ID + # generator's current token. + current_token = self._device_list_id_gen.get_current_token() + result[DeviceListsStream.NAME] = current_token + result[UserSignatureStream.NAME] = current_token return result def process_replication_rows(self, stream_name, token, rows): - if stream_name == "device_lists": + if stream_name == DeviceListsStream.NAME: self._device_list_id_gen.advance(token) for row in rows: self._invalidate_caches_for_devices(token, row.user_id, row.destination) - elif stream_name == "user_signature": + elif stream_name == UserSignatureStream.NAME: for row in rows: self._user_signature_stream_cache.entity_has_changed(row.user_id, token) return super(SlavedDeviceStore, self).process_replication_rows( From 42e707c663505cecb63da579d5fa2c4d30811db7 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Thu, 31 Oct 2019 17:32:25 +0000 Subject: [PATCH 0402/1623] rstrip slashes from url on appservice (#6306) --- changelog.d/6306.bugfix | 1 + synapse/appservice/__init__.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 changelog.d/6306.bugfix diff --git a/changelog.d/6306.bugfix b/changelog.d/6306.bugfix new file mode 100644 index 0000000000..c7dcbcdce8 --- /dev/null +++ b/changelog.d/6306.bugfix @@ -0,0 +1 @@ +Appservice requests will no longer contain a double slash prefix when the appservice url provided ends in a slash. diff --git a/synapse/appservice/__init__.py b/synapse/appservice/__init__.py index 33b3579425..aea3985a5f 100644 --- a/synapse/appservice/__init__.py +++ b/synapse/appservice/__init__.py @@ -94,7 +94,9 @@ class ApplicationService(object): ip_range_whitelist=None, ): self.token = token - self.url = url + self.url = ( + url.rstrip("/") if isinstance(url, str) else None + ) # url must not end with a slash self.hs_token = hs_token self.sender = sender self.server_name = hostname From c3fc176c6047c8194262a64599d000e9cb43f7f8 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 31 Oct 2019 22:49:48 -0400 Subject: [PATCH 0403/1623] Update synapse/storage/data_stores/main/devices.py Co-Authored-By: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> --- synapse/storage/data_stores/main/devices.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/storage/data_stores/main/devices.py b/synapse/storage/data_stores/main/devices.py index 717eab4159..71f62036c0 100644 --- a/synapse/storage/data_stores/main/devices.py +++ b/synapse/storage/data_stores/main/devices.py @@ -203,7 +203,7 @@ class DeviceWorkerStore(SQLBaseStore): result = cross_signing_keys_by_user.setdefault(user_id, {}) result["master_key"] = master_key_by_user[user_id]["key_info"] elif ( - user_id in master_key_by_user + user_id in self_signing_key_by_user and device_id == self_signing_key_by_user[user_id]["device_id"] ): result = cross_signing_keys_by_user.setdefault(user_id, {}) From c61db131837eb5bb50355b6c34542cfd73654b41 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 31 Oct 2019 11:28:18 -0400 Subject: [PATCH 0404/1623] fix hidden field in devices table for older sqlite --- .../delta/56/hidden_devices_fix.sql.sqlite | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 synapse/storage/data_stores/main/schema/delta/56/hidden_devices_fix.sql.sqlite diff --git a/synapse/storage/data_stores/main/schema/delta/56/hidden_devices_fix.sql.sqlite b/synapse/storage/data_stores/main/schema/delta/56/hidden_devices_fix.sql.sqlite new file mode 100644 index 0000000000..e8b1fd35d8 --- /dev/null +++ b/synapse/storage/data_stores/main/schema/delta/56/hidden_devices_fix.sql.sqlite @@ -0,0 +1,42 @@ +/* Copyright 2019 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* Change the hidden column from a default value of FALSE to a default value of + * 0, because sqlite3 prior to 3.23.0 caused the hidden column to contain the + * string 'FALSE', which is truthy. + * + * Since sqlite doesn't allow us to just change the default value, we have to + * recreate the table, copy the data, fix the rows that have incorrect data, and + * replace the old table with the new table. + */ + +CREATE TABLE IF NOT EXISTS devices2 ( + user_id TEXT NOT NULL, + device_id TEXT NOT NULL, + display_name TEXT, + last_seen BIGINT, + ip TEXT, + user_agent TEXT, + hidden BOOLEAN DEFAULT 0, + CONSTRAINT device_uniqueness UNIQUE (user_id, device_id) +); + +INSERT INTO devices2 SELECT * FROM devices; + +UPDATE devices2 SET hidden = 0 WHERE hidden = 'FALSE'; + +DROP TABLE devices; + +ALTER TABLE devices2 RENAME TO devices; From 1f156398b9c0c46db7907d94089000c36ce8c072 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 31 Oct 2019 23:02:20 -0400 Subject: [PATCH 0405/1623] add changelog --- changelog.d/6313.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6313.bugfix diff --git a/changelog.d/6313.bugfix b/changelog.d/6313.bugfix new file mode 100644 index 0000000000..f4d4a97f00 --- /dev/null +++ b/changelog.d/6313.bugfix @@ -0,0 +1 @@ +Fix the `hidden` field in the `devices` table for SQLite versions prior to 3.23.0. From ace947e8da30c37ead3357abe34adee8a1528296 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Fri, 1 Nov 2019 10:28:09 +0000 Subject: [PATCH 0406/1623] Depublish a room from the public rooms list when it is upgraded (#6232) --- changelog.d/6232.bugfix | 1 + synapse/federation/federation_client.py | 2 +- synapse/handlers/federation.py | 30 ++++++++- synapse/handlers/room.py | 8 ++- synapse/handlers/room_member.py | 83 ++++++++++++++++--------- 5 files changed, 93 insertions(+), 31 deletions(-) create mode 100644 changelog.d/6232.bugfix diff --git a/changelog.d/6232.bugfix b/changelog.d/6232.bugfix new file mode 100644 index 0000000000..12718ba934 --- /dev/null +++ b/changelog.d/6232.bugfix @@ -0,0 +1 @@ +Remove a room from a server's public rooms list on room upgrade. \ No newline at end of file diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index 595706d01a..545d719652 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -555,7 +555,7 @@ class FederationClient(FederationBase): Note that this does not append any events to any graphs. Args: - destinations (str): Candidate homeservers which are probably + destinations (Iterable[str]): Candidate homeservers which are probably participating in the room. room_id (str): The room in which the event will happen. user_id (str): The user whose membership is being evented. diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index a932d3085f..dab6be9573 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1106,7 +1106,7 @@ class FederationHandler(BaseHandler): @defer.inlineCallbacks def do_invite_join(self, target_hosts, room_id, joinee, content): """ Attempts to join the `joinee` to the room `room_id` via the - server `target_host`. + servers contained in `target_hosts`. This first triggers a /make_join/ request that returns a partial event that we can fill out and sign. This is then sent to the @@ -1115,6 +1115,15 @@ class FederationHandler(BaseHandler): We suspend processing of any received events from this room until we have finished processing the join. + + Args: + target_hosts (Iterable[str]): List of servers to attempt to join the room with. + + room_id (str): The ID of the room to join. + + joinee (str): The User ID of the joining user. + + content (dict): The event content to use for the join event. """ logger.debug("Joining %s to %s", joinee, room_id) @@ -1174,6 +1183,22 @@ class FederationHandler(BaseHandler): yield self._persist_auth_tree(origin, auth_chain, state, event) + # Check whether this room is the result of an upgrade of a room we already know + # about. If so, migrate over user information + predecessor = yield self.store.get_room_predecessor(room_id) + if not predecessor: + return + old_room_id = predecessor["room_id"] + logger.debug( + "Found predecessor for %s during remote join: %s", room_id, old_room_id + ) + + # We retrieve the room member handler here as to not cause a cyclic dependency + member_handler = self.hs.get_room_member_handler() + yield member_handler.transfer_room_state_on_room_upgrade( + old_room_id, room_id + ) + logger.debug("Finished joining %s to %s", joinee, room_id) finally: room_queue = self.room_queues[room_id] @@ -2442,6 +2467,8 @@ class FederationHandler(BaseHandler): raise e yield self._check_signature(event, context) + + # We retrieve the room member handler here as to not cause a cyclic dependency member_handler = self.hs.get_room_member_handler() yield member_handler.send_membership_event(None, event, context) else: @@ -2502,6 +2529,7 @@ class FederationHandler(BaseHandler): # though the sender isn't a local user. event.internal_metadata.send_on_behalf_of = get_domain_from_id(event.sender) + # We retrieve the room member handler here as to not cause a cyclic dependency member_handler = self.hs.get_room_member_handler() yield member_handler.send_membership_event(None, event, context) diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 0182e5b432..e92b2eafd5 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -129,6 +129,7 @@ class RoomCreationHandler(BaseHandler): old_room_id, new_version, # args for _upgrade_room ) + return ret @defer.inlineCallbacks @@ -189,7 +190,12 @@ class RoomCreationHandler(BaseHandler): requester, old_room_id, new_room_id, old_room_state ) - # and finally, shut down the PLs in the old room, and update them in the new + # Copy over user push rules, tags and migrate room directory state + yield self.room_member_handler.transfer_room_state_on_room_upgrade( + old_room_id, new_room_id + ) + + # finally, shut down the PLs in the old room, and update them in the new # room. yield self._update_upgraded_room_pls( requester, old_room_id, new_room_id, old_room_state diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index 9a940d2c05..06d09c2947 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -203,10 +203,6 @@ class RoomMemberHandler(object): prev_member_event = yield self.store.get_event(prev_member_event_id) newly_joined = prev_member_event.membership != Membership.JOIN if newly_joined: - # Copy over user state if we're joining an upgraded room - yield self.copy_user_state_if_room_upgrade( - room_id, requester.user.to_string() - ) yield self._user_joined_room(target, room_id) elif event.membership == Membership.LEAVE: if prev_member_event_id: @@ -455,11 +451,6 @@ class RoomMemberHandler(object): requester, remote_room_hosts, room_id, target, content ) - # Copy over user state if this is a join on an remote upgraded room - yield self.copy_user_state_if_room_upgrade( - room_id, requester.user.to_string() - ) - return remote_join_response elif effective_membership_state == Membership.LEAVE: @@ -498,36 +489,72 @@ class RoomMemberHandler(object): return res @defer.inlineCallbacks - def copy_user_state_if_room_upgrade(self, new_room_id, user_id): - """Copy user-specific information when they join a new room if that new room is the - result of a room upgrade + def transfer_room_state_on_room_upgrade(self, old_room_id, room_id): + """Upon our server becoming aware of an upgraded room, either by upgrading a room + ourselves or joining one, we can transfer over information from the previous room. + + Copies user state (tags/push rules) for every local user that was in the old room, as + well as migrating the room directory state. Args: - new_room_id (str): The ID of the room the user is joining - user_id (str): The ID of the user + old_room_id (str): The ID of the old room + + room_id (str): The ID of the new room + + Returns: + Deferred + """ + # Find all local users that were in the old room and copy over each user's state + users = yield self.store.get_users_in_room(old_room_id) + yield self.copy_user_state_on_room_upgrade(old_room_id, room_id, users) + + # Add new room to the room directory if the old room was there + # Remove old room from the room directory + old_room = yield self.store.get_room(old_room_id) + if old_room and old_room["is_public"]: + yield self.store.set_room_is_public(old_room_id, False) + yield self.store.set_room_is_public(room_id, True) + + @defer.inlineCallbacks + def copy_user_state_on_room_upgrade(self, old_room_id, new_room_id, user_ids): + """Copy user-specific information when they join a new room when that new room is the + result of a room upgrade + + Args: + old_room_id (str): The ID of upgraded room + new_room_id (str): The ID of the new room + user_ids (Iterable[str]): User IDs to copy state for Returns: Deferred """ - # Check if the new room is an upgraded room - predecessor = yield self.store.get_room_predecessor(new_room_id) - if not predecessor: - return logger.debug( - "Found predecessor for %s: %s. Copying over room tags and push " "rules", + "Copying over room tags and push rules from %s to %s for users %s", + old_room_id, new_room_id, - predecessor, + user_ids, ) - # It is an upgraded room. Copy over old tags - yield self.copy_room_tags_and_direct_to_room( - predecessor["room_id"], new_room_id, user_id - ) - # Copy over push rules - yield self.store.copy_push_rules_from_room_to_room_for_user( - predecessor["room_id"], new_room_id, user_id - ) + for user_id in user_ids: + try: + # It is an upgraded room. Copy over old tags + yield self.copy_room_tags_and_direct_to_room( + old_room_id, new_room_id, user_id + ) + # Copy over push rules + yield self.store.copy_push_rules_from_room_to_room_for_user( + old_room_id, new_room_id, user_id + ) + except Exception: + logger.exception( + "Error copying tags and/or push rules from rooms %s to %s for user %s. " + "Skipping...", + old_room_id, + new_room_id, + user_id, + ) + continue @defer.inlineCallbacks def send_membership_event(self, requester, event, context, ratelimit=True): From c6dbca2422bf77ccbf0b52d9245d28c258dac4f3 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 1 Nov 2019 10:30:51 +0000 Subject: [PATCH 0407/1623] Incorporate review --- changelog.d/6301.feature | 2 +- synapse/api/constants.py | 5 ++++- synapse/api/filtering.py | 6 ++++-- synapse/storage/data_stores/main/events.py | 12 ++++++++++-- tests/api/test_filtering.py | 10 +++++----- tests/rest/client/v1/test_rooms.py | 10 +++++----- tests/rest/client/v2_alpha/test_sync.py | 10 +++++----- 7 files changed, 34 insertions(+), 21 deletions(-) diff --git a/changelog.d/6301.feature b/changelog.d/6301.feature index b7ff3fad3b..78a187a1dc 100644 --- a/changelog.d/6301.feature +++ b/changelog.d/6301.feature @@ -1 +1 @@ -Implement label-based filtering. +Implement label-based filtering on `/sync` and `/messages` ([MSC2326](https://github.com/matrix-org/matrix-doc/pull/2326)). diff --git a/synapse/api/constants.py b/synapse/api/constants.py index 999ec02fd9..cf4ce5f5a2 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -140,4 +140,7 @@ class LimitBlockingTypes(object): HS_DISABLED = "hs_disabled" -LabelsField = "org.matrix.labels" +class EventContentFields(object): + """Fields found in events' content, regardless of type.""" + # Labels for the event, cf https://github.com/matrix-org/matrix-doc/pull/2326 + Labels = "org.matrix.labels" diff --git a/synapse/api/filtering.py b/synapse/api/filtering.py index bd91b9f018..30a7ee0a7a 100644 --- a/synapse/api/filtering.py +++ b/synapse/api/filtering.py @@ -20,7 +20,7 @@ from jsonschema import FormatChecker from twisted.internet import defer -from synapse.api.constants import LabelsField +from synapse.api.constants import EventContentFields from synapse.api.errors import SynapseError from synapse.storage.presence import UserPresenceState from synapse.types import RoomID, UserID @@ -67,6 +67,8 @@ ROOM_EVENT_FILTER_SCHEMA = { "contains_url": {"type": "boolean"}, "lazy_load_members": {"type": "boolean"}, "include_redundant_members": {"type": "boolean"}, + # Include or exclude events with the provided labels. + # cf https://github.com/matrix-org/matrix-doc/pull/2326 "org.matrix.labels": {"type": "array", "items": {"type": "string"}}, "org.matrix.not_labels": {"type": "array", "items": {"type": "string"}}, }, @@ -307,7 +309,7 @@ class Filter(object): content = event.get("content", {}) # check if there is a string url field in the content for filtering purposes contains_url = isinstance(content.get("url"), text_type) - labels = content.get(LabelsField, []) + labels = content.get(EventContentFields.Labels, []) return self.check_fields(room_id, sender, ev_type, labels, contains_url) diff --git a/synapse/storage/data_stores/main/events.py b/synapse/storage/data_stores/main/events.py index 2b900f1ce1..42ffa9066a 100644 --- a/synapse/storage/data_stores/main/events.py +++ b/synapse/storage/data_stores/main/events.py @@ -29,7 +29,7 @@ from prometheus_client import Counter, Histogram from twisted.internet import defer import synapse.metrics -from synapse.api.constants import EventTypes, LabelsField +from synapse.api.constants import EventTypes, EventContentFields from synapse.api.errors import SynapseError from synapse.events import EventBase # noqa: F401 from synapse.events.snapshot import EventContext # noqa: F401 @@ -1491,7 +1491,7 @@ class EventsStore( self._handle_event_relations(txn, event) # Store the labels for this event. - labels = event.content.get(LabelsField) + labels = event.content.get(EventContentFields.Labels) if labels: self.insert_labels_for_event_txn(txn, event.event_id, labels) @@ -2483,6 +2483,14 @@ class EventsStore( ) def insert_labels_for_event_txn(self, txn, event_id, labels): + """Store the mapping between an event's ID and its labels, with one row per + (event_id, label) tuple. + + Args: + txn (LoggingTransaction): The transaction to execute. + event_id (str): The event's ID. + labels (list[str]): A list of text labels. + """ return self._simple_insert_many_txn( txn=txn, table="event_labels", diff --git a/tests/api/test_filtering.py b/tests/api/test_filtering.py index e004ab1ee5..8ec48c4154 100644 --- a/tests/api/test_filtering.py +++ b/tests/api/test_filtering.py @@ -19,7 +19,7 @@ import jsonschema from twisted.internet import defer -from synapse.api.constants import LabelsField +from synapse.api.constants import EventContentFields from synapse.api.errors import SynapseError from synapse.api.filtering import Filter from synapse.events import FrozenEvent @@ -329,7 +329,7 @@ class FilteringTestCase(unittest.TestCase): sender="@foo:bar", type="m.room.message", room_id="!secretbase:unknown", - content={LabelsField: ["#fun"]}, + content={EventContentFields.Labels: ["#fun"]}, ) self.assertTrue(Filter(definition).check(event)) @@ -338,7 +338,7 @@ class FilteringTestCase(unittest.TestCase): sender="@foo:bar", type="m.room.message", room_id="!secretbase:unknown", - content={LabelsField: ["#notfun"]}, + content={EventContentFields.Labels: ["#notfun"]}, ) self.assertFalse(Filter(definition).check(event)) @@ -349,7 +349,7 @@ class FilteringTestCase(unittest.TestCase): sender="@foo:bar", type="m.room.message", room_id="!secretbase:unknown", - content={LabelsField: ["#fun"]}, + content={EventContentFields.Labels: ["#fun"]}, ) self.assertFalse(Filter(definition).check(event)) @@ -358,7 +358,7 @@ class FilteringTestCase(unittest.TestCase): sender="@foo:bar", type="m.room.message", room_id="!secretbase:unknown", - content={LabelsField: ["#notfun"]}, + content={EventContentFields.Labels: ["#notfun"]}, ) self.assertTrue(Filter(definition).check(event)) diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py index 188f47bd7d..0dc0faa0e5 100644 --- a/tests/rest/client/v1/test_rooms.py +++ b/tests/rest/client/v1/test_rooms.py @@ -24,7 +24,7 @@ from six.moves.urllib import parse as urlparse from twisted.internet import defer import synapse.rest.admin -from synapse.api.constants import EventTypes, LabelsField, Membership +from synapse.api.constants import EventContentFields, EventTypes, Membership from synapse.rest.client.v1 import login, profile, room from tests import unittest @@ -860,7 +860,7 @@ class RoomMessageListTestCase(RoomBase): content={ "msgtype": "m.text", "body": "with right label", - LabelsField: ["#fun"], + EventContentFields.Labels: ["#fun"], }, ) @@ -876,7 +876,7 @@ class RoomMessageListTestCase(RoomBase): content={ "msgtype": "m.text", "body": "with wrong label", - LabelsField: ["#work"], + EventContentFields.Labels: ["#work"], }, ) @@ -886,7 +886,7 @@ class RoomMessageListTestCase(RoomBase): content={ "msgtype": "m.text", "body": "with two wrong labels", - LabelsField: ["#work", "#notfun"], + EventContentFields.Labels: ["#work", "#notfun"], }, ) @@ -896,7 +896,7 @@ class RoomMessageListTestCase(RoomBase): content={ "msgtype": "m.text", "body": "with right label", - LabelsField: ["#fun"], + EventContentFields.Labels: ["#fun"], }, ) diff --git a/tests/rest/client/v2_alpha/test_sync.py b/tests/rest/client/v2_alpha/test_sync.py index c5c199d412..c3c6f75ced 100644 --- a/tests/rest/client/v2_alpha/test_sync.py +++ b/tests/rest/client/v2_alpha/test_sync.py @@ -17,7 +17,7 @@ import json from mock import Mock import synapse.rest.admin -from synapse.api.constants import EventTypes, LabelsField +from synapse.api.constants import EventContentFields, EventTypes from synapse.rest.client.v1 import login, room from synapse.rest.client.v2_alpha import sync @@ -157,7 +157,7 @@ class SyncFilterTestCase(unittest.HomeserverTestCase): content={ "msgtype": "m.text", "body": "with right label", - LabelsField: ["#fun"], + EventContentFields.Labels: ["#fun"], }, tok=tok, ) @@ -175,7 +175,7 @@ class SyncFilterTestCase(unittest.HomeserverTestCase): content={ "msgtype": "m.text", "body": "with wrong label", - LabelsField: ["#work"], + EventContentFields.Labels: ["#work"], }, tok=tok, ) @@ -186,7 +186,7 @@ class SyncFilterTestCase(unittest.HomeserverTestCase): content={ "msgtype": "m.text", "body": "with two wrong labels", - LabelsField: ["#work", "#notfun"], + EventContentFields.Labels: ["#work", "#notfun"], }, tok=tok, ) @@ -197,7 +197,7 @@ class SyncFilterTestCase(unittest.HomeserverTestCase): content={ "msgtype": "m.text", "body": "with right label", - LabelsField: ["#fun"], + EventContentFields.Labels: ["#fun"], }, tok=tok, ) From 57cdb046e48c6837fc9b41ade9b06d3ff68ec91b Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 1 Nov 2019 10:39:14 +0000 Subject: [PATCH 0408/1623] Lint --- synapse/api/constants.py | 1 + synapse/storage/data_stores/main/events.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/synapse/api/constants.py b/synapse/api/constants.py index cf4ce5f5a2..066ce18704 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -142,5 +142,6 @@ class LimitBlockingTypes(object): class EventContentFields(object): """Fields found in events' content, regardless of type.""" + # Labels for the event, cf https://github.com/matrix-org/matrix-doc/pull/2326 Labels = "org.matrix.labels" diff --git a/synapse/storage/data_stores/main/events.py b/synapse/storage/data_stores/main/events.py index 42ffa9066a..0480161056 100644 --- a/synapse/storage/data_stores/main/events.py +++ b/synapse/storage/data_stores/main/events.py @@ -29,7 +29,7 @@ from prometheus_client import Counter, Histogram from twisted.internet import defer import synapse.metrics -from synapse.api.constants import EventTypes, EventContentFields +from synapse.api.constants import EventContentFields, EventTypes from synapse.api.errors import SynapseError from synapse.events import EventBase # noqa: F401 from synapse.events.snapshot import EventContext # noqa: F401 From e3689ac6f75681c75f9931f79f248269811704c5 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 1 Nov 2019 10:41:23 +0000 Subject: [PATCH 0409/1623] Add unstable feature flag --- synapse/rest/client/versions.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/synapse/rest/client/versions.py b/synapse/rest/client/versions.py index 1044ae7b4e..bb30ce3f34 100644 --- a/synapse/rest/client/versions.py +++ b/synapse/rest/client/versions.py @@ -65,6 +65,9 @@ class VersionsRestServlet(RestServlet): "m.require_identity_server": False, # as per MSC2290 "m.separate_add_and_bind": True, + # Implements support for label-based filtering as described in + # MSC2326. + "org.matrix.label_based_filtering": True, }, }, ) From befd58f47bab1b8337032d27a995e08c7dd93a83 Mon Sep 17 00:00:00 2001 From: Neil Pilgrim Date: Fri, 1 Nov 2019 03:52:20 -0700 Subject: [PATCH 0410/1623] Document lint.sh & allow application to specified files only (#6312) --- CONTRIBUTING.rst | 8 ++++++++ changelog.d/6312.misc | 1 + scripts-dev/lint.sh | 14 +++++++++++--- 3 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 changelog.d/6312.misc diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index a71a4a696b..2fb3a95949 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -58,6 +58,14 @@ All Matrix projects have a well-defined code-style - and sometimes we've even got as far as documenting it... For instance, synapse's code style doc lives at https://github.com/matrix-org/synapse/tree/master/docs/code_style.md. +To facilitate meeting these criteria you can run ``scripts-dev/lint.sh`` +locally. Since this runs the tools listed in the above document, you'll need +python 3.6 and to install each tool. **Note that the script does not just +test/check, but also reformats code, so you may wish to ensure any new code is +committed first**. By default this script checks all files and can take some +time; if you alter only certain files, you might wish to specify paths as +arguments to reduce the run-time. + Please ensure your changes match the cosmetic style of the existing project, and **never** mix cosmetic and functional changes in the same commit, as it makes it horribly hard to review otherwise. diff --git a/changelog.d/6312.misc b/changelog.d/6312.misc new file mode 100644 index 0000000000..55e3e1654d --- /dev/null +++ b/changelog.d/6312.misc @@ -0,0 +1 @@ +Document the use of `lint.sh` for code style enforcement & extend it to run on specified paths only. diff --git a/scripts-dev/lint.sh b/scripts-dev/lint.sh index 02a2ca39e5..34c4854e11 100755 --- a/scripts-dev/lint.sh +++ b/scripts-dev/lint.sh @@ -7,7 +7,15 @@ set -e -isort -y -rc synapse tests scripts-dev scripts -flake8 synapse tests -python3 -m black synapse tests scripts-dev scripts +if [ $# -ge 1 ] +then + files=$* +else + files="synapse tests scripts-dev scripts" +fi + +echo "Linting these locations: $files" +isort -y -rc $files +flake8 $files +python3 -m black $files ./scripts-dev/config-lint.sh From 669b6cbda3e545f277def545954948090aec8980 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 1 Nov 2019 11:32:20 +0000 Subject: [PATCH 0411/1623] Fix up comment --- synapse/storage/data_stores/main/events.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/synapse/storage/data_stores/main/events.py b/synapse/storage/data_stores/main/events.py index 3a9b6bed45..a71d7346d2 100644 --- a/synapse/storage/data_stores/main/events.py +++ b/synapse/storage/data_stores/main/events.py @@ -1512,8 +1512,7 @@ class EventsStore( logger.info("[purge] finding state groups referenced by deleted events") # Get all state groups that are referenced by events that are to be - # deleted. We then go and check if they are referenced by other events - # or state groups, and if not we delete them. + # deleted. txn.execute( """ SELECT DISTINCT state_group FROM events_to_purge From a2c63c619ad1116cad07564c055aa6af8943d899 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 1 Nov 2019 11:47:28 +0000 Subject: [PATCH 0412/1623] Add more data to the event_labels table and fix the indexes --- synapse/storage/data_stores/main/events.py | 20 ++++++++++++++++--- .../main/schema/delta/56/event_labels.sql | 4 +++- synapse/storage/data_stores/main/stream.py | 2 +- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/synapse/storage/data_stores/main/events.py b/synapse/storage/data_stores/main/events.py index 0480161056..577e79bcf9 100644 --- a/synapse/storage/data_stores/main/events.py +++ b/synapse/storage/data_stores/main/events.py @@ -1493,7 +1493,9 @@ class EventsStore( # Store the labels for this event. labels = event.content.get(EventContentFields.Labels) if labels: - self.insert_labels_for_event_txn(txn, event.event_id, labels) + self.insert_labels_for_event_txn( + txn, event.event_id, labels, event.room_id, event.depth + ) # Insert into the room_memberships table. self._store_room_members_txn( @@ -2482,7 +2484,9 @@ class EventsStore( get_all_updated_current_state_deltas_txn, ) - def insert_labels_for_event_txn(self, txn, event_id, labels): + def insert_labels_for_event_txn( + self, txn, event_id, labels, room_id, topological_ordering + ): """Store the mapping between an event's ID and its labels, with one row per (event_id, label) tuple. @@ -2490,11 +2494,21 @@ class EventsStore( txn (LoggingTransaction): The transaction to execute. event_id (str): The event's ID. labels (list[str]): A list of text labels. + room_id (str): The ID of the room the event was sent to. + topological_ordering (int): The position of the event in the room's topology. """ return self._simple_insert_many_txn( txn=txn, table="event_labels", - values=[{"event_id": event_id, "label": label} for label in labels], + values=[ + { + "event_id": event_id, + "label": label, + "room_id": room_id, + "topological_ordering": topological_ordering, + } + for label in labels + ], ) diff --git a/synapse/storage/data_stores/main/schema/delta/56/event_labels.sql b/synapse/storage/data_stores/main/schema/delta/56/event_labels.sql index 9550b0adaa..765124d131 100644 --- a/synapse/storage/data_stores/main/schema/delta/56/event_labels.sql +++ b/synapse/storage/data_stores/main/schema/delta/56/event_labels.sql @@ -16,7 +16,9 @@ CREATE TABLE IF NOT EXISTS event_labels ( event_id TEXT, label TEXT, + room_id TEXT NOT NULL, + topological_ordering bigint NOT NULL, PRIMARY KEY(event_id, label) ); -CREATE INDEX event_labels_label_idx ON event_labels(label); +CREATE INDEX event_labels_room_id_label_idx ON event_labels(room_id, label, topological_ordering); diff --git a/synapse/storage/data_stores/main/stream.py b/synapse/storage/data_stores/main/stream.py index cfa34ba1e7..616ef91d4e 100644 --- a/synapse/storage/data_stores/main/stream.py +++ b/synapse/storage/data_stores/main/stream.py @@ -874,7 +874,7 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): sql = ( "SELECT DISTINCT event_id, topological_ordering, stream_ordering" " FROM events" - " LEFT JOIN event_labels USING (event_id)" + " LEFT JOIN event_labels USING (event_id, room_id, topological_ordering)" " WHERE outlier = ? AND room_id = ? AND %(bounds)s" " ORDER BY topological_ordering %(order)s," " stream_ordering %(order)s LIMIT ?" From fe1f2b452073e5939cddd23acc6f2d226673a03f Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Fri, 1 Nov 2019 12:03:44 +0000 Subject: [PATCH 0413/1623] Remove last usages of deprecated logging.warn method (#6314) --- changelog.d/6314.misc | 1 + synapse/config/logger.py | 4 ++-- synapse/handlers/directory.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 changelog.d/6314.misc diff --git a/changelog.d/6314.misc b/changelog.d/6314.misc new file mode 100644 index 0000000000..2369760272 --- /dev/null +++ b/changelog.d/6314.misc @@ -0,0 +1 @@ +Replace every instance of `logger.warn` method with `logger.warning` as the former is deprecated. \ No newline at end of file diff --git a/synapse/config/logger.py b/synapse/config/logger.py index 2d2c1e54df..75bb904718 100644 --- a/synapse/config/logger.py +++ b/synapse/config/logger.py @@ -234,8 +234,8 @@ def setup_logging( # make sure that the first thing we log is a thing we can grep backwards # for - logging.warn("***** STARTING SERVER *****") - logging.warn("Server %s version %s", sys.argv[0], get_version_string(synapse)) + logging.warning("***** STARTING SERVER *****") + logging.warning("Server %s version %s", sys.argv[0], get_version_string(synapse)) logging.info("Server hostname: %s", config.server_name) return logger diff --git a/synapse/handlers/directory.py b/synapse/handlers/directory.py index 526379c6f7..c4632f8984 100644 --- a/synapse/handlers/directory.py +++ b/synapse/handlers/directory.py @@ -250,7 +250,7 @@ class DirectoryHandler(BaseHandler): ignore_backoff=True, ) except CodeMessageException as e: - logging.warn("Error retrieving alias") + logging.warning("Error retrieving alias") if e.code == 404: result = None else: From 1cb84c6486a5131dd284f341bb657434becda255 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 1 Nov 2019 14:07:44 +0000 Subject: [PATCH 0414/1623] Support for routing outbound HTTP requests via a proxy (#6239) The `http_proxy` and `HTTPS_PROXY` env vars can be set to a `host[:port]` value which should point to a proxy. The address of the proxy should be excluded from IP blacklists such as the `url_preview_ip_range_blacklist`. The proxy will then be used for * push * url previews * phone-home stats * recaptcha validation * CAS auth validation It will *not* be used for: * Application Services * Identity servers * Outbound federation * In worker configurations, connections from workers to masters Fixes #4198. --- changelog.d/6238.feature | 1 + synapse/app/homeserver.py | 2 +- synapse/handlers/ui_auth/checkers.py | 2 +- synapse/http/client.py | 17 +- synapse/http/connectproxyclient.py | 195 ++++++++++ synapse/http/proxyagent.py | 195 ++++++++++ synapse/push/httppusher.py | 2 +- synapse/rest/client/v1/login.py | 2 +- synapse/rest/media/v1/preview_url_resource.py | 2 + synapse/server.py | 9 + synapse/server.pyi | 9 + tests/http/__init__.py | 17 + .../test_matrix_federation_agent.py | 11 +- tests/http/test_proxyagent.py | 334 ++++++++++++++++++ tests/push/test_http.py | 2 +- tests/server.py | 24 +- 16 files changed, 812 insertions(+), 12 deletions(-) create mode 100644 changelog.d/6238.feature create mode 100644 synapse/http/connectproxyclient.py create mode 100644 synapse/http/proxyagent.py create mode 100644 tests/http/test_proxyagent.py diff --git a/changelog.d/6238.feature b/changelog.d/6238.feature new file mode 100644 index 0000000000..d225ac33b6 --- /dev/null +++ b/changelog.d/6238.feature @@ -0,0 +1 @@ +Add support for outbound http proxying via http_proxy/HTTPS_PROXY env vars. diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py index 8997c1f9e7..8d28076d92 100644 --- a/synapse/app/homeserver.py +++ b/synapse/app/homeserver.py @@ -565,7 +565,7 @@ def run(hs): "Reporting stats to %s: %s" % (hs.config.report_stats_endpoint, stats) ) try: - yield hs.get_simple_http_client().put_json( + yield hs.get_proxied_http_client().put_json( hs.config.report_stats_endpoint, stats ) except Exception as e: diff --git a/synapse/handlers/ui_auth/checkers.py b/synapse/handlers/ui_auth/checkers.py index 29aa1e5aaf..8363d887a9 100644 --- a/synapse/handlers/ui_auth/checkers.py +++ b/synapse/handlers/ui_auth/checkers.py @@ -81,7 +81,7 @@ class RecaptchaAuthChecker(UserInteractiveAuthChecker): def __init__(self, hs): super().__init__(hs) self._enabled = bool(hs.config.recaptcha_private_key) - self._http_client = hs.get_simple_http_client() + self._http_client = hs.get_proxied_http_client() self._url = hs.config.recaptcha_siteverify_api self._secret = hs.config.recaptcha_private_key diff --git a/synapse/http/client.py b/synapse/http/client.py index 2df5b383b5..d4c285445e 100644 --- a/synapse/http/client.py +++ b/synapse/http/client.py @@ -45,6 +45,7 @@ from synapse.http import ( cancelled_to_request_timed_out_error, redact_uri, ) +from synapse.http.proxyagent import ProxyAgent from synapse.logging.context import make_deferred_yieldable from synapse.logging.opentracing import set_tag, start_active_span, tags from synapse.util.async_helpers import timeout_deferred @@ -183,7 +184,15 @@ class SimpleHttpClient(object): using HTTP in Matrix """ - def __init__(self, hs, treq_args={}, ip_whitelist=None, ip_blacklist=None): + def __init__( + self, + hs, + treq_args={}, + ip_whitelist=None, + ip_blacklist=None, + http_proxy=None, + https_proxy=None, + ): """ Args: hs (synapse.server.HomeServer) @@ -192,6 +201,8 @@ class SimpleHttpClient(object): we may not request. ip_whitelist (netaddr.IPSet): The whitelisted IP addresses, that we can request if it were otherwise caught in a blacklist. + http_proxy (bytes): proxy server to use for http connections. host[:port] + https_proxy (bytes): proxy server to use for https connections. host[:port] """ self.hs = hs @@ -236,11 +247,13 @@ class SimpleHttpClient(object): # The default context factory in Twisted 14.0.0 (which we require) is # BrowserLikePolicyForHTTPS which will do regular cert validation # 'like a browser' - self.agent = Agent( + self.agent = ProxyAgent( self.reactor, connectTimeout=15, contextFactory=self.hs.get_http_client_context_factory(), pool=pool, + http_proxy=http_proxy, + https_proxy=https_proxy, ) if self._ip_blacklist: diff --git a/synapse/http/connectproxyclient.py b/synapse/http/connectproxyclient.py new file mode 100644 index 0000000000..be7b2ceb8e --- /dev/null +++ b/synapse/http/connectproxyclient.py @@ -0,0 +1,195 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from zope.interface import implementer + +from twisted.internet import defer, protocol +from twisted.internet.error import ConnectError +from twisted.internet.interfaces import IStreamClientEndpoint +from twisted.internet.protocol import connectionDone +from twisted.web import http + +logger = logging.getLogger(__name__) + + +class ProxyConnectError(ConnectError): + pass + + +@implementer(IStreamClientEndpoint) +class HTTPConnectProxyEndpoint(object): + """An Endpoint implementation which will send a CONNECT request to an http proxy + + Wraps an existing HostnameEndpoint for the proxy. + + When we get the connect() request from the connection pool (via the TLS wrapper), + we'll first connect to the proxy endpoint with a ProtocolFactory which will make the + CONNECT request. Once that completes, we invoke the protocolFactory which was passed + in. + + Args: + reactor: the Twisted reactor to use for the connection + proxy_endpoint (IStreamClientEndpoint): the endpoint to use to connect to the + proxy + host (bytes): hostname that we want to CONNECT to + port (int): port that we want to connect to + """ + + def __init__(self, reactor, proxy_endpoint, host, port): + self._reactor = reactor + self._proxy_endpoint = proxy_endpoint + self._host = host + self._port = port + + def __repr__(self): + return "" % (self._proxy_endpoint,) + + def connect(self, protocolFactory): + f = HTTPProxiedClientFactory(self._host, self._port, protocolFactory) + d = self._proxy_endpoint.connect(f) + # once the tcp socket connects successfully, we need to wait for the + # CONNECT to complete. + d.addCallback(lambda conn: f.on_connection) + return d + + +class HTTPProxiedClientFactory(protocol.ClientFactory): + """ClientFactory wrapper that triggers an HTTP proxy CONNECT on connect. + + Once the CONNECT completes, invokes the original ClientFactory to build the + HTTP Protocol object and run the rest of the connection. + + Args: + dst_host (bytes): hostname that we want to CONNECT to + dst_port (int): port that we want to connect to + wrapped_factory (protocol.ClientFactory): The original Factory + """ + + def __init__(self, dst_host, dst_port, wrapped_factory): + self.dst_host = dst_host + self.dst_port = dst_port + self.wrapped_factory = wrapped_factory + self.on_connection = defer.Deferred() + + def startedConnecting(self, connector): + return self.wrapped_factory.startedConnecting(connector) + + def buildProtocol(self, addr): + wrapped_protocol = self.wrapped_factory.buildProtocol(addr) + + return HTTPConnectProtocol( + self.dst_host, self.dst_port, wrapped_protocol, self.on_connection + ) + + def clientConnectionFailed(self, connector, reason): + logger.debug("Connection to proxy failed: %s", reason) + if not self.on_connection.called: + self.on_connection.errback(reason) + return self.wrapped_factory.clientConnectionFailed(connector, reason) + + def clientConnectionLost(self, connector, reason): + logger.debug("Connection to proxy lost: %s", reason) + if not self.on_connection.called: + self.on_connection.errback(reason) + return self.wrapped_factory.clientConnectionLost(connector, reason) + + +class HTTPConnectProtocol(protocol.Protocol): + """Protocol that wraps an existing Protocol to do a CONNECT handshake at connect + + Args: + host (bytes): The original HTTP(s) hostname or IPv4 or IPv6 address literal + to put in the CONNECT request + + port (int): The original HTTP(s) port to put in the CONNECT request + + wrapped_protocol (interfaces.IProtocol): the original protocol (probably + HTTPChannel or TLSMemoryBIOProtocol, but could be anything really) + + connected_deferred (Deferred): a Deferred which will be callbacked with + wrapped_protocol when the CONNECT completes + """ + + def __init__(self, host, port, wrapped_protocol, connected_deferred): + self.host = host + self.port = port + self.wrapped_protocol = wrapped_protocol + self.connected_deferred = connected_deferred + self.http_setup_client = HTTPConnectSetupClient(self.host, self.port) + self.http_setup_client.on_connected.addCallback(self.proxyConnected) + + def connectionMade(self): + self.http_setup_client.makeConnection(self.transport) + + def connectionLost(self, reason=connectionDone): + if self.wrapped_protocol.connected: + self.wrapped_protocol.connectionLost(reason) + + self.http_setup_client.connectionLost(reason) + + if not self.connected_deferred.called: + self.connected_deferred.errback(reason) + + def proxyConnected(self, _): + self.wrapped_protocol.makeConnection(self.transport) + + self.connected_deferred.callback(self.wrapped_protocol) + + # Get any pending data from the http buf and forward it to the original protocol + buf = self.http_setup_client.clearLineBuffer() + if buf: + self.wrapped_protocol.dataReceived(buf) + + def dataReceived(self, data): + # if we've set up the HTTP protocol, we can send the data there + if self.wrapped_protocol.connected: + return self.wrapped_protocol.dataReceived(data) + + # otherwise, we must still be setting up the connection: send the data to the + # setup client + return self.http_setup_client.dataReceived(data) + + +class HTTPConnectSetupClient(http.HTTPClient): + """HTTPClient protocol to send a CONNECT message for proxies and read the response. + + Args: + host (bytes): The hostname to send in the CONNECT message + port (int): The port to send in the CONNECT message + """ + + def __init__(self, host, port): + self.host = host + self.port = port + self.on_connected = defer.Deferred() + + def connectionMade(self): + logger.debug("Connected to proxy, sending CONNECT") + self.sendCommand(b"CONNECT", b"%s:%d" % (self.host, self.port)) + self.endHeaders() + + def handleStatus(self, version, status, message): + logger.debug("Got Status: %s %s %s", status, message, version) + if status != b"200": + raise ProxyConnectError("Unexpected status on CONNECT: %s" % status) + + def handleEndHeaders(self): + logger.debug("End Headers") + self.on_connected.callback(None) + + def handleResponse(self, body): + pass diff --git a/synapse/http/proxyagent.py b/synapse/http/proxyagent.py new file mode 100644 index 0000000000..332da02a8d --- /dev/null +++ b/synapse/http/proxyagent.py @@ -0,0 +1,195 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging +import re + +from zope.interface import implementer + +from twisted.internet import defer +from twisted.internet.endpoints import HostnameEndpoint, wrapClientTLS +from twisted.python.failure import Failure +from twisted.web.client import URI, BrowserLikePolicyForHTTPS, _AgentBase +from twisted.web.error import SchemeNotSupported +from twisted.web.iweb import IAgent + +from synapse.http.connectproxyclient import HTTPConnectProxyEndpoint + +logger = logging.getLogger(__name__) + +_VALID_URI = re.compile(br"\A[\x21-\x7e]+\Z") + + +@implementer(IAgent) +class ProxyAgent(_AgentBase): + """An Agent implementation which will use an HTTP proxy if one was requested + + Args: + reactor: twisted reactor to place outgoing + connections. + + contextFactory (IPolicyForHTTPS): A factory for TLS contexts, to control the + verification parameters of OpenSSL. The default is to use a + `BrowserLikePolicyForHTTPS`, so unless you have special + requirements you can leave this as-is. + + connectTimeout (float): The amount of time that this Agent will wait + for the peer to accept a connection. + + bindAddress (bytes): The local address for client sockets to bind to. + + pool (HTTPConnectionPool|None): connection pool to be used. If None, a + non-persistent pool instance will be created. + """ + + def __init__( + self, + reactor, + contextFactory=BrowserLikePolicyForHTTPS(), + connectTimeout=None, + bindAddress=None, + pool=None, + http_proxy=None, + https_proxy=None, + ): + _AgentBase.__init__(self, reactor, pool) + + self._endpoint_kwargs = {} + if connectTimeout is not None: + self._endpoint_kwargs["timeout"] = connectTimeout + if bindAddress is not None: + self._endpoint_kwargs["bindAddress"] = bindAddress + + self.http_proxy_endpoint = _http_proxy_endpoint( + http_proxy, reactor, **self._endpoint_kwargs + ) + + self.https_proxy_endpoint = _http_proxy_endpoint( + https_proxy, reactor, **self._endpoint_kwargs + ) + + self._policy_for_https = contextFactory + self._reactor = reactor + + def request(self, method, uri, headers=None, bodyProducer=None): + """ + Issue a request to the server indicated by the given uri. + + Supports `http` and `https` schemes. + + An existing connection from the connection pool may be used or a new one may be + created. + + See also: twisted.web.iweb.IAgent.request + + Args: + method (bytes): The request method to use, such as `GET`, `POST`, etc + + uri (bytes): The location of the resource to request. + + headers (Headers|None): Extra headers to send with the request + + bodyProducer (IBodyProducer|None): An object which can generate bytes to + make up the body of this request (for example, the properly encoded + contents of a file for a file upload). Or, None if the request is to + have no body. + + Returns: + Deferred[IResponse]: completes when the header of the response has + been received (regardless of the response status code). + """ + uri = uri.strip() + if not _VALID_URI.match(uri): + raise ValueError("Invalid URI {!r}".format(uri)) + + parsed_uri = URI.fromBytes(uri) + pool_key = (parsed_uri.scheme, parsed_uri.host, parsed_uri.port) + request_path = parsed_uri.originForm + + if parsed_uri.scheme == b"http" and self.http_proxy_endpoint: + # Cache *all* connections under the same key, since we are only + # connecting to a single destination, the proxy: + pool_key = ("http-proxy", self.http_proxy_endpoint) + endpoint = self.http_proxy_endpoint + request_path = uri + elif parsed_uri.scheme == b"https" and self.https_proxy_endpoint: + endpoint = HTTPConnectProxyEndpoint( + self._reactor, + self.https_proxy_endpoint, + parsed_uri.host, + parsed_uri.port, + ) + else: + # not using a proxy + endpoint = HostnameEndpoint( + self._reactor, parsed_uri.host, parsed_uri.port, **self._endpoint_kwargs + ) + + logger.debug("Requesting %s via %s", uri, endpoint) + + if parsed_uri.scheme == b"https": + tls_connection_creator = self._policy_for_https.creatorForNetloc( + parsed_uri.host, parsed_uri.port + ) + endpoint = wrapClientTLS(tls_connection_creator, endpoint) + elif parsed_uri.scheme == b"http": + pass + else: + return defer.fail( + Failure( + SchemeNotSupported("Unsupported scheme: %r" % (parsed_uri.scheme,)) + ) + ) + + return self._requestWithEndpoint( + pool_key, endpoint, method, parsed_uri, headers, bodyProducer, request_path + ) + + +def _http_proxy_endpoint(proxy, reactor, **kwargs): + """Parses an http proxy setting and returns an endpoint for the proxy + + Args: + proxy (bytes|None): the proxy setting + reactor: reactor to be used to connect to the proxy + kwargs: other args to be passed to HostnameEndpoint + + Returns: + interfaces.IStreamClientEndpoint|None: endpoint to use to connect to the proxy, + or None + """ + if proxy is None: + return None + + # currently we only support hostname:port. Some apps also support + # protocol://[:port], which allows a way of requiring a TLS connection to the + # proxy. + + host, port = parse_host_port(proxy, default_port=1080) + return HostnameEndpoint(reactor, host, port, **kwargs) + + +def parse_host_port(hostport, default_port=None): + # could have sworn we had one of these somewhere else... + if b":" in hostport: + host, port = hostport.rsplit(b":", 1) + try: + port = int(port) + return host, port + except ValueError: + # the thing after the : wasn't a valid port; presumably this is an + # IPv6 address. + pass + + return hostport, default_port diff --git a/synapse/push/httppusher.py b/synapse/push/httppusher.py index 7dde2ad055..e994037be6 100644 --- a/synapse/push/httppusher.py +++ b/synapse/push/httppusher.py @@ -103,7 +103,7 @@ class HttpPusher(object): if "url" not in self.data: raise PusherConfigException("'url' required in data for HTTP pusher") self.url = self.data["url"] - self.http_client = hs.get_simple_http_client() + self.http_client = hs.get_proxied_http_client() self.data_minus_url = {} self.data_minus_url.update(self.data) del self.data_minus_url["url"] diff --git a/synapse/rest/client/v1/login.py b/synapse/rest/client/v1/login.py index 00a7dd6d09..24a0ce74f2 100644 --- a/synapse/rest/client/v1/login.py +++ b/synapse/rest/client/v1/login.py @@ -381,7 +381,7 @@ class CasTicketServlet(RestServlet): self.cas_displayname_attribute = hs.config.cas_displayname_attribute self.cas_required_attributes = hs.config.cas_required_attributes self._sso_auth_handler = SSOAuthHandler(hs) - self._http_client = hs.get_simple_http_client() + self._http_client = hs.get_proxied_http_client() @defer.inlineCallbacks def on_GET(self, request): diff --git a/synapse/rest/media/v1/preview_url_resource.py b/synapse/rest/media/v1/preview_url_resource.py index 5a25b6b3fc..531d923f76 100644 --- a/synapse/rest/media/v1/preview_url_resource.py +++ b/synapse/rest/media/v1/preview_url_resource.py @@ -74,6 +74,8 @@ class PreviewUrlResource(DirectServeResource): treq_args={"browser_like_redirects": True}, ip_whitelist=hs.config.url_preview_ip_range_whitelist, ip_blacklist=hs.config.url_preview_ip_range_blacklist, + http_proxy=os.getenv("http_proxy"), + https_proxy=os.getenv("HTTPS_PROXY"), ) self.media_repo = media_repo self.primary_base_path = media_repo.primary_base_path diff --git a/synapse/server.py b/synapse/server.py index 0b81af646c..f8aeebcff8 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -23,6 +23,7 @@ # Imports required for the default HomeServer() implementation import abc import logging +import os from twisted.enterprise import adbapi from twisted.mail.smtp import sendmail @@ -168,6 +169,7 @@ class HomeServer(object): "filtering", "http_client_context_factory", "simple_http_client", + "proxied_http_client", "media_repository", "media_repository_resource", "federation_transport_client", @@ -311,6 +313,13 @@ class HomeServer(object): def build_simple_http_client(self): return SimpleHttpClient(self) + def build_proxied_http_client(self): + return SimpleHttpClient( + self, + http_proxy=os.getenv("http_proxy"), + https_proxy=os.getenv("HTTPS_PROXY"), + ) + def build_room_creation_handler(self): return RoomCreationHandler(self) diff --git a/synapse/server.pyi b/synapse/server.pyi index 83d1f11283..b5e0b57095 100644 --- a/synapse/server.pyi +++ b/synapse/server.pyi @@ -12,6 +12,7 @@ import synapse.handlers.message import synapse.handlers.room import synapse.handlers.room_member import synapse.handlers.set_password +import synapse.http.client import synapse.rest.media.v1.media_repository import synapse.server_notices.server_notices_manager import synapse.server_notices.server_notices_sender @@ -38,6 +39,14 @@ class HomeServer(object): pass def get_state_resolution_handler(self) -> synapse.state.StateResolutionHandler: pass + def get_simple_http_client(self) -> synapse.http.client.SimpleHttpClient: + """Fetch an HTTP client implementation which doesn't do any blacklisting + or support any HTTP_PROXY settings""" + pass + def get_proxied_http_client(self) -> synapse.http.client.SimpleHttpClient: + """Fetch an HTTP client implementation which doesn't do any blacklisting + but does support HTTP_PROXY settings""" + pass def get_deactivate_account_handler( self, ) -> synapse.handlers.deactivate_account.DeactivateAccountHandler: diff --git a/tests/http/__init__.py b/tests/http/__init__.py index 2d5dba6464..2096ba3c91 100644 --- a/tests/http/__init__.py +++ b/tests/http/__init__.py @@ -20,6 +20,23 @@ from zope.interface import implementer from OpenSSL import SSL from OpenSSL.SSL import Connection from twisted.internet.interfaces import IOpenSSLServerConnectionCreator +from twisted.internet.ssl import Certificate, trustRootFromCertificates +from twisted.web.client import BrowserLikePolicyForHTTPS # noqa: F401 +from twisted.web.iweb import IPolicyForHTTPS # noqa: F401 + + +def get_test_https_policy(): + """Get a test IPolicyForHTTPS which trusts the test CA cert + + Returns: + IPolicyForHTTPS + """ + ca_file = get_test_ca_cert_file() + with open(ca_file) as stream: + content = stream.read() + cert = Certificate.loadPEM(content) + trust_root = trustRootFromCertificates([cert]) + return BrowserLikePolicyForHTTPS(trustRoot=trust_root) def get_test_ca_cert_file(): diff --git a/tests/http/federation/test_matrix_federation_agent.py b/tests/http/federation/test_matrix_federation_agent.py index 71d7025264..cfcd98ff7d 100644 --- a/tests/http/federation/test_matrix_federation_agent.py +++ b/tests/http/federation/test_matrix_federation_agent.py @@ -124,19 +124,24 @@ class MatrixFederationAgentTests(unittest.TestCase): FakeTransport(client_protocol, self.reactor, server_tls_protocol) ) + # grab a hold of the TLS connection, in case it gets torn down + server_tls_connection = server_tls_protocol._tlsConnection + + # fish the test server back out of the server-side TLS protocol. + http_protocol = server_tls_protocol.wrappedProtocol + # give the reactor a pump to get the TLS juices flowing. self.reactor.pump((0.1,)) # check the SNI - server_name = server_tls_protocol._tlsConnection.get_servername() + server_name = server_tls_connection.get_servername() self.assertEqual( server_name, expected_sni, "Expected SNI %s but got %s" % (expected_sni, server_name), ) - # fish the test server back out of the server-side TLS protocol. - return server_tls_protocol.wrappedProtocol + return http_protocol @defer.inlineCallbacks def _make_get_request(self, uri): diff --git a/tests/http/test_proxyagent.py b/tests/http/test_proxyagent.py new file mode 100644 index 0000000000..22abf76515 --- /dev/null +++ b/tests/http/test_proxyagent.py @@ -0,0 +1,334 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging + +import treq + +from twisted.internet import interfaces # noqa: F401 +from twisted.internet.protocol import Factory +from twisted.protocols.tls import TLSMemoryBIOFactory +from twisted.web.http import HTTPChannel + +from synapse.http.proxyagent import ProxyAgent + +from tests.http import TestServerTLSConnectionFactory, get_test_https_policy +from tests.server import FakeTransport, ThreadedMemoryReactorClock +from tests.unittest import TestCase + +logger = logging.getLogger(__name__) + +HTTPFactory = Factory.forProtocol(HTTPChannel) + + +class MatrixFederationAgentTests(TestCase): + def setUp(self): + self.reactor = ThreadedMemoryReactorClock() + + def _make_connection( + self, client_factory, server_factory, ssl=False, expected_sni=None + ): + """Builds a test server, and completes the outgoing client connection + + Args: + client_factory (interfaces.IProtocolFactory): the the factory that the + application is trying to use to make the outbound connection. We will + invoke it to build the client Protocol + + server_factory (interfaces.IProtocolFactory): a factory to build the + server-side protocol + + ssl (bool): If true, we will expect an ssl connection and wrap + server_factory with a TLSMemoryBIOFactory + + expected_sni (bytes|None): the expected SNI value + + Returns: + IProtocol: the server Protocol returned by server_factory + """ + if ssl: + server_factory = _wrap_server_factory_for_tls(server_factory) + + server_protocol = server_factory.buildProtocol(None) + + # now, tell the client protocol factory to build the client protocol, + # and wire the output of said protocol up to the server via + # a FakeTransport. + # + # Normally this would be done by the TCP socket code in Twisted, but we are + # stubbing that out here. + client_protocol = client_factory.buildProtocol(None) + client_protocol.makeConnection( + FakeTransport(server_protocol, self.reactor, client_protocol) + ) + + # tell the server protocol to send its stuff back to the client, too + server_protocol.makeConnection( + FakeTransport(client_protocol, self.reactor, server_protocol) + ) + + if ssl: + http_protocol = server_protocol.wrappedProtocol + tls_connection = server_protocol._tlsConnection + else: + http_protocol = server_protocol + tls_connection = None + + # give the reactor a pump to get the TLS juices flowing (if needed) + self.reactor.advance(0) + + if expected_sni is not None: + server_name = tls_connection.get_servername() + self.assertEqual( + server_name, + expected_sni, + "Expected SNI %s but got %s" % (expected_sni, server_name), + ) + + return http_protocol + + def test_http_request(self): + agent = ProxyAgent(self.reactor) + + self.reactor.lookups["test.com"] = "1.2.3.4" + d = agent.request(b"GET", b"http://test.com") + + # there should be a pending TCP connection + clients = self.reactor.tcpClients + self.assertEqual(len(clients), 1) + (host, port, client_factory, _timeout, _bindAddress) = clients[0] + self.assertEqual(host, "1.2.3.4") + self.assertEqual(port, 80) + + # make a test server, and wire up the client + http_server = self._make_connection( + client_factory, _get_test_protocol_factory() + ) + + # the FakeTransport is async, so we need to pump the reactor + self.reactor.advance(0) + + # now there should be a pending request + self.assertEqual(len(http_server.requests), 1) + + request = http_server.requests[0] + self.assertEqual(request.method, b"GET") + self.assertEqual(request.path, b"/") + self.assertEqual(request.requestHeaders.getRawHeaders(b"host"), [b"test.com"]) + request.write(b"result") + request.finish() + + self.reactor.advance(0) + + resp = self.successResultOf(d) + body = self.successResultOf(treq.content(resp)) + self.assertEqual(body, b"result") + + def test_https_request(self): + agent = ProxyAgent(self.reactor, contextFactory=get_test_https_policy()) + + self.reactor.lookups["test.com"] = "1.2.3.4" + d = agent.request(b"GET", b"https://test.com/abc") + + # there should be a pending TCP connection + clients = self.reactor.tcpClients + self.assertEqual(len(clients), 1) + (host, port, client_factory, _timeout, _bindAddress) = clients[0] + self.assertEqual(host, "1.2.3.4") + self.assertEqual(port, 443) + + # make a test server, and wire up the client + http_server = self._make_connection( + client_factory, + _get_test_protocol_factory(), + ssl=True, + expected_sni=b"test.com", + ) + + # the FakeTransport is async, so we need to pump the reactor + self.reactor.advance(0) + + # now there should be a pending request + self.assertEqual(len(http_server.requests), 1) + + request = http_server.requests[0] + self.assertEqual(request.method, b"GET") + self.assertEqual(request.path, b"/abc") + self.assertEqual(request.requestHeaders.getRawHeaders(b"host"), [b"test.com"]) + request.write(b"result") + request.finish() + + self.reactor.advance(0) + + resp = self.successResultOf(d) + body = self.successResultOf(treq.content(resp)) + self.assertEqual(body, b"result") + + def test_http_request_via_proxy(self): + agent = ProxyAgent(self.reactor, http_proxy=b"proxy.com:8888") + + self.reactor.lookups["proxy.com"] = "1.2.3.5" + d = agent.request(b"GET", b"http://test.com") + + # there should be a pending TCP connection + clients = self.reactor.tcpClients + self.assertEqual(len(clients), 1) + (host, port, client_factory, _timeout, _bindAddress) = clients[0] + self.assertEqual(host, "1.2.3.5") + self.assertEqual(port, 8888) + + # make a test server, and wire up the client + http_server = self._make_connection( + client_factory, _get_test_protocol_factory() + ) + + # the FakeTransport is async, so we need to pump the reactor + self.reactor.advance(0) + + # now there should be a pending request + self.assertEqual(len(http_server.requests), 1) + + request = http_server.requests[0] + self.assertEqual(request.method, b"GET") + self.assertEqual(request.path, b"http://test.com") + self.assertEqual(request.requestHeaders.getRawHeaders(b"host"), [b"test.com"]) + request.write(b"result") + request.finish() + + self.reactor.advance(0) + + resp = self.successResultOf(d) + body = self.successResultOf(treq.content(resp)) + self.assertEqual(body, b"result") + + def test_https_request_via_proxy(self): + agent = ProxyAgent( + self.reactor, + contextFactory=get_test_https_policy(), + https_proxy=b"proxy.com", + ) + + self.reactor.lookups["proxy.com"] = "1.2.3.5" + d = agent.request(b"GET", b"https://test.com/abc") + + # there should be a pending TCP connection + clients = self.reactor.tcpClients + self.assertEqual(len(clients), 1) + (host, port, client_factory, _timeout, _bindAddress) = clients[0] + self.assertEqual(host, "1.2.3.5") + self.assertEqual(port, 1080) + + # make a test HTTP server, and wire up the client + proxy_server = self._make_connection( + client_factory, _get_test_protocol_factory() + ) + + # fish the transports back out so that we can do the old switcheroo + s2c_transport = proxy_server.transport + client_protocol = s2c_transport.other + c2s_transport = client_protocol.transport + + # the FakeTransport is async, so we need to pump the reactor + self.reactor.advance(0) + + # now there should be a pending CONNECT request + self.assertEqual(len(proxy_server.requests), 1) + + request = proxy_server.requests[0] + self.assertEqual(request.method, b"CONNECT") + self.assertEqual(request.path, b"test.com:443") + + # tell the proxy server not to close the connection + proxy_server.persistent = True + + # this just stops the http Request trying to do a chunked response + # request.setHeader(b"Content-Length", b"0") + request.finish() + + # now we can replace the proxy channel with a new, SSL-wrapped HTTP channel + ssl_factory = _wrap_server_factory_for_tls(_get_test_protocol_factory()) + ssl_protocol = ssl_factory.buildProtocol(None) + http_server = ssl_protocol.wrappedProtocol + + ssl_protocol.makeConnection( + FakeTransport(client_protocol, self.reactor, ssl_protocol) + ) + c2s_transport.other = ssl_protocol + + self.reactor.advance(0) + + server_name = ssl_protocol._tlsConnection.get_servername() + expected_sni = b"test.com" + self.assertEqual( + server_name, + expected_sni, + "Expected SNI %s but got %s" % (expected_sni, server_name), + ) + + # now there should be a pending request + self.assertEqual(len(http_server.requests), 1) + + request = http_server.requests[0] + self.assertEqual(request.method, b"GET") + self.assertEqual(request.path, b"/abc") + self.assertEqual(request.requestHeaders.getRawHeaders(b"host"), [b"test.com"]) + request.write(b"result") + request.finish() + + self.reactor.advance(0) + + resp = self.successResultOf(d) + body = self.successResultOf(treq.content(resp)) + self.assertEqual(body, b"result") + + +def _wrap_server_factory_for_tls(factory, sanlist=None): + """Wrap an existing Protocol Factory with a test TLSMemoryBIOFactory + + The resultant factory will create a TLS server which presents a certificate + signed by our test CA, valid for the domains in `sanlist` + + Args: + factory (interfaces.IProtocolFactory): protocol factory to wrap + sanlist (iterable[bytes]): list of domains the cert should be valid for + + Returns: + interfaces.IProtocolFactory + """ + if sanlist is None: + sanlist = [b"DNS:test.com"] + + connection_creator = TestServerTLSConnectionFactory(sanlist=sanlist) + return TLSMemoryBIOFactory( + connection_creator, isClient=False, wrappedFactory=factory + ) + + +def _get_test_protocol_factory(): + """Get a protocol Factory which will build an HTTPChannel + + Returns: + interfaces.IProtocolFactory + """ + server_factory = Factory.forProtocol(HTTPChannel) + + # Request.finish expects the factory to have a 'log' method. + server_factory.log = _log_request + + return server_factory + + +def _log_request(request): + """Implements Factory.log, which is expected by Request.finish""" + logger.info("Completed request %s", request) diff --git a/tests/push/test_http.py b/tests/push/test_http.py index 8ce6bb62da..af2327fb66 100644 --- a/tests/push/test_http.py +++ b/tests/push/test_http.py @@ -50,7 +50,7 @@ class HTTPPusherTests(HomeserverTestCase): config = self.default_config() config["start_pushers"] = True - hs = self.setup_test_homeserver(config=config, simple_http_client=m) + hs = self.setup_test_homeserver(config=config, proxied_http_client=m) return hs diff --git a/tests/server.py b/tests/server.py index 469efb4edb..f878aeaada 100644 --- a/tests/server.py +++ b/tests/server.py @@ -395,11 +395,24 @@ class FakeTransport(object): self.disconnecting = True if self._protocol: self._protocol.connectionLost(reason) - self.disconnected = True + + # if we still have data to write, delay until that is done + if self.buffer: + logger.info( + "FakeTransport: Delaying disconnect until buffer is flushed" + ) + else: + self.disconnected = True def abortConnection(self): logger.info("FakeTransport: abortConnection()") - self.loseConnection() + + if not self.disconnecting: + self.disconnecting = True + if self._protocol: + self._protocol.connectionLost(None) + + self.disconnected = True def pauseProducing(self): if not self.producer: @@ -430,6 +443,9 @@ class FakeTransport(object): self._reactor.callLater(0.0, _produce) def write(self, byt): + if self.disconnecting: + raise Exception("Writing to disconnecting FakeTransport") + self.buffer = self.buffer + byt # always actually do the write asynchronously. Some protocols (notably the @@ -474,6 +490,10 @@ class FakeTransport(object): if self.buffer and self.autoflush: self._reactor.callLater(0.0, self.flush) + if not self.buffer and self.disconnecting: + logger.info("FakeTransport: Buffer now empty, completing disconnect") + self.disconnected = True + def connect_client(reactor: IReactorTCP, client_id: int) -> AccumulatingProtocol: """ From 67a65918ad7723400f25339ef3f4447e7b3dc1b6 Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Fri, 1 Nov 2019 16:45:09 +0200 Subject: [PATCH 0415/1623] Add contributer docs for using the provided linters script (#6164) * Add lint dependencies black, flake8 and isort These are required when running the `lint.sh` dev scripts. Signed-off-by: Jason Robinson * Add contributer docs for using the providers linters script Add also to the pull request template to avoid build failures due to people not knowing that linters need running. Signed-off-by: Jason Robinson * Fix mention of linter errors correction Co-Authored-By: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> * Add mention for installing linter dependencies Co-Authored-By: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> * Remove linters from python dependencies as per PR review Signed-off-by: Jason Robinson --- .github/PULL_REQUEST_TEMPLATE.md | 1 + CONTRIBUTING.rst | 11 +++++++++++ changelog.d/6164.doc | 1 + 3 files changed, 13 insertions(+) create mode 100644 changelog.d/6164.doc diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 1ead0d0030..8939fda67d 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -5,3 +5,4 @@ * [ ] Pull request is based on the develop branch * [ ] Pull request includes a [changelog file](https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.rst#changelog) * [ ] Pull request includes a [sign off](https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.rst#sign-off) +* [ ] Code style is correct (run the [linters](https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.rst#code-style)) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 2fb3a95949..df81f6e54f 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -70,6 +70,17 @@ Please ensure your changes match the cosmetic style of the existing project, and **never** mix cosmetic and functional changes in the same commit, as it makes it horribly hard to review otherwise. +Before doing a commit, ensure the changes you've made don't produce +linting errors. You can do this by running the linters as follows. Ensure to +commit any files that were corrected. + +:: + # Install the dependencies + pip install -U black flake8 isort + + # Run the linter script + ./scripts-dev/lint.sh + Changelog ~~~~~~~~~ diff --git a/changelog.d/6164.doc b/changelog.d/6164.doc new file mode 100644 index 0000000000..f9395b02b3 --- /dev/null +++ b/changelog.d/6164.doc @@ -0,0 +1 @@ +Contributor documentation now mentions script to run linters. From 559844565515334d1168746f96fcc0db4fce3f6e Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 1 Nov 2019 16:18:34 +0000 Subject: [PATCH 0416/1623] Update synapse/storage/data_stores/main/schema/delta/56/event_labels.sql Co-Authored-By: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> --- .../storage/data_stores/main/schema/delta/56/event_labels.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/storage/data_stores/main/schema/delta/56/event_labels.sql b/synapse/storage/data_stores/main/schema/delta/56/event_labels.sql index 765124d131..2acd8e1be5 100644 --- a/synapse/storage/data_stores/main/schema/delta/56/event_labels.sql +++ b/synapse/storage/data_stores/main/schema/delta/56/event_labels.sql @@ -17,7 +17,7 @@ CREATE TABLE IF NOT EXISTS event_labels ( event_id TEXT, label TEXT, room_id TEXT NOT NULL, - topological_ordering bigint NOT NULL, + topological_ordering BIGINT NOT NULL, PRIMARY KEY(event_id, label) ); From c6516adbe03a0acdd614ba6eb9d6f447dd4259e9 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 1 Nov 2019 16:19:09 +0000 Subject: [PATCH 0417/1623] Factor out an _AsyncEventContextImpl (#6298) The intention here is to make it clearer which fields we can expect to be populated when: notably, that the _event_type etc aren't used for the synchronous impl of EventContext. --- changelog.d/6298.misc | 1 + synapse/events/snapshot.py | 107 +++++++++++++-------------------- synapse/handlers/federation.py | 38 ++++++------ tests/test_federation.py | 4 +- 4 files changed, 65 insertions(+), 85 deletions(-) create mode 100644 changelog.d/6298.misc diff --git a/changelog.d/6298.misc b/changelog.d/6298.misc new file mode 100644 index 0000000000..d4190730b2 --- /dev/null +++ b/changelog.d/6298.misc @@ -0,0 +1 @@ +Refactor EventContext for clarity. \ No newline at end of file diff --git a/synapse/events/snapshot.py b/synapse/events/snapshot.py index 27cd8a63ff..a269de5482 100644 --- a/synapse/events/snapshot.py +++ b/synapse/events/snapshot.py @@ -37,9 +37,6 @@ class EventContext: delta_ids (dict[(str, str), str]): Delta from ``prev_group``. (type, state_key) -> event_id. ``None`` for an outlier. - prev_state_events (?): XXX: is this ever set to anything other than - the empty list? - app_service: FIXME _current_state_ids (dict[(str, str), str]|None): @@ -51,36 +48,16 @@ class EventContext: The current state map excluding the current event. None if outlier or we haven't fetched the state from DB yet. (type, state_key) -> event_id - - _fetching_state_deferred (Deferred|None): Resolves when *_state_ids have - been calculated. None if we haven't started calculating yet - - _event_type (str): The type of the event the context is associated with. - Only set when state has not been fetched yet. - - _event_state_key (str|None): The state_key of the event the context is - associated with. Only set when state has not been fetched yet. - - _prev_state_id (str|None): If the event associated with the context is - a state event, then `_prev_state_id` is the event_id of the state - that was replaced. - Only set when state has not been fetched yet. """ state_group = attr.ib(default=None) rejected = attr.ib(default=False) prev_group = attr.ib(default=None) delta_ids = attr.ib(default=None) - prev_state_events = attr.ib(default=attr.Factory(list)) app_service = attr.ib(default=None) - _current_state_ids = attr.ib(default=None) _prev_state_ids = attr.ib(default=None) - _prev_state_id = attr.ib(default=None) - - _event_type = attr.ib(default=None) - _event_state_key = attr.ib(default=None) - _fetching_state_deferred = attr.ib(default=None) + _current_state_ids = attr.ib(default=None) @staticmethod def with_state( @@ -90,7 +67,6 @@ class EventContext: current_state_ids=current_state_ids, prev_state_ids=prev_state_ids, state_group=state_group, - fetching_state_deferred=defer.succeed(None), prev_group=prev_group, delta_ids=delta_ids, ) @@ -125,7 +101,6 @@ class EventContext: "rejected": self.rejected, "prev_group": self.prev_group, "delta_ids": _encode_state_dict(self.delta_ids), - "prev_state_events": self.prev_state_events, "app_service_id": self.app_service.id if self.app_service else None, } @@ -141,7 +116,7 @@ class EventContext: Returns: EventContext """ - context = EventContext( + context = _AsyncEventContextImpl( # We use the state_group and prev_state_id stuff to pull the # current_state_ids out of the DB and construct prev_state_ids. prev_state_id=input["prev_state_id"], @@ -151,7 +126,6 @@ class EventContext: prev_group=input["prev_group"], delta_ids=_decode_state_dict(input["delta_ids"]), rejected=input["rejected"], - prev_state_events=input["prev_state_events"], ) app_service_id = input["app_service_id"] @@ -170,14 +144,7 @@ class EventContext: Maps a (type, state_key) to the event ID of the state event matching this tuple. """ - - if not self._fetching_state_deferred: - self._fetching_state_deferred = run_in_background( - self._fill_out_state, store - ) - - yield make_deferred_yieldable(self._fetching_state_deferred) - + yield self._ensure_fetched(store) return self._current_state_ids @defer.inlineCallbacks @@ -190,14 +157,7 @@ class EventContext: Maps a (type, state_key) to the event ID of the state event matching this tuple. """ - - if not self._fetching_state_deferred: - self._fetching_state_deferred = run_in_background( - self._fill_out_state, store - ) - - yield make_deferred_yieldable(self._fetching_state_deferred) - + yield self._ensure_fetched(store) return self._prev_state_ids def get_cached_current_state_ids(self): @@ -211,6 +171,44 @@ class EventContext: return self._current_state_ids + def _ensure_fetched(self, store): + return defer.succeed(None) + + +@attr.s(slots=True) +class _AsyncEventContextImpl(EventContext): + """ + An implementation of EventContext which fetches _current_state_ids and + _prev_state_ids from the database on demand. + + Attributes: + + _fetching_state_deferred (Deferred|None): Resolves when *_state_ids have + been calculated. None if we haven't started calculating yet + + _event_type (str): The type of the event the context is associated with. + + _event_state_key (str): The state_key of the event the context is + associated with. + + _prev_state_id (str|None): If the event associated with the context is + a state event, then `_prev_state_id` is the event_id of the state + that was replaced. + """ + + _prev_state_id = attr.ib(default=None) + _event_type = attr.ib(default=None) + _event_state_key = attr.ib(default=None) + _fetching_state_deferred = attr.ib(default=None) + + def _ensure_fetched(self, store): + if not self._fetching_state_deferred: + self._fetching_state_deferred = run_in_background( + self._fill_out_state, store + ) + + return make_deferred_yieldable(self._fetching_state_deferred) + @defer.inlineCallbacks def _fill_out_state(self, store): """Called to populate the _current_state_ids and _prev_state_ids @@ -228,27 +226,6 @@ class EventContext: else: self._prev_state_ids = self._current_state_ids - @defer.inlineCallbacks - def update_state( - self, state_group, prev_state_ids, current_state_ids, prev_group, delta_ids - ): - """Replace the state in the context - """ - - # We need to make sure we wait for any ongoing fetching of state - # to complete so that the updated state doesn't get clobbered - if self._fetching_state_deferred: - yield make_deferred_yieldable(self._fetching_state_deferred) - - self.state_group = state_group - self._prev_state_ids = prev_state_ids - self.prev_group = prev_group - self._current_state_ids = current_state_ids - self.delta_ids = delta_ids - - # We need to ensure that that we've marked as having fetched the state - self._fetching_state_deferred = defer.succeed(None) - def _encode_state_dict(state_dict): """Since dicts of (type, state_key) -> event_id cannot be serialized in diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index dab6be9573..8cafcfdab0 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -45,6 +45,7 @@ from synapse.api.errors import ( from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersions from synapse.crypto.event_signing import compute_event_signature from synapse.event_auth import auth_types_for_event +from synapse.events.snapshot import EventContext from synapse.events.validator import EventValidator from synapse.logging.context import ( make_deferred_yieldable, @@ -1871,14 +1872,7 @@ class FederationHandler(BaseHandler): if c and c.type == EventTypes.Create: auth_events[(c.type, c.state_key)] = c - try: - yield self.do_auth(origin, event, context, auth_events=auth_events) - except AuthError as e: - logger.warning( - "[%s %s] Rejecting: %s", event.room_id, event.event_id, e.msg - ) - - context.rejected = RejectedReason.AUTH_ERROR + context = yield self.do_auth(origin, event, context, auth_events=auth_events) if not context.rejected: yield self._check_for_soft_fail(event, state, backfilled) @@ -2047,12 +2041,12 @@ class FederationHandler(BaseHandler): Also NB that this function adds entries to it. Returns: - defer.Deferred[None] + defer.Deferred[EventContext]: updated context object """ room_version = yield self.store.get_room_version(event.room_id) try: - yield self._update_auth_events_and_context_for_auth( + context = yield self._update_auth_events_and_context_for_auth( origin, event, context, auth_events ) except Exception: @@ -2070,7 +2064,9 @@ class FederationHandler(BaseHandler): event_auth.check(room_version, event, auth_events=auth_events) except AuthError as e: logger.warning("Failed auth resolution for %r because %s", event, e) - raise e + context.rejected = RejectedReason.AUTH_ERROR + + return context @defer.inlineCallbacks def _update_auth_events_and_context_for_auth( @@ -2094,7 +2090,7 @@ class FederationHandler(BaseHandler): auth_events (dict[(str, str)->synapse.events.EventBase]): Returns: - defer.Deferred[None] + defer.Deferred[EventContext]: updated context """ event_auth_events = set(event.auth_event_ids()) @@ -2133,7 +2129,7 @@ class FederationHandler(BaseHandler): # The other side isn't around or doesn't implement the # endpoint, so lets just bail out. logger.info("Failed to get event auth from remote: %s", e) - return + return context seen_remotes = yield self.store.have_seen_events( [e.event_id for e in remote_auth_chain] @@ -2174,7 +2170,7 @@ class FederationHandler(BaseHandler): if event.internal_metadata.is_outlier(): logger.info("Skipping auth_event fetch for outlier") - return + return context # FIXME: Assumes we have and stored all the state for all the # prev_events @@ -2183,7 +2179,7 @@ class FederationHandler(BaseHandler): ) if not different_auth: - return + return context logger.info( "auth_events refers to events which are not in our calculated auth " @@ -2230,10 +2226,12 @@ class FederationHandler(BaseHandler): auth_events.update(new_state) - yield self._update_context_for_auth_events( + context = yield self._update_context_for_auth_events( event, context, auth_events, event_key ) + return context + @defer.inlineCallbacks def _update_context_for_auth_events(self, event, context, auth_events, event_key): """Update the state_ids in an event context after auth event resolution, @@ -2242,14 +2240,16 @@ class FederationHandler(BaseHandler): Args: event (Event): The event we're handling the context for - context (synapse.events.snapshot.EventContext): event context - to be updated + context (synapse.events.snapshot.EventContext): initial event context auth_events (dict[(str, str)->str]): Events to update in the event context. event_key ((str, str)): (type, state_key) for the current event. this will not be included in the current_state in the context. + + Returns: + Deferred[EventContext]: new event context """ state_updates = { k: a.event_id for k, a in iteritems(auth_events) if k != event_key @@ -2274,7 +2274,7 @@ class FederationHandler(BaseHandler): current_state_ids=current_state_ids, ) - yield context.update_state( + return EventContext.with_state( state_group=state_group, current_state_ids=current_state_ids, prev_state_ids=prev_state_ids, diff --git a/tests/test_federation.py b/tests/test_federation.py index d1acb16f30..7d82b58466 100644 --- a/tests/test_federation.py +++ b/tests/test_federation.py @@ -59,7 +59,9 @@ class MessageAcceptTests(unittest.TestCase): ) self.handler = self.homeserver.get_handlers().federation_handler - self.handler.do_auth = lambda *a, **b: succeed(True) + self.handler.do_auth = lambda origin, event, context, auth_events: succeed( + context + ) self.client = self.homeserver.get_federation_client() self.client._check_sigs_and_hash_and_fetch = lambda dest, pdus, **k: succeed( pdus From 988d8d6507a0e8b34f2c352c77b5742197762190 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 1 Nov 2019 16:22:44 +0000 Subject: [PATCH 0418/1623] Incorporate review --- synapse/api/constants.py | 2 +- synapse/api/filtering.py | 2 +- synapse/storage/data_stores/main/events.py | 2 +- .../data_stores/main/schema/delta/56/event_labels.sql | 6 ++++++ tests/api/test_filtering.py | 8 ++++---- tests/rest/client/v1/test_rooms.py | 8 ++++---- tests/rest/client/v2_alpha/test_sync.py | 8 ++++---- 7 files changed, 21 insertions(+), 15 deletions(-) diff --git a/synapse/api/constants.py b/synapse/api/constants.py index 066ce18704..49c4b85054 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -144,4 +144,4 @@ class EventContentFields(object): """Fields found in events' content, regardless of type.""" # Labels for the event, cf https://github.com/matrix-org/matrix-doc/pull/2326 - Labels = "org.matrix.labels" + LABELS = "org.matrix.labels" diff --git a/synapse/api/filtering.py b/synapse/api/filtering.py index 30a7ee0a7a..bec13f08d8 100644 --- a/synapse/api/filtering.py +++ b/synapse/api/filtering.py @@ -309,7 +309,7 @@ class Filter(object): content = event.get("content", {}) # check if there is a string url field in the content for filtering purposes contains_url = isinstance(content.get("url"), text_type) - labels = content.get(EventContentFields.Labels, []) + labels = content.get(EventContentFields.LABELS, []) return self.check_fields(room_id, sender, ev_type, labels, contains_url) diff --git a/synapse/storage/data_stores/main/events.py b/synapse/storage/data_stores/main/events.py index 577e79bcf9..1045c7fa2e 100644 --- a/synapse/storage/data_stores/main/events.py +++ b/synapse/storage/data_stores/main/events.py @@ -1491,7 +1491,7 @@ class EventsStore( self._handle_event_relations(txn, event) # Store the labels for this event. - labels = event.content.get(EventContentFields.Labels) + labels = event.content.get(EventContentFields.LABELS) if labels: self.insert_labels_for_event_txn( txn, event.event_id, labels, event.room_id, event.depth diff --git a/synapse/storage/data_stores/main/schema/delta/56/event_labels.sql b/synapse/storage/data_stores/main/schema/delta/56/event_labels.sql index 2acd8e1be5..5e29c1da19 100644 --- a/synapse/storage/data_stores/main/schema/delta/56/event_labels.sql +++ b/synapse/storage/data_stores/main/schema/delta/56/event_labels.sql @@ -13,6 +13,8 @@ * limitations under the License. */ +-- room_id and topoligical_ordering are denormalised from the events table in order to +-- make the index work. CREATE TABLE IF NOT EXISTS event_labels ( event_id TEXT, label TEXT, @@ -21,4 +23,8 @@ CREATE TABLE IF NOT EXISTS event_labels ( PRIMARY KEY(event_id, label) ); + +-- This index enables an event pagination looking for a particular label to index the +-- event_labels table first, which is much quicker than scanning the events table and then +-- filtering by label, if the label is rarely used relative to the size of the room. CREATE INDEX event_labels_room_id_label_idx ON event_labels(room_id, label, topological_ordering); diff --git a/tests/api/test_filtering.py b/tests/api/test_filtering.py index 8ec48c4154..2dc5052249 100644 --- a/tests/api/test_filtering.py +++ b/tests/api/test_filtering.py @@ -329,7 +329,7 @@ class FilteringTestCase(unittest.TestCase): sender="@foo:bar", type="m.room.message", room_id="!secretbase:unknown", - content={EventContentFields.Labels: ["#fun"]}, + content={EventContentFields.LABELS: ["#fun"]}, ) self.assertTrue(Filter(definition).check(event)) @@ -338,7 +338,7 @@ class FilteringTestCase(unittest.TestCase): sender="@foo:bar", type="m.room.message", room_id="!secretbase:unknown", - content={EventContentFields.Labels: ["#notfun"]}, + content={EventContentFields.LABELS: ["#notfun"]}, ) self.assertFalse(Filter(definition).check(event)) @@ -349,7 +349,7 @@ class FilteringTestCase(unittest.TestCase): sender="@foo:bar", type="m.room.message", room_id="!secretbase:unknown", - content={EventContentFields.Labels: ["#fun"]}, + content={EventContentFields.LABELS: ["#fun"]}, ) self.assertFalse(Filter(definition).check(event)) @@ -358,7 +358,7 @@ class FilteringTestCase(unittest.TestCase): sender="@foo:bar", type="m.room.message", room_id="!secretbase:unknown", - content={EventContentFields.Labels: ["#notfun"]}, + content={EventContentFields.LABELS: ["#notfun"]}, ) self.assertTrue(Filter(definition).check(event)) diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py index 0dc0faa0e5..5e38fd6ced 100644 --- a/tests/rest/client/v1/test_rooms.py +++ b/tests/rest/client/v1/test_rooms.py @@ -860,7 +860,7 @@ class RoomMessageListTestCase(RoomBase): content={ "msgtype": "m.text", "body": "with right label", - EventContentFields.Labels: ["#fun"], + EventContentFields.LABELS: ["#fun"], }, ) @@ -876,7 +876,7 @@ class RoomMessageListTestCase(RoomBase): content={ "msgtype": "m.text", "body": "with wrong label", - EventContentFields.Labels: ["#work"], + EventContentFields.LABELS: ["#work"], }, ) @@ -886,7 +886,7 @@ class RoomMessageListTestCase(RoomBase): content={ "msgtype": "m.text", "body": "with two wrong labels", - EventContentFields.Labels: ["#work", "#notfun"], + EventContentFields.LABELS: ["#work", "#notfun"], }, ) @@ -896,7 +896,7 @@ class RoomMessageListTestCase(RoomBase): content={ "msgtype": "m.text", "body": "with right label", - EventContentFields.Labels: ["#fun"], + EventContentFields.LABELS: ["#fun"], }, ) diff --git a/tests/rest/client/v2_alpha/test_sync.py b/tests/rest/client/v2_alpha/test_sync.py index c3c6f75ced..3283c0e47b 100644 --- a/tests/rest/client/v2_alpha/test_sync.py +++ b/tests/rest/client/v2_alpha/test_sync.py @@ -157,7 +157,7 @@ class SyncFilterTestCase(unittest.HomeserverTestCase): content={ "msgtype": "m.text", "body": "with right label", - EventContentFields.Labels: ["#fun"], + EventContentFields.LABELS: ["#fun"], }, tok=tok, ) @@ -175,7 +175,7 @@ class SyncFilterTestCase(unittest.HomeserverTestCase): content={ "msgtype": "m.text", "body": "with wrong label", - EventContentFields.Labels: ["#work"], + EventContentFields.LABELS: ["#work"], }, tok=tok, ) @@ -186,7 +186,7 @@ class SyncFilterTestCase(unittest.HomeserverTestCase): content={ "msgtype": "m.text", "body": "with two wrong labels", - EventContentFields.Labels: ["#work", "#notfun"], + EventContentFields.LABELS: ["#work", "#notfun"], }, tok=tok, ) @@ -197,7 +197,7 @@ class SyncFilterTestCase(unittest.HomeserverTestCase): content={ "msgtype": "m.text", "body": "with right label", - EventContentFields.Labels: ["#fun"], + EventContentFields.LABELS: ["#fun"], }, tok=tok, ) From c9a1b80a74863b98b5dce2954b8486a068a0151f Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 31 Oct 2019 14:41:28 +0000 Subject: [PATCH 0419/1623] MSC2326: Add background update to take previous events into account --- .../data_stores/main/events_bg_updates.py | 55 +++++++++++++++++++ .../56/event_labels_background_update.sql | 17 ++++++ 2 files changed, 72 insertions(+) create mode 100644 synapse/storage/data_stores/main/schema/delta/56/event_labels_background_update.sql diff --git a/synapse/storage/data_stores/main/events_bg_updates.py b/synapse/storage/data_stores/main/events_bg_updates.py index 51352b9966..e702fca149 100644 --- a/synapse/storage/data_stores/main/events_bg_updates.py +++ b/synapse/storage/data_stores/main/events_bg_updates.py @@ -21,6 +21,7 @@ from canonicaljson import json from twisted.internet import defer +from synapse.api.constants import LabelsField from synapse.storage._base import make_in_list_sql_clause from synapse.storage.background_updates import BackgroundUpdateStore @@ -85,6 +86,10 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): "event_fix_redactions_bytes", self._event_fix_redactions_bytes ) + self.register_background_update_handler( + "event_store_labels", self._event_store_labels + ) + @defer.inlineCallbacks def _background_reindex_fields_sender(self, progress, batch_size): target_min_stream_id = progress["target_min_stream_id_inclusive"] @@ -503,3 +508,53 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): yield self._end_background_update("event_fix_redactions_bytes") return 1 + + @defer.inlineCallbacks + def _event_store_labels(self, progress, batch_size): + """Stores labels for events.""" + last_event_id = progress.get("last_event_id", "") + + def _event_store_labels_txn(txn): + txn.execute( + """ + SELECT event_id, json FROM event_json + WHERE event_id > ? + LIMIT ? + """, + (last_event_id, batch_size) + ) + + rows = txn.fetchall() + if not rows: + return True + + for row in rows: + event_id = row["event_id"] + event_json = json.loads(row["json"]) + + self._simple_insert_many_txn( + txn=txn, + table="event_labels", + values=[ + { + "event_id": event_id, + "label": label, + } + for label in event_json["content"].get(LabelsField) + ] + ) + + self._background_update_progress_txn( + txn, "event_store_labels", {"last_event_id": event_id} + ) + + return len(rows) == batch_size + + end = yield self.runInteraction( + desc="event_store_labels", func=_event_store_labels_txn + ) + + if end: + yield self._end_background_update("event_store_labels") + + return batch_size diff --git a/synapse/storage/data_stores/main/schema/delta/56/event_labels_background_update.sql b/synapse/storage/data_stores/main/schema/delta/56/event_labels_background_update.sql new file mode 100644 index 0000000000..5f5e0499ae --- /dev/null +++ b/synapse/storage/data_stores/main/schema/delta/56/event_labels_background_update.sql @@ -0,0 +1,17 @@ +/* Copyright 2019 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +INSERT INTO background_updates (update_name, progress_json) VALUES + ('event_store_labels', '{}'); From a46574281d90d45e4a5f3ecede3261d17726719e Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 31 Oct 2019 14:46:16 +0000 Subject: [PATCH 0420/1623] Use the right format for rows --- synapse/storage/data_stores/main/events_bg_updates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/storage/data_stores/main/events_bg_updates.py b/synapse/storage/data_stores/main/events_bg_updates.py index e702fca149..013242b04e 100644 --- a/synapse/storage/data_stores/main/events_bg_updates.py +++ b/synapse/storage/data_stores/main/events_bg_updates.py @@ -524,7 +524,7 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): (last_event_id, batch_size) ) - rows = txn.fetchall() + rows = self.cursor_to_dict(txn) if not rows: return True From 07cb38e965a29624b3cc8a07d4c86f61dbe7b72a Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 31 Oct 2019 14:47:09 +0000 Subject: [PATCH 0421/1623] Use a sensible default value for labels --- synapse/storage/data_stores/main/events_bg_updates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/storage/data_stores/main/events_bg_updates.py b/synapse/storage/data_stores/main/events_bg_updates.py index 013242b04e..448048e11d 100644 --- a/synapse/storage/data_stores/main/events_bg_updates.py +++ b/synapse/storage/data_stores/main/events_bg_updates.py @@ -540,7 +540,7 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): "event_id": event_id, "label": label, } - for label in event_json["content"].get(LabelsField) + for label in event_json["content"].get(LabelsField, []) ] ) From 911b03ca312160bd8f80f010da4eaf63d7f602da Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 31 Oct 2019 14:49:41 +0000 Subject: [PATCH 0422/1623] Don't try to process events we already have a label for --- synapse/storage/data_stores/main/events_bg_updates.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/synapse/storage/data_stores/main/events_bg_updates.py b/synapse/storage/data_stores/main/events_bg_updates.py index 448048e11d..c8168f6926 100644 --- a/synapse/storage/data_stores/main/events_bg_updates.py +++ b/synapse/storage/data_stores/main/events_bg_updates.py @@ -518,7 +518,8 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): txn.execute( """ SELECT event_id, json FROM event_json - WHERE event_id > ? + LEFT JOIN event_labels USING (event_id) + WHERE event_id > ? AND label IS NULL LIMIT ? """, (last_event_id, batch_size) From 416c7baee64e0d7145c2447c51326af1c901a176 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 31 Oct 2019 15:00:40 +0000 Subject: [PATCH 0423/1623] Changelog --- changelog.d/6310.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6310.feature diff --git a/changelog.d/6310.feature b/changelog.d/6310.feature new file mode 100644 index 0000000000..b7ff3fad3b --- /dev/null +++ b/changelog.d/6310.feature @@ -0,0 +1 @@ +Implement label-based filtering. From 1c1268245d501e3edefbe3a4eb40c238124bb2e6 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 31 Oct 2019 15:01:29 +0000 Subject: [PATCH 0424/1623] Lint --- synapse/storage/data_stores/main/events_bg_updates.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/synapse/storage/data_stores/main/events_bg_updates.py b/synapse/storage/data_stores/main/events_bg_updates.py index c8168f6926..10ebc6d865 100644 --- a/synapse/storage/data_stores/main/events_bg_updates.py +++ b/synapse/storage/data_stores/main/events_bg_updates.py @@ -522,7 +522,7 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): WHERE event_id > ? AND label IS NULL LIMIT ? """, - (last_event_id, batch_size) + (last_event_id, batch_size), ) rows = self.cursor_to_dict(txn) @@ -537,12 +537,9 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): txn=txn, table="event_labels", values=[ - { - "event_id": event_id, - "label": label, - } + {"event_id": event_id, "label": label} for label in event_json["content"].get(LabelsField, []) - ] + ], ) self._background_update_progress_txn( From 1586f2c7e716fb066aa5551bdce654be8e35b01d Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 31 Oct 2019 23:01:26 +0000 Subject: [PATCH 0425/1623] Fix exit condition --- synapse/storage/data_stores/main/events_bg_updates.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/synapse/storage/data_stores/main/events_bg_updates.py b/synapse/storage/data_stores/main/events_bg_updates.py index 10ebc6d865..72d600c51b 100644 --- a/synapse/storage/data_stores/main/events_bg_updates.py +++ b/synapse/storage/data_stores/main/events_bg_updates.py @@ -546,7 +546,9 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): txn, "event_store_labels", {"last_event_id": event_id} ) - return len(rows) == batch_size + # We want to return true (to end the background update) only when + # the query returned with less rows than we asked for. + return len(rows) != batch_size end = yield self.runInteraction( desc="event_store_labels", func=_event_store_labels_txn From 49008e674f6c5463168e970349ce84c488407834 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 31 Oct 2019 23:12:15 +0000 Subject: [PATCH 0426/1623] TODO --- synapse/storage/data_stores/main/events_bg_updates.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/synapse/storage/data_stores/main/events_bg_updates.py b/synapse/storage/data_stores/main/events_bg_updates.py index 72d600c51b..b7b390485b 100644 --- a/synapse/storage/data_stores/main/events_bg_updates.py +++ b/synapse/storage/data_stores/main/events_bg_updates.py @@ -548,6 +548,8 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): # We want to return true (to end the background update) only when # the query returned with less rows than we asked for. + # TODO: this currently doesn't work, i.e. it only runs once whereas + # its opposite does the whole thing, investigate. return len(rows) != batch_size end = yield self.runInteraction( From 824bba2f78f86a9364a87c6ce79a40bff0f73cbf Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 1 Nov 2019 12:03:04 +0000 Subject: [PATCH 0427/1623] Correctly order results --- synapse/storage/data_stores/main/events_bg_updates.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/synapse/storage/data_stores/main/events_bg_updates.py b/synapse/storage/data_stores/main/events_bg_updates.py index b7b390485b..5ba1cff468 100644 --- a/synapse/storage/data_stores/main/events_bg_updates.py +++ b/synapse/storage/data_stores/main/events_bg_updates.py @@ -21,7 +21,7 @@ from canonicaljson import json from twisted.internet import defer -from synapse.api.constants import LabelsField +from synapse.api.constants import EventContentFields from synapse.storage._base import make_in_list_sql_clause from synapse.storage.background_updates import BackgroundUpdateStore @@ -520,7 +520,7 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): SELECT event_id, json FROM event_json LEFT JOIN event_labels USING (event_id) WHERE event_id > ? AND label IS NULL - LIMIT ? + ORDER BY event_id LIMIT ? """, (last_event_id, batch_size), ) @@ -538,7 +538,9 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): table="event_labels", values=[ {"event_id": event_id, "label": label} - for label in event_json["content"].get(LabelsField, []) + for label in event_json["content"].get( + EventContentFields.Labels, [] + ) ], ) @@ -548,8 +550,6 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): # We want to return true (to end the background update) only when # the query returned with less rows than we asked for. - # TODO: this currently doesn't work, i.e. it only runs once whereas - # its opposite does the whole thing, investigate. return len(rows) != batch_size end = yield self.runInteraction( From 3b29a73f9fc18912a6b775a083d9449d426fa790 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 1 Nov 2019 12:07:05 +0000 Subject: [PATCH 0428/1623] Print out the actual number of affected rows --- synapse/storage/data_stores/main/events_bg_updates.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/synapse/storage/data_stores/main/events_bg_updates.py b/synapse/storage/data_stores/main/events_bg_updates.py index 5ba1cff468..a703927471 100644 --- a/synapse/storage/data_stores/main/events_bg_updates.py +++ b/synapse/storage/data_stores/main/events_bg_updates.py @@ -527,7 +527,7 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): rows = self.cursor_to_dict(txn) if not rows: - return True + return True, 0 for row in rows: event_id = row["event_id"] @@ -550,13 +550,13 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): # We want to return true (to end the background update) only when # the query returned with less rows than we asked for. - return len(rows) != batch_size + return len(rows) != batch_size, len(rows) - end = yield self.runInteraction( + end, num_rows = yield self.runInteraction( desc="event_store_labels", func=_event_store_labels_txn ) if end: yield self._end_background_update("event_store_labels") - return batch_size + return num_rows From cc6243b4c08bfae77c9ff29d23c40568ab284924 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 4 Nov 2019 12:40:18 +0000 Subject: [PATCH 0429/1623] document the REPLICATE command a bit better (#6305) since I found myself wonder how it works --- changelog.d/6305.misc | 1 + docs/tcp_replication.md | 15 ++++- synapse/replication/slave/storage/_base.py | 10 ++- synapse/replication/tcp/client.py | 20 ++++-- synapse/replication/tcp/protocol.py | 74 +++++++++++++++++++++- 5 files changed, 110 insertions(+), 10 deletions(-) create mode 100644 changelog.d/6305.misc diff --git a/changelog.d/6305.misc b/changelog.d/6305.misc new file mode 100644 index 0000000000..f047fc3062 --- /dev/null +++ b/changelog.d/6305.misc @@ -0,0 +1 @@ +Add some documentation about worker replication. diff --git a/docs/tcp_replication.md b/docs/tcp_replication.md index e099d8a87b..ba9e874d07 100644 --- a/docs/tcp_replication.md +++ b/docs/tcp_replication.md @@ -199,7 +199,20 @@ client (C): #### REPLICATE (C) - Asks the server to replicate a given stream +Asks the server to replicate a given stream. The syntax is: + +``` + REPLICATE +``` + +Where `` may be either: + * a numeric stream_id to stream updates since (exclusive) + * `NOW` to stream all subsequent updates. + +The `` is the name of a replication stream to subscribe +to (see [here](../synapse/replication/tcp/streams/_base.py) for a list +of streams). It can also be `ALL` to subscribe to all known streams, +in which case the `` must be set to `NOW`. #### USER_SYNC (C) diff --git a/synapse/replication/slave/storage/_base.py b/synapse/replication/slave/storage/_base.py index 182cb2a1d8..456bc005a0 100644 --- a/synapse/replication/slave/storage/_base.py +++ b/synapse/replication/slave/storage/_base.py @@ -14,6 +14,7 @@ # limitations under the License. import logging +from typing import Dict import six @@ -44,7 +45,14 @@ class BaseSlavedStore(SQLBaseStore): self.hs = hs - def stream_positions(self): + def stream_positions(self) -> Dict[str, int]: + """ + Get the current positions of all the streams this store wants to subscribe to + + Returns: + map from stream name to the most recent update we have for + that stream (ie, the point we want to start replicating from) + """ pos = {} if self._cache_id_gen: pos["caches"] = self._cache_id_gen.get_current_token() diff --git a/synapse/replication/tcp/client.py b/synapse/replication/tcp/client.py index 563ce0fc53..fead78388c 100644 --- a/synapse/replication/tcp/client.py +++ b/synapse/replication/tcp/client.py @@ -16,10 +16,17 @@ """ import logging +from typing import Dict from twisted.internet import defer from twisted.internet.protocol import ReconnectingClientFactory +from synapse.replication.slave.storage._base import BaseSlavedStore +from synapse.replication.tcp.protocol import ( + AbstractReplicationClientHandler, + ClientReplicationStreamProtocol, +) + from .commands import ( FederationAckCommand, InvalidateCacheCommand, @@ -27,7 +34,6 @@ from .commands import ( UserIpCommand, UserSyncCommand, ) -from .protocol import ClientReplicationStreamProtocol logger = logging.getLogger(__name__) @@ -42,7 +48,7 @@ class ReplicationClientFactory(ReconnectingClientFactory): maxDelay = 30 # Try at least once every N seconds - def __init__(self, hs, client_name, handler): + def __init__(self, hs, client_name, handler: AbstractReplicationClientHandler): self.client_name = client_name self.handler = handler self.server_name = hs.config.server_name @@ -68,13 +74,13 @@ class ReplicationClientFactory(ReconnectingClientFactory): ReconnectingClientFactory.clientConnectionFailed(self, connector, reason) -class ReplicationClientHandler(object): +class ReplicationClientHandler(AbstractReplicationClientHandler): """A base handler that can be passed to the ReplicationClientFactory. By default proxies incoming replication data to the SlaveStore. """ - def __init__(self, store): + def __init__(self, store: BaseSlavedStore): self.store = store # The current connection. None if we are currently (re)connecting @@ -138,11 +144,13 @@ class ReplicationClientHandler(object): if d: d.callback(data) - def get_streams_to_replicate(self): + def get_streams_to_replicate(self) -> Dict[str, int]: """Called when a new connection has been established and we need to subscribe to streams. - Returns a dictionary of stream name to token. + Returns: + map from stream name to the most recent update we have for + that stream (ie, the point we want to start replicating from) """ args = self.store.stream_positions() user_account_data = args.pop("user_account_data", None) diff --git a/synapse/replication/tcp/protocol.py b/synapse/replication/tcp/protocol.py index b64f3f44b5..afaf002fe6 100644 --- a/synapse/replication/tcp/protocol.py +++ b/synapse/replication/tcp/protocol.py @@ -48,7 +48,7 @@ indicate which side is sending, these are *not* included on the wire:: > ERROR server stopping * connection closed by server * """ - +import abc import fcntl import logging import struct @@ -65,6 +65,7 @@ from twisted.python.failure import Failure from synapse.logging.context import make_deferred_yieldable, run_in_background from synapse.metrics import LaterGauge from synapse.metrics.background_process_metrics import run_as_background_process +from synapse.util import Clock from synapse.util.stringutils import random_string from .commands import ( @@ -558,11 +559,80 @@ class ServerReplicationStreamProtocol(BaseReplicationStreamProtocol): self.streamer.lost_connection(self) +class AbstractReplicationClientHandler(metaclass=abc.ABCMeta): + """ + The interface for the handler that should be passed to + ClientReplicationStreamProtocol + """ + + @abc.abstractmethod + def on_rdata(self, stream_name, token, rows): + """Called to handle a batch of replication data with a given stream token. + + Args: + stream_name (str): name of the replication stream for this batch of rows + token (int): stream token for this batch of rows + rows (list): a list of Stream.ROW_TYPE objects as returned by + Stream.parse_row. + + Returns: + Deferred|None + """ + raise NotImplementedError() + + @abc.abstractmethod + def on_position(self, stream_name, token): + """Called when we get new position data.""" + raise NotImplementedError() + + @abc.abstractmethod + def on_sync(self, data): + """Called when get a new SYNC command.""" + raise NotImplementedError() + + @abc.abstractmethod + def get_streams_to_replicate(self): + """Called when a new connection has been established and we need to + subscribe to streams. + + Returns: + map from stream name to the most recent update we have for + that stream (ie, the point we want to start replicating from) + """ + raise NotImplementedError() + + @abc.abstractmethod + def get_currently_syncing_users(self): + """Get the list of currently syncing users (if any). This is called + when a connection has been established and we need to send the + currently syncing users.""" + raise NotImplementedError() + + @abc.abstractmethod + def update_connection(self, connection): + """Called when a connection has been established (or lost with None). + """ + raise NotImplementedError() + + @abc.abstractmethod + def finished_connecting(self): + """Called when we have successfully subscribed and caught up to all + streams we're interested in. + """ + raise NotImplementedError() + + class ClientReplicationStreamProtocol(BaseReplicationStreamProtocol): VALID_INBOUND_COMMANDS = VALID_SERVER_COMMANDS VALID_OUTBOUND_COMMANDS = VALID_CLIENT_COMMANDS - def __init__(self, client_name, server_name, clock, handler): + def __init__( + self, + client_name: str, + server_name: str, + clock: Clock, + handler: AbstractReplicationClientHandler, + ): BaseReplicationStreamProtocol.__init__(self, clock) self.client_name = client_name From 7134ca7daa7ead8104e3c51ba4bc730d99d098e3 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 4 Nov 2019 13:36:57 +0000 Subject: [PATCH 0430/1623] Change to not require a state_groups.room_id index. This does mean that we won't clean up orphaned state groups (i.e. state groups that were persisted but the associated event wasn't). --- synapse/storage/data_stores/main/events.py | 70 ++++++++++++------- .../schema/delta/56/state_group_room_idx.sql | 17 ----- synapse/storage/data_stores/main/state.py | 7 -- synapse/storage/purge_events.py | 4 +- 4 files changed, 45 insertions(+), 53 deletions(-) delete mode 100644 synapse/storage/data_stores/main/schema/delta/56/state_group_room_idx.sql diff --git a/synapse/storage/data_stores/main/events.py b/synapse/storage/data_stores/main/events.py index 68f27078c4..3049a21dc5 100644 --- a/synapse/storage/data_stores/main/events.py +++ b/synapse/storage/data_stores/main/events.py @@ -1624,7 +1624,10 @@ class EventsStore( """Deletes all record of a room Args: - room_id (str): + room_id (str) + + Returns: + Deferred[List[int]]: The list of state groups to delete. """ return self.runInteraction("purge_room", self._purge_room_txn, room_id) @@ -1714,10 +1717,24 @@ class EventsStore( # index on them. In any case we should be clearing out 'stream' tables # periodically anyway (#5888) + # Now we fetch all the state groups that should be deleted. + txn.execute( + """ + SELECT DISTINCT state_group FROM events + INNER JOIN event_to_state_groups USING(event_id) + WHERE events.room_id = ? + """, + (room_id,), + ) + + state_groups = [row[0] for row in txn] + # TODO: we could probably usefully do a bunch of cache invalidation here logger.info("[purge] done") + return state_groups + def purge_unreferenced_state_groups( self, room_id: str, state_groups_to_delete: Set[int] ) -> defer.Deferred: @@ -1825,54 +1842,53 @@ class EventsStore( return {row["state_group"]: row["prev_state_group"] for row in rows} - def purge_room_state(self, room_id): + def purge_room_state(self, room_id, state_groups_to_delete): """Deletes all record of a room from state tables Args: room_id (str): + state_groups_to_delete (list[int]): State groups to delete """ return self.runInteraction( - "purge_room_state", self._purge_room_state_txn, room_id + "purge_room_state", + self._purge_room_state_txn, + room_id, + state_groups_to_delete, ) - def _purge_room_state_txn(self, txn, room_id): + def _purge_room_state_txn(self, txn, room_id, state_groups_to_delete): # first we have to delete the state groups states logger.info("[purge] removing %s from state_groups_state", room_id) - txn.execute( - """ - DELETE FROM state_groups_state - WHERE state_group IN ( - SELECT state_group FROM state_groups - WHERE room_id = ? - ) - """, - (room_id,), + self._simple_delete_many_txn( + txn, + table="state_groups_state", + column="state_group", + iterable=state_groups_to_delete, + keyvalues={}, ) # ... and the state group edges logger.info("[purge] removing %s from state_group_edges", room_id) - txn.execute( - """ - DELETE FROM state_group_edges - WHERE state_group IN ( - SELECT state_group FROM state_groups - WHERE room_id = ? - ) - """, - (room_id,), + self._simple_delete_many_txn( + txn, + table="state_group_edges", + column="state_group", + iterable=state_groups_to_delete, + keyvalues={}, ) # ... and the state groups logger.info("[purge] removing %s from state_groups", room_id) - txn.execute( - """ - DELETE FROM state_groups WHERE room_id = ? - """, - (room_id,), + self._simple_delete_many_txn( + txn, + table="state_groups", + column="id", + iterable=state_groups_to_delete, + keyvalues={}, ) async def is_event_after(self, event_id1, event_id2): diff --git a/synapse/storage/data_stores/main/schema/delta/56/state_group_room_idx.sql b/synapse/storage/data_stores/main/schema/delta/56/state_group_room_idx.sql deleted file mode 100644 index 7916ef18b2..0000000000 --- a/synapse/storage/data_stores/main/schema/delta/56/state_group_room_idx.sql +++ /dev/null @@ -1,17 +0,0 @@ -/* Copyright 2019 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -INSERT INTO background_updates (update_name, progress_json) VALUES - ('state_groups_room_id_idx', '{}'); diff --git a/synapse/storage/data_stores/main/state.py b/synapse/storage/data_stores/main/state.py index e1d3041c7c..5c5b15840e 100644 --- a/synapse/storage/data_stores/main/state.py +++ b/synapse/storage/data_stores/main/state.py @@ -1023,7 +1023,6 @@ class StateBackgroundUpdateStore( STATE_GROUP_INDEX_UPDATE_NAME = "state_group_state_type_index" CURRENT_STATE_INDEX_UPDATE_NAME = "current_state_members_idx" EVENT_STATE_GROUP_INDEX_UPDATE_NAME = "event_to_state_groups_sg_index" - STATE_GROUPS_ROOM_INDEX_UPDATE_NAME = "state_groups_room_id_idx" def __init__(self, db_conn, hs): super(StateBackgroundUpdateStore, self).__init__(db_conn, hs) @@ -1047,12 +1046,6 @@ class StateBackgroundUpdateStore( table="event_to_state_groups", columns=["state_group"], ) - self.register_background_index_update( - self.STATE_GROUPS_ROOM_INDEX_UPDATE_NAME, - index_name="state_groups_room_id_idx", - table="state_groups", - columns=["room_id"], - ) @defer.inlineCallbacks def _background_deduplicate_state(self, progress, batch_size): diff --git a/synapse/storage/purge_events.py b/synapse/storage/purge_events.py index dd45df0c88..a368182034 100644 --- a/synapse/storage/purge_events.py +++ b/synapse/storage/purge_events.py @@ -33,8 +33,8 @@ class PurgeEventsStorage(object): """Deletes all record of a room """ - yield self.stores.main.purge_room(room_id) - yield self.stores.main.purge_room_state(room_id) + state_groups_to_delete = yield self.stores.main.purge_room(room_id) + yield self.stores.main.purge_room_state(room_id, state_groups_to_delete) @defer.inlineCallbacks def purge_history(self, room_id, token, delete_local_events): From 09957ce0e4dcfd84c2de4039653059faae03065b Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Mon, 4 Nov 2019 17:09:22 +0000 Subject: [PATCH 0431/1623] Implement per-room message retention policies --- changelog.d/5815.feature | 1 + docs/sample_config.yaml | 63 ++++ synapse/api/constants.py | 2 + synapse/config/server.py | 172 ++++++++++ synapse/events/validator.py | 100 +++++- synapse/handlers/federation.py | 2 +- synapse/handlers/message.py | 4 +- synapse/handlers/pagination.py | 111 ++++++ synapse/storage/data_stores/main/events.py | 3 + synapse/storage/data_stores/main/room.py | 252 ++++++++++++++ .../main/schema/delta/56/room_retention.sql | 33 ++ synapse/visibility.py | 17 + tests/rest/client/test_retention.py | 320 ++++++++++++++++++ 13 files changed, 1074 insertions(+), 6 deletions(-) create mode 100644 changelog.d/5815.feature create mode 100644 synapse/storage/data_stores/main/schema/delta/56/room_retention.sql create mode 100644 tests/rest/client/test_retention.py diff --git a/changelog.d/5815.feature b/changelog.d/5815.feature new file mode 100644 index 0000000000..ca4df4e7f6 --- /dev/null +++ b/changelog.d/5815.feature @@ -0,0 +1 @@ +Implement per-room message retention policies. diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index d2f4aff826..87fba27d13 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -328,6 +328,69 @@ listeners: # #user_ips_max_age: 14d +# Message retention policy at the server level. +# +# Room admins and mods can define a retention period for their rooms using the +# 'm.room.retention' state event, and server admins can cap this period by setting +# the 'allowed_lifetime_min' and 'allowed_lifetime_max' config options. +# +# If this feature is enabled, Synapse will regularly look for and purge events +# which are older than the room's maximum retention period. Synapse will also +# filter events received over federation so that events that should have been +# purged are ignored and not stored again. +# +retention: + # The message retention policies feature is disabled by default. Uncomment the + # following line to enable it. + # + #enabled: true + + # Default retention policy. If set, Synapse will apply it to rooms that lack the + # 'm.room.retention' state event. Currently, the value of 'min_lifetime' doesn't + # matter much because Synapse doesn't take it into account yet. + # + #default_policy: + # min_lifetime: 1d + # max_lifetime: 1y + + # Retention policy limits. If set, a user won't be able to send a + # 'm.room.retention' event which features a 'min_lifetime' or a 'max_lifetime' + # that's not within this range. This is especially useful in closed federations, + # in which server admins can make sure every federating server applies the same + # rules. + # + #allowed_lifetime_min: 1d + #allowed_lifetime_max: 1y + + # Server admins can define the settings of the background jobs purging the + # events which lifetime has expired under the 'purge_jobs' section. + # + # If no configuration is provided, a single job will be set up to delete expired + # events in every room daily. + # + # Each job's configuration defines which range of message lifetimes the job + # takes care of. For example, if 'shortest_max_lifetime' is '2d' and + # 'longest_max_lifetime' is '3d', the job will handle purging expired events in + # rooms whose state defines a 'max_lifetime' that's both higher than 2 days, and + # lower than or equal to 3 days. Both the minimum and the maximum value of a + # range are optional, e.g. a job with no 'shortest_max_lifetime' and a + # 'longest_max_lifetime' of '3d' will handle every room with a retention policy + # which 'max_lifetime' is lower than or equal to three days. + # + # The rationale for this per-job configuration is that some rooms might have a + # retention policy with a low 'max_lifetime', where history needs to be purged + # of outdated messages on a very frequent basis (e.g. every 5min), but not want + # that purge to be performed by a job that's iterating over every room it knows, + # which would be quite heavy on the server. + # + #purge_jobs: + # - shortest_max_lifetime: 1d + # longest_max_lifetime: 3d + # interval: 5m: + # - shortest_max_lifetime: 3d + # longest_max_lifetime: 1y + # interval: 24h + ## TLS ## diff --git a/synapse/api/constants.py b/synapse/api/constants.py index 49c4b85054..e3f086f1c3 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -94,6 +94,8 @@ class EventTypes(object): ServerACL = "m.room.server_acl" Pinned = "m.room.pinned_events" + Retention = "m.room.retention" + class RejectedReason(object): AUTH_ERROR = "auth_error" diff --git a/synapse/config/server.py b/synapse/config/server.py index d556df308d..aa93a416f1 100644 --- a/synapse/config/server.py +++ b/synapse/config/server.py @@ -246,6 +246,115 @@ class ServerConfig(Config): # events with profile information that differ from the target's global profile. self.allow_per_room_profiles = config.get("allow_per_room_profiles", True) + retention_config = config.get("retention") + if retention_config is None: + retention_config = {} + + self.retention_enabled = retention_config.get("enabled", False) + + retention_default_policy = retention_config.get("default_policy") + + if retention_default_policy is not None: + self.retention_default_min_lifetime = retention_default_policy.get( + "min_lifetime" + ) + if self.retention_default_min_lifetime is not None: + self.retention_default_min_lifetime = self.parse_duration( + self.retention_default_min_lifetime + ) + + self.retention_default_max_lifetime = retention_default_policy.get( + "max_lifetime" + ) + if self.retention_default_max_lifetime is not None: + self.retention_default_max_lifetime = self.parse_duration( + self.retention_default_max_lifetime + ) + + if ( + self.retention_default_min_lifetime is not None + and self.retention_default_max_lifetime is not None + and ( + self.retention_default_min_lifetime + > self.retention_default_max_lifetime + ) + ): + raise ConfigError( + "The default retention policy's 'min_lifetime' can not be greater" + " than its 'max_lifetime'" + ) + else: + self.retention_default_min_lifetime = None + self.retention_default_max_lifetime = None + + self.retention_allowed_lifetime_min = retention_config.get("allowed_lifetime_min") + if self.retention_allowed_lifetime_min is not None: + self.retention_allowed_lifetime_min = self.parse_duration( + self.retention_allowed_lifetime_min + ) + + self.retention_allowed_lifetime_max = retention_config.get("allowed_lifetime_max") + if self.retention_allowed_lifetime_max is not None: + self.retention_allowed_lifetime_max = self.parse_duration( + self.retention_allowed_lifetime_max + ) + + if ( + self.retention_allowed_lifetime_min is not None + and self.retention_allowed_lifetime_max is not None + and self.retention_allowed_lifetime_min > self.retention_allowed_lifetime_max + ): + raise ConfigError( + "Invalid retention policy limits: 'allowed_lifetime_min' can not be" + " greater than 'allowed_lifetime_max'" + ) + + self.retention_purge_jobs = [] + for purge_job_config in retention_config.get("purge_jobs", []): + interval_config = purge_job_config.get("interval") + + if interval_config is None: + raise ConfigError( + "A retention policy's purge jobs configuration must have the" + " 'interval' key set." + ) + + interval = self.parse_duration(interval_config) + + shortest_max_lifetime = purge_job_config.get("shortest_max_lifetime") + + if shortest_max_lifetime is not None: + shortest_max_lifetime = self.parse_duration(shortest_max_lifetime) + + longest_max_lifetime = purge_job_config.get("longest_max_lifetime") + + if longest_max_lifetime is not None: + longest_max_lifetime = self.parse_duration(longest_max_lifetime) + + if ( + shortest_max_lifetime is not None + and longest_max_lifetime is not None + and shortest_max_lifetime > longest_max_lifetime + ): + raise ConfigError( + "A retention policy's purge jobs configuration's" + " 'shortest_max_lifetime' value can not be greater than its" + " 'longest_max_lifetime' value." + ) + + self.retention_purge_jobs.append({ + "interval": interval, + "shortest_max_lifetime": shortest_max_lifetime, + "longest_max_lifetime": longest_max_lifetime, + }) + + if not self.retention_purge_jobs: + self.retention_purge_jobs = [{ + "interval": self.parse_duration("1d"), + "shortest_max_lifetime": None, + "longest_max_lifetime": None, + }] + self.listeners = [] # type: List[dict] for listener in config.get("listeners", []): if not isinstance(listener.get("port", None), int): @@ -761,6 +870,69 @@ class ServerConfig(Config): # Defaults to `28d`. Set to `null` to disable clearing out of old rows. # #user_ips_max_age: 14d + + # Message retention policy at the server level. + # + # Room admins and mods can define a retention period for their rooms using the + # 'm.room.retention' state event, and server admins can cap this period by setting + # the 'allowed_lifetime_min' and 'allowed_lifetime_max' config options. + # + # If this feature is enabled, Synapse will regularly look for and purge events + # which are older than the room's maximum retention period. Synapse will also + # filter events received over federation so that events that should have been + # purged are ignored and not stored again. + # + retention: + # The message retention policies feature is disabled by default. Uncomment the + # following line to enable it. + # + #enabled: true + + # Default retention policy. If set, Synapse will apply it to rooms that lack the + # 'm.room.retention' state event. Currently, the value of 'min_lifetime' doesn't + # matter much because Synapse doesn't take it into account yet. + # + #default_policy: + # min_lifetime: 1d + # max_lifetime: 1y + + # Retention policy limits. If set, a user won't be able to send a + # 'm.room.retention' event which features a 'min_lifetime' or a 'max_lifetime' + # that's not within this range. This is especially useful in closed federations, + # in which server admins can make sure every federating server applies the same + # rules. + # + #allowed_lifetime_min: 1d + #allowed_lifetime_max: 1y + + # Server admins can define the settings of the background jobs purging the + # events which lifetime has expired under the 'purge_jobs' section. + # + # If no configuration is provided, a single job will be set up to delete expired + # events in every room daily. + # + # Each job's configuration defines which range of message lifetimes the job + # takes care of. For example, if 'shortest_max_lifetime' is '2d' and + # 'longest_max_lifetime' is '3d', the job will handle purging expired events in + # rooms whose state defines a 'max_lifetime' that's both higher than 2 days, and + # lower than or equal to 3 days. Both the minimum and the maximum value of a + # range are optional, e.g. a job with no 'shortest_max_lifetime' and a + # 'longest_max_lifetime' of '3d' will handle every room with a retention policy + # which 'max_lifetime' is lower than or equal to three days. + # + # The rationale for this per-job configuration is that some rooms might have a + # retention policy with a low 'max_lifetime', where history needs to be purged + # of outdated messages on a very frequent basis (e.g. every 5min), but not want + # that purge to be performed by a job that's iterating over every room it knows, + # which would be quite heavy on the server. + # + #purge_jobs: + # - shortest_max_lifetime: 1d + # longest_max_lifetime: 3d + # interval: 5m: + # - shortest_max_lifetime: 3d + # longest_max_lifetime: 1y + # interval: 24h """ % locals() ) diff --git a/synapse/events/validator.py b/synapse/events/validator.py index 272426e105..9b90c9ce04 100644 --- a/synapse/events/validator.py +++ b/synapse/events/validator.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from six import string_types +from six import integer_types, string_types from synapse.api.constants import MAX_ALIAS_LENGTH, EventTypes, Membership from synapse.api.errors import Codes, SynapseError @@ -22,11 +22,12 @@ from synapse.types import EventID, RoomID, UserID class EventValidator(object): - def validate_new(self, event): + def validate_new(self, event, config): """Validates the event has roughly the right format Args: - event (FrozenEvent) + event (FrozenEvent): The event to validate. + config (Config): The homeserver's configuration. """ self.validate_builder(event) @@ -67,6 +68,99 @@ class EventValidator(object): Codes.INVALID_PARAM, ) + if event.type == EventTypes.Retention: + self._validate_retention(event, config) + + def _validate_retention(self, event, config): + """Checks that an event that defines the retention policy for a room respects the + boundaries imposed by the server's administrator. + + Args: + event (FrozenEvent): The event to validate. + config (Config): The homeserver's configuration. + """ + min_lifetime = event.content.get("min_lifetime") + max_lifetime = event.content.get("max_lifetime") + + if min_lifetime is not None: + if not isinstance(min_lifetime, integer_types): + raise SynapseError( + code=400, + msg="'min_lifetime' must be an integer", + errcode=Codes.BAD_JSON, + ) + + if ( + config.retention_allowed_lifetime_min is not None + and min_lifetime < config.retention_allowed_lifetime_min + ): + raise SynapseError( + code=400, + msg=( + "'min_lifetime' can't be lower than the minimum allowed" + " value enforced by the server's administrator" + ), + errcode=Codes.BAD_JSON, + ) + + if ( + config.retention_allowed_lifetime_max is not None + and min_lifetime > config.retention_allowed_lifetime_max + ): + raise SynapseError( + code=400, + msg=( + "'min_lifetime' can't be greater than the maximum allowed" + " value enforced by the server's administrator" + ), + errcode=Codes.BAD_JSON, + ) + + if max_lifetime is not None: + if not isinstance(max_lifetime, integer_types): + raise SynapseError( + code=400, + msg="'max_lifetime' must be an integer", + errcode=Codes.BAD_JSON, + ) + + if ( + config.retention_allowed_lifetime_min is not None + and max_lifetime < config.retention_allowed_lifetime_min + ): + raise SynapseError( + code=400, + msg=( + "'max_lifetime' can't be lower than the minimum allowed value" + " enforced by the server's administrator" + ), + errcode=Codes.BAD_JSON, + ) + + if ( + config.retention_allowed_lifetime_max is not None + and max_lifetime > config.retention_allowed_lifetime_max + ): + raise SynapseError( + code=400, + msg=( + "'max_lifetime' can't be greater than the maximum allowed" + " value enforced by the server's administrator" + ), + errcode=Codes.BAD_JSON, + ) + + if ( + min_lifetime is not None + and max_lifetime is not None + and min_lifetime > max_lifetime + ): + raise SynapseError( + code=400, + msg="'min_lifetime' can't be greater than 'max_lifetime", + errcode=Codes.BAD_JSON, + ) + def validate_builder(self, event): """Validates that the builder/event has roughly the right format. Only checks values that we expect a proto event to have, rather than all the diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 8cafcfdab0..3994137d18 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -2454,7 +2454,7 @@ class FederationHandler(BaseHandler): room_version, event_dict, event, context ) - EventValidator().validate_new(event) + EventValidator().validate_new(event, self.config) # We need to tell the transaction queue to send this out, even # though the sender isn't a local user. diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index d682dc2b7a..155ed6e06a 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -417,7 +417,7 @@ class EventCreationHandler(object): 403, "You must be in the room to create an alias for it" ) - self.validator.validate_new(event) + self.validator.validate_new(event, self.config) return (event, context) @@ -634,7 +634,7 @@ class EventCreationHandler(object): if requester: context.app_service = requester.app_service - self.validator.validate_new(event) + self.validator.validate_new(event, self.config) # If this event is an annotation then we check that that the sender # can't annotate the same way twice (e.g. stops users from liking an diff --git a/synapse/handlers/pagination.py b/synapse/handlers/pagination.py index 97f15a1c32..e1800177fa 100644 --- a/synapse/handlers/pagination.py +++ b/synapse/handlers/pagination.py @@ -15,12 +15,15 @@ # limitations under the License. import logging +from six import iteritems + from twisted.internet import defer from twisted.python.failure import Failure from synapse.api.constants import EventTypes, Membership from synapse.api.errors import SynapseError from synapse.logging.context import run_in_background +from synapse.metrics.background_process_metrics import run_as_background_process from synapse.storage.state import StateFilter from synapse.types import RoomStreamToken from synapse.util.async_helpers import ReadWriteLock @@ -80,6 +83,114 @@ class PaginationHandler(object): self._purges_by_id = {} self._event_serializer = hs.get_event_client_serializer() + self._retention_default_max_lifetime = hs.config.retention_default_max_lifetime + + if hs.config.retention_enabled: + # Run the purge jobs described in the configuration file. + for job in hs.config.retention_purge_jobs: + self.clock.looping_call( + run_as_background_process, + job["interval"], + "purge_history_for_rooms_in_range", + self.purge_history_for_rooms_in_range, + job["shortest_max_lifetime"], + job["longest_max_lifetime"], + ) + + @defer.inlineCallbacks + def purge_history_for_rooms_in_range(self, min_ms, max_ms): + """Purge outdated events from rooms within the given retention range. + + If a default retention policy is defined in the server's configuration and its + 'max_lifetime' is within this range, also targets rooms which don't have a + retention policy. + + Args: + min_ms (int|None): Duration in milliseconds that define the lower limit of + the range to handle (exclusive). If None, it means that the range has no + lower limit. + max_ms (int|None): Duration in milliseconds that define the upper limit of + the range to handle (inclusive). If None, it means that the range has no + upper limit. + """ + # We want the storage layer to to include rooms with no retention policy in its + # return value only if a default retention policy is defined in the server's + # configuration and that policy's 'max_lifetime' is either lower (or equal) than + # max_ms or higher than min_ms (or both). + if self._retention_default_max_lifetime is not None: + include_null = True + + if min_ms is not None and min_ms >= self._retention_default_max_lifetime: + # The default max_lifetime is lower than (or equal to) min_ms. + include_null = False + + if max_ms is not None and max_ms < self._retention_default_max_lifetime: + # The default max_lifetime is higher than max_ms. + include_null = False + else: + include_null = False + + rooms = yield self.store.get_rooms_for_retention_period_in_range( + min_ms, max_ms, include_null + ) + + for room_id, retention_policy in iteritems(rooms): + if room_id in self._purges_in_progress_by_room: + logger.warning( + "[purge] not purging room %s as there's an ongoing purge running" + " for this room", + room_id, + ) + continue + + max_lifetime = retention_policy["max_lifetime"] + + if max_lifetime is None: + # If max_lifetime is None, it means that include_null equals True, + # therefore we can safely assume that there is a default policy defined + # in the server's configuration. + max_lifetime = self._retention_default_max_lifetime + + # Figure out what token we should start purging at. + ts = self.clock.time_msec() - max_lifetime + + stream_ordering = ( + yield self.store.find_first_stream_ordering_after_ts(ts) + ) + + r = ( + yield self.store.get_room_event_after_stream_ordering( + room_id, stream_ordering, + ) + ) + if not r: + logger.warning( + "[purge] purging events not possible: No event found " + "(ts %i => stream_ordering %i)", + ts, stream_ordering, + ) + continue + + (stream, topo, _event_id) = r + token = "t%d-%d" % (topo, stream) + + purge_id = random_string(16) + + self._purges_by_id[purge_id] = PurgeStatus() + + logger.info( + "Starting purging events in room %s (purge_id %s)" % (room_id, purge_id) + ) + + # We want to purge everything, including local events, and to run the purge in + # the background so that it's not blocking any other operation apart from + # other purges in the same room. + run_as_background_process( + "_purge_history", + self._purge_history, + purge_id, room_id, token, True, + ) + def start_purge_history(self, room_id, token, delete_local_events=False): """Start off a history purge on a room. diff --git a/synapse/storage/data_stores/main/events.py b/synapse/storage/data_stores/main/events.py index 301f8ea128..b332a42d82 100644 --- a/synapse/storage/data_stores/main/events.py +++ b/synapse/storage/data_stores/main/events.py @@ -929,6 +929,9 @@ class EventsStore( elif event.type == EventTypes.Redaction: # Insert into the redactions table. self._store_redaction(txn, event) + elif event.type == EventTypes.Retention: + # Update the room_retention table. + self._store_retention_policy_for_room_txn(txn, event) self._handle_event_relations(txn, event) diff --git a/synapse/storage/data_stores/main/room.py b/synapse/storage/data_stores/main/room.py index 67bb1b6f60..54a7d24c73 100644 --- a/synapse/storage/data_stores/main/room.py +++ b/synapse/storage/data_stores/main/room.py @@ -19,10 +19,13 @@ import logging import re from typing import Optional, Tuple +from six import integer_types + from canonicaljson import json from twisted.internet import defer +from synapse.api.constants import EventTypes from synapse.api.errors import StoreError from synapse.storage._base import SQLBaseStore from synapse.storage.data_stores.main.search import SearchStore @@ -302,6 +305,85 @@ class RoomWorkerStore(SQLBaseStore): class RoomStore(RoomWorkerStore, SearchStore): + def __init__(self, db_conn, hs): + super(RoomStore, self).__init__(db_conn, hs) + + self.config = hs.config + + self.register_background_update_handler( + "insert_room_retention", self._background_insert_retention, + ) + + @defer.inlineCallbacks + def _background_insert_retention(self, progress, batch_size): + """Retrieves a list of all rooms within a range and inserts an entry for each of + them into the room_retention table. + NULLs the property's columns if missing from the retention event in the room's + state (or NULLs all of them if there's no retention event in the room's state), + so that we fall back to the server's retention policy. + """ + + last_room = progress.get("room_id", "") + + def _background_insert_retention_txn(txn): + txn.execute( + """ + SELECT state.room_id, state.event_id, events.json + FROM current_state_events as state + LEFT JOIN event_json AS events ON (state.event_id = events.event_id) + WHERE state.room_id > ? AND state.type = '%s' + ORDER BY state.room_id ASC + LIMIT ?; + """ % EventTypes.Retention, + (last_room, batch_size) + ) + + rows = self.cursor_to_dict(txn) + + if not rows: + return True + + for row in rows: + if not row["json"]: + retention_policy = {} + else: + ev = json.loads(row["json"]) + retention_policy = json.dumps(ev["content"]) + + self._simple_insert_txn( + txn=txn, + table="room_retention", + values={ + "room_id": row["room_id"], + "event_id": row["event_id"], + "min_lifetime": retention_policy.get("min_lifetime"), + "max_lifetime": retention_policy.get("max_lifetime"), + } + ) + + logger.info("Inserted %d rows into room_retention", len(rows)) + + self._background_update_progress_txn( + txn, "insert_room_retention", { + "room_id": rows[-1]["room_id"], + } + ) + + if batch_size > len(rows): + return True + else: + return False + + end = yield self.runInteraction( + "insert_room_retention", + _background_insert_retention_txn, + ) + + if end: + yield self._end_background_update("insert_room_retention") + + defer.returnValue(batch_size) + @defer.inlineCallbacks def store_room(self, room_id, room_creator_user_id, is_public): """Stores a room. @@ -502,6 +584,37 @@ class RoomStore(RoomWorkerStore, SearchStore): txn, event, "content.body", event.content["body"] ) + def _store_retention_policy_for_room_txn(self, txn, event): + if ( + hasattr(event, "content") + and ("min_lifetime" in event.content or "max_lifetime" in event.content) + ): + if ( + ("min_lifetime" in event.content and not isinstance( + event.content.get("min_lifetime"), integer_types + )) + or ("max_lifetime" in event.content and not isinstance( + event.content.get("max_lifetime"), integer_types + )) + ): + # Ignore the event if one of the value isn't an integer. + return + + self._simple_insert_txn( + txn=txn, + table="room_retention", + values={ + "room_id": event.room_id, + "event_id": event.event_id, + "min_lifetime": event.content.get("min_lifetime"), + "max_lifetime": event.content.get("max_lifetime"), + }, + ) + + self._invalidate_cache_and_stream( + txn, self.get_retention_policy_for_room, (event.room_id,) + ) + def add_event_report( self, room_id, event_id, user_id, reason, content, received_ts ): @@ -683,3 +796,142 @@ class RoomStore(RoomWorkerStore, SearchStore): remote_media_mxcs.append((hostname, media_id)) return local_media_mxcs, remote_media_mxcs + + @defer.inlineCallbacks + def get_rooms_for_retention_period_in_range(self, min_ms, max_ms, include_null=False): + """Retrieves all of the rooms within the given retention range. + + Optionally includes the rooms which don't have a retention policy. + + Args: + min_ms (int|None): Duration in milliseconds that define the lower limit of + the range to handle (exclusive). If None, doesn't set a lower limit. + max_ms (int|None): Duration in milliseconds that define the upper limit of + the range to handle (inclusive). If None, doesn't set an upper limit. + include_null (bool): Whether to include rooms which retention policy is NULL + in the returned set. + + Returns: + dict[str, dict]: The rooms within this range, along with their retention + policy. The key is "room_id", and maps to a dict describing the retention + policy associated with this room ID. The keys for this nested dict are + "min_lifetime" (int|None), and "max_lifetime" (int|None). + """ + + def get_rooms_for_retention_period_in_range_txn(txn): + range_conditions = [] + args = [] + + if min_ms is not None: + range_conditions.append("max_lifetime > ?") + args.append(min_ms) + + if max_ms is not None: + range_conditions.append("max_lifetime <= ?") + args.append(max_ms) + + # Do a first query which will retrieve the rooms that have a retention policy + # in their current state. + sql = """ + SELECT room_id, min_lifetime, max_lifetime FROM room_retention + INNER JOIN current_state_events USING (event_id, room_id) + """ + + if len(range_conditions): + sql += " WHERE (" + " AND ".join(range_conditions) + ")" + + if include_null: + sql += " OR max_lifetime IS NULL" + + txn.execute(sql, args) + + rows = self.cursor_to_dict(txn) + rooms_dict = {} + + for row in rows: + rooms_dict[row["room_id"]] = { + "min_lifetime": row["min_lifetime"], + "max_lifetime": row["max_lifetime"], + } + + if include_null: + # If required, do a second query that retrieves all of the rooms we know + # of so we can handle rooms with no retention policy. + sql = "SELECT DISTINCT room_id FROM current_state_events" + + txn.execute(sql) + + rows = self.cursor_to_dict(txn) + + # If a room isn't already in the dict (i.e. it doesn't have a retention + # policy in its state), add it with a null policy. + for row in rows: + if row["room_id"] not in rooms_dict: + rooms_dict[row["room_id"]] = { + "min_lifetime": None, + "max_lifetime": None, + } + + return rooms_dict + + rooms = yield self.runInteraction( + "get_rooms_for_retention_period_in_range", + get_rooms_for_retention_period_in_range_txn, + ) + + defer.returnValue(rooms) + + @cachedInlineCallbacks() + def get_retention_policy_for_room(self, room_id): + """Get the retention policy for a given room. + + If no retention policy has been found for this room, returns a policy defined + by the configured default policy (which has None as both the 'min_lifetime' and + the 'max_lifetime' if no default policy has been defined in the server's + configuration). + + Args: + room_id (str): The ID of the room to get the retention policy of. + + Returns: + dict[int, int]: "min_lifetime" and "max_lifetime" for this room. + """ + + def get_retention_policy_for_room_txn(txn): + txn.execute( + """ + SELECT min_lifetime, max_lifetime FROM room_retention + INNER JOIN current_state_events USING (event_id, room_id) + WHERE room_id = ?; + """, + (room_id,) + ) + + return self.cursor_to_dict(txn) + + ret = yield self.runInteraction( + "get_retention_policy_for_room", + get_retention_policy_for_room_txn, + ) + + # If we don't know this room ID, ret will be None, in this case return the default + # policy. + if not ret: + defer.returnValue({ + "min_lifetime": self.config.retention_default_min_lifetime, + "max_lifetime": self.config.retention_default_max_lifetime, + }) + + row = ret[0] + + # If one of the room's policy's attributes isn't defined, use the matching + # attribute from the default policy. + # The default values will be None if no default policy has been defined, or if one + # of the attributes is missing from the default policy. + if row["min_lifetime"] is None: + row["min_lifetime"] = self.config.retention_default_min_lifetime + + if row["max_lifetime"] is None: + row["max_lifetime"] = self.config.retention_default_max_lifetime + + defer.returnValue(row) diff --git a/synapse/storage/data_stores/main/schema/delta/56/room_retention.sql b/synapse/storage/data_stores/main/schema/delta/56/room_retention.sql new file mode 100644 index 0000000000..ee6cdf7a14 --- /dev/null +++ b/synapse/storage/data_stores/main/schema/delta/56/room_retention.sql @@ -0,0 +1,33 @@ +/* Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +-- Tracks the retention policy of a room. +-- A NULL max_lifetime or min_lifetime means that the matching property is not defined in +-- the room's retention policy state event. +-- If a room doesn't have a retention policy state event in its state, both max_lifetime +-- and min_lifetime are NULL. +CREATE TABLE IF NOT EXISTS room_retention( + room_id TEXT, + event_id TEXT, + min_lifetime BIGINT, + max_lifetime BIGINT, + + PRIMARY KEY(room_id, event_id) +); + +CREATE INDEX room_retention_max_lifetime_idx on room_retention(max_lifetime); + +INSERT INTO background_updates (update_name, progress_json) VALUES + ('insert_room_retention', '{}'); diff --git a/synapse/visibility.py b/synapse/visibility.py index 8c843febd8..4498c156bc 100644 --- a/synapse/visibility.py +++ b/synapse/visibility.py @@ -86,6 +86,14 @@ def filter_events_for_client( erased_senders = yield storage.main.are_users_erased((e.sender for e in events)) + room_ids = set(e.room_id for e in events) + retention_policies = {} + + for room_id in room_ids: + retention_policies[room_id] = yield storage.main.get_retention_policy_for_room( + room_id + ) + def allowed(event): """ Args: @@ -103,6 +111,15 @@ def filter_events_for_client( if not event.is_state() and event.sender in ignore_list: return None + retention_policy = retention_policies[event.room_id] + max_lifetime = retention_policy.get("max_lifetime") + + if max_lifetime is not None: + oldest_allowed_ts = storage.main.clock.time_msec() - max_lifetime + + if event.origin_server_ts < oldest_allowed_ts: + return None + if event.event_id in always_include_ids: return event diff --git a/tests/rest/client/test_retention.py b/tests/rest/client/test_retention.py new file mode 100644 index 0000000000..41ea9db689 --- /dev/null +++ b/tests/rest/client/test_retention.py @@ -0,0 +1,320 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 New Vector Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from mock import Mock + +from synapse.api.constants import EventTypes +from synapse.rest import admin +from synapse.rest.client.v1 import login, room +from synapse.visibility import filter_events_for_client + +from tests import unittest + +one_hour_ms = 3600000 +one_day_ms = one_hour_ms * 24 + + +class RetentionTestCase(unittest.HomeserverTestCase): + servlets = [ + admin.register_servlets, + login.register_servlets, + room.register_servlets, + ] + + def make_homeserver(self, reactor, clock): + config = self.default_config() + config["default_room_version"] = "1" + config["retention"] = { + "enabled": True, + "default_policy": { + "min_lifetime": one_day_ms, + "max_lifetime": one_day_ms * 3, + }, + "allowed_lifetime_min": one_day_ms, + "allowed_lifetime_max": one_day_ms * 3, + } + + self.hs = self.setup_test_homeserver(config=config) + return self.hs + + def prepare(self, reactor, clock, homeserver): + self.user_id = self.register_user("user", "password") + self.token = self.login("user", "password") + + def test_retention_state_event(self): + """Tests that the server configuration can limit the values a user can set to the + room's retention policy. + """ + room_id = self.helper.create_room_as(self.user_id, tok=self.token) + + self.helper.send_state( + room_id=room_id, + event_type=EventTypes.Retention, + body={ + "max_lifetime": one_day_ms * 4, + }, + tok=self.token, + expect_code=400, + ) + + self.helper.send_state( + room_id=room_id, + event_type=EventTypes.Retention, + body={ + "max_lifetime": one_hour_ms, + }, + tok=self.token, + expect_code=400, + ) + + def test_retention_event_purged_with_state_event(self): + """Tests that expired events are correctly purged when the room's retention policy + is defined by a state event. + """ + room_id = self.helper.create_room_as(self.user_id, tok=self.token) + + # Set the room's retention period to 2 days. + lifetime = one_day_ms * 2 + self.helper.send_state( + room_id=room_id, + event_type=EventTypes.Retention, + body={ + "max_lifetime": lifetime, + }, + tok=self.token, + ) + + self._test_retention_event_purged(room_id, one_day_ms * 1.5) + + def test_retention_event_purged_without_state_event(self): + """Tests that expired events are correctly purged when the room's retention policy + is defined by the server's configuration's default retention policy. + """ + room_id = self.helper.create_room_as(self.user_id, tok=self.token) + + self._test_retention_event_purged(room_id, one_day_ms * 2) + + def test_visibility(self): + """Tests that synapse.visibility.filter_events_for_client correctly filters out + outdated events + """ + store = self.hs.get_datastore() + storage = self.hs.get_storage() + room_id = self.helper.create_room_as(self.user_id, tok=self.token) + events = [] + + # Send a first event, which should be filtered out at the end of the test. + resp = self.helper.send( + room_id=room_id, + body="1", + tok=self.token, + ) + + # Get the event from the store so that we end up with a FrozenEvent that we can + # give to filter_events_for_client. We need to do this now because the event won't + # be in the database anymore after it has expired. + events.append(self.get_success( + store.get_event( + resp.get("event_id") + ) + )) + + # Advance the time by 2 days. We're using the default retention policy, therefore + # after this the first event will still be valid. + self.reactor.advance(one_day_ms * 2 / 1000) + + # Send another event, which shouldn't get filtered out. + resp = self.helper.send( + room_id=room_id, + body="2", + tok=self.token, + ) + + valid_event_id = resp.get("event_id") + + events.append(self.get_success( + store.get_event( + valid_event_id + ) + )) + + # Advance the time by anothe 2 days. After this, the first event should be + # outdated but not the second one. + self.reactor.advance(one_day_ms * 2 / 1000) + + # Run filter_events_for_client with our list of FrozenEvents. + filtered_events = self.get_success(filter_events_for_client( + storage, self.user_id, events + )) + + # We should only get one event back. + self.assertEqual(len(filtered_events), 1, filtered_events) + # That event should be the second, not outdated event. + self.assertEqual(filtered_events[0].event_id, valid_event_id, filtered_events) + + def _test_retention_event_purged(self, room_id, increment): + # Send a first event to the room. This is the event we'll want to be purged at the + # end of the test. + resp = self.helper.send( + room_id=room_id, + body="1", + tok=self.token, + ) + + expired_event_id = resp.get("event_id") + + # Check that we can retrieve the event. + expired_event = self.get_event(room_id, expired_event_id) + self.assertEqual(expired_event.get("content", {}).get("body"), "1", expired_event) + + # Advance the time. + self.reactor.advance(increment / 1000) + + # Send another event. We need this because the purge job won't purge the most + # recent event in the room. + resp = self.helper.send( + room_id=room_id, + body="2", + tok=self.token, + ) + + valid_event_id = resp.get("event_id") + + # Advance the time again. Now our first event should have expired but our second + # one should still be kept. + self.reactor.advance(increment / 1000) + + # Check that the event has been purged from the database. + self.get_event(room_id, expired_event_id, expected_code=404) + + # Check that the event that hasn't been purged can still be retrieved. + valid_event = self.get_event(room_id, valid_event_id) + self.assertEqual(valid_event.get("content", {}).get("body"), "2", valid_event) + + def get_event(self, room_id, event_id, expected_code=200): + url = "/_matrix/client/r0/rooms/%s/event/%s" % (room_id, event_id) + + request, channel = self.make_request("GET", url, access_token=self.token) + self.render(request) + + self.assertEqual(channel.code, expected_code, channel.result) + + return channel.json_body + + +class RetentionNoDefaultPolicyTestCase(unittest.HomeserverTestCase): + servlets = [ + admin.register_servlets, + login.register_servlets, + room.register_servlets, + ] + + def make_homeserver(self, reactor, clock): + config = self.default_config() + config["default_room_version"] = "1" + config["retention"] = { + "enabled": True, + } + + mock_federation_client = Mock(spec=["backfill"]) + + self.hs = self.setup_test_homeserver( + config=config, + federation_client=mock_federation_client, + ) + return self.hs + + def prepare(self, reactor, clock, homeserver): + self.user_id = self.register_user("user", "password") + self.token = self.login("user", "password") + + def test_no_default_policy(self): + """Tests that an event doesn't get expired if there is neither a default retention + policy nor a policy specific to the room. + """ + room_id = self.helper.create_room_as(self.user_id, tok=self.token) + + self._test_retention(room_id) + + def test_state_policy(self): + """Tests that an event gets correctly expired if there is no default retention + policy but there's a policy specific to the room. + """ + room_id = self.helper.create_room_as(self.user_id, tok=self.token) + + # Set the maximum lifetime to 35 days so that the first event gets expired but not + # the second one. + self.helper.send_state( + room_id=room_id, + event_type=EventTypes.Retention, + body={ + "max_lifetime": one_day_ms * 35, + }, + tok=self.token, + ) + + self._test_retention(room_id, expected_code_for_first_event=404) + + def _test_retention(self, room_id, expected_code_for_first_event=200): + # Send a first event to the room. This is the event we'll want to be purged at the + # end of the test. + resp = self.helper.send( + room_id=room_id, + body="1", + tok=self.token, + ) + + first_event_id = resp.get("event_id") + + # Check that we can retrieve the event. + expired_event = self.get_event(room_id, first_event_id) + self.assertEqual(expired_event.get("content", {}).get("body"), "1", expired_event) + + # Advance the time by a month. + self.reactor.advance(one_day_ms * 30 / 1000) + + # Send another event. We need this because the purge job won't purge the most + # recent event in the room. + resp = self.helper.send( + room_id=room_id, + body="2", + tok=self.token, + ) + + second_event_id = resp.get("event_id") + + # Advance the time by another month. + self.reactor.advance(one_day_ms * 30 / 1000) + + # Check if the event has been purged from the database. + first_event = self.get_event( + room_id, first_event_id, expected_code=expected_code_for_first_event + ) + + if expected_code_for_first_event == 200: + self.assertEqual(first_event.get("content", {}).get("body"), "1", first_event) + + # Check that the event that hasn't been purged can still be retrieved. + second_event = self.get_event(room_id, second_event_id) + self.assertEqual(second_event.get("content", {}).get("body"), "2", second_event) + + def get_event(self, room_id, event_id, expected_code=200): + url = "/_matrix/client/r0/rooms/%s/event/%s" % (room_id, event_id) + + request, channel = self.make_request("GET", url, access_token=self.token) + self.render(request) + + self.assertEqual(channel.code, expected_code, channel.result) + + return channel.json_body From 4e1c7b79fa3498c48106c17d0edbab2f7bcc0c38 Mon Sep 17 00:00:00 2001 From: Amber Brown Date: Tue, 5 Nov 2019 05:05:48 +1100 Subject: [PATCH 0432/1623] Remove the psutil dependency (#6318) * remove psutil and replace with resource --- changelog.d/6318.misc | 1 + synapse/app/homeserver.py | 174 +++++++++++++++++---------------- synapse/python_dependencies.py | 1 - synapse/server.py | 2 + tests/test_phone_home.py | 51 ++++++++++ 5 files changed, 146 insertions(+), 83 deletions(-) create mode 100644 changelog.d/6318.misc create mode 100644 tests/test_phone_home.py diff --git a/changelog.d/6318.misc b/changelog.d/6318.misc new file mode 100644 index 0000000000..63527ccef4 --- /dev/null +++ b/changelog.d/6318.misc @@ -0,0 +1 @@ +Remove the dependency on psutil and replace functionality with the stdlib `resource` module. diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py index 8d28076d92..00a7f8330e 100644 --- a/synapse/app/homeserver.py +++ b/synapse/app/homeserver.py @@ -19,12 +19,13 @@ from __future__ import print_function import gc import logging +import math import os +import resource import sys from six import iteritems -import psutil from prometheus_client import Gauge from twisted.application import service @@ -471,6 +472,87 @@ class SynapseService(service.Service): return self._port.stopListening() +# Contains the list of processes we will be monitoring +# currently either 0 or 1 +_stats_process = [] + + +@defer.inlineCallbacks +def phone_stats_home(hs, stats, stats_process=_stats_process): + logger.info("Gathering stats for reporting") + now = int(hs.get_clock().time()) + uptime = int(now - hs.start_time) + if uptime < 0: + uptime = 0 + + stats["homeserver"] = hs.config.server_name + stats["server_context"] = hs.config.server_context + stats["timestamp"] = now + stats["uptime_seconds"] = uptime + version = sys.version_info + stats["python_version"] = "{}.{}.{}".format( + version.major, version.minor, version.micro + ) + stats["total_users"] = yield hs.get_datastore().count_all_users() + + total_nonbridged_users = yield hs.get_datastore().count_nonbridged_users() + stats["total_nonbridged_users"] = total_nonbridged_users + + daily_user_type_results = yield hs.get_datastore().count_daily_user_type() + for name, count in iteritems(daily_user_type_results): + stats["daily_user_type_" + name] = count + + room_count = yield hs.get_datastore().get_room_count() + stats["total_room_count"] = room_count + + stats["daily_active_users"] = yield hs.get_datastore().count_daily_users() + stats["monthly_active_users"] = yield hs.get_datastore().count_monthly_users() + stats["daily_active_rooms"] = yield hs.get_datastore().count_daily_active_rooms() + stats["daily_messages"] = yield hs.get_datastore().count_daily_messages() + + r30_results = yield hs.get_datastore().count_r30_users() + for name, count in iteritems(r30_results): + stats["r30_users_" + name] = count + + daily_sent_messages = yield hs.get_datastore().count_daily_sent_messages() + stats["daily_sent_messages"] = daily_sent_messages + stats["cache_factor"] = CACHE_SIZE_FACTOR + stats["event_cache_size"] = hs.config.event_cache_size + + # + # Performance statistics + # + old = stats_process[0] + new = (now, resource.getrusage(resource.RUSAGE_SELF)) + stats_process[0] = new + + # Get RSS in bytes + stats["memory_rss"] = new[1].ru_maxrss + + # Get CPU time in % of a single core, not % of all cores + used_cpu_time = (new[1].ru_utime + new[1].ru_stime) - ( + old[1].ru_utime + old[1].ru_stime + ) + if used_cpu_time == 0 or new[0] == old[0]: + stats["cpu_average"] = 0 + else: + stats["cpu_average"] = math.floor(used_cpu_time / (new[0] - old[0]) * 100) + + # + # Database version + # + + stats["database_engine"] = hs.get_datastore().database_engine_name + stats["database_server_version"] = hs.get_datastore().get_server_version() + logger.info("Reporting stats to %s: %s" % (hs.config.report_stats_endpoint, stats)) + try: + yield hs.get_proxied_http_client().put_json( + hs.config.report_stats_endpoint, stats + ) + except Exception as e: + logger.warning("Error reporting stats: %s", e) + + def run(hs): PROFILE_SYNAPSE = False if PROFILE_SYNAPSE: @@ -497,91 +579,19 @@ def run(hs): reactor.run = profile(reactor.run) clock = hs.get_clock() - start_time = clock.time() stats = {} - # Contains the list of processes we will be monitoring - # currently either 0 or 1 - stats_process = [] + def performance_stats_init(): + _stats_process.clear() + _stats_process.append( + (int(hs.get_clock().time(), resource.getrusage(resource.RUSAGE_SELF))) + ) def start_phone_stats_home(): - return run_as_background_process("phone_stats_home", phone_stats_home) - - @defer.inlineCallbacks - def phone_stats_home(): - logger.info("Gathering stats for reporting") - now = int(hs.get_clock().time()) - uptime = int(now - start_time) - if uptime < 0: - uptime = 0 - - stats["homeserver"] = hs.config.server_name - stats["server_context"] = hs.config.server_context - stats["timestamp"] = now - stats["uptime_seconds"] = uptime - version = sys.version_info - stats["python_version"] = "{}.{}.{}".format( - version.major, version.minor, version.micro + return run_as_background_process( + "phone_stats_home", phone_stats_home, hs, stats ) - stats["total_users"] = yield hs.get_datastore().count_all_users() - - total_nonbridged_users = yield hs.get_datastore().count_nonbridged_users() - stats["total_nonbridged_users"] = total_nonbridged_users - - daily_user_type_results = yield hs.get_datastore().count_daily_user_type() - for name, count in iteritems(daily_user_type_results): - stats["daily_user_type_" + name] = count - - room_count = yield hs.get_datastore().get_room_count() - stats["total_room_count"] = room_count - - stats["daily_active_users"] = yield hs.get_datastore().count_daily_users() - stats["monthly_active_users"] = yield hs.get_datastore().count_monthly_users() - stats[ - "daily_active_rooms" - ] = yield hs.get_datastore().count_daily_active_rooms() - stats["daily_messages"] = yield hs.get_datastore().count_daily_messages() - - r30_results = yield hs.get_datastore().count_r30_users() - for name, count in iteritems(r30_results): - stats["r30_users_" + name] = count - - daily_sent_messages = yield hs.get_datastore().count_daily_sent_messages() - stats["daily_sent_messages"] = daily_sent_messages - stats["cache_factor"] = CACHE_SIZE_FACTOR - stats["event_cache_size"] = hs.config.event_cache_size - - if len(stats_process) > 0: - stats["memory_rss"] = 0 - stats["cpu_average"] = 0 - for process in stats_process: - stats["memory_rss"] += process.memory_info().rss - stats["cpu_average"] += int(process.cpu_percent(interval=None)) - - stats["database_engine"] = hs.get_datastore().database_engine_name - stats["database_server_version"] = hs.get_datastore().get_server_version() - logger.info( - "Reporting stats to %s: %s" % (hs.config.report_stats_endpoint, stats) - ) - try: - yield hs.get_proxied_http_client().put_json( - hs.config.report_stats_endpoint, stats - ) - except Exception as e: - logger.warning("Error reporting stats: %s", e) - - def performance_stats_init(): - try: - process = psutil.Process() - # Ensure we can fetch both, and make the initial request for cpu_percent - # so the next request will use this as the initial point. - process.memory_info().rss - process.cpu_percent(interval=None) - logger.info("report_stats can use psutil") - stats_process.append(process) - except (AttributeError): - logger.warning("Unable to read memory/cpu stats. Disabling reporting.") def generate_user_daily_visit_stats(): return run_as_background_process( @@ -626,7 +636,7 @@ def run(hs): if hs.config.report_stats: logger.info("Scheduling stats reporting for 3 hour intervals") - clock.looping_call(start_phone_stats_home, 3 * 60 * 60 * 1000) + clock.looping_call(start_phone_stats_home, 3 * 60 * 60 * 1000, hs, stats) # We need to defer this init for the cases that we daemonize # otherwise the process ID we get is that of the non-daemon process @@ -634,7 +644,7 @@ def run(hs): # We wait 5 minutes to send the first set of stats as the server can # be quite busy the first few minutes - clock.call_later(5 * 60, start_phone_stats_home) + clock.call_later(5 * 60, start_phone_stats_home, hs, stats) _base.start_reactor( "synapse-homeserver", diff --git a/synapse/python_dependencies.py b/synapse/python_dependencies.py index aa7da1c543..5871feaafd 100644 --- a/synapse/python_dependencies.py +++ b/synapse/python_dependencies.py @@ -61,7 +61,6 @@ REQUIREMENTS = [ "bcrypt>=3.1.0", "pillow>=4.3.0", "sortedcontainers>=1.4.4", - "psutil>=2.0.0", "pymacaroons>=0.13.0", "msgpack>=0.5.2", "phonenumbers>=8.2.0", diff --git a/synapse/server.py b/synapse/server.py index f8aeebcff8..90c3b072e8 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -221,6 +221,7 @@ class HomeServer(object): self.hostname = hostname self._building = {} self._listening_services = [] + self.start_time = None self.clock = Clock(reactor) self.distributor = Distributor() @@ -240,6 +241,7 @@ class HomeServer(object): datastore = self.DATASTORE_CLASS(conn, self) self.datastores = DataStores(datastore, conn, self) conn.commit() + self.start_time = int(self.get_clock().time()) logger.info("Finished setting up.") def setup_master(self): diff --git a/tests/test_phone_home.py b/tests/test_phone_home.py new file mode 100644 index 0000000000..7657bddea5 --- /dev/null +++ b/tests/test_phone_home.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import resource + +import mock + +from synapse.app.homeserver import phone_stats_home + +from tests.unittest import HomeserverTestCase + + +class PhoneHomeStatsTestCase(HomeserverTestCase): + def test_performance_frozen_clock(self): + """ + If time doesn't move, don't error out. + """ + past_stats = [ + (self.hs.get_clock().time(), resource.getrusage(resource.RUSAGE_SELF)) + ] + stats = {} + self.get_success(phone_stats_home(self.hs, stats, past_stats)) + self.assertEqual(stats["cpu_average"], 0) + + def test_performance_100(self): + """ + 1 second of usage over 1 second is 100% CPU usage. + """ + real_res = resource.getrusage(resource.RUSAGE_SELF) + old_resource = mock.Mock(spec=real_res) + old_resource.ru_utime = real_res.ru_utime - 1 + old_resource.ru_stime = real_res.ru_stime + old_resource.ru_maxrss = real_res.ru_maxrss + + past_stats = [(self.hs.get_clock().time(), old_resource)] + stats = {} + self.reactor.advance(1) + self.get_success(phone_stats_home(self.hs, stats, past_stats)) + self.assertApproximates(stats["cpu_average"], 100, tolerance=2.5) From 0287d033eec86fb7f6bb84f929e756c99caf2113 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Mon, 4 Nov 2019 18:08:50 +0000 Subject: [PATCH 0433/1623] Transfer upgraded rooms on groups --- changelog.d/6235.bugfix | 1 + synapse/handlers/room_member.py | 9 +++++++++ synapse/storage/data_stores/main/group_server.py | 15 +++++++++++++++ 3 files changed, 25 insertions(+) create mode 100644 changelog.d/6235.bugfix diff --git a/changelog.d/6235.bugfix b/changelog.d/6235.bugfix new file mode 100644 index 0000000000..12718ba934 --- /dev/null +++ b/changelog.d/6235.bugfix @@ -0,0 +1 @@ +Remove a room from a server's public rooms list on room upgrade. \ No newline at end of file diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index 06d09c2947..01c65ee222 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -514,6 +514,15 @@ class RoomMemberHandler(object): if old_room and old_room["is_public"]: yield self.store.set_room_is_public(old_room_id, False) yield self.store.set_room_is_public(room_id, True) + + # Check if any groups we own contain the predecessor room + local_group_ids = yield self.store.get_local_groups_for_room(old_room_id) + for group_id in local_group_ids: + # Add new the new room to those groups + yield self.store.add_room_to_group(group_id, room_id, old_room["is_public"]) + + # Remove the old room from those groups + yield self.store.remove_room_from_group(group_id, old_room_id) @defer.inlineCallbacks def copy_user_state_on_room_upgrade(self, old_room_id, new_room_id, user_ids): diff --git a/synapse/storage/data_stores/main/group_server.py b/synapse/storage/data_stores/main/group_server.py index b3a2771f1b..13ad71a49c 100644 --- a/synapse/storage/data_stores/main/group_server.py +++ b/synapse/storage/data_stores/main/group_server.py @@ -552,6 +552,21 @@ class GroupServerStore(SQLBaseStore): keyvalues={"group_id": group_id, "role_id": role_id, "user_id": user_id}, desc="remove_user_from_summary", ) + + def get_local_groups_for_room(self, room_id): + """Get all of the local group that contain a given room + Args: + room_id (str): The ID of a room + Returns: + Deferred[list[str]]: A twisted.Deferred containing a list of group ids + containing this room + """ + return self._simple_select_onecol( + table="group_rooms", + keyvalues={"room_id": room_id}, + retcol="group_id", + desc="get_local_groups_for_room", + ) def get_users_for_summary_by_role(self, group_id, include_private=False): """Get the users and roles that should be included in a summary request From c2203bea57fd34de9be994f5e117da2c24338708 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Mon, 4 Nov 2019 18:17:11 +0000 Subject: [PATCH 0434/1623] Re-add docstring, with caveats detailed --- synapse/handlers/room_member.py | 2 +- synapse/storage/data_stores/main/group_server.py | 2 +- synapse/storage/data_stores/main/state.py | 6 +++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index 01c65ee222..6cfee4b361 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -514,7 +514,7 @@ class RoomMemberHandler(object): if old_room and old_room["is_public"]: yield self.store.set_room_is_public(old_room_id, False) yield self.store.set_room_is_public(room_id, True) - + # Check if any groups we own contain the predecessor room local_group_ids = yield self.store.get_local_groups_for_room(old_room_id) for group_id in local_group_ids: diff --git a/synapse/storage/data_stores/main/group_server.py b/synapse/storage/data_stores/main/group_server.py index 13ad71a49c..5ded539af8 100644 --- a/synapse/storage/data_stores/main/group_server.py +++ b/synapse/storage/data_stores/main/group_server.py @@ -552,7 +552,7 @@ class GroupServerStore(SQLBaseStore): keyvalues={"group_id": group_id, "role_id": role_id, "user_id": user_id}, desc="remove_user_from_summary", ) - + def get_local_groups_for_room(self, room_id): """Get all of the local group that contain a given room Args: diff --git a/synapse/storage/data_stores/main/state.py b/synapse/storage/data_stores/main/state.py index 3132848034..a41cac7b36 100644 --- a/synapse/storage/data_stores/main/state.py +++ b/synapse/storage/data_stores/main/state.py @@ -285,7 +285,11 @@ class StateGroupWorkerStore( room_id (str) Returns: - Deferred[unicode|None]: predecessor room id + Deferred[dict|None]: A dictionary containing the structure of the predecessor + field from the room's create event. The structure is subject to other servers, + but it is expected to be: + * room_id (str): The room ID of the predecessor room + * event_id (str): The ID of the tombstone event in the predecessor room Raises: NotFoundError if the room is unknown From 1dffa78701d43b299419090d544fb8bb91ab4d5b Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 5 Nov 2019 12:21:59 +0000 Subject: [PATCH 0435/1623] Filter events_before and events_after in /context requests While the current version of the spec doesn't say much about how this endpoint uses filters (see https://github.com/matrix-org/matrix-doc/issues/2338), the current implementation is that some fields of an EventFilter apply (the ones that are used when running the SQL query) and others don't (the ones that are used by the filter itself) because we don't call event_filter.filter(...). This seems counter-intuitive and probably not what we want so this commit fixes it. --- synapse/handlers/room.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index e92b2eafd5..899bb63114 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -874,8 +874,10 @@ class RoomContextHandler(object): room_id, event_id, before_limit, after_limit, event_filter ) - results["events_before"] = yield filter_evts(results["events_before"]) - results["events_after"] = yield filter_evts(results["events_after"]) + filtered_before_events = event_filter.filter(results["events_before"]) + results["events_before"] = yield filter_evts(filtered_before_events) + filtered_after_events = event_filter.filter(results["events_after"]) + results["events_after"] = yield filter_evts(filtered_after_events) results["event"] = event if results["events_after"]: From a7c818c79b70d6b70abc5b26f0e1e78fd60c087e Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 5 Nov 2019 13:21:26 +0000 Subject: [PATCH 0436/1623] Add test case --- tests/rest/client/v1/test_rooms.py | 182 +++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py index 5e38fd6ced..621c894e35 100644 --- a/tests/rest/client/v1/test_rooms.py +++ b/tests/rest/client/v1/test_rooms.py @@ -1106,3 +1106,185 @@ class PerRoomProfilesForbiddenTestCase(unittest.HomeserverTestCase): res_displayname = channel.json_body["content"]["displayname"] self.assertEqual(res_displayname, self.displayname, channel.result) + + +class ContextTestCase(unittest.HomeserverTestCase): + servlets = [ + synapse.rest.admin.register_servlets_for_client_rest_resource, + room.register_servlets, + login.register_servlets, + profile.register_servlets, + ] + + def test_context_filter_labels(self): + """Test that we can filter by a label.""" + context_filter = json.dumps( + { + "types": [EventTypes.Message], + "org.matrix.labels": ["#fun"], + } + ) + + res = self._test_context_filter_labels(context_filter) + + self.assertEqual( + res["event"]["content"]["body"], "with right label", res["event"] + ) + + events_before = res["events_before"] + + self.assertEqual( + len(events_before), 1, [event["content"] for event in events_before] + ) + self.assertEqual( + events_before[0]["content"]["body"], "with right label", events_before[0] + ) + + events_after = res["events_before"] + + self.assertEqual( + len(events_after), 1, [event["content"] for event in events_after] + ) + self.assertEqual( + events_after[0]["content"]["body"], "with right label", events_after[0] + ) + + def test_context_filter_not_labels(self): + """Test that we can filter by the absence of a label.""" + context_filter = json.dumps( + { + "types": [EventTypes.Message], + "org.matrix.not_labels": ["#fun"], + } + ) + + res = self._test_context_filter_labels(context_filter) + + events_before = res["events_before"] + + self.assertEqual( + len(events_before), 1, [event["content"] for event in events_before] + ) + self.assertEqual( + events_before[0]["content"]["body"], "without label", events_before[0] + ) + + events_after = res["events_after"] + + self.assertEqual( + len(events_after), 2, [event["content"] for event in events_after] + ) + self.assertEqual( + events_after[0]["content"]["body"], "with wrong label", events_after[0] + ) + self.assertEqual( + events_after[1]["content"]["body"], "with two wrong labels", events_after[1] + ) + + def test_context_filter_labels_not_labels(self): + """Test that we can filter by both a label and the absence of another label.""" + context_filter = json.dumps( + { + "types": [EventTypes.Message], + "org.matrix.labels": ["#work"], + "org.matrix.not_labels": ["#notfun"], + } + ) + + res = self._test_context_filter_labels(context_filter) + + events_before = res["events_before"] + + self.assertEqual( + len(events_before), 0, [event["content"] for event in events_before] + ) + + events_after = res["events_after"] + + self.assertEqual( + len(events_after), 1, [event["content"] for event in events_after] + ) + self.assertEqual( + events_after[0]["content"]["body"], "with wrong label", events_after[0] + ) + + def _test_context_filter_labels(self, context_filter): + user_id = self.register_user("kermit", "test") + tok = self.login("kermit", "test") + + room_id = self.helper.create_room_as(user_id, tok=tok) + + self.helper.send_event( + room_id=room_id, + type=EventTypes.Message, + content={ + "msgtype": "m.text", + "body": "with right label", + EventContentFields.LABELS: ["#fun"], + }, + tok=tok, + ) + + self.helper.send_event( + room_id=room_id, + type=EventTypes.Message, + content={"msgtype": "m.text", "body": "without label"}, + tok=tok, + ) + + # The event we'll look up the context for. + res = self.helper.send_event( + room_id=room_id, + type=EventTypes.Message, + content={ + "msgtype": "m.text", + "body": "with right label", + EventContentFields.LABELS: ["#fun"], + }, + tok=tok, + ) + event_id = res["event_id"] + + self.helper.send_event( + room_id=room_id, + type=EventTypes.Message, + content={ + "msgtype": "m.text", + "body": "with wrong label", + EventContentFields.LABELS: ["#work"], + }, + tok=tok, + ) + + self.helper.send_event( + room_id=room_id, + type=EventTypes.Message, + content={ + "msgtype": "m.text", + "body": "with two wrong labels", + EventContentFields.LABELS: ["#work", "#notfun"], + }, + tok=tok, + ) + + self.helper.send_event( + room_id=room_id, + type=EventTypes.Message, + content={ + "msgtype": "m.text", + "body": "with right label", + EventContentFields.LABELS: ["#fun"], + }, + tok=tok, + ) + + request, channel = self.make_request( + "GET", + "/rooms/%s/context/%s?filter=%s" % (room_id, event_id, context_filter), + access_token=tok, + ) + self.render(request) + self.assertEqual(channel.code, 200, channel.result) + + return channel.json_body + From 408600282774391fade9e4a6606f4967184865c0 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 5 Nov 2019 13:23:25 +0000 Subject: [PATCH 0437/1623] Improve documentation for EventContext fields (#6319) --- changelog.d/6319.misc | 1 + synapse/events/snapshot.py | 85 +++++++++++++++++++++++++++----------- synapse/state/__init__.py | 3 ++ 3 files changed, 66 insertions(+), 23 deletions(-) create mode 100644 changelog.d/6319.misc diff --git a/changelog.d/6319.misc b/changelog.d/6319.misc new file mode 100644 index 0000000000..9711ef21ed --- /dev/null +++ b/changelog.d/6319.misc @@ -0,0 +1 @@ +Improve documentation for EventContext fields. diff --git a/synapse/events/snapshot.py b/synapse/events/snapshot.py index a269de5482..5f07f6fe4b 100644 --- a/synapse/events/snapshot.py +++ b/synapse/events/snapshot.py @@ -12,6 +12,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from typing import Dict, Optional, Tuple, Union + from six import iteritems import attr @@ -19,45 +21,82 @@ from frozendict import frozendict from twisted.internet import defer +from synapse.appservice import ApplicationService from synapse.logging.context import make_deferred_yieldable, run_in_background @attr.s(slots=True) class EventContext: """ + Holds information relevant to persisting an event + Attributes: - state_group (int|None): state group id, if the state has been stored - as a state group. This is usually only None if e.g. the event is - an outlier. - rejected (bool|str): A rejection reason if the event was rejected, else - False + rejected: A rejection reason if the event was rejected, else False - prev_group (int): Previously persisted state group. ``None`` for an - outlier. - delta_ids (dict[(str, str), str]): Delta from ``prev_group``. - (type, state_key) -> event_id. ``None`` for an outlier. + state_group: The ID of the state group for this event. Note that state events + are persisted with a state group which includes the new event, so this is + effectively the state *after* the event in question. - app_service: FIXME + For a *rejected* state event, where the state of the rejected event is + ignored, this state_group should never make it into the + event_to_state_groups table. Indeed, inspecting this value for a rejected + state event is almost certainly incorrect. + + For an outlier, where we don't have the state at the event, this will be + None. + + prev_group: If it is known, ``state_group``'s prev_group. Note that this being + None does not necessarily mean that ``state_group`` does not have + a prev_group! + + If ``state_group`` is None (ie, the event is an outlier), ``prev_group`` + will always also be ``None``. + + Note that this *not* (necessarily) the state group associated with + ``_prev_state_ids``. + + delta_ids: If ``prev_group`` is not None, the state delta between ``prev_group`` + and ``state_group``. + + app_service: If this event is being sent by a (local) application service, that + app service. + + _current_state_ids: The room state map, including this event - ie, the state + in ``state_group``. - _current_state_ids (dict[(str, str), str]|None): - The current state map including the current event. None if outlier - or we haven't fetched the state from DB yet. (type, state_key) -> event_id - _prev_state_ids (dict[(str, str), str]|None): - The current state map excluding the current event. None if outlier - or we haven't fetched the state from DB yet. + FIXME: what is this for an outlier? it seems ill-defined. It seems like + it could be either {}, or the state we were given by the remote + server, depending on $THINGS + + Note that this is a private attribute: it should be accessed via + ``get_current_state_ids``. _AsyncEventContext impl calculates this + on-demand: it will be None until that happens. + + _prev_state_ids: The room state map, excluding this event. For a non-state + event, this will be the same as _current_state_events. + + Note that it is a completely different thing to prev_group! + (type, state_key) -> event_id + + FIXME: again, what is this for an outlier? + + As with _current_state_ids, this is a private attribute. It should be + accessed via get_prev_state_ids. """ - state_group = attr.ib(default=None) - rejected = attr.ib(default=False) - prev_group = attr.ib(default=None) - delta_ids = attr.ib(default=None) - app_service = attr.ib(default=None) + rejected = attr.ib(default=False, type=Union[bool, str]) + state_group = attr.ib(default=None, type=Optional[int]) + prev_group = attr.ib(default=None, type=Optional[int]) + delta_ids = attr.ib(default=None, type=Optional[Dict[Tuple[str, str], str]]) + app_service = attr.ib(default=None, type=Optional[ApplicationService]) - _prev_state_ids = attr.ib(default=None) - _current_state_ids = attr.ib(default=None) + _current_state_ids = attr.ib( + default=None, type=Optional[Dict[Tuple[str, str], str]] + ) + _prev_state_ids = attr.ib(default=None, type=Optional[Dict[Tuple[str, str], str]]) @staticmethod def with_state( diff --git a/synapse/state/__init__.py b/synapse/state/__init__.py index 4e91eb66fe..2c04ab1854 100644 --- a/synapse/state/__init__.py +++ b/synapse/state/__init__.py @@ -232,6 +232,9 @@ class StateHandler(object): # If this is an outlier, then we know it shouldn't have any current # state. Certainly store.get_current_state won't return any, and # persisting the event won't store the state group. + + # FIXME: why do we populate current_state_ids? I thought the point was + # that we weren't supposed to have any state for outliers? if old_state: prev_state_ids = {(s.type, s.state_key): s.event_id for s in old_state} if event.is_state(): From c9e4748cb75271a2178d0cae05d551829249ada3 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 5 Nov 2019 13:47:47 +0000 Subject: [PATCH 0438/1623] Merge labels tests for /context and /messages --- tests/rest/client/v1/test_rooms.py | 276 ++++++++++++++--------------- 1 file changed, 130 insertions(+), 146 deletions(-) diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py index 621c894e35..fe327d1bf8 100644 --- a/tests/rest/client/v1/test_rooms.py +++ b/tests/rest/client/v1/test_rooms.py @@ -811,105 +811,6 @@ class RoomMessageListTestCase(RoomBase): self.assertTrue("chunk" in channel.json_body) self.assertTrue("end" in channel.json_body) - def test_filter_labels(self): - """Test that we can filter by a label.""" - message_filter = json.dumps( - {"types": [EventTypes.Message], "org.matrix.labels": ["#fun"]} - ) - - events = self._test_filter_labels(message_filter) - - self.assertEqual(len(events), 2, [event["content"] for event in events]) - self.assertEqual(events[0]["content"]["body"], "with right label", events[0]) - self.assertEqual(events[1]["content"]["body"], "with right label", events[1]) - - def test_filter_not_labels(self): - """Test that we can filter by the absence of a label.""" - message_filter = json.dumps( - {"types": [EventTypes.Message], "org.matrix.not_labels": ["#fun"]} - ) - - events = self._test_filter_labels(message_filter) - - self.assertEqual(len(events), 3, [event["content"] for event in events]) - self.assertEqual(events[0]["content"]["body"], "without label", events[0]) - self.assertEqual(events[1]["content"]["body"], "with wrong label", events[1]) - self.assertEqual( - events[2]["content"]["body"], "with two wrong labels", events[2] - ) - - def test_filter_labels_not_labels(self): - """Test that we can filter by both a label and the absence of another label.""" - sync_filter = json.dumps( - { - "types": [EventTypes.Message], - "org.matrix.labels": ["#work"], - "org.matrix.not_labels": ["#notfun"], - } - ) - - events = self._test_filter_labels(sync_filter) - - self.assertEqual(len(events), 1, [event["content"] for event in events]) - self.assertEqual(events[0]["content"]["body"], "with wrong label", events[0]) - - def _test_filter_labels(self, message_filter): - self.helper.send_event( - room_id=self.room_id, - type=EventTypes.Message, - content={ - "msgtype": "m.text", - "body": "with right label", - EventContentFields.LABELS: ["#fun"], - }, - ) - - self.helper.send_event( - room_id=self.room_id, - type=EventTypes.Message, - content={"msgtype": "m.text", "body": "without label"}, - ) - - self.helper.send_event( - room_id=self.room_id, - type=EventTypes.Message, - content={ - "msgtype": "m.text", - "body": "with wrong label", - EventContentFields.LABELS: ["#work"], - }, - ) - - self.helper.send_event( - room_id=self.room_id, - type=EventTypes.Message, - content={ - "msgtype": "m.text", - "body": "with two wrong labels", - EventContentFields.LABELS: ["#work", "#notfun"], - }, - ) - - self.helper.send_event( - room_id=self.room_id, - type=EventTypes.Message, - content={ - "msgtype": "m.text", - "body": "with right label", - EventContentFields.LABELS: ["#fun"], - }, - ) - - token = "s0_0_0_0_0_0_0_0_0" - request, channel = self.make_request( - "GET", - "/rooms/%s/messages?access_token=x&from=%s&filter=%s" - % (self.room_id, token, message_filter), - ) - self.render(request) - - return channel.json_body["chunk"] - class RoomSearchTestCase(unittest.HomeserverTestCase): servlets = [ @@ -1108,7 +1009,7 @@ class PerRoomProfilesForbiddenTestCase(unittest.HomeserverTestCase): self.assertEqual(res_displayname, self.displayname, channel.result) -class ContextTestCase(unittest.HomeserverTestCase): +class LabelsTestCase(unittest.HomeserverTestCase): servlets = [ synapse.rest.admin.register_servlets_for_client_rest_resource, room.register_servlets, @@ -1116,8 +1017,13 @@ class ContextTestCase(unittest.HomeserverTestCase): profile.register_servlets, ] + def prepare(self, reactor, clock, homeserver): + self.user_id = self.register_user("test", "test") + self.tok = self.login("test", "test") + self.room_id = self.helper.create_room_as(self.user_id, tok=self.tok) + def test_context_filter_labels(self): - """Test that we can filter by a label.""" + """Test that we can filter by a label on a /context request.""" context_filter = json.dumps( { "types": [EventTypes.Message], @@ -1125,13 +1031,17 @@ class ContextTestCase(unittest.HomeserverTestCase): } ) - res = self._test_context_filter_labels(context_filter) + event_id = self._send_labelled_messages_in_room() - self.assertEqual( - res["event"]["content"]["body"], "with right label", res["event"] + request, channel = self.make_request( + "GET", + "/rooms/%s/context/%s?filter=%s" % (self.room_id, event_id, context_filter), + access_token=self.tok, ) + self.render(request) + self.assertEqual(channel.code, 200, channel.result) - events_before = res["events_before"] + events_before = channel.json_body["events_before"] self.assertEqual( len(events_before), 1, [event["content"] for event in events_before] @@ -1140,7 +1050,7 @@ class ContextTestCase(unittest.HomeserverTestCase): events_before[0]["content"]["body"], "with right label", events_before[0] ) - events_after = res["events_before"] + events_after = channel.json_body["events_before"] self.assertEqual( len(events_after), 1, [event["content"] for event in events_after] @@ -1150,7 +1060,7 @@ class ContextTestCase(unittest.HomeserverTestCase): ) def test_context_filter_not_labels(self): - """Test that we can filter by the absence of a label.""" + """Test that we can filter by the absence of a label on a /context request.""" context_filter = json.dumps( { "types": [EventTypes.Message], @@ -1158,9 +1068,17 @@ class ContextTestCase(unittest.HomeserverTestCase): } ) - res = self._test_context_filter_labels(context_filter) + event_id = self._send_labelled_messages_in_room() - events_before = res["events_before"] + request, channel = self.make_request( + "GET", + "/rooms/%s/context/%s?filter=%s" % (self.room_id, event_id, context_filter), + access_token=self.tok, + ) + self.render(request) + self.assertEqual(channel.code, 200, channel.result) + + events_before = channel.json_body["events_before"] self.assertEqual( len(events_before), 1, [event["content"] for event in events_before] @@ -1169,7 +1087,7 @@ class ContextTestCase(unittest.HomeserverTestCase): events_before[0]["content"]["body"], "without label", events_before[0] ) - events_after = res["events_after"] + events_after = channel.json_body["events_after"] self.assertEqual( len(events_after), 2, [event["content"] for event in events_after] @@ -1182,7 +1100,9 @@ class ContextTestCase(unittest.HomeserverTestCase): ) def test_context_filter_labels_not_labels(self): - """Test that we can filter by both a label and the absence of another label.""" + """Test that we can filter by both a label and the absence of another label on a + /context request. + """ context_filter = json.dumps( { "types": [EventTypes.Message], @@ -1191,15 +1111,23 @@ class ContextTestCase(unittest.HomeserverTestCase): } ) - res = self._test_context_filter_labels(context_filter) + event_id = self._send_labelled_messages_in_room() - events_before = res["events_before"] + request, channel = self.make_request( + "GET", + "/rooms/%s/context/%s?filter=%s" % (self.room_id, event_id, context_filter), + access_token=self.tok, + ) + self.render(request) + self.assertEqual(channel.code, 200, channel.result) + + events_before = channel.json_body["events_before"] self.assertEqual( len(events_before), 0, [event["content"] for event in events_before] ) - events_after = res["events_after"] + events_after = channel.json_body["events_after"] self.assertEqual( len(events_after), 1, [event["content"] for event in events_after] @@ -1208,83 +1136,139 @@ class ContextTestCase(unittest.HomeserverTestCase): events_after[0]["content"]["body"], "with wrong label", events_after[0] ) - def _test_context_filter_labels(self, context_filter): - user_id = self.register_user("kermit", "test") - tok = self.login("kermit", "test") + def test_messages_filter_labels(self): + """Test that we can filter by a label on a /messages request.""" + message_filter = json.dumps( + {"types": [EventTypes.Message], "org.matrix.labels": ["#fun"]} + ) - room_id = self.helper.create_room_as(user_id, tok=tok) + self._send_labelled_messages_in_room() + token = "s0_0_0_0_0_0_0_0_0" + request, channel = self.make_request( + "GET", + "/rooms/%s/messages?access_token=%s&from=%s&filter=%s" + % (self.room_id, self.tok, token, message_filter), + ) + self.render(request) + + events = channel.json_body["chunk"] + + self.assertEqual(len(events), 2, [event["content"] for event in events]) + self.assertEqual(events[0]["content"]["body"], "with right label", events[0]) + self.assertEqual(events[1]["content"]["body"], "with right label", events[1]) + + def test_messages_filter_not_labels(self): + """Test that we can filter by the absence of a label on a /messages request.""" + message_filter = json.dumps( + {"types": [EventTypes.Message], "org.matrix.not_labels": ["#fun"]} + ) + + self._send_labelled_messages_in_room() + + token = "s0_0_0_0_0_0_0_0_0" + request, channel = self.make_request( + "GET", + "/rooms/%s/messages?access_token=%s&from=%s&filter=%s" + % (self.room_id, self.tok, token, message_filter), + ) + self.render(request) + + events = channel.json_body["chunk"] + + self.assertEqual(len(events), 4, [event["content"] for event in events]) + self.assertEqual(events[0]["content"]["body"], "without label", events[0]) + self.assertEqual(events[1]["content"]["body"], "without label", events[1]) + self.assertEqual(events[2]["content"]["body"], "with wrong label", events[2]) + self.assertEqual( + events[3]["content"]["body"], "with two wrong labels", events[3] + ) + + def test_messages_filter_labels_not_labels(self): + """Test that we can filter by both a label and the absence of another label on a + /messages request. + """ + message_filter = json.dumps( + { + "types": [EventTypes.Message], + "org.matrix.labels": ["#work"], + "org.matrix.not_labels": ["#notfun"], + } + ) + + self._send_labelled_messages_in_room() + + token = "s0_0_0_0_0_0_0_0_0" + request, channel = self.make_request( + "GET", + "/rooms/%s/messages?access_token=%s&from=%s&filter=%s" + % (self.room_id, self.tok, token, message_filter), + ) + self.render(request) + + events = channel.json_body["chunk"] + + self.assertEqual(len(events), 1, [event["content"] for event in events]) + self.assertEqual(events[0]["content"]["body"], "with wrong label", events[0]) + + def _send_labelled_messages_in_room(self): self.helper.send_event( - room_id=room_id, + room_id=self.room_id, type=EventTypes.Message, content={ "msgtype": "m.text", "body": "with right label", EventContentFields.LABELS: ["#fun"], }, - tok=tok, + tok=self.tok, ) self.helper.send_event( - room_id=room_id, + room_id=self.room_id, type=EventTypes.Message, content={"msgtype": "m.text", "body": "without label"}, - tok=tok, + tok=self.tok, ) - # The event we'll look up the context for. res = self.helper.send_event( - room_id=room_id, + room_id=self.room_id, type=EventTypes.Message, - content={ - "msgtype": "m.text", - "body": "with right label", - EventContentFields.LABELS: ["#fun"], - }, - tok=tok, + content={"msgtype": "m.text", "body": "without label"}, + tok=self.tok, ) event_id = res["event_id"] self.helper.send_event( - room_id=room_id, + room_id=self.room_id, type=EventTypes.Message, content={ "msgtype": "m.text", "body": "with wrong label", EventContentFields.LABELS: ["#work"], }, - tok=tok, + tok=self.tok, ) self.helper.send_event( - room_id=room_id, + room_id=self.room_id, type=EventTypes.Message, content={ "msgtype": "m.text", "body": "with two wrong labels", EventContentFields.LABELS: ["#work", "#notfun"], }, - tok=tok, + tok=self.tok, ) self.helper.send_event( - room_id=room_id, + room_id=self.room_id, type=EventTypes.Message, content={ "msgtype": "m.text", "body": "with right label", EventContentFields.LABELS: ["#fun"], }, - tok=tok, + tok=self.tok, ) - request, channel = self.make_request( - "GET", - "/rooms/%s/context/%s?filter=%s" % (room_id, event_id, context_filter), - access_token=tok, - ) - self.render(request) - self.assertEqual(channel.code, 200, channel.result) - - return channel.json_body - + return event_id From 037360e6cf2ca181b7cf03884375d4a4d52ad64e Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 5 Nov 2019 14:33:18 +0000 Subject: [PATCH 0439/1623] Add tests for /search --- tests/rest/client/v1/test_rooms.py | 187 ++++++++++++++++++++++------- 1 file changed, 143 insertions(+), 44 deletions(-) diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py index fe327d1bf8..cc7499dcc0 100644 --- a/tests/rest/client/v1/test_rooms.py +++ b/tests/rest/client/v1/test_rooms.py @@ -1017,6 +1017,18 @@ class LabelsTestCase(unittest.HomeserverTestCase): profile.register_servlets, ] + # Filter that should only catch messages with the label "#fun". + FILTER_LABELS = {"types": [EventTypes.Message], "org.matrix.labels": ["#fun"]} + # Filter that should only catch messages without the label "#fun". + FILTER_NOT_LABELS = {"types": [EventTypes.Message], "org.matrix.not_labels": ["#fun"]} + # Filter that should only catch messages with the label "#work" but without the label + # "#notfun". + FILTER_LABELS_NOT_LABELS = { + "types": [EventTypes.Message], + "org.matrix.labels": ["#work"], + "org.matrix.not_labels": ["#notfun"], + } + def prepare(self, reactor, clock, homeserver): self.user_id = self.register_user("test", "test") self.tok = self.login("test", "test") @@ -1024,18 +1036,12 @@ class LabelsTestCase(unittest.HomeserverTestCase): def test_context_filter_labels(self): """Test that we can filter by a label on a /context request.""" - context_filter = json.dumps( - { - "types": [EventTypes.Message], - "org.matrix.labels": ["#fun"], - } - ) - event_id = self._send_labelled_messages_in_room() request, channel = self.make_request( "GET", - "/rooms/%s/context/%s?filter=%s" % (self.room_id, event_id, context_filter), + "/rooms/%s/context/%s?filter=%s" + % (self.room_id, event_id, json.dumps(self.FILTER_LABELS)), access_token=self.tok, ) self.render(request) @@ -1061,18 +1067,12 @@ class LabelsTestCase(unittest.HomeserverTestCase): def test_context_filter_not_labels(self): """Test that we can filter by the absence of a label on a /context request.""" - context_filter = json.dumps( - { - "types": [EventTypes.Message], - "org.matrix.not_labels": ["#fun"], - } - ) - event_id = self._send_labelled_messages_in_room() request, channel = self.make_request( "GET", - "/rooms/%s/context/%s?filter=%s" % (self.room_id, event_id, context_filter), + "/rooms/%s/context/%s?filter=%s" + % (self.room_id, event_id, json.dumps(self.FILTER_NOT_LABELS)), access_token=self.tok, ) self.render(request) @@ -1103,19 +1103,12 @@ class LabelsTestCase(unittest.HomeserverTestCase): """Test that we can filter by both a label and the absence of another label on a /context request. """ - context_filter = json.dumps( - { - "types": [EventTypes.Message], - "org.matrix.labels": ["#work"], - "org.matrix.not_labels": ["#notfun"], - } - ) - event_id = self._send_labelled_messages_in_room() request, channel = self.make_request( "GET", - "/rooms/%s/context/%s?filter=%s" % (self.room_id, event_id, context_filter), + "/rooms/%s/context/%s?filter=%s" + % (self.room_id, event_id, json.dumps(self.FILTER_LABELS_NOT_LABELS)), access_token=self.tok, ) self.render(request) @@ -1138,17 +1131,13 @@ class LabelsTestCase(unittest.HomeserverTestCase): def test_messages_filter_labels(self): """Test that we can filter by a label on a /messages request.""" - message_filter = json.dumps( - {"types": [EventTypes.Message], "org.matrix.labels": ["#fun"]} - ) - self._send_labelled_messages_in_room() token = "s0_0_0_0_0_0_0_0_0" request, channel = self.make_request( "GET", "/rooms/%s/messages?access_token=%s&from=%s&filter=%s" - % (self.room_id, self.tok, token, message_filter), + % (self.room_id, self.tok, token, json.dumps(self.FILTER_LABELS)), ) self.render(request) @@ -1160,17 +1149,13 @@ class LabelsTestCase(unittest.HomeserverTestCase): def test_messages_filter_not_labels(self): """Test that we can filter by the absence of a label on a /messages request.""" - message_filter = json.dumps( - {"types": [EventTypes.Message], "org.matrix.not_labels": ["#fun"]} - ) - self._send_labelled_messages_in_room() token = "s0_0_0_0_0_0_0_0_0" request, channel = self.make_request( "GET", "/rooms/%s/messages?access_token=%s&from=%s&filter=%s" - % (self.room_id, self.tok, token, message_filter), + % (self.room_id, self.tok, token, json.dumps(self.FILTER_NOT_LABELS)), ) self.render(request) @@ -1188,21 +1173,13 @@ class LabelsTestCase(unittest.HomeserverTestCase): """Test that we can filter by both a label and the absence of another label on a /messages request. """ - message_filter = json.dumps( - { - "types": [EventTypes.Message], - "org.matrix.labels": ["#work"], - "org.matrix.not_labels": ["#notfun"], - } - ) - self._send_labelled_messages_in_room() token = "s0_0_0_0_0_0_0_0_0" request, channel = self.make_request( "GET", "/rooms/%s/messages?access_token=%s&from=%s&filter=%s" - % (self.room_id, self.tok, token, message_filter), + % (self.room_id, self.tok, token, json.dumps(self.FILTER_LABELS_NOT_LABELS)), ) self.render(request) @@ -1211,7 +1188,128 @@ class LabelsTestCase(unittest.HomeserverTestCase): self.assertEqual(len(events), 1, [event["content"] for event in events]) self.assertEqual(events[0]["content"]["body"], "with wrong label", events[0]) + def test_search_filter_labels(self): + """Test that we can filter by a label on a /search request.""" + request_data = json.dumps({ + "search_categories": { + "room_events": { + "search_term": "label", + "filter": self.FILTER_LABELS, + } + } + }) + + self._send_labelled_messages_in_room() + + request, channel = self.make_request( + "POST", "/search?access_token=%s" % self.tok, request_data + ) + self.render(request) + + results = channel.json_body["search_categories"]["room_events"]["results"] + + self.assertEqual( + len(results), + 2, + [result["result"]["content"] for result in results], + ) + self.assertEqual( + results[0]["result"]["content"]["body"], + "with right label", + results[0]["result"]["content"]["body"], + ) + self.assertEqual( + results[1]["result"]["content"]["body"], + "with right label", + results[1]["result"]["content"]["body"], + ) + + def test_search_filter_not_labels(self): + """Test that we can filter by the absence of a label on a /search request.""" + request_data = json.dumps({ + "search_categories": { + "room_events": { + "search_term": "label", + "filter": self.FILTER_NOT_LABELS, + } + } + }) + + self._send_labelled_messages_in_room() + + request, channel = self.make_request( + "POST", "/search?access_token=%s" % self.tok, request_data + ) + self.render(request) + + results = channel.json_body["search_categories"]["room_events"]["results"] + + self.assertEqual( + len(results), + 4, + [result["result"]["content"] for result in results], + ) + self.assertEqual( + results[0]["result"]["content"]["body"], + "without label", + results[0]["result"]["content"]["body"], + ) + self.assertEqual( + results[1]["result"]["content"]["body"], + "without label", + results[1]["result"]["content"]["body"], + ) + self.assertEqual( + results[2]["result"]["content"]["body"], + "with wrong label", + results[2]["result"]["content"]["body"], + ) + self.assertEqual( + results[3]["result"]["content"]["body"], + "with two wrong labels", + results[3]["result"]["content"]["body"], + ) + + def test_search_filter_labels_not_labels(self): + """Test that we can filter by both a label and the absence of another label on a + /search request. + """ + request_data = json.dumps({ + "search_categories": { + "room_events": { + "search_term": "label", + "filter": self.FILTER_LABELS_NOT_LABELS, + } + } + }) + + self._send_labelled_messages_in_room() + + request, channel = self.make_request( + "POST", "/search?access_token=%s" % self.tok, request_data + ) + self.render(request) + + results = channel.json_body["search_categories"]["room_events"]["results"] + + self.assertEqual( + len(results), + 1, + [result["result"]["content"] for result in results], + ) + self.assertEqual( + results[0]["result"]["content"]["body"], + "with wrong label", + results[0]["result"]["content"]["body"], + ) + def _send_labelled_messages_in_room(self): + """Sends several messages to a room with different labels (or without any) to test + filtering by label. + + Returns: + The ID of the event to use if we're testing filtering on /context. + """ self.helper.send_event( room_id=self.room_id, type=EventTypes.Message, @@ -1236,6 +1334,7 @@ class LabelsTestCase(unittest.HomeserverTestCase): content={"msgtype": "m.text", "body": "without label"}, tok=self.tok, ) + # Return this event's ID when we test filtering in /context requests. event_id = res["event_id"] self.helper.send_event( From d8d808db64c3464924016fab88879085d6c63880 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 5 Nov 2019 14:42:05 +0000 Subject: [PATCH 0440/1623] Changelog --- changelog.d/6329.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6329.feature diff --git a/changelog.d/6329.feature b/changelog.d/6329.feature new file mode 100644 index 0000000000..78a187a1dc --- /dev/null +++ b/changelog.d/6329.feature @@ -0,0 +1 @@ +Implement label-based filtering on `/sync` and `/messages` ([MSC2326](https://github.com/matrix-org/matrix-doc/pull/2326)). From f5d8fdf0a71cadd4ded81e276cc57e9c0c195a2f Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 5 Nov 2019 14:44:25 +0000 Subject: [PATCH 0441/1623] Update changelog --- changelog.d/6310.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/6310.feature b/changelog.d/6310.feature index b7ff3fad3b..78a187a1dc 100644 --- a/changelog.d/6310.feature +++ b/changelog.d/6310.feature @@ -1 +1 @@ -Implement label-based filtering. +Implement label-based filtering on `/sync` and `/messages` ([MSC2326](https://github.com/matrix-org/matrix-doc/pull/2326)). From 8822b331114a2f6fdcd5916f0c91991c0acae07e Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 5 Nov 2019 10:56:39 +0000 Subject: [PATCH 0442/1623] Update copyrights --- synapse/api/constants.py | 3 ++- synapse/api/filtering.py | 3 +++ synapse/rest/client/versions.py | 3 +++ synapse/storage/data_stores/main/stream.py | 3 +++ tests/api/test_filtering.py | 3 +++ tests/rest/client/v1/test_rooms.py | 2 ++ tests/rest/client/v1/utils.py | 3 +++ tests/rest/client/v2_alpha/test_sync.py | 3 ++- 8 files changed, 21 insertions(+), 2 deletions(-) diff --git a/synapse/api/constants.py b/synapse/api/constants.py index 49c4b85054..312acff3d6 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2017 Vector Creations Ltd -# Copyright 2018 New Vector Ltd +# Copyright 2018-2019 New Vector Ltd +# Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/api/filtering.py b/synapse/api/filtering.py index bec13f08d8..6eab1f13f0 100644 --- a/synapse/api/filtering.py +++ b/synapse/api/filtering.py @@ -1,5 +1,8 @@ # -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd +# Copyright 2017 Vector Creations Ltd +# Copyright 2018-2019 New Vector Ltd +# Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/rest/client/versions.py b/synapse/rest/client/versions.py index bb30ce3f34..2a477ad22e 100644 --- a/synapse/rest/client/versions.py +++ b/synapse/rest/client/versions.py @@ -1,5 +1,8 @@ # -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd +# Copyright 2017 Vector Creations Ltd +# Copyright 2018-2019 New Vector Ltd +# Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/storage/data_stores/main/stream.py b/synapse/storage/data_stores/main/stream.py index 616ef91d4e..9cac664880 100644 --- a/synapse/storage/data_stores/main/stream.py +++ b/synapse/storage/data_stores/main/stream.py @@ -1,5 +1,8 @@ # -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd +# Copyright 2017 Vector Creations Ltd +# Copyright 2018-2019 New Vector Ltd +# Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/api/test_filtering.py b/tests/api/test_filtering.py index 2dc5052249..63d8633582 100644 --- a/tests/api/test_filtering.py +++ b/tests/api/test_filtering.py @@ -1,5 +1,8 @@ # -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd +# Copyright 2017 Vector Creations Ltd +# Copyright 2018-2019 New Vector Ltd +# Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py index cc7499dcc0..b2c1ef6f0e 100644 --- a/tests/rest/client/v1/test_rooms.py +++ b/tests/rest/client/v1/test_rooms.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd +# Copyright 2017 Vector Creations Ltd +# Copyright 2018-2019 New Vector Ltd # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/rest/client/v1/utils.py b/tests/rest/client/v1/utils.py index 8ea0cb05ea..e7417b3d14 100644 --- a/tests/rest/client/v1/utils.py +++ b/tests/rest/client/v1/utils.py @@ -1,5 +1,8 @@ # -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd +# Copyright 2017 Vector Creations Ltd +# Copyright 2018-2019 New Vector Ltd +# Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/rest/client/v2_alpha/test_sync.py b/tests/rest/client/v2_alpha/test_sync.py index 3283c0e47b..661c1f88b9 100644 --- a/tests/rest/client/v2_alpha/test_sync.py +++ b/tests/rest/client/v2_alpha/test_sync.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright 2018 New Vector +# Copyright 2018-2019 New Vector Ltd +# Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. From a6863da24934dcbb2ae09a9e0b6e37140ef390ff Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 5 Nov 2019 14:50:19 +0000 Subject: [PATCH 0443/1623] Lint --- tests/rest/client/v1/test_rooms.py | 71 +++++++++++++++++------------- 1 file changed, 41 insertions(+), 30 deletions(-) diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py index b2c1ef6f0e..c5d67fc1cd 100644 --- a/tests/rest/client/v1/test_rooms.py +++ b/tests/rest/client/v1/test_rooms.py @@ -1020,9 +1020,15 @@ class LabelsTestCase(unittest.HomeserverTestCase): ] # Filter that should only catch messages with the label "#fun". - FILTER_LABELS = {"types": [EventTypes.Message], "org.matrix.labels": ["#fun"]} + FILTER_LABELS = { + "types": [EventTypes.Message], + "org.matrix.labels": ["#fun"], + } # Filter that should only catch messages without the label "#fun". - FILTER_NOT_LABELS = {"types": [EventTypes.Message], "org.matrix.not_labels": ["#fun"]} + FILTER_NOT_LABELS = { + "types": [EventTypes.Message], + "org.matrix.not_labels": ["#fun"], + } # Filter that should only catch messages with the label "#work" but without the label # "#notfun". FILTER_LABELS_NOT_LABELS = { @@ -1181,7 +1187,12 @@ class LabelsTestCase(unittest.HomeserverTestCase): request, channel = self.make_request( "GET", "/rooms/%s/messages?access_token=%s&from=%s&filter=%s" - % (self.room_id, self.tok, token, json.dumps(self.FILTER_LABELS_NOT_LABELS)), + % ( + self.room_id, + self.tok, + token, + json.dumps(self.FILTER_LABELS_NOT_LABELS), + ), ) self.render(request) @@ -1192,14 +1203,16 @@ class LabelsTestCase(unittest.HomeserverTestCase): def test_search_filter_labels(self): """Test that we can filter by a label on a /search request.""" - request_data = json.dumps({ - "search_categories": { - "room_events": { - "search_term": "label", - "filter": self.FILTER_LABELS, + request_data = json.dumps( + { + "search_categories": { + "room_events": { + "search_term": "label", + "filter": self.FILTER_LABELS, + } } } - }) + ) self._send_labelled_messages_in_room() @@ -1211,9 +1224,7 @@ class LabelsTestCase(unittest.HomeserverTestCase): results = channel.json_body["search_categories"]["room_events"]["results"] self.assertEqual( - len(results), - 2, - [result["result"]["content"] for result in results], + len(results), 2, [result["result"]["content"] for result in results], ) self.assertEqual( results[0]["result"]["content"]["body"], @@ -1228,14 +1239,16 @@ class LabelsTestCase(unittest.HomeserverTestCase): def test_search_filter_not_labels(self): """Test that we can filter by the absence of a label on a /search request.""" - request_data = json.dumps({ - "search_categories": { - "room_events": { - "search_term": "label", - "filter": self.FILTER_NOT_LABELS, + request_data = json.dumps( + { + "search_categories": { + "room_events": { + "search_term": "label", + "filter": self.FILTER_NOT_LABELS, + } } } - }) + ) self._send_labelled_messages_in_room() @@ -1247,9 +1260,7 @@ class LabelsTestCase(unittest.HomeserverTestCase): results = channel.json_body["search_categories"]["room_events"]["results"] self.assertEqual( - len(results), - 4, - [result["result"]["content"] for result in results], + len(results), 4, [result["result"]["content"] for result in results], ) self.assertEqual( results[0]["result"]["content"]["body"], @@ -1276,14 +1287,16 @@ class LabelsTestCase(unittest.HomeserverTestCase): """Test that we can filter by both a label and the absence of another label on a /search request. """ - request_data = json.dumps({ - "search_categories": { - "room_events": { - "search_term": "label", - "filter": self.FILTER_LABELS_NOT_LABELS, + request_data = json.dumps( + { + "search_categories": { + "room_events": { + "search_term": "label", + "filter": self.FILTER_LABELS_NOT_LABELS, + } } } - }) + ) self._send_labelled_messages_in_room() @@ -1295,9 +1308,7 @@ class LabelsTestCase(unittest.HomeserverTestCase): results = channel.json_body["search_categories"]["room_events"]["results"] self.assertEqual( - len(results), - 1, - [result["result"]["content"] for result in results], + len(results), 1, [result["result"]["content"] for result in results], ) self.assertEqual( results[0]["result"]["content"]["body"], From f141af4c79b2be8e87d683420e2d8117e2a8525c Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 5 Nov 2019 14:52:38 +0000 Subject: [PATCH 0444/1623] Update copyright --- synapse/handlers/room.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 899bb63114..f6e162484c 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Copyright 2014 - 2016 OpenMarket Ltd -# Copyright 2018 New Vector Ltd +# Copyright 2018-2019 New Vector Ltd +# Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. From cb2cbe4d26b5d0c082c82a62260c0c05afde8aeb Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 5 Nov 2019 15:27:38 +0000 Subject: [PATCH 0445/1623] Only filter if a filter was provided --- synapse/handlers/room.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index f6e162484c..f47237b3fb 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -875,10 +875,12 @@ class RoomContextHandler(object): room_id, event_id, before_limit, after_limit, event_filter ) - filtered_before_events = event_filter.filter(results["events_before"]) - results["events_before"] = yield filter_evts(filtered_before_events) - filtered_after_events = event_filter.filter(results["events_after"]) - results["events_after"] = yield filter_evts(filtered_after_events) + if event_filter: + results["events_before"] = event_filter.filter(results["events_before"]) + results["events_after"] = event_filter.filter(results["events_after"]) + + results["events_before"] = yield filter_evts(results["events_before"]) + results["events_after"] = yield filter_evts(results["events_after"]) results["event"] = event if results["events_after"]: From c16e192e2f9970cc62adfd758034244631968102 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 5 Nov 2019 15:49:43 +0000 Subject: [PATCH 0446/1623] Fix caching devices for remote servers in worker. When the `/keys/query` API is hit on client_reader worker Synapse may decide that it needs to resync some remote deivces. Usually this happens on master, and then gets cached. However, that fails on workers and so it falls back to fetching devices from remotes directly, which may in turn fail if the remote is down. --- synapse/handlers/e2e_keys.py | 19 ++++++-- synapse/replication/http/__init__.py | 10 +++- synapse/replication/http/devices.py | 69 ++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 4 deletions(-) create mode 100644 synapse/replication/http/devices.py diff --git a/synapse/handlers/e2e_keys.py b/synapse/handlers/e2e_keys.py index f09a0b73c8..28c12753c1 100644 --- a/synapse/handlers/e2e_keys.py +++ b/synapse/handlers/e2e_keys.py @@ -30,6 +30,7 @@ from twisted.internet import defer from synapse.api.errors import CodeMessageException, Codes, NotFoundError, SynapseError from synapse.logging.context import make_deferred_yieldable, run_in_background from synapse.logging.opentracing import log_kv, set_tag, tag_args, trace +from synapse.replication.http.devices import ReplicationUserDevicesResyncRestServlet from synapse.types import ( UserID, get_domain_from_id, @@ -53,6 +54,12 @@ class E2eKeysHandler(object): self._edu_updater = SigningKeyEduUpdater(hs, self) + self._is_master = hs.config.worker_app is None + if not self._is_master: + self._user_device_resync_client = ReplicationUserDevicesResyncRestServlet.make_client( + hs + ) + federation_registry = hs.get_federation_registry() # FIXME: switch to m.signing_key_update when MSC1756 is merged into the spec @@ -191,9 +198,15 @@ class E2eKeysHandler(object): # probably be tracking their device lists. However, we haven't # done an initial sync on the device list so we do it now. try: - user_devices = yield self.device_handler.device_list_updater.user_device_resync( - user_id - ) + if self._is_master: + user_devices = yield self.device_handler.device_list_updater.user_device_resync( + user_id + ) + else: + user_devices = yield self._user_device_resync_client( + user_id=user_id + ) + user_devices = user_devices["devices"] for device in user_devices: results[user_id] = {device["device_id"]: device["keys"]} diff --git a/synapse/replication/http/__init__.py b/synapse/replication/http/__init__.py index 81b85352b1..28dbc6fcba 100644 --- a/synapse/replication/http/__init__.py +++ b/synapse/replication/http/__init__.py @@ -14,7 +14,14 @@ # limitations under the License. from synapse.http.server import JsonResource -from synapse.replication.http import federation, login, membership, register, send_event +from synapse.replication.http import ( + devices, + federation, + login, + membership, + register, + send_event, +) REPLICATION_PREFIX = "/_synapse/replication" @@ -30,3 +37,4 @@ class ReplicationRestResource(JsonResource): federation.register_servlets(hs, self) login.register_servlets(hs, self) register.register_servlets(hs, self) + devices.register_servlets(hs, self) diff --git a/synapse/replication/http/devices.py b/synapse/replication/http/devices.py new file mode 100644 index 0000000000..795ca7b65e --- /dev/null +++ b/synapse/replication/http/devices.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 New Vector Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from synapse.replication.http._base import ReplicationEndpoint + +logger = logging.getLogger(__name__) + + +class ReplicationUserDevicesResyncRestServlet(ReplicationEndpoint): + """Notifies that a user has joined or left the room + + Request format: + + POST /_synapse/replication/user_device_resync/:user_id + + {} + + Response is equivalent to ` /_matrix/federation/v1/user/devices/:user_id` + response, e.g.: + + { + "user_id": "@alice:example.org", + "devices": [ + { + "device_id": "JLAFKJWSCS", + "keys": { ... }, + "device_display_name": "Alice's Mobile Phone" + } + ] + } + """ + + NAME = "user_device_resync" + PATH_ARGS = ("user_id",) + CACHE = False + + def __init__(self, hs): + super(ReplicationUserDevicesResyncRestServlet, self).__init__(hs) + + self.device_list_updater = hs.get_device_handler().device_list_updater + self.store = hs.get_datastore() + self.clock = hs.get_clock() + + @staticmethod + def _serialize_payload(user_id): + return {} + + async def _handle_request(self, request, user_id): + user_devices = await self.device_list_updater.user_device_resync(user_id) + + return 200, user_devices + + +def register_servlets(hs, http_server): + ReplicationUserDevicesResyncRestServlet(hs).register(http_server) From e9bfe719ba1928dc191cea93120c5c8a89584434 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 5 Nov 2019 15:45:17 +0000 Subject: [PATCH 0447/1623] Strip overlong OpenGraph data from url preview ... to stop people causing DoSes with malicious web pages --- changelog.d/6331.feature | 1 + synapse/rest/media/v1/preview_url_resource.py | 20 ++++++++++- tests/rest/media/v1/test_url_preview.py | 34 +++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 changelog.d/6331.feature diff --git a/changelog.d/6331.feature b/changelog.d/6331.feature new file mode 100644 index 0000000000..eaf69ef3f6 --- /dev/null +++ b/changelog.d/6331.feature @@ -0,0 +1 @@ +Limit the length of data returned by url previews, to prevent DoS attacks. diff --git a/synapse/rest/media/v1/preview_url_resource.py b/synapse/rest/media/v1/preview_url_resource.py index 0c68c3aad5..6d8c39a410 100644 --- a/synapse/rest/media/v1/preview_url_resource.py +++ b/synapse/rest/media/v1/preview_url_resource.py @@ -56,6 +56,9 @@ logger = logging.getLogger(__name__) _charset_match = re.compile(br"<\s*meta[^>]*charset\s*=\s*([a-z0-9-]+)", flags=re.I) _content_type_match = re.compile(r'.*; *charset="?(.*?)"?(;|$)', flags=re.I) +OG_TAG_NAME_MAXLEN = 50 +OG_TAG_VALUE_MAXLEN = 1000 + class PreviewUrlResource(DirectServeResource): isLeaf = True @@ -167,7 +170,7 @@ class PreviewUrlResource(DirectServeResource): ts (int): Returns: - Deferred[str]: json-encoded og data + Deferred[bytes]: json-encoded og data """ # check the URL cache in the DB (which will also provide us with # historical previews, if we have any) @@ -268,6 +271,17 @@ class PreviewUrlResource(DirectServeResource): logger.warn("Failed to find any OG data in %s", url) og = {} + # filter out any stupidly long values + keys_to_remove = [] + for k, v in og.items(): + if len(k) > OG_TAG_NAME_MAXLEN or len(v) > OG_TAG_VALUE_MAXLEN: + logger.warning( + "Pruning overlong tag %s from OG data", k[:OG_TAG_NAME_MAXLEN] + ) + keys_to_remove.append(k) + for k in keys_to_remove: + del og[k] + logger.debug("Calculated OG for %s as %s" % (url, og)) jsonog = json.dumps(og) @@ -502,6 +516,10 @@ def _calc_og(tree, media_uri): og = {} for tag in tree.xpath("//*/meta[starts-with(@property, 'og:')]"): if "content" in tag.attrib: + # if we've got more than 50 tags, someone is taking the piss + if len(og) >= 50: + logger.warning("skipping OG for page with too many og: tags") + return {} og[tag.attrib["property"]] = tag.attrib["content"] # TODO: grab article: meta tags too, e.g.: diff --git a/tests/rest/media/v1/test_url_preview.py b/tests/rest/media/v1/test_url_preview.py index 976652aee8..da19a8e86f 100644 --- a/tests/rest/media/v1/test_url_preview.py +++ b/tests/rest/media/v1/test_url_preview.py @@ -247,6 +247,40 @@ class URLPreviewTests(unittest.HomeserverTestCase): self.assertEqual(channel.code, 200) self.assertEqual(channel.json_body["og:title"], "\u0434\u043a\u0430") + def test_overlong_title(self): + self.lookups["matrix.org"] = [(IPv4Address, "8.8.8.8")] + + end_content = ( + b"" + b"" + b"x" * 2000 + b"" + b'' + b"" + ) + + request, channel = self.make_request( + "GET", "url_preview?url=http://matrix.org", shorthand=False + ) + request.render(self.preview_url) + self.pump() + + client = self.reactor.tcpClients[0][2].buildProtocol(None) + server = AccumulatingProtocol() + server.makeConnection(FakeTransport(client, self.reactor)) + client.makeConnection(FakeTransport(server, self.reactor)) + client.dataReceived( + ( + b"HTTP/1.0 200 OK\r\nContent-Length: %d\r\n" + b'Content-Type: text/html; charset="windows-1251"\r\n\r\n' + ) + % (len(end_content),) + + end_content + ) + + self.pump() + self.assertEqual(channel.code, 200) + res = channel.json_body + self.assertCountEqual(["og:description"], res.keys()) + def test_ipaddr(self): """ IP addresses can be previewed directly. From 248111bae8600a3e2d4da49c6e64b72c76219850 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 5 Nov 2019 15:54:23 +0000 Subject: [PATCH 0448/1623] Newsfile --- changelog.d/6332.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6332.bugfix diff --git a/changelog.d/6332.bugfix b/changelog.d/6332.bugfix new file mode 100644 index 0000000000..b14bd7e43c --- /dev/null +++ b/changelog.d/6332.bugfix @@ -0,0 +1 @@ +Fix caching devices for remote users when using workers. From e78167c94b3f63136f7d0e4f32a05ad1befdc0ec Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 5 Nov 2019 16:46:39 +0000 Subject: [PATCH 0449/1623] Apply suggestions from code review Co-Authored-By: Brendan Abolivier Co-Authored-By: Erik Johnston --- synapse/rest/media/v1/preview_url_resource.py | 2 +- tests/rest/media/v1/test_url_preview.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/synapse/rest/media/v1/preview_url_resource.py b/synapse/rest/media/v1/preview_url_resource.py index 6d8c39a410..4d4b3c1462 100644 --- a/synapse/rest/media/v1/preview_url_resource.py +++ b/synapse/rest/media/v1/preview_url_resource.py @@ -518,7 +518,7 @@ def _calc_og(tree, media_uri): if "content" in tag.attrib: # if we've got more than 50 tags, someone is taking the piss if len(og) >= 50: - logger.warning("skipping OG for page with too many og: tags") + logger.warning("Skipping OG for page with too many 'og:' tags") return {} og[tag.attrib["property"]] = tag.attrib["content"] diff --git a/tests/rest/media/v1/test_url_preview.py b/tests/rest/media/v1/test_url_preview.py index da19a8e86f..852b8ab11c 100644 --- a/tests/rest/media/v1/test_url_preview.py +++ b/tests/rest/media/v1/test_url_preview.py @@ -279,6 +279,7 @@ class URLPreviewTests(unittest.HomeserverTestCase): self.pump() self.assertEqual(channel.code, 200) res = channel.json_body + # We should only see the `og:description` field, as `title` is too long and should be stripped out self.assertCountEqual(["og:description"], res.keys()) def test_ipaddr(self): From 81d49cbb07a4dc5a673e31a8a626af6e8a18f801 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 5 Nov 2019 17:22:58 +0000 Subject: [PATCH 0450/1623] Fix exception when OpenGraph tag values are ints --- changelog.d/6334.feature | 1 + synapse/rest/media/v1/preview_url_resource.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 changelog.d/6334.feature diff --git a/changelog.d/6334.feature b/changelog.d/6334.feature new file mode 100644 index 0000000000..eaf69ef3f6 --- /dev/null +++ b/changelog.d/6334.feature @@ -0,0 +1 @@ +Limit the length of data returned by url previews, to prevent DoS attacks. diff --git a/synapse/rest/media/v1/preview_url_resource.py b/synapse/rest/media/v1/preview_url_resource.py index 4d4b3c1462..ec9c4619c9 100644 --- a/synapse/rest/media/v1/preview_url_resource.py +++ b/synapse/rest/media/v1/preview_url_resource.py @@ -274,7 +274,8 @@ class PreviewUrlResource(DirectServeResource): # filter out any stupidly long values keys_to_remove = [] for k, v in og.items(): - if len(k) > OG_TAG_NAME_MAXLEN or len(v) > OG_TAG_VALUE_MAXLEN: + # values can be numeric as well as strings, hence the cast to str + if len(k) > OG_TAG_NAME_MAXLEN or len(str(v)) > OG_TAG_VALUE_MAXLEN: logger.warning( "Pruning overlong tag %s from OG data", k[:OG_TAG_NAME_MAXLEN] ) From 052513958de214eed9508e60fef707641fb34da4 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 5 Nov 2019 17:44:09 +0000 Subject: [PATCH 0451/1623] Fix phone home stats --- synapse/app/homeserver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py index 00a7f8330e..73e2c29d06 100644 --- a/synapse/app/homeserver.py +++ b/synapse/app/homeserver.py @@ -636,7 +636,7 @@ def run(hs): if hs.config.report_stats: logger.info("Scheduling stats reporting for 3 hour intervals") - clock.looping_call(start_phone_stats_home, 3 * 60 * 60 * 1000, hs, stats) + clock.looping_call(start_phone_stats_home, 3 * 60 * 60 * 1000) # We need to defer this init for the cases that we daemonize # otherwise the process ID we get is that of the non-daemon process @@ -644,7 +644,7 @@ def run(hs): # We wait 5 minutes to send the first set of stats as the server can # be quite busy the first few minutes - clock.call_later(5 * 60, start_phone_stats_home, hs, stats) + clock.call_later(5 * 60, start_phone_stats_home) _base.start_reactor( "synapse-homeserver", From b437eb48b6bde1cd908d472893c5638e021c6a8f Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 5 Nov 2019 17:45:29 +0000 Subject: [PATCH 0452/1623] Newsfile --- changelog.d/6336.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6336.misc diff --git a/changelog.d/6336.misc b/changelog.d/6336.misc new file mode 100644 index 0000000000..63527ccef4 --- /dev/null +++ b/changelog.d/6336.misc @@ -0,0 +1 @@ +Remove the dependency on psutil and replace functionality with the stdlib `resource` module. From 0e3ab8afdc2b89ac2f47878112d93dd03d01f7ef Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 5 Nov 2019 22:13:37 +0000 Subject: [PATCH 0453/1623] Add some checks that we aren't using state from rejected events (#6330) * Raise an exception if accessing state for rejected events Add some sanity checks on accessing state_group etc for rejected events. * Skip calculating push actions for rejected events It didn't actually cause any bugs, because rejected events get filtered out at various later points, but there's not point in trying to calculate the push actions for a rejected event. --- changelog.d/6330.misc | 1 + synapse/events/snapshot.py | 49 ++++++++++++++++++++++++++++++---- synapse/handlers/federation.py | 6 ++++- 3 files changed, 50 insertions(+), 6 deletions(-) create mode 100644 changelog.d/6330.misc diff --git a/changelog.d/6330.misc b/changelog.d/6330.misc new file mode 100644 index 0000000000..6239cba263 --- /dev/null +++ b/changelog.d/6330.misc @@ -0,0 +1 @@ +Add some checks that we aren't using state from rejected events. diff --git a/synapse/events/snapshot.py b/synapse/events/snapshot.py index 5f07f6fe4b..0f3c5989cb 100644 --- a/synapse/events/snapshot.py +++ b/synapse/events/snapshot.py @@ -33,7 +33,7 @@ class EventContext: Attributes: rejected: A rejection reason if the event was rejected, else False - state_group: The ID of the state group for this event. Note that state events + _state_group: The ID of the state group for this event. Note that state events are persisted with a state group which includes the new event, so this is effectively the state *after* the event in question. @@ -45,6 +45,9 @@ class EventContext: For an outlier, where we don't have the state at the event, this will be None. + Note that this is a private attribute: it should be accessed via + the ``state_group`` property. + prev_group: If it is known, ``state_group``'s prev_group. Note that this being None does not necessarily mean that ``state_group`` does not have a prev_group! @@ -88,7 +91,7 @@ class EventContext: """ rejected = attr.ib(default=False, type=Union[bool, str]) - state_group = attr.ib(default=None, type=Optional[int]) + _state_group = attr.ib(default=None, type=Optional[int]) prev_group = attr.ib(default=None, type=Optional[int]) delta_ids = attr.ib(default=None, type=Optional[Dict[Tuple[str, str], str]]) app_service = attr.ib(default=None, type=Optional[ApplicationService]) @@ -136,7 +139,7 @@ class EventContext: "prev_state_id": prev_state_id, "event_type": event.type, "event_state_key": event.state_key if event.is_state() else None, - "state_group": self.state_group, + "state_group": self._state_group, "rejected": self.rejected, "prev_group": self.prev_group, "delta_ids": _encode_state_dict(self.delta_ids), @@ -173,22 +176,52 @@ class EventContext: return context + @property + def state_group(self) -> Optional[int]: + """The ID of the state group for this event. + + Note that state events are persisted with a state group which includes the new + event, so this is effectively the state *after* the event in question. + + For an outlier, where we don't have the state at the event, this will be None. + + It is an error to access this for a rejected event, since rejected state should + not make it into the room state. Accessing this property will raise an exception + if ``rejected`` is set. + """ + if self.rejected: + raise RuntimeError("Attempt to access state_group of rejected event") + + return self._state_group + @defer.inlineCallbacks def get_current_state_ids(self, store): - """Gets the current state IDs + """ + Gets the room state map, including this event - ie, the state in ``state_group`` + + It is an error to access this for a rejected event, since rejected state should + not make it into the room state. This method will raise an exception if + ``rejected`` is set. Returns: Deferred[dict[(str, str), str]|None]: Returns None if state_group is None, which happens when the associated event is an outlier. + Maps a (type, state_key) to the event ID of the state event matching this tuple. """ + if self.rejected: + raise RuntimeError("Attempt to access state_ids of rejected event") + yield self._ensure_fetched(store) return self._current_state_ids @defer.inlineCallbacks def get_prev_state_ids(self, store): - """Gets the prev state IDs + """ + Gets the room state map, excluding this event. + + For a non-state event, this will be the same as get_current_state_ids(). Returns: Deferred[dict[(str, str), str]|None]: Returns None if state_group @@ -202,11 +235,17 @@ class EventContext: def get_cached_current_state_ids(self): """Gets the current state IDs if we have them already cached. + It is an error to access this for a rejected event, since rejected state should + not make it into the room state. This method will raise an exception if + ``rejected`` is set. + Returns: dict[(str, str), str]|None: Returns None if we haven't cached the state or if state_group is None, which happens when the associated event is an outlier. """ + if self.rejected: + raise RuntimeError("Attempt to access state_ids of rejected event") return self._current_state_ids diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 8cafcfdab0..b7916de909 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1688,7 +1688,11 @@ class FederationHandler(BaseHandler): # hack around with a try/finally instead. success = False try: - if not event.internal_metadata.is_outlier() and not backfilled: + if ( + not event.internal_metadata.is_outlier() + and not backfilled + and not context.rejected + ): yield self.action_generator.handle_push_actions_for_event( event, context ) From 807ec3bd99908d2991d2b3d0615b0862610c6dc3 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 6 Nov 2019 10:01:39 +0000 Subject: [PATCH 0454/1623] Fix bug which caused rejected events to be stored with the wrong room state (#6320) Fixes a bug where rejected events were persisted with the wrong state group. Also fixes an occasional internal-server-error when receiving events over federation which are rejected and (possibly because they are backwards-extremities) have no prev_group. Fixes #6289. --- changelog.d/6320.bugfix | 1 + synapse/events/snapshot.py | 25 +++- synapse/handlers/federation.py | 1 + synapse/state/__init__.py | 168 +++++++++++----------- synapse/storage/data_stores/main/state.py | 2 +- tests/handlers/test_federation.py | 126 ++++++++++++++++ tests/test_state.py | 61 ++++++-- 7 files changed, 283 insertions(+), 101 deletions(-) create mode 100644 changelog.d/6320.bugfix diff --git a/changelog.d/6320.bugfix b/changelog.d/6320.bugfix new file mode 100644 index 0000000000..2c3fad5655 --- /dev/null +++ b/changelog.d/6320.bugfix @@ -0,0 +1 @@ +Fix bug which casued rejected events to be persisted with the wrong room state. diff --git a/synapse/events/snapshot.py b/synapse/events/snapshot.py index 0f3c5989cb..64e898f40c 100644 --- a/synapse/events/snapshot.py +++ b/synapse/events/snapshot.py @@ -48,10 +48,21 @@ class EventContext: Note that this is a private attribute: it should be accessed via the ``state_group`` property. + state_group_before_event: The ID of the state group representing the state + of the room before this event. + + If this is a non-state event, this will be the same as ``state_group``. If + it's a state event, it will be the same as ``prev_group``. + + If ``state_group`` is None (ie, the event is an outlier), + ``state_group_before_event`` will always also be ``None``. + prev_group: If it is known, ``state_group``'s prev_group. Note that this being None does not necessarily mean that ``state_group`` does not have a prev_group! + If the event is a state event, this is normally the same as ``prev_group``. + If ``state_group`` is None (ie, the event is an outlier), ``prev_group`` will always also be ``None``. @@ -77,7 +88,8 @@ class EventContext: ``get_current_state_ids``. _AsyncEventContext impl calculates this on-demand: it will be None until that happens. - _prev_state_ids: The room state map, excluding this event. For a non-state + _prev_state_ids: The room state map, excluding this event - ie, the state + in ``state_group_before_event``. For a non-state event, this will be the same as _current_state_events. Note that it is a completely different thing to prev_group! @@ -92,6 +104,7 @@ class EventContext: rejected = attr.ib(default=False, type=Union[bool, str]) _state_group = attr.ib(default=None, type=Optional[int]) + state_group_before_event = attr.ib(default=None, type=Optional[int]) prev_group = attr.ib(default=None, type=Optional[int]) delta_ids = attr.ib(default=None, type=Optional[Dict[Tuple[str, str], str]]) app_service = attr.ib(default=None, type=Optional[ApplicationService]) @@ -103,12 +116,18 @@ class EventContext: @staticmethod def with_state( - state_group, current_state_ids, prev_state_ids, prev_group=None, delta_ids=None + state_group, + state_group_before_event, + current_state_ids, + prev_state_ids, + prev_group=None, + delta_ids=None, ): return EventContext( current_state_ids=current_state_ids, prev_state_ids=prev_state_ids, state_group=state_group, + state_group_before_event=state_group_before_event, prev_group=prev_group, delta_ids=delta_ids, ) @@ -140,6 +159,7 @@ class EventContext: "event_type": event.type, "event_state_key": event.state_key if event.is_state() else None, "state_group": self._state_group, + "state_group_before_event": self.state_group_before_event, "rejected": self.rejected, "prev_group": self.prev_group, "delta_ids": _encode_state_dict(self.delta_ids), @@ -165,6 +185,7 @@ class EventContext: event_type=input["event_type"], event_state_key=input["event_state_key"], state_group=input["state_group"], + state_group_before_event=input["state_group_before_event"], prev_group=input["prev_group"], delta_ids=_decode_state_dict(input["delta_ids"]), rejected=input["rejected"], diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index b7916de909..05dd8d2671 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -2280,6 +2280,7 @@ class FederationHandler(BaseHandler): return EventContext.with_state( state_group=state_group, + state_group_before_event=context.state_group_before_event, current_state_ids=current_state_ids, prev_state_ids=prev_state_ids, prev_group=prev_group, diff --git a/synapse/state/__init__.py b/synapse/state/__init__.py index 2c04ab1854..139beef8ed 100644 --- a/synapse/state/__init__.py +++ b/synapse/state/__init__.py @@ -16,6 +16,7 @@ import logging from collections import namedtuple +from typing import Iterable, Optional from six import iteritems, itervalues @@ -27,6 +28,7 @@ from twisted.internet import defer from synapse.api.constants import EventTypes from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, StateResolutionVersions +from synapse.events import EventBase from synapse.events.snapshot import EventContext from synapse.logging.utils import log_function from synapse.state import v1, v2 @@ -212,15 +214,17 @@ class StateHandler(object): return joined_hosts @defer.inlineCallbacks - def compute_event_context(self, event, old_state=None): + def compute_event_context( + self, event: EventBase, old_state: Optional[Iterable[EventBase]] = None + ): """Build an EventContext structure for the event. This works out what the current state should be for the event, and generates a new state group if necessary. Args: - event (synapse.events.EventBase): - old_state (dict|None): The state at the event if it can't be + event: + old_state: The state at the event if it can't be calculated from existing events. This is normally only specified when receiving an event from federation where we don't have the prev events for, e.g. when backfilling. @@ -251,113 +255,103 @@ class StateHandler(object): # group for it. context = EventContext.with_state( state_group=None, + state_group_before_event=None, current_state_ids=current_state_ids, prev_state_ids=prev_state_ids, ) return context + # + # first of all, figure out the state before the event + # + if old_state: - # We already have the state, so we don't need to calculate it. - # Let's just correctly fill out the context and create a - # new state group for it. + # if we're given the state before the event, then we use that + state_ids_before_event = { + (s.type, s.state_key): s.event_id for s in old_state + } + state_group_before_event = None + state_group_before_event_prev_group = None + deltas_to_state_group_before_event = None - prev_state_ids = {(s.type, s.state_key): s.event_id for s in old_state} + else: + # otherwise, we'll need to resolve the state across the prev_events. + logger.debug("calling resolve_state_groups from compute_event_context") - if event.is_state(): - key = (event.type, event.state_key) - if key in prev_state_ids: - replaces = prev_state_ids[key] - if replaces != event.event_id: # Paranoia check - event.unsigned["replaces_state"] = replaces - current_state_ids = dict(prev_state_ids) - current_state_ids[key] = event.event_id - else: - current_state_ids = prev_state_ids + entry = yield self.resolve_state_groups_for_events( + event.room_id, event.prev_event_ids() + ) - state_group = yield self.state_store.store_state_group( + state_ids_before_event = entry.state + state_group_before_event = entry.state_group + state_group_before_event_prev_group = entry.prev_group + deltas_to_state_group_before_event = entry.delta_ids + + # + # make sure that we have a state group at that point. If it's not a state event, + # that will be the state group for the new event. If it *is* a state event, + # it might get rejected (in which case we'll need to persist it with the + # previous state group) + # + + if not state_group_before_event: + state_group_before_event = yield self.state_store.store_state_group( event.event_id, event.room_id, - prev_group=None, - delta_ids=None, - current_state_ids=current_state_ids, + prev_group=state_group_before_event_prev_group, + delta_ids=deltas_to_state_group_before_event, + current_state_ids=state_ids_before_event, ) - context = EventContext.with_state( - state_group=state_group, - current_state_ids=current_state_ids, - prev_state_ids=prev_state_ids, + # XXX: can we update the state cache entry for the new state group? or + # could we set a flag on resolve_state_groups_for_events to tell it to + # always make a state group? + + # + # now if it's not a state event, we're done + # + + if not event.is_state(): + return EventContext.with_state( + state_group_before_event=state_group_before_event, + state_group=state_group_before_event, + current_state_ids=state_ids_before_event, + prev_state_ids=state_ids_before_event, + prev_group=state_group_before_event_prev_group, + delta_ids=deltas_to_state_group_before_event, ) - return context + # + # otherwise, we'll need to create a new state group for after the event + # - logger.debug("calling resolve_state_groups from compute_event_context") - - entry = yield self.resolve_state_groups_for_events( - event.room_id, event.prev_event_ids() - ) - - prev_state_ids = entry.state - prev_group = None - delta_ids = None - - if event.is_state(): - # If this is a state event then we need to create a new state - # group for the state after this event. - - key = (event.type, event.state_key) - if key in prev_state_ids: - replaces = prev_state_ids[key] + key = (event.type, event.state_key) + if key in state_ids_before_event: + replaces = state_ids_before_event[key] + if replaces != event.event_id: event.unsigned["replaces_state"] = replaces - current_state_ids = dict(prev_state_ids) - current_state_ids[key] = event.event_id + state_ids_after_event = dict(state_ids_before_event) + state_ids_after_event[key] = event.event_id + delta_ids = {key: event.event_id} - if entry.state_group: - # If the state at the event has a state group assigned then - # we can use that as the prev group - prev_group = entry.state_group - delta_ids = {key: event.event_id} - elif entry.prev_group: - # If the state at the event only has a prev group, then we can - # use that as a prev group too. - prev_group = entry.prev_group - delta_ids = dict(entry.delta_ids) - delta_ids[key] = event.event_id - - state_group = yield self.state_store.store_state_group( - event.event_id, - event.room_id, - prev_group=prev_group, - delta_ids=delta_ids, - current_state_ids=current_state_ids, - ) - else: - current_state_ids = prev_state_ids - prev_group = entry.prev_group - delta_ids = entry.delta_ids - - if entry.state_group is None: - entry.state_group = yield self.state_store.store_state_group( - event.event_id, - event.room_id, - prev_group=entry.prev_group, - delta_ids=entry.delta_ids, - current_state_ids=current_state_ids, - ) - entry.state_id = entry.state_group - - state_group = entry.state_group - - context = EventContext.with_state( - state_group=state_group, - current_state_ids=current_state_ids, - prev_state_ids=prev_state_ids, - prev_group=prev_group, + state_group_after_event = yield self.state_store.store_state_group( + event.event_id, + event.room_id, + prev_group=state_group_before_event, delta_ids=delta_ids, + current_state_ids=state_ids_after_event, ) - return context + return EventContext.with_state( + state_group=state_group_after_event, + state_group_before_event=state_group_before_event, + current_state_ids=state_ids_after_event, + prev_state_ids=state_ids_before_event, + prev_group=state_group_before_event, + delta_ids=delta_ids, + ) @measure_func() @defer.inlineCallbacks diff --git a/synapse/storage/data_stores/main/state.py b/synapse/storage/data_stores/main/state.py index 3132848034..9e1541988e 100644 --- a/synapse/storage/data_stores/main/state.py +++ b/synapse/storage/data_stores/main/state.py @@ -1231,7 +1231,7 @@ class StateStore(StateGroupWorkerStore, StateBackgroundUpdateStore): # if the event was rejected, just give it the same state as its # predecessor. if context.rejected: - state_groups[event.event_id] = context.prev_group + state_groups[event.event_id] = context.state_group_before_event continue state_groups[event.event_id] = context.state_group diff --git a/tests/handlers/test_federation.py b/tests/handlers/test_federation.py index d56220f403..b4d92cf732 100644 --- a/tests/handlers/test_federation.py +++ b/tests/handlers/test_federation.py @@ -12,13 +12,19 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import logging + from synapse.api.constants import EventTypes from synapse.api.errors import AuthError, Codes +from synapse.federation.federation_base import event_from_pdu_json +from synapse.logging.context import LoggingContext, run_in_background from synapse.rest import admin from synapse.rest.client.v1 import login, room from tests import unittest +logger = logging.getLogger(__name__) + class FederationTestCase(unittest.HomeserverTestCase): servlets = [ @@ -79,3 +85,123 @@ class FederationTestCase(unittest.HomeserverTestCase): self.assertEqual(failure.code, 403, failure) self.assertEqual(failure.errcode, Codes.FORBIDDEN, failure) self.assertEqual(failure.msg, "You are not invited to this room.") + + def test_rejected_message_event_state(self): + """ + Check that we store the state group correctly for rejected non-state events. + + Regression test for #6289. + """ + OTHER_SERVER = "otherserver" + OTHER_USER = "@otheruser:" + OTHER_SERVER + + # create the room + user_id = self.register_user("kermit", "test") + tok = self.login("kermit", "test") + room_id = self.helper.create_room_as(room_creator=user_id, tok=tok) + + # pretend that another server has joined + join_event = self._build_and_send_join_event(OTHER_SERVER, OTHER_USER, room_id) + + # check the state group + sg = self.successResultOf( + self.store._get_state_group_for_event(join_event.event_id) + ) + + # build and send an event which will be rejected + ev = event_from_pdu_json( + { + "type": EventTypes.Message, + "content": {}, + "room_id": room_id, + "sender": "@yetanotheruser:" + OTHER_SERVER, + "depth": join_event["depth"] + 1, + "prev_events": [join_event.event_id], + "auth_events": [], + "origin_server_ts": self.clock.time_msec(), + }, + join_event.format_version, + ) + + with LoggingContext(request="send_rejected"): + d = run_in_background(self.handler.on_receive_pdu, OTHER_SERVER, ev) + self.get_success(d) + + # that should have been rejected + e = self.get_success(self.store.get_event(ev.event_id, allow_rejected=True)) + self.assertIsNotNone(e.rejected_reason) + + # ... and the state group should be the same as before + sg2 = self.successResultOf(self.store._get_state_group_for_event(ev.event_id)) + + self.assertEqual(sg, sg2) + + def test_rejected_state_event_state(self): + """ + Check that we store the state group correctly for rejected state events. + + Regression test for #6289. + """ + OTHER_SERVER = "otherserver" + OTHER_USER = "@otheruser:" + OTHER_SERVER + + # create the room + user_id = self.register_user("kermit", "test") + tok = self.login("kermit", "test") + room_id = self.helper.create_room_as(room_creator=user_id, tok=tok) + + # pretend that another server has joined + join_event = self._build_and_send_join_event(OTHER_SERVER, OTHER_USER, room_id) + + # check the state group + sg = self.successResultOf( + self.store._get_state_group_for_event(join_event.event_id) + ) + + # build and send an event which will be rejected + ev = event_from_pdu_json( + { + "type": "org.matrix.test", + "state_key": "test_key", + "content": {}, + "room_id": room_id, + "sender": "@yetanotheruser:" + OTHER_SERVER, + "depth": join_event["depth"] + 1, + "prev_events": [join_event.event_id], + "auth_events": [], + "origin_server_ts": self.clock.time_msec(), + }, + join_event.format_version, + ) + + with LoggingContext(request="send_rejected"): + d = run_in_background(self.handler.on_receive_pdu, OTHER_SERVER, ev) + self.get_success(d) + + # that should have been rejected + e = self.get_success(self.store.get_event(ev.event_id, allow_rejected=True)) + self.assertIsNotNone(e.rejected_reason) + + # ... and the state group should be the same as before + sg2 = self.successResultOf(self.store._get_state_group_for_event(ev.event_id)) + + self.assertEqual(sg, sg2) + + def _build_and_send_join_event(self, other_server, other_user, room_id): + join_event = self.get_success( + self.handler.on_make_join_request(other_server, room_id, other_user) + ) + # the auth code requires that a signature exists, but doesn't check that + # signature... go figure. + join_event.signatures[other_server] = {"x": "y"} + with LoggingContext(request="send_join"): + d = run_in_background( + self.handler.on_send_join_request, other_server, join_event + ) + self.get_success(d) + + # sanity-check: the room should show that the new user is a member + r = self.get_success(self.store.get_current_state_ids(room_id)) + self.assertEqual(r[(EventTypes.Member, other_user)], join_event.event_id) + + return join_event diff --git a/tests/test_state.py b/tests/test_state.py index 38246555bd..176535947a 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -21,6 +21,7 @@ from synapse.api.auth import Auth from synapse.api.constants import EventTypes, Membership from synapse.api.room_versions import RoomVersions from synapse.events import FrozenEvent +from synapse.events.snapshot import EventContext from synapse.state import StateHandler, StateResolutionHandler from tests import unittest @@ -198,16 +199,22 @@ class StateTestCase(unittest.TestCase): self.store.register_events(graph.walk()) - context_store = {} + context_store = {} # type: dict[str, EventContext] for event in graph.walk(): context = yield self.state.compute_event_context(event) self.store.register_event_context(event, context) context_store[event.event_id] = context - prev_state_ids = yield context_store["D"].get_prev_state_ids(self.store) + ctx_c = context_store["C"] + ctx_d = context_store["D"] + + prev_state_ids = yield ctx_d.get_prev_state_ids(self.store) self.assertEqual(2, len(prev_state_ids)) + self.assertEqual(ctx_c.state_group, ctx_d.state_group_before_event) + self.assertEqual(ctx_d.state_group_before_event, ctx_d.state_group) + @defer.inlineCallbacks def test_branch_basic_conflict(self): graph = Graph( @@ -241,12 +248,19 @@ class StateTestCase(unittest.TestCase): self.store.register_event_context(event, context) context_store[event.event_id] = context - prev_state_ids = yield context_store["D"].get_prev_state_ids(self.store) + # C ends up winning the resolution between B and C + ctx_c = context_store["C"] + ctx_d = context_store["D"] + + prev_state_ids = yield ctx_d.get_prev_state_ids(self.store) self.assertSetEqual( {"START", "A", "C"}, {e_id for e_id in prev_state_ids.values()} ) + self.assertEqual(ctx_c.state_group, ctx_d.state_group_before_event) + self.assertEqual(ctx_d.state_group_before_event, ctx_d.state_group) + @defer.inlineCallbacks def test_branch_have_banned_conflict(self): graph = Graph( @@ -292,11 +306,18 @@ class StateTestCase(unittest.TestCase): self.store.register_event_context(event, context) context_store[event.event_id] = context - prev_state_ids = yield context_store["E"].get_prev_state_ids(self.store) + # C ends up winning the resolution between C and D because bans win over other + # changes + ctx_c = context_store["C"] + ctx_e = context_store["E"] + + prev_state_ids = yield ctx_e.get_prev_state_ids(self.store) self.assertSetEqual( {"START", "A", "B", "C"}, {e for e in prev_state_ids.values()} ) + self.assertEqual(ctx_c.state_group, ctx_e.state_group_before_event) + self.assertEqual(ctx_e.state_group_before_event, ctx_e.state_group) @defer.inlineCallbacks def test_branch_have_perms_conflict(self): @@ -360,12 +381,20 @@ class StateTestCase(unittest.TestCase): self.store.register_event_context(event, context) context_store[event.event_id] = context - prev_state_ids = yield context_store["D"].get_prev_state_ids(self.store) + # B ends up winning the resolution between B and C because power levels + # win over other changes. + ctx_b = context_store["B"] + ctx_d = context_store["D"] + + prev_state_ids = yield ctx_d.get_prev_state_ids(self.store) self.assertSetEqual( {"A1", "A2", "A3", "A5", "B"}, {e for e in prev_state_ids.values()} ) + self.assertEqual(ctx_b.state_group, ctx_d.state_group_before_event) + self.assertEqual(ctx_d.state_group_before_event, ctx_d.state_group) + def _add_depths(self, nodes, edges): def _get_depth(ev): node = nodes[ev] @@ -390,13 +419,16 @@ class StateTestCase(unittest.TestCase): context = yield self.state.compute_event_context(event, old_state=old_state) - current_state_ids = yield context.get_current_state_ids(self.store) + prev_state_ids = yield context.get_prev_state_ids(self.store) + self.assertCountEqual((e.event_id for e in old_state), prev_state_ids.values()) - self.assertEqual( - set(e.event_id for e in old_state), set(current_state_ids.values()) + current_state_ids = yield context.get_current_state_ids(self.store) + self.assertCountEqual( + (e.event_id for e in old_state), current_state_ids.values() ) - self.assertIsNotNone(context.state_group) + self.assertIsNotNone(context.state_group_before_event) + self.assertEqual(context.state_group_before_event, context.state_group) @defer.inlineCallbacks def test_annotate_with_old_state(self): @@ -411,11 +443,18 @@ class StateTestCase(unittest.TestCase): context = yield self.state.compute_event_context(event, old_state=old_state) prev_state_ids = yield context.get_prev_state_ids(self.store) + self.assertCountEqual((e.event_id for e in old_state), prev_state_ids.values()) - self.assertEqual( - set(e.event_id for e in old_state), set(prev_state_ids.values()) + current_state_ids = yield context.get_current_state_ids(self.store) + self.assertCountEqual( + (e.event_id for e in old_state + [event]), current_state_ids.values() ) + self.assertIsNotNone(context.state_group_before_event) + self.assertNotEqual(context.state_group_before_event, context.state_group) + self.assertEqual(context.state_group_before_event, context.prev_group) + self.assertEqual({("state", ""): event.event_id}, context.delta_ids) + @defer.inlineCallbacks def test_trivial_annotate_message(self): prev_event_id = "prev_event_id" From feafd98aca3e72d27516c79f986a28ea39886ebc Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 6 Nov 2019 10:02:23 +0000 Subject: [PATCH 0455/1623] 1.5.1 --- CHANGES.md | 9 +++++++++ changelog.d/6331.feature | 1 - changelog.d/6334.feature | 1 - debian/changelog | 6 ++++++ synapse/__init__.py | 2 +- 5 files changed, 16 insertions(+), 3 deletions(-) delete mode 100644 changelog.d/6331.feature delete mode 100644 changelog.d/6334.feature diff --git a/CHANGES.md b/CHANGES.md index 6faa4b8dce..9312dc2941 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,12 @@ +Synapse 1.5.1 (2019-11-06) +========================== + +Features +-------- + +- Limit the length of data returned by url previews, to prevent DoS attacks. ([\#6331](https://github.com/matrix-org/synapse/issues/6331), [\#6334](https://github.com/matrix-org/synapse/issues/6334)) + + Synapse 1.5.0 (2019-10-29) ========================== diff --git a/changelog.d/6331.feature b/changelog.d/6331.feature deleted file mode 100644 index eaf69ef3f6..0000000000 --- a/changelog.d/6331.feature +++ /dev/null @@ -1 +0,0 @@ -Limit the length of data returned by url previews, to prevent DoS attacks. diff --git a/changelog.d/6334.feature b/changelog.d/6334.feature deleted file mode 100644 index eaf69ef3f6..0000000000 --- a/changelog.d/6334.feature +++ /dev/null @@ -1 +0,0 @@ -Limit the length of data returned by url previews, to prevent DoS attacks. diff --git a/debian/changelog b/debian/changelog index acda7e5c63..c4415f460a 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.5.1) stable; urgency=medium + + * New synapse release 1.5.1. + + -- Synapse Packaging team Wed, 06 Nov 2019 10:02:14 +0000 + matrix-synapse-py3 (1.5.0) stable; urgency=medium * New synapse release 1.5.0. diff --git a/synapse/__init__.py b/synapse/__init__.py index 8587ffa76f..ec16f54a49 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -36,7 +36,7 @@ try: except ImportError: pass -__version__ = "1.5.0" +__version__ = "1.5.1" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 70d93cafdb4a3055849e7b84881cfd6225f0b6e5 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 6 Nov 2019 10:59:03 +0000 Subject: [PATCH 0456/1623] Update insert --- synapse/storage/data_stores/main/events_bg_updates.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/synapse/storage/data_stores/main/events_bg_updates.py b/synapse/storage/data_stores/main/events_bg_updates.py index a703927471..2fcb0c33dc 100644 --- a/synapse/storage/data_stores/main/events_bg_updates.py +++ b/synapse/storage/data_stores/main/events_bg_updates.py @@ -537,7 +537,12 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): txn=txn, table="event_labels", values=[ - {"event_id": event_id, "label": label} + { + "event_id": event_id, + "label": label, + "room_id": event_json["room_id"], + "topological_ordering": event_json["depth"], + } for label in event_json["content"].get( EventContentFields.Labels, [] ) From 24a214bd1bc396cbcc41c4c913938299ca9ffcf5 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 6 Nov 2019 11:04:19 +0000 Subject: [PATCH 0457/1623] Fix field name --- synapse/storage/data_stores/main/events_bg_updates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/storage/data_stores/main/events_bg_updates.py b/synapse/storage/data_stores/main/events_bg_updates.py index 2fcb0c33dc..309bfcafe5 100644 --- a/synapse/storage/data_stores/main/events_bg_updates.py +++ b/synapse/storage/data_stores/main/events_bg_updates.py @@ -544,7 +544,7 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): "topological_ordering": event_json["depth"], } for label in event_json["content"].get( - EventContentFields.Labels, [] + EventContentFields.LABELS, [] ) ], ) From 541f1b92d946093fef17ea8b95a7cb595fc5ffc4 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 5 Nov 2019 17:39:16 +0000 Subject: [PATCH 0458/1623] Only do `rc_login` ratelimiting on succesful login. We were doing this in a number of places which meant that some login code paths incremented the counter multiple times. It was also applying ratelimiting to UIA endpoints, which was probably not intentional. In particular, some custom auth modules were calling `check_user_exists`, which incremented the counters, meaning that people would fail to login sometimes. --- synapse/handlers/auth.py | 55 +--------------- synapse/rest/client/v1/login.py | 111 ++++++++++++++++++++++++++------ 2 files changed, 94 insertions(+), 72 deletions(-) diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index 7a0f54ca24..14c6387b6a 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -35,7 +35,6 @@ from synapse.api.errors import ( SynapseError, UserDeactivatedError, ) -from synapse.api.ratelimiting import Ratelimiter from synapse.handlers.ui_auth import INTERACTIVE_AUTH_CHECKERS from synapse.handlers.ui_auth.checkers import UserInteractiveAuthChecker from synapse.logging.context import defer_to_thread @@ -102,9 +101,6 @@ class AuthHandler(BaseHandler): login_types.append(t) self._supported_login_types = login_types - self._account_ratelimiter = Ratelimiter() - self._failed_attempts_ratelimiter = Ratelimiter() - self._clock = self.hs.get_clock() @defer.inlineCallbacks @@ -501,11 +497,8 @@ class AuthHandler(BaseHandler): multiple matches Raises: - LimitExceededError if the ratelimiter's login requests count for this - user is too high too proceed. UserDeactivatedError if a user is found but is deactivated. """ - self.ratelimit_login_per_account(user_id) res = yield self._find_user_id_and_pwd_hash(user_id) if res is not None: return res[0] @@ -572,8 +565,6 @@ class AuthHandler(BaseHandler): StoreError if there was a problem accessing the database SynapseError if there was a problem with the request LoginError if there was an authentication problem. - LimitExceededError if the ratelimiter's login requests count for this - user is too high too proceed. """ if username.startswith("@"): @@ -581,8 +572,6 @@ class AuthHandler(BaseHandler): else: qualified_user_id = UserID(username, self.hs.hostname).to_string() - self.ratelimit_login_per_account(qualified_user_id) - login_type = login_submission.get("type") known_login_type = False @@ -650,15 +639,6 @@ class AuthHandler(BaseHandler): if not known_login_type: raise SynapseError(400, "Unknown login type %s" % login_type) - # unknown username or invalid password. - self._failed_attempts_ratelimiter.ratelimit( - qualified_user_id.lower(), - time_now_s=self._clock.time(), - rate_hz=self.hs.config.rc_login_failed_attempts.per_second, - burst_count=self.hs.config.rc_login_failed_attempts.burst_count, - update=True, - ) - # We raise a 403 here, but note that if we're doing user-interactive # login, it turns all LoginErrors into a 401 anyway. raise LoginError(403, "Invalid password", errcode=Codes.FORBIDDEN) @@ -710,10 +690,6 @@ class AuthHandler(BaseHandler): Returns: Deferred[unicode] the canonical_user_id, or Deferred[None] if unknown user/bad password - - Raises: - LimitExceededError if the ratelimiter's login requests count for this - user is too high too proceed. """ lookupres = yield self._find_user_id_and_pwd_hash(user_id) if not lookupres: @@ -742,7 +718,7 @@ class AuthHandler(BaseHandler): auth_api.validate_macaroon(macaroon, "login", user_id) except Exception: raise AuthError(403, "Invalid token", errcode=Codes.FORBIDDEN) - self.ratelimit_login_per_account(user_id) + yield self.auth.check_auth_blocking(user_id) return user_id @@ -912,35 +888,6 @@ class AuthHandler(BaseHandler): else: return defer.succeed(False) - def ratelimit_login_per_account(self, user_id): - """Checks whether the process must be stopped because of ratelimiting. - - Checks against two ratelimiters: the generic one for login attempts per - account and the one specific to failed attempts. - - Args: - user_id (unicode): complete @user:id - - Raises: - LimitExceededError if one of the ratelimiters' login requests count - for this user is too high too proceed. - """ - self._failed_attempts_ratelimiter.ratelimit( - user_id.lower(), - time_now_s=self._clock.time(), - rate_hz=self.hs.config.rc_login_failed_attempts.per_second, - burst_count=self.hs.config.rc_login_failed_attempts.burst_count, - update=False, - ) - - self._account_ratelimiter.ratelimit( - user_id.lower(), - time_now_s=self._clock.time(), - rate_hz=self.hs.config.rc_login_account.per_second, - burst_count=self.hs.config.rc_login_account.burst_count, - update=True, - ) - @attr.s class MacaroonGenerator(object): diff --git a/synapse/rest/client/v1/login.py b/synapse/rest/client/v1/login.py index 24a0ce74f2..abc210da57 100644 --- a/synapse/rest/client/v1/login.py +++ b/synapse/rest/client/v1/login.py @@ -92,8 +92,11 @@ class LoginRestServlet(RestServlet): self.auth_handler = self.hs.get_auth_handler() self.registration_handler = hs.get_registration_handler() self.handlers = hs.get_handlers() + self._clock = hs.get_clock() self._well_known_builder = WellKnownBuilder(hs) self._address_ratelimiter = Ratelimiter() + self._account_ratelimiter = Ratelimiter() + self._failed_attempts_ratelimiter = Ratelimiter() def on_GET(self, request): flows = [] @@ -202,6 +205,16 @@ class LoginRestServlet(RestServlet): # (See add_threepid in synapse/handlers/auth.py) address = address.lower() + # We also apply account rate limiting using the 3PID as a key, as + # otherwise using 3PID bypasses the ratelimiting based on user ID. + self._failed_attempts_ratelimiter.ratelimit( + (medium, address), + time_now_s=self._clock.time(), + rate_hz=self.hs.config.rc_login_failed_attempts.per_second, + burst_count=self.hs.config.rc_login_failed_attempts.burst_count, + update=False, + ) + # Check for login providers that support 3pid login types ( canonical_user_id, @@ -211,7 +224,8 @@ class LoginRestServlet(RestServlet): ) if canonical_user_id: # Authentication through password provider and 3pid succeeded - result = yield self._register_device_with_callback( + + result = yield self._complete_login( canonical_user_id, login_submission, callback_3pid ) return result @@ -225,6 +239,21 @@ class LoginRestServlet(RestServlet): logger.warning( "unknown 3pid identifier medium %s, address %r", medium, address ) + # We mark that we've failed to log in here, as + # `check_password_provider_3pid` might have returned `None` due + # to an incorrect password, rather than the account not + # existing. + # + # If it returned None but the 3PID was bound then we won't hit + # this code path, which is fine as then the per-user ratelimit + # will kick in below. + self._failed_attempts_ratelimiter.can_do_action( + (medium, address), + time_now_s=self._clock.time(), + rate_hz=self.hs.config.rc_login_failed_attempts.per_second, + burst_count=self.hs.config.rc_login_failed_attempts.burst_count, + update=True, + ) raise LoginError(403, "", errcode=Codes.FORBIDDEN) identifier = {"type": "m.id.user", "user": user_id} @@ -236,29 +265,84 @@ class LoginRestServlet(RestServlet): if "user" not in identifier: raise SynapseError(400, "User identifier is missing 'user' key") - canonical_user_id, callback = yield self.auth_handler.validate_login( - identifier["user"], login_submission + if identifier["user"].startswith("@"): + qualified_user_id = identifier["user"] + else: + qualified_user_id = UserID(identifier["user"], self.hs.hostname).to_string() + + # Check if we've hit the failed ratelimit (but don't update it) + self._failed_attempts_ratelimiter.ratelimit( + qualified_user_id.lower(), + time_now_s=self._clock.time(), + rate_hz=self.hs.config.rc_login_failed_attempts.per_second, + burst_count=self.hs.config.rc_login_failed_attempts.burst_count, + update=False, ) - result = yield self._register_device_with_callback( + try: + canonical_user_id, callback = yield self.auth_handler.validate_login( + identifier["user"], login_submission + ) + except LoginError: + # The user has failed to log in, so we need to update the rate + # limiter. Using `can_do_action` avoids us raising a ratelimit + # exception and masking the LoginError. The actual ratelimiting + # should have happened above. + self._failed_attempts_ratelimiter.can_do_action( + qualified_user_id.lower(), + time_now_s=self._clock.time(), + rate_hz=self.hs.config.rc_login_failed_attempts.per_second, + burst_count=self.hs.config.rc_login_failed_attempts.burst_count, + update=True, + ) + raise + + result = yield self._complete_login( canonical_user_id, login_submission, callback ) return result @defer.inlineCallbacks - def _register_device_with_callback(self, user_id, login_submission, callback=None): - """ Registers a device with a given user_id. Optionally run a callback - function after registration has completed. + def _complete_login( + self, user_id, login_submission, callback=None, create_non_existant_users=False + ): + """Called when we've successfully authed the user and now need to + actually login them in (e.g. create devices). This gets called on + all succesful logins. + + Applies the ratelimiting for succesful login attempts against an + account. Args: user_id (str): ID of the user to register. login_submission (dict): Dictionary of login information. callback (func|None): Callback function to run after registration. + create_non_existant_users (bool): Whether to create the user if + they don't exist. Defaults to False. Returns: result (Dict[str,str]): Dictionary of account information after successful registration. """ + + # Before we actually log them in we check if they've already logged in + # too often. This happens here rather than before as we don't + # necessarily know the user before now. + self._account_ratelimiter.ratelimit( + user_id.lower(), + time_now_s=self._clock.time(), + rate_hz=self.hs.config.rc_login_account.per_second, + burst_count=self.hs.config.rc_login_account.burst_count, + update=True, + ) + + if create_non_existant_users: + user_id = yield self.auth_handler.check_user_exists(user_id) + if not user_id: + user_id = yield self.registration_handler.register_user( + localpart=UserID.from_string(user_id).localpart + ) + device_id = login_submission.get("device_id") initial_display_name = login_submission.get("initial_device_display_name") device_id, access_token = yield self.registration_handler.register_device( @@ -285,7 +369,7 @@ class LoginRestServlet(RestServlet): token ) - result = yield self._register_device_with_callback(user_id, login_submission) + result = yield self._complete_login(user_id, login_submission) return result @defer.inlineCallbacks @@ -313,16 +397,7 @@ class LoginRestServlet(RestServlet): raise LoginError(401, "Invalid JWT", errcode=Codes.UNAUTHORIZED) user_id = UserID(user, self.hs.hostname).to_string() - - registered_user_id = yield self.auth_handler.check_user_exists(user_id) - if not registered_user_id: - registered_user_id = yield self.registration_handler.register_user( - localpart=user - ) - - result = yield self._register_device_with_callback( - registered_user_id, login_submission - ) + result = yield self._complete_login(user_id, login_submission) return result From f697b4b4a2ca329a32105ddf83735737808306bf Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 6 Nov 2019 11:00:54 +0000 Subject: [PATCH 0459/1623] Add failed auth ratelimiting to UIA --- synapse/handlers/auth.py | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index 14c6387b6a..20c62bd780 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -35,6 +35,7 @@ from synapse.api.errors import ( SynapseError, UserDeactivatedError, ) +from synapse.api.ratelimiting import Ratelimiter from synapse.handlers.ui_auth import INTERACTIVE_AUTH_CHECKERS from synapse.handlers.ui_auth.checkers import UserInteractiveAuthChecker from synapse.logging.context import defer_to_thread @@ -101,6 +102,10 @@ class AuthHandler(BaseHandler): login_types.append(t) self._supported_login_types = login_types + # Ratelimiter for failed auth during UIA. Uses same ratelimit config + # as per `rc_login.failed_attempts`. + self._failed_uia_attempts_ratelimiter = Ratelimiter() + self._clock = self.hs.get_clock() @defer.inlineCallbacks @@ -129,12 +134,38 @@ class AuthHandler(BaseHandler): AuthError if the client has completed a login flow, and it gives a different user to `requester` + + LimitExceededError if the ratelimiter's failed requests count for this + user is too high too proceed + """ + user_id = requester.user.to_string() + + # Check if we should be ratelimited due to too many previous failed attempts + self._failed_uia_attempts_ratelimiter.ratelimit( + user_id, + time_now_s=self._clock.time(), + rate_hz=self.hs.config.rc_login_failed_attempts.per_second, + burst_count=self.hs.config.rc_login_failed_attempts.burst_count, + update=False, + ) + # build a list of supported flows flows = [[login_type] for login_type in self._supported_login_types] - result, params, _ = yield self.check_auth(flows, request_body, clientip) + try: + result, params, _ = yield self.check_auth(flows, request_body, clientip) + except LoginError: + # Update the ratelimite to say we failed (`can_do_action` doesn't raise). + self._failed_uia_attempts_ratelimiter.can_do_action( + user_id, + time_now_s=self._clock.time(), + rate_hz=self.hs.config.rc_login_failed_attempts.per_second, + burst_count=self.hs.config.rc_login_failed_attempts.burst_count, + update=True, + ) + raise # find the completed login type for login_type in self._supported_login_types: From 4fc53bf1fb97d52c19aa718e67f31d290218e3c1 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 5 Nov 2019 17:42:42 +0000 Subject: [PATCH 0460/1623] Newsfile --- changelog.d/6335.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6335.bugfix diff --git a/changelog.d/6335.bugfix b/changelog.d/6335.bugfix new file mode 100644 index 0000000000..a95f6b9eec --- /dev/null +++ b/changelog.d/6335.bugfix @@ -0,0 +1 @@ +Fix bug where `rc_login` ratelimiting would prematurely kick in. From b33c4f7a828e722d6115f73525e0456edb79a90f Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Wed, 6 Nov 2019 11:55:00 +0000 Subject: [PATCH 0461/1623] Numeric ID checker now checks @0, don't ratelimit on checking --- synapse/handlers/register.py | 41 +++++++++++-------- .../storage/data_stores/main/registration.py | 8 ++-- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/synapse/handlers/register.py b/synapse/handlers/register.py index cff6b0d375..3c142a4395 100644 --- a/synapse/handlers/register.py +++ b/synapse/handlers/register.py @@ -168,6 +168,7 @@ class RegistrationHandler(BaseHandler): Raises: RegistrationError if there was a problem registering. """ + yield self._check_registration_ratelimit(address) yield self.auth.check_auth_blocking(threepid=threepid) password_hash = None @@ -414,6 +415,30 @@ class RegistrationHandler(BaseHandler): ratelimit=False, ) + def _check_registration_ratelimit(self, address): + """A simple helper method to check whether the registration rate limit has been hit + for a given IP address + + Args: + address (str): the IP address used to perform the registration. + + Raises: + LimitExceededError: If the rate limit has been exceeded. + """ + time_now = self.clock.time() + + allowed, time_allowed = self.ratelimiter.can_do_action( + address, + time_now_s=time_now, + rate_hz=self.hs.config.rc_registration.per_second, + burst_count=self.hs.config.rc_registration.burst_count, + ) + + if not allowed: + raise LimitExceededError( + retry_after_ms=int(1000 * (time_allowed - time_now)) + ) + def register_with_store( self, user_id, @@ -446,22 +471,6 @@ class RegistrationHandler(BaseHandler): Returns: Deferred """ - # Don't rate limit for app services - if appservice_id is None and address is not None: - time_now = self.clock.time() - - allowed, time_allowed = self.ratelimiter.can_do_action( - address, - time_now_s=time_now, - rate_hz=self.hs.config.rc_registration.per_second, - burst_count=self.hs.config.rc_registration.burst_count, - ) - - if not allowed: - raise LimitExceededError( - retry_after_ms=int(1000 * (time_allowed - time_now)) - ) - if self.hs.config.worker_app: return self._register_client( user_id=user_id, diff --git a/synapse/storage/data_stores/main/registration.py b/synapse/storage/data_stores/main/registration.py index f70d41ecab..ee1b2b2bbf 100644 --- a/synapse/storage/data_stores/main/registration.py +++ b/synapse/storage/data_stores/main/registration.py @@ -488,14 +488,14 @@ class RegistrationWorkerStore(SQLBaseStore): we can. Unfortunately, it's possible some of them are already taken by existing users, and there may be gaps in the already taken range. This function returns the start of the first allocatable gap. This is to - avoid the case of ID 10000000 being pre-allocated, so us wasting the - first (and shortest) many generated user IDs. + avoid the case of ID 1000 being pre-allocated and starting at 1001 while + 0-999 are available. """ def _find_next_generated_user_id(txn): - # We bound between '@1' and '@a' to avoid pulling the entire table + # We bound between '@0' and '@a' to avoid pulling the entire table # out. - txn.execute("SELECT name FROM users WHERE '@1' <= name AND name < '@a'") + txn.execute("SELECT name FROM users WHERE '@0' <= name AND name < '@a'") regex = re.compile(r"^@(\d+):") From 4059d61e2608ac823ef04fe37f23fcac2387a37b Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Wed, 6 Nov 2019 12:01:54 +0000 Subject: [PATCH 0462/1623] Don't forget to ratelimit calls outside of RegistrationHandler --- synapse/handlers/register.py | 4 ++-- synapse/replication/http/register.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/synapse/handlers/register.py b/synapse/handlers/register.py index 3c142a4395..8be82e3754 100644 --- a/synapse/handlers/register.py +++ b/synapse/handlers/register.py @@ -168,7 +168,7 @@ class RegistrationHandler(BaseHandler): Raises: RegistrationError if there was a problem registering. """ - yield self._check_registration_ratelimit(address) + yield self.check_registration_ratelimit(address) yield self.auth.check_auth_blocking(threepid=threepid) password_hash = None @@ -415,7 +415,7 @@ class RegistrationHandler(BaseHandler): ratelimit=False, ) - def _check_registration_ratelimit(self, address): + def check_registration_ratelimit(self, address): """A simple helper method to check whether the registration rate limit has been hit for a given IP address diff --git a/synapse/replication/http/register.py b/synapse/replication/http/register.py index 915cfb9430..6f4bba7aa4 100644 --- a/synapse/replication/http/register.py +++ b/synapse/replication/http/register.py @@ -75,6 +75,8 @@ class ReplicationRegisterServlet(ReplicationEndpoint): async def _handle_request(self, request, user_id): content = parse_json_object_from_request(request) + await self.registration_handler.check_registration_ratelimit(content["address"]) + await self.registration_handler.register_with_store( user_id=user_id, password_hash=content["password_hash"], From d2f6a67cb4c8f1ea1a4ae563dd53139838b019c7 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Wed, 6 Nov 2019 12:03:12 +0000 Subject: [PATCH 0463/1623] Add changelog --- changelog.d/6338.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6338.bugfix diff --git a/changelog.d/6338.bugfix b/changelog.d/6338.bugfix new file mode 100644 index 0000000000..8e469f0fb6 --- /dev/null +++ b/changelog.d/6338.bugfix @@ -0,0 +1 @@ +Prevent the server taking a long time to start up when guest registration is enabled. \ No newline at end of file From 4257feb20f328c83ac7cb27113f779e844623e30 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 6 Nov 2019 13:35:56 +0000 Subject: [PATCH 0464/1623] build debs for eoan and bullseye --- scripts-dev/build_debian_packages | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts-dev/build_debian_packages b/scripts-dev/build_debian_packages index 93305ee9b1..84eaec6a95 100755 --- a/scripts-dev/build_debian_packages +++ b/scripts-dev/build_debian_packages @@ -20,11 +20,13 @@ from concurrent.futures import ThreadPoolExecutor DISTS = ( "debian:stretch", "debian:buster", + "debian:bullseye", "debian:sid", "ubuntu:xenial", "ubuntu:bionic", "ubuntu:cosmic", "ubuntu:disco", + "ubuntu:eoan", ) DESC = '''\ From 1fe3cc2c9c59001a6d3f7b28f81bd6681c3c03ac Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Wed, 6 Nov 2019 14:54:24 +0000 Subject: [PATCH 0465/1623] Address review comments --- synapse/handlers/register.py | 24 ++++++++++++------------ synapse/replication/http/register.py | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/synapse/handlers/register.py b/synapse/handlers/register.py index 8be82e3754..47b9ae8d7f 100644 --- a/synapse/handlers/register.py +++ b/synapse/handlers/register.py @@ -24,7 +24,6 @@ from synapse.api.errors import ( AuthError, Codes, ConsentNotGivenError, - LimitExceededError, RegistrationError, SynapseError, ) @@ -218,8 +217,8 @@ class RegistrationHandler(BaseHandler): else: # autogen a sequential user ID - user = None - while not user: + # Fail after being unable to find a suitable ID a few times + for x in range(10): localpart = yield self._generate_user_id() user = UserID(localpart, self.hs.hostname) user_id = user.to_string() @@ -234,10 +233,12 @@ class RegistrationHandler(BaseHandler): create_profile_with_displayname=default_display_name, address=address, ) + + # Successfully registered + break except SynapseError: # if user id is taken, just generate another - user = None - user_id = None + pass if not self.hs.config.user_consent_at_registration: yield self._auto_join_rooms(user_id) @@ -420,25 +421,24 @@ class RegistrationHandler(BaseHandler): for a given IP address Args: - address (str): the IP address used to perform the registration. + address (str|None): the IP address used to perform the registration. If this is + None, no ratelimiting will be performed. Raises: LimitExceededError: If the rate limit has been exceeded. """ + if not address: + return + time_now = self.clock.time() - allowed, time_allowed = self.ratelimiter.can_do_action( + self.ratelimiter.ratelimit( address, time_now_s=time_now, rate_hz=self.hs.config.rc_registration.per_second, burst_count=self.hs.config.rc_registration.burst_count, ) - if not allowed: - raise LimitExceededError( - retry_after_ms=int(1000 * (time_allowed - time_now)) - ) - def register_with_store( self, user_id, diff --git a/synapse/replication/http/register.py b/synapse/replication/http/register.py index 6f4bba7aa4..0c4aca1291 100644 --- a/synapse/replication/http/register.py +++ b/synapse/replication/http/register.py @@ -75,7 +75,7 @@ class ReplicationRegisterServlet(ReplicationEndpoint): async def _handle_request(self, request, user_id): content = parse_json_object_from_request(request) - await self.registration_handler.check_registration_ratelimit(content["address"]) + self.registration_handler.check_registration_ratelimit(content["address"]) await self.registration_handler.register_with_store( user_id=user_id, From 55bc8d531e0dfe6623d98a9e81ee9a63d1c2799a Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Wed, 6 Nov 2019 16:52:54 +0000 Subject: [PATCH 0466/1623] raise exception after multiple failures --- synapse/handlers/register.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/synapse/handlers/register.py b/synapse/handlers/register.py index 47b9ae8d7f..235f11c322 100644 --- a/synapse/handlers/register.py +++ b/synapse/handlers/register.py @@ -217,8 +217,13 @@ class RegistrationHandler(BaseHandler): else: # autogen a sequential user ID - # Fail after being unable to find a suitable ID a few times - for x in range(10): + fail_count = 0 + user = None + while not user: + # Fail after being unable to find a suitable ID a few times + if fail_count > 10: + raise SynapseError(500, "Unable to find a suitable guest user ID") + localpart = yield self._generate_user_id() user = UserID(localpart, self.hs.hostname) user_id = user.to_string() @@ -238,7 +243,9 @@ class RegistrationHandler(BaseHandler): break except SynapseError: # if user id is taken, just generate another - pass + user = None + user_id = None + fail_count += 1 if not self.hs.config.user_consent_at_registration: yield self._auto_join_rooms(user_id) From 71f3bd734fe9f8f903c5a496720e7dcf926f2f4a Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 6 Nov 2019 17:00:18 +0000 Subject: [PATCH 0467/1623] Use correct type annotation --- synapse/storage/data_stores/main/events.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/synapse/storage/data_stores/main/events.py b/synapse/storage/data_stores/main/events.py index 3049a21dc5..d69c59f5a1 100644 --- a/synapse/storage/data_stores/main/events.py +++ b/synapse/storage/data_stores/main/events.py @@ -19,7 +19,7 @@ import itertools import logging from collections import Counter as c_counter, OrderedDict, namedtuple from functools import wraps -from typing import Set +from typing import Collection from six import iteritems, text_type from six.moves import range @@ -1736,7 +1736,7 @@ class EventsStore( return state_groups def purge_unreferenced_state_groups( - self, room_id: str, state_groups_to_delete: Set[int] + self, room_id: str, state_groups_to_delete: Collection[int] ) -> defer.Deferred: """Deletes no longer referenced state groups and de-deltas any state groups that reference them. From 5c3363233cca7044a333b7e19ba239eaf5587ff8 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 6 Nov 2019 17:02:05 +0000 Subject: [PATCH 0468/1623] Fix deleting state groups during room purge. And fix the tests to actually test that things got deleted. --- synapse/storage/data_stores/main/events.py | 27 +++++++++++----------- tests/rest/admin/test_admin.py | 4 +++- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/synapse/storage/data_stores/main/events.py b/synapse/storage/data_stores/main/events.py index d69c59f5a1..946823876a 100644 --- a/synapse/storage/data_stores/main/events.py +++ b/synapse/storage/data_stores/main/events.py @@ -1633,7 +1633,20 @@ class EventsStore( return self.runInteraction("purge_room", self._purge_room_txn, room_id) def _purge_room_txn(self, txn, room_id): - # First delete tables which lack an index on room_id but have one on event_id + # First we fetch all the state groups that should be deleted, before + # we delete that information. + txn.execute( + """ + SELECT DISTINCT state_group FROM events + INNER JOIN event_to_state_groups USING(event_id) + WHERE events.room_id = ? + """, + (room_id,), + ) + + state_groups = [row[0] for row in txn] + + # Now we delete tables which lack an index on room_id but have one on event_id for table in ( "event_auth", "event_edges", @@ -1717,18 +1730,6 @@ class EventsStore( # index on them. In any case we should be clearing out 'stream' tables # periodically anyway (#5888) - # Now we fetch all the state groups that should be deleted. - txn.execute( - """ - SELECT DISTINCT state_group FROM events - INNER JOIN event_to_state_groups USING(event_id) - WHERE events.room_id = ? - """, - (room_id,), - ) - - state_groups = [row[0] for row in txn] - # TODO: we could probably usefully do a bunch of cache invalidation here logger.info("[purge] done") diff --git a/tests/rest/admin/test_admin.py b/tests/rest/admin/test_admin.py index 8e1ca8b738..d9f1b95cb0 100644 --- a/tests/rest/admin/test_admin.py +++ b/tests/rest/admin/test_admin.py @@ -628,10 +628,12 @@ class PurgeRoomTestCase(unittest.HomeserverTestCase): "local_invites", "room_account_data", "room_tags", + "state_groups", + "state_groups_state", ): count = self.get_success( self.store._simple_select_one_onecol( - table="events", + table=table, keyvalues={"room_id": room_id}, retcol="COUNT(*)", desc="test_purge_room", From eda14737cf0faf789ec587633b12bb2cf65fa305 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 6 Nov 2019 18:14:03 +0000 Subject: [PATCH 0469/1623] Also filter state events --- synapse/handlers/room.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index f47237b3fb..3148df0de9 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -907,7 +907,13 @@ class RoomContextHandler(object): state = yield self.state_store.get_state_for_events( [last_event_id], state_filter=state_filter ) - results["state"] = list(state[last_event_id].values()) + + # Apply the filter on state events. + state_events = list(state[last_event_id].values()) + if event_filter: + state_events = event_filter.filter(state_events) + + results["state"] = list(state_events) # We use a dummy token here as we only care about the room portion of # the token, which we replace. From f03c9d34442368c8f2c26d2ac16b770bc451c76d Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 6 Nov 2019 15:47:40 +0000 Subject: [PATCH 0470/1623] Don't apply retention policy based filtering on state events As per MSC1763, 'Retention is only considered for non-state events.', so don't filter out state events based on the room's retention policy. --- synapse/visibility.py | 15 +++++++++------ tests/rest/client/test_retention.py | 10 ++++++++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/synapse/visibility.py b/synapse/visibility.py index 4498c156bc..4d4141dacc 100644 --- a/synapse/visibility.py +++ b/synapse/visibility.py @@ -111,14 +111,17 @@ def filter_events_for_client( if not event.is_state() and event.sender in ignore_list: return None - retention_policy = retention_policies[event.room_id] - max_lifetime = retention_policy.get("max_lifetime") + # Don't try to apply the room's retention policy if the event is a state event, as + # MSC1763 states that retention is only considered for non-state events. + if not event.is_state(): + retention_policy = retention_policies[event.room_id] + max_lifetime = retention_policy.get("max_lifetime") - if max_lifetime is not None: - oldest_allowed_ts = storage.main.clock.time_msec() - max_lifetime + if max_lifetime is not None: + oldest_allowed_ts = storage.main.clock.time_msec() - max_lifetime - if event.origin_server_ts < oldest_allowed_ts: - return None + if event.origin_server_ts < oldest_allowed_ts: + return None if event.event_id in always_include_ids: return event diff --git a/tests/rest/client/test_retention.py b/tests/rest/client/test_retention.py index 41ea9db689..7b6f25a838 100644 --- a/tests/rest/client/test_retention.py +++ b/tests/rest/client/test_retention.py @@ -164,6 +164,12 @@ class RetentionTestCase(unittest.HomeserverTestCase): self.assertEqual(filtered_events[0].event_id, valid_event_id, filtered_events) def _test_retention_event_purged(self, room_id, increment): + # Get the create event to, later, check that we can still access it. + message_handler = self.hs.get_message_handler() + create_event = self.get_success( + message_handler.get_room_data(self.user_id, room_id, EventTypes.Create) + ) + # Send a first event to the room. This is the event we'll want to be purged at the # end of the test. resp = self.helper.send( @@ -202,6 +208,10 @@ class RetentionTestCase(unittest.HomeserverTestCase): valid_event = self.get_event(room_id, valid_event_id) self.assertEqual(valid_event.get("content", {}).get("body"), "2", valid_event) + # Check that we can still access state events that were sent before the event that + # has been purged. + self.get_event(room_id, create_event.event_id) + def get_event(self, room_id, event_id, expected_code=200): url = "/_matrix/client/r0/rooms/%s/event/%s" % (room_id, event_id) From affcc2cc3655531351048a4ad8ac67e22d1e398d Mon Sep 17 00:00:00 2001 From: V02460 Date: Thu, 7 Nov 2019 10:43:51 +0100 Subject: [PATCH 0471/1623] Fix LruCache callback deduplication (#6213) --- changelog.d/6213.bugfix | 1 + synapse/util/caches/descriptors.py | 48 +++++++++++++++++++++++------- 2 files changed, 38 insertions(+), 11 deletions(-) create mode 100644 changelog.d/6213.bugfix diff --git a/changelog.d/6213.bugfix b/changelog.d/6213.bugfix new file mode 100644 index 0000000000..072264fba3 --- /dev/null +++ b/changelog.d/6213.bugfix @@ -0,0 +1 @@ +Fix LruCache callback deduplication. diff --git a/synapse/util/caches/descriptors.py b/synapse/util/caches/descriptors.py index 0e8da27f53..84f5ae22c3 100644 --- a/synapse/util/caches/descriptors.py +++ b/synapse/util/caches/descriptors.py @@ -17,8 +17,8 @@ import functools import inspect import logging import threading -from collections import namedtuple -from typing import Any, cast +from typing import Any, Tuple, Union, cast +from weakref import WeakValueDictionary from six import itervalues @@ -38,6 +38,8 @@ from . import register_cache logger = logging.getLogger(__name__) +CacheKey = Union[Tuple, Any] + class _CachedFunction(Protocol): invalidate = None # type: Any @@ -430,7 +432,7 @@ class CacheDescriptor(_CacheDescriptorBase): # Add our own `cache_context` to argument list if the wrapped function # has asked for one if self.add_cache_context: - kwargs["cache_context"] = _CacheContext(cache, cache_key) + kwargs["cache_context"] = _CacheContext.get_instance(cache, cache_key) try: cached_result_d = cache.get(cache_key, callback=invalidate_callback) @@ -624,14 +626,38 @@ class CacheListDescriptor(_CacheDescriptorBase): return wrapped -class _CacheContext(namedtuple("_CacheContext", ("cache", "key"))): - # We rely on _CacheContext implementing __eq__ and __hash__ sensibly, - # which namedtuple does for us (i.e. two _CacheContext are the same if - # their caches and keys match). This is important in particular to - # dedupe when we add callbacks to lru cache nodes, otherwise the number - # of callbacks would grow. - def invalidate(self): - self.cache.invalidate(self.key) +class _CacheContext: + """Holds cache information from the cached function higher in the calling order. + + Can be used to invalidate the higher level cache entry if something changes + on a lower level. + """ + + _cache_context_objects = ( + WeakValueDictionary() + ) # type: WeakValueDictionary[Tuple[Cache, CacheKey], _CacheContext] + + def __init__(self, cache, cache_key): # type: (Cache, CacheKey) -> None + self._cache = cache + self._cache_key = cache_key + + def invalidate(self): # type: () -> None + """Invalidates the cache entry referred to by the context.""" + self._cache.invalidate(self._cache_key) + + @classmethod + def get_instance(cls, cache, cache_key): # type: (Cache, CacheKey) -> _CacheContext + """Returns an instance constructed with the given arguments. + + A new instance is only created if none already exists. + """ + + # We make sure there are no identical _CacheContext instances. This is + # important in particular to dedupe when we add callbacks to lru cache + # nodes, otherwise the number of callbacks would grow. + return cls._cache_context_objects.setdefault( + (cache, cache_key), cls(cache, cache_key) + ) def cached( From b03cddaeb99437311094fbe617ae2a6bde4c5615 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 7 Nov 2019 09:46:25 +0000 Subject: [PATCH 0472/1623] tweak changelog --- changelog.d/6213.bugfix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/6213.bugfix b/changelog.d/6213.bugfix index 072264fba3..2bb2d08851 100644 --- a/changelog.d/6213.bugfix +++ b/changelog.d/6213.bugfix @@ -1 +1 @@ -Fix LruCache callback deduplication. +Fix LruCache callback deduplication for Python 3.8. Contributed by @V02460. From 3f9b61ff9504dd88cac17fb3cb097e319babd2a3 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 7 Nov 2019 11:49:37 +0000 Subject: [PATCH 0473/1623] Fix the SQL SELECT query in _paginate_room_events_txn Doing a SELECT DISTINCT when paginating is quite expensive, because it requires the engine to do sorting on the entire events table. However, we only need to run it if we're filtering on 2+ labels, so this PR is changing the request so that DISTINCT is only used then. --- synapse/storage/data_stores/main/stream.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/synapse/storage/data_stores/main/stream.py b/synapse/storage/data_stores/main/stream.py index 616ef91d4e..ef0b1426d1 100644 --- a/synapse/storage/data_stores/main/stream.py +++ b/synapse/storage/data_stores/main/stream.py @@ -871,14 +871,25 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): args.append(int(limit)) + # Using DISTINCT in this SELECT query is quite expensive, because it requires the + # engine to sort on the entire (not limited) result set, i.e. the entire events + # table. We only need to use it when we're filtering on more than two labels, + # because that's the only scenario in which we can possibly to get multiple times + # the same event ID in the results. + if event_filter.labels and len(event_filter.labels) > 1: + select_keywords = "SELECT DISTINCT" + + else: + select_keywords = "SELECT" + sql = ( - "SELECT DISTINCT event_id, topological_ordering, stream_ordering" + "%(select_keywords)s event_id, topological_ordering, stream_ordering" " FROM events" " LEFT JOIN event_labels USING (event_id, room_id, topological_ordering)" " WHERE outlier = ? AND room_id = ? AND %(bounds)s" " ORDER BY topological_ordering %(order)s," " stream_ordering %(order)s LIMIT ?" - ) % {"bounds": bounds, "order": order} + ) % {"select_keywords": select_keywords, "bounds": bounds, "order": order} txn.execute(sql, args) From 4f519d556e32ac29a960977df4ed14c42290af5e Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 7 Nov 2019 11:51:54 +0000 Subject: [PATCH 0474/1623] Changelog --- changelog.d/6340.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6340.feature diff --git a/changelog.d/6340.feature b/changelog.d/6340.feature new file mode 100644 index 0000000000..78a187a1dc --- /dev/null +++ b/changelog.d/6340.feature @@ -0,0 +1 @@ +Implement label-based filtering on `/sync` and `/messages` ([MSC2326](https://github.com/matrix-org/matrix-doc/pull/2326)). From 15a1a02e70ca18d8af9a4faaf1bb40427ea6a643 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 7 Nov 2019 12:04:37 +0000 Subject: [PATCH 0475/1623] Handle lack of filter --- synapse/storage/data_stores/main/stream.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/synapse/storage/data_stores/main/stream.py b/synapse/storage/data_stores/main/stream.py index ef0b1426d1..bb70a0f38a 100644 --- a/synapse/storage/data_stores/main/stream.py +++ b/synapse/storage/data_stores/main/stream.py @@ -876,11 +876,9 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): # table. We only need to use it when we're filtering on more than two labels, # because that's the only scenario in which we can possibly to get multiple times # the same event ID in the results. - if event_filter.labels and len(event_filter.labels) > 1: - select_keywords = "SELECT DISTINCT" - - else: - select_keywords = "SELECT" + select_keywords = "SELECT" + if event_filter and event_filter.labels and len(event_filter.labels) > 1: + select_keywords += "DISTINCT" sql = ( "%(select_keywords)s event_id, topological_ordering, stream_ordering" From 70804392ae42949109e55e4e51566e2545e1df63 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 7 Nov 2019 14:55:10 +0000 Subject: [PATCH 0476/1623] Only join on event_labels if we're filtering on labels --- synapse/storage/data_stores/main/stream.py | 33 ++++++++++++++++------ 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/synapse/storage/data_stores/main/stream.py b/synapse/storage/data_stores/main/stream.py index bb70a0f38a..064f602a65 100644 --- a/synapse/storage/data_stores/main/stream.py +++ b/synapse/storage/data_stores/main/stream.py @@ -871,23 +871,38 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): args.append(int(limit)) - # Using DISTINCT in this SELECT query is quite expensive, because it requires the - # engine to sort on the entire (not limited) result set, i.e. the entire events - # table. We only need to use it when we're filtering on more than two labels, - # because that's the only scenario in which we can possibly to get multiple times - # the same event ID in the results. select_keywords = "SELECT" - if event_filter and event_filter.labels and len(event_filter.labels) > 1: - select_keywords += "DISTINCT" + join_clause = "" + if event_filter and event_filter.labels: + # If we're not filtering on a label, then joining on event_labels will + # return as many row for a single event as the number of labels it has. To + # avoid this, only join if we're filtering on at least one label. + join_clause = ( + "LEFT JOIN event_labels" + " USING (event_id, room_id, topological_ordering)" + ) + if len(event_filter.labels) > 1: + # Using DISTINCT in this SELECT query is quite expensive, because it + # requires the engine to sort on the entire (not limited) result set, + # i.e. the entire events table. We only need to use it when we're + # filtering on more than two labels, because that's the only scenario + # in which we can possibly to get multiple times the same event ID in + # the results. + select_keywords += "DISTINCT" sql = ( "%(select_keywords)s event_id, topological_ordering, stream_ordering" " FROM events" - " LEFT JOIN event_labels USING (event_id, room_id, topological_ordering)" + " %(join_clause)s" " WHERE outlier = ? AND room_id = ? AND %(bounds)s" " ORDER BY topological_ordering %(order)s," " stream_ordering %(order)s LIMIT ?" - ) % {"select_keywords": select_keywords, "bounds": bounds, "order": order} + ) % { + "select_keywords": select_keywords, + "join_clause": join_clause, + "bounds": bounds, + "order": order + } txn.execute(sql, args) From b9cba079628db11951fc5ed30b03e8825eb954d7 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 7 Nov 2019 14:57:15 +0000 Subject: [PATCH 0477/1623] Lint --- synapse/storage/data_stores/main/stream.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/synapse/storage/data_stores/main/stream.py b/synapse/storage/data_stores/main/stream.py index 064f602a65..90bb2d8e8f 100644 --- a/synapse/storage/data_stores/main/stream.py +++ b/synapse/storage/data_stores/main/stream.py @@ -876,7 +876,7 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): if event_filter and event_filter.labels: # If we're not filtering on a label, then joining on event_labels will # return as many row for a single event as the number of labels it has. To - # avoid this, only join if we're filtering on at least one label. + # avoid this, only join if we're filtering on at least one label. join_clause = ( "LEFT JOIN event_labels" " USING (event_id, room_id, topological_ordering)" @@ -901,7 +901,7 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): "select_keywords": select_keywords, "join_clause": join_clause, "bounds": bounds, - "order": order + "order": order, } txn.execute(sql, args) From bb78276bdc77c0462696529c27fd8a6062b37afc Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 7 Nov 2019 15:25:27 +0000 Subject: [PATCH 0478/1623] Incorporate review --- .../data_stores/main/events_bg_updates.py | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/synapse/storage/data_stores/main/events_bg_updates.py b/synapse/storage/data_stores/main/events_bg_updates.py index 309bfcafe5..9714662c11 100644 --- a/synapse/storage/data_stores/main/events_bg_updates.py +++ b/synapse/storage/data_stores/main/events_bg_updates.py @@ -525,43 +525,38 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): (last_event_id, batch_size), ) - rows = self.cursor_to_dict(txn) - if not rows: - return True, 0 - - for row in rows: - event_id = row["event_id"] - event_json = json.loads(row["json"]) - + nbrows = 0 + for (event_id, event_json) in txn: self._simple_insert_many_txn( txn=txn, table="event_labels", values=[ { "event_id": event_id, - "label": label, + "label": str(label), "room_id": event_json["room_id"], "topological_ordering": event_json["depth"], } for label in event_json["content"].get( EventContentFields.LABELS, [] ) + if label is not None ], ) + nbrows += 1 + self._background_update_progress_txn( txn, "event_store_labels", {"last_event_id": event_id} ) - # We want to return true (to end the background update) only when - # the query returned with less rows than we asked for. - return len(rows) != batch_size, len(rows) + return nbrows - end, num_rows = yield self.runInteraction( + num_rows = yield self.runInteraction( desc="event_store_labels", func=_event_store_labels_txn ) - if end: + if not num_rows: yield self._end_background_update("event_store_labels") return num_rows From ec2cb9f29829cf81f843124ef4289bc0eb88413e Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 7 Nov 2019 16:18:40 +0000 Subject: [PATCH 0479/1623] Initialise value before looping --- .../storage/data_stores/main/events_bg_updates.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/synapse/storage/data_stores/main/events_bg_updates.py b/synapse/storage/data_stores/main/events_bg_updates.py index 9714662c11..bee8c9cfa0 100644 --- a/synapse/storage/data_stores/main/events_bg_updates.py +++ b/synapse/storage/data_stores/main/events_bg_updates.py @@ -526,28 +526,32 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): ) nbrows = 0 - for (event_id, event_json) in txn: + last_row_event_id = "" + for (event_id, event_json_raw) in txn: + event_json = json.loads(event_json_raw) + self._simple_insert_many_txn( txn=txn, table="event_labels", values=[ { "event_id": event_id, - "label": str(label), + "label": label, "room_id": event_json["room_id"], "topological_ordering": event_json["depth"], } for label in event_json["content"].get( EventContentFields.LABELS, [] ) - if label is not None + if label is not None and isinstance(label, str) ], ) nbrows += 1 + last_row_event_id = event_id self._background_update_progress_txn( - txn, "event_store_labels", {"last_event_id": event_id} + txn, "event_store_labels", {"last_event_id": last_row_event_id} ) return nbrows From 1186612d6cd728e6b7ed7806579db3cea7410b54 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 7 Nov 2019 16:46:41 +0000 Subject: [PATCH 0480/1623] Back to using cursor_to_dict --- .../data_stores/main/events_bg_updates.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/synapse/storage/data_stores/main/events_bg_updates.py b/synapse/storage/data_stores/main/events_bg_updates.py index bee8c9cfa0..b858e3ac1a 100644 --- a/synapse/storage/data_stores/main/events_bg_updates.py +++ b/synapse/storage/data_stores/main/events_bg_updates.py @@ -525,10 +525,13 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): (last_event_id, batch_size), ) - nbrows = 0 - last_row_event_id = "" - for (event_id, event_json_raw) in txn: - event_json = json.loads(event_json_raw) + rows = self.cursor_to_dict(txn) + if not len(rows): + return 0 + + for row in rows: + event_id = row["event_id"] + event_json = json.loads(row["event_json"]) self._simple_insert_many_txn( txn=txn, @@ -547,14 +550,11 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): ], ) - nbrows += 1 - last_row_event_id = event_id - self._background_update_progress_txn( - txn, "event_store_labels", {"last_event_id": last_row_event_id} + txn, "event_store_labels", {"last_event_id": event_id} ) - return nbrows + return len(rows) num_rows = yield self.runInteraction( desc="event_store_labels", func=_event_store_labels_txn From cd312012676d39da8d0383cba167d1211378fece Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 7 Nov 2019 16:47:15 +0000 Subject: [PATCH 0481/1623] Revert "Back to using cursor_to_dict" This reverts commit 1186612d6cd728e6b7ed7806579db3cea7410b54. --- .../data_stores/main/events_bg_updates.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/synapse/storage/data_stores/main/events_bg_updates.py b/synapse/storage/data_stores/main/events_bg_updates.py index b858e3ac1a..bee8c9cfa0 100644 --- a/synapse/storage/data_stores/main/events_bg_updates.py +++ b/synapse/storage/data_stores/main/events_bg_updates.py @@ -525,13 +525,10 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): (last_event_id, batch_size), ) - rows = self.cursor_to_dict(txn) - if not len(rows): - return 0 - - for row in rows: - event_id = row["event_id"] - event_json = json.loads(row["event_json"]) + nbrows = 0 + last_row_event_id = "" + for (event_id, event_json_raw) in txn: + event_json = json.loads(event_json_raw) self._simple_insert_many_txn( txn=txn, @@ -550,11 +547,14 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): ], ) + nbrows += 1 + last_row_event_id = event_id + self._background_update_progress_txn( - txn, "event_store_labels", {"last_event_id": event_id} + txn, "event_store_labels", {"last_event_id": last_row_event_id} ) - return len(rows) + return nbrows num_rows = yield self.runInteraction( desc="event_store_labels", func=_event_store_labels_txn From c9b27d00445f84ebfbd4115e3177a10513aadcfc Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 7 Nov 2019 16:47:45 +0000 Subject: [PATCH 0482/1623] Copy results --- synapse/storage/data_stores/main/events_bg_updates.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/synapse/storage/data_stores/main/events_bg_updates.py b/synapse/storage/data_stores/main/events_bg_updates.py index bee8c9cfa0..a4cb64f479 100644 --- a/synapse/storage/data_stores/main/events_bg_updates.py +++ b/synapse/storage/data_stores/main/events_bg_updates.py @@ -525,9 +525,11 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): (last_event_id, batch_size), ) + results = list(txn) + nbrows = 0 last_row_event_id = "" - for (event_id, event_json_raw) in txn: + for (event_id, event_json_raw) in results: event_json = json.loads(event_json_raw) self._simple_insert_many_txn( From 6d360f099f3ea09c90795e5a6c4fc07dbffd874e Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 7 Nov 2019 17:01:43 +0000 Subject: [PATCH 0483/1623] Update synapse/storage/data_stores/main/events_bg_updates.py Co-Authored-By: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> --- synapse/storage/data_stores/main/events_bg_updates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/storage/data_stores/main/events_bg_updates.py b/synapse/storage/data_stores/main/events_bg_updates.py index a4cb64f479..18862adb9c 100644 --- a/synapse/storage/data_stores/main/events_bg_updates.py +++ b/synapse/storage/data_stores/main/events_bg_updates.py @@ -545,7 +545,7 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): for label in event_json["content"].get( EventContentFields.LABELS, [] ) - if label is not None and isinstance(label, str) + if isinstance(label, str) ], ) From dad8d68c9938e7d09eefd6aa1b633494ab89f719 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 7 Nov 2019 17:01:53 +0000 Subject: [PATCH 0484/1623] Update synapse/storage/data_stores/main/events_bg_updates.py Co-Authored-By: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> --- synapse/storage/data_stores/main/events_bg_updates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/storage/data_stores/main/events_bg_updates.py b/synapse/storage/data_stores/main/events_bg_updates.py index 18862adb9c..0ed59ef48e 100644 --- a/synapse/storage/data_stores/main/events_bg_updates.py +++ b/synapse/storage/data_stores/main/events_bg_updates.py @@ -511,7 +511,7 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): @defer.inlineCallbacks def _event_store_labels(self, progress, batch_size): - """Stores labels for events.""" + """Background update handler which will store labels for existing events.""" last_event_id = progress.get("last_event_id", "") def _event_store_labels_txn(txn): From c5abb67e432b9279c838f7b9318144ca8f2b7c0d Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 7 Nov 2019 17:14:13 +0000 Subject: [PATCH 0485/1623] Python 3.8 for tox (#6341) ... and update INSTALL.md to include py3.8. We'll also have to update the buildkite pipeline to run it --- INSTALL.md | 2 +- changelog.d/6341.misc | 1 + tox.ini | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changelog.d/6341.misc diff --git a/INSTALL.md b/INSTALL.md index e7b429c05d..29e0abafd3 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -36,7 +36,7 @@ that your email address is probably `user@example.com` rather than System requirements: - POSIX-compliant system (tested on Linux & OS X) -- Python 3.5, 3.6, or 3.7 +- Python 3.5, 3.6, 3.7 or 3.8. - At least 1GB of free RAM if you want to join large public rooms like #matrix:matrix.org Synapse is written in Python but some of the libraries it uses are written in diff --git a/changelog.d/6341.misc b/changelog.d/6341.misc new file mode 100644 index 0000000000..359b9bf1d7 --- /dev/null +++ b/changelog.d/6341.misc @@ -0,0 +1 @@ +Add continuous integration for python 3.8. \ No newline at end of file diff --git a/tox.ini b/tox.ini index afe9bc909b..62b350ea6a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = packaging, py35, py36, py37, check_codestyle, check_isort +envlist = packaging, py35, py36, py37, py38, check_codestyle, check_isort [base] basepython = python3.7 From e4ec82ce0fdd17f912fad76defd53ec99f782528 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 8 Nov 2019 09:50:48 +0000 Subject: [PATCH 0486/1623] Move type annotation into docstring --- synapse/storage/data_stores/main/events.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/synapse/storage/data_stores/main/events.py b/synapse/storage/data_stores/main/events.py index 946823876a..878f7568a6 100644 --- a/synapse/storage/data_stores/main/events.py +++ b/synapse/storage/data_stores/main/events.py @@ -19,7 +19,6 @@ import itertools import logging from collections import Counter as c_counter, OrderedDict, namedtuple from functools import wraps -from typing import Collection from six import iteritems, text_type from six.moves import range @@ -1737,7 +1736,7 @@ class EventsStore( return state_groups def purge_unreferenced_state_groups( - self, room_id: str, state_groups_to_delete: Collection[int] + self, room_id: str, state_groups_to_delete ) -> defer.Deferred: """Deletes no longer referenced state groups and de-deltas any state groups that reference them. @@ -1745,7 +1744,8 @@ class EventsStore( Args: room_id: The room the state groups belong to (must all be in the same room). - state_groups_to_delete: Set of all state groups to delete. + state_groups_to_delete (Collection[int]): Set of all state groups + to delete. """ return self.runInteraction( From b16fa433860824560c64acf594aa6c891e5f1a0a Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 8 Nov 2019 10:34:09 +0000 Subject: [PATCH 0487/1623] Incorporate review --- synapse/storage/data_stores/main/stream.py | 24 +++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/synapse/storage/data_stores/main/stream.py b/synapse/storage/data_stores/main/stream.py index 90bb2d8e8f..8780fdd989 100644 --- a/synapse/storage/data_stores/main/stream.py +++ b/synapse/storage/data_stores/main/stream.py @@ -877,10 +877,10 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): # If we're not filtering on a label, then joining on event_labels will # return as many row for a single event as the number of labels it has. To # avoid this, only join if we're filtering on at least one label. - join_clause = ( - "LEFT JOIN event_labels" - " USING (event_id, room_id, topological_ordering)" - ) + join_clause = """ + LEFT JOIN event_labels + USING (event_id, room_id, topological_ordering) + """ if len(event_filter.labels) > 1: # Using DISTINCT in this SELECT query is quite expensive, because it # requires the engine to sort on the entire (not limited) result set, @@ -890,14 +890,14 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): # the results. select_keywords += "DISTINCT" - sql = ( - "%(select_keywords)s event_id, topological_ordering, stream_ordering" - " FROM events" - " %(join_clause)s" - " WHERE outlier = ? AND room_id = ? AND %(bounds)s" - " ORDER BY topological_ordering %(order)s," - " stream_ordering %(order)s LIMIT ?" - ) % { + sql = """ + %(select_keywords)s event_id, topological_ordering, stream_ordering + FROM events + %(join_clause)s + WHERE outlier = ? AND room_id = ? AND %(bounds)s + ORDER BY topological_ordering %(order)s, + stream_ordering %(order)s LIMIT ? + """ % { "select_keywords": select_keywords, "join_clause": join_clause, "bounds": bounds, From 772d414975608c03d5690e2d8f65c7f382403a99 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 11 Oct 2019 16:05:21 +0100 Subject: [PATCH 0488/1623] Simplify _update_auth_events_and_context_for_auth move event_key calculation into _update_context_for_auth_events, since it's only used there. --- synapse/handlers/federation.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 05dd8d2671..ab152e8dcd 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -2098,11 +2098,6 @@ class FederationHandler(BaseHandler): """ event_auth_events = set(event.auth_event_ids()) - if event.is_state(): - event_key = (event.type, event.state_key) - else: - event_key = None - # if the event's auth_events refers to events which are not in our # calculated auth_events, we need to fetch those events from somewhere. # @@ -2231,13 +2226,13 @@ class FederationHandler(BaseHandler): auth_events.update(new_state) context = yield self._update_context_for_auth_events( - event, context, auth_events, event_key + event, context, auth_events ) return context @defer.inlineCallbacks - def _update_context_for_auth_events(self, event, context, auth_events, event_key): + def _update_context_for_auth_events(self, event, context, auth_events): """Update the state_ids in an event context after auth event resolution, storing the changes as a new state group. @@ -2246,18 +2241,21 @@ class FederationHandler(BaseHandler): context (synapse.events.snapshot.EventContext): initial event context - auth_events (dict[(str, str)->str]): Events to update in the event + auth_events (dict[(str, str)->EventBase]): Events to update in the event context. - event_key ((str, str)): (type, state_key) for the current event. - this will not be included in the current_state in the context. - Returns: Deferred[EventContext]: new event context """ + # exclude the state key of the new event from the current_state in the context. + if event.is_state(): + event_key = (event.type, event.state_key) + else: + event_key = None state_updates = { k: a.event_id for k, a in iteritems(auth_events) if k != event_key } + current_state_ids = yield context.get_current_state_ids(self.store) current_state_ids = dict(current_state_ids) From f8407975e7ad080d04a028771ae5a84590a19da1 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 8 Nov 2019 12:18:20 +0000 Subject: [PATCH 0489/1623] Update some docstrings and comments --- synapse/handlers/federation.py | 39 +++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index ab152e8dcd..4bc4d57efb 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -2040,8 +2040,10 @@ class FederationHandler(BaseHandler): auth_events (dict[(str, str)->synapse.events.EventBase]): Map from (event_type, state_key) to event - What we expect the event's auth_events to be, based on the event's - position in the dag. I think? maybe?? + Normally, our calculated auth_events based on the state of the room + at the event's position in the DAG, though occasionally (eg if the + event is an outlier), may be the auth events claimed by the remote + server. Also NB that this function adds entries to it. Returns: @@ -2091,25 +2093,35 @@ class FederationHandler(BaseHandler): origin (str): event (synapse.events.EventBase): context (synapse.events.snapshot.EventContext): + auth_events (dict[(str, str)->synapse.events.EventBase]): + Map from (event_type, state_key) to event + + Normally, our calculated auth_events based on the state of the room + at the event's position in the DAG, though occasionally (eg if the + event is an outlier), may be the auth events claimed by the remote + server. + + Also NB that this function adds entries to it. Returns: defer.Deferred[EventContext]: updated context """ event_auth_events = set(event.auth_event_ids()) - # if the event's auth_events refers to events which are not in our - # calculated auth_events, we need to fetch those events from somewhere. - # - # we start by fetching them from the store, and then try calling /event_auth/. + # missing_auth is the set of the event's auth_events which we don't yet have + # in auth_events. missing_auth = event_auth_events.difference( e.event_id for e in auth_events.values() ) + # if we have missing events, we need to fetch those events from somewhere. + # + # we start by checking if they are in the store, and then try calling /event_auth/. if missing_auth: # TODO: can we use store.have_seen_events here instead? have_events = yield self.store.get_seen_events_with_rejections(missing_auth) - logger.debug("Got events %s from store", have_events) + logger.debug("Found events %s in the store", have_events) missing_auth.difference_update(have_events.keys()) else: have_events = {} @@ -2164,15 +2176,23 @@ class FederationHandler(BaseHandler): event.auth_event_ids() ) except Exception: - # FIXME: logger.exception("Failed to get auth chain") if event.internal_metadata.is_outlier(): + # XXX: given that, for an outlier, we'll be working with the + # event's *claimed* auth events rather than those we calculated: + # (a) is there any point in this test, since different_auth below will + # obviously be empty + # (b) alternatively, why don't we do it earlier? logger.info("Skipping auth_event fetch for outlier") return context # FIXME: Assumes we have and stored all the state for all the # prev_events + # + # FIXME: what does the fixme above mean? where do prev_events come into + # it, why do we care about the state for those events, and what does "have and + # stored" mean? Seems erik wrote it in c1d860870b different_auth = event_auth_events.difference( e.event_id for e in auth_events.values() ) @@ -2186,6 +2206,9 @@ class FederationHandler(BaseHandler): different_auth, ) + # now we state-resolve between our own idea of the auth events, and the remote's + # idea of them. + room_version = yield self.store.get_room_version(event.room_id) different_events = yield make_deferred_yieldable( From f41027f74678f35ad9e9eb2531c416dd58a65127 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 8 Nov 2019 12:21:28 +0000 Subject: [PATCH 0490/1623] Use get_events_as_list rather than lots of calls to get_event It's more efficient and clearer. --- synapse/handlers/federation.py | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 4bc4d57efb..3d4197ed69 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -2210,26 +2210,18 @@ class FederationHandler(BaseHandler): # idea of them. room_version = yield self.store.get_room_version(event.room_id) + different_event_ids = [ + d for d in different_auth if d in have_events and not have_events[d] + ] - different_events = yield make_deferred_yieldable( - defer.gatherResults( - [ - run_in_background( - self.store.get_event, d, allow_none=True, allow_rejected=False - ) - for d in different_auth - if d in have_events and not have_events[d] - ], - consumeErrors=True, - ) - ).addErrback(unwrapFirstError) + if different_event_ids: + # XXX: currently this checks for redactions but I'm not convinced that is + # necessary? + different_events = yield self.store.get_events_as_list(different_event_ids) - if different_events: local_view = dict(auth_events) remote_view = dict(auth_events) - remote_view.update( - {(d.type, d.state_key): d for d in different_events if d} - ) + remote_view.update({(d.type, d.state_key): d for d in different_events}) new_state = yield self.state_handler.resolve_events( room_version, From c4bdf2d7855dcddb7e294e9dba458884db2be7e0 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Fri, 8 Nov 2019 11:42:55 +0000 Subject: [PATCH 0491/1623] Remove content from being sent for account data rdata stream --- synapse/replication/tcp/streams/_base.py | 6 +++--- synapse/storage/data_stores/main/account_data.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/synapse/replication/tcp/streams/_base.py b/synapse/replication/tcp/streams/_base.py index 9e45429d49..dd87733842 100644 --- a/synapse/replication/tcp/streams/_base.py +++ b/synapse/replication/tcp/streams/_base.py @@ -89,7 +89,7 @@ TagAccountDataStreamRow = namedtuple( ) AccountDataStreamRow = namedtuple( "AccountDataStream", - ("user_id", "room_id", "data_type", "data"), # str # str # str # dict + ("user_id", "room_id", "data_type"), # str # str # str ) GroupsStreamRow = namedtuple( "GroupsStreamRow", @@ -421,8 +421,8 @@ class AccountDataStream(Stream): results = list(room_results) results.extend( - (stream_id, user_id, None, account_data_type, content) - for stream_id, user_id, account_data_type, content in global_results + (stream_id, user_id, None, account_data_type) + for stream_id, user_id, account_data_type in global_results ) return results diff --git a/synapse/storage/data_stores/main/account_data.py b/synapse/storage/data_stores/main/account_data.py index 6afbfc0d74..22093484ed 100644 --- a/synapse/storage/data_stores/main/account_data.py +++ b/synapse/storage/data_stores/main/account_data.py @@ -184,14 +184,14 @@ class AccountDataWorkerStore(SQLBaseStore): current_id(int): The position to fetch up to. Returns: A deferred pair of lists of tuples of stream_id int, user_id string, - room_id string, type string, and content string. + room_id string, and type string. """ if last_room_id == current_id and last_global_id == current_id: return defer.succeed(([], [])) def get_updated_account_data_txn(txn): sql = ( - "SELECT stream_id, user_id, account_data_type, content" + "SELECT stream_id, user_id, account_data_type" " FROM account_data WHERE ? < stream_id AND stream_id <= ?" " ORDER BY stream_id ASC LIMIT ?" ) @@ -199,7 +199,7 @@ class AccountDataWorkerStore(SQLBaseStore): global_results = txn.fetchall() sql = ( - "SELECT stream_id, user_id, room_id, account_data_type, content" + "SELECT stream_id, user_id, room_id, account_data_type" " FROM room_account_data WHERE ? < stream_id AND stream_id <= ?" " ORDER BY stream_id ASC LIMIT ?" ) From 318dd21b4767039014f73917a4dddb9fc885bc56 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Fri, 8 Nov 2019 15:45:08 +0000 Subject: [PATCH 0492/1623] Add changelog --- changelog.d/6333.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6333.bugfix diff --git a/changelog.d/6333.bugfix b/changelog.d/6333.bugfix new file mode 100644 index 0000000000..a25d6ef3cb --- /dev/null +++ b/changelog.d/6333.bugfix @@ -0,0 +1 @@ +Prevent account data syncs getting lost across TCP replication. \ No newline at end of file From cd96b4586f8dfa8a6857e1bee7de2bd9660238d5 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Fri, 8 Nov 2019 15:45:45 +0000 Subject: [PATCH 0493/1623] lint --- synapse/replication/tcp/streams/_base.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/synapse/replication/tcp/streams/_base.py b/synapse/replication/tcp/streams/_base.py index dd87733842..8512923eae 100644 --- a/synapse/replication/tcp/streams/_base.py +++ b/synapse/replication/tcp/streams/_base.py @@ -88,8 +88,7 @@ TagAccountDataStreamRow = namedtuple( "TagAccountDataStreamRow", ("user_id", "room_id", "data") # str # str # dict ) AccountDataStreamRow = namedtuple( - "AccountDataStream", - ("user_id", "room_id", "data_type"), # str # str # str + "AccountDataStream", ("user_id", "room_id", "data_type") # str # str # str ) GroupsStreamRow = namedtuple( "GroupsStreamRow", From 20d687516fdc34a4be19dcbbadd8a9a9726203e4 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 8 Nov 2019 16:17:02 +0000 Subject: [PATCH 0494/1623] newsfile --- changelog.d/6343.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6343.misc diff --git a/changelog.d/6343.misc b/changelog.d/6343.misc new file mode 100644 index 0000000000..d9a44389b9 --- /dev/null +++ b/changelog.d/6343.misc @@ -0,0 +1 @@ +Refactor some code in the event authentication path for clarity. From 4c131b2c78bae793509bea776107a8183274a709 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Mon, 11 Nov 2019 15:47:47 +0000 Subject: [PATCH 0495/1623] Implement v2 API for send_join --- synapse/federation/federation_client.py | 46 +++++++++++++++++++++---- synapse/federation/transport/client.py | 13 ++++++- 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index 545d719652..50ae40504d 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -664,13 +664,7 @@ class FederationClient(FederationBase): @defer.inlineCallbacks def send_request(destination): - time_now = self._clock.time_msec() - _, content = yield self.transport_layer.send_join( - destination=destination, - room_id=pdu.room_id, - event_id=pdu.event_id, - content=pdu.get_pdu_json(time_now), - ) + content = self._do_send_join(destination, pdu) logger.debug("Got content: %s", content) @@ -737,6 +731,44 @@ class FederationClient(FederationBase): return self._try_destination_list("send_join", destinations, send_request) + @defer.inlineCallbacks + def _do_send_join(self, destination, pdu): + time_now = self._clock.time_msec() + + try: + content = yield self.transport_layer.send_join_v2( + destination=destination, + room_id=pdu.room_id, + event_id=pdu.event_id, + content=pdu.get_pdu_json(time_now), + ) + + return content + except HttpResponseException as e: + if e.code in [400, 404]: + err = e.to_synapse_error() + + # If we receive an error response that isn't a generic error, or an + # unrecognised endpoint error, we assume that the remote understands + # the v2 invite API and this is a legitimate error. + if not err.errcode in [Codes.UNKNOWN, Codes.UNRECOGNIZED]: + raise err + else: + raise e.to_synapse_error() + + logger.debug("Couldn't send_join with the v2 API, falling back to the v1 API") + + resp = yield self.transport_layer.send_join_v1( + destination=destination, + room_id=pdu.room_id, + event_id=pdu.event_id, + content=pdu.get_pdu_json(time_now), + ) + + # We expect the v1 API to respond with [200, content], so we only return the + # content. + return resp[1] + @defer.inlineCallbacks def send_invite(self, destination, room_id, event_id, pdu): room_version = yield self.store.get_room_version(room_id) diff --git a/synapse/federation/transport/client.py b/synapse/federation/transport/client.py index 920fa86853..ba68f7c0b4 100644 --- a/synapse/federation/transport/client.py +++ b/synapse/federation/transport/client.py @@ -267,7 +267,7 @@ class TransportLayerClient(object): @defer.inlineCallbacks @log_function - def send_join(self, destination, room_id, event_id, content): + def send_join_v1(self, destination, room_id, event_id, content): path = _create_v1_path("/send_join/%s/%s", room_id, event_id) response = yield self.client.put_json( @@ -276,6 +276,17 @@ class TransportLayerClient(object): return response + @defer.inlineCallbacks + @log_function + def send_join_v2(self, destination, room_id, event_id, content): + path = _create_v2_path("/send_join/%s/%s", room_id, event_id) + + response = yield self.client.put_json( + destination=destination, path=path, data=content + ) + + return response + @defer.inlineCallbacks @log_function def send_leave(self, destination, room_id, event_id, content): From 92527d7b2186a06c204c3c4bff47207252c5dea2 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Mon, 11 Nov 2019 16:20:53 +0000 Subject: [PATCH 0496/1623] Add missing yield --- synapse/federation/federation_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index 50ae40504d..4a8e65c292 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -664,7 +664,7 @@ class FederationClient(FederationBase): @defer.inlineCallbacks def send_request(destination): - content = self._do_send_join(destination, pdu) + content = yield self._do_send_join(destination, pdu) logger.debug("Got content: %s", content) From 1e202a90f15a8f518e4350075d40d0423b64318d Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Mon, 11 Nov 2019 16:26:53 +0000 Subject: [PATCH 0497/1623] Implement v2 API for send_leave --- synapse/federation/federation_client.py | 41 ++++++++++++++++++++++--- synapse/federation/transport/client.py | 20 +++++++++++- 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index 4a8e65c292..289017b2ed 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -879,17 +879,50 @@ class FederationClient(FederationBase): @defer.inlineCallbacks def send_request(destination): time_now = self._clock.time_msec() - _, content = yield self.transport_layer.send_leave( + content = yield self._do_send_leave(destination, pdu) + + logger.debug("Got content: %s", content) + return None + + return self._try_destination_list("send_leave", destinations, send_request) + + @defer.inlineCallbacks + def _do_send_leave(self, destination, pdu): + time_now = self._clock.time_msec() + + try: + content = yield self.transport_layer.send_leave_v2( destination=destination, room_id=pdu.room_id, event_id=pdu.event_id, content=pdu.get_pdu_json(time_now), ) - logger.debug("Got content: %s", content) - return None + return content + except HttpResponseException as e: + if e.code in [400, 404]: + err = e.to_synapse_error() - return self._try_destination_list("send_leave", destinations, send_request) + # If we receive an error response that isn't a generic error, or an + # unrecognised endpoint error, we assume that the remote understands + # the v2 invite API and this is a legitimate error. + if not err.errcode in [Codes.UNKNOWN, Codes.UNRECOGNIZED]: + raise err + else: + raise e.to_synapse_error() + + logger.debug("Couldn't send_leave with the v2 API, falling back to the v1 API") + + resp = yield self.transport_layer.send_leave_v1( + destination=destination, + room_id=pdu.room_id, + event_id=pdu.event_id, + content=pdu.get_pdu_json(time_now), + ) + + # We expect the v1 API to respond with [200, content], so we only return the + # content. + return resp[1] def get_public_rooms( self, diff --git a/synapse/federation/transport/client.py b/synapse/federation/transport/client.py index ba68f7c0b4..df2b5dc91b 100644 --- a/synapse/federation/transport/client.py +++ b/synapse/federation/transport/client.py @@ -289,7 +289,7 @@ class TransportLayerClient(object): @defer.inlineCallbacks @log_function - def send_leave(self, destination, room_id, event_id, content): + def send_leave_v1(self, destination, room_id, event_id, content): path = _create_v1_path("/send_leave/%s/%s", room_id, event_id) response = yield self.client.put_json( @@ -305,6 +305,24 @@ class TransportLayerClient(object): return response + @defer.inlineCallbacks + @log_function + def send_leave_v2(self, destination, room_id, event_id, content): + path = _create_v2_path("/send_leave/%s/%s", room_id, event_id) + + response = yield self.client.put_json( + destination=destination, + path=path, + data=content, + # we want to do our best to send this through. The problem is + # that if it fails, we won't retry it later, so if the remote + # server was just having a momentary blip, the room will be out of + # sync. + ignore_backoff=True, + ) + + return response + @defer.inlineCallbacks @log_function def send_invite_v1(self, destination, room_id, event_id, content): From 74897de01fc271ee04ce0638654d991f5fb8e2fa Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Mon, 11 Nov 2019 16:40:45 +0000 Subject: [PATCH 0498/1623] Add server-side support to the v2 API --- synapse/federation/federation_server.py | 17 ++++++-------- synapse/federation/transport/server.py | 30 +++++++++++++++++++++---- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index d942d77a72..0ce83cd882 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -376,15 +376,12 @@ class FederationServer(FederationBase): res_pdus = await self.handler.on_send_join_request(origin, pdu) time_now = self._clock.time_msec() - return ( - 200, - { - "state": [p.get_pdu_json(time_now) for p in res_pdus["state"]], - "auth_chain": [ - p.get_pdu_json(time_now) for p in res_pdus["auth_chain"] - ], - }, - ) + return { + "state": [p.get_pdu_json(time_now) for p in res_pdus["state"]], + "auth_chain": [ + p.get_pdu_json(time_now) for p in res_pdus["auth_chain"] + ], + } async def on_make_leave_request(self, origin, room_id, user_id): origin_host, _ = parse_server_name(origin) @@ -411,7 +408,7 @@ class FederationServer(FederationBase): pdu = await self._check_sigs_and_hash(room_version, pdu) await self.handler.on_send_leave_request(origin, pdu) - return 200, {} + return {} async def on_event_auth(self, origin, room_id, event_id): with (await self._server_linearizer.queue((origin, room_id))): diff --git a/synapse/federation/transport/server.py b/synapse/federation/transport/server.py index d6c23f22bd..5263e292c1 100644 --- a/synapse/federation/transport/server.py +++ b/synapse/federation/transport/server.py @@ -506,7 +506,15 @@ class FederationMakeLeaveServlet(BaseFederationServlet): return 200, content -class FederationSendLeaveServlet(BaseFederationServlet): +class FederationV1SendLeaveServlet(BaseFederationServlet): + PATH = "/send_leave/(?P[^/]*)/(?P[^/]*)" + + async def on_PUT(self, origin, content, query, room_id, event_id): + content = await self.handler.on_send_leave_request(origin, content, room_id) + return 200, (200, content) + + +class FederationV2SendLeaveServlet(BaseFederationServlet): PATH = "/send_leave/(?P[^/]*)/(?P[^/]*)" async def on_PUT(self, origin, content, query, room_id, event_id): @@ -521,9 +529,21 @@ class FederationEventAuthServlet(BaseFederationServlet): return await self.handler.on_event_auth(origin, context, event_id) -class FederationSendJoinServlet(BaseFederationServlet): +class FederationV1SendJoinServlet(BaseFederationServlet): PATH = "/send_join/(?P[^/]*)/(?P[^/]*)" + async def on_PUT(self, origin, content, query, context, event_id): + # TODO(paul): assert that context/event_id parsed from path actually + # match those given in content + content = await self.handler.on_send_join_request(origin, content, context) + return 200, (200, content) + + +class FederationV2SendJoinServlet(BaseFederationServlet): + PATH = "/send_join/(?P[^/]*)/(?P[^/]*)" + + PREFIX = FEDERATION_V2_PREFIX + async def on_PUT(self, origin, content, query, context, event_id): # TODO(paul): assert that context/event_id parsed from path actually # match those given in content @@ -1367,8 +1387,10 @@ FEDERATION_SERVLET_CLASSES = ( FederationMakeJoinServlet, FederationMakeLeaveServlet, FederationEventServlet, - FederationSendJoinServlet, - FederationSendLeaveServlet, + FederationV1SendJoinServlet, + FederationV2SendJoinServlet, + FederationV1SendLeaveServlet, + FederationV2SendLeaveServlet, FederationV1InviteServlet, FederationV2InviteServlet, FederationQueryAuthServlet, From 5e18dc7955180086870013b55bf5ae326fa6c695 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Mon, 11 Nov 2019 16:46:09 +0000 Subject: [PATCH 0499/1623] Fix prefix for v2/send_leave --- synapse/federation/transport/server.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/synapse/federation/transport/server.py b/synapse/federation/transport/server.py index 5263e292c1..551a162eb7 100644 --- a/synapse/federation/transport/server.py +++ b/synapse/federation/transport/server.py @@ -517,6 +517,8 @@ class FederationV1SendLeaveServlet(BaseFederationServlet): class FederationV2SendLeaveServlet(BaseFederationServlet): PATH = "/send_leave/(?P[^/]*)/(?P[^/]*)" + PREFIX = FEDERATION_V2_PREFIX + async def on_PUT(self, origin, content, query, room_id, event_id): content = await self.handler.on_send_leave_request(origin, content, room_id) return 200, content From edc4c7d4c55b1843c85d452ff92bda17dfff47bc Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Mon, 11 Nov 2019 16:51:54 +0000 Subject: [PATCH 0500/1623] Lint --- synapse/federation/federation_server.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index 0ce83cd882..08a913e08a 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -378,9 +378,7 @@ class FederationServer(FederationBase): time_now = self._clock.time_msec() return { "state": [p.get_pdu_json(time_now) for p in res_pdus["state"]], - "auth_chain": [ - p.get_pdu_json(time_now) for p in res_pdus["auth_chain"] - ], + "auth_chain": [p.get_pdu_json(time_now) for p in res_pdus["auth_chain"]], } async def on_make_leave_request(self, origin, room_id, user_id): From 21056ad12a9dbfadad085802c7a0096d3b071681 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Mon, 11 Nov 2019 16:53:29 +0000 Subject: [PATCH 0501/1623] Changelog --- changelog.d/6349.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6349.feature diff --git a/changelog.d/6349.feature b/changelog.d/6349.feature new file mode 100644 index 0000000000..56c4fbf78e --- /dev/null +++ b/changelog.d/6349.feature @@ -0,0 +1 @@ +Implement v2 APIs for the `send_join` and `send_leave` federation endpoints (as described in [MSC1802](https://github.com/matrix-org/matrix-doc/pull/1802)). From 94cdd6fffed90cceaf0396a66174ddd5c990c8eb Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Mon, 11 Nov 2019 16:56:55 +0000 Subject: [PATCH 0502/1623] Lint --- synapse/federation/federation_client.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index 289017b2ed..23c08104b3 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -751,7 +751,7 @@ class FederationClient(FederationBase): # If we receive an error response that isn't a generic error, or an # unrecognised endpoint error, we assume that the remote understands # the v2 invite API and this is a legitimate error. - if not err.errcode in [Codes.UNKNOWN, Codes.UNRECOGNIZED]: + if err.errcode not in [Codes.UNKNOWN, Codes.UNRECOGNIZED]: raise err else: raise e.to_synapse_error() @@ -878,7 +878,6 @@ class FederationClient(FederationBase): @defer.inlineCallbacks def send_request(destination): - time_now = self._clock.time_msec() content = yield self._do_send_leave(destination, pdu) logger.debug("Got content: %s", content) @@ -906,7 +905,7 @@ class FederationClient(FederationBase): # If we receive an error response that isn't a generic error, or an # unrecognised endpoint error, we assume that the remote understands # the v2 invite API and this is a legitimate error. - if not err.errcode in [Codes.UNKNOWN, Codes.UNRECOGNIZED]: + if err.errcode not in [Codes.UNKNOWN, Codes.UNRECOGNIZED]: raise err else: raise e.to_synapse_error() From bc29a19731c518dbd70f3adefc66061fb4629cee Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Tue, 12 Nov 2019 13:08:12 +0000 Subject: [PATCH 0503/1623] Replace instance variations of homeserver with correct case/spacing --- synapse/__init__.py | 2 +- synapse/_scripts/register_new_matrix_user.py | 6 +++--- synapse/api/errors.py | 2 +- synapse/config/captcha.py | 4 ++-- synapse/config/emailconfig.py | 2 +- synapse/config/server.py | 2 +- synapse/federation/federation_client.py | 6 +++--- synapse/federation/transport/__init__.py | 4 ++-- synapse/federation/transport/client.py | 6 +++--- synapse/federation/transport/server.py | 2 +- synapse/handlers/auth.py | 4 ++-- synapse/handlers/directory.py | 2 +- synapse/handlers/federation.py | 4 ++-- synapse/handlers/profile.py | 6 +++--- synapse/handlers/register.py | 2 +- synapse/handlers/typing.py | 4 ++-- synapse/http/matrixfederationclient.py | 2 +- synapse/util/httpresourcetree.py | 2 +- 18 files changed, 31 insertions(+), 31 deletions(-) diff --git a/synapse/__init__.py b/synapse/__init__.py index ec16f54a49..1c27d68009 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" This is a reference implementation of a Matrix home server. +""" This is a reference implementation of a Matrix homeserver. """ import os diff --git a/synapse/_scripts/register_new_matrix_user.py b/synapse/_scripts/register_new_matrix_user.py index bdcd915bbe..d528450c78 100644 --- a/synapse/_scripts/register_new_matrix_user.py +++ b/synapse/_scripts/register_new_matrix_user.py @@ -144,8 +144,8 @@ def main(): logging.captureWarnings(True) parser = argparse.ArgumentParser( - description="Used to register new users with a given home server when" - " registration has been disabled. The home server must be" + description="Used to register new users with a given homeserver when" + " registration has been disabled. The homeserver must be" " configured with the 'registration_shared_secret' option" " set." ) @@ -202,7 +202,7 @@ def main(): "server_url", default="https://localhost:8448", nargs="?", - help="URL to use to talk to the home server. Defaults to " + help="URL to use to talk to the homeserver. Defaults to " " 'https://localhost:8448'.", ) diff --git a/synapse/api/errors.py b/synapse/api/errors.py index cca92c34ba..5853a54c95 100644 --- a/synapse/api/errors.py +++ b/synapse/api/errors.py @@ -457,7 +457,7 @@ def cs_error(msg, code=Codes.UNKNOWN, **kwargs): class FederationError(RuntimeError): - """ This class is used to inform remote home servers about erroneous + """ This class is used to inform remote homeservers about erroneous PDUs they sent us. FATAL: The remote server could not interpret the source event. diff --git a/synapse/config/captcha.py b/synapse/config/captcha.py index 44bd5c6799..f0171bb5b2 100644 --- a/synapse/config/captcha.py +++ b/synapse/config/captcha.py @@ -35,11 +35,11 @@ class CaptchaConfig(Config): ## Captcha ## # See docs/CAPTCHA_SETUP for full details of configuring this. - # This Home Server's ReCAPTCHA public key. + # This homeserver's ReCAPTCHA public key. # #recaptcha_public_key: "YOUR_PUBLIC_KEY" - # This Home Server's ReCAPTCHA private key. + # This homeserver's ReCAPTCHA private key. # #recaptcha_private_key: "YOUR_PRIVATE_KEY" diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py index 39e7a1dddb..43fad0bf8b 100644 --- a/synapse/config/emailconfig.py +++ b/synapse/config/emailconfig.py @@ -305,7 +305,7 @@ class EmailConfig(Config): # smtp_user: "exampleusername" # smtp_pass: "examplepassword" # require_transport_security: false - # notif_from: "Your Friendly %(app)s Home Server " + # notif_from: "Your Friendly %(app)s homeserver " # app_name: Matrix # # # Enable email notifications by default diff --git a/synapse/config/server.py b/synapse/config/server.py index d556df308d..a04e600fda 100644 --- a/synapse/config/server.py +++ b/synapse/config/server.py @@ -781,7 +781,7 @@ class ServerConfig(Config): "--daemonize", action="store_true", default=None, - help="Daemonize the home server", + help="Daemonize the homeserver", ) server_group.add_argument( "--print-pidfile", diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index 545d719652..27f6aff004 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -177,7 +177,7 @@ class FederationClient(FederationBase): given destination server. Args: - dest (str): The remote home server to ask. + dest (str): The remote homeserver to ask. room_id (str): The room_id to backfill. limit (int): The maximum number of PDUs to return. extremities (list): List of PDU id and origins of the first pdus @@ -227,7 +227,7 @@ class FederationClient(FederationBase): one succeeds. Args: - destinations (list): Which home servers to query + destinations (list): Which homeservers to query event_id (str): event to fetch room_version (str): version of the room outlier (bool): Indicates whether the PDU is an `outlier`, i.e. if @@ -312,7 +312,7 @@ class FederationClient(FederationBase): @defer.inlineCallbacks @log_function def get_state_for_room(self, destination, room_id, event_id): - """Requests all of the room state at a given event from a remote home server. + """Requests all of the room state at a given event from a remote homeserver. Args: destination (str): The remote homeserver to query for the state. diff --git a/synapse/federation/transport/__init__.py b/synapse/federation/transport/__init__.py index d9fcc520a0..5db733af98 100644 --- a/synapse/federation/transport/__init__.py +++ b/synapse/federation/transport/__init__.py @@ -14,9 +14,9 @@ # limitations under the License. """The transport layer is responsible for both sending transactions to remote -home servers and receiving a variety of requests from other home servers. +homeservers and receiving a variety of requests from other homeservers. -By default this is done over HTTPS (and all home servers are required to +By default this is done over HTTPS (and all homeservers are required to support HTTPS), however individual pairings of servers may decide to communicate over a different (albeit still reliable) protocol. """ diff --git a/synapse/federation/transport/client.py b/synapse/federation/transport/client.py index 920fa86853..dc95ab2113 100644 --- a/synapse/federation/transport/client.py +++ b/synapse/federation/transport/client.py @@ -44,7 +44,7 @@ class TransportLayerClient(object): given event. Args: - destination (str): The host name of the remote home server we want + destination (str): The host name of the remote homeserver we want to get the state from. context (str): The name of the context we want the state of event_id (str): The event we want the context at. @@ -68,7 +68,7 @@ class TransportLayerClient(object): given event. Returns the state's event_id's Args: - destination (str): The host name of the remote home server we want + destination (str): The host name of the remote homeserver we want to get the state from. context (str): The name of the context we want the state of event_id (str): The event we want the context at. @@ -91,7 +91,7 @@ class TransportLayerClient(object): """ Requests the pdu with give id and origin from the given server. Args: - destination (str): The host name of the remote home server we want + destination (str): The host name of the remote homeserver we want to get the state from. event_id (str): The id of the event being requested. timeout (int): How long to try (in ms) the destination for before diff --git a/synapse/federation/transport/server.py b/synapse/federation/transport/server.py index d6c23f22bd..09baa9c57d 100644 --- a/synapse/federation/transport/server.py +++ b/synapse/federation/transport/server.py @@ -714,7 +714,7 @@ class PublicRoomList(BaseFederationServlet): This API returns information in the same format as /publicRooms on the client API, but will only ever include local public rooms and hence is - intended for consumption by other home servers. + intended for consumption by other homeservers. GET /publicRooms HTTP/1.1 diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index 7a0f54ca24..c9d0db4823 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -223,7 +223,7 @@ class AuthHandler(BaseHandler): # could continue registration from your phone having clicked the # email auth link on there). It's probably too open to abuse # because it lets unauthenticated clients store arbitrary objects - # on a home server. + # on a homeserver. # Revisit: Assumimg the REST APIs do sensible validation, the data # isn't arbintrary. session["clientdict"] = clientdict @@ -810,7 +810,7 @@ class AuthHandler(BaseHandler): @defer.inlineCallbacks def add_threepid(self, user_id, medium, address, validated_at): # 'Canonicalise' email addresses down to lower case. - # We've now moving towards the Home Server being the entity that + # We've now moving towards the homeserver being the entity that # is responsible for validating threepids used for resetting passwords # on accounts, so in future Synapse will gain knowledge of specific # types (mediums) of threepid. For now, we still use the existing diff --git a/synapse/handlers/directory.py b/synapse/handlers/directory.py index c4632f8984..69051101a6 100644 --- a/synapse/handlers/directory.py +++ b/synapse/handlers/directory.py @@ -283,7 +283,7 @@ class DirectoryHandler(BaseHandler): def on_directory_query(self, args): room_alias = RoomAlias.from_string(args["room_alias"]) if not self.hs.is_mine(room_alias): - raise SynapseError(400, "Room Alias is not hosted on this Home Server") + raise SynapseError(400, "Room Alias is not hosted on this homeserver") result = yield self.get_association_from_room_alias(room_alias) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 05dd8d2671..0e904f2da0 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -97,9 +97,9 @@ class FederationHandler(BaseHandler): """Handles events that originated from federation. Responsible for: a) handling received Pdus before handing them on as Events to the rest - of the home server (including auth and state conflict resoultion) + of the homeserver (including auth and state conflict resoultion) b) converting events that were produced by local clients that may need - to be sent to remote home servers. + to be sent to remote homeservers. c) doing the necessary dances to invite remote users and join remote rooms. """ diff --git a/synapse/handlers/profile.py b/synapse/handlers/profile.py index 22e0a04da4..1e5a4613c9 100644 --- a/synapse/handlers/profile.py +++ b/synapse/handlers/profile.py @@ -152,7 +152,7 @@ class BaseProfileHandler(BaseHandler): by_admin (bool): Whether this change was made by an administrator. """ if not self.hs.is_mine(target_user): - raise SynapseError(400, "User is not hosted on this Home Server") + raise SynapseError(400, "User is not hosted on this homeserver") if not by_admin and target_user != requester.user: raise AuthError(400, "Cannot set another user's displayname") @@ -207,7 +207,7 @@ class BaseProfileHandler(BaseHandler): """target_user is the user whose avatar_url is to be changed; auth_user is the user attempting to make this change.""" if not self.hs.is_mine(target_user): - raise SynapseError(400, "User is not hosted on this Home Server") + raise SynapseError(400, "User is not hosted on this homeserver") if not by_admin and target_user != requester.user: raise AuthError(400, "Cannot set another user's avatar_url") @@ -231,7 +231,7 @@ class BaseProfileHandler(BaseHandler): def on_profile_query(self, args): user = UserID.from_string(args["user_id"]) if not self.hs.is_mine(user): - raise SynapseError(400, "User is not hosted on this Home Server") + raise SynapseError(400, "User is not hosted on this homeserver") just_field = args.get("field", None) diff --git a/synapse/handlers/register.py b/synapse/handlers/register.py index 235f11c322..95806af41e 100644 --- a/synapse/handlers/register.py +++ b/synapse/handlers/register.py @@ -630,7 +630,7 @@ class RegistrationHandler(BaseHandler): # And we add an email pusher for them by default, but only # if email notifications are enabled (so people don't start # getting mail spam where they weren't before if email - # notifs are set up on a home server) + # notifs are set up on a homeserver) if ( self.hs.config.email_enable_notifs and self.hs.config.email_notif_for_new_users diff --git a/synapse/handlers/typing.py b/synapse/handlers/typing.py index ca8ae9fb5b..856337b7e2 100644 --- a/synapse/handlers/typing.py +++ b/synapse/handlers/typing.py @@ -120,7 +120,7 @@ class TypingHandler(object): auth_user_id = auth_user.to_string() if not self.is_mine_id(target_user_id): - raise SynapseError(400, "User is not hosted on this Home Server") + raise SynapseError(400, "User is not hosted on this homeserver") if target_user_id != auth_user_id: raise AuthError(400, "Cannot set another user's typing state") @@ -150,7 +150,7 @@ class TypingHandler(object): auth_user_id = auth_user.to_string() if not self.is_mine_id(target_user_id): - raise SynapseError(400, "User is not hosted on this Home Server") + raise SynapseError(400, "User is not hosted on this homeserver") if target_user_id != auth_user_id: raise AuthError(400, "Cannot set another user's typing state") diff --git a/synapse/http/matrixfederationclient.py b/synapse/http/matrixfederationclient.py index 691380abda..16765d54e0 100644 --- a/synapse/http/matrixfederationclient.py +++ b/synapse/http/matrixfederationclient.py @@ -530,7 +530,7 @@ class MatrixFederationHttpClient(object): """ Builds the Authorization headers for a federation request Args: - destination (bytes|None): The desination home server of the request. + destination (bytes|None): The desination homeserver of the request. May be None if the destination is an identity server, in which case destination_is must be non-None. method (bytes): The HTTP method of the request diff --git a/synapse/util/httpresourcetree.py b/synapse/util/httpresourcetree.py index 1a20c596bf..3c0e8469f3 100644 --- a/synapse/util/httpresourcetree.py +++ b/synapse/util/httpresourcetree.py @@ -20,7 +20,7 @@ logger = logging.getLogger(__name__) def create_resource_tree(desired_tree, root_resource): - """Create the resource tree for this Home Server. + """Create the resource tree for this homeserver. This in unduly complicated because Twisted does not support putting child resources more than 1 level deep at a time. From 73d091be48c6bfa1c41a8d31068d21fc28c26f6e Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Tue, 12 Nov 2019 13:12:25 +0000 Subject: [PATCH 0504/1623] A couple more instances --- synapse/config/server.py | 2 +- synapse/logging/_terse_json.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/synapse/config/server.py b/synapse/config/server.py index a04e600fda..a1bd3c0ae7 100644 --- a/synapse/config/server.py +++ b/synapse/config/server.py @@ -721,7 +721,7 @@ class ServerConfig(Config): # Used by phonehome stats to group together related servers. #server_context: context - # Resource-constrained Homeserver Settings + # Resource-constrained homeserver Settings # # If limit_remote_rooms.enabled is True, the room complexity will be # checked before a user joins a new remote room. If it is above diff --git a/synapse/logging/_terse_json.py b/synapse/logging/_terse_json.py index 0ebbde06f2..76ce7d8808 100644 --- a/synapse/logging/_terse_json.py +++ b/synapse/logging/_terse_json.py @@ -153,7 +153,7 @@ class TerseJSONToTCPLogObserver(object): An IObserver that writes JSON logs to a TCP target. Args: - hs (HomeServer): The Homeserver that is being logged for. + hs (HomeServer): The homeserver that is being logged for. host: The host of the logging target. port: The logging target's port. metadata: Metadata to be added to each log entry. From 85f172ef968a9726cd62bc61fe98e39cdf017e15 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Tue, 12 Nov 2019 13:13:19 +0000 Subject: [PATCH 0505/1623] Add changelog --- changelog.d/6357.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6357.misc diff --git a/changelog.d/6357.misc b/changelog.d/6357.misc new file mode 100644 index 0000000000..a68df0f384 --- /dev/null +++ b/changelog.d/6357.misc @@ -0,0 +1 @@ +Correct spacing/case of various instances of the word "homeserver". \ No newline at end of file From e1648dc5763bda2cf10daafa5beebb4fbdfd2cb5 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Tue, 12 Nov 2019 13:15:59 +0000 Subject: [PATCH 0506/1623] sample config --- docs/sample_config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index d2f4aff826..da7e5f2e21 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -287,7 +287,7 @@ listeners: # Used by phonehome stats to group together related servers. #server_context: context -# Resource-constrained Homeserver Settings +# Resource-constrained homeserver Settings # # If limit_remote_rooms.enabled is True, the room complexity will be # checked before a user joins a new remote room. If it is above @@ -743,11 +743,11 @@ uploads_path: "DATADIR/uploads" ## Captcha ## # See docs/CAPTCHA_SETUP for full details of configuring this. -# This Home Server's ReCAPTCHA public key. +# This homeserver's ReCAPTCHA public key. # #recaptcha_public_key: "YOUR_PUBLIC_KEY" -# This Home Server's ReCAPTCHA private key. +# This homeserver's ReCAPTCHA private key. # #recaptcha_private_key: "YOUR_PRIVATE_KEY" @@ -1270,7 +1270,7 @@ password_config: # smtp_user: "exampleusername" # smtp_pass: "examplepassword" # require_transport_security: false -# notif_from: "Your Friendly %(app)s Home Server " +# notif_from: "Your Friendly %(app)s homeserver " # app_name: Matrix # # # Enable email notifications by default From c350bc2f92d87e46a40f917f65c9e10e0f4999fc Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Wed, 13 Nov 2019 19:09:20 +0000 Subject: [PATCH 0507/1623] Blacklist PurgeRoomTestCase (#6361) --- changelog.d/6361.misc | 1 + tests/rest/admin/test_admin.py | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 changelog.d/6361.misc diff --git a/changelog.d/6361.misc b/changelog.d/6361.misc new file mode 100644 index 0000000000..324d74ebf9 --- /dev/null +++ b/changelog.d/6361.misc @@ -0,0 +1 @@ +Temporarily blacklist the failing unit test PurgeRoomTestCase.test_purge_room. diff --git a/tests/rest/admin/test_admin.py b/tests/rest/admin/test_admin.py index d9f1b95cb0..9575058252 100644 --- a/tests/rest/admin/test_admin.py +++ b/tests/rest/admin/test_admin.py @@ -641,3 +641,5 @@ class PurgeRoomTestCase(unittest.HomeserverTestCase): ) self.assertEqual(count, 0, msg="Rows not purged in {}".format(table)) + + test_purge_room.skip = "Disabled because it's currently broken" From 745a48625d9760374a7d683441185fa8bd2a2aac Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Thu, 14 Nov 2019 12:02:05 +0000 Subject: [PATCH 0508/1623] Fix guest -> real account upgrade with account validity enabled (#6359) --- changelog.d/6359.bugfix | 1 + synapse/storage/_base.py | 9 +++------ 2 files changed, 4 insertions(+), 6 deletions(-) create mode 100644 changelog.d/6359.bugfix diff --git a/changelog.d/6359.bugfix b/changelog.d/6359.bugfix new file mode 100644 index 0000000000..22bf5f642a --- /dev/null +++ b/changelog.d/6359.bugfix @@ -0,0 +1 @@ +Fix bug where upgrading a guest account to a full user would fail when account validity is enabled. \ No newline at end of file diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py index 1a2b7ebe25..ab596fa68d 100644 --- a/synapse/storage/_base.py +++ b/synapse/storage/_base.py @@ -361,14 +361,11 @@ class SQLBaseStore(object): expiration_ts, ) - self._simple_insert_txn( + self._simple_upsert_txn( txn, "account_validity", - values={ - "user_id": user_id, - "expiration_ts_ms": expiration_ts, - "email_sent": False, - }, + keyvalues={"user_id": user_id}, + values={"expiration_ts_ms": expiration_ts, "email_sent": False}, ) def start_profiling(self): From 53b6559a8906ba56f9f50469e0c2bec430614a4e Mon Sep 17 00:00:00 2001 From: James Date: Fri, 15 Nov 2019 05:42:46 +1100 Subject: [PATCH 0509/1623] Add optional python dependencies to snap packaging (#6317) Signed-off-by: James Hebden --- changelog.d/6317.misc | 1 + snap/snapcraft.yaml | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 changelog.d/6317.misc diff --git a/changelog.d/6317.misc b/changelog.d/6317.misc new file mode 100644 index 0000000000..a67d13fa72 --- /dev/null +++ b/changelog.d/6317.misc @@ -0,0 +1 @@ +Add optional python dependencies and dependant binary libraries to snapcraft packaging. diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 1f7df71db2..9e644e8567 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -20,3 +20,23 @@ parts: source: . plugin: python python-version: python3 + python-packages: + - '.[all]' + build-packages: + - libffi-dev + - libturbojpeg0-dev + - libssl-dev + - libxslt1-dev + - libpq-dev + - zlib1g-dev + stage-packages: + - libasn1-8-heimdal + - libgssapi3-heimdal + - libhcrypto4-heimdal + - libheimbase1-heimdal + - libheimntlm0-heimdal + - libhx509-5-heimdal + - libkrb5-26-heimdal + - libldap-2.4-2 + - libpq5 + - libsasl2-2 From 657d614f6a53f3dbfd2858bd85d0f81563db0041 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Fri, 15 Nov 2019 14:02:34 +0000 Subject: [PATCH 0510/1623] Replace UPDATE with UPSERT on device_max_stream_id table (#6363) --- changelog.d/6363.bugfix | 1 + synapse/storage/data_stores/main/deviceinbox.py | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 changelog.d/6363.bugfix diff --git a/changelog.d/6363.bugfix b/changelog.d/6363.bugfix new file mode 100644 index 0000000000..d023b49181 --- /dev/null +++ b/changelog.d/6363.bugfix @@ -0,0 +1 @@ +Fix `to_device` stream ID getting reset every time Synapse restarts, which had the potential to cause unable to decrypt errors. \ No newline at end of file diff --git a/synapse/storage/data_stores/main/deviceinbox.py b/synapse/storage/data_stores/main/deviceinbox.py index f04aad0743..96cd0fb77a 100644 --- a/synapse/storage/data_stores/main/deviceinbox.py +++ b/synapse/storage/data_stores/main/deviceinbox.py @@ -358,8 +358,21 @@ class DeviceInboxStore(DeviceInboxWorkerStore, DeviceInboxBackgroundUpdateStore) def _add_messages_to_local_device_inbox_txn( self, txn, stream_id, messages_by_user_then_device ): - sql = "UPDATE device_max_stream_id" " SET stream_id = ?" " WHERE stream_id < ?" - txn.execute(sql, (stream_id, stream_id)) + # Compatible method of performing an upsert + sql = "SELECT stream_id FROM device_max_stream_id" + + txn.execute(sql) + rows = txn.fetchone() + if rows: + db_stream_id = rows[0] + if db_stream_id < stream_id: + # Insert the new stream_id + sql = "UPDATE device_max_stream_id SET stream_id = ?" + else: + # No rows, perform an insert + sql = "INSERT INTO device_max_stream_id (stream_id) VALUES (?)" + + txn.execute(sql, (stream_id,)) local_by_user_then_device = {} for user_id, messages_by_device in messages_by_user_then_device.items(): From c7376cdfe3efe05942964efcdf8886d66342383c Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 18 Nov 2019 17:10:16 +0000 Subject: [PATCH 0511/1623] Apply suggestions from code review Co-Authored-By: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Co-Authored-By: Brendan Abolivier --- synapse/handlers/auth.py | 4 ++-- synapse/rest/client/v1/login.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index 20c62bd780..0955cf9dba 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -135,8 +135,8 @@ class AuthHandler(BaseHandler): AuthError if the client has completed a login flow, and it gives a different user to `requester` - LimitExceededError if the ratelimiter's failed requests count for this - user is too high too proceed + LimitExceededError if the ratelimiter's failed request count for this + user is too high to proceed """ diff --git a/synapse/rest/client/v1/login.py b/synapse/rest/client/v1/login.py index abc210da57..f8d58afb29 100644 --- a/synapse/rest/client/v1/login.py +++ b/synapse/rest/client/v1/login.py @@ -397,7 +397,7 @@ class LoginRestServlet(RestServlet): raise LoginError(401, "Invalid JWT", errcode=Codes.UNAUTHORIZED) user_id = UserID(user, self.hs.hostname).to_string() - result = yield self._complete_login(user_id, login_submission) + result = yield self._complete_login(user_id, login_submission, create_non_existant_users=True) return result From 7c24d0f443724082376c89f9f75954d81f524a8e Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 19 Nov 2019 13:22:37 +0000 Subject: [PATCH 0512/1623] Lint --- synapse/config/server.py | 39 ++++++++----- synapse/handlers/pagination.py | 17 ++---- synapse/storage/data_stores/main/room.py | 49 ++++++++-------- tests/rest/client/test_retention.py | 73 ++++++++---------------- 4 files changed, 77 insertions(+), 101 deletions(-) diff --git a/synapse/config/server.py b/synapse/config/server.py index aa93a416f1..8a55ffac4f 100644 --- a/synapse/config/server.py +++ b/synapse/config/server.py @@ -19,7 +19,7 @@ import logging import os.path import re from textwrap import indent -from typing import List +from typing import List, Dict, Optional import attr import yaml @@ -287,13 +287,17 @@ class ServerConfig(Config): self.retention_default_min_lifetime = None self.retention_default_max_lifetime = None - self.retention_allowed_lifetime_min = retention_config.get("allowed_lifetime_min") + self.retention_allowed_lifetime_min = retention_config.get( + "allowed_lifetime_min" + ) if self.retention_allowed_lifetime_min is not None: self.retention_allowed_lifetime_min = self.parse_duration( self.retention_allowed_lifetime_min ) - self.retention_allowed_lifetime_max = retention_config.get("allowed_lifetime_max") + self.retention_allowed_lifetime_max = retention_config.get( + "allowed_lifetime_max" + ) if self.retention_allowed_lifetime_max is not None: self.retention_allowed_lifetime_max = self.parse_duration( self.retention_allowed_lifetime_max @@ -302,14 +306,15 @@ class ServerConfig(Config): if ( self.retention_allowed_lifetime_min is not None and self.retention_allowed_lifetime_max is not None - and self.retention_allowed_lifetime_min > self.retention_allowed_lifetime_max + and self.retention_allowed_lifetime_min + > self.retention_allowed_lifetime_max ): raise ConfigError( "Invalid retention policy limits: 'allowed_lifetime_min' can not be" " greater than 'allowed_lifetime_max'" ) - self.retention_purge_jobs = [] + self.retention_purge_jobs = [] # type: List[Dict[str, Optional[int]]] for purge_job_config in retention_config.get("purge_jobs", []): interval_config = purge_job_config.get("interval") @@ -342,18 +347,22 @@ class ServerConfig(Config): " 'longest_max_lifetime' value." ) - self.retention_purge_jobs.append({ - "interval": interval, - "shortest_max_lifetime": shortest_max_lifetime, - "longest_max_lifetime": longest_max_lifetime, - }) + self.retention_purge_jobs.append( + { + "interval": interval, + "shortest_max_lifetime": shortest_max_lifetime, + "longest_max_lifetime": longest_max_lifetime, + } + ) if not self.retention_purge_jobs: - self.retention_purge_jobs = [{ - "interval": self.parse_duration("1d"), - "shortest_max_lifetime": None, - "longest_max_lifetime": None, - }] + self.retention_purge_jobs = [ + { + "interval": self.parse_duration("1d"), + "shortest_max_lifetime": None, + "longest_max_lifetime": None, + } + ] self.listeners = [] # type: List[dict] for listener in config.get("listeners", []): diff --git a/synapse/handlers/pagination.py b/synapse/handlers/pagination.py index e1800177fa..d122c11a4d 100644 --- a/synapse/handlers/pagination.py +++ b/synapse/handlers/pagination.py @@ -154,20 +154,17 @@ class PaginationHandler(object): # Figure out what token we should start purging at. ts = self.clock.time_msec() - max_lifetime - stream_ordering = ( - yield self.store.find_first_stream_ordering_after_ts(ts) - ) + stream_ordering = yield self.store.find_first_stream_ordering_after_ts(ts) - r = ( - yield self.store.get_room_event_after_stream_ordering( - room_id, stream_ordering, - ) + r = yield self.store.get_room_event_after_stream_ordering( + room_id, stream_ordering, ) if not r: logger.warning( "[purge] purging events not possible: No event found " "(ts %i => stream_ordering %i)", - ts, stream_ordering, + ts, + stream_ordering, ) continue @@ -186,9 +183,7 @@ class PaginationHandler(object): # the background so that it's not blocking any other operation apart from # other purges in the same room. run_as_background_process( - "_purge_history", - self._purge_history, - purge_id, room_id, token, True, + "_purge_history", self._purge_history, purge_id, room_id, token, True, ) def start_purge_history(self, room_id, token, delete_local_events=False): diff --git a/synapse/storage/data_stores/main/room.py b/synapse/storage/data_stores/main/room.py index 54a7d24c73..7fceae59ca 100644 --- a/synapse/storage/data_stores/main/room.py +++ b/synapse/storage/data_stores/main/room.py @@ -334,8 +334,9 @@ class RoomStore(RoomWorkerStore, SearchStore): WHERE state.room_id > ? AND state.type = '%s' ORDER BY state.room_id ASC LIMIT ?; - """ % EventTypes.Retention, - (last_room, batch_size) + """ + % EventTypes.Retention, + (last_room, batch_size), ) rows = self.cursor_to_dict(txn) @@ -358,15 +359,13 @@ class RoomStore(RoomWorkerStore, SearchStore): "event_id": row["event_id"], "min_lifetime": retention_policy.get("min_lifetime"), "max_lifetime": retention_policy.get("max_lifetime"), - } + }, ) logger.info("Inserted %d rows into room_retention", len(rows)) self._background_update_progress_txn( - txn, "insert_room_retention", { - "room_id": rows[-1]["room_id"], - } + txn, "insert_room_retention", {"room_id": rows[-1]["room_id"]} ) if batch_size > len(rows): @@ -375,8 +374,7 @@ class RoomStore(RoomWorkerStore, SearchStore): return False end = yield self.runInteraction( - "insert_room_retention", - _background_insert_retention_txn, + "insert_room_retention", _background_insert_retention_txn, ) if end: @@ -585,17 +583,15 @@ class RoomStore(RoomWorkerStore, SearchStore): ) def _store_retention_policy_for_room_txn(self, txn, event): - if ( - hasattr(event, "content") - and ("min_lifetime" in event.content or "max_lifetime" in event.content) + if hasattr(event, "content") and ( + "min_lifetime" in event.content or "max_lifetime" in event.content ): if ( - ("min_lifetime" in event.content and not isinstance( - event.content.get("min_lifetime"), integer_types - )) - or ("max_lifetime" in event.content and not isinstance( - event.content.get("max_lifetime"), integer_types - )) + "min_lifetime" in event.content + and not isinstance(event.content.get("min_lifetime"), integer_types) + ) or ( + "max_lifetime" in event.content + and not isinstance(event.content.get("max_lifetime"), integer_types) ): # Ignore the event if one of the value isn't an integer. return @@ -798,7 +794,9 @@ class RoomStore(RoomWorkerStore, SearchStore): return local_media_mxcs, remote_media_mxcs @defer.inlineCallbacks - def get_rooms_for_retention_period_in_range(self, min_ms, max_ms, include_null=False): + def get_rooms_for_retention_period_in_range( + self, min_ms, max_ms, include_null=False + ): """Retrieves all of the rooms within the given retention range. Optionally includes the rooms which don't have a retention policy. @@ -904,23 +902,24 @@ class RoomStore(RoomWorkerStore, SearchStore): INNER JOIN current_state_events USING (event_id, room_id) WHERE room_id = ?; """, - (room_id,) + (room_id,), ) return self.cursor_to_dict(txn) ret = yield self.runInteraction( - "get_retention_policy_for_room", - get_retention_policy_for_room_txn, + "get_retention_policy_for_room", get_retention_policy_for_room_txn, ) # If we don't know this room ID, ret will be None, in this case return the default # policy. if not ret: - defer.returnValue({ - "min_lifetime": self.config.retention_default_min_lifetime, - "max_lifetime": self.config.retention_default_max_lifetime, - }) + defer.returnValue( + { + "min_lifetime": self.config.retention_default_min_lifetime, + "max_lifetime": self.config.retention_default_max_lifetime, + } + ) row = ret[0] diff --git a/tests/rest/client/test_retention.py b/tests/rest/client/test_retention.py index 7b6f25a838..6bf485c239 100644 --- a/tests/rest/client/test_retention.py +++ b/tests/rest/client/test_retention.py @@ -61,9 +61,7 @@ class RetentionTestCase(unittest.HomeserverTestCase): self.helper.send_state( room_id=room_id, event_type=EventTypes.Retention, - body={ - "max_lifetime": one_day_ms * 4, - }, + body={"max_lifetime": one_day_ms * 4}, tok=self.token, expect_code=400, ) @@ -71,9 +69,7 @@ class RetentionTestCase(unittest.HomeserverTestCase): self.helper.send_state( room_id=room_id, event_type=EventTypes.Retention, - body={ - "max_lifetime": one_hour_ms, - }, + body={"max_lifetime": one_hour_ms}, tok=self.token, expect_code=400, ) @@ -89,9 +85,7 @@ class RetentionTestCase(unittest.HomeserverTestCase): self.helper.send_state( room_id=room_id, event_type=EventTypes.Retention, - body={ - "max_lifetime": lifetime, - }, + body={"max_lifetime": lifetime}, tok=self.token, ) @@ -115,20 +109,12 @@ class RetentionTestCase(unittest.HomeserverTestCase): events = [] # Send a first event, which should be filtered out at the end of the test. - resp = self.helper.send( - room_id=room_id, - body="1", - tok=self.token, - ) + resp = self.helper.send(room_id=room_id, body="1", tok=self.token) # Get the event from the store so that we end up with a FrozenEvent that we can # give to filter_events_for_client. We need to do this now because the event won't # be in the database anymore after it has expired. - events.append(self.get_success( - store.get_event( - resp.get("event_id") - ) - )) + events.append(self.get_success(store.get_event(resp.get("event_id")))) # Advance the time by 2 days. We're using the default retention policy, therefore # after this the first event will still be valid. @@ -143,20 +129,16 @@ class RetentionTestCase(unittest.HomeserverTestCase): valid_event_id = resp.get("event_id") - events.append(self.get_success( - store.get_event( - valid_event_id - ) - )) + events.append(self.get_success(store.get_event(valid_event_id))) # Advance the time by anothe 2 days. After this, the first event should be # outdated but not the second one. self.reactor.advance(one_day_ms * 2 / 1000) # Run filter_events_for_client with our list of FrozenEvents. - filtered_events = self.get_success(filter_events_for_client( - storage, self.user_id, events - )) + filtered_events = self.get_success( + filter_events_for_client(storage, self.user_id, events) + ) # We should only get one event back. self.assertEqual(len(filtered_events), 1, filtered_events) @@ -172,28 +154,22 @@ class RetentionTestCase(unittest.HomeserverTestCase): # Send a first event to the room. This is the event we'll want to be purged at the # end of the test. - resp = self.helper.send( - room_id=room_id, - body="1", - tok=self.token, - ) + resp = self.helper.send(room_id=room_id, body="1", tok=self.token) expired_event_id = resp.get("event_id") # Check that we can retrieve the event. expired_event = self.get_event(room_id, expired_event_id) - self.assertEqual(expired_event.get("content", {}).get("body"), "1", expired_event) + self.assertEqual( + expired_event.get("content", {}).get("body"), "1", expired_event + ) # Advance the time. self.reactor.advance(increment / 1000) # Send another event. We need this because the purge job won't purge the most # recent event in the room. - resp = self.helper.send( - room_id=room_id, - body="2", - tok=self.token, - ) + resp = self.helper.send(room_id=room_id, body="2", tok=self.token) valid_event_id = resp.get("event_id") @@ -240,8 +216,7 @@ class RetentionNoDefaultPolicyTestCase(unittest.HomeserverTestCase): mock_federation_client = Mock(spec=["backfill"]) self.hs = self.setup_test_homeserver( - config=config, - federation_client=mock_federation_client, + config=config, federation_client=mock_federation_client, ) return self.hs @@ -268,9 +243,7 @@ class RetentionNoDefaultPolicyTestCase(unittest.HomeserverTestCase): self.helper.send_state( room_id=room_id, event_type=EventTypes.Retention, - body={ - "max_lifetime": one_day_ms * 35, - }, + body={"max_lifetime": one_day_ms * 35}, tok=self.token, ) @@ -289,18 +262,16 @@ class RetentionNoDefaultPolicyTestCase(unittest.HomeserverTestCase): # Check that we can retrieve the event. expired_event = self.get_event(room_id, first_event_id) - self.assertEqual(expired_event.get("content", {}).get("body"), "1", expired_event) + self.assertEqual( + expired_event.get("content", {}).get("body"), "1", expired_event + ) # Advance the time by a month. self.reactor.advance(one_day_ms * 30 / 1000) # Send another event. We need this because the purge job won't purge the most # recent event in the room. - resp = self.helper.send( - room_id=room_id, - body="2", - tok=self.token, - ) + resp = self.helper.send(room_id=room_id, body="2", tok=self.token) second_event_id = resp.get("event_id") @@ -313,7 +284,9 @@ class RetentionNoDefaultPolicyTestCase(unittest.HomeserverTestCase): ) if expected_code_for_first_event == 200: - self.assertEqual(first_event.get("content", {}).get("body"), "1", first_event) + self.assertEqual( + first_event.get("content", {}).get("body"), "1", first_event + ) # Check that the event that hasn't been purged can still be retrieved. second_event = self.get_event(room_id, second_event_id) From bf9a11c54d3c9ca1e4fa9420b567efceb1e46d5b Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 19 Nov 2019 13:30:04 +0000 Subject: [PATCH 0513/1623] Lint again --- synapse/config/server.py | 2 +- tests/rest/client/test_retention.py | 12 ++---------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/synapse/config/server.py b/synapse/config/server.py index 8a55ffac4f..ed916e1400 100644 --- a/synapse/config/server.py +++ b/synapse/config/server.py @@ -19,7 +19,7 @@ import logging import os.path import re from textwrap import indent -from typing import List, Dict, Optional +from typing import Dict, List, Optional import attr import yaml diff --git a/tests/rest/client/test_retention.py b/tests/rest/client/test_retention.py index 6bf485c239..9e549d8a91 100644 --- a/tests/rest/client/test_retention.py +++ b/tests/rest/client/test_retention.py @@ -121,11 +121,7 @@ class RetentionTestCase(unittest.HomeserverTestCase): self.reactor.advance(one_day_ms * 2 / 1000) # Send another event, which shouldn't get filtered out. - resp = self.helper.send( - room_id=room_id, - body="2", - tok=self.token, - ) + resp = self.helper.send(room_id=room_id, body="2", tok=self.token) valid_event_id = resp.get("event_id") @@ -252,11 +248,7 @@ class RetentionNoDefaultPolicyTestCase(unittest.HomeserverTestCase): def _test_retention(self, room_id, expected_code_for_first_event=200): # Send a first event to the room. This is the event we'll want to be purged at the # end of the test. - resp = self.helper.send( - room_id=room_id, - body="1", - tok=self.token, - ) + resp = self.helper.send(room_id=room_id, body="1", tok=self.token) first_event_id = resp.get("event_id") From 97b863fe32c03ce30c4cd5bda1479c1f9a03da83 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 19 Nov 2019 13:33:58 +0000 Subject: [PATCH 0514/1623] Lint again --- synapse/config/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/config/server.py b/synapse/config/server.py index ed916e1400..4404953e27 100644 --- a/synapse/config/server.py +++ b/synapse/config/server.py @@ -879,7 +879,7 @@ class ServerConfig(Config): # Defaults to `28d`. Set to `null` to disable clearing out of old rows. # #user_ips_max_age: 14d - + # Message retention policy at the server level. # # Room admins and mods can define a retention period for their rooms using the From a6fc6754f8a680aa31e2be2b10d02e953d4ff368 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 19 Nov 2019 14:07:39 +0000 Subject: [PATCH 0515/1623] Fix 3PID invite exchange --- synapse/handlers/federation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 3994137d18..ab82f83625 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -2569,7 +2569,7 @@ class FederationHandler(BaseHandler): event, context = yield self.event_creation_handler.create_new_client_event( builder=builder ) - EventValidator().validate_new(event) + EventValidator().validate_new(event, self.config) return (event, context) @defer.inlineCallbacks From cdd3cb870d2e81fc28073abade2f6eaf4ec54fdd Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 19 Nov 2019 14:40:21 +0000 Subject: [PATCH 0516/1623] Fix worker mode --- synapse/storage/data_stores/main/room.py | 112 +++++++++++------------ 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/synapse/storage/data_stores/main/room.py b/synapse/storage/data_stores/main/room.py index 7fceae59ca..b7f9024811 100644 --- a/synapse/storage/data_stores/main/room.py +++ b/synapse/storage/data_stores/main/room.py @@ -303,6 +303,62 @@ class RoomWorkerStore(SQLBaseStore): else: return None + @cachedInlineCallbacks() + def get_retention_policy_for_room(self, room_id): + """Get the retention policy for a given room. + + If no retention policy has been found for this room, returns a policy defined + by the configured default policy (which has None as both the 'min_lifetime' and + the 'max_lifetime' if no default policy has been defined in the server's + configuration). + + Args: + room_id (str): The ID of the room to get the retention policy of. + + Returns: + dict[int, int]: "min_lifetime" and "max_lifetime" for this room. + """ + + def get_retention_policy_for_room_txn(txn): + txn.execute( + """ + SELECT min_lifetime, max_lifetime FROM room_retention + INNER JOIN current_state_events USING (event_id, room_id) + WHERE room_id = ?; + """, + (room_id,), + ) + + return self.cursor_to_dict(txn) + + ret = yield self.runInteraction( + "get_retention_policy_for_room", get_retention_policy_for_room_txn, + ) + + # If we don't know this room ID, ret will be None, in this case return the default + # policy. + if not ret: + defer.returnValue( + { + "min_lifetime": self.config.retention_default_min_lifetime, + "max_lifetime": self.config.retention_default_max_lifetime, + } + ) + + row = ret[0] + + # If one of the room's policy's attributes isn't defined, use the matching + # attribute from the default policy. + # The default values will be None if no default policy has been defined, or if one + # of the attributes is missing from the default policy. + if row["min_lifetime"] is None: + row["min_lifetime"] = self.config.retention_default_min_lifetime + + if row["max_lifetime"] is None: + row["max_lifetime"] = self.config.retention_default_max_lifetime + + defer.returnValue(row) + class RoomStore(RoomWorkerStore, SearchStore): def __init__(self, db_conn, hs): @@ -878,59 +934,3 @@ class RoomStore(RoomWorkerStore, SearchStore): ) defer.returnValue(rooms) - - @cachedInlineCallbacks() - def get_retention_policy_for_room(self, room_id): - """Get the retention policy for a given room. - - If no retention policy has been found for this room, returns a policy defined - by the configured default policy (which has None as both the 'min_lifetime' and - the 'max_lifetime' if no default policy has been defined in the server's - configuration). - - Args: - room_id (str): The ID of the room to get the retention policy of. - - Returns: - dict[int, int]: "min_lifetime" and "max_lifetime" for this room. - """ - - def get_retention_policy_for_room_txn(txn): - txn.execute( - """ - SELECT min_lifetime, max_lifetime FROM room_retention - INNER JOIN current_state_events USING (event_id, room_id) - WHERE room_id = ?; - """, - (room_id,), - ) - - return self.cursor_to_dict(txn) - - ret = yield self.runInteraction( - "get_retention_policy_for_room", get_retention_policy_for_room_txn, - ) - - # If we don't know this room ID, ret will be None, in this case return the default - # policy. - if not ret: - defer.returnValue( - { - "min_lifetime": self.config.retention_default_min_lifetime, - "max_lifetime": self.config.retention_default_max_lifetime, - } - ) - - row = ret[0] - - # If one of the room's policy's attributes isn't defined, use the matching - # attribute from the default policy. - # The default values will be None if no default policy has been defined, or if one - # of the attributes is missing from the default policy. - if row["min_lifetime"] is None: - row["min_lifetime"] = self.config.retention_default_min_lifetime - - if row["max_lifetime"] is None: - row["max_lifetime"] = self.config.retention_default_max_lifetime - - defer.returnValue(row) From 271c322d08a3d13c986d97cbc40e72eef50e92ba Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 20 Nov 2019 09:29:48 +0000 Subject: [PATCH 0517/1623] Lint --- synapse/rest/client/v1/login.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/synapse/rest/client/v1/login.py b/synapse/rest/client/v1/login.py index f8d58afb29..19eb15003d 100644 --- a/synapse/rest/client/v1/login.py +++ b/synapse/rest/client/v1/login.py @@ -397,7 +397,9 @@ class LoginRestServlet(RestServlet): raise LoginError(401, "Invalid JWT", errcode=Codes.UNAUTHORIZED) user_id = UserID(user, self.hs.hostname).to_string() - result = yield self._complete_login(user_id, login_submission, create_non_existant_users=True) + result = yield self._complete_login( + user_id, login_submission, create_non_existant_users=True + ) return result From 4f5ca455bf17b52d70ab08be043178b4678cc4b8 Mon Sep 17 00:00:00 2001 From: Manuel Stahl <37705355+awesome-manuel@users.noreply.github.com> Date: Wed, 20 Nov 2019 12:49:11 +0100 Subject: [PATCH 0518/1623] Move admin endpoints into separate files (#6308) --- changelog.d/6308.misc | 1 + synapse/rest/admin/__init__.py | 567 +-------------------------------- synapse/rest/admin/groups.py | 46 +++ synapse/rest/admin/rooms.py | 157 +++++++++ synapse/rest/admin/users.py | 406 ++++++++++++++++++++++- 5 files changed, 622 insertions(+), 555 deletions(-) create mode 100644 changelog.d/6308.misc create mode 100644 synapse/rest/admin/groups.py create mode 100644 synapse/rest/admin/rooms.py diff --git a/changelog.d/6308.misc b/changelog.d/6308.misc new file mode 100644 index 0000000000..72be63ba4b --- /dev/null +++ b/changelog.d/6308.misc @@ -0,0 +1 @@ +Move admin endpoints into separate files. Contributed by Awesome Technologies Innovationslabor GmbH. \ No newline at end of file diff --git a/synapse/rest/admin/__init__.py b/synapse/rest/admin/__init__.py index 5c2a2eb593..68a59a3424 100644 --- a/synapse/rest/admin/__init__.py +++ b/synapse/rest/admin/__init__.py @@ -14,62 +14,39 @@ # See the License for the specific language governing permissions and # limitations under the License. -import hashlib -import hmac import logging import platform import re -from six import text_type -from six.moves import http_client - import synapse -from synapse.api.constants import Membership, UserTypes from synapse.api.errors import Codes, NotFoundError, SynapseError from synapse.http.server import JsonResource -from synapse.http.servlet import ( - RestServlet, - assert_params_in_dict, - parse_integer, - parse_json_object_from_request, - parse_string, -) +from synapse.http.servlet import RestServlet, parse_json_object_from_request from synapse.rest.admin._base import ( assert_requester_is_admin, - assert_user_is_admin, historical_admin_path_patterns, ) +from synapse.rest.admin.groups import DeleteGroupAdminRestServlet from synapse.rest.admin.media import ListMediaInRoom, register_servlets_for_media_repo from synapse.rest.admin.purge_room_servlet import PurgeRoomServlet +from synapse.rest.admin.rooms import ShutdownRoomRestServlet from synapse.rest.admin.server_notice_servlet import SendServerNoticeServlet -from synapse.rest.admin.users import UserAdminServlet -from synapse.types import UserID, create_requester -from synapse.util.async_helpers import maybe_awaitable +from synapse.rest.admin.users import ( + AccountValidityRenewServlet, + DeactivateAccountRestServlet, + GetUsersPaginatedRestServlet, + ResetPasswordRestServlet, + SearchUsersRestServlet, + UserAdminServlet, + UserRegisterServlet, + UsersRestServlet, + WhoisRestServlet, +) from synapse.util.versionstring import get_version_string logger = logging.getLogger(__name__) -class UsersRestServlet(RestServlet): - PATTERNS = historical_admin_path_patterns("/users/(?P[^/]*)$") - - def __init__(self, hs): - self.hs = hs - self.auth = hs.get_auth() - self.handlers = hs.get_handlers() - - async def on_GET(self, request, user_id): - target_user = UserID.from_string(user_id) - await assert_requester_is_admin(self.auth, request) - - if not self.hs.is_mine(target_user): - raise SynapseError(400, "Can only users a local user") - - ret = await self.handlers.admin_handler.get_users() - - return 200, ret - - class VersionServlet(RestServlet): PATTERNS = (re.compile("^/_synapse/admin/v1/server_version$"),) @@ -83,159 +60,6 @@ class VersionServlet(RestServlet): return 200, self.res -class UserRegisterServlet(RestServlet): - """ - Attributes: - NONCE_TIMEOUT (int): Seconds until a generated nonce won't be accepted - nonces (dict[str, int]): The nonces that we will accept. A dict of - nonce to the time it was generated, in int seconds. - """ - - PATTERNS = historical_admin_path_patterns("/register") - NONCE_TIMEOUT = 60 - - def __init__(self, hs): - self.handlers = hs.get_handlers() - self.reactor = hs.get_reactor() - self.nonces = {} - self.hs = hs - - def _clear_old_nonces(self): - """ - Clear out old nonces that are older than NONCE_TIMEOUT. - """ - now = int(self.reactor.seconds()) - - for k, v in list(self.nonces.items()): - if now - v > self.NONCE_TIMEOUT: - del self.nonces[k] - - def on_GET(self, request): - """ - Generate a new nonce. - """ - self._clear_old_nonces() - - nonce = self.hs.get_secrets().token_hex(64) - self.nonces[nonce] = int(self.reactor.seconds()) - return 200, {"nonce": nonce} - - async def on_POST(self, request): - self._clear_old_nonces() - - if not self.hs.config.registration_shared_secret: - raise SynapseError(400, "Shared secret registration is not enabled") - - body = parse_json_object_from_request(request) - - if "nonce" not in body: - raise SynapseError(400, "nonce must be specified", errcode=Codes.BAD_JSON) - - nonce = body["nonce"] - - if nonce not in self.nonces: - raise SynapseError(400, "unrecognised nonce") - - # Delete the nonce, so it can't be reused, even if it's invalid - del self.nonces[nonce] - - if "username" not in body: - raise SynapseError( - 400, "username must be specified", errcode=Codes.BAD_JSON - ) - else: - if ( - not isinstance(body["username"], text_type) - or len(body["username"]) > 512 - ): - raise SynapseError(400, "Invalid username") - - username = body["username"].encode("utf-8") - if b"\x00" in username: - raise SynapseError(400, "Invalid username") - - if "password" not in body: - raise SynapseError( - 400, "password must be specified", errcode=Codes.BAD_JSON - ) - else: - if ( - not isinstance(body["password"], text_type) - or len(body["password"]) > 512 - ): - raise SynapseError(400, "Invalid password") - - password = body["password"].encode("utf-8") - if b"\x00" in password: - raise SynapseError(400, "Invalid password") - - admin = body.get("admin", None) - user_type = body.get("user_type", None) - - if user_type is not None and user_type not in UserTypes.ALL_USER_TYPES: - raise SynapseError(400, "Invalid user type") - - got_mac = body["mac"] - - want_mac = hmac.new( - key=self.hs.config.registration_shared_secret.encode(), - digestmod=hashlib.sha1, - ) - want_mac.update(nonce.encode("utf8")) - want_mac.update(b"\x00") - want_mac.update(username) - want_mac.update(b"\x00") - want_mac.update(password) - want_mac.update(b"\x00") - want_mac.update(b"admin" if admin else b"notadmin") - if user_type: - want_mac.update(b"\x00") - want_mac.update(user_type.encode("utf8")) - want_mac = want_mac.hexdigest() - - if not hmac.compare_digest(want_mac.encode("ascii"), got_mac.encode("ascii")): - raise SynapseError(403, "HMAC incorrect") - - # Reuse the parts of RegisterRestServlet to reduce code duplication - from synapse.rest.client.v2_alpha.register import RegisterRestServlet - - register = RegisterRestServlet(self.hs) - - user_id = await register.registration_handler.register_user( - localpart=body["username"].lower(), - password=body["password"], - admin=bool(admin), - user_type=user_type, - ) - - result = await register._create_registration_details(user_id, body) - return 200, result - - -class WhoisRestServlet(RestServlet): - PATTERNS = historical_admin_path_patterns("/whois/(?P[^/]*)") - - def __init__(self, hs): - self.hs = hs - self.auth = hs.get_auth() - self.handlers = hs.get_handlers() - - async def on_GET(self, request, user_id): - target_user = UserID.from_string(user_id) - requester = await self.auth.get_user_by_req(request) - auth_user = requester.user - - if target_user != auth_user: - await assert_user_is_admin(self.auth, auth_user) - - if not self.hs.is_mine(target_user): - raise SynapseError(400, "Can only whois a local user") - - ret = await self.handlers.admin_handler.get_whois(target_user) - - return 200, ret - - class PurgeHistoryRestServlet(RestServlet): PATTERNS = historical_admin_path_patterns( "/purge_history/(?P[^/]*)(/(?P[^/]+))?" @@ -342,369 +166,6 @@ class PurgeHistoryStatusRestServlet(RestServlet): return 200, purge_status.asdict() -class DeactivateAccountRestServlet(RestServlet): - PATTERNS = historical_admin_path_patterns("/deactivate/(?P[^/]*)") - - def __init__(self, hs): - self._deactivate_account_handler = hs.get_deactivate_account_handler() - self.auth = hs.get_auth() - - async def on_POST(self, request, target_user_id): - await assert_requester_is_admin(self.auth, request) - body = parse_json_object_from_request(request, allow_empty_body=True) - erase = body.get("erase", False) - if not isinstance(erase, bool): - raise SynapseError( - http_client.BAD_REQUEST, - "Param 'erase' must be a boolean, if given", - Codes.BAD_JSON, - ) - - UserID.from_string(target_user_id) - - result = await self._deactivate_account_handler.deactivate_account( - target_user_id, erase - ) - if result: - id_server_unbind_result = "success" - else: - id_server_unbind_result = "no-support" - - return 200, {"id_server_unbind_result": id_server_unbind_result} - - -class ShutdownRoomRestServlet(RestServlet): - """Shuts down a room by removing all local users from the room and blocking - all future invites and joins to the room. Any local aliases will be repointed - to a new room created by `new_room_user_id` and kicked users will be auto - joined to the new room. - """ - - PATTERNS = historical_admin_path_patterns("/shutdown_room/(?P[^/]+)") - - DEFAULT_MESSAGE = ( - "Sharing illegal content on this server is not permitted and rooms in" - " violation will be blocked." - ) - - def __init__(self, hs): - self.hs = hs - self.store = hs.get_datastore() - self.state = hs.get_state_handler() - self._room_creation_handler = hs.get_room_creation_handler() - self.event_creation_handler = hs.get_event_creation_handler() - self.room_member_handler = hs.get_room_member_handler() - self.auth = hs.get_auth() - - async def on_POST(self, request, room_id): - requester = await self.auth.get_user_by_req(request) - await assert_user_is_admin(self.auth, requester.user) - - content = parse_json_object_from_request(request) - assert_params_in_dict(content, ["new_room_user_id"]) - new_room_user_id = content["new_room_user_id"] - - room_creator_requester = create_requester(new_room_user_id) - - message = content.get("message", self.DEFAULT_MESSAGE) - room_name = content.get("room_name", "Content Violation Notification") - - info = await self._room_creation_handler.create_room( - room_creator_requester, - config={ - "preset": "public_chat", - "name": room_name, - "power_level_content_override": {"users_default": -10}, - }, - ratelimit=False, - ) - new_room_id = info["room_id"] - - requester_user_id = requester.user.to_string() - - logger.info( - "Shutting down room %r, joining to new room: %r", room_id, new_room_id - ) - - # This will work even if the room is already blocked, but that is - # desirable in case the first attempt at blocking the room failed below. - await self.store.block_room(room_id, requester_user_id) - - users = await self.state.get_current_users_in_room(room_id) - kicked_users = [] - failed_to_kick_users = [] - for user_id in users: - if not self.hs.is_mine_id(user_id): - continue - - logger.info("Kicking %r from %r...", user_id, room_id) - - try: - target_requester = create_requester(user_id) - await self.room_member_handler.update_membership( - requester=target_requester, - target=target_requester.user, - room_id=room_id, - action=Membership.LEAVE, - content={}, - ratelimit=False, - require_consent=False, - ) - - await self.room_member_handler.forget(target_requester.user, room_id) - - await self.room_member_handler.update_membership( - requester=target_requester, - target=target_requester.user, - room_id=new_room_id, - action=Membership.JOIN, - content={}, - ratelimit=False, - require_consent=False, - ) - - kicked_users.append(user_id) - except Exception: - logger.exception( - "Failed to leave old room and join new room for %r", user_id - ) - failed_to_kick_users.append(user_id) - - await self.event_creation_handler.create_and_send_nonmember_event( - room_creator_requester, - { - "type": "m.room.message", - "content": {"body": message, "msgtype": "m.text"}, - "room_id": new_room_id, - "sender": new_room_user_id, - }, - ratelimit=False, - ) - - aliases_for_room = await maybe_awaitable( - self.store.get_aliases_for_room(room_id) - ) - - await self.store.update_aliases_for_room( - room_id, new_room_id, requester_user_id - ) - - return ( - 200, - { - "kicked_users": kicked_users, - "failed_to_kick_users": failed_to_kick_users, - "local_aliases": aliases_for_room, - "new_room_id": new_room_id, - }, - ) - - -class ResetPasswordRestServlet(RestServlet): - """Post request to allow an administrator reset password for a user. - This needs user to have administrator access in Synapse. - Example: - http://localhost:8008/_synapse/admin/v1/reset_password/ - @user:to_reset_password?access_token=admin_access_token - JsonBodyToSend: - { - "new_password": "secret" - } - Returns: - 200 OK with empty object if success otherwise an error. - """ - - PATTERNS = historical_admin_path_patterns( - "/reset_password/(?P[^/]*)" - ) - - def __init__(self, hs): - self.store = hs.get_datastore() - self.hs = hs - self.auth = hs.get_auth() - self._set_password_handler = hs.get_set_password_handler() - - async def on_POST(self, request, target_user_id): - """Post request to allow an administrator reset password for a user. - This needs user to have administrator access in Synapse. - """ - requester = await self.auth.get_user_by_req(request) - await assert_user_is_admin(self.auth, requester.user) - - UserID.from_string(target_user_id) - - params = parse_json_object_from_request(request) - assert_params_in_dict(params, ["new_password"]) - new_password = params["new_password"] - - await self._set_password_handler.set_password( - target_user_id, new_password, requester - ) - return 200, {} - - -class GetUsersPaginatedRestServlet(RestServlet): - """Get request to get specific number of users from Synapse. - This needs user to have administrator access in Synapse. - Example: - http://localhost:8008/_synapse/admin/v1/users_paginate/ - @admin:user?access_token=admin_access_token&start=0&limit=10 - Returns: - 200 OK with json object {list[dict[str, Any]], count} or empty object. - """ - - PATTERNS = historical_admin_path_patterns( - "/users_paginate/(?P[^/]*)" - ) - - def __init__(self, hs): - self.store = hs.get_datastore() - self.hs = hs - self.auth = hs.get_auth() - self.handlers = hs.get_handlers() - - async def on_GET(self, request, target_user_id): - """Get request to get specific number of users from Synapse. - This needs user to have administrator access in Synapse. - """ - await assert_requester_is_admin(self.auth, request) - - target_user = UserID.from_string(target_user_id) - - if not self.hs.is_mine(target_user): - raise SynapseError(400, "Can only users a local user") - - order = "name" # order by name in user table - start = parse_integer(request, "start", required=True) - limit = parse_integer(request, "limit", required=True) - - logger.info("limit: %s, start: %s", limit, start) - - ret = await self.handlers.admin_handler.get_users_paginate(order, start, limit) - return 200, ret - - async def on_POST(self, request, target_user_id): - """Post request to get specific number of users from Synapse.. - This needs user to have administrator access in Synapse. - Example: - http://localhost:8008/_synapse/admin/v1/users_paginate/ - @admin:user?access_token=admin_access_token - JsonBodyToSend: - { - "start": "0", - "limit": "10 - } - Returns: - 200 OK with json object {list[dict[str, Any]], count} or empty object. - """ - await assert_requester_is_admin(self.auth, request) - UserID.from_string(target_user_id) - - order = "name" # order by name in user table - params = parse_json_object_from_request(request) - assert_params_in_dict(params, ["limit", "start"]) - limit = params["limit"] - start = params["start"] - logger.info("limit: %s, start: %s", limit, start) - - ret = await self.handlers.admin_handler.get_users_paginate(order, start, limit) - return 200, ret - - -class SearchUsersRestServlet(RestServlet): - """Get request to search user table for specific users according to - search term. - This needs user to have administrator access in Synapse. - Example: - http://localhost:8008/_synapse/admin/v1/search_users/ - @admin:user?access_token=admin_access_token&term=alice - Returns: - 200 OK with json object {list[dict[str, Any]], count} or empty object. - """ - - PATTERNS = historical_admin_path_patterns("/search_users/(?P[^/]*)") - - def __init__(self, hs): - self.store = hs.get_datastore() - self.hs = hs - self.auth = hs.get_auth() - self.handlers = hs.get_handlers() - - async def on_GET(self, request, target_user_id): - """Get request to search user table for specific users according to - search term. - This needs user to have a administrator access in Synapse. - """ - await assert_requester_is_admin(self.auth, request) - - target_user = UserID.from_string(target_user_id) - - # To allow all users to get the users list - # if not is_admin and target_user != auth_user: - # raise AuthError(403, "You are not a server admin") - - if not self.hs.is_mine(target_user): - raise SynapseError(400, "Can only users a local user") - - term = parse_string(request, "term", required=True) - logger.info("term: %s ", term) - - ret = await self.handlers.admin_handler.search_users(term) - return 200, ret - - -class DeleteGroupAdminRestServlet(RestServlet): - """Allows deleting of local groups - """ - - PATTERNS = historical_admin_path_patterns("/delete_group/(?P[^/]*)") - - def __init__(self, hs): - self.group_server = hs.get_groups_server_handler() - self.is_mine_id = hs.is_mine_id - self.auth = hs.get_auth() - - async def on_POST(self, request, group_id): - requester = await self.auth.get_user_by_req(request) - await assert_user_is_admin(self.auth, requester.user) - - if not self.is_mine_id(group_id): - raise SynapseError(400, "Can only delete local groups") - - await self.group_server.delete_group(group_id, requester.user.to_string()) - return 200, {} - - -class AccountValidityRenewServlet(RestServlet): - PATTERNS = historical_admin_path_patterns("/account_validity/validity$") - - def __init__(self, hs): - """ - Args: - hs (synapse.server.HomeServer): server - """ - self.hs = hs - self.account_activity_handler = hs.get_account_validity_handler() - self.auth = hs.get_auth() - - async def on_POST(self, request): - await assert_requester_is_admin(self.auth, request) - - body = parse_json_object_from_request(request) - - if "user_id" not in body: - raise SynapseError(400, "Missing property 'user_id' in the request body") - - expiration_ts = await self.account_activity_handler.renew_account_for_user( - body["user_id"], - body.get("expiration_ts"), - not body.get("enable_renewal_emails", True), - ) - - res = {"expiration_ts": expiration_ts} - return 200, res - - ######################################################################################## # # please don't add more servlets here: this file is already long and unwieldy. Put diff --git a/synapse/rest/admin/groups.py b/synapse/rest/admin/groups.py new file mode 100644 index 0000000000..0b54ca09f4 --- /dev/null +++ b/synapse/rest/admin/groups.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging + +from synapse.api.errors import SynapseError +from synapse.http.servlet import RestServlet +from synapse.rest.admin._base import ( + assert_user_is_admin, + historical_admin_path_patterns, +) + +logger = logging.getLogger(__name__) + + +class DeleteGroupAdminRestServlet(RestServlet): + """Allows deleting of local groups + """ + + PATTERNS = historical_admin_path_patterns("/delete_group/(?P[^/]*)") + + def __init__(self, hs): + self.group_server = hs.get_groups_server_handler() + self.is_mine_id = hs.is_mine_id + self.auth = hs.get_auth() + + async def on_POST(self, request, group_id): + requester = await self.auth.get_user_by_req(request) + await assert_user_is_admin(self.auth, requester.user) + + if not self.is_mine_id(group_id): + raise SynapseError(400, "Can only delete local groups") + + await self.group_server.delete_group(group_id, requester.user.to_string()) + return 200, {} diff --git a/synapse/rest/admin/rooms.py b/synapse/rest/admin/rooms.py new file mode 100644 index 0000000000..f7cc5e9be9 --- /dev/null +++ b/synapse/rest/admin/rooms.py @@ -0,0 +1,157 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging + +from synapse.api.constants import Membership +from synapse.http.servlet import ( + RestServlet, + assert_params_in_dict, + parse_json_object_from_request, +) +from synapse.rest.admin._base import ( + assert_user_is_admin, + historical_admin_path_patterns, +) +from synapse.types import create_requester +from synapse.util.async_helpers import maybe_awaitable + +logger = logging.getLogger(__name__) + + +class ShutdownRoomRestServlet(RestServlet): + """Shuts down a room by removing all local users from the room and blocking + all future invites and joins to the room. Any local aliases will be repointed + to a new room created by `new_room_user_id` and kicked users will be auto + joined to the new room. + """ + + PATTERNS = historical_admin_path_patterns("/shutdown_room/(?P[^/]+)") + + DEFAULT_MESSAGE = ( + "Sharing illegal content on this server is not permitted and rooms in" + " violation will be blocked." + ) + + def __init__(self, hs): + self.hs = hs + self.store = hs.get_datastore() + self.state = hs.get_state_handler() + self._room_creation_handler = hs.get_room_creation_handler() + self.event_creation_handler = hs.get_event_creation_handler() + self.room_member_handler = hs.get_room_member_handler() + self.auth = hs.get_auth() + + async def on_POST(self, request, room_id): + requester = await self.auth.get_user_by_req(request) + await assert_user_is_admin(self.auth, requester.user) + + content = parse_json_object_from_request(request) + assert_params_in_dict(content, ["new_room_user_id"]) + new_room_user_id = content["new_room_user_id"] + + room_creator_requester = create_requester(new_room_user_id) + + message = content.get("message", self.DEFAULT_MESSAGE) + room_name = content.get("room_name", "Content Violation Notification") + + info = await self._room_creation_handler.create_room( + room_creator_requester, + config={ + "preset": "public_chat", + "name": room_name, + "power_level_content_override": {"users_default": -10}, + }, + ratelimit=False, + ) + new_room_id = info["room_id"] + + requester_user_id = requester.user.to_string() + + logger.info( + "Shutting down room %r, joining to new room: %r", room_id, new_room_id + ) + + # This will work even if the room is already blocked, but that is + # desirable in case the first attempt at blocking the room failed below. + await self.store.block_room(room_id, requester_user_id) + + users = await self.state.get_current_users_in_room(room_id) + kicked_users = [] + failed_to_kick_users = [] + for user_id in users: + if not self.hs.is_mine_id(user_id): + continue + + logger.info("Kicking %r from %r...", user_id, room_id) + + try: + target_requester = create_requester(user_id) + await self.room_member_handler.update_membership( + requester=target_requester, + target=target_requester.user, + room_id=room_id, + action=Membership.LEAVE, + content={}, + ratelimit=False, + require_consent=False, + ) + + await self.room_member_handler.forget(target_requester.user, room_id) + + await self.room_member_handler.update_membership( + requester=target_requester, + target=target_requester.user, + room_id=new_room_id, + action=Membership.JOIN, + content={}, + ratelimit=False, + require_consent=False, + ) + + kicked_users.append(user_id) + except Exception: + logger.exception( + "Failed to leave old room and join new room for %r", user_id + ) + failed_to_kick_users.append(user_id) + + await self.event_creation_handler.create_and_send_nonmember_event( + room_creator_requester, + { + "type": "m.room.message", + "content": {"body": message, "msgtype": "m.text"}, + "room_id": new_room_id, + "sender": new_room_user_id, + }, + ratelimit=False, + ) + + aliases_for_room = await maybe_awaitable( + self.store.get_aliases_for_room(room_id) + ) + + await self.store.update_aliases_for_room( + room_id, new_room_id, requester_user_id + ) + + return ( + 200, + { + "kicked_users": kicked_users, + "failed_to_kick_users": failed_to_kick_users, + "local_aliases": aliases_for_room, + "new_room_id": new_room_id, + }, + ) diff --git a/synapse/rest/admin/users.py b/synapse/rest/admin/users.py index d5d124a0dc..58a83f93af 100644 --- a/synapse/rest/admin/users.py +++ b/synapse/rest/admin/users.py @@ -12,17 +12,419 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import hashlib +import hmac +import logging import re -from synapse.api.errors import SynapseError +from six import text_type +from six.moves import http_client + +from synapse.api.constants import UserTypes +from synapse.api.errors import Codes, SynapseError from synapse.http.servlet import ( RestServlet, assert_params_in_dict, + parse_integer, parse_json_object_from_request, + parse_string, +) +from synapse.rest.admin._base import ( + assert_requester_is_admin, + assert_user_is_admin, + historical_admin_path_patterns, ) -from synapse.rest.admin import assert_requester_is_admin, assert_user_is_admin from synapse.types import UserID +logger = logging.getLogger(__name__) + + +class UsersRestServlet(RestServlet): + PATTERNS = historical_admin_path_patterns("/users/(?P[^/]*)$") + + def __init__(self, hs): + self.hs = hs + self.auth = hs.get_auth() + self.admin_handler = hs.get_handlers().admin_handler + + async def on_GET(self, request, user_id): + target_user = UserID.from_string(user_id) + await assert_requester_is_admin(self.auth, request) + + if not self.hs.is_mine(target_user): + raise SynapseError(400, "Can only users a local user") + + ret = await self.admin_handler.get_users() + + return 200, ret + + +class GetUsersPaginatedRestServlet(RestServlet): + """Get request to get specific number of users from Synapse. + This needs user to have administrator access in Synapse. + Example: + http://localhost:8008/_synapse/admin/v1/users_paginate/ + @admin:user?access_token=admin_access_token&start=0&limit=10 + Returns: + 200 OK with json object {list[dict[str, Any]], count} or empty object. + """ + + PATTERNS = historical_admin_path_patterns( + "/users_paginate/(?P[^/]*)" + ) + + def __init__(self, hs): + self.store = hs.get_datastore() + self.hs = hs + self.auth = hs.get_auth() + self.handlers = hs.get_handlers() + + async def on_GET(self, request, target_user_id): + """Get request to get specific number of users from Synapse. + This needs user to have administrator access in Synapse. + """ + await assert_requester_is_admin(self.auth, request) + + target_user = UserID.from_string(target_user_id) + + if not self.hs.is_mine(target_user): + raise SynapseError(400, "Can only users a local user") + + order = "name" # order by name in user table + start = parse_integer(request, "start", required=True) + limit = parse_integer(request, "limit", required=True) + + logger.info("limit: %s, start: %s", limit, start) + + ret = await self.handlers.admin_handler.get_users_paginate(order, start, limit) + return 200, ret + + async def on_POST(self, request, target_user_id): + """Post request to get specific number of users from Synapse.. + This needs user to have administrator access in Synapse. + Example: + http://localhost:8008/_synapse/admin/v1/users_paginate/ + @admin:user?access_token=admin_access_token + JsonBodyToSend: + { + "start": "0", + "limit": "10 + } + Returns: + 200 OK with json object {list[dict[str, Any]], count} or empty object. + """ + await assert_requester_is_admin(self.auth, request) + UserID.from_string(target_user_id) + + order = "name" # order by name in user table + params = parse_json_object_from_request(request) + assert_params_in_dict(params, ["limit", "start"]) + limit = params["limit"] + start = params["start"] + logger.info("limit: %s, start: %s", limit, start) + + ret = await self.handlers.admin_handler.get_users_paginate(order, start, limit) + return 200, ret + + +class UserRegisterServlet(RestServlet): + """ + Attributes: + NONCE_TIMEOUT (int): Seconds until a generated nonce won't be accepted + nonces (dict[str, int]): The nonces that we will accept. A dict of + nonce to the time it was generated, in int seconds. + """ + + PATTERNS = historical_admin_path_patterns("/register") + NONCE_TIMEOUT = 60 + + def __init__(self, hs): + self.handlers = hs.get_handlers() + self.reactor = hs.get_reactor() + self.nonces = {} + self.hs = hs + + def _clear_old_nonces(self): + """ + Clear out old nonces that are older than NONCE_TIMEOUT. + """ + now = int(self.reactor.seconds()) + + for k, v in list(self.nonces.items()): + if now - v > self.NONCE_TIMEOUT: + del self.nonces[k] + + def on_GET(self, request): + """ + Generate a new nonce. + """ + self._clear_old_nonces() + + nonce = self.hs.get_secrets().token_hex(64) + self.nonces[nonce] = int(self.reactor.seconds()) + return 200, {"nonce": nonce} + + async def on_POST(self, request): + self._clear_old_nonces() + + if not self.hs.config.registration_shared_secret: + raise SynapseError(400, "Shared secret registration is not enabled") + + body = parse_json_object_from_request(request) + + if "nonce" not in body: + raise SynapseError(400, "nonce must be specified", errcode=Codes.BAD_JSON) + + nonce = body["nonce"] + + if nonce not in self.nonces: + raise SynapseError(400, "unrecognised nonce") + + # Delete the nonce, so it can't be reused, even if it's invalid + del self.nonces[nonce] + + if "username" not in body: + raise SynapseError( + 400, "username must be specified", errcode=Codes.BAD_JSON + ) + else: + if ( + not isinstance(body["username"], text_type) + or len(body["username"]) > 512 + ): + raise SynapseError(400, "Invalid username") + + username = body["username"].encode("utf-8") + if b"\x00" in username: + raise SynapseError(400, "Invalid username") + + if "password" not in body: + raise SynapseError( + 400, "password must be specified", errcode=Codes.BAD_JSON + ) + else: + if ( + not isinstance(body["password"], text_type) + or len(body["password"]) > 512 + ): + raise SynapseError(400, "Invalid password") + + password = body["password"].encode("utf-8") + if b"\x00" in password: + raise SynapseError(400, "Invalid password") + + admin = body.get("admin", None) + user_type = body.get("user_type", None) + + if user_type is not None and user_type not in UserTypes.ALL_USER_TYPES: + raise SynapseError(400, "Invalid user type") + + got_mac = body["mac"] + + want_mac = hmac.new( + key=self.hs.config.registration_shared_secret.encode(), + digestmod=hashlib.sha1, + ) + want_mac.update(nonce.encode("utf8")) + want_mac.update(b"\x00") + want_mac.update(username) + want_mac.update(b"\x00") + want_mac.update(password) + want_mac.update(b"\x00") + want_mac.update(b"admin" if admin else b"notadmin") + if user_type: + want_mac.update(b"\x00") + want_mac.update(user_type.encode("utf8")) + want_mac = want_mac.hexdigest() + + if not hmac.compare_digest(want_mac.encode("ascii"), got_mac.encode("ascii")): + raise SynapseError(403, "HMAC incorrect") + + # Reuse the parts of RegisterRestServlet to reduce code duplication + from synapse.rest.client.v2_alpha.register import RegisterRestServlet + + register = RegisterRestServlet(self.hs) + + user_id = await register.registration_handler.register_user( + localpart=body["username"].lower(), + password=body["password"], + admin=bool(admin), + user_type=user_type, + ) + + result = await register._create_registration_details(user_id, body) + return 200, result + + +class WhoisRestServlet(RestServlet): + PATTERNS = historical_admin_path_patterns("/whois/(?P[^/]*)") + + def __init__(self, hs): + self.hs = hs + self.auth = hs.get_auth() + self.handlers = hs.get_handlers() + + async def on_GET(self, request, user_id): + target_user = UserID.from_string(user_id) + requester = await self.auth.get_user_by_req(request) + auth_user = requester.user + + if target_user != auth_user: + await assert_user_is_admin(self.auth, auth_user) + + if not self.hs.is_mine(target_user): + raise SynapseError(400, "Can only whois a local user") + + ret = await self.handlers.admin_handler.get_whois(target_user) + + return 200, ret + + +class DeactivateAccountRestServlet(RestServlet): + PATTERNS = historical_admin_path_patterns("/deactivate/(?P[^/]*)") + + def __init__(self, hs): + self._deactivate_account_handler = hs.get_deactivate_account_handler() + self.auth = hs.get_auth() + + async def on_POST(self, request, target_user_id): + await assert_requester_is_admin(self.auth, request) + body = parse_json_object_from_request(request, allow_empty_body=True) + erase = body.get("erase", False) + if not isinstance(erase, bool): + raise SynapseError( + http_client.BAD_REQUEST, + "Param 'erase' must be a boolean, if given", + Codes.BAD_JSON, + ) + + UserID.from_string(target_user_id) + + result = await self._deactivate_account_handler.deactivate_account( + target_user_id, erase + ) + if result: + id_server_unbind_result = "success" + else: + id_server_unbind_result = "no-support" + + return 200, {"id_server_unbind_result": id_server_unbind_result} + + +class AccountValidityRenewServlet(RestServlet): + PATTERNS = historical_admin_path_patterns("/account_validity/validity$") + + def __init__(self, hs): + """ + Args: + hs (synapse.server.HomeServer): server + """ + self.hs = hs + self.account_activity_handler = hs.get_account_validity_handler() + self.auth = hs.get_auth() + + async def on_POST(self, request): + await assert_requester_is_admin(self.auth, request) + + body = parse_json_object_from_request(request) + + if "user_id" not in body: + raise SynapseError(400, "Missing property 'user_id' in the request body") + + expiration_ts = await self.account_activity_handler.renew_account_for_user( + body["user_id"], + body.get("expiration_ts"), + not body.get("enable_renewal_emails", True), + ) + + res = {"expiration_ts": expiration_ts} + return 200, res + + +class ResetPasswordRestServlet(RestServlet): + """Post request to allow an administrator reset password for a user. + This needs user to have administrator access in Synapse. + Example: + http://localhost:8008/_synapse/admin/v1/reset_password/ + @user:to_reset_password?access_token=admin_access_token + JsonBodyToSend: + { + "new_password": "secret" + } + Returns: + 200 OK with empty object if success otherwise an error. + """ + + PATTERNS = historical_admin_path_patterns( + "/reset_password/(?P[^/]*)" + ) + + def __init__(self, hs): + self.store = hs.get_datastore() + self.hs = hs + self.auth = hs.get_auth() + self._set_password_handler = hs.get_set_password_handler() + + async def on_POST(self, request, target_user_id): + """Post request to allow an administrator reset password for a user. + This needs user to have administrator access in Synapse. + """ + requester = await self.auth.get_user_by_req(request) + await assert_user_is_admin(self.auth, requester.user) + + UserID.from_string(target_user_id) + + params = parse_json_object_from_request(request) + assert_params_in_dict(params, ["new_password"]) + new_password = params["new_password"] + + await self._set_password_handler.set_password( + target_user_id, new_password, requester + ) + return 200, {} + + +class SearchUsersRestServlet(RestServlet): + """Get request to search user table for specific users according to + search term. + This needs user to have administrator access in Synapse. + Example: + http://localhost:8008/_synapse/admin/v1/search_users/ + @admin:user?access_token=admin_access_token&term=alice + Returns: + 200 OK with json object {list[dict[str, Any]], count} or empty object. + """ + + PATTERNS = historical_admin_path_patterns("/search_users/(?P[^/]*)") + + def __init__(self, hs): + self.store = hs.get_datastore() + self.hs = hs + self.auth = hs.get_auth() + self.handlers = hs.get_handlers() + + async def on_GET(self, request, target_user_id): + """Get request to search user table for specific users according to + search term. + This needs user to have a administrator access in Synapse. + """ + await assert_requester_is_admin(self.auth, request) + + target_user = UserID.from_string(target_user_id) + + # To allow all users to get the users list + # if not is_admin and target_user != auth_user: + # raise AuthError(403, "You are not a server admin") + + if not self.hs.is_mine(target_user): + raise SynapseError(400, "Can only users a local user") + + term = parse_string(request, "term", required=True) + logger.info("term: %s ", term) + + ret = await self.handlers.admin_handler.search_users(term) + return 200, ret + class UserAdminServlet(RestServlet): """ From 6356f2088f0adb681fe24a8435955b19883fa3b4 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 20 Nov 2019 12:09:06 +0000 Subject: [PATCH 0519/1623] Test if a purge can make /messages return 500 responses --- tests/rest/client/v1/test_rooms.py | 72 ++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py index 5e38fd6ced..ebaa67e899 100644 --- a/tests/rest/client/v1/test_rooms.py +++ b/tests/rest/client/v1/test_rooms.py @@ -25,7 +25,9 @@ from twisted.internet import defer import synapse.rest.admin from synapse.api.constants import EventContentFields, EventTypes, Membership +from synapse.handlers.pagination import PurgeStatus from synapse.rest.client.v1 import login, profile, room +from synapse.util.stringutils import random_string from tests import unittest @@ -910,6 +912,76 @@ class RoomMessageListTestCase(RoomBase): return channel.json_body["chunk"] + def test_room_messages_purge(self): + store = self.hs.get_datastore() + pagination_handler = self.hs.get_pagination_handler() + + # Send a first message in the room, which will be removed by the purge. + first_event_id = self.helper.send(self.room_id, "message 1")["event_id"] + first_token = self.get_success( + store.get_topological_token_for_event(first_event_id) + ) + + # Send a second message in the room, which won't be removed, and which we'll + # use as the marker to purge events before. + second_event_id = self.helper.send(self.room_id, "message 2")["event_id"] + second_token = self.get_success( + store.get_topological_token_for_event(second_event_id) + ) + + # Send a third event in the room to ensure we don't fall under any edge case + # due to our marker being the latest forward extremity in the room. + self.helper.send(self.room_id, "message 3") + + # Check that we get the first and second message when querying /messages. + request, channel = self.make_request( + "GET", + "/rooms/%s/messages?access_token=x&from=%s&dir=b&filter=%s" + % (self.room_id, second_token, json.dumps({"types": [EventTypes.Message]})), + ) + self.render(request) + self.assertEqual(channel.code, 200, channel.json_body) + + chunk = channel.json_body["chunk"] + self.assertEqual(len(chunk), 2, [event["content"] for event in chunk]) + + # Purge every event before the second event. + purge_id = random_string(16) + pagination_handler._purges_by_id[purge_id] = PurgeStatus() + self.get_success(pagination_handler._purge_history( + purge_id=purge_id, + room_id=self.room_id, + token=second_token, + delete_local_events=True, + )) + + # Check that we only get the second message through /message now that the first + # has been purged. + request, channel = self.make_request( + "GET", + "/rooms/%s/messages?access_token=x&from=%s&dir=b&filter=%s" + % (self.room_id, second_token, json.dumps({"types": [EventTypes.Message]})), + ) + self.render(request) + self.assertEqual(channel.code, 200, channel.json_body) + + chunk = channel.json_body["chunk"] + self.assertEqual(len(chunk), 1, [event["content"] for event in chunk]) + + # Check that we get no event, but also no error, when querying /messages with + # the token that was pointing at the first event, because we don't have it + # anymore. + request, channel = self.make_request( + "GET", + "/rooms/%s/messages?access_token=x&from=%s&dir=b&filter=%s" + % (self.room_id, first_token, json.dumps({"types": [EventTypes.Message]})), + ) + self.render(request) + self.assertEqual(channel.code, 200, channel.json_body) + + chunk = channel.json_body["chunk"] + self.assertEqual(len(chunk), 0, [event["content"] for event in chunk]) + class RoomSearchTestCase(unittest.HomeserverTestCase): servlets = [ From 234f55f3c4295f08399b725cda8a8aa4b559f1f5 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Wed, 20 Nov 2019 13:32:31 +0000 Subject: [PATCH 0520/1623] Docker: Change permissions for data dir before attempting to write to it (#6389) --- changelog.d/6389.bugfix | 1 + docker/start.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 changelog.d/6389.bugfix diff --git a/changelog.d/6389.bugfix b/changelog.d/6389.bugfix new file mode 100644 index 0000000000..c553622b02 --- /dev/null +++ b/changelog.d/6389.bugfix @@ -0,0 +1 @@ +Fix permission denied error when trying to generate a config file with the docker image. \ No newline at end of file diff --git a/docker/start.py b/docker/start.py index 6e1cb807a1..97fd247f8f 100755 --- a/docker/start.py +++ b/docker/start.py @@ -169,11 +169,11 @@ def run_generate_config(environ, ownership): # log("running %s" % (args, )) if ownership is not None: - args = ["su-exec", ownership] + args - os.execv("/sbin/su-exec", args) - # make sure that synapse has perms to write to the data dir. subprocess.check_output(["chown", ownership, data_dir]) + + args = ["su-exec", ownership] + args + os.execv("/sbin/su-exec", args) else: os.execv("/usr/local/bin/python", args) From 41e4566682946fca8600214969c4e9562b5a9315 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Wed, 20 Nov 2019 14:12:42 +0000 Subject: [PATCH 0521/1623] 1.6.0rc1 --- CHANGES.md | 74 ++++++++++++++++++++++++++++++++++++++++ changelog.d/5727.feature | 1 - changelog.d/6140.misc | 1 - changelog.d/6164.doc | 1 - changelog.d/6213.bugfix | 1 - changelog.d/6218.misc | 1 - changelog.d/6220.feature | 1 - changelog.d/6232.bugfix | 1 - changelog.d/6235.bugfix | 1 - changelog.d/6238.feature | 1 - changelog.d/6240.misc | 1 - changelog.d/6250.misc | 1 - changelog.d/6251.misc | 1 - changelog.d/6253.bugfix | 1 - changelog.d/6254.bugfix | 1 - changelog.d/6257.doc | 1 - changelog.d/6259.misc | 1 - changelog.d/6263.misc | 1 - changelog.d/6269.misc | 1 - changelog.d/6270.misc | 1 - changelog.d/6271.misc | 1 - changelog.d/6272.doc | 1 - changelog.d/6273.doc | 1 - changelog.d/6274.misc | 1 - changelog.d/6275.misc | 1 - changelog.d/6276.misc | 1 - changelog.d/6277.misc | 1 - changelog.d/6278.bugfix | 1 - changelog.d/6279.misc | 1 - changelog.d/6280.misc | 1 - changelog.d/6284.bugfix | 1 - changelog.d/6291.misc | 1 - changelog.d/6294.misc | 1 - changelog.d/6295.misc | 1 - changelog.d/6298.misc | 1 - changelog.d/6300.misc | 1 - changelog.d/6301.feature | 1 - changelog.d/6304.misc | 1 - changelog.d/6305.misc | 1 - changelog.d/6306.bugfix | 1 - changelog.d/6307.bugfix | 1 - changelog.d/6308.misc | 1 - changelog.d/6310.feature | 1 - changelog.d/6312.misc | 1 - changelog.d/6313.bugfix | 1 - changelog.d/6314.misc | 1 - changelog.d/6317.misc | 1 - changelog.d/6318.misc | 1 - changelog.d/6319.misc | 1 - changelog.d/6320.bugfix | 1 - changelog.d/6330.misc | 1 - changelog.d/6335.bugfix | 1 - changelog.d/6336.misc | 1 - changelog.d/6338.bugfix | 1 - changelog.d/6340.feature | 1 - changelog.d/6341.misc | 1 - changelog.d/6357.misc | 1 - changelog.d/6359.bugfix | 1 - changelog.d/6361.misc | 1 - changelog.d/6363.bugfix | 1 - changelog.d/6389.bugfix | 1 - synapse/__init__.py | 2 +- 62 files changed, 75 insertions(+), 61 deletions(-) delete mode 100644 changelog.d/5727.feature delete mode 100644 changelog.d/6140.misc delete mode 100644 changelog.d/6164.doc delete mode 100644 changelog.d/6213.bugfix delete mode 100644 changelog.d/6218.misc delete mode 100644 changelog.d/6220.feature delete mode 100644 changelog.d/6232.bugfix delete mode 100644 changelog.d/6235.bugfix delete mode 100644 changelog.d/6238.feature delete mode 100644 changelog.d/6240.misc delete mode 100644 changelog.d/6250.misc delete mode 100644 changelog.d/6251.misc delete mode 100644 changelog.d/6253.bugfix delete mode 100644 changelog.d/6254.bugfix delete mode 100644 changelog.d/6257.doc delete mode 100644 changelog.d/6259.misc delete mode 100644 changelog.d/6263.misc delete mode 100644 changelog.d/6269.misc delete mode 100644 changelog.d/6270.misc delete mode 100644 changelog.d/6271.misc delete mode 100644 changelog.d/6272.doc delete mode 100644 changelog.d/6273.doc delete mode 100644 changelog.d/6274.misc delete mode 100644 changelog.d/6275.misc delete mode 100644 changelog.d/6276.misc delete mode 100644 changelog.d/6277.misc delete mode 100644 changelog.d/6278.bugfix delete mode 100644 changelog.d/6279.misc delete mode 100644 changelog.d/6280.misc delete mode 100644 changelog.d/6284.bugfix delete mode 100644 changelog.d/6291.misc delete mode 100644 changelog.d/6294.misc delete mode 100644 changelog.d/6295.misc delete mode 100644 changelog.d/6298.misc delete mode 100644 changelog.d/6300.misc delete mode 100644 changelog.d/6301.feature delete mode 100644 changelog.d/6304.misc delete mode 100644 changelog.d/6305.misc delete mode 100644 changelog.d/6306.bugfix delete mode 100644 changelog.d/6307.bugfix delete mode 100644 changelog.d/6308.misc delete mode 100644 changelog.d/6310.feature delete mode 100644 changelog.d/6312.misc delete mode 100644 changelog.d/6313.bugfix delete mode 100644 changelog.d/6314.misc delete mode 100644 changelog.d/6317.misc delete mode 100644 changelog.d/6318.misc delete mode 100644 changelog.d/6319.misc delete mode 100644 changelog.d/6320.bugfix delete mode 100644 changelog.d/6330.misc delete mode 100644 changelog.d/6335.bugfix delete mode 100644 changelog.d/6336.misc delete mode 100644 changelog.d/6338.bugfix delete mode 100644 changelog.d/6340.feature delete mode 100644 changelog.d/6341.misc delete mode 100644 changelog.d/6357.misc delete mode 100644 changelog.d/6359.bugfix delete mode 100644 changelog.d/6361.misc delete mode 100644 changelog.d/6363.bugfix delete mode 100644 changelog.d/6389.bugfix diff --git a/CHANGES.md b/CHANGES.md index 9312dc2941..f4f61db5d4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,77 @@ +Synapse 1.6.0rc1 (2019-11-20) +============================= + +Features +-------- + +- Add federation support for cross-signing. ([\#5727](https://github.com/matrix-org/synapse/issues/5727)) +- Increase default room version from 4 to 5, thereby enforcing server key validity period checks. ([\#6220](https://github.com/matrix-org/synapse/issues/6220)) +- Add support for outbound http proxying via http_proxy/HTTPS_PROXY env vars. ([\#6238](https://github.com/matrix-org/synapse/issues/6238)) +- Implement label-based filtering on `/sync` and `/messages` ([MSC2326](https://github.com/matrix-org/matrix-doc/pull/2326)). ([\#6301](https://github.com/matrix-org/synapse/issues/6301), [\#6310](https://github.com/matrix-org/synapse/issues/6310), [\#6340](https://github.com/matrix-org/synapse/issues/6340)) + + +Bugfixes +-------- + +- Fix LruCache callback deduplication for Python 3.8. Contributed by @V02460. ([\#6213](https://github.com/matrix-org/synapse/issues/6213)) +- Remove a room from a server's public rooms list on room upgrade. ([\#6232](https://github.com/matrix-org/synapse/issues/6232), [\#6235](https://github.com/matrix-org/synapse/issues/6235)) +- Delete keys from key backup when deleting backup versions. ([\#6253](https://github.com/matrix-org/synapse/issues/6253)) +- Make notification of cross-signing signatures work with workers. ([\#6254](https://github.com/matrix-org/synapse/issues/6254)) +- Fix exception when remote servers attempt to join a room that they're not allowed to join. ([\#6278](https://github.com/matrix-org/synapse/issues/6278)) +- Prevent errors from appearing on Synapse startup if `git` is not installed. ([\#6284](https://github.com/matrix-org/synapse/issues/6284)) +- Appservice requests will no longer contain a double slash prefix when the appservice url provided ends in a slash. ([\#6306](https://github.com/matrix-org/synapse/issues/6306)) +- Fix `/purge_room` admin API. ([\#6307](https://github.com/matrix-org/synapse/issues/6307)) +- Fix the `hidden` field in the `devices` table for SQLite versions prior to 3.23.0. ([\#6313](https://github.com/matrix-org/synapse/issues/6313)) +- Fix bug which casued rejected events to be persisted with the wrong room state. ([\#6320](https://github.com/matrix-org/synapse/issues/6320)) +- Fix bug where `rc_login` ratelimiting would prematurely kick in. ([\#6335](https://github.com/matrix-org/synapse/issues/6335)) +- Prevent the server taking a long time to start up when guest registration is enabled. ([\#6338](https://github.com/matrix-org/synapse/issues/6338)) +- Fix bug where upgrading a guest account to a full user would fail when account validity is enabled. ([\#6359](https://github.com/matrix-org/synapse/issues/6359)) +- Fix `to_device` stream ID getting reset every time Synapse restarts, which had the potential to cause unable to decrypt errors. ([\#6363](https://github.com/matrix-org/synapse/issues/6363)) +- Fix permission denied error when trying to generate a config file with the docker image. ([\#6389](https://github.com/matrix-org/synapse/issues/6389)) + + +Improved Documentation +---------------------- + +- Contributor documentation now mentions script to run linters. ([\#6164](https://github.com/matrix-org/synapse/issues/6164)) +- Modify CAPTCHA_SETUP.md to update the terms `private key` and `public key` to `secret key` and `site key` respectively. Contributed by Yash Jipkate. ([\#6257](https://github.com/matrix-org/synapse/issues/6257)) +- Update `INSTALL.md` Email section to talk about `account_threepid_delegates`. ([\#6272](https://github.com/matrix-org/synapse/issues/6272)) +- Fix a small typo in `account_threepid_delegates` configuration option. ([\#6273](https://github.com/matrix-org/synapse/issues/6273)) + + +Internal Changes +---------------- + +- Add a CI job to test the `synapse_port_db` script. ([\#6140](https://github.com/matrix-org/synapse/issues/6140), [\#6276](https://github.com/matrix-org/synapse/issues/6276)) +- Convert EventContext to an attrs. ([\#6218](https://github.com/matrix-org/synapse/issues/6218)) +- Move `persist_events` out from main data store. ([\#6240](https://github.com/matrix-org/synapse/issues/6240), [\#6300](https://github.com/matrix-org/synapse/issues/6300)) +- Reduce verbosity of user/room stats. ([\#6250](https://github.com/matrix-org/synapse/issues/6250)) +- Reduce impact of debug logging. ([\#6251](https://github.com/matrix-org/synapse/issues/6251)) +- Expose some homeserver functionality to spam checkers. ([\#6259](https://github.com/matrix-org/synapse/issues/6259)) +- Change cache descriptors to always return deferreds. ([\#6263](https://github.com/matrix-org/synapse/issues/6263), [\#6291](https://github.com/matrix-org/synapse/issues/6291)) +- Fix incorrect comment regarding the functionality of an `if` statement. ([\#6269](https://github.com/matrix-org/synapse/issues/6269)) +- Update CI to run `isort` over the `scripts` and `scripts-dev` directories. ([\#6270](https://github.com/matrix-org/synapse/issues/6270)) +- Replace every instance of `logger.warn` method with `logger.warning` as the former is deprecated. ([\#6271](https://github.com/matrix-org/synapse/issues/6271), [\#6314](https://github.com/matrix-org/synapse/issues/6314)) +- Port replication http server endpoints to async/await. ([\#6274](https://github.com/matrix-org/synapse/issues/6274)) +- Port room rest handlers to async/await. ([\#6275](https://github.com/matrix-org/synapse/issues/6275)) +- Remove redundant CLI parameters on CI's `flake8` step. ([\#6277](https://github.com/matrix-org/synapse/issues/6277)) +- Port `federation_server.py` to async/await. ([\#6279](https://github.com/matrix-org/synapse/issues/6279)) +- Port receipt and read markers to async/wait. ([\#6280](https://github.com/matrix-org/synapse/issues/6280)) +- Split out state storage into separate data store. ([\#6294](https://github.com/matrix-org/synapse/issues/6294), [\#6295](https://github.com/matrix-org/synapse/issues/6295)) +- Refactor EventContext for clarity. ([\#6298](https://github.com/matrix-org/synapse/issues/6298)) +- Update the version of black used to 19.10b0. ([\#6304](https://github.com/matrix-org/synapse/issues/6304)) +- Add some documentation about worker replication. ([\#6305](https://github.com/matrix-org/synapse/issues/6305)) +- Move admin endpoints into separate files. Contributed by Awesome Technologies Innovationslabor GmbH. ([\#6308](https://github.com/matrix-org/synapse/issues/6308)) +- Document the use of `lint.sh` for code style enforcement & extend it to run on specified paths only. ([\#6312](https://github.com/matrix-org/synapse/issues/6312)) +- Add optional python dependencies and dependant binary libraries to snapcraft packaging. ([\#6317](https://github.com/matrix-org/synapse/issues/6317)) +- Remove the dependency on psutil and replace functionality with the stdlib `resource` module. ([\#6318](https://github.com/matrix-org/synapse/issues/6318), [\#6336](https://github.com/matrix-org/synapse/issues/6336)) +- Improve documentation for EventContext fields. ([\#6319](https://github.com/matrix-org/synapse/issues/6319)) +- Add some checks that we aren't using state from rejected events. ([\#6330](https://github.com/matrix-org/synapse/issues/6330)) +- Add continuous integration for python 3.8. ([\#6341](https://github.com/matrix-org/synapse/issues/6341)) +- Correct spacing/case of various instances of the word "homeserver". ([\#6357](https://github.com/matrix-org/synapse/issues/6357)) +- Temporarily blacklist the failing unit test PurgeRoomTestCase.test_purge_room. ([\#6361](https://github.com/matrix-org/synapse/issues/6361)) + + Synapse 1.5.1 (2019-11-06) ========================== diff --git a/changelog.d/5727.feature b/changelog.d/5727.feature deleted file mode 100644 index 819bebf2d7..0000000000 --- a/changelog.d/5727.feature +++ /dev/null @@ -1 +0,0 @@ -Add federation support for cross-signing. diff --git a/changelog.d/6140.misc b/changelog.d/6140.misc deleted file mode 100644 index 0feb97ec61..0000000000 --- a/changelog.d/6140.misc +++ /dev/null @@ -1 +0,0 @@ -Add a CI job to test the `synapse_port_db` script. \ No newline at end of file diff --git a/changelog.d/6164.doc b/changelog.d/6164.doc deleted file mode 100644 index f9395b02b3..0000000000 --- a/changelog.d/6164.doc +++ /dev/null @@ -1 +0,0 @@ -Contributor documentation now mentions script to run linters. diff --git a/changelog.d/6213.bugfix b/changelog.d/6213.bugfix deleted file mode 100644 index 2bb2d08851..0000000000 --- a/changelog.d/6213.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix LruCache callback deduplication for Python 3.8. Contributed by @V02460. diff --git a/changelog.d/6218.misc b/changelog.d/6218.misc deleted file mode 100644 index 49d10c36cf..0000000000 --- a/changelog.d/6218.misc +++ /dev/null @@ -1 +0,0 @@ -Convert EventContext to an attrs. diff --git a/changelog.d/6220.feature b/changelog.d/6220.feature deleted file mode 100644 index 8343e9912b..0000000000 --- a/changelog.d/6220.feature +++ /dev/null @@ -1 +0,0 @@ -Increase default room version from 4 to 5, thereby enforcing server key validity period checks. diff --git a/changelog.d/6232.bugfix b/changelog.d/6232.bugfix deleted file mode 100644 index 12718ba934..0000000000 --- a/changelog.d/6232.bugfix +++ /dev/null @@ -1 +0,0 @@ -Remove a room from a server's public rooms list on room upgrade. \ No newline at end of file diff --git a/changelog.d/6235.bugfix b/changelog.d/6235.bugfix deleted file mode 100644 index 12718ba934..0000000000 --- a/changelog.d/6235.bugfix +++ /dev/null @@ -1 +0,0 @@ -Remove a room from a server's public rooms list on room upgrade. \ No newline at end of file diff --git a/changelog.d/6238.feature b/changelog.d/6238.feature deleted file mode 100644 index d225ac33b6..0000000000 --- a/changelog.d/6238.feature +++ /dev/null @@ -1 +0,0 @@ -Add support for outbound http proxying via http_proxy/HTTPS_PROXY env vars. diff --git a/changelog.d/6240.misc b/changelog.d/6240.misc deleted file mode 100644 index 0b3d7a14a1..0000000000 --- a/changelog.d/6240.misc +++ /dev/null @@ -1 +0,0 @@ -Move `persist_events` out from main data store. diff --git a/changelog.d/6250.misc b/changelog.d/6250.misc deleted file mode 100644 index 12e3fe66b0..0000000000 --- a/changelog.d/6250.misc +++ /dev/null @@ -1 +0,0 @@ -Reduce verbosity of user/room stats. diff --git a/changelog.d/6251.misc b/changelog.d/6251.misc deleted file mode 100644 index 371c6983be..0000000000 --- a/changelog.d/6251.misc +++ /dev/null @@ -1 +0,0 @@ -Reduce impact of debug logging. diff --git a/changelog.d/6253.bugfix b/changelog.d/6253.bugfix deleted file mode 100644 index 266fae381c..0000000000 --- a/changelog.d/6253.bugfix +++ /dev/null @@ -1 +0,0 @@ -Delete keys from key backup when deleting backup versions. diff --git a/changelog.d/6254.bugfix b/changelog.d/6254.bugfix deleted file mode 100644 index 3181484b88..0000000000 --- a/changelog.d/6254.bugfix +++ /dev/null @@ -1 +0,0 @@ -Make notification of cross-signing signatures work with workers. diff --git a/changelog.d/6257.doc b/changelog.d/6257.doc deleted file mode 100644 index e985afde0e..0000000000 --- a/changelog.d/6257.doc +++ /dev/null @@ -1 +0,0 @@ -Modify CAPTCHA_SETUP.md to update the terms `private key` and `public key` to `secret key` and `site key` respectively. Contributed by Yash Jipkate. diff --git a/changelog.d/6259.misc b/changelog.d/6259.misc deleted file mode 100644 index 3ff81b1ac7..0000000000 --- a/changelog.d/6259.misc +++ /dev/null @@ -1 +0,0 @@ -Expose some homeserver functionality to spam checkers. diff --git a/changelog.d/6263.misc b/changelog.d/6263.misc deleted file mode 100644 index 7b1bb4b679..0000000000 --- a/changelog.d/6263.misc +++ /dev/null @@ -1 +0,0 @@ -Change cache descriptors to always return deferreds. diff --git a/changelog.d/6269.misc b/changelog.d/6269.misc deleted file mode 100644 index 9fd333cc89..0000000000 --- a/changelog.d/6269.misc +++ /dev/null @@ -1 +0,0 @@ -Fix incorrect comment regarding the functionality of an `if` statement. \ No newline at end of file diff --git a/changelog.d/6270.misc b/changelog.d/6270.misc deleted file mode 100644 index d1c5811323..0000000000 --- a/changelog.d/6270.misc +++ /dev/null @@ -1 +0,0 @@ -Update CI to run `isort` over the `scripts` and `scripts-dev` directories. \ No newline at end of file diff --git a/changelog.d/6271.misc b/changelog.d/6271.misc deleted file mode 100644 index 2369760272..0000000000 --- a/changelog.d/6271.misc +++ /dev/null @@ -1 +0,0 @@ -Replace every instance of `logger.warn` method with `logger.warning` as the former is deprecated. \ No newline at end of file diff --git a/changelog.d/6272.doc b/changelog.d/6272.doc deleted file mode 100644 index 232180bcdc..0000000000 --- a/changelog.d/6272.doc +++ /dev/null @@ -1 +0,0 @@ -Update `INSTALL.md` Email section to talk about `account_threepid_delegates`. \ No newline at end of file diff --git a/changelog.d/6273.doc b/changelog.d/6273.doc deleted file mode 100644 index 21a41d987d..0000000000 --- a/changelog.d/6273.doc +++ /dev/null @@ -1 +0,0 @@ -Fix a small typo in `account_threepid_delegates` configuration option. \ No newline at end of file diff --git a/changelog.d/6274.misc b/changelog.d/6274.misc deleted file mode 100644 index eb4966124f..0000000000 --- a/changelog.d/6274.misc +++ /dev/null @@ -1 +0,0 @@ -Port replication http server endpoints to async/await. diff --git a/changelog.d/6275.misc b/changelog.d/6275.misc deleted file mode 100644 index f57e2c4adb..0000000000 --- a/changelog.d/6275.misc +++ /dev/null @@ -1 +0,0 @@ -Port room rest handlers to async/await. diff --git a/changelog.d/6276.misc b/changelog.d/6276.misc deleted file mode 100644 index 4a4428251e..0000000000 --- a/changelog.d/6276.misc +++ /dev/null @@ -1 +0,0 @@ -Add a CI job to test the `synapse_port_db` script. diff --git a/changelog.d/6277.misc b/changelog.d/6277.misc deleted file mode 100644 index 490713577f..0000000000 --- a/changelog.d/6277.misc +++ /dev/null @@ -1 +0,0 @@ -Remove redundant CLI parameters on CI's `flake8` step. \ No newline at end of file diff --git a/changelog.d/6278.bugfix b/changelog.d/6278.bugfix deleted file mode 100644 index c107270461..0000000000 --- a/changelog.d/6278.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix exception when remote servers attempt to join a room that they're not allowed to join. diff --git a/changelog.d/6279.misc b/changelog.d/6279.misc deleted file mode 100644 index 5f5144a9ee..0000000000 --- a/changelog.d/6279.misc +++ /dev/null @@ -1 +0,0 @@ -Port `federation_server.py` to async/await. diff --git a/changelog.d/6280.misc b/changelog.d/6280.misc deleted file mode 100644 index 96a0eb21b2..0000000000 --- a/changelog.d/6280.misc +++ /dev/null @@ -1 +0,0 @@ -Port receipt and read markers to async/wait. diff --git a/changelog.d/6284.bugfix b/changelog.d/6284.bugfix deleted file mode 100644 index cf15053d2d..0000000000 --- a/changelog.d/6284.bugfix +++ /dev/null @@ -1 +0,0 @@ -Prevent errors from appearing on Synapse startup if `git` is not installed. \ No newline at end of file diff --git a/changelog.d/6291.misc b/changelog.d/6291.misc deleted file mode 100644 index 7b1bb4b679..0000000000 --- a/changelog.d/6291.misc +++ /dev/null @@ -1 +0,0 @@ -Change cache descriptors to always return deferreds. diff --git a/changelog.d/6294.misc b/changelog.d/6294.misc deleted file mode 100644 index a3e6b8296e..0000000000 --- a/changelog.d/6294.misc +++ /dev/null @@ -1 +0,0 @@ -Split out state storage into separate data store. diff --git a/changelog.d/6295.misc b/changelog.d/6295.misc deleted file mode 100644 index a3e6b8296e..0000000000 --- a/changelog.d/6295.misc +++ /dev/null @@ -1 +0,0 @@ -Split out state storage into separate data store. diff --git a/changelog.d/6298.misc b/changelog.d/6298.misc deleted file mode 100644 index d4190730b2..0000000000 --- a/changelog.d/6298.misc +++ /dev/null @@ -1 +0,0 @@ -Refactor EventContext for clarity. \ No newline at end of file diff --git a/changelog.d/6300.misc b/changelog.d/6300.misc deleted file mode 100644 index 0b3d7a14a1..0000000000 --- a/changelog.d/6300.misc +++ /dev/null @@ -1 +0,0 @@ -Move `persist_events` out from main data store. diff --git a/changelog.d/6301.feature b/changelog.d/6301.feature deleted file mode 100644 index 78a187a1dc..0000000000 --- a/changelog.d/6301.feature +++ /dev/null @@ -1 +0,0 @@ -Implement label-based filtering on `/sync` and `/messages` ([MSC2326](https://github.com/matrix-org/matrix-doc/pull/2326)). diff --git a/changelog.d/6304.misc b/changelog.d/6304.misc deleted file mode 100644 index 20372b4f7c..0000000000 --- a/changelog.d/6304.misc +++ /dev/null @@ -1 +0,0 @@ -Update the version of black used to 19.10b0. diff --git a/changelog.d/6305.misc b/changelog.d/6305.misc deleted file mode 100644 index f047fc3062..0000000000 --- a/changelog.d/6305.misc +++ /dev/null @@ -1 +0,0 @@ -Add some documentation about worker replication. diff --git a/changelog.d/6306.bugfix b/changelog.d/6306.bugfix deleted file mode 100644 index c7dcbcdce8..0000000000 --- a/changelog.d/6306.bugfix +++ /dev/null @@ -1 +0,0 @@ -Appservice requests will no longer contain a double slash prefix when the appservice url provided ends in a slash. diff --git a/changelog.d/6307.bugfix b/changelog.d/6307.bugfix deleted file mode 100644 index f2917c5053..0000000000 --- a/changelog.d/6307.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix `/purge_room` admin API. diff --git a/changelog.d/6308.misc b/changelog.d/6308.misc deleted file mode 100644 index 72be63ba4b..0000000000 --- a/changelog.d/6308.misc +++ /dev/null @@ -1 +0,0 @@ -Move admin endpoints into separate files. Contributed by Awesome Technologies Innovationslabor GmbH. \ No newline at end of file diff --git a/changelog.d/6310.feature b/changelog.d/6310.feature deleted file mode 100644 index 78a187a1dc..0000000000 --- a/changelog.d/6310.feature +++ /dev/null @@ -1 +0,0 @@ -Implement label-based filtering on `/sync` and `/messages` ([MSC2326](https://github.com/matrix-org/matrix-doc/pull/2326)). diff --git a/changelog.d/6312.misc b/changelog.d/6312.misc deleted file mode 100644 index 55e3e1654d..0000000000 --- a/changelog.d/6312.misc +++ /dev/null @@ -1 +0,0 @@ -Document the use of `lint.sh` for code style enforcement & extend it to run on specified paths only. diff --git a/changelog.d/6313.bugfix b/changelog.d/6313.bugfix deleted file mode 100644 index f4d4a97f00..0000000000 --- a/changelog.d/6313.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix the `hidden` field in the `devices` table for SQLite versions prior to 3.23.0. diff --git a/changelog.d/6314.misc b/changelog.d/6314.misc deleted file mode 100644 index 2369760272..0000000000 --- a/changelog.d/6314.misc +++ /dev/null @@ -1 +0,0 @@ -Replace every instance of `logger.warn` method with `logger.warning` as the former is deprecated. \ No newline at end of file diff --git a/changelog.d/6317.misc b/changelog.d/6317.misc deleted file mode 100644 index a67d13fa72..0000000000 --- a/changelog.d/6317.misc +++ /dev/null @@ -1 +0,0 @@ -Add optional python dependencies and dependant binary libraries to snapcraft packaging. diff --git a/changelog.d/6318.misc b/changelog.d/6318.misc deleted file mode 100644 index 63527ccef4..0000000000 --- a/changelog.d/6318.misc +++ /dev/null @@ -1 +0,0 @@ -Remove the dependency on psutil and replace functionality with the stdlib `resource` module. diff --git a/changelog.d/6319.misc b/changelog.d/6319.misc deleted file mode 100644 index 9711ef21ed..0000000000 --- a/changelog.d/6319.misc +++ /dev/null @@ -1 +0,0 @@ -Improve documentation for EventContext fields. diff --git a/changelog.d/6320.bugfix b/changelog.d/6320.bugfix deleted file mode 100644 index 2c3fad5655..0000000000 --- a/changelog.d/6320.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix bug which casued rejected events to be persisted with the wrong room state. diff --git a/changelog.d/6330.misc b/changelog.d/6330.misc deleted file mode 100644 index 6239cba263..0000000000 --- a/changelog.d/6330.misc +++ /dev/null @@ -1 +0,0 @@ -Add some checks that we aren't using state from rejected events. diff --git a/changelog.d/6335.bugfix b/changelog.d/6335.bugfix deleted file mode 100644 index a95f6b9eec..0000000000 --- a/changelog.d/6335.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix bug where `rc_login` ratelimiting would prematurely kick in. diff --git a/changelog.d/6336.misc b/changelog.d/6336.misc deleted file mode 100644 index 63527ccef4..0000000000 --- a/changelog.d/6336.misc +++ /dev/null @@ -1 +0,0 @@ -Remove the dependency on psutil and replace functionality with the stdlib `resource` module. diff --git a/changelog.d/6338.bugfix b/changelog.d/6338.bugfix deleted file mode 100644 index 8e469f0fb6..0000000000 --- a/changelog.d/6338.bugfix +++ /dev/null @@ -1 +0,0 @@ -Prevent the server taking a long time to start up when guest registration is enabled. \ No newline at end of file diff --git a/changelog.d/6340.feature b/changelog.d/6340.feature deleted file mode 100644 index 78a187a1dc..0000000000 --- a/changelog.d/6340.feature +++ /dev/null @@ -1 +0,0 @@ -Implement label-based filtering on `/sync` and `/messages` ([MSC2326](https://github.com/matrix-org/matrix-doc/pull/2326)). diff --git a/changelog.d/6341.misc b/changelog.d/6341.misc deleted file mode 100644 index 359b9bf1d7..0000000000 --- a/changelog.d/6341.misc +++ /dev/null @@ -1 +0,0 @@ -Add continuous integration for python 3.8. \ No newline at end of file diff --git a/changelog.d/6357.misc b/changelog.d/6357.misc deleted file mode 100644 index a68df0f384..0000000000 --- a/changelog.d/6357.misc +++ /dev/null @@ -1 +0,0 @@ -Correct spacing/case of various instances of the word "homeserver". \ No newline at end of file diff --git a/changelog.d/6359.bugfix b/changelog.d/6359.bugfix deleted file mode 100644 index 22bf5f642a..0000000000 --- a/changelog.d/6359.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix bug where upgrading a guest account to a full user would fail when account validity is enabled. \ No newline at end of file diff --git a/changelog.d/6361.misc b/changelog.d/6361.misc deleted file mode 100644 index 324d74ebf9..0000000000 --- a/changelog.d/6361.misc +++ /dev/null @@ -1 +0,0 @@ -Temporarily blacklist the failing unit test PurgeRoomTestCase.test_purge_room. diff --git a/changelog.d/6363.bugfix b/changelog.d/6363.bugfix deleted file mode 100644 index d023b49181..0000000000 --- a/changelog.d/6363.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix `to_device` stream ID getting reset every time Synapse restarts, which had the potential to cause unable to decrypt errors. \ No newline at end of file diff --git a/changelog.d/6389.bugfix b/changelog.d/6389.bugfix deleted file mode 100644 index c553622b02..0000000000 --- a/changelog.d/6389.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix permission denied error when trying to generate a config file with the docker image. \ No newline at end of file diff --git a/synapse/__init__.py b/synapse/__init__.py index 1c27d68009..1d962f5dc8 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -36,7 +36,7 @@ try: except ImportError: pass -__version__ = "1.5.1" +__version__ = "1.6.0rc1" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 486be06f4877842dfb109caac42ab052e09fd5b0 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 20 Nov 2019 15:08:03 +0000 Subject: [PATCH 0522/1623] Changelog --- changelog.d/6392.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6392.misc diff --git a/changelog.d/6392.misc b/changelog.d/6392.misc new file mode 100644 index 0000000000..a00257944f --- /dev/null +++ b/changelog.d/6392.misc @@ -0,0 +1 @@ +Add a test scenario to make sure room history purges don't break `/messages` in the future. From e2a20326e8141fdf9304434901da38c64b917a78 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 20 Nov 2019 15:08:47 +0000 Subject: [PATCH 0523/1623] Lint --- tests/rest/client/v1/test_rooms.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py index ebaa67e899..e84e578f99 100644 --- a/tests/rest/client/v1/test_rooms.py +++ b/tests/rest/client/v1/test_rooms.py @@ -948,12 +948,14 @@ class RoomMessageListTestCase(RoomBase): # Purge every event before the second event. purge_id = random_string(16) pagination_handler._purges_by_id[purge_id] = PurgeStatus() - self.get_success(pagination_handler._purge_history( - purge_id=purge_id, - room_id=self.room_id, - token=second_token, - delete_local_events=True, - )) + self.get_success( + pagination_handler._purge_history( + purge_id=purge_id, + room_id=self.room_id, + token=second_token, + delete_local_events=True, + ) + ) # Check that we only get the second message through /message now that the first # has been purged. From 49243c55a4c0a9dd82d3ba95f111bc2df430b587 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 20 Nov 2019 16:09:11 +0000 Subject: [PATCH 0524/1623] Update changelog since this isn't going to be featured in 1.6.0 --- changelog.d/6329.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/6329.feature b/changelog.d/6329.feature index 78a187a1dc..48263cdd86 100644 --- a/changelog.d/6329.feature +++ b/changelog.d/6329.feature @@ -1 +1 @@ -Implement label-based filtering on `/sync` and `/messages` ([MSC2326](https://github.com/matrix-org/matrix-doc/pull/2326)). +Filter state, events_before and events_after in /context requests. From b2f8c21a9b9389251c9343166c63b003fad278a2 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 20 Nov 2019 16:10:27 +0000 Subject: [PATCH 0525/1623] Format changelog --- changelog.d/6329.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/6329.feature b/changelog.d/6329.feature index 48263cdd86..c27dbb06a4 100644 --- a/changelog.d/6329.feature +++ b/changelog.d/6329.feature @@ -1 +1 @@ -Filter state, events_before and events_after in /context requests. +Filter `state`, `events_before` and `events_after` in `/context` requests. From 9cc168e42e14481387b4e6de73bfe1f8f9a2a508 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 20 Nov 2019 18:44:45 +0000 Subject: [PATCH 0526/1623] update macOS installation instructions --- INSTALL.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index 29e0abafd3..9b7360f0ef 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -133,9 +133,9 @@ sudo yum install libtiff-devel libjpeg-devel libzip-devel freetype-devel \ sudo yum groupinstall "Development Tools" ``` -#### Mac OS X +#### macOS -Installing prerequisites on Mac OS X: +Installing prerequisites on macOS: ``` xcode-select --install @@ -144,6 +144,14 @@ sudo pip install virtualenv brew install pkg-config libffi ``` +On macOS Catalina (10.15) you may need to explicitly install OpenSSL +via brew and inform `pip` about it so that `psycopg2` builds: + +``` +brew install openssl@1.1 +export LDFLAGS=-L/usr/local/Cellar/openssl\@1.1/1.1.1d/lib/ +``` + #### OpenSUSE Installing prerequisites on openSUSE: From 3916e1b97a1ffc481dfdf66f7da58201a52140a9 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Thu, 21 Nov 2019 12:00:14 +0000 Subject: [PATCH 0527/1623] Clean up newline quote marks around the codebase (#6362) --- changelog.d/6362.misc | 1 + synapse/app/federation_sender.py | 2 +- synapse/appservice/api.py | 2 +- synapse/config/appservice.py | 2 +- synapse/config/room_directory.py | 2 +- synapse/config/server.py | 6 +++--- synapse/federation/persistence.py | 4 ++-- synapse/federation/sender/__init__.py | 2 +- synapse/federation/sender/transaction_manager.py | 4 ++-- synapse/handlers/directory.py | 2 +- synapse/http/servlet.py | 2 +- synapse/push/httppusher.py | 5 ++--- synapse/push/mailer.py | 4 ++-- synapse/rest/media/v1/preview_url_resource.py | 2 +- synapse/server_notices/consent_server_notices.py | 2 +- synapse/storage/_base.py | 2 +- synapse/storage/data_stores/main/deviceinbox.py | 2 +- synapse/storage/data_stores/main/end_to_end_keys.py | 6 +++--- synapse/storage/data_stores/main/events.py | 8 +++----- synapse/storage/data_stores/main/filtering.py | 2 +- synapse/storage/data_stores/main/media_repository.py | 6 +++--- synapse/storage/data_stores/main/registration.py | 4 +--- synapse/storage/data_stores/main/stream.py | 2 +- synapse/storage/data_stores/main/tags.py | 4 +--- synapse/storage/prepare_database.py | 2 +- synapse/streams/config.py | 9 ++++++--- 26 files changed, 43 insertions(+), 46 deletions(-) create mode 100644 changelog.d/6362.misc diff --git a/changelog.d/6362.misc b/changelog.d/6362.misc new file mode 100644 index 0000000000..b79a5bea99 --- /dev/null +++ b/changelog.d/6362.misc @@ -0,0 +1 @@ +Clean up some unnecessary quotation marks around the codebase. \ No newline at end of file diff --git a/synapse/app/federation_sender.py b/synapse/app/federation_sender.py index 139221ad34..448e45e00f 100644 --- a/synapse/app/federation_sender.py +++ b/synapse/app/federation_sender.py @@ -69,7 +69,7 @@ class FederationSenderSlaveStore( self.federation_out_pos_startup = self._get_federation_out_pos(db_conn) def _get_federation_out_pos(self, db_conn): - sql = "SELECT stream_id FROM federation_stream_position" " WHERE type = ?" + sql = "SELECT stream_id FROM federation_stream_position WHERE type = ?" sql = self.database_engine.convert_param_style(sql) txn = db_conn.cursor() diff --git a/synapse/appservice/api.py b/synapse/appservice/api.py index 3e25bf5747..57174da021 100644 --- a/synapse/appservice/api.py +++ b/synapse/appservice/api.py @@ -185,7 +185,7 @@ class ApplicationServiceApi(SimpleHttpClient): if not _is_valid_3pe_metadata(info): logger.warning( - "query_3pe_protocol to %s did not return a" " valid result", uri + "query_3pe_protocol to %s did not return a valid result", uri ) return None diff --git a/synapse/config/appservice.py b/synapse/config/appservice.py index e77d3387ff..ca43e96bd1 100644 --- a/synapse/config/appservice.py +++ b/synapse/config/appservice.py @@ -134,7 +134,7 @@ def _load_appservice(hostname, as_info, config_filename): for regex_obj in as_info["namespaces"][ns]: if not isinstance(regex_obj, dict): raise ValueError( - "Expected namespace entry in %s to be an object," " but got %s", + "Expected namespace entry in %s to be an object, but got %s", ns, regex_obj, ) diff --git a/synapse/config/room_directory.py b/synapse/config/room_directory.py index 7c9f05bde4..7ac7699676 100644 --- a/synapse/config/room_directory.py +++ b/synapse/config/room_directory.py @@ -170,7 +170,7 @@ class _RoomDirectoryRule(object): self.action = action else: raise ConfigError( - "%s rules can only have action of 'allow'" " or 'deny'" % (option_name,) + "%s rules can only have action of 'allow' or 'deny'" % (option_name,) ) self._alias_matches_all = alias == "*" diff --git a/synapse/config/server.py b/synapse/config/server.py index 00d01c43af..11336d7549 100644 --- a/synapse/config/server.py +++ b/synapse/config/server.py @@ -223,7 +223,7 @@ class ServerConfig(Config): self.federation_ip_range_blacklist.update(["0.0.0.0", "::"]) except Exception as e: raise ConfigError( - "Invalid range(s) provided in " "federation_ip_range_blacklist: %s" % e + "Invalid range(s) provided in federation_ip_range_blacklist: %s" % e ) if self.public_baseurl is not None: @@ -787,14 +787,14 @@ class ServerConfig(Config): "--print-pidfile", action="store_true", default=None, - help="Print the path to the pidfile just" " before daemonizing", + help="Print the path to the pidfile just before daemonizing", ) server_group.add_argument( "--manhole", metavar="PORT", dest="manhole", type=int, - help="Turn on the twisted telnet manhole" " service on the given port.", + help="Turn on the twisted telnet manhole service on the given port.", ) diff --git a/synapse/federation/persistence.py b/synapse/federation/persistence.py index 44edcabed4..d68b4bd670 100644 --- a/synapse/federation/persistence.py +++ b/synapse/federation/persistence.py @@ -44,7 +44,7 @@ class TransactionActions(object): response code and response body. """ if not transaction.transaction_id: - raise RuntimeError("Cannot persist a transaction with no " "transaction_id") + raise RuntimeError("Cannot persist a transaction with no transaction_id") return self.store.get_received_txn_response(transaction.transaction_id, origin) @@ -56,7 +56,7 @@ class TransactionActions(object): Deferred """ if not transaction.transaction_id: - raise RuntimeError("Cannot persist a transaction with no " "transaction_id") + raise RuntimeError("Cannot persist a transaction with no transaction_id") return self.store.set_received_txn_response( transaction.transaction_id, origin, code, response diff --git a/synapse/federation/sender/__init__.py b/synapse/federation/sender/__init__.py index 2b2ee8612a..4ebb0e8bc0 100644 --- a/synapse/federation/sender/__init__.py +++ b/synapse/federation/sender/__init__.py @@ -49,7 +49,7 @@ sent_pdus_destination_dist_count = Counter( sent_pdus_destination_dist_total = Counter( "synapse_federation_client_sent_pdu_destinations:total", - "" "Total number of PDUs queued for sending across all destinations", + "Total number of PDUs queued for sending across all destinations", ) diff --git a/synapse/federation/sender/transaction_manager.py b/synapse/federation/sender/transaction_manager.py index 67b3e1ab6e..5fed626d5b 100644 --- a/synapse/federation/sender/transaction_manager.py +++ b/synapse/federation/sender/transaction_manager.py @@ -84,7 +84,7 @@ class TransactionManager(object): txn_id = str(self._next_txn_id) logger.debug( - "TX [%s] {%s} Attempting new transaction" " (pdus: %d, edus: %d)", + "TX [%s] {%s} Attempting new transaction (pdus: %d, edus: %d)", destination, txn_id, len(pdus), @@ -103,7 +103,7 @@ class TransactionManager(object): self._next_txn_id += 1 logger.info( - "TX [%s] {%s} Sending transaction [%s]," " (PDUs: %d, EDUs: %d)", + "TX [%s] {%s} Sending transaction [%s], (PDUs: %d, EDUs: %d)", destination, txn_id, transaction.transaction_id, diff --git a/synapse/handlers/directory.py b/synapse/handlers/directory.py index 69051101a6..a07d2f1a17 100644 --- a/synapse/handlers/directory.py +++ b/synapse/handlers/directory.py @@ -119,7 +119,7 @@ class DirectoryHandler(BaseHandler): if not service.is_interested_in_alias(room_alias.to_string()): raise SynapseError( 400, - "This application service has not reserved" " this kind of alias.", + "This application service has not reserved this kind of alias.", errcode=Codes.EXCLUSIVE, ) else: diff --git a/synapse/http/servlet.py b/synapse/http/servlet.py index e9a5e46ced..13fcb408a6 100644 --- a/synapse/http/servlet.py +++ b/synapse/http/servlet.py @@ -96,7 +96,7 @@ def parse_boolean_from_args(args, name, default=None, required=False): return {b"true": True, b"false": False}[args[name][0]] except Exception: message = ( - "Boolean query parameter %r must be one of" " ['true', 'false']" + "Boolean query parameter %r must be one of ['true', 'false']" ) % (name,) raise SynapseError(400, message) else: diff --git a/synapse/push/httppusher.py b/synapse/push/httppusher.py index e994037be6..d0879b0490 100644 --- a/synapse/push/httppusher.py +++ b/synapse/push/httppusher.py @@ -246,7 +246,7 @@ class HttpPusher(object): # fixed, we don't suddenly deliver a load # of old notifications. logger.warning( - "Giving up on a notification to user %s, " "pushkey %s", + "Giving up on a notification to user %s, pushkey %s", self.user_id, self.pushkey, ) @@ -299,8 +299,7 @@ class HttpPusher(object): # for sanity, we only remove the pushkey if it # was the one we actually sent... logger.warning( - ("Ignoring rejected pushkey %s because we" " didn't send it"), - pk, + ("Ignoring rejected pushkey %s because we didn't send it"), pk, ) else: logger.info("Pushkey %s was rejected: removing", pk) diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 1d15a06a58..b13b646bfd 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -43,7 +43,7 @@ logger = logging.getLogger(__name__) MESSAGE_FROM_PERSON_IN_ROOM = ( - "You have a message on %(app)s from %(person)s " "in the %(room)s room..." + "You have a message on %(app)s from %(person)s in the %(room)s room..." ) MESSAGE_FROM_PERSON = "You have a message on %(app)s from %(person)s..." MESSAGES_FROM_PERSON = "You have messages on %(app)s from %(person)s..." @@ -55,7 +55,7 @@ MESSAGES_FROM_PERSON_AND_OTHERS = ( "You have messages on %(app)s from %(person)s and others..." ) INVITE_FROM_PERSON_TO_ROOM = ( - "%(person)s has invited you to join the " "%(room)s room on %(app)s..." + "%(person)s has invited you to join the %(room)s room on %(app)s..." ) INVITE_FROM_PERSON = "%(person)s has invited you to chat on %(app)s..." diff --git a/synapse/rest/media/v1/preview_url_resource.py b/synapse/rest/media/v1/preview_url_resource.py index 15c15a12f5..a23d6f5c75 100644 --- a/synapse/rest/media/v1/preview_url_resource.py +++ b/synapse/rest/media/v1/preview_url_resource.py @@ -122,7 +122,7 @@ class PreviewUrlResource(DirectServeResource): pattern = entry[attrib] value = getattr(url_tuple, attrib) logger.debug( - "Matching attrib '%s' with value '%s' against" " pattern '%s'", + "Matching attrib '%s' with value '%s' against pattern '%s'", attrib, value, pattern, diff --git a/synapse/server_notices/consent_server_notices.py b/synapse/server_notices/consent_server_notices.py index 415e9c17d8..5736c56032 100644 --- a/synapse/server_notices/consent_server_notices.py +++ b/synapse/server_notices/consent_server_notices.py @@ -54,7 +54,7 @@ class ConsentServerNotices(object): ) if "body" not in self._server_notice_content: raise ConfigError( - "user_consent server_notice_consent must contain a 'body' " "key." + "user_consent server_notice_consent must contain a 'body' key." ) self._consent_uri_builder = ConsentURIBuilder(hs.config) diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py index ab596fa68d..6b8a9cd89a 100644 --- a/synapse/storage/_base.py +++ b/synapse/storage/_base.py @@ -851,7 +851,7 @@ class SQLBaseStore(object): allvalues.update(values) latter = "UPDATE SET " + ", ".join(k + "=EXCLUDED." + k for k in values) - sql = ("INSERT INTO %s (%s) VALUES (%s) " "ON CONFLICT (%s) DO %s") % ( + sql = ("INSERT INTO %s (%s) VALUES (%s) ON CONFLICT (%s) DO %s") % ( table, ", ".join(k for k in allvalues), ", ".join("?" for _ in allvalues), diff --git a/synapse/storage/data_stores/main/deviceinbox.py b/synapse/storage/data_stores/main/deviceinbox.py index 96cd0fb77a..a23744f11c 100644 --- a/synapse/storage/data_stores/main/deviceinbox.py +++ b/synapse/storage/data_stores/main/deviceinbox.py @@ -380,7 +380,7 @@ class DeviceInboxStore(DeviceInboxWorkerStore, DeviceInboxBackgroundUpdateStore) devices = list(messages_by_device.keys()) if len(devices) == 1 and devices[0] == "*": # Handle wildcard device_ids. - sql = "SELECT device_id FROM devices" " WHERE user_id = ?" + sql = "SELECT device_id FROM devices WHERE user_id = ?" txn.execute(sql, (user_id,)) message_json = json.dumps(messages_by_device["*"]) for row in txn: diff --git a/synapse/storage/data_stores/main/end_to_end_keys.py b/synapse/storage/data_stores/main/end_to_end_keys.py index 073412a78d..d8ad59ad93 100644 --- a/synapse/storage/data_stores/main/end_to_end_keys.py +++ b/synapse/storage/data_stores/main/end_to_end_keys.py @@ -138,9 +138,9 @@ class EndToEndKeyWorkerStore(SQLBaseStore): result.setdefault(user_id, {})[device_id] = None # get signatures on the device - signature_sql = ( - "SELECT * " " FROM e2e_cross_signing_signatures " " WHERE %s" - ) % (" OR ".join("(" + q + ")" for q in signature_query_clauses)) + signature_sql = ("SELECT * FROM e2e_cross_signing_signatures WHERE %s") % ( + " OR ".join("(" + q + ")" for q in signature_query_clauses) + ) txn.execute(signature_sql, signature_query_params) rows = self.cursor_to_dict(txn) diff --git a/synapse/storage/data_stores/main/events.py b/synapse/storage/data_stores/main/events.py index 878f7568a6..627c0b67f1 100644 --- a/synapse/storage/data_stores/main/events.py +++ b/synapse/storage/data_stores/main/events.py @@ -713,9 +713,7 @@ class EventsStore( metadata_json = encode_json(event.internal_metadata.get_dict()) - sql = ( - "UPDATE event_json SET internal_metadata = ?" " WHERE event_id = ?" - ) + sql = "UPDATE event_json SET internal_metadata = ? WHERE event_id = ?" txn.execute(sql, (metadata_json, event.event_id)) # Add an entry to the ex_outlier_stream table to replicate the @@ -732,7 +730,7 @@ class EventsStore( }, ) - sql = "UPDATE events SET outlier = ?" " WHERE event_id = ?" + sql = "UPDATE events SET outlier = ? WHERE event_id = ?" txn.execute(sql, (False, event.event_id)) # Update the event_backward_extremities table now that this @@ -1479,7 +1477,7 @@ class EventsStore( # We do joins against events_to_purge for e.g. calculating state # groups to purge, etc., so lets make an index. - txn.execute("CREATE INDEX events_to_purge_id" " ON events_to_purge(event_id)") + txn.execute("CREATE INDEX events_to_purge_id ON events_to_purge(event_id)") txn.execute("SELECT event_id, should_delete FROM events_to_purge") event_rows = txn.fetchall() diff --git a/synapse/storage/data_stores/main/filtering.py b/synapse/storage/data_stores/main/filtering.py index a2a2a67927..f05ace299a 100644 --- a/synapse/storage/data_stores/main/filtering.py +++ b/synapse/storage/data_stores/main/filtering.py @@ -55,7 +55,7 @@ class FilteringStore(SQLBaseStore): if filter_id_response is not None: return filter_id_response[0] - sql = "SELECT MAX(filter_id) FROM user_filters " "WHERE user_id = ?" + sql = "SELECT MAX(filter_id) FROM user_filters WHERE user_id = ?" txn.execute(sql, (user_localpart,)) max_id = txn.fetchone()[0] if max_id is None: diff --git a/synapse/storage/data_stores/main/media_repository.py b/synapse/storage/data_stores/main/media_repository.py index 84b5f3ad5e..0f2887bdce 100644 --- a/synapse/storage/data_stores/main/media_repository.py +++ b/synapse/storage/data_stores/main/media_repository.py @@ -337,7 +337,7 @@ class MediaRepositoryStore(MediaRepositoryBackgroundUpdateStore): if len(media_ids) == 0: return - sql = "DELETE FROM local_media_repository_url_cache" " WHERE media_id = ?" + sql = "DELETE FROM local_media_repository_url_cache WHERE media_id = ?" def _delete_url_cache_txn(txn): txn.executemany(sql, [(media_id,) for media_id in media_ids]) @@ -365,11 +365,11 @@ class MediaRepositoryStore(MediaRepositoryBackgroundUpdateStore): return def _delete_url_cache_media_txn(txn): - sql = "DELETE FROM local_media_repository" " WHERE media_id = ?" + sql = "DELETE FROM local_media_repository WHERE media_id = ?" txn.executemany(sql, [(media_id,) for media_id in media_ids]) - sql = "DELETE FROM local_media_repository_thumbnails" " WHERE media_id = ?" + sql = "DELETE FROM local_media_repository_thumbnails WHERE media_id = ?" txn.executemany(sql, [(media_id,) for media_id in media_ids]) diff --git a/synapse/storage/data_stores/main/registration.py b/synapse/storage/data_stores/main/registration.py index ee1b2b2bbf..6a594c160c 100644 --- a/synapse/storage/data_stores/main/registration.py +++ b/synapse/storage/data_stores/main/registration.py @@ -377,9 +377,7 @@ class RegistrationWorkerStore(SQLBaseStore): """ def f(txn): - sql = ( - "SELECT name, password_hash FROM users" " WHERE lower(name) = lower(?)" - ) + sql = "SELECT name, password_hash FROM users WHERE lower(name) = lower(?)" txn.execute(sql, (user_id,)) return dict(txn) diff --git a/synapse/storage/data_stores/main/stream.py b/synapse/storage/data_stores/main/stream.py index 8780fdd989..9ae4a913a1 100644 --- a/synapse/storage/data_stores/main/stream.py +++ b/synapse/storage/data_stores/main/stream.py @@ -616,7 +616,7 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): def _get_max_topological_txn(self, txn, room_id): txn.execute( - "SELECT MAX(topological_ordering) FROM events" " WHERE room_id = ?", + "SELECT MAX(topological_ordering) FROM events WHERE room_id = ?", (room_id,), ) diff --git a/synapse/storage/data_stores/main/tags.py b/synapse/storage/data_stores/main/tags.py index 10d1887f75..aa24339717 100644 --- a/synapse/storage/data_stores/main/tags.py +++ b/synapse/storage/data_stores/main/tags.py @@ -83,9 +83,7 @@ class TagsWorkerStore(AccountDataWorkerStore): ) def get_tag_content(txn, tag_ids): - sql = ( - "SELECT tag, content" " FROM room_tags" " WHERE user_id=? AND room_id=?" - ) + sql = "SELECT tag, content FROM room_tags WHERE user_id=? AND room_id=?" results = [] for stream_id, user_id, room_id in tag_ids: txn.execute(sql, (user_id, room_id)) diff --git a/synapse/storage/prepare_database.py b/synapse/storage/prepare_database.py index 2e7753820e..731e1c9d9c 100644 --- a/synapse/storage/prepare_database.py +++ b/synapse/storage/prepare_database.py @@ -447,7 +447,7 @@ def _apply_module_schema_files(cur, database_engine, modname, names_and_streams) # Mark as done. cur.execute( database_engine.convert_param_style( - "INSERT INTO applied_module_schemas (module_name, file)" " VALUES (?,?)" + "INSERT INTO applied_module_schemas (module_name, file) VALUES (?,?)" ), (modname, name), ) diff --git a/synapse/streams/config.py b/synapse/streams/config.py index 02994ab2a5..cd56cd91ed 100644 --- a/synapse/streams/config.py +++ b/synapse/streams/config.py @@ -88,9 +88,12 @@ class PaginationConfig(object): raise SynapseError(400, "Invalid request.") def __repr__(self): - return ( - "PaginationConfig(from_tok=%r, to_tok=%r," " direction=%r, limit=%r)" - ) % (self.from_token, self.to_token, self.direction, self.limit) + return ("PaginationConfig(from_tok=%r, to_tok=%r, direction=%r, limit=%r)") % ( + self.from_token, + self.to_token, + self.direction, + self.limit, + ) def get_source_config(self, source_name): keyname = "%s_key" % source_name From 24cc31ee967e5c387a137e22b428dcea17fc9fa5 Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Thu, 21 Nov 2019 11:38:14 -0600 Subject: [PATCH 0528/1623] Fix link to user_dir_populate.sql in the user directory docs (#6388) --- changelog.d/6388.doc | 1 + docs/user_directory.md | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 changelog.d/6388.doc diff --git a/changelog.d/6388.doc b/changelog.d/6388.doc new file mode 100644 index 0000000000..c777cb6b8f --- /dev/null +++ b/changelog.d/6388.doc @@ -0,0 +1 @@ +Fix link in the user directory documentation. diff --git a/docs/user_directory.md b/docs/user_directory.md index e64aa453cc..37dc71e751 100644 --- a/docs/user_directory.md +++ b/docs/user_directory.md @@ -7,7 +7,6 @@ who are present in a publicly viewable room present on the server. The directory info is stored in various tables, which can (typically after DB corruption) get stale or out of sync. If this happens, for now the -solution to fix it is to execute the SQL here -https://github.com/matrix-org/synapse/blob/master/synapse/storage/schema/delta/53/user_dir_populate.sql +solution to fix it is to execute the SQL [here](../synapse/storage/data_stores/main/schema/delta/53/user_dir_populate.sql) and then restart synapse. This should then start a background task to flush the current tables and regenerate the directory. From 265c0bd2fe54db7f8a7dab05f41b27ce9a450563 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Fri, 22 Nov 2019 19:54:05 +0000 Subject: [PATCH 0529/1623] Add working build command for docker image (#6390) * Add working build command for docker image * Add changelog --- changelog.d/6390.doc | 1 + docker/README.md | 12 ++++++++++++ 2 files changed, 13 insertions(+) create mode 100644 changelog.d/6390.doc diff --git a/changelog.d/6390.doc b/changelog.d/6390.doc new file mode 100644 index 0000000000..093411bec1 --- /dev/null +++ b/changelog.d/6390.doc @@ -0,0 +1 @@ +Add build instructions to the docker readme. \ No newline at end of file diff --git a/docker/README.md b/docker/README.md index 24dfa77dcc..9f112a01d0 100644 --- a/docker/README.md +++ b/docker/README.md @@ -130,3 +130,15 @@ docker run -it --rm \ This will generate the same configuration file as the legacy mode used, but will store it in `/data/homeserver.yaml` instead of a temporary location. You can then use it as shown above at [Running synapse](#running-synapse). + +## Building the image + +If you need to build the image from a Synapse checkout, use the following `docker + build` command from the repo's root: + +``` +docker build -t matrixdotorg/synapse -f docker/Dockerfile . +``` + +You can choose to build a different docker image by changing the value of the `-f` flag to +point to another Dockerfile. From b7367c339db153824fb47728d3eebe2f944530e6 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 25 Nov 2019 13:26:59 +0000 Subject: [PATCH 0530/1623] Fix exceptions from background database update for event labels. (#6407) Add some exception handling here so that events whose json cannot be parsed are ignored rather than getting us stuck in a loop. Fixes #6404. --- changelog.d/6407.bugfix | 1 + .../data_stores/main/events_bg_updates.py | 41 +++++++++++-------- 2 files changed, 25 insertions(+), 17 deletions(-) create mode 100644 changelog.d/6407.bugfix diff --git a/changelog.d/6407.bugfix b/changelog.d/6407.bugfix new file mode 100644 index 0000000000..0fdbf2a781 --- /dev/null +++ b/changelog.d/6407.bugfix @@ -0,0 +1 @@ +Fix a bug which could cause the background database update hander for event labels to get stuck in a loop raising exceptions. diff --git a/synapse/storage/data_stores/main/events_bg_updates.py b/synapse/storage/data_stores/main/events_bg_updates.py index 0ed59ef48e..aa87f9abc5 100644 --- a/synapse/storage/data_stores/main/events_bg_updates.py +++ b/synapse/storage/data_stores/main/events_bg_updates.py @@ -530,24 +530,31 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): nbrows = 0 last_row_event_id = "" for (event_id, event_json_raw) in results: - event_json = json.loads(event_json_raw) + try: + event_json = json.loads(event_json_raw) - self._simple_insert_many_txn( - txn=txn, - table="event_labels", - values=[ - { - "event_id": event_id, - "label": label, - "room_id": event_json["room_id"], - "topological_ordering": event_json["depth"], - } - for label in event_json["content"].get( - EventContentFields.LABELS, [] - ) - if isinstance(label, str) - ], - ) + self._simple_insert_many_txn( + txn=txn, + table="event_labels", + values=[ + { + "event_id": event_id, + "label": label, + "room_id": event_json["room_id"], + "topological_ordering": event_json["depth"], + } + for label in event_json["content"].get( + EventContentFields.LABELS, [] + ) + if isinstance(label, str) + ], + ) + except Exception as e: + logger.warning( + "Unable to load event %s (no labels will be imported): %s", + event_id, + e, + ) nbrows += 1 last_row_event_id = event_id From f9c9e1f07646262cb064782d4bf427dd1634617f Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 25 Nov 2019 13:28:12 +0000 Subject: [PATCH 0531/1623] 1.6.0rc2 --- CHANGES.md | 9 +++++++++ changelog.d/6407.bugfix | 1 - synapse/__init__.py | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) delete mode 100644 changelog.d/6407.bugfix diff --git a/CHANGES.md b/CHANGES.md index f4f61db5d4..d26bc7a86f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,12 @@ +Synapse 1.6.0rc2 (2019-11-25) +============================= + +Bugfixes +-------- + +- Fix a bug which could cause the background database update hander for event labels to get stuck in a loop raising exceptions. ([\#6407](https://github.com/matrix-org/synapse/issues/6407)) + + Synapse 1.6.0rc1 (2019-11-20) ============================= diff --git a/changelog.d/6407.bugfix b/changelog.d/6407.bugfix deleted file mode 100644 index 0fdbf2a781..0000000000 --- a/changelog.d/6407.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a bug which could cause the background database update hander for event labels to get stuck in a loop raising exceptions. diff --git a/synapse/__init__.py b/synapse/__init__.py index 1d962f5dc8..051c83774e 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -36,7 +36,7 @@ try: except ImportError: pass -__version__ = "1.6.0rc1" +__version__ = "1.6.0rc2" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 9eebd46048d0b34767047b2156760a1467f19ae6 Mon Sep 17 00:00:00 2001 From: Amber Brown Date: Tue, 26 Nov 2019 03:45:50 +1100 Subject: [PATCH 0532/1623] Improve the performance of structured logging (#6322) --- changelog.d/6322.misc | 1 + synapse/logging/_structured.py | 14 ++++- synapse/logging/_terse_json.py | 104 ++++++++++++++++++++++++--------- tests/server.py | 2 + 4 files changed, 92 insertions(+), 29 deletions(-) create mode 100644 changelog.d/6322.misc diff --git a/changelog.d/6322.misc b/changelog.d/6322.misc new file mode 100644 index 0000000000..70ef36ca80 --- /dev/null +++ b/changelog.d/6322.misc @@ -0,0 +1 @@ +Improve the performance of outputting structured logging. diff --git a/synapse/logging/_structured.py b/synapse/logging/_structured.py index 334ddaf39a..ffa7b20ca8 100644 --- a/synapse/logging/_structured.py +++ b/synapse/logging/_structured.py @@ -261,6 +261,18 @@ def parse_drain_configs( ) +class StoppableLogPublisher(LogPublisher): + """ + A log publisher that can tell its observers to shut down any external + communications. + """ + + def stop(self): + for obs in self._observers: + if hasattr(obs, "stop"): + obs.stop() + + def setup_structured_logging( hs, config, @@ -336,7 +348,7 @@ def setup_structured_logging( # We should never get here, but, just in case, throw an error. raise ConfigError("%s drain type cannot be configured" % (observer.type,)) - publisher = LogPublisher(*observers) + publisher = StoppableLogPublisher(*observers) log_filter = LogLevelFilterPredicate() for namespace, namespace_config in log_config.get( diff --git a/synapse/logging/_terse_json.py b/synapse/logging/_terse_json.py index 76ce7d8808..05fc64f409 100644 --- a/synapse/logging/_terse_json.py +++ b/synapse/logging/_terse_json.py @@ -17,25 +17,29 @@ Log formatters that output terse JSON. """ +import json import sys +import traceback from collections import deque from ipaddress import IPv4Address, IPv6Address, ip_address from math import floor -from typing import IO +from typing import IO, Optional import attr -from simplejson import dumps from zope.interface import implementer from twisted.application.internet import ClientService +from twisted.internet.defer import Deferred from twisted.internet.endpoints import ( HostnameEndpoint, TCP4ClientEndpoint, TCP6ClientEndpoint, ) +from twisted.internet.interfaces import IPushProducer, ITransport from twisted.internet.protocol import Factory, Protocol from twisted.logger import FileLogObserver, ILogObserver, Logger -from twisted.python.failure import Failure + +_encoder = json.JSONEncoder(ensure_ascii=False, separators=(",", ":")) def flatten_event(event: dict, metadata: dict, include_time: bool = False): @@ -141,11 +145,49 @@ def TerseJSONToConsoleLogObserver(outFile: IO[str], metadata: dict) -> FileLogOb def formatEvent(_event: dict) -> str: flattened = flatten_event(_event, metadata) - return dumps(flattened, ensure_ascii=False, separators=(",", ":")) + "\n" + return _encoder.encode(flattened) + "\n" return FileLogObserver(outFile, formatEvent) +@attr.s +@implementer(IPushProducer) +class LogProducer(object): + """ + An IPushProducer that writes logs from its buffer to its transport when it + is resumed. + + Args: + buffer: Log buffer to read logs from. + transport: Transport to write to. + """ + + transport = attr.ib(type=ITransport) + _buffer = attr.ib(type=deque) + _paused = attr.ib(default=False, type=bool, init=False) + + def pauseProducing(self): + self._paused = True + + def stopProducing(self): + self._paused = True + self._buffer = None + + def resumeProducing(self): + self._paused = False + + while self._paused is False and (self._buffer and self.transport.connected): + try: + event = self._buffer.popleft() + self.transport.write(_encoder.encode(event).encode("utf8")) + self.transport.write(b"\n") + except Exception: + # Something has gone wrong writing to the transport -- log it + # and break out of the while. + traceback.print_exc(file=sys.__stderr__) + break + + @attr.s @implementer(ILogObserver) class TerseJSONToTCPLogObserver(object): @@ -165,8 +207,9 @@ class TerseJSONToTCPLogObserver(object): metadata = attr.ib(type=dict) maximum_buffer = attr.ib(type=int) _buffer = attr.ib(default=attr.Factory(deque), type=deque) - _writer = attr.ib(default=None) + _connection_waiter = attr.ib(default=None, type=Optional[Deferred]) _logger = attr.ib(default=attr.Factory(Logger)) + _producer = attr.ib(default=None, type=Optional[LogProducer]) def start(self) -> None: @@ -187,38 +230,43 @@ class TerseJSONToTCPLogObserver(object): factory = Factory.forProtocol(Protocol) self._service = ClientService(endpoint, factory, clock=self.hs.get_reactor()) self._service.startService() + self._connect() - def _write_loop(self) -> None: + def stop(self): + self._service.stopService() + + def _connect(self) -> None: """ - Implement the write loop. + Triggers an attempt to connect then write to the remote if not already writing. """ - if self._writer: + if self._connection_waiter: return - self._writer = self._service.whenConnected() + self._connection_waiter = self._service.whenConnected(failAfterFailures=1) - @self._writer.addBoth + @self._connection_waiter.addErrback + def fail(r): + r.printTraceback(file=sys.__stderr__) + self._connection_waiter = None + self._connect() + + @self._connection_waiter.addCallback def writer(r): - if isinstance(r, Failure): - r.printTraceback(file=sys.__stderr__) - self._writer = None - self.hs.get_reactor().callLater(1, self._write_loop) + # We have a connection. If we already have a producer, and its + # transport is the same, just trigger a resumeProducing. + if self._producer and r.transport is self._producer.transport: + self._producer.resumeProducing() return - try: - for event in self._buffer: - r.transport.write( - dumps(event, ensure_ascii=False, separators=(",", ":")).encode( - "utf8" - ) - ) - r.transport.write(b"\n") - self._buffer.clear() - except Exception as e: - sys.__stderr__.write("Failed writing out logs with %s\n" % (str(e),)) + # If the producer is still producing, stop it. + if self._producer: + self._producer.stopProducing() - self._writer = False - self.hs.get_reactor().callLater(1, self._write_loop) + # Make a new producer and start it. + self._producer = LogProducer(buffer=self._buffer, transport=r.transport) + r.transport.registerProducer(self._producer, True) + self._producer.resumeProducing() + self._connection_waiter = None def _handle_pressure(self) -> None: """ @@ -277,4 +325,4 @@ class TerseJSONToTCPLogObserver(object): self._logger.failure("Failed clearing backpressure") # Try and write immediately. - self._write_loop() + self._connect() diff --git a/tests/server.py b/tests/server.py index f878aeaada..2b7cf4242e 100644 --- a/tests/server.py +++ b/tests/server.py @@ -379,6 +379,7 @@ class FakeTransport(object): disconnecting = False disconnected = False + connected = True buffer = attr.ib(default=b"") producer = attr.ib(default=None) autoflush = attr.ib(default=True) @@ -402,6 +403,7 @@ class FakeTransport(object): "FakeTransport: Delaying disconnect until buffer is flushed" ) else: + self.connected = False self.disconnected = True def abortConnection(self): From c01d5435843ad4af3d520851e86d9938b47b2d12 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 25 Nov 2019 21:03:17 +0000 Subject: [PATCH 0533/1623] Make sure that we close cursors before returning from a query (#6408) There are lots of words in the comment as to why this is a good idea. Fixes #6403. --- changelog.d/6408.bugfix | 1 + synapse/storage/_base.py | 51 ++++++++++++++++---- synapse/storage/data_stores/main/receipts.py | 2 +- 3 files changed, 44 insertions(+), 10 deletions(-) create mode 100644 changelog.d/6408.bugfix diff --git a/changelog.d/6408.bugfix b/changelog.d/6408.bugfix new file mode 100644 index 0000000000..c9babe599b --- /dev/null +++ b/changelog.d/6408.bugfix @@ -0,0 +1 @@ +Fix an intermittent exception when handling read-receipts. diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py index 6b8a9cd89a..459901ac60 100644 --- a/synapse/storage/_base.py +++ b/synapse/storage/_base.py @@ -409,16 +409,15 @@ class SQLBaseStore(object): i = 0 N = 5 while True: + cursor = LoggingTransaction( + conn.cursor(), + name, + self.database_engine, + after_callbacks, + exception_callbacks, + ) try: - txn = conn.cursor() - txn = LoggingTransaction( - txn, - name, - self.database_engine, - after_callbacks, - exception_callbacks, - ) - r = func(txn, *args, **kwargs) + r = func(cursor, *args, **kwargs) conn.commit() return r except self.database_engine.module.OperationalError as e: @@ -456,6 +455,40 @@ class SQLBaseStore(object): ) continue raise + finally: + # we're either about to retry with a new cursor, or we're about to + # release the connection. Once we release the connection, it could + # get used for another query, which might do a conn.rollback(). + # + # In the latter case, even though that probably wouldn't affect the + # results of this transaction, python's sqlite will reset all + # statements on the connection [1], which will make our cursor + # invalid [2]. + # + # In any case, continuing to read rows after commit()ing seems + # dubious from the PoV of ACID transactional semantics + # (sqlite explicitly says that once you commit, you may see rows + # from subsequent updates.) + # + # In psycopg2, cursors are essentially a client-side fabrication - + # all the data is transferred to the client side when the statement + # finishes executing - so in theory we could go on streaming results + # from the cursor, but attempting to do so would make us + # incompatible with sqlite, so let's make sure we're not doing that + # by closing the cursor. + # + # (*named* cursors in psycopg2 are different and are proper server- + # side things, but (a) we don't use them and (b) they are implicitly + # closed by ending the transaction anyway.) + # + # In short, if we haven't finished with the cursor yet, that's a + # problem waiting to bite us. + # + # TL;DR: we're done with the cursor, so we can close it. + # + # [1]: https://github.com/python/cpython/blob/v3.8.0/Modules/_sqlite/connection.c#L465 + # [2]: https://github.com/python/cpython/blob/v3.8.0/Modules/_sqlite/cursor.c#L236 + cursor.close() except Exception as e: logger.debug("[TXN FAIL] {%s} %s", name, e) raise diff --git a/synapse/storage/data_stores/main/receipts.py b/synapse/storage/data_stores/main/receipts.py index 0c24430f28..8b17334ff4 100644 --- a/synapse/storage/data_stores/main/receipts.py +++ b/synapse/storage/data_stores/main/receipts.py @@ -280,7 +280,7 @@ class ReceiptsWorkerStore(SQLBaseStore): args.append(limit) txn.execute(sql, args) - return (r[0:5] + (json.loads(r[5]),) for r in txn) + return list(r[0:5] + (json.loads(r[5]),) for r in txn) return self.runInteraction( "get_all_updated_receipts", get_all_updated_receipts_txn From 35f9165e96f6261e15aadb439a5d2199bede3c99 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 26 Nov 2019 12:04:48 +0000 Subject: [PATCH 0534/1623] Fixup docs --- changelog.d/6332.bugfix | 2 +- synapse/replication/http/devices.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/changelog.d/6332.bugfix b/changelog.d/6332.bugfix index b14bd7e43c..67d5170ba0 100644 --- a/changelog.d/6332.bugfix +++ b/changelog.d/6332.bugfix @@ -1 +1 @@ -Fix caching devices for remote users when using workers. +Fix caching devices for remote users when using workers, so that we don't attempt to refetch (and potentially fail) each time a user requests devices. diff --git a/synapse/replication/http/devices.py b/synapse/replication/http/devices.py index 795ca7b65e..e32aac0a25 100644 --- a/synapse/replication/http/devices.py +++ b/synapse/replication/http/devices.py @@ -21,7 +21,11 @@ logger = logging.getLogger(__name__) class ReplicationUserDevicesResyncRestServlet(ReplicationEndpoint): - """Notifies that a user has joined or left the room + """Ask master to resync the device list for a user by contacting their + server. + + This must happen on master so that the results can be correctly cached in + the database and streamed to workers. Request format: From 4d394d6415e3e4b64a577a1c83ee8acf147ce0af Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 26 Nov 2019 12:32:37 +0000 Subject: [PATCH 0535/1623] remove confusing fixme --- synapse/handlers/federation.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 66852153c4..97d045db10 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -2187,12 +2187,6 @@ class FederationHandler(BaseHandler): logger.info("Skipping auth_event fetch for outlier") return context - # FIXME: Assumes we have and stored all the state for all the - # prev_events - # - # FIXME: what does the fixme above mean? where do prev_events come into - # it, why do we care about the state for those events, and what does "have and - # stored" mean? Seems erik wrote it in c1d860870b different_auth = event_auth_events.difference( e.event_id for e in auth_events.values() ) From 65d54c5e8c1434eada5d1b670d7db2e37d68d4f1 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 26 Nov 2019 13:10:09 +0000 Subject: [PATCH 0536/1623] Fix phone home stats (#6418) Fix phone home stats --- changelog.d/6418.bugfix | 1 + synapse/app/homeserver.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/6418.bugfix diff --git a/changelog.d/6418.bugfix b/changelog.d/6418.bugfix new file mode 100644 index 0000000000..a1f488d3a2 --- /dev/null +++ b/changelog.d/6418.bugfix @@ -0,0 +1 @@ +Fix phone home stats reporting. diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py index 73e2c29d06..883b3fb70b 100644 --- a/synapse/app/homeserver.py +++ b/synapse/app/homeserver.py @@ -585,7 +585,7 @@ def run(hs): def performance_stats_init(): _stats_process.clear() _stats_process.append( - (int(hs.get_clock().time(), resource.getrusage(resource.RUSAGE_SELF))) + (int(hs.get_clock().time()), resource.getrusage(resource.RUSAGE_SELF)) ) def start_phone_stats_home(): From b98971e8a437eb3903506eadbefdf6cb2e0853d6 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Tue, 26 Nov 2019 12:15:46 +0000 Subject: [PATCH 0537/1623] 1.6.0 --- CHANGES.md | 9 +++++++++ changelog.d/6418.bugfix | 1 - debian/changelog | 6 ++++++ synapse/__init__.py | 2 +- 4 files changed, 16 insertions(+), 2 deletions(-) delete mode 100644 changelog.d/6418.bugfix diff --git a/CHANGES.md b/CHANGES.md index d26bc7a86f..42281483b3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,12 @@ +Synapse 1.6.0 (2019-11-26) +========================== + +Bugfixes +-------- + +- Fix phone home stats reporting. ([\#6418](https://github.com/matrix-org/synapse/issues/6418)) + + Synapse 1.6.0rc2 (2019-11-25) ============================= diff --git a/changelog.d/6418.bugfix b/changelog.d/6418.bugfix deleted file mode 100644 index a1f488d3a2..0000000000 --- a/changelog.d/6418.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix phone home stats reporting. diff --git a/debian/changelog b/debian/changelog index c4415f460a..82dae017f1 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.6.0) stable; urgency=medium + + * New synapse release 1.6.0. + + -- Synapse Packaging team Tue, 26 Nov 2019 12:15:40 +0000 + matrix-synapse-py3 (1.5.1) stable; urgency=medium * New synapse release 1.5.1. diff --git a/synapse/__init__.py b/synapse/__init__.py index 051c83774e..53eedc0048 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -36,7 +36,7 @@ try: except ImportError: pass -__version__ = "1.6.0rc2" +__version__ = "1.6.0" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 8bb7b15894462c6a0ba81f7198f23a140100331d Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 26 Nov 2019 15:50:17 +0000 Subject: [PATCH 0538/1623] Fix find_next_generated_user_id_localpart --- .../storage/data_stores/main/registration.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/synapse/storage/data_stores/main/registration.py b/synapse/storage/data_stores/main/registration.py index 6a594c160c..c124bbb88b 100644 --- a/synapse/storage/data_stores/main/registration.py +++ b/synapse/storage/data_stores/main/registration.py @@ -19,7 +19,6 @@ import logging import re from six import iterkeys -from six.moves import range from twisted.internet import defer from twisted.internet.defer import Deferred @@ -482,12 +481,8 @@ class RegistrationWorkerStore(SQLBaseStore): """ Gets the localpart of the next generated user ID. - Generated user IDs are integers, and we aim for them to be as small as - we can. Unfortunately, it's possible some of them are already taken by - existing users, and there may be gaps in the already taken range. This - function returns the start of the first allocatable gap. This is to - avoid the case of ID 1000 being pre-allocated and starting at 1001 while - 0-999 are available. + Generated user IDs are integers, so we find the largest integer user ID + already taken and return that plus one. """ def _find_next_generated_user_id(txn): @@ -503,9 +498,11 @@ class RegistrationWorkerStore(SQLBaseStore): match = regex.search(user_id) if match: found.add(int(match.group(1))) - for i in range(len(found) + 1): - if i not in found: - return i + + if not found: + return 1 + + return max(found) + 1 return ( ( From ba110a2030274dc785b37b1d5ad20acc4de9c612 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 26 Nov 2019 15:54:10 +0000 Subject: [PATCH 0539/1623] Newsfile --- changelog.d/6420.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6420.bugfix diff --git a/changelog.d/6420.bugfix b/changelog.d/6420.bugfix new file mode 100644 index 0000000000..aef47cccaa --- /dev/null +++ b/changelog.d/6420.bugfix @@ -0,0 +1 @@ +Fix broken guest registration when there are existing blocks of numeric user IDs. From f8f14ba466a18ee38827629b8e9ab2d5931e5713 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 26 Nov 2019 16:06:41 +0000 Subject: [PATCH 0540/1623] Don't construct a set --- synapse/storage/data_stores/main/registration.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/synapse/storage/data_stores/main/registration.py b/synapse/storage/data_stores/main/registration.py index c124bbb88b..0a3c1f0510 100644 --- a/synapse/storage/data_stores/main/registration.py +++ b/synapse/storage/data_stores/main/registration.py @@ -492,17 +492,14 @@ class RegistrationWorkerStore(SQLBaseStore): regex = re.compile(r"^@(\d+):") - found = set() + max_found = 0 for (user_id,) in txn: match = regex.search(user_id) if match: - found.add(int(match.group(1))) + max_found = max(int(match.group(1)), max_found) - if not found: - return 1 - - return max(found) + 1 + return max_found + 1 return ( ( From f0ef9708241ec65fe6f32f1ad36f719ab4ab2b53 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 26 Nov 2019 17:49:12 +0000 Subject: [PATCH 0541/1623] Don't restrict the tests to v1 rooms --- tests/rest/client/test_retention.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/rest/client/test_retention.py b/tests/rest/client/test_retention.py index 9e549d8a91..95475bb651 100644 --- a/tests/rest/client/test_retention.py +++ b/tests/rest/client/test_retention.py @@ -34,7 +34,6 @@ class RetentionTestCase(unittest.HomeserverTestCase): def make_homeserver(self, reactor, clock): config = self.default_config() - config["default_room_version"] = "1" config["retention"] = { "enabled": True, "default_policy": { @@ -204,7 +203,6 @@ class RetentionNoDefaultPolicyTestCase(unittest.HomeserverTestCase): def make_homeserver(self, reactor, clock): config = self.default_config() - config["default_room_version"] = "1" config["retention"] = { "enabled": True, } From ef1a85e7733bc1979f48357dd59b638110285075 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 26 Nov 2019 18:10:50 +0000 Subject: [PATCH 0542/1623] Fix startup error when http proxy is defined. (#6421) Guess I only tested this on python 2 :/ Fixes #6419. --- changelog.d/6421.bugfix | 1 + synapse/rest/media/v1/preview_url_resource.py | 4 ++-- synapse/server.py | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 changelog.d/6421.bugfix diff --git a/changelog.d/6421.bugfix b/changelog.d/6421.bugfix new file mode 100644 index 0000000000..7969f7f71d --- /dev/null +++ b/changelog.d/6421.bugfix @@ -0,0 +1 @@ +Fix startup error when http proxy is defined. diff --git a/synapse/rest/media/v1/preview_url_resource.py b/synapse/rest/media/v1/preview_url_resource.py index a23d6f5c75..fb0d02aa83 100644 --- a/synapse/rest/media/v1/preview_url_resource.py +++ b/synapse/rest/media/v1/preview_url_resource.py @@ -77,8 +77,8 @@ class PreviewUrlResource(DirectServeResource): treq_args={"browser_like_redirects": True}, ip_whitelist=hs.config.url_preview_ip_range_whitelist, ip_blacklist=hs.config.url_preview_ip_range_blacklist, - http_proxy=os.getenv("http_proxy"), - https_proxy=os.getenv("HTTPS_PROXY"), + http_proxy=os.getenvb(b"http_proxy"), + https_proxy=os.getenvb(b"HTTPS_PROXY"), ) self.media_repo = media_repo self.primary_base_path = media_repo.primary_base_path diff --git a/synapse/server.py b/synapse/server.py index 90c3b072e8..be9af7f986 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -318,8 +318,8 @@ class HomeServer(object): def build_proxied_http_client(self): return SimpleHttpClient( self, - http_proxy=os.getenv("http_proxy"), - https_proxy=os.getenv("HTTPS_PROXY"), + http_proxy=os.getenvb(b"http_proxy"), + https_proxy=os.getenvb(b"HTTPS_PROXY"), ) def build_room_creation_handler(self): From ce578031f4d0fe6f1eb26de4cb3d30a4175468db Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Tue, 26 Nov 2019 18:42:27 +0000 Subject: [PATCH 0543/1623] Remove assertion and provide a clear warning on startup for missing public_baseurl (#6379) --- changelog.d/6379.misc | 1 + synapse/config/emailconfig.py | 2 ++ synapse/config/registration.py | 7 +++++++ tests/rest/client/v2_alpha/test_register.py | 1 + 4 files changed, 11 insertions(+) create mode 100644 changelog.d/6379.misc diff --git a/changelog.d/6379.misc b/changelog.d/6379.misc new file mode 100644 index 0000000000..725c2e7d87 --- /dev/null +++ b/changelog.d/6379.misc @@ -0,0 +1 @@ +Complain on startup instead of 500'ing during runtime when `public_baseurl` isn't set when necessary. \ No newline at end of file diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py index 43fad0bf8b..ac1724045f 100644 --- a/synapse/config/emailconfig.py +++ b/synapse/config/emailconfig.py @@ -146,6 +146,8 @@ class EmailConfig(Config): if k not in email_config: missing.append("email." + k) + # public_baseurl is required to build password reset and validation links that + # will be emailed to users if config.get("public_baseurl") is None: missing.append("public_baseurl") diff --git a/synapse/config/registration.py b/synapse/config/registration.py index 1f6dac69da..ee9614c5f7 100644 --- a/synapse/config/registration.py +++ b/synapse/config/registration.py @@ -106,6 +106,13 @@ class RegistrationConfig(Config): account_threepid_delegates = config.get("account_threepid_delegates") or {} self.account_threepid_delegate_email = account_threepid_delegates.get("email") self.account_threepid_delegate_msisdn = account_threepid_delegates.get("msisdn") + if self.account_threepid_delegate_msisdn and not self.public_baseurl: + raise ConfigError( + "The configuration option `public_baseurl` is required if " + "`account_threepid_delegate.msisdn` is set, such that " + "clients know where to submit validation tokens to. Please " + "configure `public_baseurl`." + ) self.default_identity_server = config.get("default_identity_server") self.allow_guest_access = config.get("allow_guest_access", False) diff --git a/tests/rest/client/v2_alpha/test_register.py b/tests/rest/client/v2_alpha/test_register.py index dab87e5edf..c0d0d2b44e 100644 --- a/tests/rest/client/v2_alpha/test_register.py +++ b/tests/rest/client/v2_alpha/test_register.py @@ -203,6 +203,7 @@ class RegisterRestServletTestCase(unittest.HomeserverTestCase): @unittest.override_config( { + "public_baseurl": "https://test_server", "enable_registration_captcha": True, "user_consent": { "version": "1", From 70c0da4d82e30f8e84d0424666ea4797e437d4ca Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 26 Nov 2019 19:00:24 +0000 Subject: [PATCH 0544/1623] clean up buildkite output --- .buildkite/merge_base_branch.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.buildkite/merge_base_branch.sh b/.buildkite/merge_base_branch.sh index eb7219a56d..361440fd1a 100755 --- a/.buildkite/merge_base_branch.sh +++ b/.buildkite/merge_base_branch.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -set -ex +set -e if [[ "$BUILDKITE_BRANCH" =~ ^(develop|master|dinsic|shhs|release-.*)$ ]]; then echo "Not merging forward, as this is a release branch" @@ -18,6 +18,8 @@ else GITBASE=$BUILDKITE_PULL_REQUEST_BASE_BRANCH fi +echo "--- merge_base_branch $GITBASE" + # Show what we are before git --no-pager show -s From 9b9ee75666ffca8e14b44efec6c360c2ccbcf615 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 26 Nov 2019 18:10:50 +0000 Subject: [PATCH 0545/1623] Fix startup error when http proxy is defined. (#6421) Guess I only tested this on python 2 :/ Fixes #6419. --- changelog.d/6421.bugfix | 1 + synapse/rest/media/v1/preview_url_resource.py | 4 ++-- synapse/server.py | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 changelog.d/6421.bugfix diff --git a/changelog.d/6421.bugfix b/changelog.d/6421.bugfix new file mode 100644 index 0000000000..7969f7f71d --- /dev/null +++ b/changelog.d/6421.bugfix @@ -0,0 +1 @@ +Fix startup error when http proxy is defined. diff --git a/synapse/rest/media/v1/preview_url_resource.py b/synapse/rest/media/v1/preview_url_resource.py index 15c15a12f5..87343d9db9 100644 --- a/synapse/rest/media/v1/preview_url_resource.py +++ b/synapse/rest/media/v1/preview_url_resource.py @@ -77,8 +77,8 @@ class PreviewUrlResource(DirectServeResource): treq_args={"browser_like_redirects": True}, ip_whitelist=hs.config.url_preview_ip_range_whitelist, ip_blacklist=hs.config.url_preview_ip_range_blacklist, - http_proxy=os.getenv("http_proxy"), - https_proxy=os.getenv("HTTPS_PROXY"), + http_proxy=os.getenvb(b"http_proxy"), + https_proxy=os.getenvb(b"HTTPS_PROXY"), ) self.media_repo = media_repo self.primary_base_path = media_repo.primary_base_path diff --git a/synapse/server.py b/synapse/server.py index 90c3b072e8..be9af7f986 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -318,8 +318,8 @@ class HomeServer(object): def build_proxied_http_client(self): return SimpleHttpClient( self, - http_proxy=os.getenv("http_proxy"), - https_proxy=os.getenv("HTTPS_PROXY"), + http_proxy=os.getenvb(b"http_proxy"), + https_proxy=os.getenvb(b"HTTPS_PROXY"), ) def build_room_creation_handler(self): From 6f4a63df0044a304109f7d6958be94262558a7cf Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 27 Nov 2019 18:18:33 +0000 Subject: [PATCH 0546/1623] Add more tests to the worker blacklist (#6429) --- .buildkite/worker-blacklist | 29 +++++++++++++++++++++++++++++ changelog.d/6429.misc | 1 + 2 files changed, 30 insertions(+) create mode 100644 changelog.d/6429.misc diff --git a/.buildkite/worker-blacklist b/.buildkite/worker-blacklist index cda5c84e94..d7908af177 100644 --- a/.buildkite/worker-blacklist +++ b/.buildkite/worker-blacklist @@ -28,3 +28,32 @@ User sees updates to presence from other users in the incremental sync. Gapped incremental syncs include all state changes Old members are included in gappy incr LL sync if they start speaking + +# new failures as of https://github.com/matrix-org/sytest/pull/732 +Device list doesn't change if remote server is down +Remote servers cannot set power levels in rooms without existing powerlevels +Remote servers should reject attempts by non-creators to set the power levels + +# new failures as of https://github.com/matrix-org/sytest/pull/753 +GET /rooms/:room_id/messages returns a message +GET /rooms/:room_id/messages lazy loads members correctly +Read receipts are sent as events +Only original members of the room can see messages from erased users +Device deletion propagates over federation +If user leaves room, remote user changes device and rejoins we see update in /sync and /keys/changes +Changing user-signing key notifies local users +Newly updated tags appear in an incremental v2 /sync +Server correctly handles incoming m.device_list_update +Local device key changes get to remote servers with correct prev_id +AS-ghosted users can use rooms via AS +Ghost user must register before joining room +Test that a message is pushed +Invites are pushed +Rooms with aliases are correctly named in pushed +Rooms with names are correctly named in pushed +Rooms with canonical alias are correctly named in pushed +Rooms with many users are correctly pushed +Don't get pushed for rooms you've muted +Rejected events are not pushed +Test that rejected pushers are removed. +Events come down the correct room diff --git a/changelog.d/6429.misc b/changelog.d/6429.misc new file mode 100644 index 0000000000..4b32cdeac6 --- /dev/null +++ b/changelog.d/6429.misc @@ -0,0 +1 @@ +Add more tests to the blacklist when running in worker mode. From 0d27aba900136514a8801b902f9a8ac69150e2c0 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 27 Nov 2019 16:14:44 -0500 Subject: [PATCH 0547/1623] add etag and count to key backup endpoints (#5858) --- changelog.d/5858.feature | 1 + synapse/handlers/e2e_room_keys.py | 126 ++++++---- synapse/rest/client/v2_alpha/room_keys.py | 8 +- .../storage/data_stores/main/e2e_room_keys.py | 226 +++++++++++++----- .../main/schema/delta/56/room_key_etag.sql | 17 ++ tests/handlers/test_e2e_room_keys.py | 31 +++ tests/storage/test_e2e_room_keys.py | 8 +- 7 files changed, 295 insertions(+), 122 deletions(-) create mode 100644 changelog.d/5858.feature create mode 100644 synapse/storage/data_stores/main/schema/delta/56/room_key_etag.sql diff --git a/changelog.d/5858.feature b/changelog.d/5858.feature new file mode 100644 index 0000000000..55ee93051e --- /dev/null +++ b/changelog.d/5858.feature @@ -0,0 +1 @@ +Add etag and count fields to key backup endpoints to help clients guess if there are new keys. diff --git a/synapse/handlers/e2e_room_keys.py b/synapse/handlers/e2e_room_keys.py index 0cea445f0d..f1b4424a02 100644 --- a/synapse/handlers/e2e_room_keys.py +++ b/synapse/handlers/e2e_room_keys.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # Copyright 2017, 2018 New Vector Ltd +# Copyright 2019 Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -103,14 +104,35 @@ class E2eRoomKeysHandler(object): rooms session_id(string): session ID to delete keys for, for None to delete keys for all sessions + Raises: + NotFoundError: if the backup version does not exist Returns: - A deferred of the deletion transaction + A dict containing the count and etag for the backup version """ # lock for consistency with uploading with (yield self._upload_linearizer.queue(user_id)): + # make sure the backup version exists + try: + version_info = yield self.store.get_e2e_room_keys_version_info( + user_id, version + ) + except StoreError as e: + if e.code == 404: + raise NotFoundError("Unknown backup version") + else: + raise + yield self.store.delete_e2e_room_keys(user_id, version, room_id, session_id) + version_etag = version_info["etag"] + 1 + yield self.store.update_e2e_room_keys_version( + user_id, version, None, version_etag + ) + + count = yield self.store.count_e2e_room_keys(user_id, version) + return {"etag": str(version_etag), "count": count} + @trace @defer.inlineCallbacks def upload_room_keys(self, user_id, version, room_keys): @@ -138,6 +160,9 @@ class E2eRoomKeysHandler(object): } } + Returns: + A dict containing the count and etag for the backup version + Raises: NotFoundError: if there are no versions defined RoomKeysVersionError: if the uploaded version is not the current version @@ -171,59 +196,62 @@ class E2eRoomKeysHandler(object): else: raise - # go through the room_keys. - # XXX: this should/could be done concurrently, given we're in a lock. + # Fetch any existing room keys for the sessions that have been + # submitted. Then compare them with the submitted keys. If the + # key is new, insert it; if the key should be updated, then update + # it; otherwise, drop it. + existing_keys = yield self.store.get_e2e_room_keys_multi( + user_id, version, room_keys["rooms"] + ) + to_insert = [] # batch the inserts together + changed = False # if anything has changed, we need to update the etag for room_id, room in iteritems(room_keys["rooms"]): - for session_id, session in iteritems(room["sessions"]): - yield self._upload_room_key( - user_id, version, room_id, session_id, session + for session_id, room_key in iteritems(room["sessions"]): + log_kv( + { + "message": "Trying to upload room key", + "room_id": room_id, + "session_id": session_id, + "user_id": user_id, + } ) + current_room_key = existing_keys.get(room_id, {}).get(session_id) + if current_room_key: + if self._should_replace_room_key(current_room_key, room_key): + log_kv({"message": "Replacing room key."}) + # updates are done one at a time in the DB, so send + # updates right away rather than batching them up, + # like we do with the inserts + yield self.store.update_e2e_room_key( + user_id, version, room_id, session_id, room_key + ) + changed = True + else: + log_kv({"message": "Not replacing room_key."}) + else: + log_kv( + { + "message": "Room key not found.", + "room_id": room_id, + "user_id": user_id, + } + ) + log_kv({"message": "Replacing room key."}) + to_insert.append((room_id, session_id, room_key)) + changed = True - @defer.inlineCallbacks - def _upload_room_key(self, user_id, version, room_id, session_id, room_key): - """Upload a given room_key for a given room and session into a given - version of the backup. Merges the key with any which might already exist. + if len(to_insert): + yield self.store.add_e2e_room_keys(user_id, version, to_insert) - Args: - user_id(str): the user whose backup we're setting - version(str): the version ID of the backup we're updating - room_id(str): the ID of the room whose keys we're setting - session_id(str): the session whose room_key we're setting - room_key(dict): the room_key being set - """ - log_kv( - { - "message": "Trying to upload room key", - "room_id": room_id, - "session_id": session_id, - "user_id": user_id, - } - ) - # get the room_key for this particular row - current_room_key = None - try: - current_room_key = yield self.store.get_e2e_room_key( - user_id, version, room_id, session_id - ) - except StoreError as e: - if e.code == 404: - log_kv( - { - "message": "Room key not found.", - "room_id": room_id, - "user_id": user_id, - } + version_etag = version_info["etag"] + if changed: + version_etag = version_etag + 1 + yield self.store.update_e2e_room_keys_version( + user_id, version, None, version_etag ) - else: - raise - if self._should_replace_room_key(current_room_key, room_key): - log_kv({"message": "Replacing room key."}) - yield self.store.set_e2e_room_key( - user_id, version, room_id, session_id, room_key - ) - else: - log_kv({"message": "Not replacing room_key."}) + count = yield self.store.count_e2e_room_keys(user_id, version) + return {"etag": str(version_etag), "count": count} @staticmethod def _should_replace_room_key(current_room_key, room_key): @@ -314,6 +342,8 @@ class E2eRoomKeysHandler(object): raise NotFoundError("Unknown backup version") else: raise + + res["count"] = yield self.store.count_e2e_room_keys(user_id, res["version"]) return res @trace diff --git a/synapse/rest/client/v2_alpha/room_keys.py b/synapse/rest/client/v2_alpha/room_keys.py index d596786430..d83ac8e3c5 100644 --- a/synapse/rest/client/v2_alpha/room_keys.py +++ b/synapse/rest/client/v2_alpha/room_keys.py @@ -134,8 +134,8 @@ class RoomKeysServlet(RestServlet): if room_id: body = {"rooms": {room_id: body}} - yield self.e2e_room_keys_handler.upload_room_keys(user_id, version, body) - return 200, {} + ret = yield self.e2e_room_keys_handler.upload_room_keys(user_id, version, body) + return 200, ret @defer.inlineCallbacks def on_GET(self, request, room_id, session_id): @@ -239,10 +239,10 @@ class RoomKeysServlet(RestServlet): user_id = requester.user.to_string() version = parse_string(request, "version") - yield self.e2e_room_keys_handler.delete_room_keys( + ret = yield self.e2e_room_keys_handler.delete_room_keys( user_id, version, room_id, session_id ) - return 200, {} + return 200, ret class RoomKeysNewVersionServlet(RestServlet): diff --git a/synapse/storage/data_stores/main/e2e_room_keys.py b/synapse/storage/data_stores/main/e2e_room_keys.py index 1cbbae5b63..113224fd7c 100644 --- a/synapse/storage/data_stores/main/e2e_room_keys.py +++ b/synapse/storage/data_stores/main/e2e_room_keys.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # Copyright 2017 New Vector Ltd +# Copyright 2019 Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -24,49 +25,8 @@ from synapse.storage._base import SQLBaseStore class EndToEndRoomKeyStore(SQLBaseStore): @defer.inlineCallbacks - def get_e2e_room_key(self, user_id, version, room_id, session_id): - """Get the encrypted E2E room key for a given session from a given - backup version of room_keys. We only store the 'best' room key for a given - session at a given time, as determined by the handler. - - Args: - user_id(str): the user whose backup we're querying - version(str): the version ID of the backup for the set of keys we're querying - room_id(str): the ID of the room whose keys we're querying. - This is a bit redundant as it's implied by the session_id, but - we include for consistency with the rest of the API. - session_id(str): the session whose room_key we're querying. - - Returns: - A deferred dict giving the session_data and message metadata for - this room key. - """ - - row = yield self._simple_select_one( - table="e2e_room_keys", - keyvalues={ - "user_id": user_id, - "version": version, - "room_id": room_id, - "session_id": session_id, - }, - retcols=( - "first_message_index", - "forwarded_count", - "is_verified", - "session_data", - ), - desc="get_e2e_room_key", - ) - - row["session_data"] = json.loads(row["session_data"]) - - return row - - @defer.inlineCallbacks - def set_e2e_room_key(self, user_id, version, room_id, session_id, room_key): - """Replaces or inserts the encrypted E2E room key for a given session in - a given backup + def update_e2e_room_key(self, user_id, version, room_id, session_id, room_key): + """Replaces the encrypted E2E room key for a given session in a given backup Args: user_id(str): the user whose backup we're setting @@ -78,7 +38,7 @@ class EndToEndRoomKeyStore(SQLBaseStore): StoreError """ - yield self._simple_upsert( + yield self._simple_update_one( table="e2e_room_keys", keyvalues={ "user_id": user_id, @@ -86,21 +46,51 @@ class EndToEndRoomKeyStore(SQLBaseStore): "room_id": room_id, "session_id": session_id, }, - values={ + updatevalues={ "first_message_index": room_key["first_message_index"], "forwarded_count": room_key["forwarded_count"], "is_verified": room_key["is_verified"], "session_data": json.dumps(room_key["session_data"]), }, - lock=False, + desc="update_e2e_room_key", ) - log_kv( - { - "message": "Set room key", - "room_id": room_id, - "session_id": session_id, - "room_key": room_key, - } + + @defer.inlineCallbacks + def add_e2e_room_keys(self, user_id, version, room_keys): + """Bulk add room keys to a given backup. + + Args: + user_id (str): the user whose backup we're adding to + version (str): the version ID of the backup for the set of keys we're adding to + room_keys (iterable[(str, str, dict)]): the keys to add, in the form + (roomID, sessionID, keyData) + """ + + values = [] + for (room_id, session_id, room_key) in room_keys: + values.append( + { + "user_id": user_id, + "version": version, + "room_id": room_id, + "session_id": session_id, + "first_message_index": room_key["first_message_index"], + "forwarded_count": room_key["forwarded_count"], + "is_verified": room_key["is_verified"], + "session_data": json.dumps(room_key["session_data"]), + } + ) + log_kv( + { + "message": "Set room key", + "room_id": room_id, + "session_id": session_id, + "room_key": room_key, + } + ) + + yield self._simple_insert_many( + table="e2e_room_keys", values=values, desc="add_e2e_room_keys" ) @trace @@ -110,11 +100,11 @@ class EndToEndRoomKeyStore(SQLBaseStore): room, or a given session. Args: - user_id(str): the user whose backup we're querying - version(str): the version ID of the backup for the set of keys we're querying - room_id(str): Optional. the ID of the room whose keys we're querying, if any. + user_id (str): the user whose backup we're querying + version (str): the version ID of the backup for the set of keys we're querying + room_id (str): Optional. the ID of the room whose keys we're querying, if any. If not specified, we return the keys for all the rooms in the backup. - session_id(str): Optional. the session whose room_key we're querying, if any. + session_id (str): Optional. the session whose room_key we're querying, if any. If specified, we also require the room_id to be specified. If not specified, we return all the keys in this version of the backup (or for the specified room) @@ -162,6 +152,95 @@ class EndToEndRoomKeyStore(SQLBaseStore): return sessions + def get_e2e_room_keys_multi(self, user_id, version, room_keys): + """Get multiple room keys at a time. The difference between this function and + get_e2e_room_keys is that this function can be used to retrieve + multiple specific keys at a time, whereas get_e2e_room_keys is used for + getting all the keys in a backup version, all the keys for a room, or a + specific key. + + Args: + user_id (str): the user whose backup we're querying + version (str): the version ID of the backup we're querying about + room_keys (dict[str, dict[str, iterable[str]]]): a map from + room ID -> {"session": [session ids]} indicating the session IDs + that we want to query + + Returns: + Deferred[dict[str, dict[str, dict]]]: a map of room IDs to session IDs to room key + """ + + return self.runInteraction( + "get_e2e_room_keys_multi", + self._get_e2e_room_keys_multi_txn, + user_id, + version, + room_keys, + ) + + @staticmethod + def _get_e2e_room_keys_multi_txn(txn, user_id, version, room_keys): + if not room_keys: + return {} + + where_clauses = [] + params = [user_id, version] + for room_id, room in room_keys.items(): + sessions = list(room["sessions"]) + if not sessions: + continue + params.append(room_id) + params.extend(sessions) + where_clauses.append( + "(room_id = ? AND session_id IN (%s))" + % (",".join(["?" for _ in sessions]),) + ) + + # check if we're actually querying something + if not where_clauses: + return {} + + sql = """ + SELECT room_id, session_id, first_message_index, forwarded_count, + is_verified, session_data + FROM e2e_room_keys + WHERE user_id = ? AND version = ? AND (%s) + """ % ( + " OR ".join(where_clauses) + ) + + txn.execute(sql, params) + + ret = {} + + for row in txn: + room_id = row[0] + session_id = row[1] + ret.setdefault(room_id, {}) + ret[room_id][session_id] = { + "first_message_index": row[2], + "forwarded_count": row[3], + "is_verified": row[4], + "session_data": json.loads(row[5]), + } + + return ret + + def count_e2e_room_keys(self, user_id, version): + """Get the number of keys in a backup version. + + Args: + user_id (str): the user whose backup we're querying + version (str): the version ID of the backup we're querying about + """ + + return self._simple_select_one_onecol( + table="e2e_room_keys", + keyvalues={"user_id": user_id, "version": version}, + retcol="COUNT(*)", + desc="count_e2e_room_keys", + ) + @trace @defer.inlineCallbacks def delete_e2e_room_keys(self, user_id, version, room_id=None, session_id=None): @@ -219,6 +298,7 @@ class EndToEndRoomKeyStore(SQLBaseStore): version(str) algorithm(str) auth_data(object): opaque dict supplied by the client + etag(int): tag of the keys in the backup """ def _get_e2e_room_keys_version_info_txn(txn): @@ -236,10 +316,12 @@ class EndToEndRoomKeyStore(SQLBaseStore): txn, table="e2e_room_keys_versions", keyvalues={"user_id": user_id, "version": this_version, "deleted": 0}, - retcols=("version", "algorithm", "auth_data"), + retcols=("version", "algorithm", "auth_data", "etag"), ) result["auth_data"] = json.loads(result["auth_data"]) result["version"] = str(result["version"]) + if result["etag"] is None: + result["etag"] = 0 return result return self.runInteraction( @@ -288,21 +370,33 @@ class EndToEndRoomKeyStore(SQLBaseStore): ) @trace - def update_e2e_room_keys_version(self, user_id, version, info): + def update_e2e_room_keys_version( + self, user_id, version, info=None, version_etag=None + ): """Update a given backup version Args: user_id(str): the user whose backup version we're updating version(str): the version ID of the backup version we're updating - info(dict): the new backup version info to store + info (dict): the new backup version info to store. If None, then + the backup version info is not updated + version_etag (Optional[int]): etag of the keys in the backup. If + None, then the etag is not updated """ + updatevalues = {} - return self._simple_update( - table="e2e_room_keys_versions", - keyvalues={"user_id": user_id, "version": version}, - updatevalues={"auth_data": json.dumps(info["auth_data"])}, - desc="update_e2e_room_keys_version", - ) + if info is not None and "auth_data" in info: + updatevalues["auth_data"] = json.dumps(info["auth_data"]) + if version_etag is not None: + updatevalues["etag"] = version_etag + + if updatevalues: + return self._simple_update( + table="e2e_room_keys_versions", + keyvalues={"user_id": user_id, "version": version}, + updatevalues=updatevalues, + desc="update_e2e_room_keys_version", + ) @trace def delete_e2e_room_keys_version(self, user_id, version=None): diff --git a/synapse/storage/data_stores/main/schema/delta/56/room_key_etag.sql b/synapse/storage/data_stores/main/schema/delta/56/room_key_etag.sql new file mode 100644 index 0000000000..7d70dd071e --- /dev/null +++ b/synapse/storage/data_stores/main/schema/delta/56/room_key_etag.sql @@ -0,0 +1,17 @@ +/* Copyright 2019 Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +-- store the current etag of backup version +ALTER TABLE e2e_room_keys_versions ADD COLUMN etag BIGINT; diff --git a/tests/handlers/test_e2e_room_keys.py b/tests/handlers/test_e2e_room_keys.py index 0bb96674a2..70f172eb02 100644 --- a/tests/handlers/test_e2e_room_keys.py +++ b/tests/handlers/test_e2e_room_keys.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # Copyright 2017 New Vector Ltd +# Copyright 2019 Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -94,23 +95,29 @@ class E2eRoomKeysHandlerTestCase(unittest.TestCase): # check we can retrieve it as the current version res = yield self.handler.get_version_info(self.local_user) + version_etag = res["etag"] + del res["etag"] self.assertDictEqual( res, { "version": "1", "algorithm": "m.megolm_backup.v1", "auth_data": "first_version_auth_data", + "count": 0, }, ) # check we can retrieve it as a specific version res = yield self.handler.get_version_info(self.local_user, "1") + self.assertEqual(res["etag"], version_etag) + del res["etag"] self.assertDictEqual( res, { "version": "1", "algorithm": "m.megolm_backup.v1", "auth_data": "first_version_auth_data", + "count": 0, }, ) @@ -126,12 +133,14 @@ class E2eRoomKeysHandlerTestCase(unittest.TestCase): # check we can retrieve it as the current version res = yield self.handler.get_version_info(self.local_user) + del res["etag"] self.assertDictEqual( res, { "version": "2", "algorithm": "m.megolm_backup.v1", "auth_data": "second_version_auth_data", + "count": 0, }, ) @@ -158,12 +167,14 @@ class E2eRoomKeysHandlerTestCase(unittest.TestCase): # check we can retrieve it as the current version res = yield self.handler.get_version_info(self.local_user) + del res["etag"] self.assertDictEqual( res, { "algorithm": "m.megolm_backup.v1", "auth_data": "revised_first_version_auth_data", "version": version, + "count": 0, }, ) @@ -207,12 +218,14 @@ class E2eRoomKeysHandlerTestCase(unittest.TestCase): # check we can retrieve it as the current version res = yield self.handler.get_version_info(self.local_user) + del res["etag"] # etag is opaque, so don't test its contents self.assertDictEqual( res, { "algorithm": "m.megolm_backup.v1", "auth_data": "revised_first_version_auth_data", "version": version, + "count": 0, }, ) @@ -409,6 +422,11 @@ class E2eRoomKeysHandlerTestCase(unittest.TestCase): yield self.handler.upload_room_keys(self.local_user, version, room_keys) + # get the etag to compare to future versions + res = yield self.handler.get_version_info(self.local_user) + backup_etag = res["etag"] + self.assertEqual(res["count"], 1) + new_room_keys = copy.deepcopy(room_keys) new_room_key = new_room_keys["rooms"]["!abc:matrix.org"]["sessions"]["c0ff33"] @@ -423,6 +441,10 @@ class E2eRoomKeysHandlerTestCase(unittest.TestCase): "SSBBTSBBIEZJU0gK", ) + # the etag should be the same since the session did not change + res = yield self.handler.get_version_info(self.local_user) + self.assertEqual(res["etag"], backup_etag) + # test that marking the session as verified however /does/ replace it new_room_key["is_verified"] = True yield self.handler.upload_room_keys(self.local_user, version, new_room_keys) @@ -432,6 +454,11 @@ class E2eRoomKeysHandlerTestCase(unittest.TestCase): res["rooms"]["!abc:matrix.org"]["sessions"]["c0ff33"]["session_data"], "new" ) + # the etag should NOT be equal now, since the key changed + res = yield self.handler.get_version_info(self.local_user) + self.assertNotEqual(res["etag"], backup_etag) + backup_etag = res["etag"] + # test that a session with a higher forwarded_count doesn't replace one # with a lower forwarding count new_room_key["forwarded_count"] = 2 @@ -443,6 +470,10 @@ class E2eRoomKeysHandlerTestCase(unittest.TestCase): res["rooms"]["!abc:matrix.org"]["sessions"]["c0ff33"]["session_data"], "new" ) + # the etag should be the same since the session did not change + res = yield self.handler.get_version_info(self.local_user) + self.assertEqual(res["etag"], backup_etag) + # TODO: check edge cases as well as the common variations here @defer.inlineCallbacks diff --git a/tests/storage/test_e2e_room_keys.py b/tests/storage/test_e2e_room_keys.py index d128fde441..35dafbb904 100644 --- a/tests/storage/test_e2e_room_keys.py +++ b/tests/storage/test_e2e_room_keys.py @@ -39,8 +39,8 @@ class E2eRoomKeysHandlerTestCase(unittest.HomeserverTestCase): ) self.get_success( - self.store.set_e2e_room_key( - "user_id", version1, "room", "session", room_key + self.store.add_e2e_room_keys( + "user_id", version1, [("room", "session", room_key)] ) ) @@ -51,8 +51,8 @@ class E2eRoomKeysHandlerTestCase(unittest.HomeserverTestCase): ) self.get_success( - self.store.set_e2e_room_key( - "user_id", version2, "room", "session", room_key + self.store.add_e2e_room_keys( + "user_id", version2, [("room", "session", room_key)] ) ) From 0f87b912aba7e678041632bc9a6d1f7c2d24342c Mon Sep 17 00:00:00 2001 From: Amber Brown Date: Thu, 28 Nov 2019 08:54:07 +1100 Subject: [PATCH 0548/1623] Implementation of MSC2314 (#6176) --- changelog.d/6176.feature | 1 + synapse/federation/federation_server.py | 26 ++++++--- synapse/federation/transport/server.py | 6 +- sytest-blacklist | 6 +- tests/federation/test_complexity.py | 28 +-------- tests/federation/test_federation_sender.py | 4 +- tests/federation/test_federation_server.py | 63 ++++++++++++++++++++ tests/handlers/test_typing.py | 3 + tests/replication/slave/storage/_base.py | 3 + tests/replication/tcp/streams/_base.py | 4 ++ tests/storage/test_roommember.py | 26 +-------- tests/unittest.py | 68 +++++++++++++++++++++- tests/utils.py | 1 + 13 files changed, 174 insertions(+), 65 deletions(-) create mode 100644 changelog.d/6176.feature diff --git a/changelog.d/6176.feature b/changelog.d/6176.feature new file mode 100644 index 0000000000..3c66d689d4 --- /dev/null +++ b/changelog.d/6176.feature @@ -0,0 +1 @@ +Implement the `/_matrix/federation/unstable/net.atleastfornow/state/` API as drafted in MSC2314. diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index d942d77a72..84d4eca041 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd +# Copyright 2019 Matrix.org Federation C.I.C # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -73,6 +74,7 @@ class FederationServer(FederationBase): self.auth = hs.get_auth() self.handler = hs.get_handlers().federation_handler + self.state = hs.get_state_handler() self._server_linearizer = Linearizer("fed_server") self._transaction_linearizer = Linearizer("fed_txn_handler") @@ -264,9 +266,6 @@ class FederationServer(FederationBase): await self.registry.on_edu(edu_type, origin, content) async def on_context_state_request(self, origin, room_id, event_id): - if not event_id: - raise NotImplementedError("Specify an event") - origin_host, _ = parse_server_name(origin) await self.check_server_matches_acl(origin_host, room_id) @@ -280,13 +279,18 @@ class FederationServer(FederationBase): # - but that's non-trivial to get right, and anyway somewhat defeats # the point of the linearizer. with (await self._server_linearizer.queue((origin, room_id))): - resp = await self._state_resp_cache.wrap( - (room_id, event_id), - self._on_context_state_request_compute, - room_id, - event_id, + resp = dict( + await self._state_resp_cache.wrap( + (room_id, event_id), + self._on_context_state_request_compute, + room_id, + event_id, + ) ) + room_version = await self.store.get_room_version(room_id) + resp["room_version"] = room_version + return 200, resp async def on_state_ids_request(self, origin, room_id, event_id): @@ -306,7 +310,11 @@ class FederationServer(FederationBase): return 200, {"pdu_ids": state_ids, "auth_chain_ids": auth_chain_ids} async def _on_context_state_request_compute(self, room_id, event_id): - pdus = await self.handler.get_state_for_pdu(room_id, event_id) + if event_id: + pdus = await self.handler.get_state_for_pdu(room_id, event_id) + else: + pdus = (await self.state.get_current_state(room_id)).values() + auth_chain = await self.store.get_auth_chain([pdu.event_id for pdu in pdus]) return { diff --git a/synapse/federation/transport/server.py b/synapse/federation/transport/server.py index 09baa9c57d..fefc789c85 100644 --- a/synapse/federation/transport/server.py +++ b/synapse/federation/transport/server.py @@ -421,7 +421,7 @@ class FederationEventServlet(BaseFederationServlet): return await self.handler.on_pdu_request(origin, event_id) -class FederationStateServlet(BaseFederationServlet): +class FederationStateV1Servlet(BaseFederationServlet): PATH = "/state/(?P[^/]*)/?" # This is when someone asks for all data for a given context. @@ -429,7 +429,7 @@ class FederationStateServlet(BaseFederationServlet): return await self.handler.on_context_state_request( origin, context, - parse_string_from_args(query, "event_id", None, required=True), + parse_string_from_args(query, "event_id", None, required=False), ) @@ -1360,7 +1360,7 @@ class RoomComplexityServlet(BaseFederationServlet): FEDERATION_SERVLET_CLASSES = ( FederationSendServlet, FederationEventServlet, - FederationStateServlet, + FederationStateV1Servlet, FederationStateIdsServlet, FederationBackfillServlet, FederationQueryServlet, diff --git a/sytest-blacklist b/sytest-blacklist index 11785fd43f..411cce0692 100644 --- a/sytest-blacklist +++ b/sytest-blacklist @@ -1,6 +1,6 @@ # This file serves as a blacklist for SyTest tests that we expect will fail in # Synapse. -# +# # Each line of this file is scanned by sytest during a run and if the line # exactly matches the name of a test, it will be marked as "expected fail", # meaning the test will still run, but failure will not mark the entire test @@ -29,3 +29,7 @@ Enabling an unknown default rule fails with 404 # Blacklisted due to https://github.com/matrix-org/synapse/issues/1663 New federated private chats get full presence information (SYN-115) + +# Blacklisted due to https://github.com/matrix-org/matrix-doc/pull/2314 removing +# this requirement from the spec +Inbound federation of state requires event_id as a mandatory paramater diff --git a/tests/federation/test_complexity.py b/tests/federation/test_complexity.py index 51714a2b06..24fa8dbb45 100644 --- a/tests/federation/test_complexity.py +++ b/tests/federation/test_complexity.py @@ -18,17 +18,14 @@ from mock import Mock from twisted.internet import defer from synapse.api.errors import Codes, SynapseError -from synapse.config.ratelimiting import FederationRateLimitConfig -from synapse.federation.transport import server from synapse.rest import admin from synapse.rest.client.v1 import login, room from synapse.types import UserID -from synapse.util.ratelimitutils import FederationRateLimiter from tests import unittest -class RoomComplexityTests(unittest.HomeserverTestCase): +class RoomComplexityTests(unittest.FederatingHomeserverTestCase): servlets = [ admin.register_servlets, @@ -41,25 +38,6 @@ class RoomComplexityTests(unittest.HomeserverTestCase): config["limit_remote_rooms"] = {"enabled": True, "complexity": 0.05} return config - def prepare(self, reactor, clock, homeserver): - class Authenticator(object): - def authenticate_request(self, request, content): - return defer.succeed("otherserver.nottld") - - ratelimiter = FederationRateLimiter( - clock, - FederationRateLimitConfig( - window_size=1, - sleep_limit=1, - sleep_msec=1, - reject_limit=1000, - concurrent_requests=1000, - ), - ) - server.register_servlets( - homeserver, self.resource, Authenticator(), ratelimiter - ) - def test_complexity_simple(self): u1 = self.register_user("u1", "pass") @@ -105,7 +83,7 @@ class RoomComplexityTests(unittest.HomeserverTestCase): d = handler._remote_join( None, - ["otherserver.example"], + ["other.example.com"], "roomid", UserID.from_string(u1), {"membership": "join"}, @@ -146,7 +124,7 @@ class RoomComplexityTests(unittest.HomeserverTestCase): d = handler._remote_join( None, - ["otherserver.example"], + ["other.example.com"], room_1, UserID.from_string(u1), {"membership": "join"}, diff --git a/tests/federation/test_federation_sender.py b/tests/federation/test_federation_sender.py index cce8d8c6de..d456267b87 100644 --- a/tests/federation/test_federation_sender.py +++ b/tests/federation/test_federation_sender.py @@ -19,7 +19,7 @@ from twisted.internet import defer from synapse.types import ReadReceipt -from tests.unittest import HomeserverTestCase +from tests.unittest import HomeserverTestCase, override_config class FederationSenderTestCases(HomeserverTestCase): @@ -29,6 +29,7 @@ class FederationSenderTestCases(HomeserverTestCase): federation_transport_client=Mock(spec=["send_transaction"]), ) + @override_config({"send_federation": True}) def test_send_receipts(self): mock_state_handler = self.hs.get_state_handler() mock_state_handler.get_current_hosts_in_room.return_value = ["test", "host2"] @@ -69,6 +70,7 @@ class FederationSenderTestCases(HomeserverTestCase): ], ) + @override_config({"send_federation": True}) def test_send_receipts_with_backoff(self): """Send two receipts in quick succession; the second should be flushed, but only after 20ms""" diff --git a/tests/federation/test_federation_server.py b/tests/federation/test_federation_server.py index b08be451aa..1ec8c40901 100644 --- a/tests/federation/test_federation_server.py +++ b/tests/federation/test_federation_server.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd +# Copyright 2019 Matrix.org Federation C.I.C # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,6 +17,8 @@ import logging from synapse.events import FrozenEvent from synapse.federation.federation_server import server_matches_acl_event +from synapse.rest import admin +from synapse.rest.client.v1 import login, room from tests import unittest @@ -41,6 +44,66 @@ class ServerACLsTestCase(unittest.TestCase): self.assertTrue(server_matches_acl_event("1:2:3:4", e)) +class StateQueryTests(unittest.FederatingHomeserverTestCase): + + servlets = [ + admin.register_servlets, + room.register_servlets, + login.register_servlets, + ] + + def test_without_event_id(self): + """ + Querying v1/state/ without an event ID will return the current + known state. + """ + u1 = self.register_user("u1", "pass") + u1_token = self.login("u1", "pass") + + room_1 = self.helper.create_room_as(u1, tok=u1_token) + self.inject_room_member(room_1, "@user:other.example.com", "join") + + request, channel = self.make_request( + "GET", "/_matrix/federation/v1/state/%s" % (room_1,) + ) + self.render(request) + self.assertEquals(200, channel.code, channel.result) + + self.assertEqual( + channel.json_body["room_version"], + self.hs.config.default_room_version.identifier, + ) + + members = set( + map( + lambda x: x["state_key"], + filter( + lambda x: x["type"] == "m.room.member", channel.json_body["pdus"] + ), + ) + ) + + self.assertEqual(members, set(["@user:other.example.com", u1])) + self.assertEqual(len(channel.json_body["pdus"]), 6) + + def test_needs_to_be_in_room(self): + """ + Querying v1/state/ requires the server + be in the room to provide data. + """ + u1 = self.register_user("u1", "pass") + u1_token = self.login("u1", "pass") + + room_1 = self.helper.create_room_as(u1, tok=u1_token) + + request, channel = self.make_request( + "GET", "/_matrix/federation/v1/state/%s" % (room_1,) + ) + self.render(request) + self.assertEquals(403, channel.code, channel.result) + self.assertEqual(channel.json_body["errcode"], "M_FORBIDDEN") + + def _create_acl_event(content): return FrozenEvent( { diff --git a/tests/handlers/test_typing.py b/tests/handlers/test_typing.py index 5ec568f4e6..f6d8660285 100644 --- a/tests/handlers/test_typing.py +++ b/tests/handlers/test_typing.py @@ -24,6 +24,7 @@ from synapse.api.errors import AuthError from synapse.types import UserID from tests import unittest +from tests.unittest import override_config from tests.utils import register_federation_servlets # Some local users to test with @@ -174,6 +175,7 @@ class TypingNotificationsTestCase(unittest.HomeserverTestCase): ], ) + @override_config({"send_federation": True}) def test_started_typing_remote_send(self): self.room_members = [U_APPLE, U_ONION] @@ -237,6 +239,7 @@ class TypingNotificationsTestCase(unittest.HomeserverTestCase): ], ) + @override_config({"send_federation": True}) def test_stopped_typing(self): self.room_members = [U_APPLE, U_BANANA, U_ONION] diff --git a/tests/replication/slave/storage/_base.py b/tests/replication/slave/storage/_base.py index 4f924ce451..e7472e3a93 100644 --- a/tests/replication/slave/storage/_base.py +++ b/tests/replication/slave/storage/_base.py @@ -48,7 +48,10 @@ class BaseSlavedStoreTestCase(unittest.HomeserverTestCase): server_factory = ReplicationStreamProtocolFactory(self.hs) self.streamer = server_factory.streamer + handler_factory = Mock() self.replication_handler = ReplicationClientHandler(self.slaved_store) + self.replication_handler.factory = handler_factory + client_factory = ReplicationClientFactory( self.hs, "client_name", self.replication_handler ) diff --git a/tests/replication/tcp/streams/_base.py b/tests/replication/tcp/streams/_base.py index ce3835ae6a..1d14e77255 100644 --- a/tests/replication/tcp/streams/_base.py +++ b/tests/replication/tcp/streams/_base.py @@ -12,6 +12,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from mock import Mock + from synapse.replication.tcp.commands import ReplicateCommand from synapse.replication.tcp.protocol import ClientReplicationStreamProtocol from synapse.replication.tcp.resource import ReplicationStreamProtocolFactory @@ -30,7 +32,9 @@ class BaseStreamTestCase(unittest.HomeserverTestCase): server = server_factory.buildProtocol(None) # build a replication client, with a dummy handler + handler_factory = Mock() self.test_handler = TestReplicationClientHandler() + self.test_handler.factory = handler_factory self.client = ClientReplicationStreamProtocol( "client", "test", clock, self.test_handler ) diff --git a/tests/storage/test_roommember.py b/tests/storage/test_roommember.py index 9ddd17f73d..105a0c2b02 100644 --- a/tests/storage/test_roommember.py +++ b/tests/storage/test_roommember.py @@ -16,8 +16,7 @@ from unittest.mock import Mock -from synapse.api.constants import EventTypes, Membership -from synapse.api.room_versions import RoomVersions +from synapse.api.constants import Membership from synapse.rest.admin import register_servlets_for_client_rest_resource from synapse.rest.client.v1 import login, room from synapse.types import Requester, UserID @@ -44,9 +43,6 @@ class RoomMemberStoreTestCase(unittest.HomeserverTestCase): # We can't test the RoomMemberStore on its own without the other event # storage logic self.store = hs.get_datastore() - self.storage = hs.get_storage() - self.event_builder_factory = hs.get_event_builder_factory() - self.event_creation_handler = hs.get_event_creation_handler() self.u_alice = self.register_user("alice", "pass") self.t_alice = self.login("alice", "pass") @@ -55,26 +51,6 @@ class RoomMemberStoreTestCase(unittest.HomeserverTestCase): # User elsewhere on another host self.u_charlie = UserID.from_string("@charlie:elsewhere") - def inject_room_member(self, room, user, membership, replaces_state=None): - builder = self.event_builder_factory.for_room_version( - RoomVersions.V1, - { - "type": EventTypes.Member, - "sender": user, - "state_key": user, - "room_id": room, - "content": {"membership": membership}, - }, - ) - - event, context = self.get_success( - self.event_creation_handler.create_new_client_event(builder) - ) - - self.get_success(self.storage.persistence.persist_event(event, context)) - - return event - def test_one_member(self): # Alice creates the room, and is automatically joined diff --git a/tests/unittest.py b/tests/unittest.py index 561cebc223..31997a0f31 100644 --- a/tests/unittest.py +++ b/tests/unittest.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2018 New Vector +# Copyright 2019 Matrix.org Federation C.I.C # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,6 +14,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + import gc import hashlib import hmac @@ -27,13 +29,17 @@ from twisted.internet.defer import Deferred, succeed from twisted.python.threadpool import ThreadPool from twisted.trial import unittest -from synapse.api.constants import EventTypes +from synapse.api.constants import EventTypes, Membership +from synapse.api.room_versions import KNOWN_ROOM_VERSIONS from synapse.config.homeserver import HomeServerConfig +from synapse.config.ratelimiting import FederationRateLimitConfig +from synapse.federation.transport import server as federation_server from synapse.http.server import JsonResource from synapse.http.site import SynapseRequest from synapse.logging.context import LoggingContext from synapse.server import HomeServer from synapse.types import Requester, UserID, create_requester +from synapse.util.ratelimitutils import FederationRateLimiter from tests.server import get_clock, make_request, render, setup_test_homeserver from tests.test_utils.logging_setup import setup_logging @@ -559,6 +565,66 @@ class HomeserverTestCase(TestCase): self.render(request) self.assertEqual(channel.code, 403, channel.result) + def inject_room_member(self, room: str, user: str, membership: Membership) -> None: + """ + Inject a membership event into a room. + + Args: + room: Room ID to inject the event into. + user: MXID of the user to inject the membership for. + membership: The membership type. + """ + event_builder_factory = self.hs.get_event_builder_factory() + event_creation_handler = self.hs.get_event_creation_handler() + + room_version = self.get_success(self.hs.get_datastore().get_room_version(room)) + + builder = event_builder_factory.for_room_version( + KNOWN_ROOM_VERSIONS[room_version], + { + "type": EventTypes.Member, + "sender": user, + "state_key": user, + "room_id": room, + "content": {"membership": membership}, + }, + ) + + event, context = self.get_success( + event_creation_handler.create_new_client_event(builder) + ) + + self.get_success( + self.hs.get_storage().persistence.persist_event(event, context) + ) + + +class FederatingHomeserverTestCase(HomeserverTestCase): + """ + A federating homeserver that authenticates incoming requests as `other.example.com`. + """ + + def prepare(self, reactor, clock, homeserver): + class Authenticator(object): + def authenticate_request(self, request, content): + return succeed("other.example.com") + + ratelimiter = FederationRateLimiter( + clock, + FederationRateLimitConfig( + window_size=1, + sleep_limit=1, + sleep_msec=1, + reject_limit=1000, + concurrent_requests=1000, + ), + ) + federation_server.register_servlets( + homeserver, self.resource, Authenticator(), ratelimiter + ) + + return super().prepare(reactor, clock, homeserver) + def override_config(extra_config): """A decorator which can be applied to test functions to give additional HS config diff --git a/tests/utils.py b/tests/utils.py index 7dc9bdc505..de2ac1ed33 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -109,6 +109,7 @@ def default_config(name, parse=False): """ config_dict = { "server_name": name, + "send_federation": False, "media_store_path": "media", "uploads_path": "uploads", # the test signing key is just an arbitrary ed25519 key to keep the config From c48ea9800769c22d763cd97ecb137141050739e1 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 28 Nov 2019 09:29:18 +0000 Subject: [PATCH 0549/1623] Clarifications for the email configuration settings. (#6423) Cf #6422 --- changelog.d/6423.misc | 1 + docs/sample_config.yaml | 17 ++++++++++++++++- synapse/config/emailconfig.py | 17 ++++++++++++++++- 3 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 changelog.d/6423.misc diff --git a/changelog.d/6423.misc b/changelog.d/6423.misc new file mode 100644 index 0000000000..9bcd5d36c1 --- /dev/null +++ b/changelog.d/6423.misc @@ -0,0 +1 @@ +Clarifications for the email configuration settings. diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 09dd21352f..c7391f0c48 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -1333,8 +1333,23 @@ password_config: # smtp_user: "exampleusername" # smtp_pass: "examplepassword" # require_transport_security: false +# +# # notif_from defines the "From" address to use when sending emails. +# # It must be set if email sending is enabled. +# # +# # The placeholder '%(app)s' will be replaced by the application name, +# # which is normally 'app_name' (below), but may be overridden by the +# # Matrix client application. +# # +# # Note that the placeholder must be written '%(app)s', including the +# # trailing 's'. +# # # notif_from: "Your Friendly %(app)s homeserver " -# app_name: Matrix +# +# # app_name defines the default value for '%(app)s' in notif_from. It +# # defaults to 'Matrix'. +# # +# #app_name: my_branded_matrix_server # # # Enable email notifications by default # # diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py index ac1724045f..18f42a87f9 100644 --- a/synapse/config/emailconfig.py +++ b/synapse/config/emailconfig.py @@ -307,8 +307,23 @@ class EmailConfig(Config): # smtp_user: "exampleusername" # smtp_pass: "examplepassword" # require_transport_security: false + # + # # notif_from defines the "From" address to use when sending emails. + # # It must be set if email sending is enabled. + # # + # # The placeholder '%(app)s' will be replaced by the application name, + # # which is normally 'app_name' (below), but may be overridden by the + # # Matrix client application. + # # + # # Note that the placeholder must be written '%(app)s', including the + # # trailing 's'. + # # # notif_from: "Your Friendly %(app)s homeserver " - # app_name: Matrix + # + # # app_name defines the default value for '%(app)s' in notif_from. It + # # defaults to 'Matrix'. + # # + # #app_name: my_branded_matrix_server # # # Enable email notifications by default # # From a9c44d4008deb29503e2de00e5aae1a56a72d630 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Thu, 28 Nov 2019 10:40:42 +0000 Subject: [PATCH 0550/1623] Remove local threepids on account deactivation (#6426) --- changelog.d/6426.bugfix | 1 + synapse/handlers/deactivate_account.py | 3 +++ synapse/storage/data_stores/main/registration.py | 13 +++++++++++++ 3 files changed, 17 insertions(+) create mode 100644 changelog.d/6426.bugfix diff --git a/changelog.d/6426.bugfix b/changelog.d/6426.bugfix new file mode 100644 index 0000000000..3acfde4211 --- /dev/null +++ b/changelog.d/6426.bugfix @@ -0,0 +1 @@ +Clean up local threepids from user on account deactivation. \ No newline at end of file diff --git a/synapse/handlers/deactivate_account.py b/synapse/handlers/deactivate_account.py index 63267a0a4c..6dedaaff8d 100644 --- a/synapse/handlers/deactivate_account.py +++ b/synapse/handlers/deactivate_account.py @@ -95,6 +95,9 @@ class DeactivateAccountHandler(BaseHandler): user_id, threepid["medium"], threepid["address"] ) + # Remove all 3PIDs this user has bound to the homeserver + yield self.store.user_delete_threepids(user_id) + # delete any devices belonging to the user, which will also # delete corresponding access tokens. yield self._device_handler.delete_all_devices_for_user(user_id) diff --git a/synapse/storage/data_stores/main/registration.py b/synapse/storage/data_stores/main/registration.py index 0a3c1f0510..98cf6427c3 100644 --- a/synapse/storage/data_stores/main/registration.py +++ b/synapse/storage/data_stores/main/registration.py @@ -569,6 +569,19 @@ class RegistrationWorkerStore(SQLBaseStore): return self._simple_delete( "user_threepids", keyvalues={"user_id": user_id, "medium": medium, "address": address}, + desc="user_delete_threepid", + ) + + def user_delete_threepids(self, user_id: str): + """Delete all threepid this user has bound + + Args: + user_id: The user id to delete all threepids of + + """ + return self._simple_delete( + "user_threepids", + keyvalues={"user_id": user_id}, desc="user_delete_threepids", ) From 69d8fb83c6da35d7e1f04fa3afba0fd5406bd9d9 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 28 Nov 2019 11:02:04 +0000 Subject: [PATCH 0551/1623] MSC2367 Allow reason field on all member events --- synapse/rest/client/v1/room.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index 86bbcc0eea..711d4ad304 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -714,7 +714,7 @@ class RoomMembershipRestServlet(TransactionRestServlet): target = UserID.from_string(content["user_id"]) event_content = None - if "reason" in content and membership_action in ["kick", "ban"]: + if "reason" in content: event_content = {"reason": content["reason"]} await self.room_member_handler.update_membership( From 2030193e5523810c4fd6158f97b7a223cee4cb72 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Thu, 28 Nov 2019 10:40:42 +0000 Subject: [PATCH 0552/1623] Remove local threepids on account deactivation (#6426) --- changelog.d/6426.bugfix | 1 + synapse/handlers/deactivate_account.py | 3 +++ synapse/storage/data_stores/main/registration.py | 13 +++++++++++++ 3 files changed, 17 insertions(+) create mode 100644 changelog.d/6426.bugfix diff --git a/changelog.d/6426.bugfix b/changelog.d/6426.bugfix new file mode 100644 index 0000000000..3acfde4211 --- /dev/null +++ b/changelog.d/6426.bugfix @@ -0,0 +1 @@ +Clean up local threepids from user on account deactivation. \ No newline at end of file diff --git a/synapse/handlers/deactivate_account.py b/synapse/handlers/deactivate_account.py index 63267a0a4c..6dedaaff8d 100644 --- a/synapse/handlers/deactivate_account.py +++ b/synapse/handlers/deactivate_account.py @@ -95,6 +95,9 @@ class DeactivateAccountHandler(BaseHandler): user_id, threepid["medium"], threepid["address"] ) + # Remove all 3PIDs this user has bound to the homeserver + yield self.store.user_delete_threepids(user_id) + # delete any devices belonging to the user, which will also # delete corresponding access tokens. yield self._device_handler.delete_all_devices_for_user(user_id) diff --git a/synapse/storage/data_stores/main/registration.py b/synapse/storage/data_stores/main/registration.py index ee1b2b2bbf..89147ad511 100644 --- a/synapse/storage/data_stores/main/registration.py +++ b/synapse/storage/data_stores/main/registration.py @@ -577,6 +577,19 @@ class RegistrationWorkerStore(SQLBaseStore): return self._simple_delete( "user_threepids", keyvalues={"user_id": user_id, "medium": medium, "address": address}, + desc="user_delete_threepid", + ) + + def user_delete_threepids(self, user_id: str): + """Delete all threepid this user has bound + + Args: + user_id: The user id to delete all threepids of + + """ + return self._simple_delete( + "user_threepids", + keyvalues={"user_id": user_id}, desc="user_delete_threepids", ) From e7777f3668d09c87335830f785f42c851827b497 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Thu, 28 Nov 2019 11:24:11 +0000 Subject: [PATCH 0553/1623] 1.6.1 --- CHANGES.md | 15 +++++++++++++++ changelog.d/6421.bugfix | 1 - changelog.d/6426.bugfix | 1 - debian/changelog | 6 ++++++ synapse/__init__.py | 2 +- 5 files changed, 22 insertions(+), 3 deletions(-) delete mode 100644 changelog.d/6421.bugfix delete mode 100644 changelog.d/6426.bugfix diff --git a/CHANGES.md b/CHANGES.md index 42281483b3..a9afd36d2c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,18 @@ +Synapse 1.6.1 (2019-11-28) +========================== + +Security updates +---------------- + +This release includes a security fix ([\#6426](https://github.com/matrix-org/synapse/issues/6426), below). Administrators are encouraged to upgrade as soon as possible. + +Bugfixes +-------- + +- Clean up local threepids from user on account deactivation. ([\#6426](https://github.com/matrix-org/synapse/issues/6426)) +- Fix startup error when http proxy is defined. ([\#6421](https://github.com/matrix-org/synapse/issues/6421)) + + Synapse 1.6.0 (2019-11-26) ========================== diff --git a/changelog.d/6421.bugfix b/changelog.d/6421.bugfix deleted file mode 100644 index 7969f7f71d..0000000000 --- a/changelog.d/6421.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix startup error when http proxy is defined. diff --git a/changelog.d/6426.bugfix b/changelog.d/6426.bugfix deleted file mode 100644 index 3acfde4211..0000000000 --- a/changelog.d/6426.bugfix +++ /dev/null @@ -1 +0,0 @@ -Clean up local threepids from user on account deactivation. \ No newline at end of file diff --git a/debian/changelog b/debian/changelog index 82dae017f1..b8a43788ef 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.6.1) stable; urgency=medium + + * New synapse release 1.6.1. + + -- Synapse Packaging team Thu, 28 Nov 2019 11:10:40 +0000 + matrix-synapse-py3 (1.6.0) stable; urgency=medium * New synapse release 1.6.0. diff --git a/synapse/__init__.py b/synapse/__init__.py index 53eedc0048..f99de2f3f3 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -36,7 +36,7 @@ try: except ImportError: pass -__version__ = "1.6.0" +__version__ = "1.6.1" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 2173785f0d9124037ca841b568349ad0424b39cd Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 28 Nov 2019 11:31:56 +0000 Subject: [PATCH 0554/1623] Propagate reason in remotely rejected invites --- synapse/handlers/federation.py | 4 ++-- synapse/handlers/room_member.py | 13 +++++++++---- synapse/handlers/room_member_worker.py | 5 ++++- synapse/replication/http/membership.py | 7 +++++-- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index a5ae7b77d1..d3267734f7 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1428,9 +1428,9 @@ class FederationHandler(BaseHandler): return event @defer.inlineCallbacks - def do_remotely_reject_invite(self, target_hosts, room_id, user_id): + def do_remotely_reject_invite(self, target_hosts, room_id, user_id, content): origin, event, event_format_version = yield self._make_and_verify_event( - target_hosts, room_id, user_id, "leave" + target_hosts, room_id, user_id, "leave", content=content, ) # Mark as outlier as we don't have any state for this event; we're not # even in the room. diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index 6cfee4b361..7b7270fc61 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -94,7 +94,9 @@ class RoomMemberHandler(object): raise NotImplementedError() @abc.abstractmethod - def _remote_reject_invite(self, requester, remote_room_hosts, room_id, target): + def _remote_reject_invite( + self, requester, remote_room_hosts, room_id, target, content + ): """Attempt to reject an invite for a room this server is not in. If we fail to do so we locally mark the invite as rejected. @@ -104,6 +106,7 @@ class RoomMemberHandler(object): reject invite room_id (str) target (UserID): The user rejecting the invite + content (dict): The content for the rejection event Returns: Deferred[dict]: A dictionary to be returned to the client, may @@ -471,7 +474,7 @@ class RoomMemberHandler(object): # send the rejection to the inviter's HS. remote_room_hosts = remote_room_hosts + [inviter.domain] res = yield self._remote_reject_invite( - requester, remote_room_hosts, room_id, target + requester, remote_room_hosts, room_id, target, content, ) return res @@ -971,13 +974,15 @@ class RoomMemberMasterHandler(RoomMemberHandler): ) @defer.inlineCallbacks - def _remote_reject_invite(self, requester, remote_room_hosts, room_id, target): + def _remote_reject_invite( + self, requester, remote_room_hosts, room_id, target, content + ): """Implements RoomMemberHandler._remote_reject_invite """ fed_handler = self.federation_handler try: ret = yield fed_handler.do_remotely_reject_invite( - remote_room_hosts, room_id, target.to_string() + remote_room_hosts, room_id, target.to_string(), content=content, ) return ret except Exception as e: diff --git a/synapse/handlers/room_member_worker.py b/synapse/handlers/room_member_worker.py index 75e96ae1a2..69be86893b 100644 --- a/synapse/handlers/room_member_worker.py +++ b/synapse/handlers/room_member_worker.py @@ -55,7 +55,9 @@ class RoomMemberWorkerHandler(RoomMemberHandler): return ret - def _remote_reject_invite(self, requester, remote_room_hosts, room_id, target): + def _remote_reject_invite( + self, requester, remote_room_hosts, room_id, target, content + ): """Implements RoomMemberHandler._remote_reject_invite """ return self._remote_reject_client( @@ -63,6 +65,7 @@ class RoomMemberWorkerHandler(RoomMemberHandler): remote_room_hosts=remote_room_hosts, room_id=room_id, user_id=target.to_string(), + content=content, ) def _user_joined_room(self, target, room_id): diff --git a/synapse/replication/http/membership.py b/synapse/replication/http/membership.py index cc1f249740..3577611fd7 100644 --- a/synapse/replication/http/membership.py +++ b/synapse/replication/http/membership.py @@ -93,6 +93,7 @@ class ReplicationRemoteRejectInviteRestServlet(ReplicationEndpoint): { "requester": ..., "remote_room_hosts": [...], + "content": { ... } } """ @@ -107,7 +108,7 @@ class ReplicationRemoteRejectInviteRestServlet(ReplicationEndpoint): self.clock = hs.get_clock() @staticmethod - def _serialize_payload(requester, room_id, user_id, remote_room_hosts): + def _serialize_payload(requester, room_id, user_id, remote_room_hosts, content): """ Args: requester(Requester) @@ -118,12 +119,14 @@ class ReplicationRemoteRejectInviteRestServlet(ReplicationEndpoint): return { "requester": requester.serialize(), "remote_room_hosts": remote_room_hosts, + "content": content, } async def _handle_request(self, request, room_id, user_id): content = parse_json_object_from_request(request) remote_room_hosts = content["remote_room_hosts"] + event_content = content["content"] requester = Requester.deserialize(self.store, content["requester"]) @@ -134,7 +137,7 @@ class ReplicationRemoteRejectInviteRestServlet(ReplicationEndpoint): try: event = await self.federation_handler.do_remotely_reject_invite( - remote_room_hosts, room_id, user_id + remote_room_hosts, room_id, user_id, event_content, ) ret = event.get_pdu_json() except Exception as e: From 8c9a713f8db1d6fcc1f876ac6fbd0e54b5e5819c Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 28 Nov 2019 11:32:06 +0000 Subject: [PATCH 0555/1623] Add tests --- tests/rest/client/v1/test_rooms.py | 140 +++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py index e84e578f99..eda2fabc71 100644 --- a/tests/rest/client/v1/test_rooms.py +++ b/tests/rest/client/v1/test_rooms.py @@ -1180,3 +1180,143 @@ class PerRoomProfilesForbiddenTestCase(unittest.HomeserverTestCase): res_displayname = channel.json_body["content"]["displayname"] self.assertEqual(res_displayname, self.displayname, channel.result) + + +class RoomMembershipReasonTestCase(unittest.HomeserverTestCase): + """Tests that clients can add a "reason" field to membership events and + that they get correctly added to the generated events and propagated. + """ + + servlets = [ + synapse.rest.admin.register_servlets_for_client_rest_resource, + room.register_servlets, + login.register_servlets, + ] + + def prepare(self, reactor, clock, homeserver): + self.creator = self.register_user("creator", "test") + self.creator_tok = self.login("creator", "test") + + self.second_user_id = self.register_user("second", "test") + self.second_tok = self.login("second", "test") + + self.room_id = self.helper.create_room_as(self.creator, tok=self.creator_tok) + + def test_join_reason(self): + reason = "hello" + request, channel = self.make_request( + "POST", + "/_matrix/client/r0/rooms/{}/join".format(self.room_id), + content={"reason": reason}, + access_token=self.second_tok, + ) + self.render(request) + self.assertEqual(channel.code, 200, channel.result) + + self._check_for_reason(reason) + + def test_leave_reason(self): + self.helper.join(self.room_id, user=self.second_user_id, tok=self.second_tok) + + reason = "hello" + request, channel = self.make_request( + "POST", + "/_matrix/client/r0/rooms/{}/leave".format(self.room_id), + content={"reason": reason}, + access_token=self.second_tok, + ) + self.render(request) + self.assertEqual(channel.code, 200, channel.result) + + self._check_for_reason(reason) + + def test_kick_reason(self): + self.helper.join(self.room_id, user=self.second_user_id, tok=self.second_tok) + + reason = "hello" + request, channel = self.make_request( + "POST", + "/_matrix/client/r0/rooms/{}/kick".format(self.room_id), + content={"reason": reason, "user_id": self.second_user_id}, + access_token=self.second_tok, + ) + self.render(request) + self.assertEqual(channel.code, 200, channel.result) + + self._check_for_reason(reason) + + def test_ban_reason(self): + self.helper.join(self.room_id, user=self.second_user_id, tok=self.second_tok) + + reason = "hello" + request, channel = self.make_request( + "POST", + "/_matrix/client/r0/rooms/{}/ban".format(self.room_id), + content={"reason": reason, "user_id": self.second_user_id}, + access_token=self.creator_tok, + ) + self.render(request) + self.assertEqual(channel.code, 200, channel.result) + + self._check_for_reason(reason) + + def test_unban_reason(self): + reason = "hello" + request, channel = self.make_request( + "POST", + "/_matrix/client/r0/rooms/{}/unban".format(self.room_id), + content={"reason": reason, "user_id": self.second_user_id}, + access_token=self.creator_tok, + ) + self.render(request) + self.assertEqual(channel.code, 200, channel.result) + + self._check_for_reason(reason) + + def test_invite_reason(self): + reason = "hello" + request, channel = self.make_request( + "POST", + "/_matrix/client/r0/rooms/{}/invite".format(self.room_id), + content={"reason": reason, "user_id": self.second_user_id}, + access_token=self.creator_tok, + ) + self.render(request) + self.assertEqual(channel.code, 200, channel.result) + + self._check_for_reason(reason) + + def test_reject_invite_reason(self): + self.helper.invite( + self.room_id, + src=self.creator, + targ=self.second_user_id, + tok=self.creator_tok, + ) + + reason = "hello" + request, channel = self.make_request( + "POST", + "/_matrix/client/r0/rooms/{}/leave".format(self.room_id), + content={"reason": reason}, + access_token=self.second_tok, + ) + self.render(request) + self.assertEqual(channel.code, 200, channel.result) + + self._check_for_reason(reason) + + def _check_for_reason(self, reason): + request, channel = self.make_request( + "GET", + "/_matrix/client/r0/rooms/{}/state/m.room.member/{}".format( + self.room_id, self.second_user_id + ), + access_token=self.creator_tok, + ) + self.render(request) + self.assertEqual(channel.code, 200, channel.result) + + event_content = channel.json_body + + self.assertEqual(event_content.get("reason"), reason, channel.result) From 19ba7c142eaced06110c1cb2d22a489dae2ac155 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 28 Nov 2019 13:59:32 +0000 Subject: [PATCH 0556/1623] Newsfile --- changelog.d/6434.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6434.feature diff --git a/changelog.d/6434.feature b/changelog.d/6434.feature new file mode 100644 index 0000000000..affa5d50c1 --- /dev/null +++ b/changelog.d/6434.feature @@ -0,0 +1 @@ +Add support for MSC 2367, which allows specifying a reason on all membership events. From 7baeea9f37f1cb7bf9305f4991b1e1b357f161cf Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 28 Nov 2019 14:55:19 +0000 Subject: [PATCH 0557/1623] blacklist more tests --- .buildkite/worker-blacklist | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.buildkite/worker-blacklist b/.buildkite/worker-blacklist index d7908af177..7950d19db3 100644 --- a/.buildkite/worker-blacklist +++ b/.buildkite/worker-blacklist @@ -57,3 +57,10 @@ Don't get pushed for rooms you've muted Rejected events are not pushed Test that rejected pushers are removed. Events come down the correct room + +# https://buildkite.com/matrix-dot-org/sytest/builds/326#cca62404-a88a-4fcb-ad41-175fd3377603 +Presence changes to UNAVAILABLE are reported to remote room members +If remote user leaves room, changes device and rejoins we see update in sync +uploading self-signing key notifies over federation +Inbound federation can receive redacted events +Outbound federation can request missing events From 708cef88cfbf8dd6df44d2da4ab4dbc7eb584f74 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 28 Nov 2019 19:26:13 +0000 Subject: [PATCH 0558/1623] Discard retention policies when retrieving state Purge jobs don't delete the latest event in a room in order to keep the forward extremity and not break the room. On the other hand, get_state_events, when given an at_token argument calls filter_events_for_client to know if the user can see the event that matches that (sync) token. That function uses the retention policies of the events it's given to filter out those that are too old from a client's view. Some clients, such as Riot, when loading a room, request the list of members for the latest sync token it knows about, and get confused to the point of refusing to send any message if the server tells it that it can't get that information. This can happen very easily with the message retention feature turned on and a room with low activity so that the last event sent becomes too old according to the room's retention policy. An easy and clean fix for that issue is to discard the room's retention policies when retrieving state. --- synapse/handlers/message.py | 2 +- synapse/visibility.py | 22 ++++++++++++++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index 155ed6e06a..3b0156f516 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -138,7 +138,7 @@ class MessageHandler(object): raise NotFoundError("Can't find event for token %s" % (at_token,)) visible_events = yield filter_events_for_client( - self.storage, user_id, last_events + self.storage, user_id, last_events, apply_retention_policies=False ) event = last_events[0] diff --git a/synapse/visibility.py b/synapse/visibility.py index 4d4141dacc..7b037eeb0c 100644 --- a/synapse/visibility.py +++ b/synapse/visibility.py @@ -44,7 +44,8 @@ MEMBERSHIP_PRIORITY = ( @defer.inlineCallbacks def filter_events_for_client( - storage: Storage, user_id, events, is_peeking=False, always_include_ids=frozenset() + storage: Storage, user_id, events, is_peeking=False, always_include_ids=frozenset(), + apply_retention_policies=True, ): """ Check which events a user is allowed to see @@ -59,6 +60,10 @@ def filter_events_for_client( events always_include_ids (set(event_id)): set of event ids to specifically include (unless sender is ignored) + apply_retention_policies (bool): Whether to filter out events that's older than + allowed by the room's retention policy. Useful when this function is called + to e.g. check whether a user should be allowed to see the state at a given + event rather than to know if it should send an event to a user's client(s). Returns: Deferred[list[synapse.events.EventBase]] @@ -86,13 +91,14 @@ def filter_events_for_client( erased_senders = yield storage.main.are_users_erased((e.sender for e in events)) - room_ids = set(e.room_id for e in events) - retention_policies = {} + if apply_retention_policies: + room_ids = set(e.room_id for e in events) + retention_policies = {} - for room_id in room_ids: - retention_policies[room_id] = yield storage.main.get_retention_policy_for_room( - room_id - ) + for room_id in room_ids: + retention_policies[room_id] = ( + yield storage.main.get_retention_policy_for_room(room_id) + ) def allowed(event): """ @@ -113,7 +119,7 @@ def filter_events_for_client( # Don't try to apply the room's retention policy if the event is a state event, as # MSC1763 states that retention is only considered for non-state events. - if not event.is_state(): + if apply_retention_policies and not event.is_state(): retention_policy = retention_policies[event.room_id] max_lifetime = retention_policy.get("max_lifetime") From 5ee2beeddbbcbf09ac054679de71db0e0bf9df31 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 28 Nov 2019 19:32:49 +0000 Subject: [PATCH 0559/1623] Changelog --- changelog.d/6436.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6436.bugfix diff --git a/changelog.d/6436.bugfix b/changelog.d/6436.bugfix new file mode 100644 index 0000000000..954a4e1d84 --- /dev/null +++ b/changelog.d/6436.bugfix @@ -0,0 +1 @@ +Fix a bug where a room could become unusable with a low retention policy and a low activity. From 78ec11c08562bfd635497621da238c7197e69b6f Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 28 Nov 2019 20:35:22 +0000 Subject: [PATCH 0560/1623] Lint --- synapse/visibility.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/synapse/visibility.py b/synapse/visibility.py index 7b037eeb0c..dffe943b28 100644 --- a/synapse/visibility.py +++ b/synapse/visibility.py @@ -44,7 +44,11 @@ MEMBERSHIP_PRIORITY = ( @defer.inlineCallbacks def filter_events_for_client( - storage: Storage, user_id, events, is_peeking=False, always_include_ids=frozenset(), + storage: Storage, + user_id, + events, + is_peeking=False, + always_include_ids=frozenset(), apply_retention_policies=True, ): """ @@ -96,9 +100,9 @@ def filter_events_for_client( retention_policies = {} for room_id in room_ids: - retention_policies[room_id] = ( - yield storage.main.get_retention_policy_for_room(room_id) - ) + retention_policies[ + room_id + ] = yield storage.main.get_retention_policy_for_room(room_id) def allowed(event): """ From 23ea5721259059d50b80083bb7240a8cb56cf297 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Fri, 29 Nov 2019 13:51:14 +0000 Subject: [PATCH 0561/1623] Add User-Interactive Auth to /account/3pid/add (#6119) --- changelog.d/6119.feature | 1 + synapse/rest/client/v2_alpha/account.py | 5 +++++ 2 files changed, 6 insertions(+) create mode 100644 changelog.d/6119.feature diff --git a/changelog.d/6119.feature b/changelog.d/6119.feature new file mode 100644 index 0000000000..1492e83c5a --- /dev/null +++ b/changelog.d/6119.feature @@ -0,0 +1 @@ +Require User-Interactive Authentication for `/account/3pid/add`, meaning the user's password will be required to add a third-party ID to their account. \ No newline at end of file diff --git a/synapse/rest/client/v2_alpha/account.py b/synapse/rest/client/v2_alpha/account.py index f26eae794c..ad674239ab 100644 --- a/synapse/rest/client/v2_alpha/account.py +++ b/synapse/rest/client/v2_alpha/account.py @@ -642,6 +642,7 @@ class ThreepidAddRestServlet(RestServlet): self.auth = hs.get_auth() self.auth_handler = hs.get_auth_handler() + @interactive_auth_handler @defer.inlineCallbacks def on_POST(self, request): requester = yield self.auth.get_user_by_req(request) @@ -652,6 +653,10 @@ class ThreepidAddRestServlet(RestServlet): client_secret = body["client_secret"] sid = body["sid"] + yield self.auth_handler.validate_user_via_ui_auth( + requester, body, self.hs.get_ip_from_request(request) + ) + validation_session = yield self.identity_handler.validate_threepid_session( client_secret, sid ) From 81731c6e75fe904a5b44873efa361a229743d99f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=A0t=C4=9Bdronsk=C3=BD?= Date: Mon, 2 Dec 2019 12:12:55 +0000 Subject: [PATCH 0562/1623] Fix: Pillow error when uploading RGBA image (#3325) (#6241) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-Off-By: Filip Štědronský --- changelog.d/6241.bugfix | 1 + synapse/rest/media/v1/thumbnailer.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelog.d/6241.bugfix diff --git a/changelog.d/6241.bugfix b/changelog.d/6241.bugfix new file mode 100644 index 0000000000..25109ca4a6 --- /dev/null +++ b/changelog.d/6241.bugfix @@ -0,0 +1 @@ +Fix error from the Pillow library when uploading RGBA images. diff --git a/synapse/rest/media/v1/thumbnailer.py b/synapse/rest/media/v1/thumbnailer.py index 8cf415e29d..c234ea7421 100644 --- a/synapse/rest/media/v1/thumbnailer.py +++ b/synapse/rest/media/v1/thumbnailer.py @@ -129,5 +129,8 @@ class Thumbnailer(object): def _encode_image(self, output_image, output_type): output_bytes_io = BytesIO() - output_image.save(output_bytes_io, self.FORMATS[output_type], quality=80) + fmt = self.FORMATS[output_type] + if fmt == "JPEG": + output_image = output_image.convert("RGB") + output_image.save(output_bytes_io, fmt, quality=80) return output_bytes_io From 0ad75fd98ef1943ebea98c6d9f2dc5770c643b0a Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Mon, 2 Dec 2019 15:09:57 +0000 Subject: [PATCH 0563/1623] Use python3 packages for Ubuntu (#6443) --- INSTALL.md | 4 ++-- changelog.d/6443.doc | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changelog.d/6443.doc diff --git a/INSTALL.md b/INSTALL.md index 9b7360f0ef..9da2e3c734 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -109,8 +109,8 @@ Installing prerequisites on Ubuntu or Debian: ``` sudo apt-get install build-essential python3-dev libffi-dev \ - python-pip python-setuptools sqlite3 \ - libssl-dev python-virtualenv libjpeg-dev libxslt1-dev + python3-pip python3-setuptools sqlite3 \ + libssl-dev python3-virtualenv libjpeg-dev libxslt1-dev ``` #### ArchLinux diff --git a/changelog.d/6443.doc b/changelog.d/6443.doc new file mode 100644 index 0000000000..67c59f92ee --- /dev/null +++ b/changelog.d/6443.doc @@ -0,0 +1 @@ +Switch Ubuntu package install recommendation to use python3 packages in INSTALL.md. \ No newline at end of file From 72078e4be56d42421e8748e0e45d0fe1204853dd Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Mon, 2 Dec 2019 15:11:32 +0000 Subject: [PATCH 0564/1623] Transfer power level state events on room upgrade (#6237) --- changelog.d/6237.bugfix | 1 + synapse/handlers/room.py | 36 +++++++++++++++++++++++++++++++----- 2 files changed, 32 insertions(+), 5 deletions(-) create mode 100644 changelog.d/6237.bugfix diff --git a/changelog.d/6237.bugfix b/changelog.d/6237.bugfix new file mode 100644 index 0000000000..9285600b00 --- /dev/null +++ b/changelog.d/6237.bugfix @@ -0,0 +1 @@ +Transfer non-standard power levels on room upgrade. \ No newline at end of file diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index e92b2eafd5..35a759f2fe 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -198,21 +198,21 @@ class RoomCreationHandler(BaseHandler): # finally, shut down the PLs in the old room, and update them in the new # room. yield self._update_upgraded_room_pls( - requester, old_room_id, new_room_id, old_room_state + requester, old_room_id, new_room_id, old_room_state, ) return new_room_id @defer.inlineCallbacks def _update_upgraded_room_pls( - self, requester, old_room_id, new_room_id, old_room_state + self, requester, old_room_id, new_room_id, old_room_state, ): """Send updated power levels in both rooms after an upgrade Args: requester (synapse.types.Requester): the user requesting the upgrade - old_room_id (unicode): the id of the room to be replaced - new_room_id (unicode): the id of the replacement room + old_room_id (str): the id of the room to be replaced + new_room_id (str): the id of the replacement room old_room_state (dict[tuple[str, str], str]): the state map for the old room Returns: @@ -298,7 +298,7 @@ class RoomCreationHandler(BaseHandler): tombstone_event_id (unicode|str): the ID of the tombstone event in the old room. Returns: - Deferred[None] + Deferred """ user_id = requester.user.to_string() @@ -333,6 +333,7 @@ class RoomCreationHandler(BaseHandler): (EventTypes.Encryption, ""), (EventTypes.ServerACL, ""), (EventTypes.RelatedGroups, ""), + (EventTypes.PowerLevels, ""), ) old_room_state_ids = yield self.store.get_filtered_current_state_ids( @@ -346,6 +347,31 @@ class RoomCreationHandler(BaseHandler): if old_event: initial_state[k] = old_event.content + # Resolve the minimum power level required to send any state event + # We will give the upgrading user this power level temporarily (if necessary) such that + # they are able to copy all of the state events over, then revert them back to their + # original power level afterwards in _update_upgraded_room_pls + + # Copy over user power levels now as this will not be possible with >100PL users once + # the room has been created + + power_levels = initial_state[(EventTypes.PowerLevels, "")] + + # Calculate the minimum power level needed to clone the room + event_power_levels = power_levels.get("events", {}) + state_default = power_levels.get("state_default", 0) + ban = power_levels.get("ban") + needed_power_level = max(state_default, ban, max(event_power_levels.values())) + + # Raise the requester's power level in the new room if necessary + current_power_level = power_levels["users"][requester.user.to_string()] + if current_power_level < needed_power_level: + # Assign this power level to the requester + power_levels["users"][requester.user.to_string()] = needed_power_level + + # Set the power levels to the modified state + initial_state[(EventTypes.PowerLevels, "")] = power_levels + yield self._send_events_for_new_room( requester, new_room_id, From 57f09e01f528b796d6448446f56ac8079988e6ab Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Mon, 2 Dec 2019 18:23:41 +0000 Subject: [PATCH 0565/1623] Fix error when using synapse_port_db on a vanilla synapse db --- scripts/synapse_port_db | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/synapse_port_db b/scripts/synapse_port_db index 0d3321682c..f24b8ffe67 100755 --- a/scripts/synapse_port_db +++ b/scripts/synapse_port_db @@ -782,7 +782,10 @@ class Porter(object): def _setup_state_group_id_seq(self): def r(txn): txn.execute("SELECT MAX(id) FROM state_groups") - next_id = txn.fetchone()[0] + 1 + curr_id = txn.fetchone()[0] + if not curr_id: + return + next_id = curr_id + 1 txn.execute("ALTER SEQUENCE state_group_id_seq RESTART WITH %s", (next_id,)) return self.postgres_store.runInteraction("setup_state_group_id_seq", r) From 8ee62e4b98d71feef72987c94eef1ed097746f34 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Mon, 2 Dec 2019 18:43:25 +0000 Subject: [PATCH 0566/1623] Add changelog --- changelog.d/6449.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6449.bugfix diff --git a/changelog.d/6449.bugfix b/changelog.d/6449.bugfix new file mode 100644 index 0000000000..ced16544c9 --- /dev/null +++ b/changelog.d/6449.bugfix @@ -0,0 +1 @@ +Fix assumed missing state_groups index in synapse_port_db. \ No newline at end of file From 2252680a986357d2684e798e8c2d3c40d8df2a2f Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 2 Dec 2019 17:01:59 -0500 Subject: [PATCH 0567/1623] make cross signing signature index non-unique --- .../main/schema/delta/56/signing_keys.sql | 2 +- .../56/signing_keys_nonunique_signatures.sql | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 synapse/storage/data_stores/main/schema/delta/56/signing_keys_nonunique_signatures.sql diff --git a/synapse/storage/data_stores/main/schema/delta/56/signing_keys.sql b/synapse/storage/data_stores/main/schema/delta/56/signing_keys.sql index 27a96123e3..bee0a5da91 100644 --- a/synapse/storage/data_stores/main/schema/delta/56/signing_keys.sql +++ b/synapse/storage/data_stores/main/schema/delta/56/signing_keys.sql @@ -40,7 +40,7 @@ CREATE TABLE IF NOT EXISTS e2e_cross_signing_signatures ( signature TEXT NOT NULL ); -CREATE UNIQUE INDEX e2e_cross_signing_signatures_idx ON e2e_cross_signing_signatures(user_id, target_user_id, target_device_id); +CREATE INDEX e2e_cross_signing_signatures2_idx ON e2e_cross_signing_signatures(user_id, target_user_id, target_device_id); -- stream of user signature updates CREATE TABLE IF NOT EXISTS user_signature_stream ( diff --git a/synapse/storage/data_stores/main/schema/delta/56/signing_keys_nonunique_signatures.sql b/synapse/storage/data_stores/main/schema/delta/56/signing_keys_nonunique_signatures.sql new file mode 100644 index 0000000000..0aa90ebf0c --- /dev/null +++ b/synapse/storage/data_stores/main/schema/delta/56/signing_keys_nonunique_signatures.sql @@ -0,0 +1,22 @@ +/* Copyright 2019 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* The cross-signing signatures index should not be a unique index, because a + * user may upload multiple signatures for the same target user. The previous + * index was unique, so delete it if it's there and create a new non-unique + * index. */ + +DROP INDEX IF EXISTS e2e_cross_signing_signatures_idx; CREATE INDEX IF NOT +EXISTS e2e_cross_signing_signatures2_idx ON e2e_cross_signing_signatures(user_id, target_user_id, target_device_id); From e57567f99425c51f6fabb7ba86ae86175ff59992 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 2 Dec 2019 17:10:57 -0500 Subject: [PATCH 0568/1623] add changelog --- changelog.d/6451.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6451.bugfix diff --git a/changelog.d/6451.bugfix b/changelog.d/6451.bugfix new file mode 100644 index 0000000000..e678dc240b --- /dev/null +++ b/changelog.d/6451.bugfix @@ -0,0 +1 @@ +Change the index on the `e2e_cross_signing_signatures` table to be non-unique. From fdec84aa427e2e3b806eb15f462d652f8554cc8d Mon Sep 17 00:00:00 2001 From: Amber Brown Date: Tue, 3 Dec 2019 20:21:25 +1100 Subject: [PATCH 0569/1623] Add benchmarks for structured logging performance (#6266) --- changelog.d/6266.misc | 1 + synapse/logging/_terse_json.py | 1 + synmark/__init__.py | 72 ++++++++++++++++++++ synmark/__main__.py | 90 +++++++++++++++++++++++++ synmark/suites/__init__.py | 3 + synmark/suites/logging.py | 118 +++++++++++++++++++++++++++++++++ tox.ini | 9 +++ 7 files changed, 294 insertions(+) create mode 100644 changelog.d/6266.misc create mode 100644 synmark/__init__.py create mode 100644 synmark/__main__.py create mode 100644 synmark/suites/__init__.py create mode 100644 synmark/suites/logging.py diff --git a/changelog.d/6266.misc b/changelog.d/6266.misc new file mode 100644 index 0000000000..634e421a79 --- /dev/null +++ b/changelog.d/6266.misc @@ -0,0 +1 @@ +Add benchmarks for structured logging and improve output performance. diff --git a/synapse/logging/_terse_json.py b/synapse/logging/_terse_json.py index 05fc64f409..03934956f4 100644 --- a/synapse/logging/_terse_json.py +++ b/synapse/logging/_terse_json.py @@ -256,6 +256,7 @@ class TerseJSONToTCPLogObserver(object): # transport is the same, just trigger a resumeProducing. if self._producer and r.transport is self._producer.transport: self._producer.resumeProducing() + self._connection_waiter = None return # If the producer is still producing, stop it. diff --git a/synmark/__init__.py b/synmark/__init__.py new file mode 100644 index 0000000000..570eb818d9 --- /dev/null +++ b/synmark/__init__.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys + +from twisted.internet import epollreactor +from twisted.internet.main import installReactor + +from synapse.config.homeserver import HomeServerConfig +from synapse.util import Clock + +from tests.utils import default_config, setup_test_homeserver + + +async def make_homeserver(reactor, config=None): + """ + Make a Homeserver suitable for running benchmarks against. + + Args: + reactor: A Twisted reactor to run under. + config: A HomeServerConfig to use, or None. + """ + cleanup_tasks = [] + clock = Clock(reactor) + + if not config: + config = default_config("test") + + config_obj = HomeServerConfig() + config_obj.parse_config_dict(config, "", "") + + hs = await setup_test_homeserver( + cleanup_tasks.append, config=config_obj, reactor=reactor, clock=clock + ) + stor = hs.get_datastore() + + # Run the database background updates. + if hasattr(stor, "do_next_background_update"): + while not await stor.has_completed_background_updates(): + await stor.do_next_background_update(1) + + def cleanup(): + for i in cleanup_tasks: + i() + + return hs, clock.sleep, cleanup + + +def make_reactor(): + """ + Instantiate and install a Twisted reactor suitable for testing (i.e. not the + default global one). + """ + reactor = epollreactor.EPollReactor() + + if "twisted.internet.reactor" in sys.modules: + del sys.modules["twisted.internet.reactor"] + installReactor(reactor) + + return reactor diff --git a/synmark/__main__.py b/synmark/__main__.py new file mode 100644 index 0000000000..ac59befbd4 --- /dev/null +++ b/synmark/__main__.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys +from contextlib import redirect_stderr +from io import StringIO + +import pyperf +from synmark import make_reactor +from synmark.suites import SUITES + +from twisted.internet.defer import ensureDeferred +from twisted.logger import globalLogBeginner, textFileLogObserver +from twisted.python.failure import Failure + +from tests.utils import setupdb + + +def make_test(main): + """ + Take a benchmark function and wrap it in a reactor start and stop. + """ + + def _main(loops): + + reactor = make_reactor() + + file_out = StringIO() + with redirect_stderr(file_out): + + d = ensureDeferred(main(reactor, loops)) + + def on_done(_): + if isinstance(_, Failure): + _.printTraceback() + print(file_out.getvalue()) + reactor.stop() + return _ + + d.addBoth(on_done) + reactor.run() + + return d.result + + return _main + + +if __name__ == "__main__": + + def add_cmdline_args(cmd, args): + if args.log: + cmd.extend(["--log"]) + + runner = pyperf.Runner( + processes=3, min_time=2, show_name=True, add_cmdline_args=add_cmdline_args + ) + runner.argparser.add_argument("--log", action="store_true") + runner.parse_args() + + orig_loops = runner.args.loops + runner.args.inherit_environ = ["SYNAPSE_POSTGRES"] + + if runner.args.worker: + if runner.args.log: + globalLogBeginner.beginLoggingTo( + [textFileLogObserver(sys.__stdout__)], redirectStandardIO=False + ) + setupdb() + + for suite, loops in SUITES: + if loops: + runner.args.loops = loops + else: + runner.args.loops = orig_loops + loops = "auto" + runner.bench_time_func( + suite.__name__ + "_" + str(loops), make_test(suite.main), + ) diff --git a/synmark/suites/__init__.py b/synmark/suites/__init__.py new file mode 100644 index 0000000000..cfa3b0ba38 --- /dev/null +++ b/synmark/suites/__init__.py @@ -0,0 +1,3 @@ +from . import logging + +SUITES = [(logging, 1000), (logging, 10000), (logging, None)] diff --git a/synmark/suites/logging.py b/synmark/suites/logging.py new file mode 100644 index 0000000000..d8e4c7d58f --- /dev/null +++ b/synmark/suites/logging.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import warnings +from io import StringIO + +from mock import Mock + +from pyperf import perf_counter +from synmark import make_homeserver + +from twisted.internet.defer import Deferred +from twisted.internet.protocol import ServerFactory +from twisted.logger import LogBeginner, Logger, LogPublisher +from twisted.protocols.basic import LineOnlyReceiver + +from synapse.logging._structured import setup_structured_logging + + +class LineCounter(LineOnlyReceiver): + + delimiter = b"\n" + + def __init__(self, *args, **kwargs): + self.count = 0 + super().__init__(*args, **kwargs) + + def lineReceived(self, line): + self.count += 1 + + if self.count >= self.factory.wait_for and self.factory.on_done: + on_done = self.factory.on_done + self.factory.on_done = None + on_done.callback(True) + + +async def main(reactor, loops): + """ + Benchmark how long it takes to send `loops` messages. + """ + servers = [] + + def protocol(): + p = LineCounter() + servers.append(p) + return p + + logger_factory = ServerFactory.forProtocol(protocol) + logger_factory.wait_for = loops + logger_factory.on_done = Deferred() + port = reactor.listenTCP(0, logger_factory, interface="127.0.0.1") + + hs, wait, cleanup = await make_homeserver(reactor) + + errors = StringIO() + publisher = LogPublisher() + mock_sys = Mock() + beginner = LogBeginner( + publisher, errors, mock_sys, warnings, initialBufferSize=loops + ) + + log_config = { + "loggers": {"synapse": {"level": "DEBUG"}}, + "drains": { + "tersejson": { + "type": "network_json_terse", + "host": "127.0.0.1", + "port": port.getHost().port, + "maximum_buffer": 100, + } + }, + } + + logger = Logger(namespace="synapse.logging.test_terse_json", observer=publisher) + logging_system = setup_structured_logging( + hs, hs.config, log_config, logBeginner=beginner, redirect_stdlib_logging=False + ) + + # Wait for it to connect... + await logging_system._observers[0]._service.whenConnected() + + start = perf_counter() + + # Send a bunch of useful messages + for i in range(0, loops): + logger.info("test message %s" % (i,)) + + if ( + len(logging_system._observers[0]._buffer) + == logging_system._observers[0].maximum_buffer + ): + while ( + len(logging_system._observers[0]._buffer) + > logging_system._observers[0].maximum_buffer / 2 + ): + await wait(0.01) + + await logger_factory.on_done + + end = perf_counter() - start + + logging_system.stop() + port.stopListening() + cleanup() + + return end diff --git a/tox.ini b/tox.ini index 62b350ea6a..903a245fb0 100644 --- a/tox.ini +++ b/tox.ini @@ -102,6 +102,15 @@ commands = {envbindir}/coverage run "{envbindir}/trial" {env:TRIAL_FLAGS:} {posargs:tests} {env:TOXSUFFIX:} +[testenv:benchmark] +deps = + {[base]deps} + pyperf +setenv = + SYNAPSE_POSTGRES = 1 +commands = + python -m synmark {posargs:} + [testenv:packaging] skip_install=True deps = From 620f98b65b43404ea6bf99f5907170de72707f8a Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 3 Dec 2019 18:20:39 +0000 Subject: [PATCH 0570/1623] write some docs for the quarantine_media api (#6458) --- changelog.d/6458.doc | 1 + docs/admin_api/media_admin_api.md | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 changelog.d/6458.doc diff --git a/changelog.d/6458.doc b/changelog.d/6458.doc new file mode 100644 index 0000000000..3a9f831d89 --- /dev/null +++ b/changelog.d/6458.doc @@ -0,0 +1 @@ +Write some docs for the quarantine_media api. diff --git a/docs/admin_api/media_admin_api.md b/docs/admin_api/media_admin_api.md index 5e9f8e5d84..8b3666d5f5 100644 --- a/docs/admin_api/media_admin_api.md +++ b/docs/admin_api/media_admin_api.md @@ -21,3 +21,20 @@ It returns a JSON body like the following: ] } ``` + +# Quarantine media in a room + +This API 'quarantines' all the media in a room. + +The API is: + +``` +POST /_synapse/admin/v1/quarantine_media/ + +{} +``` + +Quarantining media means that it is marked as inaccessible by users. It applies +to any local media, and any locally-cached copies of remote media. + +The media file itself (and any thumbnails) is not deleted from the server. From 54dd5dc12b0ac5c48303144c4a73ce3822209488 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 3 Dec 2019 19:19:45 +0000 Subject: [PATCH 0571/1623] Add ephemeral messages support (MSC2228) (#6409) Implement part [MSC2228](https://github.com/matrix-org/matrix-doc/pull/2228). The parts that differ are: * the feature is hidden behind a configuration flag (`enable_ephemeral_messages`) * self-destruction doesn't happen for state events * only implement support for the `m.self_destruct_after` field (not the `m.self_destruct` one) * doesn't send synthetic redactions to clients because for this specific case we consider the clients to be able to destroy an event themselves, instead we just censor it (by pruning its JSON) in the database --- changelog.d/6409.feature | 1 + synapse/api/constants.py | 4 + synapse/config/server.py | 2 + synapse/handlers/federation.py | 8 ++ synapse/handlers/message.py | 123 ++++++++++++++++- synapse/storage/data_stores/main/events.py | 126 +++++++++++++++++- .../main/schema/delta/56/event_expiry.sql | 21 +++ tests/rest/client/test_ephemeral_message.py | 101 ++++++++++++++ 8 files changed, 379 insertions(+), 7 deletions(-) create mode 100644 changelog.d/6409.feature create mode 100644 synapse/storage/data_stores/main/schema/delta/56/event_expiry.sql create mode 100644 tests/rest/client/test_ephemeral_message.py diff --git a/changelog.d/6409.feature b/changelog.d/6409.feature new file mode 100644 index 0000000000..653ff5a5ad --- /dev/null +++ b/changelog.d/6409.feature @@ -0,0 +1 @@ +Add ephemeral messages support by partially implementing [MSC2228](https://github.com/matrix-org/matrix-doc/pull/2228). diff --git a/synapse/api/constants.py b/synapse/api/constants.py index e3f086f1c3..69cef369a5 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -147,3 +147,7 @@ class EventContentFields(object): # Labels for the event, cf https://github.com/matrix-org/matrix-doc/pull/2326 LABELS = "org.matrix.labels" + + # Timestamp to delete the event after + # cf https://github.com/matrix-org/matrix-doc/pull/2228 + SELF_DESTRUCT_AFTER = "org.matrix.self_destruct_after" diff --git a/synapse/config/server.py b/synapse/config/server.py index 7a9d711669..837fbe1582 100644 --- a/synapse/config/server.py +++ b/synapse/config/server.py @@ -490,6 +490,8 @@ class ServerConfig(Config): "cleanup_extremities_with_dummy_events", True ) + self.enable_ephemeral_messages = config.get("enable_ephemeral_messages", False) + def has_tls_listener(self) -> bool: return any(l["tls"] for l in self.listeners) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index d3267734f7..d9d0cd9eef 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -121,6 +121,7 @@ class FederationHandler(BaseHandler): self.pusher_pool = hs.get_pusherpool() self.spam_checker = hs.get_spam_checker() self.event_creation_handler = hs.get_event_creation_handler() + self._message_handler = hs.get_message_handler() self._server_notices_mxid = hs.config.server_notices_mxid self.config = hs.config self.http_client = hs.get_simple_http_client() @@ -141,6 +142,8 @@ class FederationHandler(BaseHandler): self.third_party_event_rules = hs.get_third_party_event_rules() + self._ephemeral_messages_enabled = hs.config.enable_ephemeral_messages + @defer.inlineCallbacks def on_receive_pdu(self, origin, pdu, sent_to_us_directly=False): """ Process a PDU received via a federation /send/ transaction, or @@ -2715,6 +2718,11 @@ class FederationHandler(BaseHandler): event_and_contexts, backfilled=backfilled ) + if self._ephemeral_messages_enabled: + for (event, context) in event_and_contexts: + # If there's an expiry timestamp on the event, schedule its expiry. + self._message_handler.maybe_schedule_expiry(event) + if not backfilled: # Never notify for backfilled events for event, _ in event_and_contexts: yield self._notify_persisted_event(event, max_stream_id) diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index 3b0156f516..4f53a5f5dc 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -15,6 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import logging +from typing import Optional from six import iteritems, itervalues, string_types @@ -22,9 +23,16 @@ from canonicaljson import encode_canonical_json, json from twisted.internet import defer from twisted.internet.defer import succeed +from twisted.internet.interfaces import IDelayedCall from synapse import event_auth -from synapse.api.constants import EventTypes, Membership, RelationTypes, UserTypes +from synapse.api.constants import ( + EventContentFields, + EventTypes, + Membership, + RelationTypes, + UserTypes, +) from synapse.api.errors import ( AuthError, Codes, @@ -62,6 +70,17 @@ class MessageHandler(object): self.storage = hs.get_storage() self.state_store = self.storage.state self._event_serializer = hs.get_event_client_serializer() + self._ephemeral_events_enabled = hs.config.enable_ephemeral_messages + self._is_worker_app = bool(hs.config.worker_app) + + # The scheduled call to self._expire_event. None if no call is currently + # scheduled. + self._scheduled_expiry = None # type: Optional[IDelayedCall] + + if not hs.config.worker_app: + run_as_background_process( + "_schedule_next_expiry", self._schedule_next_expiry + ) @defer.inlineCallbacks def get_room_data( @@ -225,6 +244,100 @@ class MessageHandler(object): for user_id, profile in iteritems(users_with_profile) } + def maybe_schedule_expiry(self, event): + """Schedule the expiry of an event if there's not already one scheduled, + or if the one running is for an event that will expire after the provided + timestamp. + + This function needs to invalidate the event cache, which is only possible on + the master process, and therefore needs to be run on there. + + Args: + event (EventBase): The event to schedule the expiry of. + """ + assert not self._is_worker_app + + expiry_ts = event.content.get(EventContentFields.SELF_DESTRUCT_AFTER) + if not isinstance(expiry_ts, int) or event.is_state(): + return + + # _schedule_expiry_for_event won't actually schedule anything if there's already + # a task scheduled for a timestamp that's sooner than the provided one. + self._schedule_expiry_for_event(event.event_id, expiry_ts) + + @defer.inlineCallbacks + def _schedule_next_expiry(self): + """Retrieve the ID and the expiry timestamp of the next event to be expired, + and schedule an expiry task for it. + + If there's no event left to expire, set _expiry_scheduled to None so that a + future call to save_expiry_ts can schedule a new expiry task. + """ + # Try to get the expiry timestamp of the next event to expire. + res = yield self.store.get_next_event_to_expire() + if res: + event_id, expiry_ts = res + self._schedule_expiry_for_event(event_id, expiry_ts) + + def _schedule_expiry_for_event(self, event_id, expiry_ts): + """Schedule an expiry task for the provided event if there's not already one + scheduled at a timestamp that's sooner than the provided one. + + Args: + event_id (str): The ID of the event to expire. + expiry_ts (int): The timestamp at which to expire the event. + """ + if self._scheduled_expiry: + # If the provided timestamp refers to a time before the scheduled time of the + # next expiry task, cancel that task and reschedule it for this timestamp. + next_scheduled_expiry_ts = self._scheduled_expiry.getTime() * 1000 + if expiry_ts < next_scheduled_expiry_ts: + self._scheduled_expiry.cancel() + else: + return + + # Figure out how many seconds we need to wait before expiring the event. + now_ms = self.clock.time_msec() + delay = (expiry_ts - now_ms) / 1000 + + # callLater doesn't support negative delays, so trim the delay to 0 if we're + # in that case. + if delay < 0: + delay = 0 + + logger.info("Scheduling expiry for event %s in %.3fs", event_id, delay) + + self._scheduled_expiry = self.clock.call_later( + delay, + run_as_background_process, + "_expire_event", + self._expire_event, + event_id, + ) + + @defer.inlineCallbacks + def _expire_event(self, event_id): + """Retrieve and expire an event that needs to be expired from the database. + + If the event doesn't exist in the database, log it and delete the expiry date + from the database (so that we don't try to expire it again). + """ + assert self._ephemeral_events_enabled + + self._scheduled_expiry = None + + logger.info("Expiring event %s", event_id) + + try: + # Expire the event if we know about it. This function also deletes the expiry + # date from the database in the same database transaction. + yield self.store.expire_event(event_id) + except Exception as e: + logger.error("Could not expire event %s: %r", event_id, e) + + # Schedule the expiry of the next event to expire. + yield self._schedule_next_expiry() + # The duration (in ms) after which rooms should be removed # `_rooms_to_exclude_from_dummy_event_insertion` (with the effect that we will try @@ -295,6 +408,10 @@ class EventCreationHandler(object): 5 * 60 * 1000, ) + self._message_handler = hs.get_message_handler() + + self._ephemeral_events_enabled = hs.config.enable_ephemeral_messages + @defer.inlineCallbacks def create_event( self, @@ -877,6 +994,10 @@ class EventCreationHandler(object): event, context=context ) + if self._ephemeral_events_enabled: + # If there's an expiry timestamp on the event, schedule its expiry. + self._message_handler.maybe_schedule_expiry(event) + yield self.pusher_pool.on_new_notifications(event_stream_id, max_stream_id) def _notify(): diff --git a/synapse/storage/data_stores/main/events.py b/synapse/storage/data_stores/main/events.py index 2737a1d3ae..79c91fe284 100644 --- a/synapse/storage/data_stores/main/events.py +++ b/synapse/storage/data_stores/main/events.py @@ -130,6 +130,8 @@ class EventsStore( if self.hs.config.redaction_retention_period is not None: hs.get_clock().looping_call(_censor_redactions, 5 * 60 * 1000) + self._ephemeral_messages_enabled = hs.config.enable_ephemeral_messages + @defer.inlineCallbacks def _read_forward_extremities(self): def fetch(txn): @@ -940,6 +942,12 @@ class EventsStore( txn, event.event_id, labels, event.room_id, event.depth ) + if self._ephemeral_messages_enabled: + # If there's an expiry timestamp on the event, store it. + expiry_ts = event.content.get(EventContentFields.SELF_DESTRUCT_AFTER) + if isinstance(expiry_ts, int) and not event.is_state(): + self._insert_event_expiry_txn(txn, event.event_id, expiry_ts) + # Insert into the room_memberships table. self._store_room_members_txn( txn, @@ -1101,12 +1109,7 @@ class EventsStore( def _update_censor_txn(txn): for redaction_id, event_id, pruned_json in updates: if pruned_json: - self._simple_update_one_txn( - txn, - table="event_json", - keyvalues={"event_id": event_id}, - updatevalues={"json": pruned_json}, - ) + self._censor_event_txn(txn, event_id, pruned_json) self._simple_update_one_txn( txn, @@ -1117,6 +1120,22 @@ class EventsStore( yield self.runInteraction("_update_censor_txn", _update_censor_txn) + def _censor_event_txn(self, txn, event_id, pruned_json): + """Censor an event by replacing its JSON in the event_json table with the + provided pruned JSON. + + Args: + txn (LoggingTransaction): The database transaction. + event_id (str): The ID of the event to censor. + pruned_json (str): The pruned JSON + """ + self._simple_update_one_txn( + txn, + table="event_json", + keyvalues={"event_id": event_id}, + updatevalues={"json": pruned_json}, + ) + @defer.inlineCallbacks def count_daily_messages(self): """ @@ -1957,6 +1976,101 @@ class EventsStore( ], ) + def _insert_event_expiry_txn(self, txn, event_id, expiry_ts): + """Save the expiry timestamp associated with a given event ID. + + Args: + txn (LoggingTransaction): The database transaction to use. + event_id (str): The event ID the expiry timestamp is associated with. + expiry_ts (int): The timestamp at which to expire (delete) the event. + """ + return self._simple_insert_txn( + txn=txn, + table="event_expiry", + values={"event_id": event_id, "expiry_ts": expiry_ts}, + ) + + @defer.inlineCallbacks + def expire_event(self, event_id): + """Retrieve and expire an event that has expired, and delete its associated + expiry timestamp. If the event can't be retrieved, delete its associated + timestamp so we don't try to expire it again in the future. + + Args: + event_id (str): The ID of the event to delete. + """ + # Try to retrieve the event's content from the database or the event cache. + event = yield self.get_event(event_id) + + def delete_expired_event_txn(txn): + # Delete the expiry timestamp associated with this event from the database. + self._delete_event_expiry_txn(txn, event_id) + + if not event: + # If we can't find the event, log a warning and delete the expiry date + # from the database so that we don't try to expire it again in the + # future. + logger.warning( + "Can't expire event %s because we don't have it.", event_id + ) + return + + # Prune the event's dict then convert it to JSON. + pruned_json = encode_json(prune_event_dict(event.get_dict())) + + # Update the event_json table to replace the event's JSON with the pruned + # JSON. + self._censor_event_txn(txn, event.event_id, pruned_json) + + # We need to invalidate the event cache entry for this event because we + # changed its content in the database. We can't call + # self._invalidate_cache_and_stream because self.get_event_cache isn't of the + # right type. + txn.call_after(self._get_event_cache.invalidate, (event.event_id,)) + # Send that invalidation to replication so that other workers also invalidate + # the event cache. + self._send_invalidation_to_replication( + txn, "_get_event_cache", (event.event_id,) + ) + + yield self.runInteraction("delete_expired_event", delete_expired_event_txn) + + def _delete_event_expiry_txn(self, txn, event_id): + """Delete the expiry timestamp associated with an event ID without deleting the + actual event. + + Args: + txn (LoggingTransaction): The transaction to use to perform the deletion. + event_id (str): The event ID to delete the associated expiry timestamp of. + """ + return self._simple_delete_txn( + txn=txn, table="event_expiry", keyvalues={"event_id": event_id} + ) + + def get_next_event_to_expire(self): + """Retrieve the entry with the lowest expiry timestamp in the event_expiry + table, or None if there's no more event to expire. + + Returns: Deferred[Optional[Tuple[str, int]]] + A tuple containing the event ID as its first element and an expiry timestamp + as its second one, if there's at least one row in the event_expiry table. + None otherwise. + """ + + def get_next_event_to_expire_txn(txn): + txn.execute( + """ + SELECT event_id, expiry_ts FROM event_expiry + ORDER BY expiry_ts ASC LIMIT 1 + """ + ) + + return txn.fetchone() + + return self.runInteraction( + desc="get_next_event_to_expire", func=get_next_event_to_expire_txn + ) + AllNewEventsResult = namedtuple( "AllNewEventsResult", diff --git a/synapse/storage/data_stores/main/schema/delta/56/event_expiry.sql b/synapse/storage/data_stores/main/schema/delta/56/event_expiry.sql new file mode 100644 index 0000000000..81a36a8b1d --- /dev/null +++ b/synapse/storage/data_stores/main/schema/delta/56/event_expiry.sql @@ -0,0 +1,21 @@ +/* Copyright 2019 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +CREATE TABLE IF NOT EXISTS event_expiry ( + event_id TEXT PRIMARY KEY, + expiry_ts BIGINT NOT NULL +); + +CREATE INDEX event_expiry_expiry_ts_idx ON event_expiry(expiry_ts); diff --git a/tests/rest/client/test_ephemeral_message.py b/tests/rest/client/test_ephemeral_message.py new file mode 100644 index 0000000000..5e9c07ebf3 --- /dev/null +++ b/tests/rest/client/test_ephemeral_message.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 New Vector Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from synapse.api.constants import EventContentFields, EventTypes +from synapse.rest import admin +from synapse.rest.client.v1 import room + +from tests import unittest + + +class EphemeralMessageTestCase(unittest.HomeserverTestCase): + + user_id = "@user:test" + + servlets = [ + admin.register_servlets, + room.register_servlets, + ] + + def make_homeserver(self, reactor, clock): + config = self.default_config() + + config["enable_ephemeral_messages"] = True + + self.hs = self.setup_test_homeserver(config=config) + return self.hs + + def prepare(self, reactor, clock, homeserver): + self.room_id = self.helper.create_room_as(self.user_id) + + def test_message_expiry_no_delay(self): + """Tests that sending a message sent with a m.self_destruct_after field set to the + past results in that event being deleted right away. + """ + # Send a message in the room that has expired. From here, the reactor clock is + # at 200ms, so 0 is in the past, and even if that wasn't the case and the clock + # is at 0ms the code path is the same if the event's expiry timestamp is the + # current timestamp. + res = self.helper.send_event( + room_id=self.room_id, + type=EventTypes.Message, + content={ + "msgtype": "m.text", + "body": "hello", + EventContentFields.SELF_DESTRUCT_AFTER: 0, + }, + ) + event_id = res["event_id"] + + # Check that we can't retrieve the content of the event. + event_content = self.get_event(self.room_id, event_id)["content"] + self.assertFalse(bool(event_content), event_content) + + def test_message_expiry_delay(self): + """Tests that sending a message with a m.self_destruct_after field set to the + future results in that event not being deleted right away, but advancing the + clock to after that expiry timestamp causes the event to be deleted. + """ + # Send a message in the room that'll expire in 1s. + res = self.helper.send_event( + room_id=self.room_id, + type=EventTypes.Message, + content={ + "msgtype": "m.text", + "body": "hello", + EventContentFields.SELF_DESTRUCT_AFTER: self.clock.time_msec() + 1000, + }, + ) + event_id = res["event_id"] + + # Check that we can retrieve the content of the event before it has expired. + event_content = self.get_event(self.room_id, event_id)["content"] + self.assertTrue(bool(event_content), event_content) + + # Advance the clock to after the deletion. + self.reactor.advance(1) + + # Check that we can't retrieve the content of the event anymore. + event_content = self.get_event(self.room_id, event_id)["content"] + self.assertFalse(bool(event_content), event_content) + + def get_event(self, room_id, event_id, expected_code=200): + url = "/_matrix/client/r0/rooms/%s/event/%s" % (room_id, event_id) + + request, channel = self.make_request("GET", url) + self.render(request) + + self.assertEqual(channel.code, expected_code, channel.result) + + return channel.json_body From 418813b205b7b6f8c9d21f639d740fd2e1016ab7 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Tue, 3 Dec 2019 15:27:00 -0500 Subject: [PATCH 0572/1623] apply changes from review --- changelog.d/6451.bugfix | 2 +- .../storage/data_stores/main/schema/delta/56/signing_keys.sql | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/changelog.d/6451.bugfix b/changelog.d/6451.bugfix index e678dc240b..23b67583ec 100644 --- a/changelog.d/6451.bugfix +++ b/changelog.d/6451.bugfix @@ -1 +1 @@ -Change the index on the `e2e_cross_signing_signatures` table to be non-unique. +Fix uploading multiple cross signing signatures for the same user. diff --git a/synapse/storage/data_stores/main/schema/delta/56/signing_keys.sql b/synapse/storage/data_stores/main/schema/delta/56/signing_keys.sql index bee0a5da91..5c5fffcafb 100644 --- a/synapse/storage/data_stores/main/schema/delta/56/signing_keys.sql +++ b/synapse/storage/data_stores/main/schema/delta/56/signing_keys.sql @@ -40,7 +40,8 @@ CREATE TABLE IF NOT EXISTS e2e_cross_signing_signatures ( signature TEXT NOT NULL ); -CREATE INDEX e2e_cross_signing_signatures2_idx ON e2e_cross_signing_signatures(user_id, target_user_id, target_device_id); +-- replaced by the index created in signing_keys_nonunique_signatures.sql +-- CREATE UNIQUE INDEX e2e_cross_signing_signatures_idx ON e2e_cross_signing_signatures(user_id, target_user_id, target_device_id); -- stream of user signature updates CREATE TABLE IF NOT EXISTS user_signature_stream ( From ce1c975ebc3f2ba6448ec97dcc94ff7f4da8d4c4 Mon Sep 17 00:00:00 2001 From: Syam G Krishnan Date: Fri, 29 Nov 2019 22:50:13 +0530 Subject: [PATCH 0573/1623] Issue #6406 Fix parameter mismatch Signed-off-by: Syam G Krishnan --- synapse/handlers/register.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/handlers/register.py b/synapse/handlers/register.py index 95806af41e..8a7d965feb 100644 --- a/synapse/handlers/register.py +++ b/synapse/handlers/register.py @@ -266,7 +266,7 @@ class RegistrationHandler(BaseHandler): } # Bind email to new account - yield self._register_email_threepid(user_id, threepid_dict, None, False) + yield self._register_email_threepid(user_id, threepid_dict, None) return user_id From b62c9db8d77d76a962300aa740d2e61d57dc6888 Mon Sep 17 00:00:00 2001 From: Syam G Krishnan Date: Fri, 29 Nov 2019 23:06:44 +0530 Subject: [PATCH 0574/1623] Add changelog file Signed-off-by: Syam G Krishnan --- changelog.d/6406.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6406.bugfix diff --git a/changelog.d/6406.bugfix b/changelog.d/6406.bugfix new file mode 100644 index 0000000000..ca9bee084b --- /dev/null +++ b/changelog.d/6406.bugfix @@ -0,0 +1 @@ +Fix bug: TypeError in `register_user()` while using LDAP auth module. From 012087546227e566eb7234faae54ab7674e017de Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 4 Dec 2019 07:38:35 +0000 Subject: [PATCH 0575/1623] Fix exception when a cross-signed device is deleted (#6462) (hopefully) ... and deobfuscate the relevant bit of code. --- changelog.d/6462.bugfix | 1 + .../data_stores/main/end_to_end_keys.py | 23 +++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 changelog.d/6462.bugfix diff --git a/changelog.d/6462.bugfix b/changelog.d/6462.bugfix new file mode 100644 index 0000000000..c435939526 --- /dev/null +++ b/changelog.d/6462.bugfix @@ -0,0 +1 @@ +Fix bug which lead to exceptions being thrown in a loop when a cross-signed device is deleted. diff --git a/synapse/storage/data_stores/main/end_to_end_keys.py b/synapse/storage/data_stores/main/end_to_end_keys.py index d8ad59ad93..643327b57b 100644 --- a/synapse/storage/data_stores/main/end_to_end_keys.py +++ b/synapse/storage/data_stores/main/end_to_end_keys.py @@ -145,13 +145,28 @@ class EndToEndKeyWorkerStore(SQLBaseStore): txn.execute(signature_sql, signature_query_params) rows = self.cursor_to_dict(txn) + # add each cross-signing signature to the correct device in the result dict. for row in rows: + signing_user_id = row["user_id"] + signing_key_id = row["key_id"] target_user_id = row["target_user_id"] target_device_id = row["target_device_id"] - if target_user_id in result and target_device_id in result[target_user_id]: - result[target_user_id][target_device_id].setdefault( - "signatures", {} - ).setdefault(row["user_id"], {})[row["key_id"]] = row["signature"] + signature = row["signature"] + + target_user_result = result.get(target_user_id) + if not target_user_result: + continue + + target_device_result = target_user_result.get(target_device_id) + if not target_device_result: + # note that target_device_result will be None for deleted devices. + continue + + target_device_signatures = target_device_result.setdefault("signatures", {}) + signing_user_signatures = target_device_signatures.setdefault( + signing_user_id, {} + ) + signing_user_signatures[signing_key_id] = signature log_kv(result) return result From cb0aeb147e3b3defc27866ad0e4982e63600a7ee Mon Sep 17 00:00:00 2001 From: Neil Johnson Date: Wed, 4 Dec 2019 09:46:16 +0000 Subject: [PATCH 0576/1623] privacy by default for room dir (#6355) Ensure that the the default settings for the room directory are that the it is hidden from public view by default. --- UPGRADE.rst | 17 ++++++++ changelog.d/6354.feature | 1 + docs/sample_config.yaml | 13 +++--- synapse/config/server.py | 26 ++++++------ tests/federation/transport/test_server.py | 52 +++++++++++++++++++++++ 5 files changed, 91 insertions(+), 18 deletions(-) create mode 100644 changelog.d/6354.feature create mode 100644 tests/federation/transport/test_server.py diff --git a/UPGRADE.rst b/UPGRADE.rst index 5ebf16a73e..d9020f2663 100644 --- a/UPGRADE.rst +++ b/UPGRADE.rst @@ -75,6 +75,23 @@ for example: wget https://packages.matrix.org/debian/pool/main/m/matrix-synapse-py3/matrix-synapse-py3_1.3.0+stretch1_amd64.deb dpkg -i matrix-synapse-py3_1.3.0+stretch1_amd64.deb +Upgrading to v1.7.0 +=================== + +In an attempt to configure Synapse in a privacy preserving way, the default +behaviours of ``allow_public_rooms_without_auth`` and +``allow_public_rooms_over_federation`` have been inverted. This means that by +default, only authenticated users querying the Client/Server API will be able +to query the room directory, and relatedly that the server will not share +room directory information with other servers over federation. + +If your installation does not explicitly set these settings one way or the other +and you want either setting to be ``true`` then it will necessary to update +your homeserver configuration file accordingly. + +For more details on the surrounding context see our `explainer +`_. + Upgrading to v1.5.0 =================== diff --git a/changelog.d/6354.feature b/changelog.d/6354.feature new file mode 100644 index 0000000000..fed9db884b --- /dev/null +++ b/changelog.d/6354.feature @@ -0,0 +1 @@ +Configure privacy preserving settings by default for the room directory. diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index c7391f0c48..10664ae8f7 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -54,15 +54,16 @@ pid_file: DATADIR/homeserver.pid # #require_auth_for_profile_requests: true -# If set to 'false', requires authentication to access the server's public rooms -# directory through the client API. Defaults to 'true'. +# If set to 'true', removes the need for authentication to access the server's +# public rooms directory through the client API, meaning that anyone can +# query the room directory. Defaults to 'false'. # -#allow_public_rooms_without_auth: false +#allow_public_rooms_without_auth: true -# If set to 'false', forbids any other homeserver to fetch the server's public -# rooms directory via federation. Defaults to 'true'. +# If set to 'true', allows any other homeserver to fetch the server's public +# rooms directory via federation. Defaults to 'false'. # -#allow_public_rooms_over_federation: false +#allow_public_rooms_over_federation: true # The default room version for newly created rooms. # diff --git a/synapse/config/server.py b/synapse/config/server.py index 837fbe1582..a4bef00936 100644 --- a/synapse/config/server.py +++ b/synapse/config/server.py @@ -118,15 +118,16 @@ class ServerConfig(Config): self.allow_public_rooms_without_auth = False self.allow_public_rooms_over_federation = False else: - # If set to 'False', requires authentication to access the server's public - # rooms directory through the client API. Defaults to 'True'. + # If set to 'true', removes the need for authentication to access the server's + # public rooms directory through the client API, meaning that anyone can + # query the room directory. Defaults to 'false'. self.allow_public_rooms_without_auth = config.get( - "allow_public_rooms_without_auth", True + "allow_public_rooms_without_auth", False ) - # If set to 'False', forbids any other homeserver to fetch the server's public - # rooms directory via federation. Defaults to 'True'. + # If set to 'true', allows any other homeserver to fetch the server's public + # rooms directory via federation. Defaults to 'false'. self.allow_public_rooms_over_federation = config.get( - "allow_public_rooms_over_federation", True + "allow_public_rooms_over_federation", False ) default_room_version = config.get("default_room_version", DEFAULT_ROOM_VERSION) @@ -620,15 +621,16 @@ class ServerConfig(Config): # #require_auth_for_profile_requests: true - # If set to 'false', requires authentication to access the server's public rooms - # directory through the client API. Defaults to 'true'. + # If set to 'true', removes the need for authentication to access the server's + # public rooms directory through the client API, meaning that anyone can + # query the room directory. Defaults to 'false'. # - #allow_public_rooms_without_auth: false + #allow_public_rooms_without_auth: true - # If set to 'false', forbids any other homeserver to fetch the server's public - # rooms directory via federation. Defaults to 'true'. + # If set to 'true', allows any other homeserver to fetch the server's public + # rooms directory via federation. Defaults to 'false'. # - #allow_public_rooms_over_federation: false + #allow_public_rooms_over_federation: true # The default room version for newly created rooms. # diff --git a/tests/federation/transport/test_server.py b/tests/federation/transport/test_server.py new file mode 100644 index 0000000000..27d83bb7d9 --- /dev/null +++ b/tests/federation/transport/test_server.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from twisted.internet import defer + +from synapse.config.ratelimiting import FederationRateLimitConfig +from synapse.federation.transport import server +from synapse.util.ratelimitutils import FederationRateLimiter + +from tests import unittest +from tests.unittest import override_config + + +class RoomDirectoryFederationTests(unittest.HomeserverTestCase): + def prepare(self, reactor, clock, homeserver): + class Authenticator(object): + def authenticate_request(self, request, content): + return defer.succeed("otherserver.nottld") + + ratelimiter = FederationRateLimiter(clock, FederationRateLimitConfig()) + server.register_servlets( + homeserver, self.resource, Authenticator(), ratelimiter + ) + + @override_config({"allow_public_rooms_over_federation": False}) + def test_blocked_public_room_list_over_federation(self): + request, channel = self.make_request( + "GET", "/_matrix/federation/v1/publicRooms" + ) + self.render(request) + self.assertEquals(403, channel.code) + + @override_config({"allow_public_rooms_over_federation": True}) + def test_open_public_room_list_over_federation(self): + request, channel = self.make_request( + "GET", "/_matrix/federation/v1/publicRooms" + ) + self.render(request) + self.assertEquals(200, channel.code) From 768b84409b6ad516796d6452394ff1fe32b9abb1 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Wed, 4 Dec 2019 10:45:56 +0000 Subject: [PATCH 0577/1623] Update changelog.d/6449.bugfix Co-Authored-By: Erik Johnston --- changelog.d/6449.bugfix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/6449.bugfix b/changelog.d/6449.bugfix index ced16544c9..002f33c450 100644 --- a/changelog.d/6449.bugfix +++ b/changelog.d/6449.bugfix @@ -1 +1 @@ -Fix assumed missing state_groups index in synapse_port_db. \ No newline at end of file +Fix error when using synapse_port_db on a vanilla synapse db. From c1ae453932da8b5761cd1644b4b0bbaa039ae6ab Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Wed, 4 Dec 2019 12:21:48 +0000 Subject: [PATCH 0578/1623] Markdownification and other fixes to CONTRIBUTING (#6461) --- .github/PULL_REQUEST_TEMPLATE.md | 8 +- CONTRIBUTING.md | 210 +++++++++++++++++++++++++++++++ CONTRIBUTING.rst | 206 ------------------------------ changelog.d/6461.doc | 1 + 4 files changed, 215 insertions(+), 210 deletions(-) create mode 100644 CONTRIBUTING.md delete mode 100644 CONTRIBUTING.rst create mode 100644 changelog.d/6461.doc diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 8939fda67d..11fb05ca96 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,8 +1,8 @@ ### Pull Request Checklist - + * [ ] Pull request is based on the develop branch -* [ ] Pull request includes a [changelog file](https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.rst#changelog) -* [ ] Pull request includes a [sign off](https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.rst#sign-off) -* [ ] Code style is correct (run the [linters](https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.rst#code-style)) +* [ ] Pull request includes a [changelog file](https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.md#changelog) +* [ ] Pull request includes a [sign off](https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.md#sign-off) +* [ ] Code style is correct (run the [linters](https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.md#code-style)) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..c0091346f3 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,210 @@ +# Contributing code to Matrix + +Everyone is welcome to contribute code to Matrix +(https://github.com/matrix-org), provided that they are willing to license +their contributions under the same license as the project itself. We follow a +simple 'inbound=outbound' model for contributions: the act of submitting an +'inbound' contribution means that the contributor agrees to license the code +under the same terms as the project's overall 'outbound' license - in our +case, this is almost always Apache Software License v2 (see [LICENSE](LICENSE)). + +## How to contribute + +The preferred and easiest way to contribute changes to Matrix is to fork the +relevant project on github, and then [create a pull request]( +https://help.github.com/articles/using-pull-requests/) to ask us to pull +your changes into our repo. + +**The single biggest thing you need to know is: please base your changes on +the develop branch - *not* master.** + +We use the master branch to track the most recent release, so that folks who +blindly clone the repo and automatically check out master get something that +works. Develop is the unstable branch where all the development actually +happens: the workflow is that contributors should fork the develop branch to +make a 'feature' branch for a particular contribution, and then make a pull +request to merge this back into the matrix.org 'official' develop branch. We +use github's pull request workflow to review the contribution, and either ask +you to make any refinements needed or merge it and make them ourselves. The +changes will then land on master when we next do a release. + +We use [Buildkite](https://buildkite.com/matrix-dot-org/synapse) for continuous +integration. If your change breaks the build, this will be shown in GitHub, so +please keep an eye on the pull request for feedback. + +To run unit tests in a local development environment, you can use: + +- ``tox -e py35`` (requires tox to be installed by ``pip install tox``) + for SQLite-backed Synapse on Python 3.5. +- ``tox -e py36`` for SQLite-backed Synapse on Python 3.6. +- ``tox -e py36-postgres`` for PostgreSQL-backed Synapse on Python 3.6 + (requires a running local PostgreSQL with access to create databases). +- ``./test_postgresql.sh`` for PostgreSQL-backed Synapse on Python 3.5 + (requires Docker). Entirely self-contained, recommended if you don't want to + set up PostgreSQL yourself. + +Docker images are available for running the integration tests (SyTest) locally, +see the [documentation in the SyTest repo]( +https://github.com/matrix-org/sytest/blob/develop/docker/README.md) for more +information. + +## Code style + +All Matrix projects have a well-defined code-style - and sometimes we've even +got as far as documenting it... For instance, synapse's code style doc lives +[here](docs/code_style.md). + +To facilitate meeting these criteria you can run `scripts-dev/lint.sh` +locally. Since this runs the tools listed in the above document, you'll need +python 3.6 and to install each tool: + +``` +# Install the dependencies +pip install -U black flake8 isort + +# Run the linter script +./scripts-dev/lint.sh +``` + +**Note that the script does not just test/check, but also reformats code, so you +may wish to ensure any new code is committed first**. By default this script +checks all files and can take some time; if you alter only certain files, you +might wish to specify paths as arguments to reduce the run-time: + +``` +./scripts-dev/lint.sh path/to/file1.py path/to/file2.py path/to/folder +``` + +Before pushing new changes, ensure they don't produce linting errors. Commit any +files that were corrected. + +Please ensure your changes match the cosmetic style of the existing project, +and **never** mix cosmetic and functional changes in the same commit, as it +makes it horribly hard to review otherwise. + + +## Changelog + +All changes, even minor ones, need a corresponding changelog / newsfragment +entry. These are managed by [Towncrier](https://github.com/hawkowl/towncrier). + +To create a changelog entry, make a new file in the `changelog.d` directory named +in the format of `PRnumber.type`. The type can be one of the following: + +* `feature` +* `bugfix` +* `docker` (for updates to the Docker image) +* `doc` (for updates to the documentation) +* `removal` (also used for deprecations) +* `misc` (for internal-only changes) + +The content of the file is your changelog entry, which should be a short +description of your change in the same style as the rest of our [changelog]( +https://github.com/matrix-org/synapse/blob/master/CHANGES.md). The file can +contain Markdown formatting, and should end with a full stop ('.') for +consistency. + +Adding credits to the changelog is encouraged, we value your +contributions and would like to have you shouted out in the release notes! + +For example, a fix in PR #1234 would have its changelog entry in +`changelog.d/1234.bugfix`, and contain content like "The security levels of +Florbs are now validated when received over federation. Contributed by Jane +Matrix.". + +## Debian changelog + +Changes which affect the debian packaging files (in `debian`) are an +exception. + +In this case, you will need to add an entry to the debian changelog for the +next release. For this, run the following command: + +``` +dch +``` + +This will make up a new version number (if there isn't already an unreleased +version in flight), and open an editor where you can add a new changelog entry. +(Our release process will ensure that the version number and maintainer name is +corrected for the release.) + +If your change affects both the debian packaging *and* files outside the debian +directory, you will need both a regular newsfragment *and* an entry in the +debian changelog. (Though typically such changes should be submitted as two +separate pull requests.) + +## Sign off + +In order to have a concrete record that your contribution is intentional +and you agree to license it under the same terms as the project's license, we've adopted the +same lightweight approach that the Linux Kernel +[submitting patches process]( +https://www.kernel.org/doc/html/latest/process/submitting-patches.html#sign-your-work-the-developer-s-certificate-of-origin>), +[Docker](https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other +projects use: the DCO (Developer Certificate of Origin: +http://developercertificate.org/). This is a simple declaration that you wrote +the contribution or otherwise have the right to contribute it to Matrix: + +``` +Developer Certificate of Origin +Version 1.1 + +Copyright (C) 2004, 2006 The Linux Foundation and its contributors. +660 York Street, Suite 102, +San Francisco, CA 94110 USA + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + +Developer's Certificate of Origin 1.1 + +By making a contribution to this project, I certify that: + +(a) The contribution was created in whole or in part by me and I + have the right to submit it under the open source license + indicated in the file; or + +(b) The contribution is based upon previous work that, to the best + of my knowledge, is covered under an appropriate open source + license and I have the right under that license to submit that + work with modifications, whether created in whole or in part + by me, under the same open source license (unless I am + permitted to submit under a different license), as indicated + in the file; or + +(c) The contribution was provided directly to me by some other + person who certified (a), (b) or (c) and I have not modified + it. + +(d) I understand and agree that this project and the contribution + are public and that a record of the contribution (including all + personal information I submit with it, including my sign-off) is + maintained indefinitely and may be redistributed consistent with + this project or the open source license(s) involved. +``` + +If you agree to this for your contribution, then all that's needed is to +include the line in your commit or pull request comment: + +``` +Signed-off-by: Your Name +``` + +We accept contributions under a legally identifiable name, such as +your name on government documentation or common-law names (names +claimed by legitimate usage or repute). Unfortunately, we cannot +accept anonymous contributions at this time. + +Git allows you to add this signoff automatically when using the `-s` +flag to `git commit`, which uses the name and email set in your +`user.name` and `user.email` git configs. + +## Conclusion + +That's it! Matrix is a very open and collaborative project as you might expect +given our obsession with open communication. If we're going to successfully +matrix together all the fragmented communication technologies out there we are +reliant on contributions and collaboration from the community to do so. So +please get involved - and we hope you have as much fun hacking on Matrix as we +do! diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst deleted file mode 100644 index df81f6e54f..0000000000 --- a/CONTRIBUTING.rst +++ /dev/null @@ -1,206 +0,0 @@ -Contributing code to Matrix -=========================== - -Everyone is welcome to contribute code to Matrix -(https://github.com/matrix-org), provided that they are willing to license -their contributions under the same license as the project itself. We follow a -simple 'inbound=outbound' model for contributions: the act of submitting an -'inbound' contribution means that the contributor agrees to license the code -under the same terms as the project's overall 'outbound' license - in our -case, this is almost always Apache Software License v2 (see LICENSE). - -How to contribute -~~~~~~~~~~~~~~~~~ - -The preferred and easiest way to contribute changes to Matrix is to fork the -relevant project on github, and then create a pull request to ask us to pull -your changes into our repo -(https://help.github.com/articles/using-pull-requests/) - -**The single biggest thing you need to know is: please base your changes on -the develop branch - /not/ master.** - -We use the master branch to track the most recent release, so that folks who -blindly clone the repo and automatically check out master get something that -works. Develop is the unstable branch where all the development actually -happens: the workflow is that contributors should fork the develop branch to -make a 'feature' branch for a particular contribution, and then make a pull -request to merge this back into the matrix.org 'official' develop branch. We -use github's pull request workflow to review the contribution, and either ask -you to make any refinements needed or merge it and make them ourselves. The -changes will then land on master when we next do a release. - -We use `Buildkite `_ for -continuous integration. Buildkite builds need to be authorised by a -maintainer. If your change breaks the build, this will be shown in GitHub, so -please keep an eye on the pull request for feedback. - -To run unit tests in a local development environment, you can use: - -- ``tox -e py35`` (requires tox to be installed by ``pip install tox``) - for SQLite-backed Synapse on Python 3.5. -- ``tox -e py36`` for SQLite-backed Synapse on Python 3.6. -- ``tox -e py36-postgres`` for PostgreSQL-backed Synapse on Python 3.6 - (requires a running local PostgreSQL with access to create databases). -- ``./test_postgresql.sh`` for PostgreSQL-backed Synapse on Python 3.5 - (requires Docker). Entirely self-contained, recommended if you don't want to - set up PostgreSQL yourself. - -Docker images are available for running the integration tests (SyTest) locally, -see the `documentation in the SyTest repo -`_ for more -information. - -Code style -~~~~~~~~~~ - -All Matrix projects have a well-defined code-style - and sometimes we've even -got as far as documenting it... For instance, synapse's code style doc lives -at https://github.com/matrix-org/synapse/tree/master/docs/code_style.md. - -To facilitate meeting these criteria you can run ``scripts-dev/lint.sh`` -locally. Since this runs the tools listed in the above document, you'll need -python 3.6 and to install each tool. **Note that the script does not just -test/check, but also reformats code, so you may wish to ensure any new code is -committed first**. By default this script checks all files and can take some -time; if you alter only certain files, you might wish to specify paths as -arguments to reduce the run-time. - -Please ensure your changes match the cosmetic style of the existing project, -and **never** mix cosmetic and functional changes in the same commit, as it -makes it horribly hard to review otherwise. - -Before doing a commit, ensure the changes you've made don't produce -linting errors. You can do this by running the linters as follows. Ensure to -commit any files that were corrected. - -:: - # Install the dependencies - pip install -U black flake8 isort - - # Run the linter script - ./scripts-dev/lint.sh - -Changelog -~~~~~~~~~ - -All changes, even minor ones, need a corresponding changelog / newsfragment -entry. These are managed by Towncrier -(https://github.com/hawkowl/towncrier). - -To create a changelog entry, make a new file in the ``changelog.d`` file named -in the format of ``PRnumber.type``. The type can be one of the following: - -* ``feature``. -* ``bugfix``. -* ``docker`` (for updates to the Docker image). -* ``doc`` (for updates to the documentation). -* ``removal`` (also used for deprecations). -* ``misc`` (for internal-only changes). - -The content of the file is your changelog entry, which should be a short -description of your change in the same style as the rest of our `changelog -`_. The file can -contain Markdown formatting, and should end with a full stop ('.') for -consistency. - -Adding credits to the changelog is encouraged, we value your -contributions and would like to have you shouted out in the release notes! - -For example, a fix in PR #1234 would have its changelog entry in -``changelog.d/1234.bugfix``, and contain content like "The security levels of -Florbs are now validated when recieved over federation. Contributed by Jane -Matrix.". - -Debian changelog ----------------- - -Changes which affect the debian packaging files (in ``debian``) are an -exception. - -In this case, you will need to add an entry to the debian changelog for the -next release. For this, run the following command:: - - dch - -This will make up a new version number (if there isn't already an unreleased -version in flight), and open an editor where you can add a new changelog entry. -(Our release process will ensure that the version number and maintainer name is -corrected for the release.) - -If your change affects both the debian packaging *and* files outside the debian -directory, you will need both a regular newsfragment *and* an entry in the -debian changelog. (Though typically such changes should be submitted as two -separate pull requests.) - -Sign off -~~~~~~~~ - -In order to have a concrete record that your contribution is intentional -and you agree to license it under the same terms as the project's license, we've adopted the -same lightweight approach that the Linux Kernel -`submitting patches process `_, Docker -(https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other -projects use: the DCO (Developer Certificate of Origin: -http://developercertificate.org/). This is a simple declaration that you wrote -the contribution or otherwise have the right to contribute it to Matrix:: - - Developer Certificate of Origin - Version 1.1 - - Copyright (C) 2004, 2006 The Linux Foundation and its contributors. - 660 York Street, Suite 102, - San Francisco, CA 94110 USA - - Everyone is permitted to copy and distribute verbatim copies of this - license document, but changing it is not allowed. - - Developer's Certificate of Origin 1.1 - - By making a contribution to this project, I certify that: - - (a) The contribution was created in whole or in part by me and I - have the right to submit it under the open source license - indicated in the file; or - - (b) The contribution is based upon previous work that, to the best - of my knowledge, is covered under an appropriate open source - license and I have the right under that license to submit that - work with modifications, whether created in whole or in part - by me, under the same open source license (unless I am - permitted to submit under a different license), as indicated - in the file; or - - (c) The contribution was provided directly to me by some other - person who certified (a), (b) or (c) and I have not modified - it. - - (d) I understand and agree that this project and the contribution - are public and that a record of the contribution (including all - personal information I submit with it, including my sign-off) is - maintained indefinitely and may be redistributed consistent with - this project or the open source license(s) involved. - -If you agree to this for your contribution, then all that's needed is to -include the line in your commit or pull request comment:: - - Signed-off-by: Your Name - -We accept contributions under a legally identifiable name, such as -your name on government documentation or common-law names (names -claimed by legitimate usage or repute). Unfortunately, we cannot -accept anonymous contributions at this time. - -Git allows you to add this signoff automatically when using the ``-s`` -flag to ``git commit``, which uses the name and email set in your -``user.name`` and ``user.email`` git configs. - -Conclusion -~~~~~~~~~~ - -That's it! Matrix is a very open and collaborative project as you might expect -given our obsession with open communication. If we're going to successfully -matrix together all the fragmented communication technologies out there we are -reliant on contributions and collaboration from the community to do so. So -please get involved - and we hope you have as much fun hacking on Matrix as we -do! diff --git a/changelog.d/6461.doc b/changelog.d/6461.doc new file mode 100644 index 0000000000..1502fa2855 --- /dev/null +++ b/changelog.d/6461.doc @@ -0,0 +1 @@ +Convert CONTRIBUTING.rst to markdown (among other small fixes). \ No newline at end of file From 08a436ecb25de2c4c8f2daf423bfcaf72e985143 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 4 Dec 2019 14:18:46 +0000 Subject: [PATCH 0579/1623] Incorporate review --- changelog.d/6329.bugfix | 1 + changelog.d/6329.feature | 1 - synapse/handlers/room.py | 3 +-- 3 files changed, 2 insertions(+), 3 deletions(-) create mode 100644 changelog.d/6329.bugfix delete mode 100644 changelog.d/6329.feature diff --git a/changelog.d/6329.bugfix b/changelog.d/6329.bugfix new file mode 100644 index 0000000000..e558d13b7d --- /dev/null +++ b/changelog.d/6329.bugfix @@ -0,0 +1 @@ +Correctly apply the event filter to the `state`, `events_before` and `events_after` fields in the response to `/context` requests. \ No newline at end of file diff --git a/changelog.d/6329.feature b/changelog.d/6329.feature deleted file mode 100644 index c27dbb06a4..0000000000 --- a/changelog.d/6329.feature +++ /dev/null @@ -1 +0,0 @@ -Filter `state`, `events_before` and `events_after` in `/context` requests. diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 3148df0de9..fd3ea8daf8 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -908,12 +908,11 @@ class RoomContextHandler(object): [last_event_id], state_filter=state_filter ) - # Apply the filter on state events. state_events = list(state[last_event_id].values()) if event_filter: state_events = event_filter.filter(state_events) - results["state"] = list(state_events) + results["state"] = state_events # We use a dummy token here as we only care about the room portion of # the token, which we replace. From 65c6aee621fecff1c6a863d6b910c973196ad6bc Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 4 Dec 2019 14:36:39 +0000 Subject: [PATCH 0580/1623] Un-remove room purge test --- tests/rest/client/v1/test_rooms.py | 72 ++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py index 4095e63aef..1ca7fa742f 100644 --- a/tests/rest/client/v1/test_rooms.py +++ b/tests/rest/client/v1/test_rooms.py @@ -815,6 +815,78 @@ class RoomMessageListTestCase(RoomBase): self.assertTrue("chunk" in channel.json_body) self.assertTrue("end" in channel.json_body) + def test_room_messages_purge(self): + store = self.hs.get_datastore() + pagination_handler = self.hs.get_pagination_handler() + + # Send a first message in the room, which will be removed by the purge. + first_event_id = self.helper.send(self.room_id, "message 1")["event_id"] + first_token = self.get_success( + store.get_topological_token_for_event(first_event_id) + ) + + # Send a second message in the room, which won't be removed, and which we'll + # use as the marker to purge events before. + second_event_id = self.helper.send(self.room_id, "message 2")["event_id"] + second_token = self.get_success( + store.get_topological_token_for_event(second_event_id) + ) + + # Send a third event in the room to ensure we don't fall under any edge case + # due to our marker being the latest forward extremity in the room. + self.helper.send(self.room_id, "message 3") + + # Check that we get the first and second message when querying /messages. + request, channel = self.make_request( + "GET", + "/rooms/%s/messages?access_token=x&from=%s&dir=b&filter=%s" + % (self.room_id, second_token, json.dumps({"types": [EventTypes.Message]})), + ) + self.render(request) + self.assertEqual(channel.code, 200, channel.json_body) + + chunk = channel.json_body["chunk"] + self.assertEqual(len(chunk), 2, [event["content"] for event in chunk]) + + # Purge every event before the second event. + purge_id = random_string(16) + pagination_handler._purges_by_id[purge_id] = PurgeStatus() + self.get_success( + pagination_handler._purge_history( + purge_id=purge_id, + room_id=self.room_id, + token=second_token, + delete_local_events=True, + ) + ) + + # Check that we only get the second message through /message now that the first + # has been purged. + request, channel = self.make_request( + "GET", + "/rooms/%s/messages?access_token=x&from=%s&dir=b&filter=%s" + % (self.room_id, second_token, json.dumps({"types": [EventTypes.Message]})), + ) + self.render(request) + self.assertEqual(channel.code, 200, channel.json_body) + + chunk = channel.json_body["chunk"] + self.assertEqual(len(chunk), 1, [event["content"] for event in chunk]) + + # Check that we get no event, but also no error, when querying /messages with + # the token that was pointing at the first event, because we don't have it + # anymore. + request, channel = self.make_request( + "GET", + "/rooms/%s/messages?access_token=x&from=%s&dir=b&filter=%s" + % (self.room_id, first_token, json.dumps({"types": [EventTypes.Message]})), + ) + self.render(request) + self.assertEqual(channel.code, 200, channel.json_body) + + chunk = channel.json_body["chunk"] + self.assertEqual(len(chunk), 0, [event["content"] for event in chunk]) + class RoomSearchTestCase(unittest.HomeserverTestCase): servlets = [ From ddd48b6851675cd192e67a143e9dfde051afecad Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 3 Dec 2019 14:00:09 +0000 Subject: [PATCH 0581/1623] Move account validity bg updates to registration store --- synapse/storage/_base.py | 66 ------------------- .../storage/data_stores/main/registration.py | 64 ++++++++++++++++++ 2 files changed, 64 insertions(+), 66 deletions(-) diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py index 459901ac60..7ebab31af0 100644 --- a/synapse/storage/_base.py +++ b/synapse/storage/_base.py @@ -252,8 +252,6 @@ class SQLBaseStore(object): # A set of tables that are not safe to use native upserts in. self._unsafe_to_upsert_tables = set(UNIQUE_INDEX_BACKGROUND_UPDATES.keys()) - self._account_validity = self.hs.config.account_validity - # We add the user_directory_search table to the blacklist on SQLite # because the existing search table does not have an index, making it # unsafe to use native upserts. @@ -272,14 +270,6 @@ class SQLBaseStore(object): self.rand = random.SystemRandom() - if self._account_validity.enabled: - self._clock.call_later( - 0.0, - run_as_background_process, - "account_validity_set_expiration_dates", - self._set_expiration_date_when_missing, - ) - @defer.inlineCallbacks def _check_safe_to_upsert(self): """ @@ -312,62 +302,6 @@ class SQLBaseStore(object): self._check_safe_to_upsert, ) - @defer.inlineCallbacks - def _set_expiration_date_when_missing(self): - """ - Retrieves the list of registered users that don't have an expiration date, and - adds an expiration date for each of them. - """ - - def select_users_with_no_expiration_date_txn(txn): - """Retrieves the list of registered users with no expiration date from the - database, filtering out deactivated users. - """ - sql = ( - "SELECT users.name FROM users" - " LEFT JOIN account_validity ON (users.name = account_validity.user_id)" - " WHERE account_validity.user_id is NULL AND users.deactivated = 0;" - ) - txn.execute(sql, []) - - res = self.cursor_to_dict(txn) - if res: - for user in res: - self.set_expiration_date_for_user_txn( - txn, user["name"], use_delta=True - ) - - yield self.runInteraction( - "get_users_with_no_expiration_date", - select_users_with_no_expiration_date_txn, - ) - - def set_expiration_date_for_user_txn(self, txn, user_id, use_delta=False): - """Sets an expiration date to the account with the given user ID. - - Args: - user_id (str): User ID to set an expiration date for. - use_delta (bool): If set to False, the expiration date for the user will be - now + validity period. If set to True, this expiration date will be a - random value in the [now + period - d ; now + period] range, d being a - delta equal to 10% of the validity period. - """ - now_ms = self._clock.time_msec() - expiration_ts = now_ms + self._account_validity.period - - if use_delta: - expiration_ts = self.rand.randrange( - expiration_ts - self._account_validity.startup_job_max_delta, - expiration_ts, - ) - - self._simple_upsert_txn( - txn, - "account_validity", - keyvalues={"user_id": user_id}, - values={"expiration_ts_ms": expiration_ts, "email_sent": False}, - ) - def start_profiling(self): self._previous_loop_ts = monotonic_time() diff --git a/synapse/storage/data_stores/main/registration.py b/synapse/storage/data_stores/main/registration.py index 98cf6427c3..653c9318cb 100644 --- a/synapse/storage/data_stores/main/registration.py +++ b/synapse/storage/data_stores/main/registration.py @@ -926,6 +926,14 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): self._account_validity = hs.config.account_validity + if self._account_validity.enabled: + self._clock.call_later( + 0.0, + run_as_background_process, + "account_validity_set_expiration_dates", + self._set_expiration_date_when_missing, + ) + # Create a background job for culling expired 3PID validity tokens def start_cull(): # run as a background process to make sure that the database transactions @@ -1502,3 +1510,59 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): self._invalidate_cache_and_stream( txn, self.get_user_deactivated_status, (user_id,) ) + + @defer.inlineCallbacks + def _set_expiration_date_when_missing(self): + """ + Retrieves the list of registered users that don't have an expiration date, and + adds an expiration date for each of them. + """ + + def select_users_with_no_expiration_date_txn(txn): + """Retrieves the list of registered users with no expiration date from the + database, filtering out deactivated users. + """ + sql = ( + "SELECT users.name FROM users" + " LEFT JOIN account_validity ON (users.name = account_validity.user_id)" + " WHERE account_validity.user_id is NULL AND users.deactivated = 0;" + ) + txn.execute(sql, []) + + res = self.cursor_to_dict(txn) + if res: + for user in res: + self.set_expiration_date_for_user_txn( + txn, user["name"], use_delta=True + ) + + yield self.runInteraction( + "get_users_with_no_expiration_date", + select_users_with_no_expiration_date_txn, + ) + + def set_expiration_date_for_user_txn(self, txn, user_id, use_delta=False): + """Sets an expiration date to the account with the given user ID. + + Args: + user_id (str): User ID to set an expiration date for. + use_delta (bool): If set to False, the expiration date for the user will be + now + validity period. If set to True, this expiration date will be a + random value in the [now + period - d ; now + period] range, d being a + delta equal to 10% of the validity period. + """ + now_ms = self._clock.time_msec() + expiration_ts = now_ms + self._account_validity.period + + if use_delta: + expiration_ts = self.rand.randrange( + expiration_ts - self._account_validity.startup_job_max_delta, + expiration_ts, + ) + + self._simple_upsert_txn( + txn, + "account_validity", + keyvalues={"user_id": user_id}, + values={"expiration_ts_ms": expiration_ts, "email_sent": False}, + ) From 6b2867096b8a2cf8afdb5de2bab93bbf31f76065 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 3 Dec 2019 14:08:48 +0000 Subject: [PATCH 0582/1623] Move event fetch vars to EventWorkStore --- synapse/storage/_base.py | 12 ------------ synapse/storage/data_stores/main/client_ips.py | 2 +- synapse/storage/data_stores/main/devices.py | 2 +- synapse/storage/data_stores/main/events_worker.py | 13 +++++++++++++ 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py index 7ebab31af0..6b8120a608 100644 --- a/synapse/storage/_base.py +++ b/synapse/storage/_base.py @@ -18,7 +18,6 @@ import itertools import logging import random import sys -import threading import time from typing import Iterable, Tuple @@ -36,7 +35,6 @@ from synapse.metrics.background_process_metrics import run_as_background_process from synapse.storage.engines import PostgresEngine, Sqlite3Engine from synapse.types import get_domain_from_id from synapse.util import batch_iter -from synapse.util.caches.descriptors import Cache from synapse.util.stringutils import exception_to_unicode # import a function which will return a monotonic time, in seconds @@ -237,16 +235,6 @@ class SQLBaseStore(object): # to watch it self._txn_perf_counters = PerformanceCounters() - self._get_event_cache = Cache( - "*getEvent*", keylen=3, max_entries=hs.config.event_cache_size - ) - - self._event_fetch_lock = threading.Condition() - self._event_fetch_list = [] - self._event_fetch_ongoing = 0 - - self._pending_ds = [] - self.database_engine = hs.database_engine # A set of tables that are not safe to use native upserts in. diff --git a/synapse/storage/data_stores/main/client_ips.py b/synapse/storage/data_stores/main/client_ips.py index 706c6a1f3f..7931b876ce 100644 --- a/synapse/storage/data_stores/main/client_ips.py +++ b/synapse/storage/data_stores/main/client_ips.py @@ -21,7 +21,7 @@ from twisted.internet import defer from synapse.metrics.background_process_metrics import wrap_as_background_process from synapse.storage import background_updates -from synapse.storage._base import Cache +from synapse.util.caches.descriptors import Cache from synapse.util.caches import CACHE_SIZE_FACTOR logger = logging.getLogger(__name__) diff --git a/synapse/storage/data_stores/main/devices.py b/synapse/storage/data_stores/main/devices.py index 71f62036c0..b50ee026a2 100644 --- a/synapse/storage/data_stores/main/devices.py +++ b/synapse/storage/data_stores/main/devices.py @@ -31,11 +31,11 @@ from synapse.logging.opentracing import ( ) from synapse.metrics.background_process_metrics import run_as_background_process from synapse.storage._base import ( - Cache, SQLBaseStore, db_to_json, make_in_list_sql_clause, ) +from synapse.util.caches.descriptors import Cache from synapse.storage.background_updates import BackgroundUpdateStore from synapse.types import get_verify_key_from_cross_signing_key from synapse.util import batch_iter diff --git a/synapse/storage/data_stores/main/events_worker.py b/synapse/storage/data_stores/main/events_worker.py index 4c4b76bd93..e782e8f481 100644 --- a/synapse/storage/data_stores/main/events_worker.py +++ b/synapse/storage/data_stores/main/events_worker.py @@ -17,6 +17,7 @@ from __future__ import division import itertools import logging +import threading from collections import namedtuple from canonicaljson import json @@ -34,6 +35,7 @@ from synapse.metrics.background_process_metrics import run_as_background_process from synapse.storage._base import SQLBaseStore, make_in_list_sql_clause from synapse.types import get_domain_from_id from synapse.util import batch_iter +from synapse.util.caches.descriptors import Cache from synapse.util.metrics import Measure logger = logging.getLogger(__name__) @@ -53,6 +55,17 @@ _EventCacheEntry = namedtuple("_EventCacheEntry", ("event", "redacted_event")) class EventsWorkerStore(SQLBaseStore): + def __init__(self, db_conn, hs): + super(EventsWorkerStore, self).__init__(db_conn, hs) + + self._get_event_cache = Cache( + "*getEvent*", keylen=3, max_entries=hs.config.event_cache_size + ) + + self._event_fetch_lock = threading.Condition() + self._event_fetch_list = [] + self._event_fetch_ongoing = 0 + def get_received_ts(self, event_id): """Get received_ts (when it was persisted) for the event. From 1056d6885a7b96be85c5ff19e26eba2ed3f90dd4 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 3 Dec 2019 14:28:46 +0000 Subject: [PATCH 0583/1623] Move cache invalidation to main data store --- synapse/replication/slave/storage/_base.py | 3 +- synapse/storage/_base.py | 104 -------------- synapse/storage/data_stores/main/__init__.py | 2 + synapse/storage/data_stores/main/cache.py | 131 ++++++++++++++++++ .../storage/data_stores/main/client_ips.py | 2 +- synapse/storage/data_stores/main/devices.py | 14 +- 6 files changed, 143 insertions(+), 113 deletions(-) create mode 100644 synapse/storage/data_stores/main/cache.py diff --git a/synapse/replication/slave/storage/_base.py b/synapse/replication/slave/storage/_base.py index 456bc005a0..71e5877aca 100644 --- a/synapse/replication/slave/storage/_base.py +++ b/synapse/replication/slave/storage/_base.py @@ -18,7 +18,8 @@ from typing import Dict import six -from synapse.storage._base import _CURRENT_STATE_CACHE_NAME, SQLBaseStore +from synapse.storage._base import SQLBaseStore +from synapse.storage.data_stores.main.cache import _CURRENT_STATE_CACHE_NAME from synapse.storage.engines import PostgresEngine from ._slaved_id_tracker import SlavedIdTracker diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py index 6b8120a608..c02248cfe9 100644 --- a/synapse/storage/_base.py +++ b/synapse/storage/_base.py @@ -14,7 +14,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import itertools import logging import random import sys @@ -34,7 +33,6 @@ from synapse.logging.context import LoggingContext, make_deferred_yieldable from synapse.metrics.background_process_metrics import run_as_background_process from synapse.storage.engines import PostgresEngine, Sqlite3Engine from synapse.types import get_domain_from_id -from synapse.util import batch_iter from synapse.util.stringutils import exception_to_unicode # import a function which will return a monotonic time, in seconds @@ -77,10 +75,6 @@ UNIQUE_INDEX_BACKGROUND_UPDATES = { "event_search": "event_search_event_id_idx", } -# This is a special cache name we use to batch multiple invalidations of caches -# based on the current state when notifying workers over replication. -_CURRENT_STATE_CACHE_NAME = "cs_cache_fake" - class LoggingTransaction(object): """An object that almost-transparently proxies for the 'txn' object @@ -1322,47 +1316,6 @@ class SQLBaseStore(object): return cache, min_val - def _invalidate_cache_and_stream(self, txn, cache_func, keys): - """Invalidates the cache and adds it to the cache stream so slaves - will know to invalidate their caches. - - This should only be used to invalidate caches where slaves won't - otherwise know from other replication streams that the cache should - be invalidated. - """ - txn.call_after(cache_func.invalidate, keys) - self._send_invalidation_to_replication(txn, cache_func.__name__, keys) - - def _invalidate_state_caches_and_stream(self, txn, room_id, members_changed): - """Special case invalidation of caches based on current state. - - We special case this so that we can batch the cache invalidations into a - single replication poke. - - Args: - txn - room_id (str): Room where state changed - members_changed (iterable[str]): The user_ids of members that have changed - """ - txn.call_after(self._invalidate_state_caches, room_id, members_changed) - - if members_changed: - # We need to be careful that the size of the `members_changed` list - # isn't so large that it causes problems sending over replication, so we - # send them in chunks. - # Max line length is 16K, and max user ID length is 255, so 50 should - # be safe. - for chunk in batch_iter(members_changed, 50): - keys = itertools.chain([room_id], chunk) - self._send_invalidation_to_replication( - txn, _CURRENT_STATE_CACHE_NAME, keys - ) - else: - # if no members changed, we still need to invalidate the other caches. - self._send_invalidation_to_replication( - txn, _CURRENT_STATE_CACHE_NAME, [room_id] - ) - def _invalidate_state_caches(self, room_id, members_changed): """Invalidates caches that are based on the current state, but does not stream invalidations down replication. @@ -1396,63 +1349,6 @@ class SQLBaseStore(object): # which is fine. pass - def _send_invalidation_to_replication(self, txn, cache_name, keys): - """Notifies replication that given cache has been invalidated. - - Note that this does *not* invalidate the cache locally. - - Args: - txn - cache_name (str) - keys (iterable[str]) - """ - - if isinstance(self.database_engine, PostgresEngine): - # get_next() returns a context manager which is designed to wrap - # the transaction. However, we want to only get an ID when we want - # to use it, here, so we need to call __enter__ manually, and have - # __exit__ called after the transaction finishes. - ctx = self._cache_id_gen.get_next() - stream_id = ctx.__enter__() - txn.call_on_exception(ctx.__exit__, None, None, None) - txn.call_after(ctx.__exit__, None, None, None) - txn.call_after(self.hs.get_notifier().on_new_replication_data) - - self._simple_insert_txn( - txn, - table="cache_invalidation_stream", - values={ - "stream_id": stream_id, - "cache_func": cache_name, - "keys": list(keys), - "invalidation_ts": self.clock.time_msec(), - }, - ) - - def get_all_updated_caches(self, last_id, current_id, limit): - if last_id == current_id: - return defer.succeed([]) - - def get_all_updated_caches_txn(txn): - # We purposefully don't bound by the current token, as we want to - # send across cache invalidations as quickly as possible. Cache - # invalidations are idempotent, so duplicates are fine. - sql = ( - "SELECT stream_id, cache_func, keys, invalidation_ts" - " FROM cache_invalidation_stream" - " WHERE stream_id > ? ORDER BY stream_id ASC LIMIT ?" - ) - txn.execute(sql, (last_id, limit)) - return txn.fetchall() - - return self.runInteraction("get_all_updated_caches", get_all_updated_caches_txn) - - def get_cache_stream_token(self): - if self._cache_id_gen: - return self._cache_id_gen.get_current_token() - else: - return 0 - def _simple_select_list_paginate( self, table, diff --git a/synapse/storage/data_stores/main/__init__.py b/synapse/storage/data_stores/main/__init__.py index 10c940df1e..474924c68f 100644 --- a/synapse/storage/data_stores/main/__init__.py +++ b/synapse/storage/data_stores/main/__init__.py @@ -32,6 +32,7 @@ from synapse.util.caches.stream_change_cache import StreamChangeCache from .account_data import AccountDataStore from .appservice import ApplicationServiceStore, ApplicationServiceTransactionStore +from .cache import CacheInvalidationStore from .client_ips import ClientIpStore from .deviceinbox import DeviceInboxStore from .devices import DeviceStore @@ -110,6 +111,7 @@ class DataStore( MonthlyActiveUsersStore, StatsStore, RelationsStore, + CacheInvalidationStore, ): def __init__(self, db_conn, hs): self.hs = hs diff --git a/synapse/storage/data_stores/main/cache.py b/synapse/storage/data_stores/main/cache.py new file mode 100644 index 0000000000..6efcc5f3b0 --- /dev/null +++ b/synapse/storage/data_stores/main/cache.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import itertools +import logging + +from twisted.internet import defer + +from synapse.storage._base import SQLBaseStore +from synapse.storage.engines import PostgresEngine +from synapse.util import batch_iter + +logger = logging.getLogger(__name__) + + +# This is a special cache name we use to batch multiple invalidations of caches +# based on the current state when notifying workers over replication. +_CURRENT_STATE_CACHE_NAME = "cs_cache_fake" + + +class CacheInvalidationStore(SQLBaseStore): + def _invalidate_cache_and_stream(self, txn, cache_func, keys): + """Invalidates the cache and adds it to the cache stream so slaves + will know to invalidate their caches. + + This should only be used to invalidate caches where slaves won't + otherwise know from other replication streams that the cache should + be invalidated. + """ + txn.call_after(cache_func.invalidate, keys) + self._send_invalidation_to_replication(txn, cache_func.__name__, keys) + + def _invalidate_state_caches_and_stream(self, txn, room_id, members_changed): + """Special case invalidation of caches based on current state. + + We special case this so that we can batch the cache invalidations into a + single replication poke. + + Args: + txn + room_id (str): Room where state changed + members_changed (iterable[str]): The user_ids of members that have changed + """ + txn.call_after(self._invalidate_state_caches, room_id, members_changed) + + if members_changed: + # We need to be careful that the size of the `members_changed` list + # isn't so large that it causes problems sending over replication, so we + # send them in chunks. + # Max line length is 16K, and max user ID length is 255, so 50 should + # be safe. + for chunk in batch_iter(members_changed, 50): + keys = itertools.chain([room_id], chunk) + self._send_invalidation_to_replication( + txn, _CURRENT_STATE_CACHE_NAME, keys + ) + else: + # if no members changed, we still need to invalidate the other caches. + self._send_invalidation_to_replication( + txn, _CURRENT_STATE_CACHE_NAME, [room_id] + ) + + def _send_invalidation_to_replication(self, txn, cache_name, keys): + """Notifies replication that given cache has been invalidated. + + Note that this does *not* invalidate the cache locally. + + Args: + txn + cache_name (str) + keys (iterable[str]) + """ + + if isinstance(self.database_engine, PostgresEngine): + # get_next() returns a context manager which is designed to wrap + # the transaction. However, we want to only get an ID when we want + # to use it, here, so we need to call __enter__ manually, and have + # __exit__ called after the transaction finishes. + ctx = self._cache_id_gen.get_next() + stream_id = ctx.__enter__() + txn.call_on_exception(ctx.__exit__, None, None, None) + txn.call_after(ctx.__exit__, None, None, None) + txn.call_after(self.hs.get_notifier().on_new_replication_data) + + self._simple_insert_txn( + txn, + table="cache_invalidation_stream", + values={ + "stream_id": stream_id, + "cache_func": cache_name, + "keys": list(keys), + "invalidation_ts": self.clock.time_msec(), + }, + ) + + def get_all_updated_caches(self, last_id, current_id, limit): + if last_id == current_id: + return defer.succeed([]) + + def get_all_updated_caches_txn(txn): + # We purposefully don't bound by the current token, as we want to + # send across cache invalidations as quickly as possible. Cache + # invalidations are idempotent, so duplicates are fine. + sql = ( + "SELECT stream_id, cache_func, keys, invalidation_ts" + " FROM cache_invalidation_stream" + " WHERE stream_id > ? ORDER BY stream_id ASC LIMIT ?" + ) + txn.execute(sql, (last_id, limit)) + return txn.fetchall() + + return self.runInteraction("get_all_updated_caches", get_all_updated_caches_txn) + + def get_cache_stream_token(self): + if self._cache_id_gen: + return self._cache_id_gen.get_current_token() + else: + return 0 diff --git a/synapse/storage/data_stores/main/client_ips.py b/synapse/storage/data_stores/main/client_ips.py index 7931b876ce..cae93b0e22 100644 --- a/synapse/storage/data_stores/main/client_ips.py +++ b/synapse/storage/data_stores/main/client_ips.py @@ -21,8 +21,8 @@ from twisted.internet import defer from synapse.metrics.background_process_metrics import wrap_as_background_process from synapse.storage import background_updates -from synapse.util.caches.descriptors import Cache from synapse.util.caches import CACHE_SIZE_FACTOR +from synapse.util.caches.descriptors import Cache logger = logging.getLogger(__name__) diff --git a/synapse/storage/data_stores/main/devices.py b/synapse/storage/data_stores/main/devices.py index b50ee026a2..a3ad23e783 100644 --- a/synapse/storage/data_stores/main/devices.py +++ b/synapse/storage/data_stores/main/devices.py @@ -30,16 +30,16 @@ from synapse.logging.opentracing import ( whitelisted_homeserver, ) from synapse.metrics.background_process_metrics import run_as_background_process -from synapse.storage._base import ( - SQLBaseStore, - db_to_json, - make_in_list_sql_clause, -) -from synapse.util.caches.descriptors import Cache +from synapse.storage._base import SQLBaseStore, db_to_json, make_in_list_sql_clause from synapse.storage.background_updates import BackgroundUpdateStore from synapse.types import get_verify_key_from_cross_signing_key from synapse.util import batch_iter -from synapse.util.caches.descriptors import cached, cachedInlineCallbacks, cachedList +from synapse.util.caches.descriptors import ( + Cache, + cached, + cachedInlineCallbacks, + cachedList, +) logger = logging.getLogger(__name__) From a785a2febe6783bfa800504c6750028bc61c84ea Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 3 Dec 2019 14:32:00 +0000 Subject: [PATCH 0584/1623] Newsfile --- changelog.d/6454.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6454.misc diff --git a/changelog.d/6454.misc b/changelog.d/6454.misc new file mode 100644 index 0000000000..9e5259157c --- /dev/null +++ b/changelog.d/6454.misc @@ -0,0 +1 @@ +Move data store specific code out of `SQLBaseStore`. From 00f0d67566cdfe8eae44aeae1c982c42a255cfcd Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 4 Dec 2019 10:45:59 +0000 Subject: [PATCH 0585/1623] Move get_user_count_txn out of base store --- synapse/storage/_base.py | 12 ------------ synapse/storage/data_stores/main/__init__.py | 12 ++++++++++++ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py index c02248cfe9..90019c8b0a 100644 --- a/synapse/storage/_base.py +++ b/synapse/storage/_base.py @@ -1440,18 +1440,6 @@ class SQLBaseStore(object): return cls.cursor_to_dict(txn) - def get_user_count_txn(self, txn): - """Get a total number of registered users in the users list. - - Args: - txn : Transaction object - Returns: - int : number of users - """ - sql_count = "SELECT COUNT(*) FROM users WHERE is_guest = 0;" - txn.execute(sql_count) - return txn.fetchone()[0] - def _simple_search_list( self, table, term, col, retcols, desc="_simple_search_list" ): diff --git a/synapse/storage/data_stores/main/__init__.py b/synapse/storage/data_stores/main/__init__.py index 474924c68f..76315935dd 100644 --- a/synapse/storage/data_stores/main/__init__.py +++ b/synapse/storage/data_stores/main/__init__.py @@ -516,6 +516,18 @@ class DataStore( retval = {"users": users, "total": count} return retval + def get_user_count_txn(self, txn): + """Get a total number of registered users in the users list. + + Args: + txn : Transaction object + Returns: + int : number of users + """ + sql_count = "SELECT COUNT(*) FROM users WHERE is_guest = 0;" + txn.execute(sql_count) + return txn.fetchone()[0] + def search_users(self, term): """Function to search users list for one or more users with the matched term. From a7f20500ff39399634d4623e284fb2f9892776ae Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 4 Dec 2019 15:45:42 +0000 Subject: [PATCH 0586/1623] _CURRENT_STATE_CACHE_NAME is public --- synapse/replication/slave/storage/_base.py | 4 ++-- synapse/storage/data_stores/main/cache.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/synapse/replication/slave/storage/_base.py b/synapse/replication/slave/storage/_base.py index 71e5877aca..6ece1d6745 100644 --- a/synapse/replication/slave/storage/_base.py +++ b/synapse/replication/slave/storage/_base.py @@ -19,7 +19,7 @@ from typing import Dict import six from synapse.storage._base import SQLBaseStore -from synapse.storage.data_stores.main.cache import _CURRENT_STATE_CACHE_NAME +from synapse.storage.data_stores.main.cache import CURRENT_STATE_CACHE_NAME from synapse.storage.engines import PostgresEngine from ._slaved_id_tracker import SlavedIdTracker @@ -63,7 +63,7 @@ class BaseSlavedStore(SQLBaseStore): if stream_name == "caches": self._cache_id_gen.advance(token) for row in rows: - if row.cache_func == _CURRENT_STATE_CACHE_NAME: + if row.cache_func == CURRENT_STATE_CACHE_NAME: room_id = row.keys[0] members_changed = set(row.keys[1:]) self._invalidate_state_caches(room_id, members_changed) diff --git a/synapse/storage/data_stores/main/cache.py b/synapse/storage/data_stores/main/cache.py index 6efcc5f3b0..258c08722a 100644 --- a/synapse/storage/data_stores/main/cache.py +++ b/synapse/storage/data_stores/main/cache.py @@ -28,7 +28,7 @@ logger = logging.getLogger(__name__) # This is a special cache name we use to batch multiple invalidations of caches # based on the current state when notifying workers over replication. -_CURRENT_STATE_CACHE_NAME = "cs_cache_fake" +CURRENT_STATE_CACHE_NAME = "cs_cache_fake" class CacheInvalidationStore(SQLBaseStore): @@ -65,12 +65,12 @@ class CacheInvalidationStore(SQLBaseStore): for chunk in batch_iter(members_changed, 50): keys = itertools.chain([room_id], chunk) self._send_invalidation_to_replication( - txn, _CURRENT_STATE_CACHE_NAME, keys + txn, CURRENT_STATE_CACHE_NAME, keys ) else: # if no members changed, we still need to invalidate the other caches. self._send_invalidation_to_replication( - txn, _CURRENT_STATE_CACHE_NAME, [room_id] + txn, CURRENT_STATE_CACHE_NAME, [room_id] ) def _send_invalidation_to_replication(self, txn, cache_name, keys): From 9186c105a0d49c60582ff93cf108e511334c57fd Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 4 Dec 2019 15:46:19 +0000 Subject: [PATCH 0587/1623] Revert "Move get_user_count_txn out of base store" This reverts commit 00f0d67566cdfe8eae44aeae1c982c42a255cfcd. Its going to get removed soon, so lets not make merge conflicts. --- synapse/storage/_base.py | 12 ++++++++++++ synapse/storage/data_stores/main/__init__.py | 12 ------------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py index 90019c8b0a..c02248cfe9 100644 --- a/synapse/storage/_base.py +++ b/synapse/storage/_base.py @@ -1440,6 +1440,18 @@ class SQLBaseStore(object): return cls.cursor_to_dict(txn) + def get_user_count_txn(self, txn): + """Get a total number of registered users in the users list. + + Args: + txn : Transaction object + Returns: + int : number of users + """ + sql_count = "SELECT COUNT(*) FROM users WHERE is_guest = 0;" + txn.execute(sql_count) + return txn.fetchone()[0] + def _simple_search_list( self, table, term, col, retcols, desc="_simple_search_list" ): diff --git a/synapse/storage/data_stores/main/__init__.py b/synapse/storage/data_stores/main/__init__.py index 76315935dd..474924c68f 100644 --- a/synapse/storage/data_stores/main/__init__.py +++ b/synapse/storage/data_stores/main/__init__.py @@ -516,18 +516,6 @@ class DataStore( retval = {"users": users, "total": count} return retval - def get_user_count_txn(self, txn): - """Get a total number of registered users in the users list. - - Args: - txn : Transaction object - Returns: - int : number of users - """ - sql_count = "SELECT COUNT(*) FROM users WHERE is_guest = 0;" - txn.execute(sql_count) - return txn.fetchone()[0] - def search_users(self, term): """Function to search users list for one or more users with the matched term. From c2f525a5251f4cbaef0cf34d6c69b42356c1f8af Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 4 Dec 2019 10:16:44 +0000 Subject: [PATCH 0588/1623] Don't call SQLBaseStore methods from outside stores --- synapse/app/homeserver.py | 4 ++-- synapse/push/bulk_push_rule_evaluator.py | 10 +--------- synapse/storage/_base.py | 8 -------- synapse/storage/data_stores/main/roommember.py | 17 +++++++++++++++++ 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py index 883b3fb70b..267aebaae9 100644 --- a/synapse/app/homeserver.py +++ b/synapse/app/homeserver.py @@ -542,8 +542,8 @@ def phone_stats_home(hs, stats, stats_process=_stats_process): # Database version # - stats["database_engine"] = hs.get_datastore().database_engine_name - stats["database_server_version"] = hs.get_datastore().get_server_version() + stats["database_engine"] = hs.database_engine.module.__name__ + stats["database_server_version"] = hs.database_engine.server_version logger.info("Reporting stats to %s: %s" % (hs.config.report_stats_endpoint, stats)) try: yield hs.get_proxied_http_client().put_json( diff --git a/synapse/push/bulk_push_rule_evaluator.py b/synapse/push/bulk_push_rule_evaluator.py index 1ba7bcd4d8..7881780760 100644 --- a/synapse/push/bulk_push_rule_evaluator.py +++ b/synapse/push/bulk_push_rule_evaluator.py @@ -386,15 +386,7 @@ class RulesForRoom(object): """ sequence = self.sequence - rows = yield self.store._simple_select_many_batch( - table="room_memberships", - column="event_id", - iterable=member_event_ids.values(), - retcols=("user_id", "membership", "event_id"), - keyvalues={}, - batch_size=500, - desc="_get_rules_for_member_event_ids", - ) + rows = yield self.store.get_membership_from_event_ids(member_event_ids.values()) members = {row["event_id"]: (row["user_id"], row["membership"]) for row in rows} diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py index c02248cfe9..1ed89d9f2a 100644 --- a/synapse/storage/_base.py +++ b/synapse/storage/_base.py @@ -1496,14 +1496,6 @@ class SQLBaseStore(object): return cls.cursor_to_dict(txn) - @property - def database_engine_name(self): - return self.database_engine.module.__name__ - - def get_server_version(self): - """Returns a string describing the server version number""" - return self.database_engine.server_version - class _RollbackButIsFineException(Exception): """ This exception is used to rollback a transaction without implying diff --git a/synapse/storage/data_stores/main/roommember.py b/synapse/storage/data_stores/main/roommember.py index 2af24a20b7..b314d75941 100644 --- a/synapse/storage/data_stores/main/roommember.py +++ b/synapse/storage/data_stores/main/roommember.py @@ -15,6 +15,7 @@ # limitations under the License. import logging +from typing import Iterable, List from six import iteritems, itervalues @@ -813,6 +814,22 @@ class RoomMemberWorkerStore(EventsWorkerStore): return set(room_ids) + def get_membership_from_event_ids( + self, member_event_ids: Iterable[str] + ) -> List[dict]: + """Get user_id and membership of a set of event IDs. + """ + + return self._simple_select_many_batch( + table="room_memberships", + column="event_id", + iterable=member_event_ids, + retcols=("user_id", "membership", "event_id"), + keyvalues={}, + batch_size=500, + desc="get_membership_from_event_ids", + ) + class RoomMemberBackgroundUpdateStore(BackgroundUpdateStore): def __init__(self, db_conn, hs): From ee86abb2d6e9c7d553858e814b4343bcf95af75a Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 4 Dec 2019 10:15:55 +0000 Subject: [PATCH 0589/1623] Remove underscore from SQLBaseStore functions --- scripts-dev/hash_history.py | 2 +- scripts/synapse_port_db | 18 +-- synapse/app/user_dir.py | 2 +- synapse/storage/_base.py | 142 +++++++++--------- synapse/storage/background_updates.py | 14 +- synapse/storage/data_stores/main/__init__.py | 16 +- .../storage/data_stores/main/account_data.py | 18 +-- .../storage/data_stores/main/appservice.py | 10 +- synapse/storage/data_stores/main/cache.py | 2 +- .../storage/data_stores/main/client_ips.py | 8 +- .../storage/data_stores/main/deviceinbox.py | 4 +- synapse/storage/data_stores/main/devices.py | 46 +++--- synapse/storage/data_stores/main/directory.py | 12 +- .../storage/data_stores/main/e2e_room_keys.py | 20 +-- .../data_stores/main/end_to_end_keys.py | 20 +-- .../data_stores/main/event_federation.py | 16 +- .../data_stores/main/event_push_actions.py | 12 +- synapse/storage/data_stores/main/events.py | 52 +++---- .../data_stores/main/events_bg_updates.py | 10 +- .../storage/data_stores/main/events_worker.py | 6 +- synapse/storage/data_stores/main/filtering.py | 2 +- .../storage/data_stores/main/group_server.py | 128 ++++++++-------- synapse/storage/data_stores/main/keys.py | 6 +- .../data_stores/main/media_repository.py | 24 +-- .../data_stores/main/monthly_active_users.py | 6 +- synapse/storage/data_stores/main/openid.py | 2 +- synapse/storage/data_stores/main/presence.py | 8 +- synapse/storage/data_stores/main/profile.py | 24 +-- synapse/storage/data_stores/main/push_rule.py | 24 +-- synapse/storage/data_stores/main/pusher.py | 26 ++-- synapse/storage/data_stores/main/receipts.py | 16 +- .../storage/data_stores/main/registration.py | 92 ++++++------ .../storage/data_stores/main/rejections.py | 4 +- synapse/storage/data_stores/main/relations.py | 4 +- synapse/storage/data_stores/main/room.py | 34 ++--- .../storage/data_stores/main/roommember.py | 16 +- synapse/storage/data_stores/main/search.py | 8 +- .../storage/data_stores/main/signatures.py | 2 +- synapse/storage/data_stores/main/state.py | 36 ++--- .../storage/data_stores/main/state_deltas.py | 2 +- synapse/storage/data_stores/main/stats.py | 28 ++-- synapse/storage/data_stores/main/stream.py | 14 +- synapse/storage/data_stores/main/tags.py | 6 +- .../storage/data_stores/main/transactions.py | 12 +- .../data_stores/main/user_directory.py | 56 ++++--- .../data_stores/main/user_erasure_store.py | 4 +- tests/handlers/test_stats.py | 30 ++-- tests/handlers/test_user_directory.py | 12 +- tests/rest/admin/test_admin.py | 2 +- tests/storage/test__base.py | 8 +- tests/storage/test_base.py | 18 +-- tests/storage/test_client_ips.py | 12 +- tests/storage/test_event_push_actions.py | 4 +- tests/storage/test_redaction.py | 4 +- tests/storage/test_roommember.py | 2 +- tests/unittest.py | 2 +- 56 files changed, 550 insertions(+), 558 deletions(-) diff --git a/scripts-dev/hash_history.py b/scripts-dev/hash_history.py index d20f6db176..bf3862a386 100644 --- a/scripts-dev/hash_history.py +++ b/scripts-dev/hash_history.py @@ -27,7 +27,7 @@ class Store(object): "_store_pdu_reference_hash_txn" ] _store_prev_pdu_hash_txn = SignatureStore.__dict__["_store_prev_pdu_hash_txn"] - _simple_insert_txn = SQLBaseStore.__dict__["_simple_insert_txn"] + simple_insert_txn = SQLBaseStore.__dict__["simple_insert_txn"] store = Store() diff --git a/scripts/synapse_port_db b/scripts/synapse_port_db index f24b8ffe67..9dd1700ff0 100755 --- a/scripts/synapse_port_db +++ b/scripts/synapse_port_db @@ -221,7 +221,7 @@ class Porter(object): def setup_table(self, table): if table in APPEND_ONLY_TABLES: # It's safe to just carry on inserting. - row = yield self.postgres_store._simple_select_one( + row = yield self.postgres_store.simple_select_one( table="port_from_sqlite3", keyvalues={"table_name": table}, retcols=("forward_rowid", "backward_rowid"), @@ -236,7 +236,7 @@ class Porter(object): ) backward_chunk = 0 else: - yield self.postgres_store._simple_insert( + yield self.postgres_store.simple_insert( table="port_from_sqlite3", values={ "table_name": table, @@ -266,7 +266,7 @@ class Porter(object): yield self.postgres_store.execute(delete_all) - yield self.postgres_store._simple_insert( + yield self.postgres_store.simple_insert( table="port_from_sqlite3", values={"table_name": table, "forward_rowid": 1, "backward_rowid": 0}, ) @@ -320,7 +320,7 @@ class Porter(object): if table == "user_directory_stream_pos": # We need to make sure there is a single row, `(X, null), as that is # what synapse expects to be there. - yield self.postgres_store._simple_insert( + yield self.postgres_store.simple_insert( table=table, values={"stream_id": None} ) self.progress.update(table, table_size) # Mark table as done @@ -375,7 +375,7 @@ class Porter(object): def insert(txn): self.postgres_store.insert_many_txn(txn, table, headers[1:], rows) - self.postgres_store._simple_update_one_txn( + self.postgres_store.simple_update_one_txn( txn, table="port_from_sqlite3", keyvalues={"table_name": table}, @@ -452,7 +452,7 @@ class Porter(object): ], ) - self.postgres_store._simple_update_one_txn( + self.postgres_store.simple_update_one_txn( txn, table="port_from_sqlite3", keyvalues={"table_name": "event_search"}, @@ -591,11 +591,11 @@ class Porter(object): # Step 2. Get tables. self.progress.set_state("Fetching tables") - sqlite_tables = yield self.sqlite_store._simple_select_onecol( + sqlite_tables = yield self.sqlite_store.simple_select_onecol( table="sqlite_master", keyvalues={"type": "table"}, retcol="name" ) - postgres_tables = yield self.postgres_store._simple_select_onecol( + postgres_tables = yield self.postgres_store.simple_select_onecol( table="information_schema.tables", keyvalues={}, retcol="distinct table_name", @@ -722,7 +722,7 @@ class Porter(object): next_chunk = yield self.sqlite_store.execute(get_start_id) next_chunk = max(max_inserted_rowid + 1, next_chunk) - yield self.postgres_store._simple_insert( + yield self.postgres_store.simple_insert( table="port_from_sqlite3", values={ "table_name": "sent_transactions", diff --git a/synapse/app/user_dir.py b/synapse/app/user_dir.py index 6cb100319f..0fa2b50999 100644 --- a/synapse/app/user_dir.py +++ b/synapse/app/user_dir.py @@ -64,7 +64,7 @@ class UserDirectorySlaveStore( super(UserDirectorySlaveStore, self).__init__(db_conn, hs) events_max = self._stream_id_gen.get_current_token() - curr_state_delta_prefill, min_curr_state_delta_id = self._get_cache_dict( + curr_state_delta_prefill, min_curr_state_delta_id = self.get_cache_dict( db_conn, "current_state_delta_stream", entity_column="room_id", diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py index 1ed89d9f2a..9205e550bb 100644 --- a/synapse/storage/_base.py +++ b/synapse/storage/_base.py @@ -262,7 +262,7 @@ class SQLBaseStore(object): If the background updates have not completed, wait 15 sec and check again. """ - updates = yield self._simple_select_list( + updates = yield self.simple_select_list( "background_updates", keyvalues=None, retcols=["update_name"], @@ -307,7 +307,7 @@ class SQLBaseStore(object): self._clock.looping_call(loop, 10000) - def _new_transaction( + def new_transaction( self, conn, desc, after_callbacks, exception_callbacks, func, *args, **kwargs ): start = monotonic_time() @@ -444,7 +444,7 @@ class SQLBaseStore(object): try: result = yield self.runWithConnection( - self._new_transaction, + self.new_transaction, desc, after_callbacks, exception_callbacks, @@ -516,7 +516,7 @@ class SQLBaseStore(object): results = list(dict(zip(col_headers, row)) for row in cursor) return results - def _execute(self, desc, decoder, query, *args): + def execute(self, desc, decoder, query, *args): """Runs a single query for a result set. Args: @@ -541,7 +541,7 @@ class SQLBaseStore(object): # no complex WHERE clauses, just a dict of values for columns. @defer.inlineCallbacks - def _simple_insert(self, table, values, or_ignore=False, desc="_simple_insert"): + def simple_insert(self, table, values, or_ignore=False, desc="simple_insert"): """Executes an INSERT query on the named table. Args: @@ -557,7 +557,7 @@ class SQLBaseStore(object): `or_ignore` is True """ try: - yield self.runInteraction(desc, self._simple_insert_txn, table, values) + yield self.runInteraction(desc, self.simple_insert_txn, table, values) except self.database_engine.module.IntegrityError: # We have to do or_ignore flag at this layer, since we can't reuse # a cursor after we receive an error from the db. @@ -567,7 +567,7 @@ class SQLBaseStore(object): return True @staticmethod - def _simple_insert_txn(txn, table, values): + def simple_insert_txn(txn, table, values): keys, vals = zip(*values.items()) sql = "INSERT INTO %s (%s) VALUES(%s)" % ( @@ -578,11 +578,11 @@ class SQLBaseStore(object): txn.execute(sql, vals) - def _simple_insert_many(self, table, values, desc): - return self.runInteraction(desc, self._simple_insert_many_txn, table, values) + def simple_insert_many(self, table, values, desc): + return self.runInteraction(desc, self.simple_insert_many_txn, table, values) @staticmethod - def _simple_insert_many_txn(txn, table, values): + def simple_insert_many_txn(txn, table, values): if not values: return @@ -611,13 +611,13 @@ class SQLBaseStore(object): txn.executemany(sql, vals) @defer.inlineCallbacks - def _simple_upsert( + def simple_upsert( self, table, keyvalues, values, insertion_values={}, - desc="_simple_upsert", + desc="simple_upsert", lock=True, ): """ @@ -649,7 +649,7 @@ class SQLBaseStore(object): try: result = yield self.runInteraction( desc, - self._simple_upsert_txn, + self.simple_upsert_txn, table, keyvalues, values, @@ -669,7 +669,7 @@ class SQLBaseStore(object): "IntegrityError when upserting into %s; retrying: %s", table, e ) - def _simple_upsert_txn( + def simple_upsert_txn( self, txn, table, keyvalues, values, insertion_values={}, lock=True ): """ @@ -693,11 +693,11 @@ class SQLBaseStore(object): self.database_engine.can_native_upsert and table not in self._unsafe_to_upsert_tables ): - return self._simple_upsert_txn_native_upsert( + return self.simple_upsert_txn_native_upsert( txn, table, keyvalues, values, insertion_values=insertion_values ) else: - return self._simple_upsert_txn_emulated( + return self.simple_upsert_txn_emulated( txn, table, keyvalues, @@ -706,7 +706,7 @@ class SQLBaseStore(object): lock=lock, ) - def _simple_upsert_txn_emulated( + def simple_upsert_txn_emulated( self, txn, table, keyvalues, values, insertion_values={}, lock=True ): """ @@ -775,7 +775,7 @@ class SQLBaseStore(object): # successfully inserted return True - def _simple_upsert_txn_native_upsert( + def simple_upsert_txn_native_upsert( self, txn, table, keyvalues, values, insertion_values={} ): """ @@ -809,7 +809,7 @@ class SQLBaseStore(object): ) txn.execute(sql, list(allvalues.values())) - def _simple_upsert_many_txn( + def simple_upsert_many_txn( self, txn, table, key_names, key_values, value_names, value_values ): """ @@ -829,15 +829,15 @@ class SQLBaseStore(object): self.database_engine.can_native_upsert and table not in self._unsafe_to_upsert_tables ): - return self._simple_upsert_many_txn_native_upsert( + return self.simple_upsert_many_txn_native_upsert( txn, table, key_names, key_values, value_names, value_values ) else: - return self._simple_upsert_many_txn_emulated( + return self.simple_upsert_many_txn_emulated( txn, table, key_names, key_values, value_names, value_values ) - def _simple_upsert_many_txn_emulated( + def simple_upsert_many_txn_emulated( self, txn, table, key_names, key_values, value_names, value_values ): """ @@ -862,9 +862,9 @@ class SQLBaseStore(object): _keys = {x: y for x, y in zip(key_names, keyv)} _vals = {x: y for x, y in zip(value_names, valv)} - self._simple_upsert_txn_emulated(txn, table, _keys, _vals) + self.simple_upsert_txn_emulated(txn, table, _keys, _vals) - def _simple_upsert_many_txn_native_upsert( + def simple_upsert_many_txn_native_upsert( self, txn, table, key_names, key_values, value_names, value_values ): """ @@ -909,8 +909,8 @@ class SQLBaseStore(object): return txn.execute_batch(sql, args) - def _simple_select_one( - self, table, keyvalues, retcols, allow_none=False, desc="_simple_select_one" + def simple_select_one( + self, table, keyvalues, retcols, allow_none=False, desc="simple_select_one" ): """Executes a SELECT query on the named table, which is expected to return a single row, returning multiple columns from it. @@ -924,16 +924,16 @@ class SQLBaseStore(object): statement returns no rows """ return self.runInteraction( - desc, self._simple_select_one_txn, table, keyvalues, retcols, allow_none + desc, self.simple_select_one_txn, table, keyvalues, retcols, allow_none ) - def _simple_select_one_onecol( + def simple_select_one_onecol( self, table, keyvalues, retcol, allow_none=False, - desc="_simple_select_one_onecol", + desc="simple_select_one_onecol", ): """Executes a SELECT query on the named table, which is expected to return a single row, returning a single column from it. @@ -945,7 +945,7 @@ class SQLBaseStore(object): """ return self.runInteraction( desc, - self._simple_select_one_onecol_txn, + self.simple_select_one_onecol_txn, table, keyvalues, retcol, @@ -953,10 +953,10 @@ class SQLBaseStore(object): ) @classmethod - def _simple_select_one_onecol_txn( + def simple_select_one_onecol_txn( cls, txn, table, keyvalues, retcol, allow_none=False ): - ret = cls._simple_select_onecol_txn( + ret = cls.simple_select_onecol_txn( txn, table=table, keyvalues=keyvalues, retcol=retcol ) @@ -969,7 +969,7 @@ class SQLBaseStore(object): raise StoreError(404, "No row found") @staticmethod - def _simple_select_onecol_txn(txn, table, keyvalues, retcol): + def simple_select_onecol_txn(txn, table, keyvalues, retcol): sql = ("SELECT %(retcol)s FROM %(table)s") % {"retcol": retcol, "table": table} if keyvalues: @@ -980,8 +980,8 @@ class SQLBaseStore(object): return [r[0] for r in txn] - def _simple_select_onecol( - self, table, keyvalues, retcol, desc="_simple_select_onecol" + def simple_select_onecol( + self, table, keyvalues, retcol, desc="simple_select_onecol" ): """Executes a SELECT query on the named table, which returns a list comprising of the values of the named column from the selected rows. @@ -995,12 +995,10 @@ class SQLBaseStore(object): Deferred: Results in a list """ return self.runInteraction( - desc, self._simple_select_onecol_txn, table, keyvalues, retcol + desc, self.simple_select_onecol_txn, table, keyvalues, retcol ) - def _simple_select_list( - self, table, keyvalues, retcols, desc="_simple_select_list" - ): + def simple_select_list(self, table, keyvalues, retcols, desc="simple_select_list"): """Executes a SELECT query on the named table, which may return zero or more rows, returning the result as a list of dicts. @@ -1014,11 +1012,11 @@ class SQLBaseStore(object): defer.Deferred: resolves to list[dict[str, Any]] """ return self.runInteraction( - desc, self._simple_select_list_txn, table, keyvalues, retcols + desc, self.simple_select_list_txn, table, keyvalues, retcols ) @classmethod - def _simple_select_list_txn(cls, txn, table, keyvalues, retcols): + def simple_select_list_txn(cls, txn, table, keyvalues, retcols): """Executes a SELECT query on the named table, which may return zero or more rows, returning the result as a list of dicts. @@ -1044,14 +1042,14 @@ class SQLBaseStore(object): return cls.cursor_to_dict(txn) @defer.inlineCallbacks - def _simple_select_many_batch( + def simple_select_many_batch( self, table, column, iterable, retcols, keyvalues={}, - desc="_simple_select_many_batch", + desc="simple_select_many_batch", batch_size=100, ): """Executes a SELECT query on the named table, which may return zero or @@ -1080,7 +1078,7 @@ class SQLBaseStore(object): for chunk in chunks: rows = yield self.runInteraction( desc, - self._simple_select_many_txn, + self.simple_select_many_txn, table, column, chunk, @@ -1093,7 +1091,7 @@ class SQLBaseStore(object): return results @classmethod - def _simple_select_many_txn(cls, txn, table, column, iterable, keyvalues, retcols): + def simple_select_many_txn(cls, txn, table, column, iterable, keyvalues, retcols): """Executes a SELECT query on the named table, which may return zero or more rows, returning the result as a list of dicts. @@ -1126,13 +1124,13 @@ class SQLBaseStore(object): txn.execute(sql, values) return cls.cursor_to_dict(txn) - def _simple_update(self, table, keyvalues, updatevalues, desc): + def simple_update(self, table, keyvalues, updatevalues, desc): return self.runInteraction( - desc, self._simple_update_txn, table, keyvalues, updatevalues + desc, self.simple_update_txn, table, keyvalues, updatevalues ) @staticmethod - def _simple_update_txn(txn, table, keyvalues, updatevalues): + def simple_update_txn(txn, table, keyvalues, updatevalues): if keyvalues: where = "WHERE %s" % " AND ".join("%s = ?" % k for k in iterkeys(keyvalues)) else: @@ -1148,8 +1146,8 @@ class SQLBaseStore(object): return txn.rowcount - def _simple_update_one( - self, table, keyvalues, updatevalues, desc="_simple_update_one" + def simple_update_one( + self, table, keyvalues, updatevalues, desc="simple_update_one" ): """Executes an UPDATE query on the named table, setting new values for columns in a row matching the key values. @@ -1169,12 +1167,12 @@ class SQLBaseStore(object): the update column in the 'keyvalues' dict as well. """ return self.runInteraction( - desc, self._simple_update_one_txn, table, keyvalues, updatevalues + desc, self.simple_update_one_txn, table, keyvalues, updatevalues ) @classmethod - def _simple_update_one_txn(cls, txn, table, keyvalues, updatevalues): - rowcount = cls._simple_update_txn(txn, table, keyvalues, updatevalues) + def simple_update_one_txn(cls, txn, table, keyvalues, updatevalues): + rowcount = cls.simple_update_txn(txn, table, keyvalues, updatevalues) if rowcount == 0: raise StoreError(404, "No row found (%s)" % (table,)) @@ -1182,7 +1180,7 @@ class SQLBaseStore(object): raise StoreError(500, "More than one row matched (%s)" % (table,)) @staticmethod - def _simple_select_one_txn(txn, table, keyvalues, retcols, allow_none=False): + def simple_select_one_txn(txn, table, keyvalues, retcols, allow_none=False): select_sql = "SELECT %s FROM %s WHERE %s" % ( ", ".join(retcols), table, @@ -1201,7 +1199,7 @@ class SQLBaseStore(object): return dict(zip(retcols, row)) - def _simple_delete_one(self, table, keyvalues, desc="_simple_delete_one"): + def simple_delete_one(self, table, keyvalues, desc="simple_delete_one"): """Executes a DELETE query on the named table, expecting to delete a single row. @@ -1209,10 +1207,10 @@ class SQLBaseStore(object): table : string giving the table name keyvalues : dict of column names and values to select the row with """ - return self.runInteraction(desc, self._simple_delete_one_txn, table, keyvalues) + return self.runInteraction(desc, self.simple_delete_one_txn, table, keyvalues) @staticmethod - def _simple_delete_one_txn(txn, table, keyvalues): + def simple_delete_one_txn(txn, table, keyvalues): """Executes a DELETE query on the named table, expecting to delete a single row. @@ -1231,11 +1229,11 @@ class SQLBaseStore(object): if txn.rowcount > 1: raise StoreError(500, "More than one row matched (%s)" % (table,)) - def _simple_delete(self, table, keyvalues, desc): - return self.runInteraction(desc, self._simple_delete_txn, table, keyvalues) + def simple_delete(self, table, keyvalues, desc): + return self.runInteraction(desc, self.simple_delete_txn, table, keyvalues) @staticmethod - def _simple_delete_txn(txn, table, keyvalues): + def simple_delete_txn(txn, table, keyvalues): sql = "DELETE FROM %s WHERE %s" % ( table, " AND ".join("%s = ?" % (k,) for k in keyvalues), @@ -1244,13 +1242,13 @@ class SQLBaseStore(object): txn.execute(sql, list(keyvalues.values())) return txn.rowcount - def _simple_delete_many(self, table, column, iterable, keyvalues, desc): + def simple_delete_many(self, table, column, iterable, keyvalues, desc): return self.runInteraction( - desc, self._simple_delete_many_txn, table, column, iterable, keyvalues + desc, self.simple_delete_many_txn, table, column, iterable, keyvalues ) @staticmethod - def _simple_delete_many_txn(txn, table, column, iterable, keyvalues): + def simple_delete_many_txn(txn, table, column, iterable, keyvalues): """Executes a DELETE query on the named table. Filters rows by if value of `column` is in `iterable`. @@ -1283,7 +1281,7 @@ class SQLBaseStore(object): return txn.rowcount - def _get_cache_dict( + def get_cache_dict( self, db_conn, table, entity_column, stream_column, max_value, limit=100000 ): # Fetch a mapping of room_id -> max stream position for "recent" rooms. @@ -1349,7 +1347,7 @@ class SQLBaseStore(object): # which is fine. pass - def _simple_select_list_paginate( + def simple_select_list_paginate( self, table, keyvalues, @@ -1358,7 +1356,7 @@ class SQLBaseStore(object): limit, retcols, order_direction="ASC", - desc="_simple_select_list_paginate", + desc="simple_select_list_paginate", ): """ Executes a SELECT query on the named table with start and limit, @@ -1380,7 +1378,7 @@ class SQLBaseStore(object): """ return self.runInteraction( desc, - self._simple_select_list_paginate_txn, + self.simple_select_list_paginate_txn, table, keyvalues, orderby, @@ -1391,7 +1389,7 @@ class SQLBaseStore(object): ) @classmethod - def _simple_select_list_paginate_txn( + def simple_select_list_paginate_txn( cls, txn, table, @@ -1452,9 +1450,7 @@ class SQLBaseStore(object): txn.execute(sql_count) return txn.fetchone()[0] - def _simple_search_list( - self, table, term, col, retcols, desc="_simple_search_list" - ): + def simple_search_list(self, table, term, col, retcols, desc="simple_search_list"): """Executes a SELECT query on the named table, which may return zero or more rows, returning the result as a list of dicts. @@ -1469,11 +1465,11 @@ class SQLBaseStore(object): """ return self.runInteraction( - desc, self._simple_search_list_txn, table, term, col, retcols + desc, self.simple_search_list_txn, table, term, col, retcols ) @classmethod - def _simple_search_list_txn(cls, txn, table, term, col, retcols): + def simple_search_list_txn(cls, txn, table, term, col, retcols): """Executes a SELECT query on the named table, which may return zero or more rows, returning the result as a list of dicts. diff --git a/synapse/storage/background_updates.py b/synapse/storage/background_updates.py index 37d469ffd7..06955a0537 100644 --- a/synapse/storage/background_updates.py +++ b/synapse/storage/background_updates.py @@ -139,7 +139,7 @@ class BackgroundUpdateStore(SQLBaseStore): # otherwise, check if there are updates to be run. This is important, # as we may be running on a worker which doesn't perform the bg updates # itself, but still wants to wait for them to happen. - updates = yield self._simple_select_onecol( + updates = yield self.simple_select_onecol( "background_updates", keyvalues=None, retcol="1", @@ -161,7 +161,7 @@ class BackgroundUpdateStore(SQLBaseStore): if update_name in self._background_update_queue: return False - update_exists = await self._simple_select_one_onecol( + update_exists = await self.simple_select_one_onecol( "background_updates", keyvalues={"update_name": update_name}, retcol="1", @@ -184,7 +184,7 @@ class BackgroundUpdateStore(SQLBaseStore): no more work to do. """ if not self._background_update_queue: - updates = yield self._simple_select_list( + updates = yield self.simple_select_list( "background_updates", keyvalues=None, retcols=("update_name", "depends_on"), @@ -226,7 +226,7 @@ class BackgroundUpdateStore(SQLBaseStore): else: batch_size = self.DEFAULT_BACKGROUND_BATCH_SIZE - progress_json = yield self._simple_select_one_onecol( + progress_json = yield self.simple_select_one_onecol( "background_updates", keyvalues={"update_name": update_name}, retcol="progress_json", @@ -413,7 +413,7 @@ class BackgroundUpdateStore(SQLBaseStore): self._background_update_queue = [] progress_json = json.dumps(progress) - return self._simple_insert( + return self.simple_insert( "background_updates", {"update_name": update_name, "progress_json": progress_json}, ) @@ -429,7 +429,7 @@ class BackgroundUpdateStore(SQLBaseStore): self._background_update_queue = [ name for name in self._background_update_queue if name != update_name ] - return self._simple_delete_one( + return self.simple_delete_one( "background_updates", keyvalues={"update_name": update_name} ) @@ -444,7 +444,7 @@ class BackgroundUpdateStore(SQLBaseStore): progress_json = json.dumps(progress) - self._simple_update_one_txn( + self.simple_update_one_txn( txn, "background_updates", keyvalues={"update_name": update_name}, diff --git a/synapse/storage/data_stores/main/__init__.py b/synapse/storage/data_stores/main/__init__.py index 474924c68f..2a5b33dda1 100644 --- a/synapse/storage/data_stores/main/__init__.py +++ b/synapse/storage/data_stores/main/__init__.py @@ -173,7 +173,7 @@ class DataStore( self._presence_on_startup = self._get_active_presence(db_conn) - presence_cache_prefill, min_presence_val = self._get_cache_dict( + presence_cache_prefill, min_presence_val = self.get_cache_dict( db_conn, "presence_stream", entity_column="user_id", @@ -187,7 +187,7 @@ class DataStore( ) max_device_inbox_id = self._device_inbox_id_gen.get_current_token() - device_inbox_prefill, min_device_inbox_id = self._get_cache_dict( + device_inbox_prefill, min_device_inbox_id = self.get_cache_dict( db_conn, "device_inbox", entity_column="user_id", @@ -202,7 +202,7 @@ class DataStore( ) # The federation outbox and the local device inbox uses the same # stream_id generator. - device_outbox_prefill, min_device_outbox_id = self._get_cache_dict( + device_outbox_prefill, min_device_outbox_id = self.get_cache_dict( db_conn, "device_federation_outbox", entity_column="destination", @@ -228,7 +228,7 @@ class DataStore( ) events_max = self._stream_id_gen.get_current_token() - curr_state_delta_prefill, min_curr_state_delta_id = self._get_cache_dict( + curr_state_delta_prefill, min_curr_state_delta_id = self.get_cache_dict( db_conn, "current_state_delta_stream", entity_column="room_id", @@ -242,7 +242,7 @@ class DataStore( prefilled_cache=curr_state_delta_prefill, ) - _group_updates_prefill, min_group_updates_id = self._get_cache_dict( + _group_updates_prefill, min_group_updates_id = self.get_cache_dict( db_conn, "local_group_updates", entity_column="user_id", @@ -482,7 +482,7 @@ class DataStore( Returns: defer.Deferred: resolves to list[dict[str, Any]] """ - return self._simple_select_list( + return self.simple_select_list( table="users", keyvalues={}, retcols=["name", "password_hash", "is_guest", "admin", "user_type"], @@ -504,7 +504,7 @@ class DataStore( """ users = yield self.runInteraction( "get_users_paginate", - self._simple_select_list_paginate_txn, + self.simple_select_list_paginate_txn, table="users", keyvalues={"is_guest": False}, orderby=order, @@ -526,7 +526,7 @@ class DataStore( Returns: defer.Deferred: resolves to list[dict[str, Any]] """ - return self._simple_search_list( + return self.simple_search_list( table="users", term=term, col="name", diff --git a/synapse/storage/data_stores/main/account_data.py b/synapse/storage/data_stores/main/account_data.py index 22093484ed..b0d22faf3f 100644 --- a/synapse/storage/data_stores/main/account_data.py +++ b/synapse/storage/data_stores/main/account_data.py @@ -67,7 +67,7 @@ class AccountDataWorkerStore(SQLBaseStore): """ def get_account_data_for_user_txn(txn): - rows = self._simple_select_list_txn( + rows = self.simple_select_list_txn( txn, "account_data", {"user_id": user_id}, @@ -78,7 +78,7 @@ class AccountDataWorkerStore(SQLBaseStore): row["account_data_type"]: json.loads(row["content"]) for row in rows } - rows = self._simple_select_list_txn( + rows = self.simple_select_list_txn( txn, "room_account_data", {"user_id": user_id}, @@ -102,7 +102,7 @@ class AccountDataWorkerStore(SQLBaseStore): Returns: Deferred: A dict """ - result = yield self._simple_select_one_onecol( + result = yield self.simple_select_one_onecol( table="account_data", keyvalues={"user_id": user_id, "account_data_type": data_type}, retcol="content", @@ -127,7 +127,7 @@ class AccountDataWorkerStore(SQLBaseStore): """ def get_account_data_for_room_txn(txn): - rows = self._simple_select_list_txn( + rows = self.simple_select_list_txn( txn, "room_account_data", {"user_id": user_id, "room_id": room_id}, @@ -156,7 +156,7 @@ class AccountDataWorkerStore(SQLBaseStore): """ def get_account_data_for_room_and_type_txn(txn): - content_json = self._simple_select_one_onecol_txn( + content_json = self.simple_select_one_onecol_txn( txn, table="room_account_data", keyvalues={ @@ -300,9 +300,9 @@ class AccountDataStore(AccountDataWorkerStore): with self._account_data_id_gen.get_next() as next_id: # no need to lock here as room_account_data has a unique constraint - # on (user_id, room_id, account_data_type) so _simple_upsert will + # on (user_id, room_id, account_data_type) so simple_upsert will # retry if there is a conflict. - yield self._simple_upsert( + yield self.simple_upsert( desc="add_room_account_data", table="room_account_data", keyvalues={ @@ -346,9 +346,9 @@ class AccountDataStore(AccountDataWorkerStore): with self._account_data_id_gen.get_next() as next_id: # no need to lock here as account_data has a unique constraint on - # (user_id, account_data_type) so _simple_upsert will retry if + # (user_id, account_data_type) so simple_upsert will retry if # there is a conflict. - yield self._simple_upsert( + yield self.simple_upsert( desc="add_user_account_data", table="account_data", keyvalues={"user_id": user_id, "account_data_type": account_data_type}, diff --git a/synapse/storage/data_stores/main/appservice.py b/synapse/storage/data_stores/main/appservice.py index 81babf2029..6b82fd392a 100644 --- a/synapse/storage/data_stores/main/appservice.py +++ b/synapse/storage/data_stores/main/appservice.py @@ -133,7 +133,7 @@ class ApplicationServiceTransactionWorkerStore( A Deferred which resolves to a list of ApplicationServices, which may be empty. """ - results = yield self._simple_select_list( + results = yield self.simple_select_list( "application_services_state", dict(state=state), ["as_id"] ) # NB: This assumes this class is linked with ApplicationServiceStore @@ -155,7 +155,7 @@ class ApplicationServiceTransactionWorkerStore( Returns: A Deferred which resolves to ApplicationServiceState. """ - result = yield self._simple_select_one( + result = yield self.simple_select_one( "application_services_state", dict(as_id=service.id), ["state"], @@ -175,7 +175,7 @@ class ApplicationServiceTransactionWorkerStore( Returns: A Deferred which resolves when the state was set successfully. """ - return self._simple_upsert( + return self.simple_upsert( "application_services_state", dict(as_id=service.id), dict(state=state) ) @@ -249,7 +249,7 @@ class ApplicationServiceTransactionWorkerStore( ) # Set current txn_id for AS to 'txn_id' - self._simple_upsert_txn( + self.simple_upsert_txn( txn, "application_services_state", dict(as_id=service.id), @@ -257,7 +257,7 @@ class ApplicationServiceTransactionWorkerStore( ) # Delete txn - self._simple_delete_txn( + self.simple_delete_txn( txn, "application_services_txns", dict(txn_id=txn_id, as_id=service.id) ) diff --git a/synapse/storage/data_stores/main/cache.py b/synapse/storage/data_stores/main/cache.py index 258c08722a..de3256049d 100644 --- a/synapse/storage/data_stores/main/cache.py +++ b/synapse/storage/data_stores/main/cache.py @@ -95,7 +95,7 @@ class CacheInvalidationStore(SQLBaseStore): txn.call_after(ctx.__exit__, None, None, None) txn.call_after(self.hs.get_notifier().on_new_replication_data) - self._simple_insert_txn( + self.simple_insert_txn( txn, table="cache_invalidation_stream", values={ diff --git a/synapse/storage/data_stores/main/client_ips.py b/synapse/storage/data_stores/main/client_ips.py index cae93b0e22..66522a04b7 100644 --- a/synapse/storage/data_stores/main/client_ips.py +++ b/synapse/storage/data_stores/main/client_ips.py @@ -431,7 +431,7 @@ class ClientIpStore(ClientIpBackgroundUpdateStore): (user_id, access_token, ip), (user_agent, device_id, last_seen) = entry try: - self._simple_upsert_txn( + self.simple_upsert_txn( txn, table="user_ips", keyvalues={ @@ -450,7 +450,7 @@ class ClientIpStore(ClientIpBackgroundUpdateStore): # Technically an access token might not be associated with # a device so we need to check. if device_id: - self._simple_upsert_txn( + self.simple_upsert_txn( txn, table="devices", keyvalues={"user_id": user_id, "device_id": device_id}, @@ -483,7 +483,7 @@ class ClientIpStore(ClientIpBackgroundUpdateStore): if device_id is not None: keyvalues["device_id"] = device_id - res = yield self._simple_select_list( + res = yield self.simple_select_list( table="devices", keyvalues=keyvalues, retcols=("user_id", "ip", "user_agent", "device_id", "last_seen"), @@ -516,7 +516,7 @@ class ClientIpStore(ClientIpBackgroundUpdateStore): user_agent, _, last_seen = self._batch_row_update[key] results[(access_token, ip)] = (user_agent, last_seen) - rows = yield self._simple_select_list( + rows = yield self.simple_select_list( table="user_ips", keyvalues={"user_id": user_id}, retcols=["access_token", "ip", "user_agent", "last_seen"], diff --git a/synapse/storage/data_stores/main/deviceinbox.py b/synapse/storage/data_stores/main/deviceinbox.py index a23744f11c..206d39134d 100644 --- a/synapse/storage/data_stores/main/deviceinbox.py +++ b/synapse/storage/data_stores/main/deviceinbox.py @@ -314,7 +314,7 @@ class DeviceInboxStore(DeviceInboxWorkerStore, DeviceInboxBackgroundUpdateStore) # Check if we've already inserted a matching message_id for that # origin. This can happen if the origin doesn't receive our # acknowledgement from the first time we received the message. - already_inserted = self._simple_select_one_txn( + already_inserted = self.simple_select_one_txn( txn, table="device_federation_inbox", keyvalues={"origin": origin, "message_id": message_id}, @@ -326,7 +326,7 @@ class DeviceInboxStore(DeviceInboxWorkerStore, DeviceInboxBackgroundUpdateStore) # Add an entry for this message_id so that we know we've processed # it. - self._simple_insert_txn( + self.simple_insert_txn( txn, table="device_federation_inbox", values={ diff --git a/synapse/storage/data_stores/main/devices.py b/synapse/storage/data_stores/main/devices.py index a3ad23e783..727c582121 100644 --- a/synapse/storage/data_stores/main/devices.py +++ b/synapse/storage/data_stores/main/devices.py @@ -61,7 +61,7 @@ class DeviceWorkerStore(SQLBaseStore): Raises: StoreError: if the device is not found """ - return self._simple_select_one( + return self.simple_select_one( table="devices", keyvalues={"user_id": user_id, "device_id": device_id, "hidden": False}, retcols=("user_id", "device_id", "display_name"), @@ -80,7 +80,7 @@ class DeviceWorkerStore(SQLBaseStore): containing "device_id", "user_id" and "display_name" for each device. """ - devices = yield self._simple_select_list( + devices = yield self.simple_select_list( table="devices", keyvalues={"user_id": user_id, "hidden": False}, retcols=("user_id", "device_id", "display_name"), @@ -414,7 +414,7 @@ class DeviceWorkerStore(SQLBaseStore): from_user_id, stream_id, ) - self._simple_insert_txn( + self.simple_insert_txn( txn, "user_signature_stream", values={ @@ -466,7 +466,7 @@ class DeviceWorkerStore(SQLBaseStore): @cachedInlineCallbacks(num_args=2, tree=True) def _get_cached_user_device(self, user_id, device_id): - content = yield self._simple_select_one_onecol( + content = yield self.simple_select_one_onecol( table="device_lists_remote_cache", keyvalues={"user_id": user_id, "device_id": device_id}, retcol="content", @@ -476,7 +476,7 @@ class DeviceWorkerStore(SQLBaseStore): @cachedInlineCallbacks() def _get_cached_devices_for_user(self, user_id): - devices = yield self._simple_select_list( + devices = yield self.simple_select_list( table="device_lists_remote_cache", keyvalues={"user_id": user_id}, retcols=("device_id", "content"), @@ -584,7 +584,7 @@ class DeviceWorkerStore(SQLBaseStore): SELECT DISTINCT user_ids FROM user_signature_stream WHERE from_user_id = ? AND stream_id > ? """ - rows = yield self._execute( + rows = yield self.execute( "get_users_whose_signatures_changed", None, sql, user_id, from_key ) return set(user for row in rows for user in json.loads(row[0])) @@ -605,7 +605,7 @@ class DeviceWorkerStore(SQLBaseStore): WHERE ? < stream_id AND stream_id <= ? GROUP BY user_id, destination """ - return self._execute( + return self.execute( "get_all_device_list_changes_for_remotes", None, sql, from_key, to_key ) @@ -614,7 +614,7 @@ class DeviceWorkerStore(SQLBaseStore): """Get the last stream_id we got for a user. May be None if we haven't got any information for them. """ - return self._simple_select_one_onecol( + return self.simple_select_one_onecol( table="device_lists_remote_extremeties", keyvalues={"user_id": user_id}, retcol="stream_id", @@ -628,7 +628,7 @@ class DeviceWorkerStore(SQLBaseStore): inlineCallbacks=True, ) def get_device_list_last_stream_id_for_remotes(self, user_ids): - rows = yield self._simple_select_many_batch( + rows = yield self.simple_select_many_batch( table="device_lists_remote_extremeties", column="user_id", iterable=user_ids, @@ -722,7 +722,7 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): return False try: - inserted = yield self._simple_insert( + inserted = yield self.simple_insert( "devices", values={ "user_id": user_id, @@ -736,7 +736,7 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): if not inserted: # if the device already exists, check if it's a real device, or # if the device ID is reserved by something else - hidden = yield self._simple_select_one_onecol( + hidden = yield self.simple_select_one_onecol( "devices", keyvalues={"user_id": user_id, "device_id": device_id}, retcol="hidden", @@ -771,7 +771,7 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): Returns: defer.Deferred """ - yield self._simple_delete_one( + yield self.simple_delete_one( table="devices", keyvalues={"user_id": user_id, "device_id": device_id, "hidden": False}, desc="delete_device", @@ -789,7 +789,7 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): Returns: defer.Deferred """ - yield self._simple_delete_many( + yield self.simple_delete_many( table="devices", column="device_id", iterable=device_ids, @@ -818,7 +818,7 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): updates["display_name"] = new_display_name if not updates: return defer.succeed(None) - return self._simple_update_one( + return self.simple_update_one( table="devices", keyvalues={"user_id": user_id, "device_id": device_id, "hidden": False}, updatevalues=updates, @@ -829,7 +829,7 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): def mark_remote_user_device_list_as_unsubscribed(self, user_id): """Mark that we no longer track device lists for remote user. """ - yield self._simple_delete( + yield self.simple_delete( table="device_lists_remote_extremeties", keyvalues={"user_id": user_id}, desc="mark_remote_user_device_list_as_unsubscribed", @@ -866,7 +866,7 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): self, txn, user_id, device_id, content, stream_id ): if content.get("deleted"): - self._simple_delete_txn( + self.simple_delete_txn( txn, table="device_lists_remote_cache", keyvalues={"user_id": user_id, "device_id": device_id}, @@ -874,7 +874,7 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): txn.call_after(self.device_id_exists_cache.invalidate, (user_id, device_id)) else: - self._simple_upsert_txn( + self.simple_upsert_txn( txn, table="device_lists_remote_cache", keyvalues={"user_id": user_id, "device_id": device_id}, @@ -890,7 +890,7 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): self.get_device_list_last_stream_id_for_remote.invalidate, (user_id,) ) - self._simple_upsert_txn( + self.simple_upsert_txn( txn, table="device_lists_remote_extremeties", keyvalues={"user_id": user_id}, @@ -923,11 +923,11 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): ) def _update_remote_device_list_cache_txn(self, txn, user_id, devices, stream_id): - self._simple_delete_txn( + self.simple_delete_txn( txn, table="device_lists_remote_cache", keyvalues={"user_id": user_id} ) - self._simple_insert_many_txn( + self.simple_insert_many_txn( txn, table="device_lists_remote_cache", values=[ @@ -946,7 +946,7 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): self.get_device_list_last_stream_id_for_remote.invalidate, (user_id,) ) - self._simple_upsert_txn( + self.simple_upsert_txn( txn, table="device_lists_remote_extremeties", keyvalues={"user_id": user_id}, @@ -995,7 +995,7 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): [(user_id, device_id, stream_id) for device_id in device_ids], ) - self._simple_insert_many_txn( + self.simple_insert_many_txn( txn, table="device_lists_stream", values=[ @@ -1006,7 +1006,7 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): context = get_active_span_text_map() - self._simple_insert_many_txn( + self.simple_insert_many_txn( txn, table="device_lists_outbound_pokes", values=[ diff --git a/synapse/storage/data_stores/main/directory.py b/synapse/storage/data_stores/main/directory.py index 297966d9f4..d332f8a409 100644 --- a/synapse/storage/data_stores/main/directory.py +++ b/synapse/storage/data_stores/main/directory.py @@ -36,7 +36,7 @@ class DirectoryWorkerStore(SQLBaseStore): Deferred: results in namedtuple with keys "room_id" and "servers" or None if no association can be found """ - room_id = yield self._simple_select_one_onecol( + room_id = yield self.simple_select_one_onecol( "room_aliases", {"room_alias": room_alias.to_string()}, "room_id", @@ -47,7 +47,7 @@ class DirectoryWorkerStore(SQLBaseStore): if not room_id: return None - servers = yield self._simple_select_onecol( + servers = yield self.simple_select_onecol( "room_alias_servers", {"room_alias": room_alias.to_string()}, "server", @@ -60,7 +60,7 @@ class DirectoryWorkerStore(SQLBaseStore): return RoomAliasMapping(room_id, room_alias.to_string(), servers) def get_room_alias_creator(self, room_alias): - return self._simple_select_one_onecol( + return self.simple_select_one_onecol( table="room_aliases", keyvalues={"room_alias": room_alias}, retcol="creator", @@ -69,7 +69,7 @@ class DirectoryWorkerStore(SQLBaseStore): @cached(max_entries=5000) def get_aliases_for_room(self, room_id): - return self._simple_select_onecol( + return self.simple_select_onecol( "room_aliases", {"room_id": room_id}, "room_alias", @@ -93,7 +93,7 @@ class DirectoryStore(DirectoryWorkerStore): """ def alias_txn(txn): - self._simple_insert_txn( + self.simple_insert_txn( txn, "room_aliases", { @@ -103,7 +103,7 @@ class DirectoryStore(DirectoryWorkerStore): }, ) - self._simple_insert_many_txn( + self.simple_insert_many_txn( txn, table="room_alias_servers", values=[ diff --git a/synapse/storage/data_stores/main/e2e_room_keys.py b/synapse/storage/data_stores/main/e2e_room_keys.py index 113224fd7c..df89eda337 100644 --- a/synapse/storage/data_stores/main/e2e_room_keys.py +++ b/synapse/storage/data_stores/main/e2e_room_keys.py @@ -38,7 +38,7 @@ class EndToEndRoomKeyStore(SQLBaseStore): StoreError """ - yield self._simple_update_one( + yield self.simple_update_one( table="e2e_room_keys", keyvalues={ "user_id": user_id, @@ -89,7 +89,7 @@ class EndToEndRoomKeyStore(SQLBaseStore): } ) - yield self._simple_insert_many( + yield self.simple_insert_many( table="e2e_room_keys", values=values, desc="add_e2e_room_keys" ) @@ -125,7 +125,7 @@ class EndToEndRoomKeyStore(SQLBaseStore): if session_id: keyvalues["session_id"] = session_id - rows = yield self._simple_select_list( + rows = yield self.simple_select_list( table="e2e_room_keys", keyvalues=keyvalues, retcols=( @@ -234,7 +234,7 @@ class EndToEndRoomKeyStore(SQLBaseStore): version (str): the version ID of the backup we're querying about """ - return self._simple_select_one_onecol( + return self.simple_select_one_onecol( table="e2e_room_keys", keyvalues={"user_id": user_id, "version": version}, retcol="COUNT(*)", @@ -267,7 +267,7 @@ class EndToEndRoomKeyStore(SQLBaseStore): if session_id: keyvalues["session_id"] = session_id - yield self._simple_delete( + yield self.simple_delete( table="e2e_room_keys", keyvalues=keyvalues, desc="delete_e2e_room_keys" ) @@ -312,7 +312,7 @@ class EndToEndRoomKeyStore(SQLBaseStore): # it isn't there. raise StoreError(404, "No row found") - result = self._simple_select_one_txn( + result = self.simple_select_one_txn( txn, table="e2e_room_keys_versions", keyvalues={"user_id": user_id, "version": this_version, "deleted": 0}, @@ -352,7 +352,7 @@ class EndToEndRoomKeyStore(SQLBaseStore): new_version = str(int(current_version) + 1) - self._simple_insert_txn( + self.simple_insert_txn( txn, table="e2e_room_keys_versions", values={ @@ -391,7 +391,7 @@ class EndToEndRoomKeyStore(SQLBaseStore): updatevalues["etag"] = version_etag if updatevalues: - return self._simple_update( + return self.simple_update( table="e2e_room_keys_versions", keyvalues={"user_id": user_id, "version": version}, updatevalues=updatevalues, @@ -420,13 +420,13 @@ class EndToEndRoomKeyStore(SQLBaseStore): else: this_version = version - self._simple_delete_txn( + self.simple_delete_txn( txn, table="e2e_room_keys", keyvalues={"user_id": user_id, "version": this_version}, ) - return self._simple_update_one_txn( + return self.simple_update_one_txn( txn, table="e2e_room_keys_versions", keyvalues={"user_id": user_id, "version": this_version}, diff --git a/synapse/storage/data_stores/main/end_to_end_keys.py b/synapse/storage/data_stores/main/end_to_end_keys.py index 643327b57b..08bcdc4725 100644 --- a/synapse/storage/data_stores/main/end_to_end_keys.py +++ b/synapse/storage/data_stores/main/end_to_end_keys.py @@ -186,7 +186,7 @@ class EndToEndKeyWorkerStore(SQLBaseStore): key_id) to json string for key """ - rows = yield self._simple_select_many_batch( + rows = yield self.simple_select_many_batch( table="e2e_one_time_keys_json", column="key_id", iterable=key_ids, @@ -219,7 +219,7 @@ class EndToEndKeyWorkerStore(SQLBaseStore): # a unique constraint. If there is a race of two calls to # `add_e2e_one_time_keys` then they'll conflict and we will only # insert one set. - self._simple_insert_many_txn( + self.simple_insert_many_txn( txn, table="e2e_one_time_keys_json", values=[ @@ -350,7 +350,7 @@ class EndToEndKeyWorkerStore(SQLBaseStore): WHERE ? < stream_id AND stream_id <= ? GROUP BY user_id """ - return self._execute( + return self.execute( "get_all_user_signature_changes_for_remotes", None, sql, from_key, to_key ) @@ -367,7 +367,7 @@ class EndToEndKeyStore(EndToEndKeyWorkerStore, SQLBaseStore): set_tag("time_now", time_now) set_tag("device_keys", device_keys) - old_key_json = self._simple_select_one_onecol_txn( + old_key_json = self.simple_select_one_onecol_txn( txn, table="e2e_device_keys_json", keyvalues={"user_id": user_id, "device_id": device_id}, @@ -383,7 +383,7 @@ class EndToEndKeyStore(EndToEndKeyWorkerStore, SQLBaseStore): log_kv({"Message": "Device key already stored."}) return False - self._simple_upsert_txn( + self.simple_upsert_txn( txn, table="e2e_device_keys_json", keyvalues={"user_id": user_id, "device_id": device_id}, @@ -442,12 +442,12 @@ class EndToEndKeyStore(EndToEndKeyWorkerStore, SQLBaseStore): "user_id": user_id, } ) - self._simple_delete_txn( + self.simple_delete_txn( txn, table="e2e_device_keys_json", keyvalues={"user_id": user_id, "device_id": device_id}, ) - self._simple_delete_txn( + self.simple_delete_txn( txn, table="e2e_one_time_keys_json", keyvalues={"user_id": user_id, "device_id": device_id}, @@ -492,7 +492,7 @@ class EndToEndKeyStore(EndToEndKeyWorkerStore, SQLBaseStore): # The "keys" property must only have one entry, which will be the public # key, so we just grab the first value in there pubkey = next(iter(key["keys"].values())) - self._simple_insert_txn( + self.simple_insert_txn( txn, "devices", values={ @@ -505,7 +505,7 @@ class EndToEndKeyStore(EndToEndKeyWorkerStore, SQLBaseStore): # and finally, store the key itself with self._cross_signing_id_gen.get_next() as stream_id: - self._simple_insert_txn( + self.simple_insert_txn( txn, "e2e_cross_signing_keys", values={ @@ -539,7 +539,7 @@ class EndToEndKeyStore(EndToEndKeyWorkerStore, SQLBaseStore): user_id (str): the user who made the signatures signatures (iterable[SignatureListItem]): signatures to add """ - return self._simple_insert_many( + return self.simple_insert_many( "e2e_cross_signing_signatures", [ { diff --git a/synapse/storage/data_stores/main/event_federation.py b/synapse/storage/data_stores/main/event_federation.py index 90bef0cd2c..051ac7a8cb 100644 --- a/synapse/storage/data_stores/main/event_federation.py +++ b/synapse/storage/data_stores/main/event_federation.py @@ -126,7 +126,7 @@ class EventFederationWorkerStore(EventsWorkerStore, SignatureWorkerStore, SQLBas Returns Deferred[int] """ - rows = yield self._simple_select_many_batch( + rows = yield self.simple_select_many_batch( table="events", column="event_id", iterable=event_ids, @@ -140,7 +140,7 @@ class EventFederationWorkerStore(EventsWorkerStore, SignatureWorkerStore, SQLBas return max(row["depth"] for row in rows) def _get_oldest_events_in_room_txn(self, txn, room_id): - return self._simple_select_onecol_txn( + return self.simple_select_onecol_txn( txn, table="event_backward_extremities", keyvalues={"room_id": room_id}, @@ -235,7 +235,7 @@ class EventFederationWorkerStore(EventsWorkerStore, SignatureWorkerStore, SQLBas @cached(max_entries=5000, iterable=True) def get_latest_event_ids_in_room(self, room_id): - return self._simple_select_onecol( + return self.simple_select_onecol( table="event_forward_extremities", keyvalues={"room_id": room_id}, retcol="event_id", @@ -271,7 +271,7 @@ class EventFederationWorkerStore(EventsWorkerStore, SignatureWorkerStore, SQLBas ) def _get_min_depth_interaction(self, txn, room_id): - min_depth = self._simple_select_one_onecol_txn( + min_depth = self.simple_select_one_onecol_txn( txn, table="room_depth", keyvalues={"room_id": room_id}, @@ -383,7 +383,7 @@ class EventFederationWorkerStore(EventsWorkerStore, SignatureWorkerStore, SQLBas queue = PriorityQueue() for event_id in event_list: - depth = self._simple_select_one_onecol_txn( + depth = self.simple_select_one_onecol_txn( txn, table="events", keyvalues={"event_id": event_id, "room_id": room_id}, @@ -468,7 +468,7 @@ class EventFederationWorkerStore(EventsWorkerStore, SignatureWorkerStore, SQLBas Returns: Deferred[list[str]] """ - rows = yield self._simple_select_many_batch( + rows = yield self.simple_select_many_batch( table="event_edges", column="prev_event_id", iterable=event_ids, @@ -508,7 +508,7 @@ class EventFederationStore(EventFederationWorkerStore): if min_depth and depth >= min_depth: return - self._simple_upsert_txn( + self.simple_upsert_txn( txn, table="room_depth", keyvalues={"room_id": room_id}, @@ -520,7 +520,7 @@ class EventFederationStore(EventFederationWorkerStore): For the given event, update the event edges table and forward and backward extremities tables. """ - self._simple_insert_many_txn( + self.simple_insert_many_txn( txn, table="event_edges", values=[ diff --git a/synapse/storage/data_stores/main/event_push_actions.py b/synapse/storage/data_stores/main/event_push_actions.py index 04ce21ac66..0a37847cfd 100644 --- a/synapse/storage/data_stores/main/event_push_actions.py +++ b/synapse/storage/data_stores/main/event_push_actions.py @@ -441,7 +441,7 @@ class EventPushActionsWorkerStore(SQLBaseStore): ) def _add_push_actions_to_staging_txn(txn): - # We don't use _simple_insert_many here to avoid the overhead + # We don't use simple_insert_many here to avoid the overhead # of generating lists of dicts. sql = """ @@ -472,7 +472,7 @@ class EventPushActionsWorkerStore(SQLBaseStore): """ try: - res = yield self._simple_delete( + res = yield self.simple_delete( table="event_push_actions_staging", keyvalues={"event_id": event_id}, desc="remove_push_actions_from_staging", @@ -677,7 +677,7 @@ class EventPushActionsStore(EventPushActionsWorkerStore): ) for event, _ in events_and_contexts: - user_ids = self._simple_select_onecol_txn( + user_ids = self.simple_select_onecol_txn( txn, table="event_push_actions_staging", keyvalues={"event_id": event.event_id}, @@ -844,7 +844,7 @@ class EventPushActionsStore(EventPushActionsWorkerStore): the archiving process has caught up or not. """ - old_rotate_stream_ordering = self._simple_select_one_onecol_txn( + old_rotate_stream_ordering = self.simple_select_one_onecol_txn( txn, table="event_push_summary_stream_ordering", keyvalues={}, @@ -880,7 +880,7 @@ class EventPushActionsStore(EventPushActionsWorkerStore): return caught_up def _rotate_notifs_before_txn(self, txn, rotate_to_stream_ordering): - old_rotate_stream_ordering = self._simple_select_one_onecol_txn( + old_rotate_stream_ordering = self.simple_select_one_onecol_txn( txn, table="event_push_summary_stream_ordering", keyvalues={}, @@ -912,7 +912,7 @@ class EventPushActionsStore(EventPushActionsWorkerStore): # If the `old.user_id` above is NULL then we know there isn't already an # entry in the table, so we simply insert it. Otherwise we update the # existing table. - self._simple_insert_many_txn( + self.simple_insert_many_txn( txn, table="event_push_summary", values=[ diff --git a/synapse/storage/data_stores/main/events.py b/synapse/storage/data_stores/main/events.py index 79c91fe284..98ae69e996 100644 --- a/synapse/storage/data_stores/main/events.py +++ b/synapse/storage/data_stores/main/events.py @@ -432,7 +432,7 @@ class EventsStore( # event's auth chain, but its easier for now just to store them (and # it doesn't take much storage compared to storing the entire event # anyway). - self._simple_insert_many_txn( + self.simple_insert_many_txn( txn, table="event_auth", values=[ @@ -580,12 +580,12 @@ class EventsStore( self, txn, new_forward_extremities, max_stream_order ): for room_id, new_extrem in iteritems(new_forward_extremities): - self._simple_delete_txn( + self.simple_delete_txn( txn, table="event_forward_extremities", keyvalues={"room_id": room_id} ) txn.call_after(self.get_latest_event_ids_in_room.invalidate, (room_id,)) - self._simple_insert_many_txn( + self.simple_insert_many_txn( txn, table="event_forward_extremities", values=[ @@ -598,7 +598,7 @@ class EventsStore( # new stream_ordering to new forward extremeties in the room. # This allows us to later efficiently look up the forward extremeties # for a room before a given stream_ordering - self._simple_insert_many_txn( + self.simple_insert_many_txn( txn, table="stream_ordering_to_exterm", values=[ @@ -722,7 +722,7 @@ class EventsStore( # change in outlier status to our workers. stream_order = event.internal_metadata.stream_ordering state_group_id = context.state_group - self._simple_insert_txn( + self.simple_insert_txn( txn, table="ex_outlier_stream", values={ @@ -794,7 +794,7 @@ class EventsStore( d.pop("redacted_because", None) return d - self._simple_insert_many_txn( + self.simple_insert_many_txn( txn, table="event_json", values=[ @@ -811,7 +811,7 @@ class EventsStore( ], ) - self._simple_insert_many_txn( + self.simple_insert_many_txn( txn, table="events", values=[ @@ -841,7 +841,7 @@ class EventsStore( # If we're persisting an unredacted event we go and ensure # that we mark any redactions that reference this event as # requiring censoring. - self._simple_update_txn( + self.simple_update_txn( txn, table="redactions", keyvalues={"redacts": event.event_id}, @@ -983,7 +983,7 @@ class EventsStore( state_values.append(vals) - self._simple_insert_many_txn(txn, table="state_events", values=state_values) + self.simple_insert_many_txn(txn, table="state_events", values=state_values) # Prefill the event cache self._add_to_cache(txn, events_and_contexts) @@ -1032,7 +1032,7 @@ class EventsStore( # invalidate the cache for the redacted event txn.call_after(self._invalidate_get_event_cache, event.redacts) - self._simple_insert_txn( + self.simple_insert_txn( txn, table="redactions", values={ @@ -1077,9 +1077,7 @@ class EventsStore( LIMIT ? """ - rows = yield self._execute( - "_censor_redactions_fetch", None, sql, before_ts, 100 - ) + rows = yield self.execute("_censor_redactions_fetch", None, sql, before_ts, 100) updates = [] @@ -1111,7 +1109,7 @@ class EventsStore( if pruned_json: self._censor_event_txn(txn, event_id, pruned_json) - self._simple_update_one_txn( + self.simple_update_one_txn( txn, table="redactions", keyvalues={"event_id": redaction_id}, @@ -1129,7 +1127,7 @@ class EventsStore( event_id (str): The ID of the event to censor. pruned_json (str): The pruned JSON """ - self._simple_update_one_txn( + self.simple_update_one_txn( txn, table="event_json", keyvalues={"event_id": event_id}, @@ -1780,7 +1778,7 @@ class EventsStore( "[purge] found %i state groups to delete", len(state_groups_to_delete) ) - rows = self._simple_select_many_txn( + rows = self.simple_select_many_txn( txn, table="state_group_edges", column="prev_state_group", @@ -1807,15 +1805,15 @@ class EventsStore( curr_state = self._get_state_groups_from_groups_txn(txn, [sg]) curr_state = curr_state[sg] - self._simple_delete_txn( + self.simple_delete_txn( txn, table="state_groups_state", keyvalues={"state_group": sg} ) - self._simple_delete_txn( + self.simple_delete_txn( txn, table="state_group_edges", keyvalues={"state_group": sg} ) - self._simple_insert_many_txn( + self.simple_insert_many_txn( txn, table="state_groups_state", values=[ @@ -1852,7 +1850,7 @@ class EventsStore( state group. """ - rows = yield self._simple_select_many_batch( + rows = yield self.simple_select_many_batch( table="state_group_edges", column="prev_state_group", iterable=state_groups, @@ -1882,7 +1880,7 @@ class EventsStore( # first we have to delete the state groups states logger.info("[purge] removing %s from state_groups_state", room_id) - self._simple_delete_many_txn( + self.simple_delete_many_txn( txn, table="state_groups_state", column="state_group", @@ -1893,7 +1891,7 @@ class EventsStore( # ... and the state group edges logger.info("[purge] removing %s from state_group_edges", room_id) - self._simple_delete_many_txn( + self.simple_delete_many_txn( txn, table="state_group_edges", column="state_group", @@ -1904,7 +1902,7 @@ class EventsStore( # ... and the state groups logger.info("[purge] removing %s from state_groups", room_id) - self._simple_delete_many_txn( + self.simple_delete_many_txn( txn, table="state_groups", column="id", @@ -1921,7 +1919,7 @@ class EventsStore( @cachedInlineCallbacks(max_entries=5000) def _get_event_ordering(self, event_id): - res = yield self._simple_select_one( + res = yield self.simple_select_one( table="events", retcols=["topological_ordering", "stream_ordering"], keyvalues={"event_id": event_id}, @@ -1962,7 +1960,7 @@ class EventsStore( room_id (str): The ID of the room the event was sent to. topological_ordering (int): The position of the event in the room's topology. """ - return self._simple_insert_many_txn( + return self.simple_insert_many_txn( txn=txn, table="event_labels", values=[ @@ -1984,7 +1982,7 @@ class EventsStore( event_id (str): The event ID the expiry timestamp is associated with. expiry_ts (int): The timestamp at which to expire (delete) the event. """ - return self._simple_insert_txn( + return self.simple_insert_txn( txn=txn, table="event_expiry", values={"event_id": event_id, "expiry_ts": expiry_ts}, @@ -2043,7 +2041,7 @@ class EventsStore( txn (LoggingTransaction): The transaction to use to perform the deletion. event_id (str): The event ID to delete the associated expiry timestamp of. """ - return self._simple_delete_txn( + return self.simple_delete_txn( txn=txn, table="event_expiry", keyvalues={"event_id": event_id} ) diff --git a/synapse/storage/data_stores/main/events_bg_updates.py b/synapse/storage/data_stores/main/events_bg_updates.py index aa87f9abc5..37dfc8c871 100644 --- a/synapse/storage/data_stores/main/events_bg_updates.py +++ b/synapse/storage/data_stores/main/events_bg_updates.py @@ -189,7 +189,7 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): chunks = [event_ids[i : i + 100] for i in range(0, len(event_ids), 100)] for chunk in chunks: - ev_rows = self._simple_select_many_txn( + ev_rows = self.simple_select_many_txn( txn, table="event_json", column="event_id", @@ -366,7 +366,7 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): to_delete.intersection_update(original_set) - deleted = self._simple_delete_many_txn( + deleted = self.simple_delete_many_txn( txn=txn, table="event_forward_extremities", column="event_id", @@ -382,7 +382,7 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): if deleted: # We now need to invalidate the caches of these rooms - rows = self._simple_select_many_txn( + rows = self.simple_select_many_txn( txn, table="events", column="event_id", @@ -396,7 +396,7 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): self.get_latest_event_ids_in_room.invalidate, (room_id,) ) - self._simple_delete_many_txn( + self.simple_delete_many_txn( txn=txn, table="_extremities_to_check", column="event_id", @@ -533,7 +533,7 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): try: event_json = json.loads(event_json_raw) - self._simple_insert_many_txn( + self.simple_insert_many_txn( txn=txn, table="event_labels", values=[ diff --git a/synapse/storage/data_stores/main/events_worker.py b/synapse/storage/data_stores/main/events_worker.py index e782e8f481..ec4af29299 100644 --- a/synapse/storage/data_stores/main/events_worker.py +++ b/synapse/storage/data_stores/main/events_worker.py @@ -78,7 +78,7 @@ class EventsWorkerStore(SQLBaseStore): Deferred[int|None]: Timestamp in milliseconds, or None for events that were persisted before received_ts was implemented. """ - return self._simple_select_one_onecol( + return self.simple_select_one_onecol( table="events", keyvalues={"event_id": event_id}, retcol="received_ts", @@ -452,7 +452,7 @@ class EventsWorkerStore(SQLBaseStore): event_id for events, _ in event_list for event_id in events ) - row_dict = self._new_transaction( + row_dict = self.new_transaction( conn, "do_fetch", [], [], self._fetch_event_rows, events_to_fetch ) @@ -745,7 +745,7 @@ class EventsWorkerStore(SQLBaseStore): """Given a list of event ids, check if we have already processed and stored them as non outliers. """ - rows = yield self._simple_select_many_batch( + rows = yield self.simple_select_many_batch( table="events", retcols=("event_id",), column="event_id", diff --git a/synapse/storage/data_stores/main/filtering.py b/synapse/storage/data_stores/main/filtering.py index f05ace299a..17ef7b9354 100644 --- a/synapse/storage/data_stores/main/filtering.py +++ b/synapse/storage/data_stores/main/filtering.py @@ -30,7 +30,7 @@ class FilteringStore(SQLBaseStore): except ValueError: raise SynapseError(400, "Invalid filter ID", Codes.INVALID_PARAM) - def_json = yield self._simple_select_one_onecol( + def_json = yield self.simple_select_one_onecol( table="user_filters", keyvalues={"user_id": user_localpart, "filter_id": filter_id}, retcol="filter_json", diff --git a/synapse/storage/data_stores/main/group_server.py b/synapse/storage/data_stores/main/group_server.py index 5ded539af8..9e1d12bcb7 100644 --- a/synapse/storage/data_stores/main/group_server.py +++ b/synapse/storage/data_stores/main/group_server.py @@ -35,7 +35,7 @@ class GroupServerStore(SQLBaseStore): * "invite" * "open" """ - return self._simple_update_one( + return self.simple_update_one( table="groups", keyvalues={"group_id": group_id}, updatevalues={"join_policy": join_policy}, @@ -43,7 +43,7 @@ class GroupServerStore(SQLBaseStore): ) def get_group(self, group_id): - return self._simple_select_one( + return self.simple_select_one( table="groups", keyvalues={"group_id": group_id}, retcols=( @@ -65,7 +65,7 @@ class GroupServerStore(SQLBaseStore): if not include_private: keyvalues["is_public"] = True - return self._simple_select_list( + return self.simple_select_list( table="group_users", keyvalues=keyvalues, retcols=("user_id", "is_public", "is_admin"), @@ -75,7 +75,7 @@ class GroupServerStore(SQLBaseStore): def get_invited_users_in_group(self, group_id): # TODO: Pagination - return self._simple_select_onecol( + return self.simple_select_onecol( table="group_invites", keyvalues={"group_id": group_id}, retcol="user_id", @@ -89,7 +89,7 @@ class GroupServerStore(SQLBaseStore): if not include_private: keyvalues["is_public"] = True - return self._simple_select_list( + return self.simple_select_list( table="group_rooms", keyvalues=keyvalues, retcols=("room_id", "is_public"), @@ -180,7 +180,7 @@ class GroupServerStore(SQLBaseStore): an order of 1 will put the room first. Otherwise, the room gets added to the end. """ - room_in_group = self._simple_select_one_onecol_txn( + room_in_group = self.simple_select_one_onecol_txn( txn, table="group_rooms", keyvalues={"group_id": group_id, "room_id": room_id}, @@ -193,7 +193,7 @@ class GroupServerStore(SQLBaseStore): if category_id is None: category_id = _DEFAULT_CATEGORY_ID else: - cat_exists = self._simple_select_one_onecol_txn( + cat_exists = self.simple_select_one_onecol_txn( txn, table="group_room_categories", keyvalues={"group_id": group_id, "category_id": category_id}, @@ -204,7 +204,7 @@ class GroupServerStore(SQLBaseStore): raise SynapseError(400, "Category doesn't exist") # TODO: Check category is part of summary already - cat_exists = self._simple_select_one_onecol_txn( + cat_exists = self.simple_select_one_onecol_txn( txn, table="group_summary_room_categories", keyvalues={"group_id": group_id, "category_id": category_id}, @@ -224,7 +224,7 @@ class GroupServerStore(SQLBaseStore): (group_id, category_id, group_id, category_id), ) - existing = self._simple_select_one_txn( + existing = self.simple_select_one_txn( txn, table="group_summary_rooms", keyvalues={ @@ -257,7 +257,7 @@ class GroupServerStore(SQLBaseStore): to_update["room_order"] = order if is_public is not None: to_update["is_public"] = is_public - self._simple_update_txn( + self.simple_update_txn( txn, table="group_summary_rooms", keyvalues={ @@ -271,7 +271,7 @@ class GroupServerStore(SQLBaseStore): if is_public is None: is_public = True - self._simple_insert_txn( + self.simple_insert_txn( txn, table="group_summary_rooms", values={ @@ -287,7 +287,7 @@ class GroupServerStore(SQLBaseStore): if category_id is None: category_id = _DEFAULT_CATEGORY_ID - return self._simple_delete( + return self.simple_delete( table="group_summary_rooms", keyvalues={ "group_id": group_id, @@ -299,7 +299,7 @@ class GroupServerStore(SQLBaseStore): @defer.inlineCallbacks def get_group_categories(self, group_id): - rows = yield self._simple_select_list( + rows = yield self.simple_select_list( table="group_room_categories", keyvalues={"group_id": group_id}, retcols=("category_id", "is_public", "profile"), @@ -316,7 +316,7 @@ class GroupServerStore(SQLBaseStore): @defer.inlineCallbacks def get_group_category(self, group_id, category_id): - category = yield self._simple_select_one( + category = yield self.simple_select_one( table="group_room_categories", keyvalues={"group_id": group_id, "category_id": category_id}, retcols=("is_public", "profile"), @@ -343,7 +343,7 @@ class GroupServerStore(SQLBaseStore): else: update_values["is_public"] = is_public - return self._simple_upsert( + return self.simple_upsert( table="group_room_categories", keyvalues={"group_id": group_id, "category_id": category_id}, values=update_values, @@ -352,7 +352,7 @@ class GroupServerStore(SQLBaseStore): ) def remove_group_category(self, group_id, category_id): - return self._simple_delete( + return self.simple_delete( table="group_room_categories", keyvalues={"group_id": group_id, "category_id": category_id}, desc="remove_group_category", @@ -360,7 +360,7 @@ class GroupServerStore(SQLBaseStore): @defer.inlineCallbacks def get_group_roles(self, group_id): - rows = yield self._simple_select_list( + rows = yield self.simple_select_list( table="group_roles", keyvalues={"group_id": group_id}, retcols=("role_id", "is_public", "profile"), @@ -377,7 +377,7 @@ class GroupServerStore(SQLBaseStore): @defer.inlineCallbacks def get_group_role(self, group_id, role_id): - role = yield self._simple_select_one( + role = yield self.simple_select_one( table="group_roles", keyvalues={"group_id": group_id, "role_id": role_id}, retcols=("is_public", "profile"), @@ -404,7 +404,7 @@ class GroupServerStore(SQLBaseStore): else: update_values["is_public"] = is_public - return self._simple_upsert( + return self.simple_upsert( table="group_roles", keyvalues={"group_id": group_id, "role_id": role_id}, values=update_values, @@ -413,7 +413,7 @@ class GroupServerStore(SQLBaseStore): ) def remove_group_role(self, group_id, role_id): - return self._simple_delete( + return self.simple_delete( table="group_roles", keyvalues={"group_id": group_id, "role_id": role_id}, desc="remove_group_role", @@ -444,7 +444,7 @@ class GroupServerStore(SQLBaseStore): an order of 1 will put the user first. Otherwise, the user gets added to the end. """ - user_in_group = self._simple_select_one_onecol_txn( + user_in_group = self.simple_select_one_onecol_txn( txn, table="group_users", keyvalues={"group_id": group_id, "user_id": user_id}, @@ -457,7 +457,7 @@ class GroupServerStore(SQLBaseStore): if role_id is None: role_id = _DEFAULT_ROLE_ID else: - role_exists = self._simple_select_one_onecol_txn( + role_exists = self.simple_select_one_onecol_txn( txn, table="group_roles", keyvalues={"group_id": group_id, "role_id": role_id}, @@ -468,7 +468,7 @@ class GroupServerStore(SQLBaseStore): raise SynapseError(400, "Role doesn't exist") # TODO: Check role is part of the summary already - role_exists = self._simple_select_one_onecol_txn( + role_exists = self.simple_select_one_onecol_txn( txn, table="group_summary_roles", keyvalues={"group_id": group_id, "role_id": role_id}, @@ -488,7 +488,7 @@ class GroupServerStore(SQLBaseStore): (group_id, role_id, group_id, role_id), ) - existing = self._simple_select_one_txn( + existing = self.simple_select_one_txn( txn, table="group_summary_users", keyvalues={"group_id": group_id, "user_id": user_id, "role_id": role_id}, @@ -517,7 +517,7 @@ class GroupServerStore(SQLBaseStore): to_update["user_order"] = order if is_public is not None: to_update["is_public"] = is_public - self._simple_update_txn( + self.simple_update_txn( txn, table="group_summary_users", keyvalues={ @@ -531,7 +531,7 @@ class GroupServerStore(SQLBaseStore): if is_public is None: is_public = True - self._simple_insert_txn( + self.simple_insert_txn( txn, table="group_summary_users", values={ @@ -547,7 +547,7 @@ class GroupServerStore(SQLBaseStore): if role_id is None: role_id = _DEFAULT_ROLE_ID - return self._simple_delete( + return self.simple_delete( table="group_summary_users", keyvalues={"group_id": group_id, "role_id": role_id, "user_id": user_id}, desc="remove_user_from_summary", @@ -561,7 +561,7 @@ class GroupServerStore(SQLBaseStore): Deferred[list[str]]: A twisted.Deferred containing a list of group ids containing this room """ - return self._simple_select_onecol( + return self.simple_select_onecol( table="group_rooms", keyvalues={"room_id": room_id}, retcol="group_id", @@ -630,7 +630,7 @@ class GroupServerStore(SQLBaseStore): ) def is_user_in_group(self, user_id, group_id): - return self._simple_select_one_onecol( + return self.simple_select_one_onecol( table="group_users", keyvalues={"group_id": group_id, "user_id": user_id}, retcol="user_id", @@ -639,7 +639,7 @@ class GroupServerStore(SQLBaseStore): ).addCallback(lambda r: bool(r)) def is_user_admin_in_group(self, group_id, user_id): - return self._simple_select_one_onecol( + return self.simple_select_one_onecol( table="group_users", keyvalues={"group_id": group_id, "user_id": user_id}, retcol="is_admin", @@ -650,7 +650,7 @@ class GroupServerStore(SQLBaseStore): def add_group_invite(self, group_id, user_id): """Record that the group server has invited a user """ - return self._simple_insert( + return self.simple_insert( table="group_invites", values={"group_id": group_id, "user_id": user_id}, desc="add_group_invite", @@ -659,7 +659,7 @@ class GroupServerStore(SQLBaseStore): def is_user_invited_to_local_group(self, group_id, user_id): """Has the group server invited a user? """ - return self._simple_select_one_onecol( + return self.simple_select_one_onecol( table="group_invites", keyvalues={"group_id": group_id, "user_id": user_id}, retcol="user_id", @@ -682,7 +682,7 @@ class GroupServerStore(SQLBaseStore): """ def _get_users_membership_in_group_txn(txn): - row = self._simple_select_one_txn( + row = self.simple_select_one_txn( txn, table="group_users", keyvalues={"group_id": group_id, "user_id": user_id}, @@ -697,7 +697,7 @@ class GroupServerStore(SQLBaseStore): "is_privileged": row["is_admin"], } - row = self._simple_select_one_onecol_txn( + row = self.simple_select_one_onecol_txn( txn, table="group_invites", keyvalues={"group_id": group_id, "user_id": user_id}, @@ -738,7 +738,7 @@ class GroupServerStore(SQLBaseStore): """ def _add_user_to_group_txn(txn): - self._simple_insert_txn( + self.simple_insert_txn( txn, table="group_users", values={ @@ -749,14 +749,14 @@ class GroupServerStore(SQLBaseStore): }, ) - self._simple_delete_txn( + self.simple_delete_txn( txn, table="group_invites", keyvalues={"group_id": group_id, "user_id": user_id}, ) if local_attestation: - self._simple_insert_txn( + self.simple_insert_txn( txn, table="group_attestations_renewals", values={ @@ -766,7 +766,7 @@ class GroupServerStore(SQLBaseStore): }, ) if remote_attestation: - self._simple_insert_txn( + self.simple_insert_txn( txn, table="group_attestations_remote", values={ @@ -781,27 +781,27 @@ class GroupServerStore(SQLBaseStore): def remove_user_from_group(self, group_id, user_id): def _remove_user_from_group_txn(txn): - self._simple_delete_txn( + self.simple_delete_txn( txn, table="group_users", keyvalues={"group_id": group_id, "user_id": user_id}, ) - self._simple_delete_txn( + self.simple_delete_txn( txn, table="group_invites", keyvalues={"group_id": group_id, "user_id": user_id}, ) - self._simple_delete_txn( + self.simple_delete_txn( txn, table="group_attestations_renewals", keyvalues={"group_id": group_id, "user_id": user_id}, ) - self._simple_delete_txn( + self.simple_delete_txn( txn, table="group_attestations_remote", keyvalues={"group_id": group_id, "user_id": user_id}, ) - self._simple_delete_txn( + self.simple_delete_txn( txn, table="group_summary_users", keyvalues={"group_id": group_id, "user_id": user_id}, @@ -812,14 +812,14 @@ class GroupServerStore(SQLBaseStore): ) def add_room_to_group(self, group_id, room_id, is_public): - return self._simple_insert( + return self.simple_insert( table="group_rooms", values={"group_id": group_id, "room_id": room_id, "is_public": is_public}, desc="add_room_to_group", ) def update_room_in_group_visibility(self, group_id, room_id, is_public): - return self._simple_update( + return self.simple_update( table="group_rooms", keyvalues={"group_id": group_id, "room_id": room_id}, updatevalues={"is_public": is_public}, @@ -828,13 +828,13 @@ class GroupServerStore(SQLBaseStore): def remove_room_from_group(self, group_id, room_id): def _remove_room_from_group_txn(txn): - self._simple_delete_txn( + self.simple_delete_txn( txn, table="group_rooms", keyvalues={"group_id": group_id, "room_id": room_id}, ) - self._simple_delete_txn( + self.simple_delete_txn( txn, table="group_summary_rooms", keyvalues={"group_id": group_id, "room_id": room_id}, @@ -847,7 +847,7 @@ class GroupServerStore(SQLBaseStore): def get_publicised_groups_for_user(self, user_id): """Get all groups a user is publicising """ - return self._simple_select_onecol( + return self.simple_select_onecol( table="local_group_membership", keyvalues={"user_id": user_id, "membership": "join", "is_publicised": True}, retcol="group_id", @@ -857,7 +857,7 @@ class GroupServerStore(SQLBaseStore): def update_group_publicity(self, group_id, user_id, publicise): """Update whether the user is publicising their membership of the group """ - return self._simple_update_one( + return self.simple_update_one( table="local_group_membership", keyvalues={"group_id": group_id, "user_id": user_id}, updatevalues={"is_publicised": publicise}, @@ -893,12 +893,12 @@ class GroupServerStore(SQLBaseStore): def _register_user_group_membership_txn(txn, next_id): # TODO: Upsert? - self._simple_delete_txn( + self.simple_delete_txn( txn, table="local_group_membership", keyvalues={"group_id": group_id, "user_id": user_id}, ) - self._simple_insert_txn( + self.simple_insert_txn( txn, table="local_group_membership", values={ @@ -911,7 +911,7 @@ class GroupServerStore(SQLBaseStore): }, ) - self._simple_insert_txn( + self.simple_insert_txn( txn, table="local_group_updates", values={ @@ -930,7 +930,7 @@ class GroupServerStore(SQLBaseStore): if membership == "join": if local_attestation: - self._simple_insert_txn( + self.simple_insert_txn( txn, table="group_attestations_renewals", values={ @@ -940,7 +940,7 @@ class GroupServerStore(SQLBaseStore): }, ) if remote_attestation: - self._simple_insert_txn( + self.simple_insert_txn( txn, table="group_attestations_remote", values={ @@ -951,12 +951,12 @@ class GroupServerStore(SQLBaseStore): }, ) else: - self._simple_delete_txn( + self.simple_delete_txn( txn, table="group_attestations_renewals", keyvalues={"group_id": group_id, "user_id": user_id}, ) - self._simple_delete_txn( + self.simple_delete_txn( txn, table="group_attestations_remote", keyvalues={"group_id": group_id, "user_id": user_id}, @@ -976,7 +976,7 @@ class GroupServerStore(SQLBaseStore): def create_group( self, group_id, user_id, name, avatar_url, short_description, long_description ): - yield self._simple_insert( + yield self.simple_insert( table="groups", values={ "group_id": group_id, @@ -991,7 +991,7 @@ class GroupServerStore(SQLBaseStore): @defer.inlineCallbacks def update_group_profile(self, group_id, profile): - yield self._simple_update_one( + yield self.simple_update_one( table="groups", keyvalues={"group_id": group_id}, updatevalues=profile, @@ -1017,7 +1017,7 @@ class GroupServerStore(SQLBaseStore): def update_attestation_renewal(self, group_id, user_id, attestation): """Update an attestation that we have renewed """ - return self._simple_update_one( + return self.simple_update_one( table="group_attestations_renewals", keyvalues={"group_id": group_id, "user_id": user_id}, updatevalues={"valid_until_ms": attestation["valid_until_ms"]}, @@ -1027,7 +1027,7 @@ class GroupServerStore(SQLBaseStore): def update_remote_attestion(self, group_id, user_id, attestation): """Update an attestation that a remote has renewed """ - return self._simple_update_one( + return self.simple_update_one( table="group_attestations_remote", keyvalues={"group_id": group_id, "user_id": user_id}, updatevalues={ @@ -1046,7 +1046,7 @@ class GroupServerStore(SQLBaseStore): group_id (str) user_id (str) """ - return self._simple_delete( + return self.simple_delete( table="group_attestations_renewals", keyvalues={"group_id": group_id, "user_id": user_id}, desc="remove_attestation_renewal", @@ -1057,7 +1057,7 @@ class GroupServerStore(SQLBaseStore): """Get the attestation that proves the remote agrees that the user is in the group. """ - row = yield self._simple_select_one( + row = yield self.simple_select_one( table="group_attestations_remote", keyvalues={"group_id": group_id, "user_id": user_id}, retcols=("valid_until_ms", "attestation_json"), @@ -1072,7 +1072,7 @@ class GroupServerStore(SQLBaseStore): return None def get_joined_groups(self, user_id): - return self._simple_select_onecol( + return self.simple_select_onecol( table="local_group_membership", keyvalues={"user_id": user_id, "membership": "join"}, retcol="group_id", @@ -1188,7 +1188,7 @@ class GroupServerStore(SQLBaseStore): ] for table in tables: - self._simple_delete_txn( + self.simple_delete_txn( txn, table=table, keyvalues={"group_id": group_id} ) diff --git a/synapse/storage/data_stores/main/keys.py b/synapse/storage/data_stores/main/keys.py index ebc7db3ed6..c7150432b3 100644 --- a/synapse/storage/data_stores/main/keys.py +++ b/synapse/storage/data_stores/main/keys.py @@ -129,7 +129,7 @@ class KeyStore(SQLBaseStore): return self.runInteraction( "store_server_verify_keys", - self._simple_upsert_many_txn, + self.simple_upsert_many_txn, table="server_signature_keys", key_names=("server_name", "key_id"), key_values=key_values, @@ -157,7 +157,7 @@ class KeyStore(SQLBaseStore): ts_valid_until_ms (int): The time when this json stops being valid. key_json (bytes): The encoded JSON. """ - return self._simple_upsert( + return self.simple_upsert( table="server_keys_json", keyvalues={ "server_name": server_name, @@ -196,7 +196,7 @@ class KeyStore(SQLBaseStore): keyvalues["key_id"] = key_id if from_server is not None: keyvalues["from_server"] = from_server - rows = self._simple_select_list_txn( + rows = self.simple_select_list_txn( txn, "server_keys_json", keyvalues=keyvalues, diff --git a/synapse/storage/data_stores/main/media_repository.py b/synapse/storage/data_stores/main/media_repository.py index 0f2887bdce..0cb9446f96 100644 --- a/synapse/storage/data_stores/main/media_repository.py +++ b/synapse/storage/data_stores/main/media_repository.py @@ -39,7 +39,7 @@ class MediaRepositoryStore(MediaRepositoryBackgroundUpdateStore): Returns: None if the media_id doesn't exist. """ - return self._simple_select_one( + return self.simple_select_one( "local_media_repository", {"media_id": media_id}, ( @@ -64,7 +64,7 @@ class MediaRepositoryStore(MediaRepositoryBackgroundUpdateStore): user_id, url_cache=None, ): - return self._simple_insert( + return self.simple_insert( "local_media_repository", { "media_id": media_id, @@ -129,7 +129,7 @@ class MediaRepositoryStore(MediaRepositoryBackgroundUpdateStore): def store_url_cache( self, url, response_code, etag, expires_ts, og, media_id, download_ts ): - return self._simple_insert( + return self.simple_insert( "local_media_repository_url_cache", { "url": url, @@ -144,7 +144,7 @@ class MediaRepositoryStore(MediaRepositoryBackgroundUpdateStore): ) def get_local_media_thumbnails(self, media_id): - return self._simple_select_list( + return self.simple_select_list( "local_media_repository_thumbnails", {"media_id": media_id}, ( @@ -166,7 +166,7 @@ class MediaRepositoryStore(MediaRepositoryBackgroundUpdateStore): thumbnail_method, thumbnail_length, ): - return self._simple_insert( + return self.simple_insert( "local_media_repository_thumbnails", { "media_id": media_id, @@ -180,7 +180,7 @@ class MediaRepositoryStore(MediaRepositoryBackgroundUpdateStore): ) def get_cached_remote_media(self, origin, media_id): - return self._simple_select_one( + return self.simple_select_one( "remote_media_cache", {"media_origin": origin, "media_id": media_id}, ( @@ -205,7 +205,7 @@ class MediaRepositoryStore(MediaRepositoryBackgroundUpdateStore): upload_name, filesystem_id, ): - return self._simple_insert( + return self.simple_insert( "remote_media_cache", { "media_origin": origin, @@ -253,7 +253,7 @@ class MediaRepositoryStore(MediaRepositoryBackgroundUpdateStore): return self.runInteraction("update_cached_last_access_time", update_cache_txn) def get_remote_media_thumbnails(self, origin, media_id): - return self._simple_select_list( + return self.simple_select_list( "remote_media_cache_thumbnails", {"media_origin": origin, "media_id": media_id}, ( @@ -278,7 +278,7 @@ class MediaRepositoryStore(MediaRepositoryBackgroundUpdateStore): thumbnail_method, thumbnail_length, ): - return self._simple_insert( + return self.simple_insert( "remote_media_cache_thumbnails", { "media_origin": origin, @@ -300,18 +300,18 @@ class MediaRepositoryStore(MediaRepositoryBackgroundUpdateStore): " WHERE last_access_ts < ?" ) - return self._execute( + return self.execute( "get_remote_media_before", self.cursor_to_dict, sql, before_ts ) def delete_remote_media(self, media_origin, media_id): def delete_remote_media_txn(txn): - self._simple_delete_txn( + self.simple_delete_txn( txn, "remote_media_cache", keyvalues={"media_origin": media_origin, "media_id": media_id}, ) - self._simple_delete_txn( + self.simple_delete_txn( txn, "remote_media_cache_thumbnails", keyvalues={"media_origin": media_origin, "media_id": media_id}, diff --git a/synapse/storage/data_stores/main/monthly_active_users.py b/synapse/storage/data_stores/main/monthly_active_users.py index b41c3d317a..b8fc28f97b 100644 --- a/synapse/storage/data_stores/main/monthly_active_users.py +++ b/synapse/storage/data_stores/main/monthly_active_users.py @@ -32,7 +32,7 @@ class MonthlyActiveUsersStore(SQLBaseStore): self._clock = hs.get_clock() self.hs = hs # Do not add more reserved users than the total allowable number - self._new_transaction( + self.new_transaction( dbconn, "initialise_mau_threepids", [], @@ -261,7 +261,7 @@ class MonthlyActiveUsersStore(SQLBaseStore): # never be a big table and alternative approaches (batching multiple # upserts into a single txn) introduced a lot of extra complexity. # See https://github.com/matrix-org/synapse/issues/3854 for more - is_insert = self._simple_upsert_txn( + is_insert = self.simple_upsert_txn( txn, table="monthly_active_users", keyvalues={"user_id": user_id}, @@ -281,7 +281,7 @@ class MonthlyActiveUsersStore(SQLBaseStore): """ - return self._simple_select_one_onecol( + return self.simple_select_one_onecol( table="monthly_active_users", keyvalues={"user_id": user_id}, retcol="timestamp", diff --git a/synapse/storage/data_stores/main/openid.py b/synapse/storage/data_stores/main/openid.py index 79b40044d9..650e49750e 100644 --- a/synapse/storage/data_stores/main/openid.py +++ b/synapse/storage/data_stores/main/openid.py @@ -3,7 +3,7 @@ from synapse.storage._base import SQLBaseStore class OpenIdStore(SQLBaseStore): def insert_open_id_token(self, token, ts_valid_until_ms, user_id): - return self._simple_insert( + return self.simple_insert( table="open_id_tokens", values={ "token": token, diff --git a/synapse/storage/data_stores/main/presence.py b/synapse/storage/data_stores/main/presence.py index 523ed6575e..a5e121efd1 100644 --- a/synapse/storage/data_stores/main/presence.py +++ b/synapse/storage/data_stores/main/presence.py @@ -46,7 +46,7 @@ class PresenceStore(SQLBaseStore): txn.call_after(self._get_presence_for_user.invalidate, (state.user_id,)) # Actually insert new rows - self._simple_insert_many_txn( + self.simple_insert_many_txn( txn, table="presence_stream", values=[ @@ -103,7 +103,7 @@ class PresenceStore(SQLBaseStore): inlineCallbacks=True, ) def get_presence_for_users(self, user_ids): - rows = yield self._simple_select_many_batch( + rows = yield self.simple_select_many_batch( table="presence_stream", column="user_id", iterable=user_ids, @@ -129,7 +129,7 @@ class PresenceStore(SQLBaseStore): return self._presence_id_gen.get_current_token() def allow_presence_visible(self, observed_localpart, observer_userid): - return self._simple_insert( + return self.simple_insert( table="presence_allow_inbound", values={ "observed_user_id": observed_localpart, @@ -140,7 +140,7 @@ class PresenceStore(SQLBaseStore): ) def disallow_presence_visible(self, observed_localpart, observer_userid): - return self._simple_delete_one( + return self.simple_delete_one( table="presence_allow_inbound", keyvalues={ "observed_user_id": observed_localpart, diff --git a/synapse/storage/data_stores/main/profile.py b/synapse/storage/data_stores/main/profile.py index e4e8a1c1d6..c8b5b60301 100644 --- a/synapse/storage/data_stores/main/profile.py +++ b/synapse/storage/data_stores/main/profile.py @@ -24,7 +24,7 @@ class ProfileWorkerStore(SQLBaseStore): @defer.inlineCallbacks def get_profileinfo(self, user_localpart): try: - profile = yield self._simple_select_one( + profile = yield self.simple_select_one( table="profiles", keyvalues={"user_id": user_localpart}, retcols=("displayname", "avatar_url"), @@ -42,7 +42,7 @@ class ProfileWorkerStore(SQLBaseStore): ) def get_profile_displayname(self, user_localpart): - return self._simple_select_one_onecol( + return self.simple_select_one_onecol( table="profiles", keyvalues={"user_id": user_localpart}, retcol="displayname", @@ -50,7 +50,7 @@ class ProfileWorkerStore(SQLBaseStore): ) def get_profile_avatar_url(self, user_localpart): - return self._simple_select_one_onecol( + return self.simple_select_one_onecol( table="profiles", keyvalues={"user_id": user_localpart}, retcol="avatar_url", @@ -58,7 +58,7 @@ class ProfileWorkerStore(SQLBaseStore): ) def get_from_remote_profile_cache(self, user_id): - return self._simple_select_one( + return self.simple_select_one( table="remote_profile_cache", keyvalues={"user_id": user_id}, retcols=("displayname", "avatar_url"), @@ -67,12 +67,12 @@ class ProfileWorkerStore(SQLBaseStore): ) def create_profile(self, user_localpart): - return self._simple_insert( + return self.simple_insert( table="profiles", values={"user_id": user_localpart}, desc="create_profile" ) def set_profile_displayname(self, user_localpart, new_displayname): - return self._simple_update_one( + return self.simple_update_one( table="profiles", keyvalues={"user_id": user_localpart}, updatevalues={"displayname": new_displayname}, @@ -80,7 +80,7 @@ class ProfileWorkerStore(SQLBaseStore): ) def set_profile_avatar_url(self, user_localpart, new_avatar_url): - return self._simple_update_one( + return self.simple_update_one( table="profiles", keyvalues={"user_id": user_localpart}, updatevalues={"avatar_url": new_avatar_url}, @@ -95,7 +95,7 @@ class ProfileStore(ProfileWorkerStore): This should only be called when `is_subscribed_remote_profile_for_user` would return true for the user. """ - return self._simple_upsert( + return self.simple_upsert( table="remote_profile_cache", keyvalues={"user_id": user_id}, values={ @@ -107,7 +107,7 @@ class ProfileStore(ProfileWorkerStore): ) def update_remote_profile_cache(self, user_id, displayname, avatar_url): - return self._simple_update( + return self.simple_update( table="remote_profile_cache", keyvalues={"user_id": user_id}, values={ @@ -125,7 +125,7 @@ class ProfileStore(ProfileWorkerStore): """ subscribed = yield self.is_subscribed_remote_profile_for_user(user_id) if not subscribed: - yield self._simple_delete( + yield self.simple_delete( table="remote_profile_cache", keyvalues={"user_id": user_id}, desc="delete_remote_profile_cache", @@ -155,7 +155,7 @@ class ProfileStore(ProfileWorkerStore): def is_subscribed_remote_profile_for_user(self, user_id): """Check whether we are interested in a remote user's profile. """ - res = yield self._simple_select_one_onecol( + res = yield self.simple_select_one_onecol( table="group_users", keyvalues={"user_id": user_id}, retcol="user_id", @@ -166,7 +166,7 @@ class ProfileStore(ProfileWorkerStore): if res: return True - res = yield self._simple_select_one_onecol( + res = yield self.simple_select_one_onecol( table="group_invites", keyvalues={"user_id": user_id}, retcol="user_id", diff --git a/synapse/storage/data_stores/main/push_rule.py b/synapse/storage/data_stores/main/push_rule.py index b520062d84..75bd499bcd 100644 --- a/synapse/storage/data_stores/main/push_rule.py +++ b/synapse/storage/data_stores/main/push_rule.py @@ -75,7 +75,7 @@ class PushRulesWorkerStore( def __init__(self, db_conn, hs): super(PushRulesWorkerStore, self).__init__(db_conn, hs) - push_rules_prefill, push_rules_id = self._get_cache_dict( + push_rules_prefill, push_rules_id = self.get_cache_dict( db_conn, "push_rules_stream", entity_column="user_id", @@ -100,7 +100,7 @@ class PushRulesWorkerStore( @cachedInlineCallbacks(max_entries=5000) def get_push_rules_for_user(self, user_id): - rows = yield self._simple_select_list( + rows = yield self.simple_select_list( table="push_rules", keyvalues={"user_name": user_id}, retcols=( @@ -124,7 +124,7 @@ class PushRulesWorkerStore( @cachedInlineCallbacks(max_entries=5000) def get_push_rules_enabled_for_user(self, user_id): - results = yield self._simple_select_list( + results = yield self.simple_select_list( table="push_rules_enable", keyvalues={"user_name": user_id}, retcols=("user_name", "rule_id", "enabled"), @@ -162,7 +162,7 @@ class PushRulesWorkerStore( results = {user_id: [] for user_id in user_ids} - rows = yield self._simple_select_many_batch( + rows = yield self.simple_select_many_batch( table="push_rules", column="user_name", iterable=user_ids, @@ -320,7 +320,7 @@ class PushRulesWorkerStore( results = {user_id: {} for user_id in user_ids} - rows = yield self._simple_select_many_batch( + rows = yield self.simple_select_many_batch( table="push_rules_enable", column="user_name", iterable=user_ids, @@ -395,7 +395,7 @@ class PushRuleStore(PushRulesWorkerStore): relative_to_rule = before or after - res = self._simple_select_one_txn( + res = self.simple_select_one_txn( txn, table="push_rules", keyvalues={"user_name": user_id, "rule_id": relative_to_rule}, @@ -499,7 +499,7 @@ class PushRuleStore(PushRulesWorkerStore): actions_json, update_stream=True, ): - """Specialised version of _simple_upsert_txn that picks a push_rule_id + """Specialised version of simple_upsert_txn that picks a push_rule_id using the _push_rule_id_gen if it needs to insert the rule. It assumes that the "push_rules" table is locked""" @@ -518,7 +518,7 @@ class PushRuleStore(PushRulesWorkerStore): # We didn't update a row with the given rule_id so insert one push_rule_id = self._push_rule_id_gen.get_next() - self._simple_insert_txn( + self.simple_insert_txn( txn, table="push_rules", values={ @@ -561,7 +561,7 @@ class PushRuleStore(PushRulesWorkerStore): """ def delete_push_rule_txn(txn, stream_id, event_stream_ordering): - self._simple_delete_one_txn( + self.simple_delete_one_txn( txn, "push_rules", {"user_name": user_id, "rule_id": rule_id} ) @@ -596,7 +596,7 @@ class PushRuleStore(PushRulesWorkerStore): self, txn, stream_id, event_stream_ordering, user_id, rule_id, enabled ): new_id = self._push_rules_enable_id_gen.get_next() - self._simple_upsert_txn( + self.simple_upsert_txn( txn, "push_rules_enable", {"user_name": user_id, "rule_id": rule_id}, @@ -636,7 +636,7 @@ class PushRuleStore(PushRulesWorkerStore): update_stream=False, ) else: - self._simple_update_one_txn( + self.simple_update_one_txn( txn, "push_rules", {"user_name": user_id, "rule_id": rule_id}, @@ -675,7 +675,7 @@ class PushRuleStore(PushRulesWorkerStore): if data is not None: values.update(data) - self._simple_insert_txn(txn, "push_rules_stream", values=values) + self.simple_insert_txn(txn, "push_rules_stream", values=values) txn.call_after(self.get_push_rules_for_user.invalidate, (user_id,)) txn.call_after(self.get_push_rules_enabled_for_user.invalidate, (user_id,)) diff --git a/synapse/storage/data_stores/main/pusher.py b/synapse/storage/data_stores/main/pusher.py index d76861cdc0..d5a169872b 100644 --- a/synapse/storage/data_stores/main/pusher.py +++ b/synapse/storage/data_stores/main/pusher.py @@ -59,7 +59,7 @@ class PusherWorkerStore(SQLBaseStore): @defer.inlineCallbacks def user_has_pusher(self, user_id): - ret = yield self._simple_select_one_onecol( + ret = yield self.simple_select_one_onecol( "pushers", {"user_name": user_id}, "id", allow_none=True ) return ret is not None @@ -72,7 +72,7 @@ class PusherWorkerStore(SQLBaseStore): @defer.inlineCallbacks def get_pushers_by(self, keyvalues): - ret = yield self._simple_select_list( + ret = yield self.simple_select_list( "pushers", keyvalues, [ @@ -193,7 +193,7 @@ class PusherWorkerStore(SQLBaseStore): inlineCallbacks=True, ) def get_if_users_have_pushers(self, user_ids): - rows = yield self._simple_select_many_batch( + rows = yield self.simple_select_many_batch( table="pushers", column="user_name", iterable=user_ids, @@ -229,8 +229,8 @@ class PusherStore(PusherWorkerStore): ): with self._pushers_id_gen.get_next() as stream_id: # no need to lock because `pushers` has a unique key on - # (app_id, pushkey, user_name) so _simple_upsert will retry - yield self._simple_upsert( + # (app_id, pushkey, user_name) so simple_upsert will retry + yield self.simple_upsert( table="pushers", keyvalues={"app_id": app_id, "pushkey": pushkey, "user_name": user_id}, values={ @@ -269,7 +269,7 @@ class PusherStore(PusherWorkerStore): txn, self.get_if_user_has_pusher, (user_id,) ) - self._simple_delete_one_txn( + self.simple_delete_one_txn( txn, "pushers", {"app_id": app_id, "pushkey": pushkey, "user_name": user_id}, @@ -278,7 +278,7 @@ class PusherStore(PusherWorkerStore): # it's possible for us to end up with duplicate rows for # (app_id, pushkey, user_id) at different stream_ids, but that # doesn't really matter. - self._simple_insert_txn( + self.simple_insert_txn( txn, table="deleted_pushers", values={ @@ -296,7 +296,7 @@ class PusherStore(PusherWorkerStore): def update_pusher_last_stream_ordering( self, app_id, pushkey, user_id, last_stream_ordering ): - yield self._simple_update_one( + yield self.simple_update_one( "pushers", {"app_id": app_id, "pushkey": pushkey, "user_name": user_id}, {"last_stream_ordering": last_stream_ordering}, @@ -319,7 +319,7 @@ class PusherStore(PusherWorkerStore): Returns: Deferred[bool]: True if the pusher still exists; False if it has been deleted. """ - updated = yield self._simple_update( + updated = yield self.simple_update( table="pushers", keyvalues={"app_id": app_id, "pushkey": pushkey, "user_name": user_id}, updatevalues={ @@ -333,7 +333,7 @@ class PusherStore(PusherWorkerStore): @defer.inlineCallbacks def update_pusher_failing_since(self, app_id, pushkey, user_id, failing_since): - yield self._simple_update( + yield self.simple_update( table="pushers", keyvalues={"app_id": app_id, "pushkey": pushkey, "user_name": user_id}, updatevalues={"failing_since": failing_since}, @@ -342,7 +342,7 @@ class PusherStore(PusherWorkerStore): @defer.inlineCallbacks def get_throttle_params_by_room(self, pusher_id): - res = yield self._simple_select_list( + res = yield self.simple_select_list( "pusher_throttle", {"pusher": pusher_id}, ["room_id", "last_sent_ts", "throttle_ms"], @@ -361,8 +361,8 @@ class PusherStore(PusherWorkerStore): @defer.inlineCallbacks def set_throttle_params(self, pusher_id, room_id, params): # no need to lock because `pusher_throttle` has a primary key on - # (pusher, room_id) so _simple_upsert will retry - yield self._simple_upsert( + # (pusher, room_id) so simple_upsert will retry + yield self.simple_upsert( "pusher_throttle", {"pusher": pusher_id, "room_id": room_id}, params, diff --git a/synapse/storage/data_stores/main/receipts.py b/synapse/storage/data_stores/main/receipts.py index 8b17334ff4..380f388e30 100644 --- a/synapse/storage/data_stores/main/receipts.py +++ b/synapse/storage/data_stores/main/receipts.py @@ -61,7 +61,7 @@ class ReceiptsWorkerStore(SQLBaseStore): @cached(num_args=2) def get_receipts_for_room(self, room_id, receipt_type): - return self._simple_select_list( + return self.simple_select_list( table="receipts_linearized", keyvalues={"room_id": room_id, "receipt_type": receipt_type}, retcols=("user_id", "event_id"), @@ -70,7 +70,7 @@ class ReceiptsWorkerStore(SQLBaseStore): @cached(num_args=3) def get_last_receipt_event_id_for_user(self, user_id, room_id, receipt_type): - return self._simple_select_one_onecol( + return self.simple_select_one_onecol( table="receipts_linearized", keyvalues={ "room_id": room_id, @@ -84,7 +84,7 @@ class ReceiptsWorkerStore(SQLBaseStore): @cachedInlineCallbacks(num_args=2) def get_receipts_for_user(self, user_id, receipt_type): - rows = yield self._simple_select_list( + rows = yield self.simple_select_list( table="receipts_linearized", keyvalues={"user_id": user_id, "receipt_type": receipt_type}, retcols=("room_id", "event_id"), @@ -335,7 +335,7 @@ class ReceiptsStore(ReceiptsWorkerStore): otherwise, the rx timestamp of the event that the RR corresponds to (or 0 if the event is unknown) """ - res = self._simple_select_one_txn( + res = self.simple_select_one_txn( txn, table="events", retcols=["stream_ordering", "received_ts"], @@ -388,7 +388,7 @@ class ReceiptsStore(ReceiptsWorkerStore): (user_id, room_id, receipt_type), ) - self._simple_delete_txn( + self.simple_delete_txn( txn, table="receipts_linearized", keyvalues={ @@ -398,7 +398,7 @@ class ReceiptsStore(ReceiptsWorkerStore): }, ) - self._simple_insert_txn( + self.simple_insert_txn( txn, table="receipts_linearized", values={ @@ -514,7 +514,7 @@ class ReceiptsStore(ReceiptsWorkerStore): self._get_linearized_receipts_for_room.invalidate_many, (room_id,) ) - self._simple_delete_txn( + self.simple_delete_txn( txn, table="receipts_graph", keyvalues={ @@ -523,7 +523,7 @@ class ReceiptsStore(ReceiptsWorkerStore): "user_id": user_id, }, ) - self._simple_insert_txn( + self.simple_insert_txn( txn, table="receipts_graph", values={ diff --git a/synapse/storage/data_stores/main/registration.py b/synapse/storage/data_stores/main/registration.py index 653c9318cb..debc6706f5 100644 --- a/synapse/storage/data_stores/main/registration.py +++ b/synapse/storage/data_stores/main/registration.py @@ -45,7 +45,7 @@ class RegistrationWorkerStore(SQLBaseStore): @cached() def get_user_by_id(self, user_id): - return self._simple_select_one( + return self.simple_select_one( table="users", keyvalues={"name": user_id}, retcols=[ @@ -109,7 +109,7 @@ class RegistrationWorkerStore(SQLBaseStore): otherwise int representation of the timestamp (as a number of milliseconds since epoch). """ - res = yield self._simple_select_one_onecol( + res = yield self.simple_select_one_onecol( table="account_validity", keyvalues={"user_id": user_id}, retcol="expiration_ts_ms", @@ -137,7 +137,7 @@ class RegistrationWorkerStore(SQLBaseStore): """ def set_account_validity_for_user_txn(txn): - self._simple_update_txn( + self.simple_update_txn( txn=txn, table="account_validity", keyvalues={"user_id": user_id}, @@ -167,7 +167,7 @@ class RegistrationWorkerStore(SQLBaseStore): Raises: StoreError: The provided token is already set for another user. """ - yield self._simple_update_one( + yield self.simple_update_one( table="account_validity", keyvalues={"user_id": user_id}, updatevalues={"renewal_token": renewal_token}, @@ -184,7 +184,7 @@ class RegistrationWorkerStore(SQLBaseStore): Returns: defer.Deferred[str]: The ID of the user to which the token belongs. """ - res = yield self._simple_select_one_onecol( + res = yield self.simple_select_one_onecol( table="account_validity", keyvalues={"renewal_token": renewal_token}, retcol="user_id", @@ -203,7 +203,7 @@ class RegistrationWorkerStore(SQLBaseStore): Returns: defer.Deferred[str]: The renewal token associated with this user ID. """ - res = yield self._simple_select_one_onecol( + res = yield self.simple_select_one_onecol( table="account_validity", keyvalues={"user_id": user_id}, retcol="renewal_token", @@ -250,7 +250,7 @@ class RegistrationWorkerStore(SQLBaseStore): email_sent (bool): Flag which indicates whether a renewal email has been sent to this user. """ - yield self._simple_update_one( + yield self.simple_update_one( table="account_validity", keyvalues={"user_id": user_id}, updatevalues={"email_sent": email_sent}, @@ -265,7 +265,7 @@ class RegistrationWorkerStore(SQLBaseStore): Args: user_id (str): ID of the user to remove from the account validity table. """ - yield self._simple_delete_one( + yield self.simple_delete_one( table="account_validity", keyvalues={"user_id": user_id}, desc="delete_account_validity_for_user", @@ -281,7 +281,7 @@ class RegistrationWorkerStore(SQLBaseStore): Returns (bool): true iff the user is a server admin, false otherwise. """ - res = yield self._simple_select_one_onecol( + res = yield self.simple_select_one_onecol( table="users", keyvalues={"name": user.to_string()}, retcol="admin", @@ -299,7 +299,7 @@ class RegistrationWorkerStore(SQLBaseStore): admin (bool): true iff the user is to be a server admin, false otherwise. """ - return self._simple_update_one( + return self.simple_update_one( table="users", keyvalues={"name": user.to_string()}, updatevalues={"admin": 1 if admin else 0}, @@ -351,7 +351,7 @@ class RegistrationWorkerStore(SQLBaseStore): return res def is_real_user_txn(self, txn, user_id): - res = self._simple_select_one_onecol_txn( + res = self.simple_select_one_onecol_txn( txn=txn, table="users", keyvalues={"name": user_id}, @@ -361,7 +361,7 @@ class RegistrationWorkerStore(SQLBaseStore): return res is None def is_support_user_txn(self, txn, user_id): - res = self._simple_select_one_onecol_txn( + res = self.simple_select_one_onecol_txn( txn=txn, table="users", keyvalues={"name": user_id}, @@ -394,7 +394,7 @@ class RegistrationWorkerStore(SQLBaseStore): Returns: str|None: the mxid of the user, or None if they are not known """ - return await self._simple_select_one_onecol( + return await self.simple_select_one_onecol( table="user_external_ids", keyvalues={"auth_provider": auth_provider, "external_id": external_id}, retcol="user_id", @@ -536,7 +536,7 @@ class RegistrationWorkerStore(SQLBaseStore): Returns: str|None: user id or None if no user id/threepid mapping exists """ - ret = self._simple_select_one_txn( + ret = self.simple_select_one_txn( txn, "user_threepids", {"medium": medium, "address": address}, @@ -549,7 +549,7 @@ class RegistrationWorkerStore(SQLBaseStore): @defer.inlineCallbacks def user_add_threepid(self, user_id, medium, address, validated_at, added_at): - yield self._simple_upsert( + yield self.simple_upsert( "user_threepids", {"medium": medium, "address": address}, {"user_id": user_id, "validated_at": validated_at, "added_at": added_at}, @@ -557,7 +557,7 @@ class RegistrationWorkerStore(SQLBaseStore): @defer.inlineCallbacks def user_get_threepids(self, user_id): - ret = yield self._simple_select_list( + ret = yield self.simple_select_list( "user_threepids", {"user_id": user_id}, ["medium", "address", "validated_at", "added_at"], @@ -566,7 +566,7 @@ class RegistrationWorkerStore(SQLBaseStore): return ret def user_delete_threepid(self, user_id, medium, address): - return self._simple_delete( + return self.simple_delete( "user_threepids", keyvalues={"user_id": user_id, "medium": medium, "address": address}, desc="user_delete_threepid", @@ -579,7 +579,7 @@ class RegistrationWorkerStore(SQLBaseStore): user_id: The user id to delete all threepids of """ - return self._simple_delete( + return self.simple_delete( "user_threepids", keyvalues={"user_id": user_id}, desc="user_delete_threepids", @@ -601,7 +601,7 @@ class RegistrationWorkerStore(SQLBaseStore): """ # We need to use an upsert, in case they user had already bound the # threepid - return self._simple_upsert( + return self.simple_upsert( table="user_threepid_id_server", keyvalues={ "user_id": user_id, @@ -627,7 +627,7 @@ class RegistrationWorkerStore(SQLBaseStore): medium (str): The medium of the threepid (e.g "email") address (str): The address of the threepid (e.g "bob@example.com") """ - return self._simple_select_list( + return self.simple_select_list( table="user_threepid_id_server", keyvalues={"user_id": user_id}, retcols=["medium", "address"], @@ -648,7 +648,7 @@ class RegistrationWorkerStore(SQLBaseStore): Returns: Deferred """ - return self._simple_delete( + return self.simple_delete( table="user_threepid_id_server", keyvalues={ "user_id": user_id, @@ -671,7 +671,7 @@ class RegistrationWorkerStore(SQLBaseStore): Returns: Deferred[list[str]]: Resolves to a list of identity servers """ - return self._simple_select_onecol( + return self.simple_select_onecol( table="user_threepid_id_server", keyvalues={"user_id": user_id, "medium": medium, "address": address}, retcol="id_server", @@ -689,7 +689,7 @@ class RegistrationWorkerStore(SQLBaseStore): defer.Deferred(bool): The requested value. """ - res = yield self._simple_select_one_onecol( + res = yield self.simple_select_one_onecol( table="users", keyvalues={"name": user_id}, retcol="deactivated", @@ -776,12 +776,12 @@ class RegistrationWorkerStore(SQLBaseStore): """ def delete_threepid_session_txn(txn): - self._simple_delete_txn( + self.simple_delete_txn( txn, table="threepid_validation_token", keyvalues={"session_id": session_id}, ) - self._simple_delete_txn( + self.simple_delete_txn( txn, table="threepid_validation_session", keyvalues={"session_id": session_id}, @@ -961,7 +961,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): """ next_id = self._access_tokens_id_gen.get_next() - yield self._simple_insert( + yield self.simple_insert( "access_tokens", { "id": next_id, @@ -1037,7 +1037,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): # Ensure that the guest user actually exists # ``allow_none=False`` makes this raise an exception # if the row isn't in the database. - self._simple_select_one_txn( + self.simple_select_one_txn( txn, "users", keyvalues={"name": user_id, "is_guest": 1}, @@ -1045,7 +1045,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): allow_none=False, ) - self._simple_update_one_txn( + self.simple_update_one_txn( txn, "users", keyvalues={"name": user_id, "is_guest": 1}, @@ -1059,7 +1059,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): }, ) else: - self._simple_insert_txn( + self.simple_insert_txn( txn, "users", values={ @@ -1114,7 +1114,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): external_id: id on that system user_id: complete mxid that it is mapped to """ - return self._simple_insert( + return self.simple_insert( table="user_external_ids", values={ "auth_provider": auth_provider, @@ -1132,7 +1132,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): """ def user_set_password_hash_txn(txn): - self._simple_update_one_txn( + self.simple_update_one_txn( txn, "users", {"name": user_id}, {"password_hash": password_hash} ) self._invalidate_cache_and_stream(txn, self.get_user_by_id, (user_id,)) @@ -1152,7 +1152,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): """ def f(txn): - self._simple_update_one_txn( + self.simple_update_one_txn( txn, table="users", keyvalues={"name": user_id}, @@ -1176,7 +1176,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): """ def f(txn): - self._simple_update_one_txn( + self.simple_update_one_txn( txn, table="users", keyvalues={"name": user_id}, @@ -1234,7 +1234,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): def delete_access_token(self, access_token): def f(txn): - self._simple_delete_one_txn( + self.simple_delete_one_txn( txn, table="access_tokens", keyvalues={"token": access_token} ) @@ -1246,7 +1246,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): @cachedInlineCallbacks() def is_guest(self, user_id): - res = yield self._simple_select_one_onecol( + res = yield self.simple_select_one_onecol( table="users", keyvalues={"name": user_id}, retcol="is_guest", @@ -1261,7 +1261,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): Adds a user to the table of users who need to be parted from all the rooms they're in """ - return self._simple_insert( + return self.simple_insert( "users_pending_deactivation", values={"user_id": user_id}, desc="add_user_pending_deactivation", @@ -1274,7 +1274,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): """ # XXX: This should be simple_delete_one but we failed to put a unique index on # the table, so somehow duplicate entries have ended up in it. - return self._simple_delete( + return self.simple_delete( "users_pending_deactivation", keyvalues={"user_id": user_id}, desc="del_user_pending_deactivation", @@ -1285,7 +1285,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): Gets one user from the table of users waiting to be parted from all the rooms they're in. """ - return self._simple_select_one_onecol( + return self.simple_select_one_onecol( "users_pending_deactivation", keyvalues={}, retcol="user_id", @@ -1315,7 +1315,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): # Insert everything into a transaction in order to run atomically def validate_threepid_session_txn(txn): - row = self._simple_select_one_txn( + row = self.simple_select_one_txn( txn, table="threepid_validation_session", keyvalues={"session_id": session_id}, @@ -1333,7 +1333,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): 400, "This client_secret does not match the provided session_id" ) - row = self._simple_select_one_txn( + row = self.simple_select_one_txn( txn, table="threepid_validation_token", keyvalues={"session_id": session_id, "token": token}, @@ -1358,7 +1358,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): ) # Looks good. Validate the session - self._simple_update_txn( + self.simple_update_txn( txn, table="threepid_validation_session", keyvalues={"session_id": session_id}, @@ -1401,7 +1401,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): if validated_at: insertion_values["validated_at"] = validated_at - return self._simple_upsert( + return self.simple_upsert( table="threepid_validation_session", keyvalues={"session_id": session_id}, values={"last_send_attempt": send_attempt}, @@ -1439,7 +1439,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): def start_or_continue_validation_session_txn(txn): # Create or update a validation session - self._simple_upsert_txn( + self.simple_upsert_txn( txn, table="threepid_validation_session", keyvalues={"session_id": session_id}, @@ -1452,7 +1452,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): ) # Create a new validation token with this session ID - self._simple_insert_txn( + self.simple_insert_txn( txn, table="threepid_validation_token", values={ @@ -1501,7 +1501,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): ) def set_user_deactivated_status_txn(self, txn, user_id, deactivated): - self._simple_update_one_txn( + self.simple_update_one_txn( txn=txn, table="users", keyvalues={"name": user_id}, @@ -1560,7 +1560,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): expiration_ts, ) - self._simple_upsert_txn( + self.simple_upsert_txn( txn, "account_validity", keyvalues={"user_id": user_id}, diff --git a/synapse/storage/data_stores/main/rejections.py b/synapse/storage/data_stores/main/rejections.py index 7d5de0ea2e..f81f9279a1 100644 --- a/synapse/storage/data_stores/main/rejections.py +++ b/synapse/storage/data_stores/main/rejections.py @@ -22,7 +22,7 @@ logger = logging.getLogger(__name__) class RejectionsStore(SQLBaseStore): def _store_rejections_txn(self, txn, event_id, reason): - self._simple_insert_txn( + self.simple_insert_txn( txn, table="rejections", values={ @@ -33,7 +33,7 @@ class RejectionsStore(SQLBaseStore): ) def get_rejection_reason(self, event_id): - return self._simple_select_one_onecol( + return self.simple_select_one_onecol( table="rejections", retcol="reason", keyvalues={"event_id": event_id}, diff --git a/synapse/storage/data_stores/main/relations.py b/synapse/storage/data_stores/main/relations.py index 858f65582b..aa5e10538b 100644 --- a/synapse/storage/data_stores/main/relations.py +++ b/synapse/storage/data_stores/main/relations.py @@ -352,7 +352,7 @@ class RelationsStore(RelationsWorkerStore): aggregation_key = relation.get("key") - self._simple_insert_txn( + self.simple_insert_txn( txn, table="event_relations", values={ @@ -380,6 +380,6 @@ class RelationsStore(RelationsWorkerStore): redacted_event_id (str): The event that was redacted. """ - self._simple_delete_txn( + self.simple_delete_txn( txn, table="event_relations", keyvalues={"event_id": redacted_event_id} ) diff --git a/synapse/storage/data_stores/main/room.py b/synapse/storage/data_stores/main/room.py index b7f9024811..8f9b6365c1 100644 --- a/synapse/storage/data_stores/main/room.py +++ b/synapse/storage/data_stores/main/room.py @@ -53,7 +53,7 @@ class RoomWorkerStore(SQLBaseStore): Returns: A dict containing the room information, or None if the room is unknown. """ - return self._simple_select_one( + return self.simple_select_one( table="rooms", keyvalues={"room_id": room_id}, retcols=("room_id", "is_public", "creator"), @@ -62,7 +62,7 @@ class RoomWorkerStore(SQLBaseStore): ) def get_public_room_ids(self): - return self._simple_select_onecol( + return self.simple_select_onecol( table="rooms", keyvalues={"is_public": True}, retcol="room_id", @@ -266,7 +266,7 @@ class RoomWorkerStore(SQLBaseStore): @cached(max_entries=10000) def is_room_blocked(self, room_id): - return self._simple_select_one_onecol( + return self.simple_select_one_onecol( table="blocked_rooms", keyvalues={"room_id": room_id}, retcol="1", @@ -287,7 +287,7 @@ class RoomWorkerStore(SQLBaseStore): of RatelimitOverride are None or 0 then ratelimitng has been disabled for that user entirely. """ - row = yield self._simple_select_one( + row = yield self.simple_select_one( table="ratelimit_override", keyvalues={"user_id": user_id}, retcols=("messages_per_second", "burst_count"), @@ -407,7 +407,7 @@ class RoomStore(RoomWorkerStore, SearchStore): ev = json.loads(row["json"]) retention_policy = json.dumps(ev["content"]) - self._simple_insert_txn( + self.simple_insert_txn( txn=txn, table="room_retention", values={ @@ -453,7 +453,7 @@ class RoomStore(RoomWorkerStore, SearchStore): try: def store_room_txn(txn, next_id): - self._simple_insert_txn( + self.simple_insert_txn( txn, "rooms", { @@ -463,7 +463,7 @@ class RoomStore(RoomWorkerStore, SearchStore): }, ) if is_public: - self._simple_insert_txn( + self.simple_insert_txn( txn, table="public_room_list_stream", values={ @@ -482,14 +482,14 @@ class RoomStore(RoomWorkerStore, SearchStore): @defer.inlineCallbacks def set_room_is_public(self, room_id, is_public): def set_room_is_public_txn(txn, next_id): - self._simple_update_one_txn( + self.simple_update_one_txn( txn, table="rooms", keyvalues={"room_id": room_id}, updatevalues={"is_public": is_public}, ) - entries = self._simple_select_list_txn( + entries = self.simple_select_list_txn( txn, table="public_room_list_stream", keyvalues={ @@ -507,7 +507,7 @@ class RoomStore(RoomWorkerStore, SearchStore): add_to_stream = bool(entries[-1]["visibility"]) != is_public if add_to_stream: - self._simple_insert_txn( + self.simple_insert_txn( txn, table="public_room_list_stream", values={ @@ -547,7 +547,7 @@ class RoomStore(RoomWorkerStore, SearchStore): def set_room_is_public_appservice_txn(txn, next_id): if is_public: try: - self._simple_insert_txn( + self.simple_insert_txn( txn, table="appservice_room_list", values={ @@ -560,7 +560,7 @@ class RoomStore(RoomWorkerStore, SearchStore): # We've already inserted, nothing to do. return else: - self._simple_delete_txn( + self.simple_delete_txn( txn, table="appservice_room_list", keyvalues={ @@ -570,7 +570,7 @@ class RoomStore(RoomWorkerStore, SearchStore): }, ) - entries = self._simple_select_list_txn( + entries = self.simple_select_list_txn( txn, table="public_room_list_stream", keyvalues={ @@ -588,7 +588,7 @@ class RoomStore(RoomWorkerStore, SearchStore): add_to_stream = bool(entries[-1]["visibility"]) != is_public if add_to_stream: - self._simple_insert_txn( + self.simple_insert_txn( txn, table="public_room_list_stream", values={ @@ -652,7 +652,7 @@ class RoomStore(RoomWorkerStore, SearchStore): # Ignore the event if one of the value isn't an integer. return - self._simple_insert_txn( + self.simple_insert_txn( txn=txn, table="room_retention", values={ @@ -671,7 +671,7 @@ class RoomStore(RoomWorkerStore, SearchStore): self, room_id, event_id, user_id, reason, content, received_ts ): next_id = self._event_reports_id_gen.get_next() - return self._simple_insert( + return self.simple_insert( table="event_reports", values={ "id": next_id, @@ -717,7 +717,7 @@ class RoomStore(RoomWorkerStore, SearchStore): Returns: Deferred """ - yield self._simple_upsert( + yield self.simple_upsert( table="blocked_rooms", keyvalues={"room_id": room_id}, values={}, diff --git a/synapse/storage/data_stores/main/roommember.py b/synapse/storage/data_stores/main/roommember.py index b314d75941..fe2428a281 100644 --- a/synapse/storage/data_stores/main/roommember.py +++ b/synapse/storage/data_stores/main/roommember.py @@ -128,7 +128,7 @@ class RoomMemberWorkerStore(EventsWorkerStore): membership column is up to date """ - pending_update = self._simple_select_one_txn( + pending_update = self.simple_select_one_txn( txn, table="background_updates", keyvalues={"update_name": _CURRENT_STATE_MEMBERSHIP_UPDATE_NAME}, @@ -603,7 +603,7 @@ class RoomMemberWorkerStore(EventsWorkerStore): to `user_id` and ProfileInfo (or None if not join event). """ - rows = yield self._simple_select_many_batch( + rows = yield self.simple_select_many_batch( table="room_memberships", column="event_id", iterable=event_ids, @@ -643,7 +643,7 @@ class RoomMemberWorkerStore(EventsWorkerStore): # the returned user actually has the correct domain. like_clause = "%:" + host - rows = yield self._execute("is_host_joined", None, sql, room_id, like_clause) + rows = yield self.execute("is_host_joined", None, sql, room_id, like_clause) if not rows: return False @@ -683,7 +683,7 @@ class RoomMemberWorkerStore(EventsWorkerStore): # the returned user actually has the correct domain. like_clause = "%:" + host - rows = yield self._execute("was_host_joined", None, sql, room_id, like_clause) + rows = yield self.execute("was_host_joined", None, sql, room_id, like_clause) if not rows: return False @@ -805,7 +805,7 @@ class RoomMemberWorkerStore(EventsWorkerStore): Deferred[set[str]]: Set of room IDs. """ - room_ids = yield self._simple_select_onecol( + room_ids = yield self.simple_select_onecol( table="room_memberships", keyvalues={"membership": Membership.JOIN, "user_id": user_id}, retcol="room_id", @@ -820,7 +820,7 @@ class RoomMemberWorkerStore(EventsWorkerStore): """Get user_id and membership of a set of event IDs. """ - return self._simple_select_many_batch( + return self.simple_select_many_batch( table="room_memberships", column="event_id", iterable=member_event_ids, @@ -990,7 +990,7 @@ class RoomMemberStore(RoomMemberWorkerStore, RoomMemberBackgroundUpdateStore): def _store_room_members_txn(self, txn, events, backfilled): """Store a room member in the database. """ - self._simple_insert_many_txn( + self.simple_insert_many_txn( txn, table="room_memberships", values=[ @@ -1028,7 +1028,7 @@ class RoomMemberStore(RoomMemberWorkerStore, RoomMemberBackgroundUpdateStore): is_mine = self.hs.is_mine_id(event.state_key) if is_new_state and is_mine: if event.membership == Membership.INVITE: - self._simple_insert_txn( + self.simple_insert_txn( txn, table="local_invites", values={ diff --git a/synapse/storage/data_stores/main/search.py b/synapse/storage/data_stores/main/search.py index d1d7c6863d..f735cf095c 100644 --- a/synapse/storage/data_stores/main/search.py +++ b/synapse/storage/data_stores/main/search.py @@ -441,7 +441,7 @@ class SearchStore(SearchBackgroundUpdateStore): # entire table from the database. sql += " ORDER BY rank DESC LIMIT 500" - results = yield self._execute("search_msgs", self.cursor_to_dict, sql, *args) + results = yield self.execute("search_msgs", self.cursor_to_dict, sql, *args) results = list(filter(lambda row: row["room_id"] in room_ids, results)) @@ -455,7 +455,7 @@ class SearchStore(SearchBackgroundUpdateStore): count_sql += " GROUP BY room_id" - count_results = yield self._execute( + count_results = yield self.execute( "search_rooms_count", self.cursor_to_dict, count_sql, *count_args ) @@ -586,7 +586,7 @@ class SearchStore(SearchBackgroundUpdateStore): args.append(limit) - results = yield self._execute("search_rooms", self.cursor_to_dict, sql, *args) + results = yield self.execute("search_rooms", self.cursor_to_dict, sql, *args) results = list(filter(lambda row: row["room_id"] in room_ids, results)) @@ -600,7 +600,7 @@ class SearchStore(SearchBackgroundUpdateStore): count_sql += " GROUP BY room_id" - count_results = yield self._execute( + count_results = yield self.execute( "search_rooms_count", self.cursor_to_dict, count_sql, *count_args ) diff --git a/synapse/storage/data_stores/main/signatures.py b/synapse/storage/data_stores/main/signatures.py index 556191b76f..f3da29ce14 100644 --- a/synapse/storage/data_stores/main/signatures.py +++ b/synapse/storage/data_stores/main/signatures.py @@ -98,4 +98,4 @@ class SignatureStore(SignatureWorkerStore): } ) - self._simple_insert_many_txn(txn, table="event_reference_hashes", values=vals) + self.simple_insert_many_txn(txn, table="event_reference_hashes", values=vals) diff --git a/synapse/storage/data_stores/main/state.py b/synapse/storage/data_stores/main/state.py index 6a90daea31..2b33ec1a35 100644 --- a/synapse/storage/data_stores/main/state.py +++ b/synapse/storage/data_stores/main/state.py @@ -89,7 +89,7 @@ class StateGroupBackgroundUpdateStore(SQLBaseStore): count = 0 while next_group: - next_group = self._simple_select_one_onecol_txn( + next_group = self.simple_select_one_onecol_txn( txn, table="state_group_edges", keyvalues={"state_group": next_group}, @@ -192,7 +192,7 @@ class StateGroupBackgroundUpdateStore(SQLBaseStore): ): break - next_group = self._simple_select_one_onecol_txn( + next_group = self.simple_select_one_onecol_txn( txn, table="state_group_edges", keyvalues={"state_group": next_group}, @@ -431,7 +431,7 @@ class StateGroupWorkerStore( """ def _get_state_group_delta_txn(txn): - prev_group = self._simple_select_one_onecol_txn( + prev_group = self.simple_select_one_onecol_txn( txn, table="state_group_edges", keyvalues={"state_group": state_group}, @@ -442,7 +442,7 @@ class StateGroupWorkerStore( if not prev_group: return _GetStateGroupDelta(None, None) - delta_ids = self._simple_select_list_txn( + delta_ids = self.simple_select_list_txn( txn, table="state_groups_state", keyvalues={"state_group": state_group}, @@ -644,7 +644,7 @@ class StateGroupWorkerStore( @cached(max_entries=50000) def _get_state_group_for_event(self, event_id): - return self._simple_select_one_onecol( + return self.simple_select_one_onecol( table="event_to_state_groups", keyvalues={"event_id": event_id}, retcol="state_group", @@ -661,7 +661,7 @@ class StateGroupWorkerStore( def _get_state_group_for_events(self, event_ids): """Returns mapping event_id -> state_group """ - rows = yield self._simple_select_many_batch( + rows = yield self.simple_select_many_batch( table="event_to_state_groups", column="event_id", iterable=event_ids, @@ -902,7 +902,7 @@ class StateGroupWorkerStore( state_group = self.database_engine.get_next_state_group_id(txn) - self._simple_insert_txn( + self.simple_insert_txn( txn, table="state_groups", values={"id": state_group, "room_id": room_id, "event_id": event_id}, @@ -911,7 +911,7 @@ class StateGroupWorkerStore( # We persist as a delta if we can, while also ensuring the chain # of deltas isn't tooo long, as otherwise read performance degrades. if prev_group: - is_in_db = self._simple_select_one_onecol_txn( + is_in_db = self.simple_select_one_onecol_txn( txn, table="state_groups", keyvalues={"id": prev_group}, @@ -926,13 +926,13 @@ class StateGroupWorkerStore( potential_hops = self._count_state_group_hops_txn(txn, prev_group) if prev_group and potential_hops < MAX_STATE_DELTA_HOPS: - self._simple_insert_txn( + self.simple_insert_txn( txn, table="state_group_edges", values={"state_group": state_group, "prev_state_group": prev_group}, ) - self._simple_insert_many_txn( + self.simple_insert_many_txn( txn, table="state_groups_state", values=[ @@ -947,7 +947,7 @@ class StateGroupWorkerStore( ], ) else: - self._simple_insert_many_txn( + self.simple_insert_many_txn( txn, table="state_groups_state", values=[ @@ -1007,7 +1007,7 @@ class StateGroupWorkerStore( referenced. """ - rows = yield self._simple_select_many_batch( + rows = yield self.simple_select_many_batch( table="event_to_state_groups", column="state_group", iterable=state_groups, @@ -1065,7 +1065,7 @@ class StateBackgroundUpdateStore( batch_size = max(1, int(batch_size / BATCH_SIZE_SCALE_FACTOR)) if max_group is None: - rows = yield self._execute( + rows = yield self.execute( "_background_deduplicate_state", None, "SELECT coalesce(max(id), 0) FROM state_groups", @@ -1135,13 +1135,13 @@ class StateBackgroundUpdateStore( if prev_state.get(key, None) != value } - self._simple_delete_txn( + self.simple_delete_txn( txn, table="state_group_edges", keyvalues={"state_group": state_group}, ) - self._simple_insert_txn( + self.simple_insert_txn( txn, table="state_group_edges", values={ @@ -1150,13 +1150,13 @@ class StateBackgroundUpdateStore( }, ) - self._simple_delete_txn( + self.simple_delete_txn( txn, table="state_groups_state", keyvalues={"state_group": state_group}, ) - self._simple_insert_many_txn( + self.simple_insert_many_txn( txn, table="state_groups_state", values=[ @@ -1263,7 +1263,7 @@ class StateStore(StateGroupWorkerStore, StateBackgroundUpdateStore): state_groups[event.event_id] = context.state_group - self._simple_insert_many_txn( + self.simple_insert_many_txn( txn, table="event_to_state_groups", values=[ diff --git a/synapse/storage/data_stores/main/state_deltas.py b/synapse/storage/data_stores/main/state_deltas.py index 28f33ec18f..03b908026b 100644 --- a/synapse/storage/data_stores/main/state_deltas.py +++ b/synapse/storage/data_stores/main/state_deltas.py @@ -105,7 +105,7 @@ class StateDeltasStore(SQLBaseStore): ) def _get_max_stream_id_in_current_state_deltas_txn(self, txn): - return self._simple_select_one_onecol_txn( + return self.simple_select_one_onecol_txn( txn, table="current_state_delta_stream", keyvalues={}, diff --git a/synapse/storage/data_stores/main/stats.py b/synapse/storage/data_stores/main/stats.py index 45b3de7d56..3aeba859fd 100644 --- a/synapse/storage/data_stores/main/stats.py +++ b/synapse/storage/data_stores/main/stats.py @@ -186,7 +186,7 @@ class StatsStore(StateDeltasStore): """ Returns the stats processor positions. """ - return self._simple_select_one_onecol( + return self.simple_select_one_onecol( table="stats_incremental_position", keyvalues={}, retcol="stream_id", @@ -215,7 +215,7 @@ class StatsStore(StateDeltasStore): if field and "\0" in field: fields[col] = None - return self._simple_upsert( + return self.simple_upsert( table="room_stats_state", keyvalues={"room_id": room_id}, values=fields, @@ -257,7 +257,7 @@ class StatsStore(StateDeltasStore): ABSOLUTE_STATS_FIELDS[stats_type] + PER_SLICE_FIELDS[stats_type] ) - slice_list = self._simple_select_list_paginate_txn( + slice_list = self.simple_select_list_paginate_txn( txn, table + "_historical", {id_col: stats_id}, @@ -282,7 +282,7 @@ class StatsStore(StateDeltasStore): "name", "topic", "canonical_alias", "avatar", "join_rules", "history_visibility" """ - return self._simple_select_one( + return self.simple_select_one( "room_stats_state", {"room_id": room_id}, retcols=( @@ -308,7 +308,7 @@ class StatsStore(StateDeltasStore): """ table, id_col = TYPE_TO_TABLE[stats_type] - return self._simple_select_one_onecol( + return self.simple_select_one_onecol( "%s_current" % (table,), keyvalues={id_col: id}, retcol="completed_delta_stream_id", @@ -344,7 +344,7 @@ class StatsStore(StateDeltasStore): complete_with_stream_id=stream_id, ) - self._simple_update_one_txn( + self.simple_update_one_txn( txn, table="stats_incremental_position", keyvalues={}, @@ -517,17 +517,17 @@ class StatsStore(StateDeltasStore): else: self.database_engine.lock_table(txn, table) retcols = list(chain(absolutes.keys(), additive_relatives.keys())) - current_row = self._simple_select_one_txn( + current_row = self.simple_select_one_txn( txn, table, keyvalues, retcols, allow_none=True ) if current_row is None: merged_dict = {**keyvalues, **absolutes, **additive_relatives} - self._simple_insert_txn(txn, table, merged_dict) + self.simple_insert_txn(txn, table, merged_dict) else: for (key, val) in additive_relatives.items(): current_row[key] += val current_row.update(absolutes) - self._simple_update_one_txn(txn, table, keyvalues, current_row) + self.simple_update_one_txn(txn, table, keyvalues, current_row) def _upsert_copy_from_table_with_additive_relatives_txn( self, @@ -614,11 +614,11 @@ class StatsStore(StateDeltasStore): txn.execute(sql, qargs) else: self.database_engine.lock_table(txn, into_table) - src_row = self._simple_select_one_txn( + src_row = self.simple_select_one_txn( txn, src_table, keyvalues, copy_columns ) all_dest_keyvalues = {**keyvalues, **extra_dst_keyvalues} - dest_current_row = self._simple_select_one_txn( + dest_current_row = self.simple_select_one_txn( txn, into_table, keyvalues=all_dest_keyvalues, @@ -634,11 +634,11 @@ class StatsStore(StateDeltasStore): **src_row, **additive_relatives, } - self._simple_insert_txn(txn, into_table, merged_dict) + self.simple_insert_txn(txn, into_table, merged_dict) else: for (key, val) in additive_relatives.items(): src_row[key] = dest_current_row[key] + val - self._simple_update_txn(txn, into_table, all_dest_keyvalues, src_row) + self.simple_update_txn(txn, into_table, all_dest_keyvalues, src_row) def get_changes_room_total_events_and_bytes(self, min_pos, max_pos): """Fetches the counts of events in the given range of stream IDs. @@ -735,7 +735,7 @@ class StatsStore(StateDeltasStore): def _fetch_current_state_stats(txn): pos = self.get_room_max_stream_ordering() - rows = self._simple_select_many_txn( + rows = self.simple_select_many_txn( txn, table="current_state_events", column="type", diff --git a/synapse/storage/data_stores/main/stream.py b/synapse/storage/data_stores/main/stream.py index 21a410afd0..60487c4559 100644 --- a/synapse/storage/data_stores/main/stream.py +++ b/synapse/storage/data_stores/main/stream.py @@ -255,7 +255,7 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): super(StreamWorkerStore, self).__init__(db_conn, hs) events_max = self.get_room_max_stream_ordering() - event_cache_prefill, min_event_val = self._get_cache_dict( + event_cache_prefill, min_event_val = self.get_cache_dict( db_conn, "events", entity_column="room_id", @@ -576,7 +576,7 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): Returns: A deferred "s%d" stream token. """ - return self._simple_select_one_onecol( + return self.simple_select_one_onecol( table="events", keyvalues={"event_id": event_id}, retcol="stream_ordering" ).addCallback(lambda row: "s%d" % (row,)) @@ -589,7 +589,7 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): Returns: A deferred "t%d-%d" topological token. """ - return self._simple_select_one( + return self.simple_select_one( table="events", keyvalues={"event_id": event_id}, retcols=("stream_ordering", "topological_ordering"), @@ -613,7 +613,7 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): "SELECT coalesce(max(topological_ordering), 0) FROM events" " WHERE room_id = ? AND stream_ordering < ?" ) - return self._execute( + return self.execute( "get_max_topological_token", None, sql, room_id, stream_key ).addCallback(lambda r: r[0][0] if r else 0) @@ -709,7 +709,7 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): dict """ - results = self._simple_select_one_txn( + results = self.simple_select_one_txn( txn, "events", keyvalues={"event_id": event_id, "room_id": room_id}, @@ -797,7 +797,7 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): return upper_bound, events def get_federation_out_pos(self, typ): - return self._simple_select_one_onecol( + return self.simple_select_one_onecol( table="federation_stream_position", retcol="stream_id", keyvalues={"type": typ}, @@ -805,7 +805,7 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): ) def update_federation_out_pos(self, typ, stream_id): - return self._simple_update_one( + return self.simple_update_one( table="federation_stream_position", keyvalues={"type": typ}, updatevalues={"stream_id": stream_id}, diff --git a/synapse/storage/data_stores/main/tags.py b/synapse/storage/data_stores/main/tags.py index aa24339717..85012403be 100644 --- a/synapse/storage/data_stores/main/tags.py +++ b/synapse/storage/data_stores/main/tags.py @@ -41,7 +41,7 @@ class TagsWorkerStore(AccountDataWorkerStore): tag strings to tag content. """ - deferred = self._simple_select_list( + deferred = self.simple_select_list( "room_tags", {"user_id": user_id}, ["room_id", "tag", "content"] ) @@ -153,7 +153,7 @@ class TagsWorkerStore(AccountDataWorkerStore): Returns: A deferred list of string tags. """ - return self._simple_select_list( + return self.simple_select_list( table="room_tags", keyvalues={"user_id": user_id, "room_id": room_id}, retcols=("tag", "content"), @@ -178,7 +178,7 @@ class TagsStore(TagsWorkerStore): content_json = json.dumps(content) def add_tag_txn(txn, next_id): - self._simple_upsert_txn( + self.simple_upsert_txn( txn, table="room_tags", keyvalues={"user_id": user_id, "room_id": room_id, "tag": tag}, diff --git a/synapse/storage/data_stores/main/transactions.py b/synapse/storage/data_stores/main/transactions.py index 01b1be5e14..c162f3ea16 100644 --- a/synapse/storage/data_stores/main/transactions.py +++ b/synapse/storage/data_stores/main/transactions.py @@ -85,7 +85,7 @@ class TransactionStore(SQLBaseStore): ) def _get_received_txn_response(self, txn, transaction_id, origin): - result = self._simple_select_one_txn( + result = self.simple_select_one_txn( txn, table="received_transactions", keyvalues={"transaction_id": transaction_id, "origin": origin}, @@ -119,7 +119,7 @@ class TransactionStore(SQLBaseStore): response_json (str) """ - return self._simple_insert( + return self.simple_insert( table="received_transactions", values={ "transaction_id": transaction_id, @@ -160,7 +160,7 @@ class TransactionStore(SQLBaseStore): return result def _get_destination_retry_timings(self, txn, destination): - result = self._simple_select_one_txn( + result = self.simple_select_one_txn( txn, table="destinations", keyvalues={"destination": destination}, @@ -227,7 +227,7 @@ class TransactionStore(SQLBaseStore): # We need to be careful here as the data may have changed from under us # due to a worker setting the timings. - prev_row = self._simple_select_one_txn( + prev_row = self.simple_select_one_txn( txn, table="destinations", keyvalues={"destination": destination}, @@ -236,7 +236,7 @@ class TransactionStore(SQLBaseStore): ) if not prev_row: - self._simple_insert_txn( + self.simple_insert_txn( txn, table="destinations", values={ @@ -247,7 +247,7 @@ class TransactionStore(SQLBaseStore): }, ) elif retry_interval == 0 or prev_row["retry_interval"] < retry_interval: - self._simple_update_one_txn( + self.simple_update_one_txn( txn, "destinations", keyvalues={"destination": destination}, diff --git a/synapse/storage/data_stores/main/user_directory.py b/synapse/storage/data_stores/main/user_directory.py index 652abe0e6a..1a85aabbfb 100644 --- a/synapse/storage/data_stores/main/user_directory.py +++ b/synapse/storage/data_stores/main/user_directory.py @@ -85,7 +85,7 @@ class UserDirectoryBackgroundUpdateStore(StateDeltasStore, BackgroundUpdateStore """ txn.execute(sql) rooms = [{"room_id": x[0], "events": x[1]} for x in txn.fetchall()] - self._simple_insert_many_txn(txn, TEMP_TABLE + "_rooms", rooms) + self.simple_insert_many_txn(txn, TEMP_TABLE + "_rooms", rooms) del rooms # If search all users is on, get all the users we want to add. @@ -100,13 +100,13 @@ class UserDirectoryBackgroundUpdateStore(StateDeltasStore, BackgroundUpdateStore txn.execute("SELECT name FROM users") users = [{"user_id": x[0]} for x in txn.fetchall()] - self._simple_insert_many_txn(txn, TEMP_TABLE + "_users", users) + self.simple_insert_many_txn(txn, TEMP_TABLE + "_users", users) new_pos = yield self.get_max_stream_id_in_current_state_deltas() yield self.runInteraction( "populate_user_directory_temp_build", _make_staging_area ) - yield self._simple_insert(TEMP_TABLE + "_position", {"position": new_pos}) + yield self.simple_insert(TEMP_TABLE + "_position", {"position": new_pos}) yield self._end_background_update("populate_user_directory_createtables") return 1 @@ -116,7 +116,7 @@ class UserDirectoryBackgroundUpdateStore(StateDeltasStore, BackgroundUpdateStore """ Update the user directory stream position, then clean up the old tables. """ - position = yield self._simple_select_one_onecol( + position = yield self.simple_select_one_onecol( TEMP_TABLE + "_position", None, "position" ) yield self.update_user_directory_stream_pos(position) @@ -243,7 +243,7 @@ class UserDirectoryBackgroundUpdateStore(StateDeltasStore, BackgroundUpdateStore to_insert.clear() # We've finished a room. Delete it from the table. - yield self._simple_delete_one(TEMP_TABLE + "_rooms", {"room_id": room_id}) + yield self.simple_delete_one(TEMP_TABLE + "_rooms", {"room_id": room_id}) # Update the remaining counter. progress["remaining"] -= 1 yield self.runInteraction( @@ -312,7 +312,7 @@ class UserDirectoryBackgroundUpdateStore(StateDeltasStore, BackgroundUpdateStore ) # We've finished processing a user. Delete it from the table. - yield self._simple_delete_one(TEMP_TABLE + "_users", {"user_id": user_id}) + yield self.simple_delete_one(TEMP_TABLE + "_users", {"user_id": user_id}) # Update the remaining counter. progress["remaining"] -= 1 yield self.runInteraction( @@ -361,7 +361,7 @@ class UserDirectoryBackgroundUpdateStore(StateDeltasStore, BackgroundUpdateStore """ def _update_profile_in_user_dir_txn(txn): - new_entry = self._simple_upsert_txn( + new_entry = self.simple_upsert_txn( txn, table="user_directory", keyvalues={"user_id": user_id}, @@ -435,7 +435,7 @@ class UserDirectoryBackgroundUpdateStore(StateDeltasStore, BackgroundUpdateStore ) elif isinstance(self.database_engine, Sqlite3Engine): value = "%s %s" % (user_id, display_name) if display_name else user_id - self._simple_upsert_txn( + self.simple_upsert_txn( txn, table="user_directory_search", keyvalues={"user_id": user_id}, @@ -462,7 +462,7 @@ class UserDirectoryBackgroundUpdateStore(StateDeltasStore, BackgroundUpdateStore """ def _add_users_who_share_room_txn(txn): - self._simple_upsert_many_txn( + self.simple_upsert_many_txn( txn, table="users_who_share_private_rooms", key_names=["user_id", "other_user_id", "room_id"], @@ -489,7 +489,7 @@ class UserDirectoryBackgroundUpdateStore(StateDeltasStore, BackgroundUpdateStore def _add_users_in_public_rooms_txn(txn): - self._simple_upsert_many_txn( + self.simple_upsert_many_txn( txn, table="users_in_public_rooms", key_names=["user_id", "room_id"], @@ -519,7 +519,7 @@ class UserDirectoryBackgroundUpdateStore(StateDeltasStore, BackgroundUpdateStore @cached() def get_user_in_directory(self, user_id): - return self._simple_select_one( + return self.simple_select_one( table="user_directory", keyvalues={"user_id": user_id}, retcols=("display_name", "avatar_url"), @@ -528,7 +528,7 @@ class UserDirectoryBackgroundUpdateStore(StateDeltasStore, BackgroundUpdateStore ) def update_user_directory_stream_pos(self, stream_id): - return self._simple_update_one( + return self.simple_update_one( table="user_directory_stream_pos", keyvalues={}, updatevalues={"stream_id": stream_id}, @@ -547,21 +547,21 @@ class UserDirectoryStore(UserDirectoryBackgroundUpdateStore): def remove_from_user_dir(self, user_id): def _remove_from_user_dir_txn(txn): - self._simple_delete_txn( + self.simple_delete_txn( txn, table="user_directory", keyvalues={"user_id": user_id} ) - self._simple_delete_txn( + self.simple_delete_txn( txn, table="user_directory_search", keyvalues={"user_id": user_id} ) - self._simple_delete_txn( + self.simple_delete_txn( txn, table="users_in_public_rooms", keyvalues={"user_id": user_id} ) - self._simple_delete_txn( + self.simple_delete_txn( txn, table="users_who_share_private_rooms", keyvalues={"user_id": user_id}, ) - self._simple_delete_txn( + self.simple_delete_txn( txn, table="users_who_share_private_rooms", keyvalues={"other_user_id": user_id}, @@ -575,14 +575,14 @@ class UserDirectoryStore(UserDirectoryBackgroundUpdateStore): """Get all user_ids that are in the room directory because they're in the given room_id """ - user_ids_share_pub = yield self._simple_select_onecol( + user_ids_share_pub = yield self.simple_select_onecol( table="users_in_public_rooms", keyvalues={"room_id": room_id}, retcol="user_id", desc="get_users_in_dir_due_to_room", ) - user_ids_share_priv = yield self._simple_select_onecol( + user_ids_share_priv = yield self.simple_select_onecol( table="users_who_share_private_rooms", keyvalues={"room_id": room_id}, retcol="other_user_id", @@ -605,17 +605,17 @@ class UserDirectoryStore(UserDirectoryBackgroundUpdateStore): """ def _remove_user_who_share_room_txn(txn): - self._simple_delete_txn( + self.simple_delete_txn( txn, table="users_who_share_private_rooms", keyvalues={"user_id": user_id, "room_id": room_id}, ) - self._simple_delete_txn( + self.simple_delete_txn( txn, table="users_who_share_private_rooms", keyvalues={"other_user_id": user_id, "room_id": room_id}, ) - self._simple_delete_txn( + self.simple_delete_txn( txn, table="users_in_public_rooms", keyvalues={"user_id": user_id, "room_id": room_id}, @@ -636,14 +636,14 @@ class UserDirectoryStore(UserDirectoryBackgroundUpdateStore): Returns: list: user_id """ - rows = yield self._simple_select_onecol( + rows = yield self.simple_select_onecol( table="users_who_share_private_rooms", keyvalues={"user_id": user_id}, retcol="room_id", desc="get_rooms_user_is_in", ) - pub_rows = yield self._simple_select_onecol( + pub_rows = yield self.simple_select_onecol( table="users_in_public_rooms", keyvalues={"user_id": user_id}, retcol="room_id", @@ -674,14 +674,14 @@ class UserDirectoryStore(UserDirectoryBackgroundUpdateStore): ) f2 USING (room_id) """ - rows = yield self._execute( + rows = yield self.execute( "get_rooms_in_common_for_users", None, sql, user_id, other_user_id ) return [room_id for room_id, in rows] def get_user_directory_stream_pos(self): - return self._simple_select_one_onecol( + return self.simple_select_one_onecol( table="user_directory_stream_pos", keyvalues={}, retcol="stream_id", @@ -786,9 +786,7 @@ class UserDirectoryStore(UserDirectoryBackgroundUpdateStore): # This should be unreachable. raise Exception("Unrecognized database engine") - results = yield self._execute( - "search_user_dir", self.cursor_to_dict, sql, *args - ) + results = yield self.execute("search_user_dir", self.cursor_to_dict, sql, *args) limited = len(results) > limit diff --git a/synapse/storage/data_stores/main/user_erasure_store.py b/synapse/storage/data_stores/main/user_erasure_store.py index aa4f0da5f0..37860af070 100644 --- a/synapse/storage/data_stores/main/user_erasure_store.py +++ b/synapse/storage/data_stores/main/user_erasure_store.py @@ -31,7 +31,7 @@ class UserErasureWorkerStore(SQLBaseStore): Returns: Deferred[bool]: True if the user has requested erasure """ - return self._simple_select_onecol( + return self.simple_select_onecol( table="erased_users", keyvalues={"user_id": user_id}, retcol="1", @@ -56,7 +56,7 @@ class UserErasureWorkerStore(SQLBaseStore): # iterate it multiple times, and (b) avoiding duplicates. user_ids = tuple(set(user_ids)) - rows = yield self._simple_select_many_batch( + rows = yield self.simple_select_many_batch( table="erased_users", column="user_id", iterable=user_ids, diff --git a/tests/handlers/test_stats.py b/tests/handlers/test_stats.py index e0075ccd32..380fd0d107 100644 --- a/tests/handlers/test_stats.py +++ b/tests/handlers/test_stats.py @@ -45,13 +45,13 @@ class StatsRoomTests(unittest.HomeserverTestCase): self.store._all_done = False self.get_success( - self.store._simple_insert( + self.store.simple_insert( "background_updates", {"update_name": "populate_stats_prepare", "progress_json": "{}"}, ) ) self.get_success( - self.store._simple_insert( + self.store.simple_insert( "background_updates", { "update_name": "populate_stats_process_rooms", @@ -61,7 +61,7 @@ class StatsRoomTests(unittest.HomeserverTestCase): ) ) self.get_success( - self.store._simple_insert( + self.store.simple_insert( "background_updates", { "update_name": "populate_stats_process_users", @@ -71,7 +71,7 @@ class StatsRoomTests(unittest.HomeserverTestCase): ) ) self.get_success( - self.store._simple_insert( + self.store.simple_insert( "background_updates", { "update_name": "populate_stats_cleanup", @@ -82,7 +82,7 @@ class StatsRoomTests(unittest.HomeserverTestCase): ) def get_all_room_state(self): - return self.store._simple_select_list( + return self.store.simple_select_list( "room_stats_state", None, retcols=("name", "topic", "canonical_alias") ) @@ -96,7 +96,7 @@ class StatsRoomTests(unittest.HomeserverTestCase): end_ts = self.store.quantise_stats_time(self.reactor.seconds() * 1000) return self.get_success( - self.store._simple_select_one( + self.store.simple_select_one( table + "_historical", {id_col: stat_id, end_ts: end_ts}, cols, @@ -180,7 +180,7 @@ class StatsRoomTests(unittest.HomeserverTestCase): self.handler.stats_enabled = True self.store._all_done = False self.get_success( - self.store._simple_update_one( + self.store.simple_update_one( table="stats_incremental_position", keyvalues={}, updatevalues={"stream_id": 0}, @@ -188,7 +188,7 @@ class StatsRoomTests(unittest.HomeserverTestCase): ) self.get_success( - self.store._simple_insert( + self.store.simple_insert( "background_updates", {"update_name": "populate_stats_prepare", "progress_json": "{}"}, ) @@ -205,13 +205,13 @@ class StatsRoomTests(unittest.HomeserverTestCase): # Now do the initial ingestion. self.get_success( - self.store._simple_insert( + self.store.simple_insert( "background_updates", {"update_name": "populate_stats_process_rooms", "progress_json": "{}"}, ) ) self.get_success( - self.store._simple_insert( + self.store.simple_insert( "background_updates", { "update_name": "populate_stats_cleanup", @@ -656,12 +656,12 @@ class StatsRoomTests(unittest.HomeserverTestCase): self.store._all_done = False self.get_success( - self.store._simple_delete( + self.store.simple_delete( "room_stats_current", {"1": 1}, "test_delete_stats" ) ) self.get_success( - self.store._simple_delete( + self.store.simple_delete( "user_stats_current", {"1": 1}, "test_delete_stats" ) ) @@ -675,7 +675,7 @@ class StatsRoomTests(unittest.HomeserverTestCase): self.store._all_done = False self.get_success( - self.store._simple_insert( + self.store.simple_insert( "background_updates", { "update_name": "populate_stats_process_rooms", @@ -685,7 +685,7 @@ class StatsRoomTests(unittest.HomeserverTestCase): ) ) self.get_success( - self.store._simple_insert( + self.store.simple_insert( "background_updates", { "update_name": "populate_stats_process_users", @@ -695,7 +695,7 @@ class StatsRoomTests(unittest.HomeserverTestCase): ) ) self.get_success( - self.store._simple_insert( + self.store.simple_insert( "background_updates", { "update_name": "populate_stats_cleanup", diff --git a/tests/handlers/test_user_directory.py b/tests/handlers/test_user_directory.py index c5e91a8c41..d5b1c5b4ac 100644 --- a/tests/handlers/test_user_directory.py +++ b/tests/handlers/test_user_directory.py @@ -158,7 +158,7 @@ class UserDirectoryTestCase(unittest.HomeserverTestCase): def get_users_in_public_rooms(self): r = self.get_success( - self.store._simple_select_list( + self.store.simple_select_list( "users_in_public_rooms", None, ("user_id", "room_id") ) ) @@ -169,7 +169,7 @@ class UserDirectoryTestCase(unittest.HomeserverTestCase): def get_users_who_share_private_rooms(self): return self.get_success( - self.store._simple_select_list( + self.store.simple_select_list( "users_who_share_private_rooms", None, ["user_id", "other_user_id", "room_id"], @@ -184,7 +184,7 @@ class UserDirectoryTestCase(unittest.HomeserverTestCase): self.store._all_done = False self.get_success( - self.store._simple_insert( + self.store.simple_insert( "background_updates", { "update_name": "populate_user_directory_createtables", @@ -193,7 +193,7 @@ class UserDirectoryTestCase(unittest.HomeserverTestCase): ) ) self.get_success( - self.store._simple_insert( + self.store.simple_insert( "background_updates", { "update_name": "populate_user_directory_process_rooms", @@ -203,7 +203,7 @@ class UserDirectoryTestCase(unittest.HomeserverTestCase): ) ) self.get_success( - self.store._simple_insert( + self.store.simple_insert( "background_updates", { "update_name": "populate_user_directory_process_users", @@ -213,7 +213,7 @@ class UserDirectoryTestCase(unittest.HomeserverTestCase): ) ) self.get_success( - self.store._simple_insert( + self.store.simple_insert( "background_updates", { "update_name": "populate_user_directory_cleanup", diff --git a/tests/rest/admin/test_admin.py b/tests/rest/admin/test_admin.py index 9575058252..124ce0768a 100644 --- a/tests/rest/admin/test_admin.py +++ b/tests/rest/admin/test_admin.py @@ -632,7 +632,7 @@ class PurgeRoomTestCase(unittest.HomeserverTestCase): "state_groups_state", ): count = self.get_success( - self.store._simple_select_one_onecol( + self.store.simple_select_one_onecol( table=table, keyvalues={"room_id": room_id}, retcol="COUNT(*)", diff --git a/tests/storage/test__base.py b/tests/storage/test__base.py index 9b81b536f5..7b7434a468 100644 --- a/tests/storage/test__base.py +++ b/tests/storage/test__base.py @@ -356,7 +356,7 @@ class UpsertManyTests(unittest.HomeserverTestCase): self.get_success( self.storage.runInteraction( "test", - self.storage._simple_upsert_many_txn, + self.storage.simple_upsert_many_txn, self.table_name, key_names, key_values, @@ -367,7 +367,7 @@ class UpsertManyTests(unittest.HomeserverTestCase): # Check results are what we expect res = self.get_success( - self.storage._simple_select_list( + self.storage.simple_select_list( self.table_name, None, ["id, username, value"] ) ) @@ -383,7 +383,7 @@ class UpsertManyTests(unittest.HomeserverTestCase): self.get_success( self.storage.runInteraction( "test", - self.storage._simple_upsert_many_txn, + self.storage.simple_upsert_many_txn, self.table_name, key_names, key_values, @@ -394,7 +394,7 @@ class UpsertManyTests(unittest.HomeserverTestCase): # Check results are what we expect res = self.get_success( - self.storage._simple_select_list( + self.storage.simple_select_list( self.table_name, None, ["id, username, value"] ) ) diff --git a/tests/storage/test_base.py b/tests/storage/test_base.py index c778de1f0c..de5e4a5fce 100644 --- a/tests/storage/test_base.py +++ b/tests/storage/test_base.py @@ -65,7 +65,7 @@ class SQLBaseStoreTestCase(unittest.TestCase): def test_insert_1col(self): self.mock_txn.rowcount = 1 - yield self.datastore._simple_insert( + yield self.datastore.simple_insert( table="tablename", values={"columname": "Value"} ) @@ -77,7 +77,7 @@ class SQLBaseStoreTestCase(unittest.TestCase): def test_insert_3cols(self): self.mock_txn.rowcount = 1 - yield self.datastore._simple_insert( + yield self.datastore.simple_insert( table="tablename", # Use OrderedDict() so we can assert on the SQL generated values=OrderedDict([("colA", 1), ("colB", 2), ("colC", 3)]), @@ -92,7 +92,7 @@ class SQLBaseStoreTestCase(unittest.TestCase): self.mock_txn.rowcount = 1 self.mock_txn.__iter__ = Mock(return_value=iter([("Value",)])) - value = yield self.datastore._simple_select_one_onecol( + value = yield self.datastore.simple_select_one_onecol( table="tablename", keyvalues={"keycol": "TheKey"}, retcol="retcol" ) @@ -106,7 +106,7 @@ class SQLBaseStoreTestCase(unittest.TestCase): self.mock_txn.rowcount = 1 self.mock_txn.fetchone.return_value = (1, 2, 3) - ret = yield self.datastore._simple_select_one( + ret = yield self.datastore.simple_select_one( table="tablename", keyvalues={"keycol": "TheKey"}, retcols=["colA", "colB", "colC"], @@ -122,7 +122,7 @@ class SQLBaseStoreTestCase(unittest.TestCase): self.mock_txn.rowcount = 0 self.mock_txn.fetchone.return_value = None - ret = yield self.datastore._simple_select_one( + ret = yield self.datastore.simple_select_one( table="tablename", keyvalues={"keycol": "Not here"}, retcols=["colA"], @@ -137,7 +137,7 @@ class SQLBaseStoreTestCase(unittest.TestCase): self.mock_txn.__iter__ = Mock(return_value=iter([(1,), (2,), (3,)])) self.mock_txn.description = (("colA", None, None, None, None, None, None),) - ret = yield self.datastore._simple_select_list( + ret = yield self.datastore.simple_select_list( table="tablename", keyvalues={"keycol": "A set"}, retcols=["colA"] ) @@ -150,7 +150,7 @@ class SQLBaseStoreTestCase(unittest.TestCase): def test_update_one_1col(self): self.mock_txn.rowcount = 1 - yield self.datastore._simple_update_one( + yield self.datastore.simple_update_one( table="tablename", keyvalues={"keycol": "TheKey"}, updatevalues={"columnname": "New Value"}, @@ -165,7 +165,7 @@ class SQLBaseStoreTestCase(unittest.TestCase): def test_update_one_4cols(self): self.mock_txn.rowcount = 1 - yield self.datastore._simple_update_one( + yield self.datastore.simple_update_one( table="tablename", keyvalues=OrderedDict([("colA", 1), ("colB", 2)]), updatevalues=OrderedDict([("colC", 3), ("colD", 4)]), @@ -180,7 +180,7 @@ class SQLBaseStoreTestCase(unittest.TestCase): def test_delete_one(self): self.mock_txn.rowcount = 1 - yield self.datastore._simple_delete_one( + yield self.datastore.simple_delete_one( table="tablename", keyvalues={"keycol": "Go away"} ) diff --git a/tests/storage/test_client_ips.py b/tests/storage/test_client_ips.py index afac5dec7f..25bdd2c163 100644 --- a/tests/storage/test_client_ips.py +++ b/tests/storage/test_client_ips.py @@ -81,7 +81,7 @@ class ClientIpStoreTestCase(unittest.HomeserverTestCase): self.pump(0) result = self.get_success( - self.store._simple_select_list( + self.store.simple_select_list( table="user_ips", keyvalues={"user_id": user_id}, retcols=["access_token", "ip", "user_agent", "device_id", "last_seen"], @@ -112,7 +112,7 @@ class ClientIpStoreTestCase(unittest.HomeserverTestCase): self.pump(0) result = self.get_success( - self.store._simple_select_list( + self.store.simple_select_list( table="user_ips", keyvalues={"user_id": user_id}, retcols=["access_token", "ip", "user_agent", "device_id", "last_seen"], @@ -218,7 +218,7 @@ class ClientIpStoreTestCase(unittest.HomeserverTestCase): # But clear the associated entry in devices table self.get_success( - self.store._simple_update( + self.store.simple_update( table="devices", keyvalues={"user_id": user_id, "device_id": "device_id"}, updatevalues={"last_seen": None, "ip": None, "user_agent": None}, @@ -245,7 +245,7 @@ class ClientIpStoreTestCase(unittest.HomeserverTestCase): # Register the background update to run again. self.get_success( - self.store._simple_insert( + self.store.simple_insert( table="background_updates", values={ "update_name": "devices_last_seen", @@ -297,7 +297,7 @@ class ClientIpStoreTestCase(unittest.HomeserverTestCase): # We should see that in the DB result = self.get_success( - self.store._simple_select_list( + self.store.simple_select_list( table="user_ips", keyvalues={"user_id": user_id}, retcols=["access_token", "ip", "user_agent", "device_id", "last_seen"], @@ -323,7 +323,7 @@ class ClientIpStoreTestCase(unittest.HomeserverTestCase): # We should get no results. result = self.get_success( - self.store._simple_select_list( + self.store.simple_select_list( table="user_ips", keyvalues={"user_id": user_id}, retcols=["access_token", "ip", "user_agent", "device_id", "last_seen"], diff --git a/tests/storage/test_event_push_actions.py b/tests/storage/test_event_push_actions.py index b114c6fb1d..2337a1ae46 100644 --- a/tests/storage/test_event_push_actions.py +++ b/tests/storage/test_event_push_actions.py @@ -116,7 +116,7 @@ class EventPushActionsStoreTestCase(tests.unittest.TestCase): yield _inject_actions(6, PlAIN_NOTIF) yield _rotate(7) - yield self.store._simple_delete( + yield self.store.simple_delete( table="event_push_actions", keyvalues={"1": 1}, desc="" ) @@ -135,7 +135,7 @@ class EventPushActionsStoreTestCase(tests.unittest.TestCase): @defer.inlineCallbacks def test_find_first_stream_ordering_after_ts(self): def add_event(so, ts): - return self.store._simple_insert( + return self.store.simple_insert( "events", { "stream_ordering": so, diff --git a/tests/storage/test_redaction.py b/tests/storage/test_redaction.py index 4561c3e383..4930b6777e 100644 --- a/tests/storage/test_redaction.py +++ b/tests/storage/test_redaction.py @@ -338,7 +338,7 @@ class RedactionTestCase(unittest.HomeserverTestCase): ) event_json = self.get_success( - self.store._simple_select_one_onecol( + self.store.simple_select_one_onecol( table="event_json", keyvalues={"event_id": msg_event.event_id}, retcol="json", @@ -356,7 +356,7 @@ class RedactionTestCase(unittest.HomeserverTestCase): self.reactor.advance(60 * 60 * 2) event_json = self.get_success( - self.store._simple_select_one_onecol( + self.store.simple_select_one_onecol( table="event_json", keyvalues={"event_id": msg_event.event_id}, retcol="json", diff --git a/tests/storage/test_roommember.py b/tests/storage/test_roommember.py index 105a0c2b02..d389cf578f 100644 --- a/tests/storage/test_roommember.py +++ b/tests/storage/test_roommember.py @@ -132,7 +132,7 @@ class CurrentStateMembershipUpdateTestCase(unittest.HomeserverTestCase): # Register the background update to run again. self.get_success( - self.store._simple_insert( + self.store.simple_insert( table="background_updates", values={ "update_name": "current_state_events_membership", diff --git a/tests/unittest.py b/tests/unittest.py index 31997a0f31..295573bc46 100644 --- a/tests/unittest.py +++ b/tests/unittest.py @@ -544,7 +544,7 @@ class HomeserverTestCase(TestCase): Add the given event as an extremity to the room. """ self.get_success( - self.hs.get_datastore()._simple_insert( + self.hs.get_datastore().simple_insert( table="event_forward_extremities", values={"room_id": room_id, "event_id": event_id}, desc="test_add_extremity", From 685fae1ba5a173279faf2b89cad62798c60d3aec Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 4 Dec 2019 16:25:34 +0000 Subject: [PATCH 0590/1623] Newsfile --- changelog.d/6464.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6464.misc diff --git a/changelog.d/6464.misc b/changelog.d/6464.misc new file mode 100644 index 0000000000..bd65276ef6 --- /dev/null +++ b/changelog.d/6464.misc @@ -0,0 +1 @@ +Prepare SQLBaseStore functions being moved out of the stores. From e203874caaae2a378ccbb6b827b6847b3d9a06b8 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 4 Dec 2019 17:27:32 +0000 Subject: [PATCH 0591/1623] get rid of (most of) have_events from _update_auth_events_and_context_for_auth (#6468) have_events was a map from event_id to rejection reason (or None) for events which are in our local database. It was used as filter on the list of event_ids being passed into get_events_as_list. However, since get_events_as_list will ignore any event_ids that are unknown or rejected, we can equivalently just leave it to get_events_as_list to do the filtering. That means that we don't have to keep `have_events` up-to-date, and can use `have_seen_events` instead of `get_seen_events_with_rejection` in the one place we do need it. --- changelog.d/6468.misc | 1 + synapse/handlers/federation.py | 62 +++++++------------ .../storage/data_stores/main/events_worker.py | 34 ---------- 3 files changed, 25 insertions(+), 72 deletions(-) create mode 100644 changelog.d/6468.misc diff --git a/changelog.d/6468.misc b/changelog.d/6468.misc new file mode 100644 index 0000000000..d9a44389b9 --- /dev/null +++ b/changelog.d/6468.misc @@ -0,0 +1 @@ +Refactor some code in the event authentication path for clarity. diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index d9d0cd9eef..7784b80b77 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -2122,14 +2122,9 @@ class FederationHandler(BaseHandler): # # we start by checking if they are in the store, and then try calling /event_auth/. if missing_auth: - # TODO: can we use store.have_seen_events here instead? - have_events = yield self.store.get_seen_events_with_rejections(missing_auth) - logger.debug("Found events %s in the store", have_events) - missing_auth.difference_update(have_events.keys()) - else: - have_events = {} - - have_events.update({e.event_id: "" for e in auth_events.values()}) + have_events = yield self.store.have_seen_events(missing_auth) + logger.debug("Events %s are in the store", have_events) + missing_auth.difference_update(have_events) if missing_auth: # If we don't have all the auth events, we need to get them. @@ -2175,9 +2170,6 @@ class FederationHandler(BaseHandler): except AuthError: pass - have_events = yield self.store.get_seen_events_with_rejections( - event.auth_event_ids() - ) except Exception: logger.exception("Failed to get auth chain") @@ -2207,39 +2199,33 @@ class FederationHandler(BaseHandler): # idea of them. room_version = yield self.store.get_room_version(event.room_id) - different_event_ids = [ - d for d in different_auth if d in have_events and not have_events[d] - ] - if different_event_ids: - # XXX: currently this checks for redactions but I'm not convinced that is - # necessary? - different_events = yield self.store.get_events_as_list(different_event_ids) + # XXX: currently this checks for redactions but I'm not convinced that is + # necessary? + different_events = yield self.store.get_events_as_list(different_auth) - local_view = dict(auth_events) - remote_view = dict(auth_events) - remote_view.update({(d.type, d.state_key): d for d in different_events}) + local_view = dict(auth_events) + remote_view = dict(auth_events) + remote_view.update({(d.type, d.state_key): d for d in different_events}) - new_state = yield self.state_handler.resolve_events( - room_version, - [list(local_view.values()), list(remote_view.values())], - event, - ) + new_state = yield self.state_handler.resolve_events( + room_version, [list(local_view.values()), list(remote_view.values())], event + ) - logger.info( - "After state res: updating auth_events with new state %s", - { - (d.type, d.state_key): d.event_id - for d in new_state.values() - if auth_events.get((d.type, d.state_key)) != d - }, - ) + logger.info( + "After state res: updating auth_events with new state %s", + { + (d.type, d.state_key): d.event_id + for d in new_state.values() + if auth_events.get((d.type, d.state_key)) != d + }, + ) - auth_events.update(new_state) + auth_events.update(new_state) - context = yield self._update_context_for_auth_events( - event, context, auth_events - ) + context = yield self._update_context_for_auth_events( + event, context, auth_events + ) return context diff --git a/synapse/storage/data_stores/main/events_worker.py b/synapse/storage/data_stores/main/events_worker.py index e782e8f481..eaddca65b7 100644 --- a/synapse/storage/data_stores/main/events_worker.py +++ b/synapse/storage/data_stores/main/events_worker.py @@ -783,40 +783,6 @@ class EventsWorkerStore(SQLBaseStore): yield self.runInteraction("have_seen_events", have_seen_events_txn, chunk) return results - def get_seen_events_with_rejections(self, event_ids): - """Given a list of event ids, check if we rejected them. - - Args: - event_ids (list[str]) - - Returns: - Deferred[dict[str, str|None): - Has an entry for each event id we already have seen. Maps to - the rejected reason string if we rejected the event, else maps - to None. - """ - if not event_ids: - return defer.succeed({}) - - def f(txn): - sql = ( - "SELECT e.event_id, reason FROM events as e " - "LEFT JOIN rejections as r ON e.event_id = r.event_id " - "WHERE e.event_id = ?" - ) - - res = {} - for event_id in event_ids: - txn.execute(sql, (event_id,)) - row = txn.fetchone() - if row: - _, rejected = row - res[event_id] = rejected - - return res - - return self.runInteraction("get_seen_events_with_rejections", f) - def _get_total_state_event_counts_txn(self, txn, room_id): """ See get_total_state_event_counts. From 6cd11109db27232d5671d14def4be88646df28d9 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 4 Dec 2019 17:48:23 +0000 Subject: [PATCH 0592/1623] Make synapse_port_db exit with a non-0 code if something failed --- scripts/synapse_port_db | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/synapse_port_db b/scripts/synapse_port_db index f24b8ffe67..705055ce43 100755 --- a/scripts/synapse_port_db +++ b/scripts/synapse_port_db @@ -1055,3 +1055,4 @@ if __name__ == "__main__": if end_error_exec_info: exc_type, exc_value, exc_traceback = end_error_exec_info traceback.print_exception(exc_type, exc_value, exc_traceback) + sys.exit(5) From 02c1f36ccd5676922f718e82bb2bce0e9cb77ca8 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 4 Dec 2019 17:49:28 +0000 Subject: [PATCH 0593/1623] Changelog --- changelog.d/6470.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6470.bugfix diff --git a/changelog.d/6470.bugfix b/changelog.d/6470.bugfix new file mode 100644 index 0000000000..c08b34c14c --- /dev/null +++ b/changelog.d/6470.bugfix @@ -0,0 +1 @@ +Fix `synapse_port_db` not exiting with a 0 code if something went wrong during the port process. From f8421a14049a78c3554210df8ffd43f797b27647 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 4 Dec 2019 17:57:35 +0000 Subject: [PATCH 0594/1623] Fix background updates for synapse_port_db --- scripts/synapse_port_db | 2 ++ synapse/storage/data_stores/main/room.py | 12 ++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/scripts/synapse_port_db b/scripts/synapse_port_db index 705055ce43..0007a15f59 100755 --- a/scripts/synapse_port_db +++ b/scripts/synapse_port_db @@ -47,6 +47,7 @@ from synapse.storage.data_stores.main.media_repository import ( from synapse.storage.data_stores.main.registration import ( RegistrationBackgroundUpdateStore, ) +from synapse.storage.data_stores.main.room import RoomBackgroundUpdateStore from synapse.storage.data_stores.main.roommember import RoomMemberBackgroundUpdateStore from synapse.storage.data_stores.main.search import SearchBackgroundUpdateStore from synapse.storage.data_stores.main.state import StateBackgroundUpdateStore @@ -131,6 +132,7 @@ class Store( EventsBackgroundUpdatesStore, MediaRepositoryBackgroundUpdateStore, RegistrationBackgroundUpdateStore, + RoomBackgroundUpdateStore, RoomMemberBackgroundUpdateStore, SearchBackgroundUpdateStore, StateBackgroundUpdateStore, diff --git a/synapse/storage/data_stores/main/room.py b/synapse/storage/data_stores/main/room.py index b7f9024811..dd1f42c23a 100644 --- a/synapse/storage/data_stores/main/room.py +++ b/synapse/storage/data_stores/main/room.py @@ -28,6 +28,7 @@ from twisted.internet import defer from synapse.api.constants import EventTypes from synapse.api.errors import StoreError from synapse.storage._base import SQLBaseStore +from synapse.storage.background_updates import BackgroundUpdateStore from synapse.storage.data_stores.main.search import SearchStore from synapse.types import ThirdPartyInstanceID from synapse.util.caches.descriptors import cached, cachedInlineCallbacks @@ -360,9 +361,9 @@ class RoomWorkerStore(SQLBaseStore): defer.returnValue(row) -class RoomStore(RoomWorkerStore, SearchStore): +class RoomBackgroundUpdateStore(BackgroundUpdateStore): def __init__(self, db_conn, hs): - super(RoomStore, self).__init__(db_conn, hs) + super(RoomBackgroundUpdateStore, self).__init__(db_conn, hs) self.config = hs.config @@ -438,6 +439,13 @@ class RoomStore(RoomWorkerStore, SearchStore): defer.returnValue(batch_size) + +class RoomStore(RoomBackgroundUpdateStore, RoomWorkerStore, SearchStore): + def __init__(self, db_conn, hs): + super(RoomStore, self).__init__(db_conn, hs) + + self.config = hs.config + @defer.inlineCallbacks def store_room(self, room_id, room_creator_user_id, is_public): """Stores a room. From 756d4942f5707922f29fe1fdfd945d73a19d7ac3 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 4 Dec 2019 13:52:46 +0000 Subject: [PATCH 0595/1623] Move DB pool and helper functions into dedicated Database class --- scripts/synapse_port_db | 73 +- synapse/app/_base.py | 2 +- synapse/app/user_dir.py | 2 +- synapse/module_api/__init__.py | 2 +- synapse/storage/_base.py | 1468 +--------------- synapse/storage/background_updates.py | 16 +- synapse/storage/data_stores/main/__init__.py | 36 +- .../storage/data_stores/main/account_data.py | 26 +- .../storage/data_stores/main/appservice.py | 24 +- synapse/storage/data_stores/main/cache.py | 6 +- .../storage/data_stores/main/client_ips.py | 24 +- .../storage/data_stores/main/deviceinbox.py | 20 +- synapse/storage/data_stores/main/devices.py | 70 +- synapse/storage/data_stores/main/directory.py | 20 +- .../storage/data_stores/main/e2e_room_keys.py | 28 +- .../data_stores/main/end_to_end_keys.py | 44 +- .../data_stores/main/event_federation.py | 40 +- .../data_stores/main/event_push_actions.py | 42 +- synapse/storage/data_stores/main/events.py | 90 +- .../data_stores/main/events_bg_updates.py | 24 +- .../storage/data_stores/main/events_worker.py | 18 +- synapse/storage/data_stores/main/filtering.py | 4 +- .../storage/data_stores/main/group_server.py | 160 +- synapse/storage/data_stores/main/keys.py | 12 +- .../data_stores/main/media_repository.py | 44 +- .../data_stores/main/monthly_active_users.py | 12 +- synapse/storage/data_stores/main/openid.py | 6 +- synapse/storage/data_stores/main/presence.py | 12 +- synapse/storage/data_stores/main/profile.py | 28 +- synapse/storage/data_stores/main/push_rule.py | 36 +- synapse/storage/data_stores/main/pusher.py | 34 +- synapse/storage/data_stores/main/receipts.py | 36 +- .../storage/data_stores/main/registration.py | 164 +- .../storage/data_stores/main/rejections.py | 4 +- synapse/storage/data_stores/main/relations.py | 12 +- synapse/storage/data_stores/main/room.py | 74 +- .../storage/data_stores/main/roommember.py | 44 +- synapse/storage/data_stores/main/search.py | 30 +- .../storage/data_stores/main/signatures.py | 4 +- synapse/storage/data_stores/main/state.py | 54 +- .../storage/data_stores/main/state_deltas.py | 8 +- synapse/storage/data_stores/main/stats.py | 48 +- synapse/storage/data_stores/main/stream.py | 30 +- synapse/storage/data_stores/main/tags.py | 18 +- .../storage/data_stores/main/transactions.py | 22 +- .../data_stores/main/user_directory.py | 80 +- .../data_stores/main/user_erasure_store.py | 6 +- synapse/storage/database.py | 1485 +++++++++++++++++ tests/handlers/test_stats.py | 30 +- tests/handlers/test_user_directory.py | 12 +- tests/rest/admin/test_admin.py | 2 +- tests/storage/test__base.py | 16 +- tests/storage/test_background_update.py | 2 +- tests/storage/test_base.py | 18 +- tests/storage/test_cleanup_extrems.py | 4 +- tests/storage/test_client_ips.py | 12 +- tests/storage/test_event_federation.py | 8 +- tests/storage/test_event_push_actions.py | 12 +- tests/storage/test_monthly_active_users.py | 6 +- tests/storage/test_redaction.py | 4 +- tests/storage/test_roommember.py | 2 +- tests/unittest.py | 2 +- 62 files changed, 2377 insertions(+), 2295 deletions(-) create mode 100644 synapse/storage/database.py diff --git a/scripts/synapse_port_db b/scripts/synapse_port_db index c4cf11d19a..7a2e177d3d 100755 --- a/scripts/synapse_port_db +++ b/scripts/synapse_port_db @@ -173,14 +173,14 @@ class Store( return (yield self.db_pool.runWithConnection(r)) def execute(self, f, *args, **kwargs): - return self.runInteraction(f.__name__, f, *args, **kwargs) + return self.db.runInteraction(f.__name__, f, *args, **kwargs) def execute_sql(self, sql, *args): def r(txn): txn.execute(sql, args) return txn.fetchall() - return self.runInteraction("execute_sql", r) + return self.db.runInteraction("execute_sql", r) def insert_many_txn(self, txn, table, headers, rows): sql = "INSERT INTO %s (%s) VALUES (%s)" % ( @@ -223,7 +223,7 @@ class Porter(object): def setup_table(self, table): if table in APPEND_ONLY_TABLES: # It's safe to just carry on inserting. - row = yield self.postgres_store.simple_select_one( + row = yield self.postgres_store.db.simple_select_one( table="port_from_sqlite3", keyvalues={"table_name": table}, retcols=("forward_rowid", "backward_rowid"), @@ -233,12 +233,14 @@ class Porter(object): total_to_port = None if row is None: if table == "sent_transactions": - forward_chunk, already_ported, total_to_port = ( - yield self._setup_sent_transactions() - ) + ( + forward_chunk, + already_ported, + total_to_port, + ) = yield self._setup_sent_transactions() backward_chunk = 0 else: - yield self.postgres_store.simple_insert( + yield self.postgres_store.db.simple_insert( table="port_from_sqlite3", values={ "table_name": table, @@ -268,7 +270,7 @@ class Porter(object): yield self.postgres_store.execute(delete_all) - yield self.postgres_store.simple_insert( + yield self.postgres_store.db.simple_insert( table="port_from_sqlite3", values={"table_name": table, "forward_rowid": 1, "backward_rowid": 0}, ) @@ -322,7 +324,7 @@ class Porter(object): if table == "user_directory_stream_pos": # We need to make sure there is a single row, `(X, null), as that is # what synapse expects to be there. - yield self.postgres_store.simple_insert( + yield self.postgres_store.db.simple_insert( table=table, values={"stream_id": None} ) self.progress.update(table, table_size) # Mark table as done @@ -363,7 +365,7 @@ class Porter(object): return headers, forward_rows, backward_rows - headers, frows, brows = yield self.sqlite_store.runInteraction("select", r) + headers, frows, brows = yield self.sqlite_store.db.runInteraction("select", r) if frows or brows: if frows: @@ -377,7 +379,7 @@ class Porter(object): def insert(txn): self.postgres_store.insert_many_txn(txn, table, headers[1:], rows) - self.postgres_store.simple_update_one_txn( + self.postgres_store.db.simple_update_one_txn( txn, table="port_from_sqlite3", keyvalues={"table_name": table}, @@ -416,7 +418,7 @@ class Porter(object): return headers, rows - headers, rows = yield self.sqlite_store.runInteraction("select", r) + headers, rows = yield self.sqlite_store.db.runInteraction("select", r) if rows: forward_chunk = rows[-1][0] + 1 @@ -433,8 +435,8 @@ class Porter(object): rows_dict = [] for row in rows: d = dict(zip(headers, row)) - if "\0" in d['value']: - logger.warning('dropping search row %s', d) + if "\0" in d["value"]: + logger.warning("dropping search row %s", d) else: rows_dict.append(d) @@ -454,7 +456,7 @@ class Porter(object): ], ) - self.postgres_store.simple_update_one_txn( + self.postgres_store.db.simple_update_one_txn( txn, table="port_from_sqlite3", keyvalues={"table_name": "event_search"}, @@ -504,17 +506,14 @@ class Porter(object): self.progress.set_state("Preparing %s" % config["name"]) conn = self.setup_db(config, engine) - db_pool = adbapi.ConnectionPool( - config["name"], **config["args"] - ) + db_pool = adbapi.ConnectionPool(config["name"], **config["args"]) hs = MockHomeserver(self.hs_config, engine, conn, db_pool) store = Store(conn, hs) - yield store.runInteraction( - "%s_engine.check_database" % config["name"], - engine.check_database, + yield store.db.runInteraction( + "%s_engine.check_database" % config["name"], engine.check_database, ) return store @@ -541,7 +540,9 @@ class Porter(object): self.sqlite_store = yield self.build_db_store(self.sqlite_config) # Check if all background updates are done, abort if not. - updates_complete = yield self.sqlite_store.has_completed_background_updates() + updates_complete = ( + yield self.sqlite_store.has_completed_background_updates() + ) if not updates_complete: sys.stderr.write( "Pending background updates exist in the SQLite3 database." @@ -582,22 +583,22 @@ class Porter(object): ) try: - yield self.postgres_store.runInteraction("alter_table", alter_table) + yield self.postgres_store.db.runInteraction("alter_table", alter_table) except Exception: # On Error Resume Next pass - yield self.postgres_store.runInteraction( + yield self.postgres_store.db.runInteraction( "create_port_table", create_port_table ) # Step 2. Get tables. self.progress.set_state("Fetching tables") - sqlite_tables = yield self.sqlite_store.simple_select_onecol( + sqlite_tables = yield self.sqlite_store.db.simple_select_onecol( table="sqlite_master", keyvalues={"type": "table"}, retcol="name" ) - postgres_tables = yield self.postgres_store.simple_select_onecol( + postgres_tables = yield self.postgres_store.db.simple_select_onecol( table="information_schema.tables", keyvalues={}, retcol="distinct table_name", @@ -687,11 +688,11 @@ class Porter(object): rows = txn.fetchall() headers = [column[0] for column in txn.description] - ts_ind = headers.index('ts') + ts_ind = headers.index("ts") return headers, [r for r in rows if r[ts_ind] < yesterday] - headers, rows = yield self.sqlite_store.runInteraction("select", r) + headers, rows = yield self.sqlite_store.db.runInteraction("select", r) rows = self._convert_rows("sent_transactions", headers, rows) @@ -724,7 +725,7 @@ class Porter(object): next_chunk = yield self.sqlite_store.execute(get_start_id) next_chunk = max(max_inserted_rowid + 1, next_chunk) - yield self.postgres_store.simple_insert( + yield self.postgres_store.db.simple_insert( table="port_from_sqlite3", values={ "table_name": "sent_transactions", @@ -737,7 +738,7 @@ class Porter(object): txn.execute( "SELECT count(*) FROM sent_transactions" " WHERE ts >= ?", (yesterday,) ) - size, = txn.fetchone() + (size,) = txn.fetchone() return int(size) remaining_count = yield self.sqlite_store.execute(get_sent_table_size) @@ -790,7 +791,7 @@ class Porter(object): next_id = curr_id + 1 txn.execute("ALTER SEQUENCE state_group_id_seq RESTART WITH %s", (next_id,)) - return self.postgres_store.runInteraction("setup_state_group_id_seq", r) + return self.postgres_store.db.runInteraction("setup_state_group_id_seq", r) ############################################## @@ -871,7 +872,7 @@ class CursesProgress(Progress): duration = int(now) - int(self.start_time) minutes, seconds = divmod(duration, 60) - duration_str = '%02dm %02ds' % (minutes, seconds) + duration_str = "%02dm %02ds" % (minutes, seconds) if self.finished: status = "Time spent: %s (Done!)" % (duration_str,) @@ -881,7 +882,7 @@ class CursesProgress(Progress): left = float(self.total_remaining) / self.total_processed est_remaining = (int(now) - self.start_time) * left - est_remaining_str = '%02dm %02ds remaining' % divmod(est_remaining, 60) + est_remaining_str = "%02dm %02ds remaining" % divmod(est_remaining, 60) else: est_remaining_str = "Unknown" status = "Time spent: %s (est. remaining: %s)" % ( @@ -967,7 +968,7 @@ if __name__ == "__main__": description="A script to port an existing synapse SQLite database to" " a new PostgreSQL database." ) - parser.add_argument("-v", action='store_true') + parser.add_argument("-v", action="store_true") parser.add_argument( "--sqlite-database", required=True, @@ -976,12 +977,12 @@ if __name__ == "__main__": ) parser.add_argument( "--postgres-config", - type=argparse.FileType('r'), + type=argparse.FileType("r"), required=True, help="The database config file for the PostgreSQL database", ) parser.add_argument( - "--curses", action='store_true', help="display a curses based progress UI" + "--curses", action="store_true", help="display a curses based progress UI" ) parser.add_argument( diff --git a/synapse/app/_base.py b/synapse/app/_base.py index 2ac7d5c064..9c96816096 100644 --- a/synapse/app/_base.py +++ b/synapse/app/_base.py @@ -269,7 +269,7 @@ def start(hs, listeners=None): # It is now safe to start your Synapse. hs.start_listening(listeners) - hs.get_datastore().start_profiling() + hs.get_datastore().db.start_profiling() setup_sentry(hs) setup_sdnotify(hs) diff --git a/synapse/app/user_dir.py b/synapse/app/user_dir.py index 0fa2b50999..b6d4481725 100644 --- a/synapse/app/user_dir.py +++ b/synapse/app/user_dir.py @@ -64,7 +64,7 @@ class UserDirectorySlaveStore( super(UserDirectorySlaveStore, self).__init__(db_conn, hs) events_max = self._stream_id_gen.get_current_token() - curr_state_delta_prefill, min_curr_state_delta_id = self.get_cache_dict( + curr_state_delta_prefill, min_curr_state_delta_id = self.db.get_cache_dict( db_conn, "current_state_delta_stream", entity_column="room_id", diff --git a/synapse/module_api/__init__.py b/synapse/module_api/__init__.py index 735b882363..305b9b0178 100644 --- a/synapse/module_api/__init__.py +++ b/synapse/module_api/__init__.py @@ -175,4 +175,4 @@ class ModuleApi(object): Returns: Deferred[object]: result of func """ - return self._store.runInteraction(desc, func, *args, **kwargs) + return self._store.db.runInteraction(desc, func, *args, **kwargs) diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py index 9205e550bb..fd5bb3e1de 100644 --- a/synapse/storage/_base.py +++ b/synapse/storage/_base.py @@ -16,1304 +16,28 @@ # limitations under the License. import logging import random -import sys -import time -from typing import Iterable, Tuple -from six import PY2, iteritems, iterkeys, itervalues -from six.moves import builtins, intern, range +from six import PY2 +from six.moves import builtins from canonicaljson import json -from prometheus_client import Histogram -from twisted.internet import defer - -from synapse.api.errors import StoreError -from synapse.logging.context import LoggingContext, make_deferred_yieldable -from synapse.metrics.background_process_metrics import run_as_background_process -from synapse.storage.engines import PostgresEngine, Sqlite3Engine +from synapse.storage.database import LoggingTransaction # noqa: F401 +from synapse.storage.database import make_in_list_sql_clause # noqa: F401 +from synapse.storage.database import Database from synapse.types import get_domain_from_id -from synapse.util.stringutils import exception_to_unicode - -# import a function which will return a monotonic time, in seconds -try: - # on python 3, use time.monotonic, since time.clock can go backwards - from time import monotonic as monotonic_time -except ImportError: - # ... but python 2 doesn't have it - from time import clock as monotonic_time logger = logging.getLogger(__name__) -try: - MAX_TXN_ID = sys.maxint - 1 -except AttributeError: - # python 3 does not have a maximum int value - MAX_TXN_ID = 2 ** 63 - 1 - -sql_logger = logging.getLogger("synapse.storage.SQL") -transaction_logger = logging.getLogger("synapse.storage.txn") -perf_logger = logging.getLogger("synapse.storage.TIME") - -sql_scheduling_timer = Histogram("synapse_storage_schedule_time", "sec") - -sql_query_timer = Histogram("synapse_storage_query_time", "sec", ["verb"]) -sql_txn_timer = Histogram("synapse_storage_transaction_time", "sec", ["desc"]) - - -# Unique indexes which have been added in background updates. Maps from table name -# to the name of the background update which added the unique index to that table. -# -# This is used by the upsert logic to figure out which tables are safe to do a proper -# UPSERT on: until the relevant background update has completed, we -# have to emulate an upsert by locking the table. -# -UNIQUE_INDEX_BACKGROUND_UPDATES = { - "user_ips": "user_ips_device_unique_index", - "device_lists_remote_extremeties": "device_lists_remote_extremeties_unique_idx", - "device_lists_remote_cache": "device_lists_remote_cache_unique_idx", - "event_search": "event_search_event_id_idx", -} - - -class LoggingTransaction(object): - """An object that almost-transparently proxies for the 'txn' object - passed to the constructor. Adds logging and metrics to the .execute() - method. - - Args: - txn: The database transcation object to wrap. - name (str): The name of this transactions for logging. - database_engine (Sqlite3Engine|PostgresEngine) - after_callbacks(list|None): A list that callbacks will be appended to - that have been added by `call_after` which should be run on - successful completion of the transaction. None indicates that no - callbacks should be allowed to be scheduled to run. - exception_callbacks(list|None): A list that callbacks will be appended - to that have been added by `call_on_exception` which should be run - if transaction ends with an error. None indicates that no callbacks - should be allowed to be scheduled to run. - """ - - __slots__ = [ - "txn", - "name", - "database_engine", - "after_callbacks", - "exception_callbacks", - ] - - def __init__( - self, txn, name, database_engine, after_callbacks=None, exception_callbacks=None - ): - object.__setattr__(self, "txn", txn) - object.__setattr__(self, "name", name) - object.__setattr__(self, "database_engine", database_engine) - object.__setattr__(self, "after_callbacks", after_callbacks) - object.__setattr__(self, "exception_callbacks", exception_callbacks) - - def call_after(self, callback, *args, **kwargs): - """Call the given callback on the main twisted thread after the - transaction has finished. Used to invalidate the caches on the - correct thread. - """ - self.after_callbacks.append((callback, args, kwargs)) - - def call_on_exception(self, callback, *args, **kwargs): - self.exception_callbacks.append((callback, args, kwargs)) - - def __getattr__(self, name): - return getattr(self.txn, name) - - def __setattr__(self, name, value): - setattr(self.txn, name, value) - - def __iter__(self): - return self.txn.__iter__() - - def execute_batch(self, sql, args): - if isinstance(self.database_engine, PostgresEngine): - from psycopg2.extras import execute_batch - - self._do_execute(lambda *x: execute_batch(self.txn, *x), sql, args) - else: - for val in args: - self.execute(sql, val) - - def execute(self, sql, *args): - self._do_execute(self.txn.execute, sql, *args) - - def executemany(self, sql, *args): - self._do_execute(self.txn.executemany, sql, *args) - - def _make_sql_one_line(self, sql): - "Strip newlines out of SQL so that the loggers in the DB are on one line" - return " ".join(l.strip() for l in sql.splitlines() if l.strip()) - - def _do_execute(self, func, sql, *args): - sql = self._make_sql_one_line(sql) - - # TODO(paul): Maybe use 'info' and 'debug' for values? - sql_logger.debug("[SQL] {%s} %s", self.name, sql) - - sql = self.database_engine.convert_param_style(sql) - if args: - try: - sql_logger.debug("[SQL values] {%s} %r", self.name, args[0]) - except Exception: - # Don't let logging failures stop SQL from working - pass - - start = time.time() - - try: - return func(sql, *args) - except Exception as e: - logger.debug("[SQL FAIL] {%s} %s", self.name, e) - raise - finally: - secs = time.time() - start - sql_logger.debug("[SQL time] {%s} %f sec", self.name, secs) - sql_query_timer.labels(sql.split()[0]).observe(secs) - - -class PerformanceCounters(object): - def __init__(self): - self.current_counters = {} - self.previous_counters = {} - - def update(self, key, duration_secs): - count, cum_time = self.current_counters.get(key, (0, 0)) - count += 1 - cum_time += duration_secs - self.current_counters[key] = (count, cum_time) - - def interval(self, interval_duration_secs, limit=3): - counters = [] - for name, (count, cum_time) in iteritems(self.current_counters): - prev_count, prev_time = self.previous_counters.get(name, (0, 0)) - counters.append( - ( - (cum_time - prev_time) / interval_duration_secs, - count - prev_count, - name, - ) - ) - - self.previous_counters = dict(self.current_counters) - - counters.sort(reverse=True) - - top_n_counters = ", ".join( - "%s(%d): %.3f%%" % (name, count, 100 * ratio) - for ratio, count, name in counters[:limit] - ) - - return top_n_counters - class SQLBaseStore(object): - _TXN_ID = 0 - def __init__(self, db_conn, hs): self.hs = hs self._clock = hs.get_clock() - self._db_pool = hs.get_db_pool() - - self._previous_txn_total_time = 0 - self._current_txn_total_time = 0 - self._previous_loop_ts = 0 - - # TODO(paul): These can eventually be removed once the metrics code - # is running in mainline, and we have some nice monitoring frontends - # to watch it - self._txn_perf_counters = PerformanceCounters() - self.database_engine = hs.database_engine - - # A set of tables that are not safe to use native upserts in. - self._unsafe_to_upsert_tables = set(UNIQUE_INDEX_BACKGROUND_UPDATES.keys()) - - # We add the user_directory_search table to the blacklist on SQLite - # because the existing search table does not have an index, making it - # unsafe to use native upserts. - if isinstance(self.database_engine, Sqlite3Engine): - self._unsafe_to_upsert_tables.add("user_directory_search") - - if self.database_engine.can_native_upsert: - # Check ASAP (and then later, every 1s) to see if we have finished - # background updates of tables that aren't safe to update. - self._clock.call_later( - 0.0, - run_as_background_process, - "upsert_safety_check", - self._check_safe_to_upsert, - ) - + self.db = Database(hs) self.rand = random.SystemRandom() - @defer.inlineCallbacks - def _check_safe_to_upsert(self): - """ - Is it safe to use native UPSERT? - - If there are background updates, we will need to wait, as they may be - the addition of indexes that set the UNIQUE constraint that we require. - - If the background updates have not completed, wait 15 sec and check again. - """ - updates = yield self.simple_select_list( - "background_updates", - keyvalues=None, - retcols=["update_name"], - desc="check_background_updates", - ) - updates = [x["update_name"] for x in updates] - - for table, update_name in UNIQUE_INDEX_BACKGROUND_UPDATES.items(): - if update_name not in updates: - logger.debug("Now safe to upsert in %s", table) - self._unsafe_to_upsert_tables.discard(table) - - # If there's any updates still running, reschedule to run. - if updates: - self._clock.call_later( - 15.0, - run_as_background_process, - "upsert_safety_check", - self._check_safe_to_upsert, - ) - - def start_profiling(self): - self._previous_loop_ts = monotonic_time() - - def loop(): - curr = self._current_txn_total_time - prev = self._previous_txn_total_time - self._previous_txn_total_time = curr - - time_now = monotonic_time() - time_then = self._previous_loop_ts - self._previous_loop_ts = time_now - - duration = time_now - time_then - ratio = (curr - prev) / duration - - top_three_counters = self._txn_perf_counters.interval(duration, limit=3) - - perf_logger.info( - "Total database time: %.3f%% {%s}", ratio * 100, top_three_counters - ) - - self._clock.looping_call(loop, 10000) - - def new_transaction( - self, conn, desc, after_callbacks, exception_callbacks, func, *args, **kwargs - ): - start = monotonic_time() - txn_id = self._TXN_ID - - # We don't really need these to be unique, so lets stop it from - # growing really large. - self._TXN_ID = (self._TXN_ID + 1) % (MAX_TXN_ID) - - name = "%s-%x" % (desc, txn_id) - - transaction_logger.debug("[TXN START] {%s}", name) - - try: - i = 0 - N = 5 - while True: - cursor = LoggingTransaction( - conn.cursor(), - name, - self.database_engine, - after_callbacks, - exception_callbacks, - ) - try: - r = func(cursor, *args, **kwargs) - conn.commit() - return r - except self.database_engine.module.OperationalError as e: - # This can happen if the database disappears mid - # transaction. - logger.warning( - "[TXN OPERROR] {%s} %s %d/%d", - name, - exception_to_unicode(e), - i, - N, - ) - if i < N: - i += 1 - try: - conn.rollback() - except self.database_engine.module.Error as e1: - logger.warning( - "[TXN EROLL] {%s} %s", name, exception_to_unicode(e1) - ) - continue - raise - except self.database_engine.module.DatabaseError as e: - if self.database_engine.is_deadlock(e): - logger.warning("[TXN DEADLOCK] {%s} %d/%d", name, i, N) - if i < N: - i += 1 - try: - conn.rollback() - except self.database_engine.module.Error as e1: - logger.warning( - "[TXN EROLL] {%s} %s", - name, - exception_to_unicode(e1), - ) - continue - raise - finally: - # we're either about to retry with a new cursor, or we're about to - # release the connection. Once we release the connection, it could - # get used for another query, which might do a conn.rollback(). - # - # In the latter case, even though that probably wouldn't affect the - # results of this transaction, python's sqlite will reset all - # statements on the connection [1], which will make our cursor - # invalid [2]. - # - # In any case, continuing to read rows after commit()ing seems - # dubious from the PoV of ACID transactional semantics - # (sqlite explicitly says that once you commit, you may see rows - # from subsequent updates.) - # - # In psycopg2, cursors are essentially a client-side fabrication - - # all the data is transferred to the client side when the statement - # finishes executing - so in theory we could go on streaming results - # from the cursor, but attempting to do so would make us - # incompatible with sqlite, so let's make sure we're not doing that - # by closing the cursor. - # - # (*named* cursors in psycopg2 are different and are proper server- - # side things, but (a) we don't use them and (b) they are implicitly - # closed by ending the transaction anyway.) - # - # In short, if we haven't finished with the cursor yet, that's a - # problem waiting to bite us. - # - # TL;DR: we're done with the cursor, so we can close it. - # - # [1]: https://github.com/python/cpython/blob/v3.8.0/Modules/_sqlite/connection.c#L465 - # [2]: https://github.com/python/cpython/blob/v3.8.0/Modules/_sqlite/cursor.c#L236 - cursor.close() - except Exception as e: - logger.debug("[TXN FAIL] {%s} %s", name, e) - raise - finally: - end = monotonic_time() - duration = end - start - - LoggingContext.current_context().add_database_transaction(duration) - - transaction_logger.debug("[TXN END] {%s} %f sec", name, duration) - - self._current_txn_total_time += duration - self._txn_perf_counters.update(desc, duration) - sql_txn_timer.labels(desc).observe(duration) - - @defer.inlineCallbacks - def runInteraction(self, desc, func, *args, **kwargs): - """Starts a transaction on the database and runs a given function - - Arguments: - desc (str): description of the transaction, for logging and metrics - func (func): callback function, which will be called with a - database transaction (twisted.enterprise.adbapi.Transaction) as - its first argument, followed by `args` and `kwargs`. - - args (list): positional args to pass to `func` - kwargs (dict): named args to pass to `func` - - Returns: - Deferred: The result of func - """ - after_callbacks = [] - exception_callbacks = [] - - if LoggingContext.current_context() == LoggingContext.sentinel: - logger.warning("Starting db txn '%s' from sentinel context", desc) - - try: - result = yield self.runWithConnection( - self.new_transaction, - desc, - after_callbacks, - exception_callbacks, - func, - *args, - **kwargs - ) - - for after_callback, after_args, after_kwargs in after_callbacks: - after_callback(*after_args, **after_kwargs) - except: # noqa: E722, as we reraise the exception this is fine. - for after_callback, after_args, after_kwargs in exception_callbacks: - after_callback(*after_args, **after_kwargs) - raise - - return result - - @defer.inlineCallbacks - def runWithConnection(self, func, *args, **kwargs): - """Wraps the .runWithConnection() method on the underlying db_pool. - - Arguments: - func (func): callback function, which will be called with a - database connection (twisted.enterprise.adbapi.Connection) as - its first argument, followed by `args` and `kwargs`. - args (list): positional args to pass to `func` - kwargs (dict): named args to pass to `func` - - Returns: - Deferred: The result of func - """ - parent_context = LoggingContext.current_context() - if parent_context == LoggingContext.sentinel: - logger.warning( - "Starting db connection from sentinel context: metrics will be lost" - ) - parent_context = None - - start_time = monotonic_time() - - def inner_func(conn, *args, **kwargs): - with LoggingContext("runWithConnection", parent_context) as context: - sched_duration_sec = monotonic_time() - start_time - sql_scheduling_timer.observe(sched_duration_sec) - context.add_database_scheduled(sched_duration_sec) - - if self.database_engine.is_connection_closed(conn): - logger.debug("Reconnecting closed database connection") - conn.reconnect() - - return func(conn, *args, **kwargs) - - result = yield make_deferred_yieldable( - self._db_pool.runWithConnection(inner_func, *args, **kwargs) - ) - - return result - - @staticmethod - def cursor_to_dict(cursor): - """Converts a SQL cursor into an list of dicts. - - Args: - cursor : The DBAPI cursor which has executed a query. - Returns: - A list of dicts where the key is the column header. - """ - col_headers = list(intern(str(column[0])) for column in cursor.description) - results = list(dict(zip(col_headers, row)) for row in cursor) - return results - - def execute(self, desc, decoder, query, *args): - """Runs a single query for a result set. - - Args: - decoder - The function which can resolve the cursor results to - something meaningful. - query - The query string to execute - *args - Query args. - Returns: - The result of decoder(results) - """ - - def interaction(txn): - txn.execute(query, args) - if decoder: - return decoder(txn) - else: - return txn.fetchall() - - return self.runInteraction(desc, interaction) - - # "Simple" SQL API methods that operate on a single table with no JOINs, - # no complex WHERE clauses, just a dict of values for columns. - - @defer.inlineCallbacks - def simple_insert(self, table, values, or_ignore=False, desc="simple_insert"): - """Executes an INSERT query on the named table. - - Args: - table : string giving the table name - values : dict of new column names and values for them - or_ignore : bool stating whether an exception should be raised - when a conflicting row already exists. If True, False will be - returned by the function instead - desc : string giving a description of the transaction - - Returns: - bool: Whether the row was inserted or not. Only useful when - `or_ignore` is True - """ - try: - yield self.runInteraction(desc, self.simple_insert_txn, table, values) - except self.database_engine.module.IntegrityError: - # We have to do or_ignore flag at this layer, since we can't reuse - # a cursor after we receive an error from the db. - if not or_ignore: - raise - return False - return True - - @staticmethod - def simple_insert_txn(txn, table, values): - keys, vals = zip(*values.items()) - - sql = "INSERT INTO %s (%s) VALUES(%s)" % ( - table, - ", ".join(k for k in keys), - ", ".join("?" for _ in keys), - ) - - txn.execute(sql, vals) - - def simple_insert_many(self, table, values, desc): - return self.runInteraction(desc, self.simple_insert_many_txn, table, values) - - @staticmethod - def simple_insert_many_txn(txn, table, values): - if not values: - return - - # This is a *slight* abomination to get a list of tuples of key names - # and a list of tuples of value names. - # - # i.e. [{"a": 1, "b": 2}, {"c": 3, "d": 4}] - # => [("a", "b",), ("c", "d",)] and [(1, 2,), (3, 4,)] - # - # The sort is to ensure that we don't rely on dictionary iteration - # order. - keys, vals = zip( - *[zip(*(sorted(i.items(), key=lambda kv: kv[0]))) for i in values if i] - ) - - for k in keys: - if k != keys[0]: - raise RuntimeError("All items must have the same keys") - - sql = "INSERT INTO %s (%s) VALUES(%s)" % ( - table, - ", ".join(k for k in keys[0]), - ", ".join("?" for _ in keys[0]), - ) - - txn.executemany(sql, vals) - - @defer.inlineCallbacks - def simple_upsert( - self, - table, - keyvalues, - values, - insertion_values={}, - desc="simple_upsert", - lock=True, - ): - """ - - `lock` should generally be set to True (the default), but can be set - to False if either of the following are true: - - * there is a UNIQUE INDEX on the key columns. In this case a conflict - will cause an IntegrityError in which case this function will retry - the update. - - * we somehow know that we are the only thread which will be updating - this table. - - Args: - table (str): The table to upsert into - keyvalues (dict): The unique key columns and their new values - values (dict): The nonunique columns and their new values - insertion_values (dict): additional key/values to use only when - inserting - lock (bool): True to lock the table when doing the upsert. - Returns: - Deferred(None or bool): Native upserts always return None. Emulated - upserts return True if a new entry was created, False if an existing - one was updated. - """ - attempts = 0 - while True: - try: - result = yield self.runInteraction( - desc, - self.simple_upsert_txn, - table, - keyvalues, - values, - insertion_values, - lock=lock, - ) - return result - except self.database_engine.module.IntegrityError as e: - attempts += 1 - if attempts >= 5: - # don't retry forever, because things other than races - # can cause IntegrityErrors - raise - - # presumably we raced with another transaction: let's retry. - logger.warning( - "IntegrityError when upserting into %s; retrying: %s", table, e - ) - - def simple_upsert_txn( - self, txn, table, keyvalues, values, insertion_values={}, lock=True - ): - """ - Pick the UPSERT method which works best on the platform. Either the - native one (Pg9.5+, recent SQLites), or fall back to an emulated method. - - Args: - txn: The transaction to use. - table (str): The table to upsert into - keyvalues (dict): The unique key tables and their new values - values (dict): The nonunique columns and their new values - insertion_values (dict): additional key/values to use only when - inserting - lock (bool): True to lock the table when doing the upsert. - Returns: - None or bool: Native upserts always return None. Emulated - upserts return True if a new entry was created, False if an existing - one was updated. - """ - if ( - self.database_engine.can_native_upsert - and table not in self._unsafe_to_upsert_tables - ): - return self.simple_upsert_txn_native_upsert( - txn, table, keyvalues, values, insertion_values=insertion_values - ) - else: - return self.simple_upsert_txn_emulated( - txn, - table, - keyvalues, - values, - insertion_values=insertion_values, - lock=lock, - ) - - def simple_upsert_txn_emulated( - self, txn, table, keyvalues, values, insertion_values={}, lock=True - ): - """ - Args: - table (str): The table to upsert into - keyvalues (dict): The unique key tables and their new values - values (dict): The nonunique columns and their new values - insertion_values (dict): additional key/values to use only when - inserting - lock (bool): True to lock the table when doing the upsert. - Returns: - bool: Return True if a new entry was created, False if an existing - one was updated. - """ - # We need to lock the table :(, unless we're *really* careful - if lock: - self.database_engine.lock_table(txn, table) - - def _getwhere(key): - # If the value we're passing in is None (aka NULL), we need to use - # IS, not =, as NULL = NULL equals NULL (False). - if keyvalues[key] is None: - return "%s IS ?" % (key,) - else: - return "%s = ?" % (key,) - - if not values: - # If `values` is empty, then all of the values we care about are in - # the unique key, so there is nothing to UPDATE. We can just do a - # SELECT instead to see if it exists. - sql = "SELECT 1 FROM %s WHERE %s" % ( - table, - " AND ".join(_getwhere(k) for k in keyvalues), - ) - sqlargs = list(keyvalues.values()) - txn.execute(sql, sqlargs) - if txn.fetchall(): - # We have an existing record. - return False - else: - # First try to update. - sql = "UPDATE %s SET %s WHERE %s" % ( - table, - ", ".join("%s = ?" % (k,) for k in values), - " AND ".join(_getwhere(k) for k in keyvalues), - ) - sqlargs = list(values.values()) + list(keyvalues.values()) - - txn.execute(sql, sqlargs) - if txn.rowcount > 0: - # successfully updated at least one row. - return False - - # We didn't find any existing rows, so insert a new one - allvalues = {} - allvalues.update(keyvalues) - allvalues.update(values) - allvalues.update(insertion_values) - - sql = "INSERT INTO %s (%s) VALUES (%s)" % ( - table, - ", ".join(k for k in allvalues), - ", ".join("?" for _ in allvalues), - ) - txn.execute(sql, list(allvalues.values())) - # successfully inserted - return True - - def simple_upsert_txn_native_upsert( - self, txn, table, keyvalues, values, insertion_values={} - ): - """ - Use the native UPSERT functionality in recent PostgreSQL versions. - - Args: - table (str): The table to upsert into - keyvalues (dict): The unique key tables and their new values - values (dict): The nonunique columns and their new values - insertion_values (dict): additional key/values to use only when - inserting - Returns: - None - """ - allvalues = {} - allvalues.update(keyvalues) - allvalues.update(insertion_values) - - if not values: - latter = "NOTHING" - else: - allvalues.update(values) - latter = "UPDATE SET " + ", ".join(k + "=EXCLUDED." + k for k in values) - - sql = ("INSERT INTO %s (%s) VALUES (%s) ON CONFLICT (%s) DO %s") % ( - table, - ", ".join(k for k in allvalues), - ", ".join("?" for _ in allvalues), - ", ".join(k for k in keyvalues), - latter, - ) - txn.execute(sql, list(allvalues.values())) - - def simple_upsert_many_txn( - self, txn, table, key_names, key_values, value_names, value_values - ): - """ - Upsert, many times. - - Args: - table (str): The table to upsert into - key_names (list[str]): The key column names. - key_values (list[list]): A list of each row's key column values. - value_names (list[str]): The value column names. If empty, no - values will be used, even if value_values is provided. - value_values (list[list]): A list of each row's value column values. - Returns: - None - """ - if ( - self.database_engine.can_native_upsert - and table not in self._unsafe_to_upsert_tables - ): - return self.simple_upsert_many_txn_native_upsert( - txn, table, key_names, key_values, value_names, value_values - ) - else: - return self.simple_upsert_many_txn_emulated( - txn, table, key_names, key_values, value_names, value_values - ) - - def simple_upsert_many_txn_emulated( - self, txn, table, key_names, key_values, value_names, value_values - ): - """ - Upsert, many times, but without native UPSERT support or batching. - - Args: - table (str): The table to upsert into - key_names (list[str]): The key column names. - key_values (list[list]): A list of each row's key column values. - value_names (list[str]): The value column names. If empty, no - values will be used, even if value_values is provided. - value_values (list[list]): A list of each row's value column values. - Returns: - None - """ - # No value columns, therefore make a blank list so that the following - # zip() works correctly. - if not value_names: - value_values = [() for x in range(len(key_values))] - - for keyv, valv in zip(key_values, value_values): - _keys = {x: y for x, y in zip(key_names, keyv)} - _vals = {x: y for x, y in zip(value_names, valv)} - - self.simple_upsert_txn_emulated(txn, table, _keys, _vals) - - def simple_upsert_many_txn_native_upsert( - self, txn, table, key_names, key_values, value_names, value_values - ): - """ - Upsert, many times, using batching where possible. - - Args: - table (str): The table to upsert into - key_names (list[str]): The key column names. - key_values (list[list]): A list of each row's key column values. - value_names (list[str]): The value column names. If empty, no - values will be used, even if value_values is provided. - value_values (list[list]): A list of each row's value column values. - Returns: - None - """ - allnames = [] - allnames.extend(key_names) - allnames.extend(value_names) - - if not value_names: - # No value columns, therefore make a blank list so that the - # following zip() works correctly. - latter = "NOTHING" - value_values = [() for x in range(len(key_values))] - else: - latter = "UPDATE SET " + ", ".join( - k + "=EXCLUDED." + k for k in value_names - ) - - sql = "INSERT INTO %s (%s) VALUES (%s) ON CONFLICT (%s) DO %s" % ( - table, - ", ".join(k for k in allnames), - ", ".join("?" for _ in allnames), - ", ".join(key_names), - latter, - ) - - args = [] - - for x, y in zip(key_values, value_values): - args.append(tuple(x) + tuple(y)) - - return txn.execute_batch(sql, args) - - def simple_select_one( - self, table, keyvalues, retcols, allow_none=False, desc="simple_select_one" - ): - """Executes a SELECT query on the named table, which is expected to - return a single row, returning multiple columns from it. - - Args: - table : string giving the table name - keyvalues : dict of column names and values to select the row with - retcols : list of strings giving the names of the columns to return - - allow_none : If true, return None instead of failing if the SELECT - statement returns no rows - """ - return self.runInteraction( - desc, self.simple_select_one_txn, table, keyvalues, retcols, allow_none - ) - - def simple_select_one_onecol( - self, - table, - keyvalues, - retcol, - allow_none=False, - desc="simple_select_one_onecol", - ): - """Executes a SELECT query on the named table, which is expected to - return a single row, returning a single column from it. - - Args: - table : string giving the table name - keyvalues : dict of column names and values to select the row with - retcol : string giving the name of the column to return - """ - return self.runInteraction( - desc, - self.simple_select_one_onecol_txn, - table, - keyvalues, - retcol, - allow_none=allow_none, - ) - - @classmethod - def simple_select_one_onecol_txn( - cls, txn, table, keyvalues, retcol, allow_none=False - ): - ret = cls.simple_select_onecol_txn( - txn, table=table, keyvalues=keyvalues, retcol=retcol - ) - - if ret: - return ret[0] - else: - if allow_none: - return None - else: - raise StoreError(404, "No row found") - - @staticmethod - def simple_select_onecol_txn(txn, table, keyvalues, retcol): - sql = ("SELECT %(retcol)s FROM %(table)s") % {"retcol": retcol, "table": table} - - if keyvalues: - sql += " WHERE %s" % " AND ".join("%s = ?" % k for k in iterkeys(keyvalues)) - txn.execute(sql, list(keyvalues.values())) - else: - txn.execute(sql) - - return [r[0] for r in txn] - - def simple_select_onecol( - self, table, keyvalues, retcol, desc="simple_select_onecol" - ): - """Executes a SELECT query on the named table, which returns a list - comprising of the values of the named column from the selected rows. - - Args: - table (str): table name - keyvalues (dict|None): column names and values to select the rows with - retcol (str): column whos value we wish to retrieve. - - Returns: - Deferred: Results in a list - """ - return self.runInteraction( - desc, self.simple_select_onecol_txn, table, keyvalues, retcol - ) - - def simple_select_list(self, table, keyvalues, retcols, desc="simple_select_list"): - """Executes a SELECT query on the named table, which may return zero or - more rows, returning the result as a list of dicts. - - Args: - table (str): the table name - keyvalues (dict[str, Any] | None): - column names and values to select the rows with, or None to not - apply a WHERE clause. - retcols (iterable[str]): the names of the columns to return - Returns: - defer.Deferred: resolves to list[dict[str, Any]] - """ - return self.runInteraction( - desc, self.simple_select_list_txn, table, keyvalues, retcols - ) - - @classmethod - def simple_select_list_txn(cls, txn, table, keyvalues, retcols): - """Executes a SELECT query on the named table, which may return zero or - more rows, returning the result as a list of dicts. - - Args: - txn : Transaction object - table (str): the table name - keyvalues (dict[str, T] | None): - column names and values to select the rows with, or None to not - apply a WHERE clause. - retcols (iterable[str]): the names of the columns to return - """ - if keyvalues: - sql = "SELECT %s FROM %s WHERE %s" % ( - ", ".join(retcols), - table, - " AND ".join("%s = ?" % (k,) for k in keyvalues), - ) - txn.execute(sql, list(keyvalues.values())) - else: - sql = "SELECT %s FROM %s" % (", ".join(retcols), table) - txn.execute(sql) - - return cls.cursor_to_dict(txn) - - @defer.inlineCallbacks - def simple_select_many_batch( - self, - table, - column, - iterable, - retcols, - keyvalues={}, - desc="simple_select_many_batch", - batch_size=100, - ): - """Executes a SELECT query on the named table, which may return zero or - more rows, returning the result as a list of dicts. - - Filters rows by if value of `column` is in `iterable`. - - Args: - table : string giving the table name - column : column name to test for inclusion against `iterable` - iterable : list - keyvalues : dict of column names and values to select the rows with - retcols : list of strings giving the names of the columns to return - """ - results = [] - - if not iterable: - return results - - # iterables can not be sliced, so convert it to a list first - it_list = list(iterable) - - chunks = [ - it_list[i : i + batch_size] for i in range(0, len(it_list), batch_size) - ] - for chunk in chunks: - rows = yield self.runInteraction( - desc, - self.simple_select_many_txn, - table, - column, - chunk, - keyvalues, - retcols, - ) - - results.extend(rows) - - return results - - @classmethod - def simple_select_many_txn(cls, txn, table, column, iterable, keyvalues, retcols): - """Executes a SELECT query on the named table, which may return zero or - more rows, returning the result as a list of dicts. - - Filters rows by if value of `column` is in `iterable`. - - Args: - txn : Transaction object - table : string giving the table name - column : column name to test for inclusion against `iterable` - iterable : list - keyvalues : dict of column names and values to select the rows with - retcols : list of strings giving the names of the columns to return - """ - if not iterable: - return [] - - clause, values = make_in_list_sql_clause(txn.database_engine, column, iterable) - clauses = [clause] - - for key, value in iteritems(keyvalues): - clauses.append("%s = ?" % (key,)) - values.append(value) - - sql = "SELECT %s FROM %s WHERE %s" % ( - ", ".join(retcols), - table, - " AND ".join(clauses), - ) - - txn.execute(sql, values) - return cls.cursor_to_dict(txn) - - def simple_update(self, table, keyvalues, updatevalues, desc): - return self.runInteraction( - desc, self.simple_update_txn, table, keyvalues, updatevalues - ) - - @staticmethod - def simple_update_txn(txn, table, keyvalues, updatevalues): - if keyvalues: - where = "WHERE %s" % " AND ".join("%s = ?" % k for k in iterkeys(keyvalues)) - else: - where = "" - - update_sql = "UPDATE %s SET %s %s" % ( - table, - ", ".join("%s = ?" % (k,) for k in updatevalues), - where, - ) - - txn.execute(update_sql, list(updatevalues.values()) + list(keyvalues.values())) - - return txn.rowcount - - def simple_update_one( - self, table, keyvalues, updatevalues, desc="simple_update_one" - ): - """Executes an UPDATE query on the named table, setting new values for - columns in a row matching the key values. - - Args: - table : string giving the table name - keyvalues : dict of column names and values to select the row with - updatevalues : dict giving column names and values to update - retcols : optional list of column names to return - - If present, retcols gives a list of column names on which to perform - a SELECT statement *before* performing the UPDATE statement. The values - of these will be returned in a dict. - - These are performed within the same transaction, allowing an atomic - get-and-set. This can be used to implement compare-and-set by putting - the update column in the 'keyvalues' dict as well. - """ - return self.runInteraction( - desc, self.simple_update_one_txn, table, keyvalues, updatevalues - ) - - @classmethod - def simple_update_one_txn(cls, txn, table, keyvalues, updatevalues): - rowcount = cls.simple_update_txn(txn, table, keyvalues, updatevalues) - - if rowcount == 0: - raise StoreError(404, "No row found (%s)" % (table,)) - if rowcount > 1: - raise StoreError(500, "More than one row matched (%s)" % (table,)) - - @staticmethod - def simple_select_one_txn(txn, table, keyvalues, retcols, allow_none=False): - select_sql = "SELECT %s FROM %s WHERE %s" % ( - ", ".join(retcols), - table, - " AND ".join("%s = ?" % (k,) for k in keyvalues), - ) - - txn.execute(select_sql, list(keyvalues.values())) - row = txn.fetchone() - - if not row: - if allow_none: - return None - raise StoreError(404, "No row found (%s)" % (table,)) - if txn.rowcount > 1: - raise StoreError(500, "More than one row matched (%s)" % (table,)) - - return dict(zip(retcols, row)) - - def simple_delete_one(self, table, keyvalues, desc="simple_delete_one"): - """Executes a DELETE query on the named table, expecting to delete a - single row. - - Args: - table : string giving the table name - keyvalues : dict of column names and values to select the row with - """ - return self.runInteraction(desc, self.simple_delete_one_txn, table, keyvalues) - - @staticmethod - def simple_delete_one_txn(txn, table, keyvalues): - """Executes a DELETE query on the named table, expecting to delete a - single row. - - Args: - table : string giving the table name - keyvalues : dict of column names and values to select the row with - """ - sql = "DELETE FROM %s WHERE %s" % ( - table, - " AND ".join("%s = ?" % (k,) for k in keyvalues), - ) - - txn.execute(sql, list(keyvalues.values())) - if txn.rowcount == 0: - raise StoreError(404, "No row found (%s)" % (table,)) - if txn.rowcount > 1: - raise StoreError(500, "More than one row matched (%s)" % (table,)) - - def simple_delete(self, table, keyvalues, desc): - return self.runInteraction(desc, self.simple_delete_txn, table, keyvalues) - - @staticmethod - def simple_delete_txn(txn, table, keyvalues): - sql = "DELETE FROM %s WHERE %s" % ( - table, - " AND ".join("%s = ?" % (k,) for k in keyvalues), - ) - - txn.execute(sql, list(keyvalues.values())) - return txn.rowcount - - def simple_delete_many(self, table, column, iterable, keyvalues, desc): - return self.runInteraction( - desc, self.simple_delete_many_txn, table, column, iterable, keyvalues - ) - - @staticmethod - def simple_delete_many_txn(txn, table, column, iterable, keyvalues): - """Executes a DELETE query on the named table. - - Filters rows by if value of `column` is in `iterable`. - - Args: - txn : Transaction object - table : string giving the table name - column : column name to test for inclusion against `iterable` - iterable : list - keyvalues : dict of column names and values to select the rows with - - Returns: - int: Number rows deleted - """ - if not iterable: - return 0 - - sql = "DELETE FROM %s" % table - - clause, values = make_in_list_sql_clause(txn.database_engine, column, iterable) - clauses = [clause] - - for key, value in iteritems(keyvalues): - clauses.append("%s = ?" % (key,)) - values.append(value) - - if clauses: - sql = "%s WHERE %s" % (sql, " AND ".join(clauses)) - txn.execute(sql, values) - - return txn.rowcount - - def get_cache_dict( - self, db_conn, table, entity_column, stream_column, max_value, limit=100000 - ): - # Fetch a mapping of room_id -> max stream position for "recent" rooms. - # It doesn't really matter how many we get, the StreamChangeCache will - # do the right thing to ensure it respects the max size of cache. - sql = ( - "SELECT %(entity)s, MAX(%(stream)s) FROM %(table)s" - " WHERE %(stream)s > ? - %(limit)s" - " GROUP BY %(entity)s" - ) % { - "table": table, - "entity": entity_column, - "stream": stream_column, - "limit": limit, - } - - sql = self.database_engine.convert_param_style(sql) - - txn = db_conn.cursor() - txn.execute(sql, (int(max_value),)) - - cache = {row[0]: int(row[1]) for row in txn} - - txn.close() - - if cache: - min_val = min(itervalues(cache)) - else: - min_val = max_value - - return cache, min_val - def _invalidate_state_caches(self, room_id, members_changed): """Invalidates caches that are based on the current state, but does not stream invalidations down replication. @@ -1347,159 +71,6 @@ class SQLBaseStore(object): # which is fine. pass - def simple_select_list_paginate( - self, - table, - keyvalues, - orderby, - start, - limit, - retcols, - order_direction="ASC", - desc="simple_select_list_paginate", - ): - """ - Executes a SELECT query on the named table with start and limit, - of row numbers, which may return zero or number of rows from start to limit, - returning the result as a list of dicts. - - Args: - table (str): the table name - keyvalues (dict[str, T] | None): - column names and values to select the rows with, or None to not - apply a WHERE clause. - orderby (str): Column to order the results by. - start (int): Index to begin the query at. - limit (int): Number of results to return. - retcols (iterable[str]): the names of the columns to return - order_direction (str): Whether the results should be ordered "ASC" or "DESC". - Returns: - defer.Deferred: resolves to list[dict[str, Any]] - """ - return self.runInteraction( - desc, - self.simple_select_list_paginate_txn, - table, - keyvalues, - orderby, - start, - limit, - retcols, - order_direction=order_direction, - ) - - @classmethod - def simple_select_list_paginate_txn( - cls, - txn, - table, - keyvalues, - orderby, - start, - limit, - retcols, - order_direction="ASC", - ): - """ - Executes a SELECT query on the named table with start and limit, - of row numbers, which may return zero or number of rows from start to limit, - returning the result as a list of dicts. - - Args: - txn : Transaction object - table (str): the table name - keyvalues (dict[str, T] | None): - column names and values to select the rows with, or None to not - apply a WHERE clause. - orderby (str): Column to order the results by. - start (int): Index to begin the query at. - limit (int): Number of results to return. - retcols (iterable[str]): the names of the columns to return - order_direction (str): Whether the results should be ordered "ASC" or "DESC". - Returns: - defer.Deferred: resolves to list[dict[str, Any]] - """ - if order_direction not in ["ASC", "DESC"]: - raise ValueError("order_direction must be one of 'ASC' or 'DESC'.") - - if keyvalues: - where_clause = "WHERE " + " AND ".join("%s = ?" % (k,) for k in keyvalues) - else: - where_clause = "" - - sql = "SELECT %s FROM %s %s ORDER BY %s %s LIMIT ? OFFSET ?" % ( - ", ".join(retcols), - table, - where_clause, - orderby, - order_direction, - ) - txn.execute(sql, list(keyvalues.values()) + [limit, start]) - - return cls.cursor_to_dict(txn) - - def get_user_count_txn(self, txn): - """Get a total number of registered users in the users list. - - Args: - txn : Transaction object - Returns: - int : number of users - """ - sql_count = "SELECT COUNT(*) FROM users WHERE is_guest = 0;" - txn.execute(sql_count) - return txn.fetchone()[0] - - def simple_search_list(self, table, term, col, retcols, desc="simple_search_list"): - """Executes a SELECT query on the named table, which may return zero or - more rows, returning the result as a list of dicts. - - Args: - table (str): the table name - term (str | None): - term for searching the table matched to a column. - col (str): column to query term should be matched to - retcols (iterable[str]): the names of the columns to return - Returns: - defer.Deferred: resolves to list[dict[str, Any]] or None - """ - - return self.runInteraction( - desc, self.simple_search_list_txn, table, term, col, retcols - ) - - @classmethod - def simple_search_list_txn(cls, txn, table, term, col, retcols): - """Executes a SELECT query on the named table, which may return zero or - more rows, returning the result as a list of dicts. - - Args: - txn : Transaction object - table (str): the table name - term (str | None): - term for searching the table matched to a column. - col (str): column to query term should be matched to - retcols (iterable[str]): the names of the columns to return - Returns: - defer.Deferred: resolves to list[dict[str, Any]] or None - """ - if term: - sql = "SELECT %s FROM %s WHERE %s LIKE ?" % (", ".join(retcols), table, col) - termvalues = ["%%" + term + "%%"] - txn.execute(sql, termvalues) - else: - return 0 - - return cls.cursor_to_dict(txn) - - -class _RollbackButIsFineException(Exception): - """ This exception is used to rollback a transaction without implying - something went wrong. - """ - - pass - def db_to_json(db_content): """ @@ -1528,30 +99,3 @@ def db_to_json(db_content): except Exception: logging.warning("Tried to decode '%r' as JSON and failed", db_content) raise - - -def make_in_list_sql_clause( - database_engine, column: str, iterable: Iterable -) -> Tuple[str, Iterable]: - """Returns an SQL clause that checks the given column is in the iterable. - - On SQLite this expands to `column IN (?, ?, ...)`, whereas on Postgres - it expands to `column = ANY(?)`. While both DBs support the `IN` form, - using the `ANY` form on postgres means that it views queries with - different length iterables as the same, helping the query stats. - - Args: - database_engine - column: Name of the column - iterable: The values to check the column against. - - Returns: - A tuple of SQL query and the args - """ - - if database_engine.supports_using_any_list: - # This should hopefully be faster, but also makes postgres query - # stats easier to understand. - return "%s = ANY(?)" % (column,), [list(iterable)] - else: - return "%s IN (%s)" % (column, ",".join("?" for _ in iterable)), list(iterable) diff --git a/synapse/storage/background_updates.py b/synapse/storage/background_updates.py index 06955a0537..dfca94b0e0 100644 --- a/synapse/storage/background_updates.py +++ b/synapse/storage/background_updates.py @@ -139,7 +139,7 @@ class BackgroundUpdateStore(SQLBaseStore): # otherwise, check if there are updates to be run. This is important, # as we may be running on a worker which doesn't perform the bg updates # itself, but still wants to wait for them to happen. - updates = yield self.simple_select_onecol( + updates = yield self.db.simple_select_onecol( "background_updates", keyvalues=None, retcol="1", @@ -161,7 +161,7 @@ class BackgroundUpdateStore(SQLBaseStore): if update_name in self._background_update_queue: return False - update_exists = await self.simple_select_one_onecol( + update_exists = await self.db.simple_select_one_onecol( "background_updates", keyvalues={"update_name": update_name}, retcol="1", @@ -184,7 +184,7 @@ class BackgroundUpdateStore(SQLBaseStore): no more work to do. """ if not self._background_update_queue: - updates = yield self.simple_select_list( + updates = yield self.db.simple_select_list( "background_updates", keyvalues=None, retcols=("update_name", "depends_on"), @@ -226,7 +226,7 @@ class BackgroundUpdateStore(SQLBaseStore): else: batch_size = self.DEFAULT_BACKGROUND_BATCH_SIZE - progress_json = yield self.simple_select_one_onecol( + progress_json = yield self.db.simple_select_one_onecol( "background_updates", keyvalues={"update_name": update_name}, retcol="progress_json", @@ -391,7 +391,7 @@ class BackgroundUpdateStore(SQLBaseStore): def updater(progress, batch_size): if runner is not None: logger.info("Adding index %s to %s", index_name, table) - yield self.runWithConnection(runner) + yield self.db.runWithConnection(runner) yield self._end_background_update(update_name) return 1 @@ -413,7 +413,7 @@ class BackgroundUpdateStore(SQLBaseStore): self._background_update_queue = [] progress_json = json.dumps(progress) - return self.simple_insert( + return self.db.simple_insert( "background_updates", {"update_name": update_name, "progress_json": progress_json}, ) @@ -429,7 +429,7 @@ class BackgroundUpdateStore(SQLBaseStore): self._background_update_queue = [ name for name in self._background_update_queue if name != update_name ] - return self.simple_delete_one( + return self.db.simple_delete_one( "background_updates", keyvalues={"update_name": update_name} ) @@ -444,7 +444,7 @@ class BackgroundUpdateStore(SQLBaseStore): progress_json = json.dumps(progress) - self.simple_update_one_txn( + self.db.simple_update_one_txn( txn, "background_updates", keyvalues={"update_name": update_name}, diff --git a/synapse/storage/data_stores/main/__init__.py b/synapse/storage/data_stores/main/__init__.py index 2a5b33dda1..46f0f26af6 100644 --- a/synapse/storage/data_stores/main/__init__.py +++ b/synapse/storage/data_stores/main/__init__.py @@ -171,9 +171,11 @@ class DataStore( else: self._cache_id_gen = None + super(DataStore, self).__init__(db_conn, hs) + self._presence_on_startup = self._get_active_presence(db_conn) - presence_cache_prefill, min_presence_val = self.get_cache_dict( + presence_cache_prefill, min_presence_val = self.db.get_cache_dict( db_conn, "presence_stream", entity_column="user_id", @@ -187,7 +189,7 @@ class DataStore( ) max_device_inbox_id = self._device_inbox_id_gen.get_current_token() - device_inbox_prefill, min_device_inbox_id = self.get_cache_dict( + device_inbox_prefill, min_device_inbox_id = self.db.get_cache_dict( db_conn, "device_inbox", entity_column="user_id", @@ -202,7 +204,7 @@ class DataStore( ) # The federation outbox and the local device inbox uses the same # stream_id generator. - device_outbox_prefill, min_device_outbox_id = self.get_cache_dict( + device_outbox_prefill, min_device_outbox_id = self.db.get_cache_dict( db_conn, "device_federation_outbox", entity_column="destination", @@ -228,7 +230,7 @@ class DataStore( ) events_max = self._stream_id_gen.get_current_token() - curr_state_delta_prefill, min_curr_state_delta_id = self.get_cache_dict( + curr_state_delta_prefill, min_curr_state_delta_id = self.db.get_cache_dict( db_conn, "current_state_delta_stream", entity_column="room_id", @@ -242,7 +244,7 @@ class DataStore( prefilled_cache=curr_state_delta_prefill, ) - _group_updates_prefill, min_group_updates_id = self.get_cache_dict( + _group_updates_prefill, min_group_updates_id = self.db.get_cache_dict( db_conn, "local_group_updates", entity_column="user_id", @@ -262,8 +264,6 @@ class DataStore( # Used in _generate_user_daily_visits to keep track of progress self._last_user_visit_update = self._get_start_of_day() - super(DataStore, self).__init__(db_conn, hs) - def take_presence_startup_info(self): active_on_startup = self._presence_on_startup self._presence_on_startup = None @@ -283,7 +283,7 @@ class DataStore( txn = db_conn.cursor() txn.execute(sql, (PresenceState.OFFLINE,)) - rows = self.cursor_to_dict(txn) + rows = self.db.cursor_to_dict(txn) txn.close() for row in rows: @@ -296,7 +296,7 @@ class DataStore( Counts the number of users who used this homeserver in the last 24 hours. """ yesterday = int(self._clock.time_msec()) - (1000 * 60 * 60 * 24) - return self.runInteraction("count_daily_users", self._count_users, yesterday) + return self.db.runInteraction("count_daily_users", self._count_users, yesterday) def count_monthly_users(self): """ @@ -306,7 +306,7 @@ class DataStore( amongst other things, includes a 3 day grace period before a user counts. """ thirty_days_ago = int(self._clock.time_msec()) - (1000 * 60 * 60 * 24 * 30) - return self.runInteraction( + return self.db.runInteraction( "count_monthly_users", self._count_users, thirty_days_ago ) @@ -406,7 +406,7 @@ class DataStore( return results - return self.runInteraction("count_r30_users", _count_r30_users) + return self.db.runInteraction("count_r30_users", _count_r30_users) def _get_start_of_day(self): """ @@ -471,7 +471,7 @@ class DataStore( # frequently self._last_user_visit_update = now - return self.runInteraction( + return self.db.runInteraction( "generate_user_daily_visits", _generate_user_daily_visits ) @@ -482,7 +482,7 @@ class DataStore( Returns: defer.Deferred: resolves to list[dict[str, Any]] """ - return self.simple_select_list( + return self.db.simple_select_list( table="users", keyvalues={}, retcols=["name", "password_hash", "is_guest", "admin", "user_type"], @@ -502,9 +502,9 @@ class DataStore( Returns: defer.Deferred: resolves to json object {list[dict[str, Any]], count} """ - users = yield self.runInteraction( + users = yield self.db.runInteraction( "get_users_paginate", - self.simple_select_list_paginate_txn, + self.db.simple_select_list_paginate_txn, table="users", keyvalues={"is_guest": False}, orderby=order, @@ -512,7 +512,9 @@ class DataStore( limit=limit, retcols=["name", "password_hash", "is_guest", "admin", "user_type"], ) - count = yield self.runInteraction("get_users_paginate", self.get_user_count_txn) + count = yield self.db.runInteraction( + "get_users_paginate", self.get_user_count_txn + ) retval = {"users": users, "total": count} return retval @@ -526,7 +528,7 @@ class DataStore( Returns: defer.Deferred: resolves to list[dict[str, Any]] """ - return self.simple_search_list( + return self.db.simple_search_list( table="users", term=term, col="name", diff --git a/synapse/storage/data_stores/main/account_data.py b/synapse/storage/data_stores/main/account_data.py index b0d22faf3f..a96fe9485c 100644 --- a/synapse/storage/data_stores/main/account_data.py +++ b/synapse/storage/data_stores/main/account_data.py @@ -67,7 +67,7 @@ class AccountDataWorkerStore(SQLBaseStore): """ def get_account_data_for_user_txn(txn): - rows = self.simple_select_list_txn( + rows = self.db.simple_select_list_txn( txn, "account_data", {"user_id": user_id}, @@ -78,7 +78,7 @@ class AccountDataWorkerStore(SQLBaseStore): row["account_data_type"]: json.loads(row["content"]) for row in rows } - rows = self.simple_select_list_txn( + rows = self.db.simple_select_list_txn( txn, "room_account_data", {"user_id": user_id}, @@ -92,7 +92,7 @@ class AccountDataWorkerStore(SQLBaseStore): return global_account_data, by_room - return self.runInteraction( + return self.db.runInteraction( "get_account_data_for_user", get_account_data_for_user_txn ) @@ -102,7 +102,7 @@ class AccountDataWorkerStore(SQLBaseStore): Returns: Deferred: A dict """ - result = yield self.simple_select_one_onecol( + result = yield self.db.simple_select_one_onecol( table="account_data", keyvalues={"user_id": user_id, "account_data_type": data_type}, retcol="content", @@ -127,7 +127,7 @@ class AccountDataWorkerStore(SQLBaseStore): """ def get_account_data_for_room_txn(txn): - rows = self.simple_select_list_txn( + rows = self.db.simple_select_list_txn( txn, "room_account_data", {"user_id": user_id, "room_id": room_id}, @@ -138,7 +138,7 @@ class AccountDataWorkerStore(SQLBaseStore): row["account_data_type"]: json.loads(row["content"]) for row in rows } - return self.runInteraction( + return self.db.runInteraction( "get_account_data_for_room", get_account_data_for_room_txn ) @@ -156,7 +156,7 @@ class AccountDataWorkerStore(SQLBaseStore): """ def get_account_data_for_room_and_type_txn(txn): - content_json = self.simple_select_one_onecol_txn( + content_json = self.db.simple_select_one_onecol_txn( txn, table="room_account_data", keyvalues={ @@ -170,7 +170,7 @@ class AccountDataWorkerStore(SQLBaseStore): return json.loads(content_json) if content_json else None - return self.runInteraction( + return self.db.runInteraction( "get_account_data_for_room_and_type", get_account_data_for_room_and_type_txn ) @@ -207,7 +207,7 @@ class AccountDataWorkerStore(SQLBaseStore): room_results = txn.fetchall() return global_results, room_results - return self.runInteraction( + return self.db.runInteraction( "get_all_updated_account_data_txn", get_updated_account_data_txn ) @@ -252,7 +252,7 @@ class AccountDataWorkerStore(SQLBaseStore): if not changed: return {}, {} - return self.runInteraction( + return self.db.runInteraction( "get_updated_account_data_for_user", get_updated_account_data_for_user_txn ) @@ -302,7 +302,7 @@ class AccountDataStore(AccountDataWorkerStore): # no need to lock here as room_account_data has a unique constraint # on (user_id, room_id, account_data_type) so simple_upsert will # retry if there is a conflict. - yield self.simple_upsert( + yield self.db.simple_upsert( desc="add_room_account_data", table="room_account_data", keyvalues={ @@ -348,7 +348,7 @@ class AccountDataStore(AccountDataWorkerStore): # no need to lock here as account_data has a unique constraint on # (user_id, account_data_type) so simple_upsert will retry if # there is a conflict. - yield self.simple_upsert( + yield self.db.simple_upsert( desc="add_user_account_data", table="account_data", keyvalues={"user_id": user_id, "account_data_type": account_data_type}, @@ -388,4 +388,4 @@ class AccountDataStore(AccountDataWorkerStore): ) txn.execute(update_max_id_sql, (next_id, next_id)) - return self.runInteraction("update_account_data_max_stream_id", _update) + return self.db.runInteraction("update_account_data_max_stream_id", _update) diff --git a/synapse/storage/data_stores/main/appservice.py b/synapse/storage/data_stores/main/appservice.py index 6b82fd392a..6b2e12719c 100644 --- a/synapse/storage/data_stores/main/appservice.py +++ b/synapse/storage/data_stores/main/appservice.py @@ -133,7 +133,7 @@ class ApplicationServiceTransactionWorkerStore( A Deferred which resolves to a list of ApplicationServices, which may be empty. """ - results = yield self.simple_select_list( + results = yield self.db.simple_select_list( "application_services_state", dict(state=state), ["as_id"] ) # NB: This assumes this class is linked with ApplicationServiceStore @@ -155,7 +155,7 @@ class ApplicationServiceTransactionWorkerStore( Returns: A Deferred which resolves to ApplicationServiceState. """ - result = yield self.simple_select_one( + result = yield self.db.simple_select_one( "application_services_state", dict(as_id=service.id), ["state"], @@ -175,7 +175,7 @@ class ApplicationServiceTransactionWorkerStore( Returns: A Deferred which resolves when the state was set successfully. """ - return self.simple_upsert( + return self.db.simple_upsert( "application_services_state", dict(as_id=service.id), dict(state=state) ) @@ -216,7 +216,7 @@ class ApplicationServiceTransactionWorkerStore( ) return AppServiceTransaction(service=service, id=new_txn_id, events=events) - return self.runInteraction("create_appservice_txn", _create_appservice_txn) + return self.db.runInteraction("create_appservice_txn", _create_appservice_txn) def complete_appservice_txn(self, txn_id, service): """Completes an application service transaction. @@ -249,7 +249,7 @@ class ApplicationServiceTransactionWorkerStore( ) # Set current txn_id for AS to 'txn_id' - self.simple_upsert_txn( + self.db.simple_upsert_txn( txn, "application_services_state", dict(as_id=service.id), @@ -257,11 +257,13 @@ class ApplicationServiceTransactionWorkerStore( ) # Delete txn - self.simple_delete_txn( + self.db.simple_delete_txn( txn, "application_services_txns", dict(txn_id=txn_id, as_id=service.id) ) - return self.runInteraction("complete_appservice_txn", _complete_appservice_txn) + return self.db.runInteraction( + "complete_appservice_txn", _complete_appservice_txn + ) @defer.inlineCallbacks def get_oldest_unsent_txn(self, service): @@ -283,7 +285,7 @@ class ApplicationServiceTransactionWorkerStore( " ORDER BY txn_id ASC LIMIT 1", (service.id,), ) - rows = self.cursor_to_dict(txn) + rows = self.db.cursor_to_dict(txn) if not rows: return None @@ -291,7 +293,7 @@ class ApplicationServiceTransactionWorkerStore( return entry - entry = yield self.runInteraction( + entry = yield self.db.runInteraction( "get_oldest_unsent_appservice_txn", _get_oldest_unsent_txn ) @@ -321,7 +323,7 @@ class ApplicationServiceTransactionWorkerStore( "UPDATE appservice_stream_position SET stream_ordering = ?", (pos,) ) - return self.runInteraction( + return self.db.runInteraction( "set_appservice_last_pos", set_appservice_last_pos_txn ) @@ -350,7 +352,7 @@ class ApplicationServiceTransactionWorkerStore( return upper_bound, [row[1] for row in rows] - upper_bound, event_ids = yield self.runInteraction( + upper_bound, event_ids = yield self.db.runInteraction( "get_new_events_for_appservice", get_new_events_for_appservice_txn ) diff --git a/synapse/storage/data_stores/main/cache.py b/synapse/storage/data_stores/main/cache.py index de3256049d..54ed8574c4 100644 --- a/synapse/storage/data_stores/main/cache.py +++ b/synapse/storage/data_stores/main/cache.py @@ -95,7 +95,7 @@ class CacheInvalidationStore(SQLBaseStore): txn.call_after(ctx.__exit__, None, None, None) txn.call_after(self.hs.get_notifier().on_new_replication_data) - self.simple_insert_txn( + self.db.simple_insert_txn( txn, table="cache_invalidation_stream", values={ @@ -122,7 +122,9 @@ class CacheInvalidationStore(SQLBaseStore): txn.execute(sql, (last_id, limit)) return txn.fetchall() - return self.runInteraction("get_all_updated_caches", get_all_updated_caches_txn) + return self.db.runInteraction( + "get_all_updated_caches", get_all_updated_caches_txn + ) def get_cache_stream_token(self): if self._cache_id_gen: diff --git a/synapse/storage/data_stores/main/client_ips.py b/synapse/storage/data_stores/main/client_ips.py index 66522a04b7..6f2a720b97 100644 --- a/synapse/storage/data_stores/main/client_ips.py +++ b/synapse/storage/data_stores/main/client_ips.py @@ -91,7 +91,7 @@ class ClientIpBackgroundUpdateStore(background_updates.BackgroundUpdateStore): txn.execute("DROP INDEX IF EXISTS user_ips_user_ip") txn.close() - yield self.runWithConnection(f) + yield self.db.runWithConnection(f) yield self._end_background_update("user_ips_drop_nonunique_index") return 1 @@ -106,7 +106,7 @@ class ClientIpBackgroundUpdateStore(background_updates.BackgroundUpdateStore): def user_ips_analyze(txn): txn.execute("ANALYZE user_ips") - yield self.runInteraction("user_ips_analyze", user_ips_analyze) + yield self.db.runInteraction("user_ips_analyze", user_ips_analyze) yield self._end_background_update("user_ips_analyze") @@ -140,7 +140,7 @@ class ClientIpBackgroundUpdateStore(background_updates.BackgroundUpdateStore): return None # Get a last seen that has roughly `batch_size` since `begin_last_seen` - end_last_seen = yield self.runInteraction( + end_last_seen = yield self.db.runInteraction( "user_ips_dups_get_last_seen", get_last_seen ) @@ -275,7 +275,7 @@ class ClientIpBackgroundUpdateStore(background_updates.BackgroundUpdateStore): txn, "user_ips_remove_dupes", {"last_seen": end_last_seen} ) - yield self.runInteraction("user_ips_dups_remove", remove) + yield self.db.runInteraction("user_ips_dups_remove", remove) if last: yield self._end_background_update("user_ips_remove_dupes") @@ -352,7 +352,7 @@ class ClientIpBackgroundUpdateStore(background_updates.BackgroundUpdateStore): return len(rows) - updated = yield self.runInteraction( + updated = yield self.db.runInteraction( "_devices_last_seen_update", _devices_last_seen_update_txn ) @@ -417,12 +417,12 @@ class ClientIpStore(ClientIpBackgroundUpdateStore): to_update = self._batch_row_update self._batch_row_update = {} - return self.runInteraction( + return self.db.runInteraction( "_update_client_ips_batch", self._update_client_ips_batch_txn, to_update ) def _update_client_ips_batch_txn(self, txn, to_update): - if "user_ips" in self._unsafe_to_upsert_tables or ( + if "user_ips" in self.db._unsafe_to_upsert_tables or ( not self.database_engine.can_native_upsert ): self.database_engine.lock_table(txn, "user_ips") @@ -431,7 +431,7 @@ class ClientIpStore(ClientIpBackgroundUpdateStore): (user_id, access_token, ip), (user_agent, device_id, last_seen) = entry try: - self.simple_upsert_txn( + self.db.simple_upsert_txn( txn, table="user_ips", keyvalues={ @@ -450,7 +450,7 @@ class ClientIpStore(ClientIpBackgroundUpdateStore): # Technically an access token might not be associated with # a device so we need to check. if device_id: - self.simple_upsert_txn( + self.db.simple_upsert_txn( txn, table="devices", keyvalues={"user_id": user_id, "device_id": device_id}, @@ -483,7 +483,7 @@ class ClientIpStore(ClientIpBackgroundUpdateStore): if device_id is not None: keyvalues["device_id"] = device_id - res = yield self.simple_select_list( + res = yield self.db.simple_select_list( table="devices", keyvalues=keyvalues, retcols=("user_id", "ip", "user_agent", "device_id", "last_seen"), @@ -516,7 +516,7 @@ class ClientIpStore(ClientIpBackgroundUpdateStore): user_agent, _, last_seen = self._batch_row_update[key] results[(access_token, ip)] = (user_agent, last_seen) - rows = yield self.simple_select_list( + rows = yield self.db.simple_select_list( table="user_ips", keyvalues={"user_id": user_id}, retcols=["access_token", "ip", "user_agent", "last_seen"], @@ -577,4 +577,4 @@ class ClientIpStore(ClientIpBackgroundUpdateStore): def _prune_old_user_ips_txn(txn): txn.execute(sql, (timestamp,)) - await self.runInteraction("_prune_old_user_ips", _prune_old_user_ips_txn) + await self.db.runInteraction("_prune_old_user_ips", _prune_old_user_ips_txn) diff --git a/synapse/storage/data_stores/main/deviceinbox.py b/synapse/storage/data_stores/main/deviceinbox.py index 206d39134d..440793ad49 100644 --- a/synapse/storage/data_stores/main/deviceinbox.py +++ b/synapse/storage/data_stores/main/deviceinbox.py @@ -69,7 +69,7 @@ class DeviceInboxWorkerStore(SQLBaseStore): stream_pos = current_stream_id return messages, stream_pos - return self.runInteraction( + return self.db.runInteraction( "get_new_messages_for_device", get_new_messages_for_device_txn ) @@ -109,7 +109,7 @@ class DeviceInboxWorkerStore(SQLBaseStore): txn.execute(sql, (user_id, device_id, up_to_stream_id)) return txn.rowcount - count = yield self.runInteraction( + count = yield self.db.runInteraction( "delete_messages_for_device", delete_messages_for_device_txn ) @@ -178,7 +178,7 @@ class DeviceInboxWorkerStore(SQLBaseStore): stream_pos = current_stream_id return messages, stream_pos - return self.runInteraction( + return self.db.runInteraction( "get_new_device_msgs_for_remote", get_new_messages_for_remote_destination_txn, ) @@ -203,7 +203,7 @@ class DeviceInboxWorkerStore(SQLBaseStore): ) txn.execute(sql, (destination, up_to_stream_id)) - return self.runInteraction( + return self.db.runInteraction( "delete_device_msgs_for_remote", delete_messages_for_remote_destination_txn ) @@ -232,7 +232,7 @@ class DeviceInboxBackgroundUpdateStore(BackgroundUpdateStore): txn.execute("DROP INDEX IF EXISTS device_inbox_stream_id") txn.close() - yield self.runWithConnection(reindex_txn) + yield self.db.runWithConnection(reindex_txn) yield self._end_background_update(self.DEVICE_INBOX_STREAM_ID) @@ -294,7 +294,7 @@ class DeviceInboxStore(DeviceInboxWorkerStore, DeviceInboxBackgroundUpdateStore) with self._device_inbox_id_gen.get_next() as stream_id: now_ms = self.clock.time_msec() - yield self.runInteraction( + yield self.db.runInteraction( "add_messages_to_device_inbox", add_messages_txn, now_ms, stream_id ) for user_id in local_messages_by_user_then_device.keys(): @@ -314,7 +314,7 @@ class DeviceInboxStore(DeviceInboxWorkerStore, DeviceInboxBackgroundUpdateStore) # Check if we've already inserted a matching message_id for that # origin. This can happen if the origin doesn't receive our # acknowledgement from the first time we received the message. - already_inserted = self.simple_select_one_txn( + already_inserted = self.db.simple_select_one_txn( txn, table="device_federation_inbox", keyvalues={"origin": origin, "message_id": message_id}, @@ -326,7 +326,7 @@ class DeviceInboxStore(DeviceInboxWorkerStore, DeviceInboxBackgroundUpdateStore) # Add an entry for this message_id so that we know we've processed # it. - self.simple_insert_txn( + self.db.simple_insert_txn( txn, table="device_federation_inbox", values={ @@ -344,7 +344,7 @@ class DeviceInboxStore(DeviceInboxWorkerStore, DeviceInboxBackgroundUpdateStore) with self._device_inbox_id_gen.get_next() as stream_id: now_ms = self.clock.time_msec() - yield self.runInteraction( + yield self.db.runInteraction( "add_messages_from_remote_to_device_inbox", add_messages_txn, now_ms, @@ -465,6 +465,6 @@ class DeviceInboxStore(DeviceInboxWorkerStore, DeviceInboxBackgroundUpdateStore) return rows - return self.runInteraction( + return self.db.runInteraction( "get_all_new_device_messages", get_all_new_device_messages_txn ) diff --git a/synapse/storage/data_stores/main/devices.py b/synapse/storage/data_stores/main/devices.py index 727c582121..d98511ddd4 100644 --- a/synapse/storage/data_stores/main/devices.py +++ b/synapse/storage/data_stores/main/devices.py @@ -61,7 +61,7 @@ class DeviceWorkerStore(SQLBaseStore): Raises: StoreError: if the device is not found """ - return self.simple_select_one( + return self.db.simple_select_one( table="devices", keyvalues={"user_id": user_id, "device_id": device_id, "hidden": False}, retcols=("user_id", "device_id", "display_name"), @@ -80,7 +80,7 @@ class DeviceWorkerStore(SQLBaseStore): containing "device_id", "user_id" and "display_name" for each device. """ - devices = yield self.simple_select_list( + devices = yield self.db.simple_select_list( table="devices", keyvalues={"user_id": user_id, "hidden": False}, retcols=("user_id", "device_id", "display_name"), @@ -122,7 +122,7 @@ class DeviceWorkerStore(SQLBaseStore): # consider the device update to be too large, and simply skip the # stream_id; the rationale being that such a large device list update # is likely an error. - updates = yield self.runInteraction( + updates = yield self.db.runInteraction( "get_device_updates_by_remote", self._get_device_updates_by_remote_txn, destination, @@ -283,7 +283,7 @@ class DeviceWorkerStore(SQLBaseStore): """ devices = ( - yield self.runInteraction( + yield self.db.runInteraction( "_get_e2e_device_keys_txn", self._get_e2e_device_keys_txn, query_map.keys(), @@ -340,12 +340,12 @@ class DeviceWorkerStore(SQLBaseStore): rows = txn.fetchall() return rows[0][0] - return self.runInteraction("get_last_device_update_for_remote_user", f) + return self.db.runInteraction("get_last_device_update_for_remote_user", f) def mark_as_sent_devices_by_remote(self, destination, stream_id): """Mark that updates have successfully been sent to the destination. """ - return self.runInteraction( + return self.db.runInteraction( "mark_as_sent_devices_by_remote", self._mark_as_sent_devices_by_remote_txn, destination, @@ -399,7 +399,7 @@ class DeviceWorkerStore(SQLBaseStore): """ with self._device_list_id_gen.get_next() as stream_id: - yield self.runInteraction( + yield self.db.runInteraction( "add_user_sig_change_to_streams", self._add_user_signature_change_txn, from_user_id, @@ -414,7 +414,7 @@ class DeviceWorkerStore(SQLBaseStore): from_user_id, stream_id, ) - self.simple_insert_txn( + self.db.simple_insert_txn( txn, "user_signature_stream", values={ @@ -466,7 +466,7 @@ class DeviceWorkerStore(SQLBaseStore): @cachedInlineCallbacks(num_args=2, tree=True) def _get_cached_user_device(self, user_id, device_id): - content = yield self.simple_select_one_onecol( + content = yield self.db.simple_select_one_onecol( table="device_lists_remote_cache", keyvalues={"user_id": user_id, "device_id": device_id}, retcol="content", @@ -476,7 +476,7 @@ class DeviceWorkerStore(SQLBaseStore): @cachedInlineCallbacks() def _get_cached_devices_for_user(self, user_id): - devices = yield self.simple_select_list( + devices = yield self.db.simple_select_list( table="device_lists_remote_cache", keyvalues={"user_id": user_id}, retcols=("device_id", "content"), @@ -492,7 +492,7 @@ class DeviceWorkerStore(SQLBaseStore): Returns: (stream_id, devices) """ - return self.runInteraction( + return self.db.runInteraction( "get_devices_with_keys_by_user", self._get_devices_with_keys_by_user_txn, user_id, @@ -565,7 +565,7 @@ class DeviceWorkerStore(SQLBaseStore): return changes - return self.runInteraction( + return self.db.runInteraction( "get_users_whose_devices_changed", _get_users_whose_devices_changed_txn ) @@ -584,7 +584,7 @@ class DeviceWorkerStore(SQLBaseStore): SELECT DISTINCT user_ids FROM user_signature_stream WHERE from_user_id = ? AND stream_id > ? """ - rows = yield self.execute( + rows = yield self.db.execute( "get_users_whose_signatures_changed", None, sql, user_id, from_key ) return set(user for row in rows for user in json.loads(row[0])) @@ -605,7 +605,7 @@ class DeviceWorkerStore(SQLBaseStore): WHERE ? < stream_id AND stream_id <= ? GROUP BY user_id, destination """ - return self.execute( + return self.db.execute( "get_all_device_list_changes_for_remotes", None, sql, from_key, to_key ) @@ -614,7 +614,7 @@ class DeviceWorkerStore(SQLBaseStore): """Get the last stream_id we got for a user. May be None if we haven't got any information for them. """ - return self.simple_select_one_onecol( + return self.db.simple_select_one_onecol( table="device_lists_remote_extremeties", keyvalues={"user_id": user_id}, retcol="stream_id", @@ -628,7 +628,7 @@ class DeviceWorkerStore(SQLBaseStore): inlineCallbacks=True, ) def get_device_list_last_stream_id_for_remotes(self, user_ids): - rows = yield self.simple_select_many_batch( + rows = yield self.db.simple_select_many_batch( table="device_lists_remote_extremeties", column="user_id", iterable=user_ids, @@ -685,7 +685,7 @@ class DeviceBackgroundUpdateStore(BackgroundUpdateStore): txn.execute("DROP INDEX IF EXISTS device_lists_remote_extremeties_id") txn.close() - yield self.runWithConnection(f) + yield self.db.runWithConnection(f) yield self._end_background_update(DROP_DEVICE_LIST_STREAMS_NON_UNIQUE_INDEXES) return 1 @@ -722,7 +722,7 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): return False try: - inserted = yield self.simple_insert( + inserted = yield self.db.simple_insert( "devices", values={ "user_id": user_id, @@ -736,7 +736,7 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): if not inserted: # if the device already exists, check if it's a real device, or # if the device ID is reserved by something else - hidden = yield self.simple_select_one_onecol( + hidden = yield self.db.simple_select_one_onecol( "devices", keyvalues={"user_id": user_id, "device_id": device_id}, retcol="hidden", @@ -771,7 +771,7 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): Returns: defer.Deferred """ - yield self.simple_delete_one( + yield self.db.simple_delete_one( table="devices", keyvalues={"user_id": user_id, "device_id": device_id, "hidden": False}, desc="delete_device", @@ -789,7 +789,7 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): Returns: defer.Deferred """ - yield self.simple_delete_many( + yield self.db.simple_delete_many( table="devices", column="device_id", iterable=device_ids, @@ -818,7 +818,7 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): updates["display_name"] = new_display_name if not updates: return defer.succeed(None) - return self.simple_update_one( + return self.db.simple_update_one( table="devices", keyvalues={"user_id": user_id, "device_id": device_id, "hidden": False}, updatevalues=updates, @@ -829,7 +829,7 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): def mark_remote_user_device_list_as_unsubscribed(self, user_id): """Mark that we no longer track device lists for remote user. """ - yield self.simple_delete( + yield self.db.simple_delete( table="device_lists_remote_extremeties", keyvalues={"user_id": user_id}, desc="mark_remote_user_device_list_as_unsubscribed", @@ -853,7 +853,7 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): Returns: Deferred[None] """ - return self.runInteraction( + return self.db.runInteraction( "update_remote_device_list_cache_entry", self._update_remote_device_list_cache_entry_txn, user_id, @@ -866,7 +866,7 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): self, txn, user_id, device_id, content, stream_id ): if content.get("deleted"): - self.simple_delete_txn( + self.db.simple_delete_txn( txn, table="device_lists_remote_cache", keyvalues={"user_id": user_id, "device_id": device_id}, @@ -874,7 +874,7 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): txn.call_after(self.device_id_exists_cache.invalidate, (user_id, device_id)) else: - self.simple_upsert_txn( + self.db.simple_upsert_txn( txn, table="device_lists_remote_cache", keyvalues={"user_id": user_id, "device_id": device_id}, @@ -890,7 +890,7 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): self.get_device_list_last_stream_id_for_remote.invalidate, (user_id,) ) - self.simple_upsert_txn( + self.db.simple_upsert_txn( txn, table="device_lists_remote_extremeties", keyvalues={"user_id": user_id}, @@ -914,7 +914,7 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): Returns: Deferred[None] """ - return self.runInteraction( + return self.db.runInteraction( "update_remote_device_list_cache", self._update_remote_device_list_cache_txn, user_id, @@ -923,11 +923,11 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): ) def _update_remote_device_list_cache_txn(self, txn, user_id, devices, stream_id): - self.simple_delete_txn( + self.db.simple_delete_txn( txn, table="device_lists_remote_cache", keyvalues={"user_id": user_id} ) - self.simple_insert_many_txn( + self.db.simple_insert_many_txn( txn, table="device_lists_remote_cache", values=[ @@ -946,7 +946,7 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): self.get_device_list_last_stream_id_for_remote.invalidate, (user_id,) ) - self.simple_upsert_txn( + self.db.simple_upsert_txn( txn, table="device_lists_remote_extremeties", keyvalues={"user_id": user_id}, @@ -962,7 +962,7 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): (if any) should be poked. """ with self._device_list_id_gen.get_next() as stream_id: - yield self.runInteraction( + yield self.db.runInteraction( "add_device_change_to_streams", self._add_device_change_txn, user_id, @@ -995,7 +995,7 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): [(user_id, device_id, stream_id) for device_id in device_ids], ) - self.simple_insert_many_txn( + self.db.simple_insert_many_txn( txn, table="device_lists_stream", values=[ @@ -1006,7 +1006,7 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): context = get_active_span_text_map() - self.simple_insert_many_txn( + self.db.simple_insert_many_txn( txn, table="device_lists_outbound_pokes", values=[ @@ -1069,7 +1069,7 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): return run_as_background_process( "prune_old_outbound_device_pokes", - self.runInteraction, + self.db.runInteraction, "_prune_old_outbound_device_pokes", _prune_txn, ) diff --git a/synapse/storage/data_stores/main/directory.py b/synapse/storage/data_stores/main/directory.py index d332f8a409..c9e7de7d12 100644 --- a/synapse/storage/data_stores/main/directory.py +++ b/synapse/storage/data_stores/main/directory.py @@ -36,7 +36,7 @@ class DirectoryWorkerStore(SQLBaseStore): Deferred: results in namedtuple with keys "room_id" and "servers" or None if no association can be found """ - room_id = yield self.simple_select_one_onecol( + room_id = yield self.db.simple_select_one_onecol( "room_aliases", {"room_alias": room_alias.to_string()}, "room_id", @@ -47,7 +47,7 @@ class DirectoryWorkerStore(SQLBaseStore): if not room_id: return None - servers = yield self.simple_select_onecol( + servers = yield self.db.simple_select_onecol( "room_alias_servers", {"room_alias": room_alias.to_string()}, "server", @@ -60,7 +60,7 @@ class DirectoryWorkerStore(SQLBaseStore): return RoomAliasMapping(room_id, room_alias.to_string(), servers) def get_room_alias_creator(self, room_alias): - return self.simple_select_one_onecol( + return self.db.simple_select_one_onecol( table="room_aliases", keyvalues={"room_alias": room_alias}, retcol="creator", @@ -69,7 +69,7 @@ class DirectoryWorkerStore(SQLBaseStore): @cached(max_entries=5000) def get_aliases_for_room(self, room_id): - return self.simple_select_onecol( + return self.db.simple_select_onecol( "room_aliases", {"room_id": room_id}, "room_alias", @@ -93,7 +93,7 @@ class DirectoryStore(DirectoryWorkerStore): """ def alias_txn(txn): - self.simple_insert_txn( + self.db.simple_insert_txn( txn, "room_aliases", { @@ -103,7 +103,7 @@ class DirectoryStore(DirectoryWorkerStore): }, ) - self.simple_insert_many_txn( + self.db.simple_insert_many_txn( txn, table="room_alias_servers", values=[ @@ -117,7 +117,9 @@ class DirectoryStore(DirectoryWorkerStore): ) try: - ret = yield self.runInteraction("create_room_alias_association", alias_txn) + ret = yield self.db.runInteraction( + "create_room_alias_association", alias_txn + ) except self.database_engine.module.IntegrityError: raise SynapseError( 409, "Room alias %s already exists" % room_alias.to_string() @@ -126,7 +128,7 @@ class DirectoryStore(DirectoryWorkerStore): @defer.inlineCallbacks def delete_room_alias(self, room_alias): - room_id = yield self.runInteraction( + room_id = yield self.db.runInteraction( "delete_room_alias", self._delete_room_alias_txn, room_alias ) @@ -168,6 +170,6 @@ class DirectoryStore(DirectoryWorkerStore): txn, self.get_aliases_for_room, (new_room_id,) ) - return self.runInteraction( + return self.db.runInteraction( "_update_aliases_for_room_txn", _update_aliases_for_room_txn ) diff --git a/synapse/storage/data_stores/main/e2e_room_keys.py b/synapse/storage/data_stores/main/e2e_room_keys.py index df89eda337..84594cf0a9 100644 --- a/synapse/storage/data_stores/main/e2e_room_keys.py +++ b/synapse/storage/data_stores/main/e2e_room_keys.py @@ -38,7 +38,7 @@ class EndToEndRoomKeyStore(SQLBaseStore): StoreError """ - yield self.simple_update_one( + yield self.db.simple_update_one( table="e2e_room_keys", keyvalues={ "user_id": user_id, @@ -89,7 +89,7 @@ class EndToEndRoomKeyStore(SQLBaseStore): } ) - yield self.simple_insert_many( + yield self.db.simple_insert_many( table="e2e_room_keys", values=values, desc="add_e2e_room_keys" ) @@ -125,7 +125,7 @@ class EndToEndRoomKeyStore(SQLBaseStore): if session_id: keyvalues["session_id"] = session_id - rows = yield self.simple_select_list( + rows = yield self.db.simple_select_list( table="e2e_room_keys", keyvalues=keyvalues, retcols=( @@ -170,7 +170,7 @@ class EndToEndRoomKeyStore(SQLBaseStore): Deferred[dict[str, dict[str, dict]]]: a map of room IDs to session IDs to room key """ - return self.runInteraction( + return self.db.runInteraction( "get_e2e_room_keys_multi", self._get_e2e_room_keys_multi_txn, user_id, @@ -234,7 +234,7 @@ class EndToEndRoomKeyStore(SQLBaseStore): version (str): the version ID of the backup we're querying about """ - return self.simple_select_one_onecol( + return self.db.simple_select_one_onecol( table="e2e_room_keys", keyvalues={"user_id": user_id, "version": version}, retcol="COUNT(*)", @@ -267,7 +267,7 @@ class EndToEndRoomKeyStore(SQLBaseStore): if session_id: keyvalues["session_id"] = session_id - yield self.simple_delete( + yield self.db.simple_delete( table="e2e_room_keys", keyvalues=keyvalues, desc="delete_e2e_room_keys" ) @@ -312,7 +312,7 @@ class EndToEndRoomKeyStore(SQLBaseStore): # it isn't there. raise StoreError(404, "No row found") - result = self.simple_select_one_txn( + result = self.db.simple_select_one_txn( txn, table="e2e_room_keys_versions", keyvalues={"user_id": user_id, "version": this_version, "deleted": 0}, @@ -324,7 +324,7 @@ class EndToEndRoomKeyStore(SQLBaseStore): result["etag"] = 0 return result - return self.runInteraction( + return self.db.runInteraction( "get_e2e_room_keys_version_info", _get_e2e_room_keys_version_info_txn ) @@ -352,7 +352,7 @@ class EndToEndRoomKeyStore(SQLBaseStore): new_version = str(int(current_version) + 1) - self.simple_insert_txn( + self.db.simple_insert_txn( txn, table="e2e_room_keys_versions", values={ @@ -365,7 +365,7 @@ class EndToEndRoomKeyStore(SQLBaseStore): return new_version - return self.runInteraction( + return self.db.runInteraction( "create_e2e_room_keys_version_txn", _create_e2e_room_keys_version_txn ) @@ -391,7 +391,7 @@ class EndToEndRoomKeyStore(SQLBaseStore): updatevalues["etag"] = version_etag if updatevalues: - return self.simple_update( + return self.db.simple_update( table="e2e_room_keys_versions", keyvalues={"user_id": user_id, "version": version}, updatevalues=updatevalues, @@ -420,19 +420,19 @@ class EndToEndRoomKeyStore(SQLBaseStore): else: this_version = version - self.simple_delete_txn( + self.db.simple_delete_txn( txn, table="e2e_room_keys", keyvalues={"user_id": user_id, "version": this_version}, ) - return self.simple_update_one_txn( + return self.db.simple_update_one_txn( txn, table="e2e_room_keys_versions", keyvalues={"user_id": user_id, "version": this_version}, updatevalues={"deleted": 1}, ) - return self.runInteraction( + return self.db.runInteraction( "delete_e2e_room_keys_version", _delete_e2e_room_keys_version_txn ) diff --git a/synapse/storage/data_stores/main/end_to_end_keys.py b/synapse/storage/data_stores/main/end_to_end_keys.py index 08bcdc4725..38cd0ca9b8 100644 --- a/synapse/storage/data_stores/main/end_to_end_keys.py +++ b/synapse/storage/data_stores/main/end_to_end_keys.py @@ -48,7 +48,7 @@ class EndToEndKeyWorkerStore(SQLBaseStore): if not query_list: return {} - results = yield self.runInteraction( + results = yield self.db.runInteraction( "get_e2e_device_keys", self._get_e2e_device_keys_txn, query_list, @@ -125,7 +125,7 @@ class EndToEndKeyWorkerStore(SQLBaseStore): ) txn.execute(sql, query_params) - rows = self.cursor_to_dict(txn) + rows = self.db.cursor_to_dict(txn) result = {} for row in rows: @@ -143,7 +143,7 @@ class EndToEndKeyWorkerStore(SQLBaseStore): ) txn.execute(signature_sql, signature_query_params) - rows = self.cursor_to_dict(txn) + rows = self.db.cursor_to_dict(txn) # add each cross-signing signature to the correct device in the result dict. for row in rows: @@ -186,7 +186,7 @@ class EndToEndKeyWorkerStore(SQLBaseStore): key_id) to json string for key """ - rows = yield self.simple_select_many_batch( + rows = yield self.db.simple_select_many_batch( table="e2e_one_time_keys_json", column="key_id", iterable=key_ids, @@ -219,7 +219,7 @@ class EndToEndKeyWorkerStore(SQLBaseStore): # a unique constraint. If there is a race of two calls to # `add_e2e_one_time_keys` then they'll conflict and we will only # insert one set. - self.simple_insert_many_txn( + self.db.simple_insert_many_txn( txn, table="e2e_one_time_keys_json", values=[ @@ -238,7 +238,7 @@ class EndToEndKeyWorkerStore(SQLBaseStore): txn, self.count_e2e_one_time_keys, (user_id, device_id) ) - yield self.runInteraction( + yield self.db.runInteraction( "add_e2e_one_time_keys_insert", _add_e2e_one_time_keys ) @@ -261,7 +261,9 @@ class EndToEndKeyWorkerStore(SQLBaseStore): result[algorithm] = key_count return result - return self.runInteraction("count_e2e_one_time_keys", _count_e2e_one_time_keys) + return self.db.runInteraction( + "count_e2e_one_time_keys", _count_e2e_one_time_keys + ) def _get_e2e_cross_signing_key_txn(self, txn, user_id, key_type, from_user_id=None): """Returns a user's cross-signing key. @@ -322,7 +324,7 @@ class EndToEndKeyWorkerStore(SQLBaseStore): Returns: dict of the key data or None if not found """ - return self.runInteraction( + return self.db.runInteraction( "get_e2e_cross_signing_key", self._get_e2e_cross_signing_key_txn, user_id, @@ -350,7 +352,7 @@ class EndToEndKeyWorkerStore(SQLBaseStore): WHERE ? < stream_id AND stream_id <= ? GROUP BY user_id """ - return self.execute( + return self.db.execute( "get_all_user_signature_changes_for_remotes", None, sql, from_key, to_key ) @@ -367,7 +369,7 @@ class EndToEndKeyStore(EndToEndKeyWorkerStore, SQLBaseStore): set_tag("time_now", time_now) set_tag("device_keys", device_keys) - old_key_json = self.simple_select_one_onecol_txn( + old_key_json = self.db.simple_select_one_onecol_txn( txn, table="e2e_device_keys_json", keyvalues={"user_id": user_id, "device_id": device_id}, @@ -383,7 +385,7 @@ class EndToEndKeyStore(EndToEndKeyWorkerStore, SQLBaseStore): log_kv({"Message": "Device key already stored."}) return False - self.simple_upsert_txn( + self.db.simple_upsert_txn( txn, table="e2e_device_keys_json", keyvalues={"user_id": user_id, "device_id": device_id}, @@ -392,7 +394,7 @@ class EndToEndKeyStore(EndToEndKeyWorkerStore, SQLBaseStore): log_kv({"message": "Device keys stored."}) return True - return self.runInteraction("set_e2e_device_keys", _set_e2e_device_keys_txn) + return self.db.runInteraction("set_e2e_device_keys", _set_e2e_device_keys_txn) def claim_e2e_one_time_keys(self, query_list): """Take a list of one time keys out of the database""" @@ -431,7 +433,9 @@ class EndToEndKeyStore(EndToEndKeyWorkerStore, SQLBaseStore): ) return result - return self.runInteraction("claim_e2e_one_time_keys", _claim_e2e_one_time_keys) + return self.db.runInteraction( + "claim_e2e_one_time_keys", _claim_e2e_one_time_keys + ) def delete_e2e_keys_by_device(self, user_id, device_id): def delete_e2e_keys_by_device_txn(txn): @@ -442,12 +446,12 @@ class EndToEndKeyStore(EndToEndKeyWorkerStore, SQLBaseStore): "user_id": user_id, } ) - self.simple_delete_txn( + self.db.simple_delete_txn( txn, table="e2e_device_keys_json", keyvalues={"user_id": user_id, "device_id": device_id}, ) - self.simple_delete_txn( + self.db.simple_delete_txn( txn, table="e2e_one_time_keys_json", keyvalues={"user_id": user_id, "device_id": device_id}, @@ -456,7 +460,7 @@ class EndToEndKeyStore(EndToEndKeyWorkerStore, SQLBaseStore): txn, self.count_e2e_one_time_keys, (user_id, device_id) ) - return self.runInteraction( + return self.db.runInteraction( "delete_e2e_keys_by_device", delete_e2e_keys_by_device_txn ) @@ -492,7 +496,7 @@ class EndToEndKeyStore(EndToEndKeyWorkerStore, SQLBaseStore): # The "keys" property must only have one entry, which will be the public # key, so we just grab the first value in there pubkey = next(iter(key["keys"].values())) - self.simple_insert_txn( + self.db.simple_insert_txn( txn, "devices", values={ @@ -505,7 +509,7 @@ class EndToEndKeyStore(EndToEndKeyWorkerStore, SQLBaseStore): # and finally, store the key itself with self._cross_signing_id_gen.get_next() as stream_id: - self.simple_insert_txn( + self.db.simple_insert_txn( txn, "e2e_cross_signing_keys", values={ @@ -524,7 +528,7 @@ class EndToEndKeyStore(EndToEndKeyWorkerStore, SQLBaseStore): key_type (str): the type of cross-signing key to set key (dict): the key data """ - return self.runInteraction( + return self.db.runInteraction( "add_e2e_cross_signing_key", self._set_e2e_cross_signing_key_txn, user_id, @@ -539,7 +543,7 @@ class EndToEndKeyStore(EndToEndKeyWorkerStore, SQLBaseStore): user_id (str): the user who made the signatures signatures (iterable[SignatureListItem]): signatures to add """ - return self.simple_insert_many( + return self.db.simple_insert_many( "e2e_cross_signing_signatures", [ { diff --git a/synapse/storage/data_stores/main/event_federation.py b/synapse/storage/data_stores/main/event_federation.py index 051ac7a8cb..77e4353b59 100644 --- a/synapse/storage/data_stores/main/event_federation.py +++ b/synapse/storage/data_stores/main/event_federation.py @@ -58,7 +58,7 @@ class EventFederationWorkerStore(EventsWorkerStore, SignatureWorkerStore, SQLBas Returns: list of event_ids """ - return self.runInteraction( + return self.db.runInteraction( "get_auth_chain_ids", self._get_auth_chain_ids_txn, event_ids, include_given ) @@ -90,12 +90,12 @@ class EventFederationWorkerStore(EventsWorkerStore, SignatureWorkerStore, SQLBas return list(results) def get_oldest_events_in_room(self, room_id): - return self.runInteraction( + return self.db.runInteraction( "get_oldest_events_in_room", self._get_oldest_events_in_room_txn, room_id ) def get_oldest_events_with_depth_in_room(self, room_id): - return self.runInteraction( + return self.db.runInteraction( "get_oldest_events_with_depth_in_room", self.get_oldest_events_with_depth_in_room_txn, room_id, @@ -126,7 +126,7 @@ class EventFederationWorkerStore(EventsWorkerStore, SignatureWorkerStore, SQLBas Returns Deferred[int] """ - rows = yield self.simple_select_many_batch( + rows = yield self.db.simple_select_many_batch( table="events", column="event_id", iterable=event_ids, @@ -140,7 +140,7 @@ class EventFederationWorkerStore(EventsWorkerStore, SignatureWorkerStore, SQLBas return max(row["depth"] for row in rows) def _get_oldest_events_in_room_txn(self, txn, room_id): - return self.simple_select_onecol_txn( + return self.db.simple_select_onecol_txn( txn, table="event_backward_extremities", keyvalues={"room_id": room_id}, @@ -188,7 +188,7 @@ class EventFederationWorkerStore(EventsWorkerStore, SignatureWorkerStore, SQLBas where *hashes* is a map from algorithm to hash. """ - return self.runInteraction( + return self.db.runInteraction( "get_latest_event_ids_and_hashes_in_room", self._get_latest_event_ids_and_hashes_in_room, room_id, @@ -229,13 +229,13 @@ class EventFederationWorkerStore(EventsWorkerStore, SignatureWorkerStore, SQLBas txn.execute(sql, query_args) return [room_id for room_id, in txn] - return self.runInteraction( + return self.db.runInteraction( "get_rooms_with_many_extremities", _get_rooms_with_many_extremities_txn ) @cached(max_entries=5000, iterable=True) def get_latest_event_ids_in_room(self, room_id): - return self.simple_select_onecol( + return self.db.simple_select_onecol( table="event_forward_extremities", keyvalues={"room_id": room_id}, retcol="event_id", @@ -266,12 +266,12 @@ class EventFederationWorkerStore(EventsWorkerStore, SignatureWorkerStore, SQLBas def get_min_depth(self, room_id): """ For hte given room, get the minimum depth we have seen for it. """ - return self.runInteraction( + return self.db.runInteraction( "get_min_depth", self._get_min_depth_interaction, room_id ) def _get_min_depth_interaction(self, txn, room_id): - min_depth = self.simple_select_one_onecol_txn( + min_depth = self.db.simple_select_one_onecol_txn( txn, table="room_depth", keyvalues={"room_id": room_id}, @@ -337,7 +337,7 @@ class EventFederationWorkerStore(EventsWorkerStore, SignatureWorkerStore, SQLBas txn.execute(sql, (stream_ordering, room_id)) return [event_id for event_id, in txn] - return self.runInteraction( + return self.db.runInteraction( "get_forward_extremeties_for_room", get_forward_extremeties_for_room_txn ) @@ -352,7 +352,7 @@ class EventFederationWorkerStore(EventsWorkerStore, SignatureWorkerStore, SQLBas limit (int) """ return ( - self.runInteraction( + self.db.runInteraction( "get_backfill_events", self._get_backfill_events, room_id, @@ -383,7 +383,7 @@ class EventFederationWorkerStore(EventsWorkerStore, SignatureWorkerStore, SQLBas queue = PriorityQueue() for event_id in event_list: - depth = self.simple_select_one_onecol_txn( + depth = self.db.simple_select_one_onecol_txn( txn, table="events", keyvalues={"event_id": event_id, "room_id": room_id}, @@ -415,7 +415,7 @@ class EventFederationWorkerStore(EventsWorkerStore, SignatureWorkerStore, SQLBas @defer.inlineCallbacks def get_missing_events(self, room_id, earliest_events, latest_events, limit): - ids = yield self.runInteraction( + ids = yield self.db.runInteraction( "get_missing_events", self._get_missing_events, room_id, @@ -468,7 +468,7 @@ class EventFederationWorkerStore(EventsWorkerStore, SignatureWorkerStore, SQLBas Returns: Deferred[list[str]] """ - rows = yield self.simple_select_many_batch( + rows = yield self.db.simple_select_many_batch( table="event_edges", column="prev_event_id", iterable=event_ids, @@ -508,7 +508,7 @@ class EventFederationStore(EventFederationWorkerStore): if min_depth and depth >= min_depth: return - self.simple_upsert_txn( + self.db.simple_upsert_txn( txn, table="room_depth", keyvalues={"room_id": room_id}, @@ -520,7 +520,7 @@ class EventFederationStore(EventFederationWorkerStore): For the given event, update the event edges table and forward and backward extremities tables. """ - self.simple_insert_many_txn( + self.db.simple_insert_many_txn( txn, table="event_edges", values=[ @@ -604,13 +604,13 @@ class EventFederationStore(EventFederationWorkerStore): return run_as_background_process( "delete_old_forward_extrem_cache", - self.runInteraction, + self.db.runInteraction, "_delete_old_forward_extrem_cache", _delete_old_forward_extrem_cache_txn, ) def clean_room_for_join(self, room_id): - return self.runInteraction( + return self.db.runInteraction( "clean_room_for_join", self._clean_room_for_join_txn, room_id ) @@ -660,7 +660,7 @@ class EventFederationStore(EventFederationWorkerStore): return min_stream_id >= target_min_stream_id - result = yield self.runInteraction( + result = yield self.db.runInteraction( self.EVENT_AUTH_STATE_ONLY, delete_event_auth ) diff --git a/synapse/storage/data_stores/main/event_push_actions.py b/synapse/storage/data_stores/main/event_push_actions.py index 0a37847cfd..725d0881dc 100644 --- a/synapse/storage/data_stores/main/event_push_actions.py +++ b/synapse/storage/data_stores/main/event_push_actions.py @@ -93,7 +93,7 @@ class EventPushActionsWorkerStore(SQLBaseStore): def get_unread_event_push_actions_by_room_for_user( self, room_id, user_id, last_read_event_id ): - ret = yield self.runInteraction( + ret = yield self.db.runInteraction( "get_unread_event_push_actions_by_room", self._get_unread_counts_by_receipt_txn, room_id, @@ -177,7 +177,7 @@ class EventPushActionsWorkerStore(SQLBaseStore): txn.execute(sql, (min_stream_ordering, max_stream_ordering)) return [r[0] for r in txn] - ret = yield self.runInteraction("get_push_action_users_in_range", f) + ret = yield self.db.runInteraction("get_push_action_users_in_range", f) return ret @defer.inlineCallbacks @@ -229,7 +229,7 @@ class EventPushActionsWorkerStore(SQLBaseStore): txn.execute(sql, args) return txn.fetchall() - after_read_receipt = yield self.runInteraction( + after_read_receipt = yield self.db.runInteraction( "get_unread_push_actions_for_user_in_range_http_arr", get_after_receipt ) @@ -257,7 +257,7 @@ class EventPushActionsWorkerStore(SQLBaseStore): txn.execute(sql, args) return txn.fetchall() - no_read_receipt = yield self.runInteraction( + no_read_receipt = yield self.db.runInteraction( "get_unread_push_actions_for_user_in_range_http_nrr", get_no_receipt ) @@ -329,7 +329,7 @@ class EventPushActionsWorkerStore(SQLBaseStore): txn.execute(sql, args) return txn.fetchall() - after_read_receipt = yield self.runInteraction( + after_read_receipt = yield self.db.runInteraction( "get_unread_push_actions_for_user_in_range_email_arr", get_after_receipt ) @@ -357,7 +357,7 @@ class EventPushActionsWorkerStore(SQLBaseStore): txn.execute(sql, args) return txn.fetchall() - no_read_receipt = yield self.runInteraction( + no_read_receipt = yield self.db.runInteraction( "get_unread_push_actions_for_user_in_range_email_nrr", get_no_receipt ) @@ -407,7 +407,7 @@ class EventPushActionsWorkerStore(SQLBaseStore): txn.execute(sql, (user_id, min_stream_ordering)) return bool(txn.fetchone()) - return self.runInteraction( + return self.db.runInteraction( "get_if_maybe_push_in_range_for_user", _get_if_maybe_push_in_range_for_user_txn, ) @@ -458,7 +458,7 @@ class EventPushActionsWorkerStore(SQLBaseStore): ), ) - return self.runInteraction( + return self.db.runInteraction( "add_push_actions_to_staging", _add_push_actions_to_staging_txn ) @@ -472,7 +472,7 @@ class EventPushActionsWorkerStore(SQLBaseStore): """ try: - res = yield self.simple_delete( + res = yield self.db.simple_delete( table="event_push_actions_staging", keyvalues={"event_id": event_id}, desc="remove_push_actions_from_staging", @@ -489,7 +489,7 @@ class EventPushActionsWorkerStore(SQLBaseStore): def _find_stream_orderings_for_times(self): return run_as_background_process( "event_push_action_stream_orderings", - self.runInteraction, + self.db.runInteraction, "_find_stream_orderings_for_times", self._find_stream_orderings_for_times_txn, ) @@ -525,7 +525,7 @@ class EventPushActionsWorkerStore(SQLBaseStore): Deferred[int]: stream ordering of the first event received on/after the timestamp """ - return self.runInteraction( + return self.db.runInteraction( "_find_first_stream_ordering_after_ts_txn", self._find_first_stream_ordering_after_ts_txn, ts, @@ -677,7 +677,7 @@ class EventPushActionsStore(EventPushActionsWorkerStore): ) for event, _ in events_and_contexts: - user_ids = self.simple_select_onecol_txn( + user_ids = self.db.simple_select_onecol_txn( txn, table="event_push_actions_staging", keyvalues={"event_id": event.event_id}, @@ -727,9 +727,9 @@ class EventPushActionsStore(EventPushActionsWorkerStore): " LIMIT ?" % (before_clause,) ) txn.execute(sql, args) - return self.cursor_to_dict(txn) + return self.db.cursor_to_dict(txn) - push_actions = yield self.runInteraction("get_push_actions_for_user", f) + push_actions = yield self.db.runInteraction("get_push_actions_for_user", f) for pa in push_actions: pa["actions"] = _deserialize_action(pa["actions"], pa["highlight"]) return push_actions @@ -748,7 +748,7 @@ class EventPushActionsStore(EventPushActionsWorkerStore): txn.execute(sql, (stream_ordering,)) return txn.fetchone() - result = yield self.runInteraction("get_time_of_last_push_action_before", f) + result = yield self.db.runInteraction("get_time_of_last_push_action_before", f) return result[0] if result else None @defer.inlineCallbacks @@ -757,7 +757,9 @@ class EventPushActionsStore(EventPushActionsWorkerStore): txn.execute("SELECT MAX(stream_ordering) FROM event_push_actions") return txn.fetchone() - result = yield self.runInteraction("get_latest_push_action_stream_ordering", f) + result = yield self.db.runInteraction( + "get_latest_push_action_stream_ordering", f + ) return result[0] or 0 def _remove_push_actions_for_event_id_txn(self, txn, room_id, event_id): @@ -830,7 +832,7 @@ class EventPushActionsStore(EventPushActionsWorkerStore): while True: logger.info("Rotating notifications") - caught_up = yield self.runInteraction( + caught_up = yield self.db.runInteraction( "_rotate_notifs", self._rotate_notifs_txn ) if caught_up: @@ -844,7 +846,7 @@ class EventPushActionsStore(EventPushActionsWorkerStore): the archiving process has caught up or not. """ - old_rotate_stream_ordering = self.simple_select_one_onecol_txn( + old_rotate_stream_ordering = self.db.simple_select_one_onecol_txn( txn, table="event_push_summary_stream_ordering", keyvalues={}, @@ -880,7 +882,7 @@ class EventPushActionsStore(EventPushActionsWorkerStore): return caught_up def _rotate_notifs_before_txn(self, txn, rotate_to_stream_ordering): - old_rotate_stream_ordering = self.simple_select_one_onecol_txn( + old_rotate_stream_ordering = self.db.simple_select_one_onecol_txn( txn, table="event_push_summary_stream_ordering", keyvalues={}, @@ -912,7 +914,7 @@ class EventPushActionsStore(EventPushActionsWorkerStore): # If the `old.user_id` above is NULL then we know there isn't already an # entry in the table, so we simply insert it. Otherwise we update the # existing table. - self.simple_insert_many_txn( + self.db.simple_insert_many_txn( txn, table="event_push_summary", values=[ diff --git a/synapse/storage/data_stores/main/events.py b/synapse/storage/data_stores/main/events.py index 98ae69e996..01ec9ec397 100644 --- a/synapse/storage/data_stores/main/events.py +++ b/synapse/storage/data_stores/main/events.py @@ -143,7 +143,7 @@ class EventsStore( ) return txn.fetchall() - res = yield self.runInteraction("read_forward_extremities", fetch) + res = yield self.db.runInteraction("read_forward_extremities", fetch) self._current_forward_extremities_amount = c_counter(list(x[0] for x in res)) @_retry_on_integrity_error @@ -208,7 +208,7 @@ class EventsStore( for (event, context), stream in zip(events_and_contexts, stream_orderings): event.internal_metadata.stream_ordering = stream - yield self.runInteraction( + yield self.db.runInteraction( "persist_events", self._persist_events_txn, events_and_contexts=events_and_contexts, @@ -281,7 +281,7 @@ class EventsStore( results.extend(r[0] for r in txn if not json.loads(r[1]).get("soft_failed")) for chunk in batch_iter(event_ids, 100): - yield self.runInteraction( + yield self.db.runInteraction( "_get_events_which_are_prevs", _get_events_which_are_prevs_txn, chunk ) @@ -345,7 +345,7 @@ class EventsStore( existing_prevs.add(prev_event_id) for chunk in batch_iter(event_ids, 100): - yield self.runInteraction( + yield self.db.runInteraction( "_get_prevs_before_rejected", _get_prevs_before_rejected_txn, chunk ) @@ -432,7 +432,7 @@ class EventsStore( # event's auth chain, but its easier for now just to store them (and # it doesn't take much storage compared to storing the entire event # anyway). - self.simple_insert_many_txn( + self.db.simple_insert_many_txn( txn, table="event_auth", values=[ @@ -580,12 +580,12 @@ class EventsStore( self, txn, new_forward_extremities, max_stream_order ): for room_id, new_extrem in iteritems(new_forward_extremities): - self.simple_delete_txn( + self.db.simple_delete_txn( txn, table="event_forward_extremities", keyvalues={"room_id": room_id} ) txn.call_after(self.get_latest_event_ids_in_room.invalidate, (room_id,)) - self.simple_insert_many_txn( + self.db.simple_insert_many_txn( txn, table="event_forward_extremities", values=[ @@ -598,7 +598,7 @@ class EventsStore( # new stream_ordering to new forward extremeties in the room. # This allows us to later efficiently look up the forward extremeties # for a room before a given stream_ordering - self.simple_insert_many_txn( + self.db.simple_insert_many_txn( txn, table="stream_ordering_to_exterm", values=[ @@ -722,7 +722,7 @@ class EventsStore( # change in outlier status to our workers. stream_order = event.internal_metadata.stream_ordering state_group_id = context.state_group - self.simple_insert_txn( + self.db.simple_insert_txn( txn, table="ex_outlier_stream", values={ @@ -794,7 +794,7 @@ class EventsStore( d.pop("redacted_because", None) return d - self.simple_insert_many_txn( + self.db.simple_insert_many_txn( txn, table="event_json", values=[ @@ -811,7 +811,7 @@ class EventsStore( ], ) - self.simple_insert_many_txn( + self.db.simple_insert_many_txn( txn, table="events", values=[ @@ -841,7 +841,7 @@ class EventsStore( # If we're persisting an unredacted event we go and ensure # that we mark any redactions that reference this event as # requiring censoring. - self.simple_update_txn( + self.db.simple_update_txn( txn, table="redactions", keyvalues={"redacts": event.event_id}, @@ -983,7 +983,7 @@ class EventsStore( state_values.append(vals) - self.simple_insert_many_txn(txn, table="state_events", values=state_values) + self.db.simple_insert_many_txn(txn, table="state_events", values=state_values) # Prefill the event cache self._add_to_cache(txn, events_and_contexts) @@ -1014,7 +1014,7 @@ class EventsStore( ) txn.execute(sql + clause, args) - rows = self.cursor_to_dict(txn) + rows = self.db.cursor_to_dict(txn) for row in rows: event = ev_map[row["event_id"]] if not row["rejects"] and not row["redacts"]: @@ -1032,7 +1032,7 @@ class EventsStore( # invalidate the cache for the redacted event txn.call_after(self._invalidate_get_event_cache, event.redacts) - self.simple_insert_txn( + self.db.simple_insert_txn( txn, table="redactions", values={ @@ -1077,7 +1077,9 @@ class EventsStore( LIMIT ? """ - rows = yield self.execute("_censor_redactions_fetch", None, sql, before_ts, 100) + rows = yield self.db.execute( + "_censor_redactions_fetch", None, sql, before_ts, 100 + ) updates = [] @@ -1109,14 +1111,14 @@ class EventsStore( if pruned_json: self._censor_event_txn(txn, event_id, pruned_json) - self.simple_update_one_txn( + self.db.simple_update_one_txn( txn, table="redactions", keyvalues={"event_id": redaction_id}, updatevalues={"have_censored": True}, ) - yield self.runInteraction("_update_censor_txn", _update_censor_txn) + yield self.db.runInteraction("_update_censor_txn", _update_censor_txn) def _censor_event_txn(self, txn, event_id, pruned_json): """Censor an event by replacing its JSON in the event_json table with the @@ -1127,7 +1129,7 @@ class EventsStore( event_id (str): The ID of the event to censor. pruned_json (str): The pruned JSON """ - self.simple_update_one_txn( + self.db.simple_update_one_txn( txn, table="event_json", keyvalues={"event_id": event_id}, @@ -1153,7 +1155,7 @@ class EventsStore( (count,) = txn.fetchone() return count - ret = yield self.runInteraction("count_messages", _count_messages) + ret = yield self.db.runInteraction("count_messages", _count_messages) return ret @defer.inlineCallbacks @@ -1174,7 +1176,7 @@ class EventsStore( (count,) = txn.fetchone() return count - ret = yield self.runInteraction("count_daily_sent_messages", _count_messages) + ret = yield self.db.runInteraction("count_daily_sent_messages", _count_messages) return ret @defer.inlineCallbacks @@ -1189,7 +1191,7 @@ class EventsStore( (count,) = txn.fetchone() return count - ret = yield self.runInteraction("count_daily_active_rooms", _count) + ret = yield self.db.runInteraction("count_daily_active_rooms", _count) return ret def get_current_backfill_token(self): @@ -1241,7 +1243,7 @@ class EventsStore( return new_event_updates - return self.runInteraction( + return self.db.runInteraction( "get_all_new_forward_event_rows", get_all_new_forward_event_rows ) @@ -1286,7 +1288,7 @@ class EventsStore( return new_event_updates - return self.runInteraction( + return self.db.runInteraction( "get_all_new_backfill_event_rows", get_all_new_backfill_event_rows ) @@ -1379,7 +1381,7 @@ class EventsStore( backward_ex_outliers, ) - return self.runInteraction("get_all_new_events", get_all_new_events_txn) + return self.db.runInteraction("get_all_new_events", get_all_new_events_txn) def purge_history(self, room_id, token, delete_local_events): """Deletes room history before a certain point @@ -1399,7 +1401,7 @@ class EventsStore( deleted events. """ - return self.runInteraction( + return self.db.runInteraction( "purge_history", self._purge_history_txn, room_id, @@ -1647,7 +1649,7 @@ class EventsStore( Deferred[List[int]]: The list of state groups to delete. """ - return self.runInteraction("purge_room", self._purge_room_txn, room_id) + return self.db.runInteraction("purge_room", self._purge_room_txn, room_id) def _purge_room_txn(self, txn, room_id): # First we fetch all the state groups that should be deleted, before @@ -1766,7 +1768,7 @@ class EventsStore( to delete. """ - return self.runInteraction( + return self.db.runInteraction( "purge_unreferenced_state_groups", self._purge_unreferenced_state_groups, room_id, @@ -1778,7 +1780,7 @@ class EventsStore( "[purge] found %i state groups to delete", len(state_groups_to_delete) ) - rows = self.simple_select_many_txn( + rows = self.db.simple_select_many_txn( txn, table="state_group_edges", column="prev_state_group", @@ -1805,15 +1807,15 @@ class EventsStore( curr_state = self._get_state_groups_from_groups_txn(txn, [sg]) curr_state = curr_state[sg] - self.simple_delete_txn( + self.db.simple_delete_txn( txn, table="state_groups_state", keyvalues={"state_group": sg} ) - self.simple_delete_txn( + self.db.simple_delete_txn( txn, table="state_group_edges", keyvalues={"state_group": sg} ) - self.simple_insert_many_txn( + self.db.simple_insert_many_txn( txn, table="state_groups_state", values=[ @@ -1850,7 +1852,7 @@ class EventsStore( state group. """ - rows = yield self.simple_select_many_batch( + rows = yield self.db.simple_select_many_batch( table="state_group_edges", column="prev_state_group", iterable=state_groups, @@ -1869,7 +1871,7 @@ class EventsStore( state_groups_to_delete (list[int]): State groups to delete """ - return self.runInteraction( + return self.db.runInteraction( "purge_room_state", self._purge_room_state_txn, room_id, @@ -1880,7 +1882,7 @@ class EventsStore( # first we have to delete the state groups states logger.info("[purge] removing %s from state_groups_state", room_id) - self.simple_delete_many_txn( + self.db.simple_delete_many_txn( txn, table="state_groups_state", column="state_group", @@ -1891,7 +1893,7 @@ class EventsStore( # ... and the state group edges logger.info("[purge] removing %s from state_group_edges", room_id) - self.simple_delete_many_txn( + self.db.simple_delete_many_txn( txn, table="state_group_edges", column="state_group", @@ -1902,7 +1904,7 @@ class EventsStore( # ... and the state groups logger.info("[purge] removing %s from state_groups", room_id) - self.simple_delete_many_txn( + self.db.simple_delete_many_txn( txn, table="state_groups", column="id", @@ -1919,7 +1921,7 @@ class EventsStore( @cachedInlineCallbacks(max_entries=5000) def _get_event_ordering(self, event_id): - res = yield self.simple_select_one( + res = yield self.db.simple_select_one( table="events", retcols=["topological_ordering", "stream_ordering"], keyvalues={"event_id": event_id}, @@ -1942,7 +1944,7 @@ class EventsStore( txn.execute(sql, (from_token, to_token, limit)) return txn.fetchall() - return self.runInteraction( + return self.db.runInteraction( "get_all_updated_current_state_deltas", get_all_updated_current_state_deltas_txn, ) @@ -1960,7 +1962,7 @@ class EventsStore( room_id (str): The ID of the room the event was sent to. topological_ordering (int): The position of the event in the room's topology. """ - return self.simple_insert_many_txn( + return self.db.simple_insert_many_txn( txn=txn, table="event_labels", values=[ @@ -1982,7 +1984,7 @@ class EventsStore( event_id (str): The event ID the expiry timestamp is associated with. expiry_ts (int): The timestamp at which to expire (delete) the event. """ - return self.simple_insert_txn( + return self.db.simple_insert_txn( txn=txn, table="event_expiry", values={"event_id": event_id, "expiry_ts": expiry_ts}, @@ -2031,7 +2033,7 @@ class EventsStore( txn, "_get_event_cache", (event.event_id,) ) - yield self.runInteraction("delete_expired_event", delete_expired_event_txn) + yield self.db.runInteraction("delete_expired_event", delete_expired_event_txn) def _delete_event_expiry_txn(self, txn, event_id): """Delete the expiry timestamp associated with an event ID without deleting the @@ -2041,7 +2043,7 @@ class EventsStore( txn (LoggingTransaction): The transaction to use to perform the deletion. event_id (str): The event ID to delete the associated expiry timestamp of. """ - return self.simple_delete_txn( + return self.db.simple_delete_txn( txn=txn, table="event_expiry", keyvalues={"event_id": event_id} ) @@ -2065,7 +2067,7 @@ class EventsStore( return txn.fetchone() - return self.runInteraction( + return self.db.runInteraction( desc="get_next_event_to_expire", func=get_next_event_to_expire_txn ) diff --git a/synapse/storage/data_stores/main/events_bg_updates.py b/synapse/storage/data_stores/main/events_bg_updates.py index 37dfc8c871..365e966956 100644 --- a/synapse/storage/data_stores/main/events_bg_updates.py +++ b/synapse/storage/data_stores/main/events_bg_updates.py @@ -151,7 +151,7 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): return len(rows) - result = yield self.runInteraction( + result = yield self.db.runInteraction( self.EVENT_FIELDS_SENDER_URL_UPDATE_NAME, reindex_txn ) @@ -189,7 +189,7 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): chunks = [event_ids[i : i + 100] for i in range(0, len(event_ids), 100)] for chunk in chunks: - ev_rows = self.simple_select_many_txn( + ev_rows = self.db.simple_select_many_txn( txn, table="event_json", column="event_id", @@ -228,7 +228,7 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): return len(rows_to_update) - result = yield self.runInteraction( + result = yield self.db.runInteraction( self.EVENT_ORIGIN_SERVER_TS_NAME, reindex_search_txn ) @@ -366,7 +366,7 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): to_delete.intersection_update(original_set) - deleted = self.simple_delete_many_txn( + deleted = self.db.simple_delete_many_txn( txn=txn, table="event_forward_extremities", column="event_id", @@ -382,7 +382,7 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): if deleted: # We now need to invalidate the caches of these rooms - rows = self.simple_select_many_txn( + rows = self.db.simple_select_many_txn( txn, table="events", column="event_id", @@ -396,7 +396,7 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): self.get_latest_event_ids_in_room.invalidate, (room_id,) ) - self.simple_delete_many_txn( + self.db.simple_delete_many_txn( txn=txn, table="_extremities_to_check", column="event_id", @@ -406,7 +406,7 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): return len(original_set) - num_handled = yield self.runInteraction( + num_handled = yield self.db.runInteraction( "_cleanup_extremities_bg_update", _cleanup_extremities_bg_update_txn ) @@ -416,7 +416,7 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): def _drop_table_txn(txn): txn.execute("DROP TABLE _extremities_to_check") - yield self.runInteraction( + yield self.db.runInteraction( "_cleanup_extremities_bg_update_drop_table", _drop_table_txn ) @@ -470,7 +470,7 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): return len(rows) - count = yield self.runInteraction( + count = yield self.db.runInteraction( "_redactions_received_ts", _redactions_received_ts_txn ) @@ -501,7 +501,7 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): txn.execute("DROP INDEX redactions_censored_redacts") - yield self.runInteraction( + yield self.db.runInteraction( "_event_fix_redactions_bytes", _event_fix_redactions_bytes_txn ) @@ -533,7 +533,7 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): try: event_json = json.loads(event_json_raw) - self.simple_insert_many_txn( + self.db.simple_insert_many_txn( txn=txn, table="event_labels", values=[ @@ -565,7 +565,7 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): return nbrows - num_rows = yield self.runInteraction( + num_rows = yield self.db.runInteraction( desc="event_store_labels", func=_event_store_labels_txn ) diff --git a/synapse/storage/data_stores/main/events_worker.py b/synapse/storage/data_stores/main/events_worker.py index 6a08a746b6..e041fc5eac 100644 --- a/synapse/storage/data_stores/main/events_worker.py +++ b/synapse/storage/data_stores/main/events_worker.py @@ -78,7 +78,7 @@ class EventsWorkerStore(SQLBaseStore): Deferred[int|None]: Timestamp in milliseconds, or None for events that were persisted before received_ts was implemented. """ - return self.simple_select_one_onecol( + return self.db.simple_select_one_onecol( table="events", keyvalues={"event_id": event_id}, retcol="received_ts", @@ -117,7 +117,7 @@ class EventsWorkerStore(SQLBaseStore): return ts - return self.runInteraction( + return self.db.runInteraction( "get_approximate_received_ts", _get_approximate_received_ts_txn ) @@ -452,7 +452,7 @@ class EventsWorkerStore(SQLBaseStore): event_id for events, _ in event_list for event_id in events ) - row_dict = self.new_transaction( + row_dict = self.db.new_transaction( conn, "do_fetch", [], [], self._fetch_event_rows, events_to_fetch ) @@ -584,7 +584,7 @@ class EventsWorkerStore(SQLBaseStore): if should_start: run_as_background_process( - "fetch_events", self.runWithConnection, self._do_fetch + "fetch_events", self.db.runWithConnection, self._do_fetch ) logger.debug("Loading %d events: %s", len(events), events) @@ -745,7 +745,7 @@ class EventsWorkerStore(SQLBaseStore): """Given a list of event ids, check if we have already processed and stored them as non outliers. """ - rows = yield self.simple_select_many_batch( + rows = yield self.db.simple_select_many_batch( table="events", retcols=("event_id",), column="event_id", @@ -780,7 +780,9 @@ class EventsWorkerStore(SQLBaseStore): # break the input up into chunks of 100 input_iterator = iter(event_ids) for chunk in iter(lambda: list(itertools.islice(input_iterator, 100)), []): - yield self.runInteraction("have_seen_events", have_seen_events_txn, chunk) + yield self.db.runInteraction( + "have_seen_events", have_seen_events_txn, chunk + ) return results def _get_total_state_event_counts_txn(self, txn, room_id): @@ -807,7 +809,7 @@ class EventsWorkerStore(SQLBaseStore): Returns: Deferred[int] """ - return self.runInteraction( + return self.db.runInteraction( "get_total_state_event_counts", self._get_total_state_event_counts_txn, room_id, @@ -832,7 +834,7 @@ class EventsWorkerStore(SQLBaseStore): Returns: Deferred[int] """ - return self.runInteraction( + return self.db.runInteraction( "get_current_state_event_counts", self._get_current_state_event_counts_txn, room_id, diff --git a/synapse/storage/data_stores/main/filtering.py b/synapse/storage/data_stores/main/filtering.py index 17ef7b9354..342d6622a4 100644 --- a/synapse/storage/data_stores/main/filtering.py +++ b/synapse/storage/data_stores/main/filtering.py @@ -30,7 +30,7 @@ class FilteringStore(SQLBaseStore): except ValueError: raise SynapseError(400, "Invalid filter ID", Codes.INVALID_PARAM) - def_json = yield self.simple_select_one_onecol( + def_json = yield self.db.simple_select_one_onecol( table="user_filters", keyvalues={"user_id": user_localpart, "filter_id": filter_id}, retcol="filter_json", @@ -71,4 +71,4 @@ class FilteringStore(SQLBaseStore): return filter_id - return self.runInteraction("add_user_filter", _do_txn) + return self.db.runInteraction("add_user_filter", _do_txn) diff --git a/synapse/storage/data_stores/main/group_server.py b/synapse/storage/data_stores/main/group_server.py index 9e1d12bcb7..7f5e8dce66 100644 --- a/synapse/storage/data_stores/main/group_server.py +++ b/synapse/storage/data_stores/main/group_server.py @@ -35,7 +35,7 @@ class GroupServerStore(SQLBaseStore): * "invite" * "open" """ - return self.simple_update_one( + return self.db.simple_update_one( table="groups", keyvalues={"group_id": group_id}, updatevalues={"join_policy": join_policy}, @@ -43,7 +43,7 @@ class GroupServerStore(SQLBaseStore): ) def get_group(self, group_id): - return self.simple_select_one( + return self.db.simple_select_one( table="groups", keyvalues={"group_id": group_id}, retcols=( @@ -65,7 +65,7 @@ class GroupServerStore(SQLBaseStore): if not include_private: keyvalues["is_public"] = True - return self.simple_select_list( + return self.db.simple_select_list( table="group_users", keyvalues=keyvalues, retcols=("user_id", "is_public", "is_admin"), @@ -75,7 +75,7 @@ class GroupServerStore(SQLBaseStore): def get_invited_users_in_group(self, group_id): # TODO: Pagination - return self.simple_select_onecol( + return self.db.simple_select_onecol( table="group_invites", keyvalues={"group_id": group_id}, retcol="user_id", @@ -89,7 +89,7 @@ class GroupServerStore(SQLBaseStore): if not include_private: keyvalues["is_public"] = True - return self.simple_select_list( + return self.db.simple_select_list( table="group_rooms", keyvalues=keyvalues, retcols=("room_id", "is_public"), @@ -153,10 +153,12 @@ class GroupServerStore(SQLBaseStore): return rooms, categories - return self.runInteraction("get_rooms_for_summary", _get_rooms_for_summary_txn) + return self.db.runInteraction( + "get_rooms_for_summary", _get_rooms_for_summary_txn + ) def add_room_to_summary(self, group_id, room_id, category_id, order, is_public): - return self.runInteraction( + return self.db.runInteraction( "add_room_to_summary", self._add_room_to_summary_txn, group_id, @@ -180,7 +182,7 @@ class GroupServerStore(SQLBaseStore): an order of 1 will put the room first. Otherwise, the room gets added to the end. """ - room_in_group = self.simple_select_one_onecol_txn( + room_in_group = self.db.simple_select_one_onecol_txn( txn, table="group_rooms", keyvalues={"group_id": group_id, "room_id": room_id}, @@ -193,7 +195,7 @@ class GroupServerStore(SQLBaseStore): if category_id is None: category_id = _DEFAULT_CATEGORY_ID else: - cat_exists = self.simple_select_one_onecol_txn( + cat_exists = self.db.simple_select_one_onecol_txn( txn, table="group_room_categories", keyvalues={"group_id": group_id, "category_id": category_id}, @@ -204,7 +206,7 @@ class GroupServerStore(SQLBaseStore): raise SynapseError(400, "Category doesn't exist") # TODO: Check category is part of summary already - cat_exists = self.simple_select_one_onecol_txn( + cat_exists = self.db.simple_select_one_onecol_txn( txn, table="group_summary_room_categories", keyvalues={"group_id": group_id, "category_id": category_id}, @@ -224,7 +226,7 @@ class GroupServerStore(SQLBaseStore): (group_id, category_id, group_id, category_id), ) - existing = self.simple_select_one_txn( + existing = self.db.simple_select_one_txn( txn, table="group_summary_rooms", keyvalues={ @@ -257,7 +259,7 @@ class GroupServerStore(SQLBaseStore): to_update["room_order"] = order if is_public is not None: to_update["is_public"] = is_public - self.simple_update_txn( + self.db.simple_update_txn( txn, table="group_summary_rooms", keyvalues={ @@ -271,7 +273,7 @@ class GroupServerStore(SQLBaseStore): if is_public is None: is_public = True - self.simple_insert_txn( + self.db.simple_insert_txn( txn, table="group_summary_rooms", values={ @@ -287,7 +289,7 @@ class GroupServerStore(SQLBaseStore): if category_id is None: category_id = _DEFAULT_CATEGORY_ID - return self.simple_delete( + return self.db.simple_delete( table="group_summary_rooms", keyvalues={ "group_id": group_id, @@ -299,7 +301,7 @@ class GroupServerStore(SQLBaseStore): @defer.inlineCallbacks def get_group_categories(self, group_id): - rows = yield self.simple_select_list( + rows = yield self.db.simple_select_list( table="group_room_categories", keyvalues={"group_id": group_id}, retcols=("category_id", "is_public", "profile"), @@ -316,7 +318,7 @@ class GroupServerStore(SQLBaseStore): @defer.inlineCallbacks def get_group_category(self, group_id, category_id): - category = yield self.simple_select_one( + category = yield self.db.simple_select_one( table="group_room_categories", keyvalues={"group_id": group_id, "category_id": category_id}, retcols=("is_public", "profile"), @@ -343,7 +345,7 @@ class GroupServerStore(SQLBaseStore): else: update_values["is_public"] = is_public - return self.simple_upsert( + return self.db.simple_upsert( table="group_room_categories", keyvalues={"group_id": group_id, "category_id": category_id}, values=update_values, @@ -352,7 +354,7 @@ class GroupServerStore(SQLBaseStore): ) def remove_group_category(self, group_id, category_id): - return self.simple_delete( + return self.db.simple_delete( table="group_room_categories", keyvalues={"group_id": group_id, "category_id": category_id}, desc="remove_group_category", @@ -360,7 +362,7 @@ class GroupServerStore(SQLBaseStore): @defer.inlineCallbacks def get_group_roles(self, group_id): - rows = yield self.simple_select_list( + rows = yield self.db.simple_select_list( table="group_roles", keyvalues={"group_id": group_id}, retcols=("role_id", "is_public", "profile"), @@ -377,7 +379,7 @@ class GroupServerStore(SQLBaseStore): @defer.inlineCallbacks def get_group_role(self, group_id, role_id): - role = yield self.simple_select_one( + role = yield self.db.simple_select_one( table="group_roles", keyvalues={"group_id": group_id, "role_id": role_id}, retcols=("is_public", "profile"), @@ -404,7 +406,7 @@ class GroupServerStore(SQLBaseStore): else: update_values["is_public"] = is_public - return self.simple_upsert( + return self.db.simple_upsert( table="group_roles", keyvalues={"group_id": group_id, "role_id": role_id}, values=update_values, @@ -413,14 +415,14 @@ class GroupServerStore(SQLBaseStore): ) def remove_group_role(self, group_id, role_id): - return self.simple_delete( + return self.db.simple_delete( table="group_roles", keyvalues={"group_id": group_id, "role_id": role_id}, desc="remove_group_role", ) def add_user_to_summary(self, group_id, user_id, role_id, order, is_public): - return self.runInteraction( + return self.db.runInteraction( "add_user_to_summary", self._add_user_to_summary_txn, group_id, @@ -444,7 +446,7 @@ class GroupServerStore(SQLBaseStore): an order of 1 will put the user first. Otherwise, the user gets added to the end. """ - user_in_group = self.simple_select_one_onecol_txn( + user_in_group = self.db.simple_select_one_onecol_txn( txn, table="group_users", keyvalues={"group_id": group_id, "user_id": user_id}, @@ -457,7 +459,7 @@ class GroupServerStore(SQLBaseStore): if role_id is None: role_id = _DEFAULT_ROLE_ID else: - role_exists = self.simple_select_one_onecol_txn( + role_exists = self.db.simple_select_one_onecol_txn( txn, table="group_roles", keyvalues={"group_id": group_id, "role_id": role_id}, @@ -468,7 +470,7 @@ class GroupServerStore(SQLBaseStore): raise SynapseError(400, "Role doesn't exist") # TODO: Check role is part of the summary already - role_exists = self.simple_select_one_onecol_txn( + role_exists = self.db.simple_select_one_onecol_txn( txn, table="group_summary_roles", keyvalues={"group_id": group_id, "role_id": role_id}, @@ -488,7 +490,7 @@ class GroupServerStore(SQLBaseStore): (group_id, role_id, group_id, role_id), ) - existing = self.simple_select_one_txn( + existing = self.db.simple_select_one_txn( txn, table="group_summary_users", keyvalues={"group_id": group_id, "user_id": user_id, "role_id": role_id}, @@ -517,7 +519,7 @@ class GroupServerStore(SQLBaseStore): to_update["user_order"] = order if is_public is not None: to_update["is_public"] = is_public - self.simple_update_txn( + self.db.simple_update_txn( txn, table="group_summary_users", keyvalues={ @@ -531,7 +533,7 @@ class GroupServerStore(SQLBaseStore): if is_public is None: is_public = True - self.simple_insert_txn( + self.db.simple_insert_txn( txn, table="group_summary_users", values={ @@ -547,7 +549,7 @@ class GroupServerStore(SQLBaseStore): if role_id is None: role_id = _DEFAULT_ROLE_ID - return self.simple_delete( + return self.db.simple_delete( table="group_summary_users", keyvalues={"group_id": group_id, "role_id": role_id, "user_id": user_id}, desc="remove_user_from_summary", @@ -561,7 +563,7 @@ class GroupServerStore(SQLBaseStore): Deferred[list[str]]: A twisted.Deferred containing a list of group ids containing this room """ - return self.simple_select_onecol( + return self.db.simple_select_onecol( table="group_rooms", keyvalues={"room_id": room_id}, retcol="group_id", @@ -625,12 +627,12 @@ class GroupServerStore(SQLBaseStore): return users, roles - return self.runInteraction( + return self.db.runInteraction( "get_users_for_summary_by_role", _get_users_for_summary_txn ) def is_user_in_group(self, user_id, group_id): - return self.simple_select_one_onecol( + return self.db.simple_select_one_onecol( table="group_users", keyvalues={"group_id": group_id, "user_id": user_id}, retcol="user_id", @@ -639,7 +641,7 @@ class GroupServerStore(SQLBaseStore): ).addCallback(lambda r: bool(r)) def is_user_admin_in_group(self, group_id, user_id): - return self.simple_select_one_onecol( + return self.db.simple_select_one_onecol( table="group_users", keyvalues={"group_id": group_id, "user_id": user_id}, retcol="is_admin", @@ -650,7 +652,7 @@ class GroupServerStore(SQLBaseStore): def add_group_invite(self, group_id, user_id): """Record that the group server has invited a user """ - return self.simple_insert( + return self.db.simple_insert( table="group_invites", values={"group_id": group_id, "user_id": user_id}, desc="add_group_invite", @@ -659,7 +661,7 @@ class GroupServerStore(SQLBaseStore): def is_user_invited_to_local_group(self, group_id, user_id): """Has the group server invited a user? """ - return self.simple_select_one_onecol( + return self.db.simple_select_one_onecol( table="group_invites", keyvalues={"group_id": group_id, "user_id": user_id}, retcol="user_id", @@ -682,7 +684,7 @@ class GroupServerStore(SQLBaseStore): """ def _get_users_membership_in_group_txn(txn): - row = self.simple_select_one_txn( + row = self.db.simple_select_one_txn( txn, table="group_users", keyvalues={"group_id": group_id, "user_id": user_id}, @@ -697,7 +699,7 @@ class GroupServerStore(SQLBaseStore): "is_privileged": row["is_admin"], } - row = self.simple_select_one_onecol_txn( + row = self.db.simple_select_one_onecol_txn( txn, table="group_invites", keyvalues={"group_id": group_id, "user_id": user_id}, @@ -710,7 +712,7 @@ class GroupServerStore(SQLBaseStore): return {} - return self.runInteraction( + return self.db.runInteraction( "get_users_membership_info_in_group", _get_users_membership_in_group_txn ) @@ -738,7 +740,7 @@ class GroupServerStore(SQLBaseStore): """ def _add_user_to_group_txn(txn): - self.simple_insert_txn( + self.db.simple_insert_txn( txn, table="group_users", values={ @@ -749,14 +751,14 @@ class GroupServerStore(SQLBaseStore): }, ) - self.simple_delete_txn( + self.db.simple_delete_txn( txn, table="group_invites", keyvalues={"group_id": group_id, "user_id": user_id}, ) if local_attestation: - self.simple_insert_txn( + self.db.simple_insert_txn( txn, table="group_attestations_renewals", values={ @@ -766,7 +768,7 @@ class GroupServerStore(SQLBaseStore): }, ) if remote_attestation: - self.simple_insert_txn( + self.db.simple_insert_txn( txn, table="group_attestations_remote", values={ @@ -777,49 +779,49 @@ class GroupServerStore(SQLBaseStore): }, ) - return self.runInteraction("add_user_to_group", _add_user_to_group_txn) + return self.db.runInteraction("add_user_to_group", _add_user_to_group_txn) def remove_user_from_group(self, group_id, user_id): def _remove_user_from_group_txn(txn): - self.simple_delete_txn( + self.db.simple_delete_txn( txn, table="group_users", keyvalues={"group_id": group_id, "user_id": user_id}, ) - self.simple_delete_txn( + self.db.simple_delete_txn( txn, table="group_invites", keyvalues={"group_id": group_id, "user_id": user_id}, ) - self.simple_delete_txn( + self.db.simple_delete_txn( txn, table="group_attestations_renewals", keyvalues={"group_id": group_id, "user_id": user_id}, ) - self.simple_delete_txn( + self.db.simple_delete_txn( txn, table="group_attestations_remote", keyvalues={"group_id": group_id, "user_id": user_id}, ) - self.simple_delete_txn( + self.db.simple_delete_txn( txn, table="group_summary_users", keyvalues={"group_id": group_id, "user_id": user_id}, ) - return self.runInteraction( + return self.db.runInteraction( "remove_user_from_group", _remove_user_from_group_txn ) def add_room_to_group(self, group_id, room_id, is_public): - return self.simple_insert( + return self.db.simple_insert( table="group_rooms", values={"group_id": group_id, "room_id": room_id, "is_public": is_public}, desc="add_room_to_group", ) def update_room_in_group_visibility(self, group_id, room_id, is_public): - return self.simple_update( + return self.db.simple_update( table="group_rooms", keyvalues={"group_id": group_id, "room_id": room_id}, updatevalues={"is_public": is_public}, @@ -828,26 +830,26 @@ class GroupServerStore(SQLBaseStore): def remove_room_from_group(self, group_id, room_id): def _remove_room_from_group_txn(txn): - self.simple_delete_txn( + self.db.simple_delete_txn( txn, table="group_rooms", keyvalues={"group_id": group_id, "room_id": room_id}, ) - self.simple_delete_txn( + self.db.simple_delete_txn( txn, table="group_summary_rooms", keyvalues={"group_id": group_id, "room_id": room_id}, ) - return self.runInteraction( + return self.db.runInteraction( "remove_room_from_group", _remove_room_from_group_txn ) def get_publicised_groups_for_user(self, user_id): """Get all groups a user is publicising """ - return self.simple_select_onecol( + return self.db.simple_select_onecol( table="local_group_membership", keyvalues={"user_id": user_id, "membership": "join", "is_publicised": True}, retcol="group_id", @@ -857,7 +859,7 @@ class GroupServerStore(SQLBaseStore): def update_group_publicity(self, group_id, user_id, publicise): """Update whether the user is publicising their membership of the group """ - return self.simple_update_one( + return self.db.simple_update_one( table="local_group_membership", keyvalues={"group_id": group_id, "user_id": user_id}, updatevalues={"is_publicised": publicise}, @@ -893,12 +895,12 @@ class GroupServerStore(SQLBaseStore): def _register_user_group_membership_txn(txn, next_id): # TODO: Upsert? - self.simple_delete_txn( + self.db.simple_delete_txn( txn, table="local_group_membership", keyvalues={"group_id": group_id, "user_id": user_id}, ) - self.simple_insert_txn( + self.db.simple_insert_txn( txn, table="local_group_membership", values={ @@ -911,7 +913,7 @@ class GroupServerStore(SQLBaseStore): }, ) - self.simple_insert_txn( + self.db.simple_insert_txn( txn, table="local_group_updates", values={ @@ -930,7 +932,7 @@ class GroupServerStore(SQLBaseStore): if membership == "join": if local_attestation: - self.simple_insert_txn( + self.db.simple_insert_txn( txn, table="group_attestations_renewals", values={ @@ -940,7 +942,7 @@ class GroupServerStore(SQLBaseStore): }, ) if remote_attestation: - self.simple_insert_txn( + self.db.simple_insert_txn( txn, table="group_attestations_remote", values={ @@ -951,12 +953,12 @@ class GroupServerStore(SQLBaseStore): }, ) else: - self.simple_delete_txn( + self.db.simple_delete_txn( txn, table="group_attestations_renewals", keyvalues={"group_id": group_id, "user_id": user_id}, ) - self.simple_delete_txn( + self.db.simple_delete_txn( txn, table="group_attestations_remote", keyvalues={"group_id": group_id, "user_id": user_id}, @@ -965,7 +967,7 @@ class GroupServerStore(SQLBaseStore): return next_id with self._group_updates_id_gen.get_next() as next_id: - res = yield self.runInteraction( + res = yield self.db.runInteraction( "register_user_group_membership", _register_user_group_membership_txn, next_id, @@ -976,7 +978,7 @@ class GroupServerStore(SQLBaseStore): def create_group( self, group_id, user_id, name, avatar_url, short_description, long_description ): - yield self.simple_insert( + yield self.db.simple_insert( table="groups", values={ "group_id": group_id, @@ -991,7 +993,7 @@ class GroupServerStore(SQLBaseStore): @defer.inlineCallbacks def update_group_profile(self, group_id, profile): - yield self.simple_update_one( + yield self.db.simple_update_one( table="groups", keyvalues={"group_id": group_id}, updatevalues=profile, @@ -1008,16 +1010,16 @@ class GroupServerStore(SQLBaseStore): WHERE valid_until_ms <= ? """ txn.execute(sql, (valid_until_ms,)) - return self.cursor_to_dict(txn) + return self.db.cursor_to_dict(txn) - return self.runInteraction( + return self.db.runInteraction( "get_attestations_need_renewals", _get_attestations_need_renewals_txn ) def update_attestation_renewal(self, group_id, user_id, attestation): """Update an attestation that we have renewed """ - return self.simple_update_one( + return self.db.simple_update_one( table="group_attestations_renewals", keyvalues={"group_id": group_id, "user_id": user_id}, updatevalues={"valid_until_ms": attestation["valid_until_ms"]}, @@ -1027,7 +1029,7 @@ class GroupServerStore(SQLBaseStore): def update_remote_attestion(self, group_id, user_id, attestation): """Update an attestation that a remote has renewed """ - return self.simple_update_one( + return self.db.simple_update_one( table="group_attestations_remote", keyvalues={"group_id": group_id, "user_id": user_id}, updatevalues={ @@ -1046,7 +1048,7 @@ class GroupServerStore(SQLBaseStore): group_id (str) user_id (str) """ - return self.simple_delete( + return self.db.simple_delete( table="group_attestations_renewals", keyvalues={"group_id": group_id, "user_id": user_id}, desc="remove_attestation_renewal", @@ -1057,7 +1059,7 @@ class GroupServerStore(SQLBaseStore): """Get the attestation that proves the remote agrees that the user is in the group. """ - row = yield self.simple_select_one( + row = yield self.db.simple_select_one( table="group_attestations_remote", keyvalues={"group_id": group_id, "user_id": user_id}, retcols=("valid_until_ms", "attestation_json"), @@ -1072,7 +1074,7 @@ class GroupServerStore(SQLBaseStore): return None def get_joined_groups(self, user_id): - return self.simple_select_onecol( + return self.db.simple_select_onecol( table="local_group_membership", keyvalues={"user_id": user_id, "membership": "join"}, retcol="group_id", @@ -1099,7 +1101,7 @@ class GroupServerStore(SQLBaseStore): for row in txn ] - return self.runInteraction( + return self.db.runInteraction( "get_all_groups_for_user", _get_all_groups_for_user_txn ) @@ -1129,7 +1131,7 @@ class GroupServerStore(SQLBaseStore): for group_id, membership, gtype, content_json in txn ] - return self.runInteraction( + return self.db.runInteraction( "get_groups_changes_for_user", _get_groups_changes_for_user_txn ) @@ -1154,7 +1156,7 @@ class GroupServerStore(SQLBaseStore): for stream_id, group_id, user_id, gtype, content_json in txn ] - return self.runInteraction( + return self.db.runInteraction( "get_all_groups_changes", _get_all_groups_changes_txn ) @@ -1188,8 +1190,8 @@ class GroupServerStore(SQLBaseStore): ] for table in tables: - self.simple_delete_txn( + self.db.simple_delete_txn( txn, table=table, keyvalues={"group_id": group_id} ) - return self.runInteraction("delete_group", _delete_group_txn) + return self.db.runInteraction("delete_group", _delete_group_txn) diff --git a/synapse/storage/data_stores/main/keys.py b/synapse/storage/data_stores/main/keys.py index c7150432b3..6b12f5a75f 100644 --- a/synapse/storage/data_stores/main/keys.py +++ b/synapse/storage/data_stores/main/keys.py @@ -92,7 +92,7 @@ class KeyStore(SQLBaseStore): _get_keys(txn, batch) return keys - return self.runInteraction("get_server_verify_keys", _txn) + return self.db.runInteraction("get_server_verify_keys", _txn) def store_server_verify_keys(self, from_server, ts_added_ms, verify_keys): """Stores NACL verification keys for remote servers. @@ -127,9 +127,9 @@ class KeyStore(SQLBaseStore): f((i,)) return res - return self.runInteraction( + return self.db.runInteraction( "store_server_verify_keys", - self.simple_upsert_many_txn, + self.db.simple_upsert_many_txn, table="server_signature_keys", key_names=("server_name", "key_id"), key_values=key_values, @@ -157,7 +157,7 @@ class KeyStore(SQLBaseStore): ts_valid_until_ms (int): The time when this json stops being valid. key_json (bytes): The encoded JSON. """ - return self.simple_upsert( + return self.db.simple_upsert( table="server_keys_json", keyvalues={ "server_name": server_name, @@ -196,7 +196,7 @@ class KeyStore(SQLBaseStore): keyvalues["key_id"] = key_id if from_server is not None: keyvalues["from_server"] = from_server - rows = self.simple_select_list_txn( + rows = self.db.simple_select_list_txn( txn, "server_keys_json", keyvalues=keyvalues, @@ -211,4 +211,4 @@ class KeyStore(SQLBaseStore): results[(server_name, key_id, from_server)] = rows return results - return self.runInteraction("get_server_keys_json", _get_server_keys_json_txn) + return self.db.runInteraction("get_server_keys_json", _get_server_keys_json_txn) diff --git a/synapse/storage/data_stores/main/media_repository.py b/synapse/storage/data_stores/main/media_repository.py index 0cb9446f96..ea02497784 100644 --- a/synapse/storage/data_stores/main/media_repository.py +++ b/synapse/storage/data_stores/main/media_repository.py @@ -39,7 +39,7 @@ class MediaRepositoryStore(MediaRepositoryBackgroundUpdateStore): Returns: None if the media_id doesn't exist. """ - return self.simple_select_one( + return self.db.simple_select_one( "local_media_repository", {"media_id": media_id}, ( @@ -64,7 +64,7 @@ class MediaRepositoryStore(MediaRepositoryBackgroundUpdateStore): user_id, url_cache=None, ): - return self.simple_insert( + return self.db.simple_insert( "local_media_repository", { "media_id": media_id, @@ -124,12 +124,12 @@ class MediaRepositoryStore(MediaRepositoryBackgroundUpdateStore): ) ) - return self.runInteraction("get_url_cache", get_url_cache_txn) + return self.db.runInteraction("get_url_cache", get_url_cache_txn) def store_url_cache( self, url, response_code, etag, expires_ts, og, media_id, download_ts ): - return self.simple_insert( + return self.db.simple_insert( "local_media_repository_url_cache", { "url": url, @@ -144,7 +144,7 @@ class MediaRepositoryStore(MediaRepositoryBackgroundUpdateStore): ) def get_local_media_thumbnails(self, media_id): - return self.simple_select_list( + return self.db.simple_select_list( "local_media_repository_thumbnails", {"media_id": media_id}, ( @@ -166,7 +166,7 @@ class MediaRepositoryStore(MediaRepositoryBackgroundUpdateStore): thumbnail_method, thumbnail_length, ): - return self.simple_insert( + return self.db.simple_insert( "local_media_repository_thumbnails", { "media_id": media_id, @@ -180,7 +180,7 @@ class MediaRepositoryStore(MediaRepositoryBackgroundUpdateStore): ) def get_cached_remote_media(self, origin, media_id): - return self.simple_select_one( + return self.db.simple_select_one( "remote_media_cache", {"media_origin": origin, "media_id": media_id}, ( @@ -205,7 +205,7 @@ class MediaRepositoryStore(MediaRepositoryBackgroundUpdateStore): upload_name, filesystem_id, ): - return self.simple_insert( + return self.db.simple_insert( "remote_media_cache", { "media_origin": origin, @@ -250,10 +250,12 @@ class MediaRepositoryStore(MediaRepositoryBackgroundUpdateStore): txn.executemany(sql, ((time_ms, media_id) for media_id in local_media)) - return self.runInteraction("update_cached_last_access_time", update_cache_txn) + return self.db.runInteraction( + "update_cached_last_access_time", update_cache_txn + ) def get_remote_media_thumbnails(self, origin, media_id): - return self.simple_select_list( + return self.db.simple_select_list( "remote_media_cache_thumbnails", {"media_origin": origin, "media_id": media_id}, ( @@ -278,7 +280,7 @@ class MediaRepositoryStore(MediaRepositoryBackgroundUpdateStore): thumbnail_method, thumbnail_length, ): - return self.simple_insert( + return self.db.simple_insert( "remote_media_cache_thumbnails", { "media_origin": origin, @@ -300,24 +302,24 @@ class MediaRepositoryStore(MediaRepositoryBackgroundUpdateStore): " WHERE last_access_ts < ?" ) - return self.execute( - "get_remote_media_before", self.cursor_to_dict, sql, before_ts + return self.db.execute( + "get_remote_media_before", self.db.cursor_to_dict, sql, before_ts ) def delete_remote_media(self, media_origin, media_id): def delete_remote_media_txn(txn): - self.simple_delete_txn( + self.db.simple_delete_txn( txn, "remote_media_cache", keyvalues={"media_origin": media_origin, "media_id": media_id}, ) - self.simple_delete_txn( + self.db.simple_delete_txn( txn, "remote_media_cache_thumbnails", keyvalues={"media_origin": media_origin, "media_id": media_id}, ) - return self.runInteraction("delete_remote_media", delete_remote_media_txn) + return self.db.runInteraction("delete_remote_media", delete_remote_media_txn) def get_expired_url_cache(self, now_ts): sql = ( @@ -331,7 +333,9 @@ class MediaRepositoryStore(MediaRepositoryBackgroundUpdateStore): txn.execute(sql, (now_ts,)) return [row[0] for row in txn] - return self.runInteraction("get_expired_url_cache", _get_expired_url_cache_txn) + return self.db.runInteraction( + "get_expired_url_cache", _get_expired_url_cache_txn + ) def delete_url_cache(self, media_ids): if len(media_ids) == 0: @@ -342,7 +346,7 @@ class MediaRepositoryStore(MediaRepositoryBackgroundUpdateStore): def _delete_url_cache_txn(txn): txn.executemany(sql, [(media_id,) for media_id in media_ids]) - return self.runInteraction("delete_url_cache", _delete_url_cache_txn) + return self.db.runInteraction("delete_url_cache", _delete_url_cache_txn) def get_url_cache_media_before(self, before_ts): sql = ( @@ -356,7 +360,7 @@ class MediaRepositoryStore(MediaRepositoryBackgroundUpdateStore): txn.execute(sql, (before_ts,)) return [row[0] for row in txn] - return self.runInteraction( + return self.db.runInteraction( "get_url_cache_media_before", _get_url_cache_media_before_txn ) @@ -373,6 +377,6 @@ class MediaRepositoryStore(MediaRepositoryBackgroundUpdateStore): txn.executemany(sql, [(media_id,) for media_id in media_ids]) - return self.runInteraction( + return self.db.runInteraction( "delete_url_cache_media", _delete_url_cache_media_txn ) diff --git a/synapse/storage/data_stores/main/monthly_active_users.py b/synapse/storage/data_stores/main/monthly_active_users.py index b8fc28f97b..34bf3a1880 100644 --- a/synapse/storage/data_stores/main/monthly_active_users.py +++ b/synapse/storage/data_stores/main/monthly_active_users.py @@ -32,7 +32,7 @@ class MonthlyActiveUsersStore(SQLBaseStore): self._clock = hs.get_clock() self.hs = hs # Do not add more reserved users than the total allowable number - self.new_transaction( + self.db.new_transaction( dbconn, "initialise_mau_threepids", [], @@ -146,7 +146,7 @@ class MonthlyActiveUsersStore(SQLBaseStore): txn.execute(sql, query_args) reserved_users = yield self.get_registered_reserved_users() - yield self.runInteraction( + yield self.db.runInteraction( "reap_monthly_active_users", _reap_users, reserved_users ) # It seems poor to invalidate the whole cache, Postgres supports @@ -174,7 +174,7 @@ class MonthlyActiveUsersStore(SQLBaseStore): (count,) = txn.fetchone() return count - return self.runInteraction("count_users", _count_users) + return self.db.runInteraction("count_users", _count_users) @defer.inlineCallbacks def get_registered_reserved_users(self): @@ -217,7 +217,7 @@ class MonthlyActiveUsersStore(SQLBaseStore): if is_support: return - yield self.runInteraction( + yield self.db.runInteraction( "upsert_monthly_active_user", self.upsert_monthly_active_user_txn, user_id ) @@ -261,7 +261,7 @@ class MonthlyActiveUsersStore(SQLBaseStore): # never be a big table and alternative approaches (batching multiple # upserts into a single txn) introduced a lot of extra complexity. # See https://github.com/matrix-org/synapse/issues/3854 for more - is_insert = self.simple_upsert_txn( + is_insert = self.db.simple_upsert_txn( txn, table="monthly_active_users", keyvalues={"user_id": user_id}, @@ -281,7 +281,7 @@ class MonthlyActiveUsersStore(SQLBaseStore): """ - return self.simple_select_one_onecol( + return self.db.simple_select_one_onecol( table="monthly_active_users", keyvalues={"user_id": user_id}, retcol="timestamp", diff --git a/synapse/storage/data_stores/main/openid.py b/synapse/storage/data_stores/main/openid.py index 650e49750e..cc21437e92 100644 --- a/synapse/storage/data_stores/main/openid.py +++ b/synapse/storage/data_stores/main/openid.py @@ -3,7 +3,7 @@ from synapse.storage._base import SQLBaseStore class OpenIdStore(SQLBaseStore): def insert_open_id_token(self, token, ts_valid_until_ms, user_id): - return self.simple_insert( + return self.db.simple_insert( table="open_id_tokens", values={ "token": token, @@ -28,4 +28,6 @@ class OpenIdStore(SQLBaseStore): else: return rows[0][0] - return self.runInteraction("get_user_id_for_token", get_user_id_for_token_txn) + return self.db.runInteraction( + "get_user_id_for_token", get_user_id_for_token_txn + ) diff --git a/synapse/storage/data_stores/main/presence.py b/synapse/storage/data_stores/main/presence.py index a5e121efd1..a2c83e0867 100644 --- a/synapse/storage/data_stores/main/presence.py +++ b/synapse/storage/data_stores/main/presence.py @@ -29,7 +29,7 @@ class PresenceStore(SQLBaseStore): ) with stream_ordering_manager as stream_orderings: - yield self.runInteraction( + yield self.db.runInteraction( "update_presence", self._update_presence_txn, stream_orderings, @@ -46,7 +46,7 @@ class PresenceStore(SQLBaseStore): txn.call_after(self._get_presence_for_user.invalidate, (state.user_id,)) # Actually insert new rows - self.simple_insert_many_txn( + self.db.simple_insert_many_txn( txn, table="presence_stream", values=[ @@ -88,7 +88,7 @@ class PresenceStore(SQLBaseStore): txn.execute(sql, (last_id, current_id)) return txn.fetchall() - return self.runInteraction( + return self.db.runInteraction( "get_all_presence_updates", get_all_presence_updates_txn ) @@ -103,7 +103,7 @@ class PresenceStore(SQLBaseStore): inlineCallbacks=True, ) def get_presence_for_users(self, user_ids): - rows = yield self.simple_select_many_batch( + rows = yield self.db.simple_select_many_batch( table="presence_stream", column="user_id", iterable=user_ids, @@ -129,7 +129,7 @@ class PresenceStore(SQLBaseStore): return self._presence_id_gen.get_current_token() def allow_presence_visible(self, observed_localpart, observer_userid): - return self.simple_insert( + return self.db.simple_insert( table="presence_allow_inbound", values={ "observed_user_id": observed_localpart, @@ -140,7 +140,7 @@ class PresenceStore(SQLBaseStore): ) def disallow_presence_visible(self, observed_localpart, observer_userid): - return self.simple_delete_one( + return self.db.simple_delete_one( table="presence_allow_inbound", keyvalues={ "observed_user_id": observed_localpart, diff --git a/synapse/storage/data_stores/main/profile.py b/synapse/storage/data_stores/main/profile.py index c8b5b60301..2b52cf9c1a 100644 --- a/synapse/storage/data_stores/main/profile.py +++ b/synapse/storage/data_stores/main/profile.py @@ -24,7 +24,7 @@ class ProfileWorkerStore(SQLBaseStore): @defer.inlineCallbacks def get_profileinfo(self, user_localpart): try: - profile = yield self.simple_select_one( + profile = yield self.db.simple_select_one( table="profiles", keyvalues={"user_id": user_localpart}, retcols=("displayname", "avatar_url"), @@ -42,7 +42,7 @@ class ProfileWorkerStore(SQLBaseStore): ) def get_profile_displayname(self, user_localpart): - return self.simple_select_one_onecol( + return self.db.simple_select_one_onecol( table="profiles", keyvalues={"user_id": user_localpart}, retcol="displayname", @@ -50,7 +50,7 @@ class ProfileWorkerStore(SQLBaseStore): ) def get_profile_avatar_url(self, user_localpart): - return self.simple_select_one_onecol( + return self.db.simple_select_one_onecol( table="profiles", keyvalues={"user_id": user_localpart}, retcol="avatar_url", @@ -58,7 +58,7 @@ class ProfileWorkerStore(SQLBaseStore): ) def get_from_remote_profile_cache(self, user_id): - return self.simple_select_one( + return self.db.simple_select_one( table="remote_profile_cache", keyvalues={"user_id": user_id}, retcols=("displayname", "avatar_url"), @@ -67,12 +67,12 @@ class ProfileWorkerStore(SQLBaseStore): ) def create_profile(self, user_localpart): - return self.simple_insert( + return self.db.simple_insert( table="profiles", values={"user_id": user_localpart}, desc="create_profile" ) def set_profile_displayname(self, user_localpart, new_displayname): - return self.simple_update_one( + return self.db.simple_update_one( table="profiles", keyvalues={"user_id": user_localpart}, updatevalues={"displayname": new_displayname}, @@ -80,7 +80,7 @@ class ProfileWorkerStore(SQLBaseStore): ) def set_profile_avatar_url(self, user_localpart, new_avatar_url): - return self.simple_update_one( + return self.db.simple_update_one( table="profiles", keyvalues={"user_id": user_localpart}, updatevalues={"avatar_url": new_avatar_url}, @@ -95,7 +95,7 @@ class ProfileStore(ProfileWorkerStore): This should only be called when `is_subscribed_remote_profile_for_user` would return true for the user. """ - return self.simple_upsert( + return self.db.simple_upsert( table="remote_profile_cache", keyvalues={"user_id": user_id}, values={ @@ -107,7 +107,7 @@ class ProfileStore(ProfileWorkerStore): ) def update_remote_profile_cache(self, user_id, displayname, avatar_url): - return self.simple_update( + return self.db.simple_update( table="remote_profile_cache", keyvalues={"user_id": user_id}, values={ @@ -125,7 +125,7 @@ class ProfileStore(ProfileWorkerStore): """ subscribed = yield self.is_subscribed_remote_profile_for_user(user_id) if not subscribed: - yield self.simple_delete( + yield self.db.simple_delete( table="remote_profile_cache", keyvalues={"user_id": user_id}, desc="delete_remote_profile_cache", @@ -144,9 +144,9 @@ class ProfileStore(ProfileWorkerStore): txn.execute(sql, (last_checked,)) - return self.cursor_to_dict(txn) + return self.db.cursor_to_dict(txn) - return self.runInteraction( + return self.db.runInteraction( "get_remote_profile_cache_entries_that_expire", _get_remote_profile_cache_entries_that_expire_txn, ) @@ -155,7 +155,7 @@ class ProfileStore(ProfileWorkerStore): def is_subscribed_remote_profile_for_user(self, user_id): """Check whether we are interested in a remote user's profile. """ - res = yield self.simple_select_one_onecol( + res = yield self.db.simple_select_one_onecol( table="group_users", keyvalues={"user_id": user_id}, retcol="user_id", @@ -166,7 +166,7 @@ class ProfileStore(ProfileWorkerStore): if res: return True - res = yield self.simple_select_one_onecol( + res = yield self.db.simple_select_one_onecol( table="group_invites", keyvalues={"user_id": user_id}, retcol="user_id", diff --git a/synapse/storage/data_stores/main/push_rule.py b/synapse/storage/data_stores/main/push_rule.py index 75bd499bcd..de682cc63a 100644 --- a/synapse/storage/data_stores/main/push_rule.py +++ b/synapse/storage/data_stores/main/push_rule.py @@ -75,7 +75,7 @@ class PushRulesWorkerStore( def __init__(self, db_conn, hs): super(PushRulesWorkerStore, self).__init__(db_conn, hs) - push_rules_prefill, push_rules_id = self.get_cache_dict( + push_rules_prefill, push_rules_id = self.db.get_cache_dict( db_conn, "push_rules_stream", entity_column="user_id", @@ -100,7 +100,7 @@ class PushRulesWorkerStore( @cachedInlineCallbacks(max_entries=5000) def get_push_rules_for_user(self, user_id): - rows = yield self.simple_select_list( + rows = yield self.db.simple_select_list( table="push_rules", keyvalues={"user_name": user_id}, retcols=( @@ -124,7 +124,7 @@ class PushRulesWorkerStore( @cachedInlineCallbacks(max_entries=5000) def get_push_rules_enabled_for_user(self, user_id): - results = yield self.simple_select_list( + results = yield self.db.simple_select_list( table="push_rules_enable", keyvalues={"user_name": user_id}, retcols=("user_name", "rule_id", "enabled"), @@ -146,7 +146,7 @@ class PushRulesWorkerStore( (count,) = txn.fetchone() return bool(count) - return self.runInteraction( + return self.db.runInteraction( "have_push_rules_changed", have_push_rules_changed_txn ) @@ -162,7 +162,7 @@ class PushRulesWorkerStore( results = {user_id: [] for user_id in user_ids} - rows = yield self.simple_select_many_batch( + rows = yield self.db.simple_select_many_batch( table="push_rules", column="user_name", iterable=user_ids, @@ -320,7 +320,7 @@ class PushRulesWorkerStore( results = {user_id: {} for user_id in user_ids} - rows = yield self.simple_select_many_batch( + rows = yield self.db.simple_select_many_batch( table="push_rules_enable", column="user_name", iterable=user_ids, @@ -350,7 +350,7 @@ class PushRuleStore(PushRulesWorkerStore): with self._push_rules_stream_id_gen.get_next() as ids: stream_id, event_stream_ordering = ids if before or after: - yield self.runInteraction( + yield self.db.runInteraction( "_add_push_rule_relative_txn", self._add_push_rule_relative_txn, stream_id, @@ -364,7 +364,7 @@ class PushRuleStore(PushRulesWorkerStore): after, ) else: - yield self.runInteraction( + yield self.db.runInteraction( "_add_push_rule_highest_priority_txn", self._add_push_rule_highest_priority_txn, stream_id, @@ -395,7 +395,7 @@ class PushRuleStore(PushRulesWorkerStore): relative_to_rule = before or after - res = self.simple_select_one_txn( + res = self.db.simple_select_one_txn( txn, table="push_rules", keyvalues={"user_name": user_id, "rule_id": relative_to_rule}, @@ -518,7 +518,7 @@ class PushRuleStore(PushRulesWorkerStore): # We didn't update a row with the given rule_id so insert one push_rule_id = self._push_rule_id_gen.get_next() - self.simple_insert_txn( + self.db.simple_insert_txn( txn, table="push_rules", values={ @@ -561,7 +561,7 @@ class PushRuleStore(PushRulesWorkerStore): """ def delete_push_rule_txn(txn, stream_id, event_stream_ordering): - self.simple_delete_one_txn( + self.db.simple_delete_one_txn( txn, "push_rules", {"user_name": user_id, "rule_id": rule_id} ) @@ -571,7 +571,7 @@ class PushRuleStore(PushRulesWorkerStore): with self._push_rules_stream_id_gen.get_next() as ids: stream_id, event_stream_ordering = ids - yield self.runInteraction( + yield self.db.runInteraction( "delete_push_rule", delete_push_rule_txn, stream_id, @@ -582,7 +582,7 @@ class PushRuleStore(PushRulesWorkerStore): def set_push_rule_enabled(self, user_id, rule_id, enabled): with self._push_rules_stream_id_gen.get_next() as ids: stream_id, event_stream_ordering = ids - yield self.runInteraction( + yield self.db.runInteraction( "_set_push_rule_enabled_txn", self._set_push_rule_enabled_txn, stream_id, @@ -596,7 +596,7 @@ class PushRuleStore(PushRulesWorkerStore): self, txn, stream_id, event_stream_ordering, user_id, rule_id, enabled ): new_id = self._push_rules_enable_id_gen.get_next() - self.simple_upsert_txn( + self.db.simple_upsert_txn( txn, "push_rules_enable", {"user_name": user_id, "rule_id": rule_id}, @@ -636,7 +636,7 @@ class PushRuleStore(PushRulesWorkerStore): update_stream=False, ) else: - self.simple_update_one_txn( + self.db.simple_update_one_txn( txn, "push_rules", {"user_name": user_id, "rule_id": rule_id}, @@ -655,7 +655,7 @@ class PushRuleStore(PushRulesWorkerStore): with self._push_rules_stream_id_gen.get_next() as ids: stream_id, event_stream_ordering = ids - yield self.runInteraction( + yield self.db.runInteraction( "set_push_rule_actions", set_push_rule_actions_txn, stream_id, @@ -675,7 +675,7 @@ class PushRuleStore(PushRulesWorkerStore): if data is not None: values.update(data) - self.simple_insert_txn(txn, "push_rules_stream", values=values) + self.db.simple_insert_txn(txn, "push_rules_stream", values=values) txn.call_after(self.get_push_rules_for_user.invalidate, (user_id,)) txn.call_after(self.get_push_rules_enabled_for_user.invalidate, (user_id,)) @@ -699,7 +699,7 @@ class PushRuleStore(PushRulesWorkerStore): txn.execute(sql, (last_id, current_id, limit)) return txn.fetchall() - return self.runInteraction( + return self.db.runInteraction( "get_all_push_rule_updates", get_all_push_rule_updates_txn ) diff --git a/synapse/storage/data_stores/main/pusher.py b/synapse/storage/data_stores/main/pusher.py index d5a169872b..f07309ef09 100644 --- a/synapse/storage/data_stores/main/pusher.py +++ b/synapse/storage/data_stores/main/pusher.py @@ -59,7 +59,7 @@ class PusherWorkerStore(SQLBaseStore): @defer.inlineCallbacks def user_has_pusher(self, user_id): - ret = yield self.simple_select_one_onecol( + ret = yield self.db.simple_select_one_onecol( "pushers", {"user_name": user_id}, "id", allow_none=True ) return ret is not None @@ -72,7 +72,7 @@ class PusherWorkerStore(SQLBaseStore): @defer.inlineCallbacks def get_pushers_by(self, keyvalues): - ret = yield self.simple_select_list( + ret = yield self.db.simple_select_list( "pushers", keyvalues, [ @@ -100,11 +100,11 @@ class PusherWorkerStore(SQLBaseStore): def get_all_pushers(self): def get_pushers(txn): txn.execute("SELECT * FROM pushers") - rows = self.cursor_to_dict(txn) + rows = self.db.cursor_to_dict(txn) return self._decode_pushers_rows(rows) - rows = yield self.runInteraction("get_all_pushers", get_pushers) + rows = yield self.db.runInteraction("get_all_pushers", get_pushers) return rows def get_all_updated_pushers(self, last_id, current_id, limit): @@ -134,7 +134,7 @@ class PusherWorkerStore(SQLBaseStore): return updated, deleted - return self.runInteraction( + return self.db.runInteraction( "get_all_updated_pushers", get_all_updated_pushers_txn ) @@ -177,7 +177,7 @@ class PusherWorkerStore(SQLBaseStore): return results - return self.runInteraction( + return self.db.runInteraction( "get_all_updated_pushers_rows", get_all_updated_pushers_rows_txn ) @@ -193,7 +193,7 @@ class PusherWorkerStore(SQLBaseStore): inlineCallbacks=True, ) def get_if_users_have_pushers(self, user_ids): - rows = yield self.simple_select_many_batch( + rows = yield self.db.simple_select_many_batch( table="pushers", column="user_name", iterable=user_ids, @@ -230,7 +230,7 @@ class PusherStore(PusherWorkerStore): with self._pushers_id_gen.get_next() as stream_id: # no need to lock because `pushers` has a unique key on # (app_id, pushkey, user_name) so simple_upsert will retry - yield self.simple_upsert( + yield self.db.simple_upsert( table="pushers", keyvalues={"app_id": app_id, "pushkey": pushkey, "user_name": user_id}, values={ @@ -255,7 +255,7 @@ class PusherStore(PusherWorkerStore): if user_has_pusher is not True: # invalidate, since we the user might not have had a pusher before - yield self.runInteraction( + yield self.db.runInteraction( "add_pusher", self._invalidate_cache_and_stream, self.get_if_user_has_pusher, @@ -269,7 +269,7 @@ class PusherStore(PusherWorkerStore): txn, self.get_if_user_has_pusher, (user_id,) ) - self.simple_delete_one_txn( + self.db.simple_delete_one_txn( txn, "pushers", {"app_id": app_id, "pushkey": pushkey, "user_name": user_id}, @@ -278,7 +278,7 @@ class PusherStore(PusherWorkerStore): # it's possible for us to end up with duplicate rows for # (app_id, pushkey, user_id) at different stream_ids, but that # doesn't really matter. - self.simple_insert_txn( + self.db.simple_insert_txn( txn, table="deleted_pushers", values={ @@ -290,13 +290,13 @@ class PusherStore(PusherWorkerStore): ) with self._pushers_id_gen.get_next() as stream_id: - yield self.runInteraction("delete_pusher", delete_pusher_txn, stream_id) + yield self.db.runInteraction("delete_pusher", delete_pusher_txn, stream_id) @defer.inlineCallbacks def update_pusher_last_stream_ordering( self, app_id, pushkey, user_id, last_stream_ordering ): - yield self.simple_update_one( + yield self.db.simple_update_one( "pushers", {"app_id": app_id, "pushkey": pushkey, "user_name": user_id}, {"last_stream_ordering": last_stream_ordering}, @@ -319,7 +319,7 @@ class PusherStore(PusherWorkerStore): Returns: Deferred[bool]: True if the pusher still exists; False if it has been deleted. """ - updated = yield self.simple_update( + updated = yield self.db.simple_update( table="pushers", keyvalues={"app_id": app_id, "pushkey": pushkey, "user_name": user_id}, updatevalues={ @@ -333,7 +333,7 @@ class PusherStore(PusherWorkerStore): @defer.inlineCallbacks def update_pusher_failing_since(self, app_id, pushkey, user_id, failing_since): - yield self.simple_update( + yield self.db.simple_update( table="pushers", keyvalues={"app_id": app_id, "pushkey": pushkey, "user_name": user_id}, updatevalues={"failing_since": failing_since}, @@ -342,7 +342,7 @@ class PusherStore(PusherWorkerStore): @defer.inlineCallbacks def get_throttle_params_by_room(self, pusher_id): - res = yield self.simple_select_list( + res = yield self.db.simple_select_list( "pusher_throttle", {"pusher": pusher_id}, ["room_id", "last_sent_ts", "throttle_ms"], @@ -362,7 +362,7 @@ class PusherStore(PusherWorkerStore): def set_throttle_params(self, pusher_id, room_id, params): # no need to lock because `pusher_throttle` has a primary key on # (pusher, room_id) so simple_upsert will retry - yield self.simple_upsert( + yield self.db.simple_upsert( "pusher_throttle", {"pusher": pusher_id, "room_id": room_id}, params, diff --git a/synapse/storage/data_stores/main/receipts.py b/synapse/storage/data_stores/main/receipts.py index 380f388e30..ac2d45bd5c 100644 --- a/synapse/storage/data_stores/main/receipts.py +++ b/synapse/storage/data_stores/main/receipts.py @@ -61,7 +61,7 @@ class ReceiptsWorkerStore(SQLBaseStore): @cached(num_args=2) def get_receipts_for_room(self, room_id, receipt_type): - return self.simple_select_list( + return self.db.simple_select_list( table="receipts_linearized", keyvalues={"room_id": room_id, "receipt_type": receipt_type}, retcols=("user_id", "event_id"), @@ -70,7 +70,7 @@ class ReceiptsWorkerStore(SQLBaseStore): @cached(num_args=3) def get_last_receipt_event_id_for_user(self, user_id, room_id, receipt_type): - return self.simple_select_one_onecol( + return self.db.simple_select_one_onecol( table="receipts_linearized", keyvalues={ "room_id": room_id, @@ -84,7 +84,7 @@ class ReceiptsWorkerStore(SQLBaseStore): @cachedInlineCallbacks(num_args=2) def get_receipts_for_user(self, user_id, receipt_type): - rows = yield self.simple_select_list( + rows = yield self.db.simple_select_list( table="receipts_linearized", keyvalues={"user_id": user_id, "receipt_type": receipt_type}, retcols=("room_id", "event_id"), @@ -108,7 +108,7 @@ class ReceiptsWorkerStore(SQLBaseStore): txn.execute(sql, (user_id,)) return txn.fetchall() - rows = yield self.runInteraction("get_receipts_for_user_with_orderings", f) + rows = yield self.db.runInteraction("get_receipts_for_user_with_orderings", f) return { row[0]: { "event_id": row[1], @@ -187,11 +187,11 @@ class ReceiptsWorkerStore(SQLBaseStore): txn.execute(sql, (room_id, to_key)) - rows = self.cursor_to_dict(txn) + rows = self.db.cursor_to_dict(txn) return rows - rows = yield self.runInteraction("get_linearized_receipts_for_room", f) + rows = yield self.db.runInteraction("get_linearized_receipts_for_room", f) if not rows: return [] @@ -237,9 +237,11 @@ class ReceiptsWorkerStore(SQLBaseStore): txn.execute(sql + clause, [to_key] + list(args)) - return self.cursor_to_dict(txn) + return self.db.cursor_to_dict(txn) - txn_results = yield self.runInteraction("_get_linearized_receipts_for_rooms", f) + txn_results = yield self.db.runInteraction( + "_get_linearized_receipts_for_rooms", f + ) results = {} for row in txn_results: @@ -282,7 +284,7 @@ class ReceiptsWorkerStore(SQLBaseStore): return list(r[0:5] + (json.loads(r[5]),) for r in txn) - return self.runInteraction( + return self.db.runInteraction( "get_all_updated_receipts", get_all_updated_receipts_txn ) @@ -335,7 +337,7 @@ class ReceiptsStore(ReceiptsWorkerStore): otherwise, the rx timestamp of the event that the RR corresponds to (or 0 if the event is unknown) """ - res = self.simple_select_one_txn( + res = self.db.simple_select_one_txn( txn, table="events", retcols=["stream_ordering", "received_ts"], @@ -388,7 +390,7 @@ class ReceiptsStore(ReceiptsWorkerStore): (user_id, room_id, receipt_type), ) - self.simple_delete_txn( + self.db.simple_delete_txn( txn, table="receipts_linearized", keyvalues={ @@ -398,7 +400,7 @@ class ReceiptsStore(ReceiptsWorkerStore): }, ) - self.simple_insert_txn( + self.db.simple_insert_txn( txn, table="receipts_linearized", values={ @@ -453,13 +455,13 @@ class ReceiptsStore(ReceiptsWorkerStore): else: raise RuntimeError("Unrecognized event_ids: %r" % (event_ids,)) - linearized_event_id = yield self.runInteraction( + linearized_event_id = yield self.db.runInteraction( "insert_receipt_conv", graph_to_linear ) stream_id_manager = self._receipts_id_gen.get_next() with stream_id_manager as stream_id: - event_ts = yield self.runInteraction( + event_ts = yield self.db.runInteraction( "insert_linearized_receipt", self.insert_linearized_receipt_txn, room_id, @@ -488,7 +490,7 @@ class ReceiptsStore(ReceiptsWorkerStore): return stream_id, max_persisted_id def insert_graph_receipt(self, room_id, receipt_type, user_id, event_ids, data): - return self.runInteraction( + return self.db.runInteraction( "insert_graph_receipt", self.insert_graph_receipt_txn, room_id, @@ -514,7 +516,7 @@ class ReceiptsStore(ReceiptsWorkerStore): self._get_linearized_receipts_for_room.invalidate_many, (room_id,) ) - self.simple_delete_txn( + self.db.simple_delete_txn( txn, table="receipts_graph", keyvalues={ @@ -523,7 +525,7 @@ class ReceiptsStore(ReceiptsWorkerStore): "user_id": user_id, }, ) - self.simple_insert_txn( + self.db.simple_insert_txn( txn, table="receipts_graph", values={ diff --git a/synapse/storage/data_stores/main/registration.py b/synapse/storage/data_stores/main/registration.py index debc6706f5..8f9aa87ceb 100644 --- a/synapse/storage/data_stores/main/registration.py +++ b/synapse/storage/data_stores/main/registration.py @@ -45,7 +45,7 @@ class RegistrationWorkerStore(SQLBaseStore): @cached() def get_user_by_id(self, user_id): - return self.simple_select_one( + return self.db.simple_select_one( table="users", keyvalues={"name": user_id}, retcols=[ @@ -94,7 +94,7 @@ class RegistrationWorkerStore(SQLBaseStore): including the keys `name`, `is_guest`, `device_id`, `token_id`, `valid_until_ms`. """ - return self.runInteraction( + return self.db.runInteraction( "get_user_by_access_token", self._query_for_auth, token ) @@ -109,7 +109,7 @@ class RegistrationWorkerStore(SQLBaseStore): otherwise int representation of the timestamp (as a number of milliseconds since epoch). """ - res = yield self.simple_select_one_onecol( + res = yield self.db.simple_select_one_onecol( table="account_validity", keyvalues={"user_id": user_id}, retcol="expiration_ts_ms", @@ -137,7 +137,7 @@ class RegistrationWorkerStore(SQLBaseStore): """ def set_account_validity_for_user_txn(txn): - self.simple_update_txn( + self.db.simple_update_txn( txn=txn, table="account_validity", keyvalues={"user_id": user_id}, @@ -151,7 +151,7 @@ class RegistrationWorkerStore(SQLBaseStore): txn, self.get_expiration_ts_for_user, (user_id,) ) - yield self.runInteraction( + yield self.db.runInteraction( "set_account_validity_for_user", set_account_validity_for_user_txn ) @@ -167,7 +167,7 @@ class RegistrationWorkerStore(SQLBaseStore): Raises: StoreError: The provided token is already set for another user. """ - yield self.simple_update_one( + yield self.db.simple_update_one( table="account_validity", keyvalues={"user_id": user_id}, updatevalues={"renewal_token": renewal_token}, @@ -184,7 +184,7 @@ class RegistrationWorkerStore(SQLBaseStore): Returns: defer.Deferred[str]: The ID of the user to which the token belongs. """ - res = yield self.simple_select_one_onecol( + res = yield self.db.simple_select_one_onecol( table="account_validity", keyvalues={"renewal_token": renewal_token}, retcol="user_id", @@ -203,7 +203,7 @@ class RegistrationWorkerStore(SQLBaseStore): Returns: defer.Deferred[str]: The renewal token associated with this user ID. """ - res = yield self.simple_select_one_onecol( + res = yield self.db.simple_select_one_onecol( table="account_validity", keyvalues={"user_id": user_id}, retcol="renewal_token", @@ -229,9 +229,9 @@ class RegistrationWorkerStore(SQLBaseStore): ) values = [False, now_ms, renew_at] txn.execute(sql, values) - return self.cursor_to_dict(txn) + return self.db.cursor_to_dict(txn) - res = yield self.runInteraction( + res = yield self.db.runInteraction( "get_users_expiring_soon", select_users_txn, self.clock.time_msec(), @@ -250,7 +250,7 @@ class RegistrationWorkerStore(SQLBaseStore): email_sent (bool): Flag which indicates whether a renewal email has been sent to this user. """ - yield self.simple_update_one( + yield self.db.simple_update_one( table="account_validity", keyvalues={"user_id": user_id}, updatevalues={"email_sent": email_sent}, @@ -265,7 +265,7 @@ class RegistrationWorkerStore(SQLBaseStore): Args: user_id (str): ID of the user to remove from the account validity table. """ - yield self.simple_delete_one( + yield self.db.simple_delete_one( table="account_validity", keyvalues={"user_id": user_id}, desc="delete_account_validity_for_user", @@ -281,7 +281,7 @@ class RegistrationWorkerStore(SQLBaseStore): Returns (bool): true iff the user is a server admin, false otherwise. """ - res = yield self.simple_select_one_onecol( + res = yield self.db.simple_select_one_onecol( table="users", keyvalues={"name": user.to_string()}, retcol="admin", @@ -299,7 +299,7 @@ class RegistrationWorkerStore(SQLBaseStore): admin (bool): true iff the user is to be a server admin, false otherwise. """ - return self.simple_update_one( + return self.db.simple_update_one( table="users", keyvalues={"name": user.to_string()}, updatevalues={"admin": 1 if admin else 0}, @@ -316,7 +316,7 @@ class RegistrationWorkerStore(SQLBaseStore): ) txn.execute(sql, (token,)) - rows = self.cursor_to_dict(txn) + rows = self.db.cursor_to_dict(txn) if rows: return rows[0] @@ -332,7 +332,9 @@ class RegistrationWorkerStore(SQLBaseStore): Returns: Deferred[bool]: True if user 'user_type' is null or empty string """ - res = yield self.runInteraction("is_real_user", self.is_real_user_txn, user_id) + res = yield self.db.runInteraction( + "is_real_user", self.is_real_user_txn, user_id + ) return res @cachedInlineCallbacks() @@ -345,13 +347,13 @@ class RegistrationWorkerStore(SQLBaseStore): Returns: Deferred[bool]: True if user is of type UserTypes.SUPPORT """ - res = yield self.runInteraction( + res = yield self.db.runInteraction( "is_support_user", self.is_support_user_txn, user_id ) return res def is_real_user_txn(self, txn, user_id): - res = self.simple_select_one_onecol_txn( + res = self.db.simple_select_one_onecol_txn( txn=txn, table="users", keyvalues={"name": user_id}, @@ -361,7 +363,7 @@ class RegistrationWorkerStore(SQLBaseStore): return res is None def is_support_user_txn(self, txn, user_id): - res = self.simple_select_one_onecol_txn( + res = self.db.simple_select_one_onecol_txn( txn=txn, table="users", keyvalues={"name": user_id}, @@ -380,7 +382,7 @@ class RegistrationWorkerStore(SQLBaseStore): txn.execute(sql, (user_id,)) return dict(txn) - return self.runInteraction("get_users_by_id_case_insensitive", f) + return self.db.runInteraction("get_users_by_id_case_insensitive", f) async def get_user_by_external_id( self, auth_provider: str, external_id: str @@ -394,7 +396,7 @@ class RegistrationWorkerStore(SQLBaseStore): Returns: str|None: the mxid of the user, or None if they are not known """ - return await self.simple_select_one_onecol( + return await self.db.simple_select_one_onecol( table="user_external_ids", keyvalues={"auth_provider": auth_provider, "external_id": external_id}, retcol="user_id", @@ -408,12 +410,12 @@ class RegistrationWorkerStore(SQLBaseStore): def _count_users(txn): txn.execute("SELECT COUNT(*) AS users FROM users") - rows = self.cursor_to_dict(txn) + rows = self.db.cursor_to_dict(txn) if rows: return rows[0]["users"] return 0 - ret = yield self.runInteraction("count_users", _count_users) + ret = yield self.db.runInteraction("count_users", _count_users) return ret def count_daily_user_type(self): @@ -445,7 +447,7 @@ class RegistrationWorkerStore(SQLBaseStore): results[row[0]] = row[1] return results - return self.runInteraction("count_daily_user_type", _count_daily_user_type) + return self.db.runInteraction("count_daily_user_type", _count_daily_user_type) @defer.inlineCallbacks def count_nonbridged_users(self): @@ -459,7 +461,7 @@ class RegistrationWorkerStore(SQLBaseStore): (count,) = txn.fetchone() return count - ret = yield self.runInteraction("count_users", _count_users) + ret = yield self.db.runInteraction("count_users", _count_users) return ret @defer.inlineCallbacks @@ -468,12 +470,12 @@ class RegistrationWorkerStore(SQLBaseStore): def _count_users(txn): txn.execute("SELECT COUNT(*) AS users FROM users where user_type is null") - rows = self.cursor_to_dict(txn) + rows = self.db.cursor_to_dict(txn) if rows: return rows[0]["users"] return 0 - ret = yield self.runInteraction("count_real_users", _count_users) + ret = yield self.db.runInteraction("count_real_users", _count_users) return ret @defer.inlineCallbacks @@ -503,7 +505,7 @@ class RegistrationWorkerStore(SQLBaseStore): return ( ( - yield self.runInteraction( + yield self.db.runInteraction( "find_next_generated_user_id", _find_next_generated_user_id ) ) @@ -520,7 +522,7 @@ class RegistrationWorkerStore(SQLBaseStore): Returns: Deferred[str|None]: user id or None if no user id/threepid mapping exists """ - user_id = yield self.runInteraction( + user_id = yield self.db.runInteraction( "get_user_id_by_threepid", self.get_user_id_by_threepid_txn, medium, address ) return user_id @@ -536,7 +538,7 @@ class RegistrationWorkerStore(SQLBaseStore): Returns: str|None: user id or None if no user id/threepid mapping exists """ - ret = self.simple_select_one_txn( + ret = self.db.simple_select_one_txn( txn, "user_threepids", {"medium": medium, "address": address}, @@ -549,7 +551,7 @@ class RegistrationWorkerStore(SQLBaseStore): @defer.inlineCallbacks def user_add_threepid(self, user_id, medium, address, validated_at, added_at): - yield self.simple_upsert( + yield self.db.simple_upsert( "user_threepids", {"medium": medium, "address": address}, {"user_id": user_id, "validated_at": validated_at, "added_at": added_at}, @@ -557,7 +559,7 @@ class RegistrationWorkerStore(SQLBaseStore): @defer.inlineCallbacks def user_get_threepids(self, user_id): - ret = yield self.simple_select_list( + ret = yield self.db.simple_select_list( "user_threepids", {"user_id": user_id}, ["medium", "address", "validated_at", "added_at"], @@ -566,7 +568,7 @@ class RegistrationWorkerStore(SQLBaseStore): return ret def user_delete_threepid(self, user_id, medium, address): - return self.simple_delete( + return self.db.simple_delete( "user_threepids", keyvalues={"user_id": user_id, "medium": medium, "address": address}, desc="user_delete_threepid", @@ -579,7 +581,7 @@ class RegistrationWorkerStore(SQLBaseStore): user_id: The user id to delete all threepids of """ - return self.simple_delete( + return self.db.simple_delete( "user_threepids", keyvalues={"user_id": user_id}, desc="user_delete_threepids", @@ -601,7 +603,7 @@ class RegistrationWorkerStore(SQLBaseStore): """ # We need to use an upsert, in case they user had already bound the # threepid - return self.simple_upsert( + return self.db.simple_upsert( table="user_threepid_id_server", keyvalues={ "user_id": user_id, @@ -627,7 +629,7 @@ class RegistrationWorkerStore(SQLBaseStore): medium (str): The medium of the threepid (e.g "email") address (str): The address of the threepid (e.g "bob@example.com") """ - return self.simple_select_list( + return self.db.simple_select_list( table="user_threepid_id_server", keyvalues={"user_id": user_id}, retcols=["medium", "address"], @@ -648,7 +650,7 @@ class RegistrationWorkerStore(SQLBaseStore): Returns: Deferred """ - return self.simple_delete( + return self.db.simple_delete( table="user_threepid_id_server", keyvalues={ "user_id": user_id, @@ -671,7 +673,7 @@ class RegistrationWorkerStore(SQLBaseStore): Returns: Deferred[list[str]]: Resolves to a list of identity servers """ - return self.simple_select_onecol( + return self.db.simple_select_onecol( table="user_threepid_id_server", keyvalues={"user_id": user_id, "medium": medium, "address": address}, retcol="id_server", @@ -689,7 +691,7 @@ class RegistrationWorkerStore(SQLBaseStore): defer.Deferred(bool): The requested value. """ - res = yield self.simple_select_one_onecol( + res = yield self.db.simple_select_one_onecol( table="users", keyvalues={"name": user_id}, retcol="deactivated", @@ -756,13 +758,13 @@ class RegistrationWorkerStore(SQLBaseStore): sql += " LIMIT 1" txn.execute(sql, list(keyvalues.values())) - rows = self.cursor_to_dict(txn) + rows = self.db.cursor_to_dict(txn) if not rows: return None return rows[0] - return self.runInteraction( + return self.db.runInteraction( "get_threepid_validation_session", get_threepid_validation_session_txn ) @@ -776,18 +778,18 @@ class RegistrationWorkerStore(SQLBaseStore): """ def delete_threepid_session_txn(txn): - self.simple_delete_txn( + self.db.simple_delete_txn( txn, table="threepid_validation_token", keyvalues={"session_id": session_id}, ) - self.simple_delete_txn( + self.db.simple_delete_txn( txn, table="threepid_validation_session", keyvalues={"session_id": session_id}, ) - return self.runInteraction( + return self.db.runInteraction( "delete_threepid_session", delete_threepid_session_txn ) @@ -857,7 +859,7 @@ class RegistrationBackgroundUpdateStore( (last_user, batch_size), ) - rows = self.cursor_to_dict(txn) + rows = self.db.cursor_to_dict(txn) if not rows: return True, 0 @@ -880,7 +882,7 @@ class RegistrationBackgroundUpdateStore( else: return False, len(rows) - end, nb_processed = yield self.runInteraction( + end, nb_processed = yield self.db.runInteraction( "users_set_deactivated_flag", _background_update_set_deactivated_flag_txn ) @@ -911,7 +913,7 @@ class RegistrationBackgroundUpdateStore( txn.executemany(sql, [(id_server,) for id_server in id_servers]) if id_servers: - yield self.runInteraction( + yield self.db.runInteraction( "_bg_user_threepids_grandfather", _bg_user_threepids_grandfather_txn ) @@ -961,7 +963,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): """ next_id = self._access_tokens_id_gen.get_next() - yield self.simple_insert( + yield self.db.simple_insert( "access_tokens", { "id": next_id, @@ -1003,7 +1005,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): Raises: StoreError if the user_id could not be registered. """ - return self.runInteraction( + return self.db.runInteraction( "register_user", self._register_user, user_id, @@ -1037,7 +1039,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): # Ensure that the guest user actually exists # ``allow_none=False`` makes this raise an exception # if the row isn't in the database. - self.simple_select_one_txn( + self.db.simple_select_one_txn( txn, "users", keyvalues={"name": user_id, "is_guest": 1}, @@ -1045,7 +1047,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): allow_none=False, ) - self.simple_update_one_txn( + self.db.simple_update_one_txn( txn, "users", keyvalues={"name": user_id, "is_guest": 1}, @@ -1059,7 +1061,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): }, ) else: - self.simple_insert_txn( + self.db.simple_insert_txn( txn, "users", values={ @@ -1114,7 +1116,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): external_id: id on that system user_id: complete mxid that it is mapped to """ - return self.simple_insert( + return self.db.simple_insert( table="user_external_ids", values={ "auth_provider": auth_provider, @@ -1132,12 +1134,14 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): """ def user_set_password_hash_txn(txn): - self.simple_update_one_txn( + self.db.simple_update_one_txn( txn, "users", {"name": user_id}, {"password_hash": password_hash} ) self._invalidate_cache_and_stream(txn, self.get_user_by_id, (user_id,)) - return self.runInteraction("user_set_password_hash", user_set_password_hash_txn) + return self.db.runInteraction( + "user_set_password_hash", user_set_password_hash_txn + ) def user_set_consent_version(self, user_id, consent_version): """Updates the user table to record privacy policy consent @@ -1152,7 +1156,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): """ def f(txn): - self.simple_update_one_txn( + self.db.simple_update_one_txn( txn, table="users", keyvalues={"name": user_id}, @@ -1160,7 +1164,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): ) self._invalidate_cache_and_stream(txn, self.get_user_by_id, (user_id,)) - return self.runInteraction("user_set_consent_version", f) + return self.db.runInteraction("user_set_consent_version", f) def user_set_consent_server_notice_sent(self, user_id, consent_version): """Updates the user table to record that we have sent the user a server @@ -1176,7 +1180,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): """ def f(txn): - self.simple_update_one_txn( + self.db.simple_update_one_txn( txn, table="users", keyvalues={"name": user_id}, @@ -1184,7 +1188,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): ) self._invalidate_cache_and_stream(txn, self.get_user_by_id, (user_id,)) - return self.runInteraction("user_set_consent_server_notice_sent", f) + return self.db.runInteraction("user_set_consent_server_notice_sent", f) def user_delete_access_tokens(self, user_id, except_token_id=None, device_id=None): """ @@ -1230,11 +1234,11 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): return tokens_and_devices - return self.runInteraction("user_delete_access_tokens", f) + return self.db.runInteraction("user_delete_access_tokens", f) def delete_access_token(self, access_token): def f(txn): - self.simple_delete_one_txn( + self.db.simple_delete_one_txn( txn, table="access_tokens", keyvalues={"token": access_token} ) @@ -1242,11 +1246,11 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): txn, self.get_user_by_access_token, (access_token,) ) - return self.runInteraction("delete_access_token", f) + return self.db.runInteraction("delete_access_token", f) @cachedInlineCallbacks() def is_guest(self, user_id): - res = yield self.simple_select_one_onecol( + res = yield self.db.simple_select_one_onecol( table="users", keyvalues={"name": user_id}, retcol="is_guest", @@ -1261,7 +1265,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): Adds a user to the table of users who need to be parted from all the rooms they're in """ - return self.simple_insert( + return self.db.simple_insert( "users_pending_deactivation", values={"user_id": user_id}, desc="add_user_pending_deactivation", @@ -1274,7 +1278,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): """ # XXX: This should be simple_delete_one but we failed to put a unique index on # the table, so somehow duplicate entries have ended up in it. - return self.simple_delete( + return self.db.simple_delete( "users_pending_deactivation", keyvalues={"user_id": user_id}, desc="del_user_pending_deactivation", @@ -1285,7 +1289,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): Gets one user from the table of users waiting to be parted from all the rooms they're in. """ - return self.simple_select_one_onecol( + return self.db.simple_select_one_onecol( "users_pending_deactivation", keyvalues={}, retcol="user_id", @@ -1315,7 +1319,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): # Insert everything into a transaction in order to run atomically def validate_threepid_session_txn(txn): - row = self.simple_select_one_txn( + row = self.db.simple_select_one_txn( txn, table="threepid_validation_session", keyvalues={"session_id": session_id}, @@ -1333,7 +1337,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): 400, "This client_secret does not match the provided session_id" ) - row = self.simple_select_one_txn( + row = self.db.simple_select_one_txn( txn, table="threepid_validation_token", keyvalues={"session_id": session_id, "token": token}, @@ -1358,7 +1362,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): ) # Looks good. Validate the session - self.simple_update_txn( + self.db.simple_update_txn( txn, table="threepid_validation_session", keyvalues={"session_id": session_id}, @@ -1368,7 +1372,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): return next_link # Return next_link if it exists - return self.runInteraction( + return self.db.runInteraction( "validate_threepid_session_txn", validate_threepid_session_txn ) @@ -1401,7 +1405,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): if validated_at: insertion_values["validated_at"] = validated_at - return self.simple_upsert( + return self.db.simple_upsert( table="threepid_validation_session", keyvalues={"session_id": session_id}, values={"last_send_attempt": send_attempt}, @@ -1439,7 +1443,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): def start_or_continue_validation_session_txn(txn): # Create or update a validation session - self.simple_upsert_txn( + self.db.simple_upsert_txn( txn, table="threepid_validation_session", keyvalues={"session_id": session_id}, @@ -1452,7 +1456,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): ) # Create a new validation token with this session ID - self.simple_insert_txn( + self.db.simple_insert_txn( txn, table="threepid_validation_token", values={ @@ -1463,7 +1467,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): }, ) - return self.runInteraction( + return self.db.runInteraction( "start_or_continue_validation_session", start_or_continue_validation_session_txn, ) @@ -1478,7 +1482,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): """ return txn.execute(sql, (ts,)) - return self.runInteraction( + return self.db.runInteraction( "cull_expired_threepid_validation_tokens", cull_expired_threepid_validation_tokens_txn, self.clock.time_msec(), @@ -1493,7 +1497,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): deactivated (bool): The value to set for `deactivated`. """ - yield self.runInteraction( + yield self.db.runInteraction( "set_user_deactivated_status", self.set_user_deactivated_status_txn, user_id, @@ -1501,7 +1505,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): ) def set_user_deactivated_status_txn(self, txn, user_id, deactivated): - self.simple_update_one_txn( + self.db.simple_update_one_txn( txn=txn, table="users", keyvalues={"name": user_id}, @@ -1529,14 +1533,14 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): ) txn.execute(sql, []) - res = self.cursor_to_dict(txn) + res = self.db.cursor_to_dict(txn) if res: for user in res: self.set_expiration_date_for_user_txn( txn, user["name"], use_delta=True ) - yield self.runInteraction( + yield self.db.runInteraction( "get_users_with_no_expiration_date", select_users_with_no_expiration_date_txn, ) @@ -1560,7 +1564,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore): expiration_ts, ) - self.simple_upsert_txn( + self.db.simple_upsert_txn( txn, "account_validity", keyvalues={"user_id": user_id}, diff --git a/synapse/storage/data_stores/main/rejections.py b/synapse/storage/data_stores/main/rejections.py index f81f9279a1..1c07c7a425 100644 --- a/synapse/storage/data_stores/main/rejections.py +++ b/synapse/storage/data_stores/main/rejections.py @@ -22,7 +22,7 @@ logger = logging.getLogger(__name__) class RejectionsStore(SQLBaseStore): def _store_rejections_txn(self, txn, event_id, reason): - self.simple_insert_txn( + self.db.simple_insert_txn( txn, table="rejections", values={ @@ -33,7 +33,7 @@ class RejectionsStore(SQLBaseStore): ) def get_rejection_reason(self, event_id): - return self.simple_select_one_onecol( + return self.db.simple_select_one_onecol( table="rejections", retcol="reason", keyvalues={"event_id": event_id}, diff --git a/synapse/storage/data_stores/main/relations.py b/synapse/storage/data_stores/main/relations.py index aa5e10538b..046c2b4845 100644 --- a/synapse/storage/data_stores/main/relations.py +++ b/synapse/storage/data_stores/main/relations.py @@ -129,7 +129,7 @@ class RelationsWorkerStore(SQLBaseStore): chunk=list(events[:limit]), next_batch=next_batch, prev_batch=from_token ) - return self.runInteraction( + return self.db.runInteraction( "get_recent_references_for_event", _get_recent_references_for_event_txn ) @@ -223,7 +223,7 @@ class RelationsWorkerStore(SQLBaseStore): chunk=list(events[:limit]), next_batch=next_batch, prev_batch=from_token ) - return self.runInteraction( + return self.db.runInteraction( "get_aggregation_groups_for_event", _get_aggregation_groups_for_event_txn ) @@ -268,7 +268,7 @@ class RelationsWorkerStore(SQLBaseStore): if row: return row[0] - edit_id = yield self.runInteraction( + edit_id = yield self.db.runInteraction( "get_applicable_edit", _get_applicable_edit_txn ) @@ -318,7 +318,7 @@ class RelationsWorkerStore(SQLBaseStore): return bool(txn.fetchone()) - return self.runInteraction( + return self.db.runInteraction( "get_if_user_has_annotated_event", _get_if_user_has_annotated_event ) @@ -352,7 +352,7 @@ class RelationsStore(RelationsWorkerStore): aggregation_key = relation.get("key") - self.simple_insert_txn( + self.db.simple_insert_txn( txn, table="event_relations", values={ @@ -380,6 +380,6 @@ class RelationsStore(RelationsWorkerStore): redacted_event_id (str): The event that was redacted. """ - self.simple_delete_txn( + self.db.simple_delete_txn( txn, table="event_relations", keyvalues={"event_id": redacted_event_id} ) diff --git a/synapse/storage/data_stores/main/room.py b/synapse/storage/data_stores/main/room.py index f309e3640c..a26ed47afc 100644 --- a/synapse/storage/data_stores/main/room.py +++ b/synapse/storage/data_stores/main/room.py @@ -54,7 +54,7 @@ class RoomWorkerStore(SQLBaseStore): Returns: A dict containing the room information, or None if the room is unknown. """ - return self.simple_select_one( + return self.db.simple_select_one( table="rooms", keyvalues={"room_id": room_id}, retcols=("room_id", "is_public", "creator"), @@ -63,7 +63,7 @@ class RoomWorkerStore(SQLBaseStore): ) def get_public_room_ids(self): - return self.simple_select_onecol( + return self.db.simple_select_onecol( table="rooms", keyvalues={"is_public": True}, retcol="room_id", @@ -120,7 +120,7 @@ class RoomWorkerStore(SQLBaseStore): txn.execute(sql, query_args) return txn.fetchone()[0] - return self.runInteraction("count_public_rooms", _count_public_rooms_txn) + return self.db.runInteraction("count_public_rooms", _count_public_rooms_txn) @defer.inlineCallbacks def get_largest_public_rooms( @@ -253,21 +253,21 @@ class RoomWorkerStore(SQLBaseStore): def _get_largest_public_rooms_txn(txn): txn.execute(sql, query_args) - results = self.cursor_to_dict(txn) + results = self.db.cursor_to_dict(txn) if not forwards: results.reverse() return results - ret_val = yield self.runInteraction( + ret_val = yield self.db.runInteraction( "get_largest_public_rooms", _get_largest_public_rooms_txn ) defer.returnValue(ret_val) @cached(max_entries=10000) def is_room_blocked(self, room_id): - return self.simple_select_one_onecol( + return self.db.simple_select_one_onecol( table="blocked_rooms", keyvalues={"room_id": room_id}, retcol="1", @@ -288,7 +288,7 @@ class RoomWorkerStore(SQLBaseStore): of RatelimitOverride are None or 0 then ratelimitng has been disabled for that user entirely. """ - row = yield self.simple_select_one( + row = yield self.db.simple_select_one( table="ratelimit_override", keyvalues={"user_id": user_id}, retcols=("messages_per_second", "burst_count"), @@ -330,9 +330,9 @@ class RoomWorkerStore(SQLBaseStore): (room_id,), ) - return self.cursor_to_dict(txn) + return self.db.cursor_to_dict(txn) - ret = yield self.runInteraction( + ret = yield self.db.runInteraction( "get_retention_policy_for_room", get_retention_policy_for_room_txn, ) @@ -396,7 +396,7 @@ class RoomBackgroundUpdateStore(BackgroundUpdateStore): (last_room, batch_size), ) - rows = self.cursor_to_dict(txn) + rows = self.db.cursor_to_dict(txn) if not rows: return True @@ -408,7 +408,7 @@ class RoomBackgroundUpdateStore(BackgroundUpdateStore): ev = json.loads(row["json"]) retention_policy = json.dumps(ev["content"]) - self.simple_insert_txn( + self.db.simple_insert_txn( txn=txn, table="room_retention", values={ @@ -430,7 +430,7 @@ class RoomBackgroundUpdateStore(BackgroundUpdateStore): else: return False - end = yield self.runInteraction( + end = yield self.db.runInteraction( "insert_room_retention", _background_insert_retention_txn, ) @@ -461,7 +461,7 @@ class RoomStore(RoomBackgroundUpdateStore, RoomWorkerStore, SearchStore): try: def store_room_txn(txn, next_id): - self.simple_insert_txn( + self.db.simple_insert_txn( txn, "rooms", { @@ -471,7 +471,7 @@ class RoomStore(RoomBackgroundUpdateStore, RoomWorkerStore, SearchStore): }, ) if is_public: - self.simple_insert_txn( + self.db.simple_insert_txn( txn, table="public_room_list_stream", values={ @@ -482,7 +482,7 @@ class RoomStore(RoomBackgroundUpdateStore, RoomWorkerStore, SearchStore): ) with self._public_room_id_gen.get_next() as next_id: - yield self.runInteraction("store_room_txn", store_room_txn, next_id) + yield self.db.runInteraction("store_room_txn", store_room_txn, next_id) except Exception as e: logger.error("store_room with room_id=%s failed: %s", room_id, e) raise StoreError(500, "Problem creating room.") @@ -490,14 +490,14 @@ class RoomStore(RoomBackgroundUpdateStore, RoomWorkerStore, SearchStore): @defer.inlineCallbacks def set_room_is_public(self, room_id, is_public): def set_room_is_public_txn(txn, next_id): - self.simple_update_one_txn( + self.db.simple_update_one_txn( txn, table="rooms", keyvalues={"room_id": room_id}, updatevalues={"is_public": is_public}, ) - entries = self.simple_select_list_txn( + entries = self.db.simple_select_list_txn( txn, table="public_room_list_stream", keyvalues={ @@ -515,7 +515,7 @@ class RoomStore(RoomBackgroundUpdateStore, RoomWorkerStore, SearchStore): add_to_stream = bool(entries[-1]["visibility"]) != is_public if add_to_stream: - self.simple_insert_txn( + self.db.simple_insert_txn( txn, table="public_room_list_stream", values={ @@ -528,7 +528,7 @@ class RoomStore(RoomBackgroundUpdateStore, RoomWorkerStore, SearchStore): ) with self._public_room_id_gen.get_next() as next_id: - yield self.runInteraction( + yield self.db.runInteraction( "set_room_is_public", set_room_is_public_txn, next_id ) self.hs.get_notifier().on_new_replication_data() @@ -555,7 +555,7 @@ class RoomStore(RoomBackgroundUpdateStore, RoomWorkerStore, SearchStore): def set_room_is_public_appservice_txn(txn, next_id): if is_public: try: - self.simple_insert_txn( + self.db.simple_insert_txn( txn, table="appservice_room_list", values={ @@ -568,7 +568,7 @@ class RoomStore(RoomBackgroundUpdateStore, RoomWorkerStore, SearchStore): # We've already inserted, nothing to do. return else: - self.simple_delete_txn( + self.db.simple_delete_txn( txn, table="appservice_room_list", keyvalues={ @@ -578,7 +578,7 @@ class RoomStore(RoomBackgroundUpdateStore, RoomWorkerStore, SearchStore): }, ) - entries = self.simple_select_list_txn( + entries = self.db.simple_select_list_txn( txn, table="public_room_list_stream", keyvalues={ @@ -596,7 +596,7 @@ class RoomStore(RoomBackgroundUpdateStore, RoomWorkerStore, SearchStore): add_to_stream = bool(entries[-1]["visibility"]) != is_public if add_to_stream: - self.simple_insert_txn( + self.db.simple_insert_txn( txn, table="public_room_list_stream", values={ @@ -609,7 +609,7 @@ class RoomStore(RoomBackgroundUpdateStore, RoomWorkerStore, SearchStore): ) with self._public_room_id_gen.get_next() as next_id: - yield self.runInteraction( + yield self.db.runInteraction( "set_room_is_public_appservice", set_room_is_public_appservice_txn, next_id, @@ -626,7 +626,7 @@ class RoomStore(RoomBackgroundUpdateStore, RoomWorkerStore, SearchStore): row = txn.fetchone() return row[0] or 0 - return self.runInteraction("get_rooms", f) + return self.db.runInteraction("get_rooms", f) def _store_room_topic_txn(self, txn, event): if hasattr(event, "content") and "topic" in event.content: @@ -660,7 +660,7 @@ class RoomStore(RoomBackgroundUpdateStore, RoomWorkerStore, SearchStore): # Ignore the event if one of the value isn't an integer. return - self.simple_insert_txn( + self.db.simple_insert_txn( txn=txn, table="room_retention", values={ @@ -679,7 +679,7 @@ class RoomStore(RoomBackgroundUpdateStore, RoomWorkerStore, SearchStore): self, room_id, event_id, user_id, reason, content, received_ts ): next_id = self._event_reports_id_gen.get_next() - return self.simple_insert( + return self.db.simple_insert( table="event_reports", values={ "id": next_id, @@ -712,7 +712,9 @@ class RoomStore(RoomBackgroundUpdateStore, RoomWorkerStore, SearchStore): if prev_id == current_id: return defer.succeed([]) - return self.runInteraction("get_all_new_public_rooms", get_all_new_public_rooms) + return self.db.runInteraction( + "get_all_new_public_rooms", get_all_new_public_rooms + ) @defer.inlineCallbacks def block_room(self, room_id, user_id): @@ -725,14 +727,14 @@ class RoomStore(RoomBackgroundUpdateStore, RoomWorkerStore, SearchStore): Returns: Deferred """ - yield self.simple_upsert( + yield self.db.simple_upsert( table="blocked_rooms", keyvalues={"room_id": room_id}, values={}, insertion_values={"user_id": user_id}, desc="block_room", ) - yield self.runInteraction( + yield self.db.runInteraction( "block_room_invalidation", self._invalidate_cache_and_stream, self.is_room_blocked, @@ -763,7 +765,9 @@ class RoomStore(RoomBackgroundUpdateStore, RoomWorkerStore, SearchStore): return local_media_mxcs, remote_media_mxcs - return self.runInteraction("get_media_ids_in_room", _get_media_mxcs_in_room_txn) + return self.db.runInteraction( + "get_media_ids_in_room", _get_media_mxcs_in_room_txn + ) def quarantine_media_ids_in_room(self, room_id, quarantined_by): """For a room loops through all events with media and quarantines @@ -802,7 +806,7 @@ class RoomStore(RoomBackgroundUpdateStore, RoomWorkerStore, SearchStore): return total_media_quarantined - return self.runInteraction( + return self.db.runInteraction( "quarantine_media_in_room", _quarantine_media_in_room_txn ) @@ -907,7 +911,7 @@ class RoomStore(RoomBackgroundUpdateStore, RoomWorkerStore, SearchStore): txn.execute(sql, args) - rows = self.cursor_to_dict(txn) + rows = self.db.cursor_to_dict(txn) rooms_dict = {} for row in rows: @@ -923,7 +927,7 @@ class RoomStore(RoomBackgroundUpdateStore, RoomWorkerStore, SearchStore): txn.execute(sql) - rows = self.cursor_to_dict(txn) + rows = self.db.cursor_to_dict(txn) # If a room isn't already in the dict (i.e. it doesn't have a retention # policy in its state), add it with a null policy. @@ -936,7 +940,7 @@ class RoomStore(RoomBackgroundUpdateStore, RoomWorkerStore, SearchStore): return rooms_dict - rooms = yield self.runInteraction( + rooms = yield self.db.runInteraction( "get_rooms_for_retention_period_in_range", get_rooms_for_retention_period_in_range_txn, ) diff --git a/synapse/storage/data_stores/main/roommember.py b/synapse/storage/data_stores/main/roommember.py index fe2428a281..7f4d02b25b 100644 --- a/synapse/storage/data_stores/main/roommember.py +++ b/synapse/storage/data_stores/main/roommember.py @@ -116,7 +116,7 @@ class RoomMemberWorkerStore(EventsWorkerStore): txn.execute(query) return list(txn)[0][0] - count = yield self.runInteraction("get_known_servers", _transact) + count = yield self.db.runInteraction("get_known_servers", _transact) # We always know about ourselves, even if we have nothing in # room_memberships (for example, the server is new). @@ -128,7 +128,7 @@ class RoomMemberWorkerStore(EventsWorkerStore): membership column is up to date """ - pending_update = self.simple_select_one_txn( + pending_update = self.db.simple_select_one_txn( txn, table="background_updates", keyvalues={"update_name": _CURRENT_STATE_MEMBERSHIP_UPDATE_NAME}, @@ -144,7 +144,7 @@ class RoomMemberWorkerStore(EventsWorkerStore): 15.0, run_as_background_process, "_check_safe_current_state_events_membership_updated", - self.runInteraction, + self.db.runInteraction, "_check_safe_current_state_events_membership_updated", self._check_safe_current_state_events_membership_updated_txn, ) @@ -161,7 +161,7 @@ class RoomMemberWorkerStore(EventsWorkerStore): @cached(max_entries=100000, iterable=True) def get_users_in_room(self, room_id): - return self.runInteraction( + return self.db.runInteraction( "get_users_in_room", self.get_users_in_room_txn, room_id ) @@ -269,7 +269,7 @@ class RoomMemberWorkerStore(EventsWorkerStore): return res - return self.runInteraction("get_room_summary", _get_room_summary_txn) + return self.db.runInteraction("get_room_summary", _get_room_summary_txn) def _get_user_counts_in_room_txn(self, txn, room_id): """ @@ -339,7 +339,7 @@ class RoomMemberWorkerStore(EventsWorkerStore): if not membership_list: return defer.succeed(None) - rooms = yield self.runInteraction( + rooms = yield self.db.runInteraction( "get_rooms_for_user_where_membership_is", self._get_rooms_for_user_where_membership_is_txn, user_id, @@ -392,7 +392,7 @@ class RoomMemberWorkerStore(EventsWorkerStore): ) txn.execute(sql, (user_id, *args)) - results = [RoomsForUser(**r) for r in self.cursor_to_dict(txn)] + results = [RoomsForUser(**r) for r in self.db.cursor_to_dict(txn)] if do_invite: sql = ( @@ -412,7 +412,7 @@ class RoomMemberWorkerStore(EventsWorkerStore): stream_ordering=r["stream_ordering"], membership=Membership.INVITE, ) - for r in self.cursor_to_dict(txn) + for r in self.db.cursor_to_dict(txn) ) return results @@ -603,7 +603,7 @@ class RoomMemberWorkerStore(EventsWorkerStore): to `user_id` and ProfileInfo (or None if not join event). """ - rows = yield self.simple_select_many_batch( + rows = yield self.db.simple_select_many_batch( table="room_memberships", column="event_id", iterable=event_ids, @@ -643,7 +643,7 @@ class RoomMemberWorkerStore(EventsWorkerStore): # the returned user actually has the correct domain. like_clause = "%:" + host - rows = yield self.execute("is_host_joined", None, sql, room_id, like_clause) + rows = yield self.db.execute("is_host_joined", None, sql, room_id, like_clause) if not rows: return False @@ -683,7 +683,7 @@ class RoomMemberWorkerStore(EventsWorkerStore): # the returned user actually has the correct domain. like_clause = "%:" + host - rows = yield self.execute("was_host_joined", None, sql, room_id, like_clause) + rows = yield self.db.execute("was_host_joined", None, sql, room_id, like_clause) if not rows: return False @@ -753,7 +753,7 @@ class RoomMemberWorkerStore(EventsWorkerStore): rows = txn.fetchall() return rows[0][0] - count = yield self.runInteraction("did_forget_membership", f) + count = yield self.db.runInteraction("did_forget_membership", f) return count == 0 @cached() @@ -790,7 +790,7 @@ class RoomMemberWorkerStore(EventsWorkerStore): txn.execute(sql, (user_id,)) return set(row[0] for row in txn if row[1] == 0) - return self.runInteraction( + return self.db.runInteraction( "get_forgotten_rooms_for_user", _get_forgotten_rooms_for_user_txn ) @@ -805,7 +805,7 @@ class RoomMemberWorkerStore(EventsWorkerStore): Deferred[set[str]]: Set of room IDs. """ - room_ids = yield self.simple_select_onecol( + room_ids = yield self.db.simple_select_onecol( table="room_memberships", keyvalues={"membership": Membership.JOIN, "user_id": user_id}, retcol="room_id", @@ -820,7 +820,7 @@ class RoomMemberWorkerStore(EventsWorkerStore): """Get user_id and membership of a set of event IDs. """ - return self.simple_select_many_batch( + return self.db.simple_select_many_batch( table="room_memberships", column="event_id", iterable=member_event_ids, @@ -874,7 +874,7 @@ class RoomMemberBackgroundUpdateStore(BackgroundUpdateStore): txn.execute(sql, (target_min_stream_id, max_stream_id, batch_size)) - rows = self.cursor_to_dict(txn) + rows = self.db.cursor_to_dict(txn) if not rows: return 0 @@ -915,7 +915,7 @@ class RoomMemberBackgroundUpdateStore(BackgroundUpdateStore): return len(rows) - result = yield self.runInteraction( + result = yield self.db.runInteraction( _MEMBERSHIP_PROFILE_UPDATE_NAME, add_membership_profile_txn ) @@ -971,7 +971,7 @@ class RoomMemberBackgroundUpdateStore(BackgroundUpdateStore): # string, which will compare before all room IDs correctly. last_processed_room = progress.get("last_processed_room", "") - row_count, finished = yield self.runInteraction( + row_count, finished = yield self.db.runInteraction( "_background_current_state_membership_update", _background_current_state_membership_txn, last_processed_room, @@ -990,7 +990,7 @@ class RoomMemberStore(RoomMemberWorkerStore, RoomMemberBackgroundUpdateStore): def _store_room_members_txn(self, txn, events, backfilled): """Store a room member in the database. """ - self.simple_insert_many_txn( + self.db.simple_insert_many_txn( txn, table="room_memberships", values=[ @@ -1028,7 +1028,7 @@ class RoomMemberStore(RoomMemberWorkerStore, RoomMemberBackgroundUpdateStore): is_mine = self.hs.is_mine_id(event.state_key) if is_new_state and is_mine: if event.membership == Membership.INVITE: - self.simple_insert_txn( + self.db.simple_insert_txn( txn, table="local_invites", values={ @@ -1068,7 +1068,7 @@ class RoomMemberStore(RoomMemberWorkerStore, RoomMemberBackgroundUpdateStore): txn.execute(sql, (stream_ordering, True, room_id, user_id)) with self._stream_id_gen.get_next() as stream_ordering: - yield self.runInteraction("locally_reject_invite", f, stream_ordering) + yield self.db.runInteraction("locally_reject_invite", f, stream_ordering) def forget(self, user_id, room_id): """Indicate that user_id wishes to discard history for room_id.""" @@ -1091,7 +1091,7 @@ class RoomMemberStore(RoomMemberWorkerStore, RoomMemberBackgroundUpdateStore): txn, self.get_forgotten_rooms_for_user, (user_id,) ) - return self.runInteraction("forget_membership", f) + return self.db.runInteraction("forget_membership", f) class _JoinedHostsCache(object): diff --git a/synapse/storage/data_stores/main/search.py b/synapse/storage/data_stores/main/search.py index f735cf095c..55a604850e 100644 --- a/synapse/storage/data_stores/main/search.py +++ b/synapse/storage/data_stores/main/search.py @@ -93,7 +93,7 @@ class SearchBackgroundUpdateStore(BackgroundUpdateStore): # store_search_entries_txn with a generator function, but that # would mean having two cursors open on the database at once. # Instead we just build a list of results. - rows = self.cursor_to_dict(txn) + rows = self.db.cursor_to_dict(txn) if not rows: return 0 @@ -159,7 +159,7 @@ class SearchBackgroundUpdateStore(BackgroundUpdateStore): return len(event_search_rows) - result = yield self.runInteraction( + result = yield self.db.runInteraction( self.EVENT_SEARCH_UPDATE_NAME, reindex_search_txn ) @@ -206,7 +206,7 @@ class SearchBackgroundUpdateStore(BackgroundUpdateStore): conn.set_session(autocommit=False) if isinstance(self.database_engine, PostgresEngine): - yield self.runWithConnection(create_index) + yield self.db.runWithConnection(create_index) yield self._end_background_update(self.EVENT_SEARCH_USE_GIN_POSTGRES_NAME) return 1 @@ -237,12 +237,12 @@ class SearchBackgroundUpdateStore(BackgroundUpdateStore): ) conn.set_session(autocommit=False) - yield self.runWithConnection(create_index) + yield self.db.runWithConnection(create_index) pg = dict(progress) pg["have_added_indexes"] = True - yield self.runInteraction( + yield self.db.runInteraction( self.EVENT_SEARCH_ORDER_UPDATE_NAME, self._background_update_progress_txn, self.EVENT_SEARCH_ORDER_UPDATE_NAME, @@ -280,7 +280,7 @@ class SearchBackgroundUpdateStore(BackgroundUpdateStore): return len(rows), True - num_rows, finished = yield self.runInteraction( + num_rows, finished = yield self.db.runInteraction( self.EVENT_SEARCH_ORDER_UPDATE_NAME, reindex_search_txn ) @@ -441,7 +441,9 @@ class SearchStore(SearchBackgroundUpdateStore): # entire table from the database. sql += " ORDER BY rank DESC LIMIT 500" - results = yield self.execute("search_msgs", self.cursor_to_dict, sql, *args) + results = yield self.db.execute( + "search_msgs", self.db.cursor_to_dict, sql, *args + ) results = list(filter(lambda row: row["room_id"] in room_ids, results)) @@ -455,8 +457,8 @@ class SearchStore(SearchBackgroundUpdateStore): count_sql += " GROUP BY room_id" - count_results = yield self.execute( - "search_rooms_count", self.cursor_to_dict, count_sql, *count_args + count_results = yield self.db.execute( + "search_rooms_count", self.db.cursor_to_dict, count_sql, *count_args ) count = sum(row["count"] for row in count_results if row["room_id"] in room_ids) @@ -586,7 +588,9 @@ class SearchStore(SearchBackgroundUpdateStore): args.append(limit) - results = yield self.execute("search_rooms", self.cursor_to_dict, sql, *args) + results = yield self.db.execute( + "search_rooms", self.db.cursor_to_dict, sql, *args + ) results = list(filter(lambda row: row["room_id"] in room_ids, results)) @@ -600,8 +604,8 @@ class SearchStore(SearchBackgroundUpdateStore): count_sql += " GROUP BY room_id" - count_results = yield self.execute( - "search_rooms_count", self.cursor_to_dict, count_sql, *count_args + count_results = yield self.db.execute( + "search_rooms_count", self.db.cursor_to_dict, count_sql, *count_args ) count = sum(row["count"] for row in count_results if row["room_id"] in room_ids) @@ -686,7 +690,7 @@ class SearchStore(SearchBackgroundUpdateStore): return highlight_words - return self.runInteraction("_find_highlights", f) + return self.db.runInteraction("_find_highlights", f) def _to_postgres_options(options_dict): diff --git a/synapse/storage/data_stores/main/signatures.py b/synapse/storage/data_stores/main/signatures.py index f3da29ce14..563216b63c 100644 --- a/synapse/storage/data_stores/main/signatures.py +++ b/synapse/storage/data_stores/main/signatures.py @@ -48,7 +48,7 @@ class SignatureWorkerStore(SQLBaseStore): for event_id in event_ids } - return self.runInteraction("get_event_reference_hashes", f) + return self.db.runInteraction("get_event_reference_hashes", f) @defer.inlineCallbacks def add_event_hashes(self, event_ids): @@ -98,4 +98,4 @@ class SignatureStore(SignatureWorkerStore): } ) - self.simple_insert_many_txn(txn, table="event_reference_hashes", values=vals) + self.db.simple_insert_many_txn(txn, table="event_reference_hashes", values=vals) diff --git a/synapse/storage/data_stores/main/state.py b/synapse/storage/data_stores/main/state.py index 2b33ec1a35..851e81d6b3 100644 --- a/synapse/storage/data_stores/main/state.py +++ b/synapse/storage/data_stores/main/state.py @@ -89,7 +89,7 @@ class StateGroupBackgroundUpdateStore(SQLBaseStore): count = 0 while next_group: - next_group = self.simple_select_one_onecol_txn( + next_group = self.db.simple_select_one_onecol_txn( txn, table="state_group_edges", keyvalues={"state_group": next_group}, @@ -192,7 +192,7 @@ class StateGroupBackgroundUpdateStore(SQLBaseStore): ): break - next_group = self.simple_select_one_onecol_txn( + next_group = self.db.simple_select_one_onecol_txn( txn, table="state_group_edges", keyvalues={"state_group": next_group}, @@ -348,7 +348,9 @@ class StateGroupWorkerStore( (intern_string(r[0]), intern_string(r[1])): to_ascii(r[2]) for r in txn } - return self.runInteraction("get_current_state_ids", _get_current_state_ids_txn) + return self.db.runInteraction( + "get_current_state_ids", _get_current_state_ids_txn + ) # FIXME: how should this be cached? def get_filtered_current_state_ids(self, room_id, state_filter=StateFilter.all()): @@ -392,7 +394,7 @@ class StateGroupWorkerStore( return results - return self.runInteraction( + return self.db.runInteraction( "get_filtered_current_state_ids", _get_filtered_current_state_ids_txn ) @@ -431,7 +433,7 @@ class StateGroupWorkerStore( """ def _get_state_group_delta_txn(txn): - prev_group = self.simple_select_one_onecol_txn( + prev_group = self.db.simple_select_one_onecol_txn( txn, table="state_group_edges", keyvalues={"state_group": state_group}, @@ -442,7 +444,7 @@ class StateGroupWorkerStore( if not prev_group: return _GetStateGroupDelta(None, None) - delta_ids = self.simple_select_list_txn( + delta_ids = self.db.simple_select_list_txn( txn, table="state_groups_state", keyvalues={"state_group": state_group}, @@ -454,7 +456,9 @@ class StateGroupWorkerStore( {(row["type"], row["state_key"]): row["event_id"] for row in delta_ids}, ) - return self.runInteraction("get_state_group_delta", _get_state_group_delta_txn) + return self.db.runInteraction( + "get_state_group_delta", _get_state_group_delta_txn + ) @defer.inlineCallbacks def get_state_groups_ids(self, _room_id, event_ids): @@ -540,7 +544,7 @@ class StateGroupWorkerStore( chunks = [groups[i : i + 100] for i in range(0, len(groups), 100)] for chunk in chunks: - res = yield self.runInteraction( + res = yield self.db.runInteraction( "_get_state_groups_from_groups", self._get_state_groups_from_groups_txn, chunk, @@ -644,7 +648,7 @@ class StateGroupWorkerStore( @cached(max_entries=50000) def _get_state_group_for_event(self, event_id): - return self.simple_select_one_onecol( + return self.db.simple_select_one_onecol( table="event_to_state_groups", keyvalues={"event_id": event_id}, retcol="state_group", @@ -661,7 +665,7 @@ class StateGroupWorkerStore( def _get_state_group_for_events(self, event_ids): """Returns mapping event_id -> state_group """ - rows = yield self.simple_select_many_batch( + rows = yield self.db.simple_select_many_batch( table="event_to_state_groups", column="event_id", iterable=event_ids, @@ -902,7 +906,7 @@ class StateGroupWorkerStore( state_group = self.database_engine.get_next_state_group_id(txn) - self.simple_insert_txn( + self.db.simple_insert_txn( txn, table="state_groups", values={"id": state_group, "room_id": room_id, "event_id": event_id}, @@ -911,7 +915,7 @@ class StateGroupWorkerStore( # We persist as a delta if we can, while also ensuring the chain # of deltas isn't tooo long, as otherwise read performance degrades. if prev_group: - is_in_db = self.simple_select_one_onecol_txn( + is_in_db = self.db.simple_select_one_onecol_txn( txn, table="state_groups", keyvalues={"id": prev_group}, @@ -926,13 +930,13 @@ class StateGroupWorkerStore( potential_hops = self._count_state_group_hops_txn(txn, prev_group) if prev_group and potential_hops < MAX_STATE_DELTA_HOPS: - self.simple_insert_txn( + self.db.simple_insert_txn( txn, table="state_group_edges", values={"state_group": state_group, "prev_state_group": prev_group}, ) - self.simple_insert_many_txn( + self.db.simple_insert_many_txn( txn, table="state_groups_state", values=[ @@ -947,7 +951,7 @@ class StateGroupWorkerStore( ], ) else: - self.simple_insert_many_txn( + self.db.simple_insert_many_txn( txn, table="state_groups_state", values=[ @@ -993,7 +997,7 @@ class StateGroupWorkerStore( return state_group - return self.runInteraction("store_state_group", _store_state_group_txn) + return self.db.runInteraction("store_state_group", _store_state_group_txn) @defer.inlineCallbacks def get_referenced_state_groups(self, state_groups): @@ -1007,7 +1011,7 @@ class StateGroupWorkerStore( referenced. """ - rows = yield self.simple_select_many_batch( + rows = yield self.db.simple_select_many_batch( table="event_to_state_groups", column="state_group", iterable=state_groups, @@ -1065,7 +1069,7 @@ class StateBackgroundUpdateStore( batch_size = max(1, int(batch_size / BATCH_SIZE_SCALE_FACTOR)) if max_group is None: - rows = yield self.execute( + rows = yield self.db.execute( "_background_deduplicate_state", None, "SELECT coalesce(max(id), 0) FROM state_groups", @@ -1135,13 +1139,13 @@ class StateBackgroundUpdateStore( if prev_state.get(key, None) != value } - self.simple_delete_txn( + self.db.simple_delete_txn( txn, table="state_group_edges", keyvalues={"state_group": state_group}, ) - self.simple_insert_txn( + self.db.simple_insert_txn( txn, table="state_group_edges", values={ @@ -1150,13 +1154,13 @@ class StateBackgroundUpdateStore( }, ) - self.simple_delete_txn( + self.db.simple_delete_txn( txn, table="state_groups_state", keyvalues={"state_group": state_group}, ) - self.simple_insert_many_txn( + self.db.simple_insert_many_txn( txn, table="state_groups_state", values=[ @@ -1183,7 +1187,7 @@ class StateBackgroundUpdateStore( return False, batch_size - finished, result = yield self.runInteraction( + finished, result = yield self.db.runInteraction( self.STATE_GROUP_DEDUPLICATION_UPDATE_NAME, reindex_txn ) @@ -1218,7 +1222,7 @@ class StateBackgroundUpdateStore( ) txn.execute("DROP INDEX IF EXISTS state_groups_state_id") - yield self.runWithConnection(reindex_txn) + yield self.db.runWithConnection(reindex_txn) yield self._end_background_update(self.STATE_GROUP_INDEX_UPDATE_NAME) @@ -1263,7 +1267,7 @@ class StateStore(StateGroupWorkerStore, StateBackgroundUpdateStore): state_groups[event.event_id] = context.state_group - self.simple_insert_many_txn( + self.db.simple_insert_many_txn( txn, table="event_to_state_groups", values=[ diff --git a/synapse/storage/data_stores/main/state_deltas.py b/synapse/storage/data_stores/main/state_deltas.py index 03b908026b..12c982cb26 100644 --- a/synapse/storage/data_stores/main/state_deltas.py +++ b/synapse/storage/data_stores/main/state_deltas.py @@ -98,14 +98,14 @@ class StateDeltasStore(SQLBaseStore): ORDER BY stream_id ASC """ txn.execute(sql, (prev_stream_id, clipped_stream_id)) - return clipped_stream_id, self.cursor_to_dict(txn) + return clipped_stream_id, self.db.cursor_to_dict(txn) - return self.runInteraction( + return self.db.runInteraction( "get_current_state_deltas", get_current_state_deltas_txn ) def _get_max_stream_id_in_current_state_deltas_txn(self, txn): - return self.simple_select_one_onecol_txn( + return self.db.simple_select_one_onecol_txn( txn, table="current_state_delta_stream", keyvalues={}, @@ -113,7 +113,7 @@ class StateDeltasStore(SQLBaseStore): ) def get_max_stream_id_in_current_state_deltas(self): - return self.runInteraction( + return self.db.runInteraction( "get_max_stream_id_in_current_state_deltas", self._get_max_stream_id_in_current_state_deltas_txn, ) diff --git a/synapse/storage/data_stores/main/stats.py b/synapse/storage/data_stores/main/stats.py index 3aeba859fd..974ffc15bd 100644 --- a/synapse/storage/data_stores/main/stats.py +++ b/synapse/storage/data_stores/main/stats.py @@ -117,7 +117,7 @@ class StatsStore(StateDeltasStore): txn.execute(sql, (last_user_id, batch_size)) return [r for r, in txn] - users_to_work_on = yield self.runInteraction( + users_to_work_on = yield self.db.runInteraction( "_populate_stats_process_users", _get_next_batch ) @@ -130,7 +130,7 @@ class StatsStore(StateDeltasStore): yield self._calculate_and_set_initial_state_for_user(user_id) progress["last_user_id"] = user_id - yield self.runInteraction( + yield self.db.runInteraction( "populate_stats_process_users", self._background_update_progress_txn, "populate_stats_process_users", @@ -160,7 +160,7 @@ class StatsStore(StateDeltasStore): txn.execute(sql, (last_room_id, batch_size)) return [r for r, in txn] - rooms_to_work_on = yield self.runInteraction( + rooms_to_work_on = yield self.db.runInteraction( "populate_stats_rooms_get_batch", _get_next_batch ) @@ -173,7 +173,7 @@ class StatsStore(StateDeltasStore): yield self._calculate_and_set_initial_state_for_room(room_id) progress["last_room_id"] = room_id - yield self.runInteraction( + yield self.db.runInteraction( "_populate_stats_process_rooms", self._background_update_progress_txn, "populate_stats_process_rooms", @@ -186,7 +186,7 @@ class StatsStore(StateDeltasStore): """ Returns the stats processor positions. """ - return self.simple_select_one_onecol( + return self.db.simple_select_one_onecol( table="stats_incremental_position", keyvalues={}, retcol="stream_id", @@ -215,7 +215,7 @@ class StatsStore(StateDeltasStore): if field and "\0" in field: fields[col] = None - return self.simple_upsert( + return self.db.simple_upsert( table="room_stats_state", keyvalues={"room_id": room_id}, values=fields, @@ -236,7 +236,7 @@ class StatsStore(StateDeltasStore): Deferred[list[dict]], where the dict has the keys of ABSOLUTE_STATS_FIELDS[stats_type], and "bucket_size" and "end_ts". """ - return self.runInteraction( + return self.db.runInteraction( "get_statistics_for_subject", self._get_statistics_for_subject_txn, stats_type, @@ -257,7 +257,7 @@ class StatsStore(StateDeltasStore): ABSOLUTE_STATS_FIELDS[stats_type] + PER_SLICE_FIELDS[stats_type] ) - slice_list = self.simple_select_list_paginate_txn( + slice_list = self.db.simple_select_list_paginate_txn( txn, table + "_historical", {id_col: stats_id}, @@ -282,7 +282,7 @@ class StatsStore(StateDeltasStore): "name", "topic", "canonical_alias", "avatar", "join_rules", "history_visibility" """ - return self.simple_select_one( + return self.db.simple_select_one( "room_stats_state", {"room_id": room_id}, retcols=( @@ -308,7 +308,7 @@ class StatsStore(StateDeltasStore): """ table, id_col = TYPE_TO_TABLE[stats_type] - return self.simple_select_one_onecol( + return self.db.simple_select_one_onecol( "%s_current" % (table,), keyvalues={id_col: id}, retcol="completed_delta_stream_id", @@ -344,14 +344,14 @@ class StatsStore(StateDeltasStore): complete_with_stream_id=stream_id, ) - self.simple_update_one_txn( + self.db.simple_update_one_txn( txn, table="stats_incremental_position", keyvalues={}, updatevalues={"stream_id": stream_id}, ) - return self.runInteraction( + return self.db.runInteraction( "bulk_update_stats_delta", _bulk_update_stats_delta_txn ) @@ -382,7 +382,7 @@ class StatsStore(StateDeltasStore): Does not work with per-slice fields. """ - return self.runInteraction( + return self.db.runInteraction( "update_stats_delta", self._update_stats_delta_txn, ts, @@ -517,17 +517,17 @@ class StatsStore(StateDeltasStore): else: self.database_engine.lock_table(txn, table) retcols = list(chain(absolutes.keys(), additive_relatives.keys())) - current_row = self.simple_select_one_txn( + current_row = self.db.simple_select_one_txn( txn, table, keyvalues, retcols, allow_none=True ) if current_row is None: merged_dict = {**keyvalues, **absolutes, **additive_relatives} - self.simple_insert_txn(txn, table, merged_dict) + self.db.simple_insert_txn(txn, table, merged_dict) else: for (key, val) in additive_relatives.items(): current_row[key] += val current_row.update(absolutes) - self.simple_update_one_txn(txn, table, keyvalues, current_row) + self.db.simple_update_one_txn(txn, table, keyvalues, current_row) def _upsert_copy_from_table_with_additive_relatives_txn( self, @@ -614,11 +614,11 @@ class StatsStore(StateDeltasStore): txn.execute(sql, qargs) else: self.database_engine.lock_table(txn, into_table) - src_row = self.simple_select_one_txn( + src_row = self.db.simple_select_one_txn( txn, src_table, keyvalues, copy_columns ) all_dest_keyvalues = {**keyvalues, **extra_dst_keyvalues} - dest_current_row = self.simple_select_one_txn( + dest_current_row = self.db.simple_select_one_txn( txn, into_table, keyvalues=all_dest_keyvalues, @@ -634,11 +634,11 @@ class StatsStore(StateDeltasStore): **src_row, **additive_relatives, } - self.simple_insert_txn(txn, into_table, merged_dict) + self.db.simple_insert_txn(txn, into_table, merged_dict) else: for (key, val) in additive_relatives.items(): src_row[key] = dest_current_row[key] + val - self.simple_update_txn(txn, into_table, all_dest_keyvalues, src_row) + self.db.simple_update_txn(txn, into_table, all_dest_keyvalues, src_row) def get_changes_room_total_events_and_bytes(self, min_pos, max_pos): """Fetches the counts of events in the given range of stream IDs. @@ -652,7 +652,7 @@ class StatsStore(StateDeltasStore): changes. """ - return self.runInteraction( + return self.db.runInteraction( "stats_incremental_total_events_and_bytes", self.get_changes_room_total_events_and_bytes_txn, min_pos, @@ -735,7 +735,7 @@ class StatsStore(StateDeltasStore): def _fetch_current_state_stats(txn): pos = self.get_room_max_stream_ordering() - rows = self.simple_select_many_txn( + rows = self.db.simple_select_many_txn( txn, table="current_state_events", column="type", @@ -791,7 +791,7 @@ class StatsStore(StateDeltasStore): current_state_events_count, users_in_room, pos, - ) = yield self.runInteraction( + ) = yield self.db.runInteraction( "get_initial_state_for_room", _fetch_current_state_stats ) @@ -866,7 +866,7 @@ class StatsStore(StateDeltasStore): (count,) = txn.fetchone() return count, pos - joined_rooms, pos = yield self.runInteraction( + joined_rooms, pos = yield self.db.runInteraction( "calculate_and_set_initial_state_for_user", _calculate_and_set_initial_state_for_user_txn, ) diff --git a/synapse/storage/data_stores/main/stream.py b/synapse/storage/data_stores/main/stream.py index 60487c4559..2ff8c57109 100644 --- a/synapse/storage/data_stores/main/stream.py +++ b/synapse/storage/data_stores/main/stream.py @@ -255,7 +255,7 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): super(StreamWorkerStore, self).__init__(db_conn, hs) events_max = self.get_room_max_stream_ordering() - event_cache_prefill, min_event_val = self.get_cache_dict( + event_cache_prefill, min_event_val = self.db.get_cache_dict( db_conn, "events", entity_column="room_id", @@ -400,7 +400,7 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): rows = [_EventDictReturn(row[0], None, row[1]) for row in txn] return rows - rows = yield self.runInteraction("get_room_events_stream_for_room", f) + rows = yield self.db.runInteraction("get_room_events_stream_for_room", f) ret = yield self.get_events_as_list( [r.event_id for r in rows], get_prev_content=True @@ -450,7 +450,7 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): return rows - rows = yield self.runInteraction("get_membership_changes_for_user", f) + rows = yield self.db.runInteraction("get_membership_changes_for_user", f) ret = yield self.get_events_as_list( [r.event_id for r in rows], get_prev_content=True @@ -511,7 +511,7 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): end_token = RoomStreamToken.parse(end_token) - rows, token = yield self.runInteraction( + rows, token = yield self.db.runInteraction( "get_recent_event_ids_for_room", self._paginate_room_events_txn, room_id, @@ -548,7 +548,7 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): txn.execute(sql, (room_id, stream_ordering)) return txn.fetchone() - return self.runInteraction("get_room_event_after_stream_ordering", _f) + return self.db.runInteraction("get_room_event_after_stream_ordering", _f) @defer.inlineCallbacks def get_room_events_max_id(self, room_id=None): @@ -562,7 +562,7 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): if room_id is None: return "s%d" % (token,) else: - topo = yield self.runInteraction( + topo = yield self.db.runInteraction( "_get_max_topological_txn", self._get_max_topological_txn, room_id ) return "t%d-%d" % (topo, token) @@ -576,7 +576,7 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): Returns: A deferred "s%d" stream token. """ - return self.simple_select_one_onecol( + return self.db.simple_select_one_onecol( table="events", keyvalues={"event_id": event_id}, retcol="stream_ordering" ).addCallback(lambda row: "s%d" % (row,)) @@ -589,7 +589,7 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): Returns: A deferred "t%d-%d" topological token. """ - return self.simple_select_one( + return self.db.simple_select_one( table="events", keyvalues={"event_id": event_id}, retcols=("stream_ordering", "topological_ordering"), @@ -613,7 +613,7 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): "SELECT coalesce(max(topological_ordering), 0) FROM events" " WHERE room_id = ? AND stream_ordering < ?" ) - return self.execute( + return self.db.execute( "get_max_topological_token", None, sql, room_id, stream_key ).addCallback(lambda r: r[0][0] if r else 0) @@ -667,7 +667,7 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): dict """ - results = yield self.runInteraction( + results = yield self.db.runInteraction( "get_events_around", self._get_events_around_txn, room_id, @@ -709,7 +709,7 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): dict """ - results = self.simple_select_one_txn( + results = self.db.simple_select_one_txn( txn, "events", keyvalues={"event_id": event_id, "room_id": room_id}, @@ -788,7 +788,7 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): return upper_bound, [row[1] for row in rows] - upper_bound, event_ids = yield self.runInteraction( + upper_bound, event_ids = yield self.db.runInteraction( "get_all_new_events_stream", get_all_new_events_stream_txn ) @@ -797,7 +797,7 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): return upper_bound, events def get_federation_out_pos(self, typ): - return self.simple_select_one_onecol( + return self.db.simple_select_one_onecol( table="federation_stream_position", retcol="stream_id", keyvalues={"type": typ}, @@ -805,7 +805,7 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): ) def update_federation_out_pos(self, typ, stream_id): - return self.simple_update_one( + return self.db.simple_update_one( table="federation_stream_position", keyvalues={"type": typ}, updatevalues={"stream_id": stream_id}, @@ -956,7 +956,7 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): if to_key: to_key = RoomStreamToken.parse(to_key) - rows, token = yield self.runInteraction( + rows, token = yield self.db.runInteraction( "paginate_room_events", self._paginate_room_events_txn, room_id, diff --git a/synapse/storage/data_stores/main/tags.py b/synapse/storage/data_stores/main/tags.py index 85012403be..2aa1bafd48 100644 --- a/synapse/storage/data_stores/main/tags.py +++ b/synapse/storage/data_stores/main/tags.py @@ -41,7 +41,7 @@ class TagsWorkerStore(AccountDataWorkerStore): tag strings to tag content. """ - deferred = self.simple_select_list( + deferred = self.db.simple_select_list( "room_tags", {"user_id": user_id}, ["room_id", "tag", "content"] ) @@ -78,7 +78,7 @@ class TagsWorkerStore(AccountDataWorkerStore): txn.execute(sql, (last_id, current_id, limit)) return txn.fetchall() - tag_ids = yield self.runInteraction( + tag_ids = yield self.db.runInteraction( "get_all_updated_tags", get_all_updated_tags_txn ) @@ -98,7 +98,7 @@ class TagsWorkerStore(AccountDataWorkerStore): batch_size = 50 results = [] for i in range(0, len(tag_ids), batch_size): - tags = yield self.runInteraction( + tags = yield self.db.runInteraction( "get_all_updated_tag_content", get_tag_content, tag_ids[i : i + batch_size], @@ -135,7 +135,9 @@ class TagsWorkerStore(AccountDataWorkerStore): if not changed: return {} - room_ids = yield self.runInteraction("get_updated_tags", get_updated_tags_txn) + room_ids = yield self.db.runInteraction( + "get_updated_tags", get_updated_tags_txn + ) results = {} if room_ids: @@ -153,7 +155,7 @@ class TagsWorkerStore(AccountDataWorkerStore): Returns: A deferred list of string tags. """ - return self.simple_select_list( + return self.db.simple_select_list( table="room_tags", keyvalues={"user_id": user_id, "room_id": room_id}, retcols=("tag", "content"), @@ -178,7 +180,7 @@ class TagsStore(TagsWorkerStore): content_json = json.dumps(content) def add_tag_txn(txn, next_id): - self.simple_upsert_txn( + self.db.simple_upsert_txn( txn, table="room_tags", keyvalues={"user_id": user_id, "room_id": room_id, "tag": tag}, @@ -187,7 +189,7 @@ class TagsStore(TagsWorkerStore): self._update_revision_txn(txn, user_id, room_id, next_id) with self._account_data_id_gen.get_next() as next_id: - yield self.runInteraction("add_tag", add_tag_txn, next_id) + yield self.db.runInteraction("add_tag", add_tag_txn, next_id) self.get_tags_for_user.invalidate((user_id,)) @@ -210,7 +212,7 @@ class TagsStore(TagsWorkerStore): self._update_revision_txn(txn, user_id, room_id, next_id) with self._account_data_id_gen.get_next() as next_id: - yield self.runInteraction("remove_tag", remove_tag_txn, next_id) + yield self.db.runInteraction("remove_tag", remove_tag_txn, next_id) self.get_tags_for_user.invalidate((user_id,)) diff --git a/synapse/storage/data_stores/main/transactions.py b/synapse/storage/data_stores/main/transactions.py index c162f3ea16..c0d155a43c 100644 --- a/synapse/storage/data_stores/main/transactions.py +++ b/synapse/storage/data_stores/main/transactions.py @@ -77,7 +77,7 @@ class TransactionStore(SQLBaseStore): this transaction or a 2-tuple of (int, dict) """ - return self.runInteraction( + return self.db.runInteraction( "get_received_txn_response", self._get_received_txn_response, transaction_id, @@ -85,7 +85,7 @@ class TransactionStore(SQLBaseStore): ) def _get_received_txn_response(self, txn, transaction_id, origin): - result = self.simple_select_one_txn( + result = self.db.simple_select_one_txn( txn, table="received_transactions", keyvalues={"transaction_id": transaction_id, "origin": origin}, @@ -119,7 +119,7 @@ class TransactionStore(SQLBaseStore): response_json (str) """ - return self.simple_insert( + return self.db.simple_insert( table="received_transactions", values={ "transaction_id": transaction_id, @@ -148,7 +148,7 @@ class TransactionStore(SQLBaseStore): if result is not SENTINEL: return result - result = yield self.runInteraction( + result = yield self.db.runInteraction( "get_destination_retry_timings", self._get_destination_retry_timings, destination, @@ -160,7 +160,7 @@ class TransactionStore(SQLBaseStore): return result def _get_destination_retry_timings(self, txn, destination): - result = self.simple_select_one_txn( + result = self.db.simple_select_one_txn( txn, table="destinations", keyvalues={"destination": destination}, @@ -187,7 +187,7 @@ class TransactionStore(SQLBaseStore): """ self._destination_retry_cache.pop(destination, None) - return self.runInteraction( + return self.db.runInteraction( "set_destination_retry_timings", self._set_destination_retry_timings, destination, @@ -227,7 +227,7 @@ class TransactionStore(SQLBaseStore): # We need to be careful here as the data may have changed from under us # due to a worker setting the timings. - prev_row = self.simple_select_one_txn( + prev_row = self.db.simple_select_one_txn( txn, table="destinations", keyvalues={"destination": destination}, @@ -236,7 +236,7 @@ class TransactionStore(SQLBaseStore): ) if not prev_row: - self.simple_insert_txn( + self.db.simple_insert_txn( txn, table="destinations", values={ @@ -247,7 +247,7 @@ class TransactionStore(SQLBaseStore): }, ) elif retry_interval == 0 or prev_row["retry_interval"] < retry_interval: - self.simple_update_one_txn( + self.db.simple_update_one_txn( txn, "destinations", keyvalues={"destination": destination}, @@ -270,4 +270,6 @@ class TransactionStore(SQLBaseStore): def _cleanup_transactions_txn(txn): txn.execute("DELETE FROM received_transactions WHERE ts < ?", (month_ago,)) - return self.runInteraction("_cleanup_transactions", _cleanup_transactions_txn) + return self.db.runInteraction( + "_cleanup_transactions", _cleanup_transactions_txn + ) diff --git a/synapse/storage/data_stores/main/user_directory.py b/synapse/storage/data_stores/main/user_directory.py index 1a85aabbfb..7118bd62f3 100644 --- a/synapse/storage/data_stores/main/user_directory.py +++ b/synapse/storage/data_stores/main/user_directory.py @@ -85,7 +85,7 @@ class UserDirectoryBackgroundUpdateStore(StateDeltasStore, BackgroundUpdateStore """ txn.execute(sql) rooms = [{"room_id": x[0], "events": x[1]} for x in txn.fetchall()] - self.simple_insert_many_txn(txn, TEMP_TABLE + "_rooms", rooms) + self.db.simple_insert_many_txn(txn, TEMP_TABLE + "_rooms", rooms) del rooms # If search all users is on, get all the users we want to add. @@ -100,13 +100,13 @@ class UserDirectoryBackgroundUpdateStore(StateDeltasStore, BackgroundUpdateStore txn.execute("SELECT name FROM users") users = [{"user_id": x[0]} for x in txn.fetchall()] - self.simple_insert_many_txn(txn, TEMP_TABLE + "_users", users) + self.db.simple_insert_many_txn(txn, TEMP_TABLE + "_users", users) new_pos = yield self.get_max_stream_id_in_current_state_deltas() - yield self.runInteraction( + yield self.db.runInteraction( "populate_user_directory_temp_build", _make_staging_area ) - yield self.simple_insert(TEMP_TABLE + "_position", {"position": new_pos}) + yield self.db.simple_insert(TEMP_TABLE + "_position", {"position": new_pos}) yield self._end_background_update("populate_user_directory_createtables") return 1 @@ -116,7 +116,7 @@ class UserDirectoryBackgroundUpdateStore(StateDeltasStore, BackgroundUpdateStore """ Update the user directory stream position, then clean up the old tables. """ - position = yield self.simple_select_one_onecol( + position = yield self.db.simple_select_one_onecol( TEMP_TABLE + "_position", None, "position" ) yield self.update_user_directory_stream_pos(position) @@ -126,7 +126,7 @@ class UserDirectoryBackgroundUpdateStore(StateDeltasStore, BackgroundUpdateStore txn.execute("DROP TABLE IF EXISTS " + TEMP_TABLE + "_users") txn.execute("DROP TABLE IF EXISTS " + TEMP_TABLE + "_position") - yield self.runInteraction( + yield self.db.runInteraction( "populate_user_directory_cleanup", _delete_staging_area ) @@ -170,7 +170,7 @@ class UserDirectoryBackgroundUpdateStore(StateDeltasStore, BackgroundUpdateStore return rooms_to_work_on - rooms_to_work_on = yield self.runInteraction( + rooms_to_work_on = yield self.db.runInteraction( "populate_user_directory_temp_read", _get_next_batch ) @@ -243,10 +243,10 @@ class UserDirectoryBackgroundUpdateStore(StateDeltasStore, BackgroundUpdateStore to_insert.clear() # We've finished a room. Delete it from the table. - yield self.simple_delete_one(TEMP_TABLE + "_rooms", {"room_id": room_id}) + yield self.db.simple_delete_one(TEMP_TABLE + "_rooms", {"room_id": room_id}) # Update the remaining counter. progress["remaining"] -= 1 - yield self.runInteraction( + yield self.db.runInteraction( "populate_user_directory", self._background_update_progress_txn, "populate_user_directory_process_rooms", @@ -291,7 +291,7 @@ class UserDirectoryBackgroundUpdateStore(StateDeltasStore, BackgroundUpdateStore return users_to_work_on - users_to_work_on = yield self.runInteraction( + users_to_work_on = yield self.db.runInteraction( "populate_user_directory_temp_read", _get_next_batch ) @@ -312,10 +312,10 @@ class UserDirectoryBackgroundUpdateStore(StateDeltasStore, BackgroundUpdateStore ) # We've finished processing a user. Delete it from the table. - yield self.simple_delete_one(TEMP_TABLE + "_users", {"user_id": user_id}) + yield self.db.simple_delete_one(TEMP_TABLE + "_users", {"user_id": user_id}) # Update the remaining counter. progress["remaining"] -= 1 - yield self.runInteraction( + yield self.db.runInteraction( "populate_user_directory", self._background_update_progress_txn, "populate_user_directory_process_users", @@ -361,7 +361,7 @@ class UserDirectoryBackgroundUpdateStore(StateDeltasStore, BackgroundUpdateStore """ def _update_profile_in_user_dir_txn(txn): - new_entry = self.simple_upsert_txn( + new_entry = self.db.simple_upsert_txn( txn, table="user_directory", keyvalues={"user_id": user_id}, @@ -435,7 +435,7 @@ class UserDirectoryBackgroundUpdateStore(StateDeltasStore, BackgroundUpdateStore ) elif isinstance(self.database_engine, Sqlite3Engine): value = "%s %s" % (user_id, display_name) if display_name else user_id - self.simple_upsert_txn( + self.db.simple_upsert_txn( txn, table="user_directory_search", keyvalues={"user_id": user_id}, @@ -448,7 +448,7 @@ class UserDirectoryBackgroundUpdateStore(StateDeltasStore, BackgroundUpdateStore txn.call_after(self.get_user_in_directory.invalidate, (user_id,)) - return self.runInteraction( + return self.db.runInteraction( "update_profile_in_user_dir", _update_profile_in_user_dir_txn ) @@ -462,7 +462,7 @@ class UserDirectoryBackgroundUpdateStore(StateDeltasStore, BackgroundUpdateStore """ def _add_users_who_share_room_txn(txn): - self.simple_upsert_many_txn( + self.db.simple_upsert_many_txn( txn, table="users_who_share_private_rooms", key_names=["user_id", "other_user_id", "room_id"], @@ -474,7 +474,7 @@ class UserDirectoryBackgroundUpdateStore(StateDeltasStore, BackgroundUpdateStore value_values=None, ) - return self.runInteraction( + return self.db.runInteraction( "add_users_who_share_room", _add_users_who_share_room_txn ) @@ -489,7 +489,7 @@ class UserDirectoryBackgroundUpdateStore(StateDeltasStore, BackgroundUpdateStore def _add_users_in_public_rooms_txn(txn): - self.simple_upsert_many_txn( + self.db.simple_upsert_many_txn( txn, table="users_in_public_rooms", key_names=["user_id", "room_id"], @@ -498,7 +498,7 @@ class UserDirectoryBackgroundUpdateStore(StateDeltasStore, BackgroundUpdateStore value_values=None, ) - return self.runInteraction( + return self.db.runInteraction( "add_users_in_public_rooms", _add_users_in_public_rooms_txn ) @@ -513,13 +513,13 @@ class UserDirectoryBackgroundUpdateStore(StateDeltasStore, BackgroundUpdateStore txn.execute("DELETE FROM users_who_share_private_rooms") txn.call_after(self.get_user_in_directory.invalidate_all) - return self.runInteraction( + return self.db.runInteraction( "delete_all_from_user_dir", _delete_all_from_user_dir_txn ) @cached() def get_user_in_directory(self, user_id): - return self.simple_select_one( + return self.db.simple_select_one( table="user_directory", keyvalues={"user_id": user_id}, retcols=("display_name", "avatar_url"), @@ -528,7 +528,7 @@ class UserDirectoryBackgroundUpdateStore(StateDeltasStore, BackgroundUpdateStore ) def update_user_directory_stream_pos(self, stream_id): - return self.simple_update_one( + return self.db.simple_update_one( table="user_directory_stream_pos", keyvalues={}, updatevalues={"stream_id": stream_id}, @@ -547,42 +547,42 @@ class UserDirectoryStore(UserDirectoryBackgroundUpdateStore): def remove_from_user_dir(self, user_id): def _remove_from_user_dir_txn(txn): - self.simple_delete_txn( + self.db.simple_delete_txn( txn, table="user_directory", keyvalues={"user_id": user_id} ) - self.simple_delete_txn( + self.db.simple_delete_txn( txn, table="user_directory_search", keyvalues={"user_id": user_id} ) - self.simple_delete_txn( + self.db.simple_delete_txn( txn, table="users_in_public_rooms", keyvalues={"user_id": user_id} ) - self.simple_delete_txn( + self.db.simple_delete_txn( txn, table="users_who_share_private_rooms", keyvalues={"user_id": user_id}, ) - self.simple_delete_txn( + self.db.simple_delete_txn( txn, table="users_who_share_private_rooms", keyvalues={"other_user_id": user_id}, ) txn.call_after(self.get_user_in_directory.invalidate, (user_id,)) - return self.runInteraction("remove_from_user_dir", _remove_from_user_dir_txn) + return self.db.runInteraction("remove_from_user_dir", _remove_from_user_dir_txn) @defer.inlineCallbacks def get_users_in_dir_due_to_room(self, room_id): """Get all user_ids that are in the room directory because they're in the given room_id """ - user_ids_share_pub = yield self.simple_select_onecol( + user_ids_share_pub = yield self.db.simple_select_onecol( table="users_in_public_rooms", keyvalues={"room_id": room_id}, retcol="user_id", desc="get_users_in_dir_due_to_room", ) - user_ids_share_priv = yield self.simple_select_onecol( + user_ids_share_priv = yield self.db.simple_select_onecol( table="users_who_share_private_rooms", keyvalues={"room_id": room_id}, retcol="other_user_id", @@ -605,23 +605,23 @@ class UserDirectoryStore(UserDirectoryBackgroundUpdateStore): """ def _remove_user_who_share_room_txn(txn): - self.simple_delete_txn( + self.db.simple_delete_txn( txn, table="users_who_share_private_rooms", keyvalues={"user_id": user_id, "room_id": room_id}, ) - self.simple_delete_txn( + self.db.simple_delete_txn( txn, table="users_who_share_private_rooms", keyvalues={"other_user_id": user_id, "room_id": room_id}, ) - self.simple_delete_txn( + self.db.simple_delete_txn( txn, table="users_in_public_rooms", keyvalues={"user_id": user_id, "room_id": room_id}, ) - return self.runInteraction( + return self.db.runInteraction( "remove_user_who_share_room", _remove_user_who_share_room_txn ) @@ -636,14 +636,14 @@ class UserDirectoryStore(UserDirectoryBackgroundUpdateStore): Returns: list: user_id """ - rows = yield self.simple_select_onecol( + rows = yield self.db.simple_select_onecol( table="users_who_share_private_rooms", keyvalues={"user_id": user_id}, retcol="room_id", desc="get_rooms_user_is_in", ) - pub_rows = yield self.simple_select_onecol( + pub_rows = yield self.db.simple_select_onecol( table="users_in_public_rooms", keyvalues={"user_id": user_id}, retcol="room_id", @@ -674,14 +674,14 @@ class UserDirectoryStore(UserDirectoryBackgroundUpdateStore): ) f2 USING (room_id) """ - rows = yield self.execute( + rows = yield self.db.execute( "get_rooms_in_common_for_users", None, sql, user_id, other_user_id ) return [room_id for room_id, in rows] def get_user_directory_stream_pos(self): - return self.simple_select_one_onecol( + return self.db.simple_select_one_onecol( table="user_directory_stream_pos", keyvalues={}, retcol="stream_id", @@ -786,7 +786,9 @@ class UserDirectoryStore(UserDirectoryBackgroundUpdateStore): # This should be unreachable. raise Exception("Unrecognized database engine") - results = yield self.execute("search_user_dir", self.cursor_to_dict, sql, *args) + results = yield self.db.execute( + "search_user_dir", self.db.cursor_to_dict, sql, *args + ) limited = len(results) > limit diff --git a/synapse/storage/data_stores/main/user_erasure_store.py b/synapse/storage/data_stores/main/user_erasure_store.py index 37860af070..af8025bc17 100644 --- a/synapse/storage/data_stores/main/user_erasure_store.py +++ b/synapse/storage/data_stores/main/user_erasure_store.py @@ -31,7 +31,7 @@ class UserErasureWorkerStore(SQLBaseStore): Returns: Deferred[bool]: True if the user has requested erasure """ - return self.simple_select_onecol( + return self.db.simple_select_onecol( table="erased_users", keyvalues={"user_id": user_id}, retcol="1", @@ -56,7 +56,7 @@ class UserErasureWorkerStore(SQLBaseStore): # iterate it multiple times, and (b) avoiding duplicates. user_ids = tuple(set(user_ids)) - rows = yield self.simple_select_many_batch( + rows = yield self.db.simple_select_many_batch( table="erased_users", column="user_id", iterable=user_ids, @@ -88,4 +88,4 @@ class UserErasureStore(UserErasureWorkerStore): self._invalidate_cache_and_stream(txn, self.is_user_erased, (user_id,)) - return self.runInteraction("mark_user_erased", f) + return self.db.runInteraction("mark_user_erased", f) diff --git a/synapse/storage/database.py b/synapse/storage/database.py new file mode 100644 index 0000000000..c2e121a001 --- /dev/null +++ b/synapse/storage/database.py @@ -0,0 +1,1485 @@ +# -*- coding: utf-8 -*- +# Copyright 2014-2016 OpenMarket Ltd +# Copyright 2017-2018 New Vector Ltd +# Copyright 2019 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging +import random +import sys +import time +from typing import Iterable, Tuple + +from six import iteritems, iterkeys, itervalues +from six.moves import intern, range + +from prometheus_client import Histogram + +from twisted.internet import defer + +from synapse.api.errors import StoreError +from synapse.logging.context import LoggingContext, make_deferred_yieldable +from synapse.metrics.background_process_metrics import run_as_background_process +from synapse.storage.engines import PostgresEngine, Sqlite3Engine +from synapse.util.stringutils import exception_to_unicode + +# import a function which will return a monotonic time, in seconds +try: + # on python 3, use time.monotonic, since time.clock can go backwards + from time import monotonic as monotonic_time +except ImportError: + # ... but python 2 doesn't have it + from time import clock as monotonic_time + +logger = logging.getLogger(__name__) + +try: + MAX_TXN_ID = sys.maxint - 1 +except AttributeError: + # python 3 does not have a maximum int value + MAX_TXN_ID = 2 ** 63 - 1 + +sql_logger = logging.getLogger("synapse.storage.SQL") +transaction_logger = logging.getLogger("synapse.storage.txn") +perf_logger = logging.getLogger("synapse.storage.TIME") + +sql_scheduling_timer = Histogram("synapse_storage_schedule_time", "sec") + +sql_query_timer = Histogram("synapse_storage_query_time", "sec", ["verb"]) +sql_txn_timer = Histogram("synapse_storage_transaction_time", "sec", ["desc"]) + + +# Unique indexes which have been added in background updates. Maps from table name +# to the name of the background update which added the unique index to that table. +# +# This is used by the upsert logic to figure out which tables are safe to do a proper +# UPSERT on: until the relevant background update has completed, we +# have to emulate an upsert by locking the table. +# +UNIQUE_INDEX_BACKGROUND_UPDATES = { + "user_ips": "user_ips_device_unique_index", + "device_lists_remote_extremeties": "device_lists_remote_extremeties_unique_idx", + "device_lists_remote_cache": "device_lists_remote_cache_unique_idx", + "event_search": "event_search_event_id_idx", +} + + +class LoggingTransaction(object): + """An object that almost-transparently proxies for the 'txn' object + passed to the constructor. Adds logging and metrics to the .execute() + method. + + Args: + txn: The database transcation object to wrap. + name (str): The name of this transactions for logging. + database_engine (Sqlite3Engine|PostgresEngine) + after_callbacks(list|None): A list that callbacks will be appended to + that have been added by `call_after` which should be run on + successful completion of the transaction. None indicates that no + callbacks should be allowed to be scheduled to run. + exception_callbacks(list|None): A list that callbacks will be appended + to that have been added by `call_on_exception` which should be run + if transaction ends with an error. None indicates that no callbacks + should be allowed to be scheduled to run. + """ + + __slots__ = [ + "txn", + "name", + "database_engine", + "after_callbacks", + "exception_callbacks", + ] + + def __init__( + self, txn, name, database_engine, after_callbacks=None, exception_callbacks=None + ): + object.__setattr__(self, "txn", txn) + object.__setattr__(self, "name", name) + object.__setattr__(self, "database_engine", database_engine) + object.__setattr__(self, "after_callbacks", after_callbacks) + object.__setattr__(self, "exception_callbacks", exception_callbacks) + + def call_after(self, callback, *args, **kwargs): + """Call the given callback on the main twisted thread after the + transaction has finished. Used to invalidate the caches on the + correct thread. + """ + self.after_callbacks.append((callback, args, kwargs)) + + def call_on_exception(self, callback, *args, **kwargs): + self.exception_callbacks.append((callback, args, kwargs)) + + def __getattr__(self, name): + return getattr(self.txn, name) + + def __setattr__(self, name, value): + setattr(self.txn, name, value) + + def __iter__(self): + return self.txn.__iter__() + + def execute_batch(self, sql, args): + if isinstance(self.database_engine, PostgresEngine): + from psycopg2.extras import execute_batch + + self._do_execute(lambda *x: execute_batch(self.txn, *x), sql, args) + else: + for val in args: + self.execute(sql, val) + + def execute(self, sql, *args): + self._do_execute(self.txn.execute, sql, *args) + + def executemany(self, sql, *args): + self._do_execute(self.txn.executemany, sql, *args) + + def _make_sql_one_line(self, sql): + "Strip newlines out of SQL so that the loggers in the DB are on one line" + return " ".join(l.strip() for l in sql.splitlines() if l.strip()) + + def _do_execute(self, func, sql, *args): + sql = self._make_sql_one_line(sql) + + # TODO(paul): Maybe use 'info' and 'debug' for values? + sql_logger.debug("[SQL] {%s} %s", self.name, sql) + + sql = self.database_engine.convert_param_style(sql) + if args: + try: + sql_logger.debug("[SQL values] {%s} %r", self.name, args[0]) + except Exception: + # Don't let logging failures stop SQL from working + pass + + start = time.time() + + try: + return func(sql, *args) + except Exception as e: + logger.debug("[SQL FAIL] {%s} %s", self.name, e) + raise + finally: + secs = time.time() - start + sql_logger.debug("[SQL time] {%s} %f sec", self.name, secs) + sql_query_timer.labels(sql.split()[0]).observe(secs) + + +class PerformanceCounters(object): + def __init__(self): + self.current_counters = {} + self.previous_counters = {} + + def update(self, key, duration_secs): + count, cum_time = self.current_counters.get(key, (0, 0)) + count += 1 + cum_time += duration_secs + self.current_counters[key] = (count, cum_time) + + def interval(self, interval_duration_secs, limit=3): + counters = [] + for name, (count, cum_time) in iteritems(self.current_counters): + prev_count, prev_time = self.previous_counters.get(name, (0, 0)) + counters.append( + ( + (cum_time - prev_time) / interval_duration_secs, + count - prev_count, + name, + ) + ) + + self.previous_counters = dict(self.current_counters) + + counters.sort(reverse=True) + + top_n_counters = ", ".join( + "%s(%d): %.3f%%" % (name, count, 100 * ratio) + for ratio, count, name in counters[:limit] + ) + + return top_n_counters + + +class Database(object): + _TXN_ID = 0 + + def __init__(self, hs): + self.hs = hs + self._clock = hs.get_clock() + self._db_pool = hs.get_db_pool() + + self._previous_txn_total_time = 0 + self._current_txn_total_time = 0 + self._previous_loop_ts = 0 + + # TODO(paul): These can eventually be removed once the metrics code + # is running in mainline, and we have some nice monitoring frontends + # to watch it + self._txn_perf_counters = PerformanceCounters() + + self.database_engine = hs.database_engine + + # A set of tables that are not safe to use native upserts in. + self._unsafe_to_upsert_tables = set(UNIQUE_INDEX_BACKGROUND_UPDATES.keys()) + + # We add the user_directory_search table to the blacklist on SQLite + # because the existing search table does not have an index, making it + # unsafe to use native upserts. + if isinstance(self.database_engine, Sqlite3Engine): + self._unsafe_to_upsert_tables.add("user_directory_search") + + if self.database_engine.can_native_upsert: + # Check ASAP (and then later, every 1s) to see if we have finished + # background updates of tables that aren't safe to update. + self._clock.call_later( + 0.0, + run_as_background_process, + "upsert_safety_check", + self._check_safe_to_upsert, + ) + + self.rand = random.SystemRandom() + + @defer.inlineCallbacks + def _check_safe_to_upsert(self): + """ + Is it safe to use native UPSERT? + + If there are background updates, we will need to wait, as they may be + the addition of indexes that set the UNIQUE constraint that we require. + + If the background updates have not completed, wait 15 sec and check again. + """ + updates = yield self.simple_select_list( + "background_updates", + keyvalues=None, + retcols=["update_name"], + desc="check_background_updates", + ) + updates = [x["update_name"] for x in updates] + + for table, update_name in UNIQUE_INDEX_BACKGROUND_UPDATES.items(): + if update_name not in updates: + logger.debug("Now safe to upsert in %s", table) + self._unsafe_to_upsert_tables.discard(table) + + # If there's any updates still running, reschedule to run. + if updates: + self._clock.call_later( + 15.0, + run_as_background_process, + "upsert_safety_check", + self._check_safe_to_upsert, + ) + + def start_profiling(self): + self._previous_loop_ts = monotonic_time() + + def loop(): + curr = self._current_txn_total_time + prev = self._previous_txn_total_time + self._previous_txn_total_time = curr + + time_now = monotonic_time() + time_then = self._previous_loop_ts + self._previous_loop_ts = time_now + + duration = time_now - time_then + ratio = (curr - prev) / duration + + top_three_counters = self._txn_perf_counters.interval(duration, limit=3) + + perf_logger.info( + "Total database time: %.3f%% {%s}", ratio * 100, top_three_counters + ) + + self._clock.looping_call(loop, 10000) + + def new_transaction( + self, conn, desc, after_callbacks, exception_callbacks, func, *args, **kwargs + ): + start = monotonic_time() + txn_id = self._TXN_ID + + # We don't really need these to be unique, so lets stop it from + # growing really large. + self._TXN_ID = (self._TXN_ID + 1) % (MAX_TXN_ID) + + name = "%s-%x" % (desc, txn_id) + + transaction_logger.debug("[TXN START] {%s}", name) + + try: + i = 0 + N = 5 + while True: + cursor = LoggingTransaction( + conn.cursor(), + name, + self.database_engine, + after_callbacks, + exception_callbacks, + ) + try: + r = func(cursor, *args, **kwargs) + conn.commit() + return r + except self.database_engine.module.OperationalError as e: + # This can happen if the database disappears mid + # transaction. + logger.warning( + "[TXN OPERROR] {%s} %s %d/%d", + name, + exception_to_unicode(e), + i, + N, + ) + if i < N: + i += 1 + try: + conn.rollback() + except self.database_engine.module.Error as e1: + logger.warning( + "[TXN EROLL] {%s} %s", name, exception_to_unicode(e1) + ) + continue + raise + except self.database_engine.module.DatabaseError as e: + if self.database_engine.is_deadlock(e): + logger.warning("[TXN DEADLOCK] {%s} %d/%d", name, i, N) + if i < N: + i += 1 + try: + conn.rollback() + except self.database_engine.module.Error as e1: + logger.warning( + "[TXN EROLL] {%s} %s", + name, + exception_to_unicode(e1), + ) + continue + raise + finally: + # we're either about to retry with a new cursor, or we're about to + # release the connection. Once we release the connection, it could + # get used for another query, which might do a conn.rollback(). + # + # In the latter case, even though that probably wouldn't affect the + # results of this transaction, python's sqlite will reset all + # statements on the connection [1], which will make our cursor + # invalid [2]. + # + # In any case, continuing to read rows after commit()ing seems + # dubious from the PoV of ACID transactional semantics + # (sqlite explicitly says that once you commit, you may see rows + # from subsequent updates.) + # + # In psycopg2, cursors are essentially a client-side fabrication - + # all the data is transferred to the client side when the statement + # finishes executing - so in theory we could go on streaming results + # from the cursor, but attempting to do so would make us + # incompatible with sqlite, so let's make sure we're not doing that + # by closing the cursor. + # + # (*named* cursors in psycopg2 are different and are proper server- + # side things, but (a) we don't use them and (b) they are implicitly + # closed by ending the transaction anyway.) + # + # In short, if we haven't finished with the cursor yet, that's a + # problem waiting to bite us. + # + # TL;DR: we're done with the cursor, so we can close it. + # + # [1]: https://github.com/python/cpython/blob/v3.8.0/Modules/_sqlite/connection.c#L465 + # [2]: https://github.com/python/cpython/blob/v3.8.0/Modules/_sqlite/cursor.c#L236 + cursor.close() + except Exception as e: + logger.debug("[TXN FAIL] {%s} %s", name, e) + raise + finally: + end = monotonic_time() + duration = end - start + + LoggingContext.current_context().add_database_transaction(duration) + + transaction_logger.debug("[TXN END] {%s} %f sec", name, duration) + + self._current_txn_total_time += duration + self._txn_perf_counters.update(desc, duration) + sql_txn_timer.labels(desc).observe(duration) + + @defer.inlineCallbacks + def runInteraction(self, desc, func, *args, **kwargs): + """Starts a transaction on the database and runs a given function + + Arguments: + desc (str): description of the transaction, for logging and metrics + func (func): callback function, which will be called with a + database transaction (twisted.enterprise.adbapi.Transaction) as + its first argument, followed by `args` and `kwargs`. + + args (list): positional args to pass to `func` + kwargs (dict): named args to pass to `func` + + Returns: + Deferred: The result of func + """ + after_callbacks = [] + exception_callbacks = [] + + if LoggingContext.current_context() == LoggingContext.sentinel: + logger.warning("Starting db txn '%s' from sentinel context", desc) + + try: + result = yield self.runWithConnection( + self.new_transaction, + desc, + after_callbacks, + exception_callbacks, + func, + *args, + **kwargs + ) + + for after_callback, after_args, after_kwargs in after_callbacks: + after_callback(*after_args, **after_kwargs) + except: # noqa: E722, as we reraise the exception this is fine. + for after_callback, after_args, after_kwargs in exception_callbacks: + after_callback(*after_args, **after_kwargs) + raise + + return result + + @defer.inlineCallbacks + def runWithConnection(self, func, *args, **kwargs): + """Wraps the .runWithConnection() method on the underlying db_pool. + + Arguments: + func (func): callback function, which will be called with a + database connection (twisted.enterprise.adbapi.Connection) as + its first argument, followed by `args` and `kwargs`. + args (list): positional args to pass to `func` + kwargs (dict): named args to pass to `func` + + Returns: + Deferred: The result of func + """ + parent_context = LoggingContext.current_context() + if parent_context == LoggingContext.sentinel: + logger.warning( + "Starting db connection from sentinel context: metrics will be lost" + ) + parent_context = None + + start_time = monotonic_time() + + def inner_func(conn, *args, **kwargs): + with LoggingContext("runWithConnection", parent_context) as context: + sched_duration_sec = monotonic_time() - start_time + sql_scheduling_timer.observe(sched_duration_sec) + context.add_database_scheduled(sched_duration_sec) + + if self.database_engine.is_connection_closed(conn): + logger.debug("Reconnecting closed database connection") + conn.reconnect() + + return func(conn, *args, **kwargs) + + result = yield make_deferred_yieldable( + self._db_pool.runWithConnection(inner_func, *args, **kwargs) + ) + + return result + + @staticmethod + def cursor_to_dict(cursor): + """Converts a SQL cursor into an list of dicts. + + Args: + cursor : The DBAPI cursor which has executed a query. + Returns: + A list of dicts where the key is the column header. + """ + col_headers = list(intern(str(column[0])) for column in cursor.description) + results = list(dict(zip(col_headers, row)) for row in cursor) + return results + + def execute(self, desc, decoder, query, *args): + """Runs a single query for a result set. + + Args: + decoder - The function which can resolve the cursor results to + something meaningful. + query - The query string to execute + *args - Query args. + Returns: + The result of decoder(results) + """ + + def interaction(txn): + txn.execute(query, args) + if decoder: + return decoder(txn) + else: + return txn.fetchall() + + return self.runInteraction(desc, interaction) + + # "Simple" SQL API methods that operate on a single table with no JOINs, + # no complex WHERE clauses, just a dict of values for columns. + + @defer.inlineCallbacks + def simple_insert(self, table, values, or_ignore=False, desc="simple_insert"): + """Executes an INSERT query on the named table. + + Args: + table : string giving the table name + values : dict of new column names and values for them + or_ignore : bool stating whether an exception should be raised + when a conflicting row already exists. If True, False will be + returned by the function instead + desc : string giving a description of the transaction + + Returns: + bool: Whether the row was inserted or not. Only useful when + `or_ignore` is True + """ + try: + yield self.runInteraction(desc, self.simple_insert_txn, table, values) + except self.database_engine.module.IntegrityError: + # We have to do or_ignore flag at this layer, since we can't reuse + # a cursor after we receive an error from the db. + if not or_ignore: + raise + return False + return True + + @staticmethod + def simple_insert_txn(txn, table, values): + keys, vals = zip(*values.items()) + + sql = "INSERT INTO %s (%s) VALUES(%s)" % ( + table, + ", ".join(k for k in keys), + ", ".join("?" for _ in keys), + ) + + txn.execute(sql, vals) + + def simple_insert_many(self, table, values, desc): + return self.runInteraction(desc, self.simple_insert_many_txn, table, values) + + @staticmethod + def simple_insert_many_txn(txn, table, values): + if not values: + return + + # This is a *slight* abomination to get a list of tuples of key names + # and a list of tuples of value names. + # + # i.e. [{"a": 1, "b": 2}, {"c": 3, "d": 4}] + # => [("a", "b",), ("c", "d",)] and [(1, 2,), (3, 4,)] + # + # The sort is to ensure that we don't rely on dictionary iteration + # order. + keys, vals = zip( + *[zip(*(sorted(i.items(), key=lambda kv: kv[0]))) for i in values if i] + ) + + for k in keys: + if k != keys[0]: + raise RuntimeError("All items must have the same keys") + + sql = "INSERT INTO %s (%s) VALUES(%s)" % ( + table, + ", ".join(k for k in keys[0]), + ", ".join("?" for _ in keys[0]), + ) + + txn.executemany(sql, vals) + + @defer.inlineCallbacks + def simple_upsert( + self, + table, + keyvalues, + values, + insertion_values={}, + desc="simple_upsert", + lock=True, + ): + """ + + `lock` should generally be set to True (the default), but can be set + to False if either of the following are true: + + * there is a UNIQUE INDEX on the key columns. In this case a conflict + will cause an IntegrityError in which case this function will retry + the update. + + * we somehow know that we are the only thread which will be updating + this table. + + Args: + table (str): The table to upsert into + keyvalues (dict): The unique key columns and their new values + values (dict): The nonunique columns and their new values + insertion_values (dict): additional key/values to use only when + inserting + lock (bool): True to lock the table when doing the upsert. + Returns: + Deferred(None or bool): Native upserts always return None. Emulated + upserts return True if a new entry was created, False if an existing + one was updated. + """ + attempts = 0 + while True: + try: + result = yield self.runInteraction( + desc, + self.simple_upsert_txn, + table, + keyvalues, + values, + insertion_values, + lock=lock, + ) + return result + except self.database_engine.module.IntegrityError as e: + attempts += 1 + if attempts >= 5: + # don't retry forever, because things other than races + # can cause IntegrityErrors + raise + + # presumably we raced with another transaction: let's retry. + logger.warning( + "IntegrityError when upserting into %s; retrying: %s", table, e + ) + + def simple_upsert_txn( + self, txn, table, keyvalues, values, insertion_values={}, lock=True + ): + """ + Pick the UPSERT method which works best on the platform. Either the + native one (Pg9.5+, recent SQLites), or fall back to an emulated method. + + Args: + txn: The transaction to use. + table (str): The table to upsert into + keyvalues (dict): The unique key tables and their new values + values (dict): The nonunique columns and their new values + insertion_values (dict): additional key/values to use only when + inserting + lock (bool): True to lock the table when doing the upsert. + Returns: + None or bool: Native upserts always return None. Emulated + upserts return True if a new entry was created, False if an existing + one was updated. + """ + if ( + self.database_engine.can_native_upsert + and table not in self._unsafe_to_upsert_tables + ): + return self.simple_upsert_txn_native_upsert( + txn, table, keyvalues, values, insertion_values=insertion_values + ) + else: + return self.simple_upsert_txn_emulated( + txn, + table, + keyvalues, + values, + insertion_values=insertion_values, + lock=lock, + ) + + def simple_upsert_txn_emulated( + self, txn, table, keyvalues, values, insertion_values={}, lock=True + ): + """ + Args: + table (str): The table to upsert into + keyvalues (dict): The unique key tables and their new values + values (dict): The nonunique columns and their new values + insertion_values (dict): additional key/values to use only when + inserting + lock (bool): True to lock the table when doing the upsert. + Returns: + bool: Return True if a new entry was created, False if an existing + one was updated. + """ + # We need to lock the table :(, unless we're *really* careful + if lock: + self.database_engine.lock_table(txn, table) + + def _getwhere(key): + # If the value we're passing in is None (aka NULL), we need to use + # IS, not =, as NULL = NULL equals NULL (False). + if keyvalues[key] is None: + return "%s IS ?" % (key,) + else: + return "%s = ?" % (key,) + + if not values: + # If `values` is empty, then all of the values we care about are in + # the unique key, so there is nothing to UPDATE. We can just do a + # SELECT instead to see if it exists. + sql = "SELECT 1 FROM %s WHERE %s" % ( + table, + " AND ".join(_getwhere(k) for k in keyvalues), + ) + sqlargs = list(keyvalues.values()) + txn.execute(sql, sqlargs) + if txn.fetchall(): + # We have an existing record. + return False + else: + # First try to update. + sql = "UPDATE %s SET %s WHERE %s" % ( + table, + ", ".join("%s = ?" % (k,) for k in values), + " AND ".join(_getwhere(k) for k in keyvalues), + ) + sqlargs = list(values.values()) + list(keyvalues.values()) + + txn.execute(sql, sqlargs) + if txn.rowcount > 0: + # successfully updated at least one row. + return False + + # We didn't find any existing rows, so insert a new one + allvalues = {} + allvalues.update(keyvalues) + allvalues.update(values) + allvalues.update(insertion_values) + + sql = "INSERT INTO %s (%s) VALUES (%s)" % ( + table, + ", ".join(k for k in allvalues), + ", ".join("?" for _ in allvalues), + ) + txn.execute(sql, list(allvalues.values())) + # successfully inserted + return True + + def simple_upsert_txn_native_upsert( + self, txn, table, keyvalues, values, insertion_values={} + ): + """ + Use the native UPSERT functionality in recent PostgreSQL versions. + + Args: + table (str): The table to upsert into + keyvalues (dict): The unique key tables and their new values + values (dict): The nonunique columns and their new values + insertion_values (dict): additional key/values to use only when + inserting + Returns: + None + """ + allvalues = {} + allvalues.update(keyvalues) + allvalues.update(insertion_values) + + if not values: + latter = "NOTHING" + else: + allvalues.update(values) + latter = "UPDATE SET " + ", ".join(k + "=EXCLUDED." + k for k in values) + + sql = ("INSERT INTO %s (%s) VALUES (%s) ON CONFLICT (%s) DO %s") % ( + table, + ", ".join(k for k in allvalues), + ", ".join("?" for _ in allvalues), + ", ".join(k for k in keyvalues), + latter, + ) + txn.execute(sql, list(allvalues.values())) + + def simple_upsert_many_txn( + self, txn, table, key_names, key_values, value_names, value_values + ): + """ + Upsert, many times. + + Args: + table (str): The table to upsert into + key_names (list[str]): The key column names. + key_values (list[list]): A list of each row's key column values. + value_names (list[str]): The value column names. If empty, no + values will be used, even if value_values is provided. + value_values (list[list]): A list of each row's value column values. + Returns: + None + """ + if ( + self.database_engine.can_native_upsert + and table not in self._unsafe_to_upsert_tables + ): + return self.simple_upsert_many_txn_native_upsert( + txn, table, key_names, key_values, value_names, value_values + ) + else: + return self.simple_upsert_many_txn_emulated( + txn, table, key_names, key_values, value_names, value_values + ) + + def simple_upsert_many_txn_emulated( + self, txn, table, key_names, key_values, value_names, value_values + ): + """ + Upsert, many times, but without native UPSERT support or batching. + + Args: + table (str): The table to upsert into + key_names (list[str]): The key column names. + key_values (list[list]): A list of each row's key column values. + value_names (list[str]): The value column names. If empty, no + values will be used, even if value_values is provided. + value_values (list[list]): A list of each row's value column values. + Returns: + None + """ + # No value columns, therefore make a blank list so that the following + # zip() works correctly. + if not value_names: + value_values = [() for x in range(len(key_values))] + + for keyv, valv in zip(key_values, value_values): + _keys = {x: y for x, y in zip(key_names, keyv)} + _vals = {x: y for x, y in zip(value_names, valv)} + + self.simple_upsert_txn_emulated(txn, table, _keys, _vals) + + def simple_upsert_many_txn_native_upsert( + self, txn, table, key_names, key_values, value_names, value_values + ): + """ + Upsert, many times, using batching where possible. + + Args: + table (str): The table to upsert into + key_names (list[str]): The key column names. + key_values (list[list]): A list of each row's key column values. + value_names (list[str]): The value column names. If empty, no + values will be used, even if value_values is provided. + value_values (list[list]): A list of each row's value column values. + Returns: + None + """ + allnames = [] + allnames.extend(key_names) + allnames.extend(value_names) + + if not value_names: + # No value columns, therefore make a blank list so that the + # following zip() works correctly. + latter = "NOTHING" + value_values = [() for x in range(len(key_values))] + else: + latter = "UPDATE SET " + ", ".join( + k + "=EXCLUDED." + k for k in value_names + ) + + sql = "INSERT INTO %s (%s) VALUES (%s) ON CONFLICT (%s) DO %s" % ( + table, + ", ".join(k for k in allnames), + ", ".join("?" for _ in allnames), + ", ".join(key_names), + latter, + ) + + args = [] + + for x, y in zip(key_values, value_values): + args.append(tuple(x) + tuple(y)) + + return txn.execute_batch(sql, args) + + def simple_select_one( + self, table, keyvalues, retcols, allow_none=False, desc="simple_select_one" + ): + """Executes a SELECT query on the named table, which is expected to + return a single row, returning multiple columns from it. + + Args: + table : string giving the table name + keyvalues : dict of column names and values to select the row with + retcols : list of strings giving the names of the columns to return + + allow_none : If true, return None instead of failing if the SELECT + statement returns no rows + """ + return self.runInteraction( + desc, self.simple_select_one_txn, table, keyvalues, retcols, allow_none + ) + + def simple_select_one_onecol( + self, + table, + keyvalues, + retcol, + allow_none=False, + desc="simple_select_one_onecol", + ): + """Executes a SELECT query on the named table, which is expected to + return a single row, returning a single column from it. + + Args: + table : string giving the table name + keyvalues : dict of column names and values to select the row with + retcol : string giving the name of the column to return + """ + return self.runInteraction( + desc, + self.simple_select_one_onecol_txn, + table, + keyvalues, + retcol, + allow_none=allow_none, + ) + + @classmethod + def simple_select_one_onecol_txn( + cls, txn, table, keyvalues, retcol, allow_none=False + ): + ret = cls.simple_select_onecol_txn( + txn, table=table, keyvalues=keyvalues, retcol=retcol + ) + + if ret: + return ret[0] + else: + if allow_none: + return None + else: + raise StoreError(404, "No row found") + + @staticmethod + def simple_select_onecol_txn(txn, table, keyvalues, retcol): + sql = ("SELECT %(retcol)s FROM %(table)s") % {"retcol": retcol, "table": table} + + if keyvalues: + sql += " WHERE %s" % " AND ".join("%s = ?" % k for k in iterkeys(keyvalues)) + txn.execute(sql, list(keyvalues.values())) + else: + txn.execute(sql) + + return [r[0] for r in txn] + + def simple_select_onecol( + self, table, keyvalues, retcol, desc="simple_select_onecol" + ): + """Executes a SELECT query on the named table, which returns a list + comprising of the values of the named column from the selected rows. + + Args: + table (str): table name + keyvalues (dict|None): column names and values to select the rows with + retcol (str): column whos value we wish to retrieve. + + Returns: + Deferred: Results in a list + """ + return self.runInteraction( + desc, self.simple_select_onecol_txn, table, keyvalues, retcol + ) + + def simple_select_list(self, table, keyvalues, retcols, desc="simple_select_list"): + """Executes a SELECT query on the named table, which may return zero or + more rows, returning the result as a list of dicts. + + Args: + table (str): the table name + keyvalues (dict[str, Any] | None): + column names and values to select the rows with, or None to not + apply a WHERE clause. + retcols (iterable[str]): the names of the columns to return + Returns: + defer.Deferred: resolves to list[dict[str, Any]] + """ + return self.runInteraction( + desc, self.simple_select_list_txn, table, keyvalues, retcols + ) + + @classmethod + def simple_select_list_txn(cls, txn, table, keyvalues, retcols): + """Executes a SELECT query on the named table, which may return zero or + more rows, returning the result as a list of dicts. + + Args: + txn : Transaction object + table (str): the table name + keyvalues (dict[str, T] | None): + column names and values to select the rows with, or None to not + apply a WHERE clause. + retcols (iterable[str]): the names of the columns to return + """ + if keyvalues: + sql = "SELECT %s FROM %s WHERE %s" % ( + ", ".join(retcols), + table, + " AND ".join("%s = ?" % (k,) for k in keyvalues), + ) + txn.execute(sql, list(keyvalues.values())) + else: + sql = "SELECT %s FROM %s" % (", ".join(retcols), table) + txn.execute(sql) + + return cls.cursor_to_dict(txn) + + @defer.inlineCallbacks + def simple_select_many_batch( + self, + table, + column, + iterable, + retcols, + keyvalues={}, + desc="simple_select_many_batch", + batch_size=100, + ): + """Executes a SELECT query on the named table, which may return zero or + more rows, returning the result as a list of dicts. + + Filters rows by if value of `column` is in `iterable`. + + Args: + table : string giving the table name + column : column name to test for inclusion against `iterable` + iterable : list + keyvalues : dict of column names and values to select the rows with + retcols : list of strings giving the names of the columns to return + """ + results = [] + + if not iterable: + return results + + # iterables can not be sliced, so convert it to a list first + it_list = list(iterable) + + chunks = [ + it_list[i : i + batch_size] for i in range(0, len(it_list), batch_size) + ] + for chunk in chunks: + rows = yield self.runInteraction( + desc, + self.simple_select_many_txn, + table, + column, + chunk, + keyvalues, + retcols, + ) + + results.extend(rows) + + return results + + @classmethod + def simple_select_many_txn(cls, txn, table, column, iterable, keyvalues, retcols): + """Executes a SELECT query on the named table, which may return zero or + more rows, returning the result as a list of dicts. + + Filters rows by if value of `column` is in `iterable`. + + Args: + txn : Transaction object + table : string giving the table name + column : column name to test for inclusion against `iterable` + iterable : list + keyvalues : dict of column names and values to select the rows with + retcols : list of strings giving the names of the columns to return + """ + if not iterable: + return [] + + clause, values = make_in_list_sql_clause(txn.database_engine, column, iterable) + clauses = [clause] + + for key, value in iteritems(keyvalues): + clauses.append("%s = ?" % (key,)) + values.append(value) + + sql = "SELECT %s FROM %s WHERE %s" % ( + ", ".join(retcols), + table, + " AND ".join(clauses), + ) + + txn.execute(sql, values) + return cls.cursor_to_dict(txn) + + def simple_update(self, table, keyvalues, updatevalues, desc): + return self.runInteraction( + desc, self.simple_update_txn, table, keyvalues, updatevalues + ) + + @staticmethod + def simple_update_txn(txn, table, keyvalues, updatevalues): + if keyvalues: + where = "WHERE %s" % " AND ".join("%s = ?" % k for k in iterkeys(keyvalues)) + else: + where = "" + + update_sql = "UPDATE %s SET %s %s" % ( + table, + ", ".join("%s = ?" % (k,) for k in updatevalues), + where, + ) + + txn.execute(update_sql, list(updatevalues.values()) + list(keyvalues.values())) + + return txn.rowcount + + def simple_update_one( + self, table, keyvalues, updatevalues, desc="simple_update_one" + ): + """Executes an UPDATE query on the named table, setting new values for + columns in a row matching the key values. + + Args: + table : string giving the table name + keyvalues : dict of column names and values to select the row with + updatevalues : dict giving column names and values to update + retcols : optional list of column names to return + + If present, retcols gives a list of column names on which to perform + a SELECT statement *before* performing the UPDATE statement. The values + of these will be returned in a dict. + + These are performed within the same transaction, allowing an atomic + get-and-set. This can be used to implement compare-and-set by putting + the update column in the 'keyvalues' dict as well. + """ + return self.runInteraction( + desc, self.simple_update_one_txn, table, keyvalues, updatevalues + ) + + @classmethod + def simple_update_one_txn(cls, txn, table, keyvalues, updatevalues): + rowcount = cls.simple_update_txn(txn, table, keyvalues, updatevalues) + + if rowcount == 0: + raise StoreError(404, "No row found (%s)" % (table,)) + if rowcount > 1: + raise StoreError(500, "More than one row matched (%s)" % (table,)) + + @staticmethod + def simple_select_one_txn(txn, table, keyvalues, retcols, allow_none=False): + select_sql = "SELECT %s FROM %s WHERE %s" % ( + ", ".join(retcols), + table, + " AND ".join("%s = ?" % (k,) for k in keyvalues), + ) + + txn.execute(select_sql, list(keyvalues.values())) + row = txn.fetchone() + + if not row: + if allow_none: + return None + raise StoreError(404, "No row found (%s)" % (table,)) + if txn.rowcount > 1: + raise StoreError(500, "More than one row matched (%s)" % (table,)) + + return dict(zip(retcols, row)) + + def simple_delete_one(self, table, keyvalues, desc="simple_delete_one"): + """Executes a DELETE query on the named table, expecting to delete a + single row. + + Args: + table : string giving the table name + keyvalues : dict of column names and values to select the row with + """ + return self.runInteraction(desc, self.simple_delete_one_txn, table, keyvalues) + + @staticmethod + def simple_delete_one_txn(txn, table, keyvalues): + """Executes a DELETE query on the named table, expecting to delete a + single row. + + Args: + table : string giving the table name + keyvalues : dict of column names and values to select the row with + """ + sql = "DELETE FROM %s WHERE %s" % ( + table, + " AND ".join("%s = ?" % (k,) for k in keyvalues), + ) + + txn.execute(sql, list(keyvalues.values())) + if txn.rowcount == 0: + raise StoreError(404, "No row found (%s)" % (table,)) + if txn.rowcount > 1: + raise StoreError(500, "More than one row matched (%s)" % (table,)) + + def simple_delete(self, table, keyvalues, desc): + return self.runInteraction(desc, self.simple_delete_txn, table, keyvalues) + + @staticmethod + def simple_delete_txn(txn, table, keyvalues): + sql = "DELETE FROM %s WHERE %s" % ( + table, + " AND ".join("%s = ?" % (k,) for k in keyvalues), + ) + + txn.execute(sql, list(keyvalues.values())) + return txn.rowcount + + def simple_delete_many(self, table, column, iterable, keyvalues, desc): + return self.runInteraction( + desc, self.simple_delete_many_txn, table, column, iterable, keyvalues + ) + + @staticmethod + def simple_delete_many_txn(txn, table, column, iterable, keyvalues): + """Executes a DELETE query on the named table. + + Filters rows by if value of `column` is in `iterable`. + + Args: + txn : Transaction object + table : string giving the table name + column : column name to test for inclusion against `iterable` + iterable : list + keyvalues : dict of column names and values to select the rows with + + Returns: + int: Number rows deleted + """ + if not iterable: + return 0 + + sql = "DELETE FROM %s" % table + + clause, values = make_in_list_sql_clause(txn.database_engine, column, iterable) + clauses = [clause] + + for key, value in iteritems(keyvalues): + clauses.append("%s = ?" % (key,)) + values.append(value) + + if clauses: + sql = "%s WHERE %s" % (sql, " AND ".join(clauses)) + txn.execute(sql, values) + + return txn.rowcount + + def get_cache_dict( + self, db_conn, table, entity_column, stream_column, max_value, limit=100000 + ): + # Fetch a mapping of room_id -> max stream position for "recent" rooms. + # It doesn't really matter how many we get, the StreamChangeCache will + # do the right thing to ensure it respects the max size of cache. + sql = ( + "SELECT %(entity)s, MAX(%(stream)s) FROM %(table)s" + " WHERE %(stream)s > ? - %(limit)s" + " GROUP BY %(entity)s" + ) % { + "table": table, + "entity": entity_column, + "stream": stream_column, + "limit": limit, + } + + sql = self.database_engine.convert_param_style(sql) + + txn = db_conn.cursor() + txn.execute(sql, (int(max_value),)) + + cache = {row[0]: int(row[1]) for row in txn} + + txn.close() + + if cache: + min_val = min(itervalues(cache)) + else: + min_val = max_value + + return cache, min_val + + def simple_select_list_paginate( + self, + table, + keyvalues, + orderby, + start, + limit, + retcols, + order_direction="ASC", + desc="simple_select_list_paginate", + ): + """ + Executes a SELECT query on the named table with start and limit, + of row numbers, which may return zero or number of rows from start to limit, + returning the result as a list of dicts. + + Args: + table (str): the table name + keyvalues (dict[str, T] | None): + column names and values to select the rows with, or None to not + apply a WHERE clause. + orderby (str): Column to order the results by. + start (int): Index to begin the query at. + limit (int): Number of results to return. + retcols (iterable[str]): the names of the columns to return + order_direction (str): Whether the results should be ordered "ASC" or "DESC". + Returns: + defer.Deferred: resolves to list[dict[str, Any]] + """ + return self.runInteraction( + desc, + self.simple_select_list_paginate_txn, + table, + keyvalues, + orderby, + start, + limit, + retcols, + order_direction=order_direction, + ) + + @classmethod + def simple_select_list_paginate_txn( + cls, + txn, + table, + keyvalues, + orderby, + start, + limit, + retcols, + order_direction="ASC", + ): + """ + Executes a SELECT query on the named table with start and limit, + of row numbers, which may return zero or number of rows from start to limit, + returning the result as a list of dicts. + + Args: + txn : Transaction object + table (str): the table name + keyvalues (dict[str, T] | None): + column names and values to select the rows with, or None to not + apply a WHERE clause. + orderby (str): Column to order the results by. + start (int): Index to begin the query at. + limit (int): Number of results to return. + retcols (iterable[str]): the names of the columns to return + order_direction (str): Whether the results should be ordered "ASC" or "DESC". + Returns: + defer.Deferred: resolves to list[dict[str, Any]] + """ + if order_direction not in ["ASC", "DESC"]: + raise ValueError("order_direction must be one of 'ASC' or 'DESC'.") + + if keyvalues: + where_clause = "WHERE " + " AND ".join("%s = ?" % (k,) for k in keyvalues) + else: + where_clause = "" + + sql = "SELECT %s FROM %s %s ORDER BY %s %s LIMIT ? OFFSET ?" % ( + ", ".join(retcols), + table, + where_clause, + orderby, + order_direction, + ) + txn.execute(sql, list(keyvalues.values()) + [limit, start]) + + return cls.cursor_to_dict(txn) + + def get_user_count_txn(self, txn): + """Get a total number of registered users in the users list. + + Args: + txn : Transaction object + Returns: + int : number of users + """ + sql_count = "SELECT COUNT(*) FROM users WHERE is_guest = 0;" + txn.execute(sql_count) + return txn.fetchone()[0] + + def simple_search_list(self, table, term, col, retcols, desc="simple_search_list"): + """Executes a SELECT query on the named table, which may return zero or + more rows, returning the result as a list of dicts. + + Args: + table (str): the table name + term (str | None): + term for searching the table matched to a column. + col (str): column to query term should be matched to + retcols (iterable[str]): the names of the columns to return + Returns: + defer.Deferred: resolves to list[dict[str, Any]] or None + """ + + return self.runInteraction( + desc, self.simple_search_list_txn, table, term, col, retcols + ) + + @classmethod + def simple_search_list_txn(cls, txn, table, term, col, retcols): + """Executes a SELECT query on the named table, which may return zero or + more rows, returning the result as a list of dicts. + + Args: + txn : Transaction object + table (str): the table name + term (str | None): + term for searching the table matched to a column. + col (str): column to query term should be matched to + retcols (iterable[str]): the names of the columns to return + Returns: + defer.Deferred: resolves to list[dict[str, Any]] or None + """ + if term: + sql = "SELECT %s FROM %s WHERE %s LIKE ?" % (", ".join(retcols), table, col) + termvalues = ["%%" + term + "%%"] + txn.execute(sql, termvalues) + else: + return 0 + + return cls.cursor_to_dict(txn) + + +def make_in_list_sql_clause( + database_engine, column: str, iterable: Iterable +) -> Tuple[str, Iterable]: + """Returns an SQL clause that checks the given column is in the iterable. + + On SQLite this expands to `column IN (?, ?, ...)`, whereas on Postgres + it expands to `column = ANY(?)`. While both DBs support the `IN` form, + using the `ANY` form on postgres means that it views queries with + different length iterables as the same, helping the query stats. + + Args: + database_engine + column: Name of the column + iterable: The values to check the column against. + + Returns: + A tuple of SQL query and the args + """ + + if database_engine.supports_using_any_list: + # This should hopefully be faster, but also makes postgres query + # stats easier to understand. + return "%s = ANY(?)" % (column,), [list(iterable)] + else: + return "%s IN (%s)" % (column, ",".join("?" for _ in iterable)), list(iterable) diff --git a/tests/handlers/test_stats.py b/tests/handlers/test_stats.py index 380fd0d107..7f7962c3dd 100644 --- a/tests/handlers/test_stats.py +++ b/tests/handlers/test_stats.py @@ -45,13 +45,13 @@ class StatsRoomTests(unittest.HomeserverTestCase): self.store._all_done = False self.get_success( - self.store.simple_insert( + self.store.db.simple_insert( "background_updates", {"update_name": "populate_stats_prepare", "progress_json": "{}"}, ) ) self.get_success( - self.store.simple_insert( + self.store.db.simple_insert( "background_updates", { "update_name": "populate_stats_process_rooms", @@ -61,7 +61,7 @@ class StatsRoomTests(unittest.HomeserverTestCase): ) ) self.get_success( - self.store.simple_insert( + self.store.db.simple_insert( "background_updates", { "update_name": "populate_stats_process_users", @@ -71,7 +71,7 @@ class StatsRoomTests(unittest.HomeserverTestCase): ) ) self.get_success( - self.store.simple_insert( + self.store.db.simple_insert( "background_updates", { "update_name": "populate_stats_cleanup", @@ -82,7 +82,7 @@ class StatsRoomTests(unittest.HomeserverTestCase): ) def get_all_room_state(self): - return self.store.simple_select_list( + return self.store.db.simple_select_list( "room_stats_state", None, retcols=("name", "topic", "canonical_alias") ) @@ -96,7 +96,7 @@ class StatsRoomTests(unittest.HomeserverTestCase): end_ts = self.store.quantise_stats_time(self.reactor.seconds() * 1000) return self.get_success( - self.store.simple_select_one( + self.store.db.simple_select_one( table + "_historical", {id_col: stat_id, end_ts: end_ts}, cols, @@ -180,7 +180,7 @@ class StatsRoomTests(unittest.HomeserverTestCase): self.handler.stats_enabled = True self.store._all_done = False self.get_success( - self.store.simple_update_one( + self.store.db.simple_update_one( table="stats_incremental_position", keyvalues={}, updatevalues={"stream_id": 0}, @@ -188,7 +188,7 @@ class StatsRoomTests(unittest.HomeserverTestCase): ) self.get_success( - self.store.simple_insert( + self.store.db.simple_insert( "background_updates", {"update_name": "populate_stats_prepare", "progress_json": "{}"}, ) @@ -205,13 +205,13 @@ class StatsRoomTests(unittest.HomeserverTestCase): # Now do the initial ingestion. self.get_success( - self.store.simple_insert( + self.store.db.simple_insert( "background_updates", {"update_name": "populate_stats_process_rooms", "progress_json": "{}"}, ) ) self.get_success( - self.store.simple_insert( + self.store.db.simple_insert( "background_updates", { "update_name": "populate_stats_cleanup", @@ -656,12 +656,12 @@ class StatsRoomTests(unittest.HomeserverTestCase): self.store._all_done = False self.get_success( - self.store.simple_delete( + self.store.db.simple_delete( "room_stats_current", {"1": 1}, "test_delete_stats" ) ) self.get_success( - self.store.simple_delete( + self.store.db.simple_delete( "user_stats_current", {"1": 1}, "test_delete_stats" ) ) @@ -675,7 +675,7 @@ class StatsRoomTests(unittest.HomeserverTestCase): self.store._all_done = False self.get_success( - self.store.simple_insert( + self.store.db.simple_insert( "background_updates", { "update_name": "populate_stats_process_rooms", @@ -685,7 +685,7 @@ class StatsRoomTests(unittest.HomeserverTestCase): ) ) self.get_success( - self.store.simple_insert( + self.store.db.simple_insert( "background_updates", { "update_name": "populate_stats_process_users", @@ -695,7 +695,7 @@ class StatsRoomTests(unittest.HomeserverTestCase): ) ) self.get_success( - self.store.simple_insert( + self.store.db.simple_insert( "background_updates", { "update_name": "populate_stats_cleanup", diff --git a/tests/handlers/test_user_directory.py b/tests/handlers/test_user_directory.py index d5b1c5b4ac..bc9d441541 100644 --- a/tests/handlers/test_user_directory.py +++ b/tests/handlers/test_user_directory.py @@ -158,7 +158,7 @@ class UserDirectoryTestCase(unittest.HomeserverTestCase): def get_users_in_public_rooms(self): r = self.get_success( - self.store.simple_select_list( + self.store.db.simple_select_list( "users_in_public_rooms", None, ("user_id", "room_id") ) ) @@ -169,7 +169,7 @@ class UserDirectoryTestCase(unittest.HomeserverTestCase): def get_users_who_share_private_rooms(self): return self.get_success( - self.store.simple_select_list( + self.store.db.simple_select_list( "users_who_share_private_rooms", None, ["user_id", "other_user_id", "room_id"], @@ -184,7 +184,7 @@ class UserDirectoryTestCase(unittest.HomeserverTestCase): self.store._all_done = False self.get_success( - self.store.simple_insert( + self.store.db.simple_insert( "background_updates", { "update_name": "populate_user_directory_createtables", @@ -193,7 +193,7 @@ class UserDirectoryTestCase(unittest.HomeserverTestCase): ) ) self.get_success( - self.store.simple_insert( + self.store.db.simple_insert( "background_updates", { "update_name": "populate_user_directory_process_rooms", @@ -203,7 +203,7 @@ class UserDirectoryTestCase(unittest.HomeserverTestCase): ) ) self.get_success( - self.store.simple_insert( + self.store.db.simple_insert( "background_updates", { "update_name": "populate_user_directory_process_users", @@ -213,7 +213,7 @@ class UserDirectoryTestCase(unittest.HomeserverTestCase): ) ) self.get_success( - self.store.simple_insert( + self.store.db.simple_insert( "background_updates", { "update_name": "populate_user_directory_cleanup", diff --git a/tests/rest/admin/test_admin.py b/tests/rest/admin/test_admin.py index 124ce0768a..0ed2594381 100644 --- a/tests/rest/admin/test_admin.py +++ b/tests/rest/admin/test_admin.py @@ -632,7 +632,7 @@ class PurgeRoomTestCase(unittest.HomeserverTestCase): "state_groups_state", ): count = self.get_success( - self.store.simple_select_one_onecol( + self.store.db.simple_select_one_onecol( table=table, keyvalues={"room_id": room_id}, retcol="COUNT(*)", diff --git a/tests/storage/test__base.py b/tests/storage/test__base.py index 7b7434a468..d491ea2924 100644 --- a/tests/storage/test__base.py +++ b/tests/storage/test__base.py @@ -323,7 +323,7 @@ class UpsertManyTests(unittest.HomeserverTestCase): self.table_name = "table_" + hs.get_secrets().token_hex(6) self.get_success( - self.storage.runInteraction( + self.storage.db.runInteraction( "create", lambda x, *a: x.execute(*a), "CREATE TABLE %s (id INTEGER, username TEXT, value TEXT)" @@ -331,7 +331,7 @@ class UpsertManyTests(unittest.HomeserverTestCase): ) ) self.get_success( - self.storage.runInteraction( + self.storage.db.runInteraction( "index", lambda x, *a: x.execute(*a), "CREATE UNIQUE INDEX %sindex ON %s(id, username)" @@ -354,9 +354,9 @@ class UpsertManyTests(unittest.HomeserverTestCase): value_values = [["hello"], ["there"]] self.get_success( - self.storage.runInteraction( + self.storage.db.runInteraction( "test", - self.storage.simple_upsert_many_txn, + self.storage.db.simple_upsert_many_txn, self.table_name, key_names, key_values, @@ -367,7 +367,7 @@ class UpsertManyTests(unittest.HomeserverTestCase): # Check results are what we expect res = self.get_success( - self.storage.simple_select_list( + self.storage.db.simple_select_list( self.table_name, None, ["id, username, value"] ) ) @@ -381,9 +381,9 @@ class UpsertManyTests(unittest.HomeserverTestCase): value_values = [["bleb"]] self.get_success( - self.storage.runInteraction( + self.storage.db.runInteraction( "test", - self.storage.simple_upsert_many_txn, + self.storage.db.simple_upsert_many_txn, self.table_name, key_names, key_values, @@ -394,7 +394,7 @@ class UpsertManyTests(unittest.HomeserverTestCase): # Check results are what we expect res = self.get_success( - self.storage.simple_select_list( + self.storage.db.simple_select_list( self.table_name, None, ["id, username, value"] ) ) diff --git a/tests/storage/test_background_update.py b/tests/storage/test_background_update.py index 9fabe3fbc0..e360297df9 100644 --- a/tests/storage/test_background_update.py +++ b/tests/storage/test_background_update.py @@ -37,7 +37,7 @@ class BackgroundUpdateTestCase(unittest.TestCase): def update(progress, count): self.clock.advance_time_msec(count * duration_ms) progress = {"my_key": progress["my_key"] + 1} - yield self.store.runInteraction( + yield self.store.db.runInteraction( "update_progress", self.store._background_update_progress_txn, "test_update", diff --git a/tests/storage/test_base.py b/tests/storage/test_base.py index de5e4a5fce..7915d48a9e 100644 --- a/tests/storage/test_base.py +++ b/tests/storage/test_base.py @@ -65,7 +65,7 @@ class SQLBaseStoreTestCase(unittest.TestCase): def test_insert_1col(self): self.mock_txn.rowcount = 1 - yield self.datastore.simple_insert( + yield self.datastore.db.simple_insert( table="tablename", values={"columname": "Value"} ) @@ -77,7 +77,7 @@ class SQLBaseStoreTestCase(unittest.TestCase): def test_insert_3cols(self): self.mock_txn.rowcount = 1 - yield self.datastore.simple_insert( + yield self.datastore.db.simple_insert( table="tablename", # Use OrderedDict() so we can assert on the SQL generated values=OrderedDict([("colA", 1), ("colB", 2), ("colC", 3)]), @@ -92,7 +92,7 @@ class SQLBaseStoreTestCase(unittest.TestCase): self.mock_txn.rowcount = 1 self.mock_txn.__iter__ = Mock(return_value=iter([("Value",)])) - value = yield self.datastore.simple_select_one_onecol( + value = yield self.datastore.db.simple_select_one_onecol( table="tablename", keyvalues={"keycol": "TheKey"}, retcol="retcol" ) @@ -106,7 +106,7 @@ class SQLBaseStoreTestCase(unittest.TestCase): self.mock_txn.rowcount = 1 self.mock_txn.fetchone.return_value = (1, 2, 3) - ret = yield self.datastore.simple_select_one( + ret = yield self.datastore.db.simple_select_one( table="tablename", keyvalues={"keycol": "TheKey"}, retcols=["colA", "colB", "colC"], @@ -122,7 +122,7 @@ class SQLBaseStoreTestCase(unittest.TestCase): self.mock_txn.rowcount = 0 self.mock_txn.fetchone.return_value = None - ret = yield self.datastore.simple_select_one( + ret = yield self.datastore.db.simple_select_one( table="tablename", keyvalues={"keycol": "Not here"}, retcols=["colA"], @@ -137,7 +137,7 @@ class SQLBaseStoreTestCase(unittest.TestCase): self.mock_txn.__iter__ = Mock(return_value=iter([(1,), (2,), (3,)])) self.mock_txn.description = (("colA", None, None, None, None, None, None),) - ret = yield self.datastore.simple_select_list( + ret = yield self.datastore.db.simple_select_list( table="tablename", keyvalues={"keycol": "A set"}, retcols=["colA"] ) @@ -150,7 +150,7 @@ class SQLBaseStoreTestCase(unittest.TestCase): def test_update_one_1col(self): self.mock_txn.rowcount = 1 - yield self.datastore.simple_update_one( + yield self.datastore.db.simple_update_one( table="tablename", keyvalues={"keycol": "TheKey"}, updatevalues={"columnname": "New Value"}, @@ -165,7 +165,7 @@ class SQLBaseStoreTestCase(unittest.TestCase): def test_update_one_4cols(self): self.mock_txn.rowcount = 1 - yield self.datastore.simple_update_one( + yield self.datastore.db.simple_update_one( table="tablename", keyvalues=OrderedDict([("colA", 1), ("colB", 2)]), updatevalues=OrderedDict([("colC", 3), ("colD", 4)]), @@ -180,7 +180,7 @@ class SQLBaseStoreTestCase(unittest.TestCase): def test_delete_one(self): self.mock_txn.rowcount = 1 - yield self.datastore.simple_delete_one( + yield self.datastore.db.simple_delete_one( table="tablename", keyvalues={"keycol": "Go away"} ) diff --git a/tests/storage/test_cleanup_extrems.py b/tests/storage/test_cleanup_extrems.py index 69dcaa63d5..e454bbff29 100644 --- a/tests/storage/test_cleanup_extrems.py +++ b/tests/storage/test_cleanup_extrems.py @@ -62,7 +62,9 @@ class CleanupExtremBackgroundUpdateStoreTestCase(HomeserverTestCase): prepare_database.executescript(txn, schema_path) self.get_success( - self.store.runInteraction("test_delete_forward_extremities", run_delta_file) + self.store.db.runInteraction( + "test_delete_forward_extremities", run_delta_file + ) ) # Ugh, have to reset this flag diff --git a/tests/storage/test_client_ips.py b/tests/storage/test_client_ips.py index 25bdd2c163..c4f838907c 100644 --- a/tests/storage/test_client_ips.py +++ b/tests/storage/test_client_ips.py @@ -81,7 +81,7 @@ class ClientIpStoreTestCase(unittest.HomeserverTestCase): self.pump(0) result = self.get_success( - self.store.simple_select_list( + self.store.db.simple_select_list( table="user_ips", keyvalues={"user_id": user_id}, retcols=["access_token", "ip", "user_agent", "device_id", "last_seen"], @@ -112,7 +112,7 @@ class ClientIpStoreTestCase(unittest.HomeserverTestCase): self.pump(0) result = self.get_success( - self.store.simple_select_list( + self.store.db.simple_select_list( table="user_ips", keyvalues={"user_id": user_id}, retcols=["access_token", "ip", "user_agent", "device_id", "last_seen"], @@ -218,7 +218,7 @@ class ClientIpStoreTestCase(unittest.HomeserverTestCase): # But clear the associated entry in devices table self.get_success( - self.store.simple_update( + self.store.db.simple_update( table="devices", keyvalues={"user_id": user_id, "device_id": "device_id"}, updatevalues={"last_seen": None, "ip": None, "user_agent": None}, @@ -245,7 +245,7 @@ class ClientIpStoreTestCase(unittest.HomeserverTestCase): # Register the background update to run again. self.get_success( - self.store.simple_insert( + self.store.db.simple_insert( table="background_updates", values={ "update_name": "devices_last_seen", @@ -297,7 +297,7 @@ class ClientIpStoreTestCase(unittest.HomeserverTestCase): # We should see that in the DB result = self.get_success( - self.store.simple_select_list( + self.store.db.simple_select_list( table="user_ips", keyvalues={"user_id": user_id}, retcols=["access_token", "ip", "user_agent", "device_id", "last_seen"], @@ -323,7 +323,7 @@ class ClientIpStoreTestCase(unittest.HomeserverTestCase): # We should get no results. result = self.get_success( - self.store.simple_select_list( + self.store.db.simple_select_list( table="user_ips", keyvalues={"user_id": user_id}, retcols=["access_token", "ip", "user_agent", "device_id", "last_seen"], diff --git a/tests/storage/test_event_federation.py b/tests/storage/test_event_federation.py index 2fe50377f8..eadfb90a22 100644 --- a/tests/storage/test_event_federation.py +++ b/tests/storage/test_event_federation.py @@ -61,7 +61,7 @@ class EventFederationWorkerStoreTestCase(tests.unittest.TestCase): ) for i in range(0, 11): - yield self.store.runInteraction("insert", insert_event, i) + yield self.store.db.runInteraction("insert", insert_event, i) # this should get the last five and five others r = yield self.store.get_prev_events_for_room(room_id) @@ -93,9 +93,9 @@ class EventFederationWorkerStoreTestCase(tests.unittest.TestCase): ) for i in range(0, 20): - yield self.store.runInteraction("insert", insert_event, i, room1) - yield self.store.runInteraction("insert", insert_event, i, room2) - yield self.store.runInteraction("insert", insert_event, i, room3) + yield self.store.db.runInteraction("insert", insert_event, i, room1) + yield self.store.db.runInteraction("insert", insert_event, i, room2) + yield self.store.db.runInteraction("insert", insert_event, i, room3) # Test simple case r = yield self.store.get_rooms_with_many_extremities(5, 5, []) diff --git a/tests/storage/test_event_push_actions.py b/tests/storage/test_event_push_actions.py index 2337a1ae46..d4bcf1821e 100644 --- a/tests/storage/test_event_push_actions.py +++ b/tests/storage/test_event_push_actions.py @@ -55,7 +55,7 @@ class EventPushActionsStoreTestCase(tests.unittest.TestCase): @defer.inlineCallbacks def _assert_counts(noitf_count, highlight_count): - counts = yield self.store.runInteraction( + counts = yield self.store.db.runInteraction( "", self.store._get_unread_counts_by_pos_txn, room_id, user_id, 0 ) self.assertEquals( @@ -74,7 +74,7 @@ class EventPushActionsStoreTestCase(tests.unittest.TestCase): yield self.store.add_push_actions_to_staging( event.event_id, {user_id: action} ) - yield self.store.runInteraction( + yield self.store.db.runInteraction( "", self.store._set_push_actions_for_event_and_users_txn, [(event, None)], @@ -82,12 +82,12 @@ class EventPushActionsStoreTestCase(tests.unittest.TestCase): ) def _rotate(stream): - return self.store.runInteraction( + return self.store.db.runInteraction( "", self.store._rotate_notifs_before_txn, stream ) def _mark_read(stream, depth): - return self.store.runInteraction( + return self.store.db.runInteraction( "", self.store._remove_old_push_actions_before_txn, room_id, @@ -116,7 +116,7 @@ class EventPushActionsStoreTestCase(tests.unittest.TestCase): yield _inject_actions(6, PlAIN_NOTIF) yield _rotate(7) - yield self.store.simple_delete( + yield self.store.db.simple_delete( table="event_push_actions", keyvalues={"1": 1}, desc="" ) @@ -135,7 +135,7 @@ class EventPushActionsStoreTestCase(tests.unittest.TestCase): @defer.inlineCallbacks def test_find_first_stream_ordering_after_ts(self): def add_event(so, ts): - return self.store.simple_insert( + return self.store.db.simple_insert( "events", { "stream_ordering": so, diff --git a/tests/storage/test_monthly_active_users.py b/tests/storage/test_monthly_active_users.py index 90a63dc477..3c78faab45 100644 --- a/tests/storage/test_monthly_active_users.py +++ b/tests/storage/test_monthly_active_users.py @@ -65,7 +65,7 @@ class MonthlyActiveUsersTestCase(unittest.HomeserverTestCase): self.store.user_add_threepid(user1, "email", user1_email, now, now) self.store.user_add_threepid(user2, "email", user2_email, now, now) - self.store.runInteraction( + self.store.db.runInteraction( "initialise", self.store._initialise_reserved_users, threepids ) self.pump() @@ -183,7 +183,7 @@ class MonthlyActiveUsersTestCase(unittest.HomeserverTestCase): ) self.hs.config.mau_limits_reserved_threepids = threepids - self.store.runInteraction( + self.store.db.runInteraction( "initialise", self.store._initialise_reserved_users, threepids ) count = self.store.get_monthly_active_count() @@ -244,7 +244,7 @@ class MonthlyActiveUsersTestCase(unittest.HomeserverTestCase): {"medium": "email", "address": user2_email}, ] self.hs.config.mau_limits_reserved_threepids = threepids - self.store.runInteraction( + self.store.db.runInteraction( "initialise", self.store._initialise_reserved_users, threepids ) diff --git a/tests/storage/test_redaction.py b/tests/storage/test_redaction.py index 4930b6777e..dc45173355 100644 --- a/tests/storage/test_redaction.py +++ b/tests/storage/test_redaction.py @@ -338,7 +338,7 @@ class RedactionTestCase(unittest.HomeserverTestCase): ) event_json = self.get_success( - self.store.simple_select_one_onecol( + self.store.db.simple_select_one_onecol( table="event_json", keyvalues={"event_id": msg_event.event_id}, retcol="json", @@ -356,7 +356,7 @@ class RedactionTestCase(unittest.HomeserverTestCase): self.reactor.advance(60 * 60 * 2) event_json = self.get_success( - self.store.simple_select_one_onecol( + self.store.db.simple_select_one_onecol( table="event_json", keyvalues={"event_id": msg_event.event_id}, retcol="json", diff --git a/tests/storage/test_roommember.py b/tests/storage/test_roommember.py index d389cf578f..5f957680a2 100644 --- a/tests/storage/test_roommember.py +++ b/tests/storage/test_roommember.py @@ -132,7 +132,7 @@ class CurrentStateMembershipUpdateTestCase(unittest.HomeserverTestCase): # Register the background update to run again. self.get_success( - self.store.simple_insert( + self.store.db.simple_insert( table="background_updates", values={ "update_name": "current_state_events_membership", diff --git a/tests/unittest.py b/tests/unittest.py index 295573bc46..fc856a574a 100644 --- a/tests/unittest.py +++ b/tests/unittest.py @@ -544,7 +544,7 @@ class HomeserverTestCase(TestCase): Add the given event as an extremity to the room. """ self.get_success( - self.hs.get_datastore().simple_insert( + self.hs.get_datastore().db.simple_insert( table="event_forward_extremities", values={"room_id": room_id, "event_id": event_id}, desc="test_add_extremity", From 8863624f7852ffc4a261aa9d17f6f7ddb5bf0c19 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 4 Dec 2019 14:00:29 +0000 Subject: [PATCH 0596/1623] Comments --- synapse/storage/__init__.py | 8 ++++---- synapse/storage/_base.py | 8 +++++++- synapse/storage/database.py | 5 +++++ 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/synapse/storage/__init__.py b/synapse/storage/__init__.py index 0460fe8cc9..8fb18203dc 100644 --- a/synapse/storage/__init__.py +++ b/synapse/storage/__init__.py @@ -17,10 +17,10 @@ """ The storage layer is split up into multiple parts to allow Synapse to run against different configurations of databases (e.g. single or multiple -databases). The `data_stores` are classes that talk directly to a single -database and have associated schemas, background updates, etc. On top of those -there are (or will be) classes that provide high level interfaces that combine -calls to multiple `data_stores`. +databases). The `Database` class represents a single physical database. The +`data_stores` are classes that talk directly to a `Database` instance and have +associated schemas, background updates, etc. On top of those there are classes +that provide high level interfaces that combine calls to multiple `data_stores`. There are also schemas that get applied to every database, regardless of the data stores associated with them (e.g. the schema version tables), which are diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py index fd5bb3e1de..b7e27d4e97 100644 --- a/synapse/storage/_base.py +++ b/synapse/storage/_base.py @@ -31,11 +31,17 @@ logger = logging.getLogger(__name__) class SQLBaseStore(object): + """Base class for data stores that holds helper functions. + + Note that multiple instances of this class will exist as there will be one + per data store (and not one per physical database). + """ + def __init__(self, db_conn, hs): self.hs = hs self._clock = hs.get_clock() self.database_engine = hs.database_engine - self.db = Database(hs) + self.db = Database(hs) # In future this will be passed in self.rand = random.SystemRandom() def _invalidate_state_caches(self, room_id, members_changed): diff --git a/synapse/storage/database.py b/synapse/storage/database.py index c2e121a001..ac64d80806 100644 --- a/synapse/storage/database.py +++ b/synapse/storage/database.py @@ -211,6 +211,11 @@ class PerformanceCounters(object): class Database(object): + """Wraps a single physical database and connection pool. + + A single database may be used by multiple data stores. + """ + _TXN_ID = 0 def __init__(self, hs): From 4a33a6dd19590b8e6626a5af5a69507dc11236f8 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 4 Dec 2019 15:09:36 +0000 Subject: [PATCH 0597/1623] Move background update handling out of store --- synapse/app/homeserver.py | 2 +- synapse/rest/media/v1/preview_url_resource.py | 2 +- synapse/storage/background_updates.py | 15 +++--- .../storage/data_stores/main/client_ips.py | 36 ++++++------- .../storage/data_stores/main/deviceinbox.py | 9 ++-- synapse/storage/data_stores/main/devices.py | 15 +++--- .../data_stores/main/event_federation.py | 6 +-- .../data_stores/main/event_push_actions.py | 4 +- synapse/storage/data_stores/main/events.py | 6 +-- .../data_stores/main/events_bg_updates.py | 49 ++++++++++-------- .../data_stores/main/media_repository.py | 6 +-- .../storage/data_stores/main/registration.py | 21 ++++---- synapse/storage/data_stores/main/room.py | 9 ++-- .../storage/data_stores/main/roommember.py | 27 ++++++---- synapse/storage/data_stores/main/search.py | 31 +++++++----- synapse/storage/data_stores/main/state.py | 19 +++---- synapse/storage/data_stores/main/stats.py | 20 ++++---- .../data_stores/main/user_directory.py | 33 +++++++----- synapse/storage/database.py | 3 ++ synmark/__init__.py | 6 +-- tests/handlers/test_stats.py | 50 +++++++++++++------ tests/handlers/test_user_directory.py | 18 +++++-- tests/storage/test_background_update.py | 26 ++++++---- tests/storage/test_cleanup_extrems.py | 14 ++++-- tests/storage/test_client_ips.py | 26 +++++++--- tests/storage/test_roommember.py | 18 +++++-- tests/unittest.py | 10 ++-- 27 files changed, 281 insertions(+), 200 deletions(-) diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py index 267aebaae9..9f81a857ab 100644 --- a/synapse/app/homeserver.py +++ b/synapse/app/homeserver.py @@ -436,7 +436,7 @@ def setup(config_options): _base.start(hs, config.listeners) hs.get_pusherpool().start() - hs.get_datastore().start_doing_background_updates() + hs.get_datastore().db.updates.start_doing_background_updates() except Exception: # Print the exception and bail out. print("Error during startup:", file=sys.stderr) diff --git a/synapse/rest/media/v1/preview_url_resource.py b/synapse/rest/media/v1/preview_url_resource.py index fb0d02aa83..6b978be876 100644 --- a/synapse/rest/media/v1/preview_url_resource.py +++ b/synapse/rest/media/v1/preview_url_resource.py @@ -402,7 +402,7 @@ class PreviewUrlResource(DirectServeResource): logger.info("Running url preview cache expiry") - if not (yield self.store.has_completed_background_updates()): + if not (yield self.store.db.updates.has_completed_background_updates()): logger.info("Still running DB updates; skipping expiry") return diff --git a/synapse/storage/background_updates.py b/synapse/storage/background_updates.py index dfca94b0e0..a9a13a2658 100644 --- a/synapse/storage/background_updates.py +++ b/synapse/storage/background_updates.py @@ -22,7 +22,6 @@ from twisted.internet import defer from synapse.metrics.background_process_metrics import run_as_background_process from . import engines -from ._base import SQLBaseStore logger = logging.getLogger(__name__) @@ -74,7 +73,7 @@ class BackgroundUpdatePerformance(object): return float(self.total_item_count) / float(self.total_duration_ms) -class BackgroundUpdateStore(SQLBaseStore): +class BackgroundUpdater(object): """ Background updates are updates to the database that run in the background. Each update processes a batch of data at once. We attempt to limit the impact of each update by monitoring how long each batch takes to @@ -86,8 +85,10 @@ class BackgroundUpdateStore(SQLBaseStore): BACKGROUND_UPDATE_INTERVAL_MS = 1000 BACKGROUND_UPDATE_DURATION_MS = 100 - def __init__(self, db_conn, hs): - super(BackgroundUpdateStore, self).__init__(db_conn, hs) + def __init__(self, hs, database): + self._clock = hs.get_clock() + self.db = database + self._background_update_performance = {} self._background_update_queue = [] self._background_update_handlers = {} @@ -101,9 +102,7 @@ class BackgroundUpdateStore(SQLBaseStore): logger.info("Starting background schema updates") while True: if sleep: - yield self.hs.get_clock().sleep( - self.BACKGROUND_UPDATE_INTERVAL_MS / 1000.0 - ) + yield self._clock.sleep(self.BACKGROUND_UPDATE_INTERVAL_MS / 1000.0) try: result = yield self.do_next_background_update( @@ -380,7 +379,7 @@ class BackgroundUpdateStore(SQLBaseStore): logger.debug("[SQL] %s", sql) c.execute(sql) - if isinstance(self.database_engine, engines.PostgresEngine): + if isinstance(self.db.database_engine, engines.PostgresEngine): runner = create_index_psql elif psql_only: runner = None diff --git a/synapse/storage/data_stores/main/client_ips.py b/synapse/storage/data_stores/main/client_ips.py index 6f2a720b97..7b470a58f1 100644 --- a/synapse/storage/data_stores/main/client_ips.py +++ b/synapse/storage/data_stores/main/client_ips.py @@ -20,7 +20,7 @@ from six import iteritems from twisted.internet import defer from synapse.metrics.background_process_metrics import wrap_as_background_process -from synapse.storage import background_updates +from synapse.storage._base import SQLBaseStore from synapse.util.caches import CACHE_SIZE_FACTOR from synapse.util.caches.descriptors import Cache @@ -32,41 +32,41 @@ logger = logging.getLogger(__name__) LAST_SEEN_GRANULARITY = 120 * 1000 -class ClientIpBackgroundUpdateStore(background_updates.BackgroundUpdateStore): +class ClientIpBackgroundUpdateStore(SQLBaseStore): def __init__(self, db_conn, hs): super(ClientIpBackgroundUpdateStore, self).__init__(db_conn, hs) - self.register_background_index_update( + self.db.updates.register_background_index_update( "user_ips_device_index", index_name="user_ips_device_id", table="user_ips", columns=["user_id", "device_id", "last_seen"], ) - self.register_background_index_update( + self.db.updates.register_background_index_update( "user_ips_last_seen_index", index_name="user_ips_last_seen", table="user_ips", columns=["user_id", "last_seen"], ) - self.register_background_index_update( + self.db.updates.register_background_index_update( "user_ips_last_seen_only_index", index_name="user_ips_last_seen_only", table="user_ips", columns=["last_seen"], ) - self.register_background_update_handler( + self.db.updates.register_background_update_handler( "user_ips_analyze", self._analyze_user_ip ) - self.register_background_update_handler( + self.db.updates.register_background_update_handler( "user_ips_remove_dupes", self._remove_user_ip_dupes ) # Register a unique index - self.register_background_index_update( + self.db.updates.register_background_index_update( "user_ips_device_unique_index", index_name="user_ips_user_token_ip_unique_index", table="user_ips", @@ -75,12 +75,12 @@ class ClientIpBackgroundUpdateStore(background_updates.BackgroundUpdateStore): ) # Drop the old non-unique index - self.register_background_update_handler( + self.db.updates.register_background_update_handler( "user_ips_drop_nonunique_index", self._remove_user_ip_nonunique ) # Update the last seen info in devices. - self.register_background_update_handler( + self.db.updates.register_background_update_handler( "devices_last_seen", self._devices_last_seen_update ) @@ -92,7 +92,7 @@ class ClientIpBackgroundUpdateStore(background_updates.BackgroundUpdateStore): txn.close() yield self.db.runWithConnection(f) - yield self._end_background_update("user_ips_drop_nonunique_index") + yield self.db.updates._end_background_update("user_ips_drop_nonunique_index") return 1 @defer.inlineCallbacks @@ -108,7 +108,7 @@ class ClientIpBackgroundUpdateStore(background_updates.BackgroundUpdateStore): yield self.db.runInteraction("user_ips_analyze", user_ips_analyze) - yield self._end_background_update("user_ips_analyze") + yield self.db.updates._end_background_update("user_ips_analyze") return 1 @@ -271,14 +271,14 @@ class ClientIpBackgroundUpdateStore(background_updates.BackgroundUpdateStore): (user_id, access_token, ip, device_id, user_agent, last_seen), ) - self._background_update_progress_txn( + self.db.updates._background_update_progress_txn( txn, "user_ips_remove_dupes", {"last_seen": end_last_seen} ) yield self.db.runInteraction("user_ips_dups_remove", remove) if last: - yield self._end_background_update("user_ips_remove_dupes") + yield self.db.updates._end_background_update("user_ips_remove_dupes") return batch_size @@ -344,7 +344,7 @@ class ClientIpBackgroundUpdateStore(background_updates.BackgroundUpdateStore): txn.execute_batch(sql, rows) _, _, _, user_id, device_id = rows[-1] - self._background_update_progress_txn( + self.db.updates._background_update_progress_txn( txn, "devices_last_seen", {"last_user_id": user_id, "last_device_id": device_id}, @@ -357,7 +357,7 @@ class ClientIpBackgroundUpdateStore(background_updates.BackgroundUpdateStore): ) if not updated: - yield self._end_background_update("devices_last_seen") + yield self.db.updates._end_background_update("devices_last_seen") return updated @@ -546,7 +546,9 @@ class ClientIpStore(ClientIpBackgroundUpdateStore): # Nothing to do return - if not await self.has_completed_background_update("devices_last_seen"): + if not await self.db.updates.has_completed_background_update( + "devices_last_seen" + ): # Only start pruning if we have finished populating the devices # last seen info. return diff --git a/synapse/storage/data_stores/main/deviceinbox.py b/synapse/storage/data_stores/main/deviceinbox.py index 440793ad49..3c9f09301a 100644 --- a/synapse/storage/data_stores/main/deviceinbox.py +++ b/synapse/storage/data_stores/main/deviceinbox.py @@ -21,7 +21,6 @@ from twisted.internet import defer from synapse.logging.opentracing import log_kv, set_tag, trace from synapse.storage._base import SQLBaseStore, make_in_list_sql_clause -from synapse.storage.background_updates import BackgroundUpdateStore from synapse.util.caches.expiringcache import ExpiringCache logger = logging.getLogger(__name__) @@ -208,20 +207,20 @@ class DeviceInboxWorkerStore(SQLBaseStore): ) -class DeviceInboxBackgroundUpdateStore(BackgroundUpdateStore): +class DeviceInboxBackgroundUpdateStore(SQLBaseStore): DEVICE_INBOX_STREAM_ID = "device_inbox_stream_drop" def __init__(self, db_conn, hs): super(DeviceInboxBackgroundUpdateStore, self).__init__(db_conn, hs) - self.register_background_index_update( + self.db.updates.register_background_index_update( "device_inbox_stream_index", index_name="device_inbox_stream_id_user_id", table="device_inbox", columns=["stream_id", "user_id"], ) - self.register_background_update_handler( + self.db.updates.register_background_update_handler( self.DEVICE_INBOX_STREAM_ID, self._background_drop_index_device_inbox ) @@ -234,7 +233,7 @@ class DeviceInboxBackgroundUpdateStore(BackgroundUpdateStore): yield self.db.runWithConnection(reindex_txn) - yield self._end_background_update(self.DEVICE_INBOX_STREAM_ID) + yield self.db.updates._end_background_update(self.DEVICE_INBOX_STREAM_ID) return 1 diff --git a/synapse/storage/data_stores/main/devices.py b/synapse/storage/data_stores/main/devices.py index d98511ddd4..91ddaf137e 100644 --- a/synapse/storage/data_stores/main/devices.py +++ b/synapse/storage/data_stores/main/devices.py @@ -31,7 +31,6 @@ from synapse.logging.opentracing import ( ) from synapse.metrics.background_process_metrics import run_as_background_process from synapse.storage._base import SQLBaseStore, db_to_json, make_in_list_sql_clause -from synapse.storage.background_updates import BackgroundUpdateStore from synapse.types import get_verify_key_from_cross_signing_key from synapse.util import batch_iter from synapse.util.caches.descriptors import ( @@ -642,11 +641,11 @@ class DeviceWorkerStore(SQLBaseStore): return results -class DeviceBackgroundUpdateStore(BackgroundUpdateStore): +class DeviceBackgroundUpdateStore(SQLBaseStore): def __init__(self, db_conn, hs): super(DeviceBackgroundUpdateStore, self).__init__(db_conn, hs) - self.register_background_index_update( + self.db.updates.register_background_index_update( "device_lists_stream_idx", index_name="device_lists_stream_user_id", table="device_lists_stream", @@ -654,7 +653,7 @@ class DeviceBackgroundUpdateStore(BackgroundUpdateStore): ) # create a unique index on device_lists_remote_cache - self.register_background_index_update( + self.db.updates.register_background_index_update( "device_lists_remote_cache_unique_idx", index_name="device_lists_remote_cache_unique_id", table="device_lists_remote_cache", @@ -663,7 +662,7 @@ class DeviceBackgroundUpdateStore(BackgroundUpdateStore): ) # And one on device_lists_remote_extremeties - self.register_background_index_update( + self.db.updates.register_background_index_update( "device_lists_remote_extremeties_unique_idx", index_name="device_lists_remote_extremeties_unique_idx", table="device_lists_remote_extremeties", @@ -672,7 +671,7 @@ class DeviceBackgroundUpdateStore(BackgroundUpdateStore): ) # once they complete, we can remove the old non-unique indexes. - self.register_background_update_handler( + self.db.updates.register_background_update_handler( DROP_DEVICE_LIST_STREAMS_NON_UNIQUE_INDEXES, self._drop_device_list_streams_non_unique_indexes, ) @@ -686,7 +685,9 @@ class DeviceBackgroundUpdateStore(BackgroundUpdateStore): txn.close() yield self.db.runWithConnection(f) - yield self._end_background_update(DROP_DEVICE_LIST_STREAMS_NON_UNIQUE_INDEXES) + yield self.db.updates._end_background_update( + DROP_DEVICE_LIST_STREAMS_NON_UNIQUE_INDEXES + ) return 1 diff --git a/synapse/storage/data_stores/main/event_federation.py b/synapse/storage/data_stores/main/event_federation.py index 77e4353b59..31d2e8eb28 100644 --- a/synapse/storage/data_stores/main/event_federation.py +++ b/synapse/storage/data_stores/main/event_federation.py @@ -494,7 +494,7 @@ class EventFederationStore(EventFederationWorkerStore): def __init__(self, db_conn, hs): super(EventFederationStore, self).__init__(db_conn, hs) - self.register_background_update_handler( + self.db.updates.register_background_update_handler( self.EVENT_AUTH_STATE_ONLY, self._background_delete_non_state_event_auth ) @@ -654,7 +654,7 @@ class EventFederationStore(EventFederationWorkerStore): "max_stream_id_exclusive": min_stream_id, } - self._background_update_progress_txn( + self.db.updates._background_update_progress_txn( txn, self.EVENT_AUTH_STATE_ONLY, new_progress ) @@ -665,6 +665,6 @@ class EventFederationStore(EventFederationWorkerStore): ) if not result: - yield self._end_background_update(self.EVENT_AUTH_STATE_ONLY) + yield self.db.updates._end_background_update(self.EVENT_AUTH_STATE_ONLY) return batch_size diff --git a/synapse/storage/data_stores/main/event_push_actions.py b/synapse/storage/data_stores/main/event_push_actions.py index 725d0881dc..eec054cd48 100644 --- a/synapse/storage/data_stores/main/event_push_actions.py +++ b/synapse/storage/data_stores/main/event_push_actions.py @@ -614,14 +614,14 @@ class EventPushActionsStore(EventPushActionsWorkerStore): def __init__(self, db_conn, hs): super(EventPushActionsStore, self).__init__(db_conn, hs) - self.register_background_index_update( + self.db.updates.register_background_index_update( self.EPA_HIGHLIGHT_INDEX, index_name="event_push_actions_u_highlight", table="event_push_actions", columns=["user_id", "stream_ordering"], ) - self.register_background_index_update( + self.db.updates.register_background_index_update( "event_push_actions_highlights_index", index_name="event_push_actions_highlights_index", table="event_push_actions", diff --git a/synapse/storage/data_stores/main/events.py b/synapse/storage/data_stores/main/events.py index 01ec9ec397..d644c82784 100644 --- a/synapse/storage/data_stores/main/events.py +++ b/synapse/storage/data_stores/main/events.py @@ -38,7 +38,6 @@ from synapse.logging.utils import log_function from synapse.metrics import BucketCollector from synapse.metrics.background_process_metrics import run_as_background_process from synapse.storage._base import make_in_list_sql_clause -from synapse.storage.background_updates import BackgroundUpdateStore from synapse.storage.data_stores.main.event_federation import EventFederationStore from synapse.storage.data_stores.main.events_worker import EventsWorkerStore from synapse.storage.data_stores.main.state import StateGroupWorkerStore @@ -94,10 +93,7 @@ def _retry_on_integrity_error(func): # inherits from EventFederationStore so that we can call _update_backward_extremities # and _handle_mult_prev_events (though arguably those could both be moved in here) class EventsStore( - StateGroupWorkerStore, - EventFederationStore, - EventsWorkerStore, - BackgroundUpdateStore, + StateGroupWorkerStore, EventFederationStore, EventsWorkerStore, ): def __init__(self, db_conn, hs): super(EventsStore, self).__init__(db_conn, hs) diff --git a/synapse/storage/data_stores/main/events_bg_updates.py b/synapse/storage/data_stores/main/events_bg_updates.py index 365e966956..cb1fc30c31 100644 --- a/synapse/storage/data_stores/main/events_bg_updates.py +++ b/synapse/storage/data_stores/main/events_bg_updates.py @@ -22,13 +22,12 @@ from canonicaljson import json from twisted.internet import defer from synapse.api.constants import EventContentFields -from synapse.storage._base import make_in_list_sql_clause -from synapse.storage.background_updates import BackgroundUpdateStore +from synapse.storage._base import SQLBaseStore, make_in_list_sql_clause logger = logging.getLogger(__name__) -class EventsBackgroundUpdatesStore(BackgroundUpdateStore): +class EventsBackgroundUpdatesStore(SQLBaseStore): EVENT_ORIGIN_SERVER_TS_NAME = "event_origin_server_ts" EVENT_FIELDS_SENDER_URL_UPDATE_NAME = "event_fields_sender_url" @@ -37,15 +36,15 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): def __init__(self, db_conn, hs): super(EventsBackgroundUpdatesStore, self).__init__(db_conn, hs) - self.register_background_update_handler( + self.db.updates.register_background_update_handler( self.EVENT_ORIGIN_SERVER_TS_NAME, self._background_reindex_origin_server_ts ) - self.register_background_update_handler( + self.db.updates.register_background_update_handler( self.EVENT_FIELDS_SENDER_URL_UPDATE_NAME, self._background_reindex_fields_sender, ) - self.register_background_index_update( + self.db.updates.register_background_index_update( "event_contains_url_index", index_name="event_contains_url_index", table="events", @@ -56,7 +55,7 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): # an event_id index on event_search is useful for the purge_history # api. Plus it means we get to enforce some integrity with a UNIQUE # clause - self.register_background_index_update( + self.db.updates.register_background_index_update( "event_search_event_id_idx", index_name="event_search_event_id_idx", table="event_search", @@ -65,16 +64,16 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): psql_only=True, ) - self.register_background_update_handler( + self.db.updates.register_background_update_handler( self.DELETE_SOFT_FAILED_EXTREMITIES, self._cleanup_extremities_bg_update ) - self.register_background_update_handler( + self.db.updates.register_background_update_handler( "redactions_received_ts", self._redactions_received_ts ) # This index gets deleted in `event_fix_redactions_bytes` update - self.register_background_index_update( + self.db.updates.register_background_index_update( "event_fix_redactions_bytes_create_index", index_name="redactions_censored_redacts", table="redactions", @@ -82,11 +81,11 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): where_clause="have_censored", ) - self.register_background_update_handler( + self.db.updates.register_background_update_handler( "event_fix_redactions_bytes", self._event_fix_redactions_bytes ) - self.register_background_update_handler( + self.db.updates.register_background_update_handler( "event_store_labels", self._event_store_labels ) @@ -145,7 +144,7 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): "rows_inserted": rows_inserted + len(rows), } - self._background_update_progress_txn( + self.db.updates._background_update_progress_txn( txn, self.EVENT_FIELDS_SENDER_URL_UPDATE_NAME, progress ) @@ -156,7 +155,9 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): ) if not result: - yield self._end_background_update(self.EVENT_FIELDS_SENDER_URL_UPDATE_NAME) + yield self.db.updates._end_background_update( + self.EVENT_FIELDS_SENDER_URL_UPDATE_NAME + ) return result @@ -222,7 +223,7 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): "rows_inserted": rows_inserted + len(rows_to_update), } - self._background_update_progress_txn( + self.db.updates._background_update_progress_txn( txn, self.EVENT_ORIGIN_SERVER_TS_NAME, progress ) @@ -233,7 +234,9 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): ) if not result: - yield self._end_background_update(self.EVENT_ORIGIN_SERVER_TS_NAME) + yield self.db.updates._end_background_update( + self.EVENT_ORIGIN_SERVER_TS_NAME + ) return result @@ -411,7 +414,9 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): ) if not num_handled: - yield self._end_background_update(self.DELETE_SOFT_FAILED_EXTREMITIES) + yield self.db.updates._end_background_update( + self.DELETE_SOFT_FAILED_EXTREMITIES + ) def _drop_table_txn(txn): txn.execute("DROP TABLE _extremities_to_check") @@ -464,7 +469,7 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): txn.execute(sql, (self._clock.time_msec(), last_event_id, upper_event_id)) - self._background_update_progress_txn( + self.db.updates._background_update_progress_txn( txn, "redactions_received_ts", {"last_event_id": upper_event_id} ) @@ -475,7 +480,7 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): ) if not count: - yield self._end_background_update("redactions_received_ts") + yield self.db.updates._end_background_update("redactions_received_ts") return count @@ -505,7 +510,7 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): "_event_fix_redactions_bytes", _event_fix_redactions_bytes_txn ) - yield self._end_background_update("event_fix_redactions_bytes") + yield self.db.updates._end_background_update("event_fix_redactions_bytes") return 1 @@ -559,7 +564,7 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): nbrows += 1 last_row_event_id = event_id - self._background_update_progress_txn( + self.db.updates._background_update_progress_txn( txn, "event_store_labels", {"last_event_id": last_row_event_id} ) @@ -570,6 +575,6 @@ class EventsBackgroundUpdatesStore(BackgroundUpdateStore): ) if not num_rows: - yield self._end_background_update("event_store_labels") + yield self.db.updates._end_background_update("event_store_labels") return num_rows diff --git a/synapse/storage/data_stores/main/media_repository.py b/synapse/storage/data_stores/main/media_repository.py index ea02497784..03c9c6f8ae 100644 --- a/synapse/storage/data_stores/main/media_repository.py +++ b/synapse/storage/data_stores/main/media_repository.py @@ -12,14 +12,14 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from synapse.storage.background_updates import BackgroundUpdateStore +from synapse.storage._base import SQLBaseStore -class MediaRepositoryBackgroundUpdateStore(BackgroundUpdateStore): +class MediaRepositoryBackgroundUpdateStore(SQLBaseStore): def __init__(self, db_conn, hs): super(MediaRepositoryBackgroundUpdateStore, self).__init__(db_conn, hs) - self.register_background_index_update( + self.db.updates.register_background_index_update( update_name="local_media_repository_url_idx", index_name="local_media_repository_url_idx", table="local_media_repository", diff --git a/synapse/storage/data_stores/main/registration.py b/synapse/storage/data_stores/main/registration.py index 8f9aa87ceb..1ef143c6d8 100644 --- a/synapse/storage/data_stores/main/registration.py +++ b/synapse/storage/data_stores/main/registration.py @@ -26,7 +26,6 @@ from twisted.internet.defer import Deferred from synapse.api.constants import UserTypes from synapse.api.errors import Codes, StoreError, SynapseError, ThreepidValidationError from synapse.metrics.background_process_metrics import run_as_background_process -from synapse.storage import background_updates from synapse.storage._base import SQLBaseStore from synapse.types import UserID from synapse.util.caches.descriptors import cached, cachedInlineCallbacks @@ -794,23 +793,21 @@ class RegistrationWorkerStore(SQLBaseStore): ) -class RegistrationBackgroundUpdateStore( - RegistrationWorkerStore, background_updates.BackgroundUpdateStore -): +class RegistrationBackgroundUpdateStore(RegistrationWorkerStore): def __init__(self, db_conn, hs): super(RegistrationBackgroundUpdateStore, self).__init__(db_conn, hs) self.clock = hs.get_clock() self.config = hs.config - self.register_background_index_update( + self.db.updates.register_background_index_update( "access_tokens_device_index", index_name="access_tokens_device_id", table="access_tokens", columns=["user_id", "device_id"], ) - self.register_background_index_update( + self.db.updates.register_background_index_update( "users_creation_ts", index_name="users_creation_ts", table="users", @@ -820,13 +817,13 @@ class RegistrationBackgroundUpdateStore( # we no longer use refresh tokens, but it's possible that some people # might have a background update queued to build this index. Just # clear the background update. - self.register_noop_background_update("refresh_tokens_device_index") + self.db.updates.register_noop_background_update("refresh_tokens_device_index") - self.register_background_update_handler( + self.db.updates.register_background_update_handler( "user_threepids_grandfather", self._bg_user_threepids_grandfather ) - self.register_background_update_handler( + self.db.updates.register_background_update_handler( "users_set_deactivated_flag", self._background_update_set_deactivated_flag ) @@ -873,7 +870,7 @@ class RegistrationBackgroundUpdateStore( logger.info("Marked %d rows as deactivated", rows_processed_nb) - self._background_update_progress_txn( + self.db.updates._background_update_progress_txn( txn, "users_set_deactivated_flag", {"user_id": rows[-1]["name"]} ) @@ -887,7 +884,7 @@ class RegistrationBackgroundUpdateStore( ) if end: - yield self._end_background_update("users_set_deactivated_flag") + yield self.db.updates._end_background_update("users_set_deactivated_flag") return nb_processed @@ -917,7 +914,7 @@ class RegistrationBackgroundUpdateStore( "_bg_user_threepids_grandfather", _bg_user_threepids_grandfather_txn ) - yield self._end_background_update("user_threepids_grandfather") + yield self.db.updates._end_background_update("user_threepids_grandfather") return 1 diff --git a/synapse/storage/data_stores/main/room.py b/synapse/storage/data_stores/main/room.py index a26ed47afc..da42dae243 100644 --- a/synapse/storage/data_stores/main/room.py +++ b/synapse/storage/data_stores/main/room.py @@ -28,7 +28,6 @@ from twisted.internet import defer from synapse.api.constants import EventTypes from synapse.api.errors import StoreError from synapse.storage._base import SQLBaseStore -from synapse.storage.background_updates import BackgroundUpdateStore from synapse.storage.data_stores.main.search import SearchStore from synapse.types import ThirdPartyInstanceID from synapse.util.caches.descriptors import cached, cachedInlineCallbacks @@ -361,13 +360,13 @@ class RoomWorkerStore(SQLBaseStore): defer.returnValue(row) -class RoomBackgroundUpdateStore(BackgroundUpdateStore): +class RoomBackgroundUpdateStore(SQLBaseStore): def __init__(self, db_conn, hs): super(RoomBackgroundUpdateStore, self).__init__(db_conn, hs) self.config = hs.config - self.register_background_update_handler( + self.db.updates.register_background_update_handler( "insert_room_retention", self._background_insert_retention, ) @@ -421,7 +420,7 @@ class RoomBackgroundUpdateStore(BackgroundUpdateStore): logger.info("Inserted %d rows into room_retention", len(rows)) - self._background_update_progress_txn( + self.db.updates._background_update_progress_txn( txn, "insert_room_retention", {"room_id": rows[-1]["room_id"]} ) @@ -435,7 +434,7 @@ class RoomBackgroundUpdateStore(BackgroundUpdateStore): ) if end: - yield self._end_background_update("insert_room_retention") + yield self.db.updates._end_background_update("insert_room_retention") defer.returnValue(batch_size) diff --git a/synapse/storage/data_stores/main/roommember.py b/synapse/storage/data_stores/main/roommember.py index 7f4d02b25b..929f6b0d39 100644 --- a/synapse/storage/data_stores/main/roommember.py +++ b/synapse/storage/data_stores/main/roommember.py @@ -26,8 +26,11 @@ from twisted.internet import defer from synapse.api.constants import EventTypes, Membership from synapse.metrics import LaterGauge from synapse.metrics.background_process_metrics import run_as_background_process -from synapse.storage._base import LoggingTransaction, make_in_list_sql_clause -from synapse.storage.background_updates import BackgroundUpdateStore +from synapse.storage._base import ( + LoggingTransaction, + SQLBaseStore, + make_in_list_sql_clause, +) from synapse.storage.data_stores.main.events_worker import EventsWorkerStore from synapse.storage.engines import Sqlite3Engine from synapse.storage.roommember import ( @@ -831,17 +834,17 @@ class RoomMemberWorkerStore(EventsWorkerStore): ) -class RoomMemberBackgroundUpdateStore(BackgroundUpdateStore): +class RoomMemberBackgroundUpdateStore(SQLBaseStore): def __init__(self, db_conn, hs): super(RoomMemberBackgroundUpdateStore, self).__init__(db_conn, hs) - self.register_background_update_handler( + self.db.updates.register_background_update_handler( _MEMBERSHIP_PROFILE_UPDATE_NAME, self._background_add_membership_profile ) - self.register_background_update_handler( + self.db.updates.register_background_update_handler( _CURRENT_STATE_MEMBERSHIP_UPDATE_NAME, self._background_current_state_membership, ) - self.register_background_index_update( + self.db.updates.register_background_index_update( "room_membership_forgotten_idx", index_name="room_memberships_user_room_forgotten", table="room_memberships", @@ -909,7 +912,7 @@ class RoomMemberBackgroundUpdateStore(BackgroundUpdateStore): "max_stream_id_exclusive": min_stream_id, } - self._background_update_progress_txn( + self.db.updates._background_update_progress_txn( txn, _MEMBERSHIP_PROFILE_UPDATE_NAME, progress ) @@ -920,7 +923,9 @@ class RoomMemberBackgroundUpdateStore(BackgroundUpdateStore): ) if not result: - yield self._end_background_update(_MEMBERSHIP_PROFILE_UPDATE_NAME) + yield self.db.updates._end_background_update( + _MEMBERSHIP_PROFILE_UPDATE_NAME + ) return result @@ -959,7 +964,7 @@ class RoomMemberBackgroundUpdateStore(BackgroundUpdateStore): last_processed_room = next_room - self._background_update_progress_txn( + self.db.updates._background_update_progress_txn( txn, _CURRENT_STATE_MEMBERSHIP_UPDATE_NAME, {"last_processed_room": last_processed_room}, @@ -978,7 +983,9 @@ class RoomMemberBackgroundUpdateStore(BackgroundUpdateStore): ) if finished: - yield self._end_background_update(_CURRENT_STATE_MEMBERSHIP_UPDATE_NAME) + yield self.db.updates._end_background_update( + _CURRENT_STATE_MEMBERSHIP_UPDATE_NAME + ) return row_count diff --git a/synapse/storage/data_stores/main/search.py b/synapse/storage/data_stores/main/search.py index 55a604850e..ffa1817e64 100644 --- a/synapse/storage/data_stores/main/search.py +++ b/synapse/storage/data_stores/main/search.py @@ -24,8 +24,7 @@ from canonicaljson import json from twisted.internet import defer from synapse.api.errors import SynapseError -from synapse.storage._base import make_in_list_sql_clause -from synapse.storage.background_updates import BackgroundUpdateStore +from synapse.storage._base import SQLBaseStore, make_in_list_sql_clause from synapse.storage.engines import PostgresEngine, Sqlite3Engine logger = logging.getLogger(__name__) @@ -36,7 +35,7 @@ SearchEntry = namedtuple( ) -class SearchBackgroundUpdateStore(BackgroundUpdateStore): +class SearchBackgroundUpdateStore(SQLBaseStore): EVENT_SEARCH_UPDATE_NAME = "event_search" EVENT_SEARCH_ORDER_UPDATE_NAME = "event_search_order" @@ -49,10 +48,10 @@ class SearchBackgroundUpdateStore(BackgroundUpdateStore): if not hs.config.enable_search: return - self.register_background_update_handler( + self.db.updates.register_background_update_handler( self.EVENT_SEARCH_UPDATE_NAME, self._background_reindex_search ) - self.register_background_update_handler( + self.db.updates.register_background_update_handler( self.EVENT_SEARCH_ORDER_UPDATE_NAME, self._background_reindex_search_order ) @@ -61,9 +60,11 @@ class SearchBackgroundUpdateStore(BackgroundUpdateStore): # a GIN index. However, it's possible that some people might still have # the background update queued, so we register a handler to clear the # background update. - self.register_noop_background_update(self.EVENT_SEARCH_USE_GIST_POSTGRES_NAME) + self.db.updates.register_noop_background_update( + self.EVENT_SEARCH_USE_GIST_POSTGRES_NAME + ) - self.register_background_update_handler( + self.db.updates.register_background_update_handler( self.EVENT_SEARCH_USE_GIN_POSTGRES_NAME, self._background_reindex_gin_search ) @@ -153,7 +154,7 @@ class SearchBackgroundUpdateStore(BackgroundUpdateStore): "rows_inserted": rows_inserted + len(event_search_rows), } - self._background_update_progress_txn( + self.db.updates._background_update_progress_txn( txn, self.EVENT_SEARCH_UPDATE_NAME, progress ) @@ -164,7 +165,7 @@ class SearchBackgroundUpdateStore(BackgroundUpdateStore): ) if not result: - yield self._end_background_update(self.EVENT_SEARCH_UPDATE_NAME) + yield self.db.updates._end_background_update(self.EVENT_SEARCH_UPDATE_NAME) return result @@ -208,7 +209,9 @@ class SearchBackgroundUpdateStore(BackgroundUpdateStore): if isinstance(self.database_engine, PostgresEngine): yield self.db.runWithConnection(create_index) - yield self._end_background_update(self.EVENT_SEARCH_USE_GIN_POSTGRES_NAME) + yield self.db.updates._end_background_update( + self.EVENT_SEARCH_USE_GIN_POSTGRES_NAME + ) return 1 @defer.inlineCallbacks @@ -244,7 +247,7 @@ class SearchBackgroundUpdateStore(BackgroundUpdateStore): yield self.db.runInteraction( self.EVENT_SEARCH_ORDER_UPDATE_NAME, - self._background_update_progress_txn, + self.db.updates._background_update_progress_txn, self.EVENT_SEARCH_ORDER_UPDATE_NAME, pg, ) @@ -274,7 +277,7 @@ class SearchBackgroundUpdateStore(BackgroundUpdateStore): "have_added_indexes": True, } - self._background_update_progress_txn( + self.db.updates._background_update_progress_txn( txn, self.EVENT_SEARCH_ORDER_UPDATE_NAME, progress ) @@ -285,7 +288,9 @@ class SearchBackgroundUpdateStore(BackgroundUpdateStore): ) if not finished: - yield self._end_background_update(self.EVENT_SEARCH_ORDER_UPDATE_NAME) + yield self.db.updates._end_background_update( + self.EVENT_SEARCH_ORDER_UPDATE_NAME + ) return num_rows diff --git a/synapse/storage/data_stores/main/state.py b/synapse/storage/data_stores/main/state.py index 851e81d6b3..7d5a9f8128 100644 --- a/synapse/storage/data_stores/main/state.py +++ b/synapse/storage/data_stores/main/state.py @@ -27,7 +27,6 @@ from synapse.api.errors import NotFoundError from synapse.events import EventBase from synapse.events.snapshot import EventContext from synapse.storage._base import SQLBaseStore -from synapse.storage.background_updates import BackgroundUpdateStore from synapse.storage.data_stores.main.events_worker import EventsWorkerStore from synapse.storage.engines import PostgresEngine from synapse.storage.state import StateFilter @@ -1023,9 +1022,7 @@ class StateGroupWorkerStore( return set(row["state_group"] for row in rows) -class StateBackgroundUpdateStore( - StateGroupBackgroundUpdateStore, BackgroundUpdateStore -): +class StateBackgroundUpdateStore(StateGroupBackgroundUpdateStore): STATE_GROUP_DEDUPLICATION_UPDATE_NAME = "state_group_state_deduplication" STATE_GROUP_INDEX_UPDATE_NAME = "state_group_state_type_index" @@ -1034,21 +1031,21 @@ class StateBackgroundUpdateStore( def __init__(self, db_conn, hs): super(StateBackgroundUpdateStore, self).__init__(db_conn, hs) - self.register_background_update_handler( + self.db.updates.register_background_update_handler( self.STATE_GROUP_DEDUPLICATION_UPDATE_NAME, self._background_deduplicate_state, ) - self.register_background_update_handler( + self.db.updates.register_background_update_handler( self.STATE_GROUP_INDEX_UPDATE_NAME, self._background_index_state ) - self.register_background_index_update( + self.db.updates.register_background_index_update( self.CURRENT_STATE_INDEX_UPDATE_NAME, index_name="current_state_events_member_index", table="current_state_events", columns=["state_key"], where_clause="type='m.room.member'", ) - self.register_background_index_update( + self.db.updates.register_background_index_update( self.EVENT_STATE_GROUP_INDEX_UPDATE_NAME, index_name="event_to_state_groups_sg_index", table="event_to_state_groups", @@ -1181,7 +1178,7 @@ class StateBackgroundUpdateStore( "max_group": max_group, } - self._background_update_progress_txn( + self.db.updates._background_update_progress_txn( txn, self.STATE_GROUP_DEDUPLICATION_UPDATE_NAME, progress ) @@ -1192,7 +1189,7 @@ class StateBackgroundUpdateStore( ) if finished: - yield self._end_background_update( + yield self.db.updates._end_background_update( self.STATE_GROUP_DEDUPLICATION_UPDATE_NAME ) @@ -1224,7 +1221,7 @@ class StateBackgroundUpdateStore( yield self.db.runWithConnection(reindex_txn) - yield self._end_background_update(self.STATE_GROUP_INDEX_UPDATE_NAME) + yield self.db.updates._end_background_update(self.STATE_GROUP_INDEX_UPDATE_NAME) return 1 diff --git a/synapse/storage/data_stores/main/stats.py b/synapse/storage/data_stores/main/stats.py index 974ffc15bd..6b91988c2a 100644 --- a/synapse/storage/data_stores/main/stats.py +++ b/synapse/storage/data_stores/main/stats.py @@ -68,17 +68,17 @@ class StatsStore(StateDeltasStore): self.stats_delta_processing_lock = DeferredLock() - self.register_background_update_handler( + self.db.updates.register_background_update_handler( "populate_stats_process_rooms", self._populate_stats_process_rooms ) - self.register_background_update_handler( + self.db.updates.register_background_update_handler( "populate_stats_process_users", self._populate_stats_process_users ) # we no longer need to perform clean-up, but we will give ourselves # the potential to reintroduce it in the future – so documentation # will still encourage the use of this no-op handler. - self.register_noop_background_update("populate_stats_cleanup") - self.register_noop_background_update("populate_stats_prepare") + self.db.updates.register_noop_background_update("populate_stats_cleanup") + self.db.updates.register_noop_background_update("populate_stats_prepare") def quantise_stats_time(self, ts): """ @@ -102,7 +102,7 @@ class StatsStore(StateDeltasStore): This is a background update which regenerates statistics for users. """ if not self.stats_enabled: - yield self._end_background_update("populate_stats_process_users") + yield self.db.updates._end_background_update("populate_stats_process_users") return 1 last_user_id = progress.get("last_user_id", "") @@ -123,7 +123,7 @@ class StatsStore(StateDeltasStore): # No more rooms -- complete the transaction. if not users_to_work_on: - yield self._end_background_update("populate_stats_process_users") + yield self.db.updates._end_background_update("populate_stats_process_users") return 1 for user_id in users_to_work_on: @@ -132,7 +132,7 @@ class StatsStore(StateDeltasStore): yield self.db.runInteraction( "populate_stats_process_users", - self._background_update_progress_txn, + self.db.updates._background_update_progress_txn, "populate_stats_process_users", progress, ) @@ -145,7 +145,7 @@ class StatsStore(StateDeltasStore): This is a background update which regenerates statistics for rooms. """ if not self.stats_enabled: - yield self._end_background_update("populate_stats_process_rooms") + yield self.db.updates._end_background_update("populate_stats_process_rooms") return 1 last_room_id = progress.get("last_room_id", "") @@ -166,7 +166,7 @@ class StatsStore(StateDeltasStore): # No more rooms -- complete the transaction. if not rooms_to_work_on: - yield self._end_background_update("populate_stats_process_rooms") + yield self.db.updates._end_background_update("populate_stats_process_rooms") return 1 for room_id in rooms_to_work_on: @@ -175,7 +175,7 @@ class StatsStore(StateDeltasStore): yield self.db.runInteraction( "_populate_stats_process_rooms", - self._background_update_progress_txn, + self.db.updates._background_update_progress_txn, "populate_stats_process_rooms", progress, ) diff --git a/synapse/storage/data_stores/main/user_directory.py b/synapse/storage/data_stores/main/user_directory.py index 7118bd62f3..62ffb34b29 100644 --- a/synapse/storage/data_stores/main/user_directory.py +++ b/synapse/storage/data_stores/main/user_directory.py @@ -19,7 +19,6 @@ import re from twisted.internet import defer from synapse.api.constants import EventTypes, JoinRules -from synapse.storage.background_updates import BackgroundUpdateStore from synapse.storage.data_stores.main.state import StateFilter from synapse.storage.data_stores.main.state_deltas import StateDeltasStore from synapse.storage.engines import PostgresEngine, Sqlite3Engine @@ -32,7 +31,7 @@ logger = logging.getLogger(__name__) TEMP_TABLE = "_temp_populate_user_directory" -class UserDirectoryBackgroundUpdateStore(StateDeltasStore, BackgroundUpdateStore): +class UserDirectoryBackgroundUpdateStore(StateDeltasStore): # How many records do we calculate before sending it to # add_users_who_share_private_rooms? @@ -43,19 +42,19 @@ class UserDirectoryBackgroundUpdateStore(StateDeltasStore, BackgroundUpdateStore self.server_name = hs.hostname - self.register_background_update_handler( + self.db.updates.register_background_update_handler( "populate_user_directory_createtables", self._populate_user_directory_createtables, ) - self.register_background_update_handler( + self.db.updates.register_background_update_handler( "populate_user_directory_process_rooms", self._populate_user_directory_process_rooms, ) - self.register_background_update_handler( + self.db.updates.register_background_update_handler( "populate_user_directory_process_users", self._populate_user_directory_process_users, ) - self.register_background_update_handler( + self.db.updates.register_background_update_handler( "populate_user_directory_cleanup", self._populate_user_directory_cleanup ) @@ -108,7 +107,9 @@ class UserDirectoryBackgroundUpdateStore(StateDeltasStore, BackgroundUpdateStore ) yield self.db.simple_insert(TEMP_TABLE + "_position", {"position": new_pos}) - yield self._end_background_update("populate_user_directory_createtables") + yield self.db.updates._end_background_update( + "populate_user_directory_createtables" + ) return 1 @defer.inlineCallbacks @@ -130,7 +131,7 @@ class UserDirectoryBackgroundUpdateStore(StateDeltasStore, BackgroundUpdateStore "populate_user_directory_cleanup", _delete_staging_area ) - yield self._end_background_update("populate_user_directory_cleanup") + yield self.db.updates._end_background_update("populate_user_directory_cleanup") return 1 @defer.inlineCallbacks @@ -176,7 +177,9 @@ class UserDirectoryBackgroundUpdateStore(StateDeltasStore, BackgroundUpdateStore # No more rooms -- complete the transaction. if not rooms_to_work_on: - yield self._end_background_update("populate_user_directory_process_rooms") + yield self.db.updates._end_background_update( + "populate_user_directory_process_rooms" + ) return 1 logger.info( @@ -248,7 +251,7 @@ class UserDirectoryBackgroundUpdateStore(StateDeltasStore, BackgroundUpdateStore progress["remaining"] -= 1 yield self.db.runInteraction( "populate_user_directory", - self._background_update_progress_txn, + self.db.updates._background_update_progress_txn, "populate_user_directory_process_rooms", progress, ) @@ -267,7 +270,9 @@ class UserDirectoryBackgroundUpdateStore(StateDeltasStore, BackgroundUpdateStore If search_all_users is enabled, add all of the users to the user directory. """ if not self.hs.config.user_directory_search_all_users: - yield self._end_background_update("populate_user_directory_process_users") + yield self.db.updates._end_background_update( + "populate_user_directory_process_users" + ) return 1 def _get_next_batch(txn): @@ -297,7 +302,9 @@ class UserDirectoryBackgroundUpdateStore(StateDeltasStore, BackgroundUpdateStore # No more users -- complete the transaction. if not users_to_work_on: - yield self._end_background_update("populate_user_directory_process_users") + yield self.db.updates._end_background_update( + "populate_user_directory_process_users" + ) return 1 logger.info( @@ -317,7 +324,7 @@ class UserDirectoryBackgroundUpdateStore(StateDeltasStore, BackgroundUpdateStore progress["remaining"] -= 1 yield self.db.runInteraction( "populate_user_directory", - self._background_update_progress_txn, + self.db.updates._background_update_progress_txn, "populate_user_directory_process_users", progress, ) diff --git a/synapse/storage/database.py b/synapse/storage/database.py index ac64d80806..be36c1b829 100644 --- a/synapse/storage/database.py +++ b/synapse/storage/database.py @@ -30,6 +30,7 @@ from twisted.internet import defer from synapse.api.errors import StoreError from synapse.logging.context import LoggingContext, make_deferred_yieldable from synapse.metrics.background_process_metrics import run_as_background_process +from synapse.storage.background_updates import BackgroundUpdater from synapse.storage.engines import PostgresEngine, Sqlite3Engine from synapse.util.stringutils import exception_to_unicode @@ -223,6 +224,8 @@ class Database(object): self._clock = hs.get_clock() self._db_pool = hs.get_db_pool() + self.updates = BackgroundUpdater(hs, self) + self._previous_txn_total_time = 0 self._current_txn_total_time = 0 self._previous_loop_ts = 0 diff --git a/synmark/__init__.py b/synmark/__init__.py index 570eb818d9..afe4fad8cb 100644 --- a/synmark/__init__.py +++ b/synmark/__init__.py @@ -47,9 +47,9 @@ async def make_homeserver(reactor, config=None): stor = hs.get_datastore() # Run the database background updates. - if hasattr(stor, "do_next_background_update"): - while not await stor.has_completed_background_updates(): - await stor.do_next_background_update(1) + if hasattr(stor.db.updates, "do_next_background_update"): + while not await stor.db.updates.has_completed_background_updates(): + await stor.db.updates.do_next_background_update(1) def cleanup(): for i in cleanup_tasks: diff --git a/tests/handlers/test_stats.py b/tests/handlers/test_stats.py index 7f7962c3dd..d9d312f0fb 100644 --- a/tests/handlers/test_stats.py +++ b/tests/handlers/test_stats.py @@ -42,7 +42,7 @@ class StatsRoomTests(unittest.HomeserverTestCase): Add the background updates we need to run. """ # Ugh, have to reset this flag - self.store._all_done = False + self.store.db.updates._all_done = False self.get_success( self.store.db.simple_insert( @@ -108,8 +108,12 @@ class StatsRoomTests(unittest.HomeserverTestCase): # Do the initial population of the stats via the background update self._add_background_updates() - while not self.get_success(self.store.has_completed_background_updates()): - self.get_success(self.store.do_next_background_update(100), by=0.1) + while not self.get_success( + self.store.db.updates.has_completed_background_updates() + ): + self.get_success( + self.store.db.updates.do_next_background_update(100), by=0.1 + ) def test_initial_room(self): """ @@ -141,8 +145,12 @@ class StatsRoomTests(unittest.HomeserverTestCase): # Do the initial population of the user directory via the background update self._add_background_updates() - while not self.get_success(self.store.has_completed_background_updates()): - self.get_success(self.store.do_next_background_update(100), by=0.1) + while not self.get_success( + self.store.db.updates.has_completed_background_updates() + ): + self.get_success( + self.store.db.updates.do_next_background_update(100), by=0.1 + ) r = self.get_success(self.get_all_room_state()) @@ -178,7 +186,7 @@ class StatsRoomTests(unittest.HomeserverTestCase): # the position that the deltas should begin at, once they take over. self.hs.config.stats_enabled = True self.handler.stats_enabled = True - self.store._all_done = False + self.store.db.updates._all_done = False self.get_success( self.store.db.simple_update_one( table="stats_incremental_position", @@ -194,8 +202,12 @@ class StatsRoomTests(unittest.HomeserverTestCase): ) ) - while not self.get_success(self.store.has_completed_background_updates()): - self.get_success(self.store.do_next_background_update(100), by=0.1) + while not self.get_success( + self.store.db.updates.has_completed_background_updates() + ): + self.get_success( + self.store.db.updates.do_next_background_update(100), by=0.1 + ) # Now, before the table is actually ingested, add some more events. self.helper.invite(room=room_1, src=u1, targ=u2, tok=u1_token) @@ -221,9 +233,13 @@ class StatsRoomTests(unittest.HomeserverTestCase): ) ) - self.store._all_done = False - while not self.get_success(self.store.has_completed_background_updates()): - self.get_success(self.store.do_next_background_update(100), by=0.1) + self.store.db.updates._all_done = False + while not self.get_success( + self.store.db.updates.has_completed_background_updates() + ): + self.get_success( + self.store.db.updates.do_next_background_update(100), by=0.1 + ) self.reactor.advance(86401) @@ -653,7 +669,7 @@ class StatsRoomTests(unittest.HomeserverTestCase): # preparation stage of the initial background update # Ugh, have to reset this flag - self.store._all_done = False + self.store.db.updates._all_done = False self.get_success( self.store.db.simple_delete( @@ -673,7 +689,7 @@ class StatsRoomTests(unittest.HomeserverTestCase): # now do the background updates - self.store._all_done = False + self.store.db.updates._all_done = False self.get_success( self.store.db.simple_insert( "background_updates", @@ -705,8 +721,12 @@ class StatsRoomTests(unittest.HomeserverTestCase): ) ) - while not self.get_success(self.store.has_completed_background_updates()): - self.get_success(self.store.do_next_background_update(100), by=0.1) + while not self.get_success( + self.store.db.updates.has_completed_background_updates() + ): + self.get_success( + self.store.db.updates.do_next_background_update(100), by=0.1 + ) r1stats_complete = self._get_current_stats("room", r1) u1stats_complete = self._get_current_stats("user", u1) diff --git a/tests/handlers/test_user_directory.py b/tests/handlers/test_user_directory.py index bc9d441541..26071059d2 100644 --- a/tests/handlers/test_user_directory.py +++ b/tests/handlers/test_user_directory.py @@ -181,7 +181,7 @@ class UserDirectoryTestCase(unittest.HomeserverTestCase): Add the background updates we need to run. """ # Ugh, have to reset this flag - self.store._all_done = False + self.store.db.updates._all_done = False self.get_success( self.store.db.simple_insert( @@ -255,8 +255,12 @@ class UserDirectoryTestCase(unittest.HomeserverTestCase): # Do the initial population of the user directory via the background update self._add_background_updates() - while not self.get_success(self.store.has_completed_background_updates()): - self.get_success(self.store.do_next_background_update(100), by=0.1) + while not self.get_success( + self.store.db.updates.has_completed_background_updates() + ): + self.get_success( + self.store.db.updates.do_next_background_update(100), by=0.1 + ) shares_private = self.get_users_who_share_private_rooms() public_users = self.get_users_in_public_rooms() @@ -290,8 +294,12 @@ class UserDirectoryTestCase(unittest.HomeserverTestCase): # Do the initial population of the user directory via the background update self._add_background_updates() - while not self.get_success(self.store.has_completed_background_updates()): - self.get_success(self.store.do_next_background_update(100), by=0.1) + while not self.get_success( + self.store.db.updates.has_completed_background_updates() + ): + self.get_success( + self.store.db.updates.do_next_background_update(100), by=0.1 + ) shares_private = self.get_users_who_share_private_rooms() public_users = self.get_users_in_public_rooms() diff --git a/tests/storage/test_background_update.py b/tests/storage/test_background_update.py index e360297df9..aec76f4ab1 100644 --- a/tests/storage/test_background_update.py +++ b/tests/storage/test_background_update.py @@ -15,7 +15,7 @@ class BackgroundUpdateTestCase(unittest.TestCase): self.update_handler = Mock() - yield self.store.register_background_update_handler( + yield self.store.db.updates.register_background_update_handler( "test_update", self.update_handler ) @@ -23,7 +23,7 @@ class BackgroundUpdateTestCase(unittest.TestCase): # (perhaps we should run them as part of the test HS setup, since we # run all of the other schema setup stuff there?) while True: - res = yield self.store.do_next_background_update(1000) + res = yield self.store.db.updates.do_next_background_update(1000) if res is None: break @@ -39,7 +39,7 @@ class BackgroundUpdateTestCase(unittest.TestCase): progress = {"my_key": progress["my_key"] + 1} yield self.store.db.runInteraction( "update_progress", - self.store._background_update_progress_txn, + self.store.db.updates._background_update_progress_txn, "test_update", progress, ) @@ -47,29 +47,37 @@ class BackgroundUpdateTestCase(unittest.TestCase): self.update_handler.side_effect = update - yield self.store.start_background_update("test_update", {"my_key": 1}) + yield self.store.db.updates.start_background_update( + "test_update", {"my_key": 1} + ) self.update_handler.reset_mock() - result = yield self.store.do_next_background_update(duration_ms * desired_count) + result = yield self.store.db.updates.do_next_background_update( + duration_ms * desired_count + ) self.assertIsNotNone(result) self.update_handler.assert_called_once_with( - {"my_key": 1}, self.store.DEFAULT_BACKGROUND_BATCH_SIZE + {"my_key": 1}, self.store.db.updates.DEFAULT_BACKGROUND_BATCH_SIZE ) # second step: complete the update @defer.inlineCallbacks def update(progress, count): - yield self.store._end_background_update("test_update") + yield self.store.db.updates._end_background_update("test_update") return count self.update_handler.side_effect = update self.update_handler.reset_mock() - result = yield self.store.do_next_background_update(duration_ms * desired_count) + result = yield self.store.db.updates.do_next_background_update( + duration_ms * desired_count + ) self.assertIsNotNone(result) self.update_handler.assert_called_once_with({"my_key": 2}, desired_count) # third step: we don't expect to be called any more self.update_handler.reset_mock() - result = yield self.store.do_next_background_update(duration_ms * desired_count) + result = yield self.store.db.updates.do_next_background_update( + duration_ms * desired_count + ) self.assertIsNone(result) self.assertFalse(self.update_handler.called) diff --git a/tests/storage/test_cleanup_extrems.py b/tests/storage/test_cleanup_extrems.py index e454bbff29..029ac26454 100644 --- a/tests/storage/test_cleanup_extrems.py +++ b/tests/storage/test_cleanup_extrems.py @@ -46,7 +46,9 @@ class CleanupExtremBackgroundUpdateStoreTestCase(HomeserverTestCase): """Re run the background update to clean up the extremities. """ # Make sure we don't clash with in progress updates. - self.assertTrue(self.store._all_done, "Background updates are still ongoing") + self.assertTrue( + self.store.db.updates._all_done, "Background updates are still ongoing" + ) schema_path = os.path.join( prepare_database.dir_path, @@ -68,10 +70,14 @@ class CleanupExtremBackgroundUpdateStoreTestCase(HomeserverTestCase): ) # Ugh, have to reset this flag - self.store._all_done = False + self.store.db.updates._all_done = False - while not self.get_success(self.store.has_completed_background_updates()): - self.get_success(self.store.do_next_background_update(100), by=0.1) + while not self.get_success( + self.store.db.updates.has_completed_background_updates() + ): + self.get_success( + self.store.db.updates.do_next_background_update(100), by=0.1 + ) def test_soft_failed_extremities_handled_correctly(self): """Test that extremities are correctly calculated in the presence of diff --git a/tests/storage/test_client_ips.py b/tests/storage/test_client_ips.py index c4f838907c..fc279340d4 100644 --- a/tests/storage/test_client_ips.py +++ b/tests/storage/test_client_ips.py @@ -202,8 +202,12 @@ class ClientIpStoreTestCase(unittest.HomeserverTestCase): def test_devices_last_seen_bg_update(self): # First make sure we have completed all updates. - while not self.get_success(self.store.has_completed_background_updates()): - self.get_success(self.store.do_next_background_update(100), by=0.1) + while not self.get_success( + self.store.db.updates.has_completed_background_updates() + ): + self.get_success( + self.store.db.updates.do_next_background_update(100), by=0.1 + ) # Insert a user IP user_id = "@user:id" @@ -256,11 +260,15 @@ class ClientIpStoreTestCase(unittest.HomeserverTestCase): ) # ... and tell the DataStore that it hasn't finished all updates yet - self.store._all_done = False + self.store.db.updates._all_done = False # Now let's actually drive the updates to completion - while not self.get_success(self.store.has_completed_background_updates()): - self.get_success(self.store.do_next_background_update(100), by=0.1) + while not self.get_success( + self.store.db.updates.has_completed_background_updates() + ): + self.get_success( + self.store.db.updates.do_next_background_update(100), by=0.1 + ) # We should now get the correct result again result = self.get_success( @@ -281,8 +289,12 @@ class ClientIpStoreTestCase(unittest.HomeserverTestCase): def test_old_user_ips_pruned(self): # First make sure we have completed all updates. - while not self.get_success(self.store.has_completed_background_updates()): - self.get_success(self.store.do_next_background_update(100), by=0.1) + while not self.get_success( + self.store.db.updates.has_completed_background_updates() + ): + self.get_success( + self.store.db.updates.do_next_background_update(100), by=0.1 + ) # Insert a user IP user_id = "@user:id" diff --git a/tests/storage/test_roommember.py b/tests/storage/test_roommember.py index 5f957680a2..7840f63fe3 100644 --- a/tests/storage/test_roommember.py +++ b/tests/storage/test_roommember.py @@ -122,8 +122,12 @@ class CurrentStateMembershipUpdateTestCase(unittest.HomeserverTestCase): def test_can_rerun_update(self): # First make sure we have completed all updates. - while not self.get_success(self.store.has_completed_background_updates()): - self.get_success(self.store.do_next_background_update(100), by=0.1) + while not self.get_success( + self.store.db.updates.has_completed_background_updates() + ): + self.get_success( + self.store.db.updates.do_next_background_update(100), by=0.1 + ) # Now let's create a room, which will insert a membership user = UserID("alice", "test") @@ -143,8 +147,12 @@ class CurrentStateMembershipUpdateTestCase(unittest.HomeserverTestCase): ) # ... and tell the DataStore that it hasn't finished all updates yet - self.store._all_done = False + self.store.db.updates._all_done = False # Now let's actually drive the updates to completion - while not self.get_success(self.store.has_completed_background_updates()): - self.get_success(self.store.do_next_background_update(100), by=0.1) + while not self.get_success( + self.store.db.updates.has_completed_background_updates() + ): + self.get_success( + self.store.db.updates.do_next_background_update(100), by=0.1 + ) diff --git a/tests/unittest.py b/tests/unittest.py index fc856a574a..68d245ec9f 100644 --- a/tests/unittest.py +++ b/tests/unittest.py @@ -401,10 +401,12 @@ class HomeserverTestCase(TestCase): hs = setup_test_homeserver(self.addCleanup, *args, **kwargs) stor = hs.get_datastore() - # Run the database background updates. - if hasattr(stor, "do_next_background_update"): - while not self.get_success(stor.has_completed_background_updates()): - self.get_success(stor.do_next_background_update(1)) + # Run the database background updates, when running against "master". + if hs.__class__.__name__ == "TestHomeServer": + while not self.get_success( + stor.db.updates.has_completed_background_updates() + ): + self.get_success(stor.db.updates.do_next_background_update(1)) return hs From 6dcd6c40a05b55dad420f7dce13f145e09992f00 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 5 Dec 2019 10:51:26 +0000 Subject: [PATCH 0598/1623] Newsfile --- changelog.d/6469.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6469.misc diff --git a/changelog.d/6469.misc b/changelog.d/6469.misc new file mode 100644 index 0000000000..32216b9046 --- /dev/null +++ b/changelog.d/6469.misc @@ -0,0 +1 @@ +Move per database functionality out of the data stores and into a dedicated `Database` class. From 8b77fc65063bd79c0c08ce80c2beb426bd041681 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 5 Dec 2019 11:20:49 +0000 Subject: [PATCH 0599/1623] Fix DB scripts --- scripts-dev/update_database | 17 +++++++---------- scripts/synapse_port_db | 14 +++++++++----- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/scripts-dev/update_database b/scripts-dev/update_database index 27a1ad1e7e..1776d202c5 100755 --- a/scripts-dev/update_database +++ b/scripts-dev/update_database @@ -58,10 +58,10 @@ if __name__ == "__main__": " on it." ) ) - parser.add_argument("-v", action='store_true') + parser.add_argument("-v", action="store_true") parser.add_argument( "--database-config", - type=argparse.FileType('r'), + type=argparse.FileType("r"), required=True, help="A database config file for either a SQLite3 database or a PostgreSQL one.", ) @@ -101,10 +101,7 @@ if __name__ == "__main__": # Instantiate and initialise the homeserver object. hs = MockHomeserver( - config, - database_engine, - db_conn, - db_config=config.database_config, + config, database_engine, db_conn, db_config=config.database_config, ) # setup instantiates the store within the homeserver object. hs.setup() @@ -112,13 +109,13 @@ if __name__ == "__main__": @defer.inlineCallbacks def run_background_updates(): - yield store.run_background_updates(sleep=False) + yield store.db.updates.run_background_updates(sleep=False) # Stop the reactor to exit the script once every background update is run. reactor.stop() # Apply all background updates on the database. - reactor.callWhenRunning(lambda: run_as_background_process( - "background_updates", run_background_updates - )) + reactor.callWhenRunning( + lambda: run_as_background_process("background_updates", run_background_updates) + ) reactor.run() diff --git a/scripts/synapse_port_db b/scripts/synapse_port_db index 7a2e177d3d..72061177c9 100755 --- a/scripts/synapse_port_db +++ b/scripts/synapse_port_db @@ -365,7 +365,9 @@ class Porter(object): return headers, forward_rows, backward_rows - headers, frows, brows = yield self.sqlite_store.db.runInteraction("select", r) + headers, frows, brows = yield self.sqlite_store.db.runInteraction( + "select", r + ) if frows or brows: if frows: @@ -521,7 +523,9 @@ class Porter(object): @defer.inlineCallbacks def run_background_updates_on_postgres(self): # Manually apply all background updates on the PostgreSQL database. - postgres_ready = yield self.postgres_store.has_completed_background_updates() + postgres_ready = ( + yield self.postgres_store.db.updates.has_completed_background_updates() + ) if not postgres_ready: # Only say that we're running background updates when there are background @@ -529,9 +533,9 @@ class Porter(object): self.progress.set_state("Running background updates on PostgreSQL") while not postgres_ready: - yield self.postgres_store.do_next_background_update(100) + yield self.postgres_store.db.updates.do_next_background_update(100) postgres_ready = yield ( - self.postgres_store.has_completed_background_updates() + self.postgres_store.db.updates.has_completed_background_updates() ) @defer.inlineCallbacks @@ -541,7 +545,7 @@ class Porter(object): # Check if all background updates are done, abort if not. updates_complete = ( - yield self.sqlite_store.has_completed_background_updates() + yield self.sqlite_store.db.updates.has_completed_background_updates() ) if not updates_complete: sys.stderr.write( From ba7af15d4eb88712742edbf129667996cf3a59b3 Mon Sep 17 00:00:00 2001 From: Clifford Garwood II Date: Thu, 5 Dec 2019 08:13:47 -0500 Subject: [PATCH 0600/1623] Modify systemd unit file reference to align with installation instruction (#6369) Signed-off-by: Clifford Garwood II cliff@cigii.com --- changelog.d/6369.doc | 1 + contrib/systemd/README.md | 17 +++++++++++++++++ contrib/systemd/matrix-synapse.service | 7 +++++-- 3 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 changelog.d/6369.doc create mode 100644 contrib/systemd/README.md diff --git a/changelog.d/6369.doc b/changelog.d/6369.doc new file mode 100644 index 0000000000..6db351d7db --- /dev/null +++ b/changelog.d/6369.doc @@ -0,0 +1 @@ +Update documentation and variables in user contributed systemd reference file. diff --git a/contrib/systemd/README.md b/contrib/systemd/README.md new file mode 100644 index 0000000000..5d42b3464f --- /dev/null +++ b/contrib/systemd/README.md @@ -0,0 +1,17 @@ +# Setup Synapse with Systemd +This is a setup for managing synapse with a user contributed systemd unit +file. It provides a `matrix-synapse` systemd unit file that should be tailored +to accommodate your installation in accordance with the installation +instructions provided in [installation instructions](../../INSTALL.md). + +## Setup +1. Under the service section, ensure the `User` variable matches which user +you installed synapse under and wish to run it as. +2. Under the service section, ensure the `WorkingDirectory` variable matches +where you have installed synapse. +3. Under the service section, ensure the `ExecStart` variable matches the +appropriate locations of your installation. +4. Copy the `matrix-synapse.service` to `/etc/systemd/system/` +5. Start Synapse: `sudo systemctl start matrix-synapse` +6. Verify Synapse is running: `sudo systemctl status matrix-synapse` +7. *optional* Enable Synapse to start at system boot: `sudo systemctl enable matrix-synapse` diff --git a/contrib/systemd/matrix-synapse.service b/contrib/systemd/matrix-synapse.service index 38d369ea3d..bd492544b6 100644 --- a/contrib/systemd/matrix-synapse.service +++ b/contrib/systemd/matrix-synapse.service @@ -4,8 +4,11 @@ # systemctl enable matrix-synapse # systemctl start matrix-synapse # +# This assumes that Synapse has been installed by a user named +# synapse. +# # This assumes that Synapse has been installed in a virtualenv in -# /opt/synapse/env. +# the user's home directory: `/home/synapse/synapse/env`. # # **NOTE:** This is an example service file that may change in the future. If you # wish to use this please copy rather than symlink it. @@ -23,7 +26,7 @@ User=synapse Group=nogroup WorkingDirectory=/opt/synapse -ExecStart=/opt/synapse/env/bin/python -m synapse.app.homeserver --config-path=/opt/synapse/homeserver.yaml +ExecStart=/home/synapse/synapse/env/bin/python -m synapse.app.homeserver --config-path=/home/synapse/synapse/homeserver.yaml SyslogIdentifier=matrix-synapse # adjust the cache factor if necessary From e1f4c83f41bf6f06bef3d160eb94eacabe59eff1 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 5 Dec 2019 14:14:45 +0000 Subject: [PATCH 0601/1623] Sanity-check the rooms of auth events before pulling them in. (#6472) --- changelog.d/6472.bugfix | 1 + synapse/handlers/federation.py | 34 +++++++++++++++++++++++++--------- 2 files changed, 26 insertions(+), 9 deletions(-) create mode 100644 changelog.d/6472.bugfix diff --git a/changelog.d/6472.bugfix b/changelog.d/6472.bugfix new file mode 100644 index 0000000000..598efb79fc --- /dev/null +++ b/changelog.d/6472.bugfix @@ -0,0 +1 @@ +Improve sanity-checking when receiving events over federation. diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 7784b80b77..f5d04cdf91 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -2195,21 +2195,37 @@ class FederationHandler(BaseHandler): different_auth, ) - # now we state-resolve between our own idea of the auth events, and the remote's - # idea of them. - - room_version = yield self.store.get_room_version(event.room_id) - # XXX: currently this checks for redactions but I'm not convinced that is # necessary? different_events = yield self.store.get_events_as_list(different_auth) - local_view = dict(auth_events) - remote_view = dict(auth_events) - remote_view.update({(d.type, d.state_key): d for d in different_events}) + for d in different_events: + if d.room_id != event.room_id: + logger.warning( + "Event %s refers to auth_event %s which is in a different room", + event.event_id, + d.event_id, + ) + # don't attempt to resolve the claimed auth events against our own + # in this case: just use our own auth events. + # + # XXX: should we reject the event in this case? It feels like we should, + # but then shouldn't we also do so if we've failed to fetch any of the + # auth events? + return context + + # now we state-resolve between our own idea of the auth events, and the remote's + # idea of them. + + local_state = auth_events.values() + remote_auth_events = dict(auth_events) + remote_auth_events.update({(d.type, d.state_key): d for d in different_events}) + remote_state = remote_auth_events.values() + + room_version = yield self.store.get_room_version(event.room_id) new_state = yield self.state_handler.resolve_events( - room_version, [list(local_view.values()), list(remote_view.values())], event + room_version, (local_state, remote_state), event ) logger.info( From 63d6ad1064c1a5fe23da3b6b64474a2b211f5eea Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 5 Dec 2019 15:02:35 +0000 Subject: [PATCH 0602/1623] Stronger typing in the federation handler (#6480) replace the event_info dict with an attrs thing --- changelog.d/6480.misc | 1 + synapse/handlers/federation.py | 81 ++++++++++++++++++++++++---------- 2 files changed, 58 insertions(+), 24 deletions(-) create mode 100644 changelog.d/6480.misc diff --git a/changelog.d/6480.misc b/changelog.d/6480.misc new file mode 100644 index 0000000000..d9a44389b9 --- /dev/null +++ b/changelog.d/6480.misc @@ -0,0 +1 @@ +Refactor some code in the event authentication path for clarity. diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index f5d04cdf91..bc26921768 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -19,11 +19,13 @@ import itertools import logging +from typing import Dict, Iterable, Optional, Sequence, Tuple import six from six import iteritems, itervalues from six.moves import http_client, zip +import attr from signedjson.key import decode_verify_key_bytes from signedjson.sign import verify_signed_json from unpaddedbase64 import decode_base64 @@ -45,6 +47,7 @@ from synapse.api.errors import ( from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersions from synapse.crypto.event_signing import compute_event_signature from synapse.event_auth import auth_types_for_event +from synapse.events import EventBase from synapse.events.snapshot import EventContext from synapse.events.validator import EventValidator from synapse.logging.context import ( @@ -72,6 +75,23 @@ from ._base import BaseHandler logger = logging.getLogger(__name__) +@attr.s +class _NewEventInfo: + """Holds information about a received event, ready for passing to _handle_new_events + + Attributes: + event: the received event + + state: the state at that event + + auth_events: the auth_event map for that event + """ + + event = attr.ib(type=EventBase) + state = attr.ib(type=Optional[Sequence[EventBase]], default=None) + auth_events = attr.ib(type=Optional[Dict[Tuple[str, str], EventBase]], default=None) + + def shortstr(iterable, maxitems=5): """If iterable has maxitems or fewer, return the stringification of a list containing those items. @@ -597,14 +617,14 @@ class FederationHandler(BaseHandler): for e in auth_chain if e.event_id in auth_ids or e.type == EventTypes.Create } - event_infos.append({"event": e, "auth_events": auth}) + event_infos.append(_NewEventInfo(event=e, auth_events=auth)) seen_ids.add(e.event_id) logger.info( "[%s %s] persisting newly-received auth/state events %s", room_id, event_id, - [e["event"].event_id for e in event_infos], + [e.event.event_id for e in event_infos], ) yield self._handle_new_events(origin, event_infos) @@ -795,9 +815,9 @@ class FederationHandler(BaseHandler): a.internal_metadata.outlier = True ev_infos.append( - { - "event": a, - "auth_events": { + _NewEventInfo( + event=a, + auth_events={ ( auth_events[a_id].type, auth_events[a_id].state_key, @@ -805,7 +825,7 @@ class FederationHandler(BaseHandler): for a_id in a.auth_event_ids() if a_id in auth_events }, - } + ) ) # Step 1b: persist the events in the chunk we fetched state for (i.e. @@ -817,10 +837,10 @@ class FederationHandler(BaseHandler): assert not ev.internal_metadata.is_outlier() ev_infos.append( - { - "event": ev, - "state": events_to_state[e_id], - "auth_events": { + _NewEventInfo( + event=ev, + state=events_to_state[e_id], + auth_events={ ( auth_events[a_id].type, auth_events[a_id].state_key, @@ -828,7 +848,7 @@ class FederationHandler(BaseHandler): for a_id in ev.auth_event_ids() if a_id in auth_events }, - } + ) ) yield self._handle_new_events(dest, ev_infos, backfilled=True) @@ -1713,7 +1733,12 @@ class FederationHandler(BaseHandler): return context @defer.inlineCallbacks - def _handle_new_events(self, origin, event_infos, backfilled=False): + def _handle_new_events( + self, + origin: str, + event_infos: Iterable[_NewEventInfo], + backfilled: bool = False, + ): """Creates the appropriate contexts and persists events. The events should not depend on one another, e.g. this should be used to persist a bunch of outliers, but not a chunk of individual events that depend @@ -1723,14 +1748,14 @@ class FederationHandler(BaseHandler): """ @defer.inlineCallbacks - def prep(ev_info): - event = ev_info["event"] + def prep(ev_info: _NewEventInfo): + event = ev_info.event with nested_logging_context(suffix=event.event_id): res = yield self._prep_event( origin, event, - state=ev_info.get("state"), - auth_events=ev_info.get("auth_events"), + state=ev_info.state, + auth_events=ev_info.auth_events, backfilled=backfilled, ) return res @@ -1744,7 +1769,7 @@ class FederationHandler(BaseHandler): yield self.persist_events_and_notify( [ - (ev_info["event"], context) + (ev_info.event, context) for ev_info, context in zip(event_infos, contexts) ], backfilled=backfilled, @@ -1846,7 +1871,14 @@ class FederationHandler(BaseHandler): yield self.persist_events_and_notify([(event, new_event_context)]) @defer.inlineCallbacks - def _prep_event(self, origin, event, state, auth_events, backfilled): + def _prep_event( + self, + origin: str, + event: EventBase, + state: Optional[Iterable[EventBase]], + auth_events: Optional[Dict[Tuple[str, str], EventBase]], + backfilled: bool, + ): """ Args: @@ -1854,7 +1886,7 @@ class FederationHandler(BaseHandler): event: state: auth_events: - backfilled (bool) + backfilled: Returns: Deferred, which resolves to synapse.events.snapshot.EventContext @@ -1890,15 +1922,16 @@ class FederationHandler(BaseHandler): return context @defer.inlineCallbacks - def _check_for_soft_fail(self, event, state, backfilled): + def _check_for_soft_fail( + self, event: EventBase, state: Optional[Iterable[EventBase]], backfilled: bool + ): """Checks if we should soft fail the event, if so marks the event as such. Args: - event (FrozenEvent) - state (dict|None): The state at the event if we don't have all the - event's prev events - backfilled (bool): Whether the event is from backfill + event + state: The state at the event if we don't have all the event's prev events + backfilled: Whether the event is from backfill Returns: Deferred From dc8747895ec026c365e687853b5ca12225fb881e Mon Sep 17 00:00:00 2001 From: Clifford Garwood II Date: Thu, 5 Dec 2019 08:13:47 -0500 Subject: [PATCH 0603/1623] Modify systemd unit file reference to align with installation instruction (#6369) Signed-off-by: Clifford Garwood II cliff@cigii.com --- changelog.d/6369.doc | 1 + contrib/systemd/README.md | 17 +++++++++++++++++ contrib/systemd/matrix-synapse.service | 7 +++++-- 3 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 changelog.d/6369.doc create mode 100644 contrib/systemd/README.md diff --git a/changelog.d/6369.doc b/changelog.d/6369.doc new file mode 100644 index 0000000000..6db351d7db --- /dev/null +++ b/changelog.d/6369.doc @@ -0,0 +1 @@ +Update documentation and variables in user contributed systemd reference file. diff --git a/contrib/systemd/README.md b/contrib/systemd/README.md new file mode 100644 index 0000000000..5d42b3464f --- /dev/null +++ b/contrib/systemd/README.md @@ -0,0 +1,17 @@ +# Setup Synapse with Systemd +This is a setup for managing synapse with a user contributed systemd unit +file. It provides a `matrix-synapse` systemd unit file that should be tailored +to accommodate your installation in accordance with the installation +instructions provided in [installation instructions](../../INSTALL.md). + +## Setup +1. Under the service section, ensure the `User` variable matches which user +you installed synapse under and wish to run it as. +2. Under the service section, ensure the `WorkingDirectory` variable matches +where you have installed synapse. +3. Under the service section, ensure the `ExecStart` variable matches the +appropriate locations of your installation. +4. Copy the `matrix-synapse.service` to `/etc/systemd/system/` +5. Start Synapse: `sudo systemctl start matrix-synapse` +6. Verify Synapse is running: `sudo systemctl status matrix-synapse` +7. *optional* Enable Synapse to start at system boot: `sudo systemctl enable matrix-synapse` diff --git a/contrib/systemd/matrix-synapse.service b/contrib/systemd/matrix-synapse.service index 38d369ea3d..bd492544b6 100644 --- a/contrib/systemd/matrix-synapse.service +++ b/contrib/systemd/matrix-synapse.service @@ -4,8 +4,11 @@ # systemctl enable matrix-synapse # systemctl start matrix-synapse # +# This assumes that Synapse has been installed by a user named +# synapse. +# # This assumes that Synapse has been installed in a virtualenv in -# /opt/synapse/env. +# the user's home directory: `/home/synapse/synapse/env`. # # **NOTE:** This is an example service file that may change in the future. If you # wish to use this please copy rather than symlink it. @@ -23,7 +26,7 @@ User=synapse Group=nogroup WorkingDirectory=/opt/synapse -ExecStart=/opt/synapse/env/bin/python -m synapse.app.homeserver --config-path=/opt/synapse/homeserver.yaml +ExecStart=/home/synapse/synapse/env/bin/python -m synapse.app.homeserver --config-path=/home/synapse/synapse/homeserver.yaml SyslogIdentifier=matrix-synapse # adjust the cache factor if necessary From e2cce15af16cd85d5379e8d961680028bfc9e754 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Thu, 5 Dec 2019 15:44:02 +0000 Subject: [PATCH 0604/1623] Remove #6369 changelog --- changelog.d/6369.doc | 1 - 1 file changed, 1 deletion(-) delete mode 100644 changelog.d/6369.doc diff --git a/changelog.d/6369.doc b/changelog.d/6369.doc deleted file mode 100644 index 6db351d7db..0000000000 --- a/changelog.d/6369.doc +++ /dev/null @@ -1 +0,0 @@ -Update documentation and variables in user contributed systemd reference file. From ff119879d618c60e1292152724d90b160514a76f Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Thu, 5 Dec 2019 15:46:19 +0000 Subject: [PATCH 0605/1623] Revert "Modify systemd unit file reference to align with installation instruction (#6369)" This reverts commit dc8747895ec026c365e687853b5ca12225fb881e. --- changelog.d/6369.doc | 1 - contrib/systemd/README.md | 17 ----------------- contrib/systemd/matrix-synapse.service | 7 ++----- 3 files changed, 2 insertions(+), 23 deletions(-) delete mode 100644 changelog.d/6369.doc delete mode 100644 contrib/systemd/README.md diff --git a/changelog.d/6369.doc b/changelog.d/6369.doc deleted file mode 100644 index 6db351d7db..0000000000 --- a/changelog.d/6369.doc +++ /dev/null @@ -1 +0,0 @@ -Update documentation and variables in user contributed systemd reference file. diff --git a/contrib/systemd/README.md b/contrib/systemd/README.md deleted file mode 100644 index 5d42b3464f..0000000000 --- a/contrib/systemd/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# Setup Synapse with Systemd -This is a setup for managing synapse with a user contributed systemd unit -file. It provides a `matrix-synapse` systemd unit file that should be tailored -to accommodate your installation in accordance with the installation -instructions provided in [installation instructions](../../INSTALL.md). - -## Setup -1. Under the service section, ensure the `User` variable matches which user -you installed synapse under and wish to run it as. -2. Under the service section, ensure the `WorkingDirectory` variable matches -where you have installed synapse. -3. Under the service section, ensure the `ExecStart` variable matches the -appropriate locations of your installation. -4. Copy the `matrix-synapse.service` to `/etc/systemd/system/` -5. Start Synapse: `sudo systemctl start matrix-synapse` -6. Verify Synapse is running: `sudo systemctl status matrix-synapse` -7. *optional* Enable Synapse to start at system boot: `sudo systemctl enable matrix-synapse` diff --git a/contrib/systemd/matrix-synapse.service b/contrib/systemd/matrix-synapse.service index bd492544b6..38d369ea3d 100644 --- a/contrib/systemd/matrix-synapse.service +++ b/contrib/systemd/matrix-synapse.service @@ -4,11 +4,8 @@ # systemctl enable matrix-synapse # systemctl start matrix-synapse # -# This assumes that Synapse has been installed by a user named -# synapse. -# # This assumes that Synapse has been installed in a virtualenv in -# the user's home directory: `/home/synapse/synapse/env`. +# /opt/synapse/env. # # **NOTE:** This is an example service file that may change in the future. If you # wish to use this please copy rather than symlink it. @@ -26,7 +23,7 @@ User=synapse Group=nogroup WorkingDirectory=/opt/synapse -ExecStart=/home/synapse/synapse/env/bin/python -m synapse.app.homeserver --config-path=/home/synapse/synapse/homeserver.yaml +ExecStart=/opt/synapse/env/bin/python -m synapse.app.homeserver --config-path=/opt/synapse/homeserver.yaml SyslogIdentifier=matrix-synapse # adjust the cache factor if necessary From 1a0997bbd5f9134b21b1105c6b14e09480f193a9 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 5 Dec 2019 15:53:06 +0000 Subject: [PATCH 0606/1623] Port rest/v1 to async/await --- synapse/app/synchrotron.py | 2 +- synapse/rest/client/v1/directory.py | 53 ++++++++++------------- synapse/rest/client/v1/events.py | 18 +++----- synapse/rest/client/v1/initial_sync.py | 8 ++-- synapse/rest/client/v1/login.py | 60 +++++++++++--------------- synapse/rest/client/v1/logout.py | 20 ++++----- synapse/rest/client/v1/presence.py | 18 +++----- synapse/rest/client/v1/profile.py | 48 +++++++++------------ synapse/rest/client/v1/push_rule.py | 24 +++++------ synapse/rest/client/v1/pusher.py | 27 +++++------- synapse/rest/client/v1/voip.py | 7 +-- 11 files changed, 118 insertions(+), 167 deletions(-) diff --git a/synapse/app/synchrotron.py b/synapse/app/synchrotron.py index b14da09f47..288ee64b42 100644 --- a/synapse/app/synchrotron.py +++ b/synapse/app/synchrotron.py @@ -151,7 +151,7 @@ class SynchrotronPresence(object): def set_state(self, user, state, ignore_status_msg=False): # TODO Hows this supposed to work? - pass + return defer.succeed(None) get_states = __func__(PresenceHandler.get_states) get_state = __func__(PresenceHandler.get_state) diff --git a/synapse/rest/client/v1/directory.py b/synapse/rest/client/v1/directory.py index 4ea3666874..5934b1fe8b 100644 --- a/synapse/rest/client/v1/directory.py +++ b/synapse/rest/client/v1/directory.py @@ -16,8 +16,6 @@ import logging -from twisted.internet import defer - from synapse.api.errors import ( AuthError, Codes, @@ -47,17 +45,15 @@ class ClientDirectoryServer(RestServlet): self.handlers = hs.get_handlers() self.auth = hs.get_auth() - @defer.inlineCallbacks - def on_GET(self, request, room_alias): + async def on_GET(self, request, room_alias): room_alias = RoomAlias.from_string(room_alias) dir_handler = self.handlers.directory_handler - res = yield dir_handler.get_association(room_alias) + res = await dir_handler.get_association(room_alias) return 200, res - @defer.inlineCallbacks - def on_PUT(self, request, room_alias): + async def on_PUT(self, request, room_alias): room_alias = RoomAlias.from_string(room_alias) content = parse_json_object_from_request(request) @@ -77,26 +73,25 @@ class ClientDirectoryServer(RestServlet): # TODO(erikj): Check types. - room = yield self.store.get_room(room_id) + room = await self.store.get_room(room_id) if room is None: raise SynapseError(400, "Room does not exist") - requester = yield self.auth.get_user_by_req(request) + requester = await self.auth.get_user_by_req(request) - yield self.handlers.directory_handler.create_association( + await self.handlers.directory_handler.create_association( requester, room_alias, room_id, servers ) return 200, {} - @defer.inlineCallbacks - def on_DELETE(self, request, room_alias): + async def on_DELETE(self, request, room_alias): dir_handler = self.handlers.directory_handler try: - service = yield self.auth.get_appservice_by_req(request) + service = await self.auth.get_appservice_by_req(request) room_alias = RoomAlias.from_string(room_alias) - yield dir_handler.delete_appservice_association(service, room_alias) + await dir_handler.delete_appservice_association(service, room_alias) logger.info( "Application service at %s deleted alias %s", service.url, @@ -107,12 +102,12 @@ class ClientDirectoryServer(RestServlet): # fallback to default user behaviour if they aren't an AS pass - requester = yield self.auth.get_user_by_req(request) + requester = await self.auth.get_user_by_req(request) user = requester.user room_alias = RoomAlias.from_string(room_alias) - yield dir_handler.delete_association(requester, room_alias) + await dir_handler.delete_association(requester, room_alias) logger.info( "User %s deleted alias %s", user.to_string(), room_alias.to_string() @@ -130,32 +125,29 @@ class ClientDirectoryListServer(RestServlet): self.handlers = hs.get_handlers() self.auth = hs.get_auth() - @defer.inlineCallbacks - def on_GET(self, request, room_id): - room = yield self.store.get_room(room_id) + async def on_GET(self, request, room_id): + room = await self.store.get_room(room_id) if room is None: raise NotFoundError("Unknown room") return 200, {"visibility": "public" if room["is_public"] else "private"} - @defer.inlineCallbacks - def on_PUT(self, request, room_id): - requester = yield self.auth.get_user_by_req(request) + async def on_PUT(self, request, room_id): + requester = await self.auth.get_user_by_req(request) content = parse_json_object_from_request(request) visibility = content.get("visibility", "public") - yield self.handlers.directory_handler.edit_published_room_list( + await self.handlers.directory_handler.edit_published_room_list( requester, room_id, visibility ) return 200, {} - @defer.inlineCallbacks - def on_DELETE(self, request, room_id): - requester = yield self.auth.get_user_by_req(request) + async def on_DELETE(self, request, room_id): + requester = await self.auth.get_user_by_req(request) - yield self.handlers.directory_handler.edit_published_room_list( + await self.handlers.directory_handler.edit_published_room_list( requester, room_id, "private" ) @@ -181,15 +173,14 @@ class ClientAppserviceDirectoryListServer(RestServlet): def on_DELETE(self, request, network_id, room_id): return self._edit(request, network_id, room_id, "private") - @defer.inlineCallbacks - def _edit(self, request, network_id, room_id, visibility): - requester = yield self.auth.get_user_by_req(request) + async def _edit(self, request, network_id, room_id, visibility): + requester = await self.auth.get_user_by_req(request) if not requester.app_service: raise AuthError( 403, "Only appservices can edit the appservice published room list" ) - yield self.handlers.directory_handler.edit_published_appservice_room_list( + await self.handlers.directory_handler.edit_published_appservice_room_list( requester.app_service.id, network_id, room_id, visibility ) diff --git a/synapse/rest/client/v1/events.py b/synapse/rest/client/v1/events.py index 6651b4cf07..4beb617733 100644 --- a/synapse/rest/client/v1/events.py +++ b/synapse/rest/client/v1/events.py @@ -16,8 +16,6 @@ """This module contains REST servlets to do with event streaming, /events.""" import logging -from twisted.internet import defer - from synapse.api.errors import SynapseError from synapse.http.servlet import RestServlet from synapse.rest.client.v2_alpha._base import client_patterns @@ -36,9 +34,8 @@ class EventStreamRestServlet(RestServlet): self.event_stream_handler = hs.get_event_stream_handler() self.auth = hs.get_auth() - @defer.inlineCallbacks - def on_GET(self, request): - requester = yield self.auth.get_user_by_req(request, allow_guest=True) + async def on_GET(self, request): + requester = await self.auth.get_user_by_req(request, allow_guest=True) is_guest = requester.is_guest room_id = None if is_guest: @@ -57,7 +54,7 @@ class EventStreamRestServlet(RestServlet): as_client_event = b"raw" not in request.args - chunk = yield self.event_stream_handler.get_stream( + chunk = await self.event_stream_handler.get_stream( requester.user.to_string(), pagin_config, timeout=timeout, @@ -83,14 +80,13 @@ class EventRestServlet(RestServlet): self.event_handler = hs.get_event_handler() self._event_serializer = hs.get_event_client_serializer() - @defer.inlineCallbacks - def on_GET(self, request, event_id): - requester = yield self.auth.get_user_by_req(request) - event = yield self.event_handler.get_event(requester.user, None, event_id) + async def on_GET(self, request, event_id): + requester = await self.auth.get_user_by_req(request) + event = await self.event_handler.get_event(requester.user, None, event_id) time_now = self.clock.time_msec() if event: - event = yield self._event_serializer.serialize_event(event, time_now) + event = await self._event_serializer.serialize_event(event, time_now) return 200, event else: return 404, "Event not found." diff --git a/synapse/rest/client/v1/initial_sync.py b/synapse/rest/client/v1/initial_sync.py index 2da3cd7511..910b3b4eeb 100644 --- a/synapse/rest/client/v1/initial_sync.py +++ b/synapse/rest/client/v1/initial_sync.py @@ -13,7 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from twisted.internet import defer from synapse.http.servlet import RestServlet, parse_boolean from synapse.rest.client.v2_alpha._base import client_patterns @@ -29,13 +28,12 @@ class InitialSyncRestServlet(RestServlet): self.initial_sync_handler = hs.get_initial_sync_handler() self.auth = hs.get_auth() - @defer.inlineCallbacks - def on_GET(self, request): - requester = yield self.auth.get_user_by_req(request) + async def on_GET(self, request): + requester = await self.auth.get_user_by_req(request) as_client_event = b"raw" not in request.args pagination_config = PaginationConfig.from_request(request) include_archived = parse_boolean(request, "archived", default=False) - content = yield self.initial_sync_handler.snapshot_all_rooms( + content = await self.initial_sync_handler.snapshot_all_rooms( user_id=requester.user.to_string(), pagin_config=pagination_config, as_client_event=as_client_event, diff --git a/synapse/rest/client/v1/login.py b/synapse/rest/client/v1/login.py index 19eb15003d..ff9c978fe7 100644 --- a/synapse/rest/client/v1/login.py +++ b/synapse/rest/client/v1/login.py @@ -18,7 +18,6 @@ import xml.etree.ElementTree as ET from six.moves import urllib -from twisted.internet import defer from twisted.web.client import PartialDownloadError from synapse.api.errors import Codes, LoginError, SynapseError @@ -130,8 +129,7 @@ class LoginRestServlet(RestServlet): def on_OPTIONS(self, request): return 200, {} - @defer.inlineCallbacks - def on_POST(self, request): + async def on_POST(self, request): self._address_ratelimiter.ratelimit( request.getClientIP(), time_now_s=self.hs.clock.time(), @@ -145,11 +143,11 @@ class LoginRestServlet(RestServlet): if self.jwt_enabled and ( login_submission["type"] == LoginRestServlet.JWT_TYPE ): - result = yield self.do_jwt_login(login_submission) + result = await self.do_jwt_login(login_submission) elif login_submission["type"] == LoginRestServlet.TOKEN_TYPE: - result = yield self.do_token_login(login_submission) + result = await self.do_token_login(login_submission) else: - result = yield self._do_other_login(login_submission) + result = await self._do_other_login(login_submission) except KeyError: raise SynapseError(400, "Missing JSON keys.") @@ -158,8 +156,7 @@ class LoginRestServlet(RestServlet): result["well_known"] = well_known_data return 200, result - @defer.inlineCallbacks - def _do_other_login(self, login_submission): + async def _do_other_login(self, login_submission): """Handle non-token/saml/jwt logins Args: @@ -219,20 +216,20 @@ class LoginRestServlet(RestServlet): ( canonical_user_id, callback_3pid, - ) = yield self.auth_handler.check_password_provider_3pid( + ) = await self.auth_handler.check_password_provider_3pid( medium, address, login_submission["password"] ) if canonical_user_id: # Authentication through password provider and 3pid succeeded - result = yield self._complete_login( + result = await self._complete_login( canonical_user_id, login_submission, callback_3pid ) return result # No password providers were able to handle this 3pid # Check local store - user_id = yield self.hs.get_datastore().get_user_id_by_threepid( + user_id = await self.hs.get_datastore().get_user_id_by_threepid( medium, address ) if not user_id: @@ -280,7 +277,7 @@ class LoginRestServlet(RestServlet): ) try: - canonical_user_id, callback = yield self.auth_handler.validate_login( + canonical_user_id, callback = await self.auth_handler.validate_login( identifier["user"], login_submission ) except LoginError: @@ -297,13 +294,12 @@ class LoginRestServlet(RestServlet): ) raise - result = yield self._complete_login( + result = await self._complete_login( canonical_user_id, login_submission, callback ) return result - @defer.inlineCallbacks - def _complete_login( + async def _complete_login( self, user_id, login_submission, callback=None, create_non_existant_users=False ): """Called when we've successfully authed the user and now need to @@ -337,15 +333,15 @@ class LoginRestServlet(RestServlet): ) if create_non_existant_users: - user_id = yield self.auth_handler.check_user_exists(user_id) + user_id = await self.auth_handler.check_user_exists(user_id) if not user_id: - user_id = yield self.registration_handler.register_user( + user_id = await self.registration_handler.register_user( localpart=UserID.from_string(user_id).localpart ) device_id = login_submission.get("device_id") initial_display_name = login_submission.get("initial_device_display_name") - device_id, access_token = yield self.registration_handler.register_device( + device_id, access_token = await self.registration_handler.register_device( user_id, device_id, initial_display_name ) @@ -357,23 +353,21 @@ class LoginRestServlet(RestServlet): } if callback is not None: - yield callback(result) + await callback(result) return result - @defer.inlineCallbacks - def do_token_login(self, login_submission): + async def do_token_login(self, login_submission): token = login_submission["token"] auth_handler = self.auth_handler - user_id = yield auth_handler.validate_short_term_login_token_and_get_user_id( + user_id = await auth_handler.validate_short_term_login_token_and_get_user_id( token ) - result = yield self._complete_login(user_id, login_submission) + result = await self._complete_login(user_id, login_submission) return result - @defer.inlineCallbacks - def do_jwt_login(self, login_submission): + async def do_jwt_login(self, login_submission): token = login_submission.get("token", None) if token is None: raise LoginError( @@ -397,7 +391,7 @@ class LoginRestServlet(RestServlet): raise LoginError(401, "Invalid JWT", errcode=Codes.UNAUTHORIZED) user_id = UserID(user, self.hs.hostname).to_string() - result = yield self._complete_login( + result = await self._complete_login( user_id, login_submission, create_non_existant_users=True ) return result @@ -460,8 +454,7 @@ class CasTicketServlet(RestServlet): self._sso_auth_handler = SSOAuthHandler(hs) self._http_client = hs.get_proxied_http_client() - @defer.inlineCallbacks - def on_GET(self, request): + async def on_GET(self, request): client_redirect_url = parse_string(request, "redirectUrl", required=True) uri = self.cas_server_url + "/proxyValidate" args = { @@ -469,12 +462,12 @@ class CasTicketServlet(RestServlet): "service": self.cas_service_url, } try: - body = yield self._http_client.get_raw(uri, args) + body = await self._http_client.get_raw(uri, args) except PartialDownloadError as pde: # Twisted raises this error if the connection is closed, # even if that's being used old-http style to signal end-of-data body = pde.response - result = yield self.handle_cas_response(request, body, client_redirect_url) + result = await self.handle_cas_response(request, body, client_redirect_url) return result def handle_cas_response(self, request, cas_response_body, client_redirect_url): @@ -555,8 +548,7 @@ class SSOAuthHandler(object): self._registration_handler = hs.get_registration_handler() self._macaroon_gen = hs.get_macaroon_generator() - @defer.inlineCallbacks - def on_successful_auth( + async def on_successful_auth( self, username, request, client_redirect_url, user_display_name=None ): """Called once the user has successfully authenticated with the SSO. @@ -582,9 +574,9 @@ class SSOAuthHandler(object): """ localpart = map_username_to_mxid_localpart(username) user_id = UserID(localpart, self._hostname).to_string() - registered_user_id = yield self._auth_handler.check_user_exists(user_id) + registered_user_id = await self._auth_handler.check_user_exists(user_id) if not registered_user_id: - registered_user_id = yield self._registration_handler.register_user( + registered_user_id = await self._registration_handler.register_user( localpart=localpart, default_display_name=user_display_name ) diff --git a/synapse/rest/client/v1/logout.py b/synapse/rest/client/v1/logout.py index 4785a34d75..1cf3caf832 100644 --- a/synapse/rest/client/v1/logout.py +++ b/synapse/rest/client/v1/logout.py @@ -15,8 +15,6 @@ import logging -from twisted.internet import defer - from synapse.http.servlet import RestServlet from synapse.rest.client.v2_alpha._base import client_patterns @@ -35,17 +33,16 @@ class LogoutRestServlet(RestServlet): def on_OPTIONS(self, request): return 200, {} - @defer.inlineCallbacks - def on_POST(self, request): - requester = yield self.auth.get_user_by_req(request) + async def on_POST(self, request): + requester = await self.auth.get_user_by_req(request) if requester.device_id is None: # the acccess token wasn't associated with a device. # Just delete the access token access_token = self.auth.get_access_token_from_request(request) - yield self._auth_handler.delete_access_token(access_token) + await self._auth_handler.delete_access_token(access_token) else: - yield self._device_handler.delete_device( + await self._device_handler.delete_device( requester.user.to_string(), requester.device_id ) @@ -64,17 +61,16 @@ class LogoutAllRestServlet(RestServlet): def on_OPTIONS(self, request): return 200, {} - @defer.inlineCallbacks - def on_POST(self, request): - requester = yield self.auth.get_user_by_req(request) + async def on_POST(self, request): + requester = await self.auth.get_user_by_req(request) user_id = requester.user.to_string() # first delete all of the user's devices - yield self._device_handler.delete_all_devices_for_user(user_id) + await self._device_handler.delete_all_devices_for_user(user_id) # .. and then delete any access tokens which weren't associated with # devices. - yield self._auth_handler.delete_access_tokens_for_user(user_id) + await self._auth_handler.delete_access_tokens_for_user(user_id) return 200, {} diff --git a/synapse/rest/client/v1/presence.py b/synapse/rest/client/v1/presence.py index 0153525cef..eec16f8ad8 100644 --- a/synapse/rest/client/v1/presence.py +++ b/synapse/rest/client/v1/presence.py @@ -19,8 +19,6 @@ import logging from six import string_types -from twisted.internet import defer - from synapse.api.errors import AuthError, SynapseError from synapse.handlers.presence import format_user_presence_state from synapse.http.servlet import RestServlet, parse_json_object_from_request @@ -40,27 +38,25 @@ class PresenceStatusRestServlet(RestServlet): self.clock = hs.get_clock() self.auth = hs.get_auth() - @defer.inlineCallbacks - def on_GET(self, request, user_id): - requester = yield self.auth.get_user_by_req(request) + async def on_GET(self, request, user_id): + requester = await self.auth.get_user_by_req(request) user = UserID.from_string(user_id) if requester.user != user: - allowed = yield self.presence_handler.is_visible( + allowed = await self.presence_handler.is_visible( observed_user=user, observer_user=requester.user ) if not allowed: raise AuthError(403, "You are not allowed to see their presence.") - state = yield self.presence_handler.get_state(target_user=user) + state = await self.presence_handler.get_state(target_user=user) state = format_user_presence_state(state, self.clock.time_msec()) return 200, state - @defer.inlineCallbacks - def on_PUT(self, request, user_id): - requester = yield self.auth.get_user_by_req(request) + async def on_PUT(self, request, user_id): + requester = await self.auth.get_user_by_req(request) user = UserID.from_string(user_id) if requester.user != user: @@ -86,7 +82,7 @@ class PresenceStatusRestServlet(RestServlet): raise SynapseError(400, "Unable to parse state") if self.hs.config.use_presence: - yield self.presence_handler.set_state(user, state) + await self.presence_handler.set_state(user, state) return 200, {} diff --git a/synapse/rest/client/v1/profile.py b/synapse/rest/client/v1/profile.py index bbce2e2b71..1eac8a44c5 100644 --- a/synapse/rest/client/v1/profile.py +++ b/synapse/rest/client/v1/profile.py @@ -14,7 +14,6 @@ # limitations under the License. """ This module contains REST servlets to do with profile: /profile/ """ -from twisted.internet import defer from synapse.http.servlet import RestServlet, parse_json_object_from_request from synapse.rest.client.v2_alpha._base import client_patterns @@ -30,19 +29,18 @@ class ProfileDisplaynameRestServlet(RestServlet): self.profile_handler = hs.get_profile_handler() self.auth = hs.get_auth() - @defer.inlineCallbacks - def on_GET(self, request, user_id): + async def on_GET(self, request, user_id): requester_user = None if self.hs.config.require_auth_for_profile_requests: - requester = yield self.auth.get_user_by_req(request) + requester = await self.auth.get_user_by_req(request) requester_user = requester.user user = UserID.from_string(user_id) - yield self.profile_handler.check_profile_query_allowed(user, requester_user) + await self.profile_handler.check_profile_query_allowed(user, requester_user) - displayname = yield self.profile_handler.get_displayname(user) + displayname = await self.profile_handler.get_displayname(user) ret = {} if displayname is not None: @@ -50,11 +48,10 @@ class ProfileDisplaynameRestServlet(RestServlet): return 200, ret - @defer.inlineCallbacks - def on_PUT(self, request, user_id): - requester = yield self.auth.get_user_by_req(request, allow_guest=True) + async def on_PUT(self, request, user_id): + requester = await self.auth.get_user_by_req(request, allow_guest=True) user = UserID.from_string(user_id) - is_admin = yield self.auth.is_server_admin(requester.user) + is_admin = await self.auth.is_server_admin(requester.user) content = parse_json_object_from_request(request) @@ -63,7 +60,7 @@ class ProfileDisplaynameRestServlet(RestServlet): except Exception: return 400, "Unable to parse name" - yield self.profile_handler.set_displayname(user, requester, new_name, is_admin) + await self.profile_handler.set_displayname(user, requester, new_name, is_admin) return 200, {} @@ -80,19 +77,18 @@ class ProfileAvatarURLRestServlet(RestServlet): self.profile_handler = hs.get_profile_handler() self.auth = hs.get_auth() - @defer.inlineCallbacks - def on_GET(self, request, user_id): + async def on_GET(self, request, user_id): requester_user = None if self.hs.config.require_auth_for_profile_requests: - requester = yield self.auth.get_user_by_req(request) + requester = await self.auth.get_user_by_req(request) requester_user = requester.user user = UserID.from_string(user_id) - yield self.profile_handler.check_profile_query_allowed(user, requester_user) + await self.profile_handler.check_profile_query_allowed(user, requester_user) - avatar_url = yield self.profile_handler.get_avatar_url(user) + avatar_url = await self.profile_handler.get_avatar_url(user) ret = {} if avatar_url is not None: @@ -100,11 +96,10 @@ class ProfileAvatarURLRestServlet(RestServlet): return 200, ret - @defer.inlineCallbacks - def on_PUT(self, request, user_id): - requester = yield self.auth.get_user_by_req(request) + async def on_PUT(self, request, user_id): + requester = await self.auth.get_user_by_req(request) user = UserID.from_string(user_id) - is_admin = yield self.auth.is_server_admin(requester.user) + is_admin = await self.auth.is_server_admin(requester.user) content = parse_json_object_from_request(request) try: @@ -112,7 +107,7 @@ class ProfileAvatarURLRestServlet(RestServlet): except Exception: return 400, "Unable to parse name" - yield self.profile_handler.set_avatar_url(user, requester, new_name, is_admin) + await self.profile_handler.set_avatar_url(user, requester, new_name, is_admin) return 200, {} @@ -129,20 +124,19 @@ class ProfileRestServlet(RestServlet): self.profile_handler = hs.get_profile_handler() self.auth = hs.get_auth() - @defer.inlineCallbacks - def on_GET(self, request, user_id): + async def on_GET(self, request, user_id): requester_user = None if self.hs.config.require_auth_for_profile_requests: - requester = yield self.auth.get_user_by_req(request) + requester = await self.auth.get_user_by_req(request) requester_user = requester.user user = UserID.from_string(user_id) - yield self.profile_handler.check_profile_query_allowed(user, requester_user) + await self.profile_handler.check_profile_query_allowed(user, requester_user) - displayname = yield self.profile_handler.get_displayname(user) - avatar_url = yield self.profile_handler.get_avatar_url(user) + displayname = await self.profile_handler.get_displayname(user) + avatar_url = await self.profile_handler.get_avatar_url(user) ret = {} if displayname is not None: diff --git a/synapse/rest/client/v1/push_rule.py b/synapse/rest/client/v1/push_rule.py index 9f8c3d09e3..4f74600239 100644 --- a/synapse/rest/client/v1/push_rule.py +++ b/synapse/rest/client/v1/push_rule.py @@ -13,7 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from twisted.internet import defer from synapse.api.errors import ( NotFoundError, @@ -46,8 +45,7 @@ class PushRuleRestServlet(RestServlet): self.notifier = hs.get_notifier() self._is_worker = hs.config.worker_app is not None - @defer.inlineCallbacks - def on_PUT(self, request, path): + async def on_PUT(self, request, path): if self._is_worker: raise Exception("Cannot handle PUT /push_rules on worker") @@ -57,7 +55,7 @@ class PushRuleRestServlet(RestServlet): except InvalidRuleException as e: raise SynapseError(400, str(e)) - requester = yield self.auth.get_user_by_req(request) + requester = await self.auth.get_user_by_req(request) if "/" in spec["rule_id"] or "\\" in spec["rule_id"]: raise SynapseError(400, "rule_id may not contain slashes") @@ -67,7 +65,7 @@ class PushRuleRestServlet(RestServlet): user_id = requester.user.to_string() if "attr" in spec: - yield self.set_rule_attr(user_id, spec, content) + await self.set_rule_attr(user_id, spec, content) self.notify_user(user_id) return 200, {} @@ -91,7 +89,7 @@ class PushRuleRestServlet(RestServlet): after = _namespaced_rule_id(spec, after) try: - yield self.store.add_push_rule( + await self.store.add_push_rule( user_id=user_id, rule_id=_namespaced_rule_id_from_spec(spec), priority_class=priority_class, @@ -108,20 +106,19 @@ class PushRuleRestServlet(RestServlet): return 200, {} - @defer.inlineCallbacks - def on_DELETE(self, request, path): + async def on_DELETE(self, request, path): if self._is_worker: raise Exception("Cannot handle DELETE /push_rules on worker") spec = _rule_spec_from_path([x for x in path.split("/")]) - requester = yield self.auth.get_user_by_req(request) + requester = await self.auth.get_user_by_req(request) user_id = requester.user.to_string() namespaced_rule_id = _namespaced_rule_id_from_spec(spec) try: - yield self.store.delete_push_rule(user_id, namespaced_rule_id) + await self.store.delete_push_rule(user_id, namespaced_rule_id) self.notify_user(user_id) return 200, {} except StoreError as e: @@ -130,15 +127,14 @@ class PushRuleRestServlet(RestServlet): else: raise - @defer.inlineCallbacks - def on_GET(self, request, path): - requester = yield self.auth.get_user_by_req(request) + async def on_GET(self, request, path): + requester = await self.auth.get_user_by_req(request) user_id = requester.user.to_string() # we build up the full structure and then decide which bits of it # to send which means doing unnecessary work sometimes but is # is probably not going to make a whole lot of difference - rules = yield self.store.get_push_rules_for_user(user_id) + rules = await self.store.get_push_rules_for_user(user_id) rules = format_push_rules_for_user(requester.user, rules) diff --git a/synapse/rest/client/v1/pusher.py b/synapse/rest/client/v1/pusher.py index 41660682d9..0791866f55 100644 --- a/synapse/rest/client/v1/pusher.py +++ b/synapse/rest/client/v1/pusher.py @@ -15,8 +15,6 @@ import logging -from twisted.internet import defer - from synapse.api.errors import Codes, StoreError, SynapseError from synapse.http.server import finish_request from synapse.http.servlet import ( @@ -39,12 +37,11 @@ class PushersRestServlet(RestServlet): self.hs = hs self.auth = hs.get_auth() - @defer.inlineCallbacks - def on_GET(self, request): - requester = yield self.auth.get_user_by_req(request) + async def on_GET(self, request): + requester = await self.auth.get_user_by_req(request) user = requester.user - pushers = yield self.hs.get_datastore().get_pushers_by_user_id(user.to_string()) + pushers = await self.hs.get_datastore().get_pushers_by_user_id(user.to_string()) allowed_keys = [ "app_display_name", @@ -78,9 +75,8 @@ class PushersSetRestServlet(RestServlet): self.notifier = hs.get_notifier() self.pusher_pool = self.hs.get_pusherpool() - @defer.inlineCallbacks - def on_POST(self, request): - requester = yield self.auth.get_user_by_req(request) + async def on_POST(self, request): + requester = await self.auth.get_user_by_req(request) user = requester.user content = parse_json_object_from_request(request) @@ -91,7 +87,7 @@ class PushersSetRestServlet(RestServlet): and "kind" in content and content["kind"] is None ): - yield self.pusher_pool.remove_pusher( + await self.pusher_pool.remove_pusher( content["app_id"], content["pushkey"], user_id=user.to_string() ) return 200, {} @@ -117,14 +113,14 @@ class PushersSetRestServlet(RestServlet): append = content["append"] if not append: - yield self.pusher_pool.remove_pushers_by_app_id_and_pushkey_not_user( + await self.pusher_pool.remove_pushers_by_app_id_and_pushkey_not_user( app_id=content["app_id"], pushkey=content["pushkey"], not_user_id=user.to_string(), ) try: - yield self.pusher_pool.add_pusher( + await self.pusher_pool.add_pusher( user_id=user.to_string(), access_token=requester.access_token_id, kind=content["kind"], @@ -164,16 +160,15 @@ class PushersRemoveRestServlet(RestServlet): self.auth = hs.get_auth() self.pusher_pool = self.hs.get_pusherpool() - @defer.inlineCallbacks - def on_GET(self, request): - requester = yield self.auth.get_user_by_req(request, rights="delete_pusher") + async def on_GET(self, request): + requester = await self.auth.get_user_by_req(request, rights="delete_pusher") user = requester.user app_id = parse_string(request, "app_id", required=True) pushkey = parse_string(request, "pushkey", required=True) try: - yield self.pusher_pool.remove_pusher( + await self.pusher_pool.remove_pusher( app_id=app_id, pushkey=pushkey, user_id=user.to_string() ) except StoreError as se: diff --git a/synapse/rest/client/v1/voip.py b/synapse/rest/client/v1/voip.py index 2afdbb89e5..747d46eac2 100644 --- a/synapse/rest/client/v1/voip.py +++ b/synapse/rest/client/v1/voip.py @@ -17,8 +17,6 @@ import base64 import hashlib import hmac -from twisted.internet import defer - from synapse.http.servlet import RestServlet from synapse.rest.client.v2_alpha._base import client_patterns @@ -31,9 +29,8 @@ class VoipRestServlet(RestServlet): self.hs = hs self.auth = hs.get_auth() - @defer.inlineCallbacks - def on_GET(self, request): - requester = yield self.auth.get_user_by_req( + async def on_GET(self, request): + requester = await self.auth.get_user_by_req( request, self.hs.config.turn_allow_guests ) From 4ca3ef10b9a8d15cf351d67d574088d944c2e3b1 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 5 Dec 2019 15:53:10 +0000 Subject: [PATCH 0607/1623] Fixup tests --- tests/rest/client/v1/test_presence.py | 3 +++ tests/rest/client/v1/test_profile.py | 10 +++++++++- tests/utils.py | 4 +++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/tests/rest/client/v1/test_presence.py b/tests/rest/client/v1/test_presence.py index 66c2b68707..0fdff79aa7 100644 --- a/tests/rest/client/v1/test_presence.py +++ b/tests/rest/client/v1/test_presence.py @@ -15,6 +15,8 @@ from mock import Mock +from twisted.internet import defer + from synapse.rest.client.v1 import presence from synapse.types import UserID @@ -36,6 +38,7 @@ class PresenceTestCase(unittest.HomeserverTestCase): ) hs.presence_handler = Mock() + hs.presence_handler.set_state.return_value = defer.succeed(None) return hs diff --git a/tests/rest/client/v1/test_profile.py b/tests/rest/client/v1/test_profile.py index 140d8b3772..12c5e95cb5 100644 --- a/tests/rest/client/v1/test_profile.py +++ b/tests/rest/client/v1/test_profile.py @@ -52,6 +52,14 @@ class MockHandlerProfileTestCase(unittest.TestCase): ] ) + self.mock_handler.get_displayname.return_value = defer.succeed(Mock()) + self.mock_handler.set_displayname.return_value = defer.succeed(Mock()) + self.mock_handler.get_avatar_url.return_value = defer.succeed(Mock()) + self.mock_handler.set_avatar_url.return_value = defer.succeed(Mock()) + self.mock_handler.check_profile_query_allowed.return_value = defer.succeed( + Mock() + ) + hs = yield setup_test_homeserver( self.addCleanup, "test", @@ -63,7 +71,7 @@ class MockHandlerProfileTestCase(unittest.TestCase): ) def _get_user_by_req(request=None, allow_guest=False): - return synapse.types.create_requester(myid) + return defer.succeed(synapse.types.create_requester(myid)) hs.get_auth().get_user_by_req = _get_user_by_req diff --git a/tests/utils.py b/tests/utils.py index de2ac1ed33..c57da59191 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -461,7 +461,9 @@ class MockHttpResource(HttpServer): try: args = [urlparse.unquote(u) for u in matcher.groups()] - (code, response) = yield func(mock_request, *args) + (code, response) = yield defer.ensureDeferred( + func(mock_request, *args) + ) return code, response except CodeMessageException as e: return (e.code, cs_error(e.msg, code=e.errcode)) From 410bfd035a5f2b77ad94d297f689fc29b9197218 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 5 Dec 2019 15:54:15 +0000 Subject: [PATCH 0608/1623] Newsfile --- changelog.d/6482.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6482.misc diff --git a/changelog.d/6482.misc b/changelog.d/6482.misc new file mode 100644 index 0000000000..bdef9cf40a --- /dev/null +++ b/changelog.d/6482.misc @@ -0,0 +1 @@ +Port synapse.rest.client.v1 to async/await. From 9c41ba4c5fe6e554bc885a1bef8ed51d14ec0f3e Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 5 Dec 2019 16:46:37 +0000 Subject: [PATCH 0609/1623] Port rest.client.v2 --- synapse/rest/client/v2_alpha/_base.py | 2 +- synapse/rest/client/v2_alpha/account.py | 119 ++++----- synapse/rest/client/v2_alpha/account_data.py | 30 +-- .../rest/client/v2_alpha/account_validity.py | 17 +- synapse/rest/client/v2_alpha/auth.py | 9 +- synapse/rest/client/v2_alpha/capabilities.py | 9 +- synapse/rest/client/v2_alpha/devices.py | 41 ++-- synapse/rest/client/v2_alpha/filter.py | 16 +- synapse/rest/client/v2_alpha/groups.py | 226 ++++++++---------- synapse/rest/client/v2_alpha/keys.py | 46 ++-- synapse/rest/client/v2_alpha/notifications.py | 15 +- synapse/rest/client/v2_alpha/openid.py | 9 +- synapse/rest/client/v2_alpha/register.py | 72 +++--- synapse/rest/client/v2_alpha/relations.py | 56 +++-- synapse/rest/client/v2_alpha/report_event.py | 9 +- synapse/rest/client/v2_alpha/room_keys.py | 51 ++-- .../v2_alpha/room_upgrade_rest_servlet.py | 9 +- synapse/rest/client/v2_alpha/sendtodevice.py | 9 +- synapse/rest/client/v2_alpha/sync.py | 54 ++--- synapse/rest/client/v2_alpha/tags.py | 23 +- synapse/rest/client/v2_alpha/thirdparty.py | 30 +-- synapse/rest/client/v2_alpha/tokenrefresh.py | 5 +- .../rest/client/v2_alpha/user_directory.py | 9 +- 23 files changed, 361 insertions(+), 505 deletions(-) diff --git a/synapse/rest/client/v2_alpha/_base.py b/synapse/rest/client/v2_alpha/_base.py index 8250ae0ae1..2a3f4dd58f 100644 --- a/synapse/rest/client/v2_alpha/_base.py +++ b/synapse/rest/client/v2_alpha/_base.py @@ -78,7 +78,7 @@ def interactive_auth_handler(orig): """ def wrapped(*args, **kwargs): - res = defer.maybeDeferred(orig, *args, **kwargs) + res = defer.ensureDeferred(orig(*args, **kwargs)) res.addErrback(_catch_incomplete_interactive_auth) return res diff --git a/synapse/rest/client/v2_alpha/account.py b/synapse/rest/client/v2_alpha/account.py index ad674239ab..fc240f5cf8 100644 --- a/synapse/rest/client/v2_alpha/account.py +++ b/synapse/rest/client/v2_alpha/account.py @@ -18,8 +18,6 @@ import logging from six.moves import http_client -from twisted.internet import defer - from synapse.api.constants import LoginType from synapse.api.errors import Codes, SynapseError, ThreepidValidationError from synapse.config.emailconfig import ThreepidBehaviour @@ -67,8 +65,7 @@ class EmailPasswordRequestTokenRestServlet(RestServlet): template_text=template_text, ) - @defer.inlineCallbacks - def on_POST(self, request): + async def on_POST(self, request): if self.config.threepid_behaviour_email == ThreepidBehaviour.OFF: if self.config.local_threepid_handling_disabled_due_to_email_config: logger.warning( @@ -95,7 +92,7 @@ class EmailPasswordRequestTokenRestServlet(RestServlet): Codes.THREEPID_DENIED, ) - existing_user_id = yield self.hs.get_datastore().get_user_id_by_threepid( + existing_user_id = await self.hs.get_datastore().get_user_id_by_threepid( "email", email ) @@ -106,7 +103,7 @@ class EmailPasswordRequestTokenRestServlet(RestServlet): assert self.hs.config.account_threepid_delegate_email # Have the configured identity server handle the request - ret = yield self.identity_handler.requestEmailToken( + ret = await self.identity_handler.requestEmailToken( self.hs.config.account_threepid_delegate_email, email, client_secret, @@ -115,7 +112,7 @@ class EmailPasswordRequestTokenRestServlet(RestServlet): ) else: # Send password reset emails from Synapse - sid = yield self.identity_handler.send_threepid_validation( + sid = await self.identity_handler.send_threepid_validation( email, client_secret, send_attempt, @@ -153,8 +150,7 @@ class PasswordResetSubmitTokenServlet(RestServlet): [self.config.email_password_reset_template_failure_html], ) - @defer.inlineCallbacks - def on_GET(self, request, medium): + async def on_GET(self, request, medium): # We currently only handle threepid token submissions for email if medium != "email": raise SynapseError( @@ -176,7 +172,7 @@ class PasswordResetSubmitTokenServlet(RestServlet): # Attempt to validate a 3PID session try: # Mark the session as valid - next_link = yield self.store.validate_threepid_session( + next_link = await self.store.validate_threepid_session( sid, client_secret, token, self.clock.time_msec() ) @@ -218,8 +214,7 @@ class PasswordRestServlet(RestServlet): self._set_password_handler = hs.get_set_password_handler() @interactive_auth_handler - @defer.inlineCallbacks - def on_POST(self, request): + async def on_POST(self, request): body = parse_json_object_from_request(request) # there are two possibilities here. Either the user does not have an @@ -233,14 +228,14 @@ class PasswordRestServlet(RestServlet): # In the second case, we require a password to confirm their identity. if self.auth.has_access_token(request): - requester = yield self.auth.get_user_by_req(request) - params = yield self.auth_handler.validate_user_via_ui_auth( + requester = await self.auth.get_user_by_req(request) + params = await self.auth_handler.validate_user_via_ui_auth( requester, body, self.hs.get_ip_from_request(request) ) user_id = requester.user.to_string() else: requester = None - result, params, _ = yield self.auth_handler.check_auth( + result, params, _ = await self.auth_handler.check_auth( [[LoginType.EMAIL_IDENTITY]], body, self.hs.get_ip_from_request(request) ) @@ -254,7 +249,7 @@ class PasswordRestServlet(RestServlet): # (See add_threepid in synapse/handlers/auth.py) threepid["address"] = threepid["address"].lower() # if using email, we must know about the email they're authing with! - threepid_user_id = yield self.datastore.get_user_id_by_threepid( + threepid_user_id = await self.datastore.get_user_id_by_threepid( threepid["medium"], threepid["address"] ) if not threepid_user_id: @@ -267,7 +262,7 @@ class PasswordRestServlet(RestServlet): assert_params_in_dict(params, ["new_password"]) new_password = params["new_password"] - yield self._set_password_handler.set_password(user_id, new_password, requester) + await self._set_password_handler.set_password(user_id, new_password, requester) return 200, {} @@ -286,8 +281,7 @@ class DeactivateAccountRestServlet(RestServlet): self._deactivate_account_handler = hs.get_deactivate_account_handler() @interactive_auth_handler - @defer.inlineCallbacks - def on_POST(self, request): + async def on_POST(self, request): body = parse_json_object_from_request(request) erase = body.get("erase", False) if not isinstance(erase, bool): @@ -297,19 +291,19 @@ class DeactivateAccountRestServlet(RestServlet): Codes.BAD_JSON, ) - requester = yield self.auth.get_user_by_req(request) + requester = await self.auth.get_user_by_req(request) # allow ASes to dectivate their own users if requester.app_service: - yield self._deactivate_account_handler.deactivate_account( + await self._deactivate_account_handler.deactivate_account( requester.user.to_string(), erase ) return 200, {} - yield self.auth_handler.validate_user_via_ui_auth( + await self.auth_handler.validate_user_via_ui_auth( requester, body, self.hs.get_ip_from_request(request) ) - result = yield self._deactivate_account_handler.deactivate_account( + result = await self._deactivate_account_handler.deactivate_account( requester.user.to_string(), erase, id_server=body.get("id_server") ) if result: @@ -346,8 +340,7 @@ class EmailThreepidRequestTokenRestServlet(RestServlet): template_text=template_text, ) - @defer.inlineCallbacks - def on_POST(self, request): + async def on_POST(self, request): if self.config.threepid_behaviour_email == ThreepidBehaviour.OFF: if self.config.local_threepid_handling_disabled_due_to_email_config: logger.warning( @@ -371,7 +364,7 @@ class EmailThreepidRequestTokenRestServlet(RestServlet): Codes.THREEPID_DENIED, ) - existing_user_id = yield self.store.get_user_id_by_threepid( + existing_user_id = await self.store.get_user_id_by_threepid( "email", body["email"] ) @@ -382,7 +375,7 @@ class EmailThreepidRequestTokenRestServlet(RestServlet): assert self.hs.config.account_threepid_delegate_email # Have the configured identity server handle the request - ret = yield self.identity_handler.requestEmailToken( + ret = await self.identity_handler.requestEmailToken( self.hs.config.account_threepid_delegate_email, email, client_secret, @@ -391,7 +384,7 @@ class EmailThreepidRequestTokenRestServlet(RestServlet): ) else: # Send threepid validation emails from Synapse - sid = yield self.identity_handler.send_threepid_validation( + sid = await self.identity_handler.send_threepid_validation( email, client_secret, send_attempt, @@ -414,8 +407,7 @@ class MsisdnThreepidRequestTokenRestServlet(RestServlet): self.store = self.hs.get_datastore() self.identity_handler = hs.get_handlers().identity_handler - @defer.inlineCallbacks - def on_POST(self, request): + async def on_POST(self, request): body = parse_json_object_from_request(request) assert_params_in_dict( body, ["client_secret", "country", "phone_number", "send_attempt"] @@ -435,7 +427,7 @@ class MsisdnThreepidRequestTokenRestServlet(RestServlet): Codes.THREEPID_DENIED, ) - existing_user_id = yield self.store.get_user_id_by_threepid("msisdn", msisdn) + existing_user_id = await self.store.get_user_id_by_threepid("msisdn", msisdn) if existing_user_id is not None: raise SynapseError(400, "MSISDN is already in use", Codes.THREEPID_IN_USE) @@ -450,7 +442,7 @@ class MsisdnThreepidRequestTokenRestServlet(RestServlet): "Adding phone numbers to user account is not supported by this homeserver", ) - ret = yield self.identity_handler.requestMsisdnToken( + ret = await self.identity_handler.requestMsisdnToken( self.hs.config.account_threepid_delegate_msisdn, country, phone_number, @@ -484,8 +476,7 @@ class AddThreepidEmailSubmitTokenServlet(RestServlet): [self.config.email_add_threepid_template_failure_html], ) - @defer.inlineCallbacks - def on_GET(self, request): + async def on_GET(self, request): if self.config.threepid_behaviour_email == ThreepidBehaviour.OFF: if self.config.local_threepid_handling_disabled_due_to_email_config: logger.warning( @@ -508,7 +499,7 @@ class AddThreepidEmailSubmitTokenServlet(RestServlet): # Attempt to validate a 3PID session try: # Mark the session as valid - next_link = yield self.store.validate_threepid_session( + next_link = await self.store.validate_threepid_session( sid, client_secret, token, self.clock.time_msec() ) @@ -558,8 +549,7 @@ class AddThreepidMsisdnSubmitTokenServlet(RestServlet): self.store = hs.get_datastore() self.identity_handler = hs.get_handlers().identity_handler - @defer.inlineCallbacks - def on_POST(self, request): + async def on_POST(self, request): if not self.config.account_threepid_delegate_msisdn: raise SynapseError( 400, @@ -571,7 +561,7 @@ class AddThreepidMsisdnSubmitTokenServlet(RestServlet): assert_params_in_dict(body, ["client_secret", "sid", "token"]) # Proxy submit_token request to msisdn threepid delegate - response = yield self.identity_handler.proxy_msisdn_submit_token( + response = await self.identity_handler.proxy_msisdn_submit_token( self.config.account_threepid_delegate_msisdn, body["client_secret"], body["sid"], @@ -591,17 +581,15 @@ class ThreepidRestServlet(RestServlet): self.auth_handler = hs.get_auth_handler() self.datastore = self.hs.get_datastore() - @defer.inlineCallbacks - def on_GET(self, request): - requester = yield self.auth.get_user_by_req(request) + async def on_GET(self, request): + requester = await self.auth.get_user_by_req(request) - threepids = yield self.datastore.user_get_threepids(requester.user.to_string()) + threepids = await self.datastore.user_get_threepids(requester.user.to_string()) return 200, {"threepids": threepids} - @defer.inlineCallbacks - def on_POST(self, request): - requester = yield self.auth.get_user_by_req(request) + async def on_POST(self, request): + requester = await self.auth.get_user_by_req(request) user_id = requester.user.to_string() body = parse_json_object_from_request(request) @@ -615,11 +603,11 @@ class ThreepidRestServlet(RestServlet): client_secret = threepid_creds["client_secret"] sid = threepid_creds["sid"] - validation_session = yield self.identity_handler.validate_threepid_session( + validation_session = await self.identity_handler.validate_threepid_session( client_secret, sid ) if validation_session: - yield self.auth_handler.add_threepid( + await self.auth_handler.add_threepid( user_id, validation_session["medium"], validation_session["address"], @@ -643,9 +631,8 @@ class ThreepidAddRestServlet(RestServlet): self.auth_handler = hs.get_auth_handler() @interactive_auth_handler - @defer.inlineCallbacks - def on_POST(self, request): - requester = yield self.auth.get_user_by_req(request) + async def on_POST(self, request): + requester = await self.auth.get_user_by_req(request) user_id = requester.user.to_string() body = parse_json_object_from_request(request) @@ -653,15 +640,15 @@ class ThreepidAddRestServlet(RestServlet): client_secret = body["client_secret"] sid = body["sid"] - yield self.auth_handler.validate_user_via_ui_auth( + await self.auth_handler.validate_user_via_ui_auth( requester, body, self.hs.get_ip_from_request(request) ) - validation_session = yield self.identity_handler.validate_threepid_session( + validation_session = await self.identity_handler.validate_threepid_session( client_secret, sid ) if validation_session: - yield self.auth_handler.add_threepid( + await self.auth_handler.add_threepid( user_id, validation_session["medium"], validation_session["address"], @@ -683,8 +670,7 @@ class ThreepidBindRestServlet(RestServlet): self.identity_handler = hs.get_handlers().identity_handler self.auth = hs.get_auth() - @defer.inlineCallbacks - def on_POST(self, request): + async def on_POST(self, request): body = parse_json_object_from_request(request) assert_params_in_dict(body, ["id_server", "sid", "client_secret"]) @@ -693,10 +679,10 @@ class ThreepidBindRestServlet(RestServlet): client_secret = body["client_secret"] id_access_token = body.get("id_access_token") # optional - requester = yield self.auth.get_user_by_req(request) + requester = await self.auth.get_user_by_req(request) user_id = requester.user.to_string() - yield self.identity_handler.bind_threepid( + await self.identity_handler.bind_threepid( client_secret, sid, user_id, id_server, id_access_token ) @@ -713,12 +699,11 @@ class ThreepidUnbindRestServlet(RestServlet): self.auth = hs.get_auth() self.datastore = self.hs.get_datastore() - @defer.inlineCallbacks - def on_POST(self, request): + async def on_POST(self, request): """Unbind the given 3pid from a specific identity server, or identity servers that are known to have this 3pid bound """ - requester = yield self.auth.get_user_by_req(request) + requester = await self.auth.get_user_by_req(request) body = parse_json_object_from_request(request) assert_params_in_dict(body, ["medium", "address"]) @@ -728,7 +713,7 @@ class ThreepidUnbindRestServlet(RestServlet): # Attempt to unbind the threepid from an identity server. If id_server is None, try to # unbind from all identity servers this threepid has been added to in the past - result = yield self.identity_handler.try_unbind_threepid( + result = await self.identity_handler.try_unbind_threepid( requester.user.to_string(), {"address": address, "medium": medium, "id_server": id_server}, ) @@ -743,16 +728,15 @@ class ThreepidDeleteRestServlet(RestServlet): self.auth = hs.get_auth() self.auth_handler = hs.get_auth_handler() - @defer.inlineCallbacks - def on_POST(self, request): + async def on_POST(self, request): body = parse_json_object_from_request(request) assert_params_in_dict(body, ["medium", "address"]) - requester = yield self.auth.get_user_by_req(request) + requester = await self.auth.get_user_by_req(request) user_id = requester.user.to_string() try: - ret = yield self.auth_handler.delete_threepid( + ret = await self.auth_handler.delete_threepid( user_id, body["medium"], body["address"], body.get("id_server") ) except Exception: @@ -777,9 +761,8 @@ class WhoamiRestServlet(RestServlet): super(WhoamiRestServlet, self).__init__() self.auth = hs.get_auth() - @defer.inlineCallbacks - def on_GET(self, request): - requester = yield self.auth.get_user_by_req(request) + async def on_GET(self, request): + requester = await self.auth.get_user_by_req(request) return 200, {"user_id": requester.user.to_string()} diff --git a/synapse/rest/client/v2_alpha/account_data.py b/synapse/rest/client/v2_alpha/account_data.py index f0db204ffa..64eb7fec3b 100644 --- a/synapse/rest/client/v2_alpha/account_data.py +++ b/synapse/rest/client/v2_alpha/account_data.py @@ -15,8 +15,6 @@ import logging -from twisted.internet import defer - from synapse.api.errors import AuthError, NotFoundError, SynapseError from synapse.http.servlet import RestServlet, parse_json_object_from_request @@ -41,15 +39,14 @@ class AccountDataServlet(RestServlet): self.store = hs.get_datastore() self.notifier = hs.get_notifier() - @defer.inlineCallbacks - def on_PUT(self, request, user_id, account_data_type): - requester = yield self.auth.get_user_by_req(request) + async def on_PUT(self, request, user_id, account_data_type): + requester = await self.auth.get_user_by_req(request) if user_id != requester.user.to_string(): raise AuthError(403, "Cannot add account data for other users.") body = parse_json_object_from_request(request) - max_id = yield self.store.add_account_data_for_user( + max_id = await self.store.add_account_data_for_user( user_id, account_data_type, body ) @@ -57,13 +54,12 @@ class AccountDataServlet(RestServlet): return 200, {} - @defer.inlineCallbacks - def on_GET(self, request, user_id, account_data_type): - requester = yield self.auth.get_user_by_req(request) + async def on_GET(self, request, user_id, account_data_type): + requester = await self.auth.get_user_by_req(request) if user_id != requester.user.to_string(): raise AuthError(403, "Cannot get account data for other users.") - event = yield self.store.get_global_account_data_by_type_for_user( + event = await self.store.get_global_account_data_by_type_for_user( account_data_type, user_id ) @@ -91,9 +87,8 @@ class RoomAccountDataServlet(RestServlet): self.store = hs.get_datastore() self.notifier = hs.get_notifier() - @defer.inlineCallbacks - def on_PUT(self, request, user_id, room_id, account_data_type): - requester = yield self.auth.get_user_by_req(request) + async def on_PUT(self, request, user_id, room_id, account_data_type): + requester = await self.auth.get_user_by_req(request) if user_id != requester.user.to_string(): raise AuthError(403, "Cannot add account data for other users.") @@ -106,7 +101,7 @@ class RoomAccountDataServlet(RestServlet): " Use /rooms/!roomId:server.name/read_markers", ) - max_id = yield self.store.add_account_data_to_room( + max_id = await self.store.add_account_data_to_room( user_id, room_id, account_data_type, body ) @@ -114,13 +109,12 @@ class RoomAccountDataServlet(RestServlet): return 200, {} - @defer.inlineCallbacks - def on_GET(self, request, user_id, room_id, account_data_type): - requester = yield self.auth.get_user_by_req(request) + async def on_GET(self, request, user_id, room_id, account_data_type): + requester = await self.auth.get_user_by_req(request) if user_id != requester.user.to_string(): raise AuthError(403, "Cannot get account data for other users.") - event = yield self.store.get_account_data_for_room_and_type( + event = await self.store.get_account_data_for_room_and_type( user_id, room_id, account_data_type ) diff --git a/synapse/rest/client/v2_alpha/account_validity.py b/synapse/rest/client/v2_alpha/account_validity.py index 33f6a23028..2f10fa64e2 100644 --- a/synapse/rest/client/v2_alpha/account_validity.py +++ b/synapse/rest/client/v2_alpha/account_validity.py @@ -15,8 +15,6 @@ import logging -from twisted.internet import defer - from synapse.api.errors import AuthError, SynapseError from synapse.http.server import finish_request from synapse.http.servlet import RestServlet @@ -45,13 +43,12 @@ class AccountValidityRenewServlet(RestServlet): self.success_html = hs.config.account_validity.account_renewed_html_content self.failure_html = hs.config.account_validity.invalid_token_html_content - @defer.inlineCallbacks - def on_GET(self, request): + async def on_GET(self, request): if b"token" not in request.args: raise SynapseError(400, "Missing renewal token") renewal_token = request.args[b"token"][0] - token_valid = yield self.account_activity_handler.renew_account( + token_valid = await self.account_activity_handler.renew_account( renewal_token.decode("utf8") ) @@ -67,7 +64,6 @@ class AccountValidityRenewServlet(RestServlet): request.setHeader(b"Content-Length", b"%d" % (len(response),)) request.write(response.encode("utf8")) finish_request(request) - defer.returnValue(None) class AccountValiditySendMailServlet(RestServlet): @@ -85,18 +81,17 @@ class AccountValiditySendMailServlet(RestServlet): self.auth = hs.get_auth() self.account_validity = self.hs.config.account_validity - @defer.inlineCallbacks - def on_POST(self, request): + async def on_POST(self, request): if not self.account_validity.renew_by_email_enabled: raise AuthError( 403, "Account renewal via email is disabled on this server." ) - requester = yield self.auth.get_user_by_req(request, allow_expired=True) + requester = await self.auth.get_user_by_req(request, allow_expired=True) user_id = requester.user.to_string() - yield self.account_activity_handler.send_renewal_email_to_user(user_id) + await self.account_activity_handler.send_renewal_email_to_user(user_id) - defer.returnValue((200, {})) + return 200, {} def register_servlets(hs, http_server): diff --git a/synapse/rest/client/v2_alpha/auth.py b/synapse/rest/client/v2_alpha/auth.py index f21aff39e5..7a256b6ecb 100644 --- a/synapse/rest/client/v2_alpha/auth.py +++ b/synapse/rest/client/v2_alpha/auth.py @@ -15,8 +15,6 @@ import logging -from twisted.internet import defer - from synapse.api.constants import LoginType from synapse.api.errors import SynapseError from synapse.api.urls import CLIENT_API_PREFIX @@ -171,8 +169,7 @@ class AuthRestServlet(RestServlet): else: raise SynapseError(404, "Unknown auth stage type") - @defer.inlineCallbacks - def on_POST(self, request, stagetype): + async def on_POST(self, request, stagetype): session = parse_string(request, "session") if not session: @@ -186,7 +183,7 @@ class AuthRestServlet(RestServlet): authdict = {"response": response, "session": session} - success = yield self.auth_handler.add_oob_auth( + success = await self.auth_handler.add_oob_auth( LoginType.RECAPTCHA, authdict, self.hs.get_ip_from_request(request) ) @@ -215,7 +212,7 @@ class AuthRestServlet(RestServlet): session = request.args["session"][0] authdict = {"session": session} - success = yield self.auth_handler.add_oob_auth( + success = await self.auth_handler.add_oob_auth( LoginType.TERMS, authdict, self.hs.get_ip_from_request(request) ) diff --git a/synapse/rest/client/v2_alpha/capabilities.py b/synapse/rest/client/v2_alpha/capabilities.py index acd58af193..fe9d019c44 100644 --- a/synapse/rest/client/v2_alpha/capabilities.py +++ b/synapse/rest/client/v2_alpha/capabilities.py @@ -14,8 +14,6 @@ # limitations under the License. import logging -from twisted.internet import defer - from synapse.api.room_versions import KNOWN_ROOM_VERSIONS from synapse.http.servlet import RestServlet @@ -40,10 +38,9 @@ class CapabilitiesRestServlet(RestServlet): self.auth = hs.get_auth() self.store = hs.get_datastore() - @defer.inlineCallbacks - def on_GET(self, request): - requester = yield self.auth.get_user_by_req(request, allow_guest=True) - user = yield self.store.get_user_by_id(requester.user.to_string()) + async def on_GET(self, request): + requester = await self.auth.get_user_by_req(request, allow_guest=True) + user = await self.store.get_user_by_id(requester.user.to_string()) change_password = bool(user["password_hash"]) response = { diff --git a/synapse/rest/client/v2_alpha/devices.py b/synapse/rest/client/v2_alpha/devices.py index 26d0235208..94ff73f384 100644 --- a/synapse/rest/client/v2_alpha/devices.py +++ b/synapse/rest/client/v2_alpha/devices.py @@ -15,8 +15,6 @@ import logging -from twisted.internet import defer - from synapse.api import errors from synapse.http.servlet import ( RestServlet, @@ -42,10 +40,9 @@ class DevicesRestServlet(RestServlet): self.auth = hs.get_auth() self.device_handler = hs.get_device_handler() - @defer.inlineCallbacks - def on_GET(self, request): - requester = yield self.auth.get_user_by_req(request, allow_guest=True) - devices = yield self.device_handler.get_devices_by_user( + async def on_GET(self, request): + requester = await self.auth.get_user_by_req(request, allow_guest=True) + devices = await self.device_handler.get_devices_by_user( requester.user.to_string() ) return 200, {"devices": devices} @@ -67,9 +64,8 @@ class DeleteDevicesRestServlet(RestServlet): self.auth_handler = hs.get_auth_handler() @interactive_auth_handler - @defer.inlineCallbacks - def on_POST(self, request): - requester = yield self.auth.get_user_by_req(request) + async def on_POST(self, request): + requester = await self.auth.get_user_by_req(request) try: body = parse_json_object_from_request(request) @@ -84,11 +80,11 @@ class DeleteDevicesRestServlet(RestServlet): assert_params_in_dict(body, ["devices"]) - yield self.auth_handler.validate_user_via_ui_auth( + await self.auth_handler.validate_user_via_ui_auth( requester, body, self.hs.get_ip_from_request(request) ) - yield self.device_handler.delete_devices( + await self.device_handler.delete_devices( requester.user.to_string(), body["devices"] ) return 200, {} @@ -108,18 +104,16 @@ class DeviceRestServlet(RestServlet): self.device_handler = hs.get_device_handler() self.auth_handler = hs.get_auth_handler() - @defer.inlineCallbacks - def on_GET(self, request, device_id): - requester = yield self.auth.get_user_by_req(request, allow_guest=True) - device = yield self.device_handler.get_device( + async def on_GET(self, request, device_id): + requester = await self.auth.get_user_by_req(request, allow_guest=True) + device = await self.device_handler.get_device( requester.user.to_string(), device_id ) return 200, device @interactive_auth_handler - @defer.inlineCallbacks - def on_DELETE(self, request, device_id): - requester = yield self.auth.get_user_by_req(request) + async def on_DELETE(self, request, device_id): + requester = await self.auth.get_user_by_req(request) try: body = parse_json_object_from_request(request) @@ -132,19 +126,18 @@ class DeviceRestServlet(RestServlet): else: raise - yield self.auth_handler.validate_user_via_ui_auth( + await self.auth_handler.validate_user_via_ui_auth( requester, body, self.hs.get_ip_from_request(request) ) - yield self.device_handler.delete_device(requester.user.to_string(), device_id) + await self.device_handler.delete_device(requester.user.to_string(), device_id) return 200, {} - @defer.inlineCallbacks - def on_PUT(self, request, device_id): - requester = yield self.auth.get_user_by_req(request, allow_guest=True) + async def on_PUT(self, request, device_id): + requester = await self.auth.get_user_by_req(request, allow_guest=True) body = parse_json_object_from_request(request) - yield self.device_handler.update_device( + await self.device_handler.update_device( requester.user.to_string(), device_id, body ) return 200, {} diff --git a/synapse/rest/client/v2_alpha/filter.py b/synapse/rest/client/v2_alpha/filter.py index 17a8bc7366..b28da017cd 100644 --- a/synapse/rest/client/v2_alpha/filter.py +++ b/synapse/rest/client/v2_alpha/filter.py @@ -15,8 +15,6 @@ import logging -from twisted.internet import defer - from synapse.api.errors import AuthError, NotFoundError, StoreError, SynapseError from synapse.http.servlet import RestServlet, parse_json_object_from_request from synapse.types import UserID @@ -35,10 +33,9 @@ class GetFilterRestServlet(RestServlet): self.auth = hs.get_auth() self.filtering = hs.get_filtering() - @defer.inlineCallbacks - def on_GET(self, request, user_id, filter_id): + async def on_GET(self, request, user_id, filter_id): target_user = UserID.from_string(user_id) - requester = yield self.auth.get_user_by_req(request) + requester = await self.auth.get_user_by_req(request) if target_user != requester.user: raise AuthError(403, "Cannot get filters for other users") @@ -52,7 +49,7 @@ class GetFilterRestServlet(RestServlet): raise SynapseError(400, "Invalid filter_id") try: - filter_collection = yield self.filtering.get_user_filter( + filter_collection = await self.filtering.get_user_filter( user_localpart=target_user.localpart, filter_id=filter_id ) except StoreError as e: @@ -72,11 +69,10 @@ class CreateFilterRestServlet(RestServlet): self.auth = hs.get_auth() self.filtering = hs.get_filtering() - @defer.inlineCallbacks - def on_POST(self, request, user_id): + async def on_POST(self, request, user_id): target_user = UserID.from_string(user_id) - requester = yield self.auth.get_user_by_req(request) + requester = await self.auth.get_user_by_req(request) if target_user != requester.user: raise AuthError(403, "Cannot create filters for other users") @@ -87,7 +83,7 @@ class CreateFilterRestServlet(RestServlet): content = parse_json_object_from_request(request) set_timeline_upper_limit(content, self.hs.config.filter_timeline_limit) - filter_id = yield self.filtering.add_user_filter( + filter_id = await self.filtering.add_user_filter( user_localpart=target_user.localpart, user_filter=content ) diff --git a/synapse/rest/client/v2_alpha/groups.py b/synapse/rest/client/v2_alpha/groups.py index 999a0fa80c..d84a6d7e11 100644 --- a/synapse/rest/client/v2_alpha/groups.py +++ b/synapse/rest/client/v2_alpha/groups.py @@ -16,8 +16,6 @@ import logging -from twisted.internet import defer - from synapse.http.servlet import RestServlet, parse_json_object_from_request from synapse.types import GroupID @@ -38,24 +36,22 @@ class GroupServlet(RestServlet): self.clock = hs.get_clock() self.groups_handler = hs.get_groups_local_handler() - @defer.inlineCallbacks - def on_GET(self, request, group_id): - requester = yield self.auth.get_user_by_req(request, allow_guest=True) + async def on_GET(self, request, group_id): + requester = await self.auth.get_user_by_req(request, allow_guest=True) requester_user_id = requester.user.to_string() - group_description = yield self.groups_handler.get_group_profile( + group_description = await self.groups_handler.get_group_profile( group_id, requester_user_id ) return 200, group_description - @defer.inlineCallbacks - def on_POST(self, request, group_id): - requester = yield self.auth.get_user_by_req(request) + async def on_POST(self, request, group_id): + requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() content = parse_json_object_from_request(request) - yield self.groups_handler.update_group_profile( + await self.groups_handler.update_group_profile( group_id, requester_user_id, content ) @@ -74,12 +70,11 @@ class GroupSummaryServlet(RestServlet): self.clock = hs.get_clock() self.groups_handler = hs.get_groups_local_handler() - @defer.inlineCallbacks - def on_GET(self, request, group_id): - requester = yield self.auth.get_user_by_req(request, allow_guest=True) + async def on_GET(self, request, group_id): + requester = await self.auth.get_user_by_req(request, allow_guest=True) requester_user_id = requester.user.to_string() - get_group_summary = yield self.groups_handler.get_group_summary( + get_group_summary = await self.groups_handler.get_group_summary( group_id, requester_user_id ) @@ -106,13 +101,12 @@ class GroupSummaryRoomsCatServlet(RestServlet): self.clock = hs.get_clock() self.groups_handler = hs.get_groups_local_handler() - @defer.inlineCallbacks - def on_PUT(self, request, group_id, category_id, room_id): - requester = yield self.auth.get_user_by_req(request) + async def on_PUT(self, request, group_id, category_id, room_id): + requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() content = parse_json_object_from_request(request) - resp = yield self.groups_handler.update_group_summary_room( + resp = await self.groups_handler.update_group_summary_room( group_id, requester_user_id, room_id=room_id, @@ -122,12 +116,11 @@ class GroupSummaryRoomsCatServlet(RestServlet): return 200, resp - @defer.inlineCallbacks - def on_DELETE(self, request, group_id, category_id, room_id): - requester = yield self.auth.get_user_by_req(request) + async def on_DELETE(self, request, group_id, category_id, room_id): + requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() - resp = yield self.groups_handler.delete_group_summary_room( + resp = await self.groups_handler.delete_group_summary_room( group_id, requester_user_id, room_id=room_id, category_id=category_id ) @@ -148,35 +141,32 @@ class GroupCategoryServlet(RestServlet): self.clock = hs.get_clock() self.groups_handler = hs.get_groups_local_handler() - @defer.inlineCallbacks - def on_GET(self, request, group_id, category_id): - requester = yield self.auth.get_user_by_req(request, allow_guest=True) + async def on_GET(self, request, group_id, category_id): + requester = await self.auth.get_user_by_req(request, allow_guest=True) requester_user_id = requester.user.to_string() - category = yield self.groups_handler.get_group_category( + category = await self.groups_handler.get_group_category( group_id, requester_user_id, category_id=category_id ) return 200, category - @defer.inlineCallbacks - def on_PUT(self, request, group_id, category_id): - requester = yield self.auth.get_user_by_req(request) + async def on_PUT(self, request, group_id, category_id): + requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() content = parse_json_object_from_request(request) - resp = yield self.groups_handler.update_group_category( + resp = await self.groups_handler.update_group_category( group_id, requester_user_id, category_id=category_id, content=content ) return 200, resp - @defer.inlineCallbacks - def on_DELETE(self, request, group_id, category_id): - requester = yield self.auth.get_user_by_req(request) + async def on_DELETE(self, request, group_id, category_id): + requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() - resp = yield self.groups_handler.delete_group_category( + resp = await self.groups_handler.delete_group_category( group_id, requester_user_id, category_id=category_id ) @@ -195,12 +185,11 @@ class GroupCategoriesServlet(RestServlet): self.clock = hs.get_clock() self.groups_handler = hs.get_groups_local_handler() - @defer.inlineCallbacks - def on_GET(self, request, group_id): - requester = yield self.auth.get_user_by_req(request, allow_guest=True) + async def on_GET(self, request, group_id): + requester = await self.auth.get_user_by_req(request, allow_guest=True) requester_user_id = requester.user.to_string() - category = yield self.groups_handler.get_group_categories( + category = await self.groups_handler.get_group_categories( group_id, requester_user_id ) @@ -219,35 +208,32 @@ class GroupRoleServlet(RestServlet): self.clock = hs.get_clock() self.groups_handler = hs.get_groups_local_handler() - @defer.inlineCallbacks - def on_GET(self, request, group_id, role_id): - requester = yield self.auth.get_user_by_req(request, allow_guest=True) + async def on_GET(self, request, group_id, role_id): + requester = await self.auth.get_user_by_req(request, allow_guest=True) requester_user_id = requester.user.to_string() - category = yield self.groups_handler.get_group_role( + category = await self.groups_handler.get_group_role( group_id, requester_user_id, role_id=role_id ) return 200, category - @defer.inlineCallbacks - def on_PUT(self, request, group_id, role_id): - requester = yield self.auth.get_user_by_req(request) + async def on_PUT(self, request, group_id, role_id): + requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() content = parse_json_object_from_request(request) - resp = yield self.groups_handler.update_group_role( + resp = await self.groups_handler.update_group_role( group_id, requester_user_id, role_id=role_id, content=content ) return 200, resp - @defer.inlineCallbacks - def on_DELETE(self, request, group_id, role_id): - requester = yield self.auth.get_user_by_req(request) + async def on_DELETE(self, request, group_id, role_id): + requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() - resp = yield self.groups_handler.delete_group_role( + resp = await self.groups_handler.delete_group_role( group_id, requester_user_id, role_id=role_id ) @@ -266,12 +252,11 @@ class GroupRolesServlet(RestServlet): self.clock = hs.get_clock() self.groups_handler = hs.get_groups_local_handler() - @defer.inlineCallbacks - def on_GET(self, request, group_id): - requester = yield self.auth.get_user_by_req(request, allow_guest=True) + async def on_GET(self, request, group_id): + requester = await self.auth.get_user_by_req(request, allow_guest=True) requester_user_id = requester.user.to_string() - category = yield self.groups_handler.get_group_roles( + category = await self.groups_handler.get_group_roles( group_id, requester_user_id ) @@ -298,13 +283,12 @@ class GroupSummaryUsersRoleServlet(RestServlet): self.clock = hs.get_clock() self.groups_handler = hs.get_groups_local_handler() - @defer.inlineCallbacks - def on_PUT(self, request, group_id, role_id, user_id): - requester = yield self.auth.get_user_by_req(request) + async def on_PUT(self, request, group_id, role_id, user_id): + requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() content = parse_json_object_from_request(request) - resp = yield self.groups_handler.update_group_summary_user( + resp = await self.groups_handler.update_group_summary_user( group_id, requester_user_id, user_id=user_id, @@ -314,12 +298,11 @@ class GroupSummaryUsersRoleServlet(RestServlet): return 200, resp - @defer.inlineCallbacks - def on_DELETE(self, request, group_id, role_id, user_id): - requester = yield self.auth.get_user_by_req(request) + async def on_DELETE(self, request, group_id, role_id, user_id): + requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() - resp = yield self.groups_handler.delete_group_summary_user( + resp = await self.groups_handler.delete_group_summary_user( group_id, requester_user_id, user_id=user_id, role_id=role_id ) @@ -338,12 +321,11 @@ class GroupRoomServlet(RestServlet): self.clock = hs.get_clock() self.groups_handler = hs.get_groups_local_handler() - @defer.inlineCallbacks - def on_GET(self, request, group_id): - requester = yield self.auth.get_user_by_req(request, allow_guest=True) + async def on_GET(self, request, group_id): + requester = await self.auth.get_user_by_req(request, allow_guest=True) requester_user_id = requester.user.to_string() - result = yield self.groups_handler.get_rooms_in_group( + result = await self.groups_handler.get_rooms_in_group( group_id, requester_user_id ) @@ -362,12 +344,11 @@ class GroupUsersServlet(RestServlet): self.clock = hs.get_clock() self.groups_handler = hs.get_groups_local_handler() - @defer.inlineCallbacks - def on_GET(self, request, group_id): - requester = yield self.auth.get_user_by_req(request, allow_guest=True) + async def on_GET(self, request, group_id): + requester = await self.auth.get_user_by_req(request, allow_guest=True) requester_user_id = requester.user.to_string() - result = yield self.groups_handler.get_users_in_group( + result = await self.groups_handler.get_users_in_group( group_id, requester_user_id ) @@ -386,12 +367,11 @@ class GroupInvitedUsersServlet(RestServlet): self.clock = hs.get_clock() self.groups_handler = hs.get_groups_local_handler() - @defer.inlineCallbacks - def on_GET(self, request, group_id): - requester = yield self.auth.get_user_by_req(request) + async def on_GET(self, request, group_id): + requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() - result = yield self.groups_handler.get_invited_users_in_group( + result = await self.groups_handler.get_invited_users_in_group( group_id, requester_user_id ) @@ -409,14 +389,13 @@ class GroupSettingJoinPolicyServlet(RestServlet): self.auth = hs.get_auth() self.groups_handler = hs.get_groups_local_handler() - @defer.inlineCallbacks - def on_PUT(self, request, group_id): - requester = yield self.auth.get_user_by_req(request) + async def on_PUT(self, request, group_id): + requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() content = parse_json_object_from_request(request) - result = yield self.groups_handler.set_group_join_policy( + result = await self.groups_handler.set_group_join_policy( group_id, requester_user_id, content ) @@ -436,9 +415,8 @@ class GroupCreateServlet(RestServlet): self.groups_handler = hs.get_groups_local_handler() self.server_name = hs.hostname - @defer.inlineCallbacks - def on_POST(self, request): - requester = yield self.auth.get_user_by_req(request) + async def on_POST(self, request): + requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() # TODO: Create group on remote server @@ -446,7 +424,7 @@ class GroupCreateServlet(RestServlet): localpart = content.pop("localpart") group_id = GroupID(localpart, self.server_name).to_string() - result = yield self.groups_handler.create_group( + result = await self.groups_handler.create_group( group_id, requester_user_id, content ) @@ -467,24 +445,22 @@ class GroupAdminRoomsServlet(RestServlet): self.clock = hs.get_clock() self.groups_handler = hs.get_groups_local_handler() - @defer.inlineCallbacks - def on_PUT(self, request, group_id, room_id): - requester = yield self.auth.get_user_by_req(request) + async def on_PUT(self, request, group_id, room_id): + requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() content = parse_json_object_from_request(request) - result = yield self.groups_handler.add_room_to_group( + result = await self.groups_handler.add_room_to_group( group_id, requester_user_id, room_id, content ) return 200, result - @defer.inlineCallbacks - def on_DELETE(self, request, group_id, room_id): - requester = yield self.auth.get_user_by_req(request) + async def on_DELETE(self, request, group_id, room_id): + requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() - result = yield self.groups_handler.remove_room_from_group( + result = await self.groups_handler.remove_room_from_group( group_id, requester_user_id, room_id ) @@ -506,13 +482,12 @@ class GroupAdminRoomsConfigServlet(RestServlet): self.clock = hs.get_clock() self.groups_handler = hs.get_groups_local_handler() - @defer.inlineCallbacks - def on_PUT(self, request, group_id, room_id, config_key): - requester = yield self.auth.get_user_by_req(request) + async def on_PUT(self, request, group_id, room_id, config_key): + requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() content = parse_json_object_from_request(request) - result = yield self.groups_handler.update_room_in_group( + result = await self.groups_handler.update_room_in_group( group_id, requester_user_id, room_id, config_key, content ) @@ -535,14 +510,13 @@ class GroupAdminUsersInviteServlet(RestServlet): self.store = hs.get_datastore() self.is_mine_id = hs.is_mine_id - @defer.inlineCallbacks - def on_PUT(self, request, group_id, user_id): - requester = yield self.auth.get_user_by_req(request) + async def on_PUT(self, request, group_id, user_id): + requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() content = parse_json_object_from_request(request) config = content.get("config", {}) - result = yield self.groups_handler.invite( + result = await self.groups_handler.invite( group_id, user_id, requester_user_id, config ) @@ -563,13 +537,12 @@ class GroupAdminUsersKickServlet(RestServlet): self.clock = hs.get_clock() self.groups_handler = hs.get_groups_local_handler() - @defer.inlineCallbacks - def on_PUT(self, request, group_id, user_id): - requester = yield self.auth.get_user_by_req(request) + async def on_PUT(self, request, group_id, user_id): + requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() content = parse_json_object_from_request(request) - result = yield self.groups_handler.remove_user_from_group( + result = await self.groups_handler.remove_user_from_group( group_id, user_id, requester_user_id, content ) @@ -588,13 +561,12 @@ class GroupSelfLeaveServlet(RestServlet): self.clock = hs.get_clock() self.groups_handler = hs.get_groups_local_handler() - @defer.inlineCallbacks - def on_PUT(self, request, group_id): - requester = yield self.auth.get_user_by_req(request) + async def on_PUT(self, request, group_id): + requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() content = parse_json_object_from_request(request) - result = yield self.groups_handler.remove_user_from_group( + result = await self.groups_handler.remove_user_from_group( group_id, requester_user_id, requester_user_id, content ) @@ -613,13 +585,12 @@ class GroupSelfJoinServlet(RestServlet): self.clock = hs.get_clock() self.groups_handler = hs.get_groups_local_handler() - @defer.inlineCallbacks - def on_PUT(self, request, group_id): - requester = yield self.auth.get_user_by_req(request) + async def on_PUT(self, request, group_id): + requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() content = parse_json_object_from_request(request) - result = yield self.groups_handler.join_group( + result = await self.groups_handler.join_group( group_id, requester_user_id, content ) @@ -638,13 +609,12 @@ class GroupSelfAcceptInviteServlet(RestServlet): self.clock = hs.get_clock() self.groups_handler = hs.get_groups_local_handler() - @defer.inlineCallbacks - def on_PUT(self, request, group_id): - requester = yield self.auth.get_user_by_req(request) + async def on_PUT(self, request, group_id): + requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() content = parse_json_object_from_request(request) - result = yield self.groups_handler.accept_invite( + result = await self.groups_handler.accept_invite( group_id, requester_user_id, content ) @@ -663,14 +633,13 @@ class GroupSelfUpdatePublicityServlet(RestServlet): self.clock = hs.get_clock() self.store = hs.get_datastore() - @defer.inlineCallbacks - def on_PUT(self, request, group_id): - requester = yield self.auth.get_user_by_req(request) + async def on_PUT(self, request, group_id): + requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() content = parse_json_object_from_request(request) publicise = content["publicise"] - yield self.store.update_group_publicity(group_id, requester_user_id, publicise) + await self.store.update_group_publicity(group_id, requester_user_id, publicise) return 200, {} @@ -688,11 +657,10 @@ class PublicisedGroupsForUserServlet(RestServlet): self.store = hs.get_datastore() self.groups_handler = hs.get_groups_local_handler() - @defer.inlineCallbacks - def on_GET(self, request, user_id): - yield self.auth.get_user_by_req(request, allow_guest=True) + async def on_GET(self, request, user_id): + await self.auth.get_user_by_req(request, allow_guest=True) - result = yield self.groups_handler.get_publicised_groups_for_user(user_id) + result = await self.groups_handler.get_publicised_groups_for_user(user_id) return 200, result @@ -710,14 +678,13 @@ class PublicisedGroupsForUsersServlet(RestServlet): self.store = hs.get_datastore() self.groups_handler = hs.get_groups_local_handler() - @defer.inlineCallbacks - def on_POST(self, request): - yield self.auth.get_user_by_req(request, allow_guest=True) + async def on_POST(self, request): + await self.auth.get_user_by_req(request, allow_guest=True) content = parse_json_object_from_request(request) user_ids = content["user_ids"] - result = yield self.groups_handler.bulk_get_publicised_groups(user_ids) + result = await self.groups_handler.bulk_get_publicised_groups(user_ids) return 200, result @@ -734,12 +701,11 @@ class GroupsForUserServlet(RestServlet): self.clock = hs.get_clock() self.groups_handler = hs.get_groups_local_handler() - @defer.inlineCallbacks - def on_GET(self, request): - requester = yield self.auth.get_user_by_req(request, allow_guest=True) + async def on_GET(self, request): + requester = await self.auth.get_user_by_req(request, allow_guest=True) requester_user_id = requester.user.to_string() - result = yield self.groups_handler.get_joined_groups(requester_user_id) + result = await self.groups_handler.get_joined_groups(requester_user_id) return 200, result diff --git a/synapse/rest/client/v2_alpha/keys.py b/synapse/rest/client/v2_alpha/keys.py index 341567ae21..f7ed4daf90 100644 --- a/synapse/rest/client/v2_alpha/keys.py +++ b/synapse/rest/client/v2_alpha/keys.py @@ -16,8 +16,6 @@ import logging -from twisted.internet import defer - from synapse.api.errors import SynapseError from synapse.http.servlet import ( RestServlet, @@ -71,9 +69,8 @@ class KeyUploadServlet(RestServlet): self.e2e_keys_handler = hs.get_e2e_keys_handler() @trace(opname="upload_keys") - @defer.inlineCallbacks - def on_POST(self, request, device_id): - requester = yield self.auth.get_user_by_req(request, allow_guest=True) + async def on_POST(self, request, device_id): + requester = await self.auth.get_user_by_req(request, allow_guest=True) user_id = requester.user.to_string() body = parse_json_object_from_request(request) @@ -103,7 +100,7 @@ class KeyUploadServlet(RestServlet): 400, "To upload keys, you must pass device_id when authenticating" ) - result = yield self.e2e_keys_handler.upload_keys_for_user( + result = await self.e2e_keys_handler.upload_keys_for_user( user_id, device_id, body ) return 200, result @@ -154,13 +151,12 @@ class KeyQueryServlet(RestServlet): self.auth = hs.get_auth() self.e2e_keys_handler = hs.get_e2e_keys_handler() - @defer.inlineCallbacks - def on_POST(self, request): - requester = yield self.auth.get_user_by_req(request, allow_guest=True) + async def on_POST(self, request): + requester = await self.auth.get_user_by_req(request, allow_guest=True) user_id = requester.user.to_string() timeout = parse_integer(request, "timeout", 10 * 1000) body = parse_json_object_from_request(request) - result = yield self.e2e_keys_handler.query_devices(body, timeout, user_id) + result = await self.e2e_keys_handler.query_devices(body, timeout, user_id) return 200, result @@ -185,9 +181,8 @@ class KeyChangesServlet(RestServlet): self.auth = hs.get_auth() self.device_handler = hs.get_device_handler() - @defer.inlineCallbacks - def on_GET(self, request): - requester = yield self.auth.get_user_by_req(request, allow_guest=True) + async def on_GET(self, request): + requester = await self.auth.get_user_by_req(request, allow_guest=True) from_token_string = parse_string(request, "from") set_tag("from", from_token_string) @@ -200,7 +195,7 @@ class KeyChangesServlet(RestServlet): user_id = requester.user.to_string() - results = yield self.device_handler.get_user_ids_changed(user_id, from_token) + results = await self.device_handler.get_user_ids_changed(user_id, from_token) return 200, results @@ -231,12 +226,11 @@ class OneTimeKeyServlet(RestServlet): self.auth = hs.get_auth() self.e2e_keys_handler = hs.get_e2e_keys_handler() - @defer.inlineCallbacks - def on_POST(self, request): - yield self.auth.get_user_by_req(request, allow_guest=True) + async def on_POST(self, request): + await self.auth.get_user_by_req(request, allow_guest=True) timeout = parse_integer(request, "timeout", 10 * 1000) body = parse_json_object_from_request(request) - result = yield self.e2e_keys_handler.claim_one_time_keys(body, timeout) + result = await self.e2e_keys_handler.claim_one_time_keys(body, timeout) return 200, result @@ -263,17 +257,16 @@ class SigningKeyUploadServlet(RestServlet): self.auth_handler = hs.get_auth_handler() @interactive_auth_handler - @defer.inlineCallbacks - def on_POST(self, request): - requester = yield self.auth.get_user_by_req(request) + async def on_POST(self, request): + requester = await self.auth.get_user_by_req(request) user_id = requester.user.to_string() body = parse_json_object_from_request(request) - yield self.auth_handler.validate_user_via_ui_auth( + await self.auth_handler.validate_user_via_ui_auth( requester, body, self.hs.get_ip_from_request(request) ) - result = yield self.e2e_keys_handler.upload_signing_keys_for_user(user_id, body) + result = await self.e2e_keys_handler.upload_signing_keys_for_user(user_id, body) return 200, result @@ -315,13 +308,12 @@ class SignaturesUploadServlet(RestServlet): self.auth = hs.get_auth() self.e2e_keys_handler = hs.get_e2e_keys_handler() - @defer.inlineCallbacks - def on_POST(self, request): - requester = yield self.auth.get_user_by_req(request, allow_guest=True) + async def on_POST(self, request): + requester = await self.auth.get_user_by_req(request, allow_guest=True) user_id = requester.user.to_string() body = parse_json_object_from_request(request) - result = yield self.e2e_keys_handler.upload_signatures_for_device_keys( + result = await self.e2e_keys_handler.upload_signatures_for_device_keys( user_id, body ) return 200, result diff --git a/synapse/rest/client/v2_alpha/notifications.py b/synapse/rest/client/v2_alpha/notifications.py index 10c1ad5b07..aa911d75ee 100644 --- a/synapse/rest/client/v2_alpha/notifications.py +++ b/synapse/rest/client/v2_alpha/notifications.py @@ -15,8 +15,6 @@ import logging -from twisted.internet import defer - from synapse.events.utils import format_event_for_client_v2_without_room_id from synapse.http.servlet import RestServlet, parse_integer, parse_string @@ -35,9 +33,8 @@ class NotificationsServlet(RestServlet): self.clock = hs.get_clock() self._event_serializer = hs.get_event_client_serializer() - @defer.inlineCallbacks - def on_GET(self, request): - requester = yield self.auth.get_user_by_req(request) + async def on_GET(self, request): + requester = await self.auth.get_user_by_req(request) user_id = requester.user.to_string() from_token = parse_string(request, "from", required=False) @@ -46,16 +43,16 @@ class NotificationsServlet(RestServlet): limit = min(limit, 500) - push_actions = yield self.store.get_push_actions_for_user( + push_actions = await self.store.get_push_actions_for_user( user_id, from_token, limit, only_highlight=(only == "highlight") ) - receipts_by_room = yield self.store.get_receipts_for_user_with_orderings( + receipts_by_room = await self.store.get_receipts_for_user_with_orderings( user_id, "m.read" ) notif_event_ids = [pa["event_id"] for pa in push_actions] - notif_events = yield self.store.get_events(notif_event_ids) + notif_events = await self.store.get_events(notif_event_ids) returned_push_actions = [] @@ -68,7 +65,7 @@ class NotificationsServlet(RestServlet): "actions": pa["actions"], "ts": pa["received_ts"], "event": ( - yield self._event_serializer.serialize_event( + await self._event_serializer.serialize_event( notif_events[pa["event_id"]], self.clock.time_msec(), event_format=format_event_for_client_v2_without_room_id, diff --git a/synapse/rest/client/v2_alpha/openid.py b/synapse/rest/client/v2_alpha/openid.py index b4925c0f59..6ae9a5a8e9 100644 --- a/synapse/rest/client/v2_alpha/openid.py +++ b/synapse/rest/client/v2_alpha/openid.py @@ -16,8 +16,6 @@ import logging -from twisted.internet import defer - from synapse.api.errors import AuthError from synapse.http.servlet import RestServlet, parse_json_object_from_request from synapse.util.stringutils import random_string @@ -68,9 +66,8 @@ class IdTokenServlet(RestServlet): self.clock = hs.get_clock() self.server_name = hs.config.server_name - @defer.inlineCallbacks - def on_POST(self, request, user_id): - requester = yield self.auth.get_user_by_req(request) + async def on_POST(self, request, user_id): + requester = await self.auth.get_user_by_req(request) if user_id != requester.user.to_string(): raise AuthError(403, "Cannot request tokens for other users.") @@ -81,7 +78,7 @@ class IdTokenServlet(RestServlet): token = random_string(24) ts_valid_until_ms = self.clock.time_msec() + self.EXPIRES_MS - yield self.store.insert_open_id_token(token, ts_valid_until_ms, user_id) + await self.store.insert_open_id_token(token, ts_valid_until_ms, user_id) return ( 200, diff --git a/synapse/rest/client/v2_alpha/register.py b/synapse/rest/client/v2_alpha/register.py index 91db923814..66de16a1fa 100644 --- a/synapse/rest/client/v2_alpha/register.py +++ b/synapse/rest/client/v2_alpha/register.py @@ -20,8 +20,6 @@ from typing import List, Union from six import string_types -from twisted.internet import defer - import synapse import synapse.types from synapse.api.constants import LoginType @@ -102,8 +100,7 @@ class EmailRegisterRequestTokenRestServlet(RestServlet): template_text=template_text, ) - @defer.inlineCallbacks - def on_POST(self, request): + async def on_POST(self, request): if self.hs.config.threepid_behaviour_email == ThreepidBehaviour.OFF: if self.hs.config.local_threepid_handling_disabled_due_to_email_config: logger.warning( @@ -129,7 +126,7 @@ class EmailRegisterRequestTokenRestServlet(RestServlet): Codes.THREEPID_DENIED, ) - existing_user_id = yield self.hs.get_datastore().get_user_id_by_threepid( + existing_user_id = await self.hs.get_datastore().get_user_id_by_threepid( "email", body["email"] ) @@ -140,7 +137,7 @@ class EmailRegisterRequestTokenRestServlet(RestServlet): assert self.hs.config.account_threepid_delegate_email # Have the configured identity server handle the request - ret = yield self.identity_handler.requestEmailToken( + ret = await self.identity_handler.requestEmailToken( self.hs.config.account_threepid_delegate_email, email, client_secret, @@ -149,7 +146,7 @@ class EmailRegisterRequestTokenRestServlet(RestServlet): ) else: # Send registration emails from Synapse - sid = yield self.identity_handler.send_threepid_validation( + sid = await self.identity_handler.send_threepid_validation( email, client_secret, send_attempt, @@ -175,8 +172,7 @@ class MsisdnRegisterRequestTokenRestServlet(RestServlet): self.hs = hs self.identity_handler = hs.get_handlers().identity_handler - @defer.inlineCallbacks - def on_POST(self, request): + async def on_POST(self, request): body = parse_json_object_from_request(request) assert_params_in_dict( @@ -197,7 +193,7 @@ class MsisdnRegisterRequestTokenRestServlet(RestServlet): Codes.THREEPID_DENIED, ) - existing_user_id = yield self.hs.get_datastore().get_user_id_by_threepid( + existing_user_id = await self.hs.get_datastore().get_user_id_by_threepid( "msisdn", msisdn ) @@ -215,7 +211,7 @@ class MsisdnRegisterRequestTokenRestServlet(RestServlet): 400, "Registration by phone number is not supported on this homeserver" ) - ret = yield self.identity_handler.requestMsisdnToken( + ret = await self.identity_handler.requestMsisdnToken( self.hs.config.account_threepid_delegate_msisdn, country, phone_number, @@ -258,8 +254,7 @@ class RegistrationSubmitTokenServlet(RestServlet): [self.config.email_registration_template_failure_html], ) - @defer.inlineCallbacks - def on_GET(self, request, medium): + async def on_GET(self, request, medium): if medium != "email": raise SynapseError( 400, "This medium is currently not supported for registration" @@ -280,7 +275,7 @@ class RegistrationSubmitTokenServlet(RestServlet): # Attempt to validate a 3PID session try: # Mark the session as valid - next_link = yield self.store.validate_threepid_session( + next_link = await self.store.validate_threepid_session( sid, client_secret, token, self.clock.time_msec() ) @@ -338,8 +333,7 @@ class UsernameAvailabilityRestServlet(RestServlet): ), ) - @defer.inlineCallbacks - def on_GET(self, request): + async def on_GET(self, request): if not self.hs.config.enable_registration: raise SynapseError( 403, "Registration has been disabled", errcode=Codes.FORBIDDEN @@ -347,11 +341,11 @@ class UsernameAvailabilityRestServlet(RestServlet): ip = self.hs.get_ip_from_request(request) with self.ratelimiter.ratelimit(ip) as wait_deferred: - yield wait_deferred + await wait_deferred username = parse_string(request, "username", required=True) - yield self.registration_handler.check_username(username) + await self.registration_handler.check_username(username) return 200, {"available": True} @@ -382,8 +376,7 @@ class RegisterRestServlet(RestServlet): ) @interactive_auth_handler - @defer.inlineCallbacks - def on_POST(self, request): + async def on_POST(self, request): body = parse_json_object_from_request(request) client_addr = request.getClientIP() @@ -408,7 +401,7 @@ class RegisterRestServlet(RestServlet): kind = request.args[b"kind"][0] if kind == b"guest": - ret = yield self._do_guest_registration(body, address=client_addr) + ret = await self._do_guest_registration(body, address=client_addr) return ret elif kind != b"user": raise UnrecognizedRequestError( @@ -435,7 +428,7 @@ class RegisterRestServlet(RestServlet): appservice = None if self.auth.has_access_token(request): - appservice = yield self.auth.get_appservice_by_req(request) + appservice = await self.auth.get_appservice_by_req(request) # fork off as soon as possible for ASes which have completely # different registration flows to normal users @@ -455,7 +448,7 @@ class RegisterRestServlet(RestServlet): access_token = self.auth.get_access_token_from_request(request) if isinstance(desired_username, string_types): - result = yield self._do_appservice_registration( + result = await self._do_appservice_registration( desired_username, access_token, body ) return 200, result # we throw for non 200 responses @@ -495,13 +488,13 @@ class RegisterRestServlet(RestServlet): ) if desired_username is not None: - yield self.registration_handler.check_username( + await self.registration_handler.check_username( desired_username, guest_access_token=guest_access_token, assigned_user_id=registered_user_id, ) - auth_result, params, session_id = yield self.auth_handler.check_auth( + auth_result, params, session_id = await self.auth_handler.check_auth( self._registration_flows, body, self.hs.get_ip_from_request(request) ) @@ -557,7 +550,7 @@ class RegisterRestServlet(RestServlet): medium = auth_result[login_type]["medium"] address = auth_result[login_type]["address"] - existing_user_id = yield self.store.get_user_id_by_threepid( + existing_user_id = await self.store.get_user_id_by_threepid( medium, address ) @@ -568,7 +561,7 @@ class RegisterRestServlet(RestServlet): Codes.THREEPID_IN_USE, ) - registered_user_id = yield self.registration_handler.register_user( + registered_user_id = await self.registration_handler.register_user( localpart=desired_username, password=new_password, guest_access_token=guest_access_token, @@ -581,7 +574,7 @@ class RegisterRestServlet(RestServlet): if is_threepid_reserved( self.hs.config.mau_limits_reserved_threepids, threepid ): - yield self.store.upsert_monthly_active_user(registered_user_id) + await self.store.upsert_monthly_active_user(registered_user_id) # remember that we've now registered that user account, and with # what user ID (since the user may not have specified) @@ -591,12 +584,12 @@ class RegisterRestServlet(RestServlet): registered = True - return_dict = yield self._create_registration_details( + return_dict = await self._create_registration_details( registered_user_id, params ) if registered: - yield self.registration_handler.post_registration_actions( + await self.registration_handler.post_registration_actions( user_id=registered_user_id, auth_result=auth_result, access_token=return_dict.get("access_token"), @@ -607,15 +600,13 @@ class RegisterRestServlet(RestServlet): def on_OPTIONS(self, _): return 200, {} - @defer.inlineCallbacks - def _do_appservice_registration(self, username, as_token, body): - user_id = yield self.registration_handler.appservice_register( + async def _do_appservice_registration(self, username, as_token, body): + user_id = await self.registration_handler.appservice_register( username, as_token ) - return (yield self._create_registration_details(user_id, body)) + return await self._create_registration_details(user_id, body) - @defer.inlineCallbacks - def _create_registration_details(self, user_id, params): + async def _create_registration_details(self, user_id, params): """Complete registration of newly-registered user Allocates device_id if one was not given; also creates access_token. @@ -631,18 +622,17 @@ class RegisterRestServlet(RestServlet): if not params.get("inhibit_login", False): device_id = params.get("device_id") initial_display_name = params.get("initial_device_display_name") - device_id, access_token = yield self.registration_handler.register_device( + device_id, access_token = await self.registration_handler.register_device( user_id, device_id, initial_display_name, is_guest=False ) result.update({"access_token": access_token, "device_id": device_id}) return result - @defer.inlineCallbacks - def _do_guest_registration(self, params, address=None): + async def _do_guest_registration(self, params, address=None): if not self.hs.config.allow_guest_access: raise SynapseError(403, "Guest access is disabled") - user_id = yield self.registration_handler.register_user( + user_id = await self.registration_handler.register_user( make_guest=True, address=address ) @@ -650,7 +640,7 @@ class RegisterRestServlet(RestServlet): # we have nowhere to store it. device_id = synapse.api.auth.GUEST_DEVICE_ID initial_display_name = params.get("initial_device_display_name") - device_id, access_token = yield self.registration_handler.register_device( + device_id, access_token = await self.registration_handler.register_device( user_id, device_id, initial_display_name, is_guest=True ) diff --git a/synapse/rest/client/v2_alpha/relations.py b/synapse/rest/client/v2_alpha/relations.py index 040b37c504..9be9a34b91 100644 --- a/synapse/rest/client/v2_alpha/relations.py +++ b/synapse/rest/client/v2_alpha/relations.py @@ -21,8 +21,6 @@ any time to reflect changes in the MSC. import logging -from twisted.internet import defer - from synapse.api.constants import EventTypes, RelationTypes from synapse.api.errors import SynapseError from synapse.http.servlet import ( @@ -86,11 +84,10 @@ class RelationSendServlet(RestServlet): request, self.on_PUT_or_POST, request, *args, **kwargs ) - @defer.inlineCallbacks - def on_PUT_or_POST( + async def on_PUT_or_POST( self, request, room_id, parent_id, relation_type, event_type, txn_id=None ): - requester = yield self.auth.get_user_by_req(request, allow_guest=True) + requester = await self.auth.get_user_by_req(request, allow_guest=True) if event_type == EventTypes.Member: # Add relations to a membership is meaningless, so we just deny it @@ -114,7 +111,7 @@ class RelationSendServlet(RestServlet): "sender": requester.user.to_string(), } - event = yield self.event_creation_handler.create_and_send_nonmember_event( + event = await self.event_creation_handler.create_and_send_nonmember_event( requester, event_dict=event_dict, txn_id=txn_id ) @@ -140,17 +137,18 @@ class RelationPaginationServlet(RestServlet): self._event_serializer = hs.get_event_client_serializer() self.event_handler = hs.get_event_handler() - @defer.inlineCallbacks - def on_GET(self, request, room_id, parent_id, relation_type=None, event_type=None): - requester = yield self.auth.get_user_by_req(request, allow_guest=True) + async def on_GET( + self, request, room_id, parent_id, relation_type=None, event_type=None + ): + requester = await self.auth.get_user_by_req(request, allow_guest=True) - yield self.auth.check_in_room_or_world_readable( + await self.auth.check_in_room_or_world_readable( room_id, requester.user.to_string() ) # This gets the original event and checks that a) the event exists and # b) the user is allowed to view it. - event = yield self.event_handler.get_event(requester.user, room_id, parent_id) + event = await self.event_handler.get_event(requester.user, room_id, parent_id) limit = parse_integer(request, "limit", default=5) from_token = parse_string(request, "from") @@ -167,7 +165,7 @@ class RelationPaginationServlet(RestServlet): if to_token: to_token = RelationPaginationToken.from_string(to_token) - pagination_chunk = yield self.store.get_relations_for_event( + pagination_chunk = await self.store.get_relations_for_event( event_id=parent_id, relation_type=relation_type, event_type=event_type, @@ -176,7 +174,7 @@ class RelationPaginationServlet(RestServlet): to_token=to_token, ) - events = yield self.store.get_events_as_list( + events = await self.store.get_events_as_list( [c["event_id"] for c in pagination_chunk.chunk] ) @@ -184,13 +182,13 @@ class RelationPaginationServlet(RestServlet): # We set bundle_aggregations to False when retrieving the original # event because we want the content before relations were applied to # it. - original_event = yield self._event_serializer.serialize_event( + original_event = await self._event_serializer.serialize_event( event, now, bundle_aggregations=False ) # Similarly, we don't allow relations to be applied to relations, so we # return the original relations without any aggregations on top of them # here. - events = yield self._event_serializer.serialize_events( + events = await self._event_serializer.serialize_events( events, now, bundle_aggregations=False ) @@ -232,17 +230,18 @@ class RelationAggregationPaginationServlet(RestServlet): self.store = hs.get_datastore() self.event_handler = hs.get_event_handler() - @defer.inlineCallbacks - def on_GET(self, request, room_id, parent_id, relation_type=None, event_type=None): - requester = yield self.auth.get_user_by_req(request, allow_guest=True) + async def on_GET( + self, request, room_id, parent_id, relation_type=None, event_type=None + ): + requester = await self.auth.get_user_by_req(request, allow_guest=True) - yield self.auth.check_in_room_or_world_readable( + await self.auth.check_in_room_or_world_readable( room_id, requester.user.to_string() ) # This checks that a) the event exists and b) the user is allowed to # view it. - event = yield self.event_handler.get_event(requester.user, room_id, parent_id) + event = await self.event_handler.get_event(requester.user, room_id, parent_id) if relation_type not in (RelationTypes.ANNOTATION, None): raise SynapseError(400, "Relation type must be 'annotation'") @@ -262,7 +261,7 @@ class RelationAggregationPaginationServlet(RestServlet): if to_token: to_token = AggregationPaginationToken.from_string(to_token) - pagination_chunk = yield self.store.get_aggregation_groups_for_event( + pagination_chunk = await self.store.get_aggregation_groups_for_event( event_id=parent_id, event_type=event_type, limit=limit, @@ -311,17 +310,16 @@ class RelationAggregationGroupPaginationServlet(RestServlet): self._event_serializer = hs.get_event_client_serializer() self.event_handler = hs.get_event_handler() - @defer.inlineCallbacks - def on_GET(self, request, room_id, parent_id, relation_type, event_type, key): - requester = yield self.auth.get_user_by_req(request, allow_guest=True) + async def on_GET(self, request, room_id, parent_id, relation_type, event_type, key): + requester = await self.auth.get_user_by_req(request, allow_guest=True) - yield self.auth.check_in_room_or_world_readable( + await self.auth.check_in_room_or_world_readable( room_id, requester.user.to_string() ) # This checks that a) the event exists and b) the user is allowed to # view it. - yield self.event_handler.get_event(requester.user, room_id, parent_id) + await self.event_handler.get_event(requester.user, room_id, parent_id) if relation_type != RelationTypes.ANNOTATION: raise SynapseError(400, "Relation type must be 'annotation'") @@ -336,7 +334,7 @@ class RelationAggregationGroupPaginationServlet(RestServlet): if to_token: to_token = RelationPaginationToken.from_string(to_token) - result = yield self.store.get_relations_for_event( + result = await self.store.get_relations_for_event( event_id=parent_id, relation_type=relation_type, event_type=event_type, @@ -346,12 +344,12 @@ class RelationAggregationGroupPaginationServlet(RestServlet): to_token=to_token, ) - events = yield self.store.get_events_as_list( + events = await self.store.get_events_as_list( [c["event_id"] for c in result.chunk] ) now = self.clock.time_msec() - events = yield self._event_serializer.serialize_events(events, now) + events = await self._event_serializer.serialize_events(events, now) return_value = result.to_dict() return_value["chunk"] = events diff --git a/synapse/rest/client/v2_alpha/report_event.py b/synapse/rest/client/v2_alpha/report_event.py index e7449864cd..f067b5edac 100644 --- a/synapse/rest/client/v2_alpha/report_event.py +++ b/synapse/rest/client/v2_alpha/report_event.py @@ -18,8 +18,6 @@ import logging from six import string_types from six.moves import http_client -from twisted.internet import defer - from synapse.api.errors import Codes, SynapseError from synapse.http.servlet import ( RestServlet, @@ -42,9 +40,8 @@ class ReportEventRestServlet(RestServlet): self.clock = hs.get_clock() self.store = hs.get_datastore() - @defer.inlineCallbacks - def on_POST(self, request, room_id, event_id): - requester = yield self.auth.get_user_by_req(request) + async def on_POST(self, request, room_id, event_id): + requester = await self.auth.get_user_by_req(request) user_id = requester.user.to_string() body = parse_json_object_from_request(request) @@ -63,7 +60,7 @@ class ReportEventRestServlet(RestServlet): Codes.BAD_JSON, ) - yield self.store.add_event_report( + await self.store.add_event_report( room_id=room_id, event_id=event_id, user_id=user_id, diff --git a/synapse/rest/client/v2_alpha/room_keys.py b/synapse/rest/client/v2_alpha/room_keys.py index d83ac8e3c5..38952a1d27 100644 --- a/synapse/rest/client/v2_alpha/room_keys.py +++ b/synapse/rest/client/v2_alpha/room_keys.py @@ -15,8 +15,6 @@ import logging -from twisted.internet import defer - from synapse.api.errors import Codes, NotFoundError, SynapseError from synapse.http.servlet import ( RestServlet, @@ -43,8 +41,7 @@ class RoomKeysServlet(RestServlet): self.auth = hs.get_auth() self.e2e_room_keys_handler = hs.get_e2e_room_keys_handler() - @defer.inlineCallbacks - def on_PUT(self, request, room_id, session_id): + async def on_PUT(self, request, room_id, session_id): """ Uploads one or more encrypted E2E room keys for backup purposes. room_id: the ID of the room the keys are for (optional) @@ -123,7 +120,7 @@ class RoomKeysServlet(RestServlet): } } """ - requester = yield self.auth.get_user_by_req(request, allow_guest=False) + requester = await self.auth.get_user_by_req(request, allow_guest=False) user_id = requester.user.to_string() body = parse_json_object_from_request(request) version = parse_string(request, "version") @@ -134,11 +131,10 @@ class RoomKeysServlet(RestServlet): if room_id: body = {"rooms": {room_id: body}} - ret = yield self.e2e_room_keys_handler.upload_room_keys(user_id, version, body) + ret = await self.e2e_room_keys_handler.upload_room_keys(user_id, version, body) return 200, ret - @defer.inlineCallbacks - def on_GET(self, request, room_id, session_id): + async def on_GET(self, request, room_id, session_id): """ Retrieves one or more encrypted E2E room keys for backup purposes. Symmetric with the PUT version of the API. @@ -190,11 +186,11 @@ class RoomKeysServlet(RestServlet): } } """ - requester = yield self.auth.get_user_by_req(request, allow_guest=False) + requester = await self.auth.get_user_by_req(request, allow_guest=False) user_id = requester.user.to_string() version = parse_string(request, "version") - room_keys = yield self.e2e_room_keys_handler.get_room_keys( + room_keys = await self.e2e_room_keys_handler.get_room_keys( user_id, version, room_id, session_id ) @@ -220,8 +216,7 @@ class RoomKeysServlet(RestServlet): return 200, room_keys - @defer.inlineCallbacks - def on_DELETE(self, request, room_id, session_id): + async def on_DELETE(self, request, room_id, session_id): """ Deletes one or more encrypted E2E room keys for a user for backup purposes. @@ -235,11 +230,11 @@ class RoomKeysServlet(RestServlet): the version must already have been created via the /change_secret API. """ - requester = yield self.auth.get_user_by_req(request, allow_guest=False) + requester = await self.auth.get_user_by_req(request, allow_guest=False) user_id = requester.user.to_string() version = parse_string(request, "version") - ret = yield self.e2e_room_keys_handler.delete_room_keys( + ret = await self.e2e_room_keys_handler.delete_room_keys( user_id, version, room_id, session_id ) return 200, ret @@ -257,8 +252,7 @@ class RoomKeysNewVersionServlet(RestServlet): self.auth = hs.get_auth() self.e2e_room_keys_handler = hs.get_e2e_room_keys_handler() - @defer.inlineCallbacks - def on_POST(self, request): + async def on_POST(self, request): """ Create a new backup version for this user's room_keys with the given info. The version is allocated by the server and returned to the user @@ -288,11 +282,11 @@ class RoomKeysNewVersionServlet(RestServlet): "version": 12345 } """ - requester = yield self.auth.get_user_by_req(request, allow_guest=False) + requester = await self.auth.get_user_by_req(request, allow_guest=False) user_id = requester.user.to_string() info = parse_json_object_from_request(request) - new_version = yield self.e2e_room_keys_handler.create_version(user_id, info) + new_version = await self.e2e_room_keys_handler.create_version(user_id, info) return 200, {"version": new_version} # we deliberately don't have a PUT /version, as these things really should @@ -311,8 +305,7 @@ class RoomKeysVersionServlet(RestServlet): self.auth = hs.get_auth() self.e2e_room_keys_handler = hs.get_e2e_room_keys_handler() - @defer.inlineCallbacks - def on_GET(self, request, version): + async def on_GET(self, request, version): """ Retrieve the version information about a given version of the user's room_keys backup. If the version part is missing, returns info about the @@ -330,18 +323,17 @@ class RoomKeysVersionServlet(RestServlet): "auth_data": "dGhpcyBzaG91bGQgYWN0dWFsbHkgYmUgZW5jcnlwdGVkIGpzb24K" } """ - requester = yield self.auth.get_user_by_req(request, allow_guest=False) + requester = await self.auth.get_user_by_req(request, allow_guest=False) user_id = requester.user.to_string() try: - info = yield self.e2e_room_keys_handler.get_version_info(user_id, version) + info = await self.e2e_room_keys_handler.get_version_info(user_id, version) except SynapseError as e: if e.code == 404: raise SynapseError(404, "No backup found", Codes.NOT_FOUND) return 200, info - @defer.inlineCallbacks - def on_DELETE(self, request, version): + async def on_DELETE(self, request, version): """ Delete the information about a given version of the user's room_keys backup. If the version part is missing, deletes the most @@ -354,14 +346,13 @@ class RoomKeysVersionServlet(RestServlet): if version is None: raise SynapseError(400, "No version specified to delete", Codes.NOT_FOUND) - requester = yield self.auth.get_user_by_req(request, allow_guest=False) + requester = await self.auth.get_user_by_req(request, allow_guest=False) user_id = requester.user.to_string() - yield self.e2e_room_keys_handler.delete_version(user_id, version) + await self.e2e_room_keys_handler.delete_version(user_id, version) return 200, {} - @defer.inlineCallbacks - def on_PUT(self, request, version): + async def on_PUT(self, request, version): """ Update the information about a given version of the user's room_keys backup. @@ -382,7 +373,7 @@ class RoomKeysVersionServlet(RestServlet): Content-Type: application/json {} """ - requester = yield self.auth.get_user_by_req(request, allow_guest=False) + requester = await self.auth.get_user_by_req(request, allow_guest=False) user_id = requester.user.to_string() info = parse_json_object_from_request(request) @@ -391,7 +382,7 @@ class RoomKeysVersionServlet(RestServlet): 400, "No version specified to update", Codes.MISSING_PARAM ) - yield self.e2e_room_keys_handler.update_version(user_id, version, info) + await self.e2e_room_keys_handler.update_version(user_id, version, info) return 200, {} diff --git a/synapse/rest/client/v2_alpha/room_upgrade_rest_servlet.py b/synapse/rest/client/v2_alpha/room_upgrade_rest_servlet.py index d2c3316eb7..ca97330797 100644 --- a/synapse/rest/client/v2_alpha/room_upgrade_rest_servlet.py +++ b/synapse/rest/client/v2_alpha/room_upgrade_rest_servlet.py @@ -15,8 +15,6 @@ import logging -from twisted.internet import defer - from synapse.api.errors import Codes, SynapseError from synapse.api.room_versions import KNOWN_ROOM_VERSIONS from synapse.http.servlet import ( @@ -59,9 +57,8 @@ class RoomUpgradeRestServlet(RestServlet): self._room_creation_handler = hs.get_room_creation_handler() self._auth = hs.get_auth() - @defer.inlineCallbacks - def on_POST(self, request, room_id): - requester = yield self._auth.get_user_by_req(request) + async def on_POST(self, request, room_id): + requester = await self._auth.get_user_by_req(request) content = parse_json_object_from_request(request) assert_params_in_dict(content, ("new_version",)) @@ -74,7 +71,7 @@ class RoomUpgradeRestServlet(RestServlet): Codes.UNSUPPORTED_ROOM_VERSION, ) - new_room_id = yield self._room_creation_handler.upgrade_room( + new_room_id = await self._room_creation_handler.upgrade_room( requester, room_id, new_version ) diff --git a/synapse/rest/client/v2_alpha/sendtodevice.py b/synapse/rest/client/v2_alpha/sendtodevice.py index d90e52ed1a..501b52fb6c 100644 --- a/synapse/rest/client/v2_alpha/sendtodevice.py +++ b/synapse/rest/client/v2_alpha/sendtodevice.py @@ -15,8 +15,6 @@ import logging -from twisted.internet import defer - from synapse.http import servlet from synapse.http.servlet import parse_json_object_from_request from synapse.logging.opentracing import set_tag, trace @@ -51,15 +49,14 @@ class SendToDeviceRestServlet(servlet.RestServlet): request, self._put, request, message_type, txn_id ) - @defer.inlineCallbacks - def _put(self, request, message_type, txn_id): - requester = yield self.auth.get_user_by_req(request, allow_guest=True) + async def _put(self, request, message_type, txn_id): + requester = await self.auth.get_user_by_req(request, allow_guest=True) content = parse_json_object_from_request(request) sender_user_id = requester.user.to_string() - yield self.device_message_handler.send_device_message( + await self.device_message_handler.send_device_message( sender_user_id, message_type, content["messages"] ) diff --git a/synapse/rest/client/v2_alpha/sync.py b/synapse/rest/client/v2_alpha/sync.py index ccd8b17b23..d8292ce29f 100644 --- a/synapse/rest/client/v2_alpha/sync.py +++ b/synapse/rest/client/v2_alpha/sync.py @@ -18,8 +18,6 @@ import logging from canonicaljson import json -from twisted.internet import defer - from synapse.api.constants import PresenceState from synapse.api.errors import Codes, StoreError, SynapseError from synapse.api.filtering import DEFAULT_FILTER_COLLECTION, FilterCollection @@ -87,8 +85,7 @@ class SyncRestServlet(RestServlet): self._server_notices_sender = hs.get_server_notices_sender() self._event_serializer = hs.get_event_client_serializer() - @defer.inlineCallbacks - def on_GET(self, request): + async def on_GET(self, request): if b"from" in request.args: # /events used to use 'from', but /sync uses 'since'. # Lets be helpful and whine if we see a 'from'. @@ -96,7 +93,7 @@ class SyncRestServlet(RestServlet): 400, "'from' is not a valid query parameter. Did you mean 'since'?" ) - requester = yield self.auth.get_user_by_req(request, allow_guest=True) + requester = await self.auth.get_user_by_req(request, allow_guest=True) user = requester.user device_id = requester.device_id @@ -138,7 +135,7 @@ class SyncRestServlet(RestServlet): filter_collection = FilterCollection(filter_object) else: try: - filter_collection = yield self.filtering.get_user_filter( + filter_collection = await self.filtering.get_user_filter( user.localpart, filter_id ) except StoreError as err: @@ -161,20 +158,20 @@ class SyncRestServlet(RestServlet): since_token = None # send any outstanding server notices to the user. - yield self._server_notices_sender.on_user_syncing(user.to_string()) + await self._server_notices_sender.on_user_syncing(user.to_string()) affect_presence = set_presence != PresenceState.OFFLINE if affect_presence: - yield self.presence_handler.set_state( + await self.presence_handler.set_state( user, {"presence": set_presence}, True ) - context = yield self.presence_handler.user_syncing( + context = await self.presence_handler.user_syncing( user.to_string(), affect_presence=affect_presence ) with context: - sync_result = yield self.sync_handler.wait_for_sync_for_user( + sync_result = await self.sync_handler.wait_for_sync_for_user( sync_config, since_token=since_token, timeout=timeout, @@ -182,14 +179,13 @@ class SyncRestServlet(RestServlet): ) time_now = self.clock.time_msec() - response_content = yield self.encode_response( + response_content = await self.encode_response( time_now, sync_result, requester.access_token_id, filter_collection ) return 200, response_content - @defer.inlineCallbacks - def encode_response(self, time_now, sync_result, access_token_id, filter): + async def encode_response(self, time_now, sync_result, access_token_id, filter): if filter.event_format == "client": event_formatter = format_event_for_client_v2_without_room_id elif filter.event_format == "federation": @@ -197,7 +193,7 @@ class SyncRestServlet(RestServlet): else: raise Exception("Unknown event format %s" % (filter.event_format,)) - joined = yield self.encode_joined( + joined = await self.encode_joined( sync_result.joined, time_now, access_token_id, @@ -205,11 +201,11 @@ class SyncRestServlet(RestServlet): event_formatter, ) - invited = yield self.encode_invited( + invited = await self.encode_invited( sync_result.invited, time_now, access_token_id, event_formatter ) - archived = yield self.encode_archived( + archived = await self.encode_archived( sync_result.archived, time_now, access_token_id, @@ -250,8 +246,9 @@ class SyncRestServlet(RestServlet): ] } - @defer.inlineCallbacks - def encode_joined(self, rooms, time_now, token_id, event_fields, event_formatter): + async def encode_joined( + self, rooms, time_now, token_id, event_fields, event_formatter + ): """ Encode the joined rooms in a sync result @@ -272,7 +269,7 @@ class SyncRestServlet(RestServlet): """ joined = {} for room in rooms: - joined[room.room_id] = yield self.encode_room( + joined[room.room_id] = await self.encode_room( room, time_now, token_id, @@ -283,8 +280,7 @@ class SyncRestServlet(RestServlet): return joined - @defer.inlineCallbacks - def encode_invited(self, rooms, time_now, token_id, event_formatter): + async def encode_invited(self, rooms, time_now, token_id, event_formatter): """ Encode the invited rooms in a sync result @@ -304,7 +300,7 @@ class SyncRestServlet(RestServlet): """ invited = {} for room in rooms: - invite = yield self._event_serializer.serialize_event( + invite = await self._event_serializer.serialize_event( room.invite, time_now, token_id=token_id, @@ -319,8 +315,9 @@ class SyncRestServlet(RestServlet): return invited - @defer.inlineCallbacks - def encode_archived(self, rooms, time_now, token_id, event_fields, event_formatter): + async def encode_archived( + self, rooms, time_now, token_id, event_fields, event_formatter + ): """ Encode the archived rooms in a sync result @@ -341,7 +338,7 @@ class SyncRestServlet(RestServlet): """ joined = {} for room in rooms: - joined[room.room_id] = yield self.encode_room( + joined[room.room_id] = await self.encode_room( room, time_now, token_id, @@ -352,8 +349,7 @@ class SyncRestServlet(RestServlet): return joined - @defer.inlineCallbacks - def encode_room( + async def encode_room( self, room, time_now, token_id, joined, only_fields, event_formatter ): """ @@ -401,8 +397,8 @@ class SyncRestServlet(RestServlet): event.room_id, ) - serialized_state = yield serialize(state_events) - serialized_timeline = yield serialize(timeline_events) + serialized_state = await serialize(state_events) + serialized_timeline = await serialize(timeline_events) account_data = room.account_data diff --git a/synapse/rest/client/v2_alpha/tags.py b/synapse/rest/client/v2_alpha/tags.py index 3b555669a0..a3f12e8a77 100644 --- a/synapse/rest/client/v2_alpha/tags.py +++ b/synapse/rest/client/v2_alpha/tags.py @@ -15,8 +15,6 @@ import logging -from twisted.internet import defer - from synapse.api.errors import AuthError from synapse.http.servlet import RestServlet, parse_json_object_from_request @@ -37,13 +35,12 @@ class TagListServlet(RestServlet): self.auth = hs.get_auth() self.store = hs.get_datastore() - @defer.inlineCallbacks - def on_GET(self, request, user_id, room_id): - requester = yield self.auth.get_user_by_req(request) + async def on_GET(self, request, user_id, room_id): + requester = await self.auth.get_user_by_req(request) if user_id != requester.user.to_string(): raise AuthError(403, "Cannot get tags for other users.") - tags = yield self.store.get_tags_for_room(user_id, room_id) + tags = await self.store.get_tags_for_room(user_id, room_id) return 200, {"tags": tags} @@ -64,27 +61,25 @@ class TagServlet(RestServlet): self.store = hs.get_datastore() self.notifier = hs.get_notifier() - @defer.inlineCallbacks - def on_PUT(self, request, user_id, room_id, tag): - requester = yield self.auth.get_user_by_req(request) + async def on_PUT(self, request, user_id, room_id, tag): + requester = await self.auth.get_user_by_req(request) if user_id != requester.user.to_string(): raise AuthError(403, "Cannot add tags for other users.") body = parse_json_object_from_request(request) - max_id = yield self.store.add_tag_to_room(user_id, room_id, tag, body) + max_id = await self.store.add_tag_to_room(user_id, room_id, tag, body) self.notifier.on_new_event("account_data_key", max_id, users=[user_id]) return 200, {} - @defer.inlineCallbacks - def on_DELETE(self, request, user_id, room_id, tag): - requester = yield self.auth.get_user_by_req(request) + async def on_DELETE(self, request, user_id, room_id, tag): + requester = await self.auth.get_user_by_req(request) if user_id != requester.user.to_string(): raise AuthError(403, "Cannot add tags for other users.") - max_id = yield self.store.remove_tag_from_room(user_id, room_id, tag) + max_id = await self.store.remove_tag_from_room(user_id, room_id, tag) self.notifier.on_new_event("account_data_key", max_id, users=[user_id]) diff --git a/synapse/rest/client/v2_alpha/thirdparty.py b/synapse/rest/client/v2_alpha/thirdparty.py index 2e8d672471..23709960ad 100644 --- a/synapse/rest/client/v2_alpha/thirdparty.py +++ b/synapse/rest/client/v2_alpha/thirdparty.py @@ -16,8 +16,6 @@ import logging -from twisted.internet import defer - from synapse.api.constants import ThirdPartyEntityKind from synapse.http.servlet import RestServlet @@ -35,11 +33,10 @@ class ThirdPartyProtocolsServlet(RestServlet): self.auth = hs.get_auth() self.appservice_handler = hs.get_application_service_handler() - @defer.inlineCallbacks - def on_GET(self, request): - yield self.auth.get_user_by_req(request, allow_guest=True) + async def on_GET(self, request): + await self.auth.get_user_by_req(request, allow_guest=True) - protocols = yield self.appservice_handler.get_3pe_protocols() + protocols = await self.appservice_handler.get_3pe_protocols() return 200, protocols @@ -52,11 +49,10 @@ class ThirdPartyProtocolServlet(RestServlet): self.auth = hs.get_auth() self.appservice_handler = hs.get_application_service_handler() - @defer.inlineCallbacks - def on_GET(self, request, protocol): - yield self.auth.get_user_by_req(request, allow_guest=True) + async def on_GET(self, request, protocol): + await self.auth.get_user_by_req(request, allow_guest=True) - protocols = yield self.appservice_handler.get_3pe_protocols( + protocols = await self.appservice_handler.get_3pe_protocols( only_protocol=protocol ) if protocol in protocols: @@ -74,14 +70,13 @@ class ThirdPartyUserServlet(RestServlet): self.auth = hs.get_auth() self.appservice_handler = hs.get_application_service_handler() - @defer.inlineCallbacks - def on_GET(self, request, protocol): - yield self.auth.get_user_by_req(request, allow_guest=True) + async def on_GET(self, request, protocol): + await self.auth.get_user_by_req(request, allow_guest=True) fields = request.args fields.pop(b"access_token", None) - results = yield self.appservice_handler.query_3pe( + results = await self.appservice_handler.query_3pe( ThirdPartyEntityKind.USER, protocol, fields ) @@ -97,14 +92,13 @@ class ThirdPartyLocationServlet(RestServlet): self.auth = hs.get_auth() self.appservice_handler = hs.get_application_service_handler() - @defer.inlineCallbacks - def on_GET(self, request, protocol): - yield self.auth.get_user_by_req(request, allow_guest=True) + async def on_GET(self, request, protocol): + await self.auth.get_user_by_req(request, allow_guest=True) fields = request.args fields.pop(b"access_token", None) - results = yield self.appservice_handler.query_3pe( + results = await self.appservice_handler.query_3pe( ThirdPartyEntityKind.LOCATION, protocol, fields ) diff --git a/synapse/rest/client/v2_alpha/tokenrefresh.py b/synapse/rest/client/v2_alpha/tokenrefresh.py index 2da0f55811..83f3b6b70a 100644 --- a/synapse/rest/client/v2_alpha/tokenrefresh.py +++ b/synapse/rest/client/v2_alpha/tokenrefresh.py @@ -13,8 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from twisted.internet import defer - from synapse.api.errors import AuthError from synapse.http.servlet import RestServlet @@ -32,8 +30,7 @@ class TokenRefreshRestServlet(RestServlet): def __init__(self, hs): super(TokenRefreshRestServlet, self).__init__() - @defer.inlineCallbacks - def on_POST(self, request): + async def on_POST(self, request): raise AuthError(403, "tokenrefresh is no longer supported.") diff --git a/synapse/rest/client/v2_alpha/user_directory.py b/synapse/rest/client/v2_alpha/user_directory.py index 2863affbab..bef91a2d3e 100644 --- a/synapse/rest/client/v2_alpha/user_directory.py +++ b/synapse/rest/client/v2_alpha/user_directory.py @@ -15,8 +15,6 @@ import logging -from twisted.internet import defer - from synapse.api.errors import SynapseError from synapse.http.servlet import RestServlet, parse_json_object_from_request @@ -38,8 +36,7 @@ class UserDirectorySearchRestServlet(RestServlet): self.auth = hs.get_auth() self.user_directory_handler = hs.get_user_directory_handler() - @defer.inlineCallbacks - def on_POST(self, request): + async def on_POST(self, request): """Searches for users in directory Returns: @@ -56,7 +53,7 @@ class UserDirectorySearchRestServlet(RestServlet): ] } """ - requester = yield self.auth.get_user_by_req(request, allow_guest=False) + requester = await self.auth.get_user_by_req(request, allow_guest=False) user_id = requester.user.to_string() if not self.hs.config.user_directory_search_enabled: @@ -72,7 +69,7 @@ class UserDirectorySearchRestServlet(RestServlet): except Exception: raise SynapseError(400, "`search_term` is required field") - results = yield self.user_directory_handler.search_users( + results = await self.user_directory_handler.search_users( user_id, search_term, limit ) From edb8b6af9ad1f5bf26c5116d909f39020a785670 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 5 Dec 2019 16:51:48 +0000 Subject: [PATCH 0610/1623] Newsfile --- changelog.d/6483.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6483.misc diff --git a/changelog.d/6483.misc b/changelog.d/6483.misc new file mode 100644 index 0000000000..cb2cd2bc39 --- /dev/null +++ b/changelog.d/6483.misc @@ -0,0 +1 @@ +Port synapse.rest.client.v2_alpha to async/await. From 8437e2383ed2dffacca5395851023eeacb33d7ba Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 5 Dec 2019 17:58:25 +0000 Subject: [PATCH 0611/1623] Port SyncHandler to async/await --- synapse/handlers/events.py | 30 ++--- synapse/handlers/sync.py | 251 +++++++++++++++++------------------- synapse/notifier.py | 29 ++--- synapse/util/metrics.py | 23 +++- tests/handlers/test_sync.py | 33 +++-- tests/unittest.py | 7 +- 6 files changed, 182 insertions(+), 191 deletions(-) diff --git a/synapse/handlers/events.py b/synapse/handlers/events.py index 45fe13c62f..ec18a42a68 100644 --- a/synapse/handlers/events.py +++ b/synapse/handlers/events.py @@ -16,8 +16,6 @@ import logging import random -from twisted.internet import defer - from synapse.api.constants import EventTypes, Membership from synapse.api.errors import AuthError, SynapseError from synapse.events import EventBase @@ -50,9 +48,8 @@ class EventStreamHandler(BaseHandler): self._server_notices_sender = hs.get_server_notices_sender() self._event_serializer = hs.get_event_client_serializer() - @defer.inlineCallbacks @log_function - def get_stream( + async def get_stream( self, auth_user_id, pagin_config, @@ -69,17 +66,17 @@ class EventStreamHandler(BaseHandler): """ if room_id: - blocked = yield self.store.is_room_blocked(room_id) + blocked = await self.store.is_room_blocked(room_id) if blocked: raise SynapseError(403, "This room has been blocked on this server") # send any outstanding server notices to the user. - yield self._server_notices_sender.on_user_syncing(auth_user_id) + await self._server_notices_sender.on_user_syncing(auth_user_id) auth_user = UserID.from_string(auth_user_id) presence_handler = self.hs.get_presence_handler() - context = yield presence_handler.user_syncing( + context = await presence_handler.user_syncing( auth_user_id, affect_presence=affect_presence ) with context: @@ -91,7 +88,7 @@ class EventStreamHandler(BaseHandler): # thundering herds on restart. timeout = random.randint(int(timeout * 0.9), int(timeout * 1.1)) - events, tokens = yield self.notifier.get_events_for( + events, tokens = await self.notifier.get_events_for( auth_user, pagin_config, timeout, @@ -112,14 +109,14 @@ class EventStreamHandler(BaseHandler): # Send down presence. if event.state_key == auth_user_id: # Send down presence for everyone in the room. - users = yield self.state.get_current_users_in_room( + users = await self.state.get_current_users_in_room( event.room_id ) - states = yield presence_handler.get_states(users, as_event=True) + states = await presence_handler.get_states(users, as_event=True) to_add.extend(states) else: - ev = yield presence_handler.get_state( + ev = await presence_handler.get_state( UserID.from_string(event.state_key), as_event=True ) to_add.append(ev) @@ -128,7 +125,7 @@ class EventStreamHandler(BaseHandler): time_now = self.clock.time_msec() - chunks = yield self._event_serializer.serialize_events( + chunks = await self._event_serializer.serialize_events( events, time_now, as_client_event=as_client_event, @@ -151,8 +148,7 @@ class EventHandler(BaseHandler): super(EventHandler, self).__init__(hs) self.storage = hs.get_storage() - @defer.inlineCallbacks - def get_event(self, user, room_id, event_id): + async def get_event(self, user, room_id, event_id): """Retrieve a single specified event. Args: @@ -167,15 +163,15 @@ class EventHandler(BaseHandler): AuthError if the user does not have the rights to inspect this event. """ - event = yield self.store.get_event(event_id, check_room_id=room_id) + event = await self.store.get_event(event_id, check_room_id=room_id) if not event: return None - users = yield self.store.get_users_in_room(event.room_id) + users = await self.store.get_users_in_room(event.room_id) is_peeking = user.to_string() not in users - filtered = yield filter_events_for_client( + filtered = await filter_events_for_client( self.storage, user.to_string(), [event], is_peeking=is_peeking ) diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index b536d410e5..12751fd8c0 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -22,8 +22,6 @@ from six import iteritems, itervalues from prometheus_client import Counter -from twisted.internet import defer - from synapse.api.constants import EventTypes, Membership from synapse.logging.context import LoggingContext from synapse.push.clientformat import format_push_rules_for_user @@ -241,8 +239,7 @@ class SyncHandler(object): expiry_ms=LAZY_LOADED_MEMBERS_CACHE_MAX_AGE, ) - @defer.inlineCallbacks - def wait_for_sync_for_user( + async def wait_for_sync_for_user( self, sync_config, since_token=None, timeout=0, full_state=False ): """Get the sync for a client if we have new data for it now. Otherwise @@ -255,9 +252,9 @@ class SyncHandler(object): # not been exceeded (if not part of the group by this point, almost certain # auth_blocking will occur) user_id = sync_config.user.to_string() - yield self.auth.check_auth_blocking(user_id) + await self.auth.check_auth_blocking(user_id) - res = yield self.response_cache.wrap( + res = await self.response_cache.wrap( sync_config.request_key, self._wait_for_sync_for_user, sync_config, @@ -267,8 +264,9 @@ class SyncHandler(object): ) return res - @defer.inlineCallbacks - def _wait_for_sync_for_user(self, sync_config, since_token, timeout, full_state): + async def _wait_for_sync_for_user( + self, sync_config, since_token, timeout, full_state + ): if since_token is None: sync_type = "initial_sync" elif full_state: @@ -283,7 +281,7 @@ class SyncHandler(object): if timeout == 0 or since_token is None or full_state: # we are going to return immediately, so don't bother calling # notifier.wait_for_events. - result = yield self.current_sync_for_user( + result = await self.current_sync_for_user( sync_config, since_token, full_state=full_state ) else: @@ -291,7 +289,7 @@ class SyncHandler(object): def current_sync_callback(before_token, after_token): return self.current_sync_for_user(sync_config, since_token) - result = yield self.notifier.wait_for_events( + result = await self.notifier.wait_for_events( sync_config.user.to_string(), timeout, current_sync_callback, @@ -314,15 +312,13 @@ class SyncHandler(object): """ return self.generate_sync_result(sync_config, since_token, full_state) - @defer.inlineCallbacks - def push_rules_for_user(self, user): + async def push_rules_for_user(self, user): user_id = user.to_string() - rules = yield self.store.get_push_rules_for_user(user_id) + rules = await self.store.get_push_rules_for_user(user_id) rules = format_push_rules_for_user(user, rules) return rules - @defer.inlineCallbacks - def ephemeral_by_room(self, sync_result_builder, now_token, since_token=None): + async def ephemeral_by_room(self, sync_result_builder, now_token, since_token=None): """Get the ephemeral events for each room the user is in Args: sync_result_builder(SyncResultBuilder) @@ -343,7 +339,7 @@ class SyncHandler(object): room_ids = sync_result_builder.joined_room_ids typing_source = self.event_sources.sources["typing"] - typing, typing_key = yield typing_source.get_new_events( + typing, typing_key = typing_source.get_new_events( user=sync_config.user, from_key=typing_key, limit=sync_config.filter_collection.ephemeral_limit(), @@ -365,7 +361,7 @@ class SyncHandler(object): receipt_key = since_token.receipt_key if since_token else "0" receipt_source = self.event_sources.sources["receipt"] - receipts, receipt_key = yield receipt_source.get_new_events( + receipts, receipt_key = await receipt_source.get_new_events( user=sync_config.user, from_key=receipt_key, limit=sync_config.filter_collection.ephemeral_limit(), @@ -382,8 +378,7 @@ class SyncHandler(object): return now_token, ephemeral_by_room - @defer.inlineCallbacks - def _load_filtered_recents( + async def _load_filtered_recents( self, room_id, sync_config, @@ -415,10 +410,10 @@ class SyncHandler(object): # ensure that we always include current state in the timeline current_state_ids = frozenset() if any(e.is_state() for e in recents): - current_state_ids = yield self.state.get_current_state_ids(room_id) + current_state_ids = await self.state.get_current_state_ids(room_id) current_state_ids = frozenset(itervalues(current_state_ids)) - recents = yield filter_events_for_client( + recents = await filter_events_for_client( self.storage, sync_config.user.to_string(), recents, @@ -449,14 +444,14 @@ class SyncHandler(object): # Otherwise, we want to return the last N events in the room # in toplogical ordering. if since_key: - events, end_key = yield self.store.get_room_events_stream_for_room( + events, end_key = await self.store.get_room_events_stream_for_room( room_id, limit=load_limit + 1, from_key=since_key, to_key=end_key, ) else: - events, end_key = yield self.store.get_recent_events_for_room( + events, end_key = await self.store.get_recent_events_for_room( room_id, limit=load_limit + 1, end_token=end_key ) loaded_recents = sync_config.filter_collection.filter_room_timeline( @@ -468,10 +463,10 @@ class SyncHandler(object): # ensure that we always include current state in the timeline current_state_ids = frozenset() if any(e.is_state() for e in loaded_recents): - current_state_ids = yield self.state.get_current_state_ids(room_id) + current_state_ids = await self.state.get_current_state_ids(room_id) current_state_ids = frozenset(itervalues(current_state_ids)) - loaded_recents = yield filter_events_for_client( + loaded_recents = await filter_events_for_client( self.storage, sync_config.user.to_string(), loaded_recents, @@ -498,8 +493,7 @@ class SyncHandler(object): limited=limited or newly_joined_room, ) - @defer.inlineCallbacks - def get_state_after_event(self, event, state_filter=StateFilter.all()): + async def get_state_after_event(self, event, state_filter=StateFilter.all()): """ Get the room state after the given event @@ -511,7 +505,7 @@ class SyncHandler(object): Returns: A Deferred map from ((type, state_key)->Event) """ - state_ids = yield self.state_store.get_state_ids_for_event( + state_ids = await self.state_store.get_state_ids_for_event( event.event_id, state_filter=state_filter ) if event.is_state(): @@ -519,8 +513,9 @@ class SyncHandler(object): state_ids[(event.type, event.state_key)] = event.event_id return state_ids - @defer.inlineCallbacks - def get_state_at(self, room_id, stream_position, state_filter=StateFilter.all()): + async def get_state_at( + self, room_id, stream_position, state_filter=StateFilter.all() + ): """ Get the room state at a particular stream position Args: @@ -536,13 +531,13 @@ class SyncHandler(object): # get_recent_events_for_room operates by topo ordering. This therefore # does not reliably give you the state at the given stream position. # (https://github.com/matrix-org/synapse/issues/3305) - last_events, _ = yield self.store.get_recent_events_for_room( + last_events, _ = await self.store.get_recent_events_for_room( room_id, end_token=stream_position.room_key, limit=1 ) if last_events: last_event = last_events[-1] - state = yield self.get_state_after_event( + state = await self.get_state_after_event( last_event, state_filter=state_filter ) @@ -551,8 +546,7 @@ class SyncHandler(object): state = {} return state - @defer.inlineCallbacks - def compute_summary(self, room_id, sync_config, batch, state, now_token): + async def compute_summary(self, room_id, sync_config, batch, state, now_token): """ Works out a room summary block for this room, summarising the number of joined members in the room, and providing the 'hero' members if the room has no name so clients can consistently name rooms. Also adds @@ -574,7 +568,7 @@ class SyncHandler(object): # FIXME: we could/should get this from room_stats when matthew/stats lands # FIXME: this promulgates https://github.com/matrix-org/synapse/issues/3305 - last_events, _ = yield self.store.get_recent_event_ids_for_room( + last_events, _ = await self.store.get_recent_event_ids_for_room( room_id, end_token=now_token.room_key, limit=1 ) @@ -582,7 +576,7 @@ class SyncHandler(object): return None last_event = last_events[-1] - state_ids = yield self.state_store.get_state_ids_for_event( + state_ids = await self.state_store.get_state_ids_for_event( last_event.event_id, state_filter=StateFilter.from_types( [(EventTypes.Name, ""), (EventTypes.CanonicalAlias, "")] @@ -590,7 +584,7 @@ class SyncHandler(object): ) # this is heavily cached, thus: fast. - details = yield self.store.get_room_summary(room_id) + details = await self.store.get_room_summary(room_id) name_id = state_ids.get((EventTypes.Name, "")) canonical_alias_id = state_ids.get((EventTypes.CanonicalAlias, "")) @@ -608,12 +602,12 @@ class SyncHandler(object): # calculating heroes. Empty strings are falsey, so we check # for the "name" value and default to an empty string. if name_id: - name = yield self.store.get_event(name_id, allow_none=True) + name = await self.store.get_event(name_id, allow_none=True) if name and name.content.get("name"): return summary if canonical_alias_id: - canonical_alias = yield self.store.get_event( + canonical_alias = await self.store.get_event( canonical_alias_id, allow_none=True ) if canonical_alias and canonical_alias.content.get("alias"): @@ -678,7 +672,7 @@ class SyncHandler(object): ) ] - missing_hero_state = yield self.store.get_events(missing_hero_event_ids) + missing_hero_state = await self.store.get_events(missing_hero_event_ids) missing_hero_state = missing_hero_state.values() for s in missing_hero_state: @@ -697,8 +691,7 @@ class SyncHandler(object): logger.debug("found LruCache for %r", cache_key) return cache - @defer.inlineCallbacks - def compute_state_delta( + async def compute_state_delta( self, room_id, batch, sync_config, since_token, now_token, full_state ): """ Works out the difference in state between the start of the timeline @@ -759,16 +752,16 @@ class SyncHandler(object): if full_state: if batch: - current_state_ids = yield self.state_store.get_state_ids_for_event( + current_state_ids = await self.state_store.get_state_ids_for_event( batch.events[-1].event_id, state_filter=state_filter ) - state_ids = yield self.state_store.get_state_ids_for_event( + state_ids = await self.state_store.get_state_ids_for_event( batch.events[0].event_id, state_filter=state_filter ) else: - current_state_ids = yield self.get_state_at( + current_state_ids = await self.get_state_at( room_id, stream_position=now_token, state_filter=state_filter ) @@ -783,13 +776,13 @@ class SyncHandler(object): ) elif batch.limited: if batch: - state_at_timeline_start = yield self.state_store.get_state_ids_for_event( + state_at_timeline_start = await self.state_store.get_state_ids_for_event( batch.events[0].event_id, state_filter=state_filter ) else: # We can get here if the user has ignored the senders of all # the recent events. - state_at_timeline_start = yield self.get_state_at( + state_at_timeline_start = await self.get_state_at( room_id, stream_position=now_token, state_filter=state_filter ) @@ -807,19 +800,19 @@ class SyncHandler(object): # about them). state_filter = StateFilter.all() - state_at_previous_sync = yield self.get_state_at( + state_at_previous_sync = await self.get_state_at( room_id, stream_position=since_token, state_filter=state_filter ) if batch: - current_state_ids = yield self.state_store.get_state_ids_for_event( + current_state_ids = await self.state_store.get_state_ids_for_event( batch.events[-1].event_id, state_filter=state_filter ) else: # Its not clear how we get here, but empirically we do # (#5407). Logging has been added elsewhere to try and # figure out where this state comes from. - current_state_ids = yield self.get_state_at( + current_state_ids = await self.get_state_at( room_id, stream_position=now_token, state_filter=state_filter ) @@ -843,7 +836,7 @@ class SyncHandler(object): # So we fish out all the member events corresponding to the # timeline here, and then dedupe any redundant ones below. - state_ids = yield self.state_store.get_state_ids_for_event( + state_ids = await self.state_store.get_state_ids_for_event( batch.events[0].event_id, # we only want members! state_filter=StateFilter.from_types( @@ -883,7 +876,7 @@ class SyncHandler(object): state = {} if state_ids: - state = yield self.store.get_events(list(state_ids.values())) + state = await self.store.get_events(list(state_ids.values())) return { (e.type, e.state_key): e @@ -892,10 +885,9 @@ class SyncHandler(object): ) } - @defer.inlineCallbacks - def unread_notifs_for_room_id(self, room_id, sync_config): + async def unread_notifs_for_room_id(self, room_id, sync_config): with Measure(self.clock, "unread_notifs_for_room_id"): - last_unread_event_id = yield self.store.get_last_receipt_event_id_for_user( + last_unread_event_id = await self.store.get_last_receipt_event_id_for_user( user_id=sync_config.user.to_string(), room_id=room_id, receipt_type="m.read", @@ -903,7 +895,7 @@ class SyncHandler(object): notifs = [] if last_unread_event_id: - notifs = yield self.store.get_unread_event_push_actions_by_room_for_user( + notifs = await self.store.get_unread_event_push_actions_by_room_for_user( room_id, sync_config.user.to_string(), last_unread_event_id ) return notifs @@ -912,8 +904,9 @@ class SyncHandler(object): # count is whatever it was last time. return None - @defer.inlineCallbacks - def generate_sync_result(self, sync_config, since_token=None, full_state=False): + async def generate_sync_result( + self, sync_config, since_token=None, full_state=False + ): """Generates a sync result. Args: @@ -928,7 +921,7 @@ class SyncHandler(object): # this is due to some of the underlying streams not supporting the ability # to query up to a given point. # Always use the `now_token` in `SyncResultBuilder` - now_token = yield self.event_sources.get_current_token() + now_token = await self.event_sources.get_current_token() logger.info( "Calculating sync response for %r between %s and %s", @@ -944,10 +937,9 @@ class SyncHandler(object): # See https://github.com/matrix-org/matrix-doc/issues/1144 raise NotImplementedError() else: - joined_room_ids = yield self.get_rooms_for_user_at( + joined_room_ids = await self.get_rooms_for_user_at( user_id, now_token.room_stream_id ) - sync_result_builder = SyncResultBuilder( sync_config, full_state, @@ -956,11 +948,11 @@ class SyncHandler(object): joined_room_ids=joined_room_ids, ) - account_data_by_room = yield self._generate_sync_entry_for_account_data( + account_data_by_room = await self._generate_sync_entry_for_account_data( sync_result_builder ) - res = yield self._generate_sync_entry_for_rooms( + res = await self._generate_sync_entry_for_rooms( sync_result_builder, account_data_by_room ) newly_joined_rooms, newly_joined_or_invited_users, _, _ = res @@ -970,13 +962,13 @@ class SyncHandler(object): since_token is None and sync_config.filter_collection.blocks_all_presence() ) if self.hs_config.use_presence and not block_all_presence_data: - yield self._generate_sync_entry_for_presence( + await self._generate_sync_entry_for_presence( sync_result_builder, newly_joined_rooms, newly_joined_or_invited_users ) - yield self._generate_sync_entry_for_to_device(sync_result_builder) + await self._generate_sync_entry_for_to_device(sync_result_builder) - device_lists = yield self._generate_sync_entry_for_device_list( + device_lists = await self._generate_sync_entry_for_device_list( sync_result_builder, newly_joined_rooms=newly_joined_rooms, newly_joined_or_invited_users=newly_joined_or_invited_users, @@ -987,11 +979,11 @@ class SyncHandler(object): device_id = sync_config.device_id one_time_key_counts = {} if device_id: - one_time_key_counts = yield self.store.count_e2e_one_time_keys( + one_time_key_counts = await self.store.count_e2e_one_time_keys( user_id, device_id ) - yield self._generate_sync_entry_for_groups(sync_result_builder) + await self._generate_sync_entry_for_groups(sync_result_builder) # debug for https://github.com/matrix-org/synapse/issues/4422 for joined_room in sync_result_builder.joined: @@ -1015,18 +1007,17 @@ class SyncHandler(object): ) @measure_func("_generate_sync_entry_for_groups") - @defer.inlineCallbacks - def _generate_sync_entry_for_groups(self, sync_result_builder): + async def _generate_sync_entry_for_groups(self, sync_result_builder): user_id = sync_result_builder.sync_config.user.to_string() since_token = sync_result_builder.since_token now_token = sync_result_builder.now_token if since_token and since_token.groups_key: - results = yield self.store.get_groups_changes_for_user( + results = self.store.get_groups_changes_for_user( user_id, since_token.groups_key, now_token.groups_key ) else: - results = yield self.store.get_all_groups_for_user( + results = await self.store.get_all_groups_for_user( user_id, now_token.groups_key ) @@ -1059,8 +1050,7 @@ class SyncHandler(object): ) @measure_func("_generate_sync_entry_for_device_list") - @defer.inlineCallbacks - def _generate_sync_entry_for_device_list( + async def _generate_sync_entry_for_device_list( self, sync_result_builder, newly_joined_rooms, @@ -1108,32 +1098,32 @@ class SyncHandler(object): # room with by looking at all users that have left a room plus users # that were in a room we've left. - users_who_share_room = yield self.store.get_users_who_share_room_with_user( + users_who_share_room = await self.store.get_users_who_share_room_with_user( user_id ) # Step 1a, check for changes in devices of users we share a room with - users_that_have_changed = yield self.store.get_users_whose_devices_changed( + users_that_have_changed = await self.store.get_users_whose_devices_changed( since_token.device_list_key, users_who_share_room ) # Step 1b, check for newly joined rooms for room_id in newly_joined_rooms: - joined_users = yield self.state.get_current_users_in_room(room_id) + joined_users = await self.state.get_current_users_in_room(room_id) newly_joined_or_invited_users.update(joined_users) # TODO: Check that these users are actually new, i.e. either they # weren't in the previous sync *or* they left and rejoined. users_that_have_changed.update(newly_joined_or_invited_users) - user_signatures_changed = yield self.store.get_users_whose_signatures_changed( + user_signatures_changed = await self.store.get_users_whose_signatures_changed( user_id, since_token.device_list_key ) users_that_have_changed.update(user_signatures_changed) # Now find users that we no longer track for room_id in newly_left_rooms: - left_users = yield self.state.get_current_users_in_room(room_id) + left_users = await self.state.get_current_users_in_room(room_id) newly_left_users.update(left_users) # Remove any users that we still share a room with. @@ -1143,8 +1133,7 @@ class SyncHandler(object): else: return DeviceLists(changed=[], left=[]) - @defer.inlineCallbacks - def _generate_sync_entry_for_to_device(self, sync_result_builder): + async def _generate_sync_entry_for_to_device(self, sync_result_builder): """Generates the portion of the sync response. Populates `sync_result_builder` with the result. @@ -1165,14 +1154,14 @@ class SyncHandler(object): # We only delete messages when a new message comes in, but that's # fine so long as we delete them at some point. - deleted = yield self.store.delete_messages_for_device( + deleted = await self.store.delete_messages_for_device( user_id, device_id, since_stream_id ) logger.debug( "Deleted %d to-device messages up to %d", deleted, since_stream_id ) - messages, stream_id = yield self.store.get_new_messages_for_device( + messages, stream_id = await self.store.get_new_messages_for_device( user_id, device_id, since_stream_id, now_token.to_device_key ) @@ -1190,8 +1179,7 @@ class SyncHandler(object): else: sync_result_builder.to_device = [] - @defer.inlineCallbacks - def _generate_sync_entry_for_account_data(self, sync_result_builder): + async def _generate_sync_entry_for_account_data(self, sync_result_builder): """Generates the account data portion of the sync response. Populates `sync_result_builder` with the result. @@ -1209,25 +1197,25 @@ class SyncHandler(object): ( account_data, account_data_by_room, - ) = yield self.store.get_updated_account_data_for_user( + ) = self.store.get_updated_account_data_for_user( user_id, since_token.account_data_key ) - push_rules_changed = yield self.store.have_push_rules_changed_for_user( + push_rules_changed = await self.store.have_push_rules_changed_for_user( user_id, int(since_token.push_rules_key) ) if push_rules_changed: - account_data["m.push_rules"] = yield self.push_rules_for_user( + account_data["m.push_rules"] = await self.push_rules_for_user( sync_config.user ) else: ( account_data, account_data_by_room, - ) = yield self.store.get_account_data_for_user(sync_config.user.to_string()) + ) = await self.store.get_account_data_for_user(sync_config.user.to_string()) - account_data["m.push_rules"] = yield self.push_rules_for_user( + account_data["m.push_rules"] = await self.push_rules_for_user( sync_config.user ) @@ -1242,8 +1230,7 @@ class SyncHandler(object): return account_data_by_room - @defer.inlineCallbacks - def _generate_sync_entry_for_presence( + async def _generate_sync_entry_for_presence( self, sync_result_builder, newly_joined_rooms, newly_joined_or_invited_users ): """Generates the presence portion of the sync response. Populates the @@ -1271,7 +1258,7 @@ class SyncHandler(object): presence_key = None include_offline = False - presence, presence_key = yield presence_source.get_new_events( + presence, presence_key = await presence_source.get_new_events( user=user, from_key=presence_key, is_guest=sync_config.is_guest, @@ -1283,12 +1270,12 @@ class SyncHandler(object): extra_users_ids = set(newly_joined_or_invited_users) for room_id in newly_joined_rooms: - users = yield self.state.get_current_users_in_room(room_id) + users = await self.state.get_current_users_in_room(room_id) extra_users_ids.update(users) extra_users_ids.discard(user.to_string()) if extra_users_ids: - states = yield self.presence_handler.get_states(extra_users_ids) + states = await self.presence_handler.get_states(extra_users_ids) presence.extend(states) # Deduplicate the presence entries so that there's at most one per user @@ -1298,8 +1285,9 @@ class SyncHandler(object): sync_result_builder.presence = presence - @defer.inlineCallbacks - def _generate_sync_entry_for_rooms(self, sync_result_builder, account_data_by_room): + async def _generate_sync_entry_for_rooms( + self, sync_result_builder, account_data_by_room + ): """Generates the rooms portion of the sync response. Populates the `sync_result_builder` with the result. @@ -1321,7 +1309,7 @@ class SyncHandler(object): if block_all_room_ephemeral: ephemeral_by_room = {} else: - now_token, ephemeral_by_room = yield self.ephemeral_by_room( + now_token, ephemeral_by_room = await self.ephemeral_by_room( sync_result_builder, now_token=sync_result_builder.now_token, since_token=sync_result_builder.since_token, @@ -1333,16 +1321,16 @@ class SyncHandler(object): 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) + have_changed = await self._have_rooms_changed(sync_result_builder) if not have_changed: - tags_by_room = yield self.store.get_updated_tags( + tags_by_room = await self.store.get_updated_tags( user_id, since_token.account_data_key ) if not tags_by_room: logger.debug("no-oping sync") return [], [], [], [] - ignored_account_data = yield self.store.get_global_account_data_by_type_for_user( + ignored_account_data = await self.store.get_global_account_data_by_type_for_user( "m.ignored_user_list", user_id=user_id ) @@ -1352,18 +1340,18 @@ class SyncHandler(object): ignored_users = frozenset() if since_token: - res = yield self._get_rooms_changed(sync_result_builder, ignored_users) + res = await self._get_rooms_changed(sync_result_builder, ignored_users) room_entries, invited, newly_joined_rooms, newly_left_rooms = res - tags_by_room = yield self.store.get_updated_tags( + tags_by_room = await self.store.get_updated_tags( user_id, since_token.account_data_key ) else: - res = yield self._get_all_rooms(sync_result_builder, ignored_users) + res = await self._get_all_rooms(sync_result_builder, ignored_users) room_entries, invited, newly_joined_rooms = res newly_left_rooms = [] - tags_by_room = yield self.store.get_tags_for_user(user_id) + tags_by_room = await self.store.get_tags_for_user(user_id) def handle_room_entries(room_entry): return self._generate_room_entry( @@ -1376,7 +1364,7 @@ class SyncHandler(object): always_include=sync_result_builder.full_state, ) - yield concurrently_execute(handle_room_entries, room_entries, 10) + await concurrently_execute(handle_room_entries, room_entries, 10) sync_result_builder.invited.extend(invited) @@ -1410,8 +1398,7 @@ class SyncHandler(object): newly_left_users, ) - @defer.inlineCallbacks - def _have_rooms_changed(self, sync_result_builder): + async 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. """ @@ -1422,7 +1409,7 @@ class SyncHandler(object): assert since_token # Get a list of membership change events that have happened. - rooms_changed = yield self.store.get_membership_changes_for_user( + rooms_changed = await self.store.get_membership_changes_for_user( user_id, since_token.room_key, now_token.room_key ) @@ -1435,8 +1422,7 @@ class SyncHandler(object): return True return False - @defer.inlineCallbacks - def _get_rooms_changed(self, sync_result_builder, ignored_users): + async def _get_rooms_changed(self, sync_result_builder, ignored_users): """Gets the the changes that have happened since the last sync. Args: @@ -1461,7 +1447,7 @@ class SyncHandler(object): assert since_token # Get a list of membership change events that have happened. - rooms_changed = yield self.store.get_membership_changes_for_user( + rooms_changed = await self.store.get_membership_changes_for_user( user_id, since_token.room_key, now_token.room_key ) @@ -1499,11 +1485,11 @@ class SyncHandler(object): continue if room_id in sync_result_builder.joined_room_ids or has_join: - old_state_ids = yield self.get_state_at(room_id, since_token) + old_state_ids = await self.get_state_at(room_id, since_token) old_mem_ev_id = old_state_ids.get((EventTypes.Member, user_id), None) old_mem_ev = None if old_mem_ev_id: - old_mem_ev = yield self.store.get_event( + old_mem_ev = await self.store.get_event( old_mem_ev_id, allow_none=True ) @@ -1536,13 +1522,13 @@ class SyncHandler(object): newly_left_rooms.append(room_id) else: if not old_state_ids: - old_state_ids = yield self.get_state_at(room_id, since_token) + old_state_ids = await self.get_state_at(room_id, since_token) old_mem_ev_id = old_state_ids.get( (EventTypes.Member, user_id), None ) old_mem_ev = None if old_mem_ev_id: - old_mem_ev = yield self.store.get_event( + old_mem_ev = await self.store.get_event( old_mem_ev_id, allow_none=True ) if old_mem_ev and old_mem_ev.membership == Membership.JOIN: @@ -1566,7 +1552,7 @@ class SyncHandler(object): if leave_events: leave_event = leave_events[-1] - leave_stream_token = yield self.store.get_stream_token_for_event( + leave_stream_token = await self.store.get_stream_token_for_event( leave_event.event_id ) leave_token = since_token.copy_and_replace( @@ -1603,7 +1589,7 @@ class SyncHandler(object): timeline_limit = sync_config.filter_collection.timeline_limit() # Get all events for rooms we're currently joined to. - room_to_events = yield self.store.get_room_events_stream_for_rooms( + room_to_events = await self.store.get_room_events_stream_for_rooms( room_ids=sync_result_builder.joined_room_ids, from_key=since_token.room_key, to_key=now_token.room_key, @@ -1652,8 +1638,7 @@ class SyncHandler(object): return room_entries, invited, newly_joined_rooms, newly_left_rooms - @defer.inlineCallbacks - def _get_all_rooms(self, sync_result_builder, ignored_users): + async def _get_all_rooms(self, sync_result_builder, ignored_users): """Returns entries for all rooms for the user. Args: @@ -1677,7 +1662,7 @@ class SyncHandler(object): Membership.BAN, ) - room_list = yield self.store.get_rooms_for_user_where_membership_is( + room_list = await self.store.get_rooms_for_user_where_membership_is( user_id=user_id, membership_list=membership_list ) @@ -1700,7 +1685,7 @@ class SyncHandler(object): elif event.membership == Membership.INVITE: if event.sender in ignored_users: continue - invite = yield self.store.get_event(event.event_id) + invite = await self.store.get_event(event.event_id) invited.append(InvitedSyncResult(room_id=event.room_id, invite=invite)) elif event.membership in (Membership.LEAVE, Membership.BAN): # Always send down rooms we were banned or kicked from. @@ -1726,8 +1711,7 @@ class SyncHandler(object): return room_entries, invited, [] - @defer.inlineCallbacks - def _generate_room_entry( + async def _generate_room_entry( self, sync_result_builder, ignored_users, @@ -1769,7 +1753,7 @@ class SyncHandler(object): since_token = room_builder.since_token upto_token = room_builder.upto_token - batch = yield self._load_filtered_recents( + batch = await self._load_filtered_recents( room_id, sync_config, now_token=upto_token, @@ -1796,7 +1780,7 @@ class SyncHandler(object): # tag was added by synapse e.g. for server notice rooms. if full_state: user_id = sync_result_builder.sync_config.user.to_string() - tags = yield self.store.get_tags_for_room(user_id, room_id) + tags = await self.store.get_tags_for_room(user_id, room_id) # If there aren't any tags, don't send the empty tags list down # sync @@ -1821,7 +1805,7 @@ class SyncHandler(object): ): return - state = yield self.compute_state_delta( + state = await self.compute_state_delta( room_id, batch, sync_config, since_token, now_token, full_state=full_state ) @@ -1844,7 +1828,7 @@ class SyncHandler(object): ) or since_token is None ): - summary = yield self.compute_summary( + summary = await self.compute_summary( room_id, sync_config, batch, state, now_token ) @@ -1861,7 +1845,7 @@ class SyncHandler(object): ) if room_sync or always_include: - notifs = yield self.unread_notifs_for_room_id(room_id, sync_config) + notifs = await self.unread_notifs_for_room_id(room_id, sync_config) if notifs is not None: unread_notifications["notification_count"] = notifs["notify_count"] @@ -1887,8 +1871,7 @@ class SyncHandler(object): else: raise Exception("Unrecognized rtype: %r", room_builder.rtype) - @defer.inlineCallbacks - def get_rooms_for_user_at(self, user_id, stream_ordering): + async def get_rooms_for_user_at(self, user_id, stream_ordering): """Get set of joined rooms for a user at the given stream ordering. The stream ordering *must* be recent, otherwise this may throw an @@ -1903,7 +1886,7 @@ class SyncHandler(object): Deferred[frozenset[str]]: Set of room_ids the user is in at given stream_ordering. """ - joined_rooms = yield self.store.get_rooms_for_user_with_stream_ordering(user_id) + joined_rooms = await self.store.get_rooms_for_user_with_stream_ordering(user_id) joined_room_ids = set() @@ -1921,10 +1904,10 @@ class SyncHandler(object): logger.info("User joined room after current token: %s", room_id) - extrems = yield self.store.get_forward_extremeties_for_room( + extrems = await self.store.get_forward_extremeties_for_room( room_id, stream_ordering ) - users_in_room = yield self.state.get_current_users_in_room(room_id, extrems) + users_in_room = await self.state.get_current_users_in_room(room_id, extrems) if user_id in users_in_room: joined_room_ids.add(room_id) diff --git a/synapse/notifier.py b/synapse/notifier.py index af161a81d7..5f5f765bea 100644 --- a/synapse/notifier.py +++ b/synapse/notifier.py @@ -304,8 +304,7 @@ class Notifier(object): without waking up any of the normal user event streams""" self.notify_replication() - @defer.inlineCallbacks - def wait_for_events( + async def wait_for_events( self, user_id, timeout, callback, room_ids=None, from_token=StreamToken.START ): """Wait until the callback returns a non empty response or the @@ -313,9 +312,9 @@ class Notifier(object): """ user_stream = self.user_to_user_stream.get(user_id) if user_stream is None: - current_token = yield self.event_sources.get_current_token() + current_token = await self.event_sources.get_current_token() if room_ids is None: - room_ids = yield self.store.get_rooms_for_user(user_id) + room_ids = await self.store.get_rooms_for_user(user_id) user_stream = _NotifierUserStream( user_id=user_id, rooms=room_ids, @@ -344,11 +343,11 @@ class Notifier(object): self.hs.get_reactor(), ) with PreserveLoggingContext(): - yield listener.deferred + await listener.deferred current_token = user_stream.current_token - result = yield callback(prev_token, current_token) + result = await callback(prev_token, current_token) if result: break @@ -364,12 +363,11 @@ class Notifier(object): # This happened if there was no timeout or if the timeout had # already expired. current_token = user_stream.current_token - result = yield callback(prev_token, current_token) + result = await callback(prev_token, current_token) return result - @defer.inlineCallbacks - def get_events_for( + async def get_events_for( self, user, pagination_config, @@ -391,15 +389,14 @@ class Notifier(object): """ from_token = pagination_config.from_token if not from_token: - from_token = yield self.event_sources.get_current_token() + from_token = await self.event_sources.get_current_token() limit = pagination_config.limit - room_ids, is_joined = yield self._get_room_ids(user, explicit_room_id) + room_ids, is_joined = await self._get_room_ids(user, explicit_room_id) is_peeking = not is_joined - @defer.inlineCallbacks - def check_for_updates(before_token, after_token): + async def check_for_updates(before_token, after_token): if not after_token.is_after(before_token): return EventStreamResult([], (from_token, from_token)) @@ -415,7 +412,7 @@ class Notifier(object): if only_keys and name not in only_keys: continue - new_events, new_key = yield source.get_new_events( + new_events, new_key = await source.get_new_events( user=user, from_key=getattr(from_token, keyname), limit=limit, @@ -425,7 +422,7 @@ class Notifier(object): ) if name == "room": - new_events = yield filter_events_for_client( + new_events = await filter_events_for_client( self.storage, user.to_string(), new_events, @@ -461,7 +458,7 @@ class Notifier(object): user_id_for_stream, ) - result = yield self.wait_for_events( + result = await self.wait_for_events( user_id_for_stream, timeout, check_for_updates, diff --git a/synapse/util/metrics.py b/synapse/util/metrics.py index 3286804322..63ddaaba87 100644 --- a/synapse/util/metrics.py +++ b/synapse/util/metrics.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import inspect import logging from functools import wraps @@ -64,12 +65,22 @@ def measure_func(name=None): def wrapper(func): block_name = func.__name__ if name is None else name - @wraps(func) - @defer.inlineCallbacks - def measured_func(self, *args, **kwargs): - with Measure(self.clock, block_name): - r = yield func(self, *args, **kwargs) - return r + if inspect.iscoroutinefunction(func): + + @wraps(func) + async def measured_func(self, *args, **kwargs): + with Measure(self.clock, block_name): + r = await func(self, *args, **kwargs) + return r + + else: + + @wraps(func) + @defer.inlineCallbacks + def measured_func(self, *args, **kwargs): + with Measure(self.clock, block_name): + r = yield func(self, *args, **kwargs) + return r return measured_func diff --git a/tests/handlers/test_sync.py b/tests/handlers/test_sync.py index 31f54bbd7d..758ee071a5 100644 --- a/tests/handlers/test_sync.py +++ b/tests/handlers/test_sync.py @@ -12,54 +12,53 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from twisted.internet import defer from synapse.api.errors import Codes, ResourceLimitError from synapse.api.filtering import DEFAULT_FILTER_COLLECTION -from synapse.handlers.sync import SyncConfig, SyncHandler +from synapse.handlers.sync import SyncConfig from synapse.types import UserID import tests.unittest import tests.utils -from tests.utils import setup_test_homeserver -class SyncTestCase(tests.unittest.TestCase): +class SyncTestCase(tests.unittest.HomeserverTestCase): """ Tests Sync Handler. """ - @defer.inlineCallbacks - def setUp(self): - self.hs = yield setup_test_homeserver(self.addCleanup) - self.sync_handler = SyncHandler(self.hs) + def prepare(self, reactor, clock, hs): + self.hs = hs + self.sync_handler = self.hs.get_sync_handler() self.store = self.hs.get_datastore() - @defer.inlineCallbacks def test_wait_for_sync_for_user_auth_blocking(self): user_id1 = "@user1:server" user_id2 = "@user2:server" sync_config = self._generate_sync_config(user_id1) + self.reactor.advance(100) # So we get not 0 time self.hs.config.limit_usage_by_mau = True self.hs.config.max_mau_value = 1 # Check that the happy case does not throw errors - yield self.store.upsert_monthly_active_user(user_id1) - yield self.sync_handler.wait_for_sync_for_user(sync_config) + self.get_success(self.store.upsert_monthly_active_user(user_id1)) + self.get_success(self.sync_handler.wait_for_sync_for_user(sync_config)) # Test that global lock works self.hs.config.hs_disabled = True - with self.assertRaises(ResourceLimitError) as e: - yield self.sync_handler.wait_for_sync_for_user(sync_config) - self.assertEquals(e.exception.errcode, Codes.RESOURCE_LIMIT_EXCEEDED) + e = self.get_failure( + self.sync_handler.wait_for_sync_for_user(sync_config), ResourceLimitError + ) + self.assertEquals(e.value.errcode, Codes.RESOURCE_LIMIT_EXCEEDED) self.hs.config.hs_disabled = False sync_config = self._generate_sync_config(user_id2) - with self.assertRaises(ResourceLimitError) as e: - yield self.sync_handler.wait_for_sync_for_user(sync_config) - self.assertEquals(e.exception.errcode, Codes.RESOURCE_LIMIT_EXCEEDED) + e = self.get_failure( + self.sync_handler.wait_for_sync_for_user(sync_config), ResourceLimitError + ) + self.assertEquals(e.value.errcode, Codes.RESOURCE_LIMIT_EXCEEDED) def _generate_sync_config(self, user_id): return SyncConfig( diff --git a/tests/unittest.py b/tests/unittest.py index 295573bc46..a1bdd963e6 100644 --- a/tests/unittest.py +++ b/tests/unittest.py @@ -18,6 +18,7 @@ import gc import hashlib import hmac +import inspect import logging import time @@ -25,7 +26,7 @@ from mock import Mock from canonicaljson import json -from twisted.internet.defer import Deferred, succeed +from twisted.internet.defer import Deferred, ensureDeferred, succeed from twisted.python.threadpool import ThreadPool from twisted.trial import unittest @@ -415,6 +416,8 @@ class HomeserverTestCase(TestCase): self.reactor.pump([by] * 100) def get_success(self, d, by=0.0): + if inspect.isawaitable(d): + d = ensureDeferred(d) if not isinstance(d, Deferred): return d self.pump(by=by) @@ -424,6 +427,8 @@ class HomeserverTestCase(TestCase): """ Run a Deferred and get a Failure from it. The failure must be of the type `exc`. """ + if inspect.isawaitable(d): + d = ensureDeferred(d) if not isinstance(d, Deferred): return d self.pump() From b2ee65ea8c29ce698906351c458f40bb2eadc65e Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 5 Dec 2019 17:59:37 +0000 Subject: [PATCH 0612/1623] Newsfile --- changelog.d/6484.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6484.misc diff --git a/changelog.d/6484.misc b/changelog.d/6484.misc new file mode 100644 index 0000000000..b7cd600012 --- /dev/null +++ b/changelog.d/6484.misc @@ -0,0 +1 @@ +Port SyncHandler to async/await. From 649b6bc0888bb1f8c408d72dd92b0c025535a866 Mon Sep 17 00:00:00 2001 From: Manuel Stahl <37705355+awesome-manuel@users.noreply.github.com> Date: Thu, 5 Dec 2019 19:12:23 +0100 Subject: [PATCH 0613/1623] Replace /admin/v1/users_paginate endpoint with /admin/v2/users (#5925) --- changelog.d/5925.feature | 1 + changelog.d/5925.removal | 1 + docs/admin_api/user_admin_api.rst | 45 +++++++++++ synapse/handlers/admin.py | 21 ++--- synapse/rest/admin/__init__.py | 4 +- synapse/rest/admin/users.py | 85 +++++++------------- synapse/storage/_base.py | 50 +++++++----- synapse/storage/data_stores/main/__init__.py | 63 ++++++++++----- synapse/storage/data_stores/main/stats.py | 2 +- 9 files changed, 162 insertions(+), 110 deletions(-) create mode 100644 changelog.d/5925.feature create mode 100644 changelog.d/5925.removal diff --git a/changelog.d/5925.feature b/changelog.d/5925.feature new file mode 100644 index 0000000000..8025cc8231 --- /dev/null +++ b/changelog.d/5925.feature @@ -0,0 +1 @@ +Add admin/v2/users endpoint with pagination. Contributed by Awesome Technologies Innovationslabor GmbH. diff --git a/changelog.d/5925.removal b/changelog.d/5925.removal new file mode 100644 index 0000000000..cbba2855cb --- /dev/null +++ b/changelog.d/5925.removal @@ -0,0 +1 @@ +Remove admin/v1/users_paginate endpoint. Contributed by Awesome Technologies Innovationslabor GmbH. diff --git a/docs/admin_api/user_admin_api.rst b/docs/admin_api/user_admin_api.rst index d0871f9438..b451dc5014 100644 --- a/docs/admin_api/user_admin_api.rst +++ b/docs/admin_api/user_admin_api.rst @@ -1,3 +1,48 @@ +List Accounts +============= + +This API returns all local user accounts. + +The api is:: + + GET /_synapse/admin/v2/users?from=0&limit=10&guests=false + +including an ``access_token`` of a server admin. +The parameters ``from`` and ``limit`` are required only for pagination. +By default, a ``limit`` of 100 is used. +The parameter ``user_id`` can be used to select only users with user ids that +contain this value. +The parameter ``guests=false`` can be used to exclude guest users, +default is to include guest users. +The parameter ``deactivated=true`` can be used to include deactivated users, +default is to exclude deactivated users. +If the endpoint does not return a ``next_token`` then there are no more users left. +It returns a JSON body like the following: + +.. code:: json + + { + "users": [ + { + "name": "", + "password_hash": "", + "is_guest": 0, + "admin": 0, + "user_type": null, + "deactivated": 0 + }, { + "name": "", + "password_hash": "", + "is_guest": 0, + "admin": 1, + "user_type": null, + "deactivated": 0 + } + ], + "next_token": "100" + } + + Query Account ============= diff --git a/synapse/handlers/admin.py b/synapse/handlers/admin.py index 6407d56f8e..14449b9a1e 100644 --- a/synapse/handlers/admin.py +++ b/synapse/handlers/admin.py @@ -56,7 +56,7 @@ class AdminHandler(BaseHandler): @defer.inlineCallbacks def get_users(self): - """Function to reterive a list of users in users table. + """Function to retrieve a list of users in users table. Args: Returns: @@ -67,19 +67,22 @@ class AdminHandler(BaseHandler): return ret @defer.inlineCallbacks - def get_users_paginate(self, order, start, limit): - """Function to reterive a paginated list of users from - users list. This will return a json object, which contains - list of users and the total number of users in users table. + def get_users_paginate(self, start, limit, name, guests, deactivated): + """Function to retrieve a paginated list of users from + users list. This will return a json list of users. Args: - order (str): column name to order the select by this column start (int): start number to begin the query from - limit (int): number of rows to reterive + limit (int): number of rows to retrieve + name (string): filter for user names + guests (bool): whether to in include guest users + deactivated (bool): whether to include deactivated users Returns: - defer.Deferred: resolves to json object {list[dict[str, Any]], count} + defer.Deferred: resolves to json list[dict[str, Any]] """ - ret = yield self.store.get_users_paginate(order, start, limit) + ret = yield self.store.get_users_paginate( + start, limit, name, guests, deactivated + ) return ret diff --git a/synapse/rest/admin/__init__.py b/synapse/rest/admin/__init__.py index 68a59a3424..c122c449f4 100644 --- a/synapse/rest/admin/__init__.py +++ b/synapse/rest/admin/__init__.py @@ -34,12 +34,12 @@ from synapse.rest.admin.server_notice_servlet import SendServerNoticeServlet from synapse.rest.admin.users import ( AccountValidityRenewServlet, DeactivateAccountRestServlet, - GetUsersPaginatedRestServlet, ResetPasswordRestServlet, SearchUsersRestServlet, UserAdminServlet, UserRegisterServlet, UsersRestServlet, + UsersRestServletV2, WhoisRestServlet, ) from synapse.util.versionstring import get_version_string @@ -191,6 +191,7 @@ def register_servlets(hs, http_server): SendServerNoticeServlet(hs).register(http_server) VersionServlet(hs).register(http_server) UserAdminServlet(hs).register(http_server) + UsersRestServletV2(hs).register(http_server) def register_servlets_for_client_rest_resource(hs, http_server): @@ -201,7 +202,6 @@ def register_servlets_for_client_rest_resource(hs, http_server): PurgeHistoryRestServlet(hs).register(http_server) UsersRestServlet(hs).register(http_server) ResetPasswordRestServlet(hs).register(http_server) - GetUsersPaginatedRestServlet(hs).register(http_server) SearchUsersRestServlet(hs).register(http_server) ShutdownRoomRestServlet(hs).register(http_server) UserRegisterServlet(hs).register(http_server) diff --git a/synapse/rest/admin/users.py b/synapse/rest/admin/users.py index 58a83f93af..1937879dbe 100644 --- a/synapse/rest/admin/users.py +++ b/synapse/rest/admin/users.py @@ -25,6 +25,7 @@ from synapse.api.errors import Codes, SynapseError from synapse.http.servlet import ( RestServlet, assert_params_in_dict, + parse_boolean, parse_integer, parse_json_object_from_request, parse_string, @@ -59,71 +60,45 @@ class UsersRestServlet(RestServlet): return 200, ret -class GetUsersPaginatedRestServlet(RestServlet): - """Get request to get specific number of users from Synapse. - This needs user to have administrator access in Synapse. - Example: - http://localhost:8008/_synapse/admin/v1/users_paginate/ - @admin:user?access_token=admin_access_token&start=0&limit=10 - Returns: - 200 OK with json object {list[dict[str, Any]], count} or empty object. - """ +class UsersRestServletV2(RestServlet): + PATTERNS = (re.compile("^/_synapse/admin/v2/users$"),) - PATTERNS = historical_admin_path_patterns( - "/users_paginate/(?P[^/]*)" - ) + """Get request to list all local users. + This needs user to have administrator access in Synapse. + + GET /_synapse/admin/v2/users?from=0&limit=10&guests=false + + returns: + 200 OK with list of users if success otherwise an error. + + The parameters `from` and `limit` are required only for pagination. + By default, a `limit` of 100 is used. + The parameter `user_id` can be used to filter by user id. + The parameter `guests` can be used to exclude guest users. + The parameter `deactivated` can be used to include deactivated users. + """ def __init__(self, hs): - self.store = hs.get_datastore() self.hs = hs self.auth = hs.get_auth() - self.handlers = hs.get_handlers() + self.admin_handler = hs.get_handlers().admin_handler - async def on_GET(self, request, target_user_id): - """Get request to get specific number of users from Synapse. - This needs user to have administrator access in Synapse. - """ + async def on_GET(self, request): await assert_requester_is_admin(self.auth, request) - target_user = UserID.from_string(target_user_id) + start = parse_integer(request, "from", default=0) + limit = parse_integer(request, "limit", default=100) + user_id = parse_string(request, "user_id", default=None) + guests = parse_boolean(request, "guests", default=True) + deactivated = parse_boolean(request, "deactivated", default=False) - if not self.hs.is_mine(target_user): - raise SynapseError(400, "Can only users a local user") + users = await self.admin_handler.get_users_paginate( + start, limit, user_id, guests, deactivated + ) + ret = {"users": users} + if len(users) >= limit: + ret["next_token"] = str(start + len(users)) - order = "name" # order by name in user table - start = parse_integer(request, "start", required=True) - limit = parse_integer(request, "limit", required=True) - - logger.info("limit: %s, start: %s", limit, start) - - ret = await self.handlers.admin_handler.get_users_paginate(order, start, limit) - return 200, ret - - async def on_POST(self, request, target_user_id): - """Post request to get specific number of users from Synapse.. - This needs user to have administrator access in Synapse. - Example: - http://localhost:8008/_synapse/admin/v1/users_paginate/ - @admin:user?access_token=admin_access_token - JsonBodyToSend: - { - "start": "0", - "limit": "10 - } - Returns: - 200 OK with json object {list[dict[str, Any]], count} or empty object. - """ - await assert_requester_is_admin(self.auth, request) - UserID.from_string(target_user_id) - - order = "name" # order by name in user table - params = parse_json_object_from_request(request) - assert_params_in_dict(params, ["limit", "start"]) - limit = params["limit"] - start = params["start"] - logger.info("limit: %s, start: %s", limit, start) - - ret = await self.handlers.admin_handler.get_users_paginate(order, start, limit) return 200, ret diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py index 9205e550bb..0d7c7dff27 100644 --- a/synapse/storage/_base.py +++ b/synapse/storage/_base.py @@ -1350,11 +1350,12 @@ class SQLBaseStore(object): def simple_select_list_paginate( self, table, - keyvalues, orderby, start, limit, retcols, + filters=None, + keyvalues=None, order_direction="ASC", desc="simple_select_list_paginate", ): @@ -1365,6 +1366,9 @@ class SQLBaseStore(object): Args: table (str): the table name + filters (dict[str, T] | None): + column names and values to filter the rows with, or None to not + apply a WHERE ? LIKE ? clause. keyvalues (dict[str, T] | None): column names and values to select the rows with, or None to not apply a WHERE clause. @@ -1380,11 +1384,12 @@ class SQLBaseStore(object): desc, self.simple_select_list_paginate_txn, table, - keyvalues, orderby, start, limit, retcols, + filters=filters, + keyvalues=keyvalues, order_direction=order_direction, ) @@ -1393,11 +1398,12 @@ class SQLBaseStore(object): cls, txn, table, - keyvalues, orderby, start, limit, retcols, + filters=None, + keyvalues=None, order_direction="ASC", ): """ @@ -1405,16 +1411,23 @@ class SQLBaseStore(object): of row numbers, which may return zero or number of rows from start to limit, returning the result as a list of dicts. + Use `filters` to search attributes using SQL wildcards and/or `keyvalues` to + select attributes with exact matches. All constraints are joined together + using 'AND'. + Args: txn : Transaction object table (str): the table name - keyvalues (dict[str, T] | None): - column names and values to select the rows with, or None to not - apply a WHERE clause. orderby (str): Column to order the results by. start (int): Index to begin the query at. limit (int): Number of results to return. retcols (iterable[str]): the names of the columns to return + filters (dict[str, T] | None): + column names and values to filter the rows with, or None to not + apply a WHERE ? LIKE ? clause. + keyvalues (dict[str, T] | None): + column names and values to select the rows with, or None to not + apply a WHERE clause. order_direction (str): Whether the results should be ordered "ASC" or "DESC". Returns: defer.Deferred: resolves to list[dict[str, Any]] @@ -1422,10 +1435,15 @@ class SQLBaseStore(object): if order_direction not in ["ASC", "DESC"]: raise ValueError("order_direction must be one of 'ASC' or 'DESC'.") + where_clause = "WHERE " if filters or keyvalues else "" + arg_list = [] + if filters: + where_clause += " AND ".join("%s LIKE ?" % (k,) for k in filters) + arg_list += list(filters.values()) + where_clause += " AND " if filters and keyvalues else "" if keyvalues: - where_clause = "WHERE " + " AND ".join("%s = ?" % (k,) for k in keyvalues) - else: - where_clause = "" + where_clause += " AND ".join("%s = ?" % (k,) for k in keyvalues) + arg_list += list(keyvalues.values()) sql = "SELECT %s FROM %s %s ORDER BY %s %s LIMIT ? OFFSET ?" % ( ", ".join(retcols), @@ -1434,22 +1452,10 @@ class SQLBaseStore(object): orderby, order_direction, ) - txn.execute(sql, list(keyvalues.values()) + [limit, start]) + txn.execute(sql, arg_list + [limit, start]) return cls.cursor_to_dict(txn) - def get_user_count_txn(self, txn): - """Get a total number of registered users in the users list. - - Args: - txn : Transaction object - Returns: - int : number of users - """ - sql_count = "SELECT COUNT(*) FROM users WHERE is_guest = 0;" - txn.execute(sql_count) - return txn.fetchone()[0] - def simple_search_list(self, table, term, col, retcols, desc="simple_search_list"): """Executes a SELECT query on the named table, which may return zero or more rows, returning the result as a list of dicts. diff --git a/synapse/storage/data_stores/main/__init__.py b/synapse/storage/data_stores/main/__init__.py index 2a5b33dda1..3720ff3088 100644 --- a/synapse/storage/data_stores/main/__init__.py +++ b/synapse/storage/data_stores/main/__init__.py @@ -19,8 +19,6 @@ import calendar import logging import time -from twisted.internet import defer - from synapse.api.constants import PresenceState from synapse.storage.engines import PostgresEngine from synapse.storage.util.id_generators import ( @@ -476,7 +474,7 @@ class DataStore( ) def get_users(self): - """Function to reterive a list of users in users table. + """Function to retrieve a list of users in users table. Args: Returns: @@ -485,36 +483,59 @@ class DataStore( return self.simple_select_list( table="users", keyvalues={}, - retcols=["name", "password_hash", "is_guest", "admin", "user_type"], + retcols=[ + "name", + "password_hash", + "is_guest", + "admin", + "user_type", + "deactivated", + ], desc="get_users", ) - @defer.inlineCallbacks - def get_users_paginate(self, order, start, limit): - """Function to reterive a paginated list of users from - users list. This will return a json object, which contains - list of users and the total number of users in users table. + def get_users_paginate( + self, start, limit, name=None, guests=True, deactivated=False + ): + """Function to retrieve a paginated list of users from + users list. This will return a json list of users. Args: - order (str): column name to order the select by this column start (int): start number to begin the query from - limit (int): number of rows to reterive + limit (int): number of rows to retrieve + name (string): filter for user names + guests (bool): whether to in include guest users + deactivated (bool): whether to include deactivated users Returns: - defer.Deferred: resolves to json object {list[dict[str, Any]], count} + defer.Deferred: resolves to list[dict[str, Any]] """ - users = yield self.runInteraction( - "get_users_paginate", - self.simple_select_list_paginate_txn, + name_filter = {} + if name: + name_filter["name"] = "%" + name + "%" + + attr_filter = {} + if not guests: + attr_filter["is_guest"] = False + if not deactivated: + attr_filter["deactivated"] = False + + return self.simple_select_list_paginate( + desc="get_users_paginate", table="users", - keyvalues={"is_guest": False}, - orderby=order, + orderby="name", start=start, limit=limit, - retcols=["name", "password_hash", "is_guest", "admin", "user_type"], + filters=name_filter, + keyvalues=attr_filter, + retcols=[ + "name", + "password_hash", + "is_guest", + "admin", + "user_type", + "deactivated", + ], ) - count = yield self.runInteraction("get_users_paginate", self.get_user_count_txn) - retval = {"users": users, "total": count} - return retval def search_users(self, term): """Function to search users list for one or more users with diff --git a/synapse/storage/data_stores/main/stats.py b/synapse/storage/data_stores/main/stats.py index 3aeba859fd..b306478824 100644 --- a/synapse/storage/data_stores/main/stats.py +++ b/synapse/storage/data_stores/main/stats.py @@ -260,11 +260,11 @@ class StatsStore(StateDeltasStore): slice_list = self.simple_select_list_paginate_txn( txn, table + "_historical", - {id_col: stats_id}, "end_ts", start, size, retcols=selected_columns + ["bucket_size", "end_ts"], + keyvalues={id_col: stats_id}, order_direction="DESC", ) From b3a4e35ca84a29fe4ccdfb1125ed098c68405d6c Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 6 Dec 2019 10:14:59 +0000 Subject: [PATCH 0614/1623] Fixup functions to consistently return deferreds --- synapse/handlers/sync.py | 6 ++--- synapse/handlers/typing.py | 2 +- .../storage/data_stores/main/account_data.py | 2 +- .../storage/data_stores/main/group_server.py | 4 ++-- tests/handlers/test_typing.py | 24 ++++++++++++++----- tests/rest/client/v1/test_typing.py | 4 +++- 6 files changed, 28 insertions(+), 14 deletions(-) diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index 12751fd8c0..2d3b8ba73c 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -339,7 +339,7 @@ class SyncHandler(object): room_ids = sync_result_builder.joined_room_ids typing_source = self.event_sources.sources["typing"] - typing, typing_key = typing_source.get_new_events( + typing, typing_key = await typing_source.get_new_events( user=sync_config.user, from_key=typing_key, limit=sync_config.filter_collection.ephemeral_limit(), @@ -1013,7 +1013,7 @@ class SyncHandler(object): now_token = sync_result_builder.now_token if since_token and since_token.groups_key: - results = self.store.get_groups_changes_for_user( + results = await self.store.get_groups_changes_for_user( user_id, since_token.groups_key, now_token.groups_key ) else: @@ -1197,7 +1197,7 @@ class SyncHandler(object): ( account_data, account_data_by_room, - ) = self.store.get_updated_account_data_for_user( + ) = await self.store.get_updated_account_data_for_user( user_id, since_token.account_data_key ) diff --git a/synapse/handlers/typing.py b/synapse/handlers/typing.py index 856337b7e2..6f78454322 100644 --- a/synapse/handlers/typing.py +++ b/synapse/handlers/typing.py @@ -313,7 +313,7 @@ class TypingNotificationEventSource(object): events.append(self._make_event_for(room_id)) - return events, handler._latest_room_serial + return defer.succeed((events, handler._latest_room_serial)) def get_current_key(self): return self.get_typing_handler()._latest_room_serial diff --git a/synapse/storage/data_stores/main/account_data.py b/synapse/storage/data_stores/main/account_data.py index b0d22faf3f..ed97b3ffe5 100644 --- a/synapse/storage/data_stores/main/account_data.py +++ b/synapse/storage/data_stores/main/account_data.py @@ -250,7 +250,7 @@ class AccountDataWorkerStore(SQLBaseStore): user_id, int(stream_id) ) if not changed: - return {}, {} + return defer.succeed(({}, {})) return self.runInteraction( "get_updated_account_data_for_user", get_updated_account_data_for_user_txn diff --git a/synapse/storage/data_stores/main/group_server.py b/synapse/storage/data_stores/main/group_server.py index 9e1d12bcb7..d29155a3b5 100644 --- a/synapse/storage/data_stores/main/group_server.py +++ b/synapse/storage/data_stores/main/group_server.py @@ -1109,7 +1109,7 @@ class GroupServerStore(SQLBaseStore): user_id, from_token ) if not has_changed: - return [] + return defer.succeed([]) def _get_groups_changes_for_user_txn(txn): sql = """ @@ -1139,7 +1139,7 @@ class GroupServerStore(SQLBaseStore): from_token ) if not has_changed: - return [] + return defer.succeed([]) def _get_all_groups_changes_txn(txn): sql = """ diff --git a/tests/handlers/test_typing.py b/tests/handlers/test_typing.py index f6d8660285..92b8726093 100644 --- a/tests/handlers/test_typing.py +++ b/tests/handlers/test_typing.py @@ -163,7 +163,9 @@ class TypingNotificationsTestCase(unittest.HomeserverTestCase): self.on_new_event.assert_has_calls([call("typing_key", 1, rooms=[ROOM_ID])]) self.assertEquals(self.event_source.get_current_key(), 1) - events = self.event_source.get_new_events(room_ids=[ROOM_ID], from_key=0) + events = self.get_success( + self.event_source.get_new_events(room_ids=[ROOM_ID], from_key=0) + ) self.assertEquals( events[0], [ @@ -227,7 +229,9 @@ class TypingNotificationsTestCase(unittest.HomeserverTestCase): self.on_new_event.assert_has_calls([call("typing_key", 1, rooms=[ROOM_ID])]) self.assertEquals(self.event_source.get_current_key(), 1) - events = self.event_source.get_new_events(room_ids=[ROOM_ID], from_key=0) + events = self.get_success( + self.event_source.get_new_events(room_ids=[ROOM_ID], from_key=0) + ) self.assertEquals( events[0], [ @@ -279,7 +283,9 @@ class TypingNotificationsTestCase(unittest.HomeserverTestCase): ) self.assertEquals(self.event_source.get_current_key(), 1) - events = self.event_source.get_new_events(room_ids=[ROOM_ID], from_key=0) + events = self.get_success( + self.event_source.get_new_events(room_ids=[ROOM_ID], from_key=0) + ) self.assertEquals( events[0], [{"type": "m.typing", "room_id": ROOM_ID, "content": {"user_ids": []}}], @@ -300,7 +306,9 @@ class TypingNotificationsTestCase(unittest.HomeserverTestCase): self.on_new_event.reset_mock() self.assertEquals(self.event_source.get_current_key(), 1) - events = self.event_source.get_new_events(room_ids=[ROOM_ID], from_key=0) + events = self.get_success( + self.event_source.get_new_events(room_ids=[ROOM_ID], from_key=0) + ) self.assertEquals( events[0], [ @@ -317,7 +325,9 @@ class TypingNotificationsTestCase(unittest.HomeserverTestCase): self.on_new_event.assert_has_calls([call("typing_key", 2, rooms=[ROOM_ID])]) self.assertEquals(self.event_source.get_current_key(), 2) - events = self.event_source.get_new_events(room_ids=[ROOM_ID], from_key=1) + events = self.get_success( + self.event_source.get_new_events(room_ids=[ROOM_ID], from_key=1) + ) self.assertEquals( events[0], [{"type": "m.typing", "room_id": ROOM_ID, "content": {"user_ids": []}}], @@ -335,7 +345,9 @@ class TypingNotificationsTestCase(unittest.HomeserverTestCase): self.on_new_event.reset_mock() self.assertEquals(self.event_source.get_current_key(), 3) - events = self.event_source.get_new_events(room_ids=[ROOM_ID], from_key=0) + events = self.get_success( + self.event_source.get_new_events(room_ids=[ROOM_ID], from_key=0) + ) self.assertEquals( events[0], [ diff --git a/tests/rest/client/v1/test_typing.py b/tests/rest/client/v1/test_typing.py index 30fb77bac8..4bc3aaf02d 100644 --- a/tests/rest/client/v1/test_typing.py +++ b/tests/rest/client/v1/test_typing.py @@ -109,7 +109,9 @@ class RoomTypingTestCase(unittest.HomeserverTestCase): self.assertEquals(200, channel.code) self.assertEquals(self.event_source.get_current_key(), 1) - events = self.event_source.get_new_events(from_key=0, room_ids=[self.room_id]) + events = self.get_success( + self.event_source.get_new_events(from_key=0, room_ids=[self.room_id]) + ) self.assertEquals( events[0], [ From e216ec381af713b9bc9d629bad219f4eb6a1a884 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 6 Dec 2019 11:15:25 +0000 Subject: [PATCH 0615/1623] Remove unused var --- .buildkite/postgres-config.yaml | 2 +- .buildkite/sqlite-config.yaml | 2 +- synapse/storage/database.py | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.buildkite/postgres-config.yaml b/.buildkite/postgres-config.yaml index a35fec394d..dcf72cfda0 100644 --- a/.buildkite/postgres-config.yaml +++ b/.buildkite/postgres-config.yaml @@ -1,7 +1,7 @@ # Configuration file used for testing the 'synapse_port_db' script. # Tells the script to connect to the postgresql database that will be available in the # CI's Docker setup at the point where this file is considered. -server_name: "test" +server_name: "localhost:8080" signing_key_path: "/src/.buildkite/test.signing.key" diff --git a/.buildkite/sqlite-config.yaml b/.buildkite/sqlite-config.yaml index 635b921764..5276aaff03 100644 --- a/.buildkite/sqlite-config.yaml +++ b/.buildkite/sqlite-config.yaml @@ -1,7 +1,7 @@ # Configuration file used for testing the 'synapse_port_db' script. # Tells the 'update_database' script to connect to the test SQLite database to upgrade its # schema and run background updates on it. -server_name: "test" +server_name: "localhost:8080" signing_key_path: "/src/.buildkite/test.signing.key" diff --git a/synapse/storage/database.py b/synapse/storage/database.py index be36c1b829..bd515d70d2 100644 --- a/synapse/storage/database.py +++ b/synapse/storage/database.py @@ -256,8 +256,6 @@ class Database(object): self._check_safe_to_upsert, ) - self.rand = random.SystemRandom() - @defer.inlineCallbacks def _check_safe_to_upsert(self): """ From 9a4fb457cf5918c85068ea249cd2d58b3e2e3cfc Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 6 Dec 2019 13:08:40 +0000 Subject: [PATCH 0616/1623] Change DataStores to accept 'database' param. --- synapse/app/federation_sender.py | 5 +++-- synapse/app/user_dir.py | 5 +++-- synapse/replication/slave/storage/_base.py | 5 +++-- synapse/replication/slave/storage/account_data.py | 5 +++-- synapse/replication/slave/storage/client_ips.py | 5 +++-- synapse/replication/slave/storage/deviceinbox.py | 5 +++-- synapse/replication/slave/storage/devices.py | 5 +++-- synapse/replication/slave/storage/events.py | 5 +++-- synapse/replication/slave/storage/filtering.py | 5 +++-- synapse/replication/slave/storage/groups.py | 5 +++-- synapse/replication/slave/storage/presence.py | 5 +++-- synapse/replication/slave/storage/push_rule.py | 5 +++-- synapse/replication/slave/storage/pushers.py | 5 +++-- synapse/replication/slave/storage/receipts.py | 5 +++-- synapse/replication/slave/storage/room.py | 5 +++-- synapse/storage/_base.py | 2 +- synapse/storage/data_stores/main/__init__.py | 5 +++-- synapse/storage/data_stores/main/account_data.py | 9 +++++---- synapse/storage/data_stores/main/appservice.py | 5 +++-- synapse/storage/data_stores/main/client_ips.py | 9 +++++---- synapse/storage/data_stores/main/deviceinbox.py | 9 +++++---- synapse/storage/data_stores/main/devices.py | 9 +++++---- .../storage/data_stores/main/event_federation.py | 5 +++-- .../storage/data_stores/main/event_push_actions.py | 9 +++++---- synapse/storage/data_stores/main/events.py | 5 +++-- .../storage/data_stores/main/events_bg_updates.py | 5 +++-- synapse/storage/data_stores/main/events_worker.py | 5 +++-- .../storage/data_stores/main/media_repository.py | 11 +++++++---- .../data_stores/main/monthly_active_users.py | 7 ++++--- synapse/storage/data_stores/main/push_rule.py | 5 +++-- synapse/storage/data_stores/main/receipts.py | 9 +++++---- synapse/storage/data_stores/main/registration.py | 13 +++++++------ synapse/storage/data_stores/main/room.py | 9 +++++---- synapse/storage/data_stores/main/roommember.py | 13 +++++++------ synapse/storage/data_stores/main/search.py | 9 +++++---- synapse/storage/data_stores/main/state.py | 13 +++++++------ synapse/storage/data_stores/main/stats.py | 5 +++-- synapse/storage/data_stores/main/stream.py | 5 +++-- synapse/storage/data_stores/main/transactions.py | 5 +++-- synapse/storage/data_stores/main/user_directory.py | 9 +++++---- tests/storage/test_appservice.py | 5 +++-- 41 files changed, 156 insertions(+), 114 deletions(-) diff --git a/synapse/app/federation_sender.py b/synapse/app/federation_sender.py index 448e45e00f..f24920a7d6 100644 --- a/synapse/app/federation_sender.py +++ b/synapse/app/federation_sender.py @@ -40,6 +40,7 @@ from synapse.replication.slave.storage.transactions import SlavedTransactionStor from synapse.replication.tcp.client import ReplicationClientHandler from synapse.replication.tcp.streams._base import ReceiptsStream from synapse.server import HomeServer +from synapse.storage.database import Database from synapse.storage.engines import create_engine from synapse.types import ReadReceipt from synapse.util.async_helpers import Linearizer @@ -59,8 +60,8 @@ class FederationSenderSlaveStore( SlavedDeviceStore, SlavedPresenceStore, ): - def __init__(self, db_conn, hs): - super(FederationSenderSlaveStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(FederationSenderSlaveStore, self).__init__(database, db_conn, hs) # We pull out the current federation stream position now so that we # always have a known value for the federation position in memory so diff --git a/synapse/app/user_dir.py b/synapse/app/user_dir.py index b6d4481725..c01fb34a9b 100644 --- a/synapse/app/user_dir.py +++ b/synapse/app/user_dir.py @@ -43,6 +43,7 @@ from synapse.replication.tcp.streams.events import ( from synapse.rest.client.v2_alpha import user_directory from synapse.server import HomeServer from synapse.storage.data_stores.main.user_directory import UserDirectoryStore +from synapse.storage.database import Database from synapse.storage.engines import create_engine from synapse.util.caches.stream_change_cache import StreamChangeCache from synapse.util.httpresourcetree import create_resource_tree @@ -60,8 +61,8 @@ class UserDirectorySlaveStore( UserDirectoryStore, BaseSlavedStore, ): - def __init__(self, db_conn, hs): - super(UserDirectorySlaveStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(UserDirectorySlaveStore, self).__init__(database, db_conn, hs) events_max = self._stream_id_gen.get_current_token() curr_state_delta_prefill, min_curr_state_delta_id = self.db.get_cache_dict( diff --git a/synapse/replication/slave/storage/_base.py b/synapse/replication/slave/storage/_base.py index 6ece1d6745..b91a528245 100644 --- a/synapse/replication/slave/storage/_base.py +++ b/synapse/replication/slave/storage/_base.py @@ -20,6 +20,7 @@ import six from synapse.storage._base import SQLBaseStore from synapse.storage.data_stores.main.cache import CURRENT_STATE_CACHE_NAME +from synapse.storage.database import Database from synapse.storage.engines import PostgresEngine from ._slaved_id_tracker import SlavedIdTracker @@ -35,8 +36,8 @@ def __func__(inp): class BaseSlavedStore(SQLBaseStore): - def __init__(self, db_conn, hs): - super(BaseSlavedStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(BaseSlavedStore, self).__init__(database, db_conn, hs) if isinstance(self.database_engine, PostgresEngine): self._cache_id_gen = SlavedIdTracker( db_conn, "cache_invalidation_stream", "stream_id" diff --git a/synapse/replication/slave/storage/account_data.py b/synapse/replication/slave/storage/account_data.py index bc2f6a12ae..ebe94909cb 100644 --- a/synapse/replication/slave/storage/account_data.py +++ b/synapse/replication/slave/storage/account_data.py @@ -18,15 +18,16 @@ from synapse.replication.slave.storage._base import BaseSlavedStore from synapse.replication.slave.storage._slaved_id_tracker import SlavedIdTracker from synapse.storage.data_stores.main.account_data import AccountDataWorkerStore from synapse.storage.data_stores.main.tags import TagsWorkerStore +from synapse.storage.database import Database class SlavedAccountDataStore(TagsWorkerStore, AccountDataWorkerStore, BaseSlavedStore): - def __init__(self, db_conn, hs): + def __init__(self, database: Database, db_conn, hs): self._account_data_id_gen = SlavedIdTracker( db_conn, "account_data_max_stream_id", "stream_id" ) - super(SlavedAccountDataStore, self).__init__(db_conn, hs) + super(SlavedAccountDataStore, self).__init__(database, db_conn, hs) def get_max_account_data_stream_id(self): return self._account_data_id_gen.get_current_token() diff --git a/synapse/replication/slave/storage/client_ips.py b/synapse/replication/slave/storage/client_ips.py index b4f58cea19..fbf996e33a 100644 --- a/synapse/replication/slave/storage/client_ips.py +++ b/synapse/replication/slave/storage/client_ips.py @@ -14,6 +14,7 @@ # limitations under the License. from synapse.storage.data_stores.main.client_ips import LAST_SEEN_GRANULARITY +from synapse.storage.database import Database from synapse.util.caches import CACHE_SIZE_FACTOR from synapse.util.caches.descriptors import Cache @@ -21,8 +22,8 @@ from ._base import BaseSlavedStore class SlavedClientIpStore(BaseSlavedStore): - def __init__(self, db_conn, hs): - super(SlavedClientIpStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(SlavedClientIpStore, self).__init__(database, db_conn, hs) self.client_ip_last_seen = Cache( name="client_ip_last_seen", keylen=4, max_entries=50000 * CACHE_SIZE_FACTOR diff --git a/synapse/replication/slave/storage/deviceinbox.py b/synapse/replication/slave/storage/deviceinbox.py index 9fb6c5c6ff..0c237c6e0f 100644 --- a/synapse/replication/slave/storage/deviceinbox.py +++ b/synapse/replication/slave/storage/deviceinbox.py @@ -16,13 +16,14 @@ from synapse.replication.slave.storage._base import BaseSlavedStore from synapse.replication.slave.storage._slaved_id_tracker import SlavedIdTracker from synapse.storage.data_stores.main.deviceinbox import DeviceInboxWorkerStore +from synapse.storage.database import Database from synapse.util.caches.expiringcache import ExpiringCache from synapse.util.caches.stream_change_cache import StreamChangeCache class SlavedDeviceInboxStore(DeviceInboxWorkerStore, BaseSlavedStore): - def __init__(self, db_conn, hs): - super(SlavedDeviceInboxStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(SlavedDeviceInboxStore, self).__init__(database, db_conn, hs) self._device_inbox_id_gen = SlavedIdTracker( db_conn, "device_max_stream_id", "stream_id" ) diff --git a/synapse/replication/slave/storage/devices.py b/synapse/replication/slave/storage/devices.py index de50748c30..dc625e0d7a 100644 --- a/synapse/replication/slave/storage/devices.py +++ b/synapse/replication/slave/storage/devices.py @@ -18,12 +18,13 @@ from synapse.replication.slave.storage._slaved_id_tracker import SlavedIdTracker from synapse.replication.tcp.streams._base import DeviceListsStream, UserSignatureStream from synapse.storage.data_stores.main.devices import DeviceWorkerStore from synapse.storage.data_stores.main.end_to_end_keys import EndToEndKeyWorkerStore +from synapse.storage.database import Database from synapse.util.caches.stream_change_cache import StreamChangeCache class SlavedDeviceStore(EndToEndKeyWorkerStore, DeviceWorkerStore, BaseSlavedStore): - def __init__(self, db_conn, hs): - super(SlavedDeviceStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(SlavedDeviceStore, self).__init__(database, db_conn, hs) self.hs = hs diff --git a/synapse/replication/slave/storage/events.py b/synapse/replication/slave/storage/events.py index d0a0eaf75b..29f35b9915 100644 --- a/synapse/replication/slave/storage/events.py +++ b/synapse/replication/slave/storage/events.py @@ -31,6 +31,7 @@ from synapse.storage.data_stores.main.signatures import SignatureWorkerStore from synapse.storage.data_stores.main.state import StateGroupWorkerStore from synapse.storage.data_stores.main.stream import StreamWorkerStore from synapse.storage.data_stores.main.user_erasure_store import UserErasureWorkerStore +from synapse.storage.database import Database from ._base import BaseSlavedStore from ._slaved_id_tracker import SlavedIdTracker @@ -59,13 +60,13 @@ class SlavedEventStore( RelationsWorkerStore, BaseSlavedStore, ): - def __init__(self, db_conn, hs): + def __init__(self, database: Database, db_conn, hs): self._stream_id_gen = SlavedIdTracker(db_conn, "events", "stream_ordering") self._backfill_id_gen = SlavedIdTracker( db_conn, "events", "stream_ordering", step=-1 ) - super(SlavedEventStore, self).__init__(db_conn, hs) + super(SlavedEventStore, self).__init__(database, db_conn, hs) # Cached functions can't be accessed through a class instance so we need # to reach inside the __dict__ to extract them. diff --git a/synapse/replication/slave/storage/filtering.py b/synapse/replication/slave/storage/filtering.py index 5c84ebd125..bcb0688954 100644 --- a/synapse/replication/slave/storage/filtering.py +++ b/synapse/replication/slave/storage/filtering.py @@ -14,13 +14,14 @@ # limitations under the License. from synapse.storage.data_stores.main.filtering import FilteringStore +from synapse.storage.database import Database from ._base import BaseSlavedStore class SlavedFilteringStore(BaseSlavedStore): - def __init__(self, db_conn, hs): - super(SlavedFilteringStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(SlavedFilteringStore, self).__init__(database, db_conn, hs) # Filters are immutable so this cache doesn't need to be expired get_user_filter = FilteringStore.__dict__["get_user_filter"] diff --git a/synapse/replication/slave/storage/groups.py b/synapse/replication/slave/storage/groups.py index 28a46edd28..69a4ae42f9 100644 --- a/synapse/replication/slave/storage/groups.py +++ b/synapse/replication/slave/storage/groups.py @@ -14,6 +14,7 @@ # limitations under the License. from synapse.storage import DataStore +from synapse.storage.database import Database from synapse.util.caches.stream_change_cache import StreamChangeCache from ._base import BaseSlavedStore, __func__ @@ -21,8 +22,8 @@ from ._slaved_id_tracker import SlavedIdTracker class SlavedGroupServerStore(BaseSlavedStore): - def __init__(self, db_conn, hs): - super(SlavedGroupServerStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(SlavedGroupServerStore, self).__init__(database, db_conn, hs) self.hs = hs diff --git a/synapse/replication/slave/storage/presence.py b/synapse/replication/slave/storage/presence.py index 747ced0c84..f552e7c972 100644 --- a/synapse/replication/slave/storage/presence.py +++ b/synapse/replication/slave/storage/presence.py @@ -15,6 +15,7 @@ from synapse.storage import DataStore from synapse.storage.data_stores.main.presence import PresenceStore +from synapse.storage.database import Database from synapse.util.caches.stream_change_cache import StreamChangeCache from ._base import BaseSlavedStore, __func__ @@ -22,8 +23,8 @@ from ._slaved_id_tracker import SlavedIdTracker class SlavedPresenceStore(BaseSlavedStore): - def __init__(self, db_conn, hs): - super(SlavedPresenceStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(SlavedPresenceStore, self).__init__(database, db_conn, hs) self._presence_id_gen = SlavedIdTracker(db_conn, "presence_stream", "stream_id") self._presence_on_startup = self._get_active_presence(db_conn) diff --git a/synapse/replication/slave/storage/push_rule.py b/synapse/replication/slave/storage/push_rule.py index 3655f05e54..eebd5a1fb6 100644 --- a/synapse/replication/slave/storage/push_rule.py +++ b/synapse/replication/slave/storage/push_rule.py @@ -15,17 +15,18 @@ # limitations under the License. from synapse.storage.data_stores.main.push_rule import PushRulesWorkerStore +from synapse.storage.database import Database from ._slaved_id_tracker import SlavedIdTracker from .events import SlavedEventStore class SlavedPushRuleStore(SlavedEventStore, PushRulesWorkerStore): - def __init__(self, db_conn, hs): + def __init__(self, database: Database, db_conn, hs): self._push_rules_stream_id_gen = SlavedIdTracker( db_conn, "push_rules_stream", "stream_id" ) - super(SlavedPushRuleStore, self).__init__(db_conn, hs) + super(SlavedPushRuleStore, self).__init__(database, db_conn, hs) def get_push_rules_stream_token(self): return ( diff --git a/synapse/replication/slave/storage/pushers.py b/synapse/replication/slave/storage/pushers.py index b4331d0799..f22c2d44a3 100644 --- a/synapse/replication/slave/storage/pushers.py +++ b/synapse/replication/slave/storage/pushers.py @@ -15,14 +15,15 @@ # limitations under the License. from synapse.storage.data_stores.main.pusher import PusherWorkerStore +from synapse.storage.database import Database from ._base import BaseSlavedStore from ._slaved_id_tracker import SlavedIdTracker class SlavedPusherStore(PusherWorkerStore, BaseSlavedStore): - def __init__(self, db_conn, hs): - super(SlavedPusherStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(SlavedPusherStore, self).__init__(database, db_conn, hs) self._pushers_id_gen = SlavedIdTracker( db_conn, "pushers", "id", extra_tables=[("deleted_pushers", "stream_id")] ) diff --git a/synapse/replication/slave/storage/receipts.py b/synapse/replication/slave/storage/receipts.py index 43d823c601..d40dc6e1f5 100644 --- a/synapse/replication/slave/storage/receipts.py +++ b/synapse/replication/slave/storage/receipts.py @@ -15,6 +15,7 @@ # limitations under the License. from synapse.storage.data_stores.main.receipts import ReceiptsWorkerStore +from synapse.storage.database import Database from ._base import BaseSlavedStore from ._slaved_id_tracker import SlavedIdTracker @@ -29,14 +30,14 @@ from ._slaved_id_tracker import SlavedIdTracker class SlavedReceiptsStore(ReceiptsWorkerStore, BaseSlavedStore): - def __init__(self, db_conn, hs): + def __init__(self, database: Database, db_conn, hs): # We instantiate this first as the ReceiptsWorkerStore constructor # needs to be able to call get_max_receipt_stream_id self._receipts_id_gen = SlavedIdTracker( db_conn, "receipts_linearized", "stream_id" ) - super(SlavedReceiptsStore, self).__init__(db_conn, hs) + super(SlavedReceiptsStore, self).__init__(database, db_conn, hs) def get_max_receipt_stream_id(self): return self._receipts_id_gen.get_current_token() diff --git a/synapse/replication/slave/storage/room.py b/synapse/replication/slave/storage/room.py index d9ad386b28..3a20f45316 100644 --- a/synapse/replication/slave/storage/room.py +++ b/synapse/replication/slave/storage/room.py @@ -14,14 +14,15 @@ # limitations under the License. from synapse.storage.data_stores.main.room import RoomWorkerStore +from synapse.storage.database import Database from ._base import BaseSlavedStore from ._slaved_id_tracker import SlavedIdTracker class RoomStore(RoomWorkerStore, BaseSlavedStore): - def __init__(self, db_conn, hs): - super(RoomStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(RoomStore, self).__init__(database, db_conn, hs) self._public_room_id_gen = SlavedIdTracker( db_conn, "public_room_list_stream", "stream_id" ) diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py index b7e27d4e97..f9e7f9a71e 100644 --- a/synapse/storage/_base.py +++ b/synapse/storage/_base.py @@ -37,7 +37,7 @@ class SQLBaseStore(object): per data store (and not one per physical database). """ - def __init__(self, db_conn, hs): + def __init__(self, database: Database, db_conn, hs): self.hs = hs self._clock = hs.get_clock() self.database_engine = hs.database_engine diff --git a/synapse/storage/data_stores/main/__init__.py b/synapse/storage/data_stores/main/__init__.py index 6adb8adb04..7f5fd81bcf 100644 --- a/synapse/storage/data_stores/main/__init__.py +++ b/synapse/storage/data_stores/main/__init__.py @@ -20,6 +20,7 @@ import logging import time from synapse.api.constants import PresenceState +from synapse.storage.database import Database from synapse.storage.engines import PostgresEngine from synapse.storage.util.id_generators import ( ChainedIdGenerator, @@ -111,7 +112,7 @@ class DataStore( RelationsStore, CacheInvalidationStore, ): - def __init__(self, db_conn, hs): + def __init__(self, database: Database, db_conn, hs): self.hs = hs self._clock = hs.get_clock() self.database_engine = hs.database_engine @@ -169,7 +170,7 @@ class DataStore( else: self._cache_id_gen = None - super(DataStore, self).__init__(db_conn, hs) + super(DataStore, self).__init__(database, db_conn, hs) self._presence_on_startup = self._get_active_presence(db_conn) diff --git a/synapse/storage/data_stores/main/account_data.py b/synapse/storage/data_stores/main/account_data.py index a96fe9485c..44d20c19bf 100644 --- a/synapse/storage/data_stores/main/account_data.py +++ b/synapse/storage/data_stores/main/account_data.py @@ -22,6 +22,7 @@ from canonicaljson import json from twisted.internet import defer from synapse.storage._base import SQLBaseStore +from synapse.storage.database import Database from synapse.storage.util.id_generators import StreamIdGenerator from synapse.util.caches.descriptors import cached, cachedInlineCallbacks from synapse.util.caches.stream_change_cache import StreamChangeCache @@ -38,13 +39,13 @@ class AccountDataWorkerStore(SQLBaseStore): # the abstract methods being implemented. __metaclass__ = abc.ABCMeta - def __init__(self, db_conn, hs): + def __init__(self, database: Database, db_conn, hs): account_max = self.get_max_account_data_stream_id() self._account_data_stream_cache = StreamChangeCache( "AccountDataAndTagsChangeCache", account_max ) - super(AccountDataWorkerStore, self).__init__(db_conn, hs) + super(AccountDataWorkerStore, self).__init__(database, db_conn, hs) @abc.abstractmethod def get_max_account_data_stream_id(self): @@ -270,12 +271,12 @@ class AccountDataWorkerStore(SQLBaseStore): class AccountDataStore(AccountDataWorkerStore): - def __init__(self, db_conn, hs): + def __init__(self, database: Database, db_conn, hs): self._account_data_id_gen = StreamIdGenerator( db_conn, "account_data_max_stream_id", "stream_id" ) - super(AccountDataStore, self).__init__(db_conn, hs) + super(AccountDataStore, self).__init__(database, db_conn, hs) def get_max_account_data_stream_id(self): """Get the current max stream id for the private user data stream diff --git a/synapse/storage/data_stores/main/appservice.py b/synapse/storage/data_stores/main/appservice.py index 6b2e12719c..b2f39649fd 100644 --- a/synapse/storage/data_stores/main/appservice.py +++ b/synapse/storage/data_stores/main/appservice.py @@ -24,6 +24,7 @@ from synapse.appservice import AppServiceTransaction from synapse.config.appservice import load_appservices from synapse.storage._base import SQLBaseStore from synapse.storage.data_stores.main.events_worker import EventsWorkerStore +from synapse.storage.database import Database logger = logging.getLogger(__name__) @@ -48,13 +49,13 @@ def _make_exclusive_regex(services_cache): class ApplicationServiceWorkerStore(SQLBaseStore): - def __init__(self, db_conn, hs): + def __init__(self, database: Database, db_conn, hs): self.services_cache = load_appservices( hs.hostname, hs.config.app_service_config_files ) self.exclusive_user_regex = _make_exclusive_regex(self.services_cache) - super(ApplicationServiceWorkerStore, self).__init__(db_conn, hs) + super(ApplicationServiceWorkerStore, self).__init__(database, db_conn, hs) def get_app_services(self): return self.services_cache diff --git a/synapse/storage/data_stores/main/client_ips.py b/synapse/storage/data_stores/main/client_ips.py index 7b470a58f1..320c5b0f07 100644 --- a/synapse/storage/data_stores/main/client_ips.py +++ b/synapse/storage/data_stores/main/client_ips.py @@ -21,6 +21,7 @@ from twisted.internet import defer from synapse.metrics.background_process_metrics import wrap_as_background_process from synapse.storage._base import SQLBaseStore +from synapse.storage.database import Database from synapse.util.caches import CACHE_SIZE_FACTOR from synapse.util.caches.descriptors import Cache @@ -33,8 +34,8 @@ LAST_SEEN_GRANULARITY = 120 * 1000 class ClientIpBackgroundUpdateStore(SQLBaseStore): - def __init__(self, db_conn, hs): - super(ClientIpBackgroundUpdateStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(ClientIpBackgroundUpdateStore, self).__init__(database, db_conn, hs) self.db.updates.register_background_index_update( "user_ips_device_index", @@ -363,13 +364,13 @@ class ClientIpBackgroundUpdateStore(SQLBaseStore): class ClientIpStore(ClientIpBackgroundUpdateStore): - def __init__(self, db_conn, hs): + def __init__(self, database: Database, db_conn, hs): self.client_ip_last_seen = Cache( name="client_ip_last_seen", keylen=4, max_entries=50000 * CACHE_SIZE_FACTOR ) - super(ClientIpStore, self).__init__(db_conn, hs) + super(ClientIpStore, self).__init__(database, db_conn, hs) self.user_ips_max_age = hs.config.user_ips_max_age diff --git a/synapse/storage/data_stores/main/deviceinbox.py b/synapse/storage/data_stores/main/deviceinbox.py index 3c9f09301a..85cfa16850 100644 --- a/synapse/storage/data_stores/main/deviceinbox.py +++ b/synapse/storage/data_stores/main/deviceinbox.py @@ -21,6 +21,7 @@ from twisted.internet import defer from synapse.logging.opentracing import log_kv, set_tag, trace from synapse.storage._base import SQLBaseStore, make_in_list_sql_clause +from synapse.storage.database import Database from synapse.util.caches.expiringcache import ExpiringCache logger = logging.getLogger(__name__) @@ -210,8 +211,8 @@ class DeviceInboxWorkerStore(SQLBaseStore): class DeviceInboxBackgroundUpdateStore(SQLBaseStore): DEVICE_INBOX_STREAM_ID = "device_inbox_stream_drop" - def __init__(self, db_conn, hs): - super(DeviceInboxBackgroundUpdateStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(DeviceInboxBackgroundUpdateStore, self).__init__(database, db_conn, hs) self.db.updates.register_background_index_update( "device_inbox_stream_index", @@ -241,8 +242,8 @@ class DeviceInboxBackgroundUpdateStore(SQLBaseStore): class DeviceInboxStore(DeviceInboxWorkerStore, DeviceInboxBackgroundUpdateStore): DEVICE_INBOX_STREAM_ID = "device_inbox_stream_drop" - def __init__(self, db_conn, hs): - super(DeviceInboxStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(DeviceInboxStore, self).__init__(database, db_conn, hs) # Map of (user_id, device_id) to the last stream_id that has been # deleted up to. This is so that we can no op deletions. diff --git a/synapse/storage/data_stores/main/devices.py b/synapse/storage/data_stores/main/devices.py index 91ddaf137e..9a828231c4 100644 --- a/synapse/storage/data_stores/main/devices.py +++ b/synapse/storage/data_stores/main/devices.py @@ -31,6 +31,7 @@ from synapse.logging.opentracing import ( ) from synapse.metrics.background_process_metrics import run_as_background_process from synapse.storage._base import SQLBaseStore, db_to_json, make_in_list_sql_clause +from synapse.storage.database import Database from synapse.types import get_verify_key_from_cross_signing_key from synapse.util import batch_iter from synapse.util.caches.descriptors import ( @@ -642,8 +643,8 @@ class DeviceWorkerStore(SQLBaseStore): class DeviceBackgroundUpdateStore(SQLBaseStore): - def __init__(self, db_conn, hs): - super(DeviceBackgroundUpdateStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(DeviceBackgroundUpdateStore, self).__init__(database, db_conn, hs) self.db.updates.register_background_index_update( "device_lists_stream_idx", @@ -692,8 +693,8 @@ class DeviceBackgroundUpdateStore(SQLBaseStore): class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): - def __init__(self, db_conn, hs): - super(DeviceStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(DeviceStore, self).__init__(database, db_conn, hs) # Map of (user_id, device_id) -> bool. If there is an entry that implies # the device exists. diff --git a/synapse/storage/data_stores/main/event_federation.py b/synapse/storage/data_stores/main/event_federation.py index 31d2e8eb28..1f517e8fad 100644 --- a/synapse/storage/data_stores/main/event_federation.py +++ b/synapse/storage/data_stores/main/event_federation.py @@ -28,6 +28,7 @@ from synapse.metrics.background_process_metrics import run_as_background_process from synapse.storage._base import SQLBaseStore, make_in_list_sql_clause from synapse.storage.data_stores.main.events_worker import EventsWorkerStore from synapse.storage.data_stores.main.signatures import SignatureWorkerStore +from synapse.storage.database import Database from synapse.util.caches.descriptors import cached logger = logging.getLogger(__name__) @@ -491,8 +492,8 @@ class EventFederationStore(EventFederationWorkerStore): EVENT_AUTH_STATE_ONLY = "event_auth_state_only" - def __init__(self, db_conn, hs): - super(EventFederationStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(EventFederationStore, self).__init__(database, db_conn, hs) self.db.updates.register_background_update_handler( self.EVENT_AUTH_STATE_ONLY, self._background_delete_non_state_event_auth diff --git a/synapse/storage/data_stores/main/event_push_actions.py b/synapse/storage/data_stores/main/event_push_actions.py index eec054cd48..9988a6d3fc 100644 --- a/synapse/storage/data_stores/main/event_push_actions.py +++ b/synapse/storage/data_stores/main/event_push_actions.py @@ -24,6 +24,7 @@ from twisted.internet import defer from synapse.metrics.background_process_metrics import run_as_background_process from synapse.storage._base import LoggingTransaction, SQLBaseStore +from synapse.storage.database import Database from synapse.util.caches.descriptors import cachedInlineCallbacks logger = logging.getLogger(__name__) @@ -68,8 +69,8 @@ def _deserialize_action(actions, is_highlight): class EventPushActionsWorkerStore(SQLBaseStore): - def __init__(self, db_conn, hs): - super(EventPushActionsWorkerStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(EventPushActionsWorkerStore, self).__init__(database, db_conn, hs) # These get correctly set by _find_stream_orderings_for_times_txn self.stream_ordering_month_ago = None @@ -611,8 +612,8 @@ class EventPushActionsWorkerStore(SQLBaseStore): class EventPushActionsStore(EventPushActionsWorkerStore): EPA_HIGHLIGHT_INDEX = "epa_highlight_index" - def __init__(self, db_conn, hs): - super(EventPushActionsStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(EventPushActionsStore, self).__init__(database, db_conn, hs) self.db.updates.register_background_index_update( self.EPA_HIGHLIGHT_INDEX, diff --git a/synapse/storage/data_stores/main/events.py b/synapse/storage/data_stores/main/events.py index d644c82784..da1529f6ea 100644 --- a/synapse/storage/data_stores/main/events.py +++ b/synapse/storage/data_stores/main/events.py @@ -41,6 +41,7 @@ from synapse.storage._base import make_in_list_sql_clause from synapse.storage.data_stores.main.event_federation import EventFederationStore from synapse.storage.data_stores.main.events_worker import EventsWorkerStore from synapse.storage.data_stores.main.state import StateGroupWorkerStore +from synapse.storage.database import Database from synapse.types import RoomStreamToken, get_domain_from_id from synapse.util import batch_iter from synapse.util.caches.descriptors import cached, cachedInlineCallbacks @@ -95,8 +96,8 @@ def _retry_on_integrity_error(func): class EventsStore( StateGroupWorkerStore, EventFederationStore, EventsWorkerStore, ): - def __init__(self, db_conn, hs): - super(EventsStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(EventsStore, self).__init__(database, db_conn, hs) # Collect metrics on the number of forward extremities that exist. # Counter of number of extremities to count diff --git a/synapse/storage/data_stores/main/events_bg_updates.py b/synapse/storage/data_stores/main/events_bg_updates.py index cb1fc30c31..efee17b929 100644 --- a/synapse/storage/data_stores/main/events_bg_updates.py +++ b/synapse/storage/data_stores/main/events_bg_updates.py @@ -23,6 +23,7 @@ from twisted.internet import defer from synapse.api.constants import EventContentFields from synapse.storage._base import SQLBaseStore, make_in_list_sql_clause +from synapse.storage.database import Database logger = logging.getLogger(__name__) @@ -33,8 +34,8 @@ class EventsBackgroundUpdatesStore(SQLBaseStore): EVENT_FIELDS_SENDER_URL_UPDATE_NAME = "event_fields_sender_url" DELETE_SOFT_FAILED_EXTREMITIES = "delete_soft_failed_extremities" - def __init__(self, db_conn, hs): - super(EventsBackgroundUpdatesStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(EventsBackgroundUpdatesStore, self).__init__(database, db_conn, hs) self.db.updates.register_background_update_handler( self.EVENT_ORIGIN_SERVER_TS_NAME, self._background_reindex_origin_server_ts diff --git a/synapse/storage/data_stores/main/events_worker.py b/synapse/storage/data_stores/main/events_worker.py index e041fc5eac..9ee117ce0f 100644 --- a/synapse/storage/data_stores/main/events_worker.py +++ b/synapse/storage/data_stores/main/events_worker.py @@ -33,6 +33,7 @@ from synapse.events.utils import prune_event from synapse.logging.context import LoggingContext, PreserveLoggingContext from synapse.metrics.background_process_metrics import run_as_background_process from synapse.storage._base import SQLBaseStore, make_in_list_sql_clause +from synapse.storage.database import Database from synapse.types import get_domain_from_id from synapse.util import batch_iter from synapse.util.caches.descriptors import Cache @@ -55,8 +56,8 @@ _EventCacheEntry = namedtuple("_EventCacheEntry", ("event", "redacted_event")) class EventsWorkerStore(SQLBaseStore): - def __init__(self, db_conn, hs): - super(EventsWorkerStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(EventsWorkerStore, self).__init__(database, db_conn, hs) self._get_event_cache = Cache( "*getEvent*", keylen=3, max_entries=hs.config.event_cache_size diff --git a/synapse/storage/data_stores/main/media_repository.py b/synapse/storage/data_stores/main/media_repository.py index 03c9c6f8ae..80ca36dedf 100644 --- a/synapse/storage/data_stores/main/media_repository.py +++ b/synapse/storage/data_stores/main/media_repository.py @@ -13,11 +13,14 @@ # See the License for the specific language governing permissions and # limitations under the License. from synapse.storage._base import SQLBaseStore +from synapse.storage.database import Database class MediaRepositoryBackgroundUpdateStore(SQLBaseStore): - def __init__(self, db_conn, hs): - super(MediaRepositoryBackgroundUpdateStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(MediaRepositoryBackgroundUpdateStore, self).__init__( + database, db_conn, hs + ) self.db.updates.register_background_index_update( update_name="local_media_repository_url_idx", @@ -31,8 +34,8 @@ class MediaRepositoryBackgroundUpdateStore(SQLBaseStore): class MediaRepositoryStore(MediaRepositoryBackgroundUpdateStore): """Persistence for attachments and avatars""" - def __init__(self, db_conn, hs): - super(MediaRepositoryStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(MediaRepositoryStore, self).__init__(database, db_conn, hs) def get_local_media(self, media_id): """Get the metadata for a local piece of media diff --git a/synapse/storage/data_stores/main/monthly_active_users.py b/synapse/storage/data_stores/main/monthly_active_users.py index 34bf3a1880..27158534cb 100644 --- a/synapse/storage/data_stores/main/monthly_active_users.py +++ b/synapse/storage/data_stores/main/monthly_active_users.py @@ -17,6 +17,7 @@ import logging from twisted.internet import defer from synapse.storage._base import SQLBaseStore +from synapse.storage.database import Database from synapse.util.caches.descriptors import cached logger = logging.getLogger(__name__) @@ -27,13 +28,13 @@ LAST_SEEN_GRANULARITY = 60 * 60 * 1000 class MonthlyActiveUsersStore(SQLBaseStore): - def __init__(self, dbconn, hs): - super(MonthlyActiveUsersStore, self).__init__(None, hs) + def __init__(self, database: Database, db_conn, hs): + super(MonthlyActiveUsersStore, self).__init__(database, db_conn, hs) self._clock = hs.get_clock() self.hs = hs # Do not add more reserved users than the total allowable number self.db.new_transaction( - dbconn, + db_conn, "initialise_mau_threepids", [], [], diff --git a/synapse/storage/data_stores/main/push_rule.py b/synapse/storage/data_stores/main/push_rule.py index de682cc63a..5ba13aa973 100644 --- a/synapse/storage/data_stores/main/push_rule.py +++ b/synapse/storage/data_stores/main/push_rule.py @@ -27,6 +27,7 @@ from synapse.storage.data_stores.main.appservice import ApplicationServiceWorker from synapse.storage.data_stores.main.pusher import PusherWorkerStore from synapse.storage.data_stores.main.receipts import ReceiptsWorkerStore from synapse.storage.data_stores.main.roommember import RoomMemberWorkerStore +from synapse.storage.database import Database from synapse.storage.push_rule import InconsistentRuleException, RuleNotFoundException from synapse.util.caches.descriptors import cachedInlineCallbacks, cachedList from synapse.util.caches.stream_change_cache import StreamChangeCache @@ -72,8 +73,8 @@ class PushRulesWorkerStore( # the abstract methods being implemented. __metaclass__ = abc.ABCMeta - def __init__(self, db_conn, hs): - super(PushRulesWorkerStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(PushRulesWorkerStore, self).__init__(database, db_conn, hs) push_rules_prefill, push_rules_id = self.db.get_cache_dict( db_conn, diff --git a/synapse/storage/data_stores/main/receipts.py b/synapse/storage/data_stores/main/receipts.py index ac2d45bd5c..96e54d145e 100644 --- a/synapse/storage/data_stores/main/receipts.py +++ b/synapse/storage/data_stores/main/receipts.py @@ -22,6 +22,7 @@ from canonicaljson import json from twisted.internet import defer from synapse.storage._base import SQLBaseStore, make_in_list_sql_clause +from synapse.storage.database import Database from synapse.storage.util.id_generators import StreamIdGenerator from synapse.util.caches.descriptors import cached, cachedInlineCallbacks, cachedList from synapse.util.caches.stream_change_cache import StreamChangeCache @@ -38,8 +39,8 @@ class ReceiptsWorkerStore(SQLBaseStore): # the abstract methods being implemented. __metaclass__ = abc.ABCMeta - def __init__(self, db_conn, hs): - super(ReceiptsWorkerStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(ReceiptsWorkerStore, self).__init__(database, db_conn, hs) self._receipts_stream_cache = StreamChangeCache( "ReceiptsRoomChangeCache", self.get_max_receipt_stream_id() @@ -315,14 +316,14 @@ class ReceiptsWorkerStore(SQLBaseStore): class ReceiptsStore(ReceiptsWorkerStore): - def __init__(self, db_conn, hs): + def __init__(self, database: Database, db_conn, hs): # We instantiate this first as the ReceiptsWorkerStore constructor # needs to be able to call get_max_receipt_stream_id self._receipts_id_gen = StreamIdGenerator( db_conn, "receipts_linearized", "stream_id" ) - super(ReceiptsStore, self).__init__(db_conn, hs) + super(ReceiptsStore, self).__init__(database, db_conn, hs) def get_max_receipt_stream_id(self): return self._receipts_id_gen.get_current_token() diff --git a/synapse/storage/data_stores/main/registration.py b/synapse/storage/data_stores/main/registration.py index 1ef143c6d8..5e8ecac0ea 100644 --- a/synapse/storage/data_stores/main/registration.py +++ b/synapse/storage/data_stores/main/registration.py @@ -27,6 +27,7 @@ from synapse.api.constants import UserTypes from synapse.api.errors import Codes, StoreError, SynapseError, ThreepidValidationError from synapse.metrics.background_process_metrics import run_as_background_process from synapse.storage._base import SQLBaseStore +from synapse.storage.database import Database from synapse.types import UserID from synapse.util.caches.descriptors import cached, cachedInlineCallbacks @@ -36,8 +37,8 @@ logger = logging.getLogger(__name__) class RegistrationWorkerStore(SQLBaseStore): - def __init__(self, db_conn, hs): - super(RegistrationWorkerStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(RegistrationWorkerStore, self).__init__(database, db_conn, hs) self.config = hs.config self.clock = hs.get_clock() @@ -794,8 +795,8 @@ class RegistrationWorkerStore(SQLBaseStore): class RegistrationBackgroundUpdateStore(RegistrationWorkerStore): - def __init__(self, db_conn, hs): - super(RegistrationBackgroundUpdateStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(RegistrationBackgroundUpdateStore, self).__init__(database, db_conn, hs) self.clock = hs.get_clock() self.config = hs.config @@ -920,8 +921,8 @@ class RegistrationBackgroundUpdateStore(RegistrationWorkerStore): class RegistrationStore(RegistrationBackgroundUpdateStore): - def __init__(self, db_conn, hs): - super(RegistrationStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(RegistrationStore, self).__init__(database, db_conn, hs) self._account_validity = hs.config.account_validity diff --git a/synapse/storage/data_stores/main/room.py b/synapse/storage/data_stores/main/room.py index da42dae243..0148be20d3 100644 --- a/synapse/storage/data_stores/main/room.py +++ b/synapse/storage/data_stores/main/room.py @@ -29,6 +29,7 @@ from synapse.api.constants import EventTypes from synapse.api.errors import StoreError from synapse.storage._base import SQLBaseStore from synapse.storage.data_stores.main.search import SearchStore +from synapse.storage.database import Database from synapse.types import ThirdPartyInstanceID from synapse.util.caches.descriptors import cached, cachedInlineCallbacks @@ -361,8 +362,8 @@ class RoomWorkerStore(SQLBaseStore): class RoomBackgroundUpdateStore(SQLBaseStore): - def __init__(self, db_conn, hs): - super(RoomBackgroundUpdateStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(RoomBackgroundUpdateStore, self).__init__(database, db_conn, hs) self.config = hs.config @@ -440,8 +441,8 @@ class RoomBackgroundUpdateStore(SQLBaseStore): class RoomStore(RoomBackgroundUpdateStore, RoomWorkerStore, SearchStore): - def __init__(self, db_conn, hs): - super(RoomStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(RoomStore, self).__init__(database, db_conn, hs) self.config = hs.config diff --git a/synapse/storage/data_stores/main/roommember.py b/synapse/storage/data_stores/main/roommember.py index 929f6b0d39..92e3b9c512 100644 --- a/synapse/storage/data_stores/main/roommember.py +++ b/synapse/storage/data_stores/main/roommember.py @@ -32,6 +32,7 @@ from synapse.storage._base import ( make_in_list_sql_clause, ) from synapse.storage.data_stores.main.events_worker import EventsWorkerStore +from synapse.storage.database import Database from synapse.storage.engines import Sqlite3Engine from synapse.storage.roommember import ( GetRoomsForUserWithStreamOrdering, @@ -54,8 +55,8 @@ _CURRENT_STATE_MEMBERSHIP_UPDATE_NAME = "current_state_events_membership" class RoomMemberWorkerStore(EventsWorkerStore): - def __init__(self, db_conn, hs): - super(RoomMemberWorkerStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(RoomMemberWorkerStore, self).__init__(database, db_conn, hs) # Is the current_state_events.membership up to date? Or is the # background update still running? @@ -835,8 +836,8 @@ class RoomMemberWorkerStore(EventsWorkerStore): class RoomMemberBackgroundUpdateStore(SQLBaseStore): - def __init__(self, db_conn, hs): - super(RoomMemberBackgroundUpdateStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(RoomMemberBackgroundUpdateStore, self).__init__(database, db_conn, hs) self.db.updates.register_background_update_handler( _MEMBERSHIP_PROFILE_UPDATE_NAME, self._background_add_membership_profile ) @@ -991,8 +992,8 @@ class RoomMemberBackgroundUpdateStore(SQLBaseStore): class RoomMemberStore(RoomMemberWorkerStore, RoomMemberBackgroundUpdateStore): - def __init__(self, db_conn, hs): - super(RoomMemberStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(RoomMemberStore, self).__init__(database, db_conn, hs) def _store_room_members_txn(self, txn, events, backfilled): """Store a room member in the database. diff --git a/synapse/storage/data_stores/main/search.py b/synapse/storage/data_stores/main/search.py index ffa1817e64..4eec2fae5e 100644 --- a/synapse/storage/data_stores/main/search.py +++ b/synapse/storage/data_stores/main/search.py @@ -25,6 +25,7 @@ from twisted.internet import defer from synapse.api.errors import SynapseError from synapse.storage._base import SQLBaseStore, make_in_list_sql_clause +from synapse.storage.database import Database from synapse.storage.engines import PostgresEngine, Sqlite3Engine logger = logging.getLogger(__name__) @@ -42,8 +43,8 @@ class SearchBackgroundUpdateStore(SQLBaseStore): EVENT_SEARCH_USE_GIST_POSTGRES_NAME = "event_search_postgres_gist" EVENT_SEARCH_USE_GIN_POSTGRES_NAME = "event_search_postgres_gin" - def __init__(self, db_conn, hs): - super(SearchBackgroundUpdateStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(SearchBackgroundUpdateStore, self).__init__(database, db_conn, hs) if not hs.config.enable_search: return @@ -342,8 +343,8 @@ class SearchBackgroundUpdateStore(SQLBaseStore): class SearchStore(SearchBackgroundUpdateStore): - def __init__(self, db_conn, hs): - super(SearchStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(SearchStore, self).__init__(database, db_conn, hs) def store_event_search_txn(self, txn, event, key, value): """Add event to the search table diff --git a/synapse/storage/data_stores/main/state.py b/synapse/storage/data_stores/main/state.py index 7d5a9f8128..9ef7b48c74 100644 --- a/synapse/storage/data_stores/main/state.py +++ b/synapse/storage/data_stores/main/state.py @@ -28,6 +28,7 @@ from synapse.events import EventBase from synapse.events.snapshot import EventContext from synapse.storage._base import SQLBaseStore from synapse.storage.data_stores.main.events_worker import EventsWorkerStore +from synapse.storage.database import Database from synapse.storage.engines import PostgresEngine from synapse.storage.state import StateFilter from synapse.util.caches import get_cache_factor_for, intern_string @@ -213,8 +214,8 @@ class StateGroupWorkerStore( STATE_GROUP_INDEX_UPDATE_NAME = "state_group_state_type_index" CURRENT_STATE_INDEX_UPDATE_NAME = "current_state_members_idx" - def __init__(self, db_conn, hs): - super(StateGroupWorkerStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(StateGroupWorkerStore, self).__init__(database, db_conn, hs) # Originally the state store used a single DictionaryCache to cache the # event IDs for the state types in a given state group to avoid hammering @@ -1029,8 +1030,8 @@ class StateBackgroundUpdateStore(StateGroupBackgroundUpdateStore): CURRENT_STATE_INDEX_UPDATE_NAME = "current_state_members_idx" EVENT_STATE_GROUP_INDEX_UPDATE_NAME = "event_to_state_groups_sg_index" - def __init__(self, db_conn, hs): - super(StateBackgroundUpdateStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(StateBackgroundUpdateStore, self).__init__(database, db_conn, hs) self.db.updates.register_background_update_handler( self.STATE_GROUP_DEDUPLICATION_UPDATE_NAME, self._background_deduplicate_state, @@ -1245,8 +1246,8 @@ class StateStore(StateGroupWorkerStore, StateBackgroundUpdateStore): * `state_groups_state`: Maps state group to state events. """ - def __init__(self, db_conn, hs): - super(StateStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(StateStore, self).__init__(database, db_conn, hs) def _store_event_state_mappings_txn( self, txn, events_and_contexts: Iterable[Tuple[EventBase, EventContext]] diff --git a/synapse/storage/data_stores/main/stats.py b/synapse/storage/data_stores/main/stats.py index 40579bf965..7bc186e9a1 100644 --- a/synapse/storage/data_stores/main/stats.py +++ b/synapse/storage/data_stores/main/stats.py @@ -22,6 +22,7 @@ from twisted.internet.defer import DeferredLock from synapse.api.constants import EventTypes, Membership from synapse.storage.data_stores.main.state_deltas import StateDeltasStore +from synapse.storage.database import Database from synapse.storage.engines import PostgresEngine from synapse.util.caches.descriptors import cached @@ -58,8 +59,8 @@ TYPE_TO_ORIGIN_TABLE = {"room": ("rooms", "room_id"), "user": ("users", "name")} class StatsStore(StateDeltasStore): - def __init__(self, db_conn, hs): - super(StatsStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(StatsStore, self).__init__(database, db_conn, hs) self.server_name = hs.hostname self.clock = self.hs.get_clock() diff --git a/synapse/storage/data_stores/main/stream.py b/synapse/storage/data_stores/main/stream.py index 2ff8c57109..140da8dad6 100644 --- a/synapse/storage/data_stores/main/stream.py +++ b/synapse/storage/data_stores/main/stream.py @@ -47,6 +47,7 @@ from twisted.internet import defer from synapse.logging.context import make_deferred_yieldable, run_in_background from synapse.storage._base import SQLBaseStore from synapse.storage.data_stores.main.events_worker import EventsWorkerStore +from synapse.storage.database import Database from synapse.storage.engines import PostgresEngine from synapse.types import RoomStreamToken from synapse.util.caches.stream_change_cache import StreamChangeCache @@ -251,8 +252,8 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): __metaclass__ = abc.ABCMeta - def __init__(self, db_conn, hs): - super(StreamWorkerStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(StreamWorkerStore, self).__init__(database, db_conn, hs) events_max = self.get_room_max_stream_ordering() event_cache_prefill, min_event_val = self.db.get_cache_dict( diff --git a/synapse/storage/data_stores/main/transactions.py b/synapse/storage/data_stores/main/transactions.py index c0d155a43c..5b07c2fbc0 100644 --- a/synapse/storage/data_stores/main/transactions.py +++ b/synapse/storage/data_stores/main/transactions.py @@ -24,6 +24,7 @@ from twisted.internet import defer from synapse.metrics.background_process_metrics import run_as_background_process from synapse.storage._base import SQLBaseStore, db_to_json +from synapse.storage.database import Database from synapse.util.caches.expiringcache import ExpiringCache # py2 sqlite has buffer hardcoded as only binary type, so we must use it, @@ -52,8 +53,8 @@ class TransactionStore(SQLBaseStore): """A collection of queries for handling PDUs. """ - def __init__(self, db_conn, hs): - super(TransactionStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(TransactionStore, self).__init__(database, db_conn, hs) self._clock.looping_call(self._start_cleanup_transactions, 30 * 60 * 1000) diff --git a/synapse/storage/data_stores/main/user_directory.py b/synapse/storage/data_stores/main/user_directory.py index 62ffb34b29..90c180ec6d 100644 --- a/synapse/storage/data_stores/main/user_directory.py +++ b/synapse/storage/data_stores/main/user_directory.py @@ -21,6 +21,7 @@ from twisted.internet import defer from synapse.api.constants import EventTypes, JoinRules from synapse.storage.data_stores.main.state import StateFilter from synapse.storage.data_stores.main.state_deltas import StateDeltasStore +from synapse.storage.database import Database from synapse.storage.engines import PostgresEngine, Sqlite3Engine from synapse.types import get_domain_from_id, get_localpart_from_id from synapse.util.caches.descriptors import cached @@ -37,8 +38,8 @@ class UserDirectoryBackgroundUpdateStore(StateDeltasStore): # add_users_who_share_private_rooms? SHARE_PRIVATE_WORKING_SET = 500 - def __init__(self, db_conn, hs): - super(UserDirectoryBackgroundUpdateStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(UserDirectoryBackgroundUpdateStore, self).__init__(database, db_conn, hs) self.server_name = hs.hostname @@ -549,8 +550,8 @@ class UserDirectoryStore(UserDirectoryBackgroundUpdateStore): # add_users_who_share_private_rooms? SHARE_PRIVATE_WORKING_SET = 500 - def __init__(self, db_conn, hs): - super(UserDirectoryStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(UserDirectoryStore, self).__init__(database, db_conn, hs) def remove_from_user_dir(self, user_id): def _remove_from_user_dir_txn(txn): diff --git a/tests/storage/test_appservice.py b/tests/storage/test_appservice.py index dfeea24599..1679112d82 100644 --- a/tests/storage/test_appservice.py +++ b/tests/storage/test_appservice.py @@ -28,6 +28,7 @@ from synapse.storage.data_stores.main.appservice import ( ApplicationServiceStore, ApplicationServiceTransactionStore, ) +from synapse.storage.database import Database from tests import unittest from tests.utils import setup_test_homeserver @@ -382,8 +383,8 @@ class ApplicationServiceTransactionStoreTestCase(unittest.TestCase): # required for ApplicationServiceTransactionStoreTestCase tests class TestTransactionStore(ApplicationServiceTransactionStore, ApplicationServiceStore): - def __init__(self, db_conn, hs): - super(TestTransactionStore, self).__init__(db_conn, hs) + def __init__(self, database: Database, db_conn, hs): + super(TestTransactionStore, self).__init__(database, db_conn, hs) class ApplicationServiceStoreConfigTestCase(unittest.TestCase): From d64bb32a73761ad55f53152756b8e0c10e1de9b0 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 6 Dec 2019 13:09:40 +0000 Subject: [PATCH 0617/1623] Move are_all_users_on_domain checks to main data store. --- synapse/app/homeserver.py | 12 +--------- synapse/storage/__init__.py | 12 ---------- synapse/storage/data_stores/main/__init__.py | 24 +++++++++++++++++++- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py index 9f81a857ab..6d6c1f8e0e 100644 --- a/synapse/app/homeserver.py +++ b/synapse/app/homeserver.py @@ -68,7 +68,7 @@ from synapse.rest.key.v2 import KeyApiV2Resource from synapse.rest.media.v0.content_repository import ContentRepoResource from synapse.rest.well_known import WellKnownResource from synapse.server import HomeServer -from synapse.storage import DataStore, are_all_users_on_domain +from synapse.storage import DataStore from synapse.storage.engines import IncorrectDatabaseSetup, create_engine from synapse.storage.prepare_database import UpgradeDatabaseException, prepare_database from synapse.util.caches import CACHE_SIZE_FACTOR @@ -295,16 +295,6 @@ class SynapseHomeServer(HomeServer): logger.warning("Unrecognized listener type: %s", listener["type"]) def run_startup_checks(self, db_conn, database_engine): - all_users_native = are_all_users_on_domain( - db_conn.cursor(), database_engine, self.hostname - ) - if not all_users_native: - quit_with_error( - "Found users in database not native to %s!\n" - "You cannot changed a synapse server_name after it's been configured" - % (self.hostname,) - ) - try: database_engine.check_database(db_conn.cursor()) except IncorrectDatabaseSetup as e: diff --git a/synapse/storage/__init__.py b/synapse/storage/__init__.py index 8fb18203dc..ec89f645d4 100644 --- a/synapse/storage/__init__.py +++ b/synapse/storage/__init__.py @@ -49,15 +49,3 @@ class Storage(object): self.persistence = EventsPersistenceStorage(hs, stores) self.purge_events = PurgeEventsStorage(hs, stores) self.state = StateGroupStorage(hs, stores) - - -def are_all_users_on_domain(txn, database_engine, domain): - sql = database_engine.convert_param_style( - "SELECT COUNT(*) FROM users WHERE name NOT LIKE ?" - ) - pat = "%:" + domain - txn.execute(sql, (pat,)) - num_not_matching = txn.fetchall()[0][0] - if num_not_matching == 0: - return True - return False diff --git a/synapse/storage/data_stores/main/__init__.py b/synapse/storage/data_stores/main/__init__.py index 7f5fd81bcf..66f8a9f3a7 100644 --- a/synapse/storage/data_stores/main/__init__.py +++ b/synapse/storage/data_stores/main/__init__.py @@ -115,7 +115,17 @@ class DataStore( def __init__(self, database: Database, db_conn, hs): self.hs = hs self._clock = hs.get_clock() - self.database_engine = hs.database_engine + self.database_engine = database.engine + + all_users_native = are_all_users_on_domain( + db_conn.cursor(), database.engine, hs.hostname + ) + if not all_users_native: + raise Exception( + "Found users in database not native to %s!\n" + "You cannot changed a synapse server_name after it's been configured" + % (self.hostname,) + ) self._stream_id_gen = StreamIdGenerator( db_conn, @@ -555,3 +565,15 @@ class DataStore( retcols=["name", "password_hash", "is_guest", "admin", "user_type"], desc="search_users", ) + + +def are_all_users_on_domain(txn, database_engine, domain): + sql = database_engine.convert_param_style( + "SELECT COUNT(*) FROM users WHERE name NOT LIKE ?" + ) + pat = "%:" + domain + txn.execute(sql, (pat,)) + num_not_matching = txn.fetchall()[0][0] + if num_not_matching == 0: + return True + return False From d537be1ebd0e7ce4c84118efa400932cc6432aa9 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 6 Dec 2019 13:40:02 +0000 Subject: [PATCH 0618/1623] Pass Database into the data store --- synapse/server.py | 3 +- synapse/storage/_base.py | 2 +- synapse/storage/background_updates.py | 2 +- synapse/storage/data_stores/__init__.py | 7 +++-- synapse/storage/database.py | 38 +++++++++++-------------- 5 files changed, 24 insertions(+), 28 deletions(-) diff --git a/synapse/server.py b/synapse/server.py index be9af7f986..2db3dab221 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -238,8 +238,7 @@ class HomeServer(object): def setup(self): logger.info("Setting up.") with self.get_db_conn() as conn: - datastore = self.DATASTORE_CLASS(conn, self) - self.datastores = DataStores(datastore, conn, self) + self.datastores = DataStores(self.DATASTORE_CLASS, conn, self) conn.commit() self.start_time = int(self.get_clock().time()) logger.info("Finished setting up.") diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py index f9e7f9a71e..b7637b5dc0 100644 --- a/synapse/storage/_base.py +++ b/synapse/storage/_base.py @@ -41,7 +41,7 @@ class SQLBaseStore(object): self.hs = hs self._clock = hs.get_clock() self.database_engine = hs.database_engine - self.db = Database(hs) # In future this will be passed in + self.db = database self.rand = random.SystemRandom() def _invalidate_state_caches(self, room_id, members_changed): diff --git a/synapse/storage/background_updates.py b/synapse/storage/background_updates.py index a9a13a2658..4f97fd5ab6 100644 --- a/synapse/storage/background_updates.py +++ b/synapse/storage/background_updates.py @@ -379,7 +379,7 @@ class BackgroundUpdater(object): logger.debug("[SQL] %s", sql) c.execute(sql) - if isinstance(self.db.database_engine, engines.PostgresEngine): + if isinstance(self.db.engine, engines.PostgresEngine): runner = create_index_psql elif psql_only: runner = None diff --git a/synapse/storage/data_stores/__init__.py b/synapse/storage/data_stores/__init__.py index cb184a98cc..79ecc62735 100644 --- a/synapse/storage/data_stores/__init__.py +++ b/synapse/storage/data_stores/__init__.py @@ -13,6 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from synapse.storage.database import Database + class DataStores(object): """The various data stores. @@ -20,7 +22,8 @@ class DataStores(object): These are low level interfaces to physical databases. """ - def __init__(self, main_store, db_conn, hs): + def __init__(self, main_store_class, db_conn, hs): # Note we pass in the main store here as workers use a different main # store. - self.main = main_store + database = Database(hs) + self.main = main_store_class(database, db_conn, hs) diff --git a/synapse/storage/database.py b/synapse/storage/database.py index 6843b7e7f8..ec19ae1d9d 100644 --- a/synapse/storage/database.py +++ b/synapse/storage/database.py @@ -234,7 +234,7 @@ class Database(object): # to watch it self._txn_perf_counters = PerformanceCounters() - self.database_engine = hs.database_engine + self.engine = hs.database_engine # A set of tables that are not safe to use native upserts in. self._unsafe_to_upsert_tables = set(UNIQUE_INDEX_BACKGROUND_UPDATES.keys()) @@ -242,10 +242,10 @@ class Database(object): # We add the user_directory_search table to the blacklist on SQLite # because the existing search table does not have an index, making it # unsafe to use native upserts. - if isinstance(self.database_engine, Sqlite3Engine): + if isinstance(self.engine, Sqlite3Engine): self._unsafe_to_upsert_tables.add("user_directory_search") - if self.database_engine.can_native_upsert: + if self.engine.can_native_upsert: # Check ASAP (and then later, every 1s) to see if we have finished # background updates of tables that aren't safe to update. self._clock.call_later( @@ -331,7 +331,7 @@ class Database(object): cursor = LoggingTransaction( conn.cursor(), name, - self.database_engine, + self.engine, after_callbacks, exception_callbacks, ) @@ -339,7 +339,7 @@ class Database(object): r = func(cursor, *args, **kwargs) conn.commit() return r - except self.database_engine.module.OperationalError as e: + except self.engine.module.OperationalError as e: # This can happen if the database disappears mid # transaction. logger.warning( @@ -353,20 +353,20 @@ class Database(object): i += 1 try: conn.rollback() - except self.database_engine.module.Error as e1: + except self.engine.module.Error as e1: logger.warning( "[TXN EROLL] {%s} %s", name, exception_to_unicode(e1) ) continue raise - except self.database_engine.module.DatabaseError as e: - if self.database_engine.is_deadlock(e): + except self.engine.module.DatabaseError as e: + if self.engine.is_deadlock(e): logger.warning("[TXN DEADLOCK] {%s} %d/%d", name, i, N) if i < N: i += 1 try: conn.rollback() - except self.database_engine.module.Error as e1: + except self.engine.module.Error as e1: logger.warning( "[TXN EROLL] {%s} %s", name, @@ -494,7 +494,7 @@ class Database(object): sql_scheduling_timer.observe(sched_duration_sec) context.add_database_scheduled(sched_duration_sec) - if self.database_engine.is_connection_closed(conn): + if self.engine.is_connection_closed(conn): logger.debug("Reconnecting closed database connection") conn.reconnect() @@ -561,7 +561,7 @@ class Database(object): """ try: yield self.runInteraction(desc, self.simple_insert_txn, table, values) - except self.database_engine.module.IntegrityError: + except self.engine.module.IntegrityError: # We have to do or_ignore flag at this layer, since we can't reuse # a cursor after we receive an error from the db. if not or_ignore: @@ -660,7 +660,7 @@ class Database(object): lock=lock, ) return result - except self.database_engine.module.IntegrityError as e: + except self.engine.module.IntegrityError as e: attempts += 1 if attempts >= 5: # don't retry forever, because things other than races @@ -692,10 +692,7 @@ class Database(object): upserts return True if a new entry was created, False if an existing one was updated. """ - if ( - self.database_engine.can_native_upsert - and table not in self._unsafe_to_upsert_tables - ): + if self.engine.can_native_upsert and table not in self._unsafe_to_upsert_tables: return self.simple_upsert_txn_native_upsert( txn, table, keyvalues, values, insertion_values=insertion_values ) @@ -726,7 +723,7 @@ class Database(object): """ # We need to lock the table :(, unless we're *really* careful if lock: - self.database_engine.lock_table(txn, table) + self.engine.lock_table(txn, table) def _getwhere(key): # If the value we're passing in is None (aka NULL), we need to use @@ -828,10 +825,7 @@ class Database(object): Returns: None """ - if ( - self.database_engine.can_native_upsert - and table not in self._unsafe_to_upsert_tables - ): + if self.engine.can_native_upsert and table not in self._unsafe_to_upsert_tables: return self.simple_upsert_many_txn_native_upsert( txn, table, key_names, key_values, value_names, value_values ) @@ -1301,7 +1295,7 @@ class Database(object): "limit": limit, } - sql = self.database_engine.convert_param_style(sql) + sql = self.engine.convert_param_style(sql) txn = db_conn.cursor() txn.execute(sql, (int(max_value),)) From 75f87450d82e5039d90eabd60b70ddea97a6bdbc Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 6 Dec 2019 13:09:40 +0000 Subject: [PATCH 0619/1623] Move start up DB checks to main data store. --- synapse/app/homeserver.py | 23 +++++------------------ synapse/storage/data_stores/__init__.py | 7 +++++++ 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py index 6d6c1f8e0e..df65d0a989 100644 --- a/synapse/app/homeserver.py +++ b/synapse/app/homeserver.py @@ -70,7 +70,7 @@ from synapse.rest.well_known import WellKnownResource from synapse.server import HomeServer from synapse.storage import DataStore from synapse.storage.engines import IncorrectDatabaseSetup, create_engine -from synapse.storage.prepare_database import UpgradeDatabaseException, prepare_database +from synapse.storage.prepare_database import UpgradeDatabaseException from synapse.util.caches import CACHE_SIZE_FACTOR from synapse.util.httpresourcetree import create_resource_tree from synapse.util.manhole import manhole @@ -294,12 +294,6 @@ class SynapseHomeServer(HomeServer): else: logger.warning("Unrecognized listener type: %s", listener["type"]) - def run_startup_checks(self, db_conn, database_engine): - try: - database_engine.check_database(db_conn.cursor()) - except IncorrectDatabaseSetup as e: - quit_with_error(str(e)) - # Gauges to expose monthly active user control metrics current_mau_gauge = Gauge("synapse_admin_mau:current", "Current MAU") @@ -347,16 +341,12 @@ def setup(config_options): synapse.config.logger.setup_logging(hs, config, use_worker_options=False) - logger.info("Preparing database: %s...", config.database_config["name"]) + logger.info("Setting up server") try: - with hs.get_db_conn(run_new_connection=False) as db_conn: - prepare_database(db_conn, database_engine, config=config) - database_engine.on_new_connection(db_conn) - - hs.run_startup_checks(db_conn, database_engine) - - db_conn.commit() + hs.setup() + except IncorrectDatabaseSetup as e: + quit_with_error(str(e)) except UpgradeDatabaseException: sys.stderr.write( "\nFailed to upgrade database.\n" @@ -365,9 +355,6 @@ def setup(config_options): ) sys.exit(1) - logger.info("Database prepared in %s.", config.database_config["name"]) - - hs.setup() hs.setup_master() @defer.inlineCallbacks diff --git a/synapse/storage/data_stores/__init__.py b/synapse/storage/data_stores/__init__.py index 79ecc62735..cecc04f03f 100644 --- a/synapse/storage/data_stores/__init__.py +++ b/synapse/storage/data_stores/__init__.py @@ -14,6 +14,7 @@ # limitations under the License. from synapse.storage.database import Database +from synapse.storage.prepare_database import prepare_database class DataStores(object): @@ -26,4 +27,10 @@ class DataStores(object): # Note we pass in the main store here as workers use a different main # store. database = Database(hs) + + # Check that db is correctly configured. + database.engine.check_database(db_conn.cursor()) + + prepare_database(db_conn, database.engine, config=hs.config) + self.main = main_store_class(database, db_conn, hs) From 852f80d8a697c9d556b1b2641a2e4c1797cbbb46 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 6 Dec 2019 16:02:50 +0000 Subject: [PATCH 0620/1623] Fixup tests --- tests/replication/slave/storage/_base.py | 5 ++++- tests/storage/test_appservice.py | 12 +++++++----- tests/storage/test_base.py | 3 ++- tests/storage/test_profile.py | 3 +-- tests/storage/test_user_directory.py | 4 +--- tests/test_federation.py | 16 +++++----------- 6 files changed, 20 insertions(+), 23 deletions(-) diff --git a/tests/replication/slave/storage/_base.py b/tests/replication/slave/storage/_base.py index e7472e3a93..3dae83c543 100644 --- a/tests/replication/slave/storage/_base.py +++ b/tests/replication/slave/storage/_base.py @@ -20,6 +20,7 @@ from synapse.replication.tcp.client import ( ReplicationClientHandler, ) from synapse.replication.tcp.resource import ReplicationStreamProtocolFactory +from synapse.storage.database import Database from tests import unittest from tests.server import FakeTransport @@ -42,7 +43,9 @@ class BaseSlavedStoreTestCase(unittest.HomeserverTestCase): self.master_store = self.hs.get_datastore() self.storage = hs.get_storage() - self.slaved_store = self.STORE_TYPE(self.hs.get_db_conn(), self.hs) + self.slaved_store = self.STORE_TYPE( + Database(hs), self.hs.get_db_conn(), self.hs + ) self.event_id = 0 server_factory = ReplicationStreamProtocolFactory(self.hs) diff --git a/tests/storage/test_appservice.py b/tests/storage/test_appservice.py index 1679112d82..2e521e9ab7 100644 --- a/tests/storage/test_appservice.py +++ b/tests/storage/test_appservice.py @@ -55,7 +55,8 @@ class ApplicationServiceStoreTestCase(unittest.TestCase): self._add_appservice("token2", "as2", "some_url", "some_hs_token", "bob") self._add_appservice("token3", "as3", "some_url", "some_hs_token", "bob") # must be done after inserts - self.store = ApplicationServiceStore(hs.get_db_conn(), hs) + database = Database(hs) + self.store = ApplicationServiceStore(database, hs.get_db_conn(), hs) def tearDown(self): # TODO: suboptimal that we need to create files for tests! @@ -124,7 +125,8 @@ class ApplicationServiceTransactionStoreTestCase(unittest.TestCase): self.as_yaml_files = [] - self.store = TestTransactionStore(hs.get_db_conn(), hs) + database = Database(hs) + self.store = TestTransactionStore(database, hs.get_db_conn(), hs) def _add_service(self, url, as_token, id): as_yaml = dict( @@ -417,7 +419,7 @@ class ApplicationServiceStoreConfigTestCase(unittest.TestCase): hs.config.event_cache_size = 1 hs.config.password_providers = [] - ApplicationServiceStore(hs.get_db_conn(), hs) + ApplicationServiceStore(Database(hs), hs.get_db_conn(), hs) @defer.inlineCallbacks def test_duplicate_ids(self): @@ -433,7 +435,7 @@ class ApplicationServiceStoreConfigTestCase(unittest.TestCase): hs.config.password_providers = [] with self.assertRaises(ConfigError) as cm: - ApplicationServiceStore(hs.get_db_conn(), hs) + ApplicationServiceStore(Database(hs), hs.get_db_conn(), hs) e = cm.exception self.assertIn(f1, str(e)) @@ -454,7 +456,7 @@ class ApplicationServiceStoreConfigTestCase(unittest.TestCase): hs.config.password_providers = [] with self.assertRaises(ConfigError) as cm: - ApplicationServiceStore(hs.get_db_conn(), hs) + ApplicationServiceStore(Database(hs), hs.get_db_conn(), hs) e = cm.exception self.assertIn(f1, str(e)) diff --git a/tests/storage/test_base.py b/tests/storage/test_base.py index 7915d48a9e..537cfe9f64 100644 --- a/tests/storage/test_base.py +++ b/tests/storage/test_base.py @@ -21,6 +21,7 @@ from mock import Mock from twisted.internet import defer from synapse.storage._base import SQLBaseStore +from synapse.storage.database import Database from synapse.storage.engines import create_engine from tests import unittest @@ -59,7 +60,7 @@ class SQLBaseStoreTestCase(unittest.TestCase): "test", db_pool=self.db_pool, config=config, database_engine=fake_engine ) - self.datastore = SQLBaseStore(None, hs) + self.datastore = SQLBaseStore(Database(hs), None, hs) @defer.inlineCallbacks def test_insert_1col(self): diff --git a/tests/storage/test_profile.py b/tests/storage/test_profile.py index 24c7fe16c3..9b6f7211ae 100644 --- a/tests/storage/test_profile.py +++ b/tests/storage/test_profile.py @@ -16,7 +16,6 @@ from twisted.internet import defer -from synapse.storage.data_stores.main.profile import ProfileStore from synapse.types import UserID from tests import unittest @@ -28,7 +27,7 @@ class ProfileStoreTestCase(unittest.TestCase): def setUp(self): hs = yield setup_test_homeserver(self.addCleanup) - self.store = ProfileStore(hs.get_db_conn(), hs) + self.store = hs.get_datastore() self.u_frank = UserID.from_string("@frank:test") diff --git a/tests/storage/test_user_directory.py b/tests/storage/test_user_directory.py index 7eea57c0e2..6a545d2eb0 100644 --- a/tests/storage/test_user_directory.py +++ b/tests/storage/test_user_directory.py @@ -15,8 +15,6 @@ from twisted.internet import defer -from synapse.storage.data_stores.main.user_directory import UserDirectoryStore - from tests import unittest from tests.utils import setup_test_homeserver @@ -29,7 +27,7 @@ class UserDirectoryStoreTestCase(unittest.TestCase): @defer.inlineCallbacks def setUp(self): self.hs = yield setup_test_homeserver(self.addCleanup) - self.store = UserDirectoryStore(self.hs.get_db_conn(), self.hs) + self.store = self.hs.get_datastore() # alice and bob are both in !room_id. bobby is not but shares # a homeserver with alice. diff --git a/tests/test_federation.py b/tests/test_federation.py index 7d82b58466..ad165d7295 100644 --- a/tests/test_federation.py +++ b/tests/test_federation.py @@ -33,6 +33,8 @@ class MessageAcceptTests(unittest.TestCase): self.reactor.advance(0.1) self.room_id = self.successResultOf(room)["room_id"] + self.store = self.homeserver.get_datastore() + # Figure out what the most recent event is most_recent = self.successResultOf( maybeDeferred( @@ -77,10 +79,7 @@ class MessageAcceptTests(unittest.TestCase): # Make sure we actually joined the room self.assertEqual( self.successResultOf( - maybeDeferred( - self.homeserver.get_datastore().get_latest_event_ids_in_room, - self.room_id, - ) + maybeDeferred(self.store.get_latest_event_ids_in_room, self.room_id) )[0], "$join:test.serv", ) @@ -100,10 +99,7 @@ class MessageAcceptTests(unittest.TestCase): # Figure out what the most recent event is most_recent = self.successResultOf( - maybeDeferred( - self.homeserver.get_datastore().get_latest_event_ids_in_room, - self.room_id, - ) + maybeDeferred(self.store.get_latest_event_ids_in_room, self.room_id) )[0] # Now lie about an event @@ -141,7 +137,5 @@ class MessageAcceptTests(unittest.TestCase): ) # Make sure the invalid event isn't there - extrem = maybeDeferred( - self.homeserver.get_datastore().get_latest_event_ids_in_room, self.room_id - ) + extrem = maybeDeferred(self.store.get_latest_event_ids_in_room, self.room_id) self.assertEqual(self.successResultOf(extrem)[0], "$join:test.serv") From 5e35f69ac35cbe12d2e7e033e68ec507222c40b3 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 6 Dec 2019 16:13:41 +0000 Subject: [PATCH 0621/1623] Newsfile --- changelog.d/6487.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6487.misc diff --git a/changelog.d/6487.misc b/changelog.d/6487.misc new file mode 100644 index 0000000000..18b49b9cbd --- /dev/null +++ b/changelog.d/6487.misc @@ -0,0 +1 @@ +Pass in `Database` object to data stores. From 71ee22c0baf7c804ecc93ce5fd9ac00e1f410e6f Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 6 Dec 2019 16:27:29 +0000 Subject: [PATCH 0622/1623] Fix port db script --- .buildkite/postgres-config.yaml | 2 +- .buildkite/sqlite-config.yaml | 2 +- scripts/synapse_port_db | 36 ++------------------ synapse/storage/data_stores/main/__init__.py | 2 +- 4 files changed, 5 insertions(+), 37 deletions(-) diff --git a/.buildkite/postgres-config.yaml b/.buildkite/postgres-config.yaml index a35fec394d..2acbe66f4c 100644 --- a/.buildkite/postgres-config.yaml +++ b/.buildkite/postgres-config.yaml @@ -1,7 +1,7 @@ # Configuration file used for testing the 'synapse_port_db' script. # Tells the script to connect to the postgresql database that will be available in the # CI's Docker setup at the point where this file is considered. -server_name: "test" +server_name: "localhost:8800" signing_key_path: "/src/.buildkite/test.signing.key" diff --git a/.buildkite/sqlite-config.yaml b/.buildkite/sqlite-config.yaml index 635b921764..6d9bf80d84 100644 --- a/.buildkite/sqlite-config.yaml +++ b/.buildkite/sqlite-config.yaml @@ -1,7 +1,7 @@ # Configuration file used for testing the 'synapse_port_db' script. # Tells the 'update_database' script to connect to the test SQLite database to upgrade its # schema and run background updates on it. -server_name: "test" +server_name: "localhost:8800" signing_key_path: "/src/.buildkite/test.signing.key" diff --git a/scripts/synapse_port_db b/scripts/synapse_port_db index 72061177c9..e393a9b2f7 100755 --- a/scripts/synapse_port_db +++ b/scripts/synapse_port_db @@ -55,6 +55,7 @@ from synapse.storage.data_stores.main.stats import StatsStore from synapse.storage.data_stores.main.user_directory import ( UserDirectoryBackgroundUpdateStore, ) +from synapse.storage.database import Database from synapse.storage.engines import create_engine from synapse.storage.prepare_database import prepare_database from synapse.util import Clock @@ -139,39 +140,6 @@ class Store( UserDirectoryBackgroundUpdateStore, StatsStore, ): - def __init__(self, db_conn, hs): - super().__init__(db_conn, hs) - self.db_pool = hs.get_db_pool() - - @defer.inlineCallbacks - def runInteraction(self, desc, func, *args, **kwargs): - def r(conn): - try: - i = 0 - N = 5 - while True: - try: - txn = conn.cursor() - return func( - LoggingTransaction(txn, desc, self.database_engine, [], []), - *args, - **kwargs - ) - except self.database_engine.module.DatabaseError as e: - if self.database_engine.is_deadlock(e): - logger.warning("[TXN DEADLOCK] {%s} %d/%d", desc, i, N) - if i < N: - i += 1 - conn.rollback() - continue - raise - except Exception as e: - logger.debug("[TXN FAIL] {%s} %s", desc, e) - raise - - with PreserveLoggingContext(): - return (yield self.db_pool.runWithConnection(r)) - def execute(self, f, *args, **kwargs): return self.db.runInteraction(f.__name__, f, *args, **kwargs) @@ -512,7 +480,7 @@ class Porter(object): hs = MockHomeserver(self.hs_config, engine, conn, db_pool) - store = Store(conn, hs) + store = Store(Database(hs), conn, hs) yield store.db.runInteraction( "%s_engine.check_database" % config["name"], engine.check_database, diff --git a/synapse/storage/data_stores/main/__init__.py b/synapse/storage/data_stores/main/__init__.py index 66f8a9f3a7..c577c0df5f 100644 --- a/synapse/storage/data_stores/main/__init__.py +++ b/synapse/storage/data_stores/main/__init__.py @@ -124,7 +124,7 @@ class DataStore( raise Exception( "Found users in database not native to %s!\n" "You cannot changed a synapse server_name after it's been configured" - % (self.hostname,) + % (hs.hostname,) ) self._stream_id_gen = StreamIdGenerator( From e519489fc43865a0a01e2295782389e322ba5100 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 9 Dec 2019 11:37:26 +0000 Subject: [PATCH 0623/1623] Remove fallback for missing /federation/v1/state_ids API (#6488) This API was added way back in 0.17.0; the code here is annoying to maintain and entirely redundant. --- changelog.d/6488.removal | 1 + synapse/federation/federation_client.py | 89 +++++-------------------- synapse/federation/transport/client.py | 24 ------- 3 files changed, 18 insertions(+), 96 deletions(-) create mode 100644 changelog.d/6488.removal diff --git a/changelog.d/6488.removal b/changelog.d/6488.removal new file mode 100644 index 0000000000..06e034a213 --- /dev/null +++ b/changelog.d/6488.removal @@ -0,0 +1 @@ +Remove fallback for federation with old servers which lack the /federation/v1/state_ids API. diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index 27f6aff004..709449c9e3 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -324,87 +324,32 @@ class FederationClient(FederationBase): A list of events in the state, and a list of events in the auth chain for the given event. """ - try: - # First we try and ask for just the IDs, as thats far quicker if - # we have most of the state and auth_chain already. - # However, this may 404 if the other side has an old synapse. - result = yield self.transport_layer.get_room_state_ids( - destination, room_id, event_id=event_id - ) - - state_event_ids = result["pdu_ids"] - auth_event_ids = result.get("auth_chain_ids", []) - - fetched_events, failed_to_fetch = yield self.get_events_from_store_or_dest( - destination, room_id, set(state_event_ids + auth_event_ids) - ) - - if failed_to_fetch: - logger.warning( - "Failed to fetch missing state/auth events for %s: %s", - room_id, - failed_to_fetch, - ) - - event_map = {ev.event_id: ev for ev in fetched_events} - - pdus = [event_map[e_id] for e_id in state_event_ids if e_id in event_map] - auth_chain = [ - event_map[e_id] for e_id in auth_event_ids if e_id in event_map - ] - - auth_chain.sort(key=lambda e: e.depth) - - return pdus, auth_chain - except HttpResponseException as e: - if e.code == 400 or e.code == 404: - logger.info("Failed to use get_room_state_ids API, falling back") - else: - raise e - - result = yield self.transport_layer.get_room_state( + result = yield self.transport_layer.get_room_state_ids( destination, room_id, event_id=event_id ) - room_version = yield self.store.get_room_version(room_id) - format_ver = room_version_to_event_format(room_version) + state_event_ids = result["pdu_ids"] + auth_event_ids = result.get("auth_chain_ids", []) - pdus = [ - event_from_pdu_json(p, format_ver, outlier=True) for p in result["pdus"] - ] - - auth_chain = [ - event_from_pdu_json(p, format_ver, outlier=True) - for p in result.get("auth_chain", []) - ] - - seen_events = yield self.store.get_events( - [ev.event_id for ev in itertools.chain(pdus, auth_chain)] + fetched_events, failed_to_fetch = yield self.get_events_from_store_or_dest( + destination, room_id, set(state_event_ids + auth_event_ids) ) - signed_pdus = yield self._check_sigs_and_hash_and_fetch( - destination, - [p for p in pdus if p.event_id not in seen_events], - outlier=True, - room_version=room_version, - ) - signed_pdus.extend( - seen_events[p.event_id] for p in pdus if p.event_id in seen_events - ) + if failed_to_fetch: + logger.warning( + "Failed to fetch missing state/auth events for %s: %s", + room_id, + failed_to_fetch, + ) - signed_auth = yield self._check_sigs_and_hash_and_fetch( - destination, - [p for p in auth_chain if p.event_id not in seen_events], - outlier=True, - room_version=room_version, - ) - signed_auth.extend( - seen_events[p.event_id] for p in auth_chain if p.event_id in seen_events - ) + event_map = {ev.event_id: ev for ev in fetched_events} - signed_auth.sort(key=lambda e: e.depth) + pdus = [event_map[e_id] for e_id in state_event_ids if e_id in event_map] + auth_chain = [event_map[e_id] for e_id in auth_event_ids if e_id in event_map] - return signed_pdus, signed_auth + auth_chain.sort(key=lambda e: e.depth) + + return pdus, auth_chain @defer.inlineCallbacks def get_events_from_store_or_dest(self, destination, room_id, event_ids): diff --git a/synapse/federation/transport/client.py b/synapse/federation/transport/client.py index dc95ab2113..46dba84cac 100644 --- a/synapse/federation/transport/client.py +++ b/synapse/federation/transport/client.py @@ -38,30 +38,6 @@ class TransportLayerClient(object): self.server_name = hs.hostname self.client = hs.get_http_client() - @log_function - def get_room_state(self, destination, room_id, event_id): - """ Requests all state for a given room from the given server at the - given event. - - Args: - destination (str): The host name of the remote homeserver we want - to get the state from. - context (str): The name of the context we want the state of - event_id (str): The event we want the context at. - - Returns: - Deferred: Results in a dict received from the remote homeserver. - """ - logger.debug("get_room_state dest=%s, room=%s", destination, room_id) - - path = _create_v1_path("/state/%s", room_id) - return self.client.get_json( - destination, - path=path, - args={"event_id": event_id}, - try_trailing_slash_on_400=True, - ) - @log_function def get_room_state_ids(self, destination, room_id, event_id): """ Requests all state for a given room from the given server at the From 8ad8bcbed0bbd8a00ddfbe693b99785b72bb8ee2 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 9 Dec 2019 11:50:34 +0000 Subject: [PATCH 0624/1623] Pull out room_invite_state_types config option once. Pulling things out of config is currently surprisingly expensive. --- synapse/handlers/message.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index 4f53a5f5dc..54fa216d83 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -363,6 +363,8 @@ class EventCreationHandler(object): self.config = hs.config self.require_membership_for_aliases = hs.config.require_membership_for_aliases + self.room_invite_state_types = self.hs.config.room_invite_state_types + self.send_event_to_master = ReplicationSendEventRestServlet.make_client(hs) # This is only used to get at ratelimit function, and maybe_kick_guest_users @@ -916,7 +918,7 @@ class EventCreationHandler(object): state_to_include_ids = [ e_id for k, e_id in iteritems(current_state_ids) - if k[0] in self.hs.config.room_invite_state_types + if k[0] in self.room_invite_state_types or k == (EventTypes.Member, event.sender) ] From 4a161a29aca6d73f7f226ca147e4e46e70e29d5b Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 9 Dec 2019 11:54:43 +0000 Subject: [PATCH 0625/1623] Newsfile --- changelog.d/6493.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6493.bugfix diff --git a/changelog.d/6493.bugfix b/changelog.d/6493.bugfix new file mode 100644 index 0000000000..440c02efbe --- /dev/null +++ b/changelog.d/6493.bugfix @@ -0,0 +1 @@ +Fix small performance regression for sending invites. From 18660a34d82ccb120efd2fa2480b42ac62dbe2b4 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 9 Dec 2019 11:55:30 +0000 Subject: [PATCH 0626/1623] Fix inaccurate per-block metrics (#6491) `Measure` incorrectly assumed that it was the only thing being done by the parent `LoggingContext`. For instance, during a "renew group attestations" operation, hundreds of `outbound_request` calls could take place in parallel, all using the same `LoggingContext`. This would mean that any resources used during *any* of those calls would be reported against *all* of them, producing wildly inaccurate results. Instead, we now give each `Measure` block its own `LoggingContext` (using the parent `LoggingContext` mechanism to ensure that the log lines look correct and that the metrics are ultimately propogated to the top level for reporting against requests/backgrond tasks). --- changelog.d/6491.bugfix | 1 + synapse/util/metrics.py | 60 +++++++++++++---------------------------- 2 files changed, 19 insertions(+), 42 deletions(-) create mode 100644 changelog.d/6491.bugfix diff --git a/changelog.d/6491.bugfix b/changelog.d/6491.bugfix new file mode 100644 index 0000000000..78204693b0 --- /dev/null +++ b/changelog.d/6491.bugfix @@ -0,0 +1 @@ +Fix inaccurate per-block Prometheus metrics. diff --git a/synapse/util/metrics.py b/synapse/util/metrics.py index 63ddaaba87..7b18455469 100644 --- a/synapse/util/metrics.py +++ b/synapse/util/metrics.py @@ -91,72 +91,48 @@ class Measure(object): __slots__ = [ "clock", "name", - "start_context", + "_logging_context", "start", - "created_context", - "start_usage", ] def __init__(self, clock, name): self.clock = clock self.name = name - self.start_context = None + self._logging_context = None self.start = None - self.created_context = False def __enter__(self): + if self._logging_context: + raise RuntimeError("Measure() objects cannot be re-used") + self.start = self.clock.time() - self.start_context = LoggingContext.current_context() - if not self.start_context: - self.start_context = LoggingContext("Measure") - self.start_context.__enter__() - self.created_context = True - - self.start_usage = self.start_context.get_resource_usage() - + parent_context = LoggingContext.current_context() + self._logging_context = LoggingContext( + "Measure[%s]" % (self.name,), parent_context + ) + self._logging_context.__enter__() in_flight.register((self.name,), self._update_in_flight) def __exit__(self, exc_type, exc_val, exc_tb): - if isinstance(exc_type, Exception) or not self.start_context: - return - - in_flight.unregister((self.name,), self._update_in_flight) + if not self._logging_context: + raise RuntimeError("Measure() block exited without being entered") duration = self.clock.time() - self.start + usage = self._logging_context.get_resource_usage() - block_counter.labels(self.name).inc() - block_timer.labels(self.name).inc(duration) + in_flight.unregister((self.name,), self._update_in_flight) + self._logging_context.__exit__(exc_type, exc_val, exc_tb) - context = LoggingContext.current_context() - - if context != self.start_context: - logger.warning( - "Context has unexpectedly changed from '%s' to '%s'. (%r)", - self.start_context, - context, - self.name, - ) - return - - if not context: - logger.warning("Expected context. (%r)", self.name) - return - - current = context.get_resource_usage() - usage = current - self.start_usage try: + block_counter.labels(self.name).inc() + block_timer.labels(self.name).inc(duration) block_ru_utime.labels(self.name).inc(usage.ru_utime) block_ru_stime.labels(self.name).inc(usage.ru_stime) block_db_txn_count.labels(self.name).inc(usage.db_txn_count) block_db_txn_duration.labels(self.name).inc(usage.db_txn_duration_sec) block_db_sched_duration.labels(self.name).inc(usage.db_sched_duration_sec) except ValueError: - logger.warning( - "Failed to save metrics! OLD: %r, NEW: %r", self.start_usage, current - ) - - if self.created_context: - self.start_context.__exit__(exc_type, exc_val, exc_tb) + logger.warning("Failed to save metrics! Usage: %s", usage) def _update_in_flight(self, metrics): """Gets called when processing in flight metrics From 65b37f672927d0b88401d97a9f27f506eec0ca6d Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 9 Dec 2019 11:47:55 +0000 Subject: [PATCH 0627/1623] Fix comment Co-Authored-By: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> --- synapse/storage/data_stores/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/storage/data_stores/__init__.py b/synapse/storage/data_stores/__init__.py index cecc04f03f..cafedd5c0d 100644 --- a/synapse/storage/data_stores/__init__.py +++ b/synapse/storage/data_stores/__init__.py @@ -24,7 +24,7 @@ class DataStores(object): """ def __init__(self, main_store_class, db_conn, hs): - # Note we pass in the main store here as workers use a different main + # Note we pass in the main store class here as workers use a different main # store. database = Database(hs) From f166a8d1f52f1fda1a64d8cd50f17275d63c8843 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 9 Dec 2019 13:42:49 +0000 Subject: [PATCH 0628/1623] Remove SnapshotCache in favour of ResponseCache --- synapse/handlers/initial_sync.py | 19 +++--- synapse/util/caches/snapshot_cache.py | 94 --------------------------- tests/util/test_snapshot_cache.py | 63 ------------------ 3 files changed, 8 insertions(+), 168 deletions(-) delete mode 100644 synapse/util/caches/snapshot_cache.py delete mode 100644 tests/util/test_snapshot_cache.py diff --git a/synapse/handlers/initial_sync.py b/synapse/handlers/initial_sync.py index 81dce96f4b..73c110a92b 100644 --- a/synapse/handlers/initial_sync.py +++ b/synapse/handlers/initial_sync.py @@ -26,7 +26,7 @@ from synapse.streams.config import PaginationConfig from synapse.types import StreamToken, UserID from synapse.util import unwrapFirstError from synapse.util.async_helpers import concurrently_execute -from synapse.util.caches.snapshot_cache import SnapshotCache +from synapse.util.caches.response_cache import ResponseCache from synapse.visibility import filter_events_for_client from ._base import BaseHandler @@ -41,7 +41,7 @@ class InitialSyncHandler(BaseHandler): self.state = hs.get_state_handler() self.clock = hs.get_clock() self.validator = EventValidator() - self.snapshot_cache = SnapshotCache() + self.snapshot_cache = ResponseCache(hs, "initial_sync_cache") self._event_serializer = hs.get_event_client_serializer() self.storage = hs.get_storage() self.state_store = self.storage.state @@ -79,17 +79,14 @@ class InitialSyncHandler(BaseHandler): as_client_event, include_archived, ) - now_ms = self.clock.time_msec() - result = self.snapshot_cache.get(now_ms, key) - if result is not None: - return result - return self.snapshot_cache.set( - now_ms, + return self.snapshot_cache.wrap( key, - self._snapshot_all_rooms( - user_id, pagin_config, as_client_event, include_archived - ), + self._snapshot_all_rooms, + user_id, + pagin_config, + as_client_event, + include_archived, ) @defer.inlineCallbacks diff --git a/synapse/util/caches/snapshot_cache.py b/synapse/util/caches/snapshot_cache.py deleted file mode 100644 index 8318db8d2c..0000000000 --- a/synapse/util/caches/snapshot_cache.py +++ /dev/null @@ -1,94 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2015, 2016 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from synapse.util.async_helpers import ObservableDeferred - - -class SnapshotCache(object): - """Cache for snapshots like the response of /initialSync. - The response of initialSync only has to be a recent snapshot of the - server state. It shouldn't matter to clients if it is a few minutes out - of date. - - This caches a deferred response. Until the deferred completes it will be - returned from the cache. This means that if the client retries the request - while the response is still being computed, that original response will be - used rather than trying to compute a new response. - - Once the deferred completes it will removed from the cache after 5 minutes. - We delay removing it from the cache because a client retrying its request - could race with us finishing computing the response. - - Rather than tracking precisely how long something has been in the cache we - keep two generations of completed responses. Every 5 minutes discard the - old generation, move the new generation to the old generation, and set the - new generation to be empty. This means that a result will be in the cache - somewhere between 5 and 10 minutes. - """ - - DURATION_MS = 5 * 60 * 1000 # Cache results for 5 minutes. - - def __init__(self): - self.pending_result_cache = {} # Request that haven't finished yet. - self.prev_result_cache = {} # The older requests that have finished. - self.next_result_cache = {} # The newer requests that have finished. - self.time_last_rotated_ms = 0 - - def rotate(self, time_now_ms): - # Rotate once if the cache duration has passed since the last rotation. - if time_now_ms - self.time_last_rotated_ms >= self.DURATION_MS: - self.prev_result_cache = self.next_result_cache - self.next_result_cache = {} - self.time_last_rotated_ms += self.DURATION_MS - - # Rotate again if the cache duration has passed twice since the last - # rotation. - if time_now_ms - self.time_last_rotated_ms >= self.DURATION_MS: - self.prev_result_cache = self.next_result_cache - self.next_result_cache = {} - self.time_last_rotated_ms = time_now_ms - - def get(self, time_now_ms, key): - self.rotate(time_now_ms) - # This cache is intended to deduplicate requests, so we expect it to be - # missed most of the time. So we just lookup the key in all of the - # dictionaries rather than trying to short circuit the lookup if the - # key is found. - result = self.prev_result_cache.get(key) - result = self.next_result_cache.get(key, result) - result = self.pending_result_cache.get(key, result) - if result is not None: - return result.observe() - else: - return None - - def set(self, time_now_ms, key, deferred): - self.rotate(time_now_ms) - - result = ObservableDeferred(deferred) - - self.pending_result_cache[key] = result - - def shuffle_along(r): - # When the deferred completes we shuffle it along to the first - # generation of the result cache. So that it will eventually - # expire from the rotation of that cache. - self.next_result_cache[key] = result - self.pending_result_cache.pop(key, None) - return r - - result.addBoth(shuffle_along) - - return result.observe() diff --git a/tests/util/test_snapshot_cache.py b/tests/util/test_snapshot_cache.py deleted file mode 100644 index 1a44f72425..0000000000 --- a/tests/util/test_snapshot_cache.py +++ /dev/null @@ -1,63 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2015, 2016 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from twisted.internet.defer import Deferred - -from synapse.util.caches.snapshot_cache import SnapshotCache - -from .. import unittest - - -class SnapshotCacheTestCase(unittest.TestCase): - def setUp(self): - self.cache = SnapshotCache() - self.cache.DURATION_MS = 1 - - def test_get_set(self): - # Check that getting a missing key returns None - self.assertEquals(self.cache.get(0, "key"), None) - - # Check that setting a key with a deferred returns - # a deferred that resolves when the initial deferred does - d = Deferred() - set_result = self.cache.set(0, "key", d) - self.assertIsNotNone(set_result) - self.assertFalse(set_result.called) - - # Check that getting the key before the deferred has resolved - # returns a deferred that resolves when the initial deferred does. - get_result_at_10 = self.cache.get(10, "key") - self.assertIsNotNone(get_result_at_10) - self.assertFalse(get_result_at_10.called) - - # Check that the returned deferreds resolve when the initial deferred - # does. - d.callback("v") - self.assertTrue(set_result.called) - self.assertTrue(get_result_at_10.called) - - # Check that getting the key after the deferred has resolved - # before the cache expires returns a resolved deferred. - get_result_at_11 = self.cache.get(11, "key") - self.assertIsNotNone(get_result_at_11) - if isinstance(get_result_at_11, Deferred): - # The cache may return the actual result rather than a deferred - self.assertTrue(get_result_at_11.called) - - # Check that getting the key after the deferred has resolved - # after the cache expires returns None - get_result_at_12 = self.cache.get(12, "key") - self.assertIsNone(get_result_at_12) From a1f8ea9051e7e7c34ed1677871585cc68e3d0311 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 9 Dec 2019 13:46:45 +0000 Subject: [PATCH 0629/1623] Port synapse.handlers.initial_sync to async/await --- synapse/handlers/initial_sync.py | 96 +++++++++++++++----------------- 1 file changed, 44 insertions(+), 52 deletions(-) diff --git a/synapse/handlers/initial_sync.py b/synapse/handlers/initial_sync.py index 73c110a92b..44ec3e66ae 100644 --- a/synapse/handlers/initial_sync.py +++ b/synapse/handlers/initial_sync.py @@ -89,8 +89,7 @@ class InitialSyncHandler(BaseHandler): include_archived, ) - @defer.inlineCallbacks - def _snapshot_all_rooms( + async def _snapshot_all_rooms( self, user_id=None, pagin_config=None, @@ -102,7 +101,7 @@ class InitialSyncHandler(BaseHandler): if include_archived: memberships.append(Membership.LEAVE) - room_list = yield self.store.get_rooms_for_user_where_membership_is( + room_list = await self.store.get_rooms_for_user_where_membership_is( user_id=user_id, membership_list=memberships ) @@ -110,33 +109,32 @@ class InitialSyncHandler(BaseHandler): rooms_ret = [] - now_token = yield self.hs.get_event_sources().get_current_token() + now_token = await self.hs.get_event_sources().get_current_token() presence_stream = self.hs.get_event_sources().sources["presence"] pagination_config = PaginationConfig(from_token=now_token) - presence, _ = yield presence_stream.get_pagination_rows( + presence, _ = await presence_stream.get_pagination_rows( user, pagination_config.get_source_config("presence"), None ) receipt_stream = self.hs.get_event_sources().sources["receipt"] - receipt, _ = yield receipt_stream.get_pagination_rows( + receipt, _ = await receipt_stream.get_pagination_rows( user, pagination_config.get_source_config("receipt"), None ) - tags_by_room = yield self.store.get_tags_for_user(user_id) + tags_by_room = await self.store.get_tags_for_user(user_id) - account_data, account_data_by_room = yield self.store.get_account_data_for_user( + account_data, account_data_by_room = await self.store.get_account_data_for_user( user_id ) - public_room_ids = yield self.store.get_public_room_ids() + public_room_ids = await self.store.get_public_room_ids() limit = pagin_config.limit if limit is None: limit = 10 - @defer.inlineCallbacks - def handle_room(event): + async def handle_room(event): d = { "room_id": event.room_id, "membership": event.membership, @@ -149,8 +147,8 @@ class InitialSyncHandler(BaseHandler): time_now = self.clock.time_msec() d["inviter"] = event.sender - invite_event = yield self.store.get_event(event.event_id) - d["invite"] = yield self._event_serializer.serialize_event( + invite_event = await self.store.get_event(event.event_id) + d["invite"] = await self._event_serializer.serialize_event( invite_event, time_now, as_client_event ) @@ -174,7 +172,7 @@ class InitialSyncHandler(BaseHandler): lambda states: states[event.event_id] ) - (messages, token), current_state = yield make_deferred_yieldable( + (messages, token), current_state = await make_deferred_yieldable( defer.gatherResults( [ run_in_background( @@ -188,7 +186,7 @@ class InitialSyncHandler(BaseHandler): ) ).addErrback(unwrapFirstError) - messages = yield filter_events_for_client( + messages = await filter_events_for_client( self.storage, user_id, messages ) @@ -198,7 +196,7 @@ class InitialSyncHandler(BaseHandler): d["messages"] = { "chunk": ( - yield self._event_serializer.serialize_events( + await self._event_serializer.serialize_events( messages, time_now=time_now, as_client_event=as_client_event ) ), @@ -206,7 +204,7 @@ class InitialSyncHandler(BaseHandler): "end": end_token.to_string(), } - d["state"] = yield self._event_serializer.serialize_events( + d["state"] = await self._event_serializer.serialize_events( current_state.values(), time_now=time_now, as_client_event=as_client_event, @@ -229,7 +227,7 @@ class InitialSyncHandler(BaseHandler): except Exception: logger.exception("Failed to get snapshot") - yield concurrently_execute(handle_room, room_list, 10) + await concurrently_execute(handle_room, room_list, 10) account_data_events = [] for account_data_type, content in account_data.items(): @@ -253,8 +251,7 @@ class InitialSyncHandler(BaseHandler): return ret - @defer.inlineCallbacks - def room_initial_sync(self, requester, room_id, pagin_config=None): + async def room_initial_sync(self, requester, room_id, pagin_config=None): """Capture the a snapshot of a room. If user is currently a member of the room this will be what is currently in the room. If the user left the room this will be what was in the room when they left. @@ -271,32 +268,32 @@ class InitialSyncHandler(BaseHandler): A JSON serialisable dict with the snapshot of the room. """ - blocked = yield self.store.is_room_blocked(room_id) + blocked = await self.store.is_room_blocked(room_id) if blocked: raise SynapseError(403, "This room has been blocked on this server") user_id = requester.user.to_string() - membership, member_event_id = yield self._check_in_room_or_world_readable( + membership, member_event_id = await self._check_in_room_or_world_readable( room_id, user_id ) is_peeking = member_event_id is None if membership == Membership.JOIN: - result = yield self._room_initial_sync_joined( + result = await self._room_initial_sync_joined( user_id, room_id, pagin_config, membership, is_peeking ) elif membership == Membership.LEAVE: - result = yield self._room_initial_sync_parted( + result = await self._room_initial_sync_parted( user_id, room_id, pagin_config, membership, member_event_id, is_peeking ) account_data_events = [] - tags = yield self.store.get_tags_for_room(user_id, room_id) + tags = await self.store.get_tags_for_room(user_id, room_id) if tags: account_data_events.append({"type": "m.tag", "content": {"tags": tags}}) - account_data = yield self.store.get_account_data_for_room(user_id, room_id) + account_data = await self.store.get_account_data_for_room(user_id, room_id) for account_data_type, content in account_data.items(): account_data_events.append({"type": account_data_type, "content": content}) @@ -304,11 +301,10 @@ class InitialSyncHandler(BaseHandler): return result - @defer.inlineCallbacks - def _room_initial_sync_parted( + async def _room_initial_sync_parted( self, user_id, room_id, pagin_config, membership, member_event_id, is_peeking ): - room_state = yield self.state_store.get_state_for_events([member_event_id]) + room_state = await self.state_store.get_state_for_events([member_event_id]) room_state = room_state[member_event_id] @@ -316,13 +312,13 @@ class InitialSyncHandler(BaseHandler): if limit is None: limit = 10 - stream_token = yield self.store.get_stream_token_for_event(member_event_id) + stream_token = await self.store.get_stream_token_for_event(member_event_id) - messages, token = yield self.store.get_recent_events_for_room( + messages, token = await self.store.get_recent_events_for_room( room_id, limit=limit, end_token=stream_token ) - messages = yield filter_events_for_client( + messages = await filter_events_for_client( self.storage, user_id, messages, is_peeking=is_peeking ) @@ -336,13 +332,13 @@ class InitialSyncHandler(BaseHandler): "room_id": room_id, "messages": { "chunk": ( - yield self._event_serializer.serialize_events(messages, time_now) + await self._event_serializer.serialize_events(messages, time_now) ), "start": start_token.to_string(), "end": end_token.to_string(), }, "state": ( - yield self._event_serializer.serialize_events( + await self._event_serializer.serialize_events( room_state.values(), time_now ) ), @@ -350,19 +346,18 @@ class InitialSyncHandler(BaseHandler): "receipts": [], } - @defer.inlineCallbacks - def _room_initial_sync_joined( + async def _room_initial_sync_joined( self, user_id, room_id, pagin_config, membership, is_peeking ): - current_state = yield self.state.get_current_state(room_id=room_id) + current_state = await self.state.get_current_state(room_id=room_id) # TODO: These concurrently time_now = self.clock.time_msec() - state = yield self._event_serializer.serialize_events( + state = await self._event_serializer.serialize_events( current_state.values(), time_now ) - now_token = yield self.hs.get_event_sources().get_current_token() + now_token = await self.hs.get_event_sources().get_current_token() limit = pagin_config.limit if pagin_config else None if limit is None: @@ -377,28 +372,26 @@ class InitialSyncHandler(BaseHandler): presence_handler = self.hs.get_presence_handler() - @defer.inlineCallbacks - def get_presence(): + async def get_presence(): # If presence is disabled, return an empty list if not self.hs.config.use_presence: return [] - states = yield presence_handler.get_states( + states = await presence_handler.get_states( [m.user_id for m in room_members], as_event=True ) return states - @defer.inlineCallbacks - def get_receipts(): - receipts = yield self.store.get_linearized_receipts_for_room( + async def get_receipts(): + receipts = await self.store.get_linearized_receipts_for_room( room_id, to_key=now_token.receipt_key ) if not receipts: receipts = [] return receipts - presence, receipts, (messages, token) = yield make_deferred_yieldable( + presence, receipts, (messages, token) = await make_deferred_yieldable( defer.gatherResults( [ run_in_background(get_presence), @@ -414,7 +407,7 @@ class InitialSyncHandler(BaseHandler): ).addErrback(unwrapFirstError) ) - messages = yield filter_events_for_client( + messages = await filter_events_for_client( self.storage, user_id, messages, is_peeking=is_peeking ) @@ -427,7 +420,7 @@ class InitialSyncHandler(BaseHandler): "room_id": room_id, "messages": { "chunk": ( - yield self._event_serializer.serialize_events(messages, time_now) + await self._event_serializer.serialize_events(messages, time_now) ), "start": start_token.to_string(), "end": end_token.to_string(), @@ -441,18 +434,17 @@ class InitialSyncHandler(BaseHandler): return ret - @defer.inlineCallbacks - def _check_in_room_or_world_readable(self, room_id, user_id): + async def _check_in_room_or_world_readable(self, room_id, user_id): try: # check_user_was_in_room will return the most recent membership # event for the user if: # * The user is a non-guest user, and was ever in the room # * The user is a guest user, and has joined the room # else it will throw. - member_event = yield self.auth.check_user_was_in_room(room_id, user_id) + member_event = await self.auth.check_user_was_in_room(room_id, user_id) return member_event.membership, member_event.event_id except AuthError: - visibility = yield self.state_handler.get_current_state( + visibility = await self.state_handler.get_current_state( room_id, EventTypes.RoomHistoryVisibility, "" ) if ( From aeaeb72ee41076b0f08b07ea4686d1c38acf2d6b Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 9 Dec 2019 13:48:14 +0000 Subject: [PATCH 0630/1623] Newsfile --- changelog.d/6496.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6496.misc diff --git a/changelog.d/6496.misc b/changelog.d/6496.misc new file mode 100644 index 0000000000..19c6e926b8 --- /dev/null +++ b/changelog.d/6496.misc @@ -0,0 +1 @@ +Port synapse.handlers.initial_sync to async/await. From adfdd82b21ae296ed77453b2f51d55414890f162 Mon Sep 17 00:00:00 2001 From: Neil Johnson Date: Mon, 9 Dec 2019 13:59:27 +0000 Subject: [PATCH 0631/1623] Back out perf regression from get_cross_signing_keys_from_cache. (#6494) Back out cross-signing code added in Synapse 1.5.0, which caused a performance regression. --- changelog.d/6494.bugfix | 1 + synapse/handlers/e2e_keys.py | 38 +++++++-------------------------- sytest-blacklist | 3 +++ tests/handlers/test_e2e_keys.py | 8 +++++++ 4 files changed, 20 insertions(+), 30 deletions(-) create mode 100644 changelog.d/6494.bugfix diff --git a/changelog.d/6494.bugfix b/changelog.d/6494.bugfix new file mode 100644 index 0000000000..78726d5d7f --- /dev/null +++ b/changelog.d/6494.bugfix @@ -0,0 +1 @@ +Back out cross-signing code added in Synapse 1.5.0, which caused a performance regression. diff --git a/synapse/handlers/e2e_keys.py b/synapse/handlers/e2e_keys.py index 28c12753c1..57a10daefd 100644 --- a/synapse/handlers/e2e_keys.py +++ b/synapse/handlers/e2e_keys.py @@ -264,7 +264,6 @@ class E2eKeysHandler(object): return ret - @defer.inlineCallbacks def get_cross_signing_keys_from_cache(self, query, from_user_id): """Get cross-signing keys for users from the database @@ -284,35 +283,14 @@ class E2eKeysHandler(object): self_signing_keys = {} user_signing_keys = {} - for user_id in query: - # XXX: consider changing the store functions to allow querying - # multiple users simultaneously. - key = yield self.store.get_e2e_cross_signing_key( - user_id, "master", from_user_id - ) - if key: - master_keys[user_id] = key - - key = yield self.store.get_e2e_cross_signing_key( - user_id, "self_signing", from_user_id - ) - if key: - self_signing_keys[user_id] = key - - # users can see other users' master and self-signing keys, but can - # only see their own user-signing keys - if from_user_id == user_id: - key = yield self.store.get_e2e_cross_signing_key( - user_id, "user_signing", from_user_id - ) - if key: - user_signing_keys[user_id] = key - - return { - "master_keys": master_keys, - "self_signing_keys": self_signing_keys, - "user_signing_keys": user_signing_keys, - } + # Currently a stub, implementation coming in https://github.com/matrix-org/synapse/pull/6486 + return defer.succeed( + { + "master_keys": master_keys, + "self_signing_keys": self_signing_keys, + "user_signing_keys": user_signing_keys, + } + ) @trace @defer.inlineCallbacks diff --git a/sytest-blacklist b/sytest-blacklist index 411cce0692..79b2d4402a 100644 --- a/sytest-blacklist +++ b/sytest-blacklist @@ -33,3 +33,6 @@ New federated private chats get full presence information (SYN-115) # Blacklisted due to https://github.com/matrix-org/matrix-doc/pull/2314 removing # this requirement from the spec Inbound federation of state requires event_id as a mandatory paramater + +# Blacklisted until https://github.com/matrix-org/synapse/pull/6486 lands +Can upload self-signing keys diff --git a/tests/handlers/test_e2e_keys.py b/tests/handlers/test_e2e_keys.py index 854eb6c024..fdfa2cbbc4 100644 --- a/tests/handlers/test_e2e_keys.py +++ b/tests/handlers/test_e2e_keys.py @@ -183,6 +183,10 @@ class E2eKeysHandlerTestCase(unittest.TestCase): ) self.assertDictEqual(devices["master_keys"], {local_user: keys2["master_key"]}) + test_replace_master_key.skip = ( + "Disabled waiting on #https://github.com/matrix-org/synapse/pull/6486" + ) + @defer.inlineCallbacks def test_reupload_signatures(self): """re-uploading a signature should not fail""" @@ -503,3 +507,7 @@ class E2eKeysHandlerTestCase(unittest.TestCase): ], other_master_key["signatures"][local_user]["ed25519:" + usersigning_pubkey], ) + + test_upload_signatures.skip = ( + "Disabled waiting on #https://github.com/matrix-org/synapse/pull/6486" + ) From 96d35f1028833ae700f39bb12c6f77a7de2c30bf Mon Sep 17 00:00:00 2001 From: Clifford Garwood II Date: Mon, 9 Dec 2019 09:40:37 -0500 Subject: [PATCH 0632/1623] Systemd documentation (#6490) --- changelog.d/6490.doc | 1 + contrib/systemd/matrix-synapse.service | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/6490.doc diff --git a/changelog.d/6490.doc b/changelog.d/6490.doc new file mode 100644 index 0000000000..6db351d7db --- /dev/null +++ b/changelog.d/6490.doc @@ -0,0 +1 @@ +Update documentation and variables in user contributed systemd reference file. diff --git a/contrib/systemd/matrix-synapse.service b/contrib/systemd/matrix-synapse.service index bd492544b6..813717b032 100644 --- a/contrib/systemd/matrix-synapse.service +++ b/contrib/systemd/matrix-synapse.service @@ -25,7 +25,7 @@ Restart=on-abort User=synapse Group=nogroup -WorkingDirectory=/opt/synapse +WorkingDirectory=/home/synapse/synapse ExecStart=/home/synapse/synapse/env/bin/python -m synapse.app.homeserver --config-path=/home/synapse/synapse/homeserver.yaml SyslogIdentifier=matrix-synapse From 24da1ffcb615ecde30c413b73434688c0d5963b9 Mon Sep 17 00:00:00 2001 From: Neil Johnson Date: Mon, 9 Dec 2019 14:46:20 +0000 Subject: [PATCH 0633/1623] 1.7.0rc1 --- CHANGES.md | 78 ++++++++++++++++++++++++++++++++++++++++ changelog.d/5815.feature | 1 - changelog.d/5858.feature | 1 - changelog.d/5925.feature | 1 - changelog.d/5925.removal | 1 - changelog.d/6119.feature | 1 - changelog.d/6176.feature | 1 - changelog.d/6237.bugfix | 1 - changelog.d/6241.bugfix | 1 - changelog.d/6266.misc | 1 - changelog.d/6322.misc | 1 - changelog.d/6329.bugfix | 1 - changelog.d/6332.bugfix | 1 - changelog.d/6333.bugfix | 1 - changelog.d/6343.misc | 1 - changelog.d/6354.feature | 1 - changelog.d/6362.misc | 1 - changelog.d/6369.doc | 1 - changelog.d/6379.misc | 1 - changelog.d/6388.doc | 1 - changelog.d/6390.doc | 1 - changelog.d/6392.misc | 1 - changelog.d/6406.bugfix | 1 - changelog.d/6408.bugfix | 1 - changelog.d/6409.feature | 1 - changelog.d/6420.bugfix | 1 - changelog.d/6421.bugfix | 1 - changelog.d/6423.misc | 1 - changelog.d/6426.bugfix | 1 - changelog.d/6429.misc | 1 - changelog.d/6434.feature | 1 - changelog.d/6436.bugfix | 1 - changelog.d/6443.doc | 1 - changelog.d/6449.bugfix | 1 - changelog.d/6451.bugfix | 1 - changelog.d/6454.misc | 1 - changelog.d/6458.doc | 1 - changelog.d/6461.doc | 1 - changelog.d/6462.bugfix | 1 - changelog.d/6464.misc | 1 - changelog.d/6468.misc | 1 - changelog.d/6469.misc | 1 - changelog.d/6470.bugfix | 1 - changelog.d/6472.bugfix | 1 - changelog.d/6480.misc | 1 - changelog.d/6482.misc | 1 - changelog.d/6483.misc | 1 - changelog.d/6484.misc | 1 - changelog.d/6487.misc | 1 - changelog.d/6488.removal | 1 - changelog.d/6490.doc | 1 - changelog.d/6491.bugfix | 1 - changelog.d/6493.bugfix | 1 - changelog.d/6494.bugfix | 1 - synapse/__init__.py | 2 +- 55 files changed, 79 insertions(+), 54 deletions(-) delete mode 100644 changelog.d/5815.feature delete mode 100644 changelog.d/5858.feature delete mode 100644 changelog.d/5925.feature delete mode 100644 changelog.d/5925.removal delete mode 100644 changelog.d/6119.feature delete mode 100644 changelog.d/6176.feature delete mode 100644 changelog.d/6237.bugfix delete mode 100644 changelog.d/6241.bugfix delete mode 100644 changelog.d/6266.misc delete mode 100644 changelog.d/6322.misc delete mode 100644 changelog.d/6329.bugfix delete mode 100644 changelog.d/6332.bugfix delete mode 100644 changelog.d/6333.bugfix delete mode 100644 changelog.d/6343.misc delete mode 100644 changelog.d/6354.feature delete mode 100644 changelog.d/6362.misc delete mode 100644 changelog.d/6369.doc delete mode 100644 changelog.d/6379.misc delete mode 100644 changelog.d/6388.doc delete mode 100644 changelog.d/6390.doc delete mode 100644 changelog.d/6392.misc delete mode 100644 changelog.d/6406.bugfix delete mode 100644 changelog.d/6408.bugfix delete mode 100644 changelog.d/6409.feature delete mode 100644 changelog.d/6420.bugfix delete mode 100644 changelog.d/6421.bugfix delete mode 100644 changelog.d/6423.misc delete mode 100644 changelog.d/6426.bugfix delete mode 100644 changelog.d/6429.misc delete mode 100644 changelog.d/6434.feature delete mode 100644 changelog.d/6436.bugfix delete mode 100644 changelog.d/6443.doc delete mode 100644 changelog.d/6449.bugfix delete mode 100644 changelog.d/6451.bugfix delete mode 100644 changelog.d/6454.misc delete mode 100644 changelog.d/6458.doc delete mode 100644 changelog.d/6461.doc delete mode 100644 changelog.d/6462.bugfix delete mode 100644 changelog.d/6464.misc delete mode 100644 changelog.d/6468.misc delete mode 100644 changelog.d/6469.misc delete mode 100644 changelog.d/6470.bugfix delete mode 100644 changelog.d/6472.bugfix delete mode 100644 changelog.d/6480.misc delete mode 100644 changelog.d/6482.misc delete mode 100644 changelog.d/6483.misc delete mode 100644 changelog.d/6484.misc delete mode 100644 changelog.d/6487.misc delete mode 100644 changelog.d/6488.removal delete mode 100644 changelog.d/6490.doc delete mode 100644 changelog.d/6491.bugfix delete mode 100644 changelog.d/6493.bugfix delete mode 100644 changelog.d/6494.bugfix diff --git a/CHANGES.md b/CHANGES.md index a9afd36d2c..0ef9794aac 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,81 @@ +Synapse 1.7.0rc1 (2019-12-09) +============================= + +Features +-------- + +- Implement per-room message retention policies. ([\#5815](https://github.com/matrix-org/synapse/issues/5815)) +- Add etag and count fields to key backup endpoints to help clients guess if there are new keys. ([\#5858](https://github.com/matrix-org/synapse/issues/5858)) +- Add admin/v2/users endpoint with pagination. Contributed by Awesome Technologies Innovationslabor GmbH. ([\#5925](https://github.com/matrix-org/synapse/issues/5925)) +- Require User-Interactive Authentication for `/account/3pid/add`, meaning the user's password will be required to add a third-party ID to their account. ([\#6119](https://github.com/matrix-org/synapse/issues/6119)) +- Implement the `/_matrix/federation/unstable/net.atleastfornow/state/` API as drafted in MSC2314. ([\#6176](https://github.com/matrix-org/synapse/issues/6176)) +- Configure privacy preserving settings by default for the room directory. ([\#6354](https://github.com/matrix-org/synapse/issues/6354)) +- Add ephemeral messages support by partially implementing [MSC2228](https://github.com/matrix-org/matrix-doc/pull/2228). ([\#6409](https://github.com/matrix-org/synapse/issues/6409)) +- Add support for MSC 2367, which allows specifying a reason on all membership events. ([\#6434](https://github.com/matrix-org/synapse/issues/6434)) + + +Bugfixes +-------- + +- Transfer non-standard power levels on room upgrade. ([\#6237](https://github.com/matrix-org/synapse/issues/6237)) +- Fix error from the Pillow library when uploading RGBA images. ([\#6241](https://github.com/matrix-org/synapse/issues/6241)) +- Correctly apply the event filter to the `state`, `events_before` and `events_after` fields in the response to `/context` requests. ([\#6329](https://github.com/matrix-org/synapse/issues/6329)) +- Fix caching devices for remote users when using workers, so that we don't attempt to refetch (and potentially fail) each time a user requests devices. ([\#6332](https://github.com/matrix-org/synapse/issues/6332)) +- Prevent account data syncs getting lost across TCP replication. ([\#6333](https://github.com/matrix-org/synapse/issues/6333)) +- Fix bug: TypeError in `register_user()` while using LDAP auth module. ([\#6406](https://github.com/matrix-org/synapse/issues/6406)) +- Fix an intermittent exception when handling read-receipts. ([\#6408](https://github.com/matrix-org/synapse/issues/6408)) +- Fix broken guest registration when there are existing blocks of numeric user IDs. ([\#6420](https://github.com/matrix-org/synapse/issues/6420)) +- Fix startup error when http proxy is defined. ([\#6421](https://github.com/matrix-org/synapse/issues/6421)) +- Clean up local threepids from user on account deactivation. ([\#6426](https://github.com/matrix-org/synapse/issues/6426)) +- Fix a bug where a room could become unusable with a low retention policy and a low activity. ([\#6436](https://github.com/matrix-org/synapse/issues/6436)) +- Fix error when using synapse_port_db on a vanilla synapse db. ([\#6449](https://github.com/matrix-org/synapse/issues/6449)) +- Fix uploading multiple cross signing signatures for the same user. ([\#6451](https://github.com/matrix-org/synapse/issues/6451)) +- Fix bug which lead to exceptions being thrown in a loop when a cross-signed device is deleted. ([\#6462](https://github.com/matrix-org/synapse/issues/6462)) +- Fix `synapse_port_db` not exiting with a 0 code if something went wrong during the port process. ([\#6470](https://github.com/matrix-org/synapse/issues/6470)) +- Improve sanity-checking when receiving events over federation. ([\#6472](https://github.com/matrix-org/synapse/issues/6472)) +- Fix inaccurate per-block Prometheus metrics. ([\#6491](https://github.com/matrix-org/synapse/issues/6491)) +- Fix small performance regression for sending invites. ([\#6493](https://github.com/matrix-org/synapse/issues/6493)) +- Back out cross-signing code added in Synapse 1.5.0, which caused a performance regression. ([\#6494](https://github.com/matrix-org/synapse/issues/6494)) + + +Improved Documentation +---------------------- + +- Update documentation and variables in user contributed systemd reference file. ([\#6369](https://github.com/matrix-org/synapse/issues/6369), [\#6490](https://github.com/matrix-org/synapse/issues/6490)) +- Fix link in the user directory documentation. ([\#6388](https://github.com/matrix-org/synapse/issues/6388)) +- Add build instructions to the docker readme. ([\#6390](https://github.com/matrix-org/synapse/issues/6390)) +- Switch Ubuntu package install recommendation to use python3 packages in INSTALL.md. ([\#6443](https://github.com/matrix-org/synapse/issues/6443)) +- Write some docs for the quarantine_media api. ([\#6458](https://github.com/matrix-org/synapse/issues/6458)) +- Convert CONTRIBUTING.rst to markdown (among other small fixes). ([\#6461](https://github.com/matrix-org/synapse/issues/6461)) + + +Deprecations and Removals +------------------------- + +- Remove admin/v1/users_paginate endpoint. Contributed by Awesome Technologies Innovationslabor GmbH. ([\#5925](https://github.com/matrix-org/synapse/issues/5925)) +- Remove fallback for federation with old servers which lack the /federation/v1/state_ids API. ([\#6488](https://github.com/matrix-org/synapse/issues/6488)) + + +Internal Changes +---------------- + +- Add benchmarks for structured logging and improve output performance. ([\#6266](https://github.com/matrix-org/synapse/issues/6266)) +- Improve the performance of outputting structured logging. ([\#6322](https://github.com/matrix-org/synapse/issues/6322)) +- Refactor some code in the event authentication path for clarity. ([\#6343](https://github.com/matrix-org/synapse/issues/6343), [\#6468](https://github.com/matrix-org/synapse/issues/6468), [\#6480](https://github.com/matrix-org/synapse/issues/6480)) +- Clean up some unnecessary quotation marks around the codebase. ([\#6362](https://github.com/matrix-org/synapse/issues/6362)) +- Complain on startup instead of 500'ing during runtime when `public_baseurl` isn't set when necessary. ([\#6379](https://github.com/matrix-org/synapse/issues/6379)) +- Add a test scenario to make sure room history purges don't break `/messages` in the future. ([\#6392](https://github.com/matrix-org/synapse/issues/6392)) +- Clarifications for the email configuration settings. ([\#6423](https://github.com/matrix-org/synapse/issues/6423)) +- Add more tests to the blacklist when running in worker mode. ([\#6429](https://github.com/matrix-org/synapse/issues/6429)) +- Move data store specific code out of `SQLBaseStore`. ([\#6454](https://github.com/matrix-org/synapse/issues/6454)) +- Prepare SQLBaseStore functions being moved out of the stores. ([\#6464](https://github.com/matrix-org/synapse/issues/6464)) +- Move per database functionality out of the data stores and into a dedicated `Database` class. ([\#6469](https://github.com/matrix-org/synapse/issues/6469)) +- Port synapse.rest.client.v1 to async/await. ([\#6482](https://github.com/matrix-org/synapse/issues/6482)) +- Port synapse.rest.client.v2_alpha to async/await. ([\#6483](https://github.com/matrix-org/synapse/issues/6483)) +- Port SyncHandler to async/await. ([\#6484](https://github.com/matrix-org/synapse/issues/6484)) +- Pass in `Database` object to data stores. ([\#6487](https://github.com/matrix-org/synapse/issues/6487)) + + Synapse 1.6.1 (2019-11-28) ========================== diff --git a/changelog.d/5815.feature b/changelog.d/5815.feature deleted file mode 100644 index ca4df4e7f6..0000000000 --- a/changelog.d/5815.feature +++ /dev/null @@ -1 +0,0 @@ -Implement per-room message retention policies. diff --git a/changelog.d/5858.feature b/changelog.d/5858.feature deleted file mode 100644 index 55ee93051e..0000000000 --- a/changelog.d/5858.feature +++ /dev/null @@ -1 +0,0 @@ -Add etag and count fields to key backup endpoints to help clients guess if there are new keys. diff --git a/changelog.d/5925.feature b/changelog.d/5925.feature deleted file mode 100644 index 8025cc8231..0000000000 --- a/changelog.d/5925.feature +++ /dev/null @@ -1 +0,0 @@ -Add admin/v2/users endpoint with pagination. Contributed by Awesome Technologies Innovationslabor GmbH. diff --git a/changelog.d/5925.removal b/changelog.d/5925.removal deleted file mode 100644 index cbba2855cb..0000000000 --- a/changelog.d/5925.removal +++ /dev/null @@ -1 +0,0 @@ -Remove admin/v1/users_paginate endpoint. Contributed by Awesome Technologies Innovationslabor GmbH. diff --git a/changelog.d/6119.feature b/changelog.d/6119.feature deleted file mode 100644 index 1492e83c5a..0000000000 --- a/changelog.d/6119.feature +++ /dev/null @@ -1 +0,0 @@ -Require User-Interactive Authentication for `/account/3pid/add`, meaning the user's password will be required to add a third-party ID to their account. \ No newline at end of file diff --git a/changelog.d/6176.feature b/changelog.d/6176.feature deleted file mode 100644 index 3c66d689d4..0000000000 --- a/changelog.d/6176.feature +++ /dev/null @@ -1 +0,0 @@ -Implement the `/_matrix/federation/unstable/net.atleastfornow/state/` API as drafted in MSC2314. diff --git a/changelog.d/6237.bugfix b/changelog.d/6237.bugfix deleted file mode 100644 index 9285600b00..0000000000 --- a/changelog.d/6237.bugfix +++ /dev/null @@ -1 +0,0 @@ -Transfer non-standard power levels on room upgrade. \ No newline at end of file diff --git a/changelog.d/6241.bugfix b/changelog.d/6241.bugfix deleted file mode 100644 index 25109ca4a6..0000000000 --- a/changelog.d/6241.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix error from the Pillow library when uploading RGBA images. diff --git a/changelog.d/6266.misc b/changelog.d/6266.misc deleted file mode 100644 index 634e421a79..0000000000 --- a/changelog.d/6266.misc +++ /dev/null @@ -1 +0,0 @@ -Add benchmarks for structured logging and improve output performance. diff --git a/changelog.d/6322.misc b/changelog.d/6322.misc deleted file mode 100644 index 70ef36ca80..0000000000 --- a/changelog.d/6322.misc +++ /dev/null @@ -1 +0,0 @@ -Improve the performance of outputting structured logging. diff --git a/changelog.d/6329.bugfix b/changelog.d/6329.bugfix deleted file mode 100644 index e558d13b7d..0000000000 --- a/changelog.d/6329.bugfix +++ /dev/null @@ -1 +0,0 @@ -Correctly apply the event filter to the `state`, `events_before` and `events_after` fields in the response to `/context` requests. \ No newline at end of file diff --git a/changelog.d/6332.bugfix b/changelog.d/6332.bugfix deleted file mode 100644 index 67d5170ba0..0000000000 --- a/changelog.d/6332.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix caching devices for remote users when using workers, so that we don't attempt to refetch (and potentially fail) each time a user requests devices. diff --git a/changelog.d/6333.bugfix b/changelog.d/6333.bugfix deleted file mode 100644 index a25d6ef3cb..0000000000 --- a/changelog.d/6333.bugfix +++ /dev/null @@ -1 +0,0 @@ -Prevent account data syncs getting lost across TCP replication. \ No newline at end of file diff --git a/changelog.d/6343.misc b/changelog.d/6343.misc deleted file mode 100644 index d9a44389b9..0000000000 --- a/changelog.d/6343.misc +++ /dev/null @@ -1 +0,0 @@ -Refactor some code in the event authentication path for clarity. diff --git a/changelog.d/6354.feature b/changelog.d/6354.feature deleted file mode 100644 index fed9db884b..0000000000 --- a/changelog.d/6354.feature +++ /dev/null @@ -1 +0,0 @@ -Configure privacy preserving settings by default for the room directory. diff --git a/changelog.d/6362.misc b/changelog.d/6362.misc deleted file mode 100644 index b79a5bea99..0000000000 --- a/changelog.d/6362.misc +++ /dev/null @@ -1 +0,0 @@ -Clean up some unnecessary quotation marks around the codebase. \ No newline at end of file diff --git a/changelog.d/6369.doc b/changelog.d/6369.doc deleted file mode 100644 index 6db351d7db..0000000000 --- a/changelog.d/6369.doc +++ /dev/null @@ -1 +0,0 @@ -Update documentation and variables in user contributed systemd reference file. diff --git a/changelog.d/6379.misc b/changelog.d/6379.misc deleted file mode 100644 index 725c2e7d87..0000000000 --- a/changelog.d/6379.misc +++ /dev/null @@ -1 +0,0 @@ -Complain on startup instead of 500'ing during runtime when `public_baseurl` isn't set when necessary. \ No newline at end of file diff --git a/changelog.d/6388.doc b/changelog.d/6388.doc deleted file mode 100644 index c777cb6b8f..0000000000 --- a/changelog.d/6388.doc +++ /dev/null @@ -1 +0,0 @@ -Fix link in the user directory documentation. diff --git a/changelog.d/6390.doc b/changelog.d/6390.doc deleted file mode 100644 index 093411bec1..0000000000 --- a/changelog.d/6390.doc +++ /dev/null @@ -1 +0,0 @@ -Add build instructions to the docker readme. \ No newline at end of file diff --git a/changelog.d/6392.misc b/changelog.d/6392.misc deleted file mode 100644 index a00257944f..0000000000 --- a/changelog.d/6392.misc +++ /dev/null @@ -1 +0,0 @@ -Add a test scenario to make sure room history purges don't break `/messages` in the future. diff --git a/changelog.d/6406.bugfix b/changelog.d/6406.bugfix deleted file mode 100644 index ca9bee084b..0000000000 --- a/changelog.d/6406.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix bug: TypeError in `register_user()` while using LDAP auth module. diff --git a/changelog.d/6408.bugfix b/changelog.d/6408.bugfix deleted file mode 100644 index c9babe599b..0000000000 --- a/changelog.d/6408.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix an intermittent exception when handling read-receipts. diff --git a/changelog.d/6409.feature b/changelog.d/6409.feature deleted file mode 100644 index 653ff5a5ad..0000000000 --- a/changelog.d/6409.feature +++ /dev/null @@ -1 +0,0 @@ -Add ephemeral messages support by partially implementing [MSC2228](https://github.com/matrix-org/matrix-doc/pull/2228). diff --git a/changelog.d/6420.bugfix b/changelog.d/6420.bugfix deleted file mode 100644 index aef47cccaa..0000000000 --- a/changelog.d/6420.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix broken guest registration when there are existing blocks of numeric user IDs. diff --git a/changelog.d/6421.bugfix b/changelog.d/6421.bugfix deleted file mode 100644 index 7969f7f71d..0000000000 --- a/changelog.d/6421.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix startup error when http proxy is defined. diff --git a/changelog.d/6423.misc b/changelog.d/6423.misc deleted file mode 100644 index 9bcd5d36c1..0000000000 --- a/changelog.d/6423.misc +++ /dev/null @@ -1 +0,0 @@ -Clarifications for the email configuration settings. diff --git a/changelog.d/6426.bugfix b/changelog.d/6426.bugfix deleted file mode 100644 index 3acfde4211..0000000000 --- a/changelog.d/6426.bugfix +++ /dev/null @@ -1 +0,0 @@ -Clean up local threepids from user on account deactivation. \ No newline at end of file diff --git a/changelog.d/6429.misc b/changelog.d/6429.misc deleted file mode 100644 index 4b32cdeac6..0000000000 --- a/changelog.d/6429.misc +++ /dev/null @@ -1 +0,0 @@ -Add more tests to the blacklist when running in worker mode. diff --git a/changelog.d/6434.feature b/changelog.d/6434.feature deleted file mode 100644 index affa5d50c1..0000000000 --- a/changelog.d/6434.feature +++ /dev/null @@ -1 +0,0 @@ -Add support for MSC 2367, which allows specifying a reason on all membership events. diff --git a/changelog.d/6436.bugfix b/changelog.d/6436.bugfix deleted file mode 100644 index 954a4e1d84..0000000000 --- a/changelog.d/6436.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a bug where a room could become unusable with a low retention policy and a low activity. diff --git a/changelog.d/6443.doc b/changelog.d/6443.doc deleted file mode 100644 index 67c59f92ee..0000000000 --- a/changelog.d/6443.doc +++ /dev/null @@ -1 +0,0 @@ -Switch Ubuntu package install recommendation to use python3 packages in INSTALL.md. \ No newline at end of file diff --git a/changelog.d/6449.bugfix b/changelog.d/6449.bugfix deleted file mode 100644 index 002f33c450..0000000000 --- a/changelog.d/6449.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix error when using synapse_port_db on a vanilla synapse db. diff --git a/changelog.d/6451.bugfix b/changelog.d/6451.bugfix deleted file mode 100644 index 23b67583ec..0000000000 --- a/changelog.d/6451.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix uploading multiple cross signing signatures for the same user. diff --git a/changelog.d/6454.misc b/changelog.d/6454.misc deleted file mode 100644 index 9e5259157c..0000000000 --- a/changelog.d/6454.misc +++ /dev/null @@ -1 +0,0 @@ -Move data store specific code out of `SQLBaseStore`. diff --git a/changelog.d/6458.doc b/changelog.d/6458.doc deleted file mode 100644 index 3a9f831d89..0000000000 --- a/changelog.d/6458.doc +++ /dev/null @@ -1 +0,0 @@ -Write some docs for the quarantine_media api. diff --git a/changelog.d/6461.doc b/changelog.d/6461.doc deleted file mode 100644 index 1502fa2855..0000000000 --- a/changelog.d/6461.doc +++ /dev/null @@ -1 +0,0 @@ -Convert CONTRIBUTING.rst to markdown (among other small fixes). \ No newline at end of file diff --git a/changelog.d/6462.bugfix b/changelog.d/6462.bugfix deleted file mode 100644 index c435939526..0000000000 --- a/changelog.d/6462.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix bug which lead to exceptions being thrown in a loop when a cross-signed device is deleted. diff --git a/changelog.d/6464.misc b/changelog.d/6464.misc deleted file mode 100644 index bd65276ef6..0000000000 --- a/changelog.d/6464.misc +++ /dev/null @@ -1 +0,0 @@ -Prepare SQLBaseStore functions being moved out of the stores. diff --git a/changelog.d/6468.misc b/changelog.d/6468.misc deleted file mode 100644 index d9a44389b9..0000000000 --- a/changelog.d/6468.misc +++ /dev/null @@ -1 +0,0 @@ -Refactor some code in the event authentication path for clarity. diff --git a/changelog.d/6469.misc b/changelog.d/6469.misc deleted file mode 100644 index 32216b9046..0000000000 --- a/changelog.d/6469.misc +++ /dev/null @@ -1 +0,0 @@ -Move per database functionality out of the data stores and into a dedicated `Database` class. diff --git a/changelog.d/6470.bugfix b/changelog.d/6470.bugfix deleted file mode 100644 index c08b34c14c..0000000000 --- a/changelog.d/6470.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix `synapse_port_db` not exiting with a 0 code if something went wrong during the port process. diff --git a/changelog.d/6472.bugfix b/changelog.d/6472.bugfix deleted file mode 100644 index 598efb79fc..0000000000 --- a/changelog.d/6472.bugfix +++ /dev/null @@ -1 +0,0 @@ -Improve sanity-checking when receiving events over federation. diff --git a/changelog.d/6480.misc b/changelog.d/6480.misc deleted file mode 100644 index d9a44389b9..0000000000 --- a/changelog.d/6480.misc +++ /dev/null @@ -1 +0,0 @@ -Refactor some code in the event authentication path for clarity. diff --git a/changelog.d/6482.misc b/changelog.d/6482.misc deleted file mode 100644 index bdef9cf40a..0000000000 --- a/changelog.d/6482.misc +++ /dev/null @@ -1 +0,0 @@ -Port synapse.rest.client.v1 to async/await. diff --git a/changelog.d/6483.misc b/changelog.d/6483.misc deleted file mode 100644 index cb2cd2bc39..0000000000 --- a/changelog.d/6483.misc +++ /dev/null @@ -1 +0,0 @@ -Port synapse.rest.client.v2_alpha to async/await. diff --git a/changelog.d/6484.misc b/changelog.d/6484.misc deleted file mode 100644 index b7cd600012..0000000000 --- a/changelog.d/6484.misc +++ /dev/null @@ -1 +0,0 @@ -Port SyncHandler to async/await. diff --git a/changelog.d/6487.misc b/changelog.d/6487.misc deleted file mode 100644 index 18b49b9cbd..0000000000 --- a/changelog.d/6487.misc +++ /dev/null @@ -1 +0,0 @@ -Pass in `Database` object to data stores. diff --git a/changelog.d/6488.removal b/changelog.d/6488.removal deleted file mode 100644 index 06e034a213..0000000000 --- a/changelog.d/6488.removal +++ /dev/null @@ -1 +0,0 @@ -Remove fallback for federation with old servers which lack the /federation/v1/state_ids API. diff --git a/changelog.d/6490.doc b/changelog.d/6490.doc deleted file mode 100644 index 6db351d7db..0000000000 --- a/changelog.d/6490.doc +++ /dev/null @@ -1 +0,0 @@ -Update documentation and variables in user contributed systemd reference file. diff --git a/changelog.d/6491.bugfix b/changelog.d/6491.bugfix deleted file mode 100644 index 78204693b0..0000000000 --- a/changelog.d/6491.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix inaccurate per-block Prometheus metrics. diff --git a/changelog.d/6493.bugfix b/changelog.d/6493.bugfix deleted file mode 100644 index 440c02efbe..0000000000 --- a/changelog.d/6493.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix small performance regression for sending invites. diff --git a/changelog.d/6494.bugfix b/changelog.d/6494.bugfix deleted file mode 100644 index 78726d5d7f..0000000000 --- a/changelog.d/6494.bugfix +++ /dev/null @@ -1 +0,0 @@ -Back out cross-signing code added in Synapse 1.5.0, which caused a performance regression. diff --git a/synapse/__init__.py b/synapse/__init__.py index f99de2f3f3..c67a51a8d5 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -36,7 +36,7 @@ try: except ImportError: pass -__version__ = "1.6.1" +__version__ = "1.7.0rc1" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 5e8abe9013427e8ad452c4652dfcb40da05c246e Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Mon, 9 Dec 2019 14:54:33 +0000 Subject: [PATCH 0634/1623] Better errors regarding changing avatar_url (#6497) --- changelog.d/6497.bugfix | 1 + synapse/rest/client/v1/profile.py | 11 ++++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 changelog.d/6497.bugfix diff --git a/changelog.d/6497.bugfix b/changelog.d/6497.bugfix new file mode 100644 index 0000000000..92ed08fc40 --- /dev/null +++ b/changelog.d/6497.bugfix @@ -0,0 +1 @@ +Fix error message when setting your profile's avatar URL mentioning displaynames, and prevent NoneType avatar_urls. \ No newline at end of file diff --git a/synapse/rest/client/v1/profile.py b/synapse/rest/client/v1/profile.py index 1eac8a44c5..4f47562c1b 100644 --- a/synapse/rest/client/v1/profile.py +++ b/synapse/rest/client/v1/profile.py @@ -103,11 +103,16 @@ class ProfileAvatarURLRestServlet(RestServlet): content = parse_json_object_from_request(request) try: - new_name = content["avatar_url"] + new_avatar_url = content.get("avatar_url") except Exception: - return 400, "Unable to parse name" + return 400, "Unable to parse avatar_url" - await self.profile_handler.set_avatar_url(user, requester, new_name, is_admin) + if new_avatar_url is None: + return 400, "Missing required key: avatar_url" + + await self.profile_handler.set_avatar_url( + user, requester, new_avatar_url, is_admin + ) return 200, {} From 21aa0a458fa8dc035895392a30c389bbb8f51ef6 Mon Sep 17 00:00:00 2001 From: Neil Johnson Date: Mon, 9 Dec 2019 14:57:09 +0000 Subject: [PATCH 0635/1623] Update CHANGES.md --- CHANGES.md | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 0ef9794aac..2a247690d1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,14 +4,14 @@ Synapse 1.7.0rc1 (2019-12-09) Features -------- -- Implement per-room message retention policies. ([\#5815](https://github.com/matrix-org/synapse/issues/5815)) +- Implement per-room message retention policies. ([\#5815](https://github.com/matrix-org/synapse/issues/5815), [\#6436](https://github.com/matrix-org/synapse/issues/6436)) - Add etag and count fields to key backup endpoints to help clients guess if there are new keys. ([\#5858](https://github.com/matrix-org/synapse/issues/5858)) -- Add admin/v2/users endpoint with pagination. Contributed by Awesome Technologies Innovationslabor GmbH. ([\#5925](https://github.com/matrix-org/synapse/issues/5925)) +- Add `/admin/v2/users` endpoint with pagination. Contributed by Awesome Technologies Innovationslabor GmbH. ([\#5925](https://github.com/matrix-org/synapse/issues/5925)) - Require User-Interactive Authentication for `/account/3pid/add`, meaning the user's password will be required to add a third-party ID to their account. ([\#6119](https://github.com/matrix-org/synapse/issues/6119)) - Implement the `/_matrix/federation/unstable/net.atleastfornow/state/` API as drafted in MSC2314. ([\#6176](https://github.com/matrix-org/synapse/issues/6176)) -- Configure privacy preserving settings by default for the room directory. ([\#6354](https://github.com/matrix-org/synapse/issues/6354)) +- Configure privacy-preserving settings by default for the room directory. ([\#6354](https://github.com/matrix-org/synapse/issues/6354)) - Add ephemeral messages support by partially implementing [MSC2228](https://github.com/matrix-org/matrix-doc/pull/2228). ([\#6409](https://github.com/matrix-org/synapse/issues/6409)) -- Add support for MSC 2367, which allows specifying a reason on all membership events. ([\#6434](https://github.com/matrix-org/synapse/issues/6434)) +- Add support for [MSC 2367](https://github.com/matrix-org/matrix-doc/pull/2367), which allows specifying a reason on all membership events. ([\#6434](https://github.com/matrix-org/synapse/issues/6434)) Bugfixes @@ -26,8 +26,6 @@ Bugfixes - Fix an intermittent exception when handling read-receipts. ([\#6408](https://github.com/matrix-org/synapse/issues/6408)) - Fix broken guest registration when there are existing blocks of numeric user IDs. ([\#6420](https://github.com/matrix-org/synapse/issues/6420)) - Fix startup error when http proxy is defined. ([\#6421](https://github.com/matrix-org/synapse/issues/6421)) -- Clean up local threepids from user on account deactivation. ([\#6426](https://github.com/matrix-org/synapse/issues/6426)) -- Fix a bug where a room could become unusable with a low retention policy and a low activity. ([\#6436](https://github.com/matrix-org/synapse/issues/6436)) - Fix error when using synapse_port_db on a vanilla synapse db. ([\#6449](https://github.com/matrix-org/synapse/issues/6449)) - Fix uploading multiple cross signing signatures for the same user. ([\#6451](https://github.com/matrix-org/synapse/issues/6451)) - Fix bug which lead to exceptions being thrown in a loop when a cross-signed device is deleted. ([\#6462](https://github.com/matrix-org/synapse/issues/6462)) @@ -67,14 +65,10 @@ Internal Changes - Add a test scenario to make sure room history purges don't break `/messages` in the future. ([\#6392](https://github.com/matrix-org/synapse/issues/6392)) - Clarifications for the email configuration settings. ([\#6423](https://github.com/matrix-org/synapse/issues/6423)) - Add more tests to the blacklist when running in worker mode. ([\#6429](https://github.com/matrix-org/synapse/issues/6429)) -- Move data store specific code out of `SQLBaseStore`. ([\#6454](https://github.com/matrix-org/synapse/issues/6454)) -- Prepare SQLBaseStore functions being moved out of the stores. ([\#6464](https://github.com/matrix-org/synapse/issues/6464)) -- Move per database functionality out of the data stores and into a dedicated `Database` class. ([\#6469](https://github.com/matrix-org/synapse/issues/6469)) +- Refactor data store layer to support multiple databases in the future. ([\#6454](https://github.com/matrix-org/synapse/issues/6454), [\#6464](https://github.com/matrix-org/synapse/issues/6464), [\#6469](https://github.com/matrix-org/synapse/issues/6469), [\#6487](https://github.com/matrix-org/synapse/issues/6487)) - Port synapse.rest.client.v1 to async/await. ([\#6482](https://github.com/matrix-org/synapse/issues/6482)) - Port synapse.rest.client.v2_alpha to async/await. ([\#6483](https://github.com/matrix-org/synapse/issues/6483)) - Port SyncHandler to async/await. ([\#6484](https://github.com/matrix-org/synapse/issues/6484)) -- Pass in `Database` object to data stores. ([\#6487](https://github.com/matrix-org/synapse/issues/6487)) - Synapse 1.6.1 (2019-11-28) ========================== From 4cade966164469b6517e821d27481e7ed019288e Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 9 Dec 2019 15:09:13 +0000 Subject: [PATCH 0636/1623] Fix support for SQLite 3.7. Partial indices support was added in 3.8.0, so we need to use the background updates that handles this correctly. --- .../data_stores/main/events_bg_updates.py | 16 ++++++++++++++++ .../main/schema/delta/56/redaction_censor.sql | 4 +++- .../main/schema/delta/56/redaction_censor2.sql | 4 +++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/synapse/storage/data_stores/main/events_bg_updates.py b/synapse/storage/data_stores/main/events_bg_updates.py index efee17b929..efb9cd57af 100644 --- a/synapse/storage/data_stores/main/events_bg_updates.py +++ b/synapse/storage/data_stores/main/events_bg_updates.py @@ -90,6 +90,22 @@ class EventsBackgroundUpdatesStore(SQLBaseStore): "event_store_labels", self._event_store_labels ) + self.db.updates.register_background_index_update( + "redactions_have_censored_idx", + index_name="redactions_have_censored", + table="redactions", + columns=["event_id"], + where_clause="NOT have_censored", + ) + + self.db.updates.register_background_index_update( + "redactions_have_censored_ts_idx", + index_name="redactions_have_censored_ts", + table="redactions", + columns=["received_ts"], + where_clause="NOT have_censored", + ) + @defer.inlineCallbacks def _background_reindex_fields_sender(self, progress, batch_size): target_min_stream_id = progress["target_min_stream_id_inclusive"] diff --git a/synapse/storage/data_stores/main/schema/delta/56/redaction_censor.sql b/synapse/storage/data_stores/main/schema/delta/56/redaction_censor.sql index fe51b02309..a8583b52cc 100644 --- a/synapse/storage/data_stores/main/schema/delta/56/redaction_censor.sql +++ b/synapse/storage/data_stores/main/schema/delta/56/redaction_censor.sql @@ -14,4 +14,6 @@ */ ALTER TABLE redactions ADD COLUMN have_censored BOOL NOT NULL DEFAULT false; -CREATE INDEX redactions_have_censored ON redactions(event_id) WHERE not have_censored; + +INSERT INTO background_updates (update_name, progress_json) VALUES + ('redactions_have_censored_idx', '{}'); diff --git a/synapse/storage/data_stores/main/schema/delta/56/redaction_censor2.sql b/synapse/storage/data_stores/main/schema/delta/56/redaction_censor2.sql index 77a5eca499..49ce35d794 100644 --- a/synapse/storage/data_stores/main/schema/delta/56/redaction_censor2.sql +++ b/synapse/storage/data_stores/main/schema/delta/56/redaction_censor2.sql @@ -14,7 +14,9 @@ */ ALTER TABLE redactions ADD COLUMN received_ts BIGINT; -CREATE INDEX redactions_have_censored_ts ON redactions(received_ts) WHERE not have_censored; INSERT INTO background_updates (update_name, progress_json) VALUES ('redactions_received_ts', '{}'); + +INSERT INTO background_updates (update_name, progress_json) VALUES + ('redactions_have_censored_ts_idx', '{}'); From 52fe9788bcc2c4b422f387421667698187fc2135 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 9 Dec 2019 15:19:32 +0000 Subject: [PATCH 0637/1623] Newsfile --- changelog.d/6499.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6499.bugfix diff --git a/changelog.d/6499.bugfix b/changelog.d/6499.bugfix new file mode 100644 index 0000000000..299feba0f8 --- /dev/null +++ b/changelog.d/6499.bugfix @@ -0,0 +1 @@ +Fix support for SQLite 3.7. From d95736a2bd189c38bc168340975bf593e89f17f4 Mon Sep 17 00:00:00 2001 From: Neil Johnson Date: Tue, 10 Dec 2019 10:05:33 +0000 Subject: [PATCH 0638/1623] Fix erroneous reference for new room directory defaults. --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 2a247690d1..c30ea4718d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,7 +9,7 @@ Features - Add `/admin/v2/users` endpoint with pagination. Contributed by Awesome Technologies Innovationslabor GmbH. ([\#5925](https://github.com/matrix-org/synapse/issues/5925)) - Require User-Interactive Authentication for `/account/3pid/add`, meaning the user's password will be required to add a third-party ID to their account. ([\#6119](https://github.com/matrix-org/synapse/issues/6119)) - Implement the `/_matrix/federation/unstable/net.atleastfornow/state/` API as drafted in MSC2314. ([\#6176](https://github.com/matrix-org/synapse/issues/6176)) -- Configure privacy-preserving settings by default for the room directory. ([\#6354](https://github.com/matrix-org/synapse/issues/6354)) +- Configure privacy-preserving settings by default for the room directory. ([\#6355](https://github.com/matrix-org/synapse/issues/6355)) - Add ephemeral messages support by partially implementing [MSC2228](https://github.com/matrix-org/matrix-doc/pull/2228). ([\#6409](https://github.com/matrix-org/synapse/issues/6409)) - Add support for [MSC 2367](https://github.com/matrix-org/matrix-doc/pull/2367), which allows specifying a reason on all membership events. ([\#6434](https://github.com/matrix-org/synapse/issues/6434)) From 353396e3a7c00c2634e3a175e9aeac98c6a4598f Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 10 Dec 2019 11:12:56 +0000 Subject: [PATCH 0639/1623] Port handlers.account_data to async/await. --- synapse/handlers/account_data.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/synapse/handlers/account_data.py b/synapse/handlers/account_data.py index 2d7e6df6e4..fe44d62a1c 100644 --- a/synapse/handlers/account_data.py +++ b/synapse/handlers/account_data.py @@ -13,8 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from twisted.internet import defer - class AccountDataEventSource(object): def __init__(self, hs): @@ -23,15 +21,14 @@ class AccountDataEventSource(object): def get_current_key(self, direction="f"): return self.store.get_max_account_data_stream_id() - @defer.inlineCallbacks - def get_new_events(self, user, from_key, **kwargs): + async def get_new_events(self, user, from_key, **kwargs): user_id = user.to_string() last_stream_id = from_key - current_stream_id = yield self.store.get_max_account_data_stream_id() + current_stream_id = await self.store.get_max_account_data_stream_id() results = [] - tags = yield self.store.get_updated_tags(user_id, last_stream_id) + tags = await self.store.get_updated_tags(user_id, last_stream_id) for room_id, room_tags in tags.items(): results.append( @@ -41,7 +38,7 @@ class AccountDataEventSource(object): ( account_data, room_account_data, - ) = yield self.store.get_updated_account_data_for_user(user_id, last_stream_id) + ) = await self.store.get_updated_account_data_for_user(user_id, last_stream_id) for account_data_type, content in account_data.items(): results.append({"type": account_data_type, "content": content}) @@ -54,6 +51,5 @@ class AccountDataEventSource(object): return results, current_stream_id - @defer.inlineCallbacks - def get_pagination_rows(self, user, config, key): + async def get_pagination_rows(self, user, config, key): return [], config.to_id From 9a2223d4c8c2a46f7ab072597bdba2614bc3e6ea Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 10 Dec 2019 11:22:12 +0000 Subject: [PATCH 0640/1623] Fix make_deferred_yieldable to work with coroutines --- synapse/logging/context.py | 9 ++++++++- tests/util/test_logcontext.py | 24 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/synapse/logging/context.py b/synapse/logging/context.py index 2c1fb9ddac..9f484ce59e 100644 --- a/synapse/logging/context.py +++ b/synapse/logging/context.py @@ -23,6 +23,7 @@ them. See doc/log_contexts.rst for details on how this works. """ +import inspect import logging import threading import types @@ -612,7 +613,8 @@ def run_in_background(f, *args, **kwargs): def make_deferred_yieldable(deferred): - """Given a deferred, make it follow the Synapse logcontext rules: + """Given a deferred (or coroutine), make it follow the Synapse logcontext + rules: If the deferred has completed (or is not actually a Deferred), essentially does nothing (just returns another completed deferred with the @@ -624,6 +626,11 @@ def make_deferred_yieldable(deferred): (This is more-or-less the opposite operation to run_in_background.) """ + if inspect.isawaitable(deferred): + # If we're given a coroutine we need to convert it to a deferred so that + # we can attach callbacks (and not immediately return). + deferred = defer.ensureDeferred(deferred) + if not isinstance(deferred, defer.Deferred): return deferred diff --git a/tests/util/test_logcontext.py b/tests/util/test_logcontext.py index 8b8455c8b7..281b32c4b8 100644 --- a/tests/util/test_logcontext.py +++ b/tests/util/test_logcontext.py @@ -179,6 +179,30 @@ class LoggingContextTestCase(unittest.TestCase): nested_context = nested_logging_context(suffix="bar") self.assertEqual(nested_context.request, "foo-bar") + @defer.inlineCallbacks + def test_make_deferred_yieldable_with_await(self): + # an async function which retuns an incomplete coroutine, but doesn't + # follow the synapse rules. + + async def blocking_function(): + d = defer.Deferred() + reactor.callLater(0, d.callback, None) + await d + + sentinel_context = LoggingContext.current_context() + + with LoggingContext() as context_one: + context_one.request = "one" + + d1 = make_deferred_yieldable(blocking_function()) + # make sure that the context was reset by make_deferred_yieldable + self.assertIs(LoggingContext.current_context(), sentinel_context) + + yield d1 + + # now it should be restored + self._check_test_key("one") + # a function which returns a deferred which has been "called", but # which had a function which returned another incomplete deferred on From f5bb1531b7307bc2d826789746e7c82fa4dbf36c Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 10 Dec 2019 11:23:52 +0000 Subject: [PATCH 0641/1623] Newsfile --- changelog.d/6505.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6505.misc diff --git a/changelog.d/6505.misc b/changelog.d/6505.misc new file mode 100644 index 0000000000..3a75b2d9dd --- /dev/null +++ b/changelog.d/6505.misc @@ -0,0 +1 @@ +Make `make_deferred_yieldable` to work with async/await. From b1e7012deea2d254ecbf92d1fed429c34a65db54 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 10 Dec 2019 11:29:44 +0000 Subject: [PATCH 0642/1623] Newsfile --- changelog.d/6506.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6506.misc diff --git a/changelog.d/6506.misc b/changelog.d/6506.misc new file mode 100644 index 0000000000..99d7a70bcf --- /dev/null +++ b/changelog.d/6506.misc @@ -0,0 +1 @@ +Remove `SnapshotCache` in favour of `ResponseCache`. From cc5f6eb6083470f3980f93f937c6251be5e971dd Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 10 Dec 2019 11:39:31 +0000 Subject: [PATCH 0643/1623] Only start censor background job after indices are created --- synapse/storage/data_stores/main/events.py | 7 +++++++ .../data_stores/main/schema/delta/56/redaction_censor2.sql | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/synapse/storage/data_stores/main/events.py b/synapse/storage/data_stores/main/events.py index da1529f6ea..bd670f0022 100644 --- a/synapse/storage/data_stores/main/events.py +++ b/synapse/storage/data_stores/main/events.py @@ -1053,6 +1053,13 @@ class EventsStore( if self.hs.config.redaction_retention_period is None: return + if self.db.updates.has_completed_background_update( + "redactions_have_censored_ts_idx" + ): + # We don't want to run this until the appropriate index has been + # created. + return + before_ts = self._clock.time_msec() - self.hs.config.redaction_retention_period # We fetch all redactions that: diff --git a/synapse/storage/data_stores/main/schema/delta/56/redaction_censor2.sql b/synapse/storage/data_stores/main/schema/delta/56/redaction_censor2.sql index 49ce35d794..6c36bd5468 100644 --- a/synapse/storage/data_stores/main/schema/delta/56/redaction_censor2.sql +++ b/synapse/storage/data_stores/main/schema/delta/56/redaction_censor2.sql @@ -18,5 +18,5 @@ ALTER TABLE redactions ADD COLUMN received_ts BIGINT; INSERT INTO background_updates (update_name, progress_json) VALUES ('redactions_received_ts', '{}'); -INSERT INTO background_updates (update_name, progress_json) VALUES - ('redactions_have_censored_ts_idx', '{}'); +INSERT INTO background_updates (update_name, progress_json, depends_on) VALUES + ('redactions_have_censored_ts_idx', '{}', 'redactions_have_censored_idx'); From 2ac78438d8c7992c9c1ab347830c5498205f7008 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 10 Dec 2019 12:31:03 +0000 Subject: [PATCH 0644/1623] Make the PusherSlaveStore inherit from the slave RoomStore So that it has access to the get_retention_policy_for_room function which is required by filter_events_for_client. --- synapse/app/pusher.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/synapse/app/pusher.py b/synapse/app/pusher.py index 01a5ffc363..dd52a9fc2d 100644 --- a/synapse/app/pusher.py +++ b/synapse/app/pusher.py @@ -33,6 +33,7 @@ from synapse.replication.slave.storage.account_data import SlavedAccountDataStor from synapse.replication.slave.storage.events import SlavedEventStore from synapse.replication.slave.storage.pushers import SlavedPusherStore from synapse.replication.slave.storage.receipts import SlavedReceiptsStore +from synapse.replication.slave.storage.room import RoomStore from synapse.replication.tcp.client import ReplicationClientHandler from synapse.server import HomeServer from synapse.storage import DataStore @@ -45,7 +46,11 @@ logger = logging.getLogger("synapse.app.pusher") class PusherSlaveStore( - SlavedEventStore, SlavedPusherStore, SlavedReceiptsStore, SlavedAccountDataStore + SlavedEventStore, + SlavedPusherStore, + SlavedReceiptsStore, + SlavedAccountDataStore, + RoomStore, ): update_pusher_last_stream_ordering_and_success = __func__( DataStore.update_pusher_last_stream_ordering_and_success From ec5fdd13339cb9bd2bb87537fdac524da2cad614 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 10 Dec 2019 12:34:33 +0000 Subject: [PATCH 0645/1623] Changelog --- changelog.d/6507.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6507.bugfix diff --git a/changelog.d/6507.bugfix b/changelog.d/6507.bugfix new file mode 100644 index 0000000000..7f95da52c9 --- /dev/null +++ b/changelog.d/6507.bugfix @@ -0,0 +1 @@ +Fix pusher worker failing because it can't retrieve retention policies for rooms. From 31da85e467250a7d638650e14782290fb4476087 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 10 Dec 2019 12:42:58 +0000 Subject: [PATCH 0646/1623] Convert _censor_redactions to async since it awaits on coroutines --- synapse/storage/data_stores/main/events.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/synapse/storage/data_stores/main/events.py b/synapse/storage/data_stores/main/events.py index bd670f0022..998bba1aad 100644 --- a/synapse/storage/data_stores/main/events.py +++ b/synapse/storage/data_stores/main/events.py @@ -1039,22 +1039,20 @@ class EventsStore( }, ) - @defer.inlineCallbacks - def _censor_redactions(self): + async def _censor_redactions(self): """Censors all redactions older than the configured period that haven't been censored yet. By censor we mean update the event_json table with the redacted event. - - Returns: - Deferred """ if self.hs.config.redaction_retention_period is None: return - if self.db.updates.has_completed_background_update( - "redactions_have_censored_ts_idx" + if not ( + await self.db.updates.has_completed_background_update( + "redactions_have_censored_ts_idx" + ) ): # We don't want to run this until the appropriate index has been # created. @@ -1081,15 +1079,15 @@ class EventsStore( LIMIT ? """ - rows = yield self.db.execute( + rows = await self.db.execute( "_censor_redactions_fetch", None, sql, before_ts, 100 ) updates = [] for redaction_id, event_id in rows: - redaction_event = yield self.get_event(redaction_id, allow_none=True) - original_event = yield self.get_event( + redaction_event = await self.get_event(redaction_id, allow_none=True) + original_event = await self.get_event( event_id, allow_rejected=True, allow_none=True ) @@ -1122,7 +1120,7 @@ class EventsStore( updatevalues={"have_censored": True}, ) - yield self.db.runInteraction("_update_censor_txn", _update_censor_txn) + await self.db.runInteraction("_update_censor_txn", _update_censor_txn) def _censor_event_txn(self, txn, event_id, pruned_json): """Censor an event by replacing its JSON in the event_json table with the From 52346990c8850a9002209b4b24c8f65b11d27ab4 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 10 Dec 2019 12:45:16 +0000 Subject: [PATCH 0647/1623] Drop unused index --- .../data_stores/main/events_bg_updates.py | 8 -------- .../main/schema/delta/56/redaction_censor.sql | 3 --- .../main/schema/delta/56/redaction_censor2.sql | 4 ++-- .../main/schema/delta/56/redaction_censor4.sql | 16 ++++++++++++++++ 4 files changed, 18 insertions(+), 13 deletions(-) create mode 100644 synapse/storage/data_stores/main/schema/delta/56/redaction_censor4.sql diff --git a/synapse/storage/data_stores/main/events_bg_updates.py b/synapse/storage/data_stores/main/events_bg_updates.py index efb9cd57af..5177b71016 100644 --- a/synapse/storage/data_stores/main/events_bg_updates.py +++ b/synapse/storage/data_stores/main/events_bg_updates.py @@ -90,14 +90,6 @@ class EventsBackgroundUpdatesStore(SQLBaseStore): "event_store_labels", self._event_store_labels ) - self.db.updates.register_background_index_update( - "redactions_have_censored_idx", - index_name="redactions_have_censored", - table="redactions", - columns=["event_id"], - where_clause="NOT have_censored", - ) - self.db.updates.register_background_index_update( "redactions_have_censored_ts_idx", index_name="redactions_have_censored_ts", diff --git a/synapse/storage/data_stores/main/schema/delta/56/redaction_censor.sql b/synapse/storage/data_stores/main/schema/delta/56/redaction_censor.sql index a8583b52cc..ea95db0ed7 100644 --- a/synapse/storage/data_stores/main/schema/delta/56/redaction_censor.sql +++ b/synapse/storage/data_stores/main/schema/delta/56/redaction_censor.sql @@ -14,6 +14,3 @@ */ ALTER TABLE redactions ADD COLUMN have_censored BOOL NOT NULL DEFAULT false; - -INSERT INTO background_updates (update_name, progress_json) VALUES - ('redactions_have_censored_idx', '{}'); diff --git a/synapse/storage/data_stores/main/schema/delta/56/redaction_censor2.sql b/synapse/storage/data_stores/main/schema/delta/56/redaction_censor2.sql index 6c36bd5468..49ce35d794 100644 --- a/synapse/storage/data_stores/main/schema/delta/56/redaction_censor2.sql +++ b/synapse/storage/data_stores/main/schema/delta/56/redaction_censor2.sql @@ -18,5 +18,5 @@ ALTER TABLE redactions ADD COLUMN received_ts BIGINT; INSERT INTO background_updates (update_name, progress_json) VALUES ('redactions_received_ts', '{}'); -INSERT INTO background_updates (update_name, progress_json, depends_on) VALUES - ('redactions_have_censored_ts_idx', '{}', 'redactions_have_censored_idx'); +INSERT INTO background_updates (update_name, progress_json) VALUES + ('redactions_have_censored_ts_idx', '{}'); diff --git a/synapse/storage/data_stores/main/schema/delta/56/redaction_censor4.sql b/synapse/storage/data_stores/main/schema/delta/56/redaction_censor4.sql new file mode 100644 index 0000000000..b7550f6f4e --- /dev/null +++ b/synapse/storage/data_stores/main/schema/delta/56/redaction_censor4.sql @@ -0,0 +1,16 @@ +/* Copyright 2019 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +DROP INDEX IF EXISTS redactions_have_censored; From 3bd049bbb771c18ba20aa724f240a82a394d0ad2 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 10 Dec 2019 13:05:35 +0000 Subject: [PATCH 0648/1623] Give the server config to the RoomWorkerStore --- synapse/storage/data_stores/main/room.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/synapse/storage/data_stores/main/room.py b/synapse/storage/data_stores/main/room.py index 0148be20d3..aa476d0fbf 100644 --- a/synapse/storage/data_stores/main/room.py +++ b/synapse/storage/data_stores/main/room.py @@ -46,6 +46,11 @@ RatelimitOverride = collections.namedtuple( class RoomWorkerStore(SQLBaseStore): + def __init__(self, database: Database, db_conn, hs): + super(RoomWorkerStore, self).__init__(database, db_conn, hs) + + self.config = hs.config + def get_room(self, room_id): """Retrieve a room. From 451ec9b8b96c17dcd465981fb8715f071a9316b4 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 10 Dec 2019 13:06:41 +0000 Subject: [PATCH 0649/1623] Changelog --- changelog.d/6509.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6509.bugfix diff --git a/changelog.d/6509.bugfix b/changelog.d/6509.bugfix new file mode 100644 index 0000000000..7f95da52c9 --- /dev/null +++ b/changelog.d/6509.bugfix @@ -0,0 +1 @@ +Fix pusher worker failing because it can't retrieve retention policies for rooms. From ffeafade4879a1135ab6795d4ba0c11131f82f01 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 10 Dec 2019 13:17:39 +0000 Subject: [PATCH 0650/1623] Update comment --- synapse/logging/context.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/synapse/logging/context.py b/synapse/logging/context.py index 9f484ce59e..6747f29e6a 100644 --- a/synapse/logging/context.py +++ b/synapse/logging/context.py @@ -627,8 +627,10 @@ def make_deferred_yieldable(deferred): (This is more-or-less the opposite operation to run_in_background.) """ if inspect.isawaitable(deferred): - # If we're given a coroutine we need to convert it to a deferred so that - # we can attach callbacks (and not immediately return). + # If we're given a coroutine we convert it to a deferred so that we + # run it and find out if it immediately finishes, it it does then we + # don't need to fiddle with log contexts at all and can return + # immediately. deferred = defer.ensureDeferred(deferred) if not isinstance(deferred, defer.Deferred): From 663238aeb4ff1df2e857a12355fdf7cf76dcd6d9 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 6 Dec 2019 17:42:14 +0000 Subject: [PATCH 0651/1623] Phone home stats DB reporting should not assume a single DB. --- synapse/app/homeserver.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py index df65d0a989..032010600a 100644 --- a/synapse/app/homeserver.py +++ b/synapse/app/homeserver.py @@ -519,8 +519,10 @@ def phone_stats_home(hs, stats, stats_process=_stats_process): # Database version # - stats["database_engine"] = hs.database_engine.module.__name__ - stats["database_server_version"] = hs.database_engine.server_version + # This only reports info about the *main* database. + stats["database_engine"] = hs.get_datastore().db.engine.module.__name__ + stats["database_server_version"] = hs.get_datastore().db.engine.server_version + logger.info("Reporting stats to %s: %s" % (hs.config.report_stats_endpoint, stats)) try: yield hs.get_proxied_http_client().put_json( From accd343f9104653c0e89d79101ae6ae767f7aa35 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 10 Dec 2019 13:22:42 +0000 Subject: [PATCH 0652/1623] Newsfile --- changelog.d/6510.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6510.misc diff --git a/changelog.d/6510.misc b/changelog.d/6510.misc new file mode 100644 index 0000000000..214f06539b --- /dev/null +++ b/changelog.d/6510.misc @@ -0,0 +1 @@ +Change phone home stats to not assume there is a single database and report information about the database used by the main data store. From 28b758fa0f396aafa92cdc96ff716469a3b083d4 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 6 Dec 2019 17:42:26 +0000 Subject: [PATCH 0653/1623] Silence mypy errors for files outside those specified --- mypy.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy.ini b/mypy.ini index 1d77c0ecc8..a66434b76b 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,7 +1,7 @@ [mypy] namespace_packages = True plugins = mypy_zope:plugin -follow_imports = normal +follow_imports = silent check_untyped_defs = True show_error_codes = True show_traceback = True From 4643bb2a37dcd6302fd5b9e185ea5d0f4eb0df8c Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 10 Dec 2019 13:36:00 +0000 Subject: [PATCH 0654/1623] Newsfile --- changelog.d/6512.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6512.misc diff --git a/changelog.d/6512.misc b/changelog.d/6512.misc new file mode 100644 index 0000000000..37a8099eec --- /dev/null +++ b/changelog.d/6512.misc @@ -0,0 +1 @@ +Silence mypy errors for files outside those specified. From ae49d29ef1dabec25dc348bf11c2919d28d1cef3 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 10 Dec 2019 13:55:03 +0000 Subject: [PATCH 0655/1623] Fixup changelogs --- changelog.d/6507.bugfix | 2 +- changelog.d/6509.bugfix | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog.d/6507.bugfix b/changelog.d/6507.bugfix index 7f95da52c9..d767a6237f 100644 --- a/changelog.d/6507.bugfix +++ b/changelog.d/6507.bugfix @@ -1 +1 @@ -Fix pusher worker failing because it can't retrieve retention policies for rooms. +Fix regression where sending email push would not work when using a pusher worker. diff --git a/changelog.d/6509.bugfix b/changelog.d/6509.bugfix index 7f95da52c9..d767a6237f 100644 --- a/changelog.d/6509.bugfix +++ b/changelog.d/6509.bugfix @@ -1 +1 @@ -Fix pusher worker failing because it can't retrieve retention policies for rooms. +Fix regression where sending email push would not work when using a pusher worker. From bc5cb8bfe8c5b0b551de9a97912cd00caeaf6b48 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 6 Dec 2019 17:42:18 +0000 Subject: [PATCH 0656/1623] Remove database config parsing from apps. --- synapse/app/admin_cmd.py | 5 ----- synapse/app/appservice.py | 5 ----- synapse/app/client_reader.py | 5 ----- synapse/app/event_creator.py | 5 ----- synapse/app/federation_reader.py | 5 ----- synapse/app/federation_sender.py | 5 ----- synapse/app/frontend_proxy.py | 5 ----- synapse/app/homeserver.py | 7 +------ synapse/app/media_repository.py | 5 ----- synapse/app/pusher.py | 5 ----- synapse/app/synchrotron.py | 5 ----- synapse/app/user_dir.py | 5 ----- synapse/server.py | 10 +++++++++- tests/test_types.py | 19 ++++++++----------- tests/utils.py | 2 -- 15 files changed, 18 insertions(+), 75 deletions(-) diff --git a/synapse/app/admin_cmd.py b/synapse/app/admin_cmd.py index 04751a6a5e..51a909419f 100644 --- a/synapse/app/admin_cmd.py +++ b/synapse/app/admin_cmd.py @@ -45,7 +45,6 @@ from synapse.replication.slave.storage.registration import SlavedRegistrationSto from synapse.replication.slave.storage.room import RoomStore from synapse.replication.tcp.client import ReplicationClientHandler from synapse.server import HomeServer -from synapse.storage.engines import create_engine from synapse.util.logcontext import LoggingContext from synapse.util.versionstring import get_version_string @@ -229,14 +228,10 @@ def start(config_options): synapse.events.USE_FROZEN_DICTS = config.use_frozen_dicts - database_engine = create_engine(config.database_config) - ss = AdminCmdServer( config.server_name, - db_config=config.database_config, config=config, version_string="Synapse/" + get_version_string(synapse), - database_engine=database_engine, ) setup_logging(ss, config, use_worker_options=True) diff --git a/synapse/app/appservice.py b/synapse/app/appservice.py index 02b900f382..e82e0f11e3 100644 --- a/synapse/app/appservice.py +++ b/synapse/app/appservice.py @@ -34,7 +34,6 @@ from synapse.replication.slave.storage.events import SlavedEventStore from synapse.replication.slave.storage.registration import SlavedRegistrationStore from synapse.replication.tcp.client import ReplicationClientHandler from synapse.server import HomeServer -from synapse.storage.engines import create_engine from synapse.util.httpresourcetree import create_resource_tree from synapse.util.manhole import manhole from synapse.util.versionstring import get_version_string @@ -143,8 +142,6 @@ def start(config_options): events.USE_FROZEN_DICTS = config.use_frozen_dicts - database_engine = create_engine(config.database_config) - if config.notify_appservices: sys.stderr.write( "\nThe appservices must be disabled in the main synapse process" @@ -159,10 +156,8 @@ def start(config_options): ps = AppserviceServer( config.server_name, - db_config=config.database_config, config=config, version_string="Synapse/" + get_version_string(synapse), - database_engine=database_engine, ) setup_logging(ps, config, use_worker_options=True) diff --git a/synapse/app/client_reader.py b/synapse/app/client_reader.py index dadb487d5f..3edfe19567 100644 --- a/synapse/app/client_reader.py +++ b/synapse/app/client_reader.py @@ -62,7 +62,6 @@ from synapse.rest.client.v2_alpha.keys import KeyChangesServlet, KeyQueryServlet from synapse.rest.client.v2_alpha.register import RegisterRestServlet from synapse.rest.client.versions import VersionsRestServlet from synapse.server import HomeServer -from synapse.storage.engines import create_engine from synapse.util.httpresourcetree import create_resource_tree from synapse.util.manhole import manhole from synapse.util.versionstring import get_version_string @@ -181,14 +180,10 @@ def start(config_options): events.USE_FROZEN_DICTS = config.use_frozen_dicts - database_engine = create_engine(config.database_config) - ss = ClientReaderServer( config.server_name, - db_config=config.database_config, config=config, version_string="Synapse/" + get_version_string(synapse), - database_engine=database_engine, ) setup_logging(ss, config, use_worker_options=True) diff --git a/synapse/app/event_creator.py b/synapse/app/event_creator.py index d110599a35..d0ddbe38fc 100644 --- a/synapse/app/event_creator.py +++ b/synapse/app/event_creator.py @@ -57,7 +57,6 @@ from synapse.rest.client.v1.room import ( ) from synapse.server import HomeServer from synapse.storage.data_stores.main.user_directory import UserDirectoryStore -from synapse.storage.engines import create_engine from synapse.util.httpresourcetree import create_resource_tree from synapse.util.manhole import manhole from synapse.util.versionstring import get_version_string @@ -180,14 +179,10 @@ def start(config_options): events.USE_FROZEN_DICTS = config.use_frozen_dicts - database_engine = create_engine(config.database_config) - ss = EventCreatorServer( config.server_name, - db_config=config.database_config, config=config, version_string="Synapse/" + get_version_string(synapse), - database_engine=database_engine, ) setup_logging(ss, config, use_worker_options=True) diff --git a/synapse/app/federation_reader.py b/synapse/app/federation_reader.py index 418c086254..311523e0ed 100644 --- a/synapse/app/federation_reader.py +++ b/synapse/app/federation_reader.py @@ -46,7 +46,6 @@ from synapse.replication.slave.storage.transactions import SlavedTransactionStor from synapse.replication.tcp.client import ReplicationClientHandler from synapse.rest.key.v2 import KeyApiV2Resource from synapse.server import HomeServer -from synapse.storage.engines import create_engine from synapse.util.httpresourcetree import create_resource_tree from synapse.util.manhole import manhole from synapse.util.versionstring import get_version_string @@ -162,14 +161,10 @@ def start(config_options): events.USE_FROZEN_DICTS = config.use_frozen_dicts - database_engine = create_engine(config.database_config) - ss = FederationReaderServer( config.server_name, - db_config=config.database_config, config=config, version_string="Synapse/" + get_version_string(synapse), - database_engine=database_engine, ) setup_logging(ss, config, use_worker_options=True) diff --git a/synapse/app/federation_sender.py b/synapse/app/federation_sender.py index f24920a7d6..83c436229c 100644 --- a/synapse/app/federation_sender.py +++ b/synapse/app/federation_sender.py @@ -41,7 +41,6 @@ from synapse.replication.tcp.client import ReplicationClientHandler from synapse.replication.tcp.streams._base import ReceiptsStream from synapse.server import HomeServer from synapse.storage.database import Database -from synapse.storage.engines import create_engine from synapse.types import ReadReceipt from synapse.util.async_helpers import Linearizer from synapse.util.httpresourcetree import create_resource_tree @@ -174,8 +173,6 @@ def start(config_options): events.USE_FROZEN_DICTS = config.use_frozen_dicts - database_engine = create_engine(config.database_config) - if config.send_federation: sys.stderr.write( "\nThe send_federation must be disabled in the main synapse process" @@ -190,10 +187,8 @@ def start(config_options): ss = FederationSenderServer( config.server_name, - db_config=config.database_config, config=config, version_string="Synapse/" + get_version_string(synapse), - database_engine=database_engine, ) setup_logging(ss, config, use_worker_options=True) diff --git a/synapse/app/frontend_proxy.py b/synapse/app/frontend_proxy.py index e647459d0e..30e435eead 100644 --- a/synapse/app/frontend_proxy.py +++ b/synapse/app/frontend_proxy.py @@ -39,7 +39,6 @@ from synapse.replication.slave.storage.registration import SlavedRegistrationSto from synapse.replication.tcp.client import ReplicationClientHandler from synapse.rest.client.v2_alpha._base import client_patterns from synapse.server import HomeServer -from synapse.storage.engines import create_engine from synapse.util.httpresourcetree import create_resource_tree from synapse.util.manhole import manhole from synapse.util.versionstring import get_version_string @@ -234,14 +233,10 @@ def start(config_options): events.USE_FROZEN_DICTS = config.use_frozen_dicts - database_engine = create_engine(config.database_config) - ss = FrontendProxyServer( config.server_name, - db_config=config.database_config, config=config, version_string="Synapse/" + get_version_string(synapse), - database_engine=database_engine, ) setup_logging(ss, config, use_worker_options=True) diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py index df65d0a989..79341784c5 100644 --- a/synapse/app/homeserver.py +++ b/synapse/app/homeserver.py @@ -69,7 +69,7 @@ from synapse.rest.media.v0.content_repository import ContentRepoResource from synapse.rest.well_known import WellKnownResource from synapse.server import HomeServer from synapse.storage import DataStore -from synapse.storage.engines import IncorrectDatabaseSetup, create_engine +from synapse.storage.engines import IncorrectDatabaseSetup from synapse.storage.prepare_database import UpgradeDatabaseException from synapse.util.caches import CACHE_SIZE_FACTOR from synapse.util.httpresourcetree import create_resource_tree @@ -328,15 +328,10 @@ def setup(config_options): events.USE_FROZEN_DICTS = config.use_frozen_dicts - database_engine = create_engine(config.database_config) - config.database_config["args"]["cp_openfun"] = database_engine.on_new_connection - hs = SynapseHomeServer( config.server_name, - db_config=config.database_config, config=config, version_string="Synapse/" + get_version_string(synapse), - database_engine=database_engine, ) synapse.config.logger.setup_logging(hs, config, use_worker_options=False) diff --git a/synapse/app/media_repository.py b/synapse/app/media_repository.py index 2c6dd3ef02..4c80f257e2 100644 --- a/synapse/app/media_repository.py +++ b/synapse/app/media_repository.py @@ -40,7 +40,6 @@ from synapse.rest.admin import register_servlets_for_media_repo from synapse.rest.media.v0.content_repository import ContentRepoResource from synapse.server import HomeServer from synapse.storage.data_stores.main.media_repository import MediaRepositoryStore -from synapse.storage.engines import create_engine from synapse.util.httpresourcetree import create_resource_tree from synapse.util.manhole import manhole from synapse.util.versionstring import get_version_string @@ -157,14 +156,10 @@ def start(config_options): events.USE_FROZEN_DICTS = config.use_frozen_dicts - database_engine = create_engine(config.database_config) - ss = MediaRepositoryServer( config.server_name, - db_config=config.database_config, config=config, version_string="Synapse/" + get_version_string(synapse), - database_engine=database_engine, ) setup_logging(ss, config, use_worker_options=True) diff --git a/synapse/app/pusher.py b/synapse/app/pusher.py index 01a5ffc363..e157cbf64b 100644 --- a/synapse/app/pusher.py +++ b/synapse/app/pusher.py @@ -36,7 +36,6 @@ from synapse.replication.slave.storage.receipts import SlavedReceiptsStore from synapse.replication.tcp.client import ReplicationClientHandler from synapse.server import HomeServer from synapse.storage import DataStore -from synapse.storage.engines import create_engine from synapse.util.httpresourcetree import create_resource_tree from synapse.util.manhole import manhole from synapse.util.versionstring import get_version_string @@ -198,14 +197,10 @@ def start(config_options): # Force the pushers to start since they will be disabled in the main config config.start_pushers = True - database_engine = create_engine(config.database_config) - ps = PusherServer( config.server_name, - db_config=config.database_config, config=config, version_string="Synapse/" + get_version_string(synapse), - database_engine=database_engine, ) setup_logging(ps, config, use_worker_options=True) diff --git a/synapse/app/synchrotron.py b/synapse/app/synchrotron.py index 288ee64b42..dd2132e608 100644 --- a/synapse/app/synchrotron.py +++ b/synapse/app/synchrotron.py @@ -55,7 +55,6 @@ from synapse.rest.client.v1.room import RoomInitialSyncRestServlet from synapse.rest.client.v2_alpha import sync from synapse.server import HomeServer from synapse.storage.data_stores.main.presence import UserPresenceState -from synapse.storage.engines import create_engine from synapse.util.httpresourcetree import create_resource_tree from synapse.util.manhole import manhole from synapse.util.stringutils import random_string @@ -437,14 +436,10 @@ def start(config_options): synapse.events.USE_FROZEN_DICTS = config.use_frozen_dicts - database_engine = create_engine(config.database_config) - ss = SynchrotronServer( config.server_name, - db_config=config.database_config, config=config, version_string="Synapse/" + get_version_string(synapse), - database_engine=database_engine, application_service_handler=SynchrotronApplicationService(), ) diff --git a/synapse/app/user_dir.py b/synapse/app/user_dir.py index c01fb34a9b..1257098f92 100644 --- a/synapse/app/user_dir.py +++ b/synapse/app/user_dir.py @@ -44,7 +44,6 @@ from synapse.rest.client.v2_alpha import user_directory from synapse.server import HomeServer from synapse.storage.data_stores.main.user_directory import UserDirectoryStore from synapse.storage.database import Database -from synapse.storage.engines import create_engine from synapse.util.caches.stream_change_cache import StreamChangeCache from synapse.util.httpresourcetree import create_resource_tree from synapse.util.manhole import manhole @@ -200,8 +199,6 @@ def start(config_options): events.USE_FROZEN_DICTS = config.use_frozen_dicts - database_engine = create_engine(config.database_config) - if config.update_user_directory: sys.stderr.write( "\nThe update_user_directory must be disabled in the main synapse process" @@ -216,10 +213,8 @@ def start(config_options): ss = UserDirectoryServer( config.server_name, - db_config=config.database_config, config=config, version_string="Synapse/" + get_version_string(synapse), - database_engine=database_engine, ) setup_logging(ss, config, use_worker_options=True) diff --git a/synapse/server.py b/synapse/server.py index 2db3dab221..a8b5459ff3 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -97,6 +97,7 @@ from synapse.server_notices.worker_server_notices_sender import ( ) from synapse.state import StateHandler, StateResolutionHandler from synapse.storage import DataStores, Storage +from synapse.storage.engines import create_engine from synapse.streams.events import EventSources from synapse.util import Clock from synapse.util.distributor import Distributor @@ -209,7 +210,7 @@ class HomeServer(object): # instantiated during setup() for future return by get_datastore() DATASTORE_CLASS = abc.abstractproperty() - def __init__(self, hostname, reactor=None, **kwargs): + def __init__(self, hostname, config, reactor=None, **kwargs): """ Args: hostname : The hostname for the server. @@ -219,6 +220,7 @@ class HomeServer(object): self._reactor = reactor self.hostname = hostname + self.config = config self._building = {} self._listening_services = [] self.start_time = None @@ -229,6 +231,12 @@ class HomeServer(object): self.admin_redaction_ratelimiter = Ratelimiter() self.registration_ratelimiter = Ratelimiter() + self.database_engine = create_engine(config.database_config) + config.database_config.setdefault("args", {})[ + "cp_openfun" + ] = self.database_engine.on_new_connection + self.db_config = config.database_config + self.datastores = None # Other kwargs are explicit dependencies diff --git a/tests/test_types.py b/tests/test_types.py index 9ab5f829b0..8d97c751ea 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -17,18 +17,15 @@ from synapse.api.errors import SynapseError from synapse.types import GroupID, RoomAlias, UserID, map_username_to_mxid_localpart from tests import unittest -from tests.utils import TestHomeServer - -mock_homeserver = TestHomeServer(hostname="my.domain") -class UserIDTestCase(unittest.TestCase): +class UserIDTestCase(unittest.HomeserverTestCase): def test_parse(self): - user = UserID.from_string("@1234abcd:my.domain") + user = UserID.from_string("@1234abcd:test") self.assertEquals("1234abcd", user.localpart) - self.assertEquals("my.domain", user.domain) - self.assertEquals(True, mock_homeserver.is_mine(user)) + self.assertEquals("test", user.domain) + self.assertEquals(True, self.hs.is_mine(user)) def test_pase_empty(self): with self.assertRaises(SynapseError): @@ -48,13 +45,13 @@ class UserIDTestCase(unittest.TestCase): self.assertTrue(userA != userB) -class RoomAliasTestCase(unittest.TestCase): +class RoomAliasTestCase(unittest.HomeserverTestCase): def test_parse(self): - room = RoomAlias.from_string("#channel:my.domain") + room = RoomAlias.from_string("#channel:test") self.assertEquals("channel", room.localpart) - self.assertEquals("my.domain", room.domain) - self.assertEquals(True, mock_homeserver.is_mine(room)) + self.assertEquals("test", room.domain) + self.assertEquals(True, self.hs.is_mine(room)) def test_build(self): room = RoomAlias("channel", "my.domain") diff --git a/tests/utils.py b/tests/utils.py index c57da59191..585f305b9a 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -260,9 +260,7 @@ def setup_test_homeserver( hs = homeserverToUse( name, config=config, - db_config=config.database_config, version_string="Synapse/tests", - database_engine=db_engine, tls_server_context_factory=Mock(), tls_client_options_factory=Mock(), reactor=reactor, From 67c991b78fd38df687f28bb22f66f34fe2c5bb9f Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 10 Dec 2019 13:32:34 +0000 Subject: [PATCH 0657/1623] Fix upgrade db script --- scripts-dev/update_database | 33 ++++++--------------------------- 1 file changed, 6 insertions(+), 27 deletions(-) diff --git a/scripts-dev/update_database b/scripts-dev/update_database index 1776d202c5..23017c21f8 100755 --- a/scripts-dev/update_database +++ b/scripts-dev/update_database @@ -26,7 +26,6 @@ from synapse.config.homeserver import HomeServerConfig from synapse.metrics.background_process_metrics import run_as_background_process from synapse.server import HomeServer from synapse.storage import DataStore -from synapse.storage.engines import create_engine from synapse.storage.prepare_database import prepare_database logger = logging.getLogger("update_database") @@ -35,21 +34,11 @@ logger = logging.getLogger("update_database") class MockHomeserver(HomeServer): DATASTORE_CLASS = DataStore - def __init__(self, config, database_engine, db_conn, **kwargs): + def __init__(self, config, **kwargs): super(MockHomeserver, self).__init__( - config.server_name, - reactor=reactor, - config=config, - database_engine=database_engine, - **kwargs + config.server_name, reactor=reactor, config=config, **kwargs ) - self.database_engine = database_engine - self.db_conn = db_conn - - def get_db_conn(self): - return self.db_conn - if __name__ == "__main__": parser = argparse.ArgumentParser( @@ -85,24 +74,14 @@ if __name__ == "__main__": config = HomeServerConfig() config.parse_config_dict(hs_config, "", "") - # Create the database engine and a connection to it. - database_engine = create_engine(config.database_config) - db_conn = database_engine.module.connect( - **{ - k: v - for k, v in config.database_config.get("args", {}).items() - if not k.startswith("cp_") - } - ) + # Instantiate and initialise the homeserver object. + hs = MockHomeserver(config) + db_conn = hs.get_db_conn() # Update the database to the latest schema. - prepare_database(db_conn, database_engine, config=config) + prepare_database(db_conn, hs.database_engine, config=config) db_conn.commit() - # Instantiate and initialise the homeserver object. - hs = MockHomeserver( - config, database_engine, db_conn, db_config=config.database_config, - ) # setup instantiates the store within the homeserver object. hs.setup() store = hs.get_datastore() From d630c8234928c008e12c79ebef10cee570779fa5 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 10 Dec 2019 13:34:17 +0000 Subject: [PATCH 0658/1623] Newsfile --- changelog.d/6511.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6511.misc diff --git a/changelog.d/6511.misc b/changelog.d/6511.misc new file mode 100644 index 0000000000..19ce435e68 --- /dev/null +++ b/changelog.d/6511.misc @@ -0,0 +1 @@ +Move database config from apps into HomeServer object. From 257ef2c727abbb289c460b930e8bac18ab80e053 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 10 Dec 2019 11:13:15 +0000 Subject: [PATCH 0659/1623] Port handlers.account_validity to async/await. --- synapse/handlers/account_data.py | 2 +- synapse/handlers/account_validity.py | 86 ++++++++++----------- tests/rest/client/v2_alpha/test_register.py | 3 +- 3 files changed, 42 insertions(+), 49 deletions(-) diff --git a/synapse/handlers/account_data.py b/synapse/handlers/account_data.py index fe44d62a1c..20ec1ca01b 100644 --- a/synapse/handlers/account_data.py +++ b/synapse/handlers/account_data.py @@ -25,7 +25,7 @@ class AccountDataEventSource(object): user_id = user.to_string() last_stream_id = from_key - current_stream_id = await self.store.get_max_account_data_stream_id() + current_stream_id = self.store.get_max_account_data_stream_id() results = [] tags = await self.store.get_updated_tags(user_id, last_stream_id) diff --git a/synapse/handlers/account_validity.py b/synapse/handlers/account_validity.py index d04e0fe576..829f52eca1 100644 --- a/synapse/handlers/account_validity.py +++ b/synapse/handlers/account_validity.py @@ -18,8 +18,7 @@ import email.utils import logging from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText - -from twisted.internet import defer +from typing import List from synapse.api.errors import StoreError from synapse.logging.context import make_deferred_yieldable @@ -78,42 +77,39 @@ class AccountValidityHandler(object): # run as a background process to make sure that the database transactions # have a logcontext to report to return run_as_background_process( - "send_renewals", self.send_renewal_emails + "send_renewals", self._send_renewal_emails ) self.clock.looping_call(send_emails, 30 * 60 * 1000) - @defer.inlineCallbacks - def send_renewal_emails(self): + async def _send_renewal_emails(self): """Gets the list of users whose account is expiring in the amount of time configured in the ``renew_at`` parameter from the ``account_validity`` configuration, and sends renewal emails to all of these users as long as they have an email 3PID attached to their account. """ - expiring_users = yield self.store.get_users_expiring_soon() + expiring_users = await self.store.get_users_expiring_soon() if expiring_users: for user in expiring_users: - yield self._send_renewal_email( + await self._send_renewal_email( user_id=user["user_id"], expiration_ts=user["expiration_ts_ms"] ) - @defer.inlineCallbacks - def send_renewal_email_to_user(self, user_id): - expiration_ts = yield self.store.get_expiration_ts_for_user(user_id) - yield self._send_renewal_email(user_id, expiration_ts) + async def send_renewal_email_to_user(self, user_id: str): + expiration_ts = await self.store.get_expiration_ts_for_user(user_id) + await self._send_renewal_email(user_id, expiration_ts) - @defer.inlineCallbacks - def _send_renewal_email(self, user_id, expiration_ts): + async def _send_renewal_email(self, user_id: str, expiration_ts: int): """Sends out a renewal email to every email address attached to the given user with a unique link allowing them to renew their account. Args: - user_id (str): ID of the user to send email(s) to. - expiration_ts (int): Timestamp in milliseconds for the expiration date of + user_id: ID of the user to send email(s) to. + expiration_ts: Timestamp in milliseconds for the expiration date of this user's account (used in the email templates). """ - addresses = yield self._get_email_addresses_for_user(user_id) + addresses = await self._get_email_addresses_for_user(user_id) # Stop right here if the user doesn't have at least one email address. # In this case, they will have to ask their server admin to renew their @@ -125,7 +121,7 @@ class AccountValidityHandler(object): return try: - user_display_name = yield self.store.get_profile_displayname( + user_display_name = await self.store.get_profile_displayname( UserID.from_string(user_id).localpart ) if user_display_name is None: @@ -133,7 +129,7 @@ class AccountValidityHandler(object): except StoreError: user_display_name = user_id - renewal_token = yield self._get_renewal_token(user_id) + renewal_token = await self._get_renewal_token(user_id) url = "%s_matrix/client/unstable/account_validity/renew?token=%s" % ( self.hs.config.public_baseurl, renewal_token, @@ -165,7 +161,7 @@ class AccountValidityHandler(object): logger.info("Sending renewal email to %s", address) - yield make_deferred_yieldable( + await make_deferred_yieldable( self.sendmail( self.hs.config.email_smtp_host, self._raw_from, @@ -180,19 +176,18 @@ class AccountValidityHandler(object): ) ) - yield self.store.set_renewal_mail_status(user_id=user_id, email_sent=True) + await self.store.set_renewal_mail_status(user_id=user_id, email_sent=True) - @defer.inlineCallbacks - def _get_email_addresses_for_user(self, user_id): + async def _get_email_addresses_for_user(self, user_id: str) -> List[str]: """Retrieve the list of email addresses attached to a user's account. Args: - user_id (str): ID of the user to lookup email addresses for. + user_id: ID of the user to lookup email addresses for. Returns: - defer.Deferred[list[str]]: Email addresses for this account. + Email addresses for this account. """ - threepids = yield self.store.user_get_threepids(user_id) + threepids = await self.store.user_get_threepids(user_id) addresses = [] for threepid in threepids: @@ -201,16 +196,15 @@ class AccountValidityHandler(object): return addresses - @defer.inlineCallbacks - def _get_renewal_token(self, user_id): + async def _get_renewal_token(self, user_id: str) -> str: """Generates a 32-byte long random string that will be inserted into the user's renewal email's unique link, then saves it into the database. Args: - user_id (str): ID of the user to generate a string for. + user_id: ID of the user to generate a string for. Returns: - defer.Deferred[str]: The generated string. + The generated string. Raises: StoreError(500): Couldn't generate a unique string after 5 attempts. @@ -219,52 +213,52 @@ class AccountValidityHandler(object): while attempts < 5: try: renewal_token = stringutils.random_string(32) - yield self.store.set_renewal_token_for_user(user_id, renewal_token) + await self.store.set_renewal_token_for_user(user_id, renewal_token) return renewal_token except StoreError: attempts += 1 raise StoreError(500, "Couldn't generate a unique string as refresh string.") - @defer.inlineCallbacks - def renew_account(self, renewal_token): + async def renew_account(self, renewal_token: str) -> bool: """Renews the account attached to a given renewal token by pushing back the expiration date by the current validity period in the server's configuration. Args: - renewal_token (str): Token sent with the renewal request. + renewal_token: Token sent with the renewal request. Returns: - bool: Whether the provided token is valid. + Whether the provided token is valid. """ try: - user_id = yield self.store.get_user_from_renewal_token(renewal_token) + user_id = await self.store.get_user_from_renewal_token(renewal_token) except StoreError: - defer.returnValue(False) + return False logger.debug("Renewing an account for user %s", user_id) - yield self.renew_account_for_user(user_id) + await self.renew_account_for_user(user_id) - defer.returnValue(True) + return True - @defer.inlineCallbacks - def renew_account_for_user(self, user_id, expiration_ts=None, email_sent=False): + async def renew_account_for_user( + self, user_id: str, expiration_ts: int = None, email_sent: bool = False + ) -> int: """Renews the account attached to a given user by pushing back the expiration date by the current validity period in the server's configuration. Args: - renewal_token (str): Token sent with the renewal request. - expiration_ts (int): New expiration date. Defaults to now + validity period. - email_sent (bool): Whether an email has been sent for this validity period. + renewal_token: Token sent with the renewal request. + expiration_ts: New expiration date. Defaults to now + validity period. + email_sen: Whether an email has been sent for this validity period. Defaults to False. Returns: - defer.Deferred[int]: New expiration date for this account, as a timestamp - in milliseconds since epoch. + New expiration date for this account, as a timestamp in + milliseconds since epoch. """ if expiration_ts is None: expiration_ts = self.clock.time_msec() + self._account_validity.period - yield self.store.set_account_validity_for_user( + await self.store.set_account_validity_for_user( user_id=user_id, expiration_ts=expiration_ts, email_sent=email_sent ) diff --git a/tests/rest/client/v2_alpha/test_register.py b/tests/rest/client/v2_alpha/test_register.py index c0d0d2b44e..d0c997e385 100644 --- a/tests/rest/client/v2_alpha/test_register.py +++ b/tests/rest/client/v2_alpha/test_register.py @@ -391,9 +391,8 @@ class AccountValidityRenewalByEmailTestCase(unittest.HomeserverTestCase): # Email config. self.email_attempts = [] - def sendmail(*args, **kwargs): + async def sendmail(*args, **kwargs): self.email_attempts.append((args, kwargs)) - return config["email"] = { "enable_notifs": True, From 3f97b4c16bbe8a32fc465bd59421f3ba879d2124 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 10 Dec 2019 11:17:13 +0000 Subject: [PATCH 0660/1623] Newsfile --- changelog.d/6504.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6504.misc diff --git a/changelog.d/6504.misc b/changelog.d/6504.misc new file mode 100644 index 0000000000..7c873459af --- /dev/null +++ b/changelog.d/6504.misc @@ -0,0 +1 @@ +Port handlers.account_data and handlers.account_validity to async/await. From 424fd58237d4a40c6f94772be136298d326fcd69 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 10 Dec 2019 15:09:45 +0000 Subject: [PATCH 0661/1623] Remove redundant code from event authorisation implementation. (#6502) --- changelog.d/6502.removal | 1 + synapse/event_auth.py | 8 ++------ 2 files changed, 3 insertions(+), 6 deletions(-) create mode 100644 changelog.d/6502.removal diff --git a/changelog.d/6502.removal b/changelog.d/6502.removal new file mode 100644 index 0000000000..0b72261d58 --- /dev/null +++ b/changelog.d/6502.removal @@ -0,0 +1 @@ +Remove redundant code from event authorisation implementation. diff --git a/synapse/event_auth.py b/synapse/event_auth.py index ec3243b27b..c940b84470 100644 --- a/synapse/event_auth.py +++ b/synapse/event_auth.py @@ -42,6 +42,8 @@ def check(room_version, event, auth_events, do_sig_check=True, do_size_check=Tru Returns: if the auth checks pass. """ + assert isinstance(auth_events, dict) + if do_size_check: _check_size_limits(event) @@ -74,12 +76,6 @@ def check(room_version, event, auth_events, do_sig_check=True, do_size_check=Tru if not event.signatures.get(event_id_domain): raise AuthError(403, "Event not signed by sending server") - if auth_events is None: - # Oh, we don't know what the state of the room was, so we - # are trusting that this is allowed (at least for now) - logger.warning("Trusting event: %s", event.event_id) - return - if event.type == EventTypes.Create: sender_domain = get_domain_from_id(event.sender) room_id_domain = get_domain_from_id(event.room_id) From c3dda2874d78790525f47e502aaed22b64961873 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 10 Dec 2019 16:22:00 +0000 Subject: [PATCH 0662/1623] Refactor get_events_from_store_or_dest to return a dict (#6501) There was a bunch of unnecessary conversion back and forth between dict and list going on here. We can simplify a bunch of the code. --- changelog.d/6501.misc | 1 + synapse/federation/federation_client.py | 44 +++++++++---------------- 2 files changed, 16 insertions(+), 29 deletions(-) create mode 100644 changelog.d/6501.misc diff --git a/changelog.d/6501.misc b/changelog.d/6501.misc new file mode 100644 index 0000000000..255f45a9c3 --- /dev/null +++ b/changelog.d/6501.misc @@ -0,0 +1 @@ +Refactor get_events_from_store_or_dest to return a dict. diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index 709449c9e3..73e1dda6a3 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -18,8 +18,6 @@ import copy import itertools import logging -from six.moves import range - from prometheus_client import Counter from twisted.internet import defer @@ -41,7 +39,7 @@ from synapse.events import builder, room_version_to_event_format from synapse.federation.federation_base import FederationBase, event_from_pdu_json from synapse.logging.context import make_deferred_yieldable, run_in_background from synapse.logging.utils import log_function -from synapse.util import unwrapFirstError +from synapse.util import batch_iter, unwrapFirstError from synapse.util.caches.expiringcache import ExpiringCache from synapse.util.retryutils import NotRetryingDestination @@ -331,10 +329,12 @@ class FederationClient(FederationBase): state_event_ids = result["pdu_ids"] auth_event_ids = result.get("auth_chain_ids", []) - fetched_events, failed_to_fetch = yield self.get_events_from_store_or_dest( - destination, room_id, set(state_event_ids + auth_event_ids) + desired_events = set(state_event_ids + auth_event_ids) + event_map = yield self.get_events_from_store_or_dest( + destination, room_id, desired_events ) + failed_to_fetch = desired_events - event_map.keys() if failed_to_fetch: logger.warning( "Failed to fetch missing state/auth events for %s: %s", @@ -342,8 +342,6 @@ class FederationClient(FederationBase): failed_to_fetch, ) - event_map = {ev.event_id: ev for ev in fetched_events} - pdus = [event_map[e_id] for e_id in state_event_ids if e_id in event_map] auth_chain = [event_map[e_id] for e_id in auth_event_ids if e_id in event_map] @@ -358,23 +356,18 @@ class FederationClient(FederationBase): Args: destination (str) room_id (str) - event_ids (list) + event_ids (Iterable[str]) Returns: - Deferred: A deferred resolving to a 2-tuple where the first is a list of - events and the second is a list of event ids that we failed to fetch. + Deferred[dict[str, EventBase]]: A deferred resolving to a map + from event_id to event """ - seen_events = yield self.store.get_events(event_ids, allow_rejected=True) - signed_events = list(seen_events.values()) + fetched_events = yield self.store.get_events(event_ids, allow_rejected=True) - failed_to_fetch = set() - - missing_events = set(event_ids) - for k in seen_events: - missing_events.discard(k) + missing_events = set(event_ids) - fetched_events.keys() if not missing_events: - return signed_events, failed_to_fetch + return fetched_events logger.debug( "Fetching unknown state/auth events %s for room %s", @@ -384,11 +377,8 @@ class FederationClient(FederationBase): room_version = yield self.store.get_room_version(room_id) - batch_size = 20 - missing_events = list(missing_events) - for i in range(0, len(missing_events), batch_size): - batch = set(missing_events[i : i + batch_size]) - + # XXX 20 requests at once? really? + for batch in batch_iter(missing_events, 20): deferreds = [ run_in_background( self.get_pdu, @@ -404,13 +394,9 @@ class FederationClient(FederationBase): ) for success, result in res: if success and result: - signed_events.append(result) - batch.discard(result.event_id) + fetched_events[result.event_id] = result - # We removed all events we successfully fetched from `batch` - failed_to_fetch.update(batch) - - return signed_events, failed_to_fetch + return fetched_events @defer.inlineCallbacks @log_function From 40eda849338b6e47a5804b4cf7000e9d2417c4d8 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 10 Dec 2019 16:22:29 +0000 Subject: [PATCH 0663/1623] Fix race which caused deleted devices to reappear (#6514) Stop the `update_client_ips` background job from recreating deleted devices. --- changelog.d/6514.bugfix | 1 + .../storage/data_stores/main/client_ips.py | 8 +-- tests/storage/test_client_ips.py | 49 +++++++++++-------- 3 files changed, 35 insertions(+), 23 deletions(-) create mode 100644 changelog.d/6514.bugfix diff --git a/changelog.d/6514.bugfix b/changelog.d/6514.bugfix new file mode 100644 index 0000000000..6dc1985c24 --- /dev/null +++ b/changelog.d/6514.bugfix @@ -0,0 +1 @@ +Fix race which occasionally caused deleted devices to reappear. diff --git a/synapse/storage/data_stores/main/client_ips.py b/synapse/storage/data_stores/main/client_ips.py index 320c5b0f07..add3037b69 100644 --- a/synapse/storage/data_stores/main/client_ips.py +++ b/synapse/storage/data_stores/main/client_ips.py @@ -451,16 +451,18 @@ class ClientIpStore(ClientIpBackgroundUpdateStore): # Technically an access token might not be associated with # a device so we need to check. if device_id: - self.db.simple_upsert_txn( + # this is always an update rather than an upsert: the row should + # already exist, and if it doesn't, that may be because it has been + # deleted, and we don't want to re-create it. + self.db.simple_update_txn( txn, table="devices", keyvalues={"user_id": user_id, "device_id": device_id}, - values={ + updatevalues={ "user_agent": user_agent, "last_seen": last_seen, "ip": ip, }, - lock=False, ) except Exception as e: # Failed to upsert, log and continue diff --git a/tests/storage/test_client_ips.py b/tests/storage/test_client_ips.py index fc279340d4..bf674dd184 100644 --- a/tests/storage/test_client_ips.py +++ b/tests/storage/test_client_ips.py @@ -37,9 +37,13 @@ class ClientIpStoreTestCase(unittest.HomeserverTestCase): self.reactor.advance(12345678) user_id = "@user:id" + device_id = "MY_DEVICE" + + # Insert a user IP + self.get_success(self.store.store_device(user_id, device_id, "display name",)) self.get_success( self.store.insert_client_ip( - user_id, "access_token", "ip", "user_agent", "device_id" + user_id, "access_token", "ip", "user_agent", device_id ) ) @@ -47,14 +51,14 @@ class ClientIpStoreTestCase(unittest.HomeserverTestCase): self.reactor.advance(10) result = self.get_success( - self.store.get_last_client_ip_by_device(user_id, "device_id") + self.store.get_last_client_ip_by_device(user_id, device_id) ) - r = result[(user_id, "device_id")] + r = result[(user_id, device_id)] self.assertDictContainsSubset( { "user_id": user_id, - "device_id": "device_id", + "device_id": device_id, "ip": "ip", "user_agent": "user_agent", "last_seen": 12345678000, @@ -209,14 +213,16 @@ class ClientIpStoreTestCase(unittest.HomeserverTestCase): self.store.db.updates.do_next_background_update(100), by=0.1 ) - # Insert a user IP user_id = "@user:id" + device_id = "MY_DEVICE" + + # Insert a user IP + self.get_success(self.store.store_device(user_id, device_id, "display name",)) self.get_success( self.store.insert_client_ip( - user_id, "access_token", "ip", "user_agent", "device_id" + user_id, "access_token", "ip", "user_agent", device_id ) ) - # Force persisting to disk self.reactor.advance(200) @@ -224,7 +230,7 @@ class ClientIpStoreTestCase(unittest.HomeserverTestCase): self.get_success( self.store.db.simple_update( table="devices", - keyvalues={"user_id": user_id, "device_id": "device_id"}, + keyvalues={"user_id": user_id, "device_id": device_id}, updatevalues={"last_seen": None, "ip": None, "user_agent": None}, desc="test_devices_last_seen_bg_update", ) @@ -232,14 +238,14 @@ class ClientIpStoreTestCase(unittest.HomeserverTestCase): # We should now get nulls when querying result = self.get_success( - self.store.get_last_client_ip_by_device(user_id, "device_id") + self.store.get_last_client_ip_by_device(user_id, device_id) ) - r = result[(user_id, "device_id")] + r = result[(user_id, device_id)] self.assertDictContainsSubset( { "user_id": user_id, - "device_id": "device_id", + "device_id": device_id, "ip": None, "user_agent": None, "last_seen": None, @@ -272,14 +278,14 @@ class ClientIpStoreTestCase(unittest.HomeserverTestCase): # We should now get the correct result again result = self.get_success( - self.store.get_last_client_ip_by_device(user_id, "device_id") + self.store.get_last_client_ip_by_device(user_id, device_id) ) - r = result[(user_id, "device_id")] + r = result[(user_id, device_id)] self.assertDictContainsSubset( { "user_id": user_id, - "device_id": "device_id", + "device_id": device_id, "ip": "ip", "user_agent": "user_agent", "last_seen": 0, @@ -296,11 +302,14 @@ class ClientIpStoreTestCase(unittest.HomeserverTestCase): self.store.db.updates.do_next_background_update(100), by=0.1 ) - # Insert a user IP user_id = "@user:id" + device_id = "MY_DEVICE" + + # Insert a user IP + self.get_success(self.store.store_device(user_id, device_id, "display name",)) self.get_success( self.store.insert_client_ip( - user_id, "access_token", "ip", "user_agent", "device_id" + user_id, "access_token", "ip", "user_agent", device_id ) ) @@ -324,7 +333,7 @@ class ClientIpStoreTestCase(unittest.HomeserverTestCase): "access_token": "access_token", "ip": "ip", "user_agent": "user_agent", - "device_id": "device_id", + "device_id": device_id, "last_seen": 0, } ], @@ -347,14 +356,14 @@ class ClientIpStoreTestCase(unittest.HomeserverTestCase): # But we should still get the correct values for the device result = self.get_success( - self.store.get_last_client_ip_by_device(user_id, "device_id") + self.store.get_last_client_ip_by_device(user_id, device_id) ) - r = result[(user_id, "device_id")] + r = result[(user_id, device_id)] self.assertDictContainsSubset( { "user_id": user_id, - "device_id": "device_id", + "device_id": device_id, "ip": "ip", "user_agent": "user_agent", "last_seen": 0, From 4947de5a147d4f6a4e60aecac1284714fb64df8a Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Tue, 10 Dec 2019 17:30:16 +0000 Subject: [PATCH 0664/1623] Allow SAML username provider plugins (#6411) --- changelog.d/6411.feature | 1 + docs/saml_mapping_providers.md | 77 ++++++++++++ docs/sample_config.yaml | 59 ++++++--- synapse/config/saml2_config.py | 180 +++++++++++++++++++--------- synapse/handlers/saml_handler.py | 200 ++++++++++++++++++++++++++----- 5 files changed, 414 insertions(+), 103 deletions(-) create mode 100644 changelog.d/6411.feature create mode 100644 docs/saml_mapping_providers.md diff --git a/changelog.d/6411.feature b/changelog.d/6411.feature new file mode 100644 index 0000000000..ebea4a208d --- /dev/null +++ b/changelog.d/6411.feature @@ -0,0 +1 @@ +Allow custom SAML username mapping functinality through an external provider plugin. \ No newline at end of file diff --git a/docs/saml_mapping_providers.md b/docs/saml_mapping_providers.md new file mode 100644 index 0000000000..92f2380488 --- /dev/null +++ b/docs/saml_mapping_providers.md @@ -0,0 +1,77 @@ +# SAML Mapping Providers + +A SAML mapping provider is a Python class (loaded via a Python module) that +works out how to map attributes of a SAML response object to Matrix-specific +user attributes. Details such as user ID localpart, displayname, and even avatar +URLs are all things that can be mapped from talking to a SSO service. + +As an example, a SSO service may return the email address +"john.smith@example.com" for a user, whereas Synapse will need to figure out how +to turn that into a displayname when creating a Matrix user for this individual. +It may choose `John Smith`, or `Smith, John [Example.com]` or any number of +variations. As each Synapse configuration may want something different, this is +where SAML mapping providers come into play. + +## Enabling Providers + +External mapping providers are provided to Synapse in the form of an external +Python module. Retrieve this module from [PyPi](https://pypi.org) or elsewhere, +then tell Synapse where to look for the handler class by editing the +`saml2_config.user_mapping_provider.module` config option. + +`saml2_config.user_mapping_provider.config` allows you to provide custom +configuration options to the module. Check with the module's documentation for +what options it provides (if any). The options listed by default are for the +user mapping provider built in to Synapse. If using a custom module, you should +comment these options out and use those specified by the module instead. + +## Building a Custom Mapping Provider + +A custom mapping provider must specify the following methods: + +* `__init__(self, parsed_config)` + - Arguments: + - `parsed_config` - A configuration object that is the return value of the + `parse_config` method. You should set any configuration options needed by + the module here. +* `saml_response_to_user_attributes(self, saml_response, failures)` + - Arguments: + - `saml_response` - A `saml2.response.AuthnResponse` object to extract user + information from. + - `failures` - An `int` that represents the amount of times the returned + mxid localpart mapping has failed. This should be used + to create a deduplicated mxid localpart which should be + returned instead. For example, if this method returns + `john.doe` as the value of `mxid_localpart` in the returned + dict, and that is already taken on the homeserver, this + method will be called again with the same parameters but + with failures=1. The method should then return a different + `mxid_localpart` value, such as `john.doe1`. + - This method must return a dictionary, which will then be used by Synapse + to build a new user. The following keys are allowed: + * `mxid_localpart` - Required. The mxid localpart of the new user. + * `displayname` - The displayname of the new user. If not provided, will default to + the value of `mxid_localpart`. +* `parse_config(config)` + - This method should have the `@staticmethod` decoration. + - Arguments: + - `config` - A `dict` representing the parsed content of the + `saml2_config.user_mapping_provider.config` homeserver config option. + Runs on homeserver startup. Providers should extract any option values + they need here. + - Whatever is returned will be passed back to the user mapping provider module's + `__init__` method during construction. +* `get_saml_attributes(config)` + - This method should have the `@staticmethod` decoration. + - Arguments: + - `config` - A object resulting from a call to `parse_config`. + - Returns a tuple of two sets. The first set equates to the saml auth + response attributes that are required for the module to function, whereas + the second set consists of those attributes which can be used if available, + but are not necessary. + +## Synapse's Default Provider + +Synapse has a built-in SAML mapping provider if a custom provider isn't +specified in the config. It is located at +[`synapse.handlers.saml_handler.DefaultSamlMappingProvider`](../synapse/handlers/saml_handler.py). diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 10664ae8f7..4d44e631d1 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -1250,33 +1250,58 @@ saml2_config: # #config_path: "CONFDIR/sp_conf.py" - # the lifetime of a SAML session. This defines how long a user has to + # The lifetime of a SAML session. This defines how long a user has to # complete the authentication process, if allow_unsolicited is unset. # The default is 5 minutes. # #saml_session_lifetime: 5m - # The SAML attribute (after mapping via the attribute maps) to use to derive - # the Matrix ID from. 'uid' by default. + # An external module can be provided here as a custom solution to + # mapping attributes returned from a saml provider onto a matrix user. # - #mxid_source_attribute: displayName + user_mapping_provider: + # The custom module's class. Uncomment to use a custom module. + # + #module: mapping_provider.SamlMappingProvider - # The mapping system to use for mapping the saml attribute onto a matrix ID. - # Options include: - # * 'hexencode' (which maps unpermitted characters to '=xx') - # * 'dotreplace' (which replaces unpermitted characters with '.'). - # The default is 'hexencode'. - # - #mxid_mapping: dotreplace + # Custom configuration values for the module. Below options are + # intended for the built-in provider, they should be changed if + # using a custom module. This section will be passed as a Python + # dictionary to the module's `parse_config` method. + # + config: + # The SAML attribute (after mapping via the attribute maps) to use + # to derive the Matrix ID from. 'uid' by default. + # + # Note: This used to be configured by the + # saml2_config.mxid_source_attribute option. If that is still + # defined, its value will be used instead. + # + #mxid_source_attribute: displayName - # In previous versions of synapse, the mapping from SAML attribute to MXID was - # always calculated dynamically rather than stored in a table. For backwards- - # compatibility, we will look for user_ids matching such a pattern before - # creating a new account. + # The mapping system to use for mapping the saml attribute onto a + # matrix ID. + # + # Options include: + # * 'hexencode' (which maps unpermitted characters to '=xx') + # * 'dotreplace' (which replaces unpermitted characters with + # '.'). + # The default is 'hexencode'. + # + # Note: This used to be configured by the + # saml2_config.mxid_mapping option. If that is still defined, its + # value will be used instead. + # + #mxid_mapping: dotreplace + + # In previous versions of synapse, the mapping from SAML attribute to + # MXID was always calculated dynamically rather than stored in a + # table. For backwards- compatibility, we will look for user_ids + # matching such a pattern before creating a new account. # # This setting controls the SAML attribute which will be used for this - # backwards-compatibility lookup. Typically it should be 'uid', but if the - # attribute maps are changed, it may be necessary to change it. + # backwards-compatibility lookup. Typically it should be 'uid', but if + # the attribute maps are changed, it may be necessary to change it. # # The default is 'uid'. # diff --git a/synapse/config/saml2_config.py b/synapse/config/saml2_config.py index c5ea2d43a1..b91414aa35 100644 --- a/synapse/config/saml2_config.py +++ b/synapse/config/saml2_config.py @@ -14,17 +14,19 @@ # See the License for the specific language governing permissions and # limitations under the License. -import re +import logging from synapse.python_dependencies import DependencyException, check_requirements -from synapse.types import ( - map_username_to_mxid_localpart, - mxid_localpart_allowed_characters, -) -from synapse.util.module_loader import load_python_module +from synapse.util.module_loader import load_module, load_python_module from ._base import Config, ConfigError +logger = logging.getLogger(__name__) + +DEFAULT_USER_MAPPING_PROVIDER = ( + "synapse.handlers.saml_handler.DefaultSamlMappingProvider" +) + def _dict_merge(merge_dict, into_dict): """Do a deep merge of two dicts @@ -75,15 +77,69 @@ class SAML2Config(Config): self.saml2_enabled = True - self.saml2_mxid_source_attribute = saml2_config.get( - "mxid_source_attribute", "uid" - ) - self.saml2_grandfathered_mxid_source_attribute = saml2_config.get( "grandfathered_mxid_source_attribute", "uid" ) - saml2_config_dict = self._default_saml_config_dict() + # user_mapping_provider may be None if the key is present but has no value + ump_dict = saml2_config.get("user_mapping_provider") or {} + + # Use the default user mapping provider if not set + ump_dict.setdefault("module", DEFAULT_USER_MAPPING_PROVIDER) + + # Ensure a config is present + ump_dict["config"] = ump_dict.get("config") or {} + + if ump_dict["module"] == DEFAULT_USER_MAPPING_PROVIDER: + # Load deprecated options for use by the default module + old_mxid_source_attribute = saml2_config.get("mxid_source_attribute") + if old_mxid_source_attribute: + logger.warning( + "The config option saml2_config.mxid_source_attribute is deprecated. " + "Please use saml2_config.user_mapping_provider.config" + ".mxid_source_attribute instead." + ) + ump_dict["config"]["mxid_source_attribute"] = old_mxid_source_attribute + + old_mxid_mapping = saml2_config.get("mxid_mapping") + if old_mxid_mapping: + logger.warning( + "The config option saml2_config.mxid_mapping is deprecated. Please " + "use saml2_config.user_mapping_provider.config.mxid_mapping instead." + ) + ump_dict["config"]["mxid_mapping"] = old_mxid_mapping + + # Retrieve an instance of the module's class + # Pass the config dictionary to the module for processing + ( + self.saml2_user_mapping_provider_class, + self.saml2_user_mapping_provider_config, + ) = load_module(ump_dict) + + # Ensure loaded user mapping module has defined all necessary methods + # Note parse_config() is already checked during the call to load_module + required_methods = [ + "get_saml_attributes", + "saml_response_to_user_attributes", + ] + missing_methods = [ + method + for method in required_methods + if not hasattr(self.saml2_user_mapping_provider_class, method) + ] + if missing_methods: + raise ConfigError( + "Class specified by saml2_config." + "user_mapping_provider.module is missing required " + "methods: %s" % (", ".join(missing_methods),) + ) + + # Get the desired saml auth response attributes from the module + saml2_config_dict = self._default_saml_config_dict( + *self.saml2_user_mapping_provider_class.get_saml_attributes( + self.saml2_user_mapping_provider_config + ) + ) _dict_merge( merge_dict=saml2_config.get("sp_config", {}), into_dict=saml2_config_dict ) @@ -103,22 +159,27 @@ class SAML2Config(Config): saml2_config.get("saml_session_lifetime", "5m") ) - mapping = saml2_config.get("mxid_mapping", "hexencode") - try: - self.saml2_mxid_mapper = MXID_MAPPER_MAP[mapping] - except KeyError: - raise ConfigError("%s is not a known mxid_mapping" % (mapping,)) + def _default_saml_config_dict( + self, required_attributes: set, optional_attributes: set + ): + """Generate a configuration dictionary with required and optional attributes that + will be needed to process new user registration - def _default_saml_config_dict(self): + Args: + required_attributes: SAML auth response attributes that are + necessary to function + optional_attributes: SAML auth response attributes that can be used to add + additional information to Synapse user accounts, but are not required + + Returns: + dict: A SAML configuration dictionary + """ import saml2 public_baseurl = self.public_baseurl if public_baseurl is None: raise ConfigError("saml2_config requires a public_baseurl to be set") - required_attributes = {"uid", self.saml2_mxid_source_attribute} - - optional_attributes = {"displayName"} if self.saml2_grandfathered_mxid_source_attribute: optional_attributes.add(self.saml2_grandfathered_mxid_source_attribute) optional_attributes -= required_attributes @@ -207,33 +268,58 @@ class SAML2Config(Config): # #config_path: "%(config_dir_path)s/sp_conf.py" - # the lifetime of a SAML session. This defines how long a user has to + # The lifetime of a SAML session. This defines how long a user has to # complete the authentication process, if allow_unsolicited is unset. # The default is 5 minutes. # #saml_session_lifetime: 5m - # The SAML attribute (after mapping via the attribute maps) to use to derive - # the Matrix ID from. 'uid' by default. + # An external module can be provided here as a custom solution to + # mapping attributes returned from a saml provider onto a matrix user. # - #mxid_source_attribute: displayName + user_mapping_provider: + # The custom module's class. Uncomment to use a custom module. + # + #module: mapping_provider.SamlMappingProvider - # The mapping system to use for mapping the saml attribute onto a matrix ID. - # Options include: - # * 'hexencode' (which maps unpermitted characters to '=xx') - # * 'dotreplace' (which replaces unpermitted characters with '.'). - # The default is 'hexencode'. - # - #mxid_mapping: dotreplace + # Custom configuration values for the module. Below options are + # intended for the built-in provider, they should be changed if + # using a custom module. This section will be passed as a Python + # dictionary to the module's `parse_config` method. + # + config: + # The SAML attribute (after mapping via the attribute maps) to use + # to derive the Matrix ID from. 'uid' by default. + # + # Note: This used to be configured by the + # saml2_config.mxid_source_attribute option. If that is still + # defined, its value will be used instead. + # + #mxid_source_attribute: displayName - # In previous versions of synapse, the mapping from SAML attribute to MXID was - # always calculated dynamically rather than stored in a table. For backwards- - # compatibility, we will look for user_ids matching such a pattern before - # creating a new account. + # The mapping system to use for mapping the saml attribute onto a + # matrix ID. + # + # Options include: + # * 'hexencode' (which maps unpermitted characters to '=xx') + # * 'dotreplace' (which replaces unpermitted characters with + # '.'). + # The default is 'hexencode'. + # + # Note: This used to be configured by the + # saml2_config.mxid_mapping option. If that is still defined, its + # value will be used instead. + # + #mxid_mapping: dotreplace + + # In previous versions of synapse, the mapping from SAML attribute to + # MXID was always calculated dynamically rather than stored in a + # table. For backwards- compatibility, we will look for user_ids + # matching such a pattern before creating a new account. # # This setting controls the SAML attribute which will be used for this - # backwards-compatibility lookup. Typically it should be 'uid', but if the - # attribute maps are changed, it may be necessary to change it. + # backwards-compatibility lookup. Typically it should be 'uid', but if + # the attribute maps are changed, it may be necessary to change it. # # The default is 'uid'. # @@ -241,23 +327,3 @@ class SAML2Config(Config): """ % { "config_dir_path": config_dir_path } - - -DOT_REPLACE_PATTERN = re.compile( - ("[^%s]" % (re.escape("".join(mxid_localpart_allowed_characters)),)) -) - - -def dot_replace_for_mxid(username: str) -> str: - username = username.lower() - username = DOT_REPLACE_PATTERN.sub(".", username) - - # regular mxids aren't allowed to start with an underscore either - username = re.sub("^_", "", username) - return username - - -MXID_MAPPER_MAP = { - "hexencode": map_username_to_mxid_localpart, - "dotreplace": dot_replace_for_mxid, -} diff --git a/synapse/handlers/saml_handler.py b/synapse/handlers/saml_handler.py index cc9e6b9bd0..0082f85c26 100644 --- a/synapse/handlers/saml_handler.py +++ b/synapse/handlers/saml_handler.py @@ -13,20 +13,36 @@ # See the License for the specific language governing permissions and # limitations under the License. import logging +import re +from typing import Tuple import attr import saml2 +import saml2.response from saml2.client import Saml2Client from synapse.api.errors import SynapseError +from synapse.config import ConfigError from synapse.http.servlet import parse_string from synapse.rest.client.v1.login import SSOAuthHandler -from synapse.types import UserID, map_username_to_mxid_localpart +from synapse.types import ( + UserID, + map_username_to_mxid_localpart, + mxid_localpart_allowed_characters, +) from synapse.util.async_helpers import Linearizer logger = logging.getLogger(__name__) +@attr.s +class Saml2SessionData: + """Data we track about SAML2 sessions""" + + # time the session was created, in milliseconds + creation_time = attr.ib() + + class SamlHandler: def __init__(self, hs): self._saml_client = Saml2Client(hs.config.saml2_sp_config) @@ -37,11 +53,14 @@ class SamlHandler: self._datastore = hs.get_datastore() self._hostname = hs.hostname self._saml2_session_lifetime = hs.config.saml2_session_lifetime - self._mxid_source_attribute = hs.config.saml2_mxid_source_attribute self._grandfathered_mxid_source_attribute = ( hs.config.saml2_grandfathered_mxid_source_attribute ) - self._mxid_mapper = hs.config.saml2_mxid_mapper + + # plugin to do custom mapping from saml response to mxid + self._user_mapping_provider = hs.config.saml2_user_mapping_provider_class( + hs.config.saml2_user_mapping_provider_config + ) # identifier for the external_ids table self._auth_provider_id = "saml" @@ -118,22 +137,10 @@ class SamlHandler: remote_user_id = saml2_auth.ava["uid"][0] except KeyError: logger.warning("SAML2 response lacks a 'uid' attestation") - raise SynapseError(400, "uid not in SAML2 response") - - try: - mxid_source = saml2_auth.ava[self._mxid_source_attribute][0] - except KeyError: - logger.warning( - "SAML2 response lacks a '%s' attestation", self._mxid_source_attribute - ) - raise SynapseError( - 400, "%s not in SAML2 response" % (self._mxid_source_attribute,) - ) + raise SynapseError(400, "'uid' not in SAML2 response") self._outstanding_requests_dict.pop(saml2_auth.in_response_to, None) - displayName = saml2_auth.ava.get("displayName", [None])[0] - with (await self._mapping_lock.queue(self._auth_provider_id)): # first of all, check if we already have a mapping for this user logger.info( @@ -173,22 +180,46 @@ class SamlHandler: ) return registered_user_id - # figure out a new mxid for this user - base_mxid_localpart = self._mxid_mapper(mxid_source) + # Map saml response to user attributes using the configured mapping provider + for i in range(1000): + attribute_dict = self._user_mapping_provider.saml_response_to_user_attributes( + saml2_auth, i + ) - suffix = 0 - while True: - localpart = base_mxid_localpart + (str(suffix) if suffix else "") + logger.debug( + "Retrieved SAML attributes from user mapping provider: %s " + "(attempt %d)", + attribute_dict, + i, + ) + + localpart = attribute_dict.get("mxid_localpart") + if not localpart: + logger.error( + "SAML mapping provider plugin did not return a " + "mxid_localpart object" + ) + raise SynapseError(500, "Error parsing SAML2 response") + + displayname = attribute_dict.get("displayname") + + # Check if this mxid already exists if not await self._datastore.get_users_by_id_case_insensitive( UserID(localpart, self._hostname).to_string() ): + # This mxid is free break - suffix += 1 - logger.info("Allocating mxid for new user with localpart %s", localpart) + else: + # Unable to generate a username in 1000 iterations + # Break and return error to the user + raise SynapseError( + 500, "Unable to generate a Matrix ID from the SAML response" + ) registered_user_id = await self._registration_handler.register_user( - localpart=localpart, default_display_name=displayName + localpart=localpart, default_display_name=displayname ) + await self._datastore.record_user_external_id( self._auth_provider_id, remote_user_id, registered_user_id ) @@ -205,9 +236,120 @@ class SamlHandler: del self._outstanding_requests_dict[reqid] -@attr.s -class Saml2SessionData: - """Data we track about SAML2 sessions""" +DOT_REPLACE_PATTERN = re.compile( + ("[^%s]" % (re.escape("".join(mxid_localpart_allowed_characters)),)) +) - # time the session was created, in milliseconds - creation_time = attr.ib() + +def dot_replace_for_mxid(username: str) -> str: + username = username.lower() + username = DOT_REPLACE_PATTERN.sub(".", username) + + # regular mxids aren't allowed to start with an underscore either + username = re.sub("^_", "", username) + return username + + +MXID_MAPPER_MAP = { + "hexencode": map_username_to_mxid_localpart, + "dotreplace": dot_replace_for_mxid, +} + + +@attr.s +class SamlConfig(object): + mxid_source_attribute = attr.ib() + mxid_mapper = attr.ib() + + +class DefaultSamlMappingProvider(object): + __version__ = "0.0.1" + + def __init__(self, parsed_config: SamlConfig): + """The default SAML user mapping provider + + Args: + parsed_config: Module configuration + """ + self._mxid_source_attribute = parsed_config.mxid_source_attribute + self._mxid_mapper = parsed_config.mxid_mapper + + def saml_response_to_user_attributes( + self, saml_response: saml2.response.AuthnResponse, failures: int = 0, + ) -> dict: + """Maps some text from a SAML response to attributes of a new user + + Args: + saml_response: A SAML auth response object + + failures: How many times a call to this function with this + saml_response has resulted in a failure + + Returns: + dict: A dict containing new user attributes. Possible keys: + * mxid_localpart (str): Required. The localpart of the user's mxid + * displayname (str): The displayname of the user + """ + try: + mxid_source = saml_response.ava[self._mxid_source_attribute][0] + except KeyError: + logger.warning( + "SAML2 response lacks a '%s' attestation", self._mxid_source_attribute, + ) + raise SynapseError( + 400, "%s not in SAML2 response" % (self._mxid_source_attribute,) + ) + + # Use the configured mapper for this mxid_source + base_mxid_localpart = self._mxid_mapper(mxid_source) + + # Append suffix integer if last call to this function failed to produce + # a usable mxid + localpart = base_mxid_localpart + (str(failures) if failures else "") + + # Retrieve the display name from the saml response + # If displayname is None, the mxid_localpart will be used instead + displayname = saml_response.ava.get("displayName", [None])[0] + + return { + "mxid_localpart": localpart, + "displayname": displayname, + } + + @staticmethod + def parse_config(config: dict) -> SamlConfig: + """Parse the dict provided by the homeserver's config + Args: + config: A dictionary containing configuration options for this provider + Returns: + SamlConfig: A custom config object for this module + """ + # Parse config options and use defaults where necessary + mxid_source_attribute = config.get("mxid_source_attribute", "uid") + mapping_type = config.get("mxid_mapping", "hexencode") + + # Retrieve the associating mapping function + try: + mxid_mapper = MXID_MAPPER_MAP[mapping_type] + except KeyError: + raise ConfigError( + "saml2_config.user_mapping_provider.config: '%s' is not a valid " + "mxid_mapping value" % (mapping_type,) + ) + + return SamlConfig(mxid_source_attribute, mxid_mapper) + + @staticmethod + def get_saml_attributes(config: SamlConfig) -> Tuple[set, set]: + """Returns the required attributes of a SAML + + Args: + config: A SamlConfig object containing configuration params for this provider + + Returns: + tuple[set,set]: The first set equates to the saml auth response + attributes that are required for the module to function, whereas the + second set consists of those attributes which can be used if + available, but are not necessary + """ + return {"uid", config.mxid_source_attribute}, {"displayName"} From f8bc2ae8830615698ae683cafe4fdddb9a05a1f9 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 10 Dec 2019 17:42:46 +0000 Subject: [PATCH 0665/1623] Move get_state methods into FederationHandler (#6503) This is a non-functional refactor as a precursor to some other work. --- changelog.d/6503.misc | 1 + synapse/federation/federation_client.py | 91 +++------------------ synapse/handlers/federation.py | 101 ++++++++++++++++++++++-- 3 files changed, 107 insertions(+), 86 deletions(-) create mode 100644 changelog.d/6503.misc diff --git a/changelog.d/6503.misc b/changelog.d/6503.misc new file mode 100644 index 0000000000..e4e9a5a3d4 --- /dev/null +++ b/changelog.d/6503.misc @@ -0,0 +1 @@ +Move get_state methods into FederationHandler. diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index 73e1dda6a3..d396e6564f 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -37,9 +37,9 @@ from synapse.api.room_versions import ( ) from synapse.events import builder, room_version_to_event_format from synapse.federation.federation_base import FederationBase, event_from_pdu_json -from synapse.logging.context import make_deferred_yieldable, run_in_background +from synapse.logging.context import make_deferred_yieldable from synapse.logging.utils import log_function -from synapse.util import batch_iter, unwrapFirstError +from synapse.util import unwrapFirstError from synapse.util.caches.expiringcache import ExpiringCache from synapse.util.retryutils import NotRetryingDestination @@ -308,19 +308,12 @@ class FederationClient(FederationBase): return signed_pdu @defer.inlineCallbacks - @log_function - def get_state_for_room(self, destination, room_id, event_id): - """Requests all of the room state at a given event from a remote homeserver. - - Args: - destination (str): The remote homeserver to query for the state. - room_id (str): The id of the room we're interested in. - event_id (str): The id of the event we want the state at. + def get_room_state_ids(self, destination: str, room_id: str, event_id: str): + """Calls the /state_ids endpoint to fetch the state at a particular point + in the room, and the auth events for the given event Returns: - Deferred[Tuple[List[EventBase], List[EventBase]]]: - A list of events in the state, and a list of events in the auth chain - for the given event. + Tuple[List[str], List[str]]: a tuple of (state event_ids, auth event_ids) """ result = yield self.transport_layer.get_room_state_ids( destination, room_id, event_id=event_id @@ -329,74 +322,12 @@ class FederationClient(FederationBase): state_event_ids = result["pdu_ids"] auth_event_ids = result.get("auth_chain_ids", []) - desired_events = set(state_event_ids + auth_event_ids) - event_map = yield self.get_events_from_store_or_dest( - destination, room_id, desired_events - ) + if not isinstance(state_event_ids, list) or not isinstance( + auth_event_ids, list + ): + raise Exception("invalid response from /state_ids") - failed_to_fetch = desired_events - event_map.keys() - if failed_to_fetch: - logger.warning( - "Failed to fetch missing state/auth events for %s: %s", - room_id, - failed_to_fetch, - ) - - pdus = [event_map[e_id] for e_id in state_event_ids if e_id in event_map] - auth_chain = [event_map[e_id] for e_id in auth_event_ids if e_id in event_map] - - auth_chain.sort(key=lambda e: e.depth) - - return pdus, auth_chain - - @defer.inlineCallbacks - def get_events_from_store_or_dest(self, destination, room_id, event_ids): - """Fetch events from a remote destination, checking if we already have them. - - Args: - destination (str) - room_id (str) - event_ids (Iterable[str]) - - Returns: - Deferred[dict[str, EventBase]]: A deferred resolving to a map - from event_id to event - """ - fetched_events = yield self.store.get_events(event_ids, allow_rejected=True) - - missing_events = set(event_ids) - fetched_events.keys() - - if not missing_events: - return fetched_events - - logger.debug( - "Fetching unknown state/auth events %s for room %s", - missing_events, - event_ids, - ) - - room_version = yield self.store.get_room_version(room_id) - - # XXX 20 requests at once? really? - for batch in batch_iter(missing_events, 20): - deferreds = [ - run_in_background( - self.get_pdu, - destinations=[destination], - event_id=e_id, - room_version=room_version, - ) - for e_id in batch - ] - - res = yield make_deferred_yieldable( - defer.DeferredList(deferreds, consumeErrors=True) - ) - for success, result in res: - if success and result: - fetched_events[result.event_id] = result - - return fetched_events + return state_event_ids, auth_event_ids @defer.inlineCallbacks @log_function diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index bc26921768..c0dcf9abf8 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -64,7 +64,7 @@ from synapse.replication.http.federation import ( from synapse.replication.http.membership import ReplicationUserJoinedLeftRoomRestServlet from synapse.state import StateResolutionStore, resolve_events_with_store from synapse.types import UserID, get_domain_from_id -from synapse.util import unwrapFirstError +from synapse.util import batch_iter, unwrapFirstError from synapse.util.async_helpers import Linearizer from synapse.util.distributor import user_joined_room from synapse.util.retryutils import NotRetryingDestination @@ -379,11 +379,9 @@ class FederationHandler(BaseHandler): ( remote_state, got_auth_chain, - ) = yield self.federation_client.get_state_for_room( - origin, room_id, p - ) + ) = yield self._get_state_for_room(origin, room_id, p) - # we want the state *after* p; get_state_for_room returns the + # we want the state *after* p; _get_state_for_room returns the # state *before* p. remote_event = yield self.federation_client.get_pdu( [origin], p, room_version, outlier=True @@ -583,6 +581,97 @@ class FederationHandler(BaseHandler): else: raise + @defer.inlineCallbacks + @log_function + def _get_state_for_room(self, destination, room_id, event_id): + """Requests all of the room state at a given event from a remote homeserver. + + Args: + destination (str): The remote homeserver to query for the state. + room_id (str): The id of the room we're interested in. + event_id (str): The id of the event we want the state at. + + Returns: + Deferred[Tuple[List[EventBase], List[EventBase]]]: + A list of events in the state, and a list of events in the auth chain + for the given event. + """ + ( + state_event_ids, + auth_event_ids, + ) = yield self.federation_client.get_room_state_ids( + destination, room_id, event_id=event_id + ) + + desired_events = set(state_event_ids + auth_event_ids) + event_map = yield self._get_events_from_store_or_dest( + destination, room_id, desired_events + ) + + failed_to_fetch = desired_events - event_map.keys() + if failed_to_fetch: + logger.warning( + "Failed to fetch missing state/auth events for %s: %s", + room_id, + failed_to_fetch, + ) + + pdus = [event_map[e_id] for e_id in state_event_ids if e_id in event_map] + auth_chain = [event_map[e_id] for e_id in auth_event_ids if e_id in event_map] + + auth_chain.sort(key=lambda e: e.depth) + + return pdus, auth_chain + + @defer.inlineCallbacks + def _get_events_from_store_or_dest(self, destination, room_id, event_ids): + """Fetch events from a remote destination, checking if we already have them. + + Args: + destination (str) + room_id (str) + event_ids (Iterable[str]) + + Returns: + Deferred[dict[str, EventBase]]: A deferred resolving to a map + from event_id to event + """ + fetched_events = yield self.store.get_events(event_ids, allow_rejected=True) + + missing_events = set(event_ids) - fetched_events.keys() + + if not missing_events: + return fetched_events + + logger.debug( + "Fetching unknown state/auth events %s for room %s", + missing_events, + event_ids, + ) + + room_version = yield self.store.get_room_version(room_id) + + # XXX 20 requests at once? really? + for batch in batch_iter(missing_events, 20): + deferreds = [ + run_in_background( + self.federation_client.get_pdu, + destinations=[destination], + event_id=e_id, + room_version=room_version, + ) + for e_id in batch + ] + + res = yield make_deferred_yieldable( + defer.DeferredList(deferreds, consumeErrors=True) + ) + for success, result in res: + if success and result: + fetched_events[result.event_id] = result + + return fetched_events + @defer.inlineCallbacks def _process_received_pdu(self, origin, event, state, auth_chain): """ Called when we have a new pdu. We need to do auth checks and put it @@ -723,7 +812,7 @@ class FederationHandler(BaseHandler): state_events = {} events_to_state = {} for e_id in edges: - state, auth = yield self.federation_client.get_state_for_room( + state, auth = yield self._get_state_for_room( destination=dest, room_id=room_id, event_id=e_id ) auth_events.update({a.event_id: a for a in auth}) From 72acca6a32697a53f8f659e641d65dbf25ff6b4d Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 11 Dec 2019 11:46:55 +0000 Subject: [PATCH 0666/1623] Back out change preventing setting null avatar URLs --- changelog.d/6497.bugfix | 2 +- synapse/rest/client/v1/profile.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/changelog.d/6497.bugfix b/changelog.d/6497.bugfix index 92ed08fc40..6a2644d8e6 100644 --- a/changelog.d/6497.bugfix +++ b/changelog.d/6497.bugfix @@ -1 +1 @@ -Fix error message when setting your profile's avatar URL mentioning displaynames, and prevent NoneType avatar_urls. \ No newline at end of file +Fix incorrect error message for invalid requests when setting user's avatar URL. diff --git a/synapse/rest/client/v1/profile.py b/synapse/rest/client/v1/profile.py index 4f47562c1b..e7fe50ed72 100644 --- a/synapse/rest/client/v1/profile.py +++ b/synapse/rest/client/v1/profile.py @@ -15,6 +15,7 @@ """ This module contains REST servlets to do with profile: /profile/ """ +from synapse.api.errors import Codes, SynapseError from synapse.http.servlet import RestServlet, parse_json_object_from_request from synapse.rest.client.v2_alpha._base import client_patterns from synapse.types import UserID @@ -103,12 +104,11 @@ class ProfileAvatarURLRestServlet(RestServlet): content = parse_json_object_from_request(request) try: - new_avatar_url = content.get("avatar_url") - except Exception: - return 400, "Unable to parse avatar_url" - - if new_avatar_url is None: - return 400, "Missing required key: avatar_url" + new_avatar_url = content["avatar_url"] + except KeyError: + raise SynapseError( + 400, "Missing key 'avatar_url'", errcode=Codes.MISSING_PARAM + ) await self.profile_handler.set_avatar_url( user, requester, new_avatar_url, is_admin From ea0f0ad4144e3ce0cf10f3ec461ecd8f654955a2 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Wed, 11 Dec 2019 13:07:25 +0000 Subject: [PATCH 0667/1623] Prevent message search in upgraded rooms we're not in (#6385) --- changelog.d/6385.bugfix | 1 + synapse/handlers/federation.py | 4 +-- synapse/handlers/search.py | 34 +++++++++++++++++------ synapse/storage/data_stores/main/state.py | 18 ++++++++---- 4 files changed, 41 insertions(+), 16 deletions(-) create mode 100644 changelog.d/6385.bugfix diff --git a/changelog.d/6385.bugfix b/changelog.d/6385.bugfix new file mode 100644 index 0000000000..7a2bc02170 --- /dev/null +++ b/changelog.d/6385.bugfix @@ -0,0 +1 @@ +Prevent error on trying to search a upgraded room when the server is not in the predecessor room. \ No newline at end of file diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index c0dcf9abf8..13865c470c 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1299,7 +1299,7 @@ class FederationHandler(BaseHandler): # Check whether this room is the result of an upgrade of a room we already know # about. If so, migrate over user information predecessor = yield self.store.get_room_predecessor(room_id) - if not predecessor: + if not predecessor or not isinstance(predecessor.get("room_id"), str): return old_room_id = predecessor["room_id"] logger.debug( @@ -1542,7 +1542,7 @@ class FederationHandler(BaseHandler): @defer.inlineCallbacks def do_remotely_reject_invite(self, target_hosts, room_id, user_id, content): origin, event, event_format_version = yield self._make_and_verify_event( - target_hosts, room_id, user_id, "leave", content=content, + target_hosts, room_id, user_id, "leave", content=content ) # Mark as outlier as we don't have any state for this event; we're not # even in the room. diff --git a/synapse/handlers/search.py b/synapse/handlers/search.py index 56ed262a1f..ef750d1497 100644 --- a/synapse/handlers/search.py +++ b/synapse/handlers/search.py @@ -21,7 +21,7 @@ from unpaddedbase64 import decode_base64, encode_base64 from twisted.internet import defer from synapse.api.constants import EventTypes, Membership -from synapse.api.errors import SynapseError +from synapse.api.errors import NotFoundError, SynapseError from synapse.api.filtering import Filter from synapse.storage.state import StateFilter from synapse.visibility import filter_events_for_client @@ -37,6 +37,7 @@ class SearchHandler(BaseHandler): self._event_serializer = hs.get_event_client_serializer() self.storage = hs.get_storage() self.state_store = self.storage.state + self.auth = hs.get_auth() @defer.inlineCallbacks def get_old_rooms_from_upgraded_room(self, room_id): @@ -53,23 +54,38 @@ class SearchHandler(BaseHandler): room_id (str): id of the room to search through. Returns: - Deferred[iterable[unicode]]: predecessor room ids + Deferred[iterable[str]]: predecessor room ids """ historical_room_ids = [] - while True: - predecessor = yield self.store.get_room_predecessor(room_id) + # The initial room must have been known for us to get this far + predecessor = yield self.store.get_room_predecessor(room_id) - # If no predecessor, assume we've hit a dead end + while True: if not predecessor: + # We have reached the end of the chain of predecessors break - # Add predecessor's room ID - historical_room_ids.append(predecessor["room_id"]) + if not isinstance(predecessor.get("room_id"), str): + # This predecessor object is malformed. Exit here + break - # Scan through the old room for further predecessors - room_id = predecessor["room_id"] + predecessor_room_id = predecessor["room_id"] + + # Don't add it to the list until we have checked that we are in the room + try: + next_predecessor_room = yield self.store.get_room_predecessor( + predecessor_room_id + ) + except NotFoundError: + # The predecessor is not a known room, so we are done here + break + + historical_room_ids.append(predecessor_room_id) + + # And repeat + predecessor = next_predecessor_room return historical_room_ids diff --git a/synapse/storage/data_stores/main/state.py b/synapse/storage/data_stores/main/state.py index 9ef7b48c74..dcc6b43cdf 100644 --- a/synapse/storage/data_stores/main/state.py +++ b/synapse/storage/data_stores/main/state.py @@ -278,7 +278,7 @@ class StateGroupWorkerStore( @defer.inlineCallbacks def get_room_predecessor(self, room_id): - """Get the predecessor room of an upgraded room if one exists. + """Get the predecessor of an upgraded room if it exists. Otherwise return None. Args: @@ -291,14 +291,22 @@ class StateGroupWorkerStore( * room_id (str): The room ID of the predecessor room * event_id (str): The ID of the tombstone event in the predecessor room + None if a predecessor key is not found, or is not a dictionary. + Raises: - NotFoundError if the room is unknown + NotFoundError if the given room is unknown """ # Retrieve the room's create event create_event = yield self.get_create_event_for_room(room_id) - # Return predecessor if present - return create_event.content.get("predecessor", None) + # Retrieve the predecessor key of the create event + predecessor = create_event.content.get("predecessor", None) + + # Ensure the key is a dictionary + if not isinstance(predecessor, dict): + return None + + return predecessor @defer.inlineCallbacks def get_create_event_for_room(self, room_id): @@ -318,7 +326,7 @@ class StateGroupWorkerStore( # If we can't find the create event, assume we've hit a dead end if not create_id: - raise NotFoundError("Unknown room %s" % (room_id)) + raise NotFoundError("Unknown room %s" % (room_id,)) # Retrieve the room's create event and return create_event = yield self.get_event(create_id) From 6676ee9c4a74e15afdd752e05ca38d82da94c2c1 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Wed, 11 Dec 2019 13:16:01 +0000 Subject: [PATCH 0668/1623] Add dev script to generate full SQL schema files (#6394) --- changelog.d/6394.feature | 1 + scripts-dev/make_full_schema.sh | 184 ++++++++++++++++++ .../main/schema/full_schemas/README.md | 13 ++ .../main/schema/full_schemas/README.txt | 19 -- 4 files changed, 198 insertions(+), 19 deletions(-) create mode 100644 changelog.d/6394.feature create mode 100755 scripts-dev/make_full_schema.sh create mode 100644 synapse/storage/data_stores/main/schema/full_schemas/README.md delete mode 100644 synapse/storage/data_stores/main/schema/full_schemas/README.txt diff --git a/changelog.d/6394.feature b/changelog.d/6394.feature new file mode 100644 index 0000000000..1a0e8845ad --- /dev/null +++ b/changelog.d/6394.feature @@ -0,0 +1 @@ +Add a develop script to generate full SQL schemas. \ No newline at end of file diff --git a/scripts-dev/make_full_schema.sh b/scripts-dev/make_full_schema.sh new file mode 100755 index 0000000000..60e8970a35 --- /dev/null +++ b/scripts-dev/make_full_schema.sh @@ -0,0 +1,184 @@ +#!/bin/bash +# +# This script generates SQL files for creating a brand new Synapse DB with the latest +# schema, on both SQLite3 and Postgres. +# +# It does so by having Synapse generate an up-to-date SQLite DB, then running +# synapse_port_db to convert it to Postgres. It then dumps the contents of both. + +POSTGRES_HOST="localhost" +POSTGRES_DB_NAME="synapse_full_schema.$$" + +SQLITE_FULL_SCHEMA_OUTPUT_FILE="full.sql.sqlite" +POSTGRES_FULL_SCHEMA_OUTPUT_FILE="full.sql.postgres" + +REQUIRED_DEPS=("matrix-synapse" "psycopg2") + +usage() { + echo + echo "Usage: $0 -p -o [-c] [-n] [-h]" + echo + echo "-p " + echo " Username to connect to local postgres instance. The password will be requested" + echo " during script execution." + echo "-c" + echo " CI mode. Enables coverage tracking and prints every command that the script runs." + echo "-o " + echo " Directory to output full schema files to." + echo "-h" + echo " Display this help text." +} + +while getopts "p:co:h" opt; do + case $opt in + p) + POSTGRES_USERNAME=$OPTARG + ;; + c) + # Print all commands that are being executed + set -x + + # Modify required dependencies for coverage + REQUIRED_DEPS+=("coverage" "coverage-enable-subprocess") + + COVERAGE=1 + ;; + o) + command -v realpath > /dev/null || (echo "The -o flag requires the 'realpath' binary to be installed" && exit 1) + OUTPUT_DIR="$(realpath "$OPTARG")" + ;; + h) + usage + exit + ;; + \?) + echo "ERROR: Invalid option: -$OPTARG" >&2 + usage + exit + ;; + esac +done + +# Check that required dependencies are installed +unsatisfied_requirements=() +for dep in "${REQUIRED_DEPS[@]}"; do + pip show "$dep" --quiet || unsatisfied_requirements+=("$dep") +done +if [ ${#unsatisfied_requirements} -ne 0 ]; then + echo "Please install the following python packages: ${unsatisfied_requirements[*]}" + exit 1 +fi + +if [ -z "$POSTGRES_USERNAME" ]; then + echo "No postgres username supplied" + usage + exit 1 +fi + +if [ -z "$OUTPUT_DIR" ]; then + echo "No output directory supplied" + usage + exit 1 +fi + +# Create the output directory if it doesn't exist +mkdir -p "$OUTPUT_DIR" + +read -rsp "Postgres password for '$POSTGRES_USERNAME': " POSTGRES_PASSWORD +echo "" + +# Exit immediately if a command fails +set -e + +# cd to root of the synapse directory +cd "$(dirname "$0")/.." + +# Create temporary SQLite and Postgres homeserver db configs and key file +TMPDIR=$(mktemp -d) +KEY_FILE=$TMPDIR/test.signing.key # default Synapse signing key path +SQLITE_CONFIG=$TMPDIR/sqlite.conf +SQLITE_DB=$TMPDIR/homeserver.db +POSTGRES_CONFIG=$TMPDIR/postgres.conf + +# Ensure these files are delete on script exit +trap 'rm -rf $TMPDIR' EXIT + +cat > "$SQLITE_CONFIG" < "$POSTGRES_CONFIG" < "$OUTPUT_DIR/$SQLITE_FULL_SCHEMA_OUTPUT_FILE" + +echo "Dumping Postgres schema to '$OUTPUT_DIR/$POSTGRES_FULL_SCHEMA_OUTPUT_FILE'..." +pg_dump --format=plain --no-tablespaces --no-acl --no-owner $POSTGRES_DB_NAME | sed -e '/^--/d' -e 's/public\.//g' -e '/^SET /d' -e '/^SELECT /d' > "$OUTPUT_DIR/$POSTGRES_FULL_SCHEMA_OUTPUT_FILE" + +echo "Cleaning up temporary Postgres database..." +dropdb $POSTGRES_DB_NAME + +echo "Done! Files dumped to: $OUTPUT_DIR" diff --git a/synapse/storage/data_stores/main/schema/full_schemas/README.md b/synapse/storage/data_stores/main/schema/full_schemas/README.md new file mode 100644 index 0000000000..bbd3f18604 --- /dev/null +++ b/synapse/storage/data_stores/main/schema/full_schemas/README.md @@ -0,0 +1,13 @@ +# Building full schema dumps + +These schemas need to be made from a database that has had all background updates run. + +To do so, use `scripts-dev/make_full_schema.sh`. This will produce +`full.sql.postgres ` and `full.sql.sqlite` files. + +Ensure postgres is installed and your user has the ability to run bash commands +such as `createdb`. + +``` +./scripts-dev/make_full_schema.sh -p postgres_username -o output_dir/ +``` diff --git a/synapse/storage/data_stores/main/schema/full_schemas/README.txt b/synapse/storage/data_stores/main/schema/full_schemas/README.txt deleted file mode 100644 index d3f6401344..0000000000 --- a/synapse/storage/data_stores/main/schema/full_schemas/README.txt +++ /dev/null @@ -1,19 +0,0 @@ -Building full schema dumps -========================== - -These schemas need to be made from a database that has had all background updates run. - -Postgres --------- - -$ pg_dump --format=plain --schema-only --no-tablespaces --no-acl --no-owner $DATABASE_NAME| sed -e '/^--/d' -e 's/public\.//g' -e '/^SET /d' -e '/^SELECT /d' > full.sql.postgres - -SQLite ------- - -$ sqlite3 $DATABASE_FILE ".schema" > full.sql.sqlite - -After ------ - -Delete the CREATE statements for "sqlite_stat1", "schema_version", "applied_schema_deltas", and "applied_module_schemas". \ No newline at end of file From fc316a4894912f49f5d0321e533aabca5624b0ba Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Wed, 11 Dec 2019 13:39:47 +0000 Subject: [PATCH 0669/1623] Prevent redacted events from appearing in message search (#6377) --- changelog.d/6377.bugfix | 1 + synapse/handlers/federation.py | 7 +- synapse/handlers/message.py | 5 +- synapse/state/__init__.py | 3 +- .../storage/data_stores/main/events_worker.py | 97 ++++++++++++------- synapse/storage/data_stores/main/search.py | 8 +- 6 files changed, 78 insertions(+), 43 deletions(-) create mode 100644 changelog.d/6377.bugfix diff --git a/changelog.d/6377.bugfix b/changelog.d/6377.bugfix new file mode 100644 index 0000000000..ccda96962f --- /dev/null +++ b/changelog.d/6377.bugfix @@ -0,0 +1 @@ +Prevent redacted events from being returned during message search. \ No newline at end of file diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 13865c470c..8f3c9d7702 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -63,6 +63,7 @@ from synapse.replication.http.federation import ( ) from synapse.replication.http.membership import ReplicationUserJoinedLeftRoomRestServlet from synapse.state import StateResolutionStore, resolve_events_with_store +from synapse.storage.data_stores.main.events_worker import EventRedactBehaviour from synapse.types import UserID, get_domain_from_id from synapse.util import batch_iter, unwrapFirstError from synapse.util.async_helpers import Linearizer @@ -423,7 +424,7 @@ class FederationHandler(BaseHandler): evs = yield self.store.get_events( list(state_map.values()), get_prev_content=False, - check_redacted=False, + redact_behaviour=EventRedactBehaviour.AS_IS, ) event_map.update(evs) @@ -1000,7 +1001,9 @@ class FederationHandler(BaseHandler): forward_events = yield self.store.get_successor_events(list(extremities)) extremities_events = yield self.store.get_events( - forward_events, check_redacted=False, get_prev_content=False + forward_events, + redact_behaviour=EventRedactBehaviour.AS_IS, + get_prev_content=False, ) # We set `check_history_visibility_only` as we might otherwise get false diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index 54fa216d83..bf9add7fe2 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -46,6 +46,7 @@ from synapse.events.validator import EventValidator from synapse.logging.context import run_in_background from synapse.metrics.background_process_metrics import run_as_background_process from synapse.replication.http.send_event import ReplicationSendEventRestServlet +from synapse.storage.data_stores.main.events_worker import EventRedactBehaviour from synapse.storage.state import StateFilter from synapse.types import RoomAlias, UserID, create_requester from synapse.util.async_helpers import Linearizer @@ -875,7 +876,7 @@ class EventCreationHandler(object): if event.type == EventTypes.Redaction: original_event = yield self.store.get_event( event.redacts, - check_redacted=False, + redact_behaviour=EventRedactBehaviour.AS_IS, get_prev_content=False, allow_rejected=False, allow_none=True, @@ -952,7 +953,7 @@ class EventCreationHandler(object): if event.type == EventTypes.Redaction: original_event = yield self.store.get_event( event.redacts, - check_redacted=False, + redact_behaviour=EventRedactBehaviour.AS_IS, get_prev_content=False, allow_rejected=False, allow_none=True, diff --git a/synapse/state/__init__.py b/synapse/state/__init__.py index 139beef8ed..3e6d62eef1 100644 --- a/synapse/state/__init__.py +++ b/synapse/state/__init__.py @@ -32,6 +32,7 @@ from synapse.events import EventBase from synapse.events.snapshot import EventContext from synapse.logging.utils import log_function from synapse.state import v1, v2 +from synapse.storage.data_stores.main.events_worker import EventRedactBehaviour from synapse.util.async_helpers import Linearizer from synapse.util.caches import get_cache_factor_for from synapse.util.caches.expiringcache import ExpiringCache @@ -645,7 +646,7 @@ class StateResolutionStore(object): return self.store.get_events( event_ids, - check_redacted=False, + redact_behaviour=EventRedactBehaviour.AS_IS, get_prev_content=False, allow_rejected=allow_rejected, ) diff --git a/synapse/storage/data_stores/main/events_worker.py b/synapse/storage/data_stores/main/events_worker.py index 9ee117ce0f..2c9142814c 100644 --- a/synapse/storage/data_stores/main/events_worker.py +++ b/synapse/storage/data_stores/main/events_worker.py @@ -19,8 +19,10 @@ import itertools import logging import threading from collections import namedtuple +from typing import List, Optional from canonicaljson import json +from constantly import NamedConstant, Names from twisted.internet import defer @@ -55,6 +57,16 @@ EVENT_QUEUE_TIMEOUT_S = 0.1 # Timeout when waiting for requests for events _EventCacheEntry = namedtuple("_EventCacheEntry", ("event", "redacted_event")) +class EventRedactBehaviour(Names): + """ + What to do when retrieving a redacted event from the database. + """ + + AS_IS = NamedConstant() + REDACT = NamedConstant() + BLOCK = NamedConstant() + + class EventsWorkerStore(SQLBaseStore): def __init__(self, database: Database, db_conn, hs): super(EventsWorkerStore, self).__init__(database, db_conn, hs) @@ -125,25 +137,27 @@ class EventsWorkerStore(SQLBaseStore): @defer.inlineCallbacks def get_event( self, - event_id, - check_redacted=True, - get_prev_content=False, - allow_rejected=False, - allow_none=False, - check_room_id=None, + event_id: List[str], + redact_behaviour: EventRedactBehaviour = EventRedactBehaviour.REDACT, + get_prev_content: bool = False, + allow_rejected: bool = False, + allow_none: bool = False, + check_room_id: Optional[str] = None, ): """Get an event from the database by event_id. Args: - event_id (str): The event_id of the event to fetch - check_redacted (bool): If True, check if event has been redacted - and redact it. - get_prev_content (bool): If True and event is a state event, + event_id: The event_id of the event to fetch + redact_behaviour: Determine what to do with a redacted event. Possible values: + * AS_IS - Return the full event body with no redacted content + * REDACT - Return the event but with a redacted body + * DISALLOW - Do not return redacted events + get_prev_content: If True and event is a state event, include the previous states content in the unsigned field. - allow_rejected (bool): If True return rejected events. - allow_none (bool): If True, return None if no event found, if + allow_rejected: If True return rejected events. + allow_none: If True, return None if no event found, if False throw a NotFoundError - check_room_id (str|None): if not None, check the room of the found event. + check_room_id: if not None, check the room of the found event. If there is a mismatch, behave as per allow_none. Returns: @@ -154,7 +168,7 @@ class EventsWorkerStore(SQLBaseStore): events = yield self.get_events_as_list( [event_id], - check_redacted=check_redacted, + redact_behaviour=redact_behaviour, get_prev_content=get_prev_content, allow_rejected=allow_rejected, ) @@ -173,27 +187,30 @@ class EventsWorkerStore(SQLBaseStore): @defer.inlineCallbacks def get_events( self, - event_ids, - check_redacted=True, - get_prev_content=False, - allow_rejected=False, + event_ids: List[str], + redact_behaviour: EventRedactBehaviour = EventRedactBehaviour.REDACT, + get_prev_content: bool = False, + allow_rejected: bool = False, ): """Get events from the database Args: - event_ids (list): The event_ids of the events to fetch - check_redacted (bool): If True, check if event has been redacted - and redact it. - get_prev_content (bool): If True and event is a state event, + event_ids: The event_ids of the events to fetch + redact_behaviour: Determine what to do with a redacted event. Possible + values: + * AS_IS - Return the full event body with no redacted content + * REDACT - Return the event but with a redacted body + * DISALLOW - Do not return redacted events + get_prev_content: If True and event is a state event, include the previous states content in the unsigned field. - allow_rejected (bool): If True return rejected events. + allow_rejected: If True return rejected events. Returns: Deferred : Dict from event_id to event. """ events = yield self.get_events_as_list( event_ids, - check_redacted=check_redacted, + redact_behaviour=redact_behaviour, get_prev_content=get_prev_content, allow_rejected=allow_rejected, ) @@ -203,21 +220,23 @@ class EventsWorkerStore(SQLBaseStore): @defer.inlineCallbacks def get_events_as_list( self, - event_ids, - check_redacted=True, - get_prev_content=False, - allow_rejected=False, + event_ids: List[str], + redact_behaviour: EventRedactBehaviour = EventRedactBehaviour.REDACT, + get_prev_content: bool = False, + allow_rejected: bool = False, ): """Get events from the database and return in a list in the same order as given by `event_ids` arg. Args: - event_ids (list): The event_ids of the events to fetch - check_redacted (bool): If True, check if event has been redacted - and redact it. - get_prev_content (bool): If True and event is a state event, + event_ids: The event_ids of the events to fetch + redact_behaviour: Determine what to do with a redacted event. Possible values: + * AS_IS - Return the full event body with no redacted content + * REDACT - Return the event but with a redacted body + * DISALLOW - Do not return redacted events + get_prev_content: If True and event is a state event, include the previous states content in the unsigned field. - allow_rejected (bool): If True return rejected events. + allow_rejected: If True, return rejected events. Returns: Deferred[list[EventBase]]: List of events fetched from the database. The @@ -319,10 +338,14 @@ class EventsWorkerStore(SQLBaseStore): # Update the cache to save doing the checks again. entry.event.internal_metadata.recheck_redaction = False - if check_redacted and entry.redacted_event: - event = entry.redacted_event - else: - event = entry.event + event = entry.event + + if entry.redacted_event: + if redact_behaviour == EventRedactBehaviour.BLOCK: + # Skip this event + continue + elif redact_behaviour == EventRedactBehaviour.REDACT: + event = entry.redacted_event events.append(event) diff --git a/synapse/storage/data_stores/main/search.py b/synapse/storage/data_stores/main/search.py index 4eec2fae5e..dfb46ee0f8 100644 --- a/synapse/storage/data_stores/main/search.py +++ b/synapse/storage/data_stores/main/search.py @@ -25,6 +25,7 @@ from twisted.internet import defer from synapse.api.errors import SynapseError from synapse.storage._base import SQLBaseStore, make_in_list_sql_clause +from synapse.storage.data_stores.main.events_worker import EventRedactBehaviour from synapse.storage.database import Database from synapse.storage.engines import PostgresEngine, Sqlite3Engine @@ -453,7 +454,12 @@ class SearchStore(SearchBackgroundUpdateStore): results = list(filter(lambda row: row["room_id"] in room_ids, results)) - events = yield self.get_events_as_list([r["event_id"] for r in results]) + # We set redact_behaviour to BLOCK here to prevent redacted events being returned in + # search results (which is a data leak) + events = yield self.get_events_as_list( + [r["event_id"] for r in results], + redact_behaviour=EventRedactBehaviour.BLOCK, + ) event_map = {ev.event_id: ev for ev in events} From d156912c4c4f65b821eab202654d740422008d82 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 11 Dec 2019 13:56:50 +0000 Subject: [PATCH 0670/1623] 1.7.0rc2 --- CHANGES.md | 11 +++++++++++ changelog.d/6497.bugfix | 1 - changelog.d/6499.bugfix | 1 - changelog.d/6507.bugfix | 1 - changelog.d/6509.bugfix | 1 - synapse/__init__.py | 2 +- 6 files changed, 12 insertions(+), 5 deletions(-) delete mode 100644 changelog.d/6497.bugfix delete mode 100644 changelog.d/6499.bugfix delete mode 100644 changelog.d/6507.bugfix delete mode 100644 changelog.d/6509.bugfix diff --git a/CHANGES.md b/CHANGES.md index c30ea4718d..c83a6afbcd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,14 @@ +Synapse 1.7.0rc2 (2019-12-11) +============================= + +Bugfixes +-------- + +- Fix incorrect error message for invalid requests when setting user's avatar URL. ([\#6497](https://github.com/matrix-org/synapse/issues/6497)) +- Fix support for SQLite 3.7. ([\#6499](https://github.com/matrix-org/synapse/issues/6499)) +- Fix regression where sending email push would not work when using a pusher worker. ([\#6507](https://github.com/matrix-org/synapse/issues/6507), [\#6509](https://github.com/matrix-org/synapse/issues/6509)) + + Synapse 1.7.0rc1 (2019-12-09) ============================= diff --git a/changelog.d/6497.bugfix b/changelog.d/6497.bugfix deleted file mode 100644 index 6a2644d8e6..0000000000 --- a/changelog.d/6497.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix incorrect error message for invalid requests when setting user's avatar URL. diff --git a/changelog.d/6499.bugfix b/changelog.d/6499.bugfix deleted file mode 100644 index 299feba0f8..0000000000 --- a/changelog.d/6499.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix support for SQLite 3.7. diff --git a/changelog.d/6507.bugfix b/changelog.d/6507.bugfix deleted file mode 100644 index d767a6237f..0000000000 --- a/changelog.d/6507.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix regression where sending email push would not work when using a pusher worker. diff --git a/changelog.d/6509.bugfix b/changelog.d/6509.bugfix deleted file mode 100644 index d767a6237f..0000000000 --- a/changelog.d/6509.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix regression where sending email push would not work when using a pusher worker. diff --git a/synapse/__init__.py b/synapse/__init__.py index c67a51a8d5..fc2a6e4ee6 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -36,7 +36,7 @@ try: except ImportError: pass -__version__ = "1.7.0rc1" +__version__ = "1.7.0rc2" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 7c429f92d6935a3e9e0140fdd82801edc43b66b8 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 11 Dec 2019 14:32:25 +0000 Subject: [PATCH 0671/1623] Clean up some logging (#6515) This just makes some of the logging easier to follow when things start going wrong. --- changelog.d/6515.misc | 1 + synapse/handlers/federation.py | 37 +++++++++++++++++----------------- 2 files changed, 20 insertions(+), 18 deletions(-) create mode 100644 changelog.d/6515.misc diff --git a/changelog.d/6515.misc b/changelog.d/6515.misc new file mode 100644 index 0000000000..a9c303ed1c --- /dev/null +++ b/changelog.d/6515.misc @@ -0,0 +1 @@ +Clean up some logging when handling incoming events over federation. diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 8f3c9d7702..cf9c46d027 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -183,7 +183,7 @@ class FederationHandler(BaseHandler): room_id = pdu.room_id event_id = pdu.event_id - logger.info("[%s %s] handling received PDU: %s", room_id, event_id, pdu) + logger.info("handling received PDU: %s", pdu) # We reprocess pdus when we have seen them only as outliers existing = yield self.store.get_event( @@ -279,9 +279,15 @@ class FederationHandler(BaseHandler): len(missing_prevs), ) - yield self._get_missing_events_for_pdu( - origin, pdu, prevs, min_depth - ) + try: + yield self._get_missing_events_for_pdu( + origin, pdu, prevs, min_depth + ) + except Exception as e: + raise Exception( + "Error fetching missing prev_events for %s: %s" + % (event_id, e) + ) # Update the set of things we've seen after trying to # fetch the missing stuff @@ -293,14 +299,6 @@ class FederationHandler(BaseHandler): room_id, event_id, ) - elif missing_prevs: - logger.info( - "[%s %s] Not recursively fetching %d missing prev_events: %s", - room_id, - event_id, - len(missing_prevs), - shortstr(missing_prevs), - ) if prevs - seen: # We've still not been able to get all of the prev_events for this event. @@ -345,6 +343,12 @@ class FederationHandler(BaseHandler): affected=pdu.event_id, ) + logger.info( + "Event %s is missing prev_events: calculating state for a " + "backwards extremity", + event_id, + ) + # Calculate the state after each of the previous events, and # resolve them to find the correct state at the current event. auth_chains = set() @@ -365,10 +369,7 @@ class FederationHandler(BaseHandler): # know about for p in prevs - seen: logger.info( - "[%s %s] Requesting state at missing prev_event %s", - room_id, - event_id, - p, + "Requesting state at missing prev_event %s", event_id, ) room_version = yield self.store.get_room_version(room_id) @@ -612,8 +613,8 @@ class FederationHandler(BaseHandler): failed_to_fetch = desired_events - event_map.keys() if failed_to_fetch: logger.warning( - "Failed to fetch missing state/auth events for %s: %s", - room_id, + "Failed to fetch missing state/auth events for %s %s", + event_id, failed_to_fetch, ) From 7712e751b87b83086a8cb0a1cda1eef40e177d07 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 10 Dec 2019 16:54:34 +0000 Subject: [PATCH 0672/1623] Convert federation backfill to async PaginationHandler.get_messages is only called by RoomMessageListRestServlet, which is async. Chase the code path down from there: - FederationHandler.maybe_backfill (and nested try_backfill) - FederationHandler.backfill --- synapse/handlers/federation.py | 47 ++++++++++++++++------------------ synapse/handlers/pagination.py | 27 ++++++++++--------- 2 files changed, 35 insertions(+), 39 deletions(-) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index cf9c46d027..e54d509b62 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -756,8 +756,7 @@ class FederationHandler(BaseHandler): yield self.user_joined_room(user, room_id) @log_function - @defer.inlineCallbacks - def backfill(self, dest, room_id, limit, extremities): + async def backfill(self, dest, room_id, limit, extremities): """ Trigger a backfill request to `dest` for the given `room_id` This will attempt to get more events from the remote. If the other side @@ -774,9 +773,9 @@ class FederationHandler(BaseHandler): if dest == self.server_name: raise SynapseError(400, "Can't backfill from self.") - room_version = yield self.store.get_room_version(room_id) + room_version = await self.store.get_room_version(room_id) - events = yield self.federation_client.backfill( + events = await self.federation_client.backfill( dest, room_id, limit=limit, extremities=extremities ) @@ -791,7 +790,7 @@ class FederationHandler(BaseHandler): # self._sanity_check_event(ev) # Don't bother processing events we already have. - seen_events = yield self.store.have_events_in_timeline( + seen_events = await self.store.have_events_in_timeline( set(e.event_id for e in events) ) @@ -814,7 +813,7 @@ class FederationHandler(BaseHandler): state_events = {} events_to_state = {} for e_id in edges: - state, auth = yield self._get_state_for_room( + state, auth = await self._get_state_for_room( destination=dest, room_id=room_id, event_id=e_id ) auth_events.update({a.event_id: a for a in auth}) @@ -839,7 +838,7 @@ class FederationHandler(BaseHandler): # We repeatedly do this until we stop finding new auth events. while missing_auth - failed_to_fetch: logger.info("Missing auth for backfill: %r", missing_auth) - ret_events = yield self.store.get_events(missing_auth - failed_to_fetch) + ret_events = await self.store.get_events(missing_auth - failed_to_fetch) auth_events.update(ret_events) required_auth.update( @@ -853,7 +852,7 @@ class FederationHandler(BaseHandler): missing_auth - failed_to_fetch, ) - results = yield make_deferred_yieldable( + results = await make_deferred_yieldable( defer.gatherResults( [ run_in_background( @@ -880,7 +879,7 @@ class FederationHandler(BaseHandler): failed_to_fetch = missing_auth - set(auth_events) - seen_events = yield self.store.have_seen_events( + seen_events = await self.store.have_seen_events( set(auth_events.keys()) | set(state_events.keys()) ) @@ -942,7 +941,7 @@ class FederationHandler(BaseHandler): ) ) - yield self._handle_new_events(dest, ev_infos, backfilled=True) + await self._handle_new_events(dest, ev_infos, backfilled=True) # Step 2: Persist the rest of the events in the chunk one by one events.sort(key=lambda e: e.depth) @@ -958,16 +957,15 @@ class FederationHandler(BaseHandler): # We store these one at a time since each event depends on the # previous to work out the state. # TODO: We can probably do something more clever here. - yield self._handle_new_event(dest, event, backfilled=True) + await self._handle_new_event(dest, event, backfilled=True) return events - @defer.inlineCallbacks - def maybe_backfill(self, room_id, current_depth): + async def maybe_backfill(self, room_id, current_depth): """Checks the database to see if we should backfill before paginating, and if so do. """ - extremities = yield self.store.get_oldest_events_with_depth_in_room(room_id) + extremities = await self.store.get_oldest_events_with_depth_in_room(room_id) if not extremities: logger.debug("Not backfilling as no extremeties found.") @@ -999,9 +997,9 @@ class FederationHandler(BaseHandler): # state *before* the event, ignoring the special casing certain event # types have. - forward_events = yield self.store.get_successor_events(list(extremities)) + forward_events = await self.store.get_successor_events(list(extremities)) - extremities_events = yield self.store.get_events( + extremities_events = await self.store.get_events( forward_events, redact_behaviour=EventRedactBehaviour.AS_IS, get_prev_content=False, @@ -1009,7 +1007,7 @@ class FederationHandler(BaseHandler): # We set `check_history_visibility_only` as we might otherwise get false # positives from users having been erased. - filtered_extremities = yield filter_events_for_server( + filtered_extremities = await filter_events_for_server( self.storage, self.server_name, list(extremities_events.values()), @@ -1039,7 +1037,7 @@ class FederationHandler(BaseHandler): # First we try hosts that are already in the room # TODO: HEURISTIC ALERT. - curr_state = yield self.state_handler.get_current_state(room_id) + curr_state = await self.state_handler.get_current_state(room_id) def get_domains_from_state(state): """Get joined domains from state @@ -1078,12 +1076,11 @@ class FederationHandler(BaseHandler): domain for domain, depth in curr_domains if domain != self.server_name ] - @defer.inlineCallbacks - def try_backfill(domains): + async def try_backfill(domains): # TODO: Should we try multiple of these at a time? for dom in domains: try: - yield self.backfill( + await self.backfill( dom, room_id, limit=100, extremities=extremities ) # If this succeeded then we probably already have the @@ -1114,7 +1111,7 @@ class FederationHandler(BaseHandler): return False - success = yield try_backfill(likely_domains) + success = await try_backfill(likely_domains) if success: return True @@ -1128,7 +1125,7 @@ class FederationHandler(BaseHandler): logger.debug("calling resolve_state_groups in _maybe_backfill") resolve = preserve_fn(self.state_handler.resolve_state_groups_for_events) - states = yield make_deferred_yieldable( + states = await make_deferred_yieldable( defer.gatherResults( [resolve(room_id, [e]) for e in event_ids], consumeErrors=True ) @@ -1138,7 +1135,7 @@ class FederationHandler(BaseHandler): # event_ids. states = dict(zip(event_ids, [s.state for s in states])) - state_map = yield self.store.get_events( + state_map = await self.store.get_events( [e_id for ids in itervalues(states) for e_id in itervalues(ids)], get_prev_content=False, ) @@ -1154,7 +1151,7 @@ class FederationHandler(BaseHandler): for e_id, _ in sorted_extremeties_tuple: likely_domains = get_domains_from_state(states[e_id]) - success = yield try_backfill( + success = await try_backfill( [dom for dom, _ in likely_domains if dom not in tried_domains] ) if success: diff --git a/synapse/handlers/pagination.py b/synapse/handlers/pagination.py index 8514ddc600..00a6afc963 100644 --- a/synapse/handlers/pagination.py +++ b/synapse/handlers/pagination.py @@ -280,8 +280,7 @@ class PaginationHandler(object): await self.storage.purge_events.purge_room(room_id) - @defer.inlineCallbacks - def get_messages( + async def get_messages( self, requester, room_id=None, @@ -307,7 +306,7 @@ class PaginationHandler(object): room_token = pagin_config.from_token.room_key else: pagin_config.from_token = ( - yield self.hs.get_event_sources().get_current_token_for_pagination() + await self.hs.get_event_sources().get_current_token_for_pagination() ) room_token = pagin_config.from_token.room_key @@ -319,11 +318,11 @@ class PaginationHandler(object): source_config = pagin_config.get_source_config("room") - with (yield self.pagination_lock.read(room_id)): + with (await self.pagination_lock.read(room_id)): ( membership, member_event_id, - ) = yield self.auth.check_in_room_or_world_readable(room_id, user_id) + ) = await self.auth.check_in_room_or_world_readable(room_id, user_id) if source_config.direction == "b": # if we're going backwards, we might need to backfill. This @@ -331,7 +330,7 @@ class PaginationHandler(object): if room_token.topological: max_topo = room_token.topological else: - max_topo = yield self.store.get_max_topological_token( + max_topo = await self.store.get_max_topological_token( room_id, room_token.stream ) @@ -339,18 +338,18 @@ class PaginationHandler(object): # If they have left the room then clamp the token to be before # they left the room, to save the effort of loading from the # database. - leave_token = yield self.store.get_topological_token_for_event( + leave_token = await self.store.get_topological_token_for_event( member_event_id ) leave_token = RoomStreamToken.parse(leave_token) if leave_token.topological < max_topo: source_config.from_key = str(leave_token) - yield self.hs.get_handlers().federation_handler.maybe_backfill( + await self.hs.get_handlers().federation_handler.maybe_backfill( room_id, max_topo ) - events, next_key = yield self.store.paginate_room_events( + events, next_key = await self.store.paginate_room_events( room_id=room_id, from_key=source_config.from_key, to_key=source_config.to_key, @@ -365,7 +364,7 @@ class PaginationHandler(object): if event_filter: events = event_filter.filter(events) - events = yield filter_events_for_client( + events = await filter_events_for_client( self.storage, user_id, events, is_peeking=(member_event_id is None) ) @@ -385,19 +384,19 @@ class PaginationHandler(object): (EventTypes.Member, event.sender) for event in events ) - state_ids = yield self.state_store.get_state_ids_for_event( + state_ids = await self.state_store.get_state_ids_for_event( events[0].event_id, state_filter=state_filter ) if state_ids: - state = yield self.store.get_events(list(state_ids.values())) + state = await self.store.get_events(list(state_ids.values())) state = state.values() time_now = self.clock.time_msec() chunk = { "chunk": ( - yield self._event_serializer.serialize_events( + await self._event_serializer.serialize_events( events, time_now, as_client_event=as_client_event ) ), @@ -406,7 +405,7 @@ class PaginationHandler(object): } if state: - chunk["state"] = yield self._event_serializer.serialize_events( + chunk["state"] = await self._event_serializer.serialize_events( state, time_now, as_client_event=as_client_event ) From e77237b9350a7054657b1a641883a09c6b5d44f3 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 10 Dec 2019 17:01:37 +0000 Subject: [PATCH 0673/1623] convert to async: FederationHandler.on_receive_pdu and associated functions: * on_receive_pdu * handle_queued_pdus * get_missing_events_for_pdu --- synapse/handlers/federation.py | 49 +++++++++++++++------------------- tests/test_federation.py | 14 ++++++---- 2 files changed, 31 insertions(+), 32 deletions(-) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index e54d509b62..01d9c5120e 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -165,8 +165,7 @@ class FederationHandler(BaseHandler): self._ephemeral_messages_enabled = hs.config.enable_ephemeral_messages - @defer.inlineCallbacks - def on_receive_pdu(self, origin, pdu, sent_to_us_directly=False): + async def on_receive_pdu(self, origin, pdu, sent_to_us_directly=False) -> None: """ Process a PDU received via a federation /send/ transaction, or via backfill of missing prev_events @@ -176,8 +175,6 @@ class FederationHandler(BaseHandler): pdu (FrozenEvent): received PDU sent_to_us_directly (bool): True if this event was pushed to us; False if we pulled it as the result of a missing prev_event. - - Returns (Deferred): completes with None """ room_id = pdu.room_id @@ -186,7 +183,7 @@ class FederationHandler(BaseHandler): logger.info("handling received PDU: %s", pdu) # We reprocess pdus when we have seen them only as outliers - existing = yield self.store.get_event( + existing = await self.store.get_event( event_id, allow_none=True, allow_rejected=True ) @@ -230,7 +227,7 @@ class FederationHandler(BaseHandler): # # Note that if we were never in the room then we would have already # dropped the event, since we wouldn't know the room version. - is_in_room = yield self.auth.check_host_in_room(room_id, self.server_name) + is_in_room = await self.auth.check_host_in_room(room_id, self.server_name) if not is_in_room: logger.info( "[%s %s] Ignoring PDU from %s as we're not in the room", @@ -246,12 +243,12 @@ class FederationHandler(BaseHandler): # Get missing pdus if necessary. if not pdu.internal_metadata.is_outlier(): # We only backfill backwards to the min depth. - min_depth = yield self.get_min_depth_for_context(pdu.room_id) + min_depth = await self.get_min_depth_for_context(pdu.room_id) logger.debug("[%s %s] min_depth: %d", room_id, event_id, min_depth) prevs = set(pdu.prev_event_ids()) - seen = yield self.store.have_seen_events(prevs) + seen = await self.store.have_seen_events(prevs) if min_depth and pdu.depth < min_depth: # This is so that we don't notify the user about this @@ -271,7 +268,7 @@ class FederationHandler(BaseHandler): len(missing_prevs), shortstr(missing_prevs), ) - with (yield self._room_pdu_linearizer.queue(pdu.room_id)): + with (await self._room_pdu_linearizer.queue(pdu.room_id)): logger.info( "[%s %s] Acquired room lock to fetch %d missing prev_events", room_id, @@ -280,7 +277,7 @@ class FederationHandler(BaseHandler): ) try: - yield self._get_missing_events_for_pdu( + await self._get_missing_events_for_pdu( origin, pdu, prevs, min_depth ) except Exception as e: @@ -291,7 +288,7 @@ class FederationHandler(BaseHandler): # Update the set of things we've seen after trying to # fetch the missing stuff - seen = yield self.store.have_seen_events(prevs) + seen = await self.store.have_seen_events(prevs) if not prevs - seen: logger.info( @@ -355,7 +352,7 @@ class FederationHandler(BaseHandler): event_map = {event_id: pdu} try: # Get the state of the events we know about - ours = yield self.state_store.get_state_groups_ids(room_id, seen) + ours = await self.state_store.get_state_groups_ids(room_id, seen) # state_maps is a list of mappings from (type, state_key) to event_id state_maps = list( @@ -372,7 +369,7 @@ class FederationHandler(BaseHandler): "Requesting state at missing prev_event %s", event_id, ) - room_version = yield self.store.get_room_version(room_id) + room_version = await self.store.get_room_version(room_id) with nested_logging_context(p): # note that if any of the missing prevs share missing state or @@ -381,11 +378,11 @@ class FederationHandler(BaseHandler): ( remote_state, got_auth_chain, - ) = yield self._get_state_for_room(origin, room_id, p) + ) = await self._get_state_for_room(origin, room_id, p) # we want the state *after* p; _get_state_for_room returns the # state *before* p. - remote_event = yield self.federation_client.get_pdu( + remote_event = await self.federation_client.get_pdu( [origin], p, room_version, outlier=True ) @@ -410,7 +407,7 @@ class FederationHandler(BaseHandler): for x in remote_state: event_map[x.event_id] = x - state_map = yield resolve_events_with_store( + state_map = await resolve_events_with_store( room_version, state_maps, event_map, @@ -422,7 +419,7 @@ class FederationHandler(BaseHandler): # First though we need to fetch all the events that are in # state_map, so we can build up the state below. - evs = yield self.store.get_events( + evs = await self.store.get_events( list(state_map.values()), get_prev_content=False, redact_behaviour=EventRedactBehaviour.AS_IS, @@ -446,12 +443,11 @@ class FederationHandler(BaseHandler): affected=event_id, ) - yield self._process_received_pdu( + await self._process_received_pdu( origin, pdu, state=state, auth_chain=auth_chain ) - @defer.inlineCallbacks - def _get_missing_events_for_pdu(self, origin, pdu, prevs, min_depth): + async def _get_missing_events_for_pdu(self, origin, pdu, prevs, min_depth): """ Args: origin (str): Origin of the pdu. Will be called to get the missing events @@ -463,12 +459,12 @@ class FederationHandler(BaseHandler): room_id = pdu.room_id event_id = pdu.event_id - seen = yield self.store.have_seen_events(prevs) + seen = await self.store.have_seen_events(prevs) if not prevs - seen: return - latest = yield self.store.get_latest_event_ids_in_room(room_id) + latest = await self.store.get_latest_event_ids_in_room(room_id) # We add the prev events that we have seen to the latest # list to ensure the remote server doesn't give them to us @@ -532,7 +528,7 @@ class FederationHandler(BaseHandler): # All that said: Let's try increasing the timout to 60s and see what happens. try: - missing_events = yield self.federation_client.get_missing_events( + missing_events = await self.federation_client.get_missing_events( origin, room_id, earliest_events_ids=list(latest), @@ -571,7 +567,7 @@ class FederationHandler(BaseHandler): ) with nested_logging_context(ev.event_id): try: - yield self.on_receive_pdu(origin, ev, sent_to_us_directly=False) + await self.on_receive_pdu(origin, ev, sent_to_us_directly=False) except FederationError as e: if e.code == 403: logger.warning( @@ -1328,8 +1324,7 @@ class FederationHandler(BaseHandler): return True - @defer.inlineCallbacks - def _handle_queued_pdus(self, room_queue): + async def _handle_queued_pdus(self, room_queue): """Process PDUs which got queued up while we were busy send_joining. Args: @@ -1345,7 +1340,7 @@ class FederationHandler(BaseHandler): p.room_id, ) with nested_logging_context(p.event_id): - yield self.on_receive_pdu(origin, p, sent_to_us_directly=True) + await self.on_receive_pdu(origin, p, sent_to_us_directly=True) except Exception as e: logger.warning( "Error handling queued PDU %s from %s: %s", p.event_id, origin, e diff --git a/tests/test_federation.py b/tests/test_federation.py index ad165d7295..68684460c6 100644 --- a/tests/test_federation.py +++ b/tests/test_federation.py @@ -1,6 +1,6 @@ from mock import Mock -from twisted.internet.defer import maybeDeferred, succeed +from twisted.internet.defer import ensureDeferred, maybeDeferred, succeed from synapse.events import FrozenEvent from synapse.logging.context import LoggingContext @@ -70,8 +70,10 @@ class MessageAcceptTests(unittest.TestCase): ) # Send the join, it should return None (which is not an error) - d = self.handler.on_receive_pdu( - "test.serv", join_event, sent_to_us_directly=True + d = ensureDeferred( + self.handler.on_receive_pdu( + "test.serv", join_event, sent_to_us_directly=True + ) ) self.reactor.advance(1) self.assertEqual(self.successResultOf(d), None) @@ -119,8 +121,10 @@ class MessageAcceptTests(unittest.TestCase): ) with LoggingContext(request="lying_event"): - d = self.handler.on_receive_pdu( - "test.serv", lying_event, sent_to_us_directly=True + d = ensureDeferred( + self.handler.on_receive_pdu( + "test.serv", lying_event, sent_to_us_directly=True + ) ) # Step the reactor, so the database fetches come back From 4db394a4b3e9c823655cdd3e716a5f234107d337 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 10 Dec 2019 17:25:18 +0000 Subject: [PATCH 0674/1623] convert to async: FederationHandler._get_state_for_room ... and _get_events_from_store_or_dest --- synapse/handlers/federation.py | 42 +++++++++++++++++----------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 01d9c5120e..724cae9647 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -19,7 +19,7 @@ import itertools import logging -from typing import Dict, Iterable, Optional, Sequence, Tuple +from typing import Dict, Iterable, List, Optional, Sequence, Tuple import six from six import iteritems, itervalues @@ -579,30 +579,30 @@ class FederationHandler(BaseHandler): else: raise - @defer.inlineCallbacks @log_function - def _get_state_for_room(self, destination, room_id, event_id): + async def _get_state_for_room( + self, destination: str, room_id: str, event_id: str + ) -> Tuple[List[EventBase], List[EventBase]]: """Requests all of the room state at a given event from a remote homeserver. Args: - destination (str): The remote homeserver to query for the state. - room_id (str): The id of the room we're interested in. - event_id (str): The id of the event we want the state at. + destination:: The remote homeserver to query for the state. + room_id: The id of the room we're interested in. + event_id: The id of the event we want the state at. Returns: - Deferred[Tuple[List[EventBase], List[EventBase]]]: - A list of events in the state, and a list of events in the auth chain - for the given event. + A list of events in the state, and a list of events in the auth chain + for the given event. """ ( state_event_ids, auth_event_ids, - ) = yield self.federation_client.get_room_state_ids( + ) = await self.federation_client.get_room_state_ids( destination, room_id, event_id=event_id ) desired_events = set(state_event_ids + auth_event_ids) - event_map = yield self._get_events_from_store_or_dest( + event_map = await self._get_events_from_store_or_dest( destination, room_id, desired_events ) @@ -621,20 +621,20 @@ class FederationHandler(BaseHandler): return pdus, auth_chain - @defer.inlineCallbacks - def _get_events_from_store_or_dest(self, destination, room_id, event_ids): + async def _get_events_from_store_or_dest( + self, destination: str, room_id: str, event_ids: Iterable[str] + ) -> Dict[str, EventBase]: """Fetch events from a remote destination, checking if we already have them. Args: - destination (str) - room_id (str) - event_ids (Iterable[str]) + destination + room_id + event_ids Returns: - Deferred[dict[str, EventBase]]: A deferred resolving to a map - from event_id to event + map from event_id to event """ - fetched_events = yield self.store.get_events(event_ids, allow_rejected=True) + fetched_events = await self.store.get_events(event_ids, allow_rejected=True) missing_events = set(event_ids) - fetched_events.keys() @@ -647,7 +647,7 @@ class FederationHandler(BaseHandler): event_ids, ) - room_version = yield self.store.get_room_version(room_id) + room_version = await self.store.get_room_version(room_id) # XXX 20 requests at once? really? for batch in batch_iter(missing_events, 20): @@ -661,7 +661,7 @@ class FederationHandler(BaseHandler): for e_id in batch ] - res = yield make_deferred_yieldable( + res = await make_deferred_yieldable( defer.DeferredList(deferreds, consumeErrors=True) ) for success, result in res: From 6637d90d778f5604b4827ab4f7d9a4cf05802466 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 10 Dec 2019 17:27:13 +0000 Subject: [PATCH 0675/1623] convert to async: FederationHandler._process_received_pdu also fix user_joined_room to consistently return deferreds --- synapse/handlers/federation.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 724cae9647..bcd3b422aa 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -670,8 +670,7 @@ class FederationHandler(BaseHandler): return fetched_events - @defer.inlineCallbacks - def _process_received_pdu(self, origin, event, state, auth_chain): + async def _process_received_pdu(self, origin, event, state, auth_chain): """ Called when we have a new pdu. We need to do auth checks and put it through the StateHandler. """ @@ -686,7 +685,7 @@ class FederationHandler(BaseHandler): if auth_chain: event_ids |= {e.event_id for e in auth_chain} - seen_ids = yield self.store.have_seen_events(event_ids) + seen_ids = await self.store.have_seen_events(event_ids) if state and auth_chain is not None: # If we have any state or auth_chain given to us by the replication @@ -713,18 +712,18 @@ class FederationHandler(BaseHandler): event_id, [e.event.event_id for e in event_infos], ) - yield self._handle_new_events(origin, event_infos) + await self._handle_new_events(origin, event_infos) try: - context = yield self._handle_new_event(origin, event, state=state) + context = await self._handle_new_event(origin, event, state=state) except AuthError as e: raise FederationError("ERROR", e.code, e.msg, affected=event.event_id) - room = yield self.store.get_room(room_id) + room = await self.store.get_room(room_id) if not room: try: - yield self.store.store_room( + await self.store.store_room( room_id=room_id, room_creator_user_id="", is_public=False ) except StoreError: @@ -737,11 +736,11 @@ class FederationHandler(BaseHandler): # changing their profile info. newly_joined = True - prev_state_ids = yield context.get_prev_state_ids(self.store) + prev_state_ids = await context.get_prev_state_ids(self.store) prev_state_id = prev_state_ids.get((event.type, event.state_key)) if prev_state_id: - prev_state = yield self.store.get_event( + prev_state = await self.store.get_event( prev_state_id, allow_none=True ) if prev_state and prev_state.membership == Membership.JOIN: @@ -749,7 +748,7 @@ class FederationHandler(BaseHandler): if newly_joined: user = UserID.from_string(event.state_key) - yield self.user_joined_room(user, room_id) + await self.user_joined_room(user, room_id) @log_function async def backfill(self, dest, room_id, limit, extremities): @@ -2899,7 +2898,7 @@ class FederationHandler(BaseHandler): room_id=room_id, user_id=user.to_string(), change="joined" ) else: - return user_joined_room(self.distributor, user, room_id) + return defer.succeed(user_joined_room(self.distributor, user, room_id)) @defer.inlineCallbacks def get_room_complexity(self, remote_room_hosts, room_id): From 5324bc20a628116aecf4781744cadd611f51d7a6 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 10 Dec 2019 17:59:46 +0000 Subject: [PATCH 0676/1623] changelog --- changelog.d/6517.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6517.misc diff --git a/changelog.d/6517.misc b/changelog.d/6517.misc new file mode 100644 index 0000000000..c6ffed9952 --- /dev/null +++ b/changelog.d/6517.misc @@ -0,0 +1 @@ +Port some of FederationHandler to async/await. \ No newline at end of file From 58fdcbdfe7f064f336f514b921a556c116c757cb Mon Sep 17 00:00:00 2001 From: Mark Nowiasz <36151963+mnowiasz@users.noreply.github.com> Date: Wed, 11 Dec 2019 17:23:38 +0100 Subject: [PATCH 0677/1623] Update workers.md to make media_repository work (again) (#6519) --- docs/workers.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/workers.md b/docs/workers.md index 4bd60ba0a0..1b5d94f5eb 100644 --- a/docs/workers.md +++ b/docs/workers.md @@ -196,7 +196,7 @@ Handles the media repository. It can handle all endpoints starting with: /_matrix/media/ -And the following regular expressions matching media-specific administration APIs: +... and the following regular expressions matching media-specific administration APIs: ^/_synapse/admin/v1/purge_media_cache$ ^/_synapse/admin/v1/room/.*/media$ @@ -206,6 +206,18 @@ You should also set `enable_media_repo: False` in the shared configuration file to stop the main synapse running background jobs related to managing the media repository. +In the `media_repository` worker configuration file, configure the http listener to +expose the `media` resource. For example: + +```yaml + worker_listeners: + - type: http + port: 8085 + resources: + - names: + - media +``` + Note this worker cannot be load-balanced: only one instance should be active. ### `synapse.app.client_reader` From 20453565176cfd358212a23cf89dfd2deab1d690 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 11 Dec 2019 16:37:51 +0000 Subject: [PATCH 0678/1623] Add `include_event_in_state` to _get_state_for_room (#6521) Make it return the state *after* the requested event, rather than the one before it. This is a bit easier and requires fewer calls to get_events_from_store_or_dest. --- changelog.d/6521.misc | 1 + synapse/handlers/federation.py | 50 +++++++++++++++++++--------------- 2 files changed, 29 insertions(+), 22 deletions(-) create mode 100644 changelog.d/6521.misc diff --git a/changelog.d/6521.misc b/changelog.d/6521.misc new file mode 100644 index 0000000000..d9a44389b9 --- /dev/null +++ b/changelog.d/6521.misc @@ -0,0 +1 @@ +Refactor some code in the event authentication path for clarity. diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index bcd3b422aa..62985bab9f 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -378,22 +378,10 @@ class FederationHandler(BaseHandler): ( remote_state, got_auth_chain, - ) = await self._get_state_for_room(origin, room_id, p) - - # we want the state *after* p; _get_state_for_room returns the - # state *before* p. - remote_event = await self.federation_client.get_pdu( - [origin], p, room_version, outlier=True + ) = await self._get_state_for_room( + origin, room_id, p, include_event_in_state=True ) - if remote_event is None: - raise Exception( - "Unable to get missing prev_event %s" % (p,) - ) - - if remote_event.is_state(): - remote_state.append(remote_event) - # XXX hrm I'm not convinced that duplicate events will compare # for equality, so I'm not sure this does what the author # hoped. @@ -579,20 +567,25 @@ class FederationHandler(BaseHandler): else: raise - @log_function async def _get_state_for_room( - self, destination: str, room_id: str, event_id: str + self, + destination: str, + room_id: str, + event_id: str, + include_event_in_state: bool = False, ) -> Tuple[List[EventBase], List[EventBase]]: """Requests all of the room state at a given event from a remote homeserver. Args: - destination:: The remote homeserver to query for the state. + destination: The remote homeserver to query for the state. room_id: The id of the room we're interested in. event_id: The id of the event we want the state at. + include_event_in_state: if true, the event itself will be included in the + returned state event list. Returns: - A list of events in the state, and a list of events in the auth chain - for the given event. + A list of events in the state, possibly including the event itself, and + a list of events in the auth chain for the given event. """ ( state_event_ids, @@ -602,6 +595,10 @@ class FederationHandler(BaseHandler): ) desired_events = set(state_event_ids + auth_event_ids) + + if include_event_in_state: + desired_events.add(event_id) + event_map = await self._get_events_from_store_or_dest( destination, room_id, desired_events ) @@ -614,12 +611,21 @@ class FederationHandler(BaseHandler): failed_to_fetch, ) - pdus = [event_map[e_id] for e_id in state_event_ids if e_id in event_map] - auth_chain = [event_map[e_id] for e_id in auth_event_ids if e_id in event_map] + remote_state = [ + event_map[e_id] for e_id in state_event_ids if e_id in event_map + ] + if include_event_in_state: + remote_event = event_map.get(event_id) + if not remote_event: + raise Exception("Unable to get missing prev_event %s" % (event_id,)) + if remote_event.is_state(): + remote_state.append(remote_event) + + auth_chain = [event_map[e_id] for e_id in auth_event_ids if e_id in event_map] auth_chain.sort(key=lambda e: e.depth) - return pdus, auth_chain + return remote_state, auth_chain async def _get_events_from_store_or_dest( self, destination: str, room_id: str, event_ids: Iterable[str] From cfcfb57e581bc1577a5fba3b34a1b1af7ccb6c0d Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 11 Dec 2019 17:27:46 +0000 Subject: [PATCH 0679/1623] Add new config param to docstring and add types --- synapse/server.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/synapse/server.py b/synapse/server.py index a8b5459ff3..5021068ce0 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -34,6 +34,7 @@ from synapse.api.filtering import Filtering from synapse.api.ratelimiting import Ratelimiter from synapse.appservice.api import ApplicationServiceApi from synapse.appservice.scheduler import ApplicationServiceScheduler +from synapse.config.homeserver import HomeServerConfig from synapse.crypto import context_factory from synapse.crypto.keyring import Keyring from synapse.events.builder import EventBuilderFactory @@ -210,10 +211,11 @@ class HomeServer(object): # instantiated during setup() for future return by get_datastore() DATASTORE_CLASS = abc.abstractproperty() - def __init__(self, hostname, config, reactor=None, **kwargs): + def __init__(self, hostname: str, config: HomeServerConfig, reactor=None, **kwargs): """ Args: hostname : The hostname for the server. + config: The full config for the homeserver. """ if not reactor: from twisted.internet import reactor From 25f12443298f4995613a475ac304e84d50317e18 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 12 Dec 2019 12:57:45 +0000 Subject: [PATCH 0680/1623] Check the room_id of events when fetching room state/auth (#6524) When we request the state/auth_events to populate a backwards extremity (on backfill or in the case of missing events in a transaction push), we should check that the returned events are in the right room rather than blindly using them in the room state or auth chain. Given that _get_events_from_store_or_dest takes a room_id, it seems clear that it should be sanity-checking the room_id of the requested events, so let's do it there. --- changelog.d/6524.misc | 2 + synapse/handlers/federation.py | 78 +++++++++++++++++++++++----------- 2 files changed, 55 insertions(+), 25 deletions(-) create mode 100644 changelog.d/6524.misc diff --git a/changelog.d/6524.misc b/changelog.d/6524.misc new file mode 100644 index 0000000000..f885597426 --- /dev/null +++ b/changelog.d/6524.misc @@ -0,0 +1,2 @@ +Improve sanity-checking when receiving events over federation. + diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 62985bab9f..2ea69c5468 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -637,6 +637,10 @@ class FederationHandler(BaseHandler): room_id event_ids + If we fail to fetch any of the events, a warning will be logged, and the event + will be omitted from the result. Likewise, any events which turn out not to + be in the given room. + Returns: map from event_id to event """ @@ -644,35 +648,59 @@ class FederationHandler(BaseHandler): missing_events = set(event_ids) - fetched_events.keys() - if not missing_events: - return fetched_events + if missing_events: + logger.debug( + "Fetching unknown state/auth events %s for room %s", + missing_events, + room_id, + ) - logger.debug( - "Fetching unknown state/auth events %s for room %s", - missing_events, - event_ids, + room_version = await self.store.get_room_version(room_id) + + # XXX 20 requests at once? really? + for batch in batch_iter(missing_events, 20): + deferreds = [ + run_in_background( + self.federation_client.get_pdu, + destinations=[destination], + event_id=e_id, + room_version=room_version, + ) + for e_id in batch + ] + + res = await make_deferred_yieldable( + defer.DeferredList(deferreds, consumeErrors=True) + ) + + for success, result in res: + if success and result: + fetched_events[result.event_id] = result + + # check for events which were in the wrong room. + # + # this can happen if a remote server claims that the state or + # auth_events at an event in room A are actually events in room B + + bad_events = list( + (event_id, event.room_id) + for event_id, event in fetched_events.items() + if event.room_id != room_id ) - room_version = await self.store.get_room_version(room_id) - - # XXX 20 requests at once? really? - for batch in batch_iter(missing_events, 20): - deferreds = [ - run_in_background( - self.federation_client.get_pdu, - destinations=[destination], - event_id=e_id, - room_version=room_version, - ) - for e_id in batch - ] - - res = await make_deferred_yieldable( - defer.DeferredList(deferreds, consumeErrors=True) + for bad_event_id, bad_room_id in bad_events: + # This is a bogus situation, but since we may only discover it a long time + # after it happened, we try our best to carry on, by just omitting the + # bad events from the returned auth/state set. + logger.warning( + "Remote server %s claims event %s in room %s is an auth/state " + "event in room %s", + destination, + bad_event_id, + bad_room_id, + room_id, ) - for success, result in res: - if success and result: - fetched_events[result.event_id] = result + del fetched_events[bad_event_id] return fetched_events From 324d4f61b89d1fe16833480d5bd5b47d3b4ef0ba Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 12 Dec 2019 14:52:11 +0000 Subject: [PATCH 0681/1623] Include more folders in mypy --- tox.ini | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 903a245fb0..2ae82d674f 100644 --- a/tox.ini +++ b/tox.ini @@ -177,5 +177,17 @@ env = MYPYPATH = stubs/ extras = all commands = mypy \ + synapse/config/ \ + synapse/handlers/ui_auth \ synapse/logging/ \ - synapse/config/ + synapse/module_api \ + synapse/rest/consent \ + synapse/rest/media/v0 \ + synapse/rest/saml2 \ + synapse/spam_checker_api \ + synapse/storage/engines \ + synapse/streams + +# To find all folders that pass mypy you run: +# +# find synapse/* -type d -not -name __pycache__ -exec bash -c "mypy '{}' > /dev/null" \; -print From c965253e4b38b9d941793469ef64b014e5a25947 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 12 Dec 2019 14:54:03 +0000 Subject: [PATCH 0682/1623] Newsfile --- changelog.d/6534.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6534.misc diff --git a/changelog.d/6534.misc b/changelog.d/6534.misc new file mode 100644 index 0000000000..7df6bb442a --- /dev/null +++ b/changelog.d/6534.misc @@ -0,0 +1 @@ +Test more folders against mypy. From 495005360cd37009818ad4214a5462c3dd0b15a6 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 12 Dec 2019 15:21:12 +0000 Subject: [PATCH 0683/1623] Bump version of mypy --- synapse/config/emailconfig.py | 3 ++- synapse/config/ratelimiting.py | 3 +-- synapse/config/server.py | 2 +- synapse/logging/_terse_json.py | 2 +- synapse/logging/context.py | 3 +++ synapse/streams/events.py | 4 +++- tox.ini | 2 +- 7 files changed, 12 insertions(+), 7 deletions(-) diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py index 18f42a87f9..35756bed87 100644 --- a/synapse/config/emailconfig.py +++ b/synapse/config/emailconfig.py @@ -21,6 +21,7 @@ from __future__ import print_function import email.utils import os from enum import Enum +from typing import Optional import pkg_resources @@ -101,7 +102,7 @@ class EmailConfig(Config): # both in RegistrationConfig and here. We should factor this bit out self.account_threepid_delegate_email = self.trusted_third_party_id_servers[ 0 - ] + ] # type: Optional[str] self.using_identity_server_from_trusted_list = True else: raise ConfigError( diff --git a/synapse/config/ratelimiting.py b/synapse/config/ratelimiting.py index 947f653e03..4a3bfc4354 100644 --- a/synapse/config/ratelimiting.py +++ b/synapse/config/ratelimiting.py @@ -83,10 +83,9 @@ class RatelimitConfig(Config): ) rc_admin_redaction = config.get("rc_admin_redaction") + self.rc_admin_redaction = None if rc_admin_redaction: self.rc_admin_redaction = RateLimitConfig(rc_admin_redaction) - else: - self.rc_admin_redaction = None def generate_config_section(self, **kwargs): return """\ diff --git a/synapse/config/server.py b/synapse/config/server.py index a4bef00936..50af858c76 100644 --- a/synapse/config/server.py +++ b/synapse/config/server.py @@ -200,7 +200,7 @@ class ServerConfig(Config): self.admin_contact = config.get("admin_contact", None) # FIXME: federation_domain_whitelist needs sytests - self.federation_domain_whitelist = None + self.federation_domain_whitelist = None # type: Optional[dict] federation_domain_whitelist = config.get("federation_domain_whitelist", None) if federation_domain_whitelist is not None: diff --git a/synapse/logging/_terse_json.py b/synapse/logging/_terse_json.py index 03934956f4..c0b9384189 100644 --- a/synapse/logging/_terse_json.py +++ b/synapse/logging/_terse_json.py @@ -171,7 +171,7 @@ class LogProducer(object): def stopProducing(self): self._paused = True - self._buffer = None + self._buffer = deque() def resumeProducing(self): self._paused = False diff --git a/synapse/logging/context.py b/synapse/logging/context.py index 6747f29e6a..33b322209d 100644 --- a/synapse/logging/context.py +++ b/synapse/logging/context.py @@ -405,6 +405,9 @@ class LoggingContext(object): """ current = get_thread_resource_usage() + # Indicate to mypy that we know that self.usage_start is None. + assert self.usage_start is not None + utime_delta = current.ru_utime - self.usage_start.ru_utime stime_delta = current.ru_stime - self.usage_start.ru_stime diff --git a/synapse/streams/events.py b/synapse/streams/events.py index b91fb2db7b..fcd2aaa9c9 100644 --- a/synapse/streams/events.py +++ b/synapse/streams/events.py @@ -13,6 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Any, Dict + from twisted.internet import defer from synapse.handlers.account_data import AccountDataEventSource @@ -35,7 +37,7 @@ class EventSources(object): def __init__(self, hs): self.sources = { name: cls(hs) for name, cls in EventSources.SOURCE_TYPES.items() - } + } # type: Dict[str, Any] self.store = hs.get_datastore() @defer.inlineCallbacks diff --git a/tox.ini b/tox.ini index 2ae82d674f..1d6428f64f 100644 --- a/tox.ini +++ b/tox.ini @@ -171,7 +171,7 @@ basepython = python3.7 skip_install = True deps = {[base]deps} - mypy==0.730 + mypy==0.750 mypy-zope env = MYPYPATH = stubs/ From 5056d6d90a58edc2a70d19233082f24fded4c73f Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 12 Dec 2019 15:22:46 +0000 Subject: [PATCH 0684/1623] Newsfile --- changelog.d/6537.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6537.misc diff --git a/changelog.d/6537.misc b/changelog.d/6537.misc new file mode 100644 index 0000000000..3543153584 --- /dev/null +++ b/changelog.d/6537.misc @@ -0,0 +1 @@ +Update `mypy` to new version. From 5bfd8855d6b9ed8bcf28a107e6654c7cd7d3da2b Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Thu, 12 Dec 2019 15:53:49 +0000 Subject: [PATCH 0685/1623] Fix redacted events being returned in search results ordered by "recent" (#6522) --- changelog.d/6522.bugfix | 1 + synapse/storage/data_stores/main/search.py | 11 ++++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 changelog.d/6522.bugfix diff --git a/changelog.d/6522.bugfix b/changelog.d/6522.bugfix new file mode 100644 index 0000000000..ccda96962f --- /dev/null +++ b/changelog.d/6522.bugfix @@ -0,0 +1 @@ +Prevent redacted events from being returned during message search. \ No newline at end of file diff --git a/synapse/storage/data_stores/main/search.py b/synapse/storage/data_stores/main/search.py index dfb46ee0f8..47ebb8a214 100644 --- a/synapse/storage/data_stores/main/search.py +++ b/synapse/storage/data_stores/main/search.py @@ -385,7 +385,7 @@ class SearchStore(SearchBackgroundUpdateStore): """ clauses = [] - search_query = search_query = _parse_query(self.database_engine, search_term) + search_query = _parse_query(self.database_engine, search_term) args = [] @@ -501,7 +501,7 @@ class SearchStore(SearchBackgroundUpdateStore): """ clauses = [] - search_query = search_query = _parse_query(self.database_engine, search_term) + search_query = _parse_query(self.database_engine, search_term) args = [] @@ -606,7 +606,12 @@ class SearchStore(SearchBackgroundUpdateStore): results = list(filter(lambda row: row["room_id"] in room_ids, results)) - events = yield self.get_events_as_list([r["event_id"] for r in results]) + # We set redact_behaviour to BLOCK here to prevent redacted events being returned in + # search results (which is a data leak) + events = yield self.get_events_as_list( + [r["event_id"] for r in results], + redact_behaviour=EventRedactBehaviour.BLOCK, + ) event_map = {ev.event_id: ev for ev in events} From cb2db179945f567410b565f29725dff28449f013 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 12 Dec 2019 12:03:28 -0500 Subject: [PATCH 0686/1623] look up cross-signing keys from the DB in bulk (#6486) --- changelog.d/6486.bugfix | 1 + synapse/handlers/e2e_keys.py | 35 ++- .../data_stores/main/end_to_end_keys.py | 217 +++++++++++++++++- synapse/util/caches/descriptors.py | 2 +- tests/handlers/test_e2e_keys.py | 8 - 5 files changed, 242 insertions(+), 21 deletions(-) create mode 100644 changelog.d/6486.bugfix diff --git a/changelog.d/6486.bugfix b/changelog.d/6486.bugfix new file mode 100644 index 0000000000..b98c5a9ae5 --- /dev/null +++ b/changelog.d/6486.bugfix @@ -0,0 +1 @@ +Improve performance of looking up cross-signing keys. diff --git a/synapse/handlers/e2e_keys.py b/synapse/handlers/e2e_keys.py index 57a10daefd..2d889364d4 100644 --- a/synapse/handlers/e2e_keys.py +++ b/synapse/handlers/e2e_keys.py @@ -264,6 +264,7 @@ class E2eKeysHandler(object): return ret + @defer.inlineCallbacks def get_cross_signing_keys_from_cache(self, query, from_user_id): """Get cross-signing keys for users from the database @@ -283,14 +284,32 @@ class E2eKeysHandler(object): self_signing_keys = {} user_signing_keys = {} - # Currently a stub, implementation coming in https://github.com/matrix-org/synapse/pull/6486 - return defer.succeed( - { - "master_keys": master_keys, - "self_signing_keys": self_signing_keys, - "user_signing_keys": user_signing_keys, - } - ) + user_ids = list(query) + + keys = yield self.store.get_e2e_cross_signing_keys_bulk(user_ids, from_user_id) + + for user_id, user_info in keys.items(): + if user_info is None: + continue + if "master" in user_info: + master_keys[user_id] = user_info["master"] + if "self_signing" in user_info: + self_signing_keys[user_id] = user_info["self_signing"] + + if ( + from_user_id in keys + and keys[from_user_id] is not None + and "user_signing" in keys[from_user_id] + ): + # users can see other users' master and self-signing keys, but can + # only see their own user-signing keys + user_signing_keys[from_user_id] = keys[from_user_id]["user_signing"] + + return { + "master_keys": master_keys, + "self_signing_keys": self_signing_keys, + "user_signing_keys": user_signing_keys, + } @trace @defer.inlineCallbacks diff --git a/synapse/storage/data_stores/main/end_to_end_keys.py b/synapse/storage/data_stores/main/end_to_end_keys.py index 38cd0ca9b8..e551606f9d 100644 --- a/synapse/storage/data_stores/main/end_to_end_keys.py +++ b/synapse/storage/data_stores/main/end_to_end_keys.py @@ -14,15 +14,18 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from typing import Dict, List + from six import iteritems from canonicaljson import encode_canonical_json, json +from twisted.enterprise.adbapi import Connection from twisted.internet import defer from synapse.logging.opentracing import log_kv, set_tag, trace from synapse.storage._base import SQLBaseStore, db_to_json -from synapse.util.caches.descriptors import cached +from synapse.util.caches.descriptors import cached, cachedList class EndToEndKeyWorkerStore(SQLBaseStore): @@ -271,7 +274,7 @@ class EndToEndKeyWorkerStore(SQLBaseStore): Args: txn (twisted.enterprise.adbapi.Connection): db connection user_id (str): the user whose key is being requested - key_type (str): the type of key that is being set: either 'master' + key_type (str): the type of key that is being requested: either 'master' for a master key, 'self_signing' for a self-signing key, or 'user_signing' for a user-signing key from_user_id (str): if specified, signatures made by this user on @@ -316,8 +319,10 @@ class EndToEndKeyWorkerStore(SQLBaseStore): """Returns a user's cross-signing key. Args: - user_id (str): the user whose self-signing key is being requested - key_type (str): the type of cross-signing key to get + user_id (str): the user whose key is being requested + key_type (str): the type of key that is being requested: either 'master' + for a master key, 'self_signing' for a self-signing key, or + 'user_signing' for a user-signing key from_user_id (str): if specified, signatures made by this user on the self-signing key will be included in the result @@ -332,6 +337,206 @@ class EndToEndKeyWorkerStore(SQLBaseStore): from_user_id, ) + @cached(num_args=1) + def _get_bare_e2e_cross_signing_keys(self, user_id): + """Dummy function. Only used to make a cache for + _get_bare_e2e_cross_signing_keys_bulk. + """ + raise NotImplementedError() + + @cachedList( + cached_method_name="_get_bare_e2e_cross_signing_keys", + list_name="user_ids", + num_args=1, + ) + def _get_bare_e2e_cross_signing_keys_bulk( + self, user_ids: List[str] + ) -> Dict[str, Dict[str, dict]]: + """Returns the cross-signing keys for a set of users. The output of this + function should be passed to _get_e2e_cross_signing_signatures_txn if + the signatures for the calling user need to be fetched. + + Args: + user_ids (list[str]): the users whose keys are being requested + + Returns: + dict[str, dict[str, dict]]: mapping from user ID to key type to key + data. If a user's cross-signing keys were not found, either + their user ID will not be in the dict, or their user ID will map + to None. + + """ + return self.db.runInteraction( + "get_bare_e2e_cross_signing_keys_bulk", + self._get_bare_e2e_cross_signing_keys_bulk_txn, + user_ids, + ) + + def _get_bare_e2e_cross_signing_keys_bulk_txn( + self, txn: Connection, user_ids: List[str], + ) -> Dict[str, Dict[str, dict]]: + """Returns the cross-signing keys for a set of users. The output of this + function should be passed to _get_e2e_cross_signing_signatures_txn if + the signatures for the calling user need to be fetched. + + Args: + txn (twisted.enterprise.adbapi.Connection): db connection + user_ids (list[str]): the users whose keys are being requested + + Returns: + dict[str, dict[str, dict]]: mapping from user ID to key type to key + data. If a user's cross-signing keys were not found, their user + ID will not be in the dict. + + """ + result = {} + + batch_size = 100 + chunks = [ + user_ids[i : i + batch_size] for i in range(0, len(user_ids), batch_size) + ] + for user_chunk in chunks: + sql = """ + SELECT k.user_id, k.keytype, k.keydata, k.stream_id + FROM e2e_cross_signing_keys k + INNER JOIN (SELECT user_id, keytype, MAX(stream_id) AS stream_id + FROM e2e_cross_signing_keys + GROUP BY user_id, keytype) s + USING (user_id, stream_id, keytype) + WHERE k.user_id IN (%s) + """ % ( + ",".join("?" for u in user_chunk), + ) + query_params = [] + query_params.extend(user_chunk) + + txn.execute(sql, query_params) + rows = self.db.cursor_to_dict(txn) + + for row in rows: + user_id = row["user_id"] + key_type = row["keytype"] + key = json.loads(row["keydata"]) + user_info = result.setdefault(user_id, {}) + user_info[key_type] = key + + return result + + def _get_e2e_cross_signing_signatures_txn( + self, txn: Connection, keys: Dict[str, Dict[str, dict]], from_user_id: str, + ) -> Dict[str, Dict[str, dict]]: + """Returns the cross-signing signatures made by a user on a set of keys. + + Args: + txn (twisted.enterprise.adbapi.Connection): db connection + keys (dict[str, dict[str, dict]]): a map of user ID to key type to + key data. This dict will be modified to add signatures. + from_user_id (str): fetch the signatures made by this user + + Returns: + dict[str, dict[str, dict]]: mapping from user ID to key type to key + data. The return value will be the same as the keys argument, + with the modifications included. + """ + + # find out what cross-signing keys (a.k.a. devices) we need to get + # signatures for. This is a map of (user_id, device_id) to key type + # (device_id is the key's public part). + devices = {} + + for user_id, user_info in keys.items(): + if user_info is None: + continue + for key_type, key in user_info.items(): + device_id = None + for k in key["keys"].values(): + device_id = k + devices[(user_id, device_id)] = key_type + + device_list = list(devices) + + # split into batches + batch_size = 100 + chunks = [ + device_list[i : i + batch_size] + for i in range(0, len(device_list), batch_size) + ] + for user_chunk in chunks: + sql = """ + SELECT target_user_id, target_device_id, key_id, signature + FROM e2e_cross_signing_signatures + WHERE user_id = ? + AND (%s) + """ % ( + " OR ".join( + "(target_user_id = ? AND target_device_id = ?)" for d in devices + ) + ) + query_params = [from_user_id] + for item in devices: + # item is a (user_id, device_id) tuple + query_params.extend(item) + + txn.execute(sql, query_params) + rows = self.db.cursor_to_dict(txn) + + # and add the signatures to the appropriate keys + for row in rows: + key_id = row["key_id"] + target_user_id = row["target_user_id"] + target_device_id = row["target_device_id"] + key_type = devices[(target_user_id, target_device_id)] + # We need to copy everything, because the result may have come + # from the cache. dict.copy only does a shallow copy, so we + # need to recursively copy the dicts that will be modified. + user_info = keys[target_user_id] = keys[target_user_id].copy() + target_user_key = user_info[key_type] = user_info[key_type].copy() + if "signatures" in target_user_key: + signatures = target_user_key["signatures"] = target_user_key[ + "signatures" + ].copy() + if from_user_id in signatures: + user_sigs = signatures[from_user_id] = signatures[from_user_id] + user_sigs[key_id] = row["signature"] + else: + signatures[from_user_id] = {key_id: row["signature"]} + else: + target_user_key["signatures"] = { + from_user_id: {key_id: row["signature"]} + } + + return keys + + @defer.inlineCallbacks + def get_e2e_cross_signing_keys_bulk( + self, user_ids: List[str], from_user_id: str = None + ) -> defer.Deferred: + """Returns the cross-signing keys for a set of users. + + Args: + user_ids (list[str]): the users whose keys are being requested + from_user_id (str): if specified, signatures made by this user on + the self-signing keys will be included in the result + + Returns: + Deferred[dict[str, dict[str, dict]]]: map of user ID to key type to + key data. If a user's cross-signing keys were not found, either + their user ID will not be in the dict, or their user ID will map + to None. + """ + + result = yield self._get_bare_e2e_cross_signing_keys_bulk(user_ids) + + if from_user_id: + result = yield self.db.runInteraction( + "get_e2e_cross_signing_signatures", + self._get_e2e_cross_signing_signatures_txn, + result, + from_user_id, + ) + + return result + def get_all_user_signature_changes_for_remotes(self, from_key, to_key): """Return a list of changes from the user signature stream to notify remotes. Note that the user signature stream represents when a user signs their @@ -520,6 +725,10 @@ class EndToEndKeyStore(EndToEndKeyWorkerStore, SQLBaseStore): }, ) + self._invalidate_cache_and_stream( + txn, self._get_bare_e2e_cross_signing_keys, (user_id,) + ) + def set_e2e_cross_signing_key(self, user_id, key_type, key): """Set a user's cross-signing key. diff --git a/synapse/util/caches/descriptors.py b/synapse/util/caches/descriptors.py index 84f5ae22c3..2e8f6543e5 100644 --- a/synapse/util/caches/descriptors.py +++ b/synapse/util/caches/descriptors.py @@ -271,7 +271,7 @@ class _CacheDescriptorBase(object): else: self.function_to_call = orig - arg_spec = inspect.getargspec(orig) + arg_spec = inspect.getfullargspec(orig) all_args = arg_spec.args if "cache_context" in all_args: diff --git a/tests/handlers/test_e2e_keys.py b/tests/handlers/test_e2e_keys.py index fdfa2cbbc4..854eb6c024 100644 --- a/tests/handlers/test_e2e_keys.py +++ b/tests/handlers/test_e2e_keys.py @@ -183,10 +183,6 @@ class E2eKeysHandlerTestCase(unittest.TestCase): ) self.assertDictEqual(devices["master_keys"], {local_user: keys2["master_key"]}) - test_replace_master_key.skip = ( - "Disabled waiting on #https://github.com/matrix-org/synapse/pull/6486" - ) - @defer.inlineCallbacks def test_reupload_signatures(self): """re-uploading a signature should not fail""" @@ -507,7 +503,3 @@ class E2eKeysHandlerTestCase(unittest.TestCase): ], other_master_key["signatures"][local_user]["ed25519:" + usersigning_pubkey], ) - - test_upload_signatures.skip = ( - "Disabled waiting on #https://github.com/matrix-org/synapse/pull/6486" - ) From 4ce05ec1716f757eb15c02e615ea9c84cb289b77 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 13 Dec 2019 10:15:20 +0000 Subject: [PATCH 0687/1623] Adjust the sytest blacklist for worker mode (#6538) Remove tests that got blacklisted while torturing was enabled, and add one that fails. --- .buildkite/worker-blacklist | 31 +++---------------------------- changelog.d/6538.misc | 1 + 2 files changed, 4 insertions(+), 28 deletions(-) create mode 100644 changelog.d/6538.misc diff --git a/.buildkite/worker-blacklist b/.buildkite/worker-blacklist index 7950d19db3..158ab79154 100644 --- a/.buildkite/worker-blacklist +++ b/.buildkite/worker-blacklist @@ -34,33 +34,8 @@ Device list doesn't change if remote server is down Remote servers cannot set power levels in rooms without existing powerlevels Remote servers should reject attempts by non-creators to set the power levels -# new failures as of https://github.com/matrix-org/sytest/pull/753 -GET /rooms/:room_id/messages returns a message -GET /rooms/:room_id/messages lazy loads members correctly -Read receipts are sent as events -Only original members of the room can see messages from erased users -Device deletion propagates over federation -If user leaves room, remote user changes device and rejoins we see update in /sync and /keys/changes -Changing user-signing key notifies local users -Newly updated tags appear in an incremental v2 /sync +# https://buildkite.com/matrix-dot-org/synapse/builds/6134#6f67bf47-e234-474d-80e8-c6e1868b15c5 Server correctly handles incoming m.device_list_update -Local device key changes get to remote servers with correct prev_id -AS-ghosted users can use rooms via AS -Ghost user must register before joining room -Test that a message is pushed -Invites are pushed -Rooms with aliases are correctly named in pushed -Rooms with names are correctly named in pushed -Rooms with canonical alias are correctly named in pushed -Rooms with many users are correctly pushed -Don't get pushed for rooms you've muted -Rejected events are not pushed -Test that rejected pushers are removed. -Events come down the correct room -# https://buildkite.com/matrix-dot-org/sytest/builds/326#cca62404-a88a-4fcb-ad41-175fd3377603 -Presence changes to UNAVAILABLE are reported to remote room members -If remote user leaves room, changes device and rejoins we see update in sync -uploading self-signing key notifies over federation -Inbound federation can receive redacted events -Outbound federation can request missing events +# this fails reliably with a torture level of 100 due to https://github.com/matrix-org/synapse/issues/6536 +Outbound federation requests missing prev_events and then asks for /state_ids and resolves the state diff --git a/changelog.d/6538.misc b/changelog.d/6538.misc new file mode 100644 index 0000000000..cb4fd56948 --- /dev/null +++ b/changelog.d/6538.misc @@ -0,0 +1 @@ +Adjust the sytest blacklist for worker mode. From f5aeea9e894244263a9d27602fc67dd51fead4d5 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 13 Dec 2019 10:19:53 +0000 Subject: [PATCH 0688/1623] 1.7.0 --- CHANGES.md | 6 ++++++ debian/changelog | 6 ++++++ synapse/__init__.py | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index c83a6afbcd..e4c12196e0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,9 @@ +Synapse 1.7.0 (2019-12-13) +========================== + +No significant changes. + + Synapse 1.7.0rc2 (2019-12-11) ============================= diff --git a/debian/changelog b/debian/changelog index b8a43788ef..bd43feb321 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.7.0) stable; urgency=medium + + * New synapse release 1.7.0. + + -- Synapse Packaging team Fri, 13 Dec 2019 10:19:38 +0000 + matrix-synapse-py3 (1.6.1) stable; urgency=medium * New synapse release 1.6.1. diff --git a/synapse/__init__.py b/synapse/__init__.py index fc2a6e4ee6..d3cf7b3d7b 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -36,7 +36,7 @@ try: except ImportError: pass -__version__ = "1.7.0rc2" +__version__ = "1.7.0" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From d046f367a4faf36de7010a93095a488d7db646ea Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 13 Dec 2019 10:36:35 +0000 Subject: [PATCH 0689/1623] Add deprecation notes --- CHANGES.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index e4c12196e0..cfa4f65704 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,7 +1,13 @@ Synapse 1.7.0 (2019-12-13) ========================== -No significant changes. +This release changes the default settings so that only local authenticated users can query the server's room directory. See the [upgrade notes](UPGRADE.rst#upgrading-to-v170) for details. + +Support for SQLite 3.7 is now deprecated, with the recommended minimum version being SQLite 3.11. A future release will hard error if used with an SQLite version before 3.11. + +Additionally, using SQLite with federation enabled is deprecated, and a future release will default to disabling federation for servers using SQLite. + +No significant changes since 1.7.0rc2. Synapse 1.7.0rc2 (2019-12-11) From bac157801364a3c187b4ddb5cf3f05b9f0d0d327 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 13 Dec 2019 10:46:31 +0000 Subject: [PATCH 0690/1623] Reword changelog --- CHANGES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index cfa4f65704..d89abdf9d1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,9 +3,9 @@ Synapse 1.7.0 (2019-12-13) This release changes the default settings so that only local authenticated users can query the server's room directory. See the [upgrade notes](UPGRADE.rst#upgrading-to-v170) for details. -Support for SQLite 3.7 is now deprecated, with the recommended minimum version being SQLite 3.11. A future release will hard error if used with an SQLite version before 3.11. +Support for SQLite versions before 3.11 is now deprecated. A future release will refuse to start if used with an SQLite version before 3.11. -Additionally, using SQLite with federation enabled is deprecated, and a future release will default to disabling federation for servers using SQLite. +Administrators are reminded that SQLite should not be used for production instances. Instructions for migrating to Postgres are available [here](docs/postgres.md). A future release of synapse will disable federation for servers using SQLite unless adminstrators explicitly override the setting. No significant changes since 1.7.0rc2. From ba57a456449bdf32cdc3b4b7418aa5022d70f5e5 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 13 Dec 2019 10:52:25 +0000 Subject: [PATCH 0691/1623] More rewording of changelog. --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index d89abdf9d1..c8aa5d177f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,7 +5,7 @@ This release changes the default settings so that only local authenticated users Support for SQLite versions before 3.11 is now deprecated. A future release will refuse to start if used with an SQLite version before 3.11. -Administrators are reminded that SQLite should not be used for production instances. Instructions for migrating to Postgres are available [here](docs/postgres.md). A future release of synapse will disable federation for servers using SQLite unless adminstrators explicitly override the setting. +Administrators are reminded that SQLite should not be used for production instances. Instructions for migrating to Postgres are available [here](docs/postgres.md). A future release of synapse will, by default, disable federation for servers using SQLite. No significant changes since 1.7.0rc2. From 971a0702b5fce743c8bb61424a5f1002d3eb63ff Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 13 Dec 2019 11:44:41 +0000 Subject: [PATCH 0692/1623] Sanity-check room ids in event auth (#6530) When we do an event auth operation, check that all of the events involved are in the right room. --- changelog.d/6530.misc | 2 ++ synapse/event_auth.py | 12 ++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 changelog.d/6530.misc diff --git a/changelog.d/6530.misc b/changelog.d/6530.misc new file mode 100644 index 0000000000..f885597426 --- /dev/null +++ b/changelog.d/6530.misc @@ -0,0 +1,2 @@ +Improve sanity-checking when receiving events over federation. + diff --git a/synapse/event_auth.py b/synapse/event_auth.py index c940b84470..80ec911b3d 100644 --- a/synapse/event_auth.py +++ b/synapse/event_auth.py @@ -50,6 +50,18 @@ def check(room_version, event, auth_events, do_sig_check=True, do_size_check=Tru if not hasattr(event, "room_id"): raise AuthError(500, "Event has no room_id: %s" % event) + room_id = event.room_id + + # I'm not really expecting to get auth events in the wrong room, but let's + # sanity-check it + for auth_event in auth_events.values(): + if auth_event.room_id != room_id: + raise Exception( + "During auth for event %s in room %s, found event %s in the state " + "which is in room %s" + % (event.event_id, room_id, auth_event.event_id, auth_event.room_id) + ) + if do_sig_check: sender_domain = get_domain_from_id(event.sender) From 1da15f05f5c9c1e47c9fd1323caff869c2e55aa3 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 13 Dec 2019 12:55:32 +0000 Subject: [PATCH 0693/1623] sanity-checking for events used in state res (#6531) When we perform state resolution, check that all of the events involved are in the right room. --- changelog.d/6531.misc | 1 + synapse/handlers/federation.py | 1 + synapse/state/__init__.py | 32 +++++++---- synapse/state/v1.py | 34 +++++++++-- synapse/state/v2.py | 100 ++++++++++++++++++++++++--------- tests/state/test_v2.py | 3 + 6 files changed, 128 insertions(+), 43 deletions(-) create mode 100644 changelog.d/6531.misc diff --git a/changelog.d/6531.misc b/changelog.d/6531.misc new file mode 100644 index 0000000000..598efb79fc --- /dev/null +++ b/changelog.d/6531.misc @@ -0,0 +1 @@ +Improve sanity-checking when receiving events over federation. diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 2ea69c5468..1d39a9a4f5 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -396,6 +396,7 @@ class FederationHandler(BaseHandler): event_map[x.event_id] = x state_map = await resolve_events_with_store( + room_id, room_version, state_maps, event_map, diff --git a/synapse/state/__init__.py b/synapse/state/__init__.py index 3e6d62eef1..5accc071ab 100644 --- a/synapse/state/__init__.py +++ b/synapse/state/__init__.py @@ -16,7 +16,7 @@ import logging from collections import namedtuple -from typing import Iterable, Optional +from typing import Dict, Iterable, List, Optional, Tuple from six import iteritems, itervalues @@ -417,6 +417,7 @@ class StateHandler(object): with Measure(self.clock, "state._resolve_events"): new_state = yield resolve_events_with_store( + event.room_id, room_version, state_set_ids, event_map=state_map, @@ -462,7 +463,7 @@ class StateResolutionHandler(object): not be called for a single state group Args: - room_id (str): room we are resolving for (used for logging) + room_id (str): room we are resolving for (used for logging and sanity checks) room_version (str): version of the room state_groups_ids (dict[int, dict[(str, str), str]]): map from state group id to the state in that state group @@ -518,6 +519,7 @@ class StateResolutionHandler(object): logger.info("Resolving conflicted state for %r", room_id) with Measure(self.clock, "state._resolve_events"): new_state = yield resolve_events_with_store( + room_id, room_version, list(itervalues(state_groups_ids)), event_map=event_map, @@ -589,36 +591,44 @@ def _make_state_cache_entry(new_state, state_groups_ids): ) -def resolve_events_with_store(room_version, state_sets, event_map, state_res_store): +def resolve_events_with_store( + room_id: str, + room_version: str, + state_sets: List[Dict[Tuple[str, str], str]], + event_map: Optional[Dict[str, EventBase]], + state_res_store: "StateResolutionStore", +): """ Args: - room_version(str): Version of the room + room_id: the room we are working in - state_sets(list): List of dicts of (type, state_key) -> event_id, + room_version: Version of the room + + state_sets: List of dicts of (type, state_key) -> event_id, which are the different state groups to resolve. - event_map(dict[str,FrozenEvent]|None): + event_map: a dict from event_id to event, for any events that we happen to have in flight (eg, those currently being persisted). This will be used as a starting point fof finding the state we need; any missing events will be requested via state_map_factory. - If None, all events will be fetched via state_map_factory. + If None, all events will be fetched via state_res_store. - state_res_store (StateResolutionStore) + state_res_store: a place to fetch events from - Returns + Returns: Deferred[dict[(str, str), str]]: a map from (type, state_key) to event_id. """ v = KNOWN_ROOM_VERSIONS[room_version] if v.state_res == StateResolutionVersions.V1: return v1.resolve_events_with_store( - state_sets, event_map, state_res_store.get_events + room_id, state_sets, event_map, state_res_store.get_events ) else: return v2.resolve_events_with_store( - room_version, state_sets, event_map, state_res_store + room_id, room_version, state_sets, event_map, state_res_store ) diff --git a/synapse/state/v1.py b/synapse/state/v1.py index a2f92d9ff9..b2f9865f39 100644 --- a/synapse/state/v1.py +++ b/synapse/state/v1.py @@ -15,6 +15,7 @@ import hashlib import logging +from typing import Callable, Dict, List, Optional, Tuple from six import iteritems, iterkeys, itervalues @@ -24,6 +25,7 @@ from synapse import event_auth from synapse.api.constants import EventTypes from synapse.api.errors import AuthError from synapse.api.room_versions import RoomVersions +from synapse.events import EventBase logger = logging.getLogger(__name__) @@ -32,13 +34,20 @@ POWER_KEY = (EventTypes.PowerLevels, "") @defer.inlineCallbacks -def resolve_events_with_store(state_sets, event_map, state_map_factory): +def resolve_events_with_store( + room_id: str, + state_sets: List[Dict[Tuple[str, str], str]], + event_map: Optional[Dict[str, EventBase]], + state_map_factory: Callable, +): """ Args: - state_sets(list): List of dicts of (type, state_key) -> event_id, + room_id: the room we are working in + + state_sets: List of dicts of (type, state_key) -> event_id, which are the different state groups to resolve. - event_map(dict[str,FrozenEvent]|None): + event_map: a dict from event_id to event, for any events that we happen to have in flight (eg, those currently being persisted). This will be used as a starting point fof finding the state we need; any missing @@ -46,11 +55,11 @@ def resolve_events_with_store(state_sets, event_map, state_map_factory): If None, all events will be fetched via state_map_factory. - state_map_factory(func): will be called + state_map_factory: will be called with a list of event_ids that are needed, and should return with a Deferred of dict of event_id to event. - Returns + Returns: Deferred[dict[(str, str), str]]: a map from (type, state_key) to event_id. """ @@ -76,6 +85,14 @@ def resolve_events_with_store(state_sets, event_map, state_map_factory): if event_map is not None: state_map.update(event_map) + # everything in the state map should be in the right room + for event in state_map.values(): + if event.room_id != room_id: + raise Exception( + "Attempting to state-resolve for room %s with event %s which is in %s" + % (room_id, event.event_id, event.room_id,) + ) + # get the ids of the auth events which allow us to authenticate the # conflicted state, picking only from the unconflicting state. # @@ -95,6 +112,13 @@ def resolve_events_with_store(state_sets, event_map, state_map_factory): ) state_map_new = yield state_map_factory(new_needed_events) + for event in state_map_new.values(): + if event.room_id != room_id: + raise Exception( + "Attempting to state-resolve for room %s with event %s which is in %s" + % (room_id, event.event_id, event.room_id,) + ) + state_map.update(state_map_new) return _resolve_with_state( diff --git a/synapse/state/v2.py b/synapse/state/v2.py index b327c86f40..cb77ed5b78 100644 --- a/synapse/state/v2.py +++ b/synapse/state/v2.py @@ -16,29 +16,40 @@ import heapq import itertools import logging +from typing import Dict, List, Optional, Tuple from six import iteritems, itervalues from twisted.internet import defer +import synapse.state from synapse import event_auth from synapse.api.constants import EventTypes from synapse.api.errors import AuthError +from synapse.events import EventBase logger = logging.getLogger(__name__) @defer.inlineCallbacks -def resolve_events_with_store(room_version, state_sets, event_map, state_res_store): +def resolve_events_with_store( + room_id: str, + room_version: str, + state_sets: List[Dict[Tuple[str, str], str]], + event_map: Optional[Dict[str, EventBase]], + state_res_store: "synapse.state.StateResolutionStore", +): """Resolves the state using the v2 state resolution algorithm Args: - room_version (str): The room version + room_id: the room we are working in - state_sets(list): List of dicts of (type, state_key) -> event_id, + room_version: The room version + + state_sets: List of dicts of (type, state_key) -> event_id, which are the different state groups to resolve. - event_map(dict[str,FrozenEvent]|None): + event_map: a dict from event_id to event, for any events that we happen to have in flight (eg, those currently being persisted). This will be used as a starting point fof finding the state we need; any missing @@ -46,9 +57,9 @@ def resolve_events_with_store(room_version, state_sets, event_map, state_res_sto If None, all events will be fetched via state_res_store. - state_res_store (StateResolutionStore) + state_res_store: - Returns + Returns: Deferred[dict[(str, str), str]]: a map from (type, state_key) to event_id. """ @@ -84,6 +95,14 @@ def resolve_events_with_store(room_version, state_sets, event_map, state_res_sto ) event_map.update(events) + # everything in the event map should be in the right room + for event in event_map.values(): + if event.room_id != room_id: + raise Exception( + "Attempting to state-resolve for room %s with event %s which is in %s" + % (room_id, event.event_id, event.room_id,) + ) + full_conflicted_set = set(eid for eid in full_conflicted_set if eid in event_map) logger.debug("%d full_conflicted_set entries", len(full_conflicted_set)) @@ -94,13 +113,14 @@ def resolve_events_with_store(room_version, state_sets, event_map, state_res_sto ) sorted_power_events = yield _reverse_topological_power_sort( - power_events, event_map, state_res_store, full_conflicted_set + room_id, power_events, event_map, state_res_store, full_conflicted_set ) logger.debug("sorted %d power events", len(sorted_power_events)) # Now sequentially auth each one resolved_state = yield _iterative_auth_checks( + room_id, room_version, sorted_power_events, unconflicted_state, @@ -121,13 +141,18 @@ def resolve_events_with_store(room_version, state_sets, event_map, state_res_sto pl = resolved_state.get((EventTypes.PowerLevels, ""), None) leftover_events = yield _mainline_sort( - leftover_events, pl, event_map, state_res_store + room_id, leftover_events, pl, event_map, state_res_store ) logger.debug("resolving remaining events") resolved_state = yield _iterative_auth_checks( - room_version, leftover_events, resolved_state, event_map, state_res_store + room_id, + room_version, + leftover_events, + resolved_state, + event_map, + state_res_store, ) logger.debug("resolved") @@ -141,11 +166,12 @@ def resolve_events_with_store(room_version, state_sets, event_map, state_res_sto @defer.inlineCallbacks -def _get_power_level_for_sender(event_id, event_map, state_res_store): +def _get_power_level_for_sender(room_id, event_id, event_map, state_res_store): """Return the power level of the sender of the given event according to their auth events. Args: + room_id (str) event_id (str) event_map (dict[str,FrozenEvent]) state_res_store (StateResolutionStore) @@ -153,11 +179,11 @@ def _get_power_level_for_sender(event_id, event_map, state_res_store): Returns: Deferred[int] """ - event = yield _get_event(event_id, event_map, state_res_store) + event = yield _get_event(room_id, event_id, event_map, state_res_store) pl = None for aid in event.auth_event_ids(): - aev = yield _get_event(aid, event_map, state_res_store) + aev = yield _get_event(room_id, aid, event_map, state_res_store) if (aev.type, aev.state_key) == (EventTypes.PowerLevels, ""): pl = aev break @@ -165,7 +191,7 @@ def _get_power_level_for_sender(event_id, event_map, state_res_store): if pl is None: # Couldn't find power level. Check if they're the creator of the room for aid in event.auth_event_ids(): - aev = yield _get_event(aid, event_map, state_res_store) + aev = yield _get_event(room_id, aid, event_map, state_res_store) if (aev.type, aev.state_key) == (EventTypes.Create, ""): if aev.content.get("creator") == event.sender: return 100 @@ -279,7 +305,7 @@ def _is_power_event(event): @defer.inlineCallbacks def _add_event_and_auth_chain_to_graph( - graph, event_id, event_map, state_res_store, auth_diff + graph, room_id, event_id, event_map, state_res_store, auth_diff ): """Helper function for _reverse_topological_power_sort that add the event and its auth chain (that is in the auth diff) to the graph @@ -287,6 +313,7 @@ def _add_event_and_auth_chain_to_graph( Args: graph (dict[str, set[str]]): A map from event ID to the events auth event IDs + room_id (str): the room we are working in event_id (str): Event to add to the graph event_map (dict[str,FrozenEvent]) state_res_store (StateResolutionStore) @@ -298,7 +325,7 @@ def _add_event_and_auth_chain_to_graph( eid = state.pop() graph.setdefault(eid, set()) - event = yield _get_event(eid, event_map, state_res_store) + event = yield _get_event(room_id, eid, event_map, state_res_store) for aid in event.auth_event_ids(): if aid in auth_diff: if aid not in graph: @@ -308,11 +335,14 @@ def _add_event_and_auth_chain_to_graph( @defer.inlineCallbacks -def _reverse_topological_power_sort(event_ids, event_map, state_res_store, auth_diff): +def _reverse_topological_power_sort( + room_id, event_ids, event_map, state_res_store, auth_diff +): """Returns a list of the event_ids sorted by reverse topological ordering, and then by power level and origin_server_ts Args: + room_id (str): the room we are working in event_ids (list[str]): The events to sort event_map (dict[str,FrozenEvent]) state_res_store (StateResolutionStore) @@ -325,12 +355,14 @@ def _reverse_topological_power_sort(event_ids, event_map, state_res_store, auth_ graph = {} for event_id in event_ids: yield _add_event_and_auth_chain_to_graph( - graph, event_id, event_map, state_res_store, auth_diff + graph, room_id, event_id, event_map, state_res_store, auth_diff ) event_to_pl = {} for event_id in graph: - pl = yield _get_power_level_for_sender(event_id, event_map, state_res_store) + pl = yield _get_power_level_for_sender( + room_id, event_id, event_map, state_res_store + ) event_to_pl[event_id] = pl def _get_power_order(event_id): @@ -348,12 +380,13 @@ def _reverse_topological_power_sort(event_ids, event_map, state_res_store, auth_ @defer.inlineCallbacks def _iterative_auth_checks( - room_version, event_ids, base_state, event_map, state_res_store + room_id, room_version, event_ids, base_state, event_map, state_res_store ): """Sequentially apply auth checks to each event in given list, updating the state as it goes along. Args: + room_id (str) room_version (str) event_ids (list[str]): Ordered list of events to apply auth checks to base_state (dict[tuple[str, str], str]): The set of state to start with @@ -370,7 +403,7 @@ def _iterative_auth_checks( auth_events = {} for aid in event.auth_event_ids(): - ev = yield _get_event(aid, event_map, state_res_store) + ev = yield _get_event(room_id, aid, event_map, state_res_store) if ev.rejected_reason is None: auth_events[(ev.type, ev.state_key)] = ev @@ -378,7 +411,7 @@ def _iterative_auth_checks( for key in event_auth.auth_types_for_event(event): if key in resolved_state: ev_id = resolved_state[key] - ev = yield _get_event(ev_id, event_map, state_res_store) + ev = yield _get_event(room_id, ev_id, event_map, state_res_store) if ev.rejected_reason is None: auth_events[key] = event_map[ev_id] @@ -400,11 +433,14 @@ def _iterative_auth_checks( @defer.inlineCallbacks -def _mainline_sort(event_ids, resolved_power_event_id, event_map, state_res_store): +def _mainline_sort( + room_id, event_ids, resolved_power_event_id, event_map, state_res_store +): """Returns a sorted list of event_ids sorted by mainline ordering based on the given event resolved_power_event_id Args: + room_id (str): room we're working in event_ids (list[str]): Events to sort resolved_power_event_id (str): The final resolved power level event ID event_map (dict[str,FrozenEvent]) @@ -417,11 +453,11 @@ def _mainline_sort(event_ids, resolved_power_event_id, event_map, state_res_stor pl = resolved_power_event_id while pl: mainline.append(pl) - pl_ev = yield _get_event(pl, event_map, state_res_store) + pl_ev = yield _get_event(room_id, pl, event_map, state_res_store) auth_events = pl_ev.auth_event_ids() pl = None for aid in auth_events: - ev = yield _get_event(aid, event_map, state_res_store) + ev = yield _get_event(room_id, aid, event_map, state_res_store) if (ev.type, ev.state_key) == (EventTypes.PowerLevels, ""): pl = aid break @@ -457,6 +493,8 @@ def _get_mainline_depth_for_event(event, mainline_map, event_map, state_res_stor Deferred[int] """ + room_id = event.room_id + # We do an iterative search, replacing `event with the power level in its # auth events (if any) while event: @@ -468,7 +506,7 @@ def _get_mainline_depth_for_event(event, mainline_map, event_map, state_res_stor event = None for aid in auth_events: - aev = yield _get_event(aid, event_map, state_res_store) + aev = yield _get_event(room_id, aid, event_map, state_res_store) if (aev.type, aev.state_key) == (EventTypes.PowerLevels, ""): event = aev break @@ -478,11 +516,12 @@ def _get_mainline_depth_for_event(event, mainline_map, event_map, state_res_stor @defer.inlineCallbacks -def _get_event(event_id, event_map, state_res_store): +def _get_event(room_id, event_id, event_map, state_res_store): """Helper function to look up event in event_map, falling back to looking it up in the store Args: + room_id (str) event_id (str) event_map (dict[str,FrozenEvent]) state_res_store (StateResolutionStore) @@ -493,7 +532,14 @@ def _get_event(event_id, event_map, state_res_store): if event_id not in event_map: events = yield state_res_store.get_events([event_id], allow_rejected=True) event_map.update(events) - return event_map[event_id] + event = event_map[event_id] + assert event is not None + if event.room_id != room_id: + raise Exception( + "In state res for room %s, event %s is in %s" + % (room_id, event_id, event.room_id) + ) + return event def lexicographical_topological_sort(graph, key): diff --git a/tests/state/test_v2.py b/tests/state/test_v2.py index 8d3845c870..0f341d3ac3 100644 --- a/tests/state/test_v2.py +++ b/tests/state/test_v2.py @@ -58,6 +58,7 @@ class FakeEvent(object): self.type = type self.state_key = state_key self.content = content + self.room_id = ROOM_ID def to_event(self, auth_events, prev_events): """Given the auth_events and prev_events, convert to a Frozen Event @@ -418,6 +419,7 @@ class StateTestCase(unittest.TestCase): state_before = dict(state_at_event[prev_events[0]]) else: state_d = resolve_events_with_store( + ROOM_ID, RoomVersions.V2.identifier, [state_at_event[n] for n in prev_events], event_map=event_map, @@ -565,6 +567,7 @@ class SimpleParamStateTestCase(unittest.TestCase): # Test that we correctly handle passing `None` as the event_map state_d = resolve_events_with_store( + ROOM_ID, RoomVersions.V2.identifier, [self.state_at_bob, self.state_at_charlie], event_map=None, From 0b90fc6ed22e6ebb137041a1f5006f52cea081e4 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Fri, 13 Dec 2019 15:28:48 +0000 Subject: [PATCH 0694/1623] Document Shutdown Room admin API (#6541) --- changelog.d/6541.doc | 1 + docs/admin_api/shutdown_room.md | 72 +++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 changelog.d/6541.doc create mode 100644 docs/admin_api/shutdown_room.md diff --git a/changelog.d/6541.doc b/changelog.d/6541.doc new file mode 100644 index 0000000000..c20029edc0 --- /dev/null +++ b/changelog.d/6541.doc @@ -0,0 +1 @@ +Document the Room Shutdown Admin API. \ No newline at end of file diff --git a/docs/admin_api/shutdown_room.md b/docs/admin_api/shutdown_room.md new file mode 100644 index 0000000000..54ce1cd234 --- /dev/null +++ b/docs/admin_api/shutdown_room.md @@ -0,0 +1,72 @@ +# Shutdown room API + +Shuts down a room, preventing new joins and moves local users and room aliases automatically +to a new room. The new room will be created with the user specified by the +`new_room_user_id` parameter as room administrator and will contain a message +explaining what happened. Users invited to the new room will have power level +-10 by default, and thus be unable to speak. The old room's power levels will be changed to +disallow any further invites or joins. + +The local server will only have the power to move local user and room aliases to +the new room. Users on other servers will be unaffected. + +## API + +You will need to authenticate with an access token for an admin user. + +### URL + +`POST /_synapse/admin/v1/shutdown_room/{room_id}` + +### URL Parameters + +* `room_id` - The ID of the room (e.g `!someroom:example.com`) + +### JSON Body Parameters + +* `new_room_user_id` - Required. A string representing the user ID of the user that will admin + the new room that all users in the old room will be moved to. +* `room_name` - Optional. A string representing the name of the room that new users will be + invited to. +* `message` - Optional. A string containing the first message that will be sent as + `new_room_user_id` in the new room. Ideally this will clearly convey why the + original room was shut down. + +If not specified, the default value of `room_name` is "Content Violation +Notification". The default value of `message` is "Sharing illegal content on +othis server is not permitted and rooms in violation will be blocked." + +### Response Parameters + +* `kicked_users` - An integer number representing the number of users that + were kicked. +* `failed_to_kick_users` - An integer number representing the number of users + that were not kicked. +* `local_aliases` - An array of strings representing the local aliases that were migrated from + the old room to the new. +* `new_room_id` - A string representing the room ID of the new room. + +## Example + +Request: + +``` +POST /_synapse/admin/v1/shutdown_room/!somebadroom%3Aexample.com + +{ + "new_room_user_id": "@someuser:example.com", + "room_name": "Content Violation Notification", + "message": "Bad Room has been shutdown due to content violations on this server. Please review our Terms of Service." +} +``` + +Response: + +``` +{ + "kicked_users": 5, + "failed_to_kick_users": 0, + "local_aliases": ["#badroom:example.com", "#evilsaloon:example.com], + "new_room_id": "!newroomid:example.com", +}, +``` From 9d173b312cc3ab170de3a7c58ac24778eddf93f6 Mon Sep 17 00:00:00 2001 From: Werner Sembach Date: Mon, 16 Dec 2019 13:12:40 +0100 Subject: [PATCH 0695/1623] Automatically delete empty groups/communities (#6453) Signed-off-by: Werner Sembach --- AUTHORS.rst | 3 ++ changelog.d/6453.feature | 1 + synapse/groups/groups_server.py | 5 ++++ .../56/nuke_empty_communities_from_db.sql | 29 +++++++++++++++++++ 4 files changed, 38 insertions(+) create mode 100644 changelog.d/6453.feature create mode 100644 synapse/storage/data_stores/main/schema/delta/56/nuke_empty_communities_from_db.sql diff --git a/AUTHORS.rst b/AUTHORS.rst index b8b31a5b47..014f16d4a2 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -46,3 +46,6 @@ Joseph Weston Benjamin Saunders * Documentation improvements + +Werner Sembach + * Automatically remove a group/community when it is empty diff --git a/changelog.d/6453.feature b/changelog.d/6453.feature new file mode 100644 index 0000000000..e7bb801c6a --- /dev/null +++ b/changelog.d/6453.feature @@ -0,0 +1 @@ +Automatically delete empty groups/communities. diff --git a/synapse/groups/groups_server.py b/synapse/groups/groups_server.py index 29e8ffc295..0ec9be3cb5 100644 --- a/synapse/groups/groups_server.py +++ b/synapse/groups/groups_server.py @@ -773,6 +773,11 @@ class GroupsServerHandler(object): if not self.hs.is_mine_id(user_id): yield self.store.maybe_delete_remote_profile_cache(user_id) + # Delete group if the last user has left + users = yield self.store.get_users_in_group(group_id, include_private=True) + if not users: + yield self.store.delete_group(group_id) + return {} @defer.inlineCallbacks diff --git a/synapse/storage/data_stores/main/schema/delta/56/nuke_empty_communities_from_db.sql b/synapse/storage/data_stores/main/schema/delta/56/nuke_empty_communities_from_db.sql new file mode 100644 index 0000000000..4f24c1405d --- /dev/null +++ b/synapse/storage/data_stores/main/schema/delta/56/nuke_empty_communities_from_db.sql @@ -0,0 +1,29 @@ +/* Copyright 2019 Werner Sembach + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +-- Groups/communities now get deleted when the last member leaves. This is a one time cleanup to remove old groups/communities that were already empty before that change was made. +DELETE FROM group_attestations_remote WHERE group_id IN (SELECT group_id FROM groups WHERE NOT EXISTS (SELECT group_id FROM group_users WHERE group_id = groups.group_id)); +DELETE FROM group_attestations_renewals WHERE group_id IN (SELECT group_id FROM groups WHERE NOT EXISTS (SELECT group_id FROM group_users WHERE group_id = groups.group_id)); +DELETE FROM group_invites WHERE group_id IN (SELECT group_id FROM groups WHERE NOT EXISTS (SELECT group_id FROM group_users WHERE group_id = groups.group_id)); +DELETE FROM group_roles WHERE group_id IN (SELECT group_id FROM groups WHERE NOT EXISTS (SELECT group_id FROM group_users WHERE group_id = groups.group_id)); +DELETE FROM group_room_categories WHERE group_id IN (SELECT group_id FROM groups WHERE NOT EXISTS (SELECT group_id FROM group_users WHERE group_id = groups.group_id)); +DELETE FROM group_rooms WHERE group_id IN (SELECT group_id FROM groups WHERE NOT EXISTS (SELECT group_id FROM group_users WHERE group_id = groups.group_id)); +DELETE FROM group_summary_roles WHERE group_id IN (SELECT group_id FROM groups WHERE NOT EXISTS (SELECT group_id FROM group_users WHERE group_id = groups.group_id)); +DELETE FROM group_summary_room_categories WHERE group_id IN (SELECT group_id FROM groups WHERE NOT EXISTS (SELECT group_id FROM group_users WHERE group_id = groups.group_id)); +DELETE FROM group_summary_rooms WHERE group_id IN (SELECT group_id FROM groups WHERE NOT EXISTS (SELECT group_id FROM group_users WHERE group_id = groups.group_id)); +DELETE FROM group_summary_users WHERE group_id IN (SELECT group_id FROM groups WHERE NOT EXISTS (SELECT group_id FROM group_users WHERE group_id = groups.group_id)); +DELETE FROM local_group_membership WHERE group_id IN (SELECT group_id FROM groups WHERE NOT EXISTS (SELECT group_id FROM group_users WHERE group_id = groups.group_id)); +DELETE FROM local_group_updates WHERE group_id IN (SELECT group_id FROM groups WHERE NOT EXISTS (SELECT group_id FROM group_users WHERE group_id = groups.group_id)); +DELETE FROM groups WHERE group_id IN (SELECT group_id FROM groups WHERE NOT EXISTS (SELECT group_id FROM group_users WHERE group_id = groups.group_id)); From 487f1bb49d5eb5840c7dd70d95ac53f2b24eba21 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Mon, 16 Dec 2019 12:14:12 +0000 Subject: [PATCH 0696/1623] Use the filtered version of an event when responding to /context requests for that event Sometimes the filtering function can return a pruned version of an event (on top of either the event itself or an empty list), if it thinks the user should be able to see that there's an event there but not the content of that event. Therefore, the previous logic of 'if filtered is empty then we can use the event we retrieved from the database' is flawed, and we should use the event returned by the filtering function. --- synapse/handlers/room.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 22768e97ff..7f979e5812 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -907,7 +907,10 @@ class RoomContextHandler(object): results["events_before"] = yield filter_evts(results["events_before"]) results["events_after"] = yield filter_evts(results["events_after"]) - results["event"] = event + # filter_evts can return a pruned event in case the user is allowed to see that + # there's something there but not see the content, so use the event that's in + # `filtered` rather than the event we retrieved from the datastore. + results["event"] = filtered[0] if results["events_after"]: last_event_id = results["events_after"][-1].event_id From ac87ddb242d146cd840b232fa6e8165c9dd01ae6 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Mon, 16 Dec 2019 12:15:37 +0000 Subject: [PATCH 0697/1623] Update the documentation of the filtering function --- synapse/visibility.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/synapse/visibility.py b/synapse/visibility.py index dffe943b28..100dc47a8a 100644 --- a/synapse/visibility.py +++ b/synapse/visibility.py @@ -52,7 +52,8 @@ def filter_events_for_client( apply_retention_policies=True, ): """ - Check which events a user is allowed to see + Check which events a user is allowed to see. If the user can see the event but its + sender asked for their data to be erased, prune the content of the event. Args: storage From 8b9f5c21c35ff7a5491121ffc381bd8c97e879ce Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Mon, 16 Dec 2019 12:19:35 +0000 Subject: [PATCH 0698/1623] Changelog --- changelog.d/6553.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6553.bugfix diff --git a/changelog.d/6553.bugfix b/changelog.d/6553.bugfix new file mode 100644 index 0000000000..e8f55e2a76 --- /dev/null +++ b/changelog.d/6553.bugfix @@ -0,0 +1 @@ +Fix a bug causing responses to the `/context` client endpoint to not use the pruned version of the event the request is for. From bc7de87650c2646cdc388f06a2a5bb6f94fc5c5c Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 16 Dec 2019 12:26:28 +0000 Subject: [PATCH 0699/1623] Persist auth/state events at backwards extremities when we fetch them (#6526) The main point here is to make sure that the state returned by _get_state_in_room has been authed before we try to use it as state in the room. --- changelog.d/6526.bugfix | 1 + synapse/handlers/federation.py | 247 +++++++++++---------------------- synapse/util/async_helpers.py | 4 +- 3 files changed, 83 insertions(+), 169 deletions(-) create mode 100644 changelog.d/6526.bugfix diff --git a/changelog.d/6526.bugfix b/changelog.d/6526.bugfix new file mode 100644 index 0000000000..53214b0748 --- /dev/null +++ b/changelog.d/6526.bugfix @@ -0,0 +1 @@ +Fix a bug which could cause the federation server to incorrectly return errors when handling certain obscure event graphs. \ No newline at end of file diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 1d39a9a4f5..3f480f2056 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -65,8 +65,7 @@ from synapse.replication.http.membership import ReplicationUserJoinedLeftRoomRes from synapse.state import StateResolutionStore, resolve_events_with_store from synapse.storage.data_stores.main.events_worker import EventRedactBehaviour from synapse.types import UserID, get_domain_from_id -from synapse.util import batch_iter, unwrapFirstError -from synapse.util.async_helpers import Linearizer +from synapse.util.async_helpers import Linearizer, concurrently_execute from synapse.util.distributor import user_joined_room from synapse.util.retryutils import NotRetryingDestination from synapse.visibility import filter_events_for_server @@ -238,7 +237,6 @@ class FederationHandler(BaseHandler): return None state = None - auth_chain = [] # Get missing pdus if necessary. if not pdu.internal_metadata.is_outlier(): @@ -348,7 +346,6 @@ class FederationHandler(BaseHandler): # Calculate the state after each of the previous events, and # resolve them to find the correct state at the current event. - auth_chains = set() event_map = {event_id: pdu} try: # Get the state of the events we know about @@ -369,24 +366,14 @@ class FederationHandler(BaseHandler): "Requesting state at missing prev_event %s", event_id, ) - room_version = await self.store.get_room_version(room_id) - with nested_logging_context(p): # note that if any of the missing prevs share missing state or # auth events, the requests to fetch those events are deduped # by the get_pdu_cache in federation_client. - ( - remote_state, - got_auth_chain, - ) = await self._get_state_for_room( + (remote_state, _,) = await self._get_state_for_room( origin, room_id, p, include_event_in_state=True ) - # XXX hrm I'm not convinced that duplicate events will compare - # for equality, so I'm not sure this does what the author - # hoped. - auth_chains.update(got_auth_chain) - remote_state_map = { (x.type, x.state_key): x.event_id for x in remote_state } @@ -395,6 +382,7 @@ class FederationHandler(BaseHandler): for x in remote_state: event_map[x.event_id] = x + room_version = await self.store.get_room_version(room_id) state_map = await resolve_events_with_store( room_id, room_version, @@ -416,7 +404,6 @@ class FederationHandler(BaseHandler): event_map.update(evs) state = [event_map[e] for e in six.itervalues(state_map)] - auth_chain = list(auth_chains) except Exception: logger.warning( "[%s %s] Error attempting to resolve state at missing " @@ -432,9 +419,7 @@ class FederationHandler(BaseHandler): affected=event_id, ) - await self._process_received_pdu( - origin, pdu, state=state, auth_chain=auth_chain - ) + await self._process_received_pdu(origin, pdu, state=state) async def _get_missing_events_for_pdu(self, origin, pdu, prevs, min_depth): """ @@ -633,10 +618,7 @@ class FederationHandler(BaseHandler): ) -> Dict[str, EventBase]: """Fetch events from a remote destination, checking if we already have them. - Args: - destination - room_id - event_ids + Persists any events we don't already have as outliers. If we fail to fetch any of the events, a warning will be logged, and the event will be omitted from the result. Likewise, any events which turn out not to @@ -656,27 +638,15 @@ class FederationHandler(BaseHandler): room_id, ) - room_version = await self.store.get_room_version(room_id) + await self._get_events_and_persist( + destination=destination, room_id=room_id, events=missing_events + ) - # XXX 20 requests at once? really? - for batch in batch_iter(missing_events, 20): - deferreds = [ - run_in_background( - self.federation_client.get_pdu, - destinations=[destination], - event_id=e_id, - room_version=room_version, - ) - for e_id in batch - ] - - res = await make_deferred_yieldable( - defer.DeferredList(deferreds, consumeErrors=True) - ) - - for success, result in res: - if success and result: - fetched_events[result.event_id] = result + # we need to make sure we re-load from the database to get the rejected + # state correct. + fetched_events.update( + (await self.store.get_events(missing_events, allow_rejected=True)) + ) # check for events which were in the wrong room. # @@ -705,50 +675,26 @@ class FederationHandler(BaseHandler): return fetched_events - async def _process_received_pdu(self, origin, event, state, auth_chain): + async def _process_received_pdu( + self, origin: str, event: EventBase, state: Optional[Iterable[EventBase]], + ): """ Called when we have a new pdu. We need to do auth checks and put it through the StateHandler. + + Args: + origin: server sending the event + + event: event to be persisted + + state: Normally None, but if we are handling a gap in the graph + (ie, we are missing one or more prev_events), the resolved state at the + event """ room_id = event.room_id event_id = event.event_id logger.debug("[%s %s] Processing event: %s", room_id, event_id, event) - event_ids = set() - if state: - event_ids |= {e.event_id for e in state} - if auth_chain: - event_ids |= {e.event_id for e in auth_chain} - - seen_ids = await self.store.have_seen_events(event_ids) - - if state and auth_chain is not None: - # If we have any state or auth_chain given to us by the replication - # layer, then we should handle them (if we haven't before.) - - event_infos = [] - - for e in itertools.chain(auth_chain, state): - if e.event_id in seen_ids: - continue - e.internal_metadata.outlier = True - auth_ids = e.auth_event_ids() - auth = { - (e.type, e.state_key): e - for e in auth_chain - if e.event_id in auth_ids or e.type == EventTypes.Create - } - event_infos.append(_NewEventInfo(event=e, auth_events=auth)) - seen_ids.add(e.event_id) - - logger.info( - "[%s %s] persisting newly-received auth/state events %s", - room_id, - event_id, - [e.event.event_id for e in event_infos], - ) - await self._handle_new_events(origin, event_infos) - try: context = await self._handle_new_event(origin, event, state=state) except AuthError as e: @@ -803,8 +749,6 @@ class FederationHandler(BaseHandler): if dest == self.server_name: raise SynapseError(400, "Can't backfill from self.") - room_version = await self.store.get_room_version(room_id) - events = await self.federation_client.backfill( dest, room_id, limit=limit, extremities=extremities ) @@ -833,6 +777,9 @@ class FederationHandler(BaseHandler): event_ids = set(e.event_id for e in events) + # build a list of events whose prev_events weren't in the batch. + # (XXX: this will include events whose prev_events we already have; that doesn't + # sound right?) edges = [ev.event_id for ev in events if set(ev.prev_event_ids()) - event_ids] logger.info("backfill: Got %d events with %d edges", len(events), len(edges)) @@ -861,95 +808,11 @@ class FederationHandler(BaseHandler): auth_events.update( {e_id: event_map[e_id] for e_id in required_auth if e_id in event_map} ) - missing_auth = required_auth - set(auth_events) - failed_to_fetch = set() - # Try and fetch any missing auth events from both DB and remote servers. - # We repeatedly do this until we stop finding new auth events. - while missing_auth - failed_to_fetch: - logger.info("Missing auth for backfill: %r", missing_auth) - ret_events = await self.store.get_events(missing_auth - failed_to_fetch) - auth_events.update(ret_events) - - required_auth.update( - a_id for event in ret_events.values() for a_id in event.auth_event_ids() - ) - missing_auth = required_auth - set(auth_events) - - if missing_auth - failed_to_fetch: - logger.info( - "Fetching missing auth for backfill: %r", - missing_auth - failed_to_fetch, - ) - - results = await make_deferred_yieldable( - defer.gatherResults( - [ - run_in_background( - self.federation_client.get_pdu, - [dest], - event_id, - room_version=room_version, - outlier=True, - timeout=10000, - ) - for event_id in missing_auth - failed_to_fetch - ], - consumeErrors=True, - ) - ).addErrback(unwrapFirstError) - auth_events.update({a.event_id: a for a in results if a}) - required_auth.update( - a_id - for event in results - if event - for a_id in event.auth_event_ids() - ) - missing_auth = required_auth - set(auth_events) - - failed_to_fetch = missing_auth - set(auth_events) - - seen_events = await self.store.have_seen_events( - set(auth_events.keys()) | set(state_events.keys()) - ) - - # We now have a chunk of events plus associated state and auth chain to - # persist. We do the persistence in two steps: - # 1. Auth events and state get persisted as outliers, plus the - # backward extremities get persisted (as non-outliers). - # 2. The rest of the events in the chunk get persisted one by one, as - # each one depends on the previous event for its state. - # - # The important thing is that events in the chunk get persisted as - # non-outliers, including when those events are also in the state or - # auth chain. Caution must therefore be taken to ensure that they are - # not accidentally marked as outliers. - - # Step 1a: persist auth events that *don't* appear in the chunk ev_infos = [] - for a in auth_events.values(): - # We only want to persist auth events as outliers that we haven't - # seen and aren't about to persist as part of the backfilled chunk. - if a.event_id in seen_events or a.event_id in event_map: - continue - a.internal_metadata.outlier = True - ev_infos.append( - _NewEventInfo( - event=a, - auth_events={ - ( - auth_events[a_id].type, - auth_events[a_id].state_key, - ): auth_events[a_id] - for a_id in a.auth_event_ids() - if a_id in auth_events - }, - ) - ) - - # Step 1b: persist the events in the chunk we fetched state for (i.e. - # the backwards extremities) as non-outliers. + # Step 1: persist the events in the chunk we fetched state for (i.e. + # the backwards extremities), with custom auth events and state for e_id in events_to_state: # For paranoia we ensure that these events are marked as # non-outliers @@ -1191,6 +1054,56 @@ class FederationHandler(BaseHandler): return False + async def _get_events_and_persist( + self, destination: str, room_id: str, events: Iterable[str] + ): + """Fetch the given events from a server, and persist them as outliers. + + Logs a warning if we can't find the given event. + """ + + room_version = await self.store.get_room_version(room_id) + + event_infos = [] + + async def get_event(event_id: str): + with nested_logging_context(event_id): + try: + event = await self.federation_client.get_pdu( + [destination], event_id, room_version, outlier=True, + ) + if event is None: + logger.warning( + "Server %s didn't return event %s", destination, event_id, + ) + return + + # recursively fetch the auth events for this event + auth_events = await self._get_events_from_store_or_dest( + destination, room_id, event.auth_event_ids() + ) + auth = {} + for auth_event_id in event.auth_event_ids(): + ae = auth_events.get(auth_event_id) + if ae: + auth[(ae.type, ae.state_key)] = ae + + event_infos.append(_NewEventInfo(event, None, auth)) + + except Exception as e: + logger.warning( + "Error fetching missing state/auth event %s: %s %s", + event_id, + type(e), + e, + ) + + await concurrently_execute(get_event, events, 5) + + await self._handle_new_events( + destination, event_infos, + ) + def _sanity_check_event(self, ev): """ Do some early sanity checks of a received event diff --git a/synapse/util/async_helpers.py b/synapse/util/async_helpers.py index 5c4de2e69f..04b6abdc24 100644 --- a/synapse/util/async_helpers.py +++ b/synapse/util/async_helpers.py @@ -140,8 +140,8 @@ def concurrently_execute(func, args, limit): Args: func (func): Function to execute, should return a deferred or coroutine. - args (list): List of arguments to pass to func, each invocation of func - gets a signle argument. + args (Iterable): List of arguments to pass to func, each invocation of func + gets a single argument. limit (int): Maximum number of conccurent executions. Returns: From 6920d88892e77aec787b6afc0e01e6e09dc36216 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 16 Dec 2019 13:14:37 +0000 Subject: [PATCH 0700/1623] Exclude rejected state events when calculating state at backwards extrems (#6527) This fixes a weird bug where, if you were determined enough, you could end up with a rejected event forming part of the state at a backwards-extremity. Authing that backwards extrem would then lead to us trying to pull the rejected event from the db (with allow_rejected=False), which would fail with a 404. --- changelog.d/6527.bugfix | 1 + synapse/handlers/federation.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/6527.bugfix diff --git a/changelog.d/6527.bugfix b/changelog.d/6527.bugfix new file mode 100644 index 0000000000..53214b0748 --- /dev/null +++ b/changelog.d/6527.bugfix @@ -0,0 +1 @@ +Fix a bug which could cause the federation server to incorrectly return errors when handling certain obscure event graphs. \ No newline at end of file diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 3f480f2056..3fccccfecd 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -605,7 +605,7 @@ class FederationHandler(BaseHandler): remote_event = event_map.get(event_id) if not remote_event: raise Exception("Unable to get missing prev_event %s" % (event_id,)) - if remote_event.is_state(): + if remote_event.is_state() and remote_event.rejected_reason is None: remote_state.append(remote_event) auth_chain = [event_map[e_id] for e_id in auth_event_ids if e_id in event_map] From 4c7b1bb6cccd726ae9a9f91b3309554a7fe6d262 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 10 Dec 2019 16:22:00 +0000 Subject: [PATCH 0701/1623] Refactor get_events_from_store_or_dest to return a dict (#6501) There was a bunch of unnecessary conversion back and forth between dict and list going on here. We can simplify a bunch of the code. --- changelog.d/6501.misc | 1 + synapse/federation/federation_client.py | 44 +++++++++---------------- 2 files changed, 16 insertions(+), 29 deletions(-) create mode 100644 changelog.d/6501.misc diff --git a/changelog.d/6501.misc b/changelog.d/6501.misc new file mode 100644 index 0000000000..255f45a9c3 --- /dev/null +++ b/changelog.d/6501.misc @@ -0,0 +1 @@ +Refactor get_events_from_store_or_dest to return a dict. diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index 709449c9e3..73e1dda6a3 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -18,8 +18,6 @@ import copy import itertools import logging -from six.moves import range - from prometheus_client import Counter from twisted.internet import defer @@ -41,7 +39,7 @@ from synapse.events import builder, room_version_to_event_format from synapse.federation.federation_base import FederationBase, event_from_pdu_json from synapse.logging.context import make_deferred_yieldable, run_in_background from synapse.logging.utils import log_function -from synapse.util import unwrapFirstError +from synapse.util import batch_iter, unwrapFirstError from synapse.util.caches.expiringcache import ExpiringCache from synapse.util.retryutils import NotRetryingDestination @@ -331,10 +329,12 @@ class FederationClient(FederationBase): state_event_ids = result["pdu_ids"] auth_event_ids = result.get("auth_chain_ids", []) - fetched_events, failed_to_fetch = yield self.get_events_from_store_or_dest( - destination, room_id, set(state_event_ids + auth_event_ids) + desired_events = set(state_event_ids + auth_event_ids) + event_map = yield self.get_events_from_store_or_dest( + destination, room_id, desired_events ) + failed_to_fetch = desired_events - event_map.keys() if failed_to_fetch: logger.warning( "Failed to fetch missing state/auth events for %s: %s", @@ -342,8 +342,6 @@ class FederationClient(FederationBase): failed_to_fetch, ) - event_map = {ev.event_id: ev for ev in fetched_events} - pdus = [event_map[e_id] for e_id in state_event_ids if e_id in event_map] auth_chain = [event_map[e_id] for e_id in auth_event_ids if e_id in event_map] @@ -358,23 +356,18 @@ class FederationClient(FederationBase): Args: destination (str) room_id (str) - event_ids (list) + event_ids (Iterable[str]) Returns: - Deferred: A deferred resolving to a 2-tuple where the first is a list of - events and the second is a list of event ids that we failed to fetch. + Deferred[dict[str, EventBase]]: A deferred resolving to a map + from event_id to event """ - seen_events = yield self.store.get_events(event_ids, allow_rejected=True) - signed_events = list(seen_events.values()) + fetched_events = yield self.store.get_events(event_ids, allow_rejected=True) - failed_to_fetch = set() - - missing_events = set(event_ids) - for k in seen_events: - missing_events.discard(k) + missing_events = set(event_ids) - fetched_events.keys() if not missing_events: - return signed_events, failed_to_fetch + return fetched_events logger.debug( "Fetching unknown state/auth events %s for room %s", @@ -384,11 +377,8 @@ class FederationClient(FederationBase): room_version = yield self.store.get_room_version(room_id) - batch_size = 20 - missing_events = list(missing_events) - for i in range(0, len(missing_events), batch_size): - batch = set(missing_events[i : i + batch_size]) - + # XXX 20 requests at once? really? + for batch in batch_iter(missing_events, 20): deferreds = [ run_in_background( self.get_pdu, @@ -404,13 +394,9 @@ class FederationClient(FederationBase): ) for success, result in res: if success and result: - signed_events.append(result) - batch.discard(result.event_id) + fetched_events[result.event_id] = result - # We removed all events we successfully fetched from `batch` - failed_to_fetch.update(batch) - - return signed_events, failed_to_fetch + return fetched_events @defer.inlineCallbacks @log_function From be294d6fde1b8b37b9d557e56973deb92790ddb8 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 10 Dec 2019 17:42:46 +0000 Subject: [PATCH 0702/1623] Move get_state methods into FederationHandler (#6503) This is a non-functional refactor as a precursor to some other work. --- changelog.d/6503.misc | 1 + synapse/federation/federation_client.py | 91 +++------------------ synapse/handlers/federation.py | 101 ++++++++++++++++++++++-- 3 files changed, 107 insertions(+), 86 deletions(-) create mode 100644 changelog.d/6503.misc diff --git a/changelog.d/6503.misc b/changelog.d/6503.misc new file mode 100644 index 0000000000..e4e9a5a3d4 --- /dev/null +++ b/changelog.d/6503.misc @@ -0,0 +1 @@ +Move get_state methods into FederationHandler. diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index 73e1dda6a3..d396e6564f 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -37,9 +37,9 @@ from synapse.api.room_versions import ( ) from synapse.events import builder, room_version_to_event_format from synapse.federation.federation_base import FederationBase, event_from_pdu_json -from synapse.logging.context import make_deferred_yieldable, run_in_background +from synapse.logging.context import make_deferred_yieldable from synapse.logging.utils import log_function -from synapse.util import batch_iter, unwrapFirstError +from synapse.util import unwrapFirstError from synapse.util.caches.expiringcache import ExpiringCache from synapse.util.retryutils import NotRetryingDestination @@ -308,19 +308,12 @@ class FederationClient(FederationBase): return signed_pdu @defer.inlineCallbacks - @log_function - def get_state_for_room(self, destination, room_id, event_id): - """Requests all of the room state at a given event from a remote homeserver. - - Args: - destination (str): The remote homeserver to query for the state. - room_id (str): The id of the room we're interested in. - event_id (str): The id of the event we want the state at. + def get_room_state_ids(self, destination: str, room_id: str, event_id: str): + """Calls the /state_ids endpoint to fetch the state at a particular point + in the room, and the auth events for the given event Returns: - Deferred[Tuple[List[EventBase], List[EventBase]]]: - A list of events in the state, and a list of events in the auth chain - for the given event. + Tuple[List[str], List[str]]: a tuple of (state event_ids, auth event_ids) """ result = yield self.transport_layer.get_room_state_ids( destination, room_id, event_id=event_id @@ -329,74 +322,12 @@ class FederationClient(FederationBase): state_event_ids = result["pdu_ids"] auth_event_ids = result.get("auth_chain_ids", []) - desired_events = set(state_event_ids + auth_event_ids) - event_map = yield self.get_events_from_store_or_dest( - destination, room_id, desired_events - ) + if not isinstance(state_event_ids, list) or not isinstance( + auth_event_ids, list + ): + raise Exception("invalid response from /state_ids") - failed_to_fetch = desired_events - event_map.keys() - if failed_to_fetch: - logger.warning( - "Failed to fetch missing state/auth events for %s: %s", - room_id, - failed_to_fetch, - ) - - pdus = [event_map[e_id] for e_id in state_event_ids if e_id in event_map] - auth_chain = [event_map[e_id] for e_id in auth_event_ids if e_id in event_map] - - auth_chain.sort(key=lambda e: e.depth) - - return pdus, auth_chain - - @defer.inlineCallbacks - def get_events_from_store_or_dest(self, destination, room_id, event_ids): - """Fetch events from a remote destination, checking if we already have them. - - Args: - destination (str) - room_id (str) - event_ids (Iterable[str]) - - Returns: - Deferred[dict[str, EventBase]]: A deferred resolving to a map - from event_id to event - """ - fetched_events = yield self.store.get_events(event_ids, allow_rejected=True) - - missing_events = set(event_ids) - fetched_events.keys() - - if not missing_events: - return fetched_events - - logger.debug( - "Fetching unknown state/auth events %s for room %s", - missing_events, - event_ids, - ) - - room_version = yield self.store.get_room_version(room_id) - - # XXX 20 requests at once? really? - for batch in batch_iter(missing_events, 20): - deferreds = [ - run_in_background( - self.get_pdu, - destinations=[destination], - event_id=e_id, - room_version=room_version, - ) - for e_id in batch - ] - - res = yield make_deferred_yieldable( - defer.DeferredList(deferreds, consumeErrors=True) - ) - for success, result in res: - if success and result: - fetched_events[result.event_id] = result - - return fetched_events + return state_event_ids, auth_event_ids @defer.inlineCallbacks @log_function diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index bc26921768..c0dcf9abf8 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -64,7 +64,7 @@ from synapse.replication.http.federation import ( from synapse.replication.http.membership import ReplicationUserJoinedLeftRoomRestServlet from synapse.state import StateResolutionStore, resolve_events_with_store from synapse.types import UserID, get_domain_from_id -from synapse.util import unwrapFirstError +from synapse.util import batch_iter, unwrapFirstError from synapse.util.async_helpers import Linearizer from synapse.util.distributor import user_joined_room from synapse.util.retryutils import NotRetryingDestination @@ -379,11 +379,9 @@ class FederationHandler(BaseHandler): ( remote_state, got_auth_chain, - ) = yield self.federation_client.get_state_for_room( - origin, room_id, p - ) + ) = yield self._get_state_for_room(origin, room_id, p) - # we want the state *after* p; get_state_for_room returns the + # we want the state *after* p; _get_state_for_room returns the # state *before* p. remote_event = yield self.federation_client.get_pdu( [origin], p, room_version, outlier=True @@ -583,6 +581,97 @@ class FederationHandler(BaseHandler): else: raise + @defer.inlineCallbacks + @log_function + def _get_state_for_room(self, destination, room_id, event_id): + """Requests all of the room state at a given event from a remote homeserver. + + Args: + destination (str): The remote homeserver to query for the state. + room_id (str): The id of the room we're interested in. + event_id (str): The id of the event we want the state at. + + Returns: + Deferred[Tuple[List[EventBase], List[EventBase]]]: + A list of events in the state, and a list of events in the auth chain + for the given event. + """ + ( + state_event_ids, + auth_event_ids, + ) = yield self.federation_client.get_room_state_ids( + destination, room_id, event_id=event_id + ) + + desired_events = set(state_event_ids + auth_event_ids) + event_map = yield self._get_events_from_store_or_dest( + destination, room_id, desired_events + ) + + failed_to_fetch = desired_events - event_map.keys() + if failed_to_fetch: + logger.warning( + "Failed to fetch missing state/auth events for %s: %s", + room_id, + failed_to_fetch, + ) + + pdus = [event_map[e_id] for e_id in state_event_ids if e_id in event_map] + auth_chain = [event_map[e_id] for e_id in auth_event_ids if e_id in event_map] + + auth_chain.sort(key=lambda e: e.depth) + + return pdus, auth_chain + + @defer.inlineCallbacks + def _get_events_from_store_or_dest(self, destination, room_id, event_ids): + """Fetch events from a remote destination, checking if we already have them. + + Args: + destination (str) + room_id (str) + event_ids (Iterable[str]) + + Returns: + Deferred[dict[str, EventBase]]: A deferred resolving to a map + from event_id to event + """ + fetched_events = yield self.store.get_events(event_ids, allow_rejected=True) + + missing_events = set(event_ids) - fetched_events.keys() + + if not missing_events: + return fetched_events + + logger.debug( + "Fetching unknown state/auth events %s for room %s", + missing_events, + event_ids, + ) + + room_version = yield self.store.get_room_version(room_id) + + # XXX 20 requests at once? really? + for batch in batch_iter(missing_events, 20): + deferreds = [ + run_in_background( + self.federation_client.get_pdu, + destinations=[destination], + event_id=e_id, + room_version=room_version, + ) + for e_id in batch + ] + + res = yield make_deferred_yieldable( + defer.DeferredList(deferreds, consumeErrors=True) + ) + for success, result in res: + if success and result: + fetched_events[result.event_id] = result + + return fetched_events + @defer.inlineCallbacks def _process_received_pdu(self, origin, event, state, auth_chain): """ Called when we have a new pdu. We need to do auth checks and put it @@ -723,7 +812,7 @@ class FederationHandler(BaseHandler): state_events = {} events_to_state = {} for e_id in edges: - state, auth = yield self.federation_client.get_state_for_room( + state, auth = yield self._get_state_for_room( destination=dest, room_id=room_id, event_id=e_id ) auth_events.update({a.event_id: a for a in auth}) From 20d5ba16e626aa4217492c83dda9fabd36bd5d2b Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 11 Dec 2019 16:37:51 +0000 Subject: [PATCH 0703/1623] Add `include_event_in_state` to _get_state_for_room (#6521) Make it return the state *after* the requested event, rather than the one before it. This is a bit easier and requires fewer calls to get_events_from_store_or_dest. --- changelog.d/6521.misc | 1 + synapse/handlers/federation.py | 39 ++++++++++++++++++---------------- 2 files changed, 22 insertions(+), 18 deletions(-) create mode 100644 changelog.d/6521.misc diff --git a/changelog.d/6521.misc b/changelog.d/6521.misc new file mode 100644 index 0000000000..d9a44389b9 --- /dev/null +++ b/changelog.d/6521.misc @@ -0,0 +1 @@ +Refactor some code in the event authentication path for clarity. diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index c0dcf9abf8..31c9132ae9 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -379,22 +379,10 @@ class FederationHandler(BaseHandler): ( remote_state, got_auth_chain, - ) = yield self._get_state_for_room(origin, room_id, p) - - # we want the state *after* p; _get_state_for_room returns the - # state *before* p. - remote_event = yield self.federation_client.get_pdu( - [origin], p, room_version, outlier=True + ) = yield self._get_state_for_room( + origin, room_id, p, include_event_in_state=True ) - if remote_event is None: - raise Exception( - "Unable to get missing prev_event %s" % (p,) - ) - - if remote_event.is_state(): - remote_state.append(remote_event) - # XXX hrm I'm not convinced that duplicate events will compare # for equality, so I'm not sure this does what the author # hoped. @@ -583,13 +571,15 @@ class FederationHandler(BaseHandler): @defer.inlineCallbacks @log_function - def _get_state_for_room(self, destination, room_id, event_id): + def _get_state_for_room(self, destination, room_id, event_id, include_event_in_state): """Requests all of the room state at a given event from a remote homeserver. Args: destination (str): The remote homeserver to query for the state. room_id (str): The id of the room we're interested in. event_id (str): The id of the event we want the state at. + include_event_in_state: if true, the event itself will be included in the + returned state event list. Returns: Deferred[Tuple[List[EventBase], List[EventBase]]]: @@ -604,6 +594,10 @@ class FederationHandler(BaseHandler): ) desired_events = set(state_event_ids + auth_event_ids) + + if include_event_in_state: + desired_events.add(event_id) + event_map = yield self._get_events_from_store_or_dest( destination, room_id, desired_events ) @@ -616,12 +610,21 @@ class FederationHandler(BaseHandler): failed_to_fetch, ) - pdus = [event_map[e_id] for e_id in state_event_ids if e_id in event_map] - auth_chain = [event_map[e_id] for e_id in auth_event_ids if e_id in event_map] + remote_state = [ + event_map[e_id] for e_id in state_event_ids if e_id in event_map + ] + if include_event_in_state: + remote_event = event_map.get(event_id) + if not remote_event: + raise Exception("Unable to get missing prev_event %s" % (event_id,)) + if remote_event.is_state(): + remote_state.append(remote_event) + + auth_chain = [event_map[e_id] for e_id in auth_event_ids if e_id in event_map] auth_chain.sort(key=lambda e: e.depth) - return pdus, auth_chain + return remote_state, auth_chain @defer.inlineCallbacks def _get_events_from_store_or_dest(self, destination, room_id, event_ids): From 35bbe4ca794d7b7b1c5b008211a377f54deecb5d Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 12 Dec 2019 12:57:45 +0000 Subject: [PATCH 0704/1623] Check the room_id of events when fetching room state/auth (#6524) When we request the state/auth_events to populate a backwards extremity (on backfill or in the case of missing events in a transaction push), we should check that the returned events are in the right room rather than blindly using them in the room state or auth chain. Given that _get_events_from_store_or_dest takes a room_id, it seems clear that it should be sanity-checking the room_id of the requested events, so let's do it there. --- changelog.d/6524.misc | 2 + synapse/handlers/federation.py | 82 +++++++++++++++++++++++----------- 2 files changed, 58 insertions(+), 26 deletions(-) create mode 100644 changelog.d/6524.misc diff --git a/changelog.d/6524.misc b/changelog.d/6524.misc new file mode 100644 index 0000000000..f885597426 --- /dev/null +++ b/changelog.d/6524.misc @@ -0,0 +1,2 @@ +Improve sanity-checking when receiving events over federation. + diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 31c9132ae9..ebeffbb768 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -571,7 +571,9 @@ class FederationHandler(BaseHandler): @defer.inlineCallbacks @log_function - def _get_state_for_room(self, destination, room_id, event_id, include_event_in_state): + def _get_state_for_room( + self, destination, room_id, event_id, include_event_in_state + ): """Requests all of the room state at a given event from a remote homeserver. Args: @@ -635,6 +637,10 @@ class FederationHandler(BaseHandler): room_id (str) event_ids (Iterable[str]) + If we fail to fetch any of the events, a warning will be logged, and the event + will be omitted from the result. Likewise, any events which turn out not to + be in the given room. + Returns: Deferred[dict[str, EventBase]]: A deferred resolving to a map from event_id to event @@ -643,35 +649,59 @@ class FederationHandler(BaseHandler): missing_events = set(event_ids) - fetched_events.keys() - if not missing_events: - return fetched_events + if missing_events: + logger.debug( + "Fetching unknown state/auth events %s for room %s", + missing_events, + room_id, + ) - logger.debug( - "Fetching unknown state/auth events %s for room %s", - missing_events, - event_ids, + room_version = yield self.store.get_room_version(room_id) + + # XXX 20 requests at once? really? + for batch in batch_iter(missing_events, 20): + deferreds = [ + run_in_background( + self.federation_client.get_pdu, + destinations=[destination], + event_id=e_id, + room_version=room_version, + ) + for e_id in batch + ] + + res = yield make_deferred_yieldable( + defer.DeferredList(deferreds, consumeErrors=True) + ) + + for success, result in res: + if success and result: + fetched_events[result.event_id] = result + + # check for events which were in the wrong room. + # + # this can happen if a remote server claims that the state or + # auth_events at an event in room A are actually events in room B + + bad_events = list( + (event_id, event.room_id) + for event_id, event in fetched_events.items() + if event.room_id != room_id ) - room_version = yield self.store.get_room_version(room_id) - - # XXX 20 requests at once? really? - for batch in batch_iter(missing_events, 20): - deferreds = [ - run_in_background( - self.federation_client.get_pdu, - destinations=[destination], - event_id=e_id, - room_version=room_version, - ) - for e_id in batch - ] - - res = yield make_deferred_yieldable( - defer.DeferredList(deferreds, consumeErrors=True) + for bad_event_id, bad_room_id in bad_events: + # This is a bogus situation, but since we may only discover it a long time + # after it happened, we try our best to carry on, by just omitting the + # bad events from the returned auth/state set. + logger.warning( + "Remote server %s claims event %s in room %s is an auth/state " + "event in room %s", + destination, + bad_event_id, + bad_room_id, + room_id, ) - for success, result in res: - if success and result: - fetched_events[result.event_id] = result + del fetched_events[bad_event_id] return fetched_events From 6577f2d8877b89f7198f7fb03cf57f10a75728ca Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 13 Dec 2019 11:44:41 +0000 Subject: [PATCH 0705/1623] Sanity-check room ids in event auth (#6530) When we do an event auth operation, check that all of the events involved are in the right room. --- changelog.d/6530.misc | 2 ++ synapse/event_auth.py | 12 ++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 changelog.d/6530.misc diff --git a/changelog.d/6530.misc b/changelog.d/6530.misc new file mode 100644 index 0000000000..f885597426 --- /dev/null +++ b/changelog.d/6530.misc @@ -0,0 +1,2 @@ +Improve sanity-checking when receiving events over federation. + diff --git a/synapse/event_auth.py b/synapse/event_auth.py index ec3243b27b..d184b0273b 100644 --- a/synapse/event_auth.py +++ b/synapse/event_auth.py @@ -48,6 +48,18 @@ def check(room_version, event, auth_events, do_sig_check=True, do_size_check=Tru if not hasattr(event, "room_id"): raise AuthError(500, "Event has no room_id: %s" % event) + room_id = event.room_id + + # I'm not really expecting to get auth events in the wrong room, but let's + # sanity-check it + for auth_event in auth_events.values(): + if auth_event.room_id != room_id: + raise Exception( + "During auth for event %s in room %s, found event %s in the state " + "which is in room %s" + % (event.event_id, room_id, auth_event.event_id, auth_event.room_id) + ) + if do_sig_check: sender_domain = get_domain_from_id(event.sender) From 83895316d4e18d4a52c43524942d98f864bac6f9 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 13 Dec 2019 12:55:32 +0000 Subject: [PATCH 0706/1623] sanity-checking for events used in state res (#6531) When we perform state resolution, check that all of the events involved are in the right room. --- changelog.d/6531.misc | 1 + synapse/handlers/federation.py | 1 + synapse/state/__init__.py | 32 +++++++---- synapse/state/v1.py | 34 +++++++++-- synapse/state/v2.py | 100 ++++++++++++++++++++++++--------- tests/state/test_v2.py | 3 + 6 files changed, 128 insertions(+), 43 deletions(-) create mode 100644 changelog.d/6531.misc diff --git a/changelog.d/6531.misc b/changelog.d/6531.misc new file mode 100644 index 0000000000..598efb79fc --- /dev/null +++ b/changelog.d/6531.misc @@ -0,0 +1 @@ +Improve sanity-checking when receiving events over federation. diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index ebeffbb768..fd3f5ced55 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -397,6 +397,7 @@ class FederationHandler(BaseHandler): event_map[x.event_id] = x state_map = yield resolve_events_with_store( + room_id, room_version, state_maps, event_map, diff --git a/synapse/state/__init__.py b/synapse/state/__init__.py index 139beef8ed..0e75e94c6f 100644 --- a/synapse/state/__init__.py +++ b/synapse/state/__init__.py @@ -16,7 +16,7 @@ import logging from collections import namedtuple -from typing import Iterable, Optional +from typing import Dict, Iterable, List, Optional, Tuple from six import iteritems, itervalues @@ -416,6 +416,7 @@ class StateHandler(object): with Measure(self.clock, "state._resolve_events"): new_state = yield resolve_events_with_store( + event.room_id, room_version, state_set_ids, event_map=state_map, @@ -461,7 +462,7 @@ class StateResolutionHandler(object): not be called for a single state group Args: - room_id (str): room we are resolving for (used for logging) + room_id (str): room we are resolving for (used for logging and sanity checks) room_version (str): version of the room state_groups_ids (dict[int, dict[(str, str), str]]): map from state group id to the state in that state group @@ -517,6 +518,7 @@ class StateResolutionHandler(object): logger.info("Resolving conflicted state for %r", room_id) with Measure(self.clock, "state._resolve_events"): new_state = yield resolve_events_with_store( + room_id, room_version, list(itervalues(state_groups_ids)), event_map=event_map, @@ -588,36 +590,44 @@ def _make_state_cache_entry(new_state, state_groups_ids): ) -def resolve_events_with_store(room_version, state_sets, event_map, state_res_store): +def resolve_events_with_store( + room_id: str, + room_version: str, + state_sets: List[Dict[Tuple[str, str], str]], + event_map: Optional[Dict[str, EventBase]], + state_res_store: "StateResolutionStore", +): """ Args: - room_version(str): Version of the room + room_id: the room we are working in - state_sets(list): List of dicts of (type, state_key) -> event_id, + room_version: Version of the room + + state_sets: List of dicts of (type, state_key) -> event_id, which are the different state groups to resolve. - event_map(dict[str,FrozenEvent]|None): + event_map: a dict from event_id to event, for any events that we happen to have in flight (eg, those currently being persisted). This will be used as a starting point fof finding the state we need; any missing events will be requested via state_map_factory. - If None, all events will be fetched via state_map_factory. + If None, all events will be fetched via state_res_store. - state_res_store (StateResolutionStore) + state_res_store: a place to fetch events from - Returns + Returns: Deferred[dict[(str, str), str]]: a map from (type, state_key) to event_id. """ v = KNOWN_ROOM_VERSIONS[room_version] if v.state_res == StateResolutionVersions.V1: return v1.resolve_events_with_store( - state_sets, event_map, state_res_store.get_events + room_id, state_sets, event_map, state_res_store.get_events ) else: return v2.resolve_events_with_store( - room_version, state_sets, event_map, state_res_store + room_id, room_version, state_sets, event_map, state_res_store ) diff --git a/synapse/state/v1.py b/synapse/state/v1.py index a2f92d9ff9..b2f9865f39 100644 --- a/synapse/state/v1.py +++ b/synapse/state/v1.py @@ -15,6 +15,7 @@ import hashlib import logging +from typing import Callable, Dict, List, Optional, Tuple from six import iteritems, iterkeys, itervalues @@ -24,6 +25,7 @@ from synapse import event_auth from synapse.api.constants import EventTypes from synapse.api.errors import AuthError from synapse.api.room_versions import RoomVersions +from synapse.events import EventBase logger = logging.getLogger(__name__) @@ -32,13 +34,20 @@ POWER_KEY = (EventTypes.PowerLevels, "") @defer.inlineCallbacks -def resolve_events_with_store(state_sets, event_map, state_map_factory): +def resolve_events_with_store( + room_id: str, + state_sets: List[Dict[Tuple[str, str], str]], + event_map: Optional[Dict[str, EventBase]], + state_map_factory: Callable, +): """ Args: - state_sets(list): List of dicts of (type, state_key) -> event_id, + room_id: the room we are working in + + state_sets: List of dicts of (type, state_key) -> event_id, which are the different state groups to resolve. - event_map(dict[str,FrozenEvent]|None): + event_map: a dict from event_id to event, for any events that we happen to have in flight (eg, those currently being persisted). This will be used as a starting point fof finding the state we need; any missing @@ -46,11 +55,11 @@ def resolve_events_with_store(state_sets, event_map, state_map_factory): If None, all events will be fetched via state_map_factory. - state_map_factory(func): will be called + state_map_factory: will be called with a list of event_ids that are needed, and should return with a Deferred of dict of event_id to event. - Returns + Returns: Deferred[dict[(str, str), str]]: a map from (type, state_key) to event_id. """ @@ -76,6 +85,14 @@ def resolve_events_with_store(state_sets, event_map, state_map_factory): if event_map is not None: state_map.update(event_map) + # everything in the state map should be in the right room + for event in state_map.values(): + if event.room_id != room_id: + raise Exception( + "Attempting to state-resolve for room %s with event %s which is in %s" + % (room_id, event.event_id, event.room_id,) + ) + # get the ids of the auth events which allow us to authenticate the # conflicted state, picking only from the unconflicting state. # @@ -95,6 +112,13 @@ def resolve_events_with_store(state_sets, event_map, state_map_factory): ) state_map_new = yield state_map_factory(new_needed_events) + for event in state_map_new.values(): + if event.room_id != room_id: + raise Exception( + "Attempting to state-resolve for room %s with event %s which is in %s" + % (room_id, event.event_id, event.room_id,) + ) + state_map.update(state_map_new) return _resolve_with_state( diff --git a/synapse/state/v2.py b/synapse/state/v2.py index b327c86f40..cb77ed5b78 100644 --- a/synapse/state/v2.py +++ b/synapse/state/v2.py @@ -16,29 +16,40 @@ import heapq import itertools import logging +from typing import Dict, List, Optional, Tuple from six import iteritems, itervalues from twisted.internet import defer +import synapse.state from synapse import event_auth from synapse.api.constants import EventTypes from synapse.api.errors import AuthError +from synapse.events import EventBase logger = logging.getLogger(__name__) @defer.inlineCallbacks -def resolve_events_with_store(room_version, state_sets, event_map, state_res_store): +def resolve_events_with_store( + room_id: str, + room_version: str, + state_sets: List[Dict[Tuple[str, str], str]], + event_map: Optional[Dict[str, EventBase]], + state_res_store: "synapse.state.StateResolutionStore", +): """Resolves the state using the v2 state resolution algorithm Args: - room_version (str): The room version + room_id: the room we are working in - state_sets(list): List of dicts of (type, state_key) -> event_id, + room_version: The room version + + state_sets: List of dicts of (type, state_key) -> event_id, which are the different state groups to resolve. - event_map(dict[str,FrozenEvent]|None): + event_map: a dict from event_id to event, for any events that we happen to have in flight (eg, those currently being persisted). This will be used as a starting point fof finding the state we need; any missing @@ -46,9 +57,9 @@ def resolve_events_with_store(room_version, state_sets, event_map, state_res_sto If None, all events will be fetched via state_res_store. - state_res_store (StateResolutionStore) + state_res_store: - Returns + Returns: Deferred[dict[(str, str), str]]: a map from (type, state_key) to event_id. """ @@ -84,6 +95,14 @@ def resolve_events_with_store(room_version, state_sets, event_map, state_res_sto ) event_map.update(events) + # everything in the event map should be in the right room + for event in event_map.values(): + if event.room_id != room_id: + raise Exception( + "Attempting to state-resolve for room %s with event %s which is in %s" + % (room_id, event.event_id, event.room_id,) + ) + full_conflicted_set = set(eid for eid in full_conflicted_set if eid in event_map) logger.debug("%d full_conflicted_set entries", len(full_conflicted_set)) @@ -94,13 +113,14 @@ def resolve_events_with_store(room_version, state_sets, event_map, state_res_sto ) sorted_power_events = yield _reverse_topological_power_sort( - power_events, event_map, state_res_store, full_conflicted_set + room_id, power_events, event_map, state_res_store, full_conflicted_set ) logger.debug("sorted %d power events", len(sorted_power_events)) # Now sequentially auth each one resolved_state = yield _iterative_auth_checks( + room_id, room_version, sorted_power_events, unconflicted_state, @@ -121,13 +141,18 @@ def resolve_events_with_store(room_version, state_sets, event_map, state_res_sto pl = resolved_state.get((EventTypes.PowerLevels, ""), None) leftover_events = yield _mainline_sort( - leftover_events, pl, event_map, state_res_store + room_id, leftover_events, pl, event_map, state_res_store ) logger.debug("resolving remaining events") resolved_state = yield _iterative_auth_checks( - room_version, leftover_events, resolved_state, event_map, state_res_store + room_id, + room_version, + leftover_events, + resolved_state, + event_map, + state_res_store, ) logger.debug("resolved") @@ -141,11 +166,12 @@ def resolve_events_with_store(room_version, state_sets, event_map, state_res_sto @defer.inlineCallbacks -def _get_power_level_for_sender(event_id, event_map, state_res_store): +def _get_power_level_for_sender(room_id, event_id, event_map, state_res_store): """Return the power level of the sender of the given event according to their auth events. Args: + room_id (str) event_id (str) event_map (dict[str,FrozenEvent]) state_res_store (StateResolutionStore) @@ -153,11 +179,11 @@ def _get_power_level_for_sender(event_id, event_map, state_res_store): Returns: Deferred[int] """ - event = yield _get_event(event_id, event_map, state_res_store) + event = yield _get_event(room_id, event_id, event_map, state_res_store) pl = None for aid in event.auth_event_ids(): - aev = yield _get_event(aid, event_map, state_res_store) + aev = yield _get_event(room_id, aid, event_map, state_res_store) if (aev.type, aev.state_key) == (EventTypes.PowerLevels, ""): pl = aev break @@ -165,7 +191,7 @@ def _get_power_level_for_sender(event_id, event_map, state_res_store): if pl is None: # Couldn't find power level. Check if they're the creator of the room for aid in event.auth_event_ids(): - aev = yield _get_event(aid, event_map, state_res_store) + aev = yield _get_event(room_id, aid, event_map, state_res_store) if (aev.type, aev.state_key) == (EventTypes.Create, ""): if aev.content.get("creator") == event.sender: return 100 @@ -279,7 +305,7 @@ def _is_power_event(event): @defer.inlineCallbacks def _add_event_and_auth_chain_to_graph( - graph, event_id, event_map, state_res_store, auth_diff + graph, room_id, event_id, event_map, state_res_store, auth_diff ): """Helper function for _reverse_topological_power_sort that add the event and its auth chain (that is in the auth diff) to the graph @@ -287,6 +313,7 @@ def _add_event_and_auth_chain_to_graph( Args: graph (dict[str, set[str]]): A map from event ID to the events auth event IDs + room_id (str): the room we are working in event_id (str): Event to add to the graph event_map (dict[str,FrozenEvent]) state_res_store (StateResolutionStore) @@ -298,7 +325,7 @@ def _add_event_and_auth_chain_to_graph( eid = state.pop() graph.setdefault(eid, set()) - event = yield _get_event(eid, event_map, state_res_store) + event = yield _get_event(room_id, eid, event_map, state_res_store) for aid in event.auth_event_ids(): if aid in auth_diff: if aid not in graph: @@ -308,11 +335,14 @@ def _add_event_and_auth_chain_to_graph( @defer.inlineCallbacks -def _reverse_topological_power_sort(event_ids, event_map, state_res_store, auth_diff): +def _reverse_topological_power_sort( + room_id, event_ids, event_map, state_res_store, auth_diff +): """Returns a list of the event_ids sorted by reverse topological ordering, and then by power level and origin_server_ts Args: + room_id (str): the room we are working in event_ids (list[str]): The events to sort event_map (dict[str,FrozenEvent]) state_res_store (StateResolutionStore) @@ -325,12 +355,14 @@ def _reverse_topological_power_sort(event_ids, event_map, state_res_store, auth_ graph = {} for event_id in event_ids: yield _add_event_and_auth_chain_to_graph( - graph, event_id, event_map, state_res_store, auth_diff + graph, room_id, event_id, event_map, state_res_store, auth_diff ) event_to_pl = {} for event_id in graph: - pl = yield _get_power_level_for_sender(event_id, event_map, state_res_store) + pl = yield _get_power_level_for_sender( + room_id, event_id, event_map, state_res_store + ) event_to_pl[event_id] = pl def _get_power_order(event_id): @@ -348,12 +380,13 @@ def _reverse_topological_power_sort(event_ids, event_map, state_res_store, auth_ @defer.inlineCallbacks def _iterative_auth_checks( - room_version, event_ids, base_state, event_map, state_res_store + room_id, room_version, event_ids, base_state, event_map, state_res_store ): """Sequentially apply auth checks to each event in given list, updating the state as it goes along. Args: + room_id (str) room_version (str) event_ids (list[str]): Ordered list of events to apply auth checks to base_state (dict[tuple[str, str], str]): The set of state to start with @@ -370,7 +403,7 @@ def _iterative_auth_checks( auth_events = {} for aid in event.auth_event_ids(): - ev = yield _get_event(aid, event_map, state_res_store) + ev = yield _get_event(room_id, aid, event_map, state_res_store) if ev.rejected_reason is None: auth_events[(ev.type, ev.state_key)] = ev @@ -378,7 +411,7 @@ def _iterative_auth_checks( for key in event_auth.auth_types_for_event(event): if key in resolved_state: ev_id = resolved_state[key] - ev = yield _get_event(ev_id, event_map, state_res_store) + ev = yield _get_event(room_id, ev_id, event_map, state_res_store) if ev.rejected_reason is None: auth_events[key] = event_map[ev_id] @@ -400,11 +433,14 @@ def _iterative_auth_checks( @defer.inlineCallbacks -def _mainline_sort(event_ids, resolved_power_event_id, event_map, state_res_store): +def _mainline_sort( + room_id, event_ids, resolved_power_event_id, event_map, state_res_store +): """Returns a sorted list of event_ids sorted by mainline ordering based on the given event resolved_power_event_id Args: + room_id (str): room we're working in event_ids (list[str]): Events to sort resolved_power_event_id (str): The final resolved power level event ID event_map (dict[str,FrozenEvent]) @@ -417,11 +453,11 @@ def _mainline_sort(event_ids, resolved_power_event_id, event_map, state_res_stor pl = resolved_power_event_id while pl: mainline.append(pl) - pl_ev = yield _get_event(pl, event_map, state_res_store) + pl_ev = yield _get_event(room_id, pl, event_map, state_res_store) auth_events = pl_ev.auth_event_ids() pl = None for aid in auth_events: - ev = yield _get_event(aid, event_map, state_res_store) + ev = yield _get_event(room_id, aid, event_map, state_res_store) if (ev.type, ev.state_key) == (EventTypes.PowerLevels, ""): pl = aid break @@ -457,6 +493,8 @@ def _get_mainline_depth_for_event(event, mainline_map, event_map, state_res_stor Deferred[int] """ + room_id = event.room_id + # We do an iterative search, replacing `event with the power level in its # auth events (if any) while event: @@ -468,7 +506,7 @@ def _get_mainline_depth_for_event(event, mainline_map, event_map, state_res_stor event = None for aid in auth_events: - aev = yield _get_event(aid, event_map, state_res_store) + aev = yield _get_event(room_id, aid, event_map, state_res_store) if (aev.type, aev.state_key) == (EventTypes.PowerLevels, ""): event = aev break @@ -478,11 +516,12 @@ def _get_mainline_depth_for_event(event, mainline_map, event_map, state_res_stor @defer.inlineCallbacks -def _get_event(event_id, event_map, state_res_store): +def _get_event(room_id, event_id, event_map, state_res_store): """Helper function to look up event in event_map, falling back to looking it up in the store Args: + room_id (str) event_id (str) event_map (dict[str,FrozenEvent]) state_res_store (StateResolutionStore) @@ -493,7 +532,14 @@ def _get_event(event_id, event_map, state_res_store): if event_id not in event_map: events = yield state_res_store.get_events([event_id], allow_rejected=True) event_map.update(events) - return event_map[event_id] + event = event_map[event_id] + assert event is not None + if event.room_id != room_id: + raise Exception( + "In state res for room %s, event %s is in %s" + % (room_id, event_id, event.room_id) + ) + return event def lexicographical_topological_sort(graph, key): diff --git a/tests/state/test_v2.py b/tests/state/test_v2.py index 8d3845c870..0f341d3ac3 100644 --- a/tests/state/test_v2.py +++ b/tests/state/test_v2.py @@ -58,6 +58,7 @@ class FakeEvent(object): self.type = type self.state_key = state_key self.content = content + self.room_id = ROOM_ID def to_event(self, auth_events, prev_events): """Given the auth_events and prev_events, convert to a Frozen Event @@ -418,6 +419,7 @@ class StateTestCase(unittest.TestCase): state_before = dict(state_at_event[prev_events[0]]) else: state_d = resolve_events_with_store( + ROOM_ID, RoomVersions.V2.identifier, [state_at_event[n] for n in prev_events], event_map=event_map, @@ -565,6 +567,7 @@ class SimpleParamStateTestCase(unittest.TestCase): # Test that we correctly handle passing `None` as the event_map state_d = resolve_events_with_store( + ROOM_ID, RoomVersions.V2.identifier, [self.state_at_bob, self.state_at_charlie], event_map=None, From ff773ff7243fbbe88fabff952d6faded0241c64e Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 16 Dec 2019 12:26:28 +0000 Subject: [PATCH 0707/1623] Persist auth/state events at backwards extremities when we fetch them (#6526) The main point here is to make sure that the state returned by _get_state_in_room has been authed before we try to use it as state in the room. --- changelog.d/6526.bugfix | 1 + synapse/handlers/federation.py | 243 +++++++++++---------------------- synapse/util/async_helpers.py | 4 +- 3 files changed, 83 insertions(+), 165 deletions(-) create mode 100644 changelog.d/6526.bugfix diff --git a/changelog.d/6526.bugfix b/changelog.d/6526.bugfix new file mode 100644 index 0000000000..53214b0748 --- /dev/null +++ b/changelog.d/6526.bugfix @@ -0,0 +1 @@ +Fix a bug which could cause the federation server to incorrectly return errors when handling certain obscure event graphs. \ No newline at end of file diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index fd3f5ced55..f4ac0bfbc8 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -64,8 +64,7 @@ from synapse.replication.http.federation import ( from synapse.replication.http.membership import ReplicationUserJoinedLeftRoomRestServlet from synapse.state import StateResolutionStore, resolve_events_with_store from synapse.types import UserID, get_domain_from_id -from synapse.util import batch_iter, unwrapFirstError -from synapse.util.async_helpers import Linearizer +from synapse.util.async_helpers import Linearizer, concurrently_execute from synapse.util.distributor import user_joined_room from synapse.util.retryutils import NotRetryingDestination from synapse.visibility import filter_events_for_server @@ -240,7 +239,6 @@ class FederationHandler(BaseHandler): return None state = None - auth_chain = [] # Get missing pdus if necessary. if not pdu.internal_metadata.is_outlier(): @@ -346,7 +344,6 @@ class FederationHandler(BaseHandler): # Calculate the state after each of the previous events, and # resolve them to find the correct state at the current event. - auth_chains = set() event_map = {event_id: pdu} try: # Get the state of the events we know about @@ -370,24 +367,14 @@ class FederationHandler(BaseHandler): p, ) - room_version = yield self.store.get_room_version(room_id) - with nested_logging_context(p): # note that if any of the missing prevs share missing state or # auth events, the requests to fetch those events are deduped # by the get_pdu_cache in federation_client. - ( - remote_state, - got_auth_chain, - ) = yield self._get_state_for_room( + (remote_state, _,) = yield self._get_state_for_room( origin, room_id, p, include_event_in_state=True ) - # XXX hrm I'm not convinced that duplicate events will compare - # for equality, so I'm not sure this does what the author - # hoped. - auth_chains.update(got_auth_chain) - remote_state_map = { (x.type, x.state_key): x.event_id for x in remote_state } @@ -396,6 +383,7 @@ class FederationHandler(BaseHandler): for x in remote_state: event_map[x.event_id] = x + room_version = yield self.store.get_room_version(room_id) state_map = yield resolve_events_with_store( room_id, room_version, @@ -417,7 +405,6 @@ class FederationHandler(BaseHandler): event_map.update(evs) state = [event_map[e] for e in six.itervalues(state_map)] - auth_chain = list(auth_chains) except Exception: logger.warning( "[%s %s] Error attempting to resolve state at missing " @@ -433,9 +420,7 @@ class FederationHandler(BaseHandler): affected=event_id, ) - yield self._process_received_pdu( - origin, pdu, state=state, auth_chain=auth_chain - ) + yield self._process_received_pdu(origin, pdu, state=state) @defer.inlineCallbacks def _get_missing_events_for_pdu(self, origin, pdu, prevs, min_depth): @@ -638,6 +623,8 @@ class FederationHandler(BaseHandler): room_id (str) event_ids (Iterable[str]) + Persists any events we don't already have as outliers. + If we fail to fetch any of the events, a warning will be logged, and the event will be omitted from the result. Likewise, any events which turn out not to be in the given room. @@ -657,27 +644,15 @@ class FederationHandler(BaseHandler): room_id, ) - room_version = yield self.store.get_room_version(room_id) + yield self._get_events_and_persist( + destination=destination, room_id=room_id, events=missing_events + ) - # XXX 20 requests at once? really? - for batch in batch_iter(missing_events, 20): - deferreds = [ - run_in_background( - self.federation_client.get_pdu, - destinations=[destination], - event_id=e_id, - room_version=room_version, - ) - for e_id in batch - ] - - res = yield make_deferred_yieldable( - defer.DeferredList(deferreds, consumeErrors=True) - ) - - for success, result in res: - if success and result: - fetched_events[result.event_id] = result + # we need to make sure we re-load from the database to get the rejected + # state correct. + fetched_events.update( + (yield self.store.get_events(missing_events, allow_rejected=True)) + ) # check for events which were in the wrong room. # @@ -707,50 +682,24 @@ class FederationHandler(BaseHandler): return fetched_events @defer.inlineCallbacks - def _process_received_pdu(self, origin, event, state, auth_chain): + def _process_received_pdu(self, origin, event, state): """ Called when we have a new pdu. We need to do auth checks and put it through the StateHandler. + + Args: + origin: server sending the event + + event: event to be persisted + + state: Normally None, but if we are handling a gap in the graph + (ie, we are missing one or more prev_events), the resolved state at the + event """ room_id = event.room_id event_id = event.event_id logger.debug("[%s %s] Processing event: %s", room_id, event_id, event) - event_ids = set() - if state: - event_ids |= {e.event_id for e in state} - if auth_chain: - event_ids |= {e.event_id for e in auth_chain} - - seen_ids = yield self.store.have_seen_events(event_ids) - - if state and auth_chain is not None: - # If we have any state or auth_chain given to us by the replication - # layer, then we should handle them (if we haven't before.) - - event_infos = [] - - for e in itertools.chain(auth_chain, state): - if e.event_id in seen_ids: - continue - e.internal_metadata.outlier = True - auth_ids = e.auth_event_ids() - auth = { - (e.type, e.state_key): e - for e in auth_chain - if e.event_id in auth_ids or e.type == EventTypes.Create - } - event_infos.append(_NewEventInfo(event=e, auth_events=auth)) - seen_ids.add(e.event_id) - - logger.info( - "[%s %s] persisting newly-received auth/state events %s", - room_id, - event_id, - [e.event.event_id for e in event_infos], - ) - yield self._handle_new_events(origin, event_infos) - try: context = yield self._handle_new_event(origin, event, state=state) except AuthError as e: @@ -806,8 +755,6 @@ class FederationHandler(BaseHandler): if dest == self.server_name: raise SynapseError(400, "Can't backfill from self.") - room_version = yield self.store.get_room_version(room_id) - events = yield self.federation_client.backfill( dest, room_id, limit=limit, extremities=extremities ) @@ -836,6 +783,9 @@ class FederationHandler(BaseHandler): event_ids = set(e.event_id for e in events) + # build a list of events whose prev_events weren't in the batch. + # (XXX: this will include events whose prev_events we already have; that doesn't + # sound right?) edges = [ev.event_id for ev in events if set(ev.prev_event_ids()) - event_ids] logger.info("backfill: Got %d events with %d edges", len(events), len(edges)) @@ -864,95 +814,11 @@ class FederationHandler(BaseHandler): auth_events.update( {e_id: event_map[e_id] for e_id in required_auth if e_id in event_map} ) - missing_auth = required_auth - set(auth_events) - failed_to_fetch = set() - # Try and fetch any missing auth events from both DB and remote servers. - # We repeatedly do this until we stop finding new auth events. - while missing_auth - failed_to_fetch: - logger.info("Missing auth for backfill: %r", missing_auth) - ret_events = yield self.store.get_events(missing_auth - failed_to_fetch) - auth_events.update(ret_events) - - required_auth.update( - a_id for event in ret_events.values() for a_id in event.auth_event_ids() - ) - missing_auth = required_auth - set(auth_events) - - if missing_auth - failed_to_fetch: - logger.info( - "Fetching missing auth for backfill: %r", - missing_auth - failed_to_fetch, - ) - - results = yield make_deferred_yieldable( - defer.gatherResults( - [ - run_in_background( - self.federation_client.get_pdu, - [dest], - event_id, - room_version=room_version, - outlier=True, - timeout=10000, - ) - for event_id in missing_auth - failed_to_fetch - ], - consumeErrors=True, - ) - ).addErrback(unwrapFirstError) - auth_events.update({a.event_id: a for a in results if a}) - required_auth.update( - a_id - for event in results - if event - for a_id in event.auth_event_ids() - ) - missing_auth = required_auth - set(auth_events) - - failed_to_fetch = missing_auth - set(auth_events) - - seen_events = yield self.store.have_seen_events( - set(auth_events.keys()) | set(state_events.keys()) - ) - - # We now have a chunk of events plus associated state and auth chain to - # persist. We do the persistence in two steps: - # 1. Auth events and state get persisted as outliers, plus the - # backward extremities get persisted (as non-outliers). - # 2. The rest of the events in the chunk get persisted one by one, as - # each one depends on the previous event for its state. - # - # The important thing is that events in the chunk get persisted as - # non-outliers, including when those events are also in the state or - # auth chain. Caution must therefore be taken to ensure that they are - # not accidentally marked as outliers. - - # Step 1a: persist auth events that *don't* appear in the chunk ev_infos = [] - for a in auth_events.values(): - # We only want to persist auth events as outliers that we haven't - # seen and aren't about to persist as part of the backfilled chunk. - if a.event_id in seen_events or a.event_id in event_map: - continue - a.internal_metadata.outlier = True - ev_infos.append( - _NewEventInfo( - event=a, - auth_events={ - ( - auth_events[a_id].type, - auth_events[a_id].state_key, - ): auth_events[a_id] - for a_id in a.auth_event_ids() - if a_id in auth_events - }, - ) - ) - - # Step 1b: persist the events in the chunk we fetched state for (i.e. - # the backwards extremities) as non-outliers. + # Step 1: persist the events in the chunk we fetched state for (i.e. + # the backwards extremities), with custom auth events and state for e_id in events_to_state: # For paranoia we ensure that these events are marked as # non-outliers @@ -1194,6 +1060,57 @@ class FederationHandler(BaseHandler): return False + @defer.inlineCallbacks + def _get_events_and_persist( + self, destination: str, room_id: str, events: Iterable[str] + ): + """Fetch the given events from a server, and persist them as outliers. + + Logs a warning if we can't find the given event. + """ + + room_version = yield self.store.get_room_version(room_id) + + event_infos = [] + + async def get_event(event_id: str): + with nested_logging_context(event_id): + try: + event = await self.federation_client.get_pdu( + [destination], event_id, room_version, outlier=True, + ) + if event is None: + logger.warning( + "Server %s didn't return event %s", destination, event_id, + ) + return + + # recursively fetch the auth events for this event + auth_events = await self._get_events_from_store_or_dest( + destination, room_id, event.auth_event_ids() + ) + auth = {} + for auth_event_id in event.auth_event_ids(): + ae = auth_events.get(auth_event_id) + if ae: + auth[(ae.type, ae.state_key)] = ae + + event_infos.append(_NewEventInfo(event, None, auth)) + + except Exception as e: + logger.warning( + "Error fetching missing state/auth event %s: %s %s", + event_id, + type(e), + e, + ) + + yield concurrently_execute(get_event, events, 5) + + yield self._handle_new_events( + destination, event_infos, + ) + def _sanity_check_event(self, ev): """ Do some early sanity checks of a received event diff --git a/synapse/util/async_helpers.py b/synapse/util/async_helpers.py index 5c4de2e69f..04b6abdc24 100644 --- a/synapse/util/async_helpers.py +++ b/synapse/util/async_helpers.py @@ -140,8 +140,8 @@ def concurrently_execute(func, args, limit): Args: func (func): Function to execute, should return a deferred or coroutine. - args (list): List of arguments to pass to func, each invocation of func - gets a signle argument. + args (Iterable): List of arguments to pass to func, each invocation of func + gets a single argument. limit (int): Maximum number of conccurent executions. Returns: From bbb75ff6eeda25e2f0eebd0a6639efd48b4dbb3c Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 16 Dec 2019 13:14:37 +0000 Subject: [PATCH 0708/1623] Exclude rejected state events when calculating state at backwards extrems (#6527) This fixes a weird bug where, if you were determined enough, you could end up with a rejected event forming part of the state at a backwards-extremity. Authing that backwards extrem would then lead to us trying to pull the rejected event from the db (with allow_rejected=False), which would fail with a 404. --- changelog.d/6527.bugfix | 1 + synapse/handlers/federation.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/6527.bugfix diff --git a/changelog.d/6527.bugfix b/changelog.d/6527.bugfix new file mode 100644 index 0000000000..53214b0748 --- /dev/null +++ b/changelog.d/6527.bugfix @@ -0,0 +1 @@ +Fix a bug which could cause the federation server to incorrectly return errors when handling certain obscure event graphs. \ No newline at end of file diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index f4ac0bfbc8..abe02907b9 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -606,7 +606,7 @@ class FederationHandler(BaseHandler): remote_event = event_map.get(event_id) if not remote_event: raise Exception("Unable to get missing prev_event %s" % (event_id,)) - if remote_event.is_state(): + if remote_event.is_state() and remote_event.rejected_reason is None: remote_state.append(remote_event) auth_chain = [event_map[e_id] for e_id in auth_event_ids if e_id in event_map] From 596dd9914dad1933ded1426bdec1e2b1e6874e39 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Mon, 16 Dec 2019 14:53:21 +0000 Subject: [PATCH 0709/1623] Add test case --- tests/rest/client/v1/test_rooms.py | 133 +++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py index 1ca7fa742f..9cb505f316 100644 --- a/tests/rest/client/v1/test_rooms.py +++ b/tests/rest/client/v1/test_rooms.py @@ -29,6 +29,7 @@ import synapse.rest.admin from synapse.api.constants import EventContentFields, EventTypes, Membership from synapse.handlers.pagination import PurgeStatus from synapse.rest.client.v1 import login, profile, room +from synapse.rest.client.v2_alpha import account from synapse.util.stringutils import random_string from tests import unittest @@ -1597,3 +1598,135 @@ class LabelsTestCase(unittest.HomeserverTestCase): ) return event_id + + +class ContextTestCase(unittest.HomeserverTestCase): + + servlets = [ + synapse.rest.admin.register_servlets_for_client_rest_resource, + room.register_servlets, + login.register_servlets, + account.register_servlets, + ] + + def make_homeserver(self, reactor, clock): + self.hs = self.setup_test_homeserver() + + return self.hs + + def prepare(self, reactor, clock, homeserver): + self.user_id = self.register_user("user", "password") + self.tok = self.login("user", "password") + self.room_id = self.helper.create_room_as(self.user_id, tok=self.tok) + + self.other_user_id = self.register_user("user2", "password") + self.other_tok = self.login("user2", "password") + + self.helper.invite(self.room_id, self.user_id, self.other_user_id, tok=self.tok) + self.helper.join(self.room_id, self.other_user_id, tok=self.other_tok) + + def test_erased_sender(self): + """Test that an erasure request results in the requester's events being hidden + from any new member of the room. + """ + + # Send a bunch of events in the room. + + self.helper.send(self.room_id, "message 1", tok=self.tok) + self.helper.send(self.room_id, "message 2", tok=self.tok) + event_id = self.helper.send(self.room_id, "message 3", tok=self.tok)["event_id"] + self.helper.send(self.room_id, "message 4", tok=self.tok) + self.helper.send(self.room_id, "message 5", tok=self.tok) + + # Check that we can still see the messages before the erasure request. + + request, channel = self.make_request( + "GET", + '/rooms/%s/context/%s?filter={"types":["m.room.message"]}' + % (self.room_id, event_id), + access_token=self.tok, + ) + self.render(request) + self.assertEqual(channel.code, 200, channel.result) + + events_before = channel.json_body["events_before"] + + self.assertEqual(len(events_before), 2, events_before) + self.assertEqual( + events_before[0].get("content", {}).get("body"), + "message 2", + events_before[0], + ) + self.assertEqual( + events_before[1].get("content", {}).get("body"), + "message 1", + events_before[1], + ) + + self.assertEqual( + channel.json_body["event"].get("content", {}).get("body"), + "message 3", + channel.json_body["event"], + ) + + events_after = channel.json_body["events_after"] + + self.assertEqual(len(events_after), 2, events_after) + self.assertEqual( + events_after[0].get("content", {}).get("body"), + "message 4", + events_after[0], + ) + self.assertEqual( + events_after[1].get("content", {}).get("body"), + "message 5", + events_after[1], + ) + + # Deactivate the first account and erase the user's data. + + deactivate_account_handler = self.hs.get_deactivate_account_handler() + self.get_success( + deactivate_account_handler.deactivate_account(self.user_id, erase_data=True) + ) + + # Invite another user in the room. This is needed because messages will be + # pruned only if the user wasn't a member of the room when the messages were + # sent. + + invited_user_id = self.register_user("user3", "password") + invited_tok = self.login("user3", "password") + + self.helper.invite( + self.room_id, self.other_user_id, invited_user_id, tok=self.other_tok + ) + self.helper.join(self.room_id, invited_user_id, tok=invited_tok) + + # Check that a user that joined the room after the erasure request can't see + # the messages anymore. + + request, channel = self.make_request( + 'GET', + '/rooms/%s/context/%s?filter={"types":["m.room.message"]}' + % (self.room_id, event_id), + access_token=invited_tok, + ) + self.render(request) + self.assertEqual(channel.code, 200, channel.result) + + events_before = channel.json_body["events_before"] + + self.assertEqual(len(events_before), 2, events_before) + self.assertDictEqual(events_before[0].get("content"), {}, events_before[0]) + self.assertDictEqual(events_before[1].get("content"), {}, events_before[1]) + + self.assertDictEqual( + channel.json_body["event"].get("content"), {}, channel.json_body["event"] + ) + + events_after = channel.json_body["events_after"] + + self.assertEqual(len(events_after), 2, events_after) + self.assertDictEqual(events_after[0].get("content"), {}, events_after[0]) + self.assertEqual(events_after[1].get("content"), {}, events_after[1]) + From a29420f9f449da72d1d38bcab4cedc182e9f2ba0 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Mon, 16 Dec 2019 14:55:50 +0000 Subject: [PATCH 0710/1623] Lint --- tests/rest/client/v1/test_rooms.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py index 9cb505f316..dca0fef97a 100644 --- a/tests/rest/client/v1/test_rooms.py +++ b/tests/rest/client/v1/test_rooms.py @@ -1706,7 +1706,7 @@ class ContextTestCase(unittest.HomeserverTestCase): # the messages anymore. request, channel = self.make_request( - 'GET', + "GET", '/rooms/%s/context/%s?filter={"types":["m.room.message"]}' % (self.room_id, event_id), access_token=invited_tok, @@ -1729,4 +1729,3 @@ class ContextTestCase(unittest.HomeserverTestCase): self.assertEqual(len(events_after), 2, events_after) self.assertDictEqual(events_after[0].get("content"), {}, events_after[0]) self.assertEqual(events_after[1].get("content"), {}, events_after[1]) - From 284e690aa0e37c0d4d7516fc2f02b2b2fede4601 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Mon, 16 Dec 2019 14:56:05 +0000 Subject: [PATCH 0711/1623] Update changelog.d/6553.bugfix Co-Authored-By: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> --- changelog.d/6553.bugfix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/6553.bugfix b/changelog.d/6553.bugfix index e8f55e2a76..4fe576b873 100644 --- a/changelog.d/6553.bugfix +++ b/changelog.d/6553.bugfix @@ -1 +1 @@ -Fix a bug causing responses to the `/context` client endpoint to not use the pruned version of the event the request is for. +Fix a bug causing responses to the `/context` client endpoint to not use the pruned version of the event. From a82006954912ed96b0d47db43db44e76e5b052d6 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Mon, 16 Dec 2019 16:00:18 +0000 Subject: [PATCH 0712/1623] Incorporate review --- synapse/handlers/room.py | 2 +- tests/rest/client/v1/test_rooms.py | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 7f979e5812..60b8bbc7a5 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -941,7 +941,7 @@ class RoomContextHandler(object): if event_filter: state_events = event_filter.filter(state_events) - results["state"] = state_events + results["state"] = yield filter_evts(state_events) # We use a dummy token here as we only care about the room portion of # the token, which we replace. diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py index dca0fef97a..e3af280ba6 100644 --- a/tests/rest/client/v1/test_rooms.py +++ b/tests/rest/client/v1/test_rooms.py @@ -1609,11 +1609,6 @@ class ContextTestCase(unittest.HomeserverTestCase): account.register_servlets, ] - def make_homeserver(self, reactor, clock): - self.hs = self.setup_test_homeserver() - - return self.hs - def prepare(self, reactor, clock, homeserver): self.user_id = self.register_user("user", "password") self.tok = self.login("user", "password") From bfb95654c97a8d3aa164eff96ecc13755c1c326d Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Mon, 16 Dec 2019 16:11:55 +0000 Subject: [PATCH 0713/1623] Add option to allow profile queries without sharing a room (#6523) --- changelog.d/6523.feature | 1 + docs/sample_config.yaml | 7 +++++++ synapse/config/server.py | 13 +++++++++++++ synapse/handlers/profile.py | 6 +++++- tests/rest/client/v1/test_profile.py | 2 ++ 5 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 changelog.d/6523.feature diff --git a/changelog.d/6523.feature b/changelog.d/6523.feature new file mode 100644 index 0000000000..798fa143df --- /dev/null +++ b/changelog.d/6523.feature @@ -0,0 +1 @@ +Add option `limit_profile_requests_to_users_who_share_rooms` to prevent requirement of a local user sharing a room with another user to query their profile information. diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 4d44e631d1..1787248f53 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -54,6 +54,13 @@ pid_file: DATADIR/homeserver.pid # #require_auth_for_profile_requests: true +# Uncomment to require a user to share a room with another user in order +# to retrieve their profile information. Only checked on Client-Server +# requests. Profile requests from other servers should be checked by the +# requesting server. Defaults to 'false'. +# +#limit_profile_requests_to_users_who_share_rooms: true + # If set to 'true', removes the need for authentication to access the server's # public rooms directory through the client API, meaning that anyone can # query the room directory. Defaults to 'false'. diff --git a/synapse/config/server.py b/synapse/config/server.py index 50af858c76..38f6ff9edc 100644 --- a/synapse/config/server.py +++ b/synapse/config/server.py @@ -102,6 +102,12 @@ class ServerConfig(Config): "require_auth_for_profile_requests", False ) + # Whether to require sharing a room with a user to retrieve their + # profile data + self.limit_profile_requests_to_users_who_share_rooms = config.get( + "limit_profile_requests_to_users_who_share_rooms", False, + ) + if "restrict_public_rooms_to_local_users" in config and ( "allow_public_rooms_without_auth" in config or "allow_public_rooms_over_federation" in config @@ -621,6 +627,13 @@ class ServerConfig(Config): # #require_auth_for_profile_requests: true + # Uncomment to require a user to share a room with another user in order + # to retrieve their profile information. Only checked on Client-Server + # requests. Profile requests from other servers should be checked by the + # requesting server. Defaults to 'false'. + # + #limit_profile_requests_to_users_who_share_rooms: true + # If set to 'true', removes the need for authentication to access the server's # public rooms directory through the client API, meaning that anyone can # query the room directory. Defaults to 'false'. diff --git a/synapse/handlers/profile.py b/synapse/handlers/profile.py index 1e5a4613c9..f9579d69ee 100644 --- a/synapse/handlers/profile.py +++ b/synapse/handlers/profile.py @@ -295,12 +295,16 @@ class BaseProfileHandler(BaseHandler): be found to be in any room the server is in, and therefore the query is denied. """ + # Implementation of MSC1301: don't allow looking up profiles if the # requester isn't in the same room as the target. We expect requester to # be None when this function is called outside of a profile query, e.g. # when building a membership event. In this case, we must allow the # lookup. - if not self.hs.config.require_auth_for_profile_requests or not requester: + if ( + not self.hs.config.limit_profile_requests_to_users_who_share_rooms + or not requester + ): return # Always allow the user to query their own profile. diff --git a/tests/rest/client/v1/test_profile.py b/tests/rest/client/v1/test_profile.py index 12c5e95cb5..8df58b4a63 100644 --- a/tests/rest/client/v1/test_profile.py +++ b/tests/rest/client/v1/test_profile.py @@ -237,6 +237,7 @@ class ProfilesRestrictedTestCase(unittest.HomeserverTestCase): config = self.default_config() config["require_auth_for_profile_requests"] = True + config["limit_profile_requests_to_users_who_share_rooms"] = True self.hs = self.setup_test_homeserver(config=config) return self.hs @@ -309,6 +310,7 @@ class OwnProfileUnrestrictedTestCase(unittest.HomeserverTestCase): def make_homeserver(self, reactor, clock): config = self.default_config() config["require_auth_for_profile_requests"] = True + config["limit_profile_requests_to_users_who_share_rooms"] = True self.hs = self.setup_test_homeserver(config=config) return self.hs From 3fbe5b7ec3abd2864d8a64893fa494e9651c430a Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 16 Dec 2019 16:59:32 +0000 Subject: [PATCH 0714/1623] Add auth events as per spec. (#6556) Previously we tried to be clever and filter out some unnecessary event IDs to keep the auth chain small, but that had some annoying interactions with state res v2 so we stop doing that for now. --- changelog.d/6556.bugfix | 1 + synapse/api/auth.py | 103 ++++++++++++++-------------------------- 2 files changed, 36 insertions(+), 68 deletions(-) create mode 100644 changelog.d/6556.bugfix diff --git a/changelog.d/6556.bugfix b/changelog.d/6556.bugfix new file mode 100644 index 0000000000..e75639f5b4 --- /dev/null +++ b/changelog.d/6556.bugfix @@ -0,0 +1 @@ +Fix a cause of state resets in room versions 2 onwards. diff --git a/synapse/api/auth.py b/synapse/api/auth.py index 5d0b7d2801..9fd52a8c77 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -14,6 +14,7 @@ # limitations under the License. import logging +from typing import Dict, Tuple from six import itervalues @@ -25,13 +26,7 @@ from twisted.internet import defer import synapse.logging.opentracing as opentracing import synapse.types from synapse import event_auth -from synapse.api.constants import ( - EventTypes, - JoinRules, - LimitBlockingTypes, - Membership, - UserTypes, -) +from synapse.api.constants import EventTypes, LimitBlockingTypes, Membership, UserTypes from synapse.api.errors import ( AuthError, Codes, @@ -513,71 +508,43 @@ class Auth(object): """ return self.store.is_server_admin(user) - @defer.inlineCallbacks - def compute_auth_events(self, event, current_state_ids, for_verification=False): + def compute_auth_events( + self, + event, + current_state_ids: Dict[Tuple[str, str], str], + for_verification: bool = False, + ): + """Given an event and current state return the list of event IDs used + to auth an event. + + If `for_verification` is False then only return auth events that + should be added to the event's `auth_events`. + + Returns: + defer.Deferred(list[str]): List of event IDs. + """ + if event.type == EventTypes.Create: - return [] + return defer.succeed([]) + + # Currently we ignore the `for_verification` flag even though there are + # some situations where we can drop particular auth events when adding + # to the event's `auth_events` (e.g. joins pointing to previous joins + # when room is publically joinable). Dropping event IDs has the + # advantage that the auth chain for the room grows slower, but we use + # the auth chain in state resolution v2 to order events, which means + # care must be taken if dropping events to ensure that it doesn't + # introduce undesirable "state reset" behaviour. + # + # All of which sounds a bit tricky so we don't bother for now. auth_ids = [] + for etype, state_key in event_auth.auth_types_for_event(event): + auth_ev_id = current_state_ids.get((etype, state_key)) + if auth_ev_id: + auth_ids.append(auth_ev_id) - key = (EventTypes.PowerLevels, "") - power_level_event_id = current_state_ids.get(key) - - if power_level_event_id: - auth_ids.append(power_level_event_id) - - key = (EventTypes.JoinRules, "") - join_rule_event_id = current_state_ids.get(key) - - key = (EventTypes.Member, event.sender) - member_event_id = current_state_ids.get(key) - - key = (EventTypes.Create, "") - create_event_id = current_state_ids.get(key) - if create_event_id: - auth_ids.append(create_event_id) - - if join_rule_event_id: - join_rule_event = yield self.store.get_event(join_rule_event_id) - join_rule = join_rule_event.content.get("join_rule") - is_public = join_rule == JoinRules.PUBLIC if join_rule else False - else: - is_public = False - - if event.type == EventTypes.Member: - e_type = event.content["membership"] - if e_type in [Membership.JOIN, Membership.INVITE]: - if join_rule_event_id: - auth_ids.append(join_rule_event_id) - - if e_type == Membership.JOIN: - if member_event_id and not is_public: - auth_ids.append(member_event_id) - else: - if member_event_id: - auth_ids.append(member_event_id) - - if for_verification: - key = (EventTypes.Member, event.state_key) - existing_event_id = current_state_ids.get(key) - if existing_event_id: - auth_ids.append(existing_event_id) - - if e_type == Membership.INVITE: - if "third_party_invite" in event.content: - key = ( - EventTypes.ThirdPartyInvite, - event.content["third_party_invite"]["signed"]["token"], - ) - third_party_invite_id = current_state_ids.get(key) - if third_party_invite_id: - auth_ids.append(third_party_invite_id) - elif member_event_id: - member_event = yield self.store.get_event(member_event_id) - if member_event.content["membership"] == Membership.JOIN: - auth_ids.append(member_event.event_id) - - return auth_ids + return defer.succeed(auth_ids) @defer.inlineCallbacks def check_can_change_room_list(self, room_id, user): From 5ca2cfadc36359c1203ea38c2d1953ce0e7ced2f Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 16 Dec 2019 16:59:32 +0000 Subject: [PATCH 0715/1623] Add auth events as per spec. (#6556) Previously we tried to be clever and filter out some unnecessary event IDs to keep the auth chain small, but that had some annoying interactions with state res v2 so we stop doing that for now. --- changelog.d/6556.bugfix | 1 + synapse/api/auth.py | 103 ++++++++++++++-------------------------- 2 files changed, 36 insertions(+), 68 deletions(-) create mode 100644 changelog.d/6556.bugfix diff --git a/changelog.d/6556.bugfix b/changelog.d/6556.bugfix new file mode 100644 index 0000000000..e75639f5b4 --- /dev/null +++ b/changelog.d/6556.bugfix @@ -0,0 +1 @@ +Fix a cause of state resets in room versions 2 onwards. diff --git a/synapse/api/auth.py b/synapse/api/auth.py index 5d0b7d2801..9fd52a8c77 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -14,6 +14,7 @@ # limitations under the License. import logging +from typing import Dict, Tuple from six import itervalues @@ -25,13 +26,7 @@ from twisted.internet import defer import synapse.logging.opentracing as opentracing import synapse.types from synapse import event_auth -from synapse.api.constants import ( - EventTypes, - JoinRules, - LimitBlockingTypes, - Membership, - UserTypes, -) +from synapse.api.constants import EventTypes, LimitBlockingTypes, Membership, UserTypes from synapse.api.errors import ( AuthError, Codes, @@ -513,71 +508,43 @@ class Auth(object): """ return self.store.is_server_admin(user) - @defer.inlineCallbacks - def compute_auth_events(self, event, current_state_ids, for_verification=False): + def compute_auth_events( + self, + event, + current_state_ids: Dict[Tuple[str, str], str], + for_verification: bool = False, + ): + """Given an event and current state return the list of event IDs used + to auth an event. + + If `for_verification` is False then only return auth events that + should be added to the event's `auth_events`. + + Returns: + defer.Deferred(list[str]): List of event IDs. + """ + if event.type == EventTypes.Create: - return [] + return defer.succeed([]) + + # Currently we ignore the `for_verification` flag even though there are + # some situations where we can drop particular auth events when adding + # to the event's `auth_events` (e.g. joins pointing to previous joins + # when room is publically joinable). Dropping event IDs has the + # advantage that the auth chain for the room grows slower, but we use + # the auth chain in state resolution v2 to order events, which means + # care must be taken if dropping events to ensure that it doesn't + # introduce undesirable "state reset" behaviour. + # + # All of which sounds a bit tricky so we don't bother for now. auth_ids = [] + for etype, state_key in event_auth.auth_types_for_event(event): + auth_ev_id = current_state_ids.get((etype, state_key)) + if auth_ev_id: + auth_ids.append(auth_ev_id) - key = (EventTypes.PowerLevels, "") - power_level_event_id = current_state_ids.get(key) - - if power_level_event_id: - auth_ids.append(power_level_event_id) - - key = (EventTypes.JoinRules, "") - join_rule_event_id = current_state_ids.get(key) - - key = (EventTypes.Member, event.sender) - member_event_id = current_state_ids.get(key) - - key = (EventTypes.Create, "") - create_event_id = current_state_ids.get(key) - if create_event_id: - auth_ids.append(create_event_id) - - if join_rule_event_id: - join_rule_event = yield self.store.get_event(join_rule_event_id) - join_rule = join_rule_event.content.get("join_rule") - is_public = join_rule == JoinRules.PUBLIC if join_rule else False - else: - is_public = False - - if event.type == EventTypes.Member: - e_type = event.content["membership"] - if e_type in [Membership.JOIN, Membership.INVITE]: - if join_rule_event_id: - auth_ids.append(join_rule_event_id) - - if e_type == Membership.JOIN: - if member_event_id and not is_public: - auth_ids.append(member_event_id) - else: - if member_event_id: - auth_ids.append(member_event_id) - - if for_verification: - key = (EventTypes.Member, event.state_key) - existing_event_id = current_state_ids.get(key) - if existing_event_id: - auth_ids.append(existing_event_id) - - if e_type == Membership.INVITE: - if "third_party_invite" in event.content: - key = ( - EventTypes.ThirdPartyInvite, - event.content["third_party_invite"]["signed"]["token"], - ) - third_party_invite_id = current_state_ids.get(key) - if third_party_invite_id: - auth_ids.append(third_party_invite_id) - elif member_event_id: - member_event = yield self.store.get_event(member_event_id) - if member_event.content["membership"] == Membership.JOIN: - auth_ids.append(member_event.event_id) - - return auth_ids + return defer.succeed(auth_ids) @defer.inlineCallbacks def check_can_change_room_list(self, room_id, user): From 02553901ce94461c6f140efc804443069b97f401 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 17 Dec 2019 11:44:32 +0000 Subject: [PATCH 0716/1623] Remove unused `get_pagination_rows` methods. (#6557) Remove unused get_pagination_rows methods --- changelog.d/6557.misc | 1 + synapse/handlers/account_data.py | 3 --- synapse/handlers/room.py | 12 ------------ synapse/handlers/typing.py | 3 --- 4 files changed, 1 insertion(+), 18 deletions(-) create mode 100644 changelog.d/6557.misc diff --git a/changelog.d/6557.misc b/changelog.d/6557.misc new file mode 100644 index 0000000000..80e7eaedb8 --- /dev/null +++ b/changelog.d/6557.misc @@ -0,0 +1 @@ +Remove unused `get_pagination_rows` methods from `EventSource` classes. diff --git a/synapse/handlers/account_data.py b/synapse/handlers/account_data.py index 20ec1ca01b..a8d3fbc6de 100644 --- a/synapse/handlers/account_data.py +++ b/synapse/handlers/account_data.py @@ -50,6 +50,3 @@ class AccountDataEventSource(object): ) return results, current_stream_id - - async def get_pagination_rows(self, user, config, key): - return [], config.to_id diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 22768e97ff..2d7925547d 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -1008,15 +1008,3 @@ class RoomEventSource(object): def get_current_key_for_room(self, room_id): return self.store.get_room_events_max_id(room_id) - - @defer.inlineCallbacks - def get_pagination_rows(self, user, config, key): - events, next_key = yield self.store.paginate_room_events( - room_id=key, - from_key=config.from_key, - to_key=config.to_key, - direction=config.direction, - limit=config.limit, - ) - - return (events, next_key) diff --git a/synapse/handlers/typing.py b/synapse/handlers/typing.py index 6f78454322..b635c339ed 100644 --- a/synapse/handlers/typing.py +++ b/synapse/handlers/typing.py @@ -317,6 +317,3 @@ class TypingNotificationEventSource(object): def get_current_key(self): return self.get_typing_handler()._latest_room_serial - - def get_pagination_rows(self, user, pagination_config, key): - return [], pagination_config.from_key From 50294225300602ec91712963736a523738195a01 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 17 Dec 2019 15:06:08 +0000 Subject: [PATCH 0717/1623] Fix bug where we added duplicate event IDs as auth_events (#6560) --- changelog.d/6560.bugfix | 1 + synapse/event_auth.py | 15 ++++++++------- 2 files changed, 9 insertions(+), 7 deletions(-) create mode 100644 changelog.d/6560.bugfix diff --git a/changelog.d/6560.bugfix b/changelog.d/6560.bugfix new file mode 100644 index 0000000000..e75639f5b4 --- /dev/null +++ b/changelog.d/6560.bugfix @@ -0,0 +1 @@ +Fix a cause of state resets in room versions 2 onwards. diff --git a/synapse/event_auth.py b/synapse/event_auth.py index d184b0273b..350ed9351f 100644 --- a/synapse/event_auth.py +++ b/synapse/event_auth.py @@ -14,6 +14,7 @@ # limitations under the License. import logging +from typing import Set, Tuple from canonicaljson import encode_canonical_json from signedjson.key import decode_verify_key_bytes @@ -637,7 +638,7 @@ def get_public_keys(invite_event): return public_keys -def auth_types_for_event(event): +def auth_types_for_event(event) -> Set[Tuple[str]]: """Given an event, return a list of (EventType, StateKey) that may be needed to auth the event. The returned list may be a superset of what would actually be required depending on the full state of the room. @@ -646,20 +647,20 @@ def auth_types_for_event(event): actually auth the event. """ if event.type == EventTypes.Create: - return [] + return set() - auth_types = [ + auth_types = { (EventTypes.PowerLevels, ""), (EventTypes.Member, event.sender), (EventTypes.Create, ""), - ] + } if event.type == EventTypes.Member: membership = event.content["membership"] if membership in [Membership.JOIN, Membership.INVITE]: - auth_types.append((EventTypes.JoinRules, "")) + auth_types.add((EventTypes.JoinRules, "")) - auth_types.append((EventTypes.Member, event.state_key)) + auth_types.add((EventTypes.Member, event.state_key)) if membership == Membership.INVITE: if "third_party_invite" in event.content: @@ -667,6 +668,6 @@ def auth_types_for_event(event): EventTypes.ThirdPartyInvite, event.content["third_party_invite"]["signed"]["token"], ) - auth_types.append(key) + auth_types.add(key) return auth_types From d656e91fc28b0f502f581fedd1dc3293b14e2bbf Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 18 Dec 2019 09:38:08 +0000 Subject: [PATCH 0718/1623] 1.7.1 --- CHANGES.md | 19 ++++++++++++++++++- changelog.d/6501.misc | 1 - changelog.d/6503.misc | 1 - changelog.d/6521.misc | 1 - changelog.d/6524.misc | 2 -- changelog.d/6526.bugfix | 1 - changelog.d/6527.bugfix | 1 - changelog.d/6530.misc | 2 -- changelog.d/6531.misc | 1 - changelog.d/6553.bugfix | 1 - changelog.d/6556.bugfix | 1 - changelog.d/6560.bugfix | 1 - debian/changelog | 6 ++++++ synapse/__init__.py | 2 +- 14 files changed, 25 insertions(+), 15 deletions(-) delete mode 100644 changelog.d/6501.misc delete mode 100644 changelog.d/6503.misc delete mode 100644 changelog.d/6521.misc delete mode 100644 changelog.d/6524.misc delete mode 100644 changelog.d/6526.bugfix delete mode 100644 changelog.d/6527.bugfix delete mode 100644 changelog.d/6530.misc delete mode 100644 changelog.d/6531.misc delete mode 100644 changelog.d/6553.bugfix delete mode 100644 changelog.d/6556.bugfix delete mode 100644 changelog.d/6560.bugfix diff --git a/CHANGES.md b/CHANGES.md index c8aa5d177f..f838a16795 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,20 @@ +Synapse 1.7.1 (2019-12-18) +========================== + +This release includes several security fixes as well as a fix to a bug exposed by the security fixes. Administrators are encouraged to upgrade as soon as possible. + +Security updates +---------------- + +- Fix a bug which could cause room events to be incorrectly authorized using events from a different room. ([\#6501](https://github.com/matrix-org/synapse/issues/6501)), ([\#6503](https://github.com/matrix-org/synapse/issues/6503)), ([\#6521](https://github.com/matrix-org/synapse/issues/6521)), ([\#6524](https://github.com/matrix-org/synapse/issues/6524), [\#6530](https://github.com/matrix-org/synapse/issues/6530), [\#6531](https://github.com/matrix-org/synapse/issues/6531)) +- Fix a bug causing responses to the `/context` client endpoint to not use the pruned version of the event. ([\#6553](https://github.com/matrix-org/synapse/issues/6553)) +- Fix a cause of state resets in room versions 2 onwards. ([\#6556](https://github.com/matrix-org/synapse/issues/6556), [\#6560](https://github.com/matrix-org/synapse/issues/6560)) + +Bugfixes +-------- + +- Fix a bug which could cause the federation server to incorrectly return errors when handling certain obscure event graphs. ([\#6526](https://github.com/matrix-org/synapse/issues/6526), [\#6527](https://github.com/matrix-org/synapse/issues/6527)) + Synapse 1.7.0 (2019-12-13) ========================== @@ -88,7 +105,7 @@ Internal Changes - Add a test scenario to make sure room history purges don't break `/messages` in the future. ([\#6392](https://github.com/matrix-org/synapse/issues/6392)) - Clarifications for the email configuration settings. ([\#6423](https://github.com/matrix-org/synapse/issues/6423)) - Add more tests to the blacklist when running in worker mode. ([\#6429](https://github.com/matrix-org/synapse/issues/6429)) -- Refactor data store layer to support multiple databases in the future. ([\#6454](https://github.com/matrix-org/synapse/issues/6454), [\#6464](https://github.com/matrix-org/synapse/issues/6464), [\#6469](https://github.com/matrix-org/synapse/issues/6469), [\#6487](https://github.com/matrix-org/synapse/issues/6487)) +- Refactor data store layer to support multiple databases in the future. ([\#6454](https://github.com/matrix-org/synapse/issues/6454), [\#6464](https://github.com/matrix-org/synapse/issues/6464), [\#6469](https://github.com/matrix-org/synapse/issues/6469), [\#6487](https://github.com/matrix-org/synapse/issues/6487)) - Port synapse.rest.client.v1 to async/await. ([\#6482](https://github.com/matrix-org/synapse/issues/6482)) - Port synapse.rest.client.v2_alpha to async/await. ([\#6483](https://github.com/matrix-org/synapse/issues/6483)) - Port SyncHandler to async/await. ([\#6484](https://github.com/matrix-org/synapse/issues/6484)) diff --git a/changelog.d/6501.misc b/changelog.d/6501.misc deleted file mode 100644 index 255f45a9c3..0000000000 --- a/changelog.d/6501.misc +++ /dev/null @@ -1 +0,0 @@ -Refactor get_events_from_store_or_dest to return a dict. diff --git a/changelog.d/6503.misc b/changelog.d/6503.misc deleted file mode 100644 index e4e9a5a3d4..0000000000 --- a/changelog.d/6503.misc +++ /dev/null @@ -1 +0,0 @@ -Move get_state methods into FederationHandler. diff --git a/changelog.d/6521.misc b/changelog.d/6521.misc deleted file mode 100644 index d9a44389b9..0000000000 --- a/changelog.d/6521.misc +++ /dev/null @@ -1 +0,0 @@ -Refactor some code in the event authentication path for clarity. diff --git a/changelog.d/6524.misc b/changelog.d/6524.misc deleted file mode 100644 index f885597426..0000000000 --- a/changelog.d/6524.misc +++ /dev/null @@ -1,2 +0,0 @@ -Improve sanity-checking when receiving events over federation. - diff --git a/changelog.d/6526.bugfix b/changelog.d/6526.bugfix deleted file mode 100644 index 53214b0748..0000000000 --- a/changelog.d/6526.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a bug which could cause the federation server to incorrectly return errors when handling certain obscure event graphs. \ No newline at end of file diff --git a/changelog.d/6527.bugfix b/changelog.d/6527.bugfix deleted file mode 100644 index 53214b0748..0000000000 --- a/changelog.d/6527.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a bug which could cause the federation server to incorrectly return errors when handling certain obscure event graphs. \ No newline at end of file diff --git a/changelog.d/6530.misc b/changelog.d/6530.misc deleted file mode 100644 index f885597426..0000000000 --- a/changelog.d/6530.misc +++ /dev/null @@ -1,2 +0,0 @@ -Improve sanity-checking when receiving events over federation. - diff --git a/changelog.d/6531.misc b/changelog.d/6531.misc deleted file mode 100644 index 598efb79fc..0000000000 --- a/changelog.d/6531.misc +++ /dev/null @@ -1 +0,0 @@ -Improve sanity-checking when receiving events over federation. diff --git a/changelog.d/6553.bugfix b/changelog.d/6553.bugfix deleted file mode 100644 index 4fe576b873..0000000000 --- a/changelog.d/6553.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a bug causing responses to the `/context` client endpoint to not use the pruned version of the event. diff --git a/changelog.d/6556.bugfix b/changelog.d/6556.bugfix deleted file mode 100644 index e75639f5b4..0000000000 --- a/changelog.d/6556.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a cause of state resets in room versions 2 onwards. diff --git a/changelog.d/6560.bugfix b/changelog.d/6560.bugfix deleted file mode 100644 index e75639f5b4..0000000000 --- a/changelog.d/6560.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a cause of state resets in room versions 2 onwards. diff --git a/debian/changelog b/debian/changelog index bd43feb321..e400619eb9 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.7.1) stable; urgency=medium + + * New synapse release 1.7.1. + + -- Synapse Packaging team Wed, 18 Dec 2019 09:37:59 +0000 + matrix-synapse-py3 (1.7.0) stable; urgency=medium * New synapse release 1.7.0. diff --git a/synapse/__init__.py b/synapse/__init__.py index d3cf7b3d7b..e951bab593 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -36,7 +36,7 @@ try: except ImportError: pass -__version__ = "1.7.0" +__version__ = "1.7.1" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From e156c86a7f3d616bcb8fd80d5d319fd9a3d73cb6 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 18 Dec 2019 09:40:03 +0000 Subject: [PATCH 0719/1623] too many parens --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index f838a16795..7927714a36 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,7 +6,7 @@ This release includes several security fixes as well as a fix to a bug exposed b Security updates ---------------- -- Fix a bug which could cause room events to be incorrectly authorized using events from a different room. ([\#6501](https://github.com/matrix-org/synapse/issues/6501)), ([\#6503](https://github.com/matrix-org/synapse/issues/6503)), ([\#6521](https://github.com/matrix-org/synapse/issues/6521)), ([\#6524](https://github.com/matrix-org/synapse/issues/6524), [\#6530](https://github.com/matrix-org/synapse/issues/6530), [\#6531](https://github.com/matrix-org/synapse/issues/6531)) +- Fix a bug which could cause room events to be incorrectly authorized using events from a different room. ([\#6501](https://github.com/matrix-org/synapse/issues/6501), [\#6503](https://github.com/matrix-org/synapse/issues/6503), [\#6521](https://github.com/matrix-org/synapse/issues/6521), [\#6524](https://github.com/matrix-org/synapse/issues/6524), [\#6530](https://github.com/matrix-org/synapse/issues/6530), [\#6531](https://github.com/matrix-org/synapse/issues/6531)) - Fix a bug causing responses to the `/context` client endpoint to not use the pruned version of the event. ([\#6553](https://github.com/matrix-org/synapse/issues/6553)) - Fix a cause of state resets in room versions 2 onwards. ([\#6556](https://github.com/matrix-org/synapse/issues/6556), [\#6560](https://github.com/matrix-org/synapse/issues/6560)) From 2284eb3a533a2df04784df08da28e67d6588a5ea Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 18 Dec 2019 10:45:12 +0000 Subject: [PATCH 0720/1623] Add database config class (#6513) This encapsulates config for a given database and is the way to get new connections. --- changelog.d/6513.misc | 1 + scripts-dev/update_database | 9 +-- scripts/synapse_port_db | 58 ++++++-------- synapse/config/database.py | 78 +++++++++++++++---- synapse/handlers/presence.py | 2 +- synapse/server.py | 41 +--------- synapse/storage/_base.py | 2 +- synapse/storage/data_stores/__init__.py | 40 ++++++++-- .../storage/data_stores/main/client_ips.py | 2 +- synapse/storage/database.py | 45 ++++++++++- synapse/storage/engines/sqlite.py | 16 +++- synapse/storage/prepare_database.py | 7 +- tests/handlers/test_typing.py | 41 +++++----- tests/replication/slave/storage/_base.py | 6 +- tests/server.py | 51 ++++++------ tests/storage/test_appservice.py | 37 ++++++--- tests/storage/test_base.py | 14 ++-- tests/storage/test_registration.py | 1 - tests/utils.py | 43 ++++------ 19 files changed, 286 insertions(+), 208 deletions(-) create mode 100644 changelog.d/6513.misc diff --git a/changelog.d/6513.misc b/changelog.d/6513.misc new file mode 100644 index 0000000000..36700f5657 --- /dev/null +++ b/changelog.d/6513.misc @@ -0,0 +1 @@ +Remove all assumptions of there being a single phyiscal DB apart from the `synapse.config`. diff --git a/scripts-dev/update_database b/scripts-dev/update_database index 23017c21f8..1d62f0403a 100755 --- a/scripts-dev/update_database +++ b/scripts-dev/update_database @@ -26,7 +26,6 @@ from synapse.config.homeserver import HomeServerConfig from synapse.metrics.background_process_metrics import run_as_background_process from synapse.server import HomeServer from synapse.storage import DataStore -from synapse.storage.prepare_database import prepare_database logger = logging.getLogger("update_database") @@ -77,12 +76,8 @@ if __name__ == "__main__": # Instantiate and initialise the homeserver object. hs = MockHomeserver(config) - db_conn = hs.get_db_conn() - # Update the database to the latest schema. - prepare_database(db_conn, hs.database_engine, config=config) - db_conn.commit() - - # setup instantiates the store within the homeserver object. + # Setup instantiates the store within the homeserver object and updates the + # DB. hs.setup() store = hs.get_datastore() diff --git a/scripts/synapse_port_db b/scripts/synapse_port_db index e393a9b2f7..5b5368988c 100755 --- a/scripts/synapse_port_db +++ b/scripts/synapse_port_db @@ -30,6 +30,7 @@ import yaml from twisted.enterprise import adbapi from twisted.internet import defer, reactor +from synapse.config.database import DatabaseConnectionConfig from synapse.config.homeserver import HomeServerConfig from synapse.logging.context import PreserveLoggingContext from synapse.storage._base import LoggingTransaction @@ -55,7 +56,7 @@ from synapse.storage.data_stores.main.stats import StatsStore from synapse.storage.data_stores.main.user_directory import ( UserDirectoryBackgroundUpdateStore, ) -from synapse.storage.database import Database +from synapse.storage.database import Database, make_conn from synapse.storage.engines import create_engine from synapse.storage.prepare_database import prepare_database from synapse.util import Clock @@ -165,23 +166,17 @@ class Store( class MockHomeserver: - def __init__(self, config, database_engine, db_conn, db_pool): - self.database_engine = database_engine - self.db_conn = db_conn - self.db_pool = db_pool + def __init__(self, config): self.clock = Clock(reactor) self.config = config self.hostname = config.server_name - def get_db_conn(self): - return self.db_conn - - def get_db_pool(self): - return self.db_pool - def get_clock(self): return self.clock + def get_reactor(self): + return reactor + class Porter(object): def __init__(self, **kwargs): @@ -445,45 +440,36 @@ class Porter(object): else: return - def setup_db(self, db_config, database_engine): - db_conn = database_engine.module.connect( - **{ - k: v - for k, v in db_config.get("args", {}).items() - if not k.startswith("cp_") - } - ) - - prepare_database(db_conn, database_engine, config=None) + def setup_db(self, db_config: DatabaseConnectionConfig, engine): + db_conn = make_conn(db_config, engine) + prepare_database(db_conn, engine, config=None) db_conn.commit() return db_conn @defer.inlineCallbacks - def build_db_store(self, config): + def build_db_store(self, db_config: DatabaseConnectionConfig): """Builds and returns a database store using the provided configuration. Args: - config: The database configuration, i.e. a dict following the structure of - the "database" section of Synapse's configuration file. + config: The database configuration Returns: The built Store object. """ - engine = create_engine(config) + self.progress.set_state("Preparing %s" % db_config.config["name"]) - self.progress.set_state("Preparing %s" % config["name"]) - conn = self.setup_db(config, engine) + engine = create_engine(db_config.config) + conn = self.setup_db(db_config, engine) - db_pool = adbapi.ConnectionPool(config["name"], **config["args"]) + hs = MockHomeserver(self.hs_config) - hs = MockHomeserver(self.hs_config, engine, conn, db_pool) - - store = Store(Database(hs), conn, hs) + store = Store(Database(hs, db_config, engine), conn, hs) yield store.db.runInteraction( - "%s_engine.check_database" % config["name"], engine.check_database, + "%s_engine.check_database" % db_config.config["name"], + engine.check_database, ) return store @@ -509,7 +495,11 @@ class Porter(object): @defer.inlineCallbacks def run(self): try: - self.sqlite_store = yield self.build_db_store(self.sqlite_config) + self.sqlite_store = yield self.build_db_store( + DatabaseConnectionConfig( + "master", self.sqlite_config, data_stores=["main"] + ) + ) # Check if all background updates are done, abort if not. updates_complete = ( @@ -524,7 +514,7 @@ class Porter(object): defer.returnValue(None) self.postgres_store = yield self.build_db_store( - self.hs_config.database_config + self.hs_config.get_single_database() ) yield self.run_background_updates_on_postgres() diff --git a/synapse/config/database.py b/synapse/config/database.py index 0e2509f0b1..5f2f3c7cfd 100644 --- a/synapse/config/database.py +++ b/synapse/config/database.py @@ -12,12 +12,43 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import logging import os from textwrap import indent +from typing import List import yaml -from ._base import Config +from synapse.config._base import Config, ConfigError + +logger = logging.getLogger(__name__) + + +class DatabaseConnectionConfig: + """Contains the connection config for a particular database. + + Args: + name: A label for the database, used for logging. + db_config: The config for a particular database, as per `database` + section of main config. Has two fields: `name` for database + module name, and `args` for the args to give to the database + connector. + data_stores: The list of data stores that should be provisioned on the + database. + """ + + def __init__(self, name: str, db_config: dict, data_stores: List[str]): + if db_config["name"] not in ("sqlite3", "psycopg2"): + raise ConfigError("Unsupported database type %r" % (db_config["name"],)) + + if db_config["name"] == "sqlite3": + db_config.setdefault("args", {}).update( + {"cp_min": 1, "cp_max": 1, "check_same_thread": False} + ) + + self.name = name + self.config = db_config + self.data_stores = data_stores class DatabaseConfig(Config): @@ -26,20 +57,14 @@ class DatabaseConfig(Config): def read_config(self, config, **kwargs): self.event_cache_size = self.parse_size(config.get("event_cache_size", "10K")) - self.database_config = config.get("database") + database_config = config.get("database") - if self.database_config is None: - self.database_config = {"name": "sqlite3", "args": {}} + if database_config is None: + database_config = {"name": "sqlite3", "args": {}} - name = self.database_config.get("name", None) - if name == "psycopg2": - pass - elif name == "sqlite3": - self.database_config.setdefault("args", {}).update( - {"cp_min": 1, "cp_max": 1, "check_same_thread": False} - ) - else: - raise RuntimeError("Unsupported database type '%s'" % (name,)) + self.databases = [ + DatabaseConnectionConfig("master", database_config, data_stores=["main"]) + ] self.set_databasepath(config.get("database_path")) @@ -76,11 +101,24 @@ class DatabaseConfig(Config): self.set_databasepath(args.database_path) def set_databasepath(self, database_path): + if database_path is None: + return + if database_path != ":memory:": database_path = self.abspath(database_path) - if self.database_config.get("name", None) == "sqlite3": - if database_path is not None: - self.database_config["args"]["database"] = database_path + + # We only support setting a database path if we have a single sqlite3 + # database. + if len(self.databases) != 1: + raise ConfigError("Cannot specify 'database_path' with multiple databases") + + database = self.get_single_database() + if database.config["name"] != "sqlite3": + # We don't raise here as we haven't done so before for this case. + logger.warn("Ignoring 'database_path' for non-sqlite3 database") + return + + database.config["args"]["database"] = database_path @staticmethod def add_arguments(parser): @@ -91,3 +129,11 @@ class DatabaseConfig(Config): metavar="SQLITE_DATABASE_PATH", help="The path to a sqlite database to use.", ) + + def get_single_database(self) -> DatabaseConnectionConfig: + """Returns the database if there is only one, useful for e.g. tests + """ + if len(self.databases) != 1: + raise Exception("More than one database exists") + + return self.databases[0] diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py index eda15bc623..240c4add12 100644 --- a/synapse/handlers/presence.py +++ b/synapse/handlers/presence.py @@ -230,7 +230,7 @@ class PresenceHandler(object): is some spurious presence changes that will self-correct. """ # If the DB pool has already terminated, don't try updating - if not self.hs.get_db_pool().running: + if not self.store.database.is_running(): return logger.info( diff --git a/synapse/server.py b/synapse/server.py index 5021068ce0..7926867b77 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -25,7 +25,6 @@ import abc import logging import os -from twisted.enterprise import adbapi from twisted.mail.smtp import sendmail from twisted.web.client import BrowserLikePolicyForHTTPS @@ -98,7 +97,6 @@ from synapse.server_notices.worker_server_notices_sender import ( ) from synapse.state import StateHandler, StateResolutionHandler from synapse.storage import DataStores, Storage -from synapse.storage.engines import create_engine from synapse.streams.events import EventSources from synapse.util import Clock from synapse.util.distributor import Distributor @@ -134,7 +132,6 @@ class HomeServer(object): DEPENDENCIES = [ "http_client", - "db_pool", "federation_client", "federation_server", "handlers", @@ -233,12 +230,6 @@ class HomeServer(object): self.admin_redaction_ratelimiter = Ratelimiter() self.registration_ratelimiter = Ratelimiter() - self.database_engine = create_engine(config.database_config) - config.database_config.setdefault("args", {})[ - "cp_openfun" - ] = self.database_engine.on_new_connection - self.db_config = config.database_config - self.datastores = None # Other kwargs are explicit dependencies @@ -247,10 +238,8 @@ class HomeServer(object): def setup(self): logger.info("Setting up.") - with self.get_db_conn() as conn: - self.datastores = DataStores(self.DATASTORE_CLASS, conn, self) - conn.commit() self.start_time = int(self.get_clock().time()) + self.datastores = DataStores(self.DATASTORE_CLASS, self) logger.info("Finished setting up.") def setup_master(self): @@ -284,6 +273,9 @@ class HomeServer(object): def get_datastore(self): return self.datastores.main + def get_datastores(self): + return self.datastores + def get_config(self): return self.config @@ -433,31 +425,6 @@ class HomeServer(object): ) return MatrixFederationHttpClient(self, tls_client_options_factory) - def build_db_pool(self): - name = self.db_config["name"] - - return adbapi.ConnectionPool( - name, cp_reactor=self.get_reactor(), **self.db_config.get("args", {}) - ) - - def get_db_conn(self, run_new_connection=True): - """Makes a new connection to the database, skipping the db pool - - Returns: - Connection: a connection object implementing the PEP-249 spec - """ - # Any param beginning with cp_ is a parameter for adbapi, and should - # not be passed to the database engine. - db_params = { - k: v - for k, v in self.db_config.get("args", {}).items() - if not k.startswith("cp_") - } - db_conn = self.database_engine.module.connect(**db_params) - if run_new_connection: - self.database_engine.on_new_connection(db_conn) - return db_conn - def build_media_repository_resource(self): # build the media repo resource. This indirects through the HomeServer # to ensure that we only have a single instance of diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py index b7637b5dc0..88546ad614 100644 --- a/synapse/storage/_base.py +++ b/synapse/storage/_base.py @@ -40,7 +40,7 @@ class SQLBaseStore(object): def __init__(self, database: Database, db_conn, hs): self.hs = hs self._clock = hs.get_clock() - self.database_engine = hs.database_engine + self.database_engine = database.engine self.db = database self.rand = random.SystemRandom() diff --git a/synapse/storage/data_stores/__init__.py b/synapse/storage/data_stores/__init__.py index cafedd5c0d..0983e059c0 100644 --- a/synapse/storage/data_stores/__init__.py +++ b/synapse/storage/data_stores/__init__.py @@ -13,24 +13,50 @@ # See the License for the specific language governing permissions and # limitations under the License. -from synapse.storage.database import Database +import logging + +from synapse.storage.database import Database, make_conn +from synapse.storage.engines import create_engine from synapse.storage.prepare_database import prepare_database +logger = logging.getLogger(__name__) + class DataStores(object): """The various data stores. These are low level interfaces to physical databases. + + Attributes: + main (DataStore) """ - def __init__(self, main_store_class, db_conn, hs): + def __init__(self, main_store_class, hs): # Note we pass in the main store class here as workers use a different main # store. - database = Database(hs) - # Check that db is correctly configured. - database.engine.check_database(db_conn.cursor()) + self.databases = [] - prepare_database(db_conn, database.engine, config=hs.config) + for database_config in hs.config.database.databases: + db_name = database_config.name + engine = create_engine(database_config.config) - self.main = main_store_class(database, db_conn, hs) + with make_conn(database_config, engine) as db_conn: + logger.info("Preparing database %r...", db_name) + + engine.check_database(db_conn.cursor()) + prepare_database( + db_conn, engine, hs.config, data_stores=database_config.data_stores, + ) + + database = Database(hs, database_config, engine) + + if "main" in database_config.data_stores: + logger.info("Starting 'main' data store") + self.main = main_store_class(database, db_conn, hs) + + db_conn.commit() + + self.databases.append(database) + + logger.info("Database %r prepared", db_name) diff --git a/synapse/storage/data_stores/main/client_ips.py b/synapse/storage/data_stores/main/client_ips.py index add3037b69..13f4c9c72e 100644 --- a/synapse/storage/data_stores/main/client_ips.py +++ b/synapse/storage/data_stores/main/client_ips.py @@ -412,7 +412,7 @@ class ClientIpStore(ClientIpBackgroundUpdateStore): def _update_client_ips_batch(self): # If the DB pool has already terminated, don't try updating - if not self.hs.get_db_pool().running: + if not self.db.is_running(): return to_update = self._batch_row_update diff --git a/synapse/storage/database.py b/synapse/storage/database.py index ec19ae1d9d..1003dd84a5 100644 --- a/synapse/storage/database.py +++ b/synapse/storage/database.py @@ -24,9 +24,11 @@ from six.moves import intern, range from prometheus_client import Histogram +from twisted.enterprise import adbapi from twisted.internet import defer from synapse.api.errors import StoreError +from synapse.config.database import DatabaseConnectionConfig from synapse.logging.context import LoggingContext, make_deferred_yieldable from synapse.metrics.background_process_metrics import run_as_background_process from synapse.storage.background_updates import BackgroundUpdater @@ -74,6 +76,37 @@ UNIQUE_INDEX_BACKGROUND_UPDATES = { } +def make_pool( + reactor, db_config: DatabaseConnectionConfig, engine +) -> adbapi.ConnectionPool: + """Get the connection pool for the database. + """ + + return adbapi.ConnectionPool( + db_config.config["name"], + cp_reactor=reactor, + cp_openfun=engine.on_new_connection, + **db_config.config.get("args", {}) + ) + + +def make_conn(db_config: DatabaseConnectionConfig, engine): + """Make a new connection to the database and return it. + + Returns: + Connection + """ + + db_params = { + k: v + for k, v in db_config.config.get("args", {}).items() + if not k.startswith("cp_") + } + db_conn = engine.module.connect(**db_params) + engine.on_new_connection(db_conn) + return db_conn + + class LoggingTransaction(object): """An object that almost-transparently proxies for the 'txn' object passed to the constructor. Adds logging and metrics to the .execute() @@ -218,10 +251,11 @@ class Database(object): _TXN_ID = 0 - def __init__(self, hs): + def __init__(self, hs, database_config: DatabaseConnectionConfig, engine): self.hs = hs self._clock = hs.get_clock() - self._db_pool = hs.get_db_pool() + self._database_config = database_config + self._db_pool = make_pool(hs.get_reactor(), database_config, engine) self.updates = BackgroundUpdater(hs, self) @@ -234,7 +268,7 @@ class Database(object): # to watch it self._txn_perf_counters = PerformanceCounters() - self.engine = hs.database_engine + self.engine = engine # A set of tables that are not safe to use native upserts in. self._unsafe_to_upsert_tables = set(UNIQUE_INDEX_BACKGROUND_UPDATES.keys()) @@ -255,6 +289,11 @@ class Database(object): self._check_safe_to_upsert, ) + def is_running(self): + """Is the database pool currently running + """ + return self._db_pool.running + @defer.inlineCallbacks def _check_safe_to_upsert(self): """ diff --git a/synapse/storage/engines/sqlite.py b/synapse/storage/engines/sqlite.py index ddad17dc5a..df039a072d 100644 --- a/synapse/storage/engines/sqlite.py +++ b/synapse/storage/engines/sqlite.py @@ -16,8 +16,6 @@ import struct import threading -from synapse.storage.prepare_database import prepare_database - class Sqlite3Engine(object): single_threaded = True @@ -25,6 +23,9 @@ class Sqlite3Engine(object): def __init__(self, database_module, database_config): self.module = database_module + database = database_config.get("args", {}).get("database") + self._is_in_memory = database in (None, ":memory:",) + # The current max state_group, or None if we haven't looked # in the DB yet. self._current_state_group_id = None @@ -59,7 +60,16 @@ class Sqlite3Engine(object): return sql def on_new_connection(self, db_conn): - prepare_database(db_conn, self, config=None) + + # We need to import here to avoid an import loop. + from synapse.storage.prepare_database import prepare_database + + if self._is_in_memory: + # In memory databases need to be rebuilt each time. Ideally we'd + # reuse the same connection as we do when starting up, but that + # would involve using adbapi before we have started the reactor. + prepare_database(db_conn, self, config=None) + db_conn.create_function("rank", 1, _rank) def is_deadlock(self, error): diff --git a/synapse/storage/prepare_database.py b/synapse/storage/prepare_database.py index 731e1c9d9c..b4194b44ee 100644 --- a/synapse/storage/prepare_database.py +++ b/synapse/storage/prepare_database.py @@ -41,7 +41,7 @@ class UpgradeDatabaseException(PrepareDatabaseException): pass -def prepare_database(db_conn, database_engine, config): +def prepare_database(db_conn, database_engine, config, data_stores=["main"]): """Prepares a database for usage. Will either create all necessary tables or upgrade from an older schema version. @@ -54,11 +54,10 @@ def prepare_database(db_conn, database_engine, config): config (synapse.config.homeserver.HomeServerConfig|None): application config, or None if we are connecting to an existing database which we expect to be configured already + data_stores (list[str]): The name of the data stores that will be used + with this database. Defaults to all data stores. """ - # For now we only have the one datastore. - data_stores = ["main"] - try: cur = db_conn.cursor() version_info = _get_or_create_schema_state(cur, database_engine) diff --git a/tests/handlers/test_typing.py b/tests/handlers/test_typing.py index 92b8726093..596ddc6970 100644 --- a/tests/handlers/test_typing.py +++ b/tests/handlers/test_typing.py @@ -64,28 +64,29 @@ class TypingNotificationsTestCase(unittest.HomeserverTestCase): mock_federation_client = Mock(spec=["put_json"]) mock_federation_client.put_json.return_value = defer.succeed((200, "OK")) - hs = self.setup_test_homeserver( - datastore=( - Mock( - spec=[ - # Bits that Federation needs - "prep_send_transaction", - "delivered_txn", - "get_received_txn_response", - "set_received_txn_response", - "get_destination_retry_timings", - "get_device_updates_by_remote", - # Bits that user_directory needs - "get_user_directory_stream_pos", - "get_current_state_deltas", - ] - ) - ), - notifier=Mock(), - http_client=mock_federation_client, - keyring=mock_keyring, + datastores = Mock() + datastores.main = Mock( + spec=[ + # Bits that Federation needs + "prep_send_transaction", + "delivered_txn", + "get_received_txn_response", + "set_received_txn_response", + "get_destination_retry_timings", + "get_devices_by_remote", + # Bits that user_directory needs + "get_user_directory_stream_pos", + "get_current_state_deltas", + "get_device_updates_by_remote", + ] ) + hs = self.setup_test_homeserver( + notifier=Mock(), http_client=mock_federation_client, keyring=mock_keyring + ) + + hs.datastores = datastores + return hs def prepare(self, reactor, clock, hs): diff --git a/tests/replication/slave/storage/_base.py b/tests/replication/slave/storage/_base.py index 3dae83c543..2a1e7c7166 100644 --- a/tests/replication/slave/storage/_base.py +++ b/tests/replication/slave/storage/_base.py @@ -20,7 +20,7 @@ from synapse.replication.tcp.client import ( ReplicationClientHandler, ) from synapse.replication.tcp.resource import ReplicationStreamProtocolFactory -from synapse.storage.database import Database +from synapse.storage.database import make_conn from tests import unittest from tests.server import FakeTransport @@ -41,10 +41,12 @@ class BaseSlavedStoreTestCase(unittest.HomeserverTestCase): def prepare(self, reactor, clock, hs): + db_config = hs.config.database.get_single_database() self.master_store = self.hs.get_datastore() self.storage = hs.get_storage() + database = hs.get_datastores().databases[0] self.slaved_store = self.STORE_TYPE( - Database(hs), self.hs.get_db_conn(), self.hs + database, make_conn(db_config, database.engine), self.hs ) self.event_id = 0 diff --git a/tests/server.py b/tests/server.py index 2b7cf4242e..a554dfdd57 100644 --- a/tests/server.py +++ b/tests/server.py @@ -302,41 +302,42 @@ def setup_test_homeserver(cleanup_func, *args, **kwargs): Set up a synchronous test server, driven by the reactor used by the homeserver. """ - d = _sth(cleanup_func, *args, **kwargs).result + server = _sth(cleanup_func, *args, **kwargs) - if isinstance(d, Failure): - d.raiseException() + database = server.config.database.get_single_database() # Make the thread pool synchronous. - clock = d.get_clock() - pool = d.get_db_pool() + clock = server.get_clock() - def runWithConnection(func, *args, **kwargs): - return threads.deferToThreadPool( - pool._reactor, - pool.threadpool, - pool._runWithConnection, - func, - *args, - **kwargs - ) + for database in server.get_datastores().databases: + pool = database._db_pool - def runInteraction(interaction, *args, **kwargs): - return threads.deferToThreadPool( - pool._reactor, - pool.threadpool, - pool._runInteraction, - interaction, - *args, - **kwargs - ) + def runWithConnection(func, *args, **kwargs): + return threads.deferToThreadPool( + pool._reactor, + pool.threadpool, + pool._runWithConnection, + func, + *args, + **kwargs + ) + + def runInteraction(interaction, *args, **kwargs): + return threads.deferToThreadPool( + pool._reactor, + pool.threadpool, + pool._runInteraction, + interaction, + *args, + **kwargs + ) - if pool: pool.runWithConnection = runWithConnection pool.runInteraction = runInteraction pool.threadpool = ThreadPool(clock._reactor) pool.running = True - return d + + return server def get_clock(): diff --git a/tests/storage/test_appservice.py b/tests/storage/test_appservice.py index 2e521e9ab7..fd52512696 100644 --- a/tests/storage/test_appservice.py +++ b/tests/storage/test_appservice.py @@ -28,7 +28,7 @@ from synapse.storage.data_stores.main.appservice import ( ApplicationServiceStore, ApplicationServiceTransactionStore, ) -from synapse.storage.database import Database +from synapse.storage.database import Database, make_conn from tests import unittest from tests.utils import setup_test_homeserver @@ -55,8 +55,10 @@ class ApplicationServiceStoreTestCase(unittest.TestCase): self._add_appservice("token2", "as2", "some_url", "some_hs_token", "bob") self._add_appservice("token3", "as3", "some_url", "some_hs_token", "bob") # must be done after inserts - database = Database(hs) - self.store = ApplicationServiceStore(database, hs.get_db_conn(), hs) + database = hs.get_datastores().databases[0] + self.store = ApplicationServiceStore( + database, make_conn(database._database_config, database.engine), hs + ) def tearDown(self): # TODO: suboptimal that we need to create files for tests! @@ -111,9 +113,6 @@ class ApplicationServiceTransactionStoreTestCase(unittest.TestCase): hs.config.event_cache_size = 1 hs.config.password_providers = [] - self.db_pool = hs.get_db_pool() - self.engine = hs.database_engine - self.as_list = [ {"token": "token1", "url": "https://matrix-as.org", "id": "id_1"}, {"token": "alpha_tok", "url": "https://alpha.com", "id": "id_alpha"}, @@ -125,8 +124,15 @@ class ApplicationServiceTransactionStoreTestCase(unittest.TestCase): self.as_yaml_files = [] - database = Database(hs) - self.store = TestTransactionStore(database, hs.get_db_conn(), hs) + # We assume there is only one database in these tests + database = hs.get_datastores().databases[0] + self.db_pool = database._db_pool + self.engine = database.engine + + db_config = hs.config.get_single_database() + self.store = TestTransactionStore( + database, make_conn(db_config, self.engine), hs + ) def _add_service(self, url, as_token, id): as_yaml = dict( @@ -419,7 +425,10 @@ class ApplicationServiceStoreConfigTestCase(unittest.TestCase): hs.config.event_cache_size = 1 hs.config.password_providers = [] - ApplicationServiceStore(Database(hs), hs.get_db_conn(), hs) + database = hs.get_datastores().databases[0] + ApplicationServiceStore( + database, make_conn(database._database_config, database.engine), hs + ) @defer.inlineCallbacks def test_duplicate_ids(self): @@ -435,7 +444,10 @@ class ApplicationServiceStoreConfigTestCase(unittest.TestCase): hs.config.password_providers = [] with self.assertRaises(ConfigError) as cm: - ApplicationServiceStore(Database(hs), hs.get_db_conn(), hs) + database = hs.get_datastores().databases[0] + ApplicationServiceStore( + database, make_conn(database._database_config, database.engine), hs + ) e = cm.exception self.assertIn(f1, str(e)) @@ -456,7 +468,10 @@ class ApplicationServiceStoreConfigTestCase(unittest.TestCase): hs.config.password_providers = [] with self.assertRaises(ConfigError) as cm: - ApplicationServiceStore(Database(hs), hs.get_db_conn(), hs) + database = hs.get_datastores().databases[0] + ApplicationServiceStore( + database, make_conn(database._database_config, database.engine), hs + ) e = cm.exception self.assertIn(f1, str(e)) diff --git a/tests/storage/test_base.py b/tests/storage/test_base.py index 537cfe9f64..cdee0a9e60 100644 --- a/tests/storage/test_base.py +++ b/tests/storage/test_base.py @@ -52,15 +52,17 @@ class SQLBaseStoreTestCase(unittest.TestCase): config = Mock() config._disable_native_upserts = True config.event_cache_size = 1 - config.database_config = {"name": "sqlite3"} - engine = create_engine(config.database_config) + hs = TestHomeServer("test", config=config) + + sqlite_config = {"name": "sqlite3"} + engine = create_engine(sqlite_config) fake_engine = Mock(wraps=engine) fake_engine.can_native_upsert = False - hs = TestHomeServer( - "test", db_pool=self.db_pool, config=config, database_engine=fake_engine - ) - self.datastore = SQLBaseStore(Database(hs), None, hs) + db = Database(Mock(), Mock(config=sqlite_config), fake_engine) + db._db_pool = self.db_pool + + self.datastore = SQLBaseStore(db, None, hs) @defer.inlineCallbacks def test_insert_1col(self): diff --git a/tests/storage/test_registration.py b/tests/storage/test_registration.py index 4578cc3b60..ed5786865a 100644 --- a/tests/storage/test_registration.py +++ b/tests/storage/test_registration.py @@ -26,7 +26,6 @@ class RegistrationStoreTestCase(unittest.TestCase): @defer.inlineCallbacks def setUp(self): hs = yield setup_test_homeserver(self.addCleanup) - self.db_pool = hs.get_db_pool() self.store = hs.get_datastore() diff --git a/tests/utils.py b/tests/utils.py index 585f305b9a..9f5bf40b4b 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -30,6 +30,7 @@ from twisted.internet import defer, reactor from synapse.api.constants import EventTypes from synapse.api.errors import CodeMessageException, cs_error from synapse.api.room_versions import RoomVersions +from synapse.config.database import DatabaseConnectionConfig from synapse.config.homeserver import HomeServerConfig from synapse.config.server import DEFAULT_ROOM_VERSION from synapse.federation.transport import server as federation_server @@ -177,7 +178,6 @@ class TestHomeServer(HomeServer): DATASTORE_CLASS = DataStore -@defer.inlineCallbacks def setup_test_homeserver( cleanup_func, name="test", @@ -214,7 +214,7 @@ def setup_test_homeserver( if USE_POSTGRES_FOR_TESTS: test_db = "synapse_test_%s" % uuid.uuid4().hex - config.database_config = { + database_config = { "name": "psycopg2", "args": { "database": test_db, @@ -226,12 +226,15 @@ def setup_test_homeserver( }, } else: - config.database_config = { + database_config = { "name": "sqlite3", "args": {"database": ":memory:", "cp_min": 1, "cp_max": 1}, } - db_engine = create_engine(config.database_config) + database = DatabaseConnectionConfig("master", database_config, ["main"]) + config.database.databases = [database] + + db_engine = create_engine(database.config) # Create the database before we actually try and connect to it, based off # the template database we generate in setupdb() @@ -251,11 +254,6 @@ def setup_test_homeserver( cur.close() db_conn.close() - # we need to configure the connection pool to run the on_new_connection - # function, so that we can test code that uses custom sqlite functions - # (like rank). - config.database_config["args"]["cp_openfun"] = db_engine.on_new_connection - if datastore is None: hs = homeserverToUse( name, @@ -267,21 +265,19 @@ def setup_test_homeserver( **kargs ) - # Prepare the DB on SQLite -- PostgreSQL is a copy of an already up to - # date db - if not isinstance(db_engine, PostgresEngine): - db_conn = hs.get_db_conn() - yield prepare_database(db_conn, db_engine, config) - db_conn.commit() - db_conn.close() + hs.setup() + if homeserverToUse.__name__ == "TestHomeServer": + hs.setup_master() + + if isinstance(db_engine, PostgresEngine): + database = hs.get_datastores().databases[0] - else: # We need to do cleanup on PostgreSQL def cleanup(): import psycopg2 # Close all the db pools - hs.get_db_pool().close() + database._db_pool.close() dropped = False @@ -320,23 +316,12 @@ def setup_test_homeserver( # Register the cleanup hook cleanup_func(cleanup) - hs.setup() - if homeserverToUse.__name__ == "TestHomeServer": - hs.setup_master() else: - # If we have been given an explicit datastore we probably want to mock - # out the DataStores somehow too. This all feels a bit wrong, but then - # mocking the stores feels wrong too. - datastores = Mock(datastore=datastore) - hs = homeserverToUse( name, - db_pool=None, datastore=datastore, - datastores=datastores, config=config, version_string="Synapse/tests", - database_engine=db_engine, tls_server_context_factory=Mock(), tls_client_options_factory=Mock(), reactor=reactor, From 7963ca83cbefb782a94c47fd65ad6e94d05dc5d1 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Wed, 18 Dec 2019 11:13:33 +0000 Subject: [PATCH 0721/1623] Add delta file to fix missing default table data (#6555) --- changelog.d/6555.bugfix | 1 + .../storage/data_stores/main/deviceinbox.py | 17 ++-------------- .../delta/56/device_stream_id_insert.sql | 20 +++++++++++++++++++ .../full_schemas/54/stream_positions.sql | 1 + 4 files changed, 24 insertions(+), 15 deletions(-) create mode 100644 changelog.d/6555.bugfix create mode 100644 synapse/storage/data_stores/main/schema/delta/56/device_stream_id_insert.sql diff --git a/changelog.d/6555.bugfix b/changelog.d/6555.bugfix new file mode 100644 index 0000000000..86a5a56cf6 --- /dev/null +++ b/changelog.d/6555.bugfix @@ -0,0 +1 @@ +Fix missing row in device_max_stream_id that could cause unable to decrypt errors after server restart. \ No newline at end of file diff --git a/synapse/storage/data_stores/main/deviceinbox.py b/synapse/storage/data_stores/main/deviceinbox.py index 85cfa16850..0613b49f4a 100644 --- a/synapse/storage/data_stores/main/deviceinbox.py +++ b/synapse/storage/data_stores/main/deviceinbox.py @@ -358,21 +358,8 @@ class DeviceInboxStore(DeviceInboxWorkerStore, DeviceInboxBackgroundUpdateStore) def _add_messages_to_local_device_inbox_txn( self, txn, stream_id, messages_by_user_then_device ): - # Compatible method of performing an upsert - sql = "SELECT stream_id FROM device_max_stream_id" - - txn.execute(sql) - rows = txn.fetchone() - if rows: - db_stream_id = rows[0] - if db_stream_id < stream_id: - # Insert the new stream_id - sql = "UPDATE device_max_stream_id SET stream_id = ?" - else: - # No rows, perform an insert - sql = "INSERT INTO device_max_stream_id (stream_id) VALUES (?)" - - txn.execute(sql, (stream_id,)) + sql = "UPDATE device_max_stream_id" " SET stream_id = ?" " WHERE stream_id < ?" + txn.execute(sql, (stream_id, stream_id)) local_by_user_then_device = {} for user_id, messages_by_device in messages_by_user_then_device.items(): diff --git a/synapse/storage/data_stores/main/schema/delta/56/device_stream_id_insert.sql b/synapse/storage/data_stores/main/schema/delta/56/device_stream_id_insert.sql new file mode 100644 index 0000000000..c2f557fde9 --- /dev/null +++ b/synapse/storage/data_stores/main/schema/delta/56/device_stream_id_insert.sql @@ -0,0 +1,20 @@ +/* Copyright 2019 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +-- This line already existed in deltas/35/device_stream_id but was not included in the +-- 54 full schema SQL. Add some SQL here to insert the missing row if it does not exist +INSERT INTO device_max_stream_id (stream_id) SELECT 0 WHERE NOT EXISTS ( + SELECT * from device_max_stream_id +); \ No newline at end of file diff --git a/synapse/storage/data_stores/main/schema/full_schemas/54/stream_positions.sql b/synapse/storage/data_stores/main/schema/full_schemas/54/stream_positions.sql index c265fd20e2..91d21b2921 100644 --- a/synapse/storage/data_stores/main/schema/full_schemas/54/stream_positions.sql +++ b/synapse/storage/data_stores/main/schema/full_schemas/54/stream_positions.sql @@ -5,3 +5,4 @@ INSERT INTO federation_stream_position (type, stream_id) SELECT 'events', coales INSERT INTO user_directory_stream_pos (stream_id) VALUES (0); INSERT INTO stats_stream_pos (stream_id) VALUES (0); INSERT INTO event_push_summary_stream_ordering (stream_ordering) VALUES (0); +-- device_max_stream_id is handled separately in 56/device_stream_id_insert.sql \ No newline at end of file From d6752ce5da38d35857fe324800d76a86ee1e64f1 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 18 Dec 2019 14:26:58 +0000 Subject: [PATCH 0722/1623] Clean up startup for the pusher (#6558) * Remove redundant python2 support code `str.decode()` doesn't exist on python3, so presumably this code was doing nothing * Filter out pushers with corrupt data When we get a row with unparsable json, drop the row, rather than returning a row with null `data`, which will then cause an explosion later on. * Improve logging when we can't start a pusher Log the ID to help us understand the problem * Make email pusher setup more robust We know we'll have a `data` member, since that comes from the database. What we *don't* know is if that is a dict, and if that has a `brand` member, and if that member is a string. --- changelog.d/6558.misc | 1 + synapse/push/pusher.py | 12 +++++---- synapse/push/pusherpool.py | 10 ++++--- synapse/rest/client/v1/pusher.py | 31 +++++++++++----------- synapse/storage/data_stores/main/pusher.py | 25 ++++++----------- tests/push/test_email.py | 3 +++ tests/push/test_http.py | 4 +++ 7 files changed, 44 insertions(+), 42 deletions(-) create mode 100644 changelog.d/6558.misc diff --git a/changelog.d/6558.misc b/changelog.d/6558.misc new file mode 100644 index 0000000000..a7572f1a85 --- /dev/null +++ b/changelog.d/6558.misc @@ -0,0 +1 @@ +Clean up logs from the push notifier at startup. \ No newline at end of file diff --git a/synapse/push/pusher.py b/synapse/push/pusher.py index f277aeb131..8ad0bf5936 100644 --- a/synapse/push/pusher.py +++ b/synapse/push/pusher.py @@ -80,9 +80,11 @@ class PusherFactory(object): return EmailPusher(self.hs, pusherdict, mailer) def _app_name_from_pusherdict(self, pusherdict): - if "data" in pusherdict and "brand" in pusherdict["data"]: - app_name = pusherdict["data"]["brand"] - else: - app_name = self.config.email_app_name + data = pusherdict["data"] - return app_name + if isinstance(data, dict): + brand = data.get("brand") + if isinstance(brand, str): + return brand + + return self.config.email_app_name diff --git a/synapse/push/pusherpool.py b/synapse/push/pusherpool.py index 0f6992202d..b9dca5bc63 100644 --- a/synapse/push/pusherpool.py +++ b/synapse/push/pusherpool.py @@ -232,7 +232,6 @@ class PusherPool: Deferred """ pushers = yield self.store.get_all_pushers() - logger.info("Starting %d pushers", len(pushers)) # Stagger starting up the pushers so we don't completely drown the # process on start up. @@ -245,7 +244,7 @@ class PusherPool: """Start the given pusher Args: - pusherdict (dict): + pusherdict (dict): dict with the values pulled from the db table Returns: Deferred[EmailPusher|HttpPusher] @@ -254,7 +253,8 @@ class PusherPool: p = self.pusher_factory.create_pusher(pusherdict) except PusherConfigException as e: logger.warning( - "Pusher incorrectly configured user=%s, appid=%s, pushkey=%s: %s", + "Pusher incorrectly configured id=%i, user=%s, appid=%s, pushkey=%s: %s", + pusherdict["id"], pusherdict.get("user_name"), pusherdict.get("app_id"), pusherdict.get("pushkey"), @@ -262,7 +262,9 @@ class PusherPool: ) return except Exception: - logger.exception("Couldn't start a pusher: caught Exception") + logger.exception( + "Couldn't start pusher id %i: caught Exception", pusherdict["id"], + ) return if not p: diff --git a/synapse/rest/client/v1/pusher.py b/synapse/rest/client/v1/pusher.py index 0791866f55..6f6b7aed6e 100644 --- a/synapse/rest/client/v1/pusher.py +++ b/synapse/rest/client/v1/pusher.py @@ -28,6 +28,17 @@ from synapse.rest.client.v2_alpha._base import client_patterns logger = logging.getLogger(__name__) +ALLOWED_KEYS = { + "app_display_name", + "app_id", + "data", + "device_display_name", + "kind", + "lang", + "profile_tag", + "pushkey", +} + class PushersRestServlet(RestServlet): PATTERNS = client_patterns("/pushers$", v1=True) @@ -43,23 +54,11 @@ class PushersRestServlet(RestServlet): pushers = await self.hs.get_datastore().get_pushers_by_user_id(user.to_string()) - allowed_keys = [ - "app_display_name", - "app_id", - "data", - "device_display_name", - "kind", - "lang", - "profile_tag", - "pushkey", - ] + filtered_pushers = list( + {k: v for k, v in p.items() if k in ALLOWED_KEYS} for p in pushers + ) - for p in pushers: - for k, v in list(p.items()): - if k not in allowed_keys: - del p[k] - - return 200, {"pushers": pushers} + return 200, {"pushers": filtered_pushers} def on_OPTIONS(self, _): return 200, {} diff --git a/synapse/storage/data_stores/main/pusher.py b/synapse/storage/data_stores/main/pusher.py index f07309ef09..6b03233262 100644 --- a/synapse/storage/data_stores/main/pusher.py +++ b/synapse/storage/data_stores/main/pusher.py @@ -15,8 +15,7 @@ # limitations under the License. import logging - -import six +from typing import Iterable, Iterator from canonicaljson import encode_canonical_json, json @@ -27,21 +26,16 @@ from synapse.util.caches.descriptors import cachedInlineCallbacks, cachedList logger = logging.getLogger(__name__) -if six.PY2: - db_binary_type = six.moves.builtins.buffer -else: - db_binary_type = memoryview - class PusherWorkerStore(SQLBaseStore): - def _decode_pushers_rows(self, rows): + def _decode_pushers_rows(self, rows: Iterable[dict]) -> Iterator[dict]: + """JSON-decode the data in the rows returned from the `pushers` table + + Drops any rows whose data cannot be decoded + """ for r in rows: dataJson = r["data"] - r["data"] = None try: - if isinstance(dataJson, db_binary_type): - dataJson = str(dataJson).decode("UTF8") - r["data"] = json.loads(dataJson) except Exception as e: logger.warning( @@ -50,12 +44,9 @@ class PusherWorkerStore(SQLBaseStore): dataJson, e.args[0], ) - pass + continue - if isinstance(r["pushkey"], db_binary_type): - r["pushkey"] = str(r["pushkey"]).decode("UTF8") - - return rows + yield r @defer.inlineCallbacks def user_has_pusher(self, user_id): diff --git a/tests/push/test_email.py b/tests/push/test_email.py index 358b593cd4..80187406bc 100644 --- a/tests/push/test_email.py +++ b/tests/push/test_email.py @@ -165,6 +165,7 @@ class EmailPusherTests(HomeserverTestCase): pushers = self.get_success( self.hs.get_datastore().get_pushers_by(dict(user_name=self.user_id)) ) + pushers = list(pushers) self.assertEqual(len(pushers), 1) last_stream_ordering = pushers[0]["last_stream_ordering"] @@ -175,6 +176,7 @@ class EmailPusherTests(HomeserverTestCase): pushers = self.get_success( self.hs.get_datastore().get_pushers_by(dict(user_name=self.user_id)) ) + pushers = list(pushers) self.assertEqual(len(pushers), 1) self.assertEqual(last_stream_ordering, pushers[0]["last_stream_ordering"]) @@ -192,5 +194,6 @@ class EmailPusherTests(HomeserverTestCase): pushers = self.get_success( self.hs.get_datastore().get_pushers_by(dict(user_name=self.user_id)) ) + pushers = list(pushers) self.assertEqual(len(pushers), 1) self.assertTrue(pushers[0]["last_stream_ordering"] > last_stream_ordering) diff --git a/tests/push/test_http.py b/tests/push/test_http.py index af2327fb66..fe3441f081 100644 --- a/tests/push/test_http.py +++ b/tests/push/test_http.py @@ -104,6 +104,7 @@ class HTTPPusherTests(HomeserverTestCase): pushers = self.get_success( self.hs.get_datastore().get_pushers_by(dict(user_name=user_id)) ) + pushers = list(pushers) self.assertEqual(len(pushers), 1) last_stream_ordering = pushers[0]["last_stream_ordering"] @@ -114,6 +115,7 @@ class HTTPPusherTests(HomeserverTestCase): pushers = self.get_success( self.hs.get_datastore().get_pushers_by(dict(user_name=user_id)) ) + pushers = list(pushers) self.assertEqual(len(pushers), 1) self.assertEqual(last_stream_ordering, pushers[0]["last_stream_ordering"]) @@ -132,6 +134,7 @@ class HTTPPusherTests(HomeserverTestCase): pushers = self.get_success( self.hs.get_datastore().get_pushers_by(dict(user_name=user_id)) ) + pushers = list(pushers) self.assertEqual(len(pushers), 1) self.assertTrue(pushers[0]["last_stream_ordering"] > last_stream_ordering) last_stream_ordering = pushers[0]["last_stream_ordering"] @@ -151,5 +154,6 @@ class HTTPPusherTests(HomeserverTestCase): pushers = self.get_success( self.hs.get_datastore().get_pushers_by(dict(user_name=user_id)) ) + pushers = list(pushers) self.assertEqual(len(pushers), 1) self.assertTrue(pushers[0]["last_stream_ordering"] > last_stream_ordering) From b95b762560441b28f06e6458da796327e394953e Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 19 Dec 2019 11:11:14 +0000 Subject: [PATCH 0723/1623] Add an export_signing_key script (#6546) I want to do some key rotation, and it is silly that we don't have a way to do this. --- changelog.d/6546.feature | 1 + docs/code_style.md | 13 +++--- docs/sample_config.yaml | 19 +++++--- scripts/export_signing_key | 94 ++++++++++++++++++++++++++++++++++++++ synapse/config/key.py | 23 ++++++---- 5 files changed, 129 insertions(+), 21 deletions(-) create mode 100644 changelog.d/6546.feature create mode 100755 scripts/export_signing_key diff --git a/changelog.d/6546.feature b/changelog.d/6546.feature new file mode 100644 index 0000000000..954aacb0d0 --- /dev/null +++ b/changelog.d/6546.feature @@ -0,0 +1 @@ +Add an export_signing_key script to extract the public part of signing keys when rotating them. diff --git a/docs/code_style.md b/docs/code_style.md index f983f72d6c..71aecd41f7 100644 --- a/docs/code_style.md +++ b/docs/code_style.md @@ -137,6 +137,7 @@ Some guidelines follow: correctly handles the top-level option being set to `None` (as it will be if no sub-options are enabled). - Lines should be wrapped at 80 characters. +- Use two-space indents. Example: @@ -155,13 +156,13 @@ Example: # Settings for the frobber # frobber: - # frobbing speed. Defaults to 1. - # - #speed: 10 + # frobbing speed. Defaults to 1. + # + #speed: 10 - # frobbing distance. Defaults to 1000. - # - #distance: 100 + # frobbing distance. Defaults to 1000. + # + #distance: 100 Note that the sample configuration is generated from the synapse code and is maintained by a script, `scripts-dev/generate_sample_config`. diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 1787248f53..e3b05423b8 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -1122,14 +1122,19 @@ metrics_flags: signing_key_path: "CONFDIR/SERVERNAME.signing.key" # The keys that the server used to sign messages with but won't use -# to sign new messages. E.g. it has lost its private key +# to sign new messages. # -#old_signing_keys: -# "ed25519:auto": -# # Base64 encoded public key -# key: "The public part of your old signing key." -# # Millisecond POSIX timestamp when the key expired. -# expired_ts: 123456789123 +old_signing_keys: + # For each key, `key` should be the base64-encoded public key, and + # `expired_ts`should be the time (in milliseconds since the unix epoch) that + # it was last used. + # + # It is possible to build an entry from an old signing.key file using the + # `export_signing_key` script which is provided with synapse. + # + # For example: + # + #"ed25519:id": { key: "base64string", expired_ts: 123456789123 } # How long key response published by this server is valid for. # Used to set the valid_until_ts in /key/v2 APIs. diff --git a/scripts/export_signing_key b/scripts/export_signing_key new file mode 100755 index 0000000000..8aec9d802b --- /dev/null +++ b/scripts/export_signing_key @@ -0,0 +1,94 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright 2019 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import sys +import time +from typing import Optional + +import nacl.signing +from signedjson.key import encode_verify_key_base64, get_verify_key, read_signing_keys + + +def exit(status: int = 0, message: Optional[str] = None): + if message: + print(message, file=sys.stderr) + sys.exit(status) + + +def format_plain(public_key: nacl.signing.VerifyKey): + print( + "%s:%s %s" + % (public_key.alg, public_key.version, encode_verify_key_base64(public_key),) + ) + + +def format_for_config(public_key: nacl.signing.VerifyKey, expiry_ts: int): + print( + ' "%s:%s": { key: "%s", expired_ts: %i }' + % ( + public_key.alg, + public_key.version, + encode_verify_key_base64(public_key), + expiry_ts, + ) + ) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + + parser.add_argument( + "key_file", nargs="+", type=argparse.FileType("r"), help="The key file to read", + ) + + parser.add_argument( + "-x", + action="store_true", + dest="for_config", + help="format the output for inclusion in the old_signing_keys config setting", + ) + + parser.add_argument( + "--expiry-ts", + type=int, + default=int(time.time() * 1000) + 6*3600000, + help=( + "The expiry time to use for -x, in milliseconds since 1970. The default " + "is (now+6h)." + ), + ) + + args = parser.parse_args() + + formatter = ( + (lambda k: format_for_config(k, args.expiry_ts)) + if args.for_config + else format_plain + ) + + keys = [] + for file in args.key_file: + try: + res = read_signing_keys(file) + except Exception as e: + exit( + status=1, + message="Error reading key from file %s: %s %s" + % (file.name, type(e), e), + ) + res = [] + for key in res: + formatter(get_verify_key(key)) diff --git a/synapse/config/key.py b/synapse/config/key.py index 52ff1b2621..066e7838c3 100644 --- a/synapse/config/key.py +++ b/synapse/config/key.py @@ -108,7 +108,7 @@ class KeyConfig(Config): self.signing_key = self.read_signing_keys(signing_key_path, "signing_key") self.old_signing_keys = self.read_old_signing_keys( - config.get("old_signing_keys", {}) + config.get("old_signing_keys") ) self.key_refresh_interval = self.parse_duration( config.get("key_refresh_interval", "1d") @@ -199,14 +199,19 @@ class KeyConfig(Config): signing_key_path: "%(base_key_name)s.signing.key" # The keys that the server used to sign messages with but won't use - # to sign new messages. E.g. it has lost its private key + # to sign new messages. # - #old_signing_keys: - # "ed25519:auto": - # # Base64 encoded public key - # key: "The public part of your old signing key." - # # Millisecond POSIX timestamp when the key expired. - # expired_ts: 123456789123 + old_signing_keys: + # For each key, `key` should be the base64-encoded public key, and + # `expired_ts`should be the time (in milliseconds since the unix epoch) that + # it was last used. + # + # It is possible to build an entry from an old signing.key file using the + # `export_signing_key` script which is provided with synapse. + # + # For example: + # + #"ed25519:id": { key: "base64string", expired_ts: 123456789123 } # How long key response published by this server is valid for. # Used to set the valid_until_ts in /key/v2 APIs. @@ -290,6 +295,8 @@ class KeyConfig(Config): raise ConfigError("Error reading %s: %s" % (name, str(e))) def read_old_signing_keys(self, old_signing_keys): + if old_signing_keys is None: + return {} keys = {} for key_id, key_data in old_signing_keys.items(): if is_signing_algorithm_supported(key_id): From 0b794cbd7b232b42a2d726e6ab6c698d4bf35093 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 19 Dec 2019 14:52:52 +0000 Subject: [PATCH 0724/1623] Fix sdnotify with acme enabled (#6571) If acme was enabled, the sdnotify startup hook would never be run because we would try to add it to a hook which had already fired. There's no need to delay it: we can sdnotify as soon as we've started the listeners. --- changelog.d/6571.bugfix | 1 + synapse/app/_base.py | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 changelog.d/6571.bugfix diff --git a/changelog.d/6571.bugfix b/changelog.d/6571.bugfix new file mode 100644 index 0000000000..e38ea7b4f7 --- /dev/null +++ b/changelog.d/6571.bugfix @@ -0,0 +1 @@ +Fix a bug which meant that we did not send systemd notifications on startup if acme was enabled. diff --git a/synapse/app/_base.py b/synapse/app/_base.py index 9c96816096..0e8b467a3e 100644 --- a/synapse/app/_base.py +++ b/synapse/app/_base.py @@ -237,6 +237,12 @@ def start(hs, listeners=None): """ Start a Synapse server or worker. + Should be called once the reactor is running and (if we're using ACME) the + TLS certificates are in place. + + Will start the main HTTP listeners and do some other startup tasks, and then + notify systemd. + Args: hs (synapse.server.HomeServer) listeners (list[dict]): Listener configuration ('listeners' in homeserver.yaml) @@ -311,9 +317,7 @@ def setup_sdnotify(hs): # Tell systemd our state, if we're using it. This will silently fail if # we're not using systemd. - hs.get_reactor().addSystemEventTrigger( - "after", "startup", sdnotify, b"READY=1\nMAINPID=%i" % (os.getpid(),) - ) + sdnotify(b"READY=1\nMAINPID=%i" % (os.getpid(),)) hs.get_reactor().addSystemEventTrigger( "before", "shutdown", sdnotify, b"STOPPING=1" From bca30cefee3849813565dd71e571172818629d85 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 19 Dec 2019 14:53:15 +0000 Subject: [PATCH 0725/1623] Improve diagnostics on database upgrade failure (#6570) `Failed to upgrade database` is not helpful, and it's unlikely that UPGRADE.rst has anything useful. --- changelog.d/6570.misc | 1 + synapse/app/homeserver.py | 9 ++------- synapse/storage/prepare_database.py | 5 ++++- 3 files changed, 7 insertions(+), 8 deletions(-) create mode 100644 changelog.d/6570.misc diff --git a/changelog.d/6570.misc b/changelog.d/6570.misc new file mode 100644 index 0000000000..e89955a51e --- /dev/null +++ b/changelog.d/6570.misc @@ -0,0 +1 @@ +Improve diagnostics on database upgrade failure. diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py index b8661457e2..0e9bf7f53a 100644 --- a/synapse/app/homeserver.py +++ b/synapse/app/homeserver.py @@ -342,13 +342,8 @@ def setup(config_options): hs.setup() except IncorrectDatabaseSetup as e: quit_with_error(str(e)) - except UpgradeDatabaseException: - sys.stderr.write( - "\nFailed to upgrade database.\n" - "Have you checked for version specific instructions in" - " UPGRADES.rst?\n" - ) - sys.exit(1) + except UpgradeDatabaseException as e: + quit_with_error("Failed to upgrade database: %s" % (e,)) hs.setup_master() diff --git a/synapse/storage/prepare_database.py b/synapse/storage/prepare_database.py index b4194b44ee..0195edf4ac 100644 --- a/synapse/storage/prepare_database.py +++ b/synapse/storage/prepare_database.py @@ -69,7 +69,10 @@ def prepare_database(db_conn, database_engine, config, data_stores=["main"]): if user_version != SCHEMA_VERSION: # If we don't pass in a config file then we are expecting to # have already upgraded the DB. - raise UpgradeDatabaseException("Database needs to be upgraded") + raise UpgradeDatabaseException( + "Expected database schema version %i but got %i" + % (SCHEMA_VERSION, user_version) + ) else: _upgrade_existing_database( cur, From 3d46124ad01990d37fa54c1599c28314dc5f5d30 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 19 Dec 2019 15:07:28 +0000 Subject: [PATCH 0726/1623] Port some admin handlers to async/await (#6559) --- changelog.d/6559.misc | 1 + synapse/app/admin_cmd.py | 6 ++- synapse/handlers/admin.py | 41 ++++++++----------- synapse/handlers/deactivate_account.py | 54 ++++++++++++-------------- 4 files changed, 46 insertions(+), 56 deletions(-) create mode 100644 changelog.d/6559.misc diff --git a/changelog.d/6559.misc b/changelog.d/6559.misc new file mode 100644 index 0000000000..8bca37457d --- /dev/null +++ b/changelog.d/6559.misc @@ -0,0 +1 @@ +Port `synapse.handlers.admin` and `synapse.handlers.deactivate_account` to async/await. diff --git a/synapse/app/admin_cmd.py b/synapse/app/admin_cmd.py index 51a909419f..8e36bc57d3 100644 --- a/synapse/app/admin_cmd.py +++ b/synapse/app/admin_cmd.py @@ -104,8 +104,10 @@ def export_data_command(hs, args): user_id = args.user_id directory = args.output_directory - res = yield hs.get_handlers().admin_handler.export_user_data( - user_id, FileExfiltrationWriter(user_id, directory=directory) + res = yield defer.ensureDeferred( + hs.get_handlers().admin_handler.export_user_data( + user_id, FileExfiltrationWriter(user_id, directory=directory) + ) ) print(res) diff --git a/synapse/handlers/admin.py b/synapse/handlers/admin.py index 14449b9a1e..1a4ba12385 100644 --- a/synapse/handlers/admin.py +++ b/synapse/handlers/admin.py @@ -15,8 +15,6 @@ import logging -from twisted.internet import defer - from synapse.api.constants import Membership from synapse.types import RoomStreamToken from synapse.visibility import filter_events_for_client @@ -33,11 +31,10 @@ class AdminHandler(BaseHandler): self.storage = hs.get_storage() self.state_store = self.storage.state - @defer.inlineCallbacks - def get_whois(self, user): + async def get_whois(self, user): connections = [] - sessions = yield self.store.get_user_ip_and_agents(user) + sessions = await self.store.get_user_ip_and_agents(user) for session in sessions: connections.append( { @@ -54,20 +51,18 @@ class AdminHandler(BaseHandler): return ret - @defer.inlineCallbacks - def get_users(self): + async def get_users(self): """Function to retrieve a list of users in users table. Args: Returns: defer.Deferred: resolves to list[dict[str, Any]] """ - ret = yield self.store.get_users() + ret = await self.store.get_users() return ret - @defer.inlineCallbacks - def get_users_paginate(self, start, limit, name, guests, deactivated): + async def get_users_paginate(self, start, limit, name, guests, deactivated): """Function to retrieve a paginated list of users from users list. This will return a json list of users. @@ -80,14 +75,13 @@ class AdminHandler(BaseHandler): Returns: defer.Deferred: resolves to json list[dict[str, Any]] """ - ret = yield self.store.get_users_paginate( + ret = await self.store.get_users_paginate( start, limit, name, guests, deactivated ) return ret - @defer.inlineCallbacks - def search_users(self, term): + async def search_users(self, term): """Function to search users list for one or more users with the matched term. @@ -96,7 +90,7 @@ class AdminHandler(BaseHandler): Returns: defer.Deferred: resolves to list[dict[str, Any]] """ - ret = yield self.store.search_users(term) + ret = await self.store.search_users(term) return ret @@ -119,8 +113,7 @@ class AdminHandler(BaseHandler): """ return self.store.set_server_admin(user, admin) - @defer.inlineCallbacks - def export_user_data(self, user_id, writer): + async def export_user_data(self, user_id, writer): """Write all data we have on the user to the given writer. Args: @@ -132,7 +125,7 @@ class AdminHandler(BaseHandler): The returned value is that returned by `writer.finished()`. """ # Get all rooms the user is in or has been in - rooms = yield self.store.get_rooms_for_user_where_membership_is( + rooms = await self.store.get_rooms_for_user_where_membership_is( user_id, membership_list=( Membership.JOIN, @@ -145,7 +138,7 @@ class AdminHandler(BaseHandler): # We only try and fetch events for rooms the user has been in. If # they've been e.g. invited to a room without joining then we handle # those seperately. - rooms_user_has_been_in = yield self.store.get_rooms_user_has_been_in(user_id) + rooms_user_has_been_in = await self.store.get_rooms_user_has_been_in(user_id) for index, room in enumerate(rooms): room_id = room.room_id @@ -154,7 +147,7 @@ class AdminHandler(BaseHandler): "[%s] Handling room %s, %d/%d", user_id, room_id, index + 1, len(rooms) ) - forgotten = yield self.store.did_forget(user_id, room_id) + forgotten = await self.store.did_forget(user_id, room_id) if forgotten: logger.info("[%s] User forgot room %d, ignoring", user_id, room_id) continue @@ -166,7 +159,7 @@ class AdminHandler(BaseHandler): if room.membership == Membership.INVITE: event_id = room.event_id - invite = yield self.store.get_event(event_id, allow_none=True) + invite = await self.store.get_event(event_id, allow_none=True) if invite: invited_state = invite.unsigned["invite_room_state"] writer.write_invite(room_id, invite, invited_state) @@ -177,7 +170,7 @@ class AdminHandler(BaseHandler): # were joined. We estimate that point by looking at the # stream_ordering of the last membership if it wasn't a join. if room.membership == Membership.JOIN: - stream_ordering = yield self.store.get_room_max_stream_ordering() + stream_ordering = self.store.get_room_max_stream_ordering() else: stream_ordering = room.stream_ordering @@ -203,7 +196,7 @@ class AdminHandler(BaseHandler): # events that we have and then filtering, this isn't the most # efficient method perhaps but it does guarantee we get everything. while True: - events, _ = yield self.store.paginate_room_events( + events, _ = await self.store.paginate_room_events( room_id, from_key, to_key, limit=100, direction="f" ) if not events: @@ -211,7 +204,7 @@ class AdminHandler(BaseHandler): from_key = events[-1].internal_metadata.after - events = yield filter_events_for_client(self.storage, user_id, events) + events = await filter_events_for_client(self.storage, user_id, events) writer.write_events(room_id, events) @@ -247,7 +240,7 @@ class AdminHandler(BaseHandler): for event_id in extremities: if not event_to_unseen_prevs[event_id]: continue - state = yield self.state_store.get_state_for_event(event_id) + state = await self.state_store.get_state_for_event(event_id) writer.write_state(room_id, event_id, state) return writer.finished() diff --git a/synapse/handlers/deactivate_account.py b/synapse/handlers/deactivate_account.py index 6dedaaff8d..4426967f88 100644 --- a/synapse/handlers/deactivate_account.py +++ b/synapse/handlers/deactivate_account.py @@ -15,8 +15,6 @@ # limitations under the License. import logging -from twisted.internet import defer - from synapse.api.errors import SynapseError from synapse.metrics.background_process_metrics import run_as_background_process from synapse.types import UserID, create_requester @@ -46,8 +44,7 @@ class DeactivateAccountHandler(BaseHandler): self._account_validity_enabled = hs.config.account_validity.enabled - @defer.inlineCallbacks - def deactivate_account(self, user_id, erase_data, id_server=None): + async def deactivate_account(self, user_id, erase_data, id_server=None): """Deactivate a user's account Args: @@ -74,11 +71,11 @@ class DeactivateAccountHandler(BaseHandler): identity_server_supports_unbinding = True # Retrieve the 3PIDs this user has bound to an identity server - threepids = yield self.store.user_get_bound_threepids(user_id) + threepids = await self.store.user_get_bound_threepids(user_id) for threepid in threepids: try: - result = yield self._identity_handler.try_unbind_threepid( + result = await self._identity_handler.try_unbind_threepid( user_id, { "medium": threepid["medium"], @@ -91,33 +88,33 @@ class DeactivateAccountHandler(BaseHandler): # Do we want this to be a fatal error or should we carry on? logger.exception("Failed to remove threepid from ID server") raise SynapseError(400, "Failed to remove threepid from ID server") - yield self.store.user_delete_threepid( + await self.store.user_delete_threepid( user_id, threepid["medium"], threepid["address"] ) # Remove all 3PIDs this user has bound to the homeserver - yield self.store.user_delete_threepids(user_id) + await self.store.user_delete_threepids(user_id) # delete any devices belonging to the user, which will also # delete corresponding access tokens. - yield self._device_handler.delete_all_devices_for_user(user_id) + await self._device_handler.delete_all_devices_for_user(user_id) # then delete any remaining access tokens which weren't associated with # a device. - yield self._auth_handler.delete_access_tokens_for_user(user_id) + await self._auth_handler.delete_access_tokens_for_user(user_id) - yield self.store.user_set_password_hash(user_id, None) + await self.store.user_set_password_hash(user_id, None) # Add the user to a table of users pending deactivation (ie. # removal from all the rooms they're a member of) - yield self.store.add_user_pending_deactivation(user_id) + await self.store.add_user_pending_deactivation(user_id) # delete from user directory - yield self.user_directory_handler.handle_user_deactivated(user_id) + await self.user_directory_handler.handle_user_deactivated(user_id) # Mark the user as erased, if they asked for that if erase_data: logger.info("Marking %s as erased", user_id) - yield self.store.mark_user_erased(user_id) + await self.store.mark_user_erased(user_id) # Now start the process that goes through that list and # parts users from rooms (if it isn't already running) @@ -125,30 +122,29 @@ class DeactivateAccountHandler(BaseHandler): # Reject all pending invites for the user, so that the user doesn't show up in the # "invited" section of rooms' members list. - yield self._reject_pending_invites_for_user(user_id) + await self._reject_pending_invites_for_user(user_id) # Remove all information on the user from the account_validity table. if self._account_validity_enabled: - yield self.store.delete_account_validity_for_user(user_id) + await self.store.delete_account_validity_for_user(user_id) # Mark the user as deactivated. - yield self.store.set_user_deactivated_status(user_id, True) + await self.store.set_user_deactivated_status(user_id, True) return identity_server_supports_unbinding - @defer.inlineCallbacks - def _reject_pending_invites_for_user(self, user_id): + async def _reject_pending_invites_for_user(self, user_id): """Reject pending invites addressed to a given user ID. Args: user_id (str): The user ID to reject pending invites for. """ user = UserID.from_string(user_id) - pending_invites = yield self.store.get_invited_rooms_for_user(user_id) + pending_invites = await self.store.get_invited_rooms_for_user(user_id) for room in pending_invites: try: - yield self._room_member_handler.update_membership( + await self._room_member_handler.update_membership( create_requester(user), user, room.room_id, @@ -180,8 +176,7 @@ class DeactivateAccountHandler(BaseHandler): if not self._user_parter_running: run_as_background_process("user_parter_loop", self._user_parter_loop) - @defer.inlineCallbacks - def _user_parter_loop(self): + async def _user_parter_loop(self): """Loop that parts deactivated users from rooms Returns: @@ -191,19 +186,18 @@ class DeactivateAccountHandler(BaseHandler): logger.info("Starting user parter") try: while True: - user_id = yield self.store.get_user_pending_deactivation() + user_id = await self.store.get_user_pending_deactivation() if user_id is None: break logger.info("User parter parting %r", user_id) - yield self._part_user(user_id) - yield self.store.del_user_pending_deactivation(user_id) + await self._part_user(user_id) + await self.store.del_user_pending_deactivation(user_id) logger.info("User parter finished parting %r", user_id) logger.info("User parter finished: stopping") finally: self._user_parter_running = False - @defer.inlineCallbacks - def _part_user(self, user_id): + async def _part_user(self, user_id): """Causes the given user_id to leave all the rooms they're joined to Returns: @@ -211,11 +205,11 @@ class DeactivateAccountHandler(BaseHandler): """ user = UserID.from_string(user_id) - rooms_for_user = yield self.store.get_rooms_for_user(user_id) + rooms_for_user = await self.store.get_rooms_for_user(user_id) for room_id in rooms_for_user: logger.info("User parter parting %r from %r", user_id, room_id) try: - yield self._room_member_handler.update_membership( + await self._room_member_handler.update_membership( create_requester(user), user, room_id, From 0b5dbadd9607714c471cbf317a64a96d935898a2 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 19 Dec 2019 15:07:37 +0000 Subject: [PATCH 0727/1623] Explode on duplicate delta file names. (#6565) --- changelog.d/6565.misc | 1 + synapse/storage/prepare_database.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 changelog.d/6565.misc diff --git a/changelog.d/6565.misc b/changelog.d/6565.misc new file mode 100644 index 0000000000..e83f245bf0 --- /dev/null +++ b/changelog.d/6565.misc @@ -0,0 +1 @@ +Add assertion that schema delta file names are unique. diff --git a/synapse/storage/prepare_database.py b/synapse/storage/prepare_database.py index 0195edf4ac..403848ad03 100644 --- a/synapse/storage/prepare_database.py +++ b/synapse/storage/prepare_database.py @@ -18,6 +18,7 @@ import imp import logging import os import re +from collections import Counter import attr @@ -315,6 +316,9 @@ def _upgrade_existing_database( ) ) + # Used to check if we have any duplicate file names + file_name_counter = Counter() + # Now find which directories have anything of interest. directory_entries = [] for directory in directories: @@ -325,6 +329,9 @@ def _upgrade_existing_database( _DirectoryListing(file_name, os.path.join(directory, file_name)) for file_name in file_names ) + + for file_name in file_names: + file_name_counter[file_name] += 1 except FileNotFoundError: # Data stores can have empty entries for a given version delta. pass @@ -333,6 +340,17 @@ def _upgrade_existing_database( "Could not open delta dir for version %d: %s" % (v, directory) ) + duplicates = set( + file_name for file_name, count in file_name_counter.items() if count > 1 + ) + if duplicates: + # We don't support using the same file name in the same delta version. + raise PrepareDatabaseException( + "Found multiple delta files with the same name in v%d: %s", + v, + duplicates, + ) + # We sort to ensure that we apply the delta files in a consistent # order (to avoid bugs caused by inconsistent directory listing order) directory_entries.sort() From 03d3792f3c7978ecc057cab19ff95c8310403665 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 20 Dec 2019 09:55:45 +0000 Subject: [PATCH 0728/1623] Fix exceptions when attempting to backfill (#6576) Fixes #6575 --- changelog.d/6576.bugfix | 1 + synapse/handlers/federation.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelog.d/6576.bugfix diff --git a/changelog.d/6576.bugfix b/changelog.d/6576.bugfix new file mode 100644 index 0000000000..f5414fce4d --- /dev/null +++ b/changelog.d/6576.bugfix @@ -0,0 +1 @@ +Fix errors when attempting to backfill rooms over federation. diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index abe02907b9..6fb453ce60 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -797,7 +797,10 @@ class FederationHandler(BaseHandler): events_to_state = {} for e_id in edges: state, auth = yield self._get_state_for_room( - destination=dest, room_id=room_id, event_id=e_id + destination=dest, + room_id=room_id, + event_id=e_id, + include_event_in_state=False, ) auth_events.update({a.event_id: a for a in auth}) auth_events.update({s.event_id: s for s in state}) From fa780e9721c940479a72eed9877ccad4fef78160 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 20 Dec 2019 10:32:02 +0000 Subject: [PATCH 0729/1623] Change EventContext to use the Storage class (#6564) --- changelog.d/6564.misc | 1 + synapse/api/auth.py | 2 +- synapse/events/snapshot.py | 36 +++++++++++-------- synapse/events/third_party_rules.py | 2 +- synapse/handlers/_base.py | 2 +- synapse/handlers/federation.py | 14 ++++---- synapse/handlers/message.py | 10 +++--- synapse/handlers/room.py | 2 +- synapse/handlers/room_member.py | 4 +-- synapse/push/bulk_push_rule_evaluator.py | 4 +-- synapse/replication/http/federation.py | 5 ++- synapse/replication/http/send_event.py | 3 +- synapse/storage/data_stores/main/push_rule.py | 2 +- .../storage/data_stores/main/roommember.py | 2 +- tests/test_state.py | 28 +++++++-------- 15 files changed, 64 insertions(+), 53 deletions(-) create mode 100644 changelog.d/6564.misc diff --git a/changelog.d/6564.misc b/changelog.d/6564.misc new file mode 100644 index 0000000000..f644f5868b --- /dev/null +++ b/changelog.d/6564.misc @@ -0,0 +1 @@ +Change `EventContext` to use the `Storage` class, in preparation for moving state database queries to a separate data store. diff --git a/synapse/api/auth.py b/synapse/api/auth.py index 9fd52a8c77..abbc7079a3 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -79,7 +79,7 @@ class Auth(object): @defer.inlineCallbacks def check_from_context(self, room_version, event, context, do_sig_check=True): - prev_state_ids = yield context.get_prev_state_ids(self.store) + prev_state_ids = yield context.get_prev_state_ids() auth_events_ids = yield self.compute_auth_events( event, prev_state_ids, for_verification=True ) diff --git a/synapse/events/snapshot.py b/synapse/events/snapshot.py index 64e898f40c..a44baea365 100644 --- a/synapse/events/snapshot.py +++ b/synapse/events/snapshot.py @@ -149,7 +149,7 @@ class EventContext: # the prev_state_ids, so if we're a state event we include the event # id that we replaced in the state. if event.is_state(): - prev_state_ids = yield self.get_prev_state_ids(store) + prev_state_ids = yield self.get_prev_state_ids() prev_state_id = prev_state_ids.get((event.type, event.state_key)) else: prev_state_id = None @@ -167,12 +167,13 @@ class EventContext: } @staticmethod - def deserialize(store, input): + def deserialize(storage, input): """Converts a dict that was produced by `serialize` back into a EventContext. Args: - store (DataStore): Used to convert AS ID to AS object + storage (Storage): Used to convert AS ID to AS object and fetch + state. input (dict): A dict produced by `serialize` Returns: @@ -181,6 +182,7 @@ class EventContext: context = _AsyncEventContextImpl( # We use the state_group and prev_state_id stuff to pull the # current_state_ids out of the DB and construct prev_state_ids. + storage=storage, prev_state_id=input["prev_state_id"], event_type=input["event_type"], event_state_key=input["event_state_key"], @@ -193,7 +195,7 @@ class EventContext: app_service_id = input["app_service_id"] if app_service_id: - context.app_service = store.get_app_service_by_id(app_service_id) + context.app_service = storage.main.get_app_service_by_id(app_service_id) return context @@ -216,7 +218,7 @@ class EventContext: return self._state_group @defer.inlineCallbacks - def get_current_state_ids(self, store): + def get_current_state_ids(self): """ Gets the room state map, including this event - ie, the state in ``state_group`` @@ -234,11 +236,11 @@ class EventContext: if self.rejected: raise RuntimeError("Attempt to access state_ids of rejected event") - yield self._ensure_fetched(store) + yield self._ensure_fetched() return self._current_state_ids @defer.inlineCallbacks - def get_prev_state_ids(self, store): + def get_prev_state_ids(self): """ Gets the room state map, excluding this event. @@ -250,7 +252,7 @@ class EventContext: Maps a (type, state_key) to the event ID of the state event matching this tuple. """ - yield self._ensure_fetched(store) + yield self._ensure_fetched() return self._prev_state_ids def get_cached_current_state_ids(self): @@ -270,7 +272,7 @@ class EventContext: return self._current_state_ids - def _ensure_fetched(self, store): + def _ensure_fetched(self): return defer.succeed(None) @@ -282,6 +284,8 @@ class _AsyncEventContextImpl(EventContext): Attributes: + _storage (Storage) + _fetching_state_deferred (Deferred|None): Resolves when *_state_ids have been calculated. None if we haven't started calculating yet @@ -295,28 +299,30 @@ class _AsyncEventContextImpl(EventContext): that was replaced. """ + # This needs to have a default as we're inheriting + _storage = attr.ib(default=None) _prev_state_id = attr.ib(default=None) _event_type = attr.ib(default=None) _event_state_key = attr.ib(default=None) _fetching_state_deferred = attr.ib(default=None) - def _ensure_fetched(self, store): + def _ensure_fetched(self): if not self._fetching_state_deferred: - self._fetching_state_deferred = run_in_background( - self._fill_out_state, store - ) + self._fetching_state_deferred = run_in_background(self._fill_out_state) return make_deferred_yieldable(self._fetching_state_deferred) @defer.inlineCallbacks - def _fill_out_state(self, store): + def _fill_out_state(self): """Called to populate the _current_state_ids and _prev_state_ids attributes by loading from the database. """ if self.state_group is None: return - self._current_state_ids = yield store.get_state_ids_for_group(self.state_group) + self._current_state_ids = yield self._storage.state.get_state_ids_for_group( + self.state_group + ) if self._prev_state_id and self._event_state_key is not None: self._prev_state_ids = dict(self._current_state_ids) diff --git a/synapse/events/third_party_rules.py b/synapse/events/third_party_rules.py index 714a9b1579..86f7e5f8aa 100644 --- a/synapse/events/third_party_rules.py +++ b/synapse/events/third_party_rules.py @@ -53,7 +53,7 @@ class ThirdPartyEventRules(object): if self.third_party_rules is None: return True - prev_state_ids = yield context.get_prev_state_ids(self.store) + prev_state_ids = yield context.get_prev_state_ids() # Retrieve the state events from the database. state_events = {} diff --git a/synapse/handlers/_base.py b/synapse/handlers/_base.py index d15c6282fb..51413d910e 100644 --- a/synapse/handlers/_base.py +++ b/synapse/handlers/_base.py @@ -134,7 +134,7 @@ class BaseHandler(object): guest_access = event.content.get("guest_access", "forbidden") if guest_access != "can_join": if context: - current_state_ids = yield context.get_current_state_ids(self.store) + current_state_ids = yield context.get_current_state_ids() current_state = yield self.store.get_events( list(current_state_ids.values()) ) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 60bb00fc6a..05ae40dde7 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -718,7 +718,7 @@ class FederationHandler(BaseHandler): # changing their profile info. newly_joined = True - prev_state_ids = await context.get_prev_state_ids(self.store) + prev_state_ids = await context.get_prev_state_ids() prev_state_id = prev_state_ids.get((event.type, event.state_key)) if prev_state_id: @@ -1418,7 +1418,7 @@ class FederationHandler(BaseHandler): user = UserID.from_string(event.state_key) yield self.user_joined_room(user, event.room_id) - prev_state_ids = yield context.get_prev_state_ids(self.store) + prev_state_ids = yield context.get_prev_state_ids() state_ids = list(prev_state_ids.values()) auth_chain = yield self.store.get_auth_chain(state_ids) @@ -1927,7 +1927,7 @@ class FederationHandler(BaseHandler): context = yield self.state_handler.compute_event_context(event, old_state=state) if not auth_events: - prev_state_ids = yield context.get_prev_state_ids(self.store) + prev_state_ids = yield context.get_prev_state_ids() auth_events_ids = yield self.auth.compute_auth_events( event, prev_state_ids, for_verification=True ) @@ -2336,12 +2336,12 @@ class FederationHandler(BaseHandler): k: a.event_id for k, a in iteritems(auth_events) if k != event_key } - current_state_ids = yield context.get_current_state_ids(self.store) + current_state_ids = yield context.get_current_state_ids() current_state_ids = dict(current_state_ids) current_state_ids.update(state_updates) - prev_state_ids = yield context.get_prev_state_ids(self.store) + prev_state_ids = yield context.get_prev_state_ids() prev_state_ids = dict(prev_state_ids) prev_state_ids.update({k: a.event_id for k, a in iteritems(auth_events)}) @@ -2625,7 +2625,7 @@ class FederationHandler(BaseHandler): event.content["third_party_invite"]["signed"]["token"], ) original_invite = None - prev_state_ids = yield context.get_prev_state_ids(self.store) + prev_state_ids = yield context.get_prev_state_ids() original_invite_id = prev_state_ids.get(key) if original_invite_id: original_invite = yield self.store.get_event( @@ -2673,7 +2673,7 @@ class FederationHandler(BaseHandler): signed = event.content["third_party_invite"]["signed"] token = signed["token"] - prev_state_ids = yield context.get_prev_state_ids(self.store) + prev_state_ids = yield context.get_prev_state_ids() invite_event_id = prev_state_ids.get((EventTypes.ThirdPartyInvite, token)) invite_event = None diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index bf9add7fe2..4ad752205f 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -515,7 +515,7 @@ class EventCreationHandler(object): # federation as well as those created locally. As of room v3, aliases events # can be created by users that are not in the room, therefore we have to # tolerate them in event_auth.check(). - prev_state_ids = yield context.get_prev_state_ids(self.store) + prev_state_ids = yield context.get_prev_state_ids() prev_event_id = prev_state_ids.get((EventTypes.Member, event.sender)) prev_event = ( yield self.store.get_event(prev_event_id, allow_none=True) @@ -665,7 +665,7 @@ class EventCreationHandler(object): If so, returns the version of the event in context. Otherwise, returns None. """ - prev_state_ids = yield context.get_prev_state_ids(self.store) + prev_state_ids = yield context.get_prev_state_ids() prev_event_id = prev_state_ids.get((event.type, event.state_key)) if not prev_event_id: return @@ -914,7 +914,7 @@ class EventCreationHandler(object): def is_inviter_member_event(e): return e.type == EventTypes.Member and e.sender == event.sender - current_state_ids = yield context.get_current_state_ids(self.store) + current_state_ids = yield context.get_current_state_ids() state_to_include_ids = [ e_id @@ -967,7 +967,7 @@ class EventCreationHandler(object): if original_event.room_id != event.room_id: raise SynapseError(400, "Cannot redact event from a different room") - prev_state_ids = yield context.get_prev_state_ids(self.store) + prev_state_ids = yield context.get_prev_state_ids() auth_events_ids = yield self.auth.compute_auth_events( event, prev_state_ids, for_verification=True ) @@ -989,7 +989,7 @@ class EventCreationHandler(object): event.internal_metadata.recheck_redaction = False if event.type == EventTypes.Create: - prev_state_ids = yield context.get_prev_state_ids(self.store) + prev_state_ids = yield context.get_prev_state_ids() if prev_state_ids: raise AuthError(403, "Changing the room create event is forbidden") diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index d3a1a7b4a6..89c9118b26 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -184,7 +184,7 @@ class RoomCreationHandler(BaseHandler): requester, tombstone_event, tombstone_context ) - old_room_state = yield tombstone_context.get_current_state_ids(self.store) + old_room_state = yield tombstone_context.get_current_state_ids() # update any aliases yield self._move_aliases_to_new_room( diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index 7b7270fc61..44c5e3239c 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -193,7 +193,7 @@ class RoomMemberHandler(object): requester, event, context, extra_users=[target], ratelimit=ratelimit ) - prev_state_ids = yield context.get_prev_state_ids(self.store) + prev_state_ids = yield context.get_prev_state_ids() prev_member_event_id = prev_state_ids.get((EventTypes.Member, user_id), None) @@ -601,7 +601,7 @@ class RoomMemberHandler(object): if prev_event is not None: return - prev_state_ids = yield context.get_prev_state_ids(self.store) + prev_state_ids = yield context.get_prev_state_ids() if event.membership == Membership.JOIN: if requester.is_guest: guest_can_join = yield self._can_guest_join(prev_state_ids) diff --git a/synapse/push/bulk_push_rule_evaluator.py b/synapse/push/bulk_push_rule_evaluator.py index 7881780760..7d9f5a38d9 100644 --- a/synapse/push/bulk_push_rule_evaluator.py +++ b/synapse/push/bulk_push_rule_evaluator.py @@ -116,7 +116,7 @@ class BulkPushRuleEvaluator(object): @defer.inlineCallbacks def _get_power_levels_and_sender_level(self, event, context): - prev_state_ids = yield context.get_prev_state_ids(self.store) + prev_state_ids = yield context.get_prev_state_ids() pl_event_id = prev_state_ids.get(POWER_KEY) if pl_event_id: # fastpath: if there's a power level event, that's all we need, and @@ -304,7 +304,7 @@ class RulesForRoom(object): push_rules_delta_state_cache_metric.inc_hits() else: - current_state_ids = yield context.get_current_state_ids(self.store) + current_state_ids = yield context.get_current_state_ids() push_rules_delta_state_cache_metric.inc_misses() push_rules_state_size_counter.inc(len(current_state_ids)) diff --git a/synapse/replication/http/federation.py b/synapse/replication/http/federation.py index 9af4e7e173..49a3251372 100644 --- a/synapse/replication/http/federation.py +++ b/synapse/replication/http/federation.py @@ -51,6 +51,7 @@ class ReplicationFederationSendEventsRestServlet(ReplicationEndpoint): super(ReplicationFederationSendEventsRestServlet, self).__init__(hs) self.store = hs.get_datastore() + self.storage = hs.get_storage() self.clock = hs.get_clock() self.federation_handler = hs.get_handlers().federation_handler @@ -100,7 +101,9 @@ class ReplicationFederationSendEventsRestServlet(ReplicationEndpoint): EventType = event_type_from_format_version(format_ver) event = EventType(event_dict, internal_metadata, rejected_reason) - context = EventContext.deserialize(self.store, event_payload["context"]) + context = EventContext.deserialize( + self.storage, event_payload["context"] + ) event_and_contexts.append((event, context)) diff --git a/synapse/replication/http/send_event.py b/synapse/replication/http/send_event.py index 9bafd60b14..84b92f16ad 100644 --- a/synapse/replication/http/send_event.py +++ b/synapse/replication/http/send_event.py @@ -54,6 +54,7 @@ class ReplicationSendEventRestServlet(ReplicationEndpoint): self.event_creation_handler = hs.get_event_creation_handler() self.store = hs.get_datastore() + self.storage = hs.get_storage() self.clock = hs.get_clock() @staticmethod @@ -100,7 +101,7 @@ class ReplicationSendEventRestServlet(ReplicationEndpoint): event = EventType(event_dict, internal_metadata, rejected_reason) requester = Requester.deserialize(self.store, content["requester"]) - context = EventContext.deserialize(self.store, content["context"]) + context = EventContext.deserialize(self.storage, content["context"]) ratelimit = content["ratelimit"] extra_users = [UserID.from_string(u) for u in content["extra_users"]] diff --git a/synapse/storage/data_stores/main/push_rule.py b/synapse/storage/data_stores/main/push_rule.py index 5ba13aa973..e2673ae073 100644 --- a/synapse/storage/data_stores/main/push_rule.py +++ b/synapse/storage/data_stores/main/push_rule.py @@ -244,7 +244,7 @@ class PushRulesWorkerStore( # To do this we set the state_group to a new object as object() != object() state_group = object() - current_state_ids = yield context.get_current_state_ids(self) + current_state_ids = yield context.get_current_state_ids() result = yield self._bulk_get_push_rules_for_room( event.room_id, state_group, current_state_ids, event=event ) diff --git a/synapse/storage/data_stores/main/roommember.py b/synapse/storage/data_stores/main/roommember.py index 92e3b9c512..70ff5751b6 100644 --- a/synapse/storage/data_stores/main/roommember.py +++ b/synapse/storage/data_stores/main/roommember.py @@ -477,7 +477,7 @@ class RoomMemberWorkerStore(EventsWorkerStore): # To do this we set the state_group to a new object as object() != object() state_group = object() - current_state_ids = yield context.get_current_state_ids(self) + current_state_ids = yield context.get_current_state_ids() result = yield self._get_joined_users_from_context( event.room_id, state_group, current_state_ids, event=event, context=context ) diff --git a/tests/test_state.py b/tests/test_state.py index 176535947a..e0aae06be4 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -209,7 +209,7 @@ class StateTestCase(unittest.TestCase): ctx_c = context_store["C"] ctx_d = context_store["D"] - prev_state_ids = yield ctx_d.get_prev_state_ids(self.store) + prev_state_ids = yield ctx_d.get_prev_state_ids() self.assertEqual(2, len(prev_state_ids)) self.assertEqual(ctx_c.state_group, ctx_d.state_group_before_event) @@ -253,7 +253,7 @@ class StateTestCase(unittest.TestCase): ctx_c = context_store["C"] ctx_d = context_store["D"] - prev_state_ids = yield ctx_d.get_prev_state_ids(self.store) + prev_state_ids = yield ctx_d.get_prev_state_ids() self.assertSetEqual( {"START", "A", "C"}, {e_id for e_id in prev_state_ids.values()} ) @@ -312,7 +312,7 @@ class StateTestCase(unittest.TestCase): ctx_c = context_store["C"] ctx_e = context_store["E"] - prev_state_ids = yield ctx_e.get_prev_state_ids(self.store) + prev_state_ids = yield ctx_e.get_prev_state_ids() self.assertSetEqual( {"START", "A", "B", "C"}, {e for e in prev_state_ids.values()} ) @@ -387,7 +387,7 @@ class StateTestCase(unittest.TestCase): ctx_b = context_store["B"] ctx_d = context_store["D"] - prev_state_ids = yield ctx_d.get_prev_state_ids(self.store) + prev_state_ids = yield ctx_d.get_prev_state_ids() self.assertSetEqual( {"A1", "A2", "A3", "A5", "B"}, {e for e in prev_state_ids.values()} ) @@ -419,10 +419,10 @@ class StateTestCase(unittest.TestCase): context = yield self.state.compute_event_context(event, old_state=old_state) - prev_state_ids = yield context.get_prev_state_ids(self.store) + prev_state_ids = yield context.get_prev_state_ids() self.assertCountEqual((e.event_id for e in old_state), prev_state_ids.values()) - current_state_ids = yield context.get_current_state_ids(self.store) + current_state_ids = yield context.get_current_state_ids() self.assertCountEqual( (e.event_id for e in old_state), current_state_ids.values() ) @@ -442,10 +442,10 @@ class StateTestCase(unittest.TestCase): context = yield self.state.compute_event_context(event, old_state=old_state) - prev_state_ids = yield context.get_prev_state_ids(self.store) + prev_state_ids = yield context.get_prev_state_ids() self.assertCountEqual((e.event_id for e in old_state), prev_state_ids.values()) - current_state_ids = yield context.get_current_state_ids(self.store) + current_state_ids = yield context.get_current_state_ids() self.assertCountEqual( (e.event_id for e in old_state + [event]), current_state_ids.values() ) @@ -479,7 +479,7 @@ class StateTestCase(unittest.TestCase): context = yield self.state.compute_event_context(event) - current_state_ids = yield context.get_current_state_ids(self.store) + current_state_ids = yield context.get_current_state_ids() self.assertEqual( set([e.event_id for e in old_state]), set(current_state_ids.values()) @@ -511,7 +511,7 @@ class StateTestCase(unittest.TestCase): context = yield self.state.compute_event_context(event) - prev_state_ids = yield context.get_prev_state_ids(self.store) + prev_state_ids = yield context.get_prev_state_ids() self.assertEqual( set([e.event_id for e in old_state]), set(prev_state_ids.values()) @@ -552,7 +552,7 @@ class StateTestCase(unittest.TestCase): event, prev_event_id1, old_state_1, prev_event_id2, old_state_2 ) - current_state_ids = yield context.get_current_state_ids(self.store) + current_state_ids = yield context.get_current_state_ids() self.assertEqual(len(current_state_ids), 6) @@ -594,7 +594,7 @@ class StateTestCase(unittest.TestCase): event, prev_event_id1, old_state_1, prev_event_id2, old_state_2 ) - current_state_ids = yield context.get_current_state_ids(self.store) + current_state_ids = yield context.get_current_state_ids() self.assertEqual(len(current_state_ids), 6) @@ -649,7 +649,7 @@ class StateTestCase(unittest.TestCase): event, prev_event_id1, old_state_1, prev_event_id2, old_state_2 ) - current_state_ids = yield context.get_current_state_ids(self.store) + current_state_ids = yield context.get_current_state_ids() self.assertEqual(old_state_2[3].event_id, current_state_ids[("test1", "1")]) @@ -677,7 +677,7 @@ class StateTestCase(unittest.TestCase): event, prev_event_id1, old_state_1, prev_event_id2, old_state_2 ) - current_state_ids = yield context.get_current_state_ids(self.store) + current_state_ids = yield context.get_current_state_ids() self.assertEqual(old_state_1[3].event_id, current_state_ids[("test1", "1")]) From 4caab0e95e6972e9c0533bc1897073bda5a464bf Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 20 Dec 2019 10:46:46 +0000 Subject: [PATCH 0730/1623] Backport fixes to sqlite upgrade from develop (#6578) Only run prepare_database on connection for in-memory databases. Fixes #6569. --- changelog.d/6578.bugfix | 1 + synapse/storage/engines/sqlite.py | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 changelog.d/6578.bugfix diff --git a/changelog.d/6578.bugfix b/changelog.d/6578.bugfix new file mode 100644 index 0000000000..fae55a4456 --- /dev/null +++ b/changelog.d/6578.bugfix @@ -0,0 +1 @@ +Fix a bug introduced in Synapse 1.7.0 which caused an error on startup when upgrading from versions before 1.3.0. diff --git a/synapse/storage/engines/sqlite.py b/synapse/storage/engines/sqlite.py index ddad17dc5a..cbc74cd302 100644 --- a/synapse/storage/engines/sqlite.py +++ b/synapse/storage/engines/sqlite.py @@ -25,6 +25,9 @@ class Sqlite3Engine(object): def __init__(self, database_module, database_config): self.module = database_module + database = database_config.get("args", {}).get("database") + self._is_in_memory = database in (None, ":memory:",) + # The current max state_group, or None if we haven't looked # in the DB yet. self._current_state_group_id = None @@ -59,7 +62,12 @@ class Sqlite3Engine(object): return sql def on_new_connection(self, db_conn): - prepare_database(db_conn, self, config=None) + if self._is_in_memory: + # In memory databases need to be rebuilt each time. Ideally we'd + # reuse the same connection as we do when starting up, but that + # would involve using adbapi before we have started the reactor. + prepare_database(db_conn, self, config=None) + db_conn.create_function("rank", 1, _rank) def is_deadlock(self, error): From 75d8f26ac85efd3816d454927f40b6e4c3032df1 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 20 Dec 2019 10:48:24 +0000 Subject: [PATCH 0731/1623] Split state groups into a separate data store (#6296) --- changelog.d/6245.misc | 1 + scripts/synapse_port_db | 8 +- synapse/config/database.py | 10 +- synapse/storage/data_stores/__init__.py | 5 + synapse/storage/data_stores/main/events.py | 157 --- .../main/schema/delta/32/remove_indices.sql | 1 - .../schema/full_schemas/54/full.sql.postgres | 52 - .../schema/full_schemas/54/full.sql.sqlite | 6 - synapse/storage/data_stores/main/state.py | 937 +----------------- synapse/storage/data_stores/state/__init__.py | 16 + .../storage/data_stores/state/bg_updates.py | 374 +++++++ .../schema/delta/23/drop_state_index.sql | 0 .../schema/delta/30/state_stream.sql | 0 .../schema/delta/32/remove_state_indices.sql | 19 + .../schema/delta/35/add_state_index.sql | 0 .../{main => state}/schema/delta/35/state.sql | 0 .../schema/delta/35/state_dedupe.sql | 0 .../schema/delta/47/state_group_seq.py | 0 .../schema/delta/56/state_group_room_idx.sql | 17 + .../state/schema/full_schemas/54/full.sql | 37 + .../full_schemas/54/sequence.sql.postgres | 21 + synapse/storage/data_stores/state/store.py | 640 ++++++++++++ synapse/storage/persist_events.py | 2 +- synapse/storage/prepare_database.py | 2 +- synapse/storage/purge_events.py | 4 +- synapse/storage/state.py | 14 +- tests/storage/test_state.py | 2 +- tests/utils.py | 2 +- 28 files changed, 1159 insertions(+), 1168 deletions(-) create mode 100644 changelog.d/6245.misc create mode 100644 synapse/storage/data_stores/state/__init__.py create mode 100644 synapse/storage/data_stores/state/bg_updates.py rename synapse/storage/data_stores/{main => state}/schema/delta/23/drop_state_index.sql (100%) rename synapse/storage/data_stores/{main => state}/schema/delta/30/state_stream.sql (100%) create mode 100644 synapse/storage/data_stores/state/schema/delta/32/remove_state_indices.sql rename synapse/storage/data_stores/{main => state}/schema/delta/35/add_state_index.sql (100%) rename synapse/storage/data_stores/{main => state}/schema/delta/35/state.sql (100%) rename synapse/storage/data_stores/{main => state}/schema/delta/35/state_dedupe.sql (100%) rename synapse/storage/data_stores/{main => state}/schema/delta/47/state_group_seq.py (100%) create mode 100644 synapse/storage/data_stores/state/schema/delta/56/state_group_room_idx.sql create mode 100644 synapse/storage/data_stores/state/schema/full_schemas/54/full.sql create mode 100644 synapse/storage/data_stores/state/schema/full_schemas/54/sequence.sql.postgres create mode 100644 synapse/storage/data_stores/state/store.py diff --git a/changelog.d/6245.misc b/changelog.d/6245.misc new file mode 100644 index 0000000000..a3e6b8296e --- /dev/null +++ b/changelog.d/6245.misc @@ -0,0 +1 @@ +Split out state storage into separate data store. diff --git a/scripts/synapse_port_db b/scripts/synapse_port_db index 5b5368988c..eb927f2094 100755 --- a/scripts/synapse_port_db +++ b/scripts/synapse_port_db @@ -51,11 +51,12 @@ from synapse.storage.data_stores.main.registration import ( from synapse.storage.data_stores.main.room import RoomBackgroundUpdateStore from synapse.storage.data_stores.main.roommember import RoomMemberBackgroundUpdateStore from synapse.storage.data_stores.main.search import SearchBackgroundUpdateStore -from synapse.storage.data_stores.main.state import StateBackgroundUpdateStore +from synapse.storage.data_stores.main.state import MainStateBackgroundUpdateStore from synapse.storage.data_stores.main.stats import StatsStore from synapse.storage.data_stores.main.user_directory import ( UserDirectoryBackgroundUpdateStore, ) +from synapse.storage.data_stores.state.bg_updates import StateBackgroundUpdateStore from synapse.storage.database import Database, make_conn from synapse.storage.engines import create_engine from synapse.storage.prepare_database import prepare_database @@ -138,6 +139,7 @@ class Store( RoomMemberBackgroundUpdateStore, SearchBackgroundUpdateStore, StateBackgroundUpdateStore, + MainStateBackgroundUpdateStore, UserDirectoryBackgroundUpdateStore, StatsStore, ): @@ -496,9 +498,7 @@ class Porter(object): def run(self): try: self.sqlite_store = yield self.build_db_store( - DatabaseConnectionConfig( - "master", self.sqlite_config, data_stores=["main"] - ) + DatabaseConnectionConfig("master-sqlite", self.sqlite_config) ) # Check if all background updates are done, abort if not. diff --git a/synapse/config/database.py b/synapse/config/database.py index 5f2f3c7cfd..134824789c 100644 --- a/synapse/config/database.py +++ b/synapse/config/database.py @@ -34,10 +34,12 @@ class DatabaseConnectionConfig: module name, and `args` for the args to give to the database connector. data_stores: The list of data stores that should be provisioned on the - database. + database. Defaults to all data stores. """ - def __init__(self, name: str, db_config: dict, data_stores: List[str]): + def __init__( + self, name: str, db_config: dict, data_stores: List[str] = ["main", "state"] + ): if db_config["name"] not in ("sqlite3", "psycopg2"): raise ConfigError("Unsupported database type %r" % (db_config["name"],)) @@ -62,9 +64,7 @@ class DatabaseConfig(Config): if database_config is None: database_config = {"name": "sqlite3", "args": {}} - self.databases = [ - DatabaseConnectionConfig("master", database_config, data_stores=["main"]) - ] + self.databases = [DatabaseConnectionConfig("master", database_config)] self.set_databasepath(config.get("database_path")) diff --git a/synapse/storage/data_stores/__init__.py b/synapse/storage/data_stores/__init__.py index 0983e059c0..d20df5f076 100644 --- a/synapse/storage/data_stores/__init__.py +++ b/synapse/storage/data_stores/__init__.py @@ -15,6 +15,7 @@ import logging +from synapse.storage.data_stores.state import StateGroupDataStore from synapse.storage.database import Database, make_conn from synapse.storage.engines import create_engine from synapse.storage.prepare_database import prepare_database @@ -55,6 +56,10 @@ class DataStores(object): logger.info("Starting 'main' data store") self.main = main_store_class(database, db_conn, hs) + if "state" in database_config.data_stores: + logger.info("Starting 'state' data store") + self.state = StateGroupDataStore(database, db_conn, hs) + db_conn.commit() self.databases.append(database) diff --git a/synapse/storage/data_stores/main/events.py b/synapse/storage/data_stores/main/events.py index 998bba1aad..58f35d7f56 100644 --- a/synapse/storage/data_stores/main/events.py +++ b/synapse/storage/data_stores/main/events.py @@ -1757,163 +1757,6 @@ class EventsStore( return state_groups - def purge_unreferenced_state_groups( - self, room_id: str, state_groups_to_delete - ) -> defer.Deferred: - """Deletes no longer referenced state groups and de-deltas any state - groups that reference them. - - Args: - room_id: The room the state groups belong to (must all be in the - same room). - state_groups_to_delete (Collection[int]): Set of all state groups - to delete. - """ - - return self.db.runInteraction( - "purge_unreferenced_state_groups", - self._purge_unreferenced_state_groups, - room_id, - state_groups_to_delete, - ) - - def _purge_unreferenced_state_groups(self, txn, room_id, state_groups_to_delete): - logger.info( - "[purge] found %i state groups to delete", len(state_groups_to_delete) - ) - - rows = self.db.simple_select_many_txn( - txn, - table="state_group_edges", - column="prev_state_group", - iterable=state_groups_to_delete, - keyvalues={}, - retcols=("state_group",), - ) - - remaining_state_groups = set( - row["state_group"] - for row in rows - if row["state_group"] not in state_groups_to_delete - ) - - logger.info( - "[purge] de-delta-ing %i remaining state groups", - len(remaining_state_groups), - ) - - # Now we turn the state groups that reference to-be-deleted state - # groups to non delta versions. - for sg in remaining_state_groups: - logger.info("[purge] de-delta-ing remaining state group %s", sg) - curr_state = self._get_state_groups_from_groups_txn(txn, [sg]) - curr_state = curr_state[sg] - - self.db.simple_delete_txn( - txn, table="state_groups_state", keyvalues={"state_group": sg} - ) - - self.db.simple_delete_txn( - txn, table="state_group_edges", keyvalues={"state_group": sg} - ) - - self.db.simple_insert_many_txn( - txn, - table="state_groups_state", - values=[ - { - "state_group": sg, - "room_id": room_id, - "type": key[0], - "state_key": key[1], - "event_id": state_id, - } - for key, state_id in iteritems(curr_state) - ], - ) - - logger.info("[purge] removing redundant state groups") - txn.executemany( - "DELETE FROM state_groups_state WHERE state_group = ?", - ((sg,) for sg in state_groups_to_delete), - ) - txn.executemany( - "DELETE FROM state_groups WHERE id = ?", - ((sg,) for sg in state_groups_to_delete), - ) - - @defer.inlineCallbacks - def get_previous_state_groups(self, state_groups): - """Fetch the previous groups of the given state groups. - - Args: - state_groups (Iterable[int]) - - Returns: - Deferred[dict[int, int]]: mapping from state group to previous - state group. - """ - - rows = yield self.db.simple_select_many_batch( - table="state_group_edges", - column="prev_state_group", - iterable=state_groups, - keyvalues={}, - retcols=("prev_state_group", "state_group"), - desc="get_previous_state_groups", - ) - - return {row["state_group"]: row["prev_state_group"] for row in rows} - - def purge_room_state(self, room_id, state_groups_to_delete): - """Deletes all record of a room from state tables - - Args: - room_id (str): - state_groups_to_delete (list[int]): State groups to delete - """ - - return self.db.runInteraction( - "purge_room_state", - self._purge_room_state_txn, - room_id, - state_groups_to_delete, - ) - - def _purge_room_state_txn(self, txn, room_id, state_groups_to_delete): - # first we have to delete the state groups states - logger.info("[purge] removing %s from state_groups_state", room_id) - - self.db.simple_delete_many_txn( - txn, - table="state_groups_state", - column="state_group", - iterable=state_groups_to_delete, - keyvalues={}, - ) - - # ... and the state group edges - logger.info("[purge] removing %s from state_group_edges", room_id) - - self.db.simple_delete_many_txn( - txn, - table="state_group_edges", - column="state_group", - iterable=state_groups_to_delete, - keyvalues={}, - ) - - # ... and the state groups - logger.info("[purge] removing %s from state_groups", room_id) - - self.db.simple_delete_many_txn( - txn, - table="state_groups", - column="id", - iterable=state_groups_to_delete, - keyvalues={}, - ) - async def is_event_after(self, event_id1, event_id2): """Returns True if event_id1 is after event_id2 in the stream """ diff --git a/synapse/storage/data_stores/main/schema/delta/32/remove_indices.sql b/synapse/storage/data_stores/main/schema/delta/32/remove_indices.sql index 4219cdd06a..2de50d408c 100644 --- a/synapse/storage/data_stores/main/schema/delta/32/remove_indices.sql +++ b/synapse/storage/data_stores/main/schema/delta/32/remove_indices.sql @@ -20,7 +20,6 @@ DROP INDEX IF EXISTS events_room_id; -- Prefix of events_room_stream DROP INDEX IF EXISTS events_order; -- Prefix of events_order_topo_stream_room DROP INDEX IF EXISTS events_topological_ordering; -- Prefix of events_order_topo_stream_room DROP INDEX IF EXISTS events_stream_ordering; -- Duplicate of PRIMARY KEY -DROP INDEX IF EXISTS state_groups_id; -- Duplicate of PRIMARY KEY DROP INDEX IF EXISTS event_to_state_groups_id; -- Duplicate of PRIMARY KEY DROP INDEX IF EXISTS event_push_actions_room_id_event_id_user_id_profile_tag; -- Duplicate of UNIQUE CONSTRAINT diff --git a/synapse/storage/data_stores/main/schema/full_schemas/54/full.sql.postgres b/synapse/storage/data_stores/main/schema/full_schemas/54/full.sql.postgres index 4ad2929f32..889a9a0ce4 100644 --- a/synapse/storage/data_stores/main/schema/full_schemas/54/full.sql.postgres +++ b/synapse/storage/data_stores/main/schema/full_schemas/54/full.sql.postgres @@ -975,40 +975,6 @@ CREATE TABLE state_events ( -CREATE TABLE state_group_edges ( - state_group bigint NOT NULL, - prev_state_group bigint NOT NULL -); - - - -CREATE SEQUENCE state_group_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - - -CREATE TABLE state_groups ( - id bigint NOT NULL, - room_id text NOT NULL, - event_id text NOT NULL -); - - - -CREATE TABLE state_groups_state ( - state_group bigint NOT NULL, - room_id text NOT NULL, - type text NOT NULL, - state_key text NOT NULL, - event_id text NOT NULL -); - - - CREATE TABLE stats_stream_pos ( lock character(1) DEFAULT 'X'::bpchar NOT NULL, stream_id bigint, @@ -1482,12 +1448,6 @@ ALTER TABLE ONLY state_events ADD CONSTRAINT state_events_event_id_key UNIQUE (event_id); - -ALTER TABLE ONLY state_groups - ADD CONSTRAINT state_groups_pkey PRIMARY KEY (id); - - - ALTER TABLE ONLY stats_stream_pos ADD CONSTRAINT stats_stream_pos_lock_key UNIQUE (lock); @@ -1928,18 +1888,6 @@ CREATE UNIQUE INDEX room_stats_room_ts ON room_stats USING btree (room_id, ts); -CREATE INDEX state_group_edges_idx ON state_group_edges USING btree (state_group); - - - -CREATE INDEX state_group_edges_prev_idx ON state_group_edges USING btree (prev_state_group); - - - -CREATE INDEX state_groups_state_type_idx ON state_groups_state USING btree (state_group, type, state_key); - - - CREATE INDEX stream_ordering_to_exterm_idx ON stream_ordering_to_exterm USING btree (stream_ordering); diff --git a/synapse/storage/data_stores/main/schema/full_schemas/54/full.sql.sqlite b/synapse/storage/data_stores/main/schema/full_schemas/54/full.sql.sqlite index bad33291e7..a0411ede7e 100644 --- a/synapse/storage/data_stores/main/schema/full_schemas/54/full.sql.sqlite +++ b/synapse/storage/data_stores/main/schema/full_schemas/54/full.sql.sqlite @@ -42,8 +42,6 @@ CREATE INDEX ev_edges_id ON event_edges(event_id); CREATE INDEX ev_edges_prev_id ON event_edges(prev_event_id); CREATE TABLE room_depth( room_id TEXT NOT NULL, min_depth INTEGER NOT NULL, UNIQUE (room_id) ); CREATE INDEX room_depth_room ON room_depth(room_id); -CREATE TABLE state_groups( id BIGINT PRIMARY KEY, room_id TEXT NOT NULL, event_id TEXT NOT NULL ); -CREATE TABLE state_groups_state( state_group BIGINT NOT NULL, room_id TEXT NOT NULL, type TEXT NOT NULL, state_key TEXT NOT NULL, event_id TEXT NOT NULL ); CREATE TABLE event_to_state_groups( event_id TEXT NOT NULL, state_group BIGINT NOT NULL, UNIQUE (event_id) ); CREATE TABLE local_media_repository ( media_id TEXT, media_type TEXT, media_length INTEGER, created_ts BIGINT, upload_name TEXT, user_id TEXT, quarantined_by TEXT, url_cache TEXT, last_access_ts BIGINT, UNIQUE (media_id) ); CREATE TABLE local_media_repository_thumbnails ( media_id TEXT, thumbnail_width INTEGER, thumbnail_height INTEGER, thumbnail_type TEXT, thumbnail_method TEXT, thumbnail_length INTEGER, UNIQUE ( media_id, thumbnail_width, thumbnail_height, thumbnail_type ) ); @@ -120,9 +118,6 @@ CREATE TABLE device_max_stream_id ( stream_id BIGINT NOT NULL ); CREATE TABLE public_room_list_stream ( stream_id BIGINT NOT NULL, room_id TEXT NOT NULL, visibility BOOLEAN NOT NULL , appservice_id TEXT, network_id TEXT); CREATE INDEX public_room_list_stream_idx on public_room_list_stream( stream_id ); CREATE INDEX public_room_list_stream_rm_idx on public_room_list_stream( room_id, stream_id ); -CREATE TABLE state_group_edges( state_group BIGINT NOT NULL, prev_state_group BIGINT NOT NULL ); -CREATE INDEX state_group_edges_idx ON state_group_edges(state_group); -CREATE INDEX state_group_edges_prev_idx ON state_group_edges(prev_state_group); CREATE TABLE stream_ordering_to_exterm ( stream_ordering BIGINT NOT NULL, room_id TEXT NOT NULL, event_id TEXT NOT NULL ); CREATE INDEX stream_ordering_to_exterm_idx on stream_ordering_to_exterm( stream_ordering ); CREATE INDEX stream_ordering_to_exterm_rm_idx on stream_ordering_to_exterm( room_id, stream_ordering ); @@ -254,6 +249,5 @@ CREATE INDEX user_ips_last_seen_only ON user_ips (last_seen); CREATE INDEX users_creation_ts ON users (creation_ts); CREATE INDEX event_to_state_groups_sg_index ON event_to_state_groups (state_group); CREATE UNIQUE INDEX device_lists_remote_cache_unique_id ON device_lists_remote_cache (user_id, device_id); -CREATE INDEX state_groups_state_type_idx ON state_groups_state(state_group, type, state_key); CREATE UNIQUE INDEX device_lists_remote_extremeties_unique_idx ON device_lists_remote_extremeties (user_id); CREATE UNIQUE INDEX user_ips_user_token_ip_unique_index ON user_ips (user_id, access_token, ip); diff --git a/synapse/storage/data_stores/main/state.py b/synapse/storage/data_stores/main/state.py index dcc6b43cdf..0dc39f139c 100644 --- a/synapse/storage/data_stores/main/state.py +++ b/synapse/storage/data_stores/main/state.py @@ -17,8 +17,7 @@ import logging from collections import namedtuple from typing import Iterable, Tuple -from six import iteritems, itervalues -from six.moves import range +from six import iteritems from twisted.internet import defer @@ -29,11 +28,9 @@ from synapse.events.snapshot import EventContext from synapse.storage._base import SQLBaseStore from synapse.storage.data_stores.main.events_worker import EventsWorkerStore from synapse.storage.database import Database -from synapse.storage.engines import PostgresEngine from synapse.storage.state import StateFilter -from synapse.util.caches import get_cache_factor_for, intern_string +from synapse.util.caches import intern_string from synapse.util.caches.descriptors import cached, cachedList -from synapse.util.caches.dictionary_cache import DictionaryCache from synapse.util.stringutils import to_ascii logger = logging.getLogger(__name__) @@ -55,207 +52,14 @@ class _GetStateGroupDelta( return len(self.delta_ids) if self.delta_ids else 0 -class StateGroupBackgroundUpdateStore(SQLBaseStore): - """Defines functions related to state groups needed to run the state backgroud - updates. - """ - - def _count_state_group_hops_txn(self, txn, state_group): - """Given a state group, count how many hops there are in the tree. - - This is used to ensure the delta chains don't get too long. - """ - if isinstance(self.database_engine, PostgresEngine): - sql = """ - WITH RECURSIVE state(state_group) AS ( - VALUES(?::bigint) - UNION ALL - SELECT prev_state_group FROM state_group_edges e, state s - WHERE s.state_group = e.state_group - ) - SELECT count(*) FROM state; - """ - - txn.execute(sql, (state_group,)) - row = txn.fetchone() - if row and row[0]: - return row[0] - else: - return 0 - else: - # We don't use WITH RECURSIVE on sqlite3 as there are distributions - # that ship with an sqlite3 version that doesn't support it (e.g. wheezy) - next_group = state_group - count = 0 - - while next_group: - next_group = self.db.simple_select_one_onecol_txn( - txn, - table="state_group_edges", - keyvalues={"state_group": next_group}, - retcol="prev_state_group", - allow_none=True, - ) - if next_group: - count += 1 - - return count - - def _get_state_groups_from_groups_txn( - self, txn, groups, state_filter=StateFilter.all() - ): - results = {group: {} for group in groups} - - where_clause, where_args = state_filter.make_sql_filter_clause() - - # Unless the filter clause is empty, we're going to append it after an - # existing where clause - if where_clause: - where_clause = " AND (%s)" % (where_clause,) - - if isinstance(self.database_engine, PostgresEngine): - # Temporarily disable sequential scans in this transaction. This is - # a temporary hack until we can add the right indices in - txn.execute("SET LOCAL enable_seqscan=off") - - # The below query walks the state_group tree so that the "state" - # table includes all state_groups in the tree. It then joins - # against `state_groups_state` to fetch the latest state. - # It assumes that previous state groups are always numerically - # lesser. - # The PARTITION is used to get the event_id in the greatest state - # group for the given type, state_key. - # This may return multiple rows per (type, state_key), but last_value - # should be the same. - sql = """ - WITH RECURSIVE state(state_group) AS ( - VALUES(?::bigint) - UNION ALL - SELECT prev_state_group FROM state_group_edges e, state s - WHERE s.state_group = e.state_group - ) - SELECT DISTINCT type, state_key, last_value(event_id) OVER ( - PARTITION BY type, state_key ORDER BY state_group ASC - ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING - ) AS event_id FROM state_groups_state - WHERE state_group IN ( - SELECT state_group FROM state - ) - """ - - for group in groups: - args = [group] - args.extend(where_args) - - txn.execute(sql + where_clause, args) - for row in txn: - typ, state_key, event_id = row - key = (typ, state_key) - results[group][key] = event_id - else: - max_entries_returned = state_filter.max_entries_returned() - - # We don't use WITH RECURSIVE on sqlite3 as there are distributions - # that ship with an sqlite3 version that doesn't support it (e.g. wheezy) - for group in groups: - next_group = group - - while next_group: - # We did this before by getting the list of group ids, and - # then passing that list to sqlite to get latest event for - # each (type, state_key). However, that was terribly slow - # without the right indices (which we can't add until - # after we finish deduping state, which requires this func) - args = [next_group] - args.extend(where_args) - - txn.execute( - "SELECT type, state_key, event_id FROM state_groups_state" - " WHERE state_group = ? " + where_clause, - args, - ) - results[group].update( - ((typ, state_key), event_id) - for typ, state_key, event_id in txn - if (typ, state_key) not in results[group] - ) - - # If the number of entries in the (type,state_key)->event_id dict - # matches the number of (type,state_keys) types we were searching - # for, then we must have found them all, so no need to go walk - # further down the tree... UNLESS our types filter contained - # wildcards (i.e. Nones) in which case we have to do an exhaustive - # search - if ( - max_entries_returned is not None - and len(results[group]) == max_entries_returned - ): - break - - next_group = self.db.simple_select_one_onecol_txn( - txn, - table="state_group_edges", - keyvalues={"state_group": next_group}, - retcol="prev_state_group", - allow_none=True, - ) - - return results - - # this inherits from EventsWorkerStore because it calls self.get_events -class StateGroupWorkerStore( - EventsWorkerStore, StateGroupBackgroundUpdateStore, SQLBaseStore -): +class StateGroupWorkerStore(EventsWorkerStore, SQLBaseStore): """The parts of StateGroupStore that can be called from workers. """ - STATE_GROUP_DEDUPLICATION_UPDATE_NAME = "state_group_state_deduplication" - STATE_GROUP_INDEX_UPDATE_NAME = "state_group_state_type_index" - CURRENT_STATE_INDEX_UPDATE_NAME = "current_state_members_idx" - def __init__(self, database: Database, db_conn, hs): super(StateGroupWorkerStore, self).__init__(database, db_conn, hs) - # Originally the state store used a single DictionaryCache to cache the - # event IDs for the state types in a given state group to avoid hammering - # on the state_group* tables. - # - # The point of using a DictionaryCache is that it can cache a subset - # of the state events for a given state group (i.e. a subset of the keys for a - # given dict which is an entry in the cache for a given state group ID). - # - # However, this poses problems when performing complicated queries - # on the store - for instance: "give me all the state for this group, but - # limit members to this subset of users", as DictionaryCache's API isn't - # rich enough to say "please cache any of these fields, apart from this subset". - # This is problematic when lazy loading members, which requires this behaviour, - # as without it the cache has no choice but to speculatively load all - # state events for the group, which negates the efficiency being sought. - # - # Rather than overcomplicating DictionaryCache's API, we instead split the - # state_group_cache into two halves - one for tracking non-member events, - # and the other for tracking member_events. This means that lazy loading - # queries can be made in a cache-friendly manner by querying both caches - # separately and then merging the result. So for the example above, you - # would query the members cache for a specific subset of state keys - # (which DictionaryCache will handle efficiently and fine) and the non-members - # cache for all state (which DictionaryCache will similarly handle fine) - # and then just merge the results together. - # - # We size the non-members cache to be smaller than the members cache as the - # vast majority of state in Matrix (today) is member events. - - self._state_group_cache = DictionaryCache( - "*stateGroupCache*", - # TODO: this hasn't been tuned yet - 50000 * get_cache_factor_for("stateGroupCache"), - ) - self._state_group_members_cache = DictionaryCache( - "*stateGroupMembersCache*", - 500000 * get_cache_factor_for("stateGroupMembersCache"), - ) - @defer.inlineCallbacks def get_room_version(self, room_id): """Get the room_version of a given room @@ -431,229 +235,6 @@ class StateGroupWorkerStore( return event.content.get("canonical_alias") - @cached(max_entries=10000, iterable=True) - def get_state_group_delta(self, state_group): - """Given a state group try to return a previous group and a delta between - the old and the new. - - Returns: - (prev_group, delta_ids), where both may be None. - """ - - def _get_state_group_delta_txn(txn): - prev_group = self.db.simple_select_one_onecol_txn( - txn, - table="state_group_edges", - keyvalues={"state_group": state_group}, - retcol="prev_state_group", - allow_none=True, - ) - - if not prev_group: - return _GetStateGroupDelta(None, None) - - delta_ids = self.db.simple_select_list_txn( - txn, - table="state_groups_state", - keyvalues={"state_group": state_group}, - retcols=("type", "state_key", "event_id"), - ) - - return _GetStateGroupDelta( - prev_group, - {(row["type"], row["state_key"]): row["event_id"] for row in delta_ids}, - ) - - return self.db.runInteraction( - "get_state_group_delta", _get_state_group_delta_txn - ) - - @defer.inlineCallbacks - def get_state_groups_ids(self, _room_id, event_ids): - """Get the event IDs of all the state for the state groups for the given events - - Args: - _room_id (str): id of the room for these events - event_ids (iterable[str]): ids of the events - - Returns: - Deferred[dict[int, dict[tuple[str, str], str]]]: - dict of state_group_id -> (dict of (type, state_key) -> event id) - """ - if not event_ids: - return {} - - event_to_groups = yield self._get_state_group_for_events(event_ids) - - groups = set(itervalues(event_to_groups)) - group_to_state = yield self._get_state_for_groups(groups) - - return group_to_state - - @defer.inlineCallbacks - def get_state_ids_for_group(self, state_group): - """Get the event IDs of all the state in the given state group - - Args: - state_group (int) - - Returns: - Deferred[dict]: Resolves to a map of (type, state_key) -> event_id - """ - group_to_state = yield self._get_state_for_groups((state_group,)) - - return group_to_state[state_group] - - @defer.inlineCallbacks - def get_state_groups(self, room_id, event_ids): - """ Get the state groups for the given list of event_ids - - Returns: - Deferred[dict[int, list[EventBase]]]: - dict of state_group_id -> list of state events. - """ - if not event_ids: - return {} - - group_to_ids = yield self.get_state_groups_ids(room_id, event_ids) - - state_event_map = yield self.get_events( - [ - ev_id - for group_ids in itervalues(group_to_ids) - for ev_id in itervalues(group_ids) - ], - get_prev_content=False, - ) - - return { - group: [ - state_event_map[v] - for v in itervalues(event_id_map) - if v in state_event_map - ] - for group, event_id_map in iteritems(group_to_ids) - } - - @defer.inlineCallbacks - def _get_state_groups_from_groups(self, groups, state_filter): - """Returns the state groups for a given set of groups, filtering on - types of state events. - - Args: - groups(list[int]): list of state group IDs to query - state_filter (StateFilter): The state filter used to fetch state - from the database. - Returns: - Deferred[dict[int, dict[tuple[str, str], str]]]: - dict of state_group_id -> (dict of (type, state_key) -> event id) - """ - results = {} - - chunks = [groups[i : i + 100] for i in range(0, len(groups), 100)] - for chunk in chunks: - res = yield self.db.runInteraction( - "_get_state_groups_from_groups", - self._get_state_groups_from_groups_txn, - chunk, - state_filter, - ) - results.update(res) - - return results - - @defer.inlineCallbacks - def get_state_for_events(self, event_ids, state_filter=StateFilter.all()): - """Given a list of event_ids and type tuples, return a list of state - dicts for each event. - - Args: - event_ids (list[string]) - state_filter (StateFilter): The state filter used to fetch state - from the database. - - Returns: - deferred: A dict of (event_id) -> (type, state_key) -> [state_events] - """ - event_to_groups = yield self._get_state_group_for_events(event_ids) - - groups = set(itervalues(event_to_groups)) - group_to_state = yield self._get_state_for_groups(groups, state_filter) - - state_event_map = yield self.get_events( - [ev_id for sd in itervalues(group_to_state) for ev_id in itervalues(sd)], - get_prev_content=False, - ) - - event_to_state = { - event_id: { - k: state_event_map[v] - for k, v in iteritems(group_to_state[group]) - if v in state_event_map - } - for event_id, group in iteritems(event_to_groups) - } - - return {event: event_to_state[event] for event in event_ids} - - @defer.inlineCallbacks - def get_state_ids_for_events(self, event_ids, state_filter=StateFilter.all()): - """ - Get the state dicts corresponding to a list of events, containing the event_ids - of the state events (as opposed to the events themselves) - - Args: - event_ids(list(str)): events whose state should be returned - state_filter (StateFilter): The state filter used to fetch state - from the database. - - Returns: - A deferred dict from event_id -> (type, state_key) -> event_id - """ - event_to_groups = yield self._get_state_group_for_events(event_ids) - - groups = set(itervalues(event_to_groups)) - group_to_state = yield self._get_state_for_groups(groups, state_filter) - - event_to_state = { - event_id: group_to_state[group] - for event_id, group in iteritems(event_to_groups) - } - - return {event: event_to_state[event] for event in event_ids} - - @defer.inlineCallbacks - def get_state_for_event(self, event_id, state_filter=StateFilter.all()): - """ - Get the state dict corresponding to a particular event - - Args: - event_id(str): event whose state should be returned - state_filter (StateFilter): The state filter used to fetch state - from the database. - - Returns: - A deferred dict from (type, state_key) -> state_event - """ - state_map = yield self.get_state_for_events([event_id], state_filter) - return state_map[event_id] - - @defer.inlineCallbacks - def get_state_ids_for_event(self, event_id, state_filter=StateFilter.all()): - """ - Get the state dict corresponding to a particular event - - Args: - event_id(str): event whose state should be returned - state_filter (StateFilter): The state filter used to fetch state - from the database. - - Returns: - A deferred dict from (type, state_key) -> state_event - """ - state_map = yield self.get_state_ids_for_events([event_id], state_filter) - return state_map[event_id] - @cached(max_entries=50000) def _get_state_group_for_event(self, event_id): return self.db.simple_select_one_onecol( @@ -684,329 +265,6 @@ class StateGroupWorkerStore( return {row["event_id"]: row["state_group"] for row in rows} - def _get_state_for_group_using_cache(self, cache, group, state_filter): - """Checks if group is in cache. See `_get_state_for_groups` - - Args: - cache(DictionaryCache): the state group cache to use - group(int): The state group to lookup - state_filter (StateFilter): The state filter used to fetch state - from the database. - - Returns 2-tuple (`state_dict`, `got_all`). - `got_all` is a bool indicating if we successfully retrieved all - requests state from the cache, if False we need to query the DB for the - missing state. - """ - is_all, known_absent, state_dict_ids = cache.get(group) - - if is_all or state_filter.is_full(): - # Either we have everything or want everything, either way - # `is_all` tells us whether we've gotten everything. - return state_filter.filter_state(state_dict_ids), is_all - - # tracks whether any of our requested types are missing from the cache - missing_types = False - - if state_filter.has_wildcards(): - # We don't know if we fetched all the state keys for the types in - # the filter that are wildcards, so we have to assume that we may - # have missed some. - missing_types = True - else: - # There aren't any wild cards, so `concrete_types()` returns the - # complete list of event types we're wanting. - for key in state_filter.concrete_types(): - if key not in state_dict_ids and key not in known_absent: - missing_types = True - break - - return state_filter.filter_state(state_dict_ids), not missing_types - - @defer.inlineCallbacks - def _get_state_for_groups(self, groups, state_filter=StateFilter.all()): - """Gets the state at each of a list of state groups, optionally - filtering by type/state_key - - Args: - groups (iterable[int]): list of state groups for which we want - to get the state. - state_filter (StateFilter): The state filter used to fetch state - from the database. - Returns: - Deferred[dict[int, dict[tuple[str, str], str]]]: - dict of state_group_id -> (dict of (type, state_key) -> event id) - """ - - member_filter, non_member_filter = state_filter.get_member_split() - - # Now we look them up in the member and non-member caches - ( - non_member_state, - incomplete_groups_nm, - ) = yield self._get_state_for_groups_using_cache( - groups, self._state_group_cache, state_filter=non_member_filter - ) - - ( - member_state, - incomplete_groups_m, - ) = yield self._get_state_for_groups_using_cache( - groups, self._state_group_members_cache, state_filter=member_filter - ) - - state = dict(non_member_state) - for group in groups: - state[group].update(member_state[group]) - - # Now fetch any missing groups from the database - - incomplete_groups = incomplete_groups_m | incomplete_groups_nm - - if not incomplete_groups: - return state - - cache_sequence_nm = self._state_group_cache.sequence - cache_sequence_m = self._state_group_members_cache.sequence - - # Help the cache hit ratio by expanding the filter a bit - db_state_filter = state_filter.return_expanded() - - group_to_state_dict = yield self._get_state_groups_from_groups( - list(incomplete_groups), state_filter=db_state_filter - ) - - # Now lets update the caches - self._insert_into_cache( - group_to_state_dict, - db_state_filter, - cache_seq_num_members=cache_sequence_m, - cache_seq_num_non_members=cache_sequence_nm, - ) - - # And finally update the result dict, by filtering out any extra - # stuff we pulled out of the database. - for group, group_state_dict in iteritems(group_to_state_dict): - # We just replace any existing entries, as we will have loaded - # everything we need from the database anyway. - state[group] = state_filter.filter_state(group_state_dict) - - return state - - def _get_state_for_groups_using_cache(self, groups, cache, state_filter): - """Gets the state at each of a list of state groups, optionally - filtering by type/state_key, querying from a specific cache. - - Args: - groups (iterable[int]): list of state groups for which we want - to get the state. - cache (DictionaryCache): the cache of group ids to state dicts which - we will pass through - either the normal state cache or the specific - members state cache. - state_filter (StateFilter): The state filter used to fetch state - from the database. - - Returns: - tuple[dict[int, dict[tuple[str, str], str]], set[int]]: Tuple of - dict of state_group_id -> (dict of (type, state_key) -> event id) - of entries in the cache, and the state group ids either missing - from the cache or incomplete. - """ - results = {} - incomplete_groups = set() - for group in set(groups): - state_dict_ids, got_all = self._get_state_for_group_using_cache( - cache, group, state_filter - ) - results[group] = state_dict_ids - - if not got_all: - incomplete_groups.add(group) - - return results, incomplete_groups - - def _insert_into_cache( - self, - group_to_state_dict, - state_filter, - cache_seq_num_members, - cache_seq_num_non_members, - ): - """Inserts results from querying the database into the relevant cache. - - Args: - group_to_state_dict (dict): The new entries pulled from database. - Map from state group to state dict - state_filter (StateFilter): The state filter used to fetch state - from the database. - cache_seq_num_members (int): Sequence number of member cache since - last lookup in cache - cache_seq_num_non_members (int): Sequence number of member cache since - last lookup in cache - """ - - # We need to work out which types we've fetched from the DB for the - # member vs non-member caches. This should be as accurate as possible, - # but can be an underestimate (e.g. when we have wild cards) - - member_filter, non_member_filter = state_filter.get_member_split() - if member_filter.is_full(): - # We fetched all member events - member_types = None - else: - # `concrete_types()` will only return a subset when there are wild - # cards in the filter, but that's fine. - member_types = member_filter.concrete_types() - - if non_member_filter.is_full(): - # We fetched all non member events - non_member_types = None - else: - non_member_types = non_member_filter.concrete_types() - - for group, group_state_dict in iteritems(group_to_state_dict): - state_dict_members = {} - state_dict_non_members = {} - - for k, v in iteritems(group_state_dict): - if k[0] == EventTypes.Member: - state_dict_members[k] = v - else: - state_dict_non_members[k] = v - - self._state_group_members_cache.update( - cache_seq_num_members, - key=group, - value=state_dict_members, - fetched_keys=member_types, - ) - - self._state_group_cache.update( - cache_seq_num_non_members, - key=group, - value=state_dict_non_members, - fetched_keys=non_member_types, - ) - - def store_state_group( - self, event_id, room_id, prev_group, delta_ids, current_state_ids - ): - """Store a new set of state, returning a newly assigned state group. - - Args: - event_id (str): The event ID for which the state was calculated - room_id (str) - prev_group (int|None): A previous state group for the room, optional. - delta_ids (dict|None): The delta between state at `prev_group` and - `current_state_ids`, if `prev_group` was given. Same format as - `current_state_ids`. - current_state_ids (dict): The state to store. Map of (type, state_key) - to event_id. - - Returns: - Deferred[int]: The state group ID - """ - - def _store_state_group_txn(txn): - if current_state_ids is None: - # AFAIK, this can never happen - raise Exception("current_state_ids cannot be None") - - state_group = self.database_engine.get_next_state_group_id(txn) - - self.db.simple_insert_txn( - txn, - table="state_groups", - values={"id": state_group, "room_id": room_id, "event_id": event_id}, - ) - - # We persist as a delta if we can, while also ensuring the chain - # of deltas isn't tooo long, as otherwise read performance degrades. - if prev_group: - is_in_db = self.db.simple_select_one_onecol_txn( - txn, - table="state_groups", - keyvalues={"id": prev_group}, - retcol="id", - allow_none=True, - ) - if not is_in_db: - raise Exception( - "Trying to persist state with unpersisted prev_group: %r" - % (prev_group,) - ) - - potential_hops = self._count_state_group_hops_txn(txn, prev_group) - if prev_group and potential_hops < MAX_STATE_DELTA_HOPS: - self.db.simple_insert_txn( - txn, - table="state_group_edges", - values={"state_group": state_group, "prev_state_group": prev_group}, - ) - - self.db.simple_insert_many_txn( - txn, - table="state_groups_state", - values=[ - { - "state_group": state_group, - "room_id": room_id, - "type": key[0], - "state_key": key[1], - "event_id": state_id, - } - for key, state_id in iteritems(delta_ids) - ], - ) - else: - self.db.simple_insert_many_txn( - txn, - table="state_groups_state", - values=[ - { - "state_group": state_group, - "room_id": room_id, - "type": key[0], - "state_key": key[1], - "event_id": state_id, - } - for key, state_id in iteritems(current_state_ids) - ], - ) - - # Prefill the state group caches with this group. - # It's fine to use the sequence like this as the state group map - # is immutable. (If the map wasn't immutable then this prefill could - # race with another update) - - current_member_state_ids = { - s: ev - for (s, ev) in iteritems(current_state_ids) - if s[0] == EventTypes.Member - } - txn.call_after( - self._state_group_members_cache.update, - self._state_group_members_cache.sequence, - key=state_group, - value=dict(current_member_state_ids), - ) - - current_non_member_state_ids = { - s: ev - for (s, ev) in iteritems(current_state_ids) - if s[0] != EventTypes.Member - } - txn.call_after( - self._state_group_cache.update, - self._state_group_cache.sequence, - key=state_group, - value=dict(current_non_member_state_ids), - ) - - return state_group - - return self.db.runInteraction("store_state_group", _store_state_group_txn) - @defer.inlineCallbacks def get_referenced_state_groups(self, state_groups): """Check if the state groups are referenced by events. @@ -1031,22 +289,14 @@ class StateGroupWorkerStore( return set(row["state_group"] for row in rows) -class StateBackgroundUpdateStore(StateGroupBackgroundUpdateStore): +class MainStateBackgroundUpdateStore(SQLBaseStore): - STATE_GROUP_DEDUPLICATION_UPDATE_NAME = "state_group_state_deduplication" - STATE_GROUP_INDEX_UPDATE_NAME = "state_group_state_type_index" CURRENT_STATE_INDEX_UPDATE_NAME = "current_state_members_idx" EVENT_STATE_GROUP_INDEX_UPDATE_NAME = "event_to_state_groups_sg_index" def __init__(self, database: Database, db_conn, hs): - super(StateBackgroundUpdateStore, self).__init__(database, db_conn, hs) - self.db.updates.register_background_update_handler( - self.STATE_GROUP_DEDUPLICATION_UPDATE_NAME, - self._background_deduplicate_state, - ) - self.db.updates.register_background_update_handler( - self.STATE_GROUP_INDEX_UPDATE_NAME, self._background_index_state - ) + super(MainStateBackgroundUpdateStore, self).__init__(database, db_conn, hs) + self.db.updates.register_background_index_update( self.CURRENT_STATE_INDEX_UPDATE_NAME, index_name="current_state_events_member_index", @@ -1061,181 +311,8 @@ class StateBackgroundUpdateStore(StateGroupBackgroundUpdateStore): columns=["state_group"], ) - @defer.inlineCallbacks - def _background_deduplicate_state(self, progress, batch_size): - """This background update will slowly deduplicate state by reencoding - them as deltas. - """ - last_state_group = progress.get("last_state_group", 0) - rows_inserted = progress.get("rows_inserted", 0) - max_group = progress.get("max_group", None) - BATCH_SIZE_SCALE_FACTOR = 100 - - batch_size = max(1, int(batch_size / BATCH_SIZE_SCALE_FACTOR)) - - if max_group is None: - rows = yield self.db.execute( - "_background_deduplicate_state", - None, - "SELECT coalesce(max(id), 0) FROM state_groups", - ) - max_group = rows[0][0] - - def reindex_txn(txn): - new_last_state_group = last_state_group - for count in range(batch_size): - txn.execute( - "SELECT id, room_id FROM state_groups" - " WHERE ? < id AND id <= ?" - " ORDER BY id ASC" - " LIMIT 1", - (new_last_state_group, max_group), - ) - row = txn.fetchone() - if row: - state_group, room_id = row - - if not row or not state_group: - return True, count - - txn.execute( - "SELECT state_group FROM state_group_edges" - " WHERE state_group = ?", - (state_group,), - ) - - # If we reach a point where we've already started inserting - # edges we should stop. - if txn.fetchall(): - return True, count - - txn.execute( - "SELECT coalesce(max(id), 0) FROM state_groups" - " WHERE id < ? AND room_id = ?", - (state_group, room_id), - ) - (prev_group,) = txn.fetchone() - new_last_state_group = state_group - - if prev_group: - potential_hops = self._count_state_group_hops_txn(txn, prev_group) - if potential_hops >= MAX_STATE_DELTA_HOPS: - # We want to ensure chains are at most this long,# - # otherwise read performance degrades. - continue - - prev_state = self._get_state_groups_from_groups_txn( - txn, [prev_group] - ) - prev_state = prev_state[prev_group] - - curr_state = self._get_state_groups_from_groups_txn( - txn, [state_group] - ) - curr_state = curr_state[state_group] - - if not set(prev_state.keys()) - set(curr_state.keys()): - # We can only do a delta if the current has a strict super set - # of keys - - delta_state = { - key: value - for key, value in iteritems(curr_state) - if prev_state.get(key, None) != value - } - - self.db.simple_delete_txn( - txn, - table="state_group_edges", - keyvalues={"state_group": state_group}, - ) - - self.db.simple_insert_txn( - txn, - table="state_group_edges", - values={ - "state_group": state_group, - "prev_state_group": prev_group, - }, - ) - - self.db.simple_delete_txn( - txn, - table="state_groups_state", - keyvalues={"state_group": state_group}, - ) - - self.db.simple_insert_many_txn( - txn, - table="state_groups_state", - values=[ - { - "state_group": state_group, - "room_id": room_id, - "type": key[0], - "state_key": key[1], - "event_id": state_id, - } - for key, state_id in iteritems(delta_state) - ], - ) - - progress = { - "last_state_group": state_group, - "rows_inserted": rows_inserted + batch_size, - "max_group": max_group, - } - - self.db.updates._background_update_progress_txn( - txn, self.STATE_GROUP_DEDUPLICATION_UPDATE_NAME, progress - ) - - return False, batch_size - - finished, result = yield self.db.runInteraction( - self.STATE_GROUP_DEDUPLICATION_UPDATE_NAME, reindex_txn - ) - - if finished: - yield self.db.updates._end_background_update( - self.STATE_GROUP_DEDUPLICATION_UPDATE_NAME - ) - - return result * BATCH_SIZE_SCALE_FACTOR - - @defer.inlineCallbacks - def _background_index_state(self, progress, batch_size): - def reindex_txn(conn): - conn.rollback() - if isinstance(self.database_engine, PostgresEngine): - # postgres insists on autocommit for the index - conn.set_session(autocommit=True) - try: - txn = conn.cursor() - txn.execute( - "CREATE INDEX CONCURRENTLY state_groups_state_type_idx" - " ON state_groups_state(state_group, type, state_key)" - ) - txn.execute("DROP INDEX IF EXISTS state_groups_state_id") - finally: - conn.set_session(autocommit=False) - else: - txn = conn.cursor() - txn.execute( - "CREATE INDEX state_groups_state_type_idx" - " ON state_groups_state(state_group, type, state_key)" - ) - txn.execute("DROP INDEX IF EXISTS state_groups_state_id") - - yield self.db.runWithConnection(reindex_txn) - - yield self.db.updates._end_background_update(self.STATE_GROUP_INDEX_UPDATE_NAME) - - return 1 - - -class StateStore(StateGroupWorkerStore, StateBackgroundUpdateStore): +class StateStore(StateGroupWorkerStore, MainStateBackgroundUpdateStore): """ Keeps track of the state at a given event. This is done by the concept of `state groups`. Every event is a assigned diff --git a/synapse/storage/data_stores/state/__init__.py b/synapse/storage/data_stores/state/__init__.py new file mode 100644 index 0000000000..86e09f6229 --- /dev/null +++ b/synapse/storage/data_stores/state/__init__.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from synapse.storage.data_stores.state.store import StateGroupDataStore # noqa: F401 diff --git a/synapse/storage/data_stores/state/bg_updates.py b/synapse/storage/data_stores/state/bg_updates.py new file mode 100644 index 0000000000..e8edaf9f7b --- /dev/null +++ b/synapse/storage/data_stores/state/bg_updates.py @@ -0,0 +1,374 @@ +# -*- coding: utf-8 -*- +# Copyright 2014-2016 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from six import iteritems + +from twisted.internet import defer + +from synapse.storage._base import SQLBaseStore +from synapse.storage.database import Database +from synapse.storage.engines import PostgresEngine +from synapse.storage.state import StateFilter + +logger = logging.getLogger(__name__) + + +MAX_STATE_DELTA_HOPS = 100 + + +class StateGroupBackgroundUpdateStore(SQLBaseStore): + """Defines functions related to state groups needed to run the state backgroud + updates. + """ + + def _count_state_group_hops_txn(self, txn, state_group): + """Given a state group, count how many hops there are in the tree. + + This is used to ensure the delta chains don't get too long. + """ + if isinstance(self.database_engine, PostgresEngine): + sql = """ + WITH RECURSIVE state(state_group) AS ( + VALUES(?::bigint) + UNION ALL + SELECT prev_state_group FROM state_group_edges e, state s + WHERE s.state_group = e.state_group + ) + SELECT count(*) FROM state; + """ + + txn.execute(sql, (state_group,)) + row = txn.fetchone() + if row and row[0]: + return row[0] + else: + return 0 + else: + # We don't use WITH RECURSIVE on sqlite3 as there are distributions + # that ship with an sqlite3 version that doesn't support it (e.g. wheezy) + next_group = state_group + count = 0 + + while next_group: + next_group = self.db.simple_select_one_onecol_txn( + txn, + table="state_group_edges", + keyvalues={"state_group": next_group}, + retcol="prev_state_group", + allow_none=True, + ) + if next_group: + count += 1 + + return count + + def _get_state_groups_from_groups_txn( + self, txn, groups, state_filter=StateFilter.all() + ): + results = {group: {} for group in groups} + + where_clause, where_args = state_filter.make_sql_filter_clause() + + # Unless the filter clause is empty, we're going to append it after an + # existing where clause + if where_clause: + where_clause = " AND (%s)" % (where_clause,) + + if isinstance(self.database_engine, PostgresEngine): + # Temporarily disable sequential scans in this transaction. This is + # a temporary hack until we can add the right indices in + txn.execute("SET LOCAL enable_seqscan=off") + + # The below query walks the state_group tree so that the "state" + # table includes all state_groups in the tree. It then joins + # against `state_groups_state` to fetch the latest state. + # It assumes that previous state groups are always numerically + # lesser. + # The PARTITION is used to get the event_id in the greatest state + # group for the given type, state_key. + # This may return multiple rows per (type, state_key), but last_value + # should be the same. + sql = """ + WITH RECURSIVE state(state_group) AS ( + VALUES(?::bigint) + UNION ALL + SELECT prev_state_group FROM state_group_edges e, state s + WHERE s.state_group = e.state_group + ) + SELECT DISTINCT type, state_key, last_value(event_id) OVER ( + PARTITION BY type, state_key ORDER BY state_group ASC + ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING + ) AS event_id FROM state_groups_state + WHERE state_group IN ( + SELECT state_group FROM state + ) + """ + + for group in groups: + args = [group] + args.extend(where_args) + + txn.execute(sql + where_clause, args) + for row in txn: + typ, state_key, event_id = row + key = (typ, state_key) + results[group][key] = event_id + else: + max_entries_returned = state_filter.max_entries_returned() + + # We don't use WITH RECURSIVE on sqlite3 as there are distributions + # that ship with an sqlite3 version that doesn't support it (e.g. wheezy) + for group in groups: + next_group = group + + while next_group: + # We did this before by getting the list of group ids, and + # then passing that list to sqlite to get latest event for + # each (type, state_key). However, that was terribly slow + # without the right indices (which we can't add until + # after we finish deduping state, which requires this func) + args = [next_group] + args.extend(where_args) + + txn.execute( + "SELECT type, state_key, event_id FROM state_groups_state" + " WHERE state_group = ? " + where_clause, + args, + ) + results[group].update( + ((typ, state_key), event_id) + for typ, state_key, event_id in txn + if (typ, state_key) not in results[group] + ) + + # If the number of entries in the (type,state_key)->event_id dict + # matches the number of (type,state_keys) types we were searching + # for, then we must have found them all, so no need to go walk + # further down the tree... UNLESS our types filter contained + # wildcards (i.e. Nones) in which case we have to do an exhaustive + # search + if ( + max_entries_returned is not None + and len(results[group]) == max_entries_returned + ): + break + + next_group = self.db.simple_select_one_onecol_txn( + txn, + table="state_group_edges", + keyvalues={"state_group": next_group}, + retcol="prev_state_group", + allow_none=True, + ) + + return results + + +class StateBackgroundUpdateStore(StateGroupBackgroundUpdateStore): + + STATE_GROUP_DEDUPLICATION_UPDATE_NAME = "state_group_state_deduplication" + STATE_GROUP_INDEX_UPDATE_NAME = "state_group_state_type_index" + STATE_GROUPS_ROOM_INDEX_UPDATE_NAME = "state_groups_room_id_idx" + + def __init__(self, database: Database, db_conn, hs): + super(StateBackgroundUpdateStore, self).__init__(database, db_conn, hs) + self.db.updates.register_background_update_handler( + self.STATE_GROUP_DEDUPLICATION_UPDATE_NAME, + self._background_deduplicate_state, + ) + self.db.updates.register_background_update_handler( + self.STATE_GROUP_INDEX_UPDATE_NAME, self._background_index_state + ) + self.db.updates.register_background_index_update( + self.STATE_GROUPS_ROOM_INDEX_UPDATE_NAME, + index_name="state_groups_room_id_idx", + table="state_groups", + columns=["room_id"], + ) + + @defer.inlineCallbacks + def _background_deduplicate_state(self, progress, batch_size): + """This background update will slowly deduplicate state by reencoding + them as deltas. + """ + last_state_group = progress.get("last_state_group", 0) + rows_inserted = progress.get("rows_inserted", 0) + max_group = progress.get("max_group", None) + + BATCH_SIZE_SCALE_FACTOR = 100 + + batch_size = max(1, int(batch_size / BATCH_SIZE_SCALE_FACTOR)) + + if max_group is None: + rows = yield self.db.execute( + "_background_deduplicate_state", + None, + "SELECT coalesce(max(id), 0) FROM state_groups", + ) + max_group = rows[0][0] + + def reindex_txn(txn): + new_last_state_group = last_state_group + for count in range(batch_size): + txn.execute( + "SELECT id, room_id FROM state_groups" + " WHERE ? < id AND id <= ?" + " ORDER BY id ASC" + " LIMIT 1", + (new_last_state_group, max_group), + ) + row = txn.fetchone() + if row: + state_group, room_id = row + + if not row or not state_group: + return True, count + + txn.execute( + "SELECT state_group FROM state_group_edges" + " WHERE state_group = ?", + (state_group,), + ) + + # If we reach a point where we've already started inserting + # edges we should stop. + if txn.fetchall(): + return True, count + + txn.execute( + "SELECT coalesce(max(id), 0) FROM state_groups" + " WHERE id < ? AND room_id = ?", + (state_group, room_id), + ) + (prev_group,) = txn.fetchone() + new_last_state_group = state_group + + if prev_group: + potential_hops = self._count_state_group_hops_txn(txn, prev_group) + if potential_hops >= MAX_STATE_DELTA_HOPS: + # We want to ensure chains are at most this long,# + # otherwise read performance degrades. + continue + + prev_state = self._get_state_groups_from_groups_txn( + txn, [prev_group] + ) + prev_state = prev_state[prev_group] + + curr_state = self._get_state_groups_from_groups_txn( + txn, [state_group] + ) + curr_state = curr_state[state_group] + + if not set(prev_state.keys()) - set(curr_state.keys()): + # We can only do a delta if the current has a strict super set + # of keys + + delta_state = { + key: value + for key, value in iteritems(curr_state) + if prev_state.get(key, None) != value + } + + self.db.simple_delete_txn( + txn, + table="state_group_edges", + keyvalues={"state_group": state_group}, + ) + + self.db.simple_insert_txn( + txn, + table="state_group_edges", + values={ + "state_group": state_group, + "prev_state_group": prev_group, + }, + ) + + self.db.simple_delete_txn( + txn, + table="state_groups_state", + keyvalues={"state_group": state_group}, + ) + + self.db.simple_insert_many_txn( + txn, + table="state_groups_state", + values=[ + { + "state_group": state_group, + "room_id": room_id, + "type": key[0], + "state_key": key[1], + "event_id": state_id, + } + for key, state_id in iteritems(delta_state) + ], + ) + + progress = { + "last_state_group": state_group, + "rows_inserted": rows_inserted + batch_size, + "max_group": max_group, + } + + self.db.updates._background_update_progress_txn( + txn, self.STATE_GROUP_DEDUPLICATION_UPDATE_NAME, progress + ) + + return False, batch_size + + finished, result = yield self.db.runInteraction( + self.STATE_GROUP_DEDUPLICATION_UPDATE_NAME, reindex_txn + ) + + if finished: + yield self.db.updates._end_background_update( + self.STATE_GROUP_DEDUPLICATION_UPDATE_NAME + ) + + return result * BATCH_SIZE_SCALE_FACTOR + + @defer.inlineCallbacks + def _background_index_state(self, progress, batch_size): + def reindex_txn(conn): + conn.rollback() + if isinstance(self.database_engine, PostgresEngine): + # postgres insists on autocommit for the index + conn.set_session(autocommit=True) + try: + txn = conn.cursor() + txn.execute( + "CREATE INDEX CONCURRENTLY state_groups_state_type_idx" + " ON state_groups_state(state_group, type, state_key)" + ) + txn.execute("DROP INDEX IF EXISTS state_groups_state_id") + finally: + conn.set_session(autocommit=False) + else: + txn = conn.cursor() + txn.execute( + "CREATE INDEX state_groups_state_type_idx" + " ON state_groups_state(state_group, type, state_key)" + ) + txn.execute("DROP INDEX IF EXISTS state_groups_state_id") + + yield self.db.runWithConnection(reindex_txn) + + yield self.db.updates._end_background_update(self.STATE_GROUP_INDEX_UPDATE_NAME) + + return 1 diff --git a/synapse/storage/data_stores/main/schema/delta/23/drop_state_index.sql b/synapse/storage/data_stores/state/schema/delta/23/drop_state_index.sql similarity index 100% rename from synapse/storage/data_stores/main/schema/delta/23/drop_state_index.sql rename to synapse/storage/data_stores/state/schema/delta/23/drop_state_index.sql diff --git a/synapse/storage/data_stores/main/schema/delta/30/state_stream.sql b/synapse/storage/data_stores/state/schema/delta/30/state_stream.sql similarity index 100% rename from synapse/storage/data_stores/main/schema/delta/30/state_stream.sql rename to synapse/storage/data_stores/state/schema/delta/30/state_stream.sql diff --git a/synapse/storage/data_stores/state/schema/delta/32/remove_state_indices.sql b/synapse/storage/data_stores/state/schema/delta/32/remove_state_indices.sql new file mode 100644 index 0000000000..1450313bfa --- /dev/null +++ b/synapse/storage/data_stores/state/schema/delta/32/remove_state_indices.sql @@ -0,0 +1,19 @@ +/* Copyright 2016 OpenMarket Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +-- The following indices are redundant, other indices are equivalent or +-- supersets +DROP INDEX IF EXISTS state_groups_id; -- Duplicate of PRIMARY KEY diff --git a/synapse/storage/data_stores/main/schema/delta/35/add_state_index.sql b/synapse/storage/data_stores/state/schema/delta/35/add_state_index.sql similarity index 100% rename from synapse/storage/data_stores/main/schema/delta/35/add_state_index.sql rename to synapse/storage/data_stores/state/schema/delta/35/add_state_index.sql diff --git a/synapse/storage/data_stores/main/schema/delta/35/state.sql b/synapse/storage/data_stores/state/schema/delta/35/state.sql similarity index 100% rename from synapse/storage/data_stores/main/schema/delta/35/state.sql rename to synapse/storage/data_stores/state/schema/delta/35/state.sql diff --git a/synapse/storage/data_stores/main/schema/delta/35/state_dedupe.sql b/synapse/storage/data_stores/state/schema/delta/35/state_dedupe.sql similarity index 100% rename from synapse/storage/data_stores/main/schema/delta/35/state_dedupe.sql rename to synapse/storage/data_stores/state/schema/delta/35/state_dedupe.sql diff --git a/synapse/storage/data_stores/main/schema/delta/47/state_group_seq.py b/synapse/storage/data_stores/state/schema/delta/47/state_group_seq.py similarity index 100% rename from synapse/storage/data_stores/main/schema/delta/47/state_group_seq.py rename to synapse/storage/data_stores/state/schema/delta/47/state_group_seq.py diff --git a/synapse/storage/data_stores/state/schema/delta/56/state_group_room_idx.sql b/synapse/storage/data_stores/state/schema/delta/56/state_group_room_idx.sql new file mode 100644 index 0000000000..7916ef18b2 --- /dev/null +++ b/synapse/storage/data_stores/state/schema/delta/56/state_group_room_idx.sql @@ -0,0 +1,17 @@ +/* Copyright 2019 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +INSERT INTO background_updates (update_name, progress_json) VALUES + ('state_groups_room_id_idx', '{}'); diff --git a/synapse/storage/data_stores/state/schema/full_schemas/54/full.sql b/synapse/storage/data_stores/state/schema/full_schemas/54/full.sql new file mode 100644 index 0000000000..35f97d6b3d --- /dev/null +++ b/synapse/storage/data_stores/state/schema/full_schemas/54/full.sql @@ -0,0 +1,37 @@ +/* Copyright 2019 The Matrix.org Foundation C.I.C + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +CREATE TABLE state_groups ( + id BIGINT PRIMARY KEY, + room_id TEXT NOT NULL, + event_id TEXT NOT NULL +); + +CREATE TABLE state_groups_state ( + state_group BIGINT NOT NULL, + room_id TEXT NOT NULL, + type TEXT NOT NULL, + state_key TEXT NOT NULL, + event_id TEXT NOT NULL +); + +CREATE TABLE state_group_edges ( + state_group BIGINT NOT NULL, + prev_state_group BIGINT NOT NULL +); + +CREATE INDEX state_group_edges_idx ON state_group_edges (state_group); +CREATE INDEX state_group_edges_prev_idx ON state_group_edges (prev_state_group); +CREATE INDEX state_groups_state_type_idx ON state_groups_state (state_group, type, state_key); diff --git a/synapse/storage/data_stores/state/schema/full_schemas/54/sequence.sql.postgres b/synapse/storage/data_stores/state/schema/full_schemas/54/sequence.sql.postgres new file mode 100644 index 0000000000..fcd926c9fb --- /dev/null +++ b/synapse/storage/data_stores/state/schema/full_schemas/54/sequence.sql.postgres @@ -0,0 +1,21 @@ +/* Copyright 2019 The Matrix.org Foundation C.I.C + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +CREATE SEQUENCE state_group_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; diff --git a/synapse/storage/data_stores/state/store.py b/synapse/storage/data_stores/state/store.py new file mode 100644 index 0000000000..d53695f238 --- /dev/null +++ b/synapse/storage/data_stores/state/store.py @@ -0,0 +1,640 @@ +# -*- coding: utf-8 -*- +# Copyright 2014-2016 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +from collections import namedtuple + +from six import iteritems +from six.moves import range + +from twisted.internet import defer + +from synapse.api.constants import EventTypes +from synapse.storage._base import SQLBaseStore +from synapse.storage.data_stores.state.bg_updates import StateBackgroundUpdateStore +from synapse.storage.database import Database +from synapse.storage.state import StateFilter +from synapse.util.caches import get_cache_factor_for +from synapse.util.caches.descriptors import cached +from synapse.util.caches.dictionary_cache import DictionaryCache + +logger = logging.getLogger(__name__) + + +MAX_STATE_DELTA_HOPS = 100 + + +class _GetStateGroupDelta( + namedtuple("_GetStateGroupDelta", ("prev_group", "delta_ids")) +): + """Return type of get_state_group_delta that implements __len__, which lets + us use the itrable flag when caching + """ + + __slots__ = [] + + def __len__(self): + return len(self.delta_ids) if self.delta_ids else 0 + + +class StateGroupDataStore(StateBackgroundUpdateStore, SQLBaseStore): + """A data store for fetching/storing state groups. + """ + + def __init__(self, database: Database, db_conn, hs): + super(StateGroupDataStore, self).__init__(database, db_conn, hs) + + # Originally the state store used a single DictionaryCache to cache the + # event IDs for the state types in a given state group to avoid hammering + # on the state_group* tables. + # + # The point of using a DictionaryCache is that it can cache a subset + # of the state events for a given state group (i.e. a subset of the keys for a + # given dict which is an entry in the cache for a given state group ID). + # + # However, this poses problems when performing complicated queries + # on the store - for instance: "give me all the state for this group, but + # limit members to this subset of users", as DictionaryCache's API isn't + # rich enough to say "please cache any of these fields, apart from this subset". + # This is problematic when lazy loading members, which requires this behaviour, + # as without it the cache has no choice but to speculatively load all + # state events for the group, which negates the efficiency being sought. + # + # Rather than overcomplicating DictionaryCache's API, we instead split the + # state_group_cache into two halves - one for tracking non-member events, + # and the other for tracking member_events. This means that lazy loading + # queries can be made in a cache-friendly manner by querying both caches + # separately and then merging the result. So for the example above, you + # would query the members cache for a specific subset of state keys + # (which DictionaryCache will handle efficiently and fine) and the non-members + # cache for all state (which DictionaryCache will similarly handle fine) + # and then just merge the results together. + # + # We size the non-members cache to be smaller than the members cache as the + # vast majority of state in Matrix (today) is member events. + + self._state_group_cache = DictionaryCache( + "*stateGroupCache*", + # TODO: this hasn't been tuned yet + 50000 * get_cache_factor_for("stateGroupCache"), + ) + self._state_group_members_cache = DictionaryCache( + "*stateGroupMembersCache*", + 500000 * get_cache_factor_for("stateGroupMembersCache"), + ) + + @cached(max_entries=10000, iterable=True) + def get_state_group_delta(self, state_group): + """Given a state group try to return a previous group and a delta between + the old and the new. + + Returns: + (prev_group, delta_ids), where both may be None. + """ + + def _get_state_group_delta_txn(txn): + prev_group = self.db.simple_select_one_onecol_txn( + txn, + table="state_group_edges", + keyvalues={"state_group": state_group}, + retcol="prev_state_group", + allow_none=True, + ) + + if not prev_group: + return _GetStateGroupDelta(None, None) + + delta_ids = self.db.simple_select_list_txn( + txn, + table="state_groups_state", + keyvalues={"state_group": state_group}, + retcols=("type", "state_key", "event_id"), + ) + + return _GetStateGroupDelta( + prev_group, + {(row["type"], row["state_key"]): row["event_id"] for row in delta_ids}, + ) + + return self.db.runInteraction( + "get_state_group_delta", _get_state_group_delta_txn + ) + + @defer.inlineCallbacks + def _get_state_groups_from_groups(self, groups, state_filter): + """Returns the state groups for a given set of groups, filtering on + types of state events. + + Args: + groups(list[int]): list of state group IDs to query + state_filter (StateFilter): The state filter used to fetch state + from the database. + Returns: + Deferred[dict[int, dict[tuple[str, str], str]]]: + dict of state_group_id -> (dict of (type, state_key) -> event id) + """ + results = {} + + chunks = [groups[i : i + 100] for i in range(0, len(groups), 100)] + for chunk in chunks: + res = yield self.db.runInteraction( + "_get_state_groups_from_groups", + self._get_state_groups_from_groups_txn, + chunk, + state_filter, + ) + results.update(res) + + return results + + def _get_state_for_group_using_cache(self, cache, group, state_filter): + """Checks if group is in cache. See `_get_state_for_groups` + + Args: + cache(DictionaryCache): the state group cache to use + group(int): The state group to lookup + state_filter (StateFilter): The state filter used to fetch state + from the database. + + Returns 2-tuple (`state_dict`, `got_all`). + `got_all` is a bool indicating if we successfully retrieved all + requests state from the cache, if False we need to query the DB for the + missing state. + """ + is_all, known_absent, state_dict_ids = cache.get(group) + + if is_all or state_filter.is_full(): + # Either we have everything or want everything, either way + # `is_all` tells us whether we've gotten everything. + return state_filter.filter_state(state_dict_ids), is_all + + # tracks whether any of our requested types are missing from the cache + missing_types = False + + if state_filter.has_wildcards(): + # We don't know if we fetched all the state keys for the types in + # the filter that are wildcards, so we have to assume that we may + # have missed some. + missing_types = True + else: + # There aren't any wild cards, so `concrete_types()` returns the + # complete list of event types we're wanting. + for key in state_filter.concrete_types(): + if key not in state_dict_ids and key not in known_absent: + missing_types = True + break + + return state_filter.filter_state(state_dict_ids), not missing_types + + @defer.inlineCallbacks + def _get_state_for_groups(self, groups, state_filter=StateFilter.all()): + """Gets the state at each of a list of state groups, optionally + filtering by type/state_key + + Args: + groups (iterable[int]): list of state groups for which we want + to get the state. + state_filter (StateFilter): The state filter used to fetch state + from the database. + Returns: + Deferred[dict[int, dict[tuple[str, str], str]]]: + dict of state_group_id -> (dict of (type, state_key) -> event id) + """ + + member_filter, non_member_filter = state_filter.get_member_split() + + # Now we look them up in the member and non-member caches + ( + non_member_state, + incomplete_groups_nm, + ) = yield self._get_state_for_groups_using_cache( + groups, self._state_group_cache, state_filter=non_member_filter + ) + + ( + member_state, + incomplete_groups_m, + ) = yield self._get_state_for_groups_using_cache( + groups, self._state_group_members_cache, state_filter=member_filter + ) + + state = dict(non_member_state) + for group in groups: + state[group].update(member_state[group]) + + # Now fetch any missing groups from the database + + incomplete_groups = incomplete_groups_m | incomplete_groups_nm + + if not incomplete_groups: + return state + + cache_sequence_nm = self._state_group_cache.sequence + cache_sequence_m = self._state_group_members_cache.sequence + + # Help the cache hit ratio by expanding the filter a bit + db_state_filter = state_filter.return_expanded() + + group_to_state_dict = yield self._get_state_groups_from_groups( + list(incomplete_groups), state_filter=db_state_filter + ) + + # Now lets update the caches + self._insert_into_cache( + group_to_state_dict, + db_state_filter, + cache_seq_num_members=cache_sequence_m, + cache_seq_num_non_members=cache_sequence_nm, + ) + + # And finally update the result dict, by filtering out any extra + # stuff we pulled out of the database. + for group, group_state_dict in iteritems(group_to_state_dict): + # We just replace any existing entries, as we will have loaded + # everything we need from the database anyway. + state[group] = state_filter.filter_state(group_state_dict) + + return state + + def _get_state_for_groups_using_cache(self, groups, cache, state_filter): + """Gets the state at each of a list of state groups, optionally + filtering by type/state_key, querying from a specific cache. + + Args: + groups (iterable[int]): list of state groups for which we want + to get the state. + cache (DictionaryCache): the cache of group ids to state dicts which + we will pass through - either the normal state cache or the specific + members state cache. + state_filter (StateFilter): The state filter used to fetch state + from the database. + + Returns: + tuple[dict[int, dict[tuple[str, str], str]], set[int]]: Tuple of + dict of state_group_id -> (dict of (type, state_key) -> event id) + of entries in the cache, and the state group ids either missing + from the cache or incomplete. + """ + results = {} + incomplete_groups = set() + for group in set(groups): + state_dict_ids, got_all = self._get_state_for_group_using_cache( + cache, group, state_filter + ) + results[group] = state_dict_ids + + if not got_all: + incomplete_groups.add(group) + + return results, incomplete_groups + + def _insert_into_cache( + self, + group_to_state_dict, + state_filter, + cache_seq_num_members, + cache_seq_num_non_members, + ): + """Inserts results from querying the database into the relevant cache. + + Args: + group_to_state_dict (dict): The new entries pulled from database. + Map from state group to state dict + state_filter (StateFilter): The state filter used to fetch state + from the database. + cache_seq_num_members (int): Sequence number of member cache since + last lookup in cache + cache_seq_num_non_members (int): Sequence number of member cache since + last lookup in cache + """ + + # We need to work out which types we've fetched from the DB for the + # member vs non-member caches. This should be as accurate as possible, + # but can be an underestimate (e.g. when we have wild cards) + + member_filter, non_member_filter = state_filter.get_member_split() + if member_filter.is_full(): + # We fetched all member events + member_types = None + else: + # `concrete_types()` will only return a subset when there are wild + # cards in the filter, but that's fine. + member_types = member_filter.concrete_types() + + if non_member_filter.is_full(): + # We fetched all non member events + non_member_types = None + else: + non_member_types = non_member_filter.concrete_types() + + for group, group_state_dict in iteritems(group_to_state_dict): + state_dict_members = {} + state_dict_non_members = {} + + for k, v in iteritems(group_state_dict): + if k[0] == EventTypes.Member: + state_dict_members[k] = v + else: + state_dict_non_members[k] = v + + self._state_group_members_cache.update( + cache_seq_num_members, + key=group, + value=state_dict_members, + fetched_keys=member_types, + ) + + self._state_group_cache.update( + cache_seq_num_non_members, + key=group, + value=state_dict_non_members, + fetched_keys=non_member_types, + ) + + def store_state_group( + self, event_id, room_id, prev_group, delta_ids, current_state_ids + ): + """Store a new set of state, returning a newly assigned state group. + + Args: + event_id (str): The event ID for which the state was calculated + room_id (str) + prev_group (int|None): A previous state group for the room, optional. + delta_ids (dict|None): The delta between state at `prev_group` and + `current_state_ids`, if `prev_group` was given. Same format as + `current_state_ids`. + current_state_ids (dict): The state to store. Map of (type, state_key) + to event_id. + + Returns: + Deferred[int]: The state group ID + """ + + def _store_state_group_txn(txn): + if current_state_ids is None: + # AFAIK, this can never happen + raise Exception("current_state_ids cannot be None") + + state_group = self.database_engine.get_next_state_group_id(txn) + + self.db.simple_insert_txn( + txn, + table="state_groups", + values={"id": state_group, "room_id": room_id, "event_id": event_id}, + ) + + # We persist as a delta if we can, while also ensuring the chain + # of deltas isn't tooo long, as otherwise read performance degrades. + if prev_group: + is_in_db = self.db.simple_select_one_onecol_txn( + txn, + table="state_groups", + keyvalues={"id": prev_group}, + retcol="id", + allow_none=True, + ) + if not is_in_db: + raise Exception( + "Trying to persist state with unpersisted prev_group: %r" + % (prev_group,) + ) + + potential_hops = self._count_state_group_hops_txn(txn, prev_group) + if prev_group and potential_hops < MAX_STATE_DELTA_HOPS: + self.db.simple_insert_txn( + txn, + table="state_group_edges", + values={"state_group": state_group, "prev_state_group": prev_group}, + ) + + self.db.simple_insert_many_txn( + txn, + table="state_groups_state", + values=[ + { + "state_group": state_group, + "room_id": room_id, + "type": key[0], + "state_key": key[1], + "event_id": state_id, + } + for key, state_id in iteritems(delta_ids) + ], + ) + else: + self.db.simple_insert_many_txn( + txn, + table="state_groups_state", + values=[ + { + "state_group": state_group, + "room_id": room_id, + "type": key[0], + "state_key": key[1], + "event_id": state_id, + } + for key, state_id in iteritems(current_state_ids) + ], + ) + + # Prefill the state group caches with this group. + # It's fine to use the sequence like this as the state group map + # is immutable. (If the map wasn't immutable then this prefill could + # race with another update) + + current_member_state_ids = { + s: ev + for (s, ev) in iteritems(current_state_ids) + if s[0] == EventTypes.Member + } + txn.call_after( + self._state_group_members_cache.update, + self._state_group_members_cache.sequence, + key=state_group, + value=dict(current_member_state_ids), + ) + + current_non_member_state_ids = { + s: ev + for (s, ev) in iteritems(current_state_ids) + if s[0] != EventTypes.Member + } + txn.call_after( + self._state_group_cache.update, + self._state_group_cache.sequence, + key=state_group, + value=dict(current_non_member_state_ids), + ) + + return state_group + + return self.db.runInteraction("store_state_group", _store_state_group_txn) + + def purge_unreferenced_state_groups( + self, room_id: str, state_groups_to_delete + ) -> defer.Deferred: + """Deletes no longer referenced state groups and de-deltas any state + groups that reference them. + + Args: + room_id: The room the state groups belong to (must all be in the + same room). + state_groups_to_delete (Collection[int]): Set of all state groups + to delete. + """ + + return self.db.runInteraction( + "purge_unreferenced_state_groups", + self._purge_unreferenced_state_groups, + room_id, + state_groups_to_delete, + ) + + def _purge_unreferenced_state_groups(self, txn, room_id, state_groups_to_delete): + logger.info( + "[purge] found %i state groups to delete", len(state_groups_to_delete) + ) + + rows = self.db.simple_select_many_txn( + txn, + table="state_group_edges", + column="prev_state_group", + iterable=state_groups_to_delete, + keyvalues={}, + retcols=("state_group",), + ) + + remaining_state_groups = set( + row["state_group"] + for row in rows + if row["state_group"] not in state_groups_to_delete + ) + + logger.info( + "[purge] de-delta-ing %i remaining state groups", + len(remaining_state_groups), + ) + + # Now we turn the state groups that reference to-be-deleted state + # groups to non delta versions. + for sg in remaining_state_groups: + logger.info("[purge] de-delta-ing remaining state group %s", sg) + curr_state = self._get_state_groups_from_groups_txn(txn, [sg]) + curr_state = curr_state[sg] + + self.db.simple_delete_txn( + txn, table="state_groups_state", keyvalues={"state_group": sg} + ) + + self.db.simple_delete_txn( + txn, table="state_group_edges", keyvalues={"state_group": sg} + ) + + self.db.simple_insert_many_txn( + txn, + table="state_groups_state", + values=[ + { + "state_group": sg, + "room_id": room_id, + "type": key[0], + "state_key": key[1], + "event_id": state_id, + } + for key, state_id in iteritems(curr_state) + ], + ) + + logger.info("[purge] removing redundant state groups") + txn.executemany( + "DELETE FROM state_groups_state WHERE state_group = ?", + ((sg,) for sg in state_groups_to_delete), + ) + txn.executemany( + "DELETE FROM state_groups WHERE id = ?", + ((sg,) for sg in state_groups_to_delete), + ) + + @defer.inlineCallbacks + def get_previous_state_groups(self, state_groups): + """Fetch the previous groups of the given state groups. + + Args: + state_groups (Iterable[int]) + + Returns: + Deferred[dict[int, int]]: mapping from state group to previous + state group. + """ + + rows = yield self.db.simple_select_many_batch( + table="state_group_edges", + column="prev_state_group", + iterable=state_groups, + keyvalues={}, + retcols=("prev_state_group", "state_group"), + desc="get_previous_state_groups", + ) + + return {row["state_group"]: row["prev_state_group"] for row in rows} + + def purge_room_state(self, room_id, state_groups_to_delete): + """Deletes all record of a room from state tables + + Args: + room_id (str): + state_groups_to_delete (list[int]): State groups to delete + """ + + return self.db.runInteraction( + "purge_room_state", + self._purge_room_state_txn, + room_id, + state_groups_to_delete, + ) + + def _purge_room_state_txn(self, txn, room_id, state_groups_to_delete): + # first we have to delete the state groups states + logger.info("[purge] removing %s from state_groups_state", room_id) + + self.db.simple_delete_many_txn( + txn, + table="state_groups_state", + column="state_group", + iterable=state_groups_to_delete, + keyvalues={}, + ) + + # ... and the state group edges + logger.info("[purge] removing %s from state_group_edges", room_id) + + self.db.simple_delete_many_txn( + txn, + table="state_group_edges", + column="state_group", + iterable=state_groups_to_delete, + keyvalues={}, + ) + + # ... and the state groups + logger.info("[purge] removing %s from state_groups", room_id) + + self.db.simple_delete_many_txn( + txn, + table="state_groups", + column="id", + iterable=state_groups_to_delete, + keyvalues={}, + ) diff --git a/synapse/storage/persist_events.py b/synapse/storage/persist_events.py index fa03ca9ff7..1ed44925fc 100644 --- a/synapse/storage/persist_events.py +++ b/synapse/storage/persist_events.py @@ -183,7 +183,7 @@ class EventsPersistenceStorage(object): # so we use separate variables here even though they point to the same # store for now. self.main_store = stores.main - self.state_store = stores.main + self.state_store = stores.state self._clock = hs.get_clock() self.is_mine_id = hs.is_mine_id diff --git a/synapse/storage/prepare_database.py b/synapse/storage/prepare_database.py index 403848ad03..e70026b80a 100644 --- a/synapse/storage/prepare_database.py +++ b/synapse/storage/prepare_database.py @@ -42,7 +42,7 @@ class UpgradeDatabaseException(PrepareDatabaseException): pass -def prepare_database(db_conn, database_engine, config, data_stores=["main"]): +def prepare_database(db_conn, database_engine, config, data_stores=["main", "state"]): """Prepares a database for usage. Will either create all necessary tables or upgrade from an older schema version. diff --git a/synapse/storage/purge_events.py b/synapse/storage/purge_events.py index a368182034..d6a7bd7834 100644 --- a/synapse/storage/purge_events.py +++ b/synapse/storage/purge_events.py @@ -58,7 +58,7 @@ class PurgeEventsStorage(object): sg_to_delete = yield self._find_unreferenced_groups(state_groups) - yield self.stores.main.purge_unreferenced_state_groups(room_id, sg_to_delete) + yield self.stores.state.purge_unreferenced_state_groups(room_id, sg_to_delete) @defer.inlineCallbacks def _find_unreferenced_groups(self, state_groups): @@ -102,7 +102,7 @@ class PurgeEventsStorage(object): # groups that are referenced. current_search -= referenced - edges = yield self.stores.main.get_previous_state_groups(current_search) + edges = yield self.stores.state.get_previous_state_groups(current_search) prevs = set(edges.values()) # We don't bother re-handling groups we've already seen diff --git a/synapse/storage/state.py b/synapse/storage/state.py index 3735846899..cbeb586014 100644 --- a/synapse/storage/state.py +++ b/synapse/storage/state.py @@ -342,7 +342,7 @@ class StateGroupStorage(object): (prev_group, delta_ids) """ - return self.stores.main.get_state_group_delta(state_group) + return self.stores.state.get_state_group_delta(state_group) @defer.inlineCallbacks def get_state_groups_ids(self, _room_id, event_ids): @@ -362,7 +362,7 @@ class StateGroupStorage(object): event_to_groups = yield self.stores.main._get_state_group_for_events(event_ids) groups = set(itervalues(event_to_groups)) - group_to_state = yield self.stores.main._get_state_for_groups(groups) + group_to_state = yield self.stores.state._get_state_for_groups(groups) return group_to_state @@ -423,7 +423,7 @@ class StateGroupStorage(object): dict of state_group_id -> (dict of (type, state_key) -> event id) """ - return self.stores.main._get_state_groups_from_groups(groups, state_filter) + return self.stores.state._get_state_groups_from_groups(groups, state_filter) @defer.inlineCallbacks def get_state_for_events(self, event_ids, state_filter=StateFilter.all()): @@ -439,7 +439,7 @@ class StateGroupStorage(object): event_to_groups = yield self.stores.main._get_state_group_for_events(event_ids) groups = set(itervalues(event_to_groups)) - group_to_state = yield self.stores.main._get_state_for_groups( + group_to_state = yield self.stores.state._get_state_for_groups( groups, state_filter ) @@ -476,7 +476,7 @@ class StateGroupStorage(object): event_to_groups = yield self.stores.main._get_state_group_for_events(event_ids) groups = set(itervalues(event_to_groups)) - group_to_state = yield self.stores.main._get_state_for_groups( + group_to_state = yield self.stores.state._get_state_for_groups( groups, state_filter ) @@ -532,7 +532,7 @@ class StateGroupStorage(object): Deferred[dict[int, dict[tuple[str, str], str]]]: dict of state_group_id -> (dict of (type, state_key) -> event id) """ - return self.stores.main._get_state_for_groups(groups, state_filter) + return self.stores.state._get_state_for_groups(groups, state_filter) def store_state_group( self, event_id, room_id, prev_group, delta_ids, current_state_ids @@ -552,6 +552,6 @@ class StateGroupStorage(object): Returns: Deferred[int]: The state group ID """ - return self.stores.main.store_state_group( + return self.stores.state.store_state_group( event_id, room_id, prev_group, delta_ids, current_state_ids ) diff --git a/tests/storage/test_state.py b/tests/storage/test_state.py index 43200654f1..d6ecf102f8 100644 --- a/tests/storage/test_state.py +++ b/tests/storage/test_state.py @@ -35,7 +35,7 @@ class StateStoreTestCase(tests.unittest.TestCase): self.store = hs.get_datastore() self.storage = hs.get_storage() - self.state_datastore = self.store + self.state_datastore = self.storage.state.stores.state self.event_builder_factory = hs.get_event_builder_factory() self.event_creation_handler = hs.get_event_creation_handler() diff --git a/tests/utils.py b/tests/utils.py index 9f5bf40b4b..e2e9cafd79 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -231,7 +231,7 @@ def setup_test_homeserver( "args": {"database": ":memory:", "cp_min": 1, "cp_max": 1}, } - database = DatabaseConnectionConfig("master", database_config, ["main"]) + database = DatabaseConnectionConfig("master", database_config) config.database.databases = [database] db_engine = create_engine(database.config) From 29794c6bc84b9ba76f041bbc8a180b7421996004 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 20 Dec 2019 10:58:07 +0000 Subject: [PATCH 0732/1623] 1.7.2 --- CHANGES.md | 12 ++++++++++++ changelog.d/6576.bugfix | 1 - changelog.d/6578.bugfix | 1 - debian/changelog | 6 ++++++ synapse/__init__.py | 2 +- 5 files changed, 19 insertions(+), 3 deletions(-) delete mode 100644 changelog.d/6576.bugfix delete mode 100644 changelog.d/6578.bugfix diff --git a/CHANGES.md b/CHANGES.md index 7927714a36..d6567e24d2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,15 @@ +Synapse 1.7.2 (2019-12-20) +========================== + +This release fixes some regressions introduced in Synapse 1.7.0 and 1.7.1. + +Bugfixes +-------- + +- Fix a regression introduced in Synapse 1.7.1 which caused errors when attempting to backfill rooms over federation. ([\#6576](https://github.com/matrix-org/synapse/issues/6576)) +- Fix a bug introduced in Synapse 1.7.0 which caused an error on startup when upgrading from versions before 1.3.0. ([\#6578](https://github.com/matrix-org/synapse/issues/6578)) + + Synapse 1.7.1 (2019-12-18) ========================== diff --git a/changelog.d/6576.bugfix b/changelog.d/6576.bugfix deleted file mode 100644 index f5414fce4d..0000000000 --- a/changelog.d/6576.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix errors when attempting to backfill rooms over federation. diff --git a/changelog.d/6578.bugfix b/changelog.d/6578.bugfix deleted file mode 100644 index fae55a4456..0000000000 --- a/changelog.d/6578.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a bug introduced in Synapse 1.7.0 which caused an error on startup when upgrading from versions before 1.3.0. diff --git a/debian/changelog b/debian/changelog index e400619eb9..2492b5db92 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.7.2) stable; urgency=medium + + * New synapse release 1.7.2. + + -- Synapse Packaging team Fri, 20 Dec 2019 10:56:50 +0000 + matrix-synapse-py3 (1.7.1) stable; urgency=medium * New synapse release 1.7.1. diff --git a/synapse/__init__.py b/synapse/__init__.py index e951bab593..996101cf09 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -36,7 +36,7 @@ try: except ImportError: pass -__version__ = "1.7.1" +__version__ = "1.7.2" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 7c6b853558feac49f1379b08a4fffb1aff1da481 Mon Sep 17 00:00:00 2001 From: dopple <54467709+doppled@users.noreply.github.com> Date: Sun, 22 Dec 2019 14:16:56 -0800 Subject: [PATCH 0733/1623] Update reverse proxy file name (#6590) s/reverse_proxy.rst/reverse_proxy.md/ --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index ae51d6ab39..2691dfc23d 100644 --- a/README.rst +++ b/README.rst @@ -393,4 +393,4 @@ something like the following in their logs:: 2019-09-11 19:32:04,271 - synapse.federation.transport.server - 288 - WARNING - GET-11752 - authenticate_request failed: 401: Invalid signature for server with key ed25519:a_EqML: Unable to verify signature for This is normally caused by a misconfiguration in your reverse-proxy. See -``_ and double-check that your settings are correct. +``_ and double-check that your settings are correct. From b2db3828418282b3059f6a0d1b9af3ffa32b52f2 Mon Sep 17 00:00:00 2001 From: dopple <54467709+doppled@users.noreply.github.com> Date: Sun, 22 Dec 2019 14:16:56 -0800 Subject: [PATCH 0734/1623] Update reverse proxy file name (#6590) s/reverse_proxy.rst/reverse_proxy.md/ --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index ae51d6ab39..2691dfc23d 100644 --- a/README.rst +++ b/README.rst @@ -393,4 +393,4 @@ something like the following in their logs:: 2019-09-11 19:32:04,271 - synapse.federation.transport.server - 288 - WARNING - GET-11752 - authenticate_request failed: 401: Invalid signature for server with key ed25519:a_EqML: Unable to verify signature for This is normally caused by a misconfiguration in your reverse-proxy. See -``_ and double-check that your settings are correct. +``_ and double-check that your settings are correct. From f03c877b32c33275f47ead9e1e01d700a8d049c0 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 24 Dec 2019 17:39:58 +0000 Subject: [PATCH 0735/1623] sample log config TODO: automate generation of this --- docs/sample_log_config.yaml | 43 +++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 docs/sample_log_config.yaml diff --git a/docs/sample_log_config.yaml b/docs/sample_log_config.yaml new file mode 100644 index 0000000000..11e8f35f41 --- /dev/null +++ b/docs/sample_log_config.yaml @@ -0,0 +1,43 @@ +# Example log config file for synapse. +# +# This is a YAML file containing a standard Python logging configuration +# dictionary. See [1] for details on the valid settings. +# +# [1]: https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema + +version: 1 + +formatters: + precise: + format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s' + +filters: + context: + (): synapse.logging.context.LoggingContextFilter + request: "" + +handlers: + file: + class: logging.handlers.RotatingFileHandler + formatter: precise + filename: /home/rav/work/synapse/homeserver.log + maxBytes: 104857600 + backupCount: 10 + filters: [context] + encoding: utf8 + console: + class: logging.StreamHandler + formatter: precise + filters: [context] + +loggers: + synapse.storage.SQL: + # beware: increasing this to DEBUG will make synapse log sensitive + # information such as access tokens. + level: INFO + +root: + level: INFO + handlers: [file, console] + +disable_existing_loggers: false From 92eac974b95c06868768f39f642361246f1c0b0f Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 31 Dec 2019 10:41:44 +0000 Subject: [PATCH 0736/1623] Hacks to work around #6605 (#6608) When we have an event which refers to non-existent auth_events, ignore said events rather than exploding in a ball of fire. Fixes #6605. --- changelog.d/6608.bugfix | 1 + synapse/state/v2.py | 52 +++++++++++++++++++++++++++++------------ 2 files changed, 38 insertions(+), 15 deletions(-) create mode 100644 changelog.d/6608.bugfix diff --git a/changelog.d/6608.bugfix b/changelog.d/6608.bugfix new file mode 100644 index 0000000000..7768e0846e --- /dev/null +++ b/changelog.d/6608.bugfix @@ -0,0 +1 @@ +Fix exceptions caused by state resolution choking on malformed events. diff --git a/synapse/state/v2.py b/synapse/state/v2.py index cb77ed5b78..72fb8a6317 100644 --- a/synapse/state/v2.py +++ b/synapse/state/v2.py @@ -183,16 +183,20 @@ def _get_power_level_for_sender(room_id, event_id, event_map, state_res_store): pl = None for aid in event.auth_event_ids(): - aev = yield _get_event(room_id, aid, event_map, state_res_store) - if (aev.type, aev.state_key) == (EventTypes.PowerLevels, ""): + aev = yield _get_event( + room_id, aid, event_map, state_res_store, allow_none=True + ) + if aev and (aev.type, aev.state_key) == (EventTypes.PowerLevels, ""): pl = aev break if pl is None: # Couldn't find power level. Check if they're the creator of the room for aid in event.auth_event_ids(): - aev = yield _get_event(room_id, aid, event_map, state_res_store) - if (aev.type, aev.state_key) == (EventTypes.Create, ""): + aev = yield _get_event( + room_id, aid, event_map, state_res_store, allow_none=True + ) + if aev and (aev.type, aev.state_key) == (EventTypes.Create, ""): if aev.content.get("creator") == event.sender: return 100 break @@ -403,10 +407,17 @@ def _iterative_auth_checks( auth_events = {} for aid in event.auth_event_ids(): - ev = yield _get_event(room_id, aid, event_map, state_res_store) + ev = yield _get_event( + room_id, aid, event_map, state_res_store, allow_none=True + ) - if ev.rejected_reason is None: - auth_events[(ev.type, ev.state_key)] = ev + if not ev: + logger.warning( + "auth_event id %s for event %s is missing", aid, event_id + ) + else: + if ev.rejected_reason is None: + auth_events[(ev.type, ev.state_key)] = ev for key in event_auth.auth_types_for_event(event): if key in resolved_state: @@ -457,8 +468,10 @@ def _mainline_sort( auth_events = pl_ev.auth_event_ids() pl = None for aid in auth_events: - ev = yield _get_event(room_id, aid, event_map, state_res_store) - if (ev.type, ev.state_key) == (EventTypes.PowerLevels, ""): + ev = yield _get_event( + room_id, aid, event_map, state_res_store, allow_none=True + ) + if ev and (ev.type, ev.state_key) == (EventTypes.PowerLevels, ""): pl = aid break @@ -506,8 +519,10 @@ def _get_mainline_depth_for_event(event, mainline_map, event_map, state_res_stor event = None for aid in auth_events: - aev = yield _get_event(room_id, aid, event_map, state_res_store) - if (aev.type, aev.state_key) == (EventTypes.PowerLevels, ""): + aev = yield _get_event( + room_id, aid, event_map, state_res_store, allow_none=True + ) + if aev and (aev.type, aev.state_key) == (EventTypes.PowerLevels, ""): event = aev break @@ -516,7 +531,7 @@ def _get_mainline_depth_for_event(event, mainline_map, event_map, state_res_stor @defer.inlineCallbacks -def _get_event(room_id, event_id, event_map, state_res_store): +def _get_event(room_id, event_id, event_map, state_res_store, allow_none=False): """Helper function to look up event in event_map, falling back to looking it up in the store @@ -525,15 +540,22 @@ def _get_event(room_id, event_id, event_map, state_res_store): event_id (str) event_map (dict[str,FrozenEvent]) state_res_store (StateResolutionStore) + allow_none (bool): if the event is not found, return None rather than raising + an exception Returns: - Deferred[FrozenEvent] + Deferred[Optional[FrozenEvent]] """ if event_id not in event_map: events = yield state_res_store.get_events([event_id], allow_rejected=True) event_map.update(events) - event = event_map[event_id] - assert event is not None + event = event_map.get(event_id) + + if event is None: + if allow_none: + return None + raise Exception("Unknown event %s" % (event_id,)) + if event.room_id != room_id: raise Exception( "In state res for room %s, event %s is in %s" From 77661ce81a799a375317dff9e4c8696da528984c Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 31 Dec 2019 10:45:12 +0000 Subject: [PATCH 0737/1623] 1.7.3 --- CHANGES.md | 11 +++++++++++ changelog.d/6608.bugfix | 1 - debian/changelog | 6 ++++++ synapse/__init__.py | 2 +- 4 files changed, 18 insertions(+), 2 deletions(-) delete mode 100644 changelog.d/6608.bugfix diff --git a/CHANGES.md b/CHANGES.md index d6567e24d2..361fd1fc6c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,14 @@ +Synapse 1.7.3 (2019-12-31) +========================== + +This release fixes a long-standing bug in the state resolution algorithm. + +Bugfixes +-------- + +- Fix exceptions caused by state resolution choking on malformed events. ([\#6608](https://github.com/matrix-org/synapse/issues/6608)) + + Synapse 1.7.2 (2019-12-20) ========================== diff --git a/changelog.d/6608.bugfix b/changelog.d/6608.bugfix deleted file mode 100644 index 7768e0846e..0000000000 --- a/changelog.d/6608.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix exceptions caused by state resolution choking on malformed events. diff --git a/debian/changelog b/debian/changelog index 2492b5db92..31791c127c 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.7.3) stable; urgency=medium + + * New synapse release 1.7.3. + + -- Synapse Packaging team Tue, 31 Dec 2019 10:45:04 +0000 + matrix-synapse-py3 (1.7.2) stable; urgency=medium * New synapse release 1.7.2. diff --git a/synapse/__init__.py b/synapse/__init__.py index 996101cf09..71cb611820 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -36,7 +36,7 @@ try: except ImportError: pass -__version__ = "1.7.2" +__version__ = "1.7.3" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 32779b59fab0b4a64198f8fe617d7c495aeb1ede Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Thu, 2 Jan 2020 04:28:20 -0600 Subject: [PATCH 0738/1623] Reword sections of federate.md that explained delegation at time of Synapse 1.0 transition (#6601) * Remove sections of federate.md explaining delegation at time of Synapse 1.0 transition Signed-off-by: Aaron Raimist * Add changelog Signed-off-by: Aaron Raimist --- changelog.d/6601.doc | 1 + docs/federate.md | 24 +++--------------------- 2 files changed, 4 insertions(+), 21 deletions(-) create mode 100644 changelog.d/6601.doc diff --git a/changelog.d/6601.doc b/changelog.d/6601.doc new file mode 100644 index 0000000000..08c5b3d215 --- /dev/null +++ b/changelog.d/6601.doc @@ -0,0 +1 @@ +Reword sections of federate.md that explained delegation at time of Synapse 1.0 transition. \ No newline at end of file diff --git a/docs/federate.md b/docs/federate.md index 193e2d2dfe..f9f17fcca5 100644 --- a/docs/federate.md +++ b/docs/federate.md @@ -66,10 +66,6 @@ therefore cannot gain access to the necessary certificate. With .well-known, federation servers will check for a valid TLS certificate for the delegated hostname (in our example: ``synapse.example.com``). -.well-known support first appeared in Synapse v0.99.0. To federate with older -servers you may need to additionally configure SRV delegation. Alternatively, -encourage the server admin in question to upgrade :). - ### DNS SRV delegation To use this delegation method, you need to have write access to your @@ -111,29 +107,15 @@ giving it a `server_name` of `example.com`, and once [ACME](acme.md) support is it would automatically generate a valid TLS certificate for you via Let's Encrypt and no SRV record or .well-known URI would be needed. -This is the common case, although you can add an SRV record or -`.well-known/matrix/server` URI for completeness if you wish. - **However**, if your server does not listen on port 8448, or if your `server_name` does not point to the host that your homeserver runs on, you will need to let other servers know how to find it. The way to do this is via .well-known or an SRV record. -#### I have created a .well-known URI. Do I still need an SRV record? +#### I have created a .well-known URI. Do I also need an SRV record? -As of Synapse 0.99, Synapse will first check for the existence of a .well-known -URI and follow any delegation it suggests. It will only then check for the -existence of an SRV record. - -That means that the SRV record will often be redundant. However, you should -remember that there may still be older versions of Synapse in the federation -which do not understand .well-known URIs, so if you removed your SRV record -you would no longer be able to federate with them. - -It is therefore best to leave the SRV record in place for now. Synapse 0.34 and -earlier will follow the SRV record (and not care about the invalid -certificate). Synapse 0.99 and later will follow the .well-known URI, with the -correct certificate chain. +No. You can use either `.well-known` delegation or use an SRV record for delegation. You +do not need to use both to delegate to the same location. #### Can I manage my own certificates rather than having Synapse renew certificates itself? From 0495097a7f6c6f5230972e0a5f32c0c9c42ef61b Mon Sep 17 00:00:00 2001 From: ewaf1 <59422220+ewaf1@users.noreply.github.com> Date: Thu, 2 Jan 2020 11:41:30 +0100 Subject: [PATCH 0739/1623] Added the section 'Configuration' in /docs/turn-howto.md (#6614) put the 2nd part of the "source installation"-section into a new section, because it also applies to Debian packages --- changelog.d/6614.doc | 1 + docs/turn-howto.md | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 changelog.d/6614.doc diff --git a/changelog.d/6614.doc b/changelog.d/6614.doc new file mode 100644 index 0000000000..38b962b062 --- /dev/null +++ b/changelog.d/6614.doc @@ -0,0 +1 @@ +Added the section 'Configuration' in /docs/turn-howto.md. diff --git a/docs/turn-howto.md b/docs/turn-howto.md index 4a983621e5..1bd3943f54 100644 --- a/docs/turn-howto.md +++ b/docs/turn-howto.md @@ -39,6 +39,8 @@ The TURN daemon `coturn` is available from a variety of sources such as native p make make install +### Configuration + 1. Create or edit the config file in `/etc/turnserver.conf`. The relevant lines, with example values, are: From 4efe1d4d3f61a0ce3769b119d7169c03ac1f9564 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 3 Jan 2020 12:57:24 +0100 Subject: [PATCH 0740/1623] Fix a typo in the purge jobs configuration example --- synapse/config/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/config/server.py b/synapse/config/server.py index 38f6ff9edc..3463d53d10 100644 --- a/synapse/config/server.py +++ b/synapse/config/server.py @@ -955,7 +955,7 @@ class ServerConfig(Config): #purge_jobs: # - shortest_max_lifetime: 1d # longest_max_lifetime: 3d - # interval: 5m: + # interval: 5m # - shortest_max_lifetime: 3d # longest_max_lifetime: 1y # interval: 24h From dd2954f78dfbea5a24da079e4d9a114f91e55698 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 3 Jan 2020 12:58:12 +0100 Subject: [PATCH 0741/1623] Update sample config --- docs/sample_config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index e3b05423b8..cc261d96d0 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -394,7 +394,7 @@ retention: #purge_jobs: # - shortest_max_lifetime: 1d # longest_max_lifetime: 3d - # interval: 5m: + # interval: 5m # - shortest_max_lifetime: 3d # longest_max_lifetime: 1y # interval: 24h From 9c59bc59c8daee1c2e98008e9929ac67cf08ff0b Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 3 Jan 2020 13:00:32 +0100 Subject: [PATCH 0742/1623] Changelog --- changelog.d/6621.doc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6621.doc diff --git a/changelog.d/6621.doc b/changelog.d/6621.doc new file mode 100644 index 0000000000..6722ccfda3 --- /dev/null +++ b/changelog.d/6621.doc @@ -0,0 +1 @@ +Fix a typo in the configuration example for purge jobs in the sample configuration file. From 9279a2c4e4c1c63e87d43fc9f6ad2c495bf47e67 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 3 Jan 2020 13:43:55 +0100 Subject: [PATCH 0743/1623] Add a complete documentation of the message retention policies support --- changelog.d/6623.doc | 1 + docs/message_retention_policies.md | 191 +++++++++++++++++++++++++++++ 2 files changed, 192 insertions(+) create mode 100644 changelog.d/6623.doc create mode 100644 docs/message_retention_policies.md diff --git a/changelog.d/6623.doc b/changelog.d/6623.doc new file mode 100644 index 0000000000..c8aade0974 --- /dev/null +++ b/changelog.d/6623.doc @@ -0,0 +1 @@ +Add a complete documentation of the message retention policies support. diff --git a/docs/message_retention_policies.md b/docs/message_retention_policies.md new file mode 100644 index 0000000000..78055b2f64 --- /dev/null +++ b/docs/message_retention_policies.md @@ -0,0 +1,191 @@ +# Message retention policies + +Synapse admins can enable support for message retention policies on +their homeserver. Message retention policies exist at a room level, +follow the semantics described in +[MSC1763](https://github.com/matrix-org/matrix-doc/blob/matthew/msc1763/proposals/1763-configurable-retention-periods.md), +and allow server and room admins to configure how long messages should +be kept in a homeserver's database before being purged from it. + +A message retention policy is mainly defined by its `max_lifetime` +parameter, which defines how long a message can be kept around after +it's been sent in the room. If a room doesn't have a message retention +policy, and there's no default one for a given server, then no message +sent in that room is ever purged on that server. + +MSC1763 also specifies semantics for a `min_lifetime` parameter which +defines the amount of time after which an event _can_ get purged (after +it's been sent to the room), but Synapse doesn't currently support it +beyond registering it. + +Both `max_lifetime` and `min_lifetime` are optional parameters. + +Note that message retention policies don't apply to state events. + +Once an event reaches its expiry date (defined as the time it was sent +plus the value for `max_lifetime` in the room), two things happen: + +* Synapse stops serving the event to clients via any endpoint. +* The message gets picked up by the next purge job (see the "Purge jobs" + section) and is removed from Synapse's database. + +Since purge jobs don't run continuously, this means that an event might +stay in a server's database for longer than the value for `max_lifetime` +in the room would allow, though hidden from clients. + +Similarly, if a server (with support for message retention policies +enabled) receives from another server an event that should have been +purged according to its room's policy, then the receiving server will +process and store that event until it's picked up by the next purge job, +though it will always hide it from clients. + + +## Room configuration + +To configure a room's message retention policy, a room's admin or +moderator needs to send a state event in that room with the type +`m.room.retention` and the following content: + +```json +{ + "max_lifetime": ... +} +``` + +In this event's content, the `max_lifetime` parameter has the same +meaning as previously described, and needs to be expressed in +milliseconds. The event's content can also include a `min_lifetime` +parameter, which has the same meaning and limited support as previously +described. + +Note that over every server in the room, only the ones with support for +message retention policies will actually remove expired events. While +we plan to eventually enable this support by default in Synapse, this +isn't currently the case. + + +## Server configuration + +Support for this feature can be enabled and configured in the +`retention` section of the Synapse configuration file (see the +[sample file](https://github.com/matrix-org/synapse/blob/v1.7.3/docs/sample_config.yaml#L332-L393)). + +To enable support for message retentions policies, set the setting +`enabled` in this section to `true`. + + +### Default policy + +A default message retention policy is a policy defined in Synapse's +configuration that is used by Synapse for every room that doesn't have a +message retention policy configured in its state. This allows server +admins to ensure that messages are never kept indefinitely in a server's +database. + +A default policy can be defined as such, in the `retention` section of +the configuration file: + +```yaml + default_policy: + min_lifetime: 1d + max_lifetime: 1y +``` + +Here, `min_lifetime` and `max_lifetime` have the same meaning and level +of support as previously described. They can be expressed either as a +duration (using the units `s` (seconds), `m` (minutes), `h` (hours), +`d` (days), `w` (weeks) and `y` (years)) or as a number of milliseconds. + + +### Purge jobs + +Purge jobs are the jobs that Synapse run in the background to purge +expired events from the database. They are only run if support for +message retention policies is enabled in the server's configuration. If +no configuration for purge jobs is configured by the server admin, +Synapse will run one daily that will handle every room with a message +retention policy (or, if the server has a default policy configured, +every room it knows), which should be enough in most cases. + +Some server admins might want a finer control on when events are removed +depending on an event's room's policy. This can be done by setting the +`purge_jobs` sub-section in the `retention` section of the configuration +file. An example of such configuration could be: + +```yaml + purge_jobs: + - longest_max_lifetime: 3d + interval: 12h + - shortest_max_lifetime: 3d + longest_max_lifetime: 1w + interval: 1d + - shortest_max_lifetime: 1w + interval: 2d +``` + +In this example, we define two jobs: + +* one that runs twice a day (every 12 hours) and purges events in rooms + which policy's `max_lifetime` is lower or equal to 3 days. +* one that runs once a day and purges events in rooms which policy's + `max_lifetime` is between 3 days and a week. +* one that runs once every 2 days and purges events in rooms which + policy's `max_lifetime` is greater than a week. + +Note that this example is tailored to show different configurations and +features slightly more jobs than it's probably necessary (in practice, a +server admin would probably consider it better to replace the two last +jobs with one that runs once a day and handles rooms which which +policy's `max_lifetime` is greater than 3 days). + +Keep in mind, when configuring these jobs, that a purge job can become +quite heavy on the server if it targets many rooms, therefore prefer +having jobs with a low interval that target a limited set of rooms. Also +make sure to include a job with no minimum and one with no maximum to +make sure your configuration handles every policy. + +As previously mentioned in this documentation, while a purge job that +runs e.g. every day means that an expired event might stay in the +database for up to a day after its expiry, Synapse hides expired events +from clients as soon as they expire, so the event is not visible to +local users between its expiry date and the moment it gets purged from +the server's database. + + +### Lifetime limits + +**Note: this feature is mainly useful within a closed federation or on +servers that don't federate, because there currently is no way to +enforce these limits in an open federation.** + +Server admins can restrict the values their local users are allowed to +use for both `min_lifetime` and `max_lifetime`. These limits can be +defined as such in the `retention` section of the configuration file: + +```yaml + allowed_lifetime_min: 1d + allowed_lifetime_max: 1y +``` + +Here, `allowed_lifetime_min` is the lowest value a local user can set +for both `min_lifetime` and `max_lifetime`, and `allowed_lifetime_max` +is the highest value. Both parameters are optional (e.g. setting +`allowed_lifetime_min` but not `allowed_lifetime_max` only enforces a +minimum and no maximum). + +Like other settings in this section, these parameters can be expressed +either as a duration or as a number of milliseconds. + + +## Note on reclaiming disk space + +While purge jobs actually delete data from the database, the disk space +used by the database might not decrease immediately on the database's +host. However, even though the database engine won't free up the disk +space, it will start writing new data into where the purged data was. + +If you want to reclaim the freed disk space anyway and return it to the +operating system, the server admin needs to run `VACUUM FULL;` on the +database (see the related +[PostgreSQL documentation](https://www.postgresql.org/docs/current/sql-vacuum.html)). + From 51b8a21f0c3f52c26c63c196f5ed11b8be2394af Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 3 Jan 2020 13:49:12 +0100 Subject: [PATCH 0744/1623] Rename changelog --- changelog.d/{6623.doc => 6624.doc} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/{6623.doc => 6624.doc} (100%) diff --git a/changelog.d/6623.doc b/changelog.d/6624.doc similarity index 100% rename from changelog.d/6623.doc rename to changelog.d/6624.doc From b7dec300b7419402a0d5fc00e34684b95618a7d9 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 3 Jan 2020 13:51:59 +0100 Subject: [PATCH 0745/1623] Fix vacuum instructions for sqlite --- docs/message_retention_policies.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/message_retention_policies.md b/docs/message_retention_policies.md index 78055b2f64..72f08fbb4c 100644 --- a/docs/message_retention_policies.md +++ b/docs/message_retention_policies.md @@ -185,7 +185,7 @@ host. However, even though the database engine won't free up the disk space, it will start writing new data into where the purged data was. If you want to reclaim the freed disk space anyway and return it to the -operating system, the server admin needs to run `VACUUM FULL;` on the -database (see the related +operating system, the server admin needs to run `VACUUM FULL;` (or +`VACUUM;` for SQLite databases) on Synapse's database (see the related [PostgreSQL documentation](https://www.postgresql.org/docs/current/sql-vacuum.html)). From 6964ea095bbd474b5c2b9dfe99c817cd370987bf Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 3 Jan 2020 14:19:09 +0000 Subject: [PATCH 0746/1623] Reduce the reconnect time when replication fails. (#6617) --- changelog.d/6617.misc | 1 + synapse/replication/tcp/client.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 changelog.d/6617.misc diff --git a/changelog.d/6617.misc b/changelog.d/6617.misc new file mode 100644 index 0000000000..94aa271d38 --- /dev/null +++ b/changelog.d/6617.misc @@ -0,0 +1 @@ +Reduce the reconnect time when worker replication fails, to make it easier to catch up. diff --git a/synapse/replication/tcp/client.py b/synapse/replication/tcp/client.py index fead78388c..bbcb84646c 100644 --- a/synapse/replication/tcp/client.py +++ b/synapse/replication/tcp/client.py @@ -46,7 +46,8 @@ class ReplicationClientFactory(ReconnectingClientFactory): is required. """ - maxDelay = 30 # Try at least once every N seconds + initialDelay = 0.1 + maxDelay = 1 # Try at least once every N seconds def __init__(self, hs, client_name, handler: AbstractReplicationClientHandler): self.client_name = client_name From b6b57ecb4e845490fc26a537ff57df8cae1587b9 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 3 Jan 2020 14:19:48 +0000 Subject: [PATCH 0747/1623] Kill off redundant SynapseRequestFactory (#6619) We already get the Site via the Channel, so there's no need for a dedicated RequestFactory: we can just use the right constructor. --- changelog.d/6619.misc | 1 + synapse/http/site.py | 18 +++--------------- tests/server.py | 6 ++++-- 3 files changed, 8 insertions(+), 17 deletions(-) create mode 100644 changelog.d/6619.misc diff --git a/changelog.d/6619.misc b/changelog.d/6619.misc new file mode 100644 index 0000000000..b608133219 --- /dev/null +++ b/changelog.d/6619.misc @@ -0,0 +1 @@ +Simplify http handling by removing redundant SynapseRequestFactory. diff --git a/synapse/http/site.py b/synapse/http/site.py index ff8184a3d0..9f2d035fa0 100644 --- a/synapse/http/site.py +++ b/synapse/http/site.py @@ -47,9 +47,9 @@ class SynapseRequest(Request): logcontext(LoggingContext) : the log context for this request """ - def __init__(self, site, channel, *args, **kw): + def __init__(self, channel, *args, **kw): Request.__init__(self, channel, *args, **kw) - self.site = site + self.site = channel.site self._channel = channel # this is used by the tests self.authenticated_entity = None self.start_time = 0 @@ -331,18 +331,6 @@ class XForwardedForRequest(SynapseRequest): ) -class SynapseRequestFactory(object): - def __init__(self, site, x_forwarded_for): - self.site = site - self.x_forwarded_for = x_forwarded_for - - def __call__(self, *args, **kwargs): - if self.x_forwarded_for: - return XForwardedForRequest(self.site, *args, **kwargs) - else: - return SynapseRequest(self.site, *args, **kwargs) - - class SynapseSite(Site): """ Subclass of a twisted http Site that does access logging with python's @@ -364,7 +352,7 @@ class SynapseSite(Site): self.site_tag = site_tag proxied = config.get("x_forwarded", False) - self.requestFactory = SynapseRequestFactory(self, proxied) + self.requestFactory = XForwardedForRequest if proxied else SynapseRequest self.access_logger = logging.getLogger(logger_name) self.server_version_string = server_version_string.encode("ascii") diff --git a/tests/server.py b/tests/server.py index a554dfdd57..1644710aa0 100644 --- a/tests/server.py +++ b/tests/server.py @@ -20,6 +20,7 @@ from twisted.python.failure import Failure from twisted.test.proto_helpers import AccumulatingProtocol, MemoryReactorClock from twisted.web.http import unquote from twisted.web.http_headers import Headers +from twisted.web.server import Site from synapse.http.site import SynapseRequest from synapse.util import Clock @@ -42,6 +43,7 @@ class FakeChannel(object): wire). """ + site = attr.ib(type=Site) _reactor = attr.ib() result = attr.ib(default=attr.Factory(dict)) _producer = None @@ -176,9 +178,9 @@ def make_request( content = content.encode("utf8") site = FakeSite() - channel = FakeChannel(reactor) + channel = FakeChannel(site, reactor) - req = request(site, channel) + req = request(channel) req.process = lambda: b"" req.content = BytesIO(content) req.postpath = list(map(unquote, path[1:].split(b"/"))) From 98247c4a0e169ee5f201fe5f0e404604d6628566 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 3 Jan 2020 17:10:52 +0000 Subject: [PATCH 0748/1623] Remove unused, undocumented "content repo" resource (#6628) This looks like it got half-killed back in #888. Fixes #6567. --- changelog.d/6628.removal | 1 + docs/sample_config.yaml | 4 - synapse/api/urls.py | 1 - synapse/app/homeserver.py | 10 +- synapse/app/media_repository.py | 6 +- synapse/config/repository.py | 5 - synapse/rest/media/v0/__init__.py | 0 synapse/rest/media/v0/content_repository.py | 103 -------------------- tox.ini | 1 - 9 files changed, 3 insertions(+), 128 deletions(-) create mode 100644 changelog.d/6628.removal delete mode 100644 synapse/rest/media/v0/__init__.py delete mode 100644 synapse/rest/media/v0/content_repository.py diff --git a/changelog.d/6628.removal b/changelog.d/6628.removal new file mode 100644 index 0000000000..66cd6aeca4 --- /dev/null +++ b/changelog.d/6628.removal @@ -0,0 +1 @@ +Remove unused, undocumented /_matrix/content API. diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index e3b05423b8..fad5f968b5 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -692,10 +692,6 @@ media_store_path: "DATADIR/media_store" # config: # directory: /mnt/some/other/directory -# Directory where in-progress uploads are stored. -# -uploads_path: "DATADIR/uploads" - # The largest allowed upload size in bytes # #max_upload_size: 10M diff --git a/synapse/api/urls.py b/synapse/api/urls.py index ff1f39e86c..f34434bd67 100644 --- a/synapse/api/urls.py +++ b/synapse/api/urls.py @@ -29,7 +29,6 @@ FEDERATION_V2_PREFIX = FEDERATION_PREFIX + "/v2" FEDERATION_UNSTABLE_PREFIX = FEDERATION_PREFIX + "/unstable" STATIC_PREFIX = "/_matrix/static" WEB_CLIENT_PREFIX = "/_matrix/client" -CONTENT_REPO_PREFIX = "/_matrix/content" SERVER_KEY_V2_PREFIX = "/_matrix/key/v2" MEDIA_PREFIX = "/_matrix/media/r0" LEGACY_MEDIA_PREFIX = "/_matrix/media/v1" diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py index 0e9bf7f53a..6208deb646 100644 --- a/synapse/app/homeserver.py +++ b/synapse/app/homeserver.py @@ -39,7 +39,6 @@ import synapse import synapse.config.logger from synapse import events from synapse.api.urls import ( - CONTENT_REPO_PREFIX, FEDERATION_PREFIX, LEGACY_MEDIA_PREFIX, MEDIA_PREFIX, @@ -65,7 +64,6 @@ from synapse.replication.tcp.resource import ReplicationStreamProtocolFactory from synapse.rest import ClientRestResource from synapse.rest.admin import AdminRestResource from synapse.rest.key.v2 import KeyApiV2Resource -from synapse.rest.media.v0.content_repository import ContentRepoResource from synapse.rest.well_known import WellKnownResource from synapse.server import HomeServer from synapse.storage import DataStore @@ -223,13 +221,7 @@ class SynapseHomeServer(HomeServer): if self.get_config().enable_media_repo: media_repo = self.get_media_repository_resource() resources.update( - { - MEDIA_PREFIX: media_repo, - LEGACY_MEDIA_PREFIX: media_repo, - CONTENT_REPO_PREFIX: ContentRepoResource( - self, self.config.uploads_path - ), - } + {MEDIA_PREFIX: media_repo, LEGACY_MEDIA_PREFIX: media_repo} ) elif name == "media": raise ConfigError( diff --git a/synapse/app/media_repository.py b/synapse/app/media_repository.py index 4c80f257e2..a63c53dc44 100644 --- a/synapse/app/media_repository.py +++ b/synapse/app/media_repository.py @@ -21,7 +21,7 @@ from twisted.web.resource import NoResource import synapse from synapse import events -from synapse.api.urls import CONTENT_REPO_PREFIX, LEGACY_MEDIA_PREFIX, MEDIA_PREFIX +from synapse.api.urls import LEGACY_MEDIA_PREFIX, MEDIA_PREFIX from synapse.app import _base from synapse.config._base import ConfigError from synapse.config.homeserver import HomeServerConfig @@ -37,7 +37,6 @@ from synapse.replication.slave.storage.registration import SlavedRegistrationSto from synapse.replication.slave.storage.transactions import SlavedTransactionStore from synapse.replication.tcp.client import ReplicationClientHandler from synapse.rest.admin import register_servlets_for_media_repo -from synapse.rest.media.v0.content_repository import ContentRepoResource from synapse.server import HomeServer from synapse.storage.data_stores.main.media_repository import MediaRepositoryStore from synapse.util.httpresourcetree import create_resource_tree @@ -82,9 +81,6 @@ class MediaRepositoryServer(HomeServer): { MEDIA_PREFIX: media_repo, LEGACY_MEDIA_PREFIX: media_repo, - CONTENT_REPO_PREFIX: ContentRepoResource( - self, self.config.uploads_path - ), "/_synapse/admin": admin_resource, } ) diff --git a/synapse/config/repository.py b/synapse/config/repository.py index d0205e14b9..7d2dd27fd0 100644 --- a/synapse/config/repository.py +++ b/synapse/config/repository.py @@ -156,7 +156,6 @@ class ContentRepositoryConfig(Config): (provider_class, parsed_config, wrapper_config) ) - self.uploads_path = self.ensure_directory(config.get("uploads_path", "uploads")) self.dynamic_thumbnails = config.get("dynamic_thumbnails", False) self.thumbnail_requirements = parse_thumbnail_requirements( config.get("thumbnail_sizes", DEFAULT_THUMBNAIL_SIZES) @@ -231,10 +230,6 @@ class ContentRepositoryConfig(Config): # config: # directory: /mnt/some/other/directory - # Directory where in-progress uploads are stored. - # - uploads_path: "%(uploads_path)s" - # The largest allowed upload size in bytes # #max_upload_size: 10M diff --git a/synapse/rest/media/v0/__init__.py b/synapse/rest/media/v0/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/synapse/rest/media/v0/content_repository.py b/synapse/rest/media/v0/content_repository.py deleted file mode 100644 index 86884c0ef4..0000000000 --- a/synapse/rest/media/v0/content_repository.py +++ /dev/null @@ -1,103 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014-2016 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import base64 -import logging -import os -import re - -from canonicaljson import json - -from twisted.protocols.basic import FileSender -from twisted.web import resource, server - -from synapse.api.errors import Codes, cs_error -from synapse.http.server import finish_request, respond_with_json_bytes - -logger = logging.getLogger(__name__) - - -class ContentRepoResource(resource.Resource): - """Provides file uploading and downloading. - - Uploads are POSTed to wherever this Resource is linked to. This resource - returns a "content token" which can be used to GET this content again. The - token is typically a path, but it may not be. Tokens can expire, be - one-time uses, etc. - - In this case, the token is a path to the file and contains 3 interesting - sections: - - User ID base64d (for namespacing content to each user) - - random 24 char string - - Content type base64d (so we can return it when clients GET it) - - """ - - isLeaf = True - - def __init__(self, hs, directory): - resource.Resource.__init__(self) - self.hs = hs - self.directory = directory - - def render_GET(self, request): - # no auth here on purpose, to allow anyone to view, even across home - # servers. - - # TODO: A little crude here, we could do this better. - filename = request.path.decode("ascii").split("/")[-1] - # be paranoid - filename = re.sub("[^0-9A-z.-_]", "", filename) - - file_path = self.directory + "/" + filename - - logger.debug("Searching for %s", file_path) - - if os.path.isfile(file_path): - # filename has the content type - base64_contentype = filename.split(".")[1] - content_type = base64.urlsafe_b64decode(base64_contentype) - logger.info("Sending file %s", file_path) - f = open(file_path, "rb") - request.setHeader("Content-Type", content_type) - - # cache for at least a day. - # XXX: we might want to turn this off for data we don't want to - # recommend caching as it's sensitive or private - or at least - # select private. don't bother setting Expires as all our matrix - # clients are smart enough to be happy with Cache-Control (right?) - request.setHeader(b"Cache-Control", b"public,max-age=86400,s-maxage=86400") - - d = FileSender().beginFileTransfer(f, request) - - # after the file has been sent, clean up and finish the request - def cbFinished(ignored): - f.close() - finish_request(request) - - d.addCallback(cbFinished) - else: - respond_with_json_bytes( - request, - 404, - json.dumps(cs_error("Not found", code=Codes.NOT_FOUND)), - send_cors=True, - ) - - return server.NOT_DONE_YET - - def render_OPTIONS(self, request): - respond_with_json_bytes(request, 200, {}, send_cors=True) - return server.NOT_DONE_YET diff --git a/tox.ini b/tox.ini index 1d6428f64f..0ab6d5666b 100644 --- a/tox.ini +++ b/tox.ini @@ -182,7 +182,6 @@ commands = mypy \ synapse/logging/ \ synapse/module_api \ synapse/rest/consent \ - synapse/rest/media/v0 \ synapse/rest/saml2 \ synapse/spam_checker_api \ synapse/storage/engines \ From e484101306787988adacf6d6de4fcd565368dec4 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 3 Jan 2020 17:11:29 +0000 Subject: [PATCH 0749/1623] Raise an error if someone tries to use the log_file config option (#6626) This has caused some confusion for people who didn't notice it going away. --- changelog.d/6626.feature | 1 + synapse/app/homeserver.py | 2 +- synapse/config/logger.py | 17 +++++++++++++++-- 3 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 changelog.d/6626.feature diff --git a/changelog.d/6626.feature b/changelog.d/6626.feature new file mode 100644 index 0000000000..15798fa59b --- /dev/null +++ b/changelog.d/6626.feature @@ -0,0 +1 @@ +Raise an error if someone tries to use the log_file config option. diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py index 6208deb646..e5b44a5eed 100644 --- a/synapse/app/homeserver.py +++ b/synapse/app/homeserver.py @@ -310,7 +310,7 @@ def setup(config_options): "Synapse Homeserver", config_options ) except ConfigError as e: - sys.stderr.write("\n" + str(e) + "\n") + sys.stderr.write("\nERROR: %s\n" % (e,)) sys.exit(1) if not config: diff --git a/synapse/config/logger.py b/synapse/config/logger.py index 75bb904718..3c455610d9 100644 --- a/synapse/config/logger.py +++ b/synapse/config/logger.py @@ -12,7 +12,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - +import argparse import logging import logging.config import os @@ -37,7 +37,7 @@ from synapse.logging._structured import ( from synapse.logging.context import LoggingContextFilter from synapse.util.versionstring import get_version_string -from ._base import Config +from ._base import Config, ConfigError DEFAULT_LOG_CONFIG = Template( """ @@ -81,11 +81,18 @@ disable_existing_loggers: false """ ) +LOG_FILE_ERROR = """\ +Support for the log_file configuration option and --log-file command-line option was +removed in Synapse 1.3.0. You should instead set up a separate log configuration file. +""" + class LoggingConfig(Config): section = "logging" def read_config(self, config, **kwargs): + if config.get("log_file"): + raise ConfigError(LOG_FILE_ERROR) self.log_config = self.abspath(config.get("log_config")) self.no_redirect_stdio = config.get("no_redirect_stdio", False) @@ -106,6 +113,8 @@ class LoggingConfig(Config): def read_arguments(self, args): if args.no_redirect_stdio is not None: self.no_redirect_stdio = args.no_redirect_stdio + if args.log_file is not None: + raise ConfigError(LOG_FILE_ERROR) @staticmethod def add_arguments(parser): @@ -118,6 +127,10 @@ class LoggingConfig(Config): help="Do not redirect stdout/stderr to the log", ) + logging_group.add_argument( + "-f", "--log-file", dest="log_file", help=argparse.SUPPRESS, + ) + def generate_files(self, config, config_dir_path): log_config = config.get("log_config") if log_config and not os.path.exists(log_config): From 08815566bca79d001ad1bf58b2b082e435b6e5df Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 3 Jan 2020 17:14:00 +0000 Subject: [PATCH 0750/1623] Automate generation of the sample and debian log configs (#6627) --- changelog.d/6627.misc | 1 + debian/build_virtualenv | 3 +++ debian/changelog | 6 +++++ debian/install | 1 - debian/log.yaml | 36 ------------------------- docs/sample_log_config.yaml | 4 +-- scripts-dev/generate_sample_config | 10 +++++++ scripts/generate_log_config | 43 ++++++++++++++++++++++++++++++ synapse/config/logger.py | 9 ++++++- 9 files changed, 73 insertions(+), 40 deletions(-) create mode 100644 changelog.d/6627.misc delete mode 100644 debian/log.yaml create mode 100755 scripts/generate_log_config diff --git a/changelog.d/6627.misc b/changelog.d/6627.misc new file mode 100644 index 0000000000..702f067070 --- /dev/null +++ b/changelog.d/6627.misc @@ -0,0 +1 @@ +Automate generation of the sample log config. diff --git a/debian/build_virtualenv b/debian/build_virtualenv index 2791896052..d892fd5c9d 100755 --- a/debian/build_virtualenv +++ b/debian/build_virtualenv @@ -85,6 +85,9 @@ PYTHONPATH="$tmpdir" \ ' > "${PACKAGE_BUILD_DIR}/etc/matrix-synapse/homeserver.yaml" +# build the log config file +"${TARGET_PYTHON}" -B "${VIRTUALENV_DIR}/bin/generate_log_config" \ + --output-file="${PACKAGE_BUILD_DIR}/etc/matrix-synapse/log.yaml" # add a dependency on the right version of python to substvars. PYPKG=`basename $SNAKE` diff --git a/debian/changelog b/debian/changelog index 31791c127c..75fe89fa97 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.7.3ubuntu1) UNRELEASED; urgency=medium + + * Automate generation of the default log configuration file. + + -- Richard van der Hoff Fri, 03 Jan 2020 13:55:38 +0000 + matrix-synapse-py3 (1.7.3) stable; urgency=medium * New synapse release 1.7.3. diff --git a/debian/install b/debian/install index 43dc8c6904..da8b726a2b 100644 --- a/debian/install +++ b/debian/install @@ -1,2 +1 @@ -debian/log.yaml etc/matrix-synapse debian/manage_debconf.pl /opt/venvs/matrix-synapse/lib/ diff --git a/debian/log.yaml b/debian/log.yaml deleted file mode 100644 index 95b655dd35..0000000000 --- a/debian/log.yaml +++ /dev/null @@ -1,36 +0,0 @@ - -version: 1 - -formatters: - precise: - format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s- %(message)s' - -filters: - context: - (): synapse.logging.context.LoggingContextFilter - request: "" - -handlers: - file: - class: logging.handlers.RotatingFileHandler - formatter: precise - filename: /var/log/matrix-synapse/homeserver.log - maxBytes: 104857600 - backupCount: 10 - filters: [context] - encoding: utf8 - console: - class: logging.StreamHandler - formatter: precise - level: WARN - -loggers: - synapse: - level: INFO - - synapse.storage.SQL: - level: INFO - -root: - level: INFO - handlers: [file, console] diff --git a/docs/sample_log_config.yaml b/docs/sample_log_config.yaml index 11e8f35f41..1a2739455e 100644 --- a/docs/sample_log_config.yaml +++ b/docs/sample_log_config.yaml @@ -1,4 +1,4 @@ -# Example log config file for synapse. +# Log configuration for Synapse. # # This is a YAML file containing a standard Python logging configuration # dictionary. See [1] for details on the valid settings. @@ -20,7 +20,7 @@ handlers: file: class: logging.handlers.RotatingFileHandler formatter: precise - filename: /home/rav/work/synapse/homeserver.log + filename: /var/log/matrix-synapse/homeserver.log maxBytes: 104857600 backupCount: 10 filters: [context] diff --git a/scripts-dev/generate_sample_config b/scripts-dev/generate_sample_config index 5e33b9b549..9cb4630a5c 100755 --- a/scripts-dev/generate_sample_config +++ b/scripts-dev/generate_sample_config @@ -7,12 +7,22 @@ set -e cd `dirname $0`/.. SAMPLE_CONFIG="docs/sample_config.yaml" +SAMPLE_LOG_CONFIG="docs/sample_log_config.yaml" + +check() { + diff -u "$SAMPLE_LOG_CONFIG" <(./scripts/generate_log_config) >/dev/null || return 1 +} if [ "$1" == "--check" ]; then diff -u "$SAMPLE_CONFIG" <(./scripts/generate_config --header-file docs/.sample_config_header.yaml) >/dev/null || { echo -e "\e[1m\e[31m$SAMPLE_CONFIG is not up-to-date. Regenerate it with \`scripts-dev/generate_sample_config\`.\e[0m" >&2 exit 1 } + diff -u "$SAMPLE_LOG_CONFIG" <(./scripts/generate_log_config) >/dev/null || { + echo -e "\e[1m\e[31m$SAMPLE_LOG_CONFIG is not up-to-date. Regenerate it with \`scripts-dev/generate_sample_config\`.\e[0m" >&2 + exit 1 + } else ./scripts/generate_config --header-file docs/.sample_config_header.yaml -o "$SAMPLE_CONFIG" + ./scripts/generate_log_config -o "$SAMPLE_LOG_CONFIG" fi diff --git a/scripts/generate_log_config b/scripts/generate_log_config new file mode 100755 index 0000000000..b6957f48a3 --- /dev/null +++ b/scripts/generate_log_config @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 + +# -*- coding: utf-8 -*- +# Copyright 2020 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import sys + +from synapse.config.logger import DEFAULT_LOG_CONFIG + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + + parser.add_argument( + "-o", + "--output-file", + type=argparse.FileType("w"), + default=sys.stdout, + help="File to write the configuration to. Default: stdout", + ) + + parser.add_argument( + "-f", + "--log-file", + type=str, + default="/var/log/matrix-synapse/homeserver.log", + help="name of the log file", + ) + + args = parser.parse_args() + args.output_file.write(DEFAULT_LOG_CONFIG.substitute(log_file=args.log_file)) diff --git a/synapse/config/logger.py b/synapse/config/logger.py index 3c455610d9..a25c70e928 100644 --- a/synapse/config/logger.py +++ b/synapse/config/logger.py @@ -40,7 +40,14 @@ from synapse.util.versionstring import get_version_string from ._base import Config, ConfigError DEFAULT_LOG_CONFIG = Template( - """ + """\ +# Log configuration for Synapse. +# +# This is a YAML file containing a standard Python logging configuration +# dictionary. See [1] for details on the valid settings. +# +# [1]: https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema + version: 1 formatters: From 01c3c6c9298d0bbdbbc6e829e9c9f1e1a52e8332 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Mon, 6 Jan 2020 04:53:07 -0500 Subject: [PATCH 0751/1623] Fix power levels being incorrectly set in old and new rooms after a room upgrade (#6633) Modify a copy of an upgraded room's PL before sending to the new room --- changelog.d/6633.bugfix | 1 + synapse/handlers/room.py | 17 ++++++++++------- 2 files changed, 11 insertions(+), 7 deletions(-) create mode 100644 changelog.d/6633.bugfix diff --git a/changelog.d/6633.bugfix b/changelog.d/6633.bugfix new file mode 100644 index 0000000000..4bacf26021 --- /dev/null +++ b/changelog.d/6633.bugfix @@ -0,0 +1 @@ +Fix bug where a moderator upgraded a room and became an admin in the new room. \ No newline at end of file diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 89c9118b26..4f489762fc 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -16,6 +16,7 @@ # limitations under the License. """Contains functions for performing events on rooms.""" +import copy import itertools import logging import math @@ -271,7 +272,7 @@ class RoomCreationHandler(BaseHandler): except AuthError as e: logger.warning("Unable to update PLs in old room: %s", e) - logger.info("Setting correct PLs in new room") + logger.info("Setting correct PLs in new room to %s", old_room_pl_state.content) yield self.event_creation_handler.create_and_send_nonmember_event( requester, { @@ -365,13 +366,15 @@ class RoomCreationHandler(BaseHandler): needed_power_level = max(state_default, ban, max(event_power_levels.values())) # Raise the requester's power level in the new room if necessary - current_power_level = power_levels["users"][requester.user.to_string()] + current_power_level = power_levels["users"][user_id] if current_power_level < needed_power_level: - # Assign this power level to the requester - power_levels["users"][requester.user.to_string()] = needed_power_level + # Perform a deepcopy in order to not modify the original power levels in a + # room, as its contents are preserved as the state for the old room later on + new_power_levels = copy.deepcopy(power_levels) + initial_state[(EventTypes.PowerLevels, "")] = new_power_levels - # Set the power levels to the modified state - initial_state[(EventTypes.PowerLevels, "")] = power_levels + # Assign this power level to the requester + new_power_levels["users"][user_id] = needed_power_level yield self._send_events_for_new_room( requester, @@ -733,7 +736,7 @@ class RoomCreationHandler(BaseHandler): initial_state, creation_content, room_alias=None, - power_level_content_override=None, + power_level_content_override=None, # Doesn't apply when initial state has power level state event content creator_join_profile=None, ): def create(etype, content, **kwargs): From 18674eebb1fa5d7445952d7e201afe33bd040523 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 6 Jan 2020 12:28:58 +0000 Subject: [PATCH 0752/1623] Workaround for error when fetching notary's own key (#6620) * Kill off redundant SynapseRequestFactory We already get the Site via the Channel, so there's no need for a dedicated RequestFactory: we can just use the right constructor. * Workaround for error when fetching notary's own key As a notary server, when we return our own keys, include all of our signing keys in verify_keys. This is a workaround for #6596. --- changelog.d/6620.misc | 1 + synapse/rest/key/v2/remote_key_resource.py | 30 ++-- tests/rest/key/v2/test_remote_key_resource.py | 130 ++++++++++++++++++ tests/unittest.py | 11 +- 4 files changed, 163 insertions(+), 9 deletions(-) create mode 100644 changelog.d/6620.misc create mode 100644 tests/rest/key/v2/test_remote_key_resource.py diff --git a/changelog.d/6620.misc b/changelog.d/6620.misc new file mode 100644 index 0000000000..8bfb78fb20 --- /dev/null +++ b/changelog.d/6620.misc @@ -0,0 +1 @@ +Add a workaround for synapse raising exceptions when fetching the notary's own key from the notary. diff --git a/synapse/rest/key/v2/remote_key_resource.py b/synapse/rest/key/v2/remote_key_resource.py index e7fc3f0431..bf5e0eb844 100644 --- a/synapse/rest/key/v2/remote_key_resource.py +++ b/synapse/rest/key/v2/remote_key_resource.py @@ -15,6 +15,7 @@ import logging from canonicaljson import encode_canonical_json, json +from signedjson.key import encode_verify_key_base64 from signedjson.sign import sign_json from twisted.internet import defer @@ -216,15 +217,28 @@ class RemoteKey(DirectServeResource): if cache_misses and query_remote_on_cache_miss: yield self.fetcher.get_keys(cache_misses) yield self.query_keys(request, query, query_remote_on_cache_miss=False) - else: - signed_keys = [] - for key_json in json_results: - key_json = json.loads(key_json) + return + + signed_keys = [] + for key_json in json_results: + key_json = json.loads(key_json) + + # backwards-compatibility hack for #6596: if the requested key belongs + # to us, make sure that all of the signing keys appear in the + # "verify_keys" section. + if key_json["server_name"] == self.config.server_name: + verify_keys = key_json["verify_keys"] for signing_key in self.config.key_server_signing_keys: - key_json = sign_json(key_json, self.config.server_name, signing_key) + key_id = "%s:%s" % (signing_key.alg, signing_key.version) + verify_keys[key_id] = { + "key": encode_verify_key_base64(signing_key.verify_key) + } - signed_keys.append(key_json) + for signing_key in self.config.key_server_signing_keys: + key_json = sign_json(key_json, self.config.server_name, signing_key) - results = {"server_keys": signed_keys} + signed_keys.append(key_json) - respond_with_json_bytes(request, 200, encode_canonical_json(results)) + results = {"server_keys": signed_keys} + + respond_with_json_bytes(request, 200, encode_canonical_json(results)) diff --git a/tests/rest/key/v2/test_remote_key_resource.py b/tests/rest/key/v2/test_remote_key_resource.py new file mode 100644 index 0000000000..d8246b4e78 --- /dev/null +++ b/tests/rest/key/v2/test_remote_key_resource.py @@ -0,0 +1,130 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import urllib.parse +from io import BytesIO + +from mock import Mock + +import signedjson.key +from nacl.signing import SigningKey +from signedjson.sign import sign_json + +from twisted.web.resource import NoResource + +from synapse.http.site import SynapseRequest +from synapse.rest.key.v2 import KeyApiV2Resource +from synapse.util.httpresourcetree import create_resource_tree + +from tests import unittest +from tests.server import FakeChannel, wait_until_result + + +class RemoteKeyResourceTestCase(unittest.HomeserverTestCase): + def make_homeserver(self, reactor, clock): + self.http_client = Mock() + return self.setup_test_homeserver(http_client=self.http_client) + + def create_test_json_resource(self): + return create_resource_tree( + {"/_matrix/key/v2": KeyApiV2Resource(self.hs)}, root_resource=NoResource() + ) + + def expect_outgoing_key_request( + self, server_name: str, signing_key: SigningKey + ) -> None: + """ + Tell the mock http client to expect an outgoing GET request for the given key + """ + + def get_json(destination, path, ignore_backoff=False, **kwargs): + self.assertTrue(ignore_backoff) + self.assertEqual(destination, server_name) + key_id = "%s:%s" % (signing_key.alg, signing_key.version) + self.assertEqual( + path, "/_matrix/key/v2/server/%s" % (urllib.parse.quote(key_id),) + ) + + response = { + "server_name": server_name, + "old_verify_keys": {}, + "valid_until_ts": 200 * 1000, + "verify_keys": { + key_id: { + "key": signedjson.key.encode_verify_key_base64( + signing_key.verify_key + ) + } + }, + } + sign_json(response, server_name, signing_key) + return response + + self.http_client.get_json.side_effect = get_json + + def make_notary_request(self, server_name: str, key_id: str) -> dict: + """Send a GET request to the test server requesting the given key. + + Checks that the response is a 200 and returns the decoded json body. + """ + channel = FakeChannel(self.site, self.reactor) + req = SynapseRequest(channel) + req.content = BytesIO(b"") + req.requestReceived( + b"GET", + b"/_matrix/key/v2/query/%s/%s" + % (server_name.encode("utf-8"), key_id.encode("utf-8")), + b"1.1", + ) + wait_until_result(self.reactor, req) + self.assertEqual(channel.code, 200) + resp = channel.json_body + return resp + + def test_get_key(self): + """Fetch a remote key""" + SERVER_NAME = "remote.server" + testkey = signedjson.key.generate_signing_key("ver1") + self.expect_outgoing_key_request(SERVER_NAME, testkey) + + resp = self.make_notary_request(SERVER_NAME, "ed25519:ver1") + keys = resp["server_keys"] + self.assertEqual(len(keys), 1) + + self.assertIn("ed25519:ver1", keys[0]["verify_keys"]) + self.assertEqual(len(keys[0]["verify_keys"]), 1) + + # it should be signed by both the origin server and the notary + self.assertIn(SERVER_NAME, keys[0]["signatures"]) + self.assertIn(self.hs.hostname, keys[0]["signatures"]) + + def test_get_own_key(self): + """Fetch our own key""" + testkey = signedjson.key.generate_signing_key("ver1") + self.expect_outgoing_key_request(self.hs.hostname, testkey) + + resp = self.make_notary_request(self.hs.hostname, "ed25519:ver1") + keys = resp["server_keys"] + self.assertEqual(len(keys), 1) + + # it should be signed by both itself, and the notary signing key + sigs = keys[0]["signatures"] + self.assertEqual(len(sigs), 1) + self.assertIn(self.hs.hostname, sigs) + oursigs = sigs[self.hs.hostname] + self.assertEqual(len(oursigs), 2) + + # and both keys should be present in the verify_keys section + self.assertIn("ed25519:ver1", keys[0]["verify_keys"]) + self.assertIn("ed25519:a_lPym", keys[0]["verify_keys"]) diff --git a/tests/unittest.py b/tests/unittest.py index b30b7d1718..cbda237278 100644 --- a/tests/unittest.py +++ b/tests/unittest.py @@ -36,7 +36,7 @@ from synapse.config.homeserver import HomeServerConfig from synapse.config.ratelimiting import FederationRateLimitConfig from synapse.federation.transport import server as federation_server from synapse.http.server import JsonResource -from synapse.http.site import SynapseRequest +from synapse.http.site import SynapseRequest, SynapseSite from synapse.logging.context import LoggingContext from synapse.server import HomeServer from synapse.types import Requester, UserID, create_requester @@ -210,6 +210,15 @@ class HomeserverTestCase(TestCase): # Register the resources self.resource = self.create_test_json_resource() + # create a site to wrap the resource. + self.site = SynapseSite( + logger_name="synapse.access.http.fake", + site_tag="test", + config={}, + resource=self.resource, + server_version_string="1", + ) + from tests.rest.client.v1.utils import RestHelper self.helper = RestHelper(self.hs, self.resource, getattr(self, "user_id", None)) From 4b36b482e0cc1a63db27534c4ea5d9608cdb6a79 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 6 Jan 2020 12:33:56 +0000 Subject: [PATCH 0753/1623] Fix exception when fetching notary server's old keys (#6625) Lift the restriction that *all* the keys used for signing v2 key responses be present in verify_keys. Fixes #6596. --- changelog.d/6625.bugfix | 1 + synapse/crypto/keyring.py | 13 ++-- tests/crypto/test_keyring.py | 147 +++++++++++++++++++++++------------ 3 files changed, 107 insertions(+), 54 deletions(-) create mode 100644 changelog.d/6625.bugfix diff --git a/changelog.d/6625.bugfix b/changelog.d/6625.bugfix new file mode 100644 index 0000000000..a8dc5587dc --- /dev/null +++ b/changelog.d/6625.bugfix @@ -0,0 +1 @@ +Fix exception when fetching the `matrix.org:ed25519:auto` key. diff --git a/synapse/crypto/keyring.py b/synapse/crypto/keyring.py index 7cfad192e8..6fe5a6a26a 100644 --- a/synapse/crypto/keyring.py +++ b/synapse/crypto/keyring.py @@ -511,17 +511,18 @@ class BaseV2KeyFetcher(object): server_name = response_json["server_name"] verified = False for key_id in response_json["signatures"].get(server_name, {}): - # each of the keys used for the signature must be present in the response - # json. key = verify_keys.get(key_id) if not key: - raise KeyLookupError( - "Key response is signed by key id %s:%s but that key is not " - "present in the response" % (server_name, key_id) - ) + # the key may not be present in verify_keys if: + # * we got the key from the notary server, and: + # * the key belongs to the notary server, and: + # * the notary server is using a different key to sign notary + # responses. + continue verify_signed_json(response_json, server_name, key.verify_key) verified = True + break if not verified: raise KeyLookupError( diff --git a/tests/crypto/test_keyring.py b/tests/crypto/test_keyring.py index 8efd39c7f7..34d5895f18 100644 --- a/tests/crypto/test_keyring.py +++ b/tests/crypto/test_keyring.py @@ -19,6 +19,7 @@ from mock import Mock import canonicaljson import signedjson.key import signedjson.sign +from nacl.signing import SigningKey from signedjson.key import encode_verify_key_base64, get_verify_key from twisted.internet import defer @@ -412,6 +413,49 @@ class PerspectivesKeyFetcherTestCase(unittest.HomeserverTestCase): handlers=None, http_client=self.http_client, config=config ) + def build_perspectives_response( + self, server_name: str, signing_key: SigningKey, valid_until_ts: int, + ) -> dict: + """ + Build a valid perspectives server response to a request for the given key + """ + verify_key = signedjson.key.get_verify_key(signing_key) + verifykey_id = "%s:%s" % (verify_key.alg, verify_key.version) + + response = { + "server_name": server_name, + "old_verify_keys": {}, + "valid_until_ts": valid_until_ts, + "verify_keys": { + verifykey_id: { + "key": signedjson.key.encode_verify_key_base64(verify_key) + } + }, + } + # the response must be signed by both the origin server and the perspectives + # server. + signedjson.sign.sign_json(response, server_name, signing_key) + self.mock_perspective_server.sign_response(response) + return response + + def expect_outgoing_key_query( + self, expected_server_name: str, expected_key_id: str, response: dict + ) -> None: + """ + Tell the mock http client to expect a perspectives-server key query + """ + + def post_json(destination, path, data, **kwargs): + self.assertEqual(destination, self.mock_perspective_server.server_name) + self.assertEqual(path, "/_matrix/key/v2/query") + + # check that the request is for the expected key + q = data["server_keys"] + self.assertEqual(list(q[expected_server_name].keys()), [expected_key_id]) + return {"server_keys": [response]} + + self.http_client.post_json.side_effect = post_json + def test_get_keys_from_perspectives(self): # arbitrarily advance the clock a bit self.reactor.advance(100) @@ -424,33 +468,61 @@ class PerspectivesKeyFetcherTestCase(unittest.HomeserverTestCase): testverifykey_id = "ed25519:ver1" VALID_UNTIL_TS = 200 * 1000 - # valid response - response = { - "server_name": SERVER_NAME, - "old_verify_keys": {}, - "valid_until_ts": VALID_UNTIL_TS, - "verify_keys": { - testverifykey_id: { - "key": signedjson.key.encode_verify_key_base64(testverifykey) - } - }, - } + response = self.build_perspectives_response( + SERVER_NAME, testkey, VALID_UNTIL_TS, + ) - # the response must be signed by both the origin server and the perspectives - # server. - signedjson.sign.sign_json(response, SERVER_NAME, testkey) - self.mock_perspective_server.sign_response(response) + self.expect_outgoing_key_query(SERVER_NAME, "key1", response) - def post_json(destination, path, data, **kwargs): - self.assertEqual(destination, self.mock_perspective_server.server_name) - self.assertEqual(path, "/_matrix/key/v2/query") + keys_to_fetch = {SERVER_NAME: {"key1": 0}} + keys = self.get_success(fetcher.get_keys(keys_to_fetch)) + self.assertIn(SERVER_NAME, keys) + k = keys[SERVER_NAME][testverifykey_id] + self.assertEqual(k.valid_until_ts, VALID_UNTIL_TS) + self.assertEqual(k.verify_key, testverifykey) + self.assertEqual(k.verify_key.alg, "ed25519") + self.assertEqual(k.verify_key.version, "ver1") - # check that the request is for the expected key - q = data["server_keys"] - self.assertEqual(list(q[SERVER_NAME].keys()), ["key1"]) - return {"server_keys": [response]} + # check that the perspectives store is correctly updated + lookup_triplet = (SERVER_NAME, testverifykey_id, None) + key_json = self.get_success( + self.hs.get_datastore().get_server_keys_json([lookup_triplet]) + ) + res = key_json[lookup_triplet] + self.assertEqual(len(res), 1) + res = res[0] + self.assertEqual(res["key_id"], testverifykey_id) + self.assertEqual(res["from_server"], self.mock_perspective_server.server_name) + self.assertEqual(res["ts_added_ms"], self.reactor.seconds() * 1000) + self.assertEqual(res["ts_valid_until_ms"], VALID_UNTIL_TS) - self.http_client.post_json.side_effect = post_json + self.assertEqual( + bytes(res["key_json"]), canonicaljson.encode_canonical_json(response) + ) + + def test_get_perspectives_own_key(self): + """Check that we can get the perspectives server's own keys + + This is slightly complicated by the fact that the perspectives server may + use different keys for signing notary responses. + """ + + # arbitrarily advance the clock a bit + self.reactor.advance(100) + + fetcher = PerspectivesKeyFetcher(self.hs) + + SERVER_NAME = self.mock_perspective_server.server_name + testkey = signedjson.key.generate_signing_key("ver1") + testverifykey = signedjson.key.get_verify_key(testkey) + testverifykey_id = "ed25519:ver1" + VALID_UNTIL_TS = 200 * 1000 + + response = self.build_perspectives_response( + SERVER_NAME, testkey, VALID_UNTIL_TS + ) + + self.expect_outgoing_key_query(SERVER_NAME, "key1", response) keys_to_fetch = {SERVER_NAME: {"key1": 0}} keys = self.get_success(fetcher.get_keys(keys_to_fetch)) @@ -490,35 +562,14 @@ class PerspectivesKeyFetcherTestCase(unittest.HomeserverTestCase): VALID_UNTIL_TS = 200 * 1000 def build_response(): - # valid response - response = { - "server_name": SERVER_NAME, - "old_verify_keys": {}, - "valid_until_ts": VALID_UNTIL_TS, - "verify_keys": { - testverifykey_id: { - "key": signedjson.key.encode_verify_key_base64(testverifykey) - } - }, - } - - # the response must be signed by both the origin server and the perspectives - # server. - signedjson.sign.sign_json(response, SERVER_NAME, testkey) - self.mock_perspective_server.sign_response(response) - return response + return self.build_perspectives_response( + SERVER_NAME, testkey, VALID_UNTIL_TS + ) def get_key_from_perspectives(response): fetcher = PerspectivesKeyFetcher(self.hs) keys_to_fetch = {SERVER_NAME: {"key1": 0}} - - def post_json(destination, path, data, **kwargs): - self.assertEqual(destination, self.mock_perspective_server.server_name) - self.assertEqual(path, "/_matrix/key/v2/query") - return {"server_keys": [response]} - - self.http_client.post_json.side_effect = post_json - + self.expect_outgoing_key_query(SERVER_NAME, "key1", response) return self.get_success(fetcher.get_keys(keys_to_fetch)) # start with a valid response so we can check we are testing the right thing From 5a047816434e2ce2df8b80eb63a49c17dc3085fb Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 3 Jan 2020 15:31:09 +0000 Subject: [PATCH 0754/1623] rename get_prev_events_for_room to get_prev_events_and_hashes_for_room ... to make way for a new method which just returns the event ids --- synapse/handlers/message.py | 6 ++++-- synapse/handlers/room_member.py | 4 +++- synapse/storage/data_stores/main/event_federation.py | 5 +++-- tests/storage/test_event_federation.py | 4 ++-- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index 4ad752205f..2695975a16 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -740,7 +740,7 @@ class EventCreationHandler(object): % (len(prev_events_and_hashes),) ) else: - prev_events_and_hashes = yield self.store.get_prev_events_for_room( + prev_events_and_hashes = yield self.store.get_prev_events_and_hashes_for_room( builder.room_id ) @@ -1042,7 +1042,9 @@ class EventCreationHandler(object): # For each room we need to find a joined member we can use to send # the dummy event with. - prev_events_and_hashes = yield self.store.get_prev_events_for_room(room_id) + prev_events_and_hashes = yield self.store.get_prev_events_and_hashes_for_room( + room_id + ) latest_event_ids = (event_id for (event_id, _, _) in prev_events_and_hashes) diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index 44c5e3239c..91bb34cd55 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -370,7 +370,9 @@ class RoomMemberHandler(object): if block_invite: raise SynapseError(403, "Invites have been disabled on this server") - prev_events_and_hashes = yield self.store.get_prev_events_for_room(room_id) + prev_events_and_hashes = yield self.store.get_prev_events_and_hashes_for_room( + room_id + ) latest_event_ids = (event_id for (event_id, _, _) in prev_events_and_hashes) current_state_ids = yield self.state_handler.get_current_state_ids( diff --git a/synapse/storage/data_stores/main/event_federation.py b/synapse/storage/data_stores/main/event_federation.py index 1f517e8fad..266fc9715f 100644 --- a/synapse/storage/data_stores/main/event_federation.py +++ b/synapse/storage/data_stores/main/event_federation.py @@ -149,9 +149,10 @@ class EventFederationWorkerStore(EventsWorkerStore, SignatureWorkerStore, SQLBas ) @defer.inlineCallbacks - def get_prev_events_for_room(self, room_id): + def get_prev_events_and_hashes_for_room(self, room_id): """ - Gets a subset of the current forward extremities in the given room. + Gets a subset of the current forward extremities in the given room, + along with their depths and hashes. Limits the result to 10 extremities, so that we can avoid creating events which refer to hundreds of prev_events. diff --git a/tests/storage/test_event_federation.py b/tests/storage/test_event_federation.py index eadfb90a22..3a68bf3274 100644 --- a/tests/storage/test_event_federation.py +++ b/tests/storage/test_event_federation.py @@ -26,7 +26,7 @@ class EventFederationWorkerStoreTestCase(tests.unittest.TestCase): self.store = hs.get_datastore() @defer.inlineCallbacks - def test_get_prev_events_for_room(self): + def test_get_prev_events_and_hashes_for_room(self): room_id = "@ROOM:local" # add a bunch of events and hashes to act as forward extremities @@ -64,7 +64,7 @@ class EventFederationWorkerStoreTestCase(tests.unittest.TestCase): yield self.store.db.runInteraction("insert", insert_event, i) # this should get the last five and five others - r = yield self.store.get_prev_events_for_room(room_id) + r = yield self.store.get_prev_events_and_hashes_for_room(room_id) self.assertEqual(10, len(r)) for i in range(0, 5): el = r[i] From 15720092ac7a1af57dde7018a8872d93bbb9d36b Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 3 Jan 2020 16:09:24 +0000 Subject: [PATCH 0755/1623] replace get_prev_events_and_hashes_for_room with get_prev_events_for_room in create_new_client_event --- synapse/handlers/message.py | 12 ++----- .../data_stores/main/event_federation.py | 35 +++++++++++++++++++ 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index 2695975a16..a1c289b24a 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -739,17 +739,11 @@ class EventCreationHandler(object): "Attempting to create an event with %i prev_events" % (len(prev_events_and_hashes),) ) + prev_event_ids = [event_id for event_id, _, _ in prev_events_and_hashes] else: - prev_events_and_hashes = yield self.store.get_prev_events_and_hashes_for_room( - builder.room_id - ) + prev_event_ids = yield self.store.get_prev_events_for_room(builder.room_id) - prev_events = [ - (event_id, prev_hashes) - for event_id, prev_hashes, _ in prev_events_and_hashes - ] - - event = yield builder.build(prev_event_ids=[p for p, _ in prev_events]) + event = yield builder.build(prev_event_ids=prev_event_ids) context = yield self.state.compute_event_context(event) if requester: context.app_service = requester.app_service diff --git a/synapse/storage/data_stores/main/event_federation.py b/synapse/storage/data_stores/main/event_federation.py index 266fc9715f..88e6489576 100644 --- a/synapse/storage/data_stores/main/event_federation.py +++ b/synapse/storage/data_stores/main/event_federation.py @@ -177,6 +177,41 @@ class EventFederationWorkerStore(EventsWorkerStore, SignatureWorkerStore, SQLBas return res + def get_prev_events_for_room(self, room_id: str): + """ + Gets a subset of the current forward extremities in the given room. + + Limits the result to 10 extremities, so that we can avoid creating + events which refer to hundreds of prev_events. + + Args: + room_id (str): room_id + + Returns: + Deferred[List[str]]: the event ids of the forward extremites + + """ + + return self.db.runInteraction( + "get_prev_events_for_room", self._get_prev_events_for_room_txn, room_id + ) + + def _get_prev_events_for_room_txn(self, txn, room_id: str): + # we just use the 10 newest events. Older events will become + # prev_events of future events. + + sql = """ + SELECT e.event_id FROM event_forward_extremities AS f + INNER JOIN events AS e USING (event_id) + WHERE f.room_id = ? + ORDER BY e.depth DESC + LIMIT 10 + """ + + txn.execute(sql, (room_id,)) + + return [row[0] for row in txn] + def get_latest_event_ids_and_hashes_in_room(self, room_id): """ Gets the current forward extremities in the given room From 66ca914dc0290b16516cbb599dc4be06793963ed Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 3 Jan 2020 16:16:09 +0000 Subject: [PATCH 0756/1623] Remove unused hashes and depths from create_new_client_event params --- synapse/handlers/message.py | 26 ++++++++++++++------------ synapse/types.py | 12 ++++++++++++ 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index a1c289b24a..5415b0c9ee 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -48,7 +48,7 @@ from synapse.metrics.background_process_metrics import run_as_background_process from synapse.replication.http.send_event import ReplicationSendEventRestServlet from synapse.storage.data_stores.main.events_worker import EventRedactBehaviour from synapse.storage.state import StateFilter -from synapse.types import RoomAlias, UserID, create_requester +from synapse.types import Collection, RoomAlias, UserID, create_requester from synapse.util.async_helpers import Linearizer from synapse.util.frozenutils import frozendict_json_encoder from synapse.util.metrics import measure_func @@ -497,10 +497,14 @@ class EventCreationHandler(object): if txn_id is not None: builder.internal_metadata.txn_id = txn_id + prev_event_ids = ( + None + if prev_events_and_hashes is None + else [event_id for event_id, _, _ in prev_events_and_hashes] + ) + event, context = yield self.create_new_client_event( - builder=builder, - requester=requester, - prev_events_and_hashes=prev_events_and_hashes, + builder=builder, requester=requester, prev_event_ids=prev_event_ids, ) # In an ideal world we wouldn't need the second part of this condition. However, @@ -714,7 +718,7 @@ class EventCreationHandler(object): @measure_func("create_new_client_event") @defer.inlineCallbacks def create_new_client_event( - self, builder, requester=None, prev_events_and_hashes=None + self, builder, requester=None, prev_event_ids: Optional[Collection[str]] = None ): """Create a new event for a local client @@ -723,10 +727,9 @@ class EventCreationHandler(object): requester (synapse.types.Requester|None): - prev_events_and_hashes (list[(str, dict[str, str], int)]|None): + prev_event_ids: the forward extremities to use as the prev_events for the - new event. For each event, a tuple of (event_id, hashes, depth) - where *hashes* is a map from algorithm to hash. + new event. If None, they will be requested from the database. @@ -734,12 +737,11 @@ class EventCreationHandler(object): Deferred[(synapse.events.EventBase, synapse.events.snapshot.EventContext)] """ - if prev_events_and_hashes is not None: - assert len(prev_events_and_hashes) <= 10, ( + if prev_event_ids is not None: + assert len(prev_event_ids) <= 10, ( "Attempting to create an event with %i prev_events" - % (len(prev_events_and_hashes),) + % (len(prev_event_ids),) ) - prev_event_ids = [event_id for event_id, _, _ in prev_events_and_hashes] else: prev_event_ids = yield self.store.get_prev_events_for_room(builder.room_id) diff --git a/synapse/types.py b/synapse/types.py index aafc3ffe74..cd996c0b5a 100644 --- a/synapse/types.py +++ b/synapse/types.py @@ -15,6 +15,7 @@ # limitations under the License. import re import string +import sys from collections import namedtuple import attr @@ -23,6 +24,17 @@ from unpaddedbase64 import decode_base64 from synapse.api.errors import SynapseError +# define a version of typing.Collection that works on python 3.5 +if sys.version_info[:3] >= (3, 6, 0): + from typing import Collection +else: + from typing import Sized, Iterable, Container, TypeVar + + T_co = TypeVar("T_co", covariant=True) + + class Collection(Iterable[T_co], Container[T_co], Sized): + __slots__ = () + class Requester( namedtuple( From 3bef62488e5cff4dfb33454f2f2e18cc928f319b Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 3 Jan 2020 16:19:55 +0000 Subject: [PATCH 0757/1623] Remove unused hashes and depths from create_event params --- synapse/handlers/message.py | 21 +++++---------------- synapse/handlers/room_member.py | 8 +++++++- tests/unittest.py | 6 +----- 3 files changed, 13 insertions(+), 22 deletions(-) diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index 5415b0c9ee..8ea3aca2f4 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -422,7 +422,7 @@ class EventCreationHandler(object): event_dict, token_id=None, txn_id=None, - prev_events_and_hashes=None, + prev_event_ids: Optional[Collection[str]] = None, require_consent=True, ): """ @@ -439,10 +439,9 @@ class EventCreationHandler(object): token_id (str) txn_id (str) - prev_events_and_hashes (list[(str, dict[str, str], int)]|None): + prev_event_ids: the forward extremities to use as the prev_events for the - new event. For each event, a tuple of (event_id, hashes, depth) - where *hashes* is a map from algorithm to hash. + new event. If None, they will be requested from the database. @@ -497,12 +496,6 @@ class EventCreationHandler(object): if txn_id is not None: builder.internal_metadata.txn_id = txn_id - prev_event_ids = ( - None - if prev_events_and_hashes is None - else [event_id for event_id, _, _ in prev_events_and_hashes] - ) - event, context = yield self.create_new_client_event( builder=builder, requester=requester, prev_event_ids=prev_event_ids, ) @@ -1038,11 +1031,7 @@ class EventCreationHandler(object): # For each room we need to find a joined member we can use to send # the dummy event with. - prev_events_and_hashes = yield self.store.get_prev_events_and_hashes_for_room( - room_id - ) - - latest_event_ids = (event_id for (event_id, _, _) in prev_events_and_hashes) + latest_event_ids = yield self.store.get_prev_events_for_room(room_id) members = yield self.state.get_current_users_in_room( room_id, latest_event_ids=latest_event_ids @@ -1061,7 +1050,7 @@ class EventCreationHandler(object): "room_id": room_id, "sender": user_id, }, - prev_events_and_hashes=prev_events_and_hashes, + prev_event_ids=latest_event_ids, ) event.internal_metadata.proactively_send = False diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index 91bb34cd55..d550ba8ab4 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -164,6 +164,12 @@ class RoomMemberHandler(object): if requester.is_guest: content["kind"] = "guest" + prev_event_ids = ( + None + if prev_events_and_hashes is None + else [event_id for event_id, _, _ in prev_events_and_hashes] + ) + event, context = yield self.event_creation_handler.create_event( requester, { @@ -177,7 +183,7 @@ class RoomMemberHandler(object): }, token_id=requester.access_token_id, txn_id=txn_id, - prev_events_and_hashes=prev_events_and_hashes, + prev_event_ids=prev_event_ids, require_consent=require_consent, ) diff --git a/tests/unittest.py b/tests/unittest.py index b30b7d1718..07b50c0ccd 100644 --- a/tests/unittest.py +++ b/tests/unittest.py @@ -522,10 +522,6 @@ class HomeserverTestCase(TestCase): secrets = self.hs.get_secrets() requester = Requester(user, None, False, None, None) - prev_events_and_hashes = None - if prev_event_ids: - prev_events_and_hashes = [[p, {}, 0] for p in prev_event_ids] - event, context = self.get_success( event_creator.create_event( requester, @@ -535,7 +531,7 @@ class HomeserverTestCase(TestCase): "sender": user.to_string(), "content": {"body": secrets.token_hex(), "msgtype": "m.text"}, }, - prev_events_and_hashes=prev_events_and_hashes, + prev_event_ids=prev_event_ids, ) ) From 38e0829a4c1b82803f018821445d130708fdf55b Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 3 Jan 2020 16:24:59 +0000 Subject: [PATCH 0758/1623] Remove unused hashes and depths from _update_membership params --- synapse/handlers/room_member.py | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index d550ba8ab4..3dc2b2dd8a 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -25,7 +25,7 @@ from twisted.internet import defer from synapse import types from synapse.api.constants import EventTypes, Membership from synapse.api.errors import AuthError, Codes, SynapseError -from synapse.types import RoomID, UserID +from synapse.types import Collection, RoomID, UserID from synapse.util.async_helpers import Linearizer from synapse.util.distributor import user_joined_room, user_left_room @@ -149,7 +149,7 @@ class RoomMemberHandler(object): target, room_id, membership, - prev_events_and_hashes, + prev_event_ids: Collection[str], txn_id=None, ratelimit=True, content=None, @@ -164,12 +164,6 @@ class RoomMemberHandler(object): if requester.is_guest: content["kind"] = "guest" - prev_event_ids = ( - None - if prev_events_and_hashes is None - else [event_id for event_id, _, _ in prev_events_and_hashes] - ) - event, context = yield self.event_creation_handler.create_event( requester, { @@ -376,10 +370,7 @@ class RoomMemberHandler(object): if block_invite: raise SynapseError(403, "Invites have been disabled on this server") - prev_events_and_hashes = yield self.store.get_prev_events_and_hashes_for_room( - room_id - ) - latest_event_ids = (event_id for (event_id, _, _) in prev_events_and_hashes) + latest_event_ids = yield self.store.get_prev_events_for_room(room_id) current_state_ids = yield self.state_handler.get_current_state_ids( room_id, latest_event_ids=latest_event_ids @@ -493,7 +484,7 @@ class RoomMemberHandler(object): membership=effective_membership_state, txn_id=txn_id, ratelimit=ratelimit, - prev_events_and_hashes=prev_events_and_hashes, + prev_event_ids=latest_event_ids, content=content, require_consent=require_consent, ) From dc41fbf0dda981df117d8cf1938e023a38836cda Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 3 Jan 2020 16:30:51 +0000 Subject: [PATCH 0759/1623] Remove unused get_prev_events_and_hashes_for_room --- .../data_stores/main/event_federation.py | 30 ------------------- tests/storage/test_event_federation.py | 19 ++++-------- 2 files changed, 6 insertions(+), 43 deletions(-) diff --git a/synapse/storage/data_stores/main/event_federation.py b/synapse/storage/data_stores/main/event_federation.py index 88e6489576..32e76621a7 100644 --- a/synapse/storage/data_stores/main/event_federation.py +++ b/synapse/storage/data_stores/main/event_federation.py @@ -14,7 +14,6 @@ # limitations under the License. import itertools import logging -import random from six.moves import range from six.moves.queue import Empty, PriorityQueue @@ -148,35 +147,6 @@ class EventFederationWorkerStore(EventsWorkerStore, SignatureWorkerStore, SQLBas retcol="event_id", ) - @defer.inlineCallbacks - def get_prev_events_and_hashes_for_room(self, room_id): - """ - Gets a subset of the current forward extremities in the given room, - along with their depths and hashes. - - Limits the result to 10 extremities, so that we can avoid creating - events which refer to hundreds of prev_events. - - Args: - room_id (str): room_id - - Returns: - Deferred[list[(str, dict[str, str], int)]] - for each event, a tuple of (event_id, hashes, depth) - where *hashes* is a map from algorithm to hash. - """ - res = yield self.get_latest_event_ids_and_hashes_in_room(room_id) - if len(res) > 10: - # Sort by reverse depth, so we point to the most recent. - res.sort(key=lambda a: -a[2]) - - # we use half of the limit for the actual most recent events, and - # the other half to randomly point to some of the older events, to - # make sure that we don't completely ignore the older events. - res = res[0:5] + random.sample(res[5:], 5) - - return res - def get_prev_events_for_room(self, room_id: str): """ Gets a subset of the current forward extremities in the given room. diff --git a/tests/storage/test_event_federation.py b/tests/storage/test_event_federation.py index 3a68bf3274..a331517f4d 100644 --- a/tests/storage/test_event_federation.py +++ b/tests/storage/test_event_federation.py @@ -26,7 +26,7 @@ class EventFederationWorkerStoreTestCase(tests.unittest.TestCase): self.store = hs.get_datastore() @defer.inlineCallbacks - def test_get_prev_events_and_hashes_for_room(self): + def test_get_prev_events_for_room(self): room_id = "@ROOM:local" # add a bunch of events and hashes to act as forward extremities @@ -60,21 +60,14 @@ class EventFederationWorkerStoreTestCase(tests.unittest.TestCase): (event_id, bytearray(b"ffff")), ) - for i in range(0, 11): + for i in range(0, 20): yield self.store.db.runInteraction("insert", insert_event, i) - # this should get the last five and five others - r = yield self.store.get_prev_events_and_hashes_for_room(room_id) + # this should get the last ten + r = yield self.store.get_prev_events_for_room(room_id) self.assertEqual(10, len(r)) - for i in range(0, 5): - el = r[i] - depth = el[2] - self.assertEqual(10 - i, depth) - - for i in range(5, 5): - el = r[i] - depth = el[2] - self.assertLessEqual(5, depth) + for i in range(0, 10): + self.assertEqual("$event_%i:local" % (19 - i), r[i]) @defer.inlineCallbacks def test_get_rooms_with_many_extremities(self): From a7d2e5b37f5bfbc285bcf4c533c1a48ff0f0ff8f Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 3 Jan 2020 16:32:14 +0000 Subject: [PATCH 0760/1623] Remove unused get_latest_event_ids_and_hashes_in_room --- .../data_stores/main/event_federation.py | 42 ------------------- 1 file changed, 42 deletions(-) diff --git a/synapse/storage/data_stores/main/event_federation.py b/synapse/storage/data_stores/main/event_federation.py index 32e76621a7..5cb8cd96d1 100644 --- a/synapse/storage/data_stores/main/event_federation.py +++ b/synapse/storage/data_stores/main/event_federation.py @@ -18,8 +18,6 @@ import logging from six.moves import range from six.moves.queue import Empty, PriorityQueue -from unpaddedbase64 import encode_base64 - from twisted.internet import defer from synapse.api.errors import StoreError @@ -182,25 +180,6 @@ class EventFederationWorkerStore(EventsWorkerStore, SignatureWorkerStore, SQLBas return [row[0] for row in txn] - def get_latest_event_ids_and_hashes_in_room(self, room_id): - """ - Gets the current forward extremities in the given room - - Args: - room_id (str): room_id - - Returns: - Deferred[list[(str, dict[str, str], int)]] - for each event, a tuple of (event_id, hashes, depth) - where *hashes* is a map from algorithm to hash. - """ - - return self.db.runInteraction( - "get_latest_event_ids_and_hashes_in_room", - self._get_latest_event_ids_and_hashes_in_room, - room_id, - ) - def get_rooms_with_many_extremities(self, min_count, limit, room_id_filter): """Get the top rooms with at least N extremities. @@ -249,27 +228,6 @@ class EventFederationWorkerStore(EventsWorkerStore, SignatureWorkerStore, SQLBas desc="get_latest_event_ids_in_room", ) - def _get_latest_event_ids_and_hashes_in_room(self, txn, room_id): - sql = ( - "SELECT e.event_id, e.depth FROM events as e " - "INNER JOIN event_forward_extremities as f " - "ON e.event_id = f.event_id " - "AND e.room_id = f.room_id " - "WHERE f.room_id = ?" - ) - - txn.execute(sql, (room_id,)) - - results = [] - for event_id, depth in txn.fetchall(): - hashes = self._get_event_reference_hashes_txn(txn, event_id) - prev_hashes = { - k: encode_base64(v) for k, v in hashes.items() if k == "sha256" - } - results.append((event_id, prev_hashes, depth)) - - return results - def get_min_depth(self, room_id): """ For hte given room, get the minimum depth we have seen for it. """ From 550b2946d8beb9c3808972e730790d6dda86d953 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 3 Jan 2020 16:54:32 +0000 Subject: [PATCH 0761/1623] changelog --- changelog.d/6629.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6629.misc diff --git a/changelog.d/6629.misc b/changelog.d/6629.misc new file mode 100644 index 0000000000..68f77af05b --- /dev/null +++ b/changelog.d/6629.misc @@ -0,0 +1 @@ +Simplify event creation code by removing redundant queries on the event_reference_hashes table. \ No newline at end of file From ab4b4ee6a7e15d1d6e83c4b826051da7df7f83e3 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 6 Jan 2020 14:34:02 +0000 Subject: [PATCH 0762/1623] Fix an error which was thrown by the PresenceHandler _on_shutdown handler. (#6640) --- changelog.d/6640.bugfix | 1 + synapse/handlers/presence.py | 9 ++------- 2 files changed, 3 insertions(+), 7 deletions(-) create mode 100644 changelog.d/6640.bugfix diff --git a/changelog.d/6640.bugfix b/changelog.d/6640.bugfix new file mode 100644 index 0000000000..8c2a129933 --- /dev/null +++ b/changelog.d/6640.bugfix @@ -0,0 +1 @@ +Fix an error which was thrown by the PresenceHandler _on_shutdown handler. diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py index 240c4add12..202aa9294f 100644 --- a/synapse/handlers/presence.py +++ b/synapse/handlers/presence.py @@ -95,12 +95,7 @@ assert LAST_ACTIVE_GRANULARITY < IDLE_TIMER class PresenceHandler(object): - def __init__(self, hs): - """ - - Args: - hs (synapse.server.HomeServer): - """ + def __init__(self, hs: "synapse.server.HomeServer"): self.hs = hs self.is_mine = hs.is_mine self.is_mine_id = hs.is_mine_id @@ -230,7 +225,7 @@ class PresenceHandler(object): is some spurious presence changes that will self-correct. """ # If the DB pool has already terminated, don't try updating - if not self.store.database.is_running(): + if not self.store.db.is_running(): return logger.info( From 9f6c1befbbb0279dca261b105148e633c3d45453 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 6 Jan 2020 14:44:01 +0000 Subject: [PATCH 0763/1623] Add experimental 'databases' config (#6580) --- changelog.d/6580.feature | 1 + synapse/config/database.py | 55 +++++++++++++++++++------ synapse/storage/data_stores/__init__.py | 21 ++++++++++ 3 files changed, 64 insertions(+), 13 deletions(-) create mode 100644 changelog.d/6580.feature diff --git a/changelog.d/6580.feature b/changelog.d/6580.feature new file mode 100644 index 0000000000..233c589c66 --- /dev/null +++ b/changelog.d/6580.feature @@ -0,0 +1 @@ +Add experimental config option to specify multiple databases. diff --git a/synapse/config/database.py b/synapse/config/database.py index 134824789c..219b32f670 100644 --- a/synapse/config/database.py +++ b/synapse/config/database.py @@ -15,7 +15,6 @@ import logging import os from textwrap import indent -from typing import List import yaml @@ -30,16 +29,13 @@ class DatabaseConnectionConfig: Args: name: A label for the database, used for logging. db_config: The config for a particular database, as per `database` - section of main config. Has two fields: `name` for database - module name, and `args` for the args to give to the database - connector. - data_stores: The list of data stores that should be provisioned on the - database. Defaults to all data stores. + section of main config. Has three fields: `name` for database + module name, `args` for the args to give to the database + connector, and optional `data_stores` that is a list of stores to + provision on this database (defaulting to all). """ - def __init__( - self, name: str, db_config: dict, data_stores: List[str] = ["main", "state"] - ): + def __init__(self, name: str, db_config: dict): if db_config["name"] not in ("sqlite3", "psycopg2"): raise ConfigError("Unsupported database type %r" % (db_config["name"],)) @@ -48,6 +44,10 @@ class DatabaseConnectionConfig: {"cp_min": 1, "cp_max": 1, "check_same_thread": False} ) + data_stores = db_config.get("data_stores") + if data_stores is None: + data_stores = ["main", "state"] + self.name = name self.config = db_config self.data_stores = data_stores @@ -59,14 +59,43 @@ class DatabaseConfig(Config): def read_config(self, config, **kwargs): self.event_cache_size = self.parse_size(config.get("event_cache_size", "10K")) + # We *experimentally* support specifying multiple databases via the + # `databases` key. This is a map from a label to database config in the + # same format as the `database` config option, plus an extra + # `data_stores` key to specify which data store goes where. For example: + # + # databases: + # master: + # name: psycopg2 + # data_stores: ["main"] + # args: {} + # state: + # name: psycopg2 + # data_stores: ["state"] + # args: {} + + multi_database_config = config.get("databases") database_config = config.get("database") - if database_config is None: - database_config = {"name": "sqlite3", "args": {}} + if multi_database_config and database_config: + raise ConfigError("Can't specify both 'database' and 'datbases' in config") - self.databases = [DatabaseConnectionConfig("master", database_config)] + if multi_database_config: + if config.get("database_path"): + raise ConfigError("Can't specify 'database_path' with 'databases'") - self.set_databasepath(config.get("database_path")) + self.databases = [ + DatabaseConnectionConfig(name, db_conf) + for name, db_conf in multi_database_config.items() + ] + + else: + if database_config is None: + database_config = {"name": "sqlite3", "args": {}} + + self.databases = [DatabaseConnectionConfig("master", database_config)] + + self.set_databasepath(config.get("database_path")) def generate_config_section(self, data_dir_path, database_conf, **kwargs): if not database_conf: diff --git a/synapse/storage/data_stores/__init__.py b/synapse/storage/data_stores/__init__.py index d20df5f076..092e803799 100644 --- a/synapse/storage/data_stores/__init__.py +++ b/synapse/storage/data_stores/__init__.py @@ -37,6 +37,8 @@ class DataStores(object): # store. self.databases = [] + self.main = None + self.state = None for database_config in hs.config.database.databases: db_name = database_config.name @@ -54,10 +56,22 @@ class DataStores(object): if "main" in database_config.data_stores: logger.info("Starting 'main' data store") + + # Sanity check we don't try and configure the main store on + # multiple databases. + if self.main: + raise Exception("'main' data store already configured") + self.main = main_store_class(database, db_conn, hs) if "state" in database_config.data_stores: logger.info("Starting 'state' data store") + + # Sanity check we don't try and configure the state store on + # multiple databases. + if self.state: + raise Exception("'state' data store already configured") + self.state = StateGroupDataStore(database, db_conn, hs) db_conn.commit() @@ -65,3 +79,10 @@ class DataStores(object): self.databases.append(database) logger.info("Database %r prepared", db_name) + + # Sanity check that we have actually configured all the required stores. + if not self.main: + raise Exception("No 'main' data store configured") + + if not self.state: + raise Exception("No 'main' data store configured") From ba897a75903129a453d4fb853190dd31f7d1193b Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 6 Jan 2020 15:22:46 +0000 Subject: [PATCH 0764/1623] Fix some test failures when frozen_dicts are enabled (#6642) Fixes #4026 --- changelog.d/6642.misc | 1 + synapse/crypto/event_signing.py | 9 ++++++--- synapse/handlers/room.py | 15 +++++++++------ synapse/handlers/room_member.py | 2 ++ synapse/storage/data_stores/main/state.py | 4 ++-- 5 files changed, 20 insertions(+), 11 deletions(-) create mode 100644 changelog.d/6642.misc diff --git a/changelog.d/6642.misc b/changelog.d/6642.misc new file mode 100644 index 0000000000..a480bbd134 --- /dev/null +++ b/changelog.d/6642.misc @@ -0,0 +1 @@ +Fix errors when frozen_dicts are enabled. diff --git a/synapse/crypto/event_signing.py b/synapse/crypto/event_signing.py index ccaa8a9920..e65bd61d97 100644 --- a/synapse/crypto/event_signing.py +++ b/synapse/crypto/event_signing.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. - +import collections.abc import hashlib import logging @@ -40,8 +40,11 @@ def check_event_content_hash(event, hash_algorithm=hashlib.sha256): # some malformed events lack a 'hashes'. Protect against it being missing # or a weird type by basically treating it the same as an unhashed event. hashes = event.get("hashes") - if not isinstance(hashes, dict): - raise SynapseError(400, "Malformed 'hashes'", Codes.UNAUTHORIZED) + # nb it might be a frozendict or a dict + if not isinstance(hashes, collections.abc.Mapping): + raise SynapseError( + 400, "Malformed 'hashes': %s" % (type(hashes),), Codes.UNAUTHORIZED + ) if name not in hashes: raise SynapseError( diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 4f489762fc..9cab2adbfb 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -16,7 +16,7 @@ # limitations under the License. """Contains functions for performing events on rooms.""" -import copy + import itertools import logging import math @@ -368,13 +368,16 @@ class RoomCreationHandler(BaseHandler): # Raise the requester's power level in the new room if necessary current_power_level = power_levels["users"][user_id] if current_power_level < needed_power_level: - # Perform a deepcopy in order to not modify the original power levels in a - # room, as its contents are preserved as the state for the old room later on - new_power_levels = copy.deepcopy(power_levels) - initial_state[(EventTypes.PowerLevels, "")] = new_power_levels + # make sure we copy the event content rather than overwriting it. + # note that if frozen_dicts are enabled, `power_levels` will be a frozen + # dict so we can't just copy.deepcopy it. - # Assign this power level to the requester + new_power_levels = {k: v for k, v in power_levels.items() if k != "users"} + new_power_levels["users"] = { + k: v for k, v in power_levels.get("users", {}).items() if k != user_id + } new_power_levels["users"][user_id] = needed_power_level + initial_state[(EventTypes.PowerLevels, "")] = new_power_levels yield self._send_events_for_new_room( requester, diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index 44c5e3239c..dbb0c3dda2 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -507,6 +507,8 @@ class RoomMemberHandler(object): Returns: Deferred """ + logger.info("Transferring room state from %s to %s", old_room_id, room_id) + # Find all local users that were in the old room and copy over each user's state users = yield self.store.get_users_in_room(old_room_id) yield self.copy_user_state_on_room_upgrade(old_room_id, room_id, users) diff --git a/synapse/storage/data_stores/main/state.py b/synapse/storage/data_stores/main/state.py index 0dc39f139c..d07440e3ed 100644 --- a/synapse/storage/data_stores/main/state.py +++ b/synapse/storage/data_stores/main/state.py @@ -12,7 +12,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - +import collections.abc import logging from collections import namedtuple from typing import Iterable, Tuple @@ -107,7 +107,7 @@ class StateGroupWorkerStore(EventsWorkerStore, SQLBaseStore): predecessor = create_event.content.get("predecessor", None) # Ensure the key is a dictionary - if not isinstance(predecessor, dict): + if not isinstance(predecessor, collections.abc.Mapping): return None return predecessor From bc42da4ab8cd26d1bb3d2d3be6e0cdd2fabbd36a Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 6 Jan 2020 17:12:06 +0000 Subject: [PATCH 0765/1623] Clarify documentation on get_event* methods Make it clearer how they behave in the face of rejected and/or missing events. --- .../storage/data_stores/main/events_worker.py | 44 +++++++++++++++---- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/synapse/storage/data_stores/main/events_worker.py b/synapse/storage/data_stores/main/events_worker.py index 2c9142814c..0cce5232f5 100644 --- a/synapse/storage/data_stores/main/events_worker.py +++ b/synapse/storage/data_stores/main/events_worker.py @@ -137,7 +137,7 @@ class EventsWorkerStore(SQLBaseStore): @defer.inlineCallbacks def get_event( self, - event_id: List[str], + event_id: str, redact_behaviour: EventRedactBehaviour = EventRedactBehaviour.REDACT, get_prev_content: bool = False, allow_rejected: bool = False, @@ -148,15 +148,22 @@ class EventsWorkerStore(SQLBaseStore): Args: event_id: The event_id of the event to fetch + redact_behaviour: Determine what to do with a redacted event. Possible values: * AS_IS - Return the full event body with no redacted content * REDACT - Return the event but with a redacted body - * DISALLOW - Do not return redacted events + * DISALLOW - Do not return redacted events (behave as per allow_none + if the event is redacted) + get_prev_content: If True and event is a state event, include the previous states content in the unsigned field. - allow_rejected: If True return rejected events. + + allow_rejected: If True, return rejected events. Otherwise, + behave as per allow_none. + allow_none: If True, return None if no event found, if False throw a NotFoundError + check_room_id: if not None, check the room of the found event. If there is a mismatch, behave as per allow_none. @@ -196,14 +203,18 @@ class EventsWorkerStore(SQLBaseStore): Args: event_ids: The event_ids of the events to fetch + redact_behaviour: Determine what to do with a redacted event. Possible values: * AS_IS - Return the full event body with no redacted content * REDACT - Return the event but with a redacted body - * DISALLOW - Do not return redacted events + * DISALLOW - Do not return redacted events (omit them from the response) + get_prev_content: If True and event is a state event, include the previous states content in the unsigned field. - allow_rejected: If True return rejected events. + + allow_rejected: If True, return rejected events. Otherwise, + omits rejeted events from the response. Returns: Deferred : Dict from event_id to event. @@ -228,15 +239,21 @@ class EventsWorkerStore(SQLBaseStore): """Get events from the database and return in a list in the same order as given by `event_ids` arg. + Unknown events will be omitted from the response. + Args: event_ids: The event_ids of the events to fetch + redact_behaviour: Determine what to do with a redacted event. Possible values: * AS_IS - Return the full event body with no redacted content * REDACT - Return the event but with a redacted body - * DISALLOW - Do not return redacted events + * DISALLOW - Do not return redacted events (omit them from the response) + get_prev_content: If True and event is a state event, include the previous states content in the unsigned field. - allow_rejected: If True, return rejected events. + + allow_rejected: If True, return rejected events. Otherwise, + omits rejected events from the response. Returns: Deferred[list[EventBase]]: List of events fetched from the database. The @@ -369,9 +386,14 @@ class EventsWorkerStore(SQLBaseStore): If events are pulled from the database, they will be cached for future lookups. + Unknown events are omitted from the response. + Args: + event_ids (Iterable[str]): The event_ids of the events to fetch - allow_rejected (bool): Whether to include rejected events + + allow_rejected (bool): Whether to include rejected events. If False, + rejected events are omitted from the response. Returns: Deferred[Dict[str, _EventCacheEntry]]: @@ -506,9 +528,13 @@ class EventsWorkerStore(SQLBaseStore): Returned events will be added to the cache for future lookups. + Unknown events are omitted from the response. + Args: event_ids (Iterable[str]): The event_ids of the events to fetch - allow_rejected (bool): Whether to include rejected events + + allow_rejected (bool): Whether to include rejected events. If False, + rejected events are omitted from the response. Returns: Deferred[Dict[str, _EventCacheEntry]]: From c74de81bfc2925ade6d11ae0961326e5d2b1cd0f Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 6 Jan 2020 17:14:28 +0000 Subject: [PATCH 0766/1623] async/await for SyncReplicationHandler.process_and_notify --- synapse/app/synchrotron.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/synapse/app/synchrotron.py b/synapse/app/synchrotron.py index dd2132e608..fa88bd12ee 100644 --- a/synapse/app/synchrotron.py +++ b/synapse/app/synchrotron.py @@ -371,8 +371,7 @@ class SyncReplicationHandler(ReplicationClientHandler): def get_currently_syncing_users(self): return self.presence_handler.get_currently_syncing_users() - @defer.inlineCallbacks - def process_and_notify(self, stream_name, token, rows): + async def process_and_notify(self, stream_name, token, rows): try: if stream_name == "events": # We shouldn't get multiple rows per token for events stream, so @@ -380,7 +379,7 @@ class SyncReplicationHandler(ReplicationClientHandler): for row in rows: if row.type != EventsStreamEventRow.TypeId: continue - event = yield self.store.get_event(row.data.event_id) + event = await self.store.get_event(row.data.event_id) extra_users = () if event.type == EventTypes.Member: extra_users = (event.state_key,) @@ -412,11 +411,11 @@ class SyncReplicationHandler(ReplicationClientHandler): elif stream_name == "device_lists": all_room_ids = set() for row in rows: - room_ids = yield self.store.get_rooms_for_user(row.user_id) + room_ids = await self.store.get_rooms_for_user(row.user_id) all_room_ids.update(room_ids) self.notifier.on_new_event("device_list_key", token, rooms=all_room_ids) elif stream_name == "presence": - yield self.presence_handler.process_replication_rows(token, rows) + await self.presence_handler.process_replication_rows(token, rows) elif stream_name == "receipts": self.notifier.on_new_event( "groups_key", token, users=[row.user_id for row in rows] From 26c5d3d39842308da326172eb54672056f90a050 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 6 Jan 2020 17:16:28 +0000 Subject: [PATCH 0767/1623] Fix exceptions in log when rejected event is replicated --- synapse/app/synchrotron.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/synapse/app/synchrotron.py b/synapse/app/synchrotron.py index fa88bd12ee..03031ee34d 100644 --- a/synapse/app/synchrotron.py +++ b/synapse/app/synchrotron.py @@ -48,7 +48,7 @@ from synapse.replication.slave.storage.receipts import SlavedReceiptsStore from synapse.replication.slave.storage.registration import SlavedRegistrationStore from synapse.replication.slave.storage.room import RoomStore from synapse.replication.tcp.client import ReplicationClientHandler -from synapse.replication.tcp.streams.events import EventsStreamEventRow +from synapse.replication.tcp.streams.events import EventsStreamEventRow, EventsStreamRow from synapse.rest.client.v1 import events from synapse.rest.client.v1.initial_sync import InitialSyncRestServlet from synapse.rest.client.v1.room import RoomInitialSyncRestServlet @@ -379,7 +379,14 @@ class SyncReplicationHandler(ReplicationClientHandler): for row in rows: if row.type != EventsStreamEventRow.TypeId: continue - event = await self.store.get_event(row.data.event_id) + assert isinstance(row, EventsStreamRow) + + event = await self.store.get_event( + row.data.event_id, allow_rejected=True + ) + if event.rejected_reason: + continue + extra_users = () if event.type == EventTypes.Member: extra_users = (event.state_key,) From 055e6fbaa2a4f2aceb82677c7d2480982fd76c9c Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 6 Jan 2020 17:17:40 +0000 Subject: [PATCH 0768/1623] changelog --- changelog.d/6645.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6645.bugfix diff --git a/changelog.d/6645.bugfix b/changelog.d/6645.bugfix new file mode 100644 index 0000000000..f648df3fc0 --- /dev/null +++ b/changelog.d/6645.bugfix @@ -0,0 +1 @@ +Fix exceptions in the synchrotron worker log when events are rejected. From cd428a93e23e56347957a62ec3d6ca26d6e03a02 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 7 Jan 2020 12:08:58 +0000 Subject: [PATCH 0769/1623] Fix conditions failing if min_depth = 0 This could result in Synapse not fetching prev_events for new events in the room if it has missed some events. --- synapse/handlers/federation.py | 4 ++-- synapse/storage/data_stores/main/event_federation.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 72a0febc2b..61b6713c88 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -248,13 +248,13 @@ class FederationHandler(BaseHandler): prevs = set(pdu.prev_event_ids()) seen = await self.store.have_seen_events(prevs) - if min_depth and pdu.depth < min_depth: + if min_depth is not None and pdu.depth < min_depth: # This is so that we don't notify the user about this # message, to work around the fact that some events will # reference really really old events we really don't want to # send to the clients. pdu.internal_metadata.outlier = True - elif min_depth and pdu.depth > min_depth: + elif min_depth is not None and pdu.depth > min_depth: missing_prevs = prevs - seen if sent_to_us_directly and missing_prevs: # If we're missing stuff, ensure we only fetch stuff one diff --git a/synapse/storage/data_stores/main/event_federation.py b/synapse/storage/data_stores/main/event_federation.py index 1f517e8fad..1d18f13801 100644 --- a/synapse/storage/data_stores/main/event_federation.py +++ b/synapse/storage/data_stores/main/event_federation.py @@ -506,7 +506,7 @@ class EventFederationStore(EventFederationWorkerStore): def _update_min_depth_for_room_txn(self, txn, room_id, depth): min_depth = self._get_min_depth_interaction(txn, room_id) - if min_depth and depth >= min_depth: + if min_depth is not None and depth >= min_depth: return self.db.simple_upsert_txn( From 1f2a5923d4e2339e214a2ed0affadf36d0af9662 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 7 Jan 2020 13:12:17 +0000 Subject: [PATCH 0770/1623] Changelog --- changelog.d/6652.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6652.bugfix diff --git a/changelog.d/6652.bugfix b/changelog.d/6652.bugfix new file mode 100644 index 0000000000..7e9781d652 --- /dev/null +++ b/changelog.d/6652.bugfix @@ -0,0 +1 @@ +Fix a bug causing Synapse not to fetch missing events when it believes it has every event in the room. From d20c3465441cd64ba3a1e84ee399bbadc0997bdf Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 7 Jan 2020 14:09:07 +0000 Subject: [PATCH 0771/1623] port BackgroundUpdateTestCase to HomeserverTestCase (#6653) --- changelog.d/6653.misc | 1 + tests/storage/test_background_update.py | 72 +++++++++++++------------ 2 files changed, 38 insertions(+), 35 deletions(-) create mode 100644 changelog.d/6653.misc diff --git a/changelog.d/6653.misc b/changelog.d/6653.misc new file mode 100644 index 0000000000..fbe7c0e7db --- /dev/null +++ b/changelog.d/6653.misc @@ -0,0 +1 @@ +Port core background update routines to async/await. diff --git a/tests/storage/test_background_update.py b/tests/storage/test_background_update.py index aec76f4ab1..ae14fb407d 100644 --- a/tests/storage/test_background_update.py +++ b/tests/storage/test_background_update.py @@ -2,44 +2,37 @@ from mock import Mock from twisted.internet import defer +from synapse.storage.background_updates import BackgroundUpdater + from tests import unittest -from tests.utils import setup_test_homeserver -class BackgroundUpdateTestCase(unittest.TestCase): - @defer.inlineCallbacks - def setUp(self): - hs = yield setup_test_homeserver(self.addCleanup) - self.store = hs.get_datastore() - self.clock = hs.get_clock() +class BackgroundUpdateTestCase(unittest.HomeserverTestCase): + def prepare(self, reactor, clock, homeserver): + self.updates = self.hs.get_datastore().db.updates # type: BackgroundUpdater + # the base test class should have run the real bg updates for us + self.assertTrue(self.updates.has_completed_background_updates()) self.update_handler = Mock() - - yield self.store.db.updates.register_background_update_handler( + self.updates.register_background_update_handler( "test_update", self.update_handler ) - # run the real background updates, to get them out the way - # (perhaps we should run them as part of the test HS setup, since we - # run all of the other schema setup stuff there?) - while True: - res = yield self.store.db.updates.do_next_background_update(1000) - if res is None: - break - - @defer.inlineCallbacks def test_do_background_update(self): - desired_count = 1000 + # the time we claim each update takes duration_ms = 42 + # the target runtime for each bg update + target_background_update_duration_ms = 50000 + # first step: make a bit of progress @defer.inlineCallbacks def update(progress, count): - self.clock.advance_time_msec(count * duration_ms) + yield self.clock.sleep((count * duration_ms) / 1000) progress = {"my_key": progress["my_key"] + 1} - yield self.store.db.runInteraction( + yield self.hs.get_datastore().db.runInteraction( "update_progress", - self.store.db.updates._background_update_progress_txn, + self.updates._background_update_progress_txn, "test_update", progress, ) @@ -47,37 +40,46 @@ class BackgroundUpdateTestCase(unittest.TestCase): self.update_handler.side_effect = update - yield self.store.db.updates.start_background_update( - "test_update", {"my_key": 1} + self.get_success( + self.updates.start_background_update("test_update", {"my_key": 1}) ) - self.update_handler.reset_mock() - result = yield self.store.db.updates.do_next_background_update( - duration_ms * desired_count + res = self.get_success( + self.updates.do_next_background_update( + target_background_update_duration_ms + ), + by=0.1, ) - self.assertIsNotNone(result) + self.assertIsNotNone(res) + + # on the first call, we should get run with the default background update size self.update_handler.assert_called_once_with( - {"my_key": 1}, self.store.db.updates.DEFAULT_BACKGROUND_BATCH_SIZE + {"my_key": 1}, self.updates.DEFAULT_BACKGROUND_BATCH_SIZE ) # second step: complete the update + # we should now get run with a much bigger number of items to update @defer.inlineCallbacks def update(progress, count): - yield self.store.db.updates._end_background_update("test_update") + self.assertEqual(progress, {"my_key": 2}) + self.assertAlmostEqual( + count, target_background_update_duration_ms / duration_ms, places=0, + ) + yield self.updates._end_background_update("test_update") return count self.update_handler.side_effect = update self.update_handler.reset_mock() - result = yield self.store.db.updates.do_next_background_update( - duration_ms * desired_count + result = self.get_success( + self.updates.do_next_background_update(target_background_update_duration_ms) ) self.assertIsNotNone(result) - self.update_handler.assert_called_once_with({"my_key": 2}, desired_count) + self.update_handler.assert_called_once() # third step: we don't expect to be called any more self.update_handler.reset_mock() - result = yield self.store.db.updates.do_next_background_update( - duration_ms * desired_count + result = self.get_success( + self.updates.do_next_background_update(target_background_update_duration_ms) ) self.assertIsNone(result) self.assertFalse(self.update_handler.called) From 9824a39d807d2d13424095743761930b853fb08f Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 7 Jan 2020 14:12:42 +0000 Subject: [PATCH 0772/1623] Async/await for background updates (#6647) so that bg update routines can be async --- changelog.d/6647.misc | 1 + synapse/storage/background_updates.py | 36 +++++++++++++++------------ 2 files changed, 21 insertions(+), 16 deletions(-) create mode 100644 changelog.d/6647.misc diff --git a/changelog.d/6647.misc b/changelog.d/6647.misc new file mode 100644 index 0000000000..fbe7c0e7db --- /dev/null +++ b/changelog.d/6647.misc @@ -0,0 +1 @@ +Port core background update routines to async/await. diff --git a/synapse/storage/background_updates.py b/synapse/storage/background_updates.py index 4f97fd5ab6..b4825acc7b 100644 --- a/synapse/storage/background_updates.py +++ b/synapse/storage/background_updates.py @@ -14,6 +14,7 @@ # limitations under the License. import logging +from typing import Optional from canonicaljson import json @@ -97,15 +98,14 @@ class BackgroundUpdater(object): def start_doing_background_updates(self): run_as_background_process("background_updates", self.run_background_updates) - @defer.inlineCallbacks - def run_background_updates(self, sleep=True): + async def run_background_updates(self, sleep=True): logger.info("Starting background schema updates") while True: if sleep: - yield self._clock.sleep(self.BACKGROUND_UPDATE_INTERVAL_MS / 1000.0) + await self._clock.sleep(self.BACKGROUND_UPDATE_INTERVAL_MS / 1000.0) try: - result = yield self.do_next_background_update( + result = await self.do_next_background_update( self.BACKGROUND_UPDATE_DURATION_MS ) except Exception: @@ -170,20 +170,21 @@ class BackgroundUpdater(object): return not update_exists - @defer.inlineCallbacks - def do_next_background_update(self, desired_duration_ms): + async def do_next_background_update( + self, desired_duration_ms: float + ) -> Optional[int]: """Does some amount of work on the next queued background update + Returns once some amount of work is done. + Args: desired_duration_ms(float): How long we want to spend updating. Returns: - A deferred that completes once some amount of work is done. - The deferred will have a value of None if there is currently - no more work to do. + None if there is no more work to do, otherwise an int """ if not self._background_update_queue: - updates = yield self.db.simple_select_list( + updates = await self.db.simple_select_list( "background_updates", keyvalues=None, retcols=("update_name", "depends_on"), @@ -201,11 +202,12 @@ class BackgroundUpdater(object): update_name = self._background_update_queue.pop(0) self._background_update_queue.append(update_name) - res = yield self._do_background_update(update_name, desired_duration_ms) + res = await self._do_background_update(update_name, desired_duration_ms) return res - @defer.inlineCallbacks - def _do_background_update(self, update_name, desired_duration_ms): + async def _do_background_update( + self, update_name: str, desired_duration_ms: float + ) -> int: logger.info("Starting update batch on background update '%s'", update_name) update_handler = self._background_update_handlers[update_name] @@ -225,7 +227,7 @@ class BackgroundUpdater(object): else: batch_size = self.DEFAULT_BACKGROUND_BATCH_SIZE - progress_json = yield self.db.simple_select_one_onecol( + progress_json = await self.db.simple_select_one_onecol( "background_updates", keyvalues={"update_name": update_name}, retcol="progress_json", @@ -234,7 +236,7 @@ class BackgroundUpdater(object): progress = json.loads(progress_json) time_start = self._clock.time_msec() - items_updated = yield update_handler(progress, batch_size) + items_updated = await update_handler(progress, batch_size) time_stop = self._clock.time_msec() duration_ms = time_stop - time_start @@ -263,7 +265,9 @@ class BackgroundUpdater(object): * A dict of the current progress * An integer count of the number of items to update in this batch. - The handler should return a deferred integer count of items updated. + The handler should return a deferred or coroutine which returns an integer count + of items updated. + The handler is responsible for updating the progress of the update. Args: From 85db7f73be15cc088f5e378980021e335001ce87 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 7 Jan 2020 14:18:43 +0000 Subject: [PATCH 0773/1623] Add a background update to clear tombstoned rooms from the directory (#6648) * Add a background update to clear tombstoned rooms from the directory * use the ABC metaclass --- changelog.d/6648.bugfix | 1 + scripts/synapse_port_db | 5 ++ synapse/storage/_base.py | 4 +- synapse/storage/background_updates.py | 15 +++++ synapse/storage/data_stores/main/room.py | 64 +++++++++++++++++++ ...remove_tombstoned_rooms_from_directory.sql | 18 ++++++ 6 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 changelog.d/6648.bugfix create mode 100644 synapse/storage/data_stores/main/schema/delta/56/remove_tombstoned_rooms_from_directory.sql diff --git a/changelog.d/6648.bugfix b/changelog.d/6648.bugfix new file mode 100644 index 0000000000..39916de437 --- /dev/null +++ b/changelog.d/6648.bugfix @@ -0,0 +1 @@ +Ensure that upgraded rooms are removed from the directory. diff --git a/scripts/synapse_port_db b/scripts/synapse_port_db index eb927f2094..cb77314f1e 100755 --- a/scripts/synapse_port_db +++ b/scripts/synapse_port_db @@ -166,6 +166,11 @@ class Store( logger.exception("Failed to insert: %s", table) raise + def set_room_is_public(self, room_id, is_public): + raise Exception( + "Attempt to set room_is_public during port_db: database not empty?" + ) + class MockHomeserver: def __init__(self, config): diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py index 88546ad614..3bb9381663 100644 --- a/synapse/storage/_base.py +++ b/synapse/storage/_base.py @@ -16,6 +16,7 @@ # limitations under the License. import logging import random +from abc import ABCMeta from six import PY2 from six.moves import builtins @@ -30,7 +31,8 @@ from synapse.types import get_domain_from_id logger = logging.getLogger(__name__) -class SQLBaseStore(object): +# some of our subclasses have abstract methods, so we use the ABCMeta metaclass. +class SQLBaseStore(metaclass=ABCMeta): """Base class for data stores that holds helper functions. Note that multiple instances of this class will exist as there will be one diff --git a/synapse/storage/background_updates.py b/synapse/storage/background_updates.py index b4825acc7b..bd547f35cf 100644 --- a/synapse/storage/background_updates.py +++ b/synapse/storage/background_updates.py @@ -436,6 +436,21 @@ class BackgroundUpdater(object): "background_updates", keyvalues={"update_name": update_name} ) + def _background_update_progress(self, update_name: str, progress: dict): + """Update the progress of a background update + + Args: + update_name: The name of the background update task + progress: The progress of the update. + """ + + return self.db.runInteraction( + "background_update_progress", + self._background_update_progress_txn, + update_name, + progress, + ) + def _background_update_progress_txn(self, txn, update_name, progress): """Update the progress of a background update diff --git a/synapse/storage/data_stores/main/room.py b/synapse/storage/data_stores/main/room.py index aa476d0fbf..79cfd39194 100644 --- a/synapse/storage/data_stores/main/room.py +++ b/synapse/storage/data_stores/main/room.py @@ -17,6 +17,7 @@ import collections import logging import re +from abc import abstractmethod from typing import Optional, Tuple from six import integer_types @@ -367,6 +368,8 @@ class RoomWorkerStore(SQLBaseStore): class RoomBackgroundUpdateStore(SQLBaseStore): + REMOVE_TOMESTONED_ROOMS_BG_UPDATE = "remove_tombstoned_rooms_from_directory" + def __init__(self, database: Database, db_conn, hs): super(RoomBackgroundUpdateStore, self).__init__(database, db_conn, hs) @@ -376,6 +379,11 @@ class RoomBackgroundUpdateStore(SQLBaseStore): "insert_room_retention", self._background_insert_retention, ) + self.db.updates.register_background_update_handler( + self.REMOVE_TOMESTONED_ROOMS_BG_UPDATE, + self._remove_tombstoned_rooms_from_directory, + ) + @defer.inlineCallbacks def _background_insert_retention(self, progress, batch_size): """Retrieves a list of all rooms within a range and inserts an entry for each of @@ -444,6 +452,62 @@ class RoomBackgroundUpdateStore(SQLBaseStore): defer.returnValue(batch_size) + async def _remove_tombstoned_rooms_from_directory( + self, progress, batch_size + ) -> int: + """Removes any rooms with tombstone events from the room directory + + Nowadays this is handled by the room upgrade handler, but we may have some + that got left behind + """ + + last_room = progress.get("room_id", "") + + def _get_rooms(txn): + txn.execute( + """ + SELECT room_id + FROM rooms r + INNER JOIN current_state_events cse USING (room_id) + WHERE room_id > ? AND r.is_public + AND cse.type = '%s' AND cse.state_key = '' + ORDER BY room_id ASC + LIMIT ?; + """ + % EventTypes.Tombstone, + (last_room, batch_size), + ) + + return [row[0] for row in txn] + + rooms = await self.db.runInteraction( + "get_tombstoned_directory_rooms", _get_rooms + ) + + if not rooms: + await self.db.updates._end_background_update( + self.REMOVE_TOMESTONED_ROOMS_BG_UPDATE + ) + return 0 + + for room_id in rooms: + logger.info("Removing tombstoned room %s from the directory", room_id) + await self.set_room_is_public(room_id, False) + + await self.db.updates._background_update_progress( + self.REMOVE_TOMESTONED_ROOMS_BG_UPDATE, {"room_id": rooms[-1]} + ) + + return len(rooms) + + @abstractmethod + def set_room_is_public(self, room_id, is_public): + # this will need to be implemented if a background update is performed with + # existing (tombstoned, public) rooms in the database. + # + # It's overridden by RoomStore for the synapse master. + raise NotImplementedError() + class RoomStore(RoomBackgroundUpdateStore, RoomWorkerStore, SearchStore): def __init__(self, database: Database, db_conn, hs): diff --git a/synapse/storage/data_stores/main/schema/delta/56/remove_tombstoned_rooms_from_directory.sql b/synapse/storage/data_stores/main/schema/delta/56/remove_tombstoned_rooms_from_directory.sql new file mode 100644 index 0000000000..aeb17813d3 --- /dev/null +++ b/synapse/storage/data_stores/main/schema/delta/56/remove_tombstoned_rooms_from_directory.sql @@ -0,0 +1,18 @@ +/* Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +-- Now that #6232 is a thing, we can remove old rooms from the directory. +INSERT INTO background_updates (update_name, progress_json) VALUES + ('remove_tombstoned_rooms_from_directory', '{}'); From 7f0e706ebf256cc8561b1cdb2efec1b91738694f Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 7 Jan 2020 14:31:13 +0000 Subject: [PATCH 0774/1623] 1.8.0rc1 --- CHANGES.md | 80 ++++++++++++++++++++++++++++++++++++++++ changelog.d/6245.misc | 1 - changelog.d/6349.feature | 1 - changelog.d/6377.bugfix | 1 - changelog.d/6385.bugfix | 1 - changelog.d/6394.feature | 1 - changelog.d/6411.feature | 1 - changelog.d/6453.feature | 1 - changelog.d/6486.bugfix | 1 - changelog.d/6496.misc | 1 - changelog.d/6502.removal | 1 - changelog.d/6504.misc | 1 - changelog.d/6505.misc | 1 - changelog.d/6506.misc | 1 - changelog.d/6510.misc | 1 - changelog.d/6511.misc | 1 - changelog.d/6512.misc | 1 - changelog.d/6513.misc | 1 - changelog.d/6514.bugfix | 1 - changelog.d/6515.misc | 1 - changelog.d/6517.misc | 1 - changelog.d/6522.bugfix | 1 - changelog.d/6523.feature | 1 - changelog.d/6534.misc | 1 - changelog.d/6537.misc | 1 - changelog.d/6538.misc | 1 - changelog.d/6541.doc | 1 - changelog.d/6546.feature | 1 - changelog.d/6555.bugfix | 1 - changelog.d/6557.misc | 1 - changelog.d/6558.misc | 1 - changelog.d/6559.misc | 1 - changelog.d/6564.misc | 1 - changelog.d/6565.misc | 1 - changelog.d/6570.misc | 1 - changelog.d/6571.bugfix | 1 - changelog.d/6580.feature | 1 - changelog.d/6601.doc | 1 - changelog.d/6614.doc | 1 - changelog.d/6617.misc | 1 - changelog.d/6619.misc | 1 - changelog.d/6620.misc | 1 - changelog.d/6625.bugfix | 1 - changelog.d/6626.feature | 1 - changelog.d/6627.misc | 1 - changelog.d/6628.removal | 1 - changelog.d/6629.misc | 1 - changelog.d/6633.bugfix | 1 - changelog.d/6640.bugfix | 1 - changelog.d/6642.misc | 1 - changelog.d/6645.bugfix | 1 - changelog.d/6647.misc | 1 - changelog.d/6648.bugfix | 1 - changelog.d/6652.bugfix | 1 - changelog.d/6653.misc | 1 - synapse/__init__.py | 2 +- 56 files changed, 81 insertions(+), 55 deletions(-) delete mode 100644 changelog.d/6245.misc delete mode 100644 changelog.d/6349.feature delete mode 100644 changelog.d/6377.bugfix delete mode 100644 changelog.d/6385.bugfix delete mode 100644 changelog.d/6394.feature delete mode 100644 changelog.d/6411.feature delete mode 100644 changelog.d/6453.feature delete mode 100644 changelog.d/6486.bugfix delete mode 100644 changelog.d/6496.misc delete mode 100644 changelog.d/6502.removal delete mode 100644 changelog.d/6504.misc delete mode 100644 changelog.d/6505.misc delete mode 100644 changelog.d/6506.misc delete mode 100644 changelog.d/6510.misc delete mode 100644 changelog.d/6511.misc delete mode 100644 changelog.d/6512.misc delete mode 100644 changelog.d/6513.misc delete mode 100644 changelog.d/6514.bugfix delete mode 100644 changelog.d/6515.misc delete mode 100644 changelog.d/6517.misc delete mode 100644 changelog.d/6522.bugfix delete mode 100644 changelog.d/6523.feature delete mode 100644 changelog.d/6534.misc delete mode 100644 changelog.d/6537.misc delete mode 100644 changelog.d/6538.misc delete mode 100644 changelog.d/6541.doc delete mode 100644 changelog.d/6546.feature delete mode 100644 changelog.d/6555.bugfix delete mode 100644 changelog.d/6557.misc delete mode 100644 changelog.d/6558.misc delete mode 100644 changelog.d/6559.misc delete mode 100644 changelog.d/6564.misc delete mode 100644 changelog.d/6565.misc delete mode 100644 changelog.d/6570.misc delete mode 100644 changelog.d/6571.bugfix delete mode 100644 changelog.d/6580.feature delete mode 100644 changelog.d/6601.doc delete mode 100644 changelog.d/6614.doc delete mode 100644 changelog.d/6617.misc delete mode 100644 changelog.d/6619.misc delete mode 100644 changelog.d/6620.misc delete mode 100644 changelog.d/6625.bugfix delete mode 100644 changelog.d/6626.feature delete mode 100644 changelog.d/6627.misc delete mode 100644 changelog.d/6628.removal delete mode 100644 changelog.d/6629.misc delete mode 100644 changelog.d/6633.bugfix delete mode 100644 changelog.d/6640.bugfix delete mode 100644 changelog.d/6642.misc delete mode 100644 changelog.d/6645.bugfix delete mode 100644 changelog.d/6647.misc delete mode 100644 changelog.d/6648.bugfix delete mode 100644 changelog.d/6652.bugfix delete mode 100644 changelog.d/6653.misc diff --git a/CHANGES.md b/CHANGES.md index 361fd1fc6c..2f1cd87e1a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,83 @@ +Synapse 1.8.0rc1 (2020-01-07) +============================= + +Features +-------- + +- Implement v2 APIs for the `send_join` and `send_leave` federation endpoints (as described in [MSC1802](https://github.com/matrix-org/matrix-doc/pull/1802)). ([\#6349](https://github.com/matrix-org/synapse/issues/6349)) +- Add a develop script to generate full SQL schemas. ([\#6394](https://github.com/matrix-org/synapse/issues/6394)) +- Allow custom SAML username mapping functinality through an external provider plugin. ([\#6411](https://github.com/matrix-org/synapse/issues/6411)) +- Automatically delete empty groups/communities. ([\#6453](https://github.com/matrix-org/synapse/issues/6453)) +- Add option `limit_profile_requests_to_users_who_share_rooms` to prevent requirement of a local user sharing a room with another user to query their profile information. ([\#6523](https://github.com/matrix-org/synapse/issues/6523)) +- Add an export_signing_key script to extract the public part of signing keys when rotating them. ([\#6546](https://github.com/matrix-org/synapse/issues/6546)) +- Add experimental config option to specify multiple databases. ([\#6580](https://github.com/matrix-org/synapse/issues/6580)) +- Raise an error if someone tries to use the log_file config option. ([\#6626](https://github.com/matrix-org/synapse/issues/6626)) + + +Bugfixes +-------- + +- Prevent redacted events from being returned during message search. ([\#6377](https://github.com/matrix-org/synapse/issues/6377), [\#6522](https://github.com/matrix-org/synapse/issues/6522)) +- Prevent error on trying to search a upgraded room when the server is not in the predecessor room. ([\#6385](https://github.com/matrix-org/synapse/issues/6385)) +- Improve performance of looking up cross-signing keys. ([\#6486](https://github.com/matrix-org/synapse/issues/6486)) +- Fix race which occasionally caused deleted devices to reappear. ([\#6514](https://github.com/matrix-org/synapse/issues/6514)) +- Fix missing row in device_max_stream_id that could cause unable to decrypt errors after server restart. ([\#6555](https://github.com/matrix-org/synapse/issues/6555)) +- Fix a bug which meant that we did not send systemd notifications on startup if acme was enabled. ([\#6571](https://github.com/matrix-org/synapse/issues/6571)) +- Fix exception when fetching the `matrix.org:ed25519:auto` key. ([\#6625](https://github.com/matrix-org/synapse/issues/6625)) +- Fix bug where a moderator upgraded a room and became an admin in the new room. ([\#6633](https://github.com/matrix-org/synapse/issues/6633)) +- Fix an error which was thrown by the PresenceHandler _on_shutdown handler. ([\#6640](https://github.com/matrix-org/synapse/issues/6640)) +- Fix exceptions in the synchrotron worker log when events are rejected. ([\#6645](https://github.com/matrix-org/synapse/issues/6645)) +- Ensure that upgraded rooms are removed from the directory. ([\#6648](https://github.com/matrix-org/synapse/issues/6648)) +- Fix a bug causing Synapse not to fetch missing events when it believes it has every event in the room. ([\#6652](https://github.com/matrix-org/synapse/issues/6652)) + + +Improved Documentation +---------------------- + +- Document the Room Shutdown Admin API. ([\#6541](https://github.com/matrix-org/synapse/issues/6541)) +- Reword sections of federate.md that explained delegation at time of Synapse 1.0 transition. ([\#6601](https://github.com/matrix-org/synapse/issues/6601)) +- Added the section 'Configuration' in /docs/turn-howto.md. ([\#6614](https://github.com/matrix-org/synapse/issues/6614)) + + +Deprecations and Removals +------------------------- + +- Remove redundant code from event authorisation implementation. ([\#6502](https://github.com/matrix-org/synapse/issues/6502)) +- Remove unused, undocumented /_matrix/content API. ([\#6628](https://github.com/matrix-org/synapse/issues/6628)) + + +Internal Changes +---------------- + +- Split out state storage into separate data store. ([\#6245](https://github.com/matrix-org/synapse/issues/6245)) +- Port synapse.handlers.initial_sync to async/await. ([\#6496](https://github.com/matrix-org/synapse/issues/6496)) +- Port handlers.account_data and handlers.account_validity to async/await. ([\#6504](https://github.com/matrix-org/synapse/issues/6504)) +- Make `make_deferred_yieldable` to work with async/await. ([\#6505](https://github.com/matrix-org/synapse/issues/6505)) +- Remove `SnapshotCache` in favour of `ResponseCache`. ([\#6506](https://github.com/matrix-org/synapse/issues/6506)) +- Change phone home stats to not assume there is a single database and report information about the database used by the main data store. ([\#6510](https://github.com/matrix-org/synapse/issues/6510)) +- Move database config from apps into HomeServer object. ([\#6511](https://github.com/matrix-org/synapse/issues/6511)) +- Silence mypy errors for files outside those specified. ([\#6512](https://github.com/matrix-org/synapse/issues/6512)) +- Remove all assumptions of there being a single phyiscal DB apart from the `synapse.config`. ([\#6513](https://github.com/matrix-org/synapse/issues/6513)) +- Clean up some logging when handling incoming events over federation. ([\#6515](https://github.com/matrix-org/synapse/issues/6515)) +- Port some of FederationHandler to async/await. ([\#6517](https://github.com/matrix-org/synapse/issues/6517)) +- Test more folders against mypy. ([\#6534](https://github.com/matrix-org/synapse/issues/6534)) +- Update `mypy` to new version. ([\#6537](https://github.com/matrix-org/synapse/issues/6537)) +- Adjust the sytest blacklist for worker mode. ([\#6538](https://github.com/matrix-org/synapse/issues/6538)) +- Remove unused `get_pagination_rows` methods from `EventSource` classes. ([\#6557](https://github.com/matrix-org/synapse/issues/6557)) +- Clean up logs from the push notifier at startup. ([\#6558](https://github.com/matrix-org/synapse/issues/6558)) +- Port `synapse.handlers.admin` and `synapse.handlers.deactivate_account` to async/await. ([\#6559](https://github.com/matrix-org/synapse/issues/6559)) +- Change `EventContext` to use the `Storage` class, in preparation for moving state database queries to a separate data store. ([\#6564](https://github.com/matrix-org/synapse/issues/6564)) +- Add assertion that schema delta file names are unique. ([\#6565](https://github.com/matrix-org/synapse/issues/6565)) +- Improve diagnostics on database upgrade failure. ([\#6570](https://github.com/matrix-org/synapse/issues/6570)) +- Reduce the reconnect time when worker replication fails, to make it easier to catch up. ([\#6617](https://github.com/matrix-org/synapse/issues/6617)) +- Simplify http handling by removing redundant SynapseRequestFactory. ([\#6619](https://github.com/matrix-org/synapse/issues/6619)) +- Add a workaround for synapse raising exceptions when fetching the notary's own key from the notary. ([\#6620](https://github.com/matrix-org/synapse/issues/6620)) +- Automate generation of the sample log config. ([\#6627](https://github.com/matrix-org/synapse/issues/6627)) +- Simplify event creation code by removing redundant queries on the event_reference_hashes table. ([\#6629](https://github.com/matrix-org/synapse/issues/6629)) +- Fix errors when frozen_dicts are enabled. ([\#6642](https://github.com/matrix-org/synapse/issues/6642)) +- Port core background update routines to async/await. ([\#6647](https://github.com/matrix-org/synapse/issues/6647), [\#6653](https://github.com/matrix-org/synapse/issues/6653)) + + Synapse 1.7.3 (2019-12-31) ========================== diff --git a/changelog.d/6245.misc b/changelog.d/6245.misc deleted file mode 100644 index a3e6b8296e..0000000000 --- a/changelog.d/6245.misc +++ /dev/null @@ -1 +0,0 @@ -Split out state storage into separate data store. diff --git a/changelog.d/6349.feature b/changelog.d/6349.feature deleted file mode 100644 index 56c4fbf78e..0000000000 --- a/changelog.d/6349.feature +++ /dev/null @@ -1 +0,0 @@ -Implement v2 APIs for the `send_join` and `send_leave` federation endpoints (as described in [MSC1802](https://github.com/matrix-org/matrix-doc/pull/1802)). diff --git a/changelog.d/6377.bugfix b/changelog.d/6377.bugfix deleted file mode 100644 index ccda96962f..0000000000 --- a/changelog.d/6377.bugfix +++ /dev/null @@ -1 +0,0 @@ -Prevent redacted events from being returned during message search. \ No newline at end of file diff --git a/changelog.d/6385.bugfix b/changelog.d/6385.bugfix deleted file mode 100644 index 7a2bc02170..0000000000 --- a/changelog.d/6385.bugfix +++ /dev/null @@ -1 +0,0 @@ -Prevent error on trying to search a upgraded room when the server is not in the predecessor room. \ No newline at end of file diff --git a/changelog.d/6394.feature b/changelog.d/6394.feature deleted file mode 100644 index 1a0e8845ad..0000000000 --- a/changelog.d/6394.feature +++ /dev/null @@ -1 +0,0 @@ -Add a develop script to generate full SQL schemas. \ No newline at end of file diff --git a/changelog.d/6411.feature b/changelog.d/6411.feature deleted file mode 100644 index ebea4a208d..0000000000 --- a/changelog.d/6411.feature +++ /dev/null @@ -1 +0,0 @@ -Allow custom SAML username mapping functinality through an external provider plugin. \ No newline at end of file diff --git a/changelog.d/6453.feature b/changelog.d/6453.feature deleted file mode 100644 index e7bb801c6a..0000000000 --- a/changelog.d/6453.feature +++ /dev/null @@ -1 +0,0 @@ -Automatically delete empty groups/communities. diff --git a/changelog.d/6486.bugfix b/changelog.d/6486.bugfix deleted file mode 100644 index b98c5a9ae5..0000000000 --- a/changelog.d/6486.bugfix +++ /dev/null @@ -1 +0,0 @@ -Improve performance of looking up cross-signing keys. diff --git a/changelog.d/6496.misc b/changelog.d/6496.misc deleted file mode 100644 index 19c6e926b8..0000000000 --- a/changelog.d/6496.misc +++ /dev/null @@ -1 +0,0 @@ -Port synapse.handlers.initial_sync to async/await. diff --git a/changelog.d/6502.removal b/changelog.d/6502.removal deleted file mode 100644 index 0b72261d58..0000000000 --- a/changelog.d/6502.removal +++ /dev/null @@ -1 +0,0 @@ -Remove redundant code from event authorisation implementation. diff --git a/changelog.d/6504.misc b/changelog.d/6504.misc deleted file mode 100644 index 7c873459af..0000000000 --- a/changelog.d/6504.misc +++ /dev/null @@ -1 +0,0 @@ -Port handlers.account_data and handlers.account_validity to async/await. diff --git a/changelog.d/6505.misc b/changelog.d/6505.misc deleted file mode 100644 index 3a75b2d9dd..0000000000 --- a/changelog.d/6505.misc +++ /dev/null @@ -1 +0,0 @@ -Make `make_deferred_yieldable` to work with async/await. diff --git a/changelog.d/6506.misc b/changelog.d/6506.misc deleted file mode 100644 index 99d7a70bcf..0000000000 --- a/changelog.d/6506.misc +++ /dev/null @@ -1 +0,0 @@ -Remove `SnapshotCache` in favour of `ResponseCache`. diff --git a/changelog.d/6510.misc b/changelog.d/6510.misc deleted file mode 100644 index 214f06539b..0000000000 --- a/changelog.d/6510.misc +++ /dev/null @@ -1 +0,0 @@ -Change phone home stats to not assume there is a single database and report information about the database used by the main data store. diff --git a/changelog.d/6511.misc b/changelog.d/6511.misc deleted file mode 100644 index 19ce435e68..0000000000 --- a/changelog.d/6511.misc +++ /dev/null @@ -1 +0,0 @@ -Move database config from apps into HomeServer object. diff --git a/changelog.d/6512.misc b/changelog.d/6512.misc deleted file mode 100644 index 37a8099eec..0000000000 --- a/changelog.d/6512.misc +++ /dev/null @@ -1 +0,0 @@ -Silence mypy errors for files outside those specified. diff --git a/changelog.d/6513.misc b/changelog.d/6513.misc deleted file mode 100644 index 36700f5657..0000000000 --- a/changelog.d/6513.misc +++ /dev/null @@ -1 +0,0 @@ -Remove all assumptions of there being a single phyiscal DB apart from the `synapse.config`. diff --git a/changelog.d/6514.bugfix b/changelog.d/6514.bugfix deleted file mode 100644 index 6dc1985c24..0000000000 --- a/changelog.d/6514.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix race which occasionally caused deleted devices to reappear. diff --git a/changelog.d/6515.misc b/changelog.d/6515.misc deleted file mode 100644 index a9c303ed1c..0000000000 --- a/changelog.d/6515.misc +++ /dev/null @@ -1 +0,0 @@ -Clean up some logging when handling incoming events over federation. diff --git a/changelog.d/6517.misc b/changelog.d/6517.misc deleted file mode 100644 index c6ffed9952..0000000000 --- a/changelog.d/6517.misc +++ /dev/null @@ -1 +0,0 @@ -Port some of FederationHandler to async/await. \ No newline at end of file diff --git a/changelog.d/6522.bugfix b/changelog.d/6522.bugfix deleted file mode 100644 index ccda96962f..0000000000 --- a/changelog.d/6522.bugfix +++ /dev/null @@ -1 +0,0 @@ -Prevent redacted events from being returned during message search. \ No newline at end of file diff --git a/changelog.d/6523.feature b/changelog.d/6523.feature deleted file mode 100644 index 798fa143df..0000000000 --- a/changelog.d/6523.feature +++ /dev/null @@ -1 +0,0 @@ -Add option `limit_profile_requests_to_users_who_share_rooms` to prevent requirement of a local user sharing a room with another user to query their profile information. diff --git a/changelog.d/6534.misc b/changelog.d/6534.misc deleted file mode 100644 index 7df6bb442a..0000000000 --- a/changelog.d/6534.misc +++ /dev/null @@ -1 +0,0 @@ -Test more folders against mypy. diff --git a/changelog.d/6537.misc b/changelog.d/6537.misc deleted file mode 100644 index 3543153584..0000000000 --- a/changelog.d/6537.misc +++ /dev/null @@ -1 +0,0 @@ -Update `mypy` to new version. diff --git a/changelog.d/6538.misc b/changelog.d/6538.misc deleted file mode 100644 index cb4fd56948..0000000000 --- a/changelog.d/6538.misc +++ /dev/null @@ -1 +0,0 @@ -Adjust the sytest blacklist for worker mode. diff --git a/changelog.d/6541.doc b/changelog.d/6541.doc deleted file mode 100644 index c20029edc0..0000000000 --- a/changelog.d/6541.doc +++ /dev/null @@ -1 +0,0 @@ -Document the Room Shutdown Admin API. \ No newline at end of file diff --git a/changelog.d/6546.feature b/changelog.d/6546.feature deleted file mode 100644 index 954aacb0d0..0000000000 --- a/changelog.d/6546.feature +++ /dev/null @@ -1 +0,0 @@ -Add an export_signing_key script to extract the public part of signing keys when rotating them. diff --git a/changelog.d/6555.bugfix b/changelog.d/6555.bugfix deleted file mode 100644 index 86a5a56cf6..0000000000 --- a/changelog.d/6555.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix missing row in device_max_stream_id that could cause unable to decrypt errors after server restart. \ No newline at end of file diff --git a/changelog.d/6557.misc b/changelog.d/6557.misc deleted file mode 100644 index 80e7eaedb8..0000000000 --- a/changelog.d/6557.misc +++ /dev/null @@ -1 +0,0 @@ -Remove unused `get_pagination_rows` methods from `EventSource` classes. diff --git a/changelog.d/6558.misc b/changelog.d/6558.misc deleted file mode 100644 index a7572f1a85..0000000000 --- a/changelog.d/6558.misc +++ /dev/null @@ -1 +0,0 @@ -Clean up logs from the push notifier at startup. \ No newline at end of file diff --git a/changelog.d/6559.misc b/changelog.d/6559.misc deleted file mode 100644 index 8bca37457d..0000000000 --- a/changelog.d/6559.misc +++ /dev/null @@ -1 +0,0 @@ -Port `synapse.handlers.admin` and `synapse.handlers.deactivate_account` to async/await. diff --git a/changelog.d/6564.misc b/changelog.d/6564.misc deleted file mode 100644 index f644f5868b..0000000000 --- a/changelog.d/6564.misc +++ /dev/null @@ -1 +0,0 @@ -Change `EventContext` to use the `Storage` class, in preparation for moving state database queries to a separate data store. diff --git a/changelog.d/6565.misc b/changelog.d/6565.misc deleted file mode 100644 index e83f245bf0..0000000000 --- a/changelog.d/6565.misc +++ /dev/null @@ -1 +0,0 @@ -Add assertion that schema delta file names are unique. diff --git a/changelog.d/6570.misc b/changelog.d/6570.misc deleted file mode 100644 index e89955a51e..0000000000 --- a/changelog.d/6570.misc +++ /dev/null @@ -1 +0,0 @@ -Improve diagnostics on database upgrade failure. diff --git a/changelog.d/6571.bugfix b/changelog.d/6571.bugfix deleted file mode 100644 index e38ea7b4f7..0000000000 --- a/changelog.d/6571.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a bug which meant that we did not send systemd notifications on startup if acme was enabled. diff --git a/changelog.d/6580.feature b/changelog.d/6580.feature deleted file mode 100644 index 233c589c66..0000000000 --- a/changelog.d/6580.feature +++ /dev/null @@ -1 +0,0 @@ -Add experimental config option to specify multiple databases. diff --git a/changelog.d/6601.doc b/changelog.d/6601.doc deleted file mode 100644 index 08c5b3d215..0000000000 --- a/changelog.d/6601.doc +++ /dev/null @@ -1 +0,0 @@ -Reword sections of federate.md that explained delegation at time of Synapse 1.0 transition. \ No newline at end of file diff --git a/changelog.d/6614.doc b/changelog.d/6614.doc deleted file mode 100644 index 38b962b062..0000000000 --- a/changelog.d/6614.doc +++ /dev/null @@ -1 +0,0 @@ -Added the section 'Configuration' in /docs/turn-howto.md. diff --git a/changelog.d/6617.misc b/changelog.d/6617.misc deleted file mode 100644 index 94aa271d38..0000000000 --- a/changelog.d/6617.misc +++ /dev/null @@ -1 +0,0 @@ -Reduce the reconnect time when worker replication fails, to make it easier to catch up. diff --git a/changelog.d/6619.misc b/changelog.d/6619.misc deleted file mode 100644 index b608133219..0000000000 --- a/changelog.d/6619.misc +++ /dev/null @@ -1 +0,0 @@ -Simplify http handling by removing redundant SynapseRequestFactory. diff --git a/changelog.d/6620.misc b/changelog.d/6620.misc deleted file mode 100644 index 8bfb78fb20..0000000000 --- a/changelog.d/6620.misc +++ /dev/null @@ -1 +0,0 @@ -Add a workaround for synapse raising exceptions when fetching the notary's own key from the notary. diff --git a/changelog.d/6625.bugfix b/changelog.d/6625.bugfix deleted file mode 100644 index a8dc5587dc..0000000000 --- a/changelog.d/6625.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix exception when fetching the `matrix.org:ed25519:auto` key. diff --git a/changelog.d/6626.feature b/changelog.d/6626.feature deleted file mode 100644 index 15798fa59b..0000000000 --- a/changelog.d/6626.feature +++ /dev/null @@ -1 +0,0 @@ -Raise an error if someone tries to use the log_file config option. diff --git a/changelog.d/6627.misc b/changelog.d/6627.misc deleted file mode 100644 index 702f067070..0000000000 --- a/changelog.d/6627.misc +++ /dev/null @@ -1 +0,0 @@ -Automate generation of the sample log config. diff --git a/changelog.d/6628.removal b/changelog.d/6628.removal deleted file mode 100644 index 66cd6aeca4..0000000000 --- a/changelog.d/6628.removal +++ /dev/null @@ -1 +0,0 @@ -Remove unused, undocumented /_matrix/content API. diff --git a/changelog.d/6629.misc b/changelog.d/6629.misc deleted file mode 100644 index 68f77af05b..0000000000 --- a/changelog.d/6629.misc +++ /dev/null @@ -1 +0,0 @@ -Simplify event creation code by removing redundant queries on the event_reference_hashes table. \ No newline at end of file diff --git a/changelog.d/6633.bugfix b/changelog.d/6633.bugfix deleted file mode 100644 index 4bacf26021..0000000000 --- a/changelog.d/6633.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix bug where a moderator upgraded a room and became an admin in the new room. \ No newline at end of file diff --git a/changelog.d/6640.bugfix b/changelog.d/6640.bugfix deleted file mode 100644 index 8c2a129933..0000000000 --- a/changelog.d/6640.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix an error which was thrown by the PresenceHandler _on_shutdown handler. diff --git a/changelog.d/6642.misc b/changelog.d/6642.misc deleted file mode 100644 index a480bbd134..0000000000 --- a/changelog.d/6642.misc +++ /dev/null @@ -1 +0,0 @@ -Fix errors when frozen_dicts are enabled. diff --git a/changelog.d/6645.bugfix b/changelog.d/6645.bugfix deleted file mode 100644 index f648df3fc0..0000000000 --- a/changelog.d/6645.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix exceptions in the synchrotron worker log when events are rejected. diff --git a/changelog.d/6647.misc b/changelog.d/6647.misc deleted file mode 100644 index fbe7c0e7db..0000000000 --- a/changelog.d/6647.misc +++ /dev/null @@ -1 +0,0 @@ -Port core background update routines to async/await. diff --git a/changelog.d/6648.bugfix b/changelog.d/6648.bugfix deleted file mode 100644 index 39916de437..0000000000 --- a/changelog.d/6648.bugfix +++ /dev/null @@ -1 +0,0 @@ -Ensure that upgraded rooms are removed from the directory. diff --git a/changelog.d/6652.bugfix b/changelog.d/6652.bugfix deleted file mode 100644 index 7e9781d652..0000000000 --- a/changelog.d/6652.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a bug causing Synapse not to fetch missing events when it believes it has every event in the room. diff --git a/changelog.d/6653.misc b/changelog.d/6653.misc deleted file mode 100644 index fbe7c0e7db..0000000000 --- a/changelog.d/6653.misc +++ /dev/null @@ -1 +0,0 @@ -Port core background update routines to async/await. diff --git a/synapse/__init__.py b/synapse/__init__.py index 71cb611820..a3bd855045 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -36,7 +36,7 @@ try: except ImportError: pass -__version__ = "1.7.3" +__version__ = "1.8.0rc1" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 06958d5bb1444fe280d557cce0a6697c1e8130c8 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 7 Jan 2020 14:38:40 +0000 Subject: [PATCH 0775/1623] Fixup changelog --- CHANGES.md | 32 +++++++++++--------------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 2f1cd87e1a..384ed0a854 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,14 +4,14 @@ Synapse 1.8.0rc1 (2020-01-07) Features -------- -- Implement v2 APIs for the `send_join` and `send_leave` federation endpoints (as described in [MSC1802](https://github.com/matrix-org/matrix-doc/pull/1802)). ([\#6349](https://github.com/matrix-org/synapse/issues/6349)) +- Add v2 APIs for the `send_join` and `send_leave` federation endpoints (as described in [MSC1802](https://github.com/matrix-org/matrix-doc/pull/1802)). ([\#6349](https://github.com/matrix-org/synapse/issues/6349)) - Add a develop script to generate full SQL schemas. ([\#6394](https://github.com/matrix-org/synapse/issues/6394)) -- Allow custom SAML username mapping functinality through an external provider plugin. ([\#6411](https://github.com/matrix-org/synapse/issues/6411)) +- Add custom SAML username mapping functinality through an external provider plugin. ([\#6411](https://github.com/matrix-org/synapse/issues/6411)) - Automatically delete empty groups/communities. ([\#6453](https://github.com/matrix-org/synapse/issues/6453)) - Add option `limit_profile_requests_to_users_who_share_rooms` to prevent requirement of a local user sharing a room with another user to query their profile information. ([\#6523](https://github.com/matrix-org/synapse/issues/6523)) -- Add an export_signing_key script to extract the public part of signing keys when rotating them. ([\#6546](https://github.com/matrix-org/synapse/issues/6546)) +- Add an `export_signing_key` script to extract the public part of signing keys when rotating them. ([\#6546](https://github.com/matrix-org/synapse/issues/6546)) - Add experimental config option to specify multiple databases. ([\#6580](https://github.com/matrix-org/synapse/issues/6580)) -- Raise an error if someone tries to use the log_file config option. ([\#6626](https://github.com/matrix-org/synapse/issues/6626)) +- Raise an error if someone tries to use the `log_file` config option. ([\#6626](https://github.com/matrix-org/synapse/issues/6626)) Bugfixes @@ -36,46 +36,36 @@ Improved Documentation - Document the Room Shutdown Admin API. ([\#6541](https://github.com/matrix-org/synapse/issues/6541)) - Reword sections of federate.md that explained delegation at time of Synapse 1.0 transition. ([\#6601](https://github.com/matrix-org/synapse/issues/6601)) -- Added the section 'Configuration' in /docs/turn-howto.md. ([\#6614](https://github.com/matrix-org/synapse/issues/6614)) +- Added the section 'Configuration' in [docs/turn-howto.md](docs/turn-howto.md). ([\#6614](https://github.com/matrix-org/synapse/issues/6614)) Deprecations and Removals ------------------------- - Remove redundant code from event authorisation implementation. ([\#6502](https://github.com/matrix-org/synapse/issues/6502)) -- Remove unused, undocumented /_matrix/content API. ([\#6628](https://github.com/matrix-org/synapse/issues/6628)) +- Remove unused, undocumented `/_matrix/content` API. ([\#6628](https://github.com/matrix-org/synapse/issues/6628)) Internal Changes ---------------- -- Split out state storage into separate data store. ([\#6245](https://github.com/matrix-org/synapse/issues/6245)) -- Port synapse.handlers.initial_sync to async/await. ([\#6496](https://github.com/matrix-org/synapse/issues/6496)) -- Port handlers.account_data and handlers.account_validity to async/await. ([\#6504](https://github.com/matrix-org/synapse/issues/6504)) -- Make `make_deferred_yieldable` to work with async/await. ([\#6505](https://github.com/matrix-org/synapse/issues/6505)) +- Split out state storage into separate data store and add *experimental* support for multiple physical databases. ([\#6245](https://github.com/matrix-org/synapse/issues/6245), [\#6510](https://github.com/matrix-org/synapse/issues/6510), [\#6511](https://github.com/matrix-org/synapse/issues/6511), [\#6513](https://github.com/matrix-org/synapse/issues/6513), [\#6564](https://github.com/matrix-org/synapse/issues/6564), [\#6565](https://github.com/matrix-org/synapse/issues/6565)) +- Port sections of code base to async/await. ([\#6496](https://github.com/matrix-org/synapse/issues/6496), [\#6504](https://github.com/matrix-org/synapse/issues/6504), [\#6505](https://github.com/matrix-org/synapse/issues/6505), [\#6517](https://github.com/matrix-org/synapse/issues/6517), [\#6559](https://github.com/matrix-org/synapse/issues/6559), [\#6647](https://github.com/matrix-org/synapse/issues/6647), [\#6653](https://github.com/matrix-org/synapse/issues/6653)) - Remove `SnapshotCache` in favour of `ResponseCache`. ([\#6506](https://github.com/matrix-org/synapse/issues/6506)) -- Change phone home stats to not assume there is a single database and report information about the database used by the main data store. ([\#6510](https://github.com/matrix-org/synapse/issues/6510)) -- Move database config from apps into HomeServer object. ([\#6511](https://github.com/matrix-org/synapse/issues/6511)) - Silence mypy errors for files outside those specified. ([\#6512](https://github.com/matrix-org/synapse/issues/6512)) -- Remove all assumptions of there being a single phyiscal DB apart from the `synapse.config`. ([\#6513](https://github.com/matrix-org/synapse/issues/6513)) - Clean up some logging when handling incoming events over federation. ([\#6515](https://github.com/matrix-org/synapse/issues/6515)) -- Port some of FederationHandler to async/await. ([\#6517](https://github.com/matrix-org/synapse/issues/6517)) - Test more folders against mypy. ([\#6534](https://github.com/matrix-org/synapse/issues/6534)) - Update `mypy` to new version. ([\#6537](https://github.com/matrix-org/synapse/issues/6537)) - Adjust the sytest blacklist for worker mode. ([\#6538](https://github.com/matrix-org/synapse/issues/6538)) - Remove unused `get_pagination_rows` methods from `EventSource` classes. ([\#6557](https://github.com/matrix-org/synapse/issues/6557)) - Clean up logs from the push notifier at startup. ([\#6558](https://github.com/matrix-org/synapse/issues/6558)) -- Port `synapse.handlers.admin` and `synapse.handlers.deactivate_account` to async/await. ([\#6559](https://github.com/matrix-org/synapse/issues/6559)) -- Change `EventContext` to use the `Storage` class, in preparation for moving state database queries to a separate data store. ([\#6564](https://github.com/matrix-org/synapse/issues/6564)) -- Add assertion that schema delta file names are unique. ([\#6565](https://github.com/matrix-org/synapse/issues/6565)) - Improve diagnostics on database upgrade failure. ([\#6570](https://github.com/matrix-org/synapse/issues/6570)) - Reduce the reconnect time when worker replication fails, to make it easier to catch up. ([\#6617](https://github.com/matrix-org/synapse/issues/6617)) -- Simplify http handling by removing redundant SynapseRequestFactory. ([\#6619](https://github.com/matrix-org/synapse/issues/6619)) +- Simplify http handling by removing redundant `SynapseRequestFactory`. ([\#6619](https://github.com/matrix-org/synapse/issues/6619)) - Add a workaround for synapse raising exceptions when fetching the notary's own key from the notary. ([\#6620](https://github.com/matrix-org/synapse/issues/6620)) - Automate generation of the sample log config. ([\#6627](https://github.com/matrix-org/synapse/issues/6627)) -- Simplify event creation code by removing redundant queries on the event_reference_hashes table. ([\#6629](https://github.com/matrix-org/synapse/issues/6629)) -- Fix errors when frozen_dicts are enabled. ([\#6642](https://github.com/matrix-org/synapse/issues/6642)) -- Port core background update routines to async/await. ([\#6647](https://github.com/matrix-org/synapse/issues/6647), [\#6653](https://github.com/matrix-org/synapse/issues/6653)) +- Simplify event creation code by removing redundant queries on the `event_reference_hashes` table. ([\#6629](https://github.com/matrix-org/synapse/issues/6629)) +- Fix errors when `frozen_dicts` are enabled. ([\#6642](https://github.com/matrix-org/synapse/issues/6642)) Synapse 1.7.3 (2019-12-31) From 2f1c1759368f7813a6a6afc850510d5c01aae7b3 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 7 Jan 2020 14:39:35 +0000 Subject: [PATCH 0776/1623] Fixup changelog --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 384ed0a854..9716c8d648 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -49,7 +49,7 @@ Deprecations and Removals Internal Changes ---------------- -- Split out state storage into separate data store and add *experimental* support for multiple physical databases. ([\#6245](https://github.com/matrix-org/synapse/issues/6245), [\#6510](https://github.com/matrix-org/synapse/issues/6510), [\#6511](https://github.com/matrix-org/synapse/issues/6511), [\#6513](https://github.com/matrix-org/synapse/issues/6513), [\#6564](https://github.com/matrix-org/synapse/issues/6564), [\#6565](https://github.com/matrix-org/synapse/issues/6565)) +- Add *experimental* support for multiple physical databases and split out state storage to separate data store. ([\#6245](https://github.com/matrix-org/synapse/issues/6245), [\#6510](https://github.com/matrix-org/synapse/issues/6510), [\#6511](https://github.com/matrix-org/synapse/issues/6511), [\#6513](https://github.com/matrix-org/synapse/issues/6513), [\#6564](https://github.com/matrix-org/synapse/issues/6564), [\#6565](https://github.com/matrix-org/synapse/issues/6565)) - Port sections of code base to async/await. ([\#6496](https://github.com/matrix-org/synapse/issues/6496), [\#6504](https://github.com/matrix-org/synapse/issues/6504), [\#6505](https://github.com/matrix-org/synapse/issues/6505), [\#6517](https://github.com/matrix-org/synapse/issues/6517), [\#6559](https://github.com/matrix-org/synapse/issues/6559), [\#6647](https://github.com/matrix-org/synapse/issues/6647), [\#6653](https://github.com/matrix-org/synapse/issues/6653)) - Remove `SnapshotCache` in favour of `ResponseCache`. ([\#6506](https://github.com/matrix-org/synapse/issues/6506)) - Silence mypy errors for files outside those specified. ([\#6512](https://github.com/matrix-org/synapse/issues/6512)) From 235d977e1f17e3a68570289d6018ada23b00f963 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 7 Jan 2020 14:45:54 +0000 Subject: [PATCH 0777/1623] Fixup changelog --- CHANGES.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 9716c8d648..df94f742c0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -21,11 +21,11 @@ Bugfixes - Prevent error on trying to search a upgraded room when the server is not in the predecessor room. ([\#6385](https://github.com/matrix-org/synapse/issues/6385)) - Improve performance of looking up cross-signing keys. ([\#6486](https://github.com/matrix-org/synapse/issues/6486)) - Fix race which occasionally caused deleted devices to reappear. ([\#6514](https://github.com/matrix-org/synapse/issues/6514)) -- Fix missing row in device_max_stream_id that could cause unable to decrypt errors after server restart. ([\#6555](https://github.com/matrix-org/synapse/issues/6555)) +- Fix missing row in `device_max_stream_id` that could cause unable to decrypt errors after server restart. ([\#6555](https://github.com/matrix-org/synapse/issues/6555)) - Fix a bug which meant that we did not send systemd notifications on startup if acme was enabled. ([\#6571](https://github.com/matrix-org/synapse/issues/6571)) - Fix exception when fetching the `matrix.org:ed25519:auto` key. ([\#6625](https://github.com/matrix-org/synapse/issues/6625)) - Fix bug where a moderator upgraded a room and became an admin in the new room. ([\#6633](https://github.com/matrix-org/synapse/issues/6633)) -- Fix an error which was thrown by the PresenceHandler _on_shutdown handler. ([\#6640](https://github.com/matrix-org/synapse/issues/6640)) +- Fix an error which was thrown by the `PresenceHandler` `_on_shutdown` handler. ([\#6640](https://github.com/matrix-org/synapse/issues/6640)) - Fix exceptions in the synchrotron worker log when events are rejected. ([\#6645](https://github.com/matrix-org/synapse/issues/6645)) - Ensure that upgraded rooms are removed from the directory. ([\#6648](https://github.com/matrix-org/synapse/issues/6648)) - Fix a bug causing Synapse not to fetch missing events when it believes it has every event in the room. ([\#6652](https://github.com/matrix-org/synapse/issues/6652)) @@ -35,7 +35,7 @@ Improved Documentation ---------------------- - Document the Room Shutdown Admin API. ([\#6541](https://github.com/matrix-org/synapse/issues/6541)) -- Reword sections of federate.md that explained delegation at time of Synapse 1.0 transition. ([\#6601](https://github.com/matrix-org/synapse/issues/6601)) +- Reword sections of [docs/federate.md](docs/federate.md) that explained delegation at time of Synapse 1.0 transition. ([\#6601](https://github.com/matrix-org/synapse/issues/6601)) - Added the section 'Configuration' in [docs/turn-howto.md](docs/turn-howto.md). ([\#6614](https://github.com/matrix-org/synapse/issues/6614)) From 3a864771624313dde75dfd1fa50dfca5cbadc8ca Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 7 Jan 2020 14:53:07 +0000 Subject: [PATCH 0778/1623] Change the example from 5min to 12h Have a purge job running every 5min is probably not something we want to advise admins to do as a sort-of default. --- docs/sample_config.yaml | 8 ++++---- synapse/config/server.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index cc261d96d0..77a3fe6cd9 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -387,17 +387,17 @@ retention: # # The rationale for this per-job configuration is that some rooms might have a # retention policy with a low 'max_lifetime', where history needs to be purged - # of outdated messages on a very frequent basis (e.g. every 5min), but not want + # of outdated messages on a more frequent basis (e.g. every 12h), but not want # that purge to be performed by a job that's iterating over every room it knows, - # which would be quite heavy on the server. + # which could be heavy on the server. # #purge_jobs: # - shortest_max_lifetime: 1d # longest_max_lifetime: 3d - # interval: 5m + # interval: 12h # - shortest_max_lifetime: 3d # longest_max_lifetime: 1y - # interval: 24h + # interval: 1d ## TLS ## diff --git a/synapse/config/server.py b/synapse/config/server.py index 3463d53d10..11ff559224 100644 --- a/synapse/config/server.py +++ b/synapse/config/server.py @@ -948,17 +948,17 @@ class ServerConfig(Config): # # The rationale for this per-job configuration is that some rooms might have a # retention policy with a low 'max_lifetime', where history needs to be purged - # of outdated messages on a very frequent basis (e.g. every 5min), but not want + # of outdated messages on a more frequent basis (e.g. every 12h), but not want # that purge to be performed by a job that's iterating over every room it knows, - # which would be quite heavy on the server. + # which could be heavy on the server. # #purge_jobs: # - shortest_max_lifetime: 1d # longest_max_lifetime: 3d - # interval: 5m + # interval: 12h # - shortest_max_lifetime: 3d # longest_max_lifetime: 1y - # interval: 24h + # interval: 1d """ % locals() ) From 391fb4779106a291724137e6c52494308729ffcb Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 7 Jan 2020 14:54:32 +0000 Subject: [PATCH 0779/1623] Reword --- docs/sample_config.yaml | 6 +++--- synapse/config/server.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 77a3fe6cd9..cec6b3e544 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -387,9 +387,9 @@ retention: # # The rationale for this per-job configuration is that some rooms might have a # retention policy with a low 'max_lifetime', where history needs to be purged - # of outdated messages on a more frequent basis (e.g. every 12h), but not want - # that purge to be performed by a job that's iterating over every room it knows, - # which could be heavy on the server. + # of outdated messages on a more frequent basis than for the rest of the rooms + # (e.g. every 12h), but not want that purge to be performed by a job that's + # iterating over every room it knows, which could be heavy on the server. # #purge_jobs: # - shortest_max_lifetime: 1d diff --git a/synapse/config/server.py b/synapse/config/server.py index 11ff559224..9ac112233b 100644 --- a/synapse/config/server.py +++ b/synapse/config/server.py @@ -948,9 +948,9 @@ class ServerConfig(Config): # # The rationale for this per-job configuration is that some rooms might have a # retention policy with a low 'max_lifetime', where history needs to be purged - # of outdated messages on a more frequent basis (e.g. every 12h), but not want - # that purge to be performed by a job that's iterating over every room it knows, - # which could be heavy on the server. + # of outdated messages on a more frequent basis than for the rest of the rooms + # (e.g. every 12h), but not want that purge to be performed by a job that's + # iterating over every room it knows, which could be heavy on the server. # #purge_jobs: # - shortest_max_lifetime: 1d From 03edfc58500197fee40c808680551ea55d1560e8 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 7 Jan 2020 15:59:05 +0100 Subject: [PATCH 0780/1623] Update changelog.d/6624.doc Co-Authored-By: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> --- changelog.d/6624.doc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/6624.doc b/changelog.d/6624.doc index c8aade0974..bc9a022db2 100644 --- a/changelog.d/6624.doc +++ b/changelog.d/6624.doc @@ -1 +1 @@ -Add a complete documentation of the message retention policies support. +Add complete documentation of the message retention policies support. From 01fbd9573626381c51700845956c9c9451cb645a Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 7 Jan 2020 15:59:38 +0100 Subject: [PATCH 0781/1623] Apply suggestions from code review Co-Authored-By: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> --- docs/message_retention_policies.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/message_retention_policies.md b/docs/message_retention_policies.md index 72f08fbb4c..84f0925230 100644 --- a/docs/message_retention_policies.md +++ b/docs/message_retention_policies.md @@ -9,13 +9,13 @@ be kept in a homeserver's database before being purged from it. A message retention policy is mainly defined by its `max_lifetime` parameter, which defines how long a message can be kept around after -it's been sent in the room. If a room doesn't have a message retention +it was sent to the room. If a room doesn't have a message retention policy, and there's no default one for a given server, then no message sent in that room is ever purged on that server. MSC1763 also specifies semantics for a `min_lifetime` parameter which defines the amount of time after which an event _can_ get purged (after -it's been sent to the room), but Synapse doesn't currently support it +it was sent to the room), but Synapse doesn't currently support it beyond registering it. Both `max_lifetime` and `min_lifetime` are optional parameters. @@ -70,7 +70,7 @@ Support for this feature can be enabled and configured in the `retention` section of the Synapse configuration file (see the [sample file](https://github.com/matrix-org/synapse/blob/v1.7.3/docs/sample_config.yaml#L332-L393)). -To enable support for message retentions policies, set the setting +To enable support for message retention policies, set the setting `enabled` in this section to `true`. @@ -99,7 +99,7 @@ duration (using the units `s` (seconds), `m` (minutes), `h` (hours), ### Purge jobs -Purge jobs are the jobs that Synapse run in the background to purge +Purge jobs are the jobs that Synapse runs in the background to purge expired events from the database. They are only run if support for message retention policies is enabled in the server's configuration. If no configuration for purge jobs is configured by the server admin, @@ -188,4 +188,3 @@ If you want to reclaim the freed disk space anyway and return it to the operating system, the server admin needs to run `VACUUM FULL;` (or `VACUUM;` for SQLite databases) on Synapse's database (see the related [PostgreSQL documentation](https://www.postgresql.org/docs/current/sql-vacuum.html)). - From 7ba98a2874c6c14c3f2ceb9b633a13d3e7345065 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 7 Jan 2020 15:14:33 +0000 Subject: [PATCH 0782/1623] Incorporate review --- docs/message_retention_policies.md | 55 +++++++++++++++--------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/docs/message_retention_policies.md b/docs/message_retention_policies.md index 84f0925230..42b637516f 100644 --- a/docs/message_retention_policies.md +++ b/docs/message_retention_policies.md @@ -6,6 +6,9 @@ follow the semantics described in [MSC1763](https://github.com/matrix-org/matrix-doc/blob/matthew/msc1763/proposals/1763-configurable-retention-periods.md), and allow server and room admins to configure how long messages should be kept in a homeserver's database before being purged from it. +**Please note that, as this feature isn't part of the Matrix +specification yet, this implementation is to be considered as +experimental.** A message retention policy is mainly defined by its `max_lifetime` parameter, which defines how long a message can be kept around after @@ -40,30 +43,6 @@ process and store that event until it's picked up by the next purge job, though it will always hide it from clients. -## Room configuration - -To configure a room's message retention policy, a room's admin or -moderator needs to send a state event in that room with the type -`m.room.retention` and the following content: - -```json -{ - "max_lifetime": ... -} -``` - -In this event's content, the `max_lifetime` parameter has the same -meaning as previously described, and needs to be expressed in -milliseconds. The event's content can also include a `min_lifetime` -parameter, which has the same meaning and limited support as previously -described. - -Note that over every server in the room, only the ones with support for -message retention policies will actually remove expired events. While -we plan to eventually enable this support by default in Synapse, this -isn't currently the case. - - ## Server configuration Support for this feature can be enabled and configured in the @@ -103,9 +82,8 @@ Purge jobs are the jobs that Synapse runs in the background to purge expired events from the database. They are only run if support for message retention policies is enabled in the server's configuration. If no configuration for purge jobs is configured by the server admin, -Synapse will run one daily that will handle every room with a message -retention policy (or, if the server has a default policy configured, -every room it knows), which should be enough in most cases. +Synapse will use a default configuration, which is described in the +[sample configuration file](https://github.com/matrix-org/synapse/blob/v1.7.3/docs/sample_config.yaml#L332-L393). Some server admins might want a finer control on when events are removed depending on an event's room's policy. This can be done by setting the @@ -177,6 +155,29 @@ Like other settings in this section, these parameters can be expressed either as a duration or as a number of milliseconds. +## Room configuration + +To configure a room's message retention policy, a room's admin or +moderator needs to send a state event in that room with the type +`m.room.retention` and the following content: + +```json +{ + "max_lifetime": ... +} +``` + +In this event's content, the `max_lifetime` parameter has the same +meaning as previously described, and needs to be expressed in +milliseconds. The event's content can also include a `min_lifetime` +parameter, which has the same meaning and limited support as previously +described. + +Note that over every server in the room, only the ones with support for +message retention policies will actually remove expired events. This +support is currently not enabled by default in Synapse. + + ## Note on reclaiming disk space While purge jobs actually delete data from the database, the disk space From 3675fb9bc6b0f9fd068bee443c1499042359ee99 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 7 Jan 2020 15:15:16 +0000 Subject: [PATCH 0783/1623] Fix reference --- docs/message_retention_policies.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/message_retention_policies.md b/docs/message_retention_policies.md index 42b637516f..c4888c81be 100644 --- a/docs/message_retention_policies.md +++ b/docs/message_retention_policies.md @@ -83,7 +83,7 @@ expired events from the database. They are only run if support for message retention policies is enabled in the server's configuration. If no configuration for purge jobs is configured by the server admin, Synapse will use a default configuration, which is described in the -[sample configuration file](https://github.com/matrix-org/synapse/blob/v1.7.3/docs/sample_config.yaml#L332-L393). +[sample configuration file](https://github.com/matrix-org/synapse/blob/master/docs/sample_config.yaml#L332-L393). Some server admins might want a finer control on when events are removed depending on an event's room's policy. This can be done by setting the From be29ed7ad86a150f603722d7dc307b71f7e98726 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 7 Jan 2020 15:36:41 +0000 Subject: [PATCH 0784/1623] Correctly proxy remote group HTTP errors. (#6654) e.g. if remote returns a 404 then that shouldn't be treated as an error but should be proxied through. --- changelog.d/6654.bugfix | 1 + synapse/handlers/groups_local.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 changelog.d/6654.bugfix diff --git a/changelog.d/6654.bugfix b/changelog.d/6654.bugfix new file mode 100644 index 0000000000..fed35252db --- /dev/null +++ b/changelog.d/6654.bugfix @@ -0,0 +1 @@ +Correctly proxy HTTP errors due to API calls to remote group servers. diff --git a/synapse/handlers/groups_local.py b/synapse/handlers/groups_local.py index 92fecbfc44..319565510f 100644 --- a/synapse/handlers/groups_local.py +++ b/synapse/handlers/groups_local.py @@ -130,6 +130,8 @@ class GroupsLocalHandler(object): res = yield self.transport_client.get_group_summary( get_domain_from_id(group_id), group_id, requester_user_id ) + except HttpResponseException as e: + raise e.to_synapse_error() except RequestSendFailed: raise SynapseError(502, "Failed to contact group server") @@ -190,6 +192,8 @@ class GroupsLocalHandler(object): res = yield self.transport_client.create_group( get_domain_from_id(group_id), group_id, user_id, content ) + except HttpResponseException as e: + raise e.to_synapse_error() except RequestSendFailed: raise SynapseError(502, "Failed to contact group server") @@ -231,6 +235,8 @@ class GroupsLocalHandler(object): res = yield self.transport_client.get_users_in_group( get_domain_from_id(group_id), group_id, requester_user_id ) + except HttpResponseException as e: + raise e.to_synapse_error() except RequestSendFailed: raise SynapseError(502, "Failed to contact group server") @@ -271,6 +277,8 @@ class GroupsLocalHandler(object): res = yield self.transport_client.join_group( get_domain_from_id(group_id), group_id, user_id, content ) + except HttpResponseException as e: + raise e.to_synapse_error() except RequestSendFailed: raise SynapseError(502, "Failed to contact group server") @@ -315,6 +323,8 @@ class GroupsLocalHandler(object): res = yield self.transport_client.accept_group_invite( get_domain_from_id(group_id), group_id, user_id, content ) + except HttpResponseException as e: + raise e.to_synapse_error() except RequestSendFailed: raise SynapseError(502, "Failed to contact group server") @@ -361,6 +371,8 @@ class GroupsLocalHandler(object): requester_user_id, content, ) + except HttpResponseException as e: + raise e.to_synapse_error() except RequestSendFailed: raise SynapseError(502, "Failed to contact group server") @@ -424,6 +436,8 @@ class GroupsLocalHandler(object): user_id, content, ) + except HttpResponseException as e: + raise e.to_synapse_error() except RequestSendFailed: raise SynapseError(502, "Failed to contact group server") @@ -460,6 +474,8 @@ class GroupsLocalHandler(object): bulk_result = yield self.transport_client.bulk_get_publicised_groups( get_domain_from_id(user_id), [user_id] ) + except HttpResponseException as e: + raise e.to_synapse_error() except RequestSendFailed: raise SynapseError(502, "Failed to contact group server") From 91718b3f23031e93341b53e0c9b6370f35838a8b Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 7 Jan 2020 15:46:04 +0000 Subject: [PATCH 0785/1623] typo --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index df94f742c0..24da66c596 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,7 +6,7 @@ Features - Add v2 APIs for the `send_join` and `send_leave` federation endpoints (as described in [MSC1802](https://github.com/matrix-org/matrix-doc/pull/1802)). ([\#6349](https://github.com/matrix-org/synapse/issues/6349)) - Add a develop script to generate full SQL schemas. ([\#6394](https://github.com/matrix-org/synapse/issues/6394)) -- Add custom SAML username mapping functinality through an external provider plugin. ([\#6411](https://github.com/matrix-org/synapse/issues/6411)) +- Add custom SAML username mapping functionality through an external provider plugin. ([\#6411](https://github.com/matrix-org/synapse/issues/6411)) - Automatically delete empty groups/communities. ([\#6453](https://github.com/matrix-org/synapse/issues/6453)) - Add option `limit_profile_requests_to_users_who_share_rooms` to prevent requirement of a local user sharing a room with another user to query their profile information. ([\#6523](https://github.com/matrix-org/synapse/issues/6523)) - Add an `export_signing_key` script to extract the public part of signing keys when rotating them. ([\#6546](https://github.com/matrix-org/synapse/issues/6546)) From dd57715de2b9a0742b38aaab63893e2495b68841 Mon Sep 17 00:00:00 2001 From: Fabian Meyer Date: Wed, 8 Jan 2020 08:25:05 +0100 Subject: [PATCH 0786/1623] contrib/docker-compose: fixing mount that overrides containers' /etc (#6656) The mount in the form of ./matrix-config:/etc overwrites the contents of the container /etc folder. Since all valid ca certificates are stored in /etc, the synapse.push.httppusher, for example, cannot validate the certificate from matrix.org. --- changelog.d/6656.doc | 1 + contrib/docker/docker-compose.yml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/6656.doc diff --git a/changelog.d/6656.doc b/changelog.d/6656.doc new file mode 100644 index 0000000000..9f32da1a88 --- /dev/null +++ b/changelog.d/6656.doc @@ -0,0 +1 @@ +No more overriding the entire /etc folder of the container in docker-compose.yaml. Contributed by Fabian Meyer. diff --git a/contrib/docker/docker-compose.yml b/contrib/docker/docker-compose.yml index 72c87054e5..2b044baf78 100644 --- a/contrib/docker/docker-compose.yml +++ b/contrib/docker/docker-compose.yml @@ -18,7 +18,7 @@ services: - SYNAPSE_CONFIG_PATH=/etc/homeserver.yaml volumes: # You may either store all the files in a local folder - - ./matrix-config:/etc + - ./matrix-config/homeserver.yaml:/etc/homeserver.yaml - ./files:/data # .. or you may split this between different storage points # - ./files:/data From 573fee759cbd76fca93bf90783cd013a11b9b4e5 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 8 Jan 2020 13:24:10 +0000 Subject: [PATCH 0787/1623] Back out ill-advised notary server hackery (#6657) This was ill-advised. We can't modify verify_keys here, because the response object has already been signed by the requested key. Furthermore, it's somewhat unnecessary because existing versions of Synapse (which get upset that the notary key isn't present in verify_keys) will fall back to a direct fetch via `/key/v2/server`. Also: more tests for fetching keys via perspectives: it would be nice if we actually tested when our fetcher can't talk to our notary impl. --- changelog.d/6657.bugfix | 1 + synapse/rest/key/v2/remote_key_resource.py | 30 ++-- tests/rest/key/__init__.py | 0 tests/rest/key/v2/__init__.py | 0 tests/rest/key/v2/test_remote_key_resource.py | 135 +++++++++++++++++- 5 files changed, 140 insertions(+), 26 deletions(-) create mode 100644 changelog.d/6657.bugfix create mode 100644 tests/rest/key/__init__.py create mode 100644 tests/rest/key/v2/__init__.py diff --git a/changelog.d/6657.bugfix b/changelog.d/6657.bugfix new file mode 100644 index 0000000000..94e51a9896 --- /dev/null +++ b/changelog.d/6657.bugfix @@ -0,0 +1 @@ +Fix incorrect signing of responses from the key server implementation. \ No newline at end of file diff --git a/synapse/rest/key/v2/remote_key_resource.py b/synapse/rest/key/v2/remote_key_resource.py index bf5e0eb844..e7fc3f0431 100644 --- a/synapse/rest/key/v2/remote_key_resource.py +++ b/synapse/rest/key/v2/remote_key_resource.py @@ -15,7 +15,6 @@ import logging from canonicaljson import encode_canonical_json, json -from signedjson.key import encode_verify_key_base64 from signedjson.sign import sign_json from twisted.internet import defer @@ -217,28 +216,15 @@ class RemoteKey(DirectServeResource): if cache_misses and query_remote_on_cache_miss: yield self.fetcher.get_keys(cache_misses) yield self.query_keys(request, query, query_remote_on_cache_miss=False) - return - - signed_keys = [] - for key_json in json_results: - key_json = json.loads(key_json) - - # backwards-compatibility hack for #6596: if the requested key belongs - # to us, make sure that all of the signing keys appear in the - # "verify_keys" section. - if key_json["server_name"] == self.config.server_name: - verify_keys = key_json["verify_keys"] + else: + signed_keys = [] + for key_json in json_results: + key_json = json.loads(key_json) for signing_key in self.config.key_server_signing_keys: - key_id = "%s:%s" % (signing_key.alg, signing_key.version) - verify_keys[key_id] = { - "key": encode_verify_key_base64(signing_key.verify_key) - } + key_json = sign_json(key_json, self.config.server_name, signing_key) - for signing_key in self.config.key_server_signing_keys: - key_json = sign_json(key_json, self.config.server_name, signing_key) + signed_keys.append(key_json) - signed_keys.append(key_json) + results = {"server_keys": signed_keys} - results = {"server_keys": signed_keys} - - respond_with_json_bytes(request, 200, encode_canonical_json(results)) + respond_with_json_bytes(request, 200, encode_canonical_json(results)) diff --git a/tests/rest/key/__init__.py b/tests/rest/key/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/rest/key/v2/__init__.py b/tests/rest/key/v2/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/rest/key/v2/test_remote_key_resource.py b/tests/rest/key/v2/test_remote_key_resource.py index d8246b4e78..6776a56cad 100644 --- a/tests/rest/key/v2/test_remote_key_resource.py +++ b/tests/rest/key/v2/test_remote_key_resource.py @@ -13,25 +13,30 @@ # See the License for the specific language governing permissions and # limitations under the License. import urllib.parse -from io import BytesIO +from io import BytesIO, StringIO from mock import Mock import signedjson.key +from canonicaljson import encode_canonical_json from nacl.signing import SigningKey from signedjson.sign import sign_json from twisted.web.resource import NoResource +from synapse.crypto.keyring import PerspectivesKeyFetcher from synapse.http.site import SynapseRequest from synapse.rest.key.v2 import KeyApiV2Resource +from synapse.storage.keys import FetchKeyResult from synapse.util.httpresourcetree import create_resource_tree +from synapse.util.stringutils import random_string from tests import unittest from tests.server import FakeChannel, wait_until_result +from tests.utils import default_config -class RemoteKeyResourceTestCase(unittest.HomeserverTestCase): +class BaseRemoteKeyResourceTestCase(unittest.HomeserverTestCase): def make_homeserver(self, reactor, clock): self.http_client = Mock() return self.setup_test_homeserver(http_client=self.http_client) @@ -73,6 +78,8 @@ class RemoteKeyResourceTestCase(unittest.HomeserverTestCase): self.http_client.get_json.side_effect = get_json + +class RemoteKeyResourceTestCase(BaseRemoteKeyResourceTestCase): def make_notary_request(self, server_name: str, key_id: str) -> dict: """Send a GET request to the test server requesting the given key. @@ -125,6 +132,126 @@ class RemoteKeyResourceTestCase(unittest.HomeserverTestCase): oursigs = sigs[self.hs.hostname] self.assertEqual(len(oursigs), 2) - # and both keys should be present in the verify_keys section + # the requested key should be present in the verify_keys section self.assertIn("ed25519:ver1", keys[0]["verify_keys"]) - self.assertIn("ed25519:a_lPym", keys[0]["verify_keys"]) + + +class EndToEndPerspectivesTests(BaseRemoteKeyResourceTestCase): + """End-to-end tests of the perspectives fetch case + + The idea here is to actually wire up a PerspectivesKeyFetcher to the notary + endpoint, to check that the two implementations are compatible. + """ + + def default_config(self, *args, **kwargs): + config = super().default_config(*args, **kwargs) + + # replace the signing key with our own + self.hs_signing_key = signedjson.key.generate_signing_key("kssk") + strm = StringIO() + signedjson.key.write_signing_keys(strm, [self.hs_signing_key]) + config["signing_key"] = strm.getvalue() + + return config + + def prepare(self, reactor, clock, homeserver): + # make a second homeserver, configured to use the first one as a key notary + self.http_client2 = Mock() + config = default_config(name="keyclient") + config["trusted_key_servers"] = [ + { + "server_name": self.hs.hostname, + "verify_keys": { + "ed25519:%s" + % ( + self.hs_signing_key.version, + ): signedjson.key.encode_verify_key_base64( + self.hs_signing_key.verify_key + ) + }, + } + ] + self.hs2 = self.setup_test_homeserver( + http_client=self.http_client2, config=config + ) + + # wire up outbound POST /key/v2/query requests from hs2 so that they + # will be forwarded to hs1 + def post_json(destination, path, data): + self.assertEqual(destination, self.hs.hostname) + self.assertEqual( + path, "/_matrix/key/v2/query", + ) + + channel = FakeChannel(self.site, self.reactor) + req = SynapseRequest(channel) + req.content = BytesIO(encode_canonical_json(data)) + + req.requestReceived( + b"POST", path.encode("utf-8"), b"1.1", + ) + wait_until_result(self.reactor, req) + self.assertEqual(channel.code, 200) + resp = channel.json_body + return resp + + self.http_client2.post_json.side_effect = post_json + + def test_get_key(self): + """Fetch a key belonging to a random server""" + # make up a key to be fetched. + testkey = signedjson.key.generate_signing_key("abc") + + # we expect hs1 to make a regular key request to the target server + self.expect_outgoing_key_request("targetserver", testkey) + keyid = "ed25519:%s" % (testkey.version,) + + fetcher = PerspectivesKeyFetcher(self.hs2) + d = fetcher.get_keys({"targetserver": {keyid: 1000}}) + res = self.get_success(d) + self.assertIn("targetserver", res) + keyres = res["targetserver"][keyid] + assert isinstance(keyres, FetchKeyResult) + self.assertEqual( + signedjson.key.encode_verify_key_base64(keyres.verify_key), + signedjson.key.encode_verify_key_base64(testkey.verify_key), + ) + + def test_get_notary_key(self): + """Fetch a key belonging to the notary server""" + # make up a key to be fetched. We randomise the keyid to try to get it to + # appear before the key server signing key sometimes (otherwise we bail out + # before fetching its signature) + testkey = signedjson.key.generate_signing_key(random_string(5)) + + # we expect hs1 to make a regular key request to itself + self.expect_outgoing_key_request(self.hs.hostname, testkey) + keyid = "ed25519:%s" % (testkey.version,) + + fetcher = PerspectivesKeyFetcher(self.hs2) + d = fetcher.get_keys({self.hs.hostname: {keyid: 1000}}) + res = self.get_success(d) + self.assertIn(self.hs.hostname, res) + keyres = res[self.hs.hostname][keyid] + assert isinstance(keyres, FetchKeyResult) + self.assertEqual( + signedjson.key.encode_verify_key_base64(keyres.verify_key), + signedjson.key.encode_verify_key_base64(testkey.verify_key), + ) + + def test_get_notary_keyserver_key(self): + """Fetch the notary's keyserver key""" + # we expect hs1 to make a regular key request to itself + self.expect_outgoing_key_request(self.hs.hostname, self.hs_signing_key) + keyid = "ed25519:%s" % (self.hs_signing_key.version,) + + fetcher = PerspectivesKeyFetcher(self.hs2) + d = fetcher.get_keys({self.hs.hostname: {keyid: 1000}}) + res = self.get_success(d) + self.assertIn(self.hs.hostname, res) + keyres = res[self.hs.hostname][keyid] + assert isinstance(keyres, FetchKeyResult) + self.assertEqual( + signedjson.key.encode_verify_key_base64(keyres.verify_key), + signedjson.key.encode_verify_key_base64(self.hs_signing_key.verify_key), + ) From 3cf7d6d5b6bc938414533d5afe31a67e97c3b7d9 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 8 Jan 2020 13:20:43 +0000 Subject: [PATCH 0788/1623] Move media admin store functions to worker store --- synapse/storage/data_stores/main/room.py | 240 +++++++++++------------ 1 file changed, 120 insertions(+), 120 deletions(-) diff --git a/synapse/storage/data_stores/main/room.py b/synapse/storage/data_stores/main/room.py index 79cfd39194..652518049a 100644 --- a/synapse/storage/data_stores/main/room.py +++ b/synapse/storage/data_stores/main/room.py @@ -366,6 +366,126 @@ class RoomWorkerStore(SQLBaseStore): defer.returnValue(row) + def get_media_mxcs_in_room(self, room_id): + """Retrieves all the local and remote media MXC URIs in a given room + + Args: + room_id (str) + + Returns: + The local and remote media as a lists of tuples where the key is + the hostname and the value is the media ID. + """ + + def _get_media_mxcs_in_room_txn(txn): + local_mxcs, remote_mxcs = self._get_media_mxcs_in_room_txn(txn, room_id) + local_media_mxcs = [] + remote_media_mxcs = [] + + # Convert the IDs to MXC URIs + for media_id in local_mxcs: + local_media_mxcs.append("mxc://%s/%s" % (self.hs.hostname, media_id)) + for hostname, media_id in remote_mxcs: + remote_media_mxcs.append("mxc://%s/%s" % (hostname, media_id)) + + return local_media_mxcs, remote_media_mxcs + + return self.db.runInteraction( + "get_media_ids_in_room", _get_media_mxcs_in_room_txn + ) + + def quarantine_media_ids_in_room(self, room_id, quarantined_by): + """For a room loops through all events with media and quarantines + the associated media + """ + + def _quarantine_media_in_room_txn(txn): + local_mxcs, remote_mxcs = self._get_media_mxcs_in_room_txn(txn, room_id) + total_media_quarantined = 0 + + # Now update all the tables to set the quarantined_by flag + + txn.executemany( + """ + UPDATE local_media_repository + SET quarantined_by = ? + WHERE media_id = ? + """, + ((quarantined_by, media_id) for media_id in local_mxcs), + ) + + txn.executemany( + """ + UPDATE remote_media_cache + SET quarantined_by = ? + WHERE media_origin = ? AND media_id = ? + """, + ( + (quarantined_by, origin, media_id) + for origin, media_id in remote_mxcs + ), + ) + + total_media_quarantined += len(local_mxcs) + total_media_quarantined += len(remote_mxcs) + + return total_media_quarantined + + return self.db.runInteraction( + "quarantine_media_in_room", _quarantine_media_in_room_txn + ) + + def _get_media_mxcs_in_room_txn(self, txn, room_id): + """Retrieves all the local and remote media MXC URIs in a given room + + Args: + txn (cursor) + room_id (str) + + Returns: + The local and remote media as a lists of tuples where the key is + the hostname and the value is the media ID. + """ + mxc_re = re.compile("^mxc://([^/]+)/([^/#?]+)") + + next_token = self.get_current_events_token() + 1 + local_media_mxcs = [] + remote_media_mxcs = [] + + while next_token: + sql = """ + SELECT stream_ordering, json FROM events + JOIN event_json USING (room_id, event_id) + WHERE room_id = ? + AND stream_ordering < ? + AND contains_url = ? AND outlier = ? + ORDER BY stream_ordering DESC + LIMIT ? + """ + txn.execute(sql, (room_id, next_token, True, False, 100)) + + next_token = None + for stream_ordering, content_json in txn: + next_token = stream_ordering + event_json = json.loads(content_json) + content = event_json["content"] + content_url = content.get("url") + thumbnail_url = content.get("info", {}).get("thumbnail_url") + + for url in (content_url, thumbnail_url): + if not url: + continue + matches = mxc_re.match(url) + if matches: + hostname = matches.group(1) + media_id = matches.group(2) + if hostname == self.hs.hostname: + local_media_mxcs.append(media_id) + else: + remote_media_mxcs.append((hostname, media_id)) + + return local_media_mxcs, remote_media_mxcs + class RoomBackgroundUpdateStore(SQLBaseStore): REMOVE_TOMESTONED_ROOMS_BG_UPDATE = "remove_tombstoned_rooms_from_directory" @@ -810,126 +930,6 @@ class RoomStore(RoomBackgroundUpdateStore, RoomWorkerStore, SearchStore): (room_id,), ) - def get_media_mxcs_in_room(self, room_id): - """Retrieves all the local and remote media MXC URIs in a given room - - Args: - room_id (str) - - Returns: - The local and remote media as a lists of tuples where the key is - the hostname and the value is the media ID. - """ - - def _get_media_mxcs_in_room_txn(txn): - local_mxcs, remote_mxcs = self._get_media_mxcs_in_room_txn(txn, room_id) - local_media_mxcs = [] - remote_media_mxcs = [] - - # Convert the IDs to MXC URIs - for media_id in local_mxcs: - local_media_mxcs.append("mxc://%s/%s" % (self.hs.hostname, media_id)) - for hostname, media_id in remote_mxcs: - remote_media_mxcs.append("mxc://%s/%s" % (hostname, media_id)) - - return local_media_mxcs, remote_media_mxcs - - return self.db.runInteraction( - "get_media_ids_in_room", _get_media_mxcs_in_room_txn - ) - - def quarantine_media_ids_in_room(self, room_id, quarantined_by): - """For a room loops through all events with media and quarantines - the associated media - """ - - def _quarantine_media_in_room_txn(txn): - local_mxcs, remote_mxcs = self._get_media_mxcs_in_room_txn(txn, room_id) - total_media_quarantined = 0 - - # Now update all the tables to set the quarantined_by flag - - txn.executemany( - """ - UPDATE local_media_repository - SET quarantined_by = ? - WHERE media_id = ? - """, - ((quarantined_by, media_id) for media_id in local_mxcs), - ) - - txn.executemany( - """ - UPDATE remote_media_cache - SET quarantined_by = ? - WHERE media_origin = ? AND media_id = ? - """, - ( - (quarantined_by, origin, media_id) - for origin, media_id in remote_mxcs - ), - ) - - total_media_quarantined += len(local_mxcs) - total_media_quarantined += len(remote_mxcs) - - return total_media_quarantined - - return self.db.runInteraction( - "quarantine_media_in_room", _quarantine_media_in_room_txn - ) - - def _get_media_mxcs_in_room_txn(self, txn, room_id): - """Retrieves all the local and remote media MXC URIs in a given room - - Args: - txn (cursor) - room_id (str) - - Returns: - The local and remote media as a lists of tuples where the key is - the hostname and the value is the media ID. - """ - mxc_re = re.compile("^mxc://([^/]+)/([^/#?]+)") - - next_token = self.get_current_events_token() + 1 - local_media_mxcs = [] - remote_media_mxcs = [] - - while next_token: - sql = """ - SELECT stream_ordering, json FROM events - JOIN event_json USING (room_id, event_id) - WHERE room_id = ? - AND stream_ordering < ? - AND contains_url = ? AND outlier = ? - ORDER BY stream_ordering DESC - LIMIT ? - """ - txn.execute(sql, (room_id, next_token, True, False, 100)) - - next_token = None - for stream_ordering, content_json in txn: - next_token = stream_ordering - event_json = json.loads(content_json) - content = event_json["content"] - content_url = content.get("url") - thumbnail_url = content.get("info", {}).get("thumbnail_url") - - for url in (content_url, thumbnail_url): - if not url: - continue - matches = mxc_re.match(url) - if matches: - hostname = matches.group(1) - media_id = matches.group(2) - if hostname == self.hs.hostname: - local_media_mxcs.append(media_id) - else: - remote_media_mxcs.append((hostname, media_id)) - - return local_media_mxcs, remote_media_mxcs - @defer.inlineCallbacks def get_rooms_for_retention_period_in_range( self, min_ms, max_ms, include_null=False From 1adf27c82a6ead7f5001b95240c5dadbc0fd386d Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 8 Jan 2020 13:25:31 +0000 Subject: [PATCH 0789/1623] Import RoomStore in media worker to fix admin APIs --- synapse/app/media_repository.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/synapse/app/media_repository.py b/synapse/app/media_repository.py index a63c53dc44..5b5832214a 100644 --- a/synapse/app/media_repository.py +++ b/synapse/app/media_repository.py @@ -34,6 +34,7 @@ from synapse.replication.slave.storage._base import BaseSlavedStore from synapse.replication.slave.storage.appservice import SlavedApplicationServiceStore from synapse.replication.slave.storage.client_ips import SlavedClientIpStore from synapse.replication.slave.storage.registration import SlavedRegistrationStore +from synapse.replication.slave.storage.room import RoomStore from synapse.replication.slave.storage.transactions import SlavedTransactionStore from synapse.replication.tcp.client import ReplicationClientHandler from synapse.rest.admin import register_servlets_for_media_repo @@ -47,6 +48,7 @@ logger = logging.getLogger("synapse.app.media_repository") class MediaRepositorySlavedStore( + RoomStore, SlavedApplicationServiceStore, SlavedRegistrationStore, SlavedClientIpStore, From 7caaa29daab7bde331dcec0bb760a8fe5870f18e Mon Sep 17 00:00:00 2001 From: Manuel Stahl <37705355+awesome-manuel@users.noreply.github.com> Date: Wed, 8 Jan 2020 14:26:40 +0100 Subject: [PATCH 0790/1623] Fix GET request on /_synapse/admin/v2/users endpoint (#6563) Fixes #6552 --- changelog.d/6563.bugfix | 1 + synapse/storage/data_stores/main/__init__.py | 4 +- tests/rest/admin/test_admin.py | 41 ++++++++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 changelog.d/6563.bugfix diff --git a/changelog.d/6563.bugfix b/changelog.d/6563.bugfix new file mode 100644 index 0000000000..3325fb1dcf --- /dev/null +++ b/changelog.d/6563.bugfix @@ -0,0 +1 @@ +Fix GET request on /_synapse/admin/v2/users endpoint. Contributed by Awesome Technologies Innovationslabor GmbH. \ No newline at end of file diff --git a/synapse/storage/data_stores/main/__init__.py b/synapse/storage/data_stores/main/__init__.py index c577c0df5f..2700cca822 100644 --- a/synapse/storage/data_stores/main/__init__.py +++ b/synapse/storage/data_stores/main/__init__.py @@ -526,9 +526,9 @@ class DataStore( attr_filter = {} if not guests: - attr_filter["is_guest"] = False + attr_filter["is_guest"] = 0 if not deactivated: - attr_filter["deactivated"] = False + attr_filter["deactivated"] = 0 return self.db.simple_select_list_paginate( desc="get_users_paginate", diff --git a/tests/rest/admin/test_admin.py b/tests/rest/admin/test_admin.py index 0ed2594381..325bd6a608 100644 --- a/tests/rest/admin/test_admin.py +++ b/tests/rest/admin/test_admin.py @@ -341,6 +341,47 @@ class UserRegisterTestCase(unittest.HomeserverTestCase): self.assertEqual("Invalid user type", channel.json_body["error"]) +class UsersListTestCase(unittest.HomeserverTestCase): + + servlets = [ + synapse.rest.admin.register_servlets, + login.register_servlets, + ] + url = "/_synapse/admin/v2/users" + + def prepare(self, reactor, clock, hs): + self.admin_user = self.register_user("admin", "pass", admin=True) + self.admin_user_tok = self.login("admin", "pass") + + self.register_user("user1", "pass1", admin=False) + self.register_user("user2", "pass2", admin=False) + + def test_no_auth(self): + """ + Try to list users without authentication. + """ + request, channel = self.make_request("GET", self.url, b"{}") + self.render(request) + + self.assertEqual(401, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("M_MISSING_TOKEN", channel.json_body["errcode"]) + + def test_all_users(self): + """ + List all users, including deactivated users. + """ + request, channel = self.make_request( + "GET", + self.url + "?deactivated=true", + b"{}", + access_token=self.admin_user_tok, + ) + self.render(request) + + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(3, len(channel.json_body["users"])) + + class ShutdownRoomTestCase(unittest.HomeserverTestCase): servlets = [ synapse.rest.admin.register_servlets_for_client_rest_resource, From 3889fcd9d7074c228b90726d97b4ecf372ecc117 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 8 Jan 2020 13:27:29 +0000 Subject: [PATCH 0791/1623] Fix typo in message retention policies doc --- docs/message_retention_policies.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/message_retention_policies.md b/docs/message_retention_policies.md index c4888c81be..4300809dfe 100644 --- a/docs/message_retention_policies.md +++ b/docs/message_retention_policies.md @@ -101,7 +101,7 @@ file. An example of such configuration could be: interval: 2d ``` -In this example, we define two jobs: +In this example, we define three jobs: * one that runs twice a day (every 12 hours) and purges events in rooms which policy's `max_lifetime` is lower or equal to 3 days. From 32ad2a3349e262a431aa9c57fef2d89f629aac31 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 8 Jan 2020 13:28:12 +0000 Subject: [PATCH 0792/1623] Changelog --- changelog.d/6665.doc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6665.doc diff --git a/changelog.d/6665.doc b/changelog.d/6665.doc new file mode 100644 index 0000000000..bc9a022db2 --- /dev/null +++ b/changelog.d/6665.doc @@ -0,0 +1 @@ +Add complete documentation of the message retention policies support. From 4e2a072a05c1f894687772e6e55a943f3d941fbc Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 8 Jan 2020 13:28:19 +0000 Subject: [PATCH 0793/1623] Newsfile --- changelog.d/6664.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6664.bugfix diff --git a/changelog.d/6664.bugfix b/changelog.d/6664.bugfix new file mode 100644 index 0000000000..8c6a6fa1c8 --- /dev/null +++ b/changelog.d/6664.bugfix @@ -0,0 +1 @@ +Fix media repo admin APIs when using a media worker. From 187dc6ad0278c12f15cf2300659d5a1e10ef2446 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 8 Jan 2020 14:23:27 +0000 Subject: [PATCH 0794/1623] Do not rely on streaming events, as media repo doesn't --- synapse/storage/data_stores/main/room.py | 38 ++++++++++++++++-------- 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/synapse/storage/data_stores/main/room.py b/synapse/storage/data_stores/main/room.py index 652518049a..0509d9f64d 100644 --- a/synapse/storage/data_stores/main/room.py +++ b/synapse/storage/data_stores/main/room.py @@ -448,21 +448,32 @@ class RoomWorkerStore(SQLBaseStore): """ mxc_re = re.compile("^mxc://([^/]+)/([^/#?]+)") - next_token = self.get_current_events_token() + 1 + next_token = None local_media_mxcs = [] remote_media_mxcs = [] - while next_token: - sql = """ - SELECT stream_ordering, json FROM events - JOIN event_json USING (room_id, event_id) - WHERE room_id = ? - AND stream_ordering < ? - AND contains_url = ? AND outlier = ? - ORDER BY stream_ordering DESC - LIMIT ? - """ - txn.execute(sql, (room_id, next_token, True, False, 100)) + while True: + if next_token is None: + sql = """ + SELECT stream_ordering, json FROM events + JOIN event_json USING (room_id, event_id) + WHERE room_id = ? + AND contains_url = ? AND outlier = ? + ORDER BY stream_ordering DESC + LIMIT ? + """ + txn.execute(sql, (room_id, True, False, 100)) + else: + sql = """ + SELECT stream_ordering, json FROM events + JOIN event_json USING (room_id, event_id) + WHERE room_id = ? + AND stream_ordering < ? + AND contains_url = ? AND outlier = ? + ORDER BY stream_ordering DESC + LIMIT ? + """ + txn.execute(sql, (room_id, next_token, True, False, 100)) next_token = None for stream_ordering, content_json in txn: @@ -484,6 +495,9 @@ class RoomWorkerStore(SQLBaseStore): else: remote_media_mxcs.append((hostname, media_id)) + if next_token is None: + break + return local_media_mxcs, remote_media_mxcs From bca3455b3860b80199e3b750a99de6e13d636d82 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 8 Jan 2020 14:27:35 +0000 Subject: [PATCH 0795/1623] Comments --- synapse/storage/data_stores/main/room.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/synapse/storage/data_stores/main/room.py b/synapse/storage/data_stores/main/room.py index 0509d9f64d..11e93fd668 100644 --- a/synapse/storage/data_stores/main/room.py +++ b/synapse/storage/data_stores/main/room.py @@ -453,6 +453,8 @@ class RoomWorkerStore(SQLBaseStore): remote_media_mxcs = [] while True: + # The first time round we just want to get the most recent + # events, then we bound by stream ordering if next_token is None: sql = """ SELECT stream_ordering, json FROM events @@ -496,6 +498,7 @@ class RoomWorkerStore(SQLBaseStore): remote_media_mxcs.append((hostname, media_id)) if next_token is None: + # We've gone through the whole room, so we're finished. break return local_media_mxcs, remote_media_mxcs From d74054afda9cf7c63c9f74a9b55c02f9df321d6d Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 8 Jan 2020 14:57:45 +0000 Subject: [PATCH 0796/1623] Shuffle the code --- synapse/storage/data_stores/main/room.py | 41 +++++++++--------------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/synapse/storage/data_stores/main/room.py b/synapse/storage/data_stores/main/room.py index 11e93fd668..8636d75030 100644 --- a/synapse/storage/data_stores/main/room.py +++ b/synapse/storage/data_stores/main/room.py @@ -448,35 +448,21 @@ class RoomWorkerStore(SQLBaseStore): """ mxc_re = re.compile("^mxc://([^/]+)/([^/#?]+)") - next_token = None + sql = """ + SELECT stream_ordering, json FROM events + JOIN event_json USING (room_id, event_id) + WHERE room_id = ? + %(where_clause)s + AND contains_url = ? AND outlier = ? + ORDER BY stream_ordering DESC + LIMIT ? + """ + txn.execute(sql % {"where_clause": ""}, (room_id, True, False, 100)) + local_media_mxcs = [] remote_media_mxcs = [] while True: - # The first time round we just want to get the most recent - # events, then we bound by stream ordering - if next_token is None: - sql = """ - SELECT stream_ordering, json FROM events - JOIN event_json USING (room_id, event_id) - WHERE room_id = ? - AND contains_url = ? AND outlier = ? - ORDER BY stream_ordering DESC - LIMIT ? - """ - txn.execute(sql, (room_id, True, False, 100)) - else: - sql = """ - SELECT stream_ordering, json FROM events - JOIN event_json USING (room_id, event_id) - WHERE room_id = ? - AND stream_ordering < ? - AND contains_url = ? AND outlier = ? - ORDER BY stream_ordering DESC - LIMIT ? - """ - txn.execute(sql, (room_id, next_token, True, False, 100)) - next_token = None for stream_ordering, content_json in txn: next_token = stream_ordering @@ -501,6 +487,11 @@ class RoomWorkerStore(SQLBaseStore): # We've gone through the whole room, so we're finished. break + txn.execute( + sql % {"where_clause": "AND stream_ordering < ?"}, + (room_id, next_token, True, False, 100), + ) + return local_media_mxcs, remote_media_mxcs From 24b2c940fb657270bfbe3d7a18c5e9363c42663d Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 9 Jan 2020 11:39:29 +0000 Subject: [PATCH 0797/1623] 1.8.0 --- CHANGES.md | 10 ++++++++++ changelog.d/6563.bugfix | 1 - changelog.d/6657.bugfix | 1 - debian/changelog | 8 ++++++-- synapse/__init__.py | 2 +- 5 files changed, 17 insertions(+), 5 deletions(-) delete mode 100644 changelog.d/6563.bugfix delete mode 100644 changelog.d/6657.bugfix diff --git a/CHANGES.md b/CHANGES.md index df94f742c0..e33e0d7f07 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,13 @@ +Synapse 1.8.0 (2020-01-09) +========================== + +Bugfixes +-------- + +- Fix GET request on /_synapse/admin/v2/users endpoint. Contributed by Awesome Technologies Innovationslabor GmbH. ([\#6563](https://github.com/matrix-org/synapse/issues/6563)) +- Fix incorrect signing of responses from the key server implementation. ([\#6657](https://github.com/matrix-org/synapse/issues/6657)) + + Synapse 1.8.0rc1 (2020-01-07) ============================= diff --git a/changelog.d/6563.bugfix b/changelog.d/6563.bugfix deleted file mode 100644 index 3325fb1dcf..0000000000 --- a/changelog.d/6563.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix GET request on /_synapse/admin/v2/users endpoint. Contributed by Awesome Technologies Innovationslabor GmbH. \ No newline at end of file diff --git a/changelog.d/6657.bugfix b/changelog.d/6657.bugfix deleted file mode 100644 index 94e51a9896..0000000000 --- a/changelog.d/6657.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix incorrect signing of responses from the key server implementation. \ No newline at end of file diff --git a/debian/changelog b/debian/changelog index 75fe89fa97..7413c238e6 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,12 @@ -matrix-synapse-py3 (1.7.3ubuntu1) UNRELEASED; urgency=medium +matrix-synapse-py3 (1.8.0) stable; urgency=medium + [ Richard van der Hoff ] * Automate generation of the default log configuration file. - -- Richard van der Hoff Fri, 03 Jan 2020 13:55:38 +0000 + [ Synapse Packaging team ] + * New synapse release 1.8.0. + + -- Synapse Packaging team Thu, 09 Jan 2020 11:39:27 +0000 matrix-synapse-py3 (1.7.3) stable; urgency=medium diff --git a/synapse/__init__.py b/synapse/__init__.py index a3bd855045..0dd538d804 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -36,7 +36,7 @@ try: except ImportError: pass -__version__ = "1.8.0rc1" +__version__ = "1.8.0" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 9dfcf47e9bb323f0597ebf8f34a1bcc9f14a02a1 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 9 Jan 2020 11:40:27 +0000 Subject: [PATCH 0798/1623] Fixup changelog --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index e33e0d7f07..690cbdaae9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,7 +4,7 @@ Synapse 1.8.0 (2020-01-09) Bugfixes -------- -- Fix GET request on /_synapse/admin/v2/users endpoint. Contributed by Awesome Technologies Innovationslabor GmbH. ([\#6563](https://github.com/matrix-org/synapse/issues/6563)) +- Fix `GET` request on `/_synapse/admin/v2/users` endpoint. Contributed by Awesome Technologies Innovationslabor GmbH. ([\#6563](https://github.com/matrix-org/synapse/issues/6563)) - Fix incorrect signing of responses from the key server implementation. ([\#6657](https://github.com/matrix-org/synapse/issues/6657)) From d2906fe6667d3384f37ef03ca87172d643d49587 Mon Sep 17 00:00:00 2001 From: Manuel Stahl <37705355+awesome-manuel@users.noreply.github.com> Date: Thu, 9 Jan 2020 14:31:00 +0100 Subject: [PATCH 0799/1623] Allow admin users to create or modify users without a shared secret (#6495) Signed-off-by: Manuel Stahl --- changelog.d/5742.feature | 1 + docs/admin_api/user_admin_api.rst | 33 +- synapse/handlers/admin.py | 9 + synapse/rest/admin/__init__.py | 2 + synapse/rest/admin/users.py | 142 ++++++ .../storage/data_stores/main/registration.py | 2 + tests/rest/admin/test_admin.py | 338 ------------- tests/rest/admin/test_user.py | 465 ++++++++++++++++++ tests/storage/test_registration.py | 2 + 9 files changed, 655 insertions(+), 339 deletions(-) create mode 100644 changelog.d/5742.feature create mode 100644 tests/rest/admin/test_user.py diff --git a/changelog.d/5742.feature b/changelog.d/5742.feature new file mode 100644 index 0000000000..de10302275 --- /dev/null +++ b/changelog.d/5742.feature @@ -0,0 +1 @@ +Allow admin to create or modify a user. Contributed by Awesome Technologies Innovationslabor GmbH. diff --git a/docs/admin_api/user_admin_api.rst b/docs/admin_api/user_admin_api.rst index b451dc5014..0b3d09d694 100644 --- a/docs/admin_api/user_admin_api.rst +++ b/docs/admin_api/user_admin_api.rst @@ -1,3 +1,33 @@ +Create or modify Account +======================== + +This API allows an administrator to create or modify a user account with a +specific ``user_id``. + +This api is:: + + PUT /_synapse/admin/v2/users/ + +with a body of: + +.. code:: json + + { + "password": "user_password", + "displayname": "User", + "avatar_url": "", + "admin": false, + "deactivated": false + } + +including an ``access_token`` of a server admin. + +The parameter ``displayname`` is optional and defaults to ``user_id``. +The parameter ``avatar_url`` is optional. +The parameter ``admin`` is optional and defaults to 'false'. +The parameter ``deactivated`` is optional and defaults to 'false'. +If the user already exists then optional parameters default to the current value. + List Accounts ============= @@ -50,7 +80,8 @@ This API returns information about a specific user account. The api is:: - GET /_synapse/admin/v1/whois/ + GET /_synapse/admin/v1/whois/ (deprecated) + GET /_synapse/admin/v2/users/ including an ``access_token`` of a server admin. diff --git a/synapse/handlers/admin.py b/synapse/handlers/admin.py index 1a4ba12385..76d18a8ba8 100644 --- a/synapse/handlers/admin.py +++ b/synapse/handlers/admin.py @@ -51,6 +51,15 @@ class AdminHandler(BaseHandler): return ret + async def get_user(self, user): + """Function to get user details""" + ret = await self.store.get_user_by_id(user.to_string()) + if ret: + profile = await self.store.get_profileinfo(user.localpart) + ret["displayname"] = profile.display_name + ret["avatar_url"] = profile.avatar_url + return ret + async def get_users(self): """Function to retrieve a list of users in users table. diff --git a/synapse/rest/admin/__init__.py b/synapse/rest/admin/__init__.py index c122c449f4..a10b4a9b72 100644 --- a/synapse/rest/admin/__init__.py +++ b/synapse/rest/admin/__init__.py @@ -38,6 +38,7 @@ from synapse.rest.admin.users import ( SearchUsersRestServlet, UserAdminServlet, UserRegisterServlet, + UserRestServletV2, UsersRestServlet, UsersRestServletV2, WhoisRestServlet, @@ -191,6 +192,7 @@ def register_servlets(hs, http_server): SendServerNoticeServlet(hs).register(http_server) VersionServlet(hs).register(http_server) UserAdminServlet(hs).register(http_server) + UserRestServletV2(hs).register(http_server) UsersRestServletV2(hs).register(http_server) diff --git a/synapse/rest/admin/users.py b/synapse/rest/admin/users.py index 1937879dbe..574cb90c74 100644 --- a/synapse/rest/admin/users.py +++ b/synapse/rest/admin/users.py @@ -102,6 +102,148 @@ class UsersRestServletV2(RestServlet): return 200, ret +class UserRestServletV2(RestServlet): + PATTERNS = (re.compile("^/_synapse/admin/v2/users/(?P@[^/]+)$"),) + + """Get request to list user details. + This needs user to have administrator access in Synapse. + + GET /_synapse/admin/v2/users/ + + returns: + 200 OK with user details if success otherwise an error. + + Put request to allow an administrator to add or modify a user. + This needs user to have administrator access in Synapse. + We use PUT instead of POST since we already know the id of the user + object to create. POST could be used to create guests. + + PUT /_synapse/admin/v2/users/ + { + "password": "secret", + "displayname": "User" + } + + returns: + 201 OK with new user object if user was created or + 200 OK with modified user object if user was modified + otherwise an error. + """ + + def __init__(self, hs): + self.hs = hs + self.auth = hs.get_auth() + self.admin_handler = hs.get_handlers().admin_handler + self.profile_handler = hs.get_profile_handler() + self.set_password_handler = hs.get_set_password_handler() + self.deactivate_account_handler = hs.get_deactivate_account_handler() + self.registration_handler = hs.get_registration_handler() + + async def on_GET(self, request, user_id): + await assert_requester_is_admin(self.auth, request) + + target_user = UserID.from_string(user_id) + if not self.hs.is_mine(target_user): + raise SynapseError(400, "Can only lookup local users") + + ret = await self.admin_handler.get_user(target_user) + + return 200, ret + + async def on_PUT(self, request, user_id): + await assert_requester_is_admin(self.auth, request) + + target_user = UserID.from_string(user_id) + body = parse_json_object_from_request(request) + + if not self.hs.is_mine(target_user): + raise SynapseError(400, "This endpoint can only be used with local users") + + user = await self.admin_handler.get_user(target_user) + + if user: # modify user + requester = await self.auth.get_user_by_req(request) + + if "displayname" in body: + await self.profile_handler.set_displayname( + target_user, requester, body["displayname"], True + ) + + if "avatar_url" in body: + await self.profile_handler.set_avatar_url( + target_user, requester, body["avatar_url"], True + ) + + if "admin" in body: + set_admin_to = bool(body["admin"]) + if set_admin_to != user["admin"]: + auth_user = requester.user + if target_user == auth_user and not set_admin_to: + raise SynapseError(400, "You may not demote yourself.") + + await self.admin_handler.set_user_server_admin( + target_user, set_admin_to + ) + + if "password" in body: + if ( + not isinstance(body["password"], text_type) + or len(body["password"]) > 512 + ): + raise SynapseError(400, "Invalid password") + else: + new_password = body["password"] + await self._set_password_handler.set_password( + target_user, new_password, requester + ) + + if "deactivated" in body: + deactivate = bool(body["deactivated"]) + if deactivate and not user["deactivated"]: + result = await self.deactivate_account_handler.deactivate_account( + target_user.to_string(), False + ) + if not result: + raise SynapseError(500, "Could not deactivate user") + + user = await self.admin_handler.get_user(target_user) + return 200, user + + else: # create user + if "password" not in body: + raise SynapseError( + 400, "password must be specified", errcode=Codes.BAD_JSON + ) + elif ( + not isinstance(body["password"], text_type) + or len(body["password"]) > 512 + ): + raise SynapseError(400, "Invalid password") + + admin = body.get("admin", None) + user_type = body.get("user_type", None) + displayname = body.get("displayname", None) + + if user_type is not None and user_type not in UserTypes.ALL_USER_TYPES: + raise SynapseError(400, "Invalid user type") + + user_id = await self.registration_handler.register_user( + localpart=target_user.localpart, + password=body["password"], + admin=bool(admin), + default_display_name=displayname, + user_type=user_type, + ) + if "avatar_url" in body: + await self.profile_handler.set_avatar_url( + user_id, requester, body["avatar_url"], True + ) + + ret = await self.admin_handler.get_user(target_user) + + return 201, ret + + class UserRegisterServlet(RestServlet): """ Attributes: diff --git a/synapse/storage/data_stores/main/registration.py b/synapse/storage/data_stores/main/registration.py index 5e8ecac0ea..cb4b2b39a0 100644 --- a/synapse/storage/data_stores/main/registration.py +++ b/synapse/storage/data_stores/main/registration.py @@ -52,11 +52,13 @@ class RegistrationWorkerStore(SQLBaseStore): "name", "password_hash", "is_guest", + "admin", "consent_version", "consent_server_notice_sent", "appservice_id", "creation_ts", "user_type", + "deactivated", ], allow_none=True, desc="get_user_by_id", diff --git a/tests/rest/admin/test_admin.py b/tests/rest/admin/test_admin.py index 325bd6a608..6ceb483aa8 100644 --- a/tests/rest/admin/test_admin.py +++ b/tests/rest/admin/test_admin.py @@ -13,14 +13,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -import hashlib -import hmac import json from mock import Mock import synapse.rest.admin -from synapse.api.constants import UserTypes from synapse.http.server import JsonResource from synapse.rest.admin import VersionServlet from synapse.rest.client.v1 import events, login, room @@ -47,341 +44,6 @@ class VersionTestCase(unittest.HomeserverTestCase): ) -class UserRegisterTestCase(unittest.HomeserverTestCase): - - servlets = [synapse.rest.admin.register_servlets_for_client_rest_resource] - - def make_homeserver(self, reactor, clock): - - self.url = "/_matrix/client/r0/admin/register" - - self.registration_handler = Mock() - self.identity_handler = Mock() - self.login_handler = Mock() - self.device_handler = Mock() - self.device_handler.check_device_registered = Mock(return_value="FAKE") - - self.datastore = Mock(return_value=Mock()) - self.datastore.get_current_state_deltas = Mock(return_value=(0, [])) - - self.secrets = Mock() - - self.hs = self.setup_test_homeserver() - - self.hs.config.registration_shared_secret = "shared" - - self.hs.get_media_repository = Mock() - self.hs.get_deactivate_account_handler = Mock() - - return self.hs - - def test_disabled(self): - """ - If there is no shared secret, registration through this method will be - prevented. - """ - self.hs.config.registration_shared_secret = None - - request, channel = self.make_request("POST", self.url, b"{}") - self.render(request) - - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) - self.assertEqual( - "Shared secret registration is not enabled", channel.json_body["error"] - ) - - def test_get_nonce(self): - """ - Calling GET on the endpoint will return a randomised nonce, using the - homeserver's secrets provider. - """ - secrets = Mock() - secrets.token_hex = Mock(return_value="abcd") - - self.hs.get_secrets = Mock(return_value=secrets) - - request, channel = self.make_request("GET", self.url) - self.render(request) - - self.assertEqual(channel.json_body, {"nonce": "abcd"}) - - def test_expired_nonce(self): - """ - Calling GET on the endpoint will return a randomised nonce, which will - only last for SALT_TIMEOUT (60s). - """ - request, channel = self.make_request("GET", self.url) - self.render(request) - nonce = channel.json_body["nonce"] - - # 59 seconds - self.reactor.advance(59) - - body = json.dumps({"nonce": nonce}) - request, channel = self.make_request("POST", self.url, body.encode("utf8")) - self.render(request) - - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) - self.assertEqual("username must be specified", channel.json_body["error"]) - - # 61 seconds - self.reactor.advance(2) - - request, channel = self.make_request("POST", self.url, body.encode("utf8")) - self.render(request) - - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) - self.assertEqual("unrecognised nonce", channel.json_body["error"]) - - def test_register_incorrect_nonce(self): - """ - Only the provided nonce can be used, as it's checked in the MAC. - """ - request, channel = self.make_request("GET", self.url) - self.render(request) - nonce = channel.json_body["nonce"] - - want_mac = hmac.new(key=b"shared", digestmod=hashlib.sha1) - want_mac.update(b"notthenonce\x00bob\x00abc123\x00admin") - want_mac = want_mac.hexdigest() - - body = json.dumps( - { - "nonce": nonce, - "username": "bob", - "password": "abc123", - "admin": True, - "mac": want_mac, - } - ) - request, channel = self.make_request("POST", self.url, body.encode("utf8")) - self.render(request) - - self.assertEqual(403, int(channel.result["code"]), msg=channel.result["body"]) - self.assertEqual("HMAC incorrect", channel.json_body["error"]) - - def test_register_correct_nonce(self): - """ - When the correct nonce is provided, and the right key is provided, the - user is registered. - """ - request, channel = self.make_request("GET", self.url) - self.render(request) - nonce = channel.json_body["nonce"] - - want_mac = hmac.new(key=b"shared", digestmod=hashlib.sha1) - want_mac.update( - nonce.encode("ascii") + b"\x00bob\x00abc123\x00admin\x00support" - ) - want_mac = want_mac.hexdigest() - - body = json.dumps( - { - "nonce": nonce, - "username": "bob", - "password": "abc123", - "admin": True, - "user_type": UserTypes.SUPPORT, - "mac": want_mac, - } - ) - request, channel = self.make_request("POST", self.url, body.encode("utf8")) - self.render(request) - - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) - self.assertEqual("@bob:test", channel.json_body["user_id"]) - - def test_nonce_reuse(self): - """ - A valid unrecognised nonce. - """ - request, channel = self.make_request("GET", self.url) - self.render(request) - nonce = channel.json_body["nonce"] - - want_mac = hmac.new(key=b"shared", digestmod=hashlib.sha1) - want_mac.update(nonce.encode("ascii") + b"\x00bob\x00abc123\x00admin") - want_mac = want_mac.hexdigest() - - body = json.dumps( - { - "nonce": nonce, - "username": "bob", - "password": "abc123", - "admin": True, - "mac": want_mac, - } - ) - request, channel = self.make_request("POST", self.url, body.encode("utf8")) - self.render(request) - - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) - self.assertEqual("@bob:test", channel.json_body["user_id"]) - - # Now, try and reuse it - request, channel = self.make_request("POST", self.url, body.encode("utf8")) - self.render(request) - - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) - self.assertEqual("unrecognised nonce", channel.json_body["error"]) - - def test_missing_parts(self): - """ - Synapse will complain if you don't give nonce, username, password, and - mac. Admin and user_types are optional. Additional checks are done for length - and type. - """ - - def nonce(): - request, channel = self.make_request("GET", self.url) - self.render(request) - return channel.json_body["nonce"] - - # - # Nonce check - # - - # Must be present - body = json.dumps({}) - request, channel = self.make_request("POST", self.url, body.encode("utf8")) - self.render(request) - - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) - self.assertEqual("nonce must be specified", channel.json_body["error"]) - - # - # Username checks - # - - # Must be present - body = json.dumps({"nonce": nonce()}) - request, channel = self.make_request("POST", self.url, body.encode("utf8")) - self.render(request) - - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) - self.assertEqual("username must be specified", channel.json_body["error"]) - - # Must be a string - body = json.dumps({"nonce": nonce(), "username": 1234}) - request, channel = self.make_request("POST", self.url, body.encode("utf8")) - self.render(request) - - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) - self.assertEqual("Invalid username", channel.json_body["error"]) - - # Must not have null bytes - body = json.dumps({"nonce": nonce(), "username": "abcd\u0000"}) - request, channel = self.make_request("POST", self.url, body.encode("utf8")) - self.render(request) - - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) - self.assertEqual("Invalid username", channel.json_body["error"]) - - # Must not have null bytes - body = json.dumps({"nonce": nonce(), "username": "a" * 1000}) - request, channel = self.make_request("POST", self.url, body.encode("utf8")) - self.render(request) - - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) - self.assertEqual("Invalid username", channel.json_body["error"]) - - # - # Password checks - # - - # Must be present - body = json.dumps({"nonce": nonce(), "username": "a"}) - request, channel = self.make_request("POST", self.url, body.encode("utf8")) - self.render(request) - - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) - self.assertEqual("password must be specified", channel.json_body["error"]) - - # Must be a string - body = json.dumps({"nonce": nonce(), "username": "a", "password": 1234}) - request, channel = self.make_request("POST", self.url, body.encode("utf8")) - self.render(request) - - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) - self.assertEqual("Invalid password", channel.json_body["error"]) - - # Must not have null bytes - body = json.dumps({"nonce": nonce(), "username": "a", "password": "abcd\u0000"}) - request, channel = self.make_request("POST", self.url, body.encode("utf8")) - self.render(request) - - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) - self.assertEqual("Invalid password", channel.json_body["error"]) - - # Super long - body = json.dumps({"nonce": nonce(), "username": "a", "password": "A" * 1000}) - request, channel = self.make_request("POST", self.url, body.encode("utf8")) - self.render(request) - - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) - self.assertEqual("Invalid password", channel.json_body["error"]) - - # - # user_type check - # - - # Invalid user_type - body = json.dumps( - { - "nonce": nonce(), - "username": "a", - "password": "1234", - "user_type": "invalid", - } - ) - request, channel = self.make_request("POST", self.url, body.encode("utf8")) - self.render(request) - - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) - self.assertEqual("Invalid user type", channel.json_body["error"]) - - -class UsersListTestCase(unittest.HomeserverTestCase): - - servlets = [ - synapse.rest.admin.register_servlets, - login.register_servlets, - ] - url = "/_synapse/admin/v2/users" - - def prepare(self, reactor, clock, hs): - self.admin_user = self.register_user("admin", "pass", admin=True) - self.admin_user_tok = self.login("admin", "pass") - - self.register_user("user1", "pass1", admin=False) - self.register_user("user2", "pass2", admin=False) - - def test_no_auth(self): - """ - Try to list users without authentication. - """ - request, channel = self.make_request("GET", self.url, b"{}") - self.render(request) - - self.assertEqual(401, int(channel.result["code"]), msg=channel.result["body"]) - self.assertEqual("M_MISSING_TOKEN", channel.json_body["errcode"]) - - def test_all_users(self): - """ - List all users, including deactivated users. - """ - request, channel = self.make_request( - "GET", - self.url + "?deactivated=true", - b"{}", - access_token=self.admin_user_tok, - ) - self.render(request) - - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) - self.assertEqual(3, len(channel.json_body["users"])) - - class ShutdownRoomTestCase(unittest.HomeserverTestCase): servlets = [ synapse.rest.admin.register_servlets_for_client_rest_resource, diff --git a/tests/rest/admin/test_user.py b/tests/rest/admin/test_user.py new file mode 100644 index 0000000000..7352d609e6 --- /dev/null +++ b/tests/rest/admin/test_user.py @@ -0,0 +1,465 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 New Vector Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import hashlib +import hmac +import json + +from mock import Mock + +import synapse.rest.admin +from synapse.api.constants import UserTypes +from synapse.rest.client.v1 import login + +from tests import unittest + + +class UserRegisterTestCase(unittest.HomeserverTestCase): + + servlets = [synapse.rest.admin.register_servlets_for_client_rest_resource] + + def make_homeserver(self, reactor, clock): + + self.url = "/_matrix/client/r0/admin/register" + + self.registration_handler = Mock() + self.identity_handler = Mock() + self.login_handler = Mock() + self.device_handler = Mock() + self.device_handler.check_device_registered = Mock(return_value="FAKE") + + self.datastore = Mock(return_value=Mock()) + self.datastore.get_current_state_deltas = Mock(return_value=(0, [])) + + self.secrets = Mock() + + self.hs = self.setup_test_homeserver() + + self.hs.config.registration_shared_secret = "shared" + + self.hs.get_media_repository = Mock() + self.hs.get_deactivate_account_handler = Mock() + + return self.hs + + def test_disabled(self): + """ + If there is no shared secret, registration through this method will be + prevented. + """ + self.hs.config.registration_shared_secret = None + + request, channel = self.make_request("POST", self.url, b"{}") + self.render(request) + + self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual( + "Shared secret registration is not enabled", channel.json_body["error"] + ) + + def test_get_nonce(self): + """ + Calling GET on the endpoint will return a randomised nonce, using the + homeserver's secrets provider. + """ + secrets = Mock() + secrets.token_hex = Mock(return_value="abcd") + + self.hs.get_secrets = Mock(return_value=secrets) + + request, channel = self.make_request("GET", self.url) + self.render(request) + + self.assertEqual(channel.json_body, {"nonce": "abcd"}) + + def test_expired_nonce(self): + """ + Calling GET on the endpoint will return a randomised nonce, which will + only last for SALT_TIMEOUT (60s). + """ + request, channel = self.make_request("GET", self.url) + self.render(request) + nonce = channel.json_body["nonce"] + + # 59 seconds + self.reactor.advance(59) + + body = json.dumps({"nonce": nonce}) + request, channel = self.make_request("POST", self.url, body.encode("utf8")) + self.render(request) + + self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("username must be specified", channel.json_body["error"]) + + # 61 seconds + self.reactor.advance(2) + + request, channel = self.make_request("POST", self.url, body.encode("utf8")) + self.render(request) + + self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("unrecognised nonce", channel.json_body["error"]) + + def test_register_incorrect_nonce(self): + """ + Only the provided nonce can be used, as it's checked in the MAC. + """ + request, channel = self.make_request("GET", self.url) + self.render(request) + nonce = channel.json_body["nonce"] + + want_mac = hmac.new(key=b"shared", digestmod=hashlib.sha1) + want_mac.update(b"notthenonce\x00bob\x00abc123\x00admin") + want_mac = want_mac.hexdigest() + + body = json.dumps( + { + "nonce": nonce, + "username": "bob", + "password": "abc123", + "admin": True, + "mac": want_mac, + } + ) + request, channel = self.make_request("POST", self.url, body.encode("utf8")) + self.render(request) + + self.assertEqual(403, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("HMAC incorrect", channel.json_body["error"]) + + def test_register_correct_nonce(self): + """ + When the correct nonce is provided, and the right key is provided, the + user is registered. + """ + request, channel = self.make_request("GET", self.url) + self.render(request) + nonce = channel.json_body["nonce"] + + want_mac = hmac.new(key=b"shared", digestmod=hashlib.sha1) + want_mac.update( + nonce.encode("ascii") + b"\x00bob\x00abc123\x00admin\x00support" + ) + want_mac = want_mac.hexdigest() + + body = json.dumps( + { + "nonce": nonce, + "username": "bob", + "password": "abc123", + "admin": True, + "user_type": UserTypes.SUPPORT, + "mac": want_mac, + } + ) + request, channel = self.make_request("POST", self.url, body.encode("utf8")) + self.render(request) + + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("@bob:test", channel.json_body["user_id"]) + + def test_nonce_reuse(self): + """ + A valid unrecognised nonce. + """ + request, channel = self.make_request("GET", self.url) + self.render(request) + nonce = channel.json_body["nonce"] + + want_mac = hmac.new(key=b"shared", digestmod=hashlib.sha1) + want_mac.update(nonce.encode("ascii") + b"\x00bob\x00abc123\x00admin") + want_mac = want_mac.hexdigest() + + body = json.dumps( + { + "nonce": nonce, + "username": "bob", + "password": "abc123", + "admin": True, + "mac": want_mac, + } + ) + request, channel = self.make_request("POST", self.url, body.encode("utf8")) + self.render(request) + + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("@bob:test", channel.json_body["user_id"]) + + # Now, try and reuse it + request, channel = self.make_request("POST", self.url, body.encode("utf8")) + self.render(request) + + self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("unrecognised nonce", channel.json_body["error"]) + + def test_missing_parts(self): + """ + Synapse will complain if you don't give nonce, username, password, and + mac. Admin and user_types are optional. Additional checks are done for length + and type. + """ + + def nonce(): + request, channel = self.make_request("GET", self.url) + self.render(request) + return channel.json_body["nonce"] + + # + # Nonce check + # + + # Must be present + body = json.dumps({}) + request, channel = self.make_request("POST", self.url, body.encode("utf8")) + self.render(request) + + self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("nonce must be specified", channel.json_body["error"]) + + # + # Username checks + # + + # Must be present + body = json.dumps({"nonce": nonce()}) + request, channel = self.make_request("POST", self.url, body.encode("utf8")) + self.render(request) + + self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("username must be specified", channel.json_body["error"]) + + # Must be a string + body = json.dumps({"nonce": nonce(), "username": 1234}) + request, channel = self.make_request("POST", self.url, body.encode("utf8")) + self.render(request) + + self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("Invalid username", channel.json_body["error"]) + + # Must not have null bytes + body = json.dumps({"nonce": nonce(), "username": "abcd\u0000"}) + request, channel = self.make_request("POST", self.url, body.encode("utf8")) + self.render(request) + + self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("Invalid username", channel.json_body["error"]) + + # Must not have null bytes + body = json.dumps({"nonce": nonce(), "username": "a" * 1000}) + request, channel = self.make_request("POST", self.url, body.encode("utf8")) + self.render(request) + + self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("Invalid username", channel.json_body["error"]) + + # + # Password checks + # + + # Must be present + body = json.dumps({"nonce": nonce(), "username": "a"}) + request, channel = self.make_request("POST", self.url, body.encode("utf8")) + self.render(request) + + self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("password must be specified", channel.json_body["error"]) + + # Must be a string + body = json.dumps({"nonce": nonce(), "username": "a", "password": 1234}) + request, channel = self.make_request("POST", self.url, body.encode("utf8")) + self.render(request) + + self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("Invalid password", channel.json_body["error"]) + + # Must not have null bytes + body = json.dumps({"nonce": nonce(), "username": "a", "password": "abcd\u0000"}) + request, channel = self.make_request("POST", self.url, body.encode("utf8")) + self.render(request) + + self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("Invalid password", channel.json_body["error"]) + + # Super long + body = json.dumps({"nonce": nonce(), "username": "a", "password": "A" * 1000}) + request, channel = self.make_request("POST", self.url, body.encode("utf8")) + self.render(request) + + self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("Invalid password", channel.json_body["error"]) + + # + # user_type check + # + + # Invalid user_type + body = json.dumps( + { + "nonce": nonce(), + "username": "a", + "password": "1234", + "user_type": "invalid", + } + ) + request, channel = self.make_request("POST", self.url, body.encode("utf8")) + self.render(request) + + self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("Invalid user type", channel.json_body["error"]) + + +class UsersListTestCase(unittest.HomeserverTestCase): + + servlets = [ + synapse.rest.admin.register_servlets, + login.register_servlets, + ] + url = "/_synapse/admin/v2/users" + + def prepare(self, reactor, clock, hs): + self.admin_user = self.register_user("admin", "pass", admin=True) + self.admin_user_tok = self.login("admin", "pass") + + self.register_user("user1", "pass1", admin=False) + self.register_user("user2", "pass2", admin=False) + + def test_no_auth(self): + """ + Try to list users without authentication. + """ + request, channel = self.make_request("GET", self.url, b"{}") + self.render(request) + + self.assertEqual(401, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("M_MISSING_TOKEN", channel.json_body["errcode"]) + + def test_all_users(self): + """ + List all users, including deactivated users. + """ + request, channel = self.make_request( + "GET", + self.url + "?deactivated=true", + b"{}", + access_token=self.admin_user_tok, + ) + self.render(request) + + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(3, len(channel.json_body["users"])) + + +class UserRestTestCase(unittest.HomeserverTestCase): + + servlets = [ + synapse.rest.admin.register_servlets, + login.register_servlets, + ] + + def prepare(self, reactor, clock, hs): + self.store = hs.get_datastore() + + self.url = "/_synapse/admin/v2/users/@bob:test" + + self.admin_user = self.register_user("admin", "pass", admin=True) + self.admin_user_tok = self.login("admin", "pass") + + self.other_user = self.register_user("user", "pass") + self.other_user_token = self.login("user", "pass") + + def test_requester_is_no_admin(self): + """ + If the user is not a server admin, an error is returned. + """ + self.hs.config.registration_shared_secret = None + + request, channel = self.make_request( + "GET", self.url, access_token=self.other_user_token, + ) + self.render(request) + + self.assertEqual(403, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("You are not a server admin", channel.json_body["error"]) + + request, channel = self.make_request( + "PUT", self.url, access_token=self.other_user_token, content=b"{}", + ) + self.render(request) + + self.assertEqual(403, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("You are not a server admin", channel.json_body["error"]) + + def test_requester_is_admin(self): + """ + If the user is a server admin, a new user is created. + """ + self.hs.config.registration_shared_secret = None + + body = json.dumps({"password": "abc123", "admin": True}) + + # Create user + request, channel = self.make_request( + "PUT", + self.url, + access_token=self.admin_user_tok, + content=body.encode(encoding="utf_8"), + ) + self.render(request) + + self.assertEqual(201, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("@bob:test", channel.json_body["name"]) + self.assertEqual("bob", channel.json_body["displayname"]) + + # Get user + request, channel = self.make_request( + "GET", self.url, access_token=self.admin_user_tok, + ) + self.render(request) + + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("@bob:test", channel.json_body["name"]) + self.assertEqual("bob", channel.json_body["displayname"]) + self.assertEqual(1, channel.json_body["admin"]) + self.assertEqual(0, channel.json_body["is_guest"]) + self.assertEqual(0, channel.json_body["deactivated"]) + + # Modify user + body = json.dumps({"displayname": "foobar", "deactivated": True}) + + request, channel = self.make_request( + "PUT", + self.url, + access_token=self.admin_user_tok, + content=body.encode(encoding="utf_8"), + ) + self.render(request) + + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("@bob:test", channel.json_body["name"]) + self.assertEqual("foobar", channel.json_body["displayname"]) + self.assertEqual(True, channel.json_body["deactivated"]) + + # Get user + request, channel = self.make_request( + "GET", self.url, access_token=self.admin_user_tok, + ) + self.render(request) + + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("@bob:test", channel.json_body["name"]) + self.assertEqual("foobar", channel.json_body["displayname"]) + self.assertEqual(1, channel.json_body["admin"]) + self.assertEqual(0, channel.json_body["is_guest"]) + self.assertEqual(1, channel.json_body["deactivated"]) diff --git a/tests/storage/test_registration.py b/tests/storage/test_registration.py index ed5786865a..71a40a0a49 100644 --- a/tests/storage/test_registration.py +++ b/tests/storage/test_registration.py @@ -43,12 +43,14 @@ class RegistrationStoreTestCase(unittest.TestCase): # TODO(paul): Surely this field should be 'user_id', not 'name' "name": self.user_id, "password_hash": self.pwhash, + "admin": 0, "is_guest": 0, "consent_version": None, "consent_server_notice_sent": None, "appservice_id": None, "creation_ts": 1000, "user_type": None, + "deactivated": 0, }, (yield self.store.get_user_by_id(self.user_id)), ) From c2ba994dbb3f084c2c0cc8492e8a9456ec0f3747 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 9 Jan 2020 17:13:36 +0000 Subject: [PATCH 0800/1623] Add note about log_file no longer be accepted (#6674) --- CHANGES.md | 3 +++ UPGRADE.rst | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 690cbdaae9..d06b8c8ad3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,9 @@ Synapse 1.8.0 (2020-01-09) ========================== +**WARNING**: As of this release Synapse will refuse to start if the `log_file` config option is specified. Support for the option was removed in v1.3.0. + + Bugfixes -------- diff --git a/UPGRADE.rst b/UPGRADE.rst index d9020f2663..a0202932b1 100644 --- a/UPGRADE.rst +++ b/UPGRADE.rst @@ -75,6 +75,15 @@ for example: wget https://packages.matrix.org/debian/pool/main/m/matrix-synapse-py3/matrix-synapse-py3_1.3.0+stretch1_amd64.deb dpkg -i matrix-synapse-py3_1.3.0+stretch1_amd64.deb + +Upgrading to v1.8.0 +=================== + +Specifying a ``log_file`` config option will now cause Synapse to refuse to +start, and should be replaced by with the ``log_config`` option. Support for +the ``log_file`` option was removed in v1.3.0 and has since had no effect. + + Upgrading to v1.7.0 =================== From e97d1cf0014668b9d4883d4175b783088444b24b Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 9 Jan 2020 17:21:30 +0000 Subject: [PATCH 0801/1623] Modify check_database to take a connection rather than a cursor We might not need the cursor at all. --- scripts/synapse_port_db | 25 +++++++------------------ synapse/storage/data_stores/__init__.py | 2 +- synapse/storage/engines/postgres.py | 17 +++++++++-------- synapse/storage/engines/sqlite.py | 2 +- 4 files changed, 18 insertions(+), 28 deletions(-) diff --git a/scripts/synapse_port_db b/scripts/synapse_port_db index cb77314f1e..a3dafaffc9 100755 --- a/scripts/synapse_port_db +++ b/scripts/synapse_port_db @@ -447,15 +447,6 @@ class Porter(object): else: return - def setup_db(self, db_config: DatabaseConnectionConfig, engine): - db_conn = make_conn(db_config, engine) - prepare_database(db_conn, engine, config=None) - - db_conn.commit() - - return db_conn - - @defer.inlineCallbacks def build_db_store(self, db_config: DatabaseConnectionConfig): """Builds and returns a database store using the provided configuration. @@ -468,16 +459,14 @@ class Porter(object): self.progress.set_state("Preparing %s" % db_config.config["name"]) engine = create_engine(db_config.config) - conn = self.setup_db(db_config, engine) hs = MockHomeserver(self.hs_config) - store = Store(Database(hs, db_config, engine), conn, hs) - - yield store.db.runInteraction( - "%s_engine.check_database" % db_config.config["name"], - engine.check_database, - ) + with make_conn(db_config, engine) as db_conn: + engine.check_database(db_conn) + prepare_database(db_conn, engine, config=None) + store = Store(Database(hs, db_config, engine), db_conn, hs) + db_conn.commit() return store @@ -502,7 +491,7 @@ class Porter(object): @defer.inlineCallbacks def run(self): try: - self.sqlite_store = yield self.build_db_store( + self.sqlite_store = self.build_db_store( DatabaseConnectionConfig("master-sqlite", self.sqlite_config) ) @@ -518,7 +507,7 @@ class Porter(object): ) defer.returnValue(None) - self.postgres_store = yield self.build_db_store( + self.postgres_store = self.build_db_store( self.hs_config.get_single_database() ) diff --git a/synapse/storage/data_stores/__init__.py b/synapse/storage/data_stores/__init__.py index 092e803799..e1d03429ca 100644 --- a/synapse/storage/data_stores/__init__.py +++ b/synapse/storage/data_stores/__init__.py @@ -47,7 +47,7 @@ class DataStores(object): with make_conn(database_config, engine) as db_conn: logger.info("Preparing database %r...", db_name) - engine.check_database(db_conn.cursor()) + engine.check_database(db_conn) prepare_database( db_conn, engine, hs.config, data_stores=database_config.data_stores, ) diff --git a/synapse/storage/engines/postgres.py b/synapse/storage/engines/postgres.py index b7c4eda338..ba19785fd7 100644 --- a/synapse/storage/engines/postgres.py +++ b/synapse/storage/engines/postgres.py @@ -32,14 +32,15 @@ class PostgresEngine(object): self.synchronous_commit = database_config.get("synchronous_commit", True) self._version = None # unknown as yet - def check_database(self, txn): - txn.execute("SHOW SERVER_ENCODING") - rows = txn.fetchall() - if rows and rows[0][0] != "UTF8": - raise IncorrectDatabaseSetup( - "Database has incorrect encoding: '%s' instead of 'UTF8'\n" - "See docs/postgres.rst for more information." % (rows[0][0],) - ) + def check_database(self, db_conn): + with db_conn.cursor() as txn: + txn.execute("SHOW SERVER_ENCODING") + rows = txn.fetchall() + if rows and rows[0][0] != "UTF8": + raise IncorrectDatabaseSetup( + "Database has incorrect encoding: '%s' instead of 'UTF8'\n" + "See docs/postgres.rst for more information." % (rows[0][0],) + ) def convert_param_style(self, sql): return sql.replace("?", "%s") diff --git a/synapse/storage/engines/sqlite.py b/synapse/storage/engines/sqlite.py index df039a072d..3b3c13360b 100644 --- a/synapse/storage/engines/sqlite.py +++ b/synapse/storage/engines/sqlite.py @@ -53,7 +53,7 @@ class Sqlite3Engine(object): """ return False - def check_database(self, txn): + def check_database(self, db_conn): pass def convert_param_style(self, sql): From e48ba84e0bfe081814941b74e610ddcd168a3ce8 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 9 Jan 2020 17:33:41 +0000 Subject: [PATCH 0802/1623] Check postgres version in check_database this saves doing it on each connection, and will allow us to pass extra options in. --- synapse/storage/engines/postgres.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/synapse/storage/engines/postgres.py b/synapse/storage/engines/postgres.py index ba19785fd7..2a285e018c 100644 --- a/synapse/storage/engines/postgres.py +++ b/synapse/storage/engines/postgres.py @@ -33,6 +33,16 @@ class PostgresEngine(object): self._version = None # unknown as yet def check_database(self, db_conn): + # Get the version of PostgreSQL that we're using. As per the psycopg2 + # docs: The number is formed by converting the major, minor, and + # revision numbers into two-decimal-digit numbers and appending them + # together. For example, version 8.1.5 will be returned as 80105 + self._version = db_conn.server_version + + # Are we on a supported PostgreSQL version? + if self._version < 90500: + raise RuntimeError("Synapse requires PostgreSQL 9.5+ or above.") + with db_conn.cursor() as txn: txn.execute("SHOW SERVER_ENCODING") rows = txn.fetchall() @@ -46,17 +56,6 @@ class PostgresEngine(object): return sql.replace("?", "%s") def on_new_connection(self, db_conn): - - # Get the version of PostgreSQL that we're using. As per the psycopg2 - # docs: The number is formed by converting the major, minor, and - # revision numbers into two-decimal-digit numbers and appending them - # together. For example, version 8.1.5 will be returned as 80105 - self._version = db_conn.server_version - - # Are we on a supported PostgreSQL version? - if self._version < 90500: - raise RuntimeError("Synapse requires PostgreSQL 9.5+ or above.") - db_conn.set_isolation_level( self.module.extensions.ISOLATION_LEVEL_REPEATABLE_READ ) @@ -120,8 +119,8 @@ class PostgresEngine(object): Returns: string """ - # note that this is a bit of a hack because it relies on on_new_connection - # having been called at least once. Still, that should be a safe bet here. + # note that this is a bit of a hack because it relies on check_database + # having been called. Still, that should be a safe bet here. numver = self._version assert numver is not None From bf468211805900e767b6b07a2bfa6046f70efb7a Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 9 Jan 2020 17:46:52 +0000 Subject: [PATCH 0803/1623] Refuse to start if sqlite is older than 3.11.0 --- scripts/synapse_port_db | 16 ++++++++++++---- synapse/storage/engines/postgres.py | 4 ++-- synapse/storage/engines/sqlite.py | 7 +++++-- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/scripts/synapse_port_db b/scripts/synapse_port_db index a3dafaffc9..f135c8bc54 100755 --- a/scripts/synapse_port_db +++ b/scripts/synapse_port_db @@ -447,11 +447,15 @@ class Porter(object): else: return - def build_db_store(self, db_config: DatabaseConnectionConfig): + def build_db_store( + self, db_config: DatabaseConnectionConfig, allow_outdated_version: bool = False, + ): """Builds and returns a database store using the provided configuration. Args: - config: The database configuration + db_config: The database configuration + allow_outdated_version: True to suppress errors about the database server + version being too old to run a complete synapse Returns: The built Store object. @@ -463,7 +467,9 @@ class Porter(object): hs = MockHomeserver(self.hs_config) with make_conn(db_config, engine) as db_conn: - engine.check_database(db_conn) + engine.check_database( + db_conn, allow_outdated_version=allow_outdated_version + ) prepare_database(db_conn, engine, config=None) store = Store(Database(hs, db_config, engine), db_conn, hs) db_conn.commit() @@ -491,8 +497,10 @@ class Porter(object): @defer.inlineCallbacks def run(self): try: + # we allow people to port away from outdated versions of sqlite. self.sqlite_store = self.build_db_store( - DatabaseConnectionConfig("master-sqlite", self.sqlite_config) + DatabaseConnectionConfig("master-sqlite", self.sqlite_config), + allow_outdated_version=True, ) # Check if all background updates are done, abort if not. diff --git a/synapse/storage/engines/postgres.py b/synapse/storage/engines/postgres.py index 2a285e018c..c84cb452b0 100644 --- a/synapse/storage/engines/postgres.py +++ b/synapse/storage/engines/postgres.py @@ -32,7 +32,7 @@ class PostgresEngine(object): self.synchronous_commit = database_config.get("synchronous_commit", True) self._version = None # unknown as yet - def check_database(self, db_conn): + def check_database(self, db_conn, allow_outdated_version: bool = False): # Get the version of PostgreSQL that we're using. As per the psycopg2 # docs: The number is formed by converting the major, minor, and # revision numbers into two-decimal-digit numbers and appending them @@ -40,7 +40,7 @@ class PostgresEngine(object): self._version = db_conn.server_version # Are we on a supported PostgreSQL version? - if self._version < 90500: + if not allow_outdated_version and self._version < 90500: raise RuntimeError("Synapse requires PostgreSQL 9.5+ or above.") with db_conn.cursor() as txn: diff --git a/synapse/storage/engines/sqlite.py b/synapse/storage/engines/sqlite.py index 3b3c13360b..cbf52f5191 100644 --- a/synapse/storage/engines/sqlite.py +++ b/synapse/storage/engines/sqlite.py @@ -53,8 +53,11 @@ class Sqlite3Engine(object): """ return False - def check_database(self, db_conn): - pass + def check_database(self, db_conn, allow_outdated_version: bool = False): + if not allow_outdated_version: + version = self.module.sqlite_version_info + if version < (3, 11, 0): + raise RuntimeError("Synapse requires sqlite 3.11 or above.") def convert_param_style(self, sql): return sql From c3843fd075c5c20f800837afce534de352517db6 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 9 Jan 2020 17:52:12 +0000 Subject: [PATCH 0804/1623] changelog --- changelog.d/6675.removal | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6675.removal diff --git a/changelog.d/6675.removal b/changelog.d/6675.removal new file mode 100644 index 0000000000..95df9a2d83 --- /dev/null +++ b/changelog.d/6675.removal @@ -0,0 +1 @@ +Synapse no longer supports versions of SQLite before 3.11, and will refuse to start when configured to use an older version. Administrators are recommended to migrate their database to Postgres (see instructions [here](docs/postgres.md)). From 937dea42e72c982ab532a2b558f0540e5d5f4f67 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 9 Jan 2020 18:01:08 +0000 Subject: [PATCH 0805/1623] update install notes for CentOS --- INSTALL.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/INSTALL.md b/INSTALL.md index 9da2e3c734..d25fcf0753 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -133,6 +133,11 @@ sudo yum install libtiff-devel libjpeg-devel libzip-devel freetype-devel \ sudo yum groupinstall "Development Tools" ``` +Note that Synapse does not support versions of SQLite before 3.11, and CentOS 7 +uses SQLite 3.7. You may be able to work around this by installing a more +recent SQLite version, but it is recommended that you instead use a Postgres +database: see [docs/postgres.md](docs/postgres.md). + #### macOS Installing prerequisites on macOS: From 473d3801b6631bb83a386626c099711aef90f8db Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Sun, 12 Jan 2020 21:31:44 +0000 Subject: [PATCH 0806/1623] Cleanups and additions to the module API Add some useful things, such as error types and logcontext handling, to the API. Make `hs` a private member to dissuade people from using it (hopefully they aren't already). Add a couple of new methods (`record_user_external_id` and `generate_short_term_login_token`). --- synapse/module_api/__init__.py | 47 +++++++++++++++++++++++++++++----- synapse/module_api/errors.py | 18 +++++++++++++ 2 files changed, 59 insertions(+), 6 deletions(-) create mode 100644 synapse/module_api/errors.py diff --git a/synapse/module_api/__init__.py b/synapse/module_api/__init__.py index 305b9b0178..d680ee95e1 100644 --- a/synapse/module_api/__init__.py +++ b/synapse/module_api/__init__.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # Copyright 2017 New Vector Ltd +# Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,18 +17,26 @@ import logging from twisted.internet import defer +from synapse.logging.context import make_deferred_yieldable, run_in_background from synapse.types import UserID +""" +This package defines the 'stable' API which can be used by extension modules which +are loaded into Synapse. +""" + +__all__ = ["errors", "make_deferred_yieldable", "run_in_background", "ModuleApi"] + logger = logging.getLogger(__name__) class ModuleApi(object): - """A proxy object that gets passed to password auth providers so they + """A proxy object that gets passed to various plugin modules so they can register new users etc if necessary. """ def __init__(self, hs, auth_handler): - self.hs = hs + self._hs = hs self._store = hs.get_datastore() self._auth = hs.get_auth() @@ -64,7 +73,7 @@ class ModuleApi(object): """ if username.startswith("@"): return username - return UserID(username, self.hs.hostname).to_string() + return UserID(username, self._hs.hostname).to_string() def check_user_exists(self, user_id): """Check if user exists. @@ -111,10 +120,14 @@ class ModuleApi(object): displayname (str|None): The displayname of the new user. emails (List[str]): Emails to bind to the new user. + Raises: + SynapseError if there is an error performing the registration. Check the + 'errcode' property for more information on the reason for failure + Returns: Deferred[str]: user_id """ - return self.hs.get_registration_handler().register_user( + return self._hs.get_registration_handler().register_user( localpart=localpart, default_display_name=displayname, bind_emails=emails ) @@ -131,12 +144,34 @@ class ModuleApi(object): Returns: defer.Deferred[tuple[str, str]]: Tuple of device ID and access token """ - return self.hs.get_registration_handler().register_device( + return self._hs.get_registration_handler().register_device( user_id=user_id, device_id=device_id, initial_display_name=initial_display_name, ) + def record_user_external_id( + self, auth_provider_id: str, remote_user_id: str, registered_user_id: str + ) -> defer.Deferred: + """Record a mapping from an external user id to a mxid + + Args: + auth_provider: identifier for the remote auth provider + external_id: id on that system + user_id: complete mxid that it is mapped to + """ + return self._store.record_user_external_id( + auth_provider_id, remote_user_id, registered_user_id + ) + + def generate_short_term_login_token( + self, user_id: str, duration_in_ms: int = (2 * 60 * 1000) + ) -> str: + """Generate a login token suitable for m.login.token authentication""" + return self._hs.get_macaroon_generator().generate_short_term_login_token( + user_id, duration_in_ms + ) + @defer.inlineCallbacks def invalidate_access_token(self, access_token): """Invalidate an access token for a user @@ -157,7 +192,7 @@ class ModuleApi(object): user_id = user_info["user"].to_string() if device_id: # delete the device, which will also delete its access tokens - yield self.hs.get_device_handler().delete_device(user_id, device_id) + yield self._hs.get_device_handler().delete_device(user_id, device_id) else: # no associated device. Just delete the access token. yield self._auth_handler.delete_access_token(access_token) diff --git a/synapse/module_api/errors.py b/synapse/module_api/errors.py new file mode 100644 index 0000000000..b15441772c --- /dev/null +++ b/synapse/module_api/errors.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Exception types which are exposed as part of the stable module API""" + +from synapse.api.errors import RedirectException, SynapseError # noqa: F401 From 01243b98e1f6c43efb988bb1f774e72079ab270f Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Sun, 12 Jan 2020 19:10:16 +0000 Subject: [PATCH 0807/1623] Handle `config` not being set for synapse plugin modules Some modules don't need any config, so having to define a `config` property just to keep the loader happy is a bit annoying. --- synapse/util/module_loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/util/module_loader.py b/synapse/util/module_loader.py index 2705cbe5f8..bb62db4637 100644 --- a/synapse/util/module_loader.py +++ b/synapse/util/module_loader.py @@ -34,7 +34,7 @@ def load_module(provider): provider_class = getattr(module, clz) try: - provider_config = provider_class.parse_config(provider["config"]) + provider_config = provider_class.parse_config(provider.get("config")) except Exception as e: raise ConfigError("Failed to parse config for %r: %r" % (provider["module"], e)) From 96ed33739a000ea539a4e7840bc99ac8a972c500 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Sun, 12 Jan 2020 21:36:10 +0000 Subject: [PATCH 0808/1623] changelog --- changelog.d/6688.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6688.misc diff --git a/changelog.d/6688.misc b/changelog.d/6688.misc new file mode 100644 index 0000000000..2a9f28ce5c --- /dev/null +++ b/changelog.d/6688.misc @@ -0,0 +1 @@ +Updates and extensions to the module API. \ No newline at end of file From 47e63cc67a059fdc28f01297740b1071c0c6ab5c Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Sat, 11 Jan 2020 11:48:43 +0000 Subject: [PATCH 0809/1623] Pass the module_api into the SamlMappingProvider ... for consistency with other modules, and because we'll need it sooner or later and it will be a pain to introduce later. --- synapse/handlers/saml_handler.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/synapse/handlers/saml_handler.py b/synapse/handlers/saml_handler.py index 0082f85c26..96f3d016fe 100644 --- a/synapse/handlers/saml_handler.py +++ b/synapse/handlers/saml_handler.py @@ -24,6 +24,7 @@ from saml2.client import Saml2Client from synapse.api.errors import SynapseError from synapse.config import ConfigError from synapse.http.servlet import parse_string +from synapse.module_api import ModuleApi from synapse.rest.client.v1.login import SSOAuthHandler from synapse.types import ( UserID, @@ -59,7 +60,8 @@ class SamlHandler: # plugin to do custom mapping from saml response to mxid self._user_mapping_provider = hs.config.saml2_user_mapping_provider_class( - hs.config.saml2_user_mapping_provider_config + hs.config.saml2_user_mapping_provider_config, + ModuleApi(hs, hs.get_auth_handler()), ) # identifier for the external_ids table @@ -265,11 +267,12 @@ class SamlConfig(object): class DefaultSamlMappingProvider(object): __version__ = "0.0.1" - def __init__(self, parsed_config: SamlConfig): + def __init__(self, parsed_config: SamlConfig, module_api: ModuleApi): """The default SAML user mapping provider Args: parsed_config: Module configuration + module_api: module api proxy """ self._mxid_source_attribute = parsed_config.mxid_source_attribute self._mxid_mapper = parsed_config.mxid_mapper From dc69a1cf432e8be2d37220e641345a70f3c692fc Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Sat, 11 Jan 2020 01:01:53 +0000 Subject: [PATCH 0810/1623] Pass client redirect URL into SAML mapping providers --- synapse/handlers/saml_handler.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/synapse/handlers/saml_handler.py b/synapse/handlers/saml_handler.py index 96f3d016fe..107f97032b 100644 --- a/synapse/handlers/saml_handler.py +++ b/synapse/handlers/saml_handler.py @@ -114,10 +114,10 @@ class SamlHandler: # the dict. self.expire_sessions() - user_id = await self._map_saml_response_to_user(resp_bytes) + user_id = await self._map_saml_response_to_user(resp_bytes, relay_state) self._sso_auth_handler.complete_sso_login(user_id, request, relay_state) - async def _map_saml_response_to_user(self, resp_bytes): + async def _map_saml_response_to_user(self, resp_bytes, client_redirect_url): try: saml2_auth = self._saml_client.parse_authn_request_response( resp_bytes, @@ -185,7 +185,7 @@ class SamlHandler: # Map saml response to user attributes using the configured mapping provider for i in range(1000): attribute_dict = self._user_mapping_provider.saml_response_to_user_attributes( - saml2_auth, i + saml2_auth, i, client_redirect_url=client_redirect_url, ) logger.debug( @@ -218,6 +218,8 @@ class SamlHandler: 500, "Unable to generate a Matrix ID from the SAML response" ) + logger.info("Mapped SAML user to local part %s", localpart) + registered_user_id = await self._registration_handler.register_user( localpart=localpart, default_display_name=displayname ) @@ -278,7 +280,10 @@ class DefaultSamlMappingProvider(object): self._mxid_mapper = parsed_config.mxid_mapper def saml_response_to_user_attributes( - self, saml_response: saml2.response.AuthnResponse, failures: int = 0, + self, + saml_response: saml2.response.AuthnResponse, + failures: int, + client_redirect_url: str, ) -> dict: """Maps some text from a SAML response to attributes of a new user @@ -288,6 +293,8 @@ class DefaultSamlMappingProvider(object): failures: How many times a call to this function with this saml_response has resulted in a failure + client_redirect_url: where the client wants to redirect to + Returns: dict: A dict containing new user attributes. Possible keys: * mxid_localpart (str): Required. The localpart of the user's mxid From d56e95ea8ba316f1fddfe33fa080d5fd0b19c008 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Sun, 12 Jan 2020 21:42:15 +0000 Subject: [PATCH 0811/1623] changelog --- changelog.d/6689.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6689.misc diff --git a/changelog.d/6689.misc b/changelog.d/6689.misc new file mode 100644 index 0000000000..17f15e73a8 --- /dev/null +++ b/changelog.d/6689.misc @@ -0,0 +1 @@ +Updates to the SAML mapping provider API. From da4e52544e326b707af6168a63d65eace34d8e9c Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Sat, 11 Jan 2020 13:00:24 +0000 Subject: [PATCH 0812/1623] comment for run_in_background --- synapse/logging/context.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/synapse/logging/context.py b/synapse/logging/context.py index 33b322209d..1b940842f6 100644 --- a/synapse/logging/context.py +++ b/synapse/logging/context.py @@ -571,6 +571,9 @@ def run_in_background(f, *args, **kwargs): yield or await on (for instance because you want to pass it to deferred.gatherResults()). + If f returns a Coroutine object, it will be wrapped into a Deferred (which will have + the side effect of executing the coroutine). + Note that if you completely discard the result, you should make sure that `f` doesn't raise any deferred exceptions, otherwise a scary-looking CRITICAL error about an unhandled error will be logged without much From feee8199734c9d8a18fa0be12fc5ec09ae140a3a Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 13 Jan 2020 12:41:51 +0000 Subject: [PATCH 0813/1623] Fix exceptions on requests for non-ascii urls (#6682) Fixes #6402 --- changelog.d/6682.bugfix | 2 ++ synapse/http/site.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 changelog.d/6682.bugfix diff --git a/changelog.d/6682.bugfix b/changelog.d/6682.bugfix new file mode 100644 index 0000000000..d48ea31477 --- /dev/null +++ b/changelog.d/6682.bugfix @@ -0,0 +1,2 @@ +Fix "CRITICAL" errors being logged when a request is received for a uri containing non-ascii characters. + diff --git a/synapse/http/site.py b/synapse/http/site.py index 9f2d035fa0..911251c0bc 100644 --- a/synapse/http/site.py +++ b/synapse/http/site.py @@ -88,7 +88,7 @@ class SynapseRequest(Request): def get_redacted_uri(self): uri = self.uri if isinstance(uri, bytes): - uri = self.uri.decode("ascii") + uri = self.uri.decode("ascii", errors="replace") return redact_uri(uri) def get_method(self): From 8039685051c08354b64890abb2522f2535c784b8 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 13 Jan 2020 12:42:44 +0000 Subject: [PATCH 0814/1623] Allow additional_resources to implement Resource directly (#6686) AdditionalResource really doesn't add any value, and it gets in the way for resources which want to support child resources or the like. So, if the resource object already implements the IResource interface, don't bother wrapping it. --- changelog.d/6686.misc | 1 + synapse/app/homeserver.py | 13 +++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 changelog.d/6686.misc diff --git a/changelog.d/6686.misc b/changelog.d/6686.misc new file mode 100644 index 0000000000..4070f2e563 --- /dev/null +++ b/changelog.d/6686.misc @@ -0,0 +1 @@ +Allow additional_resources to implement IResource directly. diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py index e5b44a5eed..c2a334a2b0 100644 --- a/synapse/app/homeserver.py +++ b/synapse/app/homeserver.py @@ -31,7 +31,7 @@ from prometheus_client import Gauge from twisted.application import service from twisted.internet import defer, reactor from twisted.python.failure import Failure -from twisted.web.resource import EncodingResourceWrapper, NoResource +from twisted.web.resource import EncodingResourceWrapper, IResource, NoResource from twisted.web.server import GzipEncoderFactory from twisted.web.static import File @@ -109,7 +109,16 @@ class SynapseHomeServer(HomeServer): for path, resmodule in additional_resources.items(): handler_cls, config = load_module(resmodule) handler = handler_cls(config, module_api) - resources[path] = AdditionalResource(self, handler.handle_request) + if IResource.providedBy(handler): + resource = handler + elif hasattr(handler, "handle_request"): + resource = AdditionalResource(self, handler.handle_request) + else: + raise ConfigError( + "additional_resource %s does not implement a known interface" + % (resmodule["module"],) + ) + resources[path] = resource # try to find something useful to redirect '/' to if WEB_CLIENT_PREFIX in resources: From 2d07c73777e837213f1c3c85b9cb446aac8b6170 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 13 Jan 2020 12:47:30 +0000 Subject: [PATCH 0815/1623] Don't assign numeric IDs for empty usernames (#6690) Fix a bug where we would assign a numeric userid if somebody tried registering with an empty username --- changelog.d/6690.bugfix | 1 + synapse/handlers/register.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/6690.bugfix diff --git a/changelog.d/6690.bugfix b/changelog.d/6690.bugfix new file mode 100644 index 0000000000..30ce1dc9f7 --- /dev/null +++ b/changelog.d/6690.bugfix @@ -0,0 +1 @@ +Fix a bug where we would assign a numeric userid if somebody tried registering with an empty username. diff --git a/synapse/handlers/register.py b/synapse/handlers/register.py index 8a7d965feb..885da82985 100644 --- a/synapse/handlers/register.py +++ b/synapse/handlers/register.py @@ -174,7 +174,7 @@ class RegistrationHandler(BaseHandler): if password: password_hash = yield self._auth_handler.hash(password) - if localpart: + if localpart is not None: yield self.check_username(localpart, guest_access_token=guest_access_token) was_guest = guest_access_token is not None From 326c893d24e0f03b62bd9d1136a335f329bb8528 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 13 Jan 2020 12:48:22 +0000 Subject: [PATCH 0816/1623] Kill off RegistrationError (#6691) This is pretty pointless. Let's just use SynapseError. --- changelog.d/6691.misc | 1 + synapse/api/errors.py | 6 ------ synapse/handlers/register.py | 12 +++--------- tests/handlers/test_register.py | 2 -- 4 files changed, 4 insertions(+), 17 deletions(-) create mode 100644 changelog.d/6691.misc diff --git a/changelog.d/6691.misc b/changelog.d/6691.misc new file mode 100644 index 0000000000..104e9ce648 --- /dev/null +++ b/changelog.d/6691.misc @@ -0,0 +1 @@ +Remove redundant RegistrationError class. diff --git a/synapse/api/errors.py b/synapse/api/errors.py index 5853a54c95..9e9844b47c 100644 --- a/synapse/api/errors.py +++ b/synapse/api/errors.py @@ -158,12 +158,6 @@ class UserDeactivatedError(SynapseError): ) -class RegistrationError(SynapseError): - """An error raised when a registration event fails.""" - - pass - - class FederationDeniedError(SynapseError): """An error raised when the server tries to federate with a server which is not on its federation whitelist. diff --git a/synapse/handlers/register.py b/synapse/handlers/register.py index 885da82985..7ffc194f0c 100644 --- a/synapse/handlers/register.py +++ b/synapse/handlers/register.py @@ -20,13 +20,7 @@ from twisted.internet import defer from synapse import types from synapse.api.constants import MAX_USERID_LENGTH, LoginType -from synapse.api.errors import ( - AuthError, - Codes, - ConsentNotGivenError, - RegistrationError, - SynapseError, -) +from synapse.api.errors import AuthError, Codes, ConsentNotGivenError, SynapseError from synapse.config.server import is_threepid_reserved from synapse.http.servlet import assert_params_in_dict from synapse.replication.http.login import RegisterDeviceReplicationServlet @@ -165,7 +159,7 @@ class RegistrationHandler(BaseHandler): Returns: Deferred[str]: user_id Raises: - RegistrationError if there was a problem registering. + SynapseError if there was a problem registering. """ yield self.check_registration_ratelimit(address) @@ -182,7 +176,7 @@ class RegistrationHandler(BaseHandler): if not was_guest: try: int(localpart) - raise RegistrationError( + raise SynapseError( 400, "Numeric user IDs are reserved for guest users." ) except ValueError: diff --git a/tests/handlers/test_register.py b/tests/handlers/test_register.py index 1e9ba3a201..e2915eb7b1 100644 --- a/tests/handlers/test_register.py +++ b/tests/handlers/test_register.py @@ -269,8 +269,6 @@ class RegistrationTestCase(unittest.HomeserverTestCase): one will be randomly generated. Returns: A tuple of (user_id, access_token). - Raises: - RegistrationError if there was a problem registering. """ if localpart is None: raise SynapseError(400, "Request must include user id") From 47f4f493f0886af5c9aad5c78885bb6869018dda Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 13 Jan 2020 15:32:02 +0000 Subject: [PATCH 0817/1623] Document more supported endpoints for workers (#6698) --- changelog.d/6698.doc | 1 + docs/workers.md | 4 ++++ 2 files changed, 5 insertions(+) create mode 100644 changelog.d/6698.doc diff --git a/changelog.d/6698.doc b/changelog.d/6698.doc new file mode 100644 index 0000000000..5aba51252d --- /dev/null +++ b/changelog.d/6698.doc @@ -0,0 +1 @@ +Add more endpoints to the documentation for Synapse workers. diff --git a/docs/workers.md b/docs/workers.md index 1b5d94f5eb..f4283aeb05 100644 --- a/docs/workers.md +++ b/docs/workers.md @@ -168,8 +168,11 @@ endpoints matching the following regular expressions: ^/_matrix/federation/v1/make_join/ ^/_matrix/federation/v1/make_leave/ ^/_matrix/federation/v1/send_join/ + ^/_matrix/federation/v2/send_join/ ^/_matrix/federation/v1/send_leave/ + ^/_matrix/federation/v2/send_leave/ ^/_matrix/federation/v1/invite/ + ^/_matrix/federation/v2/invite/ ^/_matrix/federation/v1/query_auth/ ^/_matrix/federation/v1/event_auth/ ^/_matrix/federation/v1/exchange_third_party_invite/ @@ -288,6 +291,7 @@ file. For example: Handles some event creation. It can handle REST endpoints matching: ^/_matrix/client/(api/v1|r0|unstable)/rooms/.*/send + ^/_matrix/client/(api/v1|r0|unstable)/rooms/.*/state/ ^/_matrix/client/(api/v1|r0|unstable)/rooms/.*/(join|invite|leave|ban|unban|kick)$ ^/_matrix/client/(api/v1|r0|unstable)/join/ ^/_matrix/client/(api/v1|r0|unstable)/profile/ From 1177d3f3a33bd3ae1eef46fba360d319598359ad Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Mon, 13 Jan 2020 18:10:43 +0000 Subject: [PATCH 0818/1623] Quarantine media by ID or user ID (#6681) --- changelog.d/6681.feature | 1 + docs/admin_api/media_admin_api.md | 84 +++++- docs/workers.md | 4 +- synapse/rest/admin/media.py | 68 ++++- synapse/storage/data_stores/main/room.py | 116 +++++++- tests/rest/admin/test_admin.py | 341 +++++++++++++++++++++++ tests/rest/client/v1/utils.py | 37 +++ 7 files changed, 636 insertions(+), 15 deletions(-) create mode 100644 changelog.d/6681.feature diff --git a/changelog.d/6681.feature b/changelog.d/6681.feature new file mode 100644 index 0000000000..5cf19a4e0e --- /dev/null +++ b/changelog.d/6681.feature @@ -0,0 +1 @@ +Add new quarantine media admin APIs to quarantine by media ID or by user who uploaded the media. diff --git a/docs/admin_api/media_admin_api.md b/docs/admin_api/media_admin_api.md index 8b3666d5f5..46ba7a1a71 100644 --- a/docs/admin_api/media_admin_api.md +++ b/docs/admin_api/media_admin_api.md @@ -22,19 +22,81 @@ It returns a JSON body like the following: } ``` -# Quarantine media in a room - -This API 'quarantines' all the media in a room. - -The API is: - -``` -POST /_synapse/admin/v1/quarantine_media/ - -{} -``` +# Quarantine media Quarantining media means that it is marked as inaccessible by users. It applies to any local media, and any locally-cached copies of remote media. The media file itself (and any thumbnails) is not deleted from the server. + +## Quarantining media by ID + +This API quarantines a single piece of local or remote media. + +Request: + +``` +POST /_synapse/admin/v1/media/quarantine// + +{} +``` + +Where `server_name` is in the form of `example.org`, and `media_id` is in the +form of `abcdefg12345...`. + +Response: + +``` +{} +``` + +## Quarantining media in a room + +This API quarantines all local and remote media in a room. + +Request: + +``` +POST /_synapse/admin/v1/room//media/quarantine + +{} +``` + +Where `room_id` is in the form of `!roomid12345:example.org`. + +Response: + +``` +{ + "num_quarantined": 10 # The number of media items successfully quarantined +} +``` + +Note that there is a legacy endpoint, `POST +/_synapse/admin/v1/quarantine_media/`, that operates the same. +However, it is deprecated and may be removed in a future release. + +## Quarantining all media of a user + +This API quarantines all *local* media that a *local* user has uploaded. That is to say, if +you would like to quarantine media uploaded by a user on a remote homeserver, you should +instead use one of the other APIs. + +Request: + +``` +POST /_synapse/admin/v1/user//media/quarantine + +{} +``` + +Where `user_id` is in the form of `@bob:example.org`. + +Response: + +``` +{ + "num_quarantined": 10 # The number of media items successfully quarantined +} +``` + diff --git a/docs/workers.md b/docs/workers.md index f4283aeb05..0ab269fd96 100644 --- a/docs/workers.md +++ b/docs/workers.md @@ -202,7 +202,9 @@ Handles the media repository. It can handle all endpoints starting with: ... and the following regular expressions matching media-specific administration APIs: ^/_synapse/admin/v1/purge_media_cache$ - ^/_synapse/admin/v1/room/.*/media$ + ^/_synapse/admin/v1/room/.*/media.*$ + ^/_synapse/admin/v1/user/.*/media.*$ + ^/_synapse/admin/v1/media/.*$ ^/_synapse/admin/v1/quarantine_media/.*$ You should also set `enable_media_repo: False` in the shared configuration diff --git a/synapse/rest/admin/media.py b/synapse/rest/admin/media.py index fa833e54cf..3a445d6eed 100644 --- a/synapse/rest/admin/media.py +++ b/synapse/rest/admin/media.py @@ -32,16 +32,24 @@ class QuarantineMediaInRoom(RestServlet): this server. """ - PATTERNS = historical_admin_path_patterns("/quarantine_media/(?P[^/]+)") + PATTERNS = ( + historical_admin_path_patterns("/room/(?P[^/]+)/media/quarantine") + + + # This path kept around for legacy reasons + historical_admin_path_patterns("/quarantine_media/(?P![^/]+)") + ) def __init__(self, hs): self.store = hs.get_datastore() self.auth = hs.get_auth() - async def on_POST(self, request, room_id): + async def on_POST(self, request, room_id: str): requester = await self.auth.get_user_by_req(request) await assert_user_is_admin(self.auth, requester.user) + logging.info("Quarantining room: %s", room_id) + + # Quarantine all media in this room num_quarantined = await self.store.quarantine_media_ids_in_room( room_id, requester.user.to_string() ) @@ -49,6 +57,60 @@ class QuarantineMediaInRoom(RestServlet): return 200, {"num_quarantined": num_quarantined} +class QuarantineMediaByUser(RestServlet): + """Quarantines all local media by a given user so that no one can download it via + this server. + """ + + PATTERNS = historical_admin_path_patterns( + "/user/(?P[^/]+)/media/quarantine" + ) + + def __init__(self, hs): + self.store = hs.get_datastore() + self.auth = hs.get_auth() + + async def on_POST(self, request, user_id: str): + requester = await self.auth.get_user_by_req(request) + await assert_user_is_admin(self.auth, requester.user) + + logging.info("Quarantining local media by user: %s", user_id) + + # Quarantine all media this user has uploaded + num_quarantined = await self.store.quarantine_media_ids_by_user( + user_id, requester.user.to_string() + ) + + return 200, {"num_quarantined": num_quarantined} + + +class QuarantineMediaByID(RestServlet): + """Quarantines local or remote media by a given ID so that no one can download + it via this server. + """ + + PATTERNS = historical_admin_path_patterns( + "/media/quarantine/(?P[^/]+)/(?P[^/]+)" + ) + + def __init__(self, hs): + self.store = hs.get_datastore() + self.auth = hs.get_auth() + + async def on_POST(self, request, server_name: str, media_id: str): + requester = await self.auth.get_user_by_req(request) + await assert_user_is_admin(self.auth, requester.user) + + logging.info("Quarantining local media by ID: %s/%s", server_name, media_id) + + # Quarantine this media id + await self.store.quarantine_media_by_id( + server_name, media_id, requester.user.to_string() + ) + + return 200, {} + + class ListMediaInRoom(RestServlet): """Lists all of the media in a given room. """ @@ -94,4 +156,6 @@ def register_servlets_for_media_repo(hs, http_server): """ PurgeMediaCacheRestServlet(hs).register(http_server) QuarantineMediaInRoom(hs).register(http_server) + QuarantineMediaByID(hs).register(http_server) + QuarantineMediaByUser(hs).register(http_server) ListMediaInRoom(hs).register(http_server) diff --git a/synapse/storage/data_stores/main/room.py b/synapse/storage/data_stores/main/room.py index 8636d75030..49bab62be3 100644 --- a/synapse/storage/data_stores/main/room.py +++ b/synapse/storage/data_stores/main/room.py @@ -18,7 +18,7 @@ import collections import logging import re from abc import abstractmethod -from typing import Optional, Tuple +from typing import List, Optional, Tuple from six import integer_types @@ -399,6 +399,8 @@ class RoomWorkerStore(SQLBaseStore): the associated media """ + logger.info("Quarantining media in room: %s", room_id) + def _quarantine_media_in_room_txn(txn): local_mxcs, remote_mxcs = self._get_media_mxcs_in_room_txn(txn, room_id) total_media_quarantined = 0 @@ -494,6 +496,118 @@ class RoomWorkerStore(SQLBaseStore): return local_media_mxcs, remote_media_mxcs + def quarantine_media_by_id( + self, server_name: str, media_id: str, quarantined_by: str, + ): + """quarantines a single local or remote media id + + Args: + server_name: The name of the server that holds this media + media_id: The ID of the media to be quarantined + quarantined_by: The user ID that initiated the quarantine request + """ + logger.info("Quarantining media: %s/%s", server_name, media_id) + is_local = server_name == self.config.server_name + + def _quarantine_media_by_id_txn(txn): + local_mxcs = [media_id] if is_local else [] + remote_mxcs = [(server_name, media_id)] if not is_local else [] + + return self._quarantine_media_txn( + txn, local_mxcs, remote_mxcs, quarantined_by + ) + + return self.db.runInteraction( + "quarantine_media_by_user", _quarantine_media_by_id_txn + ) + + def quarantine_media_ids_by_user(self, user_id: str, quarantined_by: str): + """quarantines all local media associated with a single user + + Args: + user_id: The ID of the user to quarantine media of + quarantined_by: The ID of the user who made the quarantine request + """ + + def _quarantine_media_by_user_txn(txn): + local_media_ids = self._get_media_ids_by_user_txn(txn, user_id) + return self._quarantine_media_txn(txn, local_media_ids, [], quarantined_by) + + return self.db.runInteraction( + "quarantine_media_by_user", _quarantine_media_by_user_txn + ) + + def _get_media_ids_by_user_txn(self, txn, user_id: str, filter_quarantined=True): + """Retrieves local media IDs by a given user + + Args: + txn (cursor) + user_id: The ID of the user to retrieve media IDs of + + Returns: + The local and remote media as a lists of tuples where the key is + the hostname and the value is the media ID. + """ + # Local media + sql = """ + SELECT media_id + FROM local_media_repository + WHERE user_id = ? + """ + if filter_quarantined: + sql += "AND quarantined_by IS NULL" + txn.execute(sql, (user_id,)) + + local_media_ids = [row[0] for row in txn] + + # TODO: Figure out all remote media a user has referenced in a message + + return local_media_ids + + def _quarantine_media_txn( + self, + txn, + local_mxcs: List[str], + remote_mxcs: List[Tuple[str, str]], + quarantined_by: str, + ) -> int: + """Quarantine local and remote media items + + Args: + txn (cursor) + local_mxcs: A list of local mxc URLs + remote_mxcs: A list of (remote server, media id) tuples representing + remote mxc URLs + quarantined_by: The ID of the user who initiated the quarantine request + Returns: + The total number of media items quarantined + """ + total_media_quarantined = 0 + + # Update all the tables to set the quarantined_by flag + txn.executemany( + """ + UPDATE local_media_repository + SET quarantined_by = ? + WHERE media_id = ? + """, + ((quarantined_by, media_id) for media_id in local_mxcs), + ) + + txn.executemany( + """ + UPDATE remote_media_cache + SET quarantined_by = ? + WHERE media_origin = ? AND media_id = ? + """, + ((quarantined_by, origin, media_id) for origin, media_id in remote_mxcs), + ) + + total_media_quarantined += len(local_mxcs) + total_media_quarantined += len(remote_mxcs) + + return total_media_quarantined + class RoomBackgroundUpdateStore(SQLBaseStore): REMOVE_TOMESTONED_ROOMS_BG_UPDATE = "remove_tombstoned_rooms_from_directory" diff --git a/tests/rest/admin/test_admin.py b/tests/rest/admin/test_admin.py index 6ceb483aa8..7a7e898843 100644 --- a/tests/rest/admin/test_admin.py +++ b/tests/rest/admin/test_admin.py @@ -14,11 +14,17 @@ # limitations under the License. import json +import os +import urllib.parse +from binascii import unhexlify from mock import Mock +from twisted.internet.defer import Deferred + import synapse.rest.admin from synapse.http.server import JsonResource +from synapse.logging.context import make_deferred_yieldable from synapse.rest.admin import VersionServlet from synapse.rest.client.v1 import events, login, room from synapse.rest.client.v2_alpha import groups @@ -346,3 +352,338 @@ class PurgeRoomTestCase(unittest.HomeserverTestCase): self.assertEqual(count, 0, msg="Rows not purged in {}".format(table)) test_purge_room.skip = "Disabled because it's currently broken" + + +class QuarantineMediaTestCase(unittest.HomeserverTestCase): + """Test /quarantine_media admin API. + """ + + servlets = [ + synapse.rest.admin.register_servlets, + synapse.rest.admin.register_servlets_for_media_repo, + login.register_servlets, + room.register_servlets, + ] + + def prepare(self, reactor, clock, hs): + self.store = hs.get_datastore() + self.hs = hs + + # Allow for uploading and downloading to/from the media repo + self.media_repo = hs.get_media_repository_resource() + self.download_resource = self.media_repo.children[b"download"] + self.upload_resource = self.media_repo.children[b"upload"] + self.image_data = unhexlify( + b"89504e470d0a1a0a0000000d4948445200000001000000010806" + b"0000001f15c4890000000a49444154789c63000100000500010d" + b"0a2db40000000049454e44ae426082" + ) + + def make_homeserver(self, reactor, clock): + + self.fetches = [] + + def get_file(destination, path, output_stream, args=None, max_size=None): + """ + Returns tuple[int,dict,str,int] of file length, response headers, + absolute URI, and response code. + """ + + def write_to(r): + data, response = r + output_stream.write(data) + return response + + d = Deferred() + d.addCallback(write_to) + self.fetches.append((d, destination, path, args)) + return make_deferred_yieldable(d) + + client = Mock() + client.get_file = get_file + + self.storage_path = self.mktemp() + self.media_store_path = self.mktemp() + os.mkdir(self.storage_path) + os.mkdir(self.media_store_path) + + config = self.default_config() + config["media_store_path"] = self.media_store_path + config["thumbnail_requirements"] = {} + config["max_image_pixels"] = 2000000 + + provider_config = { + "module": "synapse.rest.media.v1.storage_provider.FileStorageProviderBackend", + "store_local": True, + "store_synchronous": False, + "store_remote": True, + "config": {"directory": self.storage_path}, + } + config["media_storage_providers"] = [provider_config] + + hs = self.setup_test_homeserver(config=config, http_client=client) + + return hs + + def test_quarantine_media_requires_admin(self): + self.register_user("nonadmin", "pass", admin=False) + non_admin_user_tok = self.login("nonadmin", "pass") + + # Attempt quarantine media APIs as non-admin + url = "/_synapse/admin/v1/media/quarantine/example.org/abcde12345" + request, channel = self.make_request( + "POST", url.encode("ascii"), access_token=non_admin_user_tok, + ) + self.render(request) + + # Expect a forbidden error + self.assertEqual( + 403, + int(channel.result["code"]), + msg="Expected forbidden on quarantining media as a non-admin", + ) + + # And the roomID/userID endpoint + url = "/_synapse/admin/v1/room/!room%3Aexample.com/media/quarantine" + request, channel = self.make_request( + "POST", url.encode("ascii"), access_token=non_admin_user_tok, + ) + self.render(request) + + # Expect a forbidden error + self.assertEqual( + 403, + int(channel.result["code"]), + msg="Expected forbidden on quarantining media as a non-admin", + ) + + def test_quarantine_media_by_id(self): + self.register_user("id_admin", "pass", admin=True) + admin_user_tok = self.login("id_admin", "pass") + + self.register_user("id_nonadmin", "pass", admin=False) + non_admin_user_tok = self.login("id_nonadmin", "pass") + + # Upload some media into the room + response = self.helper.upload_media( + self.upload_resource, self.image_data, tok=admin_user_tok + ) + + # Extract media ID from the response + server_name_and_media_id = response["content_uri"][ + 6: + ] # Cut off the 'mxc://' bit + server_name, media_id = server_name_and_media_id.split("/") + + # Attempt to access the media + request, channel = self.make_request( + "GET", + server_name_and_media_id, + shorthand=False, + access_token=non_admin_user_tok, + ) + request.render(self.download_resource) + self.pump(1.0) + + # Should be successful + self.assertEqual(200, int(channel.code), msg=channel.result["body"]) + + # Quarantine the media + url = "/_synapse/admin/v1/media/quarantine/%s/%s" % ( + urllib.parse.quote(server_name), + urllib.parse.quote(media_id), + ) + request, channel = self.make_request("POST", url, access_token=admin_user_tok,) + self.render(request) + self.pump(1.0) + self.assertEqual(200, int(channel.code), msg=channel.result["body"]) + + # Attempt to access the media + request, channel = self.make_request( + "GET", + server_name_and_media_id, + shorthand=False, + access_token=admin_user_tok, + ) + request.render(self.download_resource) + self.pump(1.0) + + # Should be quarantined + self.assertEqual( + 404, + int(channel.code), + msg=( + "Expected to receive a 404 on accessing quarantined media: %s" + % server_name_and_media_id + ), + ) + + def test_quarantine_all_media_in_room(self): + self.register_user("room_admin", "pass", admin=True) + admin_user_tok = self.login("room_admin", "pass") + + non_admin_user = self.register_user("room_nonadmin", "pass", admin=False) + non_admin_user_tok = self.login("room_nonadmin", "pass") + + room_id = self.helper.create_room_as(non_admin_user, tok=admin_user_tok) + self.helper.join(room_id, non_admin_user, tok=non_admin_user_tok) + + # Upload some media + response_1 = self.helper.upload_media( + self.upload_resource, self.image_data, tok=non_admin_user_tok + ) + response_2 = self.helper.upload_media( + self.upload_resource, self.image_data, tok=non_admin_user_tok + ) + + # Extract mxcs + mxc_1 = response_1["content_uri"] + mxc_2 = response_2["content_uri"] + + # Send it into the room + self.helper.send_event( + room_id, + "m.room.message", + content={"body": "image-1", "msgtype": "m.image", "url": mxc_1}, + txn_id="111", + tok=non_admin_user_tok, + ) + self.helper.send_event( + room_id, + "m.room.message", + content={"body": "image-2", "msgtype": "m.image", "url": mxc_2}, + txn_id="222", + tok=non_admin_user_tok, + ) + + # Quarantine all media in the room + url = "/_synapse/admin/v1/room/%s/media/quarantine" % urllib.parse.quote( + room_id + ) + request, channel = self.make_request("POST", url, access_token=admin_user_tok,) + self.render(request) + self.pump(1.0) + self.assertEqual(200, int(channel.code), msg=channel.result["body"]) + self.assertEqual( + json.loads(channel.result["body"].decode("utf-8")), + {"num_quarantined": 2}, + "Expected 2 quarantined items", + ) + + # Convert mxc URLs to server/media_id strings + server_and_media_id_1 = mxc_1[6:] + server_and_media_id_2 = mxc_2[6:] + + # Test that we cannot download any of the media anymore + request, channel = self.make_request( + "GET", + server_and_media_id_1, + shorthand=False, + access_token=non_admin_user_tok, + ) + request.render(self.download_resource) + self.pump(1.0) + + # Should be quarantined + self.assertEqual( + 404, + int(channel.code), + msg=( + "Expected to receive a 404 on accessing quarantined media: %s" + % server_and_media_id_1 + ), + ) + + request, channel = self.make_request( + "GET", + server_and_media_id_2, + shorthand=False, + access_token=non_admin_user_tok, + ) + request.render(self.download_resource) + self.pump(1.0) + + # Should be quarantined + self.assertEqual( + 404, + int(channel.code), + msg=( + "Expected to receive a 404 on accessing quarantined media: %s" + % server_and_media_id_2 + ), + ) + + def test_quarantine_all_media_by_user(self): + self.register_user("user_admin", "pass", admin=True) + admin_user_tok = self.login("user_admin", "pass") + + non_admin_user = self.register_user("user_nonadmin", "pass", admin=False) + non_admin_user_tok = self.login("user_nonadmin", "pass") + + # Upload some media + response_1 = self.helper.upload_media( + self.upload_resource, self.image_data, tok=non_admin_user_tok + ) + response_2 = self.helper.upload_media( + self.upload_resource, self.image_data, tok=non_admin_user_tok + ) + + # Extract media IDs + server_and_media_id_1 = response_1["content_uri"][6:] + server_and_media_id_2 = response_2["content_uri"][6:] + + # Quarantine all media by this user + url = "/_synapse/admin/v1/user/%s/media/quarantine" % urllib.parse.quote( + non_admin_user + ) + request, channel = self.make_request( + "POST", url.encode("ascii"), access_token=admin_user_tok, + ) + self.render(request) + self.pump(1.0) + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual( + json.loads(channel.result["body"].decode("utf-8")), + {"num_quarantined": 2}, + "Expected 2 quarantined items", + ) + + # Attempt to access each piece of media + request, channel = self.make_request( + "GET", + server_and_media_id_1, + shorthand=False, + access_token=non_admin_user_tok, + ) + request.render(self.download_resource) + self.pump(1.0) + + # Should be quarantined + self.assertEqual( + 404, + int(channel.code), + msg=( + "Expected to receive a 404 on accessing quarantined media: %s" + % server_and_media_id_1, + ), + ) + + # Attempt to access each piece of media + request, channel = self.make_request( + "GET", + server_and_media_id_2, + shorthand=False, + access_token=non_admin_user_tok, + ) + request.render(self.download_resource) + self.pump(1.0) + + # Should be quarantined + self.assertEqual( + 404, + int(channel.code), + msg=( + "Expected to receive a 404 on accessing quarantined media: %s" + % server_and_media_id_2 + ), + ) diff --git a/tests/rest/client/v1/utils.py b/tests/rest/client/v1/utils.py index e7417b3d14..873d5ef99c 100644 --- a/tests/rest/client/v1/utils.py +++ b/tests/rest/client/v1/utils.py @@ -21,6 +21,8 @@ import time import attr +from twisted.web.resource import Resource + from synapse.api.constants import Membership from tests.server import make_request, render @@ -160,3 +162,38 @@ class RestHelper(object): ) return channel.json_body + + def upload_media( + self, + resource: Resource, + image_data: bytes, + tok: str, + filename: str = "test.png", + expect_code: int = 200, + ) -> dict: + """Upload a piece of test media to the media repo + Args: + resource: The resource that will handle the upload request + image_data: The image data to upload + tok: The user token to use during the upload + filename: The filename of the media to be uploaded + expect_code: The return code to expect from attempting to upload the media + """ + image_length = len(image_data) + path = "/_matrix/media/r0/upload?filename=%s" % (filename,) + request, channel = make_request( + self.hs.get_reactor(), "POST", path, content=image_data, access_token=tok + ) + request.requestHeaders.addRawHeader( + b"Content-Length", str(image_length).encode("UTF-8") + ) + request.render(resource) + self.hs.get_reactor().pump([100]) + + assert channel.code == expect_code, "Expected: %d, got: %d, resp: %r" % ( + expect_code, + int(channel.result["code"]), + channel.result["body"], + ) + + return channel.json_body From e8b68a4e4b439065536c281d8997af85880f6ee2 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 14 Jan 2020 14:08:06 +0000 Subject: [PATCH 0819/1623] Fixup synapse.replication to pass mypy checks (#6667) --- changelog.d/6667.misc | 1 + synapse/replication/http/_base.py | 10 ++-- synapse/replication/slave/storage/_base.py | 7 ++- synapse/replication/slave/storage/presence.py | 2 +- synapse/replication/tcp/client.py | 12 ++-- synapse/replication/tcp/commands.py | 42 +++++++------- synapse/replication/tcp/protocol.py | 36 +++++++----- synapse/replication/tcp/resource.py | 3 +- synapse/replication/tcp/streams/_base.py | 57 ++++++++++--------- synapse/replication/tcp/streams/events.py | 16 ++++-- synapse/replication/tcp/streams/federation.py | 4 +- tox.ini | 1 + 12 files changed, 105 insertions(+), 86 deletions(-) create mode 100644 changelog.d/6667.misc diff --git a/changelog.d/6667.misc b/changelog.d/6667.misc new file mode 100644 index 0000000000..227f80a508 --- /dev/null +++ b/changelog.d/6667.misc @@ -0,0 +1 @@ +Fixup `synapse.replication` to pass mypy checks. diff --git a/synapse/replication/http/_base.py b/synapse/replication/http/_base.py index c8056b0c0c..444eb7b7f4 100644 --- a/synapse/replication/http/_base.py +++ b/synapse/replication/http/_base.py @@ -16,6 +16,7 @@ import abc import logging import re +from typing import Dict, List, Tuple from six import raise_from from six.moves import urllib @@ -78,9 +79,8 @@ class ReplicationEndpoint(object): __metaclass__ = abc.ABCMeta - NAME = abc.abstractproperty() - PATH_ARGS = abc.abstractproperty() - + NAME = abc.abstractproperty() # type: str # type: ignore + PATH_ARGS = abc.abstractproperty() # type: Tuple[str, ...] # type: ignore METHOD = "POST" CACHE = True RETRY_ON_TIMEOUT = True @@ -171,7 +171,7 @@ class ReplicationEndpoint(object): # have a good idea that the request has either succeeded or failed on # the master, and so whether we should clean up or not. while True: - headers = {} + headers = {} # type: Dict[bytes, List[bytes]] inject_active_span_byte_dict(headers, None, check_destination=False) try: result = yield request_func(uri, data, headers=headers) @@ -207,7 +207,7 @@ class ReplicationEndpoint(object): method = self.METHOD if self.CACHE: - handler = self._cached_handler + handler = self._cached_handler # type: ignore url_args.append("txn_id") args = "/".join("(?P<%s>[^/]+)" % (arg,) for arg in url_args) diff --git a/synapse/replication/slave/storage/_base.py b/synapse/replication/slave/storage/_base.py index b91a528245..704282c800 100644 --- a/synapse/replication/slave/storage/_base.py +++ b/synapse/replication/slave/storage/_base.py @@ -14,7 +14,7 @@ # limitations under the License. import logging -from typing import Dict +from typing import Dict, Optional import six @@ -41,7 +41,7 @@ class BaseSlavedStore(SQLBaseStore): if isinstance(self.database_engine, PostgresEngine): self._cache_id_gen = SlavedIdTracker( db_conn, "cache_invalidation_stream", "stream_id" - ) + ) # type: Optional[SlavedIdTracker] else: self._cache_id_gen = None @@ -62,7 +62,8 @@ class BaseSlavedStore(SQLBaseStore): def process_replication_rows(self, stream_name, token, rows): if stream_name == "caches": - self._cache_id_gen.advance(token) + if self._cache_id_gen: + self._cache_id_gen.advance(token) for row in rows: if row.cache_func == CURRENT_STATE_CACHE_NAME: room_id = row.keys[0] diff --git a/synapse/replication/slave/storage/presence.py b/synapse/replication/slave/storage/presence.py index f552e7c972..ad8f0c15a9 100644 --- a/synapse/replication/slave/storage/presence.py +++ b/synapse/replication/slave/storage/presence.py @@ -29,7 +29,7 @@ class SlavedPresenceStore(BaseSlavedStore): self._presence_on_startup = self._get_active_presence(db_conn) - self.presence_stream_cache = self.presence_stream_cache = StreamChangeCache( + self.presence_stream_cache = StreamChangeCache( "PresenceStreamChangeCache", self._presence_id_gen.get_current_token() ) diff --git a/synapse/replication/tcp/client.py b/synapse/replication/tcp/client.py index bbcb84646c..aa7fd90e26 100644 --- a/synapse/replication/tcp/client.py +++ b/synapse/replication/tcp/client.py @@ -16,7 +16,7 @@ """ import logging -from typing import Dict +from typing import Dict, List, Optional from twisted.internet import defer from twisted.internet.protocol import ReconnectingClientFactory @@ -28,6 +28,7 @@ from synapse.replication.tcp.protocol import ( ) from .commands import ( + Command, FederationAckCommand, InvalidateCacheCommand, RemovePusherCommand, @@ -89,15 +90,15 @@ class ReplicationClientHandler(AbstractReplicationClientHandler): # Any pending commands to be sent once a new connection has been # established - self.pending_commands = [] + self.pending_commands = [] # type: List[Command] # Map from string -> deferred, to wake up when receiveing a SYNC with # the given string. # Used for tests. - self.awaiting_syncs = {} + self.awaiting_syncs = {} # type: Dict[str, defer.Deferred] # The factory used to create connections. - self.factory = None + self.factory = None # type: Optional[ReplicationClientFactory] def start_replication(self, hs): """Helper method to start a replication connection to the remote server @@ -235,4 +236,5 @@ class ReplicationClientHandler(AbstractReplicationClientHandler): # We don't reset the delay any earlier as otherwise if there is a # problem during start up we'll end up tight looping connecting to the # server. - self.factory.resetDelay() + if self.factory: + self.factory.resetDelay() diff --git a/synapse/replication/tcp/commands.py b/synapse/replication/tcp/commands.py index 0ff2a7199f..cbb36b9acf 100644 --- a/synapse/replication/tcp/commands.py +++ b/synapse/replication/tcp/commands.py @@ -20,15 +20,16 @@ allowed to be sent by which side. import logging import platform +from typing import Tuple, Type if platform.python_implementation() == "PyPy": import json _json_encoder = json.JSONEncoder() else: - import simplejson as json + import simplejson as json # type: ignore[no-redef] # noqa: F821 - _json_encoder = json.JSONEncoder(namedtuple_as_object=False) + _json_encoder = json.JSONEncoder(namedtuple_as_object=False) # type: ignore[call-arg] # noqa: F821 logger = logging.getLogger(__name__) @@ -44,7 +45,7 @@ class Command(object): The default implementation creates a command of form ` ` """ - NAME = None + NAME = None # type: str def __init__(self, data): self.data = data @@ -386,25 +387,24 @@ class UserIpCommand(Command): ) +_COMMANDS = ( + ServerCommand, + RdataCommand, + PositionCommand, + ErrorCommand, + PingCommand, + NameCommand, + ReplicateCommand, + UserSyncCommand, + FederationAckCommand, + SyncCommand, + RemovePusherCommand, + InvalidateCacheCommand, + UserIpCommand, +) # type: Tuple[Type[Command], ...] + # Map of command name to command type. -COMMAND_MAP = { - cmd.NAME: cmd - for cmd in ( - ServerCommand, - RdataCommand, - PositionCommand, - ErrorCommand, - PingCommand, - NameCommand, - ReplicateCommand, - UserSyncCommand, - FederationAckCommand, - SyncCommand, - RemovePusherCommand, - InvalidateCacheCommand, - UserIpCommand, - ) -} +COMMAND_MAP = {cmd.NAME: cmd for cmd in _COMMANDS} # The commands the server is allowed to send VALID_SERVER_COMMANDS = ( diff --git a/synapse/replication/tcp/protocol.py b/synapse/replication/tcp/protocol.py index afaf002fe6..db0353c996 100644 --- a/synapse/replication/tcp/protocol.py +++ b/synapse/replication/tcp/protocol.py @@ -53,6 +53,7 @@ import fcntl import logging import struct from collections import defaultdict +from typing import Any, DefaultDict, Dict, List, Set, Tuple from six import iteritems, iterkeys @@ -65,13 +66,11 @@ from twisted.python.failure import Failure from synapse.logging.context import make_deferred_yieldable, run_in_background from synapse.metrics import LaterGauge from synapse.metrics.background_process_metrics import run_as_background_process -from synapse.util import Clock -from synapse.util.stringutils import random_string - -from .commands import ( +from synapse.replication.tcp.commands import ( COMMAND_MAP, VALID_CLIENT_COMMANDS, VALID_SERVER_COMMANDS, + Command, ErrorCommand, NameCommand, PingCommand, @@ -82,6 +81,10 @@ from .commands import ( SyncCommand, UserSyncCommand, ) +from synapse.types import Collection +from synapse.util import Clock +from synapse.util.stringutils import random_string + from .streams import STREAMS_MAP connection_close_counter = Counter( @@ -124,8 +127,11 @@ class BaseReplicationStreamProtocol(LineOnlyReceiver): delimiter = b"\n" - VALID_INBOUND_COMMANDS = [] # Valid commands we expect to receive - VALID_OUTBOUND_COMMANDS = [] # Valid commans we can send + # Valid commands we expect to receive + VALID_INBOUND_COMMANDS = [] # type: Collection[str] + + # Valid commands we can send + VALID_OUTBOUND_COMMANDS = [] # type: Collection[str] max_line_buffer = 10000 @@ -144,13 +150,13 @@ class BaseReplicationStreamProtocol(LineOnlyReceiver): self.conn_id = random_string(5) # To dedupe in case of name clashes. # List of pending commands to send once we've established the connection - self.pending_commands = [] + self.pending_commands = [] # type: List[Command] # The LoopingCall for sending pings. self._send_ping_loop = None - self.inbound_commands_counter = defaultdict(int) - self.outbound_commands_counter = defaultdict(int) + self.inbound_commands_counter = defaultdict(int) # type: DefaultDict[str, int] + self.outbound_commands_counter = defaultdict(int) # type: DefaultDict[str, int] def connectionMade(self): logger.info("[%s] Connection established", self.id()) @@ -409,14 +415,14 @@ class ServerReplicationStreamProtocol(BaseReplicationStreamProtocol): self.streamer = streamer # The streams the client has subscribed to and is up to date with - self.replication_streams = set() + self.replication_streams = set() # type: Set[str] # The streams the client is currently subscribing to. - self.connecting_streams = set() + self.connecting_streams = set() # type: Set[str] # Map from stream name to list of updates to send once we've finished # subscribing the client to the stream. - self.pending_rdata = {} + self.pending_rdata = {} # type: Dict[str, List[Tuple[int, Any]]] def connectionMade(self): self.send_command(ServerCommand(self.server_name)) @@ -642,11 +648,11 @@ class ClientReplicationStreamProtocol(BaseReplicationStreamProtocol): # Set of stream names that have been subscribe to, but haven't yet # caught up with. This is used to track when the client has been fully # connected to the remote. - self.streams_connecting = set() + self.streams_connecting = set() # type: Set[str] # Map of stream to batched updates. See RdataCommand for info on how # batching works. - self.pending_batches = {} + self.pending_batches = {} # type: Dict[str, Any] def connectionMade(self): self.send_command(NameCommand(self.client_name)) @@ -766,7 +772,7 @@ def transport_kernel_read_buffer_size(protocol, read=True): op = SIOCINQ else: op = SIOCOUTQ - size = struct.unpack("I", fcntl.ioctl(fileno, op, "\0\0\0\0"))[0] + size = struct.unpack("I", fcntl.ioctl(fileno, op, b"\0\0\0\0"))[0] return size return 0 diff --git a/synapse/replication/tcp/resource.py b/synapse/replication/tcp/resource.py index d1e98428bc..cbfdaf5773 100644 --- a/synapse/replication/tcp/resource.py +++ b/synapse/replication/tcp/resource.py @@ -17,6 +17,7 @@ import logging import random +from typing import List from six import itervalues @@ -79,7 +80,7 @@ class ReplicationStreamer(object): self._replication_torture_level = hs.config.replication_torture_level # Current connections. - self.connections = [] + self.connections = [] # type: List[ServerReplicationStreamProtocol] LaterGauge( "synapse_replication_tcp_resource_total_connections", diff --git a/synapse/replication/tcp/streams/_base.py b/synapse/replication/tcp/streams/_base.py index 8512923eae..4ab0334fc1 100644 --- a/synapse/replication/tcp/streams/_base.py +++ b/synapse/replication/tcp/streams/_base.py @@ -14,10 +14,10 @@ # See the License for the specific language governing permissions and # limitations under the License. - import itertools import logging from collections import namedtuple +from typing import Any from twisted.internet import defer @@ -104,8 +104,9 @@ class Stream(object): time it was called up until the point `advance_current_token` was called. """ - NAME = None # The name of the stream - ROW_TYPE = None # The type of the row. Used by the default impl of parse_row. + NAME = None # type: str # The name of the stream + # The type of the row. Used by the default impl of parse_row. + ROW_TYPE = None # type: Any _LIMITED = True # Whether the update function takes a limit @classmethod @@ -231,8 +232,8 @@ class BackfillStream(Stream): def __init__(self, hs): store = hs.get_datastore() - self.current_token = store.get_current_backfill_token - self.update_function = store.get_all_new_backfill_event_rows + self.current_token = store.get_current_backfill_token # type: ignore + self.update_function = store.get_all_new_backfill_event_rows # type: ignore super(BackfillStream, self).__init__(hs) @@ -246,8 +247,8 @@ class PresenceStream(Stream): store = hs.get_datastore() presence_handler = hs.get_presence_handler() - self.current_token = store.get_current_presence_token - self.update_function = presence_handler.get_all_presence_updates + self.current_token = store.get_current_presence_token # type: ignore + self.update_function = presence_handler.get_all_presence_updates # type: ignore super(PresenceStream, self).__init__(hs) @@ -260,8 +261,8 @@ class TypingStream(Stream): def __init__(self, hs): typing_handler = hs.get_typing_handler() - self.current_token = typing_handler.get_current_token - self.update_function = typing_handler.get_all_typing_updates + self.current_token = typing_handler.get_current_token # type: ignore + self.update_function = typing_handler.get_all_typing_updates # type: ignore super(TypingStream, self).__init__(hs) @@ -273,8 +274,8 @@ class ReceiptsStream(Stream): def __init__(self, hs): store = hs.get_datastore() - self.current_token = store.get_max_receipt_stream_id - self.update_function = store.get_all_updated_receipts + self.current_token = store.get_max_receipt_stream_id # type: ignore + self.update_function = store.get_all_updated_receipts # type: ignore super(ReceiptsStream, self).__init__(hs) @@ -310,8 +311,8 @@ class PushersStream(Stream): def __init__(self, hs): store = hs.get_datastore() - self.current_token = store.get_pushers_stream_token - self.update_function = store.get_all_updated_pushers_rows + self.current_token = store.get_pushers_stream_token # type: ignore + self.update_function = store.get_all_updated_pushers_rows # type: ignore super(PushersStream, self).__init__(hs) @@ -327,8 +328,8 @@ class CachesStream(Stream): def __init__(self, hs): store = hs.get_datastore() - self.current_token = store.get_cache_stream_token - self.update_function = store.get_all_updated_caches + self.current_token = store.get_cache_stream_token # type: ignore + self.update_function = store.get_all_updated_caches # type: ignore super(CachesStream, self).__init__(hs) @@ -343,8 +344,8 @@ class PublicRoomsStream(Stream): def __init__(self, hs): store = hs.get_datastore() - self.current_token = store.get_current_public_room_stream_id - self.update_function = store.get_all_new_public_rooms + self.current_token = store.get_current_public_room_stream_id # type: ignore + self.update_function = store.get_all_new_public_rooms # type: ignore super(PublicRoomsStream, self).__init__(hs) @@ -360,8 +361,8 @@ class DeviceListsStream(Stream): def __init__(self, hs): store = hs.get_datastore() - self.current_token = store.get_device_stream_token - self.update_function = store.get_all_device_list_changes_for_remotes + self.current_token = store.get_device_stream_token # type: ignore + self.update_function = store.get_all_device_list_changes_for_remotes # type: ignore super(DeviceListsStream, self).__init__(hs) @@ -376,8 +377,8 @@ class ToDeviceStream(Stream): def __init__(self, hs): store = hs.get_datastore() - self.current_token = store.get_to_device_stream_token - self.update_function = store.get_all_new_device_messages + self.current_token = store.get_to_device_stream_token # type: ignore + self.update_function = store.get_all_new_device_messages # type: ignore super(ToDeviceStream, self).__init__(hs) @@ -392,8 +393,8 @@ class TagAccountDataStream(Stream): def __init__(self, hs): store = hs.get_datastore() - self.current_token = store.get_max_account_data_stream_id - self.update_function = store.get_all_updated_tags + self.current_token = store.get_max_account_data_stream_id # type: ignore + self.update_function = store.get_all_updated_tags # type: ignore super(TagAccountDataStream, self).__init__(hs) @@ -408,7 +409,7 @@ class AccountDataStream(Stream): def __init__(self, hs): self.store = hs.get_datastore() - self.current_token = self.store.get_max_account_data_stream_id + self.current_token = self.store.get_max_account_data_stream_id # type: ignore super(AccountDataStream, self).__init__(hs) @@ -434,8 +435,8 @@ class GroupServerStream(Stream): def __init__(self, hs): store = hs.get_datastore() - self.current_token = store.get_group_stream_token - self.update_function = store.get_all_groups_changes + self.current_token = store.get_group_stream_token # type: ignore + self.update_function = store.get_all_groups_changes # type: ignore super(GroupServerStream, self).__init__(hs) @@ -451,7 +452,7 @@ class UserSignatureStream(Stream): def __init__(self, hs): store = hs.get_datastore() - self.current_token = store.get_device_stream_token - self.update_function = store.get_all_user_signature_changes_for_remotes + self.current_token = store.get_device_stream_token # type: ignore + self.update_function = store.get_all_user_signature_changes_for_remotes # type: ignore super(UserSignatureStream, self).__init__(hs) diff --git a/synapse/replication/tcp/streams/events.py b/synapse/replication/tcp/streams/events.py index d97669c886..0843e5aa90 100644 --- a/synapse/replication/tcp/streams/events.py +++ b/synapse/replication/tcp/streams/events.py @@ -13,7 +13,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + import heapq +from typing import Tuple, Type import attr @@ -63,7 +65,8 @@ class BaseEventsStreamRow(object): Specifies how to identify, serialize and deserialize the different types. """ - TypeId = None # Unique string that ids the type. Must be overriden in sub classes. + # Unique string that ids the type. Must be overriden in sub classes. + TypeId = None # type: str @classmethod def from_data(cls, data): @@ -99,9 +102,12 @@ class EventsStreamCurrentStateRow(BaseEventsStreamRow): event_id = attr.ib() # str, optional -TypeToRow = { - Row.TypeId: Row for Row in (EventsStreamEventRow, EventsStreamCurrentStateRow) -} +_EventRows = ( + EventsStreamEventRow, + EventsStreamCurrentStateRow, +) # type: Tuple[Type[BaseEventsStreamRow], ...] + +TypeToRow = {Row.TypeId: Row for Row in _EventRows} class EventsStream(Stream): @@ -112,7 +118,7 @@ class EventsStream(Stream): def __init__(self, hs): self._store = hs.get_datastore() - self.current_token = self._store.get_current_events_token + self.current_token = self._store.get_current_events_token # type: ignore super(EventsStream, self).__init__(hs) diff --git a/synapse/replication/tcp/streams/federation.py b/synapse/replication/tcp/streams/federation.py index dc2484109d..615f3dc9ac 100644 --- a/synapse/replication/tcp/streams/federation.py +++ b/synapse/replication/tcp/streams/federation.py @@ -37,7 +37,7 @@ class FederationStream(Stream): def __init__(self, hs): federation_sender = hs.get_federation_sender() - self.current_token = federation_sender.get_current_token - self.update_function = federation_sender.get_replication_rows + self.current_token = federation_sender.get_current_token # type: ignore + self.update_function = federation_sender.get_replication_rows # type: ignore super(FederationStream, self).__init__(hs) diff --git a/tox.ini b/tox.ini index 0ab6d5666b..b73a993053 100644 --- a/tox.ini +++ b/tox.ini @@ -181,6 +181,7 @@ commands = mypy \ synapse/handlers/ui_auth \ synapse/logging/ \ synapse/module_api \ + synapse/replication \ synapse/rest/consent \ synapse/rest/saml2 \ synapse/spam_checker_api \ From b5ce7f5874b1d6983a4bb992cb3a8093df6b1802 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 14 Jan 2020 14:08:35 +0000 Subject: [PATCH 0820/1623] Process EDUs in parallel with PDUs. (#6697) This means that things like to device messages don't get blocked behind processing PDUs, which can potentially take *ages*. --- changelog.d/6697.misc | 1 + synapse/federation/federation_server.py | 70 ++++++++++++++++++++----- 2 files changed, 59 insertions(+), 12 deletions(-) create mode 100644 changelog.d/6697.misc diff --git a/changelog.d/6697.misc b/changelog.d/6697.misc new file mode 100644 index 0000000000..5650387804 --- /dev/null +++ b/changelog.d/6697.misc @@ -0,0 +1 @@ +Don't block processing of incoming EDUs behind processing PDUs in the same transaction. diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index d7ce333822..8eddb3bf2c 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -15,6 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import logging +from typing import Dict import six from six import iteritems @@ -22,6 +23,7 @@ from six import iteritems from canonicaljson import json from prometheus_client import Counter +from twisted.internet import defer from twisted.internet.abstract import isIPAddress from twisted.python import failure @@ -41,7 +43,11 @@ from synapse.federation.federation_base import FederationBase, event_from_pdu_js from synapse.federation.persistence import TransactionActions from synapse.federation.units import Edu, Transaction from synapse.http.endpoint import parse_server_name -from synapse.logging.context import nested_logging_context +from synapse.logging.context import ( + make_deferred_yieldable, + nested_logging_context, + run_in_background, +) from synapse.logging.opentracing import log_kv, start_active_span_from_edu, trace from synapse.logging.utils import log_function from synapse.replication.http.federation import ( @@ -49,7 +55,7 @@ from synapse.replication.http.federation import ( ReplicationGetQueryRestServlet, ) from synapse.types import get_domain_from_id -from synapse.util import glob_to_regex +from synapse.util import glob_to_regex, unwrapFirstError from synapse.util.async_helpers import Linearizer, concurrently_execute from synapse.util.caches.response_cache import ResponseCache @@ -160,6 +166,43 @@ class FederationServer(FederationBase): ) return 400, response + # We process PDUs and EDUs in parallel. This is important as we don't + # want to block things like to device messages from reaching clients + # behind the potentially expensive handling of PDUs. + pdu_results, _ = await make_deferred_yieldable( + defer.gatherResults( + [ + run_in_background( + self._handle_pdus_in_txn, origin, transaction, request_time + ), + run_in_background(self._handle_edus_in_txn, origin, transaction), + ], + consumeErrors=True, + ).addErrback(unwrapFirstError) + ) + + response = {"pdus": pdu_results} + + logger.debug("Returning: %s", str(response)) + + await self.transaction_actions.set_response(origin, transaction, 200, response) + return 200, response + + async def _handle_pdus_in_txn( + self, origin: str, transaction: Transaction, request_time: int + ) -> Dict[str, dict]: + """Process the PDUs in a received transaction. + + Args: + origin: the server making the request + transaction: incoming transaction + request_time: timestamp that the HTTP request arrived at + + Returns: + A map from event ID of a processed PDU to any errors we should + report back to the sending server. + """ + received_pdus_counter.inc(len(transaction.pdus)) origin_host, _ = parse_server_name(origin) @@ -250,20 +293,23 @@ class FederationServer(FederationBase): process_pdus_for_room, pdus_by_room.keys(), TRANSACTION_CONCURRENCY_LIMIT ) - if hasattr(transaction, "edus"): - for edu in (Edu(**x) for x in transaction.edus): - await self.received_edu(origin, edu.edu_type, edu.content) + return pdu_results - response = {"pdus": pdu_results} + async def _handle_edus_in_txn(self, origin: str, transaction: Transaction): + """Process the EDUs in a received transaction. + """ - logger.debug("Returning: %s", str(response)) + async def _process_edu(edu_dict): + received_edus_counter.inc() - await self.transaction_actions.set_response(origin, transaction, 200, response) - return 200, response + edu = Edu(**edu_dict) + await self.registry.on_edu(edu.edu_type, origin, edu.content) - async def received_edu(self, origin, edu_type, content): - received_edus_counter.inc() - await self.registry.on_edu(edu_type, origin, content) + await concurrently_execute( + _process_edu, + getattr(transaction, "edus", []), + TRANSACTION_CONCURRENCY_LIMIT, + ) async def on_context_state_request(self, origin, room_id, event_id): origin_host, _ = parse_server_name(origin) From 28c98e51ffa166bd717646b0b34228e59f253485 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 15 Jan 2020 14:59:33 +0000 Subject: [PATCH 0821/1623] Add `local_current_membership` table (#6655) Currently we rely on `current_state_events` to figure out what rooms a user was in and their last membership event in there. However, if the server leaves the room then the table may be cleaned up and that information is lost. So lets add a table that separately holds that information. --- changelog.d/6655.misc | 1 + scripts/synapse_port_db | 2 +- synapse/handlers/admin.py | 2 +- synapse/handlers/deactivate_account.py | 2 +- synapse/handlers/initial_sync.py | 2 +- synapse/handlers/room_member.py | 2 +- synapse/handlers/search.py | 2 +- synapse/handlers/sync.py | 2 +- synapse/push/push_tools.py | 2 +- synapse/replication/slave/storage/events.py | 2 +- .../server_notices/server_notices_manager.py | 2 +- synapse/storage/data_stores/main/events.py | 30 +++ .../storage/data_stores/main/roommember.py | 191 ++++++++++-------- .../delta/57/local_current_membership.py | 97 +++++++++ synapse/storage/prepare_database.py | 2 +- tests/handlers/test_sync.py | 4 +- .../replication/slave/storage/test_events.py | 4 +- tests/rest/client/v2_alpha/test_account.py | 12 +- tests/rest/client/v2_alpha/test_sync.py | 9 - tests/storage/test_roommember.py | 2 +- 20 files changed, 264 insertions(+), 108 deletions(-) create mode 100644 changelog.d/6655.misc create mode 100644 synapse/storage/data_stores/main/schema/delta/57/local_current_membership.py diff --git a/changelog.d/6655.misc b/changelog.d/6655.misc new file mode 100644 index 0000000000..01e78bc84e --- /dev/null +++ b/changelog.d/6655.misc @@ -0,0 +1 @@ +Add `local_current_membership` table for tracking local user membership state in rooms. diff --git a/scripts/synapse_port_db b/scripts/synapse_port_db index f135c8bc54..5e69104b97 100755 --- a/scripts/synapse_port_db +++ b/scripts/synapse_port_db @@ -470,7 +470,7 @@ class Porter(object): engine.check_database( db_conn, allow_outdated_version=allow_outdated_version ) - prepare_database(db_conn, engine, config=None) + prepare_database(db_conn, engine, config=self.hs_config) store = Store(Database(hs, db_config, engine), db_conn, hs) db_conn.commit() diff --git a/synapse/handlers/admin.py b/synapse/handlers/admin.py index 76d18a8ba8..a9407553b4 100644 --- a/synapse/handlers/admin.py +++ b/synapse/handlers/admin.py @@ -134,7 +134,7 @@ class AdminHandler(BaseHandler): The returned value is that returned by `writer.finished()`. """ # Get all rooms the user is in or has been in - rooms = await self.store.get_rooms_for_user_where_membership_is( + rooms = await self.store.get_rooms_for_local_user_where_membership_is( user_id, membership_list=( Membership.JOIN, diff --git a/synapse/handlers/deactivate_account.py b/synapse/handlers/deactivate_account.py index 4426967f88..2afb390a92 100644 --- a/synapse/handlers/deactivate_account.py +++ b/synapse/handlers/deactivate_account.py @@ -140,7 +140,7 @@ class DeactivateAccountHandler(BaseHandler): user_id (str): The user ID to reject pending invites for. """ user = UserID.from_string(user_id) - pending_invites = await self.store.get_invited_rooms_for_user(user_id) + pending_invites = await self.store.get_invited_rooms_for_local_user(user_id) for room in pending_invites: try: diff --git a/synapse/handlers/initial_sync.py b/synapse/handlers/initial_sync.py index 44ec3e66ae..2e6755f19c 100644 --- a/synapse/handlers/initial_sync.py +++ b/synapse/handlers/initial_sync.py @@ -101,7 +101,7 @@ class InitialSyncHandler(BaseHandler): if include_archived: memberships.append(Membership.LEAVE) - room_list = await self.store.get_rooms_for_user_where_membership_is( + room_list = await self.store.get_rooms_for_local_user_where_membership_is( user_id=user_id, membership_list=memberships ) diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index 03bb52ccfb..15e8aa5249 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -690,7 +690,7 @@ class RoomMemberHandler(object): @defer.inlineCallbacks def _get_inviter(self, user_id, room_id): - invite = yield self.store.get_invite_for_user_in_room( + invite = yield self.store.get_invite_for_local_user_in_room( user_id=user_id, room_id=room_id ) if invite: diff --git a/synapse/handlers/search.py b/synapse/handlers/search.py index ef750d1497..110097eab9 100644 --- a/synapse/handlers/search.py +++ b/synapse/handlers/search.py @@ -179,7 +179,7 @@ class SearchHandler(BaseHandler): search_filter = Filter(filter_dict) # TODO: Search through left rooms too - rooms = yield self.store.get_rooms_for_user_where_membership_is( + rooms = yield self.store.get_rooms_for_local_user_where_membership_is( user.to_string(), membership_list=[Membership.JOIN], # membership_list=[Membership.JOIN, Membership.LEAVE, Membership.Ban], diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index 2d3b8ba73c..cd95f85e3f 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -1662,7 +1662,7 @@ class SyncHandler(object): Membership.BAN, ) - room_list = await self.store.get_rooms_for_user_where_membership_is( + room_list = await self.store.get_rooms_for_local_user_where_membership_is( user_id=user_id, membership_list=membership_list ) diff --git a/synapse/push/push_tools.py b/synapse/push/push_tools.py index de5c101a58..5dae4648c0 100644 --- a/synapse/push/push_tools.py +++ b/synapse/push/push_tools.py @@ -21,7 +21,7 @@ from synapse.storage import Storage @defer.inlineCallbacks def get_badge_count(store, user_id): - invites = yield store.get_invited_rooms_for_user(user_id) + invites = yield store.get_invited_rooms_for_local_user(user_id) joins = yield store.get_rooms_for_user(user_id) my_receipts_by_room = yield store.get_receipts_for_user(user_id, "m.read") diff --git a/synapse/replication/slave/storage/events.py b/synapse/replication/slave/storage/events.py index 29f35b9915..3aa6cb8b96 100644 --- a/synapse/replication/slave/storage/events.py +++ b/synapse/replication/slave/storage/events.py @@ -152,7 +152,7 @@ class SlavedEventStore( if etype == EventTypes.Member: self._membership_stream_cache.entity_has_changed(state_key, stream_ordering) - self.get_invited_rooms_for_user.invalidate((state_key,)) + self.get_invited_rooms_for_local_user.invalidate((state_key,)) if relates_to: self.get_relations_for_event.invalidate_many((relates_to,)) diff --git a/synapse/server_notices/server_notices_manager.py b/synapse/server_notices/server_notices_manager.py index 2dac90578c..f7432c8d2f 100644 --- a/synapse/server_notices/server_notices_manager.py +++ b/synapse/server_notices/server_notices_manager.py @@ -105,7 +105,7 @@ class ServerNoticesManager(object): assert self._is_mine_id(user_id), "Cannot send server notices to remote users" - rooms = yield self._store.get_rooms_for_user_where_membership_is( + rooms = yield self._store.get_rooms_for_local_user_where_membership_is( user_id, [Membership.INVITE, Membership.JOIN] ) system_mxid = self._config.server_notices_mxid diff --git a/synapse/storage/data_stores/main/events.py b/synapse/storage/data_stores/main/events.py index 58f35d7f56..e9fe63037b 100644 --- a/synapse/storage/data_stores/main/events.py +++ b/synapse/storage/data_stores/main/events.py @@ -128,6 +128,7 @@ class EventsStore( hs.get_clock().looping_call(_censor_redactions, 5 * 60 * 1000) self._ephemeral_messages_enabled = hs.config.enable_ephemeral_messages + self.is_mine_id = hs.is_mine_id @defer.inlineCallbacks def _read_forward_extremities(self): @@ -547,6 +548,34 @@ class EventsStore( ], ) + # Note: Do we really want to delete rows here (that we do not + # subsequently reinsert below)? While technically correct it means + # we have no record of the fact the user *was* a member of the + # room but got, say, state reset out of it. + if to_delete or to_insert: + txn.executemany( + "DELETE FROM local_current_membership" + " WHERE room_id = ? AND user_id = ?", + ( + (room_id, state_key) + for etype, state_key in itertools.chain(to_delete, to_insert) + if etype == EventTypes.Member and self.is_mine_id(state_key) + ), + ) + + if to_insert: + txn.executemany( + """INSERT INTO local_current_membership + (room_id, user_id, event_id, membership) + VALUES (?, ?, ?, (SELECT membership FROM room_memberships WHERE event_id = ?)) + """, + [ + (room_id, key[1], ev_id, ev_id) + for key, ev_id in to_insert.items() + if key[0] == EventTypes.Member and self.is_mine_id(key[1]) + ], + ) + txn.call_after( self._curr_state_delta_stream_cache.entity_has_changed, room_id, @@ -1724,6 +1753,7 @@ class EventsStore( "local_invites", "room_account_data", "room_tags", + "local_current_membership", ): logger.info("[purge] removing %s from %s", room_id, table) txn.execute("DELETE FROM %s WHERE room_id=?" % (table,), (room_id,)) diff --git a/synapse/storage/data_stores/main/roommember.py b/synapse/storage/data_stores/main/roommember.py index 70ff5751b6..9acef7c950 100644 --- a/synapse/storage/data_stores/main/roommember.py +++ b/synapse/storage/data_stores/main/roommember.py @@ -297,19 +297,22 @@ class RoomMemberWorkerStore(EventsWorkerStore): return {row[0]: row[1] for row in txn} @cached() - def get_invited_rooms_for_user(self, user_id): - """ Get all the rooms the user is invited to + def get_invited_rooms_for_local_user(self, user_id): + """ Get all the rooms the *local* user is invited to + Args: user_id (str): The user ID. Returns: A deferred list of RoomsForUser. """ - return self.get_rooms_for_user_where_membership_is(user_id, [Membership.INVITE]) + return self.get_rooms_for_local_user_where_membership_is( + user_id, [Membership.INVITE] + ) @defer.inlineCallbacks - def get_invite_for_user_in_room(self, user_id, room_id): - """Gets the invite for the given user and room + def get_invite_for_local_user_in_room(self, user_id, room_id): + """Gets the invite for the given *local* user and room Args: user_id (str) @@ -319,15 +322,15 @@ class RoomMemberWorkerStore(EventsWorkerStore): Deferred: Resolves to either a RoomsForUser or None if no invite was found. """ - invites = yield self.get_invited_rooms_for_user(user_id) + invites = yield self.get_invited_rooms_for_local_user(user_id) for invite in invites: if invite.room_id == room_id: return invite return None @defer.inlineCallbacks - def get_rooms_for_user_where_membership_is(self, user_id, membership_list): - """ Get all the rooms for this user where the membership for this user + def get_rooms_for_local_user_where_membership_is(self, user_id, membership_list): + """ Get all the rooms for this *local* user where the membership for this user matches one in the membership list. Filters out forgotten rooms. @@ -344,8 +347,8 @@ class RoomMemberWorkerStore(EventsWorkerStore): return defer.succeed(None) rooms = yield self.db.runInteraction( - "get_rooms_for_user_where_membership_is", - self._get_rooms_for_user_where_membership_is_txn, + "get_rooms_for_local_user_where_membership_is", + self._get_rooms_for_local_user_where_membership_is_txn, user_id, membership_list, ) @@ -354,76 +357,42 @@ class RoomMemberWorkerStore(EventsWorkerStore): forgotten_rooms = yield self.get_forgotten_rooms_for_user(user_id) return [room for room in rooms if room.room_id not in forgotten_rooms] - def _get_rooms_for_user_where_membership_is_txn( + def _get_rooms_for_local_user_where_membership_is_txn( self, txn, user_id, membership_list ): - - do_invite = Membership.INVITE in membership_list - membership_list = [m for m in membership_list if m != Membership.INVITE] - - results = [] - if membership_list: - if self._current_state_events_membership_up_to_date: - clause, args = make_in_list_sql_clause( - self.database_engine, "c.membership", membership_list - ) - sql = """ - SELECT room_id, e.sender, c.membership, event_id, e.stream_ordering - FROM current_state_events AS c - INNER JOIN events AS e USING (room_id, event_id) - WHERE - c.type = 'm.room.member' - AND state_key = ? - AND %s - """ % ( - clause, - ) - else: - clause, args = make_in_list_sql_clause( - self.database_engine, "m.membership", membership_list - ) - sql = """ - SELECT room_id, e.sender, m.membership, event_id, e.stream_ordering - FROM current_state_events AS c - INNER JOIN room_memberships AS m USING (room_id, event_id) - INNER JOIN events AS e USING (room_id, event_id) - WHERE - c.type = 'm.room.member' - AND state_key = ? - AND %s - """ % ( - clause, - ) - - txn.execute(sql, (user_id, *args)) - results = [RoomsForUser(**r) for r in self.db.cursor_to_dict(txn)] - - if do_invite: - sql = ( - "SELECT i.room_id, inviter, i.event_id, e.stream_ordering" - " FROM local_invites as i" - " INNER JOIN events as e USING (event_id)" - " WHERE invitee = ? AND locally_rejected is NULL" - " AND replaced_by is NULL" + # Paranoia check. + if not self.hs.is_mine_id(user_id): + raise Exception( + "Cannot call 'get_rooms_for_local_user_where_membership_is' on non-local user %r" + % (user_id,), ) - txn.execute(sql, (user_id,)) - results.extend( - RoomsForUser( - room_id=r["room_id"], - sender=r["inviter"], - event_id=r["event_id"], - stream_ordering=r["stream_ordering"], - membership=Membership.INVITE, - ) - for r in self.db.cursor_to_dict(txn) - ) + clause, args = make_in_list_sql_clause( + self.database_engine, "c.membership", membership_list + ) + + sql = """ + SELECT room_id, e.sender, c.membership, event_id, e.stream_ordering + FROM local_current_membership AS c + INNER JOIN events AS e USING (room_id, event_id) + WHERE + user_id = ? + AND %s + """ % ( + clause, + ) + + txn.execute(sql, (user_id, *args)) + results = [RoomsForUser(**r) for r in self.db.cursor_to_dict(txn)] return results - @cachedInlineCallbacks(max_entries=500000, iterable=True) + @cached(max_entries=500000, iterable=True) def get_rooms_for_user_with_stream_ordering(self, user_id): - """Returns a set of room_ids the user is currently joined to + """Returns a set of room_ids the user is currently joined to. + + If a remote user only returns rooms this server is currently + participating in. Args: user_id (str) @@ -433,17 +402,49 @@ class RoomMemberWorkerStore(EventsWorkerStore): the rooms the user is in currently, along with the stream ordering of the most recent join for that user and room. """ - rooms = yield self.get_rooms_for_user_where_membership_is( - user_id, membership_list=[Membership.JOIN] - ) - return frozenset( - GetRoomsForUserWithStreamOrdering(r.room_id, r.stream_ordering) - for r in rooms + return self.db.runInteraction( + "get_rooms_for_user_with_stream_ordering", + self._get_rooms_for_user_with_stream_ordering_txn, + user_id, ) + def _get_rooms_for_user_with_stream_ordering_txn(self, txn, user_id): + # We use `current_state_events` here and not `local_current_membership` + # as a) this gets called with remote users and b) this only gets called + # for rooms the server is participating in. + if self._current_state_events_membership_up_to_date: + sql = """ + SELECT room_id, e.stream_ordering + FROM current_state_events AS c + INNER JOIN events AS e USING (room_id, event_id) + WHERE + c.type = 'm.room.member' + AND state_key = ? + AND c.membership = ? + """ + else: + sql = """ + SELECT room_id, e.stream_ordering + FROM current_state_events AS c + INNER JOIN room_memberships AS m USING (room_id, event_id) + INNER JOIN events AS e USING (room_id, event_id) + WHERE + c.type = 'm.room.member' + AND state_key = ? + AND m.membership = ? + """ + + txn.execute(sql, (user_id, Membership.JOIN)) + results = frozenset(GetRoomsForUserWithStreamOrdering(*row) for row in txn) + + return results + @defer.inlineCallbacks def get_rooms_for_user(self, user_id, on_invalidate=None): - """Returns a set of room_ids the user is currently joined to + """Returns a set of room_ids the user is currently joined to. + + If a remote user only returns rooms this server is currently + participating in. """ rooms = yield self.get_rooms_for_user_with_stream_ordering( user_id, on_invalidate=on_invalidate @@ -1022,7 +1023,7 @@ class RoomMemberStore(RoomMemberWorkerStore, RoomMemberBackgroundUpdateStore): event.internal_metadata.stream_ordering, ) txn.call_after( - self.get_invited_rooms_for_user.invalidate, (event.state_key,) + self.get_invited_rooms_for_local_user.invalidate, (event.state_key,) ) # We update the local_invites table only if the event is "current", @@ -1064,6 +1065,27 @@ class RoomMemberStore(RoomMemberWorkerStore, RoomMemberBackgroundUpdateStore): ), ) + # We also update the `local_current_membership` table with + # latest invite info. This will usually get updated by the + # `current_state_events` handling, unless its an outlier. + if event.internal_metadata.is_outlier(): + # This should only happen for out of band memberships, so + # we add a paranoia check. + assert event.internal_metadata.is_out_of_band_membership() + + self.db.simple_upsert_txn( + txn, + table="local_current_membership", + keyvalues={ + "room_id": event.room_id, + "user_id": event.state_key, + }, + values={ + "event_id": event.event_id, + "membership": event.membership, + }, + ) + @defer.inlineCallbacks def locally_reject_invite(self, user_id, room_id): sql = ( @@ -1075,6 +1097,15 @@ class RoomMemberStore(RoomMemberWorkerStore, RoomMemberBackgroundUpdateStore): def f(txn, stream_ordering): txn.execute(sql, (stream_ordering, True, room_id, user_id)) + # We also clear this entry from `local_current_membership`. + # Ideally we'd point to a leave event, but we don't have one, so + # nevermind. + self.db.simple_delete_txn( + txn, + table="local_current_membership", + keyvalues={"room_id": room_id, "user_id": user_id}, + ) + with self._stream_id_gen.get_next() as stream_ordering: yield self.db.runInteraction("locally_reject_invite", f, stream_ordering) diff --git a/synapse/storage/data_stores/main/schema/delta/57/local_current_membership.py b/synapse/storage/data_stores/main/schema/delta/57/local_current_membership.py new file mode 100644 index 0000000000..601c236c4a --- /dev/null +++ b/synapse/storage/data_stores/main/schema/delta/57/local_current_membership.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 New Vector Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +# We create a new table called `local_current_membership` that stores the latest +# membership state of local users in rooms, which helps track leaves/bans/etc +# even if the server has left the room (and so has deleted the room from +# `current_state_events`). This will also include outstanding invites for local +# users for rooms the server isn't in. +# +# If the server isn't and hasn't been in the room then it will only include +# outsstanding invites, and not e.g. pre-emptive bans of local users. +# +# If the server later rejoins a room `local_current_membership` can simply be +# replaced with the new current state of the room (which results in the +# equivalent behaviour as if the server had remained in the room). + + +def run_upgrade(cur, database_engine, config, *args, **kwargs): + # We need to do the insert in `run_upgrade` section as we don't have access + # to `config` in `run_create`. + + # This upgrade may take a bit of time for large servers (e.g. one minute for + # matrix.org) but means we avoid a lots of book keeping required to do it as + # a background update. + + # We check if the `current_state_events.membership` is up to date by + # checking if the relevant background update has finished. If it has + # finished we can avoid doing a join against `room_memberships`, which + # speesd things up. + cur.execute( + """SELECT 1 FROM background_updates + WHERE update_name = 'current_state_events_membership' + """ + ) + current_state_membership_up_to_date = not bool(cur.fetchone()) + + # Cheekily drop and recreate indices, as that is faster. + cur.execute("DROP INDEX local_current_membership_idx") + cur.execute("DROP INDEX local_current_membership_room_idx") + + if current_state_membership_up_to_date: + sql = """ + INSERT INTO local_current_membership (room_id, user_id, event_id, membership) + SELECT c.room_id, state_key AS user_id, event_id, c.membership + FROM current_state_events AS c + WHERE type = 'm.room.member' AND c.membership IS NOT NULL AND state_key like '%' || ? + """ + else: + # We can't rely on the membership column, so we need to join against + # `room_memberships`. + sql = """ + INSERT INTO local_current_membership (room_id, user_id, event_id, membership) + SELECT c.room_id, state_key AS user_id, event_id, r.membership + FROM current_state_events AS c + INNER JOIN room_memberships AS r USING (event_id) + WHERE type = 'm.room.member' and state_key like '%' || ? + """ + cur.execute(sql, (config.server_name,)) + + cur.execute( + "CREATE UNIQUE INDEX local_current_membership_idx ON local_current_membership(user_id, room_id)" + ) + cur.execute( + "CREATE INDEX local_current_membership_room_idx ON local_current_membership(room_id)" + ) + + +def run_create(cur, database_engine, *args, **kwargs): + cur.execute( + """ + CREATE TABLE local_current_membership ( + room_id TEXT NOT NULL, + user_id TEXT NOT NULL, + event_id TEXT NOT NULL, + membership TEXT NOT NULL + )""" + ) + + cur.execute( + "CREATE UNIQUE INDEX local_current_membership_idx ON local_current_membership(user_id, room_id)" + ) + cur.execute( + "CREATE INDEX local_current_membership_room_idx ON local_current_membership(room_id)" + ) diff --git a/synapse/storage/prepare_database.py b/synapse/storage/prepare_database.py index e70026b80a..e86984cd50 100644 --- a/synapse/storage/prepare_database.py +++ b/synapse/storage/prepare_database.py @@ -29,7 +29,7 @@ logger = logging.getLogger(__name__) # Remember to update this number every time a change is made to database # schema files, so the users will be informed on server restarts. -SCHEMA_VERSION = 56 +SCHEMA_VERSION = 57 dir_path = os.path.abspath(os.path.dirname(__file__)) diff --git a/tests/handlers/test_sync.py b/tests/handlers/test_sync.py index 758ee071a5..4cbe9784ed 100644 --- a/tests/handlers/test_sync.py +++ b/tests/handlers/test_sync.py @@ -32,8 +32,8 @@ class SyncTestCase(tests.unittest.HomeserverTestCase): def test_wait_for_sync_for_user_auth_blocking(self): - user_id1 = "@user1:server" - user_id2 = "@user2:server" + user_id1 = "@user1:test" + user_id2 = "@user2:test" sync_config = self._generate_sync_config(user_id1) self.reactor.advance(100) # So we get not 0 time diff --git a/tests/replication/slave/storage/test_events.py b/tests/replication/slave/storage/test_events.py index b68e9fe082..b1b037006d 100644 --- a/tests/replication/slave/storage/test_events.py +++ b/tests/replication/slave/storage/test_events.py @@ -115,13 +115,13 @@ class SlavedEventStoreTestCase(BaseSlavedStoreTestCase): def test_invites(self): self.persist(type="m.room.create", key="", creator=USER_ID) - self.check("get_invited_rooms_for_user", [USER_ID_2], []) + self.check("get_invited_rooms_for_local_user", [USER_ID_2], []) event = self.persist(type="m.room.member", key=USER_ID_2, membership="invite") self.replicate() self.check( - "get_invited_rooms_for_user", + "get_invited_rooms_for_local_user", [USER_ID_2], [ RoomsForUser( diff --git a/tests/rest/client/v2_alpha/test_account.py b/tests/rest/client/v2_alpha/test_account.py index 0f51895b81..c3facc00eb 100644 --- a/tests/rest/client/v2_alpha/test_account.py +++ b/tests/rest/client/v2_alpha/test_account.py @@ -285,7 +285,9 @@ class DeactivateTestCase(unittest.HomeserverTestCase): ) # Make sure the invite is here. - pending_invites = self.get_success(store.get_invited_rooms_for_user(invitee_id)) + pending_invites = self.get_success( + store.get_invited_rooms_for_local_user(invitee_id) + ) self.assertEqual(len(pending_invites), 1, pending_invites) self.assertEqual(pending_invites[0].room_id, room_id, pending_invites) @@ -293,12 +295,16 @@ class DeactivateTestCase(unittest.HomeserverTestCase): self.deactivate(invitee_id, invitee_tok) # Check that the invite isn't there anymore. - pending_invites = self.get_success(store.get_invited_rooms_for_user(invitee_id)) + pending_invites = self.get_success( + store.get_invited_rooms_for_local_user(invitee_id) + ) self.assertEqual(len(pending_invites), 0, pending_invites) # Check that the membership of @invitee:test in the room is now "leave". memberships = self.get_success( - store.get_rooms_for_user_where_membership_is(invitee_id, [Membership.LEAVE]) + store.get_rooms_for_local_user_where_membership_is( + invitee_id, [Membership.LEAVE] + ) ) self.assertEqual(len(memberships), 1, memberships) self.assertEqual(memberships[0].room_id, room_id, memberships) diff --git a/tests/rest/client/v2_alpha/test_sync.py b/tests/rest/client/v2_alpha/test_sync.py index 661c1f88b9..9c13a13786 100644 --- a/tests/rest/client/v2_alpha/test_sync.py +++ b/tests/rest/client/v2_alpha/test_sync.py @@ -15,8 +15,6 @@ # limitations under the License. import json -from mock import Mock - import synapse.rest.admin from synapse.api.constants import EventContentFields, EventTypes from synapse.rest.client.v1 import login, room @@ -36,13 +34,6 @@ class FilterTestCase(unittest.HomeserverTestCase): sync.register_servlets, ] - def make_homeserver(self, reactor, clock): - - hs = self.setup_test_homeserver( - "red", http_client=None, federation_client=Mock() - ) - return hs - def test_sync_argless(self): request, channel = self.make_request("GET", "/sync") self.render(request) diff --git a/tests/storage/test_roommember.py b/tests/storage/test_roommember.py index 7840f63fe3..00df0ea68e 100644 --- a/tests/storage/test_roommember.py +++ b/tests/storage/test_roommember.py @@ -57,7 +57,7 @@ class RoomMemberStoreTestCase(unittest.HomeserverTestCase): self.room = self.helper.create_room_as(self.u_alice, tok=self.t_alice) rooms_for_user = self.get_success( - self.store.get_rooms_for_user_where_membership_is( + self.store.get_rooms_for_local_user_where_membership_is( self.u_alice, [Membership.JOIN] ) ) From 8f5d7302acb7f6d15ba7051df7fd7fda7375a29e Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 15 Jan 2020 15:58:55 +0000 Subject: [PATCH 0822/1623] Implement RedirectException (#6687) Allow REST endpoint implemnentations to raise a RedirectException, which will redirect the user's browser to a given location. --- changelog.d/6687.misc | 1 + synapse/api/errors.py | 27 ++++++++++++++- synapse/http/server.py | 13 ++++--- tests/test_server.py | 79 ++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 113 insertions(+), 7 deletions(-) create mode 100644 changelog.d/6687.misc diff --git a/changelog.d/6687.misc b/changelog.d/6687.misc new file mode 100644 index 0000000000..deb0454602 --- /dev/null +++ b/changelog.d/6687.misc @@ -0,0 +1 @@ +Allow REST endpoint implementations to raise a RedirectException, which will redirect the user's browser to a given location. diff --git a/synapse/api/errors.py b/synapse/api/errors.py index 9e9844b47c..1c9456e583 100644 --- a/synapse/api/errors.py +++ b/synapse/api/errors.py @@ -17,13 +17,15 @@ """Contains exceptions and error codes.""" import logging -from typing import Dict +from typing import Dict, List from six import iteritems from six.moves import http_client from canonicaljson import json +from twisted.web import http + logger = logging.getLogger(__name__) @@ -80,6 +82,29 @@ class CodeMessageException(RuntimeError): self.msg = msg +class RedirectException(CodeMessageException): + """A pseudo-error indicating that we want to redirect the client to a different + location + + Attributes: + cookies: a list of set-cookies values to add to the response. For example: + b"sessionId=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT" + """ + + def __init__(self, location: bytes, http_code: int = http.FOUND): + """ + + Args: + location: the URI to redirect to + http_code: the HTTP response code + """ + msg = "Redirect to %s" % (location.decode("utf-8"),) + super().__init__(code=http_code, msg=msg) + self.location = location + + self.cookies = [] # type: List[bytes] + + class SynapseError(CodeMessageException): """A base exception type for matrix errors which have an errcode and error message (as well as an HTTP status code). diff --git a/synapse/http/server.py b/synapse/http/server.py index 943d12c907..04bc2385a2 100644 --- a/synapse/http/server.py +++ b/synapse/http/server.py @@ -14,8 +14,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -import cgi import collections +import html import http.client import logging import types @@ -36,6 +36,7 @@ import synapse.metrics from synapse.api.errors import ( CodeMessageException, Codes, + RedirectException, SynapseError, UnrecognizedRequestError, ) @@ -153,14 +154,18 @@ def _return_html_error(f, request): Args: f (twisted.python.failure.Failure): - request (twisted.web.iweb.IRequest): + request (twisted.web.server.Request): """ if f.check(CodeMessageException): cme = f.value code = cme.code msg = cme.msg - if isinstance(cme, SynapseError): + if isinstance(cme, RedirectException): + logger.info("%s redirect to %s", request, cme.location) + request.setHeader(b"location", cme.location) + request.cookies.extend(cme.cookies) + elif isinstance(cme, SynapseError): logger.info("%s SynapseError: %s - %s", request, code, msg) else: logger.error( @@ -178,7 +183,7 @@ def _return_html_error(f, request): exc_info=(f.type, f.value, f.getTracebackObject()), ) - body = HTML_ERROR_TEMPLATE.format(code=code, msg=cgi.escape(msg)).encode("utf-8") + body = HTML_ERROR_TEMPLATE.format(code=code, msg=html.escape(msg)).encode("utf-8") request.setResponseCode(code) request.setHeader(b"Content-Type", b"text/html; charset=utf-8") request.setHeader(b"Content-Length", b"%i" % (len(body),)) diff --git a/tests/test_server.py b/tests/test_server.py index 98fef21d55..0d57eed268 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -23,8 +23,12 @@ from twisted.test.proto_helpers import AccumulatingProtocol from twisted.web.resource import Resource from twisted.web.server import NOT_DONE_YET -from synapse.api.errors import Codes, SynapseError -from synapse.http.server import JsonResource +from synapse.api.errors import Codes, RedirectException, SynapseError +from synapse.http.server import ( + DirectServeResource, + JsonResource, + wrap_html_request_handler, +) from synapse.http.site import SynapseSite, logger from synapse.logging.context import make_deferred_yieldable from synapse.util import Clock @@ -164,6 +168,77 @@ class JsonResourceTests(unittest.TestCase): self.assertEqual(channel.json_body["errcode"], "M_UNRECOGNIZED") +class WrapHtmlRequestHandlerTests(unittest.TestCase): + class TestResource(DirectServeResource): + callback = None + + @wrap_html_request_handler + async def _async_render_GET(self, request): + return await self.callback(request) + + def setUp(self): + self.reactor = ThreadedMemoryReactorClock() + + def test_good_response(self): + def callback(request): + request.write(b"response") + request.finish() + + res = WrapHtmlRequestHandlerTests.TestResource() + res.callback = callback + + request, channel = make_request(self.reactor, b"GET", b"/path") + render(request, res, self.reactor) + + self.assertEqual(channel.result["code"], b"200") + body = channel.result["body"] + self.assertEqual(body, b"response") + + def test_redirect_exception(self): + """ + If the callback raises a RedirectException, it is turned into a 30x + with the right location. + """ + + def callback(request, **kwargs): + raise RedirectException(b"/look/an/eagle", 301) + + res = WrapHtmlRequestHandlerTests.TestResource() + res.callback = callback + + request, channel = make_request(self.reactor, b"GET", b"/path") + render(request, res, self.reactor) + + self.assertEqual(channel.result["code"], b"301") + headers = channel.result["headers"] + location_headers = [v for k, v in headers if k == b"Location"] + self.assertEqual(location_headers, [b"/look/an/eagle"]) + + def test_redirect_exception_with_cookie(self): + """ + If the callback raises a RedirectException which sets a cookie, that is + returned too + """ + + def callback(request, **kwargs): + e = RedirectException(b"/no/over/there", 304) + e.cookies.append(b"session=yespls") + raise e + + res = WrapHtmlRequestHandlerTests.TestResource() + res.callback = callback + + request, channel = make_request(self.reactor, b"GET", b"/path") + render(request, res, self.reactor) + + self.assertEqual(channel.result["code"], b"304") + headers = channel.result["headers"] + location_headers = [v for k, v in headers if k == b"Location"] + self.assertEqual(location_headers, [b"/no/over/there"]) + cookies_headers = [v for k, v in headers if k == b"Set-Cookie"] + self.assertEqual(cookies_headers, [b"session=yespls"]) + + class SiteTestCase(unittest.HomeserverTestCase): def test_lose_connection(self): """ From edc244eec429d587eee28e336e0baae9f9de0e0a Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Wed, 15 Jan 2020 18:05:18 +0000 Subject: [PATCH 0823/1623] Remove duplicate session check in web fallback servlet (#6702) --- changelog.d/6702.misc | 1 + synapse/rest/client/v2_alpha/auth.py | 4 ---- 2 files changed, 1 insertion(+), 4 deletions(-) create mode 100644 changelog.d/6702.misc diff --git a/changelog.d/6702.misc b/changelog.d/6702.misc new file mode 100644 index 0000000000..f7bc98409c --- /dev/null +++ b/changelog.d/6702.misc @@ -0,0 +1 @@ +Remove duplicate check for the `session` query parameter on the `/auth/xxx/fallback/web` Client-Server endpoint. \ No newline at end of file diff --git a/synapse/rest/client/v2_alpha/auth.py b/synapse/rest/client/v2_alpha/auth.py index 7a256b6ecb..50e080673b 100644 --- a/synapse/rest/client/v2_alpha/auth.py +++ b/synapse/rest/client/v2_alpha/auth.py @@ -206,10 +206,6 @@ class AuthRestServlet(RestServlet): return None elif stagetype == LoginType.TERMS: - if ("session" not in request.args or len(request.args["session"])) == 0: - raise SynapseError(400, "No session supplied") - - session = request.args["session"][0] authdict = {"session": session} success = await self.auth_handler.add_oob_auth( From 19a1aac48cc83fe41287a97bb0a96280a0e8c565 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 15 Jan 2020 18:13:47 +0000 Subject: [PATCH 0824/1623] Fix purge_room admin API (#6711) --- changelog.d/6711.bugfix | 1 + synapse/storage/purge_events.py | 2 +- tests/rest/admin/test_admin.py | 4 +--- 3 files changed, 3 insertions(+), 4 deletions(-) create mode 100644 changelog.d/6711.bugfix diff --git a/changelog.d/6711.bugfix b/changelog.d/6711.bugfix new file mode 100644 index 0000000000..c70506bd88 --- /dev/null +++ b/changelog.d/6711.bugfix @@ -0,0 +1 @@ +Fix `purge_room` admin API. diff --git a/synapse/storage/purge_events.py b/synapse/storage/purge_events.py index d6a7bd7834..fdc0abf5cf 100644 --- a/synapse/storage/purge_events.py +++ b/synapse/storage/purge_events.py @@ -34,7 +34,7 @@ class PurgeEventsStorage(object): """ state_groups_to_delete = yield self.stores.main.purge_room(room_id) - yield self.stores.main.purge_room_state(room_id, state_groups_to_delete) + yield self.stores.state.purge_room_state(room_id, state_groups_to_delete) @defer.inlineCallbacks def purge_history(self, room_id, token, delete_local_events): diff --git a/tests/rest/admin/test_admin.py b/tests/rest/admin/test_admin.py index 7a7e898843..f3b4a31e21 100644 --- a/tests/rest/admin/test_admin.py +++ b/tests/rest/admin/test_admin.py @@ -337,7 +337,7 @@ class PurgeRoomTestCase(unittest.HomeserverTestCase): "local_invites", "room_account_data", "room_tags", - "state_groups", + # "state_groups", # Current impl leaves orphaned state groups around. "state_groups_state", ): count = self.get_success( @@ -351,8 +351,6 @@ class PurgeRoomTestCase(unittest.HomeserverTestCase): self.assertEqual(count, 0, msg="Rows not purged in {}".format(table)) - test_purge_room.skip = "Disabled because it's currently broken" - class QuarantineMediaTestCase(unittest.HomeserverTestCase): """Test /quarantine_media admin API. From 855af069a494f826ef941d722c811287b3fc4a8c Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 15 Jan 2020 18:56:18 +0000 Subject: [PATCH 0825/1623] Fix instantiation of message retention purge jobs When figuring out which topological token to start a purge job at, we need to do the following: 1. Figure out a timestamp before which events will be purged 2. Select the first stream ordering after that timestamp 3. Select info about the first event after that stream ordering 4. Build a topological token from that info In some situations (e.g. quiet rooms with a short max_lifetime), there might not be an event after the stream ordering at step 3, therefore we abort the purge with the error `No event found`. To mitigate that, this patch fetches the first event _before_ the stream ordering, instead of after. --- synapse/handlers/pagination.py | 2 +- synapse/storage/data_stores/main/stream.py | 59 +++++++++++++++++----- 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/synapse/handlers/pagination.py b/synapse/handlers/pagination.py index 00a6afc963..3ee6a091c5 100644 --- a/synapse/handlers/pagination.py +++ b/synapse/handlers/pagination.py @@ -156,7 +156,7 @@ class PaginationHandler(object): stream_ordering = yield self.store.find_first_stream_ordering_after_ts(ts) - r = yield self.store.get_room_event_after_stream_ordering( + r = yield self.store.get_room_event_before_stream_ordering( room_id, stream_ordering, ) if not r: diff --git a/synapse/storage/data_stores/main/stream.py b/synapse/storage/data_stores/main/stream.py index 140da8dad6..223ce7fedb 100644 --- a/synapse/storage/data_stores/main/stream.py +++ b/synapse/storage/data_stores/main/stream.py @@ -536,20 +536,55 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): Deferred[(int, int, str)]: (stream ordering, topological ordering, event_id) """ + return self.db.runInteraction( + "get_room_event_after_stream_ordering", + self.get_room_event_around_stream_ordering_txn, + room_id, stream_ordering, "f", + ) - def _f(txn): - sql = ( - "SELECT stream_ordering, topological_ordering, event_id" - " FROM events" - " WHERE room_id = ? AND stream_ordering >= ?" - " AND NOT outlier" - " ORDER BY stream_ordering" - " LIMIT 1" - ) - txn.execute(sql, (room_id, stream_ordering)) - return txn.fetchone() + def get_room_event_before_stream_ordering(self, room_id, stream_ordering): + """Gets details of the first event in a room at or before a stream ordering - return self.db.runInteraction("get_room_event_after_stream_ordering", _f) + Args: + room_id (str): + stream_ordering (int): + + Returns: + Deferred[(int, int, str)]: + (stream ordering, topological ordering, event_id) + """ + return self.db.runInteraction( + "get_room_event_before_stream_ordering", + self.get_room_event_around_stream_ordering_txn, + room_id, stream_ordering, "f", + ) + + def get_room_event_around_stream_ordering_txn( + self, txn, room_id, stream_ordering, dir="f" + ): + """Gets details of the first event in a room at or either after or before a + stream ordering, depending on the provided direction. + + Args: + room_id (str): + stream_ordering (int): + dir (str): Direction in which we're looking towards in the room's history, + either "f" (forward) or "b" (backward). + + Returns: + Deferred[(int, int, str)]: + (stream ordering, topological ordering, event_id) + """ + sql = ( + "SELECT stream_ordering, topological_ordering, event_id" + " FROM events" + " WHERE room_id = ? AND stream_ordering %s ?" + " AND NOT outlier" + " ORDER BY stream_ordering" + " LIMIT 1" + ) % ("<=" if dir == "b" else ">=",) + txn.execute(sql, (room_id, stream_ordering)) + return txn.fetchone() @defer.inlineCallbacks def get_room_events_max_id(self, room_id=None): From 83635882379ecddb1509ea3d071eefdedefb647e Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 15 Jan 2020 19:13:22 +0000 Subject: [PATCH 0826/1623] Fix typo --- synapse/storage/data_stores/main/stream.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/storage/data_stores/main/stream.py b/synapse/storage/data_stores/main/stream.py index 223ce7fedb..9fa5e1f203 100644 --- a/synapse/storage/data_stores/main/stream.py +++ b/synapse/storage/data_stores/main/stream.py @@ -556,7 +556,7 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): return self.db.runInteraction( "get_room_event_before_stream_ordering", self.get_room_event_around_stream_ordering_txn, - room_id, stream_ordering, "f", + room_id, stream_ordering, "b", ) def get_room_event_around_stream_ordering_txn( From 066b9f52b80c172eec6074ca01fb24670200fd80 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 15 Jan 2020 19:32:47 +0000 Subject: [PATCH 0827/1623] Correctly order when selecting before stream ordering --- synapse/storage/data_stores/main/stream.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/synapse/storage/data_stores/main/stream.py b/synapse/storage/data_stores/main/stream.py index 9fa5e1f203..451f38296b 100644 --- a/synapse/storage/data_stores/main/stream.py +++ b/synapse/storage/data_stores/main/stream.py @@ -580,9 +580,12 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): " FROM events" " WHERE room_id = ? AND stream_ordering %s ?" " AND NOT outlier" - " ORDER BY stream_ordering" + " ORDER BY stream_ordering %s" " LIMIT 1" - ) % ("<=" if dir == "b" else ">=",) + ) % ( + "<=" if dir == "b" else ">=", + "DESC" if dir == "b" else "ASC", + ) txn.execute(sql, (room_id, stream_ordering)) return txn.fetchone() From 914e73cdd9053d6fd050e5ad04910db74a7b5cd9 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 15 Jan 2020 19:36:19 +0000 Subject: [PATCH 0828/1623] Changelog --- changelog.d/6713.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6713.bugfix diff --git a/changelog.d/6713.bugfix b/changelog.d/6713.bugfix new file mode 100644 index 0000000000..3924f1ad79 --- /dev/null +++ b/changelog.d/6713.bugfix @@ -0,0 +1 @@ +Fix a bug causing Synapse to not always purge quiet rooms with a low `max_lifetime` in their message retention policies. From 48e57a6452be3fef4372832f9e8f8f630325a648 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 15 Jan 2020 19:40:46 +0000 Subject: [PATCH 0829/1623] Rename changelog --- changelog.d/{6713.bugfix => 6714.bugfix} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/{6713.bugfix => 6714.bugfix} (100%) diff --git a/changelog.d/6713.bugfix b/changelog.d/6714.bugfix similarity index 100% rename from changelog.d/6713.bugfix rename to changelog.d/6714.bugfix From 48c3a96886de64f3141ad68b8163cd2fc0c197ff Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 16 Jan 2020 09:16:12 +0000 Subject: [PATCH 0830/1623] Port synapse.replication.tcp to async/await (#6666) * Port synapse.replication.tcp to async/await * Newsfile * Correctly document type of on_ functions as async * Don't be overenthusiastic with the asyncing.... --- changelog.d/6666.misc | 1 + synapse/app/admin_cmd.py | 3 +- synapse/app/appservice.py | 5 +- synapse/app/federation_sender.py | 5 +- synapse/app/pusher.py | 5 +- synapse/app/synchrotron.py | 5 +- synapse/app/user_dir.py | 5 +- synapse/federation/send_queue.py | 4 +- synapse/handlers/typing.py | 2 +- synapse/replication/tcp/client.py | 11 ++-- synapse/replication/tcp/protocol.py | 72 ++++++++++------------- synapse/replication/tcp/resource.py | 31 +++++----- synapse/replication/tcp/streams/_base.py | 25 ++++---- synapse/replication/tcp/streams/events.py | 9 +-- tests/replication/tcp/streams/_base.py | 2 +- 15 files changed, 80 insertions(+), 105 deletions(-) create mode 100644 changelog.d/6666.misc diff --git a/changelog.d/6666.misc b/changelog.d/6666.misc new file mode 100644 index 0000000000..e79c23d2d2 --- /dev/null +++ b/changelog.d/6666.misc @@ -0,0 +1 @@ +Port `synapse.replication.tcp` to async/await. diff --git a/synapse/app/admin_cmd.py b/synapse/app/admin_cmd.py index 8e36bc57d3..1c7c6ec0c8 100644 --- a/synapse/app/admin_cmd.py +++ b/synapse/app/admin_cmd.py @@ -84,8 +84,7 @@ class AdminCmdServer(HomeServer): class AdminCmdReplicationHandler(ReplicationClientHandler): - @defer.inlineCallbacks - def on_rdata(self, stream_name, token, rows): + async def on_rdata(self, stream_name, token, rows): pass def get_streams_to_replicate(self): diff --git a/synapse/app/appservice.py b/synapse/app/appservice.py index e82e0f11e3..2217d4a4fb 100644 --- a/synapse/app/appservice.py +++ b/synapse/app/appservice.py @@ -115,9 +115,8 @@ class ASReplicationHandler(ReplicationClientHandler): super(ASReplicationHandler, self).__init__(hs.get_datastore()) self.appservice_handler = hs.get_application_service_handler() - @defer.inlineCallbacks - def on_rdata(self, stream_name, token, rows): - yield super(ASReplicationHandler, self).on_rdata(stream_name, token, rows) + async def on_rdata(self, stream_name, token, rows): + await super(ASReplicationHandler, self).on_rdata(stream_name, token, rows) if stream_name == "events": max_stream_id = self.store.get_room_max_stream_ordering() diff --git a/synapse/app/federation_sender.py b/synapse/app/federation_sender.py index 83c436229c..a57cf991ac 100644 --- a/synapse/app/federation_sender.py +++ b/synapse/app/federation_sender.py @@ -145,9 +145,8 @@ class FederationSenderReplicationHandler(ReplicationClientHandler): super(FederationSenderReplicationHandler, self).__init__(hs.get_datastore()) self.send_handler = FederationSenderHandler(hs, self) - @defer.inlineCallbacks - def on_rdata(self, stream_name, token, rows): - yield super(FederationSenderReplicationHandler, self).on_rdata( + async def on_rdata(self, stream_name, token, rows): + await super(FederationSenderReplicationHandler, self).on_rdata( stream_name, token, rows ) self.send_handler.process_replication_rows(stream_name, token, rows) diff --git a/synapse/app/pusher.py b/synapse/app/pusher.py index 09e639040a..e46b6ac598 100644 --- a/synapse/app/pusher.py +++ b/synapse/app/pusher.py @@ -141,9 +141,8 @@ class PusherReplicationHandler(ReplicationClientHandler): self.pusher_pool = hs.get_pusherpool() - @defer.inlineCallbacks - def on_rdata(self, stream_name, token, rows): - yield super(PusherReplicationHandler, self).on_rdata(stream_name, token, rows) + async def on_rdata(self, stream_name, token, rows): + await super(PusherReplicationHandler, self).on_rdata(stream_name, token, rows) run_in_background(self.poke_pushers, stream_name, token, rows) @defer.inlineCallbacks diff --git a/synapse/app/synchrotron.py b/synapse/app/synchrotron.py index 03031ee34d..3218da07bd 100644 --- a/synapse/app/synchrotron.py +++ b/synapse/app/synchrotron.py @@ -358,9 +358,8 @@ class SyncReplicationHandler(ReplicationClientHandler): self.presence_handler = hs.get_presence_handler() self.notifier = hs.get_notifier() - @defer.inlineCallbacks - def on_rdata(self, stream_name, token, rows): - yield super(SyncReplicationHandler, self).on_rdata(stream_name, token, rows) + async def on_rdata(self, stream_name, token, rows): + await super(SyncReplicationHandler, self).on_rdata(stream_name, token, rows) run_in_background(self.process_and_notify, stream_name, token, rows) def get_streams_to_replicate(self): diff --git a/synapse/app/user_dir.py b/synapse/app/user_dir.py index 1257098f92..ba536d6f04 100644 --- a/synapse/app/user_dir.py +++ b/synapse/app/user_dir.py @@ -172,9 +172,8 @@ class UserDirectoryReplicationHandler(ReplicationClientHandler): super(UserDirectoryReplicationHandler, self).__init__(hs.get_datastore()) self.user_directory = hs.get_user_directory_handler() - @defer.inlineCallbacks - def on_rdata(self, stream_name, token, rows): - yield super(UserDirectoryReplicationHandler, self).on_rdata( + async def on_rdata(self, stream_name, token, rows): + await super(UserDirectoryReplicationHandler, self).on_rdata( stream_name, token, rows ) if stream_name == EventsStream.NAME: diff --git a/synapse/federation/send_queue.py b/synapse/federation/send_queue.py index ced4925a98..174f6e42be 100644 --- a/synapse/federation/send_queue.py +++ b/synapse/federation/send_queue.py @@ -259,7 +259,9 @@ class FederationRemoteSendQueue(object): def federation_ack(self, token): self._clear_queue_before_pos(token) - def get_replication_rows(self, from_token, to_token, limit, federation_ack=None): + async def get_replication_rows( + self, from_token, to_token, limit, federation_ack=None + ): """Get rows to be sent over federation between the two tokens Args: diff --git a/synapse/handlers/typing.py b/synapse/handlers/typing.py index b635c339ed..d5ca9cb07b 100644 --- a/synapse/handlers/typing.py +++ b/synapse/handlers/typing.py @@ -257,7 +257,7 @@ class TypingHandler(object): "typing_key", self._latest_room_serial, rooms=[member.room_id] ) - def get_all_typing_updates(self, last_id, current_id): + async def get_all_typing_updates(self, last_id, current_id): if last_id == current_id: return [] diff --git a/synapse/replication/tcp/client.py b/synapse/replication/tcp/client.py index aa7fd90e26..52a0aefe68 100644 --- a/synapse/replication/tcp/client.py +++ b/synapse/replication/tcp/client.py @@ -110,7 +110,7 @@ class ReplicationClientHandler(AbstractReplicationClientHandler): port = hs.config.worker_replication_port hs.get_reactor().connectTCP(host, port, self.factory) - def on_rdata(self, stream_name, token, rows): + async def on_rdata(self, stream_name, token, rows): """Called to handle a batch of replication data with a given stream token. By default this just pokes the slave store. Can be overridden in subclasses to @@ -121,20 +121,17 @@ class ReplicationClientHandler(AbstractReplicationClientHandler): token (int): stream token for this batch of rows rows (list): a list of Stream.ROW_TYPE objects as returned by Stream.parse_row. - - Returns: - Deferred|None """ logger.debug("Received rdata %s -> %s", stream_name, token) - return self.store.process_replication_rows(stream_name, token, rows) + self.store.process_replication_rows(stream_name, token, rows) - def on_position(self, stream_name, token): + async def on_position(self, stream_name, token): """Called when we get new position data. By default this just pokes the slave store. Can be overriden in subclasses to handle more. """ - return self.store.process_replication_rows(stream_name, token, []) + self.store.process_replication_rows(stream_name, token, []) def on_sync(self, data): """When we received a SYNC we wake up any deferreds that were waiting diff --git a/synapse/replication/tcp/protocol.py b/synapse/replication/tcp/protocol.py index db0353c996..5f4bdf84d2 100644 --- a/synapse/replication/tcp/protocol.py +++ b/synapse/replication/tcp/protocol.py @@ -81,12 +81,11 @@ from synapse.replication.tcp.commands import ( SyncCommand, UserSyncCommand, ) +from synapse.replication.tcp.streams import STREAMS_MAP from synapse.types import Collection from synapse.util import Clock from synapse.util.stringutils import random_string -from .streams import STREAMS_MAP - connection_close_counter = Counter( "synapse_replication_tcp_protocol_close_reason", "", ["reason_type"] ) @@ -241,19 +240,16 @@ class BaseReplicationStreamProtocol(LineOnlyReceiver): "replication-" + cmd.get_logcontext_id(), self.handle_command, cmd ) - def handle_command(self, cmd): + async def handle_command(self, cmd: Command): """Handle a command we have received over the replication stream. - By default delegates to on_ + By default delegates to on_, which should return an awaitable. Args: - cmd (synapse.replication.tcp.commands.Command): received command - - Returns: - Deferred + cmd: received command """ handler = getattr(self, "on_%s" % (cmd.NAME,)) - return handler(cmd) + await handler(cmd) def close(self): logger.warning("[%s] Closing connection", self.id()) @@ -326,10 +322,10 @@ class BaseReplicationStreamProtocol(LineOnlyReceiver): for cmd in pending: self.send_command(cmd) - def on_PING(self, line): + async def on_PING(self, line): self.received_ping = True - def on_ERROR(self, cmd): + async def on_ERROR(self, cmd): logger.error("[%s] Remote reported error: %r", self.id(), cmd.data) def pauseProducing(self): @@ -429,16 +425,16 @@ class ServerReplicationStreamProtocol(BaseReplicationStreamProtocol): BaseReplicationStreamProtocol.connectionMade(self) self.streamer.new_connection(self) - def on_NAME(self, cmd): + async def on_NAME(self, cmd): logger.info("[%s] Renamed to %r", self.id(), cmd.data) self.name = cmd.data - def on_USER_SYNC(self, cmd): - return self.streamer.on_user_sync( + async def on_USER_SYNC(self, cmd): + await self.streamer.on_user_sync( self.conn_id, cmd.user_id, cmd.is_syncing, cmd.last_sync_ms ) - def on_REPLICATE(self, cmd): + async def on_REPLICATE(self, cmd): stream_name = cmd.stream_name token = cmd.token @@ -449,23 +445,23 @@ class ServerReplicationStreamProtocol(BaseReplicationStreamProtocol): for stream in iterkeys(self.streamer.streams_by_name) ] - return make_deferred_yieldable( + await make_deferred_yieldable( defer.gatherResults(deferreds, consumeErrors=True) ) else: - return self.subscribe_to_stream(stream_name, token) + await self.subscribe_to_stream(stream_name, token) - def on_FEDERATION_ACK(self, cmd): - return self.streamer.federation_ack(cmd.token) + async def on_FEDERATION_ACK(self, cmd): + self.streamer.federation_ack(cmd.token) - def on_REMOVE_PUSHER(self, cmd): - return self.streamer.on_remove_pusher(cmd.app_id, cmd.push_key, cmd.user_id) + async def on_REMOVE_PUSHER(self, cmd): + await self.streamer.on_remove_pusher(cmd.app_id, cmd.push_key, cmd.user_id) - def on_INVALIDATE_CACHE(self, cmd): - return self.streamer.on_invalidate_cache(cmd.cache_func, cmd.keys) + async def on_INVALIDATE_CACHE(self, cmd): + self.streamer.on_invalidate_cache(cmd.cache_func, cmd.keys) - def on_USER_IP(self, cmd): - return self.streamer.on_user_ip( + async def on_USER_IP(self, cmd): + self.streamer.on_user_ip( cmd.user_id, cmd.access_token, cmd.ip, @@ -474,8 +470,7 @@ class ServerReplicationStreamProtocol(BaseReplicationStreamProtocol): cmd.last_seen, ) - @defer.inlineCallbacks - def subscribe_to_stream(self, stream_name, token): + async def subscribe_to_stream(self, stream_name, token): """Subscribe the remote to a stream. This invloves checking if they've missed anything and sending those @@ -487,7 +482,7 @@ class ServerReplicationStreamProtocol(BaseReplicationStreamProtocol): try: # Get missing updates - updates, current_token = yield self.streamer.get_stream_updates( + updates, current_token = await self.streamer.get_stream_updates( stream_name, token ) @@ -572,7 +567,7 @@ class AbstractReplicationClientHandler(metaclass=abc.ABCMeta): """ @abc.abstractmethod - def on_rdata(self, stream_name, token, rows): + async def on_rdata(self, stream_name, token, rows): """Called to handle a batch of replication data with a given stream token. Args: @@ -580,14 +575,11 @@ class AbstractReplicationClientHandler(metaclass=abc.ABCMeta): token (int): stream token for this batch of rows rows (list): a list of Stream.ROW_TYPE objects as returned by Stream.parse_row. - - Returns: - Deferred|None """ raise NotImplementedError() @abc.abstractmethod - def on_position(self, stream_name, token): + async def on_position(self, stream_name, token): """Called when we get new position data.""" raise NotImplementedError() @@ -676,12 +668,12 @@ class ClientReplicationStreamProtocol(BaseReplicationStreamProtocol): if not self.streams_connecting: self.handler.finished_connecting() - def on_SERVER(self, cmd): + async def on_SERVER(self, cmd): if cmd.data != self.server_name: logger.error("[%s] Connected to wrong remote: %r", self.id(), cmd.data) self.send_error("Wrong remote") - def on_RDATA(self, cmd): + async def on_RDATA(self, cmd): stream_name = cmd.stream_name inbound_rdata_count.labels(stream_name).inc() @@ -701,19 +693,19 @@ class ClientReplicationStreamProtocol(BaseReplicationStreamProtocol): # Check if this is the last of a batch of updates rows = self.pending_batches.pop(stream_name, []) rows.append(row) - return self.handler.on_rdata(stream_name, cmd.token, rows) + await self.handler.on_rdata(stream_name, cmd.token, rows) - def on_POSITION(self, cmd): + async def on_POSITION(self, cmd): # When we get a `POSITION` command it means we've finished getting # missing updates for the given stream, and are now up to date. self.streams_connecting.discard(cmd.stream_name) if not self.streams_connecting: self.handler.finished_connecting() - return self.handler.on_position(cmd.stream_name, cmd.token) + await self.handler.on_position(cmd.stream_name, cmd.token) - def on_SYNC(self, cmd): - return self.handler.on_sync(cmd.data) + async def on_SYNC(self, cmd): + self.handler.on_sync(cmd.data) def replicate(self, stream_name, token): """Send the subscription request to the server diff --git a/synapse/replication/tcp/resource.py b/synapse/replication/tcp/resource.py index cbfdaf5773..b1752e88cd 100644 --- a/synapse/replication/tcp/resource.py +++ b/synapse/replication/tcp/resource.py @@ -23,7 +23,6 @@ from six import itervalues from prometheus_client import Counter -from twisted.internet import defer from twisted.internet.protocol import Factory from synapse.metrics import LaterGauge @@ -155,8 +154,7 @@ class ReplicationStreamer(object): run_as_background_process("replication_notifier", self._run_notifier_loop) - @defer.inlineCallbacks - def _run_notifier_loop(self): + async def _run_notifier_loop(self): self.is_looping = True try: @@ -185,7 +183,7 @@ class ReplicationStreamer(object): continue if self._replication_torture_level: - yield self.clock.sleep( + await self.clock.sleep( self._replication_torture_level / 1000.0 ) @@ -196,7 +194,7 @@ class ReplicationStreamer(object): stream.upto_token, ) try: - updates, current_token = yield stream.get_updates() + updates, current_token = await stream.get_updates() except Exception: logger.info("Failed to handle stream %s", stream.NAME) raise @@ -233,7 +231,7 @@ class ReplicationStreamer(object): self.is_looping = False @measure_func("repl.get_stream_updates") - def get_stream_updates(self, stream_name, token): + async def get_stream_updates(self, stream_name, token): """For a given stream get all updates since token. This is called when a client first subscribes to a stream. """ @@ -241,7 +239,7 @@ class ReplicationStreamer(object): if not stream: raise Exception("unknown stream %s", stream_name) - return stream.get_updates_since(token) + return await stream.get_updates_since(token) @measure_func("repl.federation_ack") def federation_ack(self, token): @@ -252,22 +250,20 @@ class ReplicationStreamer(object): self.federation_sender.federation_ack(token) @measure_func("repl.on_user_sync") - @defer.inlineCallbacks - def on_user_sync(self, conn_id, user_id, is_syncing, last_sync_ms): + async def on_user_sync(self, conn_id, user_id, is_syncing, last_sync_ms): """A client has started/stopped syncing on a worker. """ user_sync_counter.inc() - yield self.presence_handler.update_external_syncs_row( + await self.presence_handler.update_external_syncs_row( conn_id, user_id, is_syncing, last_sync_ms ) @measure_func("repl.on_remove_pusher") - @defer.inlineCallbacks - def on_remove_pusher(self, app_id, push_key, user_id): + async def on_remove_pusher(self, app_id, push_key, user_id): """A client has asked us to remove a pusher """ remove_pusher_counter.inc() - yield self.store.delete_pusher_by_app_id_pushkey_user_id( + await self.store.delete_pusher_by_app_id_pushkey_user_id( app_id=app_id, pushkey=push_key, user_id=user_id ) @@ -281,15 +277,16 @@ class ReplicationStreamer(object): getattr(self.store, cache_func).invalidate(tuple(keys)) @measure_func("repl.on_user_ip") - @defer.inlineCallbacks - def on_user_ip(self, user_id, access_token, ip, user_agent, device_id, last_seen): + async def on_user_ip( + self, user_id, access_token, ip, user_agent, device_id, last_seen + ): """The client saw a user request """ user_ip_cache_counter.inc() - yield self.store.insert_client_ip( + await self.store.insert_client_ip( user_id, access_token, ip, user_agent, device_id, last_seen ) - yield self._server_notices_sender.on_user_ip(user_id) + await self._server_notices_sender.on_user_ip(user_id) def send_sync_to_all_connections(self, data): """Sends a SYNC command to all clients. diff --git a/synapse/replication/tcp/streams/_base.py b/synapse/replication/tcp/streams/_base.py index 4ab0334fc1..e03e77199b 100644 --- a/synapse/replication/tcp/streams/_base.py +++ b/synapse/replication/tcp/streams/_base.py @@ -19,8 +19,6 @@ import logging from collections import namedtuple from typing import Any -from twisted.internet import defer - logger = logging.getLogger(__name__) @@ -144,8 +142,7 @@ class Stream(object): self.upto_token = self.current_token() self.last_token = self.upto_token - @defer.inlineCallbacks - def get_updates(self): + async def get_updates(self): """Gets all updates since the last time this function was called (or since the stream was constructed if it hadn't been called before), until the `upto_token` @@ -156,13 +153,12 @@ class Stream(object): list of ``(token, row)`` entries. ``row`` will be json-serialised and sent over the replication steam. """ - updates, current_token = yield self.get_updates_since(self.last_token) + updates, current_token = await self.get_updates_since(self.last_token) self.last_token = current_token return updates, current_token - @defer.inlineCallbacks - def get_updates_since(self, from_token): + async def get_updates_since(self, from_token): """Like get_updates except allows specifying from when we should stream updates @@ -182,15 +178,16 @@ class Stream(object): if from_token == current_token: return [], current_token + logger.info("get_updates_since: %s", self.__class__) if self._LIMITED: - rows = yield self.update_function( + rows = await self.update_function( from_token, current_token, limit=MAX_EVENTS_BEHIND + 1 ) # never turn more than MAX_EVENTS_BEHIND + 1 into updates. rows = itertools.islice(rows, MAX_EVENTS_BEHIND + 1) else: - rows = yield self.update_function(from_token, current_token) + rows = await self.update_function(from_token, current_token) updates = [(row[0], row[1:]) for row in rows] @@ -295,9 +292,8 @@ class PushRulesStream(Stream): push_rules_token, _ = self.store.get_push_rules_stream_token() return push_rules_token - @defer.inlineCallbacks - def update_function(self, from_token, to_token, limit): - rows = yield self.store.get_all_push_rule_updates(from_token, to_token, limit) + async def update_function(self, from_token, to_token, limit): + rows = await self.store.get_all_push_rule_updates(from_token, to_token, limit) return [(row[0], row[2]) for row in rows] @@ -413,9 +409,8 @@ class AccountDataStream(Stream): super(AccountDataStream, self).__init__(hs) - @defer.inlineCallbacks - def update_function(self, from_token, to_token, limit): - global_results, room_results = yield self.store.get_all_updated_account_data( + async def update_function(self, from_token, to_token, limit): + global_results, room_results = await self.store.get_all_updated_account_data( from_token, from_token, to_token, limit ) diff --git a/synapse/replication/tcp/streams/events.py b/synapse/replication/tcp/streams/events.py index 0843e5aa90..b3afabb8cd 100644 --- a/synapse/replication/tcp/streams/events.py +++ b/synapse/replication/tcp/streams/events.py @@ -19,8 +19,6 @@ from typing import Tuple, Type import attr -from twisted.internet import defer - from ._base import Stream @@ -122,16 +120,15 @@ class EventsStream(Stream): super(EventsStream, self).__init__(hs) - @defer.inlineCallbacks - def update_function(self, from_token, current_token, limit=None): - event_rows = yield self._store.get_all_new_forward_event_rows( + async def update_function(self, from_token, current_token, limit=None): + event_rows = await self._store.get_all_new_forward_event_rows( from_token, current_token, limit ) event_updates = ( (row[0], EventsStreamEventRow.TypeId, row[1:]) for row in event_rows ) - state_rows = yield self._store.get_all_updated_current_state_deltas( + state_rows = await self._store.get_all_updated_current_state_deltas( from_token, current_token, limit ) state_updates = ( diff --git a/tests/replication/tcp/streams/_base.py b/tests/replication/tcp/streams/_base.py index 1d14e77255..e96ad4ca4e 100644 --- a/tests/replication/tcp/streams/_base.py +++ b/tests/replication/tcp/streams/_base.py @@ -73,6 +73,6 @@ class TestReplicationClientHandler(object): def finished_connecting(self): pass - def on_rdata(self, stream_name, token, rows): + async def on_rdata(self, stream_name, token, rows): for r in rows: self.received_rdata_rows.append((stream_name, token, r)) From 38e0e59f42de03b25ce84a95a578a8cdbe75ceb4 Mon Sep 17 00:00:00 2001 From: Neil Johnson Date: Thu, 16 Jan 2020 09:46:14 +0000 Subject: [PATCH 0831/1623] Add org.matrix.e2e_cross_signing to unstable_features in /versions as per MSC1756 (#6712) --- changelog.d/6712.feature | 1 + synapse/rest/client/versions.py | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 changelog.d/6712.feature diff --git a/changelog.d/6712.feature b/changelog.d/6712.feature new file mode 100644 index 0000000000..2cce0ecf88 --- /dev/null +++ b/changelog.d/6712.feature @@ -0,0 +1 @@ +Add org.matrix.e2e_cross_signing to unstable_features in /versions as per [MSC1756](https://github.com/matrix-org/matrix-doc/pull/1756). diff --git a/synapse/rest/client/versions.py b/synapse/rest/client/versions.py index 2a477ad22e..3d0fefb4df 100644 --- a/synapse/rest/client/versions.py +++ b/synapse/rest/client/versions.py @@ -71,6 +71,8 @@ class VersionsRestServlet(RestServlet): # Implements support for label-based filtering as described in # MSC2326. "org.matrix.label_based_filtering": True, + # Implements support for cross signing as described in MSC1756 + "org.matrix.e2e_cross_signing": True, }, }, ) From 7b14c4a0189dde1a6e7e077e2206c61bfa4b8b01 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Thu, 16 Jan 2020 09:46:36 +0000 Subject: [PATCH 0832/1623] Add tips for the changelog to the pull request template (#6663) --- .github/PULL_REQUEST_TEMPLATE.md | 6 +++++- CONTRIBUTING.md | 4 ++-- changelog.d/6663.doc | 1 + 3 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 changelog.d/6663.doc diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 11fb05ca96..fc22d89426 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -3,6 +3,10 @@ * [ ] Pull request is based on the develop branch -* [ ] Pull request includes a [changelog file](https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.md#changelog) +* [ ] Pull request includes a [changelog file](https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.md#changelog). The entry should: + - Be a short description of your change which makes sense to users. "Fixed a bug that prevented receiving messages from other servers." instead of "Moved X method from `EventStore` to `EventWorkerStore`.". + - Use markdown where necessary, mostly for `code blocks`. + - End with either a period (.) or an exclamation mark (!). + - Start with a capital letter. * [ ] Pull request includes a [sign off](https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.md#sign-off) * [ ] Code style is correct (run the [linters](https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.md#code-style)) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c0091346f3..5736ede6c4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -101,8 +101,8 @@ in the format of `PRnumber.type`. The type can be one of the following: The content of the file is your changelog entry, which should be a short description of your change in the same style as the rest of our [changelog]( https://github.com/matrix-org/synapse/blob/master/CHANGES.md). The file can -contain Markdown formatting, and should end with a full stop ('.') for -consistency. +contain Markdown formatting, and should end with a full stop (.) or an +exclamation mark (!) for consistency. Adding credits to the changelog is encouraged, we value your contributions and would like to have you shouted out in the release notes! diff --git a/changelog.d/6663.doc b/changelog.d/6663.doc new file mode 100644 index 0000000000..83b9c1626a --- /dev/null +++ b/changelog.d/6663.doc @@ -0,0 +1 @@ +Add some helpful tips about changelog entries to the github pull request template. \ No newline at end of file From e601f35d3b562495b2f8b071bd4c812fd783d6a7 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 16 Jan 2020 09:55:11 +0000 Subject: [PATCH 0833/1623] Lint --- synapse/storage/data_stores/main/stream.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/synapse/storage/data_stores/main/stream.py b/synapse/storage/data_stores/main/stream.py index 451f38296b..652cecd59b 100644 --- a/synapse/storage/data_stores/main/stream.py +++ b/synapse/storage/data_stores/main/stream.py @@ -539,7 +539,9 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): return self.db.runInteraction( "get_room_event_after_stream_ordering", self.get_room_event_around_stream_ordering_txn, - room_id, stream_ordering, "f", + room_id, + stream_ordering, + "f", ) def get_room_event_before_stream_ordering(self, room_id, stream_ordering): @@ -556,7 +558,9 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): return self.db.runInteraction( "get_room_event_before_stream_ordering", self.get_room_event_around_stream_ordering_txn, - room_id, stream_ordering, "b", + room_id, + stream_ordering, + "b", ) def get_room_event_around_stream_ordering_txn( @@ -575,6 +579,11 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): Deferred[(int, int, str)]: (stream ordering, topological ordering, event_id) """ + # Figure out which comparison operation to perform and how to order the results, + # using the provided direction. + op = "<=" if dir == "b" else ">=" + order = "DESC" if dir == "b" else "ASC" + sql = ( "SELECT stream_ordering, topological_ordering, event_id" " FROM events" @@ -582,10 +591,7 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): " AND NOT outlier" " ORDER BY stream_ordering %s" " LIMIT 1" - ) % ( - "<=" if dir == "b" else ">=", - "DESC" if dir == "b" else "ASC", - ) + ) % (op, order) txn.execute(sql, (room_id, stream_ordering)) return txn.fetchone() From d386f2f339c839ff6ec8d656492dd635dc26f811 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 16 Jan 2020 13:31:22 +0000 Subject: [PATCH 0834/1623] Add StateMap type alias (#6715) --- changelog.d/6715.misc | 1 + synapse/api/auth.py | 8 +-- synapse/events/snapshot.py | 11 ++-- .../sender/per_destination_queue.py | 3 +- synapse/handlers/admin.py | 25 ++++----- synapse/handlers/federation.py | 10 ++-- synapse/handlers/room.py | 24 ++++++--- synapse/state/__init__.py | 5 +- synapse/state/v1.py | 5 +- synapse/state/v2.py | 9 ++-- synapse/storage/data_stores/main/state.py | 11 ++-- synapse/storage/data_stores/state/store.py | 52 ++++++++++--------- synapse/storage/state.py | 35 ++++++++----- synapse/types.py | 9 +++- 14 files changed, 115 insertions(+), 93 deletions(-) create mode 100644 changelog.d/6715.misc diff --git a/changelog.d/6715.misc b/changelog.d/6715.misc new file mode 100644 index 0000000000..8876b0446d --- /dev/null +++ b/changelog.d/6715.misc @@ -0,0 +1 @@ +Add StateMap type alias to simplify types. diff --git a/synapse/api/auth.py b/synapse/api/auth.py index abbc7079a3..2cbfab2569 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -14,7 +14,6 @@ # limitations under the License. import logging -from typing import Dict, Tuple from six import itervalues @@ -35,7 +34,7 @@ from synapse.api.errors import ( ResourceLimitError, ) from synapse.config.server import is_threepid_reserved -from synapse.types import UserID +from synapse.types import StateMap, UserID from synapse.util.caches import CACHE_SIZE_FACTOR, register_cache from synapse.util.caches.lrucache import LruCache from synapse.util.metrics import Measure @@ -509,10 +508,7 @@ class Auth(object): return self.store.is_server_admin(user) def compute_auth_events( - self, - event, - current_state_ids: Dict[Tuple[str, str], str], - for_verification: bool = False, + self, event, current_state_ids: StateMap[str], for_verification: bool = False, ): """Given an event and current state return the list of event IDs used to auth an event. diff --git a/synapse/events/snapshot.py b/synapse/events/snapshot.py index a44baea365..9ea85e93e6 100644 --- a/synapse/events/snapshot.py +++ b/synapse/events/snapshot.py @@ -12,7 +12,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from typing import Dict, Optional, Tuple, Union +from typing import Optional, Union from six import iteritems @@ -23,6 +23,7 @@ from twisted.internet import defer from synapse.appservice import ApplicationService from synapse.logging.context import make_deferred_yieldable, run_in_background +from synapse.types import StateMap @attr.s(slots=True) @@ -106,13 +107,11 @@ class EventContext: _state_group = attr.ib(default=None, type=Optional[int]) state_group_before_event = attr.ib(default=None, type=Optional[int]) prev_group = attr.ib(default=None, type=Optional[int]) - delta_ids = attr.ib(default=None, type=Optional[Dict[Tuple[str, str], str]]) + delta_ids = attr.ib(default=None, type=Optional[StateMap[str]]) app_service = attr.ib(default=None, type=Optional[ApplicationService]) - _current_state_ids = attr.ib( - default=None, type=Optional[Dict[Tuple[str, str], str]] - ) - _prev_state_ids = attr.ib(default=None, type=Optional[Dict[Tuple[str, str], str]]) + _current_state_ids = attr.ib(default=None, type=Optional[StateMap[str]]) + _prev_state_ids = attr.ib(default=None, type=Optional[StateMap[str]]) @staticmethod def with_state( diff --git a/synapse/federation/sender/per_destination_queue.py b/synapse/federation/sender/per_destination_queue.py index a5b36b1827..5012aaea35 100644 --- a/synapse/federation/sender/per_destination_queue.py +++ b/synapse/federation/sender/per_destination_queue.py @@ -31,6 +31,7 @@ from synapse.handlers.presence import format_user_presence_state from synapse.metrics import sent_transactions_counter from synapse.metrics.background_process_metrics import run_as_background_process from synapse.storage.presence import UserPresenceState +from synapse.types import StateMap from synapse.util.retryutils import NotRetryingDestination, get_retry_limiter # This is defined in the Matrix spec and enforced by the receiver. @@ -77,7 +78,7 @@ class PerDestinationQueue(object): # Pending EDUs by their "key". Keyed EDUs are EDUs that get clobbered # based on their key (e.g. typing events by room_id) # Map of (edu_type, key) -> Edu - self._pending_edus_keyed = {} # type: dict[tuple[str, str], Edu] + self._pending_edus_keyed = {} # type: StateMap[Edu] # Map of user_id -> UserPresenceState of pending presence to be sent to this # destination diff --git a/synapse/handlers/admin.py b/synapse/handlers/admin.py index a9407553b4..60a7c938bc 100644 --- a/synapse/handlers/admin.py +++ b/synapse/handlers/admin.py @@ -14,9 +14,11 @@ # limitations under the License. import logging +from typing import List from synapse.api.constants import Membership -from synapse.types import RoomStreamToken +from synapse.events import FrozenEvent +from synapse.types import RoomStreamToken, StateMap from synapse.visibility import filter_events_for_client from ._base import BaseHandler @@ -259,35 +261,26 @@ class ExfiltrationWriter(object): """Interface used to specify how to write exported data. """ - def write_events(self, room_id, events): + def write_events(self, room_id: str, events: List[FrozenEvent]): """Write a batch of events for a room. - - Args: - room_id (str) - events (list[FrozenEvent]) """ pass - def write_state(self, room_id, event_id, state): + def write_state(self, room_id: str, event_id: str, state: StateMap[FrozenEvent]): """Write the state at the given event in the room. This only gets called for backward extremities rather than for each event. - - Args: - room_id (str) - event_id (str) - state (dict[tuple[str, str], FrozenEvent]) """ pass - def write_invite(self, room_id, event, state): + def write_invite(self, room_id: str, event: FrozenEvent, state: StateMap[dict]): """Write an invite for the room, with associated invite state. Args: - room_id (str) - event (FrozenEvent) - state (dict[tuple[str, str], dict]): A subset of the state at the + room_id + event + state: A subset of the state at the invite, with a subset of the event keys (type, state_key content and sender) """ diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 61b6713c88..d4f9a792fc 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -64,7 +64,7 @@ from synapse.replication.http.federation import ( from synapse.replication.http.membership import ReplicationUserJoinedLeftRoomRestServlet from synapse.state import StateResolutionStore, resolve_events_with_store from synapse.storage.data_stores.main.events_worker import EventRedactBehaviour -from synapse.types import UserID, get_domain_from_id +from synapse.types import StateMap, UserID, get_domain_from_id from synapse.util.async_helpers import Linearizer, concurrently_execute from synapse.util.distributor import user_joined_room from synapse.util.retryutils import NotRetryingDestination @@ -89,7 +89,7 @@ class _NewEventInfo: event = attr.ib(type=EventBase) state = attr.ib(type=Optional[Sequence[EventBase]], default=None) - auth_events = attr.ib(type=Optional[Dict[Tuple[str, str], EventBase]], default=None) + auth_events = attr.ib(type=Optional[StateMap[EventBase]], default=None) def shortstr(iterable, maxitems=5): @@ -352,9 +352,7 @@ class FederationHandler(BaseHandler): ours = await self.state_store.get_state_groups_ids(room_id, seen) # state_maps is a list of mappings from (type, state_key) to event_id - state_maps = list( - ours.values() - ) # type: list[dict[tuple[str, str], str]] + state_maps = list(ours.values()) # type: list[StateMap[str]] # we don't need this any more, let's delete it. del ours @@ -1912,7 +1910,7 @@ class FederationHandler(BaseHandler): origin: str, event: EventBase, state: Optional[Iterable[EventBase]], - auth_events: Optional[Dict[Tuple[str, str], EventBase]], + auth_events: Optional[StateMap[EventBase]], backfilled: bool, ): """ diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 9cab2adbfb..9f50196ea7 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -32,7 +32,15 @@ from synapse.api.errors import AuthError, Codes, NotFoundError, StoreError, Syna from synapse.api.room_versions import KNOWN_ROOM_VERSIONS from synapse.http.endpoint import parse_and_validate_server_name from synapse.storage.state import StateFilter -from synapse.types import RoomAlias, RoomID, RoomStreamToken, StreamToken, UserID +from synapse.types import ( + Requester, + RoomAlias, + RoomID, + RoomStreamToken, + StateMap, + StreamToken, + UserID, +) from synapse.util import stringutils from synapse.util.async_helpers import Linearizer from synapse.util.caches.response_cache import ResponseCache @@ -207,15 +215,19 @@ class RoomCreationHandler(BaseHandler): @defer.inlineCallbacks def _update_upgraded_room_pls( - self, requester, old_room_id, new_room_id, old_room_state, + self, + requester: Requester, + old_room_id: str, + new_room_id: str, + old_room_state: StateMap[str], ): """Send updated power levels in both rooms after an upgrade Args: - requester (synapse.types.Requester): the user requesting the upgrade - old_room_id (str): the id of the room to be replaced - new_room_id (str): the id of the replacement room - old_room_state (dict[tuple[str, str], str]): the state map for the old room + requester: the user requesting the upgrade + old_room_id: the id of the room to be replaced + new_room_id: the id of the replacement room + old_room_state: the state map for the old room Returns: Deferred diff --git a/synapse/state/__init__.py b/synapse/state/__init__.py index 5accc071ab..cacd0c0c2b 100644 --- a/synapse/state/__init__.py +++ b/synapse/state/__init__.py @@ -16,7 +16,7 @@ import logging from collections import namedtuple -from typing import Dict, Iterable, List, Optional, Tuple +from typing import Dict, Iterable, List, Optional from six import iteritems, itervalues @@ -33,6 +33,7 @@ from synapse.events.snapshot import EventContext from synapse.logging.utils import log_function from synapse.state import v1, v2 from synapse.storage.data_stores.main.events_worker import EventRedactBehaviour +from synapse.types import StateMap from synapse.util.async_helpers import Linearizer from synapse.util.caches import get_cache_factor_for from synapse.util.caches.expiringcache import ExpiringCache @@ -594,7 +595,7 @@ def _make_state_cache_entry(new_state, state_groups_ids): def resolve_events_with_store( room_id: str, room_version: str, - state_sets: List[Dict[Tuple[str, str], str]], + state_sets: List[StateMap[str]], event_map: Optional[Dict[str, EventBase]], state_res_store: "StateResolutionStore", ): diff --git a/synapse/state/v1.py b/synapse/state/v1.py index b2f9865f39..d6c34ce3b7 100644 --- a/synapse/state/v1.py +++ b/synapse/state/v1.py @@ -15,7 +15,7 @@ import hashlib import logging -from typing import Callable, Dict, List, Optional, Tuple +from typing import Callable, Dict, List, Optional from six import iteritems, iterkeys, itervalues @@ -26,6 +26,7 @@ from synapse.api.constants import EventTypes from synapse.api.errors import AuthError from synapse.api.room_versions import RoomVersions from synapse.events import EventBase +from synapse.types import StateMap logger = logging.getLogger(__name__) @@ -36,7 +37,7 @@ POWER_KEY = (EventTypes.PowerLevels, "") @defer.inlineCallbacks def resolve_events_with_store( room_id: str, - state_sets: List[Dict[Tuple[str, str], str]], + state_sets: List[StateMap[str]], event_map: Optional[Dict[str, EventBase]], state_map_factory: Callable, ): diff --git a/synapse/state/v2.py b/synapse/state/v2.py index 72fb8a6317..6216fdd204 100644 --- a/synapse/state/v2.py +++ b/synapse/state/v2.py @@ -16,7 +16,7 @@ import heapq import itertools import logging -from typing import Dict, List, Optional, Tuple +from typing import Dict, List, Optional from six import iteritems, itervalues @@ -27,6 +27,7 @@ from synapse import event_auth from synapse.api.constants import EventTypes from synapse.api.errors import AuthError from synapse.events import EventBase +from synapse.types import StateMap logger = logging.getLogger(__name__) @@ -35,7 +36,7 @@ logger = logging.getLogger(__name__) def resolve_events_with_store( room_id: str, room_version: str, - state_sets: List[Dict[Tuple[str, str], str]], + state_sets: List[StateMap[str]], event_map: Optional[Dict[str, EventBase]], state_res_store: "synapse.state.StateResolutionStore", ): @@ -393,12 +394,12 @@ def _iterative_auth_checks( room_id (str) room_version (str) event_ids (list[str]): Ordered list of events to apply auth checks to - base_state (dict[tuple[str, str], str]): The set of state to start with + base_state (StateMap[str]): The set of state to start with event_map (dict[str,FrozenEvent]) state_res_store (StateResolutionStore) Returns: - Deferred[dict[tuple[str, str], str]]: Returns the final updated state + Deferred[StateMap[str]]: Returns the final updated state """ resolved_state = base_state.copy() diff --git a/synapse/storage/data_stores/main/state.py b/synapse/storage/data_stores/main/state.py index d07440e3ed..33bebd1c48 100644 --- a/synapse/storage/data_stores/main/state.py +++ b/synapse/storage/data_stores/main/state.py @@ -165,19 +165,20 @@ class StateGroupWorkerStore(EventsWorkerStore, SQLBaseStore): ) # FIXME: how should this be cached? - def get_filtered_current_state_ids(self, room_id, state_filter=StateFilter.all()): + def get_filtered_current_state_ids( + self, room_id: str, state_filter: StateFilter = StateFilter.all() + ): """Get the current state event of a given type for a room based on the current_state_events table. This may not be as up-to-date as the result of doing a fresh state resolution as per state_handler.get_current_state Args: - room_id (str) - state_filter (StateFilter): The state filter used to fetch state + room_id + state_filter: The state filter used to fetch state from the database. Returns: - Deferred[dict[tuple[str, str], str]]: Map from type/state_key to - event ID. + defer.Deferred[StateMap[str]]: Map from type/state_key to event ID. """ where_clause, where_args = state_filter.make_sql_filter_clause() diff --git a/synapse/storage/data_stores/state/store.py b/synapse/storage/data_stores/state/store.py index d53695f238..c4ee9b7ccb 100644 --- a/synapse/storage/data_stores/state/store.py +++ b/synapse/storage/data_stores/state/store.py @@ -15,6 +15,7 @@ import logging from collections import namedtuple +from typing import Dict, Iterable, List, Set, Tuple from six import iteritems from six.moves import range @@ -26,6 +27,7 @@ from synapse.storage._base import SQLBaseStore from synapse.storage.data_stores.state.bg_updates import StateBackgroundUpdateStore from synapse.storage.database import Database from synapse.storage.state import StateFilter +from synapse.types import StateMap from synapse.util.caches import get_cache_factor_for from synapse.util.caches.descriptors import cached from synapse.util.caches.dictionary_cache import DictionaryCache @@ -133,17 +135,18 @@ class StateGroupDataStore(StateBackgroundUpdateStore, SQLBaseStore): ) @defer.inlineCallbacks - def _get_state_groups_from_groups(self, groups, state_filter): - """Returns the state groups for a given set of groups, filtering on - types of state events. + def _get_state_groups_from_groups( + self, groups: List[int], state_filter: StateFilter + ): + """Returns the state groups for a given set of groups from the + database, filtering on types of state events. Args: - groups(list[int]): list of state group IDs to query - state_filter (StateFilter): The state filter used to fetch state + groups: list of state group IDs to query + state_filter: The state filter used to fetch state from the database. Returns: - Deferred[dict[int, dict[tuple[str, str], str]]]: - dict of state_group_id -> (dict of (type, state_key) -> event id) + Deferred[Dict[int, StateMap[str]]]: Dict of state group to state map. """ results = {} @@ -199,18 +202,19 @@ class StateGroupDataStore(StateBackgroundUpdateStore, SQLBaseStore): return state_filter.filter_state(state_dict_ids), not missing_types @defer.inlineCallbacks - def _get_state_for_groups(self, groups, state_filter=StateFilter.all()): + def _get_state_for_groups( + self, groups: Iterable[int], state_filter: StateFilter = StateFilter.all() + ): """Gets the state at each of a list of state groups, optionally filtering by type/state_key Args: - groups (iterable[int]): list of state groups for which we want + groups: list of state groups for which we want to get the state. - state_filter (StateFilter): The state filter used to fetch state + state_filter: The state filter used to fetch state from the database. Returns: - Deferred[dict[int, dict[tuple[str, str], str]]]: - dict of state_group_id -> (dict of (type, state_key) -> event id) + Deferred[Dict[int, StateMap[str]]]: Dict of state group to state map. """ member_filter, non_member_filter = state_filter.get_member_split() @@ -268,24 +272,24 @@ class StateGroupDataStore(StateBackgroundUpdateStore, SQLBaseStore): return state - def _get_state_for_groups_using_cache(self, groups, cache, state_filter): + def _get_state_for_groups_using_cache( + self, groups: Iterable[int], cache: DictionaryCache, state_filter: StateFilter + ) -> Tuple[Dict[int, StateMap[str]], Set[int]]: """Gets the state at each of a list of state groups, optionally filtering by type/state_key, querying from a specific cache. Args: - groups (iterable[int]): list of state groups for which we want - to get the state. - cache (DictionaryCache): the cache of group ids to state dicts which - we will pass through - either the normal state cache or the specific - members state cache. - state_filter (StateFilter): The state filter used to fetch state - from the database. + groups: list of state groups for which we want to get the state. + cache: the cache of group ids to state dicts which + we will pass through - either the normal state cache or the + specific members state cache. + state_filter: The state filter used to fetch state from the + database. Returns: - tuple[dict[int, dict[tuple[str, str], str]], set[int]]: Tuple of - dict of state_group_id -> (dict of (type, state_key) -> event id) - of entries in the cache, and the state group ids either missing - from the cache or incomplete. + Tuple of dict of state_group_id to state map of entries in the + cache, and the state group ids either missing from the cache or + incomplete. """ results = {} incomplete_groups = set() diff --git a/synapse/storage/state.py b/synapse/storage/state.py index cbeb586014..c522c80922 100644 --- a/synapse/storage/state.py +++ b/synapse/storage/state.py @@ -14,6 +14,7 @@ # limitations under the License. import logging +from typing import Iterable, List, TypeVar from six import iteritems, itervalues @@ -22,9 +23,13 @@ import attr from twisted.internet import defer from synapse.api.constants import EventTypes +from synapse.types import StateMap logger = logging.getLogger(__name__) +# Used for generic functions below +T = TypeVar("T") + @attr.s(slots=True) class StateFilter(object): @@ -233,14 +238,14 @@ class StateFilter(object): return len(self.concrete_types()) - def filter_state(self, state_dict): + def filter_state(self, state_dict: StateMap[T]) -> StateMap[T]: """Returns the state filtered with by this StateFilter Args: - state (dict[tuple[str, str], Any]): The state map to filter + state: The state map to filter Returns: - dict[tuple[str, str], Any]: The filtered state map + The filtered state map """ if self.is_full(): return dict(state_dict) @@ -333,12 +338,12 @@ class StateGroupStorage(object): def __init__(self, hs, stores): self.stores = stores - def get_state_group_delta(self, state_group): + def get_state_group_delta(self, state_group: int): """Given a state group try to return a previous group and a delta between the old and the new. Returns: - Deferred[Tuple[Optional[int], Optional[list[dict[tuple[str, str], str]]]]]): + Deferred[Tuple[Optional[int], Optional[StateMap[str]]]]: (prev_group, delta_ids) """ @@ -353,7 +358,7 @@ class StateGroupStorage(object): event_ids (iterable[str]): ids of the events Returns: - Deferred[dict[int, dict[tuple[str, str], str]]]: + Deferred[dict[int, StateMap[str]]]: dict of state_group_id -> (dict of (type, state_key) -> event id) """ if not event_ids: @@ -410,17 +415,18 @@ class StateGroupStorage(object): for group, event_id_map in iteritems(group_to_ids) } - def _get_state_groups_from_groups(self, groups, state_filter): + def _get_state_groups_from_groups( + self, groups: List[int], state_filter: StateFilter + ): """Returns the state groups for a given set of groups, filtering on types of state events. Args: - groups(list[int]): list of state group IDs to query - state_filter (StateFilter): The state filter used to fetch state + groups: list of state group IDs to query + state_filter: The state filter used to fetch state from the database. Returns: - Deferred[dict[int, dict[tuple[str, str], str]]]: - dict of state_group_id -> (dict of (type, state_key) -> event id) + Deferred[Dict[int, StateMap[str]]]: Dict of state group to state map. """ return self.stores.state._get_state_groups_from_groups(groups, state_filter) @@ -519,7 +525,9 @@ class StateGroupStorage(object): state_map = yield self.get_state_ids_for_events([event_id], state_filter) return state_map[event_id] - def _get_state_for_groups(self, groups, state_filter=StateFilter.all()): + def _get_state_for_groups( + self, groups: Iterable[int], state_filter: StateFilter = StateFilter.all() + ): """Gets the state at each of a list of state groups, optionally filtering by type/state_key @@ -529,8 +537,7 @@ class StateGroupStorage(object): state_filter (StateFilter): The state filter used to fetch state from the database. Returns: - Deferred[dict[int, dict[tuple[str, str], str]]]: - dict of state_group_id -> (dict of (type, state_key) -> event id) + Deferred[dict[int, StateMap[str]]]: Dict of state group to state map. """ return self.stores.state._get_state_for_groups(groups, state_filter) diff --git a/synapse/types.py b/synapse/types.py index cd996c0b5a..65e4d8c181 100644 --- a/synapse/types.py +++ b/synapse/types.py @@ -17,6 +17,7 @@ import re import string import sys from collections import namedtuple +from typing import Dict, Tuple, TypeVar import attr from signedjson.key import decode_verify_key_bytes @@ -28,7 +29,7 @@ from synapse.api.errors import SynapseError if sys.version_info[:3] >= (3, 6, 0): from typing import Collection else: - from typing import Sized, Iterable, Container, TypeVar + from typing import Sized, Iterable, Container T_co = TypeVar("T_co", covariant=True) @@ -36,6 +37,12 @@ else: __slots__ = () +# Define a state map type from type/state_key to T (usually an event ID or +# event) +T = TypeVar("T") +StateMap = Dict[Tuple[str, str], T] + + class Requester( namedtuple( "Requester", ["user", "access_token_id", "is_guest", "device_id", "app_service"] From 842c2cfbf1e9f3e0d9251fa0c572eba9d6af6dbe Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 16 Jan 2020 20:24:17 +0000 Subject: [PATCH 0835/1623] Remove get_room_event_after_stream_ordering entirely --- synapse/rest/admin/__init__.py | 2 +- synapse/storage/data_stores/main/stream.py | 69 ++++------------------ 2 files changed, 13 insertions(+), 58 deletions(-) diff --git a/synapse/rest/admin/__init__.py b/synapse/rest/admin/__init__.py index a10b4a9b72..2932fe2123 100644 --- a/synapse/rest/admin/__init__.py +++ b/synapse/rest/admin/__init__.py @@ -107,7 +107,7 @@ class PurgeHistoryRestServlet(RestServlet): stream_ordering = await self.store.find_first_stream_ordering_after_ts(ts) - r = await self.store.get_room_event_after_stream_ordering( + r = await self.store.get_room_event_before_stream_ordering( room_id, stream_ordering ) if not r: diff --git a/synapse/storage/data_stores/main/stream.py b/synapse/storage/data_stores/main/stream.py index 652cecd59b..a20c3d1012 100644 --- a/synapse/storage/data_stores/main/stream.py +++ b/synapse/storage/data_stores/main/stream.py @@ -525,25 +525,6 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): return rows, token - def get_room_event_after_stream_ordering(self, room_id, stream_ordering): - """Gets details of the first event in a room at or after a stream ordering - - Args: - room_id (str): - stream_ordering (int): - - Returns: - Deferred[(int, int, str)]: - (stream ordering, topological ordering, event_id) - """ - return self.db.runInteraction( - "get_room_event_after_stream_ordering", - self.get_room_event_around_stream_ordering_txn, - room_id, - stream_ordering, - "f", - ) - def get_room_event_before_stream_ordering(self, room_id, stream_ordering): """Gets details of the first event in a room at or before a stream ordering @@ -555,45 +536,19 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): Deferred[(int, int, str)]: (stream ordering, topological ordering, event_id) """ - return self.db.runInteraction( - "get_room_event_before_stream_ordering", - self.get_room_event_around_stream_ordering_txn, - room_id, - stream_ordering, - "b", - ) + def _f(txn): + sql = ( + "SELECT stream_ordering, topological_ordering, event_id" + " FROM events" + " WHERE room_id = ? AND stream_ordering <= ?" + " AND NOT outlier" + " ORDER BY stream_ordering DESC" + " LIMIT 1" + ) + txn.execute(sql, (room_id, stream_ordering)) + return txn.fetchone() - def get_room_event_around_stream_ordering_txn( - self, txn, room_id, stream_ordering, dir="f" - ): - """Gets details of the first event in a room at or either after or before a - stream ordering, depending on the provided direction. - - Args: - room_id (str): - stream_ordering (int): - dir (str): Direction in which we're looking towards in the room's history, - either "f" (forward) or "b" (backward). - - Returns: - Deferred[(int, int, str)]: - (stream ordering, topological ordering, event_id) - """ - # Figure out which comparison operation to perform and how to order the results, - # using the provided direction. - op = "<=" if dir == "b" else ">=" - order = "DESC" if dir == "b" else "ASC" - - sql = ( - "SELECT stream_ordering, topological_ordering, event_id" - " FROM events" - " WHERE room_id = ? AND stream_ordering %s ?" - " AND NOT outlier" - " ORDER BY stream_ordering %s" - " LIMIT 1" - ) % (op, order) - txn.execute(sql, (room_id, stream_ordering)) - return txn.fetchone() + return self.db.runInteraction("get_room_event_before_stream_ordering", _f) @defer.inlineCallbacks def get_room_events_max_id(self, room_id=None): From dac148341ba2638cc9486cf0b00005932dab939d Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 16 Jan 2020 20:25:09 +0000 Subject: [PATCH 0836/1623] Fixup diff --- synapse/storage/data_stores/main/stream.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/synapse/storage/data_stores/main/stream.py b/synapse/storage/data_stores/main/stream.py index a20c3d1012..056b25b13a 100644 --- a/synapse/storage/data_stores/main/stream.py +++ b/synapse/storage/data_stores/main/stream.py @@ -536,14 +536,15 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): Deferred[(int, int, str)]: (stream ordering, topological ordering, event_id) """ + def _f(txn): sql = ( - "SELECT stream_ordering, topological_ordering, event_id" - " FROM events" - " WHERE room_id = ? AND stream_ordering <= ?" - " AND NOT outlier" - " ORDER BY stream_ordering DESC" - " LIMIT 1" + "SELECT stream_ordering, topological_ordering, event_id" + " FROM events" + " WHERE room_id = ? AND stream_ordering <= ?" + " AND NOT outlier" + " ORDER BY stream_ordering DESC" + " LIMIT 1" ) txn.execute(sql, (room_id, stream_ordering)) return txn.fetchone() From 4fb3cb208a17ba36a5da050b19e3997cf4808f9a Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 16 Jan 2020 20:27:07 +0000 Subject: [PATCH 0837/1623] Precise changelog --- changelog.d/6714.bugfix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/6714.bugfix b/changelog.d/6714.bugfix index 3924f1ad79..410516694f 100644 --- a/changelog.d/6714.bugfix +++ b/changelog.d/6714.bugfix @@ -1 +1 @@ -Fix a bug causing Synapse to not always purge quiet rooms with a low `max_lifetime` in their message retention policies. +Fix a bug causing Synapse to not always purge quiet rooms with a low `max_lifetime` in their message retention policies when running the automated purge jobs. From 14d8f342d5cae86d93d9ba2b411d486690ff54f5 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 14 Jan 2020 11:58:02 +0000 Subject: [PATCH 0838/1623] move batch_iter to a separate module --- synapse/storage/data_stores/main/cache.py | 2 +- synapse/storage/data_stores/main/devices.py | 2 +- synapse/storage/data_stores/main/events.py | 2 +- .../storage/data_stores/main/events_worker.py | 2 +- synapse/storage/data_stores/main/keys.py | 2 +- synapse/storage/data_stores/main/presence.py | 2 +- synapse/util/__init__.py | 17 --------- synapse/util/iterutils.py | 35 +++++++++++++++++++ 8 files changed, 41 insertions(+), 23 deletions(-) create mode 100644 synapse/util/iterutils.py diff --git a/synapse/storage/data_stores/main/cache.py b/synapse/storage/data_stores/main/cache.py index 54ed8574c4..bf91512daf 100644 --- a/synapse/storage/data_stores/main/cache.py +++ b/synapse/storage/data_stores/main/cache.py @@ -21,7 +21,7 @@ from twisted.internet import defer from synapse.storage._base import SQLBaseStore from synapse.storage.engines import PostgresEngine -from synapse.util import batch_iter +from synapse.util.iterutils import batch_iter logger = logging.getLogger(__name__) diff --git a/synapse/storage/data_stores/main/devices.py b/synapse/storage/data_stores/main/devices.py index 9a828231c4..f0a7962dd0 100644 --- a/synapse/storage/data_stores/main/devices.py +++ b/synapse/storage/data_stores/main/devices.py @@ -33,13 +33,13 @@ from synapse.metrics.background_process_metrics import run_as_background_process from synapse.storage._base import SQLBaseStore, db_to_json, make_in_list_sql_clause from synapse.storage.database import Database from synapse.types import get_verify_key_from_cross_signing_key -from synapse.util import batch_iter from synapse.util.caches.descriptors import ( Cache, cached, cachedInlineCallbacks, cachedList, ) +from synapse.util.iterutils import batch_iter logger = logging.getLogger(__name__) diff --git a/synapse/storage/data_stores/main/events.py b/synapse/storage/data_stores/main/events.py index e9fe63037b..bb69c20448 100644 --- a/synapse/storage/data_stores/main/events.py +++ b/synapse/storage/data_stores/main/events.py @@ -43,9 +43,9 @@ from synapse.storage.data_stores.main.events_worker import EventsWorkerStore from synapse.storage.data_stores.main.state import StateGroupWorkerStore from synapse.storage.database import Database from synapse.types import RoomStreamToken, get_domain_from_id -from synapse.util import batch_iter from synapse.util.caches.descriptors import cached, cachedInlineCallbacks from synapse.util.frozenutils import frozendict_json_encoder +from synapse.util.iterutils import batch_iter logger = logging.getLogger(__name__) diff --git a/synapse/storage/data_stores/main/events_worker.py b/synapse/storage/data_stores/main/events_worker.py index 0cce5232f5..3b93e0597a 100644 --- a/synapse/storage/data_stores/main/events_worker.py +++ b/synapse/storage/data_stores/main/events_worker.py @@ -37,8 +37,8 @@ from synapse.metrics.background_process_metrics import run_as_background_process from synapse.storage._base import SQLBaseStore, make_in_list_sql_clause from synapse.storage.database import Database from synapse.types import get_domain_from_id -from synapse.util import batch_iter from synapse.util.caches.descriptors import Cache +from synapse.util.iterutils import batch_iter from synapse.util.metrics import Measure logger = logging.getLogger(__name__) diff --git a/synapse/storage/data_stores/main/keys.py b/synapse/storage/data_stores/main/keys.py index 6b12f5a75f..ba89c68c9f 100644 --- a/synapse/storage/data_stores/main/keys.py +++ b/synapse/storage/data_stores/main/keys.py @@ -23,8 +23,8 @@ from signedjson.key import decode_verify_key_bytes from synapse.storage._base import SQLBaseStore from synapse.storage.keys import FetchKeyResult -from synapse.util import batch_iter from synapse.util.caches.descriptors import cached, cachedList +from synapse.util.iterutils import batch_iter logger = logging.getLogger(__name__) diff --git a/synapse/storage/data_stores/main/presence.py b/synapse/storage/data_stores/main/presence.py index a2c83e0867..604c8b7ddd 100644 --- a/synapse/storage/data_stores/main/presence.py +++ b/synapse/storage/data_stores/main/presence.py @@ -17,8 +17,8 @@ from twisted.internet import defer from synapse.storage._base import SQLBaseStore, make_in_list_sql_clause from synapse.storage.presence import UserPresenceState -from synapse.util import batch_iter from synapse.util.caches.descriptors import cached, cachedList +from synapse.util.iterutils import batch_iter class PresenceStore(SQLBaseStore): diff --git a/synapse/util/__init__.py b/synapse/util/__init__.py index 7856353002..60f0de70f7 100644 --- a/synapse/util/__init__.py +++ b/synapse/util/__init__.py @@ -15,7 +15,6 @@ import logging import re -from itertools import islice import attr @@ -107,22 +106,6 @@ class Clock(object): raise -def batch_iter(iterable, size): - """batch an iterable up into tuples with a maximum size - - Args: - iterable (iterable): the iterable to slice - size (int): the maximum batch size - - Returns: - an iterator over the chunks - """ - # make sure we can deal with iterables like lists too - sourceiter = iter(iterable) - # call islice until it returns an empty tuple - return iter(lambda: tuple(islice(sourceiter, size)), ()) - - def log_failure(failure, msg, consumeErrors=True): """Creates a function suitable for passing to `Deferred.addErrback` that logs any failures that occur. diff --git a/synapse/util/iterutils.py b/synapse/util/iterutils.py new file mode 100644 index 0000000000..c10016fbc5 --- /dev/null +++ b/synapse/util/iterutils.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Copyright 2014-2016 OpenMarket Ltd +# Copyright 2020 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from itertools import islice +from typing import Iterable, Iterator, Sequence, Tuple, TypeVar + +T = TypeVar("T") + + +def batch_iter(iterable: Iterable[T], size: int) -> Iterator[Tuple[T]]: + """batch an iterable up into tuples with a maximum size + + Args: + iterable (iterable): the iterable to slice + size (int): the maximum batch size + + Returns: + an iterator over the chunks + """ + # make sure we can deal with iterables like lists too + sourceiter = iter(iterable) + # call islice until it returns an empty tuple + return iter(lambda: tuple(islice(sourceiter, size)), ()) From acc7820574426cf27673d941b1b0362272113351 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 16 Jan 2020 22:26:34 +0000 Subject: [PATCH 0839/1623] Log saml assertions rather than the whole response ... since the whole response is huge. We even need to break up the assertions, since kibana otherwise truncates them. --- synapse/handlers/saml_handler.py | 13 ++++++++- synapse/util/iterutils.py | 13 +++++++++ tests/util/test_itertools.py | 47 ++++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 tests/util/test_itertools.py diff --git a/synapse/handlers/saml_handler.py b/synapse/handlers/saml_handler.py index 107f97032b..32638671c9 100644 --- a/synapse/handlers/saml_handler.py +++ b/synapse/handlers/saml_handler.py @@ -32,6 +32,7 @@ from synapse.types import ( mxid_localpart_allowed_characters, ) from synapse.util.async_helpers import Linearizer +from synapse.util.iterutils import chunk_seq logger = logging.getLogger(__name__) @@ -132,7 +133,17 @@ class SamlHandler: logger.warning("SAML2 response was not signed") raise SynapseError(400, "SAML2 response was not signed") - logger.info("SAML2 response: %s", saml2_auth.origxml) + logger.debug("SAML2 response: %s", saml2_auth.origxml) + for assertion in saml2_auth.assertions: + # kibana limits the length of a log field, whereas this is all rather + # useful, so split it up. + count = 0 + for part in chunk_seq(str(assertion), 10000): + logger.info( + "SAML2 assertion: %s%s", "(%i)..." % (count,) if count else "", part + ) + count += 1 + logger.info("SAML2 mapped attributes: %s", saml2_auth.ava) try: diff --git a/synapse/util/iterutils.py b/synapse/util/iterutils.py index c10016fbc5..06faeebe7f 100644 --- a/synapse/util/iterutils.py +++ b/synapse/util/iterutils.py @@ -33,3 +33,16 @@ def batch_iter(iterable: Iterable[T], size: int) -> Iterator[Tuple[T]]: sourceiter = iter(iterable) # call islice until it returns an empty tuple return iter(lambda: tuple(islice(sourceiter, size)), ()) + + +ISeq = TypeVar("ISeq", bound=Sequence, covariant=True) + + +def chunk_seq(iseq: ISeq, maxlen: int) -> Iterable[ISeq]: + """Split the given sequence into chunks of the given size + + The last chunk may be shorter than the given size. + + If the input is empty, no chunks are returned. + """ + return (iseq[i : i + maxlen] for i in range(0, len(iseq), maxlen)) diff --git a/tests/util/test_itertools.py b/tests/util/test_itertools.py new file mode 100644 index 0000000000..0ab0a91483 --- /dev/null +++ b/tests/util/test_itertools.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from synapse.util.iterutils import chunk_seq + +from tests.unittest import TestCase + + +class ChunkSeqTests(TestCase): + def test_short_seq(self): + parts = chunk_seq("123", 8) + + self.assertEqual( + list(parts), ["123"], + ) + + def test_long_seq(self): + parts = chunk_seq("abcdefghijklmnop", 8) + + self.assertEqual( + list(parts), ["abcdefgh", "ijklmnop"], + ) + + def test_uneven_parts(self): + parts = chunk_seq("abcdefghijklmnop", 5) + + self.assertEqual( + list(parts), ["abcde", "fghij", "klmno", "p"], + ) + + def test_empty_input(self): + parts = chunk_seq([], 5) + + self.assertEqual( + list(parts), [], + ) From 95c5b9bfb3506d06e6b0a7d42adfb1f76f2cb7ca Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 16 Jan 2020 22:29:06 +0000 Subject: [PATCH 0840/1623] changelog --- changelog.d/6724.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6724.misc diff --git a/changelog.d/6724.misc b/changelog.d/6724.misc new file mode 100644 index 0000000000..5256be75fa --- /dev/null +++ b/changelog.d/6724.misc @@ -0,0 +1 @@ +When processing a SAML response, log the assertions for easier configuration. From 5ce0b17e38404fceb8867fdb3b4b59c00db6b1e6 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 17 Jan 2020 10:04:15 +0000 Subject: [PATCH 0841/1623] Clarify the `account_validity` and `email` sections of the sample configuration. (#6685) Generally try to make this more comprehensible, and make it match the conventions. I've removed the documentation for all the settings which allow you to change the names of the template files, because I can't really see why they are useful. --- changelog.d/6685.doc | 1 + docs/sample_config.yaml | 284 ++++++++++++++++++--------------- synapse/config/emailconfig.py | 222 +++++++++++++------------- synapse/config/push.py | 2 +- synapse/config/registration.py | 83 ++++++---- 5 files changed, 320 insertions(+), 272 deletions(-) create mode 100644 changelog.d/6685.doc diff --git a/changelog.d/6685.doc b/changelog.d/6685.doc new file mode 100644 index 0000000000..7cf750fe3f --- /dev/null +++ b/changelog.d/6685.doc @@ -0,0 +1 @@ +Clarify the `account_validity` and `email` sections of the sample configuration. \ No newline at end of file diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 0a2505e7bb..8e8cf513b0 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -874,23 +874,6 @@ media_store_path: "DATADIR/media_store" # Optional account validity configuration. This allows for accounts to be denied # any request after a given period. # -# ``enabled`` defines whether the account validity feature is enabled. Defaults -# to False. -# -# ``period`` allows setting the period after which an account is valid -# after its registration. When renewing the account, its validity period -# will be extended by this amount of time. This parameter is required when using -# the account validity feature. -# -# ``renew_at`` is the amount of time before an account's expiry date at which -# Synapse will send an email to the account's email address with a renewal link. -# This needs the ``email`` and ``public_baseurl`` configuration sections to be -# filled. -# -# ``renew_email_subject`` is the subject of the email sent out with the renewal -# link. ``%(app)s`` can be used as a placeholder for the ``app_name`` parameter -# from the ``email`` section. -# # Once this feature is enabled, Synapse will look for registered users without an # expiration date at startup and will add one to every account it found using the # current settings at that time. @@ -901,21 +884,55 @@ media_store_path: "DATADIR/media_store" # date will be randomly selected within a range [now + period - d ; now + period], # where d is equal to 10% of the validity period. # -#account_validity: -# enabled: true -# period: 6w -# renew_at: 1w -# renew_email_subject: "Renew your %(app)s account" -# # Directory in which Synapse will try to find the HTML files to serve to the -# # user when trying to renew an account. Optional, defaults to -# # synapse/res/templates. -# template_dir: "res/templates" -# # HTML to be displayed to the user after they successfully renewed their -# # account. Optional. -# account_renewed_html_path: "account_renewed.html" -# # HTML to be displayed when the user tries to renew an account with an invalid -# # renewal token. Optional. -# invalid_token_html_path: "invalid_token.html" +account_validity: + # The account validity feature is disabled by default. Uncomment the + # following line to enable it. + # + #enabled: true + + # The period after which an account is valid after its registration. When + # renewing the account, its validity period will be extended by this amount + # of time. This parameter is required when using the account validity + # feature. + # + #period: 6w + + # The amount of time before an account's expiry date at which Synapse will + # send an email to the account's email address with a renewal link. By + # default, no such emails are sent. + # + # If you enable this setting, you will also need to fill out the 'email' and + # 'public_baseurl' configuration sections. + # + #renew_at: 1w + + # The subject of the email sent out with the renewal link. '%(app)s' can be + # used as a placeholder for the 'app_name' parameter from the 'email' + # section. + # + # Note that the placeholder must be written '%(app)s', including the + # trailing 's'. + # + # If this is not set, a default value is used. + # + #renew_email_subject: "Renew your %(app)s account" + + # Directory in which Synapse will try to find templates for the HTML files to + # serve to the user when trying to renew an account. If not set, default + # templates from within the Synapse package will be used. + # + #template_dir: "res/templates" + + # File within 'template_dir' giving the HTML to be displayed to the user after + # they successfully renewed their account. If not set, default text is used. + # + #account_renewed_html_path: "account_renewed.html" + + # File within 'template_dir' giving the HTML to be displayed when the user + # tries to renew an account with an invalid renewal token. If not set, + # default text is used. + # + #invalid_token_html_path: "invalid_token.html" # Time that a user's session remains valid for, after they log in. # @@ -1353,107 +1370,110 @@ password_config: #pepper: "EVEN_MORE_SECRET" +# Configuration for sending emails from Synapse. +# +email: + # The hostname of the outgoing SMTP server to use. Defaults to 'localhost'. + # + #smtp_host: mail.server -# Enable sending emails for password resets, notification events or -# account expiry notices -# -# If your SMTP server requires authentication, the optional smtp_user & -# smtp_pass variables should be used -# -#email: -# enable_notifs: false -# smtp_host: "localhost" -# smtp_port: 25 # SSL: 465, STARTTLS: 587 -# smtp_user: "exampleusername" -# smtp_pass: "examplepassword" -# require_transport_security: false -# -# # notif_from defines the "From" address to use when sending emails. -# # It must be set if email sending is enabled. -# # -# # The placeholder '%(app)s' will be replaced by the application name, -# # which is normally 'app_name' (below), but may be overridden by the -# # Matrix client application. -# # -# # Note that the placeholder must be written '%(app)s', including the -# # trailing 's'. -# # -# notif_from: "Your Friendly %(app)s homeserver " -# -# # app_name defines the default value for '%(app)s' in notif_from. It -# # defaults to 'Matrix'. -# # -# #app_name: my_branded_matrix_server -# -# # Enable email notifications by default -# # -# notif_for_new_users: true -# -# # Defining a custom URL for Riot is only needed if email notifications -# # should contain links to a self-hosted installation of Riot; when set -# # the "app_name" setting is ignored -# # -# riot_base_url: "http://localhost/riot" -# -# # Configure the time that a validation email or text message code -# # will expire after sending -# # -# # This is currently used for password resets -# # -# #validation_token_lifetime: 1h -# -# # Template directory. All template files should be stored within this -# # directory. If not set, default templates from within the Synapse -# # package will be used -# # -# # For the list of default templates, please see -# # https://github.com/matrix-org/synapse/tree/master/synapse/res/templates -# # -# #template_dir: res/templates -# -# # Templates for email notifications -# # -# notif_template_html: notif_mail.html -# notif_template_text: notif_mail.txt -# -# # Templates for account expiry notices -# # -# expiry_template_html: notice_expiry.html -# expiry_template_text: notice_expiry.txt -# -# # Templates for password reset emails sent by the homeserver -# # -# #password_reset_template_html: password_reset.html -# #password_reset_template_text: password_reset.txt -# -# # Templates for registration emails sent by the homeserver -# # -# #registration_template_html: registration.html -# #registration_template_text: registration.txt -# -# # Templates for validation emails sent by the homeserver when adding an email to -# # your user account -# # -# #add_threepid_template_html: add_threepid.html -# #add_threepid_template_text: add_threepid.txt -# -# # Templates for password reset success and failure pages that a user -# # will see after attempting to reset their password -# # -# #password_reset_template_success_html: password_reset_success.html -# #password_reset_template_failure_html: password_reset_failure.html -# -# # Templates for registration success and failure pages that a user -# # will see after attempting to register using an email or phone -# # -# #registration_template_success_html: registration_success.html -# #registration_template_failure_html: registration_failure.html -# -# # Templates for success and failure pages that a user will see after attempting -# # to add an email or phone to their account -# # -# #add_threepid_success_html: add_threepid_success.html -# #add_threepid_failure_html: add_threepid_failure.html + # The port on the mail server for outgoing SMTP. Defaults to 25. + # + #smtp_port: 587 + + # Username/password for authentication to the SMTP server. By default, no + # authentication is attempted. + # + # smtp_user: "exampleusername" + # smtp_pass: "examplepassword" + + # Uncomment the following to require TLS transport security for SMTP. + # By default, Synapse will connect over plain text, and will then switch to + # TLS via STARTTLS *if the SMTP server supports it*. If this option is set, + # Synapse will refuse to connect unless the server supports STARTTLS. + # + #require_transport_security: true + + # Enable sending emails for messages that the user has missed + # + #enable_notifs: false + + # notif_from defines the "From" address to use when sending emails. + # It must be set if email sending is enabled. + # + # The placeholder '%(app)s' will be replaced by the application name, + # which is normally 'app_name' (below), but may be overridden by the + # Matrix client application. + # + # Note that the placeholder must be written '%(app)s', including the + # trailing 's'. + # + #notif_from: "Your Friendly %(app)s homeserver " + + # app_name defines the default value for '%(app)s' in notif_from. It + # defaults to 'Matrix'. + # + #app_name: my_branded_matrix_server + + # Uncomment the following to disable automatic subscription to email + # notifications for new users. Enabled by default. + # + #notif_for_new_users: false + + # Custom URL for client links within the email notifications. By default + # links will be based on "https://matrix.to". + # + # (This setting used to be called riot_base_url; the old name is still + # supported for backwards-compatibility but is now deprecated.) + # + #client_base_url: "http://localhost/riot" + + # Configure the time that a validation email will expire after sending. + # Defaults to 1h. + # + #validation_token_lifetime: 15m + + # Directory in which Synapse will try to find the template files below. + # If not set, default templates from within the Synapse package will be used. + # + # DO NOT UNCOMMENT THIS SETTING unless you want to customise the templates. + # If you *do* uncomment it, you will need to make sure that all the templates + # below are in the directory. + # + # Synapse will look for the following templates in this directory: + # + # * The contents of email notifications of missed events: 'notif_mail.html' and + # 'notif_mail.txt'. + # + # * The contents of account expiry notice emails: 'notice_expiry.html' and + # 'notice_expiry.txt'. + # + # * The contents of password reset emails sent by the homeserver: + # 'password_reset.html' and 'password_reset.txt' + # + # * HTML pages for success and failure that a user will see when they follow + # the link in the password reset email: 'password_reset_success.html' and + # 'password_reset_failure.html' + # + # * The contents of address verification emails sent during registration: + # 'registration.html' and 'registration.txt' + # + # * HTML pages for success and failure that a user will see when they follow + # the link in an address verification email sent during registration: + # 'registration_success.html' and 'registration_failure.html' + # + # * The contents of address verification emails sent when an address is added + # to a Matrix account: 'add_threepid.html' and 'add_threepid.txt' + # + # * HTML pages for success and failure that a user will see when they follow + # the link in an address verification email sent when an address is added + # to a Matrix account: 'add_threepid_success.html' and + # 'add_threepid_failure.html' + # + # You can see the default templates at: + # https://github.com/matrix-org/synapse/tree/master/synapse/res/templates + # + #template_dir: "res/templates" #password_providers: diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py index 35756bed87..74853f9faa 100644 --- a/synapse/config/emailconfig.py +++ b/synapse/config/emailconfig.py @@ -37,10 +37,12 @@ class EmailConfig(Config): self.email_enable_notifs = False - email_config = config.get("email", {}) + email_config = config.get("email") + if email_config is None: + email_config = {} - self.email_smtp_host = email_config.get("smtp_host", None) - self.email_smtp_port = email_config.get("smtp_port", None) + self.email_smtp_host = email_config.get("smtp_host", "localhost") + self.email_smtp_port = email_config.get("smtp_port", 25) self.email_smtp_user = email_config.get("smtp_user", None) self.email_smtp_pass = email_config.get("smtp_pass", None) self.require_transport_security = email_config.get( @@ -74,9 +76,9 @@ class EmailConfig(Config): self.email_template_dir = os.path.abspath(template_dir) self.email_enable_notifs = email_config.get("enable_notifs", False) - account_validity_renewal_enabled = config.get("account_validity", {}).get( - "renew_at" - ) + + account_validity_config = config.get("account_validity") or {} + account_validity_renewal_enabled = account_validity_config.get("renew_at") self.threepid_behaviour_email = ( # Have Synapse handle the email sending if account_threepid_delegates.email @@ -278,7 +280,9 @@ class EmailConfig(Config): self.email_notif_for_new_users = email_config.get( "notif_for_new_users", True ) - self.email_riot_base_url = email_config.get("riot_base_url", None) + self.email_riot_base_url = email_config.get( + "client_base_url", email_config.get("riot_base_url", None) + ) if account_validity_renewal_enabled: self.email_expiry_template_html = email_config.get( @@ -294,107 +298,111 @@ class EmailConfig(Config): raise ConfigError("Unable to find email template file %s" % (p,)) def generate_config_section(self, config_dir_path, server_name, **kwargs): - return """ - # Enable sending emails for password resets, notification events or - # account expiry notices + return """\ + # Configuration for sending emails from Synapse. # - # If your SMTP server requires authentication, the optional smtp_user & - # smtp_pass variables should be used - # - #email: - # enable_notifs: false - # smtp_host: "localhost" - # smtp_port: 25 # SSL: 465, STARTTLS: 587 - # smtp_user: "exampleusername" - # smtp_pass: "examplepassword" - # require_transport_security: false - # - # # notif_from defines the "From" address to use when sending emails. - # # It must be set if email sending is enabled. - # # - # # The placeholder '%(app)s' will be replaced by the application name, - # # which is normally 'app_name' (below), but may be overridden by the - # # Matrix client application. - # # - # # Note that the placeholder must be written '%(app)s', including the - # # trailing 's'. - # # - # notif_from: "Your Friendly %(app)s homeserver " - # - # # app_name defines the default value for '%(app)s' in notif_from. It - # # defaults to 'Matrix'. - # # - # #app_name: my_branded_matrix_server - # - # # Enable email notifications by default - # # - # notif_for_new_users: true - # - # # Defining a custom URL for Riot is only needed if email notifications - # # should contain links to a self-hosted installation of Riot; when set - # # the "app_name" setting is ignored - # # - # riot_base_url: "http://localhost/riot" - # - # # Configure the time that a validation email or text message code - # # will expire after sending - # # - # # This is currently used for password resets - # # - # #validation_token_lifetime: 1h - # - # # Template directory. All template files should be stored within this - # # directory. If not set, default templates from within the Synapse - # # package will be used - # # - # # For the list of default templates, please see - # # https://github.com/matrix-org/synapse/tree/master/synapse/res/templates - # # - # #template_dir: res/templates - # - # # Templates for email notifications - # # - # notif_template_html: notif_mail.html - # notif_template_text: notif_mail.txt - # - # # Templates for account expiry notices - # # - # expiry_template_html: notice_expiry.html - # expiry_template_text: notice_expiry.txt - # - # # Templates for password reset emails sent by the homeserver - # # - # #password_reset_template_html: password_reset.html - # #password_reset_template_text: password_reset.txt - # - # # Templates for registration emails sent by the homeserver - # # - # #registration_template_html: registration.html - # #registration_template_text: registration.txt - # - # # Templates for validation emails sent by the homeserver when adding an email to - # # your user account - # # - # #add_threepid_template_html: add_threepid.html - # #add_threepid_template_text: add_threepid.txt - # - # # Templates for password reset success and failure pages that a user - # # will see after attempting to reset their password - # # - # #password_reset_template_success_html: password_reset_success.html - # #password_reset_template_failure_html: password_reset_failure.html - # - # # Templates for registration success and failure pages that a user - # # will see after attempting to register using an email or phone - # # - # #registration_template_success_html: registration_success.html - # #registration_template_failure_html: registration_failure.html - # - # # Templates for success and failure pages that a user will see after attempting - # # to add an email or phone to their account - # # - # #add_threepid_success_html: add_threepid_success.html - # #add_threepid_failure_html: add_threepid_failure.html + email: + # The hostname of the outgoing SMTP server to use. Defaults to 'localhost'. + # + #smtp_host: mail.server + + # The port on the mail server for outgoing SMTP. Defaults to 25. + # + #smtp_port: 587 + + # Username/password for authentication to the SMTP server. By default, no + # authentication is attempted. + # + # smtp_user: "exampleusername" + # smtp_pass: "examplepassword" + + # Uncomment the following to require TLS transport security for SMTP. + # By default, Synapse will connect over plain text, and will then switch to + # TLS via STARTTLS *if the SMTP server supports it*. If this option is set, + # Synapse will refuse to connect unless the server supports STARTTLS. + # + #require_transport_security: true + + # Enable sending emails for messages that the user has missed + # + #enable_notifs: false + + # notif_from defines the "From" address to use when sending emails. + # It must be set if email sending is enabled. + # + # The placeholder '%(app)s' will be replaced by the application name, + # which is normally 'app_name' (below), but may be overridden by the + # Matrix client application. + # + # Note that the placeholder must be written '%(app)s', including the + # trailing 's'. + # + #notif_from: "Your Friendly %(app)s homeserver " + + # app_name defines the default value for '%(app)s' in notif_from. It + # defaults to 'Matrix'. + # + #app_name: my_branded_matrix_server + + # Uncomment the following to disable automatic subscription to email + # notifications for new users. Enabled by default. + # + #notif_for_new_users: false + + # Custom URL for client links within the email notifications. By default + # links will be based on "https://matrix.to". + # + # (This setting used to be called riot_base_url; the old name is still + # supported for backwards-compatibility but is now deprecated.) + # + #client_base_url: "http://localhost/riot" + + # Configure the time that a validation email will expire after sending. + # Defaults to 1h. + # + #validation_token_lifetime: 15m + + # Directory in which Synapse will try to find the template files below. + # If not set, default templates from within the Synapse package will be used. + # + # DO NOT UNCOMMENT THIS SETTING unless you want to customise the templates. + # If you *do* uncomment it, you will need to make sure that all the templates + # below are in the directory. + # + # Synapse will look for the following templates in this directory: + # + # * The contents of email notifications of missed events: 'notif_mail.html' and + # 'notif_mail.txt'. + # + # * The contents of account expiry notice emails: 'notice_expiry.html' and + # 'notice_expiry.txt'. + # + # * The contents of password reset emails sent by the homeserver: + # 'password_reset.html' and 'password_reset.txt' + # + # * HTML pages for success and failure that a user will see when they follow + # the link in the password reset email: 'password_reset_success.html' and + # 'password_reset_failure.html' + # + # * The contents of address verification emails sent during registration: + # 'registration.html' and 'registration.txt' + # + # * HTML pages for success and failure that a user will see when they follow + # the link in an address verification email sent during registration: + # 'registration_success.html' and 'registration_failure.html' + # + # * The contents of address verification emails sent when an address is added + # to a Matrix account: 'add_threepid.html' and 'add_threepid.txt' + # + # * HTML pages for success and failure that a user will see when they follow + # the link in an address verification email sent when an address is added + # to a Matrix account: 'add_threepid_success.html' and + # 'add_threepid_failure.html' + # + # You can see the default templates at: + # https://github.com/matrix-org/synapse/tree/master/synapse/res/templates + # + #template_dir: "res/templates" """ diff --git a/synapse/config/push.py b/synapse/config/push.py index 0910958649..6f2b3a7faa 100644 --- a/synapse/config/push.py +++ b/synapse/config/push.py @@ -35,7 +35,7 @@ class PushConfig(Config): # Now check for the one in the 'email' section and honour it, # with a warning. - push_config = config.get("email", {}) + push_config = config.get("email") or {} redact_content = push_config.get("redact_content") if redact_content is not None: print( diff --git a/synapse/config/registration.py b/synapse/config/registration.py index ee9614c5f7..b873995a49 100644 --- a/synapse/config/registration.py +++ b/synapse/config/registration.py @@ -27,6 +27,8 @@ class AccountValidityConfig(Config): section = "accountvalidity" def __init__(self, config, synapse_config): + if config is None: + return self.enabled = config.get("enabled", False) self.renew_by_email_enabled = "renew_at" in config @@ -159,23 +161,6 @@ class RegistrationConfig(Config): # Optional account validity configuration. This allows for accounts to be denied # any request after a given period. # - # ``enabled`` defines whether the account validity feature is enabled. Defaults - # to False. - # - # ``period`` allows setting the period after which an account is valid - # after its registration. When renewing the account, its validity period - # will be extended by this amount of time. This parameter is required when using - # the account validity feature. - # - # ``renew_at`` is the amount of time before an account's expiry date at which - # Synapse will send an email to the account's email address with a renewal link. - # This needs the ``email`` and ``public_baseurl`` configuration sections to be - # filled. - # - # ``renew_email_subject`` is the subject of the email sent out with the renewal - # link. ``%%(app)s`` can be used as a placeholder for the ``app_name`` parameter - # from the ``email`` section. - # # Once this feature is enabled, Synapse will look for registered users without an # expiration date at startup and will add one to every account it found using the # current settings at that time. @@ -186,21 +171,55 @@ class RegistrationConfig(Config): # date will be randomly selected within a range [now + period - d ; now + period], # where d is equal to 10%% of the validity period. # - #account_validity: - # enabled: true - # period: 6w - # renew_at: 1w - # renew_email_subject: "Renew your %%(app)s account" - # # Directory in which Synapse will try to find the HTML files to serve to the - # # user when trying to renew an account. Optional, defaults to - # # synapse/res/templates. - # template_dir: "res/templates" - # # HTML to be displayed to the user after they successfully renewed their - # # account. Optional. - # account_renewed_html_path: "account_renewed.html" - # # HTML to be displayed when the user tries to renew an account with an invalid - # # renewal token. Optional. - # invalid_token_html_path: "invalid_token.html" + account_validity: + # The account validity feature is disabled by default. Uncomment the + # following line to enable it. + # + #enabled: true + + # The period after which an account is valid after its registration. When + # renewing the account, its validity period will be extended by this amount + # of time. This parameter is required when using the account validity + # feature. + # + #period: 6w + + # The amount of time before an account's expiry date at which Synapse will + # send an email to the account's email address with a renewal link. By + # default, no such emails are sent. + # + # If you enable this setting, you will also need to fill out the 'email' and + # 'public_baseurl' configuration sections. + # + #renew_at: 1w + + # The subject of the email sent out with the renewal link. '%%(app)s' can be + # used as a placeholder for the 'app_name' parameter from the 'email' + # section. + # + # Note that the placeholder must be written '%%(app)s', including the + # trailing 's'. + # + # If this is not set, a default value is used. + # + #renew_email_subject: "Renew your %%(app)s account" + + # Directory in which Synapse will try to find templates for the HTML files to + # serve to the user when trying to renew an account. If not set, default + # templates from within the Synapse package will be used. + # + #template_dir: "res/templates" + + # File within 'template_dir' giving the HTML to be displayed to the user after + # they successfully renewed their account. If not set, default text is used. + # + #account_renewed_html_path: "account_renewed.html" + + # File within 'template_dir' giving the HTML to be displayed when the user + # tries to renew an account with an invalid renewal token. If not set, + # default text is used. + # + #invalid_token_html_path: "invalid_token.html" # Time that a user's session remains valid for, after they log in. # From a8a50f5b5746279379b4511c8ecb2a40b143fe32 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 17 Jan 2020 10:27:19 +0000 Subject: [PATCH 0842/1623] Wake up transaction queue when remote server comes back online (#6706) This will be used to retry outbound transactions to a remote server if we think it might have come back up. --- changelog.d/6706.misc | 1 + docs/tcp_replication.md | 6 ++++- synapse/app/federation_sender.py | 12 +++++++++- synapse/federation/sender/__init__.py | 18 +++++++++++++-- synapse/federation/transport/server.py | 19 +++++++++++++++- synapse/notifier.py | 31 +++++++++++++++++++++++--- synapse/replication/tcp/client.py | 3 +++ synapse/replication/tcp/commands.py | 17 ++++++++++++++ synapse/replication/tcp/protocol.py | 15 +++++++++++++ synapse/replication/tcp/resource.py | 9 ++++++++ synapse/server.pyi | 12 ++++++++++ 11 files changed, 135 insertions(+), 8 deletions(-) create mode 100644 changelog.d/6706.misc diff --git a/changelog.d/6706.misc b/changelog.d/6706.misc new file mode 100644 index 0000000000..1ac11cc04b --- /dev/null +++ b/changelog.d/6706.misc @@ -0,0 +1 @@ +Attempt to retry sending a transaction when we detect a remote server has come back online, rather than waiting for a transaction to be triggered by new data. diff --git a/docs/tcp_replication.md b/docs/tcp_replication.md index ba9e874d07..a0b1d563ff 100644 --- a/docs/tcp_replication.md +++ b/docs/tcp_replication.md @@ -209,7 +209,7 @@ Where `` may be either: * a numeric stream_id to stream updates since (exclusive) * `NOW` to stream all subsequent updates. -The `` is the name of a replication stream to subscribe +The `` is the name of a replication stream to subscribe to (see [here](../synapse/replication/tcp/streams/_base.py) for a list of streams). It can also be `ALL` to subscribe to all known streams, in which case the `` must be set to `NOW`. @@ -234,6 +234,10 @@ in which case the `` must be set to `NOW`. Used exclusively in tests +### REMOTE_SERVER_UP (S, C) + + Inform other processes that a remote server may have come back online. + See `synapse/replication/tcp/commands.py` for a detailed description and the format of each command. diff --git a/synapse/app/federation_sender.py b/synapse/app/federation_sender.py index a57cf991ac..38d11fdd0f 100644 --- a/synapse/app/federation_sender.py +++ b/synapse/app/federation_sender.py @@ -158,6 +158,13 @@ class FederationSenderReplicationHandler(ReplicationClientHandler): args.update(self.send_handler.stream_positions()) return args + def on_remote_server_up(self, server: str): + """Called when get a new REMOTE_SERVER_UP command.""" + + # Let's wake up the transaction queue for the server in case we have + # pending stuff to send to it. + self.send_handler.wake_destination(server) + def start(config_options): try: @@ -205,7 +212,7 @@ class FederationSenderHandler(object): to the federation sender. """ - def __init__(self, hs, replication_client): + def __init__(self, hs: FederationSenderServer, replication_client): self.store = hs.get_datastore() self._is_mine_id = hs.is_mine_id self.federation_sender = hs.get_federation_sender() @@ -226,6 +233,9 @@ class FederationSenderHandler(object): self.store.get_room_max_stream_ordering() ) + def wake_destination(self, server: str): + self.federation_sender.wake_destination(server) + def stream_positions(self): return {"federation": self.federation_position} diff --git a/synapse/federation/sender/__init__.py b/synapse/federation/sender/__init__.py index 4ebb0e8bc0..36c83c3027 100644 --- a/synapse/federation/sender/__init__.py +++ b/synapse/federation/sender/__init__.py @@ -21,6 +21,7 @@ from prometheus_client import Counter from twisted.internet import defer +import synapse import synapse.metrics from synapse.federation.sender.per_destination_queue import PerDestinationQueue from synapse.federation.sender.transaction_manager import TransactionManager @@ -54,7 +55,7 @@ sent_pdus_destination_dist_total = Counter( class FederationSender(object): - def __init__(self, hs): + def __init__(self, hs: "synapse.server.HomeServer"): self.hs = hs self.server_name = hs.hostname @@ -482,7 +483,20 @@ class FederationSender(object): def send_device_messages(self, destination): if destination == self.server_name: - logger.info("Not sending device update to ourselves") + logger.warning("Not sending device update to ourselves") + return + + self._get_per_destination_queue(destination).attempt_new_transaction() + + def wake_destination(self, destination: str): + """Called when we want to retry sending transactions to a remote. + + This is mainly useful if the remote server has been down and we think it + might have come back. + """ + + if destination == self.server_name: + logger.warning("Not waking up ourselves") return self._get_per_destination_queue(destination).attempt_new_transaction() diff --git a/synapse/federation/transport/server.py b/synapse/federation/transport/server.py index b4cbf23394..d8cf9ed299 100644 --- a/synapse/federation/transport/server.py +++ b/synapse/federation/transport/server.py @@ -44,6 +44,7 @@ from synapse.logging.opentracing import ( tags, whitelisted_homeserver, ) +from synapse.server import HomeServer from synapse.types import ThirdPartyInstanceID, get_domain_from_id from synapse.util.ratelimitutils import FederationRateLimiter from synapse.util.versionstring import get_version_string @@ -101,12 +102,17 @@ class NoAuthenticationError(AuthenticationError): class Authenticator(object): - def __init__(self, hs): + def __init__(self, hs: HomeServer): self._clock = hs.get_clock() self.keyring = hs.get_keyring() self.server_name = hs.hostname self.store = hs.get_datastore() self.federation_domain_whitelist = hs.config.federation_domain_whitelist + self.notifer = hs.get_notifier() + + self.replication_client = None + if hs.config.worker.worker_app: + self.replication_client = hs.get_tcp_replication() # A method just so we can pass 'self' as the authenticator to the Servlets async def authenticate_request(self, request, content): @@ -166,6 +172,17 @@ class Authenticator(object): try: logger.info("Marking origin %r as up", origin) await self.store.set_destination_retry_timings(origin, None, 0, 0) + + # Inform the relevant places that the remote server is back up. + self.notifer.notify_remote_server_up(origin) + if self.replication_client: + # If we're on a worker we try and inform master about this. The + # replication client doesn't hook into the notifier to avoid + # infinite loops where we send a `REMOTE_SERVER_UP` command to + # master, which then echoes it back to us which in turn pokes + # the notifier. + self.replication_client.send_remote_server_up(origin) + except Exception: logger.exception("Error resetting retry timings on %s", origin) diff --git a/synapse/notifier.py b/synapse/notifier.py index 5f5f765bea..6132727cbd 100644 --- a/synapse/notifier.py +++ b/synapse/notifier.py @@ -15,11 +15,13 @@ import logging from collections import namedtuple +from typing import Callable, List from prometheus_client import Counter from twisted.internet import defer +import synapse.server from synapse.api.constants import EventTypes, Membership from synapse.api.errors import AuthError from synapse.handlers.presence import format_user_presence_state @@ -154,7 +156,7 @@ class Notifier(object): UNUSED_STREAM_EXPIRY_MS = 10 * 60 * 1000 - def __init__(self, hs): + def __init__(self, hs: "synapse.server.HomeServer"): self.user_to_user_stream = {} self.room_to_user_streams = {} @@ -164,7 +166,12 @@ class Notifier(object): self.store = hs.get_datastore() self.pending_new_room_events = [] - self.replication_callbacks = [] + # Called when there are new things to stream over replication + self.replication_callbacks = [] # type: List[Callable[[], None]] + + # Called when remote servers have come back online after having been + # down. + self.remote_server_up_callbacks = [] # type: List[Callable[[str], None]] self.clock = hs.get_clock() self.appservice_handler = hs.get_application_service_handler() @@ -205,7 +212,7 @@ class Notifier(object): "synapse_notifier_users", "", [], lambda: len(self.user_to_user_stream) ) - def add_replication_callback(self, cb): + def add_replication_callback(self, cb: Callable[[], None]): """Add a callback that will be called when some new data is available. Callback is not given any arguments. It should *not* return a Deferred - if it needs to do any asynchronous work, a background thread should be started and @@ -213,6 +220,12 @@ class Notifier(object): """ self.replication_callbacks.append(cb) + def add_remote_server_up_callback(self, cb: Callable[[str], None]): + """Add a callback that will be called when synapse detects a server + has been + """ + self.remote_server_up_callbacks.append(cb) + def on_new_room_event( self, event, room_stream_id, max_room_stream_id, extra_users=[] ): @@ -522,3 +535,15 @@ class Notifier(object): """Notify the any replication listeners that there's a new event""" for cb in self.replication_callbacks: cb() + + def notify_remote_server_up(self, server: str): + """Notify any replication that a remote server has come back up + """ + # We call federation_sender directly rather than registering as a + # callback as a) we already have a reference to it and b) it introduces + # circular dependencies. + if self.federation_sender: + self.federation_sender.wake_destination(server) + + for cb in self.remote_server_up_callbacks: + cb(server) diff --git a/synapse/replication/tcp/client.py b/synapse/replication/tcp/client.py index 52a0aefe68..fc06a7b053 100644 --- a/synapse/replication/tcp/client.py +++ b/synapse/replication/tcp/client.py @@ -143,6 +143,9 @@ class ReplicationClientHandler(AbstractReplicationClientHandler): if d: d.callback(data) + def on_remote_server_up(self, server: str): + """Called when get a new REMOTE_SERVER_UP command.""" + def get_streams_to_replicate(self) -> Dict[str, int]: """Called when a new connection has been established and we need to subscribe to streams. diff --git a/synapse/replication/tcp/commands.py b/synapse/replication/tcp/commands.py index cbb36b9acf..451671412d 100644 --- a/synapse/replication/tcp/commands.py +++ b/synapse/replication/tcp/commands.py @@ -387,6 +387,20 @@ class UserIpCommand(Command): ) +class RemoteServerUpCommand(Command): + """Sent when a worker has detected that a remote server is no longer + "down" and retry timings should be reset. + + If sent from a client the server will relay to all other workers. + + Format:: + + REMOTE_SERVER_UP + """ + + NAME = "REMOTE_SERVER_UP" + + _COMMANDS = ( ServerCommand, RdataCommand, @@ -401,6 +415,7 @@ _COMMANDS = ( RemovePusherCommand, InvalidateCacheCommand, UserIpCommand, + RemoteServerUpCommand, ) # type: Tuple[Type[Command], ...] # Map of command name to command type. @@ -414,6 +429,7 @@ VALID_SERVER_COMMANDS = ( ErrorCommand.NAME, PingCommand.NAME, SyncCommand.NAME, + RemoteServerUpCommand.NAME, ) # The commands the client is allowed to send @@ -427,4 +443,5 @@ VALID_CLIENT_COMMANDS = ( InvalidateCacheCommand.NAME, UserIpCommand.NAME, ErrorCommand.NAME, + RemoteServerUpCommand.NAME, ) diff --git a/synapse/replication/tcp/protocol.py b/synapse/replication/tcp/protocol.py index 5f4bdf84d2..131e5acb09 100644 --- a/synapse/replication/tcp/protocol.py +++ b/synapse/replication/tcp/protocol.py @@ -76,6 +76,7 @@ from synapse.replication.tcp.commands import ( PingCommand, PositionCommand, RdataCommand, + RemoteServerUpCommand, ReplicateCommand, ServerCommand, SyncCommand, @@ -460,6 +461,9 @@ class ServerReplicationStreamProtocol(BaseReplicationStreamProtocol): async def on_INVALIDATE_CACHE(self, cmd): self.streamer.on_invalidate_cache(cmd.cache_func, cmd.keys) + async def on_REMOTE_SERVER_UP(self, cmd: RemoteServerUpCommand): + self.streamer.on_remote_server_up(cmd.data) + async def on_USER_IP(self, cmd): self.streamer.on_user_ip( cmd.user_id, @@ -555,6 +559,9 @@ class ServerReplicationStreamProtocol(BaseReplicationStreamProtocol): def send_sync(self, data): self.send_command(SyncCommand(data)) + def send_remote_server_up(self, server: str): + self.send_command(RemoteServerUpCommand(server)) + def on_connection_closed(self): BaseReplicationStreamProtocol.on_connection_closed(self) self.streamer.lost_connection(self) @@ -588,6 +595,11 @@ class AbstractReplicationClientHandler(metaclass=abc.ABCMeta): """Called when get a new SYNC command.""" raise NotImplementedError() + @abc.abstractmethod + async def on_remote_server_up(self, server: str): + """Called when get a new REMOTE_SERVER_UP command.""" + raise NotImplementedError() + @abc.abstractmethod def get_streams_to_replicate(self): """Called when a new connection has been established and we need to @@ -707,6 +719,9 @@ class ClientReplicationStreamProtocol(BaseReplicationStreamProtocol): async def on_SYNC(self, cmd): self.handler.on_sync(cmd.data) + async def on_REMOTE_SERVER_UP(self, cmd: RemoteServerUpCommand): + self.handler.on_remote_server_up(cmd.data) + def replicate(self, stream_name, token): """Send the subscription request to the server """ diff --git a/synapse/replication/tcp/resource.py b/synapse/replication/tcp/resource.py index b1752e88cd..6ebf944f66 100644 --- a/synapse/replication/tcp/resource.py +++ b/synapse/replication/tcp/resource.py @@ -120,6 +120,7 @@ class ReplicationStreamer(object): self.federation_sender = hs.get_federation_sender() self.notifier.add_replication_callback(self.on_notifier_poke) + self.notifier.add_remote_server_up_callback(self.send_remote_server_up) # Keeps track of whether we are currently checking for updates self.is_looping = False @@ -288,6 +289,14 @@ class ReplicationStreamer(object): ) await self._server_notices_sender.on_user_ip(user_id) + @measure_func("repl.on_remote_server_up") + def on_remote_server_up(self, server: str): + self.notifier.notify_remote_server_up(server) + + def send_remote_server_up(self, server: str): + for conn in self.connections: + conn.send_remote_server_up(server) + def send_sync_to_all_connections(self, data): """Sends a SYNC command to all clients. diff --git a/synapse/server.pyi b/synapse/server.pyi index b5e0b57095..0731403047 100644 --- a/synapse/server.pyi +++ b/synapse/server.pyi @@ -1,3 +1,5 @@ +import twisted.internet + import synapse.api.auth import synapse.config.homeserver import synapse.federation.sender @@ -9,10 +11,12 @@ import synapse.handlers.deactivate_account import synapse.handlers.device import synapse.handlers.e2e_keys import synapse.handlers.message +import synapse.handlers.presence import synapse.handlers.room import synapse.handlers.room_member import synapse.handlers.set_password import synapse.http.client +import synapse.notifier import synapse.rest.media.v1.media_repository import synapse.server_notices.server_notices_manager import synapse.server_notices.server_notices_sender @@ -85,3 +89,11 @@ class HomeServer(object): self, ) -> synapse.server_notices.server_notices_sender.ServerNoticesSender: pass + def get_notifier(self) -> synapse.notifier.Notifier: + pass + def get_presence_handler(self) -> synapse.handlers.presence.PresenceHandler: + pass + def get_clock(self) -> synapse.util.Clock: + pass + def get_reactor(self) -> twisted.internet.base.ReactorBase: + pass From 2b6a77fcde8396331a790a5ddeaa744093a8c728 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 17 Jan 2020 10:32:47 +0000 Subject: [PATCH 0843/1623] Delegate remote_user_id mapping to the saml mapping provider (#6723) Turns out that figuring out a remote user id for the SAML user isn't quite as obvious as it seems. Factor it out to the SamlMappingProvider so that it's easy to control. --- changelog.d/6723.misc | 1 + synapse/config/saml2_config.py | 1 + synapse/handlers/saml_handler.py | 27 +++++++++++++++++++++------ 3 files changed, 23 insertions(+), 6 deletions(-) create mode 100644 changelog.d/6723.misc diff --git a/changelog.d/6723.misc b/changelog.d/6723.misc new file mode 100644 index 0000000000..17f15e73a8 --- /dev/null +++ b/changelog.d/6723.misc @@ -0,0 +1 @@ +Updates to the SAML mapping provider API. diff --git a/synapse/config/saml2_config.py b/synapse/config/saml2_config.py index b91414aa35..423c158b11 100644 --- a/synapse/config/saml2_config.py +++ b/synapse/config/saml2_config.py @@ -121,6 +121,7 @@ class SAML2Config(Config): required_methods = [ "get_saml_attributes", "saml_response_to_user_attributes", + "get_remote_user_id", ] missing_methods = [ method diff --git a/synapse/handlers/saml_handler.py b/synapse/handlers/saml_handler.py index 107f97032b..90e69b49ee 100644 --- a/synapse/handlers/saml_handler.py +++ b/synapse/handlers/saml_handler.py @@ -135,14 +135,15 @@ class SamlHandler: logger.info("SAML2 response: %s", saml2_auth.origxml) logger.info("SAML2 mapped attributes: %s", saml2_auth.ava) - try: - remote_user_id = saml2_auth.ava["uid"][0] - except KeyError: - logger.warning("SAML2 response lacks a 'uid' attestation") - raise SynapseError(400, "'uid' not in SAML2 response") - self._outstanding_requests_dict.pop(saml2_auth.in_response_to, None) + remote_user_id = self._user_mapping_provider.get_remote_user_id( + saml2_auth, client_redirect_url + ) + + if not remote_user_id: + raise Exception("Failed to extract remote user id from SAML response") + with (await self._mapping_lock.queue(self._auth_provider_id)): # first of all, check if we already have a mapping for this user logger.info( @@ -279,6 +280,20 @@ class DefaultSamlMappingProvider(object): self._mxid_source_attribute = parsed_config.mxid_source_attribute self._mxid_mapper = parsed_config.mxid_mapper + self._grandfathered_mxid_source_attribute = ( + module_api._hs.config.saml2_grandfathered_mxid_source_attribute + ) + + def get_remote_user_id( + self, saml_response: saml2.response.AuthnResponse, client_redirect_url: str + ): + """Extracts the remote user id from the SAML response""" + try: + return saml_response.ava["uid"][0] + except KeyError: + logger.warning("SAML2 response lacks a 'uid' attestation") + raise SynapseError(400, "'uid' not in SAML2 response") + def saml_response_to_user_attributes( self, saml_response: saml2.response.AuthnResponse, From 1dee1e900bd3964461d932944883ffaf5df58eab Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 17 Jan 2020 10:43:58 +0000 Subject: [PATCH 0844/1623] bump version to v1.9.0.dev1 --- synapse/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/__init__.py b/synapse/__init__.py index 0dd538d804..abd5297390 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -36,7 +36,7 @@ try: except ImportError: pass -__version__ = "1.8.0" +__version__ = "1.9.0.dev1" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 722b4f302d705f497355f206ecb160de1bef2074 Mon Sep 17 00:00:00 2001 From: Satsuki Yanagi <17376330+u1-liquid@users.noreply.github.com> Date: Fri, 17 Jan 2020 23:30:35 +0900 Subject: [PATCH 0845/1623] Fix syntax error in run_upgrade for schema 57 (#6728) Fix #6727 Related #6655 Co-authored-by: Erik Johnston --- changelog.d/6728.bugfix | 1 + .../main/schema/delta/57/local_current_membership.py | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 changelog.d/6728.bugfix diff --git a/changelog.d/6728.bugfix b/changelog.d/6728.bugfix new file mode 100644 index 0000000000..5a136e17be --- /dev/null +++ b/changelog.d/6728.bugfix @@ -0,0 +1 @@ +Fix a bug causing `ValueError: unsupported format character ''' (0x27) at index 312` error when running the schema 57 upgrade script. diff --git a/synapse/storage/data_stores/main/schema/delta/57/local_current_membership.py b/synapse/storage/data_stores/main/schema/delta/57/local_current_membership.py index 601c236c4a..63b5acdcf7 100644 --- a/synapse/storage/data_stores/main/schema/delta/57/local_current_membership.py +++ b/synapse/storage/data_stores/main/schema/delta/57/local_current_membership.py @@ -56,7 +56,7 @@ def run_upgrade(cur, database_engine, config, *args, **kwargs): INSERT INTO local_current_membership (room_id, user_id, event_id, membership) SELECT c.room_id, state_key AS user_id, event_id, c.membership FROM current_state_events AS c - WHERE type = 'm.room.member' AND c.membership IS NOT NULL AND state_key like '%' || ? + WHERE type = 'm.room.member' AND c.membership IS NOT NULL AND state_key LIKE ? """ else: # We can't rely on the membership column, so we need to join against @@ -66,9 +66,10 @@ def run_upgrade(cur, database_engine, config, *args, **kwargs): SELECT c.room_id, state_key AS user_id, event_id, r.membership FROM current_state_events AS c INNER JOIN room_memberships AS r USING (event_id) - WHERE type = 'm.room.member' and state_key like '%' || ? + WHERE type = 'm.room.member' AND state_key LIKE ? """ - cur.execute(sql, (config.server_name,)) + sql = database_engine.convert_param_style(sql) + cur.execute(sql, ("%:" + config.server_name,)) cur.execute( "CREATE UNIQUE INDEX local_current_membership_idx ON local_current_membership(user_id, room_id)" From 0b885d62efdc25000ad44a050e669683668dfa3c Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 17 Jan 2020 14:58:58 +0000 Subject: [PATCH 0846/1623] bump version to v1.9.0.dev2 --- synapse/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/__init__.py b/synapse/__init__.py index abd5297390..17a6f691c8 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -36,7 +36,7 @@ try: except ImportError: pass -__version__ = "1.9.0.dev1" +__version__ = "1.9.0.dev2" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 5909751936cca2e394cb30fb5da9520db76ee73a Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 17 Jan 2020 15:13:27 +0000 Subject: [PATCH 0847/1623] Fix up changelog --- changelog.d/6728.bugfix | 1 - changelog.d/6728.misc | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 changelog.d/6728.bugfix create mode 100644 changelog.d/6728.misc diff --git a/changelog.d/6728.bugfix b/changelog.d/6728.bugfix deleted file mode 100644 index 5a136e17be..0000000000 --- a/changelog.d/6728.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a bug causing `ValueError: unsupported format character ''' (0x27) at index 312` error when running the schema 57 upgrade script. diff --git a/changelog.d/6728.misc b/changelog.d/6728.misc new file mode 100644 index 0000000000..01e78bc84e --- /dev/null +++ b/changelog.d/6728.misc @@ -0,0 +1 @@ +Add `local_current_membership` table for tracking local user membership state in rooms. From a17f64361c87f06c67fd7bb5a98b54dc5a2bb4fb Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 17 Jan 2020 20:51:44 +0000 Subject: [PATCH 0848/1623] Add more logging around message retention policies support (#6717) So we can debug issues like #6683 more easily --- changelog.d/6717.misc | 1 + synapse/config/server.py | 8 ++++++++ synapse/handlers/pagination.py | 13 +++++++++++++ 3 files changed, 22 insertions(+) create mode 100644 changelog.d/6717.misc diff --git a/changelog.d/6717.misc b/changelog.d/6717.misc new file mode 100644 index 0000000000..a2a7776126 --- /dev/null +++ b/changelog.d/6717.misc @@ -0,0 +1 @@ +Add more logging around message retention policies support. diff --git a/synapse/config/server.py b/synapse/config/server.py index 9ac112233b..0ec1b0fadd 100644 --- a/synapse/config/server.py +++ b/synapse/config/server.py @@ -294,6 +294,14 @@ class ServerConfig(Config): self.retention_default_min_lifetime = None self.retention_default_max_lifetime = None + if self.retention_enabled: + logger.info( + "Message retention policies support enabled with the following default" + " policy: min_lifetime = %s ; max_lifetime = %s", + self.retention_default_min_lifetime, + self.retention_default_max_lifetime, + ) + self.retention_allowed_lifetime_min = retention_config.get( "allowed_lifetime_min" ) diff --git a/synapse/handlers/pagination.py b/synapse/handlers/pagination.py index 3ee6a091c5..71d76202c9 100644 --- a/synapse/handlers/pagination.py +++ b/synapse/handlers/pagination.py @@ -88,6 +88,8 @@ class PaginationHandler(object): if hs.config.retention_enabled: # Run the purge jobs described in the configuration file. for job in hs.config.retention_purge_jobs: + logger.info("Setting up purge job with config: %s", job) + self.clock.looping_call( run_as_background_process, job["interval"], @@ -130,11 +132,22 @@ class PaginationHandler(object): else: include_null = False + logger.info( + "[purge] Running purge job for %d < max_lifetime <= %d (include NULLs = %s)", + min_ms, + max_ms, + include_null, + ) + rooms = yield self.store.get_rooms_for_retention_period_in_range( min_ms, max_ms, include_null ) + logger.debug("[purge] Rooms to purge: %s", rooms) + for room_id, retention_policy in iteritems(rooms): + logger.info("[purge] Attempting to purge messages in room %s", room_id) + if room_id in self._purges_in_progress_by_room: logger.warning( "[purge] not purging room %s as there's an ongoing purge running" From 198d52da3a10769b74fcec718ed9855e979f2786 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Mon, 20 Jan 2020 14:01:36 +0000 Subject: [PATCH 0849/1623] Fix empty account_validity config block --- synapse/config/registration.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/synapse/config/registration.py b/synapse/config/registration.py index b873995a49..9bb3beedbc 100644 --- a/synapse/config/registration.py +++ b/synapse/config/registration.py @@ -29,6 +29,7 @@ class AccountValidityConfig(Config): def __init__(self, config, synapse_config): if config is None: return + super(AccountValidityConfig, self).__init__() self.enabled = config.get("enabled", False) self.renew_by_email_enabled = "renew_at" in config @@ -93,7 +94,7 @@ class RegistrationConfig(Config): ) self.account_validity = AccountValidityConfig( - config.get("account_validity", {}), config + config.get("account_validity") or {}, config ) self.registrations_require_3pid = config.get("registrations_require_3pid", []) From 026f4bdf3c97513b6b48e1f3857198cdb22a3334 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Mon, 20 Jan 2020 14:11:42 +0000 Subject: [PATCH 0850/1623] Add changelog --- changelog.d/6747.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6747.bugfix diff --git a/changelog.d/6747.bugfix b/changelog.d/6747.bugfix new file mode 100644 index 0000000000..cb088873e5 --- /dev/null +++ b/changelog.d/6747.bugfix @@ -0,0 +1 @@ +Fix infinite recursion and dictionary access bug when setting `account_validity` to an empty block in the homeserver config. Thanks to @Sorunome for reporting. \ No newline at end of file From 11c23af465633b8dd641db60793a19e5b7524e2b Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 20 Jan 2020 15:11:38 +0000 Subject: [PATCH 0851/1623] Newsfile --- changelog.d/6748.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6748.misc diff --git a/changelog.d/6748.misc b/changelog.d/6748.misc new file mode 100644 index 0000000000..de320d4cd9 --- /dev/null +++ b/changelog.d/6748.misc @@ -0,0 +1 @@ +Propagate cache invalidates from workers to other workers. From 2f23eb27b30bef922cfffdd22e046985f18302ac Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 20 Jan 2020 15:12:58 +0000 Subject: [PATCH 0852/1623] Revert "Newsfile" This reverts commit 11c23af465633b8dd641db60793a19e5b7524e2b. --- changelog.d/6748.misc | 1 - 1 file changed, 1 deletion(-) delete mode 100644 changelog.d/6748.misc diff --git a/changelog.d/6748.misc b/changelog.d/6748.misc deleted file mode 100644 index de320d4cd9..0000000000 --- a/changelog.d/6748.misc +++ /dev/null @@ -1 +0,0 @@ -Propagate cache invalidates from workers to other workers. From 351fdfede6e9582f7c365d41c684b9d60b6c98c2 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Mon, 20 Jan 2020 15:58:44 +0000 Subject: [PATCH 0853/1623] Update changelog.d/6747.bugfix Co-Authored-By: Erik Johnston --- changelog.d/6747.bugfix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/6747.bugfix b/changelog.d/6747.bugfix index cb088873e5..c98107e741 100644 --- a/changelog.d/6747.bugfix +++ b/changelog.d/6747.bugfix @@ -1 +1 @@ -Fix infinite recursion and dictionary access bug when setting `account_validity` to an empty block in the homeserver config. Thanks to @Sorunome for reporting. \ No newline at end of file +Fix bug when setting `account_validity` to an empty block in the config. Thanks to @Sorunome for reporting. From ceecedc68ba1af25b0ee60c5cf927fd1fd245b9f Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 20 Jan 2020 17:23:59 +0000 Subject: [PATCH 0854/1623] Fix changing password via user admin API. (#6730) --- changelog.d/6730.bugfix | 1 + synapse/rest/admin/users.py | 4 ++-- tests/rest/admin/test_user.py | 13 +++++++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 changelog.d/6730.bugfix diff --git a/changelog.d/6730.bugfix b/changelog.d/6730.bugfix new file mode 100644 index 0000000000..beb444ca66 --- /dev/null +++ b/changelog.d/6730.bugfix @@ -0,0 +1 @@ +Fix changing password via user admin API. diff --git a/synapse/rest/admin/users.py b/synapse/rest/admin/users.py index 574cb90c74..c178c960c5 100644 --- a/synapse/rest/admin/users.py +++ b/synapse/rest/admin/users.py @@ -193,8 +193,8 @@ class UserRestServletV2(RestServlet): raise SynapseError(400, "Invalid password") else: new_password = body["password"] - await self._set_password_handler.set_password( - target_user, new_password, requester + await self.set_password_handler.set_password( + target_user.to_string(), new_password, requester ) if "deactivated" in body: diff --git a/tests/rest/admin/test_user.py b/tests/rest/admin/test_user.py index 7352d609e6..8f09f51c61 100644 --- a/tests/rest/admin/test_user.py +++ b/tests/rest/admin/test_user.py @@ -435,6 +435,19 @@ class UserRestTestCase(unittest.HomeserverTestCase): self.assertEqual(0, channel.json_body["is_guest"]) self.assertEqual(0, channel.json_body["deactivated"]) + # Change password + body = json.dumps({"password": "hahaha"}) + + request, channel = self.make_request( + "PUT", + self.url, + access_token=self.admin_user_tok, + content=body.encode(encoding="utf_8"), + ) + self.render(request) + + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + # Modify user body = json.dumps({"displayname": "foobar", "deactivated": True}) From 0f6e525be309b65e07066c071b2f55ebbaac6862 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 20 Jan 2020 17:34:13 +0000 Subject: [PATCH 0855/1623] Fixup synapse.api to pass mypy (#6733) --- changelog.d/6733.misc | 1 + mypy.ini | 3 +++ synapse/api/filtering.py | 4 +++- synapse/api/ratelimiting.py | 7 +++++-- synapse/event_auth.py | 2 +- tox.ini | 1 + 6 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 changelog.d/6733.misc diff --git a/changelog.d/6733.misc b/changelog.d/6733.misc new file mode 100644 index 0000000000..bf048c0be2 --- /dev/null +++ b/changelog.d/6733.misc @@ -0,0 +1 @@ +Fixup synapse.api to pass mypy. diff --git a/mypy.ini b/mypy.ini index a66434b76b..e3c515e2c4 100644 --- a/mypy.ini +++ b/mypy.ini @@ -7,6 +7,9 @@ show_error_codes = True show_traceback = True mypy_path = stubs +[mypy-pymacaroons.*] +ignore_missing_imports = True + [mypy-zope] ignore_missing_imports = True diff --git a/synapse/api/filtering.py b/synapse/api/filtering.py index 6eab1f13f0..8b64d0a285 100644 --- a/synapse/api/filtering.py +++ b/synapse/api/filtering.py @@ -15,6 +15,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from typing import List + from six import text_type import jsonschema @@ -293,7 +295,7 @@ class Filter(object): room_id = None ev_type = "m.presence" contains_url = False - labels = [] + labels = [] # type: List[str] else: sender = event.get("sender", None) if not sender: diff --git a/synapse/api/ratelimiting.py b/synapse/api/ratelimiting.py index 172841f595..7a049b3af7 100644 --- a/synapse/api/ratelimiting.py +++ b/synapse/api/ratelimiting.py @@ -12,7 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -import collections +from collections import OrderedDict +from typing import Any, Optional, Tuple from synapse.api.errors import LimitExceededError @@ -23,7 +24,9 @@ class Ratelimiter(object): """ def __init__(self): - self.message_counts = collections.OrderedDict() + self.message_counts = ( + OrderedDict() + ) # type: OrderedDict[Any, Tuple[float, int, Optional[float]]] def can_do_action(self, key, time_now_s, rate_hz, burst_count, update=True): """Can the entity (e.g. user or IP address) perform the action? diff --git a/synapse/event_auth.py b/synapse/event_auth.py index 1033e5e121..e3a1ba47a0 100644 --- a/synapse/event_auth.py +++ b/synapse/event_auth.py @@ -634,7 +634,7 @@ def get_public_keys(invite_event): return public_keys -def auth_types_for_event(event) -> Set[Tuple[str]]: +def auth_types_for_event(event) -> Set[Tuple[str, str]]: """Given an event, return a list of (EventType, StateKey) that may be needed to auth the event. The returned list may be a superset of what would actually be required depending on the full state of the room. diff --git a/tox.ini b/tox.ini index b73a993053..edf4654177 100644 --- a/tox.ini +++ b/tox.ini @@ -177,6 +177,7 @@ env = MYPYPATH = stubs/ extras = all commands = mypy \ + synapse/api \ synapse/config/ \ synapse/handlers/ui_auth \ synapse/logging/ \ From 74b74462f1c8b2db9b0995cbf64d879cbfce0dc4 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 20 Jan 2020 17:38:09 +0000 Subject: [PATCH 0856/1623] Fix `/events/:event_id` deprecated API. (#6731) --- changelog.d/6731.bugfix | 1 + synapse/rest/client/v1/events.py | 2 +- tests/rest/client/v1/test_events.py | 27 +++++++++++++++++++++++++++ tests/unittest.py | 2 +- 4 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 changelog.d/6731.bugfix diff --git a/changelog.d/6731.bugfix b/changelog.d/6731.bugfix new file mode 100644 index 0000000000..21f6e15cbd --- /dev/null +++ b/changelog.d/6731.bugfix @@ -0,0 +1 @@ +Fix `/events/:event_id` deprecated API. diff --git a/synapse/rest/client/v1/events.py b/synapse/rest/client/v1/events.py index 4beb617733..25effd0261 100644 --- a/synapse/rest/client/v1/events.py +++ b/synapse/rest/client/v1/events.py @@ -70,7 +70,6 @@ class EventStreamRestServlet(RestServlet): return 200, {} -# TODO: Unit test gets, with and without auth, with different kinds of events. class EventRestServlet(RestServlet): PATTERNS = client_patterns("/events/(?P[^/]*)$", v1=True) @@ -78,6 +77,7 @@ class EventRestServlet(RestServlet): super(EventRestServlet, self).__init__() self.clock = hs.get_clock() self.event_handler = hs.get_event_handler() + self.auth = hs.get_auth() self._event_serializer = hs.get_event_client_serializer() async def on_GET(self, request, event_id): diff --git a/tests/rest/client/v1/test_events.py b/tests/rest/client/v1/test_events.py index f340b7e851..ffb2de1505 100644 --- a/tests/rest/client/v1/test_events.py +++ b/tests/rest/client/v1/test_events.py @@ -134,3 +134,30 @@ class EventStreamPermissionsTestCase(unittest.HomeserverTestCase): # someone else set topic, expect 6 (join,send,topic,join,send,topic) pass + + +class GetEventsTestCase(unittest.HomeserverTestCase): + servlets = [ + events.register_servlets, + room.register_servlets, + synapse.rest.admin.register_servlets_for_client_rest_resource, + login.register_servlets, + ] + + def prepare(self, hs, reactor, clock): + + # register an account + self.user_id = self.register_user("sid1", "pass") + self.token = self.login(self.user_id, "pass") + + self.room_id = self.helper.create_room_as(self.user_id, tok=self.token) + + def test_get_event_via_events(self): + resp = self.helper.send(self.room_id, tok=self.token) + event_id = resp["event_id"] + + request, channel = self.make_request( + "GET", "/events/" + event_id, access_token=self.token, + ) + self.render(request) + self.assertEquals(channel.code, 200, msg=channel.result) diff --git a/tests/unittest.py b/tests/unittest.py index ddcd4becfe..b56e249386 100644 --- a/tests/unittest.py +++ b/tests/unittest.py @@ -463,7 +463,7 @@ class HomeserverTestCase(TestCase): # Create the user request, channel = self.make_request("GET", "/_matrix/client/r0/admin/register") self.render(request) - self.assertEqual(channel.code, 200) + self.assertEqual(channel.code, 200, msg=channel.result) nonce = channel.json_body["nonce"] want_mac = hmac.new(key=b"shared", digestmod=hashlib.sha1) From b0a66ab83ce4d67e145a1129b1ebd8fc53c24408 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 20 Jan 2020 17:38:21 +0000 Subject: [PATCH 0857/1623] Fixup synapse.rest to pass mypy (#6732) --- changelog.d/6732.misc | 1 + mypy.ini | 9 ++++++++ synapse/rest/admin/users.py | 23 ++++++++++--------- synapse/rest/client/v1/login.py | 2 +- synapse/rest/client/v1/room.py | 18 ++++++++++----- synapse/rest/client/v2_alpha/register.py | 3 ++- synapse/rest/client/v2_alpha/sendtodevice.py | 3 ++- synapse/rest/key/v2/remote_key_resource.py | 5 ++-- synapse/rest/media/v1/media_repository.py | 3 ++- synapse/rest/media/v1/preview_url_resource.py | 7 +++--- synapse/rest/media/v1/thumbnail_resource.py | 14 +++++------ tox.ini | 3 +-- 12 files changed, 56 insertions(+), 35 deletions(-) create mode 100644 changelog.d/6732.misc diff --git a/changelog.d/6732.misc b/changelog.d/6732.misc new file mode 100644 index 0000000000..8edd767405 --- /dev/null +++ b/changelog.d/6732.misc @@ -0,0 +1 @@ +Fixup `synapse.rest` to pass mypy. diff --git a/mypy.ini b/mypy.ini index e3c515e2c4..69be2f67ad 100644 --- a/mypy.ini +++ b/mypy.ini @@ -66,3 +66,12 @@ ignore_missing_imports = True [mypy-sentry_sdk] ignore_missing_imports = True + +[mypy-PIL.*] +ignore_missing_imports = True + +[mypy-lxml] +ignore_missing_imports = True + +[mypy-jwt.*] +ignore_missing_imports = True diff --git a/synapse/rest/admin/users.py b/synapse/rest/admin/users.py index c178c960c5..52d27fa3e3 100644 --- a/synapse/rest/admin/users.py +++ b/synapse/rest/admin/users.py @@ -338,21 +338,22 @@ class UserRegisterServlet(RestServlet): got_mac = body["mac"] - want_mac = hmac.new( + want_mac_builder = hmac.new( key=self.hs.config.registration_shared_secret.encode(), digestmod=hashlib.sha1, ) - want_mac.update(nonce.encode("utf8")) - want_mac.update(b"\x00") - want_mac.update(username) - want_mac.update(b"\x00") - want_mac.update(password) - want_mac.update(b"\x00") - want_mac.update(b"admin" if admin else b"notadmin") + want_mac_builder.update(nonce.encode("utf8")) + want_mac_builder.update(b"\x00") + want_mac_builder.update(username) + want_mac_builder.update(b"\x00") + want_mac_builder.update(password) + want_mac_builder.update(b"\x00") + want_mac_builder.update(b"admin" if admin else b"notadmin") if user_type: - want_mac.update(b"\x00") - want_mac.update(user_type.encode("utf8")) - want_mac = want_mac.hexdigest() + want_mac_builder.update(b"\x00") + want_mac_builder.update(user_type.encode("utf8")) + + want_mac = want_mac_builder.hexdigest() if not hmac.compare_digest(want_mac.encode("ascii"), got_mac.encode("ascii")): raise SynapseError(403, "HMAC incorrect") diff --git a/synapse/rest/client/v1/login.py b/synapse/rest/client/v1/login.py index ff9c978fe7..1294e080dc 100644 --- a/synapse/rest/client/v1/login.py +++ b/synapse/rest/client/v1/login.py @@ -514,7 +514,7 @@ class CasTicketServlet(RestServlet): if user is None: raise Exception("CAS response does not contain user") except Exception: - logger.error("Error parsing CAS response", exc_info=1) + logger.exception("Error parsing CAS response") raise LoginError(401, "Invalid CAS response", errcode=Codes.UNAUTHORIZED) if not success: raise LoginError( diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index 711d4ad304..5aef8238b8 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -16,6 +16,7 @@ """ This module contains REST servlets to do with rooms: /rooms/ """ import logging +from typing import List, Optional from six.moves.urllib import parse as urlparse @@ -207,7 +208,7 @@ class RoomStateEventRestServlet(TransactionRestServlet): requester, event_dict, txn_id=txn_id ) - ret = {} + ret = {} # type: dict if event: set_tag("event_id", event.event_id) ret = {"event_id": event.event_id} @@ -285,7 +286,7 @@ class JoinRoomAliasServlet(TransactionRestServlet): try: remote_room_hosts = [ x.decode("ascii") for x in request.args[b"server_name"] - ] + ] # type: Optional[List[str]] except Exception: remote_room_hosts = None elif RoomAlias.is_valid(room_identifier): @@ -375,7 +376,7 @@ class PublicRoomListRestServlet(TransactionRestServlet): server = parse_string(request, "server", default=None) content = parse_json_object_from_request(request) - limit = int(content.get("limit", 100)) + limit = int(content.get("limit", 100)) # type: Optional[int] since_token = content.get("since", None) search_filter = content.get("filter", None) @@ -504,11 +505,16 @@ class RoomMessageListRestServlet(RestServlet): filter_bytes = parse_string(request, b"filter", encoding=None) if filter_bytes: filter_json = urlparse.unquote(filter_bytes.decode("UTF-8")) - event_filter = Filter(json.loads(filter_json)) - if event_filter.filter_json.get("event_format", "client") == "federation": + event_filter = Filter(json.loads(filter_json)) # type: Optional[Filter] + if ( + event_filter + and event_filter.filter_json.get("event_format", "client") + == "federation" + ): as_client_event = False else: event_filter = None + msgs = await self.pagination_handler.get_messages( room_id=room_id, requester=requester, @@ -611,7 +617,7 @@ class RoomEventContextServlet(RestServlet): filter_bytes = parse_string(request, "filter") if filter_bytes: filter_json = urlparse.unquote(filter_bytes) - event_filter = Filter(json.loads(filter_json)) + event_filter = Filter(json.loads(filter_json)) # type: Optional[Filter] else: event_filter = None diff --git a/synapse/rest/client/v2_alpha/register.py b/synapse/rest/client/v2_alpha/register.py index 66de16a1fa..1bda9aec7e 100644 --- a/synapse/rest/client/v2_alpha/register.py +++ b/synapse/rest/client/v2_alpha/register.py @@ -21,6 +21,7 @@ from typing import List, Union from six import string_types import synapse +import synapse.api.auth import synapse.types from synapse.api.constants import LoginType from synapse.api.errors import ( @@ -405,7 +406,7 @@ class RegisterRestServlet(RestServlet): return ret elif kind != b"user": raise UnrecognizedRequestError( - "Do not understand membership kind: %s" % (kind,) + "Do not understand membership kind: %s" % (kind.decode("utf8"),) ) # we do basic sanity checks here because the auth layer will store these diff --git a/synapse/rest/client/v2_alpha/sendtodevice.py b/synapse/rest/client/v2_alpha/sendtodevice.py index 501b52fb6c..db829f3098 100644 --- a/synapse/rest/client/v2_alpha/sendtodevice.py +++ b/synapse/rest/client/v2_alpha/sendtodevice.py @@ -14,6 +14,7 @@ # limitations under the License. import logging +from typing import Tuple from synapse.http import servlet from synapse.http.servlet import parse_json_object_from_request @@ -60,7 +61,7 @@ class SendToDeviceRestServlet(servlet.RestServlet): sender_user_id, message_type, content["messages"] ) - response = (200, {}) + response = (200, {}) # type: Tuple[int, dict] return response diff --git a/synapse/rest/key/v2/remote_key_resource.py b/synapse/rest/key/v2/remote_key_resource.py index e7fc3f0431..9d6813a047 100644 --- a/synapse/rest/key/v2/remote_key_resource.py +++ b/synapse/rest/key/v2/remote_key_resource.py @@ -13,6 +13,7 @@ # limitations under the License. import logging +from typing import Dict, Set from canonicaljson import encode_canonical_json, json from signedjson.sign import sign_json @@ -103,7 +104,7 @@ class RemoteKey(DirectServeResource): async def _async_render_GET(self, request): if len(request.postpath) == 1: (server,) = request.postpath - query = {server.decode("ascii"): {}} + query = {server.decode("ascii"): {}} # type: dict elif len(request.postpath) == 2: server, key_id = request.postpath minimum_valid_until_ts = parse_integer(request, "minimum_valid_until_ts") @@ -148,7 +149,7 @@ class RemoteKey(DirectServeResource): time_now_ms = self.clock.time_msec() - cache_misses = dict() + cache_misses = dict() # type: Dict[str, Set[str]] for (server_name, key_id, from_server), results in cached.items(): results = [(result["ts_added_ms"], result) for result in results] diff --git a/synapse/rest/media/v1/media_repository.py b/synapse/rest/media/v1/media_repository.py index bd9186fe50..490b1b45a8 100644 --- a/synapse/rest/media/v1/media_repository.py +++ b/synapse/rest/media/v1/media_repository.py @@ -18,6 +18,7 @@ import errno import logging import os import shutil +from typing import Dict, Tuple from six import iteritems @@ -605,7 +606,7 @@ class MediaRepository(object): # We deduplicate the thumbnail sizes by ignoring the cropped versions if # they have the same dimensions of a scaled one. - thumbnails = {} + thumbnails = {} # type: Dict[Tuple[int, int, str], str] for r_width, r_height, r_method, r_type in requirements: if r_method == "crop": thumbnails.setdefault((r_width, r_height, r_type), r_method) diff --git a/synapse/rest/media/v1/preview_url_resource.py b/synapse/rest/media/v1/preview_url_resource.py index 6b978be876..07e395cfd1 100644 --- a/synapse/rest/media/v1/preview_url_resource.py +++ b/synapse/rest/media/v1/preview_url_resource.py @@ -23,6 +23,7 @@ import re import shutil import sys import traceback +from typing import Dict, Optional import six from six import string_types @@ -237,8 +238,8 @@ class PreviewUrlResource(DirectServeResource): # If we don't find a match, we'll look at the HTTP Content-Type, and # if that doesn't exist, we'll fall back to UTF-8. if not encoding: - match = _content_type_match.match(media_info["media_type"]) - encoding = match.group(1) if match else "utf-8" + content_match = _content_type_match.match(media_info["media_type"]) + encoding = content_match.group(1) if content_match else "utf-8" og = decode_and_calc_og(body, media_info["uri"], encoding) @@ -518,7 +519,7 @@ def _calc_og(tree, media_uri): # "og:video:height" : "720", # "og:video:secure_url": "https://www.youtube.com/v/LXDBoHyjmtw?version=3", - og = {} + og = {} # type: Dict[str, Optional[str]] for tag in tree.xpath("//*/meta[starts-with(@property, 'og:')]"): if "content" in tag.attrib: # if we've got more than 50 tags, someone is taking the piss diff --git a/synapse/rest/media/v1/thumbnail_resource.py b/synapse/rest/media/v1/thumbnail_resource.py index 931ce79be8..eee93b4313 100644 --- a/synapse/rest/media/v1/thumbnail_resource.py +++ b/synapse/rest/media/v1/thumbnail_resource.py @@ -296,8 +296,8 @@ class ThumbnailResource(DirectServeResource): d_h = desired_height if desired_method.lower() == "crop": - info_list = [] - info_list2 = [] + crop_info_list = [] + crop_info_list2 = [] for info in thumbnail_infos: t_w = info["thumbnail_width"] t_h = info["thumbnail_height"] @@ -309,7 +309,7 @@ class ThumbnailResource(DirectServeResource): type_quality = desired_type != info["thumbnail_type"] length_quality = info["thumbnail_length"] if t_w >= d_w or t_h >= d_h: - info_list.append( + crop_info_list.append( ( aspect_quality, min_quality, @@ -320,7 +320,7 @@ class ThumbnailResource(DirectServeResource): ) ) else: - info_list2.append( + crop_info_list2.append( ( aspect_quality, min_quality, @@ -330,10 +330,10 @@ class ThumbnailResource(DirectServeResource): info, ) ) - if info_list: - return min(info_list)[-1] + if crop_info_list: + return min(crop_info_list2)[-1] else: - return min(info_list2)[-1] + return min(crop_info_list2)[-1] else: info_list = [] info_list2 = [] diff --git a/tox.ini b/tox.ini index edf4654177..1d946a02ba 100644 --- a/tox.ini +++ b/tox.ini @@ -183,8 +183,7 @@ commands = mypy \ synapse/logging/ \ synapse/module_api \ synapse/replication \ - synapse/rest/consent \ - synapse/rest/saml2 \ + synapse/rest \ synapse/spam_checker_api \ synapse/storage/engines \ synapse/streams From 0e68760078c0aac57bfaeb681d534231e191315a Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 20 Jan 2020 18:07:20 +0000 Subject: [PATCH 0858/1623] Add a DeltaState to track changes to be made to current state (#6716) --- changelog.d/6716.misc | 1 + synapse/storage/data_stores/main/events.py | 87 +++++++-------- synapse/storage/persist_events.py | 123 ++++++++++++--------- 3 files changed, 112 insertions(+), 99 deletions(-) create mode 100644 changelog.d/6716.misc diff --git a/changelog.d/6716.misc b/changelog.d/6716.misc new file mode 100644 index 0000000000..319aaa4acb --- /dev/null +++ b/changelog.d/6716.misc @@ -0,0 +1 @@ +Add a `DeltaState` to track changes to be made to current state during event persistence. diff --git a/synapse/storage/data_stores/main/events.py b/synapse/storage/data_stores/main/events.py index bb69c20448..596daf8909 100644 --- a/synapse/storage/data_stores/main/events.py +++ b/synapse/storage/data_stores/main/events.py @@ -19,6 +19,7 @@ import itertools import logging from collections import Counter as c_counter, OrderedDict, namedtuple from functools import wraps +from typing import Dict, List, Tuple from six import iteritems, text_type from six.moves import range @@ -41,8 +42,9 @@ from synapse.storage._base import make_in_list_sql_clause from synapse.storage.data_stores.main.event_federation import EventFederationStore from synapse.storage.data_stores.main.events_worker import EventsWorkerStore from synapse.storage.data_stores.main.state import StateGroupWorkerStore -from synapse.storage.database import Database -from synapse.types import RoomStreamToken, get_domain_from_id +from synapse.storage.database import Database, LoggingTransaction +from synapse.storage.persist_events import DeltaState +from synapse.types import RoomStreamToken, StateMap, get_domain_from_id from synapse.util.caches.descriptors import cached, cachedInlineCallbacks from synapse.util.frozenutils import frozendict_json_encoder from synapse.util.iterutils import batch_iter @@ -148,30 +150,26 @@ class EventsStore( @defer.inlineCallbacks def _persist_events_and_state_updates( self, - events_and_contexts, - current_state_for_room, - state_delta_for_room, - new_forward_extremeties, - backfilled=False, - delete_existing=False, + events_and_contexts: List[Tuple[EventBase, EventContext]], + current_state_for_room: Dict[str, StateMap[str]], + state_delta_for_room: Dict[str, DeltaState], + new_forward_extremeties: Dict[str, List[str]], + backfilled: bool = False, + delete_existing: bool = False, ): """Persist a set of events alongside updates to the current state and forward extremities tables. Args: - events_and_contexts (list[(EventBase, EventContext)]): - current_state_for_room (dict[str, dict]): Map from room_id to the - current state of the room based on forward extremities - state_delta_for_room (dict[str, tuple]): Map from room_id to tuple - of `(to_delete, to_insert)` where to_delete is a list - of type/state keys to remove from current state, and to_insert - is a map (type,key)->event_id giving the state delta in each - room. - new_forward_extremities (dict[str, list[str]]): Map from room_id - to list of event IDs that are the new forward extremities of - the room. - backfilled (bool) - delete_existing (bool): + events_and_contexts: + current_state_for_room: Map from room_id to the current state of + the room based on forward extremities + state_delta_for_room: Map from room_id to the delta to apply to + room state + new_forward_extremities: Map from room_id to list of event IDs + that are the new forward extremities of the room. + backfilled + delete_existing Returns: Deferred: resolves when the events have been persisted @@ -352,12 +350,12 @@ class EventsStore( @log_function def _persist_events_txn( self, - txn, - events_and_contexts, - backfilled, - delete_existing=False, - state_delta_for_room={}, - new_forward_extremeties={}, + txn: LoggingTransaction, + events_and_contexts: List[Tuple[EventBase, EventContext]], + backfilled: bool, + delete_existing: bool = False, + state_delta_for_room: Dict[str, DeltaState] = {}, + new_forward_extremeties: Dict[str, List[str]] = {}, ): """Insert some number of room events into the necessary database tables. @@ -366,21 +364,16 @@ class EventsStore( whether the event was rejected. Args: - txn (twisted.enterprise.adbapi.Connection): db connection - events_and_contexts (list[(EventBase, EventContext)]): - events to persist - backfilled (bool): True if the events were backfilled - delete_existing (bool): True to purge existing table rows for the - events from the database. This is useful when retrying due to + txn + events_and_contexts: events to persist + backfilled: True if the events were backfilled + delete_existing True to purge existing table rows for the events + from the database. This is useful when retrying due to IntegrityError. - state_delta_for_room (dict[str, (list, dict)]): - The current-state delta for each room. For each room, a tuple - (to_delete, to_insert), being a list of type/state keys to be - removed from the current state, and a state set to be added to - the current state. - new_forward_extremeties (dict[str, list[str]]): - The new forward extremities for each room. For each room, a - list of the event ids which are the forward extremities. + state_delta_for_room: The current-state delta for each room. + new_forward_extremetie: The new forward extremities for each room. + For each room, a list of the event ids which are the forward + extremities. """ all_events_and_contexts = events_and_contexts @@ -465,9 +458,15 @@ class EventsStore( # room_memberships, where applicable. self._update_current_state_txn(txn, state_delta_for_room, min_stream_order) - def _update_current_state_txn(self, txn, state_delta_by_room, stream_id): - for room_id, current_state_tuple in iteritems(state_delta_by_room): - to_delete, to_insert = current_state_tuple + def _update_current_state_txn( + self, + txn: LoggingTransaction, + state_delta_by_room: Dict[str, DeltaState], + stream_id: int, + ): + for room_id, delta_state in iteritems(state_delta_by_room): + to_delete = delta_state.to_delete + to_insert = delta_state.to_insert # First we add entries to the current_state_delta_stream. We # do this before updating the current_state_events table so diff --git a/synapse/storage/persist_events.py b/synapse/storage/persist_events.py index 1ed44925fc..368c457321 100644 --- a/synapse/storage/persist_events.py +++ b/synapse/storage/persist_events.py @@ -17,19 +17,24 @@ import logging from collections import deque, namedtuple +from typing import Iterable, List, Optional, Tuple from six import iteritems from six.moves import range +import attr from prometheus_client import Counter, Histogram from twisted.internet import defer from synapse.api.constants import EventTypes +from synapse.events import FrozenEvent +from synapse.events.snapshot import EventContext from synapse.logging.context import PreserveLoggingContext, make_deferred_yieldable from synapse.metrics.background_process_metrics import run_as_background_process from synapse.state import StateResolutionStore from synapse.storage.data_stores import DataStores +from synapse.types import StateMap from synapse.util.async_helpers import ObservableDeferred from synapse.util.metrics import Measure @@ -67,6 +72,19 @@ stale_forward_extremities_counter = Histogram( ) +@attr.s(slots=True, frozen=True) +class DeltaState: + """Deltas to use to update the `current_state_events` table. + + Attributes: + to_delete: List of type/state_keys to delete from current state + to_insert: Map of state to upsert into current state + """ + + to_delete = attr.ib(type=List[Tuple[str, str]]) + to_insert = attr.ib(type=StateMap[str]) + + class _EventPeristenceQueue(object): """Queues up events so that they can be persisted in bulk with only one concurrent transaction per room. @@ -138,13 +156,12 @@ class _EventPeristenceQueue(object): self._currently_persisting_rooms.add(room_id) - @defer.inlineCallbacks - def handle_queue_loop(): + async def handle_queue_loop(): try: queue = self._get_drainining_queue(room_id) for item in queue: try: - ret = yield per_item_callback(item) + ret = await per_item_callback(item) except Exception: with PreserveLoggingContext(): item.deferred.errback() @@ -191,12 +208,16 @@ class EventsPersistenceStorage(object): self._state_resolution_handler = hs.get_state_resolution_handler() @defer.inlineCallbacks - def persist_events(self, events_and_contexts, backfilled=False): + def persist_events( + self, + events_and_contexts: List[Tuple[FrozenEvent, EventContext]], + backfilled: bool = False, + ): """ Write events to the database Args: events_and_contexts: list of tuples of (event, context) - backfilled (bool): Whether the results are retrieved from federation + backfilled: Whether the results are retrieved from federation via backfill or not. Used to determine if they're "new" events which might update the current state etc. @@ -226,16 +247,12 @@ class EventsPersistenceStorage(object): return max_persisted_id @defer.inlineCallbacks - def persist_event(self, event, context, backfilled=False): + def persist_event( + self, event: FrozenEvent, context: EventContext, backfilled: bool = False + ): """ - - Args: - event (EventBase): - context (EventContext): - backfilled (bool): - Returns: - Deferred: resolves to (int, int): the stream ordering of ``event``, + Deferred[Tuple[int, int]]: the stream ordering of ``event``, and the stream ordering of the latest persisted event """ deferred = self._event_persist_queue.add_to_queue( @@ -249,28 +266,22 @@ class EventsPersistenceStorage(object): max_persisted_id = yield self.main_store.get_current_events_token() return (event.internal_metadata.stream_ordering, max_persisted_id) - def _maybe_start_persisting(self, room_id): - @defer.inlineCallbacks - def persisting_queue(item): + def _maybe_start_persisting(self, room_id: str): + async def persisting_queue(item): with Measure(self._clock, "persist_events"): - yield self._persist_events( + await self._persist_events( item.events_and_contexts, backfilled=item.backfilled ) self._event_persist_queue.handle_queue(room_id, persisting_queue) - @defer.inlineCallbacks - def _persist_events(self, events_and_contexts, backfilled=False): + async def _persist_events( + self, + events_and_contexts: List[Tuple[FrozenEvent, EventContext]], + backfilled: bool = False, + ): """Calculates the change to current state and forward extremities, and persists the given events and with those updates. - - Args: - events_and_contexts (list[(EventBase, EventContext)]): - backfilled (bool): - delete_existing (bool): - - Returns: - Deferred: resolves when the events have been persisted """ if not events_and_contexts: return @@ -315,10 +326,10 @@ class EventsPersistenceStorage(object): ) for room_id, ev_ctx_rm in iteritems(events_by_room): - latest_event_ids = yield self.main_store.get_latest_event_ids_in_room( + latest_event_ids = await self.main_store.get_latest_event_ids_in_room( room_id ) - new_latest_event_ids = yield self._calculate_new_extremities( + new_latest_event_ids = await self._calculate_new_extremities( room_id, ev_ctx_rm, latest_event_ids ) @@ -374,7 +385,7 @@ class EventsPersistenceStorage(object): with Measure( self._clock, "persist_events.get_new_state_after_events" ): - res = yield self._get_new_state_after_events( + res = await self._get_new_state_after_events( room_id, ev_ctx_rm, latest_event_ids, @@ -389,12 +400,12 @@ class EventsPersistenceStorage(object): # If there is a delta we know that we've # only added or replaced state, never # removed keys entirely. - state_delta_for_room[room_id] = ([], delta_ids) + state_delta_for_room[room_id] = DeltaState([], delta_ids) elif current_state is not None: with Measure( self._clock, "persist_events.calculate_state_delta" ): - delta = yield self._calculate_state_delta( + delta = await self._calculate_state_delta( room_id, current_state ) state_delta_for_room[room_id] = delta @@ -404,7 +415,7 @@ class EventsPersistenceStorage(object): if current_state is not None: current_state_for_room[room_id] = current_state - yield self.main_store._persist_events_and_state_updates( + await self.main_store._persist_events_and_state_updates( chunk, current_state_for_room=current_state_for_room, state_delta_for_room=state_delta_for_room, @@ -412,8 +423,12 @@ class EventsPersistenceStorage(object): backfilled=backfilled, ) - @defer.inlineCallbacks - def _calculate_new_extremities(self, room_id, event_contexts, latest_event_ids): + async def _calculate_new_extremities( + self, + room_id: str, + event_contexts: List[Tuple[FrozenEvent, EventContext]], + latest_event_ids: List[str], + ): """Calculates the new forward extremities for a room given events to persist. @@ -444,13 +459,13 @@ class EventsPersistenceStorage(object): ) # Remove any events which are prev_events of any existing events. - existing_prevs = yield self.main_store._get_events_which_are_prevs(result) + existing_prevs = await self.main_store._get_events_which_are_prevs(result) result.difference_update(existing_prevs) # Finally handle the case where the new events have soft-failed prev # events. If they do we need to remove them and their prev events, # otherwise we end up with dangling extremities. - existing_prevs = yield self.main_store._get_prevs_before_rejected( + existing_prevs = await self.main_store._get_prevs_before_rejected( e_id for event in new_events for e_id in event.prev_event_ids() ) result.difference_update(existing_prevs) @@ -464,10 +479,13 @@ class EventsPersistenceStorage(object): return result - @defer.inlineCallbacks - def _get_new_state_after_events( - self, room_id, events_context, old_latest_event_ids, new_latest_event_ids - ): + async def _get_new_state_after_events( + self, + room_id: str, + events_context: List[Tuple[FrozenEvent, EventContext]], + old_latest_event_ids: Iterable[str], + new_latest_event_ids: Iterable[str], + ) -> Tuple[Optional[StateMap[str]], Optional[StateMap[str]]]: """Calculate the current state dict after adding some new events to a room @@ -485,7 +503,6 @@ class EventsPersistenceStorage(object): the new forward extremities for the room. Returns: - Deferred[tuple[dict[(str,str), str]|None, dict[(str,str), str]|None]]: Returns a tuple of two state maps, the first being the full new current state and the second being the delta to the existing current state. If both are None then there has been no change. @@ -547,7 +564,7 @@ class EventsPersistenceStorage(object): if missing_event_ids: # Now pull out the state groups for any missing events from DB - event_to_groups = yield self.main_store._get_state_group_for_events( + event_to_groups = await self.main_store._get_state_group_for_events( missing_event_ids ) event_id_to_state_group.update(event_to_groups) @@ -588,7 +605,7 @@ class EventsPersistenceStorage(object): # their state IDs so we can resolve to a single state set. missing_state = new_state_groups - set(state_groups_map) if missing_state: - group_to_state = yield self.state_store._get_state_for_groups(missing_state) + group_to_state = await self.state_store._get_state_for_groups(missing_state) state_groups_map.update(group_to_state) if len(new_state_groups) == 1: @@ -612,10 +629,10 @@ class EventsPersistenceStorage(object): break if not room_version: - room_version = yield self.main_store.get_room_version(room_id) + room_version = await self.main_store.get_room_version(room_id) logger.debug("calling resolve_state_groups from preserve_events") - res = yield self._state_resolution_handler.resolve_state_groups( + res = await self._state_resolution_handler.resolve_state_groups( room_id, room_version, state_groups, @@ -625,18 +642,14 @@ class EventsPersistenceStorage(object): return res.state, None - @defer.inlineCallbacks - def _calculate_state_delta(self, room_id, current_state): + async def _calculate_state_delta( + self, room_id: str, current_state: StateMap[str] + ) -> DeltaState: """Calculate the new state deltas for a room. Assumes that we are only persisting events for one room at a time. - - Returns: - tuple[list, dict] (to_delete, to_insert): where to_delete are the - type/state_keys to remove from current_state_events and `to_insert` - are the updates to current_state_events. """ - existing_state = yield self.main_store.get_current_state_ids(room_id) + existing_state = await self.main_store.get_current_state_ids(room_id) to_delete = [key for key in existing_state if key not in current_state] @@ -646,4 +659,4 @@ class EventsPersistenceStorage(object): if ev_id != existing_state.get(key) } - return to_delete, to_insert + return DeltaState(to_delete=to_delete, to_insert=to_insert) From 07124d028df6b33336dcc2ef807fd7866f42902a Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 21 Jan 2020 19:04:58 +0000 Subject: [PATCH 0859/1623] Port synapse_port_db to async/await (#6718) * Raise an exception if there are pending background updates So we return with a non-0 code * Changelog * Port synapse_port_db to async/await * Port update_database to async/await * Add version string to mocked homeservers * Remove unused imports * Convert overseen bits to async/await * Fixup logging contexts * Fix imports * Add a way to print an error without raising an exception * Incorporate review --- changelog.d/6718.bugfix | 1 + scripts-dev/update_database | 20 ++-- scripts/synapse_port_db | 194 +++++++++++++++++++++--------------- 3 files changed, 126 insertions(+), 89 deletions(-) create mode 100644 changelog.d/6718.bugfix diff --git a/changelog.d/6718.bugfix b/changelog.d/6718.bugfix new file mode 100644 index 0000000000..23b23e3ed8 --- /dev/null +++ b/changelog.d/6718.bugfix @@ -0,0 +1 @@ +Fix a bug causing the `synapse_port_db` script to return 0 in a specific error case. diff --git a/scripts-dev/update_database b/scripts-dev/update_database index 1d62f0403a..94aa8758b4 100755 --- a/scripts-dev/update_database +++ b/scripts-dev/update_database @@ -22,10 +22,12 @@ import yaml from twisted.internet import defer, reactor +import synapse from synapse.config.homeserver import HomeServerConfig from synapse.metrics.background_process_metrics import run_as_background_process from synapse.server import HomeServer from synapse.storage import DataStore +from synapse.util.versionstring import get_version_string logger = logging.getLogger("update_database") @@ -38,6 +40,8 @@ class MockHomeserver(HomeServer): config.server_name, reactor=reactor, config=config, **kwargs ) + self.version_string = "Synapse/"+get_version_string(synapse) + if __name__ == "__main__": parser = argparse.ArgumentParser( @@ -81,15 +85,17 @@ if __name__ == "__main__": hs.setup() store = hs.get_datastore() - @defer.inlineCallbacks - def run_background_updates(): - yield store.db.updates.run_background_updates(sleep=False) + async def run_background_updates(): + await store.db.updates.run_background_updates(sleep=False) # Stop the reactor to exit the script once every background update is run. reactor.stop() - # Apply all background updates on the database. - reactor.callWhenRunning( - lambda: run_as_background_process("background_updates", run_background_updates) - ) + def run(): + # Apply all background updates on the database. + defer.ensureDeferred( + run_as_background_process("background_updates", run_background_updates) + ) + + reactor.callWhenRunning(run) reactor.run() diff --git a/scripts/synapse_port_db b/scripts/synapse_port_db index 5e69104b97..e8b698f3ff 100755 --- a/scripts/synapse_port_db +++ b/scripts/synapse_port_db @@ -27,13 +27,16 @@ from six import string_types import yaml -from twisted.enterprise import adbapi from twisted.internet import defer, reactor +import synapse from synapse.config.database import DatabaseConnectionConfig from synapse.config.homeserver import HomeServerConfig -from synapse.logging.context import PreserveLoggingContext -from synapse.storage._base import LoggingTransaction +from synapse.logging.context import ( + LoggingContext, + make_deferred_yieldable, + run_in_background, +) from synapse.storage.data_stores.main.client_ips import ClientIpBackgroundUpdateStore from synapse.storage.data_stores.main.deviceinbox import ( DeviceInboxBackgroundUpdateStore, @@ -61,6 +64,7 @@ from synapse.storage.database import Database, make_conn from synapse.storage.engines import create_engine from synapse.storage.prepare_database import prepare_database from synapse.util import Clock +from synapse.util.versionstring import get_version_string logger = logging.getLogger("synapse_port_db") @@ -125,6 +129,13 @@ APPEND_ONLY_TABLES = [ ] +# Error returned by the run function. Used at the top-level part of the script to +# handle errors and return codes. +end_error = None +# The exec_info for the error, if any. If error is defined but not exec_info the script +# will show only the error message without the stacktrace, if exec_info is defined but +# not the error then the script will show nothing outside of what's printed in the run +# function. If both are defined, the script will print both the error and the stacktrace. end_error_exec_info = None @@ -177,6 +188,7 @@ class MockHomeserver: self.clock = Clock(reactor) self.config = config self.hostname = config.server_name + self.version_string = "Synapse/"+get_version_string(synapse) def get_clock(self): return self.clock @@ -189,11 +201,10 @@ class Porter(object): def __init__(self, **kwargs): self.__dict__.update(kwargs) - @defer.inlineCallbacks - def setup_table(self, table): + async def setup_table(self, table): if table in APPEND_ONLY_TABLES: # It's safe to just carry on inserting. - row = yield self.postgres_store.db.simple_select_one( + row = await self.postgres_store.db.simple_select_one( table="port_from_sqlite3", keyvalues={"table_name": table}, retcols=("forward_rowid", "backward_rowid"), @@ -207,10 +218,10 @@ class Porter(object): forward_chunk, already_ported, total_to_port, - ) = yield self._setup_sent_transactions() + ) = await self._setup_sent_transactions() backward_chunk = 0 else: - yield self.postgres_store.db.simple_insert( + await self.postgres_store.db.simple_insert( table="port_from_sqlite3", values={ "table_name": table, @@ -227,7 +238,7 @@ class Porter(object): backward_chunk = row["backward_rowid"] if total_to_port is None: - already_ported, total_to_port = yield self._get_total_count_to_port( + already_ported, total_to_port = await self._get_total_count_to_port( table, forward_chunk, backward_chunk ) else: @@ -238,9 +249,9 @@ class Porter(object): ) txn.execute("TRUNCATE %s CASCADE" % (table,)) - yield self.postgres_store.execute(delete_all) + await self.postgres_store.execute(delete_all) - yield self.postgres_store.db.simple_insert( + await self.postgres_store.db.simple_insert( table="port_from_sqlite3", values={"table_name": table, "forward_rowid": 1, "backward_rowid": 0}, ) @@ -248,16 +259,13 @@ class Porter(object): forward_chunk = 1 backward_chunk = 0 - already_ported, total_to_port = yield self._get_total_count_to_port( + already_ported, total_to_port = await self._get_total_count_to_port( table, forward_chunk, backward_chunk ) - defer.returnValue( - (table, already_ported, total_to_port, forward_chunk, backward_chunk) - ) + return table, already_ported, total_to_port, forward_chunk, backward_chunk - @defer.inlineCallbacks - def handle_table( + async def handle_table( self, table, postgres_size, table_size, forward_chunk, backward_chunk ): logger.info( @@ -275,7 +283,7 @@ class Porter(object): self.progress.add_table(table, postgres_size, table_size) if table == "event_search": - yield self.handle_search_table( + await self.handle_search_table( postgres_size, table_size, forward_chunk, backward_chunk ) return @@ -294,7 +302,7 @@ class Porter(object): if table == "user_directory_stream_pos": # We need to make sure there is a single row, `(X, null), as that is # what synapse expects to be there. - yield self.postgres_store.db.simple_insert( + await self.postgres_store.db.simple_insert( table=table, values={"stream_id": None} ) self.progress.update(table, table_size) # Mark table as done @@ -335,7 +343,7 @@ class Porter(object): return headers, forward_rows, backward_rows - headers, frows, brows = yield self.sqlite_store.db.runInteraction( + headers, frows, brows = await self.sqlite_store.db.runInteraction( "select", r ) @@ -361,7 +369,7 @@ class Porter(object): }, ) - yield self.postgres_store.execute(insert) + await self.postgres_store.execute(insert) postgres_size += len(rows) @@ -369,8 +377,7 @@ class Porter(object): else: return - @defer.inlineCallbacks - def handle_search_table( + async def handle_search_table( self, postgres_size, table_size, forward_chunk, backward_chunk ): select = ( @@ -390,7 +397,7 @@ class Porter(object): return headers, rows - headers, rows = yield self.sqlite_store.db.runInteraction("select", r) + headers, rows = await self.sqlite_store.db.runInteraction("select", r) if rows: forward_chunk = rows[-1][0] + 1 @@ -438,7 +445,7 @@ class Porter(object): }, ) - yield self.postgres_store.execute(insert) + await self.postgres_store.execute(insert) postgres_size += len(rows) @@ -476,11 +483,10 @@ class Porter(object): return store - @defer.inlineCallbacks - def run_background_updates_on_postgres(self): + async def run_background_updates_on_postgres(self): # Manually apply all background updates on the PostgreSQL database. postgres_ready = ( - yield self.postgres_store.db.updates.has_completed_background_updates() + await self.postgres_store.db.updates.has_completed_background_updates() ) if not postgres_ready: @@ -489,13 +495,20 @@ class Porter(object): self.progress.set_state("Running background updates on PostgreSQL") while not postgres_ready: - yield self.postgres_store.db.updates.do_next_background_update(100) - postgres_ready = yield ( + await self.postgres_store.db.updates.do_next_background_update(100) + postgres_ready = await ( self.postgres_store.db.updates.has_completed_background_updates() ) - @defer.inlineCallbacks - def run(self): + async def run(self): + """Ports the SQLite database to a PostgreSQL database. + + When a fatal error is met, its message is assigned to the global "end_error" + variable. When this error comes with a stacktrace, its exec_info is assigned to + the global "end_error_exec_info" variable. + """ + global end_error + try: # we allow people to port away from outdated versions of sqlite. self.sqlite_store = self.build_db_store( @@ -505,21 +518,21 @@ class Porter(object): # Check if all background updates are done, abort if not. updates_complete = ( - yield self.sqlite_store.db.updates.has_completed_background_updates() + await self.sqlite_store.db.updates.has_completed_background_updates() ) if not updates_complete: - sys.stderr.write( + end_error = ( "Pending background updates exist in the SQLite3 database." " Please start Synapse again and wait until every update has finished" " before running this script.\n" ) - defer.returnValue(None) + return self.postgres_store = self.build_db_store( self.hs_config.get_single_database() ) - yield self.run_background_updates_on_postgres() + await self.run_background_updates_on_postgres() self.progress.set_state("Creating port tables") @@ -547,22 +560,22 @@ class Porter(object): ) try: - yield self.postgres_store.db.runInteraction("alter_table", alter_table) + await self.postgres_store.db.runInteraction("alter_table", alter_table) except Exception: # On Error Resume Next pass - yield self.postgres_store.db.runInteraction( + await self.postgres_store.db.runInteraction( "create_port_table", create_port_table ) # Step 2. Get tables. self.progress.set_state("Fetching tables") - sqlite_tables = yield self.sqlite_store.db.simple_select_onecol( + sqlite_tables = await self.sqlite_store.db.simple_select_onecol( table="sqlite_master", keyvalues={"type": "table"}, retcol="name" ) - postgres_tables = yield self.postgres_store.db.simple_select_onecol( + postgres_tables = await self.postgres_store.db.simple_select_onecol( table="information_schema.tables", keyvalues={}, retcol="distinct table_name", @@ -573,28 +586,34 @@ class Porter(object): # Step 3. Figure out what still needs copying self.progress.set_state("Checking on port progress") - setup_res = yield defer.gatherResults( - [ - self.setup_table(table) - for table in tables - if table not in ["schema_version", "applied_schema_deltas"] - and not table.startswith("sqlite_") - ], - consumeErrors=True, + setup_res = await make_deferred_yieldable( + defer.gatherResults( + [ + run_in_background(self.setup_table, table) + for table in tables + if table not in ["schema_version", "applied_schema_deltas"] + and not table.startswith("sqlite_") + ], + consumeErrors=True, + ) ) # Step 4. Do the copying. self.progress.set_state("Copying to postgres") - yield defer.gatherResults( - [self.handle_table(*res) for res in setup_res], consumeErrors=True + await make_deferred_yieldable( + defer.gatherResults( + [run_in_background(self.handle_table, *res) for res in setup_res], + consumeErrors=True, + ) ) # Step 5. Do final post-processing - yield self._setup_state_group_id_seq() + await self._setup_state_group_id_seq() self.progress.done() - except Exception: + except Exception as e: global end_error_exec_info + end_error = e end_error_exec_info = sys.exc_info() logger.exception("") finally: @@ -634,8 +653,7 @@ class Porter(object): return outrows - @defer.inlineCallbacks - def _setup_sent_transactions(self): + async def _setup_sent_transactions(self): # Only save things from the last day yesterday = int(time.time() * 1000) - 86400000 @@ -656,7 +674,7 @@ class Porter(object): return headers, [r for r in rows if r[ts_ind] < yesterday] - headers, rows = yield self.sqlite_store.db.runInteraction("select", r) + headers, rows = await self.sqlite_store.db.runInteraction("select", r) rows = self._convert_rows("sent_transactions", headers, rows) @@ -669,7 +687,7 @@ class Porter(object): txn, "sent_transactions", headers[1:], rows ) - yield self.postgres_store.execute(insert) + await self.postgres_store.execute(insert) else: max_inserted_rowid = 0 @@ -686,10 +704,10 @@ class Porter(object): else: return 1 - next_chunk = yield self.sqlite_store.execute(get_start_id) + next_chunk = await self.sqlite_store.execute(get_start_id) next_chunk = max(max_inserted_rowid + 1, next_chunk) - yield self.postgres_store.db.simple_insert( + await self.postgres_store.db.simple_insert( table="port_from_sqlite3", values={ "table_name": "sent_transactions", @@ -705,46 +723,49 @@ class Porter(object): (size,) = txn.fetchone() return int(size) - remaining_count = yield self.sqlite_store.execute(get_sent_table_size) + remaining_count = await self.sqlite_store.execute(get_sent_table_size) total_count = remaining_count + inserted_rows - defer.returnValue((next_chunk, inserted_rows, total_count)) + return next_chunk, inserted_rows, total_count - @defer.inlineCallbacks - def _get_remaining_count_to_port(self, table, forward_chunk, backward_chunk): - frows = yield self.sqlite_store.execute_sql( + async def _get_remaining_count_to_port(self, table, forward_chunk, backward_chunk): + frows = await self.sqlite_store.execute_sql( "SELECT count(*) FROM %s WHERE rowid >= ?" % (table,), forward_chunk ) - brows = yield self.sqlite_store.execute_sql( + brows = await self.sqlite_store.execute_sql( "SELECT count(*) FROM %s WHERE rowid <= ?" % (table,), backward_chunk ) - defer.returnValue(frows[0][0] + brows[0][0]) + return frows[0][0] + brows[0][0] - @defer.inlineCallbacks - def _get_already_ported_count(self, table): - rows = yield self.postgres_store.execute_sql( + async def _get_already_ported_count(self, table): + rows = await self.postgres_store.execute_sql( "SELECT count(*) FROM %s" % (table,) ) - defer.returnValue(rows[0][0]) + return rows[0][0] - @defer.inlineCallbacks - def _get_total_count_to_port(self, table, forward_chunk, backward_chunk): - remaining, done = yield defer.gatherResults( - [ - self._get_remaining_count_to_port(table, forward_chunk, backward_chunk), - self._get_already_ported_count(table), - ], - consumeErrors=True, + async def _get_total_count_to_port(self, table, forward_chunk, backward_chunk): + remaining, done = await make_deferred_yieldable( + defer.gatherResults( + [ + run_in_background( + self._get_remaining_count_to_port, + table, + forward_chunk, + backward_chunk, + ), + run_in_background(self._get_already_ported_count, table), + ], + ) ) remaining = int(remaining) if remaining else 0 done = int(done) if done else 0 - defer.returnValue((done, remaining + done)) + return done, remaining + done def _setup_state_group_id_seq(self): def r(txn): @@ -1010,7 +1031,12 @@ if __name__ == "__main__": hs_config=config, ) - reactor.callWhenRunning(porter.run) + @defer.inlineCallbacks + def run(): + with LoggingContext("synapse_port_db_run"): + yield defer.ensureDeferred(porter.run()) + + reactor.callWhenRunning(run) reactor.run() @@ -1019,7 +1045,11 @@ if __name__ == "__main__": else: start() - if end_error_exec_info: - exc_type, exc_value, exc_traceback = end_error_exec_info - traceback.print_exception(exc_type, exc_value, exc_traceback) + if end_error: + if end_error_exec_info: + exc_type, exc_value, exc_traceback = end_error_exec_info + traceback.print_exception(exc_type, exc_value, exc_traceback) + + sys.stderr.write(end_error) + sys.exit(5) From 837f62266b845cce9797fbe989a7816d4f1fadff Mon Sep 17 00:00:00 2001 From: Ivan Vilata-i-Balaguer Date: Wed, 22 Jan 2020 02:32:52 -0500 Subject: [PATCH 0860/1623] Avoid attribute error when `password_config` present but empty (#6753) The old statement returned `None` for such a `password_config` (like the one created on first run), thus retrieval of the `pepper` key failed with `AttributeError`. Fixes #5315 Signed-off-by: Ivan Vilata i Balaguer --- changelog.d/6753.bugfix | 1 + scripts/hash_password | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/6753.bugfix diff --git a/changelog.d/6753.bugfix b/changelog.d/6753.bugfix new file mode 100644 index 0000000000..5dfde793e1 --- /dev/null +++ b/changelog.d/6753.bugfix @@ -0,0 +1 @@ +Fix `AttributeError: 'NoneType' object has no attribute 'get'` in `hash_password` when configuration has an empty `password_config`. Contributed by @ivilata. diff --git a/scripts/hash_password b/scripts/hash_password index a1eb0769da..a30767f758 100755 --- a/scripts/hash_password +++ b/scripts/hash_password @@ -52,7 +52,7 @@ if __name__ == "__main__": if "config" in args and args.config: config = yaml.safe_load(args.config) bcrypt_rounds = config.get("bcrypt_rounds", bcrypt_rounds) - password_config = config.get("password_config", {}) + password_config = config.get("password_config", None) or {} password_pepper = password_config.get("pepper", password_pepper) password = args.password From 2093f83ea045d8a3fc6daa0c793da9b17237dc1f Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 22 Jan 2020 10:36:48 +0000 Subject: [PATCH 0861/1623] Remove unused CI docker compose files (#6754) These now exist in the pipelines repo. --- .buildkite/docker-compose.py35.pg95.yaml | 22 ---------------------- .buildkite/docker-compose.py37.pg11.yaml | 22 ---------------------- .buildkite/docker-compose.py37.pg95.yaml | 22 ---------------------- changelog.d/6754.misc | 1 + 4 files changed, 1 insertion(+), 66 deletions(-) delete mode 100644 .buildkite/docker-compose.py35.pg95.yaml delete mode 100644 .buildkite/docker-compose.py37.pg11.yaml delete mode 100644 .buildkite/docker-compose.py37.pg95.yaml create mode 100644 changelog.d/6754.misc diff --git a/.buildkite/docker-compose.py35.pg95.yaml b/.buildkite/docker-compose.py35.pg95.yaml deleted file mode 100644 index 43237b7775..0000000000 --- a/.buildkite/docker-compose.py35.pg95.yaml +++ /dev/null @@ -1,22 +0,0 @@ -version: '3.1' - -services: - - postgres: - image: postgres:9.5 - environment: - POSTGRES_PASSWORD: postgres - command: -c fsync=off - - testenv: - image: python:3.5 - depends_on: - - postgres - env_file: .env - environment: - SYNAPSE_POSTGRES_HOST: postgres - SYNAPSE_POSTGRES_USER: postgres - SYNAPSE_POSTGRES_PASSWORD: postgres - working_dir: /src - volumes: - - ..:/src diff --git a/.buildkite/docker-compose.py37.pg11.yaml b/.buildkite/docker-compose.py37.pg11.yaml deleted file mode 100644 index b767228147..0000000000 --- a/.buildkite/docker-compose.py37.pg11.yaml +++ /dev/null @@ -1,22 +0,0 @@ -version: '3.1' - -services: - - postgres: - image: postgres:11 - environment: - POSTGRES_PASSWORD: postgres - command: -c fsync=off - - testenv: - image: python:3.7 - depends_on: - - postgres - env_file: .env - environment: - SYNAPSE_POSTGRES_HOST: postgres - SYNAPSE_POSTGRES_USER: postgres - SYNAPSE_POSTGRES_PASSWORD: postgres - working_dir: /src - volumes: - - ..:/src diff --git a/.buildkite/docker-compose.py37.pg95.yaml b/.buildkite/docker-compose.py37.pg95.yaml deleted file mode 100644 index 02fcd28304..0000000000 --- a/.buildkite/docker-compose.py37.pg95.yaml +++ /dev/null @@ -1,22 +0,0 @@ -version: '3.1' - -services: - - postgres: - image: postgres:9.5 - environment: - POSTGRES_PASSWORD: postgres - command: -c fsync=off - - testenv: - image: python:3.7 - depends_on: - - postgres - env_file: .env - environment: - SYNAPSE_POSTGRES_HOST: postgres - SYNAPSE_POSTGRES_USER: postgres - SYNAPSE_POSTGRES_PASSWORD: postgres - working_dir: /src - volumes: - - ..:/src diff --git a/changelog.d/6754.misc b/changelog.d/6754.misc new file mode 100644 index 0000000000..0a955e47e6 --- /dev/null +++ b/changelog.d/6754.misc @@ -0,0 +1 @@ +Remove unused CI docker compose files. From 5d7a6ad2238981646b2ae7b4071d8715281d181a Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 22 Jan 2020 10:37:00 +0000 Subject: [PATCH 0862/1623] Allow streaming cache invalidate all to workers. (#6749) --- changelog.d/6749.misc | 1 + docs/tcp_replication.md | 5 ++++ synapse/replication/slave/storage/_base.py | 7 +++++- synapse/replication/tcp/streams/_base.py | 26 +++++++++++++++++---- synapse/storage/_base.py | 18 +++++++++++---- synapse/storage/data_stores/main/cache.py | 27 ++++++++++++++++++---- 6 files changed, 69 insertions(+), 15 deletions(-) create mode 100644 changelog.d/6749.misc diff --git a/changelog.d/6749.misc b/changelog.d/6749.misc new file mode 100644 index 0000000000..9fa13cb1d4 --- /dev/null +++ b/changelog.d/6749.misc @@ -0,0 +1 @@ +Allow streaming cache 'invalidate all' to workers. diff --git a/docs/tcp_replication.md b/docs/tcp_replication.md index a0b1d563ff..e3a4634b14 100644 --- a/docs/tcp_replication.md +++ b/docs/tcp_replication.md @@ -254,6 +254,11 @@ and they key to invalidate. For example: > RDATA caches 550953771 ["get_user_by_id", ["@bob:example.com"], 1550574873251] +Alternatively, an entire cache can be invalidated by sending down a `null` +instead of the key. For example: + + > RDATA caches 550953772 ["get_user_by_id", null, 1550574873252] + However, there are times when a number of caches need to be invalidated at the same time with the same key. To reduce traffic we batch those invalidations into a single poke by defining a special cache name that diff --git a/synapse/replication/slave/storage/_base.py b/synapse/replication/slave/storage/_base.py index 704282c800..f45cbd37a0 100644 --- a/synapse/replication/slave/storage/_base.py +++ b/synapse/replication/slave/storage/_base.py @@ -66,11 +66,16 @@ class BaseSlavedStore(SQLBaseStore): self._cache_id_gen.advance(token) for row in rows: if row.cache_func == CURRENT_STATE_CACHE_NAME: + if row.keys is None: + raise Exception( + "Can't send an 'invalidate all' for current state cache" + ) + room_id = row.keys[0] members_changed = set(row.keys[1:]) self._invalidate_state_caches(room_id, members_changed) else: - self._attempt_to_invalidate_cache(row.cache_func, tuple(row.keys)) + self._attempt_to_invalidate_cache(row.cache_func, row.keys) def _invalidate_cache_and_stream(self, txn, cache_func, keys): txn.call_after(cache_func.invalidate, keys) diff --git a/synapse/replication/tcp/streams/_base.py b/synapse/replication/tcp/streams/_base.py index e03e77199b..a8d568b14a 100644 --- a/synapse/replication/tcp/streams/_base.py +++ b/synapse/replication/tcp/streams/_base.py @@ -17,7 +17,9 @@ import itertools import logging from collections import namedtuple -from typing import Any +from typing import Any, List, Optional + +import attr logger = logging.getLogger(__name__) @@ -65,10 +67,24 @@ PushersStreamRow = namedtuple( "PushersStreamRow", ("user_id", "app_id", "pushkey", "deleted"), # str # str # str # bool ) -CachesStreamRow = namedtuple( - "CachesStreamRow", - ("cache_func", "keys", "invalidation_ts"), # str # list(str) # int -) + + +@attr.s +class CachesStreamRow: + """Stream to inform workers they should invalidate their cache. + + Attributes: + cache_func: Name of the cached function. + keys: The entry in the cache to invalidate. If None then will + invalidate all. + invalidation_ts: Timestamp of when the invalidation took place. + """ + + cache_func = attr.ib(type=str) + keys = attr.ib(type=Optional[List[Any]]) + invalidation_ts = attr.ib(type=int) + + PublicRoomsStreamRow = namedtuple( "PublicRoomsStreamRow", ( diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py index 3bb9381663..da3b99f93d 100644 --- a/synapse/storage/_base.py +++ b/synapse/storage/_base.py @@ -17,6 +17,7 @@ import logging import random from abc import ABCMeta +from typing import Any, Optional from six import PY2 from six.moves import builtins @@ -26,7 +27,7 @@ from canonicaljson import json from synapse.storage.database import LoggingTransaction # noqa: F401 from synapse.storage.database import make_in_list_sql_clause # noqa: F401 from synapse.storage.database import Database -from synapse.types import get_domain_from_id +from synapse.types import Collection, get_domain_from_id logger = logging.getLogger(__name__) @@ -63,17 +64,24 @@ class SQLBaseStore(metaclass=ABCMeta): self._attempt_to_invalidate_cache("get_room_summary", (room_id,)) self._attempt_to_invalidate_cache("get_current_state_ids", (room_id,)) - def _attempt_to_invalidate_cache(self, cache_name, key): + def _attempt_to_invalidate_cache( + self, cache_name: str, key: Optional[Collection[Any]] + ): """Attempts to invalidate the cache of the given name, ignoring if the cache doesn't exist. Mainly used for invalidating caches on workers, where they may not have the cache. Args: - cache_name (str) - key (tuple) + cache_name + key: Entry to invalidate. If None then invalidates the entire + cache. """ + try: - getattr(self, cache_name).invalidate(key) + if key is None: + getattr(self, cache_name).invalidate_all() + else: + getattr(self, cache_name).invalidate(tuple(key)) except AttributeError: # We probably haven't pulled in the cache in this worker, # which is fine. diff --git a/synapse/storage/data_stores/main/cache.py b/synapse/storage/data_stores/main/cache.py index bf91512daf..afa2b41c98 100644 --- a/synapse/storage/data_stores/main/cache.py +++ b/synapse/storage/data_stores/main/cache.py @@ -16,6 +16,7 @@ import itertools import logging +from typing import Any, Iterable, Optional from twisted.internet import defer @@ -43,6 +44,14 @@ class CacheInvalidationStore(SQLBaseStore): txn.call_after(cache_func.invalidate, keys) self._send_invalidation_to_replication(txn, cache_func.__name__, keys) + def _invalidate_all_cache_and_stream(self, txn, cache_func): + """Invalidates the entire cache and adds it to the cache stream so slaves + will know to invalidate their caches. + """ + + txn.call_after(cache_func.invalidate_all) + self._send_invalidation_to_replication(txn, cache_func.__name__, None) + def _invalidate_state_caches_and_stream(self, txn, room_id, members_changed): """Special case invalidation of caches based on current state. @@ -73,17 +82,24 @@ class CacheInvalidationStore(SQLBaseStore): txn, CURRENT_STATE_CACHE_NAME, [room_id] ) - def _send_invalidation_to_replication(self, txn, cache_name, keys): + def _send_invalidation_to_replication( + self, txn, cache_name: str, keys: Optional[Iterable[Any]] + ): """Notifies replication that given cache has been invalidated. Note that this does *not* invalidate the cache locally. Args: txn - cache_name (str) - keys (iterable[str]) + cache_name + keys: Entry to invalidate. If None will invalidate all. """ + if cache_name == CURRENT_STATE_CACHE_NAME and keys is None: + raise Exception( + "Can't stream invalidate all with magic current state cache" + ) + if isinstance(self.database_engine, PostgresEngine): # get_next() returns a context manager which is designed to wrap # the transaction. However, we want to only get an ID when we want @@ -95,13 +111,16 @@ class CacheInvalidationStore(SQLBaseStore): txn.call_after(ctx.__exit__, None, None, None) txn.call_after(self.hs.get_notifier().on_new_replication_data) + if keys is not None: + keys = list(keys) + self.db.simple_insert_txn( txn, table="cache_invalidation_stream", values={ "stream_id": stream_id, "cache_func": cache_name, - "keys": list(keys), + "keys": keys, "invalidation_ts": self.clock.time_msec(), }, ) From 5e52d8563bdc0ab6667f0ec2571f35791720a40a Mon Sep 17 00:00:00 2001 From: Neil Johnson Date: Wed, 22 Jan 2020 11:05:14 +0000 Subject: [PATCH 0863/1623] Allow monthly active user limiting support for worker mode, fixes #4639. (#6742) --- changelog.d/6742.bugfix | 1 + synapse/app/client_reader.py | 4 + synapse/app/event_creator.py | 4 + synapse/app/federation_reader.py | 4 + synapse/app/synchrotron.py | 4 + .../data_stores/main/monthly_active_users.py | 165 +++++++++--------- 6 files changed, 100 insertions(+), 82 deletions(-) create mode 100644 changelog.d/6742.bugfix diff --git a/changelog.d/6742.bugfix b/changelog.d/6742.bugfix new file mode 100644 index 0000000000..ca2687c8bb --- /dev/null +++ b/changelog.d/6742.bugfix @@ -0,0 +1 @@ +Fix monthly active user limiting support for worker mode, fixes [#4639](https://github.com/matrix-org/synapse/issues/4639). diff --git a/synapse/app/client_reader.py b/synapse/app/client_reader.py index 3edfe19567..ca96da6a4a 100644 --- a/synapse/app/client_reader.py +++ b/synapse/app/client_reader.py @@ -62,6 +62,9 @@ from synapse.rest.client.v2_alpha.keys import KeyChangesServlet, KeyQueryServlet from synapse.rest.client.v2_alpha.register import RegisterRestServlet from synapse.rest.client.versions import VersionsRestServlet from synapse.server import HomeServer +from synapse.storage.data_stores.main.monthly_active_users import ( + MonthlyActiveUsersWorkerStore, +) from synapse.util.httpresourcetree import create_resource_tree from synapse.util.manhole import manhole from synapse.util.versionstring import get_version_string @@ -85,6 +88,7 @@ class ClientReaderSlavedStore( SlavedTransactionStore, SlavedProfileStore, SlavedClientIpStore, + MonthlyActiveUsersWorkerStore, BaseSlavedStore, ): pass diff --git a/synapse/app/event_creator.py b/synapse/app/event_creator.py index d0ddbe38fc..58e5b354f6 100644 --- a/synapse/app/event_creator.py +++ b/synapse/app/event_creator.py @@ -56,6 +56,9 @@ from synapse.rest.client.v1.room import ( RoomStateEventRestServlet, ) from synapse.server import HomeServer +from synapse.storage.data_stores.main.monthly_active_users import ( + MonthlyActiveUsersWorkerStore, +) from synapse.storage.data_stores.main.user_directory import UserDirectoryStore from synapse.util.httpresourcetree import create_resource_tree from synapse.util.manhole import manhole @@ -81,6 +84,7 @@ class EventCreatorSlavedStore( SlavedEventStore, SlavedRegistrationStore, RoomStore, + MonthlyActiveUsersWorkerStore, BaseSlavedStore, ): pass diff --git a/synapse/app/federation_reader.py b/synapse/app/federation_reader.py index 311523e0ed..1f1cea1416 100644 --- a/synapse/app/federation_reader.py +++ b/synapse/app/federation_reader.py @@ -46,6 +46,9 @@ from synapse.replication.slave.storage.transactions import SlavedTransactionStor from synapse.replication.tcp.client import ReplicationClientHandler from synapse.rest.key.v2 import KeyApiV2Resource from synapse.server import HomeServer +from synapse.storage.data_stores.main.monthly_active_users import ( + MonthlyActiveUsersWorkerStore, +) from synapse.util.httpresourcetree import create_resource_tree from synapse.util.manhole import manhole from synapse.util.versionstring import get_version_string @@ -66,6 +69,7 @@ class FederationReaderSlavedStore( RoomStore, DirectoryStore, SlavedTransactionStore, + MonthlyActiveUsersWorkerStore, BaseSlavedStore, ): pass diff --git a/synapse/app/synchrotron.py b/synapse/app/synchrotron.py index 3218da07bd..8982c0676e 100644 --- a/synapse/app/synchrotron.py +++ b/synapse/app/synchrotron.py @@ -54,6 +54,9 @@ from synapse.rest.client.v1.initial_sync import InitialSyncRestServlet from synapse.rest.client.v1.room import RoomInitialSyncRestServlet from synapse.rest.client.v2_alpha import sync from synapse.server import HomeServer +from synapse.storage.data_stores.main.monthly_active_users import ( + MonthlyActiveUsersWorkerStore, +) from synapse.storage.data_stores.main.presence import UserPresenceState from synapse.util.httpresourcetree import create_resource_tree from synapse.util.manhole import manhole @@ -77,6 +80,7 @@ class SynchrotronSlavedStore( SlavedEventStore, SlavedClientIpStore, RoomStore, + MonthlyActiveUsersWorkerStore, BaseSlavedStore, ): pass diff --git a/synapse/storage/data_stores/main/monthly_active_users.py b/synapse/storage/data_stores/main/monthly_active_users.py index 27158534cb..89a41542a3 100644 --- a/synapse/storage/data_stores/main/monthly_active_users.py +++ b/synapse/storage/data_stores/main/monthly_active_users.py @@ -27,12 +27,76 @@ logger = logging.getLogger(__name__) LAST_SEEN_GRANULARITY = 60 * 60 * 1000 -class MonthlyActiveUsersStore(SQLBaseStore): +class MonthlyActiveUsersWorkerStore(SQLBaseStore): def __init__(self, database: Database, db_conn, hs): - super(MonthlyActiveUsersStore, self).__init__(database, db_conn, hs) + super(MonthlyActiveUsersWorkerStore, self).__init__(database, db_conn, hs) self._clock = hs.get_clock() self.hs = hs + + @cached(num_args=0) + def get_monthly_active_count(self): + """Generates current count of monthly active users + + Returns: + Defered[int]: Number of current monthly active users + """ + + def _count_users(txn): + sql = "SELECT COALESCE(count(*), 0) FROM monthly_active_users" + + txn.execute(sql) + (count,) = txn.fetchone() + return count + + return self.db.runInteraction("count_users", _count_users) + + @defer.inlineCallbacks + def get_registered_reserved_users(self): + """Of the reserved threepids defined in config, which are associated + with registered users? + + Returns: + Defered[list]: Real reserved users + """ + users = [] + + for tp in self.hs.config.mau_limits_reserved_threepids[ + : self.hs.config.max_mau_value + ]: + user_id = yield self.hs.get_datastore().get_user_id_by_threepid( + tp["medium"], tp["address"] + ) + if user_id: + users.append(user_id) + + return users + + @cached(num_args=1) + def user_last_seen_monthly_active(self, user_id): + """ + Checks if a given user is part of the monthly active user group + Arguments: + user_id (str): user to add/update + Return: + Deferred[int] : timestamp since last seen, None if never seen + + """ + + return self.db.simple_select_one_onecol( + table="monthly_active_users", + keyvalues={"user_id": user_id}, + retcol="timestamp", + allow_none=True, + desc="user_last_seen_monthly_active", + ) + + +class MonthlyActiveUsersStore(MonthlyActiveUsersWorkerStore): + def __init__(self, database: Database, db_conn, hs): + super(MonthlyActiveUsersStore, self).__init__(database, db_conn, hs) + # Do not add more reserved users than the total allowable number + # cur = LoggingTransaction( self.db.new_transaction( db_conn, "initialise_mau_threepids", @@ -146,57 +210,22 @@ class MonthlyActiveUsersStore(SQLBaseStore): txn.execute(sql, query_args) + # It seems poor to invalidate the whole cache, Postgres supports + # 'Returning' which would allow me to invalidate only the + # specific users, but sqlite has no way to do this and instead + # I would need to SELECT and the DELETE which without locking + # is racy. + # Have resolved to invalidate the whole cache for now and do + # something about it if and when the perf becomes significant + self._invalidate_all_cache_and_stream( + txn, self.user_last_seen_monthly_active + ) + self._invalidate_cache_and_stream(txn, self.get_monthly_active_count, ()) + reserved_users = yield self.get_registered_reserved_users() yield self.db.runInteraction( "reap_monthly_active_users", _reap_users, reserved_users ) - # It seems poor to invalidate the whole cache, Postgres supports - # 'Returning' which would allow me to invalidate only the - # specific users, but sqlite has no way to do this and instead - # I would need to SELECT and the DELETE which without locking - # is racy. - # Have resolved to invalidate the whole cache for now and do - # something about it if and when the perf becomes significant - self.user_last_seen_monthly_active.invalidate_all() - self.get_monthly_active_count.invalidate_all() - - @cached(num_args=0) - def get_monthly_active_count(self): - """Generates current count of monthly active users - - Returns: - Defered[int]: Number of current monthly active users - """ - - def _count_users(txn): - sql = "SELECT COALESCE(count(*), 0) FROM monthly_active_users" - - txn.execute(sql) - (count,) = txn.fetchone() - return count - - return self.db.runInteraction("count_users", _count_users) - - @defer.inlineCallbacks - def get_registered_reserved_users(self): - """Of the reserved threepids defined in config, which are associated - with registered users? - - Returns: - Defered[list]: Real reserved users - """ - users = [] - - for tp in self.hs.config.mau_limits_reserved_threepids[ - : self.hs.config.max_mau_value - ]: - user_id = yield self.hs.get_datastore().get_user_id_by_threepid( - tp["medium"], tp["address"] - ) - if user_id: - users.append(user_id) - - return users @defer.inlineCallbacks def upsert_monthly_active_user(self, user_id): @@ -222,23 +251,9 @@ class MonthlyActiveUsersStore(SQLBaseStore): "upsert_monthly_active_user", self.upsert_monthly_active_user_txn, user_id ) - user_in_mau = self.user_last_seen_monthly_active.cache.get( - (user_id,), None, update_metrics=False - ) - if user_in_mau is None: - self.get_monthly_active_count.invalidate(()) - - self.user_last_seen_monthly_active.invalidate((user_id,)) - def upsert_monthly_active_user_txn(self, txn, user_id): """Updates or inserts monthly active user member - Note that, after calling this method, it will generally be necessary - to invalidate the caches on user_last_seen_monthly_active and - get_monthly_active_count. We can't do that here, because we are running - in a database thread rather than the main thread, and we can't call - txn.call_after because txn may not be a LoggingTransaction. - We consciously do not call is_support_txn from this method because it is not possible to cache the response. is_support_txn will be false in almost all cases, so it seems reasonable to call it only for @@ -269,27 +284,13 @@ class MonthlyActiveUsersStore(SQLBaseStore): values={"timestamp": int(self._clock.time_msec())}, ) - return is_insert - - @cached(num_args=1) - def user_last_seen_monthly_active(self, user_id): - """ - Checks if a given user is part of the monthly active user group - Arguments: - user_id (str): user to add/update - Return: - Deferred[int] : timestamp since last seen, None if never seen - - """ - - return self.db.simple_select_one_onecol( - table="monthly_active_users", - keyvalues={"user_id": user_id}, - retcol="timestamp", - allow_none=True, - desc="user_last_seen_monthly_active", + self._invalidate_cache_and_stream(txn, self.get_monthly_active_count, ()) + self._invalidate_cache_and_stream( + txn, self.user_last_seen_monthly_active, (user_id,) ) + return is_insert + @defer.inlineCallbacks def populate_monthly_active_users(self, user_id): """Checks on the state of monthly active user limits and optionally From aa9b00fb2f9a7718d67fb11621a83035492ed9fb Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Wed, 22 Jan 2020 11:05:50 +0000 Subject: [PATCH 0864/1623] Fix and add test to deprecated quarantine media admin api (#6756) --- changelog.d/6756.feature | 1 + synapse/rest/admin/media.py | 2 +- tests/rest/admin/test_admin.py | 15 +++++++++++---- 3 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 changelog.d/6756.feature diff --git a/changelog.d/6756.feature b/changelog.d/6756.feature new file mode 100644 index 0000000000..6328c868f2 --- /dev/null +++ b/changelog.d/6756.feature @@ -0,0 +1 @@ +Add new quarantine media admin APIs to quarantine by media ID or by user who uploaded the media. \ No newline at end of file diff --git a/synapse/rest/admin/media.py b/synapse/rest/admin/media.py index 3a445d6eed..ee75095c0e 100644 --- a/synapse/rest/admin/media.py +++ b/synapse/rest/admin/media.py @@ -36,7 +36,7 @@ class QuarantineMediaInRoom(RestServlet): historical_admin_path_patterns("/room/(?P[^/]+)/media/quarantine") + # This path kept around for legacy reasons - historical_admin_path_patterns("/quarantine_media/(?P![^/]+)") + historical_admin_path_patterns("/quarantine_media/(?P[^/]+)") ) def __init__(self, hs): diff --git a/tests/rest/admin/test_admin.py b/tests/rest/admin/test_admin.py index f3b4a31e21..af4d604e50 100644 --- a/tests/rest/admin/test_admin.py +++ b/tests/rest/admin/test_admin.py @@ -516,7 +516,7 @@ class QuarantineMediaTestCase(unittest.HomeserverTestCase): ), ) - def test_quarantine_all_media_in_room(self): + def test_quarantine_all_media_in_room(self, override_url_template=None): self.register_user("room_admin", "pass", admin=True) admin_user_tok = self.login("room_admin", "pass") @@ -555,9 +555,12 @@ class QuarantineMediaTestCase(unittest.HomeserverTestCase): ) # Quarantine all media in the room - url = "/_synapse/admin/v1/room/%s/media/quarantine" % urllib.parse.quote( - room_id - ) + if override_url_template: + url = override_url_template % urllib.parse.quote(room_id) + else: + url = "/_synapse/admin/v1/room/%s/media/quarantine" % urllib.parse.quote( + room_id + ) request, channel = self.make_request("POST", url, access_token=admin_user_tok,) self.render(request) self.pump(1.0) @@ -611,6 +614,10 @@ class QuarantineMediaTestCase(unittest.HomeserverTestCase): ), ) + def test_quaraantine_all_media_in_room_deprecated_api_path(self): + # Perform the above test with the deprecated API path + self.test_quarantine_all_media_in_room("/_synapse/admin/v1/quarantine_media/%s") + def test_quarantine_all_media_by_user(self): self.register_user("user_admin", "pass", admin=True) admin_user_tok = self.login("user_admin", "pass") From ed83c3a018714f20bb660c39e1a42e856eb3bedb Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 22 Jan 2020 12:27:42 +0000 Subject: [PATCH 0865/1623] Fix typo in _select_thumbnail --- synapse/rest/media/v1/thumbnail_resource.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/rest/media/v1/thumbnail_resource.py b/synapse/rest/media/v1/thumbnail_resource.py index eee93b4313..d57480f761 100644 --- a/synapse/rest/media/v1/thumbnail_resource.py +++ b/synapse/rest/media/v1/thumbnail_resource.py @@ -331,7 +331,7 @@ class ThumbnailResource(DirectServeResource): ) ) if crop_info_list: - return min(crop_info_list2)[-1] + return min(crop_info_list)[-1] else: return min(crop_info_list2)[-1] else: From 67aa18e8dc81d6d5c0645fc330e049ed7d2e786e Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 22 Jan 2020 12:28:07 +0000 Subject: [PATCH 0866/1623] Add tests for thumbnailing --- tests/rest/media/v1/test_media_storage.py | 48 +++++++++++++++++++++-- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/tests/rest/media/v1/test_media_storage.py b/tests/rest/media/v1/test_media_storage.py index bc662b61db..d80a7daed3 100644 --- a/tests/rest/media/v1/test_media_storage.py +++ b/tests/rest/media/v1/test_media_storage.py @@ -17,7 +17,7 @@ import os import shutil import tempfile -from binascii import unhexlify +from binascii import unhexlify, hexlify from mock import Mock from six.moves.urllib import parse @@ -149,6 +149,7 @@ class MediaRepoTests(unittest.HomeserverTestCase): self.media_repo = hs.get_media_repository_resource() self.download_resource = self.media_repo.children[b"download"] + self.thumbnail_resource = self.media_repo.children[b"thumbnail"] # smol png self.end_content = unhexlify( @@ -157,10 +158,12 @@ class MediaRepoTests(unittest.HomeserverTestCase): b"0a2db40000000049454e44ae426082" ) + self.media_id = "example.com/12345" + def _req(self, content_disposition): request, channel = self.make_request( - "GET", "example.com/12345", shorthand=False + "GET", self.media_id, shorthand=False ) request.render(self.download_resource) self.pump() @@ -170,7 +173,7 @@ class MediaRepoTests(unittest.HomeserverTestCase): self.assertEqual(len(self.fetches), 1) self.assertEqual(self.fetches[0][1], "example.com") self.assertEqual( - self.fetches[0][2], "/_matrix/media/v1/download/example.com/12345" + self.fetches[0][2], "/_matrix/media/v1/download/" + self.media_id ) self.assertEqual(self.fetches[0][3], {"allow_remote": "false"}) @@ -229,3 +232,42 @@ class MediaRepoTests(unittest.HomeserverTestCase): headers = channel.headers self.assertEqual(headers.getRawHeaders(b"Content-Type"), [b"image/png"]) self.assertEqual(headers.getRawHeaders(b"Content-Disposition"), None) + + def test_thumbnail_crop(self): + expected_body = unhexlify( + b"89504e470d0a1a0a0000000d4948445200000020000000200806" + b"000000737a7af40000001a49444154789cedc101010000008220" + b"ffaf6e484001000000ef0610200001194334ee0000000049454e" + b"44ae426082" + ) + + self._test_thumbnail("crop", expected_body) + + def test_thumbnail_scale(self): + expected_body = unhexlify( + b"89504e470d0a1a0a0000000d4948445200000001000000010806" + b"0000001f15c4890000000d49444154789c636060606000000005" + b"0001a5f645400000000049454e44ae426082" + ) + + self._test_thumbnail("scale", expected_body) + + def _test_thumbnail(self, method, expected_body): + params = "?width=32&height=32&method=" + method + request, channel = self.make_request( + "GET", self.media_id + params, shorthand=False + ) + request.render(self.thumbnail_resource) + self.pump() + + headers = { + b"Content-Length": [b"%d" % (len(self.end_content))], + b"Content-Type": [b"image/png"], + } + self.fetches[0][0].callback( + (self.end_content, (len(self.end_content), headers)) + ) + self.pump() + + self.assertEqual(channel.code, 200) + self.assertEqual(channel.result["body"], expected_body, channel.result["body"]) From d9a8728b1183c416a89a79856f3b5185386600b2 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 22 Jan 2020 12:30:49 +0000 Subject: [PATCH 0867/1623] Remove unused import --- tests/rest/media/v1/test_media_storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/rest/media/v1/test_media_storage.py b/tests/rest/media/v1/test_media_storage.py index d80a7daed3..6345cc7637 100644 --- a/tests/rest/media/v1/test_media_storage.py +++ b/tests/rest/media/v1/test_media_storage.py @@ -17,7 +17,7 @@ import os import shutil import tempfile -from binascii import unhexlify, hexlify +from binascii import unhexlify from mock import Mock from six.moves.urllib import parse From 6ae0c8db3335faa9b5f0e4407f7a4a3713c84062 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 22 Jan 2020 12:38:18 +0000 Subject: [PATCH 0868/1623] Lint + changelog --- changelog.d/6764.misc | 1 + tests/rest/media/v1/test_media_storage.py | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) create mode 100644 changelog.d/6764.misc diff --git a/changelog.d/6764.misc b/changelog.d/6764.misc new file mode 100644 index 0000000000..8edd767405 --- /dev/null +++ b/changelog.d/6764.misc @@ -0,0 +1 @@ +Fixup `synapse.rest` to pass mypy. diff --git a/tests/rest/media/v1/test_media_storage.py b/tests/rest/media/v1/test_media_storage.py index 6345cc7637..1809ceb839 100644 --- a/tests/rest/media/v1/test_media_storage.py +++ b/tests/rest/media/v1/test_media_storage.py @@ -162,9 +162,7 @@ class MediaRepoTests(unittest.HomeserverTestCase): def _req(self, content_disposition): - request, channel = self.make_request( - "GET", self.media_id, shorthand=False - ) + request, channel = self.make_request("GET", self.media_id, shorthand=False) request.render(self.download_resource) self.pump() From 90a28fb475a29daa9e7a9ee7204f6f76cc8af441 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Wed, 22 Jan 2020 13:36:43 +0000 Subject: [PATCH 0869/1623] Admin API to list, filter and sort rooms (#6720) --- changelog.d/6720.feature | 1 + docs/admin_api/rooms.md | 173 ++++++++++ synapse/rest/admin/__init__.py | 3 +- synapse/rest/admin/_base.py | 15 + synapse/rest/admin/rooms.py | 82 +++++ synapse/rest/client/v2_alpha/_base.py | 2 +- synapse/storage/data_stores/main/room.py | 125 ++++++- tests/rest/admin/test_admin.py | 393 ++++++++++++++++++++++- 8 files changed, 787 insertions(+), 7 deletions(-) create mode 100644 changelog.d/6720.feature create mode 100644 docs/admin_api/rooms.md diff --git a/changelog.d/6720.feature b/changelog.d/6720.feature new file mode 100644 index 0000000000..dfc1b74d62 --- /dev/null +++ b/changelog.d/6720.feature @@ -0,0 +1 @@ +Add a new admin API to list and filter rooms on the server. \ No newline at end of file diff --git a/docs/admin_api/rooms.md b/docs/admin_api/rooms.md new file mode 100644 index 0000000000..082721ea95 --- /dev/null +++ b/docs/admin_api/rooms.md @@ -0,0 +1,173 @@ +# List Room API + +The List Room admin API allows server admins to get a list of rooms on their +server. There are various parameters available that allow for filtering and +sorting the returned list. This API supports pagination. + +## Parameters + +The following query parameters are available: + +* `from` - Offset in the returned list. Defaults to `0`. +* `limit` - Maximum amount of rooms to return. Defaults to `100`. +* `order_by` - The method in which to sort the returned list of rooms. Valid values are: + - `alphabetical` - Rooms are ordered alphabetically by room name. This is the default. + - `size` - Rooms are ordered by the number of members. Largest to smallest. +* `dir` - Direction of room order. Either `f` for forwards or `b` for backwards. Setting + this value to `b` will reverse the above sort order. Defaults to `f`. +* `search_term` - Filter rooms by their room name. Search term can be contained in any + part of the room name. Defaults to no filtering. + +The following fields are possible in the JSON response body: + +* `rooms` - An array of objects, each containing information about a room. + - Room objects contain the following fields: + - `room_id` - The ID of the room. + - `name` - The name of the room. + - `canonical_alias` - The canonical (main) alias address of the room. + - `joined_members` - How many users are currently in the room. +* `offset` - The current pagination offset in rooms. This parameter should be + used instead of `next_token` for room offset as `next_token` is + not intended to be parsed. +* `total_rooms` - The total number of rooms this query can return. Using this + and `offset`, you have enough information to know the current + progression through the list. +* `next_batch` - If this field is present, we know that there are potentially + more rooms on the server that did not all fit into this response. + We can use `next_batch` to get the "next page" of results. To do + so, simply repeat your request, setting the `from` parameter to + the value of `next_batch`. +* `prev_batch` - If this field is present, it is possible to paginate backwards. + Use `prev_batch` for the `from` value in the next request to + get the "previous page" of results. + +## Usage + +A standard request with no filtering: + +``` +GET /_synapse/admin/rooms + +{} +``` + +Response: + +``` +{ + "rooms": [ + { + "room_id": "!OGEhHVWSdvArJzumhm:matrix.org", + "name": "Matrix HQ", + "canonical_alias": "#matrix:matrix.org", + "joined_members": 8326 + }, + ... (8 hidden items) ... + { + "room_id": "!xYvNcQPhnkrdUmYczI:matrix.org", + "name": "This Week In Matrix (TWIM)", + "canonical_alias": "#twim:matrix.org", + "joined_members": 314 + } + ], + "offset": 0, + "total_rooms": 10 +} +``` + +Filtering by room name: + +``` +GET /_synapse/admin/rooms?search_term=TWIM + +{} +``` + +Response: + +``` +{ + "rooms": [ + { + "room_id": "!xYvNcQPhnkrdUmYczI:matrix.org", + "name": "This Week In Matrix (TWIM)", + "canonical_alias": "#twim:matrix.org", + "joined_members": 314 + } + ], + "offset": 0, + "total_rooms": 1 +} +``` + +Paginating through a list of rooms: + +``` +GET /_synapse/admin/rooms?order_by=size + +{} +``` + +Response: + +``` +{ + "rooms": [ + { + "room_id": "!OGEhHVWSdvArJzumhm:matrix.org", + "name": "Matrix HQ", + "canonical_alias": "#matrix:matrix.org", + "joined_members": 8326 + }, + ... (98 hidden items) ... + { + "room_id": "!xYvNcQPhnkrdUmYczI:matrix.org", + "name": "This Week In Matrix (TWIM)", + "canonical_alias": "#twim:matrix.org", + "joined_members": 314 + } + ], + "offset": 0, + "total_rooms": 150 + "next_token": 100 +} +``` + +The presence of the `next_token` parameter tells us that there are more rooms +than returned in this request, and we need to make another request to get them. +To get the next batch of room results, we repeat our request, setting the `from` +parameter to the value of `next_token`. + +``` +GET /_synapse/admin/rooms?order_by=size&from=100 + +{} +``` + +Response: + +``` +{ + "rooms": [ + { + "room_id": "!mscvqgqpHYjBGDxNym:matrix.org", + "name": "Music Theory", + "canonical_alias": "#musictheory:matrix.org", + "joined_members": 127 + }, + ... (48 hidden items) ... + { + "room_id": "!twcBhHVdZlQWuuxBhN:termina.org.uk", + "name": "weechat-matrix", + "canonical_alias": "#weechat-matrix:termina.org.uk", + "joined_members": 137 + } + ], + "offset": 100, + "prev_batch": 0, + "total_rooms": 150 +} +``` + +Once the `next_token` parameter is no longer present, we know we've reached the +end of the list. diff --git a/synapse/rest/admin/__init__.py b/synapse/rest/admin/__init__.py index 2932fe2123..42cc2b062a 100644 --- a/synapse/rest/admin/__init__.py +++ b/synapse/rest/admin/__init__.py @@ -29,7 +29,7 @@ from synapse.rest.admin._base import ( from synapse.rest.admin.groups import DeleteGroupAdminRestServlet from synapse.rest.admin.media import ListMediaInRoom, register_servlets_for_media_repo from synapse.rest.admin.purge_room_servlet import PurgeRoomServlet -from synapse.rest.admin.rooms import ShutdownRoomRestServlet +from synapse.rest.admin.rooms import ListRoomRestServlet, ShutdownRoomRestServlet from synapse.rest.admin.server_notice_servlet import SendServerNoticeServlet from synapse.rest.admin.users import ( AccountValidityRenewServlet, @@ -188,6 +188,7 @@ def register_servlets(hs, http_server): Register all the admin servlets. """ register_servlets_for_client_rest_resource(hs, http_server) + ListRoomRestServlet(hs).register(http_server) PurgeRoomServlet(hs).register(http_server) SendServerNoticeServlet(hs).register(http_server) VersionServlet(hs).register(http_server) diff --git a/synapse/rest/admin/_base.py b/synapse/rest/admin/_base.py index afd0647205..459482eb6d 100644 --- a/synapse/rest/admin/_base.py +++ b/synapse/rest/admin/_base.py @@ -40,6 +40,21 @@ def historical_admin_path_patterns(path_regex): ) +def admin_patterns(path_regex: str): + """Returns the list of patterns for an admin endpoint + + Args: + path_regex: The regex string to match. This should NOT have a ^ + as this will be prefixed. + + Returns: + A list of regex patterns. + """ + admin_prefix = "^/_synapse/admin/v1" + patterns = [re.compile(admin_prefix + path_regex)] + return patterns + + async def assert_requester_is_admin(auth, request): """Verify that the requester is an admin user diff --git a/synapse/rest/admin/rooms.py b/synapse/rest/admin/rooms.py index f7cc5e9be9..f9b8c0a4f0 100644 --- a/synapse/rest/admin/rooms.py +++ b/synapse/rest/admin/rooms.py @@ -15,15 +15,20 @@ import logging from synapse.api.constants import Membership +from synapse.api.errors import Codes, SynapseError from synapse.http.servlet import ( RestServlet, assert_params_in_dict, + parse_integer, parse_json_object_from_request, + parse_string, ) from synapse.rest.admin._base import ( + admin_patterns, assert_user_is_admin, historical_admin_path_patterns, ) +from synapse.storage.data_stores.main.room import RoomSortOrder from synapse.types import create_requester from synapse.util.async_helpers import maybe_awaitable @@ -155,3 +160,80 @@ class ShutdownRoomRestServlet(RestServlet): "new_room_id": new_room_id, }, ) + + +class ListRoomRestServlet(RestServlet): + """ + List all rooms that are known to the homeserver. Results are returned + in a dictionary containing room information. Supports pagination. + """ + + PATTERNS = admin_patterns("/rooms") + + def __init__(self, hs): + self.store = hs.get_datastore() + self.auth = hs.get_auth() + self.admin_handler = hs.get_handlers().admin_handler + + async def on_GET(self, request): + requester = await self.auth.get_user_by_req(request) + await assert_user_is_admin(self.auth, requester.user) + + # Extract query parameters + start = parse_integer(request, "from", default=0) + limit = parse_integer(request, "limit", default=100) + order_by = parse_string(request, "order_by", default="alphabetical") + if order_by not in ( + RoomSortOrder.ALPHABETICAL.value, + RoomSortOrder.SIZE.value, + ): + raise SynapseError( + 400, + "Unknown value for order_by: %s" % (order_by,), + errcode=Codes.INVALID_PARAM, + ) + + search_term = parse_string(request, "search_term") + if search_term == "": + raise SynapseError( + 400, + "search_term cannot be an empty string", + errcode=Codes.INVALID_PARAM, + ) + + direction = parse_string(request, "dir", default="f") + if direction not in ("f", "b"): + raise SynapseError( + 400, "Unknown direction: %s" % (direction,), errcode=Codes.INVALID_PARAM + ) + + reverse_order = True if direction == "b" else False + + # Return list of rooms according to parameters + rooms, total_rooms = await self.store.get_rooms_paginate( + start, limit, order_by, reverse_order, search_term + ) + response = { + # next_token should be opaque, so return a value the client can parse + "offset": start, + "rooms": rooms, + "total_rooms": total_rooms, + } + + # Are there more rooms to paginate through after this? + if (start + limit) < total_rooms: + # There are. Calculate where the query should start from next time + # to get the next part of the list + response["next_batch"] = start + limit + + # Is it possible to paginate backwards? Check if we currently have an + # offset + if start > 0: + if start > limit: + # Going back one iteration won't take us to the start. + # Calculate new offset + response["prev_batch"] = start - limit + else: + response["prev_batch"] = 0 + + return 200, response diff --git a/synapse/rest/client/v2_alpha/_base.py b/synapse/rest/client/v2_alpha/_base.py index 2a3f4dd58f..bc11b4dda4 100644 --- a/synapse/rest/client/v2_alpha/_base.py +++ b/synapse/rest/client/v2_alpha/_base.py @@ -32,7 +32,7 @@ def client_patterns(path_regex, releases=(0,), unstable=True, v1=False): Args: path_regex (str): The regex string to match. This should NOT have a ^ - as this will be prefixed. + as this will be prefixed. Returns: SRE_Pattern """ diff --git a/synapse/storage/data_stores/main/room.py b/synapse/storage/data_stores/main/room.py index 49bab62be3..d968803ad2 100644 --- a/synapse/storage/data_stores/main/room.py +++ b/synapse/storage/data_stores/main/room.py @@ -18,7 +18,8 @@ import collections import logging import re from abc import abstractmethod -from typing import List, Optional, Tuple +from enum import Enum +from typing import Any, Dict, List, Optional, Tuple from six import integer_types @@ -46,6 +47,18 @@ RatelimitOverride = collections.namedtuple( ) +class RoomSortOrder(Enum): + """ + Enum to define the sorting method used when returning rooms with get_rooms_paginate + + ALPHABETICAL = sort rooms alphabetically by name + SIZE = sort rooms by membership size, highest to lowest + """ + + ALPHABETICAL = "alphabetical" + SIZE = "size" + + class RoomWorkerStore(SQLBaseStore): def __init__(self, database: Database, db_conn, hs): super(RoomWorkerStore, self).__init__(database, db_conn, hs) @@ -281,6 +294,116 @@ class RoomWorkerStore(SQLBaseStore): desc="is_room_blocked", ) + async def get_rooms_paginate( + self, + start: int, + limit: int, + order_by: RoomSortOrder, + reverse_order: bool, + search_term: Optional[str], + ) -> Tuple[List[Dict[str, Any]], int]: + """Function to retrieve a paginated list of rooms as json. + + Args: + start: offset in the list + limit: maximum amount of rooms to retrieve + order_by: the sort order of the returned list + reverse_order: whether to reverse the room list + search_term: a string to filter room names by + Returns: + A list of room dicts and an integer representing the total number of + rooms that exist given this query + """ + # Filter room names by a string + where_statement = "" + if search_term: + where_statement = "WHERE state.name LIKE ?" + + # Our postgres db driver converts ? -> %s in SQL strings as that's the + # placeholder for postgres. + # HOWEVER, if you put a % into your SQL then everything goes wibbly. + # To get around this, we're going to surround search_term with %'s + # before giving it to the database in python instead + search_term = "%" + search_term + "%" + + # Set ordering + if RoomSortOrder(order_by) == RoomSortOrder.SIZE: + order_by_column = "curr.joined_members" + order_by_asc = False + elif RoomSortOrder(order_by) == RoomSortOrder.ALPHABETICAL: + # Sort alphabetically + order_by_column = "state.name" + order_by_asc = True + else: + raise StoreError( + 500, "Incorrect value for order_by provided: %s" % order_by + ) + + # Whether to return the list in reverse order + if reverse_order: + # Flip the boolean + order_by_asc = not order_by_asc + + # Create one query for getting the limited number of events that the user asked + # for, and another query for getting the total number of events that could be + # returned. Thus allowing us to see if there are more events to paginate through + info_sql = """ + SELECT state.room_id, state.name, state.canonical_alias, curr.joined_members + FROM room_stats_state state + INNER JOIN room_stats_current curr USING (room_id) + %s + ORDER BY %s %s + LIMIT ? + OFFSET ? + """ % ( + where_statement, + order_by_column, + "ASC" if order_by_asc else "DESC", + ) + + # Use a nested SELECT statement as SQL can't count(*) with an OFFSET + count_sql = """ + SELECT count(*) FROM ( + SELECT room_id FROM room_stats_state state + %s + ) AS get_room_ids + """ % ( + where_statement, + ) + + def _get_rooms_paginate_txn(txn): + # Execute the data query + sql_values = (limit, start) + if search_term: + # Add the search term into the WHERE clause + sql_values = (search_term,) + sql_values + txn.execute(info_sql, sql_values) + + # Refactor room query data into a structured dictionary + rooms = [] + for room in txn: + rooms.append( + { + "room_id": room[0], + "name": room[1], + "canonical_alias": room[2], + "joined_members": room[3], + } + ) + + # Execute the count query + + # Add the search term into the WHERE clause if present + sql_values = (search_term,) if search_term else () + txn.execute(count_sql, sql_values) + + room_count = txn.fetchone() + return rooms, room_count[0] + + return await self.db.runInteraction( + "get_rooms_paginate", _get_rooms_paginate_txn, + ) + @cachedInlineCallbacks(max_entries=10000) def get_ratelimit_for_user(self, user_id): """Check if there are any overrides for ratelimiting for the given diff --git a/tests/rest/admin/test_admin.py b/tests/rest/admin/test_admin.py index af4d604e50..0342aed416 100644 --- a/tests/rest/admin/test_admin.py +++ b/tests/rest/admin/test_admin.py @@ -17,6 +17,7 @@ import json import os import urllib.parse from binascii import unhexlify +from typing import List, Optional from mock import Mock @@ -26,7 +27,7 @@ import synapse.rest.admin from synapse.http.server import JsonResource from synapse.logging.context import make_deferred_yieldable from synapse.rest.admin import VersionServlet -from synapse.rest.client.v1 import events, login, room +from synapse.rest.client.v1 import directory, events, login, room from synapse.rest.client.v2_alpha import groups from tests import unittest @@ -468,9 +469,7 @@ class QuarantineMediaTestCase(unittest.HomeserverTestCase): ) # Extract media ID from the response - server_name_and_media_id = response["content_uri"][ - 6: - ] # Cut off the 'mxc://' bit + server_name_and_media_id = response["content_uri"][6:] # Cut off 'mxc://' server_name, media_id = server_name_and_media_id.split("/") # Attempt to access the media @@ -692,3 +691,389 @@ class QuarantineMediaTestCase(unittest.HomeserverTestCase): % server_and_media_id_2 ), ) + + +class RoomTestCase(unittest.HomeserverTestCase): + """Test /room admin API. + """ + + servlets = [ + synapse.rest.admin.register_servlets, + login.register_servlets, + room.register_servlets, + directory.register_servlets, + ] + + def prepare(self, reactor, clock, hs): + self.store = hs.get_datastore() + + # Create user + self.admin_user = self.register_user("admin", "pass", admin=True) + self.admin_user_tok = self.login("admin", "pass") + + def test_list_rooms(self): + """Test that we can list rooms""" + # Create 3 test rooms + total_rooms = 3 + room_ids = [] + for x in range(total_rooms): + room_id = self.helper.create_room_as( + self.admin_user, tok=self.admin_user_tok + ) + room_ids.append(room_id) + + # Request the list of rooms + url = "/_synapse/admin/v1/rooms" + request, channel = self.make_request( + "GET", url.encode("ascii"), access_token=self.admin_user_tok, + ) + self.render(request) + + # Check request completed successfully + self.assertEqual(200, int(channel.code), msg=channel.json_body) + + # Check that response json body contains a "rooms" key + self.assertTrue( + "rooms" in channel.json_body, + msg="Response body does not " "contain a 'rooms' key", + ) + + # Check that 3 rooms were returned + self.assertEqual(3, len(channel.json_body["rooms"]), msg=channel.json_body) + + # Check their room_ids match + returned_room_ids = [room["room_id"] for room in channel.json_body["rooms"]] + self.assertEqual(room_ids, returned_room_ids) + + # Check that all fields are available + for r in channel.json_body["rooms"]: + self.assertIn("name", r) + self.assertIn("canonical_alias", r) + self.assertIn("joined_members", r) + + # Check that the correct number of total rooms was returned + self.assertEqual(channel.json_body["total_rooms"], total_rooms) + + # Check that the offset is correct + # Should be 0 as we aren't paginating + self.assertEqual(channel.json_body["offset"], 0) + + # Check that the prev_batch parameter is not present + self.assertNotIn("prev_batch", channel.json_body) + + # We shouldn't receive a next token here as there's no further rooms to show + self.assertNotIn("next_batch", channel.json_body) + + def test_list_rooms_pagination(self): + """Test that we can get a full list of rooms through pagination""" + # Create 5 test rooms + total_rooms = 5 + room_ids = [] + for x in range(total_rooms): + room_id = self.helper.create_room_as( + self.admin_user, tok=self.admin_user_tok + ) + room_ids.append(room_id) + + # Set the name of the rooms so we get a consistent returned ordering + for idx, room_id in enumerate(room_ids): + self.helper.send_state( + room_id, "m.room.name", {"name": str(idx)}, tok=self.admin_user_tok, + ) + + # Request the list of rooms + returned_room_ids = [] + start = 0 + limit = 2 + + run_count = 0 + should_repeat = True + while should_repeat: + run_count += 1 + + url = "/_synapse/admin/v1/rooms?from=%d&limit=%d&order_by=%s" % ( + start, + limit, + "alphabetical", + ) + request, channel = self.make_request( + "GET", url.encode("ascii"), access_token=self.admin_user_tok, + ) + self.render(request) + self.assertEqual( + 200, int(channel.result["code"]), msg=channel.result["body"] + ) + + self.assertTrue("rooms" in channel.json_body) + for r in channel.json_body["rooms"]: + returned_room_ids.append(r["room_id"]) + + # Check that the correct number of total rooms was returned + self.assertEqual(channel.json_body["total_rooms"], total_rooms) + + # Check that the offset is correct + # We're only getting 2 rooms each page, so should be 2 * last run_count + self.assertEqual(channel.json_body["offset"], 2 * (run_count - 1)) + + if run_count > 1: + # Check the value of prev_batch is correct + self.assertEqual(channel.json_body["prev_batch"], 2 * (run_count - 2)) + + if "next_batch" not in channel.json_body: + # We have reached the end of the list + should_repeat = False + else: + # Make another query with an updated start value + start = channel.json_body["next_batch"] + + # We should've queried the endpoint 3 times + self.assertEqual( + run_count, + 3, + msg="Should've queried 3 times for 5 rooms with limit 2 per query", + ) + + # Check that we received all of the room ids + self.assertEqual(room_ids, returned_room_ids) + + url = "/_synapse/admin/v1/rooms?from=%d&limit=%d" % (start, limit) + request, channel = self.make_request( + "GET", url.encode("ascii"), access_token=self.admin_user_tok, + ) + self.render(request) + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + + def test_correct_room_attributes(self): + """Test the correct attributes for a room are returned""" + # Create a test room + room_id = self.helper.create_room_as(self.admin_user, tok=self.admin_user_tok) + + test_alias = "#test:test" + test_room_name = "something" + + # Have another user join the room + user_2 = self.register_user("user4", "pass") + user_tok_2 = self.login("user4", "pass") + self.helper.join(room_id, user_2, tok=user_tok_2) + + # Create a new alias to this room + url = "/_matrix/client/r0/directory/room/%s" % (urllib.parse.quote(test_alias),) + request, channel = self.make_request( + "PUT", + url.encode("ascii"), + {"room_id": room_id}, + access_token=self.admin_user_tok, + ) + self.render(request) + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + + # Set this new alias as the canonical alias for this room + self.helper.send_state( + room_id, + "m.room.aliases", + {"aliases": [test_alias]}, + tok=self.admin_user_tok, + state_key="test", + ) + self.helper.send_state( + room_id, + "m.room.canonical_alias", + {"alias": test_alias}, + tok=self.admin_user_tok, + ) + + # Set a name for the room + self.helper.send_state( + room_id, "m.room.name", {"name": test_room_name}, tok=self.admin_user_tok, + ) + + # Request the list of rooms + url = "/_synapse/admin/v1/rooms" + request, channel = self.make_request( + "GET", url.encode("ascii"), access_token=self.admin_user_tok, + ) + self.render(request) + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + + # Check that rooms were returned + self.assertTrue("rooms" in channel.json_body) + rooms = channel.json_body["rooms"] + + # Check that only one room was returned + self.assertEqual(len(rooms), 1) + + # And that the value of the total_rooms key was correct + self.assertEqual(channel.json_body["total_rooms"], 1) + + # Check that the offset is correct + # We're not paginating, so should be 0 + self.assertEqual(channel.json_body["offset"], 0) + + # Check that there is no `prev_batch` + self.assertNotIn("prev_batch", channel.json_body) + + # Check that there is no `next_batch` + self.assertNotIn("next_batch", channel.json_body) + + # Check that all provided attributes are set + r = rooms[0] + self.assertEqual(room_id, r["room_id"]) + self.assertEqual(test_room_name, r["name"]) + self.assertEqual(test_alias, r["canonical_alias"]) + + def test_room_list_sort_order(self): + """Test room list sort ordering. alphabetical versus number of members, + reversing the order, etc. + """ + # Create 3 test rooms + room_id_1 = self.helper.create_room_as(self.admin_user, tok=self.admin_user_tok) + room_id_2 = self.helper.create_room_as(self.admin_user, tok=self.admin_user_tok) + room_id_3 = self.helper.create_room_as(self.admin_user, tok=self.admin_user_tok) + + # Set room names in alphabetical order. room 1 -> A, 2 -> B, 3 -> C + self.helper.send_state( + room_id_1, "m.room.name", {"name": "A"}, tok=self.admin_user_tok, + ) + self.helper.send_state( + room_id_2, "m.room.name", {"name": "B"}, tok=self.admin_user_tok, + ) + self.helper.send_state( + room_id_3, "m.room.name", {"name": "C"}, tok=self.admin_user_tok, + ) + + # Set room member size in the reverse order. room 1 -> 1 member, 2 -> 2, 3 -> 3 + user_1 = self.register_user("bob1", "pass") + user_1_tok = self.login("bob1", "pass") + self.helper.join(room_id_2, user_1, tok=user_1_tok) + + user_2 = self.register_user("bob2", "pass") + user_2_tok = self.login("bob2", "pass") + self.helper.join(room_id_3, user_2, tok=user_2_tok) + + user_3 = self.register_user("bob3", "pass") + user_3_tok = self.login("bob3", "pass") + self.helper.join(room_id_3, user_3, tok=user_3_tok) + + def _order_test( + order_type: str, expected_room_list: List[str], reverse: bool = False, + ): + """Request the list of rooms in a certain order. Assert that order is what + we expect + + Args: + order_type: The type of ordering to give the server + expected_room_list: The list of room_ids in the order we expect to get + back from the server + """ + # Request the list of rooms in the given order + url = "/_synapse/admin/v1/rooms?order_by=%s" % (order_type,) + if reverse: + url += "&dir=b" + request, channel = self.make_request( + "GET", url.encode("ascii"), access_token=self.admin_user_tok, + ) + self.render(request) + self.assertEqual(200, channel.code, msg=channel.json_body) + + # Check that rooms were returned + self.assertTrue("rooms" in channel.json_body) + rooms = channel.json_body["rooms"] + + # Check for the correct total_rooms value + self.assertEqual(channel.json_body["total_rooms"], 3) + + # Check that the offset is correct + # We're not paginating, so should be 0 + self.assertEqual(channel.json_body["offset"], 0) + + # Check that there is no `prev_batch` + self.assertNotIn("prev_batch", channel.json_body) + + # Check that there is no `next_batch` + self.assertNotIn("next_batch", channel.json_body) + + # Check that rooms were returned in alphabetical order + returned_order = [r["room_id"] for r in rooms] + self.assertListEqual(expected_room_list, returned_order) # order is checked + + # Test different sort orders, with forward and reverse directions + _order_test("alphabetical", [room_id_1, room_id_2, room_id_3]) + _order_test("alphabetical", [room_id_3, room_id_2, room_id_1], reverse=True) + + _order_test("size", [room_id_3, room_id_2, room_id_1]) + _order_test("size", [room_id_1, room_id_2, room_id_3], reverse=True) + + def test_search_term(self): + """Test that searching for a room works correctly""" + # Create two test rooms + room_id_1 = self.helper.create_room_as(self.admin_user, tok=self.admin_user_tok) + room_id_2 = self.helper.create_room_as(self.admin_user, tok=self.admin_user_tok) + + room_name_1 = "something" + room_name_2 = "else" + + # Set the name for each room + self.helper.send_state( + room_id_1, "m.room.name", {"name": room_name_1}, tok=self.admin_user_tok, + ) + self.helper.send_state( + room_id_2, "m.room.name", {"name": room_name_2}, tok=self.admin_user_tok, + ) + + def _search_test( + expected_room_id: Optional[str], + search_term: str, + expected_http_code: int = 200, + ): + """Search for a room and check that the returned room's id is a match + + Args: + expected_room_id: The room_id expected to be returned by the API. Set + to None to expect zero results for the search + search_term: The term to search for room names with + expected_http_code: The expected http code for the request + """ + url = "/_synapse/admin/v1/rooms?search_term=%s" % (search_term,) + request, channel = self.make_request( + "GET", url.encode("ascii"), access_token=self.admin_user_tok, + ) + self.render(request) + self.assertEqual(expected_http_code, channel.code, msg=channel.json_body) + + if expected_http_code != 200: + return + + # Check that rooms were returned + self.assertTrue("rooms" in channel.json_body) + rooms = channel.json_body["rooms"] + + # Check that the expected number of rooms were returned + expected_room_count = 1 if expected_room_id else 0 + self.assertEqual(len(rooms), expected_room_count) + self.assertEqual(channel.json_body["total_rooms"], expected_room_count) + + # Check that the offset is correct + # We're not paginating, so should be 0 + self.assertEqual(channel.json_body["offset"], 0) + + # Check that there is no `prev_batch` + self.assertNotIn("prev_batch", channel.json_body) + + # Check that there is no `next_batch` + self.assertNotIn("next_batch", channel.json_body) + + if expected_room_id: + # Check that the first returned room id is correct + r = rooms[0] + self.assertEqual(expected_room_id, r["room_id"]) + + # Perform search tests + _search_test(room_id_1, "something") + _search_test(room_id_1, "thing") + + _search_test(room_id_2, "else") + _search_test(room_id_2, "se") + + _search_test(None, "foo") + _search_test(None, "bar") + _search_test(None, "", expected_http_code=400) From 0d0f32bc53fefb5eb444940998b97594da894967 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 22 Jan 2020 14:03:46 +0000 Subject: [PATCH 0870/1623] 1.9.0rc1 --- CHANGES.md | 70 ++++++++++++++++++++++++++++++++++++++++ changelog.d/5742.feature | 1 - changelog.d/6621.doc | 1 - changelog.d/6624.doc | 1 - changelog.d/6654.bugfix | 1 - changelog.d/6655.misc | 1 - changelog.d/6656.doc | 1 - changelog.d/6663.doc | 1 - changelog.d/6664.bugfix | 1 - changelog.d/6665.doc | 1 - changelog.d/6666.misc | 1 - changelog.d/6667.misc | 1 - changelog.d/6675.removal | 1 - changelog.d/6681.feature | 1 - changelog.d/6682.bugfix | 2 -- changelog.d/6685.doc | 1 - changelog.d/6686.misc | 1 - changelog.d/6687.misc | 1 - changelog.d/6688.misc | 1 - changelog.d/6689.misc | 1 - changelog.d/6690.bugfix | 1 - changelog.d/6691.misc | 1 - changelog.d/6697.misc | 1 - changelog.d/6698.doc | 1 - changelog.d/6702.misc | 1 - changelog.d/6706.misc | 1 - changelog.d/6711.bugfix | 1 - changelog.d/6712.feature | 1 - changelog.d/6714.bugfix | 1 - changelog.d/6715.misc | 1 - changelog.d/6716.misc | 1 - changelog.d/6717.misc | 1 - changelog.d/6718.bugfix | 1 - changelog.d/6720.feature | 1 - changelog.d/6723.misc | 1 - changelog.d/6724.misc | 1 - changelog.d/6728.misc | 1 - changelog.d/6730.bugfix | 1 - changelog.d/6731.bugfix | 1 - changelog.d/6732.misc | 1 - changelog.d/6733.misc | 1 - changelog.d/6742.bugfix | 1 - changelog.d/6747.bugfix | 1 - changelog.d/6749.misc | 1 - changelog.d/6753.bugfix | 1 - changelog.d/6754.misc | 1 - changelog.d/6756.feature | 1 - changelog.d/6764.misc | 1 - synapse/__init__.py | 2 +- 49 files changed, 71 insertions(+), 49 deletions(-) delete mode 100644 changelog.d/5742.feature delete mode 100644 changelog.d/6621.doc delete mode 100644 changelog.d/6624.doc delete mode 100644 changelog.d/6654.bugfix delete mode 100644 changelog.d/6655.misc delete mode 100644 changelog.d/6656.doc delete mode 100644 changelog.d/6663.doc delete mode 100644 changelog.d/6664.bugfix delete mode 100644 changelog.d/6665.doc delete mode 100644 changelog.d/6666.misc delete mode 100644 changelog.d/6667.misc delete mode 100644 changelog.d/6675.removal delete mode 100644 changelog.d/6681.feature delete mode 100644 changelog.d/6682.bugfix delete mode 100644 changelog.d/6685.doc delete mode 100644 changelog.d/6686.misc delete mode 100644 changelog.d/6687.misc delete mode 100644 changelog.d/6688.misc delete mode 100644 changelog.d/6689.misc delete mode 100644 changelog.d/6690.bugfix delete mode 100644 changelog.d/6691.misc delete mode 100644 changelog.d/6697.misc delete mode 100644 changelog.d/6698.doc delete mode 100644 changelog.d/6702.misc delete mode 100644 changelog.d/6706.misc delete mode 100644 changelog.d/6711.bugfix delete mode 100644 changelog.d/6712.feature delete mode 100644 changelog.d/6714.bugfix delete mode 100644 changelog.d/6715.misc delete mode 100644 changelog.d/6716.misc delete mode 100644 changelog.d/6717.misc delete mode 100644 changelog.d/6718.bugfix delete mode 100644 changelog.d/6720.feature delete mode 100644 changelog.d/6723.misc delete mode 100644 changelog.d/6724.misc delete mode 100644 changelog.d/6728.misc delete mode 100644 changelog.d/6730.bugfix delete mode 100644 changelog.d/6731.bugfix delete mode 100644 changelog.d/6732.misc delete mode 100644 changelog.d/6733.misc delete mode 100644 changelog.d/6742.bugfix delete mode 100644 changelog.d/6747.bugfix delete mode 100644 changelog.d/6749.misc delete mode 100644 changelog.d/6753.bugfix delete mode 100644 changelog.d/6754.misc delete mode 100644 changelog.d/6756.feature delete mode 100644 changelog.d/6764.misc diff --git a/CHANGES.md b/CHANGES.md index c8840e9c74..7629a7e8ce 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,73 @@ +Synapse 1.9.0rc1 (2020-01-22) +============================= + +Features +-------- + +- Allow admin to create or modify a user. Contributed by Awesome Technologies Innovationslabor GmbH. ([\#5742](https://github.com/matrix-org/synapse/issues/5742)) +- Add new quarantine media admin APIs to quarantine by media ID or by user who uploaded the media. ([\#6681](https://github.com/matrix-org/synapse/issues/6681), [\#6756](https://github.com/matrix-org/synapse/issues/6756)) +- Add org.matrix.e2e_cross_signing to unstable_features in /versions as per [MSC1756](https://github.com/matrix-org/matrix-doc/pull/1756). ([\#6712](https://github.com/matrix-org/synapse/issues/6712)) +- Add a new admin API to list and filter rooms on the server. ([\#6720](https://github.com/matrix-org/synapse/issues/6720)) + + +Bugfixes +-------- + +- Correctly proxy HTTP errors due to API calls to remote group servers. ([\#6654](https://github.com/matrix-org/synapse/issues/6654)) +- Fix media repo admin APIs when using a media worker. ([\#6664](https://github.com/matrix-org/synapse/issues/6664)) +- Fix "CRITICAL" errors being logged when a request is received for a uri containing non-ascii characters. ([\#6682](https://github.com/matrix-org/synapse/issues/6682)) +- Fix a bug where we would assign a numeric userid if somebody tried registering with an empty username. ([\#6690](https://github.com/matrix-org/synapse/issues/6690)) +- Fix `purge_room` admin API. ([\#6711](https://github.com/matrix-org/synapse/issues/6711)) +- Fix a bug causing Synapse to not always purge quiet rooms with a low `max_lifetime` in their message retention policies when running the automated purge jobs. ([\#6714](https://github.com/matrix-org/synapse/issues/6714)) +- Fix a bug causing the `synapse_port_db` script to return 0 in a specific error case. ([\#6718](https://github.com/matrix-org/synapse/issues/6718)) +- Fix changing password via user admin API. ([\#6730](https://github.com/matrix-org/synapse/issues/6730)) +- Fix `/events/:event_id` deprecated API. ([\#6731](https://github.com/matrix-org/synapse/issues/6731)) +- Fix monthly active user limiting support for worker mode, fixes [#4639](https://github.com/matrix-org/synapse/issues/4639). ([\#6742](https://github.com/matrix-org/synapse/issues/6742)) +- Fix bug when setting `account_validity` to an empty block in the config. Thanks to @Sorunome for reporting. ([\#6747](https://github.com/matrix-org/synapse/issues/6747)) +- Fix `AttributeError: 'NoneType' object has no attribute 'get'` in `hash_password` when configuration has an empty `password_config`. Contributed by @ivilata. ([\#6753](https://github.com/matrix-org/synapse/issues/6753)) + + +Improved Documentation +---------------------- + +- Fix a typo in the configuration example for purge jobs in the sample configuration file. ([\#6621](https://github.com/matrix-org/synapse/issues/6621)) +- Add complete documentation of the message retention policies support. ([\#6624](https://github.com/matrix-org/synapse/issues/6624), [\#6665](https://github.com/matrix-org/synapse/issues/6665)) +- No more overriding the entire /etc folder of the container in docker-compose.yaml. Contributed by Fabian Meyer. ([\#6656](https://github.com/matrix-org/synapse/issues/6656)) +- Add some helpful tips about changelog entries to the github pull request template. ([\#6663](https://github.com/matrix-org/synapse/issues/6663)) +- Clarify the `account_validity` and `email` sections of the sample configuration. ([\#6685](https://github.com/matrix-org/synapse/issues/6685)) +- Add more endpoints to the documentation for Synapse workers. ([\#6698](https://github.com/matrix-org/synapse/issues/6698)) + + +Deprecations and Removals +------------------------- + +- Synapse no longer supports versions of SQLite before 3.11, and will refuse to start when configured to use an older version. Administrators are recommended to migrate their database to Postgres (see instructions [here](docs/postgres.md)). ([\#6675](https://github.com/matrix-org/synapse/issues/6675)) + + +Internal Changes +---------------- + +- Add `local_current_membership` table for tracking local user membership state in rooms. ([\#6655](https://github.com/matrix-org/synapse/issues/6655), [\#6728](https://github.com/matrix-org/synapse/issues/6728)) +- Port `synapse.replication.tcp` to async/await. ([\#6666](https://github.com/matrix-org/synapse/issues/6666)) +- Fixup `synapse.replication` to pass mypy checks. ([\#6667](https://github.com/matrix-org/synapse/issues/6667)) +- Allow additional_resources to implement IResource directly. ([\#6686](https://github.com/matrix-org/synapse/issues/6686)) +- Allow REST endpoint implementations to raise a RedirectException, which will redirect the user's browser to a given location. ([\#6687](https://github.com/matrix-org/synapse/issues/6687)) +- Updates and extensions to the module API. ([\#6688](https://github.com/matrix-org/synapse/issues/6688)) +- Updates to the SAML mapping provider API. ([\#6689](https://github.com/matrix-org/synapse/issues/6689), [\#6723](https://github.com/matrix-org/synapse/issues/6723)) +- Remove redundant RegistrationError class. ([\#6691](https://github.com/matrix-org/synapse/issues/6691)) +- Don't block processing of incoming EDUs behind processing PDUs in the same transaction. ([\#6697](https://github.com/matrix-org/synapse/issues/6697)) +- Remove duplicate check for the `session` query parameter on the `/auth/xxx/fallback/web` Client-Server endpoint. ([\#6702](https://github.com/matrix-org/synapse/issues/6702)) +- Attempt to retry sending a transaction when we detect a remote server has come back online, rather than waiting for a transaction to be triggered by new data. ([\#6706](https://github.com/matrix-org/synapse/issues/6706)) +- Add StateMap type alias to simplify types. ([\#6715](https://github.com/matrix-org/synapse/issues/6715)) +- Add a `DeltaState` to track changes to be made to current state during event persistence. ([\#6716](https://github.com/matrix-org/synapse/issues/6716)) +- Add more logging around message retention policies support. ([\#6717](https://github.com/matrix-org/synapse/issues/6717)) +- When processing a SAML response, log the assertions for easier configuration. ([\#6724](https://github.com/matrix-org/synapse/issues/6724)) +- Fixup `synapse.rest` to pass mypy. ([\#6732](https://github.com/matrix-org/synapse/issues/6732), [\#6764](https://github.com/matrix-org/synapse/issues/6764)) +- Fixup synapse.api to pass mypy. ([\#6733](https://github.com/matrix-org/synapse/issues/6733)) +- Allow streaming cache 'invalidate all' to workers. ([\#6749](https://github.com/matrix-org/synapse/issues/6749)) +- Remove unused CI docker compose files. ([\#6754](https://github.com/matrix-org/synapse/issues/6754)) + + Synapse 1.8.0 (2020-01-09) ========================== diff --git a/changelog.d/5742.feature b/changelog.d/5742.feature deleted file mode 100644 index de10302275..0000000000 --- a/changelog.d/5742.feature +++ /dev/null @@ -1 +0,0 @@ -Allow admin to create or modify a user. Contributed by Awesome Technologies Innovationslabor GmbH. diff --git a/changelog.d/6621.doc b/changelog.d/6621.doc deleted file mode 100644 index 6722ccfda3..0000000000 --- a/changelog.d/6621.doc +++ /dev/null @@ -1 +0,0 @@ -Fix a typo in the configuration example for purge jobs in the sample configuration file. diff --git a/changelog.d/6624.doc b/changelog.d/6624.doc deleted file mode 100644 index bc9a022db2..0000000000 --- a/changelog.d/6624.doc +++ /dev/null @@ -1 +0,0 @@ -Add complete documentation of the message retention policies support. diff --git a/changelog.d/6654.bugfix b/changelog.d/6654.bugfix deleted file mode 100644 index fed35252db..0000000000 --- a/changelog.d/6654.bugfix +++ /dev/null @@ -1 +0,0 @@ -Correctly proxy HTTP errors due to API calls to remote group servers. diff --git a/changelog.d/6655.misc b/changelog.d/6655.misc deleted file mode 100644 index 01e78bc84e..0000000000 --- a/changelog.d/6655.misc +++ /dev/null @@ -1 +0,0 @@ -Add `local_current_membership` table for tracking local user membership state in rooms. diff --git a/changelog.d/6656.doc b/changelog.d/6656.doc deleted file mode 100644 index 9f32da1a88..0000000000 --- a/changelog.d/6656.doc +++ /dev/null @@ -1 +0,0 @@ -No more overriding the entire /etc folder of the container in docker-compose.yaml. Contributed by Fabian Meyer. diff --git a/changelog.d/6663.doc b/changelog.d/6663.doc deleted file mode 100644 index 83b9c1626a..0000000000 --- a/changelog.d/6663.doc +++ /dev/null @@ -1 +0,0 @@ -Add some helpful tips about changelog entries to the github pull request template. \ No newline at end of file diff --git a/changelog.d/6664.bugfix b/changelog.d/6664.bugfix deleted file mode 100644 index 8c6a6fa1c8..0000000000 --- a/changelog.d/6664.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix media repo admin APIs when using a media worker. diff --git a/changelog.d/6665.doc b/changelog.d/6665.doc deleted file mode 100644 index bc9a022db2..0000000000 --- a/changelog.d/6665.doc +++ /dev/null @@ -1 +0,0 @@ -Add complete documentation of the message retention policies support. diff --git a/changelog.d/6666.misc b/changelog.d/6666.misc deleted file mode 100644 index e79c23d2d2..0000000000 --- a/changelog.d/6666.misc +++ /dev/null @@ -1 +0,0 @@ -Port `synapse.replication.tcp` to async/await. diff --git a/changelog.d/6667.misc b/changelog.d/6667.misc deleted file mode 100644 index 227f80a508..0000000000 --- a/changelog.d/6667.misc +++ /dev/null @@ -1 +0,0 @@ -Fixup `synapse.replication` to pass mypy checks. diff --git a/changelog.d/6675.removal b/changelog.d/6675.removal deleted file mode 100644 index 95df9a2d83..0000000000 --- a/changelog.d/6675.removal +++ /dev/null @@ -1 +0,0 @@ -Synapse no longer supports versions of SQLite before 3.11, and will refuse to start when configured to use an older version. Administrators are recommended to migrate their database to Postgres (see instructions [here](docs/postgres.md)). diff --git a/changelog.d/6681.feature b/changelog.d/6681.feature deleted file mode 100644 index 5cf19a4e0e..0000000000 --- a/changelog.d/6681.feature +++ /dev/null @@ -1 +0,0 @@ -Add new quarantine media admin APIs to quarantine by media ID or by user who uploaded the media. diff --git a/changelog.d/6682.bugfix b/changelog.d/6682.bugfix deleted file mode 100644 index d48ea31477..0000000000 --- a/changelog.d/6682.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -Fix "CRITICAL" errors being logged when a request is received for a uri containing non-ascii characters. - diff --git a/changelog.d/6685.doc b/changelog.d/6685.doc deleted file mode 100644 index 7cf750fe3f..0000000000 --- a/changelog.d/6685.doc +++ /dev/null @@ -1 +0,0 @@ -Clarify the `account_validity` and `email` sections of the sample configuration. \ No newline at end of file diff --git a/changelog.d/6686.misc b/changelog.d/6686.misc deleted file mode 100644 index 4070f2e563..0000000000 --- a/changelog.d/6686.misc +++ /dev/null @@ -1 +0,0 @@ -Allow additional_resources to implement IResource directly. diff --git a/changelog.d/6687.misc b/changelog.d/6687.misc deleted file mode 100644 index deb0454602..0000000000 --- a/changelog.d/6687.misc +++ /dev/null @@ -1 +0,0 @@ -Allow REST endpoint implementations to raise a RedirectException, which will redirect the user's browser to a given location. diff --git a/changelog.d/6688.misc b/changelog.d/6688.misc deleted file mode 100644 index 2a9f28ce5c..0000000000 --- a/changelog.d/6688.misc +++ /dev/null @@ -1 +0,0 @@ -Updates and extensions to the module API. \ No newline at end of file diff --git a/changelog.d/6689.misc b/changelog.d/6689.misc deleted file mode 100644 index 17f15e73a8..0000000000 --- a/changelog.d/6689.misc +++ /dev/null @@ -1 +0,0 @@ -Updates to the SAML mapping provider API. diff --git a/changelog.d/6690.bugfix b/changelog.d/6690.bugfix deleted file mode 100644 index 30ce1dc9f7..0000000000 --- a/changelog.d/6690.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a bug where we would assign a numeric userid if somebody tried registering with an empty username. diff --git a/changelog.d/6691.misc b/changelog.d/6691.misc deleted file mode 100644 index 104e9ce648..0000000000 --- a/changelog.d/6691.misc +++ /dev/null @@ -1 +0,0 @@ -Remove redundant RegistrationError class. diff --git a/changelog.d/6697.misc b/changelog.d/6697.misc deleted file mode 100644 index 5650387804..0000000000 --- a/changelog.d/6697.misc +++ /dev/null @@ -1 +0,0 @@ -Don't block processing of incoming EDUs behind processing PDUs in the same transaction. diff --git a/changelog.d/6698.doc b/changelog.d/6698.doc deleted file mode 100644 index 5aba51252d..0000000000 --- a/changelog.d/6698.doc +++ /dev/null @@ -1 +0,0 @@ -Add more endpoints to the documentation for Synapse workers. diff --git a/changelog.d/6702.misc b/changelog.d/6702.misc deleted file mode 100644 index f7bc98409c..0000000000 --- a/changelog.d/6702.misc +++ /dev/null @@ -1 +0,0 @@ -Remove duplicate check for the `session` query parameter on the `/auth/xxx/fallback/web` Client-Server endpoint. \ No newline at end of file diff --git a/changelog.d/6706.misc b/changelog.d/6706.misc deleted file mode 100644 index 1ac11cc04b..0000000000 --- a/changelog.d/6706.misc +++ /dev/null @@ -1 +0,0 @@ -Attempt to retry sending a transaction when we detect a remote server has come back online, rather than waiting for a transaction to be triggered by new data. diff --git a/changelog.d/6711.bugfix b/changelog.d/6711.bugfix deleted file mode 100644 index c70506bd88..0000000000 --- a/changelog.d/6711.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix `purge_room` admin API. diff --git a/changelog.d/6712.feature b/changelog.d/6712.feature deleted file mode 100644 index 2cce0ecf88..0000000000 --- a/changelog.d/6712.feature +++ /dev/null @@ -1 +0,0 @@ -Add org.matrix.e2e_cross_signing to unstable_features in /versions as per [MSC1756](https://github.com/matrix-org/matrix-doc/pull/1756). diff --git a/changelog.d/6714.bugfix b/changelog.d/6714.bugfix deleted file mode 100644 index 410516694f..0000000000 --- a/changelog.d/6714.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a bug causing Synapse to not always purge quiet rooms with a low `max_lifetime` in their message retention policies when running the automated purge jobs. diff --git a/changelog.d/6715.misc b/changelog.d/6715.misc deleted file mode 100644 index 8876b0446d..0000000000 --- a/changelog.d/6715.misc +++ /dev/null @@ -1 +0,0 @@ -Add StateMap type alias to simplify types. diff --git a/changelog.d/6716.misc b/changelog.d/6716.misc deleted file mode 100644 index 319aaa4acb..0000000000 --- a/changelog.d/6716.misc +++ /dev/null @@ -1 +0,0 @@ -Add a `DeltaState` to track changes to be made to current state during event persistence. diff --git a/changelog.d/6717.misc b/changelog.d/6717.misc deleted file mode 100644 index a2a7776126..0000000000 --- a/changelog.d/6717.misc +++ /dev/null @@ -1 +0,0 @@ -Add more logging around message retention policies support. diff --git a/changelog.d/6718.bugfix b/changelog.d/6718.bugfix deleted file mode 100644 index 23b23e3ed8..0000000000 --- a/changelog.d/6718.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a bug causing the `synapse_port_db` script to return 0 in a specific error case. diff --git a/changelog.d/6720.feature b/changelog.d/6720.feature deleted file mode 100644 index dfc1b74d62..0000000000 --- a/changelog.d/6720.feature +++ /dev/null @@ -1 +0,0 @@ -Add a new admin API to list and filter rooms on the server. \ No newline at end of file diff --git a/changelog.d/6723.misc b/changelog.d/6723.misc deleted file mode 100644 index 17f15e73a8..0000000000 --- a/changelog.d/6723.misc +++ /dev/null @@ -1 +0,0 @@ -Updates to the SAML mapping provider API. diff --git a/changelog.d/6724.misc b/changelog.d/6724.misc deleted file mode 100644 index 5256be75fa..0000000000 --- a/changelog.d/6724.misc +++ /dev/null @@ -1 +0,0 @@ -When processing a SAML response, log the assertions for easier configuration. diff --git a/changelog.d/6728.misc b/changelog.d/6728.misc deleted file mode 100644 index 01e78bc84e..0000000000 --- a/changelog.d/6728.misc +++ /dev/null @@ -1 +0,0 @@ -Add `local_current_membership` table for tracking local user membership state in rooms. diff --git a/changelog.d/6730.bugfix b/changelog.d/6730.bugfix deleted file mode 100644 index beb444ca66..0000000000 --- a/changelog.d/6730.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix changing password via user admin API. diff --git a/changelog.d/6731.bugfix b/changelog.d/6731.bugfix deleted file mode 100644 index 21f6e15cbd..0000000000 --- a/changelog.d/6731.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix `/events/:event_id` deprecated API. diff --git a/changelog.d/6732.misc b/changelog.d/6732.misc deleted file mode 100644 index 8edd767405..0000000000 --- a/changelog.d/6732.misc +++ /dev/null @@ -1 +0,0 @@ -Fixup `synapse.rest` to pass mypy. diff --git a/changelog.d/6733.misc b/changelog.d/6733.misc deleted file mode 100644 index bf048c0be2..0000000000 --- a/changelog.d/6733.misc +++ /dev/null @@ -1 +0,0 @@ -Fixup synapse.api to pass mypy. diff --git a/changelog.d/6742.bugfix b/changelog.d/6742.bugfix deleted file mode 100644 index ca2687c8bb..0000000000 --- a/changelog.d/6742.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix monthly active user limiting support for worker mode, fixes [#4639](https://github.com/matrix-org/synapse/issues/4639). diff --git a/changelog.d/6747.bugfix b/changelog.d/6747.bugfix deleted file mode 100644 index c98107e741..0000000000 --- a/changelog.d/6747.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix bug when setting `account_validity` to an empty block in the config. Thanks to @Sorunome for reporting. diff --git a/changelog.d/6749.misc b/changelog.d/6749.misc deleted file mode 100644 index 9fa13cb1d4..0000000000 --- a/changelog.d/6749.misc +++ /dev/null @@ -1 +0,0 @@ -Allow streaming cache 'invalidate all' to workers. diff --git a/changelog.d/6753.bugfix b/changelog.d/6753.bugfix deleted file mode 100644 index 5dfde793e1..0000000000 --- a/changelog.d/6753.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix `AttributeError: 'NoneType' object has no attribute 'get'` in `hash_password` when configuration has an empty `password_config`. Contributed by @ivilata. diff --git a/changelog.d/6754.misc b/changelog.d/6754.misc deleted file mode 100644 index 0a955e47e6..0000000000 --- a/changelog.d/6754.misc +++ /dev/null @@ -1 +0,0 @@ -Remove unused CI docker compose files. diff --git a/changelog.d/6756.feature b/changelog.d/6756.feature deleted file mode 100644 index 6328c868f2..0000000000 --- a/changelog.d/6756.feature +++ /dev/null @@ -1 +0,0 @@ -Add new quarantine media admin APIs to quarantine by media ID or by user who uploaded the media. \ No newline at end of file diff --git a/changelog.d/6764.misc b/changelog.d/6764.misc deleted file mode 100644 index 8edd767405..0000000000 --- a/changelog.d/6764.misc +++ /dev/null @@ -1 +0,0 @@ -Fixup `synapse.rest` to pass mypy. diff --git a/synapse/__init__.py b/synapse/__init__.py index 17a6f691c8..1c44ca0999 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -36,7 +36,7 @@ try: except ImportError: pass -__version__ = "1.9.0.dev2" +__version__ = "1.9.0rc1" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From ffa637050d3a3e996747ed0a60d8d1958a0b57e0 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 22 Jan 2020 14:19:23 +0000 Subject: [PATCH 0871/1623] Fixup changelog --- CHANGES.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 7629a7e8ce..102ef5f326 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,7 +6,7 @@ Features - Allow admin to create or modify a user. Contributed by Awesome Technologies Innovationslabor GmbH. ([\#5742](https://github.com/matrix-org/synapse/issues/5742)) - Add new quarantine media admin APIs to quarantine by media ID or by user who uploaded the media. ([\#6681](https://github.com/matrix-org/synapse/issues/6681), [\#6756](https://github.com/matrix-org/synapse/issues/6756)) -- Add org.matrix.e2e_cross_signing to unstable_features in /versions as per [MSC1756](https://github.com/matrix-org/matrix-doc/pull/1756). ([\#6712](https://github.com/matrix-org/synapse/issues/6712)) +- Add `org.matrix.e2e_cross_signing` to `unstable_features` in `/versions` as per [MSC1756](https://github.com/matrix-org/matrix-doc/pull/1756). ([\#6712](https://github.com/matrix-org/synapse/issues/6712)) - Add a new admin API to list and filter rooms on the server. ([\#6720](https://github.com/matrix-org/synapse/issues/6720)) @@ -16,15 +16,16 @@ Bugfixes - Correctly proxy HTTP errors due to API calls to remote group servers. ([\#6654](https://github.com/matrix-org/synapse/issues/6654)) - Fix media repo admin APIs when using a media worker. ([\#6664](https://github.com/matrix-org/synapse/issues/6664)) - Fix "CRITICAL" errors being logged when a request is received for a uri containing non-ascii characters. ([\#6682](https://github.com/matrix-org/synapse/issues/6682)) -- Fix a bug where we would assign a numeric userid if somebody tried registering with an empty username. ([\#6690](https://github.com/matrix-org/synapse/issues/6690)) +- Fix a bug where we would assign a numeric user ID if somebody tried registering with an empty username. ([\#6690](https://github.com/matrix-org/synapse/issues/6690)) - Fix `purge_room` admin API. ([\#6711](https://github.com/matrix-org/synapse/issues/6711)) - Fix a bug causing Synapse to not always purge quiet rooms with a low `max_lifetime` in their message retention policies when running the automated purge jobs. ([\#6714](https://github.com/matrix-org/synapse/issues/6714)) -- Fix a bug causing the `synapse_port_db` script to return 0 in a specific error case. ([\#6718](https://github.com/matrix-org/synapse/issues/6718)) +- Fix the `synapse_port_db` not correctly running background updates. Thanks @tadzik for reporting. ([\#6718](https://github.com/matrix-org/synapse/issues/6718)) - Fix changing password via user admin API. ([\#6730](https://github.com/matrix-org/synapse/issues/6730)) - Fix `/events/:event_id` deprecated API. ([\#6731](https://github.com/matrix-org/synapse/issues/6731)) - Fix monthly active user limiting support for worker mode, fixes [#4639](https://github.com/matrix-org/synapse/issues/4639). ([\#6742](https://github.com/matrix-org/synapse/issues/6742)) - Fix bug when setting `account_validity` to an empty block in the config. Thanks to @Sorunome for reporting. ([\#6747](https://github.com/matrix-org/synapse/issues/6747)) - Fix `AttributeError: 'NoneType' object has no attribute 'get'` in `hash_password` when configuration has an empty `password_config`. Contributed by @ivilata. ([\#6753](https://github.com/matrix-org/synapse/issues/6753)) +- Fix the `docker-compose.yaml` overriding the entire `/etc` folder of the container. Contributed by Fabian Meyer. ([\#6656](https://github.com/matrix-org/synapse/issues/6656)) Improved Documentation @@ -32,8 +33,7 @@ Improved Documentation - Fix a typo in the configuration example for purge jobs in the sample configuration file. ([\#6621](https://github.com/matrix-org/synapse/issues/6621)) - Add complete documentation of the message retention policies support. ([\#6624](https://github.com/matrix-org/synapse/issues/6624), [\#6665](https://github.com/matrix-org/synapse/issues/6665)) -- No more overriding the entire /etc folder of the container in docker-compose.yaml. Contributed by Fabian Meyer. ([\#6656](https://github.com/matrix-org/synapse/issues/6656)) -- Add some helpful tips about changelog entries to the github pull request template. ([\#6663](https://github.com/matrix-org/synapse/issues/6663)) +- Add some helpful tips about changelog entries to the GitHub pull request template. ([\#6663](https://github.com/matrix-org/synapse/issues/6663)) - Clarify the `account_validity` and `email` sections of the sample configuration. ([\#6685](https://github.com/matrix-org/synapse/issues/6685)) - Add more endpoints to the documentation for Synapse workers. ([\#6698](https://github.com/matrix-org/synapse/issues/6698)) @@ -50,20 +50,20 @@ Internal Changes - Add `local_current_membership` table for tracking local user membership state in rooms. ([\#6655](https://github.com/matrix-org/synapse/issues/6655), [\#6728](https://github.com/matrix-org/synapse/issues/6728)) - Port `synapse.replication.tcp` to async/await. ([\#6666](https://github.com/matrix-org/synapse/issues/6666)) - Fixup `synapse.replication` to pass mypy checks. ([\#6667](https://github.com/matrix-org/synapse/issues/6667)) -- Allow additional_resources to implement IResource directly. ([\#6686](https://github.com/matrix-org/synapse/issues/6686)) -- Allow REST endpoint implementations to raise a RedirectException, which will redirect the user's browser to a given location. ([\#6687](https://github.com/matrix-org/synapse/issues/6687)) +- Allow `additional_resources` to implement `IResource` directly. ([\#6686](https://github.com/matrix-org/synapse/issues/6686)) +- Allow REST endpoint implementations to raise a `RedirectException`, which will redirect the user's browser to a given location. ([\#6687](https://github.com/matrix-org/synapse/issues/6687)) - Updates and extensions to the module API. ([\#6688](https://github.com/matrix-org/synapse/issues/6688)) - Updates to the SAML mapping provider API. ([\#6689](https://github.com/matrix-org/synapse/issues/6689), [\#6723](https://github.com/matrix-org/synapse/issues/6723)) -- Remove redundant RegistrationError class. ([\#6691](https://github.com/matrix-org/synapse/issues/6691)) +- Remove redundant `RegistrationError` class. ([\#6691](https://github.com/matrix-org/synapse/issues/6691)) - Don't block processing of incoming EDUs behind processing PDUs in the same transaction. ([\#6697](https://github.com/matrix-org/synapse/issues/6697)) - Remove duplicate check for the `session` query parameter on the `/auth/xxx/fallback/web` Client-Server endpoint. ([\#6702](https://github.com/matrix-org/synapse/issues/6702)) - Attempt to retry sending a transaction when we detect a remote server has come back online, rather than waiting for a transaction to be triggered by new data. ([\#6706](https://github.com/matrix-org/synapse/issues/6706)) -- Add StateMap type alias to simplify types. ([\#6715](https://github.com/matrix-org/synapse/issues/6715)) +- Add `StateMap` type alias to simplify types. ([\#6715](https://github.com/matrix-org/synapse/issues/6715)) - Add a `DeltaState` to track changes to be made to current state during event persistence. ([\#6716](https://github.com/matrix-org/synapse/issues/6716)) - Add more logging around message retention policies support. ([\#6717](https://github.com/matrix-org/synapse/issues/6717)) - When processing a SAML response, log the assertions for easier configuration. ([\#6724](https://github.com/matrix-org/synapse/issues/6724)) - Fixup `synapse.rest` to pass mypy. ([\#6732](https://github.com/matrix-org/synapse/issues/6732), [\#6764](https://github.com/matrix-org/synapse/issues/6764)) -- Fixup synapse.api to pass mypy. ([\#6733](https://github.com/matrix-org/synapse/issues/6733)) +- Fixup `synapse.api` to pass mypy. ([\#6733](https://github.com/matrix-org/synapse/issues/6733)) - Allow streaming cache 'invalidate all' to workers. ([\#6749](https://github.com/matrix-org/synapse/issues/6749)) - Remove unused CI docker compose files. ([\#6754](https://github.com/matrix-org/synapse/issues/6754)) From 91085ef49e8553cede573d1a6e52f0b97bdb1bf7 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 22 Jan 2020 14:30:22 +0000 Subject: [PATCH 0872/1623] Add deprecation headers --- CHANGES.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 102ef5f326..f446bdf6f1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,10 @@ Synapse 1.9.0rc1 (2020-01-22) ============================= +**WARNING**: As of this release, Synapse no longer supports versions of SQLite before 3.11, and will refuse to start when configured to use an older version. Administrators are recommended to migrate their database to Postgres (see instructions [here](docs/postgres.md)). + +If your Synapse deployment uses workers, note that the reverse-proxy configurations have changed, with the addition of a few paths (see the updated configurations [here](docs/workers.md#available-worker-applications)). + Features -------- From 33f7e5ce2a449bda2f3c8dfddef9bf0f71bff593 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 22 Jan 2020 14:49:21 +0000 Subject: [PATCH 0873/1623] Fixup warning about workers changes --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index f446bdf6f1..0392acbde4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,7 +3,7 @@ Synapse 1.9.0rc1 (2020-01-22) **WARNING**: As of this release, Synapse no longer supports versions of SQLite before 3.11, and will refuse to start when configured to use an older version. Administrators are recommended to migrate their database to Postgres (see instructions [here](docs/postgres.md)). -If your Synapse deployment uses workers, note that the reverse-proxy configurations have changed, with the addition of a few paths (see the updated configurations [here](docs/workers.md#available-worker-applications)). +If your Synapse deployment uses workers, note that the reverse-proxy configurations for the `synapse.app.media_repository`, `synapse.app.federation_reader` and `synapse.app.event_creator` have changed, with the addition of a few paths (see the updated configurations [here](docs/workers.md#available-worker-applications)). Features -------- From ce84dd9e207d9ae88e4cf9ca8a9731fcac043969 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Wed, 22 Jan 2020 15:09:57 +0000 Subject: [PATCH 0874/1623] Remove unnecessary abstractions in admin handler (#6751) --- changelog.d/6751.misc | 1 + synapse/handlers/admin.py | 62 ------------------- synapse/rest/admin/users.py | 19 +++--- .../storage/data_stores/main/registration.py | 2 +- 4 files changed, 11 insertions(+), 73 deletions(-) create mode 100644 changelog.d/6751.misc diff --git a/changelog.d/6751.misc b/changelog.d/6751.misc new file mode 100644 index 0000000000..7222520528 --- /dev/null +++ b/changelog.d/6751.misc @@ -0,0 +1 @@ +Remove some unnecessary admin handler abstraction methods. \ No newline at end of file diff --git a/synapse/handlers/admin.py b/synapse/handlers/admin.py index 60a7c938bc..9205865231 100644 --- a/synapse/handlers/admin.py +++ b/synapse/handlers/admin.py @@ -62,68 +62,6 @@ class AdminHandler(BaseHandler): ret["avatar_url"] = profile.avatar_url return ret - async def get_users(self): - """Function to retrieve a list of users in users table. - - Args: - Returns: - defer.Deferred: resolves to list[dict[str, Any]] - """ - ret = await self.store.get_users() - - return ret - - async def get_users_paginate(self, start, limit, name, guests, deactivated): - """Function to retrieve a paginated list of users from - users list. This will return a json list of users. - - Args: - start (int): start number to begin the query from - limit (int): number of rows to retrieve - name (string): filter for user names - guests (bool): whether to in include guest users - deactivated (bool): whether to include deactivated users - Returns: - defer.Deferred: resolves to json list[dict[str, Any]] - """ - ret = await self.store.get_users_paginate( - start, limit, name, guests, deactivated - ) - - return ret - - async def search_users(self, term): - """Function to search users list for one or more users with - the matched term. - - Args: - term (str): search term - Returns: - defer.Deferred: resolves to list[dict[str, Any]] - """ - ret = await self.store.search_users(term) - - return ret - - def get_user_server_admin(self, user): - """ - Get the admin bit on a user. - - Args: - user_id (UserID): the (necessarily local) user to manipulate - """ - return self.store.is_server_admin(user) - - def set_user_server_admin(self, user, admin): - """ - Set the admin bit on a user. - - Args: - user_id (UserID): the (necessarily local) user to manipulate - admin (bool): whether or not the user should be an admin of this server - """ - return self.store.set_server_admin(user, admin) - async def export_user_data(self, user_id, writer): """Write all data we have on the user to the given writer. diff --git a/synapse/rest/admin/users.py b/synapse/rest/admin/users.py index 52d27fa3e3..927e9ca9ee 100644 --- a/synapse/rest/admin/users.py +++ b/synapse/rest/admin/users.py @@ -45,6 +45,7 @@ class UsersRestServlet(RestServlet): def __init__(self, hs): self.hs = hs + self.store = hs.get_datastore() self.auth = hs.get_auth() self.admin_handler = hs.get_handlers().admin_handler @@ -55,7 +56,7 @@ class UsersRestServlet(RestServlet): if not self.hs.is_mine(target_user): raise SynapseError(400, "Can only users a local user") - ret = await self.admin_handler.get_users() + ret = await self.store.get_users() return 200, ret @@ -80,6 +81,7 @@ class UsersRestServletV2(RestServlet): def __init__(self, hs): self.hs = hs + self.store = hs.get_datastore() self.auth = hs.get_auth() self.admin_handler = hs.get_handlers().admin_handler @@ -92,7 +94,7 @@ class UsersRestServletV2(RestServlet): guests = parse_boolean(request, "guests", default=True) deactivated = parse_boolean(request, "deactivated", default=False) - users = await self.admin_handler.get_users_paginate( + users = await self.store.get_users_paginate( start, limit, user_id, guests, deactivated ) ret = {"users": users} @@ -516,8 +518,8 @@ class SearchUsersRestServlet(RestServlet): PATTERNS = historical_admin_path_patterns("/search_users/(?P[^/]*)") def __init__(self, hs): - self.store = hs.get_datastore() self.hs = hs + self.store = hs.get_datastore() self.auth = hs.get_auth() self.handlers = hs.get_handlers() @@ -540,7 +542,7 @@ class SearchUsersRestServlet(RestServlet): term = parse_string(request, "term", required=True) logger.info("term: %s ", term) - ret = await self.handlers.admin_handler.search_users(term) + ret = await self.handlers.store.search_users(term) return 200, ret @@ -574,8 +576,8 @@ class UserAdminServlet(RestServlet): def __init__(self, hs): self.hs = hs + self.store = hs.get_datastore() self.auth = hs.get_auth() - self.handlers = hs.get_handlers() async def on_GET(self, request, user_id): await assert_requester_is_admin(self.auth, request) @@ -585,8 +587,7 @@ class UserAdminServlet(RestServlet): if not self.hs.is_mine(target_user): raise SynapseError(400, "Only local users can be admins of this homeserver") - is_admin = await self.handlers.admin_handler.get_user_server_admin(target_user) - is_admin = bool(is_admin) + is_admin = await self.store.is_server_admin(target_user) return 200, {"admin": is_admin} @@ -609,8 +610,6 @@ class UserAdminServlet(RestServlet): if target_user == auth_user and not set_admin_to: raise SynapseError(400, "You may not demote yourself.") - await self.handlers.admin_handler.set_user_server_admin( - target_user, set_admin_to - ) + await self.store.set_user_server_admin(target_user, set_admin_to) return 200, {} diff --git a/synapse/storage/data_stores/main/registration.py b/synapse/storage/data_stores/main/registration.py index cb4b2b39a0..49306642ed 100644 --- a/synapse/storage/data_stores/main/registration.py +++ b/synapse/storage/data_stores/main/registration.py @@ -291,7 +291,7 @@ class RegistrationWorkerStore(SQLBaseStore): desc="is_server_admin", ) - return res if res else False + return bool(res) if res else False def set_server_admin(self, user, admin): """Sets whether a user is an admin of this homeserver. From d31f5f4d89694a6e41b1c9af09ed6405ecb07376 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Thu, 23 Jan 2020 11:37:26 +0000 Subject: [PATCH 0875/1623] Update admin room docs with correct endpoints (#6770) --- changelog.d/6770.doc | 1 + docs/admin_api/rooms.md | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 changelog.d/6770.doc diff --git a/changelog.d/6770.doc b/changelog.d/6770.doc new file mode 100644 index 0000000000..a251b82238 --- /dev/null +++ b/changelog.d/6770.doc @@ -0,0 +1 @@ +Fix endpoint documentation for the List Rooms admin api. \ No newline at end of file diff --git a/docs/admin_api/rooms.md b/docs/admin_api/rooms.md index 082721ea95..2db457c1b6 100644 --- a/docs/admin_api/rooms.md +++ b/docs/admin_api/rooms.md @@ -46,7 +46,7 @@ The following fields are possible in the JSON response body: A standard request with no filtering: ``` -GET /_synapse/admin/rooms +GET /_synapse/admin/v1/rooms {} ``` @@ -78,7 +78,7 @@ Response: Filtering by room name: ``` -GET /_synapse/admin/rooms?search_term=TWIM +GET /_synapse/admin/v1/rooms?search_term=TWIM {} ``` @@ -103,7 +103,7 @@ Response: Paginating through a list of rooms: ``` -GET /_synapse/admin/rooms?order_by=size +GET /_synapse/admin/v1/rooms?order_by=size {} ``` @@ -139,7 +139,7 @@ To get the next batch of room results, we repeat our request, setting the `from` parameter to the value of `next_token`. ``` -GET /_synapse/admin/rooms?order_by=size&from=100 +GET /_synapse/admin/v1/rooms?order_by=size&from=100 {} ``` From 5bd3cb7260984164c4c54eb2add1fa7821795360 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 23 Jan 2020 12:03:58 +0000 Subject: [PATCH 0876/1623] Minor fixes to user admin api (#6761) * don't insist on a password (this is valid if you have an SSO login) * fix reference to undefined `requester` --- changelog.d/6761.bugfix | 1 + synapse/rest/admin/users.py | 14 +++++--------- 2 files changed, 6 insertions(+), 9 deletions(-) create mode 100644 changelog.d/6761.bugfix diff --git a/changelog.d/6761.bugfix b/changelog.d/6761.bugfix new file mode 100644 index 0000000000..1c664c02df --- /dev/null +++ b/changelog.d/6761.bugfix @@ -0,0 +1 @@ +Minor fixes to `PUT /_synapse/admin/v2/users` admin api. diff --git a/synapse/rest/admin/users.py b/synapse/rest/admin/users.py index 927e9ca9ee..3455741195 100644 --- a/synapse/rest/admin/users.py +++ b/synapse/rest/admin/users.py @@ -153,7 +153,8 @@ class UserRestServletV2(RestServlet): return 200, ret async def on_PUT(self, request, user_id): - await assert_requester_is_admin(self.auth, request) + requester = await self.auth.get_user_by_req(request) + await assert_user_is_admin(self.auth, requester.user) target_user = UserID.from_string(user_id) body = parse_json_object_from_request(request) @@ -164,8 +165,6 @@ class UserRestServletV2(RestServlet): user = await self.admin_handler.get_user(target_user) if user: # modify user - requester = await self.auth.get_user_by_req(request) - if "displayname" in body: await self.profile_handler.set_displayname( target_user, requester, body["displayname"], True @@ -212,11 +211,8 @@ class UserRestServletV2(RestServlet): return 200, user else: # create user - if "password" not in body: - raise SynapseError( - 400, "password must be specified", errcode=Codes.BAD_JSON - ) - elif ( + password = body.get("password") + if password is not None and ( not isinstance(body["password"], text_type) or len(body["password"]) > 512 ): @@ -231,7 +227,7 @@ class UserRestServletV2(RestServlet): user_id = await self.registration_handler.register_user( localpart=target_user.localpart, - password=body["password"], + password=password, admin=bool(admin), default_display_name=displayname, user_type=user_type, From 6b7462a13fff8f4e1dbad0bb4d81bbe0515af7c1 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 23 Jan 2020 12:11:44 +0000 Subject: [PATCH 0877/1623] a bit of debugging for media storage providers (#6757) * a bit of debugging for media storage providers * changelog --- changelog.d/6757.misc | 1 + synapse/rest/media/v1/media_storage.py | 1 + synapse/rest/media/v1/storage_provider.py | 6 ++++++ 3 files changed, 8 insertions(+) create mode 100644 changelog.d/6757.misc diff --git a/changelog.d/6757.misc b/changelog.d/6757.misc new file mode 100644 index 0000000000..a50c5e974a --- /dev/null +++ b/changelog.d/6757.misc @@ -0,0 +1 @@ +Add some debugging for media storage providers. diff --git a/synapse/rest/media/v1/media_storage.py b/synapse/rest/media/v1/media_storage.py index 3b87717a5a..683a79c966 100644 --- a/synapse/rest/media/v1/media_storage.py +++ b/synapse/rest/media/v1/media_storage.py @@ -148,6 +148,7 @@ class MediaStorage(object): for provider in self.storage_providers: res = yield provider.fetch(path, file_info) if res: + logger.debug("Streaming %s from %s", path, provider) return res return None diff --git a/synapse/rest/media/v1/storage_provider.py b/synapse/rest/media/v1/storage_provider.py index 37687ea7f4..858680be26 100644 --- a/synapse/rest/media/v1/storage_provider.py +++ b/synapse/rest/media/v1/storage_provider.py @@ -77,6 +77,9 @@ class StorageProviderWrapper(StorageProvider): self.store_synchronous = store_synchronous self.store_remote = store_remote + def __str__(self): + return "StorageProviderWrapper[%s]" % (self.backend,) + def store_file(self, path, file_info): if not file_info.server_name and not self.store_local: return defer.succeed(None) @@ -114,6 +117,9 @@ class FileStorageProviderBackend(StorageProvider): self.cache_directory = hs.config.media_store_path self.base_directory = config + def __str__(self): + return "FileStorageProviderBackend[%s]" % (self.base_directory,) + def store_file(self, path, file_info): """See StorageProvider.store_file""" From f3eac2b3e94539b5b01ce1f5580f11e8ba205385 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 23 Jan 2020 12:57:55 +0000 Subject: [PATCH 0878/1623] 1.9.0 --- CHANGES.md | 6 ++++++ debian/changelog | 6 ++++++ synapse/__init__.py | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 0392acbde4..a582f0fb58 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,9 @@ +Synapse 1.9.0 (2020-01-23) +========================== + +No significant changes. + + Synapse 1.9.0rc1 (2020-01-22) ============================= diff --git a/debian/changelog b/debian/changelog index 7413c238e6..49f3187691 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.9.0) stable; urgency=medium + + * New synapse release 1.9.0. + + -- Synapse Packaging team Thu, 23 Jan 2020 12:56:31 +0000 + matrix-synapse-py3 (1.8.0) stable; urgency=medium [ Richard van der Hoff ] diff --git a/synapse/__init__.py b/synapse/__init__.py index 1c44ca0999..6236e13aa3 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -36,7 +36,7 @@ try: except ImportError: pass -__version__ = "1.9.0rc1" +__version__ = "1.9.0" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 1dc5a791cf0aad8d672ee4e2e84b544c6be94431 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 23 Jan 2020 12:59:29 +0000 Subject: [PATCH 0879/1623] Fixup changelog --- CHANGES.md | 5 ++++- changelog.d/6770.doc | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) delete mode 100644 changelog.d/6770.doc diff --git a/CHANGES.md b/CHANGES.md index a582f0fb58..9a53e1872d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,7 +1,10 @@ Synapse 1.9.0 (2020-01-23) ========================== -No significant changes. +Improved Documentation +---------------------- + +- Fix endpoint documentation for the List Rooms admin api. ([\#6770](https://github.com/matrix-org/synapse/issues/6770)) Synapse 1.9.0rc1 (2020-01-22) diff --git a/changelog.d/6770.doc b/changelog.d/6770.doc deleted file mode 100644 index a251b82238..0000000000 --- a/changelog.d/6770.doc +++ /dev/null @@ -1 +0,0 @@ -Fix endpoint documentation for the List Rooms admin api. \ No newline at end of file From 1755326d8a7a5c79261417675e38f5a8ccf39081 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 23 Jan 2020 13:11:07 +0000 Subject: [PATCH 0880/1623] Fixup changelog --- CHANGES.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 9a53e1872d..056726ce91 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,11 @@ Synapse 1.9.0 (2020-01-23) ========================== +**WARNING**: As of this release, Synapse no longer supports versions of SQLite before 3.11, and will refuse to start when configured to use an older version. Administrators are recommended to migrate their database to Postgres (see instructions [here](docs/postgres.md)). + +If your Synapse deployment uses workers, note that the reverse-proxy configurations for the `synapse.app.media_repository`, `synapse.app.federation_reader` and `synapse.app.event_creator` workers have changed, with the addition of a few paths (see the updated configurations [here](docs/workers.md#available-worker-applications)). Existing configurations will continue to work. + + Improved Documentation ---------------------- @@ -10,10 +15,6 @@ Improved Documentation Synapse 1.9.0rc1 (2020-01-22) ============================= -**WARNING**: As of this release, Synapse no longer supports versions of SQLite before 3.11, and will refuse to start when configured to use an older version. Administrators are recommended to migrate their database to Postgres (see instructions [here](docs/postgres.md)). - -If your Synapse deployment uses workers, note that the reverse-proxy configurations for the `synapse.app.media_repository`, `synapse.app.federation_reader` and `synapse.app.event_creator` have changed, with the addition of a few paths (see the updated configurations [here](docs/workers.md#available-worker-applications)). - Features -------- From 9bae740527c4621f9f8eb8ca936669f2372c42eb Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 23 Jan 2020 13:13:19 +0000 Subject: [PATCH 0881/1623] Fixup changelog --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 056726ce91..8a068ae8c9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,7 +9,7 @@ If your Synapse deployment uses workers, note that the reverse-proxy configurati Improved Documentation ---------------------- -- Fix endpoint documentation for the List Rooms admin api. ([\#6770](https://github.com/matrix-org/synapse/issues/6770)) +- Fix endpoint documentation for the List Rooms admin API. ([\#6770](https://github.com/matrix-org/synapse/issues/6770)) Synapse 1.9.0rc1 (2020-01-22) From fa4d609e20318821e2ffbeb35bfddbc86be81be0 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 23 Jan 2020 15:19:03 +0000 Subject: [PATCH 0882/1623] Make 'event.redacts' never raise. (#6771) There are quite a few places that we assume that a redaction event has a corresponding `redacts` key, which is not always the case. So lets cheekily make it so that event.redacts just returns None instead. --- changelog.d/6771.bugfix | 1 + synapse/events/__init__.py | 28 ++++++++++++--- synapse/storage/data_stores/main/events.py | 2 +- .../storage/data_stores/main/events_worker.py | 2 +- tests/storage/test_redaction.py | 35 +++++++++++++++++++ 5 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 changelog.d/6771.bugfix diff --git a/changelog.d/6771.bugfix b/changelog.d/6771.bugfix new file mode 100644 index 0000000000..623ba24acb --- /dev/null +++ b/changelog.d/6771.bugfix @@ -0,0 +1 @@ +Fix persisting redaction events that have been redacted (or otherwise don't have a redacts key). diff --git a/synapse/events/__init__.py b/synapse/events/__init__.py index 88ed6d764f..72c09327f4 100644 --- a/synapse/events/__init__.py +++ b/synapse/events/__init__.py @@ -116,16 +116,32 @@ class _EventInternalMetadata(object): return getattr(self, "redacted", False) -def _event_dict_property(key): +_SENTINEL = object() + + +def _event_dict_property(key, default=_SENTINEL): + """Creates a new property for the given key that delegates access to + `self._event_dict`. + + The default is used if the key is missing from the `_event_dict`, if given, + otherwise an AttributeError will be raised. + + Note: If a default is given then `hasattr` will always return true. + """ + # We want to be able to use hasattr with the event dict properties. # However, (on python3) hasattr expects AttributeError to be raised. Hence, # we need to transform the KeyError into an AttributeError - def getter(self): + + def getter_raises(self): try: return self._event_dict[key] except KeyError: raise AttributeError(key) + def getter_default(self): + return self._event_dict.get(key, default) + def setter(self, v): try: self._event_dict[key] = v @@ -138,7 +154,11 @@ def _event_dict_property(key): except KeyError: raise AttributeError(key) - return property(getter, setter, delete) + if default is _SENTINEL: + # No default given, so use the getter that raises + return property(getter_raises, setter, delete) + else: + return property(getter_default, setter, delete) class EventBase(object): @@ -165,7 +185,7 @@ class EventBase(object): origin = _event_dict_property("origin") origin_server_ts = _event_dict_property("origin_server_ts") prev_events = _event_dict_property("prev_events") - redacts = _event_dict_property("redacts") + redacts = _event_dict_property("redacts", None) room_id = _event_dict_property("room_id") sender = _event_dict_property("sender") user_id = _event_dict_property("sender") diff --git a/synapse/storage/data_stores/main/events.py b/synapse/storage/data_stores/main/events.py index 596daf8909..ce553566a5 100644 --- a/synapse/storage/data_stores/main/events.py +++ b/synapse/storage/data_stores/main/events.py @@ -951,7 +951,7 @@ class EventsStore( elif event.type == EventTypes.Message: # Insert into the event_search table. self._store_room_message_txn(txn, event) - elif event.type == EventTypes.Redaction: + elif event.type == EventTypes.Redaction and event.redacts is not None: # Insert into the redactions table. self._store_redaction(txn, event) elif event.type == EventTypes.Retention: diff --git a/synapse/storage/data_stores/main/events_worker.py b/synapse/storage/data_stores/main/events_worker.py index 3b93e0597a..7251e819f5 100644 --- a/synapse/storage/data_stores/main/events_worker.py +++ b/synapse/storage/data_stores/main/events_worker.py @@ -287,7 +287,7 @@ class EventsWorkerStore(SQLBaseStore): # we have to recheck auth now. if not allow_rejected and entry.event.type == EventTypes.Redaction: - if not hasattr(entry.event, "redacts"): + if entry.event.redacts is None: # A redacted redaction doesn't have a `redacts` key, in # which case lets just withhold the event. # diff --git a/tests/storage/test_redaction.py b/tests/storage/test_redaction.py index dc45173355..feb1c07cb2 100644 --- a/tests/storage/test_redaction.py +++ b/tests/storage/test_redaction.py @@ -398,3 +398,38 @@ class RedactionTestCase(unittest.HomeserverTestCase): self.get_success( self.store.get_event(first_redact_event.event_id, allow_none=True) ) + + def test_store_redacted_redaction(self): + """Tests that we can store a redacted redaction. + """ + + self.get_success( + self.inject_room_member(self.room1, self.u_alice, Membership.JOIN) + ) + + builder = self.event_builder_factory.for_room_version( + RoomVersions.V1, + { + "type": EventTypes.Redaction, + "sender": self.u_alice.to_string(), + "room_id": self.room1.to_string(), + "content": {"reason": "foo"}, + }, + ) + + redaction_event, context = self.get_success( + self.event_creation_handler.create_new_client_event(builder) + ) + + self.get_success( + self.storage.persistence.persist_event(redaction_event, context) + ) + + # Now lets jump to the future where we have censored the redaction event + # in the DB. + self.reactor.advance(60 * 60 * 24 * 31) + + # We just want to check that fetching the event doesn't raise an exception. + self.get_success( + self.store.get_event(redaction_event.event_id, allow_none=True) + ) From aa6ad288f16ed263b2a94b480259639d52ff6cad Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Fri, 24 Jan 2020 11:01:57 +0200 Subject: [PATCH 0883/1623] Clarifications to the workers documentation * Add note that user_dir requires disabling user dir updates from the main synapse process. * Add note that federation_reader should have the federation listener resource. Signed-off-by: Jason Robinson --- changelog.d/6775.doc | 1 + docs/workers.md | 7 +++++++ 2 files changed, 8 insertions(+) create mode 100644 changelog.d/6775.doc diff --git a/changelog.d/6775.doc b/changelog.d/6775.doc new file mode 100644 index 0000000000..9421250f8b --- /dev/null +++ b/changelog.d/6775.doc @@ -0,0 +1 @@ +Clarify documentation related to user_dir and federation_reader workers. diff --git a/docs/workers.md b/docs/workers.md index 0ab269fd96..a5d6d18f23 100644 --- a/docs/workers.md +++ b/docs/workers.md @@ -185,6 +185,9 @@ reverse-proxy configuration. The `^/_matrix/federation/v1/send/` endpoint must only be handled by a single instance. +Note that the `worker_listeners.resources.name` needs to be set to `federation` +for this worker. + ### `synapse.app.federation_sender` Handles sending federation traffic to other servers. Doesn't handle any @@ -265,6 +268,10 @@ the following regular expressions: ^/_matrix/client/(api/v1|r0|unstable)/user_directory/search$ +When using this worker you must also set `update_user_directory: False` in the +shared configuration file to stop the main synapse running background +jobs related to updating the user directory. + ### `synapse.app.frontend_proxy` Proxies some frequently-requested client endpoints to add caching and remove From 9f7aaf90b5ef76416852f35201a851d45eccc0a1 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Fri, 24 Jan 2020 14:28:40 +0000 Subject: [PATCH 0884/1623] Validate client_secret parameter (#6767) --- changelog.d/6767.bugfix | 1 + synapse/handlers/identity.py | 4 +- synapse/rest/client/v2_alpha/account.py | 23 ++++++++--- synapse/rest/client/v2_alpha/register.py | 3 ++ synapse/util/stringutils.py | 17 ++++++++ tests/util/test_stringutils.py | 51 ++++++++++++++++++++++++ 6 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 changelog.d/6767.bugfix create mode 100644 tests/util/test_stringutils.py diff --git a/changelog.d/6767.bugfix b/changelog.d/6767.bugfix new file mode 100644 index 0000000000..63c7c63315 --- /dev/null +++ b/changelog.d/6767.bugfix @@ -0,0 +1 @@ +Validate `client_secret` parameter using the regex provided by the Client-Server API, temporarily allowing `:` characters for older clients. The `:` character will be removed in a future release. diff --git a/synapse/handlers/identity.py b/synapse/handlers/identity.py index 000fbf090f..23f07832e7 100644 --- a/synapse/handlers/identity.py +++ b/synapse/handlers/identity.py @@ -38,7 +38,7 @@ from synapse.api.errors import ( from synapse.config.emailconfig import ThreepidBehaviour from synapse.http.client import SimpleHttpClient from synapse.util.hash import sha256_and_url_safe_base64 -from synapse.util.stringutils import random_string +from synapse.util.stringutils import assert_valid_client_secret, random_string from ._base import BaseHandler @@ -84,6 +84,8 @@ class IdentityHandler(BaseHandler): raise SynapseError( 400, "Missing param client_secret in creds", errcode=Codes.MISSING_PARAM ) + assert_valid_client_secret(client_secret) + session_id = creds.get("sid") if not session_id: raise SynapseError( diff --git a/synapse/rest/client/v2_alpha/account.py b/synapse/rest/client/v2_alpha/account.py index fc240f5cf8..dc837d6c75 100644 --- a/synapse/rest/client/v2_alpha/account.py +++ b/synapse/rest/client/v2_alpha/account.py @@ -30,6 +30,7 @@ from synapse.http.servlet import ( ) from synapse.push.mailer import Mailer, load_jinja2_templates from synapse.util.msisdn import phone_number_to_msisdn +from synapse.util.stringutils import assert_valid_client_secret from synapse.util.threepids import check_3pid_allowed from ._base import client_patterns, interactive_auth_handler @@ -81,6 +82,8 @@ class EmailPasswordRequestTokenRestServlet(RestServlet): # Extract params from body client_secret = body["client_secret"] + assert_valid_client_secret(client_secret) + email = body["email"] send_attempt = body["send_attempt"] next_link = body.get("next_link") # Optional param @@ -166,8 +169,9 @@ class PasswordResetSubmitTokenServlet(RestServlet): ) sid = parse_string(request, "sid", required=True) - client_secret = parse_string(request, "client_secret", required=True) token = parse_string(request, "token", required=True) + client_secret = parse_string(request, "client_secret", required=True) + assert_valid_client_secret(client_secret) # Attempt to validate a 3PID session try: @@ -353,6 +357,8 @@ class EmailThreepidRequestTokenRestServlet(RestServlet): body = parse_json_object_from_request(request) assert_params_in_dict(body, ["client_secret", "email", "send_attempt"]) client_secret = body["client_secret"] + assert_valid_client_secret(client_secret) + email = body["email"] send_attempt = body["send_attempt"] next_link = body.get("next_link") # Optional param @@ -413,6 +419,8 @@ class MsisdnThreepidRequestTokenRestServlet(RestServlet): body, ["client_secret", "country", "phone_number", "send_attempt"] ) client_secret = body["client_secret"] + assert_valid_client_secret(client_secret) + country = body["country"] phone_number = body["phone_number"] send_attempt = body["send_attempt"] @@ -493,8 +501,9 @@ class AddThreepidEmailSubmitTokenServlet(RestServlet): ) sid = parse_string(request, "sid", required=True) - client_secret = parse_string(request, "client_secret", required=True) token = parse_string(request, "token", required=True) + client_secret = parse_string(request, "client_secret", required=True) + assert_valid_client_secret(client_secret) # Attempt to validate a 3PID session try: @@ -559,6 +568,7 @@ class AddThreepidMsisdnSubmitTokenServlet(RestServlet): body = parse_json_object_from_request(request) assert_params_in_dict(body, ["client_secret", "sid", "token"]) + assert_valid_client_secret(body["client_secret"]) # Proxy submit_token request to msisdn threepid delegate response = await self.identity_handler.proxy_msisdn_submit_token( @@ -600,8 +610,9 @@ class ThreepidRestServlet(RestServlet): ) assert_params_in_dict(threepid_creds, ["client_secret", "sid"]) - client_secret = threepid_creds["client_secret"] sid = threepid_creds["sid"] + client_secret = threepid_creds["client_secret"] + assert_valid_client_secret(client_secret) validation_session = await self.identity_handler.validate_threepid_session( client_secret, sid @@ -637,8 +648,9 @@ class ThreepidAddRestServlet(RestServlet): body = parse_json_object_from_request(request) assert_params_in_dict(body, ["client_secret", "sid"]) - client_secret = body["client_secret"] sid = body["sid"] + client_secret = body["client_secret"] + assert_valid_client_secret(client_secret) await self.auth_handler.validate_user_via_ui_auth( requester, body, self.hs.get_ip_from_request(request) @@ -676,8 +688,9 @@ class ThreepidBindRestServlet(RestServlet): assert_params_in_dict(body, ["id_server", "sid", "client_secret"]) id_server = body["id_server"] sid = body["sid"] - client_secret = body["client_secret"] id_access_token = body.get("id_access_token") # optional + client_secret = body["client_secret"] + assert_valid_client_secret(client_secret) requester = await self.auth.get_user_by_req(request) user_id = requester.user.to_string() diff --git a/synapse/rest/client/v2_alpha/register.py b/synapse/rest/client/v2_alpha/register.py index 1bda9aec7e..a09189b1b4 100644 --- a/synapse/rest/client/v2_alpha/register.py +++ b/synapse/rest/client/v2_alpha/register.py @@ -49,6 +49,7 @@ from synapse.http.servlet import ( from synapse.push.mailer import load_jinja2_templates from synapse.util.msisdn import phone_number_to_msisdn from synapse.util.ratelimitutils import FederationRateLimiter +from synapse.util.stringutils import assert_valid_client_secret from synapse.util.threepids import check_3pid_allowed from ._base import client_patterns, interactive_auth_handler @@ -116,6 +117,8 @@ class EmailRegisterRequestTokenRestServlet(RestServlet): # Extract params from body client_secret = body["client_secret"] + assert_valid_client_secret(client_secret) + email = body["email"] send_attempt = body["send_attempt"] next_link = body.get("next_link") # Optional param diff --git a/synapse/util/stringutils.py b/synapse/util/stringutils.py index 982c6d81ca..2c0dcb5208 100644 --- a/synapse/util/stringutils.py +++ b/synapse/util/stringutils.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd +# Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,14 +15,22 @@ # limitations under the License. import random +import re import string import six from six import PY2, PY3 from six.moves import range +from synapse.api.errors import Codes, SynapseError + _string_with_symbols = string.digits + string.ascii_letters + ".,;:^&*-_+=#~@" +# https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-register-email-requesttoken +# Note: The : character is allowed here for older clients, but will be removed in a +# future release. Context: https://github.com/matrix-org/synapse/issues/6766 +client_secret_regex = re.compile(r"^[0-9a-zA-Z\.\=\_\-\:]+$") + # random_string and random_string_with_symbols are used for a range of things, # some cryptographically important, some less so. We use SystemRandom to make sure # we get cryptographically-secure randoms. @@ -109,3 +118,11 @@ def exception_to_unicode(e): return msg.decode("utf-8", errors="replace") else: return msg + + +def assert_valid_client_secret(client_secret): + """Validate that a given string matches the client_secret regex defined by the spec""" + if client_secret_regex.match(client_secret) is None: + raise SynapseError( + 400, "Invalid client_secret parameter", errcode=Codes.INVALID_PARAM + ) diff --git a/tests/util/test_stringutils.py b/tests/util/test_stringutils.py new file mode 100644 index 0000000000..4f4da29a98 --- /dev/null +++ b/tests/util/test_stringutils.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from synapse.api.errors import SynapseError +from synapse.util.stringutils import assert_valid_client_secret + +from .. import unittest + + +class StringUtilsTestCase(unittest.TestCase): + def test_client_secret_regex(self): + """Ensure that client_secret does not contain illegal characters""" + good = [ + "abcde12345", + "ABCabc123", + "_--something==_", + "...--==-18913", + "8Dj2odd-e9asd.cd==_--ddas-secret-", + # We temporarily allow : characters: https://github.com/matrix-org/synapse/issues/6766 + # To be removed in a future release + "SECRET:1234567890", + ] + + bad = [ + "--+-/secret", + "\\dx--dsa288", + "", + "AAS//", + "asdj**", + ">X> Date: Mon, 27 Jan 2020 10:20:48 +0200 Subject: [PATCH 0885/1623] Fix federation_reader listeners doc as per PR review Signed-off-by: Jason Robinson --- docs/workers.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/workers.md b/docs/workers.md index a5d6d18f23..09a9d8a7b8 100644 --- a/docs/workers.md +++ b/docs/workers.md @@ -185,8 +185,18 @@ reverse-proxy configuration. The `^/_matrix/federation/v1/send/` endpoint must only be handled by a single instance. -Note that the `worker_listeners.resources.name` needs to be set to `federation` -for this worker. +Note that `federation` must be added to the listener resources in the worker config: + +```yaml +worker_app: synapse.app.federation_reader +... +worker_listeners: + - type: http + port: + resources: + - names: + - federation +``` ### `synapse.app.federation_sender` From cf9d56e5cfa93dbd5f52df89e1fdb088755d811a Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Mon, 27 Jan 2020 14:09:59 +0200 Subject: [PATCH 0886/1623] Formatting of changelog Co-Authored-By: Brendan Abolivier --- changelog.d/6775.doc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/6775.doc b/changelog.d/6775.doc index 9421250f8b..c6078ef82d 100644 --- a/changelog.d/6775.doc +++ b/changelog.d/6775.doc @@ -1 +1 @@ -Clarify documentation related to user_dir and federation_reader workers. +Clarify documentation related to `user_dir` and `federation_reader` workers. From d5275fc55f4edc42d1543825da2c13df63d96927 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 27 Jan 2020 13:47:50 +0000 Subject: [PATCH 0887/1623] Propagate cache invalidates from workers to other workers. (#6748) Currently if a worker invalidates a cache it will be streamed to master, which then didn't forward those to other workers. --- changelog.d/6748.misc | 1 + synapse/replication/tcp/protocol.py | 2 +- synapse/replication/tcp/resource.py | 9 ++++++--- synapse/storage/data_stores/main/cache.py | 22 +++++++++++++++++++++- 4 files changed, 29 insertions(+), 5 deletions(-) create mode 100644 changelog.d/6748.misc diff --git a/changelog.d/6748.misc b/changelog.d/6748.misc new file mode 100644 index 0000000000..de320d4cd9 --- /dev/null +++ b/changelog.d/6748.misc @@ -0,0 +1 @@ +Propagate cache invalidates from workers to other workers. diff --git a/synapse/replication/tcp/protocol.py b/synapse/replication/tcp/protocol.py index 131e5acb09..bc1482a9bb 100644 --- a/synapse/replication/tcp/protocol.py +++ b/synapse/replication/tcp/protocol.py @@ -459,7 +459,7 @@ class ServerReplicationStreamProtocol(BaseReplicationStreamProtocol): await self.streamer.on_remove_pusher(cmd.app_id, cmd.push_key, cmd.user_id) async def on_INVALIDATE_CACHE(self, cmd): - self.streamer.on_invalidate_cache(cmd.cache_func, cmd.keys) + await self.streamer.on_invalidate_cache(cmd.cache_func, cmd.keys) async def on_REMOTE_SERVER_UP(self, cmd: RemoteServerUpCommand): self.streamer.on_remote_server_up(cmd.data) diff --git a/synapse/replication/tcp/resource.py b/synapse/replication/tcp/resource.py index 6ebf944f66..ce60ae2e07 100644 --- a/synapse/replication/tcp/resource.py +++ b/synapse/replication/tcp/resource.py @@ -17,7 +17,7 @@ import logging import random -from typing import List +from typing import Any, List from six import itervalues @@ -271,11 +271,14 @@ class ReplicationStreamer(object): self.notifier.on_new_replication_data() @measure_func("repl.on_invalidate_cache") - def on_invalidate_cache(self, cache_func, keys): + async def on_invalidate_cache(self, cache_func: str, keys: List[Any]): """The client has asked us to invalidate a cache """ invalidate_cache_counter.inc() - getattr(self.store, cache_func).invalidate(tuple(keys)) + + # We invalidate the cache locally, but then also stream that to other + # workers. + await self.store.invalidate_cache_and_stream(cache_func, tuple(keys)) @measure_func("repl.on_user_ip") async def on_user_ip( diff --git a/synapse/storage/data_stores/main/cache.py b/synapse/storage/data_stores/main/cache.py index afa2b41c98..d4c44dcc75 100644 --- a/synapse/storage/data_stores/main/cache.py +++ b/synapse/storage/data_stores/main/cache.py @@ -16,7 +16,7 @@ import itertools import logging -from typing import Any, Iterable, Optional +from typing import Any, Iterable, Optional, Tuple from twisted.internet import defer @@ -33,6 +33,26 @@ CURRENT_STATE_CACHE_NAME = "cs_cache_fake" class CacheInvalidationStore(SQLBaseStore): + async def invalidate_cache_and_stream(self, cache_name: str, keys: Tuple[Any, ...]): + """Invalidates the cache and adds it to the cache stream so slaves + will know to invalidate their caches. + + This should only be used to invalidate caches where slaves won't + otherwise know from other replication streams that the cache should + be invalidated. + """ + cache_func = getattr(self, cache_name, None) + if not cache_func: + return + + cache_func.invalidate(keys) + await self.runInteraction( + "invalidate_cache_and_stream", + self._send_invalidation_to_replication, + cache_func.__name__, + keys, + ) + def _invalidate_cache_and_stream(self, txn, cache_func, keys): """Invalidates the cache and adds it to the cache stream so slaves will know to invalidate their caches. From 8df862e45d9848c226399c8e39d31497461516ff Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 27 Jan 2020 14:30:57 +0000 Subject: [PATCH 0888/1623] Add `rooms.room_version` column (#6729) This is so that we don't have to rely on pulling it out from `current_state_events` table. --- changelog.d/6729.misc | 1 + synapse/federation/federation_client.py | 50 ++++++---- synapse/handlers/federation.py | 65 ++++++++++--- synapse/handlers/room.py | 52 ++++++---- .../v2_alpha/room_upgrade_rest_servlet.py | 3 +- synapse/storage/data_stores/main/room.py | 94 +++++++++++++++++-- .../schema/delta/57/rooms_version_column.sql | 24 +++++ synapse/storage/data_stores/main/state.py | 34 ++++--- tests/storage/test_room.py | 7 +- tests/storage/test_state.py | 5 +- tests/utils.py | 8 ++ 11 files changed, 270 insertions(+), 73 deletions(-) create mode 100644 changelog.d/6729.misc create mode 100644 synapse/storage/data_stores/main/schema/delta/57/rooms_version_column.sql diff --git a/changelog.d/6729.misc b/changelog.d/6729.misc new file mode 100644 index 0000000000..5537355bea --- /dev/null +++ b/changelog.d/6729.misc @@ -0,0 +1 @@ +Record room versions in the `rooms` table. diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index af652a7659..d57e8ca7a2 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -17,6 +17,7 @@ import copy import itertools import logging +from typing import Dict, Iterable from prometheus_client import Counter @@ -29,6 +30,7 @@ from synapse.api.errors import ( FederationDeniedError, HttpResponseException, SynapseError, + UnsupportedRoomVersionError, ) from synapse.api.room_versions import ( KNOWN_ROOM_VERSIONS, @@ -385,6 +387,8 @@ class FederationClient(FederationBase): return res except InvalidResponseError as e: logger.warning("Failed to %s via %s: %s", description, destination, e) + except UnsupportedRoomVersionError: + raise except HttpResponseException as e: if not 500 <= e.code < 600: raise e.to_synapse_error() @@ -404,7 +408,13 @@ class FederationClient(FederationBase): raise SynapseError(502, "Failed to %s via any server" % (description,)) def make_membership_event( - self, destinations, room_id, user_id, membership, content, params + self, + destinations: Iterable[str], + room_id: str, + user_id: str, + membership: str, + content: dict, + params: Dict[str, str], ): """ Creates an m.room.member event, with context, without participating in the room. @@ -417,21 +427,23 @@ class FederationClient(FederationBase): Note that this does not append any events to any graphs. Args: - destinations (Iterable[str]): Candidate homeservers which are probably + destinations: Candidate homeservers which are probably participating in the room. - room_id (str): The room in which the event will happen. - user_id (str): The user whose membership is being evented. - membership (str): The "membership" property of the event. Must be - one of "join" or "leave". - content (dict): Any additional data to put into the content field - of the event. - params (dict[str, str|Iterable[str]]): Query parameters to include in the - request. + room_id: The room in which the event will happen. + user_id: The user whose membership is being evented. + membership: The "membership" property of the event. Must be one of + "join" or "leave". + content: Any additional data to put into the content field of the + event. + params: Query parameters to include in the request. Return: - Deferred[tuple[str, FrozenEvent, int]]: resolves to a tuple of - `(origin, event, event_format)` where origin is the remote - homeserver which generated the event, and event_format is one of - `synapse.api.room_versions.EventFormatVersions`. + Deferred[Tuple[str, FrozenEvent, RoomVersion]]: resolves to a tuple of + `(origin, event, room_version)` where origin is the remote + homeserver which generated the event, and room_version is the + version of the room. + + Fails with a `UnsupportedRoomVersionError` if remote responds with + a room version we don't understand. Fails with a ``SynapseError`` if the chosen remote server returns a 300/400 code. @@ -453,8 +465,12 @@ class FederationClient(FederationBase): # Note: If not supplied, the room version may be either v1 or v2, # however either way the event format version will be v1. - room_version = ret.get("room_version", RoomVersions.V1.identifier) - event_format = room_version_to_event_format(room_version) + room_version_id = ret.get("room_version", RoomVersions.V1.identifier) + room_version = KNOWN_ROOM_VERSIONS.get(room_version_id) + if not room_version: + raise UnsupportedRoomVersionError() + + event_format = room_version_to_event_format(room_version_id) pdu_dict = ret.get("event", None) if not isinstance(pdu_dict, dict): @@ -478,7 +494,7 @@ class FederationClient(FederationBase): event_dict=pdu_dict, ) - return (destination, ev, event_format) + return (destination, ev, room_version) return self._try_destination_list( "make_" + membership, destinations, send_request diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index d4f9a792fc..f824ee79a0 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -44,10 +44,10 @@ from synapse.api.errors import ( StoreError, SynapseError, ) -from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersions +from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersion, RoomVersions from synapse.crypto.event_signing import compute_event_signature from synapse.event_auth import auth_types_for_event -from synapse.events import EventBase +from synapse.events import EventBase, room_version_to_event_format from synapse.events.snapshot import EventContext from synapse.events.validator import EventValidator from synapse.logging.context import ( @@ -703,8 +703,20 @@ class FederationHandler(BaseHandler): if not room: try: + prev_state_ids = await context.get_prev_state_ids() + create_event = await self.store.get_event( + prev_state_ids[(EventTypes.Create, "")] + ) + + room_version_id = create_event.content.get( + "room_version", RoomVersions.V1.identifier + ) + await self.store.store_room( - room_id=room_id, room_creator_user_id="", is_public=False + room_id=room_id, + room_creator_user_id="", + is_public=False, + room_version=KNOWN_ROOM_VERSIONS[room_version_id], ) except StoreError: logger.exception("Failed to store room.") @@ -1186,7 +1198,7 @@ class FederationHandler(BaseHandler): """ logger.debug("Joining %s to %s", joinee, room_id) - origin, event, event_format_version = yield self._make_and_verify_event( + origin, event, room_version = yield self._make_and_verify_event( target_hosts, room_id, joinee, @@ -1214,6 +1226,8 @@ class FederationHandler(BaseHandler): target_hosts.insert(0, origin) except ValueError: pass + + event_format_version = room_version_to_event_format(room_version.identifier) ret = yield self.federation_client.send_join( target_hosts, event, event_format_version ) @@ -1234,13 +1248,18 @@ class FederationHandler(BaseHandler): try: yield self.store.store_room( - room_id=room_id, room_creator_user_id="", is_public=False + room_id=room_id, + room_creator_user_id="", + is_public=False, + room_version=room_version, ) except Exception: # FIXME pass - yield self._persist_auth_tree(origin, auth_chain, state, event) + yield self._persist_auth_tree( + origin, auth_chain, state, event, room_version + ) # Check whether this room is the result of an upgrade of a room we already know # about. If so, migrate over user information @@ -1486,7 +1505,7 @@ class FederationHandler(BaseHandler): @defer.inlineCallbacks def do_remotely_reject_invite(self, target_hosts, room_id, user_id, content): - origin, event, event_format_version = yield self._make_and_verify_event( + origin, event, room_version = yield self._make_and_verify_event( target_hosts, room_id, user_id, "leave", content=content ) # Mark as outlier as we don't have any state for this event; we're not @@ -1513,7 +1532,11 @@ class FederationHandler(BaseHandler): def _make_and_verify_event( self, target_hosts, room_id, user_id, membership, content={}, params=None ): - origin, event, format_ver = yield self.federation_client.make_membership_event( + ( + origin, + event, + room_version, + ) = yield self.federation_client.make_membership_event( target_hosts, room_id, user_id, membership, content, params=params ) @@ -1525,7 +1548,7 @@ class FederationHandler(BaseHandler): assert event.user_id == user_id assert event.state_key == user_id assert event.room_id == room_id - return origin, event, format_ver + return origin, event, room_version @defer.inlineCallbacks @log_function @@ -1810,7 +1833,14 @@ class FederationHandler(BaseHandler): ) @defer.inlineCallbacks - def _persist_auth_tree(self, origin, auth_events, state, event): + def _persist_auth_tree( + self, + origin: str, + auth_events: List[EventBase], + state: List[EventBase], + event: EventBase, + room_version: RoomVersion, + ): """Checks the auth chain is valid (and passes auth checks) for the state and event. Then persists the auth chain and state atomically. Persists the event separately. Notifies about the persisted events @@ -1819,10 +1849,12 @@ class FederationHandler(BaseHandler): Will attempt to fetch missing auth events. Args: - origin (str): Where the events came from - auth_events (list) - state (list) - event (Event) + origin: Where the events came from + auth_events + state + event + room_version: The room version we expect this room to have, and + will raise if it doesn't match the version in the create event. Returns: Deferred @@ -1848,10 +1880,13 @@ class FederationHandler(BaseHandler): # invalid, and it would fail auth checks anyway. raise SynapseError(400, "No create event in state") - room_version = create_event.content.get( + room_version_id = create_event.content.get( "room_version", RoomVersions.V1.identifier ) + if room_version.identifier != room_version_id: + raise SynapseError(400, "Room version mismatch") + missing_auth_events = set() for e in itertools.chain(auth_events, state, [event]): for e_id in e.auth_event_ids(): diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 9f50196ea7..a9490782b7 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -29,7 +29,7 @@ from twisted.internet import defer from synapse.api.constants import EventTypes, JoinRules, RoomCreationPreset from synapse.api.errors import AuthError, Codes, NotFoundError, StoreError, SynapseError -from synapse.api.room_versions import KNOWN_ROOM_VERSIONS +from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersion from synapse.http.endpoint import parse_and_validate_server_name from synapse.storage.state import StateFilter from synapse.types import ( @@ -100,13 +100,15 @@ class RoomCreationHandler(BaseHandler): self.third_party_event_rules = hs.get_third_party_event_rules() @defer.inlineCallbacks - def upgrade_room(self, requester, old_room_id, new_version): + def upgrade_room( + self, requester: Requester, old_room_id: str, new_version: RoomVersion + ): """Replace a room with a new room with a different version Args: - requester (synapse.types.Requester): the user requesting the upgrade - old_room_id (unicode): the id of the room to be replaced - new_version (unicode): the new room version to use + requester: the user requesting the upgrade + old_room_id: the id of the room to be replaced + new_version: the new room version to use Returns: Deferred[unicode]: the new room id @@ -151,7 +153,7 @@ class RoomCreationHandler(BaseHandler): if r is None: raise NotFoundError("Unknown room id %s" % (old_room_id,)) new_room_id = yield self._generate_room_id( - creator_id=user_id, is_public=r["is_public"] + creator_id=user_id, is_public=r["is_public"], room_version=new_version, ) logger.info("Creating new room %s to replace %s", new_room_id, old_room_id) @@ -299,18 +301,22 @@ class RoomCreationHandler(BaseHandler): @defer.inlineCallbacks def clone_existing_room( - self, requester, old_room_id, new_room_id, new_room_version, tombstone_event_id + self, + requester: Requester, + old_room_id: str, + new_room_id: str, + new_room_version: RoomVersion, + tombstone_event_id: str, ): """Populate a new room based on an old room Args: - requester (synapse.types.Requester): the user requesting the upgrade - old_room_id (unicode): the id of the room to be replaced - new_room_id (unicode): the id to give the new room (should already have been + requester: the user requesting the upgrade + old_room_id : the id of the room to be replaced + new_room_id: the id to give the new room (should already have been created with _gemerate_room_id()) - new_room_version (unicode): the new room version to use - tombstone_event_id (unicode|str): the ID of the tombstone event in the old - room. + new_room_version: the new room version to use + tombstone_event_id: the ID of the tombstone event in the old room. Returns: Deferred """ @@ -320,7 +326,7 @@ class RoomCreationHandler(BaseHandler): raise SynapseError(403, "You are not permitted to create rooms") creation_content = { - "room_version": new_room_version, + "room_version": new_room_version.identifier, "predecessor": {"room_id": old_room_id, "event_id": tombstone_event_id}, } @@ -577,14 +583,15 @@ class RoomCreationHandler(BaseHandler): if ratelimit: yield self.ratelimit(requester) - room_version = config.get( + room_version_id = config.get( "room_version", self.config.default_room_version.identifier ) - if not isinstance(room_version, string_types): + if not isinstance(room_version_id, string_types): raise SynapseError(400, "room_version must be a string", Codes.BAD_JSON) - if room_version not in KNOWN_ROOM_VERSIONS: + room_version = KNOWN_ROOM_VERSIONS.get(room_version_id) + if room_version is None: raise SynapseError( 400, "Your homeserver does not support this room version", @@ -631,7 +638,9 @@ class RoomCreationHandler(BaseHandler): visibility = config.get("visibility", None) is_public = visibility == "public" - room_id = yield self._generate_room_id(creator_id=user_id, is_public=is_public) + room_id = yield self._generate_room_id( + creator_id=user_id, is_public=is_public, room_version=room_version, + ) directory_handler = self.hs.get_handlers().directory_handler if room_alias: @@ -660,7 +669,7 @@ class RoomCreationHandler(BaseHandler): creation_content = config.get("creation_content", {}) # override any attempt to set room versions via the creation_content - creation_content["room_version"] = room_version + creation_content["room_version"] = room_version.identifier yield self._send_events_for_new_room( requester, @@ -849,7 +858,9 @@ class RoomCreationHandler(BaseHandler): yield send(etype=etype, state_key=state_key, content=content) @defer.inlineCallbacks - def _generate_room_id(self, creator_id, is_public): + def _generate_room_id( + self, creator_id: str, is_public: str, room_version: RoomVersion, + ): # autogen room IDs and try to create it. We may clash, so just # try a few times till one goes through, giving up eventually. attempts = 0 @@ -863,6 +874,7 @@ class RoomCreationHandler(BaseHandler): room_id=gen_room_id, room_creator_user_id=creator_id, is_public=is_public, + room_version=room_version, ) return gen_room_id except StoreError: diff --git a/synapse/rest/client/v2_alpha/room_upgrade_rest_servlet.py b/synapse/rest/client/v2_alpha/room_upgrade_rest_servlet.py index ca97330797..f357015a70 100644 --- a/synapse/rest/client/v2_alpha/room_upgrade_rest_servlet.py +++ b/synapse/rest/client/v2_alpha/room_upgrade_rest_servlet.py @@ -64,7 +64,8 @@ class RoomUpgradeRestServlet(RestServlet): assert_params_in_dict(content, ("new_version",)) new_version = content["new_version"] - if new_version not in KNOWN_ROOM_VERSIONS: + new_version = KNOWN_ROOM_VERSIONS.get(content["new_version"]) + if new_version is None: raise SynapseError( 400, "Your homeserver does not support this room version", diff --git a/synapse/storage/data_stores/main/room.py b/synapse/storage/data_stores/main/room.py index d968803ad2..9a17e336ba 100644 --- a/synapse/storage/data_stores/main/room.py +++ b/synapse/storage/data_stores/main/room.py @@ -29,9 +29,10 @@ from twisted.internet import defer from synapse.api.constants import EventTypes from synapse.api.errors import StoreError +from synapse.api.room_versions import RoomVersion, RoomVersions from synapse.storage._base import SQLBaseStore from synapse.storage.data_stores.main.search import SearchStore -from synapse.storage.database import Database +from synapse.storage.database import Database, LoggingTransaction from synapse.types import ThirdPartyInstanceID from synapse.util.caches.descriptors import cached, cachedInlineCallbacks @@ -734,6 +735,7 @@ class RoomWorkerStore(SQLBaseStore): class RoomBackgroundUpdateStore(SQLBaseStore): REMOVE_TOMESTONED_ROOMS_BG_UPDATE = "remove_tombstoned_rooms_from_directory" + ADD_ROOMS_ROOM_VERSION_COLUMN = "add_rooms_room_version_column" def __init__(self, database: Database, db_conn, hs): super(RoomBackgroundUpdateStore, self).__init__(database, db_conn, hs) @@ -749,6 +751,11 @@ class RoomBackgroundUpdateStore(SQLBaseStore): self._remove_tombstoned_rooms_from_directory, ) + self.db.updates.register_background_update_handler( + self.ADD_ROOMS_ROOM_VERSION_COLUMN, + self._background_add_rooms_room_version_column, + ) + @defer.inlineCallbacks def _background_insert_retention(self, progress, batch_size): """Retrieves a list of all rooms within a range and inserts an entry for each of @@ -817,6 +824,73 @@ class RoomBackgroundUpdateStore(SQLBaseStore): defer.returnValue(batch_size) + async def _background_add_rooms_room_version_column( + self, progress: dict, batch_size: int + ): + """Background update to go and add room version inforamtion to `rooms` + table from `current_state_events` table. + """ + + last_room_id = progress.get("room_id", "") + + def _background_add_rooms_room_version_column_txn(txn: LoggingTransaction): + sql = """ + SELECT room_id, json FROM current_state_events + INNER JOIN event_json USING (room_id, event_id) + WHERE room_id > ? AND type = 'm.room.create' AND state_key = '' + ORDER BY room_id + LIMIT ? + """ + + txn.execute(sql, (last_room_id, batch_size)) + + updates = [] + for room_id, event_json in txn: + event_dict = json.loads(event_json) + room_version_id = event_dict.get("content", {}).get( + "room_version", RoomVersions.V1.identifier + ) + + creator = event_dict.get("content").get("creator") + + updates.append((room_id, creator, room_version_id)) + + if not updates: + return True + + new_last_room_id = "" + for room_id, creator, room_version_id in updates: + # We upsert here just in case we don't already have a row, + # mainly for paranoia as much badness would happen if we don't + # insert the row and then try and get the room version for the + # room. + self.db.simple_upsert_txn( + txn, + table="rooms", + keyvalues={"room_id": room_id}, + values={"room_version": room_version_id}, + insertion_values={"is_public": False, "creator": creator}, + ) + new_last_room_id = room_id + + self.db.updates._background_update_progress_txn( + txn, self.ADD_ROOMS_ROOM_VERSION_COLUMN, {"room_id": new_last_room_id} + ) + + return False + + end = await self.db.runInteraction( + "_background_add_rooms_room_version_column", + _background_add_rooms_room_version_column_txn, + ) + + if end: + await self.db.updates._end_background_update( + self.ADD_ROOMS_ROOM_VERSION_COLUMN + ) + + return batch_size + async def _remove_tombstoned_rooms_from_directory( self, progress, batch_size ) -> int: @@ -881,14 +955,21 @@ class RoomStore(RoomBackgroundUpdateStore, RoomWorkerStore, SearchStore): self.config = hs.config @defer.inlineCallbacks - def store_room(self, room_id, room_creator_user_id, is_public): + def store_room( + self, + room_id: str, + room_creator_user_id: str, + is_public: bool, + room_version: RoomVersion, + ): """Stores a room. Args: - room_id (str): The desired room ID, can be None. - room_creator_user_id (str): The user ID of the room creator. - is_public (bool): True to indicate that this room should appear in - public room lists. + room_id: The desired room ID, can be None. + room_creator_user_id: The user ID of the room creator. + is_public: True to indicate that this room should appear in + public room lists. + room_version: The version of the room Raises: StoreError if the room could not be stored. """ @@ -902,6 +983,7 @@ class RoomStore(RoomBackgroundUpdateStore, RoomWorkerStore, SearchStore): "room_id": room_id, "creator": room_creator_user_id, "is_public": is_public, + "room_version": room_version.identifier, }, ) if is_public: diff --git a/synapse/storage/data_stores/main/schema/delta/57/rooms_version_column.sql b/synapse/storage/data_stores/main/schema/delta/57/rooms_version_column.sql new file mode 100644 index 0000000000..352a66f5b0 --- /dev/null +++ b/synapse/storage/data_stores/main/schema/delta/57/rooms_version_column.sql @@ -0,0 +1,24 @@ +/* Copyright 2020 The Matrix.org Foundation C.I.C + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +-- We want to start storing the room version independently of +-- `current_state_events` so that we can delete stale entries from it without +-- losing the information. +ALTER TABLE rooms ADD COLUMN room_version TEXT; + + +INSERT into background_updates (update_name, progress_json) + VALUES ('add_rooms_room_version_column', '{}'); diff --git a/synapse/storage/data_stores/main/state.py b/synapse/storage/data_stores/main/state.py index 33bebd1c48..bd7b0276f1 100644 --- a/synapse/storage/data_stores/main/state.py +++ b/synapse/storage/data_stores/main/state.py @@ -60,24 +60,34 @@ class StateGroupWorkerStore(EventsWorkerStore, SQLBaseStore): def __init__(self, database: Database, db_conn, hs): super(StateGroupWorkerStore, self).__init__(database, db_conn, hs) - @defer.inlineCallbacks - def get_room_version(self, room_id): + @cached(max_entries=10000) + async def get_room_version(self, room_id: str) -> str: """Get the room_version of a given room - Args: - room_id (str) - - Returns: - Deferred[str] - Raises: - NotFoundError if the room is unknown + NotFoundError: if the room is unknown """ - # for now we do this by looking at the create event. We may want to cache this - # more intelligently in future. + + # First we try looking up room version from the database, but for old + # rooms we might not have added the room version to it yet so we fall + # back to previous behaviour and look in current state events. + + # We really should have an entry in the rooms table for every room we + # care about, but let's be a bit paranoid (at least while the background + # update is happening) to avoid breaking existing rooms. + version = await self.db.simple_select_one_onecol( + table="rooms", + keyvalues={"room_id": room_id}, + retcol="room_version", + desc="get_room_version", + allow_none=True, + ) + + if version is not None: + return version # Retrieve the room's create event - create_event = yield self.get_create_event_for_room(room_id) + create_event = await self.get_create_event_for_room(room_id) return create_event.content.get("room_version", "1") @defer.inlineCallbacks diff --git a/tests/storage/test_room.py b/tests/storage/test_room.py index 3ddaa151fe..086adeb8fd 100644 --- a/tests/storage/test_room.py +++ b/tests/storage/test_room.py @@ -17,6 +17,7 @@ from twisted.internet import defer from synapse.api.constants import EventTypes +from synapse.api.room_versions import RoomVersions from synapse.types import RoomAlias, RoomID, UserID from tests import unittest @@ -40,6 +41,7 @@ class RoomStoreTestCase(unittest.TestCase): self.room.to_string(), room_creator_user_id=self.u_creator.to_string(), is_public=True, + room_version=RoomVersions.V1, ) @defer.inlineCallbacks @@ -68,7 +70,10 @@ class RoomEventsStoreTestCase(unittest.TestCase): self.room = RoomID.from_string("!abcde:test") yield self.store.store_room( - self.room.to_string(), room_creator_user_id="@creator:text", is_public=True + self.room.to_string(), + room_creator_user_id="@creator:text", + is_public=True, + room_version=RoomVersions.V1, ) @defer.inlineCallbacks diff --git a/tests/storage/test_state.py b/tests/storage/test_state.py index d6ecf102f8..04d58fbf24 100644 --- a/tests/storage/test_state.py +++ b/tests/storage/test_state.py @@ -45,7 +45,10 @@ class StateStoreTestCase(tests.unittest.TestCase): self.room = RoomID.from_string("!abc123:test") yield self.store.store_room( - self.room.to_string(), room_creator_user_id="@creator:text", is_public=True + self.room.to_string(), + room_creator_user_id="@creator:text", + is_public=True, + room_version=RoomVersions.V1, ) @defer.inlineCallbacks diff --git a/tests/utils.py b/tests/utils.py index e2e9cafd79..513f358f4f 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -639,9 +639,17 @@ def create_room(hs, room_id, creator_id): """ persistence_store = hs.get_storage().persistence + store = hs.get_datastore() event_builder_factory = hs.get_event_builder_factory() event_creation_handler = hs.get_event_creation_handler() + yield store.store_room( + room_id=room_id, + room_creator_user_id=creator_id, + is_public=False, + room_version=RoomVersions.V1, + ) + builder = event_builder_factory.for_room_version( RoomVersions.V1, { From bdbeeb94ecc8e99f1401174df220a38e130db164 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 28 Jan 2020 13:05:24 +0000 Subject: [PATCH 0889/1623] Fix setting `mau_limit_reserved_threepids` config (#6793) Calling the invalidation function during initialisation of the data stores introduces a circular dependency, causing Synapse to fail to start. --- changelog.d/6793.bugfix | 1 + synapse/storage/data_stores/main/monthly_active_users.py | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 changelog.d/6793.bugfix diff --git a/changelog.d/6793.bugfix b/changelog.d/6793.bugfix new file mode 100644 index 0000000000..564d4596ea --- /dev/null +++ b/changelog.d/6793.bugfix @@ -0,0 +1 @@ +Fix bug where setting `mau_limit_reserved_threepids` config would cause Synapse to refuse to start. diff --git a/synapse/storage/data_stores/main/monthly_active_users.py b/synapse/storage/data_stores/main/monthly_active_users.py index 89a41542a3..1507a14e09 100644 --- a/synapse/storage/data_stores/main/monthly_active_users.py +++ b/synapse/storage/data_stores/main/monthly_active_users.py @@ -121,7 +121,13 @@ class MonthlyActiveUsersStore(MonthlyActiveUsersWorkerStore): if user_id: is_support = self.is_support_user_txn(txn, user_id) if not is_support: - self.upsert_monthly_active_user_txn(txn, user_id) + # We do this manually here to avoid hitting #6791 + self.db.simple_upsert_txn( + txn, + table="monthly_active_users", + keyvalues={"user_id": user_id}, + values={"timestamp": int(self._clock.time_msec())}, + ) else: logger.warning("mau limit reserved threepid %s not found in db" % tp) From 77d9357226687a177c865bcdeaa0e750612fc078 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 28 Jan 2020 13:09:36 +0000 Subject: [PATCH 0890/1623] 1.9.1 --- CHANGES.md | 9 +++++++++ changelog.d/6793.bugfix | 1 - debian/changelog | 6 ++++++ synapse/__init__.py | 2 +- 4 files changed, 16 insertions(+), 2 deletions(-) delete mode 100644 changelog.d/6793.bugfix diff --git a/CHANGES.md b/CHANGES.md index 8a068ae8c9..4c413b72ee 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,12 @@ +Synapse 1.9.1 (2020-01-28) +========================== + +Bugfixes +-------- + +- Fix bug where setting `mau_limit_reserved_threepids` config would cause Synapse to refuse to start. ([\#6793](https://github.com/matrix-org/synapse/issues/6793)) + + Synapse 1.9.0 (2020-01-23) ========================== diff --git a/changelog.d/6793.bugfix b/changelog.d/6793.bugfix deleted file mode 100644 index 564d4596ea..0000000000 --- a/changelog.d/6793.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix bug where setting `mau_limit_reserved_threepids` config would cause Synapse to refuse to start. diff --git a/debian/changelog b/debian/changelog index 49f3187691..74eb29c5ee 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.9.1) stable; urgency=medium + + * New synapse release 1.9.1. + + -- Synapse Packaging team Tue, 28 Jan 2020 13:09:23 +0000 + matrix-synapse-py3 (1.9.0) stable; urgency=medium * New synapse release 1.9.0. diff --git a/synapse/__init__.py b/synapse/__init__.py index 6236e13aa3..a236888d3c 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -36,7 +36,7 @@ try: except ImportError: pass -__version__ = "1.9.0" +__version__ = "1.9.1" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 02b44db922f01a35787d2535a834c9774b68020b Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 28 Jan 2020 13:44:21 +0000 Subject: [PATCH 0891/1623] Warn if postgres database has non-C locale. (#6734) As using non-C locale can cause issues on upgrading OS. --- UPGRADE.rst | 9 +++++++ changelog.d/6734.bugfix | 1 + docs/postgres.md | 20 +++++++++++++- synapse/storage/engines/postgres.py | 42 +++++++++++++++++++++++++++++ synapse/storage/engines/sqlite.py | 5 ++++ synapse/storage/prepare_database.py | 5 ++++ 6 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 changelog.d/6734.bugfix diff --git a/UPGRADE.rst b/UPGRADE.rst index a0202932b1..470246f128 100644 --- a/UPGRADE.rst +++ b/UPGRADE.rst @@ -76,6 +76,15 @@ for example: dpkg -i matrix-synapse-py3_1.3.0+stretch1_amd64.deb +Upgrading to **** +=============================== + +Synapse will now log a warning on start up if used with a PostgreSQL database +that has a non-recommended locale set. + +See [docs/postgres.md](docs/postgres.md) for details. + + Upgrading to v1.8.0 =================== diff --git a/changelog.d/6734.bugfix b/changelog.d/6734.bugfix new file mode 100644 index 0000000000..79c6bab4d1 --- /dev/null +++ b/changelog.d/6734.bugfix @@ -0,0 +1 @@ +Warn if postgres database has a non-C locale, as that can cause issues when upgrading locales (e.g. due to upgrading OS). diff --git a/docs/postgres.md b/docs/postgres.md index 7cb1ad18d4..e0793ecee8 100644 --- a/docs/postgres.md +++ b/docs/postgres.md @@ -32,7 +32,7 @@ Assuming your PostgreSQL database user is called `postgres`, first authenticate su - postgres # Or, if your system uses sudo to get administrative rights sudo -u postgres bash - + Then, create a user ``synapse_user`` with: createuser --pwprompt synapse_user @@ -63,6 +63,24 @@ You may need to enable password authentication so `synapse_user` can connect to the database. See . +### Fixing incorrect `COLLATE` or `CTYPE` + +Synapse will refuse to set up a new database if it has the wrong values of +`COLLATE` and `CTYPE` set, and will log warnings on existing databases. Using +different locales can cause issues if the locale library is updated from +underneath the database, or if a different version of the locale is used on any +replicas. + +The safest way to fix the issue is to take a dump and recreate the database with +the correct `COLLATE` and `CTYPE` parameters (as per +[docs/postgres.md](docs/postgres.md)). It is also possible to change the +parameters on a live database and run a `REINDEX` on the entire database, +however extreme care must be taken to avoid database corruption. + +Note that the above may fail with an error about duplicate rows if corruption +has already occurred, and such duplicate rows will need to be manually removed. + + ## Tuning Postgres The default settings should be fine for most deployments. For larger diff --git a/synapse/storage/engines/postgres.py b/synapse/storage/engines/postgres.py index c84cb452b0..a077345960 100644 --- a/synapse/storage/engines/postgres.py +++ b/synapse/storage/engines/postgres.py @@ -13,8 +13,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +import logging + from ._base import IncorrectDatabaseSetup +logger = logging.getLogger(__name__) + class PostgresEngine(object): single_threaded = False @@ -52,6 +56,44 @@ class PostgresEngine(object): "See docs/postgres.rst for more information." % (rows[0][0],) ) + txn.execute( + "SELECT datcollate, datctype FROM pg_database WHERE datname = current_database()" + ) + collation, ctype = txn.fetchone() + if collation != "C": + logger.warning( + "Database has incorrect collation of %r. Should be 'C'", collation + ) + + if ctype != "C": + logger.warning( + "Database has incorrect ctype of %r. Should be 'C'", ctype + ) + + def check_new_database(self, txn): + """Gets called when setting up a brand new database. This allows us to + apply stricter checks on new databases versus existing database. + """ + + txn.execute( + "SELECT datcollate, datctype FROM pg_database WHERE datname = current_database()" + ) + collation, ctype = txn.fetchone() + + errors = [] + + if collation != "C": + errors.append(" - 'COLLATE' is set to %r. Should be 'C'" % (collation,)) + + if ctype != "C": + errors.append(" - 'CTYPE' is set to %r. Should be 'C'" % (collation,)) + + if errors: + raise IncorrectDatabaseSetup( + "Database is incorrectly configured:\n\n%s\n\n" + "See docs/postgres.md for more information." % ("\n".join(errors)) + ) + def convert_param_style(self, sql): return sql.replace("?", "%s") diff --git a/synapse/storage/engines/sqlite.py b/synapse/storage/engines/sqlite.py index cbf52f5191..641e490697 100644 --- a/synapse/storage/engines/sqlite.py +++ b/synapse/storage/engines/sqlite.py @@ -59,6 +59,11 @@ class Sqlite3Engine(object): if version < (3, 11, 0): raise RuntimeError("Synapse requires sqlite 3.11 or above.") + def check_new_database(self, txn): + """Gets called when setting up a brand new database. This allows us to + apply stricter checks on new databases versus existing database. + """ + def convert_param_style(self, sql): return sql diff --git a/synapse/storage/prepare_database.py b/synapse/storage/prepare_database.py index e86984cd50..c285ef52a0 100644 --- a/synapse/storage/prepare_database.py +++ b/synapse/storage/prepare_database.py @@ -136,6 +136,11 @@ def _setup_new_database(cur, database_engine, data_stores): data_stores (list[str]): The names of the data stores to instantiate on the given database. """ + + # We're about to set up a brand new database so we check that its + # configured to our liking. + database_engine.check_new_database(cur) + current_dir = os.path.join(dir_path, "schema", "full_schemas") directory_entries = os.listdir(current_dir) From a8ce7aeb433e08f46306797a1252668c178a7825 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 28 Jan 2020 14:18:29 +0000 Subject: [PATCH 0892/1623] Pass room version object into event_auth.check and check_redaction (#6788) These are easier to work with than the strings and we normally have one around. This fixes `FederationHander._persist_auth_tree` which was passing a RoomVersion object into event_auth.check instead of a string. --- changelog.d/6788.misc | 1 + synapse/api/auth.py | 7 +++++-- synapse/event_auth.py | 34 +++++++++++++++++++++------------- synapse/handlers/federation.py | 18 +++++++++++------- synapse/handlers/message.py | 10 +++++++--- synapse/state/v1.py | 4 ++-- synapse/state/v2.py | 4 +++- tests/test_event_auth.py | 11 ++++------- 8 files changed, 54 insertions(+), 35 deletions(-) create mode 100644 changelog.d/6788.misc diff --git a/changelog.d/6788.misc b/changelog.d/6788.misc new file mode 100644 index 0000000000..5537355bea --- /dev/null +++ b/changelog.d/6788.misc @@ -0,0 +1 @@ +Record room versions in the `rooms` table. diff --git a/synapse/api/auth.py b/synapse/api/auth.py index 2cbfab2569..8b1277ad02 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -33,6 +33,7 @@ from synapse.api.errors import ( MissingClientTokenError, ResourceLimitError, ) +from synapse.api.room_versions import KNOWN_ROOM_VERSIONS from synapse.config.server import is_threepid_reserved from synapse.types import StateMap, UserID from synapse.util.caches import CACHE_SIZE_FACTOR, register_cache @@ -77,15 +78,17 @@ class Auth(object): self._account_validity = hs.config.account_validity @defer.inlineCallbacks - def check_from_context(self, room_version, event, context, do_sig_check=True): + def check_from_context(self, room_version: str, event, context, do_sig_check=True): prev_state_ids = yield context.get_prev_state_ids() auth_events_ids = yield self.compute_auth_events( event, prev_state_ids, for_verification=True ) auth_events = yield self.store.get_events(auth_events_ids) auth_events = {(e.type, e.state_key): e for e in itervalues(auth_events)} + + room_version_obj = KNOWN_ROOM_VERSIONS[room_version] event_auth.check( - room_version, event, auth_events=auth_events, do_sig_check=do_sig_check + room_version_obj, event, auth_events=auth_events, do_sig_check=do_sig_check ) @defer.inlineCallbacks diff --git a/synapse/event_auth.py b/synapse/event_auth.py index e3a1ba47a0..016d5678e5 100644 --- a/synapse/event_auth.py +++ b/synapse/event_auth.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # Copyright 2014 - 2016 OpenMarket Ltd +# Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -23,17 +24,27 @@ from unpaddedbase64 import decode_base64 from synapse.api.constants import EventTypes, JoinRules, Membership from synapse.api.errors import AuthError, EventSizeError, SynapseError -from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, EventFormatVersions +from synapse.api.room_versions import ( + KNOWN_ROOM_VERSIONS, + EventFormatVersions, + RoomVersion, +) from synapse.types import UserID, get_domain_from_id logger = logging.getLogger(__name__) -def check(room_version, event, auth_events, do_sig_check=True, do_size_check=True): +def check( + room_version_obj: RoomVersion, + event, + auth_events, + do_sig_check=True, + do_size_check=True, +): """ Checks if this event is correctly authed. Args: - room_version (str): the version of the room + room_version_obj: the version of the room event: the event being checked. auth_events (dict: event-key -> event): the existing room state. @@ -97,10 +108,11 @@ def check(room_version, event, auth_events, do_sig_check=True, do_size_check=Tru 403, "Creation event's room_id domain does not match sender's" ) - room_version = event.content.get("room_version", "1") - if room_version not in KNOWN_ROOM_VERSIONS: + room_version_prop = event.content.get("room_version", "1") + if room_version_prop not in KNOWN_ROOM_VERSIONS: raise AuthError( - 403, "room appears to have unsupported version %s" % (room_version,) + 403, + "room appears to have unsupported version %s" % (room_version_prop,), ) # FIXME logger.debug("Allowing! %s", event) @@ -160,7 +172,7 @@ def check(room_version, event, auth_events, do_sig_check=True, do_size_check=Tru _check_power_levels(event, auth_events) if event.type == EventTypes.Redaction: - check_redaction(room_version, event, auth_events) + check_redaction(room_version_obj, event, auth_events) logger.debug("Allowing! %s", event) @@ -386,7 +398,7 @@ def _can_send_event(event, auth_events): return True -def check_redaction(room_version, event, auth_events): +def check_redaction(room_version_obj: RoomVersion, event, auth_events): """Check whether the event sender is allowed to redact the target event. Returns: @@ -406,11 +418,7 @@ def check_redaction(room_version, event, auth_events): if user_level >= redact_level: return False - v = KNOWN_ROOM_VERSIONS.get(room_version) - if not v: - raise RuntimeError("Unrecognized room version %r" % (room_version,)) - - if v.event_format == EventFormatVersions.V1: + if room_version_obj.event_format == EventFormatVersions.V1: redacter_domain = get_domain_from_id(event.event_id) redactee_domain = get_domain_from_id(event.redacts) if redacter_domain == redactee_domain: diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index f824ee79a0..180f165a7a 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -47,7 +47,7 @@ from synapse.api.errors import ( from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersion, RoomVersions from synapse.crypto.event_signing import compute_event_signature from synapse.event_auth import auth_types_for_event -from synapse.events import EventBase, room_version_to_event_format +from synapse.events import EventBase from synapse.events.snapshot import EventContext from synapse.events.validator import EventValidator from synapse.logging.context import ( @@ -1198,7 +1198,7 @@ class FederationHandler(BaseHandler): """ logger.debug("Joining %s to %s", joinee, room_id) - origin, event, room_version = yield self._make_and_verify_event( + origin, event, room_version_obj = yield self._make_and_verify_event( target_hosts, room_id, joinee, @@ -1227,7 +1227,7 @@ class FederationHandler(BaseHandler): except ValueError: pass - event_format_version = room_version_to_event_format(room_version.identifier) + event_format_version = room_version_obj.event_format ret = yield self.federation_client.send_join( target_hosts, event, event_format_version ) @@ -1251,14 +1251,14 @@ class FederationHandler(BaseHandler): room_id=room_id, room_creator_user_id="", is_public=False, - room_version=room_version, + room_version=room_version_obj, ) except Exception: # FIXME pass yield self._persist_auth_tree( - origin, auth_chain, state, event, room_version + origin, auth_chain, state, event, room_version_obj ) # Check whether this room is the result of an upgrade of a room we already know @@ -2022,6 +2022,7 @@ class FederationHandler(BaseHandler): if do_soft_fail_check: room_version = yield self.store.get_room_version(event.room_id) + room_version_obj = KNOWN_ROOM_VERSIONS[room_version] # Calculate the "current state". if state is not None: @@ -2071,7 +2072,9 @@ class FederationHandler(BaseHandler): } try: - event_auth.check(room_version, event, auth_events=current_auth_events) + event_auth.check( + room_version_obj, event, auth_events=current_auth_events + ) except AuthError as e: logger.warning("Soft-failing %r because %s", event, e) event.internal_metadata.soft_failed = True @@ -2155,6 +2158,7 @@ class FederationHandler(BaseHandler): defer.Deferred[EventContext]: updated context object """ room_version = yield self.store.get_room_version(event.room_id) + room_version_obj = KNOWN_ROOM_VERSIONS[room_version] try: context = yield self._update_auth_events_and_context_for_auth( @@ -2172,7 +2176,7 @@ class FederationHandler(BaseHandler): ) try: - event_auth.check(room_version, event, auth_events=auth_events) + event_auth.check(room_version_obj, event, auth_events=auth_events) except AuthError as e: logger.warning("Failed auth resolution for %r because %s", event, e) context.rejected = RejectedReason.AUTH_ERROR diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index 8ea3aca2f4..9a0f661b9b 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -40,7 +40,7 @@ from synapse.api.errors import ( NotFoundError, SynapseError, ) -from synapse.api.room_versions import RoomVersions +from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersions from synapse.api.urls import ConsentURIBuilder from synapse.events.validator import EventValidator from synapse.logging.context import run_in_background @@ -962,9 +962,13 @@ class EventCreationHandler(object): ) auth_events = yield self.store.get_events(auth_events_ids) auth_events = {(e.type, e.state_key): e for e in auth_events.values()} - room_version = yield self.store.get_room_version(event.room_id) - if event_auth.check_redaction(room_version, event, auth_events=auth_events): + room_version = yield self.store.get_room_version(event.room_id) + room_version_obj = KNOWN_ROOM_VERSIONS[room_version] + + if event_auth.check_redaction( + room_version_obj, event, auth_events=auth_events + ): # this user doesn't have 'redact' rights, so we need to do some more # checks on the original event. Let's start by checking the original # event exists. diff --git a/synapse/state/v1.py b/synapse/state/v1.py index d6c34ce3b7..24b7c0faef 100644 --- a/synapse/state/v1.py +++ b/synapse/state/v1.py @@ -281,7 +281,7 @@ def _resolve_auth_events(events, auth_events): try: # The signatures have already been checked at this point event_auth.check( - RoomVersions.V1.identifier, + RoomVersions.V1, event, auth_events, do_sig_check=False, @@ -299,7 +299,7 @@ def _resolve_normal_events(events, auth_events): try: # The signatures have already been checked at this point event_auth.check( - RoomVersions.V1.identifier, + RoomVersions.V1, event, auth_events, do_sig_check=False, diff --git a/synapse/state/v2.py b/synapse/state/v2.py index 6216fdd204..531018c6a5 100644 --- a/synapse/state/v2.py +++ b/synapse/state/v2.py @@ -26,6 +26,7 @@ import synapse.state from synapse import event_auth from synapse.api.constants import EventTypes from synapse.api.errors import AuthError +from synapse.api.room_versions import KNOWN_ROOM_VERSIONS from synapse.events import EventBase from synapse.types import StateMap @@ -402,6 +403,7 @@ def _iterative_auth_checks( Deferred[StateMap[str]]: Returns the final updated state """ resolved_state = base_state.copy() + room_version_obj = KNOWN_ROOM_VERSIONS[room_version] for event_id in event_ids: event = event_map[event_id] @@ -430,7 +432,7 @@ def _iterative_auth_checks( try: event_auth.check( - room_version, + room_version_obj, event, auth_events, do_sig_check=False, diff --git a/tests/test_event_auth.py b/tests/test_event_auth.py index 8b2741d277..ca20b085a2 100644 --- a/tests/test_event_auth.py +++ b/tests/test_event_auth.py @@ -37,7 +37,7 @@ class EventAuthTestCase(unittest.TestCase): # creator should be able to send state event_auth.check( - RoomVersions.V1.identifier, + RoomVersions.V1, _random_state_event(creator), auth_events, do_sig_check=False, @@ -47,7 +47,7 @@ class EventAuthTestCase(unittest.TestCase): self.assertRaises( AuthError, event_auth.check, - RoomVersions.V1.identifier, + RoomVersions.V1, _random_state_event(joiner), auth_events, do_sig_check=False, @@ -76,7 +76,7 @@ class EventAuthTestCase(unittest.TestCase): self.assertRaises( AuthError, event_auth.check, - RoomVersions.V1.identifier, + RoomVersions.V1, _random_state_event(pleb), auth_events, do_sig_check=False, @@ -84,10 +84,7 @@ class EventAuthTestCase(unittest.TestCase): # king should be able to send state event_auth.check( - RoomVersions.V1.identifier, - _random_state_event(king), - auth_events, - do_sig_check=False, + RoomVersions.V1, _random_state_event(king), auth_events, do_sig_check=False, ) From 49d3bca37b91fa092e13fd28c42dcf970fb86bb7 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 27 Jan 2020 16:14:54 +0000 Subject: [PATCH 0893/1623] Implement updated auth rules from MSC2260 --- synapse/api/room_versions.py | 16 ++++++++++++++++ synapse/event_auth.py | 24 +++++++++++++++++++----- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/synapse/api/room_versions.py b/synapse/api/room_versions.py index c6f50fd7b9..cf7ee60d3a 100644 --- a/synapse/api/room_versions.py +++ b/synapse/api/room_versions.py @@ -57,6 +57,9 @@ class RoomVersion(object): state_res = attr.ib() # int; one of the StateResolutionVersions enforce_key_validity = attr.ib() # bool + # bool: before MSC2260, anyone was allowed to send an aliases event + special_case_aliases_auth = attr.ib(type=bool, default=False) + class RoomVersions(object): V1 = RoomVersion( @@ -65,6 +68,7 @@ class RoomVersions(object): EventFormatVersions.V1, StateResolutionVersions.V1, enforce_key_validity=False, + special_case_aliases_auth=True, ) V2 = RoomVersion( "2", @@ -72,6 +76,7 @@ class RoomVersions(object): EventFormatVersions.V1, StateResolutionVersions.V2, enforce_key_validity=False, + special_case_aliases_auth=True, ) V3 = RoomVersion( "3", @@ -79,6 +84,7 @@ class RoomVersions(object): EventFormatVersions.V2, StateResolutionVersions.V2, enforce_key_validity=False, + special_case_aliases_auth=True, ) V4 = RoomVersion( "4", @@ -86,6 +92,7 @@ class RoomVersions(object): EventFormatVersions.V3, StateResolutionVersions.V2, enforce_key_validity=False, + special_case_aliases_auth=True, ) V5 = RoomVersion( "5", @@ -93,6 +100,14 @@ class RoomVersions(object): EventFormatVersions.V3, StateResolutionVersions.V2, enforce_key_validity=True, + special_case_aliases_auth=True, + ) + MSC2260_DEV = RoomVersion( + "org.matrix.msc2260", + RoomDisposition.UNSTABLE, + EventFormatVersions.V3, + StateResolutionVersions.V2, + enforce_key_validity=True, ) @@ -104,5 +119,6 @@ KNOWN_ROOM_VERSIONS = { RoomVersions.V3, RoomVersions.V4, RoomVersions.V5, + RoomVersions.MSC2260_DEV, ) } # type: Dict[str, RoomVersion] diff --git a/synapse/event_auth.py b/synapse/event_auth.py index 016d5678e5..3240e8a7b2 100644 --- a/synapse/event_auth.py +++ b/synapse/event_auth.py @@ -100,7 +100,12 @@ def check( if not event.signatures.get(event_id_domain): raise AuthError(403, "Event not signed by sending server") + # Implementation of https://matrix.org/docs/spec/rooms/v1#authorization-rules + # + # 1. If type is m.room.create: if event.type == EventTypes.Create: + # 1b. If the domain of the room_id does not match the domain of the sender, + # reject. sender_domain = get_domain_from_id(event.sender) room_id_domain = get_domain_from_id(event.room_id) if room_id_domain != sender_domain: @@ -108,40 +113,49 @@ def check( 403, "Creation event's room_id domain does not match sender's" ) + # 1c. If content.room_version is present and is not a recognised version, reject room_version_prop = event.content.get("room_version", "1") if room_version_prop not in KNOWN_ROOM_VERSIONS: raise AuthError( 403, "room appears to have unsupported version %s" % (room_version_prop,), ) - # FIXME + logger.debug("Allowing! %s", event) return + # 3. If event does not have a m.room.create in its auth_events, reject. creation_event = auth_events.get((EventTypes.Create, ""), None) - if not creation_event: raise AuthError(403, "No create event in auth events") + # additional check for m.federate creating_domain = get_domain_from_id(event.room_id) originating_domain = get_domain_from_id(event.sender) if creating_domain != originating_domain: if not _can_federate(event, auth_events): raise AuthError(403, "This room has been marked as unfederatable.") - # FIXME: Temp hack + # 4. If type is m.room.aliases if event.type == EventTypes.Aliases: + # 4a. If event has no state_key, reject if not event.is_state(): raise AuthError(403, "Alias event must be a state event") if not event.state_key: raise AuthError(403, "Alias event must have non-empty state_key") + + # 4b. If sender's domain doesn't matches [sic] state_key, reject sender_domain = get_domain_from_id(event.sender) if event.state_key != sender_domain: raise AuthError( 403, "Alias event's state_key does not match sender's domain" ) - logger.debug("Allowing! %s", event) - return + + # 4c. Otherwise, allow. + # This is removed by https://github.com/matrix-org/matrix-doc/pull/2260 + if room_version.special_case_aliases_auth: + logger.debug("Allowing! %s", event) + return if logger.isEnabledFor(logging.DEBUG): logger.debug("Auth events: %s", [a.event_id for a in auth_events.values()]) From 99e205fc214a65d307d4f5484321bcbb32a60b5f Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 27 Jan 2020 16:16:16 +0000 Subject: [PATCH 0894/1623] changelog --- changelog.d/6787.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6787.misc diff --git a/changelog.d/6787.misc b/changelog.d/6787.misc new file mode 100644 index 0000000000..82fe636173 --- /dev/null +++ b/changelog.d/6787.misc @@ -0,0 +1 @@ +Implement updated auth rules from MSC2260. From fbe0a82c0d603b12d8c1d9a2a1121dafb5616213 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 28 Jan 2020 09:43:57 +0000 Subject: [PATCH 0895/1623] update changelog --- changelog.d/6787.feature | 1 + changelog.d/6787.misc | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 changelog.d/6787.feature delete mode 100644 changelog.d/6787.misc diff --git a/changelog.d/6787.feature b/changelog.d/6787.feature new file mode 100644 index 0000000000..df9e4b77ab --- /dev/null +++ b/changelog.d/6787.feature @@ -0,0 +1 @@ +Implement updated authorization rules for aliases events, from [MSC2260](https://github.com/matrix-org/matrix-doc/pull/2260). diff --git a/changelog.d/6787.misc b/changelog.d/6787.misc deleted file mode 100644 index 82fe636173..0000000000 --- a/changelog.d/6787.misc +++ /dev/null @@ -1 +0,0 @@ -Implement updated auth rules from MSC2260. From e17a11066192354f6c6144135a14e7abe524f44c Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 28 Jan 2020 14:43:21 +0000 Subject: [PATCH 0896/1623] Detect unknown remote devices and mark cache as stale (#6776) We just mark the fact that the cache may be stale in the database for now. --- changelog.d/6776.misc | 1 + synapse/handlers/devicemessage.py | 57 ++++++++++++++++++- synapse/handlers/federation.py | 20 +++++++ synapse/replication/slave/storage/devices.py | 2 +- synapse/storage/data_stores/main/devices.py | 29 ++++++++-- .../57/device_list_remote_cache_stale.sql | 25 ++++++++ 6 files changed, 126 insertions(+), 8 deletions(-) create mode 100644 changelog.d/6776.misc create mode 100644 synapse/storage/data_stores/main/schema/delta/57/device_list_remote_cache_stale.sql diff --git a/changelog.d/6776.misc b/changelog.d/6776.misc new file mode 100644 index 0000000000..4f9a4ac7a5 --- /dev/null +++ b/changelog.d/6776.misc @@ -0,0 +1 @@ +Detect unknown remote devices and mark cache as stale. diff --git a/synapse/handlers/devicemessage.py b/synapse/handlers/devicemessage.py index 73b9e120f5..5c5fe77be2 100644 --- a/synapse/handlers/devicemessage.py +++ b/synapse/handlers/devicemessage.py @@ -14,6 +14,7 @@ # limitations under the License. import logging +from typing import Any, Dict from canonicaljson import json @@ -65,6 +66,9 @@ class DeviceMessageHandler(object): logger.warning("Request for keys for non-local user %s", user_id) raise SynapseError(400, "Not a user here") + if not by_device: + continue + messages_by_device = { device_id: { "content": message_content, @@ -73,8 +77,11 @@ class DeviceMessageHandler(object): } for device_id, message_content in by_device.items() } - if messages_by_device: - local_messages[user_id] = messages_by_device + local_messages[user_id] = messages_by_device + + yield self._check_for_unknown_devices( + message_type, sender_user_id, by_device + ) stream_id = yield self.store.add_messages_from_remote_to_device_inbox( origin, message_id, local_messages @@ -84,6 +91,52 @@ class DeviceMessageHandler(object): "to_device_key", stream_id, users=local_messages.keys() ) + @defer.inlineCallbacks + def _check_for_unknown_devices( + self, + message_type: str, + sender_user_id: str, + by_device: Dict[str, Dict[str, Any]], + ): + """Checks inbound device messages for unkown remote devices, and if + found marks the remote cache for the user as stale. + """ + + if message_type != "m.room_key_request": + return + + # Get the sending device IDs + requesting_device_ids = set() + for message_content in by_device.values(): + device_id = message_content.get("requesting_device_id") + requesting_device_ids.add(device_id) + + # Check if we are tracking the devices of the remote user. + room_ids = yield self.store.get_rooms_for_user(sender_user_id) + if not room_ids: + logger.info( + "Received device message from remote device we don't" + " share a room with: %s %s", + sender_user_id, + requesting_device_ids, + ) + return + + # If we are tracking check that we know about the sending + # devices. + cached_devices = yield self.store.get_cached_devices_for_user(sender_user_id) + + unknown_devices = requesting_device_ids - set(cached_devices) + if unknown_devices: + logger.info( + "Received device message from remote device not in our cache: %s %s", + sender_user_id, + unknown_devices, + ) + yield self.store.mark_remote_user_device_cache_as_stale(sender_user_id) + # TODO: Poke something to start trying to refetch user's + # keys. + @defer.inlineCallbacks def send_device_message(self, sender_user_id, message_type, messages): set_tag("number_of_messages", len(messages)) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 180f165a7a..a67020a259 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -742,6 +742,26 @@ class FederationHandler(BaseHandler): user = UserID.from_string(event.state_key) await self.user_joined_room(user, room_id) + # For encrypted messages we check that we know about the sending device, + # if we don't then we mark the device cache for that user as stale. + if event.type == EventTypes.Encryption: + device_id = event.content.get("device_id") + if device_id is not None: + cached_devices = await self.store.get_cached_devices_for_user( + event.sender + ) + if device_id not in cached_devices: + logger.info( + "Received event from remote device not in our cache: %s %s", + event.sender, + device_id, + ) + await self.store.mark_remote_user_device_cache_as_stale( + event.sender + ) + # TODO: Poke something to start trying to refetch user's + # keys. + @log_function async def backfill(self, dest, room_id, limit, extremities): """ Trigger a backfill request to `dest` for the given `room_id` diff --git a/synapse/replication/slave/storage/devices.py b/synapse/replication/slave/storage/devices.py index dc625e0d7a..1c77687eea 100644 --- a/synapse/replication/slave/storage/devices.py +++ b/synapse/replication/slave/storage/devices.py @@ -72,6 +72,6 @@ class SlavedDeviceStore(EndToEndKeyWorkerStore, DeviceWorkerStore, BaseSlavedSto destination, token ) - self._get_cached_devices_for_user.invalidate((user_id,)) + self.get_cached_devices_for_user.invalidate((user_id,)) self._get_cached_user_device.invalidate_many((user_id,)) self.get_device_list_last_stream_id_for_remote.invalidate((user_id,)) diff --git a/synapse/storage/data_stores/main/devices.py b/synapse/storage/data_stores/main/devices.py index f0a7962dd0..30bf66b2b6 100644 --- a/synapse/storage/data_stores/main/devices.py +++ b/synapse/storage/data_stores/main/devices.py @@ -457,7 +457,7 @@ class DeviceWorkerStore(SQLBaseStore): device = yield self._get_cached_user_device(user_id, device_id) results.setdefault(user_id, {})[device_id] = device else: - results[user_id] = yield self._get_cached_devices_for_user(user_id) + results[user_id] = yield self.get_cached_devices_for_user(user_id) set_tag("in_cache", results) set_tag("not_in_cache", user_ids_not_in_cache) @@ -475,12 +475,12 @@ class DeviceWorkerStore(SQLBaseStore): return db_to_json(content) @cachedInlineCallbacks() - def _get_cached_devices_for_user(self, user_id): + def get_cached_devices_for_user(self, user_id): devices = yield self.db.simple_select_list( table="device_lists_remote_cache", keyvalues={"user_id": user_id}, retcols=("device_id", "content"), - desc="_get_cached_devices_for_user", + desc="get_cached_devices_for_user", ) return { device["device_id"]: db_to_json(device["content"]) for device in devices @@ -641,6 +641,18 @@ class DeviceWorkerStore(SQLBaseStore): return results + def mark_remote_user_device_cache_as_stale(self, user_id: str): + """Records that the server has reason to believe the cache of the devices + for the remote users is out of date. + """ + return self.db.simple_upsert( + table="device_lists_remote_resync", + keyvalues={"user_id": user_id}, + values={}, + insertion_values={"added_ts": self._clock.time_msec()}, + desc="make_remote_user_device_cache_as_stale", + ) + class DeviceBackgroundUpdateStore(SQLBaseStore): def __init__(self, database: Database, db_conn, hs): @@ -887,7 +899,7 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): ) txn.call_after(self._get_cached_user_device.invalidate, (user_id, device_id)) - txn.call_after(self._get_cached_devices_for_user.invalidate, (user_id,)) + txn.call_after(self.get_cached_devices_for_user.invalidate, (user_id,)) txn.call_after( self.get_device_list_last_stream_id_for_remote.invalidate, (user_id,) ) @@ -902,6 +914,13 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): lock=False, ) + # If we're replacing the remote user's device list cache presumably + # we've done a full resync, so we remove the entry that says we need + # to resync + self.db.simple_delete_txn( + txn, table="device_lists_remote_resync", keyvalues={"user_id": user_id}, + ) + def update_remote_device_list_cache(self, user_id, devices, stream_id): """Replace the entire cache of the remote user's devices. @@ -942,7 +961,7 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): ], ) - txn.call_after(self._get_cached_devices_for_user.invalidate, (user_id,)) + txn.call_after(self.get_cached_devices_for_user.invalidate, (user_id,)) txn.call_after(self._get_cached_user_device.invalidate_many, (user_id,)) txn.call_after( self.get_device_list_last_stream_id_for_remote.invalidate, (user_id,) diff --git a/synapse/storage/data_stores/main/schema/delta/57/device_list_remote_cache_stale.sql b/synapse/storage/data_stores/main/schema/delta/57/device_list_remote_cache_stale.sql new file mode 100644 index 0000000000..c3b6de2099 --- /dev/null +++ b/synapse/storage/data_stores/main/schema/delta/57/device_list_remote_cache_stale.sql @@ -0,0 +1,25 @@ +/* Copyright 2020 The Matrix.org Foundation C.I.C + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +-- Records whether the server thinks that the remote users cached device lists +-- may be out of date (e.g. if we have received a to device message from a +-- device we don't know about). +CREATE TABLE IF NOT EXISTS device_lists_remote_resync ( + user_id TEXT NOT NULL, + added_ts BIGINT NOT NULL +); + +CREATE UNIQUE INDEX device_lists_remote_resync_idx ON device_lists_remote_resync (user_id); +CREATE INDEX device_lists_remote_resync_ts_idx ON device_lists_remote_resync (added_ts); From a1f307f7d1ca6d4f83f9f43272a4b152cfdee299 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 28 Jan 2020 14:55:22 +0000 Subject: [PATCH 0897/1623] fix bad variable ref --- synapse/event_auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/event_auth.py b/synapse/event_auth.py index 3240e8a7b2..472f165044 100644 --- a/synapse/event_auth.py +++ b/synapse/event_auth.py @@ -153,7 +153,7 @@ def check( # 4c. Otherwise, allow. # This is removed by https://github.com/matrix-org/matrix-doc/pull/2260 - if room_version.special_case_aliases_auth: + if room_version_obj.special_case_aliases_auth: logger.debug("Allowing! %s", event) return From fcfb591b312d6ec124c67aef2136a2d5948cadbe Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 28 Jan 2020 18:59:48 +0000 Subject: [PATCH 0898/1623] Fix outbound federation request metrics (#6795) --- changelog.d/6795.bugfix | 1 + synapse/http/matrixfederationclient.py | 4 ++++ 2 files changed, 5 insertions(+) create mode 100644 changelog.d/6795.bugfix diff --git a/changelog.d/6795.bugfix b/changelog.d/6795.bugfix new file mode 100644 index 0000000000..d1585653b1 --- /dev/null +++ b/changelog.d/6795.bugfix @@ -0,0 +1 @@ +Fix outbound federation request metrics. diff --git a/synapse/http/matrixfederationclient.py b/synapse/http/matrixfederationclient.py index 16765d54e0..6f1bb04d8b 100644 --- a/synapse/http/matrixfederationclient.py +++ b/synapse/http/matrixfederationclient.py @@ -408,6 +408,8 @@ class MatrixFederationHttpClient(object): _sec_timeout, ) + outgoing_requests_counter.labels(method_bytes).inc() + try: with Measure(self.clock, "outbound_request"): # we don't want all the fancy cookie and redirect handling @@ -440,6 +442,8 @@ class MatrixFederationHttpClient(object): response.phrase.decode("ascii", errors="replace"), ) + incoming_responses_counter.labels(method_bytes, response.code).inc() + set_tag(tags.HTTP_STATUS_CODE, response.code) if 200 <= response.code < 300: From 2cad8baa7030a86efc103599d79412741654dc15 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 29 Jan 2020 09:56:41 +0000 Subject: [PATCH 0899/1623] Fix bug when querying remote user keys that require a resync. (#6796) We ended up only returning a single device, rather than all of them. --- changelog.d/6796.bugfix | 1 + synapse/handlers/e2e_keys.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 changelog.d/6796.bugfix diff --git a/changelog.d/6796.bugfix b/changelog.d/6796.bugfix new file mode 100644 index 0000000000..206a157311 --- /dev/null +++ b/changelog.d/6796.bugfix @@ -0,0 +1 @@ +Fix bug where querying a remote user's device keys that weren't cached resulted in only returning a single device. diff --git a/synapse/handlers/e2e_keys.py b/synapse/handlers/e2e_keys.py index 2d889364d4..95a9d71f41 100644 --- a/synapse/handlers/e2e_keys.py +++ b/synapse/handlers/e2e_keys.py @@ -208,8 +208,9 @@ class E2eKeysHandler(object): ) user_devices = user_devices["devices"] + user_results = results.setdefault(user_id, {}) for device in user_devices: - results[user_id] = {device["device_id"]: device["keys"]} + user_results[device["device_id"]] = device["keys"] user_ids_updated.append(user_id) except Exception as e: failures[destination] = _exception_to_failure(e) From 611215a49cedf8d5f63c53168173763731d02260 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 29 Jan 2020 11:01:32 +0000 Subject: [PATCH 0900/1623] Delete current state when server leaves a room (#6792) Otherwise its just stale data, which may get deleted later anyway so can't be relied on. It's also a bit of a shotgun if we're trying to get the current state of a room we're not in. --- changelog.d/6792.misc | 1 + synapse/storage/data_stores/main/events.py | 183 +++++++++++++-------- synapse/storage/persist_events.py | 89 +++++++++- 3 files changed, 198 insertions(+), 75 deletions(-) create mode 100644 changelog.d/6792.misc diff --git a/changelog.d/6792.misc b/changelog.d/6792.misc new file mode 100644 index 0000000000..fa31d509b3 --- /dev/null +++ b/changelog.d/6792.misc @@ -0,0 +1 @@ +Delete current state from the database when server leaves a room. diff --git a/synapse/storage/data_stores/main/events.py b/synapse/storage/data_stores/main/events.py index ce553566a5..c9d0d68c3a 100644 --- a/synapse/storage/data_stores/main/events.py +++ b/synapse/storage/data_stores/main/events.py @@ -32,6 +32,7 @@ from twisted.internet import defer import synapse.metrics from synapse.api.constants import EventContentFields, EventTypes from synapse.api.errors import SynapseError +from synapse.api.room_versions import RoomVersions from synapse.events import EventBase # noqa: F401 from synapse.events.snapshot import EventContext # noqa: F401 from synapse.events.utils import prune_event_dict @@ -468,84 +469,93 @@ class EventsStore( to_delete = delta_state.to_delete to_insert = delta_state.to_insert - # First we add entries to the current_state_delta_stream. We - # do this before updating the current_state_events table so - # that we can use it to calculate the `prev_event_id`. (This - # allows us to not have to pull out the existing state - # unnecessarily). - # - # The stream_id for the update is chosen to be the minimum of the stream_ids - # for the batch of the events that we are persisting; that means we do not - # end up in a situation where workers see events before the - # current_state_delta updates. - # - sql = """ - INSERT INTO current_state_delta_stream - (stream_id, room_id, type, state_key, event_id, prev_event_id) - SELECT ?, ?, ?, ?, ?, ( - SELECT event_id FROM current_state_events - WHERE room_id = ? AND type = ? AND state_key = ? + if delta_state.no_longer_in_room: + # Server is no longer in the room so we delete the room from + # current_state_events, being careful we've already updated the + # rooms.room_version column (which gets populated in a + # background task). + self._upsert_room_version_txn(txn, room_id) + + # Before deleting we populate the current_state_delta_stream + # so that async background tasks get told what happened. + sql = """ + INSERT INTO current_state_delta_stream + (stream_id, room_id, type, state_key, event_id, prev_event_id) + SELECT ?, room_id, type, state_key, null, event_id + FROM current_state_events + WHERE room_id = ? + """ + txn.execute(sql, (stream_id, room_id)) + + self.db.simple_delete_txn( + txn, table="current_state_events", keyvalues={"room_id": room_id}, ) - """ - txn.executemany( - sql, - ( - ( - stream_id, - room_id, - etype, - state_key, - None, - room_id, - etype, - state_key, + else: + # We're still in the room, so we update the current state as normal. + + # First we add entries to the current_state_delta_stream. We + # do this before updating the current_state_events table so + # that we can use it to calculate the `prev_event_id`. (This + # allows us to not have to pull out the existing state + # unnecessarily). + # + # The stream_id for the update is chosen to be the minimum of the stream_ids + # for the batch of the events that we are persisting; that means we do not + # end up in a situation where workers see events before the + # current_state_delta updates. + # + sql = """ + INSERT INTO current_state_delta_stream + (stream_id, room_id, type, state_key, event_id, prev_event_id) + SELECT ?, ?, ?, ?, ?, ( + SELECT event_id FROM current_state_events + WHERE room_id = ? AND type = ? AND state_key = ? ) - for etype, state_key in to_delete - # We sanity check that we're deleting rather than updating - if (etype, state_key) not in to_insert - ), - ) - txn.executemany( - sql, - ( + """ + txn.executemany( + sql, ( - stream_id, - room_id, - etype, - state_key, - ev_id, - room_id, - etype, - state_key, - ) - for (etype, state_key), ev_id in iteritems(to_insert) - ), - ) + ( + stream_id, + room_id, + etype, + state_key, + to_insert.get((etype, state_key)), + room_id, + etype, + state_key, + ) + for etype, state_key in itertools.chain(to_delete, to_insert) + ), + ) + # Now we actually update the current_state_events table - # Now we actually update the current_state_events table + txn.executemany( + "DELETE FROM current_state_events" + " WHERE room_id = ? AND type = ? AND state_key = ?", + ( + (room_id, etype, state_key) + for etype, state_key in itertools.chain(to_delete, to_insert) + ), + ) - txn.executemany( - "DELETE FROM current_state_events" - " WHERE room_id = ? AND type = ? AND state_key = ?", - ( - (room_id, etype, state_key) - for etype, state_key in itertools.chain(to_delete, to_insert) - ), - ) + # We include the membership in the current state table, hence we do + # a lookup when we insert. This assumes that all events have already + # been inserted into room_memberships. + txn.executemany( + """INSERT INTO current_state_events + (room_id, type, state_key, event_id, membership) + VALUES (?, ?, ?, ?, (SELECT membership FROM room_memberships WHERE event_id = ?)) + """, + [ + (room_id, key[0], key[1], ev_id, ev_id) + for key, ev_id in iteritems(to_insert) + ], + ) - # We include the membership in the current state table, hence we do - # a lookup when we insert. This assumes that all events have already - # been inserted into room_memberships. - txn.executemany( - """INSERT INTO current_state_events - (room_id, type, state_key, event_id, membership) - VALUES (?, ?, ?, ?, (SELECT membership FROM room_memberships WHERE event_id = ?)) - """, - [ - (room_id, key[0], key[1], ev_id, ev_id) - for key, ev_id in iteritems(to_insert) - ], - ) + # We now update `local_current_membership`. We do this regardless + # of whether we're still in the room or not to handle the case where + # e.g. we just got banned (where we need to record that fact here). # Note: Do we really want to delete rows here (that we do not # subsequently reinsert below)? While technically correct it means @@ -601,6 +611,35 @@ class EventsStore( self._invalidate_state_caches_and_stream(txn, room_id, members_changed) + def _upsert_room_version_txn(self, txn: LoggingTransaction, room_id: str): + """Update the room version in the database based off current state + events. + + This is used when we're about to delete current state and we want to + ensure that the `rooms.room_version` column is up to date. + """ + + sql = """ + SELECT json FROM event_json + INNER JOIN current_state_events USING (room_id, event_id) + WHERE room_id = ? AND type = ? AND state_key = ? + """ + txn.execute(sql, (room_id, EventTypes.Create, "")) + row = txn.fetchone() + if row: + event_json = json.loads(row[0]) + content = event_json.get("content", {}) + creator = content.get("creator") + room_version_id = content.get("room_version", RoomVersions.V1.identifier) + + self.db.simple_upsert_txn( + txn, + table="rooms", + keyvalues={"room_id": room_id}, + values={"room_version": room_version_id}, + insertion_values={"is_public": False, "creator": creator}, + ) + def _update_forward_extremities_txn( self, txn, new_forward_extremities, max_stream_order ): diff --git a/synapse/storage/persist_events.py b/synapse/storage/persist_events.py index 368c457321..d060c8b992 100644 --- a/synapse/storage/persist_events.py +++ b/synapse/storage/persist_events.py @@ -15,6 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import itertools import logging from collections import deque, namedtuple from typing import Iterable, List, Optional, Tuple @@ -27,7 +28,7 @@ from prometheus_client import Counter, Histogram from twisted.internet import defer -from synapse.api.constants import EventTypes +from synapse.api.constants import EventTypes, Membership from synapse.events import FrozenEvent from synapse.events.snapshot import EventContext from synapse.logging.context import PreserveLoggingContext, make_deferred_yieldable @@ -72,17 +73,20 @@ stale_forward_extremities_counter = Histogram( ) -@attr.s(slots=True, frozen=True) +@attr.s(slots=True) class DeltaState: """Deltas to use to update the `current_state_events` table. Attributes: to_delete: List of type/state_keys to delete from current state to_insert: Map of state to upsert into current state + no_longer_in_room: The server is not longer in the room, so the room + should e.g. be removed from `current_state_events` table. """ to_delete = attr.ib(type=List[Tuple[str, str]]) to_insert = attr.ib(type=StateMap[str]) + no_longer_in_room = attr.ib(type=bool, default=False) class _EventPeristenceQueue(object): @@ -396,11 +400,12 @@ class EventsPersistenceStorage(object): # If either are not None then there has been a change, # and we need to work out the delta (or use that # given) + delta = None if delta_ids is not None: # If there is a delta we know that we've # only added or replaced state, never # removed keys entirely. - state_delta_for_room[room_id] = DeltaState([], delta_ids) + delta = DeltaState([], delta_ids) elif current_state is not None: with Measure( self._clock, "persist_events.calculate_state_delta" @@ -408,6 +413,22 @@ class EventsPersistenceStorage(object): delta = await self._calculate_state_delta( room_id, current_state ) + + if delta: + # If we have a change of state then lets check + # whether we're actually still a member of the room, + # or if our last user left. If we're no longer in + # the room then we delete the current state and + # extremities. + is_still_joined = await self._is_server_still_joined( + room_id, ev_ctx_rm, delta, current_state + ) + if not is_still_joined: + logger.info("Server no longer in room %s", room_id) + latest_event_ids = [] + current_state = {} + delta.no_longer_in_room = True + state_delta_for_room[room_id] = delta # If we have the current_state then lets prefill @@ -660,3 +681,65 @@ class EventsPersistenceStorage(object): } return DeltaState(to_delete=to_delete, to_insert=to_insert) + + async def _is_server_still_joined( + self, + room_id: str, + ev_ctx_rm: List[Tuple[FrozenEvent, EventContext]], + delta: DeltaState, + current_state: Optional[StateMap[str]], + ) -> bool: + """Check if the server will still be joined after the given events have + been persised. + + Args: + room_id + ev_ctx_rm + delta: The delta of current state between what is in the database + and what the new current state will be. + current_state: The new current state if it already been calculated, + otherwise None. + """ + + if not any( + self.is_mine_id(state_key) + for typ, state_key in itertools.chain(delta.to_delete, delta.to_insert) + if typ == EventTypes.Member + ): + # There have been no changes to membership of our users, so nothing + # has changed and we assume we're still in the room. + return True + + # Check if any of the given events are a local join that appear in the + # current state + for (typ, state_key), event_id in delta.to_insert.items(): + if typ != EventTypes.Member or not self.is_mine_id(state_key): + continue + + for event, _ in ev_ctx_rm: + if event_id == event.event_id: + if event.membership == Membership.JOIN: + return True + + # There's been a change of membership but we don't have a local join + # event in the new events, so we need to check the full state. + if current_state is None: + current_state = await self.main_store.get_current_state_ids(room_id) + current_state = dict(current_state) + for key in delta.to_delete: + current_state.pop(key, None) + + current_state.update(delta.to_insert) + + event_ids = [ + event_id + for (typ, state_key,), event_id in current_state.items() + if typ == EventTypes.Member and self.is_mine_id(state_key) + ] + + rows = await self.main_store.get_membership_from_event_ids(event_ids) + is_still_joined = any(row["membership"] == Membership.JOIN for row in rows) + if is_still_joined: + return True + else: + return False From 6b9e1014cf9c107f3198999159fbc935376fdcc9 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 29 Jan 2020 11:23:01 +0000 Subject: [PATCH 0901/1623] Fix race in federation sender that delayed device updates. (#6799) We were sending device updates down both the federation stream and device streams. This mean there was a race if the federation sender worker processed the federation stream first, as when the sender checked if there were new device updates the slaved ID generator hadn't been updated with the new stream IDs and so returned nothing. This situation is correctly handled by events/receipts/etc by not sending updates down the federation stream and instead having the federation sender worker listen on the other streams and poke the transaction queues as appropriate. --- changelog.d/6799.bugfix | 1 + synapse/app/federation_sender.py | 20 +++++++++++++++++++- synapse/federation/send_queue.py | 32 +++----------------------------- 3 files changed, 23 insertions(+), 30 deletions(-) create mode 100644 changelog.d/6799.bugfix diff --git a/changelog.d/6799.bugfix b/changelog.d/6799.bugfix new file mode 100644 index 0000000000..322a2758af --- /dev/null +++ b/changelog.d/6799.bugfix @@ -0,0 +1 @@ +Fix race in federation sender worker that delayed sending of device updates. diff --git a/synapse/app/federation_sender.py b/synapse/app/federation_sender.py index 38d11fdd0f..63a91f1177 100644 --- a/synapse/app/federation_sender.py +++ b/synapse/app/federation_sender.py @@ -38,7 +38,11 @@ from synapse.replication.slave.storage.receipts import SlavedReceiptsStore from synapse.replication.slave.storage.registration import SlavedRegistrationStore from synapse.replication.slave.storage.transactions import SlavedTransactionStore from synapse.replication.tcp.client import ReplicationClientHandler -from synapse.replication.tcp.streams._base import ReceiptsStream +from synapse.replication.tcp.streams._base import ( + DeviceListsStream, + ReceiptsStream, + ToDeviceStream, +) from synapse.server import HomeServer from synapse.storage.database import Database from synapse.types import ReadReceipt @@ -256,6 +260,20 @@ class FederationSenderHandler(object): "process_receipts_for_federation", self._on_new_receipts, rows ) + # ... as well as device updates and messages + elif stream_name == DeviceListsStream.NAME: + hosts = set(row.destination for row in rows) + for host in hosts: + self.federation_sender.send_device_messages(host) + + elif stream_name == ToDeviceStream.NAME: + # The to_device stream includes stuff to be pushed to both local + # clients and remote servers, so we ignore entities that start with + # '@' (since they'll be local users rather than destinations). + hosts = set(row.entity for row in rows if not row.entity.startswith("@")) + for host in hosts: + self.federation_sender.send_device_messages(host) + @defer.inlineCallbacks def _on_new_receipts(self, rows): """ diff --git a/synapse/federation/send_queue.py b/synapse/federation/send_queue.py index 174f6e42be..0bb82a6bb3 100644 --- a/synapse/federation/send_queue.py +++ b/synapse/federation/send_queue.py @@ -69,8 +69,6 @@ class FederationRemoteSendQueue(object): self.edus = SortedDict() # stream position -> Edu - self.device_messages = SortedDict() # stream position -> destination - self.pos = 1 self.pos_time = SortedDict() @@ -92,7 +90,6 @@ class FederationRemoteSendQueue(object): "keyed_edu", "keyed_edu_changed", "edus", - "device_messages", "pos_time", "presence_destinations", ]: @@ -171,12 +168,6 @@ class FederationRemoteSendQueue(object): for key in keys[:i]: del self.edus[key] - # Delete things out of device map - keys = self.device_messages.keys() - i = self.device_messages.bisect_left(position_to_delete) - for key in keys[:i]: - del self.device_messages[key] - def notify_new_events(self, current_id): """As per FederationSender""" # We don't need to replicate this as it gets sent down a different @@ -249,9 +240,8 @@ class FederationRemoteSendQueue(object): def send_device_messages(self, destination): """As per FederationSender""" - pos = self._next_pos() - self.device_messages[pos] = destination - self.notifier.on_new_replication_data() + # We don't need to replicate this as it gets sent down a different + # stream. def get_current_token(self): return self.pos - 1 @@ -339,14 +329,6 @@ class FederationRemoteSendQueue(object): for (pos, edu) in edus: rows.append((pos, EduRow(edu))) - # Fetch changed device messages - i = self.device_messages.bisect_right(from_token) - j = self.device_messages.bisect_right(to_token) + 1 - device_messages = {v: k for k, v in self.device_messages.items()[i:j]} - - for (destination, pos) in iteritems(device_messages): - rows.append((pos, DeviceRow(destination=destination))) - # Sort rows based on pos rows.sort() @@ -504,7 +486,6 @@ ParsedFederationStreamData = namedtuple( "presence_destinations", # list of tuples of UserPresenceState and destinations "keyed_edus", # dict of destination -> { key -> Edu } "edus", # dict of destination -> [Edu] - "device_destinations", # set of destinations ), ) @@ -523,11 +504,7 @@ def process_rows_for_federation(transaction_queue, rows): # them into the appropriate collection and then send them off. buff = ParsedFederationStreamData( - presence=[], - presence_destinations=[], - keyed_edus={}, - edus={}, - device_destinations=set(), + presence=[], presence_destinations=[], keyed_edus={}, edus={}, ) # Parse the rows in the stream and add to the buffer @@ -555,6 +532,3 @@ def process_rows_for_federation(transaction_queue, rows): for destination, edu_list in iteritems(buff.edus): for edu in edu_list: transaction_queue.send_edu(edu, None) - - for destination in buff.device_destinations: - transaction_queue.send_device_messages(destination) From ee42a5513e020424daa736962fee7fb69bd2373a Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 28 Jan 2020 11:02:55 +0000 Subject: [PATCH 0902/1623] Factor out a `copy_power_levels_contents` method I'm going to need another copy (hah!) of this. --- synapse/events/utils.py | 37 ++++++++++++++++++++++++++++++- synapse/handlers/room.py | 23 ++++++++++--------- tests/events/test_utils.py | 45 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 90 insertions(+), 15 deletions(-) diff --git a/synapse/events/utils.py b/synapse/events/utils.py index 07d1c5bcf0..be57c6d9be 100644 --- a/synapse/events/utils.py +++ b/synapse/events/utils.py @@ -12,8 +12,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - +import collections import re +from typing import Mapping, Union from six import string_types @@ -422,3 +423,37 @@ class EventClientSerializer(object): return yieldable_gather_results( self.serialize_event, events, time_now=time_now, **kwargs ) + + +def copy_power_levels_contents( + old_power_levels: Mapping[str, Union[int, Mapping[str, int]]] +): + """Copy the content of a power_levels event, unfreezing frozendicts along the way + + Raises: + TypeError if the input does not look like a valid power levels event content + """ + if not isinstance(old_power_levels, collections.Mapping): + raise TypeError("Not a valid power-levels content: %r" % (old_power_levels,)) + + power_levels = {} + for k, v in old_power_levels.items(): + + if isinstance(v, int): + power_levels[k] = v + continue + + if isinstance(v, collections.Mapping): + power_levels[k] = h = {} + for k1, v1 in v.items(): + # we should only have one level of nesting + if not isinstance(v1, int): + raise TypeError( + "Invalid power_levels value for %s.%s: %r" % (k, k1, v) + ) + h[k1] = v1 + continue + + raise TypeError("Invalid power_levels value for %s: %r" % (k, v)) + + return power_levels diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index a9490782b7..532ee22fa4 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -30,6 +30,7 @@ from twisted.internet import defer from synapse.api.constants import EventTypes, JoinRules, RoomCreationPreset from synapse.api.errors import AuthError, Codes, NotFoundError, StoreError, SynapseError from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersion +from synapse.events.utils import copy_power_levels_contents from synapse.http.endpoint import parse_and_validate_server_name from synapse.storage.state import StateFilter from synapse.types import ( @@ -367,6 +368,15 @@ class RoomCreationHandler(BaseHandler): if old_event: initial_state[k] = old_event.content + # deep-copy the power-levels event before we start modifying it + # note that if frozen_dicts are enabled, `power_levels` will be a frozen + # dict so we can't just copy.deepcopy it. + initial_state[ + (EventTypes.PowerLevels, "") + ] = power_levels = copy_power_levels_contents( + initial_state[(EventTypes.PowerLevels, "")] + ) + # Resolve the minimum power level required to send any state event # We will give the upgrading user this power level temporarily (if necessary) such that # they are able to copy all of the state events over, then revert them back to their @@ -375,8 +385,6 @@ class RoomCreationHandler(BaseHandler): # Copy over user power levels now as this will not be possible with >100PL users once # the room has been created - power_levels = initial_state[(EventTypes.PowerLevels, "")] - # Calculate the minimum power level needed to clone the room event_power_levels = power_levels.get("events", {}) state_default = power_levels.get("state_default", 0) @@ -386,16 +394,7 @@ class RoomCreationHandler(BaseHandler): # Raise the requester's power level in the new room if necessary current_power_level = power_levels["users"][user_id] if current_power_level < needed_power_level: - # make sure we copy the event content rather than overwriting it. - # note that if frozen_dicts are enabled, `power_levels` will be a frozen - # dict so we can't just copy.deepcopy it. - - new_power_levels = {k: v for k, v in power_levels.items() if k != "users"} - new_power_levels["users"] = { - k: v for k, v in power_levels.get("users", {}).items() if k != user_id - } - new_power_levels["users"][user_id] = needed_power_level - initial_state[(EventTypes.PowerLevels, "")] = new_power_levels + power_levels["users"][user_id] = needed_power_level yield self._send_events_for_new_room( requester, diff --git a/tests/events/test_utils.py b/tests/events/test_utils.py index 9e3d4d0f47..2b13980dfd 100644 --- a/tests/events/test_utils.py +++ b/tests/events/test_utils.py @@ -15,9 +15,14 @@ from synapse.events import FrozenEvent -from synapse.events.utils import prune_event, serialize_event +from synapse.events.utils import ( + copy_power_levels_contents, + prune_event, + serialize_event, +) +from synapse.util.frozenutils import freeze -from .. import unittest +from tests import unittest def MockEvent(**kwargs): @@ -241,3 +246,39 @@ class SerializeEventTestCase(unittest.TestCase): self.serialize( MockEvent(room_id="!foo:bar", content={"foo": "bar"}), ["room_id", 4] ) + + +class CopyPowerLevelsContentTestCase(unittest.TestCase): + def setUp(self) -> None: + self.test_content = { + "ban": 50, + "events": {"m.room.name": 100, "m.room.power_levels": 100}, + "events_default": 0, + "invite": 50, + "kick": 50, + "notifications": {"room": 20}, + "redact": 50, + "state_default": 50, + "users": {"@example:localhost": 100}, + "users_default": 0, + } + + def _test(self, input): + a = copy_power_levels_contents(input) + + self.assertEqual(a["ban"], 50) + self.assertEqual(a["events"]["m.room.name"], 100) + + # make sure that changing the copy changes the copy and not the orig + a["ban"] = 10 + a["events"]["m.room.power_levels"] = 20 + + self.assertEqual(input["ban"], 50) + self.assertEqual(input["events"]["m.room.power_levels"], 100) + + def test_unfrozen(self): + self._test(self.test_content) + + def test_frozen(self): + input = freeze(self.test_content) + self._test(input) From b36095ae5cd88d95802ecf01f8b0f541593ea773 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 28 Jan 2020 11:08:38 +0000 Subject: [PATCH 0903/1623] Set the PL for aliases events to 0. --- synapse/events/utils.py | 2 +- synapse/handlers/room.py | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/synapse/events/utils.py b/synapse/events/utils.py index be57c6d9be..f70f5032fb 100644 --- a/synapse/events/utils.py +++ b/synapse/events/utils.py @@ -449,7 +449,7 @@ def copy_power_levels_contents( # we should only have one level of nesting if not isinstance(v1, int): raise TypeError( - "Invalid power_levels value for %s.%s: %r" % (k, k1, v) + "Invalid power_levels value for %s.%s: %r" % (k, k1, v1) ) h[k1] = v1 continue diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 532ee22fa4..a95b45d791 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -287,7 +287,16 @@ class RoomCreationHandler(BaseHandler): except AuthError as e: logger.warning("Unable to update PLs in old room: %s", e) - logger.info("Setting correct PLs in new room to %s", old_room_pl_state.content) + new_pl_content = copy_power_levels_contents(old_room_pl_state.content) + + # pre-msc2260 rooms may not have the right setting for aliases. If no other + # value is set, set it now. + events_default = new_pl_content.get("events_default", 0) + new_pl_content.setdefault("events", {}).setdefault( + EventTypes.Aliases, events_default + ) + + logger.info("Setting correct PLs in new room to %s", new_pl_content) yield self.event_creation_handler.create_and_send_nonmember_event( requester, { @@ -295,7 +304,7 @@ class RoomCreationHandler(BaseHandler): "state_key": "", "room_id": new_room_id, "sender": requester.user.to_string(), - "content": old_room_pl_state.content, + "content": new_pl_content, }, ratelimit=False, ) @@ -812,6 +821,10 @@ class RoomCreationHandler(BaseHandler): EventTypes.RoomHistoryVisibility: 100, EventTypes.CanonicalAlias: 50, EventTypes.RoomAvatar: 50, + # MSC2260: Allow everybody to send alias events by default + # This will be reudundant on pre-MSC2260 rooms, since the + # aliases event is special-cased. + EventTypes.Aliases: 0, }, "events_default": 0, "state_default": 50, From dcd85b976dc93851d7f5246fc0684d5c5f7d7b5b Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 28 Jan 2020 17:46:34 +0000 Subject: [PATCH 0904/1623] Make /directory/room/ handle restrictive power levels Fixes a bug where the alias would be added, but `PUT /directory/room/` would return a 403. --- synapse/handlers/directory.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/synapse/handlers/directory.py b/synapse/handlers/directory.py index a07d2f1a17..8c5980cb0c 100644 --- a/synapse/handlers/directory.py +++ b/synapse/handlers/directory.py @@ -151,7 +151,12 @@ class DirectoryHandler(BaseHandler): yield self._create_association(room_alias, room_id, servers, creator=user_id) if send_event: - yield self.send_room_alias_update_event(requester, room_id) + try: + yield self.send_room_alias_update_event(requester, room_id) + except AuthError as e: + # sending the aliases event may fail due to the user not having + # permission in the room; this is permitted. + logger.info("Skipping updating aliases event due to auth error %s", e) @defer.inlineCallbacks def delete_association(self, requester, room_alias, send_event=True): From 750d4d7599d1985bc262853494b21e9fee34c637 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 28 Jan 2020 11:09:14 +0000 Subject: [PATCH 0905/1623] changelog --- changelog.d/6790.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6790.feature diff --git a/changelog.d/6790.feature b/changelog.d/6790.feature new file mode 100644 index 0000000000..df9e4b77ab --- /dev/null +++ b/changelog.d/6790.feature @@ -0,0 +1 @@ +Implement updated authorization rules for aliases events, from [MSC2260](https://github.com/matrix-org/matrix-doc/pull/2260). From a855b7c3a82458602bd62ed00bffed269f2acfec Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 29 Jan 2020 12:06:31 +0000 Subject: [PATCH 0906/1623] Remove unused DeviceRow class (#6800) --- changelog.d/6800.bugfix | 1 + synapse/federation/send_queue.py | 21 +-------------------- 2 files changed, 2 insertions(+), 20 deletions(-) create mode 100644 changelog.d/6800.bugfix diff --git a/changelog.d/6800.bugfix b/changelog.d/6800.bugfix new file mode 100644 index 0000000000..322a2758af --- /dev/null +++ b/changelog.d/6800.bugfix @@ -0,0 +1 @@ +Fix race in federation sender worker that delayed sending of device updates. diff --git a/synapse/federation/send_queue.py b/synapse/federation/send_queue.py index 0bb82a6bb3..001bb304ae 100644 --- a/synapse/federation/send_queue.py +++ b/synapse/federation/send_queue.py @@ -454,28 +454,9 @@ class EduRow(BaseFederationRow, namedtuple("EduRow", ("edu",))): # Edu buff.edus.setdefault(self.edu.destination, []).append(self.edu) -class DeviceRow(BaseFederationRow, namedtuple("DeviceRow", ("destination",))): # str - """Streams the fact that either a) there is pending to device messages for - users on the remote, or b) a local users device has changed and needs to - be sent to the remote. - """ - - TypeId = "d" - - @staticmethod - def from_data(data): - return DeviceRow(destination=data["destination"]) - - def to_data(self): - return {"destination": self.destination} - - def add_to_buffer(self, buff): - buff.device_destinations.add(self.destination) - - TypeToRow = { Row.TypeId: Row - for Row in (PresenceRow, PresenceDestinationsRow, KeyedEduRow, EduRow, DeviceRow) + for Row in (PresenceRow, PresenceDestinationsRow, KeyedEduRow, EduRow,) } From 5a246611e3cbf27cf1dd7e4453adc8040cddd6a2 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 30 Jan 2020 11:25:59 +0000 Subject: [PATCH 0907/1623] Type defintions for use in refactoring for redaction changes (#6803) * Bump signedjson to 1.1 ... so that we can use the type definitions * Fix breakage caused by upgrade to signedjson 1.1 Thanks, @illicitonion... --- changelog.d/6803.misc | 1 + synapse/events/__init__.py | 5 +++-- synapse/python_dependencies.py | 4 +++- synapse/types.py | 7 ++++++- tests/storage/test_keys.py | 15 +++++++++++---- 5 files changed, 24 insertions(+), 8 deletions(-) create mode 100644 changelog.d/6803.misc diff --git a/changelog.d/6803.misc b/changelog.d/6803.misc new file mode 100644 index 0000000000..08aa80bcd9 --- /dev/null +++ b/changelog.d/6803.misc @@ -0,0 +1 @@ +Refactoring work in preparation for changing the event redaction algorithm. diff --git a/synapse/events/__init__.py b/synapse/events/__init__.py index 72c09327f4..f813fa2fe7 100644 --- a/synapse/events/__init__.py +++ b/synapse/events/__init__.py @@ -23,6 +23,7 @@ from unpaddedbase64 import encode_base64 from synapse.api.errors import UnsupportedRoomVersionError from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, EventFormatVersions +from synapse.types import JsonDict from synapse.util.caches import intern_dict from synapse.util.frozenutils import freeze @@ -197,7 +198,7 @@ class EventBase(object): def is_state(self): return hasattr(self, "state_key") and self.state_key is not None - def get_dict(self): + def get_dict(self) -> JsonDict: d = dict(self._event_dict) d.update({"signatures": self.signatures, "unsigned": dict(self.unsigned)}) @@ -209,7 +210,7 @@ class EventBase(object): def get_internal_metadata_dict(self): return self.internal_metadata.get_dict() - def get_pdu_json(self, time_now=None): + def get_pdu_json(self, time_now=None) -> JsonDict: pdu_json = self.get_dict() if time_now is not None and "age_ts" in pdu_json["unsigned"]: diff --git a/synapse/python_dependencies.py b/synapse/python_dependencies.py index 5871feaafd..8de8cb2c12 100644 --- a/synapse/python_dependencies.py +++ b/synapse/python_dependencies.py @@ -1,6 +1,7 @@ # Copyright 2015, 2016 OpenMarket Ltd # Copyright 2017 Vector Creations Ltd # Copyright 2018 New Vector Ltd +# Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -43,7 +44,8 @@ REQUIREMENTS = [ "frozendict>=1", "unpaddedbase64>=1.1.0", "canonicaljson>=1.1.3", - "signedjson>=1.0.0", + # we use the type definitions added in signedjson 1.1. + "signedjson>=1.1.0", "pynacl>=1.2.1", "idna>=2.5", # validating SSL certs for IP addresses requires service_identity 18.1. diff --git a/synapse/types.py b/synapse/types.py index 65e4d8c181..f3cd465735 100644 --- a/synapse/types.py +++ b/synapse/types.py @@ -17,7 +17,7 @@ import re import string import sys from collections import namedtuple -from typing import Dict, Tuple, TypeVar +from typing import Any, Dict, Tuple, TypeVar import attr from signedjson.key import decode_verify_key_bytes @@ -43,6 +43,11 @@ T = TypeVar("T") StateMap = Dict[Tuple[str, str], T] +# the type of a JSON-serialisable dict. This could be made stronger, but it will +# do for now. +JsonDict = Dict[str, Any] + + class Requester( namedtuple( "Requester", ["user", "access_token_id", "is_guest", "device_id", "app_service"] diff --git a/tests/storage/test_keys.py b/tests/storage/test_keys.py index e07ff01201..95f309fbbc 100644 --- a/tests/storage/test_keys.py +++ b/tests/storage/test_keys.py @@ -14,6 +14,7 @@ # limitations under the License. import signedjson.key +import unpaddedbase64 from twisted.internet.defer import Deferred @@ -21,11 +22,17 @@ from synapse.storage.keys import FetchKeyResult import tests.unittest -KEY_1 = signedjson.key.decode_verify_key_base64( - "ed25519", "key1", "fP5l4JzpZPq/zdbBg5xx6lQGAAOM9/3w94cqiJ5jPrw" + +def decode_verify_key_base64(key_id: str, key_base64: str): + key_bytes = unpaddedbase64.decode_base64(key_base64) + return signedjson.key.decode_verify_key_bytes(key_id, key_bytes) + + +KEY_1 = decode_verify_key_base64( + "ed25519:key1", "fP5l4JzpZPq/zdbBg5xx6lQGAAOM9/3w94cqiJ5jPrw" ) -KEY_2 = signedjson.key.decode_verify_key_base64( - "ed25519", "key2", "Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw" +KEY_2 = decode_verify_key_base64( + "ed25519:key2", "Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw" ) From c80a9fe13dc098037a37bb0920a00b2c8cb53174 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 30 Jan 2020 15:06:58 +0000 Subject: [PATCH 0908/1623] When a client asks for remote keys check if should resync. (#6797) If we detect that the remote users' keys may have changed then we should attempt to resync against the remote server rather than using the (potentially) stale local cache. --- changelog.d/6797.misc | 1 + synapse/storage/data_stores/main/devices.py | 32 +++++++++++++++++++-- 2 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 changelog.d/6797.misc diff --git a/changelog.d/6797.misc b/changelog.d/6797.misc new file mode 100644 index 0000000000..e9127bac51 --- /dev/null +++ b/changelog.d/6797.misc @@ -0,0 +1 @@ +When a client asks for a remote user's device keys check if the local cache for that user has been marked as potentially stale. diff --git a/synapse/storage/data_stores/main/devices.py b/synapse/storage/data_stores/main/devices.py index 30bf66b2b6..a34415ff14 100644 --- a/synapse/storage/data_stores/main/devices.py +++ b/synapse/storage/data_stores/main/devices.py @@ -32,7 +32,7 @@ from synapse.logging.opentracing import ( from synapse.metrics.background_process_metrics import run_as_background_process from synapse.storage._base import SQLBaseStore, db_to_json, make_in_list_sql_clause from synapse.storage.database import Database -from synapse.types import get_verify_key_from_cross_signing_key +from synapse.types import Collection, get_verify_key_from_cross_signing_key from synapse.util.caches.descriptors import ( Cache, cached, @@ -443,8 +443,15 @@ class DeviceWorkerStore(SQLBaseStore): """ user_ids = set(user_id for user_id, _ in query_list) user_map = yield self.get_device_list_last_stream_id_for_remotes(list(user_ids)) - user_ids_in_cache = set( - user_id for user_id, stream_id in user_map.items() if stream_id + + # We go and check if any of the users need to have their device lists + # resynced. If they do then we remove them from the cached list. + users_needing_resync = yield self.get_user_ids_requiring_device_list_resync( + user_ids + ) + user_ids_in_cache = ( + set(user_id for user_id, stream_id in user_map.items() if stream_id) + - users_needing_resync ) user_ids_not_in_cache = user_ids - user_ids_in_cache @@ -641,6 +648,25 @@ class DeviceWorkerStore(SQLBaseStore): return results + @defer.inlineCallbacks + def get_user_ids_requiring_device_list_resync(self, user_ids: Collection[str]): + """Given a list of remote users return the list of users that we + should resync the device lists for. + + Returns: + Deferred[Set[str]] + """ + + rows = yield self.db.simple_select_many_batch( + table="device_lists_remote_resync", + column="user_id", + iterable=user_ids, + retcols=("user_id",), + desc="get_user_ids_requiring_device_list_resync", + ) + + return {row["user_id"] for row in rows} + def mark_remote_user_device_cache_as_stale(self, user_id: str): """Records that the server has reason to believe the cache of the devices for the remote users is out of date. From a5bab2d058747eb7165b20808b34c970e34a4b11 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 30 Jan 2020 16:10:30 +0000 Subject: [PATCH 0909/1623] When server leaves room check for stale device lists. (#6801) When a server leaves a room it may stop sharing a room with remote users, and thus not get any updates to their device lists. So we need to check for this case and delete those device lists from the cache. We don't need to do this if we stop sharing a room because the remote user leaves the room, because we track that case via looking at membership changes. --- changelog.d/6801.bugfix | 1 + .../storage/data_stores/main/roommember.py | 37 +++++++++++++- synapse/storage/persist_events.py | 51 +++++++++++++++++-- 3 files changed, 83 insertions(+), 6 deletions(-) create mode 100644 changelog.d/6801.bugfix diff --git a/changelog.d/6801.bugfix b/changelog.d/6801.bugfix new file mode 100644 index 0000000000..f401fa5d69 --- /dev/null +++ b/changelog.d/6801.bugfix @@ -0,0 +1 @@ +Fix bug where Synapse didn't invalidate cache of remote users' devices when Synapse left a room. diff --git a/synapse/storage/data_stores/main/roommember.py b/synapse/storage/data_stores/main/roommember.py index 9acef7c950..042289f0e0 100644 --- a/synapse/storage/data_stores/main/roommember.py +++ b/synapse/storage/data_stores/main/roommember.py @@ -15,7 +15,7 @@ # limitations under the License. import logging -from typing import Iterable, List +from typing import Iterable, List, Set from six import iteritems, itervalues @@ -40,7 +40,7 @@ from synapse.storage.roommember import ( ProfileInfo, RoomsForUser, ) -from synapse.types import get_domain_from_id +from synapse.types import Collection, get_domain_from_id from synapse.util.async_helpers import Linearizer from synapse.util.caches import intern_string from synapse.util.caches.descriptors import cached, cachedInlineCallbacks, cachedList @@ -439,6 +439,39 @@ class RoomMemberWorkerStore(EventsWorkerStore): return results + async def get_users_server_still_shares_room_with( + self, user_ids: Collection[str] + ) -> Set[str]: + """Given a list of users return the set that the server still share a + room with. + """ + + if not user_ids: + return set() + + def _get_users_server_still_shares_room_with_txn(txn): + sql = """ + SELECT state_key FROM current_state_events + WHERE + type = 'm.room.member' + AND membership = 'join' + AND %s + GROUP BY state_key + """ + + clause, args = make_in_list_sql_clause( + self.database_engine, "state_key", user_ids + ) + + txn.execute(sql % (clause,), args) + + return set(row[0] for row in txn) + + return await self.db.runInteraction( + "get_users_server_still_shares_room_with", + _get_users_server_still_shares_room_with_txn, + ) + @defer.inlineCallbacks def get_rooms_for_user(self, user_id, on_invalidate=None): """Returns a set of room_ids the user is currently joined to. diff --git a/synapse/storage/persist_events.py b/synapse/storage/persist_events.py index d060c8b992..86166fd4c1 100644 --- a/synapse/storage/persist_events.py +++ b/synapse/storage/persist_events.py @@ -18,7 +18,7 @@ import itertools import logging from collections import deque, namedtuple -from typing import Iterable, List, Optional, Tuple +from typing import Iterable, List, Optional, Set, Tuple from six import iteritems from six.moves import range @@ -318,6 +318,11 @@ class EventsPersistenceStorage(object): # room state_delta_for_room = {} + # Set of remote users which were in rooms the server has left. We + # should check if we still share any rooms and if not we mark their + # device lists as stale. + potentially_left_users = set() # type: Set[str] + if not backfilled: with Measure(self._clock, "_calculate_state_and_extrem"): # Work out the new "current state" for each room. @@ -421,7 +426,11 @@ class EventsPersistenceStorage(object): # the room then we delete the current state and # extremities. is_still_joined = await self._is_server_still_joined( - room_id, ev_ctx_rm, delta, current_state + room_id, + ev_ctx_rm, + delta, + current_state, + potentially_left_users, ) if not is_still_joined: logger.info("Server no longer in room %s", room_id) @@ -444,6 +453,8 @@ class EventsPersistenceStorage(object): backfilled=backfilled, ) + await self._handle_potentially_left_users(potentially_left_users) + async def _calculate_new_extremities( self, room_id: str, @@ -688,6 +699,7 @@ class EventsPersistenceStorage(object): ev_ctx_rm: List[Tuple[FrozenEvent, EventContext]], delta: DeltaState, current_state: Optional[StateMap[str]], + potentially_left_users: Set[str], ) -> bool: """Check if the server will still be joined after the given events have been persised. @@ -699,6 +711,9 @@ class EventsPersistenceStorage(object): and what the new current state will be. current_state: The new current state if it already been calculated, otherwise None. + potentially_left_users: If the server has left the room, then joined + remote users will be added to this set to indicate that the + server may no longer be sharing a room with them. """ if not any( @@ -741,5 +756,33 @@ class EventsPersistenceStorage(object): is_still_joined = any(row["membership"] == Membership.JOIN for row in rows) if is_still_joined: return True - else: - return False + + # The server will leave the room, so we go and find out which remote + # users will still be joined when we leave. + remote_event_ids = [ + event_id + for (typ, state_key,), event_id in current_state.items() + if typ == EventTypes.Member and not self.is_mine_id(state_key) + ] + rows = await self.main_store.get_membership_from_event_ids(remote_event_ids) + potentially_left_users.update( + row["user_id"] for row in rows if row["membership"] == Membership.JOIN + ) + + return False + + async def _handle_potentially_left_users(self, user_ids: Set[str]): + """Given a set of remote users check if the server still shares a room with + them. If not then mark those users' device cache as stale. + """ + + if not user_ids: + return + + joined_users = await self.main_store.get_users_server_still_shares_room_with( + user_ids + ) + left_users = user_ids - joined_users + + for user_id in left_users: + await self.main_store.mark_remote_user_device_list_as_unsubscribed(user_id) From c3d4ad8afdbe181707451410100dec4817c2c01a Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 30 Jan 2020 16:42:11 +0000 Subject: [PATCH 0910/1623] Fix sending server up commands from workers (#6811) Co-authored-by: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> --- changelog.d/6811.bugfix | 1 + synapse/federation/transport/client.py | 5 ++++- synapse/federation/transport/server.py | 26 +++++++++++++++----------- synapse/replication/tcp/client.py | 4 ++++ synapse/server.pyi | 12 +++++++++++- tox.ini | 1 + 6 files changed, 36 insertions(+), 13 deletions(-) create mode 100644 changelog.d/6811.bugfix diff --git a/changelog.d/6811.bugfix b/changelog.d/6811.bugfix new file mode 100644 index 0000000000..361f2fc2e8 --- /dev/null +++ b/changelog.d/6811.bugfix @@ -0,0 +1 @@ +Fix waking up other workers when remote server is detected to have come back online. diff --git a/synapse/federation/transport/client.py b/synapse/federation/transport/client.py index 198257414b..dc563538de 100644 --- a/synapse/federation/transport/client.py +++ b/synapse/federation/transport/client.py @@ -15,6 +15,7 @@ # limitations under the License. import logging +from typing import Any, Dict from six.moves import urllib @@ -352,7 +353,9 @@ class TransportLayerClient(object): else: path = _create_v1_path("/publicRooms") - args = {"include_all_networks": "true" if include_all_networks else "false"} + args = { + "include_all_networks": "true" if include_all_networks else "false" + } # type: Dict[str, Any] if third_party_instance_id: args["third_party_instance_id"] = (third_party_instance_id,) if limit: diff --git a/synapse/federation/transport/server.py b/synapse/federation/transport/server.py index d8cf9ed299..125eadd796 100644 --- a/synapse/federation/transport/server.py +++ b/synapse/federation/transport/server.py @@ -18,6 +18,7 @@ import functools import logging import re +from typing import Optional, Tuple, Type from twisted.internet.defer import maybeDeferred @@ -267,6 +268,8 @@ class BaseFederationServlet(object): returned. """ + PATH = "" # Overridden in subclasses, the regex to match against the path. + REQUIRE_AUTH = True PREFIX = FEDERATION_V1_PREFIX # Allows specifying the API version @@ -347,9 +350,6 @@ class BaseFederationServlet(object): return response - # Extra logic that functools.wraps() doesn't finish - new_func.__self__ = func.__self__ - return new_func def register(self, server): @@ -824,7 +824,7 @@ class PublicRoomList(BaseFederationServlet): if not self.allow_access: raise FederationDeniedError(origin) - limit = int(content.get("limit", 100)) + limit = int(content.get("limit", 100)) # type: Optional[int] since_token = content.get("since", None) search_filter = content.get("filter", None) @@ -971,7 +971,7 @@ class FederationGroupsAddRoomsConfigServlet(BaseFederationServlet): if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") - result = await self.groups_handler.update_room_in_group( + result = await self.handler.update_room_in_group( group_id, requester_user_id, room_id, config_key, content ) @@ -1422,11 +1422,13 @@ FEDERATION_SERVLET_CLASSES = ( On3pidBindServlet, FederationVersionServlet, RoomComplexityServlet, -) +) # type: Tuple[Type[BaseFederationServlet], ...] -OPENID_SERVLET_CLASSES = (OpenIdUserInfo,) +OPENID_SERVLET_CLASSES = ( + OpenIdUserInfo, +) # type: Tuple[Type[BaseFederationServlet], ...] -ROOM_LIST_CLASSES = (PublicRoomList,) +ROOM_LIST_CLASSES = (PublicRoomList,) # type: Tuple[Type[PublicRoomList], ...] GROUP_SERVER_SERVLET_CLASSES = ( FederationGroupsProfileServlet, @@ -1447,17 +1449,19 @@ GROUP_SERVER_SERVLET_CLASSES = ( FederationGroupsAddRoomsServlet, FederationGroupsAddRoomsConfigServlet, FederationGroupsSettingJoinPolicyServlet, -) +) # type: Tuple[Type[BaseFederationServlet], ...] GROUP_LOCAL_SERVLET_CLASSES = ( FederationGroupsLocalInviteServlet, FederationGroupsRemoveLocalUserServlet, FederationGroupsBulkPublicisedServlet, -) +) # type: Tuple[Type[BaseFederationServlet], ...] -GROUP_ATTESTATION_SERVLET_CLASSES = (FederationGroupsRenewAttestaionServlet,) +GROUP_ATTESTATION_SERVLET_CLASSES = ( + FederationGroupsRenewAttestaionServlet, +) # type: Tuple[Type[BaseFederationServlet], ...] DEFAULT_SERVLET_GROUPS = ( "federation", diff --git a/synapse/replication/tcp/client.py b/synapse/replication/tcp/client.py index fc06a7b053..02ab5b66ea 100644 --- a/synapse/replication/tcp/client.py +++ b/synapse/replication/tcp/client.py @@ -31,6 +31,7 @@ from .commands import ( Command, FederationAckCommand, InvalidateCacheCommand, + RemoteServerUpCommand, RemovePusherCommand, UserIpCommand, UserSyncCommand, @@ -210,6 +211,9 @@ class ReplicationClientHandler(AbstractReplicationClientHandler): cmd = UserIpCommand(user_id, access_token, ip, user_agent, device_id, last_seen) self.send_command(cmd) + def send_remote_server_up(self, server: str): + self.send_command(RemoteServerUpCommand(server)) + def await_sync(self, data): """Returns a deferred that is resolved when we receive a SYNC command with given data. diff --git a/synapse/server.pyi b/synapse/server.pyi index 0731403047..90347ac23e 100644 --- a/synapse/server.pyi +++ b/synapse/server.pyi @@ -2,8 +2,8 @@ import twisted.internet import synapse.api.auth import synapse.config.homeserver +import synapse.crypto.keyring import synapse.federation.sender -import synapse.federation.transaction_queue import synapse.federation.transport.client import synapse.handlers import synapse.handlers.auth @@ -17,6 +17,7 @@ import synapse.handlers.room_member import synapse.handlers.set_password import synapse.http.client import synapse.notifier +import synapse.replication.tcp.client import synapse.rest.media.v1.media_repository import synapse.server_notices.server_notices_manager import synapse.server_notices.server_notices_sender @@ -27,6 +28,9 @@ class HomeServer(object): @property def config(self) -> synapse.config.homeserver.HomeServerConfig: pass + @property + def hostname(self) -> str: + pass def get_auth(self) -> synapse.api.auth.Auth: pass def get_auth_handler(self) -> synapse.handlers.auth.AuthHandler: @@ -97,3 +101,9 @@ class HomeServer(object): pass def get_reactor(self) -> twisted.internet.base.ReactorBase: pass + def get_keyring(self) -> synapse.crypto.keyring.Keyring: + pass + def get_tcp_replication( + self, + ) -> synapse.replication.tcp.client.ReplicationClientHandler: + pass diff --git a/tox.ini b/tox.ini index 1d946a02ba..88ef12bebd 100644 --- a/tox.ini +++ b/tox.ini @@ -179,6 +179,7 @@ extras = all commands = mypy \ synapse/api \ synapse/config/ \ + synapse/federation/transport \ synapse/handlers/ui_auth \ synapse/logging/ \ synapse/module_api \ From b660327056cdced860d532ab2404a26946da7ef5 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 30 Jan 2020 17:06:38 +0000 Subject: [PATCH 0911/1623] Resync remote device list when detected as stale. (#6786) --- changelog.d/6786.misc | 1 + synapse/handlers/devicemessage.py | 10 ++++++++-- synapse/handlers/federation.py | 18 ++++++++++++++++-- tests/handlers/test_typing.py | 6 +++--- 4 files changed, 28 insertions(+), 7 deletions(-) create mode 100644 changelog.d/6786.misc diff --git a/changelog.d/6786.misc b/changelog.d/6786.misc new file mode 100644 index 0000000000..94c692e53a --- /dev/null +++ b/changelog.d/6786.misc @@ -0,0 +1 @@ +Attempt to resync remote users' devices when detected as stale. diff --git a/synapse/handlers/devicemessage.py b/synapse/handlers/devicemessage.py index 5c5fe77be2..05c4b3eec0 100644 --- a/synapse/handlers/devicemessage.py +++ b/synapse/handlers/devicemessage.py @@ -21,6 +21,7 @@ from canonicaljson import json from twisted.internet import defer from synapse.api.errors import SynapseError +from synapse.logging.context import run_in_background from synapse.logging.opentracing import ( get_active_span_text_map, log_kv, @@ -48,6 +49,8 @@ class DeviceMessageHandler(object): "m.direct_to_device", self.on_direct_to_device_edu ) + self._device_list_updater = hs.get_device_handler().device_list_updater + @defer.inlineCallbacks def on_direct_to_device_edu(self, origin, content): local_messages = {} @@ -134,8 +137,11 @@ class DeviceMessageHandler(object): unknown_devices, ) yield self.store.mark_remote_user_device_cache_as_stale(sender_user_id) - # TODO: Poke something to start trying to refetch user's - # keys. + + # Immediately attempt a resync in the background + run_in_background( + self._device_list_updater.user_device_resync, sender_user_id + ) @defer.inlineCallbacks def send_device_message(self, sender_user_id, message_type, messages): diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index a67020a259..ca484e5458 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -57,6 +57,7 @@ from synapse.logging.context import ( run_in_background, ) from synapse.logging.utils import log_function +from synapse.replication.http.devices import ReplicationUserDevicesResyncRestServlet from synapse.replication.http.federation import ( ReplicationCleanRoomRestServlet, ReplicationFederationSendEventsRestServlet, @@ -156,6 +157,13 @@ class FederationHandler(BaseHandler): hs ) + if hs.config.worker_app: + self._user_device_resync = ReplicationUserDevicesResyncRestServlet.make_client( + hs + ) + else: + self._device_list_updater = hs.get_device_handler().device_list_updater + # When joining a room we need to queue any events for that room up self.room_queues = {} self._room_pdu_linearizer = Linearizer("fed_room_pdu") @@ -759,8 +767,14 @@ class FederationHandler(BaseHandler): await self.store.mark_remote_user_device_cache_as_stale( event.sender ) - # TODO: Poke something to start trying to refetch user's - # keys. + + # Immediately attempt a resync in the background + if self.config.worker_app: + return run_in_background(self._user_device_resync, event.sender) + else: + return run_in_background( + self._device_list_updater.user_device_resync, event.sender + ) @log_function async def backfill(self, dest, room_id, limit, extremities): diff --git a/tests/handlers/test_typing.py b/tests/handlers/test_typing.py index 596ddc6970..68b9847bd2 100644 --- a/tests/handlers/test_typing.py +++ b/tests/handlers/test_typing.py @@ -81,6 +81,9 @@ class TypingNotificationsTestCase(unittest.HomeserverTestCase): ] ) + # the tests assume that we are starting at unix time 1000 + reactor.pump((1000,)) + hs = self.setup_test_homeserver( notifier=Mock(), http_client=mock_federation_client, keyring=mock_keyring ) @@ -90,9 +93,6 @@ class TypingNotificationsTestCase(unittest.HomeserverTestCase): return hs def prepare(self, reactor, clock, hs): - # the tests assume that we are starting at unix time 1000 - reactor.pump((1000,)) - mock_notifier = hs.get_notifier() self.on_new_event = mock_notifier.on_new_event From 57ad702af0511aff36ca69fb2e9fc3399cce3a8d Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 30 Jan 2020 17:17:44 +0000 Subject: [PATCH 0912/1623] Backgroud update to clean out rooms from current state (#6802) --- changelog.d/6802.misc | 1 + .../57/delete_old_current_state_events.sql | 19 +++ synapse/storage/data_stores/main/state.py | 108 +++++++++++++++++- 3 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 changelog.d/6802.misc create mode 100644 synapse/storage/data_stores/main/schema/delta/57/delete_old_current_state_events.sql diff --git a/changelog.d/6802.misc b/changelog.d/6802.misc new file mode 100644 index 0000000000..a77ba1d7a5 --- /dev/null +++ b/changelog.d/6802.misc @@ -0,0 +1 @@ +Add background update to clean out left rooms from current state. diff --git a/synapse/storage/data_stores/main/schema/delta/57/delete_old_current_state_events.sql b/synapse/storage/data_stores/main/schema/delta/57/delete_old_current_state_events.sql new file mode 100644 index 0000000000..a133d87a19 --- /dev/null +++ b/synapse/storage/data_stores/main/schema/delta/57/delete_old_current_state_events.sql @@ -0,0 +1,19 @@ +/* Copyright 2020 The Matrix.org Foundation C.I.C + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +-- Add background update to go and delete current state events for rooms the +-- server is no longer in. +INSERT into background_updates (update_name, progress_json) + VALUES ('delete_old_current_state_events', '{}'); diff --git a/synapse/storage/data_stores/main/state.py b/synapse/storage/data_stores/main/state.py index bd7b0276f1..9b6f68e777 100644 --- a/synapse/storage/data_stores/main/state.py +++ b/synapse/storage/data_stores/main/state.py @@ -21,12 +21,13 @@ from six import iteritems from twisted.internet import defer -from synapse.api.constants import EventTypes +from synapse.api.constants import EventTypes, Membership from synapse.api.errors import NotFoundError from synapse.events import EventBase from synapse.events.snapshot import EventContext from synapse.storage._base import SQLBaseStore from synapse.storage.data_stores.main.events_worker import EventsWorkerStore +from synapse.storage.data_stores.main.roommember import RoomMemberWorkerStore from synapse.storage.database import Database from synapse.storage.state import StateFilter from synapse.util.caches import intern_string @@ -300,14 +301,17 @@ class StateGroupWorkerStore(EventsWorkerStore, SQLBaseStore): return set(row["state_group"] for row in rows) -class MainStateBackgroundUpdateStore(SQLBaseStore): +class MainStateBackgroundUpdateStore(RoomMemberWorkerStore): CURRENT_STATE_INDEX_UPDATE_NAME = "current_state_members_idx" EVENT_STATE_GROUP_INDEX_UPDATE_NAME = "event_to_state_groups_sg_index" + DELETE_CURRENT_STATE_UPDATE_NAME = "delete_old_current_state_events" def __init__(self, database: Database, db_conn, hs): super(MainStateBackgroundUpdateStore, self).__init__(database, db_conn, hs) + self.server_name = hs.hostname + self.db.updates.register_background_index_update( self.CURRENT_STATE_INDEX_UPDATE_NAME, index_name="current_state_events_member_index", @@ -321,6 +325,106 @@ class MainStateBackgroundUpdateStore(SQLBaseStore): table="event_to_state_groups", columns=["state_group"], ) + self.db.updates.register_background_update_handler( + self.DELETE_CURRENT_STATE_UPDATE_NAME, self._background_remove_left_rooms, + ) + + async def _background_remove_left_rooms(self, progress, batch_size): + """Background update to delete rows from `current_state_events` and + `event_forward_extremities` tables of rooms that the server is no + longer joined to. + """ + + last_room_id = progress.get("last_room_id", "") + + def _background_remove_left_rooms_txn(txn): + sql = """ + SELECT DISTINCT room_id FROM current_state_events + WHERE room_id > ? ORDER BY room_id LIMIT ? + """ + + txn.execute(sql, (last_room_id, batch_size)) + room_ids = list(row[0] for row in txn) + if not room_ids: + return True, set() + + sql = """ + SELECT room_id + FROM current_state_events + WHERE + room_id > ? AND room_id <= ? + AND type = 'm.room.member' + AND membership = 'join' + AND state_key LIKE ? + GROUP BY room_id + """ + + txn.execute(sql, (last_room_id, room_ids[-1], "%:" + self.server_name)) + + joined_room_ids = set(row[0] for row in txn) + + left_rooms = set(room_ids) - joined_room_ids + + # First we get all users that we still think were joined to the + # room. This is so that we can mark those device lists as + # potentially stale, since there may have been a period where the + # server didn't share a room with the remote user and therefore may + # have missed any device updates. + rows = self.db.simple_select_many_txn( + txn, + table="current_state_events", + column="room_id", + iterable=left_rooms, + keyvalues={"type": EventTypes.Member, "membership": Membership.JOIN}, + retcols=("state_key",), + ) + + potentially_left_users = set(row["state_key"] for row in rows) + + # Now lets actually delete the rooms from the DB. + self.db.simple_delete_many_txn( + txn, + table="current_state_events", + column="room_id", + iterable=left_rooms, + keyvalues={}, + ) + + self.db.simple_delete_many_txn( + txn, + table="event_forward_extremities", + column="room_id", + iterable=left_rooms, + keyvalues={}, + ) + + self.db.updates._background_update_progress_txn( + txn, + self.DELETE_CURRENT_STATE_UPDATE_NAME, + {"last_room_id": room_ids[-1]}, + ) + + return False, potentially_left_users + + finished, potentially_left_users = await self.db.runInteraction( + "_background_remove_left_rooms", _background_remove_left_rooms_txn + ) + + if finished: + await self.db.updates._end_background_update( + self.DELETE_CURRENT_STATE_UPDATE_NAME + ) + + # Now go and check if we still share a room with the remote users in + # the deleted rooms. If not mark their device lists as stale. + joined_users = await self.get_users_server_still_shares_room_with( + potentially_left_users + ) + + for user_id in potentially_left_users - joined_users: + await self.mark_remote_user_device_list_as_unsubscribed(user_id) + + return batch_size class StateStore(StateGroupWorkerStore, MainStateBackgroundUpdateStore): From 184303b8650a90256f84bc9801b749a5b81b6d4b Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 30 Jan 2020 17:20:55 +0000 Subject: [PATCH 0913/1623] MSC2260: Block direct sends of m.room.aliases events (#6794) as per MSC2260 --- changelog.d/6794.feature | 1 + synapse/rest/client/v1/room.py | 12 ++++++++ tests/rest/admin/test_admin.py | 7 ----- tests/rest/client/v1/test_directory.py | 41 ++++++++++---------------- 4 files changed, 28 insertions(+), 33 deletions(-) create mode 100644 changelog.d/6794.feature diff --git a/changelog.d/6794.feature b/changelog.d/6794.feature new file mode 100644 index 0000000000..df9e4b77ab --- /dev/null +++ b/changelog.d/6794.feature @@ -0,0 +1 @@ +Implement updated authorization rules for aliases events, from [MSC2260](https://github.com/matrix-org/matrix-doc/pull/2260). diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index 5aef8238b8..6f31584c51 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -184,6 +184,12 @@ class RoomStateEventRestServlet(TransactionRestServlet): content = parse_json_object_from_request(request) + if event_type == EventTypes.Aliases: + # MSC2260 + raise SynapseError( + 400, "Cannot send m.room.aliases events via /rooms/{room_id}/state" + ) + event_dict = { "type": event_type, "content": content, @@ -231,6 +237,12 @@ class RoomSendEventRestServlet(TransactionRestServlet): requester = await self.auth.get_user_by_req(request, allow_guest=True) content = parse_json_object_from_request(request) + if event_type == EventTypes.Aliases: + # MSC2260 + raise SynapseError( + 400, "Cannot send m.room.aliases events via /rooms/{room_id}/send" + ) + event_dict = { "type": event_type, "content": content, diff --git a/tests/rest/admin/test_admin.py b/tests/rest/admin/test_admin.py index 0342aed416..e5984aaad8 100644 --- a/tests/rest/admin/test_admin.py +++ b/tests/rest/admin/test_admin.py @@ -868,13 +868,6 @@ class RoomTestCase(unittest.HomeserverTestCase): self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) # Set this new alias as the canonical alias for this room - self.helper.send_state( - room_id, - "m.room.aliases", - {"aliases": [test_alias]}, - tok=self.admin_user_tok, - state_key="test", - ) self.helper.send_state( room_id, "m.room.canonical_alias", diff --git a/tests/rest/client/v1/test_directory.py b/tests/rest/client/v1/test_directory.py index 633b7dbda0..914cf54927 100644 --- a/tests/rest/client/v1/test_directory.py +++ b/tests/rest/client/v1/test_directory.py @@ -51,26 +51,30 @@ class DirectoryTestCase(unittest.HomeserverTestCase): self.user = self.register_user("user", "test") self.user_tok = self.login("user", "test") - def test_state_event_not_in_room(self): - self.ensure_user_left_room() - self.set_alias_via_state_event(403) + def test_cannot_set_alias_via_state_event(self): + self.ensure_user_joined_room() + url = "/_matrix/client/r0/rooms/%s/state/m.room.aliases/%s" % ( + self.room_id, + self.hs.hostname, + ) + + data = {"aliases": [self.random_alias(5)]} + request_data = json.dumps(data) + + request, channel = self.make_request( + "PUT", url, request_data, access_token=self.user_tok + ) + self.render(request) + self.assertEqual(channel.code, 400, channel.result) def test_directory_endpoint_not_in_room(self): self.ensure_user_left_room() self.set_alias_via_directory(403) - def test_state_event_in_room_too_long(self): - self.ensure_user_joined_room() - self.set_alias_via_state_event(400, alias_length=256) - def test_directory_in_room_too_long(self): self.ensure_user_joined_room() self.set_alias_via_directory(400, alias_length=256) - def test_state_event_in_room(self): - self.ensure_user_joined_room() - self.set_alias_via_state_event(200) - def test_directory_in_room(self): self.ensure_user_joined_room() self.set_alias_via_directory(200) @@ -102,21 +106,6 @@ class DirectoryTestCase(unittest.HomeserverTestCase): self.render(request) self.assertEqual(channel.code, 200, channel.result) - def set_alias_via_state_event(self, expected_code, alias_length=5): - url = "/_matrix/client/r0/rooms/%s/state/m.room.aliases/%s" % ( - self.room_id, - self.hs.hostname, - ) - - data = {"aliases": [self.random_alias(alias_length)]} - request_data = json.dumps(data) - - request, channel = self.make_request( - "PUT", url, request_data, access_token=self.user_tok - ) - self.render(request) - self.assertEqual(channel.code, expected_code, channel.result) - def set_alias_via_directory(self, expected_code, alias_length=5): url = "/_matrix/client/r0/directory/room/%s" % self.random_alias(alias_length) data = {"room_id": self.room_id} From e0992fcc5be9e850a5007d1d09fea79bea949cf6 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 30 Jan 2020 17:55:34 +0000 Subject: [PATCH 0914/1623] Log when we delete room in bg update (#6816) --- changelog.d/6816.misc | 1 + synapse/storage/data_stores/main/state.py | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 changelog.d/6816.misc diff --git a/changelog.d/6816.misc b/changelog.d/6816.misc new file mode 100644 index 0000000000..a77ba1d7a5 --- /dev/null +++ b/changelog.d/6816.misc @@ -0,0 +1 @@ +Add background update to clean out left rooms from current state. diff --git a/synapse/storage/data_stores/main/state.py b/synapse/storage/data_stores/main/state.py index 9b6f68e777..4167f83c9b 100644 --- a/synapse/storage/data_stores/main/state.py +++ b/synapse/storage/data_stores/main/state.py @@ -365,6 +365,8 @@ class MainStateBackgroundUpdateStore(RoomMemberWorkerStore): left_rooms = set(room_ids) - joined_room_ids + logger.info("Deleting current state left rooms: %r", left_rooms) + # First we get all users that we still think were joined to the # room. This is so that we can mark those device lists as # potentially stale, since there may have been a period where the From 46a446828d1b4b1ca2d9b0dcae97323a1bbc0c0b Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 30 Jan 2020 22:13:02 +0000 Subject: [PATCH 0915/1623] pass room version into FederationHandler.on_invite_request (#6805) --- changelog.d/6805.misc | 1 + synapse/federation/federation_server.py | 2 +- synapse/handlers/federation.py | 6 +++--- 3 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 changelog.d/6805.misc diff --git a/changelog.d/6805.misc b/changelog.d/6805.misc new file mode 100644 index 0000000000..08aa80bcd9 --- /dev/null +++ b/changelog.d/6805.misc @@ -0,0 +1 @@ +Refactoring work in preparation for changing the event redaction algorithm. diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index 8eddb3bf2c..9562faa3ee 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -410,7 +410,7 @@ class FederationServer(FederationBase): origin_host, _ = parse_server_name(origin) await self.check_server_matches_acl(origin_host, pdu.room_id) pdu = await self._check_sigs_and_hash(room_version, pdu) - ret_pdu = await self.handler.on_invite_request(origin, pdu) + ret_pdu = await self.handler.on_invite_request(origin, pdu, room_version) time_now = self._clock.time_msec() return {"event": ret_pdu.get_pdu_json(time_now)} diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index ca484e5458..01372f6d47 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1482,13 +1482,13 @@ class FederationHandler(BaseHandler): return {"state": list(state.values()), "auth_chain": auth_chain} @defer.inlineCallbacks - def on_invite_request(self, origin, pdu): + def on_invite_request( + self, origin: str, event: EventBase, room_version: RoomVersion + ): """ We've got an invite event. Process and persist it. Sign it. Respond with the now signed event. """ - event = pdu - if event.state_key is None: raise SynapseError(400, "The invite event did not have a state key") From ef6bdafb29abe14cb40f6b83a46e70d82cd3e041 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 29 Jan 2020 17:55:48 +0000 Subject: [PATCH 0916/1623] Store the room version in EventBuilder --- synapse/events/builder.py | 12 +++++++----- tests/handlers/test_presence.py | 4 ++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/synapse/events/builder.py b/synapse/events/builder.py index 3997751337..291fb38a26 100644 --- a/synapse/events/builder.py +++ b/synapse/events/builder.py @@ -23,6 +23,7 @@ from synapse.api.room_versions import ( KNOWN_EVENT_FORMAT_VERSIONS, KNOWN_ROOM_VERSIONS, EventFormatVersions, + RoomVersion, ) from synapse.crypto.event_signing import add_hashes_and_signatures from synapse.types import EventID @@ -40,7 +41,7 @@ class EventBuilder(object): content/unsigned/internal_metadata fields are still mutable) Attributes: - format_version (int): Event format version + room_version: Version of the target room room_id (str) type (str) sender (str) @@ -63,7 +64,7 @@ class EventBuilder(object): _hostname = attr.ib() _signing_key = attr.ib() - format_version = attr.ib() + room_version = attr.ib(type=RoomVersion) room_id = attr.ib() type = attr.ib() @@ -108,7 +109,8 @@ class EventBuilder(object): ) auth_ids = yield self._auth.compute_auth_events(self, state_ids) - if self.format_version == EventFormatVersions.V1: + format_version = self.room_version.event_format + if format_version == EventFormatVersions.V1: auth_events = yield self._store.add_event_hashes(auth_ids) prev_events = yield self._store.add_event_hashes(prev_event_ids) else: @@ -148,7 +150,7 @@ class EventBuilder(object): clock=self._clock, hostname=self._hostname, signing_key=self._signing_key, - format_version=self.format_version, + format_version=format_version, event_dict=event_dict, internal_metadata_dict=self.internal_metadata.get_dict(), ) @@ -201,7 +203,7 @@ class EventBuilderFactory(object): clock=self.clock, hostname=self.hostname, signing_key=self.signing_key, - format_version=room_version.event_format, + room_version=room_version, type=key_values["type"], state_key=key_values.get("state_key"), room_id=key_values["room_id"], diff --git a/tests/handlers/test_presence.py b/tests/handlers/test_presence.py index d4293b4312..69914428e2 100644 --- a/tests/handlers/test_presence.py +++ b/tests/handlers/test_presence.py @@ -19,7 +19,7 @@ from mock import Mock, call from signedjson.key import generate_signing_key from synapse.api.constants import EventTypes, Membership, PresenceState -from synapse.events import room_version_to_event_format +from synapse.api.room_versions import KNOWN_ROOM_VERSIONS from synapse.events.builder import EventBuilder from synapse.handlers.presence import ( EXTERNAL_PROCESS_EXPIRY, @@ -597,7 +597,7 @@ class PresenceJoinTestCase(unittest.HomeserverTestCase): clock=self.clock, hostname=hostname, signing_key=self.random_signing_key, - format_version=room_version_to_event_format(room_version), + room_version=KNOWN_ROOM_VERSIONS[room_version], room_id=room_id, type=EventTypes.Member, sender=user_id, From 54f3f369bd94ce22b3e052d4b795ad5d0c4618bc Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 29 Jan 2020 17:58:01 +0000 Subject: [PATCH 0917/1623] Pass room_version into create_local_event_from_event_dict --- synapse/events/builder.py | 40 +++++++++++-------------- synapse/federation/federation_client.py | 4 +-- 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/synapse/events/builder.py b/synapse/events/builder.py index 291fb38a26..a26f4c9044 100644 --- a/synapse/events/builder.py +++ b/synapse/events/builder.py @@ -12,8 +12,10 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from typing import Optional import attr +from nacl.signing import SigningKey from twisted.internet import defer @@ -26,11 +28,15 @@ from synapse.api.room_versions import ( RoomVersion, ) from synapse.crypto.event_signing import add_hashes_and_signatures -from synapse.types import EventID +from synapse.events import ( + EventBase, + _EventInternalMetadata, + event_type_from_format_version, +) +from synapse.types import EventID, JsonDict +from synapse.util import Clock from synapse.util.stringutils import random_string -from . import _EventInternalMetadata, event_type_from_format_version - @attr.s(slots=True, cmp=False, frozen=True) class EventBuilder(object): @@ -150,7 +156,7 @@ class EventBuilder(object): clock=self._clock, hostname=self._hostname, signing_key=self._signing_key, - format_version=format_version, + room_version=self.room_version, event_dict=event_dict, internal_metadata_dict=self.internal_metadata.get_dict(), ) @@ -216,29 +222,19 @@ class EventBuilderFactory(object): def create_local_event_from_event_dict( - clock, - hostname, - signing_key, - format_version, - event_dict, - internal_metadata_dict=None, -): + clock: Clock, + hostname: str, + signing_key: SigningKey, + room_version: RoomVersion, + event_dict: JsonDict, + internal_metadata_dict: Optional[JsonDict] = None, +) -> EventBase: """Takes a fully formed event dict, ensuring that fields like `origin` and `origin_server_ts` have correct values for a locally produced event, then signs and hashes it. - - Args: - clock (Clock) - hostname (str) - signing_key - format_version (int) - event_dict (dict) - internal_metadata_dict (dict|None) - - Returns: - FrozenEvent """ + format_version = room_version.event_format if format_version not in KNOWN_EVENT_FORMAT_VERSIONS: raise Exception("No event format defined for version %r" % (format_version,)) diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index d57e8ca7a2..9be4b69cad 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -470,8 +470,6 @@ class FederationClient(FederationBase): if not room_version: raise UnsupportedRoomVersionError() - event_format = room_version_to_event_format(room_version_id) - pdu_dict = ret.get("event", None) if not isinstance(pdu_dict, dict): raise InvalidResponseError("Bad 'event' field in response") @@ -490,7 +488,7 @@ class FederationClient(FederationBase): self._clock, self.hostname, self.signing_key, - format_version=event_format, + room_version=room_version, event_dict=pdu_dict, ) From 2a81393a4b905c8bd4c31da04a8b4407462948b9 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 29 Jan 2020 17:40:33 +0000 Subject: [PATCH 0918/1623] Pass room_version into add_hashes_and_signatures --- synapse/crypto/event_signing.py | 20 +++++++++++++------- synapse/events/builder.py | 2 +- tests/crypto/test_event_signing.py | 9 +++++++-- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/synapse/crypto/event_signing.py b/synapse/crypto/event_signing.py index e65bd61d97..1f2bccf700 100644 --- a/synapse/crypto/event_signing.py +++ b/synapse/crypto/event_signing.py @@ -20,10 +20,13 @@ import logging from canonicaljson import encode_canonical_json from signedjson.sign import sign_json +from signedjson.types import SigningKey from unpaddedbase64 import decode_base64, encode_base64 from synapse.api.errors import Codes, SynapseError +from synapse.api.room_versions import RoomVersion from synapse.events.utils import prune_event, prune_event_dict +from synapse.types import JsonDict logger = logging.getLogger(__name__) @@ -137,20 +140,23 @@ def compute_event_signature(event_dict, signature_name, signing_key): def add_hashes_and_signatures( - event_dict, signature_name, signing_key, hash_algorithm=hashlib.sha256 + room_version: RoomVersion, + event_dict: JsonDict, + signature_name: str, + signing_key: SigningKey, ): """Add content hash and sign the event Args: - event_dict (dict): The event to add hashes to and sign - signature_name (str): The name of the entity signing the event + room_version: the version of the room this event is in + + event_dict: The event to add hashes to and sign + signature_name: The name of the entity signing the event (typically the server's hostname). - signing_key (syutil.crypto.SigningKey): The key to sign with - hash_algorithm: A hasher from `hashlib`, e.g. hashlib.sha256, to use - to hash the event + signing_key: The key to sign with """ - name, digest = compute_content_hash(event_dict, hash_algorithm=hash_algorithm) + name, digest = compute_content_hash(event_dict, hash_algorithm=hashlib.sha256) event_dict.setdefault("hashes", {})[name] = encode_base64(digest) diff --git a/synapse/events/builder.py b/synapse/events/builder.py index a26f4c9044..8d63ad6dc3 100644 --- a/synapse/events/builder.py +++ b/synapse/events/builder.py @@ -255,7 +255,7 @@ def create_local_event_from_event_dict( event_dict.setdefault("signatures", {}) - add_hashes_and_signatures(event_dict, hostname, signing_key) + add_hashes_and_signatures(room_version, event_dict, hostname, signing_key) return event_type_from_format_version(format_version)( event_dict, internal_metadata_dict=internal_metadata_dict ) diff --git a/tests/crypto/test_event_signing.py b/tests/crypto/test_event_signing.py index 126e176004..6143a50ab2 100644 --- a/tests/crypto/test_event_signing.py +++ b/tests/crypto/test_event_signing.py @@ -17,6 +17,7 @@ import nacl.signing from unpaddedbase64 import decode_base64 +from synapse.api.room_versions import RoomVersions from synapse.crypto.event_signing import add_hashes_and_signatures from synapse.events import FrozenEvent @@ -49,7 +50,9 @@ class EventSigningTestCase(unittest.TestCase): "unsigned": {"age_ts": 1000000}, } - add_hashes_and_signatures(event_dict, HOSTNAME, self.signing_key) + add_hashes_and_signatures( + RoomVersions.V1, event_dict, HOSTNAME, self.signing_key + ) event = FrozenEvent(event_dict) @@ -81,7 +84,9 @@ class EventSigningTestCase(unittest.TestCase): "unsigned": {"age_ts": 1000000}, } - add_hashes_and_signatures(event_dict, HOSTNAME, self.signing_key) + add_hashes_and_signatures( + RoomVersions.V1, event_dict, HOSTNAME, self.signing_key + ) event = FrozenEvent(event_dict) From 540c5e168b3f7f22d7af905d6d01dcf2a615dff3 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 29 Jan 2020 18:19:06 +0000 Subject: [PATCH 0919/1623] changelog --- changelog.d/6806.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6806.misc diff --git a/changelog.d/6806.misc b/changelog.d/6806.misc new file mode 100644 index 0000000000..08aa80bcd9 --- /dev/null +++ b/changelog.d/6806.misc @@ -0,0 +1 @@ +Refactoring work in preparation for changing the event redaction algorithm. From 7d846e870422c65f3fb436e5b0e543dae17719fc Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 31 Jan 2020 09:49:13 +0000 Subject: [PATCH 0920/1623] Fix bug with getting missing auth event during join 500'ed (#6810) --- changelog.d/6810.misc | 1 + synapse/handlers/federation.py | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changelog.d/6810.misc diff --git a/changelog.d/6810.misc b/changelog.d/6810.misc new file mode 100644 index 0000000000..5537355bea --- /dev/null +++ b/changelog.d/6810.misc @@ -0,0 +1 @@ +Record room versions in the `rooms` table. diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 01372f6d47..1f92640f86 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1929,7 +1929,11 @@ class FederationHandler(BaseHandler): for e_id in missing_auth_events: m_ev = yield self.federation_client.get_pdu( - [origin], e_id, room_version=room_version, outlier=True, timeout=10000 + [origin], + e_id, + room_version=room_version.identifier, + outlier=True, + timeout=10000, ) if m_ev and m_ev.event_id == e_id: event_map[e_id] = m_ev From d7bf793cc1e3f5268285286341835ac54753eff6 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 31 Jan 2020 10:06:21 +0000 Subject: [PATCH 0921/1623] s/get_room_version/get_room_version_id/ ... to make way for a forthcoming get_room_version which returns a RoomVersion object. --- synapse/federation/federation_client.py | 10 +++++----- synapse/federation/federation_server.py | 16 ++++++++-------- synapse/handlers/federation.py | 18 +++++++++--------- synapse/handlers/message.py | 8 +++++--- synapse/handlers/pagination.py | 2 +- synapse/handlers/room.py | 2 +- synapse/state/__init__.py | 2 +- synapse/storage/data_stores/main/state.py | 2 +- synapse/storage/persist_events.py | 2 +- tests/handlers/test_presence.py | 2 +- tests/test_state.py | 2 +- tests/unittest.py | 4 +++- 12 files changed, 37 insertions(+), 33 deletions(-) diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index d57e8ca7a2..4ac3d81cba 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -198,7 +198,7 @@ class FederationClient(FederationBase): logger.debug("backfill transaction_data=%r", transaction_data) - room_version = yield self.store.get_room_version(room_id) + room_version = yield self.store.get_room_version_id(room_id) format_ver = room_version_to_event_format(room_version) pdus = [ @@ -336,7 +336,7 @@ class FederationClient(FederationBase): def get_event_auth(self, destination, room_id, event_id): res = yield self.transport_layer.get_event_auth(destination, room_id, event_id) - room_version = yield self.store.get_room_version(room_id) + room_version = yield self.store.get_room_version_id(room_id) format_ver = room_version_to_event_format(room_version) auth_chain = [ @@ -649,7 +649,7 @@ class FederationClient(FederationBase): @defer.inlineCallbacks def send_invite(self, destination, room_id, event_id, pdu): - room_version = yield self.store.get_room_version(room_id) + room_version = yield self.store.get_room_version_id(room_id) content = yield self._do_send_invite(destination, pdu, room_version) @@ -657,7 +657,7 @@ class FederationClient(FederationBase): logger.debug("Got response to send_invite: %s", pdu_dict) - room_version = yield self.store.get_room_version(room_id) + room_version = yield self.store.get_room_version_id(room_id) format_ver = room_version_to_event_format(room_version) pdu = event_from_pdu_json(pdu_dict, format_ver) @@ -859,7 +859,7 @@ class FederationClient(FederationBase): timeout=timeout, ) - room_version = yield self.store.get_room_version(room_id) + room_version = yield self.store.get_room_version_id(room_id) format_ver = room_version_to_event_format(room_version) events = [ diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index 9562faa3ee..a4c97ed458 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -234,7 +234,7 @@ class FederationServer(FederationBase): continue try: - room_version = await self.store.get_room_version(room_id) + room_version = await self.store.get_room_version_id(room_id) except NotFoundError: logger.info("Ignoring PDU for unknown room_id: %s", room_id) continue @@ -334,7 +334,7 @@ class FederationServer(FederationBase): ) ) - room_version = await self.store.get_room_version(room_id) + room_version = await self.store.get_room_version_id(room_id) resp["room_version"] = room_version return 200, resp @@ -385,7 +385,7 @@ class FederationServer(FederationBase): origin_host, _ = parse_server_name(origin) await self.check_server_matches_acl(origin_host, room_id) - room_version = await self.store.get_room_version(room_id) + room_version = await self.store.get_room_version_id(room_id) if room_version not in supported_versions: logger.warning( "Room version %s not in %s", room_version, supported_versions @@ -417,7 +417,7 @@ class FederationServer(FederationBase): async def on_send_join_request(self, origin, content, room_id): logger.debug("on_send_join_request: content: %s", content) - room_version = await self.store.get_room_version(room_id) + room_version = await self.store.get_room_version_id(room_id) format_ver = room_version_to_event_format(room_version) pdu = event_from_pdu_json(content, format_ver) @@ -440,7 +440,7 @@ class FederationServer(FederationBase): await self.check_server_matches_acl(origin_host, room_id) pdu = await self.handler.on_make_leave_request(origin, room_id, user_id) - room_version = await self.store.get_room_version(room_id) + room_version = await self.store.get_room_version_id(room_id) time_now = self._clock.time_msec() return {"event": pdu.get_pdu_json(time_now), "room_version": room_version} @@ -448,7 +448,7 @@ class FederationServer(FederationBase): async def on_send_leave_request(self, origin, content, room_id): logger.debug("on_send_leave_request: content: %s", content) - room_version = await self.store.get_room_version(room_id) + room_version = await self.store.get_room_version_id(room_id) format_ver = room_version_to_event_format(room_version) pdu = event_from_pdu_json(content, format_ver) @@ -495,7 +495,7 @@ class FederationServer(FederationBase): origin_host, _ = parse_server_name(origin) await self.check_server_matches_acl(origin_host, room_id) - room_version = await self.store.get_room_version(room_id) + room_version = await self.store.get_room_version_id(room_id) format_ver = room_version_to_event_format(room_version) auth_chain = [ @@ -664,7 +664,7 @@ class FederationServer(FederationBase): logger.info("Accepting join PDU %s from %s", pdu.event_id, origin) # We've already checked that we know the room version by this point - room_version = await self.store.get_room_version(pdu.room_id) + room_version = await self.store.get_room_version_id(pdu.room_id) # Check signature. try: diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 01372f6d47..30c720f093 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -388,7 +388,7 @@ class FederationHandler(BaseHandler): for x in remote_state: event_map[x.event_id] = x - room_version = await self.store.get_room_version(room_id) + room_version = await self.store.get_room_version_id(room_id) state_map = await resolve_events_with_store( room_id, room_version, @@ -1110,7 +1110,7 @@ class FederationHandler(BaseHandler): Logs a warning if we can't find the given event. """ - room_version = await self.store.get_room_version(room_id) + room_version = await self.store.get_room_version_id(room_id) event_infos = [] @@ -1373,7 +1373,7 @@ class FederationHandler(BaseHandler): event_content = {"membership": Membership.JOIN} - room_version = yield self.store.get_room_version(room_id) + room_version = yield self.store.get_room_version_id(room_id) builder = self.event_builder_factory.new( room_version, @@ -1607,7 +1607,7 @@ class FederationHandler(BaseHandler): ) raise SynapseError(403, "User not from origin", Codes.FORBIDDEN) - room_version = yield self.store.get_room_version(room_id) + room_version = yield self.store.get_room_version_id(room_id) builder = self.event_builder_factory.new( room_version, { @@ -2055,7 +2055,7 @@ class FederationHandler(BaseHandler): do_soft_fail_check = False if do_soft_fail_check: - room_version = yield self.store.get_room_version(event.room_id) + room_version = yield self.store.get_room_version_id(event.room_id) room_version_obj = KNOWN_ROOM_VERSIONS[room_version] # Calculate the "current state". @@ -2191,7 +2191,7 @@ class FederationHandler(BaseHandler): Returns: defer.Deferred[EventContext]: updated context object """ - room_version = yield self.store.get_room_version(event.room_id) + room_version = yield self.store.get_room_version_id(event.room_id) room_version_obj = KNOWN_ROOM_VERSIONS[room_version] try: @@ -2363,7 +2363,7 @@ class FederationHandler(BaseHandler): remote_auth_events.update({(d.type, d.state_key): d for d in different_events}) remote_state = remote_auth_events.values() - room_version = yield self.store.get_room_version(event.room_id) + room_version = yield self.store.get_room_version_id(event.room_id) new_state = yield self.state_handler.resolve_events( room_version, (local_state, remote_state), event ) @@ -2587,7 +2587,7 @@ class FederationHandler(BaseHandler): } if (yield self.auth.check_host_in_room(room_id, self.hs.hostname)): - room_version = yield self.store.get_room_version(room_id) + room_version = yield self.store.get_room_version_id(room_id) builder = self.event_builder_factory.new(room_version, event_dict) EventValidator().validate_builder(builder) @@ -2650,7 +2650,7 @@ class FederationHandler(BaseHandler): Returns: Deferred: resolves (to None) """ - room_version = yield self.store.get_room_version(room_id) + room_version = yield self.store.get_room_version_id(room_id) # NB: event_dict has a particular specced format we might need to fudge # if we change event formats too much. diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index 9a0f661b9b..bdf16c84d3 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -459,7 +459,9 @@ class EventCreationHandler(object): room_version = event_dict["content"]["room_version"] else: try: - room_version = yield self.store.get_room_version(event_dict["room_id"]) + room_version = yield self.store.get_room_version_id( + event_dict["room_id"] + ) except NotFoundError: raise AuthError(403, "Unknown room") @@ -788,7 +790,7 @@ class EventCreationHandler(object): ): room_version = event.content.get("room_version", RoomVersions.V1.identifier) else: - room_version = yield self.store.get_room_version(event.room_id) + room_version = yield self.store.get_room_version_id(event.room_id) event_allowed = yield self.third_party_event_rules.check_event_allowed( event, context @@ -963,7 +965,7 @@ class EventCreationHandler(object): auth_events = yield self.store.get_events(auth_events_ids) auth_events = {(e.type, e.state_key): e for e in auth_events.values()} - room_version = yield self.store.get_room_version(event.room_id) + room_version = yield self.store.get_room_version_id(event.room_id) room_version_obj = KNOWN_ROOM_VERSIONS[room_version] if event_auth.check_redaction( diff --git a/synapse/handlers/pagination.py b/synapse/handlers/pagination.py index 71d76202c9..caf841a643 100644 --- a/synapse/handlers/pagination.py +++ b/synapse/handlers/pagination.py @@ -281,7 +281,7 @@ class PaginationHandler(object): """Purge the given room from the database""" with (await self.pagination_lock.write(room_id)): # check we know about the room - await self.store.get_room_version(room_id) + await self.store.get_room_version_id(room_id) # first check that we have no users in this room joined = await defer.maybeDeferred( diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index a95b45d791..1382399557 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -178,7 +178,7 @@ class RoomCreationHandler(BaseHandler): }, token_id=requester.access_token_id, ) - old_room_version = yield self.store.get_room_version(old_room_id) + old_room_version = yield self.store.get_room_version_id(old_room_id) yield self.auth.check_from_context( old_room_version, tombstone_event, tombstone_context ) diff --git a/synapse/state/__init__.py b/synapse/state/__init__.py index cacd0c0c2b..fdd6bef6b4 100644 --- a/synapse/state/__init__.py +++ b/synapse/state/__init__.py @@ -394,7 +394,7 @@ class StateHandler(object): delta_ids=delta_ids, ) - room_version = yield self.store.get_room_version(room_id) + room_version = yield self.store.get_room_version_id(room_id) result = yield self._state_resolution_handler.resolve_state_groups( room_id, diff --git a/synapse/storage/data_stores/main/state.py b/synapse/storage/data_stores/main/state.py index 4167f83c9b..6700942523 100644 --- a/synapse/storage/data_stores/main/state.py +++ b/synapse/storage/data_stores/main/state.py @@ -62,7 +62,7 @@ class StateGroupWorkerStore(EventsWorkerStore, SQLBaseStore): super(StateGroupWorkerStore, self).__init__(database, db_conn, hs) @cached(max_entries=10000) - async def get_room_version(self, room_id: str) -> str: + async def get_room_version_id(self, room_id: str) -> str: """Get the room_version of a given room Raises: diff --git a/synapse/storage/persist_events.py b/synapse/storage/persist_events.py index 86166fd4c1..af3fd67ab9 100644 --- a/synapse/storage/persist_events.py +++ b/synapse/storage/persist_events.py @@ -661,7 +661,7 @@ class EventsPersistenceStorage(object): break if not room_version: - room_version = await self.main_store.get_room_version(room_id) + room_version = await self.main_store.get_room_version_id(room_id) logger.debug("calling resolve_state_groups from preserve_events") res = await self._state_resolution_handler.resolve_state_groups( diff --git a/tests/handlers/test_presence.py b/tests/handlers/test_presence.py index d4293b4312..e92e090c3c 100644 --- a/tests/handlers/test_presence.py +++ b/tests/handlers/test_presence.py @@ -588,7 +588,7 @@ class PresenceJoinTestCase(unittest.HomeserverTestCase): hostname = get_domain_from_id(user_id) - room_version = self.get_success(self.store.get_room_version(room_id)) + room_version = self.get_success(self.store.get_room_version_id(room_id)) builder = EventBuilder( state=self.state, diff --git a/tests/test_state.py b/tests/test_state.py index e0aae06be4..1e4449fa1c 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -119,7 +119,7 @@ class StateGroupStore(object): def register_event_id_state_group(self, event_id, state_group): self._event_to_state_group[event_id] = state_group - def get_room_version(self, room_id): + def get_room_version_id(self, room_id): return RoomVersions.V1.identifier diff --git a/tests/unittest.py b/tests/unittest.py index b56e249386..98bf27d39c 100644 --- a/tests/unittest.py +++ b/tests/unittest.py @@ -589,7 +589,9 @@ class HomeserverTestCase(TestCase): event_builder_factory = self.hs.get_event_builder_factory() event_creation_handler = self.hs.get_event_creation_handler() - room_version = self.get_success(self.hs.get_datastore().get_room_version(room)) + room_version = self.get_success( + self.hs.get_datastore().get_room_version_id(room) + ) builder = event_builder_factory.for_room_version( KNOWN_ROOM_VERSIONS[room_version], From 08f41a6f05f304f6a14ab0339cf7225ec3d9851b Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 31 Jan 2020 10:28:15 +0000 Subject: [PATCH 0922/1623] Add `get_room_version` method So that we can start factoring out some of this boilerplatey boilerplate. --- synapse/api/errors.py | 6 ++---- synapse/storage/data_stores/main/state.py | 25 ++++++++++++++++++++++- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/synapse/api/errors.py b/synapse/api/errors.py index 1c9456e583..0c20601600 100644 --- a/synapse/api/errors.py +++ b/synapse/api/errors.py @@ -402,11 +402,9 @@ class UnsupportedRoomVersionError(SynapseError): """The client's request to create a room used a room version that the server does not support.""" - def __init__(self): + def __init__(self, msg="Homeserver does not support this room version"): super(UnsupportedRoomVersionError, self).__init__( - code=400, - msg="Homeserver does not support this room version", - errcode=Codes.UNSUPPORTED_ROOM_VERSION, + code=400, msg=msg, errcode=Codes.UNSUPPORTED_ROOM_VERSION, ) diff --git a/synapse/storage/data_stores/main/state.py b/synapse/storage/data_stores/main/state.py index 6700942523..3d34103e67 100644 --- a/synapse/storage/data_stores/main/state.py +++ b/synapse/storage/data_stores/main/state.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd +# Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,7 +23,8 @@ from six import iteritems from twisted.internet import defer from synapse.api.constants import EventTypes, Membership -from synapse.api.errors import NotFoundError +from synapse.api.errors import NotFoundError, UnsupportedRoomVersionError +from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersion from synapse.events import EventBase from synapse.events.snapshot import EventContext from synapse.storage._base import SQLBaseStore @@ -61,6 +63,27 @@ class StateGroupWorkerStore(EventsWorkerStore, SQLBaseStore): def __init__(self, database: Database, db_conn, hs): super(StateGroupWorkerStore, self).__init__(database, db_conn, hs) + async def get_room_version(self, room_id: str) -> RoomVersion: + """Get the room_version of a given room + + Raises: + NotFoundError: if the room is unknown + + UnsupportedRoomVersionError: if the room uses an unknown room version. + Typically this happens if support for the room's version has been + removed from Synapse. + """ + room_version_id = await self.get_room_version_id(room_id) + v = KNOWN_ROOM_VERSIONS.get(room_version_id) + + if not v: + raise UnsupportedRoomVersionError( + "Room %s uses a room version %s which is no longer supported" + % (room_id, room_version_id) + ) + + return v + @cached(max_entries=10000) async def get_room_version_id(self, room_id: str) -> str: """Get the room_version of a given room From f6fa2c0b31e2b3695a91f34a06974f428bd5d45c Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 31 Jan 2020 10:30:29 +0000 Subject: [PATCH 0923/1623] newsfile --- changelog.d/6820.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6820.misc diff --git a/changelog.d/6820.misc b/changelog.d/6820.misc new file mode 100644 index 0000000000..08aa80bcd9 --- /dev/null +++ b/changelog.d/6820.misc @@ -0,0 +1 @@ +Refactoring work in preparation for changing the event redaction algorithm. From 7f93eb190301024d373a573fc75a58f592469e9f Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 31 Jan 2020 13:47:43 +0000 Subject: [PATCH 0924/1623] pass room_version into compute_event_signature (#6807) --- changelog.d/6807.misc | 1 + synapse/crypto/event_signing.py | 28 ++++++++++++++++++++-------- synapse/handlers/federation.py | 5 ++++- 3 files changed, 25 insertions(+), 9 deletions(-) create mode 100644 changelog.d/6807.misc diff --git a/changelog.d/6807.misc b/changelog.d/6807.misc new file mode 100644 index 0000000000..08aa80bcd9 --- /dev/null +++ b/changelog.d/6807.misc @@ -0,0 +1 @@ +Refactoring work in preparation for changing the event redaction algorithm. diff --git a/synapse/crypto/event_signing.py b/synapse/crypto/event_signing.py index 1f2bccf700..5f733c1cf5 100644 --- a/synapse/crypto/event_signing.py +++ b/synapse/crypto/event_signing.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- - +# # Copyright 2014-2016 OpenMarket Ltd +# Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,6 +18,7 @@ import collections.abc import hashlib import logging +from typing import Dict from canonicaljson import encode_canonical_json from signedjson.sign import sign_json @@ -115,18 +117,28 @@ def compute_event_reference_hash(event, hash_algorithm=hashlib.sha256): return hashed.name, hashed.digest() -def compute_event_signature(event_dict, signature_name, signing_key): +def compute_event_signature( + room_version: RoomVersion, + event_dict: JsonDict, + signature_name: str, + signing_key: SigningKey, +) -> Dict[str, Dict[str, str]]: """Compute the signature of the event for the given name and key. Args: - event_dict (dict): The event as a dict - signature_name (str): The name of the entity signing the event + room_version: the version of the room that this event is in. + (the room version determines the redaction algorithm and hence the + json to be signed) + + event_dict: The event as a dict + + signature_name: The name of the entity signing the event (typically the server's hostname). - signing_key (syutil.crypto.SigningKey): The key to sign with + + signing_key: The key to sign with Returns: - dict[str, dict[str, str]]: Returns a dictionary in the same format of - an event's signatures field. + a dictionary in the same format of an event's signatures field. """ redact_json = prune_event_dict(event_dict) redact_json.pop("age_ts", None) @@ -161,5 +173,5 @@ def add_hashes_and_signatures( event_dict.setdefault("hashes", {})[name] = encode_base64(digest) event_dict["signatures"] = compute_event_signature( - event_dict, signature_name=signature_name, signing_key=signing_key + room_version, event_dict, signature_name=signature_name, signing_key=signing_key ) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 0f10c3e9b1..c86d3177e9 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1528,7 +1528,10 @@ class FederationHandler(BaseHandler): event.signatures.update( compute_event_signature( - event.get_pdu_json(), self.hs.hostname, self.hs.config.signing_key[0] + room_version, + event.get_pdu_json(), + self.hs.hostname, + self.hs.config.signing_key[0], ) ) From 83b0ea047b355ade44985af123f4807faa7892ab Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 31 Jan 2020 14:04:15 +0000 Subject: [PATCH 0925/1623] Fix deleting of stale marker for device lists (#6819) We were in fact only deleting stale marker when we got an incremental update, rather than when we did a full resync. --- changelog.d/6819.misc | 1 + synapse/storage/data_stores/main/devices.py | 14 +++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) create mode 100644 changelog.d/6819.misc diff --git a/changelog.d/6819.misc b/changelog.d/6819.misc new file mode 100644 index 0000000000..4f9a4ac7a5 --- /dev/null +++ b/changelog.d/6819.misc @@ -0,0 +1 @@ +Detect unknown remote devices and mark cache as stale. diff --git a/synapse/storage/data_stores/main/devices.py b/synapse/storage/data_stores/main/devices.py index a34415ff14..ea0503476f 100644 --- a/synapse/storage/data_stores/main/devices.py +++ b/synapse/storage/data_stores/main/devices.py @@ -940,13 +940,6 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): lock=False, ) - # If we're replacing the remote user's device list cache presumably - # we've done a full resync, so we remove the entry that says we need - # to resync - self.db.simple_delete_txn( - txn, table="device_lists_remote_resync", keyvalues={"user_id": user_id}, - ) - def update_remote_device_list_cache(self, user_id, devices, stream_id): """Replace the entire cache of the remote user's devices. @@ -1003,6 +996,13 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): lock=False, ) + # If we're replacing the remote user's device list cache presumably + # we've done a full resync, so we remove the entry that says we need + # to resync + self.db.simple_delete_txn( + txn, table="device_lists_remote_resync", keyvalues={"user_id": user_id}, + ) + @defer.inlineCallbacks def add_device_change_to_streams(self, user_id, device_ids, hosts): """Persist that a user's devices have been updated, and which hosts From ac0d45b78b647f6744b5850da88a4ca8c76666b9 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 31 Jan 2020 15:35:37 +0000 Subject: [PATCH 0926/1623] 1.10.0rc1 --- CHANGES.md | 44 ++++++++++++++++++++++++++++++++++++++++ UPGRADE.rst | 4 ++-- changelog.d/6729.misc | 1 - changelog.d/6734.bugfix | 1 - changelog.d/6748.misc | 1 - changelog.d/6751.misc | 1 - changelog.d/6757.misc | 1 - changelog.d/6761.bugfix | 1 - changelog.d/6767.bugfix | 1 - changelog.d/6771.bugfix | 1 - changelog.d/6775.doc | 1 - changelog.d/6776.misc | 1 - changelog.d/6786.misc | 1 - changelog.d/6787.feature | 1 - changelog.d/6788.misc | 1 - changelog.d/6790.feature | 1 - changelog.d/6792.misc | 1 - changelog.d/6794.feature | 1 - changelog.d/6795.bugfix | 1 - changelog.d/6796.bugfix | 1 - changelog.d/6797.misc | 1 - changelog.d/6799.bugfix | 1 - changelog.d/6800.bugfix | 1 - changelog.d/6801.bugfix | 1 - changelog.d/6802.misc | 1 - changelog.d/6803.misc | 1 - changelog.d/6805.misc | 1 - changelog.d/6806.misc | 1 - changelog.d/6807.misc | 1 - changelog.d/6810.misc | 1 - changelog.d/6811.bugfix | 1 - changelog.d/6816.misc | 1 - changelog.d/6819.misc | 1 - changelog.d/6820.misc | 1 - synapse/__init__.py | 2 +- 35 files changed, 47 insertions(+), 35 deletions(-) delete mode 100644 changelog.d/6729.misc delete mode 100644 changelog.d/6734.bugfix delete mode 100644 changelog.d/6748.misc delete mode 100644 changelog.d/6751.misc delete mode 100644 changelog.d/6757.misc delete mode 100644 changelog.d/6761.bugfix delete mode 100644 changelog.d/6767.bugfix delete mode 100644 changelog.d/6771.bugfix delete mode 100644 changelog.d/6775.doc delete mode 100644 changelog.d/6776.misc delete mode 100644 changelog.d/6786.misc delete mode 100644 changelog.d/6787.feature delete mode 100644 changelog.d/6788.misc delete mode 100644 changelog.d/6790.feature delete mode 100644 changelog.d/6792.misc delete mode 100644 changelog.d/6794.feature delete mode 100644 changelog.d/6795.bugfix delete mode 100644 changelog.d/6796.bugfix delete mode 100644 changelog.d/6797.misc delete mode 100644 changelog.d/6799.bugfix delete mode 100644 changelog.d/6800.bugfix delete mode 100644 changelog.d/6801.bugfix delete mode 100644 changelog.d/6802.misc delete mode 100644 changelog.d/6803.misc delete mode 100644 changelog.d/6805.misc delete mode 100644 changelog.d/6806.misc delete mode 100644 changelog.d/6807.misc delete mode 100644 changelog.d/6810.misc delete mode 100644 changelog.d/6811.bugfix delete mode 100644 changelog.d/6816.misc delete mode 100644 changelog.d/6819.misc delete mode 100644 changelog.d/6820.misc diff --git a/CHANGES.md b/CHANGES.md index 4c413b72ee..6686cafa5b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,47 @@ +Synapse 1.10.0rc1 (2020-01-31) +============================== + +Features +-------- + +- Implement updated authorization rules for aliases events, from [MSC2260](https://github.com/matrix-org/matrix-doc/pull/2260). ([\#6787](https://github.com/matrix-org/synapse/issues/6787), [\#6790](https://github.com/matrix-org/synapse/issues/6790), [\#6794](https://github.com/matrix-org/synapse/issues/6794)) + + +Bugfixes +-------- + +- Warn if postgres database has a non-C locale, as that can cause issues when upgrading locales (e.g. due to upgrading OS). ([\#6734](https://github.com/matrix-org/synapse/issues/6734)) +- Minor fixes to `PUT /_synapse/admin/v2/users` admin api. ([\#6761](https://github.com/matrix-org/synapse/issues/6761)) +- Validate `client_secret` parameter using the regex provided by the Client-Server API, temporarily allowing `:` characters for older clients. The `:` character will be removed in a future release. ([\#6767](https://github.com/matrix-org/synapse/issues/6767)) +- Fix persisting redaction events that have been redacted (or otherwise don't have a redacts key). ([\#6771](https://github.com/matrix-org/synapse/issues/6771)) +- Fix outbound federation request metrics. ([\#6795](https://github.com/matrix-org/synapse/issues/6795)) +- Fix bug where querying a remote user's device keys that weren't cached resulted in only returning a single device. ([\#6796](https://github.com/matrix-org/synapse/issues/6796)) +- Fix race in federation sender worker that delayed sending of device updates. ([\#6799](https://github.com/matrix-org/synapse/issues/6799), [\#6800](https://github.com/matrix-org/synapse/issues/6800)) +- Fix bug where Synapse didn't invalidate cache of remote users' devices when Synapse left a room. ([\#6801](https://github.com/matrix-org/synapse/issues/6801)) +- Fix waking up other workers when remote server is detected to have come back online. ([\#6811](https://github.com/matrix-org/synapse/issues/6811)) + + +Improved Documentation +---------------------- + +- Clarify documentation related to `user_dir` and `federation_reader` workers. ([\#6775](https://github.com/matrix-org/synapse/issues/6775)) + + +Internal Changes +---------------- + +- Record room versions in the `rooms` table. ([\#6729](https://github.com/matrix-org/synapse/issues/6729), [\#6788](https://github.com/matrix-org/synapse/issues/6788), [\#6810](https://github.com/matrix-org/synapse/issues/6810)) +- Propagate cache invalidates from workers to other workers. ([\#6748](https://github.com/matrix-org/synapse/issues/6748)) +- Remove some unnecessary admin handler abstraction methods. ([\#6751](https://github.com/matrix-org/synapse/issues/6751)) +- Add some debugging for media storage providers. ([\#6757](https://github.com/matrix-org/synapse/issues/6757)) +- Detect unknown remote devices and mark cache as stale. ([\#6776](https://github.com/matrix-org/synapse/issues/6776), [\#6819](https://github.com/matrix-org/synapse/issues/6819)) +- Attempt to resync remote users' devices when detected as stale. ([\#6786](https://github.com/matrix-org/synapse/issues/6786)) +- Delete current state from the database when server leaves a room. ([\#6792](https://github.com/matrix-org/synapse/issues/6792)) +- When a client asks for a remote user's device keys check if the local cache for that user has been marked as potentially stale. ([\#6797](https://github.com/matrix-org/synapse/issues/6797)) +- Add background update to clean out left rooms from current state. ([\#6802](https://github.com/matrix-org/synapse/issues/6802), [\#6816](https://github.com/matrix-org/synapse/issues/6816)) +- Refactoring work in preparation for changing the event redaction algorithm. ([\#6803](https://github.com/matrix-org/synapse/issues/6803), [\#6805](https://github.com/matrix-org/synapse/issues/6805), [\#6806](https://github.com/matrix-org/synapse/issues/6806), [\#6807](https://github.com/matrix-org/synapse/issues/6807), [\#6820](https://github.com/matrix-org/synapse/issues/6820)) + + Synapse 1.9.1 (2020-01-28) ========================== diff --git a/UPGRADE.rst b/UPGRADE.rst index 470246f128..1c5db1c4a8 100644 --- a/UPGRADE.rst +++ b/UPGRADE.rst @@ -76,8 +76,8 @@ for example: dpkg -i matrix-synapse-py3_1.3.0+stretch1_amd64.deb -Upgrading to **** -=============================== +Upgrading to v1.10.0 +==================== Synapse will now log a warning on start up if used with a PostgreSQL database that has a non-recommended locale set. diff --git a/changelog.d/6729.misc b/changelog.d/6729.misc deleted file mode 100644 index 5537355bea..0000000000 --- a/changelog.d/6729.misc +++ /dev/null @@ -1 +0,0 @@ -Record room versions in the `rooms` table. diff --git a/changelog.d/6734.bugfix b/changelog.d/6734.bugfix deleted file mode 100644 index 79c6bab4d1..0000000000 --- a/changelog.d/6734.bugfix +++ /dev/null @@ -1 +0,0 @@ -Warn if postgres database has a non-C locale, as that can cause issues when upgrading locales (e.g. due to upgrading OS). diff --git a/changelog.d/6748.misc b/changelog.d/6748.misc deleted file mode 100644 index de320d4cd9..0000000000 --- a/changelog.d/6748.misc +++ /dev/null @@ -1 +0,0 @@ -Propagate cache invalidates from workers to other workers. diff --git a/changelog.d/6751.misc b/changelog.d/6751.misc deleted file mode 100644 index 7222520528..0000000000 --- a/changelog.d/6751.misc +++ /dev/null @@ -1 +0,0 @@ -Remove some unnecessary admin handler abstraction methods. \ No newline at end of file diff --git a/changelog.d/6757.misc b/changelog.d/6757.misc deleted file mode 100644 index a50c5e974a..0000000000 --- a/changelog.d/6757.misc +++ /dev/null @@ -1 +0,0 @@ -Add some debugging for media storage providers. diff --git a/changelog.d/6761.bugfix b/changelog.d/6761.bugfix deleted file mode 100644 index 1c664c02df..0000000000 --- a/changelog.d/6761.bugfix +++ /dev/null @@ -1 +0,0 @@ -Minor fixes to `PUT /_synapse/admin/v2/users` admin api. diff --git a/changelog.d/6767.bugfix b/changelog.d/6767.bugfix deleted file mode 100644 index 63c7c63315..0000000000 --- a/changelog.d/6767.bugfix +++ /dev/null @@ -1 +0,0 @@ -Validate `client_secret` parameter using the regex provided by the Client-Server API, temporarily allowing `:` characters for older clients. The `:` character will be removed in a future release. diff --git a/changelog.d/6771.bugfix b/changelog.d/6771.bugfix deleted file mode 100644 index 623ba24acb..0000000000 --- a/changelog.d/6771.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix persisting redaction events that have been redacted (or otherwise don't have a redacts key). diff --git a/changelog.d/6775.doc b/changelog.d/6775.doc deleted file mode 100644 index c6078ef82d..0000000000 --- a/changelog.d/6775.doc +++ /dev/null @@ -1 +0,0 @@ -Clarify documentation related to `user_dir` and `federation_reader` workers. diff --git a/changelog.d/6776.misc b/changelog.d/6776.misc deleted file mode 100644 index 4f9a4ac7a5..0000000000 --- a/changelog.d/6776.misc +++ /dev/null @@ -1 +0,0 @@ -Detect unknown remote devices and mark cache as stale. diff --git a/changelog.d/6786.misc b/changelog.d/6786.misc deleted file mode 100644 index 94c692e53a..0000000000 --- a/changelog.d/6786.misc +++ /dev/null @@ -1 +0,0 @@ -Attempt to resync remote users' devices when detected as stale. diff --git a/changelog.d/6787.feature b/changelog.d/6787.feature deleted file mode 100644 index df9e4b77ab..0000000000 --- a/changelog.d/6787.feature +++ /dev/null @@ -1 +0,0 @@ -Implement updated authorization rules for aliases events, from [MSC2260](https://github.com/matrix-org/matrix-doc/pull/2260). diff --git a/changelog.d/6788.misc b/changelog.d/6788.misc deleted file mode 100644 index 5537355bea..0000000000 --- a/changelog.d/6788.misc +++ /dev/null @@ -1 +0,0 @@ -Record room versions in the `rooms` table. diff --git a/changelog.d/6790.feature b/changelog.d/6790.feature deleted file mode 100644 index df9e4b77ab..0000000000 --- a/changelog.d/6790.feature +++ /dev/null @@ -1 +0,0 @@ -Implement updated authorization rules for aliases events, from [MSC2260](https://github.com/matrix-org/matrix-doc/pull/2260). diff --git a/changelog.d/6792.misc b/changelog.d/6792.misc deleted file mode 100644 index fa31d509b3..0000000000 --- a/changelog.d/6792.misc +++ /dev/null @@ -1 +0,0 @@ -Delete current state from the database when server leaves a room. diff --git a/changelog.d/6794.feature b/changelog.d/6794.feature deleted file mode 100644 index df9e4b77ab..0000000000 --- a/changelog.d/6794.feature +++ /dev/null @@ -1 +0,0 @@ -Implement updated authorization rules for aliases events, from [MSC2260](https://github.com/matrix-org/matrix-doc/pull/2260). diff --git a/changelog.d/6795.bugfix b/changelog.d/6795.bugfix deleted file mode 100644 index d1585653b1..0000000000 --- a/changelog.d/6795.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix outbound federation request metrics. diff --git a/changelog.d/6796.bugfix b/changelog.d/6796.bugfix deleted file mode 100644 index 206a157311..0000000000 --- a/changelog.d/6796.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix bug where querying a remote user's device keys that weren't cached resulted in only returning a single device. diff --git a/changelog.d/6797.misc b/changelog.d/6797.misc deleted file mode 100644 index e9127bac51..0000000000 --- a/changelog.d/6797.misc +++ /dev/null @@ -1 +0,0 @@ -When a client asks for a remote user's device keys check if the local cache for that user has been marked as potentially stale. diff --git a/changelog.d/6799.bugfix b/changelog.d/6799.bugfix deleted file mode 100644 index 322a2758af..0000000000 --- a/changelog.d/6799.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix race in federation sender worker that delayed sending of device updates. diff --git a/changelog.d/6800.bugfix b/changelog.d/6800.bugfix deleted file mode 100644 index 322a2758af..0000000000 --- a/changelog.d/6800.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix race in federation sender worker that delayed sending of device updates. diff --git a/changelog.d/6801.bugfix b/changelog.d/6801.bugfix deleted file mode 100644 index f401fa5d69..0000000000 --- a/changelog.d/6801.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix bug where Synapse didn't invalidate cache of remote users' devices when Synapse left a room. diff --git a/changelog.d/6802.misc b/changelog.d/6802.misc deleted file mode 100644 index a77ba1d7a5..0000000000 --- a/changelog.d/6802.misc +++ /dev/null @@ -1 +0,0 @@ -Add background update to clean out left rooms from current state. diff --git a/changelog.d/6803.misc b/changelog.d/6803.misc deleted file mode 100644 index 08aa80bcd9..0000000000 --- a/changelog.d/6803.misc +++ /dev/null @@ -1 +0,0 @@ -Refactoring work in preparation for changing the event redaction algorithm. diff --git a/changelog.d/6805.misc b/changelog.d/6805.misc deleted file mode 100644 index 08aa80bcd9..0000000000 --- a/changelog.d/6805.misc +++ /dev/null @@ -1 +0,0 @@ -Refactoring work in preparation for changing the event redaction algorithm. diff --git a/changelog.d/6806.misc b/changelog.d/6806.misc deleted file mode 100644 index 08aa80bcd9..0000000000 --- a/changelog.d/6806.misc +++ /dev/null @@ -1 +0,0 @@ -Refactoring work in preparation for changing the event redaction algorithm. diff --git a/changelog.d/6807.misc b/changelog.d/6807.misc deleted file mode 100644 index 08aa80bcd9..0000000000 --- a/changelog.d/6807.misc +++ /dev/null @@ -1 +0,0 @@ -Refactoring work in preparation for changing the event redaction algorithm. diff --git a/changelog.d/6810.misc b/changelog.d/6810.misc deleted file mode 100644 index 5537355bea..0000000000 --- a/changelog.d/6810.misc +++ /dev/null @@ -1 +0,0 @@ -Record room versions in the `rooms` table. diff --git a/changelog.d/6811.bugfix b/changelog.d/6811.bugfix deleted file mode 100644 index 361f2fc2e8..0000000000 --- a/changelog.d/6811.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix waking up other workers when remote server is detected to have come back online. diff --git a/changelog.d/6816.misc b/changelog.d/6816.misc deleted file mode 100644 index a77ba1d7a5..0000000000 --- a/changelog.d/6816.misc +++ /dev/null @@ -1 +0,0 @@ -Add background update to clean out left rooms from current state. diff --git a/changelog.d/6819.misc b/changelog.d/6819.misc deleted file mode 100644 index 4f9a4ac7a5..0000000000 --- a/changelog.d/6819.misc +++ /dev/null @@ -1 +0,0 @@ -Detect unknown remote devices and mark cache as stale. diff --git a/changelog.d/6820.misc b/changelog.d/6820.misc deleted file mode 100644 index 08aa80bcd9..0000000000 --- a/changelog.d/6820.misc +++ /dev/null @@ -1 +0,0 @@ -Refactoring work in preparation for changing the event redaction algorithm. diff --git a/synapse/__init__.py b/synapse/__init__.py index a236888d3c..bd942d3e1c 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -36,7 +36,7 @@ try: except ImportError: pass -__version__ = "1.9.1" +__version__ = "1.10.0rc1" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 0f8ffa38b5b5137c74c6bf088f5f654553170e11 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 31 Jan 2020 15:38:16 +0000 Subject: [PATCH 0927/1623] Fix link in upgrade.rst --- UPGRADE.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UPGRADE.rst b/UPGRADE.rst index 1c5db1c4a8..3cad8c2837 100644 --- a/UPGRADE.rst +++ b/UPGRADE.rst @@ -82,7 +82,7 @@ Upgrading to v1.10.0 Synapse will now log a warning on start up if used with a PostgreSQL database that has a non-recommended locale set. -See [docs/postgres.md](docs/postgres.md) for details. +See `docs/postgres.md `_ for details. Upgrading to v1.8.0 From 68ef7ebbef3f6ccc697d68f11b74a3f65613379f Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 31 Jan 2020 15:45:08 +0000 Subject: [PATCH 0928/1623] Update changelog --- CHANGES.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 6686cafa5b..ab6fce3e7d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,10 +1,13 @@ Synapse 1.10.0rc1 (2020-01-31) ============================== +**WARNING**: As of this release Synapse validates `client_secret` parameters in the Client-Server API as per the spec. See [\#6766](https://github.com/matrix-org/synapse/issues/6766) for details. + + Features -------- -- Implement updated authorization rules for aliases events, from [MSC2260](https://github.com/matrix-org/matrix-doc/pull/2260). ([\#6787](https://github.com/matrix-org/synapse/issues/6787), [\#6790](https://github.com/matrix-org/synapse/issues/6790), [\#6794](https://github.com/matrix-org/synapse/issues/6794)) +- Add experimental support for updated authorization rules for aliases events, from [MSC2260](https://github.com/matrix-org/matrix-doc/pull/2260). ([\#6787](https://github.com/matrix-org/synapse/issues/6787), [\#6790](https://github.com/matrix-org/synapse/issues/6790), [\#6794](https://github.com/matrix-org/synapse/issues/6794)) Bugfixes From b0d112e78b96168852260b3986d348ae3a98292f Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 3 Feb 2020 13:15:24 +0000 Subject: [PATCH 0929/1623] Fix `room_version` in `on_invite_request` flow (#6827) I messed this up a bit in #6805, but fortunately we weren't actually doing anything with the room_version so it didn't matter that it was a str not a RoomVersion. --- changelog.d/6827.misc | 1 + synapse/federation/federation_server.py | 13 ++++++++----- synapse/federation/transport/server.py | 4 ++-- 3 files changed, 11 insertions(+), 7 deletions(-) create mode 100644 changelog.d/6827.misc diff --git a/changelog.d/6827.misc b/changelog.d/6827.misc new file mode 100644 index 0000000000..08aa80bcd9 --- /dev/null +++ b/changelog.d/6827.misc @@ -0,0 +1 @@ +Refactoring work in preparation for changing the event redaction algorithm. diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index a4c97ed458..d92d5e8064 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -54,7 +54,7 @@ from synapse.replication.http.federation import ( ReplicationFederationSendEduRestServlet, ReplicationGetQueryRestServlet, ) -from synapse.types import get_domain_from_id +from synapse.types import JsonDict, get_domain_from_id from synapse.util import glob_to_regex, unwrapFirstError from synapse.util.async_helpers import Linearizer, concurrently_execute from synapse.util.caches.response_cache import ResponseCache @@ -396,20 +396,23 @@ class FederationServer(FederationBase): time_now = self._clock.time_msec() return {"event": pdu.get_pdu_json(time_now), "room_version": room_version} - async def on_invite_request(self, origin, content, room_version): - if room_version not in KNOWN_ROOM_VERSIONS: + async def on_invite_request( + self, origin: str, content: JsonDict, room_version_id: str + ): + room_version = KNOWN_ROOM_VERSIONS.get(room_version_id) + if not room_version: raise SynapseError( 400, "Homeserver does not support this room version", Codes.UNSUPPORTED_ROOM_VERSION, ) - format_ver = room_version_to_event_format(room_version) + format_ver = room_version.event_format pdu = event_from_pdu_json(content, format_ver) origin_host, _ = parse_server_name(origin) await self.check_server_matches_acl(origin_host, pdu.room_id) - pdu = await self._check_sigs_and_hash(room_version, pdu) + pdu = await self._check_sigs_and_hash(room_version.identifier, pdu) ret_pdu = await self.handler.on_invite_request(origin, pdu, room_version) time_now = self._clock.time_msec() return {"event": ret_pdu.get_pdu_json(time_now)} diff --git a/synapse/federation/transport/server.py b/synapse/federation/transport/server.py index 125eadd796..ae48ba8157 100644 --- a/synapse/federation/transport/server.py +++ b/synapse/federation/transport/server.py @@ -579,7 +579,7 @@ class FederationV1InviteServlet(BaseFederationServlet): # state resolution algorithm, and we don't use that for processing # invites content = await self.handler.on_invite_request( - origin, content, room_version=RoomVersions.V1.identifier + origin, content, room_version_id=RoomVersions.V1.identifier ) # V1 federation API is defined to return a content of `[200, {...}]` @@ -606,7 +606,7 @@ class FederationV2InviteServlet(BaseFederationServlet): event.setdefault("unsigned", {})["invite_room_state"] = invite_room_state content = await self.handler.on_invite_request( - origin, event, room_version=room_version + origin, event, room_version_id=room_version ) return 200, content From 370080531ef7ae3f075ff8f577f42c2b6e25295c Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Mon, 3 Feb 2020 13:18:42 +0000 Subject: [PATCH 0930/1623] Allow URL-encoded user IDs on user admin api paths (#6825) --- changelog.d/6825.bugfix | 1 + synapse/rest/admin/users.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changelog.d/6825.bugfix diff --git a/changelog.d/6825.bugfix b/changelog.d/6825.bugfix new file mode 100644 index 0000000000..d3cacd6d9a --- /dev/null +++ b/changelog.d/6825.bugfix @@ -0,0 +1 @@ +Allow URL-encoded User IDs on `/_synapse/admin/v2/users/[/admin]` endpoints. Thanks to @NHAS for reporting. \ No newline at end of file diff --git a/synapse/rest/admin/users.py b/synapse/rest/admin/users.py index 3455741195..f1c4434f5c 100644 --- a/synapse/rest/admin/users.py +++ b/synapse/rest/admin/users.py @@ -105,7 +105,7 @@ class UsersRestServletV2(RestServlet): class UserRestServletV2(RestServlet): - PATTERNS = (re.compile("^/_synapse/admin/v2/users/(?P@[^/]+)$"),) + PATTERNS = (re.compile("^/_synapse/admin/v2/users/(?P[^/]+)$"),) """Get request to list user details. This needs user to have administrator access in Synapse. @@ -568,7 +568,7 @@ class UserAdminServlet(RestServlet): {} """ - PATTERNS = (re.compile("^/_synapse/admin/v1/users/(?P@[^/]*)/admin$"),) + PATTERNS = (re.compile("^/_synapse/admin/v1/users/(?P[^/]*)/admin$"),) def __init__(self, hs): self.hs = hs From b3e44f0bdf4caff4107826c63b9d281c4d9ff509 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 3 Feb 2020 15:30:23 +0000 Subject: [PATCH 0931/1623] make FederationHandler.on_query_auth async --- synapse/handlers/federation.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index c86d3177e9..61df39d4bf 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -2120,15 +2120,14 @@ class FederationHandler(BaseHandler): logger.warning("Soft-failing %r because %s", event, e) event.internal_metadata.soft_failed = True - @defer.inlineCallbacks - def on_query_auth( + async def on_query_auth( self, origin, event_id, room_id, remote_auth_chain, rejects, missing ): - in_room = yield self.auth.check_host_in_room(room_id, origin) + in_room = await self.auth.check_host_in_room(room_id, origin) if not in_room: raise AuthError(403, "Host not in room.") - event = yield self.store.get_event( + event = await self.store.get_event( event_id, allow_none=False, check_room_id=room_id ) @@ -2136,19 +2135,19 @@ class FederationHandler(BaseHandler): # don't want to fall into the trap of `missing` being wrong. for e in remote_auth_chain: try: - yield self._handle_new_event(origin, e) + await self._handle_new_event(origin, e) except AuthError: pass # Now get the current auth_chain for the event. - local_auth_chain = yield self.store.get_auth_chain( + local_auth_chain = await self.store.get_auth_chain( [auth_id for auth_id in event.auth_event_ids()], include_given=True ) # TODO: Check if we would now reject event_id. If so we need to tell # everyone. - ret = yield self.construct_auth_difference(local_auth_chain, remote_auth_chain) + ret = await self.construct_auth_difference(local_auth_chain, remote_auth_chain) logger.debug("on_query_auth returning: %s", ret) From 7571bf86f0399b2376427f3a6d91b8850e45b8f8 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 3 Feb 2020 15:32:48 +0000 Subject: [PATCH 0932/1623] make FederationHandler.on_send_join_request async --- synapse/handlers/federation.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 61df39d4bf..7d6db77ae5 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1411,9 +1411,7 @@ class FederationHandler(BaseHandler): return event - @defer.inlineCallbacks - @log_function - def on_send_join_request(self, origin, pdu): + async def on_send_join_request(self, origin, pdu): """ We have received a join event for a room. Fully process it and respond with the current state and auth chains. """ @@ -1450,9 +1448,9 @@ class FederationHandler(BaseHandler): # would introduce the danger of backwards-compatibility problems. event.internal_metadata.send_on_behalf_of = origin - context = yield self._handle_new_event(origin, event) + context = await self._handle_new_event(origin, event) - event_allowed = yield self.third_party_event_rules.check_event_allowed( + event_allowed = await self.third_party_event_rules.check_event_allowed( event, context ) if not event_allowed: @@ -1470,14 +1468,14 @@ class FederationHandler(BaseHandler): if event.type == EventTypes.Member: if event.content["membership"] == Membership.JOIN: user = UserID.from_string(event.state_key) - yield self.user_joined_room(user, event.room_id) + await self.user_joined_room(user, event.room_id) - prev_state_ids = yield context.get_prev_state_ids() + prev_state_ids = await context.get_prev_state_ids() state_ids = list(prev_state_ids.values()) - auth_chain = yield self.store.get_auth_chain(state_ids) + auth_chain = await self.store.get_auth_chain(state_ids) - state = yield self.store.get_events(list(prev_state_ids.values())) + state = await self.store.get_events(list(prev_state_ids.values())) return {"state": list(state.values()), "auth_chain": auth_chain} From af8ba6b52502dda2c0b50a023f4fd0ef63b67237 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 3 Feb 2020 15:33:42 +0000 Subject: [PATCH 0933/1623] make FederationHandler.on_invite_request async --- synapse/handlers/federation.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 7d6db77ae5..b924c72c77 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1479,8 +1479,7 @@ class FederationHandler(BaseHandler): return {"state": list(state.values()), "auth_chain": auth_chain} - @defer.inlineCallbacks - def on_invite_request( + async def on_invite_request( self, origin: str, event: EventBase, room_version: RoomVersion ): """ We've got an invite event. Process and persist it. Sign it. @@ -1490,7 +1489,7 @@ class FederationHandler(BaseHandler): if event.state_key is None: raise SynapseError(400, "The invite event did not have a state key") - is_blocked = yield self.store.is_room_blocked(event.room_id) + is_blocked = await self.store.is_room_blocked(event.room_id) if is_blocked: raise SynapseError(403, "This room has been blocked on this server") @@ -1533,8 +1532,8 @@ class FederationHandler(BaseHandler): ) ) - context = yield self.state_handler.compute_event_context(event) - yield self.persist_events_and_notify([(event, context)]) + context = await self.state_handler.compute_event_context(event) + await self.persist_events_and_notify([(event, context)]) return event From 98681f90cbe70c317556ec1596df256f49d4d38a Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 3 Feb 2020 15:35:30 +0000 Subject: [PATCH 0934/1623] make FederationHandler.on_make_join_request async --- synapse/handlers/federation.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index b924c72c77..e75ebb168e 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1348,20 +1348,17 @@ class FederationHandler(BaseHandler): "Error handling queued PDU %s from %s: %s", p.event_id, origin, e ) - @defer.inlineCallbacks - @log_function - def on_make_join_request(self, origin, room_id, user_id): + async def on_make_join_request( + self, origin: str, room_id: str, user_id: str + ) -> EventBase: """ We've received a /make_join/ request, so we create a partial join event for the room and return that. We do *not* persist or process it until the other server has signed it and sent it back. Args: - origin (str): The (verified) server name of the requesting server. - room_id (str): Room to create join event in - user_id (str): The user to create the join for - - Returns: - Deferred[FrozenEvent] + origin: The (verified) server name of the requesting server. + room_id: Room to create join event in + user_id: The user to create the join for """ if get_domain_from_id(user_id) != origin: logger.info( @@ -1373,7 +1370,7 @@ class FederationHandler(BaseHandler): event_content = {"membership": Membership.JOIN} - room_version = yield self.store.get_room_version_id(room_id) + room_version = await self.store.get_room_version_id(room_id) builder = self.event_builder_factory.new( room_version, @@ -1387,14 +1384,14 @@ class FederationHandler(BaseHandler): ) try: - event, context = yield self.event_creation_handler.create_new_client_event( + event, context = await self.event_creation_handler.create_new_client_event( builder=builder ) except AuthError as e: logger.warning("Failed to create join to %s because %s", room_id, e) raise e - event_allowed = yield self.third_party_event_rules.check_event_allowed( + event_allowed = await self.third_party_event_rules.check_event_allowed( event, context ) if not event_allowed: @@ -1405,7 +1402,7 @@ class FederationHandler(BaseHandler): # The remote hasn't signed it yet, obviously. We'll do the full checks # when we get the event back in `on_send_join_request` - yield self.auth.check_from_context( + await self.auth.check_from_context( room_version, event, context, do_sig_check=False ) From d184cbc0319cbff17d04028c265cd774356f6e54 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 3 Feb 2020 15:39:24 +0000 Subject: [PATCH 0935/1623] make FederationHandler.on_send_leave_request async --- synapse/handlers/federation.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index e75ebb168e..cdde10c8b9 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1641,9 +1641,7 @@ class FederationHandler(BaseHandler): return event - @defer.inlineCallbacks - @log_function - def on_send_leave_request(self, origin, pdu): + async def on_send_leave_request(self, origin, pdu): """ We have received a leave event for a room. Fully process it.""" event = pdu @@ -1663,9 +1661,9 @@ class FederationHandler(BaseHandler): event.internal_metadata.outlier = False - context = yield self._handle_new_event(origin, event) + context = await self._handle_new_event(origin, event) - event_allowed = yield self.third_party_event_rules.check_event_allowed( + event_allowed = await self.third_party_event_rules.check_event_allowed( event, context ) if not event_allowed: From 6e89ec5e32907a5e6b036526e3eb6cb4e76d843f Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 3 Feb 2020 15:40:41 +0000 Subject: [PATCH 0936/1623] make FederationHandler.on_make_leave_request async --- synapse/handlers/federation.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index cdde10c8b9..3813680c34 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1581,20 +1581,17 @@ class FederationHandler(BaseHandler): assert event.room_id == room_id return origin, event, room_version - @defer.inlineCallbacks - @log_function - def on_make_leave_request(self, origin, room_id, user_id): + async def on_make_leave_request( + self, origin: str, room_id: str, user_id: str + ) -> EventBase: """ We've received a /make_leave/ request, so we create a partial leave event for the room and return that. We do *not* persist or process it until the other server has signed it and sent it back. Args: - origin (str): The (verified) server name of the requesting server. - room_id (str): Room to create leave event in - user_id (str): The user to create the leave for - - Returns: - Deferred[FrozenEvent] + origin: The (verified) server name of the requesting server. + room_id: Room to create leave event in + user_id: The user to create the leave for """ if get_domain_from_id(user_id) != origin: logger.info( @@ -1604,7 +1601,7 @@ class FederationHandler(BaseHandler): ) raise SynapseError(403, "User not from origin", Codes.FORBIDDEN) - room_version = yield self.store.get_room_version_id(room_id) + room_version = await self.store.get_room_version_id(room_id) builder = self.event_builder_factory.new( room_version, { @@ -1616,11 +1613,11 @@ class FederationHandler(BaseHandler): }, ) - event, context = yield self.event_creation_handler.create_new_client_event( + event, context = await self.event_creation_handler.create_new_client_event( builder=builder ) - event_allowed = yield self.third_party_event_rules.check_event_allowed( + event_allowed = await self.third_party_event_rules.check_event_allowed( event, context ) if not event_allowed: @@ -1632,7 +1629,7 @@ class FederationHandler(BaseHandler): try: # The remote hasn't signed it yet, obviously. We'll do the full checks # when we get the event back in `on_send_leave_request` - yield self.auth.check_from_context( + await self.auth.check_from_context( room_version, event, context, do_sig_check=False ) except AuthError as e: From c556ed9e15640da5b8aff15c4547609202eab6f1 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 3 Feb 2020 15:43:51 +0000 Subject: [PATCH 0937/1623] make FederationHandler._handle_new_events async --- synapse/handlers/federation.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 3813680c34..9c16f54304 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1815,13 +1815,12 @@ class FederationHandler(BaseHandler): return context - @defer.inlineCallbacks - def _handle_new_events( + async def _handle_new_events( self, origin: str, event_infos: Iterable[_NewEventInfo], backfilled: bool = False, - ): + ) -> None: """Creates the appropriate contexts and persists events. The events should not depend on one another, e.g. this should be used to persist a bunch of outliers, but not a chunk of individual events that depend @@ -1830,11 +1829,10 @@ class FederationHandler(BaseHandler): Notifies about the events where appropriate. """ - @defer.inlineCallbacks - def prep(ev_info: _NewEventInfo): + async def prep(ev_info: _NewEventInfo): event = ev_info.event with nested_logging_context(suffix=event.event_id): - res = yield self._prep_event( + res = await self._prep_event( origin, event, state=ev_info.state, @@ -1843,14 +1841,14 @@ class FederationHandler(BaseHandler): ) return res - contexts = yield make_deferred_yieldable( + contexts = await make_deferred_yieldable( defer.gatherResults( [run_in_background(prep, ev_info) for ev_info in event_infos], consumeErrors=True, ) ) - yield self.persist_events_and_notify( + await self.persist_events_and_notify( [ (ev_info.event, context) for ev_info, context in zip(event_infos, contexts) From 1cdc253e0a965515823bc34c6223dbeda0c55669 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 3 Feb 2020 15:48:33 +0000 Subject: [PATCH 0938/1623] make FederationHandler._handle_new_event async --- synapse/handlers/federation.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 9c16f54304..81eb7eecbd 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1782,11 +1782,10 @@ class FederationHandler(BaseHandler): def get_min_depth_for_context(self, context): return self.store.get_min_depth(context) - @defer.inlineCallbacks - def _handle_new_event( + async def _handle_new_event( self, origin, event, state=None, auth_events=None, backfilled=False ): - context = yield self._prep_event( + context = await self._prep_event( origin, event, state=state, auth_events=auth_events, backfilled=backfilled ) @@ -1799,11 +1798,11 @@ class FederationHandler(BaseHandler): and not backfilled and not context.rejected ): - yield self.action_generator.handle_push_actions_for_event( + await self.action_generator.handle_push_actions_for_event( event, context ) - yield self.persist_events_and_notify( + await self.persist_events_and_notify( [(event, context)], backfilled=backfilled ) success = True @@ -2296,7 +2295,9 @@ class FederationHandler(BaseHandler): logger.debug( "do_auth %s missing_auth: %s", event.event_id, e.event_id ) - yield self._handle_new_event(origin, e, auth_events=auth) + yield defer.ensureDeferred( + self._handle_new_event(origin, e, auth_events=auth) + ) if e.event_id in event_auth_events: auth_events[(e.type, e.state_key)] = e From 8033b257a7f4f8e21d5b52228234f429f1552dd2 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 3 Feb 2020 15:49:32 +0000 Subject: [PATCH 0939/1623] make FederationHandler._prep_event async --- synapse/handlers/federation.py | 33 ++++++++++----------------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 81eb7eecbd..8cb3a505f8 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1966,54 +1966,41 @@ class FederationHandler(BaseHandler): yield self.persist_events_and_notify([(event, new_event_context)]) - @defer.inlineCallbacks - def _prep_event( + async def _prep_event( self, origin: str, event: EventBase, state: Optional[Iterable[EventBase]], auth_events: Optional[StateMap[EventBase]], backfilled: bool, - ): - """ - - Args: - origin: - event: - state: - auth_events: - backfilled: - - Returns: - Deferred, which resolves to synapse.events.snapshot.EventContext - """ - context = yield self.state_handler.compute_event_context(event, old_state=state) + ) -> EventContext: + context = await self.state_handler.compute_event_context(event, old_state=state) if not auth_events: - prev_state_ids = yield context.get_prev_state_ids() - auth_events_ids = yield self.auth.compute_auth_events( + prev_state_ids = await context.get_prev_state_ids() + auth_events_ids = await self.auth.compute_auth_events( event, prev_state_ids, for_verification=True ) - auth_events = yield self.store.get_events(auth_events_ids) + auth_events = await self.store.get_events(auth_events_ids) auth_events = {(e.type, e.state_key): e for e in auth_events.values()} # This is a hack to fix some old rooms where the initial join event # didn't reference the create event in its auth events. if event.type == EventTypes.Member and not event.auth_event_ids(): if len(event.prev_event_ids()) == 1 and event.depth < 5: - c = yield self.store.get_event( + c = await self.store.get_event( event.prev_event_ids()[0], allow_none=True ) if c and c.type == EventTypes.Create: auth_events[(c.type, c.state_key)] = c - context = yield self.do_auth(origin, event, context, auth_events=auth_events) + context = await self.do_auth(origin, event, context, auth_events=auth_events) if not context.rejected: - yield self._check_for_soft_fail(event, state, backfilled) + await self._check_for_soft_fail(event, state, backfilled) if event.type == EventTypes.GuestAccess and not context.rejected: - yield self.maybe_kick_guest_users(event) + await self.maybe_kick_guest_users(event) return context From bc9b75c6f08db3672d6620a91e91be2e0188c1a0 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 3 Feb 2020 15:51:24 +0000 Subject: [PATCH 0940/1623] make FederationHandler.do_auth async --- synapse/handlers/federation.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 8cb3a505f8..780029547d 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -2149,16 +2149,20 @@ class FederationHandler(BaseHandler): return missing_events - @defer.inlineCallbacks - @log_function - def do_auth(self, origin, event, context, auth_events): + async def do_auth( + self, + origin: str, + event: EventBase, + context: EventContext, + auth_events: StateMap[EventBase], + ) -> EventContext: """ Args: - origin (str): - event (synapse.events.EventBase): - context (synapse.events.snapshot.EventContext): - auth_events (dict[(str, str)->synapse.events.EventBase]): + origin: + event: + context: + auth_events: Map from (event_type, state_key) to event Normally, our calculated auth_events based on the state of the room @@ -2168,13 +2172,13 @@ class FederationHandler(BaseHandler): Also NB that this function adds entries to it. Returns: - defer.Deferred[EventContext]: updated context object + updated context object """ - room_version = yield self.store.get_room_version_id(event.room_id) + room_version = await self.store.get_room_version_id(event.room_id) room_version_obj = KNOWN_ROOM_VERSIONS[room_version] try: - context = yield self._update_auth_events_and_context_for_auth( + context = await self._update_auth_events_and_context_for_auth( origin, event, context, auth_events ) except Exception: From a25ddf26a35f2bd25b5284736b39de70a37f8570 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 3 Feb 2020 15:53:30 +0000 Subject: [PATCH 0941/1623] make FederationHandler._update_auth_events_and_context_for_auth async --- synapse/handlers/federation.py | 41 +++++++++++++++++----------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 780029547d..8dd2e81fd4 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -2200,10 +2200,13 @@ class FederationHandler(BaseHandler): return context - @defer.inlineCallbacks - def _update_auth_events_and_context_for_auth( - self, origin, event, context, auth_events - ): + async def _update_auth_events_and_context_for_auth( + self, + origin: str, + event: EventBase, + context: EventContext, + auth_events: StateMap[EventBase], + ) -> EventContext: """Helper for do_auth. See there for docs. Checks whether a given event has the expected auth events. If it @@ -2211,16 +2214,16 @@ class FederationHandler(BaseHandler): we can come to a consensus (e.g. if one server missed some valid state). - This attempts to resovle any potential divergence of state between + This attempts to resolve any potential divergence of state between servers, but is not essential and so failures should not block further processing of the event. Args: - origin (str): - event (synapse.events.EventBase): - context (synapse.events.snapshot.EventContext): + origin: + event: + context: - auth_events (dict[(str, str)->synapse.events.EventBase]): + auth_events: Map from (event_type, state_key) to event Normally, our calculated auth_events based on the state of the room @@ -2231,7 +2234,7 @@ class FederationHandler(BaseHandler): Also NB that this function adds entries to it. Returns: - defer.Deferred[EventContext]: updated context + updated context """ event_auth_events = set(event.auth_event_ids()) @@ -2245,7 +2248,7 @@ class FederationHandler(BaseHandler): # # we start by checking if they are in the store, and then try calling /event_auth/. if missing_auth: - have_events = yield self.store.have_seen_events(missing_auth) + have_events = await self.store.have_seen_events(missing_auth) logger.debug("Events %s are in the store", have_events) missing_auth.difference_update(have_events) @@ -2254,7 +2257,7 @@ class FederationHandler(BaseHandler): logger.info("auth_events contains unknown events: %s", missing_auth) try: try: - remote_auth_chain = yield self.federation_client.get_event_auth( + remote_auth_chain = await self.federation_client.get_event_auth( origin, event.room_id, event.event_id ) except RequestSendFailed as e: @@ -2263,7 +2266,7 @@ class FederationHandler(BaseHandler): logger.info("Failed to get event auth from remote: %s", e) return context - seen_remotes = yield self.store.have_seen_events( + seen_remotes = await self.store.have_seen_events( [e.event_id for e in remote_auth_chain] ) @@ -2286,9 +2289,7 @@ class FederationHandler(BaseHandler): logger.debug( "do_auth %s missing_auth: %s", event.event_id, e.event_id ) - yield defer.ensureDeferred( - self._handle_new_event(origin, e, auth_events=auth) - ) + await self._handle_new_event(origin, e, auth_events=auth) if e.event_id in event_auth_events: auth_events[(e.type, e.state_key)] = e @@ -2322,7 +2323,7 @@ class FederationHandler(BaseHandler): # XXX: currently this checks for redactions but I'm not convinced that is # necessary? - different_events = yield self.store.get_events_as_list(different_auth) + different_events = await self.store.get_events_as_list(different_auth) for d in different_events: if d.room_id != event.room_id: @@ -2348,8 +2349,8 @@ class FederationHandler(BaseHandler): remote_auth_events.update({(d.type, d.state_key): d for d in different_events}) remote_state = remote_auth_events.values() - room_version = yield self.store.get_room_version_id(event.room_id) - new_state = yield self.state_handler.resolve_events( + room_version = await self.store.get_room_version_id(event.room_id) + new_state = await self.state_handler.resolve_events( room_version, (local_state, remote_state), event ) @@ -2364,7 +2365,7 @@ class FederationHandler(BaseHandler): auth_events.update(new_state) - context = yield self._update_context_for_auth_events( + context = await self._update_context_for_auth_events( event, context, auth_events ) From 0d5f2f4bb0a1153d7d8b7d66d5c06268b2f2ea18 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 3 Feb 2020 15:55:35 +0000 Subject: [PATCH 0942/1623] make FederationHandler._update_context_for_auth_events async --- synapse/handlers/federation.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 8dd2e81fd4..ea49f928e5 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -2371,21 +2371,21 @@ class FederationHandler(BaseHandler): return context - @defer.inlineCallbacks - def _update_context_for_auth_events(self, event, context, auth_events): + async def _update_context_for_auth_events( + self, event: EventBase, context: EventContext, auth_events: StateMap[EventBase] + ) -> EventContext: """Update the state_ids in an event context after auth event resolution, storing the changes as a new state group. Args: - event (Event): The event we're handling the context for + event: The event we're handling the context for - context (synapse.events.snapshot.EventContext): initial event context + context: initial event context - auth_events (dict[(str, str)->EventBase]): Events to update in the event - context. + auth_events: Events to update in the event context. Returns: - Deferred[EventContext]: new event context + new event context """ # exclude the state key of the new event from the current_state in the context. if event.is_state(): @@ -2396,19 +2396,19 @@ class FederationHandler(BaseHandler): k: a.event_id for k, a in iteritems(auth_events) if k != event_key } - current_state_ids = yield context.get_current_state_ids() + current_state_ids = await context.get_current_state_ids() current_state_ids = dict(current_state_ids) current_state_ids.update(state_updates) - prev_state_ids = yield context.get_prev_state_ids() + prev_state_ids = await context.get_prev_state_ids() prev_state_ids = dict(prev_state_ids) prev_state_ids.update({k: a.event_id for k, a in iteritems(auth_events)}) # create a new state group as a delta from the existing one. prev_group = context.state_group - state_group = yield self.state_store.store_state_group( + state_group = await self.state_store.store_state_group( event.event_id, event.room_id, prev_group=prev_group, From 957129f4a7febacba64bc68fbcd7375db5156186 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 3 Feb 2020 16:00:46 +0000 Subject: [PATCH 0943/1623] make FederationHandler.construct_auth_difference async --- synapse/handlers/federation.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index ea49f928e5..1d9084b326 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -2425,8 +2425,9 @@ class FederationHandler(BaseHandler): delta_ids=state_updates, ) - @defer.inlineCallbacks - def construct_auth_difference(self, local_auth, remote_auth): + async def construct_auth_difference( + self, local_auth: Iterable[EventBase], remote_auth: Iterable[EventBase] + ) -> Dict: """ Given a local and remote auth chain, find the differences. This assumes that we have already processed all events in remote_auth @@ -2535,7 +2536,7 @@ class FederationHandler(BaseHandler): reason_map = {} for e in base_remote_rejected: - reason = yield self.store.get_rejection_reason(e.event_id) + reason = await self.store.get_rejection_reason(e.event_id) if reason is None: # TODO: e is not in the current state, so we should # construct some proof of that. From 863087d18643d224f3b8eb4a511429cc8669a135 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 3 Feb 2020 16:02:50 +0000 Subject: [PATCH 0944/1623] make FederationHandler.on_exchange_third_party_invite_request async --- synapse/handlers/federation.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 1d9084b326..1eafc1bf1a 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -65,7 +65,7 @@ from synapse.replication.http.federation import ( from synapse.replication.http.membership import ReplicationUserJoinedLeftRoomRestServlet from synapse.state import StateResolutionStore, resolve_events_with_store from synapse.storage.data_stores.main.events_worker import EventRedactBehaviour -from synapse.types import StateMap, UserID, get_domain_from_id +from synapse.types import JsonDict, StateMap, UserID, get_domain_from_id from synapse.util.async_helpers import Linearizer, concurrently_execute from synapse.util.distributor import user_joined_room from synapse.util.retryutils import NotRetryingDestination @@ -2621,33 +2621,31 @@ class FederationHandler(BaseHandler): destinations, room_id, event_dict ) - @defer.inlineCallbacks - @log_function - def on_exchange_third_party_invite_request(self, room_id, event_dict): + async def on_exchange_third_party_invite_request( + self, room_id: str, event_dict: JsonDict + ) -> None: """Handle an exchange_third_party_invite request from a remote server The remote server will call this when it wants to turn a 3pid invite into a normal m.room.member invite. Args: - room_id (str): The ID of the room. + room_id: The ID of the room. event_dict (dict[str, Any]): Dictionary containing the event body. - Returns: - Deferred: resolves (to None) """ - room_version = yield self.store.get_room_version_id(room_id) + room_version = await self.store.get_room_version_id(room_id) # NB: event_dict has a particular specced format we might need to fudge # if we change event formats too much. builder = self.event_builder_factory.new(room_version, event_dict) - event, context = yield self.event_creation_handler.create_new_client_event( + event, context = await self.event_creation_handler.create_new_client_event( builder=builder ) - event_allowed = yield self.third_party_event_rules.check_event_allowed( + event_allowed = await self.third_party_event_rules.check_event_allowed( event, context ) if not event_allowed: @@ -2658,16 +2656,16 @@ class FederationHandler(BaseHandler): 403, "This event is not allowed in this context", Codes.FORBIDDEN ) - event, context = yield self.add_display_name_to_third_party_invite( + event, context = await self.add_display_name_to_third_party_invite( room_version, event_dict, event, context ) try: - yield self.auth.check_from_context(room_version, event, context) + await self.auth.check_from_context(room_version, event, context) except AuthError as e: logger.warning("Denying third party invite %r because %s", event, e) raise e - yield self._check_signature(event, context) + await self._check_signature(event, context) # We need to tell the transaction queue to send this out, even # though the sender isn't a local user. @@ -2675,7 +2673,7 @@ class FederationHandler(BaseHandler): # We retrieve the room member handler here as to not cause a cyclic dependency member_handler = self.hs.get_room_member_handler() - yield member_handler.send_membership_event(None, event, context) + await member_handler.send_membership_event(None, event, context) @defer.inlineCallbacks def add_display_name_to_third_party_invite( From 94f7b4cd54612f9f1a67b3090f7b249ac20a0c76 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 3 Feb 2020 16:06:46 +0000 Subject: [PATCH 0945/1623] make FederationHandler.on_event_auth async --- synapse/handlers/federation.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 1eafc1bf1a..4e9f240e14 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1199,13 +1199,12 @@ class FederationHandler(BaseHandler): return pdu - @defer.inlineCallbacks - def on_event_auth(self, event_id): - event = yield self.store.get_event(event_id) - auth = yield self.store.get_auth_chain( + async def on_event_auth(self, event_id: str) -> List[EventBase]: + event = await self.store.get_event(event_id) + auth = await self.store.get_auth_chain( [auth_id for auth_id in event.auth_event_ids()], include_given=True ) - return [e for e in auth] + return list(auth) @log_function @defer.inlineCallbacks From ebd6a15af3994d803b8deb2172dbd0deaeb59915 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 3 Feb 2020 16:13:13 +0000 Subject: [PATCH 0946/1623] make FederationHandler.do_invite_join async --- synapse/handlers/federation.py | 30 ++++++++++++++---------------- synapse/handlers/room_member.py | 6 ++++-- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 4e9f240e14..127dc0fc02 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1206,9 +1206,9 @@ class FederationHandler(BaseHandler): ) return list(auth) - @log_function - @defer.inlineCallbacks - def do_invite_join(self, target_hosts, room_id, joinee, content): + async def do_invite_join( + self, target_hosts: Iterable[str], room_id: str, joinee: str, content: JsonDict + ) -> None: """ Attempts to join the `joinee` to the room `room_id` via the servers contained in `target_hosts`. @@ -1221,17 +1221,17 @@ class FederationHandler(BaseHandler): have finished processing the join. Args: - target_hosts (Iterable[str]): List of servers to attempt to join the room with. + target_hosts: List of servers to attempt to join the room with. - room_id (str): The ID of the room to join. + room_id: The ID of the room to join. - joinee (str): The User ID of the joining user. + joinee: The User ID of the joining user. - content (dict): The event content to use for the join event. + content: The event content to use for the join event. """ logger.debug("Joining %s to %s", joinee, room_id) - origin, event, room_version_obj = yield self._make_and_verify_event( + origin, event, room_version_obj = await self._make_and_verify_event( target_hosts, room_id, joinee, @@ -1247,7 +1247,7 @@ class FederationHandler(BaseHandler): self.room_queues[room_id] = [] - yield self._clean_room_for_join(room_id) + await self._clean_room_for_join(room_id) handled_events = set() @@ -1261,7 +1261,7 @@ class FederationHandler(BaseHandler): pass event_format_version = room_version_obj.event_format - ret = yield self.federation_client.send_join( + ret = await self.federation_client.send_join( target_hosts, event, event_format_version ) @@ -1280,7 +1280,7 @@ class FederationHandler(BaseHandler): logger.debug("do_invite_join event: %s", event) try: - yield self.store.store_room( + await self.store.store_room( room_id=room_id, room_creator_user_id="", is_public=False, @@ -1290,13 +1290,13 @@ class FederationHandler(BaseHandler): # FIXME pass - yield self._persist_auth_tree( + await self._persist_auth_tree( origin, auth_chain, state, event, room_version_obj ) # Check whether this room is the result of an upgrade of a room we already know # about. If so, migrate over user information - predecessor = yield self.store.get_room_predecessor(room_id) + predecessor = await self.store.get_room_predecessor(room_id) if not predecessor or not isinstance(predecessor.get("room_id"), str): return old_room_id = predecessor["room_id"] @@ -1306,7 +1306,7 @@ class FederationHandler(BaseHandler): # We retrieve the room member handler here as to not cause a cyclic dependency member_handler = self.hs.get_room_member_handler() - yield member_handler.transfer_room_state_on_room_upgrade( + await member_handler.transfer_room_state_on_room_upgrade( old_room_id, room_id ) @@ -1323,8 +1323,6 @@ class FederationHandler(BaseHandler): run_in_background(self._handle_queued_pdus, room_queue) - return True - async def _handle_queued_pdus(self, room_queue): """Process PDUs which got queued up while we were busy send_joining. diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index 15e8aa5249..ce8150db6e 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -944,8 +944,10 @@ class RoomMemberMasterHandler(RoomMemberHandler): # join dance for now, since we're kinda implicitly checking # that we are allowed to join when we decide whether or not we # need to do the invite/join dance. - yield self.federation_handler.do_invite_join( - remote_room_hosts, room_id, user.to_string(), content + yield defer.ensureDeferred( + self.federation_handler.do_invite_join( + remote_room_hosts, room_id, user.to_string(), content + ) ) yield self._user_joined_room(user, room_id) From dbdf843012847536f5e87f66efd48174ad348be3 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 3 Feb 2020 16:14:58 +0000 Subject: [PATCH 0947/1623] make FederationHandler._persist_auth_tree async --- synapse/handlers/federation.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 127dc0fc02..7bc5632f4d 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1852,15 +1852,14 @@ class FederationHandler(BaseHandler): backfilled=backfilled, ) - @defer.inlineCallbacks - def _persist_auth_tree( + async def _persist_auth_tree( self, origin: str, auth_events: List[EventBase], state: List[EventBase], event: EventBase, room_version: RoomVersion, - ): + ) -> None: """Checks the auth chain is valid (and passes auth checks) for the state and event. Then persists the auth chain and state atomically. Persists the event separately. Notifies about the persisted events @@ -1875,14 +1874,11 @@ class FederationHandler(BaseHandler): event room_version: The room version we expect this room to have, and will raise if it doesn't match the version in the create event. - - Returns: - Deferred """ events_to_context = {} for e in itertools.chain(auth_events, state): e.internal_metadata.outlier = True - ctx = yield self.state_handler.compute_event_context(e) + ctx = await self.state_handler.compute_event_context(e) events_to_context[e.event_id] = ctx event_map = { @@ -1914,7 +1910,7 @@ class FederationHandler(BaseHandler): missing_auth_events.add(e_id) for e_id in missing_auth_events: - m_ev = yield self.federation_client.get_pdu( + m_ev = await self.federation_client.get_pdu( [origin], e_id, room_version=room_version.identifier, @@ -1950,18 +1946,18 @@ class FederationHandler(BaseHandler): raise events_to_context[e.event_id].rejected = RejectedReason.AUTH_ERROR - yield self.persist_events_and_notify( + await self.persist_events_and_notify( [ (e, events_to_context[e.event_id]) for e in itertools.chain(auth_events, state) ] ) - new_event_context = yield self.state_handler.compute_event_context( + new_event_context = await self.state_handler.compute_event_context( event, old_state=state ) - yield self.persist_events_and_notify([(event, new_event_context)]) + await self.persist_events_and_notify([(event, new_event_context)]) async def _prep_event( self, From c3f296af32d0da0dc9e8797949ed95d9f4ca1c7f Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 3 Feb 2020 16:16:31 +0000 Subject: [PATCH 0948/1623] make FederationHandler._check_for_soft_fail async --- synapse/handlers/federation.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 7bc5632f4d..c4a291d1c0 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1997,27 +1997,23 @@ class FederationHandler(BaseHandler): return context - @defer.inlineCallbacks - def _check_for_soft_fail( + async def _check_for_soft_fail( self, event: EventBase, state: Optional[Iterable[EventBase]], backfilled: bool - ): - """Checks if we should soft fail the event, if so marks the event as + ) -> None: + """Checks if we should soft fail the event; if so, marks the event as such. Args: event state: The state at the event if we don't have all the event's prev events backfilled: Whether the event is from backfill - - Returns: - Deferred """ # For new (non-backfilled and non-outlier) events we check if the event # passes auth based on the current state. If it doesn't then we # "soft-fail" the event. do_soft_fail_check = not backfilled and not event.internal_metadata.is_outlier() if do_soft_fail_check: - extrem_ids = yield self.store.get_latest_event_ids_in_room(event.room_id) + extrem_ids = await self.store.get_latest_event_ids_in_room(event.room_id) extrem_ids = set(extrem_ids) prev_event_ids = set(event.prev_event_ids()) @@ -2028,7 +2024,7 @@ class FederationHandler(BaseHandler): do_soft_fail_check = False if do_soft_fail_check: - room_version = yield self.store.get_room_version_id(event.room_id) + room_version = await self.store.get_room_version_id(event.room_id) room_version_obj = KNOWN_ROOM_VERSIONS[room_version] # Calculate the "current state". @@ -2045,19 +2041,19 @@ class FederationHandler(BaseHandler): # given state at the event. This should correctly handle cases # like bans, especially with state res v2. - state_sets = yield self.state_store.get_state_groups( + state_sets = await self.state_store.get_state_groups( event.room_id, extrem_ids ) state_sets = list(state_sets.values()) state_sets.append(state) - current_state_ids = yield self.state_handler.resolve_events( + current_state_ids = await self.state_handler.resolve_events( room_version, state_sets, event ) current_state_ids = { k: e.event_id for k, e in iteritems(current_state_ids) } else: - current_state_ids = yield self.state_handler.get_current_state_ids( + current_state_ids = await self.state_handler.get_current_state_ids( event.room_id, latest_event_ids=extrem_ids ) @@ -2073,7 +2069,7 @@ class FederationHandler(BaseHandler): e for k, e in iteritems(current_state_ids) if k in auth_types ] - current_auth_events = yield self.store.get_events(current_state_ids) + current_auth_events = await self.store.get_events(current_state_ids) current_auth_events = { (e.type, e.state_key): e for e in current_auth_events.values() } From 4286e429a7359cdeb670fdd63839d0b73836026b Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 3 Feb 2020 16:19:18 +0000 Subject: [PATCH 0949/1623] make FederationHandler.do_remotely_reject_invite async --- synapse/handlers/federation.py | 13 +++++++------ synapse/handlers/room_member.py | 6 ++++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index c4a291d1c0..73ef130ace 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1531,9 +1531,10 @@ class FederationHandler(BaseHandler): return event - @defer.inlineCallbacks - def do_remotely_reject_invite(self, target_hosts, room_id, user_id, content): - origin, event, room_version = yield self._make_and_verify_event( + async def do_remotely_reject_invite( + self, target_hosts: Iterable[str], room_id: str, user_id: str, content: JsonDict + ) -> EventBase: + origin, event, room_version = await self._make_and_verify_event( target_hosts, room_id, user_id, "leave", content=content ) # Mark as outlier as we don't have any state for this event; we're not @@ -1549,10 +1550,10 @@ class FederationHandler(BaseHandler): except ValueError: pass - yield self.federation_client.send_leave(target_hosts, event) + await self.federation_client.send_leave(target_hosts, event) - context = yield self.state_handler.compute_event_context(event) - yield self.persist_events_and_notify([(event, context)]) + context = await self.state_handler.compute_event_context(event) + await self.persist_events_and_notify([(event, context)]) return event diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index ce8150db6e..4260426369 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -984,8 +984,10 @@ class RoomMemberMasterHandler(RoomMemberHandler): """ fed_handler = self.federation_handler try: - ret = yield fed_handler.do_remotely_reject_invite( - remote_room_hosts, room_id, target.to_string(), content=content, + ret = yield defer.ensureDeferred( + fed_handler.do_remotely_reject_invite( + remote_room_hosts, room_id, target.to_string(), content=content, + ) ) return ret except Exception as e: From 3b7e0e002bad4ba39e9d4b4188fae0132571bded Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 3 Feb 2020 16:22:30 +0000 Subject: [PATCH 0950/1623] make FederationHandler._make_and_verify_event async --- synapse/handlers/federation.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 73ef130ace..e5fa55b973 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1557,15 +1557,20 @@ class FederationHandler(BaseHandler): return event - @defer.inlineCallbacks - def _make_and_verify_event( - self, target_hosts, room_id, user_id, membership, content={}, params=None - ): + async def _make_and_verify_event( + self, + target_hosts: Iterable[str], + room_id: str, + user_id: str, + membership: str, + content: JsonDict = {}, + params: Optional[Dict[str, str]] = None, + ) -> Tuple[str, EventBase, RoomVersion]: ( origin, event, room_version, - ) = yield self.federation_client.make_membership_event( + ) = await self.federation_client.make_membership_event( target_hosts, room_id, user_id, membership, content, params=params ) From 05299599b61bd19805a5b7843b907b1b5954e1de Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 3 Feb 2020 16:24:07 +0000 Subject: [PATCH 0951/1623] make FederationHandler.persist_events_and_notify async --- synapse/handlers/federation.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index e5fa55b973..ec010c1f9f 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -2816,27 +2816,27 @@ class FederationHandler(BaseHandler): if "valid" not in response or not response["valid"]: raise AuthError(403, "Third party certificate was invalid") - @defer.inlineCallbacks - def persist_events_and_notify(self, event_and_contexts, backfilled=False): + async def persist_events_and_notify( + self, + event_and_contexts: Sequence[Tuple[EventBase, EventContext]], + backfilled: bool = False, + ) -> None: """Persists events and tells the notifier/pushers about them, if necessary. Args: - event_and_contexts(list[tuple[FrozenEvent, EventContext]]) - backfilled (bool): Whether these events are a result of + event_and_contexts: + backfilled: Whether these events are a result of backfilling or not - - Returns: - Deferred """ if self.config.worker_app: - yield self._send_events_to_master( + await self._send_events_to_master( store=self.store, event_and_contexts=event_and_contexts, backfilled=backfilled, ) else: - max_stream_id = yield self.storage.persistence.persist_events( + max_stream_id = await self.storage.persistence.persist_events( event_and_contexts, backfilled=backfilled ) @@ -2847,7 +2847,7 @@ class FederationHandler(BaseHandler): if not backfilled: # Never notify for backfilled events for event, _ in event_and_contexts: - yield self._notify_persisted_event(event, max_stream_id) + await self._notify_persisted_event(event, max_stream_id) def _notify_persisted_event(self, event, max_stream_id): """Checks to see if notifier/pushers should be notified about the From 814cc00cb96168479a5c462e9078f3b60901589d Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 3 Feb 2020 16:25:41 +0000 Subject: [PATCH 0952/1623] make FederationHandler._notify_persisted_event async --- synapse/handlers/federation.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index ec010c1f9f..19cd55b8cf 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -2849,13 +2849,15 @@ class FederationHandler(BaseHandler): for event, _ in event_and_contexts: await self._notify_persisted_event(event, max_stream_id) - def _notify_persisted_event(self, event, max_stream_id): + async def _notify_persisted_event( + self, event: EventBase, max_stream_id: int + ) -> None: """Checks to see if notifier/pushers should be notified about the event or not. Args: - event (FrozenEvent) - max_stream_id (int): The max_stream_id returned by persist_events + event: + max_stream_id: The max_stream_id returned by persist_events """ extra_users = [] @@ -2879,7 +2881,7 @@ class FederationHandler(BaseHandler): event, event_stream_id, max_stream_id, extra_users=extra_users ) - return self.pusher_pool.on_new_notifications(event_stream_id, max_stream_id) + await self.pusher_pool.on_new_notifications(event_stream_id, max_stream_id) def _clean_room_for_join(self, room_id): """Called to clean up any data in DB for a given room, ready for the From 52642860dabac96467d7a7947976c53eb5dc4c82 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 3 Feb 2020 16:27:05 +0000 Subject: [PATCH 0953/1623] make FederationHandler._clean_room_for_join async --- synapse/handlers/federation.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 19cd55b8cf..e252e69888 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -2883,17 +2883,17 @@ class FederationHandler(BaseHandler): await self.pusher_pool.on_new_notifications(event_stream_id, max_stream_id) - def _clean_room_for_join(self, room_id): + async def _clean_room_for_join(self, room_id: str) -> None: """Called to clean up any data in DB for a given room, ready for the server to join the room. Args: - room_id (str) + room_id """ if self.config.worker_app: - return self._clean_room_for_join_client(room_id) + await self._clean_room_for_join_client(room_id) else: - return self.store.clean_room_for_join(room_id) + await self.store.clean_room_for_join(room_id) def user_joined_room(self, user, room_id): """Called when a new user has joined the room From f64c96662ed794bbdd8960002658602a1b92eb2b Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 3 Feb 2020 16:28:31 +0000 Subject: [PATCH 0954/1623] make FederationHandler.user_joined_room async --- synapse/handlers/federation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index e252e69888..c94573b547 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -2895,15 +2895,15 @@ class FederationHandler(BaseHandler): else: await self.store.clean_room_for_join(room_id) - def user_joined_room(self, user, room_id): + async def user_joined_room(self, user: UserID, room_id: str) -> None: """Called when a new user has joined the room """ if self.config.worker_app: - return self._notify_user_membership_change( + await self._notify_user_membership_change( room_id=room_id, user_id=user.to_string(), change="joined" ) else: - return defer.succeed(user_joined_room(self.distributor, user, room_id)) + user_joined_room(self.distributor, user, room_id) @defer.inlineCallbacks def get_room_complexity(self, remote_room_hosts, room_id): From e49eb1a886c6f139887b1e71f8234e02e738a84a Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 3 Feb 2020 16:30:21 +0000 Subject: [PATCH 0955/1623] changelog --- changelog.d/6837.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6837.misc diff --git a/changelog.d/6837.misc b/changelog.d/6837.misc new file mode 100644 index 0000000000..0496f12de8 --- /dev/null +++ b/changelog.d/6837.misc @@ -0,0 +1 @@ +Port much of `synapse.handlers.federation` to async/await. From ae5b3104f0023171b2bb89f08a066e5974ee7666 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 3 Feb 2020 17:10:54 +0000 Subject: [PATCH 0956/1623] Fix stacktraces when using ObservableDeferred and async/await (#6836) --- changelog.d/6836.misc | 1 + synapse/util/async_helpers.py | 4 ++++ 2 files changed, 5 insertions(+) create mode 100644 changelog.d/6836.misc diff --git a/changelog.d/6836.misc b/changelog.d/6836.misc new file mode 100644 index 0000000000..232488e1e5 --- /dev/null +++ b/changelog.d/6836.misc @@ -0,0 +1 @@ +Fix stacktraces when using `ObservableDeferred` and async/await. diff --git a/synapse/util/async_helpers.py b/synapse/util/async_helpers.py index 04b6abdc24..581dffd8a0 100644 --- a/synapse/util/async_helpers.py +++ b/synapse/util/async_helpers.py @@ -73,6 +73,10 @@ class ObservableDeferred(object): def errback(f): object.__setattr__(self, "_result", (False, f)) while self._observers: + # This is a little bit of magic to correctly propagate stack + # traces when we `await` on one of the observer deferreds. + f.value.__failure__ = f + try: # TODO: Handle errors here. self._observers.pop().errback(f) From b9391c957572224c3a7c22870102fcbd24dea4e0 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 3 Feb 2020 18:05:44 +0000 Subject: [PATCH 0957/1623] Add typing to SyncHandler (#6821) Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> --- changelog.d/6821.misc | 1 + synapse/events/__init__.py | 18 +- synapse/handlers/sync.py | 697 +++++++++++++++++--------------- tests/storage/test_redaction.py | 5 +- tox.ini | 1 + 5 files changed, 377 insertions(+), 345 deletions(-) create mode 100644 changelog.d/6821.misc diff --git a/changelog.d/6821.misc b/changelog.d/6821.misc new file mode 100644 index 0000000000..1d5265d5e2 --- /dev/null +++ b/changelog.d/6821.misc @@ -0,0 +1 @@ +Add type hints to `SyncHandler`. diff --git a/synapse/events/__init__.py b/synapse/events/__init__.py index f813fa2fe7..92f76703b3 100644 --- a/synapse/events/__init__.py +++ b/synapse/events/__init__.py @@ -189,8 +189,14 @@ class EventBase(object): redacts = _event_dict_property("redacts", None) room_id = _event_dict_property("room_id") sender = _event_dict_property("sender") + state_key = _event_dict_property("state_key") + type = _event_dict_property("type") user_id = _event_dict_property("sender") + @property + def event_id(self) -> str: + raise NotImplementedError() + @property def membership(self): return self.content["membership"] @@ -281,10 +287,7 @@ class FrozenEvent(EventBase): else: frozen_dict = event_dict - self.event_id = event_dict["event_id"] - self.type = event_dict["type"] - if "state_key" in event_dict: - self.state_key = event_dict["state_key"] + self._event_id = event_dict["event_id"] super(FrozenEvent, self).__init__( frozen_dict, @@ -294,6 +297,10 @@ class FrozenEvent(EventBase): rejected_reason=rejected_reason, ) + @property + def event_id(self) -> str: + return self._event_id + def __str__(self): return self.__repr__() @@ -332,9 +339,6 @@ class FrozenEventV2(EventBase): frozen_dict = event_dict self._event_id = None - self.type = event_dict["type"] - if "state_key" in event_dict: - self.state_key = event_dict["state_key"] super(FrozenEventV2, self).__init__( frozen_dict, diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index cd95f85e3f..5f060241b4 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -14,20 +14,30 @@ # See the License for the specific language governing permissions and # limitations under the License. -import collections import itertools import logging +from typing import Any, Dict, FrozenSet, List, Optional, Set, Tuple from six import iteritems, itervalues +import attr from prometheus_client import Counter from synapse.api.constants import EventTypes, Membership +from synapse.api.filtering import FilterCollection +from synapse.events import EventBase from synapse.logging.context import LoggingContext from synapse.push.clientformat import format_push_rules_for_user from synapse.storage.roommember import MemberSummary from synapse.storage.state import StateFilter -from synapse.types import RoomStreamToken +from synapse.types import ( + Collection, + JsonDict, + RoomStreamToken, + StateMap, + StreamToken, + UserID, +) from synapse.util.async_helpers import concurrently_execute from synapse.util.caches.expiringcache import ExpiringCache from synapse.util.caches.lrucache import LruCache @@ -62,17 +72,22 @@ LAZY_LOADED_MEMBERS_CACHE_MAX_AGE = 30 * 60 * 1000 LAZY_LOADED_MEMBERS_CACHE_MAX_SIZE = 100 -SyncConfig = collections.namedtuple( - "SyncConfig", ["user", "filter_collection", "is_guest", "request_key", "device_id"] -) +@attr.s(slots=True, frozen=True) +class SyncConfig: + user = attr.ib(type=UserID) + filter_collection = attr.ib(type=FilterCollection) + is_guest = attr.ib(type=bool) + request_key = attr.ib(type=Tuple[Any, ...]) + device_id = attr.ib(type=str) -class TimelineBatch( - collections.namedtuple("TimelineBatch", ["prev_batch", "events", "limited"]) -): - __slots__ = [] +@attr.s(slots=True, frozen=True) +class TimelineBatch: + prev_batch = attr.ib(type=StreamToken) + events = attr.ib(type=List[EventBase]) + limited = attr.ib(bool) - def __nonzero__(self): + def __nonzero__(self) -> bool: """Make the result appear empty if there are no updates. This is used to tell if room needs to be part of the sync result. """ @@ -81,23 +96,17 @@ class TimelineBatch( __bool__ = __nonzero__ # python3 -class JoinedSyncResult( - collections.namedtuple( - "JoinedSyncResult", - [ - "room_id", # str - "timeline", # TimelineBatch - "state", # dict[(str, str), FrozenEvent] - "ephemeral", - "account_data", - "unread_notifications", - "summary", - ], - ) -): - __slots__ = [] +@attr.s(slots=True, frozen=True) +class JoinedSyncResult: + room_id = attr.ib(type=str) + timeline = attr.ib(type=TimelineBatch) + state = attr.ib(type=StateMap[EventBase]) + ephemeral = attr.ib(type=List[JsonDict]) + account_data = attr.ib(type=List[JsonDict]) + unread_notifications = attr.ib(type=JsonDict) + summary = attr.ib(type=Optional[JsonDict]) - def __nonzero__(self): + def __nonzero__(self) -> bool: """Make the result appear empty if there are no updates. This is used to tell if room needs to be part of the sync result. """ @@ -113,20 +122,14 @@ class JoinedSyncResult( __bool__ = __nonzero__ # python3 -class ArchivedSyncResult( - collections.namedtuple( - "ArchivedSyncResult", - [ - "room_id", # str - "timeline", # TimelineBatch - "state", # dict[(str, str), FrozenEvent] - "account_data", - ], - ) -): - __slots__ = [] +@attr.s(slots=True, frozen=True) +class ArchivedSyncResult: + room_id = attr.ib(type=str) + timeline = attr.ib(type=TimelineBatch) + state = attr.ib(type=StateMap[EventBase]) + account_data = attr.ib(type=List[JsonDict]) - def __nonzero__(self): + def __nonzero__(self) -> bool: """Make the result appear empty if there are no updates. This is used to tell if room needs to be part of the sync result. """ @@ -135,70 +138,88 @@ class ArchivedSyncResult( __bool__ = __nonzero__ # python3 -class InvitedSyncResult( - collections.namedtuple( - "InvitedSyncResult", - ["room_id", "invite"], # str # FrozenEvent: the invite event - ) -): - __slots__ = [] +@attr.s(slots=True, frozen=True) +class InvitedSyncResult: + room_id = attr.ib(type=str) + invite = attr.ib(type=EventBase) - def __nonzero__(self): + def __nonzero__(self) -> bool: """Invited rooms should always be reported to the client""" return True __bool__ = __nonzero__ # python3 -class GroupsSyncResult( - collections.namedtuple("GroupsSyncResult", ["join", "invite", "leave"]) -): - __slots__ = [] +@attr.s(slots=True, frozen=True) +class GroupsSyncResult: + join = attr.ib(type=JsonDict) + invite = attr.ib(type=JsonDict) + leave = attr.ib(type=JsonDict) - def __nonzero__(self): + def __nonzero__(self) -> bool: return bool(self.join or self.invite or self.leave) __bool__ = __nonzero__ # python3 -class DeviceLists( - collections.namedtuple( - "DeviceLists", - [ - "changed", # list of user_ids whose devices may have changed - "left", # list of user_ids whose devices we no longer track - ], - ) -): - __slots__ = [] +@attr.s(slots=True, frozen=True) +class DeviceLists: + """ + Attributes: + changed: List of user_ids whose devices may have changed + left: List of user_ids whose devices we no longer track + """ - def __nonzero__(self): + changed = attr.ib(type=Collection[str]) + left = attr.ib(type=Collection[str]) + + def __nonzero__(self) -> bool: return bool(self.changed or self.left) __bool__ = __nonzero__ # python3 -class SyncResult( - collections.namedtuple( - "SyncResult", - [ - "next_batch", # Token for the next sync - "presence", # List of presence events for the user. - "account_data", # List of account_data events for the user. - "joined", # JoinedSyncResult for each joined room. - "invited", # InvitedSyncResult for each invited room. - "archived", # ArchivedSyncResult for each archived room. - "to_device", # List of direct messages for the device. - "device_lists", # List of user_ids whose devices have changed - "device_one_time_keys_count", # Dict of algorithm to count for one time keys - # for this device - "groups", - ], - ) -): - __slots__ = [] +@attr.s +class _RoomChanges: + """The set of room entries to include in the sync, plus the set of joined + and left room IDs since last sync. + """ - def __nonzero__(self): + room_entries = attr.ib(type=List["RoomSyncResultBuilder"]) + invited = attr.ib(type=List[InvitedSyncResult]) + newly_joined_rooms = attr.ib(type=List[str]) + newly_left_rooms = attr.ib(type=List[str]) + + +@attr.s(slots=True, frozen=True) +class SyncResult: + """ + Attributes: + next_batch: Token for the next sync + presence: List of presence events for the user. + account_data: List of account_data events for the user. + joined: JoinedSyncResult for each joined room. + invited: InvitedSyncResult for each invited room. + archived: ArchivedSyncResult for each archived room. + to_device: List of direct messages for the device. + device_lists: List of user_ids whose devices have changed + device_one_time_keys_count: Dict of algorithm to count for one time keys + for this device + groups: Group updates, if any + """ + + next_batch = attr.ib(type=StreamToken) + presence = attr.ib(type=List[JsonDict]) + account_data = attr.ib(type=List[JsonDict]) + joined = attr.ib(type=List[JoinedSyncResult]) + invited = attr.ib(type=List[InvitedSyncResult]) + archived = attr.ib(type=List[ArchivedSyncResult]) + to_device = attr.ib(type=List[JsonDict]) + device_lists = attr.ib(type=DeviceLists) + device_one_time_keys_count = attr.ib(type=JsonDict) + groups = attr.ib(type=Optional[GroupsSyncResult]) + + def __nonzero__(self) -> bool: """Make the result appear empty if there are no updates. This is used to tell if the notifier needs to wait for more events when polling for events. @@ -240,13 +261,15 @@ class SyncHandler(object): ) async def wait_for_sync_for_user( - self, sync_config, since_token=None, timeout=0, full_state=False - ): + self, + sync_config: SyncConfig, + since_token: Optional[StreamToken] = None, + timeout: int = 0, + full_state: bool = False, + ) -> SyncResult: """Get the sync for a client if we have new data for it now. Otherwise wait for new data to arrive on the server. If the timeout expires, then return an empty sync result. - Returns: - Deferred[SyncResult] """ # If the user is not part of the mau group, then check that limits have # not been exceeded (if not part of the group by this point, almost certain @@ -265,8 +288,12 @@ class SyncHandler(object): return res async def _wait_for_sync_for_user( - self, sync_config, since_token, timeout, full_state - ): + self, + sync_config: SyncConfig, + since_token: Optional[StreamToken] = None, + timeout: int = 0, + full_state: bool = False, + ) -> SyncResult: if since_token is None: sync_type = "initial_sync" elif full_state: @@ -305,25 +332,33 @@ class SyncHandler(object): return result - def current_sync_for_user(self, sync_config, since_token=None, full_state=False): + async def current_sync_for_user( + self, + sync_config: SyncConfig, + since_token: Optional[StreamToken] = None, + full_state: bool = False, + ) -> SyncResult: """Get the sync for client needed to match what the server has now. - Returns: - A Deferred SyncResult. """ - return self.generate_sync_result(sync_config, since_token, full_state) + return await self.generate_sync_result(sync_config, since_token, full_state) - async def push_rules_for_user(self, user): + async def push_rules_for_user(self, user: UserID) -> JsonDict: user_id = user.to_string() rules = await self.store.get_push_rules_for_user(user_id) rules = format_push_rules_for_user(user, rules) return rules - async def ephemeral_by_room(self, sync_result_builder, now_token, since_token=None): + async def ephemeral_by_room( + self, + sync_result_builder: "SyncResultBuilder", + now_token: StreamToken, + since_token: Optional[StreamToken] = None, + ) -> Tuple[StreamToken, Dict[str, List[JsonDict]]]: """Get the ephemeral events for each room the user is in Args: - sync_result_builder(SyncResultBuilder) - now_token (StreamToken): Where the server is currently up to. - since_token (StreamToken): Where the server was when the client + sync_result_builder + now_token: Where the server is currently up to. + since_token: Where the server was when the client last synced. Returns: A tuple of the now StreamToken, updated to reflect the which typing @@ -348,7 +383,7 @@ class SyncHandler(object): ) now_token = now_token.copy_and_replace("typing_key", typing_key) - ephemeral_by_room = {} + ephemeral_by_room = {} # type: JsonDict for event in typing: # we want to exclude the room_id from the event, but modifying the @@ -380,13 +415,13 @@ class SyncHandler(object): async def _load_filtered_recents( self, - room_id, - sync_config, - now_token, - since_token=None, - recents=None, - newly_joined_room=False, - ): + room_id: str, + sync_config: SyncConfig, + now_token: StreamToken, + since_token: Optional[StreamToken] = None, + potential_recents: Optional[List[EventBase]] = None, + newly_joined_room: bool = False, + ) -> TimelineBatch: """ Returns: a Deferred TimelineBatch @@ -397,21 +432,29 @@ class SyncHandler(object): sync_config.filter_collection.blocks_all_room_timeline() ) - if recents is None or newly_joined_room or timeline_limit < len(recents): + if ( + potential_recents is None + or newly_joined_room + or timeline_limit < len(potential_recents) + ): limited = True else: limited = False - if recents: - recents = sync_config.filter_collection.filter_room_timeline(recents) + if potential_recents: + recents = sync_config.filter_collection.filter_room_timeline( + potential_recents + ) # We check if there are any state events, if there are then we pass # all current state events to the filter_events function. This is to # ensure that we always include current state in the timeline - current_state_ids = frozenset() + current_state_ids = frozenset() # type: FrozenSet[str] if any(e.is_state() for e in recents): - current_state_ids = await self.state.get_current_state_ids(room_id) - current_state_ids = frozenset(itervalues(current_state_ids)) + current_state_ids_map = await self.state.get_current_state_ids( + room_id + ) + current_state_ids = frozenset(itervalues(current_state_ids_map)) recents = await filter_events_for_client( self.storage, @@ -463,8 +506,10 @@ class SyncHandler(object): # ensure that we always include current state in the timeline current_state_ids = frozenset() if any(e.is_state() for e in loaded_recents): - current_state_ids = await self.state.get_current_state_ids(room_id) - current_state_ids = frozenset(itervalues(current_state_ids)) + current_state_ids_map = await self.state.get_current_state_ids( + room_id + ) + current_state_ids = frozenset(itervalues(current_state_ids_map)) loaded_recents = await filter_events_for_client( self.storage, @@ -493,17 +538,15 @@ class SyncHandler(object): limited=limited or newly_joined_room, ) - async def get_state_after_event(self, event, state_filter=StateFilter.all()): + async def get_state_after_event( + self, event: EventBase, state_filter: StateFilter = StateFilter.all() + ) -> StateMap[str]: """ Get the room state after the given event Args: - event(synapse.events.EventBase): event of interest - state_filter (StateFilter): The state filter used to fetch state - from the database. - - Returns: - A Deferred map from ((type, state_key)->Event) + event: event of interest + state_filter: The state filter used to fetch state from the database. """ state_ids = await self.state_store.get_state_ids_for_event( event.event_id, state_filter=state_filter @@ -514,18 +557,17 @@ class SyncHandler(object): return state_ids async def get_state_at( - self, room_id, stream_position, state_filter=StateFilter.all() - ): + self, + room_id: str, + stream_position: StreamToken, + state_filter: StateFilter = StateFilter.all(), + ) -> StateMap[str]: """ Get the room state at a particular stream position Args: - room_id(str): room for which to get state - stream_position(StreamToken): point at which to get state - state_filter (StateFilter): The state filter used to fetch state - from the database. - - Returns: - A Deferred map from ((type, state_key)->Event) + room_id: room for which to get state + stream_position: point at which to get state + state_filter: The state filter used to fetch state from the database. """ # FIXME this claims to get the state at a stream position, but # get_recent_events_for_room operates by topo ordering. This therefore @@ -546,23 +588,25 @@ class SyncHandler(object): state = {} return state - async def compute_summary(self, room_id, sync_config, batch, state, now_token): + async def compute_summary( + self, + room_id: str, + sync_config: SyncConfig, + batch: TimelineBatch, + state: StateMap[EventBase], + now_token: StreamToken, + ) -> Optional[JsonDict]: """ Works out a room summary block for this room, summarising the number of joined members in the room, and providing the 'hero' members if the room has no name so clients can consistently name rooms. Also adds state events to 'state' if needed to describe the heroes. - Args: - room_id(str): - sync_config(synapse.handlers.sync.SyncConfig): - batch(synapse.handlers.sync.TimelineBatch): The timeline batch for - the room that will be sent to the user. - state(dict): dict of (type, state_key) -> Event as returned by - compute_state_delta - now_token(str): Token of the end of the current batch. - - Returns: - A deferred dict describing the room summary + Args + room_id + sync_config + batch: The timeline batch for the room that will be sent to the user. + state: State as returned by compute_state_delta + now_token: Token of the end of the current batch. """ # FIXME: we could/should get this from room_stats when matthew/stats lands @@ -681,7 +725,7 @@ class SyncHandler(object): return summary - def get_lazy_loaded_members_cache(self, cache_key): + def get_lazy_loaded_members_cache(self, cache_key: Tuple[str, str]) -> LruCache: cache = self.lazy_loaded_members_cache.get(cache_key) if cache is None: logger.debug("creating LruCache for %r", cache_key) @@ -692,23 +736,24 @@ class SyncHandler(object): return cache async def compute_state_delta( - self, room_id, batch, sync_config, since_token, now_token, full_state - ): + self, + room_id: str, + batch: TimelineBatch, + sync_config: SyncConfig, + since_token: Optional[StreamToken], + now_token: StreamToken, + full_state: bool, + ) -> StateMap[EventBase]: """ Works out the difference in state between the start of the timeline and the previous sync. Args: - room_id(str): - batch(synapse.handlers.sync.TimelineBatch): The timeline batch for - the room that will be sent to the user. - sync_config(synapse.handlers.sync.SyncConfig): - since_token(str|None): Token of the end of the previous batch. May - be None. - now_token(str): Token of the end of the current batch. - full_state(bool): Whether to force returning the full state. - - Returns: - A deferred dict of (type, state_key) -> Event + room_id: + batch: The timeline batch for the room that will be sent to the user. + sync_config: + since_token: Token of the end of the previous batch. May be None. + now_token: Token of the end of the current batch. + full_state: Whether to force returning the full state. """ # TODO(mjark) Check if the state events were received by the server # after the previous sync, since we need to include those state @@ -800,6 +845,10 @@ class SyncHandler(object): # about them). state_filter = StateFilter.all() + # If this is an initial sync then full_state should be set, and + # that case is handled above. We assert here to ensure that this + # is indeed the case. + assert since_token is not None state_at_previous_sync = await self.get_state_at( room_id, stream_position=since_token, state_filter=state_filter ) @@ -874,7 +923,7 @@ class SyncHandler(object): if t[0] == EventTypes.Member: cache.set(t[1], event_id) - state = {} + state = {} # type: Dict[str, EventBase] if state_ids: state = await self.store.get_events(list(state_ids.values())) @@ -885,7 +934,9 @@ class SyncHandler(object): ) } - async def unread_notifs_for_room_id(self, room_id, sync_config): + async def unread_notifs_for_room_id( + self, room_id: str, sync_config: SyncConfig + ) -> Optional[Dict[str, str]]: with Measure(self.clock, "unread_notifs_for_room_id"): last_unread_event_id = await self.store.get_last_receipt_event_id_for_user( user_id=sync_config.user.to_string(), @@ -893,7 +944,6 @@ class SyncHandler(object): receipt_type="m.read", ) - notifs = [] if last_unread_event_id: notifs = await self.store.get_unread_event_push_actions_by_room_for_user( room_id, sync_config.user.to_string(), last_unread_event_id @@ -905,17 +955,12 @@ class SyncHandler(object): return None async def generate_sync_result( - self, sync_config, since_token=None, full_state=False - ): + self, + sync_config: SyncConfig, + since_token: Optional[StreamToken] = None, + full_state: bool = False, + ) -> SyncResult: """Generates a sync result. - - Args: - sync_config (SyncConfig) - since_token (StreamToken) - full_state (bool) - - Returns: - Deferred(SyncResult) """ # NB: The now_token gets changed by some of the generate_sync_* methods, # this is due to some of the underlying streams not supporting the ability @@ -977,7 +1022,7 @@ class SyncHandler(object): ) device_id = sync_config.device_id - one_time_key_counts = {} + one_time_key_counts = {} # type: JsonDict if device_id: one_time_key_counts = await self.store.count_e2e_one_time_keys( user_id, device_id @@ -1007,7 +1052,9 @@ class SyncHandler(object): ) @measure_func("_generate_sync_entry_for_groups") - async def _generate_sync_entry_for_groups(self, sync_result_builder): + async def _generate_sync_entry_for_groups( + self, sync_result_builder: "SyncResultBuilder" + ) -> None: user_id = sync_result_builder.sync_config.user.to_string() since_token = sync_result_builder.since_token now_token = sync_result_builder.now_token @@ -1052,27 +1099,22 @@ class SyncHandler(object): @measure_func("_generate_sync_entry_for_device_list") async def _generate_sync_entry_for_device_list( self, - sync_result_builder, - newly_joined_rooms, - newly_joined_or_invited_users, - newly_left_rooms, - newly_left_users, - ): + sync_result_builder: "SyncResultBuilder", + newly_joined_rooms: Set[str], + newly_joined_or_invited_users: Set[str], + newly_left_rooms: Set[str], + newly_left_users: Set[str], + ) -> DeviceLists: """Generate the DeviceLists section of sync Args: - sync_result_builder (SyncResultBuilder) - newly_joined_rooms (set[str]): Set of rooms user has joined since + sync_result_builder + newly_joined_rooms: Set of rooms user has joined since previous sync + newly_joined_or_invited_users: Set of users that have joined or + been invited to a room since previous sync. + newly_left_rooms: Set of rooms user has left since previous sync + newly_left_users: Set of users that have left a room we're in since previous sync - newly_joined_or_invited_users (set[str]): Set of users that have - joined or been invited to a room since previous sync. - newly_left_rooms (set[str]): Set of rooms user has left since - previous sync - newly_left_users (set[str]): Set of users that have left a room - we're in since previous sync - - Returns: - Deferred[DeviceLists] """ user_id = sync_result_builder.sync_config.user.to_string() @@ -1133,15 +1175,11 @@ class SyncHandler(object): else: return DeviceLists(changed=[], left=[]) - async def _generate_sync_entry_for_to_device(self, sync_result_builder): + async def _generate_sync_entry_for_to_device( + self, sync_result_builder: "SyncResultBuilder" + ) -> None: """Generates the portion of the sync response. Populates `sync_result_builder` with the result. - - Args: - sync_result_builder(SyncResultBuilder) - - Returns: - Deferred(dict): A dictionary containing the per room account data. """ user_id = sync_result_builder.sync_config.user.to_string() device_id = sync_result_builder.sync_config.device_id @@ -1179,15 +1217,17 @@ class SyncHandler(object): else: sync_result_builder.to_device = [] - async def _generate_sync_entry_for_account_data(self, sync_result_builder): + async def _generate_sync_entry_for_account_data( + self, sync_result_builder: "SyncResultBuilder" + ) -> Dict[str, Dict[str, JsonDict]]: """Generates the account data portion of the sync response. Populates `sync_result_builder` with the result. Args: - sync_result_builder(SyncResultBuilder) + sync_result_builder Returns: - Deferred(dict): A dictionary containing the per room account data. + A dictionary containing the per room account data. """ sync_config = sync_result_builder.sync_config user_id = sync_result_builder.sync_config.user.to_string() @@ -1231,18 +1271,21 @@ class SyncHandler(object): return account_data_by_room async def _generate_sync_entry_for_presence( - self, sync_result_builder, newly_joined_rooms, newly_joined_or_invited_users - ): + self, + sync_result_builder: "SyncResultBuilder", + newly_joined_rooms: Set[str], + newly_joined_or_invited_users: Set[str], + ) -> None: """Generates the presence portion of the sync response. Populates the `sync_result_builder` with the result. Args: - sync_result_builder(SyncResultBuilder) - newly_joined_rooms(list): List of rooms that the user has joined - since the last sync (or empty if an initial sync) - newly_joined_or_invited_users(list): List of users that have joined - or been invited to rooms since the last sync (or empty if an initial - sync) + sync_result_builder + newly_joined_rooms: Set of rooms that the user has joined since + the last sync (or empty if an initial sync) + newly_joined_or_invited_users: Set of users that have joined or + been invited to rooms since the last sync (or empty if an + initial sync) """ now_token = sync_result_builder.now_token sync_config = sync_result_builder.sync_config @@ -1286,17 +1329,19 @@ class SyncHandler(object): sync_result_builder.presence = presence async def _generate_sync_entry_for_rooms( - self, sync_result_builder, account_data_by_room - ): + self, + sync_result_builder: "SyncResultBuilder", + account_data_by_room: Dict[str, Dict[str, JsonDict]], + ) -> Tuple[Set[str], Set[str], Set[str], Set[str]]: """Generates the rooms portion of the sync response. Populates the `sync_result_builder` with the result. Args: - sync_result_builder(SyncResultBuilder) - account_data_by_room(dict): Dictionary of per room account data + sync_result_builder + account_data_by_room: Dictionary of per room account data Returns: - Deferred(tuple): Returns a 4-tuple of + Returns a 4-tuple of `(newly_joined_rooms, newly_joined_or_invited_users, newly_left_rooms, newly_left_users)` """ @@ -1307,7 +1352,7 @@ class SyncHandler(object): ) if block_all_room_ephemeral: - ephemeral_by_room = {} + ephemeral_by_room = {} # type: Dict[str, List[JsonDict]] else: now_token, ephemeral_by_room = await self.ephemeral_by_room( sync_result_builder, @@ -1328,7 +1373,7 @@ class SyncHandler(object): ) if not tags_by_room: logger.debug("no-oping sync") - return [], [], [], [] + return set(), set(), set(), set() ignored_account_data = await self.store.get_global_account_data_by_type_for_user( "m.ignored_user_list", user_id=user_id @@ -1340,19 +1385,22 @@ class SyncHandler(object): ignored_users = frozenset() if since_token: - res = await self._get_rooms_changed(sync_result_builder, ignored_users) - room_entries, invited, newly_joined_rooms, newly_left_rooms = res - + room_changes = await self._get_rooms_changed( + sync_result_builder, ignored_users + ) tags_by_room = await self.store.get_updated_tags( user_id, since_token.account_data_key ) else: - res = await self._get_all_rooms(sync_result_builder, ignored_users) - room_entries, invited, newly_joined_rooms = res - newly_left_rooms = [] + room_changes = await self._get_all_rooms(sync_result_builder, ignored_users) tags_by_room = await self.store.get_tags_for_user(user_id) + room_entries = room_changes.room_entries + invited = room_changes.invited + newly_joined_rooms = room_changes.newly_joined_rooms + newly_left_rooms = room_changes.newly_left_rooms + def handle_room_entries(room_entry): return self._generate_room_entry( sync_result_builder, @@ -1392,13 +1440,15 @@ class SyncHandler(object): newly_left_users -= newly_joined_or_invited_users return ( - newly_joined_rooms, + set(newly_joined_rooms), newly_joined_or_invited_users, - newly_left_rooms, + set(newly_left_rooms), newly_left_users, ) - async def _have_rooms_changed(self, sync_result_builder): + async def _have_rooms_changed( + self, sync_result_builder: "SyncResultBuilder" + ) -> bool: """Returns whether there may be any new events that should be sent down the sync. Returns True if there are. """ @@ -1422,22 +1472,10 @@ class SyncHandler(object): return True return False - async def _get_rooms_changed(self, sync_result_builder, ignored_users): + async def _get_rooms_changed( + self, sync_result_builder: "SyncResultBuilder", ignored_users: Set[str] + ) -> _RoomChanges: """Gets the the changes that have happened since the last sync. - - Args: - sync_result_builder(SyncResultBuilder) - ignored_users(set(str)): Set of users ignored by user. - - Returns: - Deferred(tuple): Returns a tuple of the form: - `(room_entries, invited_rooms, newly_joined_rooms, newly_left_rooms)` - - where: - room_entries is a list [RoomSyncResultBuilder] - invited_rooms is a list [InvitedSyncResult] - newly_joined_rooms is a list[str] of room ids - newly_left_rooms is a list[str] of room ids """ user_id = sync_result_builder.sync_config.user.to_string() since_token = sync_result_builder.since_token @@ -1451,7 +1489,7 @@ class SyncHandler(object): user_id, since_token.room_key, now_token.room_key ) - mem_change_events_by_room_id = {} + mem_change_events_by_room_id = {} # type: Dict[str, List[EventBase]] for event in rooms_changed: mem_change_events_by_room_id.setdefault(event.room_id, []).append(event) @@ -1570,7 +1608,7 @@ class SyncHandler(object): # This is all screaming out for a refactor, as the logic here is # subtle and the moving parts numerous. if leave_event.internal_metadata.is_out_of_band_membership(): - batch_events = [leave_event] + batch_events = [leave_event] # type: Optional[List[EventBase]] else: batch_events = None @@ -1636,18 +1674,17 @@ class SyncHandler(object): ) room_entries.append(entry) - return room_entries, invited, newly_joined_rooms, newly_left_rooms + return _RoomChanges(room_entries, invited, newly_joined_rooms, newly_left_rooms) - async def _get_all_rooms(self, sync_result_builder, ignored_users): + async def _get_all_rooms( + self, sync_result_builder: "SyncResultBuilder", ignored_users: Set[str] + ) -> _RoomChanges: """Returns entries for all rooms for the user. Args: - sync_result_builder(SyncResultBuilder) - ignored_users(set(str)): Set of users ignored by user. + sync_result_builder + ignored_users: Set of users ignored by user. - Returns: - Deferred(tuple): Returns a tuple of the form: - `([RoomSyncResultBuilder], [InvitedSyncResult], [])` """ user_id = sync_result_builder.sync_config.user.to_string() @@ -1709,30 +1746,30 @@ class SyncHandler(object): ) ) - return room_entries, invited, [] + return _RoomChanges(room_entries, invited, [], []) async def _generate_room_entry( self, - sync_result_builder, - ignored_users, - room_builder, - ephemeral, - tags, - account_data, - always_include=False, + sync_result_builder: "SyncResultBuilder", + ignored_users: Set[str], + room_builder: "RoomSyncResultBuilder", + ephemeral: List[JsonDict], + tags: Optional[List[JsonDict]], + account_data: Dict[str, JsonDict], + always_include: bool = False, ): """Populates the `joined` and `archived` section of `sync_result_builder` based on the `room_builder`. Args: - sync_result_builder(SyncResultBuilder) - ignored_users(set(str)): Set of users ignored by user. - room_builder(RoomSyncResultBuilder) - ephemeral(list): List of new ephemeral events for room - tags(list): List of *all* tags for room, or None if there has been + sync_result_builder + ignored_users: Set of users ignored by user. + room_builder + ephemeral: List of new ephemeral events for room + tags: List of *all* tags for room, or None if there has been no change. - account_data(list): List of new account data for room - always_include(bool): Always include this room in the sync response, + account_data: List of new account data for room + always_include: Always include this room in the sync response, even if empty. """ newly_joined = room_builder.newly_joined @@ -1758,7 +1795,7 @@ class SyncHandler(object): sync_config, now_token=upto_token, since_token=since_token, - recents=events, + potential_recents=events, newly_joined_room=newly_joined, ) @@ -1809,7 +1846,7 @@ class SyncHandler(object): room_id, batch, sync_config, since_token, now_token, full_state=full_state ) - summary = {} + summary = {} # type: Optional[JsonDict] # we include a summary in room responses when we're lazy loading # members (as the client otherwise doesn't have enough info to form @@ -1833,7 +1870,7 @@ class SyncHandler(object): ) if room_builder.rtype == "joined": - unread_notifications = {} + unread_notifications = {} # type: Dict[str, str] room_sync = JoinedSyncResult( room_id=room_id, timeline=batch, @@ -1860,18 +1897,20 @@ class SyncHandler(object): % (room_id, user_id, len(state)) ) elif room_builder.rtype == "archived": - room_sync = ArchivedSyncResult( + archived_room_sync = ArchivedSyncResult( room_id=room_id, timeline=batch, state=state, account_data=account_data_events, ) - if room_sync or always_include: - sync_result_builder.archived.append(room_sync) + if archived_room_sync or always_include: + sync_result_builder.archived.append(archived_room_sync) else: raise Exception("Unrecognized rtype: %r", room_builder.rtype) - async def get_rooms_for_user_at(self, user_id, stream_ordering): + async def get_rooms_for_user_at( + self, user_id: str, stream_ordering: int + ) -> FrozenSet[str]: """Get set of joined rooms for a user at the given stream ordering. The stream ordering *must* be recent, otherwise this may throw an @@ -1879,12 +1918,11 @@ class SyncHandler(object): current token, which should be perfectly fine). Args: - user_id (str) - stream_ordering (int) + user_id + stream_ordering ReturnValue: - Deferred[frozenset[str]]: Set of room_ids the user is in at given - stream_ordering. + Set of room_ids the user is in at given stream_ordering. """ joined_rooms = await self.store.get_rooms_for_user_with_stream_ordering(user_id) @@ -1911,11 +1949,10 @@ class SyncHandler(object): if user_id in users_in_room: joined_room_ids.add(room_id) - joined_room_ids = frozenset(joined_room_ids) - return joined_room_ids + return frozenset(joined_room_ids) -def _action_has_highlight(actions): +def _action_has_highlight(actions: List[JsonDict]) -> bool: for action in actions: try: if action.get("set_tweak", None) == "highlight": @@ -1927,22 +1964,23 @@ def _action_has_highlight(actions): def _calculate_state( - timeline_contains, timeline_start, previous, current, lazy_load_members -): + timeline_contains: StateMap[str], + timeline_start: StateMap[str], + previous: StateMap[str], + current: StateMap[str], + lazy_load_members: bool, +) -> StateMap[str]: """Works out what state to include in a sync response. Args: - timeline_contains (dict): state in the timeline - timeline_start (dict): state at the start of the timeline - previous (dict): state at the end of the previous sync (or empty dict + timeline_contains: state in the timeline + timeline_start: state at the start of the timeline + previous: state at the end of the previous sync (or empty dict if this is an initial sync) - current (dict): state at the end of the timeline - lazy_load_members (bool): whether to return members from timeline_start + current: state at the end of the timeline + lazy_load_members: whether to return members from timeline_start or not. assumes that timeline_start has already been filtered to include only the members the client needs to know about. - - Returns: - dict """ event_id_to_key = { e: key @@ -1979,15 +2017,16 @@ def _calculate_state( return {event_id_to_key[e]: e for e in state_ids} -class SyncResultBuilder(object): +@attr.s +class SyncResultBuilder: """Used to help build up a new SyncResult for a user Attributes: - sync_config (SyncConfig) - full_state (bool) - since_token (StreamToken) - now_token (StreamToken) - joined_room_ids (list[str]) + sync_config + full_state: The full_state flag as specified by user + since_token: The token supplied by user, or None. + now_token: The token to sync up to. + joined_room_ids: List of rooms the user is joined to # The following mirror the fields in a sync response presence (list) @@ -1995,61 +2034,45 @@ class SyncResultBuilder(object): joined (list[JoinedSyncResult]) invited (list[InvitedSyncResult]) archived (list[ArchivedSyncResult]) - device (list) groups (GroupsSyncResult|None) to_device (list) """ - def __init__( - self, sync_config, full_state, since_token, now_token, joined_room_ids - ): - """ - Args: - sync_config (SyncConfig) - full_state (bool): The full_state flag as specified by user - since_token (StreamToken): The token supplied by user, or None. - now_token (StreamToken): The token to sync up to. - joined_room_ids (list[str]): List of rooms the user is joined to - """ - self.sync_config = sync_config - self.full_state = full_state - self.since_token = since_token - self.now_token = now_token - self.joined_room_ids = joined_room_ids + sync_config = attr.ib(type=SyncConfig) + full_state = attr.ib(type=bool) + since_token = attr.ib(type=Optional[StreamToken]) + now_token = attr.ib(type=StreamToken) + joined_room_ids = attr.ib(type=FrozenSet[str]) - self.presence = [] - self.account_data = [] - self.joined = [] - self.invited = [] - self.archived = [] - self.device = [] - self.groups = None - self.to_device = [] + presence = attr.ib(type=List[JsonDict], default=attr.Factory(list)) + account_data = attr.ib(type=List[JsonDict], default=attr.Factory(list)) + joined = attr.ib(type=List[JoinedSyncResult], default=attr.Factory(list)) + invited = attr.ib(type=List[InvitedSyncResult], default=attr.Factory(list)) + archived = attr.ib(type=List[ArchivedSyncResult], default=attr.Factory(list)) + groups = attr.ib(type=Optional[GroupsSyncResult], default=None) + to_device = attr.ib(type=List[JsonDict], default=attr.Factory(list)) +@attr.s class RoomSyncResultBuilder(object): """Stores information needed to create either a `JoinedSyncResult` or `ArchivedSyncResult`. + + Attributes: + room_id + rtype: One of `"joined"` or `"archived"` + events: List of events to include in the room (more events may be added + when generating result). + newly_joined: If the user has newly joined the room + full_state: Whether the full state should be sent in result + since_token: Earliest point to return events from, or None + upto_token: Latest point to return events from. """ - def __init__( - self, room_id, rtype, events, newly_joined, full_state, since_token, upto_token - ): - """ - Args: - room_id(str) - rtype(str): One of `"joined"` or `"archived"` - events(list[FrozenEvent]): List of events to include in the room - (more events may be added when generating result). - newly_joined(bool): If the user has newly joined the room - full_state(bool): Whether the full state should be sent in result - since_token(StreamToken): Earliest point to return events from, or None - upto_token(StreamToken): Latest point to return events from. - """ - self.room_id = room_id - self.rtype = rtype - self.events = events - self.newly_joined = newly_joined - self.full_state = full_state - self.since_token = since_token - self.upto_token = upto_token + room_id = attr.ib(type=str) + rtype = attr.ib(type=str) + events = attr.ib(type=Optional[List[EventBase]]) + newly_joined = attr.ib(type=bool) + full_state = attr.ib(type=bool) + since_token = attr.ib(type=Optional[StreamToken]) + upto_token = attr.ib(type=StreamToken) diff --git a/tests/storage/test_redaction.py b/tests/storage/test_redaction.py index feb1c07cb2..b9ee6ec1ec 100644 --- a/tests/storage/test_redaction.py +++ b/tests/storage/test_redaction.py @@ -238,8 +238,11 @@ class RedactionTestCase(unittest.HomeserverTestCase): @defer.inlineCallbacks def build(self, prev_event_ids): built_event = yield self._base_builder.build(prev_event_ids) - built_event.event_id = self._event_id + + built_event._event_id = self._event_id built_event._event_dict["event_id"] = self._event_id + assert built_event.event_id == self._event_id + return built_event @property diff --git a/tox.ini b/tox.ini index 88ef12bebd..ef22368cf1 100644 --- a/tox.ini +++ b/tox.ini @@ -180,6 +180,7 @@ commands = mypy \ synapse/api \ synapse/config/ \ synapse/federation/transport \ + synapse/handlers/sync.py \ synapse/handlers/ui_auth \ synapse/logging/ \ synapse/module_api \ From e81c093974faeb8d39dcedcbda7c2e4f5683091f Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 3 Feb 2020 19:15:08 +0000 Subject: [PATCH 0958/1623] make FederationHandler.on_get_missing_events async --- synapse/handlers/federation.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index c94573b547..ea2f6a91d7 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -2121,24 +2121,23 @@ class FederationHandler(BaseHandler): return ret - @defer.inlineCallbacks - def on_get_missing_events( + async def on_get_missing_events( self, origin, room_id, earliest_events, latest_events, limit ): - in_room = yield self.auth.check_host_in_room(room_id, origin) + in_room = await self.auth.check_host_in_room(room_id, origin) if not in_room: raise AuthError(403, "Host not in room.") limit = min(limit, 20) - missing_events = yield self.store.get_missing_events( + missing_events = await self.store.get_missing_events( room_id=room_id, earliest_events=earliest_events, latest_events=latest_events, limit=limit, ) - missing_events = yield filter_events_for_server( + missing_events = await filter_events_for_server( self.storage, origin, missing_events ) From 5d17c3159618b6e75bb58ba68a77a73572a85688 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 3 Feb 2020 22:28:11 +0000 Subject: [PATCH 0959/1623] make FederationHandler.send_invite async --- synapse/handlers/federation.py | 5 ++--- synapse/handlers/message.py | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index ea2f6a91d7..5728ea2ee7 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1184,13 +1184,12 @@ class FederationHandler(BaseHandler): ) raise SynapseError(http_client.BAD_REQUEST, "Too many auth_events") - @defer.inlineCallbacks - def send_invite(self, target_host, event): + async def send_invite(self, target_host, event): """ Sends the invite to the remote server for signing. Invites must be signed by the invitee's server before distribution. """ - pdu = yield self.federation_client.send_invite( + pdu = await self.federation_client.send_invite( destination=target_host, room_id=event.room_id, event_id=event.event_id, diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index bdf16c84d3..be6ae18a92 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -932,10 +932,9 @@ class EventCreationHandler(object): # way? If we have been invited by a remote server, we need # to get them to sign the event. - returned_invite = yield federation_handler.send_invite( - invitee.domain, event + returned_invite = yield defer.ensureDeferred( + federation_handler.send_invite(invitee.domain, event) ) - event.unsigned.pop("room_state", None) # TODO: Make sure the signatures actually are correct. From 0536d0c9beb15fce5f2ca24b4565f79f3140943f Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 3 Feb 2020 20:35:40 +0000 Subject: [PATCH 0960/1623] make FederationClient.backfill async --- synapse/federation/federation_client.py | 26 +++++++++++-------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index f99d17a7de..297292f389 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -17,7 +17,7 @@ import copy import itertools import logging -from typing import Dict, Iterable +from typing import Dict, Iterable, List, Tuple from prometheus_client import Counter @@ -37,7 +37,7 @@ from synapse.api.room_versions import ( EventFormatVersions, RoomVersions, ) -from synapse.events import builder, room_version_to_event_format +from synapse.events import EventBase, builder, room_version_to_event_format from synapse.federation.federation_base import FederationBase, event_from_pdu_json from synapse.logging.context import make_deferred_yieldable from synapse.logging.utils import log_function @@ -170,21 +170,17 @@ class FederationClient(FederationBase): sent_queries_counter.labels("client_one_time_keys").inc() return self.transport_layer.claim_client_keys(destination, content, timeout) - @defer.inlineCallbacks - @log_function - def backfill(self, dest, room_id, limit, extremities): - """Requests some more historic PDUs for the given context from the + async def backfill( + self, dest: str, room_id: str, limit: int, extremities: Iterable[str] + ) -> List[EventBase]: + """Requests some more historic PDUs for the given room from the given destination server. Args: dest (str): The remote homeserver to ask. room_id (str): The room_id to backfill. - limit (int): The maximum number of PDUs to return. - extremities (list): List of PDU id and origins of the first pdus - we have seen from the context - - Returns: - Deferred: Results in the received PDUs. + limit (int): The maximum number of events to return. + extremities (list): our current backwards extremities, to backfill from """ logger.debug("backfill extrem=%s", extremities) @@ -192,13 +188,13 @@ class FederationClient(FederationBase): if not extremities: return - transaction_data = yield self.transport_layer.backfill( + transaction_data = await self.transport_layer.backfill( dest, room_id, extremities, limit ) logger.debug("backfill transaction_data=%r", transaction_data) - room_version = yield self.store.get_room_version_id(room_id) + room_version = await self.store.get_room_version_id(room_id) format_ver = room_version_to_event_format(room_version) pdus = [ @@ -207,7 +203,7 @@ class FederationClient(FederationBase): ] # FIXME: We should handle signature failures more gracefully. - pdus[:] = yield make_deferred_yieldable( + pdus[:] = await make_deferred_yieldable( defer.gatherResults( self._check_sigs_and_hashes(room_version, pdus), consumeErrors=True ).addErrback(unwrapFirstError) From 0cb0c7bcd529b0dda92aff0dae6277dfcb6f6e29 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 3 Feb 2020 20:41:54 +0000 Subject: [PATCH 0961/1623] make FederationClient.get_pdu async --- synapse/federation/federation_client.py | 32 +++++++++++++------------ 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index 297292f389..b9c8d8e325 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -17,7 +17,7 @@ import copy import itertools import logging -from typing import Dict, Iterable, List, Tuple +from typing import Dict, Iterable, List, Optional, Tuple from prometheus_client import Counter @@ -211,11 +211,14 @@ class FederationClient(FederationBase): return pdus - @defer.inlineCallbacks - @log_function - def get_pdu( - self, destinations, event_id, room_version, outlier=False, timeout=None - ): + async def get_pdu( + self, + destinations: Iterable[str], + event_id: str, + room_version: str, + outlier: bool = False, + timeout: Optional[int] = None, + ) -> Optional[EventBase]: """Requests the PDU with given origin and ID from the remote home servers. @@ -223,18 +226,17 @@ class FederationClient(FederationBase): one succeeds. Args: - destinations (list): Which homeservers to query - event_id (str): event to fetch - room_version (str): version of the room - outlier (bool): Indicates whether the PDU is an `outlier`, i.e. if + destinations: Which homeservers to query + event_id: event to fetch + room_version: version of the room + outlier: Indicates whether the PDU is an `outlier`, i.e. if it's from an arbitary point in the context as opposed to part of the current block of PDUs. Defaults to `False` - timeout (int): How long to try (in ms) each destination for before + timeout: How long to try (in ms) each destination for before moving to the next destination. None indicates no timeout. Returns: - Deferred: Results in the requested PDU, or None if we were unable to find - it. + The requested PDU, or None if we were unable to find it. """ # TODO: Rate limit the number of times we try and get the same event. @@ -255,7 +257,7 @@ class FederationClient(FederationBase): continue try: - transaction_data = yield self.transport_layer.get_event( + transaction_data = await self.transport_layer.get_event( destination, event_id, timeout=timeout ) @@ -275,7 +277,7 @@ class FederationClient(FederationBase): pdu = pdu_list[0] # Check signatures are correct. - signed_pdu = yield self._check_sigs_and_hash(room_version, pdu) + signed_pdu = await self._check_sigs_and_hash(room_version, pdu) break From d73683c363a9177951b8078dd1628c1ade8de508 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 3 Feb 2020 20:42:52 +0000 Subject: [PATCH 0962/1623] make FederationClient.get_room_state_ids async --- synapse/federation/federation_client.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index b9c8d8e325..c3e556e7d9 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -307,15 +307,16 @@ class FederationClient(FederationBase): return signed_pdu - @defer.inlineCallbacks - def get_room_state_ids(self, destination: str, room_id: str, event_id: str): + async def get_room_state_ids( + self, destination: str, room_id: str, event_id: str + ) -> Tuple[List[str], List[str]]: """Calls the /state_ids endpoint to fetch the state at a particular point in the room, and the auth events for the given event Returns: - Tuple[List[str], List[str]]: a tuple of (state event_ids, auth event_ids) + a tuple of (state event_ids, auth event_ids) """ - result = yield self.transport_layer.get_room_state_ids( + result = await self.transport_layer.get_room_state_ids( destination, room_id, event_id=event_id ) From 24d814ca238c40fef61c7826d1a9d9f9dd3a144d Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 3 Feb 2020 20:43:40 +0000 Subject: [PATCH 0963/1623] make FederationClient.get_event_auth async --- synapse/federation/federation_client.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index c3e556e7d9..1da33a7a64 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -330,19 +330,17 @@ class FederationClient(FederationBase): return state_event_ids, auth_event_ids - @defer.inlineCallbacks - @log_function - def get_event_auth(self, destination, room_id, event_id): - res = yield self.transport_layer.get_event_auth(destination, room_id, event_id) + async def get_event_auth(self, destination, room_id, event_id): + res = await self.transport_layer.get_event_auth(destination, room_id, event_id) - room_version = yield self.store.get_room_version_id(room_id) + room_version = await self.store.get_room_version_id(room_id) format_ver = room_version_to_event_format(room_version) auth_chain = [ event_from_pdu_json(p, format_ver, outlier=True) for p in res["auth_chain"] ] - signed_auth = yield self._check_sigs_and_hash_and_fetch( + signed_auth = await self._check_sigs_and_hash_and_fetch( destination, auth_chain, outlier=True, room_version=room_version ) From 3f11cbb40494f0ac9622a5b686d3cf8379a44b5d Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 3 Feb 2020 20:51:26 +0000 Subject: [PATCH 0964/1623] make FederationClient.make_membership_event async --- synapse/federation/federation_client.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index 1da33a7a64..b69aac9041 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -35,6 +35,7 @@ from synapse.api.errors import ( from synapse.api.room_versions import ( KNOWN_ROOM_VERSIONS, EventFormatVersions, + RoomVersion, RoomVersions, ) from synapse.events import EventBase, builder, room_version_to_event_format @@ -404,7 +405,7 @@ class FederationClient(FederationBase): raise SynapseError(502, "Failed to %s via any server" % (description,)) - def make_membership_event( + async def make_membership_event( self, destinations: Iterable[str], room_id: str, @@ -412,7 +413,7 @@ class FederationClient(FederationBase): membership: str, content: dict, params: Dict[str, str], - ): + ) -> Tuple[str, EventBase, RoomVersion]: """ Creates an m.room.member event, with context, without participating in the room. @@ -433,19 +434,19 @@ class FederationClient(FederationBase): content: Any additional data to put into the content field of the event. params: Query parameters to include in the request. - Return: - Deferred[Tuple[str, FrozenEvent, RoomVersion]]: resolves to a tuple of + + Returns: `(origin, event, room_version)` where origin is the remote homeserver which generated the event, and room_version is the version of the room. - Fails with a `UnsupportedRoomVersionError` if remote responds with - a room version we don't understand. + Raises: + UnsupportedRoomVersionError: if remote responds with + a room version we don't understand. - Fails with a ``SynapseError`` if the chosen remote server - returns a 300/400 code. + SynapseError: if the chosen remote server returns a 300/400 code. - Fails with a ``RuntimeError`` if no servers were reachable. + RuntimeError: if no servers were reachable. """ valid_memberships = {Membership.JOIN, Membership.LEAVE} if membership not in valid_memberships: @@ -491,7 +492,7 @@ class FederationClient(FederationBase): return (destination, ev, room_version) - return self._try_destination_list( + return await self._try_destination_list( "make_" + membership, destinations, send_request ) From 8af9f11bea8d36188d7f8ff65f227b3cfdf9fa17 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 3 Feb 2020 20:55:00 +0000 Subject: [PATCH 0965/1623] make FederationClient.send_join async --- synapse/federation/federation_client.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index b69aac9041..c98b276805 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -17,7 +17,7 @@ import copy import itertools import logging -from typing import Dict, Iterable, List, Optional, Tuple +from typing import Any, Dict, Iterable, List, Optional, Tuple from prometheus_client import Counter @@ -496,27 +496,29 @@ class FederationClient(FederationBase): "make_" + membership, destinations, send_request ) - def send_join(self, destinations, pdu, event_format_version): + async def send_join( + self, destinations: Iterable[str], pdu: EventBase, event_format_version: int + ) -> Dict[str, Any]: """Sends a join event to one of a list of homeservers. Doing so will cause the remote server to add the event to the graph, and send the event out to the rest of the federation. Args: - destinations (str): Candidate homeservers which are probably + destinations: Candidate homeservers which are probably participating in the room. - pdu (BaseEvent): event to be sent - event_format_version (int): The event format version + pdu: event to be sent + event_format_version: The event format version - Return: - Deferred: resolves to a dict with members ``origin`` (a string + Returns: + a dict with members ``origin`` (a string giving the serer the event was sent to, ``state`` (?) and ``auth_chain``. - Fails with a ``SynapseError`` if the chosen remote server - returns a 300/400 code. + Raises: + SynapseError: if the chosen remote server returns a 300/400 code. - Fails with a ``RuntimeError`` if no servers were reachable. + RuntimeError: if no servers were reachable. """ def check_authchain_validity(signed_auth_chain): @@ -603,7 +605,7 @@ class FederationClient(FederationBase): "origin": destination, } - return self._try_destination_list("send_join", destinations, send_request) + return await self._try_destination_list("send_join", destinations, send_request) @defer.inlineCallbacks def _do_send_join(self, destination, pdu): From a46fabf17bca51eb3a73feb5b3de4072030033e4 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 3 Feb 2020 20:55:11 +0000 Subject: [PATCH 0966/1623] make FederationClient.send_leave async --- synapse/federation/federation_client.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index c98b276805..8d33d27137 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -730,7 +730,7 @@ class FederationClient(FederationBase): ) return content - def send_leave(self, destinations, pdu): + async def send_leave(self, destinations: Iterable[str], pdu: EventBase) -> None: """Sends a leave event to one of a list of homeservers. Doing so will cause the remote server to add the event to the graph, @@ -739,17 +739,14 @@ class FederationClient(FederationBase): This is mostly useful to reject received invites. Args: - destinations (str): Candidate homeservers which are probably + destinations: Candidate homeservers which are probably participating in the room. - pdu (BaseEvent): event to be sent + pdu: event to be sent - Return: - Deferred: resolves to None. + Raises: + SynapseError if the chosen remote server returns a 300/400 code. - Fails with a ``SynapseError`` if the chosen remote server - returns a 300/400 code. - - Fails with a ``RuntimeError`` if no servers were reachable. + RuntimeError if no servers were reachable. """ @defer.inlineCallbacks @@ -759,7 +756,9 @@ class FederationClient(FederationBase): logger.debug("Got content: %s", content) return None - return self._try_destination_list("send_leave", destinations, send_request) + return await self._try_destination_list( + "send_leave", destinations, send_request + ) @defer.inlineCallbacks def _do_send_leave(self, destination, pdu): From 1330c311b79bc4a9b4d3349e72a2353bb54dcd90 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 3 Feb 2020 20:59:10 +0000 Subject: [PATCH 0967/1623] make FederationClient._try_destination_list async --- synapse/federation/federation_client.py | 36 ++++++++++++++++++------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index 8d33d27137..11802dad0f 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -17,7 +17,17 @@ import copy import itertools import logging -from typing import Any, Dict, Iterable, List, Optional, Tuple +from typing import ( + Any, + Awaitable, + Callable, + Dict, + Iterable, + List, + Optional, + Tuple, + TypeVar, +) from prometheus_client import Counter @@ -53,6 +63,8 @@ sent_queries_counter = Counter("synapse_federation_client_sent_queries", "", ["t PDU_RETRY_TIME_MS = 1 * 60 * 1000 +T = TypeVar("T") + class InvalidResponseError(RuntimeError): """Helper for _try_destination_list: indicates that the server returned a response @@ -349,17 +361,21 @@ class FederationClient(FederationBase): return signed_auth - @defer.inlineCallbacks - def _try_destination_list(self, description, destinations, callback): + async def _try_destination_list( + self, + description: str, + destinations: Iterable[str], + callback: Callable[[str], Awaitable[T]], + ) -> T: """Try an operation on a series of servers, until it succeeds Args: - description (unicode): description of the operation we're doing, for logging + description: description of the operation we're doing, for logging - destinations (Iterable[unicode]): list of server_names to try + destinations: list of server_names to try - callback (callable): Function to run for each server. Passed a single - argument: the server_name to try. May return a deferred. + callback: Function to run for each server. Passed a single + argument: the server_name to try. If the callback raises a CodeMessageException with a 300/400 code, attempts to perform the operation stop immediately and the exception is @@ -370,7 +386,7 @@ class FederationClient(FederationBase): suppressed if the exception is an InvalidResponseError. Returns: - The [Deferred] result of callback, if it succeeds + The result of callback, if it succeeds Raises: SynapseError if the chosen remote server returns a 300/400 code, or @@ -381,7 +397,7 @@ class FederationClient(FederationBase): continue try: - res = yield callback(destination) + res = await callback(destination) return res except InvalidResponseError as e: logger.warning("Failed to %s via %s: %s", description, destination, e) @@ -400,7 +416,7 @@ class FederationClient(FederationBase): ) except Exception: logger.warning( - "Failed to %s via %s", description, destination, exc_info=1 + "Failed to %s via %s", description, destination, exc_info=True ) raise SynapseError(502, "Failed to %s via any server" % (description,)) From ad09ee92622e0d37502fb49336f1e1474af458df Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 3 Feb 2020 21:07:13 +0000 Subject: [PATCH 0968/1623] make FederationClient.make_membership_event.send_request async --- synapse/federation/federation_client.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index 11802dad0f..b59d08c4ae 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -471,9 +471,8 @@ class FederationClient(FederationBase): % (membership, ",".join(valid_memberships)) ) - @defer.inlineCallbacks - def send_request(destination): - ret = yield self.transport_layer.make_membership_event( + async def send_request(destination: str) -> Tuple[str, EventBase, RoomVersion]: + ret = await self.transport_layer.make_membership_event( destination, room_id, user_id, membership, params ) @@ -506,7 +505,7 @@ class FederationClient(FederationBase): event_dict=pdu_dict, ) - return (destination, ev, room_version) + return destination, ev, room_version return await self._try_destination_list( "make_" + membership, destinations, send_request From 3960527c2e1d9bbeb2f0f7b6218de06cd32bdcb4 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 3 Feb 2020 21:07:38 +0000 Subject: [PATCH 0969/1623] make FederationClient.send_join.send_request async --- synapse/federation/federation_client.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index b59d08c4ae..8ca36c4d32 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -553,9 +553,8 @@ class FederationClient(FederationBase): "room appears to have unsupported version %s" % (room_version,) ) - @defer.inlineCallbacks - def send_request(destination): - content = yield self._do_send_join(destination, pdu) + async def send_request(destination) -> Dict[str, Any]: + content = await self._do_send_join(destination, pdu) logger.debug("Got content: %s", content) @@ -584,7 +583,7 @@ class FederationClient(FederationBase): # invalid, and it would fail auth checks anyway. raise SynapseError(400, "No create event in state") - valid_pdus = yield self._check_sigs_and_hash_and_fetch( + valid_pdus = await self._check_sigs_and_hash_and_fetch( destination, list(pdus.values()), outlier=True, From 638001116de3e472d916f8e809e15c658bab282a Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 3 Feb 2020 21:08:24 +0000 Subject: [PATCH 0970/1623] make FederationClient._do_send_join async --- synapse/federation/federation_client.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index 8ca36c4d32..b4609b78cb 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -621,12 +621,11 @@ class FederationClient(FederationBase): return await self._try_destination_list("send_join", destinations, send_request) - @defer.inlineCallbacks - def _do_send_join(self, destination, pdu): + async def _do_send_join(self, destination: str, pdu: EventBase): time_now = self._clock.time_msec() try: - content = yield self.transport_layer.send_join_v2( + content = await self.transport_layer.send_join_v2( destination=destination, room_id=pdu.room_id, event_id=pdu.event_id, @@ -648,7 +647,7 @@ class FederationClient(FederationBase): logger.debug("Couldn't send_join with the v2 API, falling back to the v1 API") - resp = yield self.transport_layer.send_join_v1( + resp = await self.transport_layer.send_join_v1( destination=destination, room_id=pdu.room_id, event_id=pdu.event_id, From e88b90aaebd19822e310b708696036dc0b1f17f6 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 3 Feb 2020 21:08:51 +0000 Subject: [PATCH 0971/1623] make FederationClient.send_leave.send_request async --- synapse/federation/federation_client.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index b4609b78cb..f98c36039d 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -762,12 +762,9 @@ class FederationClient(FederationBase): RuntimeError if no servers were reachable. """ - @defer.inlineCallbacks - def send_request(destination): - content = yield self._do_send_leave(destination, pdu) - + async def send_request(destination: str) -> None: + content = await self._do_send_leave(destination, pdu) logger.debug("Got content: %s", content) - return None return await self._try_destination_list( "send_leave", destinations, send_request From abadf44eb2c32c9a6f1da84239043755f854b327 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 3 Feb 2020 21:09:07 +0000 Subject: [PATCH 0972/1623] make FederationClient._do_send_leave async --- synapse/federation/federation_client.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index f98c36039d..5043220d14 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -770,12 +770,11 @@ class FederationClient(FederationBase): "send_leave", destinations, send_request ) - @defer.inlineCallbacks - def _do_send_leave(self, destination, pdu): + async def _do_send_leave(self, destination, pdu): time_now = self._clock.time_msec() try: - content = yield self.transport_layer.send_leave_v2( + content = await self.transport_layer.send_leave_v2( destination=destination, room_id=pdu.room_id, event_id=pdu.event_id, @@ -797,7 +796,7 @@ class FederationClient(FederationBase): logger.debug("Couldn't send_leave with the v2 API, falling back to the v1 API") - resp = yield self.transport_layer.send_leave_v1( + resp = await self.transport_layer.send_leave_v1( destination=destination, room_id=pdu.room_id, event_id=pdu.event_id, From 6deeefb68cbd2bc54d8e108b16e6427454644019 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 3 Feb 2020 21:14:30 +0000 Subject: [PATCH 0973/1623] make FederationClient.get_missing_events async --- synapse/federation/federation_client.py | 40 ++++++++++++------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index 5043220d14..1e9b207a24 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -25,6 +25,7 @@ from typing import ( Iterable, List, Optional, + Sequence, Tuple, TypeVar, ) @@ -828,34 +829,33 @@ class FederationClient(FederationBase): third_party_instance_id=third_party_instance_id, ) - @defer.inlineCallbacks - def get_missing_events( + async def get_missing_events( self, - destination, - room_id, - earliest_events_ids, - latest_events, - limit, - min_depth, - timeout, - ): + destination: str, + room_id: str, + earliest_events_ids: Sequence[str], + latest_events: Iterable[EventBase], + limit: int, + min_depth: int, + timeout: int, + ) -> List[EventBase]: """Tries to fetch events we are missing. This is called when we receive an event without having received all of its ancestors. Args: - destination (str) - room_id (str) - earliest_events_ids (list): List of event ids. Effectively the + destination + room_id + earliest_events_ids: List of event ids. Effectively the events we expected to receive, but haven't. `get_missing_events` should only return events that didn't happen before these. - latest_events (list): List of events we have received that we don't + latest_events: List of events we have received that we don't have all previous events for. - limit (int): Maximum number of events to return. - min_depth (int): Minimum depth of events tor return. - timeout (int): Max time to wait in ms + limit: Maximum number of events to return. + min_depth: Minimum depth of events to return. + timeout: Max time to wait in ms """ try: - content = yield self.transport_layer.get_missing_events( + content = await self.transport_layer.get_missing_events( destination=destination, room_id=room_id, earliest_events=earliest_events_ids, @@ -865,14 +865,14 @@ class FederationClient(FederationBase): timeout=timeout, ) - room_version = yield self.store.get_room_version_id(room_id) + room_version = await self.store.get_room_version_id(room_id) format_ver = room_version_to_event_format(room_version) events = [ event_from_pdu_json(e, format_ver) for e in content.get("events", []) ] - signed_events = yield self._check_sigs_and_hash_and_fetch( + signed_events = await self._check_sigs_and_hash_and_fetch( destination, events, outlier=False, room_version=room_version ) except HttpResponseException as e: From 4b4536dd02cc1128335383b6cc36afc1f0f6d71c Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 3 Feb 2020 21:15:08 +0000 Subject: [PATCH 0974/1623] newsfile --- changelog.d/6840.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6840.misc diff --git a/changelog.d/6840.misc b/changelog.d/6840.misc new file mode 100644 index 0000000000..0496f12de8 --- /dev/null +++ b/changelog.d/6840.misc @@ -0,0 +1 @@ +Port much of `synapse.handlers.federation` to async/await. From ea23210b2dcc75489d9369b13636a56b7449d765 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 3 Feb 2020 22:29:49 +0000 Subject: [PATCH 0975/1623] make FederationClient.send_invite async --- synapse/federation/federation_client.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index 1e9b207a24..51f9b1d8d7 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -659,23 +659,22 @@ class FederationClient(FederationBase): # content. return resp[1] - @defer.inlineCallbacks - def send_invite(self, destination, room_id, event_id, pdu): - room_version = yield self.store.get_room_version_id(room_id) + async def send_invite(self, destination, room_id, event_id, pdu): + room_version = await self.store.get_room_version_id(room_id) - content = yield self._do_send_invite(destination, pdu, room_version) + content = await self._do_send_invite(destination, pdu, room_version) pdu_dict = content["event"] logger.debug("Got response to send_invite: %s", pdu_dict) - room_version = yield self.store.get_room_version_id(room_id) + room_version = await self.store.get_room_version_id(room_id) format_ver = room_version_to_event_format(room_version) pdu = event_from_pdu_json(pdu_dict, format_ver) # Check signatures are correct. - pdu = yield self._check_sigs_and_hash(room_version, pdu) + pdu = await self._check_sigs_and_hash(room_version, pdu) # FIXME: We should handle signature failures more gracefully. From 23d8a55c7af93d562b7e7f24b0d195fced692123 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Tue, 4 Feb 2020 00:10:21 -0500 Subject: [PATCH 0976/1623] add device signatures to device key query results --- synapse/storage/data_stores/main/devices.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/synapse/storage/data_stores/main/devices.py b/synapse/storage/data_stores/main/devices.py index ea0503476f..b7617efb80 100644 --- a/synapse/storage/data_stores/main/devices.py +++ b/synapse/storage/data_stores/main/devices.py @@ -320,6 +320,11 @@ class DeviceWorkerStore(SQLBaseStore): device_display_name = device.get("device_display_name", None) if device_display_name: result["device_display_name"] = device_display_name + if "signatures" in device: + for sig_user_id, sigs in device["signatures"].items(): + result["keys"].setdefault("signatures", {}).setdefault( + sig_user_id, {} + ).update(sigs) else: result["deleted"] = True @@ -524,6 +529,11 @@ class DeviceWorkerStore(SQLBaseStore): device_display_name = device.get("device_display_name", None) if device_display_name: result["device_display_name"] = device_display_name + if "signatures" in device: + for sig_user_id, sigs in device["signatures"].items(): + result["keys"].setdefault("signatures", {}).setdefault( + sig_user_id, {} + ).update(sigs) results.append(result) From 245ee142209f634a51942681bb141e617e1ecd55 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Tue, 4 Feb 2020 00:21:07 -0500 Subject: [PATCH 0977/1623] add changelog --- changelog.d/6844.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6844.bugfix diff --git a/changelog.d/6844.bugfix b/changelog.d/6844.bugfix new file mode 100644 index 0000000000..e84aa1029f --- /dev/null +++ b/changelog.d/6844.bugfix @@ -0,0 +1 @@ +Fix an issue with cross-signing where device signatures were not sent to remote servers. From c87572d6e426099fa36e2cd8260319531ec0fbb8 Mon Sep 17 00:00:00 2001 From: Michael Kaye <1917473+michaelkaye@users.noreply.github.com> Date: Tue, 4 Feb 2020 16:21:09 +0000 Subject: [PATCH 0978/1623] Update CONTRIBUTING.md about merging PRs. (#6846) --- CONTRIBUTING.md | 14 ++++++++++++++ changelog.d/6846.doc | 1 + 2 files changed, 15 insertions(+) create mode 100644 changelog.d/6846.doc diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5736ede6c4..4b01b6ac8c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -200,6 +200,20 @@ Git allows you to add this signoff automatically when using the `-s` flag to `git commit`, which uses the name and email set in your `user.name` and `user.email` git configs. +## Merge Strategy + +We use the commit history of develop/master extensively to identify +when regressions were introduced and what changes have been made. + +We aim to have a clean merge history, which means we normally squash-merge +changes into develop. For small changes this means there is no need to rebase +to clean up your PR before merging. Larger changes with an organised set of +commits may be merged as-is, if the history is judged to be useful. + +This use of squash-merging will mean PRs built on each other will be hard to +merge. We suggest avoiding these where possible, and if required, ensuring +each PR has a tidy set of commits to ease merging. + ## Conclusion That's it! Matrix is a very open and collaborative project as you might expect diff --git a/changelog.d/6846.doc b/changelog.d/6846.doc new file mode 100644 index 0000000000..ad69d608c0 --- /dev/null +++ b/changelog.d/6846.doc @@ -0,0 +1 @@ +Add details of PR merge strategy to contributing docs. \ No newline at end of file From 6475382d807e1fed095d1e3fbd04884799ebd612 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 4 Feb 2020 17:25:54 +0000 Subject: [PATCH 0979/1623] Fix detecting unknown devices from remote encrypted events. (#6848) We were looking at the wrong event type (`m.room.encryption` vs `m.room.encrypted`). Also fixup the duplicate `EvenTypes` entries. Introduced in #6776. --- changelog.d/6848.bugfix | 1 + synapse/api/constants.py | 3 +-- synapse/handlers/federation.py | 2 +- synapse/handlers/room.py | 2 +- synapse/handlers/stats.py | 2 +- synapse/storage/data_stores/main/stats.py | 4 ++-- 6 files changed, 7 insertions(+), 7 deletions(-) create mode 100644 changelog.d/6848.bugfix diff --git a/changelog.d/6848.bugfix b/changelog.d/6848.bugfix new file mode 100644 index 0000000000..65688e5d57 --- /dev/null +++ b/changelog.d/6848.bugfix @@ -0,0 +1 @@ +Fix detecting unknown devices from remote encrypted events. diff --git a/synapse/api/constants.py b/synapse/api/constants.py index 0ade47e624..cc8577552b 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -77,12 +77,11 @@ class EventTypes(object): Aliases = "m.room.aliases" Redaction = "m.room.redaction" ThirdPartyInvite = "m.room.third_party_invite" - Encryption = "m.room.encryption" RelatedGroups = "m.room.related_groups" RoomHistoryVisibility = "m.room.history_visibility" CanonicalAlias = "m.room.canonical_alias" - Encryption = "m.room.encryption" + Encrypted = "m.room.encrypted" RoomAvatar = "m.room.avatar" RoomEncryption = "m.room.encryption" GuestAccess = "m.room.guest_access" diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index c86d3177e9..488200a2d1 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -752,7 +752,7 @@ class FederationHandler(BaseHandler): # For encrypted messages we check that we know about the sending device, # if we don't then we mark the device cache for that user as stale. - if event.type == EventTypes.Encryption: + if event.type == EventTypes.Encrypted: device_id = event.content.get("device_id") if device_id is not None: cached_devices = await self.store.get_cached_devices_for_user( diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 1382399557..b609a65f47 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -360,7 +360,7 @@ class RoomCreationHandler(BaseHandler): (EventTypes.RoomHistoryVisibility, ""), (EventTypes.GuestAccess, ""), (EventTypes.RoomAvatar, ""), - (EventTypes.Encryption, ""), + (EventTypes.RoomEncryption, ""), (EventTypes.ServerACL, ""), (EventTypes.RelatedGroups, ""), (EventTypes.PowerLevels, ""), diff --git a/synapse/handlers/stats.py b/synapse/handlers/stats.py index 7f7d56390e..68e6edace5 100644 --- a/synapse/handlers/stats.py +++ b/synapse/handlers/stats.py @@ -286,7 +286,7 @@ class StatsHandler(StateDeltasHandler): room_state["history_visibility"] = event_content.get( "history_visibility" ) - elif typ == EventTypes.Encryption: + elif typ == EventTypes.RoomEncryption: room_state["encryption"] = event_content.get("algorithm") elif typ == EventTypes.Name: room_state["name"] = event_content.get("name") diff --git a/synapse/storage/data_stores/main/stats.py b/synapse/storage/data_stores/main/stats.py index 7bc186e9a1..7af1495e47 100644 --- a/synapse/storage/data_stores/main/stats.py +++ b/synapse/storage/data_stores/main/stats.py @@ -744,7 +744,7 @@ class StatsStore(StateDeltasStore): EventTypes.Create, EventTypes.JoinRules, EventTypes.RoomHistoryVisibility, - EventTypes.Encryption, + EventTypes.RoomEncryption, EventTypes.Name, EventTypes.Topic, EventTypes.RoomAvatar, @@ -816,7 +816,7 @@ class StatsStore(StateDeltasStore): room_state["history_visibility"] = event.content.get( "history_visibility" ) - elif event.type == EventTypes.Encryption: + elif event.type == EventTypes.RoomEncryption: room_state["encryption"] = event.content.get("algorithm") elif event.type == EventTypes.Name: room_state["name"] = event.content.get("name") From d88e0ec0802b3b0a49853fb6b777a35b7c195ea6 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 4 Feb 2020 21:31:08 +0000 Subject: [PATCH 0980/1623] Database updates to populate rooms.room_version (#6847) We're going to need this so that we can figure out how to handle redactions when fetching events from the database. --- changelog.d/6847.misc | 1 + .../57/rooms_version_column_2.sql.postgres | 35 +++++++++++++++++++ .../57/rooms_version_column_2.sql.sqlite | 22 ++++++++++++ 3 files changed, 58 insertions(+) create mode 100644 changelog.d/6847.misc create mode 100644 synapse/storage/data_stores/main/schema/delta/57/rooms_version_column_2.sql.postgres create mode 100644 synapse/storage/data_stores/main/schema/delta/57/rooms_version_column_2.sql.sqlite diff --git a/changelog.d/6847.misc b/changelog.d/6847.misc new file mode 100644 index 0000000000..094e911adb --- /dev/null +++ b/changelog.d/6847.misc @@ -0,0 +1 @@ +Populate `rooms.room_version` database column at startup, rather than in a background update. diff --git a/synapse/storage/data_stores/main/schema/delta/57/rooms_version_column_2.sql.postgres b/synapse/storage/data_stores/main/schema/delta/57/rooms_version_column_2.sql.postgres new file mode 100644 index 0000000000..c601cff6de --- /dev/null +++ b/synapse/storage/data_stores/main/schema/delta/57/rooms_version_column_2.sql.postgres @@ -0,0 +1,35 @@ +/* Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +-- when we first added the room_version column, it was populated via a background +-- update. We now need it to be populated before synapse starts, so we populate +-- any remaining rows with a NULL room version now. For servers which have completed +-- the background update, this will be pretty quick. + +-- the following query will set room_version to NULL if no create event is found for +-- the room in current_state_events, and will set it to '1' if a create event with no +-- room_version is found. + +UPDATE rooms SET room_version=( + SELECT COALESCE(json::json->'content'->>'room_version','1') + FROM current_state_events cse INNER JOIN event_json ej USING (event_id) + WHERE cse.room_id=rooms.room_id AND cse.type='m.room.create' AND cse.state_key='' +) WHERE rooms.room_version IS NULL; + +-- we still allow the background update to complete: it has the useful side-effect of +-- populating `rooms` with any missing rooms (based on the current_state_events table). + +-- see also rooms_version_column_2.sql.sqlite which has a copy of the above query, using +-- sqlite syntax for the json extraction. diff --git a/synapse/storage/data_stores/main/schema/delta/57/rooms_version_column_2.sql.sqlite b/synapse/storage/data_stores/main/schema/delta/57/rooms_version_column_2.sql.sqlite new file mode 100644 index 0000000000..335c6f2074 --- /dev/null +++ b/synapse/storage/data_stores/main/schema/delta/57/rooms_version_column_2.sql.sqlite @@ -0,0 +1,22 @@ +/* Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +-- see rooms_version_column_2.sql.postgres for details of what's going on here. + +UPDATE rooms SET room_version=( + SELECT COALESCE(json_extract(ej.json, '$.content.room_version'), '1') + FROM current_state_events cse INNER JOIN event_json ej USING (event_id) + WHERE cse.room_id=rooms.room_id AND cse.type='m.room.create' AND cse.state_key='' +) WHERE rooms.room_version IS NULL; From a831d2e4e3c424fb54f186bfa7d83a17965f933e Mon Sep 17 00:00:00 2001 From: Michael Kaye <1917473+michaelkaye@users.noreply.github.com> Date: Wed, 5 Feb 2020 08:57:38 +0000 Subject: [PATCH 0981/1623] Reduce performance logging to DEBUG (#6833) * Reduce tnx performance logging to DEBUG * Changelog.d --- changelog.d/6833.misc | 1 + synapse/storage/database.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/6833.misc diff --git a/changelog.d/6833.misc b/changelog.d/6833.misc new file mode 100644 index 0000000000..8a0605f90b --- /dev/null +++ b/changelog.d/6833.misc @@ -0,0 +1 @@ +Reducing log level to DEBUG for synapse.storage.TIME. diff --git a/synapse/storage/database.py b/synapse/storage/database.py index 1003dd84a5..3eeb2f7c04 100644 --- a/synapse/storage/database.py +++ b/synapse/storage/database.py @@ -343,7 +343,7 @@ class Database(object): top_three_counters = self._txn_perf_counters.interval(duration, limit=3) - perf_logger.info( + perf_logger.debug( "Total database time: %.3f%% {%s}", ratio * 100, top_three_counters ) From 60d06724268891ad3b1e9dc6fe7cd080f9ba21b7 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Tue, 4 Feb 2020 12:03:54 -0500 Subject: [PATCH 0982/1623] Merge pull request #6844 from matrix-org/uhoreg/cross_signing_fix_device_fed add device signatures to device key query results --- changelog.d/6844.bugfix | 1 + synapse/storage/data_stores/main/devices.py | 10 ++++++++++ 2 files changed, 11 insertions(+) create mode 100644 changelog.d/6844.bugfix diff --git a/changelog.d/6844.bugfix b/changelog.d/6844.bugfix new file mode 100644 index 0000000000..e84aa1029f --- /dev/null +++ b/changelog.d/6844.bugfix @@ -0,0 +1 @@ +Fix an issue with cross-signing where device signatures were not sent to remote servers. diff --git a/synapse/storage/data_stores/main/devices.py b/synapse/storage/data_stores/main/devices.py index ea0503476f..b7617efb80 100644 --- a/synapse/storage/data_stores/main/devices.py +++ b/synapse/storage/data_stores/main/devices.py @@ -320,6 +320,11 @@ class DeviceWorkerStore(SQLBaseStore): device_display_name = device.get("device_display_name", None) if device_display_name: result["device_display_name"] = device_display_name + if "signatures" in device: + for sig_user_id, sigs in device["signatures"].items(): + result["keys"].setdefault("signatures", {}).setdefault( + sig_user_id, {} + ).update(sigs) else: result["deleted"] = True @@ -524,6 +529,11 @@ class DeviceWorkerStore(SQLBaseStore): device_display_name = device.get("device_display_name", None) if device_display_name: result["device_display_name"] = device_display_name + if "signatures" in device: + for sig_user_id, sigs in device["signatures"].items(): + result["keys"].setdefault("signatures", {}).setdefault( + sig_user_id, {} + ).update(sigs) results.append(result) From a58860e4802c31680ba43e59ec537984af9f5637 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 5 Feb 2020 14:02:39 +0000 Subject: [PATCH 0983/1623] Check sender_key matches on inbound encrypted events. (#6850) If they don't then the device lists are probably out of sync. --- changelog.d/6850.misc | 1 + synapse/handlers/device.py | 8 +++- synapse/handlers/federation.py | 72 ++++++++++++++++++++++++++++------ 3 files changed, 67 insertions(+), 14 deletions(-) create mode 100644 changelog.d/6850.misc diff --git a/changelog.d/6850.misc b/changelog.d/6850.misc new file mode 100644 index 0000000000..418569113f --- /dev/null +++ b/changelog.d/6850.misc @@ -0,0 +1 @@ +Detect unexpected sender keys on inbound encrypted events and resync device lists. diff --git a/synapse/handlers/device.py b/synapse/handlers/device.py index 26ef5e150c..a9bd431486 100644 --- a/synapse/handlers/device.py +++ b/synapse/handlers/device.py @@ -598,7 +598,13 @@ class DeviceListUpdater(object): # happens if we've missed updates. resync = yield self._need_to_do_resync(user_id, pending_updates) - logger.debug("Need to re-sync devices for %r? %r", user_id, resync) + if logger.isEnabledFor(logging.INFO): + logger.info( + "Received device list update for %s, requiring resync: %s. Devices: %s", + user_id, + resync, + ", ".join(u[0] for u in pending_updates), + ) if resync: yield self.user_device_resync(user_id) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 488200a2d1..e9441bbeff 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -754,27 +754,73 @@ class FederationHandler(BaseHandler): # if we don't then we mark the device cache for that user as stale. if event.type == EventTypes.Encrypted: device_id = event.content.get("device_id") + sender_key = event.content.get("sender_key") + + cached_devices = await self.store.get_cached_devices_for_user(event.sender) + + resync = False # Whether we should resync device lists. + + device = None if device_id is not None: - cached_devices = await self.store.get_cached_devices_for_user( - event.sender - ) - if device_id not in cached_devices: + device = cached_devices.get(device_id) + if device is None: logger.info( "Received event from remote device not in our cache: %s %s", event.sender, device_id, ) - await self.store.mark_remote_user_device_cache_as_stale( - event.sender + resync = True + + # We also check if the `sender_key` matches what we expect. + if sender_key is not None: + # Figure out what sender key we're expecting. If we know the + # device and recognize the algorithm then we can work out the + # exact key to expect. Otherwise check it matches any key we + # have for that device. + if device: + keys = device.get("keys", {}).get("keys", {}) + + if event.content.get("algorithm") == "m.megolm.v1.aes-sha2": + # For this algorithm we expect a curve25519 key. + key_name = "curve25519:%s" % (device_id,) + current_keys = [keys.get(key_name)] + else: + # We don't know understand the algorithm, so we just + # check it matches a key for the device. + current_keys = keys.values() + elif device_id: + # We don't have any keys for the device ID. + current_keys = [] + else: + # The event didn't include a device ID, so we just look for + # keys across all devices. + current_keys = ( + key + for device in cached_devices + for key in device.get("keys", {}).get("keys", {}).values() ) - # Immediately attempt a resync in the background - if self.config.worker_app: - return run_in_background(self._user_device_resync, event.sender) - else: - return run_in_background( - self._device_list_updater.user_device_resync, event.sender - ) + # We now check that the sender key matches (one of) the expected + # keys. + if sender_key not in current_keys: + logger.info( + "Received event from remote device with unexpected sender key: %s %s: %s", + event.sender, + device_id or "", + sender_key, + ) + resync = True + + if resync: + await self.store.mark_remote_user_device_cache_as_stale(event.sender) + + # Immediately attempt a resync in the background + if self.config.worker_app: + return run_in_background(self._user_device_resync, event.sender) + else: + return run_in_background( + self._device_list_updater.user_device_resync, event.sender + ) @log_function async def backfill(self, dest, room_id, limit, extremities): From 146fec08208144c8566da5ab6c2683229d162c1a Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 5 Feb 2020 15:47:00 +0000 Subject: [PATCH 0984/1623] Apply suggestions from code review Co-Authored-By: Erik Johnston --- synapse/federation/federation_client.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index 51f9b1d8d7..b4525d28c2 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -528,7 +528,7 @@ class FederationClient(FederationBase): Returns: a dict with members ``origin`` (a string - giving the serer the event was sent to, ``state`` (?) and + giving the server the event was sent to, ``state`` (?) and ``auth_chain``. Raises: @@ -659,7 +659,9 @@ class FederationClient(FederationBase): # content. return resp[1] - async def send_invite(self, destination, room_id, event_id, pdu): + async def send_invite( + self, destination: str, room_id: str, event_id: str, pdu: EventBase, + ) -> EventBase: room_version = await self.store.get_room_version_id(room_id) content = await self._do_send_invite(destination, pdu, room_version) From 6bbd890f05b59ffa33769ae15ea73bdf3c8dc908 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 5 Feb 2020 15:49:42 +0000 Subject: [PATCH 0985/1623] make FederationClient._do_send_invite async --- synapse/federation/federation_client.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index b4525d28c2..3a840e068b 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -53,6 +53,7 @@ from synapse.events import EventBase, builder, room_version_to_event_format from synapse.federation.federation_base import FederationBase, event_from_pdu_json from synapse.logging.context import make_deferred_yieldable from synapse.logging.utils import log_function +from synapse.types import JsonDict from synapse.util import unwrapFirstError from synapse.util.caches.expiringcache import ExpiringCache from synapse.util.retryutils import NotRetryingDestination @@ -682,23 +683,19 @@ class FederationClient(FederationBase): return pdu - @defer.inlineCallbacks - def _do_send_invite(self, destination, pdu, room_version): + async def _do_send_invite( + self, destination: str, pdu: EventBase, room_version: str + ) -> JsonDict: """Actually sends the invite, first trying v2 API and falling back to v1 API if necessary. - Args: - destination (str): Target server - pdu (FrozenEvent) - room_version (str) - Returns: - dict: The event as a dict as returned by the remote server + The event as a dict as returned by the remote server """ time_now = self._clock.time_msec() try: - content = yield self.transport_layer.send_invite_v2( + content = await self.transport_layer.send_invite_v2( destination=destination, room_id=pdu.room_id, event_id=pdu.event_id, @@ -737,7 +734,7 @@ class FederationClient(FederationBase): # Didn't work, try v1 API. # Note the v1 API returns a tuple of `(200, content)` - _, content = yield self.transport_layer.send_invite_v1( + _, content = await self.transport_layer.send_invite_v1( destination=destination, room_id=pdu.room_id, event_id=pdu.event_id, From f84700fba8cd9345d7a1a025462c7f8650f27386 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 31 Jan 2020 14:07:31 +0000 Subject: [PATCH 0986/1623] Pass room version object into `FederationClient.get_pdu` --- synapse/federation/federation_client.py | 8 +++++--- synapse/handlers/federation.py | 8 ++------ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index 3a840e068b..d2d42e7009 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -230,7 +230,7 @@ class FederationClient(FederationBase): self, destinations: Iterable[str], event_id: str, - room_version: str, + room_version: RoomVersion, outlier: bool = False, timeout: Optional[int] = None, ) -> Optional[EventBase]: @@ -262,7 +262,7 @@ class FederationClient(FederationBase): pdu_attempts = self.pdu_destination_tried.setdefault(event_id, {}) - format_ver = room_version_to_event_format(room_version) + format_ver = room_version.event_format signed_pdu = None for destination in destinations: @@ -292,7 +292,9 @@ class FederationClient(FederationBase): pdu = pdu_list[0] # Check signatures are correct. - signed_pdu = await self._check_sigs_and_hash(room_version, pdu) + signed_pdu = await self._check_sigs_and_hash( + room_version.identifier, pdu + ) break diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 5728ea2ee7..5ca410e2b3 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1110,7 +1110,7 @@ class FederationHandler(BaseHandler): Logs a warning if we can't find the given event. """ - room_version = await self.store.get_room_version_id(room_id) + room_version = await self.store.get_room_version(room_id) event_infos = [] @@ -1916,11 +1916,7 @@ class FederationHandler(BaseHandler): for e_id in missing_auth_events: m_ev = await self.federation_client.get_pdu( - [origin], - e_id, - room_version=room_version.identifier, - outlier=True, - timeout=10000, + [origin], e_id, room_version=room_version, outlier=True, timeout=10000, ) if m_ev and m_ev.event_id == e_id: event_map[e_id] = m_ev From ee0525b2b2245d66a3ce0c186cb1ac1725dd93e4 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 5 Feb 2020 17:35:09 +0000 Subject: [PATCH 0987/1623] Simplify `room_version` handling in `FederationClient.send_invite` --- synapse/federation/federation_client.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index d2d42e7009..110e42b9ed 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -665,7 +665,7 @@ class FederationClient(FederationBase): async def send_invite( self, destination: str, room_id: str, event_id: str, pdu: EventBase, ) -> EventBase: - room_version = await self.store.get_room_version_id(room_id) + room_version = await self.store.get_room_version(room_id) content = await self._do_send_invite(destination, pdu, room_version) @@ -673,20 +673,17 @@ class FederationClient(FederationBase): logger.debug("Got response to send_invite: %s", pdu_dict) - room_version = await self.store.get_room_version_id(room_id) - format_ver = room_version_to_event_format(room_version) - - pdu = event_from_pdu_json(pdu_dict, format_ver) + pdu = event_from_pdu_json(pdu_dict, room_version.event_format) # Check signatures are correct. - pdu = await self._check_sigs_and_hash(room_version, pdu) + pdu = await self._check_sigs_and_hash(room_version.identifier, pdu) # FIXME: We should handle signature failures more gracefully. return pdu async def _do_send_invite( - self, destination: str, pdu: EventBase, room_version: str + self, destination: str, pdu: EventBase, room_version: RoomVersion ) -> JsonDict: """Actually sends the invite, first trying v2 API and falling back to v1 API if necessary. @@ -703,7 +700,7 @@ class FederationClient(FederationBase): event_id=pdu.event_id, content={ "event": pdu.get_pdu_json(time_now), - "room_version": room_version, + "room_version": room_version.identifier, "invite_room_state": pdu.unsigned.get("invite_room_state", []), }, ) @@ -721,8 +718,7 @@ class FederationClient(FederationBase): # Otherwise, we assume that the remote server doesn't understand # the v2 invite API. That's ok provided the room uses old-style event # IDs. - v = KNOWN_ROOM_VERSIONS.get(room_version) - if v.event_format != EventFormatVersions.V1: + if room_version.event_format != EventFormatVersions.V1: raise SynapseError( 400, "User's homeserver does not support this room version", From ff70ec0a00bd65d819ce42fdb46d67160c197202 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 31 Jan 2020 15:30:02 +0000 Subject: [PATCH 0988/1623] Newsfile --- changelog.d/6823.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6823.misc diff --git a/changelog.d/6823.misc b/changelog.d/6823.misc new file mode 100644 index 0000000000..08aa80bcd9 --- /dev/null +++ b/changelog.d/6823.misc @@ -0,0 +1 @@ +Refactoring work in preparation for changing the event redaction algorithm. From 39c2d26e0b80ee0cd5589fc577327f2d3d80a446 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 5 Feb 2020 12:41:33 -0500 Subject: [PATCH 0989/1623] Add quotes around pip install target (my shell complained without them). --- README.rst | 2 +- changelog.d/6855.misc | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/6855.misc diff --git a/README.rst b/README.rst index 2691dfc23d..4db7d17e94 100644 --- a/README.rst +++ b/README.rst @@ -272,7 +272,7 @@ to install using pip and a virtualenv:: virtualenv -p python3 env source env/bin/activate - python -m pip install --no-use-pep517 -e .[all] + python -m pip install --no-use-pep517 -e ".[all]" This will run a process of downloading and installing all the needed dependencies into a virtual env. diff --git a/changelog.d/6855.misc b/changelog.d/6855.misc new file mode 100644 index 0000000000..904361ddfb --- /dev/null +++ b/changelog.d/6855.misc @@ -0,0 +1 @@ +Update pip install directiosn in readme to avoid error when using zsh. From f0561fcffd172cb0dfe035dcc78f51bdd451c010 Mon Sep 17 00:00:00 2001 From: Robin Vleij Date: Wed, 5 Feb 2020 22:27:38 +0100 Subject: [PATCH 0990/1623] Update documentation (#6859) Update documentation to reflect the correct format of user_id (fully qualified). --- docs/admin_api/user_admin_api.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/admin_api/user_admin_api.rst b/docs/admin_api/user_admin_api.rst index 0b3d09d694..ed6df61a26 100644 --- a/docs/admin_api/user_admin_api.rst +++ b/docs/admin_api/user_admin_api.rst @@ -2,7 +2,8 @@ Create or modify Account ======================== This API allows an administrator to create or modify a user account with a -specific ``user_id``. +specific ``user_id``. Be aware that ``user_id`` is fully qualified: for example, +``@user:server.com``. This api is:: From 6a7e90ad782bddce95fa0c7d93e56291aa31c33d Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 6 Feb 2020 10:40:08 +0000 Subject: [PATCH 0991/1623] 1.10.0rc2 --- CHANGES.md | 16 ++++++++++++++++ changelog.d/6844.bugfix | 1 - changelog.d/6848.bugfix | 1 - changelog.d/6850.misc | 1 - synapse/__init__.py | 2 +- 5 files changed, 17 insertions(+), 4 deletions(-) delete mode 100644 changelog.d/6844.bugfix delete mode 100644 changelog.d/6848.bugfix delete mode 100644 changelog.d/6850.misc diff --git a/CHANGES.md b/CHANGES.md index ab6fce3e7d..ee0e5d25e4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,19 @@ +Synapse 1.10.0rc2 (2020-02-06) +============================== + +Bugfixes +-------- + +- Fix an issue with cross-signing where device signatures were not sent to remote servers. ([\#6844](https://github.com/matrix-org/synapse/issues/6844)) +- Fix detecting unknown devices from remote encrypted events. ([\#6848](https://github.com/matrix-org/synapse/issues/6848)) + + +Internal Changes +---------------- + +- Detect unexpected sender keys on inbound encrypted events and resync device lists. ([\#6850](https://github.com/matrix-org/synapse/issues/6850)) + + Synapse 1.10.0rc1 (2020-01-31) ============================== diff --git a/changelog.d/6844.bugfix b/changelog.d/6844.bugfix deleted file mode 100644 index e84aa1029f..0000000000 --- a/changelog.d/6844.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix an issue with cross-signing where device signatures were not sent to remote servers. diff --git a/changelog.d/6848.bugfix b/changelog.d/6848.bugfix deleted file mode 100644 index 65688e5d57..0000000000 --- a/changelog.d/6848.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix detecting unknown devices from remote encrypted events. diff --git a/changelog.d/6850.misc b/changelog.d/6850.misc deleted file mode 100644 index 418569113f..0000000000 --- a/changelog.d/6850.misc +++ /dev/null @@ -1 +0,0 @@ -Detect unexpected sender keys on inbound encrypted events and resync device lists. diff --git a/synapse/__init__.py b/synapse/__init__.py index bd942d3e1c..4f1859bd57 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -36,7 +36,7 @@ try: except ImportError: pass -__version__ = "1.10.0rc1" +__version__ = "1.10.0rc2" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 4a50b674f2fa730fd3e85fe5d59b84b00e34bfc7 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 6 Feb 2020 10:45:29 +0000 Subject: [PATCH 0992/1623] Update changelog --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index ee0e5d25e4..e56f738481 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,7 +17,7 @@ Internal Changes Synapse 1.10.0rc1 (2020-01-31) ============================== -**WARNING**: As of this release Synapse validates `client_secret` parameters in the Client-Server API as per the spec. See [\#6766](https://github.com/matrix-org/synapse/issues/6766) for details. +**WARNING to client developers**: As of this release Synapse validates `client_secret` parameters in the Client-Server API as per the spec. See [\#6766](https://github.com/matrix-org/synapse/issues/6766) for details. Features From b5176166b7b62f3c04c21f12a775982c99a90c9c Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 6 Feb 2020 10:51:02 +0000 Subject: [PATCH 0993/1623] Update changelog --- CHANGES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index e56f738481..17c7c91c62 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,13 +5,13 @@ Bugfixes -------- - Fix an issue with cross-signing where device signatures were not sent to remote servers. ([\#6844](https://github.com/matrix-org/synapse/issues/6844)) -- Fix detecting unknown devices from remote encrypted events. ([\#6848](https://github.com/matrix-org/synapse/issues/6848)) +- Fix to the unknown remote device detection which was indroduced in 1.10.rc1. ([\#6848](https://github.com/matrix-org/synapse/issues/6848)) Internal Changes ---------------- -- Detect unexpected sender keys on inbound encrypted events and resync device lists. ([\#6850](https://github.com/matrix-org/synapse/issues/6850)) +- Detect unexpected sender keys on remote encrypted events and resync device lists. ([\#6850](https://github.com/matrix-org/synapse/issues/6850)) Synapse 1.10.0rc1 (2020-01-31) From f663118155dbc242f99165f978817ef9dbeb9fd1 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 6 Feb 2020 10:52:25 +0000 Subject: [PATCH 0994/1623] Update changelog --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 17c7c91c62..c2aa735908 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,7 +5,7 @@ Bugfixes -------- - Fix an issue with cross-signing where device signatures were not sent to remote servers. ([\#6844](https://github.com/matrix-org/synapse/issues/6844)) -- Fix to the unknown remote device detection which was indroduced in 1.10.rc1. ([\#6848](https://github.com/matrix-org/synapse/issues/6848)) +- Fix to the unknown remote device detection which was introduced in 1.10.rc1. ([\#6848](https://github.com/matrix-org/synapse/issues/6848)) Internal Changes From ed630ea17c40d328cc0796e35d37287768c7140d Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 6 Feb 2020 13:31:05 +0000 Subject: [PATCH 0995/1623] Reduce amount of logging at INFO level. (#6862) A lot of the things we log at INFO are now a bit superfluous, so lets make them DEBUG logs to reduce the amount we log by default. Co-Authored-By: Brendan Abolivier Co-authored-by: Brendan Abolivier --- changelog.d/6862.misc | 1 + synapse/federation/federation_server.py | 6 +++--- synapse/federation/transport/server.py | 2 +- synapse/handlers/room.py | 10 +++++----- synapse/handlers/stats.py | 2 +- synapse/handlers/sync.py | 6 +++--- synapse/handlers/user_directory.py | 4 ++-- synapse/http/site.py | 2 +- synapse/push/httppusher.py | 2 +- synapse/storage/data_stores/main/user_directory.py | 4 ++-- synapse/storage/persist_events.py | 2 +- synapse/util/caches/response_cache.py | 2 +- 12 files changed, 22 insertions(+), 21 deletions(-) create mode 100644 changelog.d/6862.misc diff --git a/changelog.d/6862.misc b/changelog.d/6862.misc new file mode 100644 index 0000000000..83626d2939 --- /dev/null +++ b/changelog.d/6862.misc @@ -0,0 +1 @@ +Reduce amount we log at `INFO` level. diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index d92d5e8064..8e3933b6c5 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -573,7 +573,7 @@ class FederationServer(FederationBase): origin_host, _ = parse_server_name(origin) await self.check_server_matches_acl(origin_host, room_id) - logger.info( + logger.debug( "on_get_missing_events: earliest_events: %r, latest_events: %r," " limit: %d", earliest_events, @@ -586,11 +586,11 @@ class FederationServer(FederationBase): ) if len(missing_events) < 5: - logger.info( + logger.debug( "Returning %d events: %r", len(missing_events), missing_events ) else: - logger.info("Returning %d events", len(missing_events)) + logger.debug("Returning %d events", len(missing_events)) time_now = self._clock.time_msec() diff --git a/synapse/federation/transport/server.py b/synapse/federation/transport/server.py index ae48ba8157..92a9ae2320 100644 --- a/synapse/federation/transport/server.py +++ b/synapse/federation/transport/server.py @@ -158,7 +158,7 @@ class Authenticator(object): origin, json_request, now, "Incoming request" ) - logger.info("Request from %s", origin) + logger.debug("Request from %s", origin) request.authenticated_entity = origin # If we get a valid signed request from the other side, its probably diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index b609a65f47..559e3399b8 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -259,7 +259,7 @@ class RoomCreationHandler(BaseHandler): for v in ("invite", "events_default"): current = int(pl_content.get(v, 0)) if current < restricted_level: - logger.info( + logger.debug( "Setting level for %s in %s to %i (was %i)", v, old_room_id, @@ -269,7 +269,7 @@ class RoomCreationHandler(BaseHandler): pl_content[v] = restricted_level updated = True else: - logger.info("Not setting level for %s (already %i)", v, current) + logger.debug("Not setting level for %s (already %i)", v, current) if updated: try: @@ -296,7 +296,7 @@ class RoomCreationHandler(BaseHandler): EventTypes.Aliases, events_default ) - logger.info("Setting correct PLs in new room to %s", new_pl_content) + logger.debug("Setting correct PLs in new room to %s", new_pl_content) yield self.event_creation_handler.create_and_send_nonmember_event( requester, { @@ -782,7 +782,7 @@ class RoomCreationHandler(BaseHandler): @defer.inlineCallbacks def send(etype, content, **kwargs): event = create(etype, content, **kwargs) - logger.info("Sending %s in new room", etype) + logger.debug("Sending %s in new room", etype) yield self.event_creation_handler.create_and_send_nonmember_event( creator, event, ratelimit=False ) @@ -796,7 +796,7 @@ class RoomCreationHandler(BaseHandler): creation_content.update({"creator": creator_id}) yield send(etype=EventTypes.Create, content=creation_content) - logger.info("Sending %s in new room", EventTypes.Member) + logger.debug("Sending %s in new room", EventTypes.Member) yield self.room_member_handler.update_membership( creator, creator.user, diff --git a/synapse/handlers/stats.py b/synapse/handlers/stats.py index 68e6edace5..d93a276693 100644 --- a/synapse/handlers/stats.py +++ b/synapse/handlers/stats.py @@ -300,7 +300,7 @@ class StatsHandler(StateDeltasHandler): room_state["guest_access"] = event_content.get("guest_access") for room_id, state in room_to_state_updates.items(): - logger.info("Updating room_stats_state for %s: %s", room_id, state) + logger.debug("Updating room_stats_state for %s: %s", room_id, state) yield self.store.update_room_state(room_id, state) return room_to_stats_deltas, user_to_stats_deltas diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index 5f060241b4..f8d60d32ba 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -968,7 +968,7 @@ class SyncHandler(object): # Always use the `now_token` in `SyncResultBuilder` now_token = await self.event_sources.get_current_token() - logger.info( + logger.debug( "Calculating sync response for %r between %s and %s", sync_config.user, since_token, @@ -1498,7 +1498,7 @@ class SyncHandler(object): room_entries = [] invited = [] for room_id, events in iteritems(mem_change_events_by_room_id): - logger.info( + logger.debug( "Membership changes in %s: [%s]", room_id, ", ".join(("%s (%s)" % (e.event_id, e.membership) for e in events)), @@ -1892,7 +1892,7 @@ class SyncHandler(object): if batch.limited and since_token: user_id = sync_result_builder.sync_config.user.to_string() - logger.info( + logger.debug( "Incremental gappy sync of %s for user %s with %d state events" % (room_id, user_id, len(state)) ) diff --git a/synapse/handlers/user_directory.py b/synapse/handlers/user_directory.py index 624f05ab5b..81aa58dc8c 100644 --- a/synapse/handlers/user_directory.py +++ b/synapse/handlers/user_directory.py @@ -149,7 +149,7 @@ class UserDirectoryHandler(StateDeltasHandler): self.pos, room_max_stream_ordering ) - logger.info("Handling %d state deltas", len(deltas)) + logger.debug("Handling %d state deltas", len(deltas)) yield self._handle_deltas(deltas) self.pos = max_pos @@ -195,7 +195,7 @@ class UserDirectoryHandler(StateDeltasHandler): room_id, self.server_name ) if not is_in_room: - logger.info("Server left room: %r", room_id) + logger.debug("Server left room: %r", room_id) # Fetch all the users that we marked as being in user # directory due to being in the room and then check if # need to remove those users or not diff --git a/synapse/http/site.py b/synapse/http/site.py index 911251c0bc..e092193c9c 100644 --- a/synapse/http/site.py +++ b/synapse/http/site.py @@ -225,7 +225,7 @@ class SynapseRequest(Request): self.start_time, name=servlet_name, method=self.get_method() ) - self.site.access_logger.info( + self.site.access_logger.debug( "%s - %s - Received request: %s %s", self.getClientIP(), self.site.site_tag, diff --git a/synapse/push/httppusher.py b/synapse/push/httppusher.py index d0879b0490..5bb17d1228 100644 --- a/synapse/push/httppusher.py +++ b/synapse/push/httppusher.py @@ -398,7 +398,7 @@ class HttpPusher(object): Args: badge (int): number of unread messages """ - logger.info("Sending updated badge count %d to %s", badge, self.name) + logger.debug("Sending updated badge count %d to %s", badge, self.name) d = { "notification": { "id": "", diff --git a/synapse/storage/data_stores/main/user_directory.py b/synapse/storage/data_stores/main/user_directory.py index 90c180ec6d..6b8130bf0f 100644 --- a/synapse/storage/data_stores/main/user_directory.py +++ b/synapse/storage/data_stores/main/user_directory.py @@ -183,7 +183,7 @@ class UserDirectoryBackgroundUpdateStore(StateDeltasStore): ) return 1 - logger.info( + logger.debug( "Processing the next %d rooms of %d remaining" % (len(rooms_to_work_on), progress["remaining"]) ) @@ -308,7 +308,7 @@ class UserDirectoryBackgroundUpdateStore(StateDeltasStore): ) return 1 - logger.info( + logger.debug( "Processing the next %d users of %d remaining" % (len(users_to_work_on), progress["remaining"]) ) diff --git a/synapse/storage/persist_events.py b/synapse/storage/persist_events.py index af3fd67ab9..a5370ed527 100644 --- a/synapse/storage/persist_events.py +++ b/synapse/storage/persist_events.py @@ -390,7 +390,7 @@ class EventsPersistenceStorage(object): state_delta_reuse_delta_counter.inc() break - logger.info("Calculating state delta for room %s", room_id) + logger.debug("Calculating state delta for room %s", room_id) with Measure( self._clock, "persist_events.get_new_state_after_events" ): diff --git a/synapse/util/caches/response_cache.py b/synapse/util/caches/response_cache.py index 82d3eefe0e..b68f9fe0d4 100644 --- a/synapse/util/caches/response_cache.py +++ b/synapse/util/caches/response_cache.py @@ -144,7 +144,7 @@ class ResponseCache(object): """ result = self.get(key) if not result: - logger.info( + logger.debug( "[%s]: no cached result for [%s], calculating new one", self._name, key ) d = run_in_background(callback, *args, **kwargs) From 99fcc96289f673f96f2d180a84df84f6b8a85521 Mon Sep 17 00:00:00 2001 From: PeerD Date: Thu, 6 Feb 2020 15:15:29 +0100 Subject: [PATCH 0996/1623] Third party event rules Update (#6781) --- changelog.d/6781.bugfix | 1 + synapse/events/third_party_rules.py | 7 ++++--- synapse/handlers/room.py | 6 +++++- 3 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 changelog.d/6781.bugfix diff --git a/changelog.d/6781.bugfix b/changelog.d/6781.bugfix new file mode 100644 index 0000000000..47cd671bff --- /dev/null +++ b/changelog.d/6781.bugfix @@ -0,0 +1 @@ +Fixed third party event rules function `on_create_room`'s return value being ignored. diff --git a/synapse/events/third_party_rules.py b/synapse/events/third_party_rules.py index 86f7e5f8aa..459132d388 100644 --- a/synapse/events/third_party_rules.py +++ b/synapse/events/third_party_rules.py @@ -74,15 +74,16 @@ class ThirdPartyEventRules(object): is_requester_admin (bool): If the requester is an admin Returns: - defer.Deferred + defer.Deferred[bool]: Whether room creation is allowed or denied. """ if self.third_party_rules is None: - return + return True - yield self.third_party_rules.on_create_room( + ret = yield self.third_party_rules.on_create_room( requester, config, is_requester_admin ) + return ret @defer.inlineCallbacks def check_threepid_can_be_invited(self, medium, address, room_id): diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 559e3399b8..ab07edd2fc 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -579,9 +579,13 @@ class RoomCreationHandler(BaseHandler): # Check whether the third party rules allows/changes the room create # request. - yield self.third_party_event_rules.on_create_room( + event_allowed = yield self.third_party_event_rules.on_create_room( requester, config, is_requester_admin=is_requester_admin ) + if not event_allowed: + raise SynapseError( + 403, "You are not permitted to create rooms", Codes.FORBIDDEN + ) if not is_requester_admin and not self.spam_checker.user_may_create_room( user_id From bce557175bad82889d303b349e6575636c41b702 Mon Sep 17 00:00:00 2001 From: timfi Date: Thu, 6 Feb 2020 15:45:01 +0100 Subject: [PATCH 0997/1623] Allow empty federation_certificate_verification_whitelist (#6849) --- changelog.d/6849.bugfix | 1 + synapse/config/tls.py | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 changelog.d/6849.bugfix diff --git a/changelog.d/6849.bugfix b/changelog.d/6849.bugfix new file mode 100644 index 0000000000..d928a26ec6 --- /dev/null +++ b/changelog.d/6849.bugfix @@ -0,0 +1 @@ +Fix Synapse refusing to start if `federation_certificate_verification_whitelist` option is blank. diff --git a/synapse/config/tls.py b/synapse/config/tls.py index 2e9e478a2a..2514b0713d 100644 --- a/synapse/config/tls.py +++ b/synapse/config/tls.py @@ -109,6 +109,8 @@ class TlsConfig(Config): fed_whitelist_entries = config.get( "federation_certificate_verification_whitelist", [] ) + if fed_whitelist_entries is None: + fed_whitelist_entries = [] # Support globs (*) in whitelist values self.federation_certificate_verification_whitelist = [] # type: List[str] From b0c8bdd49dd416ed066b12daee95cf5f4828f03b Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 6 Feb 2020 15:50:39 +0000 Subject: [PATCH 0998/1623] pass room version into FederationClient.send_join (#6854) ... which allows us to sanity-check the create event. --- changelog.d/6854.misc | 1 + synapse/federation/federation_client.py | 60 +++++++++++++------------ synapse/handlers/federation.py | 3 +- 3 files changed, 34 insertions(+), 30 deletions(-) create mode 100644 changelog.d/6854.misc diff --git a/changelog.d/6854.misc b/changelog.d/6854.misc new file mode 100644 index 0000000000..08aa80bcd9 --- /dev/null +++ b/changelog.d/6854.misc @@ -0,0 +1 @@ +Refactoring work in preparation for changing the event redaction algorithm. diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index 110e42b9ed..5fb4bd414c 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -516,7 +516,7 @@ class FederationClient(FederationBase): ) async def send_join( - self, destinations: Iterable[str], pdu: EventBase, event_format_version: int + self, destinations: Iterable[str], pdu: EventBase, room_version: RoomVersion ) -> Dict[str, Any]: """Sends a join event to one of a list of homeservers. @@ -527,7 +527,8 @@ class FederationClient(FederationBase): destinations: Candidate homeservers which are probably participating in the room. pdu: event to be sent - event_format_version: The event format version + room_version: the version of the room (according to the server that + did the make_join) Returns: a dict with members ``origin`` (a string @@ -540,58 +541,51 @@ class FederationClient(FederationBase): RuntimeError: if no servers were reachable. """ - def check_authchain_validity(signed_auth_chain): - for e in signed_auth_chain: - if e.type == EventTypes.Create: - create_event = e - break - else: - raise InvalidResponseError("no %s in auth chain" % (EventTypes.Create,)) - - # the room version should be sane. - room_version = create_event.content.get("room_version", "1") - if room_version not in KNOWN_ROOM_VERSIONS: - # This shouldn't be possible, because the remote server should have - # rejected the join attempt during make_join. - raise InvalidResponseError( - "room appears to have unsupported version %s" % (room_version,) - ) - async def send_request(destination) -> Dict[str, Any]: content = await self._do_send_join(destination, pdu) logger.debug("Got content: %s", content) state = [ - event_from_pdu_json(p, event_format_version, outlier=True) + event_from_pdu_json(p, room_version.event_format, outlier=True) for p in content.get("state", []) ] auth_chain = [ - event_from_pdu_json(p, event_format_version, outlier=True) + event_from_pdu_json(p, room_version.event_format, outlier=True) for p in content.get("auth_chain", []) ] pdus = {p.event_id: p for p in itertools.chain(state, auth_chain)} - room_version = None + create_event = None for e in state: if (e.type, e.state_key) == (EventTypes.Create, ""): - room_version = e.content.get( - "room_version", RoomVersions.V1.identifier - ) + create_event = e break - if room_version is None: + if create_event is None: # If the state doesn't have a create event then the room is # invalid, and it would fail auth checks anyway. raise SynapseError(400, "No create event in state") + # the room version should be sane. + create_room_version = create_event.content.get( + "room_version", RoomVersions.V1.identifier + ) + if create_room_version != room_version.identifier: + # either the server that fulfilled the make_join, or the server that is + # handling the send_join, is lying. + raise InvalidResponseError( + "Unexpected room version %s in create event" + % (create_room_version,) + ) + valid_pdus = await self._check_sigs_and_hash_and_fetch( destination, list(pdus.values()), outlier=True, - room_version=room_version, + room_version=room_version.identifier, ) valid_pdus_map = {p.event_id: p for p in valid_pdus} @@ -615,7 +609,17 @@ class FederationClient(FederationBase): for s in signed_state: s.internal_metadata = copy.deepcopy(s.internal_metadata) - check_authchain_validity(signed_auth) + # double-check that the same create event has ended up in the auth chain + auth_chain_create_events = [ + e.event_id + for e in signed_auth + if (e.type, e.state_key) == (EventTypes.Create, "") + ] + if auth_chain_create_events != [create_event.event_id]: + raise InvalidResponseError( + "Unexpected create event(s) in auth chain" + % (auth_chain_create_events,) + ) return { "state": signed_state, diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index ef3cc264b7..10e8b6ea4c 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1305,9 +1305,8 @@ class FederationHandler(BaseHandler): except ValueError: pass - event_format_version = room_version_obj.event_format ret = await self.federation_client.send_join( - target_hosts, event, event_format_version + target_hosts, event, room_version_obj ) origin = ret["origin"] From 928edef9793bf10fa6156a42c4babbfaaaa17f88 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 31 Jan 2020 16:50:13 +0000 Subject: [PATCH 0999/1623] Pass room_version into `event_from_pdu_json` It's called from all over the shop, so this one's a bit messy. --- changelog.d/6856.misc | 1 + synapse/federation/federation_base.py | 28 +++++++++-------- synapse/federation/federation_client.py | 35 ++++++++++----------- synapse/federation/federation_server.py | 41 +++++++++---------------- tests/handlers/test_federation.py | 6 ++-- 5 files changed, 51 insertions(+), 60 deletions(-) create mode 100644 changelog.d/6856.misc diff --git a/changelog.d/6856.misc b/changelog.d/6856.misc new file mode 100644 index 0000000000..08aa80bcd9 --- /dev/null +++ b/changelog.d/6856.misc @@ -0,0 +1 @@ +Refactoring work in preparation for changing the event redaction algorithm. diff --git a/synapse/federation/federation_base.py b/synapse/federation/federation_base.py index 0e22183280..ebe8b8e9fe 100644 --- a/synapse/federation/federation_base.py +++ b/synapse/federation/federation_base.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd +# Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,9 +23,13 @@ from twisted.internet.defer import DeferredList from synapse.api.constants import MAX_DEPTH, EventTypes, Membership from synapse.api.errors import Codes, SynapseError -from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, EventFormatVersions +from synapse.api.room_versions import ( + KNOWN_ROOM_VERSIONS, + EventFormatVersions, + RoomVersion, +) from synapse.crypto.event_signing import check_event_content_hash -from synapse.events import event_type_from_format_version +from synapse.events import EventBase, event_type_from_format_version from synapse.events.utils import prune_event from synapse.http.servlet import assert_params_in_dict from synapse.logging.context import ( @@ -33,7 +38,7 @@ from synapse.logging.context import ( make_deferred_yieldable, preserve_fn, ) -from synapse.types import get_domain_from_id +from synapse.types import JsonDict, get_domain_from_id from synapse.util import unwrapFirstError logger = logging.getLogger(__name__) @@ -342,16 +347,15 @@ def _is_invite_via_3pid(event): ) -def event_from_pdu_json(pdu_json, event_format_version, outlier=False): - """Construct a FrozenEvent from an event json received over federation +def event_from_pdu_json( + pdu_json: JsonDict, room_version: RoomVersion, outlier: bool = False +) -> EventBase: + """Construct an EventBase from an event json received over federation Args: - pdu_json (object): pdu as received over federation - event_format_version (int): The event format version - outlier (bool): True to mark this event as an outlier - - Returns: - FrozenEvent + pdu_json: pdu as received over federation + room_version: The version of the room this event belongs to + outlier: True to mark this event as an outlier Raises: SynapseError: if the pdu is missing required fields or is otherwise @@ -370,7 +374,7 @@ def event_from_pdu_json(pdu_json, event_format_version, outlier=False): elif depth > MAX_DEPTH: raise SynapseError(400, "Depth too large", Codes.BAD_JSON) - event = event_type_from_format_version(event_format_version)(pdu_json) + event = event_type_from_format_version(room_version.event_format)(pdu_json) event.internal_metadata.outlier = outlier diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index 5fb4bd414c..4870e39652 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -49,7 +49,7 @@ from synapse.api.room_versions import ( RoomVersion, RoomVersions, ) -from synapse.events import EventBase, builder, room_version_to_event_format +from synapse.events import EventBase, builder from synapse.federation.federation_base import FederationBase, event_from_pdu_json from synapse.logging.context import make_deferred_yieldable from synapse.logging.utils import log_function @@ -209,18 +209,18 @@ class FederationClient(FederationBase): logger.debug("backfill transaction_data=%r", transaction_data) - room_version = await self.store.get_room_version_id(room_id) - format_ver = room_version_to_event_format(room_version) + room_version = await self.store.get_room_version(room_id) pdus = [ - event_from_pdu_json(p, format_ver, outlier=False) + event_from_pdu_json(p, room_version, outlier=False) for p in transaction_data["pdus"] ] # FIXME: We should handle signature failures more gracefully. pdus[:] = await make_deferred_yieldable( defer.gatherResults( - self._check_sigs_and_hashes(room_version, pdus), consumeErrors=True + self._check_sigs_and_hashes(room_version.identifier, pdus), + consumeErrors=True, ).addErrback(unwrapFirstError) ) @@ -262,8 +262,6 @@ class FederationClient(FederationBase): pdu_attempts = self.pdu_destination_tried.setdefault(event_id, {}) - format_ver = room_version.event_format - signed_pdu = None for destination in destinations: now = self._clock.time_msec() @@ -284,7 +282,7 @@ class FederationClient(FederationBase): ) pdu_list = [ - event_from_pdu_json(p, format_ver, outlier=outlier) + event_from_pdu_json(p, room_version, outlier=outlier) for p in transaction_data["pdus"] ] @@ -350,15 +348,15 @@ class FederationClient(FederationBase): async def get_event_auth(self, destination, room_id, event_id): res = await self.transport_layer.get_event_auth(destination, room_id, event_id) - room_version = await self.store.get_room_version_id(room_id) - format_ver = room_version_to_event_format(room_version) + room_version = await self.store.get_room_version(room_id) auth_chain = [ - event_from_pdu_json(p, format_ver, outlier=True) for p in res["auth_chain"] + event_from_pdu_json(p, room_version, outlier=True) + for p in res["auth_chain"] ] signed_auth = await self._check_sigs_and_hash_and_fetch( - destination, auth_chain, outlier=True, room_version=room_version + destination, auth_chain, outlier=True, room_version=room_version.identifier ) signed_auth.sort(key=lambda e: e.depth) @@ -547,12 +545,12 @@ class FederationClient(FederationBase): logger.debug("Got content: %s", content) state = [ - event_from_pdu_json(p, room_version.event_format, outlier=True) + event_from_pdu_json(p, room_version, outlier=True) for p in content.get("state", []) ] auth_chain = [ - event_from_pdu_json(p, room_version.event_format, outlier=True) + event_from_pdu_json(p, room_version, outlier=True) for p in content.get("auth_chain", []) ] @@ -677,7 +675,7 @@ class FederationClient(FederationBase): logger.debug("Got response to send_invite: %s", pdu_dict) - pdu = event_from_pdu_json(pdu_dict, room_version.event_format) + pdu = event_from_pdu_json(pdu_dict, room_version) # Check signatures are correct. pdu = await self._check_sigs_and_hash(room_version.identifier, pdu) @@ -865,15 +863,14 @@ class FederationClient(FederationBase): timeout=timeout, ) - room_version = await self.store.get_room_version_id(room_id) - format_ver = room_version_to_event_format(room_version) + room_version = await self.store.get_room_version(room_id) events = [ - event_from_pdu_json(e, format_ver) for e in content.get("events", []) + event_from_pdu_json(e, room_version) for e in content.get("events", []) ] signed_events = await self._check_sigs_and_hash_and_fetch( - destination, events, outlier=False, room_version=room_version + destination, events, outlier=False, room_version=room_version.identifier ) except HttpResponseException as e: if not e.code == 400: diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index 8e3933b6c5..2489832a11 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -38,7 +38,6 @@ from synapse.api.errors import ( UnsupportedRoomVersionError, ) from synapse.api.room_versions import KNOWN_ROOM_VERSIONS -from synapse.events import room_version_to_event_format from synapse.federation.federation_base import FederationBase, event_from_pdu_json from synapse.federation.persistence import TransactionActions from synapse.federation.units import Edu, Transaction @@ -234,24 +233,17 @@ class FederationServer(FederationBase): continue try: - room_version = await self.store.get_room_version_id(room_id) + room_version = await self.store.get_room_version(room_id) except NotFoundError: logger.info("Ignoring PDU for unknown room_id: %s", room_id) continue - - try: - format_ver = room_version_to_event_format(room_version) - except UnsupportedRoomVersionError: + except UnsupportedRoomVersionError as e: # this can happen if support for a given room version is withdrawn, # so that we still get events for said room. - logger.info( - "Ignoring PDU for room %s with unknown version %s", - room_id, - room_version, - ) + logger.info("Ignoring PDU: %s", e) continue - event = event_from_pdu_json(p, format_ver) + event = event_from_pdu_json(p, room_version) pdus_by_room.setdefault(room_id, []).append(event) pdu_results = {} @@ -407,9 +399,7 @@ class FederationServer(FederationBase): Codes.UNSUPPORTED_ROOM_VERSION, ) - format_ver = room_version.event_format - - pdu = event_from_pdu_json(content, format_ver) + pdu = event_from_pdu_json(content, room_version) origin_host, _ = parse_server_name(origin) await self.check_server_matches_acl(origin_host, pdu.room_id) pdu = await self._check_sigs_and_hash(room_version.identifier, pdu) @@ -420,16 +410,15 @@ class FederationServer(FederationBase): async def on_send_join_request(self, origin, content, room_id): logger.debug("on_send_join_request: content: %s", content) - room_version = await self.store.get_room_version_id(room_id) - format_ver = room_version_to_event_format(room_version) - pdu = event_from_pdu_json(content, format_ver) + room_version = await self.store.get_room_version(room_id) + pdu = event_from_pdu_json(content, room_version) origin_host, _ = parse_server_name(origin) await self.check_server_matches_acl(origin_host, pdu.room_id) logger.debug("on_send_join_request: pdu sigs: %s", pdu.signatures) - pdu = await self._check_sigs_and_hash(room_version, pdu) + pdu = await self._check_sigs_and_hash(room_version.identifier, pdu) res_pdus = await self.handler.on_send_join_request(origin, pdu) time_now = self._clock.time_msec() @@ -451,16 +440,15 @@ class FederationServer(FederationBase): async def on_send_leave_request(self, origin, content, room_id): logger.debug("on_send_leave_request: content: %s", content) - room_version = await self.store.get_room_version_id(room_id) - format_ver = room_version_to_event_format(room_version) - pdu = event_from_pdu_json(content, format_ver) + room_version = await self.store.get_room_version(room_id) + pdu = event_from_pdu_json(content, room_version) origin_host, _ = parse_server_name(origin) await self.check_server_matches_acl(origin_host, pdu.room_id) logger.debug("on_send_leave_request: pdu sigs: %s", pdu.signatures) - pdu = await self._check_sigs_and_hash(room_version, pdu) + pdu = await self._check_sigs_and_hash(room_version.identifier, pdu) await self.handler.on_send_leave_request(origin, pdu) return {} @@ -498,15 +486,14 @@ class FederationServer(FederationBase): origin_host, _ = parse_server_name(origin) await self.check_server_matches_acl(origin_host, room_id) - room_version = await self.store.get_room_version_id(room_id) - format_ver = room_version_to_event_format(room_version) + room_version = await self.store.get_room_version(room_id) auth_chain = [ - event_from_pdu_json(e, format_ver) for e in content["auth_chain"] + event_from_pdu_json(e, room_version) for e in content["auth_chain"] ] signed_auth = await self._check_sigs_and_hash_and_fetch( - origin, auth_chain, outlier=True, room_version=room_version + origin, auth_chain, outlier=True, room_version=room_version.identifier ) ret = await self.handler.on_query_auth( diff --git a/tests/handlers/test_federation.py b/tests/handlers/test_federation.py index b4d92cf732..132e35651d 100644 --- a/tests/handlers/test_federation.py +++ b/tests/handlers/test_federation.py @@ -99,6 +99,7 @@ class FederationTestCase(unittest.HomeserverTestCase): user_id = self.register_user("kermit", "test") tok = self.login("kermit", "test") room_id = self.helper.create_room_as(room_creator=user_id, tok=tok) + room_version = self.get_success(self.store.get_room_version(room_id)) # pretend that another server has joined join_event = self._build_and_send_join_event(OTHER_SERVER, OTHER_USER, room_id) @@ -120,7 +121,7 @@ class FederationTestCase(unittest.HomeserverTestCase): "auth_events": [], "origin_server_ts": self.clock.time_msec(), }, - join_event.format_version, + room_version, ) with LoggingContext(request="send_rejected"): @@ -149,6 +150,7 @@ class FederationTestCase(unittest.HomeserverTestCase): user_id = self.register_user("kermit", "test") tok = self.login("kermit", "test") room_id = self.helper.create_room_as(room_creator=user_id, tok=tok) + room_version = self.get_success(self.store.get_room_version(room_id)) # pretend that another server has joined join_event = self._build_and_send_join_event(OTHER_SERVER, OTHER_USER, room_id) @@ -171,7 +173,7 @@ class FederationTestCase(unittest.HomeserverTestCase): "auth_events": [], "origin_server_ts": self.clock.time_msec(), }, - join_event.format_version, + room_version, ) with LoggingContext(request="send_rejected"): From 7765bf398996002ee461904915de9d8bc2ea951a Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Thu, 6 Feb 2020 13:25:24 -0500 Subject: [PATCH 1000/1623] Limit the number of events that can be requested when backfilling events (#6864) Limit the maximum number of events requested when backfilling events. --- changelog.d/6864.misc | 1 + synapse/handlers/federation.py | 4 ++++ 2 files changed, 5 insertions(+) create mode 100644 changelog.d/6864.misc diff --git a/changelog.d/6864.misc b/changelog.d/6864.misc new file mode 100644 index 0000000000..d24eb68460 --- /dev/null +++ b/changelog.d/6864.misc @@ -0,0 +1 @@ +Limit the number of events that can be requested by the backfill federation API to 100. diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 10e8b6ea4c..eb20ef4aec 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1788,6 +1788,9 @@ class FederationHandler(BaseHandler): if not in_room: raise AuthError(403, "Host not in room.") + # Synapse asks for 100 events per backfill request. Do not allow more. + limit = min(limit, 100) + events = yield self.store.get_backfill_events(room_id, pdu_list, limit) events = yield filter_events_for_server(self.storage, origin, events) @@ -2168,6 +2171,7 @@ class FederationHandler(BaseHandler): if not in_room: raise AuthError(403, "Host not in room.") + # Only allow up to 20 events to be retrieved per request. limit = min(limit, 20) missing_events = await self.store.get_missing_events( From f4884444c36d92659b9d7a2a90d42324ab786873 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 7 Feb 2020 09:26:57 +0000 Subject: [PATCH 1001/1623] remove unused room_version_to_event_format (#6857) --- changelog.d/6857.misc | 1 + synapse/events/__init__.py | 24 +----------------------- 2 files changed, 2 insertions(+), 23 deletions(-) create mode 100644 changelog.d/6857.misc diff --git a/changelog.d/6857.misc b/changelog.d/6857.misc new file mode 100644 index 0000000000..08aa80bcd9 --- /dev/null +++ b/changelog.d/6857.misc @@ -0,0 +1 @@ +Refactoring work in preparation for changing the event redaction algorithm. diff --git a/synapse/events/__init__.py b/synapse/events/__init__.py index 92f76703b3..89d41d82b6 100644 --- a/synapse/events/__init__.py +++ b/synapse/events/__init__.py @@ -21,8 +21,7 @@ import six from unpaddedbase64 import encode_base64 -from synapse.api.errors import UnsupportedRoomVersionError -from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, EventFormatVersions +from synapse.api.room_versions import EventFormatVersions from synapse.types import JsonDict from synapse.util.caches import intern_dict from synapse.util.frozenutils import freeze @@ -408,27 +407,6 @@ class FrozenEventV3(FrozenEventV2): return self._event_id -def room_version_to_event_format(room_version): - """Converts a room version string to the event format - - Args: - room_version (str) - - Returns: - int - - Raises: - UnsupportedRoomVersionError if the room version is unknown - """ - v = KNOWN_ROOM_VERSIONS.get(room_version) - - if not v: - # this can happen if support is withdrawn for a room version - raise UnsupportedRoomVersionError() - - return v.event_format - - def event_type_from_format_version(format_version): """Returns the python type to use to construct an Event object for the given event format version. From 56ca93ef5941b5dfcda368f373a6bcd80d177acd Mon Sep 17 00:00:00 2001 From: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com> Date: Fri, 7 Feb 2020 11:29:36 +0100 Subject: [PATCH 1002/1623] Admin api to add an email address (#6789) --- changelog.d/6769.feature | 1 + docs/admin_api/user_admin_api.rst | 11 +++++++++ synapse/handlers/admin.py | 2 ++ synapse/handlers/auth.py | 8 +++++++ synapse/rest/admin/users.py | 39 +++++++++++++++++++++++++++++++ tests/rest/admin/test_user.py | 19 +++++++++++++-- 6 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 changelog.d/6769.feature diff --git a/changelog.d/6769.feature b/changelog.d/6769.feature new file mode 100644 index 0000000000..8a60e12907 --- /dev/null +++ b/changelog.d/6769.feature @@ -0,0 +1 @@ +Admin API to add or modify threepids of user accounts. \ No newline at end of file diff --git a/docs/admin_api/user_admin_api.rst b/docs/admin_api/user_admin_api.rst index 0b3d09d694..eb146095de 100644 --- a/docs/admin_api/user_admin_api.rst +++ b/docs/admin_api/user_admin_api.rst @@ -15,6 +15,16 @@ with a body of: { "password": "user_password", "displayname": "User", + "threepids": [ + { + "medium": "email", + "address": "" + }, + { + "medium": "email", + "address": "" + } + ], "avatar_url": "", "admin": false, "deactivated": false @@ -23,6 +33,7 @@ with a body of: including an ``access_token`` of a server admin. The parameter ``displayname`` is optional and defaults to ``user_id``. +The parameter ``threepids`` is optional. The parameter ``avatar_url`` is optional. The parameter ``admin`` is optional and defaults to 'false'. The parameter ``deactivated`` is optional and defaults to 'false'. diff --git a/synapse/handlers/admin.py b/synapse/handlers/admin.py index 9205865231..f3c0aeceb6 100644 --- a/synapse/handlers/admin.py +++ b/synapse/handlers/admin.py @@ -58,8 +58,10 @@ class AdminHandler(BaseHandler): ret = await self.store.get_user_by_id(user.to_string()) if ret: profile = await self.store.get_profileinfo(user.localpart) + threepids = await self.store.user_get_threepids(user.to_string()) ret["displayname"] = profile.display_name ret["avatar_url"] = profile.avatar_url + ret["threepids"] = threepids return ret async def export_user_data(self, user_id, writer): diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index 54a71c49d2..48a88d3c2a 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -816,6 +816,14 @@ class AuthHandler(BaseHandler): @defer.inlineCallbacks def add_threepid(self, user_id, medium, address, validated_at): + # check if medium has a valid value + if medium not in ["email", "msisdn"]: + raise SynapseError( + code=400, + msg=("'%s' is not a valid value for 'medium'" % (medium,)), + errcode=Codes.INVALID_PARAM, + ) + # 'Canonicalise' email addresses down to lower case. # We've now moving towards the homeserver being the entity that # is responsible for validating threepids used for resetting passwords diff --git a/synapse/rest/admin/users.py b/synapse/rest/admin/users.py index f1c4434f5c..e75c5f1370 100644 --- a/synapse/rest/admin/users.py +++ b/synapse/rest/admin/users.py @@ -136,6 +136,8 @@ class UserRestServletV2(RestServlet): self.hs = hs self.auth = hs.get_auth() self.admin_handler = hs.get_handlers().admin_handler + self.store = hs.get_datastore() + self.auth_handler = hs.get_auth_handler() self.profile_handler = hs.get_profile_handler() self.set_password_handler = hs.get_set_password_handler() self.deactivate_account_handler = hs.get_deactivate_account_handler() @@ -163,6 +165,7 @@ class UserRestServletV2(RestServlet): raise SynapseError(400, "This endpoint can only be used with local users") user = await self.admin_handler.get_user(target_user) + user_id = target_user.to_string() if user: # modify user if "displayname" in body: @@ -170,6 +173,29 @@ class UserRestServletV2(RestServlet): target_user, requester, body["displayname"], True ) + if "threepids" in body: + # check for required parameters for each threepid + for threepid in body["threepids"]: + assert_params_in_dict(threepid, ["medium", "address"]) + + # remove old threepids from user + threepids = await self.store.user_get_threepids(user_id) + for threepid in threepids: + try: + await self.auth_handler.delete_threepid( + user_id, threepid["medium"], threepid["address"], None + ) + except Exception: + logger.exception("Failed to remove threepids") + raise SynapseError(500, "Failed to remove threepids") + + # add new threepids to user + current_time = self.hs.get_clock().time_msec() + for threepid in body["threepids"]: + await self.auth_handler.add_threepid( + user_id, threepid["medium"], threepid["address"], current_time + ) + if "avatar_url" in body: await self.profile_handler.set_avatar_url( target_user, requester, body["avatar_url"], True @@ -221,6 +247,7 @@ class UserRestServletV2(RestServlet): admin = body.get("admin", None) user_type = body.get("user_type", None) displayname = body.get("displayname", None) + threepids = body.get("threepids", None) if user_type is not None and user_type not in UserTypes.ALL_USER_TYPES: raise SynapseError(400, "Invalid user type") @@ -232,6 +259,18 @@ class UserRestServletV2(RestServlet): default_display_name=displayname, user_type=user_type, ) + + if "threepids" in body: + # check for required parameters for each threepid + for threepid in body["threepids"]: + assert_params_in_dict(threepid, ["medium", "address"]) + + current_time = self.hs.get_clock().time_msec() + for threepid in body["threepids"]: + await self.auth_handler.add_threepid( + user_id, threepid["medium"], threepid["address"], current_time + ) + if "avatar_url" in body: await self.profile_handler.set_avatar_url( user_id, requester, body["avatar_url"], True diff --git a/tests/rest/admin/test_user.py b/tests/rest/admin/test_user.py index 8f09f51c61..3b5169b38d 100644 --- a/tests/rest/admin/test_user.py +++ b/tests/rest/admin/test_user.py @@ -407,7 +407,13 @@ class UserRestTestCase(unittest.HomeserverTestCase): """ self.hs.config.registration_shared_secret = None - body = json.dumps({"password": "abc123", "admin": True}) + body = json.dumps( + { + "password": "abc123", + "admin": True, + "threepids": [{"medium": "email", "address": "bob@bob.bob"}], + } + ) # Create user request, channel = self.make_request( @@ -421,6 +427,8 @@ class UserRestTestCase(unittest.HomeserverTestCase): self.assertEqual(201, int(channel.result["code"]), msg=channel.result["body"]) self.assertEqual("@bob:test", channel.json_body["name"]) self.assertEqual("bob", channel.json_body["displayname"]) + self.assertEqual("email", channel.json_body["threepids"][0]["medium"]) + self.assertEqual("bob@bob.bob", channel.json_body["threepids"][0]["address"]) # Get user request, channel = self.make_request( @@ -449,7 +457,13 @@ class UserRestTestCase(unittest.HomeserverTestCase): self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) # Modify user - body = json.dumps({"displayname": "foobar", "deactivated": True}) + body = json.dumps( + { + "displayname": "foobar", + "deactivated": True, + "threepids": [{"medium": "email", "address": "bob2@bob.bob"}], + } + ) request, channel = self.make_request( "PUT", @@ -463,6 +477,7 @@ class UserRestTestCase(unittest.HomeserverTestCase): self.assertEqual("@bob:test", channel.json_body["name"]) self.assertEqual("foobar", channel.json_body["displayname"]) self.assertEqual(True, channel.json_body["deactivated"]) + # the user is deactivated, the threepid will be deleted # Get user request, channel = self.make_request( From de2d267375069c2d22bceb0d6ef9c6f5a77380e3 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 7 Feb 2020 11:14:19 +0000 Subject: [PATCH 1003/1623] Allow moving group read APIs to workers (#6866) --- changelog.d/6866.feature | 1 + docs/workers.md | 8 + synapse/app/client_reader.py | 3 + synapse/app/federation_reader.py | 2 + synapse/groups/groups_server.py | 379 ++++----- synapse/handlers/groups_local.py | 270 +++---- synapse/replication/slave/storage/groups.py | 14 +- synapse/server.py | 14 +- .../storage/data_stores/main/group_server.py | 720 +++++++++--------- 9 files changed, 723 insertions(+), 688 deletions(-) create mode 100644 changelog.d/6866.feature diff --git a/changelog.d/6866.feature b/changelog.d/6866.feature new file mode 100644 index 0000000000..256feab6ff --- /dev/null +++ b/changelog.d/6866.feature @@ -0,0 +1 @@ +Add ability to run some group APIs on workers. diff --git a/docs/workers.md b/docs/workers.md index 09a9d8a7b8..82442d6a0a 100644 --- a/docs/workers.md +++ b/docs/workers.md @@ -177,8 +177,13 @@ endpoints matching the following regular expressions: ^/_matrix/federation/v1/event_auth/ ^/_matrix/federation/v1/exchange_third_party_invite/ ^/_matrix/federation/v1/send/ + ^/_matrix/federation/v1/get_groups_publicised$ ^/_matrix/key/v2/query +Additionally, the following REST endpoints can be handled for GET requests: + + ^/_matrix/federation/v1/groups/ + The above endpoints should all be routed to the federation_reader worker by the reverse-proxy configuration. @@ -254,10 +259,13 @@ following regular expressions: ^/_matrix/client/(api/v1|r0|unstable)/keys/changes$ ^/_matrix/client/versions$ ^/_matrix/client/(api/v1|r0|unstable)/voip/turnServer$ + ^/_matrix/client/(api/v1|r0|unstable)/joined_groups$ + ^/_matrix/client/(api/v1|r0|unstable)/get_groups_publicised$ Additionally, the following REST endpoints can be handled for GET requests: ^/_matrix/client/(api/v1|r0|unstable)/pushrules/.*$ + ^/_matrix/client/(api/v1|r0|unstable)/groups/.*$ Additionally, the following REST endpoints can be handled, but all requests must be routed to the same instance: diff --git a/synapse/app/client_reader.py b/synapse/app/client_reader.py index ca96da6a4a..7fa91a3b11 100644 --- a/synapse/app/client_reader.py +++ b/synapse/app/client_reader.py @@ -57,6 +57,7 @@ from synapse.rest.client.v1.room import ( RoomStateRestServlet, ) from synapse.rest.client.v1.voip import VoipRestServlet +from synapse.rest.client.v2_alpha import groups from synapse.rest.client.v2_alpha.account import ThreepidRestServlet from synapse.rest.client.v2_alpha.keys import KeyChangesServlet, KeyQueryServlet from synapse.rest.client.v2_alpha.register import RegisterRestServlet @@ -124,6 +125,8 @@ class ClientReaderServer(HomeServer): PushRuleRestServlet(self).register(resource) VersionsRestServlet(self).register(resource) + groups.register_servlets(self, resource) + resources.update({"/_matrix/client": resource}) root_resource = create_resource_tree(resources, NoResource()) diff --git a/synapse/app/federation_reader.py b/synapse/app/federation_reader.py index 1f1cea1416..5e17ef1396 100644 --- a/synapse/app/federation_reader.py +++ b/synapse/app/federation_reader.py @@ -35,6 +35,7 @@ from synapse.replication.slave.storage.account_data import SlavedAccountDataStor from synapse.replication.slave.storage.appservice import SlavedApplicationServiceStore from synapse.replication.slave.storage.directory import DirectoryStore from synapse.replication.slave.storage.events import SlavedEventStore +from synapse.replication.slave.storage.groups import SlavedGroupServerStore from synapse.replication.slave.storage.keys import SlavedKeyStore from synapse.replication.slave.storage.profile import SlavedProfileStore from synapse.replication.slave.storage.push_rule import SlavedPushRuleStore @@ -66,6 +67,7 @@ class FederationReaderSlavedStore( SlavedEventStore, SlavedKeyStore, SlavedRegistrationStore, + SlavedGroupServerStore, RoomStore, DirectoryStore, SlavedTransactionStore, diff --git a/synapse/groups/groups_server.py b/synapse/groups/groups_server.py index 0ec9be3cb5..c106abae21 100644 --- a/synapse/groups/groups_server.py +++ b/synapse/groups/groups_server.py @@ -36,7 +36,7 @@ logger = logging.getLogger(__name__) # TODO: Flairs -class GroupsServerHandler(object): +class GroupsServerWorkerHandler(object): def __init__(self, hs): self.hs = hs self.store = hs.get_datastore() @@ -51,9 +51,6 @@ class GroupsServerHandler(object): self.transport_client = hs.get_federation_transport_client() self.profile_handler = hs.get_profile_handler() - # Ensure attestations get renewed - hs.get_groups_attestation_renewer() - @defer.inlineCallbacks def check_group_is_ours( self, group_id, requester_user_id, and_exists=False, and_is_admin=None @@ -167,68 +164,6 @@ class GroupsServerHandler(object): "user": membership_info, } - @defer.inlineCallbacks - def update_group_summary_room( - self, group_id, requester_user_id, room_id, category_id, content - ): - """Add/update a room to the group summary - """ - yield self.check_group_is_ours( - group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id - ) - - RoomID.from_string(room_id) # Ensure valid room id - - order = content.get("order", None) - - is_public = _parse_visibility_from_contents(content) - - yield self.store.add_room_to_summary( - group_id=group_id, - room_id=room_id, - category_id=category_id, - order=order, - is_public=is_public, - ) - - return {} - - @defer.inlineCallbacks - def delete_group_summary_room( - self, group_id, requester_user_id, room_id, category_id - ): - """Remove a room from the summary - """ - yield self.check_group_is_ours( - group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id - ) - - yield self.store.remove_room_from_summary( - group_id=group_id, room_id=room_id, category_id=category_id - ) - - return {} - - @defer.inlineCallbacks - def set_group_join_policy(self, group_id, requester_user_id, content): - """Sets the group join policy. - - Currently supported policies are: - - "invite": an invite must be received and accepted in order to join. - - "open": anyone can join. - """ - yield self.check_group_is_ours( - group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id - ) - - join_policy = _parse_join_policy_from_contents(content) - if join_policy is None: - raise SynapseError(400, "No value specified for 'm.join_policy'") - - yield self.store.set_group_join_policy(group_id, join_policy=join_policy) - - return {} - @defer.inlineCallbacks def get_group_categories(self, group_id, requester_user_id): """Get all categories in a group (as seen by user) @@ -248,42 +183,10 @@ class GroupsServerHandler(object): group_id=group_id, category_id=category_id ) + logger.info("group %s", res) + return res - @defer.inlineCallbacks - def update_group_category(self, group_id, requester_user_id, category_id, content): - """Add/Update a group category - """ - yield self.check_group_is_ours( - group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id - ) - - is_public = _parse_visibility_from_contents(content) - profile = content.get("profile") - - yield self.store.upsert_group_category( - group_id=group_id, - category_id=category_id, - is_public=is_public, - profile=profile, - ) - - return {} - - @defer.inlineCallbacks - def delete_group_category(self, group_id, requester_user_id, category_id): - """Delete a group category - """ - yield self.check_group_is_ours( - group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id - ) - - yield self.store.remove_group_category( - group_id=group_id, category_id=category_id - ) - - return {} - @defer.inlineCallbacks def get_group_roles(self, group_id, requester_user_id): """Get all roles in a group (as seen by user) @@ -302,74 +205,6 @@ class GroupsServerHandler(object): res = yield self.store.get_group_role(group_id=group_id, role_id=role_id) return res - @defer.inlineCallbacks - def update_group_role(self, group_id, requester_user_id, role_id, content): - """Add/update a role in a group - """ - yield self.check_group_is_ours( - group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id - ) - - is_public = _parse_visibility_from_contents(content) - - profile = content.get("profile") - - yield self.store.upsert_group_role( - group_id=group_id, role_id=role_id, is_public=is_public, profile=profile - ) - - return {} - - @defer.inlineCallbacks - def delete_group_role(self, group_id, requester_user_id, role_id): - """Remove role from group - """ - yield self.check_group_is_ours( - group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id - ) - - yield self.store.remove_group_role(group_id=group_id, role_id=role_id) - - return {} - - @defer.inlineCallbacks - def update_group_summary_user( - self, group_id, requester_user_id, user_id, role_id, content - ): - """Add/update a users entry in the group summary - """ - yield self.check_group_is_ours( - group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id - ) - - order = content.get("order", None) - - is_public = _parse_visibility_from_contents(content) - - yield self.store.add_user_to_summary( - group_id=group_id, - user_id=user_id, - role_id=role_id, - order=order, - is_public=is_public, - ) - - return {} - - @defer.inlineCallbacks - def delete_group_summary_user(self, group_id, requester_user_id, user_id, role_id): - """Remove a user from the group summary - """ - yield self.check_group_is_ours( - group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id - ) - - yield self.store.remove_user_from_summary( - group_id=group_id, user_id=user_id, role_id=role_id - ) - - return {} - @defer.inlineCallbacks def get_group_profile(self, group_id, requester_user_id): """Get the group profile as seen by requester_user_id @@ -394,24 +229,6 @@ class GroupsServerHandler(object): else: raise SynapseError(404, "Unknown group") - @defer.inlineCallbacks - def update_group_profile(self, group_id, requester_user_id, content): - """Update the group profile - """ - yield self.check_group_is_ours( - group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id - ) - - profile = {} - for keyname in ("name", "avatar_url", "short_description", "long_description"): - if keyname in content: - value = content[keyname] - if not isinstance(value, string_types): - raise SynapseError(400, "%r value is not a string" % (keyname,)) - profile[keyname] = value - - yield self.store.update_group_profile(group_id, profile) - @defer.inlineCallbacks def get_users_in_group(self, group_id, requester_user_id): """Get the users in group as seen by requester_user_id. @@ -530,6 +347,196 @@ class GroupsServerHandler(object): return {"chunk": chunk, "total_room_count_estimate": len(room_results)} + +class GroupsServerHandler(GroupsServerWorkerHandler): + def __init__(self, hs): + super(GroupsServerHandler, self).__init__(hs) + + # Ensure attestations get renewed + hs.get_groups_attestation_renewer() + + @defer.inlineCallbacks + def update_group_summary_room( + self, group_id, requester_user_id, room_id, category_id, content + ): + """Add/update a room to the group summary + """ + yield self.check_group_is_ours( + group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id + ) + + RoomID.from_string(room_id) # Ensure valid room id + + order = content.get("order", None) + + is_public = _parse_visibility_from_contents(content) + + yield self.store.add_room_to_summary( + group_id=group_id, + room_id=room_id, + category_id=category_id, + order=order, + is_public=is_public, + ) + + return {} + + @defer.inlineCallbacks + def delete_group_summary_room( + self, group_id, requester_user_id, room_id, category_id + ): + """Remove a room from the summary + """ + yield self.check_group_is_ours( + group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id + ) + + yield self.store.remove_room_from_summary( + group_id=group_id, room_id=room_id, category_id=category_id + ) + + return {} + + @defer.inlineCallbacks + def set_group_join_policy(self, group_id, requester_user_id, content): + """Sets the group join policy. + + Currently supported policies are: + - "invite": an invite must be received and accepted in order to join. + - "open": anyone can join. + """ + yield self.check_group_is_ours( + group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id + ) + + join_policy = _parse_join_policy_from_contents(content) + if join_policy is None: + raise SynapseError(400, "No value specified for 'm.join_policy'") + + yield self.store.set_group_join_policy(group_id, join_policy=join_policy) + + return {} + + @defer.inlineCallbacks + def update_group_category(self, group_id, requester_user_id, category_id, content): + """Add/Update a group category + """ + yield self.check_group_is_ours( + group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id + ) + + is_public = _parse_visibility_from_contents(content) + profile = content.get("profile") + + yield self.store.upsert_group_category( + group_id=group_id, + category_id=category_id, + is_public=is_public, + profile=profile, + ) + + return {} + + @defer.inlineCallbacks + def delete_group_category(self, group_id, requester_user_id, category_id): + """Delete a group category + """ + yield self.check_group_is_ours( + group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id + ) + + yield self.store.remove_group_category( + group_id=group_id, category_id=category_id + ) + + return {} + + @defer.inlineCallbacks + def update_group_role(self, group_id, requester_user_id, role_id, content): + """Add/update a role in a group + """ + yield self.check_group_is_ours( + group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id + ) + + is_public = _parse_visibility_from_contents(content) + + profile = content.get("profile") + + yield self.store.upsert_group_role( + group_id=group_id, role_id=role_id, is_public=is_public, profile=profile + ) + + return {} + + @defer.inlineCallbacks + def delete_group_role(self, group_id, requester_user_id, role_id): + """Remove role from group + """ + yield self.check_group_is_ours( + group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id + ) + + yield self.store.remove_group_role(group_id=group_id, role_id=role_id) + + return {} + + @defer.inlineCallbacks + def update_group_summary_user( + self, group_id, requester_user_id, user_id, role_id, content + ): + """Add/update a users entry in the group summary + """ + yield self.check_group_is_ours( + group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id + ) + + order = content.get("order", None) + + is_public = _parse_visibility_from_contents(content) + + yield self.store.add_user_to_summary( + group_id=group_id, + user_id=user_id, + role_id=role_id, + order=order, + is_public=is_public, + ) + + return {} + + @defer.inlineCallbacks + def delete_group_summary_user(self, group_id, requester_user_id, user_id, role_id): + """Remove a user from the group summary + """ + yield self.check_group_is_ours( + group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id + ) + + yield self.store.remove_user_from_summary( + group_id=group_id, user_id=user_id, role_id=role_id + ) + + return {} + + @defer.inlineCallbacks + def update_group_profile(self, group_id, requester_user_id, content): + """Update the group profile + """ + yield self.check_group_is_ours( + group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id + ) + + profile = {} + for keyname in ("name", "avatar_url", "short_description", "long_description"): + if keyname in content: + value = content[keyname] + if not isinstance(value, string_types): + raise SynapseError(400, "%r value is not a string" % (keyname,)) + profile[keyname] = value + + yield self.store.update_group_profile(group_id, profile) + @defer.inlineCallbacks def add_room_to_group(self, group_id, requester_user_id, room_id, content): """Add room to group diff --git a/synapse/handlers/groups_local.py b/synapse/handlers/groups_local.py index 319565510f..ad22415782 100644 --- a/synapse/handlers/groups_local.py +++ b/synapse/handlers/groups_local.py @@ -63,7 +63,7 @@ def _create_rerouter(func_name): return f -class GroupsLocalHandler(object): +class GroupsLocalWorkerHandler(object): def __init__(self, hs): self.hs = hs self.store = hs.get_datastore() @@ -81,40 +81,17 @@ class GroupsLocalHandler(object): self.profile_handler = hs.get_profile_handler() - # Ensure attestations get renewed - hs.get_groups_attestation_renewer() - # The following functions merely route the query to the local groups server # or federation depending on if the group is local or remote get_group_profile = _create_rerouter("get_group_profile") - update_group_profile = _create_rerouter("update_group_profile") get_rooms_in_group = _create_rerouter("get_rooms_in_group") - get_invited_users_in_group = _create_rerouter("get_invited_users_in_group") - - add_room_to_group = _create_rerouter("add_room_to_group") - update_room_in_group = _create_rerouter("update_room_in_group") - remove_room_from_group = _create_rerouter("remove_room_from_group") - - update_group_summary_room = _create_rerouter("update_group_summary_room") - delete_group_summary_room = _create_rerouter("delete_group_summary_room") - - update_group_category = _create_rerouter("update_group_category") - delete_group_category = _create_rerouter("delete_group_category") get_group_category = _create_rerouter("get_group_category") get_group_categories = _create_rerouter("get_group_categories") - - update_group_summary_user = _create_rerouter("update_group_summary_user") - delete_group_summary_user = _create_rerouter("delete_group_summary_user") - - update_group_role = _create_rerouter("update_group_role") - delete_group_role = _create_rerouter("delete_group_role") get_group_role = _create_rerouter("get_group_role") get_group_roles = _create_rerouter("get_group_roles") - set_group_join_policy = _create_rerouter("set_group_join_policy") - @defer.inlineCallbacks def get_group_summary(self, group_id, requester_user_id): """Get the group summary for a group. @@ -169,6 +146,144 @@ class GroupsLocalHandler(object): return res + @defer.inlineCallbacks + def get_users_in_group(self, group_id, requester_user_id): + """Get users in a group + """ + if self.is_mine_id(group_id): + res = yield self.groups_server_handler.get_users_in_group( + group_id, requester_user_id + ) + return res + + group_server_name = get_domain_from_id(group_id) + + try: + res = yield self.transport_client.get_users_in_group( + get_domain_from_id(group_id), group_id, requester_user_id + ) + except HttpResponseException as e: + raise e.to_synapse_error() + except RequestSendFailed: + raise SynapseError(502, "Failed to contact group server") + + chunk = res["chunk"] + valid_entries = [] + for entry in chunk: + g_user_id = entry["user_id"] + attestation = entry.pop("attestation", {}) + try: + if get_domain_from_id(g_user_id) != group_server_name: + yield self.attestations.verify_attestation( + attestation, + group_id=group_id, + user_id=g_user_id, + server_name=get_domain_from_id(g_user_id), + ) + valid_entries.append(entry) + except Exception as e: + logger.info("Failed to verify user is in group: %s", e) + + res["chunk"] = valid_entries + + return res + + @defer.inlineCallbacks + def get_joined_groups(self, user_id): + group_ids = yield self.store.get_joined_groups(user_id) + return {"groups": group_ids} + + @defer.inlineCallbacks + def get_publicised_groups_for_user(self, user_id): + if self.hs.is_mine_id(user_id): + result = yield self.store.get_publicised_groups_for_user(user_id) + + # Check AS associated groups for this user - this depends on the + # RegExps in the AS registration file (under `users`) + for app_service in self.store.get_app_services(): + result.extend(app_service.get_groups_for_user(user_id)) + + return {"groups": result} + else: + try: + bulk_result = yield self.transport_client.bulk_get_publicised_groups( + get_domain_from_id(user_id), [user_id] + ) + except HttpResponseException as e: + raise e.to_synapse_error() + except RequestSendFailed: + raise SynapseError(502, "Failed to contact group server") + + result = bulk_result.get("users", {}).get(user_id) + # TODO: Verify attestations + return {"groups": result} + + @defer.inlineCallbacks + def bulk_get_publicised_groups(self, user_ids, proxy=True): + destinations = {} + local_users = set() + + for user_id in user_ids: + if self.hs.is_mine_id(user_id): + local_users.add(user_id) + else: + destinations.setdefault(get_domain_from_id(user_id), set()).add(user_id) + + if not proxy and destinations: + raise SynapseError(400, "Some user_ids are not local") + + results = {} + failed_results = [] + for destination, dest_user_ids in iteritems(destinations): + try: + r = yield self.transport_client.bulk_get_publicised_groups( + destination, list(dest_user_ids) + ) + results.update(r["users"]) + except Exception: + failed_results.extend(dest_user_ids) + + for uid in local_users: + results[uid] = yield self.store.get_publicised_groups_for_user(uid) + + # Check AS associated groups for this user - this depends on the + # RegExps in the AS registration file (under `users`) + for app_service in self.store.get_app_services(): + results[uid].extend(app_service.get_groups_for_user(uid)) + + return {"users": results} + + +class GroupsLocalHandler(GroupsLocalWorkerHandler): + def __init__(self, hs): + super(GroupsLocalHandler, self).__init__(hs) + + # Ensure attestations get renewed + hs.get_groups_attestation_renewer() + + # The following functions merely route the query to the local groups server + # or federation depending on if the group is local or remote + + update_group_profile = _create_rerouter("update_group_profile") + + add_room_to_group = _create_rerouter("add_room_to_group") + update_room_in_group = _create_rerouter("update_room_in_group") + remove_room_from_group = _create_rerouter("remove_room_from_group") + + update_group_summary_room = _create_rerouter("update_group_summary_room") + delete_group_summary_room = _create_rerouter("delete_group_summary_room") + + update_group_category = _create_rerouter("update_group_category") + delete_group_category = _create_rerouter("delete_group_category") + + update_group_summary_user = _create_rerouter("update_group_summary_user") + delete_group_summary_user = _create_rerouter("delete_group_summary_user") + + update_group_role = _create_rerouter("update_group_role") + delete_group_role = _create_rerouter("delete_group_role") + + set_group_join_policy = _create_rerouter("set_group_join_policy") + @defer.inlineCallbacks def create_group(self, group_id, user_id, content): """Create a group @@ -219,48 +334,6 @@ class GroupsLocalHandler(object): return res - @defer.inlineCallbacks - def get_users_in_group(self, group_id, requester_user_id): - """Get users in a group - """ - if self.is_mine_id(group_id): - res = yield self.groups_server_handler.get_users_in_group( - group_id, requester_user_id - ) - return res - - group_server_name = get_domain_from_id(group_id) - - try: - res = yield self.transport_client.get_users_in_group( - get_domain_from_id(group_id), group_id, requester_user_id - ) - except HttpResponseException as e: - raise e.to_synapse_error() - except RequestSendFailed: - raise SynapseError(502, "Failed to contact group server") - - chunk = res["chunk"] - valid_entries = [] - for entry in chunk: - g_user_id = entry["user_id"] - attestation = entry.pop("attestation", {}) - try: - if get_domain_from_id(g_user_id) != group_server_name: - yield self.attestations.verify_attestation( - attestation, - group_id=group_id, - user_id=g_user_id, - server_name=get_domain_from_id(g_user_id), - ) - valid_entries.append(entry) - except Exception as e: - logger.info("Failed to verify user is in group: %s", e) - - res["chunk"] = valid_entries - - return res - @defer.inlineCallbacks def join_group(self, group_id, user_id, content): """Request to join a group @@ -452,68 +525,3 @@ class GroupsLocalHandler(object): group_id, user_id, membership="leave" ) self.notifier.on_new_event("groups_key", token, users=[user_id]) - - @defer.inlineCallbacks - def get_joined_groups(self, user_id): - group_ids = yield self.store.get_joined_groups(user_id) - return {"groups": group_ids} - - @defer.inlineCallbacks - def get_publicised_groups_for_user(self, user_id): - if self.hs.is_mine_id(user_id): - result = yield self.store.get_publicised_groups_for_user(user_id) - - # Check AS associated groups for this user - this depends on the - # RegExps in the AS registration file (under `users`) - for app_service in self.store.get_app_services(): - result.extend(app_service.get_groups_for_user(user_id)) - - return {"groups": result} - else: - try: - bulk_result = yield self.transport_client.bulk_get_publicised_groups( - get_domain_from_id(user_id), [user_id] - ) - except HttpResponseException as e: - raise e.to_synapse_error() - except RequestSendFailed: - raise SynapseError(502, "Failed to contact group server") - - result = bulk_result.get("users", {}).get(user_id) - # TODO: Verify attestations - return {"groups": result} - - @defer.inlineCallbacks - def bulk_get_publicised_groups(self, user_ids, proxy=True): - destinations = {} - local_users = set() - - for user_id in user_ids: - if self.hs.is_mine_id(user_id): - local_users.add(user_id) - else: - destinations.setdefault(get_domain_from_id(user_id), set()).add(user_id) - - if not proxy and destinations: - raise SynapseError(400, "Some user_ids are not local") - - results = {} - failed_results = [] - for destination, dest_user_ids in iteritems(destinations): - try: - r = yield self.transport_client.bulk_get_publicised_groups( - destination, list(dest_user_ids) - ) - results.update(r["users"]) - except Exception: - failed_results.extend(dest_user_ids) - - for uid in local_users: - results[uid] = yield self.store.get_publicised_groups_for_user(uid) - - # Check AS associated groups for this user - this depends on the - # RegExps in the AS registration file (under `users`) - for app_service in self.store.get_app_services(): - results[uid].extend(app_service.get_groups_for_user(uid)) - - return {"users": results} diff --git a/synapse/replication/slave/storage/groups.py b/synapse/replication/slave/storage/groups.py index 69a4ae42f9..2d4fd08cf5 100644 --- a/synapse/replication/slave/storage/groups.py +++ b/synapse/replication/slave/storage/groups.py @@ -13,15 +13,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -from synapse.storage import DataStore +from synapse.replication.slave.storage._base import BaseSlavedStore +from synapse.replication.slave.storage._slaved_id_tracker import SlavedIdTracker +from synapse.storage.data_stores.main.group_server import GroupServerWorkerStore from synapse.storage.database import Database from synapse.util.caches.stream_change_cache import StreamChangeCache -from ._base import BaseSlavedStore, __func__ -from ._slaved_id_tracker import SlavedIdTracker - -class SlavedGroupServerStore(BaseSlavedStore): +class SlavedGroupServerStore(GroupServerWorkerStore, BaseSlavedStore): def __init__(self, database: Database, db_conn, hs): super(SlavedGroupServerStore, self).__init__(database, db_conn, hs) @@ -35,9 +34,8 @@ class SlavedGroupServerStore(BaseSlavedStore): self._group_updates_id_gen.get_current_token(), ) - get_groups_changes_for_user = __func__(DataStore.get_groups_changes_for_user) - get_group_stream_token = __func__(DataStore.get_group_stream_token) - get_all_groups_for_user = __func__(DataStore.get_all_groups_for_user) + def get_group_stream_token(self): + return self._group_updates_id_gen.get_current_token() def stream_positions(self): result = super(SlavedGroupServerStore, self).stream_positions() diff --git a/synapse/server.py b/synapse/server.py index 7926867b77..fd2f69e928 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -50,7 +50,7 @@ from synapse.federation.send_queue import FederationRemoteSendQueue from synapse.federation.sender import FederationSender from synapse.federation.transport.client import TransportLayerClient from synapse.groups.attestations import GroupAttestationSigning, GroupAttestionRenewer -from synapse.groups.groups_server import GroupsServerHandler +from synapse.groups.groups_server import GroupsServerHandler, GroupsServerWorkerHandler from synapse.handlers import Handlers from synapse.handlers.account_validity import AccountValidityHandler from synapse.handlers.acme import AcmeHandler @@ -62,7 +62,7 @@ from synapse.handlers.devicemessage import DeviceMessageHandler from synapse.handlers.e2e_keys import E2eKeysHandler from synapse.handlers.e2e_room_keys import E2eRoomKeysHandler from synapse.handlers.events import EventHandler, EventStreamHandler -from synapse.handlers.groups_local import GroupsLocalHandler +from synapse.handlers.groups_local import GroupsLocalHandler, GroupsLocalWorkerHandler from synapse.handlers.initial_sync import InitialSyncHandler from synapse.handlers.message import EventCreationHandler, MessageHandler from synapse.handlers.pagination import PaginationHandler @@ -460,10 +460,16 @@ class HomeServer(object): return UserDirectoryHandler(self) def build_groups_local_handler(self): - return GroupsLocalHandler(self) + if self.config.worker_app: + return GroupsLocalWorkerHandler(self) + else: + return GroupsLocalHandler(self) def build_groups_server_handler(self): - return GroupsServerHandler(self) + if self.config.worker_app: + return GroupsServerWorkerHandler(self) + else: + return GroupsServerHandler(self) def build_groups_attestation_signing(self): return GroupAttestationSigning(self) diff --git a/synapse/storage/data_stores/main/group_server.py b/synapse/storage/data_stores/main/group_server.py index 6acd45e9f3..0963e6c250 100644 --- a/synapse/storage/data_stores/main/group_server.py +++ b/synapse/storage/data_stores/main/group_server.py @@ -27,21 +27,7 @@ _DEFAULT_CATEGORY_ID = "" _DEFAULT_ROLE_ID = "" -class GroupServerStore(SQLBaseStore): - def set_group_join_policy(self, group_id, join_policy): - """Set the join policy of a group. - - join_policy can be one of: - * "invite" - * "open" - """ - return self.db.simple_update_one( - table="groups", - keyvalues={"group_id": group_id}, - updatevalues={"join_policy": join_policy}, - desc="set_group_join_policy", - ) - +class GroupServerWorkerStore(SQLBaseStore): def get_group(self, group_id): return self.db.simple_select_one( table="groups", @@ -157,6 +143,366 @@ class GroupServerStore(SQLBaseStore): "get_rooms_for_summary", _get_rooms_for_summary_txn ) + @defer.inlineCallbacks + def get_group_categories(self, group_id): + rows = yield self.db.simple_select_list( + table="group_room_categories", + keyvalues={"group_id": group_id}, + retcols=("category_id", "is_public", "profile"), + desc="get_group_categories", + ) + + return { + row["category_id"]: { + "is_public": row["is_public"], + "profile": json.loads(row["profile"]), + } + for row in rows + } + + @defer.inlineCallbacks + def get_group_category(self, group_id, category_id): + category = yield self.db.simple_select_one( + table="group_room_categories", + keyvalues={"group_id": group_id, "category_id": category_id}, + retcols=("is_public", "profile"), + desc="get_group_category", + ) + + category["profile"] = json.loads(category["profile"]) + + return category + + @defer.inlineCallbacks + def get_group_roles(self, group_id): + rows = yield self.db.simple_select_list( + table="group_roles", + keyvalues={"group_id": group_id}, + retcols=("role_id", "is_public", "profile"), + desc="get_group_roles", + ) + + return { + row["role_id"]: { + "is_public": row["is_public"], + "profile": json.loads(row["profile"]), + } + for row in rows + } + + @defer.inlineCallbacks + def get_group_role(self, group_id, role_id): + role = yield self.db.simple_select_one( + table="group_roles", + keyvalues={"group_id": group_id, "role_id": role_id}, + retcols=("is_public", "profile"), + desc="get_group_role", + ) + + role["profile"] = json.loads(role["profile"]) + + return role + + def get_local_groups_for_room(self, room_id): + """Get all of the local group that contain a given room + Args: + room_id (str): The ID of a room + Returns: + Deferred[list[str]]: A twisted.Deferred containing a list of group ids + containing this room + """ + return self.db.simple_select_onecol( + table="group_rooms", + keyvalues={"room_id": room_id}, + retcol="group_id", + desc="get_local_groups_for_room", + ) + + def get_users_for_summary_by_role(self, group_id, include_private=False): + """Get the users and roles that should be included in a summary request + + Returns ([users], [roles]) + """ + + def _get_users_for_summary_txn(txn): + keyvalues = {"group_id": group_id} + if not include_private: + keyvalues["is_public"] = True + + sql = """ + SELECT user_id, is_public, role_id, user_order + FROM group_summary_users + WHERE group_id = ? + """ + + if not include_private: + sql += " AND is_public = ?" + txn.execute(sql, (group_id, True)) + else: + txn.execute(sql, (group_id,)) + + users = [ + { + "user_id": row[0], + "is_public": row[1], + "role_id": row[2] if row[2] != _DEFAULT_ROLE_ID else None, + "order": row[3], + } + for row in txn + ] + + sql = """ + SELECT role_id, is_public, profile, role_order + FROM group_summary_roles + INNER JOIN group_roles USING (group_id, role_id) + WHERE group_id = ? + """ + + if not include_private: + sql += " AND is_public = ?" + txn.execute(sql, (group_id, True)) + else: + txn.execute(sql, (group_id,)) + + roles = { + row[0]: { + "is_public": row[1], + "profile": json.loads(row[2]), + "order": row[3], + } + for row in txn + } + + return users, roles + + return self.db.runInteraction( + "get_users_for_summary_by_role", _get_users_for_summary_txn + ) + + def is_user_in_group(self, user_id, group_id): + return self.db.simple_select_one_onecol( + table="group_users", + keyvalues={"group_id": group_id, "user_id": user_id}, + retcol="user_id", + allow_none=True, + desc="is_user_in_group", + ).addCallback(lambda r: bool(r)) + + def is_user_admin_in_group(self, group_id, user_id): + return self.db.simple_select_one_onecol( + table="group_users", + keyvalues={"group_id": group_id, "user_id": user_id}, + retcol="is_admin", + allow_none=True, + desc="is_user_admin_in_group", + ) + + def is_user_invited_to_local_group(self, group_id, user_id): + """Has the group server invited a user? + """ + return self.db.simple_select_one_onecol( + table="group_invites", + keyvalues={"group_id": group_id, "user_id": user_id}, + retcol="user_id", + desc="is_user_invited_to_local_group", + allow_none=True, + ) + + def get_users_membership_info_in_group(self, group_id, user_id): + """Get a dict describing the membership of a user in a group. + + Example if joined: + + { + "membership": "join", + "is_public": True, + "is_privileged": False, + } + + Returns an empty dict if the user is not join/invite/etc + """ + + def _get_users_membership_in_group_txn(txn): + row = self.db.simple_select_one_txn( + txn, + table="group_users", + keyvalues={"group_id": group_id, "user_id": user_id}, + retcols=("is_admin", "is_public"), + allow_none=True, + ) + + if row: + return { + "membership": "join", + "is_public": row["is_public"], + "is_privileged": row["is_admin"], + } + + row = self.db.simple_select_one_onecol_txn( + txn, + table="group_invites", + keyvalues={"group_id": group_id, "user_id": user_id}, + retcol="user_id", + allow_none=True, + ) + + if row: + return {"membership": "invite"} + + return {} + + return self.db.runInteraction( + "get_users_membership_info_in_group", _get_users_membership_in_group_txn + ) + + def get_publicised_groups_for_user(self, user_id): + """Get all groups a user is publicising + """ + return self.db.simple_select_onecol( + table="local_group_membership", + keyvalues={"user_id": user_id, "membership": "join", "is_publicised": True}, + retcol="group_id", + desc="get_publicised_groups_for_user", + ) + + def get_attestations_need_renewals(self, valid_until_ms): + """Get all attestations that need to be renewed until givent time + """ + + def _get_attestations_need_renewals_txn(txn): + sql = """ + SELECT group_id, user_id FROM group_attestations_renewals + WHERE valid_until_ms <= ? + """ + txn.execute(sql, (valid_until_ms,)) + return self.db.cursor_to_dict(txn) + + return self.db.runInteraction( + "get_attestations_need_renewals", _get_attestations_need_renewals_txn + ) + + @defer.inlineCallbacks + def get_remote_attestation(self, group_id, user_id): + """Get the attestation that proves the remote agrees that the user is + in the group. + """ + row = yield self.db.simple_select_one( + table="group_attestations_remote", + keyvalues={"group_id": group_id, "user_id": user_id}, + retcols=("valid_until_ms", "attestation_json"), + desc="get_remote_attestation", + allow_none=True, + ) + + now = int(self._clock.time_msec()) + if row and now < row["valid_until_ms"]: + return json.loads(row["attestation_json"]) + + return None + + def get_joined_groups(self, user_id): + return self.db.simple_select_onecol( + table="local_group_membership", + keyvalues={"user_id": user_id, "membership": "join"}, + retcol="group_id", + desc="get_joined_groups", + ) + + def get_all_groups_for_user(self, user_id, now_token): + def _get_all_groups_for_user_txn(txn): + sql = """ + SELECT group_id, type, membership, u.content + FROM local_group_updates AS u + INNER JOIN local_group_membership USING (group_id, user_id) + WHERE user_id = ? AND membership != 'leave' + AND stream_id <= ? + """ + txn.execute(sql, (user_id, now_token)) + return [ + { + "group_id": row[0], + "type": row[1], + "membership": row[2], + "content": json.loads(row[3]), + } + for row in txn + ] + + return self.db.runInteraction( + "get_all_groups_for_user", _get_all_groups_for_user_txn + ) + + def get_groups_changes_for_user(self, user_id, from_token, to_token): + from_token = int(from_token) + has_changed = self._group_updates_stream_cache.has_entity_changed( + user_id, from_token + ) + if not has_changed: + return defer.succeed([]) + + def _get_groups_changes_for_user_txn(txn): + sql = """ + SELECT group_id, membership, type, u.content + FROM local_group_updates AS u + INNER JOIN local_group_membership USING (group_id, user_id) + WHERE user_id = ? AND ? < stream_id AND stream_id <= ? + """ + txn.execute(sql, (user_id, from_token, to_token)) + return [ + { + "group_id": group_id, + "membership": membership, + "type": gtype, + "content": json.loads(content_json), + } + for group_id, membership, gtype, content_json in txn + ] + + return self.db.runInteraction( + "get_groups_changes_for_user", _get_groups_changes_for_user_txn + ) + + def get_all_groups_changes(self, from_token, to_token, limit): + from_token = int(from_token) + has_changed = self._group_updates_stream_cache.has_any_entity_changed( + from_token + ) + if not has_changed: + return defer.succeed([]) + + def _get_all_groups_changes_txn(txn): + sql = """ + SELECT stream_id, group_id, user_id, type, content + FROM local_group_updates + WHERE ? < stream_id AND stream_id <= ? + LIMIT ? + """ + txn.execute(sql, (from_token, to_token, limit)) + return [ + (stream_id, group_id, user_id, gtype, json.loads(content_json)) + for stream_id, group_id, user_id, gtype, content_json in txn + ] + + return self.db.runInteraction( + "get_all_groups_changes", _get_all_groups_changes_txn + ) + + +class GroupServerStore(GroupServerWorkerStore): + def set_group_join_policy(self, group_id, join_policy): + """Set the join policy of a group. + + join_policy can be one of: + * "invite" + * "open" + """ + return self.db.simple_update_one( + table="groups", + keyvalues={"group_id": group_id}, + updatevalues={"join_policy": join_policy}, + desc="set_group_join_policy", + ) + def add_room_to_summary(self, group_id, room_id, category_id, order, is_public): return self.db.runInteraction( "add_room_to_summary", @@ -299,36 +645,6 @@ class GroupServerStore(SQLBaseStore): desc="remove_room_from_summary", ) - @defer.inlineCallbacks - def get_group_categories(self, group_id): - rows = yield self.db.simple_select_list( - table="group_room_categories", - keyvalues={"group_id": group_id}, - retcols=("category_id", "is_public", "profile"), - desc="get_group_categories", - ) - - return { - row["category_id"]: { - "is_public": row["is_public"], - "profile": json.loads(row["profile"]), - } - for row in rows - } - - @defer.inlineCallbacks - def get_group_category(self, group_id, category_id): - category = yield self.db.simple_select_one( - table="group_room_categories", - keyvalues={"group_id": group_id, "category_id": category_id}, - retcols=("is_public", "profile"), - desc="get_group_category", - ) - - category["profile"] = json.loads(category["profile"]) - - return category - def upsert_group_category(self, group_id, category_id, profile, is_public): """Add/update room category for group """ @@ -360,36 +676,6 @@ class GroupServerStore(SQLBaseStore): desc="remove_group_category", ) - @defer.inlineCallbacks - def get_group_roles(self, group_id): - rows = yield self.db.simple_select_list( - table="group_roles", - keyvalues={"group_id": group_id}, - retcols=("role_id", "is_public", "profile"), - desc="get_group_roles", - ) - - return { - row["role_id"]: { - "is_public": row["is_public"], - "profile": json.loads(row["profile"]), - } - for row in rows - } - - @defer.inlineCallbacks - def get_group_role(self, group_id, role_id): - role = yield self.db.simple_select_one( - table="group_roles", - keyvalues={"group_id": group_id, "role_id": role_id}, - retcols=("is_public", "profile"), - desc="get_group_role", - ) - - role["profile"] = json.loads(role["profile"]) - - return role - def upsert_group_role(self, group_id, role_id, profile, is_public): """Add/remove user role """ @@ -555,100 +841,6 @@ class GroupServerStore(SQLBaseStore): desc="remove_user_from_summary", ) - def get_local_groups_for_room(self, room_id): - """Get all of the local group that contain a given room - Args: - room_id (str): The ID of a room - Returns: - Deferred[list[str]]: A twisted.Deferred containing a list of group ids - containing this room - """ - return self.db.simple_select_onecol( - table="group_rooms", - keyvalues={"room_id": room_id}, - retcol="group_id", - desc="get_local_groups_for_room", - ) - - def get_users_for_summary_by_role(self, group_id, include_private=False): - """Get the users and roles that should be included in a summary request - - Returns ([users], [roles]) - """ - - def _get_users_for_summary_txn(txn): - keyvalues = {"group_id": group_id} - if not include_private: - keyvalues["is_public"] = True - - sql = """ - SELECT user_id, is_public, role_id, user_order - FROM group_summary_users - WHERE group_id = ? - """ - - if not include_private: - sql += " AND is_public = ?" - txn.execute(sql, (group_id, True)) - else: - txn.execute(sql, (group_id,)) - - users = [ - { - "user_id": row[0], - "is_public": row[1], - "role_id": row[2] if row[2] != _DEFAULT_ROLE_ID else None, - "order": row[3], - } - for row in txn - ] - - sql = """ - SELECT role_id, is_public, profile, role_order - FROM group_summary_roles - INNER JOIN group_roles USING (group_id, role_id) - WHERE group_id = ? - """ - - if not include_private: - sql += " AND is_public = ?" - txn.execute(sql, (group_id, True)) - else: - txn.execute(sql, (group_id,)) - - roles = { - row[0]: { - "is_public": row[1], - "profile": json.loads(row[2]), - "order": row[3], - } - for row in txn - } - - return users, roles - - return self.db.runInteraction( - "get_users_for_summary_by_role", _get_users_for_summary_txn - ) - - def is_user_in_group(self, user_id, group_id): - return self.db.simple_select_one_onecol( - table="group_users", - keyvalues={"group_id": group_id, "user_id": user_id}, - retcol="user_id", - allow_none=True, - desc="is_user_in_group", - ).addCallback(lambda r: bool(r)) - - def is_user_admin_in_group(self, group_id, user_id): - return self.db.simple_select_one_onecol( - table="group_users", - keyvalues={"group_id": group_id, "user_id": user_id}, - retcol="is_admin", - allow_none=True, - desc="is_user_admin_in_group", - ) - def add_group_invite(self, group_id, user_id): """Record that the group server has invited a user """ @@ -658,64 +850,6 @@ class GroupServerStore(SQLBaseStore): desc="add_group_invite", ) - def is_user_invited_to_local_group(self, group_id, user_id): - """Has the group server invited a user? - """ - return self.db.simple_select_one_onecol( - table="group_invites", - keyvalues={"group_id": group_id, "user_id": user_id}, - retcol="user_id", - desc="is_user_invited_to_local_group", - allow_none=True, - ) - - def get_users_membership_info_in_group(self, group_id, user_id): - """Get a dict describing the membership of a user in a group. - - Example if joined: - - { - "membership": "join", - "is_public": True, - "is_privileged": False, - } - - Returns an empty dict if the user is not join/invite/etc - """ - - def _get_users_membership_in_group_txn(txn): - row = self.db.simple_select_one_txn( - txn, - table="group_users", - keyvalues={"group_id": group_id, "user_id": user_id}, - retcols=("is_admin", "is_public"), - allow_none=True, - ) - - if row: - return { - "membership": "join", - "is_public": row["is_public"], - "is_privileged": row["is_admin"], - } - - row = self.db.simple_select_one_onecol_txn( - txn, - table="group_invites", - keyvalues={"group_id": group_id, "user_id": user_id}, - retcol="user_id", - allow_none=True, - ) - - if row: - return {"membership": "invite"} - - return {} - - return self.db.runInteraction( - "get_users_membership_info_in_group", _get_users_membership_in_group_txn - ) - def add_user_to_group( self, group_id, @@ -846,16 +980,6 @@ class GroupServerStore(SQLBaseStore): "remove_room_from_group", _remove_room_from_group_txn ) - def get_publicised_groups_for_user(self, user_id): - """Get all groups a user is publicising - """ - return self.db.simple_select_onecol( - table="local_group_membership", - keyvalues={"user_id": user_id, "membership": "join", "is_publicised": True}, - retcol="group_id", - desc="get_publicised_groups_for_user", - ) - def update_group_publicity(self, group_id, user_id, publicise): """Update whether the user is publicising their membership of the group """ @@ -1000,22 +1124,6 @@ class GroupServerStore(SQLBaseStore): desc="update_group_profile", ) - def get_attestations_need_renewals(self, valid_until_ms): - """Get all attestations that need to be renewed until givent time - """ - - def _get_attestations_need_renewals_txn(txn): - sql = """ - SELECT group_id, user_id FROM group_attestations_renewals - WHERE valid_until_ms <= ? - """ - txn.execute(sql, (valid_until_ms,)) - return self.db.cursor_to_dict(txn) - - return self.db.runInteraction( - "get_attestations_need_renewals", _get_attestations_need_renewals_txn - ) - def update_attestation_renewal(self, group_id, user_id, attestation): """Update an attestation that we have renewed """ @@ -1054,112 +1162,6 @@ class GroupServerStore(SQLBaseStore): desc="remove_attestation_renewal", ) - @defer.inlineCallbacks - def get_remote_attestation(self, group_id, user_id): - """Get the attestation that proves the remote agrees that the user is - in the group. - """ - row = yield self.db.simple_select_one( - table="group_attestations_remote", - keyvalues={"group_id": group_id, "user_id": user_id}, - retcols=("valid_until_ms", "attestation_json"), - desc="get_remote_attestation", - allow_none=True, - ) - - now = int(self._clock.time_msec()) - if row and now < row["valid_until_ms"]: - return json.loads(row["attestation_json"]) - - return None - - def get_joined_groups(self, user_id): - return self.db.simple_select_onecol( - table="local_group_membership", - keyvalues={"user_id": user_id, "membership": "join"}, - retcol="group_id", - desc="get_joined_groups", - ) - - def get_all_groups_for_user(self, user_id, now_token): - def _get_all_groups_for_user_txn(txn): - sql = """ - SELECT group_id, type, membership, u.content - FROM local_group_updates AS u - INNER JOIN local_group_membership USING (group_id, user_id) - WHERE user_id = ? AND membership != 'leave' - AND stream_id <= ? - """ - txn.execute(sql, (user_id, now_token)) - return [ - { - "group_id": row[0], - "type": row[1], - "membership": row[2], - "content": json.loads(row[3]), - } - for row in txn - ] - - return self.db.runInteraction( - "get_all_groups_for_user", _get_all_groups_for_user_txn - ) - - def get_groups_changes_for_user(self, user_id, from_token, to_token): - from_token = int(from_token) - has_changed = self._group_updates_stream_cache.has_entity_changed( - user_id, from_token - ) - if not has_changed: - return defer.succeed([]) - - def _get_groups_changes_for_user_txn(txn): - sql = """ - SELECT group_id, membership, type, u.content - FROM local_group_updates AS u - INNER JOIN local_group_membership USING (group_id, user_id) - WHERE user_id = ? AND ? < stream_id AND stream_id <= ? - """ - txn.execute(sql, (user_id, from_token, to_token)) - return [ - { - "group_id": group_id, - "membership": membership, - "type": gtype, - "content": json.loads(content_json), - } - for group_id, membership, gtype, content_json in txn - ] - - return self.db.runInteraction( - "get_groups_changes_for_user", _get_groups_changes_for_user_txn - ) - - def get_all_groups_changes(self, from_token, to_token, limit): - from_token = int(from_token) - has_changed = self._group_updates_stream_cache.has_any_entity_changed( - from_token - ) - if not has_changed: - return defer.succeed([]) - - def _get_all_groups_changes_txn(txn): - sql = """ - SELECT stream_id, group_id, user_id, type, content - FROM local_group_updates - WHERE ? < stream_id AND stream_id <= ? - LIMIT ? - """ - txn.execute(sql, (from_token, to_token, limit)) - return [ - (stream_id, group_id, user_id, gtype, json.loads(content_json)) - for stream_id, group_id, user_id, gtype, content_json in txn - ] - - return self.db.runInteraction( - "get_all_groups_changes", _get_all_groups_changes_txn - ) - def get_group_stream_token(self): return self._group_updates_id_gen.get_current_token() From b08b0a22d505b1555f511e3f38935a62930ea25d Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 7 Feb 2020 13:56:38 +0000 Subject: [PATCH 1004/1623] Add typing to synapse.federation.sender (#6871) --- changelog.d/6871.misc | 1 + synapse/federation/federation_server.py | 7 +- synapse/federation/sender/__init__.py | 99 +++++++++---------- .../sender/per_destination_queue.py | 88 +++++++++-------- .../federation/sender/transaction_manager.py | 16 +-- synapse/federation/units.py | 23 ++++- synapse/server.pyi | 2 + tests/handlers/test_typing.py | 8 +- tox.ini | 1 + 9 files changed, 138 insertions(+), 107 deletions(-) create mode 100644 changelog.d/6871.misc diff --git a/changelog.d/6871.misc b/changelog.d/6871.misc new file mode 100644 index 0000000000..5161af9983 --- /dev/null +++ b/changelog.d/6871.misc @@ -0,0 +1 @@ +Add typing to `synapse.federation.sender` and port to async/await. diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index 2489832a11..a6c966a393 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -294,7 +294,12 @@ class FederationServer(FederationBase): async def _process_edu(edu_dict): received_edus_counter.inc() - edu = Edu(**edu_dict) + edu = Edu( + origin=origin, + destination=self.server_name, + edu_type=edu_dict["edu_type"], + content=edu_dict["content"], + ) await self.registry.on_edu(edu.edu_type, origin, edu.content) await concurrently_execute( diff --git a/synapse/federation/sender/__init__.py b/synapse/federation/sender/__init__.py index 36c83c3027..233cb33daf 100644 --- a/synapse/federation/sender/__init__.py +++ b/synapse/federation/sender/__init__.py @@ -14,6 +14,7 @@ # limitations under the License. import logging +from typing import Dict, Hashable, Iterable, List, Optional, Set from six import itervalues @@ -23,6 +24,7 @@ from twisted.internet import defer import synapse import synapse.metrics +from synapse.events import EventBase from synapse.federation.sender.per_destination_queue import PerDestinationQueue from synapse.federation.sender.transaction_manager import TransactionManager from synapse.federation.units import Edu @@ -39,6 +41,8 @@ from synapse.metrics import ( events_processed_counter, ) from synapse.metrics.background_process_metrics import run_as_background_process +from synapse.storage.presence import UserPresenceState +from synapse.types import ReadReceipt from synapse.util.metrics import Measure, measure_func logger = logging.getLogger(__name__) @@ -68,7 +72,7 @@ class FederationSender(object): self._transaction_manager = TransactionManager(hs) # map from destination to PerDestinationQueue - self._per_destination_queues = {} # type: dict[str, PerDestinationQueue] + self._per_destination_queues = {} # type: Dict[str, PerDestinationQueue] LaterGauge( "synapse_federation_transaction_queue_pending_destinations", @@ -84,7 +88,7 @@ class FederationSender(object): # Map of user_id -> UserPresenceState for all the pending presence # to be sent out by user_id. Entries here get processed and put in # pending_presence_by_dest - self.pending_presence = {} + self.pending_presence = {} # type: Dict[str, UserPresenceState] LaterGauge( "synapse_federation_transaction_queue_pending_pdus", @@ -116,20 +120,17 @@ class FederationSender(object): # and that there is a pending call to _flush_rrs_for_room in the system. self._queues_awaiting_rr_flush_by_room = ( {} - ) # type: dict[str, set[PerDestinationQueue]] + ) # type: Dict[str, Set[PerDestinationQueue]] self._rr_txn_interval_per_room_ms = ( - 1000.0 / hs.get_config().federation_rr_transactions_per_room_per_second + 1000.0 / hs.config.federation_rr_transactions_per_room_per_second ) - def _get_per_destination_queue(self, destination): + def _get_per_destination_queue(self, destination: str) -> PerDestinationQueue: """Get or create a PerDestinationQueue for the given destination Args: - destination (str): server_name of remote server - - Returns: - PerDestinationQueue + destination: server_name of remote server """ queue = self._per_destination_queues.get(destination) if not queue: @@ -137,7 +138,7 @@ class FederationSender(object): self._per_destination_queues[destination] = queue return queue - def notify_new_events(self, current_id): + def notify_new_events(self, current_id: int) -> None: """This gets called when we have some new events we might want to send out to other servers. """ @@ -151,13 +152,12 @@ class FederationSender(object): "process_event_queue_for_federation", self._process_event_queue_loop ) - @defer.inlineCallbacks - def _process_event_queue_loop(self): + async def _process_event_queue_loop(self) -> None: try: self._is_processing = True while True: - last_token = yield self.store.get_federation_out_pos("events") - next_token, events = yield self.store.get_all_new_events_stream( + last_token = await self.store.get_federation_out_pos("events") + next_token, events = await self.store.get_all_new_events_stream( last_token, self._last_poked_id, limit=100 ) @@ -166,8 +166,7 @@ class FederationSender(object): if not events and next_token >= self._last_poked_id: break - @defer.inlineCallbacks - def handle_event(event): + async def handle_event(event: EventBase) -> None: # Only send events for this server. send_on_behalf_of = event.internal_metadata.get_send_on_behalf_of() is_mine = self.is_mine_id(event.sender) @@ -184,7 +183,7 @@ class FederationSender(object): # Otherwise if the last member on a server in a room is # banned then it won't receive the event because it won't # be in the room after the ban. - destinations = yield self.state.get_hosts_in_room_at_events( + destinations = await self.state.get_hosts_in_room_at_events( event.room_id, event_ids=event.prev_event_ids() ) except Exception: @@ -206,17 +205,16 @@ class FederationSender(object): self._send_pdu(event, destinations) - @defer.inlineCallbacks - def handle_room_events(events): + async def handle_room_events(events: Iterable[EventBase]) -> None: with Measure(self.clock, "handle_room_events"): for event in events: - yield handle_event(event) + await handle_event(event) - events_by_room = {} + events_by_room = {} # type: Dict[str, List[EventBase]] for event in events: events_by_room.setdefault(event.room_id, []).append(event) - yield make_deferred_yieldable( + await make_deferred_yieldable( defer.gatherResults( [ run_in_background(handle_room_events, evs) @@ -226,11 +224,11 @@ class FederationSender(object): ) ) - yield self.store.update_federation_out_pos("events", next_token) + await self.store.update_federation_out_pos("events", next_token) if events: now = self.clock.time_msec() - ts = yield self.store.get_received_ts(events[-1].event_id) + ts = await self.store.get_received_ts(events[-1].event_id) synapse.metrics.event_processing_lag.labels( "federation_sender" @@ -254,7 +252,7 @@ class FederationSender(object): finally: self._is_processing = False - def _send_pdu(self, pdu, destinations): + def _send_pdu(self, pdu: EventBase, destinations: Iterable[str]) -> None: # We loop through all destinations to see whether we already have # a transaction in progress. If we do, stick it in the pending_pdus # table and we'll get back to it later. @@ -276,11 +274,11 @@ class FederationSender(object): self._get_per_destination_queue(destination).send_pdu(pdu, order) @defer.inlineCallbacks - def send_read_receipt(self, receipt): + def send_read_receipt(self, receipt: ReadReceipt): """Send a RR to any other servers in the room Args: - receipt (synapse.types.ReadReceipt): receipt to be sent + receipt: receipt to be sent """ # Some background on the rate-limiting going on here. @@ -343,7 +341,7 @@ class FederationSender(object): else: queue.flush_read_receipts_for_room(room_id) - def _schedule_rr_flush_for_room(self, room_id, n_domains): + def _schedule_rr_flush_for_room(self, room_id: str, n_domains: int) -> None: # that is going to cause approximately len(domains) transactions, so now back # off for that multiplied by RR_TXN_INTERVAL_PER_ROOM backoff_ms = self._rr_txn_interval_per_room_ms * n_domains @@ -352,7 +350,7 @@ class FederationSender(object): self.clock.call_later(backoff_ms, self._flush_rrs_for_room, room_id) self._queues_awaiting_rr_flush_by_room[room_id] = set() - def _flush_rrs_for_room(self, room_id): + def _flush_rrs_for_room(self, room_id: str) -> None: queues = self._queues_awaiting_rr_flush_by_room.pop(room_id) logger.debug("Flushing RRs in %s to %s", room_id, queues) @@ -368,14 +366,11 @@ class FederationSender(object): @preserve_fn # the caller should not yield on this @defer.inlineCallbacks - def send_presence(self, states): + def send_presence(self, states: List[UserPresenceState]): """Send the new presence states to the appropriate destinations. This actually queues up the presence states ready for sending and triggers a background task to process them and send out the transactions. - - Args: - states (list(UserPresenceState)) """ if not self.hs.config.use_presence: # No-op if presence is disabled. @@ -412,11 +407,10 @@ class FederationSender(object): finally: self._processing_pending_presence = False - def send_presence_to_destinations(self, states, destinations): + def send_presence_to_destinations( + self, states: List[UserPresenceState], destinations: List[str] + ) -> None: """Send the given presence states to the given destinations. - - Args: - states (list[UserPresenceState]) destinations (list[str]) """ @@ -431,12 +425,9 @@ class FederationSender(object): @measure_func("txnqueue._process_presence") @defer.inlineCallbacks - def _process_presence_inner(self, states): + def _process_presence_inner(self, states: List[UserPresenceState]): """Given a list of states populate self.pending_presence_by_dest and poke to send a new transaction to each destination - - Args: - states (list(UserPresenceState)) """ hosts_and_states = yield get_interested_remotes(self.store, states, self.state) @@ -446,14 +437,20 @@ class FederationSender(object): continue self._get_per_destination_queue(destination).send_presence(states) - def build_and_send_edu(self, destination, edu_type, content, key=None): + def build_and_send_edu( + self, + destination: str, + edu_type: str, + content: dict, + key: Optional[Hashable] = None, + ): """Construct an Edu object, and queue it for sending Args: - destination (str): name of server to send to - edu_type (str): type of EDU to send - content (dict): content of EDU - key (Any|None): clobbering key for this edu + destination: name of server to send to + edu_type: type of EDU to send + content: content of EDU + key: clobbering key for this edu """ if destination == self.server_name: logger.info("Not sending EDU to ourselves") @@ -468,12 +465,12 @@ class FederationSender(object): self.send_edu(edu, key) - def send_edu(self, edu, key): + def send_edu(self, edu: Edu, key: Optional[Hashable]): """Queue an EDU for sending Args: - edu (Edu): edu to send - key (Any|None): clobbering key for this edu + edu: edu to send + key: clobbering key for this edu """ queue = self._get_per_destination_queue(edu.destination) if key: @@ -481,7 +478,7 @@ class FederationSender(object): else: queue.send_edu(edu) - def send_device_messages(self, destination): + def send_device_messages(self, destination: str): if destination == self.server_name: logger.warning("Not sending device update to ourselves") return @@ -501,5 +498,5 @@ class FederationSender(object): self._get_per_destination_queue(destination).attempt_new_transaction() - def get_current_token(self): + def get_current_token(self) -> int: return 0 diff --git a/synapse/federation/sender/per_destination_queue.py b/synapse/federation/sender/per_destination_queue.py index 5012aaea35..e13cd20ffa 100644 --- a/synapse/federation/sender/per_destination_queue.py +++ b/synapse/federation/sender/per_destination_queue.py @@ -15,11 +15,11 @@ # limitations under the License. import datetime import logging +from typing import Dict, Hashable, Iterable, List, Tuple from prometheus_client import Counter -from twisted.internet import defer - +import synapse.server from synapse.api.errors import ( FederationDeniedError, HttpResponseException, @@ -31,7 +31,7 @@ from synapse.handlers.presence import format_user_presence_state from synapse.metrics import sent_transactions_counter from synapse.metrics.background_process_metrics import run_as_background_process from synapse.storage.presence import UserPresenceState -from synapse.types import StateMap +from synapse.types import ReadReceipt from synapse.util.retryutils import NotRetryingDestination, get_retry_limiter # This is defined in the Matrix spec and enforced by the receiver. @@ -56,13 +56,18 @@ class PerDestinationQueue(object): Manages the per-destination transmission queues. Args: - hs (synapse.HomeServer): - transaction_sender (TransactionManager): - destination (str): the server_name of the destination that we are managing + hs + transaction_sender + destination: the server_name of the destination that we are managing transmission for. """ - def __init__(self, hs, transaction_manager, destination): + def __init__( + self, + hs: "synapse.server.HomeServer", + transaction_manager: "synapse.federation.sender.TransactionManager", + destination: str, + ): self._server_name = hs.hostname self._clock = hs.get_clock() self._store = hs.get_datastore() @@ -72,20 +77,20 @@ class PerDestinationQueue(object): self.transmission_loop_running = False # a list of tuples of (pending pdu, order) - self._pending_pdus = [] # type: list[tuple[EventBase, int]] - self._pending_edus = [] # type: list[Edu] + self._pending_pdus = [] # type: List[Tuple[EventBase, int]] + self._pending_edus = [] # type: List[Edu] # Pending EDUs by their "key". Keyed EDUs are EDUs that get clobbered # based on their key (e.g. typing events by room_id) # Map of (edu_type, key) -> Edu - self._pending_edus_keyed = {} # type: StateMap[Edu] + self._pending_edus_keyed = {} # type: Dict[Tuple[str, Hashable], Edu] # Map of user_id -> UserPresenceState of pending presence to be sent to this # destination - self._pending_presence = {} # type: dict[str, UserPresenceState] + self._pending_presence = {} # type: Dict[str, UserPresenceState] # room_id -> receipt_type -> user_id -> receipt_dict - self._pending_rrs = {} + self._pending_rrs = {} # type: Dict[str, Dict[str, Dict[str, dict]]] self._rrs_pending_flush = False # stream_id of last successfully sent to-device message. @@ -95,50 +100,50 @@ class PerDestinationQueue(object): # stream_id of last successfully sent device list update. self._last_device_list_stream_id = 0 - def __str__(self): + def __str__(self) -> str: return "PerDestinationQueue[%s]" % self._destination - def pending_pdu_count(self): + def pending_pdu_count(self) -> int: return len(self._pending_pdus) - def pending_edu_count(self): + def pending_edu_count(self) -> int: return ( len(self._pending_edus) + len(self._pending_presence) + len(self._pending_edus_keyed) ) - def send_pdu(self, pdu, order): + def send_pdu(self, pdu: EventBase, order: int) -> None: """Add a PDU to the queue, and start the transmission loop if neccessary Args: - pdu (EventBase): pdu to send - order (int): + pdu: pdu to send + order """ self._pending_pdus.append((pdu, order)) self.attempt_new_transaction() - def send_presence(self, states): + def send_presence(self, states: Iterable[UserPresenceState]) -> None: """Add presence updates to the queue. Start the transmission loop if neccessary. Args: - states (iterable[UserPresenceState]): presence to send + states: presence to send """ self._pending_presence.update({state.user_id: state for state in states}) self.attempt_new_transaction() - def queue_read_receipt(self, receipt): + def queue_read_receipt(self, receipt: ReadReceipt) -> None: """Add a RR to the list to be sent. Doesn't start the transmission loop yet (see flush_read_receipts_for_room) Args: - receipt (synapse.api.receipt_info.ReceiptInfo): receipt to be queued + receipt: receipt to be queued """ self._pending_rrs.setdefault(receipt.room_id, {}).setdefault( receipt.receipt_type, {} )[receipt.user_id] = {"event_ids": receipt.event_ids, "data": receipt.data} - def flush_read_receipts_for_room(self, room_id): + def flush_read_receipts_for_room(self, room_id: str) -> None: # if we don't have any read-receipts for this room, it may be that we've already # sent them out, so we don't need to flush. if room_id not in self._pending_rrs: @@ -146,15 +151,15 @@ class PerDestinationQueue(object): self._rrs_pending_flush = True self.attempt_new_transaction() - def send_keyed_edu(self, edu, key): + def send_keyed_edu(self, edu: Edu, key: Hashable) -> None: self._pending_edus_keyed[(edu.edu_type, key)] = edu self.attempt_new_transaction() - def send_edu(self, edu): + def send_edu(self, edu) -> None: self._pending_edus.append(edu) self.attempt_new_transaction() - def attempt_new_transaction(self): + def attempt_new_transaction(self) -> None: """Try to start a new transaction to this destination If there is already a transaction in progress to this destination, @@ -177,23 +182,22 @@ class PerDestinationQueue(object): self._transaction_transmission_loop, ) - @defer.inlineCallbacks - def _transaction_transmission_loop(self): - pending_pdus = [] + async def _transaction_transmission_loop(self) -> None: + pending_pdus = [] # type: List[Tuple[EventBase, int]] try: self.transmission_loop_running = True # This will throw if we wouldn't retry. We do this here so we fail # quickly, but we will later check this again in the http client, # hence why we throw the result away. - yield get_retry_limiter(self._destination, self._clock, self._store) + await get_retry_limiter(self._destination, self._clock, self._store) pending_pdus = [] while True: # We have to keep 2 free slots for presence and rr_edus limit = MAX_EDUS_PER_TRANSACTION - 2 - device_update_edus, dev_list_id = yield self._get_device_update_edus( + device_update_edus, dev_list_id = await self._get_device_update_edus( limit ) @@ -202,7 +206,7 @@ class PerDestinationQueue(object): ( to_device_edus, device_stream_id, - ) = yield self._get_to_device_message_edus(limit) + ) = await self._get_to_device_message_edus(limit) pending_edus = device_update_edus + to_device_edus @@ -269,7 +273,7 @@ class PerDestinationQueue(object): # END CRITICAL SECTION - success = yield self._transaction_manager.send_new_transaction( + success = await self._transaction_manager.send_new_transaction( self._destination, pending_pdus, pending_edus ) if success: @@ -280,7 +284,7 @@ class PerDestinationQueue(object): # Remove the acknowledged device messages from the database # Only bother if we actually sent some device messages if to_device_edus: - yield self._store.delete_device_msgs_for_remote( + await self._store.delete_device_msgs_for_remote( self._destination, device_stream_id ) @@ -289,7 +293,7 @@ class PerDestinationQueue(object): logger.info( "Marking as sent %r %r", self._destination, dev_list_id ) - yield self._store.mark_as_sent_devices_by_remote( + await self._store.mark_as_sent_devices_by_remote( self._destination, dev_list_id ) @@ -334,7 +338,7 @@ class PerDestinationQueue(object): # We want to be *very* sure we clear this after we stop processing self.transmission_loop_running = False - def _get_rr_edus(self, force_flush): + def _get_rr_edus(self, force_flush: bool) -> Iterable[Edu]: if not self._pending_rrs: return if not force_flush and not self._rrs_pending_flush: @@ -351,17 +355,16 @@ class PerDestinationQueue(object): self._rrs_pending_flush = False yield edu - def _pop_pending_edus(self, limit): + def _pop_pending_edus(self, limit: int) -> List[Edu]: pending_edus = self._pending_edus pending_edus, self._pending_edus = pending_edus[:limit], pending_edus[limit:] return pending_edus - @defer.inlineCallbacks - def _get_device_update_edus(self, limit): + async def _get_device_update_edus(self, limit: int) -> Tuple[List[Edu], int]: last_device_list = self._last_device_list_stream_id # Retrieve list of new device updates to send to the destination - now_stream_id, results = yield self._store.get_device_updates_by_remote( + now_stream_id, results = await self._store.get_device_updates_by_remote( self._destination, last_device_list, limit=limit ) edus = [ @@ -378,11 +381,10 @@ class PerDestinationQueue(object): return (edus, now_stream_id) - @defer.inlineCallbacks - def _get_to_device_message_edus(self, limit): + async def _get_to_device_message_edus(self, limit: int) -> Tuple[List[Edu], int]: last_device_stream_id = self._last_device_stream_id to_device_stream_id = self._store.get_to_device_stream_token() - contents, stream_id = yield self._store.get_new_device_msgs_for_remote( + contents, stream_id = await self._store.get_new_device_msgs_for_remote( self._destination, last_device_stream_id, to_device_stream_id, limit ) edus = [ diff --git a/synapse/federation/sender/transaction_manager.py b/synapse/federation/sender/transaction_manager.py index 5fed626d5b..3c2a02a3b3 100644 --- a/synapse/federation/sender/transaction_manager.py +++ b/synapse/federation/sender/transaction_manager.py @@ -13,14 +13,15 @@ # See the License for the specific language governing permissions and # limitations under the License. import logging +from typing import List from canonicaljson import json -from twisted.internet import defer - +import synapse.server from synapse.api.errors import HttpResponseException +from synapse.events import EventBase from synapse.federation.persistence import TransactionActions -from synapse.federation.units import Transaction +from synapse.federation.units import Edu, Transaction from synapse.logging.opentracing import ( extract_text_map, set_tag, @@ -39,7 +40,7 @@ class TransactionManager(object): shared between PerDestinationQueue objects """ - def __init__(self, hs): + def __init__(self, hs: "synapse.server.HomeServer"): self._server_name = hs.hostname self.clock = hs.get_clock() # nb must be called this for @measure_func self._store = hs.get_datastore() @@ -50,8 +51,9 @@ class TransactionManager(object): self._next_txn_id = int(self.clock.time_msec()) @measure_func("_send_new_transaction") - @defer.inlineCallbacks - def send_new_transaction(self, destination, pending_pdus, pending_edus): + async def send_new_transaction( + self, destination: str, pending_pdus: List[EventBase], pending_edus: List[Edu] + ): # Make a transaction-sending opentracing span. This span follows on from # all the edus in that transaction. This needs to be done since there is @@ -127,7 +129,7 @@ class TransactionManager(object): return data try: - response = yield self._transport_layer.send_transaction( + response = await self._transport_layer.send_transaction( transaction, json_data_cb ) code = 200 diff --git a/synapse/federation/units.py b/synapse/federation/units.py index b4d743cde7..6b32e0dcbf 100644 --- a/synapse/federation/units.py +++ b/synapse/federation/units.py @@ -19,11 +19,15 @@ server protocol. import logging +import attr + +from synapse.types import JsonDict from synapse.util.jsonobject import JsonEncodedObject logger = logging.getLogger(__name__) +@attr.s(slots=True) class Edu(JsonEncodedObject): """ An Edu represents a piece of data sent from one homeserver to another. @@ -32,11 +36,24 @@ class Edu(JsonEncodedObject): internal ID or previous references graph. """ - valid_keys = ["origin", "destination", "edu_type", "content"] + edu_type = attr.ib(type=str) + content = attr.ib(type=dict) + origin = attr.ib(type=str) + destination = attr.ib(type=str) - required_keys = ["edu_type"] + def get_dict(self) -> JsonDict: + return { + "edu_type": self.edu_type, + "content": self.content, + } - internal_keys = ["origin", "destination"] + def get_internal_dict(self) -> JsonDict: + return { + "edu_type": self.edu_type, + "content": self.content, + "origin": self.origin, + "destination": self.destination, + } def get_context(self): return getattr(self, "content", {}).get("org.matrix.opentracing_context", "{}") diff --git a/synapse/server.pyi b/synapse/server.pyi index 90347ac23e..40eabfe5d9 100644 --- a/synapse/server.pyi +++ b/synapse/server.pyi @@ -107,3 +107,5 @@ class HomeServer(object): self, ) -> synapse.replication.tcp.client.ReplicationClientHandler: pass + def is_mine_id(self, domain_id: str) -> bool: + pass diff --git a/tests/handlers/test_typing.py b/tests/handlers/test_typing.py index 68b9847bd2..2767b0497a 100644 --- a/tests/handlers/test_typing.py +++ b/tests/handlers/test_typing.py @@ -111,7 +111,9 @@ class TypingNotificationsTestCase(unittest.HomeserverTestCase): retry_timings_res ) - self.datastore.get_device_updates_by_remote.return_value = (0, []) + self.datastore.get_device_updates_by_remote.return_value = defer.succeed( + (0, []) + ) def get_received_txn_response(*args): return defer.succeed(None) @@ -144,7 +146,9 @@ class TypingNotificationsTestCase(unittest.HomeserverTestCase): self.datastore.get_current_state_deltas.return_value = (0, None) self.datastore.get_to_device_stream_token = lambda: 0 - self.datastore.get_new_device_msgs_for_remote = lambda *args, **kargs: ([], 0) + self.datastore.get_new_device_msgs_for_remote = lambda *args, **kargs: defer.succeed( + ([], 0) + ) self.datastore.delete_device_msgs_for_remote = lambda *args, **kargs: None self.datastore.set_received_txn_response = lambda *args, **kwargs: defer.succeed( None diff --git a/tox.ini b/tox.ini index ef22368cf1..f8229eba88 100644 --- a/tox.ini +++ b/tox.ini @@ -179,6 +179,7 @@ extras = all commands = mypy \ synapse/api \ synapse/config/ \ + synapse/federation/sender \ synapse/federation/transport \ synapse/handlers/sync.py \ synapse/handlers/ui_auth \ From 799001f2c0b31d72b95a252a3808da25987e1ed3 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 7 Feb 2020 15:30:04 +0000 Subject: [PATCH 1005/1623] Add a `make_event_from_dict` method (#6858) ... and use it in places where it's trivial to do so. This will make it easier to pass room versions into the FrozenEvent constructors. --- changelog.d/6858.misc | 1 + synapse/events/__init__.py | 16 ++++++++++++++-- synapse/events/builder.py | 10 +++------- synapse/federation/federation_base.py | 5 ++--- tests/api/test_filtering.py | 4 ++-- tests/crypto/test_event_signing.py | 6 +++--- tests/events/test_utils.py | 9 +++++---- tests/federation/test_federation_server.py | 4 ++-- tests/replication/slave/storage/test_events.py | 12 ++++++++---- tests/state/test_v2.py | 4 ++-- tests/test_event_auth.py | 10 +++++----- tests/test_federation.py | 6 +++--- tests/test_state.py | 4 ++-- 13 files changed, 52 insertions(+), 39 deletions(-) create mode 100644 changelog.d/6858.misc diff --git a/changelog.d/6858.misc b/changelog.d/6858.misc new file mode 100644 index 0000000000..08aa80bcd9 --- /dev/null +++ b/changelog.d/6858.misc @@ -0,0 +1 @@ +Refactoring work in preparation for changing the event redaction algorithm. diff --git a/synapse/events/__init__.py b/synapse/events/__init__.py index 89d41d82b6..a842661a90 100644 --- a/synapse/events/__init__.py +++ b/synapse/events/__init__.py @@ -16,12 +16,13 @@ import os from distutils.util import strtobool +from typing import Optional, Type import six from unpaddedbase64 import encode_base64 -from synapse.api.room_versions import EventFormatVersions +from synapse.api.room_versions import EventFormatVersions, RoomVersion, RoomVersions from synapse.types import JsonDict from synapse.util.caches import intern_dict from synapse.util.frozenutils import freeze @@ -407,7 +408,7 @@ class FrozenEventV3(FrozenEventV2): return self._event_id -def event_type_from_format_version(format_version): +def event_type_from_format_version(format_version: int) -> Type[EventBase]: """Returns the python type to use to construct an Event object for the given event format version. @@ -427,3 +428,14 @@ def event_type_from_format_version(format_version): return FrozenEventV3 else: raise Exception("No event format %r" % (format_version,)) + + +def make_event_from_dict( + event_dict: JsonDict, + room_version: RoomVersion = RoomVersions.V1, + internal_metadata_dict: JsonDict = {}, + rejected_reason: Optional[str] = None, +) -> EventBase: + """Construct an EventBase from the given event dict""" + event_type = event_type_from_format_version(room_version.event_format) + return event_type(event_dict, internal_metadata_dict, rejected_reason) diff --git a/synapse/events/builder.py b/synapse/events/builder.py index 8d63ad6dc3..a0c4a40c27 100644 --- a/synapse/events/builder.py +++ b/synapse/events/builder.py @@ -28,11 +28,7 @@ from synapse.api.room_versions import ( RoomVersion, ) from synapse.crypto.event_signing import add_hashes_and_signatures -from synapse.events import ( - EventBase, - _EventInternalMetadata, - event_type_from_format_version, -) +from synapse.events import EventBase, _EventInternalMetadata, make_event_from_dict from synapse.types import EventID, JsonDict from synapse.util import Clock from synapse.util.stringutils import random_string @@ -256,8 +252,8 @@ def create_local_event_from_event_dict( event_dict.setdefault("signatures", {}) add_hashes_and_signatures(room_version, event_dict, hostname, signing_key) - return event_type_from_format_version(format_version)( - event_dict, internal_metadata_dict=internal_metadata_dict + return make_event_from_dict( + event_dict, room_version, internal_metadata_dict=internal_metadata_dict ) diff --git a/synapse/federation/federation_base.py b/synapse/federation/federation_base.py index ebe8b8e9fe..eea64c1c9f 100644 --- a/synapse/federation/federation_base.py +++ b/synapse/federation/federation_base.py @@ -29,7 +29,7 @@ from synapse.api.room_versions import ( RoomVersion, ) from synapse.crypto.event_signing import check_event_content_hash -from synapse.events import EventBase, event_type_from_format_version +from synapse.events import EventBase, make_event_from_dict from synapse.events.utils import prune_event from synapse.http.servlet import assert_params_in_dict from synapse.logging.context import ( @@ -374,8 +374,7 @@ def event_from_pdu_json( elif depth > MAX_DEPTH: raise SynapseError(400, "Depth too large", Codes.BAD_JSON) - event = event_type_from_format_version(room_version.event_format)(pdu_json) - + event = make_event_from_dict(pdu_json, room_version) event.internal_metadata.outlier = outlier return event diff --git a/tests/api/test_filtering.py b/tests/api/test_filtering.py index 63d8633582..4e67503cf0 100644 --- a/tests/api/test_filtering.py +++ b/tests/api/test_filtering.py @@ -25,7 +25,7 @@ from twisted.internet import defer from synapse.api.constants import EventContentFields from synapse.api.errors import SynapseError from synapse.api.filtering import Filter -from synapse.events import FrozenEvent +from synapse.events import make_event_from_dict from tests import unittest from tests.utils import DeferredMockCallable, MockHttpResource, setup_test_homeserver @@ -38,7 +38,7 @@ def MockEvent(**kwargs): kwargs["event_id"] = "fake_event_id" if "type" not in kwargs: kwargs["type"] = "fake_type" - return FrozenEvent(kwargs) + return make_event_from_dict(kwargs) class FilteringTestCase(unittest.TestCase): diff --git a/tests/crypto/test_event_signing.py b/tests/crypto/test_event_signing.py index 6143a50ab2..62f639a18d 100644 --- a/tests/crypto/test_event_signing.py +++ b/tests/crypto/test_event_signing.py @@ -19,7 +19,7 @@ from unpaddedbase64 import decode_base64 from synapse.api.room_versions import RoomVersions from synapse.crypto.event_signing import add_hashes_and_signatures -from synapse.events import FrozenEvent +from synapse.events import make_event_from_dict from tests import unittest @@ -54,7 +54,7 @@ class EventSigningTestCase(unittest.TestCase): RoomVersions.V1, event_dict, HOSTNAME, self.signing_key ) - event = FrozenEvent(event_dict) + event = make_event_from_dict(event_dict) self.assertTrue(hasattr(event, "hashes")) self.assertIn("sha256", event.hashes) @@ -88,7 +88,7 @@ class EventSigningTestCase(unittest.TestCase): RoomVersions.V1, event_dict, HOSTNAME, self.signing_key ) - event = FrozenEvent(event_dict) + event = make_event_from_dict(event_dict) self.assertTrue(hasattr(event, "hashes")) self.assertIn("sha256", event.hashes) diff --git a/tests/events/test_utils.py b/tests/events/test_utils.py index 2b13980dfd..45d55b9e94 100644 --- a/tests/events/test_utils.py +++ b/tests/events/test_utils.py @@ -13,8 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. - -from synapse.events import FrozenEvent +from synapse.events import make_event_from_dict from synapse.events.utils import ( copy_power_levels_contents, prune_event, @@ -30,7 +29,7 @@ def MockEvent(**kwargs): kwargs["event_id"] = "fake_event_id" if "type" not in kwargs: kwargs["type"] = "fake_type" - return FrozenEvent(kwargs) + return make_event_from_dict(kwargs) class PruneEventTestCase(unittest.TestCase): @@ -38,7 +37,9 @@ class PruneEventTestCase(unittest.TestCase): `matchdict` when it is redacted. """ def run_test(self, evdict, matchdict): - self.assertEquals(prune_event(FrozenEvent(evdict)).get_dict(), matchdict) + self.assertEquals( + prune_event(make_event_from_dict(evdict)).get_dict(), matchdict + ) def test_minimal(self): self.run_test( diff --git a/tests/federation/test_federation_server.py b/tests/federation/test_federation_server.py index 1ec8c40901..e7d8699040 100644 --- a/tests/federation/test_federation_server.py +++ b/tests/federation/test_federation_server.py @@ -15,7 +15,7 @@ # limitations under the License. import logging -from synapse.events import FrozenEvent +from synapse.events import make_event_from_dict from synapse.federation.federation_server import server_matches_acl_event from synapse.rest import admin from synapse.rest.client.v1 import login, room @@ -105,7 +105,7 @@ class StateQueryTests(unittest.FederatingHomeserverTestCase): def _create_acl_event(content): - return FrozenEvent( + return make_event_from_dict( { "room_id": "!a:b", "event_id": "$a:b", diff --git a/tests/replication/slave/storage/test_events.py b/tests/replication/slave/storage/test_events.py index b1b037006d..d31210fbe4 100644 --- a/tests/replication/slave/storage/test_events.py +++ b/tests/replication/slave/storage/test_events.py @@ -15,7 +15,7 @@ import logging from canonicaljson import encode_canonical_json -from synapse.events import FrozenEvent, _EventInternalMetadata +from synapse.events import FrozenEvent, _EventInternalMetadata, make_event_from_dict from synapse.events.snapshot import EventContext from synapse.handlers.room import RoomEventSource from synapse.replication.slave.storage.events import SlavedEventStore @@ -90,7 +90,9 @@ class SlavedEventStoreTestCase(BaseSlavedStoreTestCase): msg_dict["content"] = {} msg_dict["unsigned"]["redacted_by"] = redaction.event_id msg_dict["unsigned"]["redacted_because"] = redaction - redacted = FrozenEvent(msg_dict, msg.internal_metadata.get_dict()) + redacted = make_event_from_dict( + msg_dict, internal_metadata_dict=msg.internal_metadata.get_dict() + ) self.check("get_event", [msg.event_id], redacted) def test_backfilled_redactions(self): @@ -110,7 +112,9 @@ class SlavedEventStoreTestCase(BaseSlavedStoreTestCase): msg_dict["content"] = {} msg_dict["unsigned"]["redacted_by"] = redaction.event_id msg_dict["unsigned"]["redacted_because"] = redaction - redacted = FrozenEvent(msg_dict, msg.internal_metadata.get_dict()) + redacted = make_event_from_dict( + msg_dict, internal_metadata_dict=msg.internal_metadata.get_dict() + ) self.check("get_event", [msg.event_id], redacted) def test_invites(self): @@ -345,7 +349,7 @@ class SlavedEventStoreTestCase(BaseSlavedStoreTestCase): if redacts is not None: event_dict["redacts"] = redacts - event = FrozenEvent(event_dict, internal_metadata_dict=internal) + event = make_event_from_dict(event_dict, internal_metadata_dict=internal) self.event_id += 1 diff --git a/tests/state/test_v2.py b/tests/state/test_v2.py index 0f341d3ac3..5bafad9f19 100644 --- a/tests/state/test_v2.py +++ b/tests/state/test_v2.py @@ -22,7 +22,7 @@ import attr from synapse.api.constants import EventTypes, JoinRules, Membership from synapse.api.room_versions import RoomVersions from synapse.event_auth import auth_types_for_event -from synapse.events import FrozenEvent +from synapse.events import make_event_from_dict from synapse.state.v2 import lexicographical_topological_sort, resolve_events_with_store from synapse.types import EventID @@ -89,7 +89,7 @@ class FakeEvent(object): if self.state_key is not None: event_dict["state_key"] = self.state_key - return FrozenEvent(event_dict) + return make_event_from_dict(event_dict) # All graphs start with this set of events diff --git a/tests/test_event_auth.py b/tests/test_event_auth.py index ca20b085a2..bfa5d6f510 100644 --- a/tests/test_event_auth.py +++ b/tests/test_event_auth.py @@ -18,7 +18,7 @@ import unittest from synapse import event_auth from synapse.api.errors import AuthError from synapse.api.room_versions import RoomVersions -from synapse.events import FrozenEvent +from synapse.events import make_event_from_dict class EventAuthTestCase(unittest.TestCase): @@ -94,7 +94,7 @@ TEST_ROOM_ID = "!test:room" def _create_event(user_id): - return FrozenEvent( + return make_event_from_dict( { "room_id": TEST_ROOM_ID, "event_id": _get_event_id(), @@ -106,7 +106,7 @@ def _create_event(user_id): def _join_event(user_id): - return FrozenEvent( + return make_event_from_dict( { "room_id": TEST_ROOM_ID, "event_id": _get_event_id(), @@ -119,7 +119,7 @@ def _join_event(user_id): def _power_levels_event(sender, content): - return FrozenEvent( + return make_event_from_dict( { "room_id": TEST_ROOM_ID, "event_id": _get_event_id(), @@ -132,7 +132,7 @@ def _power_levels_event(sender, content): def _random_state_event(sender): - return FrozenEvent( + return make_event_from_dict( { "room_id": TEST_ROOM_ID, "event_id": _get_event_id(), diff --git a/tests/test_federation.py b/tests/test_federation.py index 68684460c6..9b5cf562f3 100644 --- a/tests/test_federation.py +++ b/tests/test_federation.py @@ -2,7 +2,7 @@ from mock import Mock from twisted.internet.defer import ensureDeferred, maybeDeferred, succeed -from synapse.events import FrozenEvent +from synapse.events import make_event_from_dict from synapse.logging.context import LoggingContext from synapse.types import Requester, UserID from synapse.util import Clock @@ -43,7 +43,7 @@ class MessageAcceptTests(unittest.TestCase): ) )[0] - join_event = FrozenEvent( + join_event = make_event_from_dict( { "room_id": self.room_id, "sender": "@baduser:test.serv", @@ -105,7 +105,7 @@ class MessageAcceptTests(unittest.TestCase): )[0] # Now lie about an event - lying_event = FrozenEvent( + lying_event = make_event_from_dict( { "room_id": self.room_id, "sender": "@baduser:test.serv", diff --git a/tests/test_state.py b/tests/test_state.py index 1e4449fa1c..d1578fe581 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -20,7 +20,7 @@ from twisted.internet import defer from synapse.api.auth import Auth from synapse.api.constants import EventTypes, Membership from synapse.api.room_versions import RoomVersions -from synapse.events import FrozenEvent +from synapse.events import make_event_from_dict from synapse.events.snapshot import EventContext from synapse.state import StateHandler, StateResolutionHandler @@ -66,7 +66,7 @@ def create_event( d.update(kwargs) - event = FrozenEvent(d) + event = make_event_from_dict(d) return event From e1d858984d71b6edf56e1024f1475224bfa49054 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 7 Feb 2020 15:30:26 +0000 Subject: [PATCH 1006/1623] Remove unused `get_room_stats_state` method. (#6869) --- changelog.d/6869.misc | 1 + synapse/storage/data_stores/main/stats.py | 25 ----------------------- 2 files changed, 1 insertion(+), 25 deletions(-) create mode 100644 changelog.d/6869.misc diff --git a/changelog.d/6869.misc b/changelog.d/6869.misc new file mode 100644 index 0000000000..14f88f9bb7 --- /dev/null +++ b/changelog.d/6869.misc @@ -0,0 +1 @@ +Remove unused `get_room_stats_state` method. diff --git a/synapse/storage/data_stores/main/stats.py b/synapse/storage/data_stores/main/stats.py index 7af1495e47..380c1ec7da 100644 --- a/synapse/storage/data_stores/main/stats.py +++ b/synapse/storage/data_stores/main/stats.py @@ -271,31 +271,6 @@ class StatsStore(StateDeltasStore): return slice_list - def get_room_stats_state(self, room_id): - """ - Returns the current room_stats_state for a room. - - Args: - room_id (str): The ID of the room to return state for. - - Returns (dict): - Dictionary containing these keys: - "name", "topic", "canonical_alias", "avatar", "join_rules", - "history_visibility" - """ - return self.db.simple_select_one( - "room_stats_state", - {"room_id": room_id}, - retcols=( - "name", - "topic", - "canonical_alias", - "avatar", - "join_rules", - "history_visibility", - ), - ) - @cached() def get_earliest_token_for_stats(self, stats_type, id): """ From 21db35f77e4718cfe6d6b292baada9dd02ef8280 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 7 Feb 2020 15:45:39 +0000 Subject: [PATCH 1007/1623] Add support for putting fed user query API on workers (#6873) --- changelog.d/6873.feature | 1 + docs/workers.md | 1 + synapse/app/federation_reader.py | 2 ++ synapse/federation/federation_server.py | 7 +++-- synapse/handlers/device.py | 35 +++++++++++-------------- 5 files changed, 25 insertions(+), 21 deletions(-) create mode 100644 changelog.d/6873.feature diff --git a/changelog.d/6873.feature b/changelog.d/6873.feature new file mode 100644 index 0000000000..bbedf8f7f0 --- /dev/null +++ b/changelog.d/6873.feature @@ -0,0 +1 @@ +Add ability to route federation user device queries to workers. diff --git a/docs/workers.md b/docs/workers.md index 82442d6a0a..6f7ec58780 100644 --- a/docs/workers.md +++ b/docs/workers.md @@ -176,6 +176,7 @@ endpoints matching the following regular expressions: ^/_matrix/federation/v1/query_auth/ ^/_matrix/federation/v1/event_auth/ ^/_matrix/federation/v1/exchange_third_party_invite/ + ^/_matrix/federation/v1/user/devices/ ^/_matrix/federation/v1/send/ ^/_matrix/federation/v1/get_groups_publicised$ ^/_matrix/key/v2/query diff --git a/synapse/app/federation_reader.py b/synapse/app/federation_reader.py index 5e17ef1396..d055d11b23 100644 --- a/synapse/app/federation_reader.py +++ b/synapse/app/federation_reader.py @@ -33,6 +33,7 @@ from synapse.metrics import METRICS_PREFIX, MetricsResource, RegistryProxy from synapse.replication.slave.storage._base import BaseSlavedStore from synapse.replication.slave.storage.account_data import SlavedAccountDataStore from synapse.replication.slave.storage.appservice import SlavedApplicationServiceStore +from synapse.replication.slave.storage.devices import SlavedDeviceStore from synapse.replication.slave.storage.directory import DirectoryStore from synapse.replication.slave.storage.events import SlavedEventStore from synapse.replication.slave.storage.groups import SlavedGroupServerStore @@ -68,6 +69,7 @@ class FederationReaderSlavedStore( SlavedKeyStore, SlavedRegistrationStore, SlavedGroupServerStore, + SlavedDeviceStore, RoomStore, DirectoryStore, SlavedTransactionStore, diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index a6c966a393..7f9da49326 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -81,6 +81,8 @@ class FederationServer(FederationBase): self.handler = hs.get_handlers().federation_handler self.state = hs.get_state_handler() + self.device_handler = hs.get_device_handler() + self._server_linearizer = Linearizer("fed_server") self._transaction_linearizer = Linearizer("fed_txn_handler") @@ -523,8 +525,9 @@ class FederationServer(FederationBase): def on_query_client_keys(self, origin, content): return self.on_query_request("client_keys", content) - def on_query_user_devices(self, origin, user_id): - return self.on_query_request("user_devices", user_id) + async def on_query_user_devices(self, origin: str, user_id: str): + keys = await self.device_handler.on_federation_query_user_devices(user_id) + return 200, keys @trace async def on_claim_client_keys(self, origin, content): diff --git a/synapse/handlers/device.py b/synapse/handlers/device.py index a9bd431486..6d8e48ed39 100644 --- a/synapse/handlers/device.py +++ b/synapse/handlers/device.py @@ -225,6 +225,22 @@ class DeviceWorkerHandler(BaseHandler): return result + @defer.inlineCallbacks + def on_federation_query_user_devices(self, user_id): + stream_id, devices = yield self.store.get_devices_with_keys_by_user(user_id) + master_key = yield self.store.get_e2e_cross_signing_key(user_id, "master") + self_signing_key = yield self.store.get_e2e_cross_signing_key( + user_id, "self_signing" + ) + + return { + "user_id": user_id, + "stream_id": stream_id, + "devices": devices, + "master_key": master_key, + "self_signing_key": self_signing_key, + } + class DeviceHandler(DeviceWorkerHandler): def __init__(self, hs): @@ -239,9 +255,6 @@ class DeviceHandler(DeviceWorkerHandler): federation_registry.register_edu_handler( "m.device_list_update", self.device_list_updater.incoming_device_list_update ) - federation_registry.register_query_handler( - "user_devices", self.on_federation_query_user_devices - ) hs.get_distributor().observe("user_left_room", self.user_left_room) @@ -456,22 +469,6 @@ class DeviceHandler(DeviceWorkerHandler): self.notifier.on_new_event("device_list_key", position, users=[from_user_id]) - @defer.inlineCallbacks - def on_federation_query_user_devices(self, user_id): - stream_id, devices = yield self.store.get_devices_with_keys_by_user(user_id) - master_key = yield self.store.get_e2e_cross_signing_key(user_id, "master") - self_signing_key = yield self.store.get_e2e_cross_signing_key( - user_id, "self_signing" - ) - - return { - "user_id": user_id, - "stream_id": stream_id, - "devices": devices, - "master_key": master_key, - "self_signing_key": self_signing_key, - } - @defer.inlineCallbacks def user_left_room(self, user, room_id): user_id = user.to_string() From fe73f0d533dcdcf11f069d89ebbff2ce88d16bb3 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 10 Feb 2020 00:41:20 +0000 Subject: [PATCH 1008/1623] Update setuptools for python 3.5 tests (#6880) Workaround for jaraco/zipp#40 --- .buildkite/scripts/test_old_deps.sh | 18 ++++++++++++++++++ changelog.d/6880.misc | 1 + 2 files changed, 19 insertions(+) create mode 100755 .buildkite/scripts/test_old_deps.sh create mode 100644 changelog.d/6880.misc diff --git a/.buildkite/scripts/test_old_deps.sh b/.buildkite/scripts/test_old_deps.sh new file mode 100755 index 0000000000..dfd71b2511 --- /dev/null +++ b/.buildkite/scripts/test_old_deps.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +# this script is run by buildkite in a plain `xenial` container; it installs the +# minimal requirements for tox and hands over to the py35-old tox environment. + +set -ex + +apt-get update +apt-get install -y python3.5 python3.5-dev python3-pip libxml2-dev libxslt-dev zlib1g-dev + +# workaround for https://github.com/jaraco/zipp/issues/40 +python3.5 -m pip install 'setuptools>=34.4.0' + +python3.5 -m pip install tox + +export LANG="C.UTF-8" + +exec tox -e py35-old,combine diff --git a/changelog.d/6880.misc b/changelog.d/6880.misc new file mode 100644 index 0000000000..8344a6ed1e --- /dev/null +++ b/changelog.d/6880.misc @@ -0,0 +1 @@ +Fix continuous integration failures with old versions of `pip`, which were introduced by a release of the `zipp` library. From 8e64c5a24c26a733c0cfd3e997ea4079ae457096 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 10 Feb 2020 09:36:23 +0000 Subject: [PATCH 1009/1623] filter out m.room.aliases from the CS API until a better solution is specced (#6878) We're in the middle of properly mitigating spam caused by malicious aliases being added to a room. However, until this work fully lands, we temporarily filter out all m.room.aliases events from /sync and /messages on the CS API, to remove abusive aliases. This is considered acceptable as m.room.aliases events were never a reliable record of the given alias->id mapping and were purely informational, and in their current state do more harm than good. --- changelog.d/6878.feature | 1 + synapse/visibility.py | 7 +++++++ 2 files changed, 8 insertions(+) create mode 100644 changelog.d/6878.feature diff --git a/changelog.d/6878.feature b/changelog.d/6878.feature new file mode 100644 index 0000000000..af3e958a43 --- /dev/null +++ b/changelog.d/6878.feature @@ -0,0 +1 @@ +Filter out m.room.aliases from the CS API to mitigate abuse while a better solution is specced. diff --git a/synapse/visibility.py b/synapse/visibility.py index 100dc47a8a..d0abd8f04f 100644 --- a/synapse/visibility.py +++ b/synapse/visibility.py @@ -122,6 +122,13 @@ def filter_events_for_client( if not event.is_state() and event.sender in ignore_list: return None + # Until MSC2261 has landed we can't redact malicious alias events, so for + # now we temporarily filter out m.room.aliases entirely to mitigate + # abuse, while we spec a better solution to advertising aliases + # on rooms. + if event.type == EventTypes.Aliases: + return None + # Don't try to apply the room's retention policy if the event is a state event, as # MSC1763 states that retention is only considered for non-state events. if apply_retention_policies and not event.is_state(): From 3de57e706209d98a331265e6d5a51bfd24939a3b Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 10 Feb 2020 09:56:42 +0000 Subject: [PATCH 1010/1623] 1.10.0rc3 --- CHANGES.md | 15 +++++++++++++++ changelog.d/6878.feature | 1 - changelog.d/6880.misc | 1 - synapse/__init__.py | 2 +- 4 files changed, 16 insertions(+), 3 deletions(-) delete mode 100644 changelog.d/6878.feature delete mode 100644 changelog.d/6880.misc diff --git a/CHANGES.md b/CHANGES.md index c2aa735908..4a81a04627 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,18 @@ +Synapse 1.10.0rc3 (2020-02-10) +============================== + +Features +-------- + +- Filter out m.room.aliases from the CS API to mitigate abuse while a better solution is specced. ([\#6878](https://github.com/matrix-org/synapse/issues/6878)) + + +Internal Changes +---------------- + +- Fix continuous integration failures with old versions of `pip`, which were introduced by a release of the `zipp` library. ([\#6880](https://github.com/matrix-org/synapse/issues/6880)) + + Synapse 1.10.0rc2 (2020-02-06) ============================== diff --git a/changelog.d/6878.feature b/changelog.d/6878.feature deleted file mode 100644 index af3e958a43..0000000000 --- a/changelog.d/6878.feature +++ /dev/null @@ -1 +0,0 @@ -Filter out m.room.aliases from the CS API to mitigate abuse while a better solution is specced. diff --git a/changelog.d/6880.misc b/changelog.d/6880.misc deleted file mode 100644 index 8344a6ed1e..0000000000 --- a/changelog.d/6880.misc +++ /dev/null @@ -1 +0,0 @@ -Fix continuous integration failures with old versions of `pip`, which were introduced by a release of the `zipp` library. diff --git a/synapse/__init__.py b/synapse/__init__.py index 4f1859bd57..36c0cf557a 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -36,7 +36,7 @@ try: except ImportError: pass -__version__ = "1.10.0rc2" +__version__ = "1.10.0rc3" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 3a3118f4ecb631dec3cc44a928a3666b734f5dcb Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Mon, 10 Feb 2020 11:47:18 -0500 Subject: [PATCH 1011/1623] Add an additional test to the SyTest blacklist for worker mode. (#6883) --- .buildkite/worker-blacklist | 2 ++ changelog.d/6883.misc | 1 + 2 files changed, 3 insertions(+) create mode 100644 changelog.d/6883.misc diff --git a/.buildkite/worker-blacklist b/.buildkite/worker-blacklist index 158ab79154..094b6c94da 100644 --- a/.buildkite/worker-blacklist +++ b/.buildkite/worker-blacklist @@ -39,3 +39,5 @@ Server correctly handles incoming m.device_list_update # this fails reliably with a torture level of 100 due to https://github.com/matrix-org/synapse/issues/6536 Outbound federation requests missing prev_events and then asks for /state_ids and resolves the state + +Can get rooms/{roomId}/members at a given point diff --git a/changelog.d/6883.misc b/changelog.d/6883.misc new file mode 100644 index 0000000000..e0837d7987 --- /dev/null +++ b/changelog.d/6883.misc @@ -0,0 +1 @@ +Add an additional entry to the SyTest blacklist for worker mode. From 01209382fbb5ccf7bdef71f1d42f754b125f625a Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 10 Feb 2020 18:07:35 +0000 Subject: [PATCH 1012/1623] filter out m.room.aliases from /sync state blocks (#6884) We forgot to filter out aliases from /sync state blocks as well as the timeline. --- changelog.d/6884.feature | 1 + synapse/handlers/sync.py | 1 + 2 files changed, 2 insertions(+) create mode 100644 changelog.d/6884.feature diff --git a/changelog.d/6884.feature b/changelog.d/6884.feature new file mode 100644 index 0000000000..4f6b9630b8 --- /dev/null +++ b/changelog.d/6884.feature @@ -0,0 +1 @@ +Filter out m.room.aliases from /sync state blocks until a full fix lands. diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index cd95f85e3f..2b62fd83fd 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -883,6 +883,7 @@ class SyncHandler(object): for e in sync_config.filter_collection.filter_room_state( list(state.values()) ) + if e.type != EventTypes.Aliases # until MSC2261 or alternative solution } async def unread_notifs_for_room_id(self, room_id, sync_config): From a92e703ab9d78aecc062e797f941bb7e206650a5 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Mon, 10 Feb 2020 16:35:26 -0500 Subject: [PATCH 1013/1623] Reject device display names that are too long (#6882) * Reject device display names that are too long. Too long is currently defined as 100 characters in length. * Add a regression test for rejecting a too long device display name. --- changelog.d/6882.misc | 1 + synapse/handlers/device.py | 14 +++++++++++++- tests/handlers/test_device.py | 18 ++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 changelog.d/6882.misc diff --git a/changelog.d/6882.misc b/changelog.d/6882.misc new file mode 100644 index 0000000000..e8382e36ae --- /dev/null +++ b/changelog.d/6882.misc @@ -0,0 +1 @@ +Reject device display names over 100 characters in length. diff --git a/synapse/handlers/device.py b/synapse/handlers/device.py index 6d8e48ed39..50cea3f378 100644 --- a/synapse/handlers/device.py +++ b/synapse/handlers/device.py @@ -26,6 +26,7 @@ from synapse.api.errors import ( FederationDeniedError, HttpResponseException, RequestSendFailed, + SynapseError, ) from synapse.logging.opentracing import log_kv, set_tag, trace from synapse.types import RoomStreamToken, get_domain_from_id @@ -39,6 +40,8 @@ from ._base import BaseHandler logger = logging.getLogger(__name__) +MAX_DEVICE_DISPLAY_NAME_LEN = 100 + class DeviceWorkerHandler(BaseHandler): def __init__(self, hs): @@ -404,9 +407,18 @@ class DeviceHandler(DeviceWorkerHandler): defer.Deferred: """ + # Reject a new displayname which is too long. + new_display_name = content.get("display_name") + if new_display_name and len(new_display_name) > MAX_DEVICE_DISPLAY_NAME_LEN: + raise SynapseError( + 400, + "Device display name is too long (max %i)" + % (MAX_DEVICE_DISPLAY_NAME_LEN,), + ) + try: yield self.store.update_device( - user_id, device_id, new_display_name=content.get("display_name") + user_id, device_id, new_display_name=new_display_name ) yield self.notify_device_update(user_id, [device_id]) except errors.StoreError as e: diff --git a/tests/handlers/test_device.py b/tests/handlers/test_device.py index a3aa0a1cf2..62b47f6574 100644 --- a/tests/handlers/test_device.py +++ b/tests/handlers/test_device.py @@ -160,6 +160,24 @@ class DeviceTestCase(unittest.HomeserverTestCase): res = self.get_success(self.handler.get_device(user1, "abc")) self.assertEqual(res["display_name"], "new display") + def test_update_device_too_long_display_name(self): + """Update a device with a display name that is invalid (too long).""" + self._record_users() + + # Request to update a device display name with a new value that is longer than allowed. + update = { + "display_name": "a" + * (synapse.handlers.device.MAX_DEVICE_DISPLAY_NAME_LEN + 1) + } + self.get_failure( + self.handler.update_device(user1, "abc", update), + synapse.api.errors.SynapseError, + ) + + # Ensure the display name was not updated. + res = self.get_success(self.handler.get_device(user1, "abc")) + self.assertEqual(res["display_name"], "display 2") + def test_update_unknown_device(self): update = {"display_name": "new_display"} res = self.handler.update_device("user_id", "unknown_device_id", update) From 3edc65dd24f1916501e24e2353b4523bc2e3635c Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 11 Feb 2020 10:43:16 +0000 Subject: [PATCH 1014/1623] 1.10.0rc4 --- CHANGES.md | 9 +++++++++ changelog.d/6884.feature | 1 - synapse/__init__.py | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) delete mode 100644 changelog.d/6884.feature diff --git a/CHANGES.md b/CHANGES.md index 4a81a04627..f776560de7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,12 @@ +Synapse 1.10.0rc4 (2020-02-11) +============================== + +Features +-------- + +- Filter out m.room.aliases from /sync state blocks until a full fix lands. ([\#6884](https://github.com/matrix-org/synapse/issues/6884)) + + Synapse 1.10.0rc3 (2020-02-10) ============================== diff --git a/changelog.d/6884.feature b/changelog.d/6884.feature deleted file mode 100644 index 4f6b9630b8..0000000000 --- a/changelog.d/6884.feature +++ /dev/null @@ -1 +0,0 @@ -Filter out m.room.aliases from /sync state blocks until a full fix lands. diff --git a/synapse/__init__.py b/synapse/__init__.py index 36c0cf557a..cc69ff0c41 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -36,7 +36,7 @@ try: except ImportError: pass -__version__ = "1.10.0rc3" +__version__ = "1.10.0rc4" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 9e45d573d499796bd1878d026cdd820347c10c00 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 11 Feb 2020 10:50:55 +0000 Subject: [PATCH 1015/1623] changelog formatting --- CHANGES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index f776560de7..8f252bb1b2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,7 +4,7 @@ Synapse 1.10.0rc4 (2020-02-11) Features -------- -- Filter out m.room.aliases from /sync state blocks until a full fix lands. ([\#6884](https://github.com/matrix-org/synapse/issues/6884)) +- Filter out `m.room.aliase`s from `/sync` state blocks until a full fix lands. ([\#6884](https://github.com/matrix-org/synapse/issues/6884)) Synapse 1.10.0rc3 (2020-02-10) @@ -13,7 +13,7 @@ Synapse 1.10.0rc3 (2020-02-10) Features -------- -- Filter out m.room.aliases from the CS API to mitigate abuse while a better solution is specced. ([\#6878](https://github.com/matrix-org/synapse/issues/6878)) +- Filter out `m.room.aliases` from the CS API to mitigate abuse while a better solution is specced. ([\#6878](https://github.com/matrix-org/synapse/issues/6878)) Internal Changes From aa7e4291ee55f4c1d817bfb4bf87930ae2f40aab Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 11 Feb 2020 10:51:13 +0000 Subject: [PATCH 1016/1623] Update CHANGES.md --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 8f252bb1b2..4248b7065b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,7 +4,7 @@ Synapse 1.10.0rc4 (2020-02-11) Features -------- -- Filter out `m.room.aliase`s from `/sync` state blocks until a full fix lands. ([\#6884](https://github.com/matrix-org/synapse/issues/6884)) +- Filter out `m.room.aliases` from `/sync` state blocks until a full fix lands. ([\#6884](https://github.com/matrix-org/synapse/issues/6884)) Synapse 1.10.0rc3 (2020-02-10) From 78d170262c7349cbb85baa4ad0749ccb5e2794e4 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 11 Feb 2020 10:53:25 +0000 Subject: [PATCH 1017/1623] changelog wording --- CHANGES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 4248b7065b..ab031fccdc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,10 +1,10 @@ Synapse 1.10.0rc4 (2020-02-11) ============================== -Features +Bugfixes -------- -- Filter out `m.room.aliases` from `/sync` state blocks until a full fix lands. ([\#6884](https://github.com/matrix-org/synapse/issues/6884)) +- Fix the filtering introduced in 1.10.0rc3 to also apply to the state blocks returned by `/sync`. ([\#6884](https://github.com/matrix-org/synapse/issues/6884)) Synapse 1.10.0rc3 (2020-02-10) From 856b2a9555b6c5a70b98ad2a1e14b7dbee0949f3 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 11 Feb 2020 11:06:28 +0000 Subject: [PATCH 1018/1623] 1.10.0rc5 --- CHANGES.md | 6 +++++- synapse/__init__.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index ab031fccdc..1995a70b19 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,4 @@ -Synapse 1.10.0rc4 (2020-02-11) +Synapse 1.10.0rc5 (2020-02-11) ============================== Bugfixes @@ -6,6 +6,10 @@ Bugfixes - Fix the filtering introduced in 1.10.0rc3 to also apply to the state blocks returned by `/sync`. ([\#6884](https://github.com/matrix-org/synapse/issues/6884)) +Synapse 1.10.0rc4 (2020-02-11) +============================== + +This release candidate was built incorrectly and is superceded by 1.10.0rc5. Synapse 1.10.0rc3 (2020-02-10) ============================== diff --git a/synapse/__init__.py b/synapse/__init__.py index cc69ff0c41..ba339004ba 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -36,7 +36,7 @@ try: except ImportError: pass -__version__ = "1.10.0rc4" +__version__ = "1.10.0rc5" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From a443d2a25dc3cd99519c6b9a3a326545a0b2f933 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 11 Feb 2020 17:37:09 +0000 Subject: [PATCH 1019/1623] Spell out that Synapse never purges the last event sent in a room --- docs/message_retention_policies.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/message_retention_policies.md b/docs/message_retention_policies.md index 4300809dfe..f2e2794252 100644 --- a/docs/message_retention_policies.md +++ b/docs/message_retention_policies.md @@ -42,6 +42,10 @@ purged according to its room's policy, then the receiving server will process and store that event until it's picked up by the next purge job, though it will always hide it from clients. +With the current implementation of this feature, in order not to break +rooms, Synapse will never delete the last message sent to a room, and +will only hide it from clients. + ## Server configuration From 705c978366146e85280af0dcf216e78d521352a3 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 11 Feb 2020 17:38:27 +0000 Subject: [PATCH 1020/1623] Changelog --- changelog.d/6891.doc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6891.doc diff --git a/changelog.d/6891.doc b/changelog.d/6891.doc new file mode 100644 index 0000000000..f3afbbccda --- /dev/null +++ b/changelog.d/6891.doc @@ -0,0 +1 @@ +Spell out that the last event sent to a room won't be deleted by the purge jobs for the message retention policies support. From 6b21986e4ee999eb3669ec90f6db3bdfa7ce71a1 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 11 Feb 2020 17:56:04 +0000 Subject: [PATCH 1021/1623] Also spell it out in the purge history API doc --- docs/admin_api/purge_history_api.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/admin_api/purge_history_api.rst b/docs/admin_api/purge_history_api.rst index f7be226fd9..f2c4dc03ac 100644 --- a/docs/admin_api/purge_history_api.rst +++ b/docs/admin_api/purge_history_api.rst @@ -8,6 +8,9 @@ Depending on the amount of history being purged a call to the API may take several minutes or longer. During this period users will not be able to paginate further back in the room from the point being purged from. +Note that, in order to not break the room, this API won't delete the last +message sent to it. + The API is: ``POST /_synapse/admin/v1/purge_history/[/]`` From a0c4769f1ae6d6aaf1a548b869f857836efbfb18 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 11 Feb 2020 17:56:42 +0000 Subject: [PATCH 1022/1623] Update the changelog file --- changelog.d/6891.doc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/6891.doc b/changelog.d/6891.doc index f3afbbccda..2f46c385b7 100644 --- a/changelog.d/6891.doc +++ b/changelog.d/6891.doc @@ -1 +1 @@ -Spell out that the last event sent to a room won't be deleted by the purge jobs for the message retention policies support. +Spell out that the last event sent to a room won't be deleted by a purge. From ba547ec3a94b17cfb634758deb4cdbc98fc840a9 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 12 Feb 2020 07:02:19 -0500 Subject: [PATCH 1023/1623] Use BSD-compatible in-place editing for sed. (#6887) --- changelog.d/6887.misc | 1 + scripts-dev/config-lint.sh | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 changelog.d/6887.misc diff --git a/changelog.d/6887.misc b/changelog.d/6887.misc new file mode 100644 index 0000000000..b351d47c7b --- /dev/null +++ b/changelog.d/6887.misc @@ -0,0 +1 @@ +Fix the use of sed in the linting scripts when using BSD sed. diff --git a/scripts-dev/config-lint.sh b/scripts-dev/config-lint.sh index 677a854c85..189ca66535 100755 --- a/scripts-dev/config-lint.sh +++ b/scripts-dev/config-lint.sh @@ -3,7 +3,8 @@ # Exits with 0 if there are no problems, or another code otherwise. # Fix non-lowercase true/false values -sed -i -E "s/: +True/: true/g; s/: +False/: false/g;" docs/sample_config.yaml +sed -i.bak -E "s/: +True/: true/g; s/: +False/: false/g;" docs/sample_config.yaml +rm docs/sample_config.yaml.bak # Check if anything changed git diff --exit-code docs/sample_config.yaml From 3dd2b5f5e313c0350a1a64f1d5e40c8bdc46029c Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 12 Feb 2020 12:02:53 +0000 Subject: [PATCH 1024/1623] bump the version of Alpine Linux used in the docker images (#6897) --- changelog.d/6897.docker | 1 + docker/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/6897.docker diff --git a/changelog.d/6897.docker b/changelog.d/6897.docker new file mode 100644 index 0000000000..8e7bcd719a --- /dev/null +++ b/changelog.d/6897.docker @@ -0,0 +1 @@ +Update the docker images to Alpine Linux 3.11. diff --git a/docker/Dockerfile b/docker/Dockerfile index e5a0d6d5f6..93d61739ae 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -16,7 +16,7 @@ ARG PYTHON_VERSION=3.7 ### ### Stage 0: builder ### -FROM docker.io/python:${PYTHON_VERSION}-alpine3.10 as builder +FROM docker.io/python:${PYTHON_VERSION}-alpine3.11 as builder # install the OS build deps From fdb816713aacb295fe804290f5043bdac866dcf5 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 12 Feb 2020 12:19:19 +0000 Subject: [PATCH 1025/1623] 1.10.0 --- CHANGES.md | 9 +++++++++ changelog.d/6897.docker | 1 - debian/changelog | 6 ++++++ synapse/__init__.py | 2 +- 4 files changed, 16 insertions(+), 2 deletions(-) delete mode 100644 changelog.d/6897.docker diff --git a/CHANGES.md b/CHANGES.md index 1995a70b19..603a50054e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,12 @@ +Synapse 1.10.0 (2020-02-12) +=========================== + +Updates to the Docker image +--------------------------- + +- Update the docker images to Alpine Linux 3.11. ([\#6897](https://github.com/matrix-org/synapse/issues/6897)) + + Synapse 1.10.0rc5 (2020-02-11) ============================== diff --git a/changelog.d/6897.docker b/changelog.d/6897.docker deleted file mode 100644 index 8e7bcd719a..0000000000 --- a/changelog.d/6897.docker +++ /dev/null @@ -1 +0,0 @@ -Update the docker images to Alpine Linux 3.11. diff --git a/debian/changelog b/debian/changelog index 74eb29c5ee..cdc3b1a5c2 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.10.0) stable; urgency=medium + + * New synapse release 1.10.0. + + -- Synapse Packaging team Wed, 12 Feb 2020 12:18:54 +0000 + matrix-synapse-py3 (1.9.1) stable; urgency=medium * New synapse release 1.9.1. diff --git a/synapse/__init__.py b/synapse/__init__.py index ba339004ba..9d285fca38 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -36,7 +36,7 @@ try: except ImportError: pass -__version__ = "1.10.0rc5" +__version__ = "1.10.0" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 7b8d654a6196d889c8e1c2a403f5176650216432 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 12 Feb 2020 12:20:37 +0000 Subject: [PATCH 1026/1623] Move the warning at the top of the release changes --- CHANGES.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 603a50054e..0bce84f400 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,8 @@ Synapse 1.10.0 (2020-02-12) =========================== +**WARNING to client developers**: As of this release Synapse validates `client_secret` parameters in the Client-Server API as per the spec. See [\#6766](https://github.com/matrix-org/synapse/issues/6766) for details. + Updates to the Docker image --------------------------- @@ -54,9 +56,6 @@ Internal Changes Synapse 1.10.0rc1 (2020-01-31) ============================== -**WARNING to client developers**: As of this release Synapse validates `client_secret` parameters in the Client-Server API as per the spec. See [\#6766](https://github.com/matrix-org/synapse/issues/6766) for details. - - Features -------- From 08e050c3fddb35cc54f6e0704fa9b54128dddc39 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 12 Feb 2020 15:39:40 +0000 Subject: [PATCH 1027/1623] Rephrase --- docs/admin_api/purge_history_api.rst | 4 ++-- docs/message_retention_policies.md | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/admin_api/purge_history_api.rst b/docs/admin_api/purge_history_api.rst index f2c4dc03ac..e2a620c54f 100644 --- a/docs/admin_api/purge_history_api.rst +++ b/docs/admin_api/purge_history_api.rst @@ -8,8 +8,8 @@ Depending on the amount of history being purged a call to the API may take several minutes or longer. During this period users will not be able to paginate further back in the room from the point being purged from. -Note that, in order to not break the room, this API won't delete the last -message sent to it. +Note that Synapse requires at least one message in each room, so it will never +delete the last message in a room. The API is: diff --git a/docs/message_retention_policies.md b/docs/message_retention_policies.md index f2e2794252..1dd60bdad9 100644 --- a/docs/message_retention_policies.md +++ b/docs/message_retention_policies.md @@ -42,9 +42,9 @@ purged according to its room's policy, then the receiving server will process and store that event until it's picked up by the next purge job, though it will always hide it from clients. -With the current implementation of this feature, in order not to break -rooms, Synapse will never delete the last message sent to a room, and -will only hide it from clients. +Synapse requires at least one message in each room, so it will never +delete the last message in a room. It will, however, hide it from +clients. ## Server configuration From d8994942f28f5028e560f6aba52512fae3ca1a6a Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Wed, 12 Feb 2020 18:14:10 +0000 Subject: [PATCH 1028/1623] Return a 404 for admin api user lookup if user not found (#6901) --- changelog.d/6901.misc | 1 + synapse/rest/admin/users.py | 5 ++++- tests/rest/admin/test_user.py | 16 ++++++++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 changelog.d/6901.misc diff --git a/changelog.d/6901.misc b/changelog.d/6901.misc new file mode 100644 index 0000000000..b2f12bbe86 --- /dev/null +++ b/changelog.d/6901.misc @@ -0,0 +1 @@ +Return a 404 instead of 200 for querying information of a non-existant user through the admin API. \ No newline at end of file diff --git a/synapse/rest/admin/users.py b/synapse/rest/admin/users.py index e75c5f1370..2107b5dc56 100644 --- a/synapse/rest/admin/users.py +++ b/synapse/rest/admin/users.py @@ -21,7 +21,7 @@ from six import text_type from six.moves import http_client from synapse.api.constants import UserTypes -from synapse.api.errors import Codes, SynapseError +from synapse.api.errors import Codes, NotFoundError, SynapseError from synapse.http.servlet import ( RestServlet, assert_params_in_dict, @@ -152,6 +152,9 @@ class UserRestServletV2(RestServlet): ret = await self.admin_handler.get_user(target_user) + if not ret: + raise NotFoundError("User not found") + return 200, ret async def on_PUT(self, request, user_id): diff --git a/tests/rest/admin/test_user.py b/tests/rest/admin/test_user.py index 3b5169b38d..490ce8f55d 100644 --- a/tests/rest/admin/test_user.py +++ b/tests/rest/admin/test_user.py @@ -401,6 +401,22 @@ class UserRestTestCase(unittest.HomeserverTestCase): self.assertEqual(403, int(channel.result["code"]), msg=channel.result["body"]) self.assertEqual("You are not a server admin", channel.json_body["error"]) + def test_user_does_not_exist(self): + """ + Tests that a lookup for a user that does not exist returns a 404 + """ + self.hs.config.registration_shared_secret = None + + request, channel = self.make_request( + "GET", + "/_synapse/admin/v2/users/@unknown_person:test", + access_token=self.admin_user_tok, + ) + self.render(request) + + self.assertEqual(404, channel.code, msg=channel.json_body) + self.assertEqual("M_NOT_FOUND", channel.json_body["errcode"]) + def test_requester_is_admin(self): """ If the user is a server admin, a new user is created. From f092029d2ddc333d557c3551ebc443d59221433c Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 12 Feb 2020 20:14:16 +0000 Subject: [PATCH 1029/1623] Update ACME.md to mention ACME v1 deprecation --- docs/ACME.md | 48 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/docs/ACME.md b/docs/ACME.md index 9eb18a9cf5..6d06cae3b3 100644 --- a/docs/ACME.md +++ b/docs/ACME.md @@ -1,12 +1,46 @@ # ACME -Synapse v1.0 will require valid TLS certificates for communication between -servers (port `8448` by default) in addition to those that are client-facing -(port `443`). If you do not already have a valid certificate for your domain, -the easiest way to get one is with Synapse's new ACME support, which will use -the ACME protocol to provision a certificate automatically. Synapse v0.99.0+ -will provision server-to-server certificates automatically for you for free -through [Let's Encrypt](https://letsencrypt.org/) if you tell it to. +From version 1.0 (June 2019) onwards, Synapse requires valid TLS +certificates for communication between servers (by default on port +`8448`) in addition to those that are client-facing (port `443`). To +help homeserver admins fulfil this new requirement, Synapse v0.99.0 +introduced support for automatically provisioning certificates through +[Let's Encrypt](https://letsencrypt.org/) using the ACME protocol. + +## Deprecation of ACME v1 + +In [March 2019](https://community.letsencrypt.org/t/end-of-life-plan-for-acmev1/88430), +Let's Encrypt announced that they were deprecating version 1 of the ACME +protocol, with the plan to disable the use of it for new accounts in +November 2019, and for existing accounts in June 2020. + +Synapse doesn't currently support version 2 of the ACME protocol, which +means that: + +* for existing installs, Synapse's built-in ACME support will continue + to work until June 2020. +* for new installs, this feature will not work at all. + +Either way, it is recommended to move from Synapse's ACME support +feature to an external automated tool such as [certbot](https://github.com/certbot/certbot) +(or browse [this list](https://letsencrypt.org/fr/docs/client-options/) +for an alternative ACME client). + +It's also recommended to use a reverse proxy for the server-facing +communications (mode documentation about this can be found +[here](/docs/reverse_proxy.md)) as well as the client-facing ones and +have it serve the certificates. + +In case you can't do that and need Synapse to serve them itself, make +sure to set the `tls_certificate_path` configuration setting to the path +of the certificate (make sure to use the certificate containing the full +certification chain, e.g. `fullchain.pem` if using certbot) and +`tls_private_key_path` to the path of the matching private key. + +If you still want to use Synapse's built-in ACME support, the rest of +this document explains how to set it up. + +## Initial setup In the case that your `server_name` config variable is the same as the hostname that the client connects to, then the same certificate can be From e45a7c09396c56d6ca7e3f42827cc354a942ba5d Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 12 Feb 2020 20:14:59 +0000 Subject: [PATCH 1030/1623] Remove duplicated info about certbot et al --- docs/ACME.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs/ACME.md b/docs/ACME.md index 6d06cae3b3..3b4416efe1 100644 --- a/docs/ACME.md +++ b/docs/ACME.md @@ -66,11 +66,6 @@ If you already have certificates, you will need to back up or delete them (files `example.com.tls.crt` and `example.com.tls.key` in Synapse's root directory), Synapse's ACME implementation will not overwrite them. -You may wish to use alternate methods such as Certbot to obtain a certificate -from Let's Encrypt, depending on your server configuration. Of course, if you -already have a valid certificate for your homeserver's domain, that can be -placed in Synapse's config directory without the need for any ACME setup. - ## ACME setup The main steps for enabling ACME support in short summary are: From e88a5dd108f607c9ec99356a2601147e41a20533 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 12 Feb 2020 20:15:41 +0000 Subject: [PATCH 1031/1623] Changelog --- changelog.d/6905.doc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6905.doc diff --git a/changelog.d/6905.doc b/changelog.d/6905.doc new file mode 100644 index 0000000000..ca10b38301 --- /dev/null +++ b/changelog.d/6905.doc @@ -0,0 +1 @@ +Mention in `ACME.md` that ACMEv1 is deprecated and explain what it means for Synapse admins. From 459d089af7e90a703df9637a071e9285bf85eb12 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 12 Feb 2020 21:05:30 +0000 Subject: [PATCH 1032/1623] Mention that using Synapse to serve certificates requires restarts --- docs/ACME.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/ACME.md b/docs/ACME.md index 3b4416efe1..97ac3c5ba3 100644 --- a/docs/ACME.md +++ b/docs/ACME.md @@ -35,7 +35,9 @@ In case you can't do that and need Synapse to serve them itself, make sure to set the `tls_certificate_path` configuration setting to the path of the certificate (make sure to use the certificate containing the full certification chain, e.g. `fullchain.pem` if using certbot) and -`tls_private_key_path` to the path of the matching private key. +`tls_private_key_path` to the path of the matching private key. Note +that in this case you will need to restart Synapse after each +certificate renewal so that Synapse stops using the old certificate. If you still want to use Synapse's built-in ACME support, the rest of this document explains how to set it up. From 862669d6cc802ff610de6f6df644ef2a6706abb3 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 13 Feb 2020 11:29:08 +0000 Subject: [PATCH 1033/1623] Update docs/ACME.md --- docs/ACME.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ACME.md b/docs/ACME.md index 97ac3c5ba3..f4c4740476 100644 --- a/docs/ACME.md +++ b/docs/ACME.md @@ -27,7 +27,7 @@ feature to an external automated tool such as [certbot](https://github.com/certb for an alternative ACME client). It's also recommended to use a reverse proxy for the server-facing -communications (mode documentation about this can be found +communications (more documentation about this can be found [here](/docs/reverse_proxy.md)) as well as the client-facing ones and have it serve the certificates. From dc3f9987061e24bec77ad9e26a7679a1907cee4f Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Thu, 13 Feb 2020 06:02:32 -0600 Subject: [PATCH 1034/1623] Remove m.lazy_load_members from unstable features since it is in CS r0.5.0 (#6877) Fixes #5528 --- changelog.d/6877.removal | 1 + synapse/rest/client/versions.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 changelog.d/6877.removal diff --git a/changelog.d/6877.removal b/changelog.d/6877.removal new file mode 100644 index 0000000000..9545e31fbe --- /dev/null +++ b/changelog.d/6877.removal @@ -0,0 +1 @@ +Remove `m.lazy_load_members` from `unstable_features` since lazy loading is in the stable Client-Server API version r0.5.0. diff --git a/synapse/rest/client/versions.py b/synapse/rest/client/versions.py index 3d0fefb4df..3eeb3607f4 100644 --- a/synapse/rest/client/versions.py +++ b/synapse/rest/client/versions.py @@ -52,7 +52,6 @@ class VersionsRestServlet(RestServlet): ], # as per MSC1497: "unstable_features": { - "m.lazy_load_members": True, # as per MSC2190, as amended by MSC2264 # to be removed in r0.6.0 "m.id_access_token": True, From 361de49c90fd1f35adc4a6bca8206e50e7f15454 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Thu, 13 Feb 2020 07:40:57 -0500 Subject: [PATCH 1035/1623] Add documentation for the spam checker module (#6906) Add documentation for the spam checker. --- changelog.d/6906.doc | 1 + docs/spam_checker.md | 85 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 changelog.d/6906.doc create mode 100644 docs/spam_checker.md diff --git a/changelog.d/6906.doc b/changelog.d/6906.doc new file mode 100644 index 0000000000..053b2436ae --- /dev/null +++ b/changelog.d/6906.doc @@ -0,0 +1 @@ +Add documentation for the spam checker. diff --git a/docs/spam_checker.md b/docs/spam_checker.md new file mode 100644 index 0000000000..97ff17f952 --- /dev/null +++ b/docs/spam_checker.md @@ -0,0 +1,85 @@ +# Handling spam in Synapse + +Synapse has support to customize spam checking behavior. It can plug into a +variety of events and affect how they are presented to users on your homeserver. + +The spam checking behavior is implemented as a Python class, which must be +able to be imported by the running Synapse. + +## Python spam checker class + +The Python class is instantiated with two objects: + +* Any configuration (see below). +* An instance of `synapse.spam_checker_api.SpamCheckerApi`. + +It then implements methods which return a boolean to alter behavior in Synapse. + +There's a generic method for checking every event (`check_event_for_spam`), as +well as some specific methods: + +* `user_may_invite` +* `user_may_create_room` +* `user_may_create_room_alias` +* `user_may_publish_room` + +The details of the each of these methods (as well as their inputs and outputs) +are documented in the `synapse.events.spamcheck.SpamChecker` class. + +The `SpamCheckerApi` class provides a way for the custom spam checker class to +call back into the homeserver internals. It currently implements the following +methods: + +* `get_state_events_in_room` + +### Example + +```python +class ExampleSpamChecker: + def __init__(self, config, api): + self.config = config + self.api = api + + def check_event_for_spam(self, foo): + return False # allow all events + + def user_may_invite(self, inviter_userid, invitee_userid, room_id): + return True # allow all invites + + def user_may_create_room(self, userid): + return True # allow all room creations + + def user_may_create_room_alias(self, userid, room_alias): + return True # allow all room aliases + + def user_may_publish_room(self, userid, room_id): + return True # allow publishing of all rooms +``` + +## Configuration + +Modify the `spam_checker` section of your `homeserver.yaml` in the following +manner: + +`module` should point to the fully qualified Python class that implements your +custom logic, e.g. `my_module.ExampleSpamChecker`. + +`config` is a dictionary that gets passed to the spam checker class. + +### Example + +This section might look like: + +```yaml +spam_checker: + module: my_module.ExampleSpamChecker + config: + # Enable or disable a specific option in ExampleSpamChecker. + my_custom_option: true +``` + +## Examples + +The [Mjolnir](https://github.com/matrix-org/mjolnir) project is a full fledged +example using the Synapse spam checking API, including a bot for dynamic +configuration. From 5820ed905f83c5241b686e03e121f67719a99046 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 13 Feb 2020 14:20:08 +0000 Subject: [PATCH 1036/1623] Add mention and warning about ACME v1 deprecation to the Synapse config --- docs/sample_config.yaml | 5 +++++ synapse/config/tls.py | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 8e8cf513b0..7232d8f3f8 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -466,6 +466,11 @@ retention: # ACME support: This will configure Synapse to request a valid TLS certificate # for your configured `server_name` via Let's Encrypt. # +# Note that ACME v1 is now deprecated, and Synapse currently doesn't support +# ACME v2. This means that this feature currently won't work with installs set +# up after November 2019. For more info, and alternative solutions, see +# https://github.com/matrix-org/synapse/blob/master/docs/ACME.md#deprecation-of-acme-v1 +# # Note that provisioning a certificate in this way requires port 80 to be # routed to Synapse so that it can complete the http-01 ACME challenge. # By default, if you enable ACME support, Synapse will attempt to listen on diff --git a/synapse/config/tls.py b/synapse/config/tls.py index 2514b0713d..694f52c032 100644 --- a/synapse/config/tls.py +++ b/synapse/config/tls.py @@ -32,6 +32,17 @@ from synapse.util import glob_to_regex logger = logging.getLogger(__name__) +ACME_SUPPORT_ENABLED_WARN = """\ +This server uses Synapse's built-in ACME support. Note that ACME v1 has been +deprecated by Let's Encrypt, and that Synapse doesn't currently support ACME v2, +which means that this feature will not work with Synapse installs set up after +November 2019, and that it may stop working on June 2020 for installs set up +before that date. + +For more info and alternative solutions, see +https://github.com/matrix-org/synapse/blob/master/docs/ACME.md#deprecation-of-acme-v1 +""" + class TlsConfig(Config): section = "tls" @@ -44,6 +55,9 @@ class TlsConfig(Config): self.acme_enabled = acme_config.get("enabled", False) + if self.acme_enabled: + logger.warning(ACME_SUPPORT_ENABLED_WARN) + # hyperlink complains on py2 if this is not a Unicode self.acme_url = six.text_type( acme_config.get("url", "https://acme-v01.api.letsencrypt.org/directory") @@ -362,6 +376,11 @@ class TlsConfig(Config): # ACME support: This will configure Synapse to request a valid TLS certificate # for your configured `server_name` via Let's Encrypt. # + # Note that ACME v1 is now deprecated, and Synapse currently doesn't support + # ACME v2. This means that this feature currently won't work with installs set + # up after November 2019. For more info, and alternative solutions, see + # https://github.com/matrix-org/synapse/blob/master/docs/ACME.md#deprecation-of-acme-v1 + # # Note that provisioning a certificate in this way requires port 80 to be # routed to Synapse so that it can complete the http-01 ACME challenge. # By default, if you enable ACME support, Synapse will attempt to listen on From 12bbcc255a77d76be13d8b8f142e9d329e91d520 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 13 Feb 2020 14:58:34 +0000 Subject: [PATCH 1037/1623] Add a comprehensive error when failing to register for an ACME account --- synapse/handlers/acme.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/synapse/handlers/acme.py b/synapse/handlers/acme.py index 46ac73106d..cfb5a4f39b 100644 --- a/synapse/handlers/acme.py +++ b/synapse/handlers/acme.py @@ -22,6 +22,7 @@ from twisted.web import server, static from twisted.web.resource import Resource from synapse.app import check_bind_error +from synapse.config import ConfigError logger = logging.getLogger(__name__) @@ -71,7 +72,18 @@ class AcmeHandler(object): # want it to control where we save the certificates, we have to reach in # and trigger the registration machinery ourselves. self._issuer._registered = False - yield self._issuer._ensure_registered() + + try: + yield self._issuer._ensure_registered() + except Exception: + raise ConfigError("Failed to register with the ACME provider. This is likely" + " happening because the install is new, and ACME v1 has" + " been deprecated by Let's Encrypt and is disabled for" + " installs set up after November 2019. At the moment," + " Synapse doesn't support ACME v2. For more info and" + " alternative solution, check out" + " https://github.com/matrix-org/synapse/blob/master/docs/" + "ACME.md#deprecation-of-acme-v1") @defer.inlineCallbacks def provision_certificate(self): From ef9c275d96bae28c6ea51f16e4907357be418419 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 13 Feb 2020 15:44:14 +0000 Subject: [PATCH 1038/1623] Add a separator for the config warning --- synapse/config/tls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/config/tls.py b/synapse/config/tls.py index 694f52c032..5ecd934602 100644 --- a/synapse/config/tls.py +++ b/synapse/config/tls.py @@ -41,7 +41,7 @@ before that date. For more info and alternative solutions, see https://github.com/matrix-org/synapse/blob/master/docs/ACME.md#deprecation-of-acme-v1 -""" +--------------------------------------------------------------------------------""" class TlsConfig(Config): From 0cb83cde7075bed522058f43a23342a4939c763a Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 13 Feb 2020 16:06:31 +0000 Subject: [PATCH 1039/1623] Lint --- synapse/handlers/acme.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/synapse/handlers/acme.py b/synapse/handlers/acme.py index cfb5a4f39b..c52796983d 100644 --- a/synapse/handlers/acme.py +++ b/synapse/handlers/acme.py @@ -77,13 +77,12 @@ class AcmeHandler(object): yield self._issuer._ensure_registered() except Exception: raise ConfigError("Failed to register with the ACME provider. This is likely" - " happening because the install is new, and ACME v1 has" - " been deprecated by Let's Encrypt and is disabled for" - " installs set up after November 2019. At the moment," - " Synapse doesn't support ACME v2. For more info and" - " alternative solution, check out" - " https://github.com/matrix-org/synapse/blob/master/docs/" - "ACME.md#deprecation-of-acme-v1") + " happening because the install is new, and ACME v1 has been deprecated" + " by Let's Encrypt and is disabled for installs set up after November" + " 2019. At the moment, Synapse doesn't support ACME v2. For more info" + " and alternative solution, check out https://github.com/matrix-org" + "/synapse/blob/master/docs/ACME.md#deprecation-of-acme-v1" + ) @defer.inlineCallbacks def provision_certificate(self): From f3f142259e5c882598b7426f36c26c4aca03c5d6 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 13 Feb 2020 16:10:16 +0000 Subject: [PATCH 1040/1623] Changelog --- changelog.d/6907.doc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6907.doc diff --git a/changelog.d/6907.doc b/changelog.d/6907.doc new file mode 100644 index 0000000000..be0e698af8 --- /dev/null +++ b/changelog.d/6907.doc @@ -0,0 +1 @@ +Update Synapse's documentation to warn about the deprecation of ACME v1. From df1c98c22a9ada46a2a103184aab3b5e08539b19 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 13 Feb 2020 16:12:20 +0000 Subject: [PATCH 1041/1623] Update changelog for #6905 to group it with upcoming PRs --- changelog.d/6905.doc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/6905.doc b/changelog.d/6905.doc index ca10b38301..be0e698af8 100644 --- a/changelog.d/6905.doc +++ b/changelog.d/6905.doc @@ -1 +1 @@ -Mention in `ACME.md` that ACMEv1 is deprecated and explain what it means for Synapse admins. +Update Synapse's documentation to warn about the deprecation of ACME v1. From 65bdc35a1f1078377e20ea3906120ba32db9057f Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 13 Feb 2020 16:14:15 +0000 Subject: [PATCH 1042/1623] Lint --- synapse/handlers/acme.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/synapse/handlers/acme.py b/synapse/handlers/acme.py index c52796983d..e6797535e6 100644 --- a/synapse/handlers/acme.py +++ b/synapse/handlers/acme.py @@ -76,12 +76,13 @@ class AcmeHandler(object): try: yield self._issuer._ensure_registered() except Exception: - raise ConfigError("Failed to register with the ACME provider. This is likely" - " happening because the install is new, and ACME v1 has been deprecated" - " by Let's Encrypt and is disabled for installs set up after November" - " 2019. At the moment, Synapse doesn't support ACME v2. For more info" - " and alternative solution, check out https://github.com/matrix-org" - "/synapse/blob/master/docs/ACME.md#deprecation-of-acme-v1" + raise ConfigError( + "Failed to register with the ACME provider. This is likely happening" + " because the install is new, and ACME v1 has been deprecated by Let's" + " Encrypt and is disabled for installs set up after November 2019. At the" + " moment, Synapse doesn't support ACME v2. For more info and alternative" + " solution, check out https://github.com/matrix-org/synapse/blob/master" + "/docs/ACME.md#deprecation-of-acme-v1" ) @defer.inlineCallbacks From 36af094017f87f0e3ec06e6ab92caa7971b43b8e Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 13 Feb 2020 17:03:41 +0000 Subject: [PATCH 1043/1623] Linters are hard but in they end they just want what's best for us --- synapse/config/tls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/config/tls.py b/synapse/config/tls.py index 5ecd934602..97a12d51f6 100644 --- a/synapse/config/tls.py +++ b/synapse/config/tls.py @@ -40,7 +40,7 @@ November 2019, and that it may stop working on June 2020 for installs set up before that date. For more info and alternative solutions, see -https://github.com/matrix-org/synapse/blob/master/docs/ACME.md#deprecation-of-acme-v1 +https://github.com/matrix-org/synapse/blob/master/docs/ACME.md#deprecation-of-acme-v1 --------------------------------------------------------------------------------""" From 71cc6bab5fc37f20a8fc14a52d8bd7930aec1c23 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 13 Feb 2020 17:22:44 +0000 Subject: [PATCH 1044/1623] Update INSTALL.md to recommend reverse proxying and warn about ACMEv1 deprecation --- INSTALL.md | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index d25fcf0753..42132f1eb4 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -388,15 +388,17 @@ Once you have installed synapse as above, you will need to configure it. ## TLS certificates -The default configuration exposes a single HTTP port: http://localhost:8008. It -is suitable for local testing, but for any practical use, you will either need -to enable a reverse proxy, or configure Synapse to expose an HTTPS port. +The default configuration exposes a single HTTP port on the local +interface: `http://localhost:8008`. It is suitable for local testing, +but for any practical use, you will need Synapse's APIs to be served +over HTTPS. -For information on using a reverse proxy, see +The recommended way to do so is to set up a reverse proxy on port +`8448`. You can find documentation on doing so in [docs/reverse_proxy.md](docs/reverse_proxy.md). -To configure Synapse to expose an HTTPS port, you will need to edit -`homeserver.yaml`, as follows: +Alternatively, you can configure Synapse to expose an HTTPS port. To do +so, you will need to edit `homeserver.yaml`, as follows: * First, under the `listeners` section, uncomment the configuration for the TLS-enabled listener. (Remove the hash sign (`#`) at the start of @@ -414,11 +416,13 @@ To configure Synapse to expose an HTTPS port, you will need to edit point these settings at an existing certificate and key, or you can enable Synapse's built-in ACME (Let's Encrypt) support. Instructions for having Synapse automatically provision and renew federation - certificates through ACME can be found at [ACME.md](docs/ACME.md). If you - are using your own certificate, be sure to use a `.pem` file that includes - the full certificate chain including any intermediate certificates (for - instance, if using certbot, use `fullchain.pem` as your certificate, not - `cert.pem`). + certificates through ACME can be found at [ACME.md](docs/ACME.md). + Note that, as pointed out in that document, this feature will not + work with installs set up after November 2020. If you are using your + own certificate, be sure to use a `.pem` file that includes the full + certificate chain including any intermediate certificates (for + instance, if using certbot, use `fullchain.pem` as your certificate, + not `cert.pem`). For a more detailed guide to configuring your server for federation, see [federate.md](docs/federate.md) From 79460ce9c987195afeb9453a33386240ffc0af3f Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 13 Feb 2020 17:24:14 +0000 Subject: [PATCH 1045/1623] Changelog --- changelog.d/6909.doc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6909.doc diff --git a/changelog.d/6909.doc b/changelog.d/6909.doc new file mode 100644 index 0000000000..be0e698af8 --- /dev/null +++ b/changelog.d/6909.doc @@ -0,0 +1 @@ +Update Synapse's documentation to warn about the deprecation of ACME v1. From ffe1fc111d3760e975f7ae2e676c2807b4363b7b Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 13 Feb 2020 18:16:48 +0000 Subject: [PATCH 1046/1623] Update INSTALL.md Co-Authored-By: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> --- INSTALL.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/INSTALL.md b/INSTALL.md index 42132f1eb4..9fe767704b 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -418,7 +418,9 @@ so, you will need to edit `homeserver.yaml`, as follows: for having Synapse automatically provision and renew federation certificates through ACME can be found at [ACME.md](docs/ACME.md). Note that, as pointed out in that document, this feature will not - work with installs set up after November 2020. If you are using your + work with installs set up after November 2020. + + If you are using your own certificate, be sure to use a `.pem` file that includes the full certificate chain including any intermediate certificates (for instance, if using certbot, use `fullchain.pem` as your certificate, From 49f877d32efc79cb40b2766cb052cf35bad31de5 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Fri, 14 Feb 2020 07:17:54 -0500 Subject: [PATCH 1047/1623] Filter the results of user directory searching via the spam checker (#6888) Add a method to the spam checker to filter the user directory results. --- changelog.d/6888.feature | 1 + docs/spam_checker.md | 3 + synapse/events/spamcheck.py | 27 ++++++++ synapse/handlers/user_directory.py | 14 +++- tests/handlers/test_user_directory.py | 92 +++++++++++++++++++++++++++ 5 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 changelog.d/6888.feature diff --git a/changelog.d/6888.feature b/changelog.d/6888.feature new file mode 100644 index 0000000000..1b7ac0c823 --- /dev/null +++ b/changelog.d/6888.feature @@ -0,0 +1 @@ +The result of a user directory search can now be filtered via the spam checker. diff --git a/docs/spam_checker.md b/docs/spam_checker.md index 97ff17f952..5b5f5000b7 100644 --- a/docs/spam_checker.md +++ b/docs/spam_checker.md @@ -54,6 +54,9 @@ class ExampleSpamChecker: def user_may_publish_room(self, userid, room_id): return True # allow publishing of all rooms + + def check_username_for_spam(self, user_profile): + return False # allow all usernames ``` ## Configuration diff --git a/synapse/events/spamcheck.py b/synapse/events/spamcheck.py index 5a907718d6..0a13fca9a4 100644 --- a/synapse/events/spamcheck.py +++ b/synapse/events/spamcheck.py @@ -15,6 +15,7 @@ # limitations under the License. import inspect +from typing import Dict from synapse.spam_checker_api import SpamCheckerApi @@ -125,3 +126,29 @@ class SpamChecker(object): return True return self.spam_checker.user_may_publish_room(userid, room_id) + + def check_username_for_spam(self, user_profile: Dict[str, str]) -> bool: + """Checks if a user ID or display name are considered "spammy" by this server. + + If the server considers a username spammy, then it will not be included in + user directory results. + + Args: + user_profile: The user information to check, it contains the keys: + * user_id + * display_name + * avatar_url + + Returns: + True if the user is spammy. + """ + if self.spam_checker is None: + return False + + # For backwards compatibility, if the method does not exist on the spam checker, fallback to not interfering. + checker = getattr(self.spam_checker, "check_username_for_spam", None) + if not checker: + return False + # Make a copy of the user profile object to ensure the spam checker + # cannot modify it. + return checker(user_profile.copy()) diff --git a/synapse/handlers/user_directory.py b/synapse/handlers/user_directory.py index 81aa58dc8c..722760c59d 100644 --- a/synapse/handlers/user_directory.py +++ b/synapse/handlers/user_directory.py @@ -52,6 +52,7 @@ class UserDirectoryHandler(StateDeltasHandler): self.is_mine_id = hs.is_mine_id self.update_user_directory = hs.config.update_user_directory self.search_all_users = hs.config.user_directory_search_all_users + self.spam_checker = hs.get_spam_checker() # The current position in the current_state_delta stream self.pos = None @@ -65,7 +66,7 @@ class UserDirectoryHandler(StateDeltasHandler): # we start populating the user directory self.clock.call_later(0, self.notify_new_event) - def search_users(self, user_id, search_term, limit): + async def search_users(self, user_id, search_term, limit): """Searches for users in directory Returns: @@ -82,7 +83,16 @@ class UserDirectoryHandler(StateDeltasHandler): ] } """ - return self.store.search_user_dir(user_id, search_term, limit) + results = await self.store.search_user_dir(user_id, search_term, limit) + + # Remove any spammy users from the results. + results["results"] = [ + user + for user in results["results"] + if not self.spam_checker.check_username_for_spam(user) + ] + + return results def notify_new_event(self): """Called when there may be more deltas to process diff --git a/tests/handlers/test_user_directory.py b/tests/handlers/test_user_directory.py index 26071059d2..0a4765fff4 100644 --- a/tests/handlers/test_user_directory.py +++ b/tests/handlers/test_user_directory.py @@ -147,6 +147,98 @@ class UserDirectoryTestCase(unittest.HomeserverTestCase): s = self.get_success(self.handler.search_users(u1, "user3", 10)) self.assertEqual(len(s["results"]), 0) + def test_spam_checker(self): + """ + A user which fails to the spam checks will not appear in search results. + """ + u1 = self.register_user("user1", "pass") + u1_token = self.login(u1, "pass") + u2 = self.register_user("user2", "pass") + u2_token = self.login(u2, "pass") + + # We do not add users to the directory until they join a room. + s = self.get_success(self.handler.search_users(u1, "user2", 10)) + self.assertEqual(len(s["results"]), 0) + + room = self.helper.create_room_as(u1, is_public=False, tok=u1_token) + self.helper.invite(room, src=u1, targ=u2, tok=u1_token) + self.helper.join(room, user=u2, tok=u2_token) + + # Check we have populated the database correctly. + shares_private = self.get_users_who_share_private_rooms() + public_users = self.get_users_in_public_rooms() + + self.assertEqual( + self._compress_shared(shares_private), set([(u1, u2, room), (u2, u1, room)]) + ) + self.assertEqual(public_users, []) + + # We get one search result when searching for user2 by user1. + s = self.get_success(self.handler.search_users(u1, "user2", 10)) + self.assertEqual(len(s["results"]), 1) + + # Configure a spam checker that does not filter any users. + spam_checker = self.hs.get_spam_checker() + + class AllowAll(object): + def check_username_for_spam(self, user_profile): + # Allow all users. + return False + + spam_checker.spam_checker = AllowAll() + + # The results do not change: + # We get one search result when searching for user2 by user1. + s = self.get_success(self.handler.search_users(u1, "user2", 10)) + self.assertEqual(len(s["results"]), 1) + + # Configure a spam checker that filters all users. + class BlockAll(object): + def check_username_for_spam(self, user_profile): + # All users are spammy. + return True + + spam_checker.spam_checker = BlockAll() + + # User1 now gets no search results for any of the other users. + s = self.get_success(self.handler.search_users(u1, "user2", 10)) + self.assertEqual(len(s["results"]), 0) + + def test_legacy_spam_checker(self): + """ + A spam checker without the expected method should be ignored. + """ + u1 = self.register_user("user1", "pass") + u1_token = self.login(u1, "pass") + u2 = self.register_user("user2", "pass") + u2_token = self.login(u2, "pass") + + # We do not add users to the directory until they join a room. + s = self.get_success(self.handler.search_users(u1, "user2", 10)) + self.assertEqual(len(s["results"]), 0) + + room = self.helper.create_room_as(u1, is_public=False, tok=u1_token) + self.helper.invite(room, src=u1, targ=u2, tok=u1_token) + self.helper.join(room, user=u2, tok=u2_token) + + # Check we have populated the database correctly. + shares_private = self.get_users_who_share_private_rooms() + public_users = self.get_users_in_public_rooms() + + self.assertEqual( + self._compress_shared(shares_private), set([(u1, u2, room), (u2, u1, room)]) + ) + self.assertEqual(public_users, []) + + # Configure a spam checker. + spam_checker = self.hs.get_spam_checker() + # The spam checker doesn't need any methods, so create a bare object. + spam_checker.spam_checker = object() + + # We get one search result when searching for user2 by user1. + s = self.get_success(self.handler.search_users(u1, "user2", 10)) + self.assertEqual(len(s["results"]), 1) + def _compress_shared(self, shared): """ Compress a list of users who share rooms dicts to a list of tuples. From 02e89021f58f931068ab0337de039181cc7f6569 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Fri, 14 Feb 2020 09:05:43 -0500 Subject: [PATCH 1048/1623] Convert the directory handler tests to use HomeserverTestCase (#6919) Convert directory handler tests to use HomeserverTestCase. --- changelog.d/6919.misc | 1 + tests/handlers/test_directory.py | 41 +++++++++++++------------------- 2 files changed, 18 insertions(+), 24 deletions(-) create mode 100644 changelog.d/6919.misc diff --git a/changelog.d/6919.misc b/changelog.d/6919.misc new file mode 100644 index 0000000000..aa2cd89998 --- /dev/null +++ b/changelog.d/6919.misc @@ -0,0 +1 @@ +Convert the directory handler tests to use HomeserverTestCase. diff --git a/tests/handlers/test_directory.py b/tests/handlers/test_directory.py index 91c7a17070..ee88cf5a4b 100644 --- a/tests/handlers/test_directory.py +++ b/tests/handlers/test_directory.py @@ -19,24 +19,16 @@ from mock import Mock from twisted.internet import defer from synapse.config.room_directory import RoomDirectoryConfig -from synapse.handlers.directory import DirectoryHandler from synapse.rest.client.v1 import directory, room from synapse.types import RoomAlias from tests import unittest -from tests.utils import setup_test_homeserver -class DirectoryHandlers(object): - def __init__(self, hs): - self.directory_handler = DirectoryHandler(hs) - - -class DirectoryTestCase(unittest.TestCase): +class DirectoryTestCase(unittest.HomeserverTestCase): """ Tests the directory service. """ - @defer.inlineCallbacks - def setUp(self): + def make_homeserver(self, reactor, clock): self.mock_federation = Mock() self.mock_registry = Mock() @@ -47,14 +39,12 @@ class DirectoryTestCase(unittest.TestCase): self.mock_registry.register_query_handler = register_query_handler - hs = yield setup_test_homeserver( - self.addCleanup, + hs = self.setup_test_homeserver( http_client=None, resource_for_federation=Mock(), federation_client=self.mock_federation, federation_registry=self.mock_registry, ) - hs.handlers = DirectoryHandlers(hs) self.handler = hs.get_handlers().directory_handler @@ -64,23 +54,25 @@ class DirectoryTestCase(unittest.TestCase): self.your_room = RoomAlias.from_string("#your-room:test") self.remote_room = RoomAlias.from_string("#another:remote") - @defer.inlineCallbacks + return hs + def test_get_local_association(self): - yield self.store.create_room_alias_association( - self.my_room, "!8765qwer:test", ["test"] + self.get_success( + self.store.create_room_alias_association( + self.my_room, "!8765qwer:test", ["test"] + ) ) - result = yield self.handler.get_association(self.my_room) + result = self.get_success(self.handler.get_association(self.my_room)) self.assertEquals({"room_id": "!8765qwer:test", "servers": ["test"]}, result) - @defer.inlineCallbacks def test_get_remote_association(self): self.mock_federation.make_query.return_value = defer.succeed( {"room_id": "!8765qwer:test", "servers": ["test", "remote"]} ) - result = yield self.handler.get_association(self.remote_room) + result = self.get_success(self.handler.get_association(self.remote_room)) self.assertEquals( {"room_id": "!8765qwer:test", "servers": ["test", "remote"]}, result @@ -93,14 +85,15 @@ class DirectoryTestCase(unittest.TestCase): ignore_backoff=True, ) - @defer.inlineCallbacks def test_incoming_fed_query(self): - yield self.store.create_room_alias_association( - self.your_room, "!8765asdf:test", ["test"] + self.get_success( + self.store.create_room_alias_association( + self.your_room, "!8765asdf:test", ["test"] + ) ) - response = yield self.query_handlers["directory"]( - {"room_alias": "#your-room:test"} + response = self.get_success( + self.handler.on_directory_query({"room_alias": "#your-room:test"}) ) self.assertEquals({"room_id": "!8765asdf:test", "servers": ["test"]}, response) From 97a42bbc3a4789620c48746f8e87291446f6f5ac Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 14 Feb 2020 16:22:30 +0000 Subject: [PATCH 1049/1623] Add a warning about indentation to generated config (#6920) Fixes #6916. --- changelog.d/6920.misc | 1 + docs/.sample_config_header.yaml | 4 +++- docs/sample_config.yaml | 12 +++++++++++- synapse/config/_base.py | 16 ++++++++++++++-- 4 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 changelog.d/6920.misc diff --git a/changelog.d/6920.misc b/changelog.d/6920.misc new file mode 100644 index 0000000000..d333add990 --- /dev/null +++ b/changelog.d/6920.misc @@ -0,0 +1 @@ +Add a warning about indentation to generated configuration files. diff --git a/docs/.sample_config_header.yaml b/docs/.sample_config_header.yaml index e001ef5983..35a591d042 100644 --- a/docs/.sample_config_header.yaml +++ b/docs/.sample_config_header.yaml @@ -1,4 +1,4 @@ -# The config is maintained as an up-to-date snapshot of the default +# This file is maintained as an up-to-date snapshot of the default # homeserver.yaml configuration generated by Synapse. # # It is intended to act as a reference for the default configuration, @@ -10,3 +10,5 @@ # homeserver.yaml. Instead, if you are starting from scratch, please generate # a fresh config using Synapse by following the instructions in INSTALL.md. +################################################################################ + diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 8e8cf513b0..93236daddc 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -1,4 +1,4 @@ -# The config is maintained as an up-to-date snapshot of the default +# This file is maintained as an up-to-date snapshot of the default # homeserver.yaml configuration generated by Synapse. # # It is intended to act as a reference for the default configuration, @@ -10,6 +10,16 @@ # homeserver.yaml. Instead, if you are starting from scratch, please generate # a fresh config using Synapse by following the instructions in INSTALL.md. +################################################################################ + +# Configuration file for Synapse. +# +# This is a YAML file: see [1] for a quick introduction. Note in particular +# that *indentation is important*: all the elements of a list or dictionary +# should have the same indentation. +# +# [1] https://docs.ansible.com/ansible/latest/reference_appendices/YAMLSyntax.html + ## Server ## # The domain name of the server, with optional explicit port. diff --git a/synapse/config/_base.py b/synapse/config/_base.py index 08619404bb..ba846042c4 100644 --- a/synapse/config/_base.py +++ b/synapse/config/_base.py @@ -53,6 +53,18 @@ Missing mandatory `server_name` config option. """ +CONFIG_FILE_HEADER = """\ +# Configuration file for Synapse. +# +# This is a YAML file: see [1] for a quick introduction. Note in particular +# that *indentation is important*: all the elements of a list or dictionary +# should have the same indentation. +# +# [1] https://docs.ansible.com/ansible/latest/reference_appendices/YAMLSyntax.html + +""" + + def path_exists(file_path): """Check if a file exists @@ -344,7 +356,7 @@ class RootConfig(object): str: the yaml config file """ - return "\n\n".join( + return CONFIG_FILE_HEADER + "\n\n".join( dedent(conf) for conf in self.invoke_all( "generate_config_section", @@ -574,8 +586,8 @@ class RootConfig(object): if not path_exists(config_dir_path): os.makedirs(config_dir_path) with open(config_path, "w") as config_file: - config_file.write("# vim:ft=yaml\n\n") config_file.write(config_str) + config_file.write("\n\n# vim:ft=yaml") config_dict = yaml.safe_load(config_str) obj.generate_missing_files(config_dict, config_dir_path) From 32873efa87055518b08d9b3b001b3bf9b60437f9 Mon Sep 17 00:00:00 2001 From: Fridtjof Mund <2780577+fridtjof@users.noreply.github.com> Date: Fri, 14 Feb 2020 17:27:29 +0100 Subject: [PATCH 1050/1623] contrib/docker: Ensure correct encoding and locale settings on DB creation (#6921) Signed-off-by: Fridtjof Mund --- changelog.d/6921.docker | 1 + contrib/docker/docker-compose.yml | 3 +++ 2 files changed, 4 insertions(+) create mode 100644 changelog.d/6921.docker diff --git a/changelog.d/6921.docker b/changelog.d/6921.docker new file mode 100644 index 0000000000..152e723339 --- /dev/null +++ b/changelog.d/6921.docker @@ -0,0 +1 @@ +Databases created using the compose file in contrib/docker will now always have correct encoding and locale settings. Contributed by Fridtjof Mund. diff --git a/contrib/docker/docker-compose.yml b/contrib/docker/docker-compose.yml index 2b044baf78..5df29379c8 100644 --- a/contrib/docker/docker-compose.yml +++ b/contrib/docker/docker-compose.yml @@ -56,6 +56,9 @@ services: environment: - POSTGRES_USER=synapse - POSTGRES_PASSWORD=changeme + # ensure the database gets created correctly + # https://github.com/matrix-org/synapse/blob/master/docs/postgres.md#set-up-database + - POSTGRES_INITDB_ARGS="--encoding=UTF-8 --lc-collate=C --lc-ctype=C" volumes: # You may store the database tables in a local folder.. - ./schemas:/var/lib/postgresql/data From 43b2be9764bf244122f25a0f2bf8ec8c761f5cad Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 7 Feb 2020 13:08:34 +0000 Subject: [PATCH 1051/1623] Replace _event_dict_property with DictProperty this amounts to the same thing, but replaces `_event_dict` with `_dict`, and removes some of the function layers generated by `property`. --- synapse/events/__init__.py | 144 ++++++++++++++++++-------------- tests/storage/test_redaction.py | 2 +- 2 files changed, 80 insertions(+), 66 deletions(-) diff --git a/synapse/events/__init__.py b/synapse/events/__init__.py index a842661a90..512254f65d 100644 --- a/synapse/events/__init__.py +++ b/synapse/events/__init__.py @@ -37,6 +37,65 @@ from synapse.util.frozenutils import freeze USE_FROZEN_DICTS = strtobool(os.environ.get("SYNAPSE_USE_FROZEN_DICTS", "0")) +class DictProperty: + """An object property which delegates to the `_dict` within its parent object.""" + + __slots__ = ["key"] + + def __init__(self, key: str): + self.key = key + + def __get__(self, instance, owner=None): + # if the property is accessed as a class property rather than an instance + # property, return the property itself rather than the value + if instance is None: + return self + try: + return instance._dict[self.key] + except KeyError as e1: + # We want this to look like a regular attribute error (mostly so that + # hasattr() works correctly), so we convert the KeyError into an + # AttributeError. + # + # To exclude the KeyError from the traceback, we explicitly + # 'raise from e1.__context__' (which is better than 'raise from None', + # becuase that would omit any *earlier* exceptions). + # + raise AttributeError( + "'%s' has no '%s' property" % (type(instance), self.key) + ) from e1.__context__ + + def __set__(self, instance, v): + instance._dict[self.key] = v + + def __delete__(self, instance): + try: + del instance._dict[self.key] + except KeyError as e1: + raise AttributeError( + "'%s' has no '%s' property" % (type(instance), self.key) + ) from e1.__context__ + + +class DefaultDictProperty(DictProperty): + """An extension of DictProperty which provides a default if the property is + not present in the parent's _dict. + + Note that this means that hasattr() on the property always returns True. + """ + + __slots__ = ["default"] + + def __init__(self, key, default): + super().__init__(key) + self.default = default + + def __get__(self, instance, owner=None): + if instance is None: + return self + return instance._dict.get(self.key, self.default) + + class _EventInternalMetadata(object): def __init__(self, internal_metadata_dict): self.__dict__ = dict(internal_metadata_dict) @@ -117,51 +176,6 @@ class _EventInternalMetadata(object): return getattr(self, "redacted", False) -_SENTINEL = object() - - -def _event_dict_property(key, default=_SENTINEL): - """Creates a new property for the given key that delegates access to - `self._event_dict`. - - The default is used if the key is missing from the `_event_dict`, if given, - otherwise an AttributeError will be raised. - - Note: If a default is given then `hasattr` will always return true. - """ - - # We want to be able to use hasattr with the event dict properties. - # However, (on python3) hasattr expects AttributeError to be raised. Hence, - # we need to transform the KeyError into an AttributeError - - def getter_raises(self): - try: - return self._event_dict[key] - except KeyError: - raise AttributeError(key) - - def getter_default(self): - return self._event_dict.get(key, default) - - def setter(self, v): - try: - self._event_dict[key] = v - except KeyError: - raise AttributeError(key) - - def delete(self): - try: - del self._event_dict[key] - except KeyError: - raise AttributeError(key) - - if default is _SENTINEL: - # No default given, so use the getter that raises - return property(getter_raises, setter, delete) - else: - return property(getter_default, setter, delete) - - class EventBase(object): def __init__( self, @@ -175,23 +189,23 @@ class EventBase(object): self.unsigned = unsigned self.rejected_reason = rejected_reason - self._event_dict = event_dict + self._dict = event_dict self.internal_metadata = _EventInternalMetadata(internal_metadata_dict) - auth_events = _event_dict_property("auth_events") - depth = _event_dict_property("depth") - content = _event_dict_property("content") - hashes = _event_dict_property("hashes") - origin = _event_dict_property("origin") - origin_server_ts = _event_dict_property("origin_server_ts") - prev_events = _event_dict_property("prev_events") - redacts = _event_dict_property("redacts", None) - room_id = _event_dict_property("room_id") - sender = _event_dict_property("sender") - state_key = _event_dict_property("state_key") - type = _event_dict_property("type") - user_id = _event_dict_property("sender") + auth_events = DictProperty("auth_events") + depth = DictProperty("depth") + content = DictProperty("content") + hashes = DictProperty("hashes") + origin = DictProperty("origin") + origin_server_ts = DictProperty("origin_server_ts") + prev_events = DictProperty("prev_events") + redacts = DefaultDictProperty("redacts", None) + room_id = DictProperty("room_id") + sender = DictProperty("sender") + state_key = DictProperty("state_key") + type = DictProperty("type") + user_id = DictProperty("sender") @property def event_id(self) -> str: @@ -205,13 +219,13 @@ class EventBase(object): return hasattr(self, "state_key") and self.state_key is not None def get_dict(self) -> JsonDict: - d = dict(self._event_dict) + d = dict(self._dict) d.update({"signatures": self.signatures, "unsigned": dict(self.unsigned)}) return d def get(self, key, default=None): - return self._event_dict.get(key, default) + return self._dict.get(key, default) def get_internal_metadata_dict(self): return self.internal_metadata.get_dict() @@ -233,16 +247,16 @@ class EventBase(object): raise AttributeError("Unrecognized attribute %s" % (instance,)) def __getitem__(self, field): - return self._event_dict[field] + return self._dict[field] def __contains__(self, field): - return field in self._event_dict + return field in self._dict def items(self): - return list(self._event_dict.items()) + return list(self._dict.items()) def keys(self): - return six.iterkeys(self._event_dict) + return six.iterkeys(self._dict) def prev_event_ids(self): """Returns the list of prev event IDs. The order matches the order diff --git a/tests/storage/test_redaction.py b/tests/storage/test_redaction.py index b9ee6ec1ec..db3667dc43 100644 --- a/tests/storage/test_redaction.py +++ b/tests/storage/test_redaction.py @@ -240,7 +240,7 @@ class RedactionTestCase(unittest.HomeserverTestCase): built_event = yield self._base_builder.build(prev_event_ids) built_event._event_id = self._event_id - built_event._event_dict["event_id"] = self._event_id + built_event._dict["event_id"] = self._event_id assert built_event.event_id == self._event_id return built_event From 9551911f88b558d20e2fd9c2ae791fe5b732db02 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 7 Feb 2020 13:22:17 +0000 Subject: [PATCH 1052/1623] Rewrite _EventInternalMetadata to back it with a _dict Mostly, this gives mypy an easier time. --- synapse/events/__init__.py | 55 +++++++++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 16 deletions(-) diff --git a/synapse/events/__init__.py b/synapse/events/__init__.py index 512254f65d..7307116556 100644 --- a/synapse/events/__init__.py +++ b/synapse/events/__init__.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2019 New Vector Ltd +# Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -97,33 +98,55 @@ class DefaultDictProperty(DictProperty): class _EventInternalMetadata(object): - def __init__(self, internal_metadata_dict): - self.__dict__ = dict(internal_metadata_dict) + __slots__ = ["_dict"] - def get_dict(self): - return dict(self.__dict__) + def __init__(self, internal_metadata_dict: JsonDict): + # we have to copy the dict, because it turns out that the same dict is + # reused. TODO: fix that + self._dict = dict(internal_metadata_dict) - def is_outlier(self): - return getattr(self, "outlier", False) + outlier = DictProperty("outlier") # type: bool + out_of_band_membership = DictProperty("out_of_band_membership") # type: bool + send_on_behalf_of = DictProperty("send_on_behalf_of") # type: str + recheck_redaction = DictProperty("recheck_redaction") # type: bool + soft_failed = DictProperty("soft_failed") # type: bool + proactively_send = DictProperty("proactively_send") # type: bool + redacted = DictProperty("redacted") # type: bool + txn_id = DictProperty("txn_id") # type: str + token_id = DictProperty("token_id") # type: str + stream_ordering = DictProperty("stream_ordering") # type: int - def is_out_of_band_membership(self): + # XXX: These are set by StreamWorkerStore._set_before_and_after. + # I'm pretty sure that these are never persisted to the database, so shouldn't + # be here + before = DictProperty("before") # type: str + after = DictProperty("after") # type: str + order = DictProperty("order") # type: int + + def get_dict(self) -> JsonDict: + return dict(self._dict) + + def is_outlier(self) -> bool: + return self._dict.get("outlier", False) + + def is_out_of_band_membership(self) -> bool: """Whether this is an out of band membership, like an invite or an invite rejection. This is needed as those events are marked as outliers, but they still need to be processed as if they're new events (e.g. updating invite state in the database, relaying to clients, etc). """ - return getattr(self, "out_of_band_membership", False) + return self._dict.get("out_of_band_membership", False) - def get_send_on_behalf_of(self): + def get_send_on_behalf_of(self) -> Optional[str]: """Whether this server should send the event on behalf of another server. This is used by the federation "send_join" API to forward the initial join event for a server in the room. returns a str with the name of the server this event is sent on behalf of. """ - return getattr(self, "send_on_behalf_of", None) + return self._dict.get("send_on_behalf_of") - def need_to_check_redaction(self): + def need_to_check_redaction(self) -> bool: """Whether the redaction event needs to be rechecked when fetching from the database. @@ -136,9 +159,9 @@ class _EventInternalMetadata(object): Returns: bool """ - return getattr(self, "recheck_redaction", False) + return self._dict.get("recheck_redaction", False) - def is_soft_failed(self): + def is_soft_failed(self) -> bool: """Whether the event has been soft failed. Soft failed events should be handled as usual, except: @@ -150,7 +173,7 @@ class _EventInternalMetadata(object): Returns: bool """ - return getattr(self, "soft_failed", False) + return self._dict.get("soft_failed", False) def should_proactively_send(self): """Whether the event, if ours, should be sent to other clients and @@ -162,7 +185,7 @@ class _EventInternalMetadata(object): Returns: bool """ - return getattr(self, "proactively_send", True) + return self._dict.get("proactively_send", True) def is_redacted(self): """Whether the event has been redacted. @@ -173,7 +196,7 @@ class _EventInternalMetadata(object): Returns: bool """ - return getattr(self, "redacted", False) + return self._dict.get("redacted", False) class EventBase(object): From 5a78f47f6e21505f84490c3b8da49e96ac8e3483 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 7 Feb 2020 13:27:58 +0000 Subject: [PATCH 1053/1623] changelog --- changelog.d/6872.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6872.misc diff --git a/changelog.d/6872.misc b/changelog.d/6872.misc new file mode 100644 index 0000000000..215a0c82c3 --- /dev/null +++ b/changelog.d/6872.misc @@ -0,0 +1 @@ +Refactor _EventInternalMetadata object to improve type safety. From 10027c80b031f1e62b47cd61c534420673f49a71 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Fri, 14 Feb 2020 12:49:40 -0500 Subject: [PATCH 1054/1623] Add type hints to the spam check module (#6915) Add typing information to the spam checker modules. --- changelog.d/6915.misc | 1 + synapse/events/spamcheck.py | 44 ++++++++++++++++------------ synapse/spam_checker_api/__init__.py | 12 +++++--- tox.ini | 1 + 4 files changed, 36 insertions(+), 22 deletions(-) create mode 100644 changelog.d/6915.misc diff --git a/changelog.d/6915.misc b/changelog.d/6915.misc new file mode 100644 index 0000000000..3a181ef243 --- /dev/null +++ b/changelog.d/6915.misc @@ -0,0 +1 @@ +Add type hints to the spam checker module. diff --git a/synapse/events/spamcheck.py b/synapse/events/spamcheck.py index 0a13fca9a4..a23b6b7b61 100644 --- a/synapse/events/spamcheck.py +++ b/synapse/events/spamcheck.py @@ -19,9 +19,13 @@ from typing import Dict from synapse.spam_checker_api import SpamCheckerApi +MYPY = False +if MYPY: + import synapse.server + class SpamChecker(object): - def __init__(self, hs): + def __init__(self, hs: "synapse.server.HomeServer"): self.spam_checker = None module = None @@ -41,7 +45,7 @@ class SpamChecker(object): else: self.spam_checker = module(config=config) - def check_event_for_spam(self, event): + def check_event_for_spam(self, event: "synapse.events.EventBase") -> bool: """Checks if a given event is considered "spammy" by this server. If the server considers an event spammy, then it will be rejected if @@ -49,26 +53,30 @@ class SpamChecker(object): users receive a blank event. Args: - event (synapse.events.EventBase): the event to be checked + event: the event to be checked Returns: - bool: True if the event is spammy. + True if the event is spammy. """ if self.spam_checker is None: return False return self.spam_checker.check_event_for_spam(event) - def user_may_invite(self, inviter_userid, invitee_userid, room_id): + def user_may_invite( + self, inviter_userid: str, invitee_userid: str, room_id: str + ) -> bool: """Checks if a given user may send an invite If this method returns false, the invite will be rejected. Args: - userid (string): The sender's user ID + inviter_userid: The user ID of the sender of the invitation + invitee_userid: The user ID targeted in the invitation + room_id: The room ID Returns: - bool: True if the user may send an invite, otherwise False + True if the user may send an invite, otherwise False """ if self.spam_checker is None: return True @@ -77,50 +85,50 @@ class SpamChecker(object): inviter_userid, invitee_userid, room_id ) - def user_may_create_room(self, userid): + def user_may_create_room(self, userid: str) -> bool: """Checks if a given user may create a room If this method returns false, the creation request will be rejected. Args: - userid (string): The sender's user ID + userid: The ID of the user attempting to create a room Returns: - bool: True if the user may create a room, otherwise False + True if the user may create a room, otherwise False """ if self.spam_checker is None: return True return self.spam_checker.user_may_create_room(userid) - def user_may_create_room_alias(self, userid, room_alias): + def user_may_create_room_alias(self, userid: str, room_alias: str) -> bool: """Checks if a given user may create a room alias If this method returns false, the association request will be rejected. Args: - userid (string): The sender's user ID - room_alias (string): The alias to be created + userid: The ID of the user attempting to create a room alias + room_alias: The alias to be created Returns: - bool: True if the user may create a room alias, otherwise False + True if the user may create a room alias, otherwise False """ if self.spam_checker is None: return True return self.spam_checker.user_may_create_room_alias(userid, room_alias) - def user_may_publish_room(self, userid, room_id): + def user_may_publish_room(self, userid: str, room_id: str) -> bool: """Checks if a given user may publish a room to the directory If this method returns false, the publish request will be rejected. Args: - userid (string): The sender's user ID - room_id (string): The ID of the room that would be published + userid: The user ID attempting to publish the room + room_id: The ID of the room that would be published Returns: - bool: True if the user may publish the room, otherwise False + True if the user may publish the room, otherwise False """ if self.spam_checker is None: return True diff --git a/synapse/spam_checker_api/__init__.py b/synapse/spam_checker_api/__init__.py index efcc10f808..9b78924d96 100644 --- a/synapse/spam_checker_api/__init__.py +++ b/synapse/spam_checker_api/__init__.py @@ -18,6 +18,10 @@ from twisted.internet import defer from synapse.storage.state import StateFilter +MYPY = False +if MYPY: + import synapse.server + logger = logging.getLogger(__name__) @@ -26,18 +30,18 @@ class SpamCheckerApi(object): access to rooms and other relevant information. """ - def __init__(self, hs): + def __init__(self, hs: "synapse.server.HomeServer"): self.hs = hs self._store = hs.get_datastore() @defer.inlineCallbacks - def get_state_events_in_room(self, room_id, types): + def get_state_events_in_room(self, room_id: str, types: tuple) -> defer.Deferred: """Gets state events for the given room. Args: - room_id (string): The room ID to get state events in. - types (tuple): The event type and state key (using None + room_id: The room ID to get state events in. + types: The event type and state key (using None to represent 'any') of the room state to acquire. Returns: diff --git a/tox.ini b/tox.ini index f8229eba88..b9132a3177 100644 --- a/tox.ini +++ b/tox.ini @@ -179,6 +179,7 @@ extras = all commands = mypy \ synapse/api \ synapse/config/ \ + synapse/events/spamcheck.py \ synapse/federation/sender \ synapse/federation/transport \ synapse/handlers/sync.py \ From 46fa66bbfd367b2c1fbdf585107cec75fa1bb193 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 17 Feb 2020 11:30:50 +0000 Subject: [PATCH 1055/1623] wait for current_state_events_membership before delete_old_current_state_events (#6924) --- changelog.d/6924.bugfix | 1 + .../schema/delta/57/delete_old_current_state_events.sql | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 changelog.d/6924.bugfix diff --git a/changelog.d/6924.bugfix b/changelog.d/6924.bugfix new file mode 100644 index 0000000000..33e6611929 --- /dev/null +++ b/changelog.d/6924.bugfix @@ -0,0 +1 @@ +Fix a bug introduced in Synapse 1.10.0 which would cause room state to be cleared in the database if Synapse was upgraded direct from 1.2.1 or earlier to 1.10.0. diff --git a/synapse/storage/data_stores/main/schema/delta/57/delete_old_current_state_events.sql b/synapse/storage/data_stores/main/schema/delta/57/delete_old_current_state_events.sql index a133d87a19..aec06c8261 100644 --- a/synapse/storage/data_stores/main/schema/delta/57/delete_old_current_state_events.sql +++ b/synapse/storage/data_stores/main/schema/delta/57/delete_old_current_state_events.sql @@ -15,5 +15,8 @@ -- Add background update to go and delete current state events for rooms the -- server is no longer in. -INSERT into background_updates (update_name, progress_json) - VALUES ('delete_old_current_state_events', '{}'); +-- +-- this relies on the 'membership' column of current_state_events, so make sure +-- that's populated first! +INSERT into background_updates (update_name, progress_json, depends_on) + VALUES ('delete_old_current_state_events', '{}', 'current_state_events_membership'); From 3404ad289b1d2e5bc5c7f277f519b9698dbdaa15 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Mon, 17 Feb 2020 13:23:37 +0000 Subject: [PATCH 1056/1623] Raise the default power levels for invites, tombstones and server acls (#6834) --- changelog.d/6834.misc | 1 + synapse/handlers/room.py | 10 +++++++++- tests/rest/client/v1/test_rooms.py | 4 +++- 3 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 changelog.d/6834.misc diff --git a/changelog.d/6834.misc b/changelog.d/6834.misc new file mode 100644 index 0000000000..79acebe516 --- /dev/null +++ b/changelog.d/6834.misc @@ -0,0 +1 @@ +Change the default power levels of invites, tombstones and server ACLs for new rooms. \ No newline at end of file diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index ab07edd2fc..033083acac 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -64,18 +64,21 @@ class RoomCreationHandler(BaseHandler): "history_visibility": "shared", "original_invitees_have_ops": False, "guest_can_join": True, + "power_level_content_override": {"invite": 0}, }, RoomCreationPreset.TRUSTED_PRIVATE_CHAT: { "join_rules": JoinRules.INVITE, "history_visibility": "shared", "original_invitees_have_ops": True, "guest_can_join": True, + "power_level_content_override": {"invite": 0}, }, RoomCreationPreset.PUBLIC_CHAT: { "join_rules": JoinRules.PUBLIC, "history_visibility": "shared", "original_invitees_have_ops": False, "guest_can_join": False, + "power_level_content_override": {}, }, } @@ -829,19 +832,24 @@ class RoomCreationHandler(BaseHandler): # This will be reudundant on pre-MSC2260 rooms, since the # aliases event is special-cased. EventTypes.Aliases: 0, + EventTypes.Tombstone: 100, + EventTypes.ServerACL: 100, }, "events_default": 0, "state_default": 50, "ban": 50, "kick": 50, "redact": 50, - "invite": 0, + "invite": 50, } if config["original_invitees_have_ops"]: for invitee in invite_list: power_level_content["users"][invitee] = 100 + # Power levels overrides are defined per chat preset + power_level_content.update(config["power_level_content_override"]) + if power_level_content_override: power_level_content.update(power_level_content_override) diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py index e3af280ba6..fb681a1db9 100644 --- a/tests/rest/client/v1/test_rooms.py +++ b/tests/rest/client/v1/test_rooms.py @@ -1612,7 +1612,9 @@ class ContextTestCase(unittest.HomeserverTestCase): def prepare(self, reactor, clock, homeserver): self.user_id = self.register_user("user", "password") self.tok = self.login("user", "password") - self.room_id = self.helper.create_room_as(self.user_id, tok=self.tok) + self.room_id = self.helper.create_room_as( + self.user_id, tok=self.tok, is_public=False + ) self.other_user_id = self.register_user("user2", "password") self.other_tok = self.login("user2", "password") From d2455ec3aafe2b06174e0343799b30e12d08171d Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 17 Feb 2020 11:30:50 +0000 Subject: [PATCH 1057/1623] wait for current_state_events_membership before delete_old_current_state_events (#6924) --- changelog.d/6924.bugfix | 1 + .../schema/delta/57/delete_old_current_state_events.sql | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 changelog.d/6924.bugfix diff --git a/changelog.d/6924.bugfix b/changelog.d/6924.bugfix new file mode 100644 index 0000000000..33e6611929 --- /dev/null +++ b/changelog.d/6924.bugfix @@ -0,0 +1 @@ +Fix a bug introduced in Synapse 1.10.0 which would cause room state to be cleared in the database if Synapse was upgraded direct from 1.2.1 or earlier to 1.10.0. diff --git a/synapse/storage/data_stores/main/schema/delta/57/delete_old_current_state_events.sql b/synapse/storage/data_stores/main/schema/delta/57/delete_old_current_state_events.sql index a133d87a19..aec06c8261 100644 --- a/synapse/storage/data_stores/main/schema/delta/57/delete_old_current_state_events.sql +++ b/synapse/storage/data_stores/main/schema/delta/57/delete_old_current_state_events.sql @@ -15,5 +15,8 @@ -- Add background update to go and delete current state events for rooms the -- server is no longer in. -INSERT into background_updates (update_name, progress_json) - VALUES ('delete_old_current_state_events', '{}'); +-- +-- this relies on the 'membership' column of current_state_events, so make sure +-- that's populated first! +INSERT into background_updates (update_name, progress_json, depends_on) + VALUES ('delete_old_current_state_events', '{}', 'current_state_events_membership'); From fd6d83ed96971e267fe4ce9c120d1d6ec87a3582 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 17 Feb 2020 16:27:33 +0000 Subject: [PATCH 1058/1623] 1.10.1 --- CHANGES.md | 9 +++++++++ changelog.d/6924.bugfix | 1 - debian/changelog | 6 ++++++ synapse/__init__.py | 2 +- 4 files changed, 16 insertions(+), 2 deletions(-) delete mode 100644 changelog.d/6924.bugfix diff --git a/CHANGES.md b/CHANGES.md index 0bce84f400..37b650a848 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,12 @@ +Synapse 1.10.1 (2020-02-17) +=========================== + +Bugfixes +-------- + +- Fix a bug introduced in Synapse 1.10.0 which would cause room state to be cleared in the database if Synapse was upgraded direct from 1.2.1 or earlier to 1.10.0. ([\#6924](https://github.com/matrix-org/synapse/issues/6924)) + + Synapse 1.10.0 (2020-02-12) =========================== diff --git a/changelog.d/6924.bugfix b/changelog.d/6924.bugfix deleted file mode 100644 index 33e6611929..0000000000 --- a/changelog.d/6924.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a bug introduced in Synapse 1.10.0 which would cause room state to be cleared in the database if Synapse was upgraded direct from 1.2.1 or earlier to 1.10.0. diff --git a/debian/changelog b/debian/changelog index cdc3b1a5c2..90314d36af 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.10.1) stable; urgency=medium + + * New synapse release 1.10.1. + + -- Synapse Packaging team Mon, 17 Feb 2020 16:27:28 +0000 + matrix-synapse-py3 (1.10.0) stable; urgency=medium * New synapse release 1.10.0. diff --git a/synapse/__init__.py b/synapse/__init__.py index 9d285fca38..8313f177d2 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -36,7 +36,7 @@ try: except ImportError: pass -__version__ = "1.10.0" +__version__ = "1.10.1" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From bc831d1d9a380efd4fc063565d5f1eda341e9644 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 17 Feb 2020 16:34:13 +0000 Subject: [PATCH 1059/1623] #6924 has been released in 1.10.1 --- changelog.d/6924.bugfix | 1 - 1 file changed, 1 deletion(-) delete mode 100644 changelog.d/6924.bugfix diff --git a/changelog.d/6924.bugfix b/changelog.d/6924.bugfix deleted file mode 100644 index 33e6611929..0000000000 --- a/changelog.d/6924.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a bug introduced in Synapse 1.10.0 which would cause room state to be cleared in the database if Synapse was upgraded direct from 1.2.1 or earlier to 1.10.0. From 3be2abd0a9a089a147b23c6d58fc26dde63faa27 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 18 Feb 2020 11:41:53 +0000 Subject: [PATCH 1060/1623] Kill off deprecated "config-on-the-fly" docker mode (#6918) Lots of people seem to get confused by this mode, and it's been deprecated since Synapse 1.1.0. It's time for it to go. --- changelog.d/6918.docker | 1 + docker/README.md | 23 +++++++++++-------- docker/start.py | 51 +++++++++++++++-------------------------- 3 files changed, 34 insertions(+), 41 deletions(-) create mode 100644 changelog.d/6918.docker diff --git a/changelog.d/6918.docker b/changelog.d/6918.docker new file mode 100644 index 0000000000..cc2db5e071 --- /dev/null +++ b/changelog.d/6918.docker @@ -0,0 +1 @@ +The deprecated "generate-config-on-the-fly" mode is no longer supported. diff --git a/docker/README.md b/docker/README.md index 9f112a01d0..8c337149ca 100644 --- a/docker/README.md +++ b/docker/README.md @@ -110,12 +110,12 @@ argument to `docker run`. ## Legacy dynamic configuration file support -For backwards-compatibility only, the docker image supports creating a dynamic -configuration file based on environment variables. This is now deprecated, but -is enabled when the `SYNAPSE_SERVER_NAME` variable is set (and `generate` is -not given). +The docker image used to support creating a dynamic configuration file based +on environment variables. This is no longer supported, and an error will be +raised if you try to run synapse without a config file. -To migrate from a dynamic configuration file to a static one, run the docker +It is, however, possible to generate a static configuration file based on +the environment variables that were previously used. To do this, run the docker container once with the environment variables set, and `migrate_config` command line option. For example: @@ -127,15 +127,20 @@ docker run -it --rm \ matrixdotorg/synapse:latest migrate_config ``` -This will generate the same configuration file as the legacy mode used, but -will store it in `/data/homeserver.yaml` instead of a temporary location. You -can then use it as shown above at [Running synapse](#running-synapse). +This will generate the same configuration file as the legacy mode used, and +will store it in `/data/homeserver.yaml`. You can then use it as shown above at +[Running synapse](#running-synapse). + +Note that the defaults used in this configuration file may be different to +those when generating a new config file with `generate`: for example, TLS is +enabled by default in this mode. You are encouraged to inspect the generated +configuration file and edit it to ensure it meets your needs. ## Building the image If you need to build the image from a Synapse checkout, use the following `docker build` command from the repo's root: - + ``` docker build -t matrixdotorg/synapse -f docker/Dockerfile . ``` diff --git a/docker/start.py b/docker/start.py index 97fd247f8f..2a25c9380e 100755 --- a/docker/start.py +++ b/docker/start.py @@ -188,11 +188,6 @@ def main(args, environ): else: ownership = "{}:{}".format(desired_uid, desired_gid) - log( - "Container running as UserID %s:%s, ENV (or defaults) requests %s:%s" - % (os.getuid(), os.getgid(), desired_uid, desired_gid) - ) - if ownership is None: log("Will not perform chmod/su-exec as UserID already matches request") @@ -213,38 +208,30 @@ def main(args, environ): if mode is not None: error("Unknown execution mode '%s'" % (mode,)) - if "SYNAPSE_SERVER_NAME" in environ: - # backwards-compatibility generate-a-config-on-the-fly mode - if "SYNAPSE_CONFIG_PATH" in environ: - error( - "SYNAPSE_SERVER_NAME can only be combined with SYNAPSE_CONFIG_PATH " - "in `generate` or `migrate_config` mode. To start synapse using a " - "config file, unset the SYNAPSE_SERVER_NAME environment variable." - ) + config_dir = environ.get("SYNAPSE_CONFIG_DIR", "/data") + config_path = environ.get("SYNAPSE_CONFIG_PATH", config_dir + "/homeserver.yaml") - config_path = "/compiled/homeserver.yaml" - log( - "Generating config file '%s' on-the-fly from environment variables.\n" - "Note that this mode is deprecated. You can migrate to a static config\n" - "file by running with 'migrate_config'. See the README for more details." - % (config_path,) - ) - - generate_config_from_template("/compiled", config_path, environ, ownership) - else: - config_dir = environ.get("SYNAPSE_CONFIG_DIR", "/data") - config_path = environ.get( - "SYNAPSE_CONFIG_PATH", config_dir + "/homeserver.yaml" - ) - if not os.path.exists(config_path): + if not os.path.exists(config_path): + if "SYNAPSE_SERVER_NAME" in environ: error( - "Config file '%s' does not exist. You should either create a new " - "config file by running with the `generate` argument (and then edit " - "the resulting file before restarting) or specify the path to an " - "existing config file with the SYNAPSE_CONFIG_PATH variable." + """\ +Config file '%s' does not exist. + +The synapse docker image no longer supports generating a config file on-the-fly +based on environment variables. You can migrate to a static config file by +running with 'migrate_config'. See the README for more details. +""" % (config_path,) ) + error( + "Config file '%s' does not exist. You should either create a new " + "config file by running with the `generate` argument (and then edit " + "the resulting file before restarting) or specify the path to an " + "existing config file with the SYNAPSE_CONFIG_PATH variable." + % (config_path,) + ) + log("Starting synapse with config file " + config_path) args = ["python", "-m", synapse_worker, "--config-path", config_path] From 8ee0d745169fa12fd116692a9484930bd7b38167 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 18 Feb 2020 12:05:45 +0000 Subject: [PATCH 1061/1623] Split the delegating documentation out of federate.md and trim it down --- docs/delegate.md | 87 +++++++++++++++++++++++++++++++ docs/federate.md | 133 ----------------------------------------------- 2 files changed, 87 insertions(+), 133 deletions(-) create mode 100644 docs/delegate.md diff --git a/docs/delegate.md b/docs/delegate.md new file mode 100644 index 0000000000..4126fffe71 --- /dev/null +++ b/docs/delegate.md @@ -0,0 +1,87 @@ +# Delegation + +For a more flexible configuration, you can have `server_name` +resources (eg: `@user:example.com`) served by a different host and +port (eg: `synapse.example.com:443`). + +Without configuring delegation, the matrix federation will +expect to find your server via `example.com:8448`. The following methods +allow you retain a `server_name` of `example.com` so that your user IDs, room +aliases, etc continue to look like `*:example.com`, whilst having your +federation traffic routed to a different server (e.g. `synapse.example.com`). + +## .well-known delegation + +To use this method, you need to be able to alter the +`server_name` 's https server to serve the `/.well-known/matrix/server` +URL. Having an active server (with a valid TLS certificate) serving your +`server_name` domain is out of the scope of this documentation. + +The URL `https:///.well-known/matrix/server` should +return a JSON structure containing the key `m.server` like so: + +```json +{ + "m.server": "[:]" +} +``` + +In our example, this would mean that URL `https://example.com/.well-known/matrix/server` +should return: + +```json +{ + "m.server": "synapse.example.com:443" +} +``` + +Note, specifying a port is optional. If no port is specified, then it defaults +to 8448. + +Most installations will not need to configure .well-known. However, it can be +useful in cases where the admin is hosting on behalf of someone else and +therefore cannot gain access to the necessary certificate. With .well-known, +federation servers will check for a valid TLS certificate for the delegated +hostname (in our example: `synapse.example.com`). + +## Delegation FAQ + +### When do I need delegation? + +If your homeserver's APIs are accessible on the default federation port (8448) +and the domain your `server_name` points to, you do not need any delegation. + +For instance, if you registered `example.com` and pointed its DNS A record at a +fresh server, you could install Synapse on that host, giving it a `server_name` +of `example.com`, and once a reverse proxy has been set up to proxy all requests +sent to the port `8448` and serve TLS certificates for `example.com`, you +wouldn't need any delegation set up. + +**However**, if your homeserver's APIs aren't accessible on port 8448 and on the +domain `server_name` points to, you will need to let other servers know how to +find it using delegation. + +### Do you still recommend against using a reverse proxy on the federation port? + +We no longer actively recommend against using a reverse proxy. Many admins will +find it easier to direct federation traffic to a reverse proxy and manage their +own TLS certificates, and this is a supported configuration. + +See [reverse_proxy.md](reverse_proxy.md) for information on setting up a +reverse proxy. + +### Do I still need to give my TLS certificates to Synapse if I am using a reverse proxy? + +This is no longer necessary. If you are using a reverse proxy for all of your +TLS traffic, then you can set `no_tls: True` in the Synapse config. + +In that case, the only reason Synapse needs the certificate is to populate a legacy +`tls_fingerprints` field in the federation API. This is ignored by Synapse 0.99.0 +and later, and the only time pre-0.99 Synapses will check it is when attempting to +fetch the server keys - and generally this is delegated via `matrix.org`, which +is running a modern version of Synapse. + +### Do I need the same certificate for the client and federation port? + +No. There is nothing stopping you from using different certificates, +particularly if you are using a reverse proxy. \ No newline at end of file diff --git a/docs/federate.md b/docs/federate.md index f9f17fcca5..8552927225 100644 --- a/docs/federate.md +++ b/docs/federate.md @@ -18,139 +18,6 @@ Once federation has been configured, you should be able to join a room over federation. A good place to start is ``#synapse:matrix.org`` - a room for Synapse admins. - -## Delegation - -For a more flexible configuration, you can have ``server_name`` -resources (eg: ``@user:example.com``) served by a different host and -port (eg: ``synapse.example.com:443``). There are two ways to do this: - -- adding a ``/.well-known/matrix/server`` URL served on ``https://example.com``. -- adding a DNS ``SRV`` record in the DNS zone of domain - ``example.com``. - -Without configuring delegation, the matrix federation will -expect to find your server via ``example.com:8448``. The following methods -allow you retain a `server_name` of `example.com` so that your user IDs, room -aliases, etc continue to look like `*:example.com`, whilst having your -federation traffic routed to a different server. - -### .well-known delegation - -To use this method, you need to be able to alter the -``server_name`` 's https server to serve the ``/.well-known/matrix/server`` -URL. Having an active server (with a valid TLS certificate) serving your -``server_name`` domain is out of the scope of this documentation. - -The URL ``https:///.well-known/matrix/server`` should -return a JSON structure containing the key ``m.server`` like so: - - { - "m.server": "[:]" - } - -In our example, this would mean that URL ``https://example.com/.well-known/matrix/server`` -should return: - - { - "m.server": "synapse.example.com:443" - } - -Note, specifying a port is optional. If a port is not specified an SRV lookup -is performed, as described below. If the target of the -delegation does not have an SRV record, then the port defaults to 8448. - -Most installations will not need to configure .well-known. However, it can be -useful in cases where the admin is hosting on behalf of someone else and -therefore cannot gain access to the necessary certificate. With .well-known, -federation servers will check for a valid TLS certificate for the delegated -hostname (in our example: ``synapse.example.com``). - -### DNS SRV delegation - -To use this delegation method, you need to have write access to your -``server_name`` 's domain zone DNS records (in our example it would be -``example.com`` DNS zone). - -This method requires the target server to provide a -valid TLS certificate for the original ``server_name``. - -You need to add a SRV record in your ``server_name`` 's DNS zone with -this format: - - _matrix._tcp. IN SRV - -In our example, we would need to add this SRV record in the -``example.com`` DNS zone: - - _matrix._tcp.example.com. 3600 IN SRV 10 5 443 synapse.example.com. - -Once done and set up, you can check the DNS record with ``dig -t srv -_matrix._tcp.``. In our example, we would expect this: - - $ dig -t srv _matrix._tcp.example.com - _matrix._tcp.example.com. 3600 IN SRV 10 0 443 synapse.example.com. - -Note that the target of a SRV record cannot be an alias (CNAME record): it has to point -directly to the server hosting the synapse instance. - -### Delegation FAQ -#### When do I need a SRV record or .well-known URI? - -If your homeserver listens on the default federation port (8448), and your -`server_name` points to the host that your homeserver runs on, you do not need an SRV -record or `.well-known/matrix/server` URI. - -For instance, if you registered `example.com` and pointed its DNS A record at a -fresh server, you could install Synapse on that host, -giving it a `server_name` of `example.com`, and once [ACME](acme.md) support is enabled, -it would automatically generate a valid TLS certificate for you via Let's Encrypt -and no SRV record or .well-known URI would be needed. - -**However**, if your server does not listen on port 8448, or if your `server_name` -does not point to the host that your homeserver runs on, you will need to let -other servers know how to find it. The way to do this is via .well-known or an -SRV record. - -#### I have created a .well-known URI. Do I also need an SRV record? - -No. You can use either `.well-known` delegation or use an SRV record for delegation. You -do not need to use both to delegate to the same location. - -#### Can I manage my own certificates rather than having Synapse renew certificates itself? - -Yes, you are welcome to manage your certificates yourself. Synapse will only -attempt to obtain certificates from Let's Encrypt if you configure it to do -so.The only requirement is that there is a valid TLS cert present for -federation end points. - -#### Do you still recommend against using a reverse proxy on the federation port? - -We no longer actively recommend against using a reverse proxy. Many admins will -find it easier to direct federation traffic to a reverse proxy and manage their -own TLS certificates, and this is a supported configuration. - -See [reverse_proxy.md](reverse_proxy.md) for information on setting up a -reverse proxy. - -#### Do I still need to give my TLS certificates to Synapse if I am using a reverse proxy? - -Practically speaking, this is no longer necessary. - -If you are using a reverse proxy for all of your TLS traffic, then you can set -`no_tls: True` in the Synapse config. In that case, the only reason Synapse -needs the certificate is to populate a legacy `tls_fingerprints` field in the -federation API. This is ignored by Synapse 0.99.0 and later, and the only time -pre-0.99 Synapses will check it is when attempting to fetch the server keys - -and generally this is delegated via `matrix.org`, which will be running a modern -version of Synapse. - -#### Do I need the same certificate for the client and federation port? - -No. There is nothing stopping you from using different certificates, -particularly if you are using a reverse proxy. However, Synapse will use the -same certificate on any ports where TLS is configured. - ## Troubleshooting You can use the [federation tester]( From fe3941f6e33a17fa7cdf209a4370f4e805341db4 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 18 Feb 2020 07:29:44 -0500 Subject: [PATCH 1062/1623] Stop sending events when creating or deleting aliases (#6904) Stop sending events when creating or deleting associations (room aliases). Send an updated canonical alias event if one of the alt_aliases is deleted. --- changelog.d/6904.removal | 1 + synapse/handlers/directory.py | 75 ++++++++------- synapse/handlers/room.py | 6 +- tests/handlers/test_directory.py | 154 ++++++++++++++++++++++++++++++- 4 files changed, 194 insertions(+), 42 deletions(-) create mode 100644 changelog.d/6904.removal diff --git a/changelog.d/6904.removal b/changelog.d/6904.removal new file mode 100644 index 0000000000..a5cc0c3605 --- /dev/null +++ b/changelog.d/6904.removal @@ -0,0 +1 @@ +Stop sending alias events during adding / removing aliases. Check alt_aliases in the latest canonical aliases event when deleting an alias. diff --git a/synapse/handlers/directory.py b/synapse/handlers/directory.py index 8c5980cb0c..f718388884 100644 --- a/synapse/handlers/directory.py +++ b/synapse/handlers/directory.py @@ -81,13 +81,7 @@ class DirectoryHandler(BaseHandler): @defer.inlineCallbacks def create_association( - self, - requester, - room_alias, - room_id, - servers=None, - send_event=True, - check_membership=True, + self, requester, room_alias, room_id, servers=None, check_membership=True, ): """Attempt to create a new alias @@ -97,7 +91,6 @@ class DirectoryHandler(BaseHandler): room_id (str) servers (list[str]|None): List of servers that others servers should try and join via - send_event (bool): Whether to send an updated m.room.aliases event check_membership (bool): Whether to check if the user is in the room before the alias can be set (if the server's config requires it). @@ -150,16 +143,9 @@ class DirectoryHandler(BaseHandler): ) yield self._create_association(room_alias, room_id, servers, creator=user_id) - if send_event: - try: - yield self.send_room_alias_update_event(requester, room_id) - except AuthError as e: - # sending the aliases event may fail due to the user not having - # permission in the room; this is permitted. - logger.info("Skipping updating aliases event due to auth error %s", e) @defer.inlineCallbacks - def delete_association(self, requester, room_alias, send_event=True): + def delete_association(self, requester, room_alias): """Remove an alias from the directory (this is only meant for human users; AS users should call @@ -168,9 +154,6 @@ class DirectoryHandler(BaseHandler): Args: requester (Requester): room_alias (RoomAlias): - send_event (bool): Whether to send an updated m.room.aliases event. - Note that, if we delete the canonical alias, we will always attempt - to send an m.room.canonical_alias event Returns: Deferred[unicode]: room id that the alias used to point to @@ -206,9 +189,6 @@ class DirectoryHandler(BaseHandler): room_id = yield self._delete_association(room_alias) try: - if send_event: - yield self.send_room_alias_update_event(requester, room_id) - yield self._update_canonical_alias( requester, requester.user.to_string(), room_id, room_alias ) @@ -319,25 +299,50 @@ class DirectoryHandler(BaseHandler): @defer.inlineCallbacks def _update_canonical_alias(self, requester, user_id, room_id, room_alias): + """ + Send an updated canonical alias event if the removed alias was set as + the canonical alias or listed in the alt_aliases field. + """ alias_event = yield self.state.get_current_state( room_id, EventTypes.CanonicalAlias, "" ) - alias_str = room_alias.to_string() - if not alias_event or alias_event.content.get("alias", "") != alias_str: + # There is no canonical alias, nothing to do. + if not alias_event: return - yield self.event_creation_handler.create_and_send_nonmember_event( - requester, - { - "type": EventTypes.CanonicalAlias, - "state_key": "", - "room_id": room_id, - "sender": user_id, - "content": {}, - }, - ratelimit=False, - ) + # Obtain a mutable version of the event content. + content = dict(alias_event.content) + send_update = False + + # Remove the alias property if it matches the removed alias. + alias_str = room_alias.to_string() + if alias_event.content.get("alias", "") == alias_str: + send_update = True + content.pop("alias", "") + + # Filter alt_aliases for the removed alias. + alt_aliases = content.pop("alt_aliases", None) + # If the aliases are not a list (or not found) do not attempt to modify + # the list. + if isinstance(alt_aliases, list): + send_update = True + alt_aliases = [alias for alias in alt_aliases if alias != alias_str] + if alt_aliases: + content["alt_aliases"] = alt_aliases + + if send_update: + yield self.event_creation_handler.create_and_send_nonmember_event( + requester, + { + "type": EventTypes.CanonicalAlias, + "state_key": "", + "room_id": room_id, + "sender": user_id, + "content": content, + }, + ratelimit=False, + ) @defer.inlineCallbacks def get_association_from_room_alias(self, room_alias): diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 033083acac..49ec2f48bc 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -478,9 +478,7 @@ class RoomCreationHandler(BaseHandler): for alias_str in aliases: alias = RoomAlias.from_string(alias_str) try: - yield directory_handler.delete_association( - requester, alias, send_event=False - ) + yield directory_handler.delete_association(requester, alias) removed_aliases.append(alias_str) except SynapseError as e: logger.warning("Unable to remove alias %s from old room: %s", alias, e) @@ -511,7 +509,6 @@ class RoomCreationHandler(BaseHandler): RoomAlias.from_string(alias), new_room_id, servers=(self.hs.hostname,), - send_event=False, check_membership=False, ) logger.info("Moved alias %s to new room", alias) @@ -664,7 +661,6 @@ class RoomCreationHandler(BaseHandler): room_id=room_id, room_alias=room_alias, servers=[self.hs.hostname], - send_event=False, check_membership=False, ) diff --git a/tests/handlers/test_directory.py b/tests/handlers/test_directory.py index ee88cf5a4b..27b916aed4 100644 --- a/tests/handlers/test_directory.py +++ b/tests/handlers/test_directory.py @@ -18,9 +18,11 @@ from mock import Mock from twisted.internet import defer +import synapse.api.errors +from synapse.api.constants import EventTypes from synapse.config.room_directory import RoomDirectoryConfig -from synapse.rest.client.v1 import directory, room -from synapse.types import RoomAlias +from synapse.rest.client.v1 import directory, login, room +from synapse.types import RoomAlias, create_requester from tests import unittest @@ -85,6 +87,38 @@ class DirectoryTestCase(unittest.HomeserverTestCase): ignore_backoff=True, ) + def test_delete_alias_not_allowed(self): + room_id = "!8765qwer:test" + self.get_success( + self.store.create_room_alias_association(self.my_room, room_id, ["test"]) + ) + + self.get_failure( + self.handler.delete_association( + create_requester("@user:test"), self.my_room + ), + synapse.api.errors.AuthError, + ) + + def test_delete_alias(self): + room_id = "!8765qwer:test" + user_id = "@user:test" + self.get_success( + self.store.create_room_alias_association( + self.my_room, room_id, ["test"], user_id + ) + ) + + result = self.get_success( + self.handler.delete_association(create_requester(user_id), self.my_room) + ) + self.assertEquals(room_id, result) + + # The alias should not be found. + self.get_failure( + self.handler.get_association(self.my_room), synapse.api.errors.SynapseError + ) + def test_incoming_fed_query(self): self.get_success( self.store.create_room_alias_association( @@ -99,6 +133,122 @@ class DirectoryTestCase(unittest.HomeserverTestCase): self.assertEquals({"room_id": "!8765asdf:test", "servers": ["test"]}, response) +class CanonicalAliasTestCase(unittest.HomeserverTestCase): + """Test modifications of the canonical alias when delete aliases. + """ + + servlets = [ + synapse.rest.admin.register_servlets, + login.register_servlets, + room.register_servlets, + directory.register_servlets, + ] + + def prepare(self, reactor, clock, hs): + self.store = hs.get_datastore() + self.handler = hs.get_handlers().directory_handler + self.state_handler = hs.get_state_handler() + + # Create user + self.admin_user = self.register_user("admin", "pass", admin=True) + self.admin_user_tok = self.login("admin", "pass") + + # Create a test room + self.room_id = self.helper.create_room_as( + self.admin_user, tok=self.admin_user_tok + ) + + self.test_alias = "#test:test" + self.room_alias = RoomAlias.from_string(self.test_alias) + + # Create a new alias to this room. + self.get_success( + self.store.create_room_alias_association( + self.room_alias, self.room_id, ["test"], self.admin_user + ) + ) + + def test_remove_alias(self): + """Removing an alias that is the canonical alias should remove it there too.""" + # Set this new alias as the canonical alias for this room + self.helper.send_state( + self.room_id, + "m.room.canonical_alias", + {"alias": self.test_alias, "alt_aliases": [self.test_alias]}, + tok=self.admin_user_tok, + ) + + data = self.get_success( + self.state_handler.get_current_state( + self.room_id, EventTypes.CanonicalAlias, "" + ) + ) + self.assertEqual(data["content"]["alias"], self.test_alias) + self.assertEqual(data["content"]["alt_aliases"], [self.test_alias]) + + # Finally, delete the alias. + self.get_success( + self.handler.delete_association( + create_requester(self.admin_user), self.room_alias + ) + ) + + data = self.get_success( + self.state_handler.get_current_state( + self.room_id, EventTypes.CanonicalAlias, "" + ) + ) + self.assertNotIn("alias", data["content"]) + self.assertNotIn("alt_aliases", data["content"]) + + def test_remove_other_alias(self): + """Removing an alias listed as in alt_aliases should remove it there too.""" + # Create a second alias. + other_test_alias = "#test2:test" + other_room_alias = RoomAlias.from_string(other_test_alias) + self.get_success( + self.store.create_room_alias_association( + other_room_alias, self.room_id, ["test"], self.admin_user + ) + ) + + # Set the alias as the canonical alias for this room. + self.helper.send_state( + self.room_id, + "m.room.canonical_alias", + { + "alias": self.test_alias, + "alt_aliases": [self.test_alias, other_test_alias], + }, + tok=self.admin_user_tok, + ) + + data = self.get_success( + self.state_handler.get_current_state( + self.room_id, EventTypes.CanonicalAlias, "" + ) + ) + self.assertEqual(data["content"]["alias"], self.test_alias) + self.assertEqual( + data["content"]["alt_aliases"], [self.test_alias, other_test_alias] + ) + + # Delete the second alias. + self.get_success( + self.handler.delete_association( + create_requester(self.admin_user), other_room_alias + ) + ) + + data = self.get_success( + self.state_handler.get_current_state( + self.room_id, EventTypes.CanonicalAlias, "" + ) + ) + self.assertEqual(data["content"]["alias"], self.test_alias) + self.assertEqual(data["content"]["alt_aliases"], [self.test_alias]) + + class TestCreateAliasACL(unittest.HomeserverTestCase): user_id = "@test:test" From 3c67eee6dc960ce7218f15489594a292a2708bd8 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 18 Feb 2020 13:51:03 +0000 Subject: [PATCH 1063/1623] Make federate.md more of a sumary of the steps to follow to set up replication --- docs/federate.md | 45 ++++++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/docs/federate.md b/docs/federate.md index 8552927225..255e907b58 100644 --- a/docs/federate.md +++ b/docs/federate.md @@ -1,30 +1,41 @@ -Setting up Federation +Setting up federation ===================== Federation is the process by which users on different servers can participate in the same room. For this to work, those other servers must be able to contact yours to send messages. -The ``server_name`` configured in the Synapse configuration file (often -``homeserver.yaml``) defines how resources (users, rooms, etc.) will be -identified (eg: ``@user:example.com``, ``#room:example.com``). By -default, it is also the domain that other servers will use to -try to reach your server (via port 8448). This is easy to set -up and will work provided you set the ``server_name`` to match your -machine's public DNS hostname, and provide Synapse with a TLS certificate -which is valid for your ``server_name``. +The `server_name` configured in the Synapse configuration file (often +`homeserver.yaml`) defines how resources (users, rooms, etc.) will be +identified (eg: `@user:example.com`, `#room:example.com`). By default, +it is also the domain that other servers will use to try to reach your +server (via port 8448). This is easy to set up and will work provided +you set the `server_name` to match your machine's public DNS hostname. + +You will also need a valid TLS certificate for this `server_name` served +on port 8448 - the preferred way to do that is by using a reverse proxy, +see [reverse_proxy.md]() for instructions on how to +correctly set one up. + +In some cases you might not want Synapse to be running on the machine that +has the `server_name` as its public DNS hostname, or federation traffic +to use port than 8448 (e.g. you want to use `example.com` as your `server_name` +but want Synapse to be reachable on `synapse.example.com:443`). This can +be done using delegation, which allows an admin to dictate where federation +traffic should be sent, see [delegate.md]() for instructions on +how to set this up. Once federation has been configured, you should be able to join a room over -federation. A good place to start is ``#synapse:matrix.org`` - a room for +federation. A good place to start is `#synapse:matrix.org` - a room for Synapse admins. ## Troubleshooting -You can use the [federation tester]( -) to check if your homeserver is -configured correctly. Alternatively try the [JSON API used by the federation tester](https://matrix.org/federationtester/api/report?server_name=DOMAIN). -Note that you'll have to modify this URL to replace ``DOMAIN`` with your -``server_name``. Hitting the API directly provides extra detail. +You can use the [federation tester]() +to check if your homeserver is configured correctly. Alternatively try the +[JSON API used by the federation tester](https://matrix.org/federationtester/api/report?server_name=DOMAIN). +Note that you'll have to modify this URL to replace `DOMAIN` with your +`server_name`. Hitting the API directly provides extra detail. The typical failure mode for federation is that when the server tries to join a room, it is rejected with "401: Unauthorized". Generally this means that other @@ -36,8 +47,8 @@ you invite them to. This can be caused by an incorrectly-configured reverse proxy: see [reverse_proxy.md]() for instructions on how to correctly configure a reverse proxy. -## Running a Demo Federation of Synapses +## Running a demo federation of Synapses If you want to get up and running quickly with a trio of homeservers in a -private federation, there is a script in the ``demo`` directory. This is mainly +private federation, there is a script in the `demo` directory. This is mainly useful just for development purposes. See [demo/README](<../demo/README>). From e837be5b5cb43406bd124e3f27a2b7be1bd31aa8 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 18 Feb 2020 13:53:46 +0000 Subject: [PATCH 1064/1623] Fix links in the reverse proxy doc --- docs/reverse_proxy.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/reverse_proxy.md b/docs/reverse_proxy.md index dcfc5c64aa..af6d73927a 100644 --- a/docs/reverse_proxy.md +++ b/docs/reverse_proxy.md @@ -18,9 +18,10 @@ When setting up a reverse proxy, remember that Matrix clients and other Matrix servers do not necessarily need to connect to your server via the same server name or port. Indeed, clients will use port 443 by default, whereas servers default to port 8448. Where these are different, we -refer to the 'client port' and the \'federation port\'. See [Setting -up federation](federate.md) for more details of the algorithm used for -federation connections. +refer to the 'client port' and the \'federation port\'. See [the Matrix +specification](https://matrix.org/docs/spec/server_server/latest#resolving-server-names) +for more details of the algorithm used for federation connections, and +[delegate.md]() for instructions on setting up delegation. Let's assume that we expect clients to connect to our server at `https://matrix.example.com`, and other servers to connect at From ba7a5238547cf6c23dda37b2f2424ea5af5d9192 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 18 Feb 2020 13:57:15 +0000 Subject: [PATCH 1065/1623] Argh trailing spaces --- docs/federate.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/federate.md b/docs/federate.md index 255e907b58..e85a077c34 100644 --- a/docs/federate.md +++ b/docs/federate.md @@ -10,7 +10,7 @@ The `server_name` configured in the Synapse configuration file (often identified (eg: `@user:example.com`, `#room:example.com`). By default, it is also the domain that other servers will use to try to reach your server (via port 8448). This is easy to set up and will work provided -you set the `server_name` to match your machine's public DNS hostname. +you set the `server_name` to match your machine's public DNS hostname. You will also need a valid TLS certificate for this `server_name` served on port 8448 - the preferred way to do that is by using a reverse proxy, @@ -23,7 +23,7 @@ to use port than 8448 (e.g. you want to use `example.com` as your `server_name` but want Synapse to be reachable on `synapse.example.com:443`). This can be done using delegation, which allows an admin to dictate where federation traffic should be sent, see [delegate.md]() for instructions on -how to set this up. +how to set this up. Once federation has been configured, you should be able to join a room over federation. A good place to start is `#synapse:matrix.org` - a room for From d009535639116ba27268a44ed2cd5514a9f8a6a8 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 18 Feb 2020 14:07:41 +0000 Subject: [PATCH 1066/1623] Add mention of SRV records as an advanced topic --- docs/delegate.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/delegate.md b/docs/delegate.md index 4126fffe71..7b55827cec 100644 --- a/docs/delegate.md +++ b/docs/delegate.md @@ -44,6 +44,16 @@ therefore cannot gain access to the necessary certificate. With .well-known, federation servers will check for a valid TLS certificate for the delegated hostname (in our example: `synapse.example.com`). +## SRV DNS record delegation + +It is also possible to do delegation using a SRV DNS record. However, that is +considered an advanced topic since it's a bit complex to set up, and `.well-known` +delegation is already enough in most cases. + +However, if you really need it, you can find some documentation on how such a +record should look like and how Synapse will use it in [the Matrix +specification](https://matrix.org/docs/spec/server_server/latest#resolving-server-names). + ## Delegation FAQ ### When do I need delegation? From b1255077f584260a2296d4f3b7b78b54596a76b5 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 18 Feb 2020 14:27:57 +0000 Subject: [PATCH 1067/1623] Changelog --- changelog.d/6940.doc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6940.doc diff --git a/changelog.d/6940.doc b/changelog.d/6940.doc new file mode 100644 index 0000000000..8c75f48d3d --- /dev/null +++ b/changelog.d/6940.doc @@ -0,0 +1 @@ +Clean up and update docs on setting up federation. From bfbe2f5b08857dc845664645b9d4e24fe479d2a0 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 18 Feb 2020 15:10:41 +0000 Subject: [PATCH 1068/1623] Print the error as an error log and raise the same exception we got --- synapse/handlers/acme.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/synapse/handlers/acme.py b/synapse/handlers/acme.py index e6797535e6..2942df3ac7 100644 --- a/synapse/handlers/acme.py +++ b/synapse/handlers/acme.py @@ -22,10 +22,17 @@ from twisted.web import server, static from twisted.web.resource import Resource from synapse.app import check_bind_error -from synapse.config import ConfigError logger = logging.getLogger(__name__) +ACME_REGISTER_FAIL_ERROR = """ +Failed to register with the ACME provider. This is likely happening because the install +is new, and ACME v1 has been deprecated by Let's Encrypt and is disabled for installs set +up after November 2019. +At the moment, Synapse doesn't support ACME v2. For more info and alternative solution, +check out https://github.com/matrix-org/synapse/blob/master/docs/ACME.md#deprecation-of-acme-v1 +------------------------------------------------------""" + class AcmeHandler(object): def __init__(self, hs): @@ -76,14 +83,8 @@ class AcmeHandler(object): try: yield self._issuer._ensure_registered() except Exception: - raise ConfigError( - "Failed to register with the ACME provider. This is likely happening" - " because the install is new, and ACME v1 has been deprecated by Let's" - " Encrypt and is disabled for installs set up after November 2019. At the" - " moment, Synapse doesn't support ACME v2. For more info and alternative" - " solution, check out https://github.com/matrix-org/synapse/blob/master" - "/docs/ACME.md#deprecation-of-acme-v1" - ) + logger.error(ACME_REGISTER_FAIL_ERROR) + raise @defer.inlineCallbacks def provision_certificate(self): From 9801a042f3e5dc5ad623ab5a2f39661a2ccbd8f9 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 18 Feb 2020 15:15:43 +0000 Subject: [PATCH 1069/1623] Make the log more noticeable --- synapse/handlers/acme.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/synapse/handlers/acme.py b/synapse/handlers/acme.py index 2942df3ac7..250faa997b 100644 --- a/synapse/handlers/acme.py +++ b/synapse/handlers/acme.py @@ -26,12 +26,13 @@ from synapse.app import check_bind_error logger = logging.getLogger(__name__) ACME_REGISTER_FAIL_ERROR = """ +-------------------------------------------------------------------------------- Failed to register with the ACME provider. This is likely happening because the install is new, and ACME v1 has been deprecated by Let's Encrypt and is disabled for installs set up after November 2019. At the moment, Synapse doesn't support ACME v2. For more info and alternative solution, check out https://github.com/matrix-org/synapse/blob/master/docs/ACME.md#deprecation-of-acme-v1 -------------------------------------------------------""" +--------------------------------------------------------------------------------""" class AcmeHandler(object): From 818def82486627513dc95e64c46c0bb452651e7e Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 18 Feb 2020 15:27:45 +0000 Subject: [PATCH 1070/1623] Fix worker docs to point `/publicised_groups` API correctly. (#6938) --- changelog.d/6938.doc | 1 + docs/workers.md | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 changelog.d/6938.doc diff --git a/changelog.d/6938.doc b/changelog.d/6938.doc new file mode 100644 index 0000000000..117f76f48a --- /dev/null +++ b/changelog.d/6938.doc @@ -0,0 +1 @@ +Fix worker docs to point `/publicised_groups` API correctly. diff --git a/docs/workers.md b/docs/workers.md index 6f7ec58780..0d84a58958 100644 --- a/docs/workers.md +++ b/docs/workers.md @@ -261,7 +261,8 @@ following regular expressions: ^/_matrix/client/versions$ ^/_matrix/client/(api/v1|r0|unstable)/voip/turnServer$ ^/_matrix/client/(api/v1|r0|unstable)/joined_groups$ - ^/_matrix/client/(api/v1|r0|unstable)/get_groups_publicised$ + ^/_matrix/client/(api/v1|r0|unstable)/publicised_groups$ + ^/_matrix/client/(api/v1|r0|unstable)/publicised_groups/ Additionally, the following REST endpoints can be handled for GET requests: @@ -287,8 +288,8 @@ the following regular expressions: ^/_matrix/client/(api/v1|r0|unstable)/user_directory/search$ -When using this worker you must also set `update_user_directory: False` in the -shared configuration file to stop the main synapse running background +When using this worker you must also set `update_user_directory: False` in the +shared configuration file to stop the main synapse running background jobs related to updating the user directory. ### `synapse.app.frontend_proxy` From 8a380d0fe24edd746256d652836ec27003a05e7e Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 18 Feb 2020 15:39:09 +0000 Subject: [PATCH 1071/1623] Increase perf of `get_auth_chain_ids` used in state res v2. (#6937) We do this by moving the recursive query to be fully in the DB. --- changelog.d/6937.misc | 1 + .../data_stores/main/event_federation.py | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 changelog.d/6937.misc diff --git a/changelog.d/6937.misc b/changelog.d/6937.misc new file mode 100644 index 0000000000..6d00e58654 --- /dev/null +++ b/changelog.d/6937.misc @@ -0,0 +1 @@ +Increase perf of `get_auth_chain_ids` used in state res v2. diff --git a/synapse/storage/data_stores/main/event_federation.py b/synapse/storage/data_stores/main/event_federation.py index 60c67457b4..1746f40adf 100644 --- a/synapse/storage/data_stores/main/event_federation.py +++ b/synapse/storage/data_stores/main/event_federation.py @@ -26,6 +26,7 @@ from synapse.storage._base import SQLBaseStore, make_in_list_sql_clause from synapse.storage.data_stores.main.events_worker import EventsWorkerStore from synapse.storage.data_stores.main.signatures import SignatureWorkerStore from synapse.storage.database import Database +from synapse.storage.engines import PostgresEngine from synapse.util.caches.descriptors import cached logger = logging.getLogger(__name__) @@ -61,6 +62,28 @@ class EventFederationWorkerStore(EventsWorkerStore, SignatureWorkerStore, SQLBas ) def _get_auth_chain_ids_txn(self, txn, event_ids, include_given): + if isinstance(self.database_engine, PostgresEngine): + # For efficiency we make the database do this if we can. + sql = """ + WITH RECURSIVE auth_chain(event_id) AS ( + SELECT auth_id FROM event_auth WHERE event_id = ANY(?) + UNION + SELECT auth_id FROM event_auth + INNER JOIN auth_chain USING (event_id) + ) + SELECT event_id FROM auth_chain + """ + txn.execute(sql, (list(event_ids),)) + + results = set(event_id for event_id, in txn) + + if include_given: + results.update(event_ids) + + return list(results) + + # Database doesn't necessarily support recursive CTE, so we fall + # back to do doing it manually. if include_given: results = set(event_ids) else: From a0d2f9d089b33ead392d1ac8ded0b44512cd6273 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 18 Feb 2020 16:16:49 +0000 Subject: [PATCH 1072/1623] Phrasing --- docs/federate.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/federate.md b/docs/federate.md index e85a077c34..55f2a6d0ee 100644 --- a/docs/federate.md +++ b/docs/federate.md @@ -13,7 +13,7 @@ server (via port 8448). This is easy to set up and will work provided you set the `server_name` to match your machine's public DNS hostname. You will also need a valid TLS certificate for this `server_name` served -on port 8448 - the preferred way to do that is by using a reverse proxy, +on port 8448. The preferred way to do that is by using a reverse proxy, see [reverse_proxy.md]() for instructions on how to correctly set one up. From adfaea8c698a38ffe14ac682a946abc9f8152635 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 18 Feb 2020 16:23:25 +0000 Subject: [PATCH 1073/1623] Implement GET /_matrix/client/r0/rooms/{roomId}/aliases (#6939) per matrix-org/matrix-doc#2432 --- changelog.d/6939.feature | 1 + synapse/handlers/directory.py | 17 +++++++- synapse/rest/client/v1/room.py | 23 ++++++++++ tests/rest/client/v1/test_rooms.py | 70 +++++++++++++++++++++++++++++- tests/unittest.py | 28 ++++++++---- 5 files changed, 128 insertions(+), 11 deletions(-) create mode 100644 changelog.d/6939.feature diff --git a/changelog.d/6939.feature b/changelog.d/6939.feature new file mode 100644 index 0000000000..40fe7fc9a9 --- /dev/null +++ b/changelog.d/6939.feature @@ -0,0 +1 @@ +Implement `GET /_matrix/client/r0/rooms/{roomId}/aliases` endpoint as per [MSC2432](https://github.com/matrix-org/matrix-doc/pull/2432). diff --git a/synapse/handlers/directory.py b/synapse/handlers/directory.py index f718388884..3f8c792149 100644 --- a/synapse/handlers/directory.py +++ b/synapse/handlers/directory.py @@ -16,6 +16,7 @@ import logging import string +from typing import List from twisted.internet import defer @@ -28,7 +29,7 @@ from synapse.api.errors import ( StoreError, SynapseError, ) -from synapse.types import RoomAlias, UserID, get_domain_from_id +from synapse.types import Requester, RoomAlias, UserID, get_domain_from_id from ._base import BaseHandler @@ -452,3 +453,17 @@ class DirectoryHandler(BaseHandler): yield self.store.set_room_is_public_appservice( room_id, appservice_id, network_id, visibility == "public" ) + + async def get_aliases_for_room( + self, requester: Requester, room_id: str + ) -> List[str]: + """ + Get a list of the aliases that currently point to this room on this server + """ + # allow access to server admins and current members of the room + is_admin = await self.auth.is_server_admin(requester.user) + if not is_admin: + await self.auth.check_joined_room(room_id, requester.user.to_string()) + + aliases = await self.store.get_aliases_for_room(room_id) + return aliases diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index 6f31584c51..143dc738c6 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -45,6 +45,10 @@ from synapse.storage.state import StateFilter from synapse.streams.config import PaginationConfig from synapse.types import RoomAlias, RoomID, StreamToken, ThirdPartyInstanceID, UserID +MYPY = False +if MYPY: + import synapse.server + logger = logging.getLogger(__name__) @@ -843,6 +847,24 @@ class RoomTypingRestServlet(RestServlet): return 200, {} +class RoomAliasListServlet(RestServlet): + PATTERNS = client_patterns("/rooms/(?P[^/]*)/aliases", unstable=False) + + def __init__(self, hs: "synapse.server.HomeServer"): + super().__init__() + self.auth = hs.get_auth() + self.directory_handler = hs.get_handlers().directory_handler + + async def on_GET(self, request, room_id): + requester = await self.auth.get_user_by_req(request) + + alias_list = await self.directory_handler.get_aliases_for_room( + requester, room_id + ) + + return 200, {"aliases": alias_list} + + class SearchRestServlet(RestServlet): PATTERNS = client_patterns("/search$", v1=True) @@ -931,6 +953,7 @@ def register_servlets(hs, http_server): JoinedRoomsRestServlet(hs).register(http_server) RoomEventServlet(hs).register(http_server) RoomEventContextServlet(hs).register(http_server) + RoomAliasListServlet(hs).register(http_server) def register_deprecated_servlets(hs, http_server): diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py index fb681a1db9..fb08a45d27 100644 --- a/tests/rest/client/v1/test_rooms.py +++ b/tests/rest/client/v1/test_rooms.py @@ -28,8 +28,9 @@ from twisted.internet import defer import synapse.rest.admin from synapse.api.constants import EventContentFields, EventTypes, Membership from synapse.handlers.pagination import PurgeStatus -from synapse.rest.client.v1 import login, profile, room +from synapse.rest.client.v1 import directory, login, profile, room from synapse.rest.client.v2_alpha import account +from synapse.types import JsonDict, RoomAlias from synapse.util.stringutils import random_string from tests import unittest @@ -1726,3 +1727,70 @@ class ContextTestCase(unittest.HomeserverTestCase): self.assertEqual(len(events_after), 2, events_after) self.assertDictEqual(events_after[0].get("content"), {}, events_after[0]) self.assertEqual(events_after[1].get("content"), {}, events_after[1]) + + +class DirectoryTestCase(unittest.HomeserverTestCase): + + servlets = [ + synapse.rest.admin.register_servlets_for_client_rest_resource, + directory.register_servlets, + login.register_servlets, + room.register_servlets, + ] + + def prepare(self, reactor, clock, homeserver): + self.room_owner = self.register_user("room_owner", "test") + self.room_owner_tok = self.login("room_owner", "test") + + self.room_id = self.helper.create_room_as( + self.room_owner, tok=self.room_owner_tok + ) + + def test_no_aliases(self): + res = self._get_aliases(self.room_owner_tok) + self.assertEqual(res["aliases"], []) + + def test_not_in_room(self): + self.register_user("user", "test") + user_tok = self.login("user", "test") + res = self._get_aliases(user_tok, expected_code=403) + self.assertEqual(res["errcode"], "M_FORBIDDEN") + + def test_with_aliases(self): + alias1 = self._random_alias() + alias2 = self._random_alias() + + self._set_alias_via_directory(alias1) + self._set_alias_via_directory(alias2) + + res = self._get_aliases(self.room_owner_tok) + self.assertEqual(set(res["aliases"]), {alias1, alias2}) + + def _get_aliases(self, access_token: str, expected_code: int = 200) -> JsonDict: + """Calls the endpoint under test. returns the json response object.""" + request, channel = self.make_request( + "GET", + "/_matrix/client/r0/rooms/%s/aliases" % (self.room_id,), + access_token=access_token, + ) + self.render(request) + self.assertEqual(channel.code, expected_code, channel.result) + res = channel.json_body + self.assertIsInstance(res, dict) + if expected_code == 200: + self.assertIsInstance(res["aliases"], list) + return res + + def _random_alias(self) -> str: + return RoomAlias(random_string(5), self.hs.hostname).to_string() + + def _set_alias_via_directory(self, alias: str, expected_code: int = 200): + url = "/_matrix/client/r0/directory/room/" + alias + data = {"room_id": self.room_id} + request_data = json.dumps(data) + + request, channel = self.make_request( + "PUT", url, request_data, access_token=self.room_owner_tok + ) + self.render(request) + self.assertEqual(channel.code, expected_code, channel.result) diff --git a/tests/unittest.py b/tests/unittest.py index 98bf27d39c..8816a4d152 100644 --- a/tests/unittest.py +++ b/tests/unittest.py @@ -21,6 +21,7 @@ import hmac import inspect import logging import time +from typing import Optional, Tuple, Type, TypeVar, Union from mock import Mock @@ -42,7 +43,13 @@ from synapse.server import HomeServer from synapse.types import Requester, UserID, create_requester from synapse.util.ratelimitutils import FederationRateLimiter -from tests.server import get_clock, make_request, render, setup_test_homeserver +from tests.server import ( + FakeChannel, + get_clock, + make_request, + render, + setup_test_homeserver, +) from tests.test_utils.logging_setup import setup_logging from tests.utils import default_config, setupdb @@ -71,6 +78,9 @@ def around(target): return _around +T = TypeVar("T") + + class TestCase(unittest.TestCase): """A subclass of twisted.trial's TestCase which looks for 'loglevel' attributes on both itself and its individual test methods, to override the @@ -334,14 +344,14 @@ class HomeserverTestCase(TestCase): def make_request( self, - method, - path, - content=b"", - access_token=None, - request=SynapseRequest, - shorthand=True, - federation_auth_origin=None, - ): + method: Union[bytes, str], + path: Union[bytes, str], + content: Union[bytes, dict] = b"", + access_token: Optional[str] = None, + request: Type[T] = SynapseRequest, + shorthand: bool = True, + federation_auth_origin: str = None, + ) -> Tuple[T, FakeChannel]: """ Create a SynapseRequest at the path using the method and containing the given content. From 61b457e3ec72ec005605b8569eae8b8e547101ee Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 18 Feb 2020 17:20:03 +0000 Subject: [PATCH 1074/1623] Incorporate review --- docs/delegate.md | 22 +++++++++------------- docs/federate.md | 16 ++++++++-------- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/docs/delegate.md b/docs/delegate.md index 7b55827cec..57ae2c4c70 100644 --- a/docs/delegate.md +++ b/docs/delegate.md @@ -1,14 +1,13 @@ # Delegation -For a more flexible configuration, you can have `server_name` -resources (eg: `@user:example.com`) served by a different host and -port (eg: `synapse.example.com:443`). +Without configuring delegation, homeservers will expect the server +responsible for resources using e.g. `example.com` as their `server_name` +(e.g. `@user:example.com`) to be served at `example.com:8448`. -Without configuring delegation, the matrix federation will -expect to find your server via `example.com:8448`. The following methods -allow you retain a `server_name` of `example.com` so that your user IDs, room -aliases, etc continue to look like `*:example.com`, whilst having your -federation traffic routed to a different server (e.g. `synapse.example.com`). +Delegation is a Matrix feature allowing a homeserver admin to retain a +`server_name` of `example.com` so that your user IDs, room aliases, etc +continue to look like `*:example.com`, whilst having your federation +traffic routed to a different server and/or port (e.g. `synapse.example.com:443`). ## .well-known delegation @@ -38,11 +37,8 @@ should return: Note, specifying a port is optional. If no port is specified, then it defaults to 8448. -Most installations will not need to configure .well-known. However, it can be -useful in cases where the admin is hosting on behalf of someone else and -therefore cannot gain access to the necessary certificate. With .well-known, -federation servers will check for a valid TLS certificate for the delegated -hostname (in our example: `synapse.example.com`). +With .well-known, federation servers will check for a valid TLS certificate +for the delegated hostname (in our example: `synapse.example.com`). ## SRV DNS record delegation diff --git a/docs/federate.md b/docs/federate.md index 55f2a6d0ee..5fc839b58b 100644 --- a/docs/federate.md +++ b/docs/federate.md @@ -17,13 +17,13 @@ on port 8448. The preferred way to do that is by using a reverse proxy, see [reverse_proxy.md]() for instructions on how to correctly set one up. -In some cases you might not want Synapse to be running on the machine that -has the `server_name` as its public DNS hostname, or federation traffic -to use port than 8448 (e.g. you want to use `example.com` as your `server_name` -but want Synapse to be reachable on `synapse.example.com:443`). This can -be done using delegation, which allows an admin to dictate where federation -traffic should be sent, see [delegate.md]() for instructions on -how to set this up. +In some cases you might not want to run Synapse on the machine that has +the `server_name` as its public DNS hostname, or you might want federation +traffic to use a different port than 8448. For example, you might want to +have your user names look like `@user:example.com`, but you want to run +Synapse on `synapse.example.com` on port 443. This can be done using +delegation, which allows an admin to control where federation traffic should +be sent. See [delegate.md](delegate.md) for instructions on how to set this up. Once federation has been configured, you should be able to join a room over federation. A good place to start is `#synapse:matrix.org` - a room for @@ -31,7 +31,7 @@ Synapse admins. ## Troubleshooting -You can use the [federation tester]() +You can use the [federation tester](https://matrix.org/federationtester) to check if your homeserver is configured correctly. Alternatively try the [JSON API used by the federation tester](https://matrix.org/federationtester/api/report?server_name=DOMAIN). Note that you'll have to modify this URL to replace `DOMAIN` with your From f31a94a6dd1d27cf53fd7865057ac86b669e910f Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 18 Feb 2020 17:29:57 +0000 Subject: [PATCH 1075/1623] Fix log in message retention purge jobs --- synapse/handlers/pagination.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/handlers/pagination.py b/synapse/handlers/pagination.py index caf841a643..9bf6d39668 100644 --- a/synapse/handlers/pagination.py +++ b/synapse/handlers/pagination.py @@ -133,7 +133,7 @@ class PaginationHandler(object): include_null = False logger.info( - "[purge] Running purge job for %d < max_lifetime <= %d (include NULLs = %s)", + "[purge] Running purge job for %s < max_lifetime <= %s (include NULLs = %s)", min_ms, max_ms, include_null, From 771d70e89cde1645650bce0b3ec5d1ac4b8bd8f5 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 18 Feb 2020 17:31:02 +0000 Subject: [PATCH 1076/1623] Changelog --- changelog.d/6945.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6945.bugfix diff --git a/changelog.d/6945.bugfix b/changelog.d/6945.bugfix new file mode 100644 index 0000000000..78470a0ef6 --- /dev/null +++ b/changelog.d/6945.bugfix @@ -0,0 +1 @@ +Fix bogus log in the purge jobs related to the message retention policies support. From b58d17e44f9d9ff7e70578e0f4e328bb9113ec7e Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 18 Feb 2020 23:13:29 +0000 Subject: [PATCH 1077/1623] Refactor the membership check methods in Auth these were getting a bit unwieldy, so let's combine `check_joined_room` and `check_user_was_in_room` into a single `check_user_in_room`. --- synapse/api/auth.py | 80 +++++++++++++++----------------- synapse/handlers/initial_sync.py | 31 ++----------- synapse/handlers/typing.py | 4 +- tests/handlers/test_typing.py | 4 +- 4 files changed, 46 insertions(+), 73 deletions(-) diff --git a/synapse/api/auth.py b/synapse/api/auth.py index 8b1277ad02..de7b75ca36 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -14,6 +14,7 @@ # limitations under the License. import logging +from typing import Optional from six import itervalues @@ -35,6 +36,7 @@ from synapse.api.errors import ( ) from synapse.api.room_versions import KNOWN_ROOM_VERSIONS from synapse.config.server import is_threepid_reserved +from synapse.events import EventBase from synapse.types import StateMap, UserID from synapse.util.caches import CACHE_SIZE_FACTOR, register_cache from synapse.util.caches.lrucache import LruCache @@ -92,20 +94,34 @@ class Auth(object): ) @defer.inlineCallbacks - def check_joined_room(self, room_id, user_id, current_state=None): - """Check if the user is currently joined in the room + def check_user_in_room( + self, + room_id: str, + user_id: str, + current_state: Optional[StateMap[EventBase]] = None, + allow_departed_users: bool = False, + ): + """Check if the user is in the room, or was at some point. Args: - room_id(str): The room to check. - user_id(str): The user to check. - current_state(dict): Optional map of the current state of the room. + room_id: The room to check. + + user_id: The user to check. + + current_state: Optional map of the current state of the room. If provided then that map is used to check whether they are a member of the room. Otherwise the current membership is loaded from the database. + + allow_departed_users: if True, accept users that were previously + members but have now departed. + Raises: - AuthError if the user is not in the room. + AuthError if the user is/was not in the room. Returns: - A deferred membership event for the user if the user is in - the room. + Deferred[Optional[EventBase]]: + Membership event for the user if the user was in the + room. This will be the join event if they are currently joined to + the room. This will be the leave event if they have left the room. """ if current_state: member = current_state.get((EventTypes.Member, user_id), None) @@ -113,37 +129,19 @@ class Auth(object): member = yield self.state.get_current_state( room_id=room_id, event_type=EventTypes.Member, state_key=user_id ) - - self._check_joined_room(member, user_id, room_id) - return member - - @defer.inlineCallbacks - def check_user_was_in_room(self, room_id, user_id): - """Check if the user was in the room at some point. - Args: - room_id(str): The room to check. - user_id(str): The user to check. - Raises: - AuthError if the user was never in the room. - Returns: - A deferred membership event for the user if the user was in the - room. This will be the join event if they are currently joined to - the room. This will be the leave event if they have left the room. - """ - member = yield self.state.get_current_state( - room_id=room_id, event_type=EventTypes.Member, state_key=user_id - ) membership = member.membership if member else None - if membership not in (Membership.JOIN, Membership.LEAVE): - raise AuthError(403, "User %s not in room %s" % (user_id, room_id)) + if membership == Membership.JOIN: + return member - if membership == Membership.LEAVE: + # XXX this looks totally bogus. Why do we not allow users who have been banned, + # or those who were members previously and have been re-invited? + if allow_departed_users and membership == Membership.LEAVE: forgot = yield self.store.did_forget(user_id, room_id) - if forgot: - raise AuthError(403, "User %s not in room %s" % (user_id, room_id)) + if not forgot: + return member - return member + raise AuthError(403, "User %s not in room %s" % (user_id, room_id)) @defer.inlineCallbacks def check_host_in_room(self, room_id, host): @@ -151,12 +149,6 @@ class Auth(object): latest_event_ids = yield self.store.is_host_joined(room_id, host) return latest_event_ids - def _check_joined_room(self, member, user_id, room_id): - if not member or member.membership != Membership.JOIN: - raise AuthError( - 403, "User %s not in room %s (%s)" % (user_id, room_id, repr(member)) - ) - def can_federate(self, event, auth_events): creation_event = auth_events.get((EventTypes.Create, "")) @@ -560,7 +552,7 @@ class Auth(object): return True user_id = user.to_string() - yield self.check_joined_room(room_id, user_id) + yield self.check_user_in_room(room_id, user_id) # We currently require the user is a "moderator" in the room. We do this # by checking if they would (theoretically) be able to change the @@ -645,12 +637,14 @@ class Auth(object): """ try: - # check_user_was_in_room will return the most recent membership + # check_user_in_room will return the most recent membership # event for the user if: # * The user is a non-guest user, and was ever in the room # * The user is a guest user, and has joined the room # else it will throw. - member_event = yield self.check_user_was_in_room(room_id, user_id) + member_event = yield self.check_user_in_room( + room_id, user_id, allow_departed_users=True + ) return member_event.membership, member_event.event_id except AuthError: visibility = yield self.state.get_current_state( diff --git a/synapse/handlers/initial_sync.py b/synapse/handlers/initial_sync.py index 2e6755f19c..b7c6a921d9 100644 --- a/synapse/handlers/initial_sync.py +++ b/synapse/handlers/initial_sync.py @@ -18,7 +18,7 @@ import logging from twisted.internet import defer from synapse.api.constants import EventTypes, Membership -from synapse.api.errors import AuthError, Codes, SynapseError +from synapse.api.errors import SynapseError from synapse.events.validator import EventValidator from synapse.handlers.presence import format_user_presence_state from synapse.logging.context import make_deferred_yieldable, run_in_background @@ -274,9 +274,10 @@ class InitialSyncHandler(BaseHandler): user_id = requester.user.to_string() - membership, member_event_id = await self._check_in_room_or_world_readable( - room_id, user_id - ) + ( + membership, + member_event_id, + ) = await self.auth.check_user_in_room_or_world_readable(room_id, user_id) is_peeking = member_event_id is None if membership == Membership.JOIN: @@ -433,25 +434,3 @@ class InitialSyncHandler(BaseHandler): ret["membership"] = membership return ret - - async def _check_in_room_or_world_readable(self, room_id, user_id): - try: - # check_user_was_in_room will return the most recent membership - # event for the user if: - # * The user is a non-guest user, and was ever in the room - # * The user is a guest user, and has joined the room - # else it will throw. - member_event = await self.auth.check_user_was_in_room(room_id, user_id) - return member_event.membership, member_event.event_id - except AuthError: - visibility = await self.state_handler.get_current_state( - room_id, EventTypes.RoomHistoryVisibility, "" - ) - if ( - visibility - and visibility.content["history_visibility"] == "world_readable" - ): - return Membership.JOIN, None - raise AuthError( - 403, "Guest access not allowed", errcode=Codes.GUEST_ACCESS_FORBIDDEN - ) diff --git a/synapse/handlers/typing.py b/synapse/handlers/typing.py index d5ca9cb07b..5406618431 100644 --- a/synapse/handlers/typing.py +++ b/synapse/handlers/typing.py @@ -125,7 +125,7 @@ class TypingHandler(object): if target_user_id != auth_user_id: raise AuthError(400, "Cannot set another user's typing state") - yield self.auth.check_joined_room(room_id, target_user_id) + yield self.auth.check_user_in_room(room_id, target_user_id) logger.debug("%s has started typing in %s", target_user_id, room_id) @@ -155,7 +155,7 @@ class TypingHandler(object): if target_user_id != auth_user_id: raise AuthError(400, "Cannot set another user's typing state") - yield self.auth.check_joined_room(room_id, target_user_id) + yield self.auth.check_user_in_room(room_id, target_user_id) logger.debug("%s has stopped typing in %s", target_user_id, room_id) diff --git a/tests/handlers/test_typing.py b/tests/handlers/test_typing.py index 2767b0497a..140cc0a3c2 100644 --- a/tests/handlers/test_typing.py +++ b/tests/handlers/test_typing.py @@ -122,11 +122,11 @@ class TypingNotificationsTestCase(unittest.HomeserverTestCase): self.room_members = [] - def check_joined_room(room_id, user_id): + def check_user_in_room(room_id, user_id): if user_id not in [u.to_string() for u in self.room_members]: raise AuthError(401, "User is not in the room") - hs.get_auth().check_joined_room = check_joined_room + hs.get_auth().check_user_in_room = check_user_in_room def get_joined_hosts_for_room(room_id): return set(member.domain for member in self.room_members) From a0a1fd0bec5cb596cc41c8f052a4aa0e8c01cf08 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 18 Feb 2020 23:14:57 +0000 Subject: [PATCH 1078/1623] Add `allow_departed_users` param to `check_in_room_or_world_readable` ... and set it everywhere it's called. while we're here, rename it for consistency with `check_user_in_room` (and to help check that I haven't missed any instances) --- synapse/api/auth.py | 16 +++++++++++++--- synapse/handlers/initial_sync.py | 4 +++- synapse/handlers/message.py | 12 ++++++++---- synapse/handlers/pagination.py | 4 +++- synapse/rest/client/v2_alpha/relations.py | 12 ++++++------ 5 files changed, 33 insertions(+), 15 deletions(-) diff --git a/synapse/api/auth.py b/synapse/api/auth.py index de7b75ca36..f576d65388 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -625,10 +625,18 @@ class Auth(object): return query_params[0].decode("ascii") @defer.inlineCallbacks - def check_in_room_or_world_readable(self, room_id, user_id): + def check_user_in_room_or_world_readable( + self, room_id: str, user_id: str, allow_departed_users: bool = False + ): """Checks that the user is or was in the room or the room is world readable. If it isn't then an exception is raised. + Args: + room_id: room to check + user_id: user to check + allow_departed_users: if True, accept users that were previously + members but have now departed + Returns: Deferred[tuple[str, str|None]]: Resolves to the current membership of the user in the room and the membership event ID of the user. If @@ -643,7 +651,7 @@ class Auth(object): # * The user is a guest user, and has joined the room # else it will throw. member_event = yield self.check_user_in_room( - room_id, user_id, allow_departed_users=True + room_id, user_id, allow_departed_users=allow_departed_users ) return member_event.membership, member_event.event_id except AuthError: @@ -656,7 +664,9 @@ class Auth(object): ): return Membership.JOIN, None raise AuthError( - 403, "Guest access not allowed", errcode=Codes.GUEST_ACCESS_FORBIDDEN + 403, + "User %s not in room %s, and room previews are disabled" + % (user_id, room_id), ) @defer.inlineCallbacks diff --git a/synapse/handlers/initial_sync.py b/synapse/handlers/initial_sync.py index b7c6a921d9..b116500c7d 100644 --- a/synapse/handlers/initial_sync.py +++ b/synapse/handlers/initial_sync.py @@ -277,7 +277,9 @@ class InitialSyncHandler(BaseHandler): ( membership, member_event_id, - ) = await self.auth.check_user_in_room_or_world_readable(room_id, user_id) + ) = await self.auth.check_user_in_room_or_world_readable( + room_id, user_id, allow_departed_users=True, + ) is_peeking = member_event_id is None if membership == Membership.JOIN: diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index be6ae18a92..d6be280952 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -99,7 +99,9 @@ class MessageHandler(object): ( membership, membership_event_id, - ) = yield self.auth.check_in_room_or_world_readable(room_id, user_id) + ) = yield self.auth.check_user_in_room_or_world_readable( + room_id, user_id, allow_departed_users=True + ) if membership == Membership.JOIN: data = yield self.state.get_current_state(room_id, event_type, state_key) @@ -177,7 +179,9 @@ class MessageHandler(object): ( membership, membership_event_id, - ) = yield self.auth.check_in_room_or_world_readable(room_id, user_id) + ) = yield self.auth.check_user_in_room_or_world_readable( + room_id, user_id, allow_departed_users=True + ) if membership == Membership.JOIN: state_ids = yield self.store.get_filtered_current_state_ids( @@ -216,8 +220,8 @@ class MessageHandler(object): if not requester.app_service: # We check AS auth after fetching the room membership, as it # requires us to pull out all joined members anyway. - membership, _ = yield self.auth.check_in_room_or_world_readable( - room_id, user_id + membership, _ = yield self.auth.check_user_in_room_or_world_readable( + room_id, user_id, allow_departed_users=True ) if membership != Membership.JOIN: raise NotImplementedError( diff --git a/synapse/handlers/pagination.py b/synapse/handlers/pagination.py index caf841a643..254a9f6856 100644 --- a/synapse/handlers/pagination.py +++ b/synapse/handlers/pagination.py @@ -335,7 +335,9 @@ class PaginationHandler(object): ( membership, member_event_id, - ) = await self.auth.check_in_room_or_world_readable(room_id, user_id) + ) = await self.auth.check_user_in_room_or_world_readable( + room_id, user_id, allow_departed_users=True + ) if source_config.direction == "b": # if we're going backwards, we might need to backfill. This diff --git a/synapse/rest/client/v2_alpha/relations.py b/synapse/rest/client/v2_alpha/relations.py index 9be9a34b91..63f07b63da 100644 --- a/synapse/rest/client/v2_alpha/relations.py +++ b/synapse/rest/client/v2_alpha/relations.py @@ -142,8 +142,8 @@ class RelationPaginationServlet(RestServlet): ): requester = await self.auth.get_user_by_req(request, allow_guest=True) - await self.auth.check_in_room_or_world_readable( - room_id, requester.user.to_string() + await self.auth.check_user_in_room_or_world_readable( + room_id, requester.user.to_string(), allow_departed_users=True ) # This gets the original event and checks that a) the event exists and @@ -235,8 +235,8 @@ class RelationAggregationPaginationServlet(RestServlet): ): requester = await self.auth.get_user_by_req(request, allow_guest=True) - await self.auth.check_in_room_or_world_readable( - room_id, requester.user.to_string() + await self.auth.check_user_in_room_or_world_readable( + room_id, requester.user.to_string(), allow_departed_users=True, ) # This checks that a) the event exists and b) the user is allowed to @@ -313,8 +313,8 @@ class RelationAggregationGroupPaginationServlet(RestServlet): async def on_GET(self, request, room_id, parent_id, relation_type, event_type, key): requester = await self.auth.get_user_by_req(request, allow_guest=True) - await self.auth.check_in_room_or_world_readable( - room_id, requester.user.to_string() + await self.auth.check_user_in_room_or_world_readable( + room_id, requester.user.to_string(), allow_departed_users=True, ) # This checks that a) the event exists and b) the user is allowed to From 709e81f5183d8ff67d86f4569234cb4a8be7a8d4 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 18 Feb 2020 23:15:54 +0000 Subject: [PATCH 1079/1623] Make room alias lists peekable As per https://github.com/matrix-org/matrix-doc/pull/2432#pullrequestreview-360566830, make room alias lists accessible to users outside world_readable rooms. --- synapse/handlers/directory.py | 4 +++- tests/rest/client/v1/test_rooms.py | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/synapse/handlers/directory.py b/synapse/handlers/directory.py index 3f8c792149..db2104c5f6 100644 --- a/synapse/handlers/directory.py +++ b/synapse/handlers/directory.py @@ -463,7 +463,9 @@ class DirectoryHandler(BaseHandler): # allow access to server admins and current members of the room is_admin = await self.auth.is_server_admin(requester.user) if not is_admin: - await self.auth.check_joined_room(room_id, requester.user.to_string()) + await self.auth.check_user_in_room_or_world_readable( + room_id, requester.user.to_string() + ) aliases = await self.store.get_aliases_for_room(room_id) return aliases diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py index fb08a45d27..8e389eb6c9 100644 --- a/tests/rest/client/v1/test_rooms.py +++ b/tests/rest/client/v1/test_rooms.py @@ -1766,6 +1766,23 @@ class DirectoryTestCase(unittest.HomeserverTestCase): res = self._get_aliases(self.room_owner_tok) self.assertEqual(set(res["aliases"]), {alias1, alias2}) + def test_peekable_room(self): + alias1 = self._random_alias() + self._set_alias_via_directory(alias1) + + self.helper.send_state( + self.room_id, + EventTypes.RoomHistoryVisibility, + body={"history_visibility": "world_readable"}, + tok=self.room_owner_tok, + ) + + self.register_user("user", "test") + user_tok = self.login("user", "test") + + res = self._get_aliases(user_tok) + self.assertEqual(res["aliases"], [alias1]) + def _get_aliases(self, access_token: str, expected_code: int = 200) -> JsonDict: """Calls the endpoint under test. returns the json response object.""" request, channel = self.make_request( From 603618c002eaf0b763f376e27477792b38ea00ef Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 18 Feb 2020 23:20:16 +0000 Subject: [PATCH 1080/1623] changelog --- changelog.d/6949.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6949.feature diff --git a/changelog.d/6949.feature b/changelog.d/6949.feature new file mode 100644 index 0000000000..40fe7fc9a9 --- /dev/null +++ b/changelog.d/6949.feature @@ -0,0 +1 @@ +Implement `GET /_matrix/client/r0/rooms/{roomId}/aliases` endpoint as per [MSC2432](https://github.com/matrix-org/matrix-doc/pull/2432). From 5a5abd55e8b47a7c1620c298a72817ccf73f90b0 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 19 Feb 2020 09:39:27 +0000 Subject: [PATCH 1081/1623] Limit size of get_auth_chain_ids query (#6947) --- changelog.d/6947.misc | 1 + .../data_stores/main/event_federation.py | 39 +++++++++++-------- 2 files changed, 23 insertions(+), 17 deletions(-) create mode 100644 changelog.d/6947.misc diff --git a/changelog.d/6947.misc b/changelog.d/6947.misc new file mode 100644 index 0000000000..6d00e58654 --- /dev/null +++ b/changelog.d/6947.misc @@ -0,0 +1 @@ +Increase perf of `get_auth_chain_ids` used in state res v2. diff --git a/synapse/storage/data_stores/main/event_federation.py b/synapse/storage/data_stores/main/event_federation.py index 1746f40adf..dcc375b840 100644 --- a/synapse/storage/data_stores/main/event_federation.py +++ b/synapse/storage/data_stores/main/event_federation.py @@ -62,32 +62,37 @@ class EventFederationWorkerStore(EventsWorkerStore, SignatureWorkerStore, SQLBas ) def _get_auth_chain_ids_txn(self, txn, event_ids, include_given): + if include_given: + results = set(event_ids) + else: + results = set() + if isinstance(self.database_engine, PostgresEngine): # For efficiency we make the database do this if we can. - sql = """ - WITH RECURSIVE auth_chain(event_id) AS ( - SELECT auth_id FROM event_auth WHERE event_id = ANY(?) - UNION - SELECT auth_id FROM event_auth - INNER JOIN auth_chain USING (event_id) - ) - SELECT event_id FROM auth_chain - """ - txn.execute(sql, (list(event_ids),)) - results = set(event_id for event_id, in txn) + # We need to be a little careful with querying large amounts at + # once, for some reason postgres really doesn't like it. We do this + # by only asking for auth chain of 500 events at a time. + event_ids = list(event_ids) + chunks = [event_ids[x : x + 500] for x in range(0, len(event_ids), 500)] + for chunk in chunks: + sql = """ + WITH RECURSIVE auth_chain(event_id) AS ( + SELECT auth_id FROM event_auth WHERE event_id = ANY(?) + UNION + SELECT auth_id FROM event_auth + INNER JOIN auth_chain USING (event_id) + ) + SELECT event_id FROM auth_chain + """ + txn.execute(sql, (chunk,)) - if include_given: - results.update(event_ids) + results.update(event_id for event_id, in txn) return list(results) # Database doesn't necessarily support recursive CTE, so we fall # back to do doing it manually. - if include_given: - results = set(event_ids) - else: - results = set() base_sql = "SELECT auth_id FROM event_auth WHERE " From fa64f836ec661a234cfb240afcb0a65bcae4cbf5 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 19 Feb 2020 09:54:13 +0000 Subject: [PATCH 1082/1623] Update changelog.d/6945.bugfix Co-Authored-By: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> --- changelog.d/6945.bugfix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/6945.bugfix b/changelog.d/6945.bugfix index 78470a0ef6..8561be16a4 100644 --- a/changelog.d/6945.bugfix +++ b/changelog.d/6945.bugfix @@ -1 +1 @@ -Fix bogus log in the purge jobs related to the message retention policies support. +Fix errors from logging in the purge jobs related to the message retention policies support. From 71d65407e7b09ff630c5388122608629fe733e5d Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 19 Feb 2020 10:03:19 +0000 Subject: [PATCH 1083/1623] Incorporate review --- docs/delegate.md | 17 +++++++++-------- docs/federate.md | 8 ++++---- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/docs/delegate.md b/docs/delegate.md index 57ae2c4c70..208ddb6277 100644 --- a/docs/delegate.md +++ b/docs/delegate.md @@ -1,13 +1,14 @@ # Delegation -Without configuring delegation, homeservers will expect the server -responsible for resources using e.g. `example.com` as their `server_name` -(e.g. `@user:example.com`) to be served at `example.com:8448`. +By default, other homeservers will expect to be able to reach yours via +your `server_name`, on port 8448. For example, if you set your `server_name` +to `example.com` (so that your user names look like `@user:example.com`), +other servers will try to connect to yours at `https://example.com:8448/`. Delegation is a Matrix feature allowing a homeserver admin to retain a -`server_name` of `example.com` so that your user IDs, room aliases, etc -continue to look like `*:example.com`, whilst having your federation -traffic routed to a different server and/or port (e.g. `synapse.example.com:443`). +`server_name` of `example.com` so that user IDs, room aliases, etc continue +to look like `*:example.com`, whilst having federation traffic routed +to a different server and/or port (e.g. `synapse.example.com:443`). ## .well-known delegation @@ -37,8 +38,8 @@ should return: Note, specifying a port is optional. If no port is specified, then it defaults to 8448. -With .well-known, federation servers will check for a valid TLS certificate -for the delegated hostname (in our example: `synapse.example.com`). +With .well-known delegation, federating servers will check for a valid TLS +certificate for the delegated hostname (in our example: `synapse.example.com`). ## SRV DNS record delegation diff --git a/docs/federate.md b/docs/federate.md index 5fc839b58b..a0786b9cf7 100644 --- a/docs/federate.md +++ b/docs/federate.md @@ -12,10 +12,10 @@ it is also the domain that other servers will use to try to reach your server (via port 8448). This is easy to set up and will work provided you set the `server_name` to match your machine's public DNS hostname. -You will also need a valid TLS certificate for this `server_name` served -on port 8448. The preferred way to do that is by using a reverse proxy, -see [reverse_proxy.md]() for instructions on how to -correctly set one up. +For this default configuration to work, you will need to listen for TLS +connections on port 8448. The preferred way to do that is by using a +reverse proxy: see [reverse_proxy.md]() for instructions +on how to correctly set one up. In some cases you might not want to run Synapse on the machine that has the `server_name` as its public DNS hostname, or you might want federation From 0d0bc35792aac0490e35cd3514b76d7aada7c8e0 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 19 Feb 2020 10:15:49 +0000 Subject: [PATCH 1084/1623] Increase DB/CPU perf of `_is_server_still_joined` check. (#6936) * Increase DB/CPU perf of `_is_server_still_joined` check. For rooms with large amount of state a single user leaving could cause us to go and load a lot of membership events and then pull out membership state in a large number of batches. * Newsfile * Update synapse/storage/persist_events.py Co-Authored-By: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * Fix adding if too soon * Update docstring * Review comments * Woops typo Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> --- changelog.d/6936.misc | 1 + .../storage/data_stores/main/roommember.py | 31 +++++++++++++ synapse/storage/persist_events.py | 43 ++++++++++++------- 3 files changed, 60 insertions(+), 15 deletions(-) create mode 100644 changelog.d/6936.misc diff --git a/changelog.d/6936.misc b/changelog.d/6936.misc new file mode 100644 index 0000000000..9400725017 --- /dev/null +++ b/changelog.d/6936.misc @@ -0,0 +1 @@ +Increase DB/CPU perf of `_is_server_still_joined` check. diff --git a/synapse/storage/data_stores/main/roommember.py b/synapse/storage/data_stores/main/roommember.py index 042289f0e0..d5ced05701 100644 --- a/synapse/storage/data_stores/main/roommember.py +++ b/synapse/storage/data_stores/main/roommember.py @@ -868,6 +868,37 @@ class RoomMemberWorkerStore(EventsWorkerStore): desc="get_membership_from_event_ids", ) + async def is_local_host_in_room_ignoring_users( + self, room_id: str, ignore_users: Collection[str] + ) -> bool: + """Check if there are any local users, excluding those in the given + list, in the room. + """ + + clause, args = make_in_list_sql_clause( + self.database_engine, "user_id", ignore_users + ) + + sql = """ + SELECT 1 FROM local_current_membership + WHERE + room_id = ? AND membership = ? + AND NOT (%s) + LIMIT 1 + """ % ( + clause, + ) + + def _is_local_host_in_room_ignoring_users_txn(txn): + txn.execute(sql, (room_id, Membership.JOIN, *args)) + + return bool(txn.fetchone()) + + return await self.db.runInteraction( + "is_local_host_in_room_ignoring_users", + _is_local_host_in_room_ignoring_users_txn, + ) + class RoomMemberBackgroundUpdateStore(SQLBaseStore): def __init__(self, database: Database, db_conn, hs): diff --git a/synapse/storage/persist_events.py b/synapse/storage/persist_events.py index a5370ed527..b950550f23 100644 --- a/synapse/storage/persist_events.py +++ b/synapse/storage/persist_events.py @@ -727,6 +727,7 @@ class EventsPersistenceStorage(object): # Check if any of the given events are a local join that appear in the # current state + events_to_check = [] # Event IDs that aren't an event we're persisting for (typ, state_key), event_id in delta.to_insert.items(): if typ != EventTypes.Member or not self.is_mine_id(state_key): continue @@ -736,8 +737,33 @@ class EventsPersistenceStorage(object): if event.membership == Membership.JOIN: return True - # There's been a change of membership but we don't have a local join - # event in the new events, so we need to check the full state. + # The event is not in `ev_ctx_rm`, so we need to pull it out of + # the DB. + events_to_check.append(event_id) + + # Check if any of the changes that we don't have events for are joins. + if events_to_check: + rows = await self.main_store.get_membership_from_event_ids(events_to_check) + is_still_joined = any(row["membership"] == Membership.JOIN for row in rows) + if is_still_joined: + return True + + # None of the new state events are local joins, so we check the database + # to see if there are any other local users in the room. We ignore users + # whose state has changed as we've already their new state above. + users_to_ignore = [ + state_key + for _, state_key in itertools.chain(delta.to_insert, delta.to_delete) + if self.is_mine_id(state_key) + ] + + if await self.main_store.is_local_host_in_room_ignoring_users( + room_id, users_to_ignore + ): + return True + + # The server will leave the room, so we go and find out which remote + # users will still be joined when we leave. if current_state is None: current_state = await self.main_store.get_current_state_ids(room_id) current_state = dict(current_state) @@ -746,19 +772,6 @@ class EventsPersistenceStorage(object): current_state.update(delta.to_insert) - event_ids = [ - event_id - for (typ, state_key,), event_id in current_state.items() - if typ == EventTypes.Member and self.is_mine_id(state_key) - ] - - rows = await self.main_store.get_membership_from_event_ids(event_ids) - is_still_joined = any(row["membership"] == Membership.JOIN for row in rows) - if is_still_joined: - return True - - # The server will leave the room, so we go and find out which remote - # users will still be joined when we leave. remote_event_ids = [ event_id for (typ, state_key,), event_id in current_state.items() From abf1e5c52669bd41ad803d4645809b6efdfcd61d Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 19 Feb 2020 10:38:20 +0000 Subject: [PATCH 1085/1623] Tiny optimisation for _get_handler_for_request (#6950) we have hundreds of path_regexes (see #5118), so let's not convert the same bytes to str for each of them. --- changelog.d/6950.misc | 1 + synapse/http/server.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 changelog.d/6950.misc diff --git a/changelog.d/6950.misc b/changelog.d/6950.misc new file mode 100644 index 0000000000..1c88936b8b --- /dev/null +++ b/changelog.d/6950.misc @@ -0,0 +1 @@ +Tiny optimisation for incoming HTTP request dispatch. diff --git a/synapse/http/server.py b/synapse/http/server.py index 04bc2385a2..042a605198 100644 --- a/synapse/http/server.py +++ b/synapse/http/server.py @@ -353,10 +353,12 @@ class JsonResource(HttpServer, resource.Resource): if request.method == b"OPTIONS": return _options_handler, "options_request_handler", {} + request_path = request.path.decode("ascii") + # Loop through all the registered callbacks to check if the method # and path regex match for path_entry in self.path_regexs.get(request.method, []): - m = path_entry.pattern.match(request.path.decode("ascii")) + m = path_entry.pattern.match(request_path) if m: # We found a match! return path_entry.callback, path_entry.servlet_classname, m.groupdict() From 880aaac1d82695b1a89f22f1f86c7f295ca205e0 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 19 Feb 2020 10:40:27 +0000 Subject: [PATCH 1086/1623] Move MSC2432 stuff onto unstable prefix (#6948) it's not in the spec yet, so needs to be unstable. Also add a feature flag for it. Also add a test for admin users. --- changelog.d/6948.feature | 1 + synapse/rest/client/v1/room.py | 8 +++++++- synapse/rest/client/versions.py | 2 ++ tests/rest/client/v1/test_rooms.py | 16 +++++++++++++--- 4 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 changelog.d/6948.feature diff --git a/changelog.d/6948.feature b/changelog.d/6948.feature new file mode 100644 index 0000000000..40fe7fc9a9 --- /dev/null +++ b/changelog.d/6948.feature @@ -0,0 +1 @@ +Implement `GET /_matrix/client/r0/rooms/{roomId}/aliases` endpoint as per [MSC2432](https://github.com/matrix-org/matrix-doc/pull/2432). diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index 143dc738c6..64f51406fb 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -16,6 +16,7 @@ """ This module contains REST servlets to do with rooms: /rooms/ """ import logging +import re from typing import List, Optional from six.moves.urllib import parse as urlparse @@ -848,7 +849,12 @@ class RoomTypingRestServlet(RestServlet): class RoomAliasListServlet(RestServlet): - PATTERNS = client_patterns("/rooms/(?P[^/]*)/aliases", unstable=False) + PATTERNS = [ + re.compile( + r"^/_matrix/client/unstable/org\.matrix\.msc2432" + r"/rooms/(?P[^/]*)/aliases" + ), + ] def __init__(self, hs: "synapse.server.HomeServer"): super().__init__() diff --git a/synapse/rest/client/versions.py b/synapse/rest/client/versions.py index 3eeb3607f4..d90a6a890b 100644 --- a/synapse/rest/client/versions.py +++ b/synapse/rest/client/versions.py @@ -72,6 +72,8 @@ class VersionsRestServlet(RestServlet): "org.matrix.label_based_filtering": True, # Implements support for cross signing as described in MSC1756 "org.matrix.e2e_cross_signing": True, + # Implements additional endpoints as described in MSC2432 + "org.matrix.msc2432": True, }, }, ) diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py index fb08a45d27..f82655677c 100644 --- a/tests/rest/client/v1/test_rooms.py +++ b/tests/rest/client/v1/test_rooms.py @@ -1729,8 +1729,7 @@ class ContextTestCase(unittest.HomeserverTestCase): self.assertEqual(events_after[1].get("content"), {}, events_after[1]) -class DirectoryTestCase(unittest.HomeserverTestCase): - +class RoomAliasListTestCase(unittest.HomeserverTestCase): servlets = [ synapse.rest.admin.register_servlets_for_client_rest_resource, directory.register_servlets, @@ -1756,6 +1755,16 @@ class DirectoryTestCase(unittest.HomeserverTestCase): res = self._get_aliases(user_tok, expected_code=403) self.assertEqual(res["errcode"], "M_FORBIDDEN") + def test_admin_user(self): + alias1 = self._random_alias() + self._set_alias_via_directory(alias1) + + self.register_user("user", "test", admin=True) + user_tok = self.login("user", "test") + + res = self._get_aliases(user_tok) + self.assertEqual(res["aliases"], [alias1]) + def test_with_aliases(self): alias1 = self._random_alias() alias2 = self._random_alias() @@ -1770,7 +1779,8 @@ class DirectoryTestCase(unittest.HomeserverTestCase): """Calls the endpoint under test. returns the json response object.""" request, channel = self.make_request( "GET", - "/_matrix/client/r0/rooms/%s/aliases" % (self.room_id,), + "/_matrix/client/unstable/org.matrix.msc2432/rooms/%s/aliases" + % (self.room_id,), access_token=access_token, ) self.render(request) From 099c96b89b54b58ecb9b6b6ed781f66f97dea112 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 19 Feb 2020 11:37:35 +0000 Subject: [PATCH 1087/1623] Revert `get_auth_chain_ids` changes (#6951) --- changelog.d/6951.misc | 1 + .../data_stores/main/event_federation.py | 28 ------------------- 2 files changed, 1 insertion(+), 28 deletions(-) create mode 100644 changelog.d/6951.misc diff --git a/changelog.d/6951.misc b/changelog.d/6951.misc new file mode 100644 index 0000000000..378f52f0a7 --- /dev/null +++ b/changelog.d/6951.misc @@ -0,0 +1 @@ +Revert #6937. diff --git a/synapse/storage/data_stores/main/event_federation.py b/synapse/storage/data_stores/main/event_federation.py index dcc375b840..60c67457b4 100644 --- a/synapse/storage/data_stores/main/event_federation.py +++ b/synapse/storage/data_stores/main/event_federation.py @@ -26,7 +26,6 @@ from synapse.storage._base import SQLBaseStore, make_in_list_sql_clause from synapse.storage.data_stores.main.events_worker import EventsWorkerStore from synapse.storage.data_stores.main.signatures import SignatureWorkerStore from synapse.storage.database import Database -from synapse.storage.engines import PostgresEngine from synapse.util.caches.descriptors import cached logger = logging.getLogger(__name__) @@ -67,33 +66,6 @@ class EventFederationWorkerStore(EventsWorkerStore, SignatureWorkerStore, SQLBas else: results = set() - if isinstance(self.database_engine, PostgresEngine): - # For efficiency we make the database do this if we can. - - # We need to be a little careful with querying large amounts at - # once, for some reason postgres really doesn't like it. We do this - # by only asking for auth chain of 500 events at a time. - event_ids = list(event_ids) - chunks = [event_ids[x : x + 500] for x in range(0, len(event_ids), 500)] - for chunk in chunks: - sql = """ - WITH RECURSIVE auth_chain(event_id) AS ( - SELECT auth_id FROM event_auth WHERE event_id = ANY(?) - UNION - SELECT auth_id FROM event_auth - INNER JOIN auth_chain USING (event_id) - ) - SELECT event_id FROM auth_chain - """ - txn.execute(sql, (chunk,)) - - results.update(event_id for event_id, in txn) - - return list(results) - - # Database doesn't necessarily support recursive CTE, so we fall - # back to do doing it manually. - base_sql = "SELECT auth_id FROM event_auth WHERE " front = set(event_ids) From 197b08de35a7cca7e45deec70312c36aa70a1662 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 19 Feb 2020 13:48:32 +0000 Subject: [PATCH 1088/1623] 1.11.0rc1 --- CHANGES.md | 73 ++++++++++++++++++++++++++++++++++++++++ changelog.d/6769.feature | 1 - changelog.d/6781.bugfix | 1 - changelog.d/6821.misc | 1 - changelog.d/6823.misc | 1 - changelog.d/6825.bugfix | 1 - changelog.d/6827.misc | 1 - changelog.d/6833.misc | 1 - changelog.d/6834.misc | 1 - changelog.d/6836.misc | 1 - changelog.d/6837.misc | 1 - changelog.d/6840.misc | 1 - changelog.d/6844.bugfix | 1 - changelog.d/6846.doc | 1 - changelog.d/6847.misc | 1 - changelog.d/6849.bugfix | 1 - changelog.d/6854.misc | 1 - changelog.d/6855.misc | 1 - changelog.d/6856.misc | 1 - changelog.d/6857.misc | 1 - changelog.d/6858.misc | 1 - changelog.d/6862.misc | 1 - changelog.d/6864.misc | 1 - changelog.d/6866.feature | 1 - changelog.d/6869.misc | 1 - changelog.d/6871.misc | 1 - changelog.d/6872.misc | 1 - changelog.d/6873.feature | 1 - changelog.d/6877.removal | 1 - changelog.d/6882.misc | 1 - changelog.d/6883.misc | 1 - changelog.d/6887.misc | 1 - changelog.d/6888.feature | 1 - changelog.d/6891.doc | 1 - changelog.d/6901.misc | 1 - changelog.d/6904.removal | 1 - changelog.d/6905.doc | 1 - changelog.d/6906.doc | 1 - changelog.d/6907.doc | 1 - changelog.d/6909.doc | 1 - changelog.d/6915.misc | 1 - changelog.d/6918.docker | 1 - changelog.d/6919.misc | 1 - changelog.d/6920.misc | 1 - changelog.d/6921.docker | 1 - changelog.d/6936.misc | 1 - changelog.d/6937.misc | 1 - changelog.d/6938.doc | 1 - changelog.d/6939.feature | 1 - changelog.d/6940.doc | 1 - changelog.d/6945.bugfix | 1 - changelog.d/6947.misc | 1 - changelog.d/6948.feature | 1 - changelog.d/6949.feature | 1 - changelog.d/6950.misc | 1 - changelog.d/6951.misc | 1 - synapse/__init__.py | 2 +- 57 files changed, 74 insertions(+), 56 deletions(-) delete mode 100644 changelog.d/6769.feature delete mode 100644 changelog.d/6781.bugfix delete mode 100644 changelog.d/6821.misc delete mode 100644 changelog.d/6823.misc delete mode 100644 changelog.d/6825.bugfix delete mode 100644 changelog.d/6827.misc delete mode 100644 changelog.d/6833.misc delete mode 100644 changelog.d/6834.misc delete mode 100644 changelog.d/6836.misc delete mode 100644 changelog.d/6837.misc delete mode 100644 changelog.d/6840.misc delete mode 100644 changelog.d/6844.bugfix delete mode 100644 changelog.d/6846.doc delete mode 100644 changelog.d/6847.misc delete mode 100644 changelog.d/6849.bugfix delete mode 100644 changelog.d/6854.misc delete mode 100644 changelog.d/6855.misc delete mode 100644 changelog.d/6856.misc delete mode 100644 changelog.d/6857.misc delete mode 100644 changelog.d/6858.misc delete mode 100644 changelog.d/6862.misc delete mode 100644 changelog.d/6864.misc delete mode 100644 changelog.d/6866.feature delete mode 100644 changelog.d/6869.misc delete mode 100644 changelog.d/6871.misc delete mode 100644 changelog.d/6872.misc delete mode 100644 changelog.d/6873.feature delete mode 100644 changelog.d/6877.removal delete mode 100644 changelog.d/6882.misc delete mode 100644 changelog.d/6883.misc delete mode 100644 changelog.d/6887.misc delete mode 100644 changelog.d/6888.feature delete mode 100644 changelog.d/6891.doc delete mode 100644 changelog.d/6901.misc delete mode 100644 changelog.d/6904.removal delete mode 100644 changelog.d/6905.doc delete mode 100644 changelog.d/6906.doc delete mode 100644 changelog.d/6907.doc delete mode 100644 changelog.d/6909.doc delete mode 100644 changelog.d/6915.misc delete mode 100644 changelog.d/6918.docker delete mode 100644 changelog.d/6919.misc delete mode 100644 changelog.d/6920.misc delete mode 100644 changelog.d/6921.docker delete mode 100644 changelog.d/6936.misc delete mode 100644 changelog.d/6937.misc delete mode 100644 changelog.d/6938.doc delete mode 100644 changelog.d/6939.feature delete mode 100644 changelog.d/6940.doc delete mode 100644 changelog.d/6945.bugfix delete mode 100644 changelog.d/6947.misc delete mode 100644 changelog.d/6948.feature delete mode 100644 changelog.d/6949.feature delete mode 100644 changelog.d/6950.misc delete mode 100644 changelog.d/6951.misc diff --git a/CHANGES.md b/CHANGES.md index 37b650a848..4032db792e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,76 @@ +Synapse 1.11.0rc1 (2020-02-19) +============================== + +Features +-------- + +- Admin API to add or modify threepids of user accounts. ([\#6769](https://github.com/matrix-org/synapse/issues/6769)) +- Limit the number of events that can be requested by the backfill federation API to 100. ([\#6864](https://github.com/matrix-org/synapse/issues/6864)) +- Add ability to run some group APIs on workers. ([\#6866](https://github.com/matrix-org/synapse/issues/6866)) +- Reject device display names over 100 characters in length. ([\#6882](https://github.com/matrix-org/synapse/issues/6882)) +- Add ability to route federation user device queries to workers. ([\#6873](https://github.com/matrix-org/synapse/issues/6873)) +- The result of a user directory search can now be filtered via the spam checker. ([\#6888](https://github.com/matrix-org/synapse/issues/6888)) +- Implement `GET /_matrix/client/r0/rooms/{roomId}/aliases` endpoint as per [MSC2432](https://github.com/matrix-org/matrix-doc/pull/2432). ([\#6939](https://github.com/matrix-org/synapse/issues/6939), [\#6948](https://github.com/matrix-org/synapse/issues/6948), [\#6949](https://github.com/matrix-org/synapse/issues/6949)) +- Stop sending `m.room.alias` events wheng adding / removing aliases. Check `alt_aliases` in the latest `m.room.canonical_alias` event when deleting an alias. ([\#6904](https://github.com/matrix-org/synapse/issues/6904)) + + +Bugfixes +-------- + +- Fixed third party event rules function `on_create_room`'s return value being ignored. ([\#6781](https://github.com/matrix-org/synapse/issues/6781)) +- Allow URL-encoded User IDs on `/_synapse/admin/v2/users/[/admin]` endpoints. Thanks to @NHAS for reporting. ([\#6825](https://github.com/matrix-org/synapse/issues/6825)) +- Fix Synapse refusing to start if `federation_certificate_verification_whitelist` option is blank. ([\#6849](https://github.com/matrix-org/synapse/issues/6849)) +- Fix errors from logging in the purge jobs related to the message retention policies support. ([\#6945](https://github.com/matrix-org/synapse/issues/6945)) +- Return a 404 instead of 200 for querying information of a non-existant user through the admin API. ([\#6901](https://github.com/matrix-org/synapse/issues/6901)) + + +Updates to the Docker image +--------------------------- + +- The deprecated "generate-config-on-the-fly" mode is no longer supported. ([\#6918](https://github.com/matrix-org/synapse/issues/6918)) + + +Improved Documentation +---------------------- + +- Add details of PR merge strategy to contributing docs. ([\#6846](https://github.com/matrix-org/synapse/issues/6846)) +- Spell out that the last event sent to a room won't be deleted by a purge. ([\#6891](https://github.com/matrix-org/synapse/issues/6891)) +- Update Synapse's documentation to warn about the deprecation of ACME v1. ([\#6905](https://github.com/matrix-org/synapse/issues/6905), [\#6907](https://github.com/matrix-org/synapse/issues/6907), [\#6909](https://github.com/matrix-org/synapse/issues/6909)) +- Add documentation for the spam checker. ([\#6906](https://github.com/matrix-org/synapse/issues/6906)) +- Fix worker docs to point `/publicised_groups` API correctly. ([\#6938](https://github.com/matrix-org/synapse/issues/6938)) +- Clean up and update docs on setting up federation. ([\#6940](https://github.com/matrix-org/synapse/issues/6940)) +- Add a warning about indentation to generated configuration files. ([\#6920](https://github.com/matrix-org/synapse/issues/6920)) +- Databases created using the compose file in contrib/docker will now always have correct encoding and locale settings. Contributed by Fridtjof Mund. ([\#6921](https://github.com/matrix-org/synapse/issues/6921)) + + +Deprecations and Removals +------------------------- + +- Remove `m.lazy_load_members` from `unstable_features` since lazy loading is in the stable Client-Server API version r0.5.0. ([\#6877](https://github.com/matrix-org/synapse/issues/6877)) + + +Internal Changes +---------------- + +- Add type hints to `SyncHandler`. ([\#6821](https://github.com/matrix-org/synapse/issues/6821)) +- Refactoring work in preparation for changing the event redaction algorithm. ([\#6823](https://github.com/matrix-org/synapse/issues/6823), [\#6827](https://github.com/matrix-org/synapse/issues/6827), [\#6854](https://github.com/matrix-org/synapse/issues/6854), [\#6856](https://github.com/matrix-org/synapse/issues/6856), [\#6857](https://github.com/matrix-org/synapse/issues/6857), [\#6858](https://github.com/matrix-org/synapse/issues/6858)) +- Change the default power levels of invites, tombstones and server ACLs for new rooms. ([\#6834](https://github.com/matrix-org/synapse/issues/6834)) +- Fix stacktraces when using `ObservableDeferred` and async/await. ([\#6836](https://github.com/matrix-org/synapse/issues/6836)) +- Port much of `synapse.handlers.federation` to async/await. ([\#6837](https://github.com/matrix-org/synapse/issues/6837), [\#6840](https://github.com/matrix-org/synapse/issues/6840)) +- Populate `rooms.room_version` database column at startup, rather than in a background update. ([\#6847](https://github.com/matrix-org/synapse/issues/6847)) +- Update pip install directions in readme to avoid error when using zsh. ([\#6855](https://github.com/matrix-org/synapse/issues/6855)) +- Reduce amount we log at `INFO` level. ([\#6833](https://github.com/matrix-org/synapse/issues/6833), [\#6862](https://github.com/matrix-org/synapse/issues/6862)) +- Remove unused `get_room_stats_state` method. ([\#6869](https://github.com/matrix-org/synapse/issues/6869)) +- Add typing to `synapse.federation.sender` and port to async/await. ([\#6871](https://github.com/matrix-org/synapse/issues/6871)) +- Refactor _EventInternalMetadata object to improve type safety. ([\#6872](https://github.com/matrix-org/synapse/issues/6872)) +- Add an additional entry to the SyTest blacklist for worker mode. ([\#6883](https://github.com/matrix-org/synapse/issues/6883)) +- Fix the use of sed in the linting scripts when using BSD sed. ([\#6887](https://github.com/matrix-org/synapse/issues/6887)) +- Add type hints to the spam checker module. ([\#6915](https://github.com/matrix-org/synapse/issues/6915)) +- Convert the directory handler tests to use HomeserverTestCase. ([\#6919](https://github.com/matrix-org/synapse/issues/6919)) +- Increase DB/CPU perf of `_is_server_still_joined` check. ([\#6936](https://github.com/matrix-org/synapse/issues/6936)) +- Tiny optimisation for incoming HTTP request dispatch. ([\#6950](https://github.com/matrix-org/synapse/issues/6950)) + + Synapse 1.10.1 (2020-02-17) =========================== diff --git a/changelog.d/6769.feature b/changelog.d/6769.feature deleted file mode 100644 index 8a60e12907..0000000000 --- a/changelog.d/6769.feature +++ /dev/null @@ -1 +0,0 @@ -Admin API to add or modify threepids of user accounts. \ No newline at end of file diff --git a/changelog.d/6781.bugfix b/changelog.d/6781.bugfix deleted file mode 100644 index 47cd671bff..0000000000 --- a/changelog.d/6781.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fixed third party event rules function `on_create_room`'s return value being ignored. diff --git a/changelog.d/6821.misc b/changelog.d/6821.misc deleted file mode 100644 index 1d5265d5e2..0000000000 --- a/changelog.d/6821.misc +++ /dev/null @@ -1 +0,0 @@ -Add type hints to `SyncHandler`. diff --git a/changelog.d/6823.misc b/changelog.d/6823.misc deleted file mode 100644 index 08aa80bcd9..0000000000 --- a/changelog.d/6823.misc +++ /dev/null @@ -1 +0,0 @@ -Refactoring work in preparation for changing the event redaction algorithm. diff --git a/changelog.d/6825.bugfix b/changelog.d/6825.bugfix deleted file mode 100644 index d3cacd6d9a..0000000000 --- a/changelog.d/6825.bugfix +++ /dev/null @@ -1 +0,0 @@ -Allow URL-encoded User IDs on `/_synapse/admin/v2/users/[/admin]` endpoints. Thanks to @NHAS for reporting. \ No newline at end of file diff --git a/changelog.d/6827.misc b/changelog.d/6827.misc deleted file mode 100644 index 08aa80bcd9..0000000000 --- a/changelog.d/6827.misc +++ /dev/null @@ -1 +0,0 @@ -Refactoring work in preparation for changing the event redaction algorithm. diff --git a/changelog.d/6833.misc b/changelog.d/6833.misc deleted file mode 100644 index 8a0605f90b..0000000000 --- a/changelog.d/6833.misc +++ /dev/null @@ -1 +0,0 @@ -Reducing log level to DEBUG for synapse.storage.TIME. diff --git a/changelog.d/6834.misc b/changelog.d/6834.misc deleted file mode 100644 index 79acebe516..0000000000 --- a/changelog.d/6834.misc +++ /dev/null @@ -1 +0,0 @@ -Change the default power levels of invites, tombstones and server ACLs for new rooms. \ No newline at end of file diff --git a/changelog.d/6836.misc b/changelog.d/6836.misc deleted file mode 100644 index 232488e1e5..0000000000 --- a/changelog.d/6836.misc +++ /dev/null @@ -1 +0,0 @@ -Fix stacktraces when using `ObservableDeferred` and async/await. diff --git a/changelog.d/6837.misc b/changelog.d/6837.misc deleted file mode 100644 index 0496f12de8..0000000000 --- a/changelog.d/6837.misc +++ /dev/null @@ -1 +0,0 @@ -Port much of `synapse.handlers.federation` to async/await. diff --git a/changelog.d/6840.misc b/changelog.d/6840.misc deleted file mode 100644 index 0496f12de8..0000000000 --- a/changelog.d/6840.misc +++ /dev/null @@ -1 +0,0 @@ -Port much of `synapse.handlers.federation` to async/await. diff --git a/changelog.d/6844.bugfix b/changelog.d/6844.bugfix deleted file mode 100644 index e84aa1029f..0000000000 --- a/changelog.d/6844.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix an issue with cross-signing where device signatures were not sent to remote servers. diff --git a/changelog.d/6846.doc b/changelog.d/6846.doc deleted file mode 100644 index ad69d608c0..0000000000 --- a/changelog.d/6846.doc +++ /dev/null @@ -1 +0,0 @@ -Add details of PR merge strategy to contributing docs. \ No newline at end of file diff --git a/changelog.d/6847.misc b/changelog.d/6847.misc deleted file mode 100644 index 094e911adb..0000000000 --- a/changelog.d/6847.misc +++ /dev/null @@ -1 +0,0 @@ -Populate `rooms.room_version` database column at startup, rather than in a background update. diff --git a/changelog.d/6849.bugfix b/changelog.d/6849.bugfix deleted file mode 100644 index d928a26ec6..0000000000 --- a/changelog.d/6849.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix Synapse refusing to start if `federation_certificate_verification_whitelist` option is blank. diff --git a/changelog.d/6854.misc b/changelog.d/6854.misc deleted file mode 100644 index 08aa80bcd9..0000000000 --- a/changelog.d/6854.misc +++ /dev/null @@ -1 +0,0 @@ -Refactoring work in preparation for changing the event redaction algorithm. diff --git a/changelog.d/6855.misc b/changelog.d/6855.misc deleted file mode 100644 index 904361ddfb..0000000000 --- a/changelog.d/6855.misc +++ /dev/null @@ -1 +0,0 @@ -Update pip install directiosn in readme to avoid error when using zsh. diff --git a/changelog.d/6856.misc b/changelog.d/6856.misc deleted file mode 100644 index 08aa80bcd9..0000000000 --- a/changelog.d/6856.misc +++ /dev/null @@ -1 +0,0 @@ -Refactoring work in preparation for changing the event redaction algorithm. diff --git a/changelog.d/6857.misc b/changelog.d/6857.misc deleted file mode 100644 index 08aa80bcd9..0000000000 --- a/changelog.d/6857.misc +++ /dev/null @@ -1 +0,0 @@ -Refactoring work in preparation for changing the event redaction algorithm. diff --git a/changelog.d/6858.misc b/changelog.d/6858.misc deleted file mode 100644 index 08aa80bcd9..0000000000 --- a/changelog.d/6858.misc +++ /dev/null @@ -1 +0,0 @@ -Refactoring work in preparation for changing the event redaction algorithm. diff --git a/changelog.d/6862.misc b/changelog.d/6862.misc deleted file mode 100644 index 83626d2939..0000000000 --- a/changelog.d/6862.misc +++ /dev/null @@ -1 +0,0 @@ -Reduce amount we log at `INFO` level. diff --git a/changelog.d/6864.misc b/changelog.d/6864.misc deleted file mode 100644 index d24eb68460..0000000000 --- a/changelog.d/6864.misc +++ /dev/null @@ -1 +0,0 @@ -Limit the number of events that can be requested by the backfill federation API to 100. diff --git a/changelog.d/6866.feature b/changelog.d/6866.feature deleted file mode 100644 index 256feab6ff..0000000000 --- a/changelog.d/6866.feature +++ /dev/null @@ -1 +0,0 @@ -Add ability to run some group APIs on workers. diff --git a/changelog.d/6869.misc b/changelog.d/6869.misc deleted file mode 100644 index 14f88f9bb7..0000000000 --- a/changelog.d/6869.misc +++ /dev/null @@ -1 +0,0 @@ -Remove unused `get_room_stats_state` method. diff --git a/changelog.d/6871.misc b/changelog.d/6871.misc deleted file mode 100644 index 5161af9983..0000000000 --- a/changelog.d/6871.misc +++ /dev/null @@ -1 +0,0 @@ -Add typing to `synapse.federation.sender` and port to async/await. diff --git a/changelog.d/6872.misc b/changelog.d/6872.misc deleted file mode 100644 index 215a0c82c3..0000000000 --- a/changelog.d/6872.misc +++ /dev/null @@ -1 +0,0 @@ -Refactor _EventInternalMetadata object to improve type safety. diff --git a/changelog.d/6873.feature b/changelog.d/6873.feature deleted file mode 100644 index bbedf8f7f0..0000000000 --- a/changelog.d/6873.feature +++ /dev/null @@ -1 +0,0 @@ -Add ability to route federation user device queries to workers. diff --git a/changelog.d/6877.removal b/changelog.d/6877.removal deleted file mode 100644 index 9545e31fbe..0000000000 --- a/changelog.d/6877.removal +++ /dev/null @@ -1 +0,0 @@ -Remove `m.lazy_load_members` from `unstable_features` since lazy loading is in the stable Client-Server API version r0.5.0. diff --git a/changelog.d/6882.misc b/changelog.d/6882.misc deleted file mode 100644 index e8382e36ae..0000000000 --- a/changelog.d/6882.misc +++ /dev/null @@ -1 +0,0 @@ -Reject device display names over 100 characters in length. diff --git a/changelog.d/6883.misc b/changelog.d/6883.misc deleted file mode 100644 index e0837d7987..0000000000 --- a/changelog.d/6883.misc +++ /dev/null @@ -1 +0,0 @@ -Add an additional entry to the SyTest blacklist for worker mode. diff --git a/changelog.d/6887.misc b/changelog.d/6887.misc deleted file mode 100644 index b351d47c7b..0000000000 --- a/changelog.d/6887.misc +++ /dev/null @@ -1 +0,0 @@ -Fix the use of sed in the linting scripts when using BSD sed. diff --git a/changelog.d/6888.feature b/changelog.d/6888.feature deleted file mode 100644 index 1b7ac0c823..0000000000 --- a/changelog.d/6888.feature +++ /dev/null @@ -1 +0,0 @@ -The result of a user directory search can now be filtered via the spam checker. diff --git a/changelog.d/6891.doc b/changelog.d/6891.doc deleted file mode 100644 index 2f46c385b7..0000000000 --- a/changelog.d/6891.doc +++ /dev/null @@ -1 +0,0 @@ -Spell out that the last event sent to a room won't be deleted by a purge. diff --git a/changelog.d/6901.misc b/changelog.d/6901.misc deleted file mode 100644 index b2f12bbe86..0000000000 --- a/changelog.d/6901.misc +++ /dev/null @@ -1 +0,0 @@ -Return a 404 instead of 200 for querying information of a non-existant user through the admin API. \ No newline at end of file diff --git a/changelog.d/6904.removal b/changelog.d/6904.removal deleted file mode 100644 index a5cc0c3605..0000000000 --- a/changelog.d/6904.removal +++ /dev/null @@ -1 +0,0 @@ -Stop sending alias events during adding / removing aliases. Check alt_aliases in the latest canonical aliases event when deleting an alias. diff --git a/changelog.d/6905.doc b/changelog.d/6905.doc deleted file mode 100644 index be0e698af8..0000000000 --- a/changelog.d/6905.doc +++ /dev/null @@ -1 +0,0 @@ -Update Synapse's documentation to warn about the deprecation of ACME v1. diff --git a/changelog.d/6906.doc b/changelog.d/6906.doc deleted file mode 100644 index 053b2436ae..0000000000 --- a/changelog.d/6906.doc +++ /dev/null @@ -1 +0,0 @@ -Add documentation for the spam checker. diff --git a/changelog.d/6907.doc b/changelog.d/6907.doc deleted file mode 100644 index be0e698af8..0000000000 --- a/changelog.d/6907.doc +++ /dev/null @@ -1 +0,0 @@ -Update Synapse's documentation to warn about the deprecation of ACME v1. diff --git a/changelog.d/6909.doc b/changelog.d/6909.doc deleted file mode 100644 index be0e698af8..0000000000 --- a/changelog.d/6909.doc +++ /dev/null @@ -1 +0,0 @@ -Update Synapse's documentation to warn about the deprecation of ACME v1. diff --git a/changelog.d/6915.misc b/changelog.d/6915.misc deleted file mode 100644 index 3a181ef243..0000000000 --- a/changelog.d/6915.misc +++ /dev/null @@ -1 +0,0 @@ -Add type hints to the spam checker module. diff --git a/changelog.d/6918.docker b/changelog.d/6918.docker deleted file mode 100644 index cc2db5e071..0000000000 --- a/changelog.d/6918.docker +++ /dev/null @@ -1 +0,0 @@ -The deprecated "generate-config-on-the-fly" mode is no longer supported. diff --git a/changelog.d/6919.misc b/changelog.d/6919.misc deleted file mode 100644 index aa2cd89998..0000000000 --- a/changelog.d/6919.misc +++ /dev/null @@ -1 +0,0 @@ -Convert the directory handler tests to use HomeserverTestCase. diff --git a/changelog.d/6920.misc b/changelog.d/6920.misc deleted file mode 100644 index d333add990..0000000000 --- a/changelog.d/6920.misc +++ /dev/null @@ -1 +0,0 @@ -Add a warning about indentation to generated configuration files. diff --git a/changelog.d/6921.docker b/changelog.d/6921.docker deleted file mode 100644 index 152e723339..0000000000 --- a/changelog.d/6921.docker +++ /dev/null @@ -1 +0,0 @@ -Databases created using the compose file in contrib/docker will now always have correct encoding and locale settings. Contributed by Fridtjof Mund. diff --git a/changelog.d/6936.misc b/changelog.d/6936.misc deleted file mode 100644 index 9400725017..0000000000 --- a/changelog.d/6936.misc +++ /dev/null @@ -1 +0,0 @@ -Increase DB/CPU perf of `_is_server_still_joined` check. diff --git a/changelog.d/6937.misc b/changelog.d/6937.misc deleted file mode 100644 index 6d00e58654..0000000000 --- a/changelog.d/6937.misc +++ /dev/null @@ -1 +0,0 @@ -Increase perf of `get_auth_chain_ids` used in state res v2. diff --git a/changelog.d/6938.doc b/changelog.d/6938.doc deleted file mode 100644 index 117f76f48a..0000000000 --- a/changelog.d/6938.doc +++ /dev/null @@ -1 +0,0 @@ -Fix worker docs to point `/publicised_groups` API correctly. diff --git a/changelog.d/6939.feature b/changelog.d/6939.feature deleted file mode 100644 index 40fe7fc9a9..0000000000 --- a/changelog.d/6939.feature +++ /dev/null @@ -1 +0,0 @@ -Implement `GET /_matrix/client/r0/rooms/{roomId}/aliases` endpoint as per [MSC2432](https://github.com/matrix-org/matrix-doc/pull/2432). diff --git a/changelog.d/6940.doc b/changelog.d/6940.doc deleted file mode 100644 index 8c75f48d3d..0000000000 --- a/changelog.d/6940.doc +++ /dev/null @@ -1 +0,0 @@ -Clean up and update docs on setting up federation. diff --git a/changelog.d/6945.bugfix b/changelog.d/6945.bugfix deleted file mode 100644 index 8561be16a4..0000000000 --- a/changelog.d/6945.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix errors from logging in the purge jobs related to the message retention policies support. diff --git a/changelog.d/6947.misc b/changelog.d/6947.misc deleted file mode 100644 index 6d00e58654..0000000000 --- a/changelog.d/6947.misc +++ /dev/null @@ -1 +0,0 @@ -Increase perf of `get_auth_chain_ids` used in state res v2. diff --git a/changelog.d/6948.feature b/changelog.d/6948.feature deleted file mode 100644 index 40fe7fc9a9..0000000000 --- a/changelog.d/6948.feature +++ /dev/null @@ -1 +0,0 @@ -Implement `GET /_matrix/client/r0/rooms/{roomId}/aliases` endpoint as per [MSC2432](https://github.com/matrix-org/matrix-doc/pull/2432). diff --git a/changelog.d/6949.feature b/changelog.d/6949.feature deleted file mode 100644 index 40fe7fc9a9..0000000000 --- a/changelog.d/6949.feature +++ /dev/null @@ -1 +0,0 @@ -Implement `GET /_matrix/client/r0/rooms/{roomId}/aliases` endpoint as per [MSC2432](https://github.com/matrix-org/matrix-doc/pull/2432). diff --git a/changelog.d/6950.misc b/changelog.d/6950.misc deleted file mode 100644 index 1c88936b8b..0000000000 --- a/changelog.d/6950.misc +++ /dev/null @@ -1 +0,0 @@ -Tiny optimisation for incoming HTTP request dispatch. diff --git a/changelog.d/6951.misc b/changelog.d/6951.misc deleted file mode 100644 index 378f52f0a7..0000000000 --- a/changelog.d/6951.misc +++ /dev/null @@ -1 +0,0 @@ -Revert #6937. diff --git a/synapse/__init__.py b/synapse/__init__.py index 8313f177d2..076a297b87 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -36,7 +36,7 @@ try: except ImportError: pass -__version__ = "1.10.1" +__version__ = "1.11.0rc1" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 0001e8397e58906f317361f1548be61f41064962 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 19 Feb 2020 13:54:05 +0000 Subject: [PATCH 1089/1623] update changes.md --- CHANGES.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 4032db792e..fabf909fa3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,12 +7,12 @@ Features - Admin API to add or modify threepids of user accounts. ([\#6769](https://github.com/matrix-org/synapse/issues/6769)) - Limit the number of events that can be requested by the backfill federation API to 100. ([\#6864](https://github.com/matrix-org/synapse/issues/6864)) - Add ability to run some group APIs on workers. ([\#6866](https://github.com/matrix-org/synapse/issues/6866)) -- Reject device display names over 100 characters in length. ([\#6882](https://github.com/matrix-org/synapse/issues/6882)) +- Reject device display names over 100 characters in length to prevent abuse. ([\#6882](https://github.com/matrix-org/synapse/issues/6882)) - Add ability to route federation user device queries to workers. ([\#6873](https://github.com/matrix-org/synapse/issues/6873)) - The result of a user directory search can now be filtered via the spam checker. ([\#6888](https://github.com/matrix-org/synapse/issues/6888)) -- Implement `GET /_matrix/client/r0/rooms/{roomId}/aliases` endpoint as per [MSC2432](https://github.com/matrix-org/matrix-doc/pull/2432). ([\#6939](https://github.com/matrix-org/synapse/issues/6939), [\#6948](https://github.com/matrix-org/synapse/issues/6948), [\#6949](https://github.com/matrix-org/synapse/issues/6949)) +- Implement new `GET /_matrix/client/unstable/org.matrix.msc2432/rooms/{roomId}/aliases` endpoint as per [MSC2432](https://github.com/matrix-org/matrix-doc/pull/2432). ([\#6939](https://github.com/matrix-org/synapse/issues/6939), [\#6948](https://github.com/matrix-org/synapse/issues/6948), [\#6949](https://github.com/matrix-org/synapse/issues/6949)) - Stop sending `m.room.alias` events wheng adding / removing aliases. Check `alt_aliases` in the latest `m.room.canonical_alias` event when deleting an alias. ([\#6904](https://github.com/matrix-org/synapse/issues/6904)) - +- Change the default power levels of invites, tombstones and server ACLs for new rooms. ([\#6834](https://github.com/matrix-org/synapse/issues/6834)) Bugfixes -------- @@ -41,6 +41,7 @@ Improved Documentation - Clean up and update docs on setting up federation. ([\#6940](https://github.com/matrix-org/synapse/issues/6940)) - Add a warning about indentation to generated configuration files. ([\#6920](https://github.com/matrix-org/synapse/issues/6920)) - Databases created using the compose file in contrib/docker will now always have correct encoding and locale settings. Contributed by Fridtjof Mund. ([\#6921](https://github.com/matrix-org/synapse/issues/6921)) +- Update pip install directions in readme to avoid error when using zsh. ([\#6855](https://github.com/matrix-org/synapse/issues/6855)) Deprecations and Removals @@ -54,15 +55,13 @@ Internal Changes - Add type hints to `SyncHandler`. ([\#6821](https://github.com/matrix-org/synapse/issues/6821)) - Refactoring work in preparation for changing the event redaction algorithm. ([\#6823](https://github.com/matrix-org/synapse/issues/6823), [\#6827](https://github.com/matrix-org/synapse/issues/6827), [\#6854](https://github.com/matrix-org/synapse/issues/6854), [\#6856](https://github.com/matrix-org/synapse/issues/6856), [\#6857](https://github.com/matrix-org/synapse/issues/6857), [\#6858](https://github.com/matrix-org/synapse/issues/6858)) -- Change the default power levels of invites, tombstones and server ACLs for new rooms. ([\#6834](https://github.com/matrix-org/synapse/issues/6834)) - Fix stacktraces when using `ObservableDeferred` and async/await. ([\#6836](https://github.com/matrix-org/synapse/issues/6836)) - Port much of `synapse.handlers.federation` to async/await. ([\#6837](https://github.com/matrix-org/synapse/issues/6837), [\#6840](https://github.com/matrix-org/synapse/issues/6840)) - Populate `rooms.room_version` database column at startup, rather than in a background update. ([\#6847](https://github.com/matrix-org/synapse/issues/6847)) -- Update pip install directions in readme to avoid error when using zsh. ([\#6855](https://github.com/matrix-org/synapse/issues/6855)) - Reduce amount we log at `INFO` level. ([\#6833](https://github.com/matrix-org/synapse/issues/6833), [\#6862](https://github.com/matrix-org/synapse/issues/6862)) - Remove unused `get_room_stats_state` method. ([\#6869](https://github.com/matrix-org/synapse/issues/6869)) - Add typing to `synapse.federation.sender` and port to async/await. ([\#6871](https://github.com/matrix-org/synapse/issues/6871)) -- Refactor _EventInternalMetadata object to improve type safety. ([\#6872](https://github.com/matrix-org/synapse/issues/6872)) +- Refactor `_EventInternalMetadata` object to improve type safety. ([\#6872](https://github.com/matrix-org/synapse/issues/6872)) - Add an additional entry to the SyTest blacklist for worker mode. ([\#6883](https://github.com/matrix-org/synapse/issues/6883)) - Fix the use of sed in the linting scripts when using BSD sed. ([\#6887](https://github.com/matrix-org/synapse/issues/6887)) - Add type hints to the spam checker module. ([\#6915](https://github.com/matrix-org/synapse/issues/6915)) From 2b37eabca1e9355e2e2ab8f65bbdda12431ecc28 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 19 Feb 2020 15:04:47 +0000 Subject: [PATCH 1090/1623] Reduce auth chains fetched during v2 state res. (#6952) The state res v2 algorithm only cares about the difference between auth chains, so we can pass in the known common state to the `get_auth_chain` storage function so that it can ignore those events. --- changelog.d/6952.misc | 1 + synapse/state/__init__.py | 15 ++++++---- synapse/state/v2.py | 2 +- .../data_stores/main/event_federation.py | 28 +++++++++++++++---- tests/state/test_v2.py | 6 ++-- 5 files changed, 39 insertions(+), 13 deletions(-) create mode 100644 changelog.d/6952.misc diff --git a/changelog.d/6952.misc b/changelog.d/6952.misc new file mode 100644 index 0000000000..e26dc5cab8 --- /dev/null +++ b/changelog.d/6952.misc @@ -0,0 +1 @@ +Improve perf of v2 state res for large rooms. diff --git a/synapse/state/__init__.py b/synapse/state/__init__.py index fdd6bef6b4..df7a4f6a89 100644 --- a/synapse/state/__init__.py +++ b/synapse/state/__init__.py @@ -16,7 +16,7 @@ import logging from collections import namedtuple -from typing import Dict, Iterable, List, Optional +from typing import Dict, Iterable, List, Optional, Set from six import iteritems, itervalues @@ -662,7 +662,7 @@ class StateResolutionStore(object): allow_rejected=allow_rejected, ) - def get_auth_chain(self, event_ids): + def get_auth_chain(self, event_ids: List[str], ignore_events: Set[str]): """Gets the full auth chain for a set of events (including rejected events). @@ -674,11 +674,16 @@ class StateResolutionStore(object): presence of rejected events Args: - event_ids (list): The event IDs of the events to fetch the auth - chain for. Must be state events. + event_ids: The event IDs of the events to fetch the auth chain for. + Must be state events. + ignore_events: Set of events to exclude from the returned auth + chain. + Returns: Deferred[list[str]]: List of event IDs of the auth chain. """ - return self.store.get_auth_chain_ids(event_ids, include_given=True) + return self.store.get_auth_chain_ids( + event_ids, include_given=True, ignore_events=ignore_events, + ) diff --git a/synapse/state/v2.py b/synapse/state/v2.py index 531018c6a5..75fe58305a 100644 --- a/synapse/state/v2.py +++ b/synapse/state/v2.py @@ -248,7 +248,7 @@ def _get_auth_chain_difference(state_sets, event_map, state_res_store): and eid not in common ) - auth_chain = yield state_res_store.get_auth_chain(auth_ids) + auth_chain = yield state_res_store.get_auth_chain(auth_ids, common) auth_ids.update(auth_chain) auth_sets.append(auth_ids) diff --git a/synapse/storage/data_stores/main/event_federation.py b/synapse/storage/data_stores/main/event_federation.py index 60c67457b4..e16da2577d 100644 --- a/synapse/storage/data_stores/main/event_federation.py +++ b/synapse/storage/data_stores/main/event_federation.py @@ -14,6 +14,7 @@ # limitations under the License. import itertools import logging +from typing import List, Optional, Set from six.moves import range from six.moves.queue import Empty, PriorityQueue @@ -46,21 +47,37 @@ class EventFederationWorkerStore(EventsWorkerStore, SignatureWorkerStore, SQLBas event_ids, include_given=include_given ).addCallback(self.get_events_as_list) - def get_auth_chain_ids(self, event_ids, include_given=False): + def get_auth_chain_ids( + self, + event_ids: List[str], + include_given: bool = False, + ignore_events: Optional[Set[str]] = None, + ): """Get auth events for given event_ids. The events *must* be state events. Args: - event_ids (list): state events - include_given (bool): include the given events in result + event_ids: state events + include_given: include the given events in result + ignore_events: Set of events to exclude from the returned auth + chain. This is useful if the caller will just discard the + given events anyway, and saves us from figuring out their auth + chains if not required. Returns: list of event_ids """ return self.db.runInteraction( - "get_auth_chain_ids", self._get_auth_chain_ids_txn, event_ids, include_given + "get_auth_chain_ids", + self._get_auth_chain_ids_txn, + event_ids, + include_given, + ignore_events, ) - def _get_auth_chain_ids_txn(self, txn, event_ids, include_given): + def _get_auth_chain_ids_txn(self, txn, event_ids, include_given, ignore_events): + if ignore_events is None: + ignore_events = set() + if include_given: results = set(event_ids) else: @@ -80,6 +97,7 @@ class EventFederationWorkerStore(EventsWorkerStore, SignatureWorkerStore, SQLBas txn.execute(base_sql + clause, list(args)) new_front.update([r[0] for r in txn]) + new_front -= ignore_events new_front -= results front = new_front diff --git a/tests/state/test_v2.py b/tests/state/test_v2.py index 5bafad9f19..5059ade850 100644 --- a/tests/state/test_v2.py +++ b/tests/state/test_v2.py @@ -603,7 +603,7 @@ class TestStateResolutionStore(object): return {eid: self.event_map[eid] for eid in event_ids if eid in self.event_map} - def get_auth_chain(self, event_ids): + def get_auth_chain(self, event_ids, ignore_events): """Gets the full auth chain for a set of events (including rejected events). @@ -617,6 +617,8 @@ class TestStateResolutionStore(object): Args: event_ids (list): The event IDs of the events to fetch the auth chain for. Must be state events. + ignore_events: Set of events to exclude from the returned auth + chain. Returns: Deferred[list[str]]: List of event IDs of the auth chain. @@ -627,7 +629,7 @@ class TestStateResolutionStore(object): stack = list(event_ids) while stack: event_id = stack.pop() - if event_id in result: + if event_id in result or event_id in ignore_events: continue result.add(event_id) From fc87d2ffb39ca17065e19bd42ef25f1c84862d2c Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 19 Feb 2020 15:09:00 +0000 Subject: [PATCH 1091/1623] Freeze allocated objects on startup. (#6953) This may make gc go a bit faster as the gc will know things like caches/data stores etc. are frozen without having to check. --- changelog.d/6953.misc | 1 + synapse/app/_base.py | 9 +++++++++ 2 files changed, 10 insertions(+) create mode 100644 changelog.d/6953.misc diff --git a/changelog.d/6953.misc b/changelog.d/6953.misc new file mode 100644 index 0000000000..0ab52041cf --- /dev/null +++ b/changelog.d/6953.misc @@ -0,0 +1 @@ +Reduce time spent doing GC by freezing objects on startup. diff --git a/synapse/app/_base.py b/synapse/app/_base.py index 0e8b467a3e..109b1e2fb5 100644 --- a/synapse/app/_base.py +++ b/synapse/app/_base.py @@ -279,6 +279,15 @@ def start(hs, listeners=None): setup_sentry(hs) setup_sdnotify(hs) + + # We now freeze all allocated objects in the hopes that (almost) + # everything currently allocated are things that will be used for the + # rest of time. Doing so means less work each GC (hopefully). + # + # This only works on Python 3.7 + if sys.version_info >= (3, 7): + gc.collect() + gc.freeze() except Exception: traceback.print_exc(file=sys.stderr) reactor = hs.get_reactor() From 7b7c3cedf2fdc0d0c05bbc651e0ff5b59921c3a2 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 19 Feb 2020 15:47:11 +0000 Subject: [PATCH 1092/1623] Minor perf fixes to `get_auth_chain_ids`. --- changelog.d/6954.misc | 1 + synapse/storage/data_stores/main/event_federation.py | 10 ++++------ synapse/storage/database.py | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) create mode 100644 changelog.d/6954.misc diff --git a/changelog.d/6954.misc b/changelog.d/6954.misc new file mode 100644 index 0000000000..8b84ce2f19 --- /dev/null +++ b/changelog.d/6954.misc @@ -0,0 +1 @@ +Minor perf fixes to `get_auth_chain_ids`. diff --git a/synapse/storage/data_stores/main/event_federation.py b/synapse/storage/data_stores/main/event_federation.py index e16da2577d..750ec1b70d 100644 --- a/synapse/storage/data_stores/main/event_federation.py +++ b/synapse/storage/data_stores/main/event_federation.py @@ -16,7 +16,6 @@ import itertools import logging from typing import List, Optional, Set -from six.moves import range from six.moves.queue import Empty, PriorityQueue from twisted.internet import defer @@ -28,6 +27,7 @@ from synapse.storage.data_stores.main.events_worker import EventsWorkerStore from synapse.storage.data_stores.main.signatures import SignatureWorkerStore from synapse.storage.database import Database from synapse.util.caches.descriptors import cached +from synapse.util.iterutils import batch_iter logger = logging.getLogger(__name__) @@ -88,14 +88,12 @@ class EventFederationWorkerStore(EventsWorkerStore, SignatureWorkerStore, SQLBas front = set(event_ids) while front: new_front = set() - front_list = list(front) - chunks = [front_list[x : x + 100] for x in range(0, len(front), 100)] - for chunk in chunks: + for chunk in batch_iter(front, 100): clause, args = make_in_list_sql_clause( txn.database_engine, "event_id", chunk ) - txn.execute(base_sql + clause, list(args)) - new_front.update([r[0] for r in txn]) + txn.execute(base_sql + clause, args) + new_front.update(r[0] for r in txn) new_front -= ignore_events new_front -= results diff --git a/synapse/storage/database.py b/synapse/storage/database.py index 3eeb2f7c04..6dcb5c04da 100644 --- a/synapse/storage/database.py +++ b/synapse/storage/database.py @@ -1504,7 +1504,7 @@ class Database(object): def make_in_list_sql_clause( database_engine, column: str, iterable: Iterable -) -> Tuple[str, Iterable]: +) -> Tuple[str, list]: """Returns an SQL clause that checks the given column is in the iterable. On SQLite this expands to `column IN (?, ?, ...)`, whereas on Postgres From 4fb5f4d0ce0444d1d3c2f0b9576b5a91b6307372 Mon Sep 17 00:00:00 2001 From: Ruben Barkow-Kuder Date: Thu, 20 Feb 2020 11:37:57 +0100 Subject: [PATCH 1093/1623] Add some clarifications to README.md in the database schema directory. (#6615) Signed-off-by: Ruben Barkow-Kuder --- changelog.d/6615.misc | 1 + .../main/schema/full_schemas/README.md | 24 ++++++++++++------- 2 files changed, 17 insertions(+), 8 deletions(-) create mode 100644 changelog.d/6615.misc diff --git a/changelog.d/6615.misc b/changelog.d/6615.misc new file mode 100644 index 0000000000..9f93152565 --- /dev/null +++ b/changelog.d/6615.misc @@ -0,0 +1 @@ +Add some clarifications to `README.md` in the database schema directory. diff --git a/synapse/storage/data_stores/main/schema/full_schemas/README.md b/synapse/storage/data_stores/main/schema/full_schemas/README.md index bbd3f18604..c00f287190 100644 --- a/synapse/storage/data_stores/main/schema/full_schemas/README.md +++ b/synapse/storage/data_stores/main/schema/full_schemas/README.md @@ -1,13 +1,21 @@ -# Building full schema dumps +# Synapse Database Schemas -These schemas need to be made from a database that has had all background updates run. +These schemas are used as a basis to create brand new Synapse databases, on both +SQLite3 and Postgres. -To do so, use `scripts-dev/make_full_schema.sh`. This will produce -`full.sql.postgres ` and `full.sql.sqlite` files. +## Building full schema dumps + +If you want to recreate these schemas, they need to be made from a database that +has had all background updates run. + +To do so, use `scripts-dev/make_full_schema.sh`. This will produce new +`full.sql.postgres ` and `full.sql.sqlite` files. Ensure postgres is installed and your user has the ability to run bash commands -such as `createdb`. +such as `createdb`, then call -``` -./scripts-dev/make_full_schema.sh -p postgres_username -o output_dir/ -``` + ./scripts-dev/make_full_schema.sh -p postgres_username -o output_dir/ + +There are currently two folders with full-schema snapshots. `16` is a snapshot +from 2015, for historical reference. The other contains the most recent full +schema snapshot. From a90d0dc5c2650eea298f8d554ca74c2cf4c097eb Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 20 Feb 2020 09:59:00 -0500 Subject: [PATCH 1094/1623] don't insert into the device table for remote cross-signing keys (#6956) --- changelog.d/6956.misc | 1 + .../data_stores/main/end_to_end_keys.py | 33 ++++++++++--------- 2 files changed, 19 insertions(+), 15 deletions(-) create mode 100644 changelog.d/6956.misc diff --git a/changelog.d/6956.misc b/changelog.d/6956.misc new file mode 100644 index 0000000000..5cb0894182 --- /dev/null +++ b/changelog.d/6956.misc @@ -0,0 +1 @@ +Don't record remote cross-signing keys in the `devices` table. diff --git a/synapse/storage/data_stores/main/end_to_end_keys.py b/synapse/storage/data_stores/main/end_to_end_keys.py index e551606f9d..001a53f9b4 100644 --- a/synapse/storage/data_stores/main/end_to_end_keys.py +++ b/synapse/storage/data_stores/main/end_to_end_keys.py @@ -680,11 +680,6 @@ class EndToEndKeyStore(EndToEndKeyWorkerStore, SQLBaseStore): 'user_signing' for a user-signing key key (dict): the key data """ - # the cross-signing keys need to occupy the same namespace as devices, - # since signatures are identified by device ID. So add an entry to the - # device table to make sure that we don't have a collision with device - # IDs - # the 'key' dict will look something like: # { # "user_id": "@alice:example.com", @@ -701,16 +696,24 @@ class EndToEndKeyStore(EndToEndKeyWorkerStore, SQLBaseStore): # The "keys" property must only have one entry, which will be the public # key, so we just grab the first value in there pubkey = next(iter(key["keys"].values())) - self.db.simple_insert_txn( - txn, - "devices", - values={ - "user_id": user_id, - "device_id": pubkey, - "display_name": key_type + " signing key", - "hidden": True, - }, - ) + + # The cross-signing keys need to occupy the same namespace as devices, + # since signatures are identified by device ID. So add an entry to the + # device table to make sure that we don't have a collision with device + # IDs. + # We only need to do this for local users, since remote servers should be + # responsible for checking this for their own users. + if self.hs.is_mine_id(user_id): + self.db.simple_insert_txn( + txn, + "devices", + values={ + "user_id": user_id, + "device_id": pubkey, + "display_name": key_type + " signing key", + "hidden": True, + }, + ) # and finally, store the key itself with self._cross_signing_id_gen.get_next() as stream_id: From 99eed85a77acdc25b68f4a7b6447a5ffaecebb0d Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Thu, 20 Feb 2020 16:24:04 -0500 Subject: [PATCH 1095/1623] Do not send alias events when creating / upgrading a room (#6941) Stop emitting room alias update events during room creation/upgrade. --- changelog.d/6941.removal | 1 + synapse/handlers/directory.py | 19 ++---------------- synapse/handlers/room.py | 36 +++++++++++++---------------------- 3 files changed, 16 insertions(+), 40 deletions(-) create mode 100644 changelog.d/6941.removal diff --git a/changelog.d/6941.removal b/changelog.d/6941.removal new file mode 100644 index 0000000000..8573be84b3 --- /dev/null +++ b/changelog.d/6941.removal @@ -0,0 +1 @@ +Stop sending m.room.aliases events during room creation and upgrade. diff --git a/synapse/handlers/directory.py b/synapse/handlers/directory.py index db2104c5f6..921d887b24 100644 --- a/synapse/handlers/directory.py +++ b/synapse/handlers/directory.py @@ -14,6 +14,7 @@ # limitations under the License. +import collections import logging import string from typing import List @@ -282,22 +283,6 @@ class DirectoryHandler(BaseHandler): Codes.NOT_FOUND, ) - @defer.inlineCallbacks - def send_room_alias_update_event(self, requester, room_id): - aliases = yield self.store.get_aliases_for_room(room_id) - - yield self.event_creation_handler.create_and_send_nonmember_event( - requester, - { - "type": EventTypes.Aliases, - "state_key": self.hs.hostname, - "room_id": room_id, - "sender": requester.user.to_string(), - "content": {"aliases": aliases}, - }, - ratelimit=False, - ) - @defer.inlineCallbacks def _update_canonical_alias(self, requester, user_id, room_id, room_alias): """ @@ -326,7 +311,7 @@ class DirectoryHandler(BaseHandler): alt_aliases = content.pop("alt_aliases", None) # If the aliases are not a list (or not found) do not attempt to modify # the list. - if isinstance(alt_aliases, list): + if isinstance(alt_aliases, collections.Sequence): send_update = True alt_aliases = [alias for alias in alt_aliases if alias != alias_str] if alt_aliases: diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 49ec2f48bc..76e8f61b74 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -149,7 +149,9 @@ class RoomCreationHandler(BaseHandler): return ret @defer.inlineCallbacks - def _upgrade_room(self, requester, old_room_id, new_version): + def _upgrade_room( + self, requester: Requester, old_room_id: str, new_version: RoomVersion + ): user_id = requester.user.to_string() # start by allocating a new room id @@ -448,19 +450,21 @@ class RoomCreationHandler(BaseHandler): @defer.inlineCallbacks def _move_aliases_to_new_room( - self, requester, old_room_id, new_room_id, old_room_state + self, + requester: Requester, + old_room_id: str, + new_room_id: str, + old_room_state: StateMap[str], ): directory_handler = self.hs.get_handlers().directory_handler aliases = yield self.store.get_aliases_for_room(old_room_id) # check to see if we have a canonical alias. - canonical_alias = None + canonical_alias_event = None canonical_alias_event_id = old_room_state.get((EventTypes.CanonicalAlias, "")) if canonical_alias_event_id: canonical_alias_event = yield self.store.get_event(canonical_alias_event_id) - if canonical_alias_event: - canonical_alias = canonical_alias_event.content.get("alias", "") # first we try to remove the aliases from the old room (we suppress sending # the room_aliases event until the end). @@ -488,19 +492,6 @@ class RoomCreationHandler(BaseHandler): if not removed_aliases: return - try: - # this can fail if, for some reason, our user doesn't have perms to send - # m.room.aliases events in the old room (note that we've already checked that - # they have perms to send a tombstone event, so that's not terribly likely). - # - # If that happens, it's regrettable, but we should carry on: it's the same - # as when you remove an alias from the directory normally - it just means that - # the aliases event gets out of sync with the directory - # (cf https://github.com/vector-im/riot-web/issues/2369) - yield directory_handler.send_room_alias_update_event(requester, old_room_id) - except AuthError as e: - logger.warning("Failed to send updated alias event on old room: %s", e) - # we can now add any aliases we successfully removed to the new room. for alias in removed_aliases: try: @@ -517,8 +508,10 @@ class RoomCreationHandler(BaseHandler): # checking module decides it shouldn't, or similar. logger.error("Error adding alias %s to new room: %s", alias, e) + # If a canonical alias event existed for the old room, fire a canonical + # alias event for the new room with a copy of the information. try: - if canonical_alias and (canonical_alias in removed_aliases): + if canonical_alias_event: yield self.event_creation_handler.create_and_send_nonmember_event( requester, { @@ -526,12 +519,10 @@ class RoomCreationHandler(BaseHandler): "state_key": "", "room_id": new_room_id, "sender": requester.user.to_string(), - "content": {"alias": canonical_alias}, + "content": canonical_alias_event.content, }, ratelimit=False, ) - - yield directory_handler.send_room_alias_update_event(requester, new_room_id) except SynapseError as e: # again I'm not really expecting this to fail, but if it does, I'd rather # we returned the new room to the client at this point. @@ -757,7 +748,6 @@ class RoomCreationHandler(BaseHandler): if room_alias: result["room_alias"] = room_alias.to_string() - yield directory_handler.send_room_alias_update_event(requester, room_id) return result From 8f6d9c4cf0c36180ad26bb84cdbb55b503a942e2 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Fri, 21 Feb 2020 08:53:01 +0000 Subject: [PATCH 1096/1623] Small grammar fixes to the ACME v1 deprecation notice (#6944) Some small fixes to the copy in #6907. --- INSTALL.md | 9 ++++----- changelog.d/6944.doc | 1 + synapse/handlers/acme.py | 10 +++++----- 3 files changed, 10 insertions(+), 10 deletions(-) create mode 100644 changelog.d/6944.doc diff --git a/INSTALL.md b/INSTALL.md index 9fe767704b..aa5eb882bb 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -420,11 +420,10 @@ so, you will need to edit `homeserver.yaml`, as follows: Note that, as pointed out in that document, this feature will not work with installs set up after November 2020. - If you are using your - own certificate, be sure to use a `.pem` file that includes the full - certificate chain including any intermediate certificates (for - instance, if using certbot, use `fullchain.pem` as your certificate, - not `cert.pem`). + If you are using your own certificate, be sure to use a `.pem` file that + includes the full certificate chain including any intermediate certificates + (for instance, if using certbot, use `fullchain.pem` as your certificate, not + `cert.pem`). For a more detailed guide to configuring your server for federation, see [federate.md](docs/federate.md) diff --git a/changelog.d/6944.doc b/changelog.d/6944.doc new file mode 100644 index 0000000000..eb0c534b56 --- /dev/null +++ b/changelog.d/6944.doc @@ -0,0 +1 @@ +Small grammatical fixes to the ACME v1 deprecation notice. \ No newline at end of file diff --git a/synapse/handlers/acme.py b/synapse/handlers/acme.py index 250faa997b..a2d7959abe 100644 --- a/synapse/handlers/acme.py +++ b/synapse/handlers/acme.py @@ -27,11 +27,11 @@ logger = logging.getLogger(__name__) ACME_REGISTER_FAIL_ERROR = """ -------------------------------------------------------------------------------- -Failed to register with the ACME provider. This is likely happening because the install -is new, and ACME v1 has been deprecated by Let's Encrypt and is disabled for installs set -up after November 2019. -At the moment, Synapse doesn't support ACME v2. For more info and alternative solution, -check out https://github.com/matrix-org/synapse/blob/master/docs/ACME.md#deprecation-of-acme-v1 +Failed to register with the ACME provider. This is likely happening because the installation +is new, and ACME v1 has been deprecated by Let's Encrypt and disabled for +new installations since November 2019. +At the moment, Synapse doesn't support ACME v2. For more information and alternative +solutions, please read https://github.com/matrix-org/synapse/blob/master/docs/ACME.md#deprecation-of-acme-v1 --------------------------------------------------------------------------------""" From 9c1b83b0078aa9cc1bb902e14d3f7302625ba099 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 21 Feb 2020 08:56:04 +0000 Subject: [PATCH 1097/1623] 1.11.0 --- CHANGES.md | 9 +++++++++ changelog.d/6944.doc | 1 - debian/changelog | 6 ++++++ synapse/__init__.py | 2 +- 4 files changed, 16 insertions(+), 2 deletions(-) delete mode 100644 changelog.d/6944.doc diff --git a/CHANGES.md b/CHANGES.md index fabf909fa3..ff681762cd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,12 @@ +Synapse 1.11.0 (2020-02-21) +=========================== + +Improved Documentation +---------------------- + +- Small grammatical fixes to the ACME v1 deprecation notice. ([\#6944](https://github.com/matrix-org/synapse/issues/6944)) + + Synapse 1.11.0rc1 (2020-02-19) ============================== diff --git a/changelog.d/6944.doc b/changelog.d/6944.doc deleted file mode 100644 index eb0c534b56..0000000000 --- a/changelog.d/6944.doc +++ /dev/null @@ -1 +0,0 @@ -Small grammatical fixes to the ACME v1 deprecation notice. \ No newline at end of file diff --git a/debian/changelog b/debian/changelog index 90314d36af..fbb44cb94b 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.11.0) stable; urgency=medium + + * New synapse release 1.11.0. + + -- Synapse Packaging team Fri, 21 Feb 2020 08:54:34 +0000 + matrix-synapse-py3 (1.10.1) stable; urgency=medium * New synapse release 1.10.1. diff --git a/synapse/__init__.py b/synapse/__init__.py index 076a297b87..3406ce634f 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -36,7 +36,7 @@ try: except ImportError: pass -__version__ = "1.11.0rc1" +__version__ = "1.11.0" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 0bd8cf435e307a72c91667caff746fec7c233f16 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 14 Feb 2018 13:53:02 +0000 Subject: [PATCH 1098/1623] Increase MAX_EVENTS_BEHIND for replication clients --- synapse/replication/tcp/streams/_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/replication/tcp/streams/_base.py b/synapse/replication/tcp/streams/_base.py index a8d568b14a..208e8a667b 100644 --- a/synapse/replication/tcp/streams/_base.py +++ b/synapse/replication/tcp/streams/_base.py @@ -24,7 +24,7 @@ import attr logger = logging.getLogger(__name__) -MAX_EVENTS_BEHIND = 10000 +MAX_EVENTS_BEHIND = 500000 BackfillStreamRow = namedtuple( "BackfillStreamRow", From 1fcb9a1a7ab2cb4833ea6c823e8250199a0b3d95 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 21 Feb 2020 09:06:18 +0000 Subject: [PATCH 1099/1623] changelog --- changelog.d/6967.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6967.bugfix diff --git a/changelog.d/6967.bugfix b/changelog.d/6967.bugfix new file mode 100644 index 0000000000..b65f80cf1d --- /dev/null +++ b/changelog.d/6967.bugfix @@ -0,0 +1 @@ +Fix an issue affecting worker-based deployments where replication would stop working, necessitating a full restart, after joining a large room. From 509e381afa8c656e72f5fef3d651a9819794174a Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Fri, 21 Feb 2020 07:15:07 -0500 Subject: [PATCH 1100/1623] Clarify list/set/dict/tuple comprehensions and enforce via flake8 (#6957) Ensure good comprehension hygiene using flake8-comprehensions. --- CONTRIBUTING.md | 2 +- changelog.d/6957.misc | 1 + docs/code_style.md | 2 +- scripts-dev/convert_server_keys.py | 2 +- synapse/app/_base.py | 2 +- synapse/app/federation_sender.py | 4 +- synapse/app/pusher.py | 2 +- synapse/config/server.py | 4 +- synapse/config/tls.py | 2 +- synapse/crypto/keyring.py | 6 +-- synapse/federation/send_queue.py | 4 +- synapse/groups/groups_server.py | 2 +- synapse/handlers/device.py | 2 +- synapse/handlers/directory.py | 4 +- synapse/handlers/federation.py | 18 ++++----- synapse/handlers/presence.py | 6 +-- synapse/handlers/receipts.py | 2 +- synapse/handlers/room.py | 2 +- synapse/handlers/search.py | 8 ++-- synapse/handlers/sync.py | 22 +++++----- synapse/handlers/typing.py | 4 +- synapse/logging/utils.py | 2 +- synapse/metrics/__init__.py | 2 +- synapse/metrics/background_process_metrics.py | 4 +- synapse/push/bulk_push_rule_evaluator.py | 8 ++-- synapse/push/emailpusher.py | 2 +- synapse/push/mailer.py | 20 ++++------ synapse/push/pusherpool.py | 2 +- synapse/rest/admin/_base.py | 4 +- synapse/rest/client/v1/push_rule.py | 6 +-- synapse/rest/client/v1/pusher.py | 4 +- synapse/rest/client/v2_alpha/sync.py | 2 +- synapse/rest/key/v2/remote_key_resource.py | 2 +- synapse/rest/media/v1/_base.py | 40 +++++++++---------- synapse/state/v1.py | 10 ++--- synapse/state/v2.py | 8 ++-- synapse/storage/_base.py | 2 +- synapse/storage/background_updates.py | 2 +- .../storage/data_stores/main/appservice.py | 14 ++++--- .../storage/data_stores/main/client_ips.py | 4 +- synapse/storage/data_stores/main/devices.py | 13 +++--- .../data_stores/main/event_federation.py | 2 +- synapse/storage/data_stores/main/events.py | 8 ++-- .../data_stores/main/events_bg_updates.py | 2 +- .../storage/data_stores/main/events_worker.py | 6 +-- synapse/storage/data_stores/main/push_rule.py | 8 ++-- synapse/storage/data_stores/main/receipts.py | 4 +- .../storage/data_stores/main/roommember.py | 4 +- synapse/storage/data_stores/main/state.py | 8 ++-- synapse/storage/data_stores/main/stream.py | 8 ++-- .../data_stores/main/user_erasure_store.py | 4 +- synapse/storage/data_stores/state/store.py | 4 +- synapse/storage/database.py | 4 +- synapse/storage/persist_events.py | 8 ++-- synapse/storage/prepare_database.py | 6 +-- synapse/util/frozenutils.py | 2 +- synapse/visibility.py | 4 +- tests/config/test_generate.py | 2 +- tests/federation/test_federation_server.py | 2 +- tests/handlers/test_presence.py | 4 +- tests/handlers/test_typing.py | 6 +-- tests/handlers/test_user_directory.py | 12 +++--- tests/push/test_email.py | 6 +-- tests/push/test_http.py | 8 ++-- tests/rest/client/v2_alpha/test_sync.py | 28 +++++++------ tests/storage/test__base.py | 4 +- tests/storage/test_appservice.py | 36 ++++++++--------- tests/storage/test_cleanup_extrems.py | 10 ++--- tests/storage/test_event_metrics.py | 36 ++++++++--------- tests/storage/test_state.py | 2 +- tests/test_state.py | 18 +++------ tests/util/test_stream_change_cache.py | 18 +++------ tox.ini | 1 + 73 files changed, 251 insertions(+), 276 deletions(-) create mode 100644 changelog.d/6957.misc diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4b01b6ac8c..253a0ca648 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -60,7 +60,7 @@ python 3.6 and to install each tool: ``` # Install the dependencies -pip install -U black flake8 isort +pip install -U black flake8 flake8-comprehensions isort # Run the linter script ./scripts-dev/lint.sh diff --git a/changelog.d/6957.misc b/changelog.d/6957.misc new file mode 100644 index 0000000000..4f98030110 --- /dev/null +++ b/changelog.d/6957.misc @@ -0,0 +1 @@ +Use flake8-comprehensions to enforce good hygiene of list/set/dict comprehensions. diff --git a/docs/code_style.md b/docs/code_style.md index 71aecd41f7..6ef6f80290 100644 --- a/docs/code_style.md +++ b/docs/code_style.md @@ -30,7 +30,7 @@ The necessary tools are detailed below. Install `flake8` with: - pip install --upgrade flake8 + pip install --upgrade flake8 flake8-comprehensions Check all application and test code with: diff --git a/scripts-dev/convert_server_keys.py b/scripts-dev/convert_server_keys.py index 179be61c30..06b4c1e2ff 100644 --- a/scripts-dev/convert_server_keys.py +++ b/scripts-dev/convert_server_keys.py @@ -103,7 +103,7 @@ def main(): yaml.safe_dump(result, sys.stdout, default_flow_style=False) - rows = list(row for server, json in result.items() for row in rows_v2(server, json)) + rows = [row for server, json in result.items() for row in rows_v2(server, json)] cursor = connection.cursor() cursor.executemany( diff --git a/synapse/app/_base.py b/synapse/app/_base.py index 109b1e2fb5..9ffd23c6df 100644 --- a/synapse/app/_base.py +++ b/synapse/app/_base.py @@ -141,7 +141,7 @@ def start_reactor( def quit_with_error(error_string): message_lines = error_string.split("\n") - line_length = max([len(l) for l in message_lines if len(l) < 80]) + 2 + line_length = max(len(l) for l in message_lines if len(l) < 80) + 2 sys.stderr.write("*" * line_length + "\n") for line in message_lines: sys.stderr.write(" %s\n" % (line.rstrip(),)) diff --git a/synapse/app/federation_sender.py b/synapse/app/federation_sender.py index 63a91f1177..b7fcf80ddc 100644 --- a/synapse/app/federation_sender.py +++ b/synapse/app/federation_sender.py @@ -262,7 +262,7 @@ class FederationSenderHandler(object): # ... as well as device updates and messages elif stream_name == DeviceListsStream.NAME: - hosts = set(row.destination for row in rows) + hosts = {row.destination for row in rows} for host in hosts: self.federation_sender.send_device_messages(host) @@ -270,7 +270,7 @@ class FederationSenderHandler(object): # The to_device stream includes stuff to be pushed to both local # clients and remote servers, so we ignore entities that start with # '@' (since they'll be local users rather than destinations). - hosts = set(row.entity for row in rows if not row.entity.startswith("@")) + hosts = {row.entity for row in rows if not row.entity.startswith("@")} for host in hosts: self.federation_sender.send_device_messages(host) diff --git a/synapse/app/pusher.py b/synapse/app/pusher.py index e46b6ac598..84e9f8d5e2 100644 --- a/synapse/app/pusher.py +++ b/synapse/app/pusher.py @@ -158,7 +158,7 @@ class PusherReplicationHandler(ReplicationClientHandler): yield self.pusher_pool.on_new_notifications(token, token) elif stream_name == "receipts": yield self.pusher_pool.on_new_receipts( - token, token, set(row.room_id for row in rows) + token, token, {row.room_id for row in rows} ) except Exception: logger.exception("Error poking pushers") diff --git a/synapse/config/server.py b/synapse/config/server.py index 0ec1b0fadd..7525765fee 100644 --- a/synapse/config/server.py +++ b/synapse/config/server.py @@ -1066,12 +1066,12 @@ KNOWN_RESOURCES = ( def _check_resource_config(listeners): - resource_names = set( + resource_names = { res_name for listener in listeners for res in listener.get("resources", []) for res_name in res.get("names", []) - ) + } for resource in resource_names: if resource not in KNOWN_RESOURCES: diff --git a/synapse/config/tls.py b/synapse/config/tls.py index 97a12d51f6..a65538562b 100644 --- a/synapse/config/tls.py +++ b/synapse/config/tls.py @@ -260,7 +260,7 @@ class TlsConfig(Config): crypto.FILETYPE_ASN1, self.tls_certificate ) sha256_fingerprint = encode_base64(sha256(x509_certificate_bytes).digest()) - sha256_fingerprints = set(f["sha256"] for f in self.tls_fingerprints) + sha256_fingerprints = {f["sha256"] for f in self.tls_fingerprints} if sha256_fingerprint not in sha256_fingerprints: self.tls_fingerprints.append({"sha256": sha256_fingerprint}) diff --git a/synapse/crypto/keyring.py b/synapse/crypto/keyring.py index 6fe5a6a26a..983f0ead8c 100644 --- a/synapse/crypto/keyring.py +++ b/synapse/crypto/keyring.py @@ -326,9 +326,7 @@ class Keyring(object): verify_requests (list[VerifyJsonRequest]): list of verify requests """ - remaining_requests = set( - (rq for rq in verify_requests if not rq.key_ready.called) - ) + remaining_requests = {rq for rq in verify_requests if not rq.key_ready.called} @defer.inlineCallbacks def do_iterations(): @@ -396,7 +394,7 @@ class Keyring(object): results = yield fetcher.get_keys(missing_keys) - completed = list() + completed = [] for verify_request in remaining_requests: server_name = verify_request.server_name diff --git a/synapse/federation/send_queue.py b/synapse/federation/send_queue.py index 001bb304ae..876fb0e245 100644 --- a/synapse/federation/send_queue.py +++ b/synapse/federation/send_queue.py @@ -129,9 +129,9 @@ class FederationRemoteSendQueue(object): for key in keys[:i]: del self.presence_changed[key] - user_ids = set( + user_ids = { user_id for uids in self.presence_changed.values() for user_id in uids - ) + } keys = self.presence_destinations.keys() i = self.presence_destinations.bisect_left(position_to_delete) diff --git a/synapse/groups/groups_server.py b/synapse/groups/groups_server.py index c106abae21..4f0dc0a209 100644 --- a/synapse/groups/groups_server.py +++ b/synapse/groups/groups_server.py @@ -608,7 +608,7 @@ class GroupsServerHandler(GroupsServerWorkerHandler): user_results = yield self.store.get_users_in_group( group_id, include_private=True ) - if user_id in [user_result["user_id"] for user_result in user_results]: + if user_id in (user_result["user_id"] for user_result in user_results): raise SynapseError(400, "User already in group") content = { diff --git a/synapse/handlers/device.py b/synapse/handlers/device.py index 50cea3f378..a514c30714 100644 --- a/synapse/handlers/device.py +++ b/synapse/handlers/device.py @@ -742,6 +742,6 @@ class DeviceListUpdater(object): # We clobber the seen updates since we've re-synced from a given # point. - self._seen_updates[user_id] = set([stream_id]) + self._seen_updates[user_id] = {stream_id} defer.returnValue(result) diff --git a/synapse/handlers/directory.py b/synapse/handlers/directory.py index 921d887b24..0b23ca919a 100644 --- a/synapse/handlers/directory.py +++ b/synapse/handlers/directory.py @@ -72,7 +72,7 @@ class DirectoryHandler(BaseHandler): # TODO(erikj): Check if there is a current association. if not servers: users = yield self.state.get_current_users_in_room(room_id) - servers = set(get_domain_from_id(u) for u in users) + servers = {get_domain_from_id(u) for u in users} if not servers: raise SynapseError(400, "Failed to get server list") @@ -255,7 +255,7 @@ class DirectoryHandler(BaseHandler): ) users = yield self.state.get_current_users_in_room(room_id) - extra_servers = set(get_domain_from_id(u) for u in users) + extra_servers = {get_domain_from_id(u) for u in users} servers = set(extra_servers) | set(servers) # If this server is in the list of servers, return it first. diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index eb20ef4aec..a689065f89 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -659,11 +659,11 @@ class FederationHandler(BaseHandler): # this can happen if a remote server claims that the state or # auth_events at an event in room A are actually events in room B - bad_events = list( + bad_events = [ (event_id, event.room_id) for event_id, event in fetched_events.items() if event.room_id != room_id - ) + ] for bad_event_id, bad_room_id in bad_events: # This is a bogus situation, but since we may only discover it a long time @@ -856,7 +856,7 @@ class FederationHandler(BaseHandler): # Don't bother processing events we already have. seen_events = await self.store.have_events_in_timeline( - set(e.event_id for e in events) + {e.event_id for e in events} ) events = [e for e in events if e.event_id not in seen_events] @@ -866,7 +866,7 @@ class FederationHandler(BaseHandler): event_map = {e.event_id: e for e in events} - event_ids = set(e.event_id for e in events) + event_ids = {e.event_id for e in events} # build a list of events whose prev_events weren't in the batch. # (XXX: this will include events whose prev_events we already have; that doesn't @@ -892,13 +892,13 @@ class FederationHandler(BaseHandler): state_events.update({s.event_id: s for s in state}) events_to_state[e_id] = state - required_auth = set( + required_auth = { a_id for event in events + list(state_events.values()) + list(auth_events.values()) for a_id in event.auth_event_ids() - ) + } auth_events.update( {e_id: event_map[e_id] for e_id in required_auth if e_id in event_map} ) @@ -1247,7 +1247,7 @@ class FederationHandler(BaseHandler): async def on_event_auth(self, event_id: str) -> List[EventBase]: event = await self.store.get_event(event_id) auth = await self.store.get_auth_chain( - [auth_id for auth_id in event.auth_event_ids()], include_given=True + list(event.auth_event_ids()), include_given=True ) return list(auth) @@ -2152,7 +2152,7 @@ class FederationHandler(BaseHandler): # Now get the current auth_chain for the event. local_auth_chain = await self.store.get_auth_chain( - [auth_id for auth_id in event.auth_event_ids()], include_given=True + list(event.auth_event_ids()), include_given=True ) # TODO: Check if we would now reject event_id. If so we need to tell @@ -2654,7 +2654,7 @@ class FederationHandler(BaseHandler): member_handler = self.hs.get_room_member_handler() yield member_handler.send_membership_event(None, event, context) else: - destinations = set(x.split(":", 1)[-1] for x in (sender_user_id, room_id)) + destinations = {x.split(":", 1)[-1] for x in (sender_user_id, room_id)} yield self.federation_client.forward_third_party_invite( destinations, room_id, event_dict ) diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py index 202aa9294f..0d6cf2b008 100644 --- a/synapse/handlers/presence.py +++ b/synapse/handlers/presence.py @@ -313,7 +313,7 @@ class PresenceHandler(object): notified_presence_counter.inc(len(to_notify)) yield self._persist_and_notify(list(to_notify.values())) - self.unpersisted_users_changes |= set(s.user_id for s in new_states) + self.unpersisted_users_changes |= {s.user_id for s in new_states} self.unpersisted_users_changes -= set(to_notify.keys()) to_federation_ping = { @@ -698,7 +698,7 @@ class PresenceHandler(object): updates = yield self.current_state_for_users(target_user_ids) updates = list(updates.values()) - for user_id in set(target_user_ids) - set(u.user_id for u in updates): + for user_id in set(target_user_ids) - {u.user_id for u in updates}: updates.append(UserPresenceState.default(user_id)) now = self.clock.time_msec() @@ -886,7 +886,7 @@ class PresenceHandler(object): hosts = yield self.state.get_current_hosts_in_room(room_id) # Filter out ourselves. - hosts = set(host for host in hosts if host != self.server_name) + hosts = {host for host in hosts if host != self.server_name} self.federation.send_presence_to_destinations( states=[state], destinations=hosts diff --git a/synapse/handlers/receipts.py b/synapse/handlers/receipts.py index 9283c039e3..8bc100db42 100644 --- a/synapse/handlers/receipts.py +++ b/synapse/handlers/receipts.py @@ -94,7 +94,7 @@ class ReceiptsHandler(BaseHandler): # no new receipts return False - affected_room_ids = list(set([r.room_id for r in receipts])) + affected_room_ids = list({r.room_id for r in receipts}) self.notifier.on_new_event("receipt_key", max_batch_id, rooms=affected_room_ids) # Note that the min here shouldn't be relied upon to be accurate. diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 76e8f61b74..8ee870f0bb 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -355,7 +355,7 @@ class RoomCreationHandler(BaseHandler): # If so, mark the new room as non-federatable as well creation_content["m.federate"] = False - initial_state = dict() + initial_state = {} # Replicate relevant room events types_to_copy = ( diff --git a/synapse/handlers/search.py b/synapse/handlers/search.py index 110097eab9..ec1542d416 100644 --- a/synapse/handlers/search.py +++ b/synapse/handlers/search.py @@ -184,7 +184,7 @@ class SearchHandler(BaseHandler): membership_list=[Membership.JOIN], # membership_list=[Membership.JOIN, Membership.LEAVE, Membership.Ban], ) - room_ids = set(r.room_id for r in rooms) + room_ids = {r.room_id for r in rooms} # If doing a subset of all rooms seearch, check if any of the rooms # are from an upgraded room, and search their contents as well @@ -374,12 +374,12 @@ class SearchHandler(BaseHandler): ).to_string() if include_profile: - senders = set( + senders = { ev.sender for ev in itertools.chain( res["events_before"], [event], res["events_after"] ) - ) + } if res["events_after"]: last_event_id = res["events_after"][-1].event_id @@ -421,7 +421,7 @@ class SearchHandler(BaseHandler): state_results = {} if include_state: - rooms = set(e.room_id for e in allowed_events) + rooms = {e.room_id for e in allowed_events} for room_id in rooms: state = yield self.state_handler.get_current_state(room_id) state_results[room_id] = list(state.values()) diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index 4324bc702e..669dbc8a48 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -682,11 +682,9 @@ class SyncHandler(object): # FIXME: order by stream ordering rather than as returned by SQL if joined_user_ids or invited_user_ids: - summary["m.heroes"] = sorted( - [user_id for user_id in (joined_user_ids + invited_user_ids)] - )[0:5] + summary["m.heroes"] = sorted(joined_user_ids + invited_user_ids)[0:5] else: - summary["m.heroes"] = sorted([user_id for user_id in gone_user_ids])[0:5] + summary["m.heroes"] = sorted(gone_user_ids)[0:5] if not sync_config.filter_collection.lazy_load_members(): return summary @@ -697,9 +695,9 @@ class SyncHandler(object): # track which members the client should already know about via LL: # Ones which are already in state... - existing_members = set( + existing_members = { user_id for (typ, user_id) in state.keys() if typ == EventTypes.Member - ) + } # ...or ones which are in the timeline... for ev in batch.events: @@ -773,10 +771,10 @@ class SyncHandler(object): # We only request state for the members needed to display the # timeline: - members_to_fetch = set( + members_to_fetch = { event.sender # FIXME: we also care about invite targets etc. for event in batch.events - ) + } if full_state: # always make sure we LL ourselves so we know we're in the room @@ -1993,10 +1991,10 @@ def _calculate_state( ) } - c_ids = set(e for e in itervalues(current)) - ts_ids = set(e for e in itervalues(timeline_start)) - p_ids = set(e for e in itervalues(previous)) - tc_ids = set(e for e in itervalues(timeline_contains)) + c_ids = set(itervalues(current)) + ts_ids = set(itervalues(timeline_start)) + p_ids = set(itervalues(previous)) + tc_ids = set(itervalues(timeline_contains)) # If we are lazyloading room members, we explicitly add the membership events # for the senders in the timeline into the state block returned by /sync, diff --git a/synapse/handlers/typing.py b/synapse/handlers/typing.py index 5406618431..391bceb0c4 100644 --- a/synapse/handlers/typing.py +++ b/synapse/handlers/typing.py @@ -198,7 +198,7 @@ class TypingHandler(object): now=now, obj=member, then=now + FEDERATION_PING_INTERVAL ) - for domain in set(get_domain_from_id(u) for u in users): + for domain in {get_domain_from_id(u) for u in users}: if domain != self.server_name: logger.debug("sending typing update to %s", domain) self.federation.build_and_send_edu( @@ -231,7 +231,7 @@ class TypingHandler(object): return users = yield self.state.get_current_users_in_room(room_id) - domains = set(get_domain_from_id(u) for u in users) + domains = {get_domain_from_id(u) for u in users} if self.server_name in domains: logger.info("Got typing update from %s: %r", user_id, content) diff --git a/synapse/logging/utils.py b/synapse/logging/utils.py index 6073fc2725..0c2527bd86 100644 --- a/synapse/logging/utils.py +++ b/synapse/logging/utils.py @@ -148,7 +148,7 @@ def trace_function(f): pathname=pathname, lineno=lineno, msg=msg, - args=tuple(), + args=(), exc_info=None, ) diff --git a/synapse/metrics/__init__.py b/synapse/metrics/__init__.py index 0b45e1f52a..0dba997a23 100644 --- a/synapse/metrics/__init__.py +++ b/synapse/metrics/__init__.py @@ -240,7 +240,7 @@ class BucketCollector(object): res.append(["+Inf", sum(data.values())]) metric = HistogramMetricFamily( - self.name, "", buckets=res, sum_value=sum([x * y for x, y in data.items()]) + self.name, "", buckets=res, sum_value=sum(x * y for x, y in data.items()) ) yield metric diff --git a/synapse/metrics/background_process_metrics.py b/synapse/metrics/background_process_metrics.py index c53d2a0d40..b65bcd8806 100644 --- a/synapse/metrics/background_process_metrics.py +++ b/synapse/metrics/background_process_metrics.py @@ -80,13 +80,13 @@ _background_process_db_sched_duration = Counter( # map from description to a counter, so that we can name our logcontexts # incrementally. (It actually duplicates _background_process_start_count, but # it's much simpler to do so than to try to combine them.) -_background_process_counts = dict() # type: dict[str, int] +_background_process_counts = {} # type: dict[str, int] # map from description to the currently running background processes. # # it's kept as a dict of sets rather than a big set so that we can keep track # of process descriptions that no longer have any active processes. -_background_processes = dict() # type: dict[str, set[_BackgroundProcess]] +_background_processes = {} # type: dict[str, set[_BackgroundProcess]] # A lock that covers the above dicts _bg_metrics_lock = threading.Lock() diff --git a/synapse/push/bulk_push_rule_evaluator.py b/synapse/push/bulk_push_rule_evaluator.py index 7d9f5a38d9..433ca2f416 100644 --- a/synapse/push/bulk_push_rule_evaluator.py +++ b/synapse/push/bulk_push_rule_evaluator.py @@ -400,11 +400,11 @@ class RulesForRoom(object): if logger.isEnabledFor(logging.DEBUG): logger.debug("Found members %r: %r", self.room_id, members.values()) - interested_in_user_ids = set( + interested_in_user_ids = { user_id for user_id, membership in itervalues(members) if membership == Membership.JOIN - ) + } logger.debug("Joined: %r", interested_in_user_ids) @@ -412,9 +412,9 @@ class RulesForRoom(object): interested_in_user_ids, on_invalidate=self.invalidate_all_cb ) - user_ids = set( + user_ids = { uid for uid, have_pusher in iteritems(if_users_with_pushers) if have_pusher - ) + } logger.debug("With pushers: %r", user_ids) diff --git a/synapse/push/emailpusher.py b/synapse/push/emailpusher.py index 8c818a86bf..ba4551d619 100644 --- a/synapse/push/emailpusher.py +++ b/synapse/push/emailpusher.py @@ -204,7 +204,7 @@ class EmailPusher(object): yield self.send_notification(unprocessed, reason) yield self.save_last_stream_ordering_and_success( - max([ea["stream_ordering"] for ea in unprocessed]) + max(ea["stream_ordering"] for ea in unprocessed) ) # we update the throttle on all the possible unprocessed push actions diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index b13b646bfd..4ccaf178ce 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -526,12 +526,10 @@ class Mailer(object): # If the room doesn't have a name, say who the messages # are from explicitly to avoid, "messages in the Bob room" sender_ids = list( - set( - [ - notif_events[n["event_id"]].sender - for n in notifs_by_room[room_id] - ] - ) + { + notif_events[n["event_id"]].sender + for n in notifs_by_room[room_id] + } ) member_events = yield self.store.get_events( @@ -558,12 +556,10 @@ class Mailer(object): # If the reason room doesn't have a name, say who the messages # are from explicitly to avoid, "messages in the Bob room" sender_ids = list( - set( - [ - notif_events[n["event_id"]].sender - for n in notifs_by_room[reason["room_id"]] - ] - ) + { + notif_events[n["event_id"]].sender + for n in notifs_by_room[reason["room_id"]] + } ) member_events = yield self.store.get_events( diff --git a/synapse/push/pusherpool.py b/synapse/push/pusherpool.py index b9dca5bc63..01789a9fb4 100644 --- a/synapse/push/pusherpool.py +++ b/synapse/push/pusherpool.py @@ -191,7 +191,7 @@ class PusherPool: min_stream_id - 1, max_stream_id ) # This returns a tuple, user_id is at index 3 - users_affected = set([r[3] for r in updated_receipts]) + users_affected = {r[3] for r in updated_receipts} for u in users_affected: if u in self.pushers: diff --git a/synapse/rest/admin/_base.py b/synapse/rest/admin/_base.py index 459482eb6d..a96f75ce26 100644 --- a/synapse/rest/admin/_base.py +++ b/synapse/rest/admin/_base.py @@ -29,7 +29,7 @@ def historical_admin_path_patterns(path_regex): Note that this should only be used for existing endpoints: new ones should just register for the /_synapse/admin path. """ - return list( + return [ re.compile(prefix + path_regex) for prefix in ( "^/_synapse/admin/v1", @@ -37,7 +37,7 @@ def historical_admin_path_patterns(path_regex): "^/_matrix/client/unstable/admin", "^/_matrix/client/r0/admin", ) - ) + ] def admin_patterns(path_regex: str): diff --git a/synapse/rest/client/v1/push_rule.py b/synapse/rest/client/v1/push_rule.py index 4f74600239..9fd4908136 100644 --- a/synapse/rest/client/v1/push_rule.py +++ b/synapse/rest/client/v1/push_rule.py @@ -49,7 +49,7 @@ class PushRuleRestServlet(RestServlet): if self._is_worker: raise Exception("Cannot handle PUT /push_rules on worker") - spec = _rule_spec_from_path([x for x in path.split("/")]) + spec = _rule_spec_from_path(path.split("/")) try: priority_class = _priority_class_from_spec(spec) except InvalidRuleException as e: @@ -110,7 +110,7 @@ class PushRuleRestServlet(RestServlet): if self._is_worker: raise Exception("Cannot handle DELETE /push_rules on worker") - spec = _rule_spec_from_path([x for x in path.split("/")]) + spec = _rule_spec_from_path(path.split("/")) requester = await self.auth.get_user_by_req(request) user_id = requester.user.to_string() @@ -138,7 +138,7 @@ class PushRuleRestServlet(RestServlet): rules = format_push_rules_for_user(requester.user, rules) - path = [x for x in path.split("/")][1:] + path = path.split("/")[1:] if path == []: # we're a reference impl: pedantry is our job. diff --git a/synapse/rest/client/v1/pusher.py b/synapse/rest/client/v1/pusher.py index 6f6b7aed6e..550a2f1b44 100644 --- a/synapse/rest/client/v1/pusher.py +++ b/synapse/rest/client/v1/pusher.py @@ -54,9 +54,9 @@ class PushersRestServlet(RestServlet): pushers = await self.hs.get_datastore().get_pushers_by_user_id(user.to_string()) - filtered_pushers = list( + filtered_pushers = [ {k: v for k, v in p.items() if k in ALLOWED_KEYS} for p in pushers - ) + ] return 200, {"pushers": filtered_pushers} diff --git a/synapse/rest/client/v2_alpha/sync.py b/synapse/rest/client/v2_alpha/sync.py index d8292ce29f..8fa68dd37f 100644 --- a/synapse/rest/client/v2_alpha/sync.py +++ b/synapse/rest/client/v2_alpha/sync.py @@ -72,7 +72,7 @@ class SyncRestServlet(RestServlet): """ PATTERNS = client_patterns("/sync$") - ALLOWED_PRESENCE = set(["online", "offline", "unavailable"]) + ALLOWED_PRESENCE = {"online", "offline", "unavailable"} def __init__(self, hs): super(SyncRestServlet, self).__init__() diff --git a/synapse/rest/key/v2/remote_key_resource.py b/synapse/rest/key/v2/remote_key_resource.py index 9d6813a047..4b6d030a57 100644 --- a/synapse/rest/key/v2/remote_key_resource.py +++ b/synapse/rest/key/v2/remote_key_resource.py @@ -149,7 +149,7 @@ class RemoteKey(DirectServeResource): time_now_ms = self.clock.time_msec() - cache_misses = dict() # type: Dict[str, Set[str]] + cache_misses = {} # type: Dict[str, Set[str]] for (server_name, key_id, from_server), results in cached.items(): results = [(result["ts_added_ms"], result) for result in results] diff --git a/synapse/rest/media/v1/_base.py b/synapse/rest/media/v1/_base.py index 65bbf00073..ba28dd089d 100644 --- a/synapse/rest/media/v1/_base.py +++ b/synapse/rest/media/v1/_base.py @@ -135,27 +135,25 @@ def add_file_headers(request, media_type, file_size, upload_name): # separators as defined in RFC2616. SP and HT are handled separately. # see _can_encode_filename_as_token. -_FILENAME_SEPARATOR_CHARS = set( - ( - "(", - ")", - "<", - ">", - "@", - ",", - ";", - ":", - "\\", - '"', - "/", - "[", - "]", - "?", - "=", - "{", - "}", - ) -) +_FILENAME_SEPARATOR_CHARS = { + "(", + ")", + "<", + ">", + "@", + ",", + ";", + ":", + "\\", + '"', + "/", + "[", + "]", + "?", + "=", + "{", + "}", +} def _can_encode_filename_as_token(x): diff --git a/synapse/state/v1.py b/synapse/state/v1.py index 24b7c0faef..9bf98d06f2 100644 --- a/synapse/state/v1.py +++ b/synapse/state/v1.py @@ -69,9 +69,9 @@ def resolve_events_with_store( unconflicted_state, conflicted_state = _seperate(state_sets) - needed_events = set( + needed_events = { event_id for event_ids in itervalues(conflicted_state) for event_id in event_ids - ) + } needed_event_count = len(needed_events) if event_map is not None: needed_events -= set(iterkeys(event_map)) @@ -261,11 +261,11 @@ def _resolve_state_events(conflicted_state, auth_events): def _resolve_auth_events(events, auth_events): - reverse = [i for i in reversed(_ordered_events(events))] + reverse = list(reversed(_ordered_events(events))) - auth_keys = set( + auth_keys = { key for event in events for key in event_auth.auth_types_for_event(event) - ) + } new_auth_events = {} for key in auth_keys: diff --git a/synapse/state/v2.py b/synapse/state/v2.py index 75fe58305a..0ffe6d8c14 100644 --- a/synapse/state/v2.py +++ b/synapse/state/v2.py @@ -105,7 +105,7 @@ def resolve_events_with_store( % (room_id, event.event_id, event.room_id,) ) - full_conflicted_set = set(eid for eid in full_conflicted_set if eid in event_map) + full_conflicted_set = {eid for eid in full_conflicted_set if eid in event_map} logger.debug("%d full_conflicted_set entries", len(full_conflicted_set)) @@ -233,7 +233,7 @@ def _get_auth_chain_difference(state_sets, event_map, state_res_store): auth_sets = [] for state_set in state_sets: - auth_ids = set( + auth_ids = { eid for key, eid in iteritems(state_set) if ( @@ -246,7 +246,7 @@ def _get_auth_chain_difference(state_sets, event_map, state_res_store): ) ) and eid not in common - ) + } auth_chain = yield state_res_store.get_auth_chain(auth_ids, common) auth_ids.update(auth_chain) @@ -275,7 +275,7 @@ def _seperate(state_sets): conflicted_state = {} for key in set(itertools.chain.from_iterable(state_sets)): - event_ids = set(state_set.get(key) for state_set in state_sets) + event_ids = {state_set.get(key) for state_set in state_sets} if len(event_ids) == 1: unconflicted_state[key] = event_ids.pop() else: diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py index da3b99f93d..13de5f1f62 100644 --- a/synapse/storage/_base.py +++ b/synapse/storage/_base.py @@ -56,7 +56,7 @@ class SQLBaseStore(metaclass=ABCMeta): members_changed (iterable[str]): The user_ids of members that have changed """ - for host in set(get_domain_from_id(u) for u in members_changed): + for host in {get_domain_from_id(u) for u in members_changed}: self._attempt_to_invalidate_cache("is_host_joined", (room_id, host)) self._attempt_to_invalidate_cache("was_host_joined", (room_id, host)) diff --git a/synapse/storage/background_updates.py b/synapse/storage/background_updates.py index bd547f35cf..eb1a7e5002 100644 --- a/synapse/storage/background_updates.py +++ b/synapse/storage/background_updates.py @@ -189,7 +189,7 @@ class BackgroundUpdater(object): keyvalues=None, retcols=("update_name", "depends_on"), ) - in_flight = set(update["update_name"] for update in updates) + in_flight = {update["update_name"] for update in updates} for update in updates: if update["depends_on"] not in in_flight: self._background_update_queue.append(update["update_name"]) diff --git a/synapse/storage/data_stores/main/appservice.py b/synapse/storage/data_stores/main/appservice.py index b2f39649fd..efbc06c796 100644 --- a/synapse/storage/data_stores/main/appservice.py +++ b/synapse/storage/data_stores/main/appservice.py @@ -135,7 +135,7 @@ class ApplicationServiceTransactionWorkerStore( may be empty. """ results = yield self.db.simple_select_list( - "application_services_state", dict(state=state), ["as_id"] + "application_services_state", {"state": state}, ["as_id"] ) # NB: This assumes this class is linked with ApplicationServiceStore as_list = self.get_app_services() @@ -158,7 +158,7 @@ class ApplicationServiceTransactionWorkerStore( """ result = yield self.db.simple_select_one( "application_services_state", - dict(as_id=service.id), + {"as_id": service.id}, ["state"], allow_none=True, desc="get_appservice_state", @@ -177,7 +177,7 @@ class ApplicationServiceTransactionWorkerStore( A Deferred which resolves when the state was set successfully. """ return self.db.simple_upsert( - "application_services_state", dict(as_id=service.id), dict(state=state) + "application_services_state", {"as_id": service.id}, {"state": state} ) def create_appservice_txn(self, service, events): @@ -253,13 +253,15 @@ class ApplicationServiceTransactionWorkerStore( self.db.simple_upsert_txn( txn, "application_services_state", - dict(as_id=service.id), - dict(last_txn=txn_id), + {"as_id": service.id}, + {"last_txn": txn_id}, ) # Delete txn self.db.simple_delete_txn( - txn, "application_services_txns", dict(txn_id=txn_id, as_id=service.id) + txn, + "application_services_txns", + {"txn_id": txn_id, "as_id": service.id}, ) return self.db.runInteraction( diff --git a/synapse/storage/data_stores/main/client_ips.py b/synapse/storage/data_stores/main/client_ips.py index 13f4c9c72e..e1ccb27142 100644 --- a/synapse/storage/data_stores/main/client_ips.py +++ b/synapse/storage/data_stores/main/client_ips.py @@ -530,7 +530,7 @@ class ClientIpStore(ClientIpBackgroundUpdateStore): ((row["access_token"], row["ip"]), (row["user_agent"], row["last_seen"])) for row in rows ) - return list( + return [ { "access_token": access_token, "ip": ip, @@ -538,7 +538,7 @@ class ClientIpStore(ClientIpBackgroundUpdateStore): "last_seen": last_seen, } for (access_token, ip), (user_agent, last_seen) in iteritems(results) - ) + ] @wrap_as_background_process("prune_old_user_ips") async def _prune_old_user_ips(self): diff --git a/synapse/storage/data_stores/main/devices.py b/synapse/storage/data_stores/main/devices.py index b7617efb80..d55733a4cd 100644 --- a/synapse/storage/data_stores/main/devices.py +++ b/synapse/storage/data_stores/main/devices.py @@ -137,7 +137,7 @@ class DeviceWorkerStore(SQLBaseStore): # get the cross-signing keys of the users in the list, so that we can # determine which of the device changes were cross-signing keys - users = set(r[0] for r in updates) + users = {r[0] for r in updates} master_key_by_user = {} self_signing_key_by_user = {} for user in users: @@ -446,7 +446,7 @@ class DeviceWorkerStore(SQLBaseStore): a set of user_ids and results_map is a mapping of user_id -> device_id -> device_info """ - user_ids = set(user_id for user_id, _ in query_list) + user_ids = {user_id for user_id, _ in query_list} user_map = yield self.get_device_list_last_stream_id_for_remotes(list(user_ids)) # We go and check if any of the users need to have their device lists @@ -454,10 +454,9 @@ class DeviceWorkerStore(SQLBaseStore): users_needing_resync = yield self.get_user_ids_requiring_device_list_resync( user_ids ) - user_ids_in_cache = ( - set(user_id for user_id, stream_id in user_map.items() if stream_id) - - users_needing_resync - ) + user_ids_in_cache = { + user_id for user_id, stream_id in user_map.items() if stream_id + } - users_needing_resync user_ids_not_in_cache = user_ids - user_ids_in_cache results = {} @@ -604,7 +603,7 @@ class DeviceWorkerStore(SQLBaseStore): rows = yield self.db.execute( "get_users_whose_signatures_changed", None, sql, user_id, from_key ) - return set(user for row in rows for user in json.loads(row[0])) + return {user for row in rows for user in json.loads(row[0])} else: return set() diff --git a/synapse/storage/data_stores/main/event_federation.py b/synapse/storage/data_stores/main/event_federation.py index 750ec1b70d..49a7b8b433 100644 --- a/synapse/storage/data_stores/main/event_federation.py +++ b/synapse/storage/data_stores/main/event_federation.py @@ -426,7 +426,7 @@ class EventFederationWorkerStore(EventsWorkerStore, SignatureWorkerStore, SQLBas query, (room_id, event_id, False, limit - len(event_results)) ) - new_results = set(t[0] for t in txn) - seen_events + new_results = {t[0] for t in txn} - seen_events new_front |= new_results seen_events |= new_results diff --git a/synapse/storage/data_stores/main/events.py b/synapse/storage/data_stores/main/events.py index c9d0d68c3a..8ae23df00a 100644 --- a/synapse/storage/data_stores/main/events.py +++ b/synapse/storage/data_stores/main/events.py @@ -145,7 +145,7 @@ class EventsStore( return txn.fetchall() res = yield self.db.runInteraction("read_forward_extremities", fetch) - self._current_forward_extremities_amount = c_counter(list(x[0] for x in res)) + self._current_forward_extremities_amount = c_counter([x[0] for x in res]) @_retry_on_integrity_error @defer.inlineCallbacks @@ -598,11 +598,11 @@ class EventsStore( # We find out which membership events we may have deleted # and which we have added, then we invlidate the caches for all # those users. - members_changed = set( + members_changed = { state_key for ev_type, state_key in itertools.chain(to_delete, to_insert) if ev_type == EventTypes.Member - ) + } for member in members_changed: txn.call_after( @@ -1615,7 +1615,7 @@ class EventsStore( """ ) - referenced_state_groups = set(sg for sg, in txn) + referenced_state_groups = {sg for sg, in txn} logger.info( "[purge] found %i referenced state groups", len(referenced_state_groups) ) diff --git a/synapse/storage/data_stores/main/events_bg_updates.py b/synapse/storage/data_stores/main/events_bg_updates.py index 5177b71016..f54c8b1ee0 100644 --- a/synapse/storage/data_stores/main/events_bg_updates.py +++ b/synapse/storage/data_stores/main/events_bg_updates.py @@ -402,7 +402,7 @@ class EventsBackgroundUpdatesStore(SQLBaseStore): keyvalues={}, retcols=("room_id",), ) - room_ids = set(row["room_id"] for row in rows) + room_ids = {row["room_id"] for row in rows} for room_id in room_ids: txn.call_after( self.get_latest_event_ids_in_room.invalidate, (room_id,) diff --git a/synapse/storage/data_stores/main/events_worker.py b/synapse/storage/data_stores/main/events_worker.py index 7251e819f5..47a3a26072 100644 --- a/synapse/storage/data_stores/main/events_worker.py +++ b/synapse/storage/data_stores/main/events_worker.py @@ -494,9 +494,9 @@ class EventsWorkerStore(SQLBaseStore): """ with Measure(self._clock, "_fetch_event_list"): try: - events_to_fetch = set( + events_to_fetch = { event_id for events, _ in event_list for event_id in events - ) + } row_dict = self.db.new_transaction( conn, "do_fetch", [], [], self._fetch_event_rows, events_to_fetch @@ -804,7 +804,7 @@ class EventsWorkerStore(SQLBaseStore): desc="have_events_in_timeline", ) - return set(r["event_id"] for r in rows) + return {r["event_id"] for r in rows} @defer.inlineCallbacks def have_seen_events(self, event_ids): diff --git a/synapse/storage/data_stores/main/push_rule.py b/synapse/storage/data_stores/main/push_rule.py index e2673ae073..62ac88d9f2 100644 --- a/synapse/storage/data_stores/main/push_rule.py +++ b/synapse/storage/data_stores/main/push_rule.py @@ -276,21 +276,21 @@ class PushRulesWorkerStore( # We ignore app service users for now. This is so that we don't fill # up the `get_if_users_have_pushers` cache with AS entries that we # know don't have pushers, nor even read receipts. - local_users_in_room = set( + local_users_in_room = { u for u in users_in_room if self.hs.is_mine_id(u) and not self.get_if_app_services_interested_in_user(u) - ) + } # users in the room who have pushers need to get push rules run because # that's how their pushers work if_users_with_pushers = yield self.get_if_users_have_pushers( local_users_in_room, on_invalidate=cache_context.invalidate ) - user_ids = set( + user_ids = { uid for uid, have_pusher in if_users_with_pushers.items() if have_pusher - ) + } users_with_receipts = yield self.get_users_with_read_receipts_in_room( room_id, on_invalidate=cache_context.invalidate diff --git a/synapse/storage/data_stores/main/receipts.py b/synapse/storage/data_stores/main/receipts.py index 96e54d145e..0d932a0672 100644 --- a/synapse/storage/data_stores/main/receipts.py +++ b/synapse/storage/data_stores/main/receipts.py @@ -58,7 +58,7 @@ class ReceiptsWorkerStore(SQLBaseStore): @cachedInlineCallbacks() def get_users_with_read_receipts_in_room(self, room_id): receipts = yield self.get_receipts_for_room(room_id, "m.read") - return set(r["user_id"] for r in receipts) + return {r["user_id"] for r in receipts} @cached(num_args=2) def get_receipts_for_room(self, room_id, receipt_type): @@ -283,7 +283,7 @@ class ReceiptsWorkerStore(SQLBaseStore): args.append(limit) txn.execute(sql, args) - return list(r[0:5] + (json.loads(r[5]),) for r in txn) + return [r[0:5] + (json.loads(r[5]),) for r in txn] return self.db.runInteraction( "get_all_updated_receipts", get_all_updated_receipts_txn diff --git a/synapse/storage/data_stores/main/roommember.py b/synapse/storage/data_stores/main/roommember.py index d5ced05701..d5bd0cb5cf 100644 --- a/synapse/storage/data_stores/main/roommember.py +++ b/synapse/storage/data_stores/main/roommember.py @@ -465,7 +465,7 @@ class RoomMemberWorkerStore(EventsWorkerStore): txn.execute(sql % (clause,), args) - return set(row[0] for row in txn) + return {row[0] for row in txn} return await self.db.runInteraction( "get_users_server_still_shares_room_with", @@ -826,7 +826,7 @@ class RoomMemberWorkerStore(EventsWorkerStore): GROUP BY room_id, user_id; """ txn.execute(sql, (user_id,)) - return set(row[0] for row in txn if row[1] == 0) + return {row[0] for row in txn if row[1] == 0} return self.db.runInteraction( "get_forgotten_rooms_for_user", _get_forgotten_rooms_for_user_txn diff --git a/synapse/storage/data_stores/main/state.py b/synapse/storage/data_stores/main/state.py index 3d34103e67..3a3b9a8e72 100644 --- a/synapse/storage/data_stores/main/state.py +++ b/synapse/storage/data_stores/main/state.py @@ -321,7 +321,7 @@ class StateGroupWorkerStore(EventsWorkerStore, SQLBaseStore): desc="get_referenced_state_groups", ) - return set(row["state_group"] for row in rows) + return {row["state_group"] for row in rows} class MainStateBackgroundUpdateStore(RoomMemberWorkerStore): @@ -367,7 +367,7 @@ class MainStateBackgroundUpdateStore(RoomMemberWorkerStore): """ txn.execute(sql, (last_room_id, batch_size)) - room_ids = list(row[0] for row in txn) + room_ids = [row[0] for row in txn] if not room_ids: return True, set() @@ -384,7 +384,7 @@ class MainStateBackgroundUpdateStore(RoomMemberWorkerStore): txn.execute(sql, (last_room_id, room_ids[-1], "%:" + self.server_name)) - joined_room_ids = set(row[0] for row in txn) + joined_room_ids = {row[0] for row in txn} left_rooms = set(room_ids) - joined_room_ids @@ -404,7 +404,7 @@ class MainStateBackgroundUpdateStore(RoomMemberWorkerStore): retcols=("state_key",), ) - potentially_left_users = set(row["state_key"] for row in rows) + potentially_left_users = {row["state_key"] for row in rows} # Now lets actually delete the rooms from the DB. self.db.simple_delete_many_txn( diff --git a/synapse/storage/data_stores/main/stream.py b/synapse/storage/data_stores/main/stream.py index 056b25b13a..ada5cce6c2 100644 --- a/synapse/storage/data_stores/main/stream.py +++ b/synapse/storage/data_stores/main/stream.py @@ -346,11 +346,11 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): from_key (str): The room_key portion of a StreamToken """ from_key = RoomStreamToken.parse_stream_token(from_key).stream - return set( + return { room_id for room_id in room_ids if self._events_stream_cache.has_entity_changed(room_id, from_key) - ) + } @defer.inlineCallbacks def get_room_events_stream_for_room( @@ -679,11 +679,11 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): ) events_before = yield self.get_events_as_list( - [e for e in results["before"]["event_ids"]], get_prev_content=True + list(results["before"]["event_ids"]), get_prev_content=True ) events_after = yield self.get_events_as_list( - [e for e in results["after"]["event_ids"]], get_prev_content=True + list(results["after"]["event_ids"]), get_prev_content=True ) return { diff --git a/synapse/storage/data_stores/main/user_erasure_store.py b/synapse/storage/data_stores/main/user_erasure_store.py index af8025bc17..ec6b8a4ffd 100644 --- a/synapse/storage/data_stores/main/user_erasure_store.py +++ b/synapse/storage/data_stores/main/user_erasure_store.py @@ -63,9 +63,9 @@ class UserErasureWorkerStore(SQLBaseStore): retcols=("user_id",), desc="are_users_erased", ) - erased_users = set(row["user_id"] for row in rows) + erased_users = {row["user_id"] for row in rows} - res = dict((u, u in erased_users) for u in user_ids) + res = {u: u in erased_users for u in user_ids} return res diff --git a/synapse/storage/data_stores/state/store.py b/synapse/storage/data_stores/state/store.py index c4ee9b7ccb..57a5267663 100644 --- a/synapse/storage/data_stores/state/store.py +++ b/synapse/storage/data_stores/state/store.py @@ -520,11 +520,11 @@ class StateGroupDataStore(StateBackgroundUpdateStore, SQLBaseStore): retcols=("state_group",), ) - remaining_state_groups = set( + remaining_state_groups = { row["state_group"] for row in rows if row["state_group"] not in state_groups_to_delete - ) + } logger.info( "[purge] de-delta-ing %i remaining state groups", diff --git a/synapse/storage/database.py b/synapse/storage/database.py index 6dcb5c04da..1953614401 100644 --- a/synapse/storage/database.py +++ b/synapse/storage/database.py @@ -554,8 +554,8 @@ class Database(object): Returns: A list of dicts where the key is the column header. """ - col_headers = list(intern(str(column[0])) for column in cursor.description) - results = list(dict(zip(col_headers, row)) for row in cursor) + col_headers = [intern(str(column[0])) for column in cursor.description] + results = [dict(zip(col_headers, row)) for row in cursor] return results def execute(self, desc, decoder, query, *args): diff --git a/synapse/storage/persist_events.py b/synapse/storage/persist_events.py index b950550f23..0f9ac1cf09 100644 --- a/synapse/storage/persist_events.py +++ b/synapse/storage/persist_events.py @@ -602,14 +602,14 @@ class EventsPersistenceStorage(object): event_id_to_state_group.update(event_to_groups) # State groups of old_latest_event_ids - old_state_groups = set( + old_state_groups = { event_id_to_state_group[evid] for evid in old_latest_event_ids - ) + } # State groups of new_latest_event_ids - new_state_groups = set( + new_state_groups = { event_id_to_state_group[evid] for evid in new_latest_event_ids - ) + } # If they old and new groups are the same then we don't need to do # anything. diff --git a/synapse/storage/prepare_database.py b/synapse/storage/prepare_database.py index c285ef52a0..fc69c32a0a 100644 --- a/synapse/storage/prepare_database.py +++ b/synapse/storage/prepare_database.py @@ -345,9 +345,9 @@ def _upgrade_existing_database( "Could not open delta dir for version %d: %s" % (v, directory) ) - duplicates = set( + duplicates = { file_name for file_name, count in file_name_counter.items() if count > 1 - ) + } if duplicates: # We don't support using the same file name in the same delta version. raise PrepareDatabaseException( @@ -454,7 +454,7 @@ def _apply_module_schema_files(cur, database_engine, modname, names_and_streams) ), (modname,), ) - applied_deltas = set(d for d, in cur) + applied_deltas = {d for d, in cur} for (name, stream) in names_and_streams: if name in applied_deltas: continue diff --git a/synapse/util/frozenutils.py b/synapse/util/frozenutils.py index 635b897d6c..f2ccd5e7c6 100644 --- a/synapse/util/frozenutils.py +++ b/synapse/util/frozenutils.py @@ -30,7 +30,7 @@ def freeze(o): return o try: - return tuple([freeze(i) for i in o]) + return tuple(freeze(i) for i in o) except TypeError: pass diff --git a/synapse/visibility.py b/synapse/visibility.py index d0abd8f04f..e60d9756b7 100644 --- a/synapse/visibility.py +++ b/synapse/visibility.py @@ -75,7 +75,7 @@ def filter_events_for_client( """ # Filter out events that have been soft failed so that we don't relay them # to clients. - events = list(e for e in events if not e.internal_metadata.is_soft_failed()) + events = [e for e in events if not e.internal_metadata.is_soft_failed()] types = ((EventTypes.RoomHistoryVisibility, ""), (EventTypes.Member, user_id)) event_id_to_state = yield storage.state.get_state_for_events( @@ -97,7 +97,7 @@ def filter_events_for_client( erased_senders = yield storage.main.are_users_erased((e.sender for e in events)) if apply_retention_policies: - room_ids = set(e.room_id for e in events) + room_ids = {e.room_id for e in events} retention_policies = {} for room_id in room_ids: diff --git a/tests/config/test_generate.py b/tests/config/test_generate.py index 2684e662de..463855ecc8 100644 --- a/tests/config/test_generate.py +++ b/tests/config/test_generate.py @@ -48,7 +48,7 @@ class ConfigGenerationTestCase(unittest.TestCase): ) self.assertSetEqual( - set(["homeserver.yaml", "lemurs.win.log.config", "lemurs.win.signing.key"]), + {"homeserver.yaml", "lemurs.win.log.config", "lemurs.win.signing.key"}, set(os.listdir(self.dir)), ) diff --git a/tests/federation/test_federation_server.py b/tests/federation/test_federation_server.py index e7d8699040..296dc887be 100644 --- a/tests/federation/test_federation_server.py +++ b/tests/federation/test_federation_server.py @@ -83,7 +83,7 @@ class StateQueryTests(unittest.FederatingHomeserverTestCase): ) ) - self.assertEqual(members, set(["@user:other.example.com", u1])) + self.assertEqual(members, {"@user:other.example.com", u1}) self.assertEqual(len(channel.json_body["pdus"]), 6) def test_needs_to_be_in_room(self): diff --git a/tests/handlers/test_presence.py b/tests/handlers/test_presence.py index c171038df8..64915bafcd 100644 --- a/tests/handlers/test_presence.py +++ b/tests/handlers/test_presence.py @@ -338,7 +338,7 @@ class PresenceTimeoutTestCase(unittest.TestCase): ) new_state = handle_timeout( - state, is_mine=True, syncing_user_ids=set([user_id]), now=now + state, is_mine=True, syncing_user_ids={user_id}, now=now ) self.assertIsNotNone(new_state) @@ -579,7 +579,7 @@ class PresenceJoinTestCase(unittest.HomeserverTestCase): ) self.assertEqual(expected_state.state, PresenceState.ONLINE) self.federation_sender.send_presence_to_destinations.assert_called_once_with( - destinations=set(("server2", "server3")), states=[expected_state] + destinations={"server2", "server3"}, states=[expected_state] ) def _add_new_user(self, room_id, user_id): diff --git a/tests/handlers/test_typing.py b/tests/handlers/test_typing.py index 140cc0a3c2..07b204666e 100644 --- a/tests/handlers/test_typing.py +++ b/tests/handlers/test_typing.py @@ -129,12 +129,12 @@ class TypingNotificationsTestCase(unittest.HomeserverTestCase): hs.get_auth().check_user_in_room = check_user_in_room def get_joined_hosts_for_room(room_id): - return set(member.domain for member in self.room_members) + return {member.domain for member in self.room_members} self.datastore.get_joined_hosts_for_room = get_joined_hosts_for_room def get_current_users_in_room(room_id): - return set(str(u) for u in self.room_members) + return {str(u) for u in self.room_members} hs.get_state_handler().get_current_users_in_room = get_current_users_in_room @@ -257,7 +257,7 @@ class TypingNotificationsTestCase(unittest.HomeserverTestCase): member = RoomMember(ROOM_ID, U_APPLE.to_string()) self.handler._member_typing_until[member] = 1002000 - self.handler._room_typing[ROOM_ID] = set([U_APPLE.to_string()]) + self.handler._room_typing[ROOM_ID] = {U_APPLE.to_string()} self.assertEquals(self.event_source.get_current_key(), 0) diff --git a/tests/handlers/test_user_directory.py b/tests/handlers/test_user_directory.py index 0a4765fff4..7b92bdbc47 100644 --- a/tests/handlers/test_user_directory.py +++ b/tests/handlers/test_user_directory.py @@ -114,7 +114,7 @@ class UserDirectoryTestCase(unittest.HomeserverTestCase): public_users = self.get_users_in_public_rooms() self.assertEqual( - self._compress_shared(shares_private), set([(u1, u2, room), (u2, u1, room)]) + self._compress_shared(shares_private), {(u1, u2, room), (u2, u1, room)} ) self.assertEqual(public_users, []) @@ -169,7 +169,7 @@ class UserDirectoryTestCase(unittest.HomeserverTestCase): public_users = self.get_users_in_public_rooms() self.assertEqual( - self._compress_shared(shares_private), set([(u1, u2, room), (u2, u1, room)]) + self._compress_shared(shares_private), {(u1, u2, room), (u2, u1, room)} ) self.assertEqual(public_users, []) @@ -226,7 +226,7 @@ class UserDirectoryTestCase(unittest.HomeserverTestCase): public_users = self.get_users_in_public_rooms() self.assertEqual( - self._compress_shared(shares_private), set([(u1, u2, room), (u2, u1, room)]) + self._compress_shared(shares_private), {(u1, u2, room), (u2, u1, room)} ) self.assertEqual(public_users, []) @@ -358,12 +358,12 @@ class UserDirectoryTestCase(unittest.HomeserverTestCase): public_users = self.get_users_in_public_rooms() # User 1 and User 2 are in the same public room - self.assertEqual(set(public_users), set([(u1, room), (u2, room)])) + self.assertEqual(set(public_users), {(u1, room), (u2, room)}) # User 1 and User 3 share private rooms self.assertEqual( self._compress_shared(shares_private), - set([(u1, u3, private_room), (u3, u1, private_room)]), + {(u1, u3, private_room), (u3, u1, private_room)}, ) def test_initial_share_all_users(self): @@ -398,7 +398,7 @@ class UserDirectoryTestCase(unittest.HomeserverTestCase): # No users share rooms self.assertEqual(public_users, []) - self.assertEqual(self._compress_shared(shares_private), set([])) + self.assertEqual(self._compress_shared(shares_private), set()) # Despite not sharing a room, search_all_users means we get a search # result. diff --git a/tests/push/test_email.py b/tests/push/test_email.py index 80187406bc..83032cc9ea 100644 --- a/tests/push/test_email.py +++ b/tests/push/test_email.py @@ -163,7 +163,7 @@ class EmailPusherTests(HomeserverTestCase): # Get the stream ordering before it gets sent pushers = self.get_success( - self.hs.get_datastore().get_pushers_by(dict(user_name=self.user_id)) + self.hs.get_datastore().get_pushers_by({"user_name": self.user_id}) ) pushers = list(pushers) self.assertEqual(len(pushers), 1) @@ -174,7 +174,7 @@ class EmailPusherTests(HomeserverTestCase): # It hasn't succeeded yet, so the stream ordering shouldn't have moved pushers = self.get_success( - self.hs.get_datastore().get_pushers_by(dict(user_name=self.user_id)) + self.hs.get_datastore().get_pushers_by({"user_name": self.user_id}) ) pushers = list(pushers) self.assertEqual(len(pushers), 1) @@ -192,7 +192,7 @@ class EmailPusherTests(HomeserverTestCase): # The stream ordering has increased pushers = self.get_success( - self.hs.get_datastore().get_pushers_by(dict(user_name=self.user_id)) + self.hs.get_datastore().get_pushers_by({"user_name": self.user_id}) ) pushers = list(pushers) self.assertEqual(len(pushers), 1) diff --git a/tests/push/test_http.py b/tests/push/test_http.py index fe3441f081..baf9c785f4 100644 --- a/tests/push/test_http.py +++ b/tests/push/test_http.py @@ -102,7 +102,7 @@ class HTTPPusherTests(HomeserverTestCase): # Get the stream ordering before it gets sent pushers = self.get_success( - self.hs.get_datastore().get_pushers_by(dict(user_name=user_id)) + self.hs.get_datastore().get_pushers_by({"user_name": user_id}) ) pushers = list(pushers) self.assertEqual(len(pushers), 1) @@ -113,7 +113,7 @@ class HTTPPusherTests(HomeserverTestCase): # It hasn't succeeded yet, so the stream ordering shouldn't have moved pushers = self.get_success( - self.hs.get_datastore().get_pushers_by(dict(user_name=user_id)) + self.hs.get_datastore().get_pushers_by({"user_name": user_id}) ) pushers = list(pushers) self.assertEqual(len(pushers), 1) @@ -132,7 +132,7 @@ class HTTPPusherTests(HomeserverTestCase): # The stream ordering has increased pushers = self.get_success( - self.hs.get_datastore().get_pushers_by(dict(user_name=user_id)) + self.hs.get_datastore().get_pushers_by({"user_name": user_id}) ) pushers = list(pushers) self.assertEqual(len(pushers), 1) @@ -152,7 +152,7 @@ class HTTPPusherTests(HomeserverTestCase): # The stream ordering has increased, again pushers = self.get_success( - self.hs.get_datastore().get_pushers_by(dict(user_name=user_id)) + self.hs.get_datastore().get_pushers_by({"user_name": user_id}) ) pushers = list(pushers) self.assertEqual(len(pushers), 1) diff --git a/tests/rest/client/v2_alpha/test_sync.py b/tests/rest/client/v2_alpha/test_sync.py index 9c13a13786..fa3a3ec1bd 100644 --- a/tests/rest/client/v2_alpha/test_sync.py +++ b/tests/rest/client/v2_alpha/test_sync.py @@ -40,16 +40,14 @@ class FilterTestCase(unittest.HomeserverTestCase): self.assertEqual(channel.code, 200) self.assertTrue( - set( - [ - "next_batch", - "rooms", - "presence", - "account_data", - "to_device", - "device_lists", - ] - ).issubset(set(channel.json_body.keys())) + { + "next_batch", + "rooms", + "presence", + "account_data", + "to_device", + "device_lists", + }.issubset(set(channel.json_body.keys())) ) def test_sync_presence_disabled(self): @@ -63,9 +61,13 @@ class FilterTestCase(unittest.HomeserverTestCase): self.assertEqual(channel.code, 200) self.assertTrue( - set( - ["next_batch", "rooms", "account_data", "to_device", "device_lists"] - ).issubset(set(channel.json_body.keys())) + { + "next_batch", + "rooms", + "account_data", + "to_device", + "device_lists", + }.issubset(set(channel.json_body.keys())) ) diff --git a/tests/storage/test__base.py b/tests/storage/test__base.py index d491ea2924..e37260a820 100644 --- a/tests/storage/test__base.py +++ b/tests/storage/test__base.py @@ -373,7 +373,7 @@ class UpsertManyTests(unittest.HomeserverTestCase): ) self.assertEqual( set(self._dump_to_tuple(res)), - set([(1, "user1", "hello"), (2, "user2", "there")]), + {(1, "user1", "hello"), (2, "user2", "there")}, ) # Update only user2 @@ -400,5 +400,5 @@ class UpsertManyTests(unittest.HomeserverTestCase): ) self.assertEqual( set(self._dump_to_tuple(res)), - set([(1, "user1", "hello"), (2, "user2", "bleb")]), + {(1, "user1", "hello"), (2, "user2", "bleb")}, ) diff --git a/tests/storage/test_appservice.py b/tests/storage/test_appservice.py index fd52512696..31710949a8 100644 --- a/tests/storage/test_appservice.py +++ b/tests/storage/test_appservice.py @@ -69,14 +69,14 @@ class ApplicationServiceStoreTestCase(unittest.TestCase): pass def _add_appservice(self, as_token, id, url, hs_token, sender): - as_yaml = dict( - url=url, - as_token=as_token, - hs_token=hs_token, - id=id, - sender_localpart=sender, - namespaces={}, - ) + as_yaml = { + "url": url, + "as_token": as_token, + "hs_token": hs_token, + "id": id, + "sender_localpart": sender, + "namespaces": {}, + } # use the token as the filename with open(as_token, "w") as outfile: outfile.write(yaml.dump(as_yaml)) @@ -135,14 +135,14 @@ class ApplicationServiceTransactionStoreTestCase(unittest.TestCase): ) def _add_service(self, url, as_token, id): - as_yaml = dict( - url=url, - as_token=as_token, - hs_token="something", - id=id, - sender_localpart="a_sender", - namespaces={}, - ) + as_yaml = { + "url": url, + "as_token": as_token, + "hs_token": "something", + "id": id, + "sender_localpart": "a_sender", + "namespaces": {}, + } # use the token as the filename with open(as_token, "w") as outfile: outfile.write(yaml.dump(as_yaml)) @@ -384,8 +384,8 @@ class ApplicationServiceTransactionStoreTestCase(unittest.TestCase): ) self.assertEquals(2, len(services)) self.assertEquals( - set([self.as_list[2]["id"], self.as_list[0]["id"]]), - set([services[0].id, services[1].id]), + {self.as_list[2]["id"], self.as_list[0]["id"]}, + {services[0].id, services[1].id}, ) diff --git a/tests/storage/test_cleanup_extrems.py b/tests/storage/test_cleanup_extrems.py index 029ac26454..0e04b2cf92 100644 --- a/tests/storage/test_cleanup_extrems.py +++ b/tests/storage/test_cleanup_extrems.py @@ -134,7 +134,7 @@ class CleanupExtremBackgroundUpdateStoreTestCase(HomeserverTestCase): latest_event_ids = self.get_success( self.store.get_latest_event_ids_in_room(self.room_id) ) - self.assertEqual(set(latest_event_ids), set((event_id_a, event_id_b))) + self.assertEqual(set(latest_event_ids), {event_id_a, event_id_b}) # Run the background update and check it did the right thing self.run_background_update() @@ -172,7 +172,7 @@ class CleanupExtremBackgroundUpdateStoreTestCase(HomeserverTestCase): latest_event_ids = self.get_success( self.store.get_latest_event_ids_in_room(self.room_id) ) - self.assertEqual(set(latest_event_ids), set((event_id_a, event_id_b))) + self.assertEqual(set(latest_event_ids), {event_id_a, event_id_b}) # Run the background update and check it did the right thing self.run_background_update() @@ -227,9 +227,7 @@ class CleanupExtremBackgroundUpdateStoreTestCase(HomeserverTestCase): latest_event_ids = self.get_success( self.store.get_latest_event_ids_in_room(self.room_id) ) - self.assertEqual( - set(latest_event_ids), set((event_id_a, event_id_b, event_id_c)) - ) + self.assertEqual(set(latest_event_ids), {event_id_a, event_id_b, event_id_c}) # Run the background update and check it did the right thing self.run_background_update() @@ -237,7 +235,7 @@ class CleanupExtremBackgroundUpdateStoreTestCase(HomeserverTestCase): latest_event_ids = self.get_success( self.store.get_latest_event_ids_in_room(self.room_id) ) - self.assertEqual(set(latest_event_ids), set([event_id_b, event_id_c])) + self.assertEqual(set(latest_event_ids), {event_id_b, event_id_c}) class CleanupExtremDummyEventsTestCase(HomeserverTestCase): diff --git a/tests/storage/test_event_metrics.py b/tests/storage/test_event_metrics.py index f26ff57a18..a7b7fd36d3 100644 --- a/tests/storage/test_event_metrics.py +++ b/tests/storage/test_event_metrics.py @@ -59,24 +59,22 @@ class ExtremStatisticsTestCase(HomeserverTestCase): ) ) - expected = set( - [ - b'synapse_forward_extremities_bucket{le="1.0"} 0.0', - b'synapse_forward_extremities_bucket{le="2.0"} 2.0', - b'synapse_forward_extremities_bucket{le="3.0"} 2.0', - b'synapse_forward_extremities_bucket{le="5.0"} 2.0', - b'synapse_forward_extremities_bucket{le="7.0"} 3.0', - b'synapse_forward_extremities_bucket{le="10.0"} 3.0', - b'synapse_forward_extremities_bucket{le="15.0"} 3.0', - b'synapse_forward_extremities_bucket{le="20.0"} 3.0', - b'synapse_forward_extremities_bucket{le="50.0"} 3.0', - b'synapse_forward_extremities_bucket{le="100.0"} 3.0', - b'synapse_forward_extremities_bucket{le="200.0"} 3.0', - b'synapse_forward_extremities_bucket{le="500.0"} 3.0', - b'synapse_forward_extremities_bucket{le="+Inf"} 3.0', - b"synapse_forward_extremities_count 3.0", - b"synapse_forward_extremities_sum 10.0", - ] - ) + expected = { + b'synapse_forward_extremities_bucket{le="1.0"} 0.0', + b'synapse_forward_extremities_bucket{le="2.0"} 2.0', + b'synapse_forward_extremities_bucket{le="3.0"} 2.0', + b'synapse_forward_extremities_bucket{le="5.0"} 2.0', + b'synapse_forward_extremities_bucket{le="7.0"} 3.0', + b'synapse_forward_extremities_bucket{le="10.0"} 3.0', + b'synapse_forward_extremities_bucket{le="15.0"} 3.0', + b'synapse_forward_extremities_bucket{le="20.0"} 3.0', + b'synapse_forward_extremities_bucket{le="50.0"} 3.0', + b'synapse_forward_extremities_bucket{le="100.0"} 3.0', + b'synapse_forward_extremities_bucket{le="200.0"} 3.0', + b'synapse_forward_extremities_bucket{le="500.0"} 3.0', + b'synapse_forward_extremities_bucket{le="+Inf"} 3.0', + b"synapse_forward_extremities_count 3.0", + b"synapse_forward_extremities_sum 10.0", + } self.assertEqual(items, expected) diff --git a/tests/storage/test_state.py b/tests/storage/test_state.py index 04d58fbf24..0b88308ff4 100644 --- a/tests/storage/test_state.py +++ b/tests/storage/test_state.py @@ -394,7 +394,7 @@ class StateStoreTestCase(tests.unittest.TestCase): ) = self.state_datastore._state_group_cache.get(group) self.assertEqual(is_all, False) - self.assertEqual(known_absent, set([(e1.type, e1.state_key)])) + self.assertEqual(known_absent, {(e1.type, e1.state_key)}) self.assertDictEqual(state_dict_ids, {(e1.type, e1.state_key): e1.event_id}) ############################################ diff --git a/tests/test_state.py b/tests/test_state.py index d1578fe581..66f22f6813 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -254,9 +254,7 @@ class StateTestCase(unittest.TestCase): ctx_d = context_store["D"] prev_state_ids = yield ctx_d.get_prev_state_ids() - self.assertSetEqual( - {"START", "A", "C"}, {e_id for e_id in prev_state_ids.values()} - ) + self.assertSetEqual({"START", "A", "C"}, set(prev_state_ids.values())) self.assertEqual(ctx_c.state_group, ctx_d.state_group_before_event) self.assertEqual(ctx_d.state_group_before_event, ctx_d.state_group) @@ -313,9 +311,7 @@ class StateTestCase(unittest.TestCase): ctx_e = context_store["E"] prev_state_ids = yield ctx_e.get_prev_state_ids() - self.assertSetEqual( - {"START", "A", "B", "C"}, {e for e in prev_state_ids.values()} - ) + self.assertSetEqual({"START", "A", "B", "C"}, set(prev_state_ids.values())) self.assertEqual(ctx_c.state_group, ctx_e.state_group_before_event) self.assertEqual(ctx_e.state_group_before_event, ctx_e.state_group) @@ -388,9 +384,7 @@ class StateTestCase(unittest.TestCase): ctx_d = context_store["D"] prev_state_ids = yield ctx_d.get_prev_state_ids() - self.assertSetEqual( - {"A1", "A2", "A3", "A5", "B"}, {e for e in prev_state_ids.values()} - ) + self.assertSetEqual({"A1", "A2", "A3", "A5", "B"}, set(prev_state_ids.values())) self.assertEqual(ctx_b.state_group, ctx_d.state_group_before_event) self.assertEqual(ctx_d.state_group_before_event, ctx_d.state_group) @@ -482,7 +476,7 @@ class StateTestCase(unittest.TestCase): current_state_ids = yield context.get_current_state_ids() self.assertEqual( - set([e.event_id for e in old_state]), set(current_state_ids.values()) + {e.event_id for e in old_state}, set(current_state_ids.values()) ) self.assertEqual(group_name, context.state_group) @@ -513,9 +507,7 @@ class StateTestCase(unittest.TestCase): prev_state_ids = yield context.get_prev_state_ids() - self.assertEqual( - set([e.event_id for e in old_state]), set(prev_state_ids.values()) - ) + self.assertEqual({e.event_id for e in old_state}, set(prev_state_ids.values())) self.assertIsNotNone(context.state_group) diff --git a/tests/util/test_stream_change_cache.py b/tests/util/test_stream_change_cache.py index f2be63706b..72a9de5370 100644 --- a/tests/util/test_stream_change_cache.py +++ b/tests/util/test_stream_change_cache.py @@ -67,7 +67,7 @@ class StreamChangeCacheTests(unittest.TestCase): # If we update an existing entity, it keeps the two existing entities cache.entity_has_changed("bar@baz.net", 5) self.assertEqual( - set(["bar@baz.net", "user@elsewhere.org"]), set(cache._entity_to_key) + {"bar@baz.net", "user@elsewhere.org"}, set(cache._entity_to_key) ) def test_get_all_entities_changed(self): @@ -137,7 +137,7 @@ class StreamChangeCacheTests(unittest.TestCase): cache.get_entities_changed( ["user@foo.com", "bar@baz.net", "user@elsewhere.org"], stream_pos=2 ), - set(["bar@baz.net", "user@elsewhere.org"]), + {"bar@baz.net", "user@elsewhere.org"}, ) # Query all the entries mid-way through the stream, but include one @@ -153,7 +153,7 @@ class StreamChangeCacheTests(unittest.TestCase): ], stream_pos=2, ), - set(["bar@baz.net", "user@elsewhere.org"]), + {"bar@baz.net", "user@elsewhere.org"}, ) # Query all the entries, but before the first known point. We will get @@ -168,21 +168,13 @@ class StreamChangeCacheTests(unittest.TestCase): ], stream_pos=0, ), - set( - [ - "user@foo.com", - "bar@baz.net", - "user@elsewhere.org", - "not@here.website", - ] - ), + {"user@foo.com", "bar@baz.net", "user@elsewhere.org", "not@here.website"}, ) # Query a subset of the entries mid-way through the stream. We should # only get back the subset. self.assertEqual( - cache.get_entities_changed(["bar@baz.net"], stream_pos=2), - set(["bar@baz.net"]), + cache.get_entities_changed(["bar@baz.net"], stream_pos=2), {"bar@baz.net"}, ) def test_max_pos(self): diff --git a/tox.ini b/tox.ini index b9132a3177..b715ea0bff 100644 --- a/tox.ini +++ b/tox.ini @@ -123,6 +123,7 @@ skip_install = True basepython = python3.6 deps = flake8 + flake8-comprehensions black==19.10b0 # We pin so that our tests don't start failing on new releases of black. commands = python -m black --check --diff . From 7936d2a96e4781ad7d1ae27f78b65c8eb8d5c3f5 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Fri, 21 Feb 2020 07:18:33 -0500 Subject: [PATCH 1101/1623] Publishing/removing from the directory requires a power level greater than canonical aliases. --- changelog.d/6965.feature | 1 + synapse/api/auth.py | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 changelog.d/6965.feature diff --git a/changelog.d/6965.feature b/changelog.d/6965.feature new file mode 100644 index 0000000000..6ad9956e40 --- /dev/null +++ b/changelog.d/6965.feature @@ -0,0 +1 @@ +Publishing/removing a room from the room directory now requires the user to have a power level capable of modifying the canonical alias, instead of the room aliases. diff --git a/synapse/api/auth.py b/synapse/api/auth.py index f576d65388..5ca18b4301 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -538,13 +538,13 @@ class Auth(object): return defer.succeed(auth_ids) @defer.inlineCallbacks - def check_can_change_room_list(self, room_id, user): + def check_can_change_room_list(self, room_id: str, user: UserID): """Check if the user is allowed to edit the room's entry in the published room list. Args: - room_id (str) - user (UserID) + room_id + user """ is_admin = yield self.is_server_admin(user) @@ -556,7 +556,7 @@ class Auth(object): # We currently require the user is a "moderator" in the room. We do this # by checking if they would (theoretically) be able to change the - # m.room.aliases events + # m.room.canonical_alias events power_level_event = yield self.state.get_current_state( room_id, EventTypes.PowerLevels, "" ) @@ -566,7 +566,7 @@ class Auth(object): auth_events[(EventTypes.PowerLevels, "")] = power_level_event send_level = event_auth.get_send_level( - EventTypes.Aliases, "", power_level_event + EventTypes.CanonicalAlias, "", power_level_event ) user_level = event_auth.get_user_power_level(user_id, auth_events) From fcf45994881d652571d965547b2287d796f798fc Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Fri, 21 Feb 2020 12:40:23 -0500 Subject: [PATCH 1102/1623] Stop returning aliases as part of the room list. (#6970) --- changelog.d/6970.removal | 1 + synapse/handlers/room_list.py | 9 --------- 2 files changed, 1 insertion(+), 9 deletions(-) create mode 100644 changelog.d/6970.removal diff --git a/changelog.d/6970.removal b/changelog.d/6970.removal new file mode 100644 index 0000000000..89bd363b95 --- /dev/null +++ b/changelog.d/6970.removal @@ -0,0 +1 @@ +The room list endpoint no longer returns a list of aliases. diff --git a/synapse/handlers/room_list.py b/synapse/handlers/room_list.py index c615206df1..0b7d3da680 100644 --- a/synapse/handlers/room_list.py +++ b/synapse/handlers/room_list.py @@ -216,15 +216,6 @@ class RoomListHandler(BaseHandler): direction_is_forward=False, ).to_token() - for room in results: - # populate search result entries with additional fields, namely - # 'aliases' - room_id = room["room_id"] - - aliases = yield self.store.get_aliases_for_room(room_id) - if aliases: - room["aliases"] = aliases - response["chunk"] = results response["total_room_count_estimate"] = yield self.store.count_public_rooms( From 7b0e2d961ce9b70ed2d41f27f624ab752af26400 Mon Sep 17 00:00:00 2001 From: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com> Date: Fri, 21 Feb 2020 18:44:03 +0100 Subject: [PATCH 1103/1623] Change displayname of user as admin in rooms (#6876) --- changelog.d/6572.bugfix | 1 + synapse/handlers/profile.py | 12 +++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 changelog.d/6572.bugfix diff --git a/changelog.d/6572.bugfix b/changelog.d/6572.bugfix new file mode 100644 index 0000000000..4f708f409f --- /dev/null +++ b/changelog.d/6572.bugfix @@ -0,0 +1 @@ +When a user's profile is updated via the admin API, also generate a displayname/avatar update for that user in each room. diff --git a/synapse/handlers/profile.py b/synapse/handlers/profile.py index f9579d69ee..50ce0c585b 100644 --- a/synapse/handlers/profile.py +++ b/synapse/handlers/profile.py @@ -28,7 +28,7 @@ from synapse.api.errors import ( SynapseError, ) from synapse.metrics.background_process_metrics import run_as_background_process -from synapse.types import UserID, get_domain_from_id +from synapse.types import UserID, create_requester, get_domain_from_id from ._base import BaseHandler @@ -165,6 +165,12 @@ class BaseProfileHandler(BaseHandler): if new_displayname == "": new_displayname = None + # If the admin changes the display name of a user, the requesting user cannot send + # the join event to update the displayname in the rooms. + # This must be done by the target user himself. + if by_admin: + requester = create_requester(target_user) + yield self.store.set_profile_displayname(target_user.localpart, new_displayname) if self.hs.config.user_directory_search_all_users: @@ -217,6 +223,10 @@ class BaseProfileHandler(BaseHandler): 400, "Avatar URL is too long (max %i)" % (MAX_AVATAR_URL_LEN,) ) + # Same like set_displayname + if by_admin: + requester = create_requester(target_user) + yield self.store.set_profile_avatar_url(target_user.localpart, new_avatar_url) if self.hs.config.user_directory_search_all_users: From af6c3895015580d04c5affd95321c75802c3cb62 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Fri, 21 Feb 2020 12:50:48 -0500 Subject: [PATCH 1104/1623] No longer use room alias events to calculate room names for push notifications. (#6966) --- changelog.d/6966.removal | 1 + synapse/push/presentable_names.py | 36 ++++++++++++------------------- 2 files changed, 15 insertions(+), 22 deletions(-) create mode 100644 changelog.d/6966.removal diff --git a/changelog.d/6966.removal b/changelog.d/6966.removal new file mode 100644 index 0000000000..69673d9139 --- /dev/null +++ b/changelog.d/6966.removal @@ -0,0 +1 @@ +Synapse no longer uses room alias events to calculate room names for email notifications. diff --git a/synapse/push/presentable_names.py b/synapse/push/presentable_names.py index 16a7e8e31d..0644a13cfc 100644 --- a/synapse/push/presentable_names.py +++ b/synapse/push/presentable_names.py @@ -18,6 +18,8 @@ import re from twisted.internet import defer +from synapse.api.constants import EventTypes + logger = logging.getLogger(__name__) # intentionally looser than what aliases we allow to be registered since @@ -50,17 +52,17 @@ def calculate_room_name( (string or None) A human readable name for the room. """ # does it have a name? - if ("m.room.name", "") in room_state_ids: + if (EventTypes.Name, "") in room_state_ids: m_room_name = yield store.get_event( - room_state_ids[("m.room.name", "")], allow_none=True + room_state_ids[(EventTypes.Name, "")], allow_none=True ) if m_room_name and m_room_name.content and m_room_name.content["name"]: return m_room_name.content["name"] # does it have a canonical alias? - if ("m.room.canonical_alias", "") in room_state_ids: + if (EventTypes.CanonicalAlias, "") in room_state_ids: canon_alias = yield store.get_event( - room_state_ids[("m.room.canonical_alias", "")], allow_none=True + room_state_ids[(EventTypes.CanonicalAlias, "")], allow_none=True ) if ( canon_alias @@ -74,32 +76,22 @@ def calculate_room_name( # for an event type, so rearrange the data structure room_state_bytype_ids = _state_as_two_level_dict(room_state_ids) - # right then, any aliases at all? - if "m.room.aliases" in room_state_bytype_ids: - m_room_aliases = room_state_bytype_ids["m.room.aliases"] - for alias_id in m_room_aliases.values(): - alias_event = yield store.get_event(alias_id, allow_none=True) - if alias_event and alias_event.content.get("aliases"): - the_aliases = alias_event.content["aliases"] - if len(the_aliases) > 0 and _looks_like_an_alias(the_aliases[0]): - return the_aliases[0] - if not fallback_to_members: return None my_member_event = None - if ("m.room.member", user_id) in room_state_ids: + if (EventTypes.Member, user_id) in room_state_ids: my_member_event = yield store.get_event( - room_state_ids[("m.room.member", user_id)], allow_none=True + room_state_ids[(EventTypes.Member, user_id)], allow_none=True ) if ( my_member_event is not None and my_member_event.content["membership"] == "invite" ): - if ("m.room.member", my_member_event.sender) in room_state_ids: + if (EventTypes.Member, my_member_event.sender) in room_state_ids: inviter_member_event = yield store.get_event( - room_state_ids[("m.room.member", my_member_event.sender)], + room_state_ids[(EventTypes.Member, my_member_event.sender)], allow_none=True, ) if inviter_member_event: @@ -114,9 +106,9 @@ def calculate_room_name( # we're going to have to generate a name based on who's in the room, # so find out who is in the room that isn't the user. - if "m.room.member" in room_state_bytype_ids: + if EventTypes.Member in room_state_bytype_ids: member_events = yield store.get_events( - list(room_state_bytype_ids["m.room.member"].values()) + list(room_state_bytype_ids[EventTypes.Member].values()) ) all_members = [ ev @@ -138,9 +130,9 @@ def calculate_room_name( # self-chat, peeked room with 1 participant, # or inbound invite, or outbound 3PID invite. if all_members[0].sender == user_id: - if "m.room.third_party_invite" in room_state_bytype_ids: + if EventTypes.ThirdPartyInvite in room_state_bytype_ids: third_party_invites = room_state_bytype_ids[ - "m.room.third_party_invite" + EventTypes.ThirdPartyInvite ].values() if len(third_party_invites) > 0: From 4c2ed3f20ef5361ea04da9c678d157d8735ca120 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 24 Feb 2020 15:18:38 +0000 Subject: [PATCH 1105/1623] Fix minor issues with email config (#6962) * Give `notif_template_html`, `notif_template_text` default values (fixes #6960) * Don't complain if `smtp_host` and `smtp_port` are unset, since they have sensible defaults (fixes #6961) * Set the example for `enable_notifs` to `True`, for consistency and because it's more useful * Raise errors as ConfigError rather than RuntimeError for nicer formatting --- changelog.d/6962.bugfix | 1 + docs/sample_config.yaml | 9 ++--- synapse/config/emailconfig.py | 66 ++++++++++++++++------------------- 3 files changed, 36 insertions(+), 40 deletions(-) create mode 100644 changelog.d/6962.bugfix diff --git a/changelog.d/6962.bugfix b/changelog.d/6962.bugfix new file mode 100644 index 0000000000..9f5229d400 --- /dev/null +++ b/changelog.d/6962.bugfix @@ -0,0 +1 @@ +Fix a couple of bugs in email configuration handling. diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 8a036071e1..54cbe840d5 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -1409,10 +1409,6 @@ email: # #require_transport_security: true - # Enable sending emails for messages that the user has missed - # - #enable_notifs: false - # notif_from defines the "From" address to use when sending emails. # It must be set if email sending is enabled. # @@ -1430,6 +1426,11 @@ email: # #app_name: my_branded_matrix_server + # Uncomment the following to enable sending emails for messages that the user + # has missed. Disabled by default. + # + #enable_notifs: true + # Uncomment the following to disable automatic subscription to email # notifications for new users. Enabled by default. # diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py index 74853f9faa..f31fc85ec8 100644 --- a/synapse/config/emailconfig.py +++ b/synapse/config/emailconfig.py @@ -27,6 +27,12 @@ import pkg_resources from ._base import Config, ConfigError +MISSING_PASSWORD_RESET_CONFIG_ERROR = """\ +Password reset emails are enabled on this homeserver due to a partial +'email' block. However, the following required keys are missing: + %s +""" + class EmailConfig(Config): section = "email" @@ -142,24 +148,18 @@ class EmailConfig(Config): bleach if self.threepid_behaviour_email == ThreepidBehaviour.LOCAL: - required = ["smtp_host", "smtp_port", "notif_from"] - missing = [] - for k in required: - if k not in email_config: - missing.append("email." + k) + if not self.email_notif_from: + missing.append("email.notif_from") # public_baseurl is required to build password reset and validation links that # will be emailed to users if config.get("public_baseurl") is None: missing.append("public_baseurl") - if len(missing) > 0: - raise RuntimeError( - "Password resets emails are configured to be sent from " - "this homeserver due to a partial 'email' block. " - "However, the following required keys are missing: %s" - % (", ".join(missing),) + if missing: + raise ConfigError( + MISSING_PASSWORD_RESET_CONFIG_ERROR % (", ".join(missing),) ) # These email templates have placeholders in them, and thus must be @@ -245,32 +245,25 @@ class EmailConfig(Config): ) if self.email_enable_notifs: - required = [ - "smtp_host", - "smtp_port", - "notif_from", - "notif_template_html", - "notif_template_text", - ] - missing = [] - for k in required: - if k not in email_config: - missing.append(k) - - if len(missing) > 0: - raise RuntimeError( - "email.enable_notifs is True but required keys are missing: %s" - % (", ".join(["email." + k for k in missing]),) - ) + if not self.email_notif_from: + missing.append("email.notif_from") if config.get("public_baseurl") is None: - raise RuntimeError( - "email.enable_notifs is True but no public_baseurl is set" + missing.append("public_baseurl") + + if missing: + raise ConfigError( + "email.enable_notifs is True but required keys are missing: %s" + % (", ".join(missing),) ) - self.email_notif_template_html = email_config["notif_template_html"] - self.email_notif_template_text = email_config["notif_template_text"] + self.email_notif_template_html = email_config.get( + "notif_template_html", "notif_mail.html" + ) + self.email_notif_template_text = email_config.get( + "notif_template_text", "notif_mail.txt" + ) for f in self.email_notif_template_text, self.email_notif_template_html: p = os.path.join(self.email_template_dir, f) @@ -323,10 +316,6 @@ class EmailConfig(Config): # #require_transport_security: true - # Enable sending emails for messages that the user has missed - # - #enable_notifs: false - # notif_from defines the "From" address to use when sending emails. # It must be set if email sending is enabled. # @@ -344,6 +333,11 @@ class EmailConfig(Config): # #app_name: my_branded_matrix_server + # Uncomment the following to enable sending emails for messages that the user + # has missed. Disabled by default. + # + #enable_notifs: true + # Uncomment the following to disable automatic subscription to email # notifications for new users. Enabled by default. # From a301934f4610ffce490fbb925aaa898aac2829bc Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 24 Feb 2020 15:46:41 +0000 Subject: [PATCH 1106/1623] Upsert room version when we join over federation (#6968) This is intended as a precursor to storing room versions when we receive an invite over federation, but has the happy side-effect of fixing #3374 at last. In short: change the store_room with try/except to a proper upsert which updates the right columns. --- changelog.d/6968.bugfix | 1 + synapse/handlers/federation.py | 22 ++++++++++++---------- synapse/storage/data_stores/main/room.py | 17 +++++++++++++++++ 3 files changed, 30 insertions(+), 10 deletions(-) create mode 100644 changelog.d/6968.bugfix diff --git a/changelog.d/6968.bugfix b/changelog.d/6968.bugfix new file mode 100644 index 0000000000..9965bfc0c3 --- /dev/null +++ b/changelog.d/6968.bugfix @@ -0,0 +1 @@ +Fix `duplicate key` error which was logged when rejoining a room over federation. diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index a689065f89..fb0a586eaa 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1323,16 +1323,18 @@ class FederationHandler(BaseHandler): logger.debug("do_invite_join event: %s", event) - try: - await self.store.store_room( - room_id=room_id, - room_creator_user_id="", - is_public=False, - room_version=room_version_obj, - ) - except Exception: - # FIXME - pass + # if this is the first time we've joined this room, it's time to add + # a row to `rooms` with the correct room version. If there's already a + # row there, we should override it, since it may have been populated + # based on an invite request which lied about the room version. + # + # federation_client.send_join has already checked that the room + # version in the received create event is the same as room_version_obj, + # so we can rely on it now. + # + await self.store.upsert_room_on_join( + room_id=room_id, room_version=room_version_obj, + ) await self._persist_auth_tree( origin, auth_chain, state, event, room_version_obj diff --git a/synapse/storage/data_stores/main/room.py b/synapse/storage/data_stores/main/room.py index 9a17e336ba..70137dfbe4 100644 --- a/synapse/storage/data_stores/main/room.py +++ b/synapse/storage/data_stores/main/room.py @@ -954,6 +954,23 @@ class RoomStore(RoomBackgroundUpdateStore, RoomWorkerStore, SearchStore): self.config = hs.config + async def upsert_room_on_join(self, room_id: str, room_version: RoomVersion): + """Ensure that the room is stored in the table + + Called when we join a room over federation, and overwrites any room version + currently in the table. + """ + await self.db.simple_upsert( + desc="upsert_room_on_join", + table="rooms", + keyvalues={"room_id": room_id}, + values={"room_version": room_version.identifier}, + insertion_values={"is_public": False, "creator": ""}, + # rooms has a unique constraint on room_id, so no need to lock when doing an + # emulated upsert. + lock=False, + ) + @defer.inlineCallbacks def store_room( self, From 691659568fa57f6afd9918886efc72b9e7081d8f Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 24 Feb 2020 17:20:45 +0000 Subject: [PATCH 1107/1623] Remove redundant store_room call (#6979) `_process_received_pdu` is only called by `on_receive_pdu`, which ignores any events for unknown rooms, so this is redundant. --- changelog.d/6979.misc | 1 + synapse/handlers/federation.py | 23 ----------------------- 2 files changed, 1 insertion(+), 23 deletions(-) create mode 100644 changelog.d/6979.misc diff --git a/changelog.d/6979.misc b/changelog.d/6979.misc new file mode 100644 index 0000000000..c57b398c2f --- /dev/null +++ b/changelog.d/6979.misc @@ -0,0 +1 @@ +Remove redundant `store_room` call from `FederationHandler._process_received_pdu`. diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index fb0a586eaa..c2e6ee266d 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -41,7 +41,6 @@ from synapse.api.errors import ( FederationDeniedError, FederationError, RequestSendFailed, - StoreError, SynapseError, ) from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersion, RoomVersions @@ -707,28 +706,6 @@ class FederationHandler(BaseHandler): except AuthError as e: raise FederationError("ERROR", e.code, e.msg, affected=event.event_id) - room = await self.store.get_room(room_id) - - if not room: - try: - prev_state_ids = await context.get_prev_state_ids() - create_event = await self.store.get_event( - prev_state_ids[(EventTypes.Create, "")] - ) - - room_version_id = create_event.content.get( - "room_version", RoomVersions.V1.identifier - ) - - await self.store.store_room( - room_id=room_id, - room_creator_user_id="", - is_public=False, - room_version=KNOWN_ROOM_VERSIONS[room_version_id], - ) - except StoreError: - logger.exception("Failed to store room.") - if event.type == EventTypes.Member: if event.membership == Membership.JOIN: # Only fire user_joined_room if the user has acutally From 4aea0bd292bf9e33d166fcfd632159cfe35050dd Mon Sep 17 00:00:00 2001 From: Fridtjof Mund <2780577+fridtjof@users.noreply.github.com> Date: Tue, 25 Feb 2020 11:48:13 +0100 Subject: [PATCH 1108/1623] contrib/docker: remove quotes for POSTGRES_INITDB_ARGS (#6984) I made a mistake in https://github.com/matrix-org/synapse/pull/6921 - the quotes break the postgres container's startup script (or docker-compose), which makes initdb fail: https://github.com/matrix-org/synapse/pull/6921#issuecomment-590657154 Signed-off-by: Fridtjof Mund --- changelog.d/6984.docker | 1 + contrib/docker/docker-compose.yml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/6984.docker diff --git a/changelog.d/6984.docker b/changelog.d/6984.docker new file mode 100644 index 0000000000..84a55e1267 --- /dev/null +++ b/changelog.d/6984.docker @@ -0,0 +1 @@ +Fix `POSTGRES_INITDB_ARGS` in the `contrib/docker/docker-compose.yml` example docker-compose configuration. diff --git a/contrib/docker/docker-compose.yml b/contrib/docker/docker-compose.yml index 5df29379c8..453b305053 100644 --- a/contrib/docker/docker-compose.yml +++ b/contrib/docker/docker-compose.yml @@ -58,7 +58,7 @@ services: - POSTGRES_PASSWORD=changeme # ensure the database gets created correctly # https://github.com/matrix-org/synapse/blob/master/docs/postgres.md#set-up-database - - POSTGRES_INITDB_ARGS="--encoding=UTF-8 --lc-collate=C --lc-ctype=C" + - POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C volumes: # You may store the database tables in a local folder.. - ./schemas:/var/lib/postgresql/data From bbf8886a05be6a929556d6f09a1b6ce053a3c403 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 25 Feb 2020 16:56:55 +0000 Subject: [PATCH 1109/1623] Merge worker apps into one. (#6964) --- changelog.d/6964.misc | 1 + synapse/app/appservice.py | 156 +--- synapse/app/client_reader.py | 190 +--- synapse/app/event_creator.py | 186 +--- synapse/app/federation_reader.py | 172 +--- synapse/app/federation_sender.py | 303 +------ synapse/app/frontend_proxy.py | 236 +---- synapse/app/generic_worker.py | 917 ++++++++++++++++++++ synapse/app/media_repository.py | 157 +--- synapse/app/pusher.py | 209 +---- synapse/app/synchrotron.py | 449 +--------- synapse/app/user_dir.py | 211 +---- synapse/replication/slave/storage/events.py | 20 + synapse/storage/data_stores/main/pusher.py | 156 ++-- tests/app/test_frontend_proxy.py | 12 +- tests/app/test_openid_listener.py | 4 +- 16 files changed, 1052 insertions(+), 2327 deletions(-) create mode 100644 changelog.d/6964.misc create mode 100644 synapse/app/generic_worker.py diff --git a/changelog.d/6964.misc b/changelog.d/6964.misc new file mode 100644 index 0000000000..ec5c004bbe --- /dev/null +++ b/changelog.d/6964.misc @@ -0,0 +1 @@ +Merge worker apps together. diff --git a/synapse/app/appservice.py b/synapse/app/appservice.py index 2217d4a4fb..add43147b3 100644 --- a/synapse/app/appservice.py +++ b/synapse/app/appservice.py @@ -13,161 +13,11 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import logging + import sys -from twisted.internet import defer, reactor -from twisted.web.resource import NoResource - -import synapse -from synapse import events -from synapse.app import _base -from synapse.config._base import ConfigError -from synapse.config.homeserver import HomeServerConfig -from synapse.config.logger import setup_logging -from synapse.http.site import SynapseSite -from synapse.logging.context import LoggingContext, run_in_background -from synapse.metrics import METRICS_PREFIX, MetricsResource, RegistryProxy -from synapse.replication.slave.storage.appservice import SlavedApplicationServiceStore -from synapse.replication.slave.storage.directory import DirectoryStore -from synapse.replication.slave.storage.events import SlavedEventStore -from synapse.replication.slave.storage.registration import SlavedRegistrationStore -from synapse.replication.tcp.client import ReplicationClientHandler -from synapse.server import HomeServer -from synapse.util.httpresourcetree import create_resource_tree -from synapse.util.manhole import manhole -from synapse.util.versionstring import get_version_string - -logger = logging.getLogger("synapse.app.appservice") - - -class AppserviceSlaveStore( - DirectoryStore, - SlavedEventStore, - SlavedApplicationServiceStore, - SlavedRegistrationStore, -): - pass - - -class AppserviceServer(HomeServer): - DATASTORE_CLASS = AppserviceSlaveStore - - def _listen_http(self, listener_config): - port = listener_config["port"] - bind_addresses = listener_config["bind_addresses"] - site_tag = listener_config.get("tag", port) - resources = {} - for res in listener_config["resources"]: - for name in res["names"]: - if name == "metrics": - resources[METRICS_PREFIX] = MetricsResource(RegistryProxy) - - root_resource = create_resource_tree(resources, NoResource()) - - _base.listen_tcp( - bind_addresses, - port, - SynapseSite( - "synapse.access.http.%s" % (site_tag,), - site_tag, - listener_config, - root_resource, - self.version_string, - ), - ) - - logger.info("Synapse appservice now listening on port %d", port) - - def start_listening(self, listeners): - for listener in listeners: - if listener["type"] == "http": - self._listen_http(listener) - elif listener["type"] == "manhole": - _base.listen_tcp( - listener["bind_addresses"], - listener["port"], - manhole( - username="matrix", password="rabbithole", globals={"hs": self} - ), - ) - elif listener["type"] == "metrics": - if not self.get_config().enable_metrics: - logger.warning( - ( - "Metrics listener configured, but " - "enable_metrics is not True!" - ) - ) - else: - _base.listen_metrics(listener["bind_addresses"], listener["port"]) - else: - logger.warning("Unrecognized listener type: %s", listener["type"]) - - self.get_tcp_replication().start_replication(self) - - def build_tcp_replication(self): - return ASReplicationHandler(self) - - -class ASReplicationHandler(ReplicationClientHandler): - def __init__(self, hs): - super(ASReplicationHandler, self).__init__(hs.get_datastore()) - self.appservice_handler = hs.get_application_service_handler() - - async def on_rdata(self, stream_name, token, rows): - await super(ASReplicationHandler, self).on_rdata(stream_name, token, rows) - - if stream_name == "events": - max_stream_id = self.store.get_room_max_stream_ordering() - run_in_background(self._notify_app_services, max_stream_id) - - @defer.inlineCallbacks - def _notify_app_services(self, room_stream_id): - try: - yield self.appservice_handler.notify_interested_services(room_stream_id) - except Exception: - logger.exception("Error notifying application services of event") - - -def start(config_options): - try: - config = HomeServerConfig.load_config("Synapse appservice", config_options) - except ConfigError as e: - sys.stderr.write("\n" + str(e) + "\n") - sys.exit(1) - - assert config.worker_app == "synapse.app.appservice" - - events.USE_FROZEN_DICTS = config.use_frozen_dicts - - if config.notify_appservices: - sys.stderr.write( - "\nThe appservices must be disabled in the main synapse process" - "\nbefore they can be run in a separate worker." - "\nPlease add ``notify_appservices: false`` to the main config" - "\n" - ) - sys.exit(1) - - # Force the pushers to start since they will be disabled in the main config - config.notify_appservices = True - - ps = AppserviceServer( - config.server_name, - config=config, - version_string="Synapse/" + get_version_string(synapse), - ) - - setup_logging(ps, config, use_worker_options=True) - - ps.setup() - reactor.addSystemEventTrigger( - "before", "startup", _base.start, ps, config.worker_listeners - ) - - _base.start_worker_reactor("synapse-appservice", config) - +from synapse.app.generic_worker import start +from synapse.util.logcontext import LoggingContext if __name__ == "__main__": with LoggingContext("main"): diff --git a/synapse/app/client_reader.py b/synapse/app/client_reader.py index 7fa91a3b11..add43147b3 100644 --- a/synapse/app/client_reader.py +++ b/synapse/app/client_reader.py @@ -13,195 +13,11 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import logging + import sys -from twisted.internet import reactor -from twisted.web.resource import NoResource - -import synapse -from synapse import events -from synapse.app import _base -from synapse.config._base import ConfigError -from synapse.config.homeserver import HomeServerConfig -from synapse.config.logger import setup_logging -from synapse.http.server import JsonResource -from synapse.http.site import SynapseSite -from synapse.logging.context import LoggingContext -from synapse.metrics import METRICS_PREFIX, MetricsResource, RegistryProxy -from synapse.replication.slave.storage._base import BaseSlavedStore -from synapse.replication.slave.storage.account_data import SlavedAccountDataStore -from synapse.replication.slave.storage.appservice import SlavedApplicationServiceStore -from synapse.replication.slave.storage.client_ips import SlavedClientIpStore -from synapse.replication.slave.storage.deviceinbox import SlavedDeviceInboxStore -from synapse.replication.slave.storage.devices import SlavedDeviceStore -from synapse.replication.slave.storage.directory import DirectoryStore -from synapse.replication.slave.storage.events import SlavedEventStore -from synapse.replication.slave.storage.groups import SlavedGroupServerStore -from synapse.replication.slave.storage.keys import SlavedKeyStore -from synapse.replication.slave.storage.profile import SlavedProfileStore -from synapse.replication.slave.storage.push_rule import SlavedPushRuleStore -from synapse.replication.slave.storage.receipts import SlavedReceiptsStore -from synapse.replication.slave.storage.registration import SlavedRegistrationStore -from synapse.replication.slave.storage.room import RoomStore -from synapse.replication.slave.storage.transactions import SlavedTransactionStore -from synapse.replication.tcp.client import ReplicationClientHandler -from synapse.rest.client.v1.login import LoginRestServlet -from synapse.rest.client.v1.push_rule import PushRuleRestServlet -from synapse.rest.client.v1.room import ( - JoinedRoomMemberListRestServlet, - PublicRoomListRestServlet, - RoomEventContextServlet, - RoomMemberListRestServlet, - RoomMessageListRestServlet, - RoomStateRestServlet, -) -from synapse.rest.client.v1.voip import VoipRestServlet -from synapse.rest.client.v2_alpha import groups -from synapse.rest.client.v2_alpha.account import ThreepidRestServlet -from synapse.rest.client.v2_alpha.keys import KeyChangesServlet, KeyQueryServlet -from synapse.rest.client.v2_alpha.register import RegisterRestServlet -from synapse.rest.client.versions import VersionsRestServlet -from synapse.server import HomeServer -from synapse.storage.data_stores.main.monthly_active_users import ( - MonthlyActiveUsersWorkerStore, -) -from synapse.util.httpresourcetree import create_resource_tree -from synapse.util.manhole import manhole -from synapse.util.versionstring import get_version_string - -logger = logging.getLogger("synapse.app.client_reader") - - -class ClientReaderSlavedStore( - SlavedDeviceInboxStore, - SlavedDeviceStore, - SlavedReceiptsStore, - SlavedPushRuleStore, - SlavedGroupServerStore, - SlavedAccountDataStore, - SlavedEventStore, - SlavedKeyStore, - RoomStore, - DirectoryStore, - SlavedApplicationServiceStore, - SlavedRegistrationStore, - SlavedTransactionStore, - SlavedProfileStore, - SlavedClientIpStore, - MonthlyActiveUsersWorkerStore, - BaseSlavedStore, -): - pass - - -class ClientReaderServer(HomeServer): - DATASTORE_CLASS = ClientReaderSlavedStore - - def _listen_http(self, listener_config): - port = listener_config["port"] - bind_addresses = listener_config["bind_addresses"] - site_tag = listener_config.get("tag", port) - resources = {} - for res in listener_config["resources"]: - for name in res["names"]: - if name == "metrics": - resources[METRICS_PREFIX] = MetricsResource(RegistryProxy) - elif name == "client": - resource = JsonResource(self, canonical_json=False) - - PublicRoomListRestServlet(self).register(resource) - RoomMemberListRestServlet(self).register(resource) - JoinedRoomMemberListRestServlet(self).register(resource) - RoomStateRestServlet(self).register(resource) - RoomEventContextServlet(self).register(resource) - RoomMessageListRestServlet(self).register(resource) - RegisterRestServlet(self).register(resource) - LoginRestServlet(self).register(resource) - ThreepidRestServlet(self).register(resource) - KeyQueryServlet(self).register(resource) - KeyChangesServlet(self).register(resource) - VoipRestServlet(self).register(resource) - PushRuleRestServlet(self).register(resource) - VersionsRestServlet(self).register(resource) - - groups.register_servlets(self, resource) - - resources.update({"/_matrix/client": resource}) - - root_resource = create_resource_tree(resources, NoResource()) - - _base.listen_tcp( - bind_addresses, - port, - SynapseSite( - "synapse.access.http.%s" % (site_tag,), - site_tag, - listener_config, - root_resource, - self.version_string, - ), - ) - - logger.info("Synapse client reader now listening on port %d", port) - - def start_listening(self, listeners): - for listener in listeners: - if listener["type"] == "http": - self._listen_http(listener) - elif listener["type"] == "manhole": - _base.listen_tcp( - listener["bind_addresses"], - listener["port"], - manhole( - username="matrix", password="rabbithole", globals={"hs": self} - ), - ) - elif listener["type"] == "metrics": - if not self.get_config().enable_metrics: - logger.warning( - ( - "Metrics listener configured, but " - "enable_metrics is not True!" - ) - ) - else: - _base.listen_metrics(listener["bind_addresses"], listener["port"]) - else: - logger.warning("Unrecognized listener type: %s", listener["type"]) - - self.get_tcp_replication().start_replication(self) - - def build_tcp_replication(self): - return ReplicationClientHandler(self.get_datastore()) - - -def start(config_options): - try: - config = HomeServerConfig.load_config("Synapse client reader", config_options) - except ConfigError as e: - sys.stderr.write("\n" + str(e) + "\n") - sys.exit(1) - - assert config.worker_app == "synapse.app.client_reader" - - events.USE_FROZEN_DICTS = config.use_frozen_dicts - - ss = ClientReaderServer( - config.server_name, - config=config, - version_string="Synapse/" + get_version_string(synapse), - ) - - setup_logging(ss, config, use_worker_options=True) - - ss.setup() - reactor.addSystemEventTrigger( - "before", "startup", _base.start, ss, config.worker_listeners - ) - - _base.start_worker_reactor("synapse-client-reader", config) - +from synapse.app.generic_worker import start +from synapse.util.logcontext import LoggingContext if __name__ == "__main__": with LoggingContext("main"): diff --git a/synapse/app/event_creator.py b/synapse/app/event_creator.py index 58e5b354f6..e9c098c4e7 100644 --- a/synapse/app/event_creator.py +++ b/synapse/app/event_creator.py @@ -13,191 +13,11 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import logging + import sys -from twisted.internet import reactor -from twisted.web.resource import NoResource - -import synapse -from synapse import events -from synapse.app import _base -from synapse.config._base import ConfigError -from synapse.config.homeserver import HomeServerConfig -from synapse.config.logger import setup_logging -from synapse.http.server import JsonResource -from synapse.http.site import SynapseSite -from synapse.logging.context import LoggingContext -from synapse.metrics import METRICS_PREFIX, MetricsResource, RegistryProxy -from synapse.replication.slave.storage._base import BaseSlavedStore -from synapse.replication.slave.storage.account_data import SlavedAccountDataStore -from synapse.replication.slave.storage.appservice import SlavedApplicationServiceStore -from synapse.replication.slave.storage.client_ips import SlavedClientIpStore -from synapse.replication.slave.storage.devices import SlavedDeviceStore -from synapse.replication.slave.storage.directory import DirectoryStore -from synapse.replication.slave.storage.events import SlavedEventStore -from synapse.replication.slave.storage.profile import SlavedProfileStore -from synapse.replication.slave.storage.push_rule import SlavedPushRuleStore -from synapse.replication.slave.storage.pushers import SlavedPusherStore -from synapse.replication.slave.storage.receipts import SlavedReceiptsStore -from synapse.replication.slave.storage.registration import SlavedRegistrationStore -from synapse.replication.slave.storage.room import RoomStore -from synapse.replication.slave.storage.transactions import SlavedTransactionStore -from synapse.replication.tcp.client import ReplicationClientHandler -from synapse.rest.client.v1.profile import ( - ProfileAvatarURLRestServlet, - ProfileDisplaynameRestServlet, - ProfileRestServlet, -) -from synapse.rest.client.v1.room import ( - JoinRoomAliasServlet, - RoomMembershipRestServlet, - RoomSendEventRestServlet, - RoomStateEventRestServlet, -) -from synapse.server import HomeServer -from synapse.storage.data_stores.main.monthly_active_users import ( - MonthlyActiveUsersWorkerStore, -) -from synapse.storage.data_stores.main.user_directory import UserDirectoryStore -from synapse.util.httpresourcetree import create_resource_tree -from synapse.util.manhole import manhole -from synapse.util.versionstring import get_version_string - -logger = logging.getLogger("synapse.app.event_creator") - - -class EventCreatorSlavedStore( - # FIXME(#3714): We need to add UserDirectoryStore as we write directly - # rather than going via the correct worker. - UserDirectoryStore, - DirectoryStore, - SlavedTransactionStore, - SlavedProfileStore, - SlavedAccountDataStore, - SlavedPusherStore, - SlavedReceiptsStore, - SlavedPushRuleStore, - SlavedDeviceStore, - SlavedClientIpStore, - SlavedApplicationServiceStore, - SlavedEventStore, - SlavedRegistrationStore, - RoomStore, - MonthlyActiveUsersWorkerStore, - BaseSlavedStore, -): - pass - - -class EventCreatorServer(HomeServer): - DATASTORE_CLASS = EventCreatorSlavedStore - - def _listen_http(self, listener_config): - port = listener_config["port"] - bind_addresses = listener_config["bind_addresses"] - site_tag = listener_config.get("tag", port) - resources = {} - for res in listener_config["resources"]: - for name in res["names"]: - if name == "metrics": - resources[METRICS_PREFIX] = MetricsResource(RegistryProxy) - elif name == "client": - resource = JsonResource(self, canonical_json=False) - RoomSendEventRestServlet(self).register(resource) - RoomMembershipRestServlet(self).register(resource) - RoomStateEventRestServlet(self).register(resource) - JoinRoomAliasServlet(self).register(resource) - ProfileAvatarURLRestServlet(self).register(resource) - ProfileDisplaynameRestServlet(self).register(resource) - ProfileRestServlet(self).register(resource) - resources.update( - { - "/_matrix/client/r0": resource, - "/_matrix/client/unstable": resource, - "/_matrix/client/v2_alpha": resource, - "/_matrix/client/api/v1": resource, - } - ) - - root_resource = create_resource_tree(resources, NoResource()) - - _base.listen_tcp( - bind_addresses, - port, - SynapseSite( - "synapse.access.http.%s" % (site_tag,), - site_tag, - listener_config, - root_resource, - self.version_string, - ), - ) - - logger.info("Synapse event creator now listening on port %d", port) - - def start_listening(self, listeners): - for listener in listeners: - if listener["type"] == "http": - self._listen_http(listener) - elif listener["type"] == "manhole": - _base.listen_tcp( - listener["bind_addresses"], - listener["port"], - manhole( - username="matrix", password="rabbithole", globals={"hs": self} - ), - ) - elif listener["type"] == "metrics": - if not self.get_config().enable_metrics: - logger.warning( - ( - "Metrics listener configured, but " - "enable_metrics is not True!" - ) - ) - else: - _base.listen_metrics(listener["bind_addresses"], listener["port"]) - else: - logger.warning("Unrecognized listener type: %s", listener["type"]) - - self.get_tcp_replication().start_replication(self) - - def build_tcp_replication(self): - return ReplicationClientHandler(self.get_datastore()) - - -def start(config_options): - try: - config = HomeServerConfig.load_config("Synapse event creator", config_options) - except ConfigError as e: - sys.stderr.write("\n" + str(e) + "\n") - sys.exit(1) - - assert config.worker_app == "synapse.app.event_creator" - - assert config.worker_replication_http_port is not None - - # This should only be done on the user directory worker or the master - config.update_user_directory = False - - events.USE_FROZEN_DICTS = config.use_frozen_dicts - - ss = EventCreatorServer( - config.server_name, - config=config, - version_string="Synapse/" + get_version_string(synapse), - ) - - setup_logging(ss, config, use_worker_options=True) - - ss.setup() - reactor.addSystemEventTrigger( - "before", "startup", _base.start, ss, config.worker_listeners - ) - - _base.start_worker_reactor("synapse-event-creator", config) - +from synapse.app.generic_worker import start +from synapse.util.logcontext import LoggingContext if __name__ == "__main__": with LoggingContext("main"): diff --git a/synapse/app/federation_reader.py b/synapse/app/federation_reader.py index d055d11b23..add43147b3 100644 --- a/synapse/app/federation_reader.py +++ b/synapse/app/federation_reader.py @@ -13,177 +13,11 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import logging + import sys -from twisted.internet import reactor -from twisted.web.resource import NoResource - -import synapse -from synapse import events -from synapse.api.urls import FEDERATION_PREFIX, SERVER_KEY_V2_PREFIX -from synapse.app import _base -from synapse.config._base import ConfigError -from synapse.config.homeserver import HomeServerConfig -from synapse.config.logger import setup_logging -from synapse.federation.transport.server import TransportLayerServer -from synapse.http.site import SynapseSite -from synapse.logging.context import LoggingContext -from synapse.metrics import METRICS_PREFIX, MetricsResource, RegistryProxy -from synapse.replication.slave.storage._base import BaseSlavedStore -from synapse.replication.slave.storage.account_data import SlavedAccountDataStore -from synapse.replication.slave.storage.appservice import SlavedApplicationServiceStore -from synapse.replication.slave.storage.devices import SlavedDeviceStore -from synapse.replication.slave.storage.directory import DirectoryStore -from synapse.replication.slave.storage.events import SlavedEventStore -from synapse.replication.slave.storage.groups import SlavedGroupServerStore -from synapse.replication.slave.storage.keys import SlavedKeyStore -from synapse.replication.slave.storage.profile import SlavedProfileStore -from synapse.replication.slave.storage.push_rule import SlavedPushRuleStore -from synapse.replication.slave.storage.pushers import SlavedPusherStore -from synapse.replication.slave.storage.receipts import SlavedReceiptsStore -from synapse.replication.slave.storage.registration import SlavedRegistrationStore -from synapse.replication.slave.storage.room import RoomStore -from synapse.replication.slave.storage.transactions import SlavedTransactionStore -from synapse.replication.tcp.client import ReplicationClientHandler -from synapse.rest.key.v2 import KeyApiV2Resource -from synapse.server import HomeServer -from synapse.storage.data_stores.main.monthly_active_users import ( - MonthlyActiveUsersWorkerStore, -) -from synapse.util.httpresourcetree import create_resource_tree -from synapse.util.manhole import manhole -from synapse.util.versionstring import get_version_string - -logger = logging.getLogger("synapse.app.federation_reader") - - -class FederationReaderSlavedStore( - SlavedAccountDataStore, - SlavedProfileStore, - SlavedApplicationServiceStore, - SlavedPusherStore, - SlavedPushRuleStore, - SlavedReceiptsStore, - SlavedEventStore, - SlavedKeyStore, - SlavedRegistrationStore, - SlavedGroupServerStore, - SlavedDeviceStore, - RoomStore, - DirectoryStore, - SlavedTransactionStore, - MonthlyActiveUsersWorkerStore, - BaseSlavedStore, -): - pass - - -class FederationReaderServer(HomeServer): - DATASTORE_CLASS = FederationReaderSlavedStore - - def _listen_http(self, listener_config): - port = listener_config["port"] - bind_addresses = listener_config["bind_addresses"] - site_tag = listener_config.get("tag", port) - resources = {} - for res in listener_config["resources"]: - for name in res["names"]: - if name == "metrics": - resources[METRICS_PREFIX] = MetricsResource(RegistryProxy) - elif name == "federation": - resources.update({FEDERATION_PREFIX: TransportLayerServer(self)}) - if name == "openid" and "federation" not in res["names"]: - # Only load the openid resource separately if federation resource - # is not specified since federation resource includes openid - # resource. - resources.update( - { - FEDERATION_PREFIX: TransportLayerServer( - self, servlet_groups=["openid"] - ) - } - ) - - if name in ["keys", "federation"]: - resources[SERVER_KEY_V2_PREFIX] = KeyApiV2Resource(self) - - root_resource = create_resource_tree(resources, NoResource()) - - _base.listen_tcp( - bind_addresses, - port, - SynapseSite( - "synapse.access.http.%s" % (site_tag,), - site_tag, - listener_config, - root_resource, - self.version_string, - ), - reactor=self.get_reactor(), - ) - - logger.info("Synapse federation reader now listening on port %d", port) - - def start_listening(self, listeners): - for listener in listeners: - if listener["type"] == "http": - self._listen_http(listener) - elif listener["type"] == "manhole": - _base.listen_tcp( - listener["bind_addresses"], - listener["port"], - manhole( - username="matrix", password="rabbithole", globals={"hs": self} - ), - ) - elif listener["type"] == "metrics": - if not self.get_config().enable_metrics: - logger.warning( - ( - "Metrics listener configured, but " - "enable_metrics is not True!" - ) - ) - else: - _base.listen_metrics(listener["bind_addresses"], listener["port"]) - else: - logger.warning("Unrecognized listener type: %s", listener["type"]) - - self.get_tcp_replication().start_replication(self) - - def build_tcp_replication(self): - return ReplicationClientHandler(self.get_datastore()) - - -def start(config_options): - try: - config = HomeServerConfig.load_config( - "Synapse federation reader", config_options - ) - except ConfigError as e: - sys.stderr.write("\n" + str(e) + "\n") - sys.exit(1) - - assert config.worker_app == "synapse.app.federation_reader" - - events.USE_FROZEN_DICTS = config.use_frozen_dicts - - ss = FederationReaderServer( - config.server_name, - config=config, - version_string="Synapse/" + get_version_string(synapse), - ) - - setup_logging(ss, config, use_worker_options=True) - - ss.setup() - reactor.addSystemEventTrigger( - "before", "startup", _base.start, ss, config.worker_listeners - ) - - _base.start_worker_reactor("synapse-federation-reader", config) - +from synapse.app.generic_worker import start +from synapse.util.logcontext import LoggingContext if __name__ == "__main__": with LoggingContext("main"): diff --git a/synapse/app/federation_sender.py b/synapse/app/federation_sender.py index b7fcf80ddc..add43147b3 100644 --- a/synapse/app/federation_sender.py +++ b/synapse/app/federation_sender.py @@ -13,308 +13,11 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import logging + import sys -from twisted.internet import defer, reactor -from twisted.web.resource import NoResource - -import synapse -from synapse import events -from synapse.app import _base -from synapse.config._base import ConfigError -from synapse.config.homeserver import HomeServerConfig -from synapse.config.logger import setup_logging -from synapse.federation import send_queue -from synapse.http.site import SynapseSite -from synapse.logging.context import LoggingContext, run_in_background -from synapse.metrics import METRICS_PREFIX, MetricsResource, RegistryProxy -from synapse.metrics.background_process_metrics import run_as_background_process -from synapse.replication.slave.storage.deviceinbox import SlavedDeviceInboxStore -from synapse.replication.slave.storage.devices import SlavedDeviceStore -from synapse.replication.slave.storage.events import SlavedEventStore -from synapse.replication.slave.storage.presence import SlavedPresenceStore -from synapse.replication.slave.storage.receipts import SlavedReceiptsStore -from synapse.replication.slave.storage.registration import SlavedRegistrationStore -from synapse.replication.slave.storage.transactions import SlavedTransactionStore -from synapse.replication.tcp.client import ReplicationClientHandler -from synapse.replication.tcp.streams._base import ( - DeviceListsStream, - ReceiptsStream, - ToDeviceStream, -) -from synapse.server import HomeServer -from synapse.storage.database import Database -from synapse.types import ReadReceipt -from synapse.util.async_helpers import Linearizer -from synapse.util.httpresourcetree import create_resource_tree -from synapse.util.manhole import manhole -from synapse.util.versionstring import get_version_string - -logger = logging.getLogger("synapse.app.federation_sender") - - -class FederationSenderSlaveStore( - SlavedDeviceInboxStore, - SlavedTransactionStore, - SlavedReceiptsStore, - SlavedEventStore, - SlavedRegistrationStore, - SlavedDeviceStore, - SlavedPresenceStore, -): - def __init__(self, database: Database, db_conn, hs): - super(FederationSenderSlaveStore, self).__init__(database, db_conn, hs) - - # We pull out the current federation stream position now so that we - # always have a known value for the federation position in memory so - # that we don't have to bounce via a deferred once when we start the - # replication streams. - self.federation_out_pos_startup = self._get_federation_out_pos(db_conn) - - def _get_federation_out_pos(self, db_conn): - sql = "SELECT stream_id FROM federation_stream_position WHERE type = ?" - sql = self.database_engine.convert_param_style(sql) - - txn = db_conn.cursor() - txn.execute(sql, ("federation",)) - rows = txn.fetchall() - txn.close() - - return rows[0][0] if rows else -1 - - -class FederationSenderServer(HomeServer): - DATASTORE_CLASS = FederationSenderSlaveStore - - def _listen_http(self, listener_config): - port = listener_config["port"] - bind_addresses = listener_config["bind_addresses"] - site_tag = listener_config.get("tag", port) - resources = {} - for res in listener_config["resources"]: - for name in res["names"]: - if name == "metrics": - resources[METRICS_PREFIX] = MetricsResource(RegistryProxy) - - root_resource = create_resource_tree(resources, NoResource()) - - _base.listen_tcp( - bind_addresses, - port, - SynapseSite( - "synapse.access.http.%s" % (site_tag,), - site_tag, - listener_config, - root_resource, - self.version_string, - ), - ) - - logger.info("Synapse federation_sender now listening on port %d", port) - - def start_listening(self, listeners): - for listener in listeners: - if listener["type"] == "http": - self._listen_http(listener) - elif listener["type"] == "manhole": - _base.listen_tcp( - listener["bind_addresses"], - listener["port"], - manhole( - username="matrix", password="rabbithole", globals={"hs": self} - ), - ) - elif listener["type"] == "metrics": - if not self.get_config().enable_metrics: - logger.warning( - ( - "Metrics listener configured, but " - "enable_metrics is not True!" - ) - ) - else: - _base.listen_metrics(listener["bind_addresses"], listener["port"]) - else: - logger.warning("Unrecognized listener type: %s", listener["type"]) - - self.get_tcp_replication().start_replication(self) - - def build_tcp_replication(self): - return FederationSenderReplicationHandler(self) - - -class FederationSenderReplicationHandler(ReplicationClientHandler): - def __init__(self, hs): - super(FederationSenderReplicationHandler, self).__init__(hs.get_datastore()) - self.send_handler = FederationSenderHandler(hs, self) - - async def on_rdata(self, stream_name, token, rows): - await super(FederationSenderReplicationHandler, self).on_rdata( - stream_name, token, rows - ) - self.send_handler.process_replication_rows(stream_name, token, rows) - - def get_streams_to_replicate(self): - args = super( - FederationSenderReplicationHandler, self - ).get_streams_to_replicate() - args.update(self.send_handler.stream_positions()) - return args - - def on_remote_server_up(self, server: str): - """Called when get a new REMOTE_SERVER_UP command.""" - - # Let's wake up the transaction queue for the server in case we have - # pending stuff to send to it. - self.send_handler.wake_destination(server) - - -def start(config_options): - try: - config = HomeServerConfig.load_config( - "Synapse federation sender", config_options - ) - except ConfigError as e: - sys.stderr.write("\n" + str(e) + "\n") - sys.exit(1) - - assert config.worker_app == "synapse.app.federation_sender" - - events.USE_FROZEN_DICTS = config.use_frozen_dicts - - if config.send_federation: - sys.stderr.write( - "\nThe send_federation must be disabled in the main synapse process" - "\nbefore they can be run in a separate worker." - "\nPlease add ``send_federation: false`` to the main config" - "\n" - ) - sys.exit(1) - - # Force the pushers to start since they will be disabled in the main config - config.send_federation = True - - ss = FederationSenderServer( - config.server_name, - config=config, - version_string="Synapse/" + get_version_string(synapse), - ) - - setup_logging(ss, config, use_worker_options=True) - - ss.setup() - reactor.addSystemEventTrigger( - "before", "startup", _base.start, ss, config.worker_listeners - ) - - _base.start_worker_reactor("synapse-federation-sender", config) - - -class FederationSenderHandler(object): - """Processes the replication stream and forwards the appropriate entries - to the federation sender. - """ - - def __init__(self, hs: FederationSenderServer, replication_client): - self.store = hs.get_datastore() - self._is_mine_id = hs.is_mine_id - self.federation_sender = hs.get_federation_sender() - self.replication_client = replication_client - - self.federation_position = self.store.federation_out_pos_startup - self._fed_position_linearizer = Linearizer(name="_fed_position_linearizer") - - self._last_ack = self.federation_position - - self._room_serials = {} - self._room_typing = {} - - def on_start(self): - # There may be some events that are persisted but haven't been sent, - # so send them now. - self.federation_sender.notify_new_events( - self.store.get_room_max_stream_ordering() - ) - - def wake_destination(self, server: str): - self.federation_sender.wake_destination(server) - - def stream_positions(self): - return {"federation": self.federation_position} - - def process_replication_rows(self, stream_name, token, rows): - # The federation stream contains things that we want to send out, e.g. - # presence, typing, etc. - if stream_name == "federation": - send_queue.process_rows_for_federation(self.federation_sender, rows) - run_in_background(self.update_token, token) - - # We also need to poke the federation sender when new events happen - elif stream_name == "events": - self.federation_sender.notify_new_events(token) - - # ... and when new receipts happen - elif stream_name == ReceiptsStream.NAME: - run_as_background_process( - "process_receipts_for_federation", self._on_new_receipts, rows - ) - - # ... as well as device updates and messages - elif stream_name == DeviceListsStream.NAME: - hosts = {row.destination for row in rows} - for host in hosts: - self.federation_sender.send_device_messages(host) - - elif stream_name == ToDeviceStream.NAME: - # The to_device stream includes stuff to be pushed to both local - # clients and remote servers, so we ignore entities that start with - # '@' (since they'll be local users rather than destinations). - hosts = {row.entity for row in rows if not row.entity.startswith("@")} - for host in hosts: - self.federation_sender.send_device_messages(host) - - @defer.inlineCallbacks - def _on_new_receipts(self, rows): - """ - Args: - rows (iterable[synapse.replication.tcp.streams.ReceiptsStreamRow]): - new receipts to be processed - """ - for receipt in rows: - # we only want to send on receipts for our own users - if not self._is_mine_id(receipt.user_id): - continue - receipt_info = ReadReceipt( - receipt.room_id, - receipt.receipt_type, - receipt.user_id, - [receipt.event_id], - receipt.data, - ) - yield self.federation_sender.send_read_receipt(receipt_info) - - @defer.inlineCallbacks - def update_token(self, token): - try: - self.federation_position = token - - # We linearize here to ensure we don't have races updating the token - with (yield self._fed_position_linearizer.queue(None)): - if self._last_ack < self.federation_position: - yield self.store.update_federation_out_pos( - "federation", self.federation_position - ) - - # We ACK this token over replication so that the master can drop - # its in memory queues - self.replication_client.send_federation_ack( - self.federation_position - ) - self._last_ack = self.federation_position - except Exception: - logger.exception("Error updating federation stream position") - +from synapse.app.generic_worker import start +from synapse.util.logcontext import LoggingContext if __name__ == "__main__": with LoggingContext("main"): diff --git a/synapse/app/frontend_proxy.py b/synapse/app/frontend_proxy.py index 30e435eead..add43147b3 100644 --- a/synapse/app/frontend_proxy.py +++ b/synapse/app/frontend_proxy.py @@ -13,241 +13,11 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import logging + import sys -from twisted.internet import defer, reactor -from twisted.web.resource import NoResource - -import synapse -from synapse import events -from synapse.api.errors import HttpResponseException, SynapseError -from synapse.app import _base -from synapse.config._base import ConfigError -from synapse.config.homeserver import HomeServerConfig -from synapse.config.logger import setup_logging -from synapse.http.server import JsonResource -from synapse.http.servlet import RestServlet, parse_json_object_from_request -from synapse.http.site import SynapseSite -from synapse.logging.context import LoggingContext -from synapse.metrics import METRICS_PREFIX, MetricsResource, RegistryProxy -from synapse.replication.slave.storage._base import BaseSlavedStore -from synapse.replication.slave.storage.appservice import SlavedApplicationServiceStore -from synapse.replication.slave.storage.client_ips import SlavedClientIpStore -from synapse.replication.slave.storage.devices import SlavedDeviceStore -from synapse.replication.slave.storage.registration import SlavedRegistrationStore -from synapse.replication.tcp.client import ReplicationClientHandler -from synapse.rest.client.v2_alpha._base import client_patterns -from synapse.server import HomeServer -from synapse.util.httpresourcetree import create_resource_tree -from synapse.util.manhole import manhole -from synapse.util.versionstring import get_version_string - -logger = logging.getLogger("synapse.app.frontend_proxy") - - -class PresenceStatusStubServlet(RestServlet): - PATTERNS = client_patterns("/presence/(?P[^/]*)/status") - - def __init__(self, hs): - super(PresenceStatusStubServlet, self).__init__() - self.http_client = hs.get_simple_http_client() - self.auth = hs.get_auth() - self.main_uri = hs.config.worker_main_http_uri - - @defer.inlineCallbacks - def on_GET(self, request, user_id): - # Pass through the auth headers, if any, in case the access token - # is there. - auth_headers = request.requestHeaders.getRawHeaders("Authorization", []) - headers = {"Authorization": auth_headers} - - try: - result = yield self.http_client.get_json( - self.main_uri + request.uri.decode("ascii"), headers=headers - ) - except HttpResponseException as e: - raise e.to_synapse_error() - - return 200, result - - @defer.inlineCallbacks - def on_PUT(self, request, user_id): - yield self.auth.get_user_by_req(request) - return 200, {} - - -class KeyUploadServlet(RestServlet): - PATTERNS = client_patterns("/keys/upload(/(?P[^/]+))?$") - - def __init__(self, hs): - """ - Args: - hs (synapse.server.HomeServer): server - """ - super(KeyUploadServlet, self).__init__() - self.auth = hs.get_auth() - self.store = hs.get_datastore() - self.http_client = hs.get_simple_http_client() - self.main_uri = hs.config.worker_main_http_uri - - @defer.inlineCallbacks - def on_POST(self, request, device_id): - requester = yield self.auth.get_user_by_req(request, allow_guest=True) - user_id = requester.user.to_string() - body = parse_json_object_from_request(request) - - if device_id is not None: - # passing the device_id here is deprecated; however, we allow it - # for now for compatibility with older clients. - if requester.device_id is not None and device_id != requester.device_id: - logger.warning( - "Client uploading keys for a different device " - "(logged in as %s, uploading for %s)", - requester.device_id, - device_id, - ) - else: - device_id = requester.device_id - - if device_id is None: - raise SynapseError( - 400, "To upload keys, you must pass device_id when authenticating" - ) - - if body: - # They're actually trying to upload something, proxy to main synapse. - # Pass through the auth headers, if any, in case the access token - # is there. - auth_headers = request.requestHeaders.getRawHeaders(b"Authorization", []) - headers = {"Authorization": auth_headers} - result = yield self.http_client.post_json_get_json( - self.main_uri + request.uri.decode("ascii"), body, headers=headers - ) - - return 200, result - else: - # Just interested in counts. - result = yield self.store.count_e2e_one_time_keys(user_id, device_id) - return 200, {"one_time_key_counts": result} - - -class FrontendProxySlavedStore( - SlavedDeviceStore, - SlavedClientIpStore, - SlavedApplicationServiceStore, - SlavedRegistrationStore, - BaseSlavedStore, -): - pass - - -class FrontendProxyServer(HomeServer): - DATASTORE_CLASS = FrontendProxySlavedStore - - def _listen_http(self, listener_config): - port = listener_config["port"] - bind_addresses = listener_config["bind_addresses"] - site_tag = listener_config.get("tag", port) - resources = {} - for res in listener_config["resources"]: - for name in res["names"]: - if name == "metrics": - resources[METRICS_PREFIX] = MetricsResource(RegistryProxy) - elif name == "client": - resource = JsonResource(self, canonical_json=False) - KeyUploadServlet(self).register(resource) - - # If presence is disabled, use the stub servlet that does - # not allow sending presence - if not self.config.use_presence: - PresenceStatusStubServlet(self).register(resource) - - resources.update( - { - "/_matrix/client/r0": resource, - "/_matrix/client/unstable": resource, - "/_matrix/client/v2_alpha": resource, - "/_matrix/client/api/v1": resource, - } - ) - - root_resource = create_resource_tree(resources, NoResource()) - - _base.listen_tcp( - bind_addresses, - port, - SynapseSite( - "synapse.access.http.%s" % (site_tag,), - site_tag, - listener_config, - root_resource, - self.version_string, - ), - reactor=self.get_reactor(), - ) - - logger.info("Synapse client reader now listening on port %d", port) - - def start_listening(self, listeners): - for listener in listeners: - if listener["type"] == "http": - self._listen_http(listener) - elif listener["type"] == "manhole": - _base.listen_tcp( - listener["bind_addresses"], - listener["port"], - manhole( - username="matrix", password="rabbithole", globals={"hs": self} - ), - ) - elif listener["type"] == "metrics": - if not self.get_config().enable_metrics: - logger.warning( - ( - "Metrics listener configured, but " - "enable_metrics is not True!" - ) - ) - else: - _base.listen_metrics(listener["bind_addresses"], listener["port"]) - else: - logger.warning("Unrecognized listener type: %s", listener["type"]) - - self.get_tcp_replication().start_replication(self) - - def build_tcp_replication(self): - return ReplicationClientHandler(self.get_datastore()) - - -def start(config_options): - try: - config = HomeServerConfig.load_config("Synapse frontend proxy", config_options) - except ConfigError as e: - sys.stderr.write("\n" + str(e) + "\n") - sys.exit(1) - - assert config.worker_app == "synapse.app.frontend_proxy" - - assert config.worker_main_http_uri is not None - - events.USE_FROZEN_DICTS = config.use_frozen_dicts - - ss = FrontendProxyServer( - config.server_name, - config=config, - version_string="Synapse/" + get_version_string(synapse), - ) - - setup_logging(ss, config, use_worker_options=True) - - ss.setup() - reactor.addSystemEventTrigger( - "before", "startup", _base.start, ss, config.worker_listeners - ) - - _base.start_worker_reactor("synapse-frontend-proxy", config) - +from synapse.app.generic_worker import start +from synapse.util.logcontext import LoggingContext if __name__ == "__main__": with LoggingContext("main"): diff --git a/synapse/app/generic_worker.py b/synapse/app/generic_worker.py new file mode 100644 index 0000000000..30efd39092 --- /dev/null +++ b/synapse/app/generic_worker.py @@ -0,0 +1,917 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright 2016 OpenMarket Ltd +# Copyright 2020 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import contextlib +import logging +import sys + +from twisted.internet import defer, reactor +from twisted.web.resource import NoResource + +import synapse +import synapse.events +from synapse.api.constants import EventTypes +from synapse.api.errors import HttpResponseException, SynapseError +from synapse.api.urls import ( + CLIENT_API_PREFIX, + FEDERATION_PREFIX, + LEGACY_MEDIA_PREFIX, + MEDIA_PREFIX, + SERVER_KEY_V2_PREFIX, +) +from synapse.app import _base +from synapse.config._base import ConfigError +from synapse.config.homeserver import HomeServerConfig +from synapse.config.logger import setup_logging +from synapse.federation import send_queue +from synapse.federation.transport.server import TransportLayerServer +from synapse.handlers.presence import PresenceHandler, get_interested_parties +from synapse.http.server import JsonResource +from synapse.http.servlet import RestServlet, parse_json_object_from_request +from synapse.http.site import SynapseSite +from synapse.logging.context import LoggingContext, run_in_background +from synapse.metrics import METRICS_PREFIX, MetricsResource, RegistryProxy +from synapse.metrics.background_process_metrics import run_as_background_process +from synapse.replication.slave.storage._base import BaseSlavedStore, __func__ +from synapse.replication.slave.storage.account_data import SlavedAccountDataStore +from synapse.replication.slave.storage.appservice import SlavedApplicationServiceStore +from synapse.replication.slave.storage.client_ips import SlavedClientIpStore +from synapse.replication.slave.storage.deviceinbox import SlavedDeviceInboxStore +from synapse.replication.slave.storage.devices import SlavedDeviceStore +from synapse.replication.slave.storage.directory import DirectoryStore +from synapse.replication.slave.storage.events import SlavedEventStore +from synapse.replication.slave.storage.filtering import SlavedFilteringStore +from synapse.replication.slave.storage.groups import SlavedGroupServerStore +from synapse.replication.slave.storage.keys import SlavedKeyStore +from synapse.replication.slave.storage.presence import SlavedPresenceStore +from synapse.replication.slave.storage.profile import SlavedProfileStore +from synapse.replication.slave.storage.push_rule import SlavedPushRuleStore +from synapse.replication.slave.storage.pushers import SlavedPusherStore +from synapse.replication.slave.storage.receipts import SlavedReceiptsStore +from synapse.replication.slave.storage.registration import SlavedRegistrationStore +from synapse.replication.slave.storage.room import RoomStore +from synapse.replication.slave.storage.transactions import SlavedTransactionStore +from synapse.replication.tcp.client import ReplicationClientHandler +from synapse.replication.tcp.streams._base import ( + DeviceListsStream, + ReceiptsStream, + ToDeviceStream, +) +from synapse.replication.tcp.streams.events import EventsStreamEventRow, EventsStreamRow +from synapse.rest.admin import register_servlets_for_media_repo +from synapse.rest.client.v1 import events +from synapse.rest.client.v1.initial_sync import InitialSyncRestServlet +from synapse.rest.client.v1.login import LoginRestServlet +from synapse.rest.client.v1.profile import ( + ProfileAvatarURLRestServlet, + ProfileDisplaynameRestServlet, + ProfileRestServlet, +) +from synapse.rest.client.v1.push_rule import PushRuleRestServlet +from synapse.rest.client.v1.room import ( + JoinedRoomMemberListRestServlet, + JoinRoomAliasServlet, + PublicRoomListRestServlet, + RoomEventContextServlet, + RoomInitialSyncRestServlet, + RoomMemberListRestServlet, + RoomMembershipRestServlet, + RoomMessageListRestServlet, + RoomSendEventRestServlet, + RoomStateEventRestServlet, + RoomStateRestServlet, +) +from synapse.rest.client.v1.voip import VoipRestServlet +from synapse.rest.client.v2_alpha import groups, sync, user_directory +from synapse.rest.client.v2_alpha._base import client_patterns +from synapse.rest.client.v2_alpha.account import ThreepidRestServlet +from synapse.rest.client.v2_alpha.keys import KeyChangesServlet, KeyQueryServlet +from synapse.rest.client.v2_alpha.register import RegisterRestServlet +from synapse.rest.client.versions import VersionsRestServlet +from synapse.rest.key.v2 import KeyApiV2Resource +from synapse.server import HomeServer +from synapse.storage.data_stores.main.media_repository import MediaRepositoryStore +from synapse.storage.data_stores.main.monthly_active_users import ( + MonthlyActiveUsersWorkerStore, +) +from synapse.storage.data_stores.main.presence import UserPresenceState +from synapse.storage.data_stores.main.user_directory import UserDirectoryStore +from synapse.types import ReadReceipt +from synapse.util.async_helpers import Linearizer +from synapse.util.httpresourcetree import create_resource_tree +from synapse.util.manhole import manhole +from synapse.util.stringutils import random_string +from synapse.util.versionstring import get_version_string + +logger = logging.getLogger("synapse.app.generic_worker") + + +class PresenceStatusStubServlet(RestServlet): + """If presence is disabled this servlet can be used to stub out setting + presence status, while proxying the getters to the master instance. + """ + + PATTERNS = client_patterns("/presence/(?P[^/]*)/status") + + def __init__(self, hs): + super(PresenceStatusStubServlet, self).__init__() + self.http_client = hs.get_simple_http_client() + self.auth = hs.get_auth() + self.main_uri = hs.config.worker_main_http_uri + + async def on_GET(self, request, user_id): + # Pass through the auth headers, if any, in case the access token + # is there. + auth_headers = request.requestHeaders.getRawHeaders("Authorization", []) + headers = {"Authorization": auth_headers} + + try: + result = await self.http_client.get_json( + self.main_uri + request.uri.decode("ascii"), headers=headers + ) + except HttpResponseException as e: + raise e.to_synapse_error() + + return 200, result + + async def on_PUT(self, request, user_id): + await self.auth.get_user_by_req(request) + return 200, {} + + +class KeyUploadServlet(RestServlet): + """An implementation of the `KeyUploadServlet` that responds to read only + requests, but otherwise proxies through to the master instance. + """ + + PATTERNS = client_patterns("/keys/upload(/(?P[^/]+))?$") + + def __init__(self, hs): + """ + Args: + hs (synapse.server.HomeServer): server + """ + super(KeyUploadServlet, self).__init__() + self.auth = hs.get_auth() + self.store = hs.get_datastore() + self.http_client = hs.get_simple_http_client() + self.main_uri = hs.config.worker_main_http_uri + + async def on_POST(self, request, device_id): + requester = await self.auth.get_user_by_req(request, allow_guest=True) + user_id = requester.user.to_string() + body = parse_json_object_from_request(request) + + if device_id is not None: + # passing the device_id here is deprecated; however, we allow it + # for now for compatibility with older clients. + if requester.device_id is not None and device_id != requester.device_id: + logger.warning( + "Client uploading keys for a different device " + "(logged in as %s, uploading for %s)", + requester.device_id, + device_id, + ) + else: + device_id = requester.device_id + + if device_id is None: + raise SynapseError( + 400, "To upload keys, you must pass device_id when authenticating" + ) + + if body: + # They're actually trying to upload something, proxy to main synapse. + # Pass through the auth headers, if any, in case the access token + # is there. + auth_headers = request.requestHeaders.getRawHeaders(b"Authorization", []) + headers = {"Authorization": auth_headers} + result = await self.http_client.post_json_get_json( + self.main_uri + request.uri.decode("ascii"), body, headers=headers + ) + + return 200, result + else: + # Just interested in counts. + result = await self.store.count_e2e_one_time_keys(user_id, device_id) + return 200, {"one_time_key_counts": result} + + +UPDATE_SYNCING_USERS_MS = 10 * 1000 + + +class GenericWorkerPresence(object): + def __init__(self, hs): + self.hs = hs + self.is_mine_id = hs.is_mine_id + self.http_client = hs.get_simple_http_client() + self.store = hs.get_datastore() + self.user_to_num_current_syncs = {} + self.clock = hs.get_clock() + self.notifier = hs.get_notifier() + + active_presence = self.store.take_presence_startup_info() + self.user_to_current_state = {state.user_id: state for state in active_presence} + + # user_id -> last_sync_ms. Lists the users that have stopped syncing + # but we haven't notified the master of that yet + self.users_going_offline = {} + + self._send_stop_syncing_loop = self.clock.looping_call( + self.send_stop_syncing, UPDATE_SYNCING_USERS_MS + ) + + self.process_id = random_string(16) + logger.info("Presence process_id is %r", self.process_id) + + def send_user_sync(self, user_id, is_syncing, last_sync_ms): + if self.hs.config.use_presence: + self.hs.get_tcp_replication().send_user_sync( + user_id, is_syncing, last_sync_ms + ) + + def mark_as_coming_online(self, user_id): + """A user has started syncing. Send a UserSync to the master, unless they + had recently stopped syncing. + + Args: + user_id (str) + """ + going_offline = self.users_going_offline.pop(user_id, None) + if not going_offline: + # Safe to skip because we haven't yet told the master they were offline + self.send_user_sync(user_id, True, self.clock.time_msec()) + + def mark_as_going_offline(self, user_id): + """A user has stopped syncing. We wait before notifying the master as + its likely they'll come back soon. This allows us to avoid sending + a stopped syncing immediately followed by a started syncing notification + to the master + + Args: + user_id (str) + """ + self.users_going_offline[user_id] = self.clock.time_msec() + + def send_stop_syncing(self): + """Check if there are any users who have stopped syncing a while ago + and haven't come back yet. If there are poke the master about them. + """ + now = self.clock.time_msec() + for user_id, last_sync_ms in list(self.users_going_offline.items()): + if now - last_sync_ms > UPDATE_SYNCING_USERS_MS: + self.users_going_offline.pop(user_id, None) + self.send_user_sync(user_id, False, last_sync_ms) + + def set_state(self, user, state, ignore_status_msg=False): + # TODO Hows this supposed to work? + return defer.succeed(None) + + get_states = __func__(PresenceHandler.get_states) + get_state = __func__(PresenceHandler.get_state) + current_state_for_users = __func__(PresenceHandler.current_state_for_users) + + def user_syncing(self, user_id, affect_presence): + if affect_presence: + curr_sync = self.user_to_num_current_syncs.get(user_id, 0) + self.user_to_num_current_syncs[user_id] = curr_sync + 1 + + # If we went from no in flight sync to some, notify replication + if self.user_to_num_current_syncs[user_id] == 1: + self.mark_as_coming_online(user_id) + + def _end(): + # We check that the user_id is in user_to_num_current_syncs because + # user_to_num_current_syncs may have been cleared if we are + # shutting down. + if affect_presence and user_id in self.user_to_num_current_syncs: + self.user_to_num_current_syncs[user_id] -= 1 + + # If we went from one in flight sync to non, notify replication + if self.user_to_num_current_syncs[user_id] == 0: + self.mark_as_going_offline(user_id) + + @contextlib.contextmanager + def _user_syncing(): + try: + yield + finally: + _end() + + return defer.succeed(_user_syncing()) + + @defer.inlineCallbacks + def notify_from_replication(self, states, stream_id): + parties = yield get_interested_parties(self.store, states) + room_ids_to_states, users_to_states = parties + + self.notifier.on_new_event( + "presence_key", + stream_id, + rooms=room_ids_to_states.keys(), + users=users_to_states.keys(), + ) + + @defer.inlineCallbacks + def process_replication_rows(self, token, rows): + states = [ + UserPresenceState( + row.user_id, + row.state, + row.last_active_ts, + row.last_federation_update_ts, + row.last_user_sync_ts, + row.status_msg, + row.currently_active, + ) + for row in rows + ] + + for state in states: + self.user_to_current_state[state.user_id] = state + + stream_id = token + yield self.notify_from_replication(states, stream_id) + + def get_currently_syncing_users(self): + if self.hs.config.use_presence: + return [ + user_id + for user_id, count in self.user_to_num_current_syncs.items() + if count > 0 + ] + else: + return set() + + +class GenericWorkerTyping(object): + def __init__(self, hs): + self._latest_room_serial = 0 + self._reset() + + def _reset(self): + """ + Reset the typing handler's data caches. + """ + # map room IDs to serial numbers + self._room_serials = {} + # map room IDs to sets of users currently typing + self._room_typing = {} + + def stream_positions(self): + # We must update this typing token from the response of the previous + # sync. In particular, the stream id may "reset" back to zero/a low + # value which we *must* use for the next replication request. + return {"typing": self._latest_room_serial} + + def process_replication_rows(self, token, rows): + if self._latest_room_serial > token: + # The master has gone backwards. To prevent inconsistent data, just + # clear everything. + self._reset() + + # Set the latest serial token to whatever the server gave us. + self._latest_room_serial = token + + for row in rows: + self._room_serials[row.room_id] = token + self._room_typing[row.room_id] = row.user_ids + + +class GenericWorkerSlavedStore( + # FIXME(#3714): We need to add UserDirectoryStore as we write directly + # rather than going via the correct worker. + UserDirectoryStore, + SlavedDeviceInboxStore, + SlavedDeviceStore, + SlavedReceiptsStore, + SlavedPushRuleStore, + SlavedGroupServerStore, + SlavedAccountDataStore, + SlavedPusherStore, + SlavedEventStore, + SlavedKeyStore, + RoomStore, + DirectoryStore, + SlavedApplicationServiceStore, + SlavedRegistrationStore, + SlavedTransactionStore, + SlavedProfileStore, + SlavedClientIpStore, + SlavedPresenceStore, + SlavedFilteringStore, + MonthlyActiveUsersWorkerStore, + MediaRepositoryStore, + BaseSlavedStore, +): + def __init__(self, database, db_conn, hs): + super(GenericWorkerSlavedStore, self).__init__(database, db_conn, hs) + + # We pull out the current federation stream position now so that we + # always have a known value for the federation position in memory so + # that we don't have to bounce via a deferred once when we start the + # replication streams. + self.federation_out_pos_startup = self._get_federation_out_pos(db_conn) + + def _get_federation_out_pos(self, db_conn): + sql = "SELECT stream_id FROM federation_stream_position WHERE type = ?" + sql = self.database_engine.convert_param_style(sql) + + txn = db_conn.cursor() + txn.execute(sql, ("federation",)) + rows = txn.fetchall() + txn.close() + + return rows[0][0] if rows else -1 + + +class GenericWorkerServer(HomeServer): + DATASTORE_CLASS = GenericWorkerSlavedStore + + def _listen_http(self, listener_config): + port = listener_config["port"] + bind_addresses = listener_config["bind_addresses"] + site_tag = listener_config.get("tag", port) + resources = {} + for res in listener_config["resources"]: + for name in res["names"]: + if name == "metrics": + resources[METRICS_PREFIX] = MetricsResource(RegistryProxy) + elif name == "client": + resource = JsonResource(self, canonical_json=False) + + PublicRoomListRestServlet(self).register(resource) + RoomMemberListRestServlet(self).register(resource) + JoinedRoomMemberListRestServlet(self).register(resource) + RoomStateRestServlet(self).register(resource) + RoomEventContextServlet(self).register(resource) + RoomMessageListRestServlet(self).register(resource) + RegisterRestServlet(self).register(resource) + LoginRestServlet(self).register(resource) + ThreepidRestServlet(self).register(resource) + KeyQueryServlet(self).register(resource) + KeyChangesServlet(self).register(resource) + VoipRestServlet(self).register(resource) + PushRuleRestServlet(self).register(resource) + VersionsRestServlet(self).register(resource) + RoomSendEventRestServlet(self).register(resource) + RoomMembershipRestServlet(self).register(resource) + RoomStateEventRestServlet(self).register(resource) + JoinRoomAliasServlet(self).register(resource) + ProfileAvatarURLRestServlet(self).register(resource) + ProfileDisplaynameRestServlet(self).register(resource) + ProfileRestServlet(self).register(resource) + KeyUploadServlet(self).register(resource) + + sync.register_servlets(self, resource) + events.register_servlets(self, resource) + InitialSyncRestServlet(self).register(resource) + RoomInitialSyncRestServlet(self).register(resource) + + user_directory.register_servlets(self, resource) + + # If presence is disabled, use the stub servlet that does + # not allow sending presence + if not self.config.use_presence: + PresenceStatusStubServlet(self).register(resource) + + groups.register_servlets(self, resource) + + resources.update({CLIENT_API_PREFIX: resource}) + elif name == "federation": + resources.update({FEDERATION_PREFIX: TransportLayerServer(self)}) + elif name == "media": + media_repo = self.get_media_repository_resource() + + # We need to serve the admin servlets for media on the + # worker. + admin_resource = JsonResource(self, canonical_json=False) + register_servlets_for_media_repo(self, admin_resource) + + resources.update( + { + MEDIA_PREFIX: media_repo, + LEGACY_MEDIA_PREFIX: media_repo, + "/_synapse/admin": admin_resource, + } + ) + + if name == "openid" and "federation" not in res["names"]: + # Only load the openid resource separately if federation resource + # is not specified since federation resource includes openid + # resource. + resources.update( + { + FEDERATION_PREFIX: TransportLayerServer( + self, servlet_groups=["openid"] + ) + } + ) + + if name in ["keys", "federation"]: + resources[SERVER_KEY_V2_PREFIX] = KeyApiV2Resource(self) + + root_resource = create_resource_tree(resources, NoResource()) + + _base.listen_tcp( + bind_addresses, + port, + SynapseSite( + "synapse.access.http.%s" % (site_tag,), + site_tag, + listener_config, + root_resource, + self.version_string, + ), + reactor=self.get_reactor(), + ) + + logger.info("Synapse worker now listening on port %d", port) + + def start_listening(self, listeners): + for listener in listeners: + if listener["type"] == "http": + self._listen_http(listener) + elif listener["type"] == "manhole": + _base.listen_tcp( + listener["bind_addresses"], + listener["port"], + manhole( + username="matrix", password="rabbithole", globals={"hs": self} + ), + ) + elif listener["type"] == "metrics": + if not self.get_config().enable_metrics: + logger.warning( + ( + "Metrics listener configured, but " + "enable_metrics is not True!" + ) + ) + else: + _base.listen_metrics(listener["bind_addresses"], listener["port"]) + else: + logger.warning("Unrecognized listener type: %s", listener["type"]) + + self.get_tcp_replication().start_replication(self) + + def remove_pusher(self, app_id, push_key, user_id): + self.get_tcp_replication().send_remove_pusher(app_id, push_key, user_id) + + def build_tcp_replication(self): + return GenericWorkerReplicationHandler(self) + + def build_presence_handler(self): + return GenericWorkerPresence(self) + + def build_typing_handler(self): + return GenericWorkerTyping(self) + + +class GenericWorkerReplicationHandler(ReplicationClientHandler): + def __init__(self, hs): + super(GenericWorkerReplicationHandler, self).__init__(hs.get_datastore()) + + self.store = hs.get_datastore() + self.typing_handler = hs.get_typing_handler() + # NB this is a SynchrotronPresence, not a normal PresenceHandler + self.presence_handler = hs.get_presence_handler() + self.notifier = hs.get_notifier() + + self.notify_pushers = hs.config.start_pushers + self.pusher_pool = hs.get_pusherpool() + + if hs.config.send_federation: + self.send_handler = FederationSenderHandler(hs, self) + else: + self.send_handler = None + + async def on_rdata(self, stream_name, token, rows): + await super(GenericWorkerReplicationHandler, self).on_rdata( + stream_name, token, rows + ) + run_in_background(self.process_and_notify, stream_name, token, rows) + + def get_streams_to_replicate(self): + args = super(GenericWorkerReplicationHandler, self).get_streams_to_replicate() + args.update(self.typing_handler.stream_positions()) + if self.send_handler: + args.update(self.send_handler.stream_positions()) + return args + + def get_currently_syncing_users(self): + return self.presence_handler.get_currently_syncing_users() + + async def process_and_notify(self, stream_name, token, rows): + try: + if self.send_handler: + self.send_handler.process_replication_rows(stream_name, token, rows) + + if stream_name == "events": + # We shouldn't get multiple rows per token for events stream, so + # we don't need to optimise this for multiple rows. + for row in rows: + if row.type != EventsStreamEventRow.TypeId: + continue + assert isinstance(row, EventsStreamRow) + + event = await self.store.get_event( + row.data.event_id, allow_rejected=True + ) + if event.rejected_reason: + continue + + extra_users = () + if event.type == EventTypes.Member: + extra_users = (event.state_key,) + max_token = self.store.get_room_max_stream_ordering() + self.notifier.on_new_room_event( + event, token, max_token, extra_users + ) + + await self.pusher_pool.on_new_notifications(token, token) + elif stream_name == "push_rules": + self.notifier.on_new_event( + "push_rules_key", token, users=[row.user_id for row in rows] + ) + elif stream_name in ("account_data", "tag_account_data"): + self.notifier.on_new_event( + "account_data_key", token, users=[row.user_id for row in rows] + ) + elif stream_name == "receipts": + self.notifier.on_new_event( + "receipt_key", token, rooms=[row.room_id for row in rows] + ) + await self.pusher_pool.on_new_receipts( + token, token, {row.room_id for row in rows} + ) + elif stream_name == "typing": + self.typing_handler.process_replication_rows(token, rows) + self.notifier.on_new_event( + "typing_key", token, rooms=[row.room_id for row in rows] + ) + elif stream_name == "to_device": + entities = [row.entity for row in rows if row.entity.startswith("@")] + if entities: + self.notifier.on_new_event("to_device_key", token, users=entities) + elif stream_name == "device_lists": + all_room_ids = set() + for row in rows: + room_ids = await self.store.get_rooms_for_user(row.user_id) + all_room_ids.update(room_ids) + self.notifier.on_new_event("device_list_key", token, rooms=all_room_ids) + elif stream_name == "presence": + await self.presence_handler.process_replication_rows(token, rows) + elif stream_name == "receipts": + self.notifier.on_new_event( + "groups_key", token, users=[row.user_id for row in rows] + ) + elif stream_name == "pushers": + for row in rows: + if row.deleted: + self.stop_pusher(row.user_id, row.app_id, row.pushkey) + else: + await self.start_pusher(row.user_id, row.app_id, row.pushkey) + except Exception: + logger.exception("Error processing replication") + + def stop_pusher(self, user_id, app_id, pushkey): + if not self.notify_pushers: + return + + key = "%s:%s" % (app_id, pushkey) + pushers_for_user = self.pusher_pool.pushers.get(user_id, {}) + pusher = pushers_for_user.pop(key, None) + if pusher is None: + return + logger.info("Stopping pusher %r / %r", user_id, key) + pusher.on_stop() + + async def start_pusher(self, user_id, app_id, pushkey): + if not self.notify_pushers: + return + + key = "%s:%s" % (app_id, pushkey) + logger.info("Starting pusher %r / %r", user_id, key) + return await self.pusher_pool.start_pusher_by_id(app_id, pushkey, user_id) + + def on_remote_server_up(self, server: str): + """Called when get a new REMOTE_SERVER_UP command.""" + + # Let's wake up the transaction queue for the server in case we have + # pending stuff to send to it. + if self.send_handler: + self.send_handler.wake_destination(server) + + +class FederationSenderHandler(object): + """Processes the replication stream and forwards the appropriate entries + to the federation sender. + """ + + def __init__(self, hs: GenericWorkerServer, replication_client): + self.store = hs.get_datastore() + self._is_mine_id = hs.is_mine_id + self.federation_sender = hs.get_federation_sender() + self.replication_client = replication_client + + self.federation_position = self.store.federation_out_pos_startup + self._fed_position_linearizer = Linearizer(name="_fed_position_linearizer") + + self._last_ack = self.federation_position + + self._room_serials = {} + self._room_typing = {} + + def on_start(self): + # There may be some events that are persisted but haven't been sent, + # so send them now. + self.federation_sender.notify_new_events( + self.store.get_room_max_stream_ordering() + ) + + def wake_destination(self, server: str): + self.federation_sender.wake_destination(server) + + def stream_positions(self): + return {"federation": self.federation_position} + + def process_replication_rows(self, stream_name, token, rows): + # The federation stream contains things that we want to send out, e.g. + # presence, typing, etc. + if stream_name == "federation": + send_queue.process_rows_for_federation(self.federation_sender, rows) + run_in_background(self.update_token, token) + + # We also need to poke the federation sender when new events happen + elif stream_name == "events": + self.federation_sender.notify_new_events(token) + + # ... and when new receipts happen + elif stream_name == ReceiptsStream.NAME: + run_as_background_process( + "process_receipts_for_federation", self._on_new_receipts, rows + ) + + # ... as well as device updates and messages + elif stream_name == DeviceListsStream.NAME: + hosts = {row.destination for row in rows} + for host in hosts: + self.federation_sender.send_device_messages(host) + + elif stream_name == ToDeviceStream.NAME: + # The to_device stream includes stuff to be pushed to both local + # clients and remote servers, so we ignore entities that start with + # '@' (since they'll be local users rather than destinations). + hosts = {row.entity for row in rows if not row.entity.startswith("@")} + for host in hosts: + self.federation_sender.send_device_messages(host) + + async def _on_new_receipts(self, rows): + """ + Args: + rows (iterable[synapse.replication.tcp.streams.ReceiptsStreamRow]): + new receipts to be processed + """ + for receipt in rows: + # we only want to send on receipts for our own users + if not self._is_mine_id(receipt.user_id): + continue + receipt_info = ReadReceipt( + receipt.room_id, + receipt.receipt_type, + receipt.user_id, + [receipt.event_id], + receipt.data, + ) + await self.federation_sender.send_read_receipt(receipt_info) + + async def update_token(self, token): + try: + self.federation_position = token + + # We linearize here to ensure we don't have races updating the token + with (await self._fed_position_linearizer.queue(None)): + if self._last_ack < self.federation_position: + await self.store.update_federation_out_pos( + "federation", self.federation_position + ) + + # We ACK this token over replication so that the master can drop + # its in memory queues + self.replication_client.send_federation_ack( + self.federation_position + ) + self._last_ack = self.federation_position + except Exception: + logger.exception("Error updating federation stream position") + + +def start(config_options): + try: + config = HomeServerConfig.load_config("Synapse worker", config_options) + except ConfigError as e: + sys.stderr.write("\n" + str(e) + "\n") + sys.exit(1) + + # For backwards compatibility let any of the old app names. + assert config.worker_app in ( + "synapse.app.appservice", + "synapse.app.client_reader", + "synapse.app.event_creator", + "synapse.app.federation_reader", + "synapse.app.federation_sender", + "synapse.app.frontend_proxy", + "synapse.app.generic_worker", + "synapse.app.media_repository", + "synapse.app.pusher", + "synapse.app.synchrotron", + "synapse.app.user_dir", + ) + + if config.worker_app == "synapse.app.appservice": + if config.notify_appservices: + sys.stderr.write( + "\nThe appservices must be disabled in the main synapse process" + "\nbefore they can be run in a separate worker." + "\nPlease add ``notify_appservices: false`` to the main config" + "\n" + ) + sys.exit(1) + + # Force the appservice to start since they will be disabled in the main config + config.notify_appservices = True + + if config.worker_app == "synapse.app.pusher": + if config.start_pushers: + sys.stderr.write( + "\nThe pushers must be disabled in the main synapse process" + "\nbefore they can be run in a separate worker." + "\nPlease add ``start_pushers: false`` to the main config" + "\n" + ) + sys.exit(1) + + # Force the pushers to start since they will be disabled in the main config + config.start_pushers = True + + if config.worker_app == "synapse.app.user_dir": + if config.update_user_directory: + sys.stderr.write( + "\nThe update_user_directory must be disabled in the main synapse process" + "\nbefore they can be run in a separate worker." + "\nPlease add ``update_user_directory: false`` to the main config" + "\n" + ) + sys.exit(1) + + # Force the pushers to start since they will be disabled in the main config + config.update_user_directory = True + + if config.worker_app == "synapse.app.federation_sender": + if config.send_federation: + sys.stderr.write( + "\nThe send_federation must be disabled in the main synapse process" + "\nbefore they can be run in a separate worker." + "\nPlease add ``send_federation: false`` to the main config" + "\n" + ) + sys.exit(1) + + # Force the pushers to start since they will be disabled in the main config + config.send_federation = True + + synapse.events.USE_FROZEN_DICTS = config.use_frozen_dicts + + ss = GenericWorkerServer( + config.server_name, + config=config, + version_string="Synapse/" + get_version_string(synapse), + ) + + setup_logging(ss, config, use_worker_options=True) + + ss.setup() + reactor.addSystemEventTrigger( + "before", "startup", _base.start, ss, config.worker_listeners + ) + + _base.start_worker_reactor("synapse-generic-worker", config) + + +if __name__ == "__main__": + with LoggingContext("main"): + start(sys.argv[1:]) diff --git a/synapse/app/media_repository.py b/synapse/app/media_repository.py index 5b5832214a..add43147b3 100644 --- a/synapse/app/media_repository.py +++ b/synapse/app/media_repository.py @@ -13,162 +13,11 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import logging + import sys -from twisted.internet import reactor -from twisted.web.resource import NoResource - -import synapse -from synapse import events -from synapse.api.urls import LEGACY_MEDIA_PREFIX, MEDIA_PREFIX -from synapse.app import _base -from synapse.config._base import ConfigError -from synapse.config.homeserver import HomeServerConfig -from synapse.config.logger import setup_logging -from synapse.http.server import JsonResource -from synapse.http.site import SynapseSite -from synapse.logging.context import LoggingContext -from synapse.metrics import METRICS_PREFIX, MetricsResource, RegistryProxy -from synapse.replication.slave.storage._base import BaseSlavedStore -from synapse.replication.slave.storage.appservice import SlavedApplicationServiceStore -from synapse.replication.slave.storage.client_ips import SlavedClientIpStore -from synapse.replication.slave.storage.registration import SlavedRegistrationStore -from synapse.replication.slave.storage.room import RoomStore -from synapse.replication.slave.storage.transactions import SlavedTransactionStore -from synapse.replication.tcp.client import ReplicationClientHandler -from synapse.rest.admin import register_servlets_for_media_repo -from synapse.server import HomeServer -from synapse.storage.data_stores.main.media_repository import MediaRepositoryStore -from synapse.util.httpresourcetree import create_resource_tree -from synapse.util.manhole import manhole -from synapse.util.versionstring import get_version_string - -logger = logging.getLogger("synapse.app.media_repository") - - -class MediaRepositorySlavedStore( - RoomStore, - SlavedApplicationServiceStore, - SlavedRegistrationStore, - SlavedClientIpStore, - SlavedTransactionStore, - BaseSlavedStore, - MediaRepositoryStore, -): - pass - - -class MediaRepositoryServer(HomeServer): - DATASTORE_CLASS = MediaRepositorySlavedStore - - def _listen_http(self, listener_config): - port = listener_config["port"] - bind_addresses = listener_config["bind_addresses"] - site_tag = listener_config.get("tag", port) - resources = {} - for res in listener_config["resources"]: - for name in res["names"]: - if name == "metrics": - resources[METRICS_PREFIX] = MetricsResource(RegistryProxy) - elif name == "media": - media_repo = self.get_media_repository_resource() - - # We need to serve the admin servlets for media on the - # worker. - admin_resource = JsonResource(self, canonical_json=False) - register_servlets_for_media_repo(self, admin_resource) - - resources.update( - { - MEDIA_PREFIX: media_repo, - LEGACY_MEDIA_PREFIX: media_repo, - "/_synapse/admin": admin_resource, - } - ) - - root_resource = create_resource_tree(resources, NoResource()) - - _base.listen_tcp( - bind_addresses, - port, - SynapseSite( - "synapse.access.http.%s" % (site_tag,), - site_tag, - listener_config, - root_resource, - self.version_string, - ), - ) - - logger.info("Synapse media repository now listening on port %d", port) - - def start_listening(self, listeners): - for listener in listeners: - if listener["type"] == "http": - self._listen_http(listener) - elif listener["type"] == "manhole": - _base.listen_tcp( - listener["bind_addresses"], - listener["port"], - manhole( - username="matrix", password="rabbithole", globals={"hs": self} - ), - ) - elif listener["type"] == "metrics": - if not self.get_config().enable_metrics: - logger.warning( - ( - "Metrics listener configured, but " - "enable_metrics is not True!" - ) - ) - else: - _base.listen_metrics(listener["bind_addresses"], listener["port"]) - else: - logger.warning("Unrecognized listener type: %s", listener["type"]) - - self.get_tcp_replication().start_replication(self) - - def build_tcp_replication(self): - return ReplicationClientHandler(self.get_datastore()) - - -def start(config_options): - try: - config = HomeServerConfig.load_config( - "Synapse media repository", config_options - ) - except ConfigError as e: - sys.stderr.write("\n" + str(e) + "\n") - sys.exit(1) - - assert config.worker_app == "synapse.app.media_repository" - - if config.enable_media_repo: - _base.quit_with_error( - "enable_media_repo must be disabled in the main synapse process\n" - "before the media repo can be run in a separate worker.\n" - "Please add ``enable_media_repo: false`` to the main config\n" - ) - - events.USE_FROZEN_DICTS = config.use_frozen_dicts - - ss = MediaRepositoryServer( - config.server_name, - config=config, - version_string="Synapse/" + get_version_string(synapse), - ) - - setup_logging(ss, config, use_worker_options=True) - - ss.setup() - reactor.addSystemEventTrigger( - "before", "startup", _base.start, ss, config.worker_listeners - ) - - _base.start_worker_reactor("synapse-media-repository", config) - +from synapse.app.generic_worker import start +from synapse.util.logcontext import LoggingContext if __name__ == "__main__": with LoggingContext("main"): diff --git a/synapse/app/pusher.py b/synapse/app/pusher.py index 84e9f8d5e2..add43147b3 100644 --- a/synapse/app/pusher.py +++ b/synapse/app/pusher.py @@ -13,213 +13,12 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import logging + import sys -from twisted.internet import defer, reactor -from twisted.web.resource import NoResource - -import synapse -from synapse import events -from synapse.app import _base -from synapse.config._base import ConfigError -from synapse.config.homeserver import HomeServerConfig -from synapse.config.logger import setup_logging -from synapse.http.site import SynapseSite -from synapse.logging.context import LoggingContext, run_in_background -from synapse.metrics import METRICS_PREFIX, MetricsResource, RegistryProxy -from synapse.replication.slave.storage._base import __func__ -from synapse.replication.slave.storage.account_data import SlavedAccountDataStore -from synapse.replication.slave.storage.events import SlavedEventStore -from synapse.replication.slave.storage.pushers import SlavedPusherStore -from synapse.replication.slave.storage.receipts import SlavedReceiptsStore -from synapse.replication.slave.storage.room import RoomStore -from synapse.replication.tcp.client import ReplicationClientHandler -from synapse.server import HomeServer -from synapse.storage import DataStore -from synapse.util.httpresourcetree import create_resource_tree -from synapse.util.manhole import manhole -from synapse.util.versionstring import get_version_string - -logger = logging.getLogger("synapse.app.pusher") - - -class PusherSlaveStore( - SlavedEventStore, - SlavedPusherStore, - SlavedReceiptsStore, - SlavedAccountDataStore, - RoomStore, -): - update_pusher_last_stream_ordering_and_success = __func__( - DataStore.update_pusher_last_stream_ordering_and_success - ) - - update_pusher_failing_since = __func__(DataStore.update_pusher_failing_since) - - update_pusher_last_stream_ordering = __func__( - DataStore.update_pusher_last_stream_ordering - ) - - get_throttle_params_by_room = __func__(DataStore.get_throttle_params_by_room) - - set_throttle_params = __func__(DataStore.set_throttle_params) - - get_time_of_last_push_action_before = __func__( - DataStore.get_time_of_last_push_action_before - ) - - get_profile_displayname = __func__(DataStore.get_profile_displayname) - - -class PusherServer(HomeServer): - DATASTORE_CLASS = PusherSlaveStore - - def remove_pusher(self, app_id, push_key, user_id): - self.get_tcp_replication().send_remove_pusher(app_id, push_key, user_id) - - def _listen_http(self, listener_config): - port = listener_config["port"] - bind_addresses = listener_config["bind_addresses"] - site_tag = listener_config.get("tag", port) - resources = {} - for res in listener_config["resources"]: - for name in res["names"]: - if name == "metrics": - resources[METRICS_PREFIX] = MetricsResource(RegistryProxy) - - root_resource = create_resource_tree(resources, NoResource()) - - _base.listen_tcp( - bind_addresses, - port, - SynapseSite( - "synapse.access.http.%s" % (site_tag,), - site_tag, - listener_config, - root_resource, - self.version_string, - ), - ) - - logger.info("Synapse pusher now listening on port %d", port) - - def start_listening(self, listeners): - for listener in listeners: - if listener["type"] == "http": - self._listen_http(listener) - elif listener["type"] == "manhole": - _base.listen_tcp( - listener["bind_addresses"], - listener["port"], - manhole( - username="matrix", password="rabbithole", globals={"hs": self} - ), - ) - elif listener["type"] == "metrics": - if not self.get_config().enable_metrics: - logger.warning( - ( - "Metrics listener configured, but " - "enable_metrics is not True!" - ) - ) - else: - _base.listen_metrics(listener["bind_addresses"], listener["port"]) - else: - logger.warning("Unrecognized listener type: %s", listener["type"]) - - self.get_tcp_replication().start_replication(self) - - def build_tcp_replication(self): - return PusherReplicationHandler(self) - - -class PusherReplicationHandler(ReplicationClientHandler): - def __init__(self, hs): - super(PusherReplicationHandler, self).__init__(hs.get_datastore()) - - self.pusher_pool = hs.get_pusherpool() - - async def on_rdata(self, stream_name, token, rows): - await super(PusherReplicationHandler, self).on_rdata(stream_name, token, rows) - run_in_background(self.poke_pushers, stream_name, token, rows) - - @defer.inlineCallbacks - def poke_pushers(self, stream_name, token, rows): - try: - if stream_name == "pushers": - for row in rows: - if row.deleted: - yield self.stop_pusher(row.user_id, row.app_id, row.pushkey) - else: - yield self.start_pusher(row.user_id, row.app_id, row.pushkey) - elif stream_name == "events": - yield self.pusher_pool.on_new_notifications(token, token) - elif stream_name == "receipts": - yield self.pusher_pool.on_new_receipts( - token, token, {row.room_id for row in rows} - ) - except Exception: - logger.exception("Error poking pushers") - - def stop_pusher(self, user_id, app_id, pushkey): - key = "%s:%s" % (app_id, pushkey) - pushers_for_user = self.pusher_pool.pushers.get(user_id, {}) - pusher = pushers_for_user.pop(key, None) - if pusher is None: - return - logger.info("Stopping pusher %r / %r", user_id, key) - pusher.on_stop() - - def start_pusher(self, user_id, app_id, pushkey): - key = "%s:%s" % (app_id, pushkey) - logger.info("Starting pusher %r / %r", user_id, key) - return self.pusher_pool.start_pusher_by_id(app_id, pushkey, user_id) - - -def start(config_options): - try: - config = HomeServerConfig.load_config("Synapse pusher", config_options) - except ConfigError as e: - sys.stderr.write("\n" + str(e) + "\n") - sys.exit(1) - - assert config.worker_app == "synapse.app.pusher" - - events.USE_FROZEN_DICTS = config.use_frozen_dicts - - if config.start_pushers: - sys.stderr.write( - "\nThe pushers must be disabled in the main synapse process" - "\nbefore they can be run in a separate worker." - "\nPlease add ``start_pushers: false`` to the main config" - "\n" - ) - sys.exit(1) - - # Force the pushers to start since they will be disabled in the main config - config.start_pushers = True - - ps = PusherServer( - config.server_name, - config=config, - version_string="Synapse/" + get_version_string(synapse), - ) - - setup_logging(ps, config, use_worker_options=True) - - ps.setup() - - def start(): - _base.start(ps, config.worker_listeners) - ps.get_pusherpool().start() - - reactor.addSystemEventTrigger("before", "startup", start) - - _base.start_worker_reactor("synapse-pusher", config) - +from synapse.app.generic_worker import start +from synapse.util.logcontext import LoggingContext if __name__ == "__main__": with LoggingContext("main"): - ps = start(sys.argv[1:]) + start(sys.argv[1:]) diff --git a/synapse/app/synchrotron.py b/synapse/app/synchrotron.py index 8982c0676e..add43147b3 100644 --- a/synapse/app/synchrotron.py +++ b/synapse/app/synchrotron.py @@ -13,454 +13,11 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import contextlib -import logging + import sys -from six import iteritems - -from twisted.internet import defer, reactor -from twisted.web.resource import NoResource - -import synapse -from synapse.api.constants import EventTypes -from synapse.app import _base -from synapse.config._base import ConfigError -from synapse.config.homeserver import HomeServerConfig -from synapse.config.logger import setup_logging -from synapse.handlers.presence import PresenceHandler, get_interested_parties -from synapse.http.server import JsonResource -from synapse.http.site import SynapseSite -from synapse.logging.context import LoggingContext, run_in_background -from synapse.metrics import METRICS_PREFIX, MetricsResource, RegistryProxy -from synapse.replication.slave.storage._base import BaseSlavedStore, __func__ -from synapse.replication.slave.storage.account_data import SlavedAccountDataStore -from synapse.replication.slave.storage.appservice import SlavedApplicationServiceStore -from synapse.replication.slave.storage.client_ips import SlavedClientIpStore -from synapse.replication.slave.storage.deviceinbox import SlavedDeviceInboxStore -from synapse.replication.slave.storage.devices import SlavedDeviceStore -from synapse.replication.slave.storage.events import SlavedEventStore -from synapse.replication.slave.storage.filtering import SlavedFilteringStore -from synapse.replication.slave.storage.groups import SlavedGroupServerStore -from synapse.replication.slave.storage.presence import SlavedPresenceStore -from synapse.replication.slave.storage.push_rule import SlavedPushRuleStore -from synapse.replication.slave.storage.receipts import SlavedReceiptsStore -from synapse.replication.slave.storage.registration import SlavedRegistrationStore -from synapse.replication.slave.storage.room import RoomStore -from synapse.replication.tcp.client import ReplicationClientHandler -from synapse.replication.tcp.streams.events import EventsStreamEventRow, EventsStreamRow -from synapse.rest.client.v1 import events -from synapse.rest.client.v1.initial_sync import InitialSyncRestServlet -from synapse.rest.client.v1.room import RoomInitialSyncRestServlet -from synapse.rest.client.v2_alpha import sync -from synapse.server import HomeServer -from synapse.storage.data_stores.main.monthly_active_users import ( - MonthlyActiveUsersWorkerStore, -) -from synapse.storage.data_stores.main.presence import UserPresenceState -from synapse.util.httpresourcetree import create_resource_tree -from synapse.util.manhole import manhole -from synapse.util.stringutils import random_string -from synapse.util.versionstring import get_version_string - -logger = logging.getLogger("synapse.app.synchrotron") - - -class SynchrotronSlavedStore( - SlavedReceiptsStore, - SlavedAccountDataStore, - SlavedApplicationServiceStore, - SlavedRegistrationStore, - SlavedFilteringStore, - SlavedPresenceStore, - SlavedGroupServerStore, - SlavedDeviceInboxStore, - SlavedDeviceStore, - SlavedPushRuleStore, - SlavedEventStore, - SlavedClientIpStore, - RoomStore, - MonthlyActiveUsersWorkerStore, - BaseSlavedStore, -): - pass - - -UPDATE_SYNCING_USERS_MS = 10 * 1000 - - -class SynchrotronPresence(object): - def __init__(self, hs): - self.hs = hs - self.is_mine_id = hs.is_mine_id - self.http_client = hs.get_simple_http_client() - self.store = hs.get_datastore() - self.user_to_num_current_syncs = {} - self.clock = hs.get_clock() - self.notifier = hs.get_notifier() - - active_presence = self.store.take_presence_startup_info() - self.user_to_current_state = {state.user_id: state for state in active_presence} - - # user_id -> last_sync_ms. Lists the users that have stopped syncing - # but we haven't notified the master of that yet - self.users_going_offline = {} - - self._send_stop_syncing_loop = self.clock.looping_call( - self.send_stop_syncing, 10 * 1000 - ) - - self.process_id = random_string(16) - logger.info("Presence process_id is %r", self.process_id) - - def send_user_sync(self, user_id, is_syncing, last_sync_ms): - if self.hs.config.use_presence: - self.hs.get_tcp_replication().send_user_sync( - user_id, is_syncing, last_sync_ms - ) - - def mark_as_coming_online(self, user_id): - """A user has started syncing. Send a UserSync to the master, unless they - had recently stopped syncing. - - Args: - user_id (str) - """ - going_offline = self.users_going_offline.pop(user_id, None) - if not going_offline: - # Safe to skip because we haven't yet told the master they were offline - self.send_user_sync(user_id, True, self.clock.time_msec()) - - def mark_as_going_offline(self, user_id): - """A user has stopped syncing. We wait before notifying the master as - its likely they'll come back soon. This allows us to avoid sending - a stopped syncing immediately followed by a started syncing notification - to the master - - Args: - user_id (str) - """ - self.users_going_offline[user_id] = self.clock.time_msec() - - def send_stop_syncing(self): - """Check if there are any users who have stopped syncing a while ago - and haven't come back yet. If there are poke the master about them. - """ - now = self.clock.time_msec() - for user_id, last_sync_ms in list(self.users_going_offline.items()): - if now - last_sync_ms > 10 * 1000: - self.users_going_offline.pop(user_id, None) - self.send_user_sync(user_id, False, last_sync_ms) - - def set_state(self, user, state, ignore_status_msg=False): - # TODO Hows this supposed to work? - return defer.succeed(None) - - get_states = __func__(PresenceHandler.get_states) - get_state = __func__(PresenceHandler.get_state) - current_state_for_users = __func__(PresenceHandler.current_state_for_users) - - def user_syncing(self, user_id, affect_presence): - if affect_presence: - curr_sync = self.user_to_num_current_syncs.get(user_id, 0) - self.user_to_num_current_syncs[user_id] = curr_sync + 1 - - # If we went from no in flight sync to some, notify replication - if self.user_to_num_current_syncs[user_id] == 1: - self.mark_as_coming_online(user_id) - - def _end(): - # We check that the user_id is in user_to_num_current_syncs because - # user_to_num_current_syncs may have been cleared if we are - # shutting down. - if affect_presence and user_id in self.user_to_num_current_syncs: - self.user_to_num_current_syncs[user_id] -= 1 - - # If we went from one in flight sync to non, notify replication - if self.user_to_num_current_syncs[user_id] == 0: - self.mark_as_going_offline(user_id) - - @contextlib.contextmanager - def _user_syncing(): - try: - yield - finally: - _end() - - return defer.succeed(_user_syncing()) - - @defer.inlineCallbacks - def notify_from_replication(self, states, stream_id): - parties = yield get_interested_parties(self.store, states) - room_ids_to_states, users_to_states = parties - - self.notifier.on_new_event( - "presence_key", - stream_id, - rooms=room_ids_to_states.keys(), - users=users_to_states.keys(), - ) - - @defer.inlineCallbacks - def process_replication_rows(self, token, rows): - states = [ - UserPresenceState( - row.user_id, - row.state, - row.last_active_ts, - row.last_federation_update_ts, - row.last_user_sync_ts, - row.status_msg, - row.currently_active, - ) - for row in rows - ] - - for state in states: - self.user_to_current_state[state.user_id] = state - - stream_id = token - yield self.notify_from_replication(states, stream_id) - - def get_currently_syncing_users(self): - if self.hs.config.use_presence: - return [ - user_id - for user_id, count in iteritems(self.user_to_num_current_syncs) - if count > 0 - ] - else: - return set() - - -class SynchrotronTyping(object): - def __init__(self, hs): - self._latest_room_serial = 0 - self._reset() - - def _reset(self): - """ - Reset the typing handler's data caches. - """ - # map room IDs to serial numbers - self._room_serials = {} - # map room IDs to sets of users currently typing - self._room_typing = {} - - def stream_positions(self): - # We must update this typing token from the response of the previous - # sync. In particular, the stream id may "reset" back to zero/a low - # value which we *must* use for the next replication request. - return {"typing": self._latest_room_serial} - - def process_replication_rows(self, token, rows): - if self._latest_room_serial > token: - # The master has gone backwards. To prevent inconsistent data, just - # clear everything. - self._reset() - - # Set the latest serial token to whatever the server gave us. - self._latest_room_serial = token - - for row in rows: - self._room_serials[row.room_id] = token - self._room_typing[row.room_id] = row.user_ids - - -class SynchrotronApplicationService(object): - def notify_interested_services(self, event): - pass - - -class SynchrotronServer(HomeServer): - DATASTORE_CLASS = SynchrotronSlavedStore - - def _listen_http(self, listener_config): - port = listener_config["port"] - bind_addresses = listener_config["bind_addresses"] - site_tag = listener_config.get("tag", port) - resources = {} - for res in listener_config["resources"]: - for name in res["names"]: - if name == "metrics": - resources[METRICS_PREFIX] = MetricsResource(RegistryProxy) - elif name == "client": - resource = JsonResource(self, canonical_json=False) - sync.register_servlets(self, resource) - events.register_servlets(self, resource) - InitialSyncRestServlet(self).register(resource) - RoomInitialSyncRestServlet(self).register(resource) - resources.update( - { - "/_matrix/client/r0": resource, - "/_matrix/client/unstable": resource, - "/_matrix/client/v2_alpha": resource, - "/_matrix/client/api/v1": resource, - } - ) - - root_resource = create_resource_tree(resources, NoResource()) - - _base.listen_tcp( - bind_addresses, - port, - SynapseSite( - "synapse.access.http.%s" % (site_tag,), - site_tag, - listener_config, - root_resource, - self.version_string, - ), - ) - - logger.info("Synapse synchrotron now listening on port %d", port) - - def start_listening(self, listeners): - for listener in listeners: - if listener["type"] == "http": - self._listen_http(listener) - elif listener["type"] == "manhole": - _base.listen_tcp( - listener["bind_addresses"], - listener["port"], - manhole( - username="matrix", password="rabbithole", globals={"hs": self} - ), - ) - elif listener["type"] == "metrics": - if not self.get_config().enable_metrics: - logger.warning( - ( - "Metrics listener configured, but " - "enable_metrics is not True!" - ) - ) - else: - _base.listen_metrics(listener["bind_addresses"], listener["port"]) - else: - logger.warning("Unrecognized listener type: %s", listener["type"]) - - self.get_tcp_replication().start_replication(self) - - def build_tcp_replication(self): - return SyncReplicationHandler(self) - - def build_presence_handler(self): - return SynchrotronPresence(self) - - def build_typing_handler(self): - return SynchrotronTyping(self) - - -class SyncReplicationHandler(ReplicationClientHandler): - def __init__(self, hs): - super(SyncReplicationHandler, self).__init__(hs.get_datastore()) - - self.store = hs.get_datastore() - self.typing_handler = hs.get_typing_handler() - # NB this is a SynchrotronPresence, not a normal PresenceHandler - self.presence_handler = hs.get_presence_handler() - self.notifier = hs.get_notifier() - - async def on_rdata(self, stream_name, token, rows): - await super(SyncReplicationHandler, self).on_rdata(stream_name, token, rows) - run_in_background(self.process_and_notify, stream_name, token, rows) - - def get_streams_to_replicate(self): - args = super(SyncReplicationHandler, self).get_streams_to_replicate() - args.update(self.typing_handler.stream_positions()) - return args - - def get_currently_syncing_users(self): - return self.presence_handler.get_currently_syncing_users() - - async def process_and_notify(self, stream_name, token, rows): - try: - if stream_name == "events": - # We shouldn't get multiple rows per token for events stream, so - # we don't need to optimise this for multiple rows. - for row in rows: - if row.type != EventsStreamEventRow.TypeId: - continue - assert isinstance(row, EventsStreamRow) - - event = await self.store.get_event( - row.data.event_id, allow_rejected=True - ) - if event.rejected_reason: - continue - - extra_users = () - if event.type == EventTypes.Member: - extra_users = (event.state_key,) - max_token = self.store.get_room_max_stream_ordering() - self.notifier.on_new_room_event( - event, token, max_token, extra_users - ) - elif stream_name == "push_rules": - self.notifier.on_new_event( - "push_rules_key", token, users=[row.user_id for row in rows] - ) - elif stream_name in ("account_data", "tag_account_data"): - self.notifier.on_new_event( - "account_data_key", token, users=[row.user_id for row in rows] - ) - elif stream_name == "receipts": - self.notifier.on_new_event( - "receipt_key", token, rooms=[row.room_id for row in rows] - ) - elif stream_name == "typing": - self.typing_handler.process_replication_rows(token, rows) - self.notifier.on_new_event( - "typing_key", token, rooms=[row.room_id for row in rows] - ) - elif stream_name == "to_device": - entities = [row.entity for row in rows if row.entity.startswith("@")] - if entities: - self.notifier.on_new_event("to_device_key", token, users=entities) - elif stream_name == "device_lists": - all_room_ids = set() - for row in rows: - room_ids = await self.store.get_rooms_for_user(row.user_id) - all_room_ids.update(room_ids) - self.notifier.on_new_event("device_list_key", token, rooms=all_room_ids) - elif stream_name == "presence": - await self.presence_handler.process_replication_rows(token, rows) - elif stream_name == "receipts": - self.notifier.on_new_event( - "groups_key", token, users=[row.user_id for row in rows] - ) - except Exception: - logger.exception("Error processing replication") - - -def start(config_options): - try: - config = HomeServerConfig.load_config("Synapse synchrotron", config_options) - except ConfigError as e: - sys.stderr.write("\n" + str(e) + "\n") - sys.exit(1) - - assert config.worker_app == "synapse.app.synchrotron" - - synapse.events.USE_FROZEN_DICTS = config.use_frozen_dicts - - ss = SynchrotronServer( - config.server_name, - config=config, - version_string="Synapse/" + get_version_string(synapse), - application_service_handler=SynchrotronApplicationService(), - ) - - setup_logging(ss, config, use_worker_options=True) - - ss.setup() - reactor.addSystemEventTrigger( - "before", "startup", _base.start, ss, config.worker_listeners - ) - - _base.start_worker_reactor("synapse-synchrotron", config) - +from synapse.app.generic_worker import start +from synapse.util.logcontext import LoggingContext if __name__ == "__main__": with LoggingContext("main"): diff --git a/synapse/app/user_dir.py b/synapse/app/user_dir.py index ba536d6f04..503d44f687 100644 --- a/synapse/app/user_dir.py +++ b/synapse/app/user_dir.py @@ -14,217 +14,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -import logging import sys -from twisted.internet import defer, reactor -from twisted.web.resource import NoResource - -import synapse -from synapse import events -from synapse.app import _base -from synapse.config._base import ConfigError -from synapse.config.homeserver import HomeServerConfig -from synapse.config.logger import setup_logging -from synapse.http.server import JsonResource -from synapse.http.site import SynapseSite -from synapse.logging.context import LoggingContext, run_in_background -from synapse.metrics import METRICS_PREFIX, MetricsResource, RegistryProxy -from synapse.replication.slave.storage._base import BaseSlavedStore -from synapse.replication.slave.storage.appservice import SlavedApplicationServiceStore -from synapse.replication.slave.storage.client_ips import SlavedClientIpStore -from synapse.replication.slave.storage.events import SlavedEventStore -from synapse.replication.slave.storage.registration import SlavedRegistrationStore -from synapse.replication.tcp.client import ReplicationClientHandler -from synapse.replication.tcp.streams.events import ( - EventsStream, - EventsStreamCurrentStateRow, -) -from synapse.rest.client.v2_alpha import user_directory -from synapse.server import HomeServer -from synapse.storage.data_stores.main.user_directory import UserDirectoryStore -from synapse.storage.database import Database -from synapse.util.caches.stream_change_cache import StreamChangeCache -from synapse.util.httpresourcetree import create_resource_tree -from synapse.util.manhole import manhole -from synapse.util.versionstring import get_version_string - -logger = logging.getLogger("synapse.app.user_dir") - - -class UserDirectorySlaveStore( - SlavedEventStore, - SlavedApplicationServiceStore, - SlavedRegistrationStore, - SlavedClientIpStore, - UserDirectoryStore, - BaseSlavedStore, -): - def __init__(self, database: Database, db_conn, hs): - super(UserDirectorySlaveStore, self).__init__(database, db_conn, hs) - - events_max = self._stream_id_gen.get_current_token() - curr_state_delta_prefill, min_curr_state_delta_id = self.db.get_cache_dict( - db_conn, - "current_state_delta_stream", - entity_column="room_id", - stream_column="stream_id", - max_value=events_max, # As we share the stream id with events token - limit=1000, - ) - self._curr_state_delta_stream_cache = StreamChangeCache( - "_curr_state_delta_stream_cache", - min_curr_state_delta_id, - prefilled_cache=curr_state_delta_prefill, - ) - - def stream_positions(self): - result = super(UserDirectorySlaveStore, self).stream_positions() - return result - - def process_replication_rows(self, stream_name, token, rows): - if stream_name == EventsStream.NAME: - self._stream_id_gen.advance(token) - for row in rows: - if row.type != EventsStreamCurrentStateRow.TypeId: - continue - self._curr_state_delta_stream_cache.entity_has_changed( - row.data.room_id, token - ) - return super(UserDirectorySlaveStore, self).process_replication_rows( - stream_name, token, rows - ) - - -class UserDirectoryServer(HomeServer): - DATASTORE_CLASS = UserDirectorySlaveStore - - def _listen_http(self, listener_config): - port = listener_config["port"] - bind_addresses = listener_config["bind_addresses"] - site_tag = listener_config.get("tag", port) - resources = {} - for res in listener_config["resources"]: - for name in res["names"]: - if name == "metrics": - resources[METRICS_PREFIX] = MetricsResource(RegistryProxy) - elif name == "client": - resource = JsonResource(self, canonical_json=False) - user_directory.register_servlets(self, resource) - resources.update( - { - "/_matrix/client/r0": resource, - "/_matrix/client/unstable": resource, - "/_matrix/client/v2_alpha": resource, - "/_matrix/client/api/v1": resource, - } - ) - - root_resource = create_resource_tree(resources, NoResource()) - - _base.listen_tcp( - bind_addresses, - port, - SynapseSite( - "synapse.access.http.%s" % (site_tag,), - site_tag, - listener_config, - root_resource, - self.version_string, - ), - ) - - logger.info("Synapse user_dir now listening on port %d", port) - - def start_listening(self, listeners): - for listener in listeners: - if listener["type"] == "http": - self._listen_http(listener) - elif listener["type"] == "manhole": - _base.listen_tcp( - listener["bind_addresses"], - listener["port"], - manhole( - username="matrix", password="rabbithole", globals={"hs": self} - ), - ) - elif listener["type"] == "metrics": - if not self.get_config().enable_metrics: - logger.warning( - ( - "Metrics listener configured, but " - "enable_metrics is not True!" - ) - ) - else: - _base.listen_metrics(listener["bind_addresses"], listener["port"]) - else: - logger.warning("Unrecognized listener type: %s", listener["type"]) - - self.get_tcp_replication().start_replication(self) - - def build_tcp_replication(self): - return UserDirectoryReplicationHandler(self) - - -class UserDirectoryReplicationHandler(ReplicationClientHandler): - def __init__(self, hs): - super(UserDirectoryReplicationHandler, self).__init__(hs.get_datastore()) - self.user_directory = hs.get_user_directory_handler() - - async def on_rdata(self, stream_name, token, rows): - await super(UserDirectoryReplicationHandler, self).on_rdata( - stream_name, token, rows - ) - if stream_name == EventsStream.NAME: - run_in_background(self._notify_directory) - - @defer.inlineCallbacks - def _notify_directory(self): - try: - yield self.user_directory.notify_new_event() - except Exception: - logger.exception("Error notifiying user directory of state update") - - -def start(config_options): - try: - config = HomeServerConfig.load_config("Synapse user directory", config_options) - except ConfigError as e: - sys.stderr.write("\n" + str(e) + "\n") - sys.exit(1) - - assert config.worker_app == "synapse.app.user_dir" - - events.USE_FROZEN_DICTS = config.use_frozen_dicts - - if config.update_user_directory: - sys.stderr.write( - "\nThe update_user_directory must be disabled in the main synapse process" - "\nbefore they can be run in a separate worker." - "\nPlease add ``update_user_directory: false`` to the main config" - "\n" - ) - sys.exit(1) - - # Force the pushers to start since they will be disabled in the main config - config.update_user_directory = True - - ss = UserDirectoryServer( - config.server_name, - config=config, - version_string="Synapse/" + get_version_string(synapse), - ) - - setup_logging(ss, config, use_worker_options=True) - - ss.setup() - reactor.addSystemEventTrigger( - "before", "startup", _base.start, ss, config.worker_listeners - ) - - _base.start_worker_reactor("synapse-user-dir", config) - +from synapse.app.generic_worker import start +from synapse.util.logcontext import LoggingContext if __name__ == "__main__": with LoggingContext("main"): diff --git a/synapse/replication/slave/storage/events.py b/synapse/replication/slave/storage/events.py index 3aa6cb8b96..e73342c657 100644 --- a/synapse/replication/slave/storage/events.py +++ b/synapse/replication/slave/storage/events.py @@ -32,6 +32,7 @@ from synapse.storage.data_stores.main.state import StateGroupWorkerStore from synapse.storage.data_stores.main.stream import StreamWorkerStore from synapse.storage.data_stores.main.user_erasure_store import UserErasureWorkerStore from synapse.storage.database import Database +from synapse.util.caches.stream_change_cache import StreamChangeCache from ._base import BaseSlavedStore from ._slaved_id_tracker import SlavedIdTracker @@ -68,6 +69,21 @@ class SlavedEventStore( super(SlavedEventStore, self).__init__(database, db_conn, hs) + events_max = self._stream_id_gen.get_current_token() + curr_state_delta_prefill, min_curr_state_delta_id = self.db.get_cache_dict( + db_conn, + "current_state_delta_stream", + entity_column="room_id", + stream_column="stream_id", + max_value=events_max, # As we share the stream id with events token + limit=1000, + ) + self._curr_state_delta_stream_cache = StreamChangeCache( + "_curr_state_delta_stream_cache", + min_curr_state_delta_id, + prefilled_cache=curr_state_delta_prefill, + ) + # Cached functions can't be accessed through a class instance so we need # to reach inside the __dict__ to extract them. @@ -120,6 +136,10 @@ class SlavedEventStore( backfilled=False, ) elif row.type == EventsStreamCurrentStateRow.TypeId: + self._curr_state_delta_stream_cache.entity_has_changed( + row.data.room_id, token + ) + if data.type == EventTypes.Member: self.get_rooms_for_user_with_stream_ordering.invalidate( (data.state_key,) diff --git a/synapse/storage/data_stores/main/pusher.py b/synapse/storage/data_stores/main/pusher.py index 6b03233262..547b9d69cb 100644 --- a/synapse/storage/data_stores/main/pusher.py +++ b/synapse/storage/data_stores/main/pusher.py @@ -197,6 +197,84 @@ class PusherWorkerStore(SQLBaseStore): return result + @defer.inlineCallbacks + def update_pusher_last_stream_ordering( + self, app_id, pushkey, user_id, last_stream_ordering + ): + yield self.db.simple_update_one( + "pushers", + {"app_id": app_id, "pushkey": pushkey, "user_name": user_id}, + {"last_stream_ordering": last_stream_ordering}, + desc="update_pusher_last_stream_ordering", + ) + + @defer.inlineCallbacks + def update_pusher_last_stream_ordering_and_success( + self, app_id, pushkey, user_id, last_stream_ordering, last_success + ): + """Update the last stream ordering position we've processed up to for + the given pusher. + + Args: + app_id (str) + pushkey (str) + last_stream_ordering (int) + last_success (int) + + Returns: + Deferred[bool]: True if the pusher still exists; False if it has been deleted. + """ + updated = yield self.db.simple_update( + table="pushers", + keyvalues={"app_id": app_id, "pushkey": pushkey, "user_name": user_id}, + updatevalues={ + "last_stream_ordering": last_stream_ordering, + "last_success": last_success, + }, + desc="update_pusher_last_stream_ordering_and_success", + ) + + return bool(updated) + + @defer.inlineCallbacks + def update_pusher_failing_since(self, app_id, pushkey, user_id, failing_since): + yield self.db.simple_update( + table="pushers", + keyvalues={"app_id": app_id, "pushkey": pushkey, "user_name": user_id}, + updatevalues={"failing_since": failing_since}, + desc="update_pusher_failing_since", + ) + + @defer.inlineCallbacks + def get_throttle_params_by_room(self, pusher_id): + res = yield self.db.simple_select_list( + "pusher_throttle", + {"pusher": pusher_id}, + ["room_id", "last_sent_ts", "throttle_ms"], + desc="get_throttle_params_by_room", + ) + + params_by_room = {} + for row in res: + params_by_room[row["room_id"]] = { + "last_sent_ts": row["last_sent_ts"], + "throttle_ms": row["throttle_ms"], + } + + return params_by_room + + @defer.inlineCallbacks + def set_throttle_params(self, pusher_id, room_id, params): + # no need to lock because `pusher_throttle` has a primary key on + # (pusher, room_id) so simple_upsert will retry + yield self.db.simple_upsert( + "pusher_throttle", + {"pusher": pusher_id, "room_id": room_id}, + params, + desc="set_throttle_params", + lock=False, + ) + class PusherStore(PusherWorkerStore): def get_pushers_stream_token(self): @@ -282,81 +360,3 @@ class PusherStore(PusherWorkerStore): with self._pushers_id_gen.get_next() as stream_id: yield self.db.runInteraction("delete_pusher", delete_pusher_txn, stream_id) - - @defer.inlineCallbacks - def update_pusher_last_stream_ordering( - self, app_id, pushkey, user_id, last_stream_ordering - ): - yield self.db.simple_update_one( - "pushers", - {"app_id": app_id, "pushkey": pushkey, "user_name": user_id}, - {"last_stream_ordering": last_stream_ordering}, - desc="update_pusher_last_stream_ordering", - ) - - @defer.inlineCallbacks - def update_pusher_last_stream_ordering_and_success( - self, app_id, pushkey, user_id, last_stream_ordering, last_success - ): - """Update the last stream ordering position we've processed up to for - the given pusher. - - Args: - app_id (str) - pushkey (str) - last_stream_ordering (int) - last_success (int) - - Returns: - Deferred[bool]: True if the pusher still exists; False if it has been deleted. - """ - updated = yield self.db.simple_update( - table="pushers", - keyvalues={"app_id": app_id, "pushkey": pushkey, "user_name": user_id}, - updatevalues={ - "last_stream_ordering": last_stream_ordering, - "last_success": last_success, - }, - desc="update_pusher_last_stream_ordering_and_success", - ) - - return bool(updated) - - @defer.inlineCallbacks - def update_pusher_failing_since(self, app_id, pushkey, user_id, failing_since): - yield self.db.simple_update( - table="pushers", - keyvalues={"app_id": app_id, "pushkey": pushkey, "user_name": user_id}, - updatevalues={"failing_since": failing_since}, - desc="update_pusher_failing_since", - ) - - @defer.inlineCallbacks - def get_throttle_params_by_room(self, pusher_id): - res = yield self.db.simple_select_list( - "pusher_throttle", - {"pusher": pusher_id}, - ["room_id", "last_sent_ts", "throttle_ms"], - desc="get_throttle_params_by_room", - ) - - params_by_room = {} - for row in res: - params_by_room[row["room_id"]] = { - "last_sent_ts": row["last_sent_ts"], - "throttle_ms": row["throttle_ms"], - } - - return params_by_room - - @defer.inlineCallbacks - def set_throttle_params(self, pusher_id, room_id, params): - # no need to lock because `pusher_throttle` has a primary key on - # (pusher, room_id) so simple_upsert will retry - yield self.db.simple_upsert( - "pusher_throttle", - {"pusher": pusher_id, "room_id": room_id}, - params, - desc="set_throttle_params", - lock=False, - ) diff --git a/tests/app/test_frontend_proxy.py b/tests/app/test_frontend_proxy.py index 8bdbc608a9..160e55aca9 100644 --- a/tests/app/test_frontend_proxy.py +++ b/tests/app/test_frontend_proxy.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from synapse.app.frontend_proxy import FrontendProxyServer +from synapse.app.generic_worker import GenericWorkerServer from tests.unittest import HomeserverTestCase @@ -22,7 +22,7 @@ class FrontendProxyTests(HomeserverTestCase): def make_homeserver(self, reactor, clock): hs = self.setup_test_homeserver( - http_client=None, homeserverToUse=FrontendProxyServer + http_client=None, homeserverToUse=GenericWorkerServer ) return hs @@ -46,9 +46,7 @@ class FrontendProxyTests(HomeserverTestCase): # Grab the resource from the site that was told to listen self.assertEqual(len(self.reactor.tcpServers), 1) site = self.reactor.tcpServers[0][1] - self.resource = ( - site.resource.children[b"_matrix"].children[b"client"].children[b"r0"] - ) + self.resource = site.resource.children[b"_matrix"].children[b"client"] request, channel = self.make_request("PUT", "presence/a/status") self.render(request) @@ -76,9 +74,7 @@ class FrontendProxyTests(HomeserverTestCase): # Grab the resource from the site that was told to listen self.assertEqual(len(self.reactor.tcpServers), 1) site = self.reactor.tcpServers[0][1] - self.resource = ( - site.resource.children[b"_matrix"].children[b"client"].children[b"r0"] - ) + self.resource = site.resource.children[b"_matrix"].children[b"client"] request, channel = self.make_request("PUT", "presence/a/status") self.render(request) diff --git a/tests/app/test_openid_listener.py b/tests/app/test_openid_listener.py index 48792d1480..1fe048048b 100644 --- a/tests/app/test_openid_listener.py +++ b/tests/app/test_openid_listener.py @@ -16,7 +16,7 @@ from mock import Mock, patch from parameterized import parameterized -from synapse.app.federation_reader import FederationReaderServer +from synapse.app.generic_worker import GenericWorkerServer from synapse.app.homeserver import SynapseHomeServer from tests.unittest import HomeserverTestCase @@ -25,7 +25,7 @@ from tests.unittest import HomeserverTestCase class FederationReaderOpenIDListenerTests(HomeserverTestCase): def make_homeserver(self, reactor, clock): hs = self.setup_test_homeserver( - http_client=None, homeserverToUse=FederationReaderServer + http_client=None, homeserverToUse=GenericWorkerServer ) return hs From e66f099ca952ef47944c7bba3fd942f98245d39f Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 25 Feb 2020 17:46:00 +0000 Subject: [PATCH 1110/1623] Sanity-check database before running upgrades (#6982) Some of the database deltas rely on `config.server_name` being set correctly, so we should check that it is before running the deltas. Fixes #6870. --- changelog.d/6982.feature | 1 + synapse/storage/data_stores/main/__init__.py | 34 +++++++++++--------- synapse/storage/prepare_database.py | 15 +++++++-- 3 files changed, 33 insertions(+), 17 deletions(-) create mode 100644 changelog.d/6982.feature diff --git a/changelog.d/6982.feature b/changelog.d/6982.feature new file mode 100644 index 0000000000..934cc5141a --- /dev/null +++ b/changelog.d/6982.feature @@ -0,0 +1 @@ +Check that server_name is correctly set before running database updates. diff --git a/synapse/storage/data_stores/main/__init__.py b/synapse/storage/data_stores/main/__init__.py index 2700cca822..acca079f23 100644 --- a/synapse/storage/data_stores/main/__init__.py +++ b/synapse/storage/data_stores/main/__init__.py @@ -20,6 +20,7 @@ import logging import time from synapse.api.constants import PresenceState +from synapse.config.homeserver import HomeServerConfig from synapse.storage.database import Database from synapse.storage.engines import PostgresEngine from synapse.storage.util.id_generators import ( @@ -117,16 +118,6 @@ class DataStore( self._clock = hs.get_clock() self.database_engine = database.engine - all_users_native = are_all_users_on_domain( - db_conn.cursor(), database.engine, hs.hostname - ) - if not all_users_native: - raise Exception( - "Found users in database not native to %s!\n" - "You cannot changed a synapse server_name after it's been configured" - % (hs.hostname,) - ) - self._stream_id_gen = StreamIdGenerator( db_conn, "events", @@ -567,13 +558,26 @@ class DataStore( ) -def are_all_users_on_domain(txn, database_engine, domain): +def check_database_before_upgrade(cur, database_engine, config: HomeServerConfig): + """Called before upgrading an existing database to check that it is broadly sane + compared with the configuration. + """ + domain = config.server_name + sql = database_engine.convert_param_style( "SELECT COUNT(*) FROM users WHERE name NOT LIKE ?" ) pat = "%:" + domain - txn.execute(sql, (pat,)) - num_not_matching = txn.fetchall()[0][0] + cur.execute(sql, (pat,)) + num_not_matching = cur.fetchall()[0][0] if num_not_matching == 0: - return True - return False + return + + raise Exception( + "Found users in database not native to %s!\n" + "You cannot changed a synapse server_name after it's been configured" + % (domain,) + ) + + +__all__ = ["DataStore", "check_database_before_upgrade"] diff --git a/synapse/storage/prepare_database.py b/synapse/storage/prepare_database.py index fc69c32a0a..6cb7d4b922 100644 --- a/synapse/storage/prepare_database.py +++ b/synapse/storage/prepare_database.py @@ -278,13 +278,17 @@ def _upgrade_existing_database( the current_version wasn't generated by applying those delta files. database_engine (DatabaseEngine) config (synapse.config.homeserver.HomeServerConfig|None): - application config, or None if we are connecting to an existing - database which we expect to be configured already + None if we are initialising a blank database, otherwise the application + config data_stores (list[str]): The names of the data stores to instantiate on the given database. is_empty (bool): Is this a blank database? I.e. do we need to run the upgrade portions of the delta scripts. """ + if is_empty: + assert not applied_delta_files + else: + assert config if current_version > SCHEMA_VERSION: raise ValueError( @@ -292,6 +296,13 @@ def _upgrade_existing_database( + "new for the server to understand" ) + # some of the deltas assume that config.server_name is set correctly, so now + # is a good time to run the sanity check. + if not is_empty and "main" in data_stores: + from synapse.storage.data_stores.main import check_database_before_upgrade + + check_database_before_upgrade(cur, database_engine, config) + start_ver = current_version if not upgraded: start_ver += 1 From 8c75b621bfe03725cc8da071516ebc66d3872760 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Wed, 26 Feb 2020 12:22:55 +0000 Subject: [PATCH 1112/1623] Ensure 'deactivated' parameter is a boolean on user admin API, Fix error handling of call to deactivate user (#6990) --- changelog.d/6990.bugfix | 1 + synapse/rest/admin/users.py | 11 +++--- synapse/rest/client/v1/login.py | 1 + tests/rest/admin/test_user.py | 59 +++++++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 changelog.d/6990.bugfix diff --git a/changelog.d/6990.bugfix b/changelog.d/6990.bugfix new file mode 100644 index 0000000000..8c1c48f4d4 --- /dev/null +++ b/changelog.d/6990.bugfix @@ -0,0 +1 @@ +Prevent user from setting 'deactivated' to anything other than a bool on the v2 PUT /users Admin API. \ No newline at end of file diff --git a/synapse/rest/admin/users.py b/synapse/rest/admin/users.py index 2107b5dc56..c5b461a236 100644 --- a/synapse/rest/admin/users.py +++ b/synapse/rest/admin/users.py @@ -228,13 +228,16 @@ class UserRestServletV2(RestServlet): ) if "deactivated" in body: - deactivate = bool(body["deactivated"]) + deactivate = body["deactivated"] + if not isinstance(deactivate, bool): + raise SynapseError( + 400, "'deactivated' parameter is not of type boolean" + ) + if deactivate and not user["deactivated"]: - result = await self.deactivate_account_handler.deactivate_account( + await self.deactivate_account_handler.deactivate_account( target_user.to_string(), False ) - if not result: - raise SynapseError(500, "Could not deactivate user") user = await self.admin_handler.get_user(target_user) return 200, user diff --git a/synapse/rest/client/v1/login.py b/synapse/rest/client/v1/login.py index 1294e080dc..2c99536678 100644 --- a/synapse/rest/client/v1/login.py +++ b/synapse/rest/client/v1/login.py @@ -599,6 +599,7 @@ class SSOAuthHandler(object): redirect_url = self._add_login_token_to_redirect_url( client_redirect_url, login_token ) + # Load page request.redirect(redirect_url) finish_request(request) diff --git a/tests/rest/admin/test_user.py b/tests/rest/admin/test_user.py index 490ce8f55d..cbe4a6a51f 100644 --- a/tests/rest/admin/test_user.py +++ b/tests/rest/admin/test_user.py @@ -507,3 +507,62 @@ class UserRestTestCase(unittest.HomeserverTestCase): self.assertEqual(1, channel.json_body["admin"]) self.assertEqual(0, channel.json_body["is_guest"]) self.assertEqual(1, channel.json_body["deactivated"]) + + def test_accidental_deactivation_prevention(self): + """ + Ensure an account can't accidentally be deactivated by using a str value + for the deactivated body parameter + """ + self.hs.config.registration_shared_secret = None + + # Create user + body = json.dumps({"password": "abc123"}) + + request, channel = self.make_request( + "PUT", + self.url, + access_token=self.admin_user_tok, + content=body.encode(encoding="utf_8"), + ) + self.render(request) + + self.assertEqual(201, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("@bob:test", channel.json_body["name"]) + self.assertEqual("bob", channel.json_body["displayname"]) + + # Get user + request, channel = self.make_request( + "GET", self.url, access_token=self.admin_user_tok, + ) + self.render(request) + + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("@bob:test", channel.json_body["name"]) + self.assertEqual("bob", channel.json_body["displayname"]) + self.assertEqual(0, channel.json_body["deactivated"]) + + # Change password (and use a str for deactivate instead of a bool) + body = json.dumps({"password": "abc123", "deactivated": "false"}) # oops! + + request, channel = self.make_request( + "PUT", + self.url, + access_token=self.admin_user_tok, + content=body.encode(encoding="utf_8"), + ) + self.render(request) + + self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + + # Check user is not deactivated + request, channel = self.make_request( + "GET", self.url, access_token=self.admin_user_tok, + ) + self.render(request) + + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("@bob:test", channel.json_body["name"]) + self.assertEqual("bob", channel.json_body["displayname"]) + + # Ensure they're still alive + self.assertEqual(0, channel.json_body["deactivated"]) From 7728d87fd7e1af17dd6b0c619cbfecb1fadb624f Mon Sep 17 00:00:00 2001 From: Uday Bansal <43824981+udaybansal19@users.noreply.github.com> Date: Wed, 26 Feb 2020 20:47:03 +0530 Subject: [PATCH 1113/1623] Updated warning for incorrect database collation/ctype (#6985) Signed-off-by: Uday Bansal <43824981+udaybansal19@users.noreply.github.com> --- changelog.d/6985.misc | 1 + synapse/storage/engines/postgres.py | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 changelog.d/6985.misc diff --git a/changelog.d/6985.misc b/changelog.d/6985.misc new file mode 100644 index 0000000000..ba367fa9af --- /dev/null +++ b/changelog.d/6985.misc @@ -0,0 +1 @@ +Update warning for incorrect database collation/ctype to include link to documentation. diff --git a/synapse/storage/engines/postgres.py b/synapse/storage/engines/postgres.py index a077345960..53b3f372b0 100644 --- a/synapse/storage/engines/postgres.py +++ b/synapse/storage/engines/postgres.py @@ -53,7 +53,7 @@ class PostgresEngine(object): if rows and rows[0][0] != "UTF8": raise IncorrectDatabaseSetup( "Database has incorrect encoding: '%s' instead of 'UTF8'\n" - "See docs/postgres.rst for more information." % (rows[0][0],) + "See docs/postgres.md for more information." % (rows[0][0],) ) txn.execute( @@ -62,12 +62,16 @@ class PostgresEngine(object): collation, ctype = txn.fetchone() if collation != "C": logger.warning( - "Database has incorrect collation of %r. Should be 'C'", collation + "Database has incorrect collation of %r. Should be 'C'\n" + "See docs/postgres.md for more information.", + collation, ) if ctype != "C": logger.warning( - "Database has incorrect ctype of %r. Should be 'C'", ctype + "Database has incorrect ctype of %r. Should be 'C'\n" + "See docs/postgres.md for more information.", + ctype, ) def check_new_database(self, txn): From 1f773eec912e4908ab60f7823f5c0a024261af4d Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 26 Feb 2020 15:33:26 +0000 Subject: [PATCH 1114/1623] Port PresenceHandler to async/await (#6991) --- changelog.d/6991.misc | 1 + synapse/handlers/message.py | 5 +- synapse/handlers/presence.py | 192 +++++++++++++--------------- synapse/replication/tcp/resource.py | 6 +- synapse/server.pyi | 5 + tests/handlers/test_presence.py | 18 ++- tox.ini | 1 + 7 files changed, 113 insertions(+), 115 deletions(-) create mode 100644 changelog.d/6991.misc diff --git a/changelog.d/6991.misc b/changelog.d/6991.misc new file mode 100644 index 0000000000..5130f4e8af --- /dev/null +++ b/changelog.d/6991.misc @@ -0,0 +1 @@ +Port `synapse.handlers.presence` to async/await. diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index d6be280952..a0103addd3 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -1016,11 +1016,10 @@ class EventCreationHandler(object): # matters as sometimes presence code can take a while. run_in_background(self._bump_active_time, requester.user) - @defer.inlineCallbacks - def _bump_active_time(self, user): + async def _bump_active_time(self, user): try: presence = self.hs.get_presence_handler() - yield presence.bump_presence_active_time(user) + await presence.bump_presence_active_time(user) except Exception: logger.exception("Error bumping presence active time") diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py index 0d6cf2b008..5526015ddb 100644 --- a/synapse/handlers/presence.py +++ b/synapse/handlers/presence.py @@ -24,11 +24,12 @@ The methods that define policy are: import logging from contextlib import contextmanager -from typing import Dict, Set +from typing import Dict, List, Set from six import iteritems, itervalues from prometheus_client import Counter +from typing_extensions import ContextManager from twisted.internet import defer @@ -42,10 +43,14 @@ from synapse.metrics.background_process_metrics import run_as_background_process from synapse.storage.presence import UserPresenceState from synapse.types import UserID, get_domain_from_id from synapse.util.async_helpers import Linearizer -from synapse.util.caches.descriptors import cachedInlineCallbacks +from synapse.util.caches.descriptors import cached from synapse.util.metrics import Measure from synapse.util.wheel_timer import WheelTimer +MYPY = False +if MYPY: + import synapse.server + logger = logging.getLogger(__name__) @@ -97,7 +102,6 @@ assert LAST_ACTIVE_GRANULARITY < IDLE_TIMER class PresenceHandler(object): def __init__(self, hs: "synapse.server.HomeServer"): self.hs = hs - self.is_mine = hs.is_mine self.is_mine_id = hs.is_mine_id self.server_name = hs.hostname self.clock = hs.get_clock() @@ -150,7 +154,7 @@ class PresenceHandler(object): # Set of users who have presence in the `user_to_current_state` that # have not yet been persisted - self.unpersisted_users_changes = set() + self.unpersisted_users_changes = set() # type: Set[str] hs.get_reactor().addSystemEventTrigger( "before", @@ -160,12 +164,11 @@ class PresenceHandler(object): self._on_shutdown, ) - self.serial_to_user = {} self._next_serial = 1 # Keeps track of the number of *ongoing* syncs on this process. While # this is non zero a user will never go offline. - self.user_to_num_current_syncs = {} + self.user_to_num_current_syncs = {} # type: Dict[str, int] # Keeps track of the number of *ongoing* syncs on other processes. # While any sync is ongoing on another process the user will never @@ -213,8 +216,7 @@ class PresenceHandler(object): self._event_pos = self.store.get_current_events_token() self._event_processing = False - @defer.inlineCallbacks - def _on_shutdown(self): + async def _on_shutdown(self): """Gets called when shutting down. This lets us persist any updates that we haven't yet persisted, e.g. updates that only changes some internal timers. This allows changes to persist across startup without having to @@ -235,7 +237,7 @@ class PresenceHandler(object): if self.unpersisted_users_changes: - yield self.store.update_presence( + await self.store.update_presence( [ self.user_to_current_state[user_id] for user_id in self.unpersisted_users_changes @@ -243,8 +245,7 @@ class PresenceHandler(object): ) logger.info("Finished _on_shutdown") - @defer.inlineCallbacks - def _persist_unpersisted_changes(self): + async def _persist_unpersisted_changes(self): """We periodically persist the unpersisted changes, as otherwise they may stack up and slow down shutdown times. """ @@ -253,12 +254,11 @@ class PresenceHandler(object): if unpersisted: logger.info("Persisting %d unpersisted presence updates", len(unpersisted)) - yield self.store.update_presence( + await self.store.update_presence( [self.user_to_current_state[user_id] for user_id in unpersisted] ) - @defer.inlineCallbacks - def _update_states(self, new_states): + async def _update_states(self, new_states): """Updates presence of users. Sets the appropriate timeouts. Pokes the notifier and federation if and only if the changed presence state should be sent to clients/servers. @@ -267,7 +267,7 @@ class PresenceHandler(object): with Measure(self.clock, "presence_update_states"): - # NOTE: We purposefully don't yield between now and when we've + # NOTE: We purposefully don't await between now and when we've # calculated what we want to do with the new states, to avoid races. to_notify = {} # Changes we want to notify everyone about @@ -311,7 +311,7 @@ class PresenceHandler(object): if to_notify: notified_presence_counter.inc(len(to_notify)) - yield self._persist_and_notify(list(to_notify.values())) + await self._persist_and_notify(list(to_notify.values())) self.unpersisted_users_changes |= {s.user_id for s in new_states} self.unpersisted_users_changes -= set(to_notify.keys()) @@ -326,7 +326,7 @@ class PresenceHandler(object): self._push_to_remotes(to_federation_ping.values()) - def _handle_timeouts(self): + async def _handle_timeouts(self): """Checks the presence of users that have timed out and updates as appropriate. """ @@ -368,10 +368,9 @@ class PresenceHandler(object): now=now, ) - return self._update_states(changes) + return await self._update_states(changes) - @defer.inlineCallbacks - def bump_presence_active_time(self, user): + async def bump_presence_active_time(self, user): """We've seen the user do something that indicates they're interacting with the app. """ @@ -383,16 +382,17 @@ class PresenceHandler(object): bump_active_time_counter.inc() - prev_state = yield self.current_state_for_user(user_id) + prev_state = await self.current_state_for_user(user_id) new_fields = {"last_active_ts": self.clock.time_msec()} if prev_state.state == PresenceState.UNAVAILABLE: new_fields["state"] = PresenceState.ONLINE - yield self._update_states([prev_state.copy_and_replace(**new_fields)]) + await self._update_states([prev_state.copy_and_replace(**new_fields)]) - @defer.inlineCallbacks - def user_syncing(self, user_id, affect_presence=True): + async def user_syncing( + self, user_id: str, affect_presence: bool = True + ) -> ContextManager[None]: """Returns a context manager that should surround any stream requests from the user. @@ -415,11 +415,11 @@ class PresenceHandler(object): curr_sync = self.user_to_num_current_syncs.get(user_id, 0) self.user_to_num_current_syncs[user_id] = curr_sync + 1 - prev_state = yield self.current_state_for_user(user_id) + prev_state = await self.current_state_for_user(user_id) if prev_state.state == PresenceState.OFFLINE: # If they're currently offline then bring them online, otherwise # just update the last sync times. - yield self._update_states( + await self._update_states( [ prev_state.copy_and_replace( state=PresenceState.ONLINE, @@ -429,7 +429,7 @@ class PresenceHandler(object): ] ) else: - yield self._update_states( + await self._update_states( [ prev_state.copy_and_replace( last_user_sync_ts=self.clock.time_msec() @@ -437,13 +437,12 @@ class PresenceHandler(object): ] ) - @defer.inlineCallbacks - def _end(): + async def _end(): try: self.user_to_num_current_syncs[user_id] -= 1 - prev_state = yield self.current_state_for_user(user_id) - yield self._update_states( + prev_state = await self.current_state_for_user(user_id) + await self._update_states( [ prev_state.copy_and_replace( last_user_sync_ts=self.clock.time_msec() @@ -480,8 +479,7 @@ class PresenceHandler(object): else: return set() - @defer.inlineCallbacks - def update_external_syncs_row( + async def update_external_syncs_row( self, process_id, user_id, is_syncing, sync_time_msec ): """Update the syncing users for an external process as a delta. @@ -494,8 +492,8 @@ class PresenceHandler(object): is_syncing (bool): Whether or not the user is now syncing sync_time_msec(int): Time in ms when the user was last syncing """ - with (yield self.external_sync_linearizer.queue(process_id)): - prev_state = yield self.current_state_for_user(user_id) + with (await self.external_sync_linearizer.queue(process_id)): + prev_state = await self.current_state_for_user(user_id) process_presence = self.external_process_to_current_syncs.setdefault( process_id, set() @@ -525,25 +523,24 @@ class PresenceHandler(object): process_presence.discard(user_id) if updates: - yield self._update_states(updates) + await self._update_states(updates) self.external_process_last_updated_ms[process_id] = self.clock.time_msec() - @defer.inlineCallbacks - def update_external_syncs_clear(self, process_id): + async def update_external_syncs_clear(self, process_id): """Marks all users that had been marked as syncing by a given process as offline. Used when the process has stopped/disappeared. """ - with (yield self.external_sync_linearizer.queue(process_id)): + with (await self.external_sync_linearizer.queue(process_id)): process_presence = self.external_process_to_current_syncs.pop( process_id, set() ) - prev_states = yield self.current_state_for_users(process_presence) + prev_states = await self.current_state_for_users(process_presence) time_now_ms = self.clock.time_msec() - yield self._update_states( + await self._update_states( [ prev_state.copy_and_replace(last_user_sync_ts=time_now_ms) for prev_state in itervalues(prev_states) @@ -551,15 +548,13 @@ class PresenceHandler(object): ) self.external_process_last_updated_ms.pop(process_id, None) - @defer.inlineCallbacks - def current_state_for_user(self, user_id): + async def current_state_for_user(self, user_id): """Get the current presence state for a user. """ - res = yield self.current_state_for_users([user_id]) + res = await self.current_state_for_users([user_id]) return res[user_id] - @defer.inlineCallbacks - def current_state_for_users(self, user_ids): + async def current_state_for_users(self, user_ids): """Get the current presence state for multiple users. Returns: @@ -574,7 +569,7 @@ class PresenceHandler(object): if missing: # There are things not in our in memory cache. Lets pull them out of # the database. - res = yield self.store.get_presence_for_users(missing) + res = await self.store.get_presence_for_users(missing) states.update(res) missing = [user_id for user_id, state in iteritems(states) if not state] @@ -587,14 +582,13 @@ class PresenceHandler(object): return states - @defer.inlineCallbacks - def _persist_and_notify(self, states): + async def _persist_and_notify(self, states): """Persist states in the database, poke the notifier and send to interested remote servers """ - stream_id, max_token = yield self.store.update_presence(states) + stream_id, max_token = await self.store.update_presence(states) - parties = yield get_interested_parties(self.store, states) + parties = await get_interested_parties(self.store, states) room_ids_to_states, users_to_states = parties self.notifier.on_new_event( @@ -606,9 +600,8 @@ class PresenceHandler(object): self._push_to_remotes(states) - @defer.inlineCallbacks - def notify_for_states(self, state, stream_id): - parties = yield get_interested_parties(self.store, [state]) + async def notify_for_states(self, state, stream_id): + parties = await get_interested_parties(self.store, [state]) room_ids_to_states, users_to_states = parties self.notifier.on_new_event( @@ -626,8 +619,7 @@ class PresenceHandler(object): """ self.federation.send_presence(states) - @defer.inlineCallbacks - def incoming_presence(self, origin, content): + async def incoming_presence(self, origin, content): """Called when we receive a `m.presence` EDU from a remote server. """ now = self.clock.time_msec() @@ -670,21 +662,19 @@ class PresenceHandler(object): new_fields["status_msg"] = push.get("status_msg", None) new_fields["currently_active"] = push.get("currently_active", False) - prev_state = yield self.current_state_for_user(user_id) + prev_state = await self.current_state_for_user(user_id) updates.append(prev_state.copy_and_replace(**new_fields)) if updates: federation_presence_counter.inc(len(updates)) - yield self._update_states(updates) + await self._update_states(updates) - @defer.inlineCallbacks - def get_state(self, target_user, as_event=False): - results = yield self.get_states([target_user.to_string()], as_event=as_event) + async def get_state(self, target_user, as_event=False): + results = await self.get_states([target_user.to_string()], as_event=as_event) return results[0] - @defer.inlineCallbacks - def get_states(self, target_user_ids, as_event=False): + async def get_states(self, target_user_ids, as_event=False): """Get the presence state for users. Args: @@ -695,7 +685,7 @@ class PresenceHandler(object): list """ - updates = yield self.current_state_for_users(target_user_ids) + updates = await self.current_state_for_users(target_user_ids) updates = list(updates.values()) for user_id in set(target_user_ids) - {u.user_id for u in updates}: @@ -713,8 +703,7 @@ class PresenceHandler(object): else: return updates - @defer.inlineCallbacks - def set_state(self, target_user, state, ignore_status_msg=False): + async def set_state(self, target_user, state, ignore_status_msg=False): """Set the presence state of the user. """ status_msg = state.get("status_msg", None) @@ -730,7 +719,7 @@ class PresenceHandler(object): user_id = target_user.to_string() - prev_state = yield self.current_state_for_user(user_id) + prev_state = await self.current_state_for_user(user_id) new_fields = {"state": presence} @@ -741,16 +730,15 @@ class PresenceHandler(object): if presence == PresenceState.ONLINE: new_fields["last_active_ts"] = self.clock.time_msec() - yield self._update_states([prev_state.copy_and_replace(**new_fields)]) + await self._update_states([prev_state.copy_and_replace(**new_fields)]) - @defer.inlineCallbacks - def is_visible(self, observed_user, observer_user): + async def is_visible(self, observed_user, observer_user): """Returns whether a user can see another user's presence. """ - observer_room_ids = yield self.store.get_rooms_for_user( + observer_room_ids = await self.store.get_rooms_for_user( observer_user.to_string() ) - observed_room_ids = yield self.store.get_rooms_for_user( + observed_room_ids = await self.store.get_rooms_for_user( observed_user.to_string() ) @@ -759,8 +747,7 @@ class PresenceHandler(object): return False - @defer.inlineCallbacks - def get_all_presence_updates(self, last_id, current_id): + async def get_all_presence_updates(self, last_id, current_id): """ Gets a list of presence update rows from between the given stream ids. Each row has: @@ -775,7 +762,7 @@ class PresenceHandler(object): """ # TODO(markjh): replicate the unpersisted changes. # This could use the in-memory stores for recent changes. - rows = yield self.store.get_all_presence_updates(last_id, current_id) + rows = await self.store.get_all_presence_updates(last_id, current_id) return rows def notify_new_event(self): @@ -786,20 +773,18 @@ class PresenceHandler(object): if self._event_processing: return - @defer.inlineCallbacks - def _process_presence(): + async def _process_presence(): assert not self._event_processing self._event_processing = True try: - yield self._unsafe_process() + await self._unsafe_process() finally: self._event_processing = False run_as_background_process("presence.notify_new_event", _process_presence) - @defer.inlineCallbacks - def _unsafe_process(self): + async def _unsafe_process(self): # Loop round handling deltas until we're up to date while True: with Measure(self.clock, "presence_delta"): @@ -812,10 +797,10 @@ class PresenceHandler(object): self._event_pos, room_max_stream_ordering, ) - max_pos, deltas = yield self.store.get_current_state_deltas( + max_pos, deltas = await self.store.get_current_state_deltas( self._event_pos, room_max_stream_ordering ) - yield self._handle_state_delta(deltas) + await self._handle_state_delta(deltas) self._event_pos = max_pos @@ -824,8 +809,7 @@ class PresenceHandler(object): max_pos ) - @defer.inlineCallbacks - def _handle_state_delta(self, deltas): + async def _handle_state_delta(self, deltas): """Process current state deltas to find new joins that need to be handled. """ @@ -846,13 +830,13 @@ class PresenceHandler(object): # joins. continue - event = yield self.store.get_event(event_id, allow_none=True) + event = await self.store.get_event(event_id, allow_none=True) if not event or event.content.get("membership") != Membership.JOIN: # We only care about joins continue if prev_event_id: - prev_event = yield self.store.get_event(prev_event_id, allow_none=True) + prev_event = await self.store.get_event(prev_event_id, allow_none=True) if ( prev_event and prev_event.content.get("membership") == Membership.JOIN @@ -860,10 +844,9 @@ class PresenceHandler(object): # Ignore changes to join events. continue - yield self._on_user_joined_room(room_id, state_key) + await self._on_user_joined_room(room_id, state_key) - @defer.inlineCallbacks - def _on_user_joined_room(self, room_id, user_id): + async def _on_user_joined_room(self, room_id, user_id): """Called when we detect a user joining the room via the current state delta stream. @@ -882,8 +865,8 @@ class PresenceHandler(object): # TODO: We should be able to filter the hosts down to those that # haven't previously seen the user - state = yield self.current_state_for_user(user_id) - hosts = yield self.state.get_current_hosts_in_room(room_id) + state = await self.current_state_for_user(user_id) + hosts = await self.state.get_current_hosts_in_room(room_id) # Filter out ourselves. hosts = {host for host in hosts if host != self.server_name} @@ -903,10 +886,10 @@ class PresenceHandler(object): # TODO: Check that this is actually a new server joining the # room. - user_ids = yield self.state.get_current_users_in_room(room_id) + user_ids = await self.state.get_current_users_in_room(room_id) user_ids = list(filter(self.is_mine_id, user_ids)) - states = yield self.current_state_for_users(user_ids) + states = await self.current_state_for_users(user_ids) # Filter out old presence, i.e. offline presence states where # the user hasn't been active for a week. We can change this @@ -996,9 +979,8 @@ class PresenceEventSource(object): self.store = hs.get_datastore() self.state = hs.get_state_handler() - @defer.inlineCallbacks @log_function - def get_new_events( + async def get_new_events( self, user, from_key, @@ -1045,7 +1027,7 @@ class PresenceEventSource(object): presence = self.get_presence_handler() stream_change_cache = self.store.presence_stream_cache - users_interested_in = yield self._get_interested_in(user, explicit_room_id) + users_interested_in = await self._get_interested_in(user, explicit_room_id) user_ids_changed = set() changed = None @@ -1071,7 +1053,7 @@ class PresenceEventSource(object): else: user_ids_changed = users_interested_in - updates = yield presence.current_state_for_users(user_ids_changed) + updates = await presence.current_state_for_users(user_ids_changed) if include_offline: return (list(updates.values()), max_token) @@ -1084,11 +1066,11 @@ class PresenceEventSource(object): def get_current_key(self): return self.store.get_current_presence_token() - def get_pagination_rows(self, user, pagination_config, key): - return self.get_new_events(user, from_key=None, include_offline=False) + async def get_pagination_rows(self, user, pagination_config, key): + return await self.get_new_events(user, from_key=None, include_offline=False) - @cachedInlineCallbacks(num_args=2, cache_context=True) - def _get_interested_in(self, user, explicit_room_id, cache_context): + @cached(num_args=2, cache_context=True) + async def _get_interested_in(self, user, explicit_room_id, cache_context): """Returns the set of users that the given user should see presence updates for """ @@ -1096,13 +1078,13 @@ class PresenceEventSource(object): users_interested_in = set() users_interested_in.add(user_id) # So that we receive our own presence - users_who_share_room = yield self.store.get_users_who_share_room_with_user( + users_who_share_room = await self.store.get_users_who_share_room_with_user( user_id, on_invalidate=cache_context.invalidate ) users_interested_in.update(users_who_share_room) if explicit_room_id: - user_ids = yield self.store.get_users_in_room( + user_ids = await self.store.get_users_in_room( explicit_room_id, on_invalidate=cache_context.invalidate ) users_interested_in.update(user_ids) @@ -1277,8 +1259,8 @@ def get_interested_parties(store, states): 2-tuple: `(room_ids_to_states, users_to_states)`, with each item being a dict of `entity_name` -> `[UserPresenceState]` """ - room_ids_to_states = {} - users_to_states = {} + room_ids_to_states = {} # type: Dict[str, List[UserPresenceState]] + users_to_states = {} # type: Dict[str, List[UserPresenceState]] for state in states: room_ids = yield store.get_rooms_for_user(state.user_id) for room_id in room_ids: diff --git a/synapse/replication/tcp/resource.py b/synapse/replication/tcp/resource.py index ce60ae2e07..ce9d1fae12 100644 --- a/synapse/replication/tcp/resource.py +++ b/synapse/replication/tcp/resource.py @@ -323,7 +323,11 @@ class ReplicationStreamer(object): # We need to tell the presence handler that the connection has been # lost so that it can handle any ongoing syncs on that connection. - self.presence_handler.update_external_syncs_clear(connection.conn_id) + run_as_background_process( + "update_external_syncs_clear", + self.presence_handler.update_external_syncs_clear, + connection.conn_id, + ) def _batch_updates(updates): diff --git a/synapse/server.pyi b/synapse/server.pyi index 40eabfe5d9..3844f0e12f 100644 --- a/synapse/server.pyi +++ b/synapse/server.pyi @@ -3,6 +3,7 @@ import twisted.internet import synapse.api.auth import synapse.config.homeserver import synapse.crypto.keyring +import synapse.federation.federation_server import synapse.federation.sender import synapse.federation.transport.client import synapse.handlers @@ -107,5 +108,9 @@ class HomeServer(object): self, ) -> synapse.replication.tcp.client.ReplicationClientHandler: pass + def get_federation_registry( + self, + ) -> synapse.federation.federation_server.FederationHandlerRegistry: + pass def is_mine_id(self, domain_id: str) -> bool: pass diff --git a/tests/handlers/test_presence.py b/tests/handlers/test_presence.py index 64915bafcd..05ea40a7de 100644 --- a/tests/handlers/test_presence.py +++ b/tests/handlers/test_presence.py @@ -494,8 +494,10 @@ class PresenceJoinTestCase(unittest.HomeserverTestCase): self.helper.join(room_id, "@test2:server") # Mark test2 as online, test will be offline with a last_active of 0 - self.presence_handler.set_state( - UserID.from_string("@test2:server"), {"presence": PresenceState.ONLINE} + self.get_success( + self.presence_handler.set_state( + UserID.from_string("@test2:server"), {"presence": PresenceState.ONLINE} + ) ) self.reactor.pump([0]) # Wait for presence updates to be handled @@ -543,14 +545,18 @@ class PresenceJoinTestCase(unittest.HomeserverTestCase): room_id = self.helper.create_room_as(self.user_id) # Mark test as online - self.presence_handler.set_state( - UserID.from_string("@test:server"), {"presence": PresenceState.ONLINE} + self.get_success( + self.presence_handler.set_state( + UserID.from_string("@test:server"), {"presence": PresenceState.ONLINE} + ) ) # Mark test2 as online, test will be offline with a last_active of 0. # Note we don't join them to the room yet - self.presence_handler.set_state( - UserID.from_string("@test2:server"), {"presence": PresenceState.ONLINE} + self.get_success( + self.presence_handler.set_state( + UserID.from_string("@test2:server"), {"presence": PresenceState.ONLINE} + ) ) # Add servers to the room diff --git a/tox.ini b/tox.ini index b715ea0bff..4ccfde01b5 100644 --- a/tox.ini +++ b/tox.ini @@ -183,6 +183,7 @@ commands = mypy \ synapse/events/spamcheck.py \ synapse/federation/sender \ synapse/federation/transport \ + synapse/handlers/presence.py \ synapse/handlers/sync.py \ synapse/handlers/ui_auth \ synapse/logging/ \ From 380122866f8cf7b891c95f10a60c83537ef6c780 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 26 Feb 2020 11:32:13 -0500 Subject: [PATCH 1115/1623] Cast a coroutine into a Deferred in the federation base (#6996) Properly convert a coroutine into a Deferred in federation_base to fix an error when joining a room. --- changelog.d/6996.bugfix | 1 + synapse/federation/federation_base.py | 14 ++++++++------ 2 files changed, 9 insertions(+), 6 deletions(-) create mode 100644 changelog.d/6996.bugfix diff --git a/changelog.d/6996.bugfix b/changelog.d/6996.bugfix new file mode 100644 index 0000000000..765d376c7c --- /dev/null +++ b/changelog.d/6996.bugfix @@ -0,0 +1 @@ +Fix bug which caused an error when joining a room, with `'coroutine' object has no attribute 'event_id'`. diff --git a/synapse/federation/federation_base.py b/synapse/federation/federation_base.py index eea64c1c9f..9fff65716a 100644 --- a/synapse/federation/federation_base.py +++ b/synapse/federation/federation_base.py @@ -96,12 +96,14 @@ class FederationBase(object): if not res and pdu.origin != origin: try: - res = yield self.get_pdu( - destinations=[pdu.origin], - event_id=pdu.event_id, - room_version=room_version, - outlier=outlier, - timeout=10000, + res = yield defer.ensureDeferred( + self.get_pdu( + destinations=[pdu.origin], + event_id=pdu.event_id, + room_version=room_version, + outlier=outlier, + timeout=10000, + ) ) except SynapseError: pass From 3e99528f2bfaa686c4708fb8efcddce935b2397d Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 26 Feb 2020 16:58:33 +0000 Subject: [PATCH 1116/1623] Store room version on invite (#6983) When we get an invite over federation, store the room version in the rooms table. The general idea here is that, when we pull the invite out again, we'll want to know what room_version it belongs to (so that we can later redact it if need be). So we need to store it somewhere... --- changelog.d/6983.misc | 1 + synapse/handlers/federation.py | 12 ++++++++ synapse/replication/http/_base.py | 2 +- synapse/replication/http/federation.py | 36 +++++++++++++++++++++++- synapse/storage/data_stores/main/room.py | 20 +++++++++++++ tests/app/test_openid_listener.py | 8 ++++++ tests/handlers/test_typing.py | 1 + 7 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 changelog.d/6983.misc diff --git a/changelog.d/6983.misc b/changelog.d/6983.misc new file mode 100644 index 0000000000..08aa80bcd9 --- /dev/null +++ b/changelog.d/6983.misc @@ -0,0 +1 @@ +Refactoring work in preparation for changing the event redaction algorithm. diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index c2e6ee266d..38ab6a8fc3 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -60,6 +60,7 @@ from synapse.replication.http.devices import ReplicationUserDevicesResyncRestSer from synapse.replication.http.federation import ( ReplicationCleanRoomRestServlet, ReplicationFederationSendEventsRestServlet, + ReplicationStoreRoomOnInviteRestServlet, ) from synapse.replication.http.membership import ReplicationUserJoinedLeftRoomRestServlet from synapse.state import StateResolutionStore, resolve_events_with_store @@ -160,8 +161,12 @@ class FederationHandler(BaseHandler): self._user_device_resync = ReplicationUserDevicesResyncRestServlet.make_client( hs ) + self._maybe_store_room_on_invite = ReplicationStoreRoomOnInviteRestServlet.make_client( + hs + ) else: self._device_list_updater = hs.get_device_handler().device_list_updater + self._maybe_store_room_on_invite = self.store.maybe_store_room_on_invite # When joining a room we need to queue any events for that room up self.room_queues = {} @@ -1537,6 +1542,13 @@ class FederationHandler(BaseHandler): if event.state_key == self._server_notices_mxid: raise SynapseError(http_client.FORBIDDEN, "Cannot invite this user") + # keep a record of the room version, if we don't yet know it. + # (this may get overwritten if we later get a different room version in a + # join dance). + await self._maybe_store_room_on_invite( + room_id=event.room_id, room_version=room_version + ) + event.internal_metadata.outlier = True event.internal_metadata.out_of_band_membership = True diff --git a/synapse/replication/http/_base.py b/synapse/replication/http/_base.py index 444eb7b7f4..1be1ccbdf3 100644 --- a/synapse/replication/http/_base.py +++ b/synapse/replication/http/_base.py @@ -44,7 +44,7 @@ class ReplicationEndpoint(object): """Helper base class for defining new replication HTTP endpoints. This creates an endpoint under `/_synapse/replication/:NAME/:PATH_ARGS..` - (with an `/:txn_id` prefix for cached requests.), where NAME is a name, + (with a `/:txn_id` suffix for cached requests), where NAME is a name, PATH_ARGS are a tuple of parameters to be encoded in the URL. For example, if `NAME` is "send_event" and `PATH_ARGS` is `("event_id",)`, diff --git a/synapse/replication/http/federation.py b/synapse/replication/http/federation.py index 49a3251372..8794720101 100644 --- a/synapse/replication/http/federation.py +++ b/synapse/replication/http/federation.py @@ -17,6 +17,7 @@ import logging from twisted.internet import defer +from synapse.api.room_versions import KNOWN_ROOM_VERSIONS from synapse.events import event_type_from_format_version from synapse.events.snapshot import EventContext from synapse.http.servlet import parse_json_object_from_request @@ -211,7 +212,7 @@ class ReplicationCleanRoomRestServlet(ReplicationEndpoint): Request format: - POST /_synapse/replication/fed_query/:fed_cleanup_room/:txn_id + POST /_synapse/replication/fed_cleanup_room/:room_id/:txn_id {} """ @@ -238,8 +239,41 @@ class ReplicationCleanRoomRestServlet(ReplicationEndpoint): return 200, {} +class ReplicationStoreRoomOnInviteRestServlet(ReplicationEndpoint): + """Called to clean up any data in DB for a given room, ready for the + server to join the room. + + Request format: + + POST /_synapse/replication/store_room_on_invite/:room_id/:txn_id + + { + "room_version": "1", + } + """ + + NAME = "store_room_on_invite" + PATH_ARGS = ("room_id",) + + def __init__(self, hs): + super().__init__(hs) + + self.store = hs.get_datastore() + + @staticmethod + def _serialize_payload(room_id, room_version): + return {"room_version": room_version.identifier} + + async def _handle_request(self, request, room_id): + content = parse_json_object_from_request(request) + room_version = KNOWN_ROOM_VERSIONS[content["room_version"]] + await self.store.maybe_store_room_on_invite(room_id, room_version) + return 200, {} + + def register_servlets(hs, http_server): ReplicationFederationSendEventsRestServlet(hs).register(http_server) ReplicationFederationSendEduRestServlet(hs).register(http_server) ReplicationGetQueryRestServlet(hs).register(http_server) ReplicationCleanRoomRestServlet(hs).register(http_server) + ReplicationStoreRoomOnInviteRestServlet(hs).register(http_server) diff --git a/synapse/storage/data_stores/main/room.py b/synapse/storage/data_stores/main/room.py index 70137dfbe4..e6c10c6316 100644 --- a/synapse/storage/data_stores/main/room.py +++ b/synapse/storage/data_stores/main/room.py @@ -1020,6 +1020,26 @@ class RoomStore(RoomBackgroundUpdateStore, RoomWorkerStore, SearchStore): logger.error("store_room with room_id=%s failed: %s", room_id, e) raise StoreError(500, "Problem creating room.") + async def maybe_store_room_on_invite(self, room_id: str, room_version: RoomVersion): + """ + When we receive an invite over federation, store the version of the room if we + don't already know the room version. + """ + await self.db.simple_upsert( + desc="maybe_store_room_on_invite", + table="rooms", + keyvalues={"room_id": room_id}, + values={}, + insertion_values={ + "room_version": room_version.identifier, + "is_public": False, + "creator": "", + }, + # rooms has a unique constraint on room_id, so no need to lock when doing an + # emulated upsert. + lock=False, + ) + @defer.inlineCallbacks def set_room_is_public(self, room_id, is_public): def set_room_is_public_txn(txn, next_id): diff --git a/tests/app/test_openid_listener.py b/tests/app/test_openid_listener.py index 1fe048048b..89fcc3889a 100644 --- a/tests/app/test_openid_listener.py +++ b/tests/app/test_openid_listener.py @@ -29,6 +29,14 @@ class FederationReaderOpenIDListenerTests(HomeserverTestCase): ) return hs + def default_config(self, name="test"): + conf = super().default_config(name) + # we're using FederationReaderServer, which uses a SlavedStore, so we + # have to tell the FederationHandler not to try to access stuff that is only + # in the primary store. + conf["worker_app"] = "yes" + return conf + @parameterized.expand( [ (["federation"], "auth_fail"), diff --git a/tests/handlers/test_typing.py b/tests/handlers/test_typing.py index 07b204666e..51e2b37218 100644 --- a/tests/handlers/test_typing.py +++ b/tests/handlers/test_typing.py @@ -74,6 +74,7 @@ class TypingNotificationsTestCase(unittest.HomeserverTestCase): "set_received_txn_response", "get_destination_retry_timings", "get_devices_by_remote", + "maybe_store_room_on_invite", # Bits that user_directory needs "get_user_directory_stream_pos", "get_current_state_deltas", From 132b673dbefa42eb7669a11522426f26e225ac05 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 27 Feb 2020 11:53:40 +0000 Subject: [PATCH 1117/1623] Add some type annotations in `synapse.storage` (#6987) I cracked, and added some type definitions in synapse.storage. --- changelog.d/6987.misc | 1 + synapse/storage/database.py | 143 ++++++++++++++++------------ synapse/storage/engines/__init__.py | 28 +++--- synapse/storage/engines/_base.py | 87 +++++++++++++++++ synapse/storage/engines/postgres.py | 12 ++- synapse/storage/engines/sqlite.py | 13 ++- synapse/storage/types.py | 65 +++++++++++++ tox.ini | 5 +- 8 files changed, 270 insertions(+), 84 deletions(-) create mode 100644 changelog.d/6987.misc create mode 100644 synapse/storage/types.py diff --git a/changelog.d/6987.misc b/changelog.d/6987.misc new file mode 100644 index 0000000000..7ff74cda55 --- /dev/null +++ b/changelog.d/6987.misc @@ -0,0 +1 @@ +Add some type annotations to the database storage classes. diff --git a/synapse/storage/database.py b/synapse/storage/database.py index 1953614401..609db40616 100644 --- a/synapse/storage/database.py +++ b/synapse/storage/database.py @@ -15,9 +15,9 @@ # See the License for the specific language governing permissions and # limitations under the License. import logging -import sys import time -from typing import Iterable, Tuple +from time import monotonic as monotonic_time +from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional, Tuple from six import iteritems, iterkeys, itervalues from six.moves import intern, range @@ -32,24 +32,14 @@ from synapse.config.database import DatabaseConnectionConfig from synapse.logging.context import LoggingContext, make_deferred_yieldable from synapse.metrics.background_process_metrics import run_as_background_process from synapse.storage.background_updates import BackgroundUpdater -from synapse.storage.engines import PostgresEngine, Sqlite3Engine +from synapse.storage.engines import BaseDatabaseEngine, PostgresEngine, Sqlite3Engine +from synapse.storage.types import Connection, Cursor from synapse.util.stringutils import exception_to_unicode -# import a function which will return a monotonic time, in seconds -try: - # on python 3, use time.monotonic, since time.clock can go backwards - from time import monotonic as monotonic_time -except ImportError: - # ... but python 2 doesn't have it - from time import clock as monotonic_time - logger = logging.getLogger(__name__) -try: - MAX_TXN_ID = sys.maxint - 1 -except AttributeError: - # python 3 does not have a maximum int value - MAX_TXN_ID = 2 ** 63 - 1 +# python 3 does not have a maximum int value +MAX_TXN_ID = 2 ** 63 - 1 sql_logger = logging.getLogger("synapse.storage.SQL") transaction_logger = logging.getLogger("synapse.storage.txn") @@ -77,7 +67,7 @@ UNIQUE_INDEX_BACKGROUND_UPDATES = { def make_pool( - reactor, db_config: DatabaseConnectionConfig, engine + reactor, db_config: DatabaseConnectionConfig, engine: BaseDatabaseEngine ) -> adbapi.ConnectionPool: """Get the connection pool for the database. """ @@ -90,7 +80,9 @@ def make_pool( ) -def make_conn(db_config: DatabaseConnectionConfig, engine): +def make_conn( + db_config: DatabaseConnectionConfig, engine: BaseDatabaseEngine +) -> Connection: """Make a new connection to the database and return it. Returns: @@ -107,20 +99,27 @@ def make_conn(db_config: DatabaseConnectionConfig, engine): return db_conn -class LoggingTransaction(object): +# The type of entry which goes on our after_callbacks and exception_callbacks lists. +# +# Python 3.5.2 doesn't support Callable with an ellipsis, so we wrap it in quotes so +# that mypy sees the type but the runtime python doesn't. +_CallbackListEntry = Tuple["Callable[..., None]", Iterable[Any], Dict[str, Any]] + + +class LoggingTransaction: """An object that almost-transparently proxies for the 'txn' object passed to the constructor. Adds logging and metrics to the .execute() method. Args: txn: The database transcation object to wrap. - name (str): The name of this transactions for logging. - database_engine (Sqlite3Engine|PostgresEngine) - after_callbacks(list|None): A list that callbacks will be appended to + name: The name of this transactions for logging. + database_engine + after_callbacks: A list that callbacks will be appended to that have been added by `call_after` which should be run on successful completion of the transaction. None indicates that no callbacks should be allowed to be scheduled to run. - exception_callbacks(list|None): A list that callbacks will be appended + exception_callbacks: A list that callbacks will be appended to that have been added by `call_on_exception` which should be run if transaction ends with an error. None indicates that no callbacks should be allowed to be scheduled to run. @@ -135,46 +134,67 @@ class LoggingTransaction(object): ] def __init__( - self, txn, name, database_engine, after_callbacks=None, exception_callbacks=None + self, + txn: Cursor, + name: str, + database_engine: BaseDatabaseEngine, + after_callbacks: Optional[List[_CallbackListEntry]] = None, + exception_callbacks: Optional[List[_CallbackListEntry]] = None, ): - object.__setattr__(self, "txn", txn) - object.__setattr__(self, "name", name) - object.__setattr__(self, "database_engine", database_engine) - object.__setattr__(self, "after_callbacks", after_callbacks) - object.__setattr__(self, "exception_callbacks", exception_callbacks) + self.txn = txn + self.name = name + self.database_engine = database_engine + self.after_callbacks = after_callbacks + self.exception_callbacks = exception_callbacks - def call_after(self, callback, *args, **kwargs): + def call_after(self, callback: "Callable[..., None]", *args, **kwargs): """Call the given callback on the main twisted thread after the transaction has finished. Used to invalidate the caches on the correct thread. """ + # if self.after_callbacks is None, that means that whatever constructed the + # LoggingTransaction isn't expecting there to be any callbacks; assert that + # is not the case. + assert self.after_callbacks is not None self.after_callbacks.append((callback, args, kwargs)) - def call_on_exception(self, callback, *args, **kwargs): + def call_on_exception(self, callback: "Callable[..., None]", *args, **kwargs): + # if self.exception_callbacks is None, that means that whatever constructed the + # LoggingTransaction isn't expecting there to be any callbacks; assert that + # is not the case. + assert self.exception_callbacks is not None self.exception_callbacks.append((callback, args, kwargs)) - def __getattr__(self, name): - return getattr(self.txn, name) + def fetchall(self) -> List[Tuple]: + return self.txn.fetchall() - def __setattr__(self, name, value): - setattr(self.txn, name, value) + def fetchone(self) -> Tuple: + return self.txn.fetchone() - def __iter__(self): + def __iter__(self) -> Iterator[Tuple]: return self.txn.__iter__() + @property + def rowcount(self) -> int: + return self.txn.rowcount + + @property + def description(self) -> Any: + return self.txn.description + def execute_batch(self, sql, args): if isinstance(self.database_engine, PostgresEngine): - from psycopg2.extras import execute_batch + from psycopg2.extras import execute_batch # type: ignore self._do_execute(lambda *x: execute_batch(self.txn, *x), sql, args) else: for val in args: self.execute(sql, val) - def execute(self, sql, *args): + def execute(self, sql: str, *args: Any): self._do_execute(self.txn.execute, sql, *args) - def executemany(self, sql, *args): + def executemany(self, sql: str, *args: Any): self._do_execute(self.txn.executemany, sql, *args) def _make_sql_one_line(self, sql): @@ -207,6 +227,9 @@ class LoggingTransaction(object): sql_logger.debug("[SQL time] {%s} %f sec", self.name, secs) sql_query_timer.labels(sql.split()[0]).observe(secs) + def close(self): + self.txn.close() + class PerformanceCounters(object): def __init__(self): @@ -251,7 +274,9 @@ class Database(object): _TXN_ID = 0 - def __init__(self, hs, database_config: DatabaseConnectionConfig, engine): + def __init__( + self, hs, database_config: DatabaseConnectionConfig, engine: BaseDatabaseEngine + ): self.hs = hs self._clock = hs.get_clock() self._database_config = database_config @@ -259,9 +284,9 @@ class Database(object): self.updates = BackgroundUpdater(hs, self) - self._previous_txn_total_time = 0 - self._current_txn_total_time = 0 - self._previous_loop_ts = 0 + self._previous_txn_total_time = 0.0 + self._current_txn_total_time = 0.0 + self._previous_loop_ts = 0.0 # TODO(paul): These can eventually be removed once the metrics code # is running in mainline, and we have some nice monitoring frontends @@ -463,23 +488,23 @@ class Database(object): sql_txn_timer.labels(desc).observe(duration) @defer.inlineCallbacks - def runInteraction(self, desc, func, *args, **kwargs): + def runInteraction(self, desc: str, func: Callable, *args: Any, **kwargs: Any): """Starts a transaction on the database and runs a given function Arguments: - desc (str): description of the transaction, for logging and metrics - func (func): callback function, which will be called with a + desc: description of the transaction, for logging and metrics + func: callback function, which will be called with a database transaction (twisted.enterprise.adbapi.Transaction) as its first argument, followed by `args` and `kwargs`. - args (list): positional args to pass to `func` - kwargs (dict): named args to pass to `func` + args: positional args to pass to `func` + kwargs: named args to pass to `func` Returns: Deferred: The result of func """ - after_callbacks = [] - exception_callbacks = [] + after_callbacks = [] # type: List[_CallbackListEntry] + exception_callbacks = [] # type: List[_CallbackListEntry] if LoggingContext.current_context() == LoggingContext.sentinel: logger.warning("Starting db txn '%s' from sentinel context", desc) @@ -505,15 +530,15 @@ class Database(object): return result @defer.inlineCallbacks - def runWithConnection(self, func, *args, **kwargs): + def runWithConnection(self, func: Callable, *args: Any, **kwargs: Any): """Wraps the .runWithConnection() method on the underlying db_pool. Arguments: - func (func): callback function, which will be called with a + func: callback function, which will be called with a database connection (twisted.enterprise.adbapi.Connection) as its first argument, followed by `args` and `kwargs`. - args (list): positional args to pass to `func` - kwargs (dict): named args to pass to `func` + args: positional args to pass to `func` + kwargs: named args to pass to `func` Returns: Deferred: The result of func @@ -800,7 +825,7 @@ class Database(object): return False # We didn't find any existing rows, so insert a new one - allvalues = {} + allvalues = {} # type: Dict[str, Any] allvalues.update(keyvalues) allvalues.update(values) allvalues.update(insertion_values) @@ -829,7 +854,7 @@ class Database(object): Returns: None """ - allvalues = {} + allvalues = {} # type: Dict[str, Any] allvalues.update(keyvalues) allvalues.update(insertion_values) @@ -916,7 +941,7 @@ class Database(object): Returns: None """ - allnames = [] + allnames = [] # type: List[str] allnames.extend(key_names) allnames.extend(value_names) @@ -1100,7 +1125,7 @@ class Database(object): keyvalues : dict of column names and values to select the rows with retcols : list of strings giving the names of the columns to return """ - results = [] + results = [] # type: List[Dict[str, Any]] if not iterable: return results @@ -1439,7 +1464,7 @@ class Database(object): raise ValueError("order_direction must be one of 'ASC' or 'DESC'.") where_clause = "WHERE " if filters or keyvalues else "" - arg_list = [] + arg_list = [] # type: List[Any] if filters: where_clause += " AND ".join("%s LIKE ?" % (k,) for k in filters) arg_list += list(filters.values()) diff --git a/synapse/storage/engines/__init__.py b/synapse/storage/engines/__init__.py index 9d2d519922..035f9ea6e9 100644 --- a/synapse/storage/engines/__init__.py +++ b/synapse/storage/engines/__init__.py @@ -12,29 +12,31 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - -import importlib import platform -from ._base import IncorrectDatabaseSetup +from ._base import BaseDatabaseEngine, IncorrectDatabaseSetup from .postgres import PostgresEngine from .sqlite import Sqlite3Engine -SUPPORTED_MODULE = {"sqlite3": Sqlite3Engine, "psycopg2": PostgresEngine} - -def create_engine(database_config): +def create_engine(database_config) -> BaseDatabaseEngine: name = database_config["name"] - engine_class = SUPPORTED_MODULE.get(name, None) - if engine_class: + if name == "sqlite3": + import sqlite3 + + return Sqlite3Engine(sqlite3, database_config) + + if name == "psycopg2": # pypy requires psycopg2cffi rather than psycopg2 - if name == "psycopg2" and platform.python_implementation() == "PyPy": - name = "psycopg2cffi" - module = importlib.import_module(name) - return engine_class(module, database_config) + if platform.python_implementation() == "PyPy": + import psycopg2cffi as psycopg2 # type: ignore + else: + import psycopg2 # type: ignore + + return PostgresEngine(psycopg2, database_config) raise RuntimeError("Unsupported database engine '%s'" % (name,)) -__all__ = ["create_engine", "IncorrectDatabaseSetup"] +__all__ = ["create_engine", "BaseDatabaseEngine", "IncorrectDatabaseSetup"] diff --git a/synapse/storage/engines/_base.py b/synapse/storage/engines/_base.py index ec5a4d198b..ab0bbe4bd3 100644 --- a/synapse/storage/engines/_base.py +++ b/synapse/storage/engines/_base.py @@ -12,7 +12,94 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import abc +from typing import Generic, TypeVar + +from synapse.storage.types import Connection class IncorrectDatabaseSetup(RuntimeError): pass + + +ConnectionType = TypeVar("ConnectionType", bound=Connection) + + +class BaseDatabaseEngine(Generic[ConnectionType], metaclass=abc.ABCMeta): + def __init__(self, module, database_config: dict): + self.module = module + + @property + @abc.abstractmethod + def single_threaded(self) -> bool: + ... + + @property + @abc.abstractmethod + def can_native_upsert(self) -> bool: + """ + Do we support native UPSERTs? + """ + ... + + @property + @abc.abstractmethod + def supports_tuple_comparison(self) -> bool: + """ + Do we support comparing tuples, i.e. `(a, b) > (c, d)`? + """ + ... + + @property + @abc.abstractmethod + def supports_using_any_list(self) -> bool: + """ + Do we support using `a = ANY(?)` and passing a list + """ + ... + + @abc.abstractmethod + def check_database( + self, db_conn: ConnectionType, allow_outdated_version: bool = False + ) -> None: + ... + + @abc.abstractmethod + def check_new_database(self, txn) -> None: + """Gets called when setting up a brand new database. This allows us to + apply stricter checks on new databases versus existing database. + """ + ... + + @abc.abstractmethod + def convert_param_style(self, sql: str) -> str: + ... + + @abc.abstractmethod + def on_new_connection(self, db_conn: ConnectionType) -> None: + ... + + @abc.abstractmethod + def is_deadlock(self, error: Exception) -> bool: + ... + + @abc.abstractmethod + def is_connection_closed(self, conn: ConnectionType) -> bool: + ... + + @abc.abstractmethod + def lock_table(self, txn, table: str) -> None: + ... + + @abc.abstractmethod + def get_next_state_group_id(self, txn) -> int: + """Returns an int that can be used as a new state_group ID + """ + ... + + @property + @abc.abstractmethod + def server_version(self) -> str: + """Gets a string giving the server version. For example: '3.22.0' + """ + ... diff --git a/synapse/storage/engines/postgres.py b/synapse/storage/engines/postgres.py index 53b3f372b0..6c7d08a6f2 100644 --- a/synapse/storage/engines/postgres.py +++ b/synapse/storage/engines/postgres.py @@ -15,16 +15,14 @@ import logging -from ._base import IncorrectDatabaseSetup +from ._base import BaseDatabaseEngine, IncorrectDatabaseSetup logger = logging.getLogger(__name__) -class PostgresEngine(object): - single_threaded = False - +class PostgresEngine(BaseDatabaseEngine): def __init__(self, database_module, database_config): - self.module = database_module + super().__init__(database_module, database_config) self.module.extensions.register_type(self.module.extensions.UNICODE) # Disables passing `bytes` to txn.execute, c.f. #6186. If you do @@ -36,6 +34,10 @@ class PostgresEngine(object): self.synchronous_commit = database_config.get("synchronous_commit", True) self._version = None # unknown as yet + @property + def single_threaded(self) -> bool: + return False + def check_database(self, db_conn, allow_outdated_version: bool = False): # Get the version of PostgreSQL that we're using. As per the psycopg2 # docs: The number is formed by converting the major, minor, and diff --git a/synapse/storage/engines/sqlite.py b/synapse/storage/engines/sqlite.py index 641e490697..2bfeefd54e 100644 --- a/synapse/storage/engines/sqlite.py +++ b/synapse/storage/engines/sqlite.py @@ -12,16 +12,16 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - +import sqlite3 import struct import threading +from synapse.storage.engines import BaseDatabaseEngine -class Sqlite3Engine(object): - single_threaded = True +class Sqlite3Engine(BaseDatabaseEngine[sqlite3.Connection]): def __init__(self, database_module, database_config): - self.module = database_module + super().__init__(database_module, database_config) database = database_config.get("args", {}).get("database") self._is_in_memory = database in (None, ":memory:",) @@ -31,6 +31,10 @@ class Sqlite3Engine(object): self._current_state_group_id = None self._current_state_group_id_lock = threading.Lock() + @property + def single_threaded(self) -> bool: + return True + @property def can_native_upsert(self): """ @@ -68,7 +72,6 @@ class Sqlite3Engine(object): return sql def on_new_connection(self, db_conn): - # We need to import here to avoid an import loop. from synapse.storage.prepare_database import prepare_database diff --git a/synapse/storage/types.py b/synapse/storage/types.py new file mode 100644 index 0000000000..daff81c5ee --- /dev/null +++ b/synapse/storage/types.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Any, Iterable, Iterator, List, Tuple + +from typing_extensions import Protocol + + +""" +Some very basic protocol definitions for the DB-API2 classes specified in PEP-249 +""" + + +class Cursor(Protocol): + def execute(self, sql: str, parameters: Iterable[Any] = ...) -> Any: + ... + + def executemany(self, sql: str, parameters: Iterable[Iterable[Any]]) -> Any: + ... + + def fetchall(self) -> List[Tuple]: + ... + + def fetchone(self) -> Tuple: + ... + + @property + def description(self) -> Any: + return None + + @property + def rowcount(self) -> int: + return 0 + + def __iter__(self) -> Iterator[Tuple]: + ... + + def close(self) -> None: + ... + + +class Connection(Protocol): + def cursor(self) -> Cursor: + ... + + def close(self) -> None: + ... + + def commit(self) -> None: + ... + + def rollback(self, *args, **kwargs) -> None: + ... diff --git a/tox.ini b/tox.ini index 4ccfde01b5..6521535137 100644 --- a/tox.ini +++ b/tox.ini @@ -168,7 +168,6 @@ commands= coverage html [testenv:mypy] -basepython = python3.7 skip_install = True deps = {[base]deps} @@ -179,7 +178,8 @@ env = extras = all commands = mypy \ synapse/api \ - synapse/config/ \ + synapse/appservice \ + synapse/config \ synapse/events/spamcheck.py \ synapse/federation/sender \ synapse/federation/transport \ @@ -192,6 +192,7 @@ commands = mypy \ synapse/rest \ synapse/spam_checker_api \ synapse/storage/engines \ + synapse/storage/database.py \ synapse/streams # To find all folders that pass mypy you run: From b32ac60c22493cd191d63eae5104fa9d69c37495 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 27 Feb 2020 23:47:40 +1100 Subject: [PATCH 1118/1623] Expose common commands via snap run interface to allow easier invocation (#6315) Signed-off-by: James Hebden --- changelog.d/6315.feature | 1 + snap/snapcraft.yaml | 21 ++++++++++++++++----- 2 files changed, 17 insertions(+), 5 deletions(-) create mode 100644 changelog.d/6315.feature diff --git a/changelog.d/6315.feature b/changelog.d/6315.feature new file mode 100644 index 0000000000..c5377dd1e9 --- /dev/null +++ b/changelog.d/6315.feature @@ -0,0 +1 @@ +Expose the `synctl`, `hash_password` and `generate_config` commands in the snapcraft package. Contributed by @devec0. diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 9e644e8567..6b62b79114 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,20 +1,31 @@ name: matrix-synapse base: core18 -version: git +version: git summary: Reference Matrix homeserver description: | Synapse is the reference Matrix homeserver. Matrix is a federated and decentralised instant messaging and VoIP system. -grade: stable -confinement: strict +grade: stable +confinement: strict apps: - matrix-synapse: + matrix-synapse: command: synctl --no-daemonize start $SNAP_COMMON/homeserver.yaml stop-command: synctl -c $SNAP_COMMON stop plugs: [network-bind, network] - daemon: simple + daemon: simple + hash-password: + command: hash_password + generate-config: + command: generate_config + generate-signing-key: + command: generate_signing_key.py + register-new-matrix-user: + command: register_new_matrix_user + plugs: [network] + synctl: + command: synctl parts: matrix-synapse: source: . From cab4a52535097c5836fe67c5e09e8350d7ccf03c Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 27 Feb 2020 13:08:43 +0000 Subject: [PATCH 1119/1623] set worker_app for frontend proxy test (#7003) to stop the federationhandler trying to do master stuff --- changelog.d/7003.misc | 1 + tests/app/test_frontend_proxy.py | 5 +++++ 2 files changed, 6 insertions(+) create mode 100644 changelog.d/7003.misc diff --git a/changelog.d/7003.misc b/changelog.d/7003.misc new file mode 100644 index 0000000000..08aa80bcd9 --- /dev/null +++ b/changelog.d/7003.misc @@ -0,0 +1 @@ +Refactoring work in preparation for changing the event redaction algorithm. diff --git a/tests/app/test_frontend_proxy.py b/tests/app/test_frontend_proxy.py index 160e55aca9..d3feafa1b7 100644 --- a/tests/app/test_frontend_proxy.py +++ b/tests/app/test_frontend_proxy.py @@ -27,6 +27,11 @@ class FrontendProxyTests(HomeserverTestCase): return hs + def default_config(self, name="test"): + c = super().default_config(name) + c["worker_app"] = "synapse.app.frontend_proxy" + return c + def test_listen_http_with_presence_enabled(self): """ When presence is on, the stub servlet will not register. From 2201bc979588720bd99880b9cd8df2292b2d483f Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 27 Feb 2020 16:33:21 +0000 Subject: [PATCH 1120/1623] Don't refuse to start worker if media listener configured. (#7002) Instead lets just warn if the worker has a media listener configured but has the media repository disabled. Previously non media repository workers would just ignore the media listener. --- changelog.d/7002.misc | 1 + synapse/app/generic_worker.py | 30 ++++++++++++++++++------------ 2 files changed, 19 insertions(+), 12 deletions(-) create mode 100644 changelog.d/7002.misc diff --git a/changelog.d/7002.misc b/changelog.d/7002.misc new file mode 100644 index 0000000000..ec5c004bbe --- /dev/null +++ b/changelog.d/7002.misc @@ -0,0 +1 @@ +Merge worker apps together. diff --git a/synapse/app/generic_worker.py b/synapse/app/generic_worker.py index 30efd39092..b2c764bfe8 100644 --- a/synapse/app/generic_worker.py +++ b/synapse/app/generic_worker.py @@ -494,20 +494,26 @@ class GenericWorkerServer(HomeServer): elif name == "federation": resources.update({FEDERATION_PREFIX: TransportLayerServer(self)}) elif name == "media": - media_repo = self.get_media_repository_resource() + if self.config.can_load_media_repo: + media_repo = self.get_media_repository_resource() - # We need to serve the admin servlets for media on the - # worker. - admin_resource = JsonResource(self, canonical_json=False) - register_servlets_for_media_repo(self, admin_resource) + # We need to serve the admin servlets for media on the + # worker. + admin_resource = JsonResource(self, canonical_json=False) + register_servlets_for_media_repo(self, admin_resource) - resources.update( - { - MEDIA_PREFIX: media_repo, - LEGACY_MEDIA_PREFIX: media_repo, - "/_synapse/admin": admin_resource, - } - ) + resources.update( + { + MEDIA_PREFIX: media_repo, + LEGACY_MEDIA_PREFIX: media_repo, + "/_synapse/admin": admin_resource, + } + ) + else: + logger.warning( + "A 'media' listener is configured but the media" + " repository is disabled. Ignoring." + ) if name == "openid" and "federation" not in res["names"]: # Only load the openid resource separately if federation resource From 9b06d8f8a62dc5c423aa9a694e0759eaf1c3c77e Mon Sep 17 00:00:00 2001 From: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com> Date: Fri, 28 Feb 2020 10:58:05 +0100 Subject: [PATCH 1121/1623] Fixed set a user as an admin with the new API (#6928) Fix #6910 --- changelog.d/6910.bugfix | 1 + synapse/rest/admin/users.py | 6 +- .../storage/data_stores/main/registration.py | 16 +- tests/rest/admin/test_user.py | 220 +++++++++++++++--- 4 files changed, 200 insertions(+), 43 deletions(-) create mode 100644 changelog.d/6910.bugfix diff --git a/changelog.d/6910.bugfix b/changelog.d/6910.bugfix new file mode 100644 index 0000000000..707f1ff7b5 --- /dev/null +++ b/changelog.d/6910.bugfix @@ -0,0 +1 @@ +Fixed set a user as an admin with the admin API `PUT /_synapse/admin/v2/users/`. Contributed by @dklimpel. diff --git a/synapse/rest/admin/users.py b/synapse/rest/admin/users.py index c5b461a236..80f959248d 100644 --- a/synapse/rest/admin/users.py +++ b/synapse/rest/admin/users.py @@ -211,9 +211,7 @@ class UserRestServletV2(RestServlet): if target_user == auth_user and not set_admin_to: raise SynapseError(400, "You may not demote yourself.") - await self.admin_handler.set_user_server_admin( - target_user, set_admin_to - ) + await self.store.set_server_admin(target_user, set_admin_to) if "password" in body: if ( @@ -651,6 +649,6 @@ class UserAdminServlet(RestServlet): if target_user == auth_user and not set_admin_to: raise SynapseError(400, "You may not demote yourself.") - await self.store.set_user_server_admin(target_user, set_admin_to) + await self.store.set_server_admin(target_user, set_admin_to) return 200, {} diff --git a/synapse/storage/data_stores/main/registration.py b/synapse/storage/data_stores/main/registration.py index 49306642ed..3e53c8568a 100644 --- a/synapse/storage/data_stores/main/registration.py +++ b/synapse/storage/data_stores/main/registration.py @@ -301,12 +301,16 @@ class RegistrationWorkerStore(SQLBaseStore): admin (bool): true iff the user is to be a server admin, false otherwise. """ - return self.db.simple_update_one( - table="users", - keyvalues={"name": user.to_string()}, - updatevalues={"admin": 1 if admin else 0}, - desc="set_server_admin", - ) + + def set_server_admin_txn(txn): + self.db.simple_update_one_txn( + txn, "users", {"name": user.to_string()}, {"admin": 1 if admin else 0} + ) + self._invalidate_cache_and_stream( + txn, self.get_user_by_id, (user.to_string(),) + ) + + return self.db.runInteraction("set_server_admin", set_server_admin_txn) def _query_for_auth(self, txn, token): sql = ( diff --git a/tests/rest/admin/test_user.py b/tests/rest/admin/test_user.py index cbe4a6a51f..6416fb5d2a 100644 --- a/tests/rest/admin/test_user.py +++ b/tests/rest/admin/test_user.py @@ -16,6 +16,7 @@ import hashlib import hmac import json +import urllib.parse from mock import Mock @@ -371,22 +372,24 @@ class UserRestTestCase(unittest.HomeserverTestCase): def prepare(self, reactor, clock, hs): self.store = hs.get_datastore() - self.url = "/_synapse/admin/v2/users/@bob:test" - self.admin_user = self.register_user("admin", "pass", admin=True) self.admin_user_tok = self.login("admin", "pass") self.other_user = self.register_user("user", "pass") self.other_user_token = self.login("user", "pass") + self.url_other_user = "/_synapse/admin/v2/users/%s" % urllib.parse.quote( + self.other_user + ) def test_requester_is_no_admin(self): """ If the user is not a server admin, an error is returned. """ self.hs.config.registration_shared_secret = None + url = "/_synapse/admin/v2/users/@bob:test" request, channel = self.make_request( - "GET", self.url, access_token=self.other_user_token, + "GET", url, access_token=self.other_user_token, ) self.render(request) @@ -394,7 +397,7 @@ class UserRestTestCase(unittest.HomeserverTestCase): self.assertEqual("You are not a server admin", channel.json_body["error"]) request, channel = self.make_request( - "PUT", self.url, access_token=self.other_user_token, content=b"{}", + "PUT", url, access_token=self.other_user_token, content=b"{}", ) self.render(request) @@ -417,24 +420,26 @@ class UserRestTestCase(unittest.HomeserverTestCase): self.assertEqual(404, channel.code, msg=channel.json_body) self.assertEqual("M_NOT_FOUND", channel.json_body["errcode"]) - def test_requester_is_admin(self): + def test_create_server_admin(self): """ - If the user is a server admin, a new user is created. + Check that a new admin user is created successfully. """ self.hs.config.registration_shared_secret = None + url = "/_synapse/admin/v2/users/@bob:test" + # Create user (server admin) body = json.dumps( { "password": "abc123", "admin": True, + "displayname": "Bob's name", "threepids": [{"medium": "email", "address": "bob@bob.bob"}], } ) - # Create user request, channel = self.make_request( "PUT", - self.url, + url, access_token=self.admin_user_tok, content=body.encode(encoding="utf_8"), ) @@ -442,29 +447,85 @@ class UserRestTestCase(unittest.HomeserverTestCase): self.assertEqual(201, int(channel.result["code"]), msg=channel.result["body"]) self.assertEqual("@bob:test", channel.json_body["name"]) - self.assertEqual("bob", channel.json_body["displayname"]) + self.assertEqual("Bob's name", channel.json_body["displayname"]) self.assertEqual("email", channel.json_body["threepids"][0]["medium"]) self.assertEqual("bob@bob.bob", channel.json_body["threepids"][0]["address"]) + self.assertEqual(True, channel.json_body["admin"]) # Get user request, channel = self.make_request( - "GET", self.url, access_token=self.admin_user_tok, + "GET", url, access_token=self.admin_user_tok, ) self.render(request) self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) self.assertEqual("@bob:test", channel.json_body["name"]) - self.assertEqual("bob", channel.json_body["displayname"]) - self.assertEqual(1, channel.json_body["admin"]) - self.assertEqual(0, channel.json_body["is_guest"]) - self.assertEqual(0, channel.json_body["deactivated"]) + self.assertEqual("Bob's name", channel.json_body["displayname"]) + self.assertEqual("email", channel.json_body["threepids"][0]["medium"]) + self.assertEqual("bob@bob.bob", channel.json_body["threepids"][0]["address"]) + self.assertEqual(True, channel.json_body["admin"]) + self.assertEqual(False, channel.json_body["is_guest"]) + self.assertEqual(False, channel.json_body["deactivated"]) + + def test_create_user(self): + """ + Check that a new regular user is created successfully. + """ + self.hs.config.registration_shared_secret = None + url = "/_synapse/admin/v2/users/@bob:test" + + # Create user + body = json.dumps( + { + "password": "abc123", + "admin": False, + "displayname": "Bob's name", + "threepids": [{"medium": "email", "address": "bob@bob.bob"}], + } + ) + + request, channel = self.make_request( + "PUT", + url, + access_token=self.admin_user_tok, + content=body.encode(encoding="utf_8"), + ) + self.render(request) + + self.assertEqual(201, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("@bob:test", channel.json_body["name"]) + self.assertEqual("Bob's name", channel.json_body["displayname"]) + self.assertEqual("email", channel.json_body["threepids"][0]["medium"]) + self.assertEqual("bob@bob.bob", channel.json_body["threepids"][0]["address"]) + self.assertEqual(False, channel.json_body["admin"]) + + # Get user + request, channel = self.make_request( + "GET", url, access_token=self.admin_user_tok, + ) + self.render(request) + + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("@bob:test", channel.json_body["name"]) + self.assertEqual("Bob's name", channel.json_body["displayname"]) + self.assertEqual("email", channel.json_body["threepids"][0]["medium"]) + self.assertEqual("bob@bob.bob", channel.json_body["threepids"][0]["address"]) + self.assertEqual(False, channel.json_body["admin"]) + self.assertEqual(False, channel.json_body["is_guest"]) + self.assertEqual(False, channel.json_body["deactivated"]) + + def test_set_password(self): + """ + Test setting a new password for another user. + """ + self.hs.config.registration_shared_secret = None # Change password body = json.dumps({"password": "hahaha"}) request, channel = self.make_request( "PUT", - self.url, + self.url_other_user, access_token=self.admin_user_tok, content=body.encode(encoding="utf_8"), ) @@ -472,41 +533,133 @@ class UserRestTestCase(unittest.HomeserverTestCase): self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + def test_set_displayname(self): + """ + Test setting the displayname of another user. + """ + self.hs.config.registration_shared_secret = None + # Modify user + body = json.dumps({"displayname": "foobar"}) + + request, channel = self.make_request( + "PUT", + self.url_other_user, + access_token=self.admin_user_tok, + content=body.encode(encoding="utf_8"), + ) + self.render(request) + + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("@user:test", channel.json_body["name"]) + self.assertEqual("foobar", channel.json_body["displayname"]) + + # Get user + request, channel = self.make_request( + "GET", self.url_other_user, access_token=self.admin_user_tok, + ) + self.render(request) + + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("@user:test", channel.json_body["name"]) + self.assertEqual("foobar", channel.json_body["displayname"]) + + def test_set_threepid(self): + """ + Test setting threepid for an other user. + """ + self.hs.config.registration_shared_secret = None + + # Delete old and add new threepid to user body = json.dumps( - { - "displayname": "foobar", - "deactivated": True, - "threepids": [{"medium": "email", "address": "bob2@bob.bob"}], - } + {"threepids": [{"medium": "email", "address": "bob3@bob.bob"}]} ) request, channel = self.make_request( "PUT", - self.url, + self.url_other_user, access_token=self.admin_user_tok, content=body.encode(encoding="utf_8"), ) self.render(request) self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) - self.assertEqual("@bob:test", channel.json_body["name"]) - self.assertEqual("foobar", channel.json_body["displayname"]) + self.assertEqual("@user:test", channel.json_body["name"]) + self.assertEqual("email", channel.json_body["threepids"][0]["medium"]) + self.assertEqual("bob3@bob.bob", channel.json_body["threepids"][0]["address"]) + + # Get user + request, channel = self.make_request( + "GET", self.url_other_user, access_token=self.admin_user_tok, + ) + self.render(request) + + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("@user:test", channel.json_body["name"]) + self.assertEqual("email", channel.json_body["threepids"][0]["medium"]) + self.assertEqual("bob3@bob.bob", channel.json_body["threepids"][0]["address"]) + + def test_deactivate_user(self): + """ + Test deactivating another user. + """ + + # Deactivate user + body = json.dumps({"deactivated": True}) + + request, channel = self.make_request( + "PUT", + self.url_other_user, + access_token=self.admin_user_tok, + content=body.encode(encoding="utf_8"), + ) + self.render(request) + + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("@user:test", channel.json_body["name"]) self.assertEqual(True, channel.json_body["deactivated"]) # the user is deactivated, the threepid will be deleted # Get user request, channel = self.make_request( - "GET", self.url, access_token=self.admin_user_tok, + "GET", self.url_other_user, access_token=self.admin_user_tok, ) self.render(request) self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) - self.assertEqual("@bob:test", channel.json_body["name"]) - self.assertEqual("foobar", channel.json_body["displayname"]) - self.assertEqual(1, channel.json_body["admin"]) - self.assertEqual(0, channel.json_body["is_guest"]) - self.assertEqual(1, channel.json_body["deactivated"]) + self.assertEqual("@user:test", channel.json_body["name"]) + self.assertEqual(True, channel.json_body["deactivated"]) + + def test_set_user_as_admin(self): + """ + Test setting the admin flag on a user. + """ + self.hs.config.registration_shared_secret = None + + # Set a user as an admin + body = json.dumps({"admin": True}) + + request, channel = self.make_request( + "PUT", + self.url_other_user, + access_token=self.admin_user_tok, + content=body.encode(encoding="utf_8"), + ) + self.render(request) + + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("@user:test", channel.json_body["name"]) + self.assertEqual(True, channel.json_body["admin"]) + + # Get user + request, channel = self.make_request( + "GET", self.url_other_user, access_token=self.admin_user_tok, + ) + self.render(request) + + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("@user:test", channel.json_body["name"]) + self.assertEqual(True, channel.json_body["admin"]) def test_accidental_deactivation_prevention(self): """ @@ -514,13 +667,14 @@ class UserRestTestCase(unittest.HomeserverTestCase): for the deactivated body parameter """ self.hs.config.registration_shared_secret = None + url = "/_synapse/admin/v2/users/@bob:test" # Create user body = json.dumps({"password": "abc123"}) request, channel = self.make_request( "PUT", - self.url, + url, access_token=self.admin_user_tok, content=body.encode(encoding="utf_8"), ) @@ -532,7 +686,7 @@ class UserRestTestCase(unittest.HomeserverTestCase): # Get user request, channel = self.make_request( - "GET", self.url, access_token=self.admin_user_tok, + "GET", url, access_token=self.admin_user_tok, ) self.render(request) @@ -546,7 +700,7 @@ class UserRestTestCase(unittest.HomeserverTestCase): request, channel = self.make_request( "PUT", - self.url, + url, access_token=self.admin_user_tok, content=body.encode(encoding="utf_8"), ) @@ -556,7 +710,7 @@ class UserRestTestCase(unittest.HomeserverTestCase): # Check user is not deactivated request, channel = self.make_request( - "GET", self.url, access_token=self.admin_user_tok, + "GET", url, access_token=self.admin_user_tok, ) self.render(request) From c3c6c0e6222cc1bc8ae35a66389dc428d0ddbc92 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 28 Feb 2020 11:15:11 +0000 Subject: [PATCH 1122/1623] Add 'device_lists_outbound_pokes' as extra table. This makes sure we check all the relevant tables to get the current max stream ID. Currently not doing so isn't problematic as the max stream ID in `device_lists_outbound_pokes` is the same as in `device_lists_stream`, however that will change. --- synapse/replication/slave/storage/devices.py | 8 +++++++- synapse/storage/data_stores/main/__init__.py | 5 ++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/synapse/replication/slave/storage/devices.py b/synapse/replication/slave/storage/devices.py index 1c77687eea..bf46cc4f8a 100644 --- a/synapse/replication/slave/storage/devices.py +++ b/synapse/replication/slave/storage/devices.py @@ -29,7 +29,13 @@ class SlavedDeviceStore(EndToEndKeyWorkerStore, DeviceWorkerStore, BaseSlavedSto self.hs = hs self._device_list_id_gen = SlavedIdTracker( - db_conn, "device_lists_stream", "stream_id" + db_conn, + "device_lists_stream", + "stream_id", + extra_tables=[ + ("user_signature_stream", "stream_id"), + ("device_lists_outbound_pokes", "stream_id"), + ], ) device_list_max = self._device_list_id_gen.get_current_token() self._device_list_stream_cache = StreamChangeCache( diff --git a/synapse/storage/data_stores/main/__init__.py b/synapse/storage/data_stores/main/__init__.py index acca079f23..649e835303 100644 --- a/synapse/storage/data_stores/main/__init__.py +++ b/synapse/storage/data_stores/main/__init__.py @@ -144,7 +144,10 @@ class DataStore( db_conn, "device_lists_stream", "stream_id", - extra_tables=[("user_signature_stream", "stream_id")], + extra_tables=[ + ("user_signature_stream", "stream_id"), + ("device_lists_outbound_pokes", "stream_id"), + ], ) self._cross_signing_id_gen = StreamIdGenerator( db_conn, "e2e_cross_signing_keys", "stream_id" From f5caa1864e3d3c24c691b3a3bff723f77def129e Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 28 Feb 2020 11:21:25 +0000 Subject: [PATCH 1123/1623] Change device lists stream to have one row per id. This will make it possible to process the streams more incrementally, avoiding having to process large chunks at once. --- synapse/storage/data_stores/main/devices.py | 59 ++++++++++++++------- 1 file changed, 41 insertions(+), 18 deletions(-) diff --git a/synapse/storage/data_stores/main/devices.py b/synapse/storage/data_stores/main/devices.py index d55733a4cd..3299607910 100644 --- a/synapse/storage/data_stores/main/devices.py +++ b/synapse/storage/data_stores/main/devices.py @@ -1017,29 +1017,41 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): """Persist that a user's devices have been updated, and which hosts (if any) should be poked. """ - with self._device_list_id_gen.get_next() as stream_id: + if not device_ids: + return + + with self._device_list_id_gen.get_next_mult(len(device_ids)) as stream_ids: yield self.db.runInteraction( - "add_device_change_to_streams", - self._add_device_change_txn, + "add_device_change_to_stream", + self._add_device_change_to_stream_txn, + user_id, + device_ids, + stream_ids, + ) + + if not hosts: + return stream_ids[-1] + + context = get_active_span_text_map() + with self._device_list_id_gen.get_next_mult( + len(hosts) * len(device_ids) + ) as stream_ids: + yield self.db.runInteraction( + "add_device_outbound_poke_to_stream", + self._add_device_outbound_poke_to_stream_txn, user_id, device_ids, hosts, - stream_id, + stream_ids, + context, ) - return stream_id - def _add_device_change_txn(self, txn, user_id, device_ids, hosts, stream_id): - now = self._clock.time_msec() + return stream_ids[-1] + def _add_device_change_to_stream_txn(self, txn, user_id, device_ids, stream_ids): txn.call_after( - self._device_list_stream_cache.entity_has_changed, user_id, stream_id + self._device_list_stream_cache.entity_has_changed, user_id, stream_ids[-1], ) - for host in hosts: - txn.call_after( - self._device_list_federation_stream_cache.entity_has_changed, - host, - stream_id, - ) # Delete older entries in the table, as we really only care about # when the latest change happened. @@ -1048,7 +1060,7 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): DELETE FROM device_lists_stream WHERE user_id = ? AND device_id = ? AND stream_id < ? """, - [(user_id, device_id, stream_id) for device_id in device_ids], + [(user_id, device_id, stream_ids[0]) for device_id in device_ids], ) self.db.simple_insert_many_txn( @@ -1056,11 +1068,22 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): table="device_lists_stream", values=[ {"stream_id": stream_id, "user_id": user_id, "device_id": device_id} - for device_id in device_ids + for stream_id, device_id in zip(stream_ids, device_ids) ], ) - context = get_active_span_text_map() + def _add_device_outbound_poke_to_stream_txn( + self, txn, user_id, device_ids, hosts, stream_ids, context, + ): + for host in hosts: + txn.call_after( + self._device_list_federation_stream_cache.entity_has_changed, + host, + stream_ids[-1], + ) + + now = self._clock.time_msec() + next_stream_id = iter(stream_ids) self.db.simple_insert_many_txn( txn, @@ -1068,7 +1091,7 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): values=[ { "destination": destination, - "stream_id": stream_id, + "stream_id": next(next_stream_id), "user_id": user_id, "device_id": device_id, "sent": False, From 9ce4e344a808e15a36a2d9ea03b77ebfc6ac7fe2 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 28 Feb 2020 11:24:05 +0000 Subject: [PATCH 1124/1623] Change device list replication to match new semantics. Instead of sending down batches of user ID/host tuples, send down a row per entity (user ID or host). --- synapse/app/generic_worker.py | 2 +- synapse/replication/slave/storage/devices.py | 25 ++++++++++---------- synapse/replication/tcp/streams/_base.py | 13 ++++++---- synapse/storage/data_stores/main/devices.py | 15 +++++++----- 4 files changed, 32 insertions(+), 23 deletions(-) diff --git a/synapse/app/generic_worker.py b/synapse/app/generic_worker.py index b2c764bfe8..561a6f4b22 100644 --- a/synapse/app/generic_worker.py +++ b/synapse/app/generic_worker.py @@ -774,7 +774,7 @@ class FederationSenderHandler(object): # ... as well as device updates and messages elif stream_name == DeviceListsStream.NAME: - hosts = {row.destination for row in rows} + hosts = {row.entity for row in rows if not row.entity.startswith("@")} for host in hosts: self.federation_sender.send_device_messages(host) diff --git a/synapse/replication/slave/storage/devices.py b/synapse/replication/slave/storage/devices.py index bf46cc4f8a..01a4f85884 100644 --- a/synapse/replication/slave/storage/devices.py +++ b/synapse/replication/slave/storage/devices.py @@ -61,23 +61,24 @@ class SlavedDeviceStore(EndToEndKeyWorkerStore, DeviceWorkerStore, BaseSlavedSto def process_replication_rows(self, stream_name, token, rows): if stream_name == DeviceListsStream.NAME: self._device_list_id_gen.advance(token) - for row in rows: - self._invalidate_caches_for_devices(token, row.user_id, row.destination) + self._invalidate_caches_for_devices(token, rows) elif stream_name == UserSignatureStream.NAME: + self._device_list_id_gen.advance(token) for row in rows: self._user_signature_stream_cache.entity_has_changed(row.user_id, token) return super(SlavedDeviceStore, self).process_replication_rows( stream_name, token, rows ) - def _invalidate_caches_for_devices(self, token, user_id, destination): - self._device_list_stream_cache.entity_has_changed(user_id, token) + def _invalidate_caches_for_devices(self, token, rows): + for row in rows: + if row.entity.startswith("@"): + self._device_list_stream_cache.entity_has_changed(row.entity, token) + self.get_cached_devices_for_user.invalidate((row.entity,)) + self._get_cached_user_device.invalidate_many((row.entity,)) + self.get_device_list_last_stream_id_for_remote.invalidate((row.entity,)) - if destination: - self._device_list_federation_stream_cache.entity_has_changed( - destination, token - ) - - self.get_cached_devices_for_user.invalidate((user_id,)) - self._get_cached_user_device.invalidate_many((user_id,)) - self.get_device_list_last_stream_id_for_remote.invalidate((user_id,)) + else: + self._device_list_federation_stream_cache.entity_has_changed( + row.entity, token + ) diff --git a/synapse/replication/tcp/streams/_base.py b/synapse/replication/tcp/streams/_base.py index 208e8a667b..7a8b6e9df1 100644 --- a/synapse/replication/tcp/streams/_base.py +++ b/synapse/replication/tcp/streams/_base.py @@ -94,9 +94,13 @@ PublicRoomsStreamRow = namedtuple( "network_id", # str, optional ), ) -DeviceListsStreamRow = namedtuple( - "DeviceListsStreamRow", ("user_id", "destination") # str # str -) + + +@attr.s +class DeviceListsStreamRow: + entity = attr.ib(type=str) + + ToDeviceStreamRow = namedtuple("ToDeviceStreamRow", ("entity",)) # str TagAccountDataStreamRow = namedtuple( "TagAccountDataStreamRow", ("user_id", "room_id", "data") # str # str # dict @@ -363,7 +367,8 @@ class PublicRoomsStream(Stream): class DeviceListsStream(Stream): - """Someone added/changed/removed a device + """Either a user has updated their devices or a remote server needs to be + told about a device update. """ NAME = "device_lists" diff --git a/synapse/storage/data_stores/main/devices.py b/synapse/storage/data_stores/main/devices.py index 3299607910..768afe7a6c 100644 --- a/synapse/storage/data_stores/main/devices.py +++ b/synapse/storage/data_stores/main/devices.py @@ -612,15 +612,18 @@ class DeviceWorkerStore(SQLBaseStore): combined list of changes to devices, and which destinations need to be poked. `destination` may be None if no destinations need to be poked. """ - # We do a group by here as there can be a large number of duplicate - # entries, since we throw away device IDs. + + # This query Does The Right Thing where it'll correctly apply the + # bounds to the inner queries. sql = """ - SELECT MAX(stream_id) AS stream_id, user_id, destination - FROM device_lists_stream - LEFT JOIN device_lists_outbound_pokes USING (stream_id, user_id, device_id) + SELECT stream_id, entity FROM ( + SELECT stream_id, user_id AS entity FROM device_lists_stream + UNION ALL + SELECT stream_id, destination AS entity FROM device_lists_outbound_pokes + ) AS e WHERE ? < stream_id AND stream_id <= ? - GROUP BY user_id, destination """ + return self.db.execute( "get_all_device_list_changes_for_remotes", None, sql, from_key, to_key ) From 59ad93d2a415cd07ab6f6afd490d0a5ceeec93a0 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 28 Feb 2020 11:27:37 +0000 Subject: [PATCH 1125/1623] Newsfile --- changelog.d/7010.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7010.misc diff --git a/changelog.d/7010.misc b/changelog.d/7010.misc new file mode 100644 index 0000000000..4ba1f6cdf8 --- /dev/null +++ b/changelog.d/7010.misc @@ -0,0 +1 @@ +Change device list streams to have one row per ID. From f70f44abc73689a66d0a05dc703ca38241092174 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 28 Feb 2020 11:45:35 +0000 Subject: [PATCH 1126/1623] Remove handling of multiple rows per ID --- synapse/storage/data_stores/main/devices.py | 35 +--------------- tests/storage/test_devices.py | 45 --------------------- 2 files changed, 1 insertion(+), 79 deletions(-) diff --git a/synapse/storage/data_stores/main/devices.py b/synapse/storage/data_stores/main/devices.py index 768afe7a6c..06e1d9f033 100644 --- a/synapse/storage/data_stores/main/devices.py +++ b/synapse/storage/data_stores/main/devices.py @@ -112,23 +112,13 @@ class DeviceWorkerStore(SQLBaseStore): if not has_changed: return now_stream_id, [] - # We retrieve n+1 devices from the list of outbound pokes where n is - # our outbound device update limit. We then check if the very last - # device has the same stream_id as the second-to-last device. If so, - # then we ignore all devices with that stream_id and only send the - # devices with a lower stream_id. - # - # If when culling the list we end up with no devices afterwards, we - # consider the device update to be too large, and simply skip the - # stream_id; the rationale being that such a large device list update - # is likely an error. updates = yield self.db.runInteraction( "get_device_updates_by_remote", self._get_device_updates_by_remote_txn, destination, from_stream_id, now_stream_id, - limit + 1, + limit, ) # Return an empty list if there are no updates @@ -166,14 +156,6 @@ class DeviceWorkerStore(SQLBaseStore): "device_id": verify_key.version, } - # if we have exceeded the limit, we need to exclude any results with the - # same stream_id as the last row. - if len(updates) > limit: - stream_id_cutoff = updates[-1][2] - now_stream_id = stream_id_cutoff - 1 - else: - stream_id_cutoff = None - # Perform the equivalent of a GROUP BY # # Iterate through the updates list and copy non-duplicate @@ -192,10 +174,6 @@ class DeviceWorkerStore(SQLBaseStore): query_map = {} cross_signing_keys_by_user = {} for user_id, device_id, update_stream_id, update_context in updates: - if stream_id_cutoff is not None and update_stream_id >= stream_id_cutoff: - # Stop processing updates - break - if ( user_id in master_key_by_user and device_id == master_key_by_user[user_id]["device_id"] @@ -218,17 +196,6 @@ class DeviceWorkerStore(SQLBaseStore): if update_stream_id > previous_update_stream_id: query_map[key] = (update_stream_id, update_context) - # If we didn't find any updates with a stream_id lower than the cutoff, it - # means that there are more than limit updates all of which have the same - # steam_id. - - # That should only happen if a client is spamming the server with new - # devices, in which case E2E isn't going to work well anyway. We'll just - # skip that stream_id and return an empty list, and continue with the next - # stream_id next time. - if not query_map and not cross_signing_keys_by_user: - return stream_id_cutoff, [] - results = yield self._get_device_update_edus_by_remote( destination, from_stream_id, query_map ) diff --git a/tests/storage/test_devices.py b/tests/storage/test_devices.py index 6f8d990959..c2539b353a 100644 --- a/tests/storage/test_devices.py +++ b/tests/storage/test_devices.py @@ -88,51 +88,6 @@ class DeviceStoreTestCase(tests.unittest.TestCase): # Check original device_ids are contained within these updates self._check_devices_in_updates(device_ids, device_updates) - @defer.inlineCallbacks - def test_get_device_updates_by_remote_limited(self): - # Test breaking the update limit in 1, 101, and 1 device_id segments - - # first add one device - device_ids1 = ["device_id0"] - yield self.store.add_device_change_to_streams( - "user_id", device_ids1, ["someotherhost"] - ) - - # then add 101 - device_ids2 = ["device_id" + str(i + 1) for i in range(101)] - yield self.store.add_device_change_to_streams( - "user_id", device_ids2, ["someotherhost"] - ) - - # then one more - device_ids3 = ["newdevice"] - yield self.store.add_device_change_to_streams( - "user_id", device_ids3, ["someotherhost"] - ) - - # - # now read them back. - # - - # first we should get a single update - now_stream_id, device_updates = yield self.store.get_device_updates_by_remote( - "someotherhost", -1, limit=100 - ) - self._check_devices_in_updates(device_ids1, device_updates) - - # Then we should get an empty list back as the 101 devices broke the limit - now_stream_id, device_updates = yield self.store.get_device_updates_by_remote( - "someotherhost", now_stream_id, limit=100 - ) - self.assertEqual(len(device_updates), 0) - - # The 101 devices should've been cleared, so we should now just get one device - # update - now_stream_id, device_updates = yield self.store.get_device_updates_by_remote( - "someotherhost", now_stream_id, limit=100 - ) - self._check_devices_in_updates(device_ids3, device_updates) - def _check_devices_in_updates(self, expected_device_ids, device_updates): """Check that an specific device ids exist in a list of device update EDUs""" self.assertEqual(len(device_updates), len(expected_device_ids)) From 12d425900048b29a95b06428f04ed6ecc9e09d15 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Fri, 28 Feb 2020 07:31:07 -0500 Subject: [PATCH 1127/1623] Add some type annotations to the federation base & client classes (#6995) --- changelog.d/6995.misc | 1 + synapse/federation/federation_base.py | 60 +++++++++++++++---------- synapse/federation/federation_client.py | 10 ++--- tox.ini | 2 + 4 files changed, 45 insertions(+), 28 deletions(-) create mode 100644 changelog.d/6995.misc diff --git a/changelog.d/6995.misc b/changelog.d/6995.misc new file mode 100644 index 0000000000..884b4cf4ee --- /dev/null +++ b/changelog.d/6995.misc @@ -0,0 +1 @@ +Add some type annotations to the federation base & client classes. diff --git a/synapse/federation/federation_base.py b/synapse/federation/federation_base.py index 9fff65716a..190ea1fba1 100644 --- a/synapse/federation/federation_base.py +++ b/synapse/federation/federation_base.py @@ -15,11 +15,13 @@ # limitations under the License. import logging from collections import namedtuple +from typing import Iterable, List import six from twisted.internet import defer -from twisted.internet.defer import DeferredList +from twisted.internet.defer import Deferred, DeferredList +from twisted.python.failure import Failure from synapse.api.constants import MAX_DEPTH, EventTypes, Membership from synapse.api.errors import Codes, SynapseError @@ -29,6 +31,7 @@ from synapse.api.room_versions import ( RoomVersion, ) from synapse.crypto.event_signing import check_event_content_hash +from synapse.crypto.keyring import Keyring from synapse.events import EventBase, make_event_from_dict from synapse.events.utils import prune_event from synapse.http.servlet import assert_params_in_dict @@ -56,7 +59,12 @@ class FederationBase(object): @defer.inlineCallbacks def _check_sigs_and_hash_and_fetch( - self, origin, pdus, room_version, outlier=False, include_none=False + self, + origin: str, + pdus: List[EventBase], + room_version: str, + outlier: bool = False, + include_none: bool = False, ): """Takes a list of PDUs and checks the signatures and hashs of each one. If a PDU fails its signature check then we check if we have it in @@ -69,11 +77,11 @@ class FederationBase(object): a new list. Args: - origin (str) - pdu (list) - room_version (str) - outlier (bool): Whether the events are outliers or not - include_none (str): Whether to include None in the returned list + origin + pdu + room_version + outlier: Whether the events are outliers or not + include_none: Whether to include None in the returned list for events that have failed their checks Returns: @@ -82,7 +90,7 @@ class FederationBase(object): deferreds = self._check_sigs_and_hashes(room_version, pdus) @defer.inlineCallbacks - def handle_check_result(pdu, deferred): + def handle_check_result(pdu: EventBase, deferred: Deferred): try: res = yield make_deferred_yieldable(deferred) except SynapseError: @@ -96,8 +104,10 @@ class FederationBase(object): if not res and pdu.origin != origin: try: + # This should not exist in the base implementation, until + # this is fixed, ignore it for typing. See issue #6997. res = yield defer.ensureDeferred( - self.get_pdu( + self.get_pdu( # type: ignore destinations=[pdu.origin], event_id=pdu.event_id, room_version=room_version, @@ -127,21 +137,23 @@ class FederationBase(object): else: return [p for p in valid_pdus if p] - def _check_sigs_and_hash(self, room_version, pdu): + def _check_sigs_and_hash(self, room_version: str, pdu: EventBase) -> Deferred: return make_deferred_yieldable( self._check_sigs_and_hashes(room_version, [pdu])[0] ) - def _check_sigs_and_hashes(self, room_version, pdus): + def _check_sigs_and_hashes( + self, room_version: str, pdus: List[EventBase] + ) -> List[Deferred]: """Checks that each of the received events is correctly signed by the sending server. Args: - room_version (str): The room version of the PDUs - pdus (list[FrozenEvent]): the events to be checked + room_version: The room version of the PDUs + pdus: the events to be checked Returns: - list[Deferred]: for each input event, a deferred which: + For each input event, a deferred which: * returns the original event if the checks pass * returns a redacted version of the event (if the signature matched but the hash did not) @@ -152,7 +164,7 @@ class FederationBase(object): ctx = LoggingContext.current_context() - def callback(_, pdu): + def callback(_, pdu: EventBase): with PreserveLoggingContext(ctx): if not check_event_content_hash(pdu): # let's try to distinguish between failures because the event was @@ -189,7 +201,7 @@ class FederationBase(object): return pdu - def errback(failure, pdu): + def errback(failure: Failure, pdu: EventBase): failure.trap(SynapseError) with PreserveLoggingContext(ctx): logger.warning( @@ -215,16 +227,18 @@ class PduToCheckSig( pass -def _check_sigs_on_pdus(keyring, room_version, pdus): +def _check_sigs_on_pdus( + keyring: Keyring, room_version: str, pdus: Iterable[EventBase] +) -> List[Deferred]: """Check that the given events are correctly signed Args: - keyring (synapse.crypto.Keyring): keyring object to do the checks - room_version (str): the room version of the PDUs - pdus (Collection[EventBase]): the events to be checked + keyring: keyring object to do the checks + room_version: the room version of the PDUs + pdus: the events to be checked Returns: - List[Deferred]: a Deferred for each event in pdus, which will either succeed if + A Deferred for each event in pdus, which will either succeed if the signatures are valid, or fail (with a SynapseError) if not. """ @@ -329,7 +343,7 @@ def _check_sigs_on_pdus(keyring, room_version, pdus): return [_flatten_deferred_list(p.deferreds) for p in pdus_to_check] -def _flatten_deferred_list(deferreds): +def _flatten_deferred_list(deferreds: List[Deferred]) -> Deferred: """Given a list of deferreds, either return the single deferred, combine into a DeferredList, or return an already resolved deferred. """ @@ -341,7 +355,7 @@ def _flatten_deferred_list(deferreds): return defer.succeed(None) -def _is_invite_via_3pid(event): +def _is_invite_via_3pid(event: EventBase) -> bool: return ( event.type == EventTypes.Member and event.membership == Membership.INVITE diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index 4870e39652..b5538bc07a 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -187,7 +187,7 @@ class FederationClient(FederationBase): async def backfill( self, dest: str, room_id: str, limit: int, extremities: Iterable[str] - ) -> List[EventBase]: + ) -> Optional[List[EventBase]]: """Requests some more historic PDUs for the given room from the given destination server. @@ -199,9 +199,9 @@ class FederationClient(FederationBase): """ logger.debug("backfill extrem=%s", extremities) - # If there are no extremeties then we've (probably) reached the start. + # If there are no extremities then we've (probably) reached the start. if not extremities: - return + return None transaction_data = await self.transport_layer.backfill( dest, room_id, extremities, limit @@ -284,7 +284,7 @@ class FederationClient(FederationBase): pdu_list = [ event_from_pdu_json(p, room_version, outlier=outlier) for p in transaction_data["pdus"] - ] + ] # type: List[EventBase] if pdu_list and pdu_list[0]: pdu = pdu_list[0] @@ -615,7 +615,7 @@ class FederationClient(FederationBase): ] if auth_chain_create_events != [create_event.event_id]: raise InvalidResponseError( - "Unexpected create event(s) in auth chain" + "Unexpected create event(s) in auth chain: %s" % (auth_chain_create_events,) ) diff --git a/tox.ini b/tox.ini index 6521535137..097ebb8774 100644 --- a/tox.ini +++ b/tox.ini @@ -181,6 +181,8 @@ commands = mypy \ synapse/appservice \ synapse/config \ synapse/events/spamcheck.py \ + synapse/federation/federation_base.py \ + synapse/federation/federation_client.py \ synapse/federation/sender \ synapse/federation/transport \ synapse/handlers/presence.py \ From d96ac97d29bb55a98a9ea2b7ab8f98fd72e4a419 Mon Sep 17 00:00:00 2001 From: Sandro Date: Sun, 1 Mar 2020 00:32:26 +0100 Subject: [PATCH 1128/1623] Fix mounting of homeserver.yaml when it does not exist on host (#6913) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Sandro Jäckel --- contrib/docker/docker-compose.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contrib/docker/docker-compose.yml b/contrib/docker/docker-compose.yml index 453b305053..17354b6610 100644 --- a/contrib/docker/docker-compose.yml +++ b/contrib/docker/docker-compose.yml @@ -15,10 +15,9 @@ services: restart: unless-stopped # See the readme for a full documentation of the environment settings environment: - - SYNAPSE_CONFIG_PATH=/etc/homeserver.yaml + - SYNAPSE_CONFIG_PATH=/data/homeserver.yaml volumes: # You may either store all the files in a local folder - - ./matrix-config/homeserver.yaml:/etc/homeserver.yaml - ./files:/data # .. or you may split this between different storage points # - ./files:/data From e4ffb14d5764d49efc28e7f3970d443eae11f087 Mon Sep 17 00:00:00 2001 From: Uday Bansal <43824981+udaybansal19@users.noreply.github.com> Date: Sun, 1 Mar 2020 05:07:23 +0530 Subject: [PATCH 1129/1623] Fix last date for ACMEv1 install (#7015) Support for getting TLS certificates through ACMEv1 ended on November 2019. Signed-off-by: Uday Bansal <43824981+udaybansal19@users.noreply.github.com> --- INSTALL.md | 2 +- changelog.d/7015.misc | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/7015.misc diff --git a/INSTALL.md b/INSTALL.md index aa5eb882bb..ffb82bdcc3 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -418,7 +418,7 @@ so, you will need to edit `homeserver.yaml`, as follows: for having Synapse automatically provision and renew federation certificates through ACME can be found at [ACME.md](docs/ACME.md). Note that, as pointed out in that document, this feature will not - work with installs set up after November 2020. + work with installs set up after November 2019. If you are using your own certificate, be sure to use a `.pem` file that includes the full certificate chain including any intermediate certificates diff --git a/changelog.d/7015.misc b/changelog.d/7015.misc new file mode 100644 index 0000000000..9709dc606e --- /dev/null +++ b/changelog.d/7015.misc @@ -0,0 +1 @@ +Change date in INSTALL.md#tls-certificates for last date of getting TLS certificates to November 2019. \ No newline at end of file From cc7ab0d84afd7cef3f5e0aabd72602535e9d4fbf Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 1 Mar 2020 21:21:36 +0000 Subject: [PATCH 1130/1623] rst->md --- contrib/grafana/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/grafana/README.md b/contrib/grafana/README.md index 6a6cc0bed4..ca780d412e 100644 --- a/contrib/grafana/README.md +++ b/contrib/grafana/README.md @@ -1,6 +1,6 @@ # Using the Synapse Grafana dashboard 0. Set up Prometheus and Grafana. Out of scope for this readme. Useful documentation about using Grafana with Prometheus: http://docs.grafana.org/features/datasources/prometheus/ -1. Have your Prometheus scrape your Synapse. https://github.com/matrix-org/synapse/blob/master/docs/metrics-howto.rst +1. Have your Prometheus scrape your Synapse. https://github.com/matrix-org/synapse/blob/master/docs/metrics-howto.md 2. Import dashboard into Grafana. Download `synapse.json`. Import it to Grafana and select the correct Prometheus datasource. http://docs.grafana.org/reference/export_import/ 3. Set up additional recording rules From e53744c737527ebb2af94b677b359743473b0434 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 2 Mar 2020 12:52:28 +0000 Subject: [PATCH 1131/1623] Fix worker handling --- synapse/app/generic_worker.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/synapse/app/generic_worker.py b/synapse/app/generic_worker.py index 561a6f4b22..d596852419 100644 --- a/synapse/app/generic_worker.py +++ b/synapse/app/generic_worker.py @@ -676,8 +676,9 @@ class GenericWorkerReplicationHandler(ReplicationClientHandler): elif stream_name == "device_lists": all_room_ids = set() for row in rows: - room_ids = await self.store.get_rooms_for_user(row.user_id) - all_room_ids.update(room_ids) + if row.entity.startswith("@"): + room_ids = await self.store.get_rooms_for_user(row.entity) + all_room_ids.update(room_ids) self.notifier.on_new_event("device_list_key", token, rooms=all_room_ids) elif stream_name == "presence": await self.presence_handler.process_replication_rows(token, rows) From bbeee33d63c43cb80118c0dccf8abd9d4ac1b8f3 Mon Sep 17 00:00:00 2001 From: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com> Date: Fri, 28 Feb 2020 10:58:05 +0100 Subject: [PATCH 1132/1623] Fixed set a user as an admin with the new API (#6928) Fix #6910 --- changelog.d/6910.bugfix | 1 + synapse/rest/admin/users.py | 6 +- .../storage/data_stores/main/registration.py | 16 +- tests/rest/admin/test_user.py | 211 +++++++++++++++--- 4 files changed, 195 insertions(+), 39 deletions(-) create mode 100644 changelog.d/6910.bugfix diff --git a/changelog.d/6910.bugfix b/changelog.d/6910.bugfix new file mode 100644 index 0000000000..707f1ff7b5 --- /dev/null +++ b/changelog.d/6910.bugfix @@ -0,0 +1 @@ +Fixed set a user as an admin with the admin API `PUT /_synapse/admin/v2/users/`. Contributed by @dklimpel. diff --git a/synapse/rest/admin/users.py b/synapse/rest/admin/users.py index 2107b5dc56..064908fbb0 100644 --- a/synapse/rest/admin/users.py +++ b/synapse/rest/admin/users.py @@ -211,9 +211,7 @@ class UserRestServletV2(RestServlet): if target_user == auth_user and not set_admin_to: raise SynapseError(400, "You may not demote yourself.") - await self.admin_handler.set_user_server_admin( - target_user, set_admin_to - ) + await self.store.set_server_admin(target_user, set_admin_to) if "password" in body: if ( @@ -648,6 +646,6 @@ class UserAdminServlet(RestServlet): if target_user == auth_user and not set_admin_to: raise SynapseError(400, "You may not demote yourself.") - await self.store.set_user_server_admin(target_user, set_admin_to) + await self.store.set_server_admin(target_user, set_admin_to) return 200, {} diff --git a/synapse/storage/data_stores/main/registration.py b/synapse/storage/data_stores/main/registration.py index 49306642ed..3e53c8568a 100644 --- a/synapse/storage/data_stores/main/registration.py +++ b/synapse/storage/data_stores/main/registration.py @@ -301,12 +301,16 @@ class RegistrationWorkerStore(SQLBaseStore): admin (bool): true iff the user is to be a server admin, false otherwise. """ - return self.db.simple_update_one( - table="users", - keyvalues={"name": user.to_string()}, - updatevalues={"admin": 1 if admin else 0}, - desc="set_server_admin", - ) + + def set_server_admin_txn(txn): + self.db.simple_update_one_txn( + txn, "users", {"name": user.to_string()}, {"admin": 1 if admin else 0} + ) + self._invalidate_cache_and_stream( + txn, self.get_user_by_id, (user.to_string(),) + ) + + return self.db.runInteraction("set_server_admin", set_server_admin_txn) def _query_for_auth(self, txn, token): sql = ( diff --git a/tests/rest/admin/test_user.py b/tests/rest/admin/test_user.py index 490ce8f55d..70688c2494 100644 --- a/tests/rest/admin/test_user.py +++ b/tests/rest/admin/test_user.py @@ -16,6 +16,7 @@ import hashlib import hmac import json +import urllib.parse from mock import Mock @@ -371,22 +372,24 @@ class UserRestTestCase(unittest.HomeserverTestCase): def prepare(self, reactor, clock, hs): self.store = hs.get_datastore() - self.url = "/_synapse/admin/v2/users/@bob:test" - self.admin_user = self.register_user("admin", "pass", admin=True) self.admin_user_tok = self.login("admin", "pass") self.other_user = self.register_user("user", "pass") self.other_user_token = self.login("user", "pass") + self.url_other_user = "/_synapse/admin/v2/users/%s" % urllib.parse.quote( + self.other_user + ) def test_requester_is_no_admin(self): """ If the user is not a server admin, an error is returned. """ self.hs.config.registration_shared_secret = None + url = "/_synapse/admin/v2/users/@bob:test" request, channel = self.make_request( - "GET", self.url, access_token=self.other_user_token, + "GET", url, access_token=self.other_user_token, ) self.render(request) @@ -394,7 +397,7 @@ class UserRestTestCase(unittest.HomeserverTestCase): self.assertEqual("You are not a server admin", channel.json_body["error"]) request, channel = self.make_request( - "PUT", self.url, access_token=self.other_user_token, content=b"{}", + "PUT", url, access_token=self.other_user_token, content=b"{}", ) self.render(request) @@ -417,24 +420,26 @@ class UserRestTestCase(unittest.HomeserverTestCase): self.assertEqual(404, channel.code, msg=channel.json_body) self.assertEqual("M_NOT_FOUND", channel.json_body["errcode"]) - def test_requester_is_admin(self): + def test_create_server_admin(self): """ - If the user is a server admin, a new user is created. + Check that a new admin user is created successfully. """ self.hs.config.registration_shared_secret = None + url = "/_synapse/admin/v2/users/@bob:test" + # Create user (server admin) body = json.dumps( { "password": "abc123", "admin": True, + "displayname": "Bob's name", "threepids": [{"medium": "email", "address": "bob@bob.bob"}], } ) - # Create user request, channel = self.make_request( "PUT", - self.url, + url, access_token=self.admin_user_tok, content=body.encode(encoding="utf_8"), ) @@ -442,29 +447,85 @@ class UserRestTestCase(unittest.HomeserverTestCase): self.assertEqual(201, int(channel.result["code"]), msg=channel.result["body"]) self.assertEqual("@bob:test", channel.json_body["name"]) - self.assertEqual("bob", channel.json_body["displayname"]) + self.assertEqual("Bob's name", channel.json_body["displayname"]) self.assertEqual("email", channel.json_body["threepids"][0]["medium"]) self.assertEqual("bob@bob.bob", channel.json_body["threepids"][0]["address"]) + self.assertEqual(True, channel.json_body["admin"]) # Get user request, channel = self.make_request( - "GET", self.url, access_token=self.admin_user_tok, + "GET", url, access_token=self.admin_user_tok, ) self.render(request) self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) self.assertEqual("@bob:test", channel.json_body["name"]) - self.assertEqual("bob", channel.json_body["displayname"]) - self.assertEqual(1, channel.json_body["admin"]) - self.assertEqual(0, channel.json_body["is_guest"]) - self.assertEqual(0, channel.json_body["deactivated"]) + self.assertEqual("Bob's name", channel.json_body["displayname"]) + self.assertEqual("email", channel.json_body["threepids"][0]["medium"]) + self.assertEqual("bob@bob.bob", channel.json_body["threepids"][0]["address"]) + self.assertEqual(True, channel.json_body["admin"]) + self.assertEqual(False, channel.json_body["is_guest"]) + self.assertEqual(False, channel.json_body["deactivated"]) + + def test_create_user(self): + """ + Check that a new regular user is created successfully. + """ + self.hs.config.registration_shared_secret = None + url = "/_synapse/admin/v2/users/@bob:test" + + # Create user + body = json.dumps( + { + "password": "abc123", + "admin": False, + "displayname": "Bob's name", + "threepids": [{"medium": "email", "address": "bob@bob.bob"}], + } + ) + + request, channel = self.make_request( + "PUT", + url, + access_token=self.admin_user_tok, + content=body.encode(encoding="utf_8"), + ) + self.render(request) + + self.assertEqual(201, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("@bob:test", channel.json_body["name"]) + self.assertEqual("Bob's name", channel.json_body["displayname"]) + self.assertEqual("email", channel.json_body["threepids"][0]["medium"]) + self.assertEqual("bob@bob.bob", channel.json_body["threepids"][0]["address"]) + self.assertEqual(False, channel.json_body["admin"]) + + # Get user + request, channel = self.make_request( + "GET", url, access_token=self.admin_user_tok, + ) + self.render(request) + + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("@bob:test", channel.json_body["name"]) + self.assertEqual("Bob's name", channel.json_body["displayname"]) + self.assertEqual("email", channel.json_body["threepids"][0]["medium"]) + self.assertEqual("bob@bob.bob", channel.json_body["threepids"][0]["address"]) + self.assertEqual(False, channel.json_body["admin"]) + self.assertEqual(False, channel.json_body["is_guest"]) + self.assertEqual(False, channel.json_body["deactivated"]) + + def test_set_password(self): + """ + Test setting a new password for another user. + """ + self.hs.config.registration_shared_secret = None # Change password body = json.dumps({"password": "hahaha"}) request, channel = self.make_request( "PUT", - self.url, + self.url_other_user, access_token=self.admin_user_tok, content=body.encode(encoding="utf_8"), ) @@ -472,38 +533,130 @@ class UserRestTestCase(unittest.HomeserverTestCase): self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + def test_set_displayname(self): + """ + Test setting the displayname of another user. + """ + self.hs.config.registration_shared_secret = None + # Modify user + body = json.dumps({"displayname": "foobar"}) + + request, channel = self.make_request( + "PUT", + self.url_other_user, + access_token=self.admin_user_tok, + content=body.encode(encoding="utf_8"), + ) + self.render(request) + + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("@user:test", channel.json_body["name"]) + self.assertEqual("foobar", channel.json_body["displayname"]) + + # Get user + request, channel = self.make_request( + "GET", self.url_other_user, access_token=self.admin_user_tok, + ) + self.render(request) + + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("@user:test", channel.json_body["name"]) + self.assertEqual("foobar", channel.json_body["displayname"]) + + def test_set_threepid(self): + """ + Test setting threepid for an other user. + """ + self.hs.config.registration_shared_secret = None + + # Delete old and add new threepid to user body = json.dumps( - { - "displayname": "foobar", - "deactivated": True, - "threepids": [{"medium": "email", "address": "bob2@bob.bob"}], - } + {"threepids": [{"medium": "email", "address": "bob3@bob.bob"}]} ) request, channel = self.make_request( "PUT", - self.url, + self.url_other_user, access_token=self.admin_user_tok, content=body.encode(encoding="utf_8"), ) self.render(request) self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) - self.assertEqual("@bob:test", channel.json_body["name"]) - self.assertEqual("foobar", channel.json_body["displayname"]) + self.assertEqual("@user:test", channel.json_body["name"]) + self.assertEqual("email", channel.json_body["threepids"][0]["medium"]) + self.assertEqual("bob3@bob.bob", channel.json_body["threepids"][0]["address"]) + + # Get user + request, channel = self.make_request( + "GET", self.url_other_user, access_token=self.admin_user_tok, + ) + self.render(request) + + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("@user:test", channel.json_body["name"]) + self.assertEqual("email", channel.json_body["threepids"][0]["medium"]) + self.assertEqual("bob3@bob.bob", channel.json_body["threepids"][0]["address"]) + + def test_deactivate_user(self): + """ + Test deactivating another user. + """ + + # Deactivate user + body = json.dumps({"deactivated": True}) + + request, channel = self.make_request( + "PUT", + self.url_other_user, + access_token=self.admin_user_tok, + content=body.encode(encoding="utf_8"), + ) + self.render(request) + + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("@user:test", channel.json_body["name"]) self.assertEqual(True, channel.json_body["deactivated"]) # the user is deactivated, the threepid will be deleted # Get user request, channel = self.make_request( - "GET", self.url, access_token=self.admin_user_tok, + "GET", self.url_other_user, access_token=self.admin_user_tok, ) self.render(request) self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) - self.assertEqual("@bob:test", channel.json_body["name"]) - self.assertEqual("foobar", channel.json_body["displayname"]) - self.assertEqual(1, channel.json_body["admin"]) - self.assertEqual(0, channel.json_body["is_guest"]) - self.assertEqual(1, channel.json_body["deactivated"]) + self.assertEqual("@user:test", channel.json_body["name"]) + self.assertEqual(True, channel.json_body["deactivated"]) + + def test_set_user_as_admin(self): + """ + Test setting the admin flag on a user. + """ + self.hs.config.registration_shared_secret = None + + # Set a user as an admin + body = json.dumps({"admin": True}) + + request, channel = self.make_request( + "PUT", + self.url_other_user, + access_token=self.admin_user_tok, + content=body.encode(encoding="utf_8"), + ) + self.render(request) + + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("@user:test", channel.json_body["name"]) + self.assertEqual(True, channel.json_body["admin"]) + + # Get user + request, channel = self.make_request( + "GET", self.url_other_user, access_token=self.admin_user_tok, + ) + self.render(request) + + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("@user:test", channel.json_body["name"]) + self.assertEqual(True, channel.json_body["admin"]) From 174aaa1d62e54b57499d0606bf0f24bf81c6adf2 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 2 Mar 2020 14:53:56 +0000 Subject: [PATCH 1133/1623] remove spurious changelog --- changelog.d/6910.bugfix | 1 - 1 file changed, 1 deletion(-) delete mode 100644 changelog.d/6910.bugfix diff --git a/changelog.d/6910.bugfix b/changelog.d/6910.bugfix deleted file mode 100644 index 707f1ff7b5..0000000000 --- a/changelog.d/6910.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fixed set a user as an admin with the admin API `PUT /_synapse/admin/v2/users/`. Contributed by @dklimpel. From 3ab8e9c2932476d18af94b6c60cc3613139148ec Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 2 Mar 2020 16:17:11 +0000 Subject: [PATCH 1134/1623] Fix py35-old CI by using native tox. (#7018) I'm not really sure how this was going wrong, but this seems like the right approach anyway. --- .buildkite/scripts/test_old_deps.sh | 7 +------ changelog.d/7018.bugfix | 1 + 2 files changed, 2 insertions(+), 6 deletions(-) create mode 100644 changelog.d/7018.bugfix diff --git a/.buildkite/scripts/test_old_deps.sh b/.buildkite/scripts/test_old_deps.sh index dfd71b2511..cdb77b556c 100755 --- a/.buildkite/scripts/test_old_deps.sh +++ b/.buildkite/scripts/test_old_deps.sh @@ -6,12 +6,7 @@ set -ex apt-get update -apt-get install -y python3.5 python3.5-dev python3-pip libxml2-dev libxslt-dev zlib1g-dev - -# workaround for https://github.com/jaraco/zipp/issues/40 -python3.5 -m pip install 'setuptools>=34.4.0' - -python3.5 -m pip install tox +apt-get install -y python3.5 python3.5-dev python3-pip libxml2-dev libxslt-dev zlib1g-dev tox export LANG="C.UTF-8" diff --git a/changelog.d/7018.bugfix b/changelog.d/7018.bugfix new file mode 100644 index 0000000000..d1b6c1d464 --- /dev/null +++ b/changelog.d/7018.bugfix @@ -0,0 +1 @@ +Fix py35-old CI by using native tox package. From b2bd54a2e31d9a248f73fadb184ae9b4cbdb49f9 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Mon, 2 Mar 2020 16:36:32 +0000 Subject: [PATCH 1135/1623] Add a confirmation step to the SSO login flow --- docs/sample_config.yaml | 34 ++++++++ synapse/config/_base.pyi | 2 + synapse/config/homeserver.py | 2 + synapse/config/sso.py | 74 ++++++++++++++++ .../res/templates/sso_redirect_confirm.html | 14 +++ synapse/rest/client/v1/login.py | 40 +++++++-- tests/rest/client/v1/test_login.py | 85 +++++++++++++++++++ 7 files changed, 245 insertions(+), 6 deletions(-) create mode 100644 synapse/config/sso.py create mode 100644 synapse/res/templates/sso_redirect_confirm.html diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 8a036071e1..bbb8a4d934 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -1360,6 +1360,40 @@ saml2_config: # # name: value +# Additional settings to use with single-sign on systems such as SAML2 and CAS. +# +sso: + # Directory in which Synapse will try to find the template files below. + # If not set, default templates from within the Synapse package will be used. + # + # DO NOT UNCOMMENT THIS SETTING unless you want to customise the templates. + # If you *do* uncomment it, you will need to make sure that all the templates + # below are in the directory. + # + # Synapse will look for the following templates in this directory: + # + # * HTML page for confirmation of redirect during authentication: + # 'sso_redirect_confirm.html'. + # + # When rendering, this template is given three variables: + # * redirect_url: the URL the user is about to be redirected to. Needs + # manual escaping (see + # https://jinja.palletsprojects.com/en/2.11.x/templates/#html-escaping). + # + # * display_url: the same as `redirect_url`, but with the query + # parameters stripped. The intention is to have a + # human-readable URL to show to users, not to use it as + # the final address to redirect to. Needs manual escaping + # (see https://jinja.palletsprojects.com/en/2.11.x/templates/#html-escaping). + # + # * server_name: the homeserver's name. + # + # You can see the default templates at: + # https://github.com/matrix-org/synapse/tree/master/synapse/res/templates + # + #template_dir: "res/templates" + + # The JWT needs to contain a globally unique "sub" (subject) claim. # #jwt_config: diff --git a/synapse/config/_base.pyi b/synapse/config/_base.pyi index 86bc965ee4..3053fc9d27 100644 --- a/synapse/config/_base.pyi +++ b/synapse/config/_base.pyi @@ -24,6 +24,7 @@ from synapse.config import ( server, server_notices_config, spam_checker, + sso, stats, third_party_event_rules, tls, @@ -57,6 +58,7 @@ class RootConfig: key: key.KeyConfig saml2: saml2_config.SAML2Config cas: cas.CasConfig + sso: sso.SSOConfig jwt: jwt_config.JWTConfig password: password.PasswordConfig email: emailconfig.EmailConfig diff --git a/synapse/config/homeserver.py b/synapse/config/homeserver.py index 6e348671c7..b4bca08b20 100644 --- a/synapse/config/homeserver.py +++ b/synapse/config/homeserver.py @@ -38,6 +38,7 @@ from .saml2_config import SAML2Config from .server import ServerConfig from .server_notices_config import ServerNoticesConfig from .spam_checker import SpamCheckerConfig +from .sso import SSOConfig from .stats import StatsConfig from .third_party_event_rules import ThirdPartyRulesConfig from .tls import TlsConfig @@ -65,6 +66,7 @@ class HomeServerConfig(RootConfig): KeyConfig, SAML2Config, CasConfig, + SSOConfig, JWTConfig, PasswordConfig, EmailConfig, diff --git a/synapse/config/sso.py b/synapse/config/sso.py new file mode 100644 index 0000000000..f426b65b4f --- /dev/null +++ b/synapse/config/sso.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Any, Dict + +import pkg_resources + +from ._base import Config, ConfigError + + +class SSOConfig(Config): + """SSO Configuration + """ + + section = "sso" + + def read_config(self, config, **kwargs): + sso_config = config.get("sso") or {} # type: Dict[str, Any] + + # Pick a template directory in order of: + # * The sso-specific template_dir + # * /path/to/synapse/install/res/templates + template_dir = sso_config.get("template_dir") + if not template_dir: + template_dir = pkg_resources.resource_filename("synapse", "res/templates",) + + self.sso_redirect_confirm_template_dir = template_dir + + def generate_config_section(self, **kwargs): + return """\ + # Additional settings to use with single-sign on systems such as SAML2 and CAS. + # + sso: + # Directory in which Synapse will try to find the template files below. + # If not set, default templates from within the Synapse package will be used. + # + # DO NOT UNCOMMENT THIS SETTING unless you want to customise the templates. + # If you *do* uncomment it, you will need to make sure that all the templates + # below are in the directory. + # + # Synapse will look for the following templates in this directory: + # + # * HTML page for a confirmation step before redirecting back to the client + # with the login token: 'sso_redirect_confirm.html'. + # + # When rendering, this template is given three variables: + # * redirect_url: the URL the user is about to be redirected to. Needs + # manual escaping (see + # https://jinja.palletsprojects.com/en/2.11.x/templates/#html-escaping). + # + # * display_url: the same as `redirect_url`, but with the query + # parameters stripped. The intention is to have a + # human-readable URL to show to users, not to use it as + # the final address to redirect to. Needs manual escaping + # (see https://jinja.palletsprojects.com/en/2.11.x/templates/#html-escaping). + # + # * server_name: the homeserver's name. + # + # You can see the default templates at: + # https://github.com/matrix-org/synapse/tree/master/synapse/res/templates + # + #template_dir: "res/templates" + """ diff --git a/synapse/res/templates/sso_redirect_confirm.html b/synapse/res/templates/sso_redirect_confirm.html new file mode 100644 index 0000000000..20a15e1e74 --- /dev/null +++ b/synapse/res/templates/sso_redirect_confirm.html @@ -0,0 +1,14 @@ + + + + + SSO redirect confirmation + + +

    The application at {{ display_url | e }} is requesting full access to your {{ server_name }} Matrix account.

    +

    If you don't recognise this address, you should ignore this and close this tab.

    +

    + I trust this address +

    + + \ No newline at end of file diff --git a/synapse/rest/client/v1/login.py b/synapse/rest/client/v1/login.py index 1294e080dc..1acfd01d8e 100644 --- a/synapse/rest/client/v1/login.py +++ b/synapse/rest/client/v1/login.py @@ -29,6 +29,7 @@ from synapse.http.servlet import ( parse_string, ) from synapse.http.site import SynapseRequest +from synapse.push.mailer import load_jinja2_templates from synapse.rest.client.v2_alpha._base import client_patterns from synapse.rest.well_known import WellKnownBuilder from synapse.types import UserID, map_username_to_mxid_localpart @@ -548,6 +549,13 @@ class SSOAuthHandler(object): self._registration_handler = hs.get_registration_handler() self._macaroon_gen = hs.get_macaroon_generator() + # Load the redirect page HTML template + self._template = load_jinja2_templates( + hs.config.sso_redirect_confirm_template_dir, ["sso_redirect_confirm.html"], + )[0] + + self._server_name = hs.config.server_name + async def on_successful_auth( self, username, request, client_redirect_url, user_display_name=None ): @@ -592,21 +600,41 @@ class SSOAuthHandler(object): request: client_redirect_url: """ - + # Create a login token login_token = self._macaroon_gen.generate_short_term_login_token( registered_user_id ) - redirect_url = self._add_login_token_to_redirect_url( - client_redirect_url, login_token + + # Remove the query parameters from the redirect URL to get a shorter version of + # it. This is only to display a human-readable URL in the template, but not the + # URL we redirect users to. + redirect_url_no_params = client_redirect_url.split("?")[0] + + # Append the login token to the original redirect URL (i.e. with its query + # parameters kept intact) to build the URL to which the template needs to + # redirect the users once they have clicked on the confirmation link. + redirect_url = self._add_query_param_to_url( + client_redirect_url, "loginToken", login_token ) - request.redirect(redirect_url) + + # Serve the redirect confirmation page + html = self._template.render( + display_url=redirect_url_no_params, + redirect_url=redirect_url, + server_name=self._server_name, + ) + + request.setResponseCode(200) + request.setHeader(b"Content-Type", b"text/html; charset=utf-8") + request.setHeader(b"Content-Length", b"%d" % (len(html),)) + request.write(html.encode("utf8")) finish_request(request) @staticmethod - def _add_login_token_to_redirect_url(url, token): + def _add_query_param_to_url(url, param_name, param): url_parts = list(urllib.parse.urlparse(url)) query = dict(urllib.parse.parse_qsl(url_parts[4])) - query.update({"loginToken": token}) + query.update({param_name: param}) url_parts[4] = urllib.parse.urlencode(query) return urllib.parse.urlunparse(url_parts) diff --git a/tests/rest/client/v1/test_login.py b/tests/rest/client/v1/test_login.py index eae5411325..2b8ad5c753 100644 --- a/tests/rest/client/v1/test_login.py +++ b/tests/rest/client/v1/test_login.py @@ -1,4 +1,7 @@ import json +import urllib.parse + +from mock import Mock import synapse.rest.admin from synapse.rest.client.v1 import login @@ -252,3 +255,85 @@ class LoginRestServletTestCase(unittest.HomeserverTestCase): ) self.render(request) self.assertEquals(channel.code, 200, channel.result) + + +class CASRedirectConfirmTestCase(unittest.HomeserverTestCase): + + servlets = [ + login.register_servlets, + ] + + def make_homeserver(self, reactor, clock): + self.base_url = "https://matrix.goodserver.com/" + self.redirect_path = "_synapse/client/login/sso/redirect/confirm" + + config = self.default_config() + config["enable_registration"] = True + config["cas_config"] = { + "enabled": True, + "server_url": "https://fake.test", + "service_url": "https://matrix.goodserver.com:8448", + } + config["public_baseurl"] = self.base_url + + async def get_raw(uri, args): + """Return an example response payload from a call to the `/proxyValidate` + endpoint of a CAS server, copied from + https://apereo.github.io/cas/5.0.x/protocol/CAS-Protocol-V2-Specification.html#26-proxyvalidate-cas-20 + + This needs to be returned by an async function (as opposed to set as the + mock's return value) because the corresponding Synapse code awaits on it. + """ + return """ + + + username + PGTIOU-84678-8a9d... + + https://proxy2/pgtUrl + https://proxy1/pgtUrl + + + + """ + + mocked_http_client = Mock(spec=["get_raw"]) + mocked_http_client.get_raw.side_effect = get_raw + + self.hs = self.setup_test_homeserver( + config=config, proxied_http_client=mocked_http_client, + ) + + return self.hs + + def test_cas_redirect_confirm(self): + """Tests that the SSO login flow serves a confirmation page before redirecting a + user to the redirect URL. + """ + base_url = "/login/cas/ticket?redirectUrl" + redirect_url = "https://dodgy-site.com/" + + url_parts = list(urllib.parse.urlparse(base_url)) + query = dict(urllib.parse.parse_qsl(url_parts[4])) + query.update({"redirectUrl": redirect_url}) + query.update({"ticket": "ticket"}) + url_parts[4] = urllib.parse.urlencode(query) + cas_ticket_url = urllib.parse.urlunparse(url_parts) + + # Get Synapse to call the fake CAS and serve the template. + request, channel = self.make_request("GET", cas_ticket_url) + self.render(request) + + # Test that the response is HTML. + content_type_header_value = "" + for header in channel.result.get("headers", []): + if header[0] == b"Content-Type": + content_type_header_value = header[1].decode("utf8") + + self.assertTrue(content_type_header_value.startswith("text/html")) + + # Test that the body isn't empty. + self.assertTrue(len(channel.result["body"]) > 0) + + # And that it contains our redirect link + self.assertIn(redirect_url, channel.result["body"].decode("UTF-8")) From b29474e0aa866a50ec96cd921cc5025fc9718e73 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 2 Mar 2020 16:52:15 +0000 Subject: [PATCH 1136/1623] Always return a deferred from `get_current_state_deltas`. (#7019) This currently causes presence notify code to log exceptions when there is no state changes to process. This doesn't actually cause any problems as we'd simply do nothing anyway. --- changelog.d/7019.misc | 1 + synapse/storage/data_stores/main/state_deltas.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 changelog.d/7019.misc diff --git a/changelog.d/7019.misc b/changelog.d/7019.misc new file mode 100644 index 0000000000..5130f4e8af --- /dev/null +++ b/changelog.d/7019.misc @@ -0,0 +1 @@ +Port `synapse.handlers.presence` to async/await. diff --git a/synapse/storage/data_stores/main/state_deltas.py b/synapse/storage/data_stores/main/state_deltas.py index 12c982cb26..725e12507f 100644 --- a/synapse/storage/data_stores/main/state_deltas.py +++ b/synapse/storage/data_stores/main/state_deltas.py @@ -15,6 +15,8 @@ import logging +from twisted.internet import defer + from synapse.storage._base import SQLBaseStore logger = logging.getLogger(__name__) @@ -56,7 +58,7 @@ class StateDeltasStore(SQLBaseStore): # if the CSDs haven't changed between prev_stream_id and now, we # know for certain that they haven't changed between prev_stream_id and # max_stream_id. - return max_stream_id, [] + return defer.succeed((max_stream_id, [])) def get_current_state_deltas_txn(txn): # First we calculate the max stream id that will give us less than From b68041df3dcbcf3ca04c500d1712aa22a3c2580c Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 2 Mar 2020 17:05:09 +0000 Subject: [PATCH 1137/1623] Add a whitelist for the SSO confirmation step. --- docs/sample_config.yaml | 22 +++++++++++++++++--- synapse/config/sso.py | 18 +++++++++++++++++ synapse/rest/client/v1/login.py | 26 ++++++++++++++++-------- tests/rest/client/v1/test_login.py | 32 +++++++++++++++++++++++++++--- 4 files changed, 84 insertions(+), 14 deletions(-) diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index bbb8a4d934..f719ec696f 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -1363,6 +1363,22 @@ saml2_config: # Additional settings to use with single-sign on systems such as SAML2 and CAS. # sso: + # A list of client URLs which are whitelisted so that the user does not + # have to confirm giving access to their account to the URL. Any client + # whose URL starts with an entry in the following list will not be subject + # to an additional confirmation step after the SSO login is completed. + # + # WARNING: An entry such as "https://my.client" is insecure, because it + # will also match "https://my.client.evil.site", exposing your users to + # phishing attacks from evil.site. To avoid this, include a slash after the + # hostname: "https://my.client/". + # + # By default, this list is empty. + # + #client_whitelist: + # - https://riot.im/develop + # - https://my.custom.client/ + # Directory in which Synapse will try to find the template files below. # If not set, default templates from within the Synapse package will be used. # @@ -1372,8 +1388,8 @@ sso: # # Synapse will look for the following templates in this directory: # - # * HTML page for confirmation of redirect during authentication: - # 'sso_redirect_confirm.html'. + # * HTML page for a confirmation step before redirecting back to the client + # with the login token: 'sso_redirect_confirm.html'. # # When rendering, this template is given three variables: # * redirect_url: the URL the user is about to be redirected to. Needs @@ -1381,7 +1397,7 @@ sso: # https://jinja.palletsprojects.com/en/2.11.x/templates/#html-escaping). # # * display_url: the same as `redirect_url`, but with the query - # parameters stripped. The intention is to have a + # parameters stripped. The intention is to have a # human-readable URL to show to users, not to use it as # the final address to redirect to. Needs manual escaping # (see https://jinja.palletsprojects.com/en/2.11.x/templates/#html-escaping). diff --git a/synapse/config/sso.py b/synapse/config/sso.py index f426b65b4f..56299bd4e4 100644 --- a/synapse/config/sso.py +++ b/synapse/config/sso.py @@ -37,11 +37,29 @@ class SSOConfig(Config): self.sso_redirect_confirm_template_dir = template_dir + self.sso_client_whitelist = sso_config.get("client_whitelist") or [] + def generate_config_section(self, **kwargs): return """\ # Additional settings to use with single-sign on systems such as SAML2 and CAS. # sso: + # A list of client URLs which are whitelisted so that the user does not + # have to confirm giving access to their account to the URL. Any client + # whose URL starts with an entry in the following list will not be subject + # to an additional confirmation step after the SSO login is completed. + # + # WARNING: An entry such as "https://my.client" is insecure, because it + # will also match "https://my.client.evil.site", exposing your users to + # phishing attacks from evil.site. To avoid this, include a slash after the + # hostname: "https://my.client/". + # + # By default, this list is empty. + # + #client_whitelist: + # - https://riot.im/develop + # - https://my.custom.client/ + # Directory in which Synapse will try to find the template files below. # If not set, default templates from within the Synapse package will be used. # diff --git a/synapse/rest/client/v1/login.py b/synapse/rest/client/v1/login.py index 1acfd01d8e..b2bc7537db 100644 --- a/synapse/rest/client/v1/login.py +++ b/synapse/rest/client/v1/login.py @@ -556,6 +556,9 @@ class SSOAuthHandler(object): self._server_name = hs.config.server_name + # cast to tuple for use with str.startswith + self._whitelisted_sso_clients = tuple(hs.config.sso_client_whitelist) + async def on_successful_auth( self, username, request, client_redirect_url, user_display_name=None ): @@ -605,11 +608,6 @@ class SSOAuthHandler(object): registered_user_id ) - # Remove the query parameters from the redirect URL to get a shorter version of - # it. This is only to display a human-readable URL in the template, but not the - # URL we redirect users to. - redirect_url_no_params = client_redirect_url.split("?")[0] - # Append the login token to the original redirect URL (i.e. with its query # parameters kept intact) to build the URL to which the template needs to # redirect the users once they have clicked on the confirmation link. @@ -617,17 +615,29 @@ class SSOAuthHandler(object): client_redirect_url, "loginToken", login_token ) - # Serve the redirect confirmation page + # if the client is whitelisted, we can redirect straight to it + if client_redirect_url.startswith(self._whitelisted_sso_clients): + request.redirect(redirect_url) + finish_request(request) + return + + # Otherwise, serve the redirect confirmation page. + + # Remove the query parameters from the redirect URL to get a shorter version of + # it. This is only to display a human-readable URL in the template, but not the + # URL we redirect users to. + redirect_url_no_params = client_redirect_url.split("?")[0] + html = self._template.render( display_url=redirect_url_no_params, redirect_url=redirect_url, server_name=self._server_name, - ) + ).encode("utf-8") request.setResponseCode(200) request.setHeader(b"Content-Type", b"text/html; charset=utf-8") request.setHeader(b"Content-Length", b"%d" % (len(html),)) - request.write(html.encode("utf8")) + request.write(html) finish_request(request) @staticmethod diff --git a/tests/rest/client/v1/test_login.py b/tests/rest/client/v1/test_login.py index 2b8ad5c753..da2c9bfa1e 100644 --- a/tests/rest/client/v1/test_login.py +++ b/tests/rest/client/v1/test_login.py @@ -268,13 +268,11 @@ class CASRedirectConfirmTestCase(unittest.HomeserverTestCase): self.redirect_path = "_synapse/client/login/sso/redirect/confirm" config = self.default_config() - config["enable_registration"] = True config["cas_config"] = { "enabled": True, "server_url": "https://fake.test", "service_url": "https://matrix.goodserver.com:8448", } - config["public_baseurl"] = self.base_url async def get_raw(uri, args): """Return an example response payload from a call to the `/proxyValidate` @@ -310,7 +308,7 @@ class CASRedirectConfirmTestCase(unittest.HomeserverTestCase): """Tests that the SSO login flow serves a confirmation page before redirecting a user to the redirect URL. """ - base_url = "/login/cas/ticket?redirectUrl" + base_url = "/_matrix/client/r0/login/cas/ticket?redirectUrl" redirect_url = "https://dodgy-site.com/" url_parts = list(urllib.parse.urlparse(base_url)) @@ -325,6 +323,7 @@ class CASRedirectConfirmTestCase(unittest.HomeserverTestCase): self.render(request) # Test that the response is HTML. + self.assertEqual(channel.code, 200) content_type_header_value = "" for header in channel.result.get("headers", []): if header[0] == b"Content-Type": @@ -337,3 +336,30 @@ class CASRedirectConfirmTestCase(unittest.HomeserverTestCase): # And that it contains our redirect link self.assertIn(redirect_url, channel.result["body"].decode("UTF-8")) + + @override_config( + { + "sso": { + "client_whitelist": [ + "https://legit-site.com/", + "https://other-site.com/", + ] + } + } + ) + def test_cas_redirect_whitelisted(self): + """Tests that the SSO login flow serves a redirect to a whitelisted url + """ + redirect_url = "https://legit-site.com/" + cas_ticket_url = ( + "/_matrix/client/r0/login/cas/ticket?redirectUrl=%s&ticket=ticket" + % (urllib.parse.quote(redirect_url)) + ) + + # Get Synapse to call the fake CAS and serve the template. + request, channel = self.make_request("GET", cas_ticket_url) + self.render(request) + + self.assertEqual(channel.code, 302) + location_headers = channel.headers.getRawHeaders("Location") + self.assertEqual(location_headers[0][: len(redirect_url)], redirect_url) From 65c73cdfec1876a9fec2fd2c3a74923cd146fe0b Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 3 Mar 2020 10:54:44 +0000 Subject: [PATCH 1138/1623] Factor out complete_sso_login and expose it to the Module API --- synapse/config/sso.py | 2 +- synapse/handlers/auth.py | 74 +++++++++++++++++++++++++++++++++ synapse/module_api/__init__.py | 19 +++++++++ synapse/rest/client/v1/login.py | 58 +------------------------- 4 files changed, 96 insertions(+), 57 deletions(-) diff --git a/synapse/config/sso.py b/synapse/config/sso.py index 56299bd4e4..95762689bc 100644 --- a/synapse/config/sso.py +++ b/synapse/config/sso.py @@ -16,7 +16,7 @@ from typing import Any, Dict import pkg_resources -from ._base import Config, ConfigError +from ._base import Config class SSOConfig(Config): diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index 48a88d3c2a..7ca90f91c4 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -17,6 +17,8 @@ import logging import time import unicodedata +import urllib.parse +from typing import Any import attr import bcrypt @@ -38,8 +40,11 @@ from synapse.api.errors import ( from synapse.api.ratelimiting import Ratelimiter from synapse.handlers.ui_auth import INTERACTIVE_AUTH_CHECKERS from synapse.handlers.ui_auth.checkers import UserInteractiveAuthChecker +from synapse.http.server import finish_request +from synapse.http.site import SynapseRequest from synapse.logging.context import defer_to_thread from synapse.module_api import ModuleApi +from synapse.push.mailer import load_jinja2_templates from synapse.types import UserID from synapse.util.caches.expiringcache import ExpiringCache @@ -108,6 +113,16 @@ class AuthHandler(BaseHandler): self._clock = self.hs.get_clock() + # Load the SSO redirect confirmation page HTML template + self._sso_redirect_confirm_template = load_jinja2_templates( + hs.config.sso_redirect_confirm_template_dir, ["sso_redirect_confirm.html"], + )[0] + + self._server_name = hs.config.server_name + + # cast to tuple for use with str.startswith + self._whitelisted_sso_clients = tuple(hs.config.sso_client_whitelist) + @defer.inlineCallbacks def validate_user_via_ui_auth(self, requester, request_body, clientip): """ @@ -927,6 +942,65 @@ class AuthHandler(BaseHandler): else: return defer.succeed(False) + def complete_sso_login( + self, + registered_user_id: str, + request: SynapseRequest, + client_redirect_url: str, + ): + """Having figured out a mxid for this user, complete the HTTP request + + Args: + registered_user_id: The registered user ID to complete SSO login for. + request: The request to complete. + client_redirect_url: The URL to which to redirect the user at the end of the + process. + """ + # Create a login token + login_token = self.macaroon_gen.generate_short_term_login_token( + registered_user_id + ) + + # Append the login token to the original redirect URL (i.e. with its query + # parameters kept intact) to build the URL to which the template needs to + # redirect the users once they have clicked on the confirmation link. + redirect_url = self.add_query_param_to_url( + client_redirect_url, "loginToken", login_token + ) + + # if the client is whitelisted, we can redirect straight to it + if client_redirect_url.startswith(self._whitelisted_sso_clients): + request.redirect(redirect_url) + finish_request(request) + return + + # Otherwise, serve the redirect confirmation page. + + # Remove the query parameters from the redirect URL to get a shorter version of + # it. This is only to display a human-readable URL in the template, but not the + # URL we redirect users to. + redirect_url_no_params = client_redirect_url.split("?")[0] + + html = self._sso_redirect_confirm_template.render( + display_url=redirect_url_no_params, + redirect_url=redirect_url, + server_name=self._server_name, + ).encode("utf-8") + + request.setResponseCode(200) + request.setHeader(b"Content-Type", b"text/html; charset=utf-8") + request.setHeader(b"Content-Length", b"%d" % (len(html),)) + request.write(html) + finish_request(request) + + @staticmethod + def add_query_param_to_url(url: str, param_name: str, param: Any): + url_parts = list(urllib.parse.urlparse(url)) + query = dict(urllib.parse.parse_qsl(url_parts[4])) + query.update({param_name: param}) + url_parts[4] = urllib.parse.urlencode(query) + return urllib.parse.urlunparse(url_parts) + @attr.s class MacaroonGenerator(object): diff --git a/synapse/module_api/__init__.py b/synapse/module_api/__init__.py index d680ee95e1..c7fffd72f2 100644 --- a/synapse/module_api/__init__.py +++ b/synapse/module_api/__init__.py @@ -17,6 +17,7 @@ import logging from twisted.internet import defer +from synapse.http.site import SynapseRequest from synapse.logging.context import make_deferred_yieldable, run_in_background from synapse.types import UserID @@ -211,3 +212,21 @@ class ModuleApi(object): Deferred[object]: result of func """ return self._store.db.runInteraction(desc, func, *args, **kwargs) + + def complete_sso_login( + self, registered_user_id: str, request: SynapseRequest, client_redirect_url: str + ): + """Complete a SSO login by redirecting the user to a page to confirm whether they + want their access token sent to `client_redirect_url`, or redirect them to that + URL with a token directly if the URL matches with one of the whitelisted clients. + + Args: + registered_user_id: The MXID that has been registered as a previous step of + of this SSO login. + request: The request to respond to. + client_redirect_url: The URL to which to offer to redirect the user (or to + redirect them directly if whitelisted). + """ + self._auth_handler.complete_sso_login( + registered_user_id, request, client_redirect_url, + ) diff --git a/synapse/rest/client/v1/login.py b/synapse/rest/client/v1/login.py index b2bc7537db..d0d4999795 100644 --- a/synapse/rest/client/v1/login.py +++ b/synapse/rest/client/v1/login.py @@ -28,7 +28,6 @@ from synapse.http.servlet import ( parse_json_object_from_request, parse_string, ) -from synapse.http.site import SynapseRequest from synapse.push.mailer import load_jinja2_templates from synapse.rest.client.v2_alpha._base import client_patterns from synapse.rest.well_known import WellKnownBuilder @@ -591,63 +590,10 @@ class SSOAuthHandler(object): localpart=localpart, default_display_name=user_display_name ) - self.complete_sso_login(registered_user_id, request, client_redirect_url) - - def complete_sso_login( - self, registered_user_id: str, request: SynapseRequest, client_redirect_url: str - ): - """Having figured out a mxid for this user, complete the HTTP request - - Args: - registered_user_id: - request: - client_redirect_url: - """ - # Create a login token - login_token = self._macaroon_gen.generate_short_term_login_token( - registered_user_id + self._auth_handler.complete_sso_login( + registered_user_id, request, client_redirect_url ) - # Append the login token to the original redirect URL (i.e. with its query - # parameters kept intact) to build the URL to which the template needs to - # redirect the users once they have clicked on the confirmation link. - redirect_url = self._add_query_param_to_url( - client_redirect_url, "loginToken", login_token - ) - - # if the client is whitelisted, we can redirect straight to it - if client_redirect_url.startswith(self._whitelisted_sso_clients): - request.redirect(redirect_url) - finish_request(request) - return - - # Otherwise, serve the redirect confirmation page. - - # Remove the query parameters from the redirect URL to get a shorter version of - # it. This is only to display a human-readable URL in the template, but not the - # URL we redirect users to. - redirect_url_no_params = client_redirect_url.split("?")[0] - - html = self._template.render( - display_url=redirect_url_no_params, - redirect_url=redirect_url, - server_name=self._server_name, - ).encode("utf-8") - - request.setResponseCode(200) - request.setHeader(b"Content-Type", b"text/html; charset=utf-8") - request.setHeader(b"Content-Length", b"%d" % (len(html),)) - request.write(html) - finish_request(request) - - @staticmethod - def _add_query_param_to_url(url, param_name, param): - url_parts = list(urllib.parse.urlparse(url)) - query = dict(urllib.parse.parse_qsl(url_parts[4])) - query.update({param_name: param}) - url_parts[4] = urllib.parse.urlencode(query) - return urllib.parse.urlunparse(url_parts) - def register_servlets(hs, http_server): LoginRestServlet(hs).register(http_server) From a0178df10422a76fd403b82d2b2a4ed28a9a9d1e Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 3 Mar 2020 11:29:07 +0000 Subject: [PATCH 1139/1623] Fix wrong handler being used in SAML handler --- synapse/handlers/saml_handler.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/synapse/handlers/saml_handler.py b/synapse/handlers/saml_handler.py index 7f411b53b9..9406753393 100644 --- a/synapse/handlers/saml_handler.py +++ b/synapse/handlers/saml_handler.py @@ -25,7 +25,6 @@ from synapse.api.errors import SynapseError from synapse.config import ConfigError from synapse.http.servlet import parse_string from synapse.module_api import ModuleApi -from synapse.rest.client.v1.login import SSOAuthHandler from synapse.types import ( UserID, map_username_to_mxid_localpart, @@ -48,7 +47,7 @@ class Saml2SessionData: class SamlHandler: def __init__(self, hs): self._saml_client = Saml2Client(hs.config.saml2_sp_config) - self._sso_auth_handler = SSOAuthHandler(hs) + self._auth_handler = hs.get_auth_handler() self._registration_handler = hs.get_registration_handler() self._clock = hs.get_clock() @@ -116,7 +115,7 @@ class SamlHandler: self.expire_sessions() user_id = await self._map_saml_response_to_user(resp_bytes, relay_state) - self._sso_auth_handler.complete_sso_login(user_id, request, relay_state) + self._auth_handler.complete_sso_login(user_id, request, relay_state) async def _map_saml_response_to_user(self, resp_bytes, client_redirect_url): try: From 7dcbc33a1be04c46b930699c03c15bc759f4b22c Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 3 Mar 2020 07:12:45 -0500 Subject: [PATCH 1140/1623] Validate the alt_aliases property of canonical alias events (#6971) --- changelog.d/6971.feature | 1 + synapse/api/errors.py | 1 + synapse/handlers/directory.py | 14 +-- synapse/handlers/message.py | 47 ++++++++- synapse/types.py | 15 ++- tests/handlers/test_directory.py | 70 ++++++------- tests/rest/client/v1/test_rooms.py | 160 +++++++++++++++++++++++++++++ tests/test_types.py | 2 +- 8 files changed, 256 insertions(+), 54 deletions(-) create mode 100644 changelog.d/6971.feature diff --git a/changelog.d/6971.feature b/changelog.d/6971.feature new file mode 100644 index 0000000000..ccf02a61df --- /dev/null +++ b/changelog.d/6971.feature @@ -0,0 +1 @@ +Validate the alt_aliases property of canonical alias events. diff --git a/synapse/api/errors.py b/synapse/api/errors.py index 0c20601600..616942b057 100644 --- a/synapse/api/errors.py +++ b/synapse/api/errors.py @@ -66,6 +66,7 @@ class Codes(object): EXPIRED_ACCOUNT = "ORG_MATRIX_EXPIRED_ACCOUNT" INVALID_SIGNATURE = "M_INVALID_SIGNATURE" USER_DEACTIVATED = "M_USER_DEACTIVATED" + BAD_ALIAS = "M_BAD_ALIAS" class CodeMessageException(RuntimeError): diff --git a/synapse/handlers/directory.py b/synapse/handlers/directory.py index 0b23ca919a..61eb49059b 100644 --- a/synapse/handlers/directory.py +++ b/synapse/handlers/directory.py @@ -13,8 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. - -import collections import logging import string from typing import List @@ -307,15 +305,17 @@ class DirectoryHandler(BaseHandler): send_update = True content.pop("alias", "") - # Filter alt_aliases for the removed alias. - alt_aliases = content.pop("alt_aliases", None) - # If the aliases are not a list (or not found) do not attempt to modify - # the list. - if isinstance(alt_aliases, collections.Sequence): + # Filter the alt_aliases property for the removed alias. Note that the + # value is not modified if alt_aliases is of an unexpected form. + alt_aliases = content.get("alt_aliases") + if isinstance(alt_aliases, (list, tuple)) and alias_str in alt_aliases: send_update = True alt_aliases = [alias for alias in alt_aliases if alias != alias_str] + if alt_aliases: content["alt_aliases"] = alt_aliases + else: + del content["alt_aliases"] if send_update: yield self.event_creation_handler.create_and_send_nonmember_event( diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index a0103addd3..0c84c6cec4 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -888,19 +888,60 @@ class EventCreationHandler(object): yield self.base_handler.maybe_kick_guest_users(event, context) if event.type == EventTypes.CanonicalAlias: - # Check the alias is acually valid (at this time at least) + # Validate a newly added alias or newly added alt_aliases. + + original_alias = None + original_alt_aliases = set() + + original_event_id = event.unsigned.get("replaces_state") + if original_event_id: + original_event = yield self.store.get_event(original_event_id) + + if original_event: + original_alias = original_event.content.get("alias", None) + original_alt_aliases = original_event.content.get("alt_aliases", []) + + # Check the alias is currently valid (if it has changed). room_alias_str = event.content.get("alias", None) - if room_alias_str: + directory_handler = self.hs.get_handlers().directory_handler + if room_alias_str and room_alias_str != original_alias: room_alias = RoomAlias.from_string(room_alias_str) - directory_handler = self.hs.get_handlers().directory_handler mapping = yield directory_handler.get_association(room_alias) if mapping["room_id"] != event.room_id: raise SynapseError( 400, "Room alias %s does not point to the room" % (room_alias_str,), + Codes.BAD_ALIAS, ) + # Check that alt_aliases is the proper form. + alt_aliases = event.content.get("alt_aliases", []) + if not isinstance(alt_aliases, (list, tuple)): + raise SynapseError( + 400, "The alt_aliases property must be a list.", Codes.INVALID_PARAM + ) + + # If the old version of alt_aliases is of an unknown form, + # completely replace it. + if not isinstance(original_alt_aliases, (list, tuple)): + original_alt_aliases = [] + + # Check that each alias is currently valid. + new_alt_aliases = set(alt_aliases) - set(original_alt_aliases) + if new_alt_aliases: + for alias_str in new_alt_aliases: + room_alias = RoomAlias.from_string(alias_str) + mapping = yield directory_handler.get_association(room_alias) + + if mapping["room_id"] != event.room_id: + raise SynapseError( + 400, + "Room alias %s does not point to the room" + % (room_alias_str,), + Codes.BAD_ALIAS, + ) + federation_handler = self.hs.get_handlers().federation_handler if event.type == EventTypes.Member: diff --git a/synapse/types.py b/synapse/types.py index f3cd465735..acf60baddc 100644 --- a/synapse/types.py +++ b/synapse/types.py @@ -23,7 +23,7 @@ import attr from signedjson.key import decode_verify_key_bytes from unpaddedbase64 import decode_base64 -from synapse.api.errors import SynapseError +from synapse.api.errors import Codes, SynapseError # define a version of typing.Collection that works on python 3.5 if sys.version_info[:3] >= (3, 6, 0): @@ -166,11 +166,13 @@ class DomainSpecificString(namedtuple("DomainSpecificString", ("localpart", "dom return self @classmethod - def from_string(cls, s): + def from_string(cls, s: str): """Parse the string given by 's' into a structure object.""" if len(s) < 1 or s[0:1] != cls.SIGIL: raise SynapseError( - 400, "Expected %s string to start with '%s'" % (cls.__name__, cls.SIGIL) + 400, + "Expected %s string to start with '%s'" % (cls.__name__, cls.SIGIL), + Codes.INVALID_PARAM, ) parts = s[1:].split(":", 1) @@ -179,6 +181,7 @@ class DomainSpecificString(namedtuple("DomainSpecificString", ("localpart", "dom 400, "Expected %s of the form '%slocalname:domain'" % (cls.__name__, cls.SIGIL), + Codes.INVALID_PARAM, ) domain = parts[1] @@ -235,11 +238,13 @@ class GroupID(DomainSpecificString): def from_string(cls, s): group_id = super(GroupID, cls).from_string(s) if not group_id.localpart: - raise SynapseError(400, "Group ID cannot be empty") + raise SynapseError(400, "Group ID cannot be empty", Codes.INVALID_PARAM) if contains_invalid_mxid_characters(group_id.localpart): raise SynapseError( - 400, "Group ID can only contain characters a-z, 0-9, or '=_-./'" + 400, + "Group ID can only contain characters a-z, 0-9, or '=_-./'", + Codes.INVALID_PARAM, ) return group_id diff --git a/tests/handlers/test_directory.py b/tests/handlers/test_directory.py index 27b916aed4..3397cfa485 100644 --- a/tests/handlers/test_directory.py +++ b/tests/handlers/test_directory.py @@ -88,6 +88,7 @@ class DirectoryTestCase(unittest.HomeserverTestCase): ) def test_delete_alias_not_allowed(self): + """Removing an alias should be denied if a user does not have the proper permissions.""" room_id = "!8765qwer:test" self.get_success( self.store.create_room_alias_association(self.my_room, room_id, ["test"]) @@ -101,6 +102,7 @@ class DirectoryTestCase(unittest.HomeserverTestCase): ) def test_delete_alias(self): + """Removing an alias should work when a user does has the proper permissions.""" room_id = "!8765qwer:test" user_id = "@user:test" self.get_success( @@ -159,30 +161,42 @@ class CanonicalAliasTestCase(unittest.HomeserverTestCase): ) self.test_alias = "#test:test" - self.room_alias = RoomAlias.from_string(self.test_alias) + self.room_alias = self._add_alias(self.test_alias) + + def _add_alias(self, alias: str) -> RoomAlias: + """Add an alias to the test room.""" + room_alias = RoomAlias.from_string(alias) # Create a new alias to this room. self.get_success( self.store.create_room_alias_association( - self.room_alias, self.room_id, ["test"], self.admin_user + room_alias, self.room_id, ["test"], self.admin_user + ) + ) + return room_alias + + def _set_canonical_alias(self, content): + """Configure the canonical alias state on the room.""" + self.helper.send_state( + self.room_id, "m.room.canonical_alias", content, tok=self.admin_user_tok, + ) + + def _get_canonical_alias(self): + """Get the canonical alias state of the room.""" + return self.get_success( + self.state_handler.get_current_state( + self.room_id, EventTypes.CanonicalAlias, "" ) ) def test_remove_alias(self): """Removing an alias that is the canonical alias should remove it there too.""" # Set this new alias as the canonical alias for this room - self.helper.send_state( - self.room_id, - "m.room.canonical_alias", - {"alias": self.test_alias, "alt_aliases": [self.test_alias]}, - tok=self.admin_user_tok, + self._set_canonical_alias( + {"alias": self.test_alias, "alt_aliases": [self.test_alias]} ) - data = self.get_success( - self.state_handler.get_current_state( - self.room_id, EventTypes.CanonicalAlias, "" - ) - ) + data = self._get_canonical_alias() self.assertEqual(data["content"]["alias"], self.test_alias) self.assertEqual(data["content"]["alt_aliases"], [self.test_alias]) @@ -193,11 +207,7 @@ class CanonicalAliasTestCase(unittest.HomeserverTestCase): ) ) - data = self.get_success( - self.state_handler.get_current_state( - self.room_id, EventTypes.CanonicalAlias, "" - ) - ) + data = self._get_canonical_alias() self.assertNotIn("alias", data["content"]) self.assertNotIn("alt_aliases", data["content"]) @@ -205,29 +215,17 @@ class CanonicalAliasTestCase(unittest.HomeserverTestCase): """Removing an alias listed as in alt_aliases should remove it there too.""" # Create a second alias. other_test_alias = "#test2:test" - other_room_alias = RoomAlias.from_string(other_test_alias) - self.get_success( - self.store.create_room_alias_association( - other_room_alias, self.room_id, ["test"], self.admin_user - ) - ) + other_room_alias = self._add_alias(other_test_alias) # Set the alias as the canonical alias for this room. - self.helper.send_state( - self.room_id, - "m.room.canonical_alias", + self._set_canonical_alias( { "alias": self.test_alias, "alt_aliases": [self.test_alias, other_test_alias], - }, - tok=self.admin_user_tok, + } ) - data = self.get_success( - self.state_handler.get_current_state( - self.room_id, EventTypes.CanonicalAlias, "" - ) - ) + data = self._get_canonical_alias() self.assertEqual(data["content"]["alias"], self.test_alias) self.assertEqual( data["content"]["alt_aliases"], [self.test_alias, other_test_alias] @@ -240,11 +238,7 @@ class CanonicalAliasTestCase(unittest.HomeserverTestCase): ) ) - data = self.get_success( - self.state_handler.get_current_state( - self.room_id, EventTypes.CanonicalAlias, "" - ) - ) + data = self._get_canonical_alias() self.assertEqual(data["content"]["alias"], self.test_alias) self.assertEqual(data["content"]["alt_aliases"], [self.test_alias]) diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py index 2f3df5f88f..7dd86d0c27 100644 --- a/tests/rest/client/v1/test_rooms.py +++ b/tests/rest/client/v1/test_rooms.py @@ -1821,3 +1821,163 @@ class RoomAliasListTestCase(unittest.HomeserverTestCase): ) self.render(request) self.assertEqual(channel.code, expected_code, channel.result) + + +class RoomCanonicalAliasTestCase(unittest.HomeserverTestCase): + servlets = [ + synapse.rest.admin.register_servlets_for_client_rest_resource, + directory.register_servlets, + login.register_servlets, + room.register_servlets, + ] + + def prepare(self, reactor, clock, homeserver): + self.room_owner = self.register_user("room_owner", "test") + self.room_owner_tok = self.login("room_owner", "test") + + self.room_id = self.helper.create_room_as( + self.room_owner, tok=self.room_owner_tok + ) + + self.alias = "#alias:test" + self._set_alias_via_directory(self.alias) + + def _set_alias_via_directory(self, alias: str, expected_code: int = 200): + url = "/_matrix/client/r0/directory/room/" + alias + data = {"room_id": self.room_id} + request_data = json.dumps(data) + + request, channel = self.make_request( + "PUT", url, request_data, access_token=self.room_owner_tok + ) + self.render(request) + self.assertEqual(channel.code, expected_code, channel.result) + + def _get_canonical_alias(self, expected_code: int = 200) -> JsonDict: + """Calls the endpoint under test. returns the json response object.""" + request, channel = self.make_request( + "GET", + "rooms/%s/state/m.room.canonical_alias" % (self.room_id,), + access_token=self.room_owner_tok, + ) + self.render(request) + self.assertEqual(channel.code, expected_code, channel.result) + res = channel.json_body + self.assertIsInstance(res, dict) + return res + + def _set_canonical_alias(self, content: str, expected_code: int = 200) -> JsonDict: + """Calls the endpoint under test. returns the json response object.""" + request, channel = self.make_request( + "PUT", + "rooms/%s/state/m.room.canonical_alias" % (self.room_id,), + json.dumps(content), + access_token=self.room_owner_tok, + ) + self.render(request) + self.assertEqual(channel.code, expected_code, channel.result) + res = channel.json_body + self.assertIsInstance(res, dict) + return res + + def test_canonical_alias(self): + """Test a basic alias message.""" + # There is no canonical alias to start with. + self._get_canonical_alias(expected_code=404) + + # Create an alias. + self._set_canonical_alias({"alias": self.alias}) + + # Canonical alias now exists! + res = self._get_canonical_alias() + self.assertEqual(res, {"alias": self.alias}) + + # Now remove the alias. + self._set_canonical_alias({}) + + # There is an alias event, but it is empty. + res = self._get_canonical_alias() + self.assertEqual(res, {}) + + def test_alt_aliases(self): + """Test a canonical alias message with alt_aliases.""" + # Create an alias. + self._set_canonical_alias({"alt_aliases": [self.alias]}) + + # Canonical alias now exists! + res = self._get_canonical_alias() + self.assertEqual(res, {"alt_aliases": [self.alias]}) + + # Now remove the alt_aliases. + self._set_canonical_alias({}) + + # There is an alias event, but it is empty. + res = self._get_canonical_alias() + self.assertEqual(res, {}) + + def test_alias_alt_aliases(self): + """Test a canonical alias message with an alias and alt_aliases.""" + # Create an alias. + self._set_canonical_alias({"alias": self.alias, "alt_aliases": [self.alias]}) + + # Canonical alias now exists! + res = self._get_canonical_alias() + self.assertEqual(res, {"alias": self.alias, "alt_aliases": [self.alias]}) + + # Now remove the alias and alt_aliases. + self._set_canonical_alias({}) + + # There is an alias event, but it is empty. + res = self._get_canonical_alias() + self.assertEqual(res, {}) + + def test_partial_modify(self): + """Test removing only the alt_aliases.""" + # Create an alias. + self._set_canonical_alias({"alias": self.alias, "alt_aliases": [self.alias]}) + + # Canonical alias now exists! + res = self._get_canonical_alias() + self.assertEqual(res, {"alias": self.alias, "alt_aliases": [self.alias]}) + + # Now remove the alt_aliases. + self._set_canonical_alias({"alias": self.alias}) + + # There is an alias event, but it is empty. + res = self._get_canonical_alias() + self.assertEqual(res, {"alias": self.alias}) + + def test_add_alias(self): + """Test removing only the alt_aliases.""" + # Create an additional alias. + second_alias = "#second:test" + self._set_alias_via_directory(second_alias) + + # Add the canonical alias. + self._set_canonical_alias({"alias": self.alias, "alt_aliases": [self.alias]}) + + # Then add the second alias. + self._set_canonical_alias( + {"alias": self.alias, "alt_aliases": [self.alias, second_alias]} + ) + + # Canonical alias now exists! + res = self._get_canonical_alias() + self.assertEqual( + res, {"alias": self.alias, "alt_aliases": [self.alias, second_alias]} + ) + + def test_bad_data(self): + """Invalid data for alt_aliases should cause errors.""" + self._set_canonical_alias({"alt_aliases": "@bad:test"}, expected_code=400) + self._set_canonical_alias({"alt_aliases": None}, expected_code=400) + self._set_canonical_alias({"alt_aliases": 0}, expected_code=400) + self._set_canonical_alias({"alt_aliases": 1}, expected_code=400) + self._set_canonical_alias({"alt_aliases": False}, expected_code=400) + self._set_canonical_alias({"alt_aliases": True}, expected_code=400) + self._set_canonical_alias({"alt_aliases": {}}, expected_code=400) + + def test_bad_alias(self): + """An alias which does not point to the room raises a SynapseError.""" + self._set_canonical_alias({"alias": "@unknown:test"}, expected_code=400) + self._set_canonical_alias({"alt_aliases": ["@unknown:test"]}, expected_code=400) diff --git a/tests/test_types.py b/tests/test_types.py index 8d97c751ea..480bea1bdc 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -75,7 +75,7 @@ class GroupIDTestCase(unittest.TestCase): self.fail("Parsing '%s' should raise exception" % id_string) except SynapseError as exc: self.assertEqual(400, exc.code) - self.assertEqual("M_UNKNOWN", exc.errcode) + self.assertEqual("M_INVALID_PARAM", exc.errcode) class MapUsernameTestCase(unittest.TestCase): From fd983fad968941987314501b67147a264e2e927a Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 3 Mar 2020 14:58:37 +0000 Subject: [PATCH 1141/1623] v1.11.1 --- CHANGES.md | 15 +++++++++++++++ changelog.d/6910.bugfix | 1 - changelog.d/6996.bugfix | 1 - synapse/__init__.py | 2 +- 4 files changed, 16 insertions(+), 3 deletions(-) delete mode 100644 changelog.d/6910.bugfix delete mode 100644 changelog.d/6996.bugfix diff --git a/CHANGES.md b/CHANGES.md index ff681762cd..dc9ca05ad1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,18 @@ +Synapse 1.11.1 (2020-03-03) +=========================== + +This release includes a security fix impacting installations using Single Sign-On (i.e. SAML2 or CAS) for authentication. Administrators of such installations are encouraged to upgrade as soon as possible. + +The release also includes fixes for a couple of other bugs. + +Bugfixes +-------- + +- Add a confirmation step to the SSO login flow before redirecting users to the redirect URL. ([b2bd54a2](https://github.com/matrix-org/synapse/commit/b2bd54a2e31d9a248f73fadb184ae9b4cbdb49f9), [65c73cdf](https://github.com/matrix-org/synapse/commit/65c73cdfec1876a9fec2fd2c3a74923cd146fe0b), [a0178df1](https://github.com/matrix-org/synapse/commit/a0178df10422a76fd403b82d2b2a4ed28a9a9d1e)) +- Fixed set a user as an admin with the admin API `PUT /_synapse/admin/v2/users/`. Contributed by @dklimpel. ([\#6910](https://github.com/matrix-org/synapse/issues/6910)) +- Fix bug introduced in Synapse 1.11.0 which sometimes caused errors when joining rooms over federation, with `'coroutine' object has no attribute 'event_id'`. ([\#6996](https://github.com/matrix-org/synapse/issues/6996)) + + Synapse 1.11.0 (2020-02-21) =========================== diff --git a/changelog.d/6910.bugfix b/changelog.d/6910.bugfix deleted file mode 100644 index 707f1ff7b5..0000000000 --- a/changelog.d/6910.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fixed set a user as an admin with the admin API `PUT /_synapse/admin/v2/users/`. Contributed by @dklimpel. diff --git a/changelog.d/6996.bugfix b/changelog.d/6996.bugfix deleted file mode 100644 index 765d376c7c..0000000000 --- a/changelog.d/6996.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix bug which caused an error when joining a room, with `'coroutine' object has no attribute 'event_id'`. diff --git a/synapse/__init__.py b/synapse/__init__.py index 3406ce634f..e56ba89ff4 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -36,7 +36,7 @@ try: except ImportError: pass -__version__ = "1.11.0" +__version__ = "1.11.1" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 6b0ef34706e32121ff6f802d62b3eb8545785afe Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 3 Mar 2020 15:01:43 +0000 Subject: [PATCH 1142/1623] Update debian changelog --- debian/changelog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/debian/changelog b/debian/changelog index fbb44cb94b..c39ea8f47f 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.11.1) stable; urgency=medium + + * New synapse release 1.11.1. + + -- Synapse Packaging team Tue, 03 Mar 2020 15:01:22 +0000 + matrix-synapse-py3 (1.11.0) stable; urgency=medium * New synapse release 1.11.0. From 8ef8fb2c1c7c4aeb80fce4deea477b37754ce539 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 4 Mar 2020 13:11:04 +0000 Subject: [PATCH 1143/1623] Read the room version from database when fetching events (#6874) This is a precursor to giving EventBase objects the knowledge of which room version they belong to. --- changelog.d/6874.misc | 1 + .../storage/data_stores/main/events_worker.py | 84 +++++++++++++++---- .../replication/slave/storage/test_events.py | 10 +++ 3 files changed, 79 insertions(+), 16 deletions(-) create mode 100644 changelog.d/6874.misc diff --git a/changelog.d/6874.misc b/changelog.d/6874.misc new file mode 100644 index 0000000000..08aa80bcd9 --- /dev/null +++ b/changelog.d/6874.misc @@ -0,0 +1 @@ +Refactoring work in preparation for changing the event redaction algorithm. diff --git a/synapse/storage/data_stores/main/events_worker.py b/synapse/storage/data_stores/main/events_worker.py index 47a3a26072..ca237c6f12 100644 --- a/synapse/storage/data_stores/main/events_worker.py +++ b/synapse/storage/data_stores/main/events_worker.py @@ -28,9 +28,12 @@ from twisted.internet import defer from synapse.api.constants import EventTypes from synapse.api.errors import NotFoundError -from synapse.api.room_versions import EventFormatVersions -from synapse.events import FrozenEvent, event_type_from_format_version # noqa: F401 -from synapse.events.snapshot import EventContext # noqa: F401 +from synapse.api.room_versions import ( + KNOWN_ROOM_VERSIONS, + EventFormatVersions, + RoomVersions, +) +from synapse.events import make_event_from_dict from synapse.events.utils import prune_event from synapse.logging.context import LoggingContext, PreserveLoggingContext from synapse.metrics.background_process_metrics import run_as_background_process @@ -580,8 +583,49 @@ class EventsWorkerStore(SQLBaseStore): # of a event format version, so it must be a V1 event. format_version = EventFormatVersions.V1 - original_ev = event_type_from_format_version(format_version)( + room_version_id = row["room_version_id"] + + if not room_version_id: + # this should only happen for out-of-band membership events + if not internal_metadata.get("out_of_band_membership"): + logger.warning( + "Room %s for event %s is unknown", d["room_id"], event_id + ) + continue + + # take a wild stab at the room version based on the event format + if format_version == EventFormatVersions.V1: + room_version = RoomVersions.V1 + elif format_version == EventFormatVersions.V2: + room_version = RoomVersions.V3 + else: + room_version = RoomVersions.V5 + else: + room_version = KNOWN_ROOM_VERSIONS.get(room_version_id) + if not room_version: + logger.error( + "Event %s in room %s has unknown room version %s", + event_id, + d["room_id"], + room_version_id, + ) + continue + + if room_version.event_format != format_version: + logger.error( + "Event %s in room %s with version %s has wrong format: " + "expected %s, was %s", + event_id, + d["room_id"], + room_version_id, + room_version.event_format, + format_version, + ) + continue + + original_ev = make_event_from_dict( event_dict=d, + room_version=room_version, internal_metadata_dict=internal_metadata, rejected_reason=rejected_reason, ) @@ -661,6 +705,12 @@ class EventsWorkerStore(SQLBaseStore): of EventFormatVersions. 'None' means the event predates EventFormatVersions (so the event is format V1). + * room_version_id (str|None): The version of the room which contains the event. + Hopefully one of RoomVersions. + + Due to historical reasons, there may be a few events in the database which + do not have an associated room; in this case None will be returned here. + * rejected_reason (str|None): if the event was rejected, the reason why. @@ -676,17 +726,18 @@ class EventsWorkerStore(SQLBaseStore): """ event_dict = {} for evs in batch_iter(event_ids, 200): - sql = ( - "SELECT " - " e.event_id, " - " e.internal_metadata," - " e.json," - " e.format_version, " - " rej.reason " - " FROM event_json as e" - " LEFT JOIN rejections as rej USING (event_id)" - " WHERE " - ) + sql = """\ + SELECT + e.event_id, + e.internal_metadata, + e.json, + e.format_version, + r.room_version, + rej.reason + FROM event_json as e + LEFT JOIN rooms r USING (room_id) + LEFT JOIN rejections as rej USING (event_id) + WHERE """ clause, args = make_in_list_sql_clause( txn.database_engine, "e.event_id", evs @@ -701,7 +752,8 @@ class EventsWorkerStore(SQLBaseStore): "internal_metadata": row[1], "json": row[2], "format_version": row[3], - "rejected_reason": row[4], + "room_version_id": row[4], + "rejected_reason": row[5], "redactions": [], } diff --git a/tests/replication/slave/storage/test_events.py b/tests/replication/slave/storage/test_events.py index d31210fbe4..f0561b30e3 100644 --- a/tests/replication/slave/storage/test_events.py +++ b/tests/replication/slave/storage/test_events.py @@ -15,6 +15,7 @@ import logging from canonicaljson import encode_canonical_json +from synapse.api.room_versions import RoomVersions from synapse.events import FrozenEvent, _EventInternalMetadata, make_event_from_dict from synapse.events.snapshot import EventContext from synapse.handlers.room import RoomEventSource @@ -58,6 +59,15 @@ class SlavedEventStoreTestCase(BaseSlavedStoreTestCase): self.unpatches = [patch__eq__(_EventInternalMetadata), patch__eq__(FrozenEvent)] return super(SlavedEventStoreTestCase, self).setUp() + def prepare(self, *args, **kwargs): + super().prepare(*args, **kwargs) + + self.get_success( + self.master_store.store_room( + ROOM_ID, USER_ID, is_public=False, room_version=RoomVersions.V1, + ) + ) + def tearDown(self): [unpatch() for unpatch in self.unpatches] From 13892776ef7e0b1af2f82c9ca53f7bbd1c60d66f Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 4 Mar 2020 11:30:46 -0500 Subject: [PATCH 1144/1623] Allow deleting an alias if the user has sufficient power level (#6986) --- changelog.d/6986.feature | 1 + synapse/api/auth.py | 9 +- synapse/handlers/directory.py | 107 +++++++++++++++------- tests/handlers/test_directory.py | 148 ++++++++++++++++++++++++------- tox.ini | 1 + 5 files changed, 192 insertions(+), 74 deletions(-) create mode 100644 changelog.d/6986.feature diff --git a/changelog.d/6986.feature b/changelog.d/6986.feature new file mode 100644 index 0000000000..16dea8bd7f --- /dev/null +++ b/changelog.d/6986.feature @@ -0,0 +1 @@ +Users with a power level sufficient to modify the canonical alias of a room can now delete room aliases. diff --git a/synapse/api/auth.py b/synapse/api/auth.py index 5ca18b4301..c1ade1333b 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -539,7 +539,7 @@ class Auth(object): @defer.inlineCallbacks def check_can_change_room_list(self, room_id: str, user: UserID): - """Check if the user is allowed to edit the room's entry in the + """Determine whether the user is allowed to edit the room's entry in the published room list. Args: @@ -570,12 +570,7 @@ class Auth(object): ) user_level = event_auth.get_user_power_level(user_id, auth_events) - if user_level < send_level: - raise AuthError( - 403, - "This server requires you to be a moderator in the room to" - " edit its room list entry", - ) + return user_level >= send_level @staticmethod def has_access_token(request): diff --git a/synapse/handlers/directory.py b/synapse/handlers/directory.py index 61eb49059b..1d842c369b 100644 --- a/synapse/handlers/directory.py +++ b/synapse/handlers/directory.py @@ -15,7 +15,7 @@ import logging import string -from typing import List +from typing import Iterable, List, Optional from twisted.internet import defer @@ -28,6 +28,7 @@ from synapse.api.errors import ( StoreError, SynapseError, ) +from synapse.appservice import ApplicationService from synapse.types import Requester, RoomAlias, UserID, get_domain_from_id from ._base import BaseHandler @@ -55,7 +56,13 @@ class DirectoryHandler(BaseHandler): self.spam_checker = hs.get_spam_checker() @defer.inlineCallbacks - def _create_association(self, room_alias, room_id, servers=None, creator=None): + def _create_association( + self, + room_alias: RoomAlias, + room_id: str, + servers: Optional[Iterable[str]] = None, + creator: Optional[str] = None, + ): # general association creation for both human users and app services for wchar in string.whitespace: @@ -81,17 +88,21 @@ class DirectoryHandler(BaseHandler): @defer.inlineCallbacks def create_association( - self, requester, room_alias, room_id, servers=None, check_membership=True, + self, + requester: Requester, + room_alias: RoomAlias, + room_id: str, + servers: Optional[List[str]] = None, + check_membership: bool = True, ): """Attempt to create a new alias Args: - requester (Requester) - room_alias (RoomAlias) - room_id (str) - servers (list[str]|None): List of servers that others servers - should try and join via - check_membership (bool): Whether to check if the user is in the room + requester + room_alias + room_id + servers: Iterable of servers that others servers should try and join via + check_membership: Whether to check if the user is in the room before the alias can be set (if the server's config requires it). Returns: @@ -145,15 +156,15 @@ class DirectoryHandler(BaseHandler): yield self._create_association(room_alias, room_id, servers, creator=user_id) @defer.inlineCallbacks - def delete_association(self, requester, room_alias): + def delete_association(self, requester: Requester, room_alias: RoomAlias): """Remove an alias from the directory (this is only meant for human users; AS users should call delete_appservice_association) Args: - requester (Requester): - room_alias (RoomAlias): + requester + room_alias Returns: Deferred[unicode]: room id that the alias used to point to @@ -189,16 +200,16 @@ class DirectoryHandler(BaseHandler): room_id = yield self._delete_association(room_alias) try: - yield self._update_canonical_alias( - requester, requester.user.to_string(), room_id, room_alias - ) + yield self._update_canonical_alias(requester, user_id, room_id, room_alias) except AuthError as e: logger.info("Failed to update alias events: %s", e) return room_id @defer.inlineCallbacks - def delete_appservice_association(self, service, room_alias): + def delete_appservice_association( + self, service: ApplicationService, room_alias: RoomAlias + ): if not service.is_interested_in_alias(room_alias.to_string()): raise SynapseError( 400, @@ -208,7 +219,7 @@ class DirectoryHandler(BaseHandler): yield self._delete_association(room_alias) @defer.inlineCallbacks - def _delete_association(self, room_alias): + def _delete_association(self, room_alias: RoomAlias): if not self.hs.is_mine(room_alias): raise SynapseError(400, "Room alias must be local") @@ -217,7 +228,7 @@ class DirectoryHandler(BaseHandler): return room_id @defer.inlineCallbacks - def get_association(self, room_alias): + def get_association(self, room_alias: RoomAlias): room_id = None if self.hs.is_mine(room_alias): result = yield self.get_association_from_room_alias(room_alias) @@ -282,7 +293,9 @@ class DirectoryHandler(BaseHandler): ) @defer.inlineCallbacks - def _update_canonical_alias(self, requester, user_id, room_id, room_alias): + def _update_canonical_alias( + self, requester: Requester, user_id: str, room_id: str, room_alias: RoomAlias + ): """ Send an updated canonical alias event if the removed alias was set as the canonical alias or listed in the alt_aliases field. @@ -331,7 +344,7 @@ class DirectoryHandler(BaseHandler): ) @defer.inlineCallbacks - def get_association_from_room_alias(self, room_alias): + def get_association_from_room_alias(self, room_alias: RoomAlias): result = yield self.store.get_association_from_room_alias(room_alias) if not result: # Query AS to see if it exists @@ -339,7 +352,7 @@ class DirectoryHandler(BaseHandler): result = yield as_handler.query_room_alias_exists(room_alias) return result - def can_modify_alias(self, alias, user_id=None): + def can_modify_alias(self, alias: RoomAlias, user_id: Optional[str] = None): # Any application service "interested" in an alias they are regexing on # can modify the alias. # Users can only modify the alias if ALL the interested services have @@ -360,22 +373,42 @@ class DirectoryHandler(BaseHandler): return defer.succeed(True) @defer.inlineCallbacks - def _user_can_delete_alias(self, alias, user_id): + def _user_can_delete_alias(self, alias: RoomAlias, user_id: str): + """Determine whether a user can delete an alias. + + One of the following must be true: + + 1. The user created the alias. + 2. The user is a server administrator. + 3. The user has a power-level sufficient to send a canonical alias event + for the current room. + + """ creator = yield self.store.get_room_alias_creator(alias.to_string()) if creator is not None and creator == user_id: return True - is_admin = yield self.auth.is_server_admin(UserID.from_string(user_id)) - return is_admin + # Resolve the alias to the corresponding room. + room_mapping = yield self.get_association(alias) + room_id = room_mapping["room_id"] + if not room_id: + return False + + res = yield self.auth.check_can_change_room_list( + room_id, UserID.from_string(user_id) + ) + return res @defer.inlineCallbacks - def edit_published_room_list(self, requester, room_id, visibility): + def edit_published_room_list( + self, requester: Requester, room_id: str, visibility: str + ): """Edit the entry of the room in the published room list. requester - room_id (str) - visibility (str): "public" or "private" + room_id + visibility: "public" or "private" """ user_id = requester.user.to_string() @@ -400,7 +433,15 @@ class DirectoryHandler(BaseHandler): if room is None: raise SynapseError(400, "Unknown room") - yield self.auth.check_can_change_room_list(room_id, requester.user) + can_change_room_list = yield self.auth.check_can_change_room_list( + room_id, requester.user + ) + if not can_change_room_list: + raise AuthError( + 403, + "This server requires you to be a moderator in the room to" + " edit its room list entry", + ) making_public = visibility == "public" if making_public: @@ -421,16 +462,16 @@ class DirectoryHandler(BaseHandler): @defer.inlineCallbacks def edit_published_appservice_room_list( - self, appservice_id, network_id, room_id, visibility + self, appservice_id: str, network_id: str, room_id: str, visibility: str ): """Add or remove a room from the appservice/network specific public room list. Args: - appservice_id (str): ID of the appservice that owns the list - network_id (str): The ID of the network the list is associated with - room_id (str) - visibility (str): either "public" or "private" + appservice_id: ID of the appservice that owns the list + network_id: The ID of the network the list is associated with + room_id + visibility: either "public" or "private" """ if visibility not in ["public", "private"]: raise SynapseError(400, "Invalid visibility setting") diff --git a/tests/handlers/test_directory.py b/tests/handlers/test_directory.py index 3397cfa485..5e40adba52 100644 --- a/tests/handlers/test_directory.py +++ b/tests/handlers/test_directory.py @@ -18,6 +18,7 @@ from mock import Mock from twisted.internet import defer +import synapse import synapse.api.errors from synapse.api.constants import EventTypes from synapse.config.room_directory import RoomDirectoryConfig @@ -87,40 +88,6 @@ class DirectoryTestCase(unittest.HomeserverTestCase): ignore_backoff=True, ) - def test_delete_alias_not_allowed(self): - """Removing an alias should be denied if a user does not have the proper permissions.""" - room_id = "!8765qwer:test" - self.get_success( - self.store.create_room_alias_association(self.my_room, room_id, ["test"]) - ) - - self.get_failure( - self.handler.delete_association( - create_requester("@user:test"), self.my_room - ), - synapse.api.errors.AuthError, - ) - - def test_delete_alias(self): - """Removing an alias should work when a user does has the proper permissions.""" - room_id = "!8765qwer:test" - user_id = "@user:test" - self.get_success( - self.store.create_room_alias_association( - self.my_room, room_id, ["test"], user_id - ) - ) - - result = self.get_success( - self.handler.delete_association(create_requester(user_id), self.my_room) - ) - self.assertEquals(room_id, result) - - # The alias should not be found. - self.get_failure( - self.handler.get_association(self.my_room), synapse.api.errors.SynapseError - ) - def test_incoming_fed_query(self): self.get_success( self.store.create_room_alias_association( @@ -135,6 +102,119 @@ class DirectoryTestCase(unittest.HomeserverTestCase): self.assertEquals({"room_id": "!8765asdf:test", "servers": ["test"]}, response) +class TestDeleteAlias(unittest.HomeserverTestCase): + servlets = [ + synapse.rest.admin.register_servlets, + login.register_servlets, + room.register_servlets, + directory.register_servlets, + ] + + def prepare(self, reactor, clock, hs): + self.store = hs.get_datastore() + self.handler = hs.get_handlers().directory_handler + self.state_handler = hs.get_state_handler() + + # Create user + self.admin_user = self.register_user("admin", "pass", admin=True) + self.admin_user_tok = self.login("admin", "pass") + + # Create a test room + self.room_id = self.helper.create_room_as( + self.admin_user, tok=self.admin_user_tok + ) + + self.test_alias = "#test:test" + self.room_alias = RoomAlias.from_string(self.test_alias) + + # Create a test user. + self.test_user = self.register_user("user", "pass", admin=False) + self.test_user_tok = self.login("user", "pass") + self.helper.join(room=self.room_id, user=self.test_user, tok=self.test_user_tok) + + def _create_alias(self, user): + # Create a new alias to this room. + self.get_success( + self.store.create_room_alias_association( + self.room_alias, self.room_id, ["test"], user + ) + ) + + def test_delete_alias_not_allowed(self): + """A user that doesn't meet the expected guidelines cannot delete an alias.""" + self._create_alias(self.admin_user) + self.get_failure( + self.handler.delete_association( + create_requester(self.test_user), self.room_alias + ), + synapse.api.errors.AuthError, + ) + + def test_delete_alias_creator(self): + """An alias creator can delete their own alias.""" + # Create an alias from a different user. + self._create_alias(self.test_user) + + # Delete the user's alias. + result = self.get_success( + self.handler.delete_association( + create_requester(self.test_user), self.room_alias + ) + ) + self.assertEquals(self.room_id, result) + + # Confirm the alias is gone. + self.get_failure( + self.handler.get_association(self.room_alias), + synapse.api.errors.SynapseError, + ) + + def test_delete_alias_admin(self): + """A server admin can delete an alias created by another user.""" + # Create an alias from a different user. + self._create_alias(self.test_user) + + # Delete the user's alias as the admin. + result = self.get_success( + self.handler.delete_association( + create_requester(self.admin_user), self.room_alias + ) + ) + self.assertEquals(self.room_id, result) + + # Confirm the alias is gone. + self.get_failure( + self.handler.get_association(self.room_alias), + synapse.api.errors.SynapseError, + ) + + def test_delete_alias_sufficient_power(self): + """A user with a sufficient power level should be able to delete an alias.""" + self._create_alias(self.admin_user) + + # Increase the user's power level. + self.helper.send_state( + self.room_id, + "m.room.power_levels", + {"users": {self.test_user: 100}}, + tok=self.admin_user_tok, + ) + + # They can now delete the alias. + result = self.get_success( + self.handler.delete_association( + create_requester(self.test_user), self.room_alias + ) + ) + self.assertEquals(self.room_id, result) + + # Confirm the alias is gone. + self.get_failure( + self.handler.get_association(self.room_alias), + synapse.api.errors.SynapseError, + ) + + class CanonicalAliasTestCase(unittest.HomeserverTestCase): """Test modifications of the canonical alias when delete aliases. """ diff --git a/tox.ini b/tox.ini index 097ebb8774..7622aa19f1 100644 --- a/tox.ini +++ b/tox.ini @@ -185,6 +185,7 @@ commands = mypy \ synapse/federation/federation_client.py \ synapse/federation/sender \ synapse/federation/transport \ + synapse/handlers/directory.py \ synapse/handlers/presence.py \ synapse/handlers/sync.py \ synapse/handlers/ui_auth \ From 31a2116331fea015fe162f298eca19d9a5a58ecb Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 4 Mar 2020 17:28:13 +0000 Subject: [PATCH 1145/1623] Hide extremities dummy events from clients --- synapse/visibility.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/synapse/visibility.py b/synapse/visibility.py index e60d9756b7..a48a4f3dfe 100644 --- a/synapse/visibility.py +++ b/synapse/visibility.py @@ -119,6 +119,9 @@ def filter_events_for_client( the original event if they can see it as normal. """ + if event.type == "org.matrix.dummy_event": + return None + if not event.is_state() and event.sender in ignore_list: return None From 83b6c69d3d0c6249610ed33a86f3d0526334089c Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 4 Mar 2020 17:29:09 +0000 Subject: [PATCH 1146/1623] Changelog --- changelog.d/7035.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7035.bugfix diff --git a/changelog.d/7035.bugfix b/changelog.d/7035.bugfix new file mode 100644 index 0000000000..56292dc8ac --- /dev/null +++ b/changelog.d/7035.bugfix @@ -0,0 +1 @@ +Fix a bug causing `org.matrix.dummy_event` to be included in responses from `/sync`. From 78a15b1f9d3ba3aca49dc4332e86203180d5c863 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 5 Mar 2020 15:46:44 +0000 Subject: [PATCH 1147/1623] Store room_versions in EventBase objects (#6875) This is a bit fiddly because it all has to be done on one fell swoop: * Wherever we create a new event, pass in the room version (and check it matches the format version) * When we prune an event, use the room version of the unpruned event to create the pruned version. * When we pass an event over the replication protocol, pass the room version over alongside it, and use it when deserialising the event again. --- changelog.d/6875.misc | 1 + synapse/events/__init__.py | 53 +++++++++++++++++++------- synapse/events/utils.py | 14 ++----- synapse/replication/http/federation.py | 13 +++++-- synapse/replication/http/send_event.py | 14 +++++-- 5 files changed, 63 insertions(+), 32 deletions(-) create mode 100644 changelog.d/6875.misc diff --git a/changelog.d/6875.misc b/changelog.d/6875.misc new file mode 100644 index 0000000000..08aa80bcd9 --- /dev/null +++ b/changelog.d/6875.misc @@ -0,0 +1 @@ +Refactoring work in preparation for changing the event redaction algorithm. diff --git a/synapse/events/__init__.py b/synapse/events/__init__.py index 7307116556..533ba327f5 100644 --- a/synapse/events/__init__.py +++ b/synapse/events/__init__.py @@ -15,9 +15,10 @@ # See the License for the specific language governing permissions and # limitations under the License. +import abc import os from distutils.util import strtobool -from typing import Optional, Type +from typing import Dict, Optional, Type import six @@ -199,15 +200,25 @@ class _EventInternalMetadata(object): return self._dict.get("redacted", False) -class EventBase(object): +class EventBase(metaclass=abc.ABCMeta): + @property + @abc.abstractmethod + def format_version(self) -> int: + """The EventFormatVersion implemented by this event""" + ... + def __init__( self, - event_dict, - signatures={}, - unsigned={}, - internal_metadata_dict={}, - rejected_reason=None, + event_dict: JsonDict, + room_version: RoomVersion, + signatures: Dict[str, Dict[str, str]], + unsigned: JsonDict, + internal_metadata_dict: JsonDict, + rejected_reason: Optional[str], ): + assert room_version.event_format == self.format_version + + self.room_version = room_version self.signatures = signatures self.unsigned = unsigned self.rejected_reason = rejected_reason @@ -303,7 +314,13 @@ class EventBase(object): class FrozenEvent(EventBase): format_version = EventFormatVersions.V1 # All events of this type are V1 - def __init__(self, event_dict, internal_metadata_dict={}, rejected_reason=None): + def __init__( + self, + event_dict: JsonDict, + room_version: RoomVersion, + internal_metadata_dict: JsonDict = {}, + rejected_reason: Optional[str] = None, + ): event_dict = dict(event_dict) # Signatures is a dict of dicts, and this is faster than doing a @@ -326,8 +343,9 @@ class FrozenEvent(EventBase): self._event_id = event_dict["event_id"] - super(FrozenEvent, self).__init__( + super().__init__( frozen_dict, + room_version=room_version, signatures=signatures, unsigned=unsigned, internal_metadata_dict=internal_metadata_dict, @@ -352,7 +370,13 @@ class FrozenEvent(EventBase): class FrozenEventV2(EventBase): format_version = EventFormatVersions.V2 # All events of this type are V2 - def __init__(self, event_dict, internal_metadata_dict={}, rejected_reason=None): + def __init__( + self, + event_dict: JsonDict, + room_version: RoomVersion, + internal_metadata_dict: JsonDict = {}, + rejected_reason: Optional[str] = None, + ): event_dict = dict(event_dict) # Signatures is a dict of dicts, and this is faster than doing a @@ -377,8 +401,9 @@ class FrozenEventV2(EventBase): self._event_id = None - super(FrozenEventV2, self).__init__( + super().__init__( frozen_dict, + room_version=room_version, signatures=signatures, unsigned=unsigned, internal_metadata_dict=internal_metadata_dict, @@ -445,7 +470,7 @@ class FrozenEventV3(FrozenEventV2): return self._event_id -def event_type_from_format_version(format_version: int) -> Type[EventBase]: +def _event_type_from_format_version(format_version: int) -> Type[EventBase]: """Returns the python type to use to construct an Event object for the given event format version. @@ -474,5 +499,5 @@ def make_event_from_dict( rejected_reason: Optional[str] = None, ) -> EventBase: """Construct an EventBase from the given event dict""" - event_type = event_type_from_format_version(room_version.event_format) - return event_type(event_dict, internal_metadata_dict, rejected_reason) + event_type = _event_type_from_format_version(room_version.event_format) + return event_type(event_dict, room_version, internal_metadata_dict, rejected_reason) diff --git a/synapse/events/utils.py b/synapse/events/utils.py index f70f5032fb..bc6f98ae3b 100644 --- a/synapse/events/utils.py +++ b/synapse/events/utils.py @@ -35,26 +35,20 @@ from . import EventBase SPLIT_FIELD_REGEX = re.compile(r"(? EventBase: """ Returns a pruned version of the given event, which removes all keys we don't know about or think could potentially be dodgy. This is used when we "redact" an event. We want to remove all fields that the user has specified, but we do want to keep necessary information like type, state_key etc. - - Args: - event (FrozenEvent) - - Returns: - FrozenEvent """ pruned_event_dict = prune_event_dict(event.get_dict()) - from . import event_type_from_format_version + from . import make_event_from_dict - pruned_event = event_type_from_format_version(event.format_version)( - pruned_event_dict, event.internal_metadata.get_dict() + pruned_event = make_event_from_dict( + pruned_event_dict, event.room_version, event.internal_metadata.get_dict() ) # Mark the event as redacted diff --git a/synapse/replication/http/federation.py b/synapse/replication/http/federation.py index 8794720101..7e23b565b9 100644 --- a/synapse/replication/http/federation.py +++ b/synapse/replication/http/federation.py @@ -18,7 +18,7 @@ import logging from twisted.internet import defer from synapse.api.room_versions import KNOWN_ROOM_VERSIONS -from synapse.events import event_type_from_format_version +from synapse.events import make_event_from_dict from synapse.events.snapshot import EventContext from synapse.http.servlet import parse_json_object_from_request from synapse.replication.http._base import ReplicationEndpoint @@ -38,6 +38,9 @@ class ReplicationFederationSendEventsRestServlet(ReplicationEndpoint): { "events": [{ "event": { .. serialized event .. }, + "room_version": .., // "1", "2", "3", etc: the version of the room + // containing the event + "event_format_version": .., // 1,2,3 etc: the event format version "internal_metadata": { .. serialized internal_metadata .. }, "rejected_reason": .., // The event.rejected_reason field "context": { .. serialized event context .. }, @@ -73,6 +76,7 @@ class ReplicationFederationSendEventsRestServlet(ReplicationEndpoint): event_payloads.append( { "event": event.get_pdu_json(), + "room_version": event.room_version.identifier, "event_format_version": event.format_version, "internal_metadata": event.internal_metadata.get_dict(), "rejected_reason": event.rejected_reason, @@ -95,12 +99,13 @@ class ReplicationFederationSendEventsRestServlet(ReplicationEndpoint): event_and_contexts = [] for event_payload in event_payloads: event_dict = event_payload["event"] - format_ver = event_payload["event_format_version"] + room_ver = KNOWN_ROOM_VERSIONS[event_payload["room_version"]] internal_metadata = event_payload["internal_metadata"] rejected_reason = event_payload["rejected_reason"] - EventType = event_type_from_format_version(format_ver) - event = EventType(event_dict, internal_metadata, rejected_reason) + event = make_event_from_dict( + event_dict, room_ver, internal_metadata, rejected_reason + ) context = EventContext.deserialize( self.storage, event_payload["context"] diff --git a/synapse/replication/http/send_event.py b/synapse/replication/http/send_event.py index 84b92f16ad..b74b088ff4 100644 --- a/synapse/replication/http/send_event.py +++ b/synapse/replication/http/send_event.py @@ -17,7 +17,8 @@ import logging from twisted.internet import defer -from synapse.events import event_type_from_format_version +from synapse.api.room_versions import KNOWN_ROOM_VERSIONS +from synapse.events import make_event_from_dict from synapse.events.snapshot import EventContext from synapse.http.servlet import parse_json_object_from_request from synapse.replication.http._base import ReplicationEndpoint @@ -37,6 +38,9 @@ class ReplicationSendEventRestServlet(ReplicationEndpoint): { "event": { .. serialized event .. }, + "room_version": .., // "1", "2", "3", etc: the version of the room + // containing the event + "event_format_version": .., // 1,2,3 etc: the event format version "internal_metadata": { .. serialized internal_metadata .. }, "rejected_reason": .., // The event.rejected_reason field "context": { .. serialized event context .. }, @@ -77,6 +81,7 @@ class ReplicationSendEventRestServlet(ReplicationEndpoint): payload = { "event": event.get_pdu_json(), + "room_version": event.room_version.identifier, "event_format_version": event.format_version, "internal_metadata": event.internal_metadata.get_dict(), "rejected_reason": event.rejected_reason, @@ -93,12 +98,13 @@ class ReplicationSendEventRestServlet(ReplicationEndpoint): content = parse_json_object_from_request(request) event_dict = content["event"] - format_ver = content["event_format_version"] + room_ver = KNOWN_ROOM_VERSIONS[content["room_version"]] internal_metadata = content["internal_metadata"] rejected_reason = content["rejected_reason"] - EventType = event_type_from_format_version(format_ver) - event = EventType(event_dict, internal_metadata, rejected_reason) + event = make_event_from_dict( + event_dict, room_ver, internal_metadata, rejected_reason + ) requester = Requester.deserialize(self.store, content["requester"]) context = EventContext.deserialize(self.storage, content["context"]) From 87972f07e5da0760ca5e11e62b1bda8c49f6f606 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Thu, 5 Mar 2020 11:29:56 -0500 Subject: [PATCH 1148/1623] Convert remote key resource REST layer to async/await. (#7020) --- changelog.d/7020.misc | 1 + synapse/rest/key/v2/remote_key_resource.py | 11 ++++------- 2 files changed, 5 insertions(+), 7 deletions(-) create mode 100644 changelog.d/7020.misc diff --git a/changelog.d/7020.misc b/changelog.d/7020.misc new file mode 100644 index 0000000000..188b4378cb --- /dev/null +++ b/changelog.d/7020.misc @@ -0,0 +1 @@ +Port `synapse.rest.keys` to async/await. diff --git a/synapse/rest/key/v2/remote_key_resource.py b/synapse/rest/key/v2/remote_key_resource.py index 4b6d030a57..ab671f7334 100644 --- a/synapse/rest/key/v2/remote_key_resource.py +++ b/synapse/rest/key/v2/remote_key_resource.py @@ -18,8 +18,6 @@ from typing import Dict, Set from canonicaljson import encode_canonical_json, json from signedjson.sign import sign_json -from twisted.internet import defer - from synapse.api.errors import Codes, SynapseError from synapse.crypto.keyring import ServerKeyFetcher from synapse.http.server import ( @@ -125,8 +123,7 @@ class RemoteKey(DirectServeResource): await self.query_keys(request, query, query_remote_on_cache_miss=True) - @defer.inlineCallbacks - def query_keys(self, request, query, query_remote_on_cache_miss=False): + async def query_keys(self, request, query, query_remote_on_cache_miss=False): logger.info("Handling query for keys %r", query) store_queries = [] @@ -143,7 +140,7 @@ class RemoteKey(DirectServeResource): for key_id in key_ids: store_queries.append((server_name, key_id, None)) - cached = yield self.store.get_server_keys_json(store_queries) + cached = await self.store.get_server_keys_json(store_queries) json_results = set() @@ -215,8 +212,8 @@ class RemoteKey(DirectServeResource): json_results.add(bytes(result["key_json"])) if cache_misses and query_remote_on_cache_miss: - yield self.fetcher.get_keys(cache_misses) - yield self.query_keys(request, query, query_remote_on_cache_miss=False) + await self.fetcher.get_keys(cache_misses) + await self.query_keys(request, query, query_remote_on_cache_miss=False) else: signed_keys = [] for key_json in json_results: From 80e580ae92d1170a4ac2f6afb2fa70f5f8e7b4ac Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 6 Mar 2020 11:02:52 +0000 Subject: [PATCH 1149/1623] Make sure that is_verified is a boolean when processing room keys --- synapse/handlers/e2e_room_keys.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/synapse/handlers/e2e_room_keys.py b/synapse/handlers/e2e_room_keys.py index f1b4424a02..854c181fcc 100644 --- a/synapse/handlers/e2e_room_keys.py +++ b/synapse/handlers/e2e_room_keys.py @@ -207,6 +207,12 @@ class E2eRoomKeysHandler(object): changed = False # if anything has changed, we need to update the etag for room_id, room in iteritems(room_keys["rooms"]): for session_id, room_key in iteritems(room["sessions"]): + if not isinstance(room_key["is_verified"], bool): + msg = ( + "is_verified must be a boolean in keys for room %s" % room_id + ) + raise SynapseError(400, msg, Codes.INVALID_PARAM) + log_kv( { "message": "Trying to upload room key", From a27056d539724614e960f3da3c2e3443aa8625ad Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 6 Mar 2020 11:06:47 +0000 Subject: [PATCH 1150/1623] Changelog --- changelog.d/7045.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7045.misc diff --git a/changelog.d/7045.misc b/changelog.d/7045.misc new file mode 100644 index 0000000000..74c1abea56 --- /dev/null +++ b/changelog.d/7045.misc @@ -0,0 +1 @@ +Add a type check to `is_verified` when processing room keys. From 45df9d35a9500e9a21139951845980a296a62e0b Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 6 Mar 2020 11:10:52 +0000 Subject: [PATCH 1151/1623] Lint --- synapse/handlers/e2e_room_keys.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/synapse/handlers/e2e_room_keys.py b/synapse/handlers/e2e_room_keys.py index 854c181fcc..a953a7fe04 100644 --- a/synapse/handlers/e2e_room_keys.py +++ b/synapse/handlers/e2e_room_keys.py @@ -209,7 +209,8 @@ class E2eRoomKeysHandler(object): for session_id, room_key in iteritems(room["sessions"]): if not isinstance(room_key["is_verified"], bool): msg = ( - "is_verified must be a boolean in keys for room %s" % room_id + "is_verified must be a boolean in keys for room %s" + % room_id ) raise SynapseError(400, msg, Codes.INVALID_PARAM) From 297aaf48166f153d35c38160d0c747770d925f39 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 6 Mar 2020 15:07:28 +0000 Subject: [PATCH 1152/1623] Mention the session ID in the error message --- synapse/handlers/e2e_room_keys.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/synapse/handlers/e2e_room_keys.py b/synapse/handlers/e2e_room_keys.py index a953a7fe04..cad38d2bdd 100644 --- a/synapse/handlers/e2e_room_keys.py +++ b/synapse/handlers/e2e_room_keys.py @@ -209,8 +209,9 @@ class E2eRoomKeysHandler(object): for session_id, room_key in iteritems(room["sessions"]): if not isinstance(room_key["is_verified"], bool): msg = ( - "is_verified must be a boolean in keys for room %s" - % room_id + "is_verified must be a boolean in keys for session %s in" + "room %s" + % (session_id, room_id) ) raise SynapseError(400, msg, Codes.INVALID_PARAM) From 54b78a0e3b6efcc9b576e5a706991382d2984d10 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 6 Mar 2020 15:11:13 +0000 Subject: [PATCH 1153/1623] Lint --- synapse/handlers/e2e_room_keys.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/synapse/handlers/e2e_room_keys.py b/synapse/handlers/e2e_room_keys.py index cad38d2bdd..9abaf13b8f 100644 --- a/synapse/handlers/e2e_room_keys.py +++ b/synapse/handlers/e2e_room_keys.py @@ -210,8 +210,7 @@ class E2eRoomKeysHandler(object): if not isinstance(room_key["is_verified"], bool): msg = ( "is_verified must be a boolean in keys for session %s in" - "room %s" - % (session_id, room_id) + "room %s" % (session_id, room_id) ) raise SynapseError(400, msg, Codes.INVALID_PARAM) From 1d66dce83e58827aae12080552edeaeb357b1997 Mon Sep 17 00:00:00 2001 From: Neil Johnson Date: Fri, 6 Mar 2020 18:14:19 +0000 Subject: [PATCH 1154/1623] Break down monthly active users by appservice_id (#7030) * Break down monthly active users by appservice_id and emit via prometheus. Co-authored-by: Brendan Abolivier --- changelog.d/7030.feature | 1 + synapse/app/homeserver.py | 13 ++++++ .../data_stores/main/monthly_active_users.py | 32 +++++++++++++- tests/storage/test_monthly_active_users.py | 42 +++++++++++++++++++ 4 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 changelog.d/7030.feature diff --git a/changelog.d/7030.feature b/changelog.d/7030.feature new file mode 100644 index 0000000000..fcfdb8d8a1 --- /dev/null +++ b/changelog.d/7030.feature @@ -0,0 +1 @@ +Break down monthly active users by `appservice_id` and emit via Prometheus. diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py index c2a334a2b0..e0fdddfdc9 100644 --- a/synapse/app/homeserver.py +++ b/synapse/app/homeserver.py @@ -298,6 +298,11 @@ class SynapseHomeServer(HomeServer): # Gauges to expose monthly active user control metrics current_mau_gauge = Gauge("synapse_admin_mau:current", "Current MAU") +current_mau_by_service_gauge = Gauge( + "synapse_admin_mau_current_mau_by_service", + "Current MAU by service", + ["app_service"], +) max_mau_gauge = Gauge("synapse_admin_mau:max", "MAU Limit") registered_reserved_users_mau_gauge = Gauge( "synapse_admin_mau:registered_reserved_users", @@ -585,12 +590,20 @@ def run(hs): @defer.inlineCallbacks def generate_monthly_active_users(): current_mau_count = 0 + current_mau_count_by_service = {} reserved_users = () store = hs.get_datastore() if hs.config.limit_usage_by_mau or hs.config.mau_stats_only: current_mau_count = yield store.get_monthly_active_count() + current_mau_count_by_service = ( + yield store.get_monthly_active_count_by_service() + ) reserved_users = yield store.get_registered_reserved_users() current_mau_gauge.set(float(current_mau_count)) + + for app_service, count in current_mau_count_by_service.items(): + current_mau_by_service_gauge.labels(app_service).set(float(count)) + registered_reserved_users_mau_gauge.set(float(len(reserved_users))) max_mau_gauge.set(float(hs.config.max_mau_value)) diff --git a/synapse/storage/data_stores/main/monthly_active_users.py b/synapse/storage/data_stores/main/monthly_active_users.py index 1507a14e09..925bc5691b 100644 --- a/synapse/storage/data_stores/main/monthly_active_users.py +++ b/synapse/storage/data_stores/main/monthly_active_users.py @@ -43,13 +43,40 @@ class MonthlyActiveUsersWorkerStore(SQLBaseStore): def _count_users(txn): sql = "SELECT COALESCE(count(*), 0) FROM monthly_active_users" - txn.execute(sql) (count,) = txn.fetchone() return count return self.db.runInteraction("count_users", _count_users) + @cached(num_args=0) + def get_monthly_active_count_by_service(self): + """Generates current count of monthly active users broken down by service. + A service is typically an appservice but also includes native matrix users. + Since the `monthly_active_users` table is populated from the `user_ips` table + `config.track_appservice_user_ips` must be set to `true` for this + method to return anything other than native matrix users. + + Returns: + Deferred[dict]: dict that includes a mapping between app_service_id + and the number of occurrences. + + """ + + def _count_users_by_service(txn): + sql = """ + SELECT COALESCE(appservice_id, 'native'), COALESCE(count(*), 0) + FROM monthly_active_users + LEFT JOIN users ON monthly_active_users.user_id=users.name + GROUP BY appservice_id; + """ + + txn.execute(sql) + result = txn.fetchall() + return dict(result) + + return self.db.runInteraction("count_users_by_service", _count_users_by_service) + @defer.inlineCallbacks def get_registered_reserved_users(self): """Of the reserved threepids defined in config, which are associated @@ -291,6 +318,9 @@ class MonthlyActiveUsersStore(MonthlyActiveUsersWorkerStore): ) self._invalidate_cache_and_stream(txn, self.get_monthly_active_count, ()) + self._invalidate_cache_and_stream( + txn, self.get_monthly_active_count_by_service, () + ) self._invalidate_cache_and_stream( txn, self.user_last_seen_monthly_active, (user_id,) ) diff --git a/tests/storage/test_monthly_active_users.py b/tests/storage/test_monthly_active_users.py index 3c78faab45..bc53bf0951 100644 --- a/tests/storage/test_monthly_active_users.py +++ b/tests/storage/test_monthly_active_users.py @@ -303,3 +303,45 @@ class MonthlyActiveUsersTestCase(unittest.HomeserverTestCase): self.pump() self.store.upsert_monthly_active_user.assert_not_called() + + def test_get_monthly_active_count_by_service(self): + appservice1_user1 = "@appservice1_user1:example.com" + appservice1_user2 = "@appservice1_user2:example.com" + + appservice2_user1 = "@appservice2_user1:example.com" + native_user1 = "@native_user1:example.com" + + service1 = "service1" + service2 = "service2" + native = "native" + + self.store.register_user( + user_id=appservice1_user1, password_hash=None, appservice_id=service1 + ) + self.store.register_user( + user_id=appservice1_user2, password_hash=None, appservice_id=service1 + ) + self.store.register_user( + user_id=appservice2_user1, password_hash=None, appservice_id=service2 + ) + self.store.register_user(user_id=native_user1, password_hash=None) + self.pump() + + count = self.store.get_monthly_active_count_by_service() + self.assertEqual({}, self.get_success(count)) + + self.store.upsert_monthly_active_user(native_user1) + self.store.upsert_monthly_active_user(appservice1_user1) + self.store.upsert_monthly_active_user(appservice1_user2) + self.store.upsert_monthly_active_user(appservice2_user1) + self.pump() + + count = self.store.get_monthly_active_count() + self.assertEqual(4, self.get_success(count)) + + count = self.store.get_monthly_active_count_by_service() + result = self.get_success(count) + + self.assertEqual(2, result[service1]) + self.assertEqual(1, result[service2]) + self.assertEqual(1, result[native]) From 2bff4457d9a40ffdd8ae1b5d1249a5e78fb8da01 Mon Sep 17 00:00:00 2001 From: Neil Pilgrim Date: Sat, 7 Mar 2020 09:57:26 -0800 Subject: [PATCH 1155/1623] Add type hints to logging/context.py (#6309) * Add type hints to logging/context.py Signed-off-by: neiljp (Neil Pilgrim) --- changelog.d/6309.misc | 1 + synapse/logging/context.py | 121 +++++++++++++++++++++++-------------- 2 files changed, 75 insertions(+), 47 deletions(-) create mode 100644 changelog.d/6309.misc diff --git a/changelog.d/6309.misc b/changelog.d/6309.misc new file mode 100644 index 0000000000..1aa7294617 --- /dev/null +++ b/changelog.d/6309.misc @@ -0,0 +1 @@ +Add type hints to `logging/context.py`. diff --git a/synapse/logging/context.py b/synapse/logging/context.py index 1b940842f6..1eccc0e83f 100644 --- a/synapse/logging/context.py +++ b/synapse/logging/context.py @@ -27,10 +27,15 @@ import inspect import logging import threading import types -from typing import Any, List +from typing import TYPE_CHECKING, Optional, Tuple, TypeVar, Union + +from typing_extensions import Literal from twisted.internet import defer, threads +if TYPE_CHECKING: + from synapse.logging.scopecontextmanager import _LogContextScope + logger = logging.getLogger(__name__) try: @@ -91,7 +96,7 @@ class ContextResourceUsage(object): "evt_db_fetch_count", ] - def __init__(self, copy_from=None): + def __init__(self, copy_from: "Optional[ContextResourceUsage]" = None) -> None: """Create a new ContextResourceUsage Args: @@ -101,27 +106,28 @@ class ContextResourceUsage(object): if copy_from is None: self.reset() else: - self.ru_utime = copy_from.ru_utime - self.ru_stime = copy_from.ru_stime - self.db_txn_count = copy_from.db_txn_count + # FIXME: mypy can't infer the types set via reset() above, so specify explicitly for now + self.ru_utime = copy_from.ru_utime # type: float + self.ru_stime = copy_from.ru_stime # type: float + self.db_txn_count = copy_from.db_txn_count # type: int - self.db_txn_duration_sec = copy_from.db_txn_duration_sec - self.db_sched_duration_sec = copy_from.db_sched_duration_sec - self.evt_db_fetch_count = copy_from.evt_db_fetch_count + self.db_txn_duration_sec = copy_from.db_txn_duration_sec # type: float + self.db_sched_duration_sec = copy_from.db_sched_duration_sec # type: float + self.evt_db_fetch_count = copy_from.evt_db_fetch_count # type: int - def copy(self): + def copy(self) -> "ContextResourceUsage": return ContextResourceUsage(copy_from=self) - def reset(self): + def reset(self) -> None: self.ru_stime = 0.0 self.ru_utime = 0.0 self.db_txn_count = 0 - self.db_txn_duration_sec = 0 - self.db_sched_duration_sec = 0 + self.db_txn_duration_sec = 0.0 + self.db_sched_duration_sec = 0.0 self.evt_db_fetch_count = 0 - def __repr__(self): + def __repr__(self) -> str: return ( " "ContextResourceUsage": """Add another ContextResourceUsage's stats to this one's. Args: @@ -149,7 +155,7 @@ class ContextResourceUsage(object): self.evt_db_fetch_count += other.evt_db_fetch_count return self - def __isub__(self, other): + def __isub__(self, other: "ContextResourceUsage") -> "ContextResourceUsage": self.ru_utime -= other.ru_utime self.ru_stime -= other.ru_stime self.db_txn_count -= other.db_txn_count @@ -158,17 +164,20 @@ class ContextResourceUsage(object): self.evt_db_fetch_count -= other.evt_db_fetch_count return self - def __add__(self, other): + def __add__(self, other: "ContextResourceUsage") -> "ContextResourceUsage": res = ContextResourceUsage(copy_from=self) res += other return res - def __sub__(self, other): + def __sub__(self, other: "ContextResourceUsage") -> "ContextResourceUsage": res = ContextResourceUsage(copy_from=self) res -= other return res +LoggingContextOrSentinel = Union["LoggingContext", "LoggingContext.Sentinel"] + + class LoggingContext(object): """Additional context for log formatting. Contexts are scoped within a "with" block. @@ -201,7 +210,14 @@ class LoggingContext(object): class Sentinel(object): """Sentinel to represent the root context""" - __slots__ = [] # type: List[Any] + __slots__ = ["previous_context", "alive", "request", "scope"] + + def __init__(self) -> None: + # Minimal set for compatibility with LoggingContext + self.previous_context = None + self.alive = None + self.request = None + self.scope = None def __str__(self): return "sentinel" @@ -235,7 +251,7 @@ class LoggingContext(object): sentinel = Sentinel() - def __init__(self, name=None, parent_context=None, request=None): + def __init__(self, name=None, parent_context=None, request=None) -> None: self.previous_context = LoggingContext.current_context() self.name = name @@ -250,7 +266,7 @@ class LoggingContext(object): self.request = None self.tag = "" self.alive = True - self.scope = None + self.scope = None # type: Optional[_LogContextScope] self.parent_context = parent_context @@ -261,13 +277,13 @@ class LoggingContext(object): # the request param overrides the request from the parent context self.request = request - def __str__(self): + def __str__(self) -> str: if self.request: return str(self.request) return "%s@%x" % (self.name, id(self)) @classmethod - def current_context(cls): + def current_context(cls) -> LoggingContextOrSentinel: """Get the current logging context from thread local storage Returns: @@ -276,7 +292,9 @@ class LoggingContext(object): return getattr(cls.thread_local, "current_context", cls.sentinel) @classmethod - def set_current_context(cls, context): + def set_current_context( + cls, context: LoggingContextOrSentinel + ) -> LoggingContextOrSentinel: """Set the current logging context in thread local storage Args: context(LoggingContext): The context to activate. @@ -291,7 +309,7 @@ class LoggingContext(object): context.start() return current - def __enter__(self): + def __enter__(self) -> "LoggingContext": """Enters this logging context into thread local storage""" old_context = self.set_current_context(self) if self.previous_context != old_context: @@ -304,7 +322,7 @@ class LoggingContext(object): return self - def __exit__(self, type, value, traceback): + def __exit__(self, type, value, traceback) -> None: """Restore the logging context in thread local storage to the state it was before this context was entered. Returns: @@ -318,7 +336,6 @@ class LoggingContext(object): logger.warning( "Expected logging context %s but found %s", self, current ) - self.previous_context = None self.alive = False # if we have a parent, pass our CPU usage stats on @@ -330,7 +347,7 @@ class LoggingContext(object): # reset them in case we get entered again self._resource_usage.reset() - def copy_to(self, record): + def copy_to(self, record) -> None: """Copy logging fields from this context to a log record or another LoggingContext """ @@ -341,14 +358,14 @@ class LoggingContext(object): # we also track the current scope: record.scope = self.scope - def copy_to_twisted_log_entry(self, record): + def copy_to_twisted_log_entry(self, record) -> None: """ Copy logging fields from this context to a Twisted log record. """ record["request"] = self.request record["scope"] = self.scope - def start(self): + def start(self) -> None: if get_thread_id() != self.main_thread: logger.warning("Started logcontext %s on different thread", self) return @@ -358,7 +375,7 @@ class LoggingContext(object): if not self.usage_start: self.usage_start = get_thread_resource_usage() - def stop(self): + def stop(self) -> None: if get_thread_id() != self.main_thread: logger.warning("Stopped logcontext %s on different thread", self) return @@ -378,7 +395,7 @@ class LoggingContext(object): self.usage_start = None - def get_resource_usage(self): + def get_resource_usage(self) -> ContextResourceUsage: """Get resources used by this logcontext so far. Returns: @@ -398,11 +415,13 @@ class LoggingContext(object): return res - def _get_cputime(self): + def _get_cputime(self) -> Tuple[float, float]: """Get the cpu usage time so far Returns: Tuple[float, float]: seconds in user mode, seconds in system mode """ + assert self.usage_start is not None + current = get_thread_resource_usage() # Indicate to mypy that we know that self.usage_start is None. @@ -430,13 +449,13 @@ class LoggingContext(object): return utime_delta, stime_delta - def add_database_transaction(self, duration_sec): + def add_database_transaction(self, duration_sec: float) -> None: if duration_sec < 0: raise ValueError("DB txn time can only be non-negative") self._resource_usage.db_txn_count += 1 self._resource_usage.db_txn_duration_sec += duration_sec - def add_database_scheduled(self, sched_sec): + def add_database_scheduled(self, sched_sec: float) -> None: """Record a use of the database pool Args: @@ -447,7 +466,7 @@ class LoggingContext(object): raise ValueError("DB scheduling time can only be non-negative") self._resource_usage.db_sched_duration_sec += sched_sec - def record_event_fetch(self, event_count): + def record_event_fetch(self, event_count: int) -> None: """Record a number of events being fetched from the db Args: @@ -464,10 +483,10 @@ class LoggingContextFilter(logging.Filter): missing fields """ - def __init__(self, **defaults): + def __init__(self, **defaults) -> None: self.defaults = defaults - def filter(self, record): + def filter(self, record) -> Literal[True]: """Add each fields from the logging contexts to the record. Returns: True to include the record in the log output. @@ -492,12 +511,13 @@ class PreserveLoggingContext(object): __slots__ = ["current_context", "new_context", "has_parent"] - def __init__(self, new_context=None): + def __init__(self, new_context: Optional[LoggingContext] = None) -> None: if new_context is None: - new_context = LoggingContext.sentinel - self.new_context = new_context + self.new_context = LoggingContext.sentinel # type: LoggingContextOrSentinel + else: + self.new_context = new_context - def __enter__(self): + def __enter__(self) -> None: """Captures the current logging context""" self.current_context = LoggingContext.set_current_context(self.new_context) @@ -506,7 +526,7 @@ class PreserveLoggingContext(object): if not self.current_context.alive: logger.debug("Entering dead context: %s", self.current_context) - def __exit__(self, type, value, traceback): + def __exit__(self, type, value, traceback) -> None: """Restores the current logging context""" context = LoggingContext.set_current_context(self.current_context) @@ -525,7 +545,9 @@ class PreserveLoggingContext(object): logger.debug("Restoring dead context: %s", self.current_context) -def nested_logging_context(suffix, parent_context=None): +def nested_logging_context( + suffix: str, parent_context: Optional[LoggingContext] = None +) -> LoggingContext: """Creates a new logging context as a child of another. The nested logging context will have a 'request' made up of the parent context's @@ -546,10 +568,12 @@ def nested_logging_context(suffix, parent_context=None): Returns: LoggingContext: new logging context. """ - if parent_context is None: - parent_context = LoggingContext.current_context() + if parent_context is not None: + context = parent_context # type: LoggingContextOrSentinel + else: + context = LoggingContext.current_context() return LoggingContext( - parent_context=parent_context, request=parent_context.request + "-" + suffix + parent_context=context, request=str(context.request) + "-" + suffix ) @@ -654,7 +678,10 @@ def make_deferred_yieldable(deferred): return deferred -def _set_context_cb(result, context): +ResultT = TypeVar("ResultT") + + +def _set_context_cb(result: ResultT, context: LoggingContext) -> ResultT: """A callback function which just sets the logging context""" LoggingContext.set_current_context(context) return result From 1f5f3ae8b1c5db96d36ac7c104f13553bc4283da Mon Sep 17 00:00:00 2001 From: dklimpel <5740567+dklimpel@users.noreply.github.com> Date: Sun, 8 Mar 2020 14:49:33 +0100 Subject: [PATCH 1156/1623] Add options to disable setting profile info for prevent changes. --- synapse/config/registration.py | 11 +++++++++++ synapse/handlers/profile.py | 10 ++++++++++ tests/handlers/test_profile.py | 33 ++++++++++++++++++++++++++++++++- 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/synapse/config/registration.py b/synapse/config/registration.py index 9bb3beedbc..d9f452dcea 100644 --- a/synapse/config/registration.py +++ b/synapse/config/registration.py @@ -129,6 +129,9 @@ class RegistrationConfig(Config): raise ConfigError("Invalid auto_join_rooms entry %s" % (room_alias,)) self.autocreate_auto_join_rooms = config.get("autocreate_auto_join_rooms", True) + self.disable_set_displayname = config.get("disable_set_displayname", False) + self.disable_set_avatar_url = config.get("disable_set_avatar_url", False) + self.disable_msisdn_registration = config.get( "disable_msisdn_registration", False ) @@ -330,6 +333,14 @@ class RegistrationConfig(Config): #email: https://example.com # Delegate email sending to example.com #msisdn: http://localhost:8090 # Delegate SMS sending to this local process + # If enabled, don't let users set their own display names/avatars + # other than for the very first time (unless they are a server admin). + # Useful when provisioning users based on the contents of a 3rd party + # directory and to avoid ambiguities. + # + # disable_set_displayname: False + # disable_set_avatar_url: False + # Users who register on this homeserver will automatically be joined # to these rooms # diff --git a/synapse/handlers/profile.py b/synapse/handlers/profile.py index 50ce0c585b..fb7e84f3b8 100644 --- a/synapse/handlers/profile.py +++ b/synapse/handlers/profile.py @@ -157,6 +157,11 @@ class BaseProfileHandler(BaseHandler): if not by_admin and target_user != requester.user: raise AuthError(400, "Cannot set another user's displayname") + if not by_admin and self.hs.config.disable_set_displayname: + profile = yield self.store.get_profileinfo(target_user.localpart) + if profile.display_name: + raise SynapseError(400, "Changing displayname is disabled on this server") + if len(new_displayname) > MAX_DISPLAYNAME_LEN: raise SynapseError( 400, "Displayname is too long (max %i)" % (MAX_DISPLAYNAME_LEN,) @@ -218,6 +223,11 @@ class BaseProfileHandler(BaseHandler): if not by_admin and target_user != requester.user: raise AuthError(400, "Cannot set another user's avatar_url") + if not by_admin and self.hs.config.disable_set_avatar_url: + profile = yield self.store.get_profileinfo(target_user.localpart) + if profile.avatar_url: + raise SynapseError(400, "Changing avatar url is disabled on this server") + if len(new_avatar_url) > MAX_AVATAR_URL_LEN: raise SynapseError( 400, "Avatar URL is too long (max %i)" % (MAX_AVATAR_URL_LEN,) diff --git a/tests/handlers/test_profile.py b/tests/handlers/test_profile.py index d60c124eec..b85520c688 100644 --- a/tests/handlers/test_profile.py +++ b/tests/handlers/test_profile.py @@ -19,7 +19,7 @@ from mock import Mock, NonCallableMock from twisted.internet import defer import synapse.types -from synapse.api.errors import AuthError +from synapse.api.errors import AuthError, SynapseError from synapse.handlers.profile import MasterProfileHandler from synapse.types import UserID @@ -70,6 +70,7 @@ class ProfileTestCase(unittest.TestCase): yield self.store.create_profile(self.frank.localpart) self.handler = hs.get_profile_handler() + self.config = hs.config @defer.inlineCallbacks def test_get_my_name(self): @@ -90,6 +91,19 @@ class ProfileTestCase(unittest.TestCase): "Frank Jr.", ) + @defer.inlineCallbacks + def test_set_my_name_if_disabled(self): + self.config.disable_set_displayname = True + + # Set first displayname is allowed, if displayname is null + self.store.set_profile_displayname(self.frank.localpart, "Frank") + + d = self.handler.set_displayname( + self.frank, synapse.types.create_requester(self.frank), "Frank Jr." + ) + + yield self.assertFailure(d, SynapseError) + @defer.inlineCallbacks def test_set_my_name_noauth(self): d = self.handler.set_displayname( @@ -147,3 +161,20 @@ class ProfileTestCase(unittest.TestCase): (yield self.store.get_profile_avatar_url(self.frank.localpart)), "http://my.server/pic.gif", ) + + @defer.inlineCallbacks + def test_set_my_avatar_if_disabled(self): + self.config.disable_set_avatar_url = True + + # Set first time avatar is allowed, if displayname is null + self.store.set_profile_avatar_url( + self.frank.localpart, "http://my.server/me.png" + ) + + d = self.handler.set_avatar_url( + self.frank, + synapse.types.create_requester(self.frank), + "http://my.server/pic.gif", + ) + + yield self.assertFailure(d, SynapseError) From fb078f921b4d49fe3087d89563bce7b8cee0292c Mon Sep 17 00:00:00 2001 From: dklimpel <5740567+dklimpel@users.noreply.github.com> Date: Sun, 8 Mar 2020 15:19:07 +0100 Subject: [PATCH 1157/1623] changelog --- changelog.d/7053.feature | 1 + docs/sample_config.yaml | 8 ++++++++ synapse/config/registration.py | 4 ++-- synapse/handlers/profile.py | 10 ++++++++-- 4 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 changelog.d/7053.feature diff --git a/changelog.d/7053.feature b/changelog.d/7053.feature new file mode 100644 index 0000000000..79955b9780 --- /dev/null +++ b/changelog.d/7053.feature @@ -0,0 +1 @@ +Add options to disable setting profile info for prevent changes. \ No newline at end of file diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 54cbe840d5..d646f0cefe 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -1057,6 +1057,14 @@ account_threepid_delegates: #email: https://example.com # Delegate email sending to example.com #msisdn: http://localhost:8090 # Delegate SMS sending to this local process +# If enabled, don't let users set their own display names/avatars +# other than for the very first time (unless they are a server admin). +# Useful when provisioning users based on the contents of a 3rd party +# directory and to avoid ambiguities. +# +#disable_set_displayname: False +#disable_set_avatar_url: False + # Users who register on this homeserver will automatically be joined # to these rooms # diff --git a/synapse/config/registration.py b/synapse/config/registration.py index d9f452dcea..bdbd6f3130 100644 --- a/synapse/config/registration.py +++ b/synapse/config/registration.py @@ -338,8 +338,8 @@ class RegistrationConfig(Config): # Useful when provisioning users based on the contents of a 3rd party # directory and to avoid ambiguities. # - # disable_set_displayname: False - # disable_set_avatar_url: False + #disable_set_displayname: False + #disable_set_avatar_url: False # Users who register on this homeserver will automatically be joined # to these rooms diff --git a/synapse/handlers/profile.py b/synapse/handlers/profile.py index fb7e84f3b8..445981bf3d 100644 --- a/synapse/handlers/profile.py +++ b/synapse/handlers/profile.py @@ -160,7 +160,10 @@ class BaseProfileHandler(BaseHandler): if not by_admin and self.hs.config.disable_set_displayname: profile = yield self.store.get_profileinfo(target_user.localpart) if profile.display_name: - raise SynapseError(400, "Changing displayname is disabled on this server") + raise SynapseError( + 400, + "Changing displayname is disabled on this server" + ) if len(new_displayname) > MAX_DISPLAYNAME_LEN: raise SynapseError( @@ -226,7 +229,10 @@ class BaseProfileHandler(BaseHandler): if not by_admin and self.hs.config.disable_set_avatar_url: profile = yield self.store.get_profileinfo(target_user.localpart) if profile.avatar_url: - raise SynapseError(400, "Changing avatar url is disabled on this server") + raise SynapseError( + 400, + "Changing avatar url is disabled on this server" + ) if len(new_avatar_url) > MAX_AVATAR_URL_LEN: raise SynapseError( From ce460dc31c6de5852277310db825d23c27d4b9fd Mon Sep 17 00:00:00 2001 From: dklimpel <5740567+dklimpel@users.noreply.github.com> Date: Sun, 8 Mar 2020 15:22:43 +0100 Subject: [PATCH 1158/1623] lint --- synapse/handlers/profile.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/synapse/handlers/profile.py b/synapse/handlers/profile.py index 445981bf3d..b049dd8e26 100644 --- a/synapse/handlers/profile.py +++ b/synapse/handlers/profile.py @@ -161,8 +161,7 @@ class BaseProfileHandler(BaseHandler): profile = yield self.store.get_profileinfo(target_user.localpart) if profile.display_name: raise SynapseError( - 400, - "Changing displayname is disabled on this server" + 400, "Changing displayname is disabled on this server" ) if len(new_displayname) > MAX_DISPLAYNAME_LEN: @@ -230,8 +229,7 @@ class BaseProfileHandler(BaseHandler): profile = yield self.store.get_profileinfo(target_user.localpart) if profile.avatar_url: raise SynapseError( - 400, - "Changing avatar url is disabled on this server" + 400, "Changing avatar url is disabled on this server" ) if len(new_avatar_url) > MAX_AVATAR_URL_LEN: From 20545a2199359f627977d14d477d0288f7fb3a07 Mon Sep 17 00:00:00 2001 From: dklimpel <5740567+dklimpel@users.noreply.github.com> Date: Sun, 8 Mar 2020 15:28:00 +0100 Subject: [PATCH 1159/1623] lint2 --- docs/sample_config.yaml | 4 ++-- synapse/config/registration.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index d646f0cefe..a73e4498fe 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -1062,8 +1062,8 @@ account_threepid_delegates: # Useful when provisioning users based on the contents of a 3rd party # directory and to avoid ambiguities. # -#disable_set_displayname: False -#disable_set_avatar_url: False +#disable_set_displayname: false +#disable_set_avatar_url: false # Users who register on this homeserver will automatically be joined # to these rooms diff --git a/synapse/config/registration.py b/synapse/config/registration.py index bdbd6f3130..0422c39451 100644 --- a/synapse/config/registration.py +++ b/synapse/config/registration.py @@ -338,8 +338,8 @@ class RegistrationConfig(Config): # Useful when provisioning users based on the contents of a 3rd party # directory and to avoid ambiguities. # - #disable_set_displayname: False - #disable_set_avatar_url: False + #disable_set_displayname: false + #disable_set_avatar_url: false # Users who register on this homeserver will automatically be joined # to these rooms From 99bbe177b67f85fb70be61d47068a57fbb3b92f6 Mon Sep 17 00:00:00 2001 From: dklimpel <5740567+dklimpel@users.noreply.github.com> Date: Sun, 8 Mar 2020 21:58:12 +0100 Subject: [PATCH 1160/1623] add disable_3pid_changes --- docs/sample_config.yaml | 5 +++++ synapse/config/registration.py | 6 ++++++ synapse/rest/client/v2_alpha/account.py | 10 ++++++++++ 3 files changed, 21 insertions(+) diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index a73e4498fe..d3ecffac7d 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -1065,6 +1065,11 @@ account_threepid_delegates: #disable_set_displayname: false #disable_set_avatar_url: false +# If true, stop users from trying to change the 3PIDs associated with +# their accounts. +# +#disable_3pid_changes: false + # Users who register on this homeserver will automatically be joined # to these rooms # diff --git a/synapse/config/registration.py b/synapse/config/registration.py index 0422c39451..1abc0a79af 100644 --- a/synapse/config/registration.py +++ b/synapse/config/registration.py @@ -131,6 +131,7 @@ class RegistrationConfig(Config): self.disable_set_displayname = config.get("disable_set_displayname", False) self.disable_set_avatar_url = config.get("disable_set_avatar_url", False) + self.disable_3pid_changes = config.get("disable_3pid_changes", False) self.disable_msisdn_registration = config.get( "disable_msisdn_registration", False @@ -341,6 +342,11 @@ class RegistrationConfig(Config): #disable_set_displayname: false #disable_set_avatar_url: false + # If true, stop users from trying to change the 3PIDs associated with + # their accounts. + # + #disable_3pid_changes: false + # Users who register on this homeserver will automatically be joined # to these rooms # diff --git a/synapse/rest/client/v2_alpha/account.py b/synapse/rest/client/v2_alpha/account.py index dc837d6c75..97bddf36d9 100644 --- a/synapse/rest/client/v2_alpha/account.py +++ b/synapse/rest/client/v2_alpha/account.py @@ -599,6 +599,9 @@ class ThreepidRestServlet(RestServlet): return 200, {"threepids": threepids} async def on_POST(self, request): + if self.hs.config.disable_3pid_changes: + raise SynapseError(400, "3PID changes disabled on this server") + requester = await self.auth.get_user_by_req(request) user_id = requester.user.to_string() body = parse_json_object_from_request(request) @@ -643,6 +646,9 @@ class ThreepidAddRestServlet(RestServlet): @interactive_auth_handler async def on_POST(self, request): + if self.hs.config.disable_3pid_changes: + raise SynapseError(400, "3PID changes disabled on this server") + requester = await self.auth.get_user_by_req(request) user_id = requester.user.to_string() body = parse_json_object_from_request(request) @@ -738,10 +744,14 @@ class ThreepidDeleteRestServlet(RestServlet): def __init__(self, hs): super(ThreepidDeleteRestServlet, self).__init__() + self.hs = hs self.auth = hs.get_auth() self.auth_handler = hs.get_auth_handler() async def on_POST(self, request): + if self.hs.config.disable_3pid_changes: + raise SynapseError(400, "3PID changes disabled on this server") + body = parse_json_object_from_request(request) assert_params_in_dict(body, ["medium", "address"]) From 66315d862fdec0ddc1414010626b344d48c14167 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Mon, 9 Mar 2020 07:19:24 -0400 Subject: [PATCH 1161/1623] Update routing of fallback auth in the worker docs. (#7048) --- changelog.d/7048.doc | 1 + docs/workers.md | 1 + 2 files changed, 2 insertions(+) create mode 100644 changelog.d/7048.doc diff --git a/changelog.d/7048.doc b/changelog.d/7048.doc new file mode 100644 index 0000000000..c9666f333e --- /dev/null +++ b/changelog.d/7048.doc @@ -0,0 +1 @@ +Document that the fallback auth endpoints must be routed to the same worker node as the register endpoints. diff --git a/docs/workers.md b/docs/workers.md index 0d84a58958..cf460283d5 100644 --- a/docs/workers.md +++ b/docs/workers.md @@ -273,6 +273,7 @@ Additionally, the following REST endpoints can be handled, but all requests must be routed to the same instance: ^/_matrix/client/(r0|unstable)/register$ + ^/_matrix/client/(r0|unstable)/auth/.*/fallback/web$ Pagination requests can also be handled, but all requests with the same path room must be routed to the same instance. Additionally, care must be taken to From 06eb5cae08272c401a586991fc81f788825f910b Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Mon, 9 Mar 2020 08:58:25 -0400 Subject: [PATCH 1162/1623] Remove special auth and redaction rules for aliases events in experimental room ver. (#7037) --- changelog.d/7037.feature | 1 + synapse/api/room_versions.py | 9 ++- synapse/crypto/event_signing.py | 2 +- synapse/event_auth.py | 8 +- synapse/events/utils.py | 12 ++- synapse/storage/data_stores/main/events.py | 10 ++- tests/events/test_utils.py | 35 +++++++- tests/test_event_auth.py | 93 +++++++++++++++++++++- 8 files changed, 148 insertions(+), 22 deletions(-) create mode 100644 changelog.d/7037.feature diff --git a/changelog.d/7037.feature b/changelog.d/7037.feature new file mode 100644 index 0000000000..4bc1b3b19f --- /dev/null +++ b/changelog.d/7037.feature @@ -0,0 +1 @@ +Implement updated authorization rules and redaction rules for aliases events, from [MSC2261](https://github.com/matrix-org/matrix-doc/pull/2261) and [MSC2432](https://github.com/matrix-org/matrix-doc/pull/2432). diff --git a/synapse/api/room_versions.py b/synapse/api/room_versions.py index cf7ee60d3a..871179749a 100644 --- a/synapse/api/room_versions.py +++ b/synapse/api/room_versions.py @@ -57,7 +57,7 @@ class RoomVersion(object): state_res = attr.ib() # int; one of the StateResolutionVersions enforce_key_validity = attr.ib() # bool - # bool: before MSC2260, anyone was allowed to send an aliases event + # bool: before MSC2261/MSC2432, m.room.aliases had special auth rules and redaction rules special_case_aliases_auth = attr.ib(type=bool, default=False) @@ -102,12 +102,13 @@ class RoomVersions(object): enforce_key_validity=True, special_case_aliases_auth=True, ) - MSC2260_DEV = RoomVersion( - "org.matrix.msc2260", + MSC2432_DEV = RoomVersion( + "org.matrix.msc2432", RoomDisposition.UNSTABLE, EventFormatVersions.V3, StateResolutionVersions.V2, enforce_key_validity=True, + special_case_aliases_auth=False, ) @@ -119,6 +120,6 @@ KNOWN_ROOM_VERSIONS = { RoomVersions.V3, RoomVersions.V4, RoomVersions.V5, - RoomVersions.MSC2260_DEV, + RoomVersions.MSC2432_DEV, ) } # type: Dict[str, RoomVersion] diff --git a/synapse/crypto/event_signing.py b/synapse/crypto/event_signing.py index 5f733c1cf5..0422c43fab 100644 --- a/synapse/crypto/event_signing.py +++ b/synapse/crypto/event_signing.py @@ -140,7 +140,7 @@ def compute_event_signature( Returns: a dictionary in the same format of an event's signatures field. """ - redact_json = prune_event_dict(event_dict) + redact_json = prune_event_dict(room_version, event_dict) redact_json.pop("age_ts", None) redact_json.pop("unsigned", None) if logger.isEnabledFor(logging.DEBUG): diff --git a/synapse/event_auth.py b/synapse/event_auth.py index 472f165044..46beb5334f 100644 --- a/synapse/event_auth.py +++ b/synapse/event_auth.py @@ -137,7 +137,7 @@ def check( raise AuthError(403, "This room has been marked as unfederatable.") # 4. If type is m.room.aliases - if event.type == EventTypes.Aliases: + if event.type == EventTypes.Aliases and room_version_obj.special_case_aliases_auth: # 4a. If event has no state_key, reject if not event.is_state(): raise AuthError(403, "Alias event must be a state event") @@ -152,10 +152,8 @@ def check( ) # 4c. Otherwise, allow. - # This is removed by https://github.com/matrix-org/matrix-doc/pull/2260 - if room_version_obj.special_case_aliases_auth: - logger.debug("Allowing! %s", event) - return + logger.debug("Allowing! %s", event) + return if logger.isEnabledFor(logging.DEBUG): logger.debug("Auth events: %s", [a.event_id for a in auth_events.values()]) diff --git a/synapse/events/utils.py b/synapse/events/utils.py index bc6f98ae3b..b75b097e5e 100644 --- a/synapse/events/utils.py +++ b/synapse/events/utils.py @@ -23,6 +23,7 @@ from frozendict import frozendict from twisted.internet import defer from synapse.api.constants import EventTypes, RelationTypes +from synapse.api.room_versions import RoomVersion from synapse.util.async_helpers import yieldable_gather_results from . import EventBase @@ -43,7 +44,7 @@ def prune_event(event: EventBase) -> EventBase: the user has specified, but we do want to keep necessary information like type, state_key etc. """ - pruned_event_dict = prune_event_dict(event.get_dict()) + pruned_event_dict = prune_event_dict(event.room_version, event.get_dict()) from . import make_event_from_dict @@ -57,15 +58,12 @@ def prune_event(event: EventBase) -> EventBase: return pruned_event -def prune_event_dict(event_dict): +def prune_event_dict(room_version: RoomVersion, event_dict: dict) -> dict: """Redacts the event_dict in the same way as `prune_event`, except it operates on dicts rather than event objects - Args: - event_dict (dict) - Returns: - dict: A copy of the pruned event dict + A copy of the pruned event dict """ allowed_keys = [ @@ -112,7 +110,7 @@ def prune_event_dict(event_dict): "kick", "redact", ) - elif event_type == EventTypes.Aliases: + elif event_type == EventTypes.Aliases and room_version.special_case_aliases_auth: add_fields("aliases") elif event_type == EventTypes.RoomHistoryVisibility: add_fields("history_visibility") diff --git a/synapse/storage/data_stores/main/events.py b/synapse/storage/data_stores/main/events.py index 8ae23df00a..d593ef47b8 100644 --- a/synapse/storage/data_stores/main/events.py +++ b/synapse/storage/data_stores/main/events.py @@ -1168,7 +1168,11 @@ class EventsStore( and original_event.internal_metadata.is_redacted() ): # Redaction was allowed - pruned_json = encode_json(prune_event_dict(original_event.get_dict())) + pruned_json = encode_json( + prune_event_dict( + original_event.room_version, original_event.get_dict() + ) + ) else: # Redaction wasn't allowed pruned_json = None @@ -1929,7 +1933,9 @@ class EventsStore( return # Prune the event's dict then convert it to JSON. - pruned_json = encode_json(prune_event_dict(event.get_dict())) + pruned_json = encode_json( + prune_event_dict(event.room_version, event.get_dict()) + ) # Update the event_json table to replace the event's JSON with the pruned # JSON. diff --git a/tests/events/test_utils.py b/tests/events/test_utils.py index 45d55b9e94..ab5f5ac549 100644 --- a/tests/events/test_utils.py +++ b/tests/events/test_utils.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from synapse.api.room_versions import RoomVersions from synapse.events import make_event_from_dict from synapse.events.utils import ( copy_power_levels_contents, @@ -36,9 +37,9 @@ class PruneEventTestCase(unittest.TestCase): """ Asserts that a new event constructed with `evdict` will look like `matchdict` when it is redacted. """ - def run_test(self, evdict, matchdict): + def run_test(self, evdict, matchdict, **kwargs): self.assertEquals( - prune_event(make_event_from_dict(evdict)).get_dict(), matchdict + prune_event(make_event_from_dict(evdict, **kwargs)).get_dict(), matchdict ) def test_minimal(self): @@ -128,6 +129,36 @@ class PruneEventTestCase(unittest.TestCase): }, ) + def test_alias_event(self): + """Alias events have special behavior up through room version 6.""" + self.run_test( + { + "type": "m.room.aliases", + "event_id": "$test:domain", + "content": {"aliases": ["test"]}, + }, + { + "type": "m.room.aliases", + "event_id": "$test:domain", + "content": {"aliases": ["test"]}, + "signatures": {}, + "unsigned": {}, + }, + ) + + def test_msc2432_alias_event(self): + """After MSC2432, alias events have no special behavior.""" + self.run_test( + {"type": "m.room.aliases", "content": {"aliases": ["test"]}}, + { + "type": "m.room.aliases", + "content": {}, + "signatures": {}, + "unsigned": {}, + }, + room_version=RoomVersions.MSC2432_DEV, + ) + class SerializeEventTestCase(unittest.TestCase): def serialize(self, ev, fields): diff --git a/tests/test_event_auth.py b/tests/test_event_auth.py index bfa5d6f510..6c2351cf55 100644 --- a/tests/test_event_auth.py +++ b/tests/test_event_auth.py @@ -19,6 +19,7 @@ from synapse import event_auth from synapse.api.errors import AuthError from synapse.api.room_versions import RoomVersions from synapse.events import make_event_from_dict +from synapse.types import get_domain_from_id class EventAuthTestCase(unittest.TestCase): @@ -51,7 +52,7 @@ class EventAuthTestCase(unittest.TestCase): _random_state_event(joiner), auth_events, do_sig_check=False, - ), + ) def test_state_default_level(self): """ @@ -87,6 +88,83 @@ class EventAuthTestCase(unittest.TestCase): RoomVersions.V1, _random_state_event(king), auth_events, do_sig_check=False, ) + def test_alias_event(self): + """Alias events have special behavior up through room version 6.""" + creator = "@creator:example.com" + other = "@other:example.com" + auth_events = { + ("m.room.create", ""): _create_event(creator), + ("m.room.member", creator): _join_event(creator), + } + + # creator should be able to send aliases + event_auth.check( + RoomVersions.V1, _alias_event(creator), auth_events, do_sig_check=False, + ) + + # Reject an event with no state key. + with self.assertRaises(AuthError): + event_auth.check( + RoomVersions.V1, + _alias_event(creator, state_key=""), + auth_events, + do_sig_check=False, + ) + + # If the domain of the sender does not match the state key, reject. + with self.assertRaises(AuthError): + event_auth.check( + RoomVersions.V1, + _alias_event(creator, state_key="test.com"), + auth_events, + do_sig_check=False, + ) + + # Note that the member does *not* need to be in the room. + event_auth.check( + RoomVersions.V1, _alias_event(other), auth_events, do_sig_check=False, + ) + + def test_msc2432_alias_event(self): + """After MSC2432, alias events have no special behavior.""" + creator = "@creator:example.com" + other = "@other:example.com" + auth_events = { + ("m.room.create", ""): _create_event(creator), + ("m.room.member", creator): _join_event(creator), + } + + # creator should be able to send aliases + event_auth.check( + RoomVersions.MSC2432_DEV, + _alias_event(creator), + auth_events, + do_sig_check=False, + ) + + # No particular checks are done on the state key. + event_auth.check( + RoomVersions.MSC2432_DEV, + _alias_event(creator, state_key=""), + auth_events, + do_sig_check=False, + ) + event_auth.check( + RoomVersions.MSC2432_DEV, + _alias_event(creator, state_key="test.com"), + auth_events, + do_sig_check=False, + ) + + # Per standard auth rules, the member must be in the room. + with self.assertRaises(AuthError): + event_auth.check( + RoomVersions.MSC2432_DEV, + _alias_event(other), + auth_events, + do_sig_check=False, + ) + # helpers for making events @@ -131,6 +209,19 @@ def _power_levels_event(sender, content): ) +def _alias_event(sender, **kwargs): + data = { + "room_id": TEST_ROOM_ID, + "event_id": _get_event_id(), + "type": "m.room.aliases", + "sender": sender, + "state_key": get_domain_from_id(sender), + "content": {"aliases": []}, + } + data.update(**kwargs) + return make_event_from_dict(data) + + def _random_state_event(sender): return make_event_from_dict( { From 87c65576e08c1cdceb821bca15880110c4edd203 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Mon, 9 Mar 2020 13:58:38 +0000 Subject: [PATCH 1163/1623] Move `get_time_of_last_push_action_before` to the `EventPushActionsWorkerStore` Fixes #7054 I also had a look at the rest of the functions in `EventPushActionsStore` and in the push notifications send code and it looks to me like there shouldn't be any other method with this issue in this part of the codebase. --- .../data_stores/main/event_push_actions.py | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/synapse/storage/data_stores/main/event_push_actions.py b/synapse/storage/data_stores/main/event_push_actions.py index 9988a6d3fc..8eed590929 100644 --- a/synapse/storage/data_stores/main/event_push_actions.py +++ b/synapse/storage/data_stores/main/event_push_actions.py @@ -608,6 +608,23 @@ class EventPushActionsWorkerStore(SQLBaseStore): return range_end + @defer.inlineCallbacks + def get_time_of_last_push_action_before(self, stream_ordering): + def f(txn): + sql = ( + "SELECT e.received_ts" + " FROM event_push_actions AS ep" + " JOIN events e ON ep.room_id = e.room_id AND ep.event_id = e.event_id" + " WHERE ep.stream_ordering > ?" + " ORDER BY ep.stream_ordering ASC" + " LIMIT 1" + ) + txn.execute(sql, (stream_ordering,)) + return txn.fetchone() + + result = yield self.db.runInteraction("get_time_of_last_push_action_before", f) + return result[0] if result else None + class EventPushActionsStore(EventPushActionsWorkerStore): EPA_HIGHLIGHT_INDEX = "epa_highlight_index" @@ -735,23 +752,6 @@ class EventPushActionsStore(EventPushActionsWorkerStore): pa["actions"] = _deserialize_action(pa["actions"], pa["highlight"]) return push_actions - @defer.inlineCallbacks - def get_time_of_last_push_action_before(self, stream_ordering): - def f(txn): - sql = ( - "SELECT e.received_ts" - " FROM event_push_actions AS ep" - " JOIN events e ON ep.room_id = e.room_id AND ep.event_id = e.event_id" - " WHERE ep.stream_ordering > ?" - " ORDER BY ep.stream_ordering ASC" - " LIMIT 1" - ) - txn.execute(sql, (stream_ordering,)) - return txn.fetchone() - - result = yield self.db.runInteraction("get_time_of_last_push_action_before", f) - return result[0] if result else None - @defer.inlineCallbacks def get_latest_push_action_stream_ordering(self): def f(txn): From aee2bae9523a639c31c18ef7fab7a8a08ed3db03 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Mon, 9 Mar 2020 14:10:19 +0000 Subject: [PATCH 1164/1623] Fix undefined `room_id` in `make_summary_text` This would break notifications about un-named rooms when processing notifications in a batch. --- synapse/push/mailer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 4ccaf178ce..73580c1c6c 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -555,10 +555,12 @@ class Mailer(object): else: # If the reason room doesn't have a name, say who the messages # are from explicitly to avoid, "messages in the Bob room" + room_id = reason["room_id"] + sender_ids = list( { notif_events[n["event_id"]].sender - for n in notifs_by_room[reason["room_id"]] + for n in notifs_by_room[room_id] } ) From f9e3a3f4d0ceef55d7254ba412982edf0192ccc1 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Mon, 9 Mar 2020 14:21:01 +0000 Subject: [PATCH 1165/1623] Changelog It's the same as in #6964 since it's the most likely cause of the bug and that change hasn't been released yet. --- changelog.d/7055.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7055.misc diff --git a/changelog.d/7055.misc b/changelog.d/7055.misc new file mode 100644 index 0000000000..ec5c004bbe --- /dev/null +++ b/changelog.d/7055.misc @@ -0,0 +1 @@ +Merge worker apps together. From 04f4b5f6f87fbba0b2f1a4f011c496de3021c81a Mon Sep 17 00:00:00 2001 From: dklimpel <5740567+dklimpel@users.noreply.github.com> Date: Mon, 9 Mar 2020 19:51:31 +0100 Subject: [PATCH 1166/1623] add tests --- tests/handlers/test_profile.py | 6 +- tests/rest/client/v2_alpha/test_account.py | 308 +++++++++++++++++++++ 2 files changed, 311 insertions(+), 3 deletions(-) diff --git a/tests/handlers/test_profile.py b/tests/handlers/test_profile.py index b85520c688..98b508c3d4 100644 --- a/tests/handlers/test_profile.py +++ b/tests/handlers/test_profile.py @@ -70,7 +70,7 @@ class ProfileTestCase(unittest.TestCase): yield self.store.create_profile(self.frank.localpart) self.handler = hs.get_profile_handler() - self.config = hs.config + self.hs = hs @defer.inlineCallbacks def test_get_my_name(self): @@ -93,7 +93,7 @@ class ProfileTestCase(unittest.TestCase): @defer.inlineCallbacks def test_set_my_name_if_disabled(self): - self.config.disable_set_displayname = True + self.hs.config.disable_set_displayname = True # Set first displayname is allowed, if displayname is null self.store.set_profile_displayname(self.frank.localpart, "Frank") @@ -164,7 +164,7 @@ class ProfileTestCase(unittest.TestCase): @defer.inlineCallbacks def test_set_my_avatar_if_disabled(self): - self.config.disable_set_avatar_url = True + self.hs.config.disable_set_avatar_url = True # Set first time avatar is allowed, if displayname is null self.store.set_profile_avatar_url( diff --git a/tests/rest/client/v2_alpha/test_account.py b/tests/rest/client/v2_alpha/test_account.py index c3facc00eb..ac9f200de3 100644 --- a/tests/rest/client/v2_alpha/test_account.py +++ b/tests/rest/client/v2_alpha/test_account.py @@ -325,3 +325,311 @@ class DeactivateTestCase(unittest.HomeserverTestCase): ) self.render(request) self.assertEqual(request.code, 200) + + +class ThreepidEmailRestTestCase(unittest.HomeserverTestCase): + + servlets = [ + account.register_servlets, + login.register_servlets, + synapse.rest.admin.register_servlets_for_client_rest_resource, + ] + + def make_homeserver(self, reactor, clock): + config = self.default_config() + + # Email config. + self.email_attempts = [] + + def sendmail(smtphost, from_addr, to_addrs, msg, **kwargs): + self.email_attempts.append(msg) + return + + config["email"] = { + "enable_notifs": False, + "template_dir": os.path.abspath( + pkg_resources.resource_filename("synapse", "res/templates") + ), + "smtp_host": "127.0.0.1", + "smtp_port": 20, + "require_transport_security": False, + "smtp_user": None, + "smtp_pass": None, + "notif_from": "test@example.com", + } + config["public_baseurl"] = "https://example.com" + + self.hs = self.setup_test_homeserver(config=config, sendmail=sendmail) + return self.hs + + def prepare(self, reactor, clock, hs): + self.store = hs.get_datastore() + + self.user_id = self.register_user("kermit", "test") + self.user_id_tok = self.login("kermit", "test") + self.email = "test@example.com" + self.url_3pid = b"account/3pid" + + def test_add_email(self): + """Test add mail to profile + """ + client_secret = "foobar" + session_id = self._request_token(self.email, client_secret) + + self.assertEquals(len(self.email_attempts), 1) + link = self._get_link_from_email() + + self._validate_token(link) + + request, channel = self.make_request( + "POST", + b"/_matrix/client/unstable/account/3pid/add", + { + "client_secret": client_secret, + "sid": session_id, + "auth": { + "type": "m.login.password", + "user": self.user_id, + "password": "test", + }, + }, + access_token=self.user_id_tok, + ) + + self.render(request) + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + + # Get user + request, channel = self.make_request( + "GET", self.url_3pid, access_token=self.user_id_tok, + ) + self.render(request) + + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("email", channel.json_body["threepids"][0]["medium"]) + self.assertEqual(self.email, channel.json_body["threepids"][0]["address"]) + + def test_add_email_if_disabled(self): + """Test add mail to profile if disabled + """ + self.hs.config.disable_3pid_changes = True + + client_secret = "foobar" + session_id = self._request_token(self.email, client_secret) + + self.assertEquals(len(self.email_attempts), 1) + link = self._get_link_from_email() + + self._validate_token(link) + + request, channel = self.make_request( + "POST", + b"/_matrix/client/unstable/account/3pid/add", + { + "client_secret": client_secret, + "sid": session_id, + "auth": { + "type": "m.login.password", + "user": self.user_id, + "password": "test", + }, + }, + access_token=self.user_id_tok, + ) + self.render(request) + self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("3PID changes disabled on this server", channel.json_body["error"]) + + # Get user + request, channel = self.make_request( + "GET", self.url_3pid, access_token=self.user_id_tok, + ) + self.render(request) + + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertFalse(channel.json_body["threepids"]) + + def test_delete_email(self): + """Test delete mail from profile + """ + # Add a threepid + self.get_success( + self.store.user_add_threepid( + user_id=self.user_id, + medium="email", + address=self.email, + validated_at=0, + added_at=0, + ) + ) + + request, channel = self.make_request( + "POST", + b"account/3pid/delete", + { + "medium": "email", + "address": self.email + }, + access_token=self.user_id_tok, + ) + self.render(request) + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + + # Get user + request, channel = self.make_request( + "GET", self.url_3pid, access_token=self.user_id_tok, + ) + self.render(request) + + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertFalse(channel.json_body["threepids"]) + + def test_delete_email_if_disabled(self): + """Test delete mail from profile if disabled + """ + self.hs.config.disable_3pid_changes = True + + # Add a threepid + self.get_success( + self.store.user_add_threepid( + user_id=self.user_id, + medium="email", + address=self.email, + validated_at=0, + added_at=0, + ) + ) + + request, channel = self.make_request( + "POST", + b"account/3pid/delete", + { + "medium": "email", + "address": self.email + }, + access_token=self.user_id_tok, + ) + self.render(request) + + self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("3PID changes disabled on this server", channel.json_body["error"]) + + # Get user + request, channel = self.make_request( + "GET", self.url_3pid, access_token=self.user_id_tok, + ) + self.render(request) + + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("email", channel.json_body["threepids"][0]["medium"]) + self.assertEqual(self.email, channel.json_body["threepids"][0]["address"]) + + def test_cant_add_email_without_clicking_link(self): + """Test that we do actually need to click the link in the email + """ + client_secret = "foobar" + session_id = self._request_token(self.email, client_secret) + + self.assertEquals(len(self.email_attempts), 1) + + # Attempt to add email without clicking the link + request, channel = self.make_request( + "POST", + b"/_matrix/client/unstable/account/3pid/add", + { + "client_secret": client_secret, + "sid": session_id, + "auth": { + "type": "m.login.password", + "user": self.user_id, + "password": "test", + }, + }, + access_token=self.user_id_tok, + ) + self.render(request) + self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("No validated 3pid session found", channel.json_body["error"]) + + # Get user + request, channel = self.make_request( + "GET", self.url_3pid, access_token=self.user_id_tok, + ) + self.render(request) + + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertFalse(channel.json_body["threepids"]) + + def test_no_valid_token(self): + """Test that we do actually need to request a token and can't just + make a session up. + """ + client_secret = "foobar" + session_id = "weasle" + + # Attempt to add email without even requesting an email + request, channel = self.make_request( + "POST", + b"/_matrix/client/unstable/account/3pid/add", + { + "client_secret": client_secret, + "sid": session_id, + "auth": { + "type": "m.login.password", + "user": self.user_id, + "password": "test", + }, + }, + access_token=self.user_id_tok, + ) + self.render(request) + self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("No validated 3pid session found", channel.json_body["error"]) + + # Get user + request, channel = self.make_request( + "GET", self.url_3pid, access_token=self.user_id_tok, + ) + self.render(request) + + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertFalse(channel.json_body["threepids"]) + + def _request_token(self, email, client_secret): + request, channel = self.make_request( + "POST", + b"account/3pid/email/requestToken", + {"client_secret": client_secret, "email": email, "send_attempt": 1}, + ) + self.render(request) + self.assertEquals(200, channel.code, channel.result) + + return channel.json_body["sid"] + + def _validate_token(self, link): + # Remove the host + path = link.replace("https://example.com", "") + + request, channel = self.make_request("GET", path, shorthand=False) + self.render(request) + self.assertEquals(200, channel.code, channel.result) + + def _get_link_from_email(self): + assert self.email_attempts, "No emails have been sent" + + raw_msg = self.email_attempts[-1].decode("UTF-8") + mail = Parser().parsestr(raw_msg) + + text = None + for part in mail.walk(): + if part.get_content_type() == "text/plain": + text = part.get_payload(decode=True).decode("UTF-8") + break + + if not text: + self.fail("Could not find text portion of email to parse") + + match = re.search(r"https://example.com\S+", text) + assert match, "Could not find link in email" + + return match.group(0) From 50ea178c201588b5e6b3f93e1af56aef0b4e8368 Mon Sep 17 00:00:00 2001 From: dklimpel <5740567+dklimpel@users.noreply.github.com> Date: Mon, 9 Mar 2020 19:57:04 +0100 Subject: [PATCH 1167/1623] lint --- tests/rest/client/v2_alpha/test_account.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/tests/rest/client/v2_alpha/test_account.py b/tests/rest/client/v2_alpha/test_account.py index ac9f200de3..e178a53335 100644 --- a/tests/rest/client/v2_alpha/test_account.py +++ b/tests/rest/client/v2_alpha/test_account.py @@ -438,7 +438,9 @@ class ThreepidEmailRestTestCase(unittest.HomeserverTestCase): ) self.render(request) self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) - self.assertEqual("3PID changes disabled on this server", channel.json_body["error"]) + self.assertEqual( + "3PID changes disabled on this server", channel.json_body["error"] + ) # Get user request, channel = self.make_request( @@ -466,10 +468,7 @@ class ThreepidEmailRestTestCase(unittest.HomeserverTestCase): request, channel = self.make_request( "POST", b"account/3pid/delete", - { - "medium": "email", - "address": self.email - }, + {"medium": "email", "address": self.email}, access_token=self.user_id_tok, ) self.render(request) @@ -503,16 +502,15 @@ class ThreepidEmailRestTestCase(unittest.HomeserverTestCase): request, channel = self.make_request( "POST", b"account/3pid/delete", - { - "medium": "email", - "address": self.email - }, + {"medium": "email", "address": self.email}, access_token=self.user_id_tok, ) self.render(request) self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) - self.assertEqual("3PID changes disabled on this server", channel.json_body["error"]) + self.assertEqual( + "3PID changes disabled on this server", channel.json_body["error"] + ) # Get user request, channel = self.make_request( From 7e5f40e7716813f0d32e2efcb32df3c263fbfc63 Mon Sep 17 00:00:00 2001 From: dklimpel <5740567+dklimpel@users.noreply.github.com> Date: Mon, 9 Mar 2020 21:00:36 +0100 Subject: [PATCH 1168/1623] fix tests --- tests/handlers/test_profile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/handlers/test_profile.py b/tests/handlers/test_profile.py index 98b508c3d4..f8c0da5ced 100644 --- a/tests/handlers/test_profile.py +++ b/tests/handlers/test_profile.py @@ -96,7 +96,7 @@ class ProfileTestCase(unittest.TestCase): self.hs.config.disable_set_displayname = True # Set first displayname is allowed, if displayname is null - self.store.set_profile_displayname(self.frank.localpart, "Frank") + yield self.store.set_profile_displayname(self.frank.localpart, "Frank") d = self.handler.set_displayname( self.frank, synapse.types.create_requester(self.frank), "Frank Jr." @@ -167,7 +167,7 @@ class ProfileTestCase(unittest.TestCase): self.hs.config.disable_set_avatar_url = True # Set first time avatar is allowed, if displayname is null - self.store.set_profile_avatar_url( + yield self.store.set_profile_avatar_url( self.frank.localpart, "http://my.server/me.png" ) From 885134529ffd95dd118d3228e69f0e3553f5a6a7 Mon Sep 17 00:00:00 2001 From: dklimpel <5740567+dklimpel@users.noreply.github.com> Date: Mon, 9 Mar 2020 22:09:29 +0100 Subject: [PATCH 1169/1623] updates after review --- changelog.d/7053.feature | 2 +- docs/sample_config.yaml | 10 +++++----- synapse/config/registration.py | 16 ++++++++-------- synapse/handlers/profile.py | 8 ++++---- synapse/rest/client/v2_alpha/account.py | 18 ++++++++++++------ tests/handlers/test_profile.py | 6 +++--- tests/rest/client/v2_alpha/test_account.py | 17 +++++++---------- 7 files changed, 40 insertions(+), 37 deletions(-) diff --git a/changelog.d/7053.feature b/changelog.d/7053.feature index 79955b9780..00f47b2a14 100644 --- a/changelog.d/7053.feature +++ b/changelog.d/7053.feature @@ -1 +1 @@ -Add options to disable setting profile info for prevent changes. \ No newline at end of file +Add options to prevent users from changing their profile or associated 3PIDs. \ No newline at end of file diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index d3ecffac7d..8333800a10 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -1057,18 +1057,18 @@ account_threepid_delegates: #email: https://example.com # Delegate email sending to example.com #msisdn: http://localhost:8090 # Delegate SMS sending to this local process -# If enabled, don't let users set their own display names/avatars +# If disabled, don't let users set their own display names/avatars # other than for the very first time (unless they are a server admin). # Useful when provisioning users based on the contents of a 3rd party # directory and to avoid ambiguities. # -#disable_set_displayname: false -#disable_set_avatar_url: false +#enable_set_displayname: true +#enable_set_avatar_url: true -# If true, stop users from trying to change the 3PIDs associated with +# If false, stop users from trying to change the 3PIDs associated with # their accounts. # -#disable_3pid_changes: false +#enable_3pid_changes: true # Users who register on this homeserver will automatically be joined # to these rooms diff --git a/synapse/config/registration.py b/synapse/config/registration.py index 1abc0a79af..d4897ec9b6 100644 --- a/synapse/config/registration.py +++ b/synapse/config/registration.py @@ -129,9 +129,9 @@ class RegistrationConfig(Config): raise ConfigError("Invalid auto_join_rooms entry %s" % (room_alias,)) self.autocreate_auto_join_rooms = config.get("autocreate_auto_join_rooms", True) - self.disable_set_displayname = config.get("disable_set_displayname", False) - self.disable_set_avatar_url = config.get("disable_set_avatar_url", False) - self.disable_3pid_changes = config.get("disable_3pid_changes", False) + self.enable_set_displayname = config.get("enable_set_displayname", True) + self.enable_set_avatar_url = config.get("enable_set_avatar_url", True) + self.enable_3pid_changes = config.get("enable_3pid_changes", True) self.disable_msisdn_registration = config.get( "disable_msisdn_registration", False @@ -334,18 +334,18 @@ class RegistrationConfig(Config): #email: https://example.com # Delegate email sending to example.com #msisdn: http://localhost:8090 # Delegate SMS sending to this local process - # If enabled, don't let users set their own display names/avatars + # If disabled, don't let users set their own display names/avatars # other than for the very first time (unless they are a server admin). # Useful when provisioning users based on the contents of a 3rd party # directory and to avoid ambiguities. # - #disable_set_displayname: false - #disable_set_avatar_url: false + #enable_set_displayname: true + #enable_set_avatar_url: true - # If true, stop users from trying to change the 3PIDs associated with + # If false, stop users from trying to change the 3PIDs associated with # their accounts. # - #disable_3pid_changes: false + #enable_3pid_changes: true # Users who register on this homeserver will automatically be joined # to these rooms diff --git a/synapse/handlers/profile.py b/synapse/handlers/profile.py index b049dd8e26..eb85dba015 100644 --- a/synapse/handlers/profile.py +++ b/synapse/handlers/profile.py @@ -157,11 +157,11 @@ class BaseProfileHandler(BaseHandler): if not by_admin and target_user != requester.user: raise AuthError(400, "Cannot set another user's displayname") - if not by_admin and self.hs.config.disable_set_displayname: + if not by_admin and not self.hs.config.enable_set_displayname: profile = yield self.store.get_profileinfo(target_user.localpart) if profile.display_name: raise SynapseError( - 400, "Changing displayname is disabled on this server" + 400, "Changing display name is disabled on this server", Codes.FORBIDDEN ) if len(new_displayname) > MAX_DISPLAYNAME_LEN: @@ -225,11 +225,11 @@ class BaseProfileHandler(BaseHandler): if not by_admin and target_user != requester.user: raise AuthError(400, "Cannot set another user's avatar_url") - if not by_admin and self.hs.config.disable_set_avatar_url: + if not by_admin and not self.hs.config.enable_set_avatar_url: profile = yield self.store.get_profileinfo(target_user.localpart) if profile.avatar_url: raise SynapseError( - 400, "Changing avatar url is disabled on this server" + 400, "Changing avatar is disabled on this server", Codes.FORBIDDEN ) if len(new_avatar_url) > MAX_AVATAR_URL_LEN: diff --git a/synapse/rest/client/v2_alpha/account.py b/synapse/rest/client/v2_alpha/account.py index 97bddf36d9..e40136f2f3 100644 --- a/synapse/rest/client/v2_alpha/account.py +++ b/synapse/rest/client/v2_alpha/account.py @@ -599,8 +599,10 @@ class ThreepidRestServlet(RestServlet): return 200, {"threepids": threepids} async def on_POST(self, request): - if self.hs.config.disable_3pid_changes: - raise SynapseError(400, "3PID changes disabled on this server") + if not self.hs.config.enable_3pid_changes: + raise SynapseError( + 400, "3PID changes are disabled on this server", Codes.FORBIDDEN + ) requester = await self.auth.get_user_by_req(request) user_id = requester.user.to_string() @@ -646,8 +648,10 @@ class ThreepidAddRestServlet(RestServlet): @interactive_auth_handler async def on_POST(self, request): - if self.hs.config.disable_3pid_changes: - raise SynapseError(400, "3PID changes disabled on this server") + if not self.hs.config.enable_3pid_changes: + raise SynapseError( + 400, "3PID changes are disabled on this server", Codes.FORBIDDEN + ) requester = await self.auth.get_user_by_req(request) user_id = requester.user.to_string() @@ -749,8 +753,10 @@ class ThreepidDeleteRestServlet(RestServlet): self.auth_handler = hs.get_auth_handler() async def on_POST(self, request): - if self.hs.config.disable_3pid_changes: - raise SynapseError(400, "3PID changes disabled on this server") + if not self.hs.config.enable_3pid_changes: + raise SynapseError( + 400, "3PID changes are disabled on this server", Codes.FORBIDDEN + ) body = parse_json_object_from_request(request) assert_params_in_dict(body, ["medium", "address"]) diff --git a/tests/handlers/test_profile.py b/tests/handlers/test_profile.py index f8c0da5ced..e600b9777b 100644 --- a/tests/handlers/test_profile.py +++ b/tests/handlers/test_profile.py @@ -93,7 +93,7 @@ class ProfileTestCase(unittest.TestCase): @defer.inlineCallbacks def test_set_my_name_if_disabled(self): - self.hs.config.disable_set_displayname = True + self.hs.config.enable_set_displayname = False # Set first displayname is allowed, if displayname is null yield self.store.set_profile_displayname(self.frank.localpart, "Frank") @@ -164,9 +164,9 @@ class ProfileTestCase(unittest.TestCase): @defer.inlineCallbacks def test_set_my_avatar_if_disabled(self): - self.hs.config.disable_set_avatar_url = True + self.hs.config.enable_set_avatar_url = False - # Set first time avatar is allowed, if displayname is null + # Set first time avatar is allowed, if avatar is null yield self.store.set_profile_avatar_url( self.frank.localpart, "http://my.server/me.png" ) diff --git a/tests/rest/client/v2_alpha/test_account.py b/tests/rest/client/v2_alpha/test_account.py index e178a53335..34e40a36d0 100644 --- a/tests/rest/client/v2_alpha/test_account.py +++ b/tests/rest/client/v2_alpha/test_account.py @@ -24,6 +24,7 @@ import pkg_resources import synapse.rest.admin from synapse.api.constants import LoginType, Membership +from synapse.api.errors import Codes from synapse.rest.client.v1 import login, room from synapse.rest.client.v2_alpha import account, register @@ -412,7 +413,7 @@ class ThreepidEmailRestTestCase(unittest.HomeserverTestCase): def test_add_email_if_disabled(self): """Test add mail to profile if disabled """ - self.hs.config.disable_3pid_changes = True + self.hs.config.enable_3pid_changes = True client_secret = "foobar" session_id = self._request_token(self.email, client_secret) @@ -438,9 +439,7 @@ class ThreepidEmailRestTestCase(unittest.HomeserverTestCase): ) self.render(request) self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) - self.assertEqual( - "3PID changes disabled on this server", channel.json_body["error"] - ) + self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"]) # Get user request, channel = self.make_request( @@ -486,7 +485,7 @@ class ThreepidEmailRestTestCase(unittest.HomeserverTestCase): def test_delete_email_if_disabled(self): """Test delete mail from profile if disabled """ - self.hs.config.disable_3pid_changes = True + self.hs.config.enable_3pid_changes = True # Add a threepid self.get_success( @@ -508,9 +507,7 @@ class ThreepidEmailRestTestCase(unittest.HomeserverTestCase): self.render(request) self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) - self.assertEqual( - "3PID changes disabled on this server", channel.json_body["error"] - ) + self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"]) # Get user request, channel = self.make_request( @@ -547,7 +544,7 @@ class ThreepidEmailRestTestCase(unittest.HomeserverTestCase): ) self.render(request) self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) - self.assertEqual("No validated 3pid session found", channel.json_body["error"]) + self.assertEqual(Codes.THREEPID_AUTH_FAILED, channel.json_body["errcode"]) # Get user request, channel = self.make_request( @@ -582,7 +579,7 @@ class ThreepidEmailRestTestCase(unittest.HomeserverTestCase): ) self.render(request) self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) - self.assertEqual("No validated 3pid session found", channel.json_body["error"]) + self.assertEqual(Codes.THREEPID_AUTH_FAILED, channel.json_body["errcode"]) # Get user request, channel = self.make_request( From 39f6595b4ab108cb451072ae251a91117002191c Mon Sep 17 00:00:00 2001 From: dklimpel <5740567+dklimpel@users.noreply.github.com> Date: Mon, 9 Mar 2020 22:13:20 +0100 Subject: [PATCH 1170/1623] lint, fix tests --- synapse/handlers/profile.py | 4 +++- tests/rest/client/v2_alpha/test_account.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/synapse/handlers/profile.py b/synapse/handlers/profile.py index eb85dba015..6aa1c0f5e0 100644 --- a/synapse/handlers/profile.py +++ b/synapse/handlers/profile.py @@ -161,7 +161,9 @@ class BaseProfileHandler(BaseHandler): profile = yield self.store.get_profileinfo(target_user.localpart) if profile.display_name: raise SynapseError( - 400, "Changing display name is disabled on this server", Codes.FORBIDDEN + 400, + "Changing display name is disabled on this server", + Codes.FORBIDDEN, ) if len(new_displayname) > MAX_DISPLAYNAME_LEN: diff --git a/tests/rest/client/v2_alpha/test_account.py b/tests/rest/client/v2_alpha/test_account.py index 34e40a36d0..99cc9163f3 100644 --- a/tests/rest/client/v2_alpha/test_account.py +++ b/tests/rest/client/v2_alpha/test_account.py @@ -413,7 +413,7 @@ class ThreepidEmailRestTestCase(unittest.HomeserverTestCase): def test_add_email_if_disabled(self): """Test add mail to profile if disabled """ - self.hs.config.enable_3pid_changes = True + self.hs.config.enable_3pid_changes = False client_secret = "foobar" session_id = self._request_token(self.email, client_secret) @@ -485,7 +485,7 @@ class ThreepidEmailRestTestCase(unittest.HomeserverTestCase): def test_delete_email_if_disabled(self): """Test delete mail from profile if disabled """ - self.hs.config.enable_3pid_changes = True + self.hs.config.enable_3pid_changes = False # Add a threepid self.get_success( From 6b0efe73e21a5d346111df4dd367bc39a03108bb Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 10 Mar 2020 13:49:11 +0000 Subject: [PATCH 1171/1623] SAML2: render a comprehensible error page if something goes wrong If an error happened while processing a SAML AuthN response, or a client ends up doing a `GET` request to `/authn_response`, then render a customisable error page rather than a confusing error. --- synapse/config/saml2_config.py | 26 +++++++++++++++++++++++++ synapse/handlers/saml_handler.py | 20 ++++++++++++++++++- synapse/rest/saml2/response_resource.py | 18 ++++++++++++++++- 3 files changed, 62 insertions(+), 2 deletions(-) diff --git a/synapse/config/saml2_config.py b/synapse/config/saml2_config.py index 423c158b11..db035bdb5d 100644 --- a/synapse/config/saml2_config.py +++ b/synapse/config/saml2_config.py @@ -27,6 +27,18 @@ DEFAULT_USER_MAPPING_PROVIDER = ( "synapse.handlers.saml_handler.DefaultSamlMappingProvider" ) +SAML2_ERROR_DEFAULT_HTML = """ + + +

    Oops! Something went wrong

    +

    + Try logging in again from the application and if the problem persists + please contact the administrator. +

    + + +""" + def _dict_merge(merge_dict, into_dict): """Do a deep merge of two dicts @@ -160,6 +172,13 @@ class SAML2Config(Config): saml2_config.get("saml_session_lifetime", "5m") ) + if "error_html_path" in config: + self.saml2_error_html_content = self.read_file( + config["error_html_path"], "saml2_config.error_html_path", + ) + else: + self.saml2_error_html_content = SAML2_ERROR_DEFAULT_HTML + def _default_saml_config_dict( self, required_attributes: set, optional_attributes: set ): @@ -325,6 +344,13 @@ class SAML2Config(Config): # The default is 'uid'. # #grandfathered_mxid_source_attribute: upn + + # Path to a file containing HTML content to serve in case an error happens + # when the user gets redirected from the SAML IdP back to Synapse. + # If no file is provided, this defaults to some minimalistic HTML telling the + # user that something went wrong and they should try authenticating again. + # + #error_html_path: /path/to/static/content/saml_error.html """ % { "config_dir_path": config_dir_path } diff --git a/synapse/handlers/saml_handler.py b/synapse/handlers/saml_handler.py index 9406753393..72c109981b 100644 --- a/synapse/handlers/saml_handler.py +++ b/synapse/handlers/saml_handler.py @@ -23,6 +23,7 @@ from saml2.client import Saml2Client from synapse.api.errors import SynapseError from synapse.config import ConfigError +from synapse.http.server import finish_request from synapse.http.servlet import parse_string from synapse.module_api import ModuleApi from synapse.types import ( @@ -73,6 +74,8 @@ class SamlHandler: # a lock on the mappings self._mapping_lock = Linearizer(name="saml_mapping", clock=self._clock) + self._error_html_content = hs.config.saml2_error_html_content + def handle_redirect_request(self, client_redirect_url): """Handle an incoming request to /login/sso/redirect @@ -114,7 +117,22 @@ class SamlHandler: # the dict. self.expire_sessions() - user_id = await self._map_saml_response_to_user(resp_bytes, relay_state) + try: + user_id = await self._map_saml_response_to_user(resp_bytes, relay_state) + except Exception as e: + # If decoding the response or mapping it to a user failed, then log the + # error and tell the user that something went wrong. + logger.error(e) + + request.setResponseCode(400) + request.setHeader(b"Content-Type", b"text/html; charset=utf-8") + request.setHeader( + b"Content-Length", b"%d" % (len(self._error_html_content),) + ) + request.write(self._error_html_content.encode("utf8")) + finish_request(request) + return + self._auth_handler.complete_sso_login(user_id, request, relay_state) async def _map_saml_response_to_user(self, resp_bytes, client_redirect_url): diff --git a/synapse/rest/saml2/response_resource.py b/synapse/rest/saml2/response_resource.py index 69ecc5e4b4..a545c13db7 100644 --- a/synapse/rest/saml2/response_resource.py +++ b/synapse/rest/saml2/response_resource.py @@ -14,7 +14,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -from synapse.http.server import DirectServeResource, wrap_html_request_handler +from synapse.http.server import ( + DirectServeResource, + finish_request, + wrap_html_request_handler, +) class SAML2ResponseResource(DirectServeResource): @@ -24,8 +28,20 @@ class SAML2ResponseResource(DirectServeResource): def __init__(self, hs): super().__init__() + self._error_html_content = hs.config.saml2_error_html_content self._saml_handler = hs.get_saml_handler() + async def _async_render_GET(self, request): + # We're not expecting any GET request on that resource if everything goes right, + # but some IdPs sometimes end up responding with a 302 redirect on this endpoint. + # In this case, just tell the user that something went wrong and they should + # try to authenticate again. + request.setResponseCode(400) + request.setHeader(b"Content-Type", b"text/html; charset=utf-8") + request.setHeader(b"Content-Length", b"%d" % (len(self._error_html_content),)) + request.write(self._error_html_content.encode("utf8")) + finish_request(request) + @wrap_html_request_handler async def _async_render_POST(self, request): return await self._saml_handler.handle_saml_response(request) From 51c094c4ace1ee70f2ca3cb1766121885dbb92da Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 10 Mar 2020 14:00:29 +0000 Subject: [PATCH 1172/1623] Update sample config --- docs/sample_config.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 6f6f6fd54b..01957a90dd 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -1347,6 +1347,13 @@ saml2_config: # #grandfathered_mxid_source_attribute: upn + # Path to a file containing HTML content to serve in case an error happens + # when the user gets redirected from the SAML IdP back to Synapse. + # If no file is provided, this defaults to some minimalistic HTML telling the + # user that something went wrong and they should try authenticating again. + # + #error_html_path: /path/to/static/content/saml_error.html + # Enable CAS for registration and login. From 156f2718673f88188627c76952102ef08ea34256 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 10 Mar 2020 14:01:24 +0000 Subject: [PATCH 1173/1623] Changelog --- changelog.d/7058.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7058.feature diff --git a/changelog.d/7058.feature b/changelog.d/7058.feature new file mode 100644 index 0000000000..53ea485e03 --- /dev/null +++ b/changelog.d/7058.feature @@ -0,0 +1 @@ +Render a configurable and comprehensible error page if something goes wrong during the SAML2 authentication process. From 5ec2077bf905ef2edb5e4d6d6028fdc6aaa99c90 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 10 Mar 2020 14:04:20 +0000 Subject: [PATCH 1174/1623] Lint --- synapse/config/saml2_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/config/saml2_config.py b/synapse/config/saml2_config.py index db035bdb5d..d3e281604f 100644 --- a/synapse/config/saml2_config.py +++ b/synapse/config/saml2_config.py @@ -344,7 +344,7 @@ class SAML2Config(Config): # The default is 'uid'. # #grandfathered_mxid_source_attribute: upn - + # Path to a file containing HTML content to serve in case an error happens # when the user gets redirected from the SAML IdP back to Synapse. # If no file is provided, this defaults to some minimalistic HTML telling the From fe593ef99097f16e7c325c574364536d4b221c92 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 10 Mar 2020 14:19:06 +0000 Subject: [PATCH 1175/1623] Attempt at appeasing the gods of mypy --- synapse/logging/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/logging/context.py b/synapse/logging/context.py index 1eccc0e83f..56805120be 100644 --- a/synapse/logging/context.py +++ b/synapse/logging/context.py @@ -511,7 +511,7 @@ class PreserveLoggingContext(object): __slots__ = ["current_context", "new_context", "has_parent"] - def __init__(self, new_context: Optional[LoggingContext] = None) -> None: + def __init__(self, new_context: Optional[LoggingContextOrSentinel] = None) -> None: if new_context is None: self.new_context = LoggingContext.sentinel # type: LoggingContextOrSentinel else: From dc6fb56c5ffb41d907b7fd645a701c2d9684afc3 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 10 Mar 2020 14:40:28 +0000 Subject: [PATCH 1176/1623] Hopefully mypy is happy now --- synapse/logging/context.py | 3 ++- synapse/storage/database.py | 10 ++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/synapse/logging/context.py b/synapse/logging/context.py index 56805120be..860b99a4c6 100644 --- a/synapse/logging/context.py +++ b/synapse/logging/context.py @@ -210,7 +210,7 @@ class LoggingContext(object): class Sentinel(object): """Sentinel to represent the root context""" - __slots__ = ["previous_context", "alive", "request", "scope"] + __slots__ = ["previous_context", "alive", "request", "scope", "tag"] def __init__(self) -> None: # Minimal set for compatibility with LoggingContext @@ -218,6 +218,7 @@ class LoggingContext(object): self.alive = None self.request = None self.scope = None + self.tag = None def __str__(self): return "sentinel" diff --git a/synapse/storage/database.py b/synapse/storage/database.py index 609db40616..e61595336c 100644 --- a/synapse/storage/database.py +++ b/synapse/storage/database.py @@ -29,7 +29,11 @@ from twisted.internet import defer from synapse.api.errors import StoreError from synapse.config.database import DatabaseConnectionConfig -from synapse.logging.context import LoggingContext, make_deferred_yieldable +from synapse.logging.context import ( + LoggingContext, + LoggingContextOrSentinel, + make_deferred_yieldable, +) from synapse.metrics.background_process_metrics import run_as_background_process from synapse.storage.background_updates import BackgroundUpdater from synapse.storage.engines import BaseDatabaseEngine, PostgresEngine, Sqlite3Engine @@ -543,7 +547,9 @@ class Database(object): Returns: Deferred: The result of func """ - parent_context = LoggingContext.current_context() + parent_context = ( + LoggingContext.current_context() + ) # type: Optional[LoggingContextOrSentinel] if parent_context == LoggingContext.sentinel: logger.warning( "Starting db connection from sentinel context: metrics will be lost" From 8f826f98ac5d0a08f6726d3157c94265bbb2501c Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 10 Mar 2020 17:22:45 +0000 Subject: [PATCH 1177/1623] Rephrase default message --- synapse/config/saml2_config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/synapse/config/saml2_config.py b/synapse/config/saml2_config.py index d3e281604f..07895c4315 100644 --- a/synapse/config/saml2_config.py +++ b/synapse/config/saml2_config.py @@ -32,8 +32,8 @@ SAML2_ERROR_DEFAULT_HTML = """

    Oops! Something went wrong

    - Try logging in again from the application and if the problem persists - please contact the administrator. + Try logging in again from your Matrix client and if the problem persists + please contact the server's administrator.

    From 42ac4ca47709a4a9fb8b71a60c4b92cc615b0908 Mon Sep 17 00:00:00 2001 From: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com> Date: Tue, 10 Mar 2020 21:26:55 +0100 Subject: [PATCH 1178/1623] Update synapse/config/registration.py Co-Authored-By: Brendan Abolivier --- synapse/config/registration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/config/registration.py b/synapse/config/registration.py index d4897ec9b6..ee737eb40d 100644 --- a/synapse/config/registration.py +++ b/synapse/config/registration.py @@ -335,7 +335,7 @@ class RegistrationConfig(Config): #msisdn: http://localhost:8090 # Delegate SMS sending to this local process # If disabled, don't let users set their own display names/avatars - # other than for the very first time (unless they are a server admin). + # (unless they are a server admin) other than for the very first time. # Useful when provisioning users based on the contents of a 3rd party # directory and to avoid ambiguities. # From 751d51dd128be154c01f23f5f614317689336812 Mon Sep 17 00:00:00 2001 From: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com> Date: Tue, 10 Mar 2020 21:41:25 +0100 Subject: [PATCH 1179/1623] Update sample_config.yaml --- docs/sample_config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 8333800a10..5940a6506b 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -1058,7 +1058,7 @@ account_threepid_delegates: #msisdn: http://localhost:8090 # Delegate SMS sending to this local process # If disabled, don't let users set their own display names/avatars -# other than for the very first time (unless they are a server admin). +# (unless they are a server admin) other than for the very first time. # Useful when provisioning users based on the contents of a 3rd party # directory and to avoid ambiguities. # From 69ce55c51082d03e549863f2149b4cf10cb1de19 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 11 Mar 2020 15:21:25 +0000 Subject: [PATCH 1180/1623] Don't filter out dummy events when we're checking the visibility of state --- synapse/handlers/message.py | 2 +- synapse/visibility.py | 15 +++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index 0c84c6cec4..b743fc2dcc 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -160,7 +160,7 @@ class MessageHandler(object): raise NotFoundError("Can't find event for token %s" % (at_token,)) visible_events = yield filter_events_for_client( - self.storage, user_id, last_events, apply_retention_policies=False + self.storage, user_id, last_events, filter_send_to_client=False ) event = last_events[0] diff --git a/synapse/visibility.py b/synapse/visibility.py index a48a4f3dfe..1d538b206d 100644 --- a/synapse/visibility.py +++ b/synapse/visibility.py @@ -49,7 +49,7 @@ def filter_events_for_client( events, is_peeking=False, always_include_ids=frozenset(), - apply_retention_policies=True, + filter_send_to_client=True, ): """ Check which events a user is allowed to see. If the user can see the event but its @@ -65,10 +65,9 @@ def filter_events_for_client( events always_include_ids (set(event_id)): set of event ids to specifically include (unless sender is ignored) - apply_retention_policies (bool): Whether to filter out events that's older than - allowed by the room's retention policy. Useful when this function is called - to e.g. check whether a user should be allowed to see the state at a given - event rather than to know if it should send an event to a user's client(s). + filter_send_to_client (bool): Whether we're checking an event that's going to be + sent to a client. This might not always be the case since this function can + also be called to check whether a user can see the state at a given point. Returns: Deferred[list[synapse.events.EventBase]] @@ -96,7 +95,7 @@ def filter_events_for_client( erased_senders = yield storage.main.are_users_erased((e.sender for e in events)) - if apply_retention_policies: + if not filter_send_to_client: room_ids = {e.room_id for e in events} retention_policies = {} @@ -119,7 +118,7 @@ def filter_events_for_client( the original event if they can see it as normal. """ - if event.type == "org.matrix.dummy_event": + if event.type == "org.matrix.dummy_event" and filter_send_to_client: return None if not event.is_state() and event.sender in ignore_list: @@ -134,7 +133,7 @@ def filter_events_for_client( # Don't try to apply the room's retention policy if the event is a state event, as # MSC1763 states that retention is only considered for non-state events. - if apply_retention_policies and not event.is_state(): + if filter_send_to_client and not event.is_state(): retention_policy = retention_policies[event.room_id] max_lifetime = retention_policy.get("max_lifetime") From 9c0775e86ab39b193670723927a1caf67f6bfc11 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 11 Mar 2020 15:32:07 +0000 Subject: [PATCH 1181/1623] Fix condition --- synapse/visibility.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/visibility.py b/synapse/visibility.py index 1d538b206d..d0b2241e48 100644 --- a/synapse/visibility.py +++ b/synapse/visibility.py @@ -95,7 +95,7 @@ def filter_events_for_client( erased_senders = yield storage.main.are_users_erased((e.sender for e in events)) - if not filter_send_to_client: + if filter_send_to_client: room_ids = {e.room_id for e in events} retention_policies = {} From 2dce68c65110d4fe41efcc7150c9c6300ac71d2c Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 11 Mar 2020 17:04:18 +0000 Subject: [PATCH 1182/1623] Also don't filter out events sent by ignored users when checking state visibility --- synapse/visibility.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/visibility.py b/synapse/visibility.py index d0b2241e48..82a2132427 100644 --- a/synapse/visibility.py +++ b/synapse/visibility.py @@ -121,7 +121,7 @@ def filter_events_for_client( if event.type == "org.matrix.dummy_event" and filter_send_to_client: return None - if not event.is_state() and event.sender in ignore_list: + if not event.is_state() and event.sender in ignore_list and filter_send_to_client: return None # Until MSC2261 has landed we can't redact malicious alias events, so for From 1cde4cf3f15413b941c699ac5048c464a49137a4 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 11 Mar 2020 18:03:56 +0000 Subject: [PATCH 1183/1623] Changelog --- changelog.d/7066.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7066.bugfix diff --git a/changelog.d/7066.bugfix b/changelog.d/7066.bugfix new file mode 100644 index 0000000000..94bb096287 --- /dev/null +++ b/changelog.d/7066.bugfix @@ -0,0 +1 @@ +Fix a bug that would cause Synapse to respond with an error about event visibility if a client tried to request the state of a room at a given token. From e38c44b418328e79e8da3e8ed259ee51d2f1c215 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 11 Mar 2020 18:06:07 +0000 Subject: [PATCH 1184/1623] Lint --- synapse/visibility.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/synapse/visibility.py b/synapse/visibility.py index 82a2132427..fce5855413 100644 --- a/synapse/visibility.py +++ b/synapse/visibility.py @@ -121,7 +121,11 @@ def filter_events_for_client( if event.type == "org.matrix.dummy_event" and filter_send_to_client: return None - if not event.is_state() and event.sender in ignore_list and filter_send_to_client: + if ( + not event.is_state() + and event.sender in ignore_list + and filter_send_to_client + ): return None # Until MSC2261 has landed we can't redact malicious alias events, so for From 37a9873f6360a8e6f243c3d3d081ff7abc0f9da1 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 11 Mar 2020 18:43:41 +0000 Subject: [PATCH 1185/1623] Also don't fail on aliases events in this case --- synapse/visibility.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/visibility.py b/synapse/visibility.py index fce5855413..56603eb276 100644 --- a/synapse/visibility.py +++ b/synapse/visibility.py @@ -132,7 +132,7 @@ def filter_events_for_client( # now we temporarily filter out m.room.aliases entirely to mitigate # abuse, while we spec a better solution to advertising aliases # on rooms. - if event.type == EventTypes.Aliases: + if event.type == EventTypes.Aliases and filter_send_to_client: return None # Don't try to apply the room's retention policy if the event is a state event, as From 8120a238a465de576ad4d171e3072b28e5df32ac Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 11 Mar 2020 18:49:41 +0000 Subject: [PATCH 1186/1623] Refactor a bit --- synapse/visibility.py | 48 ++++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/synapse/visibility.py b/synapse/visibility.py index 56603eb276..bab41182b9 100644 --- a/synapse/visibility.py +++ b/synapse/visibility.py @@ -118,34 +118,36 @@ def filter_events_for_client( the original event if they can see it as normal. """ - if event.type == "org.matrix.dummy_event" and filter_send_to_client: - return None + # Only run some checks if these events aren't about to be sent to clients. This is + # because, if this is not the case, we're probably only checking if the users can + # see events in the room at that point in the DAG, and that shouldn't be decided + # on those checks. + if filter_send_to_client: + if event.type == "org.matrix.dummy_event": + return None - if ( - not event.is_state() - and event.sender in ignore_list - and filter_send_to_client - ): - return None + if not event.is_state() and event.sender in ignore_list: + return None - # Until MSC2261 has landed we can't redact malicious alias events, so for - # now we temporarily filter out m.room.aliases entirely to mitigate - # abuse, while we spec a better solution to advertising aliases - # on rooms. - if event.type == EventTypes.Aliases and filter_send_to_client: - return None + # Until MSC2261 has landed we can't redact malicious alias events, so for + # now we temporarily filter out m.room.aliases entirely to mitigate + # abuse, while we spec a better solution to advertising aliases + # on rooms. + if event.type == EventTypes.Aliases: + return None - # Don't try to apply the room's retention policy if the event is a state event, as - # MSC1763 states that retention is only considered for non-state events. - if filter_send_to_client and not event.is_state(): - retention_policy = retention_policies[event.room_id] - max_lifetime = retention_policy.get("max_lifetime") + # Don't try to apply the room's retention policy if the event is a state + # event, as MSC1763 states that retention is only considered for non-state + # events. + if not event.is_state(): + retention_policy = retention_policies[event.room_id] + max_lifetime = retention_policy.get("max_lifetime") - if max_lifetime is not None: - oldest_allowed_ts = storage.main.clock.time_msec() - max_lifetime + if max_lifetime is not None: + oldest_allowed_ts = storage.main.clock.time_msec() - max_lifetime - if event.origin_server_ts < oldest_allowed_ts: - return None + if event.origin_server_ts < oldest_allowed_ts: + return None if event.event_id in always_include_ids: return event From b8cfe79ffcc1184547673264563884e0188e47a7 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 11 Mar 2020 19:33:16 +0000 Subject: [PATCH 1187/1623] Move the default SAML2 error HTML to a dedicated file Also add some JS to it to process any error we might have in the URI (see #6893). --- synapse/config/saml2_config.py | 29 +++++++----------- synapse/res/templates/saml_error.html | 44 +++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 18 deletions(-) create mode 100644 synapse/res/templates/saml_error.html diff --git a/synapse/config/saml2_config.py b/synapse/config/saml2_config.py index 07895c4315..882aa3bb5b 100644 --- a/synapse/config/saml2_config.py +++ b/synapse/config/saml2_config.py @@ -15,6 +15,9 @@ # limitations under the License. import logging +import os + +import pkg_resources from synapse.python_dependencies import DependencyException, check_requirements from synapse.util.module_loader import load_module, load_python_module @@ -27,18 +30,6 @@ DEFAULT_USER_MAPPING_PROVIDER = ( "synapse.handlers.saml_handler.DefaultSamlMappingProvider" ) -SAML2_ERROR_DEFAULT_HTML = """ - - -

    Oops! Something went wrong

    -

    - Try logging in again from your Matrix client and if the problem persists - please contact the server's administrator. -

    - - -""" - def _dict_merge(merge_dict, into_dict): """Do a deep merge of two dicts @@ -172,12 +163,14 @@ class SAML2Config(Config): saml2_config.get("saml_session_lifetime", "5m") ) - if "error_html_path" in config: - self.saml2_error_html_content = self.read_file( - config["error_html_path"], "saml2_config.error_html_path", - ) - else: - self.saml2_error_html_content = SAML2_ERROR_DEFAULT_HTML + error_html_path = config.get("error_html_path") + if not error_html_path: + template_dir = pkg_resources.resource_filename("synapse", "res/templates") + error_html_path = os.path.join(template_dir, "saml_error.html") + + self.saml2_error_html_content = self.read_file( + error_html_path, "saml2_config.error_html_path", + ) def _default_saml_config_dict( self, required_attributes: set, optional_attributes: set diff --git a/synapse/res/templates/saml_error.html b/synapse/res/templates/saml_error.html new file mode 100644 index 0000000000..c112ac833f --- /dev/null +++ b/synapse/res/templates/saml_error.html @@ -0,0 +1,44 @@ + + + + + SSO error + + +

    Oops! Something went wrong during authentication.

    +

    + If you are seeing this page after clicking a link sent to you via email, make + sure you only click the confirmation link once, and that you open the + validation link in the same client you're logging in from. +

    +

    + Try logging in again from your Matrix client and if the problem persists + please contact the server's administrator. +

    + + + + \ No newline at end of file From e55a240681a2d3adf34eb48198475e9255b53358 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 11 Mar 2020 19:37:04 +0000 Subject: [PATCH 1188/1623] Changelog --- changelog.d/7067.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7067.feature diff --git a/changelog.d/7067.feature b/changelog.d/7067.feature new file mode 100644 index 0000000000..53ea485e03 --- /dev/null +++ b/changelog.d/7067.feature @@ -0,0 +1 @@ +Render a configurable and comprehensible error page if something goes wrong during the SAML2 authentication process. From 900bca970790f01fd1416b217e678f6ea6325f95 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 11 Mar 2020 19:40:30 +0000 Subject: [PATCH 1189/1623] Update wording and config --- docs/sample_config.yaml | 3 +++ synapse/config/saml2_config.py | 3 +++ synapse/res/templates/saml_error.html | 4 ++-- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 301e6ae6b7..36be995726 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -1365,6 +1365,9 @@ saml2_config: # If no file is provided, this defaults to some minimalistic HTML telling the # user that something went wrong and they should try authenticating again. # + # See https://github.com/matrix-org/synapse/blob/master/synapse/res/templates/saml_error.html + # for an example. + # #error_html_path: /path/to/static/content/saml_error.html diff --git a/synapse/config/saml2_config.py b/synapse/config/saml2_config.py index 882aa3bb5b..1526f72748 100644 --- a/synapse/config/saml2_config.py +++ b/synapse/config/saml2_config.py @@ -343,6 +343,9 @@ class SAML2Config(Config): # If no file is provided, this defaults to some minimalistic HTML telling the # user that something went wrong and they should try authenticating again. # + # See https://github.com/matrix-org/synapse/blob/master/synapse/res/templates/saml_error.html + # for an example. + # #error_html_path: /path/to/static/content/saml_error.html """ % { "config_dir_path": config_dir_path diff --git a/synapse/res/templates/saml_error.html b/synapse/res/templates/saml_error.html index c112ac833f..223d3a74bc 100644 --- a/synapse/res/templates/saml_error.html +++ b/synapse/res/templates/saml_error.html @@ -24,8 +24,8 @@ // we just don't print anything specific. let searchStr = ""; if (window.location.search) { - // For some reason window.location.searchParams isn't always defined when - // window.location.search is, so we can't just use it right away. + // window.location.searchParams isn't always defined when + // window.location.search is, so it's more reliable to parse the latter. searchStr = window.location.search; } else if (window.location.hash) { // Replace the # with a ? so that URLSearchParams does the right thing and From f9e98176bf211593c7cb8661ea5ac97de9a61e31 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 11 Mar 2020 20:31:31 +0000 Subject: [PATCH 1190/1623] Put the file in the templates directory --- docs/sample_config.yaml | 23 ++++++++++++++++------- synapse/config/saml2_config.py | 33 +++++++++++++++++++++------------ 2 files changed, 37 insertions(+), 19 deletions(-) diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 36be995726..91eff4c8ad 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -1360,15 +1360,24 @@ saml2_config: # #grandfathered_mxid_source_attribute: upn - # Path to a file containing HTML content to serve in case an error happens - # when the user gets redirected from the SAML IdP back to Synapse. - # If no file is provided, this defaults to some minimalistic HTML telling the - # user that something went wrong and they should try authenticating again. + # Directory in which Synapse will try to find the template files below. + # If not set, default templates from within the Synapse package will be used. # - # See https://github.com/matrix-org/synapse/blob/master/synapse/res/templates/saml_error.html - # for an example. + # DO NOT UNCOMMENT THIS SETTING unless you want to customise the templates. + # If you *do* uncomment it, you will need to make sure that all the templates + # below are in the directory. # - #error_html_path: /path/to/static/content/saml_error.html + # Synapse will look for the following templates in this directory: + # + # * HTML page to display to users if something goes wrong during the + # authentication process: 'saml_error.html'. + # + # This template doesn't currently need any variable to render. + # + # You can see the default templates at: + # https://github.com/matrix-org/synapse/tree/master/synapse/res/templates + # + #template_dir: "res/templates" diff --git a/synapse/config/saml2_config.py b/synapse/config/saml2_config.py index 1526f72748..3113f11ebb 100644 --- a/synapse/config/saml2_config.py +++ b/synapse/config/saml2_config.py @@ -163,13 +163,13 @@ class SAML2Config(Config): saml2_config.get("saml_session_lifetime", "5m") ) - error_html_path = config.get("error_html_path") - if not error_html_path: - template_dir = pkg_resources.resource_filename("synapse", "res/templates") - error_html_path = os.path.join(template_dir, "saml_error.html") + template_dir = saml2_config.get("template_dir") + if not template_dir: + template_dir = pkg_resources.resource_filename("synapse", "res/templates",) self.saml2_error_html_content = self.read_file( - error_html_path, "saml2_config.error_html_path", + os.path.join(template_dir, "saml_error.html"), + "saml2_config.saml_error", ) def _default_saml_config_dict( @@ -338,15 +338,24 @@ class SAML2Config(Config): # #grandfathered_mxid_source_attribute: upn - # Path to a file containing HTML content to serve in case an error happens - # when the user gets redirected from the SAML IdP back to Synapse. - # If no file is provided, this defaults to some minimalistic HTML telling the - # user that something went wrong and they should try authenticating again. + # Directory in which Synapse will try to find the template files below. + # If not set, default templates from within the Synapse package will be used. # - # See https://github.com/matrix-org/synapse/blob/master/synapse/res/templates/saml_error.html - # for an example. + # DO NOT UNCOMMENT THIS SETTING unless you want to customise the templates. + # If you *do* uncomment it, you will need to make sure that all the templates + # below are in the directory. # - #error_html_path: /path/to/static/content/saml_error.html + # Synapse will look for the following templates in this directory: + # + # * HTML page to display to users if something goes wrong during the + # authentication process: 'saml_error.html'. + # + # This template doesn't currently need any variable to render. + # + # You can see the default templates at: + # https://github.com/matrix-org/synapse/tree/master/synapse/res/templates + # + #template_dir: "res/templates" """ % { "config_dir_path": config_dir_path } From 0de9f9486a242c8dbee4b9bc65cad166b863094f Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 11 Mar 2020 20:39:18 +0000 Subject: [PATCH 1191/1623] Lint --- synapse/config/saml2_config.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/synapse/config/saml2_config.py b/synapse/config/saml2_config.py index 3113f11ebb..8fe64d90f8 100644 --- a/synapse/config/saml2_config.py +++ b/synapse/config/saml2_config.py @@ -168,8 +168,7 @@ class SAML2Config(Config): template_dir = pkg_resources.resource_filename("synapse", "res/templates",) self.saml2_error_html_content = self.read_file( - os.path.join(template_dir, "saml_error.html"), - "saml2_config.saml_error", + os.path.join(template_dir, "saml_error.html"), "saml2_config.saml_error", ) def _default_saml_config_dict( From 77d0a4507b1c8ce3a1195851e87e723287332786 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Thu, 12 Mar 2020 11:36:27 -0400 Subject: [PATCH 1192/1623] Add type annotations and comments to auth handler (#7063) --- changelog.d/7063.misc | 1 + synapse/handlers/auth.py | 193 +++++++++++++++++++++------------------ tox.ini | 1 + 3 files changed, 106 insertions(+), 89 deletions(-) create mode 100644 changelog.d/7063.misc diff --git a/changelog.d/7063.misc b/changelog.d/7063.misc new file mode 100644 index 0000000000..e7b1cd3cd8 --- /dev/null +++ b/changelog.d/7063.misc @@ -0,0 +1 @@ +Add type annotations and comments to the auth handler. diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index 7ca90f91c4..7860f9625e 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -18,10 +18,10 @@ import logging import time import unicodedata import urllib.parse -from typing import Any +from typing import Any, Dict, Iterable, List, Optional import attr -import bcrypt +import bcrypt # type: ignore[import] import pymacaroons from twisted.internet import defer @@ -45,7 +45,7 @@ from synapse.http.site import SynapseRequest from synapse.logging.context import defer_to_thread from synapse.module_api import ModuleApi from synapse.push.mailer import load_jinja2_templates -from synapse.types import UserID +from synapse.types import Requester, UserID from synapse.util.caches.expiringcache import ExpiringCache from ._base import BaseHandler @@ -63,11 +63,11 @@ class AuthHandler(BaseHandler): """ super(AuthHandler, self).__init__(hs) - self.checkers = {} # type: dict[str, UserInteractiveAuthChecker] + self.checkers = {} # type: Dict[str, UserInteractiveAuthChecker] for auth_checker_class in INTERACTIVE_AUTH_CHECKERS: inst = auth_checker_class(hs) if inst.is_enabled(): - self.checkers[inst.AUTH_TYPE] = inst + self.checkers[inst.AUTH_TYPE] = inst # type: ignore self.bcrypt_rounds = hs.config.bcrypt_rounds @@ -124,7 +124,9 @@ class AuthHandler(BaseHandler): self._whitelisted_sso_clients = tuple(hs.config.sso_client_whitelist) @defer.inlineCallbacks - def validate_user_via_ui_auth(self, requester, request_body, clientip): + def validate_user_via_ui_auth( + self, requester: Requester, request_body: Dict[str, Any], clientip: str + ): """ Checks that the user is who they claim to be, via a UI auth. @@ -133,11 +135,11 @@ class AuthHandler(BaseHandler): that it isn't stolen by re-authenticating them. Args: - requester (Requester): The user, as given by the access token + requester: The user, as given by the access token - request_body (dict): The body of the request sent by the client + request_body: The body of the request sent by the client - clientip (str): The IP address of the client. + clientip: The IP address of the client. Returns: defer.Deferred[dict]: the parameters for this request (which may @@ -208,7 +210,9 @@ class AuthHandler(BaseHandler): return self.checkers.keys() @defer.inlineCallbacks - def check_auth(self, flows, clientdict, clientip): + def check_auth( + self, flows: List[List[str]], clientdict: Dict[str, Any], clientip: str + ): """ Takes a dictionary sent by the client in the login / registration protocol and handles the User-Interactive Auth flow. @@ -223,14 +227,14 @@ class AuthHandler(BaseHandler): decorator. Args: - flows (list): A list of login flows. Each flow is an ordered list of - strings representing auth-types. At least one full - flow must be completed in order for auth to be successful. + flows: A list of login flows. Each flow is an ordered list of + strings representing auth-types. At least one full + flow must be completed in order for auth to be successful. clientdict: The dictionary from the client root level, not the 'auth' key: this method prompts for auth if none is sent. - clientip (str): The IP address of the client. + clientip: The IP address of the client. Returns: defer.Deferred[dict, dict, str]: a deferred tuple of @@ -250,7 +254,7 @@ class AuthHandler(BaseHandler): """ authdict = None - sid = None + sid = None # type: Optional[str] if clientdict and "auth" in clientdict: authdict = clientdict["auth"] del clientdict["auth"] @@ -283,9 +287,9 @@ class AuthHandler(BaseHandler): creds = session["creds"] # check auth type currently being presented - errordict = {} + errordict = {} # type: Dict[str, Any] if "type" in authdict: - login_type = authdict["type"] + login_type = authdict["type"] # type: str try: result = yield self._check_auth_dict(authdict, clientip) if result: @@ -326,7 +330,7 @@ class AuthHandler(BaseHandler): raise InteractiveAuthIncompleteError(ret) @defer.inlineCallbacks - def add_oob_auth(self, stagetype, authdict, clientip): + def add_oob_auth(self, stagetype: str, authdict: Dict[str, Any], clientip: str): """ Adds the result of out-of-band authentication into an existing auth session. Currently used for adding the result of fallback auth. @@ -348,7 +352,7 @@ class AuthHandler(BaseHandler): return True return False - def get_session_id(self, clientdict): + def get_session_id(self, clientdict: Dict[str, Any]) -> Optional[str]: """ Gets the session ID for a client given the client dictionary @@ -356,7 +360,7 @@ class AuthHandler(BaseHandler): clientdict: The dictionary sent by the client in the request Returns: - str|None: The string session ID the client sent. If the client did + The string session ID the client sent. If the client did not send a session ID, returns None. """ sid = None @@ -366,40 +370,42 @@ class AuthHandler(BaseHandler): sid = authdict["session"] return sid - def set_session_data(self, session_id, key, value): + def set_session_data(self, session_id: str, key: str, value: Any) -> None: """ Store a key-value pair into the sessions data associated with this request. This data is stored server-side and cannot be modified by the client. Args: - session_id (string): The ID of this session as returned from check_auth - key (string): The key to store the data under - value (any): The data to store + session_id: The ID of this session as returned from check_auth + key: The key to store the data under + value: The data to store """ sess = self._get_session_info(session_id) sess.setdefault("serverdict", {})[key] = value self._save_session(sess) - def get_session_data(self, session_id, key, default=None): + def get_session_data( + self, session_id: str, key: str, default: Optional[Any] = None + ) -> Any: """ Retrieve data stored with set_session_data Args: - session_id (string): The ID of this session as returned from check_auth - key (string): The key to store the data under - default (any): Value to return if the key has not been set + session_id: The ID of this session as returned from check_auth + key: The key to store the data under + default: Value to return if the key has not been set """ sess = self._get_session_info(session_id) return sess.setdefault("serverdict", {}).get(key, default) @defer.inlineCallbacks - def _check_auth_dict(self, authdict, clientip): + def _check_auth_dict(self, authdict: Dict[str, Any], clientip: str): """Attempt to validate the auth dict provided by a client Args: - authdict (object): auth dict provided by the client - clientip (str): IP address of the client + authdict: auth dict provided by the client + clientip: IP address of the client Returns: Deferred: result of the stage verification. @@ -425,10 +431,10 @@ class AuthHandler(BaseHandler): (canonical_id, callback) = yield self.validate_login(user_id, authdict) return canonical_id - def _get_params_recaptcha(self): + def _get_params_recaptcha(self) -> dict: return {"public_key": self.hs.config.recaptcha_public_key} - def _get_params_terms(self): + def _get_params_terms(self) -> dict: return { "policies": { "privacy_policy": { @@ -445,7 +451,9 @@ class AuthHandler(BaseHandler): } } - def _auth_dict_for_flows(self, flows, session): + def _auth_dict_for_flows( + self, flows: List[List[str]], session: Dict[str, Any] + ) -> Dict[str, Any]: public_flows = [] for f in flows: public_flows.append(f) @@ -455,7 +463,7 @@ class AuthHandler(BaseHandler): LoginType.TERMS: self._get_params_terms, } - params = {} + params = {} # type: Dict[str, Any] for f in public_flows: for stage in f: @@ -468,7 +476,13 @@ class AuthHandler(BaseHandler): "params": params, } - def _get_session_info(self, session_id): + def _get_session_info(self, session_id: Optional[str]) -> dict: + """ + Gets or creates a session given a session ID. + + The session can be used to track data across multiple requests, e.g. for + interactive authentication. + """ if session_id not in self.sessions: session_id = None @@ -481,7 +495,9 @@ class AuthHandler(BaseHandler): return self.sessions[session_id] @defer.inlineCallbacks - def get_access_token_for_user_id(self, user_id, device_id, valid_until_ms): + def get_access_token_for_user_id( + self, user_id: str, device_id: Optional[str], valid_until_ms: Optional[int] + ): """ Creates a new access token for the user with the given user ID. @@ -491,11 +507,11 @@ class AuthHandler(BaseHandler): The device will be recorded in the table if it is not there already. Args: - user_id (str): canonical User ID - device_id (str|None): the device ID to associate with the tokens. + user_id: canonical User ID + device_id: the device ID to associate with the tokens. None to leave the tokens unassociated with a device (deprecated: we should always have a device ID) - valid_until_ms (int|None): when the token is valid until. None for + valid_until_ms: when the token is valid until. None for no expiry. Returns: The access token for the user's session. @@ -530,13 +546,13 @@ class AuthHandler(BaseHandler): return access_token @defer.inlineCallbacks - def check_user_exists(self, user_id): + def check_user_exists(self, user_id: str): """ Checks to see if a user with the given id exists. Will check case insensitively, but return None if there are multiple inexact matches. Args: - (unicode|bytes) user_id: complete @user:id + user_id: complete @user:id Returns: defer.Deferred: (unicode) canonical_user_id, or None if zero or @@ -551,7 +567,7 @@ class AuthHandler(BaseHandler): return None @defer.inlineCallbacks - def _find_user_id_and_pwd_hash(self, user_id): + def _find_user_id_and_pwd_hash(self, user_id: str): """Checks to see if a user with the given id exists. Will check case insensitively, but will return None if there are multiple inexact matches. @@ -581,7 +597,7 @@ class AuthHandler(BaseHandler): ) return result - def get_supported_login_types(self): + def get_supported_login_types(self) -> Iterable[str]: """Get a the login types supported for the /login API By default this is just 'm.login.password' (unless password_enabled is @@ -589,20 +605,20 @@ class AuthHandler(BaseHandler): other login types. Returns: - Iterable[str]: login types + login types """ return self._supported_login_types @defer.inlineCallbacks - def validate_login(self, username, login_submission): + def validate_login(self, username: str, login_submission: Dict[str, Any]): """Authenticates the user for the /login API Also used by the user-interactive auth flow to validate m.login.password auth types. Args: - username (str): username supplied by the user - login_submission (dict): the whole of the login submission + username: username supplied by the user + login_submission: the whole of the login submission (including 'type' and other relevant fields) Returns: Deferred[str, func]: canonical user id, and optional callback @@ -690,13 +706,13 @@ class AuthHandler(BaseHandler): raise LoginError(403, "Invalid password", errcode=Codes.FORBIDDEN) @defer.inlineCallbacks - def check_password_provider_3pid(self, medium, address, password): + def check_password_provider_3pid(self, medium: str, address: str, password: str): """Check if a password provider is able to validate a thirdparty login Args: - medium (str): The medium of the 3pid (ex. email). - address (str): The address of the 3pid (ex. jdoe@example.com). - password (str): The password of the user. + medium: The medium of the 3pid (ex. email). + address: The address of the 3pid (ex. jdoe@example.com). + password: The password of the user. Returns: Deferred[(str|None, func|None)]: A tuple of `(user_id, @@ -724,15 +740,15 @@ class AuthHandler(BaseHandler): return None, None @defer.inlineCallbacks - def _check_local_password(self, user_id, password): + def _check_local_password(self, user_id: str, password: str): """Authenticate a user against the local password database. user_id is checked case insensitively, but will return None if there are multiple inexact matches. Args: - user_id (unicode): complete @user:id - password (unicode): the provided password + user_id: complete @user:id + password: the provided password Returns: Deferred[unicode] the canonical_user_id, or Deferred[None] if unknown user/bad password @@ -755,7 +771,7 @@ class AuthHandler(BaseHandler): return user_id @defer.inlineCallbacks - def validate_short_term_login_token_and_get_user_id(self, login_token): + def validate_short_term_login_token_and_get_user_id(self, login_token: str): auth_api = self.hs.get_auth() user_id = None try: @@ -769,11 +785,11 @@ class AuthHandler(BaseHandler): return user_id @defer.inlineCallbacks - def delete_access_token(self, access_token): + def delete_access_token(self, access_token: str): """Invalidate a single access token Args: - access_token (str): access token to be deleted + access_token: access token to be deleted Returns: Deferred @@ -798,15 +814,17 @@ class AuthHandler(BaseHandler): @defer.inlineCallbacks def delete_access_tokens_for_user( - self, user_id, except_token_id=None, device_id=None + self, + user_id: str, + except_token_id: Optional[str] = None, + device_id: Optional[str] = None, ): """Invalidate access tokens belonging to a user Args: - user_id (str): ID of user the tokens belong to - except_token_id (str|None): access_token ID which should *not* be - deleted - device_id (str|None): ID of device the tokens are associated with. + user_id: ID of user the tokens belong to + except_token_id: access_token ID which should *not* be deleted + device_id: ID of device the tokens are associated with. If None, tokens associated with any device (or no device) will be deleted Returns: @@ -830,7 +848,7 @@ class AuthHandler(BaseHandler): ) @defer.inlineCallbacks - def add_threepid(self, user_id, medium, address, validated_at): + def add_threepid(self, user_id: str, medium: str, address: str, validated_at: int): # check if medium has a valid value if medium not in ["email", "msisdn"]: raise SynapseError( @@ -856,19 +874,20 @@ class AuthHandler(BaseHandler): ) @defer.inlineCallbacks - def delete_threepid(self, user_id, medium, address, id_server=None): + def delete_threepid( + self, user_id: str, medium: str, address: str, id_server: Optional[str] = None + ): """Attempts to unbind the 3pid on the identity servers and deletes it from the local database. Args: - user_id (str) - medium (str) - address (str) - id_server (str|None): Use the given identity server when unbinding + user_id: ID of user to remove the 3pid from. + medium: The medium of the 3pid being removed: "email" or "msisdn". + address: The 3pid address to remove. + id_server: Use the given identity server when unbinding any threepids. If None then will attempt to unbind using the identity server specified when binding (if known). - Returns: Deferred[bool]: Returns True if successfully unbound the 3pid on the identity server, False if identity server doesn't support the @@ -887,17 +906,18 @@ class AuthHandler(BaseHandler): yield self.store.user_delete_threepid(user_id, medium, address) return result - def _save_session(self, session): + def _save_session(self, session: Dict[str, Any]) -> None: + """Update the last used time on the session to now and add it back to the session store.""" # TODO: Persistent storage logger.debug("Saving session %s", session) session["last_used"] = self.hs.get_clock().time_msec() self.sessions[session["id"]] = session - def hash(self, password): + def hash(self, password: str): """Computes a secure hash of password. Args: - password (unicode): Password to hash. + password: Password to hash. Returns: Deferred(unicode): Hashed password. @@ -914,12 +934,12 @@ class AuthHandler(BaseHandler): return defer_to_thread(self.hs.get_reactor(), _do_hash) - def validate_hash(self, password, stored_hash): + def validate_hash(self, password: str, stored_hash: bytes): """Validates that self.hash(password) == stored_hash. Args: - password (unicode): Password to hash. - stored_hash (bytes): Expected hash value. + password: Password to hash. + stored_hash: Expected hash value. Returns: Deferred(bool): Whether self.hash(password) == stored_hash. @@ -1007,7 +1027,9 @@ class MacaroonGenerator(object): hs = attr.ib() - def generate_access_token(self, user_id, extra_caveats=None): + def generate_access_token( + self, user_id: str, extra_caveats: Optional[List[str]] = None + ) -> str: extra_caveats = extra_caveats or [] macaroon = self._generate_base_macaroon(user_id) macaroon.add_first_party_caveat("type = access") @@ -1020,16 +1042,9 @@ class MacaroonGenerator(object): macaroon.add_first_party_caveat(caveat) return macaroon.serialize() - def generate_short_term_login_token(self, user_id, duration_in_ms=(2 * 60 * 1000)): - """ - - Args: - user_id (unicode): - duration_in_ms (int): - - Returns: - unicode - """ + def generate_short_term_login_token( + self, user_id: str, duration_in_ms: int = (2 * 60 * 1000) + ) -> str: macaroon = self._generate_base_macaroon(user_id) macaroon.add_first_party_caveat("type = login") now = self.hs.get_clock().time_msec() @@ -1037,12 +1052,12 @@ class MacaroonGenerator(object): macaroon.add_first_party_caveat("time < %d" % (expiry,)) return macaroon.serialize() - def generate_delete_pusher_token(self, user_id): + def generate_delete_pusher_token(self, user_id: str) -> str: macaroon = self._generate_base_macaroon(user_id) macaroon.add_first_party_caveat("type = delete_pusher") return macaroon.serialize() - def _generate_base_macaroon(self, user_id): + def _generate_base_macaroon(self, user_id: str) -> pymacaroons.Macaroon: macaroon = pymacaroons.Macaroon( location=self.hs.config.server_name, identifier="key", diff --git a/tox.ini b/tox.ini index 7622aa19f1..8b4c37c2ee 100644 --- a/tox.ini +++ b/tox.ini @@ -185,6 +185,7 @@ commands = mypy \ synapse/federation/federation_client.py \ synapse/federation/sender \ synapse/federation/transport \ + synapse/handlers/auth.py \ synapse/handlers/directory.py \ synapse/handlers/presence.py \ synapse/handlers/sync.py \ From ebfcbbff9c75ee3e3009b04ba5388c33f2d7e8da Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 13 Mar 2020 19:09:22 +0000 Subject: [PATCH 1193/1623] Use innerText instead of innerHTML --- synapse/res/templates/saml_error.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/synapse/res/templates/saml_error.html b/synapse/res/templates/saml_error.html index 223d3a74bc..bfd6449c5d 100644 --- a/synapse/res/templates/saml_error.html +++ b/synapse/res/templates/saml_error.html @@ -37,7 +37,8 @@ // to print one. let errorDesc = new URLSearchParams(searchStr).get("error_description") if (errorDesc) { - document.getElementById("errormsg").innerHTML = ` ("${errorDesc}")`; + + document.getElementById("errormsg").innerText = ` ("${errorDesc}")`; } From beb19cf61a79e4bfb06b4b1fffd51388b64698ca Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Mon, 16 Mar 2020 12:16:30 +0000 Subject: [PATCH 1194/1623] Fix buggy condition in account validity handler (#7074) --- changelog.d/7074.bugfix | 1 + synapse/handlers/account_validity.py | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changelog.d/7074.bugfix diff --git a/changelog.d/7074.bugfix b/changelog.d/7074.bugfix new file mode 100644 index 0000000000..38d7455971 --- /dev/null +++ b/changelog.d/7074.bugfix @@ -0,0 +1 @@ +Fix a bug causing account validity renewal emails to be sent even if the feature is turned off in some cases. diff --git a/synapse/handlers/account_validity.py b/synapse/handlers/account_validity.py index 829f52eca1..590135d19c 100644 --- a/synapse/handlers/account_validity.py +++ b/synapse/handlers/account_validity.py @@ -44,7 +44,11 @@ class AccountValidityHandler(object): self._account_validity = self.hs.config.account_validity - if self._account_validity.renew_by_email_enabled and load_jinja2_templates: + if ( + self._account_validity.enabled + and self._account_validity.renew_by_email_enabled + and load_jinja2_templates + ): # Don't do email-specific configuration if renewal by email is disabled. try: app_name = self.hs.config.email_app_name From 7df04ca0e6c4140f4f30720db0b9b5148a865287 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Mon, 16 Mar 2020 22:31:47 +0000 Subject: [PATCH 1195/1623] Populate the room version from state events (#7070) Fixes #7065 This is basically the same as https://github.com/matrix-org/synapse/pull/6847 except it tries to populate events from `state_events` rather than `current_state_events`, since the latter might have been cleared from the state of some rooms too early, leaving them with a `NULL` room version. --- changelog.d/7070.bugfix | 1 + .../57/rooms_version_column_3.sql.postgres | 39 +++++++++++++++++++ .../57/rooms_version_column_3.sql.sqlite | 23 +++++++++++ 3 files changed, 63 insertions(+) create mode 100644 changelog.d/7070.bugfix create mode 100644 synapse/storage/data_stores/main/schema/delta/57/rooms_version_column_3.sql.postgres create mode 100644 synapse/storage/data_stores/main/schema/delta/57/rooms_version_column_3.sql.sqlite diff --git a/changelog.d/7070.bugfix b/changelog.d/7070.bugfix new file mode 100644 index 0000000000..9031927546 --- /dev/null +++ b/changelog.d/7070.bugfix @@ -0,0 +1 @@ +Repair a data-corruption issue which was introduced in Synapse 1.10, and fixed in Synapse 1.11, and which could cause `/sync` to return with 404 errors about missing events and unknown rooms. diff --git a/synapse/storage/data_stores/main/schema/delta/57/rooms_version_column_3.sql.postgres b/synapse/storage/data_stores/main/schema/delta/57/rooms_version_column_3.sql.postgres new file mode 100644 index 0000000000..92aaadde0d --- /dev/null +++ b/synapse/storage/data_stores/main/schema/delta/57/rooms_version_column_3.sql.postgres @@ -0,0 +1,39 @@ +/* Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +-- When we first added the room_version column to the rooms table, it was populated from +-- the current_state_events table. However, there was an issue causing a background +-- update to clean up the current_state_events table for rooms where the server is no +-- longer participating, before that column could be populated. Therefore, some rooms had +-- a NULL room_version. + +-- The rooms_version_column_2.sql.* delta files were introduced to make the populating +-- synchronous instead of running it in a background update, which fixed this issue. +-- However, all of the instances of Synapse installed or updated in the meantime got +-- their rooms table corrupted with NULL room_versions. + +-- This query fishes out the room versions from the create event using the state_events +-- table instead of the current_state_events one, as the former still have all of the +-- create events. + +UPDATE rooms SET room_version=( + SELECT COALESCE(json::json->'content'->>'room_version','1') + FROM state_events se INNER JOIN event_json ej USING (event_id) + WHERE se.room_id=rooms.room_id AND se.type='m.room.create' AND se.state_key='' + LIMIT 1 +) WHERE rooms.room_version IS NULL; + +-- see also rooms_version_column_3.sql.sqlite which has a copy of the above query, using +-- sqlite syntax for the json extraction. diff --git a/synapse/storage/data_stores/main/schema/delta/57/rooms_version_column_3.sql.sqlite b/synapse/storage/data_stores/main/schema/delta/57/rooms_version_column_3.sql.sqlite new file mode 100644 index 0000000000..e19dab97cb --- /dev/null +++ b/synapse/storage/data_stores/main/schema/delta/57/rooms_version_column_3.sql.sqlite @@ -0,0 +1,23 @@ +/* Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +-- see rooms_version_column_3.sql.postgres for details of what's going on here. + +UPDATE rooms SET room_version=( + SELECT COALESCE(json_extract(ej.json, '$.content.room_version'), '1') + FROM state_events se INNER JOIN event_json ej USING (event_id) + WHERE se.room_id=rooms.room_id AND se.type='m.room.create' AND se.state_key='' + LIMIT 1 +) WHERE rooms.room_version IS NULL; From 6a35046363a6f5d41199256c80eef4ea7e385986 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 17 Mar 2020 11:25:01 +0000 Subject: [PATCH 1196/1623] Revert "Add options to disable setting profile info for prevent changes. (#7053)" This reverts commit 54dd28621b070ca67de9f773fe9a89e1f4dc19da, reversing changes made to 6640460d054e8f4444046a34bdf638921b31c01e. --- changelog.d/7053.feature | 1 - docs/sample_config.yaml | 13 - synapse/config/registration.py | 17 -- synapse/handlers/profile.py | 16 -- synapse/rest/client/v2_alpha/account.py | 16 -- tests/handlers/test_profile.py | 33 +-- tests/rest/client/v2_alpha/test_account.py | 303 --------------------- 7 files changed, 1 insertion(+), 398 deletions(-) delete mode 100644 changelog.d/7053.feature diff --git a/changelog.d/7053.feature b/changelog.d/7053.feature deleted file mode 100644 index 00f47b2a14..0000000000 --- a/changelog.d/7053.feature +++ /dev/null @@ -1 +0,0 @@ -Add options to prevent users from changing their profile or associated 3PIDs. \ No newline at end of file diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 91eff4c8ad..2ff0dd05a2 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -1057,19 +1057,6 @@ account_threepid_delegates: #email: https://example.com # Delegate email sending to example.com #msisdn: http://localhost:8090 # Delegate SMS sending to this local process -# If disabled, don't let users set their own display names/avatars -# (unless they are a server admin) other than for the very first time. -# Useful when provisioning users based on the contents of a 3rd party -# directory and to avoid ambiguities. -# -#enable_set_displayname: true -#enable_set_avatar_url: true - -# If false, stop users from trying to change the 3PIDs associated with -# their accounts. -# -#enable_3pid_changes: true - # Users who register on this homeserver will automatically be joined # to these rooms # diff --git a/synapse/config/registration.py b/synapse/config/registration.py index ee737eb40d..9bb3beedbc 100644 --- a/synapse/config/registration.py +++ b/synapse/config/registration.py @@ -129,10 +129,6 @@ class RegistrationConfig(Config): raise ConfigError("Invalid auto_join_rooms entry %s" % (room_alias,)) self.autocreate_auto_join_rooms = config.get("autocreate_auto_join_rooms", True) - self.enable_set_displayname = config.get("enable_set_displayname", True) - self.enable_set_avatar_url = config.get("enable_set_avatar_url", True) - self.enable_3pid_changes = config.get("enable_3pid_changes", True) - self.disable_msisdn_registration = config.get( "disable_msisdn_registration", False ) @@ -334,19 +330,6 @@ class RegistrationConfig(Config): #email: https://example.com # Delegate email sending to example.com #msisdn: http://localhost:8090 # Delegate SMS sending to this local process - # If disabled, don't let users set their own display names/avatars - # (unless they are a server admin) other than for the very first time. - # Useful when provisioning users based on the contents of a 3rd party - # directory and to avoid ambiguities. - # - #enable_set_displayname: true - #enable_set_avatar_url: true - - # If false, stop users from trying to change the 3PIDs associated with - # their accounts. - # - #enable_3pid_changes: true - # Users who register on this homeserver will automatically be joined # to these rooms # diff --git a/synapse/handlers/profile.py b/synapse/handlers/profile.py index 6aa1c0f5e0..50ce0c585b 100644 --- a/synapse/handlers/profile.py +++ b/synapse/handlers/profile.py @@ -157,15 +157,6 @@ class BaseProfileHandler(BaseHandler): if not by_admin and target_user != requester.user: raise AuthError(400, "Cannot set another user's displayname") - if not by_admin and not self.hs.config.enable_set_displayname: - profile = yield self.store.get_profileinfo(target_user.localpart) - if profile.display_name: - raise SynapseError( - 400, - "Changing display name is disabled on this server", - Codes.FORBIDDEN, - ) - if len(new_displayname) > MAX_DISPLAYNAME_LEN: raise SynapseError( 400, "Displayname is too long (max %i)" % (MAX_DISPLAYNAME_LEN,) @@ -227,13 +218,6 @@ class BaseProfileHandler(BaseHandler): if not by_admin and target_user != requester.user: raise AuthError(400, "Cannot set another user's avatar_url") - if not by_admin and not self.hs.config.enable_set_avatar_url: - profile = yield self.store.get_profileinfo(target_user.localpart) - if profile.avatar_url: - raise SynapseError( - 400, "Changing avatar is disabled on this server", Codes.FORBIDDEN - ) - if len(new_avatar_url) > MAX_AVATAR_URL_LEN: raise SynapseError( 400, "Avatar URL is too long (max %i)" % (MAX_AVATAR_URL_LEN,) diff --git a/synapse/rest/client/v2_alpha/account.py b/synapse/rest/client/v2_alpha/account.py index e40136f2f3..dc837d6c75 100644 --- a/synapse/rest/client/v2_alpha/account.py +++ b/synapse/rest/client/v2_alpha/account.py @@ -599,11 +599,6 @@ class ThreepidRestServlet(RestServlet): return 200, {"threepids": threepids} async def on_POST(self, request): - if not self.hs.config.enable_3pid_changes: - raise SynapseError( - 400, "3PID changes are disabled on this server", Codes.FORBIDDEN - ) - requester = await self.auth.get_user_by_req(request) user_id = requester.user.to_string() body = parse_json_object_from_request(request) @@ -648,11 +643,6 @@ class ThreepidAddRestServlet(RestServlet): @interactive_auth_handler async def on_POST(self, request): - if not self.hs.config.enable_3pid_changes: - raise SynapseError( - 400, "3PID changes are disabled on this server", Codes.FORBIDDEN - ) - requester = await self.auth.get_user_by_req(request) user_id = requester.user.to_string() body = parse_json_object_from_request(request) @@ -748,16 +738,10 @@ class ThreepidDeleteRestServlet(RestServlet): def __init__(self, hs): super(ThreepidDeleteRestServlet, self).__init__() - self.hs = hs self.auth = hs.get_auth() self.auth_handler = hs.get_auth_handler() async def on_POST(self, request): - if not self.hs.config.enable_3pid_changes: - raise SynapseError( - 400, "3PID changes are disabled on this server", Codes.FORBIDDEN - ) - body = parse_json_object_from_request(request) assert_params_in_dict(body, ["medium", "address"]) diff --git a/tests/handlers/test_profile.py b/tests/handlers/test_profile.py index e600b9777b..d60c124eec 100644 --- a/tests/handlers/test_profile.py +++ b/tests/handlers/test_profile.py @@ -19,7 +19,7 @@ from mock import Mock, NonCallableMock from twisted.internet import defer import synapse.types -from synapse.api.errors import AuthError, SynapseError +from synapse.api.errors import AuthError from synapse.handlers.profile import MasterProfileHandler from synapse.types import UserID @@ -70,7 +70,6 @@ class ProfileTestCase(unittest.TestCase): yield self.store.create_profile(self.frank.localpart) self.handler = hs.get_profile_handler() - self.hs = hs @defer.inlineCallbacks def test_get_my_name(self): @@ -91,19 +90,6 @@ class ProfileTestCase(unittest.TestCase): "Frank Jr.", ) - @defer.inlineCallbacks - def test_set_my_name_if_disabled(self): - self.hs.config.enable_set_displayname = False - - # Set first displayname is allowed, if displayname is null - yield self.store.set_profile_displayname(self.frank.localpart, "Frank") - - d = self.handler.set_displayname( - self.frank, synapse.types.create_requester(self.frank), "Frank Jr." - ) - - yield self.assertFailure(d, SynapseError) - @defer.inlineCallbacks def test_set_my_name_noauth(self): d = self.handler.set_displayname( @@ -161,20 +147,3 @@ class ProfileTestCase(unittest.TestCase): (yield self.store.get_profile_avatar_url(self.frank.localpart)), "http://my.server/pic.gif", ) - - @defer.inlineCallbacks - def test_set_my_avatar_if_disabled(self): - self.hs.config.enable_set_avatar_url = False - - # Set first time avatar is allowed, if avatar is null - yield self.store.set_profile_avatar_url( - self.frank.localpart, "http://my.server/me.png" - ) - - d = self.handler.set_avatar_url( - self.frank, - synapse.types.create_requester(self.frank), - "http://my.server/pic.gif", - ) - - yield self.assertFailure(d, SynapseError) diff --git a/tests/rest/client/v2_alpha/test_account.py b/tests/rest/client/v2_alpha/test_account.py index 99cc9163f3..c3facc00eb 100644 --- a/tests/rest/client/v2_alpha/test_account.py +++ b/tests/rest/client/v2_alpha/test_account.py @@ -24,7 +24,6 @@ import pkg_resources import synapse.rest.admin from synapse.api.constants import LoginType, Membership -from synapse.api.errors import Codes from synapse.rest.client.v1 import login, room from synapse.rest.client.v2_alpha import account, register @@ -326,305 +325,3 @@ class DeactivateTestCase(unittest.HomeserverTestCase): ) self.render(request) self.assertEqual(request.code, 200) - - -class ThreepidEmailRestTestCase(unittest.HomeserverTestCase): - - servlets = [ - account.register_servlets, - login.register_servlets, - synapse.rest.admin.register_servlets_for_client_rest_resource, - ] - - def make_homeserver(self, reactor, clock): - config = self.default_config() - - # Email config. - self.email_attempts = [] - - def sendmail(smtphost, from_addr, to_addrs, msg, **kwargs): - self.email_attempts.append(msg) - return - - config["email"] = { - "enable_notifs": False, - "template_dir": os.path.abspath( - pkg_resources.resource_filename("synapse", "res/templates") - ), - "smtp_host": "127.0.0.1", - "smtp_port": 20, - "require_transport_security": False, - "smtp_user": None, - "smtp_pass": None, - "notif_from": "test@example.com", - } - config["public_baseurl"] = "https://example.com" - - self.hs = self.setup_test_homeserver(config=config, sendmail=sendmail) - return self.hs - - def prepare(self, reactor, clock, hs): - self.store = hs.get_datastore() - - self.user_id = self.register_user("kermit", "test") - self.user_id_tok = self.login("kermit", "test") - self.email = "test@example.com" - self.url_3pid = b"account/3pid" - - def test_add_email(self): - """Test add mail to profile - """ - client_secret = "foobar" - session_id = self._request_token(self.email, client_secret) - - self.assertEquals(len(self.email_attempts), 1) - link = self._get_link_from_email() - - self._validate_token(link) - - request, channel = self.make_request( - "POST", - b"/_matrix/client/unstable/account/3pid/add", - { - "client_secret": client_secret, - "sid": session_id, - "auth": { - "type": "m.login.password", - "user": self.user_id, - "password": "test", - }, - }, - access_token=self.user_id_tok, - ) - - self.render(request) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) - - # Get user - request, channel = self.make_request( - "GET", self.url_3pid, access_token=self.user_id_tok, - ) - self.render(request) - - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) - self.assertEqual("email", channel.json_body["threepids"][0]["medium"]) - self.assertEqual(self.email, channel.json_body["threepids"][0]["address"]) - - def test_add_email_if_disabled(self): - """Test add mail to profile if disabled - """ - self.hs.config.enable_3pid_changes = False - - client_secret = "foobar" - session_id = self._request_token(self.email, client_secret) - - self.assertEquals(len(self.email_attempts), 1) - link = self._get_link_from_email() - - self._validate_token(link) - - request, channel = self.make_request( - "POST", - b"/_matrix/client/unstable/account/3pid/add", - { - "client_secret": client_secret, - "sid": session_id, - "auth": { - "type": "m.login.password", - "user": self.user_id, - "password": "test", - }, - }, - access_token=self.user_id_tok, - ) - self.render(request) - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) - self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"]) - - # Get user - request, channel = self.make_request( - "GET", self.url_3pid, access_token=self.user_id_tok, - ) - self.render(request) - - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) - self.assertFalse(channel.json_body["threepids"]) - - def test_delete_email(self): - """Test delete mail from profile - """ - # Add a threepid - self.get_success( - self.store.user_add_threepid( - user_id=self.user_id, - medium="email", - address=self.email, - validated_at=0, - added_at=0, - ) - ) - - request, channel = self.make_request( - "POST", - b"account/3pid/delete", - {"medium": "email", "address": self.email}, - access_token=self.user_id_tok, - ) - self.render(request) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) - - # Get user - request, channel = self.make_request( - "GET", self.url_3pid, access_token=self.user_id_tok, - ) - self.render(request) - - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) - self.assertFalse(channel.json_body["threepids"]) - - def test_delete_email_if_disabled(self): - """Test delete mail from profile if disabled - """ - self.hs.config.enable_3pid_changes = False - - # Add a threepid - self.get_success( - self.store.user_add_threepid( - user_id=self.user_id, - medium="email", - address=self.email, - validated_at=0, - added_at=0, - ) - ) - - request, channel = self.make_request( - "POST", - b"account/3pid/delete", - {"medium": "email", "address": self.email}, - access_token=self.user_id_tok, - ) - self.render(request) - - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) - self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"]) - - # Get user - request, channel = self.make_request( - "GET", self.url_3pid, access_token=self.user_id_tok, - ) - self.render(request) - - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) - self.assertEqual("email", channel.json_body["threepids"][0]["medium"]) - self.assertEqual(self.email, channel.json_body["threepids"][0]["address"]) - - def test_cant_add_email_without_clicking_link(self): - """Test that we do actually need to click the link in the email - """ - client_secret = "foobar" - session_id = self._request_token(self.email, client_secret) - - self.assertEquals(len(self.email_attempts), 1) - - # Attempt to add email without clicking the link - request, channel = self.make_request( - "POST", - b"/_matrix/client/unstable/account/3pid/add", - { - "client_secret": client_secret, - "sid": session_id, - "auth": { - "type": "m.login.password", - "user": self.user_id, - "password": "test", - }, - }, - access_token=self.user_id_tok, - ) - self.render(request) - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) - self.assertEqual(Codes.THREEPID_AUTH_FAILED, channel.json_body["errcode"]) - - # Get user - request, channel = self.make_request( - "GET", self.url_3pid, access_token=self.user_id_tok, - ) - self.render(request) - - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) - self.assertFalse(channel.json_body["threepids"]) - - def test_no_valid_token(self): - """Test that we do actually need to request a token and can't just - make a session up. - """ - client_secret = "foobar" - session_id = "weasle" - - # Attempt to add email without even requesting an email - request, channel = self.make_request( - "POST", - b"/_matrix/client/unstable/account/3pid/add", - { - "client_secret": client_secret, - "sid": session_id, - "auth": { - "type": "m.login.password", - "user": self.user_id, - "password": "test", - }, - }, - access_token=self.user_id_tok, - ) - self.render(request) - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) - self.assertEqual(Codes.THREEPID_AUTH_FAILED, channel.json_body["errcode"]) - - # Get user - request, channel = self.make_request( - "GET", self.url_3pid, access_token=self.user_id_tok, - ) - self.render(request) - - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) - self.assertFalse(channel.json_body["threepids"]) - - def _request_token(self, email, client_secret): - request, channel = self.make_request( - "POST", - b"account/3pid/email/requestToken", - {"client_secret": client_secret, "email": email, "send_attempt": 1}, - ) - self.render(request) - self.assertEquals(200, channel.code, channel.result) - - return channel.json_body["sid"] - - def _validate_token(self, link): - # Remove the host - path = link.replace("https://example.com", "") - - request, channel = self.make_request("GET", path, shorthand=False) - self.render(request) - self.assertEquals(200, channel.code, channel.result) - - def _get_link_from_email(self): - assert self.email_attempts, "No emails have been sent" - - raw_msg = self.email_attempts[-1].decode("UTF-8") - mail = Parser().parsestr(raw_msg) - - text = None - for part in mail.walk(): - if part.get_content_type() == "text/plain": - text = part.get_payload(decode=True).decode("UTF-8") - break - - if not text: - self.fail("Could not find text portion of email to parse") - - match = re.search(r"https://example.com\S+", text) - assert match, "Could not find link in email" - - return match.group(0) From 60724c46b7dc5300243fd97d5a485564b3e00afe Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 17 Mar 2020 07:37:04 -0400 Subject: [PATCH 1197/1623] Remove special casing of `m.room.aliases` events (#7034) --- changelog.d/7034.removal | 1 + synapse/handlers/room.py | 16 +--------- synapse/rest/client/v1/room.py | 12 -------- tests/rest/admin/test_admin.py | 7 +++++ tests/rest/client/v1/test_directory.py | 41 ++++++++++++++++---------- 5 files changed, 35 insertions(+), 42 deletions(-) create mode 100644 changelog.d/7034.removal diff --git a/changelog.d/7034.removal b/changelog.d/7034.removal new file mode 100644 index 0000000000..be8d20e14f --- /dev/null +++ b/changelog.d/7034.removal @@ -0,0 +1 @@ +Remove special handling of aliases events from [MSC2260](https://github.com/matrix-org/matrix-doc/pull/2260) added in v1.10.0rc1. diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 8ee870f0bb..f580ab2e9f 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -292,16 +292,6 @@ class RoomCreationHandler(BaseHandler): except AuthError as e: logger.warning("Unable to update PLs in old room: %s", e) - new_pl_content = copy_power_levels_contents(old_room_pl_state.content) - - # pre-msc2260 rooms may not have the right setting for aliases. If no other - # value is set, set it now. - events_default = new_pl_content.get("events_default", 0) - new_pl_content.setdefault("events", {}).setdefault( - EventTypes.Aliases, events_default - ) - - logger.debug("Setting correct PLs in new room to %s", new_pl_content) yield self.event_creation_handler.create_and_send_nonmember_event( requester, { @@ -309,7 +299,7 @@ class RoomCreationHandler(BaseHandler): "state_key": "", "room_id": new_room_id, "sender": requester.user.to_string(), - "content": new_pl_content, + "content": old_room_pl_state.content, }, ratelimit=False, ) @@ -814,10 +804,6 @@ class RoomCreationHandler(BaseHandler): EventTypes.RoomHistoryVisibility: 100, EventTypes.CanonicalAlias: 50, EventTypes.RoomAvatar: 50, - # MSC2260: Allow everybody to send alias events by default - # This will be reudundant on pre-MSC2260 rooms, since the - # aliases event is special-cased. - EventTypes.Aliases: 0, EventTypes.Tombstone: 100, EventTypes.ServerACL: 100, }, diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index 64f51406fb..bffd43de5f 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -189,12 +189,6 @@ class RoomStateEventRestServlet(TransactionRestServlet): content = parse_json_object_from_request(request) - if event_type == EventTypes.Aliases: - # MSC2260 - raise SynapseError( - 400, "Cannot send m.room.aliases events via /rooms/{room_id}/state" - ) - event_dict = { "type": event_type, "content": content, @@ -242,12 +236,6 @@ class RoomSendEventRestServlet(TransactionRestServlet): requester = await self.auth.get_user_by_req(request, allow_guest=True) content = parse_json_object_from_request(request) - if event_type == EventTypes.Aliases: - # MSC2260 - raise SynapseError( - 400, "Cannot send m.room.aliases events via /rooms/{room_id}/send" - ) - event_dict = { "type": event_type, "content": content, diff --git a/tests/rest/admin/test_admin.py b/tests/rest/admin/test_admin.py index e5984aaad8..0342aed416 100644 --- a/tests/rest/admin/test_admin.py +++ b/tests/rest/admin/test_admin.py @@ -868,6 +868,13 @@ class RoomTestCase(unittest.HomeserverTestCase): self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) # Set this new alias as the canonical alias for this room + self.helper.send_state( + room_id, + "m.room.aliases", + {"aliases": [test_alias]}, + tok=self.admin_user_tok, + state_key="test", + ) self.helper.send_state( room_id, "m.room.canonical_alias", diff --git a/tests/rest/client/v1/test_directory.py b/tests/rest/client/v1/test_directory.py index 914cf54927..633b7dbda0 100644 --- a/tests/rest/client/v1/test_directory.py +++ b/tests/rest/client/v1/test_directory.py @@ -51,30 +51,26 @@ class DirectoryTestCase(unittest.HomeserverTestCase): self.user = self.register_user("user", "test") self.user_tok = self.login("user", "test") - def test_cannot_set_alias_via_state_event(self): - self.ensure_user_joined_room() - url = "/_matrix/client/r0/rooms/%s/state/m.room.aliases/%s" % ( - self.room_id, - self.hs.hostname, - ) - - data = {"aliases": [self.random_alias(5)]} - request_data = json.dumps(data) - - request, channel = self.make_request( - "PUT", url, request_data, access_token=self.user_tok - ) - self.render(request) - self.assertEqual(channel.code, 400, channel.result) + def test_state_event_not_in_room(self): + self.ensure_user_left_room() + self.set_alias_via_state_event(403) def test_directory_endpoint_not_in_room(self): self.ensure_user_left_room() self.set_alias_via_directory(403) + def test_state_event_in_room_too_long(self): + self.ensure_user_joined_room() + self.set_alias_via_state_event(400, alias_length=256) + def test_directory_in_room_too_long(self): self.ensure_user_joined_room() self.set_alias_via_directory(400, alias_length=256) + def test_state_event_in_room(self): + self.ensure_user_joined_room() + self.set_alias_via_state_event(200) + def test_directory_in_room(self): self.ensure_user_joined_room() self.set_alias_via_directory(200) @@ -106,6 +102,21 @@ class DirectoryTestCase(unittest.HomeserverTestCase): self.render(request) self.assertEqual(channel.code, 200, channel.result) + def set_alias_via_state_event(self, expected_code, alias_length=5): + url = "/_matrix/client/r0/rooms/%s/state/m.room.aliases/%s" % ( + self.room_id, + self.hs.hostname, + ) + + data = {"aliases": [self.random_alias(alias_length)]} + request_data = json.dumps(data) + + request, channel = self.make_request( + "PUT", url, request_data, access_token=self.user_tok + ) + self.render(request) + self.assertEqual(channel.code, expected_code, channel.result) + def set_alias_via_directory(self, expected_code, alias_length=5): url = "/_matrix/client/r0/directory/room/%s" % self.random_alias(alias_length) data = {"room_id": self.room_id} From 7581d30e9f939263f9ab07644f269b6e7cd2d226 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 17 Mar 2020 08:04:49 -0400 Subject: [PATCH 1198/1623] Remove unused federation endpoint (`query_auth`) (#7026) --- changelog.d/7026.removal | 1 + synapse/federation/federation_base.py | 82 ------------------------- synapse/federation/federation_client.py | 80 +++++++++++++++++++++++- synapse/federation/federation_server.py | 51 --------------- synapse/federation/transport/server.py | 12 ---- 5 files changed, 80 insertions(+), 146 deletions(-) create mode 100644 changelog.d/7026.removal diff --git a/changelog.d/7026.removal b/changelog.d/7026.removal new file mode 100644 index 0000000000..4c8c563bb0 --- /dev/null +++ b/changelog.d/7026.removal @@ -0,0 +1 @@ +Remove the unused query_auth federation endpoint per MSC2451. diff --git a/synapse/federation/federation_base.py b/synapse/federation/federation_base.py index 190ea1fba1..5c991e5412 100644 --- a/synapse/federation/federation_base.py +++ b/synapse/federation/federation_base.py @@ -39,10 +39,8 @@ from synapse.logging.context import ( LoggingContext, PreserveLoggingContext, make_deferred_yieldable, - preserve_fn, ) from synapse.types import JsonDict, get_domain_from_id -from synapse.util import unwrapFirstError logger = logging.getLogger(__name__) @@ -57,86 +55,6 @@ class FederationBase(object): self.store = hs.get_datastore() self._clock = hs.get_clock() - @defer.inlineCallbacks - def _check_sigs_and_hash_and_fetch( - self, - origin: str, - pdus: List[EventBase], - room_version: str, - outlier: bool = False, - include_none: bool = False, - ): - """Takes a list of PDUs and checks the signatures and hashs of each - one. If a PDU fails its signature check then we check if we have it in - the database and if not then request if from the originating server of - that PDU. - - If a PDU fails its content hash check then it is redacted. - - The given list of PDUs are not modified, instead the function returns - a new list. - - Args: - origin - pdu - room_version - outlier: Whether the events are outliers or not - include_none: Whether to include None in the returned list - for events that have failed their checks - - Returns: - Deferred : A list of PDUs that have valid signatures and hashes. - """ - deferreds = self._check_sigs_and_hashes(room_version, pdus) - - @defer.inlineCallbacks - def handle_check_result(pdu: EventBase, deferred: Deferred): - try: - res = yield make_deferred_yieldable(deferred) - except SynapseError: - res = None - - if not res: - # Check local db. - res = yield self.store.get_event( - pdu.event_id, allow_rejected=True, allow_none=True - ) - - if not res and pdu.origin != origin: - try: - # This should not exist in the base implementation, until - # this is fixed, ignore it for typing. See issue #6997. - res = yield defer.ensureDeferred( - self.get_pdu( # type: ignore - destinations=[pdu.origin], - event_id=pdu.event_id, - room_version=room_version, - outlier=outlier, - timeout=10000, - ) - ) - except SynapseError: - pass - - if not res: - logger.warning( - "Failed to find copy of %s with valid signature", pdu.event_id - ) - - return res - - handle = preserve_fn(handle_check_result) - deferreds2 = [handle(pdu, deferred) for pdu, deferred in zip(pdus, deferreds)] - - valid_pdus = yield make_deferred_yieldable( - defer.gatherResults(deferreds2, consumeErrors=True) - ).addErrback(unwrapFirstError) - - if include_none: - return valid_pdus - else: - return [p for p in valid_pdus if p] - def _check_sigs_and_hash(self, room_version: str, pdu: EventBase) -> Deferred: return make_deferred_yieldable( self._check_sigs_and_hashes(room_version, [pdu])[0] diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index b5538bc07a..8c6b839478 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -33,6 +33,7 @@ from typing import ( from prometheus_client import Counter from twisted.internet import defer +from twisted.internet.defer import Deferred from synapse.api.constants import EventTypes, Membership from synapse.api.errors import ( @@ -51,7 +52,7 @@ from synapse.api.room_versions import ( ) from synapse.events import EventBase, builder from synapse.federation.federation_base import FederationBase, event_from_pdu_json -from synapse.logging.context import make_deferred_yieldable +from synapse.logging.context import make_deferred_yieldable, preserve_fn from synapse.logging.utils import log_function from synapse.types import JsonDict from synapse.util import unwrapFirstError @@ -345,6 +346,83 @@ class FederationClient(FederationBase): return state_event_ids, auth_event_ids + async def _check_sigs_and_hash_and_fetch( + self, + origin: str, + pdus: List[EventBase], + room_version: str, + outlier: bool = False, + include_none: bool = False, + ) -> List[EventBase]: + """Takes a list of PDUs and checks the signatures and hashs of each + one. If a PDU fails its signature check then we check if we have it in + the database and if not then request if from the originating server of + that PDU. + + If a PDU fails its content hash check then it is redacted. + + The given list of PDUs are not modified, instead the function returns + a new list. + + Args: + origin + pdu + room_version + outlier: Whether the events are outliers or not + include_none: Whether to include None in the returned list + for events that have failed their checks + + Returns: + Deferred : A list of PDUs that have valid signatures and hashes. + """ + deferreds = self._check_sigs_and_hashes(room_version, pdus) + + @defer.inlineCallbacks + def handle_check_result(pdu: EventBase, deferred: Deferred): + try: + res = yield make_deferred_yieldable(deferred) + except SynapseError: + res = None + + if not res: + # Check local db. + res = yield self.store.get_event( + pdu.event_id, allow_rejected=True, allow_none=True + ) + + if not res and pdu.origin != origin: + try: + res = yield defer.ensureDeferred( + self.get_pdu( + destinations=[pdu.origin], + event_id=pdu.event_id, + room_version=room_version, # type: ignore + outlier=outlier, + timeout=10000, + ) + ) + except SynapseError: + pass + + if not res: + logger.warning( + "Failed to find copy of %s with valid signature", pdu.event_id + ) + + return res + + handle = preserve_fn(handle_check_result) + deferreds2 = [handle(pdu, deferred) for pdu, deferred in zip(pdus, deferreds)] + + valid_pdus = await make_deferred_yieldable( + defer.gatherResults(deferreds2, consumeErrors=True) + ).addErrback(unwrapFirstError) + + if include_none: + return valid_pdus + else: + return [p for p in valid_pdus if p] + async def get_event_auth(self, destination, room_id, event_id): res = await self.transport_layer.get_event_auth(destination, room_id, event_id) diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index 7f9da49326..275b9c99d7 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -470,57 +470,6 @@ class FederationServer(FederationBase): res = {"auth_chain": [a.get_pdu_json(time_now) for a in auth_pdus]} return 200, res - async def on_query_auth_request(self, origin, content, room_id, event_id): - """ - Content is a dict with keys:: - auth_chain (list): A list of events that give the auth chain. - missing (list): A list of event_ids indicating what the other - side (`origin`) think we're missing. - rejects (dict): A mapping from event_id to a 2-tuple of reason - string and a proof (or None) of why the event was rejected. - The keys of this dict give the list of events the `origin` has - rejected. - - Args: - origin (str) - content (dict) - event_id (str) - - Returns: - Deferred: Results in `dict` with the same format as `content` - """ - with (await self._server_linearizer.queue((origin, room_id))): - origin_host, _ = parse_server_name(origin) - await self.check_server_matches_acl(origin_host, room_id) - - room_version = await self.store.get_room_version(room_id) - - auth_chain = [ - event_from_pdu_json(e, room_version) for e in content["auth_chain"] - ] - - signed_auth = await self._check_sigs_and_hash_and_fetch( - origin, auth_chain, outlier=True, room_version=room_version.identifier - ) - - ret = await self.handler.on_query_auth( - origin, - event_id, - room_id, - signed_auth, - content.get("rejects", []), - content.get("missing", []), - ) - - time_now = self._clock.time_msec() - send_content = { - "auth_chain": [e.get_pdu_json(time_now) for e in ret["auth_chain"]], - "rejects": ret.get("rejects", []), - "missing": ret.get("missing", []), - } - - return 200, send_content - @log_function def on_query_client_keys(self, origin, content): return self.on_query_request("client_keys", content) diff --git a/synapse/federation/transport/server.py b/synapse/federation/transport/server.py index 92a9ae2320..af4595498c 100644 --- a/synapse/federation/transport/server.py +++ b/synapse/federation/transport/server.py @@ -643,17 +643,6 @@ class FederationClientKeysClaimServlet(BaseFederationServlet): return 200, response -class FederationQueryAuthServlet(BaseFederationServlet): - PATH = "/query_auth/(?P[^/]*)/(?P[^/]*)" - - async def on_POST(self, origin, content, query, context, event_id): - new_content = await self.handler.on_query_auth_request( - origin, content, context, event_id - ) - - return 200, new_content - - class FederationGetMissingEventsServlet(BaseFederationServlet): # TODO(paul): Why does this path alone end with "/?" optional? PATH = "/get_missing_events/(?P[^/]*)/?" @@ -1412,7 +1401,6 @@ FEDERATION_SERVLET_CLASSES = ( FederationV2SendLeaveServlet, FederationV1InviteServlet, FederationV2InviteServlet, - FederationQueryAuthServlet, FederationGetMissingEventsServlet, FederationEventAuthServlet, FederationClientKeysQueryServlet, From 5e477c1debfd932ced56ec755204d6ead4ce8ec8 Mon Sep 17 00:00:00 2001 From: The Stranjer <791672+TheStranjer@users.noreply.github.com> Date: Tue, 17 Mar 2020 09:29:09 -0400 Subject: [PATCH 1199/1623] Set charset to utf-8 when adding headers for certain text content types (#7044) Fixes #7043 --- changelog.d/7044.bugfix | 1 + synapse/rest/media/v1/_base.py | 25 ++++++++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 changelog.d/7044.bugfix diff --git a/changelog.d/7044.bugfix b/changelog.d/7044.bugfix new file mode 100644 index 0000000000..790088ddb4 --- /dev/null +++ b/changelog.d/7044.bugfix @@ -0,0 +1 @@ +Fix a bug that renders UTF-8 text files incorrectly when loaded from media. Contributed by @TheStranjer. diff --git a/synapse/rest/media/v1/_base.py b/synapse/rest/media/v1/_base.py index ba28dd089d..503f2bed98 100644 --- a/synapse/rest/media/v1/_base.py +++ b/synapse/rest/media/v1/_base.py @@ -30,6 +30,22 @@ from synapse.util.stringutils import is_ascii logger = logging.getLogger(__name__) +# list all text content types that will have the charset default to UTF-8 when +# none is given +TEXT_CONTENT_TYPES = [ + "text/css", + "text/csv", + "text/html", + "text/calendar", + "text/plain", + "text/javascript", + "application/json", + "application/ld+json", + "application/rtf", + "image/svg+xml", + "text/xml", +] + def parse_media_id(request): try: @@ -96,7 +112,14 @@ def add_file_headers(request, media_type, file_size, upload_name): def _quote(x): return urllib.parse.quote(x.encode("utf-8")) - request.setHeader(b"Content-Type", media_type.encode("UTF-8")) + # Default to a UTF-8 charset for text content types. + # ex, uses UTF-8 for 'text/css' but not 'text/css; charset=UTF-16' + if media_type.lower() in TEXT_CONTENT_TYPES: + content_type = media_type + "; charset=UTF-8" + else: + content_type = media_type + + request.setHeader(b"Content-Type", content_type.encode("UTF-8")) if upload_name: # RFC6266 section 4.1 [1] defines both `filename` and `filename*`. # From 4ce50519cdfe482ef5833488295f0235ad9fe0a9 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 17 Mar 2020 18:08:43 +0000 Subject: [PATCH 1200/1623] Update postgres.md fix broken link --- docs/postgres.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/postgres.md b/docs/postgres.md index e0793ecee8..ca7ef1cf3a 100644 --- a/docs/postgres.md +++ b/docs/postgres.md @@ -72,8 +72,7 @@ underneath the database, or if a different version of the locale is used on any replicas. The safest way to fix the issue is to take a dump and recreate the database with -the correct `COLLATE` and `CTYPE` parameters (as per -[docs/postgres.md](docs/postgres.md)). It is also possible to change the +the correct `COLLATE` and `CTYPE` parameters (as shown above). It is also possible to change the parameters on a live database and run a `REINDEX` on the entire database, however extreme care must be taken to avoid database corruption. From c37db0211e36cd298426ff8811e547b0acd10bf4 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 17 Mar 2020 22:32:25 +0100 Subject: [PATCH 1201/1623] Share SSL contexts for non-federation requests (#7094) Extends #5794 etc to the SimpleHttpClient so that it also applies to non-federation requests. Fixes #7092. --- changelog.d/7094.misc | 1 + synapse/crypto/context_factory.py | 68 ++++++++++++------- synapse/http/client.py | 3 - .../federation/matrix_federation_agent.py | 2 +- synapse/server.py | 6 +- tests/config/test_tls.py | 29 +++++--- .../test_matrix_federation_agent.py | 6 +- 7 files changed, 71 insertions(+), 44 deletions(-) create mode 100644 changelog.d/7094.misc diff --git a/changelog.d/7094.misc b/changelog.d/7094.misc new file mode 100644 index 0000000000..aa093ee3c0 --- /dev/null +++ b/changelog.d/7094.misc @@ -0,0 +1 @@ +Improve performance when making HTTPS requests to sygnal, sydent, etc, by sharing the SSL context object between connections. diff --git a/synapse/crypto/context_factory.py b/synapse/crypto/context_factory.py index e93f0b3705..a5a2a7815d 100644 --- a/synapse/crypto/context_factory.py +++ b/synapse/crypto/context_factory.py @@ -75,7 +75,7 @@ class ServerContextFactory(ContextFactory): @implementer(IPolicyForHTTPS) -class ClientTLSOptionsFactory(object): +class FederationPolicyForHTTPS(object): """Factory for Twisted SSLClientConnectionCreators that are used to make connections to remote servers for federation. @@ -103,15 +103,15 @@ class ClientTLSOptionsFactory(object): # let us do). minTLS = _TLS_VERSION_MAP[config.federation_client_minimum_tls_version] - self._verify_ssl = CertificateOptions( + _verify_ssl = CertificateOptions( trustRoot=trust_root, insecurelyLowerMinimumTo=minTLS ) - self._verify_ssl_context = self._verify_ssl.getContext() - self._verify_ssl_context.set_info_callback(self._context_info_cb) + self._verify_ssl_context = _verify_ssl.getContext() + self._verify_ssl_context.set_info_callback(_context_info_cb) - self._no_verify_ssl = CertificateOptions(insecurelyLowerMinimumTo=minTLS) - self._no_verify_ssl_context = self._no_verify_ssl.getContext() - self._no_verify_ssl_context.set_info_callback(self._context_info_cb) + _no_verify_ssl = CertificateOptions(insecurelyLowerMinimumTo=minTLS) + self._no_verify_ssl_context = _no_verify_ssl.getContext() + self._no_verify_ssl_context.set_info_callback(_context_info_cb) def get_options(self, host: bytes): @@ -136,23 +136,6 @@ class ClientTLSOptionsFactory(object): return SSLClientConnectionCreator(host, ssl_context, should_verify) - @staticmethod - def _context_info_cb(ssl_connection, where, ret): - """The 'information callback' for our openssl context object.""" - # we assume that the app_data on the connection object has been set to - # a TLSMemoryBIOProtocol object. (This is done by SSLClientConnectionCreator) - tls_protocol = ssl_connection.get_app_data() - try: - # ... we further assume that SSLClientConnectionCreator has set the - # '_synapse_tls_verifier' attribute to a ConnectionVerifier object. - tls_protocol._synapse_tls_verifier.verify_context_info_cb( - ssl_connection, where - ) - except: # noqa: E722, taken from the twisted implementation - logger.exception("Error during info_callback") - f = Failure() - tls_protocol.failVerification(f) - def creatorForNetloc(self, hostname, port): """Implements the IPolicyForHTTPS interace so that this can be passed directly to agents. @@ -160,6 +143,43 @@ class ClientTLSOptionsFactory(object): return self.get_options(hostname) +@implementer(IPolicyForHTTPS) +class RegularPolicyForHTTPS(object): + """Factory for Twisted SSLClientConnectionCreators that are used to make connections + to remote servers, for other than federation. + + Always uses the same OpenSSL context object, which uses the default OpenSSL CA + trust root. + """ + + def __init__(self): + trust_root = platformTrust() + self._ssl_context = CertificateOptions(trustRoot=trust_root).getContext() + self._ssl_context.set_info_callback(_context_info_cb) + + def creatorForNetloc(self, hostname, port): + return SSLClientConnectionCreator(hostname, self._ssl_context, True) + + +def _context_info_cb(ssl_connection, where, ret): + """The 'information callback' for our openssl context objects. + + Note: Once this is set as the info callback on a Context object, the Context should + only be used with the SSLClientConnectionCreator. + """ + # we assume that the app_data on the connection object has been set to + # a TLSMemoryBIOProtocol object. (This is done by SSLClientConnectionCreator) + tls_protocol = ssl_connection.get_app_data() + try: + # ... we further assume that SSLClientConnectionCreator has set the + # '_synapse_tls_verifier' attribute to a ConnectionVerifier object. + tls_protocol._synapse_tls_verifier.verify_context_info_cb(ssl_connection, where) + except: # noqa: E722, taken from the twisted implementation + logger.exception("Error during info_callback") + f = Failure() + tls_protocol.failVerification(f) + + @implementer(IOpenSSLClientConnectionCreator) class SSLClientConnectionCreator(object): """Creates openssl connection objects for client connections. diff --git a/synapse/http/client.py b/synapse/http/client.py index d4c285445e..3797545824 100644 --- a/synapse/http/client.py +++ b/synapse/http/client.py @@ -244,9 +244,6 @@ class SimpleHttpClient(object): pool.maxPersistentPerHost = max((100 * CACHE_SIZE_FACTOR, 5)) pool.cachedConnectionTimeout = 2 * 60 - # The default context factory in Twisted 14.0.0 (which we require) is - # BrowserLikePolicyForHTTPS which will do regular cert validation - # 'like a browser' self.agent = ProxyAgent( self.reactor, connectTimeout=15, diff --git a/synapse/http/federation/matrix_federation_agent.py b/synapse/http/federation/matrix_federation_agent.py index 647d26dc56..f5f917f5ae 100644 --- a/synapse/http/federation/matrix_federation_agent.py +++ b/synapse/http/federation/matrix_federation_agent.py @@ -45,7 +45,7 @@ class MatrixFederationAgent(object): Args: reactor (IReactor): twisted reactor to use for underlying requests - tls_client_options_factory (ClientTLSOptionsFactory|None): + tls_client_options_factory (FederationPolicyForHTTPS|None): factory to use for fetching client tls options, or none to disable TLS. _srv_resolver (SrvResolver|None): diff --git a/synapse/server.py b/synapse/server.py index fd2f69e928..1b980371de 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -26,7 +26,6 @@ import logging import os from twisted.mail.smtp import sendmail -from twisted.web.client import BrowserLikePolicyForHTTPS from synapse.api.auth import Auth from synapse.api.filtering import Filtering @@ -35,6 +34,7 @@ from synapse.appservice.api import ApplicationServiceApi from synapse.appservice.scheduler import ApplicationServiceScheduler from synapse.config.homeserver import HomeServerConfig from synapse.crypto import context_factory +from synapse.crypto.context_factory import RegularPolicyForHTTPS from synapse.crypto.keyring import Keyring from synapse.events.builder import EventBuilderFactory from synapse.events.spamcheck import SpamChecker @@ -310,7 +310,7 @@ class HomeServer(object): return ( InsecureInterceptableContextFactory() if self.config.use_insecure_ssl_client_just_for_testing_do_not_use - else BrowserLikePolicyForHTTPS() + else RegularPolicyForHTTPS() ) def build_simple_http_client(self): @@ -420,7 +420,7 @@ class HomeServer(object): return PusherPool(self) def build_http_client(self): - tls_client_options_factory = context_factory.ClientTLSOptionsFactory( + tls_client_options_factory = context_factory.FederationPolicyForHTTPS( self.config ) return MatrixFederationHttpClient(self, tls_client_options_factory) diff --git a/tests/config/test_tls.py b/tests/config/test_tls.py index 1be6ff563b..ec32d4b1ca 100644 --- a/tests/config/test_tls.py +++ b/tests/config/test_tls.py @@ -23,7 +23,7 @@ from OpenSSL import SSL from synapse.config._base import Config, RootConfig from synapse.config.tls import ConfigError, TlsConfig -from synapse.crypto.context_factory import ClientTLSOptionsFactory +from synapse.crypto.context_factory import FederationPolicyForHTTPS from tests.unittest import TestCase @@ -180,12 +180,13 @@ s4niecZKPBizL6aucT59CsunNmmb5Glq8rlAcU+1ZTZZzGYqVYhF6axB9Qg= t = TestConfig() t.read_config(config, config_dir_path="", data_dir_path="") - cf = ClientTLSOptionsFactory(t) + cf = FederationPolicyForHTTPS(t) + options = _get_ssl_context_options(cf._verify_ssl_context) # The context has had NO_TLSv1_1 and NO_TLSv1_0 set, but not NO_TLSv1_2 - self.assertNotEqual(cf._verify_ssl._options & SSL.OP_NO_TLSv1, 0) - self.assertNotEqual(cf._verify_ssl._options & SSL.OP_NO_TLSv1_1, 0) - self.assertEqual(cf._verify_ssl._options & SSL.OP_NO_TLSv1_2, 0) + self.assertNotEqual(options & SSL.OP_NO_TLSv1, 0) + self.assertNotEqual(options & SSL.OP_NO_TLSv1_1, 0) + self.assertEqual(options & SSL.OP_NO_TLSv1_2, 0) def test_tls_client_minimum_set_passed_through_1_0(self): """ @@ -195,12 +196,13 @@ s4niecZKPBizL6aucT59CsunNmmb5Glq8rlAcU+1ZTZZzGYqVYhF6axB9Qg= t = TestConfig() t.read_config(config, config_dir_path="", data_dir_path="") - cf = ClientTLSOptionsFactory(t) + cf = FederationPolicyForHTTPS(t) + options = _get_ssl_context_options(cf._verify_ssl_context) # The context has not had any of the NO_TLS set. - self.assertEqual(cf._verify_ssl._options & SSL.OP_NO_TLSv1, 0) - self.assertEqual(cf._verify_ssl._options & SSL.OP_NO_TLSv1_1, 0) - self.assertEqual(cf._verify_ssl._options & SSL.OP_NO_TLSv1_2, 0) + self.assertEqual(options & SSL.OP_NO_TLSv1, 0) + self.assertEqual(options & SSL.OP_NO_TLSv1_1, 0) + self.assertEqual(options & SSL.OP_NO_TLSv1_2, 0) def test_acme_disabled_in_generated_config_no_acme_domain_provied(self): """ @@ -273,7 +275,7 @@ s4niecZKPBizL6aucT59CsunNmmb5Glq8rlAcU+1ZTZZzGYqVYhF6axB9Qg= t = TestConfig() t.read_config(config, config_dir_path="", data_dir_path="") - cf = ClientTLSOptionsFactory(t) + cf = FederationPolicyForHTTPS(t) # Not in the whitelist opts = cf.get_options(b"notexample.com") @@ -282,3 +284,10 @@ s4niecZKPBizL6aucT59CsunNmmb5Glq8rlAcU+1ZTZZzGYqVYhF6axB9Qg= # Caught by the wildcard opts = cf.get_options(idna.encode("テスト.ドメイン.テスト")) self.assertFalse(opts._verifier._verify_certs) + + +def _get_ssl_context_options(ssl_context: SSL.Context) -> int: + """get the options bits from an openssl context object""" + # the OpenSSL.SSL.Context wrapper doesn't expose get_options, so we have to + # use the low-level interface + return SSL._lib.SSL_CTX_get_options(ssl_context._context) diff --git a/tests/http/federation/test_matrix_federation_agent.py b/tests/http/federation/test_matrix_federation_agent.py index cfcd98ff7d..fdc1d918ff 100644 --- a/tests/http/federation/test_matrix_federation_agent.py +++ b/tests/http/federation/test_matrix_federation_agent.py @@ -31,7 +31,7 @@ from twisted.web.http_headers import Headers from twisted.web.iweb import IPolicyForHTTPS from synapse.config.homeserver import HomeServerConfig -from synapse.crypto.context_factory import ClientTLSOptionsFactory +from synapse.crypto.context_factory import FederationPolicyForHTTPS from synapse.http.federation.matrix_federation_agent import MatrixFederationAgent from synapse.http.federation.srv_resolver import Server from synapse.http.federation.well_known_resolver import ( @@ -79,7 +79,7 @@ class MatrixFederationAgentTests(unittest.TestCase): self._config = config = HomeServerConfig() config.parse_config_dict(config_dict, "", "") - self.tls_factory = ClientTLSOptionsFactory(config) + self.tls_factory = FederationPolicyForHTTPS(config) self.well_known_cache = TTLCache("test_cache", timer=self.reactor.seconds) self.had_well_known_cache = TTLCache("test_cache", timer=self.reactor.seconds) @@ -715,7 +715,7 @@ class MatrixFederationAgentTests(unittest.TestCase): config = default_config("test", parse=True) # Build a new agent and WellKnownResolver with a different tls factory - tls_factory = ClientTLSOptionsFactory(config) + tls_factory = FederationPolicyForHTTPS(config) agent = MatrixFederationAgent( reactor=self.reactor, tls_client_options_factory=tls_factory, From 6d110ddea4b4c300a1d062442da060d021a280cf Mon Sep 17 00:00:00 2001 From: Richard von Kellner Date: Tue, 17 Mar 2020 22:48:23 +0100 Subject: [PATCH 1202/1623] Update INSTALL.md updated CentOS8 install instructions (#6925) --- INSTALL.md | 13 +++++++++++-- changelog.d/6925.doc | 1 + 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 changelog.d/6925.doc diff --git a/INSTALL.md b/INSTALL.md index ffb82bdcc3..c0926ba590 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -124,12 +124,21 @@ sudo pacman -S base-devel python python-pip \ #### CentOS/Fedora -Installing prerequisites on CentOS 7 or Fedora 25: +Installing prerequisites on CentOS 8 or Fedora>26: + +``` +sudo dnf install libtiff-devel libjpeg-devel libzip-devel freetype-devel \ + libwebp-devel tk-devel redhat-rpm-config \ + python3-virtualenv libffi-devel openssl-devel +sudo dnf groupinstall "Development Tools" +``` + +Installing prerequisites on CentOS 7 or Fedora<=25: ``` sudo yum install libtiff-devel libjpeg-devel libzip-devel freetype-devel \ lcms2-devel libwebp-devel tcl-devel tk-devel redhat-rpm-config \ - python-virtualenv libffi-devel openssl-devel + python3-virtualenv libffi-devel openssl-devel sudo yum groupinstall "Development Tools" ``` diff --git a/changelog.d/6925.doc b/changelog.d/6925.doc new file mode 100644 index 0000000000..b8e6c73630 --- /dev/null +++ b/changelog.d/6925.doc @@ -0,0 +1 @@ +Updated CentOS8 install instructions. Contributed by Richard Kellner. From 6e6476ef07c2d72fbea85603f2eb2a61a6866732 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 18 Mar 2020 10:13:55 +0000 Subject: [PATCH 1203/1623] Comments from review --- synapse/app/generic_worker.py | 3 +++ synapse/replication/slave/storage/devices.py | 3 +++ synapse/storage/data_stores/main/devices.py | 27 ++++++++++++++------ 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/synapse/app/generic_worker.py b/synapse/app/generic_worker.py index d596852419..cdc078cf11 100644 --- a/synapse/app/generic_worker.py +++ b/synapse/app/generic_worker.py @@ -775,6 +775,9 @@ class FederationSenderHandler(object): # ... as well as device updates and messages elif stream_name == DeviceListsStream.NAME: + # The entities are either user IDs (starting with '@') whose devices + # have changed, or remote servers that we need to tell about + # changes. hosts = {row.entity for row in rows if not row.entity.startswith("@")} for host in hosts: self.federation_sender.send_device_messages(host) diff --git a/synapse/replication/slave/storage/devices.py b/synapse/replication/slave/storage/devices.py index 01a4f85884..23b1650e41 100644 --- a/synapse/replication/slave/storage/devices.py +++ b/synapse/replication/slave/storage/devices.py @@ -72,6 +72,9 @@ class SlavedDeviceStore(EndToEndKeyWorkerStore, DeviceWorkerStore, BaseSlavedSto def _invalidate_caches_for_devices(self, token, rows): for row in rows: + # The entities are either user IDs (starting with '@') whose devices + # have changed, or remote servers that we need to tell about + # changes. if row.entity.startswith("@"): self._device_list_stream_cache.entity_has_changed(row.entity, token) self.get_cached_devices_for_user.invalidate((row.entity,)) diff --git a/synapse/storage/data_stores/main/devices.py b/synapse/storage/data_stores/main/devices.py index 06e1d9f033..4c19c02bbc 100644 --- a/synapse/storage/data_stores/main/devices.py +++ b/synapse/storage/data_stores/main/devices.py @@ -15,6 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import logging +from typing import List, Tuple from six import iteritems @@ -31,7 +32,7 @@ from synapse.logging.opentracing import ( ) from synapse.metrics.background_process_metrics import run_as_background_process from synapse.storage._base import SQLBaseStore, db_to_json, make_in_list_sql_clause -from synapse.storage.database import Database +from synapse.storage.database import Database, LoggingTransaction from synapse.types import Collection, get_verify_key_from_cross_signing_key from synapse.util.caches.descriptors import ( Cache, @@ -574,10 +575,12 @@ class DeviceWorkerStore(SQLBaseStore): else: return set() - def get_all_device_list_changes_for_remotes(self, from_key, to_key): - """Return a list of `(stream_id, user_id, destination)` which is the - combined list of changes to devices, and which destinations need to be - poked. `destination` may be None if no destinations need to be poked. + async def get_all_device_list_changes_for_remotes( + self, from_key: int, to_key: int + ) -> List[Tuple[int, str]]: + """Return a list of `(stream_id, entity)` which is the combined list of + changes to devices and which destinations need to be poked. Entity is + either a user ID (starting with '@') or a remote destination. """ # This query Does The Right Thing where it'll correctly apply the @@ -591,7 +594,7 @@ class DeviceWorkerStore(SQLBaseStore): WHERE ? < stream_id AND stream_id <= ? """ - return self.db.execute( + return await self.db.execute( "get_all_device_list_changes_for_remotes", None, sql, from_key, to_key ) @@ -1018,11 +1021,19 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): return stream_ids[-1] - def _add_device_change_to_stream_txn(self, txn, user_id, device_ids, stream_ids): + def _add_device_change_to_stream_txn( + self, + txn: LoggingTransaction, + user_id: str, + device_ids: Collection[str], + stream_ids: List[str], + ): txn.call_after( self._device_list_stream_cache.entity_has_changed, user_id, stream_ids[-1], ) + min_stream_id = stream_ids[0] + # Delete older entries in the table, as we really only care about # when the latest change happened. txn.executemany( @@ -1030,7 +1041,7 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): DELETE FROM device_lists_stream WHERE user_id = ? AND device_id = ? AND stream_id < ? """, - [(user_id, device_id, stream_ids[0]) for device_id in device_ids], + [(user_id, device_id, min_stream_id) for device_id in device_ids], ) self.db.simple_insert_many_txn( From 88b41986dbc54e8601ad4d889f4ebff952858b4f Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 18 Mar 2020 07:50:00 -0400 Subject: [PATCH 1204/1623] Add an option to the set password API to choose whether to logout other devices. (#7085) --- changelog.d/7085.feature | 1 + docs/admin_api/user_admin_api.rst | 6 +++- synapse/handlers/set_password.py | 39 +++++++++++++++---------- synapse/rest/admin/users.py | 6 ++-- synapse/rest/client/v2_alpha/account.py | 5 +++- 5 files changed, 38 insertions(+), 19 deletions(-) create mode 100644 changelog.d/7085.feature diff --git a/changelog.d/7085.feature b/changelog.d/7085.feature new file mode 100644 index 0000000000..df6d0f990d --- /dev/null +++ b/changelog.d/7085.feature @@ -0,0 +1 @@ +Add an optional parameter to control whether other sessions are logged out when a user's password is modified. diff --git a/docs/admin_api/user_admin_api.rst b/docs/admin_api/user_admin_api.rst index 6b02d963e6..9ce10119ff 100644 --- a/docs/admin_api/user_admin_api.rst +++ b/docs/admin_api/user_admin_api.rst @@ -38,6 +38,7 @@ The parameter ``threepids`` is optional. The parameter ``avatar_url`` is optional. The parameter ``admin`` is optional and defaults to 'false'. The parameter ``deactivated`` is optional and defaults to 'false'. +The parameter ``password`` is optional. If provided the user's password is updated and all devices are logged out. If the user already exists then optional parameters default to the current value. List Accounts @@ -168,11 +169,14 @@ with a body of: .. code:: json { - "new_password": "" + "new_password": "", + "logout_devices": true, } including an ``access_token`` of a server admin. +The parameter ``new_password`` is required. +The parameter ``logout_devices`` is optional and defaults to ``true``. Get whether a user is a server administrator or not =================================================== diff --git a/synapse/handlers/set_password.py b/synapse/handlers/set_password.py index d90c9e0108..12657ca698 100644 --- a/synapse/handlers/set_password.py +++ b/synapse/handlers/set_password.py @@ -13,10 +13,12 @@ # See the License for the specific language governing permissions and # limitations under the License. import logging +from typing import Optional from twisted.internet import defer from synapse.api.errors import Codes, StoreError, SynapseError +from synapse.types import Requester from ._base import BaseHandler @@ -32,14 +34,17 @@ class SetPasswordHandler(BaseHandler): self._device_handler = hs.get_device_handler() @defer.inlineCallbacks - def set_password(self, user_id, newpassword, requester=None): + def set_password( + self, + user_id: str, + new_password: str, + logout_devices: bool, + requester: Optional[Requester] = None, + ): if not self.hs.config.password_localdb_enabled: raise SynapseError(403, "Password change disabled", errcode=Codes.FORBIDDEN) - password_hash = yield self._auth_handler.hash(newpassword) - - except_device_id = requester.device_id if requester else None - except_access_token_id = requester.access_token_id if requester else None + password_hash = yield self._auth_handler.hash(new_password) try: yield self.store.user_set_password_hash(user_id, password_hash) @@ -48,14 +53,18 @@ class SetPasswordHandler(BaseHandler): raise SynapseError(404, "Unknown user", Codes.NOT_FOUND) raise e - # we want to log out all of the user's other sessions. First delete - # all his other devices. - yield self._device_handler.delete_all_devices_for_user( - user_id, except_device_id=except_device_id - ) + # Optionally, log out all of the user's other sessions. + if logout_devices: + except_device_id = requester.device_id if requester else None + except_access_token_id = requester.access_token_id if requester else None - # and now delete any access tokens which weren't associated with - # devices (or were associated with this device). - yield self._auth_handler.delete_access_tokens_for_user( - user_id, except_token_id=except_access_token_id - ) + # First delete all of their other devices. + yield self._device_handler.delete_all_devices_for_user( + user_id, except_device_id=except_device_id + ) + + # and now delete any access tokens which weren't associated with + # devices (or were associated with this device). + yield self._auth_handler.delete_access_tokens_for_user( + user_id, except_token_id=except_access_token_id + ) diff --git a/synapse/rest/admin/users.py b/synapse/rest/admin/users.py index 80f959248d..8551ac19b8 100644 --- a/synapse/rest/admin/users.py +++ b/synapse/rest/admin/users.py @@ -221,8 +221,9 @@ class UserRestServletV2(RestServlet): raise SynapseError(400, "Invalid password") else: new_password = body["password"] + logout_devices = True await self.set_password_handler.set_password( - target_user.to_string(), new_password, requester + target_user.to_string(), new_password, logout_devices, requester ) if "deactivated" in body: @@ -536,9 +537,10 @@ class ResetPasswordRestServlet(RestServlet): params = parse_json_object_from_request(request) assert_params_in_dict(params, ["new_password"]) new_password = params["new_password"] + logout_devices = params.get("logout_devices", True) await self._set_password_handler.set_password( - target_user_id, new_password, requester + target_user_id, new_password, logout_devices, requester ) return 200, {} diff --git a/synapse/rest/client/v2_alpha/account.py b/synapse/rest/client/v2_alpha/account.py index dc837d6c75..631cc74cb4 100644 --- a/synapse/rest/client/v2_alpha/account.py +++ b/synapse/rest/client/v2_alpha/account.py @@ -265,8 +265,11 @@ class PasswordRestServlet(RestServlet): assert_params_in_dict(params, ["new_password"]) new_password = params["new_password"] + logout_devices = params.get("logout_devices", True) - await self._set_password_handler.set_password(user_id, new_password, requester) + await self._set_password_handler.set_password( + user_id, new_password, logout_devices, requester + ) return 200, {} From 4a17a647a9508b70de35130fd82e3e21474270a9 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 18 Mar 2020 16:46:41 +0000 Subject: [PATCH 1205/1623] Improve get auth chain difference algorithm. (#7095) It was originally implemented by pulling the full auth chain of all state sets out of the database and doing set comparison. However, that can take a lot work if the state and auth chains are large. Instead, lets try and fetch the auth chains at the same time and calculate the difference on the fly, allowing us to bail early if all the auth chains converge. Assuming that the auth chains do converge more often than not, this should improve performance. Hopefully. --- changelog.d/7095.misc | 1 + synapse/state/__init__.py | 28 +--- synapse/state/v2.py | 32 +--- .../data_stores/main/event_federation.py | 150 ++++++++++++++++- tests/state/test_v2.py | 13 +- tests/storage/test_event_federation.py | 157 ++++++++++++++++-- 6 files changed, 310 insertions(+), 71 deletions(-) create mode 100644 changelog.d/7095.misc diff --git a/changelog.d/7095.misc b/changelog.d/7095.misc new file mode 100644 index 0000000000..44fc9f616f --- /dev/null +++ b/changelog.d/7095.misc @@ -0,0 +1 @@ +Attempt to improve performance of state res v2 algorithm. diff --git a/synapse/state/__init__.py b/synapse/state/__init__.py index df7a4f6a89..4afefc6b1d 100644 --- a/synapse/state/__init__.py +++ b/synapse/state/__init__.py @@ -662,28 +662,16 @@ class StateResolutionStore(object): allow_rejected=allow_rejected, ) - def get_auth_chain(self, event_ids: List[str], ignore_events: Set[str]): - """Gets the full auth chain for a set of events (including rejected - events). - - Includes the given event IDs in the result. - - Note that: - 1. All events must be state events. - 2. For v1 rooms this may not have the full auth chain in the - presence of rejected events - - Args: - event_ids: The event IDs of the events to fetch the auth chain for. - Must be state events. - ignore_events: Set of events to exclude from the returned auth - chain. + def get_auth_chain_difference(self, state_sets: List[Set[str]]): + """Given sets of state events figure out the auth chain difference (as + per state res v2 algorithm). + This equivalent to fetching the full auth chain for each set of state + and returning the events that don't appear in each and every auth + chain. Returns: - Deferred[list[str]]: List of event IDs of the auth chain. + Deferred[Set[str]]: Set of event IDs. """ - return self.store.get_auth_chain_ids( - event_ids, include_given=True, ignore_events=ignore_events, - ) + return self.store.get_auth_chain_difference(state_sets) diff --git a/synapse/state/v2.py b/synapse/state/v2.py index 0ffe6d8c14..18484e2fa6 100644 --- a/synapse/state/v2.py +++ b/synapse/state/v2.py @@ -227,36 +227,12 @@ def _get_auth_chain_difference(state_sets, event_map, state_res_store): Returns: Deferred[set[str]]: Set of event IDs """ - common = set(itervalues(state_sets[0])).intersection( - *(itervalues(s) for s in state_sets[1:]) + + difference = yield state_res_store.get_auth_chain_difference( + [set(state_set.values()) for state_set in state_sets] ) - auth_sets = [] - for state_set in state_sets: - auth_ids = { - eid - for key, eid in iteritems(state_set) - if ( - key[0] in (EventTypes.Member, EventTypes.ThirdPartyInvite) - or key - in ( - (EventTypes.PowerLevels, ""), - (EventTypes.Create, ""), - (EventTypes.JoinRules, ""), - ) - ) - and eid not in common - } - - auth_chain = yield state_res_store.get_auth_chain(auth_ids, common) - auth_ids.update(auth_chain) - - auth_sets.append(auth_ids) - - intersection = set(auth_sets[0]).intersection(*auth_sets[1:]) - union = set().union(*auth_sets) - - return union - intersection + return difference def _seperate(state_sets): diff --git a/synapse/storage/data_stores/main/event_federation.py b/synapse/storage/data_stores/main/event_federation.py index 49a7b8b433..62d4e9f599 100644 --- a/synapse/storage/data_stores/main/event_federation.py +++ b/synapse/storage/data_stores/main/event_federation.py @@ -14,7 +14,7 @@ # limitations under the License. import itertools import logging -from typing import List, Optional, Set +from typing import Dict, List, Optional, Set, Tuple from six.moves.queue import Empty, PriorityQueue @@ -103,6 +103,154 @@ class EventFederationWorkerStore(EventsWorkerStore, SignatureWorkerStore, SQLBas return list(results) + def get_auth_chain_difference(self, state_sets: List[Set[str]]): + """Given sets of state events figure out the auth chain difference (as + per state res v2 algorithm). + + This equivalent to fetching the full auth chain for each set of state + and returning the events that don't appear in each and every auth + chain. + + Returns: + Deferred[Set[str]] + """ + + return self.db.runInteraction( + "get_auth_chain_difference", + self._get_auth_chain_difference_txn, + state_sets, + ) + + def _get_auth_chain_difference_txn( + self, txn, state_sets: List[Set[str]] + ) -> Set[str]: + + # Algorithm Description + # ~~~~~~~~~~~~~~~~~~~~~ + # + # The idea here is to basically walk the auth graph of each state set in + # tandem, keeping track of which auth events are reachable by each state + # set. If we reach an auth event we've already visited (via a different + # state set) then we mark that auth event and all ancestors as reachable + # by the state set. This requires that we keep track of the auth chains + # in memory. + # + # Doing it in a such a way means that we can stop early if all auth + # events we're currently walking are reachable by all state sets. + # + # *Note*: We can't stop walking an event's auth chain if it is reachable + # by all state sets. This is because other auth chains we're walking + # might be reachable only via the original auth chain. For example, + # given the following auth chain: + # + # A -> C -> D -> E + # / / + # B -´---------´ + # + # and state sets {A} and {B} then walking the auth chains of A and B + # would immediately show that C is reachable by both. However, if we + # stopped at C then we'd only reach E via the auth chain of B and so E + # would errornously get included in the returned difference. + # + # The other thing that we do is limit the number of auth chains we walk + # at once, due to practical limits (i.e. we can only query the database + # with a limited set of parameters). We pick the auth chains we walk + # each iteration based on their depth, in the hope that events with a + # lower depth are likely reachable by those with higher depths. + # + # We could use any ordering that we believe would give a rough + # topological ordering, e.g. origin server timestamp. If the ordering + # chosen is not topological then the algorithm still produces the right + # result, but perhaps a bit more inefficiently. This is why it is safe + # to use "depth" here. + + initial_events = set(state_sets[0]).union(*state_sets[1:]) + + # Dict from events in auth chains to which sets *cannot* reach them. + # I.e. if the set is empty then all sets can reach the event. + event_to_missing_sets = { + event_id: {i for i, a in enumerate(state_sets) if event_id not in a} + for event_id in initial_events + } + + # We need to get the depth of the initial events for sorting purposes. + sql = """ + SELECT depth, event_id FROM events + WHERE %s + ORDER BY depth ASC + """ + clause, args = make_in_list_sql_clause( + txn.database_engine, "event_id", initial_events + ) + txn.execute(sql % (clause,), args) + + # The sorted list of events whose auth chains we should walk. + search = txn.fetchall() # type: List[Tuple[int, str]] + + # Map from event to its auth events + event_to_auth_events = {} # type: Dict[str, Set[str]] + + base_sql = """ + SELECT a.event_id, auth_id, depth + FROM event_auth AS a + INNER JOIN events AS e ON (e.event_id = a.auth_id) + WHERE + """ + + while search: + # Check whether all our current walks are reachable by all state + # sets. If so we can bail. + if all(not event_to_missing_sets[eid] for _, eid in search): + break + + # Fetch the auth events and their depths of the N last events we're + # currently walking + search, chunk = search[:-100], search[-100:] + clause, args = make_in_list_sql_clause( + txn.database_engine, "a.event_id", [e_id for _, e_id in chunk] + ) + txn.execute(base_sql + clause, args) + + for event_id, auth_event_id, auth_event_depth in txn: + event_to_auth_events.setdefault(event_id, set()).add(auth_event_id) + + sets = event_to_missing_sets.get(auth_event_id) + if sets is None: + # First time we're seeing this event, so we add it to the + # queue of things to fetch. + search.append((auth_event_depth, auth_event_id)) + + # Assume that this event is unreachable from any of the + # state sets until proven otherwise + sets = event_to_missing_sets[auth_event_id] = set( + range(len(state_sets)) + ) + else: + # We've previously seen this event, so look up its auth + # events and recursively mark all ancestors as reachable + # by the current event's state set. + a_ids = event_to_auth_events.get(auth_event_id) + while a_ids: + new_aids = set() + for a_id in a_ids: + event_to_missing_sets[a_id].intersection_update( + event_to_missing_sets[event_id] + ) + + b = event_to_auth_events.get(a_id) + if b: + new_aids.update(b) + + a_ids = new_aids + + # Mark that the auth event is reachable by the approriate sets. + sets.intersection_update(event_to_missing_sets[event_id]) + + search.sort() + + # Return all events where not all sets can reach them. + return {eid for eid, n in event_to_missing_sets.items() if n} + def get_oldest_events_in_room(self, room_id): return self.db.runInteraction( "get_oldest_events_in_room", self._get_oldest_events_in_room_txn, room_id diff --git a/tests/state/test_v2.py b/tests/state/test_v2.py index 5059ade850..a44960203e 100644 --- a/tests/state/test_v2.py +++ b/tests/state/test_v2.py @@ -603,7 +603,7 @@ class TestStateResolutionStore(object): return {eid: self.event_map[eid] for eid in event_ids if eid in self.event_map} - def get_auth_chain(self, event_ids, ignore_events): + def _get_auth_chain(self, event_ids): """Gets the full auth chain for a set of events (including rejected events). @@ -617,9 +617,6 @@ class TestStateResolutionStore(object): Args: event_ids (list): The event IDs of the events to fetch the auth chain for. Must be state events. - ignore_events: Set of events to exclude from the returned auth - chain. - Returns: Deferred[list[str]]: List of event IDs of the auth chain. """ @@ -629,7 +626,7 @@ class TestStateResolutionStore(object): stack = list(event_ids) while stack: event_id = stack.pop() - if event_id in result or event_id in ignore_events: + if event_id in result: continue result.add(event_id) @@ -639,3 +636,9 @@ class TestStateResolutionStore(object): stack.append(aid) return list(result) + + def get_auth_chain_difference(self, auth_sets): + chains = [frozenset(self._get_auth_chain(a)) for a in auth_sets] + + common = set(chains[0]).intersection(*chains[1:]) + return set(chains[0]).union(*chains[1:]) - common diff --git a/tests/storage/test_event_federation.py b/tests/storage/test_event_federation.py index a331517f4d..3aeec0dc0f 100644 --- a/tests/storage/test_event_federation.py +++ b/tests/storage/test_event_federation.py @@ -13,19 +13,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -from twisted.internet import defer - import tests.unittest import tests.utils -class EventFederationWorkerStoreTestCase(tests.unittest.TestCase): - @defer.inlineCallbacks - def setUp(self): - hs = yield tests.utils.setup_test_homeserver(self.addCleanup) +class EventFederationWorkerStoreTestCase(tests.unittest.HomeserverTestCase): + def prepare(self, reactor, clock, hs): self.store = hs.get_datastore() - @defer.inlineCallbacks def test_get_prev_events_for_room(self): room_id = "@ROOM:local" @@ -61,15 +56,14 @@ class EventFederationWorkerStoreTestCase(tests.unittest.TestCase): ) for i in range(0, 20): - yield self.store.db.runInteraction("insert", insert_event, i) + self.get_success(self.store.db.runInteraction("insert", insert_event, i)) # this should get the last ten - r = yield self.store.get_prev_events_for_room(room_id) + r = self.get_success(self.store.get_prev_events_for_room(room_id)) self.assertEqual(10, len(r)) for i in range(0, 10): self.assertEqual("$event_%i:local" % (19 - i), r[i]) - @defer.inlineCallbacks def test_get_rooms_with_many_extremities(self): room1 = "#room1" room2 = "#room2" @@ -86,25 +80,154 @@ class EventFederationWorkerStoreTestCase(tests.unittest.TestCase): ) for i in range(0, 20): - yield self.store.db.runInteraction("insert", insert_event, i, room1) - yield self.store.db.runInteraction("insert", insert_event, i, room2) - yield self.store.db.runInteraction("insert", insert_event, i, room3) + self.get_success( + self.store.db.runInteraction("insert", insert_event, i, room1) + ) + self.get_success( + self.store.db.runInteraction("insert", insert_event, i, room2) + ) + self.get_success( + self.store.db.runInteraction("insert", insert_event, i, room3) + ) # Test simple case - r = yield self.store.get_rooms_with_many_extremities(5, 5, []) + r = self.get_success(self.store.get_rooms_with_many_extremities(5, 5, [])) self.assertEqual(len(r), 3) # Does filter work? - r = yield self.store.get_rooms_with_many_extremities(5, 5, [room1]) + r = self.get_success(self.store.get_rooms_with_many_extremities(5, 5, [room1])) self.assertTrue(room2 in r) self.assertTrue(room3 in r) self.assertEqual(len(r), 2) - r = yield self.store.get_rooms_with_many_extremities(5, 5, [room1, room2]) + r = self.get_success( + self.store.get_rooms_with_many_extremities(5, 5, [room1, room2]) + ) self.assertEqual(r, [room3]) # Does filter and limit work? - r = yield self.store.get_rooms_with_many_extremities(5, 1, [room1]) + r = self.get_success(self.store.get_rooms_with_many_extremities(5, 1, [room1])) self.assertTrue(r == [room2] or r == [room3]) + + def test_auth_difference(self): + room_id = "@ROOM:local" + + # The silly auth graph we use to test the auth difference algorithm, + # where the top are the most recent events. + # + # A B + # \ / + # D E + # \ | + # ` F C + # | /| + # G ´ | + # | \ | + # H I + # | | + # K J + + auth_graph = { + "a": ["e"], + "b": ["e"], + "c": ["g", "i"], + "d": ["f"], + "e": ["f"], + "f": ["g"], + "g": ["h", "i"], + "h": ["k"], + "i": ["j"], + "k": [], + "j": [], + } + + depth_map = { + "a": 7, + "b": 7, + "c": 4, + "d": 6, + "e": 6, + "f": 5, + "g": 3, + "h": 2, + "i": 2, + "k": 1, + "j": 1, + } + + # We rudely fiddle with the appropriate tables directly, as that's much + # easier than constructing events properly. + + def insert_event(txn, event_id, stream_ordering): + + depth = depth_map[event_id] + + self.store.db.simple_insert_txn( + txn, + table="events", + values={ + "event_id": event_id, + "room_id": room_id, + "depth": depth, + "topological_ordering": depth, + "type": "m.test", + "processed": True, + "outlier": False, + "stream_ordering": stream_ordering, + }, + ) + + self.store.db.simple_insert_many_txn( + txn, + table="event_auth", + values=[ + {"event_id": event_id, "room_id": room_id, "auth_id": a} + for a in auth_graph[event_id] + ], + ) + + next_stream_ordering = 0 + for event_id in auth_graph: + next_stream_ordering += 1 + self.get_success( + self.store.db.runInteraction( + "insert", insert_event, event_id, next_stream_ordering + ) + ) + + # Now actually test that various combinations give the right result: + + difference = self.get_success( + self.store.get_auth_chain_difference([{"a"}, {"b"}]) + ) + self.assertSetEqual(difference, {"a", "b"}) + + difference = self.get_success( + self.store.get_auth_chain_difference([{"a"}, {"b"}, {"c"}]) + ) + self.assertSetEqual(difference, {"a", "b", "c", "e", "f"}) + + difference = self.get_success( + self.store.get_auth_chain_difference([{"a", "c"}, {"b"}]) + ) + self.assertSetEqual(difference, {"a", "b", "c"}) + + difference = self.get_success( + self.store.get_auth_chain_difference([{"a"}, {"b"}, {"d"}]) + ) + self.assertSetEqual(difference, {"a", "b", "d", "e"}) + + difference = self.get_success( + self.store.get_auth_chain_difference([{"a"}, {"b"}, {"c"}, {"d"}]) + ) + self.assertSetEqual(difference, {"a", "b", "c", "d", "e", "f"}) + + difference = self.get_success( + self.store.get_auth_chain_difference([{"a"}, {"b"}, {"e"}]) + ) + self.assertSetEqual(difference, {"a", "b"}) + + difference = self.get_success(self.store.get_auth_chain_difference([{"a"}])) + self.assertSetEqual(difference, set()) From 443162e57724c34099215732eda690ea25cb1e4c Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 19 Mar 2020 10:48:45 +0100 Subject: [PATCH 1206/1623] Move pusherpool startup into _base.setup (#7104) This should be safe to do on all workers/masters because it is guarded by a config option which will ensure it is only actually done on the worker assigned as a pusher. --- changelog.d/7104.misc | 1 + synapse/app/_base.py | 1 + synapse/app/homeserver.py | 1 - 3 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/7104.misc diff --git a/changelog.d/7104.misc b/changelog.d/7104.misc new file mode 100644 index 0000000000..ec5c004bbe --- /dev/null +++ b/changelog.d/7104.misc @@ -0,0 +1 @@ +Merge worker apps together. diff --git a/synapse/app/_base.py b/synapse/app/_base.py index 9ffd23c6df..4d84f4595a 100644 --- a/synapse/app/_base.py +++ b/synapse/app/_base.py @@ -276,6 +276,7 @@ def start(hs, listeners=None): # It is now safe to start your Synapse. hs.start_listening(listeners) hs.get_datastore().db.start_profiling() + hs.get_pusherpool().start() setup_sentry(hs) setup_sdnotify(hs) diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py index e0fdddfdc9..f2b56a636f 100644 --- a/synapse/app/homeserver.py +++ b/synapse/app/homeserver.py @@ -408,7 +408,6 @@ def setup(config_options): _base.start(hs, config.listeners) - hs.get_pusherpool().start() hs.get_datastore().db.updates.start_doing_background_updates() except Exception: # Print the exception and bail out. From 8c75667ad7810b4c05e40f7665e724a40aaf4d64 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 19 Mar 2020 11:00:24 +0100 Subject: [PATCH 1207/1623] Add prometheus metrics for the number of active pushers (#7103) --- changelog.d/7103.feature | 1 + synapse/metrics/__init__.py | 12 ++++++---- synapse/metrics/background_process_metrics.py | 5 ++-- synapse/push/pusherpool.py | 24 ++++++++++++++++++- tox.ini | 2 ++ 5 files changed, 36 insertions(+), 8 deletions(-) create mode 100644 changelog.d/7103.feature diff --git a/changelog.d/7103.feature b/changelog.d/7103.feature new file mode 100644 index 0000000000..413e7f29d7 --- /dev/null +++ b/changelog.d/7103.feature @@ -0,0 +1 @@ +Add prometheus metrics for the number of active pushers. diff --git a/synapse/metrics/__init__.py b/synapse/metrics/__init__.py index 0dba997a23..d2fd29acb4 100644 --- a/synapse/metrics/__init__.py +++ b/synapse/metrics/__init__.py @@ -20,7 +20,7 @@ import os import platform import threading import time -from typing import Dict, Union +from typing import Callable, Dict, Iterable, Optional, Tuple, Union import six @@ -59,10 +59,12 @@ class RegistryProxy(object): @attr.s(hash=True) class LaterGauge(object): - name = attr.ib() - desc = attr.ib() - labels = attr.ib(hash=False) - caller = attr.ib() + name = attr.ib(type=str) + desc = attr.ib(type=str) + labels = attr.ib(hash=False, type=Optional[Iterable[str]]) + # callback: should either return a value (if there are no labels for this metric), + # or dict mapping from a label tuple to a value + caller = attr.ib(type=Callable[[], Union[Dict[Tuple[str, ...], float], float]]) def collect(self): diff --git a/synapse/metrics/background_process_metrics.py b/synapse/metrics/background_process_metrics.py index b65bcd8806..8449ef82f7 100644 --- a/synapse/metrics/background_process_metrics.py +++ b/synapse/metrics/background_process_metrics.py @@ -17,6 +17,7 @@ import logging import threading from asyncio import iscoroutine from functools import wraps +from typing import Dict, Set import six @@ -80,13 +81,13 @@ _background_process_db_sched_duration = Counter( # map from description to a counter, so that we can name our logcontexts # incrementally. (It actually duplicates _background_process_start_count, but # it's much simpler to do so than to try to combine them.) -_background_process_counts = {} # type: dict[str, int] +_background_process_counts = {} # type: Dict[str, int] # map from description to the currently running background processes. # # it's kept as a dict of sets rather than a big set so that we can keep track # of process descriptions that no longer have any active processes. -_background_processes = {} # type: dict[str, set[_BackgroundProcess]] +_background_processes = {} # type: Dict[str, Set[_BackgroundProcess]] # A lock that covers the above dicts _bg_metrics_lock = threading.Lock() diff --git a/synapse/push/pusherpool.py b/synapse/push/pusherpool.py index 01789a9fb4..bf721759df 100644 --- a/synapse/push/pusherpool.py +++ b/synapse/push/pusherpool.py @@ -15,11 +15,16 @@ # limitations under the License. import logging +from collections import defaultdict +from typing import Dict, Tuple, Union from twisted.internet import defer +from synapse.metrics import LaterGauge from synapse.metrics.background_process_metrics import run_as_background_process from synapse.push import PusherConfigException +from synapse.push.emailpusher import EmailPusher +from synapse.push.httppusher import HttpPusher from synapse.push.pusher import PusherFactory from synapse.util.async_helpers import concurrently_execute @@ -47,7 +52,24 @@ class PusherPool: self._should_start_pushers = _hs.config.start_pushers self.store = self.hs.get_datastore() self.clock = self.hs.get_clock() - self.pushers = {} + + # map from user id to app_id:pushkey to pusher + self.pushers = {} # type: Dict[str, Dict[str, Union[HttpPusher, EmailPusher]]] + + def count_pushers(): + results = defaultdict(int) # type: Dict[Tuple[str, str], int] + for pushers in self.pushers.values(): + for pusher in pushers.values(): + k = (type(pusher).__name__, pusher.app_id) + results[k] += 1 + return results + + LaterGauge( + name="synapse_pushers", + desc="the number of active pushers", + labels=["kind", "app_id"], + caller=count_pushers, + ) def start(self): """Starts the pushers off in a background process. diff --git a/tox.ini b/tox.ini index 8b4c37c2ee..8e3f09e638 100644 --- a/tox.ini +++ b/tox.ini @@ -191,7 +191,9 @@ commands = mypy \ synapse/handlers/sync.py \ synapse/handlers/ui_auth \ synapse/logging/ \ + synapse/metrics \ synapse/module_api \ + synapse/push/pusherpool.py \ synapse/replication \ synapse/rest \ synapse/spam_checker_api \ From e913823a220b89a205a09efe53116fab435dfdfb Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 19 Mar 2020 11:28:49 +0100 Subject: [PATCH 1208/1623] Fix concurrent modification errors in pusher metrics (#7106) add a lock to try to make this metric actually work --- changelog.d/7106.feature | 1 + synapse/push/pusherpool.py | 27 ++++++++++++++++++--------- 2 files changed, 19 insertions(+), 9 deletions(-) create mode 100644 changelog.d/7106.feature diff --git a/changelog.d/7106.feature b/changelog.d/7106.feature new file mode 100644 index 0000000000..413e7f29d7 --- /dev/null +++ b/changelog.d/7106.feature @@ -0,0 +1 @@ +Add prometheus metrics for the number of active pushers. diff --git a/synapse/push/pusherpool.py b/synapse/push/pusherpool.py index bf721759df..88d203aa44 100644 --- a/synapse/push/pusherpool.py +++ b/synapse/push/pusherpool.py @@ -16,6 +16,7 @@ import logging from collections import defaultdict +from threading import Lock from typing import Dict, Tuple, Union from twisted.internet import defer @@ -56,12 +57,17 @@ class PusherPool: # map from user id to app_id:pushkey to pusher self.pushers = {} # type: Dict[str, Dict[str, Union[HttpPusher, EmailPusher]]] + # a lock for the pushers dict, since `count_pushers` is called from an different + # and we otherwise get concurrent modification errors + self._pushers_lock = Lock() + def count_pushers(): results = defaultdict(int) # type: Dict[Tuple[str, str], int] - for pushers in self.pushers.values(): - for pusher in pushers.values(): - k = (type(pusher).__name__, pusher.app_id) - results[k] += 1 + with self._pushers_lock: + for pushers in self.pushers.values(): + for pusher in pushers.values(): + k = (type(pusher).__name__, pusher.app_id) + results[k] += 1 return results LaterGauge( @@ -293,11 +299,12 @@ class PusherPool: return appid_pushkey = "%s:%s" % (pusherdict["app_id"], pusherdict["pushkey"]) - byuser = self.pushers.setdefault(pusherdict["user_name"], {}) - if appid_pushkey in byuser: - byuser[appid_pushkey].on_stop() - byuser[appid_pushkey] = p + with self._pushers_lock: + byuser = self.pushers.setdefault(pusherdict["user_name"], {}) + if appid_pushkey in byuser: + byuser[appid_pushkey].on_stop() + byuser[appid_pushkey] = p # Check if there *may* be push to process. We do this as this check is a # lot cheaper to do than actually fetching the exact rows we need to @@ -326,7 +333,9 @@ class PusherPool: if appid_pushkey in byuser: logger.info("Stopping pusher %s / %s", user_id, appid_pushkey) byuser[appid_pushkey].on_stop() - del byuser[appid_pushkey] + with self._pushers_lock: + del byuser[appid_pushkey] + yield self.store.delete_pusher_by_app_id_pushkey_user_id( app_id, pushkey, user_id ) From 782b811789b697b822a4daca67c455efeb7a60d4 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 19 Mar 2020 10:45:40 +0000 Subject: [PATCH 1209/1623] update grafana dashboard --- contrib/grafana/synapse.json | 245 ++++++++++++++++++++++++++++++----- 1 file changed, 213 insertions(+), 32 deletions(-) diff --git a/contrib/grafana/synapse.json b/contrib/grafana/synapse.json index 5b1bfd1679..656a442597 100644 --- a/contrib/grafana/synapse.json +++ b/contrib/grafana/synapse.json @@ -18,7 +18,7 @@ "gnetId": null, "graphTooltip": 0, "id": 1, - "iteration": 1561447718159, + "iteration": 1584612489167, "links": [ { "asDropdown": true, @@ -34,6 +34,7 @@ "panels": [ { "collapsed": false, + "datasource": null, "gridPos": { "h": 1, "w": 24, @@ -52,12 +53,14 @@ "dashes": false, "datasource": "$datasource", "fill": 1, + "fillGradient": 0, "gridPos": { "h": 9, "w": 12, "x": 0, "y": 1 }, + "hiddenSeries": false, "id": 75, "legend": { "avg": false, @@ -72,7 +75,9 @@ "linewidth": 1, "links": [], "nullPointMode": "null", - "options": {}, + "options": { + "dataLinks": [] + }, "paceLength": 10, "percentage": false, "pointradius": 5, @@ -151,6 +156,7 @@ "editable": true, "error": false, "fill": 1, + "fillGradient": 0, "grid": {}, "gridPos": { "h": 9, @@ -158,6 +164,7 @@ "x": 12, "y": 1 }, + "hiddenSeries": false, "id": 33, "legend": { "avg": false, @@ -172,7 +179,9 @@ "linewidth": 2, "links": [], "nullPointMode": "null", - "options": {}, + "options": { + "dataLinks": [] + }, "paceLength": 10, "percentage": false, "pointradius": 5, @@ -302,12 +311,14 @@ "dashes": false, "datasource": "$datasource", "fill": 0, + "fillGradient": 0, "gridPos": { "h": 9, "w": 12, "x": 12, "y": 10 }, + "hiddenSeries": false, "id": 107, "legend": { "avg": false, @@ -322,7 +333,9 @@ "linewidth": 1, "links": [], "nullPointMode": "null", - "options": {}, + "options": { + "dataLinks": [] + }, "paceLength": 10, "percentage": false, "pointradius": 5, @@ -425,12 +438,14 @@ "dashes": false, "datasource": "$datasource", "fill": 0, + "fillGradient": 0, "gridPos": { "h": 9, "w": 12, "x": 0, "y": 19 }, + "hiddenSeries": false, "id": 118, "legend": { "avg": false, @@ -445,7 +460,9 @@ "linewidth": 1, "links": [], "nullPointMode": "null", - "options": {}, + "options": { + "dataLinks": [] + }, "paceLength": 10, "percentage": false, "pointradius": 5, @@ -542,6 +559,7 @@ }, { "collapsed": true, + "datasource": null, "gridPos": { "h": 1, "w": 24, @@ -1361,6 +1379,7 @@ }, { "collapsed": true, + "datasource": null, "gridPos": { "h": 1, "w": 24, @@ -1732,6 +1751,7 @@ }, { "collapsed": true, + "datasource": null, "gridPos": { "h": 1, "w": 24, @@ -2439,6 +2459,7 @@ }, { "collapsed": true, + "datasource": null, "gridPos": { "h": 1, "w": 24, @@ -2635,6 +2656,7 @@ }, { "collapsed": true, + "datasource": null, "gridPos": { "h": 1, "w": 24, @@ -2650,11 +2672,12 @@ "dashes": false, "datasource": "$datasource", "fill": 1, + "fillGradient": 0, "gridPos": { "h": 9, "w": 12, "x": 0, - "y": 61 + "y": 33 }, "id": 79, "legend": { @@ -2670,6 +2693,9 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "options": { + "dataLinks": [] + }, "paceLength": 10, "percentage": false, "pointradius": 5, @@ -2684,8 +2710,13 @@ "expr": "sum(rate(synapse_federation_client_sent_transactions{instance=\"$instance\"}[$bucket_size]))", "format": "time_series", "intervalFactor": 1, - "legendFormat": "txn rate", + "legendFormat": "successful txn rate", "refId": "A" + }, + { + "expr": "sum(rate(synapse_util_metrics_block_count{block_name=\"_send_new_transaction\",instance=\"$instance\"}[$bucket_size]) - ignoring (block_name) rate(synapse_federation_client_sent_transactions{instance=\"$instance\"}[$bucket_size]))", + "legendFormat": "failed txn rate", + "refId": "B" } ], "thresholds": [], @@ -2736,11 +2767,12 @@ "dashes": false, "datasource": "$datasource", "fill": 1, + "fillGradient": 0, "gridPos": { "h": 9, "w": 12, "x": 12, - "y": 61 + "y": 33 }, "id": 83, "legend": { @@ -2756,6 +2788,9 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "options": { + "dataLinks": [] + }, "paceLength": 10, "percentage": false, "pointradius": 5, @@ -2829,11 +2864,12 @@ "dashes": false, "datasource": "$datasource", "fill": 1, + "fillGradient": 0, "gridPos": { "h": 9, "w": 12, "x": 0, - "y": 70 + "y": 42 }, "id": 109, "legend": { @@ -2849,6 +2885,9 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "options": { + "dataLinks": [] + }, "paceLength": 10, "percentage": false, "pointradius": 5, @@ -2923,11 +2962,12 @@ "dashes": false, "datasource": "$datasource", "fill": 1, + "fillGradient": 0, "gridPos": { "h": 9, "w": 12, "x": 12, - "y": 70 + "y": 42 }, "id": 111, "legend": { @@ -2943,6 +2983,9 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "options": { + "dataLinks": [] + }, "paceLength": 10, "percentage": false, "pointradius": 5, @@ -3009,6 +3052,7 @@ }, { "collapsed": true, + "datasource": null, "gridPos": { "h": 1, "w": 24, @@ -3024,12 +3068,14 @@ "dashes": false, "datasource": "$datasource", "fill": 1, + "fillGradient": 0, "gridPos": { - "h": 7, + "h": 8, "w": 12, "x": 0, - "y": 62 + "y": 34 }, + "hiddenSeries": false, "id": 51, "legend": { "avg": false, @@ -3044,6 +3090,9 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "options": { + "dataLinks": [] + }, "paceLength": 10, "percentage": false, "pointradius": 5, @@ -3112,6 +3161,95 @@ "align": false, "alignLevel": null } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "$datasource", + "description": "", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 34 + }, + "hiddenSeries": false, + "id": 134, + "legend": { + "avg": false, + "current": false, + "hideZero": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "topk(10,synapse_pushers{job=~\"$job\",index=~\"$index\", instance=\"$instance\"})", + "legendFormat": "{{kind}} {{app_id}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Active pusher instances by app", + "tooltip": { + "shared": false, + "sort": 2, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } } ], "repeat": null, @@ -3120,6 +3258,7 @@ }, { "collapsed": true, + "datasource": null, "gridPos": { "h": 1, "w": 24, @@ -3523,6 +3662,7 @@ }, { "collapsed": true, + "datasource": null, "gridPos": { "h": 1, "w": 24, @@ -3540,6 +3680,7 @@ "editable": true, "error": false, "fill": 1, + "fillGradient": 0, "grid": {}, "gridPos": { "h": 13, @@ -3562,6 +3703,9 @@ "linewidth": 2, "links": [], "nullPointMode": "null", + "options": { + "dataLinks": [] + }, "paceLength": 10, "percentage": false, "pointradius": 5, @@ -3630,6 +3774,7 @@ "editable": true, "error": false, "fill": 1, + "fillGradient": 0, "grid": {}, "gridPos": { "h": 13, @@ -3652,6 +3797,9 @@ "linewidth": 2, "links": [], "nullPointMode": "null", + "options": { + "dataLinks": [] + }, "paceLength": 10, "percentage": false, "pointradius": 5, @@ -3720,6 +3868,7 @@ "editable": true, "error": false, "fill": 1, + "fillGradient": 0, "grid": {}, "gridPos": { "h": 13, @@ -3742,6 +3891,9 @@ "linewidth": 2, "links": [], "nullPointMode": "null", + "options": { + "dataLinks": [] + }, "paceLength": 10, "percentage": false, "pointradius": 5, @@ -3810,6 +3962,7 @@ "editable": true, "error": false, "fill": 1, + "fillGradient": 0, "grid": {}, "gridPos": { "h": 13, @@ -3832,6 +3985,9 @@ "linewidth": 2, "links": [], "nullPointMode": "null", + "options": { + "dataLinks": [] + }, "paceLength": 10, "percentage": false, "pointradius": 5, @@ -3921,6 +4077,7 @@ "linewidth": 2, "links": [], "nullPointMode": "null", + "options": {}, "paceLength": 10, "percentage": false, "pointradius": 5, @@ -4010,6 +4167,7 @@ "linewidth": 2, "links": [], "nullPointMode": "null", + "options": {}, "paceLength": 10, "percentage": false, "pointradius": 5, @@ -4076,6 +4234,7 @@ }, { "collapsed": true, + "datasource": null, "gridPos": { "h": 1, "w": 24, @@ -4540,6 +4699,7 @@ }, { "collapsed": true, + "datasource": null, "gridPos": { "h": 1, "w": 24, @@ -5060,6 +5220,7 @@ }, { "collapsed": true, + "datasource": null, "gridPos": { "h": 1, "w": 24, @@ -5079,7 +5240,7 @@ "h": 7, "w": 12, "x": 0, - "y": 67 + "y": 39 }, "id": 2, "legend": { @@ -5095,6 +5256,7 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "options": {}, "paceLength": 10, "percentage": false, "pointradius": 5, @@ -5198,7 +5360,7 @@ "h": 7, "w": 12, "x": 12, - "y": 67 + "y": 39 }, "id": 41, "legend": { @@ -5214,6 +5376,7 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "options": {}, "paceLength": 10, "percentage": false, "pointradius": 5, @@ -5286,7 +5449,7 @@ "h": 7, "w": 12, "x": 0, - "y": 74 + "y": 46 }, "id": 42, "legend": { @@ -5302,6 +5465,7 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "options": {}, "paceLength": 10, "percentage": false, "pointradius": 5, @@ -5373,7 +5537,7 @@ "h": 7, "w": 12, "x": 12, - "y": 74 + "y": 46 }, "id": 43, "legend": { @@ -5389,6 +5553,7 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "options": {}, "paceLength": 10, "percentage": false, "pointradius": 5, @@ -5460,7 +5625,7 @@ "h": 7, "w": 12, "x": 0, - "y": 81 + "y": 53 }, "id": 113, "legend": { @@ -5476,6 +5641,7 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "options": {}, "paceLength": 10, "percentage": false, "pointradius": 5, @@ -5546,7 +5712,7 @@ "h": 7, "w": 12, "x": 12, - "y": 81 + "y": 53 }, "id": 115, "legend": { @@ -5562,6 +5728,7 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "options": {}, "paceLength": 10, "percentage": false, "pointradius": 5, @@ -5573,7 +5740,7 @@ "steppedLine": false, "targets": [ { - "expr": "rate(synapse_replication_tcp_protocol_close_reason{job=\"$job\",index=~\"$index\",instance=\"$instance\"}[$bucket_size])", + "expr": "rate(synapse_replication_tcp_protocol_close_reason{job=~\"$job\",index=~\"$index\",instance=\"$instance\"}[$bucket_size])", "format": "time_series", "intervalFactor": 1, "legendFormat": "{{job}}-{{index}} {{reason_type}}", @@ -5628,6 +5795,7 @@ }, { "collapsed": true, + "datasource": null, "gridPos": { "h": 1, "w": 24, @@ -5643,11 +5811,12 @@ "dashes": false, "datasource": "$datasource", "fill": 1, + "fillGradient": 0, "gridPos": { "h": 9, "w": 12, "x": 0, - "y": 13 + "y": 40 }, "id": 67, "legend": { @@ -5663,7 +5832,9 @@ "linewidth": 1, "links": [], "nullPointMode": "connected", - "options": {}, + "options": { + "dataLinks": [] + }, "paceLength": 10, "percentage": false, "pointradius": 5, @@ -5679,7 +5850,7 @@ "format": "time_series", "interval": "", "intervalFactor": 1, - "legendFormat": "{{job}}-{{index}} ", + "legendFormat": "{{job}}-{{index}} {{name}}", "refId": "A" } ], @@ -5731,11 +5902,12 @@ "dashes": false, "datasource": "$datasource", "fill": 1, + "fillGradient": 0, "gridPos": { "h": 9, "w": 12, "x": 12, - "y": 13 + "y": 40 }, "id": 71, "legend": { @@ -5751,7 +5923,9 @@ "linewidth": 1, "links": [], "nullPointMode": "connected", - "options": {}, + "options": { + "dataLinks": [] + }, "paceLength": 10, "percentage": false, "pointradius": 5, @@ -5819,11 +5993,12 @@ "dashes": false, "datasource": "$datasource", "fill": 1, + "fillGradient": 0, "gridPos": { "h": 9, "w": 12, "x": 0, - "y": 22 + "y": 49 }, "id": 121, "interval": "", @@ -5840,7 +6015,9 @@ "linewidth": 1, "links": [], "nullPointMode": "connected", - "options": {}, + "options": { + "dataLinks": [] + }, "paceLength": 10, "percentage": false, "pointradius": 5, @@ -5909,6 +6086,7 @@ }, { "collapsed": true, + "datasource": null, "gridPos": { "h": 1, "w": 24, @@ -6607,7 +6785,7 @@ } ], "refresh": "5m", - "schemaVersion": 18, + "schemaVersion": 22, "style": "dark", "tags": [ "matrix" @@ -6616,7 +6794,7 @@ "list": [ { "current": { - "tags": [], + "selected": true, "text": "Prometheus", "value": "Prometheus" }, @@ -6638,6 +6816,7 @@ "auto_count": 100, "auto_min": "30s", "current": { + "selected": false, "text": "auto", "value": "$__auto_interval_bucket_size" }, @@ -6719,9 +6898,9 @@ "allFormat": "regex wildcard", "allValue": "", "current": { - "text": "All", + "text": "synapse", "value": [ - "$__all" + "synapse" ] }, "datasource": "$datasource", @@ -6751,7 +6930,9 @@ "allValue": ".*", "current": { "text": "All", - "value": "$__all" + "value": [ + "$__all" + ] }, "datasource": "$datasource", "definition": "", @@ -6810,5 +6991,5 @@ "timezone": "", "title": "Synapse", "uid": "000000012", - "version": 10 + "version": 19 } \ No newline at end of file From e43e78b985c586133fedd9779eaf19e1a16ad68b Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 19 Mar 2020 11:07:16 +0000 Subject: [PATCH 1210/1623] 1.12.0rc1 --- CHANGES.md | 77 ++++++++++++++++++++++++++++++++++++++++ changelog.d/6309.misc | 1 - changelog.d/6315.feature | 1 - changelog.d/6572.bugfix | 1 - changelog.d/6615.misc | 1 - changelog.d/6874.misc | 1 - changelog.d/6875.misc | 1 - changelog.d/6925.doc | 1 - changelog.d/6941.removal | 1 - changelog.d/6952.misc | 1 - changelog.d/6953.misc | 1 - changelog.d/6954.misc | 1 - changelog.d/6956.misc | 1 - changelog.d/6957.misc | 1 - changelog.d/6962.bugfix | 1 - changelog.d/6964.misc | 1 - changelog.d/6965.feature | 1 - changelog.d/6966.removal | 1 - changelog.d/6967.bugfix | 1 - changelog.d/6968.bugfix | 1 - changelog.d/6970.removal | 1 - changelog.d/6971.feature | 1 - changelog.d/6979.misc | 1 - changelog.d/6982.feature | 1 - changelog.d/6983.misc | 1 - changelog.d/6984.docker | 1 - changelog.d/6985.misc | 1 - changelog.d/6986.feature | 1 - changelog.d/6987.misc | 1 - changelog.d/6990.bugfix | 1 - changelog.d/6991.misc | 1 - changelog.d/6995.misc | 1 - changelog.d/7002.misc | 1 - changelog.d/7003.misc | 1 - changelog.d/7015.misc | 1 - changelog.d/7018.bugfix | 1 - changelog.d/7019.misc | 1 - changelog.d/7020.misc | 1 - changelog.d/7026.removal | 1 - changelog.d/7030.feature | 1 - changelog.d/7034.removal | 1 - changelog.d/7035.bugfix | 1 - changelog.d/7037.feature | 1 - changelog.d/7044.bugfix | 1 - changelog.d/7045.misc | 1 - changelog.d/7048.doc | 1 - changelog.d/7055.misc | 1 - changelog.d/7058.feature | 1 - changelog.d/7063.misc | 1 - changelog.d/7066.bugfix | 1 - changelog.d/7067.feature | 1 - changelog.d/7070.bugfix | 1 - changelog.d/7074.bugfix | 1 - changelog.d/7085.feature | 1 - changelog.d/7094.misc | 1 - changelog.d/7095.misc | 1 - changelog.d/7103.feature | 1 - changelog.d/7104.misc | 1 - changelog.d/7106.feature | 1 - synapse/__init__.py | 2 +- 60 files changed, 78 insertions(+), 59 deletions(-) delete mode 100644 changelog.d/6309.misc delete mode 100644 changelog.d/6315.feature delete mode 100644 changelog.d/6572.bugfix delete mode 100644 changelog.d/6615.misc delete mode 100644 changelog.d/6874.misc delete mode 100644 changelog.d/6875.misc delete mode 100644 changelog.d/6925.doc delete mode 100644 changelog.d/6941.removal delete mode 100644 changelog.d/6952.misc delete mode 100644 changelog.d/6953.misc delete mode 100644 changelog.d/6954.misc delete mode 100644 changelog.d/6956.misc delete mode 100644 changelog.d/6957.misc delete mode 100644 changelog.d/6962.bugfix delete mode 100644 changelog.d/6964.misc delete mode 100644 changelog.d/6965.feature delete mode 100644 changelog.d/6966.removal delete mode 100644 changelog.d/6967.bugfix delete mode 100644 changelog.d/6968.bugfix delete mode 100644 changelog.d/6970.removal delete mode 100644 changelog.d/6971.feature delete mode 100644 changelog.d/6979.misc delete mode 100644 changelog.d/6982.feature delete mode 100644 changelog.d/6983.misc delete mode 100644 changelog.d/6984.docker delete mode 100644 changelog.d/6985.misc delete mode 100644 changelog.d/6986.feature delete mode 100644 changelog.d/6987.misc delete mode 100644 changelog.d/6990.bugfix delete mode 100644 changelog.d/6991.misc delete mode 100644 changelog.d/6995.misc delete mode 100644 changelog.d/7002.misc delete mode 100644 changelog.d/7003.misc delete mode 100644 changelog.d/7015.misc delete mode 100644 changelog.d/7018.bugfix delete mode 100644 changelog.d/7019.misc delete mode 100644 changelog.d/7020.misc delete mode 100644 changelog.d/7026.removal delete mode 100644 changelog.d/7030.feature delete mode 100644 changelog.d/7034.removal delete mode 100644 changelog.d/7035.bugfix delete mode 100644 changelog.d/7037.feature delete mode 100644 changelog.d/7044.bugfix delete mode 100644 changelog.d/7045.misc delete mode 100644 changelog.d/7048.doc delete mode 100644 changelog.d/7055.misc delete mode 100644 changelog.d/7058.feature delete mode 100644 changelog.d/7063.misc delete mode 100644 changelog.d/7066.bugfix delete mode 100644 changelog.d/7067.feature delete mode 100644 changelog.d/7070.bugfix delete mode 100644 changelog.d/7074.bugfix delete mode 100644 changelog.d/7085.feature delete mode 100644 changelog.d/7094.misc delete mode 100644 changelog.d/7095.misc delete mode 100644 changelog.d/7103.feature delete mode 100644 changelog.d/7104.misc delete mode 100644 changelog.d/7106.feature diff --git a/CHANGES.md b/CHANGES.md index dc9ca05ad1..18ffcea4cd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,80 @@ +Synapse 1.12.0rc1 (2020-03-19) +============================== + +Features +-------- + +- Changes related to room alias management ([MSC2432](https://github.com/matrix-org/matrix-doc/pull/2432): + - Publishing/removing a room from the room directory now requires the user to have a power level capable of modifying the canonical alias, instead of the room aliases. ([\#6965](https://github.com/matrix-org/synapse/issues/6965)) + - Validate the alt_aliases property of canonical alias events. ([\#6971](https://github.com/matrix-org/synapse/issues/6971)) + - Users with a power level sufficient to modify the canonical alias of a room can now delete room aliases. ([\#6986](https://github.com/matrix-org/synapse/issues/6986)) + - Implement updated authorization rules and redaction rules for aliases events, from [MSC2261](https://github.com/matrix-org/matrix-doc/pull/2261) and [MSC2432](https://github.com/matrix-org/matrix-doc/pull/2432). ([\#7037](https://github.com/matrix-org/synapse/issues/7037)) + - Stop sending m.room.aliases events during room creation and upgrade. ([\#6941](https://github.com/matrix-org/synapse/issues/6941)) + - Synapse no longer uses room alias events to calculate room names for email notifications. ([\#6966](https://github.com/matrix-org/synapse/issues/6966)) + - The room list endpoint no longer returns a list of aliases. ([\#6970](https://github.com/matrix-org/synapse/issues/6970)) + - Remove special handling of aliases events from [MSC2260](https://github.com/matrix-org/matrix-doc/pull/2260) added in v1.10.0rc1. ([\#7034](https://github.com/matrix-org/synapse/issues/7034)) +- Expose the `synctl`, `hash_password` and `generate_config` commands in the snapcraft package. Contributed by @devec0. ([\#6315](https://github.com/matrix-org/synapse/issues/6315)) +- Check that server_name is correctly set before running database updates. ([\#6982](https://github.com/matrix-org/synapse/issues/6982)) +- Break down monthly active users by `appservice_id` and emit via Prometheus. ([\#7030](https://github.com/matrix-org/synapse/issues/7030)) +- Render a configurable and comprehensible error page if something goes wrong during the SAML2 authentication process. ([\#7058](https://github.com/matrix-org/synapse/issues/7058), [\#7067](https://github.com/matrix-org/synapse/issues/7067)) +- Add an optional parameter to control whether other sessions are logged out when a user's password is modified. ([\#7085](https://github.com/matrix-org/synapse/issues/7085)) +- Add prometheus metrics for the number of active pushers. ([\#7103](https://github.com/matrix-org/synapse/issues/7103), [\#7106](https://github.com/matrix-org/synapse/issues/7106)) +- Improve performance when making HTTPS requests to sygnal, sydent, etc, by sharing the SSL context object between connections. ([\#7094](https://github.com/matrix-org/synapse/issues/7094)) + + +Bugfixes +-------- + +- When a user's profile is updated via the admin API, also generate a displayname/avatar update for that user in each room. ([\#6572](https://github.com/matrix-org/synapse/issues/6572)) +- Fix a couple of bugs in email configuration handling. ([\#6962](https://github.com/matrix-org/synapse/issues/6962)) +- Fix an issue affecting worker-based deployments where replication would stop working, necessitating a full restart, after joining a large room. ([\#6967](https://github.com/matrix-org/synapse/issues/6967)) +- Fix `duplicate key` error which was logged when rejoining a room over federation. ([\#6968](https://github.com/matrix-org/synapse/issues/6968)) +- Prevent user from setting 'deactivated' to anything other than a bool on the v2 PUT /users Admin API. ([\#6990](https://github.com/matrix-org/synapse/issues/6990)) +- Fix py35-old CI by using native tox package. ([\#7018](https://github.com/matrix-org/synapse/issues/7018)) +- Fix a bug causing `org.matrix.dummy_event` to be included in responses from `/sync`. ([\#7035](https://github.com/matrix-org/synapse/issues/7035)) +- Fix a bug that renders UTF-8 text files incorrectly when loaded from media. Contributed by @TheStranjer. ([\#7044](https://github.com/matrix-org/synapse/issues/7044)) +- Fix a bug that would cause Synapse to respond with an error about event visibility if a client tried to request the state of a room at a given token. ([\#7066](https://github.com/matrix-org/synapse/issues/7066)) +- Repair a data-corruption issue which was introduced in Synapse 1.10, and fixed in Synapse 1.11, and which could cause `/sync` to return with 404 errors about missing events and unknown rooms. ([\#7070](https://github.com/matrix-org/synapse/issues/7070)) +- Fix a bug causing account validity renewal emails to be sent even if the feature is turned off in some cases. ([\#7074](https://github.com/matrix-org/synapse/issues/7074)) + + +Improved Documentation +---------------------- + +- Updated CentOS8 install instructions. Contributed by Richard Kellner. ([\#6925](https://github.com/matrix-org/synapse/issues/6925)) +- Fix `POSTGRES_INITDB_ARGS` in the `contrib/docker/docker-compose.yml` example docker-compose configuration. ([\#6984](https://github.com/matrix-org/synapse/issues/6984)) +- Document that the fallback auth endpoints must be routed to the same worker node as the register endpoints. ([\#7048](https://github.com/matrix-org/synapse/issues/7048)) + + +Deprecations and Removals +------------------------- + +- Remove the unused query_auth federation endpoint per MSC2451. ([\#7026](https://github.com/matrix-org/synapse/issues/7026)) + + +Internal Changes +---------------- + +- Add type hints to `logging/context.py`. ([\#6309](https://github.com/matrix-org/synapse/issues/6309)) +- Add some clarifications to `README.md` in the database schema directory. ([\#6615](https://github.com/matrix-org/synapse/issues/6615)) +- Refactoring work in preparation for changing the event redaction algorithm. ([\#6874](https://github.com/matrix-org/synapse/issues/6874), [\#6875](https://github.com/matrix-org/synapse/issues/6875), [\#6983](https://github.com/matrix-org/synapse/issues/6983), [\#7003](https://github.com/matrix-org/synapse/issues/7003)) +- Improve performance of v2 state resolution for large rooms. ([\#6952](https://github.com/matrix-org/synapse/issues/6952), [\#7095](https://github.com/matrix-org/synapse/issues/7095)) +- Reduce time spent doing GC, by freezing objects on startup. ([\#6953](https://github.com/matrix-org/synapse/issues/6953)) +- Minor perfermance fixes to `get_auth_chain_ids`. ([\#6954](https://github.com/matrix-org/synapse/issues/6954)) +- Don't record remote cross-signing keys in the `devices` table. ([\#6956](https://github.com/matrix-org/synapse/issues/6956)) +- Use flake8-comprehensions to enforce good hygiene of list/set/dict comprehensions. ([\#6957](https://github.com/matrix-org/synapse/issues/6957)) +- Merge worker apps together. ([\#6964](https://github.com/matrix-org/synapse/issues/6964), [\#7002](https://github.com/matrix-org/synapse/issues/7002), [\#7055](https://github.com/matrix-org/synapse/issues/7055), [\#7104](https://github.com/matrix-org/synapse/issues/7104)) +- Remove redundant `store_room` call from `FederationHandler._process_received_pdu`. ([\#6979](https://github.com/matrix-org/synapse/issues/6979)) +- Update warning for incorrect database collation/ctype to include link to documentation. ([\#6985](https://github.com/matrix-org/synapse/issues/6985)) +- Add some type annotations to the database storage classes. ([\#6987](https://github.com/matrix-org/synapse/issues/6987)) +- Port `synapse.handlers.presence` to async/await. ([\#6991](https://github.com/matrix-org/synapse/issues/6991), [\#7019](https://github.com/matrix-org/synapse/issues/7019)) +- Add some type annotations to the federation base & client classes. ([\#6995](https://github.com/matrix-org/synapse/issues/6995)) +- Change date in [INSTALL.md#tls-certificates] for last date of getting TLS certificates to November 2019. ([\#7015](https://github.com/matrix-org/synapse/issues/7015)) +- Port `synapse.rest.keys` to async/await. ([\#7020](https://github.com/matrix-org/synapse/issues/7020)) +- Add a type check to `is_verified` when processing room keys. ([\#7045](https://github.com/matrix-org/synapse/issues/7045)) +- Add type annotations and comments to the auth handler. ([\#7063](https://github.com/matrix-org/synapse/issues/7063)) + + Synapse 1.11.1 (2020-03-03) =========================== diff --git a/changelog.d/6309.misc b/changelog.d/6309.misc deleted file mode 100644 index 1aa7294617..0000000000 --- a/changelog.d/6309.misc +++ /dev/null @@ -1 +0,0 @@ -Add type hints to `logging/context.py`. diff --git a/changelog.d/6315.feature b/changelog.d/6315.feature deleted file mode 100644 index c5377dd1e9..0000000000 --- a/changelog.d/6315.feature +++ /dev/null @@ -1 +0,0 @@ -Expose the `synctl`, `hash_password` and `generate_config` commands in the snapcraft package. Contributed by @devec0. diff --git a/changelog.d/6572.bugfix b/changelog.d/6572.bugfix deleted file mode 100644 index 4f708f409f..0000000000 --- a/changelog.d/6572.bugfix +++ /dev/null @@ -1 +0,0 @@ -When a user's profile is updated via the admin API, also generate a displayname/avatar update for that user in each room. diff --git a/changelog.d/6615.misc b/changelog.d/6615.misc deleted file mode 100644 index 9f93152565..0000000000 --- a/changelog.d/6615.misc +++ /dev/null @@ -1 +0,0 @@ -Add some clarifications to `README.md` in the database schema directory. diff --git a/changelog.d/6874.misc b/changelog.d/6874.misc deleted file mode 100644 index 08aa80bcd9..0000000000 --- a/changelog.d/6874.misc +++ /dev/null @@ -1 +0,0 @@ -Refactoring work in preparation for changing the event redaction algorithm. diff --git a/changelog.d/6875.misc b/changelog.d/6875.misc deleted file mode 100644 index 08aa80bcd9..0000000000 --- a/changelog.d/6875.misc +++ /dev/null @@ -1 +0,0 @@ -Refactoring work in preparation for changing the event redaction algorithm. diff --git a/changelog.d/6925.doc b/changelog.d/6925.doc deleted file mode 100644 index b8e6c73630..0000000000 --- a/changelog.d/6925.doc +++ /dev/null @@ -1 +0,0 @@ -Updated CentOS8 install instructions. Contributed by Richard Kellner. diff --git a/changelog.d/6941.removal b/changelog.d/6941.removal deleted file mode 100644 index 8573be84b3..0000000000 --- a/changelog.d/6941.removal +++ /dev/null @@ -1 +0,0 @@ -Stop sending m.room.aliases events during room creation and upgrade. diff --git a/changelog.d/6952.misc b/changelog.d/6952.misc deleted file mode 100644 index e26dc5cab8..0000000000 --- a/changelog.d/6952.misc +++ /dev/null @@ -1 +0,0 @@ -Improve perf of v2 state res for large rooms. diff --git a/changelog.d/6953.misc b/changelog.d/6953.misc deleted file mode 100644 index 0ab52041cf..0000000000 --- a/changelog.d/6953.misc +++ /dev/null @@ -1 +0,0 @@ -Reduce time spent doing GC by freezing objects on startup. diff --git a/changelog.d/6954.misc b/changelog.d/6954.misc deleted file mode 100644 index 8b84ce2f19..0000000000 --- a/changelog.d/6954.misc +++ /dev/null @@ -1 +0,0 @@ -Minor perf fixes to `get_auth_chain_ids`. diff --git a/changelog.d/6956.misc b/changelog.d/6956.misc deleted file mode 100644 index 5cb0894182..0000000000 --- a/changelog.d/6956.misc +++ /dev/null @@ -1 +0,0 @@ -Don't record remote cross-signing keys in the `devices` table. diff --git a/changelog.d/6957.misc b/changelog.d/6957.misc deleted file mode 100644 index 4f98030110..0000000000 --- a/changelog.d/6957.misc +++ /dev/null @@ -1 +0,0 @@ -Use flake8-comprehensions to enforce good hygiene of list/set/dict comprehensions. diff --git a/changelog.d/6962.bugfix b/changelog.d/6962.bugfix deleted file mode 100644 index 9f5229d400..0000000000 --- a/changelog.d/6962.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a couple of bugs in email configuration handling. diff --git a/changelog.d/6964.misc b/changelog.d/6964.misc deleted file mode 100644 index ec5c004bbe..0000000000 --- a/changelog.d/6964.misc +++ /dev/null @@ -1 +0,0 @@ -Merge worker apps together. diff --git a/changelog.d/6965.feature b/changelog.d/6965.feature deleted file mode 100644 index 6ad9956e40..0000000000 --- a/changelog.d/6965.feature +++ /dev/null @@ -1 +0,0 @@ -Publishing/removing a room from the room directory now requires the user to have a power level capable of modifying the canonical alias, instead of the room aliases. diff --git a/changelog.d/6966.removal b/changelog.d/6966.removal deleted file mode 100644 index 69673d9139..0000000000 --- a/changelog.d/6966.removal +++ /dev/null @@ -1 +0,0 @@ -Synapse no longer uses room alias events to calculate room names for email notifications. diff --git a/changelog.d/6967.bugfix b/changelog.d/6967.bugfix deleted file mode 100644 index b65f80cf1d..0000000000 --- a/changelog.d/6967.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix an issue affecting worker-based deployments where replication would stop working, necessitating a full restart, after joining a large room. diff --git a/changelog.d/6968.bugfix b/changelog.d/6968.bugfix deleted file mode 100644 index 9965bfc0c3..0000000000 --- a/changelog.d/6968.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix `duplicate key` error which was logged when rejoining a room over federation. diff --git a/changelog.d/6970.removal b/changelog.d/6970.removal deleted file mode 100644 index 89bd363b95..0000000000 --- a/changelog.d/6970.removal +++ /dev/null @@ -1 +0,0 @@ -The room list endpoint no longer returns a list of aliases. diff --git a/changelog.d/6971.feature b/changelog.d/6971.feature deleted file mode 100644 index ccf02a61df..0000000000 --- a/changelog.d/6971.feature +++ /dev/null @@ -1 +0,0 @@ -Validate the alt_aliases property of canonical alias events. diff --git a/changelog.d/6979.misc b/changelog.d/6979.misc deleted file mode 100644 index c57b398c2f..0000000000 --- a/changelog.d/6979.misc +++ /dev/null @@ -1 +0,0 @@ -Remove redundant `store_room` call from `FederationHandler._process_received_pdu`. diff --git a/changelog.d/6982.feature b/changelog.d/6982.feature deleted file mode 100644 index 934cc5141a..0000000000 --- a/changelog.d/6982.feature +++ /dev/null @@ -1 +0,0 @@ -Check that server_name is correctly set before running database updates. diff --git a/changelog.d/6983.misc b/changelog.d/6983.misc deleted file mode 100644 index 08aa80bcd9..0000000000 --- a/changelog.d/6983.misc +++ /dev/null @@ -1 +0,0 @@ -Refactoring work in preparation for changing the event redaction algorithm. diff --git a/changelog.d/6984.docker b/changelog.d/6984.docker deleted file mode 100644 index 84a55e1267..0000000000 --- a/changelog.d/6984.docker +++ /dev/null @@ -1 +0,0 @@ -Fix `POSTGRES_INITDB_ARGS` in the `contrib/docker/docker-compose.yml` example docker-compose configuration. diff --git a/changelog.d/6985.misc b/changelog.d/6985.misc deleted file mode 100644 index ba367fa9af..0000000000 --- a/changelog.d/6985.misc +++ /dev/null @@ -1 +0,0 @@ -Update warning for incorrect database collation/ctype to include link to documentation. diff --git a/changelog.d/6986.feature b/changelog.d/6986.feature deleted file mode 100644 index 16dea8bd7f..0000000000 --- a/changelog.d/6986.feature +++ /dev/null @@ -1 +0,0 @@ -Users with a power level sufficient to modify the canonical alias of a room can now delete room aliases. diff --git a/changelog.d/6987.misc b/changelog.d/6987.misc deleted file mode 100644 index 7ff74cda55..0000000000 --- a/changelog.d/6987.misc +++ /dev/null @@ -1 +0,0 @@ -Add some type annotations to the database storage classes. diff --git a/changelog.d/6990.bugfix b/changelog.d/6990.bugfix deleted file mode 100644 index 8c1c48f4d4..0000000000 --- a/changelog.d/6990.bugfix +++ /dev/null @@ -1 +0,0 @@ -Prevent user from setting 'deactivated' to anything other than a bool on the v2 PUT /users Admin API. \ No newline at end of file diff --git a/changelog.d/6991.misc b/changelog.d/6991.misc deleted file mode 100644 index 5130f4e8af..0000000000 --- a/changelog.d/6991.misc +++ /dev/null @@ -1 +0,0 @@ -Port `synapse.handlers.presence` to async/await. diff --git a/changelog.d/6995.misc b/changelog.d/6995.misc deleted file mode 100644 index 884b4cf4ee..0000000000 --- a/changelog.d/6995.misc +++ /dev/null @@ -1 +0,0 @@ -Add some type annotations to the federation base & client classes. diff --git a/changelog.d/7002.misc b/changelog.d/7002.misc deleted file mode 100644 index ec5c004bbe..0000000000 --- a/changelog.d/7002.misc +++ /dev/null @@ -1 +0,0 @@ -Merge worker apps together. diff --git a/changelog.d/7003.misc b/changelog.d/7003.misc deleted file mode 100644 index 08aa80bcd9..0000000000 --- a/changelog.d/7003.misc +++ /dev/null @@ -1 +0,0 @@ -Refactoring work in preparation for changing the event redaction algorithm. diff --git a/changelog.d/7015.misc b/changelog.d/7015.misc deleted file mode 100644 index 9709dc606e..0000000000 --- a/changelog.d/7015.misc +++ /dev/null @@ -1 +0,0 @@ -Change date in INSTALL.md#tls-certificates for last date of getting TLS certificates to November 2019. \ No newline at end of file diff --git a/changelog.d/7018.bugfix b/changelog.d/7018.bugfix deleted file mode 100644 index d1b6c1d464..0000000000 --- a/changelog.d/7018.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix py35-old CI by using native tox package. diff --git a/changelog.d/7019.misc b/changelog.d/7019.misc deleted file mode 100644 index 5130f4e8af..0000000000 --- a/changelog.d/7019.misc +++ /dev/null @@ -1 +0,0 @@ -Port `synapse.handlers.presence` to async/await. diff --git a/changelog.d/7020.misc b/changelog.d/7020.misc deleted file mode 100644 index 188b4378cb..0000000000 --- a/changelog.d/7020.misc +++ /dev/null @@ -1 +0,0 @@ -Port `synapse.rest.keys` to async/await. diff --git a/changelog.d/7026.removal b/changelog.d/7026.removal deleted file mode 100644 index 4c8c563bb0..0000000000 --- a/changelog.d/7026.removal +++ /dev/null @@ -1 +0,0 @@ -Remove the unused query_auth federation endpoint per MSC2451. diff --git a/changelog.d/7030.feature b/changelog.d/7030.feature deleted file mode 100644 index fcfdb8d8a1..0000000000 --- a/changelog.d/7030.feature +++ /dev/null @@ -1 +0,0 @@ -Break down monthly active users by `appservice_id` and emit via Prometheus. diff --git a/changelog.d/7034.removal b/changelog.d/7034.removal deleted file mode 100644 index be8d20e14f..0000000000 --- a/changelog.d/7034.removal +++ /dev/null @@ -1 +0,0 @@ -Remove special handling of aliases events from [MSC2260](https://github.com/matrix-org/matrix-doc/pull/2260) added in v1.10.0rc1. diff --git a/changelog.d/7035.bugfix b/changelog.d/7035.bugfix deleted file mode 100644 index 56292dc8ac..0000000000 --- a/changelog.d/7035.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a bug causing `org.matrix.dummy_event` to be included in responses from `/sync`. diff --git a/changelog.d/7037.feature b/changelog.d/7037.feature deleted file mode 100644 index 4bc1b3b19f..0000000000 --- a/changelog.d/7037.feature +++ /dev/null @@ -1 +0,0 @@ -Implement updated authorization rules and redaction rules for aliases events, from [MSC2261](https://github.com/matrix-org/matrix-doc/pull/2261) and [MSC2432](https://github.com/matrix-org/matrix-doc/pull/2432). diff --git a/changelog.d/7044.bugfix b/changelog.d/7044.bugfix deleted file mode 100644 index 790088ddb4..0000000000 --- a/changelog.d/7044.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a bug that renders UTF-8 text files incorrectly when loaded from media. Contributed by @TheStranjer. diff --git a/changelog.d/7045.misc b/changelog.d/7045.misc deleted file mode 100644 index 74c1abea56..0000000000 --- a/changelog.d/7045.misc +++ /dev/null @@ -1 +0,0 @@ -Add a type check to `is_verified` when processing room keys. diff --git a/changelog.d/7048.doc b/changelog.d/7048.doc deleted file mode 100644 index c9666f333e..0000000000 --- a/changelog.d/7048.doc +++ /dev/null @@ -1 +0,0 @@ -Document that the fallback auth endpoints must be routed to the same worker node as the register endpoints. diff --git a/changelog.d/7055.misc b/changelog.d/7055.misc deleted file mode 100644 index ec5c004bbe..0000000000 --- a/changelog.d/7055.misc +++ /dev/null @@ -1 +0,0 @@ -Merge worker apps together. diff --git a/changelog.d/7058.feature b/changelog.d/7058.feature deleted file mode 100644 index 53ea485e03..0000000000 --- a/changelog.d/7058.feature +++ /dev/null @@ -1 +0,0 @@ -Render a configurable and comprehensible error page if something goes wrong during the SAML2 authentication process. diff --git a/changelog.d/7063.misc b/changelog.d/7063.misc deleted file mode 100644 index e7b1cd3cd8..0000000000 --- a/changelog.d/7063.misc +++ /dev/null @@ -1 +0,0 @@ -Add type annotations and comments to the auth handler. diff --git a/changelog.d/7066.bugfix b/changelog.d/7066.bugfix deleted file mode 100644 index 94bb096287..0000000000 --- a/changelog.d/7066.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a bug that would cause Synapse to respond with an error about event visibility if a client tried to request the state of a room at a given token. diff --git a/changelog.d/7067.feature b/changelog.d/7067.feature deleted file mode 100644 index 53ea485e03..0000000000 --- a/changelog.d/7067.feature +++ /dev/null @@ -1 +0,0 @@ -Render a configurable and comprehensible error page if something goes wrong during the SAML2 authentication process. diff --git a/changelog.d/7070.bugfix b/changelog.d/7070.bugfix deleted file mode 100644 index 9031927546..0000000000 --- a/changelog.d/7070.bugfix +++ /dev/null @@ -1 +0,0 @@ -Repair a data-corruption issue which was introduced in Synapse 1.10, and fixed in Synapse 1.11, and which could cause `/sync` to return with 404 errors about missing events and unknown rooms. diff --git a/changelog.d/7074.bugfix b/changelog.d/7074.bugfix deleted file mode 100644 index 38d7455971..0000000000 --- a/changelog.d/7074.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a bug causing account validity renewal emails to be sent even if the feature is turned off in some cases. diff --git a/changelog.d/7085.feature b/changelog.d/7085.feature deleted file mode 100644 index df6d0f990d..0000000000 --- a/changelog.d/7085.feature +++ /dev/null @@ -1 +0,0 @@ -Add an optional parameter to control whether other sessions are logged out when a user's password is modified. diff --git a/changelog.d/7094.misc b/changelog.d/7094.misc deleted file mode 100644 index aa093ee3c0..0000000000 --- a/changelog.d/7094.misc +++ /dev/null @@ -1 +0,0 @@ -Improve performance when making HTTPS requests to sygnal, sydent, etc, by sharing the SSL context object between connections. diff --git a/changelog.d/7095.misc b/changelog.d/7095.misc deleted file mode 100644 index 44fc9f616f..0000000000 --- a/changelog.d/7095.misc +++ /dev/null @@ -1 +0,0 @@ -Attempt to improve performance of state res v2 algorithm. diff --git a/changelog.d/7103.feature b/changelog.d/7103.feature deleted file mode 100644 index 413e7f29d7..0000000000 --- a/changelog.d/7103.feature +++ /dev/null @@ -1 +0,0 @@ -Add prometheus metrics for the number of active pushers. diff --git a/changelog.d/7104.misc b/changelog.d/7104.misc deleted file mode 100644 index ec5c004bbe..0000000000 --- a/changelog.d/7104.misc +++ /dev/null @@ -1 +0,0 @@ -Merge worker apps together. diff --git a/changelog.d/7106.feature b/changelog.d/7106.feature deleted file mode 100644 index 413e7f29d7..0000000000 --- a/changelog.d/7106.feature +++ /dev/null @@ -1 +0,0 @@ -Add prometheus metrics for the number of active pushers. diff --git a/synapse/__init__.py b/synapse/__init__.py index e56ba89ff4..020e0536be 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -36,7 +36,7 @@ try: except ImportError: pass -__version__ = "1.11.1" +__version__ = "1.12.0rc1" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 5aa6dff99ea7307e3c38d52a9f3dff1f1e2a9630 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 19 Mar 2020 11:15:48 +0000 Subject: [PATCH 1211/1623] fix typo --- CHANGES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 18ffcea4cd..79f24fc643 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,9 +4,9 @@ Synapse 1.12.0rc1 (2020-03-19) Features -------- -- Changes related to room alias management ([MSC2432](https://github.com/matrix-org/matrix-doc/pull/2432): +- Changes related to room alias management ([MSC2432](https://github.com/matrix-org/matrix-doc/pull/2432)): - Publishing/removing a room from the room directory now requires the user to have a power level capable of modifying the canonical alias, instead of the room aliases. ([\#6965](https://github.com/matrix-org/synapse/issues/6965)) - - Validate the alt_aliases property of canonical alias events. ([\#6971](https://github.com/matrix-org/synapse/issues/6971)) + - Validate the `alt_aliases` property of canonical alias events. ([\#6971](https://github.com/matrix-org/synapse/issues/6971)) - Users with a power level sufficient to modify the canonical alias of a room can now delete room aliases. ([\#6986](https://github.com/matrix-org/synapse/issues/6986)) - Implement updated authorization rules and redaction rules for aliases events, from [MSC2261](https://github.com/matrix-org/matrix-doc/pull/2261) and [MSC2432](https://github.com/matrix-org/matrix-doc/pull/2432). ([\#7037](https://github.com/matrix-org/synapse/issues/7037)) - Stop sending m.room.aliases events during room creation and upgrade. ([\#6941](https://github.com/matrix-org/synapse/issues/6941)) From 163f23785adb7d531d8ef2ca9bca161a2dcf76c9 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 19 Mar 2020 11:25:32 +0000 Subject: [PATCH 1212/1623] changelog fixes --- CHANGES.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 79f24fc643..153ace20b8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,7 +10,7 @@ Features - Users with a power level sufficient to modify the canonical alias of a room can now delete room aliases. ([\#6986](https://github.com/matrix-org/synapse/issues/6986)) - Implement updated authorization rules and redaction rules for aliases events, from [MSC2261](https://github.com/matrix-org/matrix-doc/pull/2261) and [MSC2432](https://github.com/matrix-org/matrix-doc/pull/2432). ([\#7037](https://github.com/matrix-org/synapse/issues/7037)) - Stop sending m.room.aliases events during room creation and upgrade. ([\#6941](https://github.com/matrix-org/synapse/issues/6941)) - - Synapse no longer uses room alias events to calculate room names for email notifications. ([\#6966](https://github.com/matrix-org/synapse/issues/6966)) + - Synapse no longer uses room alias events to calculate room names for push notifications. ([\#6966](https://github.com/matrix-org/synapse/issues/6966)) - The room list endpoint no longer returns a list of aliases. ([\#6970](https://github.com/matrix-org/synapse/issues/6970)) - Remove special handling of aliases events from [MSC2260](https://github.com/matrix-org/matrix-doc/pull/2260) added in v1.10.0rc1. ([\#7034](https://github.com/matrix-org/synapse/issues/7034)) - Expose the `synctl`, `hash_password` and `generate_config` commands in the snapcraft package. Contributed by @devec0. ([\#6315](https://github.com/matrix-org/synapse/issues/6315)) @@ -49,7 +49,7 @@ Improved Documentation Deprecations and Removals ------------------------- -- Remove the unused query_auth federation endpoint per MSC2451. ([\#7026](https://github.com/matrix-org/synapse/issues/7026)) +- Remove the unused query_auth federation endpoint per [MSC2451](https://github.com/matrix-org/matrix-doc/pull/2451). ([\#7026](https://github.com/matrix-org/synapse/issues/7026)) Internal Changes @@ -69,7 +69,7 @@ Internal Changes - Add some type annotations to the database storage classes. ([\#6987](https://github.com/matrix-org/synapse/issues/6987)) - Port `synapse.handlers.presence` to async/await. ([\#6991](https://github.com/matrix-org/synapse/issues/6991), [\#7019](https://github.com/matrix-org/synapse/issues/7019)) - Add some type annotations to the federation base & client classes. ([\#6995](https://github.com/matrix-org/synapse/issues/6995)) -- Change date in [INSTALL.md#tls-certificates] for last date of getting TLS certificates to November 2019. ([\#7015](https://github.com/matrix-org/synapse/issues/7015)) +- Change date in [INSTALL.md](./INSTALL.md#tls-certificates) for last date of getting TLS certificates to November 2019. ([\#7015](https://github.com/matrix-org/synapse/issues/7015)) - Port `synapse.rest.keys` to async/await. ([\#7020](https://github.com/matrix-org/synapse/issues/7020)) - Add a type check to `is_verified` when processing room keys. ([\#7045](https://github.com/matrix-org/synapse/issues/7045)) - Add type annotations and comments to the auth handler. ([\#7063](https://github.com/matrix-org/synapse/issues/7063)) From c8c926f9c9d54306a7cc344978de339837070cb5 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 19 Mar 2020 11:26:51 +0000 Subject: [PATCH 1213/1623] more changelog --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 153ace20b8..e3550497a4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -43,6 +43,7 @@ Improved Documentation - Updated CentOS8 install instructions. Contributed by Richard Kellner. ([\#6925](https://github.com/matrix-org/synapse/issues/6925)) - Fix `POSTGRES_INITDB_ARGS` in the `contrib/docker/docker-compose.yml` example docker-compose configuration. ([\#6984](https://github.com/matrix-org/synapse/issues/6984)) +- Change date in [INSTALL.md](./INSTALL.md#tls-certificates) for last date of getting TLS certificates to November 2019. ([\#7015](https://github.com/matrix-org/synapse/issues/7015)) - Document that the fallback auth endpoints must be routed to the same worker node as the register endpoints. ([\#7048](https://github.com/matrix-org/synapse/issues/7048)) @@ -69,7 +70,6 @@ Internal Changes - Add some type annotations to the database storage classes. ([\#6987](https://github.com/matrix-org/synapse/issues/6987)) - Port `synapse.handlers.presence` to async/await. ([\#6991](https://github.com/matrix-org/synapse/issues/6991), [\#7019](https://github.com/matrix-org/synapse/issues/7019)) - Add some type annotations to the federation base & client classes. ([\#6995](https://github.com/matrix-org/synapse/issues/6995)) -- Change date in [INSTALL.md](./INSTALL.md#tls-certificates) for last date of getting TLS certificates to November 2019. ([\#7015](https://github.com/matrix-org/synapse/issues/7015)) - Port `synapse.rest.keys` to async/await. ([\#7020](https://github.com/matrix-org/synapse/issues/7020)) - Add a type check to `is_verified` when processing room keys. ([\#7045](https://github.com/matrix-org/synapse/issues/7045)) - Add type annotations and comments to the auth handler. ([\#7063](https://github.com/matrix-org/synapse/issues/7063)) From c2db6599c820d97e3c8a02d782e90af80121c903 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Thu, 19 Mar 2020 08:22:56 -0400 Subject: [PATCH 1214/1623] Fix a bug in the federation API which could cause occasional "Failed to get PDU" errors (#7089). --- changelog.d/7089.bugfix | 1 + synapse/federation/federation_base.py | 24 +++++++++--------------- synapse/federation/federation_client.py | 19 ++++++++----------- synapse/federation/federation_server.py | 8 ++++---- 4 files changed, 22 insertions(+), 30 deletions(-) create mode 100644 changelog.d/7089.bugfix diff --git a/changelog.d/7089.bugfix b/changelog.d/7089.bugfix new file mode 100644 index 0000000000..f1f440f23a --- /dev/null +++ b/changelog.d/7089.bugfix @@ -0,0 +1 @@ +Fix a bug in the federation API which could cause occasional "Failed to get PDU" errors. diff --git a/synapse/federation/federation_base.py b/synapse/federation/federation_base.py index 5c991e5412..b0b0eba41e 100644 --- a/synapse/federation/federation_base.py +++ b/synapse/federation/federation_base.py @@ -25,11 +25,7 @@ from twisted.python.failure import Failure from synapse.api.constants import MAX_DEPTH, EventTypes, Membership from synapse.api.errors import Codes, SynapseError -from synapse.api.room_versions import ( - KNOWN_ROOM_VERSIONS, - EventFormatVersions, - RoomVersion, -) +from synapse.api.room_versions import EventFormatVersions, RoomVersion from synapse.crypto.event_signing import check_event_content_hash from synapse.crypto.keyring import Keyring from synapse.events import EventBase, make_event_from_dict @@ -55,13 +51,15 @@ class FederationBase(object): self.store = hs.get_datastore() self._clock = hs.get_clock() - def _check_sigs_and_hash(self, room_version: str, pdu: EventBase) -> Deferred: + def _check_sigs_and_hash( + self, room_version: RoomVersion, pdu: EventBase + ) -> Deferred: return make_deferred_yieldable( self._check_sigs_and_hashes(room_version, [pdu])[0] ) def _check_sigs_and_hashes( - self, room_version: str, pdus: List[EventBase] + self, room_version: RoomVersion, pdus: List[EventBase] ) -> List[Deferred]: """Checks that each of the received events is correctly signed by the sending server. @@ -146,7 +144,7 @@ class PduToCheckSig( def _check_sigs_on_pdus( - keyring: Keyring, room_version: str, pdus: Iterable[EventBase] + keyring: Keyring, room_version: RoomVersion, pdus: Iterable[EventBase] ) -> List[Deferred]: """Check that the given events are correctly signed @@ -191,10 +189,6 @@ def _check_sigs_on_pdus( for p in pdus ] - v = KNOWN_ROOM_VERSIONS.get(room_version) - if not v: - raise RuntimeError("Unrecognized room version %s" % (room_version,)) - # First we check that the sender event is signed by the sender's domain # (except if its a 3pid invite, in which case it may be sent by any server) pdus_to_check_sender = [p for p in pdus_to_check if not _is_invite_via_3pid(p.pdu)] @@ -204,7 +198,7 @@ def _check_sigs_on_pdus( ( p.sender_domain, p.redacted_pdu_json, - p.pdu.origin_server_ts if v.enforce_key_validity else 0, + p.pdu.origin_server_ts if room_version.enforce_key_validity else 0, p.pdu.event_id, ) for p in pdus_to_check_sender @@ -227,7 +221,7 @@ def _check_sigs_on_pdus( # event id's domain (normally only the case for joins/leaves), and add additional # checks. Only do this if the room version has a concept of event ID domain # (ie, the room version uses old-style non-hash event IDs). - if v.event_format == EventFormatVersions.V1: + if room_version.event_format == EventFormatVersions.V1: pdus_to_check_event_id = [ p for p in pdus_to_check @@ -239,7 +233,7 @@ def _check_sigs_on_pdus( ( get_domain_from_id(p.pdu.event_id), p.redacted_pdu_json, - p.pdu.origin_server_ts if v.enforce_key_validity else 0, + p.pdu.origin_server_ts if room_version.enforce_key_validity else 0, p.pdu.event_id, ) for p in pdus_to_check_event_id diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index 8c6b839478..a0071fec94 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -220,8 +220,7 @@ class FederationClient(FederationBase): # FIXME: We should handle signature failures more gracefully. pdus[:] = await make_deferred_yieldable( defer.gatherResults( - self._check_sigs_and_hashes(room_version.identifier, pdus), - consumeErrors=True, + self._check_sigs_and_hashes(room_version, pdus), consumeErrors=True, ).addErrback(unwrapFirstError) ) @@ -291,9 +290,7 @@ class FederationClient(FederationBase): pdu = pdu_list[0] # Check signatures are correct. - signed_pdu = await self._check_sigs_and_hash( - room_version.identifier, pdu - ) + signed_pdu = await self._check_sigs_and_hash(room_version, pdu) break @@ -350,7 +347,7 @@ class FederationClient(FederationBase): self, origin: str, pdus: List[EventBase], - room_version: str, + room_version: RoomVersion, outlier: bool = False, include_none: bool = False, ) -> List[EventBase]: @@ -396,7 +393,7 @@ class FederationClient(FederationBase): self.get_pdu( destinations=[pdu.origin], event_id=pdu.event_id, - room_version=room_version, # type: ignore + room_version=room_version, outlier=outlier, timeout=10000, ) @@ -434,7 +431,7 @@ class FederationClient(FederationBase): ] signed_auth = await self._check_sigs_and_hash_and_fetch( - destination, auth_chain, outlier=True, room_version=room_version.identifier + destination, auth_chain, outlier=True, room_version=room_version ) signed_auth.sort(key=lambda e: e.depth) @@ -661,7 +658,7 @@ class FederationClient(FederationBase): destination, list(pdus.values()), outlier=True, - room_version=room_version.identifier, + room_version=room_version, ) valid_pdus_map = {p.event_id: p for p in valid_pdus} @@ -756,7 +753,7 @@ class FederationClient(FederationBase): pdu = event_from_pdu_json(pdu_dict, room_version) # Check signatures are correct. - pdu = await self._check_sigs_and_hash(room_version.identifier, pdu) + pdu = await self._check_sigs_and_hash(room_version, pdu) # FIXME: We should handle signature failures more gracefully. @@ -948,7 +945,7 @@ class FederationClient(FederationBase): ] signed_events = await self._check_sigs_and_hash_and_fetch( - destination, events, outlier=False, room_version=room_version.identifier + destination, events, outlier=False, room_version=room_version ) except HttpResponseException as e: if not e.code == 400: diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index 275b9c99d7..89d521bc31 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -409,7 +409,7 @@ class FederationServer(FederationBase): pdu = event_from_pdu_json(content, room_version) origin_host, _ = parse_server_name(origin) await self.check_server_matches_acl(origin_host, pdu.room_id) - pdu = await self._check_sigs_and_hash(room_version.identifier, pdu) + pdu = await self._check_sigs_and_hash(room_version, pdu) ret_pdu = await self.handler.on_invite_request(origin, pdu, room_version) time_now = self._clock.time_msec() return {"event": ret_pdu.get_pdu_json(time_now)} @@ -425,7 +425,7 @@ class FederationServer(FederationBase): logger.debug("on_send_join_request: pdu sigs: %s", pdu.signatures) - pdu = await self._check_sigs_and_hash(room_version.identifier, pdu) + pdu = await self._check_sigs_and_hash(room_version, pdu) res_pdus = await self.handler.on_send_join_request(origin, pdu) time_now = self._clock.time_msec() @@ -455,7 +455,7 @@ class FederationServer(FederationBase): logger.debug("on_send_leave_request: pdu sigs: %s", pdu.signatures) - pdu = await self._check_sigs_and_hash(room_version.identifier, pdu) + pdu = await self._check_sigs_and_hash(room_version, pdu) await self.handler.on_send_leave_request(origin, pdu) return {} @@ -611,7 +611,7 @@ class FederationServer(FederationBase): logger.info("Accepting join PDU %s from %s", pdu.event_id, origin) # We've already checked that we know the room version by this point - room_version = await self.store.get_room_version_id(pdu.room_id) + room_version = await self.store.get_room_version(pdu.room_id) # Check signature. try: From caec7d4fa0041697b7714e638477772f0a827ff6 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Fri, 20 Mar 2020 07:20:02 -0400 Subject: [PATCH 1215/1623] Convert some of the media REST code to async/await (#7110) --- changelog.d/7110.misc | 1 + synapse/rest/media/v1/media_repository.py | 110 ++++++++---------- synapse/rest/media/v1/preview_url_resource.py | 37 +++--- synapse/rest/media/v1/thumbnail_resource.py | 54 ++++----- 4 files changed, 91 insertions(+), 111 deletions(-) create mode 100644 changelog.d/7110.misc diff --git a/changelog.d/7110.misc b/changelog.d/7110.misc new file mode 100644 index 0000000000..fac5bc0403 --- /dev/null +++ b/changelog.d/7110.misc @@ -0,0 +1 @@ +Convert some of synapse.rest.media to async/await. diff --git a/synapse/rest/media/v1/media_repository.py b/synapse/rest/media/v1/media_repository.py index 490b1b45a8..fd10d42f2f 100644 --- a/synapse/rest/media/v1/media_repository.py +++ b/synapse/rest/media/v1/media_repository.py @@ -24,7 +24,6 @@ from six import iteritems import twisted.internet.error import twisted.web.http -from twisted.internet import defer from twisted.web.resource import Resource from synapse.api.errors import ( @@ -114,15 +113,14 @@ class MediaRepository(object): "update_recently_accessed_media", self._update_recently_accessed ) - @defer.inlineCallbacks - def _update_recently_accessed(self): + async def _update_recently_accessed(self): remote_media = self.recently_accessed_remotes self.recently_accessed_remotes = set() local_media = self.recently_accessed_locals self.recently_accessed_locals = set() - yield self.store.update_cached_last_access_time( + await self.store.update_cached_last_access_time( local_media, remote_media, self.clock.time_msec() ) @@ -138,8 +136,7 @@ class MediaRepository(object): else: self.recently_accessed_locals.add(media_id) - @defer.inlineCallbacks - def create_content( + async def create_content( self, media_type, upload_name, content, content_length, auth_user ): """Store uploaded content for a local user and return the mxc URL @@ -158,11 +155,11 @@ class MediaRepository(object): file_info = FileInfo(server_name=None, file_id=media_id) - fname = yield self.media_storage.store_file(content, file_info) + fname = await self.media_storage.store_file(content, file_info) logger.info("Stored local media in file %r", fname) - yield self.store.store_local_media( + await self.store.store_local_media( media_id=media_id, media_type=media_type, time_now_ms=self.clock.time_msec(), @@ -171,12 +168,11 @@ class MediaRepository(object): user_id=auth_user, ) - yield self._generate_thumbnails(None, media_id, media_id, media_type) + await self._generate_thumbnails(None, media_id, media_id, media_type) return "mxc://%s/%s" % (self.server_name, media_id) - @defer.inlineCallbacks - def get_local_media(self, request, media_id, name): + async def get_local_media(self, request, media_id, name): """Responds to reqests for local media, if exists, or returns 404. Args: @@ -190,7 +186,7 @@ class MediaRepository(object): Deferred: Resolves once a response has successfully been written to request """ - media_info = yield self.store.get_local_media(media_id) + media_info = await self.store.get_local_media(media_id) if not media_info or media_info["quarantined_by"]: respond_404(request) return @@ -204,13 +200,12 @@ class MediaRepository(object): file_info = FileInfo(None, media_id, url_cache=url_cache) - responder = yield self.media_storage.fetch_media(file_info) - yield respond_with_responder( + responder = await self.media_storage.fetch_media(file_info) + await respond_with_responder( request, responder, media_type, media_length, upload_name ) - @defer.inlineCallbacks - def get_remote_media(self, request, server_name, media_id, name): + async def get_remote_media(self, request, server_name, media_id, name): """Respond to requests for remote media. Args: @@ -236,8 +231,8 @@ class MediaRepository(object): # We linearize here to ensure that we don't try and download remote # media multiple times concurrently key = (server_name, media_id) - with (yield self.remote_media_linearizer.queue(key)): - responder, media_info = yield self._get_remote_media_impl( + with (await self.remote_media_linearizer.queue(key)): + responder, media_info = await self._get_remote_media_impl( server_name, media_id ) @@ -246,14 +241,13 @@ class MediaRepository(object): media_type = media_info["media_type"] media_length = media_info["media_length"] upload_name = name if name else media_info["upload_name"] - yield respond_with_responder( + await respond_with_responder( request, responder, media_type, media_length, upload_name ) else: respond_404(request) - @defer.inlineCallbacks - def get_remote_media_info(self, server_name, media_id): + async def get_remote_media_info(self, server_name, media_id): """Gets the media info associated with the remote file, downloading if necessary. @@ -274,8 +268,8 @@ class MediaRepository(object): # We linearize here to ensure that we don't try and download remote # media multiple times concurrently key = (server_name, media_id) - with (yield self.remote_media_linearizer.queue(key)): - responder, media_info = yield self._get_remote_media_impl( + with (await self.remote_media_linearizer.queue(key)): + responder, media_info = await self._get_remote_media_impl( server_name, media_id ) @@ -286,8 +280,7 @@ class MediaRepository(object): return media_info - @defer.inlineCallbacks - def _get_remote_media_impl(self, server_name, media_id): + async def _get_remote_media_impl(self, server_name, media_id): """Looks for media in local cache, if not there then attempt to download from remote server. @@ -299,7 +292,7 @@ class MediaRepository(object): Returns: Deferred[(Responder, media_info)] """ - media_info = yield self.store.get_cached_remote_media(server_name, media_id) + media_info = await self.store.get_cached_remote_media(server_name, media_id) # file_id is the ID we use to track the file locally. If we've already # seen the file then reuse the existing ID, otherwise genereate a new @@ -317,19 +310,18 @@ class MediaRepository(object): logger.info("Media is quarantined") raise NotFoundError() - responder = yield self.media_storage.fetch_media(file_info) + responder = await self.media_storage.fetch_media(file_info) if responder: return responder, media_info # Failed to find the file anywhere, lets download it. - media_info = yield self._download_remote_file(server_name, media_id, file_id) + media_info = await self._download_remote_file(server_name, media_id, file_id) - responder = yield self.media_storage.fetch_media(file_info) + responder = await self.media_storage.fetch_media(file_info) return responder, media_info - @defer.inlineCallbacks - def _download_remote_file(self, server_name, media_id, file_id): + async def _download_remote_file(self, server_name, media_id, file_id): """Attempt to download the remote file from the given server name, using the given file_id as the local id. @@ -351,7 +343,7 @@ class MediaRepository(object): ("/_matrix/media/v1/download", server_name, media_id) ) try: - length, headers = yield self.client.get_file( + length, headers = await self.client.get_file( server_name, request_path, output_stream=f, @@ -397,7 +389,7 @@ class MediaRepository(object): ) raise SynapseError(502, "Failed to fetch remote media") - yield finish() + await finish() media_type = headers[b"Content-Type"][0].decode("ascii") upload_name = get_filename_from_headers(headers) @@ -405,7 +397,7 @@ class MediaRepository(object): logger.info("Stored remote media in file %r", fname) - yield self.store.store_cached_remote_media( + await self.store.store_cached_remote_media( origin=server_name, media_id=media_id, media_type=media_type, @@ -423,7 +415,7 @@ class MediaRepository(object): "filesystem_id": file_id, } - yield self._generate_thumbnails(server_name, media_id, file_id, media_type) + await self._generate_thumbnails(server_name, media_id, file_id, media_type) return media_info @@ -458,16 +450,15 @@ class MediaRepository(object): return t_byte_source - @defer.inlineCallbacks - def generate_local_exact_thumbnail( + async def generate_local_exact_thumbnail( self, media_id, t_width, t_height, t_method, t_type, url_cache ): - input_path = yield self.media_storage.ensure_media_is_in_local_cache( + input_path = await self.media_storage.ensure_media_is_in_local_cache( FileInfo(None, media_id, url_cache=url_cache) ) thumbnailer = Thumbnailer(input_path) - t_byte_source = yield defer_to_thread( + t_byte_source = await defer_to_thread( self.hs.get_reactor(), self._generate_thumbnail, thumbnailer, @@ -490,7 +481,7 @@ class MediaRepository(object): thumbnail_type=t_type, ) - output_path = yield self.media_storage.store_file( + output_path = await self.media_storage.store_file( t_byte_source, file_info ) finally: @@ -500,22 +491,21 @@ class MediaRepository(object): t_len = os.path.getsize(output_path) - yield self.store.store_local_thumbnail( + await self.store.store_local_thumbnail( media_id, t_width, t_height, t_type, t_method, t_len ) return output_path - @defer.inlineCallbacks - def generate_remote_exact_thumbnail( + async def generate_remote_exact_thumbnail( self, server_name, file_id, media_id, t_width, t_height, t_method, t_type ): - input_path = yield self.media_storage.ensure_media_is_in_local_cache( + input_path = await self.media_storage.ensure_media_is_in_local_cache( FileInfo(server_name, file_id, url_cache=False) ) thumbnailer = Thumbnailer(input_path) - t_byte_source = yield defer_to_thread( + t_byte_source = await defer_to_thread( self.hs.get_reactor(), self._generate_thumbnail, thumbnailer, @@ -537,7 +527,7 @@ class MediaRepository(object): thumbnail_type=t_type, ) - output_path = yield self.media_storage.store_file( + output_path = await self.media_storage.store_file( t_byte_source, file_info ) finally: @@ -547,7 +537,7 @@ class MediaRepository(object): t_len = os.path.getsize(output_path) - yield self.store.store_remote_media_thumbnail( + await self.store.store_remote_media_thumbnail( server_name, media_id, file_id, @@ -560,8 +550,7 @@ class MediaRepository(object): return output_path - @defer.inlineCallbacks - def _generate_thumbnails( + async def _generate_thumbnails( self, server_name, media_id, file_id, media_type, url_cache=False ): """Generate and store thumbnails for an image. @@ -582,7 +571,7 @@ class MediaRepository(object): if not requirements: return - input_path = yield self.media_storage.ensure_media_is_in_local_cache( + input_path = await self.media_storage.ensure_media_is_in_local_cache( FileInfo(server_name, file_id, url_cache=url_cache) ) @@ -600,7 +589,7 @@ class MediaRepository(object): return if thumbnailer.transpose_method is not None: - m_width, m_height = yield defer_to_thread( + m_width, m_height = await defer_to_thread( self.hs.get_reactor(), thumbnailer.transpose ) @@ -620,11 +609,11 @@ class MediaRepository(object): for (t_width, t_height, t_type), t_method in iteritems(thumbnails): # Generate the thumbnail if t_method == "crop": - t_byte_source = yield defer_to_thread( + t_byte_source = await defer_to_thread( self.hs.get_reactor(), thumbnailer.crop, t_width, t_height, t_type ) elif t_method == "scale": - t_byte_source = yield defer_to_thread( + t_byte_source = await defer_to_thread( self.hs.get_reactor(), thumbnailer.scale, t_width, t_height, t_type ) else: @@ -646,7 +635,7 @@ class MediaRepository(object): url_cache=url_cache, ) - output_path = yield self.media_storage.store_file( + output_path = await self.media_storage.store_file( t_byte_source, file_info ) finally: @@ -656,7 +645,7 @@ class MediaRepository(object): # Write to database if server_name: - yield self.store.store_remote_media_thumbnail( + await self.store.store_remote_media_thumbnail( server_name, media_id, file_id, @@ -667,15 +656,14 @@ class MediaRepository(object): t_len, ) else: - yield self.store.store_local_thumbnail( + await self.store.store_local_thumbnail( media_id, t_width, t_height, t_type, t_method, t_len ) return {"width": m_width, "height": m_height} - @defer.inlineCallbacks - def delete_old_remote_media(self, before_ts): - old_media = yield self.store.get_remote_media_before(before_ts) + async def delete_old_remote_media(self, before_ts): + old_media = await self.store.get_remote_media_before(before_ts) deleted = 0 @@ -689,7 +677,7 @@ class MediaRepository(object): # TODO: Should we delete from the backup store - with (yield self.remote_media_linearizer.queue(key)): + with (await self.remote_media_linearizer.queue(key)): full_path = self.filepaths.remote_media_filepath(origin, file_id) try: os.remove(full_path) @@ -705,7 +693,7 @@ class MediaRepository(object): ) shutil.rmtree(thumbnail_dir, ignore_errors=True) - yield self.store.delete_remote_media(origin, media_id) + await self.store.delete_remote_media(origin, media_id) deleted += 1 return {"deleted": deleted} diff --git a/synapse/rest/media/v1/preview_url_resource.py b/synapse/rest/media/v1/preview_url_resource.py index 07e395cfd1..c46676f8fc 100644 --- a/synapse/rest/media/v1/preview_url_resource.py +++ b/synapse/rest/media/v1/preview_url_resource.py @@ -165,8 +165,7 @@ class PreviewUrlResource(DirectServeResource): og = await make_deferred_yieldable(defer.maybeDeferred(observable.observe)) respond_with_json_bytes(request, 200, og, send_cors=True) - @defer.inlineCallbacks - def _do_preview(self, url, user, ts): + async def _do_preview(self, url, user, ts): """Check the db, and download the URL and build a preview Args: @@ -179,7 +178,7 @@ class PreviewUrlResource(DirectServeResource): """ # check the URL cache in the DB (which will also provide us with # historical previews, if we have any) - cache_result = yield self.store.get_url_cache(url, ts) + cache_result = await self.store.get_url_cache(url, ts) if ( cache_result and cache_result["expires_ts"] > ts @@ -192,13 +191,13 @@ class PreviewUrlResource(DirectServeResource): og = og.encode("utf8") return og - media_info = yield self._download_url(url, user) + media_info = await self._download_url(url, user) logger.debug("got media_info of '%s'", media_info) if _is_media(media_info["media_type"]): file_id = media_info["filesystem_id"] - dims = yield self.media_repo._generate_thumbnails( + dims = await self.media_repo._generate_thumbnails( None, file_id, file_id, media_info["media_type"], url_cache=True ) @@ -248,14 +247,14 @@ class PreviewUrlResource(DirectServeResource): # request itself and benefit from the same caching etc. But for now we # just rely on the caching on the master request to speed things up. if "og:image" in og and og["og:image"]: - image_info = yield self._download_url( + image_info = await self._download_url( _rebase_url(og["og:image"], media_info["uri"]), user ) if _is_media(image_info["media_type"]): # TODO: make sure we don't choke on white-on-transparent images file_id = image_info["filesystem_id"] - dims = yield self.media_repo._generate_thumbnails( + dims = await self.media_repo._generate_thumbnails( None, file_id, file_id, image_info["media_type"], url_cache=True ) if dims: @@ -293,7 +292,7 @@ class PreviewUrlResource(DirectServeResource): jsonog = json.dumps(og) # store OG in history-aware DB cache - yield self.store.store_url_cache( + await self.store.store_url_cache( url, media_info["response_code"], media_info["etag"], @@ -305,8 +304,7 @@ class PreviewUrlResource(DirectServeResource): return jsonog.encode("utf8") - @defer.inlineCallbacks - def _download_url(self, url, user): + async def _download_url(self, url, user): # TODO: we should probably honour robots.txt... except in practice # we're most likely being explicitly triggered by a human rather than a # bot, so are we really a robot? @@ -318,7 +316,7 @@ class PreviewUrlResource(DirectServeResource): with self.media_storage.store_into_file(file_info) as (f, fname, finish): try: logger.debug("Trying to get url '%s'", url) - length, headers, uri, code = yield self.client.get_file( + length, headers, uri, code = await self.client.get_file( url, output_stream=f, max_size=self.max_spider_size ) except SynapseError: @@ -345,7 +343,7 @@ class PreviewUrlResource(DirectServeResource): % (traceback.format_exception_only(sys.exc_info()[0], e),), Codes.UNKNOWN, ) - yield finish() + await finish() try: if b"Content-Type" in headers: @@ -356,7 +354,7 @@ class PreviewUrlResource(DirectServeResource): download_name = get_filename_from_headers(headers) - yield self.store.store_local_media( + await self.store.store_local_media( media_id=file_id, media_type=media_type, time_now_ms=self.clock.time_msec(), @@ -393,8 +391,7 @@ class PreviewUrlResource(DirectServeResource): "expire_url_cache_data", self._expire_url_cache_data ) - @defer.inlineCallbacks - def _expire_url_cache_data(self): + async def _expire_url_cache_data(self): """Clean up expired url cache content, media and thumbnails. """ # TODO: Delete from backup media store @@ -403,12 +400,12 @@ class PreviewUrlResource(DirectServeResource): logger.info("Running url preview cache expiry") - if not (yield self.store.db.updates.has_completed_background_updates()): + if not (await self.store.db.updates.has_completed_background_updates()): logger.info("Still running DB updates; skipping expiry") return # First we delete expired url cache entries - media_ids = yield self.store.get_expired_url_cache(now) + media_ids = await self.store.get_expired_url_cache(now) removed_media = [] for media_id in media_ids: @@ -430,7 +427,7 @@ class PreviewUrlResource(DirectServeResource): except Exception: pass - yield self.store.delete_url_cache(removed_media) + await self.store.delete_url_cache(removed_media) if removed_media: logger.info("Deleted %d entries from url cache", len(removed_media)) @@ -440,7 +437,7 @@ class PreviewUrlResource(DirectServeResource): # may have a room open with a preview url thing open). # So we wait a couple of days before deleting, just in case. expire_before = now - 2 * 24 * 60 * 60 * 1000 - media_ids = yield self.store.get_url_cache_media_before(expire_before) + media_ids = await self.store.get_url_cache_media_before(expire_before) removed_media = [] for media_id in media_ids: @@ -478,7 +475,7 @@ class PreviewUrlResource(DirectServeResource): except Exception: pass - yield self.store.delete_url_cache_media(removed_media) + await self.store.delete_url_cache_media(removed_media) logger.info("Deleted %d media from url cache", len(removed_media)) diff --git a/synapse/rest/media/v1/thumbnail_resource.py b/synapse/rest/media/v1/thumbnail_resource.py index d57480f761..0b87220234 100644 --- a/synapse/rest/media/v1/thumbnail_resource.py +++ b/synapse/rest/media/v1/thumbnail_resource.py @@ -16,8 +16,6 @@ import logging -from twisted.internet import defer - from synapse.http.server import ( DirectServeResource, set_cors_headers, @@ -79,11 +77,10 @@ class ThumbnailResource(DirectServeResource): ) self.media_repo.mark_recently_accessed(server_name, media_id) - @defer.inlineCallbacks - def _respond_local_thumbnail( + async def _respond_local_thumbnail( self, request, media_id, width, height, method, m_type ): - media_info = yield self.store.get_local_media(media_id) + media_info = await self.store.get_local_media(media_id) if not media_info: respond_404(request) @@ -93,7 +90,7 @@ class ThumbnailResource(DirectServeResource): respond_404(request) return - thumbnail_infos = yield self.store.get_local_media_thumbnails(media_id) + thumbnail_infos = await self.store.get_local_media_thumbnails(media_id) if thumbnail_infos: thumbnail_info = self._select_thumbnail( @@ -114,14 +111,13 @@ class ThumbnailResource(DirectServeResource): t_type = file_info.thumbnail_type t_length = thumbnail_info["thumbnail_length"] - responder = yield self.media_storage.fetch_media(file_info) - yield respond_with_responder(request, responder, t_type, t_length) + responder = await self.media_storage.fetch_media(file_info) + await respond_with_responder(request, responder, t_type, t_length) else: logger.info("Couldn't find any generated thumbnails") respond_404(request) - @defer.inlineCallbacks - def _select_or_generate_local_thumbnail( + async def _select_or_generate_local_thumbnail( self, request, media_id, @@ -130,7 +126,7 @@ class ThumbnailResource(DirectServeResource): desired_method, desired_type, ): - media_info = yield self.store.get_local_media(media_id) + media_info = await self.store.get_local_media(media_id) if not media_info: respond_404(request) @@ -140,7 +136,7 @@ class ThumbnailResource(DirectServeResource): respond_404(request) return - thumbnail_infos = yield self.store.get_local_media_thumbnails(media_id) + thumbnail_infos = await self.store.get_local_media_thumbnails(media_id) for info in thumbnail_infos: t_w = info["thumbnail_width"] == desired_width t_h = info["thumbnail_height"] == desired_height @@ -162,15 +158,15 @@ class ThumbnailResource(DirectServeResource): t_type = file_info.thumbnail_type t_length = info["thumbnail_length"] - responder = yield self.media_storage.fetch_media(file_info) + responder = await self.media_storage.fetch_media(file_info) if responder: - yield respond_with_responder(request, responder, t_type, t_length) + await respond_with_responder(request, responder, t_type, t_length) return logger.debug("We don't have a thumbnail of that size. Generating") # Okay, so we generate one. - file_path = yield self.media_repo.generate_local_exact_thumbnail( + file_path = await self.media_repo.generate_local_exact_thumbnail( media_id, desired_width, desired_height, @@ -180,13 +176,12 @@ class ThumbnailResource(DirectServeResource): ) if file_path: - yield respond_with_file(request, desired_type, file_path) + await respond_with_file(request, desired_type, file_path) else: logger.warning("Failed to generate thumbnail") respond_404(request) - @defer.inlineCallbacks - def _select_or_generate_remote_thumbnail( + async def _select_or_generate_remote_thumbnail( self, request, server_name, @@ -196,9 +191,9 @@ class ThumbnailResource(DirectServeResource): desired_method, desired_type, ): - media_info = yield self.media_repo.get_remote_media_info(server_name, media_id) + media_info = await self.media_repo.get_remote_media_info(server_name, media_id) - thumbnail_infos = yield self.store.get_remote_media_thumbnails( + thumbnail_infos = await self.store.get_remote_media_thumbnails( server_name, media_id ) @@ -224,15 +219,15 @@ class ThumbnailResource(DirectServeResource): t_type = file_info.thumbnail_type t_length = info["thumbnail_length"] - responder = yield self.media_storage.fetch_media(file_info) + responder = await self.media_storage.fetch_media(file_info) if responder: - yield respond_with_responder(request, responder, t_type, t_length) + await respond_with_responder(request, responder, t_type, t_length) return logger.debug("We don't have a thumbnail of that size. Generating") # Okay, so we generate one. - file_path = yield self.media_repo.generate_remote_exact_thumbnail( + file_path = await self.media_repo.generate_remote_exact_thumbnail( server_name, file_id, media_id, @@ -243,21 +238,20 @@ class ThumbnailResource(DirectServeResource): ) if file_path: - yield respond_with_file(request, desired_type, file_path) + await respond_with_file(request, desired_type, file_path) else: logger.warning("Failed to generate thumbnail") respond_404(request) - @defer.inlineCallbacks - def _respond_remote_thumbnail( + async def _respond_remote_thumbnail( self, request, server_name, media_id, width, height, method, m_type ): # TODO: Don't download the whole remote file # We should proxy the thumbnail from the remote server instead of # downloading the remote file and generating our own thumbnails. - media_info = yield self.media_repo.get_remote_media_info(server_name, media_id) + media_info = await self.media_repo.get_remote_media_info(server_name, media_id) - thumbnail_infos = yield self.store.get_remote_media_thumbnails( + thumbnail_infos = await self.store.get_remote_media_thumbnails( server_name, media_id ) @@ -278,8 +272,8 @@ class ThumbnailResource(DirectServeResource): t_type = file_info.thumbnail_type t_length = thumbnail_info["thumbnail_length"] - responder = yield self.media_storage.fetch_media(file_info) - yield respond_with_responder(request, responder, t_type, t_length) + responder = await self.media_storage.fetch_media(file_info) + await respond_with_responder(request, responder, t_type, t_length) else: logger.info("Failed to find any generated thumbnails") respond_404(request) From fdb13447167da0670dd6ad95fdf4a99cde450eb9 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 20 Mar 2020 14:40:47 +0000 Subject: [PATCH 1216/1623] Remove concept of a non-limited stream. (#7011) --- changelog.d/7011.misc | 1 + synapse/handlers/presence.py | 4 +- synapse/handlers/typing.py | 11 ++- synapse/replication/tcp/resource.py | 9 +-- synapse/replication/tcp/streams/_base.py | 68 ++++++++----------- synapse/storage/data_stores/main/devices.py | 10 ++- .../data_stores/main/end_to_end_keys.py | 14 ++-- synapse/storage/data_stores/main/presence.py | 23 ++++--- 8 files changed, 72 insertions(+), 68 deletions(-) create mode 100644 changelog.d/7011.misc diff --git a/changelog.d/7011.misc b/changelog.d/7011.misc new file mode 100644 index 0000000000..41c3b37574 --- /dev/null +++ b/changelog.d/7011.misc @@ -0,0 +1 @@ +Remove concept of a non-limited stream. diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py index 5526015ddb..6912165622 100644 --- a/synapse/handlers/presence.py +++ b/synapse/handlers/presence.py @@ -747,7 +747,7 @@ class PresenceHandler(object): return False - async def get_all_presence_updates(self, last_id, current_id): + async def get_all_presence_updates(self, last_id, current_id, limit): """ Gets a list of presence update rows from between the given stream ids. Each row has: @@ -762,7 +762,7 @@ class PresenceHandler(object): """ # TODO(markjh): replicate the unpersisted changes. # This could use the in-memory stores for recent changes. - rows = await self.store.get_all_presence_updates(last_id, current_id) + rows = await self.store.get_all_presence_updates(last_id, current_id, limit) return rows def notify_new_event(self): diff --git a/synapse/handlers/typing.py b/synapse/handlers/typing.py index 391bceb0c4..c7bc14c623 100644 --- a/synapse/handlers/typing.py +++ b/synapse/handlers/typing.py @@ -15,6 +15,7 @@ import logging from collections import namedtuple +from typing import List from twisted.internet import defer @@ -257,7 +258,13 @@ class TypingHandler(object): "typing_key", self._latest_room_serial, rooms=[member.room_id] ) - async def get_all_typing_updates(self, last_id, current_id): + async def get_all_typing_updates( + self, last_id: int, current_id: int, limit: int + ) -> List[dict]: + """Get up to `limit` typing updates between the given tokens, earliest + updates first. + """ + if last_id == current_id: return [] @@ -275,7 +282,7 @@ class TypingHandler(object): typing = self._room_typing[room_id] rows.append((serial, room_id, list(typing))) rows.sort() - return rows + return rows[:limit] def get_current_token(self): return self._latest_room_serial diff --git a/synapse/replication/tcp/resource.py b/synapse/replication/tcp/resource.py index ce9d1fae12..6e2ebaf614 100644 --- a/synapse/replication/tcp/resource.py +++ b/synapse/replication/tcp/resource.py @@ -166,11 +166,6 @@ class ReplicationStreamer(object): self.pending_updates = False with Measure(self.clock, "repl.stream.get_updates"): - # First we tell the streams that they should update their - # current tokens. - for stream in self.streams: - stream.advance_current_token() - all_streams = self.streams if self._replication_torture_level is not None: @@ -180,7 +175,7 @@ class ReplicationStreamer(object): random.shuffle(all_streams) for stream in all_streams: - if stream.last_token == stream.upto_token: + if stream.last_token == stream.current_token(): continue if self._replication_torture_level: @@ -192,7 +187,7 @@ class ReplicationStreamer(object): "Getting stream: %s: %s -> %s", stream.NAME, stream.last_token, - stream.upto_token, + stream.current_token(), ) try: updates, current_token = await stream.get_updates() diff --git a/synapse/replication/tcp/streams/_base.py b/synapse/replication/tcp/streams/_base.py index 7a8b6e9df1..abf5c6c6a8 100644 --- a/synapse/replication/tcp/streams/_base.py +++ b/synapse/replication/tcp/streams/_base.py @@ -17,10 +17,12 @@ import itertools import logging from collections import namedtuple -from typing import Any, List, Optional +from typing import Any, List, Optional, Tuple import attr +from synapse.types import JsonDict + logger = logging.getLogger(__name__) @@ -119,13 +121,12 @@ class Stream(object): """Base class for the streams. Provides a `get_updates()` function that returns new updates since the last - time it was called up until the point `advance_current_token` was called. + time it was called. """ NAME = None # type: str # The name of the stream # The type of the row. Used by the default impl of parse_row. ROW_TYPE = None # type: Any - _LIMITED = True # Whether the update function takes a limit @classmethod def parse_row(cls, row): @@ -146,26 +147,15 @@ class Stream(object): # The token from which we last asked for updates self.last_token = self.current_token() - # The token that we will get updates up to - self.upto_token = self.current_token() - - def advance_current_token(self): - """Updates `upto_token` to "now", which updates up until which point - get_updates[_since] will fetch rows till. - """ - self.upto_token = self.current_token() - def discard_updates_and_advance(self): """Called when the stream should advance but the updates would be discarded, e.g. when there are no currently connected workers. """ - self.upto_token = self.current_token() - self.last_token = self.upto_token + self.last_token = self.current_token() async def get_updates(self): """Gets all updates since the last time this function was called (or - since the stream was constructed if it hadn't been called before), - until the `upto_token` + since the stream was constructed if it hadn't been called before). Returns: Deferred[Tuple[List[Tuple[int, Any]], int]: @@ -178,44 +168,45 @@ class Stream(object): return updates, current_token - async def get_updates_since(self, from_token): + async def get_updates_since( + self, from_token: int + ) -> Tuple[List[Tuple[int, JsonDict]], int]: """Like get_updates except allows specifying from when we should stream updates Returns: - Deferred[Tuple[List[Tuple[int, Any]], int]: - Resolves to a pair ``(updates, current_token)``, where ``updates`` is a - list of ``(token, row)`` entries. ``row`` will be json-serialised and - sent over the replication steam. + Resolves to a pair `(updates, new_last_token)`, where `updates` is + a list of `(token, row)` entries and `new_last_token` is the new + position in stream. """ - if from_token in ("NOW", "now"): - return [], self.upto_token - current_token = self.upto_token + if from_token in ("NOW", "now"): + return [], self.current_token() + + current_token = self.current_token() from_token = int(from_token) if from_token == current_token: return [], current_token - logger.info("get_updates_since: %s", self.__class__) - if self._LIMITED: - rows = await self.update_function( - from_token, current_token, limit=MAX_EVENTS_BEHIND + 1 - ) + rows = await self.update_function( + from_token, current_token, limit=MAX_EVENTS_BEHIND + 1 + ) - # never turn more than MAX_EVENTS_BEHIND + 1 into updates. - rows = itertools.islice(rows, MAX_EVENTS_BEHIND + 1) - else: - rows = await self.update_function(from_token, current_token) + # never turn more than MAX_EVENTS_BEHIND + 1 into updates. + rows = itertools.islice(rows, MAX_EVENTS_BEHIND + 1) updates = [(row[0], row[1:]) for row in rows] # check we didn't get more rows than the limit. # doing it like this allows the update_function to be a generator. - if self._LIMITED and len(updates) >= MAX_EVENTS_BEHIND: + if len(updates) >= MAX_EVENTS_BEHIND: raise Exception("stream %s has fallen behind" % (self.NAME)) + # The update function didn't hit the limit, so we must have got all + # the updates to `current_token`, and can return that as our new + # stream position. return updates, current_token def current_token(self): @@ -227,9 +218,8 @@ class Stream(object): """ raise NotImplementedError() - def update_function(self, from_token, current_token, limit=None): - """Get updates between from_token and to_token. If Stream._LIMITED is - True then limit is provided, otherwise it's not. + def update_function(self, from_token, current_token, limit): + """Get updates between from_token and to_token. Returns: Deferred(list(tuple)): the first entry in the tuple is the token for @@ -257,7 +247,6 @@ class BackfillStream(Stream): class PresenceStream(Stream): NAME = "presence" - _LIMITED = False ROW_TYPE = PresenceStreamRow def __init__(self, hs): @@ -272,7 +261,6 @@ class PresenceStream(Stream): class TypingStream(Stream): NAME = "typing" - _LIMITED = False ROW_TYPE = TypingStreamRow def __init__(self, hs): @@ -372,7 +360,6 @@ class DeviceListsStream(Stream): """ NAME = "device_lists" - _LIMITED = False ROW_TYPE = DeviceListsStreamRow def __init__(self, hs): @@ -462,7 +449,6 @@ class UserSignatureStream(Stream): """ NAME = "user_signature" - _LIMITED = False ROW_TYPE = UserSignatureStreamRow def __init__(self, hs): diff --git a/synapse/storage/data_stores/main/devices.py b/synapse/storage/data_stores/main/devices.py index 4c19c02bbc..2d47cfd131 100644 --- a/synapse/storage/data_stores/main/devices.py +++ b/synapse/storage/data_stores/main/devices.py @@ -576,7 +576,7 @@ class DeviceWorkerStore(SQLBaseStore): return set() async def get_all_device_list_changes_for_remotes( - self, from_key: int, to_key: int + self, from_key: int, to_key: int, limit: int, ) -> List[Tuple[int, str]]: """Return a list of `(stream_id, entity)` which is the combined list of changes to devices and which destinations need to be poked. Entity is @@ -592,10 +592,16 @@ class DeviceWorkerStore(SQLBaseStore): SELECT stream_id, destination AS entity FROM device_lists_outbound_pokes ) AS e WHERE ? < stream_id AND stream_id <= ? + LIMIT ? """ return await self.db.execute( - "get_all_device_list_changes_for_remotes", None, sql, from_key, to_key + "get_all_device_list_changes_for_remotes", + None, + sql, + from_key, + to_key, + limit, ) @cached(max_entries=10000) diff --git a/synapse/storage/data_stores/main/end_to_end_keys.py b/synapse/storage/data_stores/main/end_to_end_keys.py index 001a53f9b4..bcf746b7ef 100644 --- a/synapse/storage/data_stores/main/end_to_end_keys.py +++ b/synapse/storage/data_stores/main/end_to_end_keys.py @@ -537,7 +537,7 @@ class EndToEndKeyWorkerStore(SQLBaseStore): return result - def get_all_user_signature_changes_for_remotes(self, from_key, to_key): + def get_all_user_signature_changes_for_remotes(self, from_key, to_key, limit): """Return a list of changes from the user signature stream to notify remotes. Note that the user signature stream represents when a user signs their device with their user-signing key, which is not published to other @@ -552,13 +552,19 @@ class EndToEndKeyWorkerStore(SQLBaseStore): Deferred[list[(int,str)]] a list of `(stream_id, user_id)` """ sql = """ - SELECT MAX(stream_id) AS stream_id, from_user_id AS user_id + SELECT stream_id, from_user_id AS user_id FROM user_signature_stream WHERE ? < stream_id AND stream_id <= ? - GROUP BY user_id + ORDER BY stream_id ASC + LIMIT ? """ return self.db.execute( - "get_all_user_signature_changes_for_remotes", None, sql, from_key, to_key + "get_all_user_signature_changes_for_remotes", + None, + sql, + from_key, + to_key, + limit, ) diff --git a/synapse/storage/data_stores/main/presence.py b/synapse/storage/data_stores/main/presence.py index 604c8b7ddd..dab31e0c2d 100644 --- a/synapse/storage/data_stores/main/presence.py +++ b/synapse/storage/data_stores/main/presence.py @@ -60,7 +60,7 @@ class PresenceStore(SQLBaseStore): "status_msg": state.status_msg, "currently_active": state.currently_active, } - for state in presence_states + for stream_id, state in zip(stream_orderings, presence_states) ], ) @@ -73,19 +73,22 @@ class PresenceStore(SQLBaseStore): ) txn.execute(sql + clause, [stream_id] + list(args)) - def get_all_presence_updates(self, last_id, current_id): + def get_all_presence_updates(self, last_id, current_id, limit): if last_id == current_id: return defer.succeed([]) def get_all_presence_updates_txn(txn): - sql = ( - "SELECT stream_id, user_id, state, last_active_ts," - " last_federation_update_ts, last_user_sync_ts, status_msg," - " currently_active" - " FROM presence_stream" - " WHERE ? < stream_id AND stream_id <= ?" - ) - txn.execute(sql, (last_id, current_id)) + sql = """ + SELECT stream_id, user_id, state, last_active_ts, + last_federation_update_ts, last_user_sync_ts, + status_msg, + currently_active + FROM presence_stream + WHERE ? < stream_id AND stream_id <= ? + ORDER BY stream_id ASC + LIMIT ? + """ + txn.execute(sql, (last_id, current_id, limit)) return txn.fetchall() return self.db.runInteraction( From c165c1233b8ef244fadca97c7d465fdcf473d077 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 20 Mar 2020 16:24:22 +0100 Subject: [PATCH 1217/1623] Improve database configuration docs (#6988) Attempts to clarify the sample config for databases, and add some stuff about tcp keepalives to `postgres.md`. --- changelog.d/6988.doc | 1 + docs/postgres.md | 42 ++++++++++++---- docs/sample_config.yaml | 43 ++++++++++++++-- synapse/config/_base.py | 2 - synapse/config/database.py | 93 ++++++++++++++++++++++------------- tests/config/test_database.py | 22 +-------- 6 files changed, 132 insertions(+), 71 deletions(-) create mode 100644 changelog.d/6988.doc diff --git a/changelog.d/6988.doc b/changelog.d/6988.doc new file mode 100644 index 0000000000..b6f71bb966 --- /dev/null +++ b/changelog.d/6988.doc @@ -0,0 +1 @@ +Improve the documentation for database configuration. diff --git a/docs/postgres.md b/docs/postgres.md index e0793ecee8..16a630c3d1 100644 --- a/docs/postgres.md +++ b/docs/postgres.md @@ -105,19 +105,41 @@ of free memory the database host has available. When you are ready to start using PostgreSQL, edit the `database` section in your config file to match the following lines: - database: - name: psycopg2 - args: - user: - password: - database: - host: - cp_min: 5 - cp_max: 10 +```yaml +database: + name: psycopg2 + args: + user: + password: + database: + host: + cp_min: 5 + cp_max: 10 +``` All key, values in `args` are passed to the `psycopg2.connect(..)` function, except keys beginning with `cp_`, which are consumed by the -twisted adbapi connection pool. +twisted adbapi connection pool. See the [libpq +documentation](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS) +for a list of options which can be passed. + +You should consider tuning the `args.keepalives_*` options if there is any danger of +the connection between your homeserver and database dropping, otherwise Synapse +may block for an extended period while it waits for a response from the +database server. Example values might be: + +```yaml +# seconds of inactivity after which TCP should send a keepalive message to the server +keepalives_idle: 10 + +# the number of seconds after which a TCP keepalive message that is not +# acknowledged by the server should be retransmitted +keepalives_interval: 10 + +# the number of TCP keepalives that can be lost before the client's connection +# to the server is considered dead +keepalives_count: 3 +``` ## Porting from SQLite diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 2ff0dd05a2..276e43b732 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -578,13 +578,46 @@ acme: ## Database ## +# The 'database' setting defines the database that synapse uses to store all of +# its data. +# +# 'name' gives the database engine to use: either 'sqlite3' (for SQLite) or +# 'psycopg2' (for PostgreSQL). +# +# 'args' gives options which are passed through to the database engine, +# except for options starting 'cp_', which are used to configure the Twisted +# connection pool. For a reference to valid arguments, see: +# * for sqlite: https://docs.python.org/3/library/sqlite3.html#sqlite3.connect +# * for postgres: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS +# * for the connection pool: https://twistedmatrix.com/documents/current/api/twisted.enterprise.adbapi.ConnectionPool.html#__init__ +# +# +# Example SQLite configuration: +# +#database: +# name: sqlite3 +# args: +# database: /path/to/homeserver.db +# +# +# Example Postgres configuration: +# +#database: +# name: psycopg2 +# args: +# user: synapse +# password: secretpassword +# database: synapse +# host: localhost +# cp_min: 5 +# cp_max: 10 +# +# For more information on using Synapse with Postgres, see `docs/postgres.md`. +# database: - # The database engine name - name: "sqlite3" - # Arguments to pass to the engine + name: sqlite3 args: - # Path to the database - database: "DATADIR/homeserver.db" + database: DATADIR/homeserver.db # Number of events to cache in memory. # diff --git a/synapse/config/_base.py b/synapse/config/_base.py index ba846042c4..efe2af5504 100644 --- a/synapse/config/_base.py +++ b/synapse/config/_base.py @@ -294,7 +294,6 @@ class RootConfig(object): report_stats=None, open_private_ports=False, listeners=None, - database_conf=None, tls_certificate_path=None, tls_private_key_path=None, acme_domain=None, @@ -367,7 +366,6 @@ class RootConfig(object): report_stats=report_stats, open_private_ports=open_private_ports, listeners=listeners, - database_conf=database_conf, tls_certificate_path=tls_certificate_path, tls_private_key_path=tls_private_key_path, acme_domain=acme_domain, diff --git a/synapse/config/database.py b/synapse/config/database.py index 219b32f670..b8ab2f86ac 100644 --- a/synapse/config/database.py +++ b/synapse/config/database.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd +# Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,14 +15,60 @@ # limitations under the License. import logging import os -from textwrap import indent - -import yaml from synapse.config._base import Config, ConfigError logger = logging.getLogger(__name__) +DEFAULT_CONFIG = """\ +## Database ## + +# The 'database' setting defines the database that synapse uses to store all of +# its data. +# +# 'name' gives the database engine to use: either 'sqlite3' (for SQLite) or +# 'psycopg2' (for PostgreSQL). +# +# 'args' gives options which are passed through to the database engine, +# except for options starting 'cp_', which are used to configure the Twisted +# connection pool. For a reference to valid arguments, see: +# * for sqlite: https://docs.python.org/3/library/sqlite3.html#sqlite3.connect +# * for postgres: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS +# * for the connection pool: https://twistedmatrix.com/documents/current/api/twisted.enterprise.adbapi.ConnectionPool.html#__init__ +# +# +# Example SQLite configuration: +# +#database: +# name: sqlite3 +# args: +# database: /path/to/homeserver.db +# +# +# Example Postgres configuration: +# +#database: +# name: psycopg2 +# args: +# user: synapse +# password: secretpassword +# database: synapse +# host: localhost +# cp_min: 5 +# cp_max: 10 +# +# For more information on using Synapse with Postgres, see `docs/postgres.md`. +# +database: + name: sqlite3 + args: + database: %(database_path)s + +# Number of events to cache in memory. +# +#event_cache_size: 10K +""" + class DatabaseConnectionConfig: """Contains the connection config for a particular database. @@ -36,10 +83,12 @@ class DatabaseConnectionConfig: """ def __init__(self, name: str, db_config: dict): - if db_config["name"] not in ("sqlite3", "psycopg2"): - raise ConfigError("Unsupported database type %r" % (db_config["name"],)) + db_engine = db_config.get("name", "sqlite3") - if db_config["name"] == "sqlite3": + if db_engine not in ("sqlite3", "psycopg2"): + raise ConfigError("Unsupported database type %r" % (db_engine,)) + + if db_engine == "sqlite3": db_config.setdefault("args", {}).update( {"cp_min": 1, "cp_max": 1, "check_same_thread": False} ) @@ -97,34 +146,10 @@ class DatabaseConfig(Config): self.set_databasepath(config.get("database_path")) - def generate_config_section(self, data_dir_path, database_conf, **kwargs): - if not database_conf: - database_path = os.path.join(data_dir_path, "homeserver.db") - database_conf = ( - """# The database engine name - name: "sqlite3" - # Arguments to pass to the engine - args: - # Path to the database - database: "%(database_path)s" - """ - % locals() - ) - else: - database_conf = indent(yaml.dump(database_conf), " " * 10).lstrip() - - return ( - """\ - ## Database ## - - database: - %(database_conf)s - # Number of events to cache in memory. - # - #event_cache_size: 10K - """ - % locals() - ) + def generate_config_section(self, data_dir_path, **kwargs): + return DEFAULT_CONFIG % { + "database_path": os.path.join(data_dir_path, "homeserver.db") + } def read_arguments(self, args): self.set_databasepath(args.database_path) diff --git a/tests/config/test_database.py b/tests/config/test_database.py index 151d3006ac..f675bde68e 100644 --- a/tests/config/test_database.py +++ b/tests/config/test_database.py @@ -21,9 +21,9 @@ from tests import unittest class DatabaseConfigTestCase(unittest.TestCase): - def test_database_configured_correctly_no_database_conf_param(self): + def test_database_configured_correctly(self): conf = yaml.safe_load( - DatabaseConfig().generate_config_section("/data_dir_path", None) + DatabaseConfig().generate_config_section(data_dir_path="/data_dir_path") ) expected_database_conf = { @@ -32,21 +32,3 @@ class DatabaseConfigTestCase(unittest.TestCase): } self.assertEqual(conf["database"], expected_database_conf) - - def test_database_configured_correctly_database_conf_param(self): - - database_conf = { - "name": "my super fast datastore", - "args": { - "user": "matrix", - "password": "synapse_database_password", - "host": "synapse_database_host", - "database": "matrix", - }, - } - - conf = yaml.safe_load( - DatabaseConfig().generate_config_section("/data_dir_path", database_conf) - ) - - self.assertEqual(conf["database"], database_conf) From 477c4f5b1c2c7733d4b2cf578dc9aa8e048011b0 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Fri, 20 Mar 2020 16:22:47 -0400 Subject: [PATCH 1218/1623] Clean-up some auth/login REST code (#7115) --- changelog.d/7115.misc | 1 + synapse/rest/client/v1/login.py | 8 ----- synapse/rest/client/v2_alpha/auth.py | 53 +++++++++++----------------- 3 files changed, 21 insertions(+), 41 deletions(-) create mode 100644 changelog.d/7115.misc diff --git a/changelog.d/7115.misc b/changelog.d/7115.misc new file mode 100644 index 0000000000..7d4a011e3e --- /dev/null +++ b/changelog.d/7115.misc @@ -0,0 +1 @@ +De-duplicate / remove unused REST code for login and auth. diff --git a/synapse/rest/client/v1/login.py b/synapse/rest/client/v1/login.py index d0d4999795..31551524f8 100644 --- a/synapse/rest/client/v1/login.py +++ b/synapse/rest/client/v1/login.py @@ -28,7 +28,6 @@ from synapse.http.servlet import ( parse_json_object_from_request, parse_string, ) -from synapse.push.mailer import load_jinja2_templates from synapse.rest.client.v2_alpha._base import client_patterns from synapse.rest.well_known import WellKnownBuilder from synapse.types import UserID, map_username_to_mxid_localpart @@ -548,13 +547,6 @@ class SSOAuthHandler(object): self._registration_handler = hs.get_registration_handler() self._macaroon_gen = hs.get_macaroon_generator() - # Load the redirect page HTML template - self._template = load_jinja2_templates( - hs.config.sso_redirect_confirm_template_dir, ["sso_redirect_confirm.html"], - )[0] - - self._server_name = hs.config.server_name - # cast to tuple for use with str.startswith self._whitelisted_sso_clients = tuple(hs.config.sso_client_whitelist) diff --git a/synapse/rest/client/v2_alpha/auth.py b/synapse/rest/client/v2_alpha/auth.py index 50e080673b..85cf5a14c6 100644 --- a/synapse/rest/client/v2_alpha/auth.py +++ b/synapse/rest/client/v2_alpha/auth.py @@ -142,14 +142,6 @@ class AuthRestServlet(RestServlet): % (CLIENT_API_PREFIX, LoginType.RECAPTCHA), "sitekey": self.hs.config.recaptcha_public_key, } - html_bytes = html.encode("utf8") - request.setResponseCode(200) - request.setHeader(b"Content-Type", b"text/html; charset=utf-8") - request.setHeader(b"Content-Length", b"%d" % (len(html_bytes),)) - - request.write(html_bytes) - finish_request(request) - return None elif stagetype == LoginType.TERMS: html = TERMS_TEMPLATE % { "session": session, @@ -158,17 +150,19 @@ class AuthRestServlet(RestServlet): "myurl": "%s/r0/auth/%s/fallback/web" % (CLIENT_API_PREFIX, LoginType.TERMS), } - html_bytes = html.encode("utf8") - request.setResponseCode(200) - request.setHeader(b"Content-Type", b"text/html; charset=utf-8") - request.setHeader(b"Content-Length", b"%d" % (len(html_bytes),)) - - request.write(html_bytes) - finish_request(request) - return None else: raise SynapseError(404, "Unknown auth stage type") + # Render the HTML and return. + html_bytes = html.encode("utf8") + request.setResponseCode(200) + request.setHeader(b"Content-Type", b"text/html; charset=utf-8") + request.setHeader(b"Content-Length", b"%d" % (len(html_bytes),)) + + request.write(html_bytes) + finish_request(request) + return None + async def on_POST(self, request, stagetype): session = parse_string(request, "session") @@ -196,15 +190,6 @@ class AuthRestServlet(RestServlet): % (CLIENT_API_PREFIX, LoginType.RECAPTCHA), "sitekey": self.hs.config.recaptcha_public_key, } - html_bytes = html.encode("utf8") - request.setResponseCode(200) - request.setHeader(b"Content-Type", b"text/html; charset=utf-8") - request.setHeader(b"Content-Length", b"%d" % (len(html_bytes),)) - - request.write(html_bytes) - finish_request(request) - - return None elif stagetype == LoginType.TERMS: authdict = {"session": session} @@ -225,17 +210,19 @@ class AuthRestServlet(RestServlet): "myurl": "%s/r0/auth/%s/fallback/web" % (CLIENT_API_PREFIX, LoginType.TERMS), } - html_bytes = html.encode("utf8") - request.setResponseCode(200) - request.setHeader(b"Content-Type", b"text/html; charset=utf-8") - request.setHeader(b"Content-Length", b"%d" % (len(html_bytes),)) - - request.write(html_bytes) - finish_request(request) - return None else: raise SynapseError(404, "Unknown auth stage type") + # Render the HTML and return. + html_bytes = html.encode("utf8") + request.setResponseCode(200) + request.setHeader(b"Content-Type", b"text/html; charset=utf-8") + request.setHeader(b"Content-Length", b"%d" % (len(html_bytes),)) + + request.write(html_bytes) + finish_request(request) + return None + def on_OPTIONS(self, _): return 200, {} From 96071eea8f5e18282c07da3a61e4b3431f694cc5 Mon Sep 17 00:00:00 2001 From: Dionysis Grigoropoulos Date: Mon, 23 Mar 2020 11:48:28 +0200 Subject: [PATCH 1219/1623] Set Referrer-Policy to no-referrer for media (#7009) --- changelog.d/7009.feature | 1 + synapse/rest/media/v1/download_resource.py | 3 +++ 2 files changed, 4 insertions(+) create mode 100644 changelog.d/7009.feature diff --git a/changelog.d/7009.feature b/changelog.d/7009.feature new file mode 100644 index 0000000000..cd2705d5ba --- /dev/null +++ b/changelog.d/7009.feature @@ -0,0 +1 @@ +Set `Referrer-Policy` header to `no-referrer` on media downloads. diff --git a/synapse/rest/media/v1/download_resource.py b/synapse/rest/media/v1/download_resource.py index 66a01559e1..24d3ae5bbc 100644 --- a/synapse/rest/media/v1/download_resource.py +++ b/synapse/rest/media/v1/download_resource.py @@ -50,6 +50,9 @@ class DownloadResource(DirectServeResource): b" media-src 'self';" b" object-src 'self';", ) + request.setHeader( + b"Referrer-Policy", b"no-referrer", + ) server_name, media_id, name = parse_media_id(request) if server_name == self.server_name: await self.media_repo.get_local_media(request, media_id, name) From b3cee0ce670ada582b2a4b36c377f160c7ee1d09 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 23 Mar 2020 11:39:36 +0000 Subject: [PATCH 1220/1623] Fix processing of `groups` stream, and use symbolic names for streams (#7117) `groups` != `receipts` Introduced in #6964 --- changelog.d/7117.bugfix | 1 + synapse/app/generic_worker.py | 35 +++++++---- synapse/replication/tcp/streams/__init__.py | 70 +++++++++++++++------ 3 files changed, 76 insertions(+), 30 deletions(-) create mode 100644 changelog.d/7117.bugfix diff --git a/changelog.d/7117.bugfix b/changelog.d/7117.bugfix new file mode 100644 index 0000000000..1896d7ad49 --- /dev/null +++ b/changelog.d/7117.bugfix @@ -0,0 +1 @@ +Fix a bug which meant that groups updates were not correctly replicated between workers. diff --git a/synapse/app/generic_worker.py b/synapse/app/generic_worker.py index cdc078cf11..136babe6ce 100644 --- a/synapse/app/generic_worker.py +++ b/synapse/app/generic_worker.py @@ -65,12 +65,23 @@ from synapse.replication.slave.storage.registration import SlavedRegistrationSto from synapse.replication.slave.storage.room import RoomStore from synapse.replication.slave.storage.transactions import SlavedTransactionStore from synapse.replication.tcp.client import ReplicationClientHandler -from synapse.replication.tcp.streams._base import ( +from synapse.replication.tcp.streams import ( + AccountDataStream, DeviceListsStream, + GroupServerStream, + PresenceStream, + PushersStream, + PushRulesStream, ReceiptsStream, + TagAccountDataStream, ToDeviceStream, + TypingStream, +) +from synapse.replication.tcp.streams.events import ( + EventsStream, + EventsStreamEventRow, + EventsStreamRow, ) -from synapse.replication.tcp.streams.events import EventsStreamEventRow, EventsStreamRow from synapse.rest.admin import register_servlets_for_media_repo from synapse.rest.client.v1 import events from synapse.rest.client.v1.initial_sync import InitialSyncRestServlet @@ -626,7 +637,7 @@ class GenericWorkerReplicationHandler(ReplicationClientHandler): if self.send_handler: self.send_handler.process_replication_rows(stream_name, token, rows) - if stream_name == "events": + if stream_name == EventsStream.NAME: # We shouldn't get multiple rows per token for events stream, so # we don't need to optimise this for multiple rows. for row in rows: @@ -649,44 +660,44 @@ class GenericWorkerReplicationHandler(ReplicationClientHandler): ) await self.pusher_pool.on_new_notifications(token, token) - elif stream_name == "push_rules": + elif stream_name == PushRulesStream.NAME: self.notifier.on_new_event( "push_rules_key", token, users=[row.user_id for row in rows] ) - elif stream_name in ("account_data", "tag_account_data"): + elif stream_name in (AccountDataStream.NAME, TagAccountDataStream.NAME): self.notifier.on_new_event( "account_data_key", token, users=[row.user_id for row in rows] ) - elif stream_name == "receipts": + elif stream_name == ReceiptsStream.NAME: self.notifier.on_new_event( "receipt_key", token, rooms=[row.room_id for row in rows] ) await self.pusher_pool.on_new_receipts( token, token, {row.room_id for row in rows} ) - elif stream_name == "typing": + elif stream_name == TypingStream.NAME: self.typing_handler.process_replication_rows(token, rows) self.notifier.on_new_event( "typing_key", token, rooms=[row.room_id for row in rows] ) - elif stream_name == "to_device": + elif stream_name == ToDeviceStream.NAME: entities = [row.entity for row in rows if row.entity.startswith("@")] if entities: self.notifier.on_new_event("to_device_key", token, users=entities) - elif stream_name == "device_lists": + elif stream_name == DeviceListsStream.NAME: all_room_ids = set() for row in rows: if row.entity.startswith("@"): room_ids = await self.store.get_rooms_for_user(row.entity) all_room_ids.update(room_ids) self.notifier.on_new_event("device_list_key", token, rooms=all_room_ids) - elif stream_name == "presence": + elif stream_name == PresenceStream.NAME: await self.presence_handler.process_replication_rows(token, rows) - elif stream_name == "receipts": + elif stream_name == GroupServerStream.NAME: self.notifier.on_new_event( "groups_key", token, users=[row.user_id for row in rows] ) - elif stream_name == "pushers": + elif stream_name == PushersStream.NAME: for row in rows: if row.deleted: self.stop_pusher(row.user_id, row.app_id, row.pushkey) diff --git a/synapse/replication/tcp/streams/__init__.py b/synapse/replication/tcp/streams/__init__.py index 5f52264e84..29199f5b46 100644 --- a/synapse/replication/tcp/streams/__init__.py +++ b/synapse/replication/tcp/streams/__init__.py @@ -24,27 +24,61 @@ Each stream is defined by the following information: current_token: The function that returns the current token for the stream update_function: The function that returns a list of updates between two tokens """ - -from . import _base, events, federation +from synapse.replication.tcp.streams._base import ( + AccountDataStream, + BackfillStream, + CachesStream, + DeviceListsStream, + GroupServerStream, + PresenceStream, + PublicRoomsStream, + PushersStream, + PushRulesStream, + ReceiptsStream, + TagAccountDataStream, + ToDeviceStream, + TypingStream, + UserSignatureStream, +) +from synapse.replication.tcp.streams.events import EventsStream +from synapse.replication.tcp.streams.federation import FederationStream STREAMS_MAP = { stream.NAME: stream for stream in ( - events.EventsStream, - _base.BackfillStream, - _base.PresenceStream, - _base.TypingStream, - _base.ReceiptsStream, - _base.PushRulesStream, - _base.PushersStream, - _base.CachesStream, - _base.PublicRoomsStream, - _base.DeviceListsStream, - _base.ToDeviceStream, - federation.FederationStream, - _base.TagAccountDataStream, - _base.AccountDataStream, - _base.GroupServerStream, - _base.UserSignatureStream, + EventsStream, + BackfillStream, + PresenceStream, + TypingStream, + ReceiptsStream, + PushRulesStream, + PushersStream, + CachesStream, + PublicRoomsStream, + DeviceListsStream, + ToDeviceStream, + FederationStream, + TagAccountDataStream, + AccountDataStream, + GroupServerStream, + UserSignatureStream, ) } + +__all__ = [ + "STREAMS_MAP", + "BackfillStream", + "PresenceStream", + "TypingStream", + "ReceiptsStream", + "PushRulesStream", + "PushersStream", + "CachesStream", + "PublicRoomsStream", + "DeviceListsStream", + "ToDeviceStream", + "TagAccountDataStream", + "AccountDataStream", + "GroupServerStream", + "UserSignatureStream", +] From 2fa55c0cc6396cab4ed74b450eb1a73b0a595ec6 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 23 Mar 2020 12:13:09 +0000 Subject: [PATCH 1221/1623] 1.12.0 --- CHANGES.md | 6 ++++++ debian/changelog | 6 ++++++ synapse/__init__.py | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index e3550497a4..9ba930e729 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,9 @@ +Synapse 1.12.0 (2020-03-23) +=========================== + +No significant changes. + + Synapse 1.12.0rc1 (2020-03-19) ============================== diff --git a/debian/changelog b/debian/changelog index c39ea8f47f..39ec9da7ab 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.12.0) stable; urgency=medium + + * New synapse release 1.12.0. + + -- Synapse Packaging team Mon, 23 Mar 2020 12:13:03 +0000 + matrix-synapse-py3 (1.11.1) stable; urgency=medium * New synapse release 1.11.1. diff --git a/synapse/__init__.py b/synapse/__init__.py index 020e0536be..5b86008945 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -36,7 +36,7 @@ try: except ImportError: pass -__version__ = "1.12.0rc1" +__version__ = "1.12.0" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From a438950a00170fb371054bc2d37373426bcbbc18 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 23 Mar 2020 13:00:40 +0000 Subject: [PATCH 1222/1623] 1.12.0 changelog --- CHANGES.md | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 9ba930e729..d94a802fa7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,7 +1,59 @@ Synapse 1.12.0 (2020-03-23) =========================== -No significant changes. +No significant changes since 1.12.0rc1. + +Debian packages and Docker images are rebuilt using the letest versions of +dependency libraries, including Twisted 20.3.0. **Please see security advisory +below**. + +Security advisory +----------------- + +Synapse may be vulnerable to request-smuggling attacks when it is used with a +reverse-proxy. The vulnerabilties are fixed in Twisted 20.3.0, and are +described in +[CVE-2020-10108](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-10108) +and +[CVE-2020-10109](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-10109). +For a good introduction to this class of request-smuggling attacks, see +https://portswigger.net/research/http-desync-attacks-request-smuggling-reborn. + +We are not aware of these vulnerabilities being exploited in the world, and +do not believe that they are exploitable with current versions of any reverse +proxies. Nevertheless, we recommend that all Synapse administrators ensure that +they have the latest versions of the Twisted library to ensure that their +installation remains secore. + +* Administrators using the [`matrix.org` Docker + image](https://hub.docker.com/r/matrixdotorg/synapse/) or the [Debian/Ubuntu + packages from + `matrix.org`](https://github.com/matrix-org/synapse/blob/master/INSTALL.md#matrixorg-packages) + should ensure that they have version 1.12.0 installed: these images include + Twisted 20.3.0. +* Administrators who have [installed Synapse from + source](https://github.com/matrix-org/synapse/blob/master/INSTALL.md#installing-from-source) + should upgrade Twisted within their virtualenv by running: + ```sh + /bin/pip install 'Twisted>=20.3.0' + ``` +* Administrators who have installed Synapse from distribution packages should + consult the information from their distributions. + +Advance notice of change to the default `git` branch for Synapse +---------------------------------------------------------------- + +Currently, the default `git` branch for Synapse is `master`, which tracks the +latest release. + +After the release of Synapse 1.13.0, we intend to change this default to +`develop`, which is the development tip. This is more consistent with common +practice and modern `git` usage. + +Although we try to keep `develop` in a stable state, there may be occasions +where regressions keep in. Developers and distributors who have scripts which +run builds using the default branch of `Synapse` should therefore consider +pinning their scripts to `master`. Synapse 1.12.0rc1 (2020-03-19) From 56b5f1d0eebb0e414badf36deed83542bbf296d1 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 23 Mar 2020 13:23:21 +0000 Subject: [PATCH 1223/1623] changelog typos --- CHANGES.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index d94a802fa7..3b66006072 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,7 +3,7 @@ Synapse 1.12.0 (2020-03-23) No significant changes since 1.12.0rc1. -Debian packages and Docker images are rebuilt using the letest versions of +Debian packages and Docker images are rebuilt using the latest versions of dependency libraries, including Twisted 20.3.0. **Please see security advisory below**. @@ -23,7 +23,7 @@ We are not aware of these vulnerabilities being exploited in the world, and do not believe that they are exploitable with current versions of any reverse proxies. Nevertheless, we recommend that all Synapse administrators ensure that they have the latest versions of the Twisted library to ensure that their -installation remains secore. +installation remains secure. * Administrators using the [`matrix.org` Docker image](https://hub.docker.com/r/matrixdotorg/synapse/) or the [Debian/Ubuntu @@ -51,7 +51,7 @@ After the release of Synapse 1.13.0, we intend to change this default to practice and modern `git` usage. Although we try to keep `develop` in a stable state, there may be occasions -where regressions keep in. Developers and distributors who have scripts which +where regressions creep in. Developers and distributors who have scripts which run builds using the default branch of `Synapse` should therefore consider pinning their scripts to `master`. From 066804f5916289d6d62cf94dfb1eb09438ce7a2a Mon Sep 17 00:00:00 2001 From: Neil Johnson Date: Mon, 23 Mar 2020 13:36:16 +0000 Subject: [PATCH 1224/1623] Update CHANGES.md --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 3b66006072..076b046d23 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -19,7 +19,7 @@ and For a good introduction to this class of request-smuggling attacks, see https://portswigger.net/research/http-desync-attacks-request-smuggling-reborn. -We are not aware of these vulnerabilities being exploited in the world, and +We are not aware of these vulnerabilities being exploited in the wild, and do not believe that they are exploitable with current versions of any reverse proxies. Nevertheless, we recommend that all Synapse administrators ensure that they have the latest versions of the Twisted library to ensure that their From 88bb6c27e1ddf67ba8620eb1d856b113214e3507 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 23 Mar 2020 13:37:52 +0000 Subject: [PATCH 1225/1623] matrix.org was fine --- CHANGES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 076b046d23..f794c585b7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -40,6 +40,8 @@ installation remains secure. * Administrators who have installed Synapse from distribution packages should consult the information from their distributions. +The `matrix.org` Synapse instance was not vulnerable to these vulnerabilities. + Advance notice of change to the default `git` branch for Synapse ---------------------------------------------------------------- From a564b92d37625855940fe599c730a9958c33f973 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 23 Mar 2020 13:59:11 +0000 Subject: [PATCH 1226/1623] Convert `*StreamRow` classes to inner classes (#7116) This just helps keep the rows closer to their streams, so that it's easier to see what the format of each stream is. --- changelog.d/7116.misc | 1 + synapse/app/generic_worker.py | 2 +- synapse/federation/send_queue.py | 2 +- synapse/replication/tcp/streams/_base.py | 181 +++++++++--------- synapse/replication/tcp/streams/federation.py | 16 +- .../replication/tcp/streams/test_receipts.py | 4 +- 6 files changed, 106 insertions(+), 100 deletions(-) create mode 100644 changelog.d/7116.misc diff --git a/changelog.d/7116.misc b/changelog.d/7116.misc new file mode 100644 index 0000000000..89d90bd49e --- /dev/null +++ b/changelog.d/7116.misc @@ -0,0 +1 @@ +Convert `*StreamRow` classes to inner classes. diff --git a/synapse/app/generic_worker.py b/synapse/app/generic_worker.py index 136babe6ce..c8fd8909a4 100644 --- a/synapse/app/generic_worker.py +++ b/synapse/app/generic_worker.py @@ -804,7 +804,7 @@ class FederationSenderHandler(object): async def _on_new_receipts(self, rows): """ Args: - rows (iterable[synapse.replication.tcp.streams.ReceiptsStreamRow]): + rows (Iterable[synapse.replication.tcp.streams.ReceiptsStream.ReceiptsStreamRow]): new receipts to be processed """ for receipt in rows: diff --git a/synapse/federation/send_queue.py b/synapse/federation/send_queue.py index 876fb0e245..e1700ca8aa 100644 --- a/synapse/federation/send_queue.py +++ b/synapse/federation/send_queue.py @@ -477,7 +477,7 @@ def process_rows_for_federation(transaction_queue, rows): Args: transaction_queue (FederationSender) - rows (list(synapse.replication.tcp.streams.FederationStreamRow)) + rows (list(synapse.replication.tcp.streams.federation.FederationStream.FederationStreamRow)) """ # The federation stream contains a bunch of different types of diff --git a/synapse/replication/tcp/streams/_base.py b/synapse/replication/tcp/streams/_base.py index abf5c6c6a8..32d9514883 100644 --- a/synapse/replication/tcp/streams/_base.py +++ b/synapse/replication/tcp/streams/_base.py @@ -28,94 +28,6 @@ logger = logging.getLogger(__name__) MAX_EVENTS_BEHIND = 500000 -BackfillStreamRow = namedtuple( - "BackfillStreamRow", - ( - "event_id", # str - "room_id", # str - "type", # str - "state_key", # str, optional - "redacts", # str, optional - "relates_to", # str, optional - ), -) -PresenceStreamRow = namedtuple( - "PresenceStreamRow", - ( - "user_id", # str - "state", # str - "last_active_ts", # int - "last_federation_update_ts", # int - "last_user_sync_ts", # int - "status_msg", # str - "currently_active", # bool - ), -) -TypingStreamRow = namedtuple( - "TypingStreamRow", ("room_id", "user_ids") # str # list(str) -) -ReceiptsStreamRow = namedtuple( - "ReceiptsStreamRow", - ( - "room_id", # str - "receipt_type", # str - "user_id", # str - "event_id", # str - "data", # dict - ), -) -PushRulesStreamRow = namedtuple("PushRulesStreamRow", ("user_id",)) # str -PushersStreamRow = namedtuple( - "PushersStreamRow", - ("user_id", "app_id", "pushkey", "deleted"), # str # str # str # bool -) - - -@attr.s -class CachesStreamRow: - """Stream to inform workers they should invalidate their cache. - - Attributes: - cache_func: Name of the cached function. - keys: The entry in the cache to invalidate. If None then will - invalidate all. - invalidation_ts: Timestamp of when the invalidation took place. - """ - - cache_func = attr.ib(type=str) - keys = attr.ib(type=Optional[List[Any]]) - invalidation_ts = attr.ib(type=int) - - -PublicRoomsStreamRow = namedtuple( - "PublicRoomsStreamRow", - ( - "room_id", # str - "visibility", # str - "appservice_id", # str, optional - "network_id", # str, optional - ), -) - - -@attr.s -class DeviceListsStreamRow: - entity = attr.ib(type=str) - - -ToDeviceStreamRow = namedtuple("ToDeviceStreamRow", ("entity",)) # str -TagAccountDataStreamRow = namedtuple( - "TagAccountDataStreamRow", ("user_id", "room_id", "data") # str # str # dict -) -AccountDataStreamRow = namedtuple( - "AccountDataStream", ("user_id", "room_id", "data_type") # str # str # str -) -GroupsStreamRow = namedtuple( - "GroupsStreamRow", - ("group_id", "user_id", "type", "content"), # str # str # str # dict -) -UserSignatureStreamRow = namedtuple("UserSignatureStreamRow", ("user_id")) # str - class Stream(object): """Base class for the streams. @@ -234,6 +146,18 @@ class BackfillStream(Stream): or it went from being an outlier to not. """ + BackfillStreamRow = namedtuple( + "BackfillStreamRow", + ( + "event_id", # str + "room_id", # str + "type", # str + "state_key", # str, optional + "redacts", # str, optional + "relates_to", # str, optional + ), + ) + NAME = "backfill" ROW_TYPE = BackfillStreamRow @@ -246,6 +170,19 @@ class BackfillStream(Stream): class PresenceStream(Stream): + PresenceStreamRow = namedtuple( + "PresenceStreamRow", + ( + "user_id", # str + "state", # str + "last_active_ts", # int + "last_federation_update_ts", # int + "last_user_sync_ts", # int + "status_msg", # str + "currently_active", # bool + ), + ) + NAME = "presence" ROW_TYPE = PresenceStreamRow @@ -260,6 +197,10 @@ class PresenceStream(Stream): class TypingStream(Stream): + TypingStreamRow = namedtuple( + "TypingStreamRow", ("room_id", "user_ids") # str # list(str) + ) + NAME = "typing" ROW_TYPE = TypingStreamRow @@ -273,6 +214,17 @@ class TypingStream(Stream): class ReceiptsStream(Stream): + ReceiptsStreamRow = namedtuple( + "ReceiptsStreamRow", + ( + "room_id", # str + "receipt_type", # str + "user_id", # str + "event_id", # str + "data", # dict + ), + ) + NAME = "receipts" ROW_TYPE = ReceiptsStreamRow @@ -289,6 +241,8 @@ class PushRulesStream(Stream): """A user has changed their push rules """ + PushRulesStreamRow = namedtuple("PushRulesStreamRow", ("user_id",)) # str + NAME = "push_rules" ROW_TYPE = PushRulesStreamRow @@ -309,6 +263,11 @@ class PushersStream(Stream): """A user has added/changed/removed a pusher """ + PushersStreamRow = namedtuple( + "PushersStreamRow", + ("user_id", "app_id", "pushkey", "deleted"), # str # str # str # bool + ) + NAME = "pushers" ROW_TYPE = PushersStreamRow @@ -326,6 +285,21 @@ class CachesStream(Stream): the cache on the workers """ + @attr.s + class CachesStreamRow: + """Stream to inform workers they should invalidate their cache. + + Attributes: + cache_func: Name of the cached function. + keys: The entry in the cache to invalidate. If None then will + invalidate all. + invalidation_ts: Timestamp of when the invalidation took place. + """ + + cache_func = attr.ib(type=str) + keys = attr.ib(type=Optional[List[Any]]) + invalidation_ts = attr.ib(type=int) + NAME = "caches" ROW_TYPE = CachesStreamRow @@ -342,6 +316,16 @@ class PublicRoomsStream(Stream): """The public rooms list changed """ + PublicRoomsStreamRow = namedtuple( + "PublicRoomsStreamRow", + ( + "room_id", # str + "visibility", # str + "appservice_id", # str, optional + "network_id", # str, optional + ), + ) + NAME = "public_rooms" ROW_TYPE = PublicRoomsStreamRow @@ -359,6 +343,10 @@ class DeviceListsStream(Stream): told about a device update. """ + @attr.s + class DeviceListsStreamRow: + entity = attr.ib(type=str) + NAME = "device_lists" ROW_TYPE = DeviceListsStreamRow @@ -375,6 +363,8 @@ class ToDeviceStream(Stream): """New to_device messages for a client """ + ToDeviceStreamRow = namedtuple("ToDeviceStreamRow", ("entity",)) # str + NAME = "to_device" ROW_TYPE = ToDeviceStreamRow @@ -391,6 +381,10 @@ class TagAccountDataStream(Stream): """Someone added/removed a tag for a room """ + TagAccountDataStreamRow = namedtuple( + "TagAccountDataStreamRow", ("user_id", "room_id", "data") # str # str # dict + ) + NAME = "tag_account_data" ROW_TYPE = TagAccountDataStreamRow @@ -407,6 +401,10 @@ class AccountDataStream(Stream): """Global or per room account data was changed """ + AccountDataStreamRow = namedtuple( + "AccountDataStream", ("user_id", "room_id", "data_type") # str # str # str + ) + NAME = "account_data" ROW_TYPE = AccountDataStreamRow @@ -432,6 +430,11 @@ class AccountDataStream(Stream): class GroupServerStream(Stream): + GroupsStreamRow = namedtuple( + "GroupsStreamRow", + ("group_id", "user_id", "type", "content"), # str # str # str # dict + ) + NAME = "groups" ROW_TYPE = GroupsStreamRow @@ -448,6 +451,8 @@ class UserSignatureStream(Stream): """A user has signed their own device with their user-signing key """ + UserSignatureStreamRow = namedtuple("UserSignatureStreamRow", ("user_id")) # str + NAME = "user_signature" ROW_TYPE = UserSignatureStreamRow diff --git a/synapse/replication/tcp/streams/federation.py b/synapse/replication/tcp/streams/federation.py index 615f3dc9ac..f5f9336430 100644 --- a/synapse/replication/tcp/streams/federation.py +++ b/synapse/replication/tcp/streams/federation.py @@ -17,20 +17,20 @@ from collections import namedtuple from ._base import Stream -FederationStreamRow = namedtuple( - "FederationStreamRow", - ( - "type", # str, the type of data as defined in the BaseFederationRows - "data", # dict, serialization of a federation.send_queue.BaseFederationRow - ), -) - class FederationStream(Stream): """Data to be sent over federation. Only available when master has federation sending disabled. """ + FederationStreamRow = namedtuple( + "FederationStreamRow", + ( + "type", # str, the type of data as defined in the BaseFederationRows + "data", # dict, serialization of a federation.send_queue.BaseFederationRow + ), + ) + NAME = "federation" ROW_TYPE = FederationStreamRow diff --git a/tests/replication/tcp/streams/test_receipts.py b/tests/replication/tcp/streams/test_receipts.py index d5a99f6caa..fa2493cad6 100644 --- a/tests/replication/tcp/streams/test_receipts.py +++ b/tests/replication/tcp/streams/test_receipts.py @@ -12,7 +12,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from synapse.replication.tcp.streams._base import ReceiptsStreamRow +from synapse.replication.tcp.streams._base import ReceiptsStream from tests.replication.tcp.streams._base import BaseStreamTestCase @@ -38,7 +38,7 @@ class ReceiptsStreamTestCase(BaseStreamTestCase): rdata_rows = self.test_handler.received_rdata_rows self.assertEqual(1, len(rdata_rows)) self.assertEqual(rdata_rows[0][0], "receipts") - row = rdata_rows[0][2] # type: ReceiptsStreamRow + row = rdata_rows[0][2] # type: ReceiptsStream.ReceiptsStreamRow self.assertEqual(ROOM_ID, row.room_id) self.assertEqual("m.read", row.receipt_type) self.assertEqual(USER_ID, row.user_id) From e341518f92132ad0b71a826857146b0bd2e56d6b Mon Sep 17 00:00:00 2001 From: "Kartikaya Gupta (kats)" Date: Mon, 23 Mar 2020 11:31:02 -0400 Subject: [PATCH 1227/1623] Update pre-built package name for FreeBSD (#7107). (#7107) Signed-off-by: Kartikaya Gupta --- INSTALL.md | 2 +- changelog.d/7107.doc | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/7107.doc diff --git a/INSTALL.md b/INSTALL.md index c0926ba590..f9e13b4cf6 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -383,7 +383,7 @@ Synapse can be found in the void repositories as 'synapse': Synapse can be installed via FreeBSD Ports or Packages contributed by Brendan Molloy from: - Ports: `cd /usr/ports/net-im/py-matrix-synapse && make install clean` - - Packages: `pkg install py27-matrix-synapse` + - Packages: `pkg install py37-matrix-synapse` ### NixOS diff --git a/changelog.d/7107.doc b/changelog.d/7107.doc new file mode 100644 index 0000000000..f6da32d406 --- /dev/null +++ b/changelog.d/7107.doc @@ -0,0 +1 @@ +Update pre-built package name for FreeBSD. From 190ab593b7a2c0d79569758c0faa4d2442bc2c5f Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Mon, 23 Mar 2020 15:21:54 -0400 Subject: [PATCH 1228/1623] Use the proper error code when a canonical alias that does not exist is used. (#7109) --- changelog.d/7109.bugfix | 1 + synapse/handlers/message.py | 57 ++++++++++++++++++++++++------------- 2 files changed, 39 insertions(+), 19 deletions(-) create mode 100644 changelog.d/7109.bugfix diff --git a/changelog.d/7109.bugfix b/changelog.d/7109.bugfix new file mode 100644 index 0000000000..268de9978e --- /dev/null +++ b/changelog.d/7109.bugfix @@ -0,0 +1 @@ +Return the proper error (M_BAD_ALIAS) when a non-existant canonical alias is provided. diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index b743fc2dcc..522271eed1 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -851,6 +851,38 @@ class EventCreationHandler(object): self.store.remove_push_actions_from_staging, event.event_id ) + @defer.inlineCallbacks + def _validate_canonical_alias( + self, directory_handler, room_alias_str, expected_room_id + ): + """ + Ensure that the given room alias points to the expected room ID. + + Args: + directory_handler: The directory handler object. + room_alias_str: The room alias to check. + expected_room_id: The room ID that the alias should point to. + """ + room_alias = RoomAlias.from_string(room_alias_str) + try: + mapping = yield directory_handler.get_association(room_alias) + except SynapseError as e: + # Turn M_NOT_FOUND errors into M_BAD_ALIAS errors. + if e.errcode == Codes.NOT_FOUND: + raise SynapseError( + 400, + "Room alias %s does not point to the room" % (room_alias_str,), + Codes.BAD_ALIAS, + ) + raise + + if mapping["room_id"] != expected_room_id: + raise SynapseError( + 400, + "Room alias %s does not point to the room" % (room_alias_str,), + Codes.BAD_ALIAS, + ) + @defer.inlineCallbacks def persist_and_notify_client_event( self, requester, event, context, ratelimit=True, extra_users=[] @@ -905,15 +937,9 @@ class EventCreationHandler(object): room_alias_str = event.content.get("alias", None) directory_handler = self.hs.get_handlers().directory_handler if room_alias_str and room_alias_str != original_alias: - room_alias = RoomAlias.from_string(room_alias_str) - mapping = yield directory_handler.get_association(room_alias) - - if mapping["room_id"] != event.room_id: - raise SynapseError( - 400, - "Room alias %s does not point to the room" % (room_alias_str,), - Codes.BAD_ALIAS, - ) + yield self._validate_canonical_alias( + directory_handler, room_alias_str, event.room_id + ) # Check that alt_aliases is the proper form. alt_aliases = event.content.get("alt_aliases", []) @@ -931,16 +957,9 @@ class EventCreationHandler(object): new_alt_aliases = set(alt_aliases) - set(original_alt_aliases) if new_alt_aliases: for alias_str in new_alt_aliases: - room_alias = RoomAlias.from_string(alias_str) - mapping = yield directory_handler.get_association(room_alias) - - if mapping["room_id"] != event.room_id: - raise SynapseError( - 400, - "Room alias %s does not point to the room" - % (room_alias_str,), - Codes.BAD_ALIAS, - ) + yield self._validate_canonical_alias( + directory_handler, alias_str, event.room_id + ) federation_handler = self.hs.get_handlers().federation_handler From c816072d47c16d2840116418698d95a855f0f24c Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 24 Mar 2020 10:35:00 +0000 Subject: [PATCH 1229/1623] Fix starting workers when federation sending not split out. --- synapse/app/generic_worker.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/synapse/app/generic_worker.py b/synapse/app/generic_worker.py index b2c764bfe8..5363642d64 100644 --- a/synapse/app/generic_worker.py +++ b/synapse/app/generic_worker.py @@ -860,6 +860,9 @@ def start(config_options): # Force the appservice to start since they will be disabled in the main config config.notify_appservices = True + else: + # For other worker types we force this to off. + config.notify_appservices = False if config.worker_app == "synapse.app.pusher": if config.start_pushers: @@ -873,6 +876,9 @@ def start(config_options): # Force the pushers to start since they will be disabled in the main config config.start_pushers = True + else: + # For other worker types we force this to off. + config.start_pushers = False if config.worker_app == "synapse.app.user_dir": if config.update_user_directory: @@ -886,6 +892,9 @@ def start(config_options): # Force the pushers to start since they will be disabled in the main config config.update_user_directory = True + else: + # For other worker types we force this to off. + config.update_user_directory = False if config.worker_app == "synapse.app.federation_sender": if config.send_federation: @@ -899,6 +908,9 @@ def start(config_options): # Force the pushers to start since they will be disabled in the main config config.send_federation = True + else: + # For other worker types we force this to off. + config.send_federation = False synapse.events.USE_FROZEN_DICTS = config.use_frozen_dicts From d6828c129ffa5bbdd8bd0ed620772f77be45c006 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 24 Mar 2020 10:36:44 +0000 Subject: [PATCH 1230/1623] Newsfile --- changelog.d/7133.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7133.bugfix diff --git a/changelog.d/7133.bugfix b/changelog.d/7133.bugfix new file mode 100644 index 0000000000..61a86fd34e --- /dev/null +++ b/changelog.d/7133.bugfix @@ -0,0 +1 @@ +Fix starting workers when federation sending not split out. From 1fcf9c6f95fcfcacab95bb78849d79b8c7fa22e9 Mon Sep 17 00:00:00 2001 From: Naugrimm Date: Tue, 24 Mar 2020 12:59:04 +0100 Subject: [PATCH 1231/1623] Fix CAS redirect url (#6634) Build the same service URL when requesting the CAS ticket and when calling the proxyValidate URL. --- changelog.d/6634.bugfix | 1 + synapse/rest/client/v1/login.py | 27 ++++++++++++++++----------- 2 files changed, 17 insertions(+), 11 deletions(-) create mode 100644 changelog.d/6634.bugfix diff --git a/changelog.d/6634.bugfix b/changelog.d/6634.bugfix new file mode 100644 index 0000000000..ec48fdc0a0 --- /dev/null +++ b/changelog.d/6634.bugfix @@ -0,0 +1 @@ +Fix single-sign on with CAS systems: pass the same service URL when requesting the CAS ticket and when calling the `proxyValidate` URL. Contributed by @Naugrimm. diff --git a/synapse/rest/client/v1/login.py b/synapse/rest/client/v1/login.py index 31551524f8..56d713462a 100644 --- a/synapse/rest/client/v1/login.py +++ b/synapse/rest/client/v1/login.py @@ -72,6 +72,14 @@ def login_id_thirdparty_from_phone(identifier): return {"type": "m.id.thirdparty", "medium": "msisdn", "address": msisdn} +def build_service_param(cas_service_url, client_redirect_url): + return "%s%s?redirectUrl=%s" % ( + cas_service_url, + "/_matrix/client/r0/login/cas/ticket", + urllib.parse.quote(client_redirect_url, safe=""), + ) + + class LoginRestServlet(RestServlet): PATTERNS = client_patterns("/login$", v1=True) CAS_TYPE = "m.login.cas" @@ -427,18 +435,15 @@ class BaseSSORedirectServlet(RestServlet): class CasRedirectServlet(BaseSSORedirectServlet): def __init__(self, hs): super(CasRedirectServlet, self).__init__() - self.cas_server_url = hs.config.cas_server_url.encode("ascii") - self.cas_service_url = hs.config.cas_service_url.encode("ascii") + self.cas_server_url = hs.config.cas_server_url + self.cas_service_url = hs.config.cas_service_url def get_sso_url(self, client_redirect_url): - client_redirect_url_param = urllib.parse.urlencode( - {b"redirectUrl": client_redirect_url} - ).encode("ascii") - hs_redirect_url = self.cas_service_url + b"/_matrix/client/r0/login/cas/ticket" - service_param = urllib.parse.urlencode( - {b"service": b"%s?%s" % (hs_redirect_url, client_redirect_url_param)} - ).encode("ascii") - return b"%s/login?%s" % (self.cas_server_url, service_param) + args = urllib.parse.urlencode( + {"service": build_service_param(self.cas_service_url, client_redirect_url)} + ) + + return "%s/login?%s" % (self.cas_server_url, args) class CasTicketServlet(RestServlet): @@ -458,7 +463,7 @@ class CasTicketServlet(RestServlet): uri = self.cas_server_url + "/proxyValidate" args = { "ticket": parse_string(request, "ticket", required=True), - "service": self.cas_service_url, + "service": build_service_param(self.cas_service_url, client_redirect_url), } try: body = await self._http_client.get_raw(uri, args) From 39230d217104f3cd7aba9065dc478f935ce1e614 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 24 Mar 2020 14:45:33 +0000 Subject: [PATCH 1232/1623] Clean up some LoggingContext stuff (#7120) * Pull Sentinel out of LoggingContext ... and drop a few unnecessary references to it * Factor out LoggingContext.current_context move `current_context` and `set_context` out to top-level functions. Mostly this means that I can more easily trace what's actually referring to LoggingContext, but I think it's generally neater. * move copy-to-parent into `stop` this really just makes `start` and `stop` more symetric. It also means that it behaves correctly if you manually `set_log_context` rather than using the context manager. * Replace `LoggingContext.alive` with `finished` Turn `alive` into `finished` and make it a bit better defined. --- changelog.d/7120.misc | 1 + docs/log_contexts.md | 5 +- synapse/crypto/keyring.py | 4 +- synapse/federation/federation_base.py | 4 +- synapse/handlers/sync.py | 4 +- synapse/http/request_metrics.py | 6 +- synapse/logging/_structured.py | 4 +- synapse/logging/context.py | 234 +++++++++--------- synapse/logging/scopecontextmanager.py | 13 +- .../storage/data_stores/main/events_worker.py | 4 +- synapse/storage/database.py | 11 +- synapse/util/metrics.py | 4 +- synapse/util/patch_inline_callbacks.py | 36 +-- tests/crypto/test_keyring.py | 7 +- .../test_matrix_federation_agent.py | 6 +- tests/http/federation/test_srv_resolver.py | 6 +- tests/http/test_fedclient.py | 6 +- tests/rest/client/test_transactions.py | 16 +- tests/unittest.py | 12 +- tests/util/caches/test_descriptors.py | 22 +- tests/util/test_async_utils.py | 15 +- tests/util/test_linearizer.py | 6 +- tests/util/test_logcontext.py | 22 +- tests/utils.py | 6 +- 24 files changed, 232 insertions(+), 222 deletions(-) create mode 100644 changelog.d/7120.misc diff --git a/changelog.d/7120.misc b/changelog.d/7120.misc new file mode 100644 index 0000000000..731f4dcb52 --- /dev/null +++ b/changelog.d/7120.misc @@ -0,0 +1 @@ +Clean up some LoggingContext code. diff --git a/docs/log_contexts.md b/docs/log_contexts.md index 5331e8c88b..fe30ca2791 100644 --- a/docs/log_contexts.md +++ b/docs/log_contexts.md @@ -29,14 +29,13 @@ from synapse.logging import context # omitted from future snippets def handle_request(request_id): request_context = context.LoggingContext() - calling_context = context.LoggingContext.current_context() - context.LoggingContext.set_current_context(request_context) + calling_context = context.set_current_context(request_context) try: request_context.request = request_id do_request_handling() logger.debug("finished") finally: - context.LoggingContext.set_current_context(calling_context) + context.set_current_context(calling_context) def do_request_handling(): logger.debug("phew") # this will be logged against request_id diff --git a/synapse/crypto/keyring.py b/synapse/crypto/keyring.py index 983f0ead8c..a9f4025bfe 100644 --- a/synapse/crypto/keyring.py +++ b/synapse/crypto/keyring.py @@ -43,8 +43,8 @@ from synapse.api.errors import ( SynapseError, ) from synapse.logging.context import ( - LoggingContext, PreserveLoggingContext, + current_context, make_deferred_yieldable, preserve_fn, run_in_background, @@ -236,7 +236,7 @@ class Keyring(object): """ try: - ctx = LoggingContext.current_context() + ctx = current_context() # map from server name to a set of outstanding request ids server_to_request_ids = {} diff --git a/synapse/federation/federation_base.py b/synapse/federation/federation_base.py index b0b0eba41e..4b115aac04 100644 --- a/synapse/federation/federation_base.py +++ b/synapse/federation/federation_base.py @@ -32,8 +32,8 @@ from synapse.events import EventBase, make_event_from_dict from synapse.events.utils import prune_event from synapse.http.servlet import assert_params_in_dict from synapse.logging.context import ( - LoggingContext, PreserveLoggingContext, + current_context, make_deferred_yieldable, ) from synapse.types import JsonDict, get_domain_from_id @@ -78,7 +78,7 @@ class FederationBase(object): """ deferreds = _check_sigs_on_pdus(self.keyring, room_version, pdus) - ctx = LoggingContext.current_context() + ctx = current_context() def callback(_, pdu: EventBase): with PreserveLoggingContext(ctx): diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index 669dbc8a48..5746fdea14 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -26,7 +26,7 @@ from prometheus_client import Counter from synapse.api.constants import EventTypes, Membership from synapse.api.filtering import FilterCollection from synapse.events import EventBase -from synapse.logging.context import LoggingContext +from synapse.logging.context import current_context from synapse.push.clientformat import format_push_rules_for_user from synapse.storage.roommember import MemberSummary from synapse.storage.state import StateFilter @@ -301,7 +301,7 @@ class SyncHandler(object): else: sync_type = "incremental_sync" - context = LoggingContext.current_context() + context = current_context() if context: context.tag = sync_type diff --git a/synapse/http/request_metrics.py b/synapse/http/request_metrics.py index 58f9cc61c8..b58ae3d9db 100644 --- a/synapse/http/request_metrics.py +++ b/synapse/http/request_metrics.py @@ -19,7 +19,7 @@ import threading from prometheus_client.core import Counter, Histogram -from synapse.logging.context import LoggingContext +from synapse.logging.context import current_context from synapse.metrics import LaterGauge logger = logging.getLogger(__name__) @@ -148,7 +148,7 @@ LaterGauge( class RequestMetrics(object): def start(self, time_sec, name, method): self.start = time_sec - self.start_context = LoggingContext.current_context() + self.start_context = current_context() self.name = name self.method = method @@ -163,7 +163,7 @@ class RequestMetrics(object): with _in_flight_requests_lock: _in_flight_requests.discard(self) - context = LoggingContext.current_context() + context = current_context() tag = "" if context: diff --git a/synapse/logging/_structured.py b/synapse/logging/_structured.py index ffa7b20ca8..7372450b45 100644 --- a/synapse/logging/_structured.py +++ b/synapse/logging/_structured.py @@ -42,7 +42,7 @@ from synapse.logging._terse_json import ( TerseJSONToConsoleLogObserver, TerseJSONToTCPLogObserver, ) -from synapse.logging.context import LoggingContext +from synapse.logging.context import current_context def stdlib_log_level_to_twisted(level: str) -> LogLevel: @@ -86,7 +86,7 @@ class LogContextObserver(object): ].startswith("Timing out client"): return - context = LoggingContext.current_context() + context = current_context() # Copy the context information to the log event. if context is not None: diff --git a/synapse/logging/context.py b/synapse/logging/context.py index 860b99a4c6..a8eafb1c7c 100644 --- a/synapse/logging/context.py +++ b/synapse/logging/context.py @@ -175,7 +175,54 @@ class ContextResourceUsage(object): return res -LoggingContextOrSentinel = Union["LoggingContext", "LoggingContext.Sentinel"] +LoggingContextOrSentinel = Union["LoggingContext", "_Sentinel"] + + +class _Sentinel(object): + """Sentinel to represent the root context""" + + __slots__ = ["previous_context", "finished", "request", "scope", "tag"] + + def __init__(self) -> None: + # Minimal set for compatibility with LoggingContext + self.previous_context = None + self.finished = False + self.request = None + self.scope = None + self.tag = None + + def __str__(self): + return "sentinel" + + def copy_to(self, record): + pass + + def copy_to_twisted_log_entry(self, record): + record["request"] = None + record["scope"] = None + + def start(self): + pass + + def stop(self): + pass + + def add_database_transaction(self, duration_sec): + pass + + def add_database_scheduled(self, sched_sec): + pass + + def record_event_fetch(self, event_count): + pass + + def __nonzero__(self): + return False + + __bool__ = __nonzero__ # python3 + + +SENTINEL_CONTEXT = _Sentinel() class LoggingContext(object): @@ -199,76 +246,33 @@ class LoggingContext(object): "_resource_usage", "usage_start", "main_thread", - "alive", + "finished", "request", "tag", "scope", ] - thread_local = threading.local() - - class Sentinel(object): - """Sentinel to represent the root context""" - - __slots__ = ["previous_context", "alive", "request", "scope", "tag"] - - def __init__(self) -> None: - # Minimal set for compatibility with LoggingContext - self.previous_context = None - self.alive = None - self.request = None - self.scope = None - self.tag = None - - def __str__(self): - return "sentinel" - - def copy_to(self, record): - pass - - def copy_to_twisted_log_entry(self, record): - record["request"] = None - record["scope"] = None - - def start(self): - pass - - def stop(self): - pass - - def add_database_transaction(self, duration_sec): - pass - - def add_database_scheduled(self, sched_sec): - pass - - def record_event_fetch(self, event_count): - pass - - def __nonzero__(self): - return False - - __bool__ = __nonzero__ # python3 - - sentinel = Sentinel() - def __init__(self, name=None, parent_context=None, request=None) -> None: - self.previous_context = LoggingContext.current_context() + self.previous_context = current_context() self.name = name # track the resources used by this context so far self._resource_usage = ContextResourceUsage() - # If alive has the thread resource usage when the logcontext last - # became active. + # The thread resource usage when the logcontext became active. None + # if the context is not currently active. self.usage_start = None self.main_thread = get_thread_id() self.request = None self.tag = "" - self.alive = True self.scope = None # type: Optional[_LogContextScope] + # keep track of whether we have hit the __exit__ block for this context + # (suggesting that the the thing that created the context thinks it should + # be finished, and that re-activating it would suggest an error). + self.finished = False + self.parent_context = parent_context if self.parent_context is not None: @@ -283,44 +287,15 @@ class LoggingContext(object): return str(self.request) return "%s@%x" % (self.name, id(self)) - @classmethod - def current_context(cls) -> LoggingContextOrSentinel: - """Get the current logging context from thread local storage - - Returns: - LoggingContext: the current logging context - """ - return getattr(cls.thread_local, "current_context", cls.sentinel) - - @classmethod - def set_current_context( - cls, context: LoggingContextOrSentinel - ) -> LoggingContextOrSentinel: - """Set the current logging context in thread local storage - Args: - context(LoggingContext): The context to activate. - Returns: - The context that was previously active - """ - current = cls.current_context() - - if current is not context: - current.stop() - cls.thread_local.current_context = context - context.start() - return current - def __enter__(self) -> "LoggingContext": """Enters this logging context into thread local storage""" - old_context = self.set_current_context(self) + old_context = set_current_context(self) if self.previous_context != old_context: logger.warning( "Expected previous context %r, found %r", self.previous_context, old_context, ) - self.alive = True - return self def __exit__(self, type, value, traceback) -> None: @@ -329,24 +304,19 @@ class LoggingContext(object): Returns: None to avoid suppressing any exceptions that were thrown. """ - current = self.set_current_context(self.previous_context) + current = set_current_context(self.previous_context) if current is not self: - if current is self.sentinel: + if current is SENTINEL_CONTEXT: logger.warning("Expected logging context %s was lost", self) else: logger.warning( "Expected logging context %s but found %s", self, current ) - self.alive = False - # if we have a parent, pass our CPU usage stats on - if self.parent_context is not None and hasattr( - self.parent_context, "_resource_usage" - ): - self.parent_context._resource_usage += self._resource_usage - - # reset them in case we get entered again - self._resource_usage.reset() + # the fact that we are here suggests that the caller thinks that everything + # is done and dusted for this logcontext, and further activity will not get + # recorded against the correct metrics. + self.finished = True def copy_to(self, record) -> None: """Copy logging fields from this context to a log record or @@ -371,9 +341,14 @@ class LoggingContext(object): logger.warning("Started logcontext %s on different thread", self) return + if self.finished: + logger.warning("Re-starting finished log context %s", self) + # If we haven't already started record the thread resource usage so # far - if not self.usage_start: + if self.usage_start: + logger.warning("Re-starting already-active log context %s", self) + else: self.usage_start = get_thread_resource_usage() def stop(self) -> None: @@ -396,6 +371,15 @@ class LoggingContext(object): self.usage_start = None + # if we have a parent, pass our CPU usage stats on + if self.parent_context is not None and hasattr( + self.parent_context, "_resource_usage" + ): + self.parent_context._resource_usage += self._resource_usage + + # reset them in case we get entered again + self._resource_usage.reset() + def get_resource_usage(self) -> ContextResourceUsage: """Get resources used by this logcontext so far. @@ -409,7 +393,7 @@ class LoggingContext(object): # If we are on the correct thread and we're currently running then we # can include resource usage so far. is_main_thread = get_thread_id() == self.main_thread - if self.alive and self.usage_start and is_main_thread: + if self.usage_start and is_main_thread: utime_delta, stime_delta = self._get_cputime() res.ru_utime += utime_delta res.ru_stime += stime_delta @@ -492,7 +476,7 @@ class LoggingContextFilter(logging.Filter): Returns: True to include the record in the log output. """ - context = LoggingContext.current_context() + context = current_context() for key, value in self.defaults.items(): setattr(record, key, value) @@ -512,27 +496,24 @@ class PreserveLoggingContext(object): __slots__ = ["current_context", "new_context", "has_parent"] - def __init__(self, new_context: Optional[LoggingContextOrSentinel] = None) -> None: - if new_context is None: - self.new_context = LoggingContext.sentinel # type: LoggingContextOrSentinel - else: - self.new_context = new_context + def __init__( + self, new_context: LoggingContextOrSentinel = SENTINEL_CONTEXT + ) -> None: + self.new_context = new_context def __enter__(self) -> None: """Captures the current logging context""" - self.current_context = LoggingContext.set_current_context(self.new_context) + self.current_context = set_current_context(self.new_context) if self.current_context: self.has_parent = self.current_context.previous_context is not None - if not self.current_context.alive: - logger.debug("Entering dead context: %s", self.current_context) def __exit__(self, type, value, traceback) -> None: """Restores the current logging context""" - context = LoggingContext.set_current_context(self.current_context) + context = set_current_context(self.current_context) if context != self.new_context: - if context is LoggingContext.sentinel: + if not context: logger.warning("Expected logging context %s was lost", self.new_context) else: logger.warning( @@ -541,9 +522,30 @@ class PreserveLoggingContext(object): context, ) - if self.current_context is not LoggingContext.sentinel: - if not self.current_context.alive: - logger.debug("Restoring dead context: %s", self.current_context) + +_thread_local = threading.local() +_thread_local.current_context = SENTINEL_CONTEXT + + +def current_context() -> LoggingContextOrSentinel: + """Get the current logging context from thread local storage""" + return getattr(_thread_local, "current_context", SENTINEL_CONTEXT) + + +def set_current_context(context: LoggingContextOrSentinel) -> LoggingContextOrSentinel: + """Set the current logging context in thread local storage + Args: + context(LoggingContext): The context to activate. + Returns: + The context that was previously active + """ + current = current_context() + + if current is not context: + current.stop() + _thread_local.current_context = context + context.start() + return current def nested_logging_context( @@ -572,7 +574,7 @@ def nested_logging_context( if parent_context is not None: context = parent_context # type: LoggingContextOrSentinel else: - context = LoggingContext.current_context() + context = current_context() return LoggingContext( parent_context=context, request=str(context.request) + "-" + suffix ) @@ -604,7 +606,7 @@ def run_in_background(f, *args, **kwargs): CRITICAL error about an unhandled error will be logged without much indication about where it came from. """ - current = LoggingContext.current_context() + current = current_context() try: res = f(*args, **kwargs) except: # noqa: E722 @@ -625,7 +627,7 @@ def run_in_background(f, *args, **kwargs): # The function may have reset the context before returning, so # we need to restore it now. - ctx = LoggingContext.set_current_context(current) + ctx = set_current_context(current) # The original context will be restored when the deferred # completes, but there is nothing waiting for it, so it will @@ -674,7 +676,7 @@ def make_deferred_yieldable(deferred): # ok, we can't be sure that a yield won't block, so let's reset the # logcontext, and add a callback to the deferred to restore it. - prev_context = LoggingContext.set_current_context(LoggingContext.sentinel) + prev_context = set_current_context(SENTINEL_CONTEXT) deferred.addBoth(_set_context_cb, prev_context) return deferred @@ -684,7 +686,7 @@ ResultT = TypeVar("ResultT") def _set_context_cb(result: ResultT, context: LoggingContext) -> ResultT: """A callback function which just sets the logging context""" - LoggingContext.set_current_context(context) + set_current_context(context) return result @@ -752,7 +754,7 @@ def defer_to_threadpool(reactor, threadpool, f, *args, **kwargs): Deferred: A Deferred which fires a callback with the result of `f`, or an errback if `f` throws an exception. """ - logcontext = LoggingContext.current_context() + logcontext = current_context() def g(): with LoggingContext(parent_context=logcontext): diff --git a/synapse/logging/scopecontextmanager.py b/synapse/logging/scopecontextmanager.py index 4eed4f2338..dc3ab00cbb 100644 --- a/synapse/logging/scopecontextmanager.py +++ b/synapse/logging/scopecontextmanager.py @@ -19,7 +19,7 @@ from opentracing import Scope, ScopeManager import twisted -from synapse.logging.context import LoggingContext, nested_logging_context +from synapse.logging.context import current_context, nested_logging_context logger = logging.getLogger(__name__) @@ -49,11 +49,8 @@ class LogContextScopeManager(ScopeManager): (Scope) : the Scope that is active, or None if not available. """ - ctx = LoggingContext.current_context() - if ctx is LoggingContext.sentinel: - return None - else: - return ctx.scope + ctx = current_context() + return ctx.scope def activate(self, span, finish_on_close): """ @@ -70,9 +67,9 @@ class LogContextScopeManager(ScopeManager): """ enter_logcontext = False - ctx = LoggingContext.current_context() + ctx = current_context() - if ctx is LoggingContext.sentinel: + if not ctx: # We don't want this scope to affect. logger.error("Tried to activate scope outside of loggingcontext") return Scope(None, span) diff --git a/synapse/storage/data_stores/main/events_worker.py b/synapse/storage/data_stores/main/events_worker.py index ca237c6f12..3013f49d32 100644 --- a/synapse/storage/data_stores/main/events_worker.py +++ b/synapse/storage/data_stores/main/events_worker.py @@ -35,7 +35,7 @@ from synapse.api.room_versions import ( ) from synapse.events import make_event_from_dict from synapse.events.utils import prune_event -from synapse.logging.context import LoggingContext, PreserveLoggingContext +from synapse.logging.context import PreserveLoggingContext, current_context from synapse.metrics.background_process_metrics import run_as_background_process from synapse.storage._base import SQLBaseStore, make_in_list_sql_clause from synapse.storage.database import Database @@ -409,7 +409,7 @@ class EventsWorkerStore(SQLBaseStore): missing_events_ids = [e for e in event_ids if e not in event_entry_map] if missing_events_ids: - log_ctx = LoggingContext.current_context() + log_ctx = current_context() log_ctx.record_event_fetch(len(missing_events_ids)) # Note that _get_events_from_db is also responsible for turning db rows diff --git a/synapse/storage/database.py b/synapse/storage/database.py index e61595336c..715c0346dd 100644 --- a/synapse/storage/database.py +++ b/synapse/storage/database.py @@ -32,6 +32,7 @@ from synapse.config.database import DatabaseConnectionConfig from synapse.logging.context import ( LoggingContext, LoggingContextOrSentinel, + current_context, make_deferred_yieldable, ) from synapse.metrics.background_process_metrics import run_as_background_process @@ -483,7 +484,7 @@ class Database(object): end = monotonic_time() duration = end - start - LoggingContext.current_context().add_database_transaction(duration) + current_context().add_database_transaction(duration) transaction_logger.debug("[TXN END] {%s} %f sec", name, duration) @@ -510,7 +511,7 @@ class Database(object): after_callbacks = [] # type: List[_CallbackListEntry] exception_callbacks = [] # type: List[_CallbackListEntry] - if LoggingContext.current_context() == LoggingContext.sentinel: + if not current_context(): logger.warning("Starting db txn '%s' from sentinel context", desc) try: @@ -547,10 +548,8 @@ class Database(object): Returns: Deferred: The result of func """ - parent_context = ( - LoggingContext.current_context() - ) # type: Optional[LoggingContextOrSentinel] - if parent_context == LoggingContext.sentinel: + parent_context = current_context() # type: Optional[LoggingContextOrSentinel] + if not parent_context: logger.warning( "Starting db connection from sentinel context: metrics will be lost" ) diff --git a/synapse/util/metrics.py b/synapse/util/metrics.py index 7b18455469..ec61e14423 100644 --- a/synapse/util/metrics.py +++ b/synapse/util/metrics.py @@ -21,7 +21,7 @@ from prometheus_client import Counter from twisted.internet import defer -from synapse.logging.context import LoggingContext +from synapse.logging.context import LoggingContext, current_context from synapse.metrics import InFlightGauge logger = logging.getLogger(__name__) @@ -106,7 +106,7 @@ class Measure(object): raise RuntimeError("Measure() objects cannot be re-used") self.start = self.clock.time() - parent_context = LoggingContext.current_context() + parent_context = current_context() self._logging_context = LoggingContext( "Measure[%s]" % (self.name,), parent_context ) diff --git a/synapse/util/patch_inline_callbacks.py b/synapse/util/patch_inline_callbacks.py index 3925927f9f..fdff195771 100644 --- a/synapse/util/patch_inline_callbacks.py +++ b/synapse/util/patch_inline_callbacks.py @@ -32,7 +32,7 @@ def do_patch(): Patch defer.inlineCallbacks so that it checks the state of the logcontext on exit """ - from synapse.logging.context import LoggingContext + from synapse.logging.context import current_context global _already_patched @@ -43,35 +43,35 @@ def do_patch(): def new_inline_callbacks(f): @functools.wraps(f) def wrapped(*args, **kwargs): - start_context = LoggingContext.current_context() + start_context = current_context() changes = [] # type: List[str] orig = orig_inline_callbacks(_check_yield_points(f, changes)) try: res = orig(*args, **kwargs) except Exception: - if LoggingContext.current_context() != start_context: + if current_context() != start_context: for err in changes: print(err, file=sys.stderr) err = "%s changed context from %s to %s on exception" % ( f, start_context, - LoggingContext.current_context(), + current_context(), ) print(err, file=sys.stderr) raise Exception(err) raise if not isinstance(res, Deferred) or res.called: - if LoggingContext.current_context() != start_context: + if current_context() != start_context: for err in changes: print(err, file=sys.stderr) err = "Completed %s changed context from %s to %s" % ( f, start_context, - LoggingContext.current_context(), + current_context(), ) # print the error to stderr because otherwise all we # see in travis-ci is the 500 error @@ -79,23 +79,23 @@ def do_patch(): raise Exception(err) return res - if LoggingContext.current_context() != LoggingContext.sentinel: + if current_context(): err = ( "%s returned incomplete deferred in non-sentinel context " "%s (start was %s)" - ) % (f, LoggingContext.current_context(), start_context) + ) % (f, current_context(), start_context) print(err, file=sys.stderr) raise Exception(err) def check_ctx(r): - if LoggingContext.current_context() != start_context: + if current_context() != start_context: for err in changes: print(err, file=sys.stderr) err = "%s completion of %s changed context from %s to %s" % ( "Failure" if isinstance(r, Failure) else "Success", f, start_context, - LoggingContext.current_context(), + current_context(), ) print(err, file=sys.stderr) raise Exception(err) @@ -127,7 +127,7 @@ def _check_yield_points(f: Callable, changes: List[str]): function """ - from synapse.logging.context import LoggingContext + from synapse.logging.context import current_context @functools.wraps(f) def check_yield_points_inner(*args, **kwargs): @@ -136,7 +136,7 @@ def _check_yield_points(f: Callable, changes: List[str]): last_yield_line_no = gen.gi_frame.f_lineno result = None # type: Any while True: - expected_context = LoggingContext.current_context() + expected_context = current_context() try: isFailure = isinstance(result, Failure) @@ -145,7 +145,7 @@ def _check_yield_points(f: Callable, changes: List[str]): else: d = gen.send(result) except (StopIteration, defer._DefGen_Return) as e: - if LoggingContext.current_context() != expected_context: + if current_context() != expected_context: # This happens when the context is lost sometime *after* the # final yield and returning. E.g. we forgot to yield on a # function that returns a deferred. @@ -159,7 +159,7 @@ def _check_yield_points(f: Callable, changes: List[str]): % ( f.__qualname__, expected_context, - LoggingContext.current_context(), + current_context(), f.__code__.co_filename, last_yield_line_no, ) @@ -173,13 +173,13 @@ def _check_yield_points(f: Callable, changes: List[str]): # This happens if we yield on a deferred that doesn't follow # the log context rules without wrapping in a `make_deferred_yieldable`. # We raise here as this should never happen. - if LoggingContext.current_context() is not LoggingContext.sentinel: + if current_context(): err = ( "%s yielded with context %s rather than sentinel," " yielded on line %d in %s" % ( frame.f_code.co_name, - LoggingContext.current_context(), + current_context(), frame.f_lineno, frame.f_code.co_filename, ) @@ -191,7 +191,7 @@ def _check_yield_points(f: Callable, changes: List[str]): except Exception as e: result = Failure(e) - if LoggingContext.current_context() != expected_context: + if current_context() != expected_context: # This happens because the context is lost sometime *after* the # previous yield and *after* the current yield. E.g. the @@ -206,7 +206,7 @@ def _check_yield_points(f: Callable, changes: List[str]): % ( frame.f_code.co_name, expected_context, - LoggingContext.current_context(), + current_context(), last_yield_line_no, frame.f_lineno, frame.f_code.co_filename, diff --git a/tests/crypto/test_keyring.py b/tests/crypto/test_keyring.py index 34d5895f18..70c8e72303 100644 --- a/tests/crypto/test_keyring.py +++ b/tests/crypto/test_keyring.py @@ -34,6 +34,7 @@ from synapse.crypto.keyring import ( from synapse.logging.context import ( LoggingContext, PreserveLoggingContext, + current_context, make_deferred_yieldable, ) from synapse.storage.keys import FetchKeyResult @@ -83,9 +84,7 @@ class KeyringTestCase(unittest.HomeserverTestCase): ) def check_context(self, _, expected): - self.assertEquals( - getattr(LoggingContext.current_context(), "request", None), expected - ) + self.assertEquals(getattr(current_context(), "request", None), expected) def test_verify_json_objects_for_server_awaits_previous_requests(self): key1 = signedjson.key.generate_signing_key(1) @@ -105,7 +104,7 @@ class KeyringTestCase(unittest.HomeserverTestCase): @defer.inlineCallbacks def get_perspectives(**kwargs): - self.assertEquals(LoggingContext.current_context().request, "11") + self.assertEquals(current_context().request, "11") with PreserveLoggingContext(): yield persp_deferred return persp_resp diff --git a/tests/http/federation/test_matrix_federation_agent.py b/tests/http/federation/test_matrix_federation_agent.py index fdc1d918ff..562397cdda 100644 --- a/tests/http/federation/test_matrix_federation_agent.py +++ b/tests/http/federation/test_matrix_federation_agent.py @@ -38,7 +38,7 @@ from synapse.http.federation.well_known_resolver import ( WellKnownResolver, _cache_period_from_headers, ) -from synapse.logging.context import LoggingContext +from synapse.logging.context import SENTINEL_CONTEXT, LoggingContext, current_context from synapse.util.caches.ttlcache import TTLCache from tests import unittest @@ -155,7 +155,7 @@ class MatrixFederationAgentTests(unittest.TestCase): self.assertNoResult(fetch_d) # should have reset logcontext to the sentinel - _check_logcontext(LoggingContext.sentinel) + _check_logcontext(SENTINEL_CONTEXT) try: fetch_res = yield fetch_d @@ -1197,7 +1197,7 @@ class TestCachePeriodFromHeaders(unittest.TestCase): def _check_logcontext(context): - current = LoggingContext.current_context() + current = current_context() if current is not context: raise AssertionError("Expected logcontext %s but was %s" % (context, current)) diff --git a/tests/http/federation/test_srv_resolver.py b/tests/http/federation/test_srv_resolver.py index df034ab237..babc201643 100644 --- a/tests/http/federation/test_srv_resolver.py +++ b/tests/http/federation/test_srv_resolver.py @@ -22,7 +22,7 @@ from twisted.internet.error import ConnectError from twisted.names import dns, error from synapse.http.federation.srv_resolver import SrvResolver -from synapse.logging.context import LoggingContext +from synapse.logging.context import SENTINEL_CONTEXT, LoggingContext, current_context from tests import unittest from tests.utils import MockClock @@ -54,12 +54,12 @@ class SrvResolverTestCase(unittest.TestCase): self.assertNoResult(resolve_d) # should have reset to the sentinel context - self.assertIs(LoggingContext.current_context(), LoggingContext.sentinel) + self.assertIs(current_context(), SENTINEL_CONTEXT) result = yield resolve_d # should have restored our context - self.assertIs(LoggingContext.current_context(), ctx) + self.assertIs(current_context(), ctx) return result diff --git a/tests/http/test_fedclient.py b/tests/http/test_fedclient.py index 2b01f40a42..fff4f0cbf4 100644 --- a/tests/http/test_fedclient.py +++ b/tests/http/test_fedclient.py @@ -29,14 +29,14 @@ from synapse.http.matrixfederationclient import ( MatrixFederationHttpClient, MatrixFederationRequest, ) -from synapse.logging.context import LoggingContext +from synapse.logging.context import SENTINEL_CONTEXT, LoggingContext, current_context from tests.server import FakeTransport from tests.unittest import HomeserverTestCase def check_logcontext(context): - current = LoggingContext.current_context() + current = current_context() if current is not context: raise AssertionError("Expected logcontext %s but was %s" % (context, current)) @@ -64,7 +64,7 @@ class FederationClientTests(HomeserverTestCase): self.assertNoResult(fetch_d) # should have reset logcontext to the sentinel - check_logcontext(LoggingContext.sentinel) + check_logcontext(SENTINEL_CONTEXT) try: fetch_res = yield fetch_d diff --git a/tests/rest/client/test_transactions.py b/tests/rest/client/test_transactions.py index a3d7e3c046..171632e195 100644 --- a/tests/rest/client/test_transactions.py +++ b/tests/rest/client/test_transactions.py @@ -2,7 +2,7 @@ from mock import Mock, call from twisted.internet import defer, reactor -from synapse.logging.context import LoggingContext +from synapse.logging.context import SENTINEL_CONTEXT, LoggingContext, current_context from synapse.rest.client.transactions import CLEANUP_PERIOD_MS, HttpTransactionCache from synapse.util import Clock @@ -52,14 +52,14 @@ class HttpTransactionCacheTestCase(unittest.TestCase): def test(): with LoggingContext("c") as c1: res = yield self.cache.fetch_or_execute(self.mock_key, cb) - self.assertIs(LoggingContext.current_context(), c1) + self.assertIs(current_context(), c1) self.assertEqual(res, "yay") # run the test twice in parallel d = defer.gatherResults([test(), test()]) - self.assertIs(LoggingContext.current_context(), LoggingContext.sentinel) + self.assertIs(current_context(), SENTINEL_CONTEXT) yield d - self.assertIs(LoggingContext.current_context(), LoggingContext.sentinel) + self.assertIs(current_context(), SENTINEL_CONTEXT) @defer.inlineCallbacks def test_does_not_cache_exceptions(self): @@ -81,11 +81,11 @@ class HttpTransactionCacheTestCase(unittest.TestCase): yield self.cache.fetch_or_execute(self.mock_key, cb) except Exception as e: self.assertEqual(e.args[0], "boo") - self.assertIs(LoggingContext.current_context(), test_context) + self.assertIs(current_context(), test_context) res = yield self.cache.fetch_or_execute(self.mock_key, cb) self.assertEqual(res, self.mock_http_response) - self.assertIs(LoggingContext.current_context(), test_context) + self.assertIs(current_context(), test_context) @defer.inlineCallbacks def test_does_not_cache_failures(self): @@ -107,11 +107,11 @@ class HttpTransactionCacheTestCase(unittest.TestCase): yield self.cache.fetch_or_execute(self.mock_key, cb) except Exception as e: self.assertEqual(e.args[0], "boo") - self.assertIs(LoggingContext.current_context(), test_context) + self.assertIs(current_context(), test_context) res = yield self.cache.fetch_or_execute(self.mock_key, cb) self.assertEqual(res, self.mock_http_response) - self.assertIs(LoggingContext.current_context(), test_context) + self.assertIs(current_context(), test_context) @defer.inlineCallbacks def test_cleans_up(self): diff --git a/tests/unittest.py b/tests/unittest.py index 8816a4d152..439174dbfc 100644 --- a/tests/unittest.py +++ b/tests/unittest.py @@ -38,7 +38,11 @@ from synapse.config.ratelimiting import FederationRateLimitConfig from synapse.federation.transport import server as federation_server from synapse.http.server import JsonResource from synapse.http.site import SynapseRequest, SynapseSite -from synapse.logging.context import LoggingContext +from synapse.logging.context import ( + SENTINEL_CONTEXT, + current_context, + set_current_context, +) from synapse.server import HomeServer from synapse.types import Requester, UserID, create_requester from synapse.util.ratelimitutils import FederationRateLimiter @@ -97,10 +101,10 @@ class TestCase(unittest.TestCase): def setUp(orig): # if we're not starting in the sentinel logcontext, then to be honest # all future bets are off. - if LoggingContext.current_context() is not LoggingContext.sentinel: + if current_context(): self.fail( "Test starting with non-sentinel logging context %s" - % (LoggingContext.current_context(),) + % (current_context(),) ) old_level = logging.getLogger().level @@ -122,7 +126,7 @@ class TestCase(unittest.TestCase): # force a GC to workaround problems with deferreds leaking logcontexts when # they are GCed (see the logcontext docs) gc.collect() - LoggingContext.set_current_context(LoggingContext.sentinel) + set_current_context(SENTINEL_CONTEXT) return ret diff --git a/tests/util/caches/test_descriptors.py b/tests/util/caches/test_descriptors.py index 39e360fe24..4d2b9e0d64 100644 --- a/tests/util/caches/test_descriptors.py +++ b/tests/util/caches/test_descriptors.py @@ -22,8 +22,10 @@ from twisted.internet import defer, reactor from synapse.api.errors import SynapseError from synapse.logging.context import ( + SENTINEL_CONTEXT, LoggingContext, PreserveLoggingContext, + current_context, make_deferred_yieldable, ) from synapse.util.caches import descriptors @@ -194,7 +196,7 @@ class DescriptorTestCase(unittest.TestCase): with LoggingContext() as c1: c1.name = "c1" r = yield obj.fn(1) - self.assertEqual(LoggingContext.current_context(), c1) + self.assertEqual(current_context(), c1) return r def check_result(r): @@ -204,12 +206,12 @@ class DescriptorTestCase(unittest.TestCase): # set off a deferred which will do a cache lookup d1 = do_lookup() - self.assertEqual(LoggingContext.current_context(), LoggingContext.sentinel) + self.assertEqual(current_context(), SENTINEL_CONTEXT) d1.addCallback(check_result) # and another d2 = do_lookup() - self.assertEqual(LoggingContext.current_context(), LoggingContext.sentinel) + self.assertEqual(current_context(), SENTINEL_CONTEXT) d2.addCallback(check_result) # let the lookup complete @@ -239,14 +241,14 @@ class DescriptorTestCase(unittest.TestCase): try: d = obj.fn(1) self.assertEqual( - LoggingContext.current_context(), LoggingContext.sentinel + current_context(), SENTINEL_CONTEXT, ) yield d self.fail("No exception thrown") except SynapseError: pass - self.assertEqual(LoggingContext.current_context(), c1) + self.assertEqual(current_context(), c1) # the cache should now be empty self.assertEqual(len(obj.fn.cache.cache), 0) @@ -255,7 +257,7 @@ class DescriptorTestCase(unittest.TestCase): # set off a deferred which will do a cache lookup d1 = do_lookup() - self.assertEqual(LoggingContext.current_context(), LoggingContext.sentinel) + self.assertEqual(current_context(), SENTINEL_CONTEXT) return d1 @@ -366,10 +368,10 @@ class CachedListDescriptorTestCase(unittest.TestCase): @descriptors.cachedList("fn", "args1", inlineCallbacks=True) def list_fn(self, args1, arg2): - assert LoggingContext.current_context().request == "c1" + assert current_context().request == "c1" # we want this to behave like an asynchronous function yield run_on_reactor() - assert LoggingContext.current_context().request == "c1" + assert current_context().request == "c1" return self.mock(args1, arg2) with LoggingContext() as c1: @@ -377,9 +379,9 @@ class CachedListDescriptorTestCase(unittest.TestCase): obj = Cls() obj.mock.return_value = {10: "fish", 20: "chips"} d1 = obj.list_fn([10, 20], 2) - self.assertEqual(LoggingContext.current_context(), LoggingContext.sentinel) + self.assertEqual(current_context(), SENTINEL_CONTEXT) r = yield d1 - self.assertEqual(LoggingContext.current_context(), c1) + self.assertEqual(current_context(), c1) obj.mock.assert_called_once_with([10, 20], 2) self.assertEqual(r, {10: "fish", 20: "chips"}) obj.mock.reset_mock() diff --git a/tests/util/test_async_utils.py b/tests/util/test_async_utils.py index f60918069a..17fd86d02d 100644 --- a/tests/util/test_async_utils.py +++ b/tests/util/test_async_utils.py @@ -16,7 +16,12 @@ from twisted.internet import defer from twisted.internet.defer import CancelledError, Deferred from twisted.internet.task import Clock -from synapse.logging.context import LoggingContext, PreserveLoggingContext +from synapse.logging.context import ( + SENTINEL_CONTEXT, + LoggingContext, + PreserveLoggingContext, + current_context, +) from synapse.util.async_helpers import timeout_deferred from tests.unittest import TestCase @@ -79,10 +84,10 @@ class TimeoutDeferredTest(TestCase): # the errbacks should be run in the test logcontext def errback(res, deferred_name): self.assertIs( - LoggingContext.current_context(), + current_context(), context_one, "errback %s run in unexpected logcontext %s" - % (deferred_name, LoggingContext.current_context()), + % (deferred_name, current_context()), ) return res @@ -90,7 +95,7 @@ class TimeoutDeferredTest(TestCase): original_deferred.addErrback(errback, "orig") timing_out_d = timeout_deferred(original_deferred, 1.0, self.clock) self.assertNoResult(timing_out_d) - self.assertIs(LoggingContext.current_context(), LoggingContext.sentinel) + self.assertIs(current_context(), SENTINEL_CONTEXT) timing_out_d.addErrback(errback, "timingout") self.clock.pump((1.0,)) @@ -99,4 +104,4 @@ class TimeoutDeferredTest(TestCase): blocking_was_cancelled[0], "non-completing deferred was not cancelled" ) self.failureResultOf(timing_out_d, defer.TimeoutError) - self.assertIs(LoggingContext.current_context(), context_one) + self.assertIs(current_context(), context_one) diff --git a/tests/util/test_linearizer.py b/tests/util/test_linearizer.py index 0ec8ef90ce..852ef23185 100644 --- a/tests/util/test_linearizer.py +++ b/tests/util/test_linearizer.py @@ -19,7 +19,7 @@ from six.moves import range from twisted.internet import defer, reactor from twisted.internet.defer import CancelledError -from synapse.logging.context import LoggingContext +from synapse.logging.context import LoggingContext, current_context from synapse.util import Clock from synapse.util.async_helpers import Linearizer @@ -54,11 +54,11 @@ class LinearizerTestCase(unittest.TestCase): def func(i, sleep=False): with LoggingContext("func(%s)" % i) as lc: with (yield linearizer.queue("")): - self.assertEqual(LoggingContext.current_context(), lc) + self.assertEqual(current_context(), lc) if sleep: yield Clock(reactor).sleep(0) - self.assertEqual(LoggingContext.current_context(), lc) + self.assertEqual(current_context(), lc) func(0, sleep=True) for i in range(1, 100): diff --git a/tests/util/test_logcontext.py b/tests/util/test_logcontext.py index 281b32c4b8..95301c013c 100644 --- a/tests/util/test_logcontext.py +++ b/tests/util/test_logcontext.py @@ -2,8 +2,10 @@ import twisted.python.failure from twisted.internet import defer, reactor from synapse.logging.context import ( + SENTINEL_CONTEXT, LoggingContext, PreserveLoggingContext, + current_context, make_deferred_yieldable, nested_logging_context, run_in_background, @@ -15,7 +17,7 @@ from .. import unittest class LoggingContextTestCase(unittest.TestCase): def _check_test_key(self, value): - self.assertEquals(LoggingContext.current_context().request, value) + self.assertEquals(current_context().request, value) def test_with_context(self): with LoggingContext() as context_one: @@ -41,7 +43,7 @@ class LoggingContextTestCase(unittest.TestCase): self._check_test_key("one") def _test_run_in_background(self, function): - sentinel_context = LoggingContext.current_context() + sentinel_context = current_context() callback_completed = [False] @@ -71,7 +73,7 @@ class LoggingContextTestCase(unittest.TestCase): # make sure that the context was reset before it got thrown back # into the reactor try: - self.assertIs(LoggingContext.current_context(), sentinel_context) + self.assertIs(current_context(), sentinel_context) d2.callback(None) except BaseException: d2.errback(twisted.python.failure.Failure()) @@ -108,7 +110,7 @@ class LoggingContextTestCase(unittest.TestCase): async def testfunc(): self._check_test_key("one") d = Clock(reactor).sleep(0) - self.assertIs(LoggingContext.current_context(), LoggingContext.sentinel) + self.assertIs(current_context(), SENTINEL_CONTEXT) await d self._check_test_key("one") @@ -129,14 +131,14 @@ class LoggingContextTestCase(unittest.TestCase): reactor.callLater(0, d.callback, None) return d - sentinel_context = LoggingContext.current_context() + sentinel_context = current_context() with LoggingContext() as context_one: context_one.request = "one" d1 = make_deferred_yieldable(blocking_function()) # make sure that the context was reset by make_deferred_yieldable - self.assertIs(LoggingContext.current_context(), sentinel_context) + self.assertIs(current_context(), sentinel_context) yield d1 @@ -145,14 +147,14 @@ class LoggingContextTestCase(unittest.TestCase): @defer.inlineCallbacks def test_make_deferred_yieldable_with_chained_deferreds(self): - sentinel_context = LoggingContext.current_context() + sentinel_context = current_context() with LoggingContext() as context_one: context_one.request = "one" d1 = make_deferred_yieldable(_chained_deferred_function()) # make sure that the context was reset by make_deferred_yieldable - self.assertIs(LoggingContext.current_context(), sentinel_context) + self.assertIs(current_context(), sentinel_context) yield d1 @@ -189,14 +191,14 @@ class LoggingContextTestCase(unittest.TestCase): reactor.callLater(0, d.callback, None) await d - sentinel_context = LoggingContext.current_context() + sentinel_context = current_context() with LoggingContext() as context_one: context_one.request = "one" d1 = make_deferred_yieldable(blocking_function()) # make sure that the context was reset by make_deferred_yieldable - self.assertIs(LoggingContext.current_context(), sentinel_context) + self.assertIs(current_context(), sentinel_context) yield d1 diff --git a/tests/utils.py b/tests/utils.py index 513f358f4f..968d109f77 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -35,7 +35,7 @@ from synapse.config.homeserver import HomeServerConfig from synapse.config.server import DEFAULT_ROOM_VERSION from synapse.federation.transport import server as federation_server from synapse.http.server import HttpServer -from synapse.logging.context import LoggingContext +from synapse.logging.context import current_context, set_current_context from synapse.server import HomeServer from synapse.storage import DataStore from synapse.storage.engines import PostgresEngine, create_engine @@ -493,10 +493,10 @@ class MockClock(object): return self.time() * 1000 def call_later(self, delay, callback, *args, **kwargs): - current_context = LoggingContext.current_context() + ctx = current_context() def wrapped_callback(): - LoggingContext.thread_local.current_context = current_context + set_current_context(ctx) callback(*args, **kwargs) t = [self.now + delay, wrapped_callback, False] From 28d9d6e8a9d6a6d5162de41cada1b6d6d4b0f941 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 24 Mar 2020 18:33:49 +0000 Subject: [PATCH 1233/1623] Remove spurious "name" parameter to `default_config` this is never set to anything other than "test", and is a source of unnecessary boilerplate. --- tests/app/test_frontend_proxy.py | 4 ++-- tests/app/test_openid_listener.py | 4 ++-- tests/federation/test_complexity.py | 4 ++-- tests/handlers/test_register.py | 2 +- tests/rest/client/v2_alpha/test_register.py | 4 ++-- tests/rest/key/v2/test_remote_key_resource.py | 4 ++-- .../server_notices/test_resource_limits_server_notices.py | 2 +- tests/test_terms_auth.py | 4 ++-- tests/unittest.py | 7 ++----- 9 files changed, 16 insertions(+), 19 deletions(-) diff --git a/tests/app/test_frontend_proxy.py b/tests/app/test_frontend_proxy.py index d3feafa1b7..be20a89682 100644 --- a/tests/app/test_frontend_proxy.py +++ b/tests/app/test_frontend_proxy.py @@ -27,8 +27,8 @@ class FrontendProxyTests(HomeserverTestCase): return hs - def default_config(self, name="test"): - c = super().default_config(name) + def default_config(self): + c = super().default_config() c["worker_app"] = "synapse.app.frontend_proxy" return c diff --git a/tests/app/test_openid_listener.py b/tests/app/test_openid_listener.py index 89fcc3889a..7364f9f1ec 100644 --- a/tests/app/test_openid_listener.py +++ b/tests/app/test_openid_listener.py @@ -29,8 +29,8 @@ class FederationReaderOpenIDListenerTests(HomeserverTestCase): ) return hs - def default_config(self, name="test"): - conf = super().default_config(name) + def default_config(self): + conf = super().default_config() # we're using FederationReaderServer, which uses a SlavedStore, so we # have to tell the FederationHandler not to try to access stuff that is only # in the primary store. diff --git a/tests/federation/test_complexity.py b/tests/federation/test_complexity.py index 24fa8dbb45..94980733c4 100644 --- a/tests/federation/test_complexity.py +++ b/tests/federation/test_complexity.py @@ -33,8 +33,8 @@ class RoomComplexityTests(unittest.FederatingHomeserverTestCase): login.register_servlets, ] - def default_config(self, name="test"): - config = super().default_config(name=name) + def default_config(self): + config = super().default_config() config["limit_remote_rooms"] = {"enabled": True, "complexity": 0.05} return config diff --git a/tests/handlers/test_register.py b/tests/handlers/test_register.py index e2915eb7b1..e7b638dbfe 100644 --- a/tests/handlers/test_register.py +++ b/tests/handlers/test_register.py @@ -34,7 +34,7 @@ class RegistrationTestCase(unittest.HomeserverTestCase): """ Tests the RegistrationHandler. """ def make_homeserver(self, reactor, clock): - hs_config = self.default_config("test") + hs_config = self.default_config() # some of the tests rely on us having a user consent version hs_config["user_consent"] = { diff --git a/tests/rest/client/v2_alpha/test_register.py b/tests/rest/client/v2_alpha/test_register.py index d0c997e385..b6ed06e02d 100644 --- a/tests/rest/client/v2_alpha/test_register.py +++ b/tests/rest/client/v2_alpha/test_register.py @@ -36,8 +36,8 @@ class RegisterRestServletTestCase(unittest.HomeserverTestCase): servlets = [register.register_servlets] url = b"/_matrix/client/r0/register" - def default_config(self, name="test"): - config = super().default_config(name) + def default_config(self): + config = super().default_config() config["allow_guest_access"] = True return config diff --git a/tests/rest/key/v2/test_remote_key_resource.py b/tests/rest/key/v2/test_remote_key_resource.py index 6776a56cad..99eb477149 100644 --- a/tests/rest/key/v2/test_remote_key_resource.py +++ b/tests/rest/key/v2/test_remote_key_resource.py @@ -143,8 +143,8 @@ class EndToEndPerspectivesTests(BaseRemoteKeyResourceTestCase): endpoint, to check that the two implementations are compatible. """ - def default_config(self, *args, **kwargs): - config = super().default_config(*args, **kwargs) + def default_config(self): + config = super().default_config() # replace the signing key with our own self.hs_signing_key = signedjson.key.generate_signing_key("kssk") diff --git a/tests/server_notices/test_resource_limits_server_notices.py b/tests/server_notices/test_resource_limits_server_notices.py index eb540e34f6..0d27b92a86 100644 --- a/tests/server_notices/test_resource_limits_server_notices.py +++ b/tests/server_notices/test_resource_limits_server_notices.py @@ -28,7 +28,7 @@ from tests import unittest class TestResourceLimitsServerNotices(unittest.HomeserverTestCase): def make_homeserver(self, reactor, clock): - hs_config = self.default_config("test") + hs_config = self.default_config() hs_config["server_notices"] = { "system_mxid_localpart": "server", "system_mxid_display_name": "test display name", diff --git a/tests/test_terms_auth.py b/tests/test_terms_auth.py index 5ec5d2b358..81d796f3f3 100644 --- a/tests/test_terms_auth.py +++ b/tests/test_terms_auth.py @@ -28,8 +28,8 @@ from tests import unittest class TermsTestCase(unittest.HomeserverTestCase): servlets = [register_servlets] - def default_config(self, name="test"): - config = super().default_config(name) + def default_config(self): + config = super().default_config() config.update( { "public_baseurl": "https://example.org/", diff --git a/tests/unittest.py b/tests/unittest.py index 8816a4d152..23b59bea22 100644 --- a/tests/unittest.py +++ b/tests/unittest.py @@ -311,14 +311,11 @@ class HomeserverTestCase(TestCase): return resource - def default_config(self, name="test"): + def default_config(self): """ Get a default HomeServer config dict. - - Args: - name (str): The homeserver name/domain. """ - config = default_config(name) + config = default_config("test") # apply any additional config which was specified via the override_config # decorator. From 7bab642707ecd985ebd736af890f4bfe2c3232fe Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Wed, 25 Mar 2020 13:56:40 +0000 Subject: [PATCH 1234/1623] Various cleanups to INSTALL.md (#7141) --- INSTALL.md | 98 ++++++++++++++++++-------------------------- changelog.d/7141.doc | 1 + 2 files changed, 40 insertions(+), 59 deletions(-) create mode 100644 changelog.d/7141.doc diff --git a/INSTALL.md b/INSTALL.md index f9e13b4cf6..af9a5ef439 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -2,7 +2,6 @@ - [Installing Synapse](#installing-synapse) - [Installing from source](#installing-from-source) - [Platform-Specific Instructions](#platform-specific-instructions) - - [Troubleshooting Installation](#troubleshooting-installation) - [Prebuilt packages](#prebuilt-packages) - [Setting up Synapse](#setting-up-synapse) - [TLS certificates](#tls-certificates) @@ -10,6 +9,7 @@ - [Registering a user](#registering-a-user) - [Setting up a TURN server](#setting-up-a-turn-server) - [URL previews](#url-previews) +- [Troubleshooting Installation](#troubleshooting-installation) # Choosing your server name @@ -70,7 +70,7 @@ pip install -U matrix-synapse ``` Before you can start Synapse, you will need to generate a configuration -file. To do this, run (in your virtualenv, as before):: +file. To do this, run (in your virtualenv, as before): ``` cd ~/synapse @@ -84,22 +84,24 @@ python -m synapse.app.homeserver \ ... substituting an appropriate value for `--server-name`. This command will generate you a config file that you can then customise, but it will -also generate a set of keys for you. These keys will allow your Home Server to -identify itself to other Home Servers, so don't lose or delete them. It would be +also generate a set of keys for you. These keys will allow your homeserver to +identify itself to other homeserver, so don't lose or delete them. It would be wise to back them up somewhere safe. (If, for whatever reason, you do need to -change your Home Server's keys, you may find that other Home Servers have the +change your homeserver's keys, you may find that other homeserver have the old key cached. If you update the signing key, you should change the name of the key in the `.signing.key` file (the second word) to something different. See the [spec](https://matrix.org/docs/spec/server_server/latest.html#retrieving-server-keys) -for more information on key management.) +for more information on key management). To actually run your new homeserver, pick a working directory for Synapse to -run (e.g. `~/synapse`), and:: +run (e.g. `~/synapse`), and: - cd ~/synapse - source env/bin/activate - synctl start +``` +cd ~/synapse +source env/bin/activate +synctl start +``` ### Platform-Specific Instructions @@ -188,7 +190,7 @@ doas pkg_add python libffi py-pip py-setuptools sqlite3 py-virtualenv \ There is currently no port for OpenBSD. Additionally, OpenBSD's security settings require a slightly more difficult installation process. -XXX: I suspect this is out of date. +(XXX: I suspect this is out of date) 1. Create a new directory in `/usr/local` called `_synapse`. Also, create a new user called `_synapse` and set that directory as the new user's home. @@ -196,7 +198,7 @@ XXX: I suspect this is out of date. write and execute permissions on the same memory space to be run from `/usr/local`. 2. `su` to the new `_synapse` user and change to their home directory. -3. Create a new virtualenv: `virtualenv -p python2.7 ~/.synapse` +3. Create a new virtualenv: `virtualenv -p python3 ~/.synapse` 4. Source the virtualenv configuration located at `/usr/local/_synapse/.synapse/bin/activate`. This is done in `ksh` by using the `.` command, rather than `bash`'s `source`. @@ -217,45 +219,6 @@ be found at https://docs.microsoft.com/en-us/windows/wsl/install-win10 for Windows 10 and https://docs.microsoft.com/en-us/windows/wsl/install-on-server for Windows Server. -### Troubleshooting Installation - -XXX a bunch of this is no longer relevant. - -Synapse requires pip 8 or later, so if your OS provides too old a version you -may need to manually upgrade it:: - - sudo pip install --upgrade pip - -Installing may fail with `Could not find any downloads that satisfy the requirement pymacaroons-pynacl (from matrix-synapse==0.12.0)`. -You can fix this by manually upgrading pip and virtualenv:: - - sudo pip install --upgrade virtualenv - -You can next rerun `virtualenv -p python3 synapse` to update the virtual env. - -Installing may fail during installing virtualenv with `InsecurePlatformWarning: A true SSLContext object is not available. This prevents urllib3 from configuring SSL appropriately and may cause certain SSL connections to fail. For more information, see https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning.` -You can fix this by manually installing ndg-httpsclient:: - - pip install --upgrade ndg-httpsclient - -Installing may fail with `mock requires setuptools>=17.1. Aborting installation`. -You can fix this by upgrading setuptools:: - - pip install --upgrade setuptools - -If pip crashes mid-installation for reason (e.g. lost terminal), pip may -refuse to run until you remove the temporary installation directory it -created. To reset the installation:: - - rm -rf /tmp/pip_install_matrix - -pip seems to leak *lots* of memory during installation. For instance, a Linux -host with 512MB of RAM may run out of memory whilst installing Twisted. If this -happens, you will have to individually install the dependencies which are -failing, e.g.:: - - pip install twisted - ## Prebuilt packages As an alternative to installing from source, prebuilt packages are available @@ -314,7 +277,7 @@ For `buster` and `sid`, Synapse is available in the Debian repositories and it should be possible to install it with simply: ``` - sudo apt install matrix-synapse +sudo apt install matrix-synapse ``` There is also a version of `matrix-synapse` in `stretch-backports`. Please see @@ -375,8 +338,10 @@ sudo pip install py-bcrypt Synapse can be found in the void repositories as 'synapse': - xbps-install -Su - xbps-install -S synapse +``` +xbps-install -Su +xbps-install -S synapse +``` ### FreeBSD @@ -420,6 +385,7 @@ so, you will need to edit `homeserver.yaml`, as follows: resources: - names: [client, federation] ``` + * You will also need to uncomment the `tls_certificate_path` and `tls_private_key_path` lines under the `TLS` section. You can either point these settings at an existing certificate and key, or you can @@ -435,7 +401,7 @@ so, you will need to edit `homeserver.yaml`, as follows: `cert.pem`). For a more detailed guide to configuring your server for federation, see -[federate.md](docs/federate.md) +[federate.md](docs/federate.md). ## Email @@ -482,7 +448,7 @@ on your server even if `enable_registration` is `false`. ## Setting up a TURN server For reliable VoIP calls to be routed via this homeserver, you MUST configure -a TURN server. See [docs/turn-howto.md](docs/turn-howto.md) for details. +a TURN server. See [docs/turn-howto.md](docs/turn-howto.md) for details. ## URL previews @@ -491,10 +457,24 @@ turn it on you must enable the `url_preview_enabled: True` config parameter and explicitly specify the IP ranges that Synapse is not allowed to spider for previewing in the `url_preview_ip_range_blacklist` configuration parameter. This is critical from a security perspective to stop arbitrary Matrix users -spidering 'internal' URLs on your network. At the very least we recommend that +spidering 'internal' URLs on your network. At the very least we recommend that your loopback and RFC1918 IP addresses are blacklisted. -This also requires the optional lxml and netaddr python dependencies to be -installed. This in turn requires the libxml2 library to be available - on +This also requires the optional `lxml` and `netaddr` python dependencies to be +installed. This in turn requires the `libxml2` library to be available - on Debian/Ubuntu this means `apt-get install libxml2-dev`, or equivalent for your OS. + +# Troubleshooting Installation + +`pip` seems to leak *lots* of memory during installation. For instance, a Linux +host with 512MB of RAM may run out of memory whilst installing Twisted. If this +happens, you will have to individually install the dependencies which are +failing, e.g.: + +``` +pip install twisted +``` + +If you have any other problems, feel free to ask in +[#synapse:matrix.org](https://matrix.to/#/#synapse:matrix.org). diff --git a/changelog.d/7141.doc b/changelog.d/7141.doc new file mode 100644 index 0000000000..2fcbd666c2 --- /dev/null +++ b/changelog.d/7141.doc @@ -0,0 +1 @@ +Clean up INSTALL.md a bit. \ No newline at end of file From 4cff617df1ba6f241fee6957cc44859f57edcc0e Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 25 Mar 2020 14:54:01 +0000 Subject: [PATCH 1235/1623] Move catchup of replication streams to worker. (#7024) This changes the replication protocol so that the server does not send down `RDATA` for rows that happened before the client connected. Instead, the server will send a `POSITION` and clients then query the database (or master out of band) to get up to date. --- changelog.d/7024.misc | 1 + docs/tcp_replication.md | 46 ++-- synapse/app/generic_worker.py | 3 + synapse/federation/sender/__init__.py | 9 + synapse/replication/http/__init__.py | 2 + synapse/replication/http/streams.py | 78 +++++++ synapse/replication/slave/storage/_base.py | 14 +- synapse/replication/slave/storage/pushers.py | 3 + synapse/replication/tcp/client.py | 3 +- synapse/replication/tcp/commands.py | 34 +-- synapse/replication/tcp/protocol.py | 206 ++++++------------ synapse/replication/tcp/resource.py | 19 +- synapse/replication/tcp/streams/__init__.py | 8 +- synapse/replication/tcp/streams/_base.py | 160 +++++++++----- synapse/replication/tcp/streams/events.py | 5 +- synapse/replication/tcp/streams/federation.py | 19 +- synapse/server.py | 5 + synapse/storage/data_stores/main/cache.py | 44 ++-- .../storage/data_stores/main/deviceinbox.py | 88 ++++---- synapse/storage/data_stores/main/events.py | 114 ---------- .../storage/data_stores/main/events_worker.py | 114 ++++++++++ synapse/storage/data_stores/main/room.py | 40 ++-- tests/replication/tcp/streams/_base.py | 55 +++-- .../replication/tcp/streams/test_receipts.py | 52 ++++- 24 files changed, 635 insertions(+), 487 deletions(-) create mode 100644 changelog.d/7024.misc create mode 100644 synapse/replication/http/streams.py diff --git a/changelog.d/7024.misc b/changelog.d/7024.misc new file mode 100644 index 0000000000..676f285377 --- /dev/null +++ b/changelog.d/7024.misc @@ -0,0 +1 @@ +Move catchup of replication streams logic to worker. diff --git a/docs/tcp_replication.md b/docs/tcp_replication.md index e3a4634b14..d4f7d9ec18 100644 --- a/docs/tcp_replication.md +++ b/docs/tcp_replication.md @@ -14,16 +14,16 @@ example flow would be (where '>' indicates master to worker and '<' worker to master flows): > SERVER example.com - < REPLICATE events 53 + < REPLICATE + > POSITION events 53 > RDATA events 54 ["$foo1:bar.com", ...] > RDATA events 55 ["$foo4:bar.com", ...] -The example shows the server accepting a new connection and sending its -identity with the `SERVER` command, followed by the client asking to -subscribe to the `events` stream from the token `53`. The server then -periodically sends `RDATA` commands which have the format -`RDATA `, where the format of `` is -defined by the individual streams. +The example shows the server accepting a new connection and sending its identity +with the `SERVER` command, followed by the client server to respond with the +position of all streams. The server then periodically sends `RDATA` commands +which have the format `RDATA `, where the format of +`` is defined by the individual streams. Error reporting happens by either the client or server sending an ERROR command, and usually the connection will be closed. @@ -32,9 +32,6 @@ Since the protocol is a simple line based, its possible to manually connect to the server using a tool like netcat. A few things should be noted when manually using the protocol: -- When subscribing to a stream using `REPLICATE`, the special token - `NOW` can be used to get all future updates. The special stream name - `ALL` can be used with `NOW` to subscribe to all available streams. - The federation stream is only available if federation sending has been disabled on the main process. - The server will only time connections out that have sent a `PING` @@ -91,9 +88,7 @@ The client: - Sends a `NAME` command, allowing the server to associate a human friendly name with the connection. This is optional. - Sends a `PING` as above -- For each stream the client wishes to subscribe to it sends a - `REPLICATE` with the `stream_name` and token it wants to subscribe - from. +- Sends a `REPLICATE` to get the current position of all streams. - On receipt of a `SERVER` command, checks that the server name matches the expected server name. @@ -140,9 +135,7 @@ the wire: > PING 1490197665618 < NAME synapse.app.appservice < PING 1490197665618 - < REPLICATE events 1 - < REPLICATE backfill 1 - < REPLICATE caches 1 + < REPLICATE > POSITION events 1 > POSITION backfill 1 > POSITION caches 1 @@ -181,9 +174,9 @@ client (C): #### POSITION (S) - The position of the stream has been updated. Sent to the client - after all missing updates for a stream have been sent to the client - and they're now up to date. + On receipt of a POSITION command clients should check if they have missed any + updates, and if so then fetch them out of band. Sent in response to a + REPLICATE command (but can happen at any time). #### ERROR (S, C) @@ -199,20 +192,7 @@ client (C): #### REPLICATE (C) -Asks the server to replicate a given stream. The syntax is: - -``` - REPLICATE -``` - -Where `` may be either: - * a numeric stream_id to stream updates since (exclusive) - * `NOW` to stream all subsequent updates. - -The `` is the name of a replication stream to subscribe -to (see [here](../synapse/replication/tcp/streams/_base.py) for a list -of streams). It can also be `ALL` to subscribe to all known streams, -in which case the `` must be set to `NOW`. +Asks the server for the current position of all streams. #### USER_SYNC (C) diff --git a/synapse/app/generic_worker.py b/synapse/app/generic_worker.py index bd1733573b..fba7ad9551 100644 --- a/synapse/app/generic_worker.py +++ b/synapse/app/generic_worker.py @@ -401,6 +401,9 @@ class GenericWorkerTyping(object): self._room_serials[row.room_id] = token self._room_typing[row.room_id] = row.user_ids + def get_current_token(self) -> int: + return self._latest_room_serial + class GenericWorkerSlavedStore( # FIXME(#3714): We need to add UserDirectoryStore as we write directly diff --git a/synapse/federation/sender/__init__.py b/synapse/federation/sender/__init__.py index 233cb33daf..a477578e44 100644 --- a/synapse/federation/sender/__init__.py +++ b/synapse/federation/sender/__init__.py @@ -499,4 +499,13 @@ class FederationSender(object): self._get_per_destination_queue(destination).attempt_new_transaction() def get_current_token(self) -> int: + # Dummy implementation for case where federation sender isn't offloaded + # to a worker. return 0 + + async def get_replication_rows( + self, from_token, to_token, limit, federation_ack=None + ): + # Dummy implementation for case where federation sender isn't offloaded + # to a worker. + return [] diff --git a/synapse/replication/http/__init__.py b/synapse/replication/http/__init__.py index 28dbc6fcba..4613b2538c 100644 --- a/synapse/replication/http/__init__.py +++ b/synapse/replication/http/__init__.py @@ -21,6 +21,7 @@ from synapse.replication.http import ( membership, register, send_event, + streams, ) REPLICATION_PREFIX = "/_synapse/replication" @@ -38,3 +39,4 @@ class ReplicationRestResource(JsonResource): login.register_servlets(hs, self) register.register_servlets(hs, self) devices.register_servlets(hs, self) + streams.register_servlets(hs, self) diff --git a/synapse/replication/http/streams.py b/synapse/replication/http/streams.py new file mode 100644 index 0000000000..ffd4c61993 --- /dev/null +++ b/synapse/replication/http/streams.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from synapse.api.errors import SynapseError +from synapse.http.servlet import parse_integer +from synapse.replication.http._base import ReplicationEndpoint + +logger = logging.getLogger(__name__) + + +class ReplicationGetStreamUpdates(ReplicationEndpoint): + """Fetches stream updates from a server. Used for streams not persisted to + the database, e.g. typing notifications. + + The API looks like: + + GET /_synapse/replication/get_repl_stream_updates/events?from_token=0&to_token=10&limit=100 + + 200 OK + + { + updates: [ ... ], + upto_token: 10, + limited: False, + } + + """ + + NAME = "get_repl_stream_updates" + PATH_ARGS = ("stream_name",) + METHOD = "GET" + + def __init__(self, hs): + super().__init__(hs) + + # We pull the streams from the replication steamer (if we try and make + # them ourselves we end up in an import loop). + self.streams = hs.get_replication_streamer().get_streams() + + @staticmethod + def _serialize_payload(stream_name, from_token, upto_token, limit): + return {"from_token": from_token, "upto_token": upto_token, "limit": limit} + + async def _handle_request(self, request, stream_name): + stream = self.streams.get(stream_name) + if stream is None: + raise SynapseError(400, "Unknown stream") + + from_token = parse_integer(request, "from_token", required=True) + upto_token = parse_integer(request, "upto_token", required=True) + limit = parse_integer(request, "limit", required=True) + + updates, upto_token, limited = await stream.get_updates_since( + from_token, upto_token, limit + ) + + return ( + 200, + {"updates": updates, "upto_token": upto_token, "limited": limited}, + ) + + +def register_servlets(hs, http_server): + ReplicationGetStreamUpdates(hs).register(http_server) diff --git a/synapse/replication/slave/storage/_base.py b/synapse/replication/slave/storage/_base.py index f45cbd37a0..751c799d94 100644 --- a/synapse/replication/slave/storage/_base.py +++ b/synapse/replication/slave/storage/_base.py @@ -18,8 +18,10 @@ from typing import Dict, Optional import six -from synapse.storage._base import SQLBaseStore -from synapse.storage.data_stores.main.cache import CURRENT_STATE_CACHE_NAME +from synapse.storage.data_stores.main.cache import ( + CURRENT_STATE_CACHE_NAME, + CacheInvalidationWorkerStore, +) from synapse.storage.database import Database from synapse.storage.engines import PostgresEngine @@ -35,7 +37,7 @@ def __func__(inp): return inp.__func__ -class BaseSlavedStore(SQLBaseStore): +class BaseSlavedStore(CacheInvalidationWorkerStore): def __init__(self, database: Database, db_conn, hs): super(BaseSlavedStore, self).__init__(database, db_conn, hs) if isinstance(self.database_engine, PostgresEngine): @@ -60,6 +62,12 @@ class BaseSlavedStore(SQLBaseStore): pos["caches"] = self._cache_id_gen.get_current_token() return pos + def get_cache_stream_token(self): + if self._cache_id_gen: + return self._cache_id_gen.get_current_token() + else: + return 0 + def process_replication_rows(self, stream_name, token, rows): if stream_name == "caches": if self._cache_id_gen: diff --git a/synapse/replication/slave/storage/pushers.py b/synapse/replication/slave/storage/pushers.py index f22c2d44a3..bce8a3d115 100644 --- a/synapse/replication/slave/storage/pushers.py +++ b/synapse/replication/slave/storage/pushers.py @@ -33,6 +33,9 @@ class SlavedPusherStore(PusherWorkerStore, BaseSlavedStore): result["pushers"] = self._pushers_id_gen.get_current_token() return result + def get_pushers_stream_token(self): + return self._pushers_id_gen.get_current_token() + def process_replication_rows(self, stream_name, token, rows): if stream_name == "pushers": self._pushers_id_gen.advance(token) diff --git a/synapse/replication/tcp/client.py b/synapse/replication/tcp/client.py index 02ab5b66ea..7e7ad0f798 100644 --- a/synapse/replication/tcp/client.py +++ b/synapse/replication/tcp/client.py @@ -55,6 +55,7 @@ class ReplicationClientFactory(ReconnectingClientFactory): self.client_name = client_name self.handler = handler self.server_name = hs.config.server_name + self.hs = hs self._clock = hs.get_clock() # As self.clock is defined in super class hs.get_reactor().addSystemEventTrigger("before", "shutdown", self.stopTrying) @@ -65,7 +66,7 @@ class ReplicationClientFactory(ReconnectingClientFactory): def buildProtocol(self, addr): logger.info("Connected to replication: %r", addr) return ClientReplicationStreamProtocol( - self.client_name, self.server_name, self._clock, self.handler + self.hs, self.client_name, self.server_name, self._clock, self.handler, ) def clientConnectionLost(self, connector, reason): diff --git a/synapse/replication/tcp/commands.py b/synapse/replication/tcp/commands.py index 451671412d..5a6b734094 100644 --- a/synapse/replication/tcp/commands.py +++ b/synapse/replication/tcp/commands.py @@ -136,8 +136,8 @@ class PositionCommand(Command): """Sent by the server to tell the client the stream postition without needing to send an RDATA. - Sent to the client after all missing updates for a stream have been sent - to the client and they're now up to date. + On receipt of a POSITION command clients should check if they have missed + any updates, and if so then fetch them out of band. """ NAME = "POSITION" @@ -179,42 +179,24 @@ class NameCommand(Command): class ReplicateCommand(Command): - """Sent by the client to subscribe to the stream. + """Sent by the client to subscribe to streams. Format:: - REPLICATE - - Where may be either: - * a numeric stream_id to stream updates from - * "NOW" to stream all subsequent updates. - - The can be "ALL" to subscribe to all known streams, in which - case the must be set to "NOW", i.e.:: - - REPLICATE ALL NOW + REPLICATE """ NAME = "REPLICATE" - def __init__(self, stream_name, token): - self.stream_name = stream_name - self.token = token + def __init__(self): + pass @classmethod def from_line(cls, line): - stream_name, token = line.split(" ", 1) - if token in ("NOW", "now"): - token = "NOW" - else: - token = int(token) - return cls(stream_name, token) + return cls() def to_line(self): - return " ".join((self.stream_name, str(self.token))) - - def get_logcontext_id(self): - return "REPLICATE-" + self.stream_name + return "" class UserSyncCommand(Command): diff --git a/synapse/replication/tcp/protocol.py b/synapse/replication/tcp/protocol.py index bc1482a9bb..f81d2e2442 100644 --- a/synapse/replication/tcp/protocol.py +++ b/synapse/replication/tcp/protocol.py @@ -35,9 +35,7 @@ indicate which side is sending, these are *not* included on the wire:: > PING 1490197665618 < NAME synapse.app.appservice < PING 1490197665618 - < REPLICATE events 1 - < REPLICATE backfill 1 - < REPLICATE caches 1 + < REPLICATE > POSITION events 1 > POSITION backfill 1 > POSITION caches 1 @@ -53,17 +51,15 @@ import fcntl import logging import struct from collections import defaultdict -from typing import Any, DefaultDict, Dict, List, Set, Tuple +from typing import Any, DefaultDict, Dict, List, Set -from six import iteritems, iterkeys +from six import iteritems from prometheus_client import Counter -from twisted.internet import defer from twisted.protocols.basic import LineOnlyReceiver from twisted.python.failure import Failure -from synapse.logging.context import make_deferred_yieldable, run_in_background from synapse.metrics import LaterGauge from synapse.metrics.background_process_metrics import run_as_background_process from synapse.replication.tcp.commands import ( @@ -82,11 +78,16 @@ from synapse.replication.tcp.commands import ( SyncCommand, UserSyncCommand, ) -from synapse.replication.tcp.streams import STREAMS_MAP +from synapse.replication.tcp.streams import STREAMS_MAP, Stream from synapse.types import Collection from synapse.util import Clock from synapse.util.stringutils import random_string +MYPY = False +if MYPY: + from synapse.server import HomeServer + + connection_close_counter = Counter( "synapse_replication_tcp_protocol_close_reason", "", ["reason_type"] ) @@ -411,16 +412,6 @@ class ServerReplicationStreamProtocol(BaseReplicationStreamProtocol): self.server_name = server_name self.streamer = streamer - # The streams the client has subscribed to and is up to date with - self.replication_streams = set() # type: Set[str] - - # The streams the client is currently subscribing to. - self.connecting_streams = set() # type: Set[str] - - # Map from stream name to list of updates to send once we've finished - # subscribing the client to the stream. - self.pending_rdata = {} # type: Dict[str, List[Tuple[int, Any]]] - def connectionMade(self): self.send_command(ServerCommand(self.server_name)) BaseReplicationStreamProtocol.connectionMade(self) @@ -436,21 +427,10 @@ class ServerReplicationStreamProtocol(BaseReplicationStreamProtocol): ) async def on_REPLICATE(self, cmd): - stream_name = cmd.stream_name - token = cmd.token - - if stream_name == "ALL": - # Subscribe to all streams we're publishing to. - deferreds = [ - run_in_background(self.subscribe_to_stream, stream, token) - for stream in iterkeys(self.streamer.streams_by_name) - ] - - await make_deferred_yieldable( - defer.gatherResults(deferreds, consumeErrors=True) - ) - else: - await self.subscribe_to_stream(stream_name, token) + # Subscribe to all streams we're publishing to. + for stream_name in self.streamer.streams_by_name: + current_token = self.streamer.get_stream_token(stream_name) + self.send_command(PositionCommand(stream_name, current_token)) async def on_FEDERATION_ACK(self, cmd): self.streamer.federation_ack(cmd.token) @@ -474,87 +454,12 @@ class ServerReplicationStreamProtocol(BaseReplicationStreamProtocol): cmd.last_seen, ) - async def subscribe_to_stream(self, stream_name, token): - """Subscribe the remote to a stream. - - This invloves checking if they've missed anything and sending those - updates down if they have. During that time new updates for the stream - are queued and sent once we've sent down any missed updates. - """ - self.replication_streams.discard(stream_name) - self.connecting_streams.add(stream_name) - - try: - # Get missing updates - updates, current_token = await self.streamer.get_stream_updates( - stream_name, token - ) - - # Send all the missing updates - for update in updates: - token, row = update[0], update[1] - self.send_command(RdataCommand(stream_name, token, row)) - - # We send a POSITION command to ensure that they have an up to - # date token (especially useful if we didn't send any updates - # above) - self.send_command(PositionCommand(stream_name, current_token)) - - # Now we can send any updates that came in while we were subscribing - pending_rdata = self.pending_rdata.pop(stream_name, []) - updates = [] - for token, update in pending_rdata: - # If the token is null, it is part of a batch update. Batches - # are multiple updates that share a single token. To denote - # this, the token is set to None for all tokens in the batch - # except for the last. If we find a None token, we keep looking - # through tokens until we find one that is not None and then - # process all previous updates in the batch as if they had the - # final token. - if token is None: - # Store this update as part of a batch - updates.append(update) - continue - - if token <= current_token: - # This update or batch of updates is older than - # current_token, dismiss it - updates = [] - continue - - updates.append(update) - - # Send all updates that are part of this batch with the - # found token - for update in updates: - self.send_command(RdataCommand(stream_name, token, update)) - - # Clear stored updates - updates = [] - - # They're now fully subscribed - self.replication_streams.add(stream_name) - except Exception as e: - logger.exception("[%s] Failed to handle REPLICATE command", self.id()) - self.send_error("failed to handle replicate: %r", e) - finally: - self.connecting_streams.discard(stream_name) - def stream_update(self, stream_name, token, data): """Called when a new update is available to stream to clients. We need to check if the client is interested in the stream or not """ - if stream_name in self.replication_streams: - # The client is subscribed to the stream - self.send_command(RdataCommand(stream_name, token, data)) - elif stream_name in self.connecting_streams: - # The client is being subscribed to the stream - logger.debug("[%s] Queuing RDATA %r %r", self.id(), stream_name, token) - self.pending_rdata.setdefault(stream_name, []).append((token, data)) - else: - # The client isn't subscribed - logger.debug("[%s] Dropping RDATA %r %r", self.id(), stream_name, token) + self.send_command(RdataCommand(stream_name, token, data)) def send_sync(self, data): self.send_command(SyncCommand(data)) @@ -638,6 +543,7 @@ class ClientReplicationStreamProtocol(BaseReplicationStreamProtocol): def __init__( self, + hs: "HomeServer", client_name: str, server_name: str, clock: Clock, @@ -649,22 +555,25 @@ class ClientReplicationStreamProtocol(BaseReplicationStreamProtocol): self.server_name = server_name self.handler = handler + self.streams = { + stream.NAME: stream(hs) for stream in STREAMS_MAP.values() + } # type: Dict[str, Stream] + # Set of stream names that have been subscribe to, but haven't yet # caught up with. This is used to track when the client has been fully # connected to the remote. - self.streams_connecting = set() # type: Set[str] + self.streams_connecting = set(STREAMS_MAP) # type: Set[str] # Map of stream to batched updates. See RdataCommand for info on how # batching works. - self.pending_batches = {} # type: Dict[str, Any] + self.pending_batches = {} # type: Dict[str, List[Any]] def connectionMade(self): self.send_command(NameCommand(self.client_name)) BaseReplicationStreamProtocol.connectionMade(self) # Once we've connected subscribe to the necessary streams - for stream_name, token in iteritems(self.handler.get_streams_to_replicate()): - self.replicate(stream_name, token) + self.replicate() # Tell the server if we have any users currently syncing (should only # happen on synchrotrons) @@ -676,10 +585,6 @@ class ClientReplicationStreamProtocol(BaseReplicationStreamProtocol): # We've now finished connecting to so inform the client handler self.handler.update_connection(self) - # This will happen if we don't actually subscribe to any streams - if not self.streams_connecting: - self.handler.finished_connecting() - async def on_SERVER(self, cmd): if cmd.data != self.server_name: logger.error("[%s] Connected to wrong remote: %r", self.id(), cmd.data) @@ -697,7 +602,7 @@ class ClientReplicationStreamProtocol(BaseReplicationStreamProtocol): ) raise - if cmd.token is None: + if cmd.token is None or stream_name in self.streams_connecting: # I.e. this is part of a batch of updates for this stream. Batch # until we get an update for the stream with a non None token self.pending_batches.setdefault(stream_name, []).append(row) @@ -707,14 +612,55 @@ class ClientReplicationStreamProtocol(BaseReplicationStreamProtocol): rows.append(row) await self.handler.on_rdata(stream_name, cmd.token, rows) - async def on_POSITION(self, cmd): - # When we get a `POSITION` command it means we've finished getting - # missing updates for the given stream, and are now up to date. + async def on_POSITION(self, cmd: PositionCommand): + stream = self.streams.get(cmd.stream_name) + if not stream: + logger.error("Got POSITION for unknown stream: %s", cmd.stream_name) + return + + # Find where we previously streamed up to. + current_token = self.handler.get_streams_to_replicate().get(cmd.stream_name) + if current_token is None: + logger.warning( + "Got POSITION for stream we're not subscribed to: %s", cmd.stream_name + ) + return + + # Fetch all updates between then and now. + limited = True + while limited: + updates, current_token, limited = await stream.get_updates_since( + current_token, cmd.token + ) + + # Check if the connection was closed underneath us, if so we bail + # rather than risk having concurrent catch ups going on. + if self.state == ConnectionStates.CLOSED: + return + + if updates: + await self.handler.on_rdata( + cmd.stream_name, + current_token, + [stream.parse_row(update[1]) for update in updates], + ) + + # We've now caught up to position sent to us, notify handler. + await self.handler.on_position(cmd.stream_name, cmd.token) + self.streams_connecting.discard(cmd.stream_name) if not self.streams_connecting: self.handler.finished_connecting() - await self.handler.on_position(cmd.stream_name, cmd.token) + # Check if the connection was closed underneath us, if so we bail + # rather than risk having concurrent catch ups going on. + if self.state == ConnectionStates.CLOSED: + return + + # Handle any RDATA that came in while we were catching up. + rows = self.pending_batches.pop(cmd.stream_name, []) + if rows: + await self.handler.on_rdata(cmd.stream_name, rows[-1].token, rows) async def on_SYNC(self, cmd): self.handler.on_sync(cmd.data) @@ -722,22 +668,12 @@ class ClientReplicationStreamProtocol(BaseReplicationStreamProtocol): async def on_REMOTE_SERVER_UP(self, cmd: RemoteServerUpCommand): self.handler.on_remote_server_up(cmd.data) - def replicate(self, stream_name, token): + def replicate(self): """Send the subscription request to the server """ - if stream_name not in STREAMS_MAP: - raise Exception("Invalid stream name %r" % (stream_name,)) + logger.info("[%s] Subscribing to replication streams", self.id()) - logger.info( - "[%s] Subscribing to replication stream: %r from %r", - self.id(), - stream_name, - token, - ) - - self.streams_connecting.add(stream_name) - - self.send_command(ReplicateCommand(stream_name, token)) + self.send_command(ReplicateCommand()) def on_connection_closed(self): BaseReplicationStreamProtocol.on_connection_closed(self) diff --git a/synapse/replication/tcp/resource.py b/synapse/replication/tcp/resource.py index 6e2ebaf614..4374e99e32 100644 --- a/synapse/replication/tcp/resource.py +++ b/synapse/replication/tcp/resource.py @@ -17,7 +17,7 @@ import logging import random -from typing import Any, List +from typing import Any, Dict, List from six import itervalues @@ -30,7 +30,7 @@ from synapse.metrics.background_process_metrics import run_as_background_process from synapse.util.metrics import Measure, measure_func from .protocol import ServerReplicationStreamProtocol -from .streams import STREAMS_MAP +from .streams import STREAMS_MAP, Stream from .streams.federation import FederationStream stream_updates_counter = Counter( @@ -52,7 +52,7 @@ class ReplicationStreamProtocolFactory(Factory): """ def __init__(self, hs): - self.streamer = ReplicationStreamer(hs) + self.streamer = hs.get_replication_streamer() self.clock = hs.get_clock() self.server_name = hs.config.server_name @@ -133,6 +133,11 @@ class ReplicationStreamer(object): for conn in self.connections: conn.send_error("server shutting down") + def get_streams(self) -> Dict[str, Stream]: + """Get a mapp from stream name to stream instance. + """ + return self.streams_by_name + def on_notifier_poke(self): """Checks if there is actually any new data and sends it to the connections if there are. @@ -190,7 +195,8 @@ class ReplicationStreamer(object): stream.current_token(), ) try: - updates, current_token = await stream.get_updates() + updates, current_token, limited = await stream.get_updates() + self.pending_updates |= limited except Exception: logger.info("Failed to handle stream %s", stream.NAME) raise @@ -226,8 +232,7 @@ class ReplicationStreamer(object): self.pending_updates = False self.is_looping = False - @measure_func("repl.get_stream_updates") - async def get_stream_updates(self, stream_name, token): + def get_stream_token(self, stream_name): """For a given stream get all updates since token. This is called when a client first subscribes to a stream. """ @@ -235,7 +240,7 @@ class ReplicationStreamer(object): if not stream: raise Exception("unknown stream %s", stream_name) - return await stream.get_updates_since(token) + return stream.current_token() @measure_func("repl.federation_ack") def federation_ack(self, token): diff --git a/synapse/replication/tcp/streams/__init__.py b/synapse/replication/tcp/streams/__init__.py index 29199f5b46..37bcd3de66 100644 --- a/synapse/replication/tcp/streams/__init__.py +++ b/synapse/replication/tcp/streams/__init__.py @@ -24,6 +24,9 @@ Each stream is defined by the following information: current_token: The function that returns the current token for the stream update_function: The function that returns a list of updates between two tokens """ + +from typing import Dict, Type + from synapse.replication.tcp.streams._base import ( AccountDataStream, BackfillStream, @@ -35,6 +38,7 @@ from synapse.replication.tcp.streams._base import ( PushersStream, PushRulesStream, ReceiptsStream, + Stream, TagAccountDataStream, ToDeviceStream, TypingStream, @@ -63,10 +67,12 @@ STREAMS_MAP = { GroupServerStream, UserSignatureStream, ) -} +} # type: Dict[str, Type[Stream]] + __all__ = [ "STREAMS_MAP", + "Stream", "BackfillStream", "PresenceStream", "TypingStream", diff --git a/synapse/replication/tcp/streams/_base.py b/synapse/replication/tcp/streams/_base.py index 32d9514883..c14dff6c64 100644 --- a/synapse/replication/tcp/streams/_base.py +++ b/synapse/replication/tcp/streams/_base.py @@ -14,13 +14,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -import itertools import logging from collections import namedtuple -from typing import Any, List, Optional, Tuple +from typing import Any, Awaitable, Callable, List, Optional, Tuple import attr +from synapse.replication.http.streams import ReplicationGetStreamUpdates from synapse.types import JsonDict logger = logging.getLogger(__name__) @@ -29,6 +29,15 @@ logger = logging.getLogger(__name__) MAX_EVENTS_BEHIND = 500000 +# Some type aliases to make things a bit easier. + +# A stream position token +Token = int + +# A pair of position in stream and args used to create an instance of `ROW_TYPE`. +StreamRow = Tuple[Token, tuple] + + class Stream(object): """Base class for the streams. @@ -56,6 +65,7 @@ class Stream(object): return cls.ROW_TYPE(*row) def __init__(self, hs): + # The token from which we last asked for updates self.last_token = self.current_token() @@ -65,61 +75,46 @@ class Stream(object): """ self.last_token = self.current_token() - async def get_updates(self): + async def get_updates(self) -> Tuple[List[Tuple[Token, JsonDict]], Token, bool]: """Gets all updates since the last time this function was called (or since the stream was constructed if it hadn't been called before). Returns: - Deferred[Tuple[List[Tuple[int, Any]], int]: - Resolves to a pair ``(updates, current_token)``, where ``updates`` is a - list of ``(token, row)`` entries. ``row`` will be json-serialised and - sent over the replication steam. + A triplet `(updates, new_last_token, limited)`, where `updates` is + a list of `(token, row)` entries, `new_last_token` is the new + position in stream, and `limited` is whether there are more updates + to fetch. """ - updates, current_token = await self.get_updates_since(self.last_token) + current_token = self.current_token() + updates, current_token, limited = await self.get_updates_since( + self.last_token, current_token + ) self.last_token = current_token - return updates, current_token + return updates, current_token, limited async def get_updates_since( - self, from_token: int - ) -> Tuple[List[Tuple[int, JsonDict]], int]: + self, from_token: Token, upto_token: Token, limit: int = 100 + ) -> Tuple[List[Tuple[Token, JsonDict]], Token, bool]: """Like get_updates except allows specifying from when we should stream updates Returns: - Resolves to a pair `(updates, new_last_token)`, where `updates` is - a list of `(token, row)` entries and `new_last_token` is the new - position in stream. + A triplet `(updates, new_last_token, limited)`, where `updates` is + a list of `(token, row)` entries, `new_last_token` is the new + position in stream, and `limited` is whether there are more updates + to fetch. """ - if from_token in ("NOW", "now"): - return [], self.current_token() - - current_token = self.current_token() - from_token = int(from_token) - if from_token == current_token: - return [], current_token + if from_token == upto_token: + return [], upto_token, False - rows = await self.update_function( - from_token, current_token, limit=MAX_EVENTS_BEHIND + 1 + updates, upto_token, limited = await self.update_function( + from_token, upto_token, limit=limit, ) - - # never turn more than MAX_EVENTS_BEHIND + 1 into updates. - rows = itertools.islice(rows, MAX_EVENTS_BEHIND + 1) - - updates = [(row[0], row[1:]) for row in rows] - - # check we didn't get more rows than the limit. - # doing it like this allows the update_function to be a generator. - if len(updates) >= MAX_EVENTS_BEHIND: - raise Exception("stream %s has fallen behind" % (self.NAME)) - - # The update function didn't hit the limit, so we must have got all - # the updates to `current_token`, and can return that as our new - # stream position. - return updates, current_token + return updates, upto_token, limited def current_token(self): """Gets the current token of the underlying streams. Should be provided @@ -141,6 +136,48 @@ class Stream(object): raise NotImplementedError() +def db_query_to_update_function( + query_function: Callable[[Token, Token, int], Awaitable[List[tuple]]] +) -> Callable[[Token, Token, int], Awaitable[Tuple[List[StreamRow], Token, bool]]]: + """Wraps a db query function which returns a list of rows to make it + suitable for use as an `update_function` for the Stream class + """ + + async def update_function(from_token, upto_token, limit): + rows = await query_function(from_token, upto_token, limit) + updates = [(row[0], row[1:]) for row in rows] + limited = False + if len(updates) == limit: + upto_token = rows[-1][0] + limited = True + + return updates, upto_token, limited + + return update_function + + +def make_http_update_function( + hs, stream_name: str +) -> Callable[[Token, Token, Token], Awaitable[Tuple[List[StreamRow], Token, bool]]]: + """Makes a suitable function for use as an `update_function` that queries + the master process for updates. + """ + + client = ReplicationGetStreamUpdates.make_client(hs) + + async def update_function( + from_token: int, upto_token: int, limit: int + ) -> Tuple[List[Tuple[int, tuple]], int, bool]: + return await client( + stream_name=stream_name, + from_token=from_token, + upto_token=upto_token, + limit=limit, + ) + + return update_function + + class BackfillStream(Stream): """We fetched some old events and either we had never seen that event before or it went from being an outlier to not. @@ -164,7 +201,7 @@ class BackfillStream(Stream): def __init__(self, hs): store = hs.get_datastore() self.current_token = store.get_current_backfill_token # type: ignore - self.update_function = store.get_all_new_backfill_event_rows # type: ignore + self.update_function = db_query_to_update_function(store.get_all_new_backfill_event_rows) # type: ignore super(BackfillStream, self).__init__(hs) @@ -190,8 +227,15 @@ class PresenceStream(Stream): store = hs.get_datastore() presence_handler = hs.get_presence_handler() + self._is_worker = hs.config.worker_app is not None + self.current_token = store.get_current_presence_token # type: ignore - self.update_function = presence_handler.get_all_presence_updates # type: ignore + + if hs.config.worker_app is None: + self.update_function = db_query_to_update_function(presence_handler.get_all_presence_updates) # type: ignore + else: + # Query master process + self.update_function = make_http_update_function(hs, self.NAME) # type: ignore super(PresenceStream, self).__init__(hs) @@ -208,7 +252,12 @@ class TypingStream(Stream): typing_handler = hs.get_typing_handler() self.current_token = typing_handler.get_current_token # type: ignore - self.update_function = typing_handler.get_all_typing_updates # type: ignore + + if hs.config.worker_app is None: + self.update_function = db_query_to_update_function(typing_handler.get_all_typing_updates) # type: ignore + else: + # Query master process + self.update_function = make_http_update_function(hs, self.NAME) # type: ignore super(TypingStream, self).__init__(hs) @@ -232,7 +281,7 @@ class ReceiptsStream(Stream): store = hs.get_datastore() self.current_token = store.get_max_receipt_stream_id # type: ignore - self.update_function = store.get_all_updated_receipts # type: ignore + self.update_function = db_query_to_update_function(store.get_all_updated_receipts) # type: ignore super(ReceiptsStream, self).__init__(hs) @@ -256,7 +305,13 @@ class PushRulesStream(Stream): async def update_function(self, from_token, to_token, limit): rows = await self.store.get_all_push_rule_updates(from_token, to_token, limit) - return [(row[0], row[2]) for row in rows] + + limited = False + if len(rows) == limit: + to_token = rows[-1][0] + limited = True + + return [(row[0], (row[2],)) for row in rows], to_token, limited class PushersStream(Stream): @@ -275,7 +330,7 @@ class PushersStream(Stream): store = hs.get_datastore() self.current_token = store.get_pushers_stream_token # type: ignore - self.update_function = store.get_all_updated_pushers_rows # type: ignore + self.update_function = db_query_to_update_function(store.get_all_updated_pushers_rows) # type: ignore super(PushersStream, self).__init__(hs) @@ -307,7 +362,7 @@ class CachesStream(Stream): store = hs.get_datastore() self.current_token = store.get_cache_stream_token # type: ignore - self.update_function = store.get_all_updated_caches # type: ignore + self.update_function = db_query_to_update_function(store.get_all_updated_caches) # type: ignore super(CachesStream, self).__init__(hs) @@ -333,7 +388,7 @@ class PublicRoomsStream(Stream): store = hs.get_datastore() self.current_token = store.get_current_public_room_stream_id # type: ignore - self.update_function = store.get_all_new_public_rooms # type: ignore + self.update_function = db_query_to_update_function(store.get_all_new_public_rooms) # type: ignore super(PublicRoomsStream, self).__init__(hs) @@ -354,7 +409,7 @@ class DeviceListsStream(Stream): store = hs.get_datastore() self.current_token = store.get_device_stream_token # type: ignore - self.update_function = store.get_all_device_list_changes_for_remotes # type: ignore + self.update_function = db_query_to_update_function(store.get_all_device_list_changes_for_remotes) # type: ignore super(DeviceListsStream, self).__init__(hs) @@ -372,7 +427,7 @@ class ToDeviceStream(Stream): store = hs.get_datastore() self.current_token = store.get_to_device_stream_token # type: ignore - self.update_function = store.get_all_new_device_messages # type: ignore + self.update_function = db_query_to_update_function(store.get_all_new_device_messages) # type: ignore super(ToDeviceStream, self).__init__(hs) @@ -392,7 +447,7 @@ class TagAccountDataStream(Stream): store = hs.get_datastore() self.current_token = store.get_max_account_data_stream_id # type: ignore - self.update_function = store.get_all_updated_tags # type: ignore + self.update_function = db_query_to_update_function(store.get_all_updated_tags) # type: ignore super(TagAccountDataStream, self).__init__(hs) @@ -412,10 +467,11 @@ class AccountDataStream(Stream): self.store = hs.get_datastore() self.current_token = self.store.get_max_account_data_stream_id # type: ignore + self.update_function = db_query_to_update_function(self._update_function) # type: ignore super(AccountDataStream, self).__init__(hs) - async def update_function(self, from_token, to_token, limit): + async def _update_function(self, from_token, to_token, limit): global_results, room_results = await self.store.get_all_updated_account_data( from_token, from_token, to_token, limit ) @@ -442,7 +498,7 @@ class GroupServerStream(Stream): store = hs.get_datastore() self.current_token = store.get_group_stream_token # type: ignore - self.update_function = store.get_all_groups_changes # type: ignore + self.update_function = db_query_to_update_function(store.get_all_groups_changes) # type: ignore super(GroupServerStream, self).__init__(hs) @@ -460,6 +516,6 @@ class UserSignatureStream(Stream): store = hs.get_datastore() self.current_token = store.get_device_stream_token # type: ignore - self.update_function = store.get_all_user_signature_changes_for_remotes # type: ignore + self.update_function = db_query_to_update_function(store.get_all_user_signature_changes_for_remotes) # type: ignore super(UserSignatureStream, self).__init__(hs) diff --git a/synapse/replication/tcp/streams/events.py b/synapse/replication/tcp/streams/events.py index b3afabb8cd..c6a595629f 100644 --- a/synapse/replication/tcp/streams/events.py +++ b/synapse/replication/tcp/streams/events.py @@ -19,7 +19,7 @@ from typing import Tuple, Type import attr -from ._base import Stream +from ._base import Stream, db_query_to_update_function """Handling of the 'events' replication stream @@ -117,10 +117,11 @@ class EventsStream(Stream): def __init__(self, hs): self._store = hs.get_datastore() self.current_token = self._store.get_current_events_token # type: ignore + self.update_function = db_query_to_update_function(self._update_function) # type: ignore super(EventsStream, self).__init__(hs) - async def update_function(self, from_token, current_token, limit=None): + async def _update_function(self, from_token, current_token, limit=None): event_rows = await self._store.get_all_new_forward_event_rows( from_token, current_token, limit ) diff --git a/synapse/replication/tcp/streams/federation.py b/synapse/replication/tcp/streams/federation.py index f5f9336430..48c1d45718 100644 --- a/synapse/replication/tcp/streams/federation.py +++ b/synapse/replication/tcp/streams/federation.py @@ -15,7 +15,9 @@ # limitations under the License. from collections import namedtuple -from ._base import Stream +from twisted.internet import defer + +from synapse.replication.tcp.streams._base import Stream, db_query_to_update_function class FederationStream(Stream): @@ -33,11 +35,18 @@ class FederationStream(Stream): NAME = "federation" ROW_TYPE = FederationStreamRow + _QUERY_MASTER = True def __init__(self, hs): - federation_sender = hs.get_federation_sender() - - self.current_token = federation_sender.get_current_token # type: ignore - self.update_function = federation_sender.get_replication_rows # type: ignore + # Not all synapse instances will have a federation sender instance, + # whether that's a `FederationSender` or a `FederationRemoteSendQueue`, + # so we stub the stream out when that is the case. + if hs.config.worker_app is None or hs.should_send_federation(): + federation_sender = hs.get_federation_sender() + self.current_token = federation_sender.get_current_token # type: ignore + self.update_function = db_query_to_update_function(federation_sender.get_replication_rows) # type: ignore + else: + self.current_token = lambda: 0 # type: ignore + self.update_function = lambda from_token, upto_token, limit: defer.succeed(([], upto_token, bool)) # type: ignore super(FederationStream, self).__init__(hs) diff --git a/synapse/server.py b/synapse/server.py index 1b980371de..9426eb1672 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -85,6 +85,7 @@ from synapse.http.matrixfederationclient import MatrixFederationHttpClient from synapse.notifier import Notifier from synapse.push.action_generator import ActionGenerator from synapse.push.pusherpool import PusherPool +from synapse.replication.tcp.resource import ReplicationStreamer from synapse.rest.media.v1.media_repository import ( MediaRepository, MediaRepositoryResource, @@ -199,6 +200,7 @@ class HomeServer(object): "saml_handler", "event_client_serializer", "storage", + "replication_streamer", ] REQUIRED_ON_MASTER_STARTUP = ["user_directory_handler", "stats_handler"] @@ -536,6 +538,9 @@ class HomeServer(object): def build_storage(self) -> Storage: return Storage(self, self.datastores) + def build_replication_streamer(self) -> ReplicationStreamer: + return ReplicationStreamer(self) + def remove_pusher(self, app_id, push_key, user_id): return self.get_pusherpool().remove_pusher(app_id, push_key, user_id) diff --git a/synapse/storage/data_stores/main/cache.py b/synapse/storage/data_stores/main/cache.py index d4c44dcc75..4dc5da3fe8 100644 --- a/synapse/storage/data_stores/main/cache.py +++ b/synapse/storage/data_stores/main/cache.py @@ -32,7 +32,29 @@ logger = logging.getLogger(__name__) CURRENT_STATE_CACHE_NAME = "cs_cache_fake" -class CacheInvalidationStore(SQLBaseStore): +class CacheInvalidationWorkerStore(SQLBaseStore): + def get_all_updated_caches(self, last_id, current_id, limit): + if last_id == current_id: + return defer.succeed([]) + + def get_all_updated_caches_txn(txn): + # We purposefully don't bound by the current token, as we want to + # send across cache invalidations as quickly as possible. Cache + # invalidations are idempotent, so duplicates are fine. + sql = ( + "SELECT stream_id, cache_func, keys, invalidation_ts" + " FROM cache_invalidation_stream" + " WHERE stream_id > ? ORDER BY stream_id ASC LIMIT ?" + ) + txn.execute(sql, (last_id, limit)) + return txn.fetchall() + + return self.db.runInteraction( + "get_all_updated_caches", get_all_updated_caches_txn + ) + + +class CacheInvalidationStore(CacheInvalidationWorkerStore): async def invalidate_cache_and_stream(self, cache_name: str, keys: Tuple[Any, ...]): """Invalidates the cache and adds it to the cache stream so slaves will know to invalidate their caches. @@ -145,26 +167,6 @@ class CacheInvalidationStore(SQLBaseStore): }, ) - def get_all_updated_caches(self, last_id, current_id, limit): - if last_id == current_id: - return defer.succeed([]) - - def get_all_updated_caches_txn(txn): - # We purposefully don't bound by the current token, as we want to - # send across cache invalidations as quickly as possible. Cache - # invalidations are idempotent, so duplicates are fine. - sql = ( - "SELECT stream_id, cache_func, keys, invalidation_ts" - " FROM cache_invalidation_stream" - " WHERE stream_id > ? ORDER BY stream_id ASC LIMIT ?" - ) - txn.execute(sql, (last_id, limit)) - return txn.fetchall() - - return self.db.runInteraction( - "get_all_updated_caches", get_all_updated_caches_txn - ) - def get_cache_stream_token(self): if self._cache_id_gen: return self._cache_id_gen.get_current_token() diff --git a/synapse/storage/data_stores/main/deviceinbox.py b/synapse/storage/data_stores/main/deviceinbox.py index 0613b49f4a..9a1178fb39 100644 --- a/synapse/storage/data_stores/main/deviceinbox.py +++ b/synapse/storage/data_stores/main/deviceinbox.py @@ -207,6 +207,50 @@ class DeviceInboxWorkerStore(SQLBaseStore): "delete_device_msgs_for_remote", delete_messages_for_remote_destination_txn ) + def get_all_new_device_messages(self, last_pos, current_pos, limit): + """ + Args: + last_pos(int): + current_pos(int): + limit(int): + Returns: + A deferred list of rows from the device inbox + """ + if last_pos == current_pos: + return defer.succeed([]) + + def get_all_new_device_messages_txn(txn): + # We limit like this as we might have multiple rows per stream_id, and + # we want to make sure we always get all entries for any stream_id + # we return. + upper_pos = min(current_pos, last_pos + limit) + sql = ( + "SELECT max(stream_id), user_id" + " FROM device_inbox" + " WHERE ? < stream_id AND stream_id <= ?" + " GROUP BY user_id" + ) + txn.execute(sql, (last_pos, upper_pos)) + rows = txn.fetchall() + + sql = ( + "SELECT max(stream_id), destination" + " FROM device_federation_outbox" + " WHERE ? < stream_id AND stream_id <= ?" + " GROUP BY destination" + ) + txn.execute(sql, (last_pos, upper_pos)) + rows.extend(txn) + + # Order by ascending stream ordering + rows.sort() + + return rows + + return self.db.runInteraction( + "get_all_new_device_messages", get_all_new_device_messages_txn + ) + class DeviceInboxBackgroundUpdateStore(SQLBaseStore): DEVICE_INBOX_STREAM_ID = "device_inbox_stream_drop" @@ -411,47 +455,3 @@ class DeviceInboxStore(DeviceInboxWorkerStore, DeviceInboxBackgroundUpdateStore) rows.append((user_id, device_id, stream_id, message_json)) txn.executemany(sql, rows) - - def get_all_new_device_messages(self, last_pos, current_pos, limit): - """ - Args: - last_pos(int): - current_pos(int): - limit(int): - Returns: - A deferred list of rows from the device inbox - """ - if last_pos == current_pos: - return defer.succeed([]) - - def get_all_new_device_messages_txn(txn): - # We limit like this as we might have multiple rows per stream_id, and - # we want to make sure we always get all entries for any stream_id - # we return. - upper_pos = min(current_pos, last_pos + limit) - sql = ( - "SELECT max(stream_id), user_id" - " FROM device_inbox" - " WHERE ? < stream_id AND stream_id <= ?" - " GROUP BY user_id" - ) - txn.execute(sql, (last_pos, upper_pos)) - rows = txn.fetchall() - - sql = ( - "SELECT max(stream_id), destination" - " FROM device_federation_outbox" - " WHERE ? < stream_id AND stream_id <= ?" - " GROUP BY destination" - ) - txn.execute(sql, (last_pos, upper_pos)) - rows.extend(txn) - - # Order by ascending stream ordering - rows.sort() - - return rows - - return self.db.runInteraction( - "get_all_new_device_messages", get_all_new_device_messages_txn - ) diff --git a/synapse/storage/data_stores/main/events.py b/synapse/storage/data_stores/main/events.py index d593ef47b8..e71c23541d 100644 --- a/synapse/storage/data_stores/main/events.py +++ b/synapse/storage/data_stores/main/events.py @@ -1267,104 +1267,6 @@ class EventsStore( ret = yield self.db.runInteraction("count_daily_active_rooms", _count) return ret - def get_current_backfill_token(self): - """The current minimum token that backfilled events have reached""" - return -self._backfill_id_gen.get_current_token() - - def get_current_events_token(self): - """The current maximum token that events have reached""" - return self._stream_id_gen.get_current_token() - - def get_all_new_forward_event_rows(self, last_id, current_id, limit): - if last_id == current_id: - return defer.succeed([]) - - def get_all_new_forward_event_rows(txn): - sql = ( - "SELECT e.stream_ordering, e.event_id, e.room_id, e.type," - " state_key, redacts, relates_to_id" - " FROM events AS e" - " LEFT JOIN redactions USING (event_id)" - " LEFT JOIN state_events USING (event_id)" - " LEFT JOIN event_relations USING (event_id)" - " WHERE ? < stream_ordering AND stream_ordering <= ?" - " ORDER BY stream_ordering ASC" - " LIMIT ?" - ) - txn.execute(sql, (last_id, current_id, limit)) - new_event_updates = txn.fetchall() - - if len(new_event_updates) == limit: - upper_bound = new_event_updates[-1][0] - else: - upper_bound = current_id - - sql = ( - "SELECT event_stream_ordering, e.event_id, e.room_id, e.type," - " state_key, redacts, relates_to_id" - " FROM events AS e" - " INNER JOIN ex_outlier_stream USING (event_id)" - " LEFT JOIN redactions USING (event_id)" - " LEFT JOIN state_events USING (event_id)" - " LEFT JOIN event_relations USING (event_id)" - " WHERE ? < event_stream_ordering" - " AND event_stream_ordering <= ?" - " ORDER BY event_stream_ordering DESC" - ) - txn.execute(sql, (last_id, upper_bound)) - new_event_updates.extend(txn) - - return new_event_updates - - return self.db.runInteraction( - "get_all_new_forward_event_rows", get_all_new_forward_event_rows - ) - - def get_all_new_backfill_event_rows(self, last_id, current_id, limit): - if last_id == current_id: - return defer.succeed([]) - - def get_all_new_backfill_event_rows(txn): - sql = ( - "SELECT -e.stream_ordering, e.event_id, e.room_id, e.type," - " state_key, redacts, relates_to_id" - " FROM events AS e" - " LEFT JOIN redactions USING (event_id)" - " LEFT JOIN state_events USING (event_id)" - " LEFT JOIN event_relations USING (event_id)" - " WHERE ? > stream_ordering AND stream_ordering >= ?" - " ORDER BY stream_ordering ASC" - " LIMIT ?" - ) - txn.execute(sql, (-last_id, -current_id, limit)) - new_event_updates = txn.fetchall() - - if len(new_event_updates) == limit: - upper_bound = new_event_updates[-1][0] - else: - upper_bound = current_id - - sql = ( - "SELECT -event_stream_ordering, e.event_id, e.room_id, e.type," - " state_key, redacts, relates_to_id" - " FROM events AS e" - " INNER JOIN ex_outlier_stream USING (event_id)" - " LEFT JOIN redactions USING (event_id)" - " LEFT JOIN state_events USING (event_id)" - " LEFT JOIN event_relations USING (event_id)" - " WHERE ? > event_stream_ordering" - " AND event_stream_ordering >= ?" - " ORDER BY event_stream_ordering DESC" - ) - txn.execute(sql, (-last_id, -upper_bound)) - new_event_updates.extend(txn.fetchall()) - - return new_event_updates - - return self.db.runInteraction( - "get_all_new_backfill_event_rows", get_all_new_backfill_event_rows - ) - @cached(num_args=5, max_entries=10) def get_all_new_events( self, @@ -1850,22 +1752,6 @@ class EventsStore( return (int(res["topological_ordering"]), int(res["stream_ordering"])) - def get_all_updated_current_state_deltas(self, from_token, to_token, limit): - def get_all_updated_current_state_deltas_txn(txn): - sql = """ - SELECT stream_id, room_id, type, state_key, event_id - FROM current_state_delta_stream - WHERE ? < stream_id AND stream_id <= ? - ORDER BY stream_id ASC LIMIT ? - """ - txn.execute(sql, (from_token, to_token, limit)) - return txn.fetchall() - - return self.db.runInteraction( - "get_all_updated_current_state_deltas", - get_all_updated_current_state_deltas_txn, - ) - def insert_labels_for_event_txn( self, txn, event_id, labels, room_id, topological_ordering ): diff --git a/synapse/storage/data_stores/main/events_worker.py b/synapse/storage/data_stores/main/events_worker.py index 3013f49d32..16ea8948b1 100644 --- a/synapse/storage/data_stores/main/events_worker.py +++ b/synapse/storage/data_stores/main/events_worker.py @@ -963,3 +963,117 @@ class EventsWorkerStore(SQLBaseStore): complexity_v1 = round(state_events / 500, 2) return {"v1": complexity_v1} + + def get_current_backfill_token(self): + """The current minimum token that backfilled events have reached""" + return -self._backfill_id_gen.get_current_token() + + def get_current_events_token(self): + """The current maximum token that events have reached""" + return self._stream_id_gen.get_current_token() + + def get_all_new_forward_event_rows(self, last_id, current_id, limit): + if last_id == current_id: + return defer.succeed([]) + + def get_all_new_forward_event_rows(txn): + sql = ( + "SELECT e.stream_ordering, e.event_id, e.room_id, e.type," + " state_key, redacts, relates_to_id" + " FROM events AS e" + " LEFT JOIN redactions USING (event_id)" + " LEFT JOIN state_events USING (event_id)" + " LEFT JOIN event_relations USING (event_id)" + " WHERE ? < stream_ordering AND stream_ordering <= ?" + " ORDER BY stream_ordering ASC" + " LIMIT ?" + ) + txn.execute(sql, (last_id, current_id, limit)) + new_event_updates = txn.fetchall() + + if len(new_event_updates) == limit: + upper_bound = new_event_updates[-1][0] + else: + upper_bound = current_id + + sql = ( + "SELECT event_stream_ordering, e.event_id, e.room_id, e.type," + " state_key, redacts, relates_to_id" + " FROM events AS e" + " INNER JOIN ex_outlier_stream USING (event_id)" + " LEFT JOIN redactions USING (event_id)" + " LEFT JOIN state_events USING (event_id)" + " LEFT JOIN event_relations USING (event_id)" + " WHERE ? < event_stream_ordering" + " AND event_stream_ordering <= ?" + " ORDER BY event_stream_ordering DESC" + ) + txn.execute(sql, (last_id, upper_bound)) + new_event_updates.extend(txn) + + return new_event_updates + + return self.db.runInteraction( + "get_all_new_forward_event_rows", get_all_new_forward_event_rows + ) + + def get_all_new_backfill_event_rows(self, last_id, current_id, limit): + if last_id == current_id: + return defer.succeed([]) + + def get_all_new_backfill_event_rows(txn): + sql = ( + "SELECT -e.stream_ordering, e.event_id, e.room_id, e.type," + " state_key, redacts, relates_to_id" + " FROM events AS e" + " LEFT JOIN redactions USING (event_id)" + " LEFT JOIN state_events USING (event_id)" + " LEFT JOIN event_relations USING (event_id)" + " WHERE ? > stream_ordering AND stream_ordering >= ?" + " ORDER BY stream_ordering ASC" + " LIMIT ?" + ) + txn.execute(sql, (-last_id, -current_id, limit)) + new_event_updates = txn.fetchall() + + if len(new_event_updates) == limit: + upper_bound = new_event_updates[-1][0] + else: + upper_bound = current_id + + sql = ( + "SELECT -event_stream_ordering, e.event_id, e.room_id, e.type," + " state_key, redacts, relates_to_id" + " FROM events AS e" + " INNER JOIN ex_outlier_stream USING (event_id)" + " LEFT JOIN redactions USING (event_id)" + " LEFT JOIN state_events USING (event_id)" + " LEFT JOIN event_relations USING (event_id)" + " WHERE ? > event_stream_ordering" + " AND event_stream_ordering >= ?" + " ORDER BY event_stream_ordering DESC" + ) + txn.execute(sql, (-last_id, -upper_bound)) + new_event_updates.extend(txn.fetchall()) + + return new_event_updates + + return self.db.runInteraction( + "get_all_new_backfill_event_rows", get_all_new_backfill_event_rows + ) + + def get_all_updated_current_state_deltas(self, from_token, to_token, limit): + def get_all_updated_current_state_deltas_txn(txn): + sql = """ + SELECT stream_id, room_id, type, state_key, event_id + FROM current_state_delta_stream + WHERE ? < stream_id AND stream_id <= ? + ORDER BY stream_id ASC LIMIT ? + """ + txn.execute(sql, (from_token, to_token, limit)) + return txn.fetchall() + + return self.db.runInteraction( + "get_all_updated_current_state_deltas", + get_all_updated_current_state_deltas_txn, + ) diff --git a/synapse/storage/data_stores/main/room.py b/synapse/storage/data_stores/main/room.py index e6c10c6316..aaebe427d3 100644 --- a/synapse/storage/data_stores/main/room.py +++ b/synapse/storage/data_stores/main/room.py @@ -732,6 +732,26 @@ class RoomWorkerStore(SQLBaseStore): return total_media_quarantined + def get_all_new_public_rooms(self, prev_id, current_id, limit): + def get_all_new_public_rooms(txn): + sql = """ + SELECT stream_id, room_id, visibility, appservice_id, network_id + FROM public_room_list_stream + WHERE stream_id > ? AND stream_id <= ? + ORDER BY stream_id ASC + LIMIT ? + """ + + txn.execute(sql, (prev_id, current_id, limit)) + return txn.fetchall() + + if prev_id == current_id: + return defer.succeed([]) + + return self.db.runInteraction( + "get_all_new_public_rooms", get_all_new_public_rooms + ) + class RoomBackgroundUpdateStore(SQLBaseStore): REMOVE_TOMESTONED_ROOMS_BG_UPDATE = "remove_tombstoned_rooms_from_directory" @@ -1249,26 +1269,6 @@ class RoomStore(RoomBackgroundUpdateStore, RoomWorkerStore, SearchStore): def get_current_public_room_stream_id(self): return self._public_room_id_gen.get_current_token() - def get_all_new_public_rooms(self, prev_id, current_id, limit): - def get_all_new_public_rooms(txn): - sql = """ - SELECT stream_id, room_id, visibility, appservice_id, network_id - FROM public_room_list_stream - WHERE stream_id > ? AND stream_id <= ? - ORDER BY stream_id ASC - LIMIT ? - """ - - txn.execute(sql, (prev_id, current_id, limit)) - return txn.fetchall() - - if prev_id == current_id: - return defer.succeed([]) - - return self.db.runInteraction( - "get_all_new_public_rooms", get_all_new_public_rooms - ) - @defer.inlineCallbacks def block_room(self, room_id, user_id): """Marks the room as blocked. Can be called multiple times. diff --git a/tests/replication/tcp/streams/_base.py b/tests/replication/tcp/streams/_base.py index e96ad4ca4e..a755fe2879 100644 --- a/tests/replication/tcp/streams/_base.py +++ b/tests/replication/tcp/streams/_base.py @@ -12,6 +12,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + from mock import Mock from synapse.replication.tcp.commands import ReplicateCommand @@ -29,19 +30,37 @@ class BaseStreamTestCase(unittest.HomeserverTestCase): # build a replication server server_factory = ReplicationStreamProtocolFactory(self.hs) self.streamer = server_factory.streamer - server = server_factory.buildProtocol(None) + self.server = server_factory.buildProtocol(None) - # build a replication client, with a dummy handler - handler_factory = Mock() - self.test_handler = TestReplicationClientHandler() - self.test_handler.factory = handler_factory + self.test_handler = Mock(wraps=TestReplicationClientHandler()) self.client = ClientReplicationStreamProtocol( - "client", "test", clock, self.test_handler + hs, "client", "test", clock, self.test_handler, ) - # wire them together - self.client.makeConnection(FakeTransport(server, reactor)) - server.makeConnection(FakeTransport(self.client, reactor)) + self._client_transport = None + self._server_transport = None + + def reconnect(self): + if self._client_transport: + self.client.close() + + if self._server_transport: + self.server.close() + + self._client_transport = FakeTransport(self.server, self.reactor) + self.client.makeConnection(self._client_transport) + + self._server_transport = FakeTransport(self.client, self.reactor) + self.server.makeConnection(self._server_transport) + + def disconnect(self): + if self._client_transport: + self._client_transport = None + self.client.close() + + if self._server_transport: + self._server_transport = None + self.server.close() def replicate(self): """Tell the master side of replication that something has happened, and then @@ -50,19 +69,24 @@ class BaseStreamTestCase(unittest.HomeserverTestCase): self.streamer.on_notifier_poke() self.pump(0.1) - def replicate_stream(self, stream, token="NOW"): + def replicate_stream(self): """Make the client end a REPLICATE command to set up a subscription to a stream""" - self.client.send_command(ReplicateCommand(stream, token)) + self.client.send_command(ReplicateCommand()) class TestReplicationClientHandler(object): """Drop-in for ReplicationClientHandler which just collects RDATA rows""" def __init__(self): - self.received_rdata_rows = [] + self.streams = set() + self._received_rdata_rows = [] def get_streams_to_replicate(self): - return {} + positions = {s: 0 for s in self.streams} + for stream, token, _ in self._received_rdata_rows: + if stream in self.streams: + positions[stream] = max(token, positions.get(stream, 0)) + return positions def get_currently_syncing_users(self): return [] @@ -73,6 +97,9 @@ class TestReplicationClientHandler(object): def finished_connecting(self): pass + async def on_position(self, stream_name, token): + """Called when we get new position data.""" + async def on_rdata(self, stream_name, token, rows): for r in rows: - self.received_rdata_rows.append((stream_name, token, r)) + self._received_rdata_rows.append((stream_name, token, r)) diff --git a/tests/replication/tcp/streams/test_receipts.py b/tests/replication/tcp/streams/test_receipts.py index fa2493cad6..0ec0825a0e 100644 --- a/tests/replication/tcp/streams/test_receipts.py +++ b/tests/replication/tcp/streams/test_receipts.py @@ -17,30 +17,64 @@ from synapse.replication.tcp.streams._base import ReceiptsStream from tests.replication.tcp.streams._base import BaseStreamTestCase USER_ID = "@feeling:blue" -ROOM_ID = "!room:blue" -EVENT_ID = "$event:blue" class ReceiptsStreamTestCase(BaseStreamTestCase): def test_receipt(self): + self.reconnect() + # make the client subscribe to the receipts stream - self.replicate_stream("receipts", "NOW") + self.replicate_stream() + self.test_handler.streams.add("receipts") # tell the master to send a new receipt self.get_success( self.hs.get_datastore().insert_receipt( - ROOM_ID, "m.read", USER_ID, [EVENT_ID], {"a": 1} + "!room:blue", "m.read", USER_ID, ["$event:blue"], {"a": 1} ) ) self.replicate() # there should be one RDATA command - rdata_rows = self.test_handler.received_rdata_rows + self.test_handler.on_rdata.assert_called_once() + stream_name, token, rdata_rows = self.test_handler.on_rdata.call_args[0] + self.assertEqual(stream_name, "receipts") self.assertEqual(1, len(rdata_rows)) - self.assertEqual(rdata_rows[0][0], "receipts") - row = rdata_rows[0][2] # type: ReceiptsStream.ReceiptsStreamRow - self.assertEqual(ROOM_ID, row.room_id) + row = rdata_rows[0] # type: ReceiptsStream.ReceiptsStreamRow + self.assertEqual("!room:blue", row.room_id) self.assertEqual("m.read", row.receipt_type) self.assertEqual(USER_ID, row.user_id) - self.assertEqual(EVENT_ID, row.event_id) + self.assertEqual("$event:blue", row.event_id) self.assertEqual({"a": 1}, row.data) + + # Now let's disconnect and insert some data. + self.disconnect() + + self.test_handler.on_rdata.reset_mock() + + self.get_success( + self.hs.get_datastore().insert_receipt( + "!room2:blue", "m.read", USER_ID, ["$event2:foo"], {"a": 2} + ) + ) + self.replicate() + + # Nothing should have happened as we are disconnected + self.test_handler.on_rdata.assert_not_called() + + self.reconnect() + self.pump(0.1) + + # We should now have caught up and get the missing data + self.test_handler.on_rdata.assert_called_once() + stream_name, token, rdata_rows = self.test_handler.on_rdata.call_args[0] + self.assertEqual(stream_name, "receipts") + self.assertEqual(token, 3) + self.assertEqual(1, len(rdata_rows)) + + row = rdata_rows[0] # type: ReceiptsStream.ReceiptsStreamRow + self.assertEqual("!room2:blue", row.room_id) + self.assertEqual("m.read", row.receipt_type) + self.assertEqual(USER_ID, row.user_id) + self.assertEqual("$event2:foo", row.event_id) + self.assertEqual({"a": 2}, row.data) From 6ca5e56fd12bbccb6b3ab43ed7c0281e4822274a Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Wed, 25 Mar 2020 12:49:34 -0500 Subject: [PATCH 1236/1623] Remove unused captcha_bypass_secret option (#7137) Signed-off-by: Aaron Raimist --- changelog.d/7137.removal | 1 + docs/sample_config.yaml | 4 ---- synapse/config/captcha.py | 5 ----- 3 files changed, 1 insertion(+), 9 deletions(-) create mode 100644 changelog.d/7137.removal diff --git a/changelog.d/7137.removal b/changelog.d/7137.removal new file mode 100644 index 0000000000..75266a06bb --- /dev/null +++ b/changelog.d/7137.removal @@ -0,0 +1 @@ +Remove nonfunctional `captcha_bypass_secret` option from `homeserver.yaml`. \ No newline at end of file diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 276e43b732..2ef83646b3 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -872,10 +872,6 @@ media_store_path: "DATADIR/media_store" # #enable_registration_captcha: false -# A secret key used to bypass the captcha test entirely. -# -#captcha_bypass_secret: "YOUR_SECRET_HERE" - # The API endpoint to use for verifying m.login.recaptcha responses. # #recaptcha_siteverify_api: "https://www.recaptcha.net/recaptcha/api/siteverify" diff --git a/synapse/config/captcha.py b/synapse/config/captcha.py index f0171bb5b2..56c87fa296 100644 --- a/synapse/config/captcha.py +++ b/synapse/config/captcha.py @@ -24,7 +24,6 @@ class CaptchaConfig(Config): self.enable_registration_captcha = config.get( "enable_registration_captcha", False ) - self.captcha_bypass_secret = config.get("captcha_bypass_secret") self.recaptcha_siteverify_api = config.get( "recaptcha_siteverify_api", "https://www.recaptcha.net/recaptcha/api/siteverify", @@ -49,10 +48,6 @@ class CaptchaConfig(Config): # #enable_registration_captcha: false - # A secret key used to bypass the captcha test entirely. - # - #captcha_bypass_secret: "YOUR_SECRET_HERE" - # The API endpoint to use for verifying m.login.recaptcha responses. # #recaptcha_siteverify_api: "https://www.recaptcha.net/recaptcha/api/siteverify" From 1c1242acba9694a3a4b1eb3b14ec0bac11ee4ff8 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Thu, 26 Mar 2020 07:39:34 -0400 Subject: [PATCH 1237/1623] Validate that the session is not modified during UI-Auth (#7068) --- changelog.d/7068.bugfix | 1 + synapse/handlers/auth.py | 37 +++++++++++-- synapse/rest/client/v2_alpha/account.py | 11 ++-- synapse/rest/client/v2_alpha/devices.py | 4 +- synapse/rest/client/v2_alpha/keys.py | 2 +- synapse/rest/client/v2_alpha/register.py | 5 +- tests/rest/client/v2_alpha/test_auth.py | 68 +++++++++++++++++++++++- tests/test_terms_auth.py | 3 +- 8 files changed, 117 insertions(+), 14 deletions(-) create mode 100644 changelog.d/7068.bugfix diff --git a/changelog.d/7068.bugfix b/changelog.d/7068.bugfix new file mode 100644 index 0000000000..d1693a7f22 --- /dev/null +++ b/changelog.d/7068.bugfix @@ -0,0 +1 @@ +Ensure that a user inteactive authentication session is tied to a single request. diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index 7860f9625e..2ce1425dfa 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -125,7 +125,11 @@ class AuthHandler(BaseHandler): @defer.inlineCallbacks def validate_user_via_ui_auth( - self, requester: Requester, request_body: Dict[str, Any], clientip: str + self, + requester: Requester, + request: SynapseRequest, + request_body: Dict[str, Any], + clientip: str, ): """ Checks that the user is who they claim to be, via a UI auth. @@ -137,6 +141,8 @@ class AuthHandler(BaseHandler): Args: requester: The user, as given by the access token + request: The request sent by the client. + request_body: The body of the request sent by the client clientip: The IP address of the client. @@ -172,7 +178,9 @@ class AuthHandler(BaseHandler): flows = [[login_type] for login_type in self._supported_login_types] try: - result, params, _ = yield self.check_auth(flows, request_body, clientip) + result, params, _ = yield self.check_auth( + flows, request, request_body, clientip + ) except LoginError: # Update the ratelimite to say we failed (`can_do_action` doesn't raise). self._failed_uia_attempts_ratelimiter.can_do_action( @@ -211,7 +219,11 @@ class AuthHandler(BaseHandler): @defer.inlineCallbacks def check_auth( - self, flows: List[List[str]], clientdict: Dict[str, Any], clientip: str + self, + flows: List[List[str]], + request: SynapseRequest, + clientdict: Dict[str, Any], + clientip: str, ): """ Takes a dictionary sent by the client in the login / registration @@ -231,6 +243,8 @@ class AuthHandler(BaseHandler): strings representing auth-types. At least one full flow must be completed in order for auth to be successful. + request: The request sent by the client. + clientdict: The dictionary from the client root level, not the 'auth' key: this method prompts for auth if none is sent. @@ -270,13 +284,27 @@ class AuthHandler(BaseHandler): # email auth link on there). It's probably too open to abuse # because it lets unauthenticated clients store arbitrary objects # on a homeserver. - # Revisit: Assumimg the REST APIs do sensible validation, the data + # Revisit: Assuming the REST APIs do sensible validation, the data # isn't arbintrary. session["clientdict"] = clientdict self._save_session(session) elif "clientdict" in session: clientdict = session["clientdict"] + # Ensure that the queried operation does not vary between stages of + # the UI authentication session. This is done by generating a stable + # comparator based on the URI, method, and body (minus the auth dict) + # and storing it during the initial query. Subsequent queries ensure + # that this comparator has not changed. + comparator = (request.uri, request.method, clientdict) + if "ui_auth" not in session: + session["ui_auth"] = comparator + elif session["ui_auth"] != comparator: + raise SynapseError( + 403, + "Requested operation has changed during the UI authentication session.", + ) + if not authdict: raise InteractiveAuthIncompleteError( self._auth_dict_for_flows(flows, session) @@ -322,6 +350,7 @@ class AuthHandler(BaseHandler): creds, list(clientdict), ) + return creds, clientdict, session["id"] ret = self._auth_dict_for_flows(flows, session) diff --git a/synapse/rest/client/v2_alpha/account.py b/synapse/rest/client/v2_alpha/account.py index 631cc74cb4..b1249b664c 100644 --- a/synapse/rest/client/v2_alpha/account.py +++ b/synapse/rest/client/v2_alpha/account.py @@ -234,13 +234,16 @@ class PasswordRestServlet(RestServlet): if self.auth.has_access_token(request): requester = await self.auth.get_user_by_req(request) params = await self.auth_handler.validate_user_via_ui_auth( - requester, body, self.hs.get_ip_from_request(request) + requester, request, body, self.hs.get_ip_from_request(request), ) user_id = requester.user.to_string() else: requester = None result, params, _ = await self.auth_handler.check_auth( - [[LoginType.EMAIL_IDENTITY]], body, self.hs.get_ip_from_request(request) + [[LoginType.EMAIL_IDENTITY]], + request, + body, + self.hs.get_ip_from_request(request), ) if LoginType.EMAIL_IDENTITY in result: @@ -308,7 +311,7 @@ class DeactivateAccountRestServlet(RestServlet): return 200, {} await self.auth_handler.validate_user_via_ui_auth( - requester, body, self.hs.get_ip_from_request(request) + requester, request, body, self.hs.get_ip_from_request(request), ) result = await self._deactivate_account_handler.deactivate_account( requester.user.to_string(), erase, id_server=body.get("id_server") @@ -656,7 +659,7 @@ class ThreepidAddRestServlet(RestServlet): assert_valid_client_secret(client_secret) await self.auth_handler.validate_user_via_ui_auth( - requester, body, self.hs.get_ip_from_request(request) + requester, request, body, self.hs.get_ip_from_request(request), ) validation_session = await self.identity_handler.validate_threepid_session( diff --git a/synapse/rest/client/v2_alpha/devices.py b/synapse/rest/client/v2_alpha/devices.py index 94ff73f384..119d979052 100644 --- a/synapse/rest/client/v2_alpha/devices.py +++ b/synapse/rest/client/v2_alpha/devices.py @@ -81,7 +81,7 @@ class DeleteDevicesRestServlet(RestServlet): assert_params_in_dict(body, ["devices"]) await self.auth_handler.validate_user_via_ui_auth( - requester, body, self.hs.get_ip_from_request(request) + requester, request, body, self.hs.get_ip_from_request(request), ) await self.device_handler.delete_devices( @@ -127,7 +127,7 @@ class DeviceRestServlet(RestServlet): raise await self.auth_handler.validate_user_via_ui_auth( - requester, body, self.hs.get_ip_from_request(request) + requester, request, body, self.hs.get_ip_from_request(request), ) await self.device_handler.delete_device(requester.user.to_string(), device_id) diff --git a/synapse/rest/client/v2_alpha/keys.py b/synapse/rest/client/v2_alpha/keys.py index f7ed4daf90..5eb7ef35a4 100644 --- a/synapse/rest/client/v2_alpha/keys.py +++ b/synapse/rest/client/v2_alpha/keys.py @@ -263,7 +263,7 @@ class SigningKeyUploadServlet(RestServlet): body = parse_json_object_from_request(request) await self.auth_handler.validate_user_via_ui_auth( - requester, body, self.hs.get_ip_from_request(request) + requester, request, body, self.hs.get_ip_from_request(request), ) result = await self.e2e_keys_handler.upload_signing_keys_for_user(user_id, body) diff --git a/synapse/rest/client/v2_alpha/register.py b/synapse/rest/client/v2_alpha/register.py index a09189b1b4..6963d79310 100644 --- a/synapse/rest/client/v2_alpha/register.py +++ b/synapse/rest/client/v2_alpha/register.py @@ -499,7 +499,10 @@ class RegisterRestServlet(RestServlet): ) auth_result, params, session_id = await self.auth_handler.check_auth( - self._registration_flows, body, self.hs.get_ip_from_request(request) + self._registration_flows, + request, + body, + self.hs.get_ip_from_request(request), ) # Check that we're not trying to register a denied 3pid. diff --git a/tests/rest/client/v2_alpha/test_auth.py b/tests/rest/client/v2_alpha/test_auth.py index b6df1396ad..624bf5ada2 100644 --- a/tests/rest/client/v2_alpha/test_auth.py +++ b/tests/rest/client/v2_alpha/test_auth.py @@ -104,7 +104,7 @@ class FallbackAuthTests(unittest.HomeserverTestCase): ) self.render(request) - # Now we should have fufilled a complete auth flow, including + # Now we should have fulfilled a complete auth flow, including # the recaptcha fallback step, we can then send a # request to the register API with the session in the authdict. request, channel = self.make_request( @@ -115,3 +115,69 @@ class FallbackAuthTests(unittest.HomeserverTestCase): # We're given a registered user. self.assertEqual(channel.json_body["user_id"], "@user:test") + + def test_cannot_change_operation(self): + """ + The initial requested operation cannot be modified during the user interactive authentication session. + """ + + # Make the initial request to register. (Later on a different password + # will be used.) + request, channel = self.make_request( + "POST", + "register", + {"username": "user", "type": "m.login.password", "password": "bar"}, + ) + self.render(request) + + # Returns a 401 as per the spec + self.assertEqual(request.code, 401) + # Grab the session + session = channel.json_body["session"] + # Assert our configured public key is being given + self.assertEqual( + channel.json_body["params"]["m.login.recaptcha"]["public_key"], "brokencake" + ) + + request, channel = self.make_request( + "GET", "auth/m.login.recaptcha/fallback/web?session=" + session + ) + self.render(request) + self.assertEqual(request.code, 200) + + request, channel = self.make_request( + "POST", + "auth/m.login.recaptcha/fallback/web?session=" + + session + + "&g-recaptcha-response=a", + ) + self.render(request) + self.assertEqual(request.code, 200) + + # The recaptcha handler is called with the response given + attempts = self.recaptcha_checker.recaptcha_attempts + self.assertEqual(len(attempts), 1) + self.assertEqual(attempts[0][0]["response"], "a") + + # also complete the dummy auth + request, channel = self.make_request( + "POST", "register", {"auth": {"session": session, "type": "m.login.dummy"}} + ) + self.render(request) + + # Now we should have fulfilled a complete auth flow, including + # the recaptcha fallback step. Make the initial request again, but + # with a different password. This causes the request to fail since the + # operaiton was modified during the ui auth session. + request, channel = self.make_request( + "POST", + "register", + { + "username": "user", + "type": "m.login.password", + "password": "foo", # Note this doesn't match the original request. + "auth": {"session": session}, + }, + ) + self.render(request) + self.assertEqual(channel.code, 403) diff --git a/tests/test_terms_auth.py b/tests/test_terms_auth.py index 5ec5d2b358..a3f98a1412 100644 --- a/tests/test_terms_auth.py +++ b/tests/test_terms_auth.py @@ -53,7 +53,8 @@ class TermsTestCase(unittest.HomeserverTestCase): def test_ui_auth(self): # Do a UI auth request - request, channel = self.make_request(b"POST", self.url, b"{}") + request_data = json.dumps({"username": "kermit", "password": "monkey"}) + request, channel = self.make_request(b"POST", self.url, request_data) self.render(request) self.assertEquals(channel.result["code"], b"401", channel.result) From e8e2ddb60ae11db488f159901d918cb159695912 Mon Sep 17 00:00:00 2001 From: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com> Date: Thu, 26 Mar 2020 17:51:13 +0100 Subject: [PATCH 1238/1623] Allow server admins to define and enforce a password policy (MSC2000). (#7118) --- changelog.d/7118.feature | 1 + docs/sample_config.yaml | 35 ++++ synapse/api/errors.py | 21 ++ synapse/config/password.py | 39 ++++ synapse/handlers/password_policy.py | 93 +++++++++ synapse/handlers/set_password.py | 2 + synapse/rest/__init__.py | 2 + .../rest/client/v2_alpha/password_policy.py | 58 ++++++ synapse/rest/client/v2_alpha/register.py | 2 + synapse/server.py | 5 + .../client/v2_alpha/test_password_policy.py | 179 ++++++++++++++++++ 11 files changed, 437 insertions(+) create mode 100644 changelog.d/7118.feature create mode 100644 synapse/handlers/password_policy.py create mode 100644 synapse/rest/client/v2_alpha/password_policy.py create mode 100644 tests/rest/client/v2_alpha/test_password_policy.py diff --git a/changelog.d/7118.feature b/changelog.d/7118.feature new file mode 100644 index 0000000000..5cbfd98160 --- /dev/null +++ b/changelog.d/7118.feature @@ -0,0 +1 @@ +Allow server admins to define and enforce a password policy (MSC2000). \ No newline at end of file diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 2ef83646b3..1a1d061759 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -1482,6 +1482,41 @@ password_config: # #pepper: "EVEN_MORE_SECRET" + # Define and enforce a password policy. Each parameter is optional. + # This is an implementation of MSC2000. + # + policy: + # Whether to enforce the password policy. + # Defaults to 'false'. + # + #enabled: true + + # Minimum accepted length for a password. + # Defaults to 0. + # + #minimum_length: 15 + + # Whether a password must contain at least one digit. + # Defaults to 'false'. + # + #require_digit: true + + # Whether a password must contain at least one symbol. + # A symbol is any character that's not a number or a letter. + # Defaults to 'false'. + # + #require_symbol: true + + # Whether a password must contain at least one lowercase letter. + # Defaults to 'false'. + # + #require_lowercase: true + + # Whether a password must contain at least one lowercase letter. + # Defaults to 'false'. + # + #require_uppercase: true + # Configuration for sending emails from Synapse. # diff --git a/synapse/api/errors.py b/synapse/api/errors.py index 616942b057..11da016ac5 100644 --- a/synapse/api/errors.py +++ b/synapse/api/errors.py @@ -64,6 +64,13 @@ class Codes(object): INCOMPATIBLE_ROOM_VERSION = "M_INCOMPATIBLE_ROOM_VERSION" WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION" EXPIRED_ACCOUNT = "ORG_MATRIX_EXPIRED_ACCOUNT" + PASSWORD_TOO_SHORT = "M_PASSWORD_TOO_SHORT" + PASSWORD_NO_DIGIT = "M_PASSWORD_NO_DIGIT" + PASSWORD_NO_UPPERCASE = "M_PASSWORD_NO_UPPERCASE" + PASSWORD_NO_LOWERCASE = "M_PASSWORD_NO_LOWERCASE" + PASSWORD_NO_SYMBOL = "M_PASSWORD_NO_SYMBOL" + PASSWORD_IN_DICTIONARY = "M_PASSWORD_IN_DICTIONARY" + WEAK_PASSWORD = "M_WEAK_PASSWORD" INVALID_SIGNATURE = "M_INVALID_SIGNATURE" USER_DEACTIVATED = "M_USER_DEACTIVATED" BAD_ALIAS = "M_BAD_ALIAS" @@ -439,6 +446,20 @@ class IncompatibleRoomVersionError(SynapseError): return cs_error(self.msg, self.errcode, room_version=self._room_version) +class PasswordRefusedError(SynapseError): + """A password has been refused, either during password reset/change or registration. + """ + + def __init__( + self, + msg="This password doesn't comply with the server's policy", + errcode=Codes.WEAK_PASSWORD, + ): + super(PasswordRefusedError, self).__init__( + code=400, msg=msg, errcode=errcode, + ) + + class RequestSendFailed(RuntimeError): """Sending a HTTP request over federation failed due to not being able to talk to the remote server for some reason. diff --git a/synapse/config/password.py b/synapse/config/password.py index 2a634ac751..9c0ea8c30a 100644 --- a/synapse/config/password.py +++ b/synapse/config/password.py @@ -31,6 +31,10 @@ class PasswordConfig(Config): self.password_localdb_enabled = password_config.get("localdb_enabled", True) self.password_pepper = password_config.get("pepper", "") + # Password policy + self.password_policy = password_config.get("policy") or {} + self.password_policy_enabled = self.password_policy.get("enabled", False) + def generate_config_section(self, config_dir_path, server_name, **kwargs): return """\ password_config: @@ -48,4 +52,39 @@ class PasswordConfig(Config): # DO NOT CHANGE THIS AFTER INITIAL SETUP! # #pepper: "EVEN_MORE_SECRET" + + # Define and enforce a password policy. Each parameter is optional. + # This is an implementation of MSC2000. + # + policy: + # Whether to enforce the password policy. + # Defaults to 'false'. + # + #enabled: true + + # Minimum accepted length for a password. + # Defaults to 0. + # + #minimum_length: 15 + + # Whether a password must contain at least one digit. + # Defaults to 'false'. + # + #require_digit: true + + # Whether a password must contain at least one symbol. + # A symbol is any character that's not a number or a letter. + # Defaults to 'false'. + # + #require_symbol: true + + # Whether a password must contain at least one lowercase letter. + # Defaults to 'false'. + # + #require_lowercase: true + + # Whether a password must contain at least one lowercase letter. + # Defaults to 'false'. + # + #require_uppercase: true """ diff --git a/synapse/handlers/password_policy.py b/synapse/handlers/password_policy.py new file mode 100644 index 0000000000..d06b110269 --- /dev/null +++ b/synapse/handlers/password_policy.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 New Vector Ltd +# Copyright 2019 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import re + +from synapse.api.errors import Codes, PasswordRefusedError + +logger = logging.getLogger(__name__) + + +class PasswordPolicyHandler(object): + def __init__(self, hs): + self.policy = hs.config.password_policy + self.enabled = hs.config.password_policy_enabled + + # Regexps for the spec'd policy parameters. + self.regexp_digit = re.compile("[0-9]") + self.regexp_symbol = re.compile("[^a-zA-Z0-9]") + self.regexp_uppercase = re.compile("[A-Z]") + self.regexp_lowercase = re.compile("[a-z]") + + def validate_password(self, password): + """Checks whether a given password complies with the server's policy. + + Args: + password (str): The password to check against the server's policy. + + Raises: + PasswordRefusedError: The password doesn't comply with the server's policy. + """ + + if not self.enabled: + return + + minimum_accepted_length = self.policy.get("minimum_length", 0) + if len(password) < minimum_accepted_length: + raise PasswordRefusedError( + msg=( + "The password must be at least %d characters long" + % minimum_accepted_length + ), + errcode=Codes.PASSWORD_TOO_SHORT, + ) + + if ( + self.policy.get("require_digit", False) + and self.regexp_digit.search(password) is None + ): + raise PasswordRefusedError( + msg="The password must include at least one digit", + errcode=Codes.PASSWORD_NO_DIGIT, + ) + + if ( + self.policy.get("require_symbol", False) + and self.regexp_symbol.search(password) is None + ): + raise PasswordRefusedError( + msg="The password must include at least one symbol", + errcode=Codes.PASSWORD_NO_SYMBOL, + ) + + if ( + self.policy.get("require_uppercase", False) + and self.regexp_uppercase.search(password) is None + ): + raise PasswordRefusedError( + msg="The password must include at least one uppercase letter", + errcode=Codes.PASSWORD_NO_UPPERCASE, + ) + + if ( + self.policy.get("require_lowercase", False) + and self.regexp_lowercase.search(password) is None + ): + raise PasswordRefusedError( + msg="The password must include at least one lowercase letter", + errcode=Codes.PASSWORD_NO_LOWERCASE, + ) diff --git a/synapse/handlers/set_password.py b/synapse/handlers/set_password.py index 12657ca698..7d1263caf2 100644 --- a/synapse/handlers/set_password.py +++ b/synapse/handlers/set_password.py @@ -32,6 +32,7 @@ class SetPasswordHandler(BaseHandler): super(SetPasswordHandler, self).__init__(hs) self._auth_handler = hs.get_auth_handler() self._device_handler = hs.get_device_handler() + self._password_policy_handler = hs.get_password_policy_handler() @defer.inlineCallbacks def set_password( @@ -44,6 +45,7 @@ class SetPasswordHandler(BaseHandler): if not self.hs.config.password_localdb_enabled: raise SynapseError(403, "Password change disabled", errcode=Codes.FORBIDDEN) + self._password_policy_handler.validate_password(new_password) password_hash = yield self._auth_handler.hash(new_password) try: diff --git a/synapse/rest/__init__.py b/synapse/rest/__init__.py index 4a1fc2ec2b..46e458e95b 100644 --- a/synapse/rest/__init__.py +++ b/synapse/rest/__init__.py @@ -41,6 +41,7 @@ from synapse.rest.client.v2_alpha import ( keys, notifications, openid, + password_policy, read_marker, receipts, register, @@ -118,6 +119,7 @@ class ClientRestResource(JsonResource): capabilities.register_servlets(hs, client_resource) account_validity.register_servlets(hs, client_resource) relations.register_servlets(hs, client_resource) + password_policy.register_servlets(hs, client_resource) # moving to /_synapse/admin synapse.rest.admin.register_servlets_for_client_rest_resource( diff --git a/synapse/rest/client/v2_alpha/password_policy.py b/synapse/rest/client/v2_alpha/password_policy.py new file mode 100644 index 0000000000..968403cca4 --- /dev/null +++ b/synapse/rest/client/v2_alpha/password_policy.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from synapse.http.servlet import RestServlet + +from ._base import client_patterns + +logger = logging.getLogger(__name__) + + +class PasswordPolicyServlet(RestServlet): + PATTERNS = client_patterns("/password_policy$") + + def __init__(self, hs): + """ + Args: + hs (synapse.server.HomeServer): server + """ + super(PasswordPolicyServlet, self).__init__() + + self.policy = hs.config.password_policy + self.enabled = hs.config.password_policy_enabled + + def on_GET(self, request): + if not self.enabled or not self.policy: + return (200, {}) + + policy = {} + + for param in [ + "minimum_length", + "require_digit", + "require_symbol", + "require_lowercase", + "require_uppercase", + ]: + if param in self.policy: + policy["m.%s" % param] = self.policy[param] + + return (200, policy) + + +def register_servlets(hs, http_server): + PasswordPolicyServlet(hs).register(http_server) diff --git a/synapse/rest/client/v2_alpha/register.py b/synapse/rest/client/v2_alpha/register.py index 6963d79310..66fc8ec179 100644 --- a/synapse/rest/client/v2_alpha/register.py +++ b/synapse/rest/client/v2_alpha/register.py @@ -373,6 +373,7 @@ class RegisterRestServlet(RestServlet): self.room_member_handler = hs.get_room_member_handler() self.macaroon_gen = hs.get_macaroon_generator() self.ratelimiter = hs.get_registration_ratelimiter() + self.password_policy_handler = hs.get_password_policy_handler() self.clock = hs.get_clock() self._registration_flows = _calculate_registration_flows( @@ -420,6 +421,7 @@ class RegisterRestServlet(RestServlet): or len(body["password"]) > 512 ): raise SynapseError(400, "Invalid password") + self.password_policy_handler.validate_password(body["password"]) desired_username = None if "username" in body: diff --git a/synapse/server.py b/synapse/server.py index 9426eb1672..d0d80e8ac5 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -66,6 +66,7 @@ from synapse.handlers.groups_local import GroupsLocalHandler, GroupsLocalWorkerH from synapse.handlers.initial_sync import InitialSyncHandler from synapse.handlers.message import EventCreationHandler, MessageHandler from synapse.handlers.pagination import PaginationHandler +from synapse.handlers.password_policy import PasswordPolicyHandler from synapse.handlers.presence import PresenceHandler from synapse.handlers.profile import BaseProfileHandler, MasterProfileHandler from synapse.handlers.read_marker import ReadMarkerHandler @@ -199,6 +200,7 @@ class HomeServer(object): "account_validity_handler", "saml_handler", "event_client_serializer", + "password_policy_handler", "storage", "replication_streamer", ] @@ -535,6 +537,9 @@ class HomeServer(object): def build_event_client_serializer(self): return EventClientSerializer(self) + def build_password_policy_handler(self): + return PasswordPolicyHandler(self) + def build_storage(self) -> Storage: return Storage(self, self.datastores) diff --git a/tests/rest/client/v2_alpha/test_password_policy.py b/tests/rest/client/v2_alpha/test_password_policy.py new file mode 100644 index 0000000000..c57072f50c --- /dev/null +++ b/tests/rest/client/v2_alpha/test_password_policy.py @@ -0,0 +1,179 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json + +from synapse.api.constants import LoginType +from synapse.api.errors import Codes +from synapse.rest import admin +from synapse.rest.client.v1 import login +from synapse.rest.client.v2_alpha import account, password_policy, register + +from tests import unittest + + +class PasswordPolicyTestCase(unittest.HomeserverTestCase): + """Tests the password policy feature and its compliance with MSC2000. + + When validating a password, Synapse does the necessary checks in this order: + + 1. Password is long enough + 2. Password contains digit(s) + 3. Password contains symbol(s) + 4. Password contains uppercase letter(s) + 5. Password contains lowercase letter(s) + + For each test below that checks whether a password triggers the right error code, + that test provides a password good enough to pass the previous tests, but not the + one it is currently testing (nor any test that comes afterward). + """ + + servlets = [ + admin.register_servlets_for_client_rest_resource, + login.register_servlets, + register.register_servlets, + password_policy.register_servlets, + account.register_servlets, + ] + + def make_homeserver(self, reactor, clock): + self.register_url = "/_matrix/client/r0/register" + self.policy = { + "enabled": True, + "minimum_length": 10, + "require_digit": True, + "require_symbol": True, + "require_lowercase": True, + "require_uppercase": True, + } + + config = self.default_config() + config["password_config"] = { + "policy": self.policy, + } + + hs = self.setup_test_homeserver(config=config) + return hs + + def test_get_policy(self): + """Tests if the /password_policy endpoint returns the configured policy.""" + + request, channel = self.make_request( + "GET", "/_matrix/client/r0/password_policy" + ) + self.render(request) + + self.assertEqual(channel.code, 200, channel.result) + self.assertEqual( + channel.json_body, + { + "m.minimum_length": 10, + "m.require_digit": True, + "m.require_symbol": True, + "m.require_lowercase": True, + "m.require_uppercase": True, + }, + channel.result, + ) + + def test_password_too_short(self): + request_data = json.dumps({"username": "kermit", "password": "shorty"}) + request, channel = self.make_request("POST", self.register_url, request_data) + self.render(request) + + self.assertEqual(channel.code, 400, channel.result) + self.assertEqual( + channel.json_body["errcode"], Codes.PASSWORD_TOO_SHORT, channel.result, + ) + + def test_password_no_digit(self): + request_data = json.dumps({"username": "kermit", "password": "longerpassword"}) + request, channel = self.make_request("POST", self.register_url, request_data) + self.render(request) + + self.assertEqual(channel.code, 400, channel.result) + self.assertEqual( + channel.json_body["errcode"], Codes.PASSWORD_NO_DIGIT, channel.result, + ) + + def test_password_no_symbol(self): + request_data = json.dumps({"username": "kermit", "password": "l0ngerpassword"}) + request, channel = self.make_request("POST", self.register_url, request_data) + self.render(request) + + self.assertEqual(channel.code, 400, channel.result) + self.assertEqual( + channel.json_body["errcode"], Codes.PASSWORD_NO_SYMBOL, channel.result, + ) + + def test_password_no_uppercase(self): + request_data = json.dumps({"username": "kermit", "password": "l0ngerpassword!"}) + request, channel = self.make_request("POST", self.register_url, request_data) + self.render(request) + + self.assertEqual(channel.code, 400, channel.result) + self.assertEqual( + channel.json_body["errcode"], Codes.PASSWORD_NO_UPPERCASE, channel.result, + ) + + def test_password_no_lowercase(self): + request_data = json.dumps({"username": "kermit", "password": "L0NGERPASSWORD!"}) + request, channel = self.make_request("POST", self.register_url, request_data) + self.render(request) + + self.assertEqual(channel.code, 400, channel.result) + self.assertEqual( + channel.json_body["errcode"], Codes.PASSWORD_NO_LOWERCASE, channel.result, + ) + + def test_password_compliant(self): + request_data = json.dumps({"username": "kermit", "password": "L0ngerpassword!"}) + request, channel = self.make_request("POST", self.register_url, request_data) + self.render(request) + + # Getting a 401 here means the password has passed validation and the server has + # responded with a list of registration flows. + self.assertEqual(channel.code, 401, channel.result) + + def test_password_change(self): + """This doesn't test every possible use case, only that hitting /account/password + triggers the password validation code. + """ + compliant_password = "C0mpl!antpassword" + not_compliant_password = "notcompliantpassword" + + user_id = self.register_user("kermit", compliant_password) + tok = self.login("kermit", compliant_password) + + request_data = json.dumps( + { + "new_password": not_compliant_password, + "auth": { + "password": compliant_password, + "type": LoginType.PASSWORD, + "user": user_id, + }, + } + ) + request, channel = self.make_request( + "POST", + "/_matrix/client/r0/account/password", + request_data, + access_token=tok, + ) + self.render(request) + + self.assertEqual(channel.code, 400, channel.result) + self.assertEqual(channel.json_body["errcode"], Codes.PASSWORD_NO_DIGIT) From 060e7dce09ae2197f29811769b13db30ed340211 Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Thu, 26 Mar 2020 19:02:35 +0200 Subject: [PATCH 1239/1623] Allow RedirectResponse in SAML response handler Allow custom SAML handlers to redirect after processing an auth response. Fixes #7149 Signed-off-by: Jason Robinson --- changelog.d/7151.bugfix | 1 + synapse/handlers/saml_handler.py | 4 ++++ 2 files changed, 5 insertions(+) create mode 100644 changelog.d/7151.bugfix diff --git a/changelog.d/7151.bugfix b/changelog.d/7151.bugfix new file mode 100644 index 0000000000..69cde9351d --- /dev/null +++ b/changelog.d/7151.bugfix @@ -0,0 +1 @@ +Allow custom SAML handlers to redirect after processing an auth response. diff --git a/synapse/handlers/saml_handler.py b/synapse/handlers/saml_handler.py index 72c109981b..dc04b53f43 100644 --- a/synapse/handlers/saml_handler.py +++ b/synapse/handlers/saml_handler.py @@ -26,6 +26,7 @@ from synapse.config import ConfigError from synapse.http.server import finish_request from synapse.http.servlet import parse_string from synapse.module_api import ModuleApi +from synapse.module_api.errors import RedirectException from synapse.types import ( UserID, map_username_to_mxid_localpart, @@ -119,6 +120,9 @@ class SamlHandler: try: user_id = await self._map_saml_response_to_user(resp_bytes, relay_state) + except RedirectException: + # Raise the exception as per the wishes of the SAML module response + raise except Exception as e: # If decoding the response or mapping it to a user failed, then log the # error and tell the user that something went wrong. From 825fb5d0a5699fb5b5eef9a8c2170d0c76158001 Mon Sep 17 00:00:00 2001 From: Nektarios Katakis Date: Thu, 26 Mar 2020 17:13:14 +0000 Subject: [PATCH 1240/1623] Don't default to an invalid sqlite config if no database configuration is provided (#6573) --- changelog.d/6573.bugfix | 1 + synapse/config/database.py | 69 ++++++++++++++++++++++++++------------ 2 files changed, 48 insertions(+), 22 deletions(-) create mode 100644 changelog.d/6573.bugfix diff --git a/changelog.d/6573.bugfix b/changelog.d/6573.bugfix new file mode 100644 index 0000000000..1bb8014db7 --- /dev/null +++ b/changelog.d/6573.bugfix @@ -0,0 +1 @@ +Don't attempt to use an invalid sqlite config if no database configuration is provided. Contributed by @nekatak. diff --git a/synapse/config/database.py b/synapse/config/database.py index b8ab2f86ac..c27fef157b 100644 --- a/synapse/config/database.py +++ b/synapse/config/database.py @@ -20,6 +20,11 @@ from synapse.config._base import Config, ConfigError logger = logging.getLogger(__name__) +NON_SQLITE_DATABASE_PATH_WARNING = """\ +Ignoring 'database_path' setting: not using a sqlite3 database. +-------------------------------------------------------------------------------- +""" + DEFAULT_CONFIG = """\ ## Database ## @@ -105,6 +110,11 @@ class DatabaseConnectionConfig: class DatabaseConfig(Config): section = "database" + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.databases = [] + def read_config(self, config, **kwargs): self.event_cache_size = self.parse_size(config.get("event_cache_size", "10K")) @@ -125,12 +135,13 @@ class DatabaseConfig(Config): multi_database_config = config.get("databases") database_config = config.get("database") + database_path = config.get("database_path") if multi_database_config and database_config: raise ConfigError("Can't specify both 'database' and 'datbases' in config") if multi_database_config: - if config.get("database_path"): + if database_path: raise ConfigError("Can't specify 'database_path' with 'databases'") self.databases = [ @@ -138,13 +149,17 @@ class DatabaseConfig(Config): for name, db_conf in multi_database_config.items() ] - else: - if database_config is None: - database_config = {"name": "sqlite3", "args": {}} - + if database_config: self.databases = [DatabaseConnectionConfig("master", database_config)] - self.set_databasepath(config.get("database_path")) + if database_path: + if self.databases and self.databases[0].name != "sqlite3": + logger.warning(NON_SQLITE_DATABASE_PATH_WARNING) + return + + database_config = {"name": "sqlite3", "args": {}} + self.databases = [DatabaseConnectionConfig("master", database_config)] + self.set_databasepath(database_path) def generate_config_section(self, data_dir_path, **kwargs): return DEFAULT_CONFIG % { @@ -152,27 +167,37 @@ class DatabaseConfig(Config): } def read_arguments(self, args): - self.set_databasepath(args.database_path) + """ + Cases for the cli input: + - If no databases are configured and no database_path is set, raise. + - No databases and only database_path available ==> sqlite3 db. + - If there are multiple databases and a database_path raise an error. + - If the database set in the config file is sqlite then + overwrite with the command line argument. + """ + + if args.database_path is None: + if not self.databases: + raise ConfigError("No database config provided") + return + + if len(self.databases) == 0: + database_config = {"name": "sqlite3", "args": {}} + self.databases = [DatabaseConnectionConfig("master", database_config)] + self.set_databasepath(args.database_path) + return + + if self.get_single_database().name == "sqlite3": + self.set_databasepath(args.database_path) + else: + logger.warning(NON_SQLITE_DATABASE_PATH_WARNING) def set_databasepath(self, database_path): - if database_path is None: - return if database_path != ":memory:": database_path = self.abspath(database_path) - # We only support setting a database path if we have a single sqlite3 - # database. - if len(self.databases) != 1: - raise ConfigError("Cannot specify 'database_path' with multiple databases") - - database = self.get_single_database() - if database.config["name"] != "sqlite3": - # We don't raise here as we haven't done so before for this case. - logger.warn("Ignoring 'database_path' for non-sqlite3 database") - return - - database.config["args"]["database"] = database_path + self.databases[0].config["args"]["database"] = database_path @staticmethod def add_arguments(parser): @@ -187,7 +212,7 @@ class DatabaseConfig(Config): def get_single_database(self) -> DatabaseConnectionConfig: """Returns the database if there is only one, useful for e.g. tests """ - if len(self.databases) != 1: + if not self.databases: raise Exception("More than one database exists") return self.databases[0] From c2ab0b30664439d09436a39e5448728c67b0414c Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 26 Mar 2020 18:58:58 +0100 Subject: [PATCH 1241/1623] Whitelist the login fallback by default for SSO --- synapse/config/sso.py | 17 ++++++++++++++++- tests/rest/client/v1/test_login.py | 13 ++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/synapse/config/sso.py b/synapse/config/sso.py index 95762689bc..5ae9db83d0 100644 --- a/synapse/config/sso.py +++ b/synapse/config/sso.py @@ -39,6 +39,17 @@ class SSOConfig(Config): self.sso_client_whitelist = sso_config.get("client_whitelist") or [] + # Attempt to also whitelist the server's login fallback, since that fallback sets + # the redirect URL to itself (so it can process the login token then return + # gracefully to the client). This would make it pointless to ask the user for + # confirmation, since the URL the confirmation page would be showing wouldn't be + # the client's. + # public_baseurl is an optional setting, so we only add the fallback's URL to the + # list if it's provided (because we can't figure out what that URL is otherwise). + if self.public_baseurl: + login_fallback_url = self.public_baseurl + "_matrix/static/client/login" + self.sso_client_whitelist.append(login_fallback_url) + def generate_config_section(self, **kwargs): return """\ # Additional settings to use with single-sign on systems such as SAML2 and CAS. @@ -54,7 +65,11 @@ class SSOConfig(Config): # phishing attacks from evil.site. To avoid this, include a slash after the # hostname: "https://my.client/". # - # By default, this list is empty. + # If public_baseurl is set, then the login fallback page (used by clients + # that don't have full support for SSO) is always included in this list. + # + # By default, this list is empty, except if public_baseurl is set (in which + # case the login fallback page is the only element in the list). # #client_whitelist: # - https://riot.im/develop diff --git a/tests/rest/client/v1/test_login.py b/tests/rest/client/v1/test_login.py index da2c9bfa1e..ed02ff1dcc 100644 --- a/tests/rest/client/v1/test_login.py +++ b/tests/rest/client/v1/test_login.py @@ -350,7 +350,18 @@ class CASRedirectConfirmTestCase(unittest.HomeserverTestCase): def test_cas_redirect_whitelisted(self): """Tests that the SSO login flow serves a redirect to a whitelisted url """ - redirect_url = "https://legit-site.com/" + self._test_redirect("https://legit-site.com/") + + @override_config( + { + "public_baseurl": "https://example.com" + } + ) + def test_cas_redirect_login_fallback(self): + self._test_redirect("https://example.com/_matrix/static/client/login") + + def _test_redirect(self, redirect_url): + """Tests that the SSO login flow serves a redirect for the given redirect URL.""" cas_ticket_url = ( "/_matrix/client/r0/login/cas/ticket?redirectUrl=%s&ticket=ticket" % (urllib.parse.quote(redirect_url)) From 7083147961d7bc45ce49047c4878da2bf5202f79 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 26 Mar 2020 19:01:54 +0100 Subject: [PATCH 1242/1623] Regenerate sample config --- docs/sample_config.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 2ff0dd05a2..556d4419f5 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -1392,7 +1392,11 @@ sso: # phishing attacks from evil.site. To avoid this, include a slash after the # hostname: "https://my.client/". # - # By default, this list is empty. + # If public_baseurl is set, then the login fallback page (used by clients + # that don't have full support for SSO) is always included in this list. + # + # By default, this list is empty, except if public_baseurl is set (in which + # case the login fallback page is the only element in the list). # #client_whitelist: # - https://riot.im/develop From 48b37f61ce17f5a667a9c8b7c6ad1d6abb7fdae8 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 26 Mar 2020 19:02:59 +0100 Subject: [PATCH 1243/1623] Changelog --- changelog.d/7153.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7153.feature diff --git a/changelog.d/7153.feature b/changelog.d/7153.feature new file mode 100644 index 0000000000..414ebe1f69 --- /dev/null +++ b/changelog.d/7153.feature @@ -0,0 +1 @@ +Always whitelist the login fallback in the SSO configuration if `public_baseurl` is set. From bdf3cdaec800966a493998c65665ee951b3d39f5 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 26 Mar 2020 19:06:44 +0100 Subject: [PATCH 1244/1623] Lint --- tests/rest/client/v1/test_login.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/rest/client/v1/test_login.py b/tests/rest/client/v1/test_login.py index ed02ff1dcc..aed8853d6e 100644 --- a/tests/rest/client/v1/test_login.py +++ b/tests/rest/client/v1/test_login.py @@ -352,11 +352,7 @@ class CASRedirectConfirmTestCase(unittest.HomeserverTestCase): """ self._test_redirect("https://legit-site.com/") - @override_config( - { - "public_baseurl": "https://example.com" - } - ) + @override_config({"public_baseurl": "https://example.com"}) def test_cas_redirect_login_fallback(self): self._test_redirect("https://example.com/_matrix/static/client/login") From 55ca6cf88cee15519cd094f60c92ab959973e4c6 Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Thu, 26 Mar 2020 20:35:50 +0200 Subject: [PATCH 1245/1623] Update changelog.d/7151.bugfix Co-Authored-By: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> --- changelog.d/7151.bugfix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/7151.bugfix b/changelog.d/7151.bugfix index 69cde9351d..8aaa2dc659 100644 --- a/changelog.d/7151.bugfix +++ b/changelog.d/7151.bugfix @@ -1 +1 @@ -Allow custom SAML handlers to redirect after processing an auth response. +Fix error page being shown when a custom SAML handler attempted to redirect when processing an auth response. From fa4f12102d52b75d252d9209b45251d2b1591fdf Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Thu, 26 Mar 2020 15:05:26 -0400 Subject: [PATCH 1246/1623] Refactor the CAS code (move the logic out of the REST layer to a handler) (#7136) --- changelog.d/7136.misc | 1 + synapse/handlers/cas_handler.py | 204 ++++++++++++++++++++++++++++++++ synapse/rest/client/v1/login.py | 169 +++----------------------- synapse/server.py | 5 + tox.ini | 1 + 5 files changed, 226 insertions(+), 154 deletions(-) create mode 100644 changelog.d/7136.misc create mode 100644 synapse/handlers/cas_handler.py diff --git a/changelog.d/7136.misc b/changelog.d/7136.misc new file mode 100644 index 0000000000..3f666d25fd --- /dev/null +++ b/changelog.d/7136.misc @@ -0,0 +1 @@ +Refactored the CAS authentication logic to a separate class. diff --git a/synapse/handlers/cas_handler.py b/synapse/handlers/cas_handler.py new file mode 100644 index 0000000000..f8dc274b78 --- /dev/null +++ b/synapse/handlers/cas_handler.py @@ -0,0 +1,204 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import xml.etree.ElementTree as ET +from typing import AnyStr, Dict, Optional, Tuple + +from six.moves import urllib + +from twisted.web.client import PartialDownloadError + +from synapse.api.errors import Codes, LoginError +from synapse.http.site import SynapseRequest +from synapse.types import UserID, map_username_to_mxid_localpart + +logger = logging.getLogger(__name__) + + +class CasHandler: + """ + Utility class for to handle the response from a CAS SSO service. + + Args: + hs (synapse.server.HomeServer) + """ + + def __init__(self, hs): + self._hostname = hs.hostname + self._auth_handler = hs.get_auth_handler() + self._registration_handler = hs.get_registration_handler() + + self._cas_server_url = hs.config.cas_server_url + self._cas_service_url = hs.config.cas_service_url + self._cas_displayname_attribute = hs.config.cas_displayname_attribute + self._cas_required_attributes = hs.config.cas_required_attributes + + self._http_client = hs.get_proxied_http_client() + + def _build_service_param(self, client_redirect_url: AnyStr) -> str: + return "%s%s?%s" % ( + self._cas_service_url, + "/_matrix/client/r0/login/cas/ticket", + urllib.parse.urlencode({"redirectUrl": client_redirect_url}), + ) + + async def _handle_cas_response( + self, request: SynapseRequest, cas_response_body: str, client_redirect_url: str + ) -> None: + """ + Retrieves the user and display name from the CAS response and continues with the authentication. + + Args: + request: The original client request. + cas_response_body: The response from the CAS server. + client_redirect_url: The URl to redirect the client to when + everything is done. + """ + user, attributes = self._parse_cas_response(cas_response_body) + displayname = attributes.pop(self._cas_displayname_attribute, None) + + for required_attribute, required_value in self._cas_required_attributes.items(): + # If required attribute was not in CAS Response - Forbidden + if required_attribute not in attributes: + raise LoginError(401, "Unauthorized", errcode=Codes.UNAUTHORIZED) + + # Also need to check value + if required_value is not None: + actual_value = attributes[required_attribute] + # If required attribute value does not match expected - Forbidden + if required_value != actual_value: + raise LoginError(401, "Unauthorized", errcode=Codes.UNAUTHORIZED) + + await self._on_successful_auth(user, request, client_redirect_url, displayname) + + def _parse_cas_response( + self, cas_response_body: str + ) -> Tuple[str, Dict[str, Optional[str]]]: + """ + Retrieve the user and other parameters from the CAS response. + + Args: + cas_response_body: The response from the CAS query. + + Returns: + A tuple of the user and a mapping of other attributes. + """ + user = None + attributes = {} + try: + root = ET.fromstring(cas_response_body) + if not root.tag.endswith("serviceResponse"): + raise Exception("root of CAS response is not serviceResponse") + success = root[0].tag.endswith("authenticationSuccess") + for child in root[0]: + if child.tag.endswith("user"): + user = child.text + if child.tag.endswith("attributes"): + for attribute in child: + # ElementTree library expands the namespace in + # attribute tags to the full URL of the namespace. + # We don't care about namespace here and it will always + # be encased in curly braces, so we remove them. + tag = attribute.tag + if "}" in tag: + tag = tag.split("}")[1] + attributes[tag] = attribute.text + if user is None: + raise Exception("CAS response does not contain user") + except Exception: + logger.exception("Error parsing CAS response") + raise LoginError(401, "Invalid CAS response", errcode=Codes.UNAUTHORIZED) + if not success: + raise LoginError( + 401, "Unsuccessful CAS response", errcode=Codes.UNAUTHORIZED + ) + return user, attributes + + async def _on_successful_auth( + self, + username: str, + request: SynapseRequest, + client_redirect_url: str, + user_display_name: Optional[str] = None, + ) -> None: + """Called once the user has successfully authenticated with the SSO. + + Registers the user if necessary, and then returns a redirect (with + a login token) to the client. + + Args: + username: the remote user id. We'll map this onto + something sane for a MXID localpath. + + request: the incoming request from the browser. We'll + respond to it with a redirect. + + client_redirect_url: the redirect_url the client gave us when + it first started the process. + + user_display_name: if set, and we have to register a new user, + we will set their displayname to this. + """ + localpart = map_username_to_mxid_localpart(username) + user_id = UserID(localpart, self._hostname).to_string() + registered_user_id = await self._auth_handler.check_user_exists(user_id) + if not registered_user_id: + registered_user_id = await self._registration_handler.register_user( + localpart=localpart, default_display_name=user_display_name + ) + + self._auth_handler.complete_sso_login( + registered_user_id, request, client_redirect_url + ) + + def handle_redirect_request(self, client_redirect_url: bytes) -> bytes: + """ + Generates a URL to the CAS server where the client should be redirected. + + Args: + client_redirect_url: The final URL the client should go to after the + user has negotiated SSO. + + Returns: + The URL to redirect to. + """ + args = urllib.parse.urlencode( + {"service": self._build_service_param(client_redirect_url)} + ) + + return ("%s/login?%s" % (self._cas_server_url, args)).encode("ascii") + + async def handle_ticket_request( + self, request: SynapseRequest, client_redirect_url: str, ticket: str + ) -> None: + """ + Validates a CAS ticket sent by the client for login/registration. + + On a successful request, writes a redirect to the request. + """ + uri = self._cas_server_url + "/proxyValidate" + args = { + "ticket": ticket, + "service": self._build_service_param(client_redirect_url), + } + try: + body = await self._http_client.get_raw(uri, args) + except PartialDownloadError as pde: + # Twisted raises this error if the connection is closed, + # even if that's being used old-http style to signal end-of-data + body = pde.response + + await self._handle_cas_response(request, body, client_redirect_url) diff --git a/synapse/rest/client/v1/login.py b/synapse/rest/client/v1/login.py index 56d713462a..59593cbf6e 100644 --- a/synapse/rest/client/v1/login.py +++ b/synapse/rest/client/v1/login.py @@ -14,11 +14,6 @@ # limitations under the License. import logging -import xml.etree.ElementTree as ET - -from six.moves import urllib - -from twisted.web.client import PartialDownloadError from synapse.api.errors import Codes, LoginError, SynapseError from synapse.api.ratelimiting import Ratelimiter @@ -28,9 +23,10 @@ from synapse.http.servlet import ( parse_json_object_from_request, parse_string, ) +from synapse.http.site import SynapseRequest from synapse.rest.client.v2_alpha._base import client_patterns from synapse.rest.well_known import WellKnownBuilder -from synapse.types import UserID, map_username_to_mxid_localpart +from synapse.types import UserID from synapse.util.msisdn import phone_number_to_msisdn logger = logging.getLogger(__name__) @@ -72,14 +68,6 @@ def login_id_thirdparty_from_phone(identifier): return {"type": "m.id.thirdparty", "medium": "msisdn", "address": msisdn} -def build_service_param(cas_service_url, client_redirect_url): - return "%s%s?redirectUrl=%s" % ( - cas_service_url, - "/_matrix/client/r0/login/cas/ticket", - urllib.parse.quote(client_redirect_url, safe=""), - ) - - class LoginRestServlet(RestServlet): PATTERNS = client_patterns("/login$", v1=True) CAS_TYPE = "m.login.cas" @@ -409,7 +397,7 @@ class BaseSSORedirectServlet(RestServlet): PATTERNS = client_patterns("/login/(cas|sso)/redirect", v1=True) - def on_GET(self, request): + def on_GET(self, request: SynapseRequest): args = request.args if b"redirectUrl" not in args: return 400, "Redirect URL not specified for SSO auth" @@ -418,15 +406,15 @@ class BaseSSORedirectServlet(RestServlet): request.redirect(sso_url) finish_request(request) - def get_sso_url(self, client_redirect_url): + def get_sso_url(self, client_redirect_url: bytes) -> bytes: """Get the URL to redirect to, to perform SSO auth Args: - client_redirect_url (bytes): the URL that we should redirect the + client_redirect_url: the URL that we should redirect the client to when everything is done Returns: - bytes: URL to redirect to + URL to redirect to """ # to be implemented by subclasses raise NotImplementedError() @@ -434,16 +422,10 @@ class BaseSSORedirectServlet(RestServlet): class CasRedirectServlet(BaseSSORedirectServlet): def __init__(self, hs): - super(CasRedirectServlet, self).__init__() - self.cas_server_url = hs.config.cas_server_url - self.cas_service_url = hs.config.cas_service_url + self._cas_handler = hs.get_cas_handler() - def get_sso_url(self, client_redirect_url): - args = urllib.parse.urlencode( - {"service": build_service_param(self.cas_service_url, client_redirect_url)} - ) - - return "%s/login?%s" % (self.cas_server_url, args) + def get_sso_url(self, client_redirect_url: bytes) -> bytes: + return self._cas_handler.handle_redirect_request(client_redirect_url) class CasTicketServlet(RestServlet): @@ -451,81 +433,15 @@ class CasTicketServlet(RestServlet): def __init__(self, hs): super(CasTicketServlet, self).__init__() - self.cas_server_url = hs.config.cas_server_url - self.cas_service_url = hs.config.cas_service_url - self.cas_displayname_attribute = hs.config.cas_displayname_attribute - self.cas_required_attributes = hs.config.cas_required_attributes - self._sso_auth_handler = SSOAuthHandler(hs) - self._http_client = hs.get_proxied_http_client() + self._cas_handler = hs.get_cas_handler() - async def on_GET(self, request): + async def on_GET(self, request: SynapseRequest) -> None: client_redirect_url = parse_string(request, "redirectUrl", required=True) - uri = self.cas_server_url + "/proxyValidate" - args = { - "ticket": parse_string(request, "ticket", required=True), - "service": build_service_param(self.cas_service_url, client_redirect_url), - } - try: - body = await self._http_client.get_raw(uri, args) - except PartialDownloadError as pde: - # Twisted raises this error if the connection is closed, - # even if that's being used old-http style to signal end-of-data - body = pde.response - result = await self.handle_cas_response(request, body, client_redirect_url) - return result - - def handle_cas_response(self, request, cas_response_body, client_redirect_url): - user, attributes = self.parse_cas_response(cas_response_body) - displayname = attributes.pop(self.cas_displayname_attribute, None) - - for required_attribute, required_value in self.cas_required_attributes.items(): - # If required attribute was not in CAS Response - Forbidden - if required_attribute not in attributes: - raise LoginError(401, "Unauthorized", errcode=Codes.UNAUTHORIZED) - - # Also need to check value - if required_value is not None: - actual_value = attributes[required_attribute] - # If required attribute value does not match expected - Forbidden - if required_value != actual_value: - raise LoginError(401, "Unauthorized", errcode=Codes.UNAUTHORIZED) - - return self._sso_auth_handler.on_successful_auth( - user, request, client_redirect_url, displayname + ticket = parse_string(request, "ticket", required=True) + await self._cas_handler.handle_ticket_request( + request, client_redirect_url, ticket ) - def parse_cas_response(self, cas_response_body): - user = None - attributes = {} - try: - root = ET.fromstring(cas_response_body) - if not root.tag.endswith("serviceResponse"): - raise Exception("root of CAS response is not serviceResponse") - success = root[0].tag.endswith("authenticationSuccess") - for child in root[0]: - if child.tag.endswith("user"): - user = child.text - if child.tag.endswith("attributes"): - for attribute in child: - # ElementTree library expands the namespace in - # attribute tags to the full URL of the namespace. - # We don't care about namespace here and it will always - # be encased in curly braces, so we remove them. - tag = attribute.tag - if "}" in tag: - tag = tag.split("}")[1] - attributes[tag] = attribute.text - if user is None: - raise Exception("CAS response does not contain user") - except Exception: - logger.exception("Error parsing CAS response") - raise LoginError(401, "Invalid CAS response", errcode=Codes.UNAUTHORIZED) - if not success: - raise LoginError( - 401, "Unsuccessful CAS response", errcode=Codes.UNAUTHORIZED - ) - return user, attributes - class SAMLRedirectServlet(BaseSSORedirectServlet): PATTERNS = client_patterns("/login/sso/redirect", v1=True) @@ -533,65 +449,10 @@ class SAMLRedirectServlet(BaseSSORedirectServlet): def __init__(self, hs): self._saml_handler = hs.get_saml_handler() - def get_sso_url(self, client_redirect_url): + def get_sso_url(self, client_redirect_url: bytes) -> bytes: return self._saml_handler.handle_redirect_request(client_redirect_url) -class SSOAuthHandler(object): - """ - Utility class for Resources and Servlets which handle the response from a SSO - service - - Args: - hs (synapse.server.HomeServer) - """ - - def __init__(self, hs): - self._hostname = hs.hostname - self._auth_handler = hs.get_auth_handler() - self._registration_handler = hs.get_registration_handler() - self._macaroon_gen = hs.get_macaroon_generator() - - # cast to tuple for use with str.startswith - self._whitelisted_sso_clients = tuple(hs.config.sso_client_whitelist) - - async def on_successful_auth( - self, username, request, client_redirect_url, user_display_name=None - ): - """Called once the user has successfully authenticated with the SSO. - - Registers the user if necessary, and then returns a redirect (with - a login token) to the client. - - Args: - username (unicode|bytes): the remote user id. We'll map this onto - something sane for a MXID localpath. - - request (SynapseRequest): the incoming request from the browser. We'll - respond to it with a redirect. - - client_redirect_url (unicode): the redirect_url the client gave us when - it first started the process. - - user_display_name (unicode|None): if set, and we have to register a new user, - we will set their displayname to this. - - Returns: - Deferred[none]: Completes once we have handled the request. - """ - localpart = map_username_to_mxid_localpart(username) - user_id = UserID(localpart, self._hostname).to_string() - registered_user_id = await self._auth_handler.check_user_exists(user_id) - if not registered_user_id: - registered_user_id = await self._registration_handler.register_user( - localpart=localpart, default_display_name=user_display_name - ) - - self._auth_handler.complete_sso_login( - registered_user_id, request, client_redirect_url - ) - - def register_servlets(hs, http_server): LoginRestServlet(hs).register(http_server) if hs.config.cas_enabled: diff --git a/synapse/server.py b/synapse/server.py index d0d80e8ac5..c7ca2bda0d 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -56,6 +56,7 @@ from synapse.handlers.account_validity import AccountValidityHandler from synapse.handlers.acme import AcmeHandler from synapse.handlers.appservice import ApplicationServicesHandler from synapse.handlers.auth import AuthHandler, MacaroonGenerator +from synapse.handlers.cas_handler import CasHandler from synapse.handlers.deactivate_account import DeactivateAccountHandler from synapse.handlers.device import DeviceHandler, DeviceWorkerHandler from synapse.handlers.devicemessage import DeviceMessageHandler @@ -198,6 +199,7 @@ class HomeServer(object): "sendmail", "registration_handler", "account_validity_handler", + "cas_handler", "saml_handler", "event_client_serializer", "password_policy_handler", @@ -529,6 +531,9 @@ class HomeServer(object): def build_account_validity_handler(self): return AccountValidityHandler(self) + def build_cas_handler(self): + return CasHandler(self) + def build_saml_handler(self): from synapse.handlers.saml_handler import SamlHandler diff --git a/tox.ini b/tox.ini index 8e3f09e638..a79fc93b57 100644 --- a/tox.ini +++ b/tox.ini @@ -186,6 +186,7 @@ commands = mypy \ synapse/federation/sender \ synapse/federation/transport \ synapse/handlers/auth.py \ + synapse/handlers/cas_handler.py \ synapse/handlers/directory.py \ synapse/handlers/presence.py \ synapse/handlers/sync.py \ From 665630fcaab8f09e83ff77f35d5244a718e20701 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 27 Mar 2020 11:39:43 +0000 Subject: [PATCH 1247/1623] Add tests for outbound device pokes --- changelog.d/7157.misc | 1 + tests/federation/test_federation_sender.py | 303 ++++++++++++++++++++- tests/unittest.py | 1 + 3 files changed, 302 insertions(+), 3 deletions(-) create mode 100644 changelog.d/7157.misc diff --git a/changelog.d/7157.misc b/changelog.d/7157.misc new file mode 100644 index 0000000000..0eb1128c7a --- /dev/null +++ b/changelog.d/7157.misc @@ -0,0 +1 @@ +Add tests for outbound device pokes. diff --git a/tests/federation/test_federation_sender.py b/tests/federation/test_federation_sender.py index d456267b87..7763b12159 100644 --- a/tests/federation/test_federation_sender.py +++ b/tests/federation/test_federation_sender.py @@ -12,19 +12,25 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from typing import Optional from mock import Mock +from signedjson import key, sign +from signedjson.types import BaseKey, SigningKey + from twisted.internet import defer -from synapse.types import ReadReceipt +from synapse.rest import admin +from synapse.rest.client.v1 import login +from synapse.types import JsonDict, ReadReceipt from tests.unittest import HomeserverTestCase, override_config -class FederationSenderTestCases(HomeserverTestCase): +class FederationSenderReceiptsTestCases(HomeserverTestCase): def make_homeserver(self, reactor, clock): - return super(FederationSenderTestCases, self).setup_test_homeserver( + return self.setup_test_homeserver( state_handler=Mock(spec=["get_current_hosts_in_room"]), federation_transport_client=Mock(spec=["send_transaction"]), ) @@ -147,3 +153,294 @@ class FederationSenderTestCases(HomeserverTestCase): } ], ) + + +class FederationSenderDevicesTestCases(HomeserverTestCase): + servlets = [ + admin.register_servlets, + login.register_servlets, + ] + + def make_homeserver(self, reactor, clock): + return self.setup_test_homeserver( + state_handler=Mock(spec=["get_current_hosts_in_room"]), + federation_transport_client=Mock(spec=["send_transaction"]), + ) + + def default_config(self): + c = super().default_config() + c["send_federation"] = True + return c + + def prepare(self, reactor, clock, hs): + # stub out get_current_hosts_in_room + mock_state_handler = hs.get_state_handler() + mock_state_handler.get_current_hosts_in_room.return_value = ["test", "host2"] + + # stub out get_users_who_share_room_with_user so that it claims that + # `@user2:host2` is in the room + def get_users_who_share_room_with_user(user_id): + return defer.succeed({"@user2:host2"}) + + hs.get_datastore().get_users_who_share_room_with_user = ( + get_users_who_share_room_with_user + ) + + # whenever send_transaction is called, record the edu data + self.edus = [] + self.hs.get_federation_transport_client().send_transaction.side_effect = ( + self.record_transaction + ) + + def record_transaction(self, txn, json_cb): + data = json_cb() + self.edus.extend(data["edus"]) + return defer.succeed({}) + + def test_send_device_updates(self): + """Basic case: each device update should result in an EDU""" + # create a device + u1 = self.register_user("user", "pass") + self.login(u1, "pass", device_id="D1") + + # expect one edu + self.assertEqual(len(self.edus), 1) + stream_id = self.check_device_update_edu(self.edus.pop(0), u1, "D1", None) + + # a second call should produce no new device EDUs + self.hs.get_federation_sender().send_device_messages("host2") + self.pump() + self.assertEqual(self.edus, []) + + # a second device + self.login("user", "pass", device_id="D2") + + self.assertEqual(len(self.edus), 1) + self.check_device_update_edu(self.edus.pop(0), u1, "D2", stream_id) + + def test_upload_signatures(self): + """Uploading signatures on some devices should produce updates for that user""" + + e2e_handler = self.hs.get_e2e_keys_handler() + + # register two devices + u1 = self.register_user("user", "pass") + self.login(u1, "pass", device_id="D1") + self.login(u1, "pass", device_id="D2") + + # expect two edus + self.assertEqual(len(self.edus), 2) + stream_id = None + stream_id = self.check_device_update_edu(self.edus.pop(0), u1, "D1", stream_id) + stream_id = self.check_device_update_edu(self.edus.pop(0), u1, "D2", stream_id) + + # upload signing keys for each device + device1_signing_key = self.generate_and_upload_device_signing_key(u1, "D1") + device2_signing_key = self.generate_and_upload_device_signing_key(u1, "D2") + + # expect two more edus + self.assertEqual(len(self.edus), 2) + stream_id = self.check_device_update_edu(self.edus.pop(0), u1, "D1", stream_id) + stream_id = self.check_device_update_edu(self.edus.pop(0), u1, "D2", stream_id) + + # upload master key and self-signing key + master_signing_key = generate_self_id_key() + master_key = { + "user_id": u1, + "usage": ["master"], + "keys": {key_id(master_signing_key): encode_pubkey(master_signing_key)}, + } + + # private key: HvQBbU+hc2Zr+JP1sE0XwBe1pfZZEYtJNPJLZJtS+F8 + selfsigning_signing_key = generate_self_id_key() + selfsigning_key = { + "user_id": u1, + "usage": ["self_signing"], + "keys": { + key_id(selfsigning_signing_key): encode_pubkey(selfsigning_signing_key) + }, + } + sign.sign_json(selfsigning_key, u1, master_signing_key) + + cross_signing_keys = { + "master_key": master_key, + "self_signing_key": selfsigning_key, + } + + self.get_success( + e2e_handler.upload_signing_keys_for_user(u1, cross_signing_keys) + ) + + # expect signing key update edu + self.assertEqual(len(self.edus), 1) + self.assertEqual(self.edus.pop(0)["edu_type"], "org.matrix.signing_key_update") + + # sign the devices + d1_json = build_device_dict(u1, "D1", device1_signing_key) + sign.sign_json(d1_json, u1, selfsigning_signing_key) + d2_json = build_device_dict(u1, "D2", device2_signing_key) + sign.sign_json(d2_json, u1, selfsigning_signing_key) + + ret = self.get_success( + e2e_handler.upload_signatures_for_device_keys( + u1, {u1: {"D1": d1_json, "D2": d2_json}}, + ) + ) + self.assertEqual(ret["failures"], {}) + + # expect two edus, in one or two transactions. We don't know what order the + # devices will be updated. + self.assertEqual(len(self.edus), 2) + stream_id = None # FIXME: there is a discontinuity in the stream IDs: see #7142 + for edu in self.edus: + self.assertEqual(edu["edu_type"], "m.device_list_update") + c = edu["content"] + if stream_id is not None: + self.assertEqual(c["prev_id"], [stream_id]) + stream_id = c["stream_id"] + devices = {edu["content"]["device_id"] for edu in self.edus} + self.assertEqual({"D1", "D2"}, devices) + + def test_delete_devices(self): + """If devices are deleted, that should result in EDUs too""" + + # create devices + u1 = self.register_user("user", "pass") + self.login("user", "pass", device_id="D1") + self.login("user", "pass", device_id="D2") + self.login("user", "pass", device_id="D3") + + # expect three edus + self.assertEqual(len(self.edus), 3) + stream_id = self.check_device_update_edu(self.edus.pop(0), u1, "D1", None) + stream_id = self.check_device_update_edu(self.edus.pop(0), u1, "D2", stream_id) + stream_id = self.check_device_update_edu(self.edus.pop(0), u1, "D3", stream_id) + + # delete them again + self.get_success( + self.hs.get_device_handler().delete_devices(u1, ["D1", "D2", "D3"]) + ) + + # expect three edus, in an unknown order + self.assertEqual(len(self.edus), 3) + for edu in self.edus: + self.assertEqual(edu["edu_type"], "m.device_list_update") + c = edu["content"] + self.assertGreaterEqual( + c.items(), + {"user_id": u1, "prev_id": [stream_id], "deleted": True}.items(), + ) + stream_id = c["stream_id"] + devices = {edu["content"]["device_id"] for edu in self.edus} + self.assertEqual({"D1", "D2", "D3"}, devices) + + def test_unreachable_server(self): + """If the destination server is unreachable, all the updates should get sent on + recovery + """ + mock_send_txn = self.hs.get_federation_transport_client().send_transaction + mock_send_txn.side_effect = lambda t, cb: defer.fail("fail") + + # create devices + u1 = self.register_user("user", "pass") + self.login("user", "pass", device_id="D1") + self.login("user", "pass", device_id="D2") + self.login("user", "pass", device_id="D3") + + # delete them again + self.get_success( + self.hs.get_device_handler().delete_devices(u1, ["D1", "D2", "D3"]) + ) + + self.assertGreaterEqual(mock_send_txn.call_count, 4) + + # recover the server + mock_send_txn.side_effect = self.record_transaction + self.hs.get_federation_sender().send_device_messages("host2") + self.pump() + + # for each device, there should be a single update + self.assertEqual(len(self.edus), 3) + stream_id = None + for edu in self.edus: + self.assertEqual(edu["edu_type"], "m.device_list_update") + c = edu["content"] + self.assertEqual(c["prev_id"], [stream_id] if stream_id is not None else []) + stream_id = c["stream_id"] + devices = {edu["content"]["device_id"] for edu in self.edus} + self.assertEqual({"D1", "D2", "D3"}, devices) + + def check_device_update_edu( + self, + edu: JsonDict, + user_id: str, + device_id: str, + prev_stream_id: Optional[int], + ) -> int: + """Check that the given EDU is an update for the given device + Returns the stream_id. + """ + self.assertEqual(edu["edu_type"], "m.device_list_update") + content = edu["content"] + + expected = { + "user_id": user_id, + "device_id": device_id, + "prev_id": [prev_stream_id] if prev_stream_id is not None else [], + } + + self.assertLessEqual(expected.items(), content.items()) + return content["stream_id"] + + def check_signing_key_update_txn(self, txn: JsonDict,) -> None: + """Check that the txn has an EDU with a signing key update. + """ + edus = txn["edus"] + self.assertEqual(len(edus), 1) + + def generate_and_upload_device_signing_key( + self, user_id: str, device_id: str + ) -> SigningKey: + """Generate a signing keypair for the given device, and upload it""" + sk = key.generate_signing_key(device_id) + + device_dict = build_device_dict(user_id, device_id, sk) + + self.get_success( + self.hs.get_e2e_keys_handler().upload_keys_for_user( + user_id, device_id, {"device_keys": device_dict}, + ) + ) + return sk + + +def generate_self_id_key() -> SigningKey: + """generate a signing key whose version is its public key + + ... as used by the cross-signing-keys. + """ + k = key.generate_signing_key("x") + k.version = encode_pubkey(k) + return k + + +def key_id(k: BaseKey) -> str: + return "%s:%s" % (k.alg, k.version) + + +def encode_pubkey(sk: SigningKey) -> str: + """Encode the public key corresponding to the given signing key as base64""" + return key.encode_verify_key_base64(key.get_verify_key(sk)) + + +def build_device_dict(user_id: str, device_id: str, sk: SigningKey): + """Build a dict representing the given device""" + return { + "user_id": user_id, + "device_id": device_id, + "algorithms": ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"], + "keys": { + "curve25519:" + device_id: "curve25519+key", + key_id(sk): encode_pubkey(sk), + }, + } diff --git a/tests/unittest.py b/tests/unittest.py index 23b59bea22..3d57b77a5d 100644 --- a/tests/unittest.py +++ b/tests/unittest.py @@ -490,6 +490,7 @@ class HomeserverTestCase(TestCase): "password": password, "admin": admin, "mac": want_mac, + "inhibit_login": True, } ) request, channel = self.make_request( From 09cc058a4c3eae58ee6e08c1925bffd9cf7d52c6 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 27 Mar 2020 12:26:47 +0000 Subject: [PATCH 1248/1623] Always send the user updates to their own device list This will allow clients to notify users about new devices even if the user isn't in any rooms (yet). --- synapse/handlers/device.py | 19 ++++++++++++++++--- synapse/handlers/sync.py | 7 ++++--- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/synapse/handlers/device.py b/synapse/handlers/device.py index a514c30714..54931c355b 100644 --- a/synapse/handlers/device.py +++ b/synapse/handlers/device.py @@ -122,11 +122,22 @@ class DeviceWorkerHandler(BaseHandler): # First we check if any devices have changed for users that we share # rooms with. - users_who_share_room = yield self.store.get_users_who_share_room_with_user( + tracked_users = yield self.store.get_users_who_share_room_with_user( user_id ) + # always tell the user about their own devices + tracked_users.add(user_id) + + logger.info( + "tracked users ids: %r", tracked_users, + ) + changed = yield self.store.get_users_whose_devices_changed( - from_token.device_list_key, users_who_share_room + from_token.device_list_key, tracked_users + ) + + logger.info( + "changed users IDs: %r", changed, ) # Then work out if any users have since joined @@ -456,7 +467,9 @@ class DeviceHandler(DeviceWorkerHandler): room_ids = yield self.store.get_rooms_for_user(user_id) - yield self.notifier.on_new_event("device_list_key", position, rooms=room_ids) + # specify the user ID too since the user should always get their own device list + # updates, even if they aren't in any rooms. + yield self.notifier.on_new_event("device_list_key", position, users=[user_id], rooms=room_ids) if hosts: logger.info( diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index 5746fdea14..fd68a31b09 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -1139,13 +1139,14 @@ class SyncHandler(object): # room with by looking at all users that have left a room plus users # that were in a room we've left. - users_who_share_room = await self.store.get_users_who_share_room_with_user( + users_we_track = await self.store.get_users_who_share_room_with_user( user_id ) + users_we_track.add(user_id) # Step 1a, check for changes in devices of users we share a room with users_that_have_changed = await self.store.get_users_whose_devices_changed( - since_token.device_list_key, users_who_share_room + since_token.device_list_key, users_we_track ) # Step 1b, check for newly joined rooms @@ -1168,7 +1169,7 @@ class SyncHandler(object): newly_left_users.update(left_users) # Remove any users that we still share a room with. - newly_left_users -= users_who_share_room + newly_left_users -= users_we_track return DeviceLists(changed=users_that_have_changed, left=newly_left_users) else: From d9965fb8d678837947cabd41c410127fb59d1b82 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 27 Mar 2020 12:30:59 +0000 Subject: [PATCH 1249/1623] changelog --- changelog.d/7160.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7160.feature diff --git a/changelog.d/7160.feature b/changelog.d/7160.feature new file mode 100644 index 0000000000..c1205969a1 --- /dev/null +++ b/changelog.d/7160.feature @@ -0,0 +1 @@ +Always send users their own device updates. From a07e03ce908f3de4a5ca80d0d7db6da2a6ed98b5 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 27 Mar 2020 12:35:32 +0000 Subject: [PATCH 1250/1623] black --- synapse/handlers/device.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/synapse/handlers/device.py b/synapse/handlers/device.py index 54931c355b..80e7374c6a 100644 --- a/synapse/handlers/device.py +++ b/synapse/handlers/device.py @@ -122,9 +122,7 @@ class DeviceWorkerHandler(BaseHandler): # First we check if any devices have changed for users that we share # rooms with. - tracked_users = yield self.store.get_users_who_share_room_with_user( - user_id - ) + tracked_users = yield self.store.get_users_who_share_room_with_user(user_id) # always tell the user about their own devices tracked_users.add(user_id) @@ -469,7 +467,9 @@ class DeviceHandler(DeviceWorkerHandler): # specify the user ID too since the user should always get their own device list # updates, even if they aren't in any rooms. - yield self.notifier.on_new_event("device_list_key", position, users=[user_id], rooms=room_ids) + yield self.notifier.on_new_event( + "device_list_key", position, users=[user_id], rooms=room_ids + ) if hosts: logger.info( From 16ee97988abb1951e7009620f69904f9bb7c9215 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 27 Mar 2020 12:39:54 +0000 Subject: [PATCH 1251/1623] Fix undefined variable & remove debug logging --- synapse/handlers/device.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/synapse/handlers/device.py b/synapse/handlers/device.py index 80e7374c6a..07ce553995 100644 --- a/synapse/handlers/device.py +++ b/synapse/handlers/device.py @@ -126,18 +126,10 @@ class DeviceWorkerHandler(BaseHandler): # always tell the user about their own devices tracked_users.add(user_id) - logger.info( - "tracked users ids: %r", tracked_users, - ) - changed = yield self.store.get_users_whose_devices_changed( from_token.device_list_key, tracked_users ) - logger.info( - "changed users IDs: %r", changed, - ) - # Then work out if any users have since joined rooms_changed = self.store.get_rooms_that_changed(room_ids, from_token.room_key) @@ -225,8 +217,8 @@ class DeviceWorkerHandler(BaseHandler): if possibly_changed or possibly_left: # Take the intersection of the users whose devices may have changed # and those that actually still share a room with the user - possibly_joined = possibly_changed & users_who_share_room - possibly_left = (possibly_changed | possibly_left) - users_who_share_room + possibly_joined = possibly_changed & tracked_users + possibly_left = (possibly_changed | possibly_left) - tracked_users else: possibly_joined = [] possibly_left = [] From fbf0782c63bd2aba3c504dabd04abdf10d269a22 Mon Sep 17 00:00:00 2001 From: David Vo Date: Sat, 28 Mar 2020 00:20:00 +1100 Subject: [PATCH 1252/1623] Only import sqlite3 when type checking (#7155) Fixes: #7127 Signed-off-by: David Vo --- changelog.d/7155.bugfix | 1 + synapse/storage/engines/sqlite.py | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 changelog.d/7155.bugfix diff --git a/changelog.d/7155.bugfix b/changelog.d/7155.bugfix new file mode 100644 index 0000000000..0bf51e7aba --- /dev/null +++ b/changelog.d/7155.bugfix @@ -0,0 +1 @@ +Avoid importing `sqlite3` when using the postgres backend. Contributed by David Vo. diff --git a/synapse/storage/engines/sqlite.py b/synapse/storage/engines/sqlite.py index 2bfeefd54e..3bc2e8b986 100644 --- a/synapse/storage/engines/sqlite.py +++ b/synapse/storage/engines/sqlite.py @@ -12,14 +12,17 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import sqlite3 import struct import threading +import typing from synapse.storage.engines import BaseDatabaseEngine +if typing.TYPE_CHECKING: + import sqlite3 # noqa: F401 -class Sqlite3Engine(BaseDatabaseEngine[sqlite3.Connection]): + +class Sqlite3Engine(BaseDatabaseEngine["sqlite3.Connection"]): def __init__(self, database_module, database_config): super().__init__(database_module, database_config) From 12aa5a7fa761a729364d324405a033cf78da26de Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Fri, 27 Mar 2020 13:30:22 +0000 Subject: [PATCH 1253/1623] Ensure is_verified on /_matrix/client/r0/room_keys/keys is a boolean (#7150) --- changelog.d/7150.bugfix | 1 + synapse/rest/client/v2_alpha/room_keys.py | 2 +- synapse/storage/data_stores/main/e2e_room_keys.py | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 changelog.d/7150.bugfix diff --git a/changelog.d/7150.bugfix b/changelog.d/7150.bugfix new file mode 100644 index 0000000000..1feb294799 --- /dev/null +++ b/changelog.d/7150.bugfix @@ -0,0 +1 @@ +Ensure `is_verified` is a boolean in responses to `GET /_matrix/client/r0/room_keys/keys`. Also warn the user if they forgot the `version` query param. \ No newline at end of file diff --git a/synapse/rest/client/v2_alpha/room_keys.py b/synapse/rest/client/v2_alpha/room_keys.py index 38952a1d27..59529707df 100644 --- a/synapse/rest/client/v2_alpha/room_keys.py +++ b/synapse/rest/client/v2_alpha/room_keys.py @@ -188,7 +188,7 @@ class RoomKeysServlet(RestServlet): """ requester = await self.auth.get_user_by_req(request, allow_guest=False) user_id = requester.user.to_string() - version = parse_string(request, "version") + version = parse_string(request, "version", required=True) room_keys = await self.e2e_room_keys_handler.get_room_keys( user_id, version, room_id, session_id diff --git a/synapse/storage/data_stores/main/e2e_room_keys.py b/synapse/storage/data_stores/main/e2e_room_keys.py index 84594cf0a9..23f4570c4b 100644 --- a/synapse/storage/data_stores/main/e2e_room_keys.py +++ b/synapse/storage/data_stores/main/e2e_room_keys.py @@ -146,7 +146,8 @@ class EndToEndRoomKeyStore(SQLBaseStore): room_entry["sessions"][row["session_id"]] = { "first_message_index": row["first_message_index"], "forwarded_count": row["forwarded_count"], - "is_verified": row["is_verified"], + # is_verified must be returned to the client as a boolean + "is_verified": bool(row["is_verified"]), "session_data": json.loads(row["session_data"]), } From 63aea691a761a9b6a2058b54792fc5859e12cfba Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 27 Mar 2020 15:09:12 +0100 Subject: [PATCH 1254/1623] Update the wording of the config comment --- docs/sample_config.yaml | 6 +++--- synapse/config/sso.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 556d4419f5..07e922dc27 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -1393,10 +1393,10 @@ sso: # hostname: "https://my.client/". # # If public_baseurl is set, then the login fallback page (used by clients - # that don't have full support for SSO) is always included in this list. + # that don't natively support the required login flows) is whitelisted in + # addition to any URLs in this list. # - # By default, this list is empty, except if public_baseurl is set (in which - # case the login fallback page is the only element in the list). + # By default, this list is empty. # #client_whitelist: # - https://riot.im/develop diff --git a/synapse/config/sso.py b/synapse/config/sso.py index 5ae9db83d0..ec3dca9efc 100644 --- a/synapse/config/sso.py +++ b/synapse/config/sso.py @@ -66,10 +66,10 @@ class SSOConfig(Config): # hostname: "https://my.client/". # # If public_baseurl is set, then the login fallback page (used by clients - # that don't have full support for SSO) is always included in this list. + # that don't natively support the required login flows) is whitelisted in + # addition to any URLs in this list. # - # By default, this list is empty, except if public_baseurl is set (in which - # case the login fallback page is the only element in the list). + # By default, this list is empty. # #client_whitelist: # - https://riot.im/develop From 90246344e340bce3417fb330da6be9338a701c5c Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 27 Mar 2020 15:44:13 +0100 Subject: [PATCH 1255/1623] Improve the UX of the login fallback when using SSO (#7152) * Don't show the login forms if we're currently logging in with a password or a token. * Submit directly the SSO login form, showing only a spinner to the user, in order to eliminate from the clunkiness of SSO through this fallback. --- changelog.d/7152.feature | 1 + synapse/static/client/login/index.html | 2 +- synapse/static/client/login/js/login.js | 51 +++++++++++++++---------- 3 files changed, 32 insertions(+), 22 deletions(-) create mode 100644 changelog.d/7152.feature diff --git a/changelog.d/7152.feature b/changelog.d/7152.feature new file mode 100644 index 0000000000..fafa79c7e7 --- /dev/null +++ b/changelog.d/7152.feature @@ -0,0 +1 @@ +Improve the support for SSO authentication on the login fallback page. diff --git a/synapse/static/client/login/index.html b/synapse/static/client/login/index.html index bcb6bc6bb7..712b0e3980 100644 --- a/synapse/static/client/login/index.html +++ b/synapse/static/client/login/index.html @@ -9,7 +9,7 @@

    -

    Log in with one of the following methods

    +

    diff --git a/synapse/static/client/login/js/login.js b/synapse/static/client/login/js/login.js index 276c271bbe..debe464371 100644 --- a/synapse/static/client/login/js/login.js +++ b/synapse/static/client/login/js/login.js @@ -1,37 +1,41 @@ window.matrixLogin = { endpoint: location.origin + "/_matrix/client/r0/login", serverAcceptsPassword: false, - serverAcceptsCas: false, serverAcceptsSso: false, }; +var title_pre_auth = "Log in with one of the following methods"; +var title_post_auth = "Logging in..."; + var submitPassword = function(user, pwd) { console.log("Logging in with password..."); + set_title(title_post_auth); var data = { type: "m.login.password", user: user, password: pwd, }; $.post(matrixLogin.endpoint, JSON.stringify(data), function(response) { - show_login(); matrixLogin.onLogin(response); }).error(errorFunc); }; var submitToken = function(loginToken) { console.log("Logging in with login token..."); + set_title(title_post_auth); var data = { type: "m.login.token", token: loginToken }; $.post(matrixLogin.endpoint, JSON.stringify(data), function(response) { - show_login(); matrixLogin.onLogin(response); }).error(errorFunc); }; var errorFunc = function(err) { - show_login(); + // We want to show the error to the user rather than redirecting immediately to the + // SSO portal (if SSO is the only login option), so we inhibit the redirect. + show_login(true); if (err.responseJSON && err.responseJSON.error) { setFeedbackString(err.responseJSON.error + " (" + err.responseJSON.errcode + ")"); @@ -45,26 +49,33 @@ var setFeedbackString = function(text) { $("#feedback").text(text); }; -var show_login = function() { - $("#loading").hide(); - +var show_login = function(inhibit_redirect) { var this_page = window.location.origin + window.location.pathname; $("#sso_redirect_url").val(this_page); + // If inhibit_redirect is false, and SSO is the only supported login method, we can + // redirect straight to the SSO page + if (matrixLogin.serverAcceptsSso) { + if (!inhibit_redirect && !matrixLogin.serverAcceptsPassword) { + $("#sso_form").submit(); + return; + } + + // Otherwise, show the SSO form + $("#sso_form").show(); + } + if (matrixLogin.serverAcceptsPassword) { $("#password_flow").show(); } - if (matrixLogin.serverAcceptsSso) { - $("#sso_flow").show(); - } else if (matrixLogin.serverAcceptsCas) { - $("#sso_form").attr("action", "/_matrix/client/r0/login/cas/redirect"); - $("#sso_flow").show(); - } - - if (!matrixLogin.serverAcceptsPassword && !matrixLogin.serverAcceptsCas && !matrixLogin.serverAcceptsSso) { + if (!matrixLogin.serverAcceptsPassword && !matrixLogin.serverAcceptsSso) { $("#no_login_types").show(); } + + set_title(title_pre_auth); + + $("#loading").hide(); }; var show_spinner = function() { @@ -74,17 +85,15 @@ var show_spinner = function() { $("#loading").show(); }; +var set_title = function(title) { + $("#title").text(title); +}; var fetch_info = function(cb) { $.get(matrixLogin.endpoint, function(response) { var serverAcceptsPassword = false; - var serverAcceptsCas = false; for (var i=0; i Date: Fri, 27 Mar 2020 16:02:00 +0100 Subject: [PATCH 1256/1623] update debian installation instructions to recommend installing `virtualenv` instead of `python3-virtualenv` (#6892) * change debian package from python3-virtualenv to virtualenv The virtualenv package is needed for the virtualenv command. The virtualenv package depends on python3-virtualenv (at least since debian jessie) so there is no need to specify python3-virtualenv additionally. Signed-off-by: Vieno Hakkerinen * Add changelog Co-authored-by: Andrew Morgan --- INSTALL.md | 2 +- changelog.d/6892.doc | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/6892.doc diff --git a/INSTALL.md b/INSTALL.md index af9a5ef439..9c6f507db8 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -112,7 +112,7 @@ Installing prerequisites on Ubuntu or Debian: ``` sudo apt-get install build-essential python3-dev libffi-dev \ python3-pip python3-setuptools sqlite3 \ - libssl-dev python3-virtualenv libjpeg-dev libxslt1-dev + libssl-dev virtualenv libjpeg-dev libxslt1-dev ``` #### ArchLinux diff --git a/changelog.d/6892.doc b/changelog.d/6892.doc new file mode 100644 index 0000000000..0d04cf0bdb --- /dev/null +++ b/changelog.d/6892.doc @@ -0,0 +1 @@ +Update Debian installation instructions to recommend installing the `virtualenv` package instead of `python3-virtualenv`. \ No newline at end of file From 8327eb9280cbcb492e05652a96be9f1cd1c0e7c4 Mon Sep 17 00:00:00 2001 From: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com> Date: Fri, 27 Mar 2020 20:15:23 +0100 Subject: [PATCH 1257/1623] Add options to prevent users from changing their profile. (#7096) --- changelog.d/7096.feature | 1 + docs/sample_config.yaml | 23 ++ synapse/config/registration.py | 27 ++ synapse/handlers/profile.py | 16 ++ synapse/rest/client/v2_alpha/account.py | 16 ++ tests/handlers/test_profile.py | 65 ++++- tests/rest/client/v2_alpha/test_account.py | 302 +++++++++++++++++++++ 7 files changed, 449 insertions(+), 1 deletion(-) create mode 100644 changelog.d/7096.feature diff --git a/changelog.d/7096.feature b/changelog.d/7096.feature new file mode 100644 index 0000000000..00f47b2a14 --- /dev/null +++ b/changelog.d/7096.feature @@ -0,0 +1 @@ +Add options to prevent users from changing their profile or associated 3PIDs. \ No newline at end of file diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 1a1d061759..545226f753 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -1086,6 +1086,29 @@ account_threepid_delegates: #email: https://example.com # Delegate email sending to example.com #msisdn: http://localhost:8090 # Delegate SMS sending to this local process +# Whether users are allowed to change their displayname after it has +# been initially set. Useful when provisioning users based on the +# contents of a third-party directory. +# +# Does not apply to server administrators. Defaults to 'true' +# +#enable_set_displayname: false + +# Whether users are allowed to change their avatar after it has been +# initially set. Useful when provisioning users based on the contents +# of a third-party directory. +# +# Does not apply to server administrators. Defaults to 'true' +# +#enable_set_avatar_url: false + +# Whether users can change the 3PIDs associated with their accounts +# (email address and msisdn). +# +# Defaults to 'true' +# +#enable_3pid_changes: false + # Users who register on this homeserver will automatically be joined # to these rooms # diff --git a/synapse/config/registration.py b/synapse/config/registration.py index 9bb3beedbc..e7ea3a01cb 100644 --- a/synapse/config/registration.py +++ b/synapse/config/registration.py @@ -129,6 +129,10 @@ class RegistrationConfig(Config): raise ConfigError("Invalid auto_join_rooms entry %s" % (room_alias,)) self.autocreate_auto_join_rooms = config.get("autocreate_auto_join_rooms", True) + self.enable_set_displayname = config.get("enable_set_displayname", True) + self.enable_set_avatar_url = config.get("enable_set_avatar_url", True) + self.enable_3pid_changes = config.get("enable_3pid_changes", True) + self.disable_msisdn_registration = config.get( "disable_msisdn_registration", False ) @@ -330,6 +334,29 @@ class RegistrationConfig(Config): #email: https://example.com # Delegate email sending to example.com #msisdn: http://localhost:8090 # Delegate SMS sending to this local process + # Whether users are allowed to change their displayname after it has + # been initially set. Useful when provisioning users based on the + # contents of a third-party directory. + # + # Does not apply to server administrators. Defaults to 'true' + # + #enable_set_displayname: false + + # Whether users are allowed to change their avatar after it has been + # initially set. Useful when provisioning users based on the contents + # of a third-party directory. + # + # Does not apply to server administrators. Defaults to 'true' + # + #enable_set_avatar_url: false + + # Whether users can change the 3PIDs associated with their accounts + # (email address and msisdn). + # + # Defaults to 'true' + # + #enable_3pid_changes: false + # Users who register on this homeserver will automatically be joined # to these rooms # diff --git a/synapse/handlers/profile.py b/synapse/handlers/profile.py index 50ce0c585b..6aa1c0f5e0 100644 --- a/synapse/handlers/profile.py +++ b/synapse/handlers/profile.py @@ -157,6 +157,15 @@ class BaseProfileHandler(BaseHandler): if not by_admin and target_user != requester.user: raise AuthError(400, "Cannot set another user's displayname") + if not by_admin and not self.hs.config.enable_set_displayname: + profile = yield self.store.get_profileinfo(target_user.localpart) + if profile.display_name: + raise SynapseError( + 400, + "Changing display name is disabled on this server", + Codes.FORBIDDEN, + ) + if len(new_displayname) > MAX_DISPLAYNAME_LEN: raise SynapseError( 400, "Displayname is too long (max %i)" % (MAX_DISPLAYNAME_LEN,) @@ -218,6 +227,13 @@ class BaseProfileHandler(BaseHandler): if not by_admin and target_user != requester.user: raise AuthError(400, "Cannot set another user's avatar_url") + if not by_admin and not self.hs.config.enable_set_avatar_url: + profile = yield self.store.get_profileinfo(target_user.localpart) + if profile.avatar_url: + raise SynapseError( + 400, "Changing avatar is disabled on this server", Codes.FORBIDDEN + ) + if len(new_avatar_url) > MAX_AVATAR_URL_LEN: raise SynapseError( 400, "Avatar URL is too long (max %i)" % (MAX_AVATAR_URL_LEN,) diff --git a/synapse/rest/client/v2_alpha/account.py b/synapse/rest/client/v2_alpha/account.py index b1249b664c..f80b5e40ea 100644 --- a/synapse/rest/client/v2_alpha/account.py +++ b/synapse/rest/client/v2_alpha/account.py @@ -605,6 +605,11 @@ class ThreepidRestServlet(RestServlet): return 200, {"threepids": threepids} async def on_POST(self, request): + if not self.hs.config.enable_3pid_changes: + raise SynapseError( + 400, "3PID changes are disabled on this server", Codes.FORBIDDEN + ) + requester = await self.auth.get_user_by_req(request) user_id = requester.user.to_string() body = parse_json_object_from_request(request) @@ -649,6 +654,11 @@ class ThreepidAddRestServlet(RestServlet): @interactive_auth_handler async def on_POST(self, request): + if not self.hs.config.enable_3pid_changes: + raise SynapseError( + 400, "3PID changes are disabled on this server", Codes.FORBIDDEN + ) + requester = await self.auth.get_user_by_req(request) user_id = requester.user.to_string() body = parse_json_object_from_request(request) @@ -744,10 +754,16 @@ class ThreepidDeleteRestServlet(RestServlet): def __init__(self, hs): super(ThreepidDeleteRestServlet, self).__init__() + self.hs = hs self.auth = hs.get_auth() self.auth_handler = hs.get_auth_handler() async def on_POST(self, request): + if not self.hs.config.enable_3pid_changes: + raise SynapseError( + 400, "3PID changes are disabled on this server", Codes.FORBIDDEN + ) + body = parse_json_object_from_request(request) assert_params_in_dict(body, ["medium", "address"]) diff --git a/tests/handlers/test_profile.py b/tests/handlers/test_profile.py index d60c124eec..be665262c6 100644 --- a/tests/handlers/test_profile.py +++ b/tests/handlers/test_profile.py @@ -19,7 +19,7 @@ from mock import Mock, NonCallableMock from twisted.internet import defer import synapse.types -from synapse.api.errors import AuthError +from synapse.api.errors import AuthError, SynapseError from synapse.handlers.profile import MasterProfileHandler from synapse.types import UserID @@ -70,6 +70,7 @@ class ProfileTestCase(unittest.TestCase): yield self.store.create_profile(self.frank.localpart) self.handler = hs.get_profile_handler() + self.hs = hs @defer.inlineCallbacks def test_get_my_name(self): @@ -90,6 +91,33 @@ class ProfileTestCase(unittest.TestCase): "Frank Jr.", ) + # Set displayname again + yield self.handler.set_displayname( + self.frank, synapse.types.create_requester(self.frank), "Frank" + ) + + self.assertEquals( + (yield self.store.get_profile_displayname(self.frank.localpart)), "Frank", + ) + + @defer.inlineCallbacks + def test_set_my_name_if_disabled(self): + self.hs.config.enable_set_displayname = False + + # Setting displayname for the first time is allowed + yield self.store.set_profile_displayname(self.frank.localpart, "Frank") + + self.assertEquals( + (yield self.store.get_profile_displayname(self.frank.localpart)), "Frank", + ) + + # Setting displayname a second time is forbidden + d = self.handler.set_displayname( + self.frank, synapse.types.create_requester(self.frank), "Frank Jr." + ) + + yield self.assertFailure(d, SynapseError) + @defer.inlineCallbacks def test_set_my_name_noauth(self): d = self.handler.set_displayname( @@ -147,3 +175,38 @@ class ProfileTestCase(unittest.TestCase): (yield self.store.get_profile_avatar_url(self.frank.localpart)), "http://my.server/pic.gif", ) + + # Set avatar again + yield self.handler.set_avatar_url( + self.frank, + synapse.types.create_requester(self.frank), + "http://my.server/me.png", + ) + + self.assertEquals( + (yield self.store.get_profile_avatar_url(self.frank.localpart)), + "http://my.server/me.png", + ) + + @defer.inlineCallbacks + def test_set_my_avatar_if_disabled(self): + self.hs.config.enable_set_avatar_url = False + + # Setting displayname for the first time is allowed + yield self.store.set_profile_avatar_url( + self.frank.localpart, "http://my.server/me.png" + ) + + self.assertEquals( + (yield self.store.get_profile_avatar_url(self.frank.localpart)), + "http://my.server/me.png", + ) + + # Set avatar a second time is forbidden + d = self.handler.set_avatar_url( + self.frank, + synapse.types.create_requester(self.frank), + "http://my.server/pic.gif", + ) + + yield self.assertFailure(d, SynapseError) diff --git a/tests/rest/client/v2_alpha/test_account.py b/tests/rest/client/v2_alpha/test_account.py index c3facc00eb..45a9d445f8 100644 --- a/tests/rest/client/v2_alpha/test_account.py +++ b/tests/rest/client/v2_alpha/test_account.py @@ -24,6 +24,7 @@ import pkg_resources import synapse.rest.admin from synapse.api.constants import LoginType, Membership +from synapse.api.errors import Codes from synapse.rest.client.v1 import login, room from synapse.rest.client.v2_alpha import account, register @@ -325,3 +326,304 @@ class DeactivateTestCase(unittest.HomeserverTestCase): ) self.render(request) self.assertEqual(request.code, 200) + + +class ThreepidEmailRestTestCase(unittest.HomeserverTestCase): + + servlets = [ + account.register_servlets, + login.register_servlets, + synapse.rest.admin.register_servlets_for_client_rest_resource, + ] + + def make_homeserver(self, reactor, clock): + config = self.default_config() + + # Email config. + self.email_attempts = [] + + def sendmail(smtphost, from_addr, to_addrs, msg, **kwargs): + self.email_attempts.append(msg) + + config["email"] = { + "enable_notifs": False, + "template_dir": os.path.abspath( + pkg_resources.resource_filename("synapse", "res/templates") + ), + "smtp_host": "127.0.0.1", + "smtp_port": 20, + "require_transport_security": False, + "smtp_user": None, + "smtp_pass": None, + "notif_from": "test@example.com", + } + config["public_baseurl"] = "https://example.com" + + self.hs = self.setup_test_homeserver(config=config, sendmail=sendmail) + return self.hs + + def prepare(self, reactor, clock, hs): + self.store = hs.get_datastore() + + self.user_id = self.register_user("kermit", "test") + self.user_id_tok = self.login("kermit", "test") + self.email = "test@example.com" + self.url_3pid = b"account/3pid" + + def test_add_email(self): + """Test adding an email to profile + """ + client_secret = "foobar" + session_id = self._request_token(self.email, client_secret) + + self.assertEquals(len(self.email_attempts), 1) + link = self._get_link_from_email() + + self._validate_token(link) + + request, channel = self.make_request( + "POST", + b"/_matrix/client/unstable/account/3pid/add", + { + "client_secret": client_secret, + "sid": session_id, + "auth": { + "type": "m.login.password", + "user": self.user_id, + "password": "test", + }, + }, + access_token=self.user_id_tok, + ) + + self.render(request) + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + + # Get user + request, channel = self.make_request( + "GET", self.url_3pid, access_token=self.user_id_tok, + ) + self.render(request) + + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("email", channel.json_body["threepids"][0]["medium"]) + self.assertEqual(self.email, channel.json_body["threepids"][0]["address"]) + + def test_add_email_if_disabled(self): + """Test adding email to profile when doing so is disallowed + """ + self.hs.config.enable_3pid_changes = False + + client_secret = "foobar" + session_id = self._request_token(self.email, client_secret) + + self.assertEquals(len(self.email_attempts), 1) + link = self._get_link_from_email() + + self._validate_token(link) + + request, channel = self.make_request( + "POST", + b"/_matrix/client/unstable/account/3pid/add", + { + "client_secret": client_secret, + "sid": session_id, + "auth": { + "type": "m.login.password", + "user": self.user_id, + "password": "test", + }, + }, + access_token=self.user_id_tok, + ) + self.render(request) + self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"]) + + # Get user + request, channel = self.make_request( + "GET", self.url_3pid, access_token=self.user_id_tok, + ) + self.render(request) + + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertFalse(channel.json_body["threepids"]) + + def test_delete_email(self): + """Test deleting an email from profile + """ + # Add a threepid + self.get_success( + self.store.user_add_threepid( + user_id=self.user_id, + medium="email", + address=self.email, + validated_at=0, + added_at=0, + ) + ) + + request, channel = self.make_request( + "POST", + b"account/3pid/delete", + {"medium": "email", "address": self.email}, + access_token=self.user_id_tok, + ) + self.render(request) + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + + # Get user + request, channel = self.make_request( + "GET", self.url_3pid, access_token=self.user_id_tok, + ) + self.render(request) + + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertFalse(channel.json_body["threepids"]) + + def test_delete_email_if_disabled(self): + """Test deleting an email from profile when disallowed + """ + self.hs.config.enable_3pid_changes = False + + # Add a threepid + self.get_success( + self.store.user_add_threepid( + user_id=self.user_id, + medium="email", + address=self.email, + validated_at=0, + added_at=0, + ) + ) + + request, channel = self.make_request( + "POST", + b"account/3pid/delete", + {"medium": "email", "address": self.email}, + access_token=self.user_id_tok, + ) + self.render(request) + + self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"]) + + # Get user + request, channel = self.make_request( + "GET", self.url_3pid, access_token=self.user_id_tok, + ) + self.render(request) + + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("email", channel.json_body["threepids"][0]["medium"]) + self.assertEqual(self.email, channel.json_body["threepids"][0]["address"]) + + def test_cant_add_email_without_clicking_link(self): + """Test that we do actually need to click the link in the email + """ + client_secret = "foobar" + session_id = self._request_token(self.email, client_secret) + + self.assertEquals(len(self.email_attempts), 1) + + # Attempt to add email without clicking the link + request, channel = self.make_request( + "POST", + b"/_matrix/client/unstable/account/3pid/add", + { + "client_secret": client_secret, + "sid": session_id, + "auth": { + "type": "m.login.password", + "user": self.user_id, + "password": "test", + }, + }, + access_token=self.user_id_tok, + ) + self.render(request) + self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(Codes.THREEPID_AUTH_FAILED, channel.json_body["errcode"]) + + # Get user + request, channel = self.make_request( + "GET", self.url_3pid, access_token=self.user_id_tok, + ) + self.render(request) + + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertFalse(channel.json_body["threepids"]) + + def test_no_valid_token(self): + """Test that we do actually need to request a token and can't just + make a session up. + """ + client_secret = "foobar" + session_id = "weasle" + + # Attempt to add email without even requesting an email + request, channel = self.make_request( + "POST", + b"/_matrix/client/unstable/account/3pid/add", + { + "client_secret": client_secret, + "sid": session_id, + "auth": { + "type": "m.login.password", + "user": self.user_id, + "password": "test", + }, + }, + access_token=self.user_id_tok, + ) + self.render(request) + self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(Codes.THREEPID_AUTH_FAILED, channel.json_body["errcode"]) + + # Get user + request, channel = self.make_request( + "GET", self.url_3pid, access_token=self.user_id_tok, + ) + self.render(request) + + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertFalse(channel.json_body["threepids"]) + + def _request_token(self, email, client_secret): + request, channel = self.make_request( + "POST", + b"account/3pid/email/requestToken", + {"client_secret": client_secret, "email": email, "send_attempt": 1}, + ) + self.render(request) + self.assertEquals(200, channel.code, channel.result) + + return channel.json_body["sid"] + + def _validate_token(self, link): + # Remove the host + path = link.replace("https://example.com", "") + + request, channel = self.make_request("GET", path, shorthand=False) + self.render(request) + self.assertEquals(200, channel.code, channel.result) + + def _get_link_from_email(self): + assert self.email_attempts, "No emails have been sent" + + raw_msg = self.email_attempts[-1].decode("UTF-8") + mail = Parser().parsestr(raw_msg) + + text = None + for part in mail.walk(): + if part.get_content_type() == "text/plain": + text = part.get_payload(decode=True).decode("UTF-8") + break + + if not text: + self.fail("Could not find text portion of email to parse") + + match = re.search(r"https://example.com\S+", text) + assert match, "Could not find link in email" + + return match.group(0) From fb69690761762092c8e44d509d4f72408c4c67e0 Mon Sep 17 00:00:00 2001 From: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com> Date: Fri, 27 Mar 2020 20:16:43 +0100 Subject: [PATCH 1258/1623] Admin API to join users to a room. (#7051) --- changelog.d/7051.feature | 1 + docs/admin_api/room_membership.md | 34 ++++ synapse/rest/admin/__init__.py | 7 +- synapse/rest/admin/rooms.py | 79 +++++++- tests/rest/admin/test_room.py | 288 ++++++++++++++++++++++++++++++ 5 files changed, 405 insertions(+), 4 deletions(-) create mode 100644 changelog.d/7051.feature create mode 100644 docs/admin_api/room_membership.md create mode 100644 tests/rest/admin/test_room.py diff --git a/changelog.d/7051.feature b/changelog.d/7051.feature new file mode 100644 index 0000000000..3e36a3f65e --- /dev/null +++ b/changelog.d/7051.feature @@ -0,0 +1 @@ +Admin API `POST /_synapse/admin/v1/join/` to join users to a room like `auto_join_rooms` for creation of users. \ No newline at end of file diff --git a/docs/admin_api/room_membership.md b/docs/admin_api/room_membership.md new file mode 100644 index 0000000000..16736d3d37 --- /dev/null +++ b/docs/admin_api/room_membership.md @@ -0,0 +1,34 @@ +# Edit Room Membership API + +This API allows an administrator to join an user account with a given `user_id` +to a room with a given `room_id_or_alias`. You can only modify the membership of +local users. The server administrator must be in the room and have permission to +invite users. + +## Parameters + +The following parameters are available: + +* `user_id` - Fully qualified user: for example, `@user:server.com`. +* `room_id_or_alias` - The room identifier or alias to join: for example, + `!636q39766251:server.com`. + +## Usage + +``` +POST /_synapse/admin/v1/join/ + +{ + "user_id": "@user:server.com" +} +``` + +Including an `access_token` of a server admin. + +Response: + +``` +{ + "room_id": "!636q39766251:server.com" +} +``` diff --git a/synapse/rest/admin/__init__.py b/synapse/rest/admin/__init__.py index 42cc2b062a..ed70d448a1 100644 --- a/synapse/rest/admin/__init__.py +++ b/synapse/rest/admin/__init__.py @@ -29,7 +29,11 @@ from synapse.rest.admin._base import ( from synapse.rest.admin.groups import DeleteGroupAdminRestServlet from synapse.rest.admin.media import ListMediaInRoom, register_servlets_for_media_repo from synapse.rest.admin.purge_room_servlet import PurgeRoomServlet -from synapse.rest.admin.rooms import ListRoomRestServlet, ShutdownRoomRestServlet +from synapse.rest.admin.rooms import ( + JoinRoomAliasServlet, + ListRoomRestServlet, + ShutdownRoomRestServlet, +) from synapse.rest.admin.server_notice_servlet import SendServerNoticeServlet from synapse.rest.admin.users import ( AccountValidityRenewServlet, @@ -189,6 +193,7 @@ def register_servlets(hs, http_server): """ register_servlets_for_client_rest_resource(hs, http_server) ListRoomRestServlet(hs).register(http_server) + JoinRoomAliasServlet(hs).register(http_server) PurgeRoomServlet(hs).register(http_server) SendServerNoticeServlet(hs).register(http_server) VersionServlet(hs).register(http_server) diff --git a/synapse/rest/admin/rooms.py b/synapse/rest/admin/rooms.py index f9b8c0a4f0..659b8a10ee 100644 --- a/synapse/rest/admin/rooms.py +++ b/synapse/rest/admin/rooms.py @@ -13,9 +13,10 @@ # See the License for the specific language governing permissions and # limitations under the License. import logging +from typing import List, Optional -from synapse.api.constants import Membership -from synapse.api.errors import Codes, SynapseError +from synapse.api.constants import EventTypes, JoinRules, Membership +from synapse.api.errors import Codes, NotFoundError, SynapseError from synapse.http.servlet import ( RestServlet, assert_params_in_dict, @@ -29,7 +30,7 @@ from synapse.rest.admin._base import ( historical_admin_path_patterns, ) from synapse.storage.data_stores.main.room import RoomSortOrder -from synapse.types import create_requester +from synapse.types import RoomAlias, RoomID, UserID, create_requester from synapse.util.async_helpers import maybe_awaitable logger = logging.getLogger(__name__) @@ -237,3 +238,75 @@ class ListRoomRestServlet(RestServlet): response["prev_batch"] = 0 return 200, response + + +class JoinRoomAliasServlet(RestServlet): + + PATTERNS = admin_patterns("/join/(?P[^/]*)") + + def __init__(self, hs): + self.hs = hs + self.auth = hs.get_auth() + self.room_member_handler = hs.get_room_member_handler() + self.admin_handler = hs.get_handlers().admin_handler + self.state_handler = hs.get_state_handler() + + async def on_POST(self, request, room_identifier): + requester = await self.auth.get_user_by_req(request) + await assert_user_is_admin(self.auth, requester.user) + + content = parse_json_object_from_request(request) + + assert_params_in_dict(content, ["user_id"]) + target_user = UserID.from_string(content["user_id"]) + + if not self.hs.is_mine(target_user): + raise SynapseError(400, "This endpoint can only be used with local users") + + if not await self.admin_handler.get_user(target_user): + raise NotFoundError("User not found") + + if RoomID.is_valid(room_identifier): + room_id = room_identifier + try: + remote_room_hosts = [ + x.decode("ascii") for x in request.args[b"server_name"] + ] # type: Optional[List[str]] + except Exception: + remote_room_hosts = None + elif RoomAlias.is_valid(room_identifier): + handler = self.room_member_handler + room_alias = RoomAlias.from_string(room_identifier) + room_id, remote_room_hosts = await handler.lookup_room_alias(room_alias) + room_id = room_id.to_string() + else: + raise SynapseError( + 400, "%s was not legal room ID or room alias" % (room_identifier,) + ) + + fake_requester = create_requester(target_user) + + # send invite if room has "JoinRules.INVITE" + room_state = await self.state_handler.get_current_state(room_id) + join_rules_event = room_state.get((EventTypes.JoinRules, "")) + if join_rules_event: + if not (join_rules_event.content.get("join_rule") == JoinRules.PUBLIC): + await self.room_member_handler.update_membership( + requester=requester, + target=fake_requester.user, + room_id=room_id, + action="invite", + remote_room_hosts=remote_room_hosts, + ratelimit=False, + ) + + await self.room_member_handler.update_membership( + requester=fake_requester, + target=fake_requester.user, + room_id=room_id, + action="join", + remote_room_hosts=remote_room_hosts, + ratelimit=False, + ) + + return 200, {"room_id": room_id} diff --git a/tests/rest/admin/test_room.py b/tests/rest/admin/test_room.py new file mode 100644 index 0000000000..672cc3eac5 --- /dev/null +++ b/tests/rest/admin/test_room.py @@ -0,0 +1,288 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Dirk Klimpel +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json + +import synapse.rest.admin +from synapse.api.errors import Codes +from synapse.rest.client.v1 import login, room + +from tests import unittest + +"""Tests admin REST events for /rooms paths.""" + + +class JoinAliasRoomTestCase(unittest.HomeserverTestCase): + + servlets = [ + synapse.rest.admin.register_servlets, + room.register_servlets, + login.register_servlets, + ] + + def prepare(self, reactor, clock, homeserver): + self.admin_user = self.register_user("admin", "pass", admin=True) + self.admin_user_tok = self.login("admin", "pass") + + self.creator = self.register_user("creator", "test") + self.creator_tok = self.login("creator", "test") + + self.second_user_id = self.register_user("second", "test") + self.second_tok = self.login("second", "test") + + self.public_room_id = self.helper.create_room_as( + self.creator, tok=self.creator_tok, is_public=True + ) + self.url = "/_synapse/admin/v1/join/{}".format(self.public_room_id) + + def test_requester_is_no_admin(self): + """ + If the user is not a server admin, an error 403 is returned. + """ + body = json.dumps({"user_id": self.second_user_id}) + + request, channel = self.make_request( + "POST", + self.url, + content=body.encode(encoding="utf_8"), + access_token=self.second_tok, + ) + self.render(request) + + self.assertEqual(403, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"]) + + def test_invalid_parameter(self): + """ + If a parameter is missing, return an error + """ + body = json.dumps({"unknown_parameter": "@unknown:test"}) + + request, channel = self.make_request( + "POST", + self.url, + content=body.encode(encoding="utf_8"), + access_token=self.admin_user_tok, + ) + self.render(request) + + self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(Codes.MISSING_PARAM, channel.json_body["errcode"]) + + def test_local_user_does_not_exist(self): + """ + Tests that a lookup for a user that does not exist returns a 404 + """ + body = json.dumps({"user_id": "@unknown:test"}) + + request, channel = self.make_request( + "POST", + self.url, + content=body.encode(encoding="utf_8"), + access_token=self.admin_user_tok, + ) + self.render(request) + + self.assertEqual(404, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(Codes.NOT_FOUND, channel.json_body["errcode"]) + + def test_remote_user(self): + """ + Check that only local user can join rooms. + """ + body = json.dumps({"user_id": "@not:exist.bla"}) + + request, channel = self.make_request( + "POST", + self.url, + content=body.encode(encoding="utf_8"), + access_token=self.admin_user_tok, + ) + self.render(request) + + self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual( + "This endpoint can only be used with local users", + channel.json_body["error"], + ) + + def test_room_does_not_exist(self): + """ + Check that unknown rooms/server return error 404. + """ + body = json.dumps({"user_id": self.second_user_id}) + url = "/_synapse/admin/v1/join/!unknown:test" + + request, channel = self.make_request( + "POST", + url, + content=body.encode(encoding="utf_8"), + access_token=self.admin_user_tok, + ) + self.render(request) + + self.assertEqual(404, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("No known servers", channel.json_body["error"]) + + def test_room_is_not_valid(self): + """ + Check that invalid room names, return an error 400. + """ + body = json.dumps({"user_id": self.second_user_id}) + url = "/_synapse/admin/v1/join/invalidroom" + + request, channel = self.make_request( + "POST", + url, + content=body.encode(encoding="utf_8"), + access_token=self.admin_user_tok, + ) + self.render(request) + + self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual( + "invalidroom was not legal room ID or room alias", + channel.json_body["error"], + ) + + def test_join_public_room(self): + """ + Test joining a local user to a public room with "JoinRules.PUBLIC" + """ + body = json.dumps({"user_id": self.second_user_id}) + + request, channel = self.make_request( + "POST", + self.url, + content=body.encode(encoding="utf_8"), + access_token=self.admin_user_tok, + ) + self.render(request) + + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(self.public_room_id, channel.json_body["room_id"]) + + # Validate if user is a member of the room + + request, channel = self.make_request( + "GET", "/_matrix/client/r0/joined_rooms", access_token=self.second_tok, + ) + self.render(request) + self.assertEquals(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(self.public_room_id, channel.json_body["joined_rooms"][0]) + + def test_join_private_room_if_not_member(self): + """ + Test joining a local user to a private room with "JoinRules.INVITE" + when server admin is not member of this room. + """ + private_room_id = self.helper.create_room_as( + self.creator, tok=self.creator_tok, is_public=False + ) + url = "/_synapse/admin/v1/join/{}".format(private_room_id) + body = json.dumps({"user_id": self.second_user_id}) + + request, channel = self.make_request( + "POST", + url, + content=body.encode(encoding="utf_8"), + access_token=self.admin_user_tok, + ) + self.render(request) + + self.assertEqual(403, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"]) + + def test_join_private_room_if_member(self): + """ + Test joining a local user to a private room with "JoinRules.INVITE", + when server admin is member of this room. + """ + private_room_id = self.helper.create_room_as( + self.creator, tok=self.creator_tok, is_public=False + ) + self.helper.invite( + room=private_room_id, + src=self.creator, + targ=self.admin_user, + tok=self.creator_tok, + ) + self.helper.join( + room=private_room_id, user=self.admin_user, tok=self.admin_user_tok + ) + + # Validate if server admin is a member of the room + + request, channel = self.make_request( + "GET", "/_matrix/client/r0/joined_rooms", access_token=self.admin_user_tok, + ) + self.render(request) + self.assertEquals(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(private_room_id, channel.json_body["joined_rooms"][0]) + + # Join user to room. + + url = "/_synapse/admin/v1/join/{}".format(private_room_id) + body = json.dumps({"user_id": self.second_user_id}) + + request, channel = self.make_request( + "POST", + url, + content=body.encode(encoding="utf_8"), + access_token=self.admin_user_tok, + ) + self.render(request) + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(private_room_id, channel.json_body["room_id"]) + + # Validate if user is a member of the room + + request, channel = self.make_request( + "GET", "/_matrix/client/r0/joined_rooms", access_token=self.second_tok, + ) + self.render(request) + self.assertEquals(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(private_room_id, channel.json_body["joined_rooms"][0]) + + def test_join_private_room_if_owner(self): + """ + Test joining a local user to a private room with "JoinRules.INVITE", + when server admin is owner of this room. + """ + private_room_id = self.helper.create_room_as( + self.admin_user, tok=self.admin_user_tok, is_public=False + ) + url = "/_synapse/admin/v1/join/{}".format(private_room_id) + body = json.dumps({"user_id": self.second_user_id}) + + request, channel = self.make_request( + "POST", + url, + content=body.encode(encoding="utf_8"), + access_token=self.admin_user_tok, + ) + self.render(request) + + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(private_room_id, channel.json_body["room_id"]) + + # Validate if user is a member of the room + + request, channel = self.make_request( + "GET", "/_matrix/client/r0/joined_rooms", access_token=self.second_tok, + ) + self.render(request) + self.assertEquals(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(private_room_id, channel.json_body["joined_rooms"][0]) From 84f7eaed16a8169f1d70d047c9354c8232b9fb9f Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 27 Mar 2020 15:44:13 +0100 Subject: [PATCH 1259/1623] Improve the UX of the login fallback when using SSO (#7152) * Don't show the login forms if we're currently logging in with a password or a token. * Submit directly the SSO login form, showing only a spinner to the user, in order to eliminate from the clunkiness of SSO through this fallback. --- changelog.d/7152.feature | 1 + synapse/static/client/login/index.html | 2 +- synapse/static/client/login/js/login.js | 51 +++++++++++++++---------- 3 files changed, 32 insertions(+), 22 deletions(-) create mode 100644 changelog.d/7152.feature diff --git a/changelog.d/7152.feature b/changelog.d/7152.feature new file mode 100644 index 0000000000..fafa79c7e7 --- /dev/null +++ b/changelog.d/7152.feature @@ -0,0 +1 @@ +Improve the support for SSO authentication on the login fallback page. diff --git a/synapse/static/client/login/index.html b/synapse/static/client/login/index.html index bcb6bc6bb7..712b0e3980 100644 --- a/synapse/static/client/login/index.html +++ b/synapse/static/client/login/index.html @@ -9,7 +9,7 @@

    -

    Log in with one of the following methods

    +

    diff --git a/synapse/static/client/login/js/login.js b/synapse/static/client/login/js/login.js index 276c271bbe..debe464371 100644 --- a/synapse/static/client/login/js/login.js +++ b/synapse/static/client/login/js/login.js @@ -1,37 +1,41 @@ window.matrixLogin = { endpoint: location.origin + "/_matrix/client/r0/login", serverAcceptsPassword: false, - serverAcceptsCas: false, serverAcceptsSso: false, }; +var title_pre_auth = "Log in with one of the following methods"; +var title_post_auth = "Logging in..."; + var submitPassword = function(user, pwd) { console.log("Logging in with password..."); + set_title(title_post_auth); var data = { type: "m.login.password", user: user, password: pwd, }; $.post(matrixLogin.endpoint, JSON.stringify(data), function(response) { - show_login(); matrixLogin.onLogin(response); }).error(errorFunc); }; var submitToken = function(loginToken) { console.log("Logging in with login token..."); + set_title(title_post_auth); var data = { type: "m.login.token", token: loginToken }; $.post(matrixLogin.endpoint, JSON.stringify(data), function(response) { - show_login(); matrixLogin.onLogin(response); }).error(errorFunc); }; var errorFunc = function(err) { - show_login(); + // We want to show the error to the user rather than redirecting immediately to the + // SSO portal (if SSO is the only login option), so we inhibit the redirect. + show_login(true); if (err.responseJSON && err.responseJSON.error) { setFeedbackString(err.responseJSON.error + " (" + err.responseJSON.errcode + ")"); @@ -45,26 +49,33 @@ var setFeedbackString = function(text) { $("#feedback").text(text); }; -var show_login = function() { - $("#loading").hide(); - +var show_login = function(inhibit_redirect) { var this_page = window.location.origin + window.location.pathname; $("#sso_redirect_url").val(this_page); + // If inhibit_redirect is false, and SSO is the only supported login method, we can + // redirect straight to the SSO page + if (matrixLogin.serverAcceptsSso) { + if (!inhibit_redirect && !matrixLogin.serverAcceptsPassword) { + $("#sso_form").submit(); + return; + } + + // Otherwise, show the SSO form + $("#sso_form").show(); + } + if (matrixLogin.serverAcceptsPassword) { $("#password_flow").show(); } - if (matrixLogin.serverAcceptsSso) { - $("#sso_flow").show(); - } else if (matrixLogin.serverAcceptsCas) { - $("#sso_form").attr("action", "/_matrix/client/r0/login/cas/redirect"); - $("#sso_flow").show(); - } - - if (!matrixLogin.serverAcceptsPassword && !matrixLogin.serverAcceptsCas && !matrixLogin.serverAcceptsSso) { + if (!matrixLogin.serverAcceptsPassword && !matrixLogin.serverAcceptsSso) { $("#no_login_types").show(); } + + set_title(title_pre_auth); + + $("#loading").hide(); }; var show_spinner = function() { @@ -74,17 +85,15 @@ var show_spinner = function() { $("#loading").show(); }; +var set_title = function(title) { + $("#title").text(title); +}; var fetch_info = function(cb) { $.get(matrixLogin.endpoint, function(response) { var serverAcceptsPassword = false; - var serverAcceptsCas = false; for (var i=0; i Date: Fri, 27 Mar 2020 20:24:52 +0000 Subject: [PATCH 1260/1623] Always whitelist the login fallback for SSO (#7153) That fallback sets the redirect URL to itself (so it can process the login token then return gracefully to the client). This would make it pointless to ask the user for confirmation, since the URL the confirmation page would be showing wouldn't be the client's. --- changelog.d/7153.feature | 1 + docs/sample_config.yaml | 4 ++++ synapse/config/sso.py | 15 +++++++++++++++ tests/rest/client/v1/test_login.py | 9 ++++++++- 4 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 changelog.d/7153.feature diff --git a/changelog.d/7153.feature b/changelog.d/7153.feature new file mode 100644 index 0000000000..414ebe1f69 --- /dev/null +++ b/changelog.d/7153.feature @@ -0,0 +1 @@ +Always whitelist the login fallback in the SSO configuration if `public_baseurl` is set. diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 545226f753..743949945a 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -1444,6 +1444,10 @@ sso: # phishing attacks from evil.site. To avoid this, include a slash after the # hostname: "https://my.client/". # + # If public_baseurl is set, then the login fallback page (used by clients + # that don't natively support the required login flows) is whitelisted in + # addition to any URLs in this list. + # # By default, this list is empty. # #client_whitelist: diff --git a/synapse/config/sso.py b/synapse/config/sso.py index 95762689bc..ec3dca9efc 100644 --- a/synapse/config/sso.py +++ b/synapse/config/sso.py @@ -39,6 +39,17 @@ class SSOConfig(Config): self.sso_client_whitelist = sso_config.get("client_whitelist") or [] + # Attempt to also whitelist the server's login fallback, since that fallback sets + # the redirect URL to itself (so it can process the login token then return + # gracefully to the client). This would make it pointless to ask the user for + # confirmation, since the URL the confirmation page would be showing wouldn't be + # the client's. + # public_baseurl is an optional setting, so we only add the fallback's URL to the + # list if it's provided (because we can't figure out what that URL is otherwise). + if self.public_baseurl: + login_fallback_url = self.public_baseurl + "_matrix/static/client/login" + self.sso_client_whitelist.append(login_fallback_url) + def generate_config_section(self, **kwargs): return """\ # Additional settings to use with single-sign on systems such as SAML2 and CAS. @@ -54,6 +65,10 @@ class SSOConfig(Config): # phishing attacks from evil.site. To avoid this, include a slash after the # hostname: "https://my.client/". # + # If public_baseurl is set, then the login fallback page (used by clients + # that don't natively support the required login flows) is whitelisted in + # addition to any URLs in this list. + # # By default, this list is empty. # #client_whitelist: diff --git a/tests/rest/client/v1/test_login.py b/tests/rest/client/v1/test_login.py index da2c9bfa1e..aed8853d6e 100644 --- a/tests/rest/client/v1/test_login.py +++ b/tests/rest/client/v1/test_login.py @@ -350,7 +350,14 @@ class CASRedirectConfirmTestCase(unittest.HomeserverTestCase): def test_cas_redirect_whitelisted(self): """Tests that the SSO login flow serves a redirect to a whitelisted url """ - redirect_url = "https://legit-site.com/" + self._test_redirect("https://legit-site.com/") + + @override_config({"public_baseurl": "https://example.com"}) + def test_cas_redirect_login_fallback(self): + self._test_redirect("https://example.com/_matrix/static/client/login") + + def _test_redirect(self, redirect_url): + """Tests that the SSO login flow serves a redirect for the given redirect URL.""" cas_ticket_url = ( "/_matrix/client/r0/login/cas/ticket?redirectUrl=%s&ticket=ticket" % (urllib.parse.quote(redirect_url)) From 9fc588e6dc03bb64f569b4e27a786abd78c36218 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 30 Mar 2020 10:11:26 +0100 Subject: [PATCH 1261/1623] Just add own user ID to the list we track device changes for --- synapse/handlers/device.py | 8 +++++--- synapse/handlers/sync.py | 10 ++++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/synapse/handlers/device.py b/synapse/handlers/device.py index 07ce553995..51bfad01c8 100644 --- a/synapse/handlers/device.py +++ b/synapse/handlers/device.py @@ -122,7 +122,9 @@ class DeviceWorkerHandler(BaseHandler): # First we check if any devices have changed for users that we share # rooms with. - tracked_users = yield self.store.get_users_who_share_room_with_user(user_id) + users_who_share_room = yield self.store.get_users_who_share_room_with_user(user_id) + + tracked_users = set(users_who_share_room) # always tell the user about their own devices tracked_users.add(user_id) @@ -217,8 +219,8 @@ class DeviceWorkerHandler(BaseHandler): if possibly_changed or possibly_left: # Take the intersection of the users whose devices may have changed # and those that actually still share a room with the user - possibly_joined = possibly_changed & tracked_users - possibly_left = (possibly_changed | possibly_left) - tracked_users + possibly_joined = possibly_changed & users_who_share_room + possibly_left = (possibly_changed | possibly_left) - users_who_share_room else: possibly_joined = [] possibly_left = [] diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index fd68a31b09..725f41c4d9 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -1139,14 +1139,16 @@ class SyncHandler(object): # room with by looking at all users that have left a room plus users # that were in a room we've left. - users_we_track = await self.store.get_users_who_share_room_with_user( + users_who_share_room = await self.store.get_users_who_share_room_with_user( user_id ) - users_we_track.add(user_id) + + tracked_users = set(users_who_share_room) + tracked_users.add(user_id) # Step 1a, check for changes in devices of users we share a room with users_that_have_changed = await self.store.get_users_whose_devices_changed( - since_token.device_list_key, users_we_track + since_token.device_list_key, tracked_users ) # Step 1b, check for newly joined rooms @@ -1169,7 +1171,7 @@ class SyncHandler(object): newly_left_users.update(left_users) # Remove any users that we still share a room with. - newly_left_users -= users_we_track + newly_left_users -= users_who_share_room return DeviceLists(changed=users_that_have_changed, left=newly_left_users) else: From 740647752576228e469918bafbb97ff556ff5ebe Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 30 Mar 2020 10:18:33 +0100 Subject: [PATCH 1262/1623] black --- synapse/handlers/device.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/synapse/handlers/device.py b/synapse/handlers/device.py index 51bfad01c8..d256b86ce5 100644 --- a/synapse/handlers/device.py +++ b/synapse/handlers/device.py @@ -122,7 +122,9 @@ class DeviceWorkerHandler(BaseHandler): # First we check if any devices have changed for users that we share # rooms with. - users_who_share_room = yield self.store.get_users_who_share_room_with_user(user_id) + users_who_share_room = yield self.store.get_users_who_share_room_with_user( + user_id + ) tracked_users = set(users_who_share_room) # always tell the user about their own devices From c5f89fba55b2529b2c8a76e272a21d551ffa82fe Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Mon, 30 Mar 2020 07:28:42 -0400 Subject: [PATCH 1263/1623] Add developer documentation for running a local CAS server (#7147) --- changelog.d/7147.doc | 1 + docs/dev/cas.md | 64 ++++++++++++++++++++++++++++++++++++++++++++ docs/dev/saml.md | 8 ++++-- 3 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 changelog.d/7147.doc create mode 100644 docs/dev/cas.md diff --git a/changelog.d/7147.doc b/changelog.d/7147.doc new file mode 100644 index 0000000000..2c855ff5f7 --- /dev/null +++ b/changelog.d/7147.doc @@ -0,0 +1 @@ +Add documentation for running a local CAS server for testing. diff --git a/docs/dev/cas.md b/docs/dev/cas.md new file mode 100644 index 0000000000..f8d02cc82c --- /dev/null +++ b/docs/dev/cas.md @@ -0,0 +1,64 @@ +# How to test CAS as a developer without a server + +The [django-mama-cas](https://github.com/jbittel/django-mama-cas) project is an +easy to run CAS implementation built on top of Django. + +## Prerequisites + +1. Create a new virtualenv: `python3 -m venv ` +2. Activate your virtualenv: `source /path/to/your/virtualenv/bin/activate` +3. Install Django and django-mama-cas: + ``` + python -m pip install "django<3" "django-mama-cas==2.4.0" + ``` +4. Create a Django project in the current directory: + ``` + django-admin startproject cas_test . + ``` +5. Follow the [install directions](https://django-mama-cas.readthedocs.io/en/latest/installation.html#configuring) for django-mama-cas +6. Setup the SQLite database: `python manage.py migrate` +7. Create a user: + ``` + python manage.py createsuperuser + ``` + 1. Use whatever you want as the username and password. + 2. Leave the other fields blank. +8. Use the built-in Django test server to serve the CAS endpoints on port 8000: + ``` + python manage.py runserver + ``` + +You should now have a Django project configured to serve CAS authentication with +a single user created. + +## Configure Synapse (and Riot) to use CAS + +1. Modify your `homeserver.yaml` to enable CAS and point it to your locally + running Django test server: + ```yaml + cas_config: + enabled: true + server_url: "http://localhost:8000" + service_url: "http://localhost:8081" + #displayname_attribute: name + #required_attributes: + # name: value + ``` +2. Restart Synapse. + +Note that the above configuration assumes the homeserver is running on port 8081 +and that the CAS server is on port 8000, both on localhost. + +## Testing the configuration + +Then in Riot: + +1. Visit the login page with a Riot pointing at your homeserver. +2. Click the Single Sign-On button. +3. Login using the credentials created with `createsuperuser`. +4. You should be logged in. + +If you want to repeat this process you'll need to manually logout first: + +1. http://localhost:8000/admin/ +2. Click "logout" in the top right. diff --git a/docs/dev/saml.md b/docs/dev/saml.md index f41aadce47..a9bfd2dc05 100644 --- a/docs/dev/saml.md +++ b/docs/dev/saml.md @@ -18,9 +18,13 @@ To make Synapse (and therefore Riot) use it: metadata: local: ["samling.xml"] ``` -5. Run `apt-get install xmlsec1` and `pip install --upgrade --force 'pysaml2>=4.5.0'` to ensure +5. Ensure that your `homeserver.yaml` has a setting for `public_baseurl`: + ```yaml + public_baseurl: http://localhost:8080/ + ``` +6. Run `apt-get install xmlsec1` and `pip install --upgrade --force 'pysaml2>=4.5.0'` to ensure the dependencies are installed and ready to go. -6. Restart Synapse. +7. Restart Synapse. Then in Riot: From e577c5d607b1edb827a5a137dd77587e394fdf5b Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 30 Mar 2020 13:55:01 +0100 Subject: [PATCH 1264/1623] Attempt to clarify Python version requirements (#7161) In particular, we depend on `typing.TYPE_CHECKING`, which is only present in 3.5.2. It turns out that Ubuntu Xenial, despite having a package called `python 3 (3.5.1-3)`, actually has python 3.5.2, so I think this is fine. --- INSTALL.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index c0926ba590..8ded6e9092 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -36,7 +36,7 @@ that your email address is probably `user@example.com` rather than System requirements: - POSIX-compliant system (tested on Linux & OS X) -- Python 3.5, 3.6, 3.7 or 3.8. +- Python 3.5.2 or later, up to Python 3.8. - At least 1GB of free RAM if you want to join large public rooms like #matrix:matrix.org Synapse is written in Python but some of the libraries it uses are written in @@ -427,8 +427,8 @@ so, you will need to edit `homeserver.yaml`, as follows: for having Synapse automatically provision and renew federation certificates through ACME can be found at [ACME.md](docs/ACME.md). Note that, as pointed out in that document, this feature will not - work with installs set up after November 2019. - + work with installs set up after November 2019. + If you are using your own certificate, be sure to use a `.pem` file that includes the full certificate chain including any intermediate certificates (for instance, if using certbot, use `fullchain.pem` as your certificate, not From 104844c1e1a99df9c4a7e022e715517578533db7 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Mon, 30 Mar 2020 14:00:11 +0100 Subject: [PATCH 1265/1623] Add explanatory comment --- synapse/handlers/device.py | 3 ++- synapse/handlers/sync.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/synapse/handlers/device.py b/synapse/handlers/device.py index d256b86ce5..993499f446 100644 --- a/synapse/handlers/device.py +++ b/synapse/handlers/device.py @@ -127,7 +127,8 @@ class DeviceWorkerHandler(BaseHandler): ) tracked_users = set(users_who_share_room) - # always tell the user about their own devices + + # Always tell the user about their own devices tracked_users.add(user_id) changed = yield self.store.get_users_whose_devices_changed( diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index 725f41c4d9..1f1cde2feb 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -1144,6 +1144,8 @@ class SyncHandler(object): ) tracked_users = set(users_who_share_room) + + # Always tell the user about their own devices tracked_users.add(user_id) # Step 1a, check for changes in devices of users we share a room with From 4f21c33be301b8ea6369039c3ad8baa51878e4d5 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 30 Mar 2020 16:37:24 +0100 Subject: [PATCH 1266/1623] Remove usage of "conn_id" for presence. (#7128) * Remove `conn_id` usage for UserSyncCommand. Each tcp replication connection is assigned a "conn_id", which is used to give an ID to a remotely connected worker. In a redis world, there will no longer be a one to one mapping between connection and instance, so instead we need to replace such usages with an ID generated by the remote instances and included in the replicaiton commands. This really only effects UserSyncCommand. * Add CLEAR_USER_SYNCS command that is sent on shutdown. This should help with the case where a synchrotron gets restarted gracefully, rather than rely on 5 minute timeout. --- changelog.d/7128.misc | 1 + docs/tcp_replication.md | 6 +++++ synapse/app/generic_worker.py | 20 ++++++++++++---- synapse/replication/tcp/client.py | 6 +++-- synapse/replication/tcp/commands.py | 36 +++++++++++++++++++++++++---- synapse/replication/tcp/protocol.py | 9 ++++++-- synapse/replication/tcp/resource.py | 17 ++++++-------- synapse/server.py | 11 +++++++++ synapse/server.pyi | 2 ++ 9 files changed, 86 insertions(+), 22 deletions(-) create mode 100644 changelog.d/7128.misc diff --git a/changelog.d/7128.misc b/changelog.d/7128.misc new file mode 100644 index 0000000000..5703f6d2ec --- /dev/null +++ b/changelog.d/7128.misc @@ -0,0 +1 @@ +Add explicit `instance_id` for USER_SYNC commands and remove implicit `conn_id` usage. diff --git a/docs/tcp_replication.md b/docs/tcp_replication.md index d4f7d9ec18..3be8e50c4c 100644 --- a/docs/tcp_replication.md +++ b/docs/tcp_replication.md @@ -198,6 +198,12 @@ Asks the server for the current position of all streams. A user has started or stopped syncing +#### CLEAR_USER_SYNC (C) + + The server should clear all associated user sync data from the worker. + + This is used when a worker is shutting down. + #### FEDERATION_ACK (C) Acknowledge receipt of some federation data diff --git a/synapse/app/generic_worker.py b/synapse/app/generic_worker.py index fba7ad9551..1ee266f7c5 100644 --- a/synapse/app/generic_worker.py +++ b/synapse/app/generic_worker.py @@ -65,6 +65,7 @@ from synapse.replication.slave.storage.registration import SlavedRegistrationSto from synapse.replication.slave.storage.room import RoomStore from synapse.replication.slave.storage.transactions import SlavedTransactionStore from synapse.replication.tcp.client import ReplicationClientHandler +from synapse.replication.tcp.commands import ClearUserSyncsCommand from synapse.replication.tcp.streams import ( AccountDataStream, DeviceListsStream, @@ -124,7 +125,6 @@ from synapse.types import ReadReceipt from synapse.util.async_helpers import Linearizer from synapse.util.httpresourcetree import create_resource_tree from synapse.util.manhole import manhole -from synapse.util.stringutils import random_string from synapse.util.versionstring import get_version_string logger = logging.getLogger("synapse.app.generic_worker") @@ -233,6 +233,7 @@ class GenericWorkerPresence(object): self.user_to_num_current_syncs = {} self.clock = hs.get_clock() self.notifier = hs.get_notifier() + self.instance_id = hs.get_instance_id() active_presence = self.store.take_presence_startup_info() self.user_to_current_state = {state.user_id: state for state in active_presence} @@ -245,13 +246,24 @@ class GenericWorkerPresence(object): self.send_stop_syncing, UPDATE_SYNCING_USERS_MS ) - self.process_id = random_string(16) - logger.info("Presence process_id is %r", self.process_id) + hs.get_reactor().addSystemEventTrigger( + "before", + "shutdown", + run_as_background_process, + "generic_presence.on_shutdown", + self._on_shutdown, + ) + + def _on_shutdown(self): + if self.hs.config.use_presence: + self.hs.get_tcp_replication().send_command( + ClearUserSyncsCommand(self.instance_id) + ) def send_user_sync(self, user_id, is_syncing, last_sync_ms): if self.hs.config.use_presence: self.hs.get_tcp_replication().send_user_sync( - user_id, is_syncing, last_sync_ms + self.instance_id, user_id, is_syncing, last_sync_ms ) def mark_as_coming_online(self, user_id): diff --git a/synapse/replication/tcp/client.py b/synapse/replication/tcp/client.py index 7e7ad0f798..e86d9805f1 100644 --- a/synapse/replication/tcp/client.py +++ b/synapse/replication/tcp/client.py @@ -189,10 +189,12 @@ class ReplicationClientHandler(AbstractReplicationClientHandler): """ self.send_command(FederationAckCommand(token)) - def send_user_sync(self, user_id, is_syncing, last_sync_ms): + def send_user_sync(self, instance_id, user_id, is_syncing, last_sync_ms): """Poke the master that a user has started/stopped syncing. """ - self.send_command(UserSyncCommand(user_id, is_syncing, last_sync_ms)) + self.send_command( + UserSyncCommand(instance_id, user_id, is_syncing, last_sync_ms) + ) def send_remove_pusher(self, app_id, push_key, user_id): """Poke the master to remove a pusher for a user diff --git a/synapse/replication/tcp/commands.py b/synapse/replication/tcp/commands.py index 5a6b734094..e4eec643f7 100644 --- a/synapse/replication/tcp/commands.py +++ b/synapse/replication/tcp/commands.py @@ -207,30 +207,32 @@ class UserSyncCommand(Command): Format:: - USER_SYNC + USER_SYNC Where is either "start" or "stop" """ NAME = "USER_SYNC" - def __init__(self, user_id, is_syncing, last_sync_ms): + def __init__(self, instance_id, user_id, is_syncing, last_sync_ms): + self.instance_id = instance_id self.user_id = user_id self.is_syncing = is_syncing self.last_sync_ms = last_sync_ms @classmethod def from_line(cls, line): - user_id, state, last_sync_ms = line.split(" ", 2) + instance_id, user_id, state, last_sync_ms = line.split(" ", 3) if state not in ("start", "end"): raise Exception("Invalid USER_SYNC state %r" % (state,)) - return cls(user_id, state == "start", int(last_sync_ms)) + return cls(instance_id, user_id, state == "start", int(last_sync_ms)) def to_line(self): return " ".join( ( + self.instance_id, self.user_id, "start" if self.is_syncing else "end", str(self.last_sync_ms), @@ -238,6 +240,30 @@ class UserSyncCommand(Command): ) +class ClearUserSyncsCommand(Command): + """Sent by the client to inform the server that it should drop all + information about syncing users sent by the client. + + Mainly used when client is about to shut down. + + Format:: + + CLEAR_USER_SYNC + """ + + NAME = "CLEAR_USER_SYNC" + + def __init__(self, instance_id): + self.instance_id = instance_id + + @classmethod + def from_line(cls, line): + return cls(line) + + def to_line(self): + return self.instance_id + + class FederationAckCommand(Command): """Sent by the client when it has processed up to a given point in the federation stream. This allows the master to drop in-memory caches of the @@ -398,6 +424,7 @@ _COMMANDS = ( InvalidateCacheCommand, UserIpCommand, RemoteServerUpCommand, + ClearUserSyncsCommand, ) # type: Tuple[Type[Command], ...] # Map of command name to command type. @@ -420,6 +447,7 @@ VALID_CLIENT_COMMANDS = ( ReplicateCommand.NAME, PingCommand.NAME, UserSyncCommand.NAME, + ClearUserSyncsCommand.NAME, FederationAckCommand.NAME, RemovePusherCommand.NAME, InvalidateCacheCommand.NAME, diff --git a/synapse/replication/tcp/protocol.py b/synapse/replication/tcp/protocol.py index f81d2e2442..dae246825f 100644 --- a/synapse/replication/tcp/protocol.py +++ b/synapse/replication/tcp/protocol.py @@ -423,9 +423,12 @@ class ServerReplicationStreamProtocol(BaseReplicationStreamProtocol): async def on_USER_SYNC(self, cmd): await self.streamer.on_user_sync( - self.conn_id, cmd.user_id, cmd.is_syncing, cmd.last_sync_ms + cmd.instance_id, cmd.user_id, cmd.is_syncing, cmd.last_sync_ms ) + async def on_CLEAR_USER_SYNC(self, cmd): + await self.streamer.on_clear_user_syncs(cmd.instance_id) + async def on_REPLICATE(self, cmd): # Subscribe to all streams we're publishing to. for stream_name in self.streamer.streams_by_name: @@ -551,6 +554,8 @@ class ClientReplicationStreamProtocol(BaseReplicationStreamProtocol): ): BaseReplicationStreamProtocol.__init__(self, clock) + self.instance_id = hs.get_instance_id() + self.client_name = client_name self.server_name = server_name self.handler = handler @@ -580,7 +585,7 @@ class ClientReplicationStreamProtocol(BaseReplicationStreamProtocol): currently_syncing = self.handler.get_currently_syncing_users() now = self.clock.time_msec() for user_id in currently_syncing: - self.send_command(UserSyncCommand(user_id, True, now)) + self.send_command(UserSyncCommand(self.instance_id, user_id, True, now)) # We've now finished connecting to so inform the client handler self.handler.update_connection(self) diff --git a/synapse/replication/tcp/resource.py b/synapse/replication/tcp/resource.py index 4374e99e32..8b6067e20d 100644 --- a/synapse/replication/tcp/resource.py +++ b/synapse/replication/tcp/resource.py @@ -251,14 +251,19 @@ class ReplicationStreamer(object): self.federation_sender.federation_ack(token) @measure_func("repl.on_user_sync") - async def on_user_sync(self, conn_id, user_id, is_syncing, last_sync_ms): + async def on_user_sync(self, instance_id, user_id, is_syncing, last_sync_ms): """A client has started/stopped syncing on a worker. """ user_sync_counter.inc() await self.presence_handler.update_external_syncs_row( - conn_id, user_id, is_syncing, last_sync_ms + instance_id, user_id, is_syncing, last_sync_ms ) + async def on_clear_user_syncs(self, instance_id): + """A replication client wants us to drop all their UserSync data. + """ + await self.presence_handler.update_external_syncs_clear(instance_id) + @measure_func("repl.on_remove_pusher") async def on_remove_pusher(self, app_id, push_key, user_id): """A client has asked us to remove a pusher @@ -321,14 +326,6 @@ class ReplicationStreamer(object): except ValueError: pass - # We need to tell the presence handler that the connection has been - # lost so that it can handle any ongoing syncs on that connection. - run_as_background_process( - "update_external_syncs_clear", - self.presence_handler.update_external_syncs_clear, - connection.conn_id, - ) - def _batch_updates(updates): """Takes a list of updates of form [(token, row)] and sets the token to diff --git a/synapse/server.py b/synapse/server.py index c7ca2bda0d..cd86475d6b 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -103,6 +103,7 @@ from synapse.storage import DataStores, Storage from synapse.streams.events import EventSources from synapse.util import Clock from synapse.util.distributor import Distributor +from synapse.util.stringutils import random_string logger = logging.getLogger(__name__) @@ -230,6 +231,8 @@ class HomeServer(object): self._listening_services = [] self.start_time = None + self.instance_id = random_string(5) + self.clock = Clock(reactor) self.distributor = Distributor() self.ratelimiter = Ratelimiter() @@ -242,6 +245,14 @@ class HomeServer(object): for depname in kwargs: setattr(self, depname, kwargs[depname]) + def get_instance_id(self): + """A unique ID for this synapse process instance. + + This is used to distinguish running instances in worker-based + deployments. + """ + return self.instance_id + def setup(self): logger.info("Setting up.") self.start_time = int(self.get_clock().time()) diff --git a/synapse/server.pyi b/synapse/server.pyi index 3844f0e12f..9d1dfa71e7 100644 --- a/synapse/server.pyi +++ b/synapse/server.pyi @@ -114,3 +114,5 @@ class HomeServer(object): pass def is_mine_id(self, domain_id: str) -> bool: pass + def get_instance_id(self) -> str: + pass From d9f29f8daef2f49464382b0e80ee93ff38681e99 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Mon, 30 Mar 2020 17:38:21 +0100 Subject: [PATCH 1267/1623] Fix a small typo in the `metrics_flags` config option. (#7171) --- changelog.d/7171.doc | 1 + docs/sample_config.yaml | 2 +- synapse/config/metrics.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changelog.d/7171.doc diff --git a/changelog.d/7171.doc b/changelog.d/7171.doc new file mode 100644 index 0000000000..25a3bd8ac6 --- /dev/null +++ b/changelog.d/7171.doc @@ -0,0 +1 @@ +Fix a small typo in the `metrics_flags` config option. \ No newline at end of file diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 743949945a..6a770508f9 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -1144,7 +1144,7 @@ account_threepid_delegates: # enabled by default, either for performance reasons or limited use. # metrics_flags: - # Publish synapse_federation_known_servers, a g auge of the number of + # Publish synapse_federation_known_servers, a gauge of the number of # servers this homeserver knows about, including itself. May cause # performance problems on large homeservers. # diff --git a/synapse/config/metrics.py b/synapse/config/metrics.py index 22538153e1..6f517a71d0 100644 --- a/synapse/config/metrics.py +++ b/synapse/config/metrics.py @@ -86,7 +86,7 @@ class MetricsConfig(Config): # enabled by default, either for performance reasons or limited use. # metrics_flags: - # Publish synapse_federation_known_servers, a g auge of the number of + # Publish synapse_federation_known_servers, a gauge of the number of # servers this homeserver knows about, including itself. May cause # performance problems on large homeservers. # From 7042840b3201644ee71ea3e446576aa347b6d2a3 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Mon, 30 Mar 2020 17:53:25 +0100 Subject: [PATCH 1268/1623] Transfer alias mappings when joining an upgraded room (#6946) --- changelog.d/6946.bugfix | 1 + synapse/handlers/room_member.py | 3 +++ synapse/storage/data_stores/main/directory.py | 26 ++++++++++++++++--- 3 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 changelog.d/6946.bugfix diff --git a/changelog.d/6946.bugfix b/changelog.d/6946.bugfix new file mode 100644 index 0000000000..a238c83a18 --- /dev/null +++ b/changelog.d/6946.bugfix @@ -0,0 +1 @@ +Transfer alias mappings on room upgrade. \ No newline at end of file diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index 4260426369..c3ee8db4f0 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -519,6 +519,9 @@ class RoomMemberHandler(object): yield self.store.set_room_is_public(old_room_id, False) yield self.store.set_room_is_public(room_id, True) + # Transfer alias mappings in the room directory + yield self.store.update_aliases_for_room(old_room_id, room_id) + # Check if any groups we own contain the predecessor room local_group_ids = yield self.store.get_local_groups_for_room(old_room_id) for group_id in local_group_ids: diff --git a/synapse/storage/data_stores/main/directory.py b/synapse/storage/data_stores/main/directory.py index c9e7de7d12..e1d1bc3e05 100644 --- a/synapse/storage/data_stores/main/directory.py +++ b/synapse/storage/data_stores/main/directory.py @@ -14,6 +14,7 @@ # limitations under the License. from collections import namedtuple +from typing import Optional from twisted.internet import defer @@ -159,10 +160,29 @@ class DirectoryStore(DirectoryWorkerStore): return room_id - def update_aliases_for_room(self, old_room_id, new_room_id, creator): + def update_aliases_for_room( + self, old_room_id: str, new_room_id: str, creator: Optional[str] = None, + ): + """Repoint all of the aliases for a given room, to a different room. + + Args: + old_room_id: + new_room_id: + creator: The user to record as the creator of the new mapping. + If None, the creator will be left unchanged. + """ + def _update_aliases_for_room_txn(txn): - sql = "UPDATE room_aliases SET room_id = ?, creator = ? WHERE room_id = ?" - txn.execute(sql, (new_room_id, creator, old_room_id)) + update_creator_sql = "" + sql_params = (new_room_id, old_room_id) + if creator: + update_creator_sql = ", creator = ?" + sql_params = (new_room_id, creator, old_room_id) + + sql = "UPDATE room_aliases SET room_id = ? %s WHERE room_id = ?" % ( + update_creator_sql, + ) + txn.execute(sql, sql_params) self._invalidate_cache_and_stream( txn, self.get_aliases_for_room, (old_room_id,) ) From 7966a1cde9d4b598faa06620424844f2b35c94af Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 30 Mar 2020 19:06:52 +0100 Subject: [PATCH 1269/1623] Rewrite prune_old_outbound_device_pokes for efficiency (#7159) make sure we clear out all but one update for the user --- changelog.d/7159.bugfix | 1 + synapse/handlers/federation.py | 25 +----- synapse/storage/data_stores/main/devices.py | 71 +++++++++++++--- synapse/util/stringutils.py | 21 ++++- tests/federation/test_federation_sender.py | 92 +++++++++++++++++++++ 5 files changed, 173 insertions(+), 37 deletions(-) create mode 100644 changelog.d/7159.bugfix diff --git a/changelog.d/7159.bugfix b/changelog.d/7159.bugfix new file mode 100644 index 0000000000..1b341b127b --- /dev/null +++ b/changelog.d/7159.bugfix @@ -0,0 +1 @@ +Fix excessive CPU usage by `prune_old_outbound_device_pokes` job. diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 38ab6a8fc3..c7aa7acf3b 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -49,6 +49,7 @@ from synapse.event_auth import auth_types_for_event from synapse.events import EventBase from synapse.events.snapshot import EventContext from synapse.events.validator import EventValidator +from synapse.handlers._base import BaseHandler from synapse.logging.context import ( make_deferred_yieldable, nested_logging_context, @@ -69,10 +70,9 @@ from synapse.types import JsonDict, StateMap, UserID, get_domain_from_id from synapse.util.async_helpers import Linearizer, concurrently_execute from synapse.util.distributor import user_joined_room from synapse.util.retryutils import NotRetryingDestination +from synapse.util.stringutils import shortstr from synapse.visibility import filter_events_for_server -from ._base import BaseHandler - logger = logging.getLogger(__name__) @@ -93,27 +93,6 @@ class _NewEventInfo: auth_events = attr.ib(type=Optional[StateMap[EventBase]], default=None) -def shortstr(iterable, maxitems=5): - """If iterable has maxitems or fewer, return the stringification of a list - containing those items. - - Otherwise, return the stringification of a a list with the first maxitems items, - followed by "...". - - Args: - iterable (Iterable): iterable to truncate - maxitems (int): number of items to return before truncating - - Returns: - unicode - """ - - items = list(itertools.islice(iterable, maxitems + 1)) - if len(items) <= maxitems: - return str(items) - return "[" + ", ".join(repr(r) for r in items[:maxitems]) + ", ...]" - - class FederationHandler(BaseHandler): """Handles events that originated from federation. Responsible for: diff --git a/synapse/storage/data_stores/main/devices.py b/synapse/storage/data_stores/main/devices.py index 2d47cfd131..3140e1b722 100644 --- a/synapse/storage/data_stores/main/devices.py +++ b/synapse/storage/data_stores/main/devices.py @@ -41,6 +41,7 @@ from synapse.util.caches.descriptors import ( cachedList, ) from synapse.util.iterutils import batch_iter +from synapse.util.stringutils import shortstr logger = logging.getLogger(__name__) @@ -1092,18 +1093,47 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): ], ) - def _prune_old_outbound_device_pokes(self): + def _prune_old_outbound_device_pokes(self, prune_age=24 * 60 * 60 * 1000): """Delete old entries out of the device_lists_outbound_pokes to ensure - that we don't fill up due to dead servers. We keep one entry per - (destination, user_id) tuple to ensure that the prev_ids remain correct - if the server does come back. + that we don't fill up due to dead servers. + + Normally, we try to send device updates as a delta since a previous known point: + this is done by setting the prev_id in the m.device_list_update EDU. However, + for that to work, we have to have a complete record of each change to + each device, which can add up to quite a lot of data. + + An alternative mechanism is that, if the remote server sees that it has missed + an entry in the stream_id sequence for a given user, it will request a full + list of that user's devices. Hence, we can reduce the amount of data we have to + store (and transmit in some future transaction), by clearing almost everything + for a given destination out of the database, and having the remote server + resync. + + All we need to do is make sure we keep at least one row for each + (user, destination) pair, to remind us to send a m.device_list_update EDU for + that user when the destination comes back. It doesn't matter which device + we keep. """ - yesterday = self._clock.time_msec() - 24 * 60 * 60 * 1000 + yesterday = self._clock.time_msec() - prune_age def _prune_txn(txn): + # look for (user, destination) pairs which have an update older than + # the cutoff. + # + # For each pair, we also need to know the most recent stream_id, and + # an arbitrary device_id at that stream_id. select_sql = """ - SELECT destination, user_id, max(stream_id) as stream_id - FROM device_lists_outbound_pokes + SELECT + dlop1.destination, + dlop1.user_id, + MAX(dlop1.stream_id) AS stream_id, + (SELECT MIN(dlop2.device_id) AS device_id FROM + device_lists_outbound_pokes dlop2 + WHERE dlop2.destination = dlop1.destination AND + dlop2.user_id=dlop1.user_id AND + dlop2.stream_id=MAX(dlop1.stream_id) + ) + FROM device_lists_outbound_pokes dlop1 GROUP BY destination, user_id HAVING min(ts) < ? AND count(*) > 1 """ @@ -1114,14 +1144,29 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): if not rows: return + logger.info( + "Pruning old outbound device list updates for %i users/destinations: %s", + len(rows), + shortstr((row[0], row[1]) for row in rows), + ) + + # we want to keep the update with the highest stream_id for each user. + # + # there might be more than one update (with different device_ids) with the + # same stream_id, so we also delete all but one rows with the max stream id. delete_sql = """ DELETE FROM device_lists_outbound_pokes - WHERE ts < ? AND destination = ? AND user_id = ? AND stream_id < ? + WHERE destination = ? AND user_id = ? AND ( + stream_id < ? OR + (stream_id = ? AND device_id != ?) + ) """ - - txn.executemany( - delete_sql, ((yesterday, row[0], row[1], row[2]) for row in rows) - ) + count = 0 + for (destination, user_id, stream_id, device_id) in rows: + txn.execute( + delete_sql, (destination, user_id, stream_id, stream_id, device_id) + ) + count += txn.rowcount # Since we've deleted unsent deltas, we need to remove the entry # of last successful sent so that the prev_ids are correctly set. @@ -1131,7 +1176,7 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): """ txn.executemany(sql, ((row[0], row[1]) for row in rows)) - logger.info("Pruned %d device list outbound pokes", txn.rowcount) + logger.info("Pruned %d device list outbound pokes", count) return run_as_background_process( "prune_old_outbound_device_pokes", diff --git a/synapse/util/stringutils.py b/synapse/util/stringutils.py index 2c0dcb5208..6899bcb788 100644 --- a/synapse/util/stringutils.py +++ b/synapse/util/stringutils.py @@ -13,10 +13,11 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - +import itertools import random import re import string +from collections import Iterable import six from six import PY2, PY3 @@ -126,3 +127,21 @@ def assert_valid_client_secret(client_secret): raise SynapseError( 400, "Invalid client_secret parameter", errcode=Codes.INVALID_PARAM ) + + +def shortstr(iterable: Iterable, maxitems: int = 5) -> str: + """If iterable has maxitems or fewer, return the stringification of a list + containing those items. + + Otherwise, return the stringification of a a list with the first maxitems items, + followed by "...". + + Args: + iterable: iterable to truncate + maxitems: number of items to return before truncating + """ + + items = list(itertools.islice(iterable, maxitems + 1)) + if len(items) <= maxitems: + return str(items) + return "[" + ", ".join(repr(r) for r in items[:maxitems]) + ", ...]" diff --git a/tests/federation/test_federation_sender.py b/tests/federation/test_federation_sender.py index 7763b12159..a5fe5c6880 100644 --- a/tests/federation/test_federation_sender.py +++ b/tests/federation/test_federation_sender.py @@ -370,6 +370,98 @@ class FederationSenderDevicesTestCases(HomeserverTestCase): devices = {edu["content"]["device_id"] for edu in self.edus} self.assertEqual({"D1", "D2", "D3"}, devices) + def test_prune_outbound_device_pokes1(self): + """If a destination is unreachable, and the updates are pruned, we should get + a single update. + + This case tests the behaviour when the server has never been reachable. + """ + mock_send_txn = self.hs.get_federation_transport_client().send_transaction + mock_send_txn.side_effect = lambda t, cb: defer.fail("fail") + + # create devices + u1 = self.register_user("user", "pass") + self.login("user", "pass", device_id="D1") + self.login("user", "pass", device_id="D2") + self.login("user", "pass", device_id="D3") + + # delete them again + self.get_success( + self.hs.get_device_handler().delete_devices(u1, ["D1", "D2", "D3"]) + ) + + self.assertGreaterEqual(mock_send_txn.call_count, 4) + + # run the prune job + self.reactor.advance(10) + self.get_success( + self.hs.get_datastore()._prune_old_outbound_device_pokes(prune_age=1) + ) + + # recover the server + mock_send_txn.side_effect = self.record_transaction + self.hs.get_federation_sender().send_device_messages("host2") + self.pump() + + # there should be a single update for this user. + self.assertEqual(len(self.edus), 1) + edu = self.edus.pop(0) + self.assertEqual(edu["edu_type"], "m.device_list_update") + c = edu["content"] + + # synapse uses an empty prev_id list to indicate "needs a full resync". + self.assertEqual(c["prev_id"], []) + + def test_prune_outbound_device_pokes2(self): + """If a destination is unreachable, and the updates are pruned, we should get + a single update. + + This case tests the behaviour when the server was reachable, but then goes + offline. + """ + + # create first device + u1 = self.register_user("user", "pass") + self.login("user", "pass", device_id="D1") + + # expect the update EDU + self.assertEqual(len(self.edus), 1) + self.check_device_update_edu(self.edus.pop(0), u1, "D1", None) + + # now the server goes offline + mock_send_txn = self.hs.get_federation_transport_client().send_transaction + mock_send_txn.side_effect = lambda t, cb: defer.fail("fail") + + self.login("user", "pass", device_id="D2") + self.login("user", "pass", device_id="D3") + + # delete them again + self.get_success( + self.hs.get_device_handler().delete_devices(u1, ["D1", "D2", "D3"]) + ) + + self.assertGreaterEqual(mock_send_txn.call_count, 3) + + # run the prune job + self.reactor.advance(10) + self.get_success( + self.hs.get_datastore()._prune_old_outbound_device_pokes(prune_age=1) + ) + + # recover the server + mock_send_txn.side_effect = self.record_transaction + self.hs.get_federation_sender().send_device_messages("host2") + self.pump() + + # ... and we should get a single update for this user. + self.assertEqual(len(self.edus), 1) + edu = self.edus.pop(0) + self.assertEqual(edu["edu_type"], "m.device_list_update") + c = edu["content"] + + # synapse uses an empty prev_id list to indicate "needs a full resync". + self.assertEqual(c["prev_id"], []) + def check_device_update_edu( self, edu: JsonDict, From db098ec994a924fc9c5792b316c6a151c00bac47 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 24 Mar 2020 10:35:00 +0000 Subject: [PATCH 1270/1623] Fix starting workers when federation sending not split out. --- synapse/app/generic_worker.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/synapse/app/generic_worker.py b/synapse/app/generic_worker.py index b2c764bfe8..5363642d64 100644 --- a/synapse/app/generic_worker.py +++ b/synapse/app/generic_worker.py @@ -860,6 +860,9 @@ def start(config_options): # Force the appservice to start since they will be disabled in the main config config.notify_appservices = True + else: + # For other worker types we force this to off. + config.notify_appservices = False if config.worker_app == "synapse.app.pusher": if config.start_pushers: @@ -873,6 +876,9 @@ def start(config_options): # Force the pushers to start since they will be disabled in the main config config.start_pushers = True + else: + # For other worker types we force this to off. + config.start_pushers = False if config.worker_app == "synapse.app.user_dir": if config.update_user_directory: @@ -886,6 +892,9 @@ def start(config_options): # Force the pushers to start since they will be disabled in the main config config.update_user_directory = True + else: + # For other worker types we force this to off. + config.update_user_directory = False if config.worker_app == "synapse.app.federation_sender": if config.send_federation: @@ -899,6 +908,9 @@ def start(config_options): # Force the pushers to start since they will be disabled in the main config config.send_federation = True + else: + # For other worker types we force this to off. + config.send_federation = False synapse.events.USE_FROZEN_DICTS = config.use_frozen_dicts From b5ecafd1576649c86fdb31d4d4a2f374a3464184 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Mon, 30 Mar 2020 18:05:09 +0100 Subject: [PATCH 1271/1623] Only setdefault for signatures if device has key_json --- synapse/storage/data_stores/main/devices.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/synapse/storage/data_stores/main/devices.py b/synapse/storage/data_stores/main/devices.py index d55733a4cd..84d8deca18 100644 --- a/synapse/storage/data_stores/main/devices.py +++ b/synapse/storage/data_stores/main/devices.py @@ -317,14 +317,16 @@ class DeviceWorkerStore(SQLBaseStore): key_json = device.get("key_json", None) if key_json: result["keys"] = db_to_json(key_json) + + if "signatures" in device: + for sig_user_id, sigs in device["signatures"].items(): + result["keys"].setdefault("signatures", {}).setdefault( + sig_user_id, {} + ).update(sigs) + device_display_name = device.get("device_display_name", None) if device_display_name: result["device_display_name"] = device_display_name - if "signatures" in device: - for sig_user_id, sigs in device["signatures"].items(): - result["keys"].setdefault("signatures", {}).setdefault( - sig_user_id, {} - ).update(sigs) else: result["deleted"] = True From b5d0b038f4f2400dfd3ca7c698638c9821af6086 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Mon, 30 Mar 2020 19:18:00 +0100 Subject: [PATCH 1272/1623] Fix another instance --- synapse/storage/data_stores/main/devices.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/synapse/storage/data_stores/main/devices.py b/synapse/storage/data_stores/main/devices.py index 84d8deca18..8af5f7de54 100644 --- a/synapse/storage/data_stores/main/devices.py +++ b/synapse/storage/data_stores/main/devices.py @@ -527,14 +527,16 @@ class DeviceWorkerStore(SQLBaseStore): key_json = device.get("key_json", None) if key_json: result["keys"] = db_to_json(key_json) + + if "signatures" in device: + for sig_user_id, sigs in device["signatures"].items(): + result["keys"].setdefault("signatures", {}).setdefault( + sig_user_id, {} + ).update(sigs) + device_display_name = device.get("device_display_name", None) if device_display_name: result["device_display_name"] = device_display_name - if "signatures" in device: - for sig_user_id, sigs in device["signatures"].items(): - result["keys"].setdefault("signatures", {}).setdefault( - sig_user_id, {} - ).update(sigs) results.append(result) From 5bd2b275254bcbf001bee20d821c0ef567b9587f Mon Sep 17 00:00:00 2001 From: David Vo Date: Fri, 27 Mar 2020 12:26:55 +1100 Subject: [PATCH 1273/1623] Only import sqlite3 when type checking Fixes: #7127 Signed-off-by: David Vo --- changelog.d/7155.bugfix | 1 + synapse/storage/engines/sqlite.py | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 changelog.d/7155.bugfix diff --git a/changelog.d/7155.bugfix b/changelog.d/7155.bugfix new file mode 100644 index 0000000000..0bf51e7aba --- /dev/null +++ b/changelog.d/7155.bugfix @@ -0,0 +1 @@ +Avoid importing `sqlite3` when using the postgres backend. Contributed by David Vo. diff --git a/synapse/storage/engines/sqlite.py b/synapse/storage/engines/sqlite.py index 2bfeefd54e..3bc2e8b986 100644 --- a/synapse/storage/engines/sqlite.py +++ b/synapse/storage/engines/sqlite.py @@ -12,14 +12,17 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import sqlite3 import struct import threading +import typing from synapse.storage.engines import BaseDatabaseEngine +if typing.TYPE_CHECKING: + import sqlite3 # noqa: F401 -class Sqlite3Engine(BaseDatabaseEngine[sqlite3.Connection]): + +class Sqlite3Engine(BaseDatabaseEngine["sqlite3.Connection"]): def __init__(self, database_module, database_config): super().__init__(database_module, database_config) From 2cb38ca871a65eaa4236a908c34b2b9873371b93 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Mon, 30 Mar 2020 19:15:06 +0100 Subject: [PATCH 1274/1623] Add changelog --- changelog.d/7177.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7177.bugfix diff --git a/changelog.d/7177.bugfix b/changelog.d/7177.bugfix new file mode 100644 index 0000000000..908801c360 --- /dev/null +++ b/changelog.d/7177.bugfix @@ -0,0 +1 @@ +Mitigation for a bug in `_get_e2e_device_keys_txn` which broke federation for some servers. \ No newline at end of file From 2cf115f0ea3932bb65cbe3e1e563dca69aa642d7 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Tue, 31 Mar 2020 11:20:07 +0100 Subject: [PATCH 1275/1623] Rewrite changelog --- changelog.d/7177.bugfix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/7177.bugfix b/changelog.d/7177.bugfix index 908801c360..329a96cb0b 100644 --- a/changelog.d/7177.bugfix +++ b/changelog.d/7177.bugfix @@ -1 +1 @@ -Mitigation for a bug in `_get_e2e_device_keys_txn` which broke federation for some servers. \ No newline at end of file +Fix a bug which could cause outbound federation traffic to stop working if a client uploaded an incorrect e2e device signature. \ No newline at end of file From 5d99bde7883201d2c6011ca40ee87dbc64b938e0 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 24 Mar 2020 10:36:44 +0000 Subject: [PATCH 1276/1623] Newsfile --- changelog.d/7133.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7133.bugfix diff --git a/changelog.d/7133.bugfix b/changelog.d/7133.bugfix new file mode 100644 index 0000000000..61a86fd34e --- /dev/null +++ b/changelog.d/7133.bugfix @@ -0,0 +1 @@ +Fix starting workers when federation sending not split out. From 3fb9fc40f59e3688f82672410f812022a1af9daa Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Tue, 31 Mar 2020 11:49:43 +0100 Subject: [PATCH 1277/1623] 1.12.1rc1 --- CHANGES.md | 11 +++++++++++ changelog.d/7133.bugfix | 1 - changelog.d/7155.bugfix | 1 - changelog.d/7177.bugfix | 1 - synapse/__init__.py | 2 +- 5 files changed, 12 insertions(+), 4 deletions(-) delete mode 100644 changelog.d/7133.bugfix delete mode 100644 changelog.d/7155.bugfix delete mode 100644 changelog.d/7177.bugfix diff --git a/CHANGES.md b/CHANGES.md index f794c585b7..5b97d7ff82 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,14 @@ +Synapse 1.12.1rc1 (2020-03-31) +============================== + +Bugfixes +-------- + +- Fix starting workers when federation sending not split out. ([\#7133](https://github.com/matrix-org/synapse/issues/7133)) +- Avoid importing `sqlite3` when using the postgres backend. Contributed by David Vo. ([\#7155](https://github.com/matrix-org/synapse/issues/7155)) +- Fix a bug which could cause outbound federation traffic to stop working if a client uploaded an incorrect e2e device signature. ([\#7177](https://github.com/matrix-org/synapse/issues/7177)) + + Synapse 1.12.0 (2020-03-23) =========================== diff --git a/changelog.d/7133.bugfix b/changelog.d/7133.bugfix deleted file mode 100644 index 61a86fd34e..0000000000 --- a/changelog.d/7133.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix starting workers when federation sending not split out. diff --git a/changelog.d/7155.bugfix b/changelog.d/7155.bugfix deleted file mode 100644 index 0bf51e7aba..0000000000 --- a/changelog.d/7155.bugfix +++ /dev/null @@ -1 +0,0 @@ -Avoid importing `sqlite3` when using the postgres backend. Contributed by David Vo. diff --git a/changelog.d/7177.bugfix b/changelog.d/7177.bugfix deleted file mode 100644 index 329a96cb0b..0000000000 --- a/changelog.d/7177.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a bug which could cause outbound federation traffic to stop working if a client uploaded an incorrect e2e device signature. \ No newline at end of file diff --git a/synapse/__init__.py b/synapse/__init__.py index 5b86008945..c3c5b20f11 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -36,7 +36,7 @@ try: except ImportError: pass -__version__ = "1.12.0" +__version__ = "1.12.1rc1" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 677d0edbac97b7069413446b1ca01e6df6d9728e Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Tue, 31 Mar 2020 11:58:48 +0100 Subject: [PATCH 1278/1623] Note where bugs were introduced --- CHANGES.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 5b97d7ff82..a77768de58 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,9 +4,9 @@ Synapse 1.12.1rc1 (2020-03-31) Bugfixes -------- -- Fix starting workers when federation sending not split out. ([\#7133](https://github.com/matrix-org/synapse/issues/7133)) -- Avoid importing `sqlite3` when using the postgres backend. Contributed by David Vo. ([\#7155](https://github.com/matrix-org/synapse/issues/7155)) -- Fix a bug which could cause outbound federation traffic to stop working if a client uploaded an incorrect e2e device signature. ([\#7177](https://github.com/matrix-org/synapse/issues/7177)) +- Fix starting workers when federation sending not split out. ([\#7133](https://github.com/matrix-org/synapse/issues/7133)). Introduced in v1.12.0. +- Avoid importing `sqlite3` when using the postgres backend. Contributed by David Vo. ([\#7155](https://github.com/matrix-org/synapse/issues/7155)). Introduced in v1.12.0rc1. +- Fix a bug which could cause outbound federation traffic to stop working if a client uploaded an incorrect e2e device signature. ([\#7177](https://github.com/matrix-org/synapse/issues/7177)). Introduced in v1.11.0. Synapse 1.12.0 (2020-03-23) From 62a7289133840b4f4a55844b4f24ec664c3d917b Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 31 Mar 2020 13:09:16 +0100 Subject: [PATCH 1279/1623] Fix a bug which could cause incorrect 'cyclic dependency' error. (#7178) If there was an exception setting up one of the attributes of the Homeserver god object, then future attempts to fetch that attribute would raise a confusing "Cyclic dependency" error. Let's make sure that we clear the `building` flag so that we just get the original exception. Ref: #7169 --- changelog.d/7178.bugfix | 1 + synapse/server.py | 22 ++++++++++------------ 2 files changed, 11 insertions(+), 12 deletions(-) create mode 100644 changelog.d/7178.bugfix diff --git a/changelog.d/7178.bugfix b/changelog.d/7178.bugfix new file mode 100644 index 0000000000..35ea645d75 --- /dev/null +++ b/changelog.d/7178.bugfix @@ -0,0 +1 @@ +Fix a bug which could cause incorrect 'cyclic dependency' error. diff --git a/synapse/server.py b/synapse/server.py index cd86475d6b..9228e1c892 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -583,24 +583,22 @@ def _make_dependency_method(depname): try: builder = getattr(hs, "build_%s" % (depname)) except AttributeError: - builder = None + raise NotImplementedError( + "%s has no %s nor a builder for it" % (type(hs).__name__, depname) + ) - if builder: - # Prevent cyclic dependencies from deadlocking - if depname in hs._building: - raise ValueError("Cyclic dependency while building %s" % (depname,)) - hs._building[depname] = 1 + # Prevent cyclic dependencies from deadlocking + if depname in hs._building: + raise ValueError("Cyclic dependency while building %s" % (depname,)) + hs._building[depname] = 1 + try: dep = builder() setattr(hs, depname, dep) - + finally: del hs._building[depname] - return dep - - raise NotImplementedError( - "%s has no %s nor a builder for it" % (type(hs).__name__, depname) - ) + return dep setattr(HomeServer, "get_%s" % (depname), _get) From 0a7b0882c1d1f52bde46d6f367f265bc330e8bd0 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 31 Mar 2020 09:33:02 -0400 Subject: [PATCH 1280/1623] Fix use of async/await in media code (#7184) --- changelog.d/7184.misc | 1 + synapse/storage/data_stores/main/media_repository.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changelog.d/7184.misc diff --git a/changelog.d/7184.misc b/changelog.d/7184.misc new file mode 100644 index 0000000000..fac5bc0403 --- /dev/null +++ b/changelog.d/7184.misc @@ -0,0 +1 @@ +Convert some of synapse.rest.media to async/await. diff --git a/synapse/storage/data_stores/main/media_repository.py b/synapse/storage/data_stores/main/media_repository.py index 80ca36dedf..cf195f8aa6 100644 --- a/synapse/storage/data_stores/main/media_repository.py +++ b/synapse/storage/data_stores/main/media_repository.py @@ -340,7 +340,7 @@ class MediaRepositoryStore(MediaRepositoryBackgroundUpdateStore): "get_expired_url_cache", _get_expired_url_cache_txn ) - def delete_url_cache(self, media_ids): + async def delete_url_cache(self, media_ids): if len(media_ids) == 0: return @@ -349,7 +349,7 @@ class MediaRepositoryStore(MediaRepositoryBackgroundUpdateStore): def _delete_url_cache_txn(txn): txn.executemany(sql, [(media_id,) for media_id in media_ids]) - return self.db.runInteraction("delete_url_cache", _delete_url_cache_txn) + return await self.db.runInteraction("delete_url_cache", _delete_url_cache_txn) def get_url_cache_media_before(self, before_ts): sql = ( From b994e86e359fd095f82feabbf38fb18a5d10e0ae Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Tue, 31 Mar 2020 14:51:22 +0100 Subject: [PATCH 1281/1623] Only setdefault for signatures if device has key_json (#7177) --- changelog.d/7177.bugfix | 1 + synapse/storage/data_stores/main/devices.py | 24 ++++++++++++--------- 2 files changed, 15 insertions(+), 10 deletions(-) create mode 100644 changelog.d/7177.bugfix diff --git a/changelog.d/7177.bugfix b/changelog.d/7177.bugfix new file mode 100644 index 0000000000..329a96cb0b --- /dev/null +++ b/changelog.d/7177.bugfix @@ -0,0 +1 @@ +Fix a bug which could cause outbound federation traffic to stop working if a client uploaded an incorrect e2e device signature. \ No newline at end of file diff --git a/synapse/storage/data_stores/main/devices.py b/synapse/storage/data_stores/main/devices.py index 3140e1b722..20995e1b78 100644 --- a/synapse/storage/data_stores/main/devices.py +++ b/synapse/storage/data_stores/main/devices.py @@ -286,14 +286,16 @@ class DeviceWorkerStore(SQLBaseStore): key_json = device.get("key_json", None) if key_json: result["keys"] = db_to_json(key_json) + + if "signatures" in device: + for sig_user_id, sigs in device["signatures"].items(): + result["keys"].setdefault("signatures", {}).setdefault( + sig_user_id, {} + ).update(sigs) + device_display_name = device.get("device_display_name", None) if device_display_name: result["device_display_name"] = device_display_name - if "signatures" in device: - for sig_user_id, sigs in device["signatures"].items(): - result["keys"].setdefault("signatures", {}).setdefault( - sig_user_id, {} - ).update(sigs) else: result["deleted"] = True @@ -494,14 +496,16 @@ class DeviceWorkerStore(SQLBaseStore): key_json = device.get("key_json", None) if key_json: result["keys"] = db_to_json(key_json) + + if "signatures" in device: + for sig_user_id, sigs in device["signatures"].items(): + result["keys"].setdefault("signatures", {}).setdefault( + sig_user_id, {} + ).update(sigs) + device_display_name = device.get("device_display_name", None) if device_display_name: result["device_display_name"] = device_display_name - if "signatures" in device: - for sig_user_id, sigs in device["signatures"].items(): - result["keys"].setdefault("signatures", {}).setdefault( - sig_user_id, {} - ).update(sigs) results.append(result) From fe1580bfd91151c2c375d3c403ed911828f3899e Mon Sep 17 00:00:00 2001 From: Karlinde Date: Tue, 31 Mar 2020 16:08:56 +0200 Subject: [PATCH 1282/1623] Fill in the 'default' field for user-defined push rules (#6639) Signed-off-by: Karl Linderhed --- changelog.d/6639.bugfix | 1 + synapse/storage/data_stores/main/push_rule.py | 1 + 2 files changed, 2 insertions(+) create mode 100644 changelog.d/6639.bugfix diff --git a/changelog.d/6639.bugfix b/changelog.d/6639.bugfix new file mode 100644 index 0000000000..c7593a6e84 --- /dev/null +++ b/changelog.d/6639.bugfix @@ -0,0 +1 @@ +Fix missing field `default` when fetching user-defined push rules. diff --git a/synapse/storage/data_stores/main/push_rule.py b/synapse/storage/data_stores/main/push_rule.py index 62ac88d9f2..46f9bda773 100644 --- a/synapse/storage/data_stores/main/push_rule.py +++ b/synapse/storage/data_stores/main/push_rule.py @@ -41,6 +41,7 @@ def _load_rules(rawrules, enabled_map): rule = dict(rawrule) rule["conditions"] = json.loads(rawrule["conditions"]) rule["actions"] = json.loads(rawrule["actions"]) + rule["default"] = False ruleslist.append(rule) # We're going to be mutating this a lot, so do a deep copy From 60adcbed919afd5c85442775eca822fec43d816d Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 31 Mar 2020 15:18:41 +0100 Subject: [PATCH 1283/1623] Fix "'NoneType' has no attribute start|stop" logcontext errors (#7181) Fixes #7179. --- changelog.d/7181.misc | 1 + synapse/http/site.py | 13 ++++++------- synapse/logging/context.py | 5 +++++ 3 files changed, 12 insertions(+), 7 deletions(-) create mode 100644 changelog.d/7181.misc diff --git a/changelog.d/7181.misc b/changelog.d/7181.misc new file mode 100644 index 0000000000..731f4dcb52 --- /dev/null +++ b/changelog.d/7181.misc @@ -0,0 +1 @@ +Clean up some LoggingContext code. diff --git a/synapse/http/site.py b/synapse/http/site.py index e092193c9c..32feb0d968 100644 --- a/synapse/http/site.py +++ b/synapse/http/site.py @@ -193,6 +193,12 @@ class SynapseRequest(Request): self.finish_time = time.time() Request.connectionLost(self, reason) + if self.logcontext is None: + logger.info( + "Connection from %s lost before request headers were read", self.client + ) + return + # we only get here if the connection to the client drops before we send # the response. # @@ -236,13 +242,6 @@ class SynapseRequest(Request): def _finished_processing(self): """Log the completion of this request and update the metrics """ - - if self.logcontext is None: - # this can happen if the connection closed before we read the - # headers (so render was never called). In that case we'll already - # have logged a warning, so just bail out. - return - usage = self.logcontext.get_resource_usage() if self._processing_finished_time is None: diff --git a/synapse/logging/context.py b/synapse/logging/context.py index a8eafb1c7c..3254d6a8df 100644 --- a/synapse/logging/context.py +++ b/synapse/logging/context.py @@ -539,6 +539,11 @@ def set_current_context(context: LoggingContextOrSentinel) -> LoggingContextOrSe Returns: The context that was previously active """ + # everything blows up if we allow current_context to be set to None, so sanity-check + # that now. + if context is None: + raise TypeError("'context' argument may not be None") + current = current_context() if current is not context: From 2e826cd80c97cbdcec3e600b802c43ec27263e39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20Kj=C3=B8nigsen?= Date: Tue, 31 Mar 2020 16:50:48 +0200 Subject: [PATCH 1284/1623] Improve TURN documentation. (#7167) --- changelog.d/7167.doc | 1 + docs/turn-howto.md | 7 +++++++ 2 files changed, 8 insertions(+) create mode 100644 changelog.d/7167.doc diff --git a/changelog.d/7167.doc b/changelog.d/7167.doc new file mode 100644 index 0000000000..a7e7ba9b51 --- /dev/null +++ b/changelog.d/7167.doc @@ -0,0 +1 @@ +Improve README.md by being explicit about public IP recommendation for TURN relaying. diff --git a/docs/turn-howto.md b/docs/turn-howto.md index 1bd3943f54..b26e41f19e 100644 --- a/docs/turn-howto.md +++ b/docs/turn-howto.md @@ -11,6 +11,13 @@ TURN server. The following sections describe how to install [coturn]() (which implements the TURN REST API) and integrate it with synapse. +## Requirements + +For TURN relaying with `coturn` to work, it must be hosted on a server/endpoint with a public IP. + +Hosting TURN behind a NAT (even with appropriate port forwarding) is known to cause issues +and to often not work. + ## `coturn` Setup ### Initial installation From cfe8c8ab8e412b6320e5963ced0670fbc7b00d1b Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 31 Mar 2020 17:24:06 +0100 Subject: [PATCH 1285/1623] Remove unused `start_background_update` This was only used in a unit test, so let's just inline it in the test. --- synapse/storage/background_updates.py | 21 --------------------- tests/storage/test_background_update.py | 14 +++++++++----- 2 files changed, 9 insertions(+), 26 deletions(-) diff --git a/synapse/storage/background_updates.py b/synapse/storage/background_updates.py index eb1a7e5002..d4e26eab6c 100644 --- a/synapse/storage/background_updates.py +++ b/synapse/storage/background_updates.py @@ -400,27 +400,6 @@ class BackgroundUpdater(object): self.register_background_update_handler(update_name, updater) - def start_background_update(self, update_name, progress): - """Starts a background update running. - - Args: - update_name: The update to set running. - progress: The initial state of the progress of the update. - - Returns: - A deferred that completes once the task has been added to the - queue. - """ - # Clear the background update queue so that we will pick up the new - # task on the next iteration of do_background_update. - self._background_update_queue = [] - progress_json = json.dumps(progress) - - return self.db.simple_insert( - "background_updates", - {"update_name": update_name, "progress_json": progress_json}, - ) - def _end_background_update(self, update_name): """Removes a completed background update task from the queue. diff --git a/tests/storage/test_background_update.py b/tests/storage/test_background_update.py index ae14fb407d..aca41eb215 100644 --- a/tests/storage/test_background_update.py +++ b/tests/storage/test_background_update.py @@ -25,12 +25,20 @@ class BackgroundUpdateTestCase(unittest.HomeserverTestCase): # the target runtime for each bg update target_background_update_duration_ms = 50000 + store = self.hs.get_datastore() + self.get_success( + store.db.simple_insert( + "background_updates", + values={"update_name": "test_update", "progress_json": '{"my_key": 1}'}, + ) + ) + # first step: make a bit of progress @defer.inlineCallbacks def update(progress, count): yield self.clock.sleep((count * duration_ms) / 1000) progress = {"my_key": progress["my_key"] + 1} - yield self.hs.get_datastore().db.runInteraction( + yield store.db.runInteraction( "update_progress", self.updates._background_update_progress_txn, "test_update", @@ -39,10 +47,6 @@ class BackgroundUpdateTestCase(unittest.HomeserverTestCase): return count self.update_handler.side_effect = update - - self.get_success( - self.updates.start_background_update("test_update", {"my_key": 1}) - ) self.update_handler.reset_mock() res = self.get_success( self.updates.do_next_background_update( From 26d17b9bdc0de51d5f1a7526e8ab70e7f7796e4d Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 31 Mar 2020 17:25:10 +0100 Subject: [PATCH 1286/1623] Make `has_completed_background_updates` async (Almost) everywhere that uses it is happy with an awaitable. --- synapse/storage/background_updates.py | 7 +++---- tests/storage/test_background_update.py | 4 +++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/synapse/storage/background_updates.py b/synapse/storage/background_updates.py index d4e26eab6c..79494bcdf5 100644 --- a/synapse/storage/background_updates.py +++ b/synapse/storage/background_updates.py @@ -119,12 +119,11 @@ class BackgroundUpdater(object): self._all_done = True return None - @defer.inlineCallbacks - def has_completed_background_updates(self): + async def has_completed_background_updates(self) -> bool: """Check if all the background updates have completed Returns: - Deferred[bool]: True if all background updates have completed + True if all background updates have completed """ # if we've previously determined that there is nothing left to do, that # is easy @@ -138,7 +137,7 @@ class BackgroundUpdater(object): # otherwise, check if there are updates to be run. This is important, # as we may be running on a worker which doesn't perform the bg updates # itself, but still wants to wait for them to happen. - updates = yield self.db.simple_select_onecol( + updates = await self.db.simple_select_onecol( "background_updates", keyvalues=None, retcol="1", diff --git a/tests/storage/test_background_update.py b/tests/storage/test_background_update.py index aca41eb215..e578de8acf 100644 --- a/tests/storage/test_background_update.py +++ b/tests/storage/test_background_update.py @@ -11,7 +11,9 @@ class BackgroundUpdateTestCase(unittest.HomeserverTestCase): def prepare(self, reactor, clock, homeserver): self.updates = self.hs.get_datastore().db.updates # type: BackgroundUpdater # the base test class should have run the real bg updates for us - self.assertTrue(self.updates.has_completed_background_updates()) + self.assertTrue( + self.get_success(self.updates.has_completed_background_updates()) + ) self.update_handler = Mock() self.updates.register_background_update_handler( From 51f4d52cb4663a056372d779b78488aeae45f554 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 31 Mar 2020 17:27:56 +0100 Subject: [PATCH 1287/1623] Set a logging context while running the bg updates This mostly just reduces the amount of "running from sentinel context" spam during unittest setup. --- tests/unittest.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/unittest.py b/tests/unittest.py index d0406ca2fd..27af5228fe 100644 --- a/tests/unittest.py +++ b/tests/unittest.py @@ -40,6 +40,7 @@ from synapse.http.server import JsonResource from synapse.http.site import SynapseRequest, SynapseSite from synapse.logging.context import ( SENTINEL_CONTEXT, + LoggingContext, current_context, set_current_context, ) @@ -419,15 +420,17 @@ class HomeserverTestCase(TestCase): config_obj.parse_config_dict(config, "", "") kwargs["config"] = config_obj + async def run_bg_updates(): + with LoggingContext("run_bg_updates", request="run_bg_updates-1"): + while not await stor.db.updates.has_completed_background_updates(): + await stor.db.updates.do_next_background_update(1) + hs = setup_test_homeserver(self.addCleanup, *args, **kwargs) stor = hs.get_datastore() # Run the database background updates, when running against "master". if hs.__class__.__name__ == "TestHomeServer": - while not self.get_success( - stor.db.updates.has_completed_background_updates() - ): - self.get_success(stor.db.updates.do_next_background_update(1)) + self.get_success(run_bg_updates()) return hs From b4c22342320d7de86c02dfb36415a38c62bec88d Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 31 Mar 2020 17:31:32 +0100 Subject: [PATCH 1288/1623] Make do_next_background_update return a bool returning a None or an int that we don't use is confusing. --- synapse/storage/background_updates.py | 12 +++++------- tests/storage/test_background_update.py | 6 +++--- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/synapse/storage/background_updates.py b/synapse/storage/background_updates.py index 79494bcdf5..4a59132bf3 100644 --- a/synapse/storage/background_updates.py +++ b/synapse/storage/background_updates.py @@ -111,7 +111,7 @@ class BackgroundUpdater(object): except Exception: logger.exception("Error doing update") else: - if result is None: + if result: logger.info( "No more background updates to do." " Unscheduling background update task." @@ -169,9 +169,7 @@ class BackgroundUpdater(object): return not update_exists - async def do_next_background_update( - self, desired_duration_ms: float - ) -> Optional[int]: + async def do_next_background_update(self, desired_duration_ms: float) -> bool: """Does some amount of work on the next queued background update Returns once some amount of work is done. @@ -180,7 +178,7 @@ class BackgroundUpdater(object): desired_duration_ms(float): How long we want to spend updating. Returns: - None if there is no more work to do, otherwise an int + True if there is no more work to do, otherwise False """ if not self._background_update_queue: updates = await self.db.simple_select_list( @@ -195,14 +193,14 @@ class BackgroundUpdater(object): if not self._background_update_queue: # no work left to do - return None + return True # pop from the front, and add back to the back update_name = self._background_update_queue.pop(0) self._background_update_queue.append(update_name) res = await self._do_background_update(update_name, desired_duration_ms) - return res + return False async def _do_background_update( self, update_name: str, desired_duration_ms: float diff --git a/tests/storage/test_background_update.py b/tests/storage/test_background_update.py index e578de8acf..940b166129 100644 --- a/tests/storage/test_background_update.py +++ b/tests/storage/test_background_update.py @@ -56,7 +56,7 @@ class BackgroundUpdateTestCase(unittest.HomeserverTestCase): ), by=0.1, ) - self.assertIsNotNone(res) + self.assertFalse(res) # on the first call, we should get run with the default background update size self.update_handler.assert_called_once_with( @@ -79,7 +79,7 @@ class BackgroundUpdateTestCase(unittest.HomeserverTestCase): result = self.get_success( self.updates.do_next_background_update(target_background_update_duration_ms) ) - self.assertIsNotNone(result) + self.assertFalse(result) self.update_handler.assert_called_once() # third step: we don't expect to be called any more @@ -87,5 +87,5 @@ class BackgroundUpdateTestCase(unittest.HomeserverTestCase): result = self.get_success( self.updates.do_next_background_update(target_background_update_duration_ms) ) - self.assertIsNone(result) + self.assertTrue(result) self.assertFalse(self.update_handler.called) From 7b608cf4683c0df2dbb55aacd472c407a0f6b1fa Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 31 Mar 2020 17:43:19 +0100 Subject: [PATCH 1289/1623] Only run one background update at a time --- synapse/storage/background_updates.py | 74 ++++++++++++------- synapse/storage/prepare_database.py | 2 +- .../delta/58/00background_update_ordering.sql | 19 +++++ 3 files changed, 68 insertions(+), 27 deletions(-) create mode 100644 synapse/storage/schema/delta/58/00background_update_ordering.sql diff --git a/synapse/storage/background_updates.py b/synapse/storage/background_updates.py index 4a59132bf3..0e430356cd 100644 --- a/synapse/storage/background_updates.py +++ b/synapse/storage/background_updates.py @@ -90,8 +90,10 @@ class BackgroundUpdater(object): self._clock = hs.get_clock() self.db = database + # if a background update is currently running, its name. + self._current_background_update = None # type: Optional[str] + self._background_update_performance = {} - self._background_update_queue = [] self._background_update_handlers = {} self._all_done = False @@ -131,7 +133,7 @@ class BackgroundUpdater(object): return True # obviously, if we have things in our queue, we're not done. - if self._background_update_queue: + if self._current_background_update: return False # otherwise, check if there are updates to be run. This is important, @@ -152,11 +154,10 @@ class BackgroundUpdater(object): async def has_completed_background_update(self, update_name) -> bool: """Check if the given background update has finished running. """ - if self._all_done: return True - if update_name in self._background_update_queue: + if update_name == self._current_background_update: return False update_exists = await self.db.simple_select_one_onecol( @@ -180,31 +181,49 @@ class BackgroundUpdater(object): Returns: True if there is no more work to do, otherwise False """ - if not self._background_update_queue: - updates = await self.db.simple_select_list( - "background_updates", - keyvalues=None, - retcols=("update_name", "depends_on"), + + def get_background_updates_txn(txn): + txn.execute( + """ + SELECT update_name, depends_on FROM background_updates + ORDER BY ordering, update_name + """ ) - in_flight = {update["update_name"] for update in updates} - for update in updates: - if update["depends_on"] not in in_flight: - self._background_update_queue.append(update["update_name"]) + return self.db.cursor_to_dict(txn) - if not self._background_update_queue: - # no work left to do - return True + if not self._current_background_update: + all_pending_updates = await self.db.runInteraction( + "background_updates", get_background_updates_txn, + ) + if not all_pending_updates: + # no work left to do + return True - # pop from the front, and add back to the back - update_name = self._background_update_queue.pop(0) - self._background_update_queue.append(update_name) + # find the first update which isn't dependent on another one in the queue. + pending = {update["update_name"] for update in all_pending_updates} + for upd in all_pending_updates: + depends_on = upd["depends_on"] + if not depends_on or depends_on not in pending: + break + logger.info( + "Not starting on bg update %s until %s is done", + upd["update_name"], + depends_on, + ) + else: + # if we get to the end of that for loop, there is a problem + raise Exception( + "Unable to find a background update which doesn't depend on " + "another: dependency cycle?" + ) - res = await self._do_background_update(update_name, desired_duration_ms) + self._current_background_update = upd["update_name"] + + await self._do_background_update(desired_duration_ms) return False - async def _do_background_update( - self, update_name: str, desired_duration_ms: float - ) -> int: + async def _do_background_update(self, desired_duration_ms: float) -> int: + update_name = self._current_background_update logger.info("Starting update batch on background update '%s'", update_name) update_handler = self._background_update_handlers[update_name] @@ -405,9 +424,12 @@ class BackgroundUpdater(object): Returns: A deferred that completes once the task is removed. """ - self._background_update_queue = [ - name for name in self._background_update_queue if name != update_name - ] + if update_name != self._current_background_update: + raise Exception( + "Cannot end background update %s which isn't currently running" + % update_name + ) + self._current_background_update = None return self.db.simple_delete_one( "background_updates", keyvalues={"update_name": update_name} ) diff --git a/synapse/storage/prepare_database.py b/synapse/storage/prepare_database.py index 6cb7d4b922..1712932f31 100644 --- a/synapse/storage/prepare_database.py +++ b/synapse/storage/prepare_database.py @@ -29,7 +29,7 @@ logger = logging.getLogger(__name__) # Remember to update this number every time a change is made to database # schema files, so the users will be informed on server restarts. -SCHEMA_VERSION = 57 +SCHEMA_VERSION = 58 dir_path = os.path.abspath(os.path.dirname(__file__)) diff --git a/synapse/storage/schema/delta/58/00background_update_ordering.sql b/synapse/storage/schema/delta/58/00background_update_ordering.sql new file mode 100644 index 0000000000..02dae587cc --- /dev/null +++ b/synapse/storage/schema/delta/58/00background_update_ordering.sql @@ -0,0 +1,19 @@ +/* Copyright 2020 The Matrix.org Foundation C.I.C + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* add an "ordering" column to background_updates, which can be used to sort them + to achieve some level of consistency. */ + +ALTER TABLE background_updates ADD COLUMN ordering INT NOT NULL DEFAULT 0; From b413ab8aa64a3e1a01db8e3e6bce0c486f916618 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 31 Mar 2020 17:44:36 +0100 Subject: [PATCH 1290/1623] changelog --- changelog.d/7190.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7190.misc diff --git a/changelog.d/7190.misc b/changelog.d/7190.misc new file mode 100644 index 0000000000..34348873f1 --- /dev/null +++ b/changelog.d/7190.misc @@ -0,0 +1 @@ +Only run one background database update at a time. From dfa07822542da96b93ef9d871d43bf1a36dc4664 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 1 Apr 2020 10:40:46 +0100 Subject: [PATCH 1291/1623] Remove connections per replication stream metric. (#7195) This broke in a recent PR (#7024) and is no longer useful due to all replication clients implicitly subscribing to all streams, so let's just remove it. --- changelog.d/7195.misc | 1 + synapse/replication/tcp/resource.py | 16 ---------------- 2 files changed, 1 insertion(+), 16 deletions(-) create mode 100644 changelog.d/7195.misc diff --git a/changelog.d/7195.misc b/changelog.d/7195.misc new file mode 100644 index 0000000000..676f285377 --- /dev/null +++ b/changelog.d/7195.misc @@ -0,0 +1 @@ +Move catchup of replication streams logic to worker. diff --git a/synapse/replication/tcp/resource.py b/synapse/replication/tcp/resource.py index 8b6067e20d..30021ee309 100644 --- a/synapse/replication/tcp/resource.py +++ b/synapse/replication/tcp/resource.py @@ -99,22 +99,6 @@ class ReplicationStreamer(object): self.streams_by_name = {stream.NAME: stream for stream in self.streams} - LaterGauge( - "synapse_replication_tcp_resource_connections_per_stream", - "", - ["stream_name"], - lambda: { - (stream_name,): len( - [ - conn - for conn in self.connections - if stream_name in conn.replication_streams - ] - ) - for stream_name in self.streams_by_name - }, - ) - self.federation_sender = None if not hs.config.send_federation: self.federation_sender = hs.get_federation_sender() From 250f87d0dec15f33fced7d06252e27d9c258b90c Mon Sep 17 00:00:00 2001 From: siroccal <41478263+siroccal@users.noreply.github.com> Date: Wed, 1 Apr 2020 13:44:51 +0200 Subject: [PATCH 1292/1623] Update postgres.md (#7119) --- changelog.d/7119.doc | 1 + docs/postgres.md | 28 +++++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 changelog.d/7119.doc diff --git a/changelog.d/7119.doc b/changelog.d/7119.doc new file mode 100644 index 0000000000..05192966c3 --- /dev/null +++ b/changelog.d/7119.doc @@ -0,0 +1 @@ +Update postgres docs with login troubleshooting information. \ No newline at end of file diff --git a/docs/postgres.md b/docs/postgres.md index 04aa746051..70fe29cdcc 100644 --- a/docs/postgres.md +++ b/docs/postgres.md @@ -61,7 +61,33 @@ Note that the PostgreSQL database *must* have the correct encoding set You may need to enable password authentication so `synapse_user` can connect to the database. See -. +. + +If you get an error along the lines of `FATAL: Ident authentication failed for +user "synapse_user"`, you may need to use an authentication method other than +`ident`: + +* If the `synapse_user` user has a password, add the password to the `database:` + section of `homeserver.yaml`. Then add the following to `pg_hba.conf`: + + ``` + host synapse synapse_user ::1/128 md5 # or `scram-sha-256` instead of `md5` if you use that + ``` + +* If the `synapse_user` user does not have a password, then a password doesn't + have to be added to `homeserver.yaml`. But the following does need to be added + to `pg_hba.conf`: + + ``` + host synapse synapse_user ::1/128 trust + ``` + +Note that line order matters in `pg_hba.conf`, so make sure that if you do add a +new line, it is inserted before: + +``` +host all all ::1/128 ident +``` ### Fixing incorrect `COLLATE` or `CTYPE` From 468dcc767bf379ba2b4ed4b6d1c6537473175eab Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 1 Apr 2020 08:27:05 -0400 Subject: [PATCH 1293/1623] Allow admins to create aliases when they are not in the room (#7191) --- changelog.d/7191.feature | 1 + synapse/handlers/directory.py | 6 +++- tests/handlers/test_directory.py | 62 ++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 changelog.d/7191.feature diff --git a/changelog.d/7191.feature b/changelog.d/7191.feature new file mode 100644 index 0000000000..83d5685bb2 --- /dev/null +++ b/changelog.d/7191.feature @@ -0,0 +1 @@ +Admin users are no longer required to be in a room to create an alias for it. diff --git a/synapse/handlers/directory.py b/synapse/handlers/directory.py index 1d842c369b..53e5f585d9 100644 --- a/synapse/handlers/directory.py +++ b/synapse/handlers/directory.py @@ -127,7 +127,11 @@ class DirectoryHandler(BaseHandler): errcode=Codes.EXCLUSIVE, ) else: - if self.require_membership and check_membership: + # Server admins are not subject to the same constraints as normal + # users when creating an alias (e.g. being in the room). + is_admin = yield self.auth.is_server_admin(requester.user) + + if (self.require_membership and check_membership) and not is_admin: rooms_for_user = yield self.store.get_rooms_for_user(user_id) if room_id not in rooms_for_user: raise AuthError( diff --git a/tests/handlers/test_directory.py b/tests/handlers/test_directory.py index 5e40adba52..00bb776271 100644 --- a/tests/handlers/test_directory.py +++ b/tests/handlers/test_directory.py @@ -102,6 +102,68 @@ class DirectoryTestCase(unittest.HomeserverTestCase): self.assertEquals({"room_id": "!8765asdf:test", "servers": ["test"]}, response) +class TestCreateAlias(unittest.HomeserverTestCase): + servlets = [ + synapse.rest.admin.register_servlets, + login.register_servlets, + room.register_servlets, + directory.register_servlets, + ] + + def prepare(self, reactor, clock, hs): + self.handler = hs.get_handlers().directory_handler + + # Create user + self.admin_user = self.register_user("admin", "pass", admin=True) + self.admin_user_tok = self.login("admin", "pass") + + # Create a test room + self.room_id = self.helper.create_room_as( + self.admin_user, tok=self.admin_user_tok + ) + + self.test_alias = "#test:test" + self.room_alias = RoomAlias.from_string(self.test_alias) + + # Create a test user. + self.test_user = self.register_user("user", "pass", admin=False) + self.test_user_tok = self.login("user", "pass") + self.helper.join(room=self.room_id, user=self.test_user, tok=self.test_user_tok) + + def test_create_alias_joined_room(self): + """A user can create an alias for a room they're in.""" + self.get_success( + self.handler.create_association( + create_requester(self.test_user), self.room_alias, self.room_id, + ) + ) + + def test_create_alias_other_room(self): + """A user cannot create an alias for a room they're NOT in.""" + other_room_id = self.helper.create_room_as( + self.admin_user, tok=self.admin_user_tok + ) + + self.get_failure( + self.handler.create_association( + create_requester(self.test_user), self.room_alias, other_room_id, + ), + synapse.api.errors.SynapseError, + ) + + def test_create_alias_admin(self): + """An admin can create an alias for a room they're NOT in.""" + other_room_id = self.helper.create_room_as( + self.test_user, tok=self.test_user_tok + ) + + self.get_success( + self.handler.create_association( + create_requester(self.admin_user), self.room_alias, other_room_id, + ) + ) + + class TestDeleteAlias(unittest.HomeserverTestCase): servlets = [ synapse.rest.admin.register_servlets, From b9930d24a05e47c36845d8607b12a45eea889be0 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 1 Apr 2020 08:48:00 -0400 Subject: [PATCH 1294/1623] Support SAML in the user interactive authentication workflow. (#7102) --- CHANGES.md | 8 ++ changelog.d/7102.feature | 1 + synapse/api/constants.py | 1 + synapse/handlers/auth.py | 116 +++++++++++++++++++- synapse/handlers/saml_handler.py | 51 +++++++-- synapse/res/templates/sso_auth_confirm.html | 14 +++ synapse/rest/client/v2_alpha/account.py | 19 +++- synapse/rest/client/v2_alpha/auth.py | 42 +++---- synapse/rest/client/v2_alpha/devices.py | 12 +- synapse/rest/client/v2_alpha/keys.py | 6 +- synapse/rest/client/v2_alpha/register.py | 1 + 11 files changed, 227 insertions(+), 44 deletions(-) create mode 100644 changelog.d/7102.feature create mode 100644 synapse/res/templates/sso_auth_confirm.html diff --git a/CHANGES.md b/CHANGES.md index f794c585b7..b997af1630 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,11 @@ +Next version +============ + +* A new template (`sso_auth_confirm.html`) was added to Synapse. If your Synapse + is configured to use SSO and a custom `sso_redirect_confirm_template_dir` + configuration then this template will need to be duplicated into that + directory. + Synapse 1.12.0 (2020-03-23) =========================== diff --git a/changelog.d/7102.feature b/changelog.d/7102.feature new file mode 100644 index 0000000000..01057aa396 --- /dev/null +++ b/changelog.d/7102.feature @@ -0,0 +1 @@ +Support SSO in the user interactive authentication workflow. diff --git a/synapse/api/constants.py b/synapse/api/constants.py index cc8577552b..fda2c2e5bb 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -61,6 +61,7 @@ class LoginType(object): MSISDN = "m.login.msisdn" RECAPTCHA = "m.login.recaptcha" TERMS = "m.login.terms" + SSO = "org.matrix.login.sso" DUMMY = "m.login.dummy" # Only for C/S API v1 diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index 2ce1425dfa..7c09d15a72 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -53,6 +53,31 @@ from ._base import BaseHandler logger = logging.getLogger(__name__) +SUCCESS_TEMPLATE = """ + + +Success! + + + + + +
    +

    Thank you

    +

    You may now close this window and return to the application

    +
    + + +""" + + class AuthHandler(BaseHandler): SESSION_EXPIRE_MS = 48 * 60 * 60 * 1000 @@ -91,6 +116,7 @@ class AuthHandler(BaseHandler): self.hs = hs # FIXME better possibility to access registrationHandler later? self.macaroon_gen = hs.get_macaroon_generator() self._password_enabled = hs.config.password_enabled + self._saml2_enabled = hs.config.saml2_enabled # we keep this as a list despite the O(N^2) implication so that we can # keep PASSWORD first and avoid confusing clients which pick the first @@ -106,6 +132,13 @@ class AuthHandler(BaseHandler): if t not in login_types: login_types.append(t) self._supported_login_types = login_types + # Login types and UI Auth types have a heavy overlap, but are not + # necessarily identical. Login types have SSO (and other login types) + # added in the rest layer, see synapse.rest.client.v1.login.LoginRestServerlet.on_GET. + ui_auth_types = login_types.copy() + if self._saml2_enabled: + ui_auth_types.append(LoginType.SSO) + self._supported_ui_auth_types = ui_auth_types # Ratelimiter for failed auth during UIA. Uses same ratelimit config # as per `rc_login.failed_attempts`. @@ -113,10 +146,21 @@ class AuthHandler(BaseHandler): self._clock = self.hs.get_clock() - # Load the SSO redirect confirmation page HTML template + # Load the SSO HTML templates. + + # The following template is shown to the user during a client login via SSO, + # after the SSO completes and before redirecting them back to their client. + # It notifies the user they are about to give access to their matrix account + # to the client. self._sso_redirect_confirm_template = load_jinja2_templates( hs.config.sso_redirect_confirm_template_dir, ["sso_redirect_confirm.html"], )[0] + # The following template is shown during user interactive authentication + # in the fallback auth scenario. It notifies the user that they are + # authenticating for an operation to occur on their account. + self._sso_auth_confirm_template = load_jinja2_templates( + hs.config.sso_redirect_confirm_template_dir, ["sso_auth_confirm.html"], + )[0] self._server_name = hs.config.server_name @@ -130,6 +174,7 @@ class AuthHandler(BaseHandler): request: SynapseRequest, request_body: Dict[str, Any], clientip: str, + description: str, ): """ Checks that the user is who they claim to be, via a UI auth. @@ -147,6 +192,9 @@ class AuthHandler(BaseHandler): clientip: The IP address of the client. + description: A human readable string to be displayed to the user that + describes the operation happening on their account. + Returns: defer.Deferred[dict]: the parameters for this request (which may have been given only in a previous call). @@ -175,11 +223,11 @@ class AuthHandler(BaseHandler): ) # build a list of supported flows - flows = [[login_type] for login_type in self._supported_login_types] + flows = [[login_type] for login_type in self._supported_ui_auth_types] try: result, params, _ = yield self.check_auth( - flows, request, request_body, clientip + flows, request, request_body, clientip, description ) except LoginError: # Update the ratelimite to say we failed (`can_do_action` doesn't raise). @@ -193,7 +241,7 @@ class AuthHandler(BaseHandler): raise # find the completed login type - for login_type in self._supported_login_types: + for login_type in self._supported_ui_auth_types: if login_type not in result: continue @@ -224,6 +272,7 @@ class AuthHandler(BaseHandler): request: SynapseRequest, clientdict: Dict[str, Any], clientip: str, + description: str, ): """ Takes a dictionary sent by the client in the login / registration @@ -250,6 +299,9 @@ class AuthHandler(BaseHandler): clientip: The IP address of the client. + description: A human readable string to be displayed to the user that + describes the operation happening on their account. + Returns: defer.Deferred[dict, dict, str]: a deferred tuple of (creds, params, session_id). @@ -299,12 +351,18 @@ class AuthHandler(BaseHandler): comparator = (request.uri, request.method, clientdict) if "ui_auth" not in session: session["ui_auth"] = comparator + self._save_session(session) elif session["ui_auth"] != comparator: raise SynapseError( 403, "Requested operation has changed during the UI authentication session.", ) + # Add a human readable description to the session. + if "description" not in session: + session["description"] = description + self._save_session(session) + if not authdict: raise InteractiveAuthIncompleteError( self._auth_dict_for_flows(flows, session) @@ -991,6 +1049,56 @@ class AuthHandler(BaseHandler): else: return defer.succeed(False) + def start_sso_ui_auth(self, redirect_url: str, session_id: str) -> str: + """ + Get the HTML for the SSO redirect confirmation page. + + Args: + redirect_url: The URL to redirect to the SSO provider. + session_id: The user interactive authentication session ID. + + Returns: + The HTML to render. + """ + session = self._get_session_info(session_id) + # Get the human readable operation of what is occurring, falling back to + # a generic message if it isn't available for some reason. + description = session.get("description", "modify your account") + return self._sso_auth_confirm_template.render( + description=description, redirect_url=redirect_url, + ) + + def complete_sso_ui_auth( + self, registered_user_id: str, session_id: str, request: SynapseRequest, + ): + """Having figured out a mxid for this user, complete the HTTP request + + Args: + registered_user_id: The registered user ID to complete SSO login for. + request: The request to complete. + client_redirect_url: The URL to which to redirect the user at the end of the + process. + """ + # Mark the stage of the authentication as successful. + sess = self._get_session_info(session_id) + if "creds" not in sess: + sess["creds"] = {} + creds = sess["creds"] + + # Save the user who authenticated with SSO, this will be used to ensure + # that the account be modified is also the person who logged in. + creds[LoginType.SSO] = registered_user_id + self._save_session(sess) + + # Render the HTML and return. + html_bytes = SUCCESS_TEMPLATE.encode("utf8") + request.setResponseCode(200) + request.setHeader(b"Content-Type", b"text/html; charset=utf-8") + request.setHeader(b"Content-Length", b"%d" % (len(html_bytes),)) + + request.write(html_bytes) + finish_request(request) + def complete_sso_login( self, registered_user_id: str, diff --git a/synapse/handlers/saml_handler.py b/synapse/handlers/saml_handler.py index dc04b53f43..4741c82f61 100644 --- a/synapse/handlers/saml_handler.py +++ b/synapse/handlers/saml_handler.py @@ -14,7 +14,7 @@ # limitations under the License. import logging import re -from typing import Tuple +from typing import Optional, Tuple import attr import saml2 @@ -44,11 +44,15 @@ class Saml2SessionData: # time the session was created, in milliseconds creation_time = attr.ib() + # The user interactive authentication session ID associated with this SAML + # session (or None if this SAML session is for an initial login). + ui_auth_session_id = attr.ib(type=Optional[str], default=None) class SamlHandler: def __init__(self, hs): self._saml_client = Saml2Client(hs.config.saml2_sp_config) + self._auth = hs.get_auth() self._auth_handler = hs.get_auth_handler() self._registration_handler = hs.get_registration_handler() @@ -77,12 +81,14 @@ class SamlHandler: self._error_html_content = hs.config.saml2_error_html_content - def handle_redirect_request(self, client_redirect_url): + def handle_redirect_request(self, client_redirect_url, ui_auth_session_id=None): """Handle an incoming request to /login/sso/redirect Args: client_redirect_url (bytes): the URL that we should redirect the client to when everything is done + ui_auth_session_id (Optional[str]): The session ID of the ongoing UI Auth (or + None if this is a login). Returns: bytes: URL to redirect to @@ -92,7 +98,9 @@ class SamlHandler: ) now = self._clock.time_msec() - self._outstanding_requests_dict[reqid] = Saml2SessionData(creation_time=now) + self._outstanding_requests_dict[reqid] = Saml2SessionData( + creation_time=now, ui_auth_session_id=ui_auth_session_id, + ) for key, value in info["headers"]: if key == "Location": @@ -119,7 +127,9 @@ class SamlHandler: self.expire_sessions() try: - user_id = await self._map_saml_response_to_user(resp_bytes, relay_state) + user_id, current_session = await self._map_saml_response_to_user( + resp_bytes, relay_state + ) except RedirectException: # Raise the exception as per the wishes of the SAML module response raise @@ -137,9 +147,28 @@ class SamlHandler: finish_request(request) return - self._auth_handler.complete_sso_login(user_id, request, relay_state) + # Complete the interactive auth session or the login. + if current_session and current_session.ui_auth_session_id: + self._auth_handler.complete_sso_ui_auth( + user_id, current_session.ui_auth_session_id, request + ) - async def _map_saml_response_to_user(self, resp_bytes, client_redirect_url): + else: + self._auth_handler.complete_sso_login(user_id, request, relay_state) + + async def _map_saml_response_to_user( + self, resp_bytes: str, client_redirect_url: str + ) -> Tuple[str, Optional[Saml2SessionData]]: + """ + Given a sample response, retrieve the cached session and user for it. + + Args: + resp_bytes: The SAML response. + client_redirect_url: The redirect URL passed in by the client. + + Returns: + Tuple of the user ID and SAML session associated with this response. + """ try: saml2_auth = self._saml_client.parse_authn_request_response( resp_bytes, @@ -167,7 +196,9 @@ class SamlHandler: logger.info("SAML2 mapped attributes: %s", saml2_auth.ava) - self._outstanding_requests_dict.pop(saml2_auth.in_response_to, None) + current_session = self._outstanding_requests_dict.pop( + saml2_auth.in_response_to, None + ) remote_user_id = self._user_mapping_provider.get_remote_user_id( saml2_auth, client_redirect_url @@ -188,7 +219,7 @@ class SamlHandler: ) if registered_user_id is not None: logger.info("Found existing mapping %s", registered_user_id) - return registered_user_id + return registered_user_id, current_session # backwards-compatibility hack: see if there is an existing user with a # suitable mapping from the uid @@ -213,7 +244,7 @@ class SamlHandler: await self._datastore.record_user_external_id( self._auth_provider_id, remote_user_id, registered_user_id ) - return registered_user_id + return registered_user_id, current_session # Map saml response to user attributes using the configured mapping provider for i in range(1000): @@ -260,7 +291,7 @@ class SamlHandler: await self._datastore.record_user_external_id( self._auth_provider_id, remote_user_id, registered_user_id ) - return registered_user_id + return registered_user_id, current_session def expire_sessions(self): expire_before = self._clock.time_msec() - self._saml2_session_lifetime diff --git a/synapse/res/templates/sso_auth_confirm.html b/synapse/res/templates/sso_auth_confirm.html new file mode 100644 index 0000000000..0d9de9d465 --- /dev/null +++ b/synapse/res/templates/sso_auth_confirm.html @@ -0,0 +1,14 @@ + + + Authentication + + +
    +

    + A client is trying to {{ description | e }}. To confirm this action, + re-authenticate with single sign-on. + If you did not expect this, your account may be compromised! +

    +
    + + diff --git a/synapse/rest/client/v2_alpha/account.py b/synapse/rest/client/v2_alpha/account.py index f80b5e40ea..31435b1e1c 100644 --- a/synapse/rest/client/v2_alpha/account.py +++ b/synapse/rest/client/v2_alpha/account.py @@ -234,7 +234,11 @@ class PasswordRestServlet(RestServlet): if self.auth.has_access_token(request): requester = await self.auth.get_user_by_req(request) params = await self.auth_handler.validate_user_via_ui_auth( - requester, request, body, self.hs.get_ip_from_request(request), + requester, + request, + body, + self.hs.get_ip_from_request(request), + "modify your account password", ) user_id = requester.user.to_string() else: @@ -244,6 +248,7 @@ class PasswordRestServlet(RestServlet): request, body, self.hs.get_ip_from_request(request), + "modify your account password", ) if LoginType.EMAIL_IDENTITY in result: @@ -311,7 +316,11 @@ class DeactivateAccountRestServlet(RestServlet): return 200, {} await self.auth_handler.validate_user_via_ui_auth( - requester, request, body, self.hs.get_ip_from_request(request), + requester, + request, + body, + self.hs.get_ip_from_request(request), + "deactivate your account", ) result = await self._deactivate_account_handler.deactivate_account( requester.user.to_string(), erase, id_server=body.get("id_server") @@ -669,7 +678,11 @@ class ThreepidAddRestServlet(RestServlet): assert_valid_client_secret(client_secret) await self.auth_handler.validate_user_via_ui_auth( - requester, request, body, self.hs.get_ip_from_request(request), + requester, + request, + body, + self.hs.get_ip_from_request(request), + "add a third-party identifier to your account", ) validation_session = await self.identity_handler.validate_threepid_session( diff --git a/synapse/rest/client/v2_alpha/auth.py b/synapse/rest/client/v2_alpha/auth.py index 85cf5a14c6..1787562b90 100644 --- a/synapse/rest/client/v2_alpha/auth.py +++ b/synapse/rest/client/v2_alpha/auth.py @@ -18,6 +18,7 @@ import logging from synapse.api.constants import LoginType from synapse.api.errors import SynapseError from synapse.api.urls import CLIENT_API_PREFIX +from synapse.handlers.auth import SUCCESS_TEMPLATE from synapse.http.server import finish_request from synapse.http.servlet import RestServlet, parse_string @@ -89,30 +90,6 @@ TERMS_TEMPLATE = """ """ -SUCCESS_TEMPLATE = """ - - -Success! - - - - - -
    -

    Thank you

    -

    You may now close this window and return to the application

    -
    - - -""" - class AuthRestServlet(RestServlet): """ @@ -130,6 +107,11 @@ class AuthRestServlet(RestServlet): self.auth_handler = hs.get_auth_handler() self.registration_handler = hs.get_registration_handler() + # SSO configuration. + self._saml_enabled = hs.config.saml2_enabled + if self._saml_enabled: + self._saml_handler = hs.get_saml_handler() + def on_GET(self, request, stagetype): session = parse_string(request, "session") if not session: @@ -150,6 +132,15 @@ class AuthRestServlet(RestServlet): "myurl": "%s/r0/auth/%s/fallback/web" % (CLIENT_API_PREFIX, LoginType.TERMS), } + + elif stagetype == LoginType.SSO and self._saml_enabled: + # Display a confirmation page which prompts the user to + # re-authenticate with their SSO provider. + client_redirect_url = "" + sso_redirect_url = self._saml_handler.handle_redirect_request( + client_redirect_url, session + ) + html = self.auth_handler.start_sso_ui_auth(sso_redirect_url, session) else: raise SynapseError(404, "Unknown auth stage type") @@ -210,6 +201,9 @@ class AuthRestServlet(RestServlet): "myurl": "%s/r0/auth/%s/fallback/web" % (CLIENT_API_PREFIX, LoginType.TERMS), } + elif stagetype == LoginType.SSO: + # The SSO fallback workflow should not post here, + raise SynapseError(404, "Fallback SSO auth does not support POST requests.") else: raise SynapseError(404, "Unknown auth stage type") diff --git a/synapse/rest/client/v2_alpha/devices.py b/synapse/rest/client/v2_alpha/devices.py index 119d979052..c0714fcfb1 100644 --- a/synapse/rest/client/v2_alpha/devices.py +++ b/synapse/rest/client/v2_alpha/devices.py @@ -81,7 +81,11 @@ class DeleteDevicesRestServlet(RestServlet): assert_params_in_dict(body, ["devices"]) await self.auth_handler.validate_user_via_ui_auth( - requester, request, body, self.hs.get_ip_from_request(request), + requester, + request, + body, + self.hs.get_ip_from_request(request), + "remove device(s) from your account", ) await self.device_handler.delete_devices( @@ -127,7 +131,11 @@ class DeviceRestServlet(RestServlet): raise await self.auth_handler.validate_user_via_ui_auth( - requester, request, body, self.hs.get_ip_from_request(request), + requester, + request, + body, + self.hs.get_ip_from_request(request), + "remove a device from your account", ) await self.device_handler.delete_device(requester.user.to_string(), device_id) diff --git a/synapse/rest/client/v2_alpha/keys.py b/synapse/rest/client/v2_alpha/keys.py index 5eb7ef35a4..8f41a3edbf 100644 --- a/synapse/rest/client/v2_alpha/keys.py +++ b/synapse/rest/client/v2_alpha/keys.py @@ -263,7 +263,11 @@ class SigningKeyUploadServlet(RestServlet): body = parse_json_object_from_request(request) await self.auth_handler.validate_user_via_ui_auth( - requester, request, body, self.hs.get_ip_from_request(request), + requester, + request, + body, + self.hs.get_ip_from_request(request), + "add a device signing key to your account", ) result = await self.e2e_keys_handler.upload_signing_keys_for_user(user_id, body) diff --git a/synapse/rest/client/v2_alpha/register.py b/synapse/rest/client/v2_alpha/register.py index 66fc8ec179..431ecf4f84 100644 --- a/synapse/rest/client/v2_alpha/register.py +++ b/synapse/rest/client/v2_alpha/register.py @@ -505,6 +505,7 @@ class RegisterRestServlet(RestServlet): request, body, self.hs.get_ip_from_request(request), + "register a new account", ) # Check that we're not trying to register a denied 3pid. From 529462b5c044f7f7491fd41ab7d0682d01a6236b Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Thu, 2 Apr 2020 11:30:49 +0100 Subject: [PATCH 1295/1623] 1.12.1 --- CHANGES.md | 6 ++++++ debian/changelog | 6 ++++++ synapse/__init__.py | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index a77768de58..e5608baa11 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,9 @@ +Synapse 1.12.1 (2020-04-02) +=========================== + +No significant changes since 1.12.1rc1. + + Synapse 1.12.1rc1 (2020-03-31) ============================== diff --git a/debian/changelog b/debian/changelog index 39ec9da7ab..8e14e59c0d 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.12.1) stable; urgency=medium + + * New synapse release 1.12.1. + + -- Synapse Packaging team Mon, 02 Apr 2020 11:30:47 +0000 + matrix-synapse-py3 (1.12.0) stable; urgency=medium * New synapse release 1.12.0. diff --git a/synapse/__init__.py b/synapse/__init__.py index c3c5b20f11..5df7d51ab1 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -36,7 +36,7 @@ try: except ImportError: pass -__version__ = "1.12.1rc1" +__version__ = "1.12.1" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From af47264b78c33698f6a70ce1ce3d32774d65de72 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 2 Apr 2020 12:04:55 +0100 Subject: [PATCH 1296/1623] review comment --- synapse/storage/background_updates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/storage/background_updates.py b/synapse/storage/background_updates.py index 0e430356cd..510963eb7f 100644 --- a/synapse/storage/background_updates.py +++ b/synapse/storage/background_updates.py @@ -132,7 +132,7 @@ class BackgroundUpdater(object): if self._all_done: return True - # obviously, if we have things in our queue, we're not done. + # obviously, if we are currently processing an update, we're not done. if self._current_background_update: return False From b730480abbb31ce0e9c9957ef30dfcd0c646a1b1 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Thu, 2 Apr 2020 11:30:49 +0100 Subject: [PATCH 1297/1623] 1.12.1 --- CHANGES.md | 6 ++++++ debian/changelog | 6 ++++++ synapse/__init__.py | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index a77768de58..e5608baa11 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,9 @@ +Synapse 1.12.1 (2020-04-02) +=========================== + +No significant changes since 1.12.1rc1. + + Synapse 1.12.1rc1 (2020-03-31) ============================== diff --git a/debian/changelog b/debian/changelog index 39ec9da7ab..8e14e59c0d 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.12.1) stable; urgency=medium + + * New synapse release 1.12.1. + + -- Synapse Packaging team Mon, 02 Apr 2020 11:30:47 +0000 + matrix-synapse-py3 (1.12.0) stable; urgency=medium * New synapse release 1.12.0. diff --git a/synapse/__init__.py b/synapse/__init__.py index c3c5b20f11..5df7d51ab1 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -36,7 +36,7 @@ try: except ImportError: pass -__version__ = "1.12.1rc1" +__version__ = "1.12.1" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From ec56620ff66aa06953b2d675e71df177ef7f375d Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Thu, 2 Apr 2020 18:08:03 +0100 Subject: [PATCH 1298/1623] Pin Pillow>=4.3.0,<7.1.0 to fix dep issue --- synapse/python_dependencies.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/synapse/python_dependencies.py b/synapse/python_dependencies.py index 8de8cb2c12..3274eb9863 100644 --- a/synapse/python_dependencies.py +++ b/synapse/python_dependencies.py @@ -61,7 +61,9 @@ REQUIREMENTS = [ "pyasn1-modules>=0.0.7", "daemonize>=2.3.1", "bcrypt>=3.1.0", - "pillow>=4.3.0", + # Pillow 7.1.0 causes the following issue on debian buster: + # https://github.com/python-pillow/Pillow/issues/2377 + "pillow>=4.3.0,<7.1.0", "sortedcontainers>=1.4.4", "pymacaroons>=0.13.0", "msgpack>=0.5.2", From 08edefe694a59294aae1f24df31ec09979987a95 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Thu, 2 Apr 2020 19:02:45 +0100 Subject: [PATCH 1299/1623] 1.12.2 --- CHANGES.md | 10 ++++++++++ debian/changelog | 6 ++++++ synapse/__init__.py | 2 +- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index e5608baa11..1ac3c60043 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,13 @@ +Synapse 1.12.2 (2020-04-02) +=========================== + +This release fixes [an +issue](https://github.com/matrix-org/synapse/issues/7208) with building the +debian packages. + +No other significant changes since 1.12.1. + + Synapse 1.12.1 (2020-04-02) =========================== diff --git a/debian/changelog b/debian/changelog index 8e14e59c0d..03b30cd12f 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.12.2) stable; urgency=medium + + * New synapse release 1.12.2. + + -- Synapse Packaging team Mon, 02 Apr 2020 19:02:17 +0000 + matrix-synapse-py3 (1.12.1) stable; urgency=medium * New synapse release 1.12.1. diff --git a/synapse/__init__.py b/synapse/__init__.py index 5df7d51ab1..bdad75113d 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -36,7 +36,7 @@ try: except ImportError: pass -__version__ = "1.12.1" +__version__ = "1.12.2" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From f7d6e849b35bef390c5234d1309a301dd04f8330 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Thu, 2 Apr 2020 19:08:06 +0100 Subject: [PATCH 1300/1623] Fix changelog wording --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 1ac3c60043..5cec3d817d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,7 +1,7 @@ Synapse 1.12.2 (2020-04-02) =========================== -This release fixes [an +This release works around [an issue](https://github.com/matrix-org/synapse/issues/7208) with building the debian packages. From 6d7cec7a57ca258bcf28e7eb174d970670f7a652 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 3 Apr 2020 10:23:36 +0100 Subject: [PATCH 1301/1623] Fix the debian build in a better way. (#7212) --- changelog.d/7212.misc | 1 + debian/changelog | 7 +++++++ debian/rules | 33 +++++++++++++++++++++++++++------ synapse/python_dependencies.py | 4 +--- 4 files changed, 36 insertions(+), 9 deletions(-) create mode 100644 changelog.d/7212.misc diff --git a/changelog.d/7212.misc b/changelog.d/7212.misc new file mode 100644 index 0000000000..b57fc5f288 --- /dev/null +++ b/changelog.d/7212.misc @@ -0,0 +1 @@ +Roll back the pin to Pillow 7.0 which was introduced in Synapse 1.12.2. diff --git a/debian/changelog b/debian/changelog index 03b30cd12f..6bafe468d9 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,10 @@ +matrix-synapse-py3 (1.12.2ubuntu1) UNRELEASED; urgency=medium + + * Update the Debian build scripts to handle the new installation paths + for the support libraries introduced by Pillow 7.1.1. + + -- Richard van der Hoff Thu, 02 Apr 2020 23:18:52 +0100 + matrix-synapse-py3 (1.12.2) stable; urgency=medium * New synapse release 1.12.2. diff --git a/debian/rules b/debian/rules index a4d2ce2ba4..c744060a57 100755 --- a/debian/rules +++ b/debian/rules @@ -15,17 +15,38 @@ override_dh_installinit: # we don't really want to strip the symbols from our object files. override_dh_strip: +# dh_shlibdeps calls dpkg-shlibdeps, which finds all the binary files +# (executables and shared libs) in the package, and looks for the shared +# libraries that they depend on. It then adds a dependency on the package that +# contains that library to the package. +# +# We make two modifications to that process... +# override_dh_shlibdeps: - # make the postgres package's dependencies a recommendation - # rather than a hard dependency. + # Firstly, postgres is not a hard dependency for us, so we want to make + # the things that psycopg2 depends on (such as libpq) be + # recommendations rather than hard dependencies. We do so by + # running dpkg-shlibdeps manually on psycopg2's libs. + # find debian/$(PACKAGE_NAME)/ -path '*/site-packages/psycopg2/*.so' | \ xargs dpkg-shlibdeps -Tdebian/$(PACKAGE_NAME).substvars \ -pshlibs1 -dRecommends - # all the other dependencies can be normal 'Depends' requirements, - # except for PIL's, which is self-contained and which confuses - # dpkg-shlibdeps. - dh_shlibdeps -X site-packages/PIL/.libs -X site-packages/psycopg2 + # secondly, we exclude PIL's libraries from the process. They are known + # to be self-contained, but they have interdependencies and + # dpkg-shlibdeps doesn't know how to resolve them. + # + # As of Pillow 7.1.0, these libraries are in + # site-packages/Pillow.libs. Previously, they were in + # site-packages/PIL/.libs. + # + # (we also need to exclude psycopg2, of course, since we've already + # dealt with that.) + # + dh_shlibdeps \ + -X site-packages/PIL/.libs \ + -X site-packages/Pillow.libs \ + -X site-packages/psycopg2 override_dh_virtualenv: ./debian/build_virtualenv diff --git a/synapse/python_dependencies.py b/synapse/python_dependencies.py index 3274eb9863..8de8cb2c12 100644 --- a/synapse/python_dependencies.py +++ b/synapse/python_dependencies.py @@ -61,9 +61,7 @@ REQUIREMENTS = [ "pyasn1-modules>=0.0.7", "daemonize>=2.3.1", "bcrypt>=3.1.0", - # Pillow 7.1.0 causes the following issue on debian buster: - # https://github.com/python-pillow/Pillow/issues/2377 - "pillow>=4.3.0,<7.1.0", + "pillow>=4.3.0", "sortedcontainers>=1.4.4", "pymacaroons>=0.13.0", "msgpack>=0.5.2", From daa1ac89a0be4dd3cc941da4caeb2ddcbd701eff Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 3 Apr 2020 10:40:22 +0100 Subject: [PATCH 1302/1623] Fix device list update stream ids going backward (#7158) Occasionally we could get a federation device list update transaction which looked like: ``` [ {'edu_type': 'm.device_list_update', 'content': {'user_id': '@user:test', 'device_id': 'D2', 'prev_id': [], 'stream_id': 12, 'deleted': True}}, {'edu_type': 'm.device_list_update', 'content': {'user_id': '@user:test', 'device_id': 'D1', 'prev_id': [12], 'stream_id': 11, 'deleted': True}}, {'edu_type': 'm.device_list_update', 'content': {'user_id': '@user:test', 'device_id': 'D3', 'prev_id': [11], 'stream_id': 13, 'deleted': True}} ] ``` Having `stream_ids` which are lower than `prev_ids` looks odd. It might work (I'm not actually sure), but in any case it doesn't seem like a reasonable thing to expect other implementations to support. --- changelog.d/7158.misc | 1 + synapse/storage/data_stores/main/devices.py | 10 ++++++++-- tests/federation/test_federation_sender.py | 6 ++++++ 3 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 changelog.d/7158.misc diff --git a/changelog.d/7158.misc b/changelog.d/7158.misc new file mode 100644 index 0000000000..269b8daeb0 --- /dev/null +++ b/changelog.d/7158.misc @@ -0,0 +1 @@ +Fix device list update stream ids going backward. diff --git a/synapse/storage/data_stores/main/devices.py b/synapse/storage/data_stores/main/devices.py index 20995e1b78..dd3561e9b2 100644 --- a/synapse/storage/data_stores/main/devices.py +++ b/synapse/storage/data_stores/main/devices.py @@ -165,7 +165,6 @@ class DeviceWorkerStore(SQLBaseStore): # the max stream_id across each set of duplicate entries # # maps (user_id, device_id) -> (stream_id, opentracing_context) - # as long as their stream_id does not match that of the last row # # opentracing_context contains the opentracing metadata for the request # that created the poke @@ -270,7 +269,14 @@ class DeviceWorkerStore(SQLBaseStore): prev_id = yield self._get_last_device_update_for_remote_user( destination, user_id, from_stream_id ) - for device_id, device in iteritems(user_devices): + + # make sure we go through the devices in stream order + device_ids = sorted( + user_devices.keys(), key=lambda i: query_map[(user_id, i)][0], + ) + + for device_id in device_ids: + device = user_devices[device_id] stream_id, opentracing_context = query_map[(user_id, device_id)] result = { "user_id": user_id, diff --git a/tests/federation/test_federation_sender.py b/tests/federation/test_federation_sender.py index a5fe5c6880..33105576af 100644 --- a/tests/federation/test_federation_sender.py +++ b/tests/federation/test_federation_sender.py @@ -297,6 +297,7 @@ class FederationSenderDevicesTestCases(HomeserverTestCase): c = edu["content"] if stream_id is not None: self.assertEqual(c["prev_id"], [stream_id]) + self.assertGreaterEqual(c["stream_id"], stream_id) stream_id = c["stream_id"] devices = {edu["content"]["device_id"] for edu in self.edus} self.assertEqual({"D1", "D2"}, devices) @@ -330,6 +331,7 @@ class FederationSenderDevicesTestCases(HomeserverTestCase): c.items(), {"user_id": u1, "prev_id": [stream_id], "deleted": True}.items(), ) + self.assertGreaterEqual(c["stream_id"], stream_id) stream_id = c["stream_id"] devices = {edu["content"]["device_id"] for edu in self.edus} self.assertEqual({"D1", "D2", "D3"}, devices) @@ -366,6 +368,8 @@ class FederationSenderDevicesTestCases(HomeserverTestCase): self.assertEqual(edu["edu_type"], "m.device_list_update") c = edu["content"] self.assertEqual(c["prev_id"], [stream_id] if stream_id is not None else []) + if stream_id is not None: + self.assertGreaterEqual(c["stream_id"], stream_id) stream_id = c["stream_id"] devices = {edu["content"]["device_id"] for edu in self.edus} self.assertEqual({"D1", "D2", "D3"}, devices) @@ -482,6 +486,8 @@ class FederationSenderDevicesTestCases(HomeserverTestCase): } self.assertLessEqual(expected.items(), content.items()) + if prev_stream_id is not None: + self.assertGreaterEqual(content["stream_id"], prev_stream_id) return content["stream_id"] def check_signing_key_update_txn(self, txn: JsonDict,) -> None: From fcc2de7a0c0c7a02b935c4b35394f228a5d5a304 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 3 Apr 2020 10:51:32 +0100 Subject: [PATCH 1303/1623] Update docstring per review comments --- synapse/storage/background_updates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/storage/background_updates.py b/synapse/storage/background_updates.py index 510963eb7f..59f3394b0a 100644 --- a/synapse/storage/background_updates.py +++ b/synapse/storage/background_updates.py @@ -179,7 +179,7 @@ class BackgroundUpdater(object): desired_duration_ms(float): How long we want to spend updating. Returns: - True if there is no more work to do, otherwise False + True if we have finished running all the background updates, otherwise False """ def get_background_updates_txn(txn): From 29ce90358c06ca2452b2ecb55670103de3557109 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 3 Apr 2020 10:57:07 +0100 Subject: [PATCH 1304/1623] 1.12.3 --- CHANGES.md | 9 +++++++++ changelog.d/7212.misc | 1 - debian/changelog | 8 ++++++-- synapse/__init__.py | 2 +- 4 files changed, 16 insertions(+), 4 deletions(-) delete mode 100644 changelog.d/7212.misc diff --git a/CHANGES.md b/CHANGES.md index 5cec3d817d..e9ca767644 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,12 @@ +Synapse 1.12.3 (2020-04-03) +=========================== + +Internal Changes +---------------- + +- Roll back the pin to Pillow 7.0 which was introduced in Synapse 1.12.2. ([\#7212](https://github.com/matrix-org/synapse/issues/7212)) + + Synapse 1.12.2 (2020-04-02) =========================== diff --git a/changelog.d/7212.misc b/changelog.d/7212.misc deleted file mode 100644 index b57fc5f288..0000000000 --- a/changelog.d/7212.misc +++ /dev/null @@ -1 +0,0 @@ -Roll back the pin to Pillow 7.0 which was introduced in Synapse 1.12.2. diff --git a/debian/changelog b/debian/changelog index 6bafe468d9..642115fc5a 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,9 +1,13 @@ -matrix-synapse-py3 (1.12.2ubuntu1) UNRELEASED; urgency=medium +matrix-synapse-py3 (1.12.3) stable; urgency=medium + [ Richard van der Hoff ] * Update the Debian build scripts to handle the new installation paths for the support libraries introduced by Pillow 7.1.1. - -- Richard van der Hoff Thu, 02 Apr 2020 23:18:52 +0100 + [ Synapse Packaging team ] + * New synapse release 1.12.3. + + -- Synapse Packaging team Fri, 03 Apr 2020 10:55:03 +0100 matrix-synapse-py3 (1.12.2) stable; urgency=medium diff --git a/synapse/__init__.py b/synapse/__init__.py index bdad75113d..3bf2d02450 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -36,7 +36,7 @@ try: except ImportError: pass -__version__ = "1.12.2" +__version__ = "1.12.3" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 553c8a9b6b23c6aa79f70e61e9490fd6f97def2e Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 3 Apr 2020 11:00:57 +0100 Subject: [PATCH 1305/1623] tweak changelog --- CHANGES.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index e9ca767644..e78c8e48e7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,11 +1,8 @@ Synapse 1.12.3 (2020-04-03) =========================== -Internal Changes ----------------- - -- Roll back the pin to Pillow 7.0 which was introduced in Synapse 1.12.2. ([\#7212](https://github.com/matrix-org/synapse/issues/7212)) - +- Remove the the pin to Pillow 7.0 which was introduced in Synapse 1.12.2, and +correctly fix the issue with building the Debian packages. ([\#7212](https://github.com/matrix-org/synapse/issues/7212)) Synapse 1.12.2 (2020-04-02) =========================== From 8d4cbdeaa9765ae0d87ec82b053f12ed8162f6f5 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 27 Mar 2020 20:18:56 +0000 Subject: [PATCH 1306/1623] Revert "Improve the UX of the login fallback when using SSO (#7152)" This was incorrectly merged to `master` instead of develop. This reverts commit 90246344e340bce3417fb330da6be9338a701c5c. --- changelog.d/7152.feature | 1 - synapse/static/client/login/index.html | 2 +- synapse/static/client/login/js/login.js | 51 ++++++++++--------------- 3 files changed, 22 insertions(+), 32 deletions(-) delete mode 100644 changelog.d/7152.feature diff --git a/changelog.d/7152.feature b/changelog.d/7152.feature deleted file mode 100644 index fafa79c7e7..0000000000 --- a/changelog.d/7152.feature +++ /dev/null @@ -1 +0,0 @@ -Improve the support for SSO authentication on the login fallback page. diff --git a/synapse/static/client/login/index.html b/synapse/static/client/login/index.html index 712b0e3980..bcb6bc6bb7 100644 --- a/synapse/static/client/login/index.html +++ b/synapse/static/client/login/index.html @@ -9,7 +9,7 @@

    -

    +

    Log in with one of the following methods

    diff --git a/synapse/static/client/login/js/login.js b/synapse/static/client/login/js/login.js index debe464371..276c271bbe 100644 --- a/synapse/static/client/login/js/login.js +++ b/synapse/static/client/login/js/login.js @@ -1,41 +1,37 @@ window.matrixLogin = { endpoint: location.origin + "/_matrix/client/r0/login", serverAcceptsPassword: false, + serverAcceptsCas: false, serverAcceptsSso: false, }; -var title_pre_auth = "Log in with one of the following methods"; -var title_post_auth = "Logging in..."; - var submitPassword = function(user, pwd) { console.log("Logging in with password..."); - set_title(title_post_auth); var data = { type: "m.login.password", user: user, password: pwd, }; $.post(matrixLogin.endpoint, JSON.stringify(data), function(response) { + show_login(); matrixLogin.onLogin(response); }).error(errorFunc); }; var submitToken = function(loginToken) { console.log("Logging in with login token..."); - set_title(title_post_auth); var data = { type: "m.login.token", token: loginToken }; $.post(matrixLogin.endpoint, JSON.stringify(data), function(response) { + show_login(); matrixLogin.onLogin(response); }).error(errorFunc); }; var errorFunc = function(err) { - // We want to show the error to the user rather than redirecting immediately to the - // SSO portal (if SSO is the only login option), so we inhibit the redirect. - show_login(true); + show_login(); if (err.responseJSON && err.responseJSON.error) { setFeedbackString(err.responseJSON.error + " (" + err.responseJSON.errcode + ")"); @@ -49,33 +45,26 @@ var setFeedbackString = function(text) { $("#feedback").text(text); }; -var show_login = function(inhibit_redirect) { +var show_login = function() { + $("#loading").hide(); + var this_page = window.location.origin + window.location.pathname; $("#sso_redirect_url").val(this_page); - // If inhibit_redirect is false, and SSO is the only supported login method, we can - // redirect straight to the SSO page - if (matrixLogin.serverAcceptsSso) { - if (!inhibit_redirect && !matrixLogin.serverAcceptsPassword) { - $("#sso_form").submit(); - return; - } - - // Otherwise, show the SSO form - $("#sso_form").show(); - } - if (matrixLogin.serverAcceptsPassword) { $("#password_flow").show(); } - if (!matrixLogin.serverAcceptsPassword && !matrixLogin.serverAcceptsSso) { - $("#no_login_types").show(); + if (matrixLogin.serverAcceptsSso) { + $("#sso_flow").show(); + } else if (matrixLogin.serverAcceptsCas) { + $("#sso_form").attr("action", "/_matrix/client/r0/login/cas/redirect"); + $("#sso_flow").show(); } - set_title(title_pre_auth); - - $("#loading").hide(); + if (!matrixLogin.serverAcceptsPassword && !matrixLogin.serverAcceptsCas && !matrixLogin.serverAcceptsSso) { + $("#no_login_types").show(); + } }; var show_spinner = function() { @@ -85,15 +74,17 @@ var show_spinner = function() { $("#loading").show(); }; -var set_title = function(title) { - $("#title").text(title); -}; var fetch_info = function(cb) { $.get(matrixLogin.endpoint, function(response) { var serverAcceptsPassword = false; + var serverAcceptsCas = false; for (var i=0; i Date: Fri, 27 Mar 2020 20:21:09 +0000 Subject: [PATCH 1307/1623] Revert "Merge pull request #7153 from matrix-org/babolivier/sso_whitelist_login_fallback" This was incorrectly merged to master. This reverts commit 319c41f573eb14a966367b60b2e6e93bf6b028d9, reversing changes made to 229eb81498b0fe1da81e9b5b333a0285acde9446. --- changelog.d/7153.feature | 1 - docs/sample_config.yaml | 4 ---- synapse/config/sso.py | 15 --------------- tests/rest/client/v1/test_login.py | 9 +-------- 4 files changed, 1 insertion(+), 28 deletions(-) delete mode 100644 changelog.d/7153.feature diff --git a/changelog.d/7153.feature b/changelog.d/7153.feature deleted file mode 100644 index 414ebe1f69..0000000000 --- a/changelog.d/7153.feature +++ /dev/null @@ -1 +0,0 @@ -Always whitelist the login fallback in the SSO configuration if `public_baseurl` is set. diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 07e922dc27..2ff0dd05a2 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -1392,10 +1392,6 @@ sso: # phishing attacks from evil.site. To avoid this, include a slash after the # hostname: "https://my.client/". # - # If public_baseurl is set, then the login fallback page (used by clients - # that don't natively support the required login flows) is whitelisted in - # addition to any URLs in this list. - # # By default, this list is empty. # #client_whitelist: diff --git a/synapse/config/sso.py b/synapse/config/sso.py index ec3dca9efc..95762689bc 100644 --- a/synapse/config/sso.py +++ b/synapse/config/sso.py @@ -39,17 +39,6 @@ class SSOConfig(Config): self.sso_client_whitelist = sso_config.get("client_whitelist") or [] - # Attempt to also whitelist the server's login fallback, since that fallback sets - # the redirect URL to itself (so it can process the login token then return - # gracefully to the client). This would make it pointless to ask the user for - # confirmation, since the URL the confirmation page would be showing wouldn't be - # the client's. - # public_baseurl is an optional setting, so we only add the fallback's URL to the - # list if it's provided (because we can't figure out what that URL is otherwise). - if self.public_baseurl: - login_fallback_url = self.public_baseurl + "_matrix/static/client/login" - self.sso_client_whitelist.append(login_fallback_url) - def generate_config_section(self, **kwargs): return """\ # Additional settings to use with single-sign on systems such as SAML2 and CAS. @@ -65,10 +54,6 @@ class SSOConfig(Config): # phishing attacks from evil.site. To avoid this, include a slash after the # hostname: "https://my.client/". # - # If public_baseurl is set, then the login fallback page (used by clients - # that don't natively support the required login flows) is whitelisted in - # addition to any URLs in this list. - # # By default, this list is empty. # #client_whitelist: diff --git a/tests/rest/client/v1/test_login.py b/tests/rest/client/v1/test_login.py index aed8853d6e..da2c9bfa1e 100644 --- a/tests/rest/client/v1/test_login.py +++ b/tests/rest/client/v1/test_login.py @@ -350,14 +350,7 @@ class CASRedirectConfirmTestCase(unittest.HomeserverTestCase): def test_cas_redirect_whitelisted(self): """Tests that the SSO login flow serves a redirect to a whitelisted url """ - self._test_redirect("https://legit-site.com/") - - @override_config({"public_baseurl": "https://example.com"}) - def test_cas_redirect_login_fallback(self): - self._test_redirect("https://example.com/_matrix/static/client/login") - - def _test_redirect(self, redirect_url): - """Tests that the SSO login flow serves a redirect for the given redirect URL.""" + redirect_url = "https://legit-site.com/" cas_ticket_url = ( "/_matrix/client/r0/login/cas/ticket?redirectUrl=%s&ticket=ticket" % (urllib.parse.quote(redirect_url)) From 14a8e7129793be3c7d27ea810b26efe4fba09bc9 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 3 Apr 2020 11:28:43 +0100 Subject: [PATCH 1308/1623] Revert "Revert "Improve the UX of the login fallback when using SSO (#7152)"" This reverts commit 8d4cbdeaa9765ae0d87ec82b053f12ed8162f6f5. --- changelog.d/7152.feature | 1 + synapse/static/client/login/index.html | 2 +- synapse/static/client/login/js/login.js | 51 +++++++++++++++---------- 3 files changed, 32 insertions(+), 22 deletions(-) create mode 100644 changelog.d/7152.feature diff --git a/changelog.d/7152.feature b/changelog.d/7152.feature new file mode 100644 index 0000000000..fafa79c7e7 --- /dev/null +++ b/changelog.d/7152.feature @@ -0,0 +1 @@ +Improve the support for SSO authentication on the login fallback page. diff --git a/synapse/static/client/login/index.html b/synapse/static/client/login/index.html index bcb6bc6bb7..712b0e3980 100644 --- a/synapse/static/client/login/index.html +++ b/synapse/static/client/login/index.html @@ -9,7 +9,7 @@

    -

    Log in with one of the following methods

    +

    diff --git a/synapse/static/client/login/js/login.js b/synapse/static/client/login/js/login.js index 276c271bbe..debe464371 100644 --- a/synapse/static/client/login/js/login.js +++ b/synapse/static/client/login/js/login.js @@ -1,37 +1,41 @@ window.matrixLogin = { endpoint: location.origin + "/_matrix/client/r0/login", serverAcceptsPassword: false, - serverAcceptsCas: false, serverAcceptsSso: false, }; +var title_pre_auth = "Log in with one of the following methods"; +var title_post_auth = "Logging in..."; + var submitPassword = function(user, pwd) { console.log("Logging in with password..."); + set_title(title_post_auth); var data = { type: "m.login.password", user: user, password: pwd, }; $.post(matrixLogin.endpoint, JSON.stringify(data), function(response) { - show_login(); matrixLogin.onLogin(response); }).error(errorFunc); }; var submitToken = function(loginToken) { console.log("Logging in with login token..."); + set_title(title_post_auth); var data = { type: "m.login.token", token: loginToken }; $.post(matrixLogin.endpoint, JSON.stringify(data), function(response) { - show_login(); matrixLogin.onLogin(response); }).error(errorFunc); }; var errorFunc = function(err) { - show_login(); + // We want to show the error to the user rather than redirecting immediately to the + // SSO portal (if SSO is the only login option), so we inhibit the redirect. + show_login(true); if (err.responseJSON && err.responseJSON.error) { setFeedbackString(err.responseJSON.error + " (" + err.responseJSON.errcode + ")"); @@ -45,26 +49,33 @@ var setFeedbackString = function(text) { $("#feedback").text(text); }; -var show_login = function() { - $("#loading").hide(); - +var show_login = function(inhibit_redirect) { var this_page = window.location.origin + window.location.pathname; $("#sso_redirect_url").val(this_page); + // If inhibit_redirect is false, and SSO is the only supported login method, we can + // redirect straight to the SSO page + if (matrixLogin.serverAcceptsSso) { + if (!inhibit_redirect && !matrixLogin.serverAcceptsPassword) { + $("#sso_form").submit(); + return; + } + + // Otherwise, show the SSO form + $("#sso_form").show(); + } + if (matrixLogin.serverAcceptsPassword) { $("#password_flow").show(); } - if (matrixLogin.serverAcceptsSso) { - $("#sso_flow").show(); - } else if (matrixLogin.serverAcceptsCas) { - $("#sso_form").attr("action", "/_matrix/client/r0/login/cas/redirect"); - $("#sso_flow").show(); - } - - if (!matrixLogin.serverAcceptsPassword && !matrixLogin.serverAcceptsCas && !matrixLogin.serverAcceptsSso) { + if (!matrixLogin.serverAcceptsPassword && !matrixLogin.serverAcceptsSso) { $("#no_login_types").show(); } + + set_title(title_pre_auth); + + $("#loading").hide(); }; var show_spinner = function() { @@ -74,17 +85,15 @@ var show_spinner = function() { $("#loading").show(); }; +var set_title = function(title) { + $("#title").text(title); +}; var fetch_info = function(cb) { $.get(matrixLogin.endpoint, function(response) { var serverAcceptsPassword = false; - var serverAcceptsCas = false; for (var i=0; i Date: Fri, 3 Apr 2020 11:28:49 +0100 Subject: [PATCH 1309/1623] Revert "Revert "Merge pull request #7153 from matrix-org/babolivier/sso_whitelist_login_fallback"" This reverts commit 0122ef1037c8bfe826ea09d9fc7cd63fb9c59fd1. --- changelog.d/7153.feature | 1 + docs/sample_config.yaml | 4 ++++ synapse/config/sso.py | 15 +++++++++++++++ tests/rest/client/v1/test_login.py | 9 ++++++++- 4 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 changelog.d/7153.feature diff --git a/changelog.d/7153.feature b/changelog.d/7153.feature new file mode 100644 index 0000000000..414ebe1f69 --- /dev/null +++ b/changelog.d/7153.feature @@ -0,0 +1 @@ +Always whitelist the login fallback in the SSO configuration if `public_baseurl` is set. diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 2ff0dd05a2..07e922dc27 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -1392,6 +1392,10 @@ sso: # phishing attacks from evil.site. To avoid this, include a slash after the # hostname: "https://my.client/". # + # If public_baseurl is set, then the login fallback page (used by clients + # that don't natively support the required login flows) is whitelisted in + # addition to any URLs in this list. + # # By default, this list is empty. # #client_whitelist: diff --git a/synapse/config/sso.py b/synapse/config/sso.py index 95762689bc..ec3dca9efc 100644 --- a/synapse/config/sso.py +++ b/synapse/config/sso.py @@ -39,6 +39,17 @@ class SSOConfig(Config): self.sso_client_whitelist = sso_config.get("client_whitelist") or [] + # Attempt to also whitelist the server's login fallback, since that fallback sets + # the redirect URL to itself (so it can process the login token then return + # gracefully to the client). This would make it pointless to ask the user for + # confirmation, since the URL the confirmation page would be showing wouldn't be + # the client's. + # public_baseurl is an optional setting, so we only add the fallback's URL to the + # list if it's provided (because we can't figure out what that URL is otherwise). + if self.public_baseurl: + login_fallback_url = self.public_baseurl + "_matrix/static/client/login" + self.sso_client_whitelist.append(login_fallback_url) + def generate_config_section(self, **kwargs): return """\ # Additional settings to use with single-sign on systems such as SAML2 and CAS. @@ -54,6 +65,10 @@ class SSOConfig(Config): # phishing attacks from evil.site. To avoid this, include a slash after the # hostname: "https://my.client/". # + # If public_baseurl is set, then the login fallback page (used by clients + # that don't natively support the required login flows) is whitelisted in + # addition to any URLs in this list. + # # By default, this list is empty. # #client_whitelist: diff --git a/tests/rest/client/v1/test_login.py b/tests/rest/client/v1/test_login.py index da2c9bfa1e..aed8853d6e 100644 --- a/tests/rest/client/v1/test_login.py +++ b/tests/rest/client/v1/test_login.py @@ -350,7 +350,14 @@ class CASRedirectConfirmTestCase(unittest.HomeserverTestCase): def test_cas_redirect_whitelisted(self): """Tests that the SSO login flow serves a redirect to a whitelisted url """ - redirect_url = "https://legit-site.com/" + self._test_redirect("https://legit-site.com/") + + @override_config({"public_baseurl": "https://example.com"}) + def test_cas_redirect_login_fallback(self): + self._test_redirect("https://example.com/_matrix/static/client/login") + + def _test_redirect(self, redirect_url): + """Tests that the SSO login flow serves a redirect for the given redirect URL.""" cas_ticket_url = ( "/_matrix/client/r0/login/cas/ticket?redirectUrl=%s&ticket=ticket" % (urllib.parse.quote(redirect_url)) From bae32740daa5551b6613cafafb5d5bc1a73141ec Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 3 Apr 2020 12:29:30 +0100 Subject: [PATCH 1310/1623] Remove some `run_in_background` calls in replication code (#7203) By running this stuff with `run_in_background`, it won't be correctly reported against the relevant CPU usage stats. Fixes #7202 --- changelog.d/7203.bugfix | 1 + synapse/app/generic_worker.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) create mode 100644 changelog.d/7203.bugfix diff --git a/changelog.d/7203.bugfix b/changelog.d/7203.bugfix new file mode 100644 index 0000000000..8b383952e5 --- /dev/null +++ b/changelog.d/7203.bugfix @@ -0,0 +1 @@ +Fix some worker-mode replication handling not being correctly recorded in CPU usage stats. diff --git a/synapse/app/generic_worker.py b/synapse/app/generic_worker.py index 1ee266f7c5..174bef360f 100644 --- a/synapse/app/generic_worker.py +++ b/synapse/app/generic_worker.py @@ -42,7 +42,7 @@ from synapse.handlers.presence import PresenceHandler, get_interested_parties from synapse.http.server import JsonResource from synapse.http.servlet import RestServlet, parse_json_object_from_request from synapse.http.site import SynapseSite -from synapse.logging.context import LoggingContext, run_in_background +from synapse.logging.context import LoggingContext from synapse.metrics import METRICS_PREFIX, MetricsResource, RegistryProxy from synapse.metrics.background_process_metrics import run_as_background_process from synapse.replication.slave.storage._base import BaseSlavedStore, __func__ @@ -635,7 +635,7 @@ class GenericWorkerReplicationHandler(ReplicationClientHandler): await super(GenericWorkerReplicationHandler, self).on_rdata( stream_name, token, rows ) - run_in_background(self.process_and_notify, stream_name, token, rows) + await self.process_and_notify(stream_name, token, rows) def get_streams_to_replicate(self): args = super(GenericWorkerReplicationHandler, self).get_streams_to_replicate() @@ -650,7 +650,9 @@ class GenericWorkerReplicationHandler(ReplicationClientHandler): async def process_and_notify(self, stream_name, token, rows): try: if self.send_handler: - self.send_handler.process_replication_rows(stream_name, token, rows) + await self.send_handler.process_replication_rows( + stream_name, token, rows + ) if stream_name == EventsStream.NAME: # We shouldn't get multiple rows per token for events stream, so @@ -782,12 +784,12 @@ class FederationSenderHandler(object): def stream_positions(self): return {"federation": self.federation_position} - def process_replication_rows(self, stream_name, token, rows): + async def process_replication_rows(self, stream_name, token, rows): # The federation stream contains things that we want to send out, e.g. # presence, typing, etc. if stream_name == "federation": send_queue.process_rows_for_federation(self.federation_sender, rows) - run_in_background(self.update_token, token) + await self.update_token(token) # We also need to poke the federation sender when new events happen elif stream_name == "events": @@ -795,9 +797,7 @@ class FederationSenderHandler(object): # ... and when new receipts happen elif stream_name == ReceiptsStream.NAME: - run_as_background_process( - "process_receipts_for_federation", self._on_new_receipts, rows - ) + await self._on_new_receipts(rows) # ... as well as device updates and messages elif stream_name == DeviceListsStream.NAME: From 0f05fd15304f1931ef167351de63cc8ffa1d3a98 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 3 Apr 2020 13:21:30 +0100 Subject: [PATCH 1311/1623] Reduce the number of calls to `resource.getrusage` (#7183) Let's just call `getrusage` once on each logcontext change, rather than twice. --- changelog.d/7183.misc | 1 + synapse/logging/context.py | 102 +++++++++++++++++++++++-------------- 2 files changed, 64 insertions(+), 39 deletions(-) create mode 100644 changelog.d/7183.misc diff --git a/changelog.d/7183.misc b/changelog.d/7183.misc new file mode 100644 index 0000000000..731f4dcb52 --- /dev/null +++ b/changelog.d/7183.misc @@ -0,0 +1 @@ +Clean up some LoggingContext code. diff --git a/synapse/logging/context.py b/synapse/logging/context.py index 3254d6a8df..a8f674d13d 100644 --- a/synapse/logging/context.py +++ b/synapse/logging/context.py @@ -51,7 +51,7 @@ try: is_thread_resource_usage_supported = True - def get_thread_resource_usage(): + def get_thread_resource_usage() -> "Optional[resource._RUsage]": return resource.getrusage(RUSAGE_THREAD) @@ -60,7 +60,7 @@ except Exception: # won't track resource usage. is_thread_resource_usage_supported = False - def get_thread_resource_usage(): + def get_thread_resource_usage() -> "Optional[resource._RUsage]": return None @@ -201,10 +201,10 @@ class _Sentinel(object): record["request"] = None record["scope"] = None - def start(self): + def start(self, rusage: "Optional[resource._RUsage]"): pass - def stop(self): + def stop(self, rusage: "Optional[resource._RUsage]"): pass def add_database_transaction(self, duration_sec): @@ -261,7 +261,7 @@ class LoggingContext(object): # The thread resource usage when the logcontext became active. None # if the context is not currently active. - self.usage_start = None + self.usage_start = None # type: Optional[resource._RUsage] self.main_thread = get_thread_id() self.request = None @@ -336,7 +336,17 @@ class LoggingContext(object): record["request"] = self.request record["scope"] = self.scope - def start(self) -> None: + def start(self, rusage: "Optional[resource._RUsage]") -> None: + """ + Record that this logcontext is currently running. + + This should not be called directly: use set_current_context + + Args: + rusage: the resources used by the current thread, at the point of + switching to this logcontext. May be None if this platform doesn't + support getrusuage. + """ if get_thread_id() != self.main_thread: logger.warning("Started logcontext %s on different thread", self) return @@ -349,36 +359,48 @@ class LoggingContext(object): if self.usage_start: logger.warning("Re-starting already-active log context %s", self) else: - self.usage_start = get_thread_resource_usage() + self.usage_start = rusage - def stop(self) -> None: - if get_thread_id() != self.main_thread: - logger.warning("Stopped logcontext %s on different thread", self) - return + def stop(self, rusage: "Optional[resource._RUsage]") -> None: + """ + Record that this logcontext is no longer running. - # When we stop, let's record the cpu used since we started - if not self.usage_start: - # Log a warning on platforms that support thread usage tracking - if is_thread_resource_usage_supported: + This should not be called directly: use set_current_context + + Args: + rusage: the resources used by the current thread, at the point of + switching away from this logcontext. May be None if this platform + doesn't support getrusuage. + """ + + try: + if get_thread_id() != self.main_thread: + logger.warning("Stopped logcontext %s on different thread", self) + return + + if not rusage: + return + + # Record the cpu used since we started + if not self.usage_start: logger.warning( - "Called stop on logcontext %s without calling start", self + "Called stop on logcontext %s without recording a start rusage", + self, ) - return + return - utime_delta, stime_delta = self._get_cputime() - self._resource_usage.ru_utime += utime_delta - self._resource_usage.ru_stime += stime_delta + utime_delta, stime_delta = self._get_cputime(rusage) + self._resource_usage.ru_utime += utime_delta + self._resource_usage.ru_stime += stime_delta - self.usage_start = None + # if we have a parent, pass our CPU usage stats on + if self.parent_context: + self.parent_context._resource_usage += self._resource_usage - # if we have a parent, pass our CPU usage stats on - if self.parent_context is not None and hasattr( - self.parent_context, "_resource_usage" - ): - self.parent_context._resource_usage += self._resource_usage - - # reset them in case we get entered again - self._resource_usage.reset() + # reset them in case we get entered again + self._resource_usage.reset() + finally: + self.usage_start = None def get_resource_usage(self) -> ContextResourceUsage: """Get resources used by this logcontext so far. @@ -394,24 +416,24 @@ class LoggingContext(object): # can include resource usage so far. is_main_thread = get_thread_id() == self.main_thread if self.usage_start and is_main_thread: - utime_delta, stime_delta = self._get_cputime() + rusage = get_thread_resource_usage() + assert rusage is not None + utime_delta, stime_delta = self._get_cputime(rusage) res.ru_utime += utime_delta res.ru_stime += stime_delta return res - def _get_cputime(self) -> Tuple[float, float]: - """Get the cpu usage time so far + def _get_cputime(self, current: "resource._RUsage") -> Tuple[float, float]: + """Get the cpu usage time between start() and the given rusage + + Args: + rusage: the current resource usage Returns: Tuple[float, float]: seconds in user mode, seconds in system mode """ assert self.usage_start is not None - current = get_thread_resource_usage() - - # Indicate to mypy that we know that self.usage_start is None. - assert self.usage_start is not None - utime_delta = current.ru_utime - self.usage_start.ru_utime stime_delta = current.ru_stime - self.usage_start.ru_stime @@ -547,9 +569,11 @@ def set_current_context(context: LoggingContextOrSentinel) -> LoggingContextOrSe current = current_context() if current is not context: - current.stop() + rusage = get_thread_resource_usage() + current.stop(rusage) _thread_local.current_context = context - context.start() + context.start(rusage) + return current From 07b88c546de1b24f5cbc9b4cb6da98400a8155af Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Fri, 3 Apr 2020 14:26:07 +0100 Subject: [PATCH 1312/1623] Convert http.HTTPStatus objects to their int equivalent (#7188) --- changelog.d/7188.misc | 1 + synapse/api/errors.py | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 changelog.d/7188.misc diff --git a/changelog.d/7188.misc b/changelog.d/7188.misc new file mode 100644 index 0000000000..f72955b95b --- /dev/null +++ b/changelog.d/7188.misc @@ -0,0 +1 @@ +Fix consistency of HTTP status codes reported in log lines. diff --git a/synapse/api/errors.py b/synapse/api/errors.py index 11da016ac5..d54dfb385d 100644 --- a/synapse/api/errors.py +++ b/synapse/api/errors.py @@ -86,7 +86,14 @@ class CodeMessageException(RuntimeError): def __init__(self, code, msg): super(CodeMessageException, self).__init__("%d: %s" % (code, msg)) - self.code = code + + # Some calls to this method pass instances of http.HTTPStatus for `code`. + # While HTTPStatus is a subclass of int, it has magic __str__ methods + # which emit `HTTPStatus.FORBIDDEN` when converted to a str, instead of `403`. + # This causes inconsistency in our log lines. + # + # To eliminate this behaviour, we convert them to their integer equivalents here. + self.code = int(code) self.msg = msg From 334bfdbc9088cfe2fbe43cfe1c349c27734bb341 Mon Sep 17 00:00:00 2001 From: Amber Brown Date: Sat, 4 Apr 2020 02:31:52 +1100 Subject: [PATCH 1313/1623] Add some benchmarks for LruCache (#6446) --- changelog.d/6446.misc | 1 + synmark/__main__.py | 16 ++++++++++++--- synmark/suites/__init__.py | 10 +++++++-- synmark/suites/lrucache.py | 34 +++++++++++++++++++++++++++++++ synmark/suites/lrucache_evict.py | 35 ++++++++++++++++++++++++++++++++ 5 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 changelog.d/6446.misc create mode 100644 synmark/suites/lrucache.py create mode 100644 synmark/suites/lrucache_evict.py diff --git a/changelog.d/6446.misc b/changelog.d/6446.misc new file mode 100644 index 0000000000..c42df16f1a --- /dev/null +++ b/changelog.d/6446.misc @@ -0,0 +1 @@ +Add benchmarks for LruCache. diff --git a/synmark/__main__.py b/synmark/__main__.py index ac59befbd4..17df9ddeb7 100644 --- a/synmark/__main__.py +++ b/synmark/__main__.py @@ -14,6 +14,7 @@ # limitations under the License. import sys +from argparse import REMAINDER from contextlib import redirect_stderr from io import StringIO @@ -21,7 +22,7 @@ import pyperf from synmark import make_reactor from synmark.suites import SUITES -from twisted.internet.defer import ensureDeferred +from twisted.internet.defer import Deferred, ensureDeferred from twisted.logger import globalLogBeginner, textFileLogObserver from twisted.python.failure import Failure @@ -40,7 +41,8 @@ def make_test(main): file_out = StringIO() with redirect_stderr(file_out): - d = ensureDeferred(main(reactor, loops)) + d = Deferred() + d.addCallback(lambda _: ensureDeferred(main(reactor, loops))) def on_done(_): if isinstance(_, Failure): @@ -50,6 +52,7 @@ def make_test(main): return _ d.addBoth(on_done) + reactor.callWhenRunning(lambda: d.callback(True)) reactor.run() return d.result @@ -62,11 +65,13 @@ if __name__ == "__main__": def add_cmdline_args(cmd, args): if args.log: cmd.extend(["--log"]) + cmd.extend(args.tests) runner = pyperf.Runner( - processes=3, min_time=2, show_name=True, add_cmdline_args=add_cmdline_args + processes=3, min_time=1.5, show_name=True, add_cmdline_args=add_cmdline_args ) runner.argparser.add_argument("--log", action="store_true") + runner.argparser.add_argument("tests", nargs=REMAINDER) runner.parse_args() orig_loops = runner.args.loops @@ -79,6 +84,11 @@ if __name__ == "__main__": ) setupdb() + if runner.args.tests: + SUITES = list( + filter(lambda x: x[0].__name__.split(".")[-1] in runner.args.tests, SUITES) + ) + for suite, loops in SUITES: if loops: runner.args.loops = loops diff --git a/synmark/suites/__init__.py b/synmark/suites/__init__.py index cfa3b0ba38..d8445fc3df 100644 --- a/synmark/suites/__init__.py +++ b/synmark/suites/__init__.py @@ -1,3 +1,9 @@ -from . import logging +from . import logging, lrucache, lrucache_evict -SUITES = [(logging, 1000), (logging, 10000), (logging, None)] +SUITES = [ + (logging, 1000), + (logging, 10000), + (logging, None), + (lrucache, None), + (lrucache_evict, None), +] diff --git a/synmark/suites/lrucache.py b/synmark/suites/lrucache.py new file mode 100644 index 0000000000..69ab042ccc --- /dev/null +++ b/synmark/suites/lrucache.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pyperf import perf_counter + +from synapse.util.caches.lrucache import LruCache + + +async def main(reactor, loops): + """ + Benchmark `loops` number of insertions into LruCache without eviction. + """ + cache = LruCache(loops) + + start = perf_counter() + + for i in range(loops): + cache[i] = True + + end = perf_counter() - start + + return end diff --git a/synmark/suites/lrucache_evict.py b/synmark/suites/lrucache_evict.py new file mode 100644 index 0000000000..532b1cc702 --- /dev/null +++ b/synmark/suites/lrucache_evict.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pyperf import perf_counter + +from synapse.util.caches.lrucache import LruCache + + +async def main(reactor, loops): + """ + Benchmark `loops` number of insertions into LruCache where half of them are + evicted. + """ + cache = LruCache(loops // 2) + + start = perf_counter() + + for i in range(loops): + cache[i] = True + + end = perf_counter() - start + + return end From b0db928c633ad2e225623cffb20293629c5d5a43 Mon Sep 17 00:00:00 2001 From: Martin Milata Date: Fri, 3 Apr 2020 17:57:34 +0200 Subject: [PATCH 1314/1623] Extend web_client_location to handle absolute URLs (#7006) Log warning when filesystem path is used. Signed-off-by: Martin Milata --- changelog.d/7006.feature | 1 + docs/sample_config.yaml | 11 ++++++++--- synapse/app/homeserver.py | 16 +++++++++++++--- synapse/config/server.py | 11 ++++++++--- 4 files changed, 30 insertions(+), 9 deletions(-) create mode 100644 changelog.d/7006.feature diff --git a/changelog.d/7006.feature b/changelog.d/7006.feature new file mode 100644 index 0000000000..d2ce9dbaca --- /dev/null +++ b/changelog.d/7006.feature @@ -0,0 +1 @@ +Extend the `web_client_location` option to accept an absolute URL to use as a redirect. Adds a warning when running the web client on the same hostname as homeserver. Contributed by Martin Milata. diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 6a770508f9..be742969cc 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -33,10 +33,15 @@ server_name: "SERVERNAME" # pid_file: DATADIR/homeserver.pid -# The path to the web client which will be served at /_matrix/client/ -# if 'webclient' is configured under the 'listeners' configuration. +# The absolute URL to the web client which /_matrix/client will redirect +# to if 'webclient' is configured under the 'listeners' configuration. # -#web_client_location: "/path/to/web/root" +# This option can be also set to the filesystem path to the web client +# which will be served at /_matrix/client/ if 'webclient' is configured +# under the 'listeners' configuration, however this is a security risk: +# https://github.com/matrix-org/synapse#security-note +# +#web_client_location: https://riot.example.com/ # The public-facing base URL that clients use to access this HS # (not including _matrix/...). This is the same URL a user would diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py index f2b56a636f..49df63acd0 100644 --- a/synapse/app/homeserver.py +++ b/synapse/app/homeserver.py @@ -241,16 +241,26 @@ class SynapseHomeServer(HomeServer): resources[SERVER_KEY_V2_PREFIX] = KeyApiV2Resource(self) if name == "webclient": - webclient_path = self.get_config().web_client_location + webclient_loc = self.get_config().web_client_location - if webclient_path is None: + if webclient_loc is None: logger.warning( "Not enabling webclient resource, as web_client_location is unset." ) + elif webclient_loc.startswith("http://") or webclient_loc.startswith( + "https://" + ): + resources[WEB_CLIENT_PREFIX] = RootRedirect(webclient_loc) else: + logger.warning( + "Running webclient on the same domain is not recommended: " + "https://github.com/matrix-org/synapse#security-note - " + "after you move webclient to different host you can set " + "web_client_location to its full URL to enable redirection." + ) # GZip is disabled here due to # https://twistedmatrix.com/trac/ticket/7678 - resources[WEB_CLIENT_PREFIX] = File(webclient_path) + resources[WEB_CLIENT_PREFIX] = File(webclient_loc) if name == "metrics" and self.get_config().enable_metrics: resources[METRICS_PREFIX] = MetricsResource(RegistryProxy) diff --git a/synapse/config/server.py b/synapse/config/server.py index 7525765fee..28e2a031fb 100644 --- a/synapse/config/server.py +++ b/synapse/config/server.py @@ -604,10 +604,15 @@ class ServerConfig(Config): # pid_file: %(pid_file)s - # The path to the web client which will be served at /_matrix/client/ - # if 'webclient' is configured under the 'listeners' configuration. + # The absolute URL to the web client which /_matrix/client will redirect + # to if 'webclient' is configured under the 'listeners' configuration. # - #web_client_location: "/path/to/web/root" + # This option can be also set to the filesystem path to the web client + # which will be served at /_matrix/client/ if 'webclient' is configured + # under the 'listeners' configuration, however this is a security risk: + # https://github.com/matrix-org/synapse#security-note + # + #web_client_location: https://riot.example.com/ # The public-facing base URL that clients use to access this HS # (not including _matrix/...). This is the same URL a user would From 694d8bed0e56366f080a49db0f930d635ca6cdf4 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Fri, 3 Apr 2020 15:35:05 -0400 Subject: [PATCH 1315/1623] Support CAS in UI Auth flows. (#7186) --- changelog.d/7186.feature | 1 + synapse/handlers/auth.py | 4 +- synapse/handlers/cas_handler.py | 173 +++++++++++++++------------ synapse/rest/client/v1/login.py | 20 +++- synapse/rest/client/v2_alpha/auth.py | 28 ++++- 5 files changed, 137 insertions(+), 89 deletions(-) create mode 100644 changelog.d/7186.feature diff --git a/changelog.d/7186.feature b/changelog.d/7186.feature new file mode 100644 index 0000000000..01057aa396 --- /dev/null +++ b/changelog.d/7186.feature @@ -0,0 +1 @@ +Support SSO in the user interactive authentication workflow. diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index 7c09d15a72..892adb00b9 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -116,7 +116,7 @@ class AuthHandler(BaseHandler): self.hs = hs # FIXME better possibility to access registrationHandler later? self.macaroon_gen = hs.get_macaroon_generator() self._password_enabled = hs.config.password_enabled - self._saml2_enabled = hs.config.saml2_enabled + self._sso_enabled = hs.config.saml2_enabled or hs.config.cas_enabled # we keep this as a list despite the O(N^2) implication so that we can # keep PASSWORD first and avoid confusing clients which pick the first @@ -136,7 +136,7 @@ class AuthHandler(BaseHandler): # necessarily identical. Login types have SSO (and other login types) # added in the rest layer, see synapse.rest.client.v1.login.LoginRestServerlet.on_GET. ui_auth_types = login_types.copy() - if self._saml2_enabled: + if self._sso_enabled: ui_auth_types.append(LoginType.SSO) self._supported_ui_auth_types = ui_auth_types diff --git a/synapse/handlers/cas_handler.py b/synapse/handlers/cas_handler.py index f8dc274b78..d977badf35 100644 --- a/synapse/handlers/cas_handler.py +++ b/synapse/handlers/cas_handler.py @@ -15,7 +15,7 @@ import logging import xml.etree.ElementTree as ET -from typing import AnyStr, Dict, Optional, Tuple +from typing import Dict, Optional, Tuple from six.moves import urllib @@ -48,26 +48,47 @@ class CasHandler: self._http_client = hs.get_proxied_http_client() - def _build_service_param(self, client_redirect_url: AnyStr) -> str: + def _build_service_param(self, args: Dict[str, str]) -> str: + """ + Generates a value to use as the "service" parameter when redirecting or + querying the CAS service. + + Args: + args: Additional arguments to include in the final redirect URL. + + Returns: + The URL to use as a "service" parameter. + """ return "%s%s?%s" % ( self._cas_service_url, "/_matrix/client/r0/login/cas/ticket", - urllib.parse.urlencode({"redirectUrl": client_redirect_url}), + urllib.parse.urlencode(args), ) - async def _handle_cas_response( - self, request: SynapseRequest, cas_response_body: str, client_redirect_url: str - ) -> None: + async def _validate_ticket( + self, ticket: str, service_args: Dict[str, str] + ) -> Tuple[str, Optional[str]]: """ - Retrieves the user and display name from the CAS response and continues with the authentication. + Validate a CAS ticket with the server, parse the response, and return the user and display name. Args: - request: The original client request. - cas_response_body: The response from the CAS server. - client_redirect_url: The URl to redirect the client to when - everything is done. + ticket: The CAS ticket from the client. + service_args: Additional arguments to include in the service URL. + Should be the same as those passed to `get_redirect_url`. """ - user, attributes = self._parse_cas_response(cas_response_body) + uri = self._cas_server_url + "/proxyValidate" + args = { + "ticket": ticket, + "service": self._build_service_param(service_args), + } + try: + body = await self._http_client.get_raw(uri, args) + except PartialDownloadError as pde: + # Twisted raises this error if the connection is closed, + # even if that's being used old-http style to signal end-of-data + body = pde.response + + user, attributes = self._parse_cas_response(body) displayname = attributes.pop(self._cas_displayname_attribute, None) for required_attribute, required_value in self._cas_required_attributes.items(): @@ -82,7 +103,7 @@ class CasHandler: if required_value != actual_value: raise LoginError(401, "Unauthorized", errcode=Codes.UNAUTHORIZED) - await self._on_successful_auth(user, request, client_redirect_url, displayname) + return user, displayname def _parse_cas_response( self, cas_response_body: str @@ -127,78 +148,74 @@ class CasHandler: ) return user, attributes - async def _on_successful_auth( - self, - username: str, - request: SynapseRequest, - client_redirect_url: str, - user_display_name: Optional[str] = None, - ) -> None: - """Called once the user has successfully authenticated with the SSO. - - Registers the user if necessary, and then returns a redirect (with - a login token) to the client. + def get_redirect_url(self, service_args: Dict[str, str]) -> str: + """ + Generates a URL for the CAS server where the client should be redirected. Args: - username: the remote user id. We'll map this onto - something sane for a MXID localpath. + service_args: Additional arguments to include in the final redirect URL. - request: the incoming request from the browser. We'll - respond to it with a redirect. - - client_redirect_url: the redirect_url the client gave us when - it first started the process. - - user_display_name: if set, and we have to register a new user, - we will set their displayname to this. + Returns: + The URL to redirect the client to. """ + args = urllib.parse.urlencode( + {"service": self._build_service_param(service_args)} + ) + + return "%s/login?%s" % (self._cas_server_url, args) + + async def handle_ticket( + self, + request: SynapseRequest, + ticket: str, + client_redirect_url: Optional[str], + session: Optional[str], + ) -> None: + """ + Called once the user has successfully authenticated with the SSO. + Validates a CAS ticket sent by the client and completes the auth process. + + If the user interactive authentication session is provided, marks the + UI Auth session as complete, then returns an HTML page notifying the + user they are done. + + Otherwise, this registers the user if necessary, and then returns a + redirect (with a login token) to the client. + + Args: + request: the incoming request from the browser. We'll + respond to it with a redirect or an HTML page. + + ticket: The CAS ticket provided by the client. + + client_redirect_url: the redirectUrl parameter from the `/cas/ticket` HTTP request, if given. + This should be the same as the redirectUrl from the original `/login/sso/redirect` request. + + session: The session parameter from the `/cas/ticket` HTTP request, if given. + This should be the UI Auth session id. + """ + args = {} + if client_redirect_url: + args["redirectUrl"] = client_redirect_url + if session: + args["session"] = session + username, user_display_name = await self._validate_ticket(ticket, args) + localpart = map_username_to_mxid_localpart(username) user_id = UserID(localpart, self._hostname).to_string() registered_user_id = await self._auth_handler.check_user_exists(user_id) - if not registered_user_id: - registered_user_id = await self._registration_handler.register_user( - localpart=localpart, default_display_name=user_display_name + + if session: + self._auth_handler.complete_sso_ui_auth( + registered_user_id, session, request, ) - self._auth_handler.complete_sso_login( - registered_user_id, request, client_redirect_url - ) + else: + if not registered_user_id: + registered_user_id = await self._registration_handler.register_user( + localpart=localpart, default_display_name=user_display_name + ) - def handle_redirect_request(self, client_redirect_url: bytes) -> bytes: - """ - Generates a URL to the CAS server where the client should be redirected. - - Args: - client_redirect_url: The final URL the client should go to after the - user has negotiated SSO. - - Returns: - The URL to redirect to. - """ - args = urllib.parse.urlencode( - {"service": self._build_service_param(client_redirect_url)} - ) - - return ("%s/login?%s" % (self._cas_server_url, args)).encode("ascii") - - async def handle_ticket_request( - self, request: SynapseRequest, client_redirect_url: str, ticket: str - ) -> None: - """ - Validates a CAS ticket sent by the client for login/registration. - - On a successful request, writes a redirect to the request. - """ - uri = self._cas_server_url + "/proxyValidate" - args = { - "ticket": ticket, - "service": self._build_service_param(client_redirect_url), - } - try: - body = await self._http_client.get_raw(uri, args) - except PartialDownloadError as pde: - # Twisted raises this error if the connection is closed, - # even if that's being used old-http style to signal end-of-data - body = pde.response - - await self._handle_cas_response(request, body, client_redirect_url) + self._auth_handler.complete_sso_login( + registered_user_id, request, client_redirect_url + ) diff --git a/synapse/rest/client/v1/login.py b/synapse/rest/client/v1/login.py index 59593cbf6e..4de2f97d06 100644 --- a/synapse/rest/client/v1/login.py +++ b/synapse/rest/client/v1/login.py @@ -425,7 +425,9 @@ class CasRedirectServlet(BaseSSORedirectServlet): self._cas_handler = hs.get_cas_handler() def get_sso_url(self, client_redirect_url: bytes) -> bytes: - return self._cas_handler.handle_redirect_request(client_redirect_url) + return self._cas_handler.get_redirect_url( + {"redirectUrl": client_redirect_url} + ).encode("ascii") class CasTicketServlet(RestServlet): @@ -436,10 +438,20 @@ class CasTicketServlet(RestServlet): self._cas_handler = hs.get_cas_handler() async def on_GET(self, request: SynapseRequest) -> None: - client_redirect_url = parse_string(request, "redirectUrl", required=True) + client_redirect_url = parse_string(request, "redirectUrl") ticket = parse_string(request, "ticket", required=True) - await self._cas_handler.handle_ticket_request( - request, client_redirect_url, ticket + + # Maybe get a session ID (if this ticket is from user interactive + # authentication). + session = parse_string(request, "session") + + # Either client_redirect_url or session must be provided. + if not client_redirect_url and not session: + message = "Missing string query parameter redirectUrl or session" + raise SynapseError(400, message, errcode=Codes.MISSING_PARAM) + + await self._cas_handler.handle_ticket( + request, ticket, client_redirect_url, session ) diff --git a/synapse/rest/client/v2_alpha/auth.py b/synapse/rest/client/v2_alpha/auth.py index 1787562b90..13f9604407 100644 --- a/synapse/rest/client/v2_alpha/auth.py +++ b/synapse/rest/client/v2_alpha/auth.py @@ -111,6 +111,11 @@ class AuthRestServlet(RestServlet): self._saml_enabled = hs.config.saml2_enabled if self._saml_enabled: self._saml_handler = hs.get_saml_handler() + self._cas_enabled = hs.config.cas_enabled + if self._cas_enabled: + self._cas_handler = hs.get_cas_handler() + self._cas_server_url = hs.config.cas_server_url + self._cas_service_url = hs.config.cas_service_url def on_GET(self, request, stagetype): session = parse_string(request, "session") @@ -133,14 +138,27 @@ class AuthRestServlet(RestServlet): % (CLIENT_API_PREFIX, LoginType.TERMS), } - elif stagetype == LoginType.SSO and self._saml_enabled: + elif stagetype == LoginType.SSO: # Display a confirmation page which prompts the user to # re-authenticate with their SSO provider. - client_redirect_url = "" - sso_redirect_url = self._saml_handler.handle_redirect_request( - client_redirect_url, session - ) + if self._cas_enabled: + # Generate a request to CAS that redirects back to an endpoint + # to verify the successful authentication. + sso_redirect_url = self._cas_handler.get_redirect_url( + {"session": session}, + ) + + elif self._saml_enabled: + client_redirect_url = "" + sso_redirect_url = self._saml_handler.handle_redirect_request( + client_redirect_url, session + ) + + else: + raise SynapseError(400, "Homeserver not configured for SSO.") + html = self.auth_handler.start_sso_ui_auth(sso_redirect_url, session) + else: raise SynapseError(404, "Unknown auth stage type") From d73bf18d13031d9f9c0375b83f2cc5ff6f415251 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Sat, 4 Apr 2020 17:27:45 +0200 Subject: [PATCH 1316/1623] Server notices: Dissociate room creation/lookup from invite (#7199) Fixes #6815 Before figuring out whether we should alert a user on MAU, we call get_notice_room_for_user to get some info on the existing server notices room for this user. This function, if the room doesn't exist, creates it and invites the user in it. This means that, if we decide later that no server notice is needed, the user gets invited in a room with no message in it. This happens at every restart of the server, since the room ID returned by get_notice_room_for_user is cached. This PR fixes that by moving the inviting bit to a dedicated function, that's only called when the server actually needs to send a notice to the user. A potential issue with this approach is that the room that's created by get_notice_room_for_user doesn't match how that same function looks for an existing room (i.e. it creates a room that doesn't have an invite or a join for the current user in it, so it could lead to a new room being created each time a user syncs), but I'm not sure this is a problem given it's cached until the server restarts, so that function won't run very often. It also renames get_notice_room_for_user into get_or_create_notice_room_for_user to make what it does clearer. --- changelog.d/7199.bugfix | 1 + .../resource_limits_server_notices.py | 4 +- .../server_notices/server_notices_manager.py | 51 ++++++-- .../test_resource_limits_server_notices.py | 120 ++++++++++++++++-- 4 files changed, 154 insertions(+), 22 deletions(-) create mode 100644 changelog.d/7199.bugfix diff --git a/changelog.d/7199.bugfix b/changelog.d/7199.bugfix new file mode 100644 index 0000000000..b234163ea8 --- /dev/null +++ b/changelog.d/7199.bugfix @@ -0,0 +1 @@ +Fix a bug that could cause a user to be invited to a server notices (aka System Alerts) room without any notice being sent. diff --git a/synapse/server_notices/resource_limits_server_notices.py b/synapse/server_notices/resource_limits_server_notices.py index 9fae2e0afe..ce4a828894 100644 --- a/synapse/server_notices/resource_limits_server_notices.py +++ b/synapse/server_notices/resource_limits_server_notices.py @@ -80,7 +80,9 @@ class ResourceLimitsServerNotices(object): # In practice, not sure we can ever get here return - room_id = yield self._server_notices_manager.get_notice_room_for_user(user_id) + room_id = yield self._server_notices_manager.get_or_create_notice_room_for_user( + user_id + ) if not room_id: logger.warning("Failed to get server notices room") diff --git a/synapse/server_notices/server_notices_manager.py b/synapse/server_notices/server_notices_manager.py index f7432c8d2f..bf0943f265 100644 --- a/synapse/server_notices/server_notices_manager.py +++ b/synapse/server_notices/server_notices_manager.py @@ -17,7 +17,7 @@ import logging from twisted.internet import defer from synapse.api.constants import EventTypes, Membership, RoomCreationPreset -from synapse.types import create_requester +from synapse.types import UserID, create_requester from synapse.util.caches.descriptors import cachedInlineCallbacks logger = logging.getLogger(__name__) @@ -36,10 +36,12 @@ class ServerNoticesManager(object): self._store = hs.get_datastore() self._config = hs.config self._room_creation_handler = hs.get_room_creation_handler() + self._room_member_handler = hs.get_room_member_handler() self._event_creation_handler = hs.get_event_creation_handler() self._is_mine_id = hs.is_mine_id self._notifier = hs.get_notifier() + self.server_notices_mxid = self._config.server_notices_mxid def is_enabled(self): """Checks if server notices are enabled on this server. @@ -66,7 +68,8 @@ class ServerNoticesManager(object): Returns: Deferred[FrozenEvent] """ - room_id = yield self.get_notice_room_for_user(user_id) + room_id = yield self.get_or_create_notice_room_for_user(user_id) + yield self.maybe_invite_user_to_room(user_id, room_id) system_mxid = self._config.server_notices_mxid requester = create_requester(system_mxid) @@ -89,10 +92,11 @@ class ServerNoticesManager(object): return res @cachedInlineCallbacks() - def get_notice_room_for_user(self, user_id): + def get_or_create_notice_room_for_user(self, user_id): """Get the room for notices for a given user - If we have not yet created a notice room for this user, create it + If we have not yet created a notice room for this user, create it, but don't + invite the user to it. Args: user_id (str): complete user id for the user we want a room for @@ -108,7 +112,6 @@ class ServerNoticesManager(object): rooms = yield self._store.get_rooms_for_local_user_where_membership_is( user_id, [Membership.INVITE, Membership.JOIN] ) - system_mxid = self._config.server_notices_mxid for room in rooms: # it's worth noting that there is an asymmetry here in that we # expect the user to be invited or joined, but the system user must @@ -116,10 +119,14 @@ class ServerNoticesManager(object): # manages to invite the system user to a room, that doesn't make it # the server notices room. user_ids = yield self._store.get_users_in_room(room.room_id) - if system_mxid in user_ids: + if self.server_notices_mxid in user_ids: # we found a room which our user shares with the system notice # user - logger.info("Using room %s", room.room_id) + logger.info( + "Using existing server notices room %s for user %s", + room.room_id, + user_id, + ) return room.room_id # apparently no existing notice room: create a new one @@ -138,14 +145,13 @@ class ServerNoticesManager(object): "avatar_url": self._config.server_notices_mxid_avatar_url, } - requester = create_requester(system_mxid) + requester = create_requester(self.server_notices_mxid) info = yield self._room_creation_handler.create_room( requester, config={ "preset": RoomCreationPreset.PRIVATE_CHAT, "name": self._config.server_notices_room_name, "power_level_content_override": {"users_default": -10}, - "invite": (user_id,), }, ratelimit=False, creator_join_profile=join_profile, @@ -159,3 +165,30 @@ class ServerNoticesManager(object): logger.info("Created server notices room %s for %s", room_id, user_id) return room_id + + @defer.inlineCallbacks + def maybe_invite_user_to_room(self, user_id: str, room_id: str): + """Invite the given user to the given server room, unless the user has already + joined or been invited to it. + + Args: + user_id: The ID of the user to invite. + room_id: The ID of the room to invite the user to. + """ + requester = create_requester(self.server_notices_mxid) + + # Check whether the user has already joined or been invited to this room. If + # that's the case, there is no need to re-invite them. + joined_rooms = yield self._store.get_rooms_for_local_user_where_membership_is( + user_id, [Membership.INVITE, Membership.JOIN] + ) + for room in joined_rooms: + if room.room_id == room_id: + return + + yield self._room_member_handler.update_membership( + requester=requester, + target=UserID.from_string(user_id), + room_id=room_id, + action="invite", + ) diff --git a/tests/server_notices/test_resource_limits_server_notices.py b/tests/server_notices/test_resource_limits_server_notices.py index 0d27b92a86..93eb053b8c 100644 --- a/tests/server_notices/test_resource_limits_server_notices.py +++ b/tests/server_notices/test_resource_limits_server_notices.py @@ -19,6 +19,9 @@ from twisted.internet import defer from synapse.api.constants import EventTypes, LimitBlockingTypes, ServerNoticeMsgType from synapse.api.errors import ResourceLimitError +from synapse.rest import admin +from synapse.rest.client.v1 import login, room +from synapse.rest.client.v2_alpha import sync from synapse.server_notices.resource_limits_server_notices import ( ResourceLimitsServerNotices, ) @@ -67,7 +70,7 @@ class TestResourceLimitsServerNotices(unittest.HomeserverTestCase): # self.server_notices_mxid_avatar_url = None # self.server_notices_room_name = "Server Notices" - self._rlsn._server_notices_manager.get_notice_room_for_user = Mock( + self._rlsn._server_notices_manager.get_or_create_notice_room_for_user = Mock( returnValue="" ) self._rlsn._store.add_tag_to_room = Mock() @@ -215,6 +218,26 @@ class TestResourceLimitsServerNotices(unittest.HomeserverTestCase): class TestResourceLimitsServerNoticesWithRealRooms(unittest.HomeserverTestCase): + servlets = [ + admin.register_servlets, + login.register_servlets, + room.register_servlets, + sync.register_servlets, + ] + + def default_config(self): + c = super().default_config() + c["server_notices"] = { + "system_mxid_localpart": "server", + "system_mxid_display_name": None, + "system_mxid_avatar_url": None, + "room_name": "Test Server Notice Room", + } + c["limit_usage_by_mau"] = True + c["max_mau_value"] = 5 + c["admin_contact"] = "mailto:user@test.com" + return c + def prepare(self, reactor, clock, hs): self.store = self.hs.get_datastore() self.server_notices_sender = self.hs.get_server_notices_sender() @@ -228,18 +251,8 @@ class TestResourceLimitsServerNoticesWithRealRooms(unittest.HomeserverTestCase): if not isinstance(self._rlsn, ResourceLimitsServerNotices): raise Exception("Failed to find reference to ResourceLimitsServerNotices") - self.hs.config.limit_usage_by_mau = True - self.hs.config.hs_disabled = False - self.hs.config.max_mau_value = 5 - self.hs.config.server_notices_mxid = "@server:test" - self.hs.config.server_notices_mxid_display_name = None - self.hs.config.server_notices_mxid_avatar_url = None - self.hs.config.server_notices_room_name = "Test Server Notice Room" - self.user_id = "@user_id:test" - self.hs.config.admin_contact = "mailto:user@test.com" - def test_server_notice_only_sent_once(self): self.store.get_monthly_active_count = Mock(return_value=1000) @@ -253,7 +266,7 @@ class TestResourceLimitsServerNoticesWithRealRooms(unittest.HomeserverTestCase): # Now lets get the last load of messages in the service notice room and # check that there is only one server notice room_id = self.get_success( - self.server_notices_manager.get_notice_room_for_user(self.user_id) + self.server_notices_manager.get_or_create_notice_room_for_user(self.user_id) ) token = self.get_success(self.event_source.get_current_token()) @@ -273,3 +286,86 @@ class TestResourceLimitsServerNoticesWithRealRooms(unittest.HomeserverTestCase): count += 1 self.assertEqual(count, 1) + + def test_no_invite_without_notice(self): + """Tests that a user doesn't get invited to a server notices room without a + server notice being sent. + + The scenario for this test is a single user on a server where the MAU limit + hasn't been reached (since it's the only user and the limit is 5), so users + shouldn't receive a server notice. + """ + self.register_user("user", "password") + tok = self.login("user", "password") + + request, channel = self.make_request("GET", "/sync?timeout=0", access_token=tok) + self.render(request) + + invites = channel.json_body["rooms"]["invite"] + self.assertEqual(len(invites), 0, invites) + + def test_invite_with_notice(self): + """Tests that, if the MAU limit is hit, the server notices user invites each user + to a room in which it has sent a notice. + """ + user_id, tok, room_id = self._trigger_notice_and_join() + + # Sync again to retrieve the events in the room, so we can check whether this + # room has a notice in it. + request, channel = self.make_request("GET", "/sync?timeout=0", access_token=tok) + self.render(request) + + # Scan the events in the room to search for a message from the server notices + # user. + events = channel.json_body["rooms"]["join"][room_id]["timeline"]["events"] + notice_in_room = False + for event in events: + if ( + event["type"] == EventTypes.Message + and event["sender"] == self.hs.config.server_notices_mxid + ): + notice_in_room = True + + self.assertTrue(notice_in_room, "No server notice in room") + + def _trigger_notice_and_join(self): + """Creates enough active users to hit the MAU limit and trigger a system notice + about it, then joins the system notices room with one of the users created. + + Returns: + user_id (str): The ID of the user that joined the room. + tok (str): The access token of the user that joined the room. + room_id (str): The ID of the room that's been joined. + """ + user_id = None + tok = None + invites = [] + + # Register as many users as the MAU limit allows. + for i in range(self.hs.config.max_mau_value): + localpart = "user%d" % i + user_id = self.register_user(localpart, "password") + tok = self.login(localpart, "password") + + # Sync with the user's token to mark the user as active. + request, channel = self.make_request( + "GET", "/sync?timeout=0", access_token=tok, + ) + self.render(request) + + # Also retrieves the list of invites for this user. We don't care about that + # one except if we're processing the last user, which should have received an + # invite to a room with a server notice about the MAU limit being reached. + # We could also pick another user and sync with it, which would return an + # invite to a system notices room, but it doesn't matter which user we're + # using so we use the last one because it saves us an extra sync. + invites = channel.json_body["rooms"]["invite"] + + # Make sure we have an invite to process. + self.assertEqual(len(invites), 1, invites) + + # Join the room. + room_id = list(invites.keys())[0] + self.helper.join(room=room_id, user=user_id, tok=tok) + + return user_id, tok, room_id From 5016b162fcf0372fe35404c64f80aeaf21461f31 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 6 Apr 2020 09:58:42 +0100 Subject: [PATCH 1317/1623] Move client command handling out of TCP protocol (#7185) The aim here is to move the command handling out of the TCP protocol classes and to also merge the client and server command handling (so that we can reuse them for redis protocol). This PR simply moves the client paths to the new `ReplicationCommandHandler`, a future PR will move the server paths too. --- changelog.d/7185.misc | 1 + synapse/app/admin_cmd.py | 12 - synapse/app/generic_worker.py | 9 +- synapse/replication/tcp/__init__.py | 30 ++- synapse/replication/tcp/client.py | 177 ++---------- synapse/replication/tcp/handler.py | 252 ++++++++++++++++++ synapse/replication/tcp/protocol.py | 201 +++----------- synapse/server.py | 8 +- synapse/server.pyi | 7 +- tests/replication/slave/storage/_base.py | 15 +- tests/replication/tcp/streams/_base.py | 38 ++- .../replication/tcp/streams/test_receipts.py | 1 - 12 files changed, 379 insertions(+), 372 deletions(-) create mode 100644 changelog.d/7185.misc create mode 100644 synapse/replication/tcp/handler.py diff --git a/changelog.d/7185.misc b/changelog.d/7185.misc new file mode 100644 index 0000000000..deb9ca7021 --- /dev/null +++ b/changelog.d/7185.misc @@ -0,0 +1 @@ +Move client command handling out of TCP protocol. diff --git a/synapse/app/admin_cmd.py b/synapse/app/admin_cmd.py index 1c7c6ec0c8..a37818fe9a 100644 --- a/synapse/app/admin_cmd.py +++ b/synapse/app/admin_cmd.py @@ -43,7 +43,6 @@ from synapse.replication.slave.storage.push_rule import SlavedPushRuleStore from synapse.replication.slave.storage.receipts import SlavedReceiptsStore from synapse.replication.slave.storage.registration import SlavedRegistrationStore from synapse.replication.slave.storage.room import RoomStore -from synapse.replication.tcp.client import ReplicationClientHandler from synapse.server import HomeServer from synapse.util.logcontext import LoggingContext from synapse.util.versionstring import get_version_string @@ -79,17 +78,6 @@ class AdminCmdServer(HomeServer): def start_listening(self, listeners): pass - def build_tcp_replication(self): - return AdminCmdReplicationHandler(self) - - -class AdminCmdReplicationHandler(ReplicationClientHandler): - async def on_rdata(self, stream_name, token, rows): - pass - - def get_streams_to_replicate(self): - return {} - @defer.inlineCallbacks def export_data_command(hs, args): diff --git a/synapse/app/generic_worker.py b/synapse/app/generic_worker.py index 174bef360f..dcd0709a02 100644 --- a/synapse/app/generic_worker.py +++ b/synapse/app/generic_worker.py @@ -64,7 +64,7 @@ from synapse.replication.slave.storage.receipts import SlavedReceiptsStore from synapse.replication.slave.storage.registration import SlavedRegistrationStore from synapse.replication.slave.storage.room import RoomStore from synapse.replication.slave.storage.transactions import SlavedTransactionStore -from synapse.replication.tcp.client import ReplicationClientHandler +from synapse.replication.tcp.client import ReplicationDataHandler from synapse.replication.tcp.commands import ClearUserSyncsCommand from synapse.replication.tcp.streams import ( AccountDataStream, @@ -603,7 +603,7 @@ class GenericWorkerServer(HomeServer): def remove_pusher(self, app_id, push_key, user_id): self.get_tcp_replication().send_remove_pusher(app_id, push_key, user_id) - def build_tcp_replication(self): + def build_replication_data_handler(self): return GenericWorkerReplicationHandler(self) def build_presence_handler(self): @@ -613,7 +613,7 @@ class GenericWorkerServer(HomeServer): return GenericWorkerTyping(self) -class GenericWorkerReplicationHandler(ReplicationClientHandler): +class GenericWorkerReplicationHandler(ReplicationDataHandler): def __init__(self, hs): super(GenericWorkerReplicationHandler, self).__init__(hs.get_datastore()) @@ -644,9 +644,6 @@ class GenericWorkerReplicationHandler(ReplicationClientHandler): args.update(self.send_handler.stream_positions()) return args - def get_currently_syncing_users(self): - return self.presence_handler.get_currently_syncing_users() - async def process_and_notify(self, stream_name, token, rows): try: if self.send_handler: diff --git a/synapse/replication/tcp/__init__.py b/synapse/replication/tcp/__init__.py index 81c2ea7ee9..523a1358d4 100644 --- a/synapse/replication/tcp/__init__.py +++ b/synapse/replication/tcp/__init__.py @@ -20,11 +20,31 @@ Further details can be found in docs/tcp_replication.rst Structure of the module: - * client.py - the client classes used for workers to connect to master + * handler.py - the classes used to handle sending/receiving commands to + replication * command.py - the definitions of all the valid commands - * protocol.py - contains bot the client and server protocol implementations, - these should not be used directly - * resource.py - the server classes that accepts and handle client connections - * streams.py - the definitons of all the valid streams + * protocol.py - the TCP protocol classes + * resource.py - handles streaming stream updates to replications + * streams/ - the definitons of all the valid streams + +The general interaction of the classes are: + + +---------------------+ + | ReplicationStreamer | + +---------------------+ + | + v + +---------------------------+ +----------------------+ + | ReplicationCommandHandler |---->|ReplicationDataHandler| + +---------------------------+ +----------------------+ + | ^ + v | + +-------------+ + | Protocols | + | (TCP/redis) | + +-------------+ + +Where the ReplicationDataHandler (or subclasses) handles incoming stream +updates. """ diff --git a/synapse/replication/tcp/client.py b/synapse/replication/tcp/client.py index e86d9805f1..700ae79158 100644 --- a/synapse/replication/tcp/client.py +++ b/synapse/replication/tcp/client.py @@ -16,26 +16,16 @@ """ import logging -from typing import Dict, List, Optional +from typing import TYPE_CHECKING, Dict -from twisted.internet import defer from twisted.internet.protocol import ReconnectingClientFactory from synapse.replication.slave.storage._base import BaseSlavedStore -from synapse.replication.tcp.protocol import ( - AbstractReplicationClientHandler, - ClientReplicationStreamProtocol, -) +from synapse.replication.tcp.protocol import ClientReplicationStreamProtocol -from .commands import ( - Command, - FederationAckCommand, - InvalidateCacheCommand, - RemoteServerUpCommand, - RemovePusherCommand, - UserIpCommand, - UserSyncCommand, -) +if TYPE_CHECKING: + from synapse.server import HomeServer + from synapse.replication.tcp.handler import ReplicationCommandHandler logger = logging.getLogger(__name__) @@ -44,16 +34,20 @@ class ReplicationClientFactory(ReconnectingClientFactory): """Factory for building connections to the master. Will reconnect if the connection is lost. - Accepts a handler that will be called when new data is available or data - is required. + Accepts a handler that is passed to `ClientReplicationStreamProtocol`. """ initialDelay = 0.1 maxDelay = 1 # Try at least once every N seconds - def __init__(self, hs, client_name, handler: AbstractReplicationClientHandler): + def __init__( + self, + hs: "HomeServer", + client_name: str, + command_handler: "ReplicationCommandHandler", + ): self.client_name = client_name - self.handler = handler + self.command_handler = command_handler self.server_name = hs.config.server_name self.hs = hs self._clock = hs.get_clock() # As self.clock is defined in super class @@ -66,7 +60,11 @@ class ReplicationClientFactory(ReconnectingClientFactory): def buildProtocol(self, addr): logger.info("Connected to replication: %r", addr) return ClientReplicationStreamProtocol( - self.hs, self.client_name, self.server_name, self._clock, self.handler, + self.hs, + self.client_name, + self.server_name, + self._clock, + self.command_handler, ) def clientConnectionLost(self, connector, reason): @@ -78,41 +76,17 @@ class ReplicationClientFactory(ReconnectingClientFactory): ReconnectingClientFactory.clientConnectionFailed(self, connector, reason) -class ReplicationClientHandler(AbstractReplicationClientHandler): - """A base handler that can be passed to the ReplicationClientFactory. +class ReplicationDataHandler: + """Handles incoming stream updates from replication. - By default proxies incoming replication data to the SlaveStore. + This instance notifies the slave data store about updates. Can be subclassed + to handle updates in additional ways. """ def __init__(self, store: BaseSlavedStore): self.store = store - # The current connection. None if we are currently (re)connecting - self.connection = None - - # Any pending commands to be sent once a new connection has been - # established - self.pending_commands = [] # type: List[Command] - - # Map from string -> deferred, to wake up when receiveing a SYNC with - # the given string. - # Used for tests. - self.awaiting_syncs = {} # type: Dict[str, defer.Deferred] - - # The factory used to create connections. - self.factory = None # type: Optional[ReplicationClientFactory] - - def start_replication(self, hs): - """Helper method to start a replication connection to the remote server - using TCP. - """ - client_name = hs.config.worker_name - self.factory = ReplicationClientFactory(hs, client_name, self) - host = hs.config.worker_replication_host - port = hs.config.worker_replication_port - hs.get_reactor().connectTCP(host, port, self.factory) - - async def on_rdata(self, stream_name, token, rows): + async def on_rdata(self, stream_name: str, token: int, rows: list): """Called to handle a batch of replication data with a given stream token. By default this just pokes the slave store. Can be overridden in subclasses to @@ -124,30 +98,8 @@ class ReplicationClientHandler(AbstractReplicationClientHandler): rows (list): a list of Stream.ROW_TYPE objects as returned by Stream.parse_row. """ - logger.debug("Received rdata %s -> %s", stream_name, token) self.store.process_replication_rows(stream_name, token, rows) - async def on_position(self, stream_name, token): - """Called when we get new position data. By default this just pokes - the slave store. - - Can be overriden in subclasses to handle more. - """ - self.store.process_replication_rows(stream_name, token, []) - - def on_sync(self, data): - """When we received a SYNC we wake up any deferreds that were waiting - for the sync with the given data. - - Used by tests. - """ - d = self.awaiting_syncs.pop(data, None) - if d: - d.callback(data) - - def on_remote_server_up(self, server: str): - """Called when get a new REMOTE_SERVER_UP command.""" - def get_streams_to_replicate(self) -> Dict[str, int]: """Called when a new connection has been established and we need to subscribe to streams. @@ -163,85 +115,10 @@ class ReplicationClientHandler(AbstractReplicationClientHandler): args["account_data"] = user_account_data elif room_account_data: args["account_data"] = room_account_data - return args - def get_currently_syncing_users(self): - """Get the list of currently syncing users (if any). This is called - when a connection has been established and we need to send the - currently syncing users. (Overriden by the synchrotron's only) - """ - return [] + async def on_position(self, stream_name: str, token: int): + self.store.process_replication_rows(stream_name, token, []) - def send_command(self, cmd): - """Send a command to master (when we get establish a connection if we - don't have one already.) - """ - if self.connection: - self.connection.send_command(cmd) - else: - logger.warning("Queuing command as not connected: %r", cmd.NAME) - self.pending_commands.append(cmd) - - def send_federation_ack(self, token): - """Ack data for the federation stream. This allows the master to drop - data stored purely in memory. - """ - self.send_command(FederationAckCommand(token)) - - def send_user_sync(self, instance_id, user_id, is_syncing, last_sync_ms): - """Poke the master that a user has started/stopped syncing. - """ - self.send_command( - UserSyncCommand(instance_id, user_id, is_syncing, last_sync_ms) - ) - - def send_remove_pusher(self, app_id, push_key, user_id): - """Poke the master to remove a pusher for a user - """ - cmd = RemovePusherCommand(app_id, push_key, user_id) - self.send_command(cmd) - - def send_invalidate_cache(self, cache_func, keys): - """Poke the master to invalidate a cache. - """ - cmd = InvalidateCacheCommand(cache_func.__name__, keys) - self.send_command(cmd) - - def send_user_ip(self, user_id, access_token, ip, user_agent, device_id, last_seen): - """Tell the master that the user made a request. - """ - cmd = UserIpCommand(user_id, access_token, ip, user_agent, device_id, last_seen) - self.send_command(cmd) - - def send_remote_server_up(self, server: str): - self.send_command(RemoteServerUpCommand(server)) - - def await_sync(self, data): - """Returns a deferred that is resolved when we receive a SYNC command - with given data. - - [Not currently] used by tests. - """ - return self.awaiting_syncs.setdefault(data, defer.Deferred()) - - def update_connection(self, connection): - """Called when a connection has been established (or lost with None). - """ - self.connection = connection - if connection: - for cmd in self.pending_commands: - connection.send_command(cmd) - self.pending_commands = [] - - def finished_connecting(self): - """Called when we have successfully subscribed and caught up to all - streams we're interested in. - """ - logger.info("Finished connecting to server") - - # We don't reset the delay any earlier as otherwise if there is a - # problem during start up we'll end up tight looping connecting to the - # server. - if self.factory: - self.factory.resetDelay() + def on_remote_server_up(self, server: str): + """Called when get a new REMOTE_SERVER_UP command.""" diff --git a/synapse/replication/tcp/handler.py b/synapse/replication/tcp/handler.py new file mode 100644 index 0000000000..12a1cfd6d1 --- /dev/null +++ b/synapse/replication/tcp/handler.py @@ -0,0 +1,252 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Vector Creations Ltd +# Copyright 2020 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +from typing import Any, Callable, Dict, List, Optional, Set + +from prometheus_client import Counter + +from synapse.replication.tcp.client import ReplicationClientFactory +from synapse.replication.tcp.commands import ( + Command, + FederationAckCommand, + InvalidateCacheCommand, + PositionCommand, + RdataCommand, + RemoteServerUpCommand, + RemovePusherCommand, + SyncCommand, + UserIpCommand, + UserSyncCommand, +) +from synapse.replication.tcp.streams import STREAMS_MAP, Stream +from synapse.util.async_helpers import Linearizer + +logger = logging.getLogger(__name__) + + +# number of updates received for each RDATA stream +inbound_rdata_count = Counter( + "synapse_replication_tcp_protocol_inbound_rdata_count", "", ["stream_name"] +) + + +class ReplicationCommandHandler: + """Handles incoming commands from replication as well as sending commands + back out to connections. + """ + + def __init__(self, hs): + self._replication_data_handler = hs.get_replication_data_handler() + self._presence_handler = hs.get_presence_handler() + + # Set of streams that we've caught up with. + self._streams_connected = set() # type: Set[str] + + self._streams = { + stream.NAME: stream(hs) for stream in STREAMS_MAP.values() + } # type: Dict[str, Stream] + + self._position_linearizer = Linearizer("replication_position") + + # Map of stream to batched updates. See RdataCommand for info on how + # batching works. + self._pending_batches = {} # type: Dict[str, List[Any]] + + # The factory used to create connections. + self._factory = None # type: Optional[ReplicationClientFactory] + + # The current connection. None if we are currently (re)connecting + self._connection = None + + def start_replication(self, hs): + """Helper method to start a replication connection to the remote server + using TCP. + """ + client_name = hs.config.worker_name + self._factory = ReplicationClientFactory(hs, client_name, self) + host = hs.config.worker_replication_host + port = hs.config.worker_replication_port + hs.get_reactor().connectTCP(host, port, self._factory) + + async def on_RDATA(self, cmd: RdataCommand): + stream_name = cmd.stream_name + inbound_rdata_count.labels(stream_name).inc() + + try: + row = STREAMS_MAP[stream_name].parse_row(cmd.row) + except Exception: + logger.exception("Failed to parse RDATA: %r %r", stream_name, cmd.row) + raise + + if cmd.token is None or stream_name not in self._streams_connected: + # I.e. either this is part of a batch of updates for this stream (in + # which case batch until we get an update for the stream with a non + # None token) or we're currently connecting so we queue up rows. + self._pending_batches.setdefault(stream_name, []).append(row) + else: + # Check if this is the last of a batch of updates + rows = self._pending_batches.pop(stream_name, []) + rows.append(row) + await self.on_rdata(stream_name, cmd.token, rows) + + async def on_rdata(self, stream_name: str, token: int, rows: list): + """Called to handle a batch of replication data with a given stream token. + + Args: + stream_name: name of the replication stream for this batch of rows + token: stream token for this batch of rows + rows: a list of Stream.ROW_TYPE objects as returned by + Stream.parse_row. + """ + logger.debug("Received rdata %s -> %s", stream_name, token) + await self._replication_data_handler.on_rdata(stream_name, token, rows) + + async def on_POSITION(self, cmd: PositionCommand): + stream = self._streams.get(cmd.stream_name) + if not stream: + logger.error("Got POSITION for unknown stream: %s", cmd.stream_name) + return + + # We protect catching up with a linearizer in case the replication + # connection reconnects under us. + with await self._position_linearizer.queue(cmd.stream_name): + # We're about to go and catch up with the stream, so mark as connecting + # to stop RDATA being handled at the same time by removing stream from + # list of connected streams. We also clear any batched up RDATA from + # before we got the POSITION. + self._streams_connected.discard(cmd.stream_name) + self._pending_batches.clear() + + # Find where we previously streamed up to. + current_token = self._replication_data_handler.get_streams_to_replicate().get( + cmd.stream_name + ) + if current_token is None: + logger.warning( + "Got POSITION for stream we're not subscribed to: %s", + cmd.stream_name, + ) + return + + # Fetch all updates between then and now. + limited = True + while limited: + updates, current_token, limited = await stream.get_updates_since( + current_token, cmd.token + ) + if updates: + await self.on_rdata( + cmd.stream_name, + current_token, + [stream.parse_row(update[1]) for update in updates], + ) + + # We've now caught up to position sent to us, notify handler. + await self._replication_data_handler.on_position(cmd.stream_name, cmd.token) + + # Handle any RDATA that came in while we were catching up. + rows = self._pending_batches.pop(cmd.stream_name, []) + if rows: + await self._replication_data_handler.on_rdata( + cmd.stream_name, rows[-1].token, rows + ) + + self._streams_connected.add(cmd.stream_name) + + async def on_SYNC(self, cmd: SyncCommand): + pass + + async def on_REMOTE_SERVER_UP(self, cmd: RemoteServerUpCommand): + """"Called when get a new REMOTE_SERVER_UP command.""" + self._replication_data_handler.on_remote_server_up(cmd.data) + + def get_currently_syncing_users(self): + """Get the list of currently syncing users (if any). This is called + when a connection has been established and we need to send the + currently syncing users. + """ + return self._presence_handler.get_currently_syncing_users() + + def update_connection(self, connection): + """Called when a connection has been established (or lost with None). + """ + self._connection = connection + + def finished_connecting(self): + """Called when we have successfully subscribed and caught up to all + streams we're interested in. + """ + logger.info("Finished connecting to server") + + # We don't reset the delay any earlier as otherwise if there is a + # problem during start up we'll end up tight looping connecting to the + # server. + if self._factory: + self._factory.resetDelay() + + def send_command(self, cmd: Command): + """Send a command to master (when we get establish a connection if we + don't have one already.) + """ + if self._connection: + self._connection.send_command(cmd) + else: + logger.warning("Dropping command as not connected: %r", cmd.NAME) + + def send_federation_ack(self, token: int): + """Ack data for the federation stream. This allows the master to drop + data stored purely in memory. + """ + self.send_command(FederationAckCommand(token)) + + def send_user_sync( + self, instance_id: str, user_id: str, is_syncing: bool, last_sync_ms: int + ): + """Poke the master that a user has started/stopped syncing. + """ + self.send_command( + UserSyncCommand(instance_id, user_id, is_syncing, last_sync_ms) + ) + + def send_remove_pusher(self, app_id: str, push_key: str, user_id: str): + """Poke the master to remove a pusher for a user + """ + cmd = RemovePusherCommand(app_id, push_key, user_id) + self.send_command(cmd) + + def send_invalidate_cache(self, cache_func: Callable, keys: tuple): + """Poke the master to invalidate a cache. + """ + cmd = InvalidateCacheCommand(cache_func.__name__, keys) + self.send_command(cmd) + + def send_user_ip( + self, + user_id: str, + access_token: str, + ip: str, + user_agent: str, + device_id: str, + last_seen: int, + ): + """Tell the master that the user made a request. + """ + cmd = UserIpCommand(user_id, access_token, ip, user_agent, device_id, last_seen) + self.send_command(cmd) + + def send_remote_server_up(self, server: str): + self.send_command(RemoteServerUpCommand(server)) diff --git a/synapse/replication/tcp/protocol.py b/synapse/replication/tcp/protocol.py index dae246825f..f2a37f568e 100644 --- a/synapse/replication/tcp/protocol.py +++ b/synapse/replication/tcp/protocol.py @@ -46,12 +46,11 @@ indicate which side is sending, these are *not* included on the wire:: > ERROR server stopping * connection closed by server * """ -import abc import fcntl import logging import struct from collections import defaultdict -from typing import Any, DefaultDict, Dict, List, Set +from typing import TYPE_CHECKING, DefaultDict, List from six import iteritems @@ -78,13 +77,12 @@ from synapse.replication.tcp.commands import ( SyncCommand, UserSyncCommand, ) -from synapse.replication.tcp.streams import STREAMS_MAP, Stream from synapse.types import Collection from synapse.util import Clock from synapse.util.stringutils import random_string -MYPY = False -if MYPY: +if TYPE_CHECKING: + from synapse.replication.tcp.handler import ReplicationCommandHandler from synapse.server import HomeServer @@ -475,71 +473,6 @@ class ServerReplicationStreamProtocol(BaseReplicationStreamProtocol): self.streamer.lost_connection(self) -class AbstractReplicationClientHandler(metaclass=abc.ABCMeta): - """ - The interface for the handler that should be passed to - ClientReplicationStreamProtocol - """ - - @abc.abstractmethod - async def on_rdata(self, stream_name, token, rows): - """Called to handle a batch of replication data with a given stream token. - - Args: - stream_name (str): name of the replication stream for this batch of rows - token (int): stream token for this batch of rows - rows (list): a list of Stream.ROW_TYPE objects as returned by - Stream.parse_row. - """ - raise NotImplementedError() - - @abc.abstractmethod - async def on_position(self, stream_name, token): - """Called when we get new position data.""" - raise NotImplementedError() - - @abc.abstractmethod - def on_sync(self, data): - """Called when get a new SYNC command.""" - raise NotImplementedError() - - @abc.abstractmethod - async def on_remote_server_up(self, server: str): - """Called when get a new REMOTE_SERVER_UP command.""" - raise NotImplementedError() - - @abc.abstractmethod - def get_streams_to_replicate(self): - """Called when a new connection has been established and we need to - subscribe to streams. - - Returns: - map from stream name to the most recent update we have for - that stream (ie, the point we want to start replicating from) - """ - raise NotImplementedError() - - @abc.abstractmethod - def get_currently_syncing_users(self): - """Get the list of currently syncing users (if any). This is called - when a connection has been established and we need to send the - currently syncing users.""" - raise NotImplementedError() - - @abc.abstractmethod - def update_connection(self, connection): - """Called when a connection has been established (or lost with None). - """ - raise NotImplementedError() - - @abc.abstractmethod - def finished_connecting(self): - """Called when we have successfully subscribed and caught up to all - streams we're interested in. - """ - raise NotImplementedError() - - class ClientReplicationStreamProtocol(BaseReplicationStreamProtocol): VALID_INBOUND_COMMANDS = VALID_SERVER_COMMANDS VALID_OUTBOUND_COMMANDS = VALID_CLIENT_COMMANDS @@ -550,7 +483,7 @@ class ClientReplicationStreamProtocol(BaseReplicationStreamProtocol): client_name: str, server_name: str, clock: Clock, - handler: AbstractReplicationClientHandler, + command_handler: "ReplicationCommandHandler", ): BaseReplicationStreamProtocol.__init__(self, clock) @@ -558,20 +491,7 @@ class ClientReplicationStreamProtocol(BaseReplicationStreamProtocol): self.client_name = client_name self.server_name = server_name - self.handler = handler - - self.streams = { - stream.NAME: stream(hs) for stream in STREAMS_MAP.values() - } # type: Dict[str, Stream] - - # Set of stream names that have been subscribe to, but haven't yet - # caught up with. This is used to track when the client has been fully - # connected to the remote. - self.streams_connecting = set(STREAMS_MAP) # type: Set[str] - - # Map of stream to batched updates. See RdataCommand for info on how - # batching works. - self.pending_batches = {} # type: Dict[str, List[Any]] + self.handler = command_handler def connectionMade(self): self.send_command(NameCommand(self.client_name)) @@ -589,90 +509,40 @@ class ClientReplicationStreamProtocol(BaseReplicationStreamProtocol): # We've now finished connecting to so inform the client handler self.handler.update_connection(self) + self.handler.finished_connecting() + + async def handle_command(self, cmd: Command): + """Handle a command we have received over the replication stream. + + Delegates to `command_handler.on_`, which must return an + awaitable. + + Args: + cmd: received command + """ + handled = False + + # First call any command handlers on this instance. These are for TCP + # specific handling. + cmd_func = getattr(self, "on_%s" % (cmd.NAME,), None) + if cmd_func: + await cmd_func(cmd) + handled = True + + # Then call out to the handler. + cmd_func = getattr(self.handler, "on_%s" % (cmd.NAME,), None) + if cmd_func: + await cmd_func(cmd) + handled = True + + if not handled: + logger.warning("Unhandled command: %r", cmd) async def on_SERVER(self, cmd): if cmd.data != self.server_name: logger.error("[%s] Connected to wrong remote: %r", self.id(), cmd.data) self.send_error("Wrong remote") - async def on_RDATA(self, cmd): - stream_name = cmd.stream_name - inbound_rdata_count.labels(stream_name).inc() - - try: - row = STREAMS_MAP[stream_name].parse_row(cmd.row) - except Exception: - logger.exception( - "[%s] Failed to parse RDATA: %r %r", self.id(), stream_name, cmd.row - ) - raise - - if cmd.token is None or stream_name in self.streams_connecting: - # I.e. this is part of a batch of updates for this stream. Batch - # until we get an update for the stream with a non None token - self.pending_batches.setdefault(stream_name, []).append(row) - else: - # Check if this is the last of a batch of updates - rows = self.pending_batches.pop(stream_name, []) - rows.append(row) - await self.handler.on_rdata(stream_name, cmd.token, rows) - - async def on_POSITION(self, cmd: PositionCommand): - stream = self.streams.get(cmd.stream_name) - if not stream: - logger.error("Got POSITION for unknown stream: %s", cmd.stream_name) - return - - # Find where we previously streamed up to. - current_token = self.handler.get_streams_to_replicate().get(cmd.stream_name) - if current_token is None: - logger.warning( - "Got POSITION for stream we're not subscribed to: %s", cmd.stream_name - ) - return - - # Fetch all updates between then and now. - limited = True - while limited: - updates, current_token, limited = await stream.get_updates_since( - current_token, cmd.token - ) - - # Check if the connection was closed underneath us, if so we bail - # rather than risk having concurrent catch ups going on. - if self.state == ConnectionStates.CLOSED: - return - - if updates: - await self.handler.on_rdata( - cmd.stream_name, - current_token, - [stream.parse_row(update[1]) for update in updates], - ) - - # We've now caught up to position sent to us, notify handler. - await self.handler.on_position(cmd.stream_name, cmd.token) - - self.streams_connecting.discard(cmd.stream_name) - if not self.streams_connecting: - self.handler.finished_connecting() - - # Check if the connection was closed underneath us, if so we bail - # rather than risk having concurrent catch ups going on. - if self.state == ConnectionStates.CLOSED: - return - - # Handle any RDATA that came in while we were catching up. - rows = self.pending_batches.pop(cmd.stream_name, []) - if rows: - await self.handler.on_rdata(cmd.stream_name, rows[-1].token, rows) - - async def on_SYNC(self, cmd): - self.handler.on_sync(cmd.data) - - async def on_REMOTE_SERVER_UP(self, cmd: RemoteServerUpCommand): - self.handler.on_remote_server_up(cmd.data) - def replicate(self): """Send the subscription request to the server """ @@ -768,8 +638,3 @@ tcp_outbound_commands = LaterGauge( for k, count in iteritems(p.outbound_commands_counter) }, ) - -# number of updates received for each RDATA stream -inbound_rdata_count = Counter( - "synapse_replication_tcp_protocol_inbound_rdata_count", "", ["stream_name"] -) diff --git a/synapse/server.py b/synapse/server.py index 9228e1c892..9d273c980c 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -87,6 +87,8 @@ from synapse.http.matrixfederationclient import MatrixFederationHttpClient from synapse.notifier import Notifier from synapse.push.action_generator import ActionGenerator from synapse.push.pusherpool import PusherPool +from synapse.replication.tcp.client import ReplicationDataHandler +from synapse.replication.tcp.handler import ReplicationCommandHandler from synapse.replication.tcp.resource import ReplicationStreamer from synapse.rest.media.v1.media_repository import ( MediaRepository, @@ -206,6 +208,7 @@ class HomeServer(object): "password_policy_handler", "storage", "replication_streamer", + "replication_data_handler", ] REQUIRED_ON_MASTER_STARTUP = ["user_directory_handler", "stats_handler"] @@ -468,7 +471,7 @@ class HomeServer(object): return ReadMarkerHandler(self) def build_tcp_replication(self): - raise NotImplementedError() + return ReplicationCommandHandler(self) def build_action_generator(self): return ActionGenerator(self) @@ -562,6 +565,9 @@ class HomeServer(object): def build_replication_streamer(self) -> ReplicationStreamer: return ReplicationStreamer(self) + def build_replication_data_handler(self): + return ReplicationDataHandler(self.get_datastore()) + def remove_pusher(self, app_id, push_key, user_id): return self.get_pusherpool().remove_pusher(app_id, push_key, user_id) diff --git a/synapse/server.pyi b/synapse/server.pyi index 9d1dfa71e7..9013e9bac9 100644 --- a/synapse/server.pyi +++ b/synapse/server.pyi @@ -19,6 +19,7 @@ import synapse.handlers.set_password import synapse.http.client import synapse.notifier import synapse.replication.tcp.client +import synapse.replication.tcp.handler import synapse.rest.media.v1.media_repository import synapse.server_notices.server_notices_manager import synapse.server_notices.server_notices_sender @@ -106,7 +107,11 @@ class HomeServer(object): pass def get_tcp_replication( self, - ) -> synapse.replication.tcp.client.ReplicationClientHandler: + ) -> synapse.replication.tcp.handler.ReplicationCommandHandler: + pass + def get_replication_data_handler( + self, + ) -> synapse.replication.tcp.client.ReplicationDataHandler: pass def get_federation_registry( self, diff --git a/tests/replication/slave/storage/_base.py b/tests/replication/slave/storage/_base.py index 2a1e7c7166..8902a5ab69 100644 --- a/tests/replication/slave/storage/_base.py +++ b/tests/replication/slave/storage/_base.py @@ -17,8 +17,9 @@ from mock import Mock, NonCallableMock from synapse.replication.tcp.client import ( ReplicationClientFactory, - ReplicationClientHandler, + ReplicationDataHandler, ) +from synapse.replication.tcp.handler import ReplicationCommandHandler from synapse.replication.tcp.resource import ReplicationStreamProtocolFactory from synapse.storage.database import make_conn @@ -51,15 +52,19 @@ class BaseSlavedStoreTestCase(unittest.HomeserverTestCase): self.event_id = 0 server_factory = ReplicationStreamProtocolFactory(self.hs) - self.streamer = server_factory.streamer + self.streamer = hs.get_replication_streamer() - handler_factory = Mock() - self.replication_handler = ReplicationClientHandler(self.slaved_store) - self.replication_handler.factory = handler_factory + # We now do some gut wrenching so that we have a client that is based + # off of the slave store rather than the main store. + self.replication_handler = ReplicationCommandHandler(self.hs) + self.replication_handler._replication_data_handler = ReplicationDataHandler( + self.slaved_store + ) client_factory = ReplicationClientFactory( self.hs, "client_name", self.replication_handler ) + client_factory.handler = self.replication_handler server = server_factory.buildProtocol(None) client = client_factory.buildProtocol(None) diff --git a/tests/replication/tcp/streams/_base.py b/tests/replication/tcp/streams/_base.py index a755fe2879..32238fe79a 100644 --- a/tests/replication/tcp/streams/_base.py +++ b/tests/replication/tcp/streams/_base.py @@ -15,7 +15,7 @@ from mock import Mock -from synapse.replication.tcp.commands import ReplicateCommand +from synapse.replication.tcp.handler import ReplicationCommandHandler from synapse.replication.tcp.protocol import ClientReplicationStreamProtocol from synapse.replication.tcp.resource import ReplicationStreamProtocolFactory @@ -26,15 +26,20 @@ from tests.server import FakeTransport class BaseStreamTestCase(unittest.HomeserverTestCase): """Base class for tests of the replication streams""" + def make_homeserver(self, reactor, clock): + self.test_handler = Mock(wraps=TestReplicationDataHandler()) + return self.setup_test_homeserver(replication_data_handler=self.test_handler) + def prepare(self, reactor, clock, hs): # build a replication server - server_factory = ReplicationStreamProtocolFactory(self.hs) - self.streamer = server_factory.streamer + server_factory = ReplicationStreamProtocolFactory(hs) + self.streamer = hs.get_replication_streamer() self.server = server_factory.buildProtocol(None) - self.test_handler = Mock(wraps=TestReplicationClientHandler()) + repl_handler = ReplicationCommandHandler(hs) + repl_handler.handler = self.test_handler self.client = ClientReplicationStreamProtocol( - hs, "client", "test", clock, self.test_handler, + hs, "client", "test", clock, repl_handler, ) self._client_transport = None @@ -69,13 +74,9 @@ class BaseStreamTestCase(unittest.HomeserverTestCase): self.streamer.on_notifier_poke() self.pump(0.1) - def replicate_stream(self): - """Make the client end a REPLICATE command to set up a subscription to a stream""" - self.client.send_command(ReplicateCommand()) - -class TestReplicationClientHandler(object): - """Drop-in for ReplicationClientHandler which just collects RDATA rows""" +class TestReplicationDataHandler: + """Drop-in for ReplicationDataHandler which just collects RDATA rows""" def __init__(self): self.streams = set() @@ -88,18 +89,9 @@ class TestReplicationClientHandler(object): positions[stream] = max(token, positions.get(stream, 0)) return positions - def get_currently_syncing_users(self): - return [] - - def update_connection(self, connection): - pass - - def finished_connecting(self): - pass - - async def on_position(self, stream_name, token): - """Called when we get new position data.""" - async def on_rdata(self, stream_name, token, rows): for r in rows: self._received_rdata_rows.append((stream_name, token, r)) + + async def on_position(self, stream_name, token): + pass diff --git a/tests/replication/tcp/streams/test_receipts.py b/tests/replication/tcp/streams/test_receipts.py index 0ec0825a0e..a0206f7363 100644 --- a/tests/replication/tcp/streams/test_receipts.py +++ b/tests/replication/tcp/streams/test_receipts.py @@ -24,7 +24,6 @@ class ReceiptsStreamTestCase(BaseStreamTestCase): self.reconnect() # make the client subscribe to the receipts stream - self.replicate_stream() self.test_handler.streams.add("receipts") # tell the master to send a new receipt From b21000a44fa8b6f5d28a2089033f76767dff868b Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Mon, 6 Apr 2020 12:35:30 +0100 Subject: [PATCH 1318/1623] Improve error responses when a remote server doesn't allow you to access its public rooms list (#6899) --- changelog.d/6899.bugfix | 1 + synapse/handlers/room_list.py | 23 ++++++++++++----------- synapse/rest/client/v1/room.py | 33 ++++++++++++++++++++------------- 3 files changed, 33 insertions(+), 24 deletions(-) create mode 100644 changelog.d/6899.bugfix diff --git a/changelog.d/6899.bugfix b/changelog.d/6899.bugfix new file mode 100644 index 0000000000..efa8a40b1f --- /dev/null +++ b/changelog.d/6899.bugfix @@ -0,0 +1 @@ +Improve error responses when accessing remote public room lists. \ No newline at end of file diff --git a/synapse/handlers/room_list.py b/synapse/handlers/room_list.py index 0b7d3da680..59c9906b31 100644 --- a/synapse/handlers/room_list.py +++ b/synapse/handlers/room_list.py @@ -15,6 +15,7 @@ import logging from collections import namedtuple +from typing import Any, Dict, Optional from six import iteritems @@ -105,22 +106,22 @@ class RoomListHandler(BaseHandler): @defer.inlineCallbacks def _get_public_room_list( self, - limit=None, - since_token=None, - search_filter=None, - network_tuple=EMPTY_THIRD_PARTY_ID, - from_federation=False, - ): + limit: Optional[int] = None, + since_token: Optional[str] = None, + search_filter: Optional[Dict] = None, + network_tuple: ThirdPartyInstanceID = EMPTY_THIRD_PARTY_ID, + from_federation: bool = False, + ) -> Dict[str, Any]: """Generate a public room list. Args: - limit (int|None): Maximum amount of rooms to return. - since_token (str|None) - search_filter (dict|None): Dictionary to filter rooms by. - network_tuple (ThirdPartyInstanceID): Which public list to use. + limit: Maximum amount of rooms to return. + since_token: + search_filter: Dictionary to filter rooms by. + network_tuple: Which public list to use. This can be (None, None) to indicate the main list, or a particular appservice and network id to use an appservice specific one. Setting to None returns all public rooms across all lists. - from_federation (bool): Whether this request originated from a + from_federation: Whether this request originated from a federating server or a client. Used for room filtering. """ diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index bffd43de5f..6b5830cc3f 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -27,6 +27,7 @@ from synapse.api.constants import EventTypes, Membership from synapse.api.errors import ( AuthError, Codes, + HttpResponseException, InvalidClientCredentialsError, SynapseError, ) @@ -364,10 +365,13 @@ class PublicRoomListRestServlet(TransactionRestServlet): limit = None handler = self.hs.get_room_list_handler() - if server: - data = await handler.get_remote_public_room_list( - server, limit=limit, since_token=since_token - ) + if server and server != self.hs.config.server_name: + try: + data = await handler.get_remote_public_room_list( + server, limit=limit, since_token=since_token + ) + except HttpResponseException as e: + raise e.to_synapse_error() else: data = await handler.get_local_public_room_list( limit=limit, since_token=since_token @@ -404,15 +408,18 @@ class PublicRoomListRestServlet(TransactionRestServlet): limit = None handler = self.hs.get_room_list_handler() - if server: - data = await handler.get_remote_public_room_list( - server, - limit=limit, - since_token=since_token, - search_filter=search_filter, - include_all_networks=include_all_networks, - third_party_instance_id=third_party_instance_id, - ) + if server and server != self.hs.config.server_name: + try: + data = await handler.get_remote_public_room_list( + server, + limit=limit, + since_token=since_token, + search_filter=search_filter, + include_all_networks=include_all_networks, + third_party_instance_id=third_party_instance_id, + ) + except HttpResponseException as e: + raise e.to_synapse_error() else: data = await handler.get_local_public_room_list( limit=limit, From 4b0f00ad0c6bbe153f82b95980a2ba16238b4449 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Mon, 6 Apr 2020 12:40:34 +0100 Subject: [PATCH 1319/1623] Remove stream before/after debug log lines (#7207) --- changelog.d/7207.misc | 1 + synapse/storage/data_stores/main/stream.py | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) create mode 100644 changelog.d/7207.misc diff --git a/changelog.d/7207.misc b/changelog.d/7207.misc new file mode 100644 index 0000000000..4f9b6a1089 --- /dev/null +++ b/changelog.d/7207.misc @@ -0,0 +1 @@ +Remove some extraneous debugging log lines. \ No newline at end of file diff --git a/synapse/storage/data_stores/main/stream.py b/synapse/storage/data_stores/main/stream.py index ada5cce6c2..e89f0bffb5 100644 --- a/synapse/storage/data_stores/main/stream.py +++ b/synapse/storage/data_stores/main/stream.py @@ -481,11 +481,9 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore): room_id, limit, end_token ) - logger.debug("stream before") events = yield self.get_events_as_list( [r.event_id for r in rows], get_prev_content=True ) - logger.debug("stream after") self._set_before_and_after(events, rows) From 71953139d15e85898e77c0572f7f94d09d58e747 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Mon, 6 Apr 2020 17:02:44 -0400 Subject: [PATCH 1320/1623] Add information about .well-known to Debian installation. (#7227) --- debian/changelog | 6 ++++++ debian/po/templates.pot | 13 ++++++++----- debian/templates | 6 ++++-- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/debian/changelog b/debian/changelog index 642115fc5a..412e850325 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.12.3ubuntu1) UNRELEASED; urgency=medium + + * Add information about .well-known files to Debian installation scripts. + + -- Patrick Cloke Mon, 06 Apr 2020 10:10:38 -0400 + matrix-synapse-py3 (1.12.3) stable; urgency=medium [ Richard van der Hoff ] diff --git a/debian/po/templates.pot b/debian/po/templates.pot index 84d960761a..f0af9e70fb 100644 --- a/debian/po/templates.pot +++ b/debian/po/templates.pot @@ -1,14 +1,14 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the matrix-synapse package. +# This file is distributed under the same license as the matrix-synapse-py3 package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" -"Project-Id-Version: matrix-synapse\n" -"Report-Msgid-Bugs-To: matrix-synapse@packages.debian.org\n" -"POT-Creation-Date: 2017-02-21 07:51+0000\n" +"Project-Id-Version: matrix-synapse-py3\n" +"Report-Msgid-Bugs-To: matrix-synapse-py3@packages.debian.org\n" +"POT-Creation-Date: 2020-04-06 16:39-0400\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -28,7 +28,10 @@ msgstr "" #: ../templates:1001 msgid "" "The name that this homeserver will appear as, to clients and other servers " -"via federation. This name should match the SRV record published in DNS." +"via federation. This is normally the public hostname of the server running " +"synapse, but can be different if you set up delegation. Please refer to the " +"delegation documentation in this case: https://github.com/matrix-org/synapse/" +"blob/master/docs/delegate.md." msgstr "" #. Type: boolean diff --git a/debian/templates b/debian/templates index 647358731c..458fe8bbe9 100644 --- a/debian/templates +++ b/debian/templates @@ -2,8 +2,10 @@ Template: matrix-synapse/server-name Type: string _Description: Name of the server: The name that this homeserver will appear as, to clients and other - servers via federation. This name should match the SRV record - published in DNS. + servers via federation. This is normally the public hostname of the + server running synapse, but can be different if you set up delegation. + Please refer to the delegation documentation in this case: + https://github.com/matrix-org/synapse/blob/master/docs/delegate.md. Template: matrix-synapse/report-stats Type: boolean From 82498ee9019747eb86ed753e08fac0990d4ac8b9 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 7 Apr 2020 10:51:07 +0100 Subject: [PATCH 1321/1623] Move server command handling out of TCP protocol (#7187) This completes the merging of server and client command processing. --- changelog.d/7187.misc | 1 + synapse/replication/tcp/handler.py | 179 +++++++++++++++++++++++++--- synapse/replication/tcp/protocol.py | 165 ++++++++----------------- synapse/replication/tcp/resource.py | 163 ++++--------------------- 4 files changed, 238 insertions(+), 270 deletions(-) create mode 100644 changelog.d/7187.misc diff --git a/changelog.d/7187.misc b/changelog.d/7187.misc new file mode 100644 index 0000000000..60d68ae877 --- /dev/null +++ b/changelog.d/7187.misc @@ -0,0 +1 @@ +Move server command handling out of TCP protocol. diff --git a/synapse/replication/tcp/handler.py b/synapse/replication/tcp/handler.py index 12a1cfd6d1..8ec0119697 100644 --- a/synapse/replication/tcp/handler.py +++ b/synapse/replication/tcp/handler.py @@ -19,8 +19,10 @@ from typing import Any, Callable, Dict, List, Optional, Set from prometheus_client import Counter +from synapse.metrics import LaterGauge from synapse.replication.tcp.client import ReplicationClientFactory from synapse.replication.tcp.commands import ( + ClearUserSyncsCommand, Command, FederationAckCommand, InvalidateCacheCommand, @@ -28,10 +30,12 @@ from synapse.replication.tcp.commands import ( RdataCommand, RemoteServerUpCommand, RemovePusherCommand, + ReplicateCommand, SyncCommand, UserIpCommand, UserSyncCommand, ) +from synapse.replication.tcp.protocol import AbstractConnection from synapse.replication.tcp.streams import STREAMS_MAP, Stream from synapse.util.async_helpers import Linearizer @@ -42,6 +46,13 @@ logger = logging.getLogger(__name__) inbound_rdata_count = Counter( "synapse_replication_tcp_protocol_inbound_rdata_count", "", ["stream_name"] ) +user_sync_counter = Counter("synapse_replication_tcp_resource_user_sync", "") +federation_ack_counter = Counter("synapse_replication_tcp_resource_federation_ack", "") +remove_pusher_counter = Counter("synapse_replication_tcp_resource_remove_pusher", "") +invalidate_cache_counter = Counter( + "synapse_replication_tcp_resource_invalidate_cache", "" +) +user_ip_cache_counter = Counter("synapse_replication_tcp_resource_user_ip_cache", "") class ReplicationCommandHandler: @@ -52,6 +63,10 @@ class ReplicationCommandHandler: def __init__(self, hs): self._replication_data_handler = hs.get_replication_data_handler() self._presence_handler = hs.get_presence_handler() + self._store = hs.get_datastore() + self._notifier = hs.get_notifier() + self._clock = hs.get_clock() + self._instance_id = hs.get_instance_id() # Set of streams that we've caught up with. self._streams_connected = set() # type: Set[str] @@ -69,8 +84,26 @@ class ReplicationCommandHandler: # The factory used to create connections. self._factory = None # type: Optional[ReplicationClientFactory] - # The current connection. None if we are currently (re)connecting - self._connection = None + # The currently connected connections. + self._connections = [] # type: List[AbstractConnection] + + LaterGauge( + "synapse_replication_tcp_resource_total_connections", + "", + [], + lambda: len(self._connections), + ) + + self._is_master = hs.config.worker_app is None + + self._federation_sender = None + if self._is_master and not hs.config.send_federation: + self._federation_sender = hs.get_federation_sender() + + self._server_notices_sender = None + if self._is_master: + self._server_notices_sender = hs.get_server_notices_sender() + self._notifier.add_remote_server_up_callback(self.send_remote_server_up) def start_replication(self, hs): """Helper method to start a replication connection to the remote server @@ -82,6 +115,70 @@ class ReplicationCommandHandler: port = hs.config.worker_replication_port hs.get_reactor().connectTCP(host, port, self._factory) + async def on_REPLICATE(self, cmd: ReplicateCommand): + # We only want to announce positions by the writer of the streams. + # Currently this is just the master process. + if not self._is_master: + return + + for stream_name, stream in self._streams.items(): + current_token = stream.current_token() + self.send_command(PositionCommand(stream_name, current_token)) + + async def on_USER_SYNC(self, cmd: UserSyncCommand): + user_sync_counter.inc() + + if self._is_master: + await self._presence_handler.update_external_syncs_row( + cmd.instance_id, cmd.user_id, cmd.is_syncing, cmd.last_sync_ms + ) + + async def on_CLEAR_USER_SYNC(self, cmd: ClearUserSyncsCommand): + if self._is_master: + await self._presence_handler.update_external_syncs_clear(cmd.instance_id) + + async def on_FEDERATION_ACK(self, cmd: FederationAckCommand): + federation_ack_counter.inc() + + if self._federation_sender: + self._federation_sender.federation_ack(cmd.token) + + async def on_REMOVE_PUSHER(self, cmd: RemovePusherCommand): + remove_pusher_counter.inc() + + if self._is_master: + await self._store.delete_pusher_by_app_id_pushkey_user_id( + app_id=cmd.app_id, pushkey=cmd.push_key, user_id=cmd.user_id + ) + + self._notifier.on_new_replication_data() + + async def on_INVALIDATE_CACHE(self, cmd: InvalidateCacheCommand): + invalidate_cache_counter.inc() + + if self._is_master: + # We invalidate the cache locally, but then also stream that to other + # workers. + await self._store.invalidate_cache_and_stream( + cmd.cache_func, tuple(cmd.keys) + ) + + async def on_USER_IP(self, cmd: UserIpCommand): + user_ip_cache_counter.inc() + + if self._is_master: + await self._store.insert_client_ip( + cmd.user_id, + cmd.access_token, + cmd.ip, + cmd.user_agent, + cmd.device_id, + cmd.last_seen, + ) + + if self._server_notices_sender: + await self._server_notices_sender.on_user_ip(cmd.user_id) + async def on_RDATA(self, cmd: RdataCommand): stream_name = cmd.stream_name inbound_rdata_count.labels(stream_name).inc() @@ -174,6 +271,9 @@ class ReplicationCommandHandler: """"Called when get a new REMOTE_SERVER_UP command.""" self._replication_data_handler.on_remote_server_up(cmd.data) + if self._is_master: + self._notifier.notify_remote_server_up(cmd.data) + def get_currently_syncing_users(self): """Get the list of currently syncing users (if any). This is called when a connection has been established and we need to send the @@ -181,29 +281,63 @@ class ReplicationCommandHandler: """ return self._presence_handler.get_currently_syncing_users() - def update_connection(self, connection): - """Called when a connection has been established (or lost with None). + def new_connection(self, connection: AbstractConnection): + """Called when we have a new connection. """ - self._connection = connection + self._connections.append(connection) - def finished_connecting(self): - """Called when we have successfully subscribed and caught up to all - streams we're interested in. - """ - logger.info("Finished connecting to server") - - # We don't reset the delay any earlier as otherwise if there is a - # problem during start up we'll end up tight looping connecting to the - # server. + # If we are connected to replication as a client (rather than a server) + # we need to reset the reconnection delay on the client factory (which + # is used to do exponential back off when the connection drops). + # + # Ideally we would reset the delay when we've "fully established" the + # connection (for some definition thereof) to stop us from tightlooping + # on reconnection if something fails after this point and we drop the + # connection. Unfortunately, we don't really have a better definition of + # "fully established" than the connection being established. if self._factory: self._factory.resetDelay() - def send_command(self, cmd: Command): - """Send a command to master (when we get establish a connection if we - don't have one already.) + # Tell the server if we have any users currently syncing (should only + # happen on synchrotrons) + currently_syncing = self.get_currently_syncing_users() + now = self._clock.time_msec() + for user_id in currently_syncing: + connection.send_command( + UserSyncCommand(self._instance_id, user_id, True, now) + ) + + def lost_connection(self, connection: AbstractConnection): + """Called when a connection is closed/lost. """ - if self._connection: - self._connection.send_command(cmd) + try: + self._connections.remove(connection) + except ValueError: + pass + + def connected(self) -> bool: + """Do we have any replication connections open? + + Is used by e.g. `ReplicationStreamer` to no-op if nothing is connected. + """ + return bool(self._connections) + + def send_command(self, cmd: Command): + """Send a command to all connected connections. + """ + if self._connections: + for connection in self._connections: + try: + connection.send_command(cmd) + except Exception: + # We probably want to catch some types of exceptions here + # and log them as warnings (e.g. connection gone), but I + # can't find what those exception types they would be. + logger.exception( + "Failed to write command %s to connection %s", + cmd.NAME, + connection, + ) else: logger.warning("Dropping command as not connected: %r", cmd.NAME) @@ -250,3 +384,10 @@ class ReplicationCommandHandler: def send_remote_server_up(self, server: str): self.send_command(RemoteServerUpCommand(server)) + + def stream_update(self, stream_name: str, token: str, data: Any): + """Called when a new update is available to stream to clients. + + We need to check if the client is interested in the stream or not + """ + self.send_command(RdataCommand(stream_name, token, data)) diff --git a/synapse/replication/tcp/protocol.py b/synapse/replication/tcp/protocol.py index f2a37f568e..9aabb9c586 100644 --- a/synapse/replication/tcp/protocol.py +++ b/synapse/replication/tcp/protocol.py @@ -46,6 +46,7 @@ indicate which side is sending, these are *not* included on the wire:: > ERROR server stopping * connection closed by server * """ +import abc import fcntl import logging import struct @@ -69,13 +70,8 @@ from synapse.replication.tcp.commands import ( ErrorCommand, NameCommand, PingCommand, - PositionCommand, - RdataCommand, - RemoteServerUpCommand, ReplicateCommand, ServerCommand, - SyncCommand, - UserSyncCommand, ) from synapse.types import Collection from synapse.util import Clock @@ -118,7 +114,7 @@ class BaseReplicationStreamProtocol(LineOnlyReceiver): are only sent by the server. On receiving a new command it calls `on_` with the parsed - command. + command before delegating to `ReplicationCommandHandler.on_`. It also sends `PING` periodically, and correctly times out remote connections (if they send a `PING` command) @@ -134,8 +130,9 @@ class BaseReplicationStreamProtocol(LineOnlyReceiver): max_line_buffer = 10000 - def __init__(self, clock): + def __init__(self, clock: Clock, handler: "ReplicationCommandHandler"): self.clock = clock + self.command_handler = handler self.last_received_command = self.clock.time_msec() self.last_sent_command = 0 @@ -175,6 +172,8 @@ class BaseReplicationStreamProtocol(LineOnlyReceiver): # can time us out. self.send_command(PingCommand(self.clock.time_msec())) + self.command_handler.new_connection(self) + def send_ping(self): """Periodically sends a ping and checks if we should close the connection due to the other side timing out. @@ -243,13 +242,31 @@ class BaseReplicationStreamProtocol(LineOnlyReceiver): async def handle_command(self, cmd: Command): """Handle a command we have received over the replication stream. - By default delegates to on_, which should return an awaitable. + First calls `self.on_` if it exists, then calls + `self.command_handler.on_` if it exists. This allows for + protocol level handling of commands (e.g. PINGs), before delegating to + the handler. Args: cmd: received command """ - handler = getattr(self, "on_%s" % (cmd.NAME,)) - await handler(cmd) + handled = False + + # First call any command handlers on this instance. These are for TCP + # specific handling. + cmd_func = getattr(self, "on_%s" % (cmd.NAME,), None) + if cmd_func: + await cmd_func(cmd) + handled = True + + # Then call out to the handler. + cmd_func = getattr(self.command_handler, "on_%s" % (cmd.NAME,), None) + if cmd_func: + await cmd_func(cmd) + handled = True + + if not handled: + logger.warning("Unhandled command: %r", cmd) def close(self): logger.warning("[%s] Closing connection", self.id()) @@ -378,6 +395,8 @@ class BaseReplicationStreamProtocol(LineOnlyReceiver): self.state = ConnectionStates.CLOSED self.pending_commands = [] + self.command_handler.lost_connection(self) + if self.transport: self.transport.unregisterProducer() @@ -404,74 +423,21 @@ class ServerReplicationStreamProtocol(BaseReplicationStreamProtocol): VALID_INBOUND_COMMANDS = VALID_CLIENT_COMMANDS VALID_OUTBOUND_COMMANDS = VALID_SERVER_COMMANDS - def __init__(self, server_name, clock, streamer): - BaseReplicationStreamProtocol.__init__(self, clock) # Old style class + def __init__( + self, server_name: str, clock: Clock, handler: "ReplicationCommandHandler" + ): + super().__init__(clock, handler) self.server_name = server_name - self.streamer = streamer def connectionMade(self): self.send_command(ServerCommand(self.server_name)) - BaseReplicationStreamProtocol.connectionMade(self) - self.streamer.new_connection(self) + super().connectionMade() async def on_NAME(self, cmd): logger.info("[%s] Renamed to %r", self.id(), cmd.data) self.name = cmd.data - async def on_USER_SYNC(self, cmd): - await self.streamer.on_user_sync( - cmd.instance_id, cmd.user_id, cmd.is_syncing, cmd.last_sync_ms - ) - - async def on_CLEAR_USER_SYNC(self, cmd): - await self.streamer.on_clear_user_syncs(cmd.instance_id) - - async def on_REPLICATE(self, cmd): - # Subscribe to all streams we're publishing to. - for stream_name in self.streamer.streams_by_name: - current_token = self.streamer.get_stream_token(stream_name) - self.send_command(PositionCommand(stream_name, current_token)) - - async def on_FEDERATION_ACK(self, cmd): - self.streamer.federation_ack(cmd.token) - - async def on_REMOVE_PUSHER(self, cmd): - await self.streamer.on_remove_pusher(cmd.app_id, cmd.push_key, cmd.user_id) - - async def on_INVALIDATE_CACHE(self, cmd): - await self.streamer.on_invalidate_cache(cmd.cache_func, cmd.keys) - - async def on_REMOTE_SERVER_UP(self, cmd: RemoteServerUpCommand): - self.streamer.on_remote_server_up(cmd.data) - - async def on_USER_IP(self, cmd): - self.streamer.on_user_ip( - cmd.user_id, - cmd.access_token, - cmd.ip, - cmd.user_agent, - cmd.device_id, - cmd.last_seen, - ) - - def stream_update(self, stream_name, token, data): - """Called when a new update is available to stream to clients. - - We need to check if the client is interested in the stream or not - """ - self.send_command(RdataCommand(stream_name, token, data)) - - def send_sync(self, data): - self.send_command(SyncCommand(data)) - - def send_remote_server_up(self, server: str): - self.send_command(RemoteServerUpCommand(server)) - - def on_connection_closed(self): - BaseReplicationStreamProtocol.on_connection_closed(self) - self.streamer.lost_connection(self) - class ClientReplicationStreamProtocol(BaseReplicationStreamProtocol): VALID_INBOUND_COMMANDS = VALID_SERVER_COMMANDS @@ -485,59 +451,18 @@ class ClientReplicationStreamProtocol(BaseReplicationStreamProtocol): clock: Clock, command_handler: "ReplicationCommandHandler", ): - BaseReplicationStreamProtocol.__init__(self, clock) - - self.instance_id = hs.get_instance_id() + super().__init__(clock, command_handler) self.client_name = client_name self.server_name = server_name - self.handler = command_handler def connectionMade(self): self.send_command(NameCommand(self.client_name)) - BaseReplicationStreamProtocol.connectionMade(self) + super().connectionMade() # Once we've connected subscribe to the necessary streams self.replicate() - # Tell the server if we have any users currently syncing (should only - # happen on synchrotrons) - currently_syncing = self.handler.get_currently_syncing_users() - now = self.clock.time_msec() - for user_id in currently_syncing: - self.send_command(UserSyncCommand(self.instance_id, user_id, True, now)) - - # We've now finished connecting to so inform the client handler - self.handler.update_connection(self) - self.handler.finished_connecting() - - async def handle_command(self, cmd: Command): - """Handle a command we have received over the replication stream. - - Delegates to `command_handler.on_`, which must return an - awaitable. - - Args: - cmd: received command - """ - handled = False - - # First call any command handlers on this instance. These are for TCP - # specific handling. - cmd_func = getattr(self, "on_%s" % (cmd.NAME,), None) - if cmd_func: - await cmd_func(cmd) - handled = True - - # Then call out to the handler. - cmd_func = getattr(self.handler, "on_%s" % (cmd.NAME,), None) - if cmd_func: - await cmd_func(cmd) - handled = True - - if not handled: - logger.warning("Unhandled command: %r", cmd) - async def on_SERVER(self, cmd): if cmd.data != self.server_name: logger.error("[%s] Connected to wrong remote: %r", self.id(), cmd.data) @@ -550,9 +475,21 @@ class ClientReplicationStreamProtocol(BaseReplicationStreamProtocol): self.send_command(ReplicateCommand()) - def on_connection_closed(self): - BaseReplicationStreamProtocol.on_connection_closed(self) - self.handler.update_connection(None) + +class AbstractConnection(abc.ABC): + """An interface for replication connections. + """ + + @abc.abstractmethod + def send_command(self, cmd: Command): + """Send the command down the connection + """ + pass + + +# This tells python that `BaseReplicationStreamProtocol` implements the +# interface. +AbstractConnection.register(BaseReplicationStreamProtocol) # The following simply registers metrics for the replication connections diff --git a/synapse/replication/tcp/resource.py b/synapse/replication/tcp/resource.py index 30021ee309..b2d6baa2a2 100644 --- a/synapse/replication/tcp/resource.py +++ b/synapse/replication/tcp/resource.py @@ -17,7 +17,7 @@ import logging import random -from typing import Any, Dict, List +from typing import Dict from six import itervalues @@ -25,24 +25,14 @@ from prometheus_client import Counter from twisted.internet.protocol import Factory -from synapse.metrics import LaterGauge from synapse.metrics.background_process_metrics import run_as_background_process -from synapse.util.metrics import Measure, measure_func - -from .protocol import ServerReplicationStreamProtocol -from .streams import STREAMS_MAP, Stream -from .streams.federation import FederationStream +from synapse.replication.tcp.protocol import ServerReplicationStreamProtocol +from synapse.replication.tcp.streams import STREAMS_MAP, FederationStream, Stream +from synapse.util.metrics import Measure stream_updates_counter = Counter( "synapse_replication_tcp_resource_stream_updates", "", ["stream_name"] ) -user_sync_counter = Counter("synapse_replication_tcp_resource_user_sync", "") -federation_ack_counter = Counter("synapse_replication_tcp_resource_federation_ack", "") -remove_pusher_counter = Counter("synapse_replication_tcp_resource_remove_pusher", "") -invalidate_cache_counter = Counter( - "synapse_replication_tcp_resource_invalidate_cache", "" -) -user_ip_cache_counter = Counter("synapse_replication_tcp_resource_user_ip_cache", "") logger = logging.getLogger(__name__) @@ -52,13 +42,23 @@ class ReplicationStreamProtocolFactory(Factory): """ def __init__(self, hs): - self.streamer = hs.get_replication_streamer() + self.command_handler = hs.get_tcp_replication() self.clock = hs.get_clock() self.server_name = hs.config.server_name + # If we've created a `ReplicationStreamProtocolFactory` then we're + # almost certainly registering a replication listener, so let's ensure + # that we've started a `ReplicationStreamer` instance to actually push + # data. + # + # (This is a bit of a weird place to do this, but the alternatives such + # as putting this in `HomeServer.setup()`, requires either passing the + # listener config again or always starting a `ReplicationStreamer`.) + hs.get_replication_streamer() + def buildProtocol(self, addr): return ServerReplicationStreamProtocol( - self.server_name, self.clock, self.streamer + self.server_name, self.clock, self.command_handler ) @@ -78,16 +78,6 @@ class ReplicationStreamer(object): self._replication_torture_level = hs.config.replication_torture_level - # Current connections. - self.connections = [] # type: List[ServerReplicationStreamProtocol] - - LaterGauge( - "synapse_replication_tcp_resource_total_connections", - "", - [], - lambda: len(self.connections), - ) - # List of streams that clients can subscribe to. # We only support federation stream if federation sending hase been # disabled on the master. @@ -104,18 +94,12 @@ class ReplicationStreamer(object): self.federation_sender = hs.get_federation_sender() self.notifier.add_replication_callback(self.on_notifier_poke) - self.notifier.add_remote_server_up_callback(self.send_remote_server_up) # Keeps track of whether we are currently checking for updates self.is_looping = False self.pending_updates = False - hs.get_reactor().addSystemEventTrigger("before", "shutdown", self.on_shutdown) - - def on_shutdown(self): - # close all connections on shutdown - for conn in self.connections: - conn.send_error("server shutting down") + self.command_handler = hs.get_tcp_replication() def get_streams(self) -> Dict[str, Stream]: """Get a mapp from stream name to stream instance. @@ -129,7 +113,7 @@ class ReplicationStreamer(object): This should get called each time new data is available, even if it is currently being executed, so that nothing gets missed """ - if not self.connections: + if not self.command_handler.connected(): # Don't bother if nothing is listening. We still need to advance # the stream tokens otherwise they'll fall beihind forever for stream in self.streams: @@ -186,9 +170,7 @@ class ReplicationStreamer(object): raise logger.debug( - "Sending %d updates to %d connections", - len(updates), - len(self.connections), + "Sending %d updates", len(updates), ) if updates: @@ -204,112 +186,19 @@ class ReplicationStreamer(object): # token. See RdataCommand for more details. batched_updates = _batch_updates(updates) - for conn in self.connections: - for token, row in batched_updates: - try: - conn.stream_update(stream.NAME, token, row) - except Exception: - logger.exception("Failed to replicate") + for token, row in batched_updates: + try: + self.command_handler.stream_update( + stream.NAME, token, row + ) + except Exception: + logger.exception("Failed to replicate") logger.debug("No more pending updates, breaking poke loop") finally: self.pending_updates = False self.is_looping = False - def get_stream_token(self, stream_name): - """For a given stream get all updates since token. This is called when - a client first subscribes to a stream. - """ - stream = self.streams_by_name.get(stream_name, None) - if not stream: - raise Exception("unknown stream %s", stream_name) - - return stream.current_token() - - @measure_func("repl.federation_ack") - def federation_ack(self, token): - """We've received an ack for federation stream from a client. - """ - federation_ack_counter.inc() - if self.federation_sender: - self.federation_sender.federation_ack(token) - - @measure_func("repl.on_user_sync") - async def on_user_sync(self, instance_id, user_id, is_syncing, last_sync_ms): - """A client has started/stopped syncing on a worker. - """ - user_sync_counter.inc() - await self.presence_handler.update_external_syncs_row( - instance_id, user_id, is_syncing, last_sync_ms - ) - - async def on_clear_user_syncs(self, instance_id): - """A replication client wants us to drop all their UserSync data. - """ - await self.presence_handler.update_external_syncs_clear(instance_id) - - @measure_func("repl.on_remove_pusher") - async def on_remove_pusher(self, app_id, push_key, user_id): - """A client has asked us to remove a pusher - """ - remove_pusher_counter.inc() - await self.store.delete_pusher_by_app_id_pushkey_user_id( - app_id=app_id, pushkey=push_key, user_id=user_id - ) - - self.notifier.on_new_replication_data() - - @measure_func("repl.on_invalidate_cache") - async def on_invalidate_cache(self, cache_func: str, keys: List[Any]): - """The client has asked us to invalidate a cache - """ - invalidate_cache_counter.inc() - - # We invalidate the cache locally, but then also stream that to other - # workers. - await self.store.invalidate_cache_and_stream(cache_func, tuple(keys)) - - @measure_func("repl.on_user_ip") - async def on_user_ip( - self, user_id, access_token, ip, user_agent, device_id, last_seen - ): - """The client saw a user request - """ - user_ip_cache_counter.inc() - await self.store.insert_client_ip( - user_id, access_token, ip, user_agent, device_id, last_seen - ) - await self._server_notices_sender.on_user_ip(user_id) - - @measure_func("repl.on_remote_server_up") - def on_remote_server_up(self, server: str): - self.notifier.notify_remote_server_up(server) - - def send_remote_server_up(self, server: str): - for conn in self.connections: - conn.send_remote_server_up(server) - - def send_sync_to_all_connections(self, data): - """Sends a SYNC command to all clients. - - Used in tests. - """ - for conn in self.connections: - conn.send_sync(data) - - def new_connection(self, connection): - """A new client connection has been established - """ - self.connections.append(connection) - - def lost_connection(self, connection): - """A client connection has been lost - """ - try: - self.connections.remove(connection) - except ValueError: - pass - def _batch_updates(updates): """Takes a list of updates of form [(token, row)] and sets the token to From ce72355d7f67a986d60a7d86489b1b40f93fb152 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 7 Apr 2020 11:01:04 +0100 Subject: [PATCH 1322/1623] Fix race in replication (#7226) Fixes a race between handling `POSITION` and `RDATA` commands. We do this by simply linearizing handling of them. --- changelog.d/7226.misc | 1 + synapse/replication/tcp/handler.py | 73 ++++++++++++------- synapse/replication/tcp/streams/_base.py | 3 +- synapse/storage/data_stores/main/push_rule.py | 40 +++++----- 4 files changed, 68 insertions(+), 49 deletions(-) create mode 100644 changelog.d/7226.misc diff --git a/changelog.d/7226.misc b/changelog.d/7226.misc new file mode 100644 index 0000000000..676f285377 --- /dev/null +++ b/changelog.d/7226.misc @@ -0,0 +1 @@ +Move catchup of replication streams logic to worker. diff --git a/synapse/replication/tcp/handler.py b/synapse/replication/tcp/handler.py index 8ec0119697..dd71d1bc34 100644 --- a/synapse/replication/tcp/handler.py +++ b/synapse/replication/tcp/handler.py @@ -189,16 +189,34 @@ class ReplicationCommandHandler: logger.exception("Failed to parse RDATA: %r %r", stream_name, cmd.row) raise - if cmd.token is None or stream_name not in self._streams_connected: - # I.e. either this is part of a batch of updates for this stream (in - # which case batch until we get an update for the stream with a non - # None token) or we're currently connecting so we queue up rows. - self._pending_batches.setdefault(stream_name, []).append(row) - else: - # Check if this is the last of a batch of updates - rows = self._pending_batches.pop(stream_name, []) - rows.append(row) - await self.on_rdata(stream_name, cmd.token, rows) + # We linearize here for two reasons: + # 1. so we don't try and concurrently handle multiple rows for the + # same stream, and + # 2. so we don't race with getting a POSITION command and fetching + # missing RDATA. + with await self._position_linearizer.queue(cmd.stream_name): + if stream_name not in self._streams_connected: + # If the stream isn't marked as connected then we haven't seen a + # `POSITION` command yet, and so we may have missed some rows. + # Let's drop the row for now, on the assumption we'll receive a + # `POSITION` soon and we'll catch up correctly then. + logger.warning( + "Discarding RDATA for unconnected stream %s -> %s", + stream_name, + cmd.token, + ) + return + + if cmd.token is None: + # I.e. this is part of a batch of updates for this stream (in + # which case batch until we get an update for the stream with a non + # None token). + self._pending_batches.setdefault(stream_name, []).append(row) + else: + # Check if this is the last of a batch of updates + rows = self._pending_batches.pop(stream_name, []) + rows.append(row) + await self.on_rdata(stream_name, cmd.token, rows) async def on_rdata(self, stream_name: str, token: int, rows: list): """Called to handle a batch of replication data with a given stream token. @@ -221,12 +239,13 @@ class ReplicationCommandHandler: # We protect catching up with a linearizer in case the replication # connection reconnects under us. with await self._position_linearizer.queue(cmd.stream_name): - # We're about to go and catch up with the stream, so mark as connecting - # to stop RDATA being handled at the same time by removing stream from - # list of connected streams. We also clear any batched up RDATA from - # before we got the POSITION. + # We're about to go and catch up with the stream, so remove from set + # of connected streams. self._streams_connected.discard(cmd.stream_name) - self._pending_batches.clear() + + # We clear the pending batches for the stream as the fetching of the + # missing updates below will fetch all rows in the batch. + self._pending_batches.pop(cmd.stream_name, []) # Find where we previously streamed up to. current_token = self._replication_data_handler.get_streams_to_replicate().get( @@ -239,12 +258,17 @@ class ReplicationCommandHandler: ) return - # Fetch all updates between then and now. - limited = True - while limited: - updates, current_token, limited = await stream.get_updates_since( - current_token, cmd.token - ) + # If the position token matches our current token then we're up to + # date and there's nothing to do. Otherwise, fetch all updates + # between then and now. + missing_updates = cmd.token != current_token + while missing_updates: + ( + updates, + current_token, + missing_updates, + ) = await stream.get_updates_since(current_token, cmd.token) + if updates: await self.on_rdata( cmd.stream_name, @@ -255,13 +279,6 @@ class ReplicationCommandHandler: # We've now caught up to position sent to us, notify handler. await self._replication_data_handler.on_position(cmd.stream_name, cmd.token) - # Handle any RDATA that came in while we were catching up. - rows = self._pending_batches.pop(cmd.stream_name, []) - if rows: - await self._replication_data_handler.on_rdata( - cmd.stream_name, rows[-1].token, rows - ) - self._streams_connected.add(cmd.stream_name) async def on_SYNC(self, cmd: SyncCommand): diff --git a/synapse/replication/tcp/streams/_base.py b/synapse/replication/tcp/streams/_base.py index c14dff6c64..f56a0fd4b5 100644 --- a/synapse/replication/tcp/streams/_base.py +++ b/synapse/replication/tcp/streams/_base.py @@ -168,12 +168,13 @@ def make_http_update_function( async def update_function( from_token: int, upto_token: int, limit: int ) -> Tuple[List[Tuple[int, tuple]], int, bool]: - return await client( + result = await client( stream_name=stream_name, from_token=from_token, upto_token=upto_token, limit=limit, ) + return result["updates"], result["upto_token"], result["limited"] return update_function diff --git a/synapse/storage/data_stores/main/push_rule.py b/synapse/storage/data_stores/main/push_rule.py index 46f9bda773..b3faafa0a4 100644 --- a/synapse/storage/data_stores/main/push_rule.py +++ b/synapse/storage/data_stores/main/push_rule.py @@ -334,6 +334,26 @@ class PushRulesWorkerStore( results.setdefault(row["user_name"], {})[row["rule_id"]] = enabled return results + def get_all_push_rule_updates(self, last_id, current_id, limit): + """Get all the push rules changes that have happend on the server""" + if last_id == current_id: + return defer.succeed([]) + + def get_all_push_rule_updates_txn(txn): + sql = ( + "SELECT stream_id, event_stream_ordering, user_id, rule_id," + " op, priority_class, priority, conditions, actions" + " FROM push_rules_stream" + " WHERE ? < stream_id AND stream_id <= ?" + " ORDER BY stream_id ASC LIMIT ?" + ) + txn.execute(sql, (last_id, current_id, limit)) + return txn.fetchall() + + return self.db.runInteraction( + "get_all_push_rule_updates", get_all_push_rule_updates_txn + ) + class PushRuleStore(PushRulesWorkerStore): @defer.inlineCallbacks @@ -685,26 +705,6 @@ class PushRuleStore(PushRulesWorkerStore): self.push_rules_stream_cache.entity_has_changed, user_id, stream_id ) - def get_all_push_rule_updates(self, last_id, current_id, limit): - """Get all the push rules changes that have happend on the server""" - if last_id == current_id: - return defer.succeed([]) - - def get_all_push_rule_updates_txn(txn): - sql = ( - "SELECT stream_id, event_stream_ordering, user_id, rule_id," - " op, priority_class, priority, conditions, actions" - " FROM push_rules_stream" - " WHERE ? < stream_id AND stream_id <= ?" - " ORDER BY stream_id ASC LIMIT ?" - ) - txn.execute(sql, (last_id, current_id, limit)) - return txn.fetchall() - - return self.db.runInteraction( - "get_all_push_rule_updates", get_all_push_rule_updates_txn - ) - def get_push_rules_stream_token(self): """Get the position of the push rules stream. Returns a pair of a stream id for the push_rules stream and the From 2e105c156be036ebd408b8fbb87b5c218574726e Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 7 Apr 2020 15:19:19 +0100 Subject: [PATCH 1323/1623] Remove sent outbound device list pokes from the database (#7192) They just get in the way. --- changelog.d/7192.misc | 1 + synapse/storage/data_stores/main/devices.py | 4 ++-- .../delta/57/remove_sent_outbound_pokes.sql | 21 +++++++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 changelog.d/7192.misc create mode 100644 synapse/storage/data_stores/main/schema/delta/57/remove_sent_outbound_pokes.sql diff --git a/changelog.d/7192.misc b/changelog.d/7192.misc new file mode 100644 index 0000000000..e401e36399 --- /dev/null +++ b/changelog.d/7192.misc @@ -0,0 +1 @@ +Remove sent outbound device list pokes from the database. diff --git a/synapse/storage/data_stores/main/devices.py b/synapse/storage/data_stores/main/devices.py index dd3561e9b2..4c5bea4a5c 100644 --- a/synapse/storage/data_stores/main/devices.py +++ b/synapse/storage/data_stores/main/devices.py @@ -227,11 +227,11 @@ class DeviceWorkerStore(SQLBaseStore): # get the list of device updates that need to be sent sql = """ SELECT user_id, device_id, stream_id, opentracing_context FROM device_lists_outbound_pokes - WHERE destination = ? AND ? < stream_id AND stream_id <= ? AND sent = ? + WHERE destination = ? AND ? < stream_id AND stream_id <= ? ORDER BY stream_id LIMIT ? """ - txn.execute(sql, (destination, from_stream_id, now_stream_id, False, limit)) + txn.execute(sql, (destination, from_stream_id, now_stream_id, limit)) return list(txn) diff --git a/synapse/storage/data_stores/main/schema/delta/57/remove_sent_outbound_pokes.sql b/synapse/storage/data_stores/main/schema/delta/57/remove_sent_outbound_pokes.sql new file mode 100644 index 0000000000..133d80af35 --- /dev/null +++ b/synapse/storage/data_stores/main/schema/delta/57/remove_sent_outbound_pokes.sql @@ -0,0 +1,21 @@ +/* Copyright 2020 The Matrix.org Foundation C.I.C + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +-- we no longer keep sent outbound device pokes in the db; clear them out +-- so that we don't have to worry about them. +-- +-- This is a sequence scan, but it doesn't take too long. + +DELETE FROM device_lists_outbound_pokes WHERE sent; From ec5ac8e2b129f645a06f441143c2dcd2fb1c7037 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 7 Apr 2020 18:31:50 +0200 Subject: [PATCH 1324/1623] Fix typo in the login fallback javascript (#7235) * Fix typo in the login fallback javascript * Changelog --- changelog.d/7235.bugfix | 1 + synapse/static/client/login/js/login.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/7235.bugfix diff --git a/changelog.d/7235.bugfix b/changelog.d/7235.bugfix new file mode 100644 index 0000000000..d185efe537 --- /dev/null +++ b/changelog.d/7235.bugfix @@ -0,0 +1 @@ +Fix a bug causing the login fallback to not display the SSO login form. diff --git a/synapse/static/client/login/js/login.js b/synapse/static/client/login/js/login.js index debe464371..5ca0317755 100644 --- a/synapse/static/client/login/js/login.js +++ b/synapse/static/client/login/js/login.js @@ -62,7 +62,7 @@ var show_login = function(inhibit_redirect) { } // Otherwise, show the SSO form - $("#sso_form").show(); + $("#sso_flow").show(); } if (matrixLogin.serverAcceptsPassword) { From 6a519a0ca06f42c35d5c88f3fa5f62cfd1553905 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 7 Apr 2020 16:59:40 +0100 Subject: [PATCH 1325/1623] Remove vestigal references to SYNC replication command We've ripped pretty much all of this out: let's remove the remains. --- synapse/replication/tcp/commands.py | 10 ---------- synapse/replication/tcp/handler.py | 4 ---- 2 files changed, 14 deletions(-) diff --git a/synapse/replication/tcp/commands.py b/synapse/replication/tcp/commands.py index e4eec643f7..a07a01278a 100644 --- a/synapse/replication/tcp/commands.py +++ b/synapse/replication/tcp/commands.py @@ -289,14 +289,6 @@ class FederationAckCommand(Command): return str(self.token) -class SyncCommand(Command): - """Used for testing. The client protocol implementation allows waiting - on a SYNC command with a specified data. - """ - - NAME = "SYNC" - - class RemovePusherCommand(Command): """Sent by the client to request the master remove the given pusher. @@ -419,7 +411,6 @@ _COMMANDS = ( ReplicateCommand, UserSyncCommand, FederationAckCommand, - SyncCommand, RemovePusherCommand, InvalidateCacheCommand, UserIpCommand, @@ -437,7 +428,6 @@ VALID_SERVER_COMMANDS = ( PositionCommand.NAME, ErrorCommand.NAME, PingCommand.NAME, - SyncCommand.NAME, RemoteServerUpCommand.NAME, ) diff --git a/synapse/replication/tcp/handler.py b/synapse/replication/tcp/handler.py index dd71d1bc34..2f5a299141 100644 --- a/synapse/replication/tcp/handler.py +++ b/synapse/replication/tcp/handler.py @@ -31,7 +31,6 @@ from synapse.replication.tcp.commands import ( RemoteServerUpCommand, RemovePusherCommand, ReplicateCommand, - SyncCommand, UserIpCommand, UserSyncCommand, ) @@ -281,9 +280,6 @@ class ReplicationCommandHandler: self._streams_connected.add(cmd.stream_name) - async def on_SYNC(self, cmd: SyncCommand): - pass - async def on_REMOTE_SERVER_UP(self, cmd: RemoteServerUpCommand): """"Called when get a new REMOTE_SERVER_UP command.""" self._replication_data_handler.on_remote_server_up(cmd.data) From c3e4b4edb270ad31321207125007ff41344510ed Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 7 Apr 2020 17:40:22 +0100 Subject: [PATCH 1326/1623] Fix warnings about not calling superclass constructor Separate `SimpleCommand` from `Command`, so that things which don't want to use the `data` property don't have to, and thus fix the warnings PyCharm was giving me about not calling `__init__` in the base class. --- synapse/replication/tcp/commands.py | 39 ++++++++++++++++++----------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/synapse/replication/tcp/commands.py b/synapse/replication/tcp/commands.py index a07a01278a..5ec89d0fb8 100644 --- a/synapse/replication/tcp/commands.py +++ b/synapse/replication/tcp/commands.py @@ -17,7 +17,7 @@ The VALID_SERVER_COMMANDS and VALID_CLIENT_COMMANDS define which commands are allowed to be sent by which side. """ - +import abc import logging import platform from typing import Tuple, Type @@ -34,34 +34,29 @@ else: logger = logging.getLogger(__name__) -class Command(object): +class Command(metaclass=abc.ABCMeta): """The base command class. All subclasses must set the NAME variable which equates to the name of the command on the wire. A full command line on the wire is constructed from `NAME + " " + to_line()` - - The default implementation creates a command of form ` ` """ NAME = None # type: str - def __init__(self, data): - self.data = data - @classmethod + @abc.abstractmethod def from_line(cls, line): """Deserialises a line from the wire into this command. `line` does not include the command. """ - return cls(line) - def to_line(self): + @abc.abstractmethod + def to_line(self) -> str: """Serialises the comamnd for the wire. Does not include the command prefix. """ - return self.data def get_logcontext_id(self): """Get a suitable string for the logcontext when processing this command""" @@ -70,7 +65,21 @@ class Command(object): return self.NAME -class ServerCommand(Command): +class _SimpleCommand(Command): + """An implementation of Command whose argument is just a 'data' string.""" + + def __init__(self, data): + self.data = data + + @classmethod + def from_line(cls, line): + return cls(line) + + def to_line(self) -> str: + return self.data + + +class ServerCommand(_SimpleCommand): """Sent by the server on new connection and includes the server_name. Format:: @@ -155,7 +164,7 @@ class PositionCommand(Command): return " ".join((self.stream_name, str(self.token))) -class ErrorCommand(Command): +class ErrorCommand(_SimpleCommand): """Sent by either side if there was an ERROR. The data is a string describing the error. """ @@ -163,14 +172,14 @@ class ErrorCommand(Command): NAME = "ERROR" -class PingCommand(Command): +class PingCommand(_SimpleCommand): """Sent by either side as a keep alive. The data is arbitary (often timestamp) """ NAME = "PING" -class NameCommand(Command): +class NameCommand(_SimpleCommand): """Sent by client to inform the server of the client's identity. The data is the name """ @@ -387,7 +396,7 @@ class UserIpCommand(Command): ) -class RemoteServerUpCommand(Command): +class RemoteServerUpCommand(_SimpleCommand): """Sent when a worker has detected that a remote server is no longer "down" and retry timings should be reset. From e13c6c7a96dc71200eef9f966bee21c27ae54426 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 7 Apr 2020 17:21:13 +0100 Subject: [PATCH 1327/1623] Handle one-word replication commands correctly `REPLICATE` is now a valid command, and it's nice if you can issue it from the console without remembering to call it `REPLICATE ` with a trailing space. --- synapse/replication/tcp/protocol.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/synapse/replication/tcp/protocol.py b/synapse/replication/tcp/protocol.py index 9aabb9c586..9276ed2965 100644 --- a/synapse/replication/tcp/protocol.py +++ b/synapse/replication/tcp/protocol.py @@ -201,15 +201,23 @@ class BaseReplicationStreamProtocol(LineOnlyReceiver): ) self.send_error("ping timeout") - def lineReceived(self, line): + def lineReceived(self, line: bytes): """Called when we've received a line """ if line.strip() == "": # Ignore blank lines return - line = line.decode("utf-8") - cmd_name, rest_of_line = line.split(" ", 1) + linestr = line.decode("utf-8") + + # split at the first " ", handling one-word commands + idx = linestr.index(" ") + if idx >= 0: + cmd_name = linestr[:idx] + rest_of_line = linestr[idx + 1 :] + else: + cmd_name = linestr + rest_of_line = "" if cmd_name not in self.VALID_INBOUND_COMMANDS: logger.error("[%s] invalid command %s", self.id(), cmd_name) From bd2ea3432b617537a2596f1704de4478cda60dde Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 7 Apr 2020 17:44:51 +0100 Subject: [PATCH 1328/1623] changelog --- changelog.d/7329.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7329.misc diff --git a/changelog.d/7329.misc b/changelog.d/7329.misc new file mode 100644 index 0000000000..676f285377 --- /dev/null +++ b/changelog.d/7329.misc @@ -0,0 +1 @@ +Move catchup of replication streams logic to worker. From d78cb31588e01468ab06a36e6120a80fb6fbf097 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 7 Apr 2020 15:03:23 -0400 Subject: [PATCH 1329/1623] Add typing information to federation_server. (#7219) --- changelog.d/7219.misc | 1 + synapse/federation/federation_server.py | 171 +++++++++++++++--------- tox.ini | 1 + 3 files changed, 108 insertions(+), 65 deletions(-) create mode 100644 changelog.d/7219.misc diff --git a/changelog.d/7219.misc b/changelog.d/7219.misc new file mode 100644 index 0000000000..4af5da8646 --- /dev/null +++ b/changelog.d/7219.misc @@ -0,0 +1 @@ +Add typing information to federation server code. diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index 89d521bc31..32a8a2ee46 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -15,7 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import logging -from typing import Dict +from typing import Any, Callable, Dict, List, Match, Optional, Tuple, Union import six from six import iteritems @@ -38,6 +38,7 @@ from synapse.api.errors import ( UnsupportedRoomVersionError, ) from synapse.api.room_versions import KNOWN_ROOM_VERSIONS +from synapse.events import EventBase from synapse.federation.federation_base import FederationBase, event_from_pdu_json from synapse.federation.persistence import TransactionActions from synapse.federation.units import Edu, Transaction @@ -94,7 +95,9 @@ class FederationServer(FederationBase): # come in waves. self._state_resp_cache = ResponseCache(hs, "state_resp", timeout_ms=30000) - async def on_backfill_request(self, origin, room_id, versions, limit): + async def on_backfill_request( + self, origin: str, room_id: str, versions: List[str], limit: int + ) -> Tuple[int, Dict[str, Any]]: with (await self._server_linearizer.queue((origin, room_id))): origin_host, _ = parse_server_name(origin) await self.check_server_matches_acl(origin_host, room_id) @@ -107,23 +110,25 @@ class FederationServer(FederationBase): return 200, res - async def on_incoming_transaction(self, origin, transaction_data): + async def on_incoming_transaction( + self, origin: str, transaction_data: JsonDict + ) -> Tuple[int, Dict[str, Any]]: # keep this as early as possible to make the calculated origin ts as # accurate as possible. request_time = self._clock.time_msec() transaction = Transaction(**transaction_data) - if not transaction.transaction_id: + if not transaction.transaction_id: # type: ignore raise Exception("Transaction missing transaction_id") - logger.debug("[%s] Got transaction", transaction.transaction_id) + logger.debug("[%s] Got transaction", transaction.transaction_id) # type: ignore # use a linearizer to ensure that we don't process the same transaction # multiple times in parallel. with ( await self._transaction_linearizer.queue( - (origin, transaction.transaction_id) + (origin, transaction.transaction_id) # type: ignore ) ): result = await self._handle_incoming_transaction( @@ -132,31 +137,33 @@ class FederationServer(FederationBase): return result - async def _handle_incoming_transaction(self, origin, transaction, request_time): + async def _handle_incoming_transaction( + self, origin: str, transaction: Transaction, request_time: int + ) -> Tuple[int, Dict[str, Any]]: """ Process an incoming transaction and return the HTTP response Args: - origin (unicode): the server making the request - transaction (Transaction): incoming transaction - request_time (int): timestamp that the HTTP request arrived at + origin: the server making the request + transaction: incoming transaction + request_time: timestamp that the HTTP request arrived at Returns: - Deferred[(int, object)]: http response code and body + HTTP response code and body """ response = await self.transaction_actions.have_responded(origin, transaction) if response: logger.debug( "[%s] We've already responded to this request", - transaction.transaction_id, + transaction.transaction_id, # type: ignore ) return response - logger.debug("[%s] Transaction is new", transaction.transaction_id) + logger.debug("[%s] Transaction is new", transaction.transaction_id) # type: ignore # Reject if PDU count > 50 or EDU count > 100 - if len(transaction.pdus) > 50 or ( - hasattr(transaction, "edus") and len(transaction.edus) > 100 + if len(transaction.pdus) > 50 or ( # type: ignore + hasattr(transaction, "edus") and len(transaction.edus) > 100 # type: ignore ): logger.info("Transaction PDU or EDU count too large. Returning 400") @@ -204,13 +211,13 @@ class FederationServer(FederationBase): report back to the sending server. """ - received_pdus_counter.inc(len(transaction.pdus)) + received_pdus_counter.inc(len(transaction.pdus)) # type: ignore origin_host, _ = parse_server_name(origin) - pdus_by_room = {} + pdus_by_room = {} # type: Dict[str, List[EventBase]] - for p in transaction.pdus: + for p in transaction.pdus: # type: ignore if "unsigned" in p: unsigned = p["unsigned"] if "age" in unsigned: @@ -254,7 +261,7 @@ class FederationServer(FederationBase): # require callouts to other servers to fetch missing events), but # impose a limit to avoid going too crazy with ram/cpu. - async def process_pdus_for_room(room_id): + async def process_pdus_for_room(room_id: str): logger.debug("Processing PDUs for %s", room_id) try: await self.check_server_matches_acl(origin_host, room_id) @@ -310,7 +317,9 @@ class FederationServer(FederationBase): TRANSACTION_CONCURRENCY_LIMIT, ) - async def on_context_state_request(self, origin, room_id, event_id): + async def on_context_state_request( + self, origin: str, room_id: str, event_id: str + ) -> Tuple[int, Dict[str, Any]]: origin_host, _ = parse_server_name(origin) await self.check_server_matches_acl(origin_host, room_id) @@ -338,7 +347,9 @@ class FederationServer(FederationBase): return 200, resp - async def on_state_ids_request(self, origin, room_id, event_id): + async def on_state_ids_request( + self, origin: str, room_id: str, event_id: str + ) -> Tuple[int, Dict[str, Any]]: if not event_id: raise NotImplementedError("Specify an event") @@ -354,7 +365,9 @@ class FederationServer(FederationBase): return 200, {"pdu_ids": state_ids, "auth_chain_ids": auth_chain_ids} - async def _on_context_state_request_compute(self, room_id, event_id): + async def _on_context_state_request_compute( + self, room_id: str, event_id: str + ) -> Dict[str, list]: if event_id: pdus = await self.handler.get_state_for_pdu(room_id, event_id) else: @@ -367,7 +380,9 @@ class FederationServer(FederationBase): "auth_chain": [pdu.get_pdu_json() for pdu in auth_chain], } - async def on_pdu_request(self, origin, event_id): + async def on_pdu_request( + self, origin: str, event_id: str + ) -> Tuple[int, Union[JsonDict, str]]: pdu = await self.handler.get_persisted_pdu(origin, event_id) if pdu: @@ -375,12 +390,16 @@ class FederationServer(FederationBase): else: return 404, "" - async def on_query_request(self, query_type, args): + async def on_query_request( + self, query_type: str, args: Dict[str, str] + ) -> Tuple[int, Dict[str, Any]]: received_queries_counter.labels(query_type).inc() resp = await self.registry.on_query(query_type, args) return 200, resp - async def on_make_join_request(self, origin, room_id, user_id, supported_versions): + async def on_make_join_request( + self, origin: str, room_id: str, user_id: str, supported_versions: List[str] + ) -> Dict[str, Any]: origin_host, _ = parse_server_name(origin) await self.check_server_matches_acl(origin_host, room_id) @@ -397,7 +416,7 @@ class FederationServer(FederationBase): async def on_invite_request( self, origin: str, content: JsonDict, room_version_id: str - ): + ) -> Dict[str, Any]: room_version = KNOWN_ROOM_VERSIONS.get(room_version_id) if not room_version: raise SynapseError( @@ -414,7 +433,9 @@ class FederationServer(FederationBase): time_now = self._clock.time_msec() return {"event": ret_pdu.get_pdu_json(time_now)} - async def on_send_join_request(self, origin, content, room_id): + async def on_send_join_request( + self, origin: str, content: JsonDict, room_id: str + ) -> Dict[str, Any]: logger.debug("on_send_join_request: content: %s", content) room_version = await self.store.get_room_version(room_id) @@ -434,7 +455,9 @@ class FederationServer(FederationBase): "auth_chain": [p.get_pdu_json(time_now) for p in res_pdus["auth_chain"]], } - async def on_make_leave_request(self, origin, room_id, user_id): + async def on_make_leave_request( + self, origin: str, room_id: str, user_id: str + ) -> Dict[str, Any]: origin_host, _ = parse_server_name(origin) await self.check_server_matches_acl(origin_host, room_id) pdu = await self.handler.on_make_leave_request(origin, room_id, user_id) @@ -444,7 +467,9 @@ class FederationServer(FederationBase): time_now = self._clock.time_msec() return {"event": pdu.get_pdu_json(time_now), "room_version": room_version} - async def on_send_leave_request(self, origin, content, room_id): + async def on_send_leave_request( + self, origin: str, content: JsonDict, room_id: str + ) -> dict: logger.debug("on_send_leave_request: content: %s", content) room_version = await self.store.get_room_version(room_id) @@ -460,7 +485,9 @@ class FederationServer(FederationBase): await self.handler.on_send_leave_request(origin, pdu) return {} - async def on_event_auth(self, origin, room_id, event_id): + async def on_event_auth( + self, origin: str, room_id: str, event_id: str + ) -> Tuple[int, Dict[str, Any]]: with (await self._server_linearizer.queue((origin, room_id))): origin_host, _ = parse_server_name(origin) await self.check_server_matches_acl(origin_host, room_id) @@ -471,15 +498,21 @@ class FederationServer(FederationBase): return 200, res @log_function - def on_query_client_keys(self, origin, content): - return self.on_query_request("client_keys", content) + async def on_query_client_keys( + self, origin: str, content: Dict[str, str] + ) -> Tuple[int, Dict[str, Any]]: + return await self.on_query_request("client_keys", content) - async def on_query_user_devices(self, origin: str, user_id: str): + async def on_query_user_devices( + self, origin: str, user_id: str + ) -> Tuple[int, Dict[str, Any]]: keys = await self.device_handler.on_federation_query_user_devices(user_id) return 200, keys @trace - async def on_claim_client_keys(self, origin, content): + async def on_claim_client_keys( + self, origin: str, content: JsonDict + ) -> Dict[str, Any]: query = [] for user_id, device_keys in content.get("one_time_keys", {}).items(): for device_id, algorithm in device_keys.items(): @@ -488,7 +521,7 @@ class FederationServer(FederationBase): log_kv({"message": "Claiming one time keys.", "user, device pairs": query}) results = await self.store.claim_e2e_one_time_keys(query) - json_result = {} + json_result = {} # type: Dict[str, Dict[str, dict]] for user_id, device_keys in results.items(): for device_id, keys in device_keys.items(): for key_id, json_bytes in keys.items(): @@ -511,8 +544,13 @@ class FederationServer(FederationBase): return {"one_time_keys": json_result} async def on_get_missing_events( - self, origin, room_id, earliest_events, latest_events, limit - ): + self, + origin: str, + room_id: str, + earliest_events: List[str], + latest_events: List[str], + limit: int, + ) -> Dict[str, list]: with (await self._server_linearizer.queue((origin, room_id))): origin_host, _ = parse_server_name(origin) await self.check_server_matches_acl(origin_host, room_id) @@ -541,11 +579,11 @@ class FederationServer(FederationBase): return {"events": [ev.get_pdu_json(time_now) for ev in missing_events]} @log_function - def on_openid_userinfo(self, token): + async def on_openid_userinfo(self, token: str) -> Optional[str]: ts_now_ms = self._clock.time_msec() - return self.store.get_user_id_for_open_id_token(token, ts_now_ms) + return await self.store.get_user_id_for_open_id_token(token, ts_now_ms) - def _transaction_from_pdus(self, pdu_list): + def _transaction_from_pdus(self, pdu_list: List[EventBase]) -> Transaction: """Returns a new Transaction containing the given PDUs suitable for transmission. """ @@ -558,7 +596,7 @@ class FederationServer(FederationBase): destination=None, ) - async def _handle_received_pdu(self, origin, pdu): + async def _handle_received_pdu(self, origin: str, pdu: EventBase) -> None: """ Process a PDU received in a federation /send/ transaction. If the event is invalid, then this method throws a FederationError. @@ -579,10 +617,8 @@ class FederationServer(FederationBase): until we try to backfill across the discontinuity. Args: - origin (str): server which sent the pdu - pdu (FrozenEvent): received pdu - - Returns (Deferred): completes with None + origin: server which sent the pdu + pdu: received pdu Raises: FederationError if the signatures / hash do not match, or if the event was unacceptable for any other reason (eg, too large, @@ -625,25 +661,27 @@ class FederationServer(FederationBase): return "" % self.server_name async def exchange_third_party_invite( - self, sender_user_id, target_user_id, room_id, signed + self, sender_user_id: str, target_user_id: str, room_id: str, signed: Dict ): ret = await self.handler.exchange_third_party_invite( sender_user_id, target_user_id, room_id, signed ) return ret - async def on_exchange_third_party_invite_request(self, room_id, event_dict): + async def on_exchange_third_party_invite_request( + self, room_id: str, event_dict: Dict + ): ret = await self.handler.on_exchange_third_party_invite_request( room_id, event_dict ) return ret - async def check_server_matches_acl(self, server_name, room_id): + async def check_server_matches_acl(self, server_name: str, room_id: str): """Check if the given server is allowed by the server ACLs in the room Args: - server_name (str): name of server, *without any port part* - room_id (str): ID of the room to check + server_name: name of server, *without any port part* + room_id: ID of the room to check Raises: AuthError if the server does not match the ACL @@ -661,15 +699,15 @@ class FederationServer(FederationBase): raise AuthError(code=403, msg="Server is banned from room") -def server_matches_acl_event(server_name, acl_event): +def server_matches_acl_event(server_name: str, acl_event: EventBase) -> bool: """Check if the given server is allowed by the ACL event Args: - server_name (str): name of server, without any port part - acl_event (EventBase): m.room.server_acl event + server_name: name of server, without any port part + acl_event: m.room.server_acl event Returns: - bool: True if this server is allowed by the ACLs + True if this server is allowed by the ACLs """ logger.debug("Checking %s against acl %s", server_name, acl_event.content) @@ -713,7 +751,7 @@ def server_matches_acl_event(server_name, acl_event): return False -def _acl_entry_matches(server_name, acl_entry): +def _acl_entry_matches(server_name: str, acl_entry: str) -> Match: if not isinstance(acl_entry, six.string_types): logger.warning( "Ignoring non-str ACL entry '%s' (is %s)", acl_entry, type(acl_entry) @@ -732,13 +770,13 @@ class FederationHandlerRegistry(object): self.edu_handlers = {} self.query_handlers = {} - def register_edu_handler(self, edu_type, handler): + def register_edu_handler(self, edu_type: str, handler: Callable[[str, dict], None]): """Sets the handler callable that will be used to handle an incoming federation EDU of the given type. Args: - edu_type (str): The type of the incoming EDU to register handler for - handler (Callable[[str, dict]]): A callable invoked on incoming EDU + edu_type: The type of the incoming EDU to register handler for + handler: A callable invoked on incoming EDU of the given type. The arguments are the origin server name and the EDU contents. """ @@ -749,14 +787,16 @@ class FederationHandlerRegistry(object): self.edu_handlers[edu_type] = handler - def register_query_handler(self, query_type, handler): + def register_query_handler( + self, query_type: str, handler: Callable[[dict], defer.Deferred] + ): """Sets the handler callable that will be used to handle an incoming federation query of the given type. Args: - query_type (str): Category name of the query, which should match + query_type: Category name of the query, which should match the string used by make_query. - handler (Callable[[dict], Deferred[dict]]): Invoked to handle + handler: Invoked to handle incoming queries of this type. The return will be yielded on and the result used as the response to the query request. """ @@ -767,10 +807,11 @@ class FederationHandlerRegistry(object): self.query_handlers[query_type] = handler - async def on_edu(self, edu_type, origin, content): + async def on_edu(self, edu_type: str, origin: str, content: dict): handler = self.edu_handlers.get(edu_type) if not handler: logger.warning("No handler registered for EDU type %s", edu_type) + return with start_active_span_from_edu(content, "handle_edu"): try: @@ -780,7 +821,7 @@ class FederationHandlerRegistry(object): except Exception: logger.exception("Failed to handle edu %r", edu_type) - def on_query(self, query_type, args): + def on_query(self, query_type: str, args: dict) -> defer.Deferred: handler = self.query_handlers.get(query_type) if not handler: logger.warning("No handler registered for query type %s", query_type) @@ -807,7 +848,7 @@ class ReplicationFederationHandlerRegistry(FederationHandlerRegistry): super(ReplicationFederationHandlerRegistry, self).__init__() - async def on_edu(self, edu_type, origin, content): + async def on_edu(self, edu_type: str, origin: str, content: dict): """Overrides FederationHandlerRegistry """ if not self.config.use_presence and edu_type == "m.presence": @@ -821,7 +862,7 @@ class ReplicationFederationHandlerRegistry(FederationHandlerRegistry): return await self._send_edu(edu_type=edu_type, origin=origin, content=content) - async def on_query(self, query_type, args): + async def on_query(self, query_type: str, args: dict): """Overrides FederationHandlerRegistry """ handler = self.query_handlers.get(query_type) diff --git a/tox.ini b/tox.ini index a79fc93b57..763c8463d9 100644 --- a/tox.ini +++ b/tox.ini @@ -183,6 +183,7 @@ commands = mypy \ synapse/events/spamcheck.py \ synapse/federation/federation_base.py \ synapse/federation/federation_client.py \ + synapse/federation/federation_server.py \ synapse/federation/sender \ synapse/federation/transport \ synapse/handlers/auth.py \ From 1722b8a527b8caa0f76706bf4acaf240e167daf4 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 7 Apr 2020 16:56:34 -0400 Subject: [PATCH 1330/1623] Convert delete_url_cache_media to async/await. (#7241) --- changelog.d/7241.misc | 1 + synapse/storage/data_stores/main/media_repository.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changelog.d/7241.misc diff --git a/changelog.d/7241.misc b/changelog.d/7241.misc new file mode 100644 index 0000000000..fac5bc0403 --- /dev/null +++ b/changelog.d/7241.misc @@ -0,0 +1 @@ +Convert some of synapse.rest.media to async/await. diff --git a/synapse/storage/data_stores/main/media_repository.py b/synapse/storage/data_stores/main/media_repository.py index cf195f8aa6..8aecd414c2 100644 --- a/synapse/storage/data_stores/main/media_repository.py +++ b/synapse/storage/data_stores/main/media_repository.py @@ -367,7 +367,7 @@ class MediaRepositoryStore(MediaRepositoryBackgroundUpdateStore): "get_url_cache_media_before", _get_url_cache_media_before_txn ) - def delete_url_cache_media(self, media_ids): + async def delete_url_cache_media(self, media_ids): if len(media_ids) == 0: return @@ -380,6 +380,6 @@ class MediaRepositoryStore(MediaRepositoryBackgroundUpdateStore): txn.executemany(sql, [(media_id,) for media_id in media_ids]) - return self.db.runInteraction( + return await self.db.runInteraction( "delete_url_cache_media", _delete_url_cache_media_txn ) From f31e65a749f84f8b3278c91784509d908d4fb342 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 7 Apr 2020 23:06:39 +0100 Subject: [PATCH 1331/1623] bg update to clear out duplicate outbound_device_list_pokes (#7193) We seem to have some duplicates, which could do with being cleared out. --- changelog.d/7193.misc | 1 + .../storage/data_stores/main/client_ips.py | 16 ++-- synapse/storage/data_stores/main/devices.py | 73 +++++++++++++++- .../delta/58/02remove_dup_outbound_pokes.sql | 22 +++++ synapse/storage/database.py | 83 ++++++++++++++++++- tests/storage/test_database.py | 52 ++++++++++++ 6 files changed, 234 insertions(+), 13 deletions(-) create mode 100644 changelog.d/7193.misc create mode 100644 synapse/storage/data_stores/main/schema/delta/58/02remove_dup_outbound_pokes.sql create mode 100644 tests/storage/test_database.py diff --git a/changelog.d/7193.misc b/changelog.d/7193.misc new file mode 100644 index 0000000000..383a738e64 --- /dev/null +++ b/changelog.d/7193.misc @@ -0,0 +1 @@ +Add a background database update job to clear out duplicate `device_lists_outbound_pokes`. diff --git a/synapse/storage/data_stores/main/client_ips.py b/synapse/storage/data_stores/main/client_ips.py index e1ccb27142..92bc06919b 100644 --- a/synapse/storage/data_stores/main/client_ips.py +++ b/synapse/storage/data_stores/main/client_ips.py @@ -21,7 +21,7 @@ from twisted.internet import defer from synapse.metrics.background_process_metrics import wrap_as_background_process from synapse.storage._base import SQLBaseStore -from synapse.storage.database import Database +from synapse.storage.database import Database, make_tuple_comparison_clause from synapse.util.caches import CACHE_SIZE_FACTOR from synapse.util.caches.descriptors import Cache @@ -303,16 +303,10 @@ class ClientIpBackgroundUpdateStore(SQLBaseStore): # we'll just end up updating the same device row multiple # times, which is fine. - if self.database_engine.supports_tuple_comparison: - where_clause = "(user_id, device_id) > (?, ?)" - where_args = [last_user_id, last_device_id] - else: - # We explicitly do a `user_id >= ? AND (...)` here to ensure - # that an index is used, as doing `user_id > ? OR (user_id = ? AND ...)` - # makes it hard for query optimiser to tell that it can use the - # index on user_id - where_clause = "user_id >= ? AND (user_id > ? OR device_id > ?)" - where_args = [last_user_id, last_user_id, last_device_id] + where_clause, where_args = make_tuple_comparison_clause( + self.database_engine, + [("user_id", last_user_id), ("device_id", last_device_id)], + ) sql = """ SELECT diff --git a/synapse/storage/data_stores/main/devices.py b/synapse/storage/data_stores/main/devices.py index 4c5bea4a5c..ee3a2ab031 100644 --- a/synapse/storage/data_stores/main/devices.py +++ b/synapse/storage/data_stores/main/devices.py @@ -32,7 +32,11 @@ from synapse.logging.opentracing import ( ) from synapse.metrics.background_process_metrics import run_as_background_process from synapse.storage._base import SQLBaseStore, db_to_json, make_in_list_sql_clause -from synapse.storage.database import Database, LoggingTransaction +from synapse.storage.database import ( + Database, + LoggingTransaction, + make_tuple_comparison_clause, +) from synapse.types import Collection, get_verify_key_from_cross_signing_key from synapse.util.caches.descriptors import ( Cache, @@ -49,6 +53,8 @@ DROP_DEVICE_LIST_STREAMS_NON_UNIQUE_INDEXES = ( "drop_device_list_streams_non_unique_indexes" ) +BG_UPDATE_REMOVE_DUP_OUTBOUND_POKES = "remove_dup_outbound_pokes" + class DeviceWorkerStore(SQLBaseStore): def get_device(self, user_id, device_id): @@ -714,6 +720,11 @@ class DeviceBackgroundUpdateStore(SQLBaseStore): self._drop_device_list_streams_non_unique_indexes, ) + # clear out duplicate device list outbound pokes + self.db.updates.register_background_update_handler( + BG_UPDATE_REMOVE_DUP_OUTBOUND_POKES, self._remove_duplicate_outbound_pokes, + ) + @defer.inlineCallbacks def _drop_device_list_streams_non_unique_indexes(self, progress, batch_size): def f(conn): @@ -728,6 +739,66 @@ class DeviceBackgroundUpdateStore(SQLBaseStore): ) return 1 + async def _remove_duplicate_outbound_pokes(self, progress, batch_size): + # for some reason, we have accumulated duplicate entries in + # device_lists_outbound_pokes, which makes prune_outbound_device_list_pokes less + # efficient. + # + # For each duplicate, we delete all the existing rows and put one back. + + KEY_COLS = ["stream_id", "destination", "user_id", "device_id"] + last_row = progress.get( + "last_row", + {"stream_id": 0, "destination": "", "user_id": "", "device_id": ""}, + ) + + def _txn(txn): + clause, args = make_tuple_comparison_clause( + self.db.engine, [(x, last_row[x]) for x in KEY_COLS] + ) + sql = """ + SELECT stream_id, destination, user_id, device_id, MAX(ts) AS ts + FROM device_lists_outbound_pokes + WHERE %s + GROUP BY %s + HAVING count(*) > 1 + ORDER BY %s + LIMIT ? + """ % ( + clause, # WHERE + ",".join(KEY_COLS), # GROUP BY + ",".join(KEY_COLS), # ORDER BY + ) + txn.execute(sql, args + [batch_size]) + rows = self.db.cursor_to_dict(txn) + + row = None + for row in rows: + self.db.simple_delete_txn( + txn, "device_lists_outbound_pokes", {x: row[x] for x in KEY_COLS}, + ) + + row["sent"] = False + self.db.simple_insert_txn( + txn, "device_lists_outbound_pokes", row, + ) + + if row: + self.db.updates._background_update_progress_txn( + txn, BG_UPDATE_REMOVE_DUP_OUTBOUND_POKES, {"last_row": row}, + ) + + return len(rows) + + rows = await self.db.runInteraction(BG_UPDATE_REMOVE_DUP_OUTBOUND_POKES, _txn) + + if not rows: + await self.db.updates._end_background_update( + BG_UPDATE_REMOVE_DUP_OUTBOUND_POKES + ) + + return rows + class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore): def __init__(self, database: Database, db_conn, hs): diff --git a/synapse/storage/data_stores/main/schema/delta/58/02remove_dup_outbound_pokes.sql b/synapse/storage/data_stores/main/schema/delta/58/02remove_dup_outbound_pokes.sql new file mode 100644 index 0000000000..fdc39e9ba5 --- /dev/null +++ b/synapse/storage/data_stores/main/schema/delta/58/02remove_dup_outbound_pokes.sql @@ -0,0 +1,22 @@ +/* Copyright 2020 The Matrix.org Foundation C.I.C + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + /* for some reason, we have accumulated duplicate entries in + * device_lists_outbound_pokes, which makes prune_outbound_device_list_pokes less + * efficient. + */ + +INSERT INTO background_updates (ordering, update_name, progress_json) + VALUES (5800, 'remove_dup_outbound_pokes', '{}'); diff --git a/synapse/storage/database.py b/synapse/storage/database.py index 715c0346dd..a7cd97b0b0 100644 --- a/synapse/storage/database.py +++ b/synapse/storage/database.py @@ -17,7 +17,17 @@ import logging import time from time import monotonic as monotonic_time -from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional, Tuple +from typing import ( + Any, + Callable, + Dict, + Iterable, + Iterator, + List, + Optional, + Tuple, + TypeVar, +) from six import iteritems, iterkeys, itervalues from six.moves import intern, range @@ -1557,3 +1567,74 @@ def make_in_list_sql_clause( return "%s = ANY(?)" % (column,), [list(iterable)] else: return "%s IN (%s)" % (column, ",".join("?" for _ in iterable)), list(iterable) + + +KV = TypeVar("KV") + + +def make_tuple_comparison_clause( + database_engine: BaseDatabaseEngine, keys: List[Tuple[str, KV]] +) -> Tuple[str, List[KV]]: + """Returns a tuple comparison SQL clause + + Depending what the SQL engine supports, builds a SQL clause that looks like either + "(a, b) > (?, ?)", or "(a > ?) OR (a == ? AND b > ?)". + + Args: + database_engine + keys: A set of (column, value) pairs to be compared. + + Returns: + A tuple of SQL query and the args + """ + if database_engine.supports_tuple_comparison: + return ( + "(%s) > (%s)" % (",".join(k[0] for k in keys), ",".join("?" for _ in keys)), + [k[1] for k in keys], + ) + + # we want to build a clause + # (a > ?) OR + # (a == ? AND b > ?) OR + # (a == ? AND b == ? AND c > ?) + # ... + # (a == ? AND b == ? AND ... AND z > ?) + # + # or, equivalently: + # + # (a > ? OR (a == ? AND + # (b > ? OR (b == ? AND + # ... + # (y > ? OR (y == ? AND + # z > ? + # )) + # ... + # )) + # )) + # + # which itself is equivalent to (and apparently easier for the query optimiser): + # + # (a >= ? AND (a > ? OR + # (b >= ? AND (b > ? OR + # ... + # (y >= ? AND (y > ? OR + # z > ? + # )) + # ... + # )) + # )) + # + # + + clause = "" + args = [] # type: List[KV] + for k, v in keys[:-1]: + clause = clause + "(%s >= ? AND (%s > ? OR " % (k, k) + args.extend([v, v]) + + (k, v) = keys[-1] + clause += "%s > ?" % (k,) + args.append(v) + + clause += "))" * (len(keys) - 1) + return clause, args diff --git a/tests/storage/test_database.py b/tests/storage/test_database.py new file mode 100644 index 0000000000..5a77c84962 --- /dev/null +++ b/tests/storage/test_database.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from synapse.storage.database import make_tuple_comparison_clause +from synapse.storage.engines import BaseDatabaseEngine + +from tests import unittest + + +def _stub_db_engine(**kwargs) -> BaseDatabaseEngine: + # returns a DatabaseEngine, circumventing the abc mechanism + # any kwargs are set as attributes on the class before instantiating it + t = type( + "TestBaseDatabaseEngine", + (BaseDatabaseEngine,), + dict(BaseDatabaseEngine.__dict__), + ) + # defeat the abc mechanism + t.__abstractmethods__ = set() + for k, v in kwargs.items(): + setattr(t, k, v) + return t(None, None) + + +class TupleComparisonClauseTestCase(unittest.TestCase): + def test_native_tuple_comparison(self): + db_engine = _stub_db_engine(supports_tuple_comparison=True) + clause, args = make_tuple_comparison_clause(db_engine, [("a", 1), ("b", 2)]) + self.assertEqual(clause, "(a,b) > (?,?)") + self.assertEqual(args, [1, 2]) + + def test_emulated_tuple_comparison(self): + db_engine = _stub_db_engine(supports_tuple_comparison=False) + clause, args = make_tuple_comparison_clause( + db_engine, [("a", 1), ("b", 2), ("c", 3)] + ) + self.assertEqual( + clause, "(a >= ? AND (a > ? OR (b >= ? AND (b > ? OR c > ?))))" + ) + self.assertEqual(args, [1, 1, 2, 2, 3]) From 29b7e22b939c473649c8619fdfbecec0cee6b029 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Wed, 8 Apr 2020 00:46:50 +0100 Subject: [PATCH 1332/1623] Add documentation to password_providers config option (#7238) --- changelog.d/7238.doc | 1 + docs/password_auth_providers.md | 5 ++++- docs/sample_config.yaml | 14 +++++++++++++- synapse/config/password_auth_providers.py | 16 ++++++++++++++-- 4 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 changelog.d/7238.doc diff --git a/changelog.d/7238.doc b/changelog.d/7238.doc new file mode 100644 index 0000000000..0e3b4be428 --- /dev/null +++ b/changelog.d/7238.doc @@ -0,0 +1 @@ +Add documentation to the `password_providers` config option. Add known password provider implementations to docs. \ No newline at end of file diff --git a/docs/password_auth_providers.md b/docs/password_auth_providers.md index 0db1a3804a..96f9841b7a 100644 --- a/docs/password_auth_providers.md +++ b/docs/password_auth_providers.md @@ -9,7 +9,10 @@ into Synapse, and provides a number of methods by which it can integrate with the authentication system. This document serves as a reference for those looking to implement their -own password auth providers. +own password auth providers. Additionally, here is a list of known +password auth provider module implementations: + +* [matrix-synapse-ldap3](https://github.com/matrix-org/matrix-synapse-ldap3/) ## Required methods diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index be742969cc..3417813750 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -1657,7 +1657,19 @@ email: #template_dir: "res/templates" -#password_providers: +# Password providers allow homeserver administrators to integrate +# their Synapse installation with existing authentication methods +# ex. LDAP, external tokens, etc. +# +# For more information and known implementations, please see +# https://github.com/matrix-org/synapse/blob/master/docs/password_auth_providers.md +# +# Note: instances wishing to use SAML or CAS authentication should +# instead use the `saml2_config` or `cas_config` options, +# respectively. +# +password_providers: +# # Example config for an LDAP auth provider # - module: "ldap_auth_provider.LdapAuthProvider" # config: # enabled: true diff --git a/synapse/config/password_auth_providers.py b/synapse/config/password_auth_providers.py index 9746bbc681..4fda8ae987 100644 --- a/synapse/config/password_auth_providers.py +++ b/synapse/config/password_auth_providers.py @@ -35,7 +35,7 @@ class PasswordAuthProviderConfig(Config): if ldap_config.get("enabled", False): providers.append({"module": LDAP_PROVIDER, "config": ldap_config}) - providers.extend(config.get("password_providers", [])) + providers.extend(config.get("password_providers") or []) for provider in providers: mod_name = provider["module"] @@ -52,7 +52,19 @@ class PasswordAuthProviderConfig(Config): def generate_config_section(self, **kwargs): return """\ - #password_providers: + # Password providers allow homeserver administrators to integrate + # their Synapse installation with existing authentication methods + # ex. LDAP, external tokens, etc. + # + # For more information and known implementations, please see + # https://github.com/matrix-org/synapse/blob/master/docs/password_auth_providers.md + # + # Note: instances wishing to use SAML or CAS authentication should + # instead use the `saml2_config` or `cas_config` options, + # respectively. + # + password_providers: + # # Example config for an LDAP auth provider # - module: "ldap_auth_provider.LdapAuthProvider" # config: # enabled: true From c11d24d48c2f0b0b57d70087e5659290b9ddd154 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 8 Apr 2020 11:59:51 +0200 Subject: [PATCH 1333/1623] Fix changelog for #7235 --- changelog.d/7235.bugfix | 1 - changelog.d/7235.feature | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 changelog.d/7235.bugfix create mode 100644 changelog.d/7235.feature diff --git a/changelog.d/7235.bugfix b/changelog.d/7235.bugfix deleted file mode 100644 index d185efe537..0000000000 --- a/changelog.d/7235.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a bug causing the login fallback to not display the SSO login form. diff --git a/changelog.d/7235.feature b/changelog.d/7235.feature new file mode 100644 index 0000000000..fafa79c7e7 --- /dev/null +++ b/changelog.d/7235.feature @@ -0,0 +1 @@ +Improve the support for SSO authentication on the login fallback page. From cae412148483763a108c3dd797c92ad89f5c1568 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 8 Apr 2020 11:59:26 +0100 Subject: [PATCH 1334/1623] Make systemd-with-workers doc official (#7234) Simplify and update this documentation, and make it part of the core dist. --- changelog.d/7234.doc | 1 + contrib/systemd-with-workers/README.md | 152 +----------------- .../system/matrix-synapse-worker@.service | 19 --- .../systemd-with-workers/system/matrix.target | 7 - docs/systemd-with-workers/README.md | 67 ++++++++ .../system/matrix-synapse-worker@.service | 20 +++ .../system/matrix-synapse.service | 7 +- .../system/matrix-synapse.target | 6 + .../workers/federation_reader.yaml | 1 - docs/workers.md | 48 ++++-- 10 files changed, 134 insertions(+), 194 deletions(-) create mode 100644 changelog.d/7234.doc delete mode 100644 contrib/systemd-with-workers/system/matrix-synapse-worker@.service delete mode 100644 contrib/systemd-with-workers/system/matrix.target create mode 100644 docs/systemd-with-workers/README.md create mode 100644 docs/systemd-with-workers/system/matrix-synapse-worker@.service rename {contrib => docs}/systemd-with-workers/system/matrix-synapse.service (79%) create mode 100644 docs/systemd-with-workers/system/matrix-synapse.target rename {contrib => docs}/systemd-with-workers/workers/federation_reader.yaml (92%) diff --git a/changelog.d/7234.doc b/changelog.d/7234.doc new file mode 100644 index 0000000000..d284f1422b --- /dev/null +++ b/changelog.d/7234.doc @@ -0,0 +1 @@ +Update the contributed documentation on managing synapse workers with systemd, and bring it into the core distribution. diff --git a/contrib/systemd-with-workers/README.md b/contrib/systemd-with-workers/README.md index 74b261e9fb..8d21d532bd 100644 --- a/contrib/systemd-with-workers/README.md +++ b/contrib/systemd-with-workers/README.md @@ -1,150 +1,2 @@ -# Setup Synapse with Workers and Systemd - -This is a setup for managing synapse with systemd including support for -managing workers. It provides a `matrix-synapse`, as well as a -`matrix-synapse-worker@` service for any workers you require. Additionally to -group the required services it sets up a `matrix.target`. You can use this to -automatically start any bot- or bridge-services. More on this in -[Bots and Bridges](#bots-and-bridges). - -See the folder [system](system) for any service and target files. - -The folder [workers](workers) contains an example configuration for the -`federation_reader` worker. Pay special attention to the name of the -configuration file. In order to work with the `matrix-synapse-worker@.service` -service, it needs to have the exact same name as the worker app. - -This setup expects neither the homeserver nor any workers to fork. Forking is -handled by systemd. - -## Setup - -1. Adjust your matrix configs. Make sure that the worker config files have the -exact same name as the worker app. Compare `matrix-synapse-worker@.service` for -why. You can find an example worker config in the [workers](workers) folder. See -below for relevant settings in the `homeserver.yaml`. -2. Copy the `*.service` and `*.target` files in [system](system) to -`/etc/systemd/system`. -3. `systemctl enable matrix-synapse.service` this adds the homeserver -app to the `matrix.target` -4. *Optional.* `systemctl enable -matrix-synapse-worker@federation_reader.service` this adds the federation_reader -app to the `matrix-synapse.service` -5. *Optional.* Repeat step 4 for any additional workers you require. -6. *Optional.* Add any bots or bridges by enabling them. -7. Start all matrix related services via `systemctl start matrix.target` -8. *Optional.* Enable autostart of all matrix related services on system boot -via `systemctl enable matrix.target` - -## Usage - -After you have setup you can use the following commands to manage your synapse -installation: - -``` -# Start matrix-synapse, all workers and any enabled bots or bridges. -systemctl start matrix.target - -# Restart matrix-synapse and all workers (not necessarily restarting bots -# or bridges, see "Bots and Bridges") -systemctl restart matrix-synapse.service - -# Stop matrix-synapse and all workers (not necessarily restarting bots -# or bridges, see "Bots and Bridges") -systemctl stop matrix-synapse.service - -# Restart a specific worker (i. e. federation_reader), the homeserver is -# unaffected by this. -systemctl restart matrix-synapse-worker@federation_reader.service - -# Add a new worker (assuming all configs are setup already) -systemctl enable matrix-synapse-worker@federation_writer.service -systemctl restart matrix-synapse.service -``` - -## The Configs - -Make sure the `worker_app` is set in the `homeserver.yaml` and it does not fork. - -``` -worker_app: synapse.app.homeserver -daemonize: false -``` - -None of the workers should fork, as forking is handled by systemd. Hence make -sure this is present in all worker config files. - -``` -worker_daemonize: false -``` - -The config files of all workers are expected to be located in -`/etc/matrix-synapse/workers`. If you want to use a different location you have -to edit the provided `*.service` files accordingly. - -## Bots and Bridges - -Most bots and bridges do not care if the homeserver goes down or is restarted. -Depending on the implementation this may crash them though. So look up the docs -or ask the community of the specific bridge or bot you want to run to make sure -you choose the correct setup. - -Whichever configuration you choose, after the setup the following will enable -automatically starting (and potentially restarting) your bot/bridge with the -`matrix.target`. - -``` -systemctl enable .service -``` - -**Note** that from an inactive synapse the bots/bridges will only be started with -synapse if you start the `matrix.target`, not if you start the -`matrix-synapse.service`. This is on purpose. Think of `matrix-synapse.service` -as *just* synapse, but `matrix.target` being anything matrix related, including -synapse and any and all enabled bots and bridges. - -### Start with synapse but ignore synapse going down - -If the bridge can handle shutdowns of the homeserver you'll want to install the -service in the `matrix.target` and optionally add a -`After=matrix-synapse.service` dependency to have the bot/bridge start after -synapse on starting everything. - -In this case the service file should look like this. - -``` -[Unit] -# ... -# Optional, this will only ensure that if you start everything, synapse will -# be started before the bot/bridge will be started. -After=matrix-synapse.service - -[Service] -# ... - -[Install] -WantedBy=matrix.target -``` - -### Stop/restart when synapse stops/restarts - -If the bridge can't handle shutdowns of the homeserver you'll still want to -install the service in the `matrix.target` but also have to specify the -`After=matrix-synapse.service` *and* `BindsTo=matrix-synapse.service` -dependencies to have the bot/bridge stop/restart with synapse. - -In this case the service file should look like this. - -``` -[Unit] -# ... -# Mandatory -After=matrix-synapse.service -BindsTo=matrix-synapse.service - -[Service] -# ... - -[Install] -WantedBy=matrix.target -``` +The documentation for using systemd to manage synapse workers is now part of +the main synapse distribution. See [docs/systemd-with-workers](../../docs/systemd-with-workers). diff --git a/contrib/systemd-with-workers/system/matrix-synapse-worker@.service b/contrib/systemd-with-workers/system/matrix-synapse-worker@.service deleted file mode 100644 index 3507e2e989..0000000000 --- a/contrib/systemd-with-workers/system/matrix-synapse-worker@.service +++ /dev/null @@ -1,19 +0,0 @@ -[Unit] -Description=Synapse Matrix Worker -After=matrix-synapse.service -BindsTo=matrix-synapse.service - -[Service] -Type=notify -NotifyAccess=main -User=matrix-synapse -WorkingDirectory=/var/lib/matrix-synapse -EnvironmentFile=/etc/default/matrix-synapse -ExecStart=/opt/venvs/matrix-synapse/bin/python -m synapse.app.%i --config-path=/etc/matrix-synapse/homeserver.yaml --config-path=/etc/matrix-synapse/conf.d/ --config-path=/etc/matrix-synapse/workers/%i.yaml -ExecReload=/bin/kill -HUP $MAINPID -Restart=always -RestartSec=3 -SyslogIdentifier=matrix-synapse-%i - -[Install] -WantedBy=matrix-synapse.service diff --git a/contrib/systemd-with-workers/system/matrix.target b/contrib/systemd-with-workers/system/matrix.target deleted file mode 100644 index aff97d03ef..0000000000 --- a/contrib/systemd-with-workers/system/matrix.target +++ /dev/null @@ -1,7 +0,0 @@ -[Unit] -Description=Contains matrix services like synapse, bridges and bots -After=network.target -AllowIsolate=no - -[Install] -WantedBy=multi-user.target diff --git a/docs/systemd-with-workers/README.md b/docs/systemd-with-workers/README.md new file mode 100644 index 0000000000..257c09446f --- /dev/null +++ b/docs/systemd-with-workers/README.md @@ -0,0 +1,67 @@ +# Setting up Synapse with Workers and Systemd + +This is a setup for managing synapse with systemd, including support for +managing workers. It provides a `matrix-synapse` service for the master, as +well as a `matrix-synapse-worker@` service template for any workers you +require. Additionally, to group the required services, it sets up a +`matrix-synapse.target`. + +See the folder [system](system) for the systemd unit files. + +The folder [workers](workers) contains an example configuration for the +`federation_reader` worker. + +## Synapse configuration files + +See [workers.md](../workers.md) for information on how to set up the +configuration files and reverse-proxy correctly. You can find an example worker +config in the [workers](workers) folder. + +Systemd manages daemonization itself, so ensure that none of the configuration +files set either `daemonize` or `worker_daemonize`. + +The config files of all workers are expected to be located in +`/etc/matrix-synapse/workers`. If you want to use a different location, edit +the provided `*.service` files accordingly. + +There is no need for a separate configuration file for the master process. + +## Set up + +1. Adjust synapse configuration files as above. +1. Copy the `*.service` and `*.target` files in [system](system) to +`/etc/systemd/system`. +1. Run `systemctl deamon-reload` to tell systemd to load the new unit files. +1. Run `systemctl enable matrix-synapse.service`. This will configure the +synapse master process to be started as part of the `matrix-synapse.target` +target. +1. For each worker process to be enabled, run `systemctl enable +matrix-synapse-worker@.service`. For each ``, there +should be a corresponding configuration file +`/etc/matrix-synapse/workers/.yaml`. +1. Start all the synapse processes with `systemctl start matrix-synapse.target`. +1. Tell systemd to start synapse on boot with `systemctl enable matrix-synapse.target`/ + +## Usage + +Once the services are correctly set up, you can use the following commands +to manage your synapse installation: + +```sh +# Restart Synapse master and all workers +systemctl restart matrix-synapse.target + +# Stop Synapse and all workers +systemctl stop matrix-synapse.target + +# Restart the master alone +systemctl start matrix-synapse.service + +# Restart a specific worker (eg. federation_reader); the master is +# unaffected by this. +systemctl restart matrix-synapse-worker@federation_reader.service + +# Add a new worker (assuming all configs are set up already) +systemctl enable matrix-synapse-worker@federation_writer.service +systemctl restart matrix-synapse.target +``` diff --git a/docs/systemd-with-workers/system/matrix-synapse-worker@.service b/docs/systemd-with-workers/system/matrix-synapse-worker@.service new file mode 100644 index 0000000000..70589a7a51 --- /dev/null +++ b/docs/systemd-with-workers/system/matrix-synapse-worker@.service @@ -0,0 +1,20 @@ +[Unit] +Description=Synapse %i + +# This service should be restarted when the synapse target is restarted. +PartOf=matrix-synapse.target + +[Service] +Type=notify +NotifyAccess=main +User=matrix-synapse +WorkingDirectory=/var/lib/matrix-synapse +EnvironmentFile=/etc/default/matrix-synapse +ExecStart=/opt/venvs/matrix-synapse/bin/python -m synapse.app.generic_worker --config-path=/etc/matrix-synapse/homeserver.yaml --config-path=/etc/matrix-synapse/conf.d/ --config-path=/etc/matrix-synapse/workers/%i.yaml +ExecReload=/bin/kill -HUP $MAINPID +Restart=always +RestartSec=3 +SyslogIdentifier=matrix-synapse-%i + +[Install] +WantedBy=matrix-synapse.target diff --git a/contrib/systemd-with-workers/system/matrix-synapse.service b/docs/systemd-with-workers/system/matrix-synapse.service similarity index 79% rename from contrib/systemd-with-workers/system/matrix-synapse.service rename to docs/systemd-with-workers/system/matrix-synapse.service index 68e8991f18..c7b5ddfa49 100644 --- a/contrib/systemd-with-workers/system/matrix-synapse.service +++ b/docs/systemd-with-workers/system/matrix-synapse.service @@ -1,5 +1,8 @@ [Unit] -Description=Synapse Matrix Homeserver +Description=Synapse master + +# This service should be restarted when the synapse target is restarted. +PartOf=matrix-synapse.target [Service] Type=notify @@ -15,4 +18,4 @@ RestartSec=3 SyslogIdentifier=matrix-synapse [Install] -WantedBy=matrix.target +WantedBy=matrix-synapse.target diff --git a/docs/systemd-with-workers/system/matrix-synapse.target b/docs/systemd-with-workers/system/matrix-synapse.target new file mode 100644 index 0000000000..e0eba1b342 --- /dev/null +++ b/docs/systemd-with-workers/system/matrix-synapse.target @@ -0,0 +1,6 @@ +[Unit] +Description=Synapse parent target +After=network.target + +[Install] +WantedBy=multi-user.target diff --git a/contrib/systemd-with-workers/workers/federation_reader.yaml b/docs/systemd-with-workers/workers/federation_reader.yaml similarity index 92% rename from contrib/systemd-with-workers/workers/federation_reader.yaml rename to docs/systemd-with-workers/workers/federation_reader.yaml index 47c54ec0d4..5b65c7040d 100644 --- a/contrib/systemd-with-workers/workers/federation_reader.yaml +++ b/docs/systemd-with-workers/workers/federation_reader.yaml @@ -10,5 +10,4 @@ worker_listeners: resources: - names: [federation] -worker_daemonize: false worker_log_config: /etc/matrix-synapse/federation-reader-log.yaml diff --git a/docs/workers.md b/docs/workers.md index cf460283d5..2ce2259b22 100644 --- a/docs/workers.md +++ b/docs/workers.md @@ -52,24 +52,20 @@ synapse process.) You then create a set of configs for the various worker processes. These should be worker configuration files, and should be stored in a dedicated -subdirectory, to allow synctl to manipulate them. An additional configuration -for the master synapse process will need to be created because the process will -not be started automatically. That configuration should look like this: - - worker_app: synapse.app.homeserver - daemonize: true +subdirectory, to allow synctl to manipulate them. Each worker configuration file inherits the configuration of the main homeserver configuration file. You can then override configuration specific to that worker, e.g. the HTTP listener that it provides (if any); logging configuration; etc. You should minimise the number of overrides though to maintain a usable config. -You must specify the type of worker application (`worker_app`). The currently -available worker applications are listed below. You must also specify the -replication endpoints that it's talking to on the main synapse process. -`worker_replication_host` should specify the host of the main synapse, -`worker_replication_port` should point to the TCP replication listener port and -`worker_replication_http_port` should point to the HTTP replication port. +In the config file for each worker, you must specify the type of worker +application (`worker_app`). The currently available worker applications are +listed below. You must also specify the replication endpoints that it's talking +to on the main synapse process. `worker_replication_host` should specify the +host of the main synapse, `worker_replication_port` should point to the TCP +replication listener port and `worker_replication_http_port` should point to +the HTTP replication port. Currently, the `event_creator` and `federation_reader` workers require specifying `worker_replication_http_port`. @@ -90,8 +86,6 @@ For instance: - names: - client - worker_daemonize: True - worker_pid_file: /home/matrix/synapse/synchrotron.pid worker_log_config: /home/matrix/synapse/config/synchrotron_log_config.yaml ...is a full configuration for a synchrotron worker instance, which will expose a @@ -101,7 +95,31 @@ by the main synapse. Obviously you should configure your reverse-proxy to route the relevant endpoints to the worker (`localhost:8083` in the above example). -Finally, to actually run your worker-based synapse, you must pass synctl the -a +Finally, you need to start your worker processes. This can be done with either +`synctl` or your distribution's preferred service manager such as `systemd`. We +recommend the use of `systemd` where available: for information on setting up +`systemd` to start synapse workers, see +[systemd-with-workers](systemd-with-workers). To use `synctl`, see below. + +### Using synctl + +If you want to use `synctl` to manage your synapse processes, you will need to +create an an additional configuration file for the master synapse process. That +configuration should look like this: + +```yaml +worker_app: synapse.app.homeserver +``` + +Additionally, each worker app must be configured with the name of a "pid file", +to which it will write its process ID when it starts. For example, for a +synchrotron, you might write: + +```yaml +worker_pid_file: /home/matrix/synapse/synchrotron.pid +``` + +Finally, to actually run your worker-based synapse, you must pass synctl the `-a` commandline option to tell it to operate on all the worker configurations found in the given directory, e.g.: From 23f8d285ebd36d4091c2a03831d0b7c825f12e7e Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 8 Apr 2020 11:59:47 +0100 Subject: [PATCH 1335/1623] Remove redundant checks on `daemonize` from synctl (#7233) We pass --daemonize on the commandline, which (since at least #4853) overrides whatever the config file, so there is no need for it to be set in the config file. --- changelog.d/7233.misc | 1 + synctl | 20 +++++++++++--------- 2 files changed, 12 insertions(+), 9 deletions(-) create mode 100644 changelog.d/7233.misc diff --git a/changelog.d/7233.misc b/changelog.d/7233.misc new file mode 100644 index 0000000000..d9ad582726 --- /dev/null +++ b/changelog.d/7233.misc @@ -0,0 +1 @@ +Remove redundant checks on `daemonize` from synctl. diff --git a/synctl b/synctl index 45acece30b..bbccd05290 100755 --- a/synctl +++ b/synctl @@ -117,7 +117,17 @@ def start_worker(app: str, configfile: str, worker_configfile: str) -> bool: False if there was an error starting the process """ - args = [sys.executable, "-B", "-m", app, "-c", configfile, "-c", worker_configfile] + args = [ + sys.executable, + "-B", + "-m", + app, + "-c", + configfile, + "-c", + worker_configfile, + "--daemonize", + ] try: subprocess.check_call(args) @@ -266,9 +276,6 @@ def main(): worker_cache_factors = ( worker_config.get("synctl_cache_factors") or cache_factors ) - daemonize = worker_config.get("daemonize") or config.get("daemonize") - assert daemonize, "Main process must have daemonize set to true" - # The master process doesn't support using worker_* config. for key in worker_config: if key == "worker_app": # But we allow worker_app @@ -278,11 +285,6 @@ def main(): ), "Main process cannot use worker_* config" else: worker_pidfile = worker_config["worker_pid_file"] - worker_daemonize = worker_config["worker_daemonize"] - assert worker_daemonize, "In config %r: expected '%s' to be True" % ( - worker_configfile, - "worker_daemonize", - ) worker_cache_factor = worker_config.get("synctl_cache_factor") worker_cache_factors = worker_config.get("synctl_cache_factors", {}) workers.append( From 24722de7c86408053551dcd0bb9fb1c5397a9c0d Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 8 Apr 2020 17:41:46 +0100 Subject: [PATCH 1336/1623] Fix bad merge of CHANGES.md --- CHANGES.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index bee4d6baba..96021a5e69 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,14 +15,10 @@ correctly fix the issue with building the Debian packages. ([\#7212](https://git Synapse 1.12.2 (2020-04-02) =========================== -This release works around [an -issue](https://github.com/matrix-org/synapse/issues/7208) with building the -debian packages. +This release works around [an issue](https://github.com/matrix-org/synapse/issues/7208) with building the debian packages. No other significant changes since 1.12.1. ->>>>>>> master - Synapse 1.12.1 (2020-04-02) =========================== From 55d46da59a9d0db58888243137b69ee342921b11 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Thu, 9 Apr 2020 12:23:30 +0100 Subject: [PATCH 1337/1623] Upgrade jQuery to 3.x on fallback login/registration screens (#7236) --- changelog.d/7236.misc | 1 + synapse/static/client/login/index.html | 3 ++- synapse/static/client/login/js/jquery-2.1.3.min.js | 4 ---- synapse/static/client/login/js/jquery-3.4.1.min.js | 2 ++ synapse/static/client/login/js/login.js | 6 +++--- synapse/static/client/register/index.html | 3 ++- synapse/static/client/register/js/jquery-2.1.3.min.js | 4 ---- synapse/static/client/register/js/jquery-3.4.1.min.js | 2 ++ synapse/static/client/register/js/register.js | 6 +++--- 9 files changed, 15 insertions(+), 16 deletions(-) create mode 100644 changelog.d/7236.misc delete mode 100644 synapse/static/client/login/js/jquery-2.1.3.min.js create mode 100644 synapse/static/client/login/js/jquery-3.4.1.min.js delete mode 100644 synapse/static/client/register/js/jquery-2.1.3.min.js create mode 100644 synapse/static/client/register/js/jquery-3.4.1.min.js diff --git a/changelog.d/7236.misc b/changelog.d/7236.misc new file mode 100644 index 0000000000..e4a2702b54 --- /dev/null +++ b/changelog.d/7236.misc @@ -0,0 +1 @@ +Upgrade jQuery to v3.4.1 on fallback login/registration pages. \ No newline at end of file diff --git a/synapse/static/client/login/index.html b/synapse/static/client/login/index.html index 712b0e3980..6fefdaaff7 100644 --- a/synapse/static/client/login/index.html +++ b/synapse/static/client/login/index.html @@ -1,9 +1,10 @@ + Login - + diff --git a/synapse/static/client/login/js/jquery-2.1.3.min.js b/synapse/static/client/login/js/jquery-2.1.3.min.js deleted file mode 100644 index 25714ed29a..0000000000 --- a/synapse/static/client/login/js/jquery-2.1.3.min.js +++ /dev/null @@ -1,4 +0,0 @@ -/*! jQuery v2.1.3 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */ -!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l=a.document,m="2.1.3",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return n.each(this,a,b)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(n.isPlainObject(d)||(e=n.isArray(d)))?(e?(e=!1,f=c&&n.isArray(c)?c:[]):f=c&&n.isPlainObject(c)?c:{},g[b]=n.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){return!n.isArray(a)&&a-parseFloat(a)+1>=0},isPlainObject:function(a){return"object"!==n.type(a)||a.nodeType||n.isWindow(a)?!1:a.constructor&&!j.call(a.constructor.prototype,"isPrototypeOf")?!1:!0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(a){var b,c=eval;a=n.trim(a),a&&(1===a.indexOf("use strict")?(b=l.createElement("script"),b.text=a,l.head.appendChild(b).parentNode.removeChild(b)):c(a))},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=s(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:g.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;c>d;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=s(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(c=a[b],b=a,a=c),n.isFunction(a)?(e=d.call(arguments,2),f=function(){return a.apply(b||this,e.concat(d.call(arguments)))},f.guid=a.guid=a.guid||n.guid++,f):void 0},now:Date.now,support:k}),n.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=hb(),z=hb(),A=hb(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N=M.replace("w","w#"),O="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+N+"))|)"+L+"*\\]",P=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+O+")*)|.*)\\)|)",Q=new RegExp(L+"+","g"),R=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),S=new RegExp("^"+L+"*,"+L+"*"),T=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),U=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),V=new RegExp(P),W=new RegExp("^"+N+"$"),X={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M.replace("w","w*")+")"),ATTR:new RegExp("^"+O),PSEUDO:new RegExp("^"+P),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ab=/[+~]/,bb=/'|\\/g,cb=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),db=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},eb=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(fb){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function gb(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],k=b.nodeType,"string"!=typeof a||!a||1!==k&&9!==k&&11!==k)return d;if(!e&&p){if(11!==k&&(f=_.exec(a)))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return H.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName)return H.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=1!==k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(bb,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+rb(o[l]);w=ab.test(a)&&pb(b.parentNode)||b,x=o.join(",")}if(x)try{return H.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function hb(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ib(a){return a[u]=!0,a}function jb(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function kb(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function lb(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function mb(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function nb(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function ob(a){return ib(function(b){return b=+b,ib(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function pb(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=gb.support={},f=gb.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=gb.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=g.documentElement,e=g.defaultView,e&&e!==e.top&&(e.addEventListener?e.addEventListener("unload",eb,!1):e.attachEvent&&e.attachEvent("onunload",eb)),p=!f(g),c.attributes=jb(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=jb(function(a){return a.appendChild(g.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(g.getElementsByClassName),c.getById=jb(function(a){return o.appendChild(a).id=u,!g.getElementsByName||!g.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(g.querySelectorAll))&&(jb(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),jb(function(a){var b=g.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&jb(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",P)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===g||a.ownerDocument===v&&t(v,a)?-1:b===g||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,h=[a],i=[b];if(!e||!f)return a===g?-1:b===g?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return lb(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?lb(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},g):n},gb.matches=function(a,b){return gb(a,null,null,b)},gb.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return gb(b,n,null,[a]).length>0},gb.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},gb.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},gb.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},gb.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=gb.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=gb.selectors={cacheLength:50,createPseudo:ib,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(cb,db),a[3]=(a[3]||a[4]||a[5]||"").replace(cb,db),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||gb.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&gb.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(cb,db).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=gb.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(Q," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||gb.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ib(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ib(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?ib(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ib(function(a){return function(b){return gb(a,b).length>0}}),contains:ib(function(a){return a=a.replace(cb,db),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ib(function(a){return W.test(a||"")||gb.error("unsupported lang: "+a),a=a.replace(cb,db).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:ob(function(){return[0]}),last:ob(function(a,b){return[b-1]}),eq:ob(function(a,b,c){return[0>c?c+b:c]}),even:ob(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:ob(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:ob(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:ob(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function sb(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function tb(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ub(a,b,c){for(var d=0,e=b.length;e>d;d++)gb(a,b[d],c);return c}function vb(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function wb(a,b,c,d,e,f){return d&&!d[u]&&(d=wb(d)),e&&!e[u]&&(e=wb(e,f)),ib(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ub(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:vb(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=vb(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=vb(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function xb(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=sb(function(a){return a===b},h,!0),l=sb(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[sb(tb(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return wb(i>1&&tb(m),i>1&&rb(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&xb(a.slice(i,e)),f>e&&xb(a=a.slice(e)),f>e&&rb(a))}m.push(c)}return tb(m)}function yb(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=F.call(i));s=vb(s)}H.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&gb.uniqueSort(i)}return k&&(w=v,j=t),r};return c?ib(f):f}return h=gb.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=xb(b[c]),f[u]?d.push(f):e.push(f);f=A(a,yb(e,d)),f.selector=a}return f},i=gb.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(cb,db),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(cb,db),ab.test(j[0].type)&&pb(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&rb(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,ab.test(a)&&pb(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=jb(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),jb(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||kb("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&jb(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||kb("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),jb(function(a){return null==a.getAttribute("disabled")})||kb(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),gb}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=n.expr.match.needsContext,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^.[^:#\[\.,]*$/;function x(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(w.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return g.call(b,a)>=0!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=this.length,d=[],e=this;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;c>b;b++)if(n.contains(e[b],this))return!0}));for(b=0;c>b;b++)n.find(a,e[b],d);return d=this.pushStack(c>1?n.unique(d):d),d.selector=this.selector?this.selector+" "+a:a,d},filter:function(a){return this.pushStack(x(this,a||[],!1))},not:function(a){return this.pushStack(x(this,a||[],!0))},is:function(a){return!!x(this,"string"==typeof a&&u.test(a)?n(a):a||[],!1).length}});var y,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=n.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||y).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:l,!0)),v.test(c[1])&&n.isPlainObject(b))for(c in b)n.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}return d=l.getElementById(c[2]),d&&d.parentNode&&(this.length=1,this[0]=d),this.context=l,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof y.ready?y.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};A.prototype=n.fn,y=n(l);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};n.extend({dir:function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),n.fn.extend({has:function(a){var b=n(a,this),c=b.length;return this.filter(function(){for(var a=0;c>a;a++)if(n.contains(this,b[a]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=u.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.unique(f):f)},index:function(a){return a?"string"==typeof a?g.call(n(a),this[0]):g.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.unique(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){while((a=a[b])&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return n.dir(a,"parentNode")},parentsUntil:function(a,b,c){return n.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return n.dir(a,"nextSibling")},prevAll:function(a){return n.dir(a,"previousSibling")},nextUntil:function(a,b,c){return n.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return n.dir(a,"previousSibling",c)},siblings:function(a){return n.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return n.sibling(a.firstChild)},contents:function(a){return a.contentDocument||n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(C[a]||n.unique(e),B.test(a)&&e.reverse()),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return n.each(a.match(E)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):n.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(b=a.memory&&l,c=!0,g=e||0,e=0,f=h.length,d=!0;h&&f>g;g++)if(h[g].apply(l[0],l[1])===!1&&a.stopOnFalse){b=!1;break}d=!1,h&&(i?i.length&&j(i.shift()):b?h=[]:k.disable())},k={add:function(){if(h){var c=h.length;!function g(b){n.each(b,function(b,c){var d=n.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&g(c)})}(arguments),d?f=h.length:b&&(e=c,j(b))}return this},remove:function(){return h&&n.each(arguments,function(a,b){var c;while((c=n.inArray(b,h,c))>-1)h.splice(c,1),d&&(f>=c&&f--,g>=c&&g--)}),this},has:function(a){return a?n.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],f=0,this},disable:function(){return h=i=b=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,b||k.disable(),this},locked:function(){return!i},fireWith:function(a,b){return!h||c&&!i||(b=b||[],b=[a,b.slice?b.slice():b],d?i.push(b):j(b)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!c}};return k},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&n.isFunction(a.promise)?e:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(H.resolveWith(l,[n]),n.fn.triggerHandler&&(n(l).triggerHandler("ready"),n(l).off("ready"))))}});function I(){l.removeEventListener("DOMContentLoaded",I,!1),a.removeEventListener("load",I,!1),n.ready()}n.ready.promise=function(b){return H||(H=n.Deferred(),"complete"===l.readyState?setTimeout(n.ready):(l.addEventListener("DOMContentLoaded",I,!1),a.addEventListener("load",I,!1))),H.promise(b)},n.ready.promise();var J=n.access=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===n.type(c)){e=!0;for(h in c)n.access(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,n.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(n(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f};n.acceptData=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function K(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=n.expando+K.uid++}K.uid=1,K.accepts=n.acceptData,K.prototype={key:function(a){if(!K.accepts(a))return 0;var b={},c=a[this.expando];if(!c){c=K.uid++;try{b[this.expando]={value:c},Object.defineProperties(a,b)}catch(d){b[this.expando]=c,n.extend(a,b)}}return this.cache[c]||(this.cache[c]={}),c},set:function(a,b,c){var d,e=this.key(a),f=this.cache[e];if("string"==typeof b)f[b]=c;else if(n.isEmptyObject(f))n.extend(this.cache[e],b);else for(d in b)f[d]=b[d];return f},get:function(a,b){var c=this.cache[this.key(a)];return void 0===b?c:c[b]},access:function(a,b,c){var d;return void 0===b||b&&"string"==typeof b&&void 0===c?(d=this.get(a,b),void 0!==d?d:this.get(a,n.camelCase(b))):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d,e,f=this.key(a),g=this.cache[f];if(void 0===b)this.cache[f]={};else{n.isArray(b)?d=b.concat(b.map(n.camelCase)):(e=n.camelCase(b),b in g?d=[b,e]:(d=e,d=d in g?[d]:d.match(E)||[])),c=d.length;while(c--)delete g[d[c]]}},hasData:function(a){return!n.isEmptyObject(this.cache[a[this.expando]]||{})},discard:function(a){a[this.expando]&&delete this.cache[a[this.expando]]}};var L=new K,M=new K,N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(O,"-$1").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}M.set(a,b,c)}else c=void 0;return c}n.extend({hasData:function(a){return M.hasData(a)||L.hasData(a)},data:function(a,b,c){return M.access(a,b,c) -},removeData:function(a,b){M.remove(a,b)},_data:function(a,b,c){return L.access(a,b,c)},_removeData:function(a,b){L.remove(a,b)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=M.get(f),1===f.nodeType&&!L.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d])));L.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){M.set(this,a)}):J(this,function(b){var c,d=n.camelCase(a);if(f&&void 0===b){if(c=M.get(f,a),void 0!==c)return c;if(c=M.get(f,d),void 0!==c)return c;if(c=P(f,d,void 0),void 0!==c)return c}else this.each(function(){var c=M.get(this,d);M.set(this,d,b),-1!==a.indexOf("-")&&void 0!==c&&M.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){M.remove(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=L.get(a,b),c&&(!d||n.isArray(c)?d=L.access(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return L.get(a,c)||L.access(a,c,{empty:n.Callbacks("once memory").add(function(){L.remove(a,[b+"queue",c])})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthx",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var U="undefined";k.focusinBubbles="onfocusin"in a;var V=/^key/,W=/^(?:mouse|pointer|contextmenu)|click/,X=/^(?:focusinfocus|focusoutblur)$/,Y=/^([^.]*)(?:\.(.+)|)$/;function Z(){return!0}function $(){return!1}function _(){try{return l.activeElement}catch(a){}}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.get(a);if(r){c.handler&&(f=c,c=f.handler,e=f.selector),c.guid||(c.guid=n.guid++),(i=r.events)||(i=r.events={}),(g=r.handle)||(g=r.handle=function(b){return typeof n!==U&&n.event.triggered!==b.type?n.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(E)||[""],j=b.length;while(j--)h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o&&(l=n.event.special[o]||{},o=(e?l.delegateType:l.bindType)||o,l=n.event.special[o]||{},k=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},f),(m=i[o])||(m=i[o]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,p,g)!==!1||a.addEventListener&&a.addEventListener(o,g,!1)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),n.event.global[o]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.hasData(a)&&L.get(a);if(r&&(i=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=i[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&q!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete i[o])}else for(o in i)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(i)&&(delete r.handle,L.remove(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,m,o,p=[d||l],q=j.call(b,"type")?b.type:b,r=j.call(b,"namespace")?b.namespace.split("."):[];if(g=h=d=d||l,3!==d.nodeType&&8!==d.nodeType&&!X.test(q+n.event.triggered)&&(q.indexOf(".")>=0&&(r=q.split("."),q=r.shift(),r.sort()),k=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=r.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:n.makeArray(c,[b]),o=n.event.special[q]||{},e||!o.trigger||o.trigger.apply(d,c)!==!1)){if(!e&&!o.noBubble&&!n.isWindow(d)){for(i=o.delegateType||q,X.test(i+q)||(g=g.parentNode);g;g=g.parentNode)p.push(g),h=g;h===(d.ownerDocument||l)&&p.push(h.defaultView||h.parentWindow||a)}f=0;while((g=p[f++])&&!b.isPropagationStopped())b.type=f>1?i:o.bindType||q,m=(L.get(g,"events")||{})[b.type]&&L.get(g,"handle"),m&&m.apply(g,c),m=k&&g[k],m&&m.apply&&n.acceptData(g)&&(b.result=m.apply(g,c),b.result===!1&&b.preventDefault());return b.type=q,e||b.isDefaultPrevented()||o._default&&o._default.apply(p.pop(),c)!==!1||!n.acceptData(d)||k&&n.isFunction(d[q])&&!n.isWindow(d)&&(h=d[k],h&&(d[k]=null),n.event.triggered=q,d[q](),n.event.triggered=void 0,h&&(d[k]=h)),b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(L.get(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(g.namespace))&&(a.handleObj=g,a.data=g.data,e=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==e&&(a.result=e)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!==this;i=i.parentNode||this)if(i.disabled!==!0||"click"!==a.type){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>=0:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h]*)\/>/gi,bb=/<([\w:]+)/,cb=/<|&#?\w+;/,db=/<(?:script|style|link)/i,eb=/checked\s*(?:[^=]|=\s*.checked.)/i,fb=/^$|\/(?:java|ecma)script/i,gb=/^true\/(.*)/,hb=/^\s*\s*$/g,ib={option:[1,""],thead:[1,"","
    "],col:[2,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],_default:[0,"",""]};ib.optgroup=ib.option,ib.tbody=ib.tfoot=ib.colgroup=ib.caption=ib.thead,ib.th=ib.td;function jb(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function kb(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function lb(a){var b=gb.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function mb(a,b){for(var c=0,d=a.length;d>c;c++)L.set(a[c],"globalEval",!b||L.get(b[c],"globalEval"))}function nb(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(L.hasData(a)&&(f=L.access(a),g=L.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)n.event.add(b,e,j[e][c])}M.hasData(a)&&(h=M.access(a),i=n.extend({},h),M.set(b,i))}}function ob(a,b){var c=a.getElementsByTagName?a.getElementsByTagName(b||"*"):a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&n.nodeName(a,b)?n.merge([a],c):c}function pb(a,b){var c=b.nodeName.toLowerCase();"input"===c&&T.test(a.type)?b.checked=a.checked:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}n.extend({clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=n.contains(a.ownerDocument,a);if(!(k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(g=ob(h),f=ob(a),d=0,e=f.length;e>d;d++)pb(f[d],g[d]);if(b)if(c)for(f=f||ob(a),g=g||ob(h),d=0,e=f.length;e>d;d++)nb(f[d],g[d]);else nb(a,h);return g=ob(h,"script"),g.length>0&&mb(g,!i&&ob(a,"script")),h},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,k=b.createDocumentFragment(),l=[],m=0,o=a.length;o>m;m++)if(e=a[m],e||0===e)if("object"===n.type(e))n.merge(l,e.nodeType?[e]:e);else if(cb.test(e)){f=f||k.appendChild(b.createElement("div")),g=(bb.exec(e)||["",""])[1].toLowerCase(),h=ib[g]||ib._default,f.innerHTML=h[1]+e.replace(ab,"<$1>")+h[2],j=h[0];while(j--)f=f.lastChild;n.merge(l,f.childNodes),f=k.firstChild,f.textContent=""}else l.push(b.createTextNode(e));k.textContent="",m=0;while(e=l[m++])if((!d||-1===n.inArray(e,d))&&(i=n.contains(e.ownerDocument,e),f=ob(k.appendChild(e),"script"),i&&mb(f),c)){j=0;while(e=f[j++])fb.test(e.type||"")&&c.push(e)}return k},cleanData:function(a){for(var b,c,d,e,f=n.event.special,g=0;void 0!==(c=a[g]);g++){if(n.acceptData(c)&&(e=c[L.expando],e&&(b=L.cache[e]))){if(b.events)for(d in b.events)f[d]?n.event.remove(c,d):n.removeEvent(c,d,b.handle);L.cache[e]&&delete L.cache[e]}delete M.cache[c[M.expando]]}}}),n.fn.extend({text:function(a){return J(this,function(a){return void 0===a?n.text(this):this.empty().each(function(){(1===this.nodeType||11===this.nodeType||9===this.nodeType)&&(this.textContent=a)})},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=jb(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=jb(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?n.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||n.cleanData(ob(c)),c.parentNode&&(b&&n.contains(c.ownerDocument,c)&&mb(ob(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(n.cleanData(ob(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return J(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!db.test(a)&&!ib[(bb.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(ab,"<$1>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(ob(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,n.cleanData(ob(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,m=this,o=l-1,p=a[0],q=n.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&eb.test(p))return this.each(function(c){var d=m.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(c=n.buildFragment(a,this[0].ownerDocument,!1,this),d=c.firstChild,1===c.childNodes.length&&(c=d),d)){for(f=n.map(ob(c,"script"),kb),g=f.length;l>j;j++)h=c,j!==o&&(h=n.clone(h,!0,!0),g&&n.merge(f,ob(h,"script"))),b.call(this[j],h,j);if(g)for(i=f[f.length-1].ownerDocument,n.map(f,lb),j=0;g>j;j++)h=f[j],fb.test(h.type||"")&&!L.access(h,"globalEval")&&n.contains(i,h)&&(h.src?n._evalUrl&&n._evalUrl(h.src):n.globalEval(h.textContent.replace(hb,"")))}return this}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=[],e=n(a),g=e.length-1,h=0;g>=h;h++)c=h===g?this:this.clone(!0),n(e[h])[b](c),f.apply(d,c.get());return this.pushStack(d)}});var qb,rb={};function sb(b,c){var d,e=n(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:n.css(e[0],"display");return e.detach(),f}function tb(a){var b=l,c=rb[a];return c||(c=sb(a,b),"none"!==c&&c||(qb=(qb||n("

    u~Y8M5&&MC3}#bMq*k<3whlsuj!sP+agiWpRq+|lVL6BVp%E# zV%-?q^72E(Tp0PrX7i z9SOQR0gqP7)=~i>33XU14>Pd_jLUH-fU->w6euHMulXTutue#ypaYG$$iM@pSZ!wN zIkuCrS5Q6J&ZByyf$Es->7?_zrziUnQmr!;Bm#0inI8H9jk$1b&}zCsgrf0a+@FNo z9+%#WAXRO!y$j($ zW9rvl0PviB_Q*WZrHE>)$RlV{!7+Q-htL^12q!`ALbOy@yqJiJ4at$t={?hUHg$rP zu8n~%4b`#W%wYN{v6<>bL4StoSny~t-QS_ALxFyb(}e|R2GjivsZIoRWvGsYe5ay_L5|%Xy;QEjSYHm|y5oXul78 zzYj9$DiCQQp2EtkJt`I)l+l+)W%_*q-mYpc1u&3BJmqJav1~z26Rvc+hD&Kt=+ow8 zKx=oEizK6g{95g#M%x>%W(Dpf>TSjq2nSZ=Yc}2Xw7;vMIlWOukh+@5U?k+qlQ0~u=FmfA`JwG* zdHrMb879j+w_`N<8jR7Bw^^tbJ9JfW=N!RJX4xO;6kV>S##Tv4B_KYjHLJlIE&I7V zB?i4QxvD1YF(MXHLBgg9~ zw?`%xc?aGFN%2}-?+{u3)poPI{+;z1Hp@G;V+6cV#?MfRG=(s7Ih#ofo?^|r$t?RC z5mz2_v+rUyL9^P#f z@Fgb#i6{k)(5f7jT^%&pV&6RY5d=RW_h&A`>Ot1s#oVl_%0SSanXy#;9#@AyLs zu_ExmwH1^OmX?(!@Kps?9pDFKcf*?&Vzpq`GZlpA`aML_{wkUlLhLna$ZSovXEHv zbfgaLmymW*=u%phOqOD@3gI;=TWtHwM_!Y4A&lEwtgo7mU=1ZiGQOo$i^;XCYS_A?FSVg+?ma=+z2%&+R5-nIzfe;Z!CLN`nJb zM^cl7N_rwhHV|ub zkVEd7r;i(T4hZyNloVd~fHYE!@1Q<{$TPrj)90fCEpSzW3$fjN(z&2=zH~7H2dXhK zn5l4(+v%yYiCTo~^y)`mB0&phA~6O9wbZKJC{>f$Xd@pAQfNl5;32e5(D`g2U*i&d z)XUePW*`aKsY)D=eJ39q-1SE)wBWCWER4BdzoAXn%6u>vmy z(VXV%?;Vjkw%Qwyr=UIxh-B!r%gcP1m)k9HW_{`DR;tuJJ;zfE0sot6pOMbd0RGg6 z=^XlOGs0wcPM2V}-FR4Ts!M@_Q|_QpGUC;XkiC+nB8c4|kR8!HB`t(wNO?(3wDnXu zWltaRvyJyvoc3Ih;ktojnC~rgTZ9NX9c8we!zwWWE{2-8PmMrvP;;Q#>mmd%SC;}U zNL}EQewt@7KigcmR48d+%M^t>O`Qrte63NeOmyeQ1Bo^wDUEeO?*!}^ri~wcvc=& zER@84<|*W1bkA+<4;x=14n5q!jvYPSIGyleqZ`8&2b3;3%C1@`ClcY17LUNJD;`)1 zgWW6?Po`3}e0!#u9j%V zL#CE71)LDt>I zy75ja6VEiAb;%#-Hn1if$YdQdd&I9BGFQk$Y$X|I+a;pf(>hElgZgSxz85S;=Of>81(^KPwf|f}QS}r;r_oIydi%fh9_!LsSrz-gBTY?nqfE|uP!7prJeQ^9RaGISwQ{al7K)t zQY`yX%I7n8WHp8k6mqxCKXUTu?8vU;NB58a7JMEzHjRD zwl_}x^GtEL0MfUP$|r~^+KWAWAO^rEaA~az;5@ji-C#+Qm{F6Ce5bzr3og`NfKH- zAz^uT+tn#W<+qqi>k{W#~8)mcB&jWYR9v|Zc3^I_km4A$1*IgB^fL!HhpAyXdb4h z1M?tOi)g{$$kb{~GN#&PG?DNH>8^hnp#+tUG&5;6DN6(r?g{fQt*dY_4MbH=!XYvz z*8IS07uW`4atTCF)Z~1dmZVlUR2TL&Gej?y;!Dk(o?v}!fNTw6u$^%bLxt#g@Gum0 zFUoi>j*DWBTju2^tK_;Fp(>&Q5$k1Ai5id2XDBu6WT|AQ%|ae~tQl8=o)pv)rMTQ^ z6-&)(b6HOCe!i}z_obRtqVA^xNj#jS@JKHo8Nx6%bLD7%e*i}LK-gK#33(x3*IQ+u zv+jhG4BW$WiJY_1WHQTmAuA`kyjMuit1wq?J2hu1D#p8FuTn0vdBPJQNY_Lv|R9p8w8bWvjs>% z1vbEisYnuyI+{YK8DQt}l#FH_y-c%>mNcP-*&7`Zxbk)K?Vyv``^OV2MV315_Kk{aJj6oW`<89^IvB-2rZY!?kU3Q<1m4b&tqnrrP#O2x9T+EqGC zMAAUG2S*KI7(MME21u+e=G}`4QF6nIT=l9_R2Ka?wpJ^Z3UQW<3l#yX3CSijk8y6s zM^=b-USdJQ9b9fL=0TXPt1d1Q%ZX)8?<51uN*VJNdi!E2vfQo#M~`~iSIhQ-1?4q2x}JF9$*Sj)3kTKl`bnDfzOFr#~;%rUWfz(LWm7J z9bBp#?)IEw#*UIg3{!CvTNF0)%aP!ddF&o&dtfjU6wR{^EF%)xWV915mr%{#KoL@L z$EtCOW*Jwp;>8hHr_!7Ek-2&)*XVRLsG9QVoqWpebXFBZYU>)mBjTkO>^sqcEZ}Fo!KRm?Z0LmBEJ61=-rXxs+I>9gKIL%@v46 zcRSLJ^-4Z_25igWY|>RElCb8iMePIxQ@FjGCKS}W4~s{&a6{@9>Vz816M-BtjA1e} zi8_p$F5}U#nuiDo!CN6s42D=**jK0&wT{Nu7(GOiT}_dO z9~ZmNJ&3`=9woP3bv7TT+RI_sLD`Xdyz0Y?=@`$5y?7u_;@&jj&d2KR1bB3ByGK28 z0E1)?1{*4zq>524=J67$W7!>%r(Ue zs1mi}C; z_U`MXum;WRT%bV7qS9;Or6Jv%U=GfM%MqsR@j8!z!xBXSWrRAxgj8|hBo?Gd2UKXr z;Zz`3lfnMrstPGD#^>dVQ$q8ZJc9bsQW=X?a;-cCvL`nYdrHq^c0X;{7+o~~7q z28N(HM}mq~t99-qh4@@aR2D1$B!NR<=*e@egEDo_R@RLp8NCMcjj)Hd*xhm?nSeIn!RWC zy|b6i#z)^Y@z&7?CThF(PFynf*|F=!gs}_8j^6e2*)wMzvpsD4s_lAP!xpfeJoP(J zbMPHg%9MZV#L3@IerfXUlhPzM`TU88CcZF0Yfg>N8Bgrm1-6JDa0}MjA%7ZUa+X$bddTs`un}KK544B=Ber`L;Ea`vR_L?Pq zz&2-=^k=qNv!wUiX3UcQhi#8p(*K{mFM)I8s_)-RD`~VxxI=+pn}sB7mYvZ#3Xnwi zeUHu%)-$7#W^_xVYZQnb$l;JcY$MaqQb-|%00B};`wx_saMrYxrU%76plM5KN@<%? zO2ZNOzj?cB?@C%}#@S%fdh_}C>v(_fH{Y4}e(%k^_d6a{AyP&EPNkMA`c2i=tUY;i z_!}z8Z$y8q+M3`eYyBJ5)+^a$(Z5#BNxiMVQk{K$ob>i~Ml~Z<>(^D&Qbqq#H6>N_ zYs#NV75xk4Po;|fx$?KZ~W@cu)w1-kK=-q-@E%yp!S*5}$V{(t&YoBscP@Ry4}&yXi@`%-^>)0Ijwy}}CMe4jH$s`Y|hh_(c$ zD`CPyemm`SGz>;}+Y+Y&OwpS%_li&1m1@tZMBw4|OpJ@-TXkEtP1hTFr=M)6Fxum# zSf9hTy6&JIBp&IR8sT!l12P!~p4qNc*zT}I=!>|2&W%Rgyr(Z!L7*76p4P60fFrQ2 z%C81WZMGF{5Y<)_514vvj)qutiPjq}km>UD~)K;GuLqww$AU7JVX@!cbj8>yH=O0v|DC zCC2TGw=Oz2F7;n^)*@w$s;6v`_@3Uz&&){0oSUQC8Es*`onEI%+FhlER#Ip6q=Ft@ z$>eXY8sj-@xe_d;s?TtX+$`C3*1K_kqZ^I7y}cV<`ApcUx3#;KjNTcGyE$zZwFjd9 z^@gDnYnHqIILSJzX2D=Me~WB{df_hV#$53t*0!#j%QeB^$mlAWlqu>8Rw8;n5?*aE z7P~D&xRc>LTcI6Eht9cL3YBYmoHy5H6yg;G486>=3fW9~ZB4yS;|oOr8BI9(xA zUQ3mnojTqt7`m~7JIxrHg|@E}!fPFxui>VQ$ttPbm|48_O@qAZtU<~cR8QF;Vf58V z|Nm!Z1x^{^zQ1?|xreJie2p274cHYSXV_Q&e}fL+v+kzb9U=&eH@J@CAkRgz6VN#s zXI{vPdIX zK@*|xc(s2nwdhb?bUd+T5xmB;ulZw~d)_SaCwec0I0le{z*+w^P&lp3I z4&w(U*;84)&!SD#U5sPZRuK52M_;RHt$f`E+&#N7#*ihn7AdbTo3{>T%45a8RFNEg`XkluMg}U%D8jQpb|lvj2kdBPZ6lG;1+b(R&osoGO*bAIoxNF)582|?2gJ-% zfG%vik!5mZtJDNK#7Xk(EOX1K4Mf_<89OlFV@bI+uM>r}Z$GEQHevU#(D8|~RCtZg>Ab-JoU@A87cnn-@V zi-H)p;(1Run1|PCd9Ndt$?+w$n{lPfOxnxo3O2C8SWcKgL3C%jsdYqrXxK+2vkVA0 z%Qalq)Oyz%E?R5rp*ToyVol=uuoayCrFwO(RbrAe@z%j~b6dNQ!DU{U-rve!Qm31d zIe6tvH{vQfv<%BtFfe^=)+@U}6UXMod6ugIYd9p<&=QGcLA{?6DQwyNA<AS9s?bV>?7YrVr-~lR5(+T7e7)o(aHgj#=uPLJ#fKtG7}vvzvd0y$xOggT z?0M|GlZ~xfdJN@hlI5B)P_HI>Ex}wf8CjjcfwVpOAmit(Xa#(sHLTuTCY&^9DywKC z74TT>UWwcI;;jSAyOrysqUGJOF)imcb#je6iI#_rNVYq|mc)M=u#U~@c^Ak!;7Z;G zODk6uSCvCB1qx(T71t)|i|tOM7#;4v%P7R#J>S7@!cBYT($t3vt&*dat!2D!&fa#n zS#vK$Q|X#6XDmdbL0li}CIm*u$MN%CW>9M>!w2&ghU83EB7s(n!Br5T7PlF*g07NW z7YYVn7X-d&-9+3L=%K!*)zrhiZgLe%27Kk!q$$TV8Vx?dmV@m&=WUvtJ&UyXfAQAA zU`LjII%S)C&eWza|NIL?Uwiet)%>={|L#Zt(TiX7?*|b9FIBrmkNmmk%zs|}lF@$q zqNl#%9g3pjIuI@Jow?6!`u~f5{@ZZ|^HwpQ;9QizxyS(!O|S@}3f?wN&n!=WYxS+^ANF*Rb3~;?!hWP%AUT^|){pr;H-_}Kqo8CiI zFqVoDWHuCF!GTXL&~p#Cuz}c|Uy&+$RC!dY=n>@+siHS2Z;~qdG3Cdkihfl2QK_OI zQGP_K=mW|Jq>6r6`C+M|A5wlus^|xmACxM3zw&;mqBkpV-V%-2Lk-GOtW+YIT%2O_ zgMO6i+tjy7)%s55JEe-=qr69|=&ve&RjTNlm2Z|R`X=R@q>6q(`2ne-?^nKGs_1Vh ze?zM1eaic!ioQ?zKB=PbRlZlM=&vh(U8?B4%6p}XzDM~UsiN;zzFVs3uPJ{`s_46v z?~*F|%gSGtDtfE(R;i-5C~w&kooI%~)W@WX-mbn~DuH(>-yv1>4&@zEMUN@BxlUY+ zZdXd?dYe-6M084dN-Edm%HvW+?^fO|RrGDjw@DR!tMaWZxm zVdY_|q8rMMEzyYvb5eOys^|&ji7nBIv&&o6+k^tKX`=Ty>A?D&^;t;rZ{(U$6KN#Zkrc=kA+ZpS^eX>X{GETs!@>X?p5^fQqp{ zH)~E@Ha`=&{02sK_dz>T%2_S0wyi;=@hqL+K2Vjbm@i}_8y2>#?dX^|cj6_0jc$=qy-hNkd<#^-*dS>d zq|cC zYxqcLrCIP+v=r6~kxpNePNR4fkE7N=)9KchV)1;MOgsIVjDSXJo&1Rxfh(?qE8Zpf zisE+mMx#|?_!T@5EAvjW9;#|1Ag7X^?4)C8i14|)Nn<0L->^GkVNbg1$Fc$L#4l|< z$2%ooY3EVcipOoYtT}yD(M(#2JYUzBt1(|I-bT@l7-p>3&_L9Y6Ig?hL%|SwA-I@0 zgx(_gV&jL9ZeZN4=uB1CZ}YgoxlYL54R17^^$@{Wk}f>ibD(&}L)5UWAEehJtMrLW zMK<6e-yxaJ<_7JG)52DR2_AE1l7XPjQp{DEc-@eQ*fxxAYn2WZY=*VYnu|mAX>ijo z*t+Rs2aBPyjSx7l=dS0#*%w=i4)B*`H`;+lD^+h0qtbf4Jzrr0x_q{qwxL;237)PR z0(xDeU2(^IF0X)@48_KY=L0rZUdgC#m&^tpSu%X73eGRtD&M?X%;B`+E9kD^B~nE zUI-)tCL>7yX05m}kPeP8=s|*Y!kt*lHB80QiRXYTnZcEAk$k0r)8G}#g4#P#vz-uV zCYP)SIvaU?r?+9JnmUiuYj4{OE^92q5mp13_nr+dC(e6smVCKg_F8geEO-Xwjq^H+ z8*5mW#8TOIx(m|eSPf>nX7T46UCd=lhuaY_be{z-Cfe(pBwuX2z3$9721gO$>2%{{ z#@kx!IXH)dWH3L)HUmj3(`!aMW?wyJucdYQmO1C4Ph29h0k?j$WHwv&+V$o9ZNBaB z+dU}JHR(5acdkQr>&7*GFKE@-(;a^=YxRec?P~SJGPsI3R*xR6dE=gDAx(th-iXuF z6}Z7fW?SYuVywFA4m3*owe2SE#mbB)-xYLel1N~^T*XpCNgqnCIg{N)*B;29SOQlR z$LbNuSKKjHJp}_+?1BWO`e4T7taD8q7r1h}Mc1%q)Z8|e-7!zQ+w8g4EZm7ja3yi@ z+$8x*y9Q5$cNoDpfu&7XN`%&6!tsbdpXg?6Nw%|QaC-7S8z?j5$6Ra&44wtCyTIUi zqvVT?A3TGV&sW;XBw;GDNt@MaP4E@3OXm+b*;b6P)SF>zt^!uPoIMbVcN<``*NAMu zt-nDso2|ivCb3ej?rrDN&$;UMVmK0S=LY@Cq)hg>dDmpGHLt7+>7=ioK_mMur{s5eB?Jxj4*HCB6A$KH-r$(qCOPp14? zo3*%JrvV++79HW~q7$)aYCWGVR`FWAW-RK9*eoE4YJyL-c~8^O6*}z>X$Y^!YtDT2 zgc7h3Exjw5%?L|pOx<{1SM-?8ZnEM`XlKW$DJIan)EA_;iskyDexsr(0h)2$|Gs-S4ihVob>IkGZ&H)@cz(+1ysQwj^I{ ztfk|FrQuinj@Wt|b(?CYV%;9ZyC8{YH``v1YXjDHz~j)hjFi!mGFOYX0DWRsWCL!! zDVfcdrI(C+r)>z^DrJ8sY|huRPSWi0>jZ7CLSgHuSVX zVET)L2qrG(CyBHr6lT1fuhk{GAt4Ns!Si*3t{y)E*oc;1lgws>rJL^y>4+Zvs~oExjuFiaRV_hFdtY@F!C#ki2jD zZR0cVZD;;d^J=$v-Y=fwi&^`&v-~Z8ZD*dq%`AfAY5u~?wsZFF^aK#S4W>VNF2~?9 zG4s3P3BP#0znM*N=4$cWf9@JFqo9~2a4?G?pe^x7^*zbw|5Eo&Zj^rH(vyDVB5WVI zkeN=vm$s+8(NfG;hy;^^>93VWWqOONKPjY0@CqJhvoTAx8t?6z1yFSH^2KYU-quah z&;8yg{V3}V(vPy*($9Qa($9RF+h;yx5oA2|cXKwjKeOe4Fj@2iGg}S_ zBpK~QGTES|#0l-FyH?-_$G|rg-`aXu6Gi_>@uyNn|4^}&33W1oFDw2;s@BtruSymD z1H~Up75$RpD^f+jsQ4qPqF+$_p;Xb&E50mM^mB^SQbj+j_yeh;4=cVTRrE88FG>~t zeZ?2HM7a>(>9EyIy};6i9@V1<2H_I5L`W4Smts;y4=+Wfif$}Lq>8?NDJ)g=hNX~H z(bp{nrHWp^6xb52I2yiY%EKpo4hP@p4ZXW8#g-gWMWai0siKi3n^e*8l2xi`Xvrc~ zG`Mu#rYJS>mF%j;Ev}PAR~NUqP8NOT;uhD*qE{|T=6c2AwOdc1S&0&X5?5@Ji8vcb zcXQ>ixuxs3#=ykm z_dAupEmia_%1=lYy+iq1Qbms`KQ2}DcI9tM6}?UQpj6RYrMqO_vh9*N`L+C~sw*}L zOcs4ob-7g0pQv6TRrJTImrH#D{I-V~IdN#KHO?&p6Gc^;?fG@$=g!YmSBmHVv$M~h zTIkHb9>B$)i%;PH_!GFz-8aI(%a=^A(CK8$uBR$`n#l)Kj-Hk86?hZns5AtZ#jo$U zTVc$eYiiSMe)2w@#PBce$b>X<(ieb$C^>;^VgQw#5Yy556=dwEj!H<=1im#FCU9+M zxY>T@U&B!yNSXki?a1n~=5p9n>RG|bh>djz;;U|);Tb1iTsmH%JCjb9%mh)2G-z`P zk2qaXdw@>TE`Ku;Y_uc#u+whwMt#vLC#nAW=Bs@_UA0IWt@ST)SEH#V$#bOyIw5u|%K z7~+E`YT^?*zerOY)oSrQ^QX8I;YxOuNTGb4Vf?y|JyZ$uI^1Wk+S9x@Q_UJ9RNn1~ zmGhnRo(rrsqT@C<;wCiLWWw&Im!r5~4>NEvJq-$xV%B<)GF5XN5%B4|zO+yb+7nhY zqt!Y<1An<_Mte?Opj^V@CT%U@Z#=|gwFU!bzM9;KcRemK z`nSO`&8Ce2NdyfYYs5jWyVpI@hPF~Ng!CD`ua2^@prPmG!>f6VH5905nR-W0t&%l6 zWeJuXI+r!;h&$rtrVYcbwHzpi>~Ba4>puJFH;2{+3{!^P4`^Kv)?}e3=IYqa({b(; zP3-Mm!$kZCDH(Zq`MG|;8Z^Y@oK@~(vDaW7%2_hNcDJ30w<+F_G%MRXdBOSI>kVLG zxLwiLiwnbDF90$2($7WrZ(-0~h=oCJ?wq0Q8FE&Z5%~dz52eL1Z~;>Aj35F`kY`Dd zdInB$+etA-r~BJw5aKPI@2rerI1ZYRd_(g`vaMA+n~5fK=^pF#GzFnYyW{0zSzs&q zBGKz*f|f?4R&AYshFq+pr9xg08;yd@SplC_FjurSA6LsJn>g+2bxIwZkZ;&PJwM<~ zlJMDZS2P=?Jc%Y9u=H9wW4&ba@Y=Y(<4J_vS{s#4p*@?ltXAT!149(Q|6e*qPAwk+ z)%xBD;`?s|(fI;6neTuY{}zbO=Rs_LUHQ1O3Tp6`K}3IX?mKfFIFrxMe0zogk@++z z9!QS)WW4D<-yHCxSdrm4x+25ha*&p$y``oe# z$naW}Lck@PfDB(zDFmF5fDC8X{q6#XYy$h-1$Nm4_PGmekbn%O``rar*#!2v3oMX; z47cNzeI{@nBp|~DctwWO=wx@vo9??-HUSw?Bb$H>S5_$mUJD7x@H3X{*@6IS+_0164n2o%`gUHqHwTbE5hMgq~v zyG1wM_bS;0WSkyIA+QDs$T%F4b5u8(fcX7?+0+-NmTy}6$x?6eiN*H9_ZJ$P$2Aq? zyGTL(m^!EWmWon7s(k(Y-_BhDu?;Z8DIdRZb+l6b9@j0~zu4D>5SEr7*~EGLWHl zMMlKD6b89X1~RnD38I(6fZ1dqBQ#&m4yhCd*-Zv=_FZLckxF4eZ!(afb$>TrkDR17 z8FYJzK(T;vWs^YH2uGsqTZ=JWTdoA#riXPeI|fL9g!6yTTND2i-jr19Yite zZdlrmrju=SQKOA==v9*>;7VUxiH+xt6m@^zaG1l$>^g;%*mI&ae^Lk7%L z47uH%80T##u}aC&2}F53RKhyd&Pp>FYYXK9mNjs$2-^Ww>9PrjRhQypd5n$cofKEf z>I+7&K=PA!o{pTnev^R=t#YyzNMVrKWFTh)lygax$^W zK+g7$jC~3z&mg|ZK!(=+-TgRnlGtQ$!FC^xoQ!Ra1L=7R_SYfz$jRs?0~v0<`?~>h zIEef5_?rlzKuxieGCPb@#a{MhoN%V(A!Sw6k|@bW{;4=&%oeDCr-%g2|G zEgxO(F4vaX<-^OVMGB3e5q{rJ74LD$Nw~1oAlY81g7`26+TI zjXaDzggl7ckKBvggB(YWAxDueQbSneFp@&Th#R>M!4VX>0y%(Oid=%IkSX;O>c_!8 z$fN2r>POV4)eoy5Qa`A^UwyCo9`$kcG4)Y(S6x%H>ci@kIt+3nT&Kp>sQL=^0rjQo zOVld$lW`jta7Wa zQ{gI9b%pAH>QdDuDwS$V`2_I3cns{ooKZfaJgt0K`H=EK<^9TgmG>x*D~~CUD!a;> zl2sm7rj%i&TX~%lSE9-*ln0cTDlbv0lvDFh%s)Q=*!-jOXXYQ7KRy5O{6q5(&fh&EjP${P7 zo|t=l?yCJ3aUC+(UB@&fPzE@7z6e$LEgC9i8jW)#li_!*i**@SJ<@x;cCf zox5W0z}%&Cm&~c=re>d*eH?hTJUV*@>}{Q%eR%ev*#~Fu2RSb9nLR#xZ1(7EceXam z&K{mk&4y>)v)7&5@|wQq+}|($?HA<f1=Sq{Hg|l_{SPG#II;n5dTP{g!qS= zd5B-uC?KBJ%t8DE%`C((X=WgPQ8Nwk3z{j2pGSTM@pH&eA$}Hl65_+iPau8<`7y-b zM}7qH_mCe#{50|e#NS2!2jZuYA3*#h^4}02LcS02{~`Yc@pq8#LHupxKOuesc^u+z zA^!pK@h#x|}4e^7>KS8`7 z`4+?vApZ#Q{m4H+{0-#qA>M~P3h{l&-$8sY@=b`pj(h{+y~y7}d=K(B5Z{gbHN;;- z{tDu|kTVc}75O^EUqSv7;yaPALA(d~3y8mr{5iyTAb$q&6!NDK`^X~@-;VqV#FNNZ zA)Y|~7~*l{D-iES{s`jRkUxa@R^-bN??O&Pyc78Yh;Ko@1n~~!ix7_?Ux0W!@_C52 zA)kYIEAm;0w;&Hgd^7SHh;KrEAL7l(??F6@d>Y~rkGv0J4tXy`2KjY}S>#@bH1ZyZ6!LC} zB=Tzz4?xeH;zh};12 z0P;GBFGa40co~v`_!1-y@x@3A;){?Z#9u-Z5MPMIAzq3Q5MO}AAU+?7LVO+)f%se` z4DmTg2;#GmAjD@O0f?6%eu&G6Pejdv#sg8KaYF+qfjFgkt%%6aG_Qg9Q_ZU(KB>7H;!iYYh(Ffg5PzgGLHwb{ z2=NJxVGH#T|3{;P_ydg=;(u!}h~L+s5dTZF4)J@MS3&$w%^JkVHCI9W56vpXf7iSc z;=gIGg!o;}6%hYbb2-HCXkG#FUoDQ6sQ_z*|1-A4G}3{z2vu*guE@f&GKbA+UdtSp@bEGK0YWL8g(-{+U8x z|EPb4!2VJH6oLJteiDKGqy7m3`$zp_1on^mM+od6^$!u)Kk6qC*gxw3L16!=e}KUL zQU5mr`$zqK1on^mzYy3z>hB@2f7Jhp!2VG`j==s={|5s5NB!>z>>u^NA+Uec-$h{m zsQ(p#{iFU40{ch(F9_@(^>u^tBCvnde}lmOQU5jae6fGje}y~`;u++* z5WkK*2jX8M&xZIlS_x|3Lhd`Uem{ss3+>52?Qo@&Bp+3*zsnzX$QR z)&B|c6Y9qy{+9YbAbwo^?+|}e{cjK-RDT!Z$JGA{@uTYRK>UdMUm!l9ehlJ=)&C6f zL+Wos{Gj@u#Pk2TsWVfWkE%YaaDl)4FZlu0x~~=kjKpf)GB(*JZ`}<j z=4usN?ToUUT&;UZ!^LXdUy}}2`#0U-qv9E5cngl6KIAGG z2&O$3!n!lY@aHsfJe7b~T%3i*hLt~sYTZNg6}Z;<1~3{-+C2*xhgzuw)uJklYt7vTrT)mlZxua$@Q#^0W%Q{FWYik-!$(S>u_wjewVwyB!A9&@C4hp&uUr4%s6 zQSsJ+<$-G5xM+D|wQd>1b^k2SG;Dcd=#J2=v01Iz1-rGEKJUTK zc|6f}IG>Lev}7Wm%;#f`4k!z7W7#o;bHIgyOhQMiune1F`%43rqM&FqRW-!uL@ zPPW_Yz|+I#4Aeqx%)ynjHtlL47I$SLY{uPlm>Y~K(XBNF5O59dYV;Hi5@MYF-3HGz z+Dv2M4MJx`^+04`zBnZ3sQcXFeKxU$IN$7Cbdza;i&zqEuG!`N8DGLSSWQHvIr(jU zR7#Xx(fE6%rx?|6&$k8fjf-ry8jSd=b>pF(2}ZzM;_d_^p{%6}{^<}_$+5iASMUUi zDc0aH=Gyv{p~AZeO1o}!2Aj@K)*5ZBIyz}jdEI4oqo5u|-rIIp1*0AFwyplW5KQYS zzL4OoPO>H7g=kz1r3a5?=k@nYaMF&NTE0xDlI(W*f*W`o3O#*Mn_6YH;(xVZtz-|a zV@G93;k^4(as$vU%K%1*gNZ*kU)4`91LuNkBU4jq)wDu!gKA!mr}3V}ktVyI z&QF)$ddB`a@lWS1dyGi)U@fVeSNbWD@A^5u3ORnsTYS$YkeSR=Au_a7#Xqeb$vm&@ zCq; zXEI+nCtZHI$oGmTCG%bBhef_S=FT}eUgTSRcg&p=Yv;k~Xys!f&zJv>WS+0+heV#c zrXv|%;afa+O-B=%!s+P1Z;DJ0{)=R$2l_#g>8|NWhWGdu)2-=fB1bqKz2vmW@iL2K zj+gZVgVT|WgcVy9H$Q_X4@{BUi>`uy9+AxOWqrT+6xYsKIxrXChCy`pkYveg@#;&&4P3zwilwJmjwx4~fJ)+p}dTP>vKEKOq;2|Bk}7G$%zw$3ijLp-nnxVZnWthRw@KzHjFNUy2LF%;Wx*_tS?jvd4&q4_tR9P;524eGpIk% zYIrh1YTaMaTBIFVZ$0{7pgiP5HUUmMo@yTQA)RCGOkN)Hz1=bwoLvh-=0F(X*A{QhNxe3SjoR!E^RRzEdi$2PKhvRj5I_!uaF}Q@!Xcj1v0?11jHl!T8k=i zo8^wSJ&MkHMrhyO^$9KwTESE!W9<>~SJtHtQmE04w>j6mOB{Q6%2_*7^V1 z)NfBMe{eav^y8)bmI8}ESbWc-d*MG9er3U?`L5=a<{IRmL8QD{{SWFp)q2%8RJWs%2&?+`TQH^U#|G7qN6x4_vN|9+>2(vFk7B|{>*1)*qLWde`=bZUI421|A#n* z&CNs@q;;@SOQijT*F`&nSy#{5OBDvHlCC(cIJ=gObi$cDW{xmv1E|;5^9F#IFJ&de zc|TnbwRC#UpL0csQz%s1l%UpS=lG^G;4D|NwMso7^thO2gKNgT?o5Rw-4&h9rce1Y zo~TbxoLYx!c?oK}MGIpW*q%=3;i&kA!{zn3S{^f+HFYZ-dye(wwnJkHHxZ+JZuvYNUaSL1WHg+R&RK$!;`k(RuV7W^C$%=|Vl&imX|yIuZrr z_T^A5CqZpnANGYCTCN&2p>-9?xns#!(2VR$zKF&SD;!}g4%ov z>)0uis}^jO^u>D9ZLE6?akP~;x|``V;mYQVu3pV3G;r{2Uk26YC+F2%v02IyeT!p4 zrE0opbL(S8W04PZ{qc>gCmZ+rvULdzHG%p(WuMQ% zYK!?oHnL%1%i4~Pi4&&|K($#3YV|x}X*0EYH{ZocbCobuVor0xXY^$|w1ultwT-li zis@s?WR*DeQmB@epf-c$i}|{%W-oiqYkpm(Mdi_$P{tEnPzY4wwrG)Z8REVoOE`#A zmqE3Z1hvhAx1y!6R)}=^nsge)qj(&(2AWQ{wiJu!(_|XV6&V4I)H=kemq4|o1hse| zR_2{#Jyg|3&_Ya4cG594MEKm@q_GjrZ`d8NuqR#hW7z<4>cvp)VF_wwB&{V*y$Gt^ zkf2sZCR*auFG01hm!Q^ZVXMIek2y2RK+t9>=BiA*Zb(FI8%DRaN(Txy!&+y}#i4pI zjlU48yRGX2YRz}iT;?(n? z+O!0d(ToLYivqY~80NUutqT7+sN64c7bnM#~mfNH}M z)XGSNN}SR_wIK;=W%#iXrx2(%C_$|Zr!(S|8mg6$8COnaBjS__s`XFac9rAYMVwMX zwLS@IW#lX+PR&ELUI}VtBqAkFDWF=91hq0eo)+hB2l4;km|FOR^2xc6fWQ22`nmlT zeT#~T-2B43UOFvjWK4d{NRAWc)3k#+>hO2qIOW`c0}o_O?9`sI zOKfgE97wNHKE0bkL&e5wwbV0*3=Xmr%x2=oP&?^cHDRHMr))`Ei$bKwn4^ZWv6N>6 z$$X$#ak)|{L!#Ya`HYb_XN#+iuu)oIBkV@74GCtQS4^IHZu9rA6T=_xeA%>|o;g2r z9u@wmGhB#M0vW5sIGZ(t(J?Z1Ht4f4tBz<|?+H{A-YC}2eU(BZPOljmW(}X!E;GwpuN3aAJ;zLz6pOG}OuG#S-kU%s_u@sBiCx z#XYUgz3OvOEpy6ftNTGVb*)e>)>7+HOUjg^wBYV9d}mqp0vM$ z#|_4$-w>&ItFCCU;SC!N{`EvGNIFu4(Y{B-;O>}H*b~6&?$GtG71u}X_`zv8E5qbx zJZgOupZP|$vOzH{S;}oxbA#tKo3XMZ?M{Co>`v|}_fB`BAzvUc=%f`}hpt4sqn4qU z#Lne+&%@$GB;&HyT=h_okNasHZ8thzt=-Qwbu~L0_BFA1izCx*T1##oT8(xi+U$tU z-;H!5!ceVYdOQz)ncSxDFn$;px*<4BVW^u6^IUDIN#BB7Gop$6rW$~1K;4M=Oig}ve zaff`fz}0t5raNao6ZqBe`agOan|%InHesTN!N|8FYfon`H)se@>(L-tN`t$a?p-hfK+U?Fp^bIeUi5VR_OVpQRW2CK=VXfgpS!V>9 z^kG16YVQSsKd&i{R4 z{lBk(O#j(s6jT8`z0?PJ`mX{R`M&^i@4s&G3Q!U7;f1?EhV~VZi~Z9e>pG!10ICB% zgxmr$=D!%^#{UG!g6~&f3MvIYsCom)VgFo^t^NU!m)@?t1XK;Yf4&K_$}fQY@%MpD z@vl*+Kn1~j=Sm<0`y9x%{w|PJT|YYostMjRlbu1Qe*$U>_NNa|Up4iF)XIWbUzlZP zR=^|5rLESCBkOQwwI)1A*+axyn{g)WJZYigXfj?)SkjSpuc&$u>vv(Ba<*VYLngZH z;sm?XpSI^MR)P3tYz@v^|MT)f~=eH80=#5UE0vE)r*{*AtEBSX^U-evmsistdW*V((X3#K`It%N0X?L^|{(?!(KP( zIyy_fR4RDms*hm(me{5X=2wTORdaD%tGyPo+KH;(WfF?Pt}$Z{*NUZNfGegjCQ!7f z9>Drdv5ku;H>E^K8RqPuCJu%o1^jw2e$?tybWfRz*AC zT?6yAOHLnC43}-0d@Gmrb3scyR(0qLJ%{RpSidf|SSLHW!H-c|64twCaAWUlZFjtwASlbFsLmm(9~c)75SDqRpJKs^?N56-&q0 ztJ`Bby^bouHdV1rs_iYb<6c9Q4|ZaXs<9I^x6KK2f{4YKpgCUc>FQW3X0dl_0r5H& zv5nplq}o~!T`pFPF<$6`SMFje*TlR2TBd-~EXOdoN5HCUR9b9P7TY8pzo08bZ*Cq|QPA*j<>$x?5t=JPntys}r zcR9_zX4+uJH;Mr^TME{?xY(vBwlQxMo&IBX$yo6Q0_lbmCu{Y%&u^&~;r*~;o4P;HC>T*wJgcLe zwhfGkI8C`~QJbh6t08|SnQdl^f`zFU3RUqsd9h8D=c*ClSs189Gl>}ETJt)cwQeL3 zG}`k#!$*7RfHPxbz4-($w#kWYHW<9-wBwY4CTKRg5i@wGHDP0Iqu?SeCElrHb8MPI ziMFSdSA78MGh!Rgoy&&v!GI?z*a@Knb~Hl%SRNeAqh)P4*7Id!B{JplGaXA>^?t0M zh1@dstTDR5r!78hxmk>3y)_+CqxBgd)`&O5Zkh)NHi!1}br-MIFQ4zk%CwO#5n=5HA9EzK z?nI}R+3<0+Knj61N5ZA`=XzPrqk13KC&e~3V~fZ0W=~T-*%u(G)M{M&tv5m!FN|CKV(Mp(uMr_Sz4Fov0 z8Cdg%6FF|sosh86Ji?~E>&WixY$O9zm)2iu|6TTk>Ly_wuwP*(mkD2 z@4)&|v5gEjCe(Zu65Gh|E>fMu`a!Xc4A&vm39KIw z+sN<>QXR+oezA=VrysG6Pi!N@qepc&*7u5SWVq|7-iGx(VjCI0IAR;O*hYqT8;x&!Om#5OWKS5(KazEx}^ z!!1Q@V-efP@HtW4j`gn-+sJSz5!+lVwvpj2qPh+1Un91W;R>R<73;rNY$LY$LtOt?DS&$Hg`>94Ev!Cb5kS zuL!Y?QEVf_>l?&2GW;1-H(`Cf*hYr4g6fS}UnjPak+Meh2CT0Y+sMcvqq-66 zV`3W_CzE0uRBR*TXhdwYF1C@e2dff5{lBTJW}Z7W_ixLKOP^jUF8$KtmqD$)%NPD~ z;nvw7&H3iuwV>DhqvoW>0;=e}2MMa5R6n4;UX7?eIr~A?3zT0{wv;O%4&Y7m7^sY< z1XbehRt~)j{rTy}^ueh|rjG1!yf=Mnk6&E;=L_`&!gDu7W=8Lc z8w|#QAb^84(q0aB`B28=6D&2Vo8ku~X5KPZp{xAg2JQY6zX^1 zGGLg9b5w)y6zM3HYgF@8Br$NwneI4+y20d_2OX?4&hq&PWlfeF6<3+;4JzcuDjd4y z$qEZ&74Gg76g?Pr7ZZ(?Maa2|&So!0<_E)7W{Tq!V!Ov7c!r*m%Udh-8tt~l+3FCM ze0A`aH^wP6>{bXEP9E6D#wy%>%aX?7oA+Dco5m^}8a6bS6 zWURu$)QtxxE4*o(LjCR|JS~%6+-Anl8LUvW2+4^eB)S!gJ%~K6ux1s!rc=r z=;ch?pP(x40wa{8oYPZGHwGErI9B1%IGn7oH&)?!%r?R}g@*GO*td*RsNby+^z!8S zurpTS?&%A3O0q_<&7dQ}HsVw~Ys&__gPqdeZ-uR~3WrQ@GQ;Lrg}Y4;+;Tfw4uD-g zJ7a6r!0vgSqz7-A-*1JDaSC-qwmg|(eXPPETLx=M+)>HaT@8*YH9M4z9lYh*ScSW@ zZGmw(Sz&do!rhYtP)If_Q6f;{icK;RX9Ee3eUM>gtiqwmVY0&VScSVM2f%Rhx?CEk zP&;IL9cwXewG=|VN=WE7{4`q{yyg5@g+rDySs^=C;ciO-w>(*4Zk$4F_rM0G$GfBM zYO0rRG{Uh`(pszzjYDR?6=ugO9I}+j4C%27cUua$WlPlpB78f6BAd2Gc*@562YZ>? zZ-wMog+rz{nc?Aa3eV^L@Q$$xcdvTj8BYFAxfRy`oB8C_LPx!-ct7~d#h;5$;F<6Q zK;0*g=maL#eUh)00@IIhNfo`O1_BaBI8u$S`&0~-ZG^yaJ$F54 z^JdvnbYQ-dLK0NQ)*5;q^FKN@tonngp5J z+N)-3u3AX;SdbP4C>awTv8R{jst)aCdi7STX3O%OSS}Gwg>!jbH&m+^%xOKJ^0(T- z5*DO|UfUR6H^g|8t>ZR&`7UdW$IM|H8fz6-BaTWgnp$@S1AbjBC#}37y!AJ}arXA< zzFTzEp1rdOM!WQV{>kBe1EXE6uhr~sc|aaApcou=BkG&#NBTg@*s>QD%h`H|@-nvf z&$$=x3^kB(-T@U1_(1OP_}`hWY+`zS_!NQrJFKs@r;QE1M~ba>+HRT8pX1YHy3iey zs%8bza9O5Qs=^_@ojYk?T_rJ-*|?|IdBo!?(`FZ*>9i?FE<@ouM+-9+3Yfj-(pP;| zfsX>$la`TnpMRx6zZ*5A>p`{(s;?QlSzMd;_`B6mwBphwNS?R)92QsEvd-$)SB;^3 zvuA3Q{ravYm9Ca~iY+^$CAW(-X|9s+)`1ZMw`13~+p&ysbY3<% za>W^$MD`cnJO3oCLNW+aRe3BiAxG9TeupdIE|u5>>&Q5(ZN1y% zEqBxLNUqy#+VZqLPr6KIlilL9wWA&)X{Z$lI}?tt`-+~Lv4mOT9<$3=kr;U--nzJ; zDf&*)mc+c9G6u;0+0w|oo4@e3B;y!;lr4=&cL|o53$T)nAOe;!o+Uvj>#jA-0Qe2N z`|sPEC2+16uQ2k>vW6wWP7AlnSc|-;VK0~qxk|t-_%WW0Spo*a&m86(hA5u@`)1xbHK&_X%zl0LgR?iz z24-J6^ItPxT>i7=`UUf$GArKo7R2@)$Px%Gq zJ<5XeHOi&=N9R92f6IJg{*{WKDgIRPK1EC6QoL~PU*~>r?(R8q=AARenb*!NPyhY& zZ%yAiot(aM>c>-mJoe*s`YU69eesvim?yA0d;Rq2k5S{`dvauL_J+Ou74(B2t}`#& zTl2s{6pDJsXdXI#m^m~~dT_^d=D;}V-P@tSq2S;+>A|hZnajpW4{rI-ymXxO;C}wh zi^oat-hu?YU$VEf5#7Dd4WwT`PI~AG-Y`yj=$TwUPI~B>WX4GkJ(JgslOB2|sd3V~ zk2}CKNsp5r8r_L;(u0OGlYHfDdJiibd?GI#V>W~R7~Y!%2yq$MNtz+XNe>x9Y@GB^ zr$@(058mR;xQWC()Qw~Ba;RfM7l;y zjgubgn{ksXKGZj!ak_^L(mhUks3%?Hq=yXBIZk@0-yP$mhx*+_5_rXUpZZ0+q_2l{(9)_WweAPJVp`KhDCp}1e=BjbhLp`}V zPI{{Gjra4>awsKB;3AN#bAh7XLoz+_FiBj3p>6I(qTr@3~CIw2)#|OV0id21B3tb2s zBROv}0V>Fs`9j0hEwf3x)n{bGPCug$P;{_0Zd2`+d}f+j1~3J3NoHpmi3isX0d-pn-*|54<) z$!CD%rpx3x;;=nMiZMX&uSJe7ZvTC=p2+t9ag{dL6mJJUfbHM{aWZ%s9W(Vty%15TKO|o61)99$JvrZahLT;zxff~os;iT*Qr<;z=o9{F&O;59! zbc1@JuB0;)=d^XQTy_-JZCN+Z2@!2NL6I3!z~fxH87T!?V3ML75mUqwcY_T1wSq6~ zk9E6VN1~bZ3Dvz4qYu<(5N}$&2_A6%0%t zn{^Q`&=NeNC<9<9sj7n+6*&(Tg%Hhk+oHtKs|gl0+8sJ=Myb+-jSt4 zgLkJ`5mDpP8!+B(vS|Ex)mvg!u1dV>HKYPXZ8=_4@GgZb zQg`UGp4h4=p$OpGS^>?{UL}JS;5TrKJOAHII;`x?_x}|Gmk!KcFgrVQ+03ESk4~qi zemzy38kzjwq#WD^1Rp)0;65T>Z~2P%`8v}(!Jl8RR~hmf)<9qO-+!Q==-7+)rS(D( z6aV$PAB95La1@kmHksn$gKIBZa`X3<*oGsaUbD&c5tw_?dYhU|dP4x}7@$rc!Mqo( z)A5#-*x-j^&Gy+x6!1lhb+k_(lxnumK7xlYTB>(-?L0vi+Td;a0+ZcGQ1V6lBDGI$ zKEZuNB;WGo@QZ4notsCnvtZ|)=_Aznf5@4{1_t`ltaN>ZLtnHni88#zDHw`G;LU}? z1`kwf_7QyqQD3yuLVW}c^_qP|9|71Gt+%O!PWEyW^W%cpfR%_`H!F5Z^cW9D@_s*I^wOwE~?*URWKNw3LhF>+!S zx2m0lC0X{w&_c;=jAt2#EshZ`wpPq7TB>)I>0Aj3Z7?={fyrLdWvI_pndbZd4g)6) z%o=8j(^pNsI<@;G1J0UwVEnUj?O1N~ijh}Fb{TdJT_k%Beur;^4=(l338r@wjtxufnOvbLB*wQ#u$i!m9jczuTy_JB6?UbGmAgd=6RoeN&_8oX;-y zIT+%ctr^o@!l9S18E8kh`vQ!*l$O-;Iay_{1Pl%WI> zrag%uZ*qD9@l+zAD&Sc~gRcf@ok!ozz0c25Rfferx10~L=2}T?u%VnOAIR0sg~cZW zIl;V+Y9(XWW@#kr6--$z8=N8%Lvl%jAmr$Lvf>YTEuk1on%&hPrq36AnA*Qs_qsjT zIi1;ZGTQr6UCIN2<>B5`9z-=&(aJL^rX2MXq$ZrSYxsu3;WukloJMZ3+H=`RRuL}R zqg|_2r-MtEkRH`aeZqn3T&-m6JU#$l2^N3DWjFUeKa1*Ktva7u9?4njE;QlK^2PvK zXkKI6>Q5h6WOhqPr-O@qZp#Vj zQGb-MJe-@#qmP>xySexIF{)erK@6SWp)N0n1tv8(th4^~EDr@N567nR z=;O-8g%h&5gNuD`%L(aP9#9k}1K*U4K5l31=HBPO7VKUgozHD~LA6IR^srjlH&v@X zE{yzllhL`Dyyax{s8)zU56i>0sXY3)!LeIHIvrf>b6ZYGkMhvLWLP&PqmL!;Ztiui zVK4T%Ew2OiNQM?B!?GzEeJq7`b8m5TLB!Dc+`pBK=3LOcDH(k%aCS?^=jVb7#bTe^ z^72lP(rC^Fu~sr#bHP5AMi=W|x92*iT3b#=&qm&y3z{~SM;}Xdi*<0Z&uuv&J<3B3 zlVRMHj6N3Ny1DoHIbQc_)%o0(S4es!qdBueHzlKwg_&;2SlrBN^AOhmRx*?@8AohN zMjwj>-IDS7nN|1F=zMO=iw`|YLji_v95`g){(-?GVRXH}48A{jm+aOd*VuVui7^e_ z?SEqQqETV=kkN^eXGeZ8QW`N%-Z*)}q-%0d@RsbY*~bRY8n|D!H@J1^gqbODDqv-- z?9|x{z~0l9iT7vIldnzOHF4I=(<9pq?FsJ;+<*KpId%{er5QY;bVvG!#fYXHgw}OHoe`zU#5ODb?ww~(=Tjs$)R<DXDX?zsaiwr^@)wTC(DT; z%3UTxa=i&&WF4V$Ze!S*isYs=ZH7OQVo(9KM7=`9gVw65vN0?A3PqD6S&#X#V#btp z2W{mzqL+PZp5Y4kt7U~RVYQ-kpyZCG*c9zaH{^b~sI#clmOvw@bIDT_1uy?>Uf&3> z!!B5zF=IMRZZ6lh%x3dYnu^@)GZMbOZ$aV%^i?OG=Y%CXYo{wH5XCz z6-_0rNep;3e9jMk*!BuzeS%3-a>dn!ptnNOb-BfB7aeNE<%lDx7^=l$QI#4s<%#k4 z+YG0HaB_&*P>J&OtXr-y8q!8CXeTjwLm5wVkxg3wx zt?cAQZAK`m^>|Hgd4Y=27CbKK0ufy$2`#|mb{IqiAj;m&79w6z}o;)?n2U|Hz?e6!9i;>$-2RhyKObmNE6|7)S(t( zkY!hj8gsT8%A(Pbs1>~yWs&kn8hnwqSW*?y>`4W}h^y)@kX1gKaEtof)UNO@*uj;i zDs11elwfg}l$M&RKr?8jOo~*xoU9-$9?9B^^<>Ez4AgQ*wIodCEaOut!Ue%tb#Vr> zJ}27UYHuC0XY>Ww8P;sks9+3O>?QxmvUx@-;lvS-pQxE54ACej!Z^!kGBz5qMagK& z$s@2a;vTsg8hO2X~4S3E+>KK9`} z1E=wdPFV>cxdg3<(3}tnhCD`~t(EPg_;BhTT?X+?uRt zwhMaF>5ihhk~n&7n~{nH;tCPPaMBm{R#Dj3;qtoHNLQ@7SSF|~l$G^xl%j&c$k;LS z42;+MurNts*<7L;HtI|;ty(^#M_g_;pbe-h@i5x(lV-)hhBhPVR2AfuO5;m%rM%u6 zF4YamXh`t$YMZT)#5F>R@Hw5%4S1(O zfiCk_Ux{>PXd&S&VsNXva1qn&pd0UiA459ImjX>9p3s)Dls5 z%u-0DWA?B!i52W_EoP-gc5gGnSkSK1`)o!^^!p)ith3$HBZ@};C9 zguAO_SF{;^9qABNWHgwy2V7A{G^90?Xa*j)Cdv_mmb5qI3P;tY($kde>NdmIuy8gW zAyG9_Abb|wU_wG}i=okQDhdk5US~YzLXimv47u@eo53lf+K z#%Iuhr`inMm`~)?QJ+QaK$2KSsg#ptt38Rkd4DFILda^(n_=_zWHmEzZJS{)g?t{h zJi~`mZjo_k9J-vjk@U-1MWdARF=n&77D%%s&L?D3ZHC3hI#`8LNl$99csoAz!WxnK#ocrHk2O$IiE=$w^QR>N!5?s%CR3CuI-c-<(v<*Gt0lunx*tg)Ua zl(^PZa8r?3tVpNpHjSLE(8%!3^9(#`a1=}gPvPNwR229?!e~s9J}Vhl7*!fN=yJIj zESgH$#|PUCU%>BEAQ7EEW3j*sfxW>}x@K(P3I&okC>y*%EQB&SL&+>eX0YiO27_O0 z0R1m(5joH<5H^ez3o51Dl~9Q^RuMf7V?G{@Y8p<;OQ|zfcezBw5}c~0n7XXZ;5B;K z`t&|Rql%fssj$k0!a{c;{uu1-b>*tcqQ*j^*KI+j-0{^gOjhE-?H<{K2k z+Z`p6g`+pFDVH-YIB(GKXhp5{gv|~3{aZkjo0895^2IcYw98q;>RiQAG5MovYoM&v zda@CXL+L_UwVIG~sNLn`5PfE5huP6DrV+5m9L0npZ;uECZQUQKunB`E%4P~RO(PMj zq}UKg8zNLvgG}BHGa>pf8Sx+awVk_O*4?Pvj8!Xq`$Che2}8o*$)%O%O04eYwbcSq zfqTP79qT~B{E)-YepZ#!=^}9RTa{CYLY$11*g_hy7dcHzrB2gm#KW1?qJR34wuU|t z6}7rzq`}o}xSl1oHD5p(vCxLJU`kXiSVCVwRymasBMWwD#{V<;d zd+KzOS~_Drbw0mo_s>c?Jla#Ilho1~=}B3rz6}rebggp-Yd#Bxdg^qNTCT}@>U37o zd7Z(YI-R7J&OlF{&MlF)&d_H)bvj8+ouPm9)alZG{->u-C#j|L_ntbPn+o&k`CCt& zPEt$fuf00>g(_RBmt&j7GoN?t(_W5kMrt|sNl%^5W<9U-m!3MEq?XQ~d+KyH>v^4z zd+KzOS~?%~)alIWd7Te?>U5G?Iv@1Z>Ff*Ub>8o((@ARSyw_8wvoDy}dAFxdC#j|L zPEVcAzF=PG?VdWFq?XQ~dg^rc1@k&@_0;JkwRGO>sngjP%T;E()nXg%7T5t zAA0o#3sr(-ue59y<9u-&3cP)Y5sjSA$z9rxVi?I}ISR0|&-m9KU!xHh#d^ zyJJ5dtB)N9_x>LoJ!_PPZ|T3Dy?^%fS=a0iGtbXlI1`!KZ~E=&8>Xw%Uzr}9dSL2| zDfiTllP^qOG#Q=TAG|a2{gL>{s^RyBZyG*)xG_9D^oyZy4DB-HkzFEtNtTcu2u~Q? z45I}cF*q{t(7<|dBdCExfo$UE6K76P(3OFK!SDU=zoY)wg9URyzwY2+*_!1t=fX!= zG|1=cq{@IXcAt?Ut&CH@&d{RbU8tK~rss0{yp+#qqFwrRx)#;ejoR7s;;7q$lbDai z_3QL4s=XVva{}kaiGbBfnT!GbI;2H4ccXUpkF=K~EO0L{KnJZ_Xf z;J5i$3^%g+b=$P4dv>92c2k_kZM6m{+F^qgakfR>qZ_p|n_L!Kz#A}A7%bPB7IpV- z)Xub8ye^N&>BmUu@N|p1TQ_QFZNgzVN}95hR_O3li@Iw!>gT&Pg~Ers2#b+{pL6Ng zO}41JbfIpxDn;7OfdJuhxNZ7%phexe8@01<;z*;@zh2b z0An{|HrVJ!TGZ{jQ9C;rjv*Z$%*!&+;o%l_+b-12mI*i~Jc{6AJU$vuL!Mi32>kDV z{-4eP{knZyRifKheyYxjXd+-;qPl9g1!HjTU1py zYG()Iv=Uy6#o@NY4rXPGs_a5lcJ>lZ-0TZ@am))_&fYC*m#+mh%Fd>a8GWSNZt`2; zlxszcy1JWcXFu#Tl0L?QGj=#!EpJf|>PGGChkbT2&)N2 zY1{{=AH*!To&B(n zLA|WQ#kgQ?+M`9?zZ1wME^h8@03g`;C69%VKtsaH6+Mi~8k-shfX_$N4;FlaDmh(BYk1 z)Ro<+ojL5X2Uy(UHhE0?bvw1Fdv~L5mQ6;V5wp^m1&*&fwy0g>19xVVm3Dhs6H0Qh zO6<_0F7Kw=*>Y@V)JAXwJl?2Zw|$GcS2t>B%Q4z)CL8CrQLxc%*P<@#M(r#d5850d zP4Jv2m`V&x4WtIfADjLz7@T=@5*bJh%VfVCykImrq8U3M{&{JBPE-!Bi^vEJt@%~8 zieF_>guI5b-P-zH3|S40D3(K97+N)@(JH}^`GA!xN3FaN@^W>%wZzA$T$RAgRlkbQ z*sW1`NDMXDF|z~OZ1@HEyveAHsJObX`FBi_psbd1ScQ~`G+J{pX2`|k1e2(d4YcZH zSS5kw#juJ@hONm;L``X3SfWaxEe%ym!$xZ&RyN`G7?XY#$w$;gQ>#vRTUyYzCLQoV zAFNR-!qW~CE7f9rt{P6k6Je%m>)AjSKC99sqc*M%pUqSoRhO8_TWTg4n9$41Q*5+W zkdxJLiPRx-##1yJb5-15j8=29nXNC&$fe3M%w7&Da226O%A7ALM$IH0;LwG#j8QlP z4sSRYQ-}qR*HYpvR&$t+hVwYZ;__fNsdre}nw=1QiJWZPtuM=n^b*xFizdma3?gSs zvV_H~qgX>JW)>I9GCsn@H06dS*sxFzJ)z-Q-p16jN^6WJU98sBFwwD^8wL{9p*h($ zTVa+lOO$11Wd(jglZx*GQ$~2a!Fu!7IB#(}gm%Bz(U%bpBhMt& z)c}@C7q}=MEd@&Q5Z5w_^ldcVA4iCS&6}n3aKy!4^{E_NZy0z6|4BZol87R0(>W#}Ez0v{c>SFz2csZIw1T zEj4F~uwr;ND(2ycxx-eMWeQ@cYMF&29T`#;=aUE}l&q93AySHk`Z7}}6m0loaz^cs za2d1L5sn7p0Y8qa)LEWStKCtu;I~w><_ZS8|B0*}3wbv@Hk~V^0yJh~8g;=QEa_7hBE-8m!x1{Ho5g>nQN)C z%wjH)J`Lr|DAFX$$HI*g5^gLsFT?U>8|Ks6%b5(5$dw%>tIwB+F=$=Pm-0d#E0(;p zDkZnn5^>ltVU)2!_0qgY{6Ax5>#JqNQg#0p8u6rwwA+*S`&nZg=lw-rX2FK3#gZ|B zw;I*XU{F`JG}Jb%LMOdcG#;jcBxaANjA@!kigW~ym|*M6GS^bQFqsW8yV_E;s{?UY zmcv38x<>h0@4ofV&&#l))>evXeI6IWh`iCQjC;$5Y(k)Tht-#a@6Wg~p0rX-Hm8MI zHr0&(H#v|Pm^pX)fXTZ6GycZd+R^QYuNXRX@JaaJOZ9VN;-uU-+&f+ufjz+|3olb> z6rHO%o7T&=qIH8Rxm3*nD^)z^q{qo+44e-w#8_M)x!eL1M-xF=^1h0f!t%*fM!~Y# zI_FXnb#1_4cKcmS-3-6@Oq8?ws+-Eetj{ zsA;cVq0|!Ag_dYc!IC>?56H8u$sUMDHBmd`Ad@Ja5mJ$Y-sTeWMWm>9L{J^q%4$K% z>OXhO>dh7!rS;gYDyx@j3Slb60v=_Cv*8xnTT@ajig--O0<{`T*lTKYByP%x4WW#? zQ3Y$Sit;S#)|tv&LxU*e?uJH{dqzQP3!$zRaP%mt+tBwfS_~|T)<0ZEb18CDs!6QN5XJD`19j0S~M3u zc~NgG*ogxCW?v};GpvIt#iDf;Y7M9ezd!5?>k}@8t(DbjDXTwRIIDZ@k8C|zO=*_y z9Yj;))VM%1$s*3{3!Vm_4O{&SWVH!V1f8l%o)B1hHs|wWfl#LE^H;*LxH4*rdt4S@ zv8c*rYN;IBtko$gt3T+L)txhtP5Vh(Q&un848%&)0js&_hZkFs=70jvasei-SRkuW zM9w;uaXi&%=<2Crsv2X8o;ZiPQ&v8J2wKin@M$BOgp0Sb6DK8U%549^^y&X818_}WTG=IR$Xbd#PGSAeSYum z^L@1`>ro}LM3K;iwPuYctn(DoL`h}R73=nf3FGTkLn1&s^m(}8*sRrYDXZV@met+& zcekdjUb=m}d?swus0gz%A29QQ5XWiYA%z7x5R=DC8Eb@_s+&b45#nopH?B`d3VvLT zRLf2_F2s{=nhFKgR4c1vQdYmyEvvilhi^?;y=41;)<7fbMateZ?}}gvkyS+m8ioXe zY3-PSV3n{(<+ciCA_o^q3{{lOs=Y)ukAz7=t*yIR!mM+ZLV9o9Sj{!x{|_Aa@xbim zv*gUjGZ)O5rr(@Cds;vB%G4=S^2w(skDXiregX1e=ZQNfViQy2H;xC!2gj}$bB+Fa z^n0V&$Xg@d95D?4X86?MgNB|NI&Nrh*+cL<{#^#|8jKH4L&N`pA8xKR9NX2oGG&WL z-3gMg!;n@?z1lFiTux5{$rKcV%I#@5q8>ivvpZu|)GsDODvkEE>Pv?WIhXS%xeio+ z^!qdLnUz=BZ$9+Ehsyg0jymz>yGOY>v1OPJU{Ng{6#b|}C6qF(vLRP9`3MqlD2r^# zZHtIZEyJL-a#`OtY~JwF8_WK5jO^y`KEK1Whu=beTOYk9LqGNN;(hO*tx(?fE;m@CDEAw{mvy=9Bjv{rZ884cyKTO`dw> z?%zLP**m52z08Q~4Q`Ha8O~)GQo||oF!Cc4vRTq~!b8&BSEB+B|?t7xwugtAFen`EE6J;T6++C9b)46}_^ONPYhgzuxO7NBzKt zUCYfSTZa1x?cFx~z!6`X-A{a_xI%EV966Q6LK`M!6qmc^IHoV6>zc)O1(Yb-a?H&I2K3IFl z*H?T<76Om|YW$VQ*G;@s{0BD|YZ>k%Jb2si)wld@YWL&z-1DSoPWtXS`qy_YD$J)} zrha+)o?n0Pt%qN~2JpH$0wEz6${VG>6p_JNM)U5f=1nL+FdMhI+jt3 ziuwNdv$yy8;8NR(58v@qKlAC%XYBZ$%MW~R+kp?CIP7-zZR{^MTDZAL%W%11$~aP- z3Qbs?0WPgqYit^mn@rRy9&IK_V_{b~NgGuTMTjJUJYx-9r?nsY(peYpc-9S( zi|*Uu+gFxuf12Be#V>#K!<$Y??ifuSv(2je_k8}s>z*S%-uE$r@clTbkg_V4y}8jGBNttg*RMxA2)YY%diO*D5RW< zxnTI)jLC+0GTMwt)SPTEYLCL>rBTFbgi&OzRnvUS8=u(lyFXrdaA21W%Rj-%jn_Zo z@chMAHsaM2e|Ge~qjw*~%^le?+(#(*w#S*F7x#W_oJqg*%bmab;BRkvfO-F%mu@)f z=}(6bejs|K+5-EEK+A9+!P(n}cYAeBc(3?|m@488FGv!>#-Qp4y_|vji9M~I3M)u!XCuq_2$}!e|O*A zcf6T?VCuNc3){b5I{DA4+xGwU9(oQ-;p(ZAHV4V}3$E}rZ!|&bqgX_5~4?b(dlgLjpquV|H>O;$R zyzQD@4xhr-FF$?qnWwlpwq>}FYbx7@_sZY>34Q)KzdC`OJm7cf?GAWt^;51VpS|*j zkKc!Vb-((Xa&C@k8SW#}ecSLWw=F+##pn^&%*<~8*^dL?Abzvc?)Trgy=&Ly2bW8C zou0aqoAa~`_i@8y+wczKNzIX(9nU=z*=yz6Q|GQa*FE`hr%vJK z=$2s{TB_vo&N>qCDhU|ZFlM*bVcbOzUeTCijZ`A4BQ#^AGG;{l(QA4L`c!_??X09Mv-1N3{I5#}}Vd7QUgMnRr=U+w=af z{kU@H&Bbpxf8Tibg0gy-3vPP%JZ{e2GHkMD!)CRHvLkiA;I=x8wh%nWRMrNtsxFqa zDI@hfPKWb#i&D`xJf3=S`?c=9&%Er4_s_Vxa`k=>-xmGKA&qT677wtSZ!7M+hMRM> z4EJ%VW!vxphyK!j&Oxh>{&seVJ$2bTDEosC?x^2<#=G0S#y`I7@;~jw&5^bhqwt^3*5!nOzOSA(uXvHwE84@OSDj!!eI&8Sdkb%C_O>l@ILu zvtv#=r1XJ$NA;bjZrG4m=EqJxtakl5Pwg4J=(=0EIcLjoAD2?L4fE4OKU-#U?fa$U z(`VlMiT>Wh@6EpS;0_z!ojm&l`*+S6JC~btv<&xg0cG3poaKo3FSvHt|6%@(NbR7HKj7x9oaT~-{lYA`S5)Qsu0(gKKaMbUisP8>9^idBYWN( zk$qU&?yFoie`j@Ymrl|F8d?Qko;iVpQmi^5D|mx_a!2QZ5tA22;@$ z2k(U#)CcahLM#tCv%L(m#TCwFzI+_GAmW5f^ zwAXF1uyebz$%^E%C78vKcI=Ag9T_mZJKu6DUk>xB$cp3PGtn3f&kUc?z~5Vm7E7U0 z^PwY~+0X*POsw9NZko&$Q;AfziTGS3#3yrN>0rHHq1g1dpcpGwV#Q#qWR)5n48N<= ztJDfrvqB_8Vlvh&@?%%Dn{U%ngi_V4CRWs!1K~&^?9y`KuqMpbLN)IyTO;C7DNNT5)_c3(MK2N3mwLhc!D9TOjGjA3t;|m~%GU-I>b=m-ZIo#kM=$|H*dO#}Kwq zyX&}{xmivB7aCvdvh8Ly+&dUVNf#Lm$~XI*_H(Tffqwo6TcZj#k8Z6oZ_z06RnApO zCduo=xNR%MZP$7onalK~MYBiH_7BlsT3O5vt7GK7c zSX66lCLOlM(=v08W@}s@8SH25*>Z2a*w(0Bs5SO6`)StXes29%x=BYF(Irg7JcQzz6$3so?uM6C$ z4gUnqZTj2hw}^K>(b*?`-gc-GDutRuy{6@3+n)|6+r#oE$6LOIL(Ory^=l>-$^7&8 zVk{bpG*j35OZy@@h+4+~Iruk+=Ko-KqeN8A4sF4~AEylP>qob!6LJnq$wBgAM=@Zk z!n*-XR;FlBuh!ReUV3%5fwt!>Uu5tn%0=^PZAyukDmqf{PlzhiqAj`!CuViq3)!@Y znexSema-_a;Zj5G)`xs57B9Hd1|yYp+8bPiVy*62sIEl0c+|=w=86ujE~>j}CLMM+ zr&Q+f)}*^W*iWNfy6#5v?{+uM+2CfA;(q$8rh$&`qkUWa|5C@%78jGLJZufE>2cGM zXiCh(8@1-!{E7u{--b8sH6Lv^)L^;DLj&0|&o{f2f3_$U{Cx{8YSH}_H1$QN-3p!3 z0Ou2q&WY&VDUpfQHNH$PZpk!Uk#fwI*EcNnWX)Wsd3Q{gcT~M)G+S1zUd`4if^y|a zgA0un3h)jnQEzfGQ5;>|K;KL{YfV{kIqed6?qea6onUp02;7&rR*s5pemu9rCnZy%%v-WaI&xpXk{ z*FFjVef+;U@ZTI*Tn>cRA2k}I_MIN+XDMjQN_u@gB~;3hys|_aDmuaDu~%xPhEsq>NT5ZADjA%*4xxP6YJyiIoQV@|Dts|T1*T|HCs#{yZ(!o>Rov{ zKQCyvm}t`xP3IV>*K9g{Z1*o(Z&TAruMa|<=E&B^&f=nVHXGTFhGNZ;t&i=L zMT>Q`(=|}4*-radTVAwO?<(852inZ-=KKG$fzZJ0S7-h)b3DA^@7SroPSxS9epT>i zP@ec`LLC2KyfF6OSZ?&4QDNjyBbnhhhf_m;97@Pum&FEO9gGb84$`~-tlbEY49BRI zeH>9++Fp6>MtHQiPY0H?&sw_?9zpKYfh8R;)^3DHm7xQD4C6~XimcrTk2Lq`z!Db$ zJo?4@Q5^YppW^@QWrs_ZwHpR2;hA(^f0X%vS9q8koX{n0<9?|d9fh8^ic$B|S2bQ=9;E{jmKp(G>OI-x;(||r5SmGjp zpAhuvz!Db${L}zC(8nA0QWpXIB%x0SmbeJurwe^Lu*5|GKXHH#^kH;qivSF+8aQ;| z(}A%I#?oVkv9Zx#jcyz*jvg^OGxF<^D@ST07I-S)cf;2YuN`&{?>0ma{c-vVc=w-q zdi$y0PF**3{7imkYRWOS>*OD1o}RpE@}x<6=8}O=r(c-dYjza81AYol2R^V5Tt~QP zV#CDI69*36JpR}52gkoPbJ%!vXx;e0*ki+v;a$g7vM&$)VerGDn}$vrqK1~q-j>}i z`}*u-vlq;!XZ76|7o@+yRH!7ds9%&rG1@BF%X$xW|eq z`H(!EvvL?y#S_TPy=_J=#brJ23=<2wb!mTzftOk+LqS$Y8n}ujEvMpIUKdhD%#?lR z<~Adv4VIDx+9aZ;3R}o2BVk90O~y6Fv_BrzX;Fny>kZ{Y4O?dBvU#sipEU^zh%z%W(`M-WbV{K!#tT@mjQHqcE$sCr$}Us9 z!mAs>07^7e#*{W;)0zk7z0wv3xpS{*kaq=`xB98R+l%d&!+rpcU; z!Q;8IL0ixSs^48g*584a`Ql|8bJc7kSS~{Ul7O|Sz!72lIU7=5AJi1&1jVg*o zo6Rn(wHb1CnGou@#f|w455KkoRL5% zsxpVok$_<5?4FE;bjFBsLO?Nt1`863RFn}M=E3D{M!ixlR47cLG^#7CmW<;*d&o!I zldhoNm@PEO5KiW?3`?WzaA=;9cOcLOfoUk!-g=FTCgI&^6(3=&7lf*+7zvY_h9O-K zHgZFMYBOpUvLI+v)-YM9M)76hpNVGu+deE-BLe-n}hXQ#z zEoO27OGy;+S-k^GRZC)>Q~Rs2G@Dm2DCw*yWcRf-DwH!|3TR+msHz!EZi~8`LCwXa z&t!4AY+NMca{F91g;t0RUO3NiM`7HonmdwJ>wP4Ku?@FAhszz6XjH|zSuLK@2g|q( z_2q^hRSd-jR|+Cq52{RNs~F5$vgMeGCb_EC<6x644}+!D5_K)Di$yZYV8YXu5Sa`e za)ey0FNyJ$l-lP=cpP*MQB;knEpI6nM4bb+9wbZ24s1&lNoOOL(E1w*%x_It%&JUA z)EToevq|YxRE4S$wy&}-q08jdZHaucikow(OaXoaRMZPG6khw1HP8`C&^8o?atfhy zlsus`n+RF8&B#?l8NWeL1X6yAwR!SP$>uZV8kE~pSEY;&T~tpX{(4<3C`Q>9BbzTd z%P@2pt#kS#4qh&Lb2(?xAmqpi>-R#WyS#;=Cs@fR9uv(dDdvAW^Kp9H)E$63S>AtSe!+`eiG z(>XGN%Nj7)eFf2!8-BFymC$HVVJyk<9)Aea3Wzf!$8fY>Nc(E$STbVIhzVB&DaIHC zoZV(*YK2@SS5vvt0jn-aISfW`6b-U5Bh66?(d8(X41$}`*(qY+v3Z8Yfs{NAPf3^X z$wi~i91BwpUBzjY!yNF3-~kwixnic6qJ3mdn~_A}9f26Uk4TRYWslw$&e#)bA?4E2 zRlJ(1n;oiz(-kWFLW!}X+KdF_GV(^e?#Ps#q@h?aW-3*ktDMgV0wu3W(3g{DES(M; zw1n)KHY3hEELmm47=br{IuQrutfv}ApEe~#^^As9Ax^|*3TpKkD=}J{XM~Meb5zNu zqnU!fkv7%r9%YnJiUkYC`QxR0)L$>B3y7;!9lT|pp+gM8tRomg%?({8g(~W5I&0Oa zs&w4!jc}q|Oyx0;V51|l`Mio@VLThj85CaL9}==k2C?}El5-dIx{+3E>6qq zs4B`}4R>7UY{=2H(yXfv&etn%UP)9CO}ri_m2NT?3c`CNjOsj+G?el+T%#29WKD0# zA-?L^D{T!9W8G;UX9MN}+Yn;{!l5SE)un66imk>Nbm@pa4o~GHynaH~>KL-L1rKOK zf>rIUpa~6;;*H_5-03k{6i%fns)#BBNuwoM%Uk?MwAYIB zk_`dT;WVET>1a%}W2Tb9JaB)T;l}xrUmXlkK~4|9Vkmk_t|+O_d*$&2FF5i{9u0?$ z4KJ_A$iCKQEIxn{nD>gbB#j1lTFa*mPPaZ-%VjB2lThRYgsJ2czK{@b69^?{(ZuL& zZ4JU@@zgPk9VsQ9N=JoelMO-bj?gf`q!nIi7}t9J#-iNRsEcFs713Vv<4!BZdOc)G zpiu?YFfmn=TaIP@KGwTcWW1fm!(71VVXQB-1z zMyMz-a#awHT9mxT4!z&@%H}nhYL1{-$0K;fn}S1^CS%fRD`+iXQ)Vl)%9^)%y((i; zF}}wU>EdC&dw4yON-Va25^Yajzg6@^Py#8*jK8LDZf| z6}4nJh*<@SF~bi~ZDOPC6k2fN85?4@D+)Fx?ur_fv^z_`FzqYQMwg#bj+DbL zRBq2l^&X6O<~0tjLhaH|BW;GgF4iJB8xJpi^rrG2U(hH~Xm+L}k>0VoXys zB+8L#RhvOuD^YV%<;*9;6ymK{7_S2rl=&EGlbZ@Lm0y8IgEbXn^(CfPwHYoZNLg@2 zFzR%KNm0vJ8eTXd6V+i>G(|F6V^XP>qpq-%$j!{pb-AiB=W?1o3QsWWOe;{MOXc?l z+(n(rNF+>X!cO{N#5_Fcw$FSSzTrIh|A)6602A@#&hv?y4@}W5R`ojPOcIB*rX0GtKZf*d#oP{0V(U?tcQj86Pz;*S&0Pdqen`^41~-<$Zx#8)Ts z6QK!u!Zd+Qe0gH0iLvod#@`r!Vf^9opN?NMe&P7~@vn^+#>3;Daco>OzR&p1Yj=nYe>(NI??;O2u^y1O8 zM^79TN28>I-(!he`L3j z$>F~bzdii&@Gpn&9{$nrCBx?ouNy88$A`Vcwqe8Y0mHiwPYwOg&^tq~3_U*dv!NTN zgsG#aTvJC(DW_IUZ9g?U`SIlIlg~~*IC<;jm6PXBo;i8^WOgz*>7GOgwPD|Cv<@X zSKJ292j@$0+26o-!FMIN=lk-+Y;RFW$-QVEeUQD0_TEr zB{=m6m;-YX9DNp?1J04)$PD-<_@)G9E5O;{?0>@N{_X%9zy=9^@&Q;6)=ThX7JLJI zLxS(!0sGjqB>46b;7o9)1mD;JoB_^|;A?+}i?*jr@b@o+uY<2k@V9S))4*vGeC084 zDmYbwFJa&maEb(9yaawVcCrMYzZ|Rs>m>MW6`TZ4lHfDfgA>7t5`1zPoB&Rc;G=Hv zHSjeFKJ+;FD)_1dAN&d&Le@&~{y&1_!SNEjcRz3(I8K6h90`sE$4c&JxDxf04D{`O=$`ZUR4N9OS!Am{@A`m5bksB02QG(ym zfC4B;aPCnUbT==-Z$1o1$eaY%X94^t!86VV0uUtl^;y6JUV?Esv>xd{#e2XIJm_H=*) zT!Pa@UJ38KePc9pb~s91C9VkNbpZ9!QtR=3I1_!a2PmDg0DUe4h4rw@ONv$SHM>!_{u@x z5OByp;qxy)3LXcKOYqkRfnS1OO7OW)!DHYt3I1vrJPIC_;4kk3kAO!c_{acw7(6V& z2Y&$`0uM>>feYa*>=zQe_bBincu<0O=fKaw&n0-r0pJ1ffCO*-2;2|um*6e;fcwCG z61-`5a4)!5g4axfd%!&s{2>5813#1C<#KR0xLbl732+yppoF?D*yrz2|h-__Y1WIAGsc=fJ%Z7y$X~-DZvLf0tHY=@PS9c zYOq>@_f3I=z(Eqc^Cch$atZ!)92^J^l;EubSOr!|@aC()0pI`$-e?BN%7x*&xvIH-B7rs8Nl;8!ggT2At5;`s|VEq-aE7(8nNoA?X>c3}Cy83PkfOc3J_ja$a< z89QwB_EFu)jU%gvuNvNW_R?9?(53Lrzc#Z6+?PLp`ljj9w0i1&+0J0i;5mb{17}PL zlW$DMz;FNUH>ne3qwpxixNN1?YL1%iYEcvBZ4tI!Md`RQ>Gx?uo+J}7#Ig!skc&ji zo@|L-Nfk3FUTe@8Z}%DEKC3#JwZ$D3O;9a|7ct{%y^cc~p17Y9a?2~Ocq~u}m*CZ< z@m#@^wD1Ri{xi{`T{rmvm*gwahC%L#Q-VobV8ZbN;fvdusyZ1osFhKJNGkPej1j6U z8Ks)e6Hd7yr!y5DBxW%b6P%d|Y29HXiYW@^9AQo}Ox@!5FE>Uqfq0Ti6eIN%ku;+y z-swa73;AGRy(NoFlV%MbLxS_96{>hRTX9Dfd_~KK8gMT$=kTdgU4tRwMnDo`!xo%b+eAE0Q&P*aPnvt)ux4A0|it#Rnv7E;xy_XfAtLF;}wbGfcyk zlDjmipe31xtEN$(IZ%MnU{@+DPOU+a$S74|kLbYvKla`P&XualAMZ*kdoPG0ZXgZa zAUxWay%SU*`(8<9Pgp`#QrRm@RV9^#y-`4Ad1$Ms2#7l);5LemJK(;J&Ww&aiW@rP zGK-41{Kns{N`+2W`n^;?_3zC8*U!)Aw4}cG+;h*p=kE8MmOqhFAtN+ zawWvJx(<7*-70!*1tPa*s7Iokrd*`!Ff_r9fON;WX#!sPQk{IK)VJX-ucec+n>O0r zNMIG5hWpzCitrHwgsdmhovug*)>_@$>r_@#!*Dy0 zB}Zv5J1mmU6j2q)AiMKP87#wo8#5$}hENNHpYZ3K4R^@o2z1$K3n&A$1Dr{cB;5~o zDr=s~#)h%gNxCbQW<1TNd-+}`Ge}la4sWW_VUhj1d9{{;ULE5F!Nf<=&Ml4 zFx@K*@+<;^MEFo!rkfk~_3@y`Rtx0GMA#LJ5Bei^)f5g(P>s5#u> zQMwXCokcWnis6N_-oaplzGE}T5S2z_)l>DRs&oa#of(p9`ABRe(u3x=Q!*5W{6g~DVm>!$S%Q?!%Lnv9#93{CZ|!}eOgo2HGFJ>7~BHgAOq`+OBU;c_GX zw4*==ONEr33FbWoR};}ou#4Yb3-RLxv}Wm6TyA~V=fT-DfglNlBuWEg&EK#%b6vWd zOBrp2sK2~XvPRadSiu+~+>AG34)$F229qpSoPH`athWNeoD;`Md;@sX!3u@QYGaUe z_W~FlLpoWe7N=`vksdlbYUr9k51S03Ry3z)3MG;W2HIAS&)N+}hWT#R6l%wTavL|J zbiw7vhM|FP=#E#D=Dw%t0e3+i;mW{GMp0uM1$BY0Ca4J7aYwvyPbePogjqZ+mf^G= z8EOF!W^e>6J$KbmBRd$|b9sGEmm3`UrNHoOH~VY-ELFlWdaVY+*AiGb+9#5}3K%D; z3f(dK;}{x1<2{xDkKlutbwD!Jjm9d4xiaYt>T6|jvOx?wE<_}Q;-@>!q3S7{Y!qp> zSfZ9fjAUJ=n8B7~)0B}3R?D@VyG!&;x&B5vwPuX=Xn)c>>@cjULsYBbM#G#zH{6Ce z(?yCY+bFc*h(}Agg2Ay`O0`m9Z`j9Fuyl#_GcB=M^mo)ilu{kKlC^|AIp9IRwb`v9 zbkuFHg^7Nmsw7hx|2Jwr;rR(Cm_@s7V}NjuufX2B9SC2L3_9VrCc)JDz~viJ&S zB<7;bKF_M7?dX#&Hqxl(oG6XEXmM3&zCR@cip0x3)Q)8aR;*Dqri`vijL`?!QZHjP zhcbawZxfFi?4Wljr!X?&2En1NiDt_XK#*9coa~xX7B3A(Swo~)ttA(ZaVm+ELJx?^jCvXdp?jfIY;d-Qw(lqh9zUEI_g-X zG+T6*s$|parW|XwBI~MdbZJAo7)^t5KAZ&Skv`15QK0Z*E>*|_H!pg3y4DOtR`XG| zhT|P7;me4Z3En+Y9EkLVF{fu*F+MxQP-; zx5H^={pEfw8wK|_>bRe#+r3`g)=s26mDOyZOO66wyy5quu|Q|YHbgSWPTY|pXmLk2 z?5rKLb;?B-IdVJvwRSXWF7{$ZW0(TXKbTDyZQ!hSt*gh&`hIL=w~$Uhnx^w0I6#QZ z<@?Qivktnuwia_`&`pdGX9qakcgG`5XDM0?H^psJ&N`GPlZjho*r&t;UI;Cg zHLFeh)2d0UHBBPa7QU9xhHP><6k>t2E%uS-^| z)p%5wte8G{h)(KT{+6@vH)5_36KKa=F7$9INYMViVKlG}!HvTxMyHP3B`cf7E1oQB zaK!Om$K1AbyGSWDO85~AQXa%YjU3_vw?n=0U?<#&*4$p|uqzZY7E9HvCsZQ5BTSzQ z$KCFRwd1R87W19D+hcM%BsoLoiq1Jh_HV~{XUL~OYx`)f`?l<(MIf;l{u-9O4|h0w zTlPMlQ}1Qj3~)#G7qaW=dBGlbJ?$6Swx5sGl=s z{iSftRt=>6CBLULsPr}m9!J8E#X}+bxNYfhrCM%Lb+qCRMu7jw!#Qe{_4csFuvJb{ zT~iHV!K#PCt%*7@GDe!4V5aXcgH86D&986hV~L2<<}mpwmQCbIo6C#$hdH-fT2v7$ zC_1pE8=kfucwical9Z=oaoOWmkCj9bT%y}?uIQXyDz*{cE>DK`(F{r!_{XKz9H?~ z#63~eo3{iLMl|JMi)LH@kj*f%Vy5I_t1m~O2y(dG&17t7sa*H8os}RqavaXtSby3d zr8XOV(k;#ZKUZ{Mh}`}Ev(;BFto;4TJuCEzd*!6%$Cf{^eB*L%`JAQSFMVO@uBFbB zW9h`jA1uCa@gKVA% z9z@ca_}<*F62UFtMK_TJ_w)9KUnzo{!KPC!LD!E`O;?EECWF~p^0|uBCwkoepdkC@ zBDld|wy@4&i`iS4(YbJB%9<>CLo_@TwRWRj;62>U8-#2#n}UOw!?`!_fe3B`ar@{_ zj}>2#6S%XpaKj+sM~}k2FM@9x%z6qTJV$AdJrUe$Fxk`Da4x#H?943Omh<#l{k`F| z2yQW$Hp|Xd>?rM#62Z*|a7(6FY326j-W9=(29u>7DWk)^;ho)eYBuPLo_eOY3;blA zwne=429t^Pk5>{ExYfWF%b7fiY1BnOlrE_xg6j=NBc4hUM=_3~ z79K667d{Iw{F(Ci%BuVV`HA3>(rv)sz*|>_i;2ZEb-$JAWpe1Bpm)eVAbXkY5}jx9 zYS|BU&(Qu9xZn$IFi8r_D|zz%S=2P)H_Q9U^5H=8VLMQIBF9 z!MGzUQY*YnE|XHh^5y1oQz}>n{2??BDNqd%V^Az1!yw}IlrX2yJ1o*-tJ2-3yKTZa zTr4`X^|}Y6J%bLNZHhbi6Eu%$9+L`IY5qm?FH*ru&9^n*mI_vAzNPt=R4}ahrskVc z!CK9unn$IAHJa~gzAF{12CeoxQo&1_A83A{ouD0~Y!M0jBfYLST8t8dx~L(6XE|Dh zR4{n7qlE?V+_s_6y-N2gsbE;Qt=pCgmg`=rd!iD9=B%y?UkEO6Z-?#vhIMfx0IS``KhL+wP?so_x{erQM=-q=mQPNd?XYYk+@| zNd?XYtKnC`lM0**R>8NylOAa0f@R9EGCUb3=gF)LDMJ&+ z^InW;z+=Zr?>+Z>6&KuSm{6%0cPXfomqvbo3rqpN|u zBNa{dd^7I|RcepgGvPcBtOQPz+)}{`wM*@i3Wn8AwR2f$_j9{##T8cwjOTU~;LWUd zqUO0^@S;{bQS)$Kud<@~k>*DeG3J4nH9yq+P%2ob`JU!`PtH{5b%o$5uU7Wt;5cVC z!oBpJh%s*z}29WtwkjzA+Jb9%s$hHD8y?cv16x&G#23oafE) zm+Q-Qf%Dv<42ExkZ<&ZOkFjF8wcMJ3=c!q-$S$%{!SG^#u`d-YU+gXRz}#tL&y~<# zKTbH$E1+_@z1*IFlPDQ>0}qLGF%t?$5*hKntPD6}Tc6a)Jg{a>vnDW}+d0c275fCVEsR(jf3_7?(3lk z>;Ij8<6!;I_n!yre_jm->;I{4^I-kIdrUo8|L@GV4%YuWx;R+>PftA$*8fvo=)wAb z$4vIW{^#ERKVzZ3uyWDz2bWJ;>MVX~@f_VXz~B7yG*_v=t+uJQmH)1cDBc185su66 zmHkeZg5C?f{?+%|WIFb5*RF1NR~8`RY;<8kqlOfUOVrvme{{1RE!N7-bld6CGkIGq zxY5rwYBs%PkYU^z7g9j0j#jDIH{`tDmX zdc79-NdLy|7LLC5`xj!sHQI(zZ?GHmdSk(0Gi>Vh8)o~oyA5uQd+ll49Ztxzc7)Wd zQQ`8Y+sWAcaiiOii(-vYo%Ce`Y&_R(MnF_CmKhD3G;qvg-4T++32MHK8cw?HFLLx% z_a05gX)hr*o6&ylsoQN%#xr(gRIe2_^rWqe1S)jT-uE_B8*w7+&DUCCq!M?;V(}Qo zq&HJZ6Rpn)isT-=&X-Wl$@YrR^Ykl6lW<0htJ^J6Rjbx|T~{MiOB)>(Z#bW>+e{X} z#T7#;gMPx=D8$`(6tBje#!x6<5hN5I^34}g#ff)oi=#)Zl7&2Xo8*MNU`LJ0H7XxM zsUQ}>OKE?mYcnR2rcxbqr0aOF-m;;ornRH*0q=Xk#-yrepEAvtSIJ5G^4mCi^L>)# zUE6MQ@*duiSFzS9MVuRWab)V3D)CL4N+YS#$jM@4$`ec*LzN!e93`26IbcNvrPkZE zR)Oj@33|Sq3Qoe8uJH7WlI1*myTQq6-;onui*|+^^>Elo+Cu(NG4D1GoZ$Xgsg}hM zSH#BP9jiNGOcX{T+$P9bx4CV_9b7H>aYaG4)vyz29bGyz7YuXW3zP6dv z#|?u3Vl8_%3+7EUGl<6=rhvI-urg*OSMMS{bi>&;VS0xkY^Ti!Wlv2S^$sBfOYegUHni&0L5ONdEvJ-^ua#6`_1sRY?T70>K z6OVm8Pv3YnAx)x0U=S*cs#>!K+~MH?n>mp7QKivh;3G93 zYLDVgQB}{CkspNUi#QqSZy!xY^Q?@7NJjOVuNN=nYEH7@OAT?eyQmMh)A~?=423*Z zW1~_pp#?{kG_oclB^Zv-9Ouf&4@2$GaWZy-N0V_zkEd>zI2oq~O4XVf_~Yvno$jDA zuwnLg($Y$J?e&l~RAP&cS_khGa+`RyaH z!Q5zduB80vQ+te)wEjuSlAgPbbCOPtmWnl|9&8zLLobSXLrgKJx5aHKy)(ji{l;b@ zRmovFFx2OVdbGm|lI}5D&Xt!Rh01qgr~R9{b+ zfqT}3KAH^q2N^3BuN2&#L@bbNhe6~-LEfp+a;}{G0EGXXle74DlI1*e`!Z1rkgo;& zZpVff_;9sj&SZt|n#pz|)JygC`a;5B?RNB$xTidx09KML z>1o>qPSRZikZH4B4ZDCt+mJr$9>fZaw;N3oj*QQi8F};1&$jLm!nZ`6REBxje-E0erLNFskZ^jht+`Z*nw$v1AEXw=dzexN89NJD7gE6CMK3hK5KG?+^jsFqC&Ef_;mJ z7}YAJ&4`7@gLZEtAk_C{0BS@@S8`aZjr`rRD_E*F#h1?cRZ!xsoREp5t}w(7YQO+= zv8bvw`*AY%q>%h7Xs(R>02F-*Cu8jCM^mIxJOG`#eGw<)ROhbVkGmv6M!s{OD#B%IOW>ULgK)vEnCp%EmUtb*nW$&Wz(PjW&AzbsWq?)^V1BNtZwW#!J5 z8&>=)hgMEl{`vC#;2wy6`I$@KT>8k;Jxf(wOsYE>TRl< zR5evtwWR!#@>bi-Jq7*+`~cj64e+z%-;#e6#Jjsj?vTr5 z56RvsBV{p}MRp?ed+5W^UC`A~335QsUikLHBOr>@a}5Pk`)9Nz@NLM?@1gAv3)+6C z0GZPE-wE3O8{yL~ZGT9R>|RNdiH8dH2L)}vLr5-Y`$2*0+a+w;$=<6;a`ShwxeZNo@cM2c(sEYf9s(6C{nW~C+3asxC zKJBWCzY)ZKz3_27!`{1FHQT41R&zVv>U)K%c%6`3sET(8Twg1Rt9XxE{dOVw?Z=n= zZR-lz*=NhtuMv_9lc3#qY}9WRl=^A`wTDvQA}IA$0%S_5Z+@;qW(U3Jw(v=`+Z1(ROMYl@>`ED`JF<6 zZxNF3YQS$0bbYh%aSvVJA?W%h0WziQ*9)v)E_~Xh%hw5FzfAaeY+cTbRIe3u`BEXd z(133jxZWs<>+X>X=<+o}3%o%{zIO|JwV;*j1=Jo|d6l4*mk5w4t!xV|@WsL>aSME< zptb9S@4K}23ZZ;k!pCE4ZDz!|P0-r4lf1_*@Q6^LYlNToDA27!fvy%HQw6$35cn$L z(=LH;76iUh_;_rAcMoQuLT(ZSzCuVYRP)OPu9r*UYTG>!fg$o`Lh{k^C4Z@qe0Y4x zZxpmW5R&hq?HdGbPj=9zw0*sx?Y@wDm$qLbNY;}i+3ukfX#2&2wizM0pzZ4fuCyes zyT=-!?JXfWb$rRM6_R(4FZnfswmU-dJ+ytbpzXK3cf1!cV)(&9mG2HrpeB#2~{RglI}4;yn&1!2ig} zIF?5wB+Y=++^OGq*>J2iVb!P=8hQ!}Uc zT>OFlc9M@jFogDFw12+t#`33jH905jqV8inTm36_9OKN7FY8(fgitaiT zB0UbTFMOesY*9$Gc*xf0z@eD=S^%R=B_Zd zOqj663@sPv2>9N+#kq$D&aJu5A6#E7@ZI(}{FKpV(u1M{%X>*}JHc7rOAF9`n(Lp> z^31cAw}&n5VIAWM>tD$__)t83Ftma&Xh6ZIn>jgniQ4-cuDP0;3^RuLM!{usoQ>UR zHy+*dICD6dICRJzK@H|?pkEmcs*J^A_0loA;tw2dZRQ;PbSM-XG|1y0W+J9+vWNOc z1K(yXWOVx6tz4>_Nz-X7S+<&jaoktwglIP1jakiDE@dlc^{hLb=sPEeL3 zZyus6MIZ1R*mCu8f8QZ3%nes`E(CyVJFX_sYvnH|p|yRSfFD;30A|oC)Hv%q-J<71 zA?-QOHO)5v)7SpeA+7%8z&Gt6V!(4|vm10U5mTi!$hq_&u(Gk(wnWS(s^9XbM!w+C zuxV-R6|6(JN+7n~xyQ|1g85To@xBvpn(YOKQ#ouQLzn0zX{4Kumb2KfPw8=U_7IZq#^cqND~6V>oApx0;5OD+z2#83OcWdS zXv`gKSX2JPVVAT3Ib6|!6>{r;92}#sd}HNZD>trOwqgV4|36v&`10+`on`;>X-iKm zeP!wGOV=!Am-I{O#YYz(Ts*Q^UA%Dd8M^<_eFpgR8|Y%X=jj%}i-C7(Z`57}JO!Sh z`HAM^n%gxUjbC$``U&6{@a^hr)LFG&tyVp%dQf$%s;oL+^>pPgl%G=Gsq8B+QCgHs ziXSOHqIi{pRCpApC}8;8@cZDK;1c{o_-XQ=$sd;AA%C&_V!2tqDEp!8!?JBzQ|6XE z2l^HC1?bJt6;Kj77lJ_{ME};WQ_E4!YN(h+GDsN@7jv{Tl=lb3ejL}YQ*pSt(Gu6M zQ*t;4b0FE4&zWyA4vL8PC~;8A_3IQou9zz_{#XqQjp$H?$~IG?Q?T{xU_M`}l}#|A zYy&|X?Or$KsLbHx9Bz~-hFY$?%Nq*>+9gK~6?>#xzfQ*C!q^Dh#`EW?T4?0z_kFDR zQtJA35Ql4ef~jDB(9Op}d4eIxn)vR}`gIE&4i9IBSu9)Xr>dodquox4eeJGqUCrTg zWV{#1)CS%T*7Xo>G%R+Ry1sQ4&#ztzyGl;B5yEQKcp&Z-hrC?hx{||nM<5Wpqei$Y zz69P0MbR1gwyqfCdLYJ>pR7?ewvMGDRk5$j^{vY}Tr%M;Vp+GdOS$46f80Oli}k!U z;&6_rr_@b{V%}1T>f)hTRP2aueQU_!Sf>-sd17@CmAdU1xjUZT48H+~Yv+COq0`Yo z+?0o?Il37!zmv9Dxk0l!y(`Ga?_A&NbLoq5yj&gROEH@9&@o@kF`G}1!_kh6x6@Cg zVxB=hoG!*Z(*Z%bqrUYmhQsALMG}i3kx{+RRB{Z}6bC(B-=aBO48&<}Bk=*@@pNn9 zVLCC>jw{YI8Vuuj_AglI4smg*Ow_VAdYCS{x3+T2+K}wbJPhnU4pl zq}Vaq)3->u0a~5jKjrl=U*BqS4CrpdIl}$^U^Izj*kP+XtB3}N>jqjGG{zuk+39Vk zM=2&Yqlh|(^X8-B1{TJh9jt~^xIaIb!PPh%I_Qqjw#OOBSILn-;G@J|4%fG;9IosR zB(N^xtTv-;m7oTRS%psw_G=g4`x6ggmBO)uIkCX>1DjC=%#>-gKARu#=v z*(M&$c0;Jxh3(U~N}OV+k8&o8EpiN^E|1?)FGjN&WS9wf5`n}_MmUG-)@yatS&p!@ zE0#=GLNT$^;q@(y!!_#l6dh@^U?Gq|OuHkU;=XG=0vYW40w0 zI9w!KW!qgO+b0Hu+ntE!lQZ&N%HhC5G+6e!;?0D|9Sb$lnAoxKnOm30QD`;Ta)h!` zsyVFD-c+X&jIknqC!D!;F$c`X9r+;9&O56x1p!ur-lT+Ti=TuQawo#rr| zYFD!E(QMz4=Wwm^z+Ea<+~szAFswO7nAjif`c{s^Wql5^RmRf&x|>0RysxW2U)UeSX)itM;Z$pgyOG&35as##=op#F;_X2`&qSN_ zAU9Kv7>6tM^R05Q-EubuO^R}7Q{rIh>swI{*U$UvSrCr2?ZVorY^&j#tq+95`J6Fd zF6?e(iw@dfX_R}jmK5P|<&tMelFeeOU$4;BetJ0DrGz=$TpzXTTOkgYNV!Live#MZ z^jr=kI*8Ba8{}|Qh6FPaXNB}eO6h*F9Gfj~fWuX?{=7 zc+yoWg{MlcQbZh-Vtwm84%d&9kzzaOCO}L>hnwwUv;B~r!7SB!Bw~QPv(DVg6^%2$YJG?zM>8P}4+JS+?xe{Fu(j7U7 z5teWH8VoL;60C3OIb7e_Zw5WtR00j9awHup&d%8nbGW$MpbgfW}=kQ9hTPi&Bd-+*TgU6R;g({uSdAAFmH@snENm3& zzNDi`drj4HHHYhy{Xnx)JyHf)9_F&VT5^^<7jxEX{&Fc+K_Y?7(48EzBYQL&PE^2K zV?Ak%B}SoAqKRb#VS;t!>PHBWm5IyhD#=+*AE2+r^X`&0UZ8W1kdb5>DZ^;PlFtXXJZt6?E zws%xuYW*Hf1&1h3R^zdeu@@oG(vc!34p;aqB#R@o=pbET6J*g^Y$jvI0-f-rOa3(5 z%NIhm;t(4~HauycFaCVO!Xn>i2rajc^tPu~2D8XH@T*Qfn#mpp!JGsxH(OXjbjq(5sanFPSR=g+JRdIG(9AYj_Q$1L6*Gf_atXIW%?ect-4V{*;u|}u%krI;x03z zU?i}K<4!DA2^YK3p2=Jbmwjmc$R!h1GLo~}W%@pMjb^+NFWD+}THOi~@DU7EV-i7s zzJ+1_mdWKZXG8fwJjAu5i$QMuU`9*M?bxQj`!Xdk{XxS)xQEq|$rPyJse;EB4ETBh zyS}tJD6}^{nb^SQb#G)zYuQ#hauFvEXZn<6aRk#Z`*MDK7%|c$n@HPT8^$7Qw{#P< zE9x3;I?|xal$`4xcC-&)G}R*+kR@+NP03kK+0lO7$Q((7tn6GJbEF$#*rF-y>40uKNd)+@M?oz!Cen5Ah?j^cQbynSq z_Q%?fYj0P6SKHMFw5KaC(ELvGRk)zJ7rsGxt7c1ckt(AxYnBzP`p4>zDJ$yNsM~+) z*1|#h0}cE?t^u$aC|m6{adb2&#j>MbA8B{Hm6WJ|f@e2OhAORuFP*F-;zJnd<2-I|Oit*%V_c>hpu=7{Ur%vb6&!8M_G^0=O_lfZ#Xx5^-mM7s!)5+4FWui|n2mX|wPOO%s1 zoj^hqQxWUsHXb+k;tzE57&q_Y4|Efcn|q-LdKr(Kd(i~CoX5?*00OZ*ZtjH*h#B*n zckcme@wmD73ZNQ~n|n+ST{h-7?~oVD@VL2$qY%pD<{oB32#=e4NC|~`+}uMz$j9U6 z9YobS9t7|6)u<{n-^=kmC@hZoQx9yjW_x0ok{mNboSk7O z=#EF4&Qi1(Ze~Qn4aYEV2_e{KsFkWkoP$=55+6Wc#1~;$u8k^9(v>Y0yRM3xN{Y`# zp%cfrd11C7?HD&N_!{(_F>YS?Dd;r57<0o=j%#dg49bOv$L!_>^;+enn;W8Q;lFrx zbK?q*l{YsQ+`>&`b}gzF8)X}T2w6%Z?O;eePM*q3$5wkoGT086Q>BKtGysQ$q80$@ z#_UqPcBx;XNk_073Z|ScaWlSj%r0DGg1LUBj|4|pD-r06k3<*VGG-U3M({E^%HhE_ z9xJg!@%6pKW0+%zx1-()9gVRKvJ!CwMA8-cT>C?px134_L%CqWjg`Dr@r8i}VNSp> z4T?z&{hnYp9clK%;sao4am=sNtMvn@+fyC(s?l(!BOXf^KFEvLPm_sKvB|~)-JBb5 zkumWl+HqIbkH?V#GW6l?upg;*(Z1L&7G6AN7cCR{KF)M{{&KsUsCvcc;*BvZh~~)X zAnFXW-q0x5K*YzZCyxu^NI3#|%GL5GiHMItdg6Op3)l1PdV|oAr2Kgz8_&7&EYT1* zl(8}Pm>`XyJ)8(+`^99)*AXB7E_{*Cw$}}KBH2-iK!|K9NP_1@qAG*NX3o=IBfea+@ZeayAQSRslSs(}ZjB>R-^`)&Gk6%&!0P=#$ls1eGCn#^IK_8M zXkI!d3_Omuk`;Ho?&#CScthObLC+ntbJMXf9g2oyM2zud;;gu4$AdiG&qcw-zr3G8 z(NQx}^u)x4g?M&!Ig9rl#b_#69r@Udrz5^qeAO6M9D3<&F%<~b!hr~ziHI*=oiK*Q zGHG9{)~VNO8MHHG5wVeJ__ET$Ru9cZhg7ec>SWz?Sj=tV8$6C;Q@ym4Dl0T+d{IN2Zu9hZ0%SokV09+l6qgwNxuHUb_cuAJ8|t}#eC#(^V|V$|J7YhgMXz=3CTk&YMsT?=2o5L)=f0(1*hhR%nc zE>p_hD!WRSR-Xi0;br+_zzg52HDA=cMRTPlrP8)Gu1OOzxGRDtkWn zVnE(n{)y`6%O96LF8k8*YnQ3z(DK^S|5^H{r3aR7T*8*lTY9SM5$=7#;!hVJT6{eS zDW8!J_eCxPh0`;$^Pf$m8bTag^OY#^rk~(Q10#SSJv3M!{54WaSVw#^quOEE4n4 z;Z{GFAla&TXa>H0xov`K#y@l<*-*JRYEj)}ZP*j@bB%F{k&~`brC7$#r0ZBEEAHqQ zzQg0@+uN^Jz9GNh|M4A~gW?}(;CMB_n_0c-OOz0XDHd~sdO6%+#q%@yc(Bp-JdwIrHw)xzC)#LZN)47i0W z?k;*fzxlZFu2XH^ReRYzV}A1j0LjL?Fmta;%Dm(9&U*?Vd(#-#LZeP%;LUaV!4?DN zYT^kg^dX734~}t_dX8n;dM1ceSZWky#4D?XuSmpwSt9OBJgymuI~#Z|=tljOk~8o0 z&P)!k8{-_lWHA@$MVwhW9rwASgs6TO-p|in8-t{~7r^Kk(#bM4aOF_EDmjH`*I=`Q zh7a{NFoI}#lW0)9qJy3x5jS3|HP~PUXWA9d$kol^scd;>wsggqA5|sF=~l7rp&Blx z)u)G|VhEdnjk&mpM1JGVLhwQ@mT~7xP8`pM!tGXG{K5n}W6W0z$ci*>%hF)uZO3|`+)q+0QKB;89!|HK5wUHT#+C05Iid_nIDB|6*r^%8N1!BpaTQl=B!`TtK~ctKat<#Y*MMCa2vbT80Zbcc0k z>(0=fqB~jl6rE1{OYP59X4Ugm&r_YQdXDM@l~x5S|6BQl^1qZnQ$D7A6ujpDrt%+v zSO3o_KdJn%@?Pbe!OQ;Jm9J3Vq`Y2vjdG-Xzmie5lvS{vf0y!NWm*|i29<8*`AVD8 zs61DBrt&o9vy@L$E-Teanc@$M#})sf_&3E5L1ch$D88!rg5pz(k1IZ;c%R~a#oHD4 zDDF_aT5&}2GR2D(S1ASxO3_r56_+XUiX_+@@GG2(7b>g@z2Y3jn&MQ&GZZH(78Odx z0{lDpS0K8;PvGwZ-va*xe+B*={4o4c_(AwR@H^qR!gs^3gSX*Z;2YsB_zJiWVi44U z_kl~{EF6c!uou1%K2N96{!#l|*bF}(eja=}{2ch{@CmRMhP97rzYDw{{Jr*b+K0gY z#ouc0*S<}AtCrOkv_9=&?aA5&;1A&|ng=y^Yi`tZHF=FobFStj^{>=lRliGpi@K=} zs-LG;s(z^Yr0Q{PN}6vTu2PS-$kW zrH?JWVd?Uv+|uUKNsGT+{QJcR7H?i`ECv?O0`E3{p!F0U+S)P8Hr=R5MCwTgCo_>_4AK~eTdHQ!e{UA>t3ewkZk|5C)BAaPA5Y)O(|dXP4xYZ9r*GrwTX_0rp1z5v_we*?p5DdN zJ9+vBp1z)^ujA=!d3rlfU(M53@pPM~ujJ`%JUznGTX}j5PjBMs%X#`Tp1zc)H}Ld& zp1y>qFXrjBJiUgeSM&5Lo?gk*%XvEF>42v!Py0M&cuMn>;%S$sZJv@mZSu6i(>hP9 zJT3E-;Ax4cMV?|jy^N;?o?gn+i+P&oX^y8^o~C)4-)5AR7;OQZrp2O3#d3qL4*LZpc zPfzFRX*@lJr_bT(vw8YVo}SFpGkJOvPoKuq6M1?9Pghpu9@&M30(kX|6bc(gOsW{h}5gxH+n@i7r%`p*5B z#|K3S%M6eAiV)NHf5$xDDMFZLc)V4FnAPc>BE+mtw?&9qoxc1uxofKCW&syHU4B@0 z;j|*Td)u-$|MRr=_@Ar%&(kX7f4+qOd0J2W&nf=r>7w&Ldry`dWEYyImB#%pZ;pQ# z(_gkO(; zpV7iE#=nb;EBo>IcX4rLk1FLWGP}&i{jtf6+@DR^x!fPCY@PdKk)6W*G0RTk{+MKo z+#jP%#{DrsPjG+q(7*GByUf!?o_>m_kMQ&%p6Yn2;i;OZDxNBMD&r}{(*=%d|C^_O z;OXyq`dgm{w&bh8{IMCNaqZs%jf5fP7Rg^dxn z?nanrx>WG|cvshMp4I8gM2K0PZix`HI=xDSnAPb>gqYP>Uxb*|2Ps0#>Z3Rpap`V^ zX;x!V5n@(j0TE(WV=fV5R%0&`A!aqUDMHM)LW2k~tJ5<@h}l;7ED>T>>!QhlX;$m9 z-Gk6fCK`NqCcFA#z9DOVBSOq5O!G4lVy3W~$9Vt$GV{XXjp{!tt^xn&;NO7;=4jx0 z|8|ad5Pk}@pK!|aU$IDxM_|?O<)?KYZ+$pFqx*5N%;Wd*c4zPS1^al{wU?h(E>bBc z2=DE9?8|n(M!5v@owQE(3*5AjOEnd@VA?J6CiwJc^S^2`8|_DR`R%XhP4T*`TsIH% z7!f{v$UjIJ%zD;03R*LP)+p7gg1dQDy4lemxBu0+*V{0aklrB8I_pej;KmwFvfmqq z>u$Z-+O2wfN!QR+aYP+B13b!AqQ;ol+7E{ay|aV_Jlz28_0?iU(tBt`b!&$Ylhr8e zmgIkxFFNOJ|MKlDXG=H9q5TB@o?r74ZOLN)UujGGc(AyaElt0-{Oj1%>AOS#u7JT4 z=>qpma+kYf&N=a{wT&ZcGzi{tGAI6Vx<59VOtz!Oy6~cy&)PBhuqGS!Z&q4<+Ud{4 zdzE6^*$f-dNY`J<9;#vWK{#^U=9h}4>V0#^Y9Va#yekxMpI z52J|Fa9CetI!&}2iUkHKs_8mx>=wz0@36acNN+aBl5whI0#ROon8)LX{BWG{_snQ~ z5VM=DMjvA=v6~~{V>($HwNo{;@H!XOx?{HZ#>@b#T z;cb`OWoY*E2`d`f%t;J=!4;hw2bt|OKd0?lX?nV8{>$2(!D8now0rvhcOjuo0t8?x#qC%JKU__vZHMCh zkSCardtH^LEn&iw5z6MU+Z)Gicc=kpjTUebVOWjW;@N1jlnV?4O%jV1iHJT#8SBkf zZnT-jkbJ3Cts1KaUo<>)rvnLdx)};=4x90$+Z_mDj6HbRl=FHL&azWluqUqQ!0x#7 z{|)%`1+_)>U#btNvdZ5oA6DJ~&gY-1_zLjHZv`jazl9%yuU)CFylG`)`HAIEEMK+k zTKbQr+m@0`&t3e<;#(H$i#pwxbg$7Rb%#9B?l{IW6M7f_QT3H$U=BVVXy8Bte{l_5ssK(=WUFDX zv)Ud6JEXJgrC86%7n?f1<2DT>cm>+rwPDXC(zP54;J*d}K`X*VkIx(K)Jx4=GZ*rS zZxIXZXJ>5BwEzM4!C~wp<=HP(04F`ti#6?5Mpyr{>STIznAb_|JOyyDBVD#92T@wr zaB}LDo^)27y5>GLGrf@2kyfrUq~|}b3|&>oGc>6Bib>p)A(@H~>6F@oy&KtdZ9wa0 z1$qX^Q)*%^Mke$;p$erYI+mj!gI>IW2A%U~Wj}p7z_k!)O=uxfrQc~G(3wIDk(zY2 zg+ONsl`NITxRRkWgi4NNT;W6}<11x~iCP~?hs6&N1XG``WcgDCg-T6)%=)rVpB!-t zm7gdmR4V%&h00G76e>08tU~3_5ELqv#aN;8r(B=_j`(D&l9OSrcB@vzng!m+q5Nqc z1#pfiJ^h}Z5#=Y_73c;hg)`G)LvhwSYW3qC$7n$9ZMzI`B2P2z76IQrX%G@qPB#en zwUY)RG4Zh)gwZ~IP{=h1_?Ah7kjQ?gLBK~Q4MJkl*#-gMHfazNS&SP5eDkF1?mOb? zKqr_d;^+Vm5#{XORWYqlMNd#@SYqO1DbzG`MMS|&1~rN7cND7VPX;xKNoN(RU?<~} zL>6O(DrhjYtge9JY+gt$?r^5S@5|=2Cr@f&UZOqvIqfqhwIDI^v1-A(>oy42thFb` z!T9WvP@vtjxhJo=nWl%L@qLPCO)6HR#5)zMefFecB_^G%SnWxZij~M>T(R0`f{I<= z$E1;IcWTAcUck*6rO#ET&Fd7lwSrn_=ieFc?R?!&?C_6pf=x;iz{Id4BGTEv>Tc&))e$a)-1I!Q&r#odk#F3tq zK4nBI)T<0jg?f}B0a^sdyks#>ge1DpV;`ONAn{bD@u`C!OY8=&#k2PIE5wSL#WpIT!j%^`z5`#)j2= ztQcxg{RHU_^u@DkP zQ+cmc=o^&p5THFeN`;wBJ{iVIG(VV$j;H>H=4k>H&kahRUOGU8Lw>d!^oXD1YrdeJ zj8$`?pVv;ts=3h5X(waVTWTWVaG$Qp(yqa_eBeUKvN>EAQIu}}mic+CCgiD2D z5GECR8FZOcXaOongDiwMObcs~x#n8n9bne0_sEkR429!~$P`?rppmUA;Cz>_w z;=K#ug^w-(pMR)qO^a#G&=X4!F1=j)ciLMt-_qQ#zDM0t`_<1@{X+E#)wZgtdVy+L z`90ffusq|7TflnaWl zDc-6WYSjxrU5qV!bn!~yBe1!A!SabqKT<>#r@_C3KLx)QZo(J9C(3^$e^7ptydXEq z6|!&0{$@PTpR3?xTDo+}ummrD9f&OaJ6C~1+}eL$M-GE9hM$rOmT4Z=JUju<)1gBB zL-h}(f?@R!)IX34maD(7{=QVOO#MCe_YNsgjijBBN7au`uz`G%aJmNlP9)JOc718F zml)Z7vil}NIJ}4xqkO$|zL#-TfR9bl3IK)FBL2Y8zVlcV42dZ^iJwzZUH??&xG;3%@MGRAzPFR2KyK?om4Q` z$&hKKg27&fOd}Nxb~9vZsbH|5AyY{OgB=Z-QYskiX~+~(!7>>vgC`X@uTo@knS8={ z-hPP=Y?!#Dg0-*{c1i_nUKfvt3XeFfmE;(J|8}R0-m=TSE`;+Js}mW zQ2j>r8>wJe^|wQvGVuH0Bjh^-I++CyeJUCRJd=;U1}ArSgr+ zH%bL7ly@udmI{WIcPa0Z3YG)MKzB+7%am_WzG0%ddD>9kp}b?lc%D%y;1|I!k_v|5 z7s4-;3YNn!fL|aLEQ8O3&zqEQp3JZvwoe$(8?{xk_sZTY6|9uKNA{kH6!T_MnujzG zO&HIc4=AAxXhSMk0i6q-I|0utpF;L-*}J8JVcEN6@0x(;&F7ZYKUe=;DtJl#Gxg7; zf)~|4RsU2fSf~Cs^}k64Yt{d%{#U7Bjru3*pGXC(K@hGVO9iXckEtJlYFYRVU>HdPAc*H-RWUR1tOb_VpH3%^;ZgLr{T zaGZOyETz;b{#o%hMNi>UJO%zPd>?!{9E6`Ce{AJT^7qNN&{xEJ-OmL4CcsAZ^lB0Ne2 zL;XyB9hLTF9 zl1f9BWSc@IsY)tIC6(qX+s|NVu7QO383SQ3<1+;4fcr~KXYkhvbP^!YgaFBBI!TA- zBVlM7h?}Gd2JYl)p=3`lH?-@4k0`YpvhjYwfMAy|>mH5=#hC#AH%p z8sq+AT+bDiY9VWE)J@x?Fr)TG^YHu^ctnRf%qv zv~qpHK_?$5By3#DsuP<+Y^Y+bm8kl8okGH*Toghr zK4I%MwA6(_Js7XOXqL)eL^zyWU7>h1lJQxub?~n*+aYoKDtEC75jyi>d60Vzyj@*s zoS0X6a`M&t`j#I_E;v5;tv+(_z3xig;mi!SHl%UYt-fnSf(5D6DHtl6!0)bGd6 zov*!+Nb?k3R2KEpaMf!jRdt%6cA zQv!o0CVV?bdrP-kr|T$Y)&>L6Ti(i>b@KB4XD%gD6sMAawCyA<^-)apA+ou0*u0^_ zT;RCFp6!e(X3s(HB!xsXO2@R9JIcsG^APoGdOS z9I43*_DCV7oE;HK7m+7SJEe4P;3~_pnn#Nv8kH#?KmCkL31-Pa@N^AQ*7-ryaDBR> zt;V=8G2GQ`rb4OBtQ%_?bE4g^EF@}lW4?@qsBBG#ixyv+hl0o%W-pW2N$f1<<}6@C z-Eg7k_}^Vh;PO)ACQ8`xVuFvO-i)3Z?S4IDs?~XBjHhb{mITF?TZcbh-Zyt2<`vC~ zp%pY`T`qMq+p?4i*(As*)r`hmQxD0=sl@}fgO=&SM0pm}E1BqbgGyN`dbG3=K*1?ER8nteHh)kDe99luCSFWzplHvzJ z3*YeBCdH zY1n2=e>z9_Qe)A<`BAuJp@o}v4c($B<<2)05*fu2OllaJJ_z_|R%TJQ3faTa90lpk zMiU}UP_+RybrCuHN0$;#IE+xGU-wt#5NXXCWZd?_958qMz3Qa| z+?kr1npflX0I}N=+a?yysx@vi3!CX_}cKC^gUN z8pF9bJLU_C$qG|jDW%Y>)vz^{r!-Oyx~bey`z|!^HwI}-N?HqvozCvwyO7A?b+=px z+1jdRBWkj|)rkfY#II^#ws4n7-ziU=W&)4dY47mOg^ii)3=Mq}deLH<aJF~y{gONGR=!&O=c1yW`iH7=@5lQd+RYPTVAST=^-WjmLm%Bg zO*^@g*ZYf}o|n~FrOO0GmDVME77dZ(Pc0-SUS~{3En$^Lt>J3wqH{;XdZ65Q&vXgd zwBe9Mmt@Bjse{XwPv%V;71D06Sq>PoZubX5Ns*w$76KkuE4&8=C_nGUb86@N3L9hA z(omS-BXPkbW4`IlMxg5WVl)cpNO`%^)}xB(hbu$&PpCpdH9&Q0cG$$wwMlY$?D|Z5 z#L@;|>rF~4e!3d2iLg`wkAwZwONj=L)!Q}9@coh$B$K*kIgPP03_^o)mOU0$rem)n zYxwN&?-dfFq*hpkU&uz*<|E2g@Sq(l;}u5Yf}nNRxK&%!=m{v!bnqR8gb>L+4ZQq$*H4HDI^9EmBVe73Mi7ACBv0vCqa#Is5LR? zWUX6af?5zXh6%0juL}uY&`2@YI5^56gC2W5UgAx4S%YS5S}H@xm>P$QuKId*Vigko z7S<1;W|hsfu7Kf-nm0`OL8Xqftvr?8vB>A<;#$wy+!!GlCNqeb$(l78T711h=xV>iq#l(-mH?4Tn^;_t_T{bw*_o zNSCPfd)%1ia<{_{8vW9;G7r`mgRBYQ6?8eH5L_)^P9;=Tvud%vAj^(#IiFlrh z`r&JeW{{>D_e7knuqA4dcW9>Nn4#*e^8p{rv)ZZxC0tF^38QuRU4;a$aw?O$M#)mE z)?|VjCEc<*sczLWA6})_bO3o7-Ic2P5%Z!1Ark?JKuPzZI*G5$1+r{5sujT5c}a_-Nt#yT#Ky!)DOX z)<+b0x+0ApnuQ8)cbnyA?ynUd6}b9JOv@=vc>1nexT9UF=$#ZDAu8SrbqU23QfXFb zNhOp~YuGagktit)SZX1c>oK5r)EHDygQmyXOsg=o$rAl8HLSU**h`mAQ(L-2$m-9I ze!FNmY}OaU6*;%5q+QN>GC~iD@lb5%BX5aoPQ5wjkVfi7Xm&?=NGC*M{`)N*=k(^kri2kJ!Xuc&S>*A0qZvXK^h zc>|;)m?aH~C{st3LZYI~!0aDQfSr|LigUU|37Jx3))?>7AwBJs^;$ZQ){AKGa_w5` z+W}^Hr9^gnRJCPiE-{NoL8GrKxM=XvSW~%bH;{YyL0;G>!6Pb!z2!tUNU%6qio$TE zN|Q;GG6iii$S8?}h62xwrQI*RkVx0@f`He9a5=$Ky4DO0dYVj^bF5i!^k~!S(Q4!~ z18q*?2VZ%=FhZoDKt0)JysA9KD{)Z5hL9~*{gIT!F3xB~cZR1#+PC%lUsgzzrxUr} z@t5;VaMxyN$8Ojwr-)l`&Qh4?TH>T858FM?aqi0(5;-!!!+JN%!)du)HQ-fDEQT0x zfdowk3y+-!9BD-)L|U!0c44F003)Qt20}&9;U+PJ+Q!%`F;xpjGP#Pc>u?{X+g8}8 z&h8cxGr2M3EUWxh_KFe(!!-NYq9i8@S>SP8M<$ zQBkXlFl>{)$kf#NvJs3jJ9K>y=Hm-&gFdN@r-67921&ue-`M@Bu3!0 zFA{EQ(KT2KI4y~+xbCsNgD)>w&7Hzh9vK+cC_ucb&CKxD>E#t{s6rH|NhGcDCSzwxYKu!Yu#W3v?ySZh+(Mco>Vg*pWm zTjSDrPKt;d5jATHUY^QgO1ZP!Ft}4N*h1?=yE8So8QY_VD@+f+eWk5>C0r(} z%yezo)NL3X6%2w#618S9H7~jAT5SON^KcgMvY%mKH%K6?L4~e_@KB@L_`_)(65whz zsRcYrHAXYB-C$Y_A*D%DEiog$(+?vgSkIH1^y)B>6-J~7Fr)JSly?Q z78?7Kf%;Bl2I!Dz`HWSGCcfZLn+u+zaH%YooZB$iFBoj0^`Y;GY+Wt~7;K1IhnTN= z!eCH$K{f><6KzHtcW_7M>`BgNw_&hXFxW!tLxgchTGDe+5oQ~eQWx#@Y%A`SXilD@ z9$F<}ygBCOm03k@!(g{yu!Yu#INGvG2A^^2tOE08KW=zNe!}@ zDaqf4!A`+o3#|`H%#LEBM|cY(SS)8OMS^`LI~M4Uy3n-FqSkIhqpnH~y0?kM_r9}W zu!Yu#xr3P4$^xU7vM16QCuxdID7b7i^ReH=hg{wwu+qqDOk20;gZDmHFxW!t!zrnB zCaYdG@-jh&+oh_W!S$sOwfk+bNh}-O0=zm%(^9{3n>cvyI|>G!eskW}?WrsKC|-`$ z7Dty-#j#uEdOnP?VVfQE7P2t(6nYpkQ?MHmw>pC=R#Ezzvz~k8h{^`a((SQZ&0lnJpnrkYhS96D5cC{5TPc~}-H=+<*&XxhaXYsI1(E!P^Q3TWEclEZa`kmaE8&U{rbDs?|I7HpkBLRA@Nk0XFGR ztN~i>cGcN!7_18hTWEb4s$D*$HELqWo#C=o6~vk`*ARI=q32)&S#ldeIe`a5+PRe% zR+nD5z1D|;(L+lUeO*h9*`OJPGAkKtG}Al@r{!vQmSpZ)j46m}@VDuMykM|})`$II zRhlzQ)oXP-U0ubhIa;Yz3>)O+ZueU8BxqnlRT!_klh$n*WCepQv_5Q#0yqK;eS*Y20Znsk&57W{pm)C?aXh z9C95rR$=Q_aS#^_+K9)4om8{$`ij%;K!Ve7qF&BOV}mjzx@$lTI#YeNsUjGIs` zh?2?BSm&m;>XWtH1*=oIx*D+}I6MkTwd=13E^>2Gf_J_*Di~~`^&y!I48=x0@CLG) zK$u=;e7W4Eylxk_s?Ka~@MFaCs#(pHZo?of7;K^S=@$EX=X;k011J)a)C`vSEN^MB zFV+_V0nfXQE;vg;>|hd%mF7f%K+0JE>6S=(=X-;K!4{Rdo^F+xcfQvz7;I6`=;@Zn zIM4sj?)>EL-fIs(^?vZ+=kCAh#Jqp+-oLr`gD1Q9vP0q?e(zK6{_mi|{5$S;?|$a} zUpn~U*{|=v>g+?iKe78|dtZ0#?EK`xukQZ-@xOR*bn-`gqqDC%8=Spre{kn_?tJ*p z*Ps0K@%!&6cV2h$EvNta^nc!c=A?H2dmprR{_F00PM<#=pOy}uJ75kz>%n*I|Hfgy z_d9zZ-v6=vuRew!%#V+c{?*a<9W9UEbae0V-yHtQ*Z$XRGhA$yq)EOyN}6g+7V&!P$(~*snSU5HEd9o=lVo(AXG}n`Fgml zxn!;5DW{{tMx95ZMH`;Vvf|X~28uPx)AmGc&?)2kRh6HFZ9xP4z(L;+7VT)^em!54 znm~_B0|gw6q0<3cv&~_pvVe@XkJP7Lop*wfJiI>DmxLQxF)3ZddoEF zgj~&9I=VX>t^7O1$$3h&c!uZFvO{yrA=a`P%r=B%*nn&e?}%ci;8LmFZHY_$@WVwr zZolQcUtBHOVvphYj|v;!5SlM-K^8TkI)oH&5D}alP;>(Kb!ku^2WWGo;l$fCp^)P*DI~1Yv}3KHH9G9-El1O3 zHLzw5IPd5WxaCw~dt?x|tFBbX$8Fw*><)p(ObJ&=Z5m^f zI3;YaEA4);U^T6{Hi-$HKGeg@RKJ#bmY9(xn2$TOTcKw2i7Cvvg|aH2y{TyV0xv!=Jn$mEHcx;Ntl(Nw-gdvoJC#U$k^g!>EOafS}vQK0)cBQZ0dN? zfSXTENT;xM#HRk3Mk;|}O?=oyhst4b^pP@n9BCbIO&jy}8p@JZ$LyxGKc0_g)VhO> ziFxMM5-Tk$XO~lZqh+L0v)Y2@U5-)1#XRkR%>+?3s_|$MGERL`R`Y>1?l_0x1*>EP z&B4;KHtAutCCiXQl0vJNXpIM+E4k2QiF)< z6N>585lW2Ai88cGzEG^!I-r?<9Jl()fzV$Ps>QGaBHqlu2)@%dOam+ER0SkKFtap3k*3RW) z(5S7266s~*MvNnd!8*pYEA*SLVAgoGo=US^05@71idNqLn!?8UVNJmD`VwuzX*SG~ zB#V5l+Favhk6OWCL1Ow;wNE*-gD);5WLBxlT+qRlK1Eybj2lZ+bOm!dW@#fN;O2bFO}>+HU-kQitSs9mAs$^hIiVAUQt+z@hwgB{hW_;QcaBF0{g@VKAtU#@-l5#r!w zzBZ?44oe~{RB)jHteNm?soieJ^I#>yLm6tz|o+-eD(@+7g#4XfmJnY9SX zJX=cezA!bBxNcWreRscT1}Y{?P$R5ltL-{98Uo$(0$vO{s(@P8A=cxC|}8DUZ@@ zu7{ArR>0|iMeY4=A>pS|W7=DWMynTRo~KXRApavGRjP8m9X2(|iA$qSP8*58`_~GI z1q4dlG7YU)sV-q?G-pGfCt%NB&ueU*uYl+UEAG0E5Xf$^98285&}Nq44(GGFn+7lv z5KD5UqA=BKNZ>wa#xGin_QFH`gFh^6xL%j;P7%=e&3rb_>wdSNSS7-Z6fBrc!c^({ zL0TF#)7C-JyNTnj!KIzN%~4)$s7L*V2t%o_HNm?xN(%#XA;NR6SubnZe(}6XETxPF z2p-V$q2rdY(6zyWSB}TH<&I%^4(6lO)4QaVA$#vHSWV@u(!}x_R(C^L)&);$sam7b zQv3$jn2FVq3+WXmOyz9vj|+(jtXJ5$9x&P1SPQevw%rxjZD!FXN%X-Trf%2ZLZ9@9 zt=(@ZBxGVpOj}t6;+yN0G33;GEy22zpa$;DRZ(A|I3%U%Rz2x8Zuc`?ruOk&)U0(FfWu3A*L#Dhy zsmi)$Q02%n#v*_4wu@j-xO&U$>B|Nrqzec^J$&urepjR@vD96dED@sTm*&a5}< zsZ~Pm(rjbAhzKE{XI&%rC~*M_)L!1&NtW z!Qh626*Wz>fM+JrDy(yJ)O0Dq>oAGx&0U?S+AMzZ6&DiO?eBb?X@w2Yo4z$!!M&=# z8n1R2kHX5}*m*ru%*j|3AI?bLL zMcT~#GpY^iMYG4{YGX5x&v{IIyBs%WD$5hG=r_$FIUn%0D^3y~8ug$$PNID`$)9ltO?a65w=4~N z-gHnJ_T8QLz4^Q(^WS*I?$bT}*cgQ8wY6Vz%vJv^#@rUgr_bYYn^T#KAr^#YIyO^t z5l3KVdGq!P9*>EO63#`rpFgD`x>*+O+>GuoV!$4df7@#cBzhsf1vqo{uRILdpsaS4NEm(x@1$_KY;qVS!FZ zYF{(gjBlEHW6k%}XRx4Q)n(W++lz@-tLi;$*0Pe`$SAW@3~U}?{A=EE-qp$Cyj(me zc7N_oFx8JQqOwv4E`qAy#zE2a&;Q+EyX8KXbK{s;_C*sJlC2@w_-Uz>7*q_V)YUZqrRS0@j#6Lx zdGAfk@O-SCPc4_LqMLniE)bauuv|J1jh7pen>A9;rCxq6xvT{bFi&eXX#riI0J90y zg3lK!FFBjEgho88n(j)gz-_CYx&A2XHe*ljB!eI~z14cq3N3IyRD(Y5*~F@%UfyPy zP9A5hGC?TJs8nmOHwGCes@|;K=DSNK4#-hfg%bpos}9OP6Q!a*i6W6TYgNy7sMvYE z(V}dRVluMaVsAN{TsFOM?f4?Z{$yqcYl2m9@wm9GJovZ(-bHxDT5P^J>2F5PpFq#U z?=o=CC%2CyCV|)$CXb)6w|ew!7w6kOGcVpxUhtKCWfvS=5ZFTL)4hiK+z0lzCh(M9uy;XVi@xix_;ZfV}42VQvYg1{EnC!X%r&*xrv$JPX%vJ2k6HG!w> zg0~ehu*DtdE$*(|CI;496L`ulSZz(Nx+M2*qc0qVSU<;*BdlW3UCh(M95L^)0BCwwBX9&-|;9n5f;*jR) z?wvjN!eVOzTWpWtym9p03*OcQz*pElD@PSV9roBZ;!J&0TGnO0J#KVRU+>M^^-@+M zZIGqpHU!+Q2~ZsjDG}X?18b-_il z{`Jw{I$9qwN1uE6dxt-9_`bvGVe9bD!M{EDk%O<;`{P%*Ejp*Z(fJqJdxe|mg~?@js_4X-+UeV zN_>CQb?7JK8|Z%DHR$@2-T|ZGJzJsw#&zf`zWm0m(C^&}{rpzwZ@3P9C93|~R_L$4 z4t*sC|N2(wue%O?C1SsJEA+d!LO;3=ePuL!&2{J}M+1n#|86VvS8s*>s;$uP+6w)Z z*P*YB;;*<4ePtBC^E&jEr_^)TprI$nJeVQgaUJ@~D1Q5O=qKYF`0{Pnp|3>j`a1NJ zQ3Z6bwnFFEp`VOF&?nh-=qG&%n50{wlk3n|JRe_&e$w-RNwgI@yarvn5+}>+&{yIl zxDI_~j`goYKN(fPJB#bkSE9_kb{Vxaa)~)N_D8K5HzUs?Q-ul)0 zt6zy9zRa6jd9zP@7?La1JXNYhSp(T3De~gakw(I61M{Ii2C~@Bk*p`OflA7WLAstc zlvflPyC&~vJq*v3s_Rgs;ZAG>rZBtT-c~y*ys%EIw{RK0kxDW>*}@M zxvf=icwr0ZuFq8ga3Em0Z^x-Bv5q>b^Zg8m+e3MU}jNEeh6Uis+`YHJYi&Fu&1V(mX zd4w!YiS}68zwqCaK56%OA{eDZ9t$knDNE8~CHFxhPLS9i=ajyAMXY0#Wr|$(J6xO;r(v*3BC=w%9AS98wwNJ?R1E0M0CZ9Yx zuYQ7!6Y(keq#c_oNQEnRXkswTE4+k2LK}JspI|d8ui^T%?L=!Yp*f^1f&Ag61;RDk ziM*~ul_|HbS1q^d@jl;eqZA@zJaTKFynYY($TDQnj@=(wrSq>=eFOEQB| zug!zca&P%OxoZQTy!j@dY_9h{p?vbR#t9j%RIkGHWOc|pM9mzr9D{?@=3qVWcubxS@+EbLtXwsVX%bm{zRI)_n^_j)Ye_( zV4#wCsJKSPg2EB>LRQ6j`6YaUfJ|LXWlG!Yb#rBhc4ms9FqIL`CS4Ws8gd`ht;yh1)PoCC0BNjN5PQBX0D}jo;swT^Iu!en!ancrsJev17euYP9Pjf+f z7P>XpmkM12iGUbs>P;EW**z716C`>ume+W(>b z&))mdJ$&~UcQK&g|I6Q3o<0A@qdoq0Z#7ST^wqjguKLJKS-7Z*EY}My-#Ada{Gg=x zjN|3@DmHhy9*z~!950k$8RI6I;#E*@^)W<@)-GtWFx!gm0fX=g*bbjPd zi;gchTzXVtO8nTaK{_5JYkX$XD!t|^G{%W`RTgzmr}`rAKmS(&n>XDvPyY6;*;pXy zHK++SPVLB^*sdL~9=2ex5JgHF0!oZ@nh`q@`srW@Qr~pR)C#d>vzFD6nd{DJY4-eI z0Ty2mEdKYmw)iI>?L-gr)~yTd+0VZLSgHX_KYVLTfuEV-*ahF*y3s=tB={SIphgFi zb?12y!I2e`m=een0RegLtD)L21@p*E77%bNeCgI7y0x+Eqil1v{BUd!l*vq->gl|? zsz>>}L?r`$k!Q^+HBx#}*r9xGsBm_>r$661X9Jr4gSTd*gG>go`B13WjK#2-do>g# zYhk6ExIIpeFlBvPiEFuX$4$ap%Xmsu zpB9}h3UJuZe+gi7J{rF3)@){p8O38w2cM=#+mz4npDzJh=d;Rp-rCkn%qm+HwwOKt z#lYhEtn&U_Tl|<=Ws6S-?dPAl?8oo8wWTL!l`X!J10nQ9z}WdZ?Avc`?D|<{^F4W+ z&+zo;zwn$5=;Uv^HJi&>Ws7g^K%>9lveDmq^Y=ElnpK`W006Vf79Yt0Iwj!r_if5w_|JbnFnT_ozxmciUt&COQSM^){6k>zd^~^Ctu21cc;4dMBlr2w z1D0T5>3z4hq@QO+u|aQ+FS>S{8W#5RuL0)Hr{QnBwYevzk1akZ0ipY9VC;PQc<-%^ zT|a$nZs~0E&5Hi~tIpYgraymcHkZ@K79Xa7M!)j1(eJtWdj74ZkIg-kZL&3;-~T7~ z9`2mzCza#>=lFj){`O=0xOsed^ovK|bL1b;N6+rP`{-<+JN)ItAJ~7_!CyX14%^`U z|5qOT+=K7f`v(u^4_Xh7?*G#L@4X+~f7AYdJg*$E|G5W0a_1<55Vs|-23gbSKj$Q@BG6%UwdbG=krhhuhSnt{TrumJ$U=+GkfyMe?9rhlW*Gn z={f6DQ?0(VCAME_I+iWcy|BKt8{sQyy^uR?P%9YtFHKfoUjXW9T=k%=6WD%6B z2vRbZo|}()-Fiea2ucU#duSoCpn8-i#QxOLm-wtAGP1##rl?r5HD@z3m^9`rNStia zy?YNYB=W2b2~Ddh4CNuXZz0tQAw!2CQFeFe2m~mG70d^drJ_>Z^1aU}Y=AY`stGA* zeeKkxQqpRdK%w1|HOfG(<5oVmJ1*YAh-_?0cZ+h&E7MdIf7&GlC0V4UKv6>2h?o}H zu2AQ){3iZ z-WoR+`D!KkoF$aK1_W2Ij!yR+Zn9|lJSW{P%44rYjLU|81Lh2aEAb;xmAyMJtpjyJ z;e8cqOrnNcA{I8mJ9mp@%&X3@g~xIRbK?-Vf~DOoDSnQjnml*M5tN(bZp>=2BqYw= zw-&5wN|zR}oGvYq8A|7hTalKfQkm#0jmnf)B$&4{kY%-BNA7-J(eh~Fw7R1~P|Mx1g`OP!BNYy6jn#M>6;=K-G>9uhr#`r4=QmrN^xiyNJVQz90 zb4c*85m>>(>kwTyl>^UWR$0H-Mw)|G9ODQ7t&mu)dkr#^MjW`nLNhkx zg0A;S#v4ydN@odEK0F@JgK8h=cb+dKvPm0P`%*<+^y!uY33Vc7n8DQ7yR{&((ReE6 zqjU|*50`g~48x!{CsOtx$8JYcWr9Hj)gxc8OvnMGPxZKli!C}>Su!b6&}orDJrCmw ztuA;Z0m*T5U9eejQwyn;Z9iY(gxWXdurzZ(ndL+|{&xkda2TjcGOXIjnu;gLnpq-i zbkR(t$-0>>Fc>7#mTi~;wGvNjml9RERbEXx2r?^s&Ll9#-PxilG<{7J_#q99VM)*& zSzZ0k*A)`WkkpK^x>8i49ivX5N3B6@*NtG%@Di~xE)$kv<8jSx-TCZG2{iBYQI*kL z3^v84Jt>!GE?uv6CUY8j9pIasE5Lm}Y@O17%CR8ZS|?SyJo$<>M^(~GPM3iL#LFrVXt6}U9$_I8Rq`>O@0(a}+m zPT+$xVhW+8@_=lr=%A#vMqLk-OP;UEZe`FP@P|fW!%L!pT&G8_U9FJ|I2hD4+fD}v zN@9r+6Ji+y2g)NCgQVkseJN4xgX-UzI0_pPGoyQO39V3JFrSQ}jywVtK$?iB+c+0d zyRR%H+yT^`Vam`Ujam>8X=f0V;G(5M@5?N z)fN)#t!=C>C8%ZUxKw0s%CzJ+iv);=otAa(y(Q66`b3u>2Dm+#jA%U$)kqv9gx)%GG z0Yg;Nt<0FNvIw=N9qkrrzE}DJ)VCbrm8xwsh7PWHeGdDozkN*Re^~y!&u^~hm>(^waiOwMBVxOg@i_z*tW2sGrQG^ zrYRi7YRSP?N`<95Yr>OP<$l`d7a67OT((>TB{auFE~%KzvJB#@25U6boRe0+B^s{I zQYM_X5QA>c4*x@8Lt(K~WHYR+>Z6|5?}425RMcGcFU%rh*S6Py)$>*bofcIXGLw?-V)hS8^~26PqpdCDGF1q`Rn^ z2thBpb9dN`7`eu(6Sg-Vhg0c9C~U|qLEueToO<5Osred&Vbe;px`~g(ra>c9XEYuQ`ff3cuKLTCCK;{f z0t2N`)u2C_GGkD+hR;TNgI)S6T1(Le8RSBA{N}<&-)xUuwa<;qd0uhX%2=R5ZKP}+ zCSIS5hhDjgN~N`(*L#P*ekoBikOin`QC1r2dfZ;>(>x4C($PY_8hEwUKq=RiUVCCw zcZ#R;iq!=jmB=t?1!X}?#kSl`z^b*x44cMcDSFiiTbDu7eU-ng60xG4(weAKWjpMb zC40Qqtc8nItAt=~XM!@NUTn zC0w7c!lB+bt7fkZIg#k%S-&trHkWpPqLc8zFF}Ke7A8vs$8k9@N5ni>>-jV_rjD`j z^uymQB=EEvkd@hD>Bl18_vbbfQCi2CN$sdqNr|x9QDvJG5WKwiQ1?6LVlj2PEPF?H3n8_ zNrU9|1f1D(tQX+yisIVhV1c(FnW?c#8rBBWUTqbc(b2T90Y^HhK~PGxS%7p`Wf;7l zv`{ovaYku?Q%#MdyCkDeT4`(Nj|z#Fz03k5Rv3+U0#MmqSR&&Tk&^+QWcs?Hm}|PN zTOG!o?f=gC`Tv`C{?5*WfBfL9AG9AF-~VU#zwSN--s6Av-h1x#LG8a^0AKj4cR%y& zm(SjRrk%a&&ad2g;f{Xib*KOF^oLIE)6&UrpM3DdKdBx6-^V|8oF2d7=#P(n;^=FR z=%f3Gzi{}?htlDv9sKga`w!l7@HzXxvj4)qzW;f9zrOdwdvowz|L^Sn?cL>WdFS_m z`E7o0e8io;_3+^I^mO;-J{pHMuZr$^-|$=S**SdeTXv2<^&{}3KG<$JI642l%zc0> z3bc>>(uMdNAF($Y-#q2rFy6b-_@;u##+jRpU-_77!+7^5<5v{CX#Ckn^o8-0lhc>$ z_S%!jW99N=ZE{_sH?e$=^<{HB5z$-|Gx8)Y}izbO0cBjUz} zKe>ay*>L>EhHvg$ZZsUbk^H8D7sl7CV5*+=M&4d2{*d`$e@@Qt!BKRjDE%D!Bb!jGCa%D(*QXxu3KazO$=s^2L4 za>4oRBj~08t8V)Dae$nUtlCWhR(W_|X^MXna$_W8+_NVf^IfAN-Zgc=;IPpZ%He zqu1SNd{e=T#-Dxk+QRtDuUf8T(|K%s|EJ#g=<{#txhvnfK9)ay#ToqQ;Z5YP5O|S1 z{OI#;l-(RHFUmgq=ruPsd~-T}O#FO`dG$?RD{sd7WBJo_uf6I<@|y%+Bo9A&<&CnN z#$S|u#$SGZgW%10`55EpBjrBis|MC0ZdOy5R-+#@$KfLz~Alu)&?!Eb5?cUve?C!7a zy>R!#ci(r{yG!i7ec!$Nxo7|7?B~wD|LnQ5;o0lYj_>^YJOAUI_uR4XAa_3N^rQRl zJN=o{?>l|lsc`zt>A}grJ^9edUw`nC2OoU!?g!d~H$3>{6Z7PaC!c=&yT?Cy{N2a- zF}L@#$6s)~bM%p;4<3E(k#+ju=NC@WEGu-2~|1 z?*6as{r>)kx7j%O{|)wa`~2Rktzl<{h*@`I7?$Yhpc`Z`%E$0;jdXZ4RDXj3%CA=0Tg? z!0r6Yi!cqfir&Xk9&9N0sRgg{1g@(p%7#UNVF?KzK5ds)=xM0wWb zHiKYSFZ2nLz%au(mg^+Ao}(I9dga{(j#>r%bCRcPM$W)} zpAB_8zgws?kkro@t3&JH){vVMo4tHcs8h+o3Pl_i3vts%mP2Mk-R`PTH|o+z2%ZY@ zOj8gR4QCs;>V>*R&g4-JvJzRe62 z^Eg^dn%@poe}!M^l~SRuH$a_7tP%7s<%8Gr7OJmF}jxgbQI z-fZj6doFSGY!dr3eBE9u6KOQvd_?a2e1Qusb1E-%9}k^&yhx+XNAAuK6u3a-hL~!g zlgX&d_zShWL2u`qie@-7aHa`vRV-$SxJ3LRwt@TfLft$u+ue?*Xo@+-@qpi4QSJQV zC2lT7JwJ85b?iFXeuD~IASEG1qB*r8$6r|Z^ z_wByo0yj@hnNx)k-KSS9W=^=xN7~MJUo>|l^o+)%;&_K!us7whSB^6Z1?L594NgmhM8%ELnfI-Aa%+HZs#8s zIIN5GW6wm$i6REs3coTr{^12ZJMLorcI@{YLc&E1WFFhlCtaws=Y2ad(q!FdSZ~I5 zug+3Zp>E3O9gjtHK_EMFZ$90erPhTy-7*EPHw#QJ$|!?(t_<(LU*JSuXxq$k(8J={ zD3*-&2D_b~DR4}OoFV=_{d36*x7?%!Nx}T0n7Ve|4pocYeCSDV(77I5J`usUmop zwK;@#KD=`QT%@{-`GJixc)yEMmbv-%vh$BG+OdfLU-sTSz;X1d7cIN?rK^D~kW7}T zo=PT3$0_g2WW%z&+p=X#-X;^rvQ}%gB+IgACVA5XS?(lc>e3At2qc7r9TLdCK!C6X z-U}f-AY2G=*>6Y)a3QP@?q|7rDY;xF_jCw(xB8Eon(m|D`RV)4Ip3o5eP=MNk+?~Z zku00dH#_!rjvt9paWiQ`=^WhwDF=guecn}@YGnyL+szW}put!AGzE8Tt>U^mm7`6V z>@qMdgN#X#64|kPn(O`}IXztv3T%xTgKX5)fSwtwK0W#V?^}A!(#3aOeC5RpyC2=< zcb|UY6Bo=2_wM}G&NuG_xBqzizi!93{(9@JTgc}BxA~)+_it`(d}xE+`15$HC40drO}JDo*-?TxqVL z8xQQ28VX1~Bjh2t$}7Q^Nik)6^Euz?qh6aFa;0GbHgEzSlwq!jHF7O@6fbvef^z^P zF`FE6rEUS%SUk=PvV?T%`HEEFyY^@~5753ri(IK$fFq(|fLjgCGRTHk@|ieqpXba2 zjLa~^ExPAYN*vjER@`gwr%4JYJEb;|IpsN@{5GG%zyX2d+ez)TyW79eS~ zBswQcFmDQ~3O8{3)Oa4Ce+IB@0UE7zuaRt^a-t*S4T(?KU3wm%eV-M%QnCQkP&wT! zz?7z5ccVix!~SC6Q4MNhQe+lunJh znmszrVK{SxKXRpD0akH%%+o1|0f94)l+235R}Rm*Ufu#!&)P(o$8gs5auy)dpc@q! zYfv?#ltIZ($39t^2RQ3`SqqRoYv7v)Xx}GBu4F90PBo9@tLa8LEoX_UN+j*uF!KOs zeK~Cbp1B5}2RQ3`DGN|9^NDm0%8Pxxk&`Q`WS<7l1Dtieqy>mdVnK!CCY9C6jMg#h z_Ep3@z**OWEkO3HEzLQAp;Mu zY%j+xz%fYf1v&byjK(7AmXU=Vx?X6uQ@+sx>~iF2FrxU{IG4#7BU!Z1{^z~&Z1{Tl zv<=VNk7|#_CA2#;Q00Rsdwtz9^sf%Cm<3Vhhlyh{dj)gQ>(oiNnKw$KhmY zCRY%-a-RiAr+GXnk^QvFQGyPR6xmnYa~RGf03ugn7GOu0Dp*nAc)e4@*|+3*#y0F!JAXEg>Tpt`6^X>n#HFb{CHQ^FSD z5aMwt-Yboz(#TW`UDn~aJd^l?TnSl#XMQP|!*C{_2DuWn0ONAp5T#*}XPXeJ5edz{ zK{yX^Hhcvvz^stYDZR0381d9#h{YNEs&F3QZ20n9fQf29iS);DYTRi_Xv=4|Gxr|n0nWPK3oXDi zZ-AQzIO}?sEx-XGVk8PG4XSL5PvZsHbkr%c`3cCC7g&JmSs%+zW*CgkPB@=$0di;U z+sp&Bcgke_zr7t;x_Is43wA%Yo4N4W3-rzxcg&p&+usKM`uxv%3!JyWc?+DkzIAq! zXm;~V^9(df_1GUBlvnOYAGpU-;P?7sKCjo`_C~jlcqPAmp}tp2Bs1||x74U3qk1-3&Cb3OxO&cG=N$Igkp{>q zZ)g|ZJ2M~cn%$cp0xQLt1ke*od5Yx z(*m#Cy85bh3hlgc&$Y-Eo@X1!gO_xfr=&JVUb^pYu*vkoyDw=vxGG+3%PL8!oWy{? z_lLughabGu8R%>q+?_A!8u-imFYPaI!RI9ayuZNRn3lW|Ea%JMax58%c>6>>-GOR+ zryfW1QJnO*!iGNyF4G$aBLzNw=>ecXQ7PlL2~Gg?kwSr2yL5EdrtR|72`5#S#c5+* zI=gWv4``GG+HnHv)DI_5b!ABh?F=3R%|5vkcLH$BXl#!HmmUTWk(8pdKrje?x0*uJ z!Il5uFYceboAp0!o*GS{COTx5V>oH@f5)%ez4>q8+A`Gu1rx0P_JzQdBj}F?y`g{) zJkEABmYVQ-@RG)M{Gsp!$uLkT*pdCUpeS696uLe|CKEj|+$xkZIVm=l(QY`H>z82# zwJ2*GBX#rvxZ}hEvJshJQ=~r>0`*=y?GhQ6CpvO88O?P-6`*0SCn1d5Vrr>wtKO@p zwU*HZMK@C=CNHIZIjz|bL!)#phe_cvOBSjG+rqTK2#$If}*-Czc8nabqZHc8Q)fiQ7YFL;F;!STWCAw=qk6Auhc{`DxT z9D8pI?&iPEUPTQa1`2OV*kjUh?2ggVJCkS=vN%rp*ho$T!A;^8H=;9%1lF%S*X6JO zu2N*DJ9#_nUpUF&tbk&HUk^6{K<>*IZUV7LFbXuB7;dzFyre>;S<(ieS`|N%v0jl$ z2Sn1_%cv0sHZne?pGjBw%I$|6B+5~~WE|<5)o`5|)^&vQ25~kKtyb9xtw)W}IM)>N z-Uf!lX0^;led7Z|!Vog-EeLS~)bfoHbV}n|Oe)ap@L~?fc?i%sJKR{W4#LfAjjOMm zgqz1ku0<}fzH{Lw^hCqWWc-;KoEM2#698NInBM;V6$%_vQ=z&9>Uu5+IdAvsME zI-?_>zcjgeeP;a&=G_y~siU?BssI3_fgnuG;krlp!S82yl;d~>pl)-Z55#=_36m2& z4_x5p4G>wMKja&sj6|CWnarkhHC7O#B~gep>Vp9mVMJ8$-u6IW2!OKzS&>p2eKJ4F z){JT?L9)4CwTv5~hGDc22^s}r39&I&`AjGxMUfz4Xa%UiCUd=Ea9E*0=L*ryH#k7i|Kg3rW{#H@p z6PgJ&+DddMCHnpOmVDdB?qkBqY%V{Ls^IFsipi#B36xll5TmMa&}js{T~KWHa==gy za2h7)M$9}IX@PhcN;O7AE)?qa@>i_-F z#mer#?7m~Sc;QPI-g+Uk^EW%+xs%xb{Pwl&mu>wyIMW~7{G-jsHiH|V-FWTB!TM*` zA6b6^$o+q4?K!KTTph07z4D0_b>*(*pI;W2pX&aYn{|J!>%*?DYv;>S84x>s*@_#b zHhy~V%q^FFGREMk3>whdoRpTTQ8rXC+Qx`&VrI4t@e(g8h!2H3FxSKmUuwbr)ZUpn zFZ;D@amZKXIBr%e!Du}uXx>K7%g{Za?unN9bdNxXJ})(hm9p63ODx!rI)FuSrmwT< zCg2;(nL{X%C`KR@)TiyR9x)HAo>DtfWP}VN7O}$@Td*H-0Nd~+b27LOnoY^gR%Q_F zlT^dND=NX~BIPDAC}Lf@rHDGo2eHHZEZ7e_fMtT!YFYNwy@hB_9x!yxm&dd&?r$cW zWhk62h=dm|l+3Oo1K5}a`ymIggq{xRRRJvg3w%GEiPw5~XiOR2AQ7O)nA9UADqn@_ zY5+m8!>9%O!M!uHiuSv3CV-TMsHf)Z`#MaKs8pFYIL%pY(Hz70`#H%w7KZIgq#DHz zBNprj9KcqYLYh?l0RzV>?P3k7;5{(Diy}lgplt*O6R|c?Q3-^5YwD#}F+z)ce^ihnVxJ^qQeouxMyko`D2@hLFK7g>1^e!^OF8bh zScGmuP%B9$)1*HVr3tu%c?>F5gSAeaz`b=`sl}V9F$RrrV8J@5in>UFQ|$0X7OaEn zruz#ND#Z>x7OaCZrHfP}#SUL+!8)i#x=86!?C`P$>!9%HB2`AQ!xvbv4oZwJQY{oa ze7*(ipjPN2Pe7XhephV>U0#zZg!>3uW4oX5UQr!_d{00lw zL21YR1*$Y+hhJ~OI;hdONF7G(@TnH8gDQ;s3lv+#4xeJdIw-Zczd&V0?C{AJtb@9W zi+p~?4xePfI;fwx$af^{@arsC2bB^R*)O)f|GV#6+IjuPzppajm-9d8Eii3?*JiGg z))$Cpy7t>1uNdtkSwg6#{(BLZQzZ| z=%@%sc;6skFv-q1TV}O-!j~CzatCnE;nzIt)xkFF>z;hIJMpDAhFpsbsCUknykC{R zw7?|cBwsozEd7;nr)dh0mE&^%saZS^-0FCfT&!8gJC?>{%a^(R&mTd5EH+6pIPqJb zuQKlC*&3Wqh0?+50McZ`mtZg?iz<`qWlcRmjY|iq94Pd6+n+yr6*TXcy#vKS!l0U_ z&L_m=SSx486>wfRgCyc(tx{#-LO)u|=aUCRco<05#9^U2&M?u-nF8)T&<7ofC>=l& zTPAXtSBpVICtumESD*Oh>Q%z>%ddAWG9$fXei@E^Rr=)uv+R@n@`QvJV1{3TRG<@x zfG_(Xvkj!cSXO=g<9zsRw|oNMA%lLeckTe$KrYj4b5K4gbs~PqXvTw#H%4PVoTL?9 z&SEeVjYYkz2u{o2wj;usD6RFIfrvEjnI(xR#N#qa5dv9LalIm-@nIsbD?W~n9;g)! z$IVu-d0CB)s^g$I_9hLy#x#>%CJ`f391NjIs_4D!>^r3O>cA0St6%L{lg)u^ku}_% zbHwOZr6ZPIhm6&n;vb>o98dJroTg^=MsXO{z~@6VRtp9r5wcf|SMxj- z7!Hz3LQB-qLEhK8T+Yh9i1%Pz8&(@3HqFLJI-{!1(Pgv-JIiXFy!z4i{}tDFFKz#q zjg{4}2fv*E`Onh=AP11Of+ENPG@Tb|-aGTU30unXWVLtF0f5MfIe-i7idr$pn$F%x zk_9IS76q`akWbD5JoB9=5$^1~eEZ?L$y2xH_f?gk#p<75HOi~f#^T?S>%Z#yieGenHPthM?%qoD`m z{e!9&V+&b1n5p&)b$^|bE2&YqY;?Mjx*!F0ZzJt5r?ixST)fKX zyrIeL{=_+eQ7b2EW+9K~;q<6DYSxiJKZ~YDv1n5pH+4GS3`111MU^_y+ctK*S~8?! ziKuBhC@j$xsk+!@v&B*t;(}5a85gLQSj1bU=4GA<29m9OLCR$P{;{DWm>R2PxqzAW zONmk8pdUh;s$j6OH0;pW!K+Ue!S1W{28BMd{Fvz4&y*s)QkSEJnhYhqpo)1p=P{Z* z7xjkBx(?S9NE>D264^C^w>A0J&7*T?mqK1EMfAjhC-7A{8&?Z-1cvlzrs9Wv%~FRS z_e4-~eOPJsg<>p-3_)QNK?8TDg@Y%}#zjig!11H6&{SO1n41HwPMOv&G$E7L(J_`1 zgz56~`ij^$**Tissk+@EC*%)Iw!TjELQ_pi(6HO%1HPVYit)?=BDM6(#!w`NM09K- zj6pK3nk!fXF&IKC4*|ClAq!CV-^W8c=&0Rqf3a1`W3Zmya{W|4Eb4I*R=b#8MkP;Kkm}Vka z6zVCdRi3)m$-Xn~i0Qwbd^R%}2;W#I-#X7;;G^BmiMqAb42;1#KKf+&Qk{3m<)jC12CNCw7u{SL+|Cix?jWZyzF`J}y9k29F*v2e0z9VBEP>VYj6cYZ ze3{hcA$E{$3^)>s(XBBYAiE8r7eP@r5D)qdBn?tvWw_Q28iR-)%q74;3@pgzSjUgJ z;$}8I@xF)%mwlsB^B|HW>I2mr)Z-dbOA;WvGhb&fbC)BDroYzeCs3$GBylEOrz5OD z;^px{GRPb01FqVs5&i4|g=1VH7fy~FYGO_Z0{VQu;L-a3u9c@O?Y`u~uU=^F{7-Q6 z-#WPY?>SrV-AZhJdb6|f*Bh_d*uD7Xi_hBqvGwm>f8p9k)^e-Av&ydg!^&${p0fOo z<$(L=+^FjhUGmbuExqXpFP)cP|AZmV0nS_Cyak?63rLUdt}Z?B$i9qtc^0rz*5BNarcE%gs|5ksjSXRpIf`&Fh_~&@MMm zVOV-}>r{pItAOSyw9Cy?7?K{{JXPWGn|b`Pc?#`v^ArZ9M>kGYXuqFlor{ou zjbWZ!%$7Kwrw|y;(y0pV#xU;o2$N>?5_O=0wS38#6HCO9KEM`1K3U3u_Sh4x`*&KpMU za`O~Mr7Po871~FEc?#`v^Av`qE2C2tI`qS+U2dMjpmfDNRpD{}n%{?Zxp@kK9}Z7d zX!oyqO=_2$rx5s|ajL@O{x$y%?Q-)J0zVv_s?hFV^WM-dH%}q(L;W;`Go{q#D4da- zqc8%7r`6LG#*X{f+$Nooo1-uWrim-3Dzs1k=Dgv|HU9Gyf@$LNsS1xf^!zup%gs|5 z-r5*0eWUAXZr8x|?`wOj|7-0>*4Nj&>p#13*VeyoeRAtD_d%hgN@K{R?Zqv-7v>hieb7URe2wwbJUhFaOid&dR;pwUs|u{?*kNuROZ^r0o~D zKd}7W?myjr_j25Qzw7(lZ&>AAzrHN5;2T+2+U>b$TzvZO=Uq?RC3b&$_i@+1PjoN+ z<=WoH@WN-7ero+=JM7kY3)#A~`K8TY+5DbObu+d3^o_sT__>XD-dNkX21~EF@jrL? z7tito?vuMS6glpaUa$K{PmvDWW1{;<=1T`>3L3gS^QA)}yRW*x<230RCf_|@Iv73v zmj0OgW%H#&VY@51f9w?Lnc_?CZ#_kNrkIrbEvHBakGBcn{f5(|kH>Q${rXd+X9^y< z-*}32@OXOz?AM+m9Xjr_z^dMQigfUJ?*QpHogy7LK1>4XM^2HRsUYNj=rrk>7JT>= z>6uz+?zf#HJyQ?L{n#neGq?S^)l;Nr?rV0x<`ik)@fHMqDV`!7urGkzS5A=*&0v4q zDbnCt#N*q^?%pZV-Wk87=Sv5pGfZA`n)LCW2UZoHFCCgGBjq+vlb*pQ=Sv4?=AG`; zeCgT!D>pM=Iyf_HcNgbNhi1wsxoh*KXO|%E7tfat`HuTMut9d?uFK2!yG|abW`gY9 zr(n6$K5(j+CC9>zw$KcnGjJpP5SsS1&pwMn)FNy=1!A7-h!Y_u+yYxS`a!- zdM2QhPm@0GYJkZ%&UZ9h{PueHPLZ|KxU~KIOD}S6yYo4RX6i+{AK$s}4q^Le+T+0s zcQ1X=Iq&~GlBcwwZzin0>SD$P{paES)yU+O_Kw({!TtWzq-UbT`%aUdi4N~QO?sy1 zfASRRnM&2}pSbvrJLG9IMXTNK0=@oaTjp*}z+LZixiR+(-7Buoy56_@9lQPA?Czxt z|9s)o7k>D{l?$y4{tMeXf4uY2Wq9YR`yD&N4!rZs?Jv0>S{AlHx&8gyuimb0zi4}X z>kqa*u=VCGX6qZbp0@cnV9oya<*SAlQT7TQRxn6ev-|Np?Ut0Uj+Pl}@u+~|Nt$pqCGgd#h`tjB8 zTGdwbtM{z@%gS%8{OHOzt-N9-xU%E^yX8Mw{^(buM?7K9_b#LEJ+_`1#j#62p;_9g}biu7T7ynzX^W zJ~EX{Yh|(CAY`-E(o2G7+RJpi(5c*6#j;%qORguRt0bc9-EPzD(FJ~3x9k1+sazUG z(hZ6oFgj-R@N_M5vsh}Po_j6(XZO&-et*gI^8QY*kq?v$rX5luFc4OpC#9mMO6`pLlo5+ zR_k(Odw00L*OC($s;75{!f05MvyEYr8QJxIrzNN5qygG2jnn~)D-BpH*!yGYx2AHL zN=9yso2pu_3U!E=?IoFA!Kqxlo>Gh|UQU{=Lakd)&e*}gl2eEHpa!Y5p3cBfNl3Qr z?Y#8+Q@OKBpSqqumFpzxwK|%X8idGGrjoE1s9*Y&WzK4=KBUsg1k0NyNT;P?$kr}T zo5Iya!$?Dnm@1Mrqf3{0o0uziB$pmms}!xa#%MFqOl35?+{#oArJCImY@l30D`g2( zwb$HU`odH$$5tz2u@0db0i<)o4a|nO^j1r*KOU$(G$V25Dv+2dAVuMZ#+@&au38c;r8MgUFa@M-O z=S<}?a#CW^COZ^}T%(^y?d7Rml+`TqSZZ|=N(mVtaHr5wd3)?5EjW41NJCtp2-6rB zYQs^+9!i(~Y6?fj4S85#Gr6wF=LIumZ{?}U%dK=}1jTs-R+AJL@7L|+yT))*`>dBZ^G09Si>7e&I9uT)D4tHM z;O>z4z}_bO6b=J7ytdGUNW%kE%bBKP)4KFKQ@N6whsrFab96ofi%Fqqm-~^aTv0QX zPFCoS`0}76!6SQB_NDKi%0Z}-?qs$c#GY6v?hJ;#B${wcRDO@s#=UQgJRnE64of0{_A-f)#!o|~A zx0xLjp@vqgq!T5|#;#$B@g@cjvK_6AC=H#fXdG=5dy=JG31E97E_blN=}dO#*Mw_MvL&*QRnROVKr{D<@Sxp-YBlA0n52U@E6zV7VHp|>3xuszcBWb@ z1DG;EHMO*AoOI7-e0 zvwyPaae5vuC3LySr|={O>w?`xaZ4@ND{%wT7>r8=L}ZXWown1P4v-v;ni&y^cdONO zrdI6H_9!);!gX1yS}rmQC2GSgiSjer6w($%IYq(yvVM3Yi?C zrL^&YP^A$8BV(JKt1^{4tM0q&d6pcgTo306v0*eIZk(4n&TjIq>QqibOAuwInu?K= zbTF%G+4L@b&T3_r={1MLVXvM;n4vl@>9S3Zp2ATKUWQwVWKx7!4awzZOgS@^YYm2! zB&6Vuk}X4}(M&5Z{qj_7z@Qpkx+I%^ZWGS>l)$)Ffmd z!I!>kDra;BSXMZwkSyh)EK6l<@3HhV*858OU|1t@lO7{kHk)sD?EMix5~Jc~(uC4E zx|7Z-gM?{=t4+1C1fK0?33kxnD}9=RJ2o-bw@&3~6DGS1Ov^HuG{napyGOcOM{;_) zAQaddGcFBFsR2DRsQtPnCmIZ%NosVu5A$UdH)f4~|9t)bgjeJL@c>rmp0EFJ{S4~$ zy3W`C=j;Ed*L%MHKVSd5QdWpm&e#8Pw_rtqibAjFY;Lh&0u=UGZ`sVwrFc|K`RJmYy#XO&B$|qY-L4j&#tX20 z`(@W9zUec6{QK6=Kbeyy8H?FAV*tqw8_5g%!6ZT${w(-lB898k$PDMZd;=5$HVaCt z=k2l_MMbkCpX?!;i;}!Bq4z)j()#(23y@s5k=)s@w^SiIPSn{n)1mxBE>qFFjNbA$ zt0g|k)Ozgyu0^K@+h4jCc_24uM%6BwvpV}N3;5{uC|MQ4CO0y zRrMtkDX|$4!cwkSY^tefN9p7FLR{t-w#khNz26>MKOb~V@>$p16O#Ab3_LQidjyTTV zzkN2})d|!8r#$)jYv16Q@BY=-O!yw$6qtp zufBT1GjNmV@**|lj(95akW$+MxC}~gz;t=S@t0pR`S}UwVe{^*58JHNy+~QQ1+COQ zVfPDvG5Pt6sN>dr*42kDE^Qup(&l8ku}F!#BZ||i+Glt8gxklTXZ?JaV}^TIADpz} zEfb=xeI-25qD(B?^9bWWpc0gODx;t^c?f%Qk!p&q_;NlqR_|mJ$s4U`-5hxK^`q9{ z^HcXvNdEN0^GHVQ6QaAXj%{7NxkydF8zilObmmp0*4XnQWy0~p&^(Uz6Aq8ueRX8x zxUonjzXds3L(luI@c#Y}%;PxIj?cPkPBrtkWMW2P^>YD}0O8-Nz5g?PmDgaPQxmbC_;A9?G9?~nezW4`-W z2NS->2A~CsEFST_WdJ&pr!@fmpgiIELytP<`P{3z%>tGesoHqN^Vk4%CR1wwdS`FK z^!pDTGrjw&Heq_pG|9clE$K&0r*{UPZKc)#^!C4-aQr^fF~?_JRqX@N0@q0&Q9K%e z&TPgBx3^joqqqK^Q-%OJszg_;wvbp?H_vhS?UHtgP zhcCYL;@a*fc3-=jzwr4B@48@K2<`my&Ufv+Wc#z*Ke)}iG1q;rhh2Z~`XHzj@TXfp zzQu2OH-CNeEt~Dl=WqP&#=AE3ji;=CX8n8DU%7tI+Rv}OZY{p%TK$#P*R2*-zOeEw zD+jLV(%&zA7%ZZfKeF`V+g?jA4{rO>JMbX1ioX1Ud(SYF#d;IN$+1xw)mq5de&X(E z6K8g(ar^Q6R?&k6AZ~8{ZNK>?t7u?>M{nJa+y0hUuA=uo;GFt&i|phbrAXLs`&zL5 zrZ9kCfZKF?Yx|AYSxKpe#3!mWMrc(euj=+QKgTxiPG%JjAHSu@>)lFL0IMW;>BZpN z?Ko0G+HFtZ)-Mi0e=ro<%3kk3hmbe>Z!3M>dK^AFYdu?;>&@&CV%p4G@O8^+rF)HJ z1C1^VU<8cKqnhM*7GohY*A|ljR<`211UHVI^t5)8quB-2Cr1 zy4Bgkr-iey?aofGck?qhy44x{&2HWNy&K)??9th7-TeI<-RcZt+O3A3m)?eYR_4!A)vEbG<J#oBMYIBHNG95U^$64!&R!>4WywyTZc zDtgZXDy5D{t-k80@0{{Ht?vT13n#ta)!OwT&*2N*?7P)hTp#irK04cXtFOF1N%we+TWuZ1IH%8_7~@vS>$aflysB46nPjd}D+)%zew6wImt24Lbz5-w z@Wd9@FI~3?ppr&WpVkb?vJ@y z_t&~U?CQF90Q8;zgI#;&iW{Z2-oBS)aTDrERle3xFclS4`)z9bwQO<7SL8TuRx811 zJtk=0M$XI7J)iE0mics#K!-jrHHek6*tG{N*za`!i{eaQXVXnIpv7|L5K1J95eQXP ze~0ymd06$7+L0n7WDv24U3-NE`!)x#4No#BM}s~#B{y4{L9kC!4Fj*J1fPqPo5Y}q zb?KHOf^_5{cI|!(_N@+JnP9bAmOXWEA)1p13|;f(F|CXHo5^Mw3TF!<;e`t&v#ZDe zwq?P7j{{gjPlxoX(1B_Nz8}uSYdt(PrVMY82+(6p>Jbu^uR?V-fS}m5rUm=mduLu{ zw4bJi+OX42jx&5cTC7whuh%CPeM~|k$Mqzg4^bs9VGI>T*RX313-&DzU{mFOzyrtH z0nQf_^&BUqBRMt^;N+C)3lhP2rWvmUDr7CIda-MD3--Glz;@wG04WPmPtDi&b(kVi zsWNSF=~T5va}49}=Oph~7`7{sY81OxvtYl|0jxh>$9t(HOnN%KK%!P9MU|I((MX=b zL*5ik`Z7r}ng%w80t3S>*za%v%Z?1#Hyp$YT969LY)lm8iozPrRz20Ba3onCw&AGK zZ@_^jb`7&&|C<9?pU|dYh$`SE@O~F^tz!o*M{O=Qq2eInp(j_fo?pBU8`EK z-|hgmkML5p)dn?HGipW;jS^&;&nkMmo2n2UL#eT5k@hMnEy-ioPz&~&1K8q_Gr}I8!v z8xqFed@~vL*Fv6DIx9Z9<@Bf&XPS}#wc`D;)Uh9p+HbK4-Grc4l1!#ae!9##yonlP&OimXS ze~#}*#Sz7Fcn?@Y(Skj604vwRLayN}c+fT%2zHaoV4P&ATq`#&`*Ls@ZpY%~_CNO4amF4f^oL_>&b$*9!5VK$#OSQYL^0>eO@5rke2$FAiq*f%?X zHKPW2=Yf*uxPjp*K`GQSdXr#G{_Df z=6j~1#WWV2`e`aPq8djtCBND#d6b?;4J2S-Sqt_}4q)3{rAaUt-8FjtQAjg;p6sB4 zO6D-)&o-JxP0eKGa=lsR5+KxMEZ8?XfUSlCq*RZXVgQ2D0o9i`gjAm5%VMI%3}~{z zA#uDaCxg8)u!OV)``Fo8_WPa|jnnB~V(6thu;~lLeKM2IsiSDDSxG28iKJ0HtVJ6f z*2S)+EZG0z09J>PR5X!hqu>f$raRKp=3rdW>98E+8pSF;%#ZtUH4qFC0)S0guy1ex zyTIW_>>6yrzTN@s0>=@tYY7YXbq-(`I6#P9gDlwBI)Gi^$RBnsZoz)D1K0%)>0#Ht z(SrRZ2e1nqtHZ9n+=6}70qgX*o4+>zpyr)Z7RqM7B~M`v*k|jAc;^_8|wb zIgJ@s#csGOU}b+>t0rq?rZyb3+Ig-!>Wqq>WG(G4&|{JVE1if1`{3Dm?~9y9!mfoa z*s%jxIuxwN{Qf~eL7{B8!C;ASAzDb`#$e$06dGVSQ? z&L+@AR$yZlA?uHb!LpLfL-8}4t6bI!44h3$~=aI zYF-77kys~_>WzxkN+l#H6(%*rxI(9$N%FNaN0{gkyXLoG4F|9boP(LH|DV0oS-SX| z-GAGC_`<(lco2N&A8r5R_HgS|MTj1K5vDh(ESCbMEeU&NY0*zy!xH@E=*v7sf!Z=&s~_n0y8}) z1fF9NSOD}+cftO`1nzJb++z_~z_9l3Fo8=Jfdz)uMGn)RJvzMloqG!tSYT9jLg4O& z2`n(8IU(@ug$XP$0dPX#Sr&l>=0JDa2A;Vvfjis<&sdni9qxj=ECLHy_MPs6r!P$4 z4tK%R7AA0qyWkrv0t*s=&LuwuWnT`*byUs@_HUHrC-o4XI~e#3>wF6`}mXeYb<(e1|8 zuWofV|7r6L8~?f?Z)~r>di}|3KML|J-nDvZ<)>GC%fGbTc7M_RnCstMS6%lmeGGss zEs2M#YZS-m*9FSrMY)z&7Y%` zsM)*^>Tm_86_S=ZtPhp*((Xv#itL%N~@h^^BGz$(44NC`yR{)-B79_1#QS z=^ZWuJu#qXXhL+4z?0*WS2ff`fs*N79QA~yOcHIVeIzIeMxN}l*{t*TvL&1`hi;%O z0F>3Bx{?eI$Rt;*x67qorpyL>kr6=zYH}qb#eEeY*$H%F4JPXNrEbmtcx3a?1ytSp zz|y`_t~MKHLTe?xZIjnyW3%NkvoS4eh*GpBd!$ANPY2pWR}VT?by^0|Y^ChZ;nIW) zcx^Jq;HeBqt!RUf^inm-h6+a87_m*v%(fw3;zb4Vp>PN07HDIY>VQf|kG}@!nAqn6 zc_C+KLd>_%$6pQfP3&`lgb~O3ZrUgF_`^WY#AFu8AaShcxXC>J(4?Dy$t;jn;#k$` zCS$KA-Ff`M2^a9%3#6hr<}yw5dSvtQ@zDz}km_;vcl{%qIsJu?XGQvBfXwKK%mSGl zUnVl*<0jxTai#^*KOA#8A=p1Y1o|eJRlqZeKv|K#i~Zb!~!KW_w_Nj)O7KzhlSiH!Jo zA8?sC(*ijuj=7xVObXC9ai#^n%sAF}t24``|vur=sd)oJz{oVf(O^Efl%0$%%$?tR+r zmp*#o1-2s3-db{Czron8$9bbsYi30qky&6*?8`)kd7K1XCPufw-i%`|x1N32a{t0o z!Tq!)w+u4)zh_0;*joRC_4ls70i4_qtZ%G+cI_v@>HSx%`PSAh{_w@uT*NLuALI%A z#`Zt1JahAdJNNAV;O@h_mEC(m-oUS4`2GtIu4Fdfv`K76Hg`7u$Hx0N9@}`;MhM(2 z@Jq{YS?({VmhW=^t@{((zqbAT+YfJJ+n2Z9TfejQ?yc8ty>ctGwX^xh7k|_Joo?9u zwAEi-ecS3_HMjcgmA_m1+1(!mdVly^MEFClz~r(3NMdTyiSuR9g}-1^#)XuO_Mj4EDEnyo^uTTYU;vvfPZx%1CQqM1rY zZj76%TCNIph?nh^o45XK`^a3*yv=k=-+K2H^=wgX>s?Mo)vX^t;(DfO-?Q>Or=piu z?sY1-|neM_cc6Dr#=M*Qw}m>pf0Ijjf+_DmvKuiK(b|rn7%>`?6EfzuJC*Q_(MMKi{e7 z=eMtW@R?lya{IampDFse?du+Vrs)6NzV5+iivGp+bq_vM^v}2NIckA3L-TL#{L9gR z*Bzy_Miwp*RP@((zT{N&Q#*e@6+L_6g{=3TiWb+E zBLZhmaG$>N?8*24W!HO_cHX=8UEn|GfBy5gz#GV`@&<)I;yYY+EmDT(?ERq!FUbR4 z;3)OdeRtCxL8D%H_a%*z$hIuC35x9qbX%suzg@cZ`YwO$(nL%L-`B;qtdf+TNI<$&d>ssRgMAJCwAQQ-`X0b8lYsD)k(e(;288rLxD&z z2skJzWgP3mMVrh5&~%W00XW=0X~anlJ&*x3p>(!cPMT_BhL&AkI{C)O*$rLmsfo@u zPmLyu7I{z~DOT?t+e)?&IY#Q}1NEp+Zgm8&7if9#5=Ht$Az$piPP;_L<%!}0swm>T zBBbej@}R^WbbYa;Hi}BItSB_aW-F}WW;!rvUQUXkC{E!)DAG^X%|@at_>e&+GolLR zo|q+Q2n~n$R*Fpef-qzC?5+BNBOcd}o7wb4KV9^SqlVWz6o*Wt$yFFh$4W#wNA$sA zOqBu^#zeA9=TaEs&+73SgW*Az#0XG5lFiXbxn6I`2SuUp^%F9-~OdksM+GzWm0gT$R=-wDWBd*CLOeZBGwg z(q*2KruGL6XJTF_g)iS%L-;cJI0cVbP$(|T5qLY9&SlesY|KkW%(fg!*2`uz1$!D| zn2uWEYsQ%uINE3d%yo?~gHPKls7*ZZ*7$oofO3+-fe-^^7Kp(!A^g?|JTZ;g;AL6# z?L?K6<1mODK+S1!*dLAffXInsK`xxG;;}fD$(hZ3tYU~HJPwvebu7YM782kRkW%Mz zJumw+2bvWzChodu0KPW$gkY5u8b$7MD&FMFBegJ6Jb z#^Nam=szid*@nQQrk|Qt+cNR%0quSeM~)8@H@_NNL&dFwkKJ-_h)x3Zk>O70oITFX z_yl-thRM`1W?0M{0%tG*llgvTO;mt{GcSuwSe)o(xAU$KfPvw)nXBStV0i4AuKfj~ z*PR;}ykCid;Ry%&1wJL86zC^qCnsI`A0Os#jmQzeU^XJ(c3|~Iy#Vm!zO|mR z?8_CB4Uk!m=lao@)bfQZNR%FP)rc?Oib?&th~IW#O&o|wo^KMca+#HQRxd}GShtMG zdA3p2rLi~KD#)XV(j;mTEop{n-mC=_NGkSov7jjod1lmP$Hjv|1rI8Lc!1+*r8k)w zItgsx)qyW2>ZW-H`?J`iqyo>5c$B0gy*P);)!8xc6%* zC=vD1BNGkM;aad4t@?y=d^~RQv7ib@6tkQuR+(7tpwZo@N2PAu+c>~RGOC0N^;$BR zW_&{|0ij?#Bp6@va;#p9WziP1H|S@R#m1PV5UN4u#KF-1b#(LI8yxWVkQmi#8PuZ; zeVJfN4*H-NEsev`jzoq1i2z-U=UKwT#PEGwM_O2oqd2%#WK1d8j1JNmlk^XpRm#+K zZwe|`^tSAlyFY`-~l{daG0sKH{ZSX3+iw3NxHx*!H?hQidy zd~=K|S#pr%!)7{Rq#_)?pJ}U+QmEP+3uvL=fC6=YBaeiF@fOvWgJo~dS11N6XcXzz zvU~8LpWzjZt|xFpsH!Gv|H`)AzR4ksBm*tF#g1YbB?R@DZpB;V>Y7>zYh4eDT8ZaC_jLa*c#D)?Skt0x3 zgF_4^ZvUvgPTk;u8x)%K>8*?uF@;LaJ1~eMi3-|?z&u>04-O?5Y|-@OhP~iE?SpWU zjE@Yu17k!YK~c023|C`Z3?CznMz=%zqxyi1V;*_0suocNE~YBUe5IHy42gnm>v!FE zlY_2_sdiJXgb@fA7!Q|>cSgwqn+s>_=~BXE5v|M>ya6VN@5>xr352}EC=^0)2om~3 z+EXpX*&5d*nNrf5Pk3_8kx$|4!#!BSGcstzG-V=UuUb?3HV$jwcUv5&V30|nUOCe$ zskktX4@&uflF88dXdnV7d83u^fjia)c&rZW;Y<#1zsW)Cw?^ecxL(gFLbRh9$uWsG zr9`(=X;Qv2$B~MW3XLGV*k-xSj$tY@;RW=B2N4*uBMEq$S)elbf&2O>P?K^*SZl2l0LFgD`rrMQn%f_r40DPe;yR70VBt4B*D zV&@RJ$$r7SKP<_<-P$$lyrt3 zv}hyMsN{R)bWlZ^e8!V8qd|-+wX&YNOa}KPeK4$%xJi$ZESt?YJ7XIM*T3B0P>bjB zL@37h8$O?!PnI+q=8KR|2?=Nu4~8P;1U>T612|6y_c<VVu%sEv z1iT$mV009wX`S$v3;~pdi8b4SVa&U4#AF!Nec;L~JXopHX-e^q5@M<5t#>G`Hprs+ zlGg7*QY<~(VFwN3c5D zDj9MhxZn2Vv5rTE{3)hf;-Gw!5~^)YhBINrgVeIwwifp*Xi7>O-MvnyGDKS>5*N6U zQOSTC4sG_Z`t+L|x^Saw_Qxdcfqf9m*M{{(lF9q%ln09Q-W-@*^+mlQVXfglnF-)2 zNYr9v8TOa;IFeV)1RtasB{yPJM5;sKAqZE|a1Grn=@8DQDZI+T#d;+V?wPc4*oxiY zP)iRZ12x@@>$QA{F_xZ9nzz+NEHXW6MLar|L zDWei7X&%{ZbfKb_^|Gj_h9ffU-5X~hh8W|mjK(7AmXU?3b`H11s6Jb-D}_RCoTvoo zyY<&L(VbfX>cb`RA(Wxb~qjG!E0n+;52=mL`i zv0sy`BG?r08nD!k_p|$BUow(Ph-Pe19pu7|bduJMFwP^Havw`a3K6qIk@;?<)`FsY zaXAhSB@T-`+k{Y!NNDzPbMwz`a9H33<()kOx%p$@WBA4sn2!Gc=Fyvdkx76nn<;kK ztalYETG2(k8HlyyjLEi(yy=&Qm69(c1j-5T{xAz}(hlfdDeVn5xxQ9oQ)+UUBWNTO zCyT`n(|}Sro*$=85Fem~QEl|}7M?5%<#fidr?$A&n;eo#Xw1N9FVKon;P|eD!2aBj zMW|AT>&J#=M9i7~LR814?mkH(O1D$)CAx)-$Uy_L$|W&DX$AbUS{9l(OE<8j5F< zRI!#4SB;vkl%Xo45KS^*&>>vF_cGmLni9JewICG~ zwUo=*Qwdz3y2&9>YZ=w_pcT=Zo+78f5*6h0o>&AiE4iFn6sd5S6mmG}{Ss)VyoJ?iL4Gb>ZIIPxhaKOirD#vwWRg?;l zajl%{l>{OXrn=ElB|{A(QhqF@X_XKKZNJQOcsb$8`@!9?jewD@`h&xKqwb@Fow@{} z)uw5fw6EP58O^<6mMs!AI&Kmp!zuxcJZxx%-d1`h~yVe#Hf0 z{UsZ}y!OGh;PxMGeP;FDo1a>J;mQxL>@UA<`5Es2=61*Q){f{ZRjE`32~bi=r7EdZDlH|ISVXB7srE%ul_X@YJd2EzJ)u!Vep*3;;9O z+%aMI@iQ`*T(Q<;6}CO#cyidLSevCW3ksTQDmy0XDoxvyEHNfXX%#$i<0#$5s>)R9 z_c3Y|8*q;BT~h5sAk;Q$O@|rP$FSDaJKiemu`<;v_C(4z!P{)KSvht`XJjzE8j_lN&3}u9$ z2wCX3T$qq-j<2c3pl(!6K8iQ#=e{$J5K~Q;c+_Fg0ErA>R@&wuhB`HokF~Y}(#8-b zTXjr;*sdw7PLg4QF|il3N)>g+vPKfn0>(HJ~ zSicRd$ z>SH1yvku)cigc+NDygAz+^7q+tWgT895w8LyHm|>T8BOFJxhj|Yr1t58#XEm(ra?D z*CiVbiLxu5TAb!;agfgNWi-k|t4brq=3qM`%9)9vwu^kfjg3))Y>KtU2-j5(iQ!xq z7i-|G%vjSZUl>!QUSz=i;#9;lxT;|1}~FhpPLTL zbZ(q1hi<7Huj=g`D?sx@C+Qw(`h%+*X zLY*iS+iHRxlFx<7O6zn3>?_V^&N!aE3g8) zRp_JfaV^YL)ZVZ$FnFCQHmv^U7FoN}XJn{P6Ov1Ia(*a*2!#-e6GjH`@-k#Mie=}w!yjM244@dEu&SprV#H0VyoV23tHraql9k^nd87Bj5QiO{H^hLIGyNJW|Is% zZa5Ei;5gIhN|SmM+s`S5)nK^%)tMfYD^aZK*YF zgjY@A74xdPrnotpjxkdm1xYR6WDO=KtO^_#M|QTDtW46qywo--@$nqj&u1(B*m0YpYYtm2ZqqvKM$gDlMeuY+?F==qp)qk6Y^YQobt{U? zF_hj!7(MT*Rf8XU%BqT$k3>2zc~%R9Cn=Z;&Z z*%)wM3~ypJ5AKDXRkZ<@swzB!oI{2-b9rKLYf9}Sw zGwUE`J;qKaw2oMh>UAgwyqc^XHuIwEI6{QemkO4CfLke%3r zCH`E8CqPzx+sN` zMYPsRG085}sSl1TQOoD1%D7za56Z*gI2vx2p?^jOF{w1MWT!;be zqnoJ5afQKqhGgL56oIeihB|P~s8SptaJK=Vw77;1(;?hV)J&x4r%eNIc>Q8Dft3sQ zzdpj8k)bHsQVuL~05w?~%MQBkw8qy!{$r14;slWsBrP`T7TyQv!?2x7cT77sn(B}~ z1>Ll$B2PPgE?v(?W-V8#v<*9+P-wkzysl(RBPP=bhxwq_r+V^a(>fgf`!g~yLzbAXtp@CN?j+P6o7?f4z+x?Tkke7v05&rhLyJG zLdU!1TB}xgvF*7W{fgG%}q`RMHz6rQXp%PE#_hV@Xqmr@&F1Dodd@ zZkN5|5wC0Qs5|sW1>Fm3{ru(@*}E<~BLi#O2 zOLjoIFVt=IRy`(SduXdvWR$w^kvZ@VoG^TrC%GQoOJixH(7?l7vH|s(xTXdsDZI1TM z2ritiMr=A}W>jaV(TzZs{UVqDe2I8>;xJ36hUV!!Gun=->GH^)6(7vlTgVgM|8$UW{-g9!J z@5-}sg!#9Yp;)(nA8M+s%w{+L%|==znCsaYV6)`%J7<8Mlao7J_lr#lOQ(<|Am1`2 zY_wJ`(8j>yvpoa>(fbf-jM|)tuv1OX^3!s!CBO~vsGq2d?EH(#wM7VnXG&QrnFJk; z)TuaWOnio?bawSw?9u3`Dfu<0f@ikDDjozD4d(5Kwot%y~6q zuqn6qOh)b536Q^{gX7#h_vP|$=jB;j=HK34XEfXI7tp9}pI3m@#cCc#AQ&LuQuAu- z#1#zG^Biqv3D0zsMTz#EsaMsMAl<{(QP2~;^uS6cZB zPm$Y8#+*$ZXx_7oQ^8F0p3_-bX1iWG%|q|3d0Xhx7TU1@q`RB0)wx1~>NVh+nHRaC z^LuxO_v_6n8P8kzb*IbrTCn|YSBhD|#RZh&u3R%%^8D9KKWv6gGQ;aCO@fwq)e4)Z z@j%q;QmI(b6q>5yY|}~tpSy6;n|&Ik$10Pz?S3fdvMi|5DAXGooOFWpIxeiGn^s;* z*Qkn0uE)KqK-6tD#RZDx44`C{#B0Mkje2#l8|1M%NLfj*!%-`<&CL#`4mSI#Qu(WbUHU_=n?-a<8jXP-^zMZE5Esci#TL{Cd^9oY%hucHUNYv7-$Jv%)>F zC}-bn7X)3Jui^!(GA~qF()HANab{n$ZEwcX8T8KFyhCj~oNw_vv~p&e-dQa#_R2xi z=&Yb%%PU7_WM?D|)oMm)G9Z~3A7@g@f#3HX;QN@4D6Jy2NV7CZ_q>bN!s;4UlfwZW ziM_Qh!@{^TGC<%%8(5i?qLgHja7O&3(uKw{g=7mYA)sYq6tCw7o|ae56q6nFtNFlS z)rcn*SdzwSp8|)%>>%4*nYqc>n@t^P;o|-O-W`X%{a@O{t-SZw?(zFufzG=E&pvYM zy7>wEJOBi+M5E!0vKP)eLE+?F_qQmSCDL1+pi2O!&KNIa`po}+S;xTq{5bn*utYcI zB~H*^)!X1s2PRm<+mBUlakv9)0;j8mSEbKAOz$Y}zoVpaX4+lkVTz#`bYA;CPf}sM z+-L+j5o0vI3(64Cu94|Z)+WUQS?!B$+$R~0X{>PQq~UTkOv^Y|3UIv*8Es{I z_WOM5Gn3}XscY|0$R#$_T~+aWHA(!Yw{_CfZ=g~cE_8DEky4l*dbaPH~}>cGe}w z&G|{k>4HrD?J6Xg`a&U}c85YPv8nE|Le5M2w|Zj`TROLX?G>^Z$tgY7TeckNiIQ(k zC;X^p6#_a>6o(Rs8FwtMui9mQyg->aW5Ung6pud78{^mThD@D%LvHOFyI6%pQrLNK zNKh~0`YnQ72Vq?zxyG0CcweGNSwj>NH|yqVVtpcUmYT&c{)P+@w!~|A$(M#uV-S?t zmT&u1Bj%f3CPRvYx<*&zuXSGhuW114N3{IQWCBLq^T|Zb$72qp67;~`|U1pLvHPsyI6%x67YEya%$0h zkOV2s*@@iZnDvwZ>PGrp$s%x$g{UT5joU=kjAZ@d-~U}suZ@DDn};WZI9F7P>+(n{ zRHe3`_PRXCf)Mz+A)0d7oF)}YP8a$}xu^?Jm#r!dLa>W=XvH>UP33$asfO)>22Zlv z^1@-Wsc*dhckTR_9fxn&|KH$$zy12{3jF_Y1pt&-Gz+ZEpu{d|tDNnIG>N;zFS0~q zoJ-?JS)%jL)|b-ENih@UY)AC3%1G2^gWrzDxEz#NY60Q(3xlk)d*;rL>vPf;whpoa z=I5l9Ebulj_Qr#t^9NarNha0pGSC{_T9>LVgJxo^Yr+gE(`Me_@&e+9C8lh35$K|w zGy|d4oz$~wKS|-`;(AtV=^59+v&me@Aw(uemG$BpV`s*>X_@08ye3;j1`&n^Q3yR) zgzaRxksj3yrd%&UI#d|;)4gS|5QjX<5t5h%cnuYW%R*gk9fPaJG^Ns@sD&Y$CC?Ps z(jb$c(pwNK8IFrc>!WGZ4lK?o6rhq=vTV3354%~LLtU{=h%+d&3wzwLjivwPLW%jK zu5BobkLQBHnXLk#cNI$PPCUWBKRXBNmy^nH5xH5{&Y;Aez30?ub}>BfzAKluZ`pP) z2K?^081Ayee3_eNi`T~%X=iuOp}&P7AYT9g?Se$Ka~uJw3!mPzd+Q=kdFw#oHWsoL*XLFF63YA`)7T^UPW6w(Ai;X_q4rWUTY zOpICE-l;gBI#9&xho^?~q_fe=rETc9T}AxOj9e|_INU-J??9m(+)Y;39Rkeqbk4fF zZjbkG9ickMYPw5$2NUN__AFv+13x15bt8=prKV;=>k=p)8x+@|DPlW3Rty{c02FnK z7w;i=uu_U>>t(4rsyc%;F>T4iwuSf9HFQ|-x59Km>qS(?fR%i;-0!ynM_U&Qc&fpy znz%v)U{__=gLZy*=X-YEuv0v8&z-9)KU{hL$_sYfcI@NFUVh9xh9BE|^wy)V zIqDyM+|k2FJ_iK6;1|40+qoJ;dRI&pyugfW_7uR-5ABeHBGOK!dl4a2@Ol>!n^~@6 zSx4WtpfD>0U32h^Jt|k!Vc&?$R#et$P{IjwlV%94PXt#$S*eWg{``WXnsqR^oiz|l zN+JHZ85i1)M^j9((h&1vsyg=aP=RLJR^!Ms7ZeoBT2Z$ZBun`s)~DL#Qj1QhgrFpw zS#jievW$;=)x1z>94#*>jAmG^m8Lbe7&Sc))U5S$NTQEh`Am?Tgr%uXy9x#XARYA3 z%^MWCMx5*#3~8Wne=6x#*L9|8(&{!4jn}gy0RZtZgaIHvb{<+#^ns0yX8OXYoI)~* zBFZ~iC+nsYkTU?FVPbNU$qia!uE*iKKf9pliB?lpx}h_0^>|!#+5+LYOr$UxKu7`! zm{!{Cce9kFG5hEZ3I;&5D4@ivoM-cedYO}ikeY@Apgq9mR{JfVbdpA?m%|TSwV==o z6&NQOTL@I7G61+;QCKb|i&&#V#U0cRvg5`8?%`P7+odlk)Fd^47`!=(DI$RZ9J6FO z1FU(xH|a~c%-BLJVy9i~c?7vLTu>;*34?n&(K{dJCDC@n$$OeT|=+Fba zHi-s!bE4ZB(~>Dpc59qw=S9sRLOK)B1+C8Pe#!=gnPxKp0FFyTeu+*Qe6^MU0A?ej z<_oztEI5H&5zOMyWOlr6LD4DqIiA4*J2wPx77+w5(*~rv<26_aH5Ld zeq}+?Rsph1xtXRiQ{dtF6Oz$|bDCc^6{&GRlifzgo7LVs@`bBWl3;FHO>kH1{4VT*`d^#P;{NMU;zh$wz!2YmQiD*R=}@p(fMyv0NbrF{-j1hZ7ot)(#M~SB?@8N_Ln| zC@F1EiTD4(f`Z2>K14ymZY&pTRL(`5CV<3&Q@Juejzl{!#riN9u_-3r^OFTd*%%Gn zYQD(jWlx*ttwfjO{q{7}S-2R3y&xB>pI{rr0sv-ri3wD2im9tp(M5FJGy-d|5RUE-b5l&4%56iU)PfEG#HU zv&js0J^BnQYCVl2cJqN=rLrj4eehHv3;bb zo$p^z6dEd)$d&;7d#t_nl3 zhfH8}R6*)WUd8vkWRulbpvfu4y)C2%BDMU&ii9KQx^>xrEA!dt>q_UU4 zogy#heXwU6iif7?+(ZMIqiHM&*X$0NEKW21c55^?lMQKKZiAu-1|5P*$vEXJIf+S0 zsWLxe&5V$fv!Ym0EE$b*E!Xo_yaff-Lz{-uVk)XeVq7K;CBZ1Bq4e0BN&8W~EWr8LYo+IgvkH#s$5&nKi3IOfTXy8!TQVO2Pz zvTP^r$#99)%6Y!XG!Z+hb5UTW<-l&DeZIH%dkdimE#)>Rv}rJ-EEIDh$u{$yW<5J@ zIYCagQfae7Vv#e*#QWa6ph&muL8H(ZTb-Wg7AtlxKt>Add#OPblA&u5GH?P7cz$Rd zeC-AW20oBP*{{Jhg-GX{XjbhwiBhspkg=G|O;ZG)VYB^^XAV7VL4g%1fQ33qc^XV| zwG0C1a)~B^$%$m%0X5Ts3!rH6OcEVS``@{sNDV*%E#0owt7%tnCZe%f@i3V4OwtWn zSb36Q(+L?$xfOK(`xX?)nC%#BKPq*q?QVmgFhy>xs03Ox1}5?Y#Vgs zb|qQ#MJ~7dnhgq~#e{?tbVbnUGD-yXT65BFbh~LrtI6GJ%rq)!vnG@}jTLfE5lx3~ zo|sm{pqqhS&@N-=@rP>Z-m&QyXt5R^3h2<%}S6xUIh=XKEh%Tq$V9F($?h#s+Di=huGho$D zgBY^yTqkRnp_MBa6kcx_q#^>OzEffjXX=vK;b9hb`T-Acbabmk7M%e`l3i}cyA~8q z6Ey<^1mNXbvWZGgb&#)!KEsX(UTIWQ9Y2$9wR1TO&F%m621Rb_^}7>-OL~ET=p#fM z(9<>@_}xN0P7{NO9#>F~hGFK=gBKKbpwU<_2-}H?ZYd5kKm?slqY7h!8-VYdL1ttI z>?j4{J2u8JDhY3()$6tlkAQ1n1VF@ivfLa71Xb!`R=&@F4+>g8Q|;AhP!F%}RAYoX1&TH><9t_`xAv9ZQ-O4-}DuoxmYmq0TPp4Rw+ z*dL{bQ%#^qo&z@)MOXZso%iiNxC`=m@7Rv{7;(9kn|7+HKFQ>p3P2iQLAku;ks5(Y zjbcKT&Ai=B&>lEkkA7xB(G%eoGz#ER!LX=mRjGg;Jrz02=Rwm6ITdx>C1@eKiaJ1x_7hYSHR?&P)c`%UawR!m?B$|-#2d=dKR@!h9mxwmYR>tC z#ae}S-7MtXW?Q9OMt|r~eV8|iDH(bIFBctnawXP0%mlbdM-2c!dDA&8e*2HRbp;l? z#LXjA9m|FE6lHC_Bo<_ZE(-~G7|=`;Km~!qHemqCLauT2-U|vxD9IXGDTSKN_k6|b z4oM>7>v=osvk1}fEAnX2AEul#w|m3RHwVdPz9jT3R6D6P3m9u8NFDd`jqZrzf=+9I zQjsI3L|R%obnuX{<9By_Xy>kX?0Whxe%AwcerM;qcRqV(W9LIwez5YsmFKTKapjRa zesb_V2VQXSh6C?k_yP`n^U%8vJn_II_uqE#yZb+U3y*X(9?-*?wH!0C(4B~g_h)sOSb&MG!O0j$by0-gOL-U zsfx+FDJ?B`)85E;R5zH)QkC?iGTTdL)uJT}dlyeypJ31_XoijpZGF_r_2C)~`m~W; zhVdghp_Z~10gx(Md@j%JUD${|?#l%+?N3Hl5@x4Sk1z^iYfSU#2qcTx*tE%20mg&| z0Ag1DZXp#m8sI&GF%vdu164&mGwk_-=*1MUUuyY&6~cH)?}7r@sZg7yus*=pD~=sD7AI_Ex#{kJ zU=poqP)U*@1r-(Y zg-{7a>eC6UG)-CgqBf>MT6rSVi8I2%y^Ap*fEDj@<$*m)Apu%f+Pd0c7{uvh$$+MF zxdOOG^wcC3=k_d~n|`7n=v-jp0@8*CU^fhtQ`k)m#uFt#s+yMpU~egCoCNted!9NU zV+qKXyPNrQs|O*aN))Xay`8)m`Yg8w?lGJSGqmI>@_`y zlcj#1YKeHwqK55$cg*a%?|h5^RH3K&K_^>~C&e*6p{o5lc%9g}E(y|gdt=Jsg`_6{ z>q^+SFk^y@nTT@)@S#C)foil`HGRYY3sEZ$^$xCD^+tgQX-#sz8t-}4VhoapbrU#} z`gJQcjXe`Q?O59Ck3#^*CUtrVRG>I9)fO_+?v)LSjACd|*S4};*r07o%v3Xpo@O;+ zAz_L#DF?Z9QqI~Tvuk0x2B|@+M&zc1hf+Rc&=#8Px^_)1(y~1P?ukKIZG!fIPgQ84Q-4ug>a)PMZWn+M>i+XGucwU%PF5dTw1x1oT(?;6|$yr)Y z^qSLVq$&|Gt-!`o#)+Q6*2y6ivL!^=xiDh_sH;O!WlE%+OhFI_ZM5Aif^LA3-L^ZW zR67>!4uvU_p0IXBi!q@5q|xk<-L9SEg}QE{k^r@+j;~ZYy3p!0DZ|Ej z-|kWnFyfoQvAt{KWQd38pjHGeP3jaJxB3x;HGwCwLMA%2S;EvLtkl{iJ0?kPW$|3` z<2nRkMm0%|ClK$8sMJmBl+o^CoG^4ayoJ&_(~0GFS=#Zp^H;DxYz^X3eK0DGiH0er z1Os5ocB0qSVID#be%g z0*eLS0k%oysFpv;!$zvtGg5Wf>2eJ|P6JnzR%lGsDYNtEi!sb*w`ao%xm9)vK}^&% zwlC+ZW8BU1$&{nheha|El^aG<*?DY(LZq3j&}DK1hx8hpq}ZYd2YQlD;mM++rDz*f zJB-nmnU%%C09pohJR?yoso5mnPDB78(XUIQT;M57EsYW*L#Z=-9!*uzJ8Fbs zU_*oF06fsc3IOfzO#CKN6INcf7(>q%oAm%eM&cN)1m#?D+OZ(77u5?xfvjabs5~0S zrD959R{ncIq2W@p*jKaZMB2{<843f_%7N#tlK0X|vrdt$$B#wFY~*&|yx1giEh3|$ z->RVjN}x(|jFLn?-OF;*G4Re1F^S0qs#7C7=;E>BcWO>&LX6pVd8F0wVl!kpeE|Aq z))EyZR#i>ugEj}OT4$J@#zLqjNy5BkX2+dEv)pFPcF!Laz-w^i#Ek(uO37Hy=R>yW z#C!i~@mld20FIyT7!A40fesL(M7gabplZ7;is&txJ0dK^YE)z-Wwr;32LRxJTM`%Y06i-W z`Y(*V8i;J^12GF18;C6&dfS4cQj=Qa?4X^4I%8`{OvmlSm>~uXfa1qRFX6Td4I1fm zl_7fIEgKYRc)*61SsbK1&@&*#IY=ZW_$VR@JxdJS9tc)7)w-cdyRTeO@O6;VtN`TN zVF#yuG-2puIiqL^*E3|MRhf3$eJw5dQwchF%Yq_bRYBSi5756;%7~H~zn!A0DnEh; zaRT51m)U9{CaML&M0YNHKEBFj5?y7AL~5^^9)x1Kp-wZg>@;$$m@M{2u?_YqgEE+K zV7&N10z8dGXwUO?JU=e@O^=k^ZqXnHR*Px|ZO|qIJXwpCKt-hke{uMJJJbsrtatGb z7E^D+xdDf{VIAjc9arsF?QW~u!-q;YU`44^uGCP7OrUrYU-`y@V*7A*hd*@qPk&YO z7$*YIh@&+sGuF7H_eG_eKt@B!(i2QxkxROc6o(j(gerdcdq?hdQ4eQ7Du_%Y99V9p znMx3NiXWRPJyFc1*&qt*{FIfDUXez7gmBO}^7$Pvx{DvsFM)jV3F@dzxqQoxY#d>c zCh&j<&9MnpG{pok4IW{S`*q%>f==$hH-Q7-N52F?e#`RLcLf%k#HZ0Fl+lODAw`?H zq~i`of?X-p5=^OORPdTffs?{z>=v^>H>>~uogMGoaqJDp%18hG=*y30k9_;cA05FD zfBEqBhZBcxJ#@{Xs}KI|LI2==5B${u?ZDyv@7Ukozq0R*`}n>8vG)~wsXgD>^TIvE z?yu~A_U=dT`qZwc?Rv=0zuOt?yaMu zO3K$jQ+R-^svD-5$%$svc3rR9VZfE21!1X*w_tp$zILeV4Q_tQe>qn??lWI|euWYr z``JI9UiP!=Kl<{-Kl=YEo_^q*aSVX`&2A$%<)JVFZA~;!H%TFZxrLn23WGB0+EGze z`V6xn@dxhFdH%b0fArth{_5?|oj&{PE63jR^57m%dC&8|Ht`<$nd4VoFP?tDobeL< za~2ZU@BJtDdhRRk`B3p^KYHy0Zi_$j-j{i!J~es&rylrZE4t+=$HdcD&KdjLw?K@8K^rxlir*+|h@Lr|&ms%=YtL&<`O~anedaVHJTYfv69SQf)Zl6O|khzyjxX z8GvQGkoaMjJrR1{(;w%4;f;^G@d?RqV}Jd>kG$f#H~ixV|KqKdWb~2W|C)IEiaFzW z`@yjV;}5*+8-GUL`eFVnuL*BF#s0;+|HgRj%hum=@SD&5^lQGqcJwiy6;I!H&RAr) z*s6MDV_d7Ec$2UPP>Y3nx(N>xc*Ua4t`NhJ2I3eS64##r{n7n*)Gq&{uYc&47k%u$ z4=+6U#wT9){h#iA{yU-1&~N)OkoZ1x#zLbP4EwYMR+(VOaVDGP(itHPLC>CqQ>4NK z5Ab?>Aa?9<3yI0XqyGG|mt?Q`y@x+wW&cASyXW9{KKsV&Uh=B%-1fcyN*GZRIL>?s$th+(-o;Z;_8at_MM?>ZK!nTs|Q~D@zgWhSE$$g z??3s>i*KPUH*?MJUH|{W=?CjCeQQ(}PhUP~+(T=%ikE0YC7ks}Jza#0^?}kzN;#>L z!I+ZMg{TrgR(x-R@iU(J!pGhCqJMbbf!BQN!OiN)Kl>2&)cmo1uQ~M6TXy}<}vyhm} z)qj5TtKToCh>v~e$6vkW%O4}Y?#6#uxc8gCaI1Lwo^!^u77t=s$xRDZ6X&Wm7lL4y zgL_$5oi=lNF{SZFHi~`FQfa~Xe&79_zkb8+k52CM{SQ`O$)#TYv8Nu42QB*VzVY?f z9Q@fMU;laW^ks9#`AUE!D58THFyRbYpe1RxpG6EWKZemRN){xtid0pR384k!%m4E) zZhQQRFD0(M`TulZ?*8pF6JLAPlSW_ts~r#g{mZ_1-^SIqh^Ox{XRJ^Kq+GG_ga`+) z+~c~{TCZbgOSQIc09gEhb)l&pcdF?czF_RbKS6)`w4XlN{L~u{JD0!e=auI&FaFve z9C_2ju0>va?34etE}lL%XPhLP4Ktsk)P!szS+S-%q2-x)y4Y)^puDcag`A4hV8;m? z+wGtBoO+(HEB=?)e&M0j$o%KmJpQqd+C%(9^NY6z5B$FS(WmbcPamB#?oYsbG#(M9 z0ji;1pKu(n>h~k5-OS5cqnxY69SZk@7KLn_70+J#z-JEK^5~C0=xaAzd$W1j>#zJL zO>R%J)Z>qR;DwOl#&SH#nY=Zu50 z<<{Dr*o6yW!M3@tBD9G8KVTqvZ+xt5{GmhTuQ)Y>sgFH?10q6t{nMd zf6VC+a0KP7g~VU~>`xwg)rY@)@WbUtd}`W$#7%$b{NKM8oHzfg_<+(0HurW$JbiG^ zn2U`FOISvIq)Hsrr5_y{OT*ZaDA){_+9*;TPHGlPUzVOQYcaDRD{=l5^C`GV=BohJTVbKIQc!EjEO*?_A5^iad zgwnNKZ9)o2AKF-ozkDV0C)eNdX7(}fTRr^3w>CezNTIa*DoalK3N^+crQMQR|E-3HWbfIcgd5KT9hLcdq%srQgB`{dr( z{n@o||2Y4hFWtNH-TvNNKdC*kzV{yQBgE7D7K|O$%tCxxacE;gb?gyX#HR<8Vzpo*5+h{KwoSIN*cCtelJN3lH}4!hZxM+_30Ws)@ zG$r|Bw?+yDJ{h+81n3~Cjogjxc0c^f`M>zttKNI?#{1j<@e%6^?2(Ok{{9mNfBx2! zfA#vuKIlEyiKlnY8F!4T)a})EB{Gwf)L?8Tm{C=5$8kEzv?Oj278tM6lqyDc;}H7! zYRldI!$kIpbOiQQc;G>>!y8 zjf8M9vO(y=ssIG=9%y7zGRkD5=H*axy0I&YulV9C+#kHmC$F6T`=frEd*r_!d&$F| z`tF|;Z}^AH*{9<-f?cs=&N$K58#--fV@Q*!F^rI~*Q42Rf)i3T>zl5s3k__5w~V^7 zSmU3*_|~WO|Kymqeth}2KXKpZq{UMw=8T6KfKHI5 z`#3XId&*=8`k_s$Lml%_qs7%4ad8?@H3UVv_6Fm3-YfV0kG}QFE8mg- z%zwW2h8M#Rh1MSPjN)}^(ADGAb#ul`fLJXge##G(TQZaU2mAv2nD>9#`}aAT*2)vmueDW1A^&Ugtho(1EtWS{uH?gMUS{*(RapMC2Mt55!~r+x4}(v9@- z=X~?M4@vZY`sd=Qr_C8J0V=X!EZzK#A3-mAX8-=z*jGRB$|wE7rz80xue;}-(ckzb z_r)jP_7d^bHFL&GfFmpzKk7f;ePZ8dPQU4ux4lRG#6#Z3lHb0f!@lbU*Z%0IhoALX z?hOwXPfh2Hmp~3!F#gF;_arFvh3fquu6+B^PmHg6>rU)}um0r8A71g6-QT+9UG$g5 zQVs${Z-gwex4LhjpvL7W*9fa4wtM8R#mTsa4wq%O^$8M zWjRg?^SJB>jbe%PEYR$Gc9}lmxqkb7AN?%4n)~saZ+*hEUi$oRd^;?`fB2yvyveM* z=KZfJh^OK?noJ8)gdr*+RtiUH>Fprx@2?92(N1*W<29DCzC)^2?3 zV}&n%{_^#I2eFCko`Z>Rdn5ktPe-3oKeF#lFL;c2Dw;E1qQ-b3asDH3Bu~9NIg}a? zzy7vA%Dv(8!|7Lj<i;eZPCmId`0D_7pU zLhtzV9iR{3u3tmP*)PEX@jr#-it;~Yrgzgk<(m%g)r5y1*|ECBGxB(@-xz01w`Peg zza9#9xVeSn2J-&`l=$J4gQyQ6|_(7S0r7zY94 zEb*4w_Bdy^aR49bzKxG`iPzP(hx)}IDRRz73PH04kZ;;MlLt(FiAUMC<=NQBM{n9Q zpY(lKmw2EZS3G*clK{)z&a}y3>dKp2du(eTC(oI5lCU*7(wrNw0|L!9@)Fnbg^{c_ z1lnRF-*_z;Z?=(_sK4I!cxP?o8=nTonQi1H%1gIB&bb@;#%t!E>Af#)r-^NRrYNx) zsbId5Z=BBL0aIV1UBtGhe*1xZ<78vfOF(yD81{Tylg2k~1rlg`6c;jP6*7w=GX^1ln?TWJLi`w9bt&7-^<4WXW#H zBhJdyi82N!iW!ar3C&!Bt%!QY$t3ecF5MBtiIya{J8>_w#JM$R* z-3gZ*IB`uRvWcV;6RDs!$nsfGiz=!W7gPx~^aKxY|0BIaW5SI|hb0}KE7!u7-x3!FX2Yp_BiM6SnP-+uj1TLFMtKD`5zbqVj_h0UHVuvgA%BLLwu=#DKxlDBNc zvV<`@W4w$7H~;%3>J{hcb$4nbu!IA2Yxuh@8?pSV@b8vrBY>QPjRarPo-CjbGl7GT zZ_A!6t+)^S-k>UT8DD|MVpY)A98f%H`k)xRFj%jlez{8b13>|hY_rW}^jvv33bjSo zMcY$kyYCH#kHK*O+4q>MuDM!Mlc^L;JVx$-mI2-jg=RQxXQ_VEmsMzg!%>;<4%)*tWQ`tEI_*-i-R)r*LiLMWM(vYOF@v{#oCRnA&3tpa zn5!D#bE=lwWIZ4m6Vmmw$~$RKJ|q@j_=UNCHB-^*2j6vh%GNj_%~7?mx#Ch#^~d` z^~x{S$Jupu3H1?0&a02*CNK2bR#k0``(4~popPOS=0Pf`f}#_DQq1FSo6%&R%`q2! zKaMK8hy^8(Zv#?^C!htcTN>Ie$fQtN1NcT>82J1c#_ftjOBE+GfbuOPpI7-Nrsw*V zAq`-ZXfEK|;W|KNBjs!l`(U75KoF)g`vnMXBSK+6VncYTpt; z>8-R6bfp{4?1NuDHuKDqyB!d4*3jnC=@LYw&x`+0*3*W!uA~ubm$v>ryr#Lw&v1l=7B?*GJ}}EcqFdmkjEo^@9H3}+ zYL?{g8k?3NxUsfI2xM_G;*BX4`SjY9^(w7FzC?u0)Q~3u4CrjdesTXlY&LbEOSAX? zgLZsy$Fb{<{_N=V$ZbcW!#_S89{T*ApinxsYH9uB%HV zQEx|rM=T^*g6rxM+1A^U;Nc4imf*U&L|XQCBzV|Df+e_K+Q9pXlMh`;utf6q>Js_b z7Y>=8IJvrzU5|T77{FxV{vKoBTk%r@IrzmvMw%d=ERAU4_ZjDL_WsqB~7R}aq@u+36|h`X;Uap zoP5ASf+cb$R@>XpQ@?NoJr)Z z7k=WNIC=R(f+e_K+TPM9PTp%F!4k>-t4rkXZ)XkexsYH9uB%HV`fo>q%N7zW!F6?s zih%7%aF2xqOK@Giq`|=xCyy;8Sc2=NjfR{!d2}Jc5;2z5OX~kUaq`GQf+e_KT9?;6 z{=Wm-arpZE_uchI@XZCjt{=X%cYB`>olu7wl*CP)L0B41m`EB?hdo~(wj{y7Jng2)|@GZ{h3RZM|)J#tFB)Ex;!&&|_bv@<)qYttPmLU%)-4g^3m!T78L&z3Q$+x_~e@#`6qQp6O8+hqy8Xg?jTU~ynHjJ|IBG14fiWY*8%(=r-D z)dF4DY);F$#zd(bL5{7CYXg}pl!tck?`~99R{iaRHytPWoEnJ5N?ug~a&xi5mp0`-ZPL5hGHmLC9EK?Kd1Op=`I(FDMc_S%%hl5pRxWt# z5ycjoO-H4=UP1A;*`Yy8x0xE;NspFgu&EFA*%@dvfzI}2UvQpP=5Ta!iu z&}DF*da!lP1cu&;!(*<3A!zQ>ozZqwO_xXZ5b&5KUTBf#4i^kJ(!gBV(ZDZLv)^v~ z=$v`a*`4{_owE%AF&Oa7UnsKcxdup`=*YGq&&E2N_oD0J{D(6c7xHWpa?!BojNo(n zuI%2<$}$w|_U}RKEm@h(ZvICbjTpgPXUuPp0wCb~J4XR7OFWROaRUyYU+Hm!rq3JE z?^;m?AR;Po2DV^llBv-s33}_%NTtXiEvmoxB*k2Mf9VWs`FRHWMy@UEB?}1H(TbnEo0q0`Su&>j{EqDSFbM1 zVAJujNMgU6?3BgexA#My+5a;^RAb=V@?^urxO!$7030)z0QN$%1LJTSf)c=jnf>W6 z?1nt+x_5yKeB0&`V0BTsZD9MsNO~s8mhTE&5IGI2p&mXet+fYQo{=(XHCY&82Hyh+ z1~BYr!!=HtKo_5DiqQrD?9|FND8qq98R@mK=2ir&Ph`;bqCQeGwG!s_b6k<8N`A)E zOMX+!1CV2cpi;?H!>i|Mfnc(TY082`@Tp-Xt8DY4pG|!u{=a)?yyLL9|4Vy*0KWO{ z*Kb$gm%jqnN2hn*e&8?RjlXcL=bYy(D0%_fedp(FG7W8+K)FQa&(_b=Wf^Siz`tMx z^X#;}kc;QeMNc!Tvmd4xf8Yb&zq|u~Yr!w-z)#{>nt;x0Y6T;d%U1khFs?--WHMCO z+&*pq$jlrRrMPZt=(|j*nQN>!y^A)rtdKVQh}KWBu?+x|bdrj&WREl~mInBx)n>C- zT0^tN#>BB@2(+UdB=gxm(sYWDK9HyiRb){MuQu1+emR0p3_;mw6U%2YTg#flUo>1vmb5JjztaKY^R;=w)qul$9%{LxL*u!=C!yQovTl% z$wM!k{d;R`WsA)_GtzH&W6ZYV1+1XiFc-bqA?Uo7G?l(Zuhj;2J1(&ON>muK64M^# z1q<|um;_D{OU1RcVPf^NaM7Ec&ni>4!9i?eeUd47sfJEW#cnZamZn**Wi_w~-__Ft zMu>T|T9#YAu$EnOLaek7D{J_a>*3ujGDthwX_gu0vUQ(uDBu{}PIe=(L+6z=M^5j! zqqJ>_nCpe(XXos8fF%Gvrn5@h;Q990;^M&w;x{sPc!_B7R=L9tsQs6F+GgGTH7so- z=DYn41)XIF-&sR1Rv}XaIx|SNd~Wnh6-tSa^=T=?PQpy9+wW=XX44rVekHwTXQbj< zag;6k>xy*I3Mux$YqheFjEtAT}yzd)4Evjt+t=HF^U3HSwYNXi> zdoneeHjuJhLC}2n|6}i4;9bYcdiQ&G@9xLQb9l}S9AMx~IZ2zg2_ld-Nt-^>N1DE1 zC{5BPX_`lqwn+;EjH2Kbh%gdpIN9`u=0OL5kM%(K}9#>ph8z=g-%oTd+Yifw>WWg$K|X5?B>7ZPWUF&xP(a;^xr zgJE##$It&)ujng>Pd#}1!Tt8ly$|m_9Qgi!Z1?(|=lQyFcC`*f%9jaBUz zzkm|nH}-(L+NGU5a=ZD_N7s5hLo=T1%XrIQ;@ItUrt2O@Cb}@mYmXjsVnq6ui7>c>q{g?b(>eM)c8_?c z_x;FsFU;}bM-TQmK62KP+snit9CP$S;!bC{?Qy&2KPTTmxGRPyv+CV^?a_ly^y1bs zF$zoe;+DtmVUL}B|Kq|OZ#cTz3#omVW!s}-T%ZWU0WuCV#!fDS$(c*dK|C%$if^SessT+tIrZyDvv21uRf>V zaF5&GC%o_d>lbEt?a_Tt^rCN>9G4gO;;AphCNt*NpLra8$X#(fneg)FhacU?<9N}G zxkQ}EF~`Y_c`C!njJbK>G2Fb*T`@eF#0$)rKXIZLx0Z<`S+W-=Gv?YOJdSJsb777* z99`vcylBQ;A|~XR<0WRyQ~T7LF>m{R+WY=Na$&w_jt)J(7tNSUM35ZwotrUF<>}3s zw|)0tyzlS5ePN!DK05GtUNmDa5odDD^P(B^RHojHdE38n-uHLjwJ_6bkM>WD(zRux zT$UWA-i&$MH{auZf9v%NbA0&G-pLtriIc5kipMkNsom&t`^TqDzF)pD!|RUxUN6pV zgs(cY#QWVldU3iDKJ`81RUW;+yWiyd;g=?P=X|nYx4wIVGJj;?i^yvNd_r340 zuP#b*cjsX%)s?TV`2NNBk;BKX9fBcNo|LyvF)_-|jT7TlfXAfR?aMMBY;4%B(-~Z#iSMQDXn7v2( zzw7_x-lx~jueH{m;CqGtRer~R-R^gGKfe3Q-N9~Z_nMt=@BGouukP6EKe6`pwRdg& z{q}=4-@k46Gl!+sudV+6>Mw2m)7A&JZr##+Js;(J$o4n4KeYYw!{=xai}6xPA^=9ZTh)`4=>vEvj-nqwCNuoeBijL zB@-!Ls}8F%Wt=t?C4GXc|4V!Oi#Gkj-tMAJZ`<2BZkn+3TBXVxZoS^mb#1qHlE8=j z-#KpDlDIrkkCUVplWiuIDxPfmZ2y}UZF-acw-;^tEdLvin}QO_v6f5K%wb9gr$lo7 z1OeaGzSyEoALzpuZTbM;Q;wTfk|Vt20C%0SZuLsa=wxKe=F>L$MVmf#v$<%~`sOnh zZCcxGEZVfX`SeAbRyJ=uZpw~hwql9-O0r+>G^OgvdIak)UH_Fuo4#cI<%>2wT7TK1 zO>bGh^|h5b7ZQ9xWjYXR( zyRTWasl5B@X;b;s;qqV~ar~>N4wpaKTR;BQQ}5QF?rkl~wYPgb6Q16$=I$F7{Z(W4 zw-#-x@BZeZO|{+EFWR)b`?_gU`P3EWIsP{vH$8RT`hfr2i#Gj3zgIoGy!b|A(U!*q zP90hI_dR*hruXwb>A0!XsX30*siYXmvBtT+abnI|f5y7FXw$~JuxQh#uiw7Jol4-w z_1l-YQ=2|*{q`m9)TU2ezkP{2wP}6*_9gDrrnU9km$*}#R@a|)+*zj@_viPv7wy-| z`coHeT3)XoH?0qxmfod^rb*{xxlvm=F$T8AThCpz>1gXYi#Bz)p1o*OXX~a#n+~_0 zwP@49)-#Wro;qbb(|w)KQ9&LK==`VcX>Xl8wb&3kdq)-&EC8C9Ptt9C2 zoE{Cv&vx?UWjh9H#j;-NChCw{tm~bu-96hJ%9T>T*E(lqD&?w^3UzG6)v_*EL7j2j zWDUM+BnzS&OXll54>?*nf(x~?ioiJ`36Zp(9a|_)WOC`iG4SX5M;ns}t-mElE<^AVF)p){(jSol-a)_%YB2|VR}n6^vj7Gsi zI%zTes+%p1H8Ij}7CWi4oxZ5i9js>zlGTpnQoTek$L1tB*F~D=Y7$Ro$)cN#6yqWb zO2aG?vH~JjATJ>I1?thdXLnwrpCtyKdAP&`n|+&$R=S-;m1{~dj_>D+ zEGl)oN|3wkWj^nYVo)!Gnhe{Do;{Z{9qBBW2t(s)Tk6@No>{F}5KG6x{kQ;W1k5{D zk+!%XuL{XbA~$9jIX23s6hera&5T?vR>#pe2-x$23{T1b?_D~WZgvm$S28OfT3LJK z{ulROzwhjm`}f`Z%F2iK-n4hqUV86A{=f0R&Hpp2kM$S)*Q{~7|G4|E-JjpBtiE^m zQ9J*-_Wadf+-FDVzj-~k{;;)wSo{6eZykQ>@D+!h zL-gQP2j=0{!RHow5P{cQYiEjarqYV$OBHP#Y}ybK2!&K7m2ano&_He}iY0Uh(OlB* z1?ht~O(fW0V^G73T}~!KY=A?cUZGbq#?fd!0Me9|uzuRELpWIuw`&L5R7a*|6pbR> z=W=PeOawcv!ceVu(Hv@bLu8IBX$6F0CtNY~QW&!{{dheh?0sf>9i_M|x^Sn2 zg^(1;Sre&`JM|Eiv1G~0M3a0EZza%Jkb#2s&J(5*1uHVfViB{GLE7~p%@TE5ss_be z%LtW9$+Vs7P?lVaHu~AMPfaD5K&hY6I-)=qYnoDMarJ6LMdVZ{t!2zipJWZO)X|*b zh}{1AR3fd_#=Q_b#xz*%mW@CqQ>~McVPwqed==#50_l88ZUYHbvn$^_mS{CnPP5^b zqb<_G6G3XE%Si;X$|UAy1yxHaog~pH^o+vx>!uPZ(-GrgN5ES`+vd@9W#DG*7#`%4 zZmNrdP+O%PD<*7-#5aml399KL99w4E-8>Z*#;r<1#wblSQ~iFq&@K(4@{q97bZS6v zoS#aNZXeHCwQ8mm$|tdYFbFqryx9nL48rbIN1QzvMdWBT6chG_QwhTD)$653Lob(X z2(}w}kk=#-9WjG^P32IlW!ibGH&k*1di`@#iTFTJy_{vP8f?V{y;tBGLn+=cVjWNs zI>TqOEvFhQ#p9&3_48ASn1(i@%AnBg!B(SDXh$SdVj(yX2~Y*4>ayuts2mC?xPY!7 z-w#Z2hEYCJ6vl!qM~H!=z$9ksxF|z6_|^QWabau$5M=nZ)Wz zmNZ62DqPLjl}t_vd~VS^`Jyk#}UMrzCj2?@#9yTVAc zBbYLZ+Uxh3N(6F5urzE-5RQ&j0)_))Bf@|X*+NF4(nYW`Ib=mnGc;#xefL-b?d15l zBz2IKh-atXIsH`t8B%rRwIj2 zOsXC(R%J7wh)}zr`rkd(0TK=r>TEcx7wcuVl?bFL%5)1^CPGMd%Px*GQYRQl+%k?Zhl2R3V*s2sO`1W5;B`irDu(A4(!^Uue z%n#G(P>ZGV!%mFiY@FnadY#Zq#>U5| z5-O&l5MAYkPRp*_F)`KXM@MKgUP`E$M65$FnJNzx9H-L$M@}WAR5?K^(S&Sad7~81 zvW-q2>Q_=`QIA{g6oveMuxTPD+(T-pEGNT6N6F>xpK!mZXxSLc2Q52SWqYH=U`A zVx)yagKDl1GDZzY(Xk$@7vwzCYl2+~IX0DXHB_p1b9F=T-*>7*Xo+>YP0(^B$0$ij zlUv##oHATIlg!7Yd|VtjopiH?_Y14PFqPn&oYid_WKMKq^<1YA(+7iE$pzC9!Jk%Wb3Sx;gkx_}oAhy8y^lD)Y9g{Zo!g658x4o^1>H{f>Y;K^if22frH~nob4*HYtF>?? zP;J9G8C{J{b+9Vcw9^jWrCXAX_A1)2r8+{R)E--4q3RpuV55olxFqVV{nN2TOLwDK zGZp4cr0(`TZ@`Yh-^Z1_A4XdOT`)Wy$@sym>yAqAXlhpwnrCJDuLy{XVMRKL60d0hj zb+A^D?iH#fJ3*CVMqymhNiq@0hiVCPm`KuPBN!AFv0kP(rmI1b4&jaPkTAzXmFu`h zhH@JEI7RlGVcL|?P%_8Xa&)t(4W;d0o9ZC-VMt^OBPwdp!$FZ{ODUtA#F}QpMG{Qj z$}$O6Oi>vZS(~m=McjhILSQgT^NDaU8qbZ%LZ=lAg$gc`&Y9IzUn&GS7wz!m%5*(1 z;`un+AJmEk9TZh-SfO!AO^Fe!6JUnTw%n)HQgNJva%>yl{nnHfs>R4`Fsuv&p%f&d zI<2VL0MoNYDir}~Ow3*zsbV9%!%3@;o=QZEVxy3;3W0o~NhYIhI04eO20Ek6&_-*C zGL|=sr5>#}Mzrt6QwapbcV{G{n{u2;t}@a_EiRx{!a_zQx&yn8OA&{bo3)C?t*%WZ z3Zqmv6XlXMvkaLMLz@Y%&g9!$q=W}KNvLL&dc_v-vC*+gZmJNf7JDfK13NG2@Ty}8yWS`TI^{|^Rvy`Sxl@j} z^oTU1WHZ_$yP1K-6y#!^I)O*QPPwY@ev4bl$h_+%WC@Tl`rWzjCW2*1d zQ;9O6YCM{ZflYC##!5|{X(lzKL_00j)U2YTjG7M9Q}d3r{ZjAz|Js#TtsK7i@cM(d z9Xx7Z*4W-zj&E=KW!u56S8atiKeSoj_?wO1`d8M+Yk$9X z)7pJje{D4ge&8Pa+`M;TcYCeym>XLwyKld)NRW8d>QxKMcp%3)Q044|W6K>;6JGf6 zrK2Fi5OzZ7OkR?Nl4WTuZ&dAW31m~Q^<_L~(}A3xj3x`JV?l{N4~N)n=fV!46WRxK z-nuXyX#hfn8%;p~iEz%il#?yyjj^knEddjNI)46_)7*eWmt(R6qYOg;I+63Ar z*qEYXvD!{7i|bBYr$ zY|?=Y|JK5ECVA&ucGmz&=w-w1KLJG{=!~w7#|55{%U!nCBtZVW zj2a+P_^8*Zx}fHQK&!Tt6A-IaLd*-RKlaozXGd_NX2pZ4p%6A(AjfVk-)}3Oa*Ql@ zhIL&xiVd32>hVxX$Q#=E=KxihH)03VYuX>jlu}f5>mCuiX%E^fR^@UZ=jo2kh&vwp_ zfU3y|dga2ZE*e2gRG9@M$n|cSw}be#g|*%N2s)WDXqlWl;(5oTnA5Jg3wfiflCxBRJ<8(43 z%`%BymPua%ZrR}WTlTVhiu)49Sic*7k0{wMPqUyL)RqnLTHj93Es|2nt{ z#P2_I?|XYA5WDYySp5Nr(YHZnfZp~uw+#@FuWf!~Q{DLc4F$yhOCV~$1tRqYkUXGy zHxK{MeobPd)haBL}UV**rK7hcal&7)b3hoG#e{7p#%Bd@Mp|G#(-{Iae?Z5&98}@VGF~eT$LccAOQ-MJpZywmX#z|5 z;4eY|TAIKTj}?m$I5#1%#ADwwdGZz^@c5+(Eb$b#2!X)T1g`KHxM67mS9lDZoe)^! zIs8hGfyXUP;0oV@$1Y9air#`r{Qp{NW#@0Ueg*t-@8{kgxXd1SLH5W80@s8WJ!*BC z_mHR0lh1sF0lb5iNnmli*|l3BxaAU+T%u6)Qk7hmu$D>W+7jkHX}`q!HHo~Qk9MBE z)V{sOYPZ!Kwk`^`yvy1o%}lX}x52qOq!c5etc#&CICCzY)gtF2VYBa2Sq|;g;BF0? zh9CoPYzY)_1tFdDN9aM!yMpPzH9%>3P~NODDJ2hwK{O%)!k|OpNf`7b;B&sP?GlNf zXYuNjAW^{Ksb1XcJy62!`B?iM`7<)pqkrQ#l>vTNjG8tJtS5+iD+Toi0EQQKq)y$c)+)CB@qLa*kDl@E- zs`vVpL>Hr?T^OUYz=^n>K!*L)xKKw%nIvyk)Rx+@Q*tdGpVoUmaX&puGyR-PCp`Vh z7$^&`VHLa;r>t{g#SAB7HK(A8h7;EFELMuEW!@G?@!nvZAgdjs({xm!Cy-|cqa3Yw zip7)=8g|_%k*P8TxN79u;$20P1AUlRH4DUgbeq84sd4*lf5b97Xb{4-{&UHr?MVds z!@z_BqLJcIE-GjVWkgV!8X@5^k`BgPrLUR6l-fgqFQlw+9deNgrEjP$5cwejvUigT zBJ!h{PnHdQy_0P&J0YDI>0X9*Z=Bx)26{4MyQgMuI-IYJ=tX7TIn4ah(-<&2(E4tt zG5D^d%5_tA1Txk=7jF*3sYuZ+aM^PuxEbyOw@q_Y&jB}CiYN}dZ1%FlJV_?(4$sm} z9s~yV(?a)by)+t%IB*Ovp*4|gV=b|hiN@G2Q&;;oD5lLudIDsb@pxWr)saYO$jfO? z9V&RgU8jceYDtYRQs&v4!NH{iTYBOBkG4EpdP#J388f_cwiNzR*wPY@Yu*rA=GkJl ztj({BOFYP2DhB zv^(g+EMn&f(zR@?Qqd!DCaZ?q&H$r(usp)jAUKTRR3wSj&XvtzyM8X%4%Ksc0vn5H zKVjMyigF`}LyT(Y_}bYTS)%kBPKl$l6+BZKvu&kmI%f+_ zod>HtHp)k{kWjWogH4XHx{||kayG^_LnKNSdX?HaT~ZQ3e6b7{$6Y@iT8+sNzkPG? zRFD;BWVXD`%CYCe?CjhdSr-ix?=R-ojYr(k)Dxe}S#lXQvB4U2CnhtiyokkJ?IurI zug`nwcZF82JiX*x)p2Xuy^P`c-FyHApbwI_nN&2_@585_5bqj zqj%oDlL8R{-1gsXkGHSd`qiz+ZoYgI*?7$cw*Kk$8`r+I#;<;MRa^NB@DmGulnafm ziF^Z)igUTm+GSbcd3r`2ifajm@(+ew`-tW7(y)blw;hAAY>_fG7!yVhDJdIaV zazz}_nQR!ROLmK|Rpfxtgmauw$At?wdTjPvzE3U8=GfDSwn4nW2tD5M`4VvnU>5Zom>{sdFnBpPux>6)9zJ=9YWUtGFZ1^uQK$&N+?_F5g-4ER#CSpUlkoDMrOaFs~*&N&J z5&;{=g$&T*jn(&@?h$vqueronT_RpXx{wAMy|Mc4g*DEN)g{6;lnW`K$s0WHT3FLX zgJ%ifN3dU@z1sx_&pQ`ZcK3tlhY8jYE+joR;L?A8VK&EuX9*ulaM6k5i+;!Hu51fA zhyO648h{RUOy~FRDLSl_Nr+Z3G~`OfMz+ip(+Wr)#bh0XEa0I=r)S{J{75Xqc@)qg zkLkSqo}#maU!rk=0Bk%{*;y|Nk>2FsuC9cK4S4!vul|7oOs=0hj(e3$r=4^d+2=z(qg#_@du@`s>w& zEdAv9)iTGgyPL0HAy)olef3}0YFp;kleYFY|9qOqZBvcg}AAV1KxJc&7to< zRzL>9y)XOUx=(C>YWo$(Z1y+52ne43+3u_aPj5BfVv;Jsb=fusrG`j^L88GFtwlm` zA_kI6S-n=X$)N&fQ6z?F`LKF0&AQ@ReK=Lu3;3WbP_Do#3?D@B5TqFHh@|Q{kk2U~ zsi~GcsQISZW89vVXo`*cs8*-q{g`SBHd^c}VbLr`v_UD-fGAm1$bP7c`j2(gDP$B^ zL1L6(Pf1m|OtHh$9EWnu@>1-Mk38%;9g^}AOc*;^c6{zTfL>Tels0o?nVcVpu1~1wM zo3R;m<0ZQPv(f%hLYv~S( z!>C*9r#U=VcCk!09=Ai5oK3)qa)ZEYAx%w-l}<_8R;M~T2^uPsE;quPu}Bz7^A-iE zAj4h)qak7lUO{3J78_P*CcAV0sRZZZnu?c6M`BT|H&C!FFT}K<8E{y=PIq!>o`E5$ zT4xO3G~LH$&NRkIS5N1h3foZX zT2z)AazEZ_LjnUrol)QKPbCs|LT>~}L)J%lHkltc$y94Z6-ULg8jngNkmN)yV`H7` z7~2-K-U4a4DWQ=L+oBGVvm;V6f}7o_SaK_4IZV-gR0z>zfpC)yhXXIli9pN*uak?R zD8l7N`6L_}CPB`mz#tv2i9A6^xnL4xYgEg43HhlhCv2RC!Xy+7wjy>EWLQbsMGh&9 zLSW={dw34qb~QQxDF@ow&8JT#qV}+n($Z251Igu_a;HE*AVH7@(P*Dy2R5H&Xp{=Z za6|PSkE&>Nh)NhV_nVi&Qur-@qJ5Lxau^L))pYJIBEx3f}8663kB zlL-bpj@W3ARg7b6N*Ue!;IymYrrbwHNSo>Ahh0aJN6r!EUpi8(=M zD4XWO6zjw)%ur2NRF+bB&g>NVUad~2p@U!F1~VKyKYyKyCpsL5&QqD8C^|yXEZ7pn z#F!wWRH_|dT&*fk4F|D$1c?eI-2d0d5`;6TI)fBQPK2kj-DJ|L*s)qJY>XfaN2N%j zA2)gxs-qffp<@Xw*={xKZm>Tt;!2!j;*_qA&_+C?E17Jwksen{Y$@Ev$(`$_5<}WC z;=y7X5~vc$+E-NLwvgz`xYNr-0%kK7mJDk!V3mfw`Id=iRTQ(h~V7cBh&4+|f82NjO|J@8k=qjDmDwE3YLoTB9APC3_fcV2wt{4Cctf%5P1t z!{Os(J;%XyJ(ogj2pgvQnM|>52WboE11iH;WN$gCc5z=?zqOxxvQIA+B7F&C?pl5#GQ z6VZ$ta1xNcnVU+;Qp<{`89_>u)gBs(fGmp+<#q%k%vOeIF($PFtw2q#v+Bk)W1uS) z!%c7jOaYamX{S&i;9FHWWT*RWVRGBa9 zG@QwWq@FdfSO%nYv*_Tc8>2O3RhiC+%}Toti^X)_?j~Jx5b9O313pJ|2`*6Ow7llV z5Uhe~d`x}fR7Iqe=to331>2Rl!6s0a>b66&kZ+E;L_q2fC`$*)IlC3pI9#7fa6%%* z05u(?BW-3ucl2yIe zFSr?}gx7`~J!m)5AcHLJSQbcK%V)zJ%IZlmB&{7xC9H~BuMH|e1s~IW9v^3V_&BCV z2OK|+Iy!-Y9xp3c#w<#^*X*S(Z?iiX;E;x>O5ASOcmkp5Y~mbP)ryGF7WyHwgr>}q zTuyK_vOArp-L588ZEo1=l6X{g(avTplZQ zoQjOhz(A^*Vu9idaW|Ijb%WtlI0o`*7mK4nN*ifX(uEF=dpF$%3G#+yhM=L68$}v+ zvh6h8UL=5wnOKOZIhnB-%j7CUW4AQz-Eldqc3?$|js&wrmbG+%ZMkvAEf-T#CdA8` zI)c=*P?0R`vWK5q5%1|~c*?35bCbDPJ=5$GSgRD34I-7q)VS14kVPd9JTg(D;PxD% zM(*KL3BEQg7aBI1t@mp|-Zi>oA*x66tjKV56OWBaHzlBz9MN|6uAfRMs+7tMs*skb zCu5~F3WMC<6{KCmN{Sur6I2BO38AEc19N-6sf4YoXu4Xo4L1@hW}JYQFbNV1Vx3+& z9jUpdV#v`rm{>d9et#;_E_9t%gc*PoNP>;^M&bYl4=6D!o(m01)lfF0)yZJ41BvL~ ze@!LSFk-V~NR30`a8KZ4Skq|g_^+iO5`nPwR=QYQ|aN;CNe!4uONL~7h?0RMq9YZgL#~6w zifXr#Rn#in%BuM8i;h|0En*~(Lm?V$z8YjAMJEO{28mI(VMNJF-bE=QP}L%Bbl09r zDA8C8cx%MR79!*!C*`(k5nfD03h55W0R{_>77hk7oC@y8Yo`+3Oa+MBq=g2GHoyx2SamDY*EOz_0*#%eXShGBuSUWzUO|C(SFhA-vHx5R5d z&)sF2aK1_VB|-%+?V7g?LC-<`ZrzdQ-SfIl4&=$V$6V8hn{{JEo=mCd@ju-?=j5A9 zPihe-C$VMrWpHEe;lfP7k@PMLqCq(%=mrg<8`0z{RxAejPO+CCTdk^lE@Pjq)up6T zO5=M`pJVlZ7fyMr!e zcT2o^zA|>StRbGTqxrJ}kI;|6l1?wobSGQ9w=ff!VW4>j{Sdf!cXrUf$U)M(y_f5y z8;OMOGWh8%GRRP7*>-Tt84aOylCe>%XeVjz-5QVxq@Rk zL9jg?<(p;%ww!aBa#gG5`DnA86$x=S;H4}m1hGhArl@!6Ui8cGXm_&Ak5;d2^#6j(JcRwQ(~Y-^-YGg|nfdAx z%kcE{??*IoOqJgruIHVvdPElU+zok6@76!wt9y8gMcsu@Ds-p$3jEY%TOI^?JHJc% z$Je8f6e__)mhXyYzcvsD6dJR;*_b^ZpUpNS=SsOCS~_OaknLb?=*OC0&=(SYQ3R&icxrKE_bkpA@nYTK^$suu4q{5IaKPU zXtH1Ewc)XBgiuNepNlc|k({qqvZaiYpGIGRBgCZxLq{gbGDgWhcdj&o;|?}1R61qk zEM`DsT}VeP3smz+ob5Jis*E8OhfE5Nq;#@Ht}Yg%Nn;@QM^^Tdzx&XgzukH3&U1G%J6D4^fj9e}aMEAR;wNY=UQ7 z^++h$K`Sn(!BZ%dLB$@__jULv?#XB_#q_j@lHi%1Bc{x8i)ZR$Z&;{>K}KpB&M|iNVPVt_G8NLLl`n2(oS1 zp3F~qGNA+&F|3|#VKSO&*Fn9&QKVHJszDBd9iyh^(xIpv&`d-jJ((wZG7MJCK;fz( z#mb-ni;A@^EP^#FnGQ36MUAhev)v%xl=*CYay>uk$#ipKp22Cd*2 zR6}Zx$`y)9o3%31V3J`i%d`Mv@lg*+LqZ-ATu z1xavCG?JG~0)^gwu=|?MB#o1Fh+9pe6af3?BtOnGx`Ls1dD7w1STKh+FG4 zP}wk|F)SY$Xn_FZAWF_`Gf6Xx`2G?;n!E+&qtyWysW1?#RD?t(oRZzVtUHAol))QT zSmQ^WgpW&Ns5X%S2UcrmvU%Ew=SulZS{-!C`5Zlf*)eI9rK(&JB$6y3DR6L12nm7o z{crf_oF~JV2T>^3wqsVkucE1sjK#)DAUfQ^I{FVaNaGCALu;G@TTGSyTv zIO=3Aa})_lqHX8dnAQt1MJFgR`4*_W9)|3IVU%^L;K>9$88yTc0}E@?iF{B^_0w5g zq{G=n2Vwi6cB~z4PbSl<2a-V$mSbBkQ^dpRC|$s7qd1OtoqkzVNi0oeBXr)3 zBfc-dN00MlnqrwQhAiOL(yn#-J-ZRF^Fpo(2g^{G?1uWG7&3CAl_1|M_-=!b9_z`( zo8UZ6V2Dml>$~}S0K9r>QdNo_#@t-is#@`W1SHq5gu6z~_h<0Y^`1=C2ugK@lA@qU zHlmjt#L9L1rih5SP_Y^ohxur}$EvLqX-mG(!$*(tWaNB1=p^gCOfd|#BXq}b%N*Jj zYkEcxCSWsGtZ@MmBt(~+jwkbIPo`zpqHGt`%N>pp7~FOz%(Rk@oDCQOY{Wryqz4TY zK2s@5S>Na2qepo%%|g?l2H2oJ0`)b*z{ds5-HixGH1H^)n03g8qPj#xgLq!`WX^ap zWtJ@~su>L_z=5X2H^yxc-6o?cECwn-Y3X1Liw_bJh~#C%_gVPpk)8};w`!&Yw;*aj*O9bMzeED@&W`xJci za8D*3ra4dzvER-?{WOxZtA<^VMiQW^Yo_B^99J(6NdeYI1KRO@5 z!Jf<#<$XMv2YE6}gn0Qr1|MDR$t+Q5$M;9@(E~l1C4#PeABB$|;K?je)5iA^_~`zg z%o1@4)15PWps z$@MH=c*c{tk0+Be7PBwNd?q%&d`=ZJNZ^U(@G9A&3w@axBEK5IGpc;@X=3r zGE0=2@no*@WYUXA9r->0A02u!OT-WP{t!Mo@MM;7l<~bEKHB$WmI&tYy$?Rx^JJE& zk>YzVeB}3JmWZDLu9io;o=i%n_$(-5k=LR^kBH<8M5f08zbm2+2TUwaVm-K!)p=UV z(Z2V?ehO5wiKgUu zNHHiEMtB5If|}}~BG^P&kSDY1$t;n0+ml)GWP*5w>rf;i2AZ8vyv&N4r5YVHnwGdQ zM72dYNs4G9nw1J!-`n9^p5w_Z5yj!jJlm65A`Zj%Hu#pCJeeg9UVU$cZ+Vs{v&5m1 z?|0!_p6SUfaoXa03w+DilUd>wtM7Mq!4tNB<<%=|+XpY-NBp1Km3P**U$GV0_{_Qn z{`$w_^V7|(r#`QF&FWpe{yg~tDNEpPE$9wwK?-VCumN~W=wZ8=0GTrg1IZD%6Yt~bR9?^+@w^cBlEI~T8U{|FRVwYENj3&r5_|0*x0@}x8un;Vds@}g7 zkW@%e0}uev&vtM(S0aIIs+}!KE;GiROo?=&j~NlbQ~Itz3ps7@_d9t)1A zp9cIr{ox_6Ff=pKc&enkcC_BBM z;Qoa>lY2m_NDJgf1CJ?bu@j56O7&(bj!fFtb-*hamc?Xw1lksdc{EtjE2E%6N^!RY zQ3-a$LkS!m(O4%H$fv-*Bvl-sC~n-*$*cc-??9G{31&nGi^+OfY=g)YH>(x%?4<^B zT(olGTDu5YxkLaS#6h$m25XWb_VW2J+)h8^|XsfBrZPq(}_K{m`P#wB_=uGmB~&Y-(x7`#zsprjMwZ0}SF%R))5UjA>*bT_9Dba7nHgW9UXF*gCaJ=F z5Kg9{B6wvUc0|s(L@&qiMuk+UaZB&B5jh3k_T??SVP|Y3n`xU#s#p()>Rl%ifsFZH z{Q zr%xGfx*556gAs2hqHrXWwInkyI$FXCW07P_D+ysrAIaT9u$j?oJf~QWG~dhrdRi}E zoD%?oeyn==O3lk;IMk|&WF=Y86*!j>NOMq7LAETg<-53-aW_y@iEbvC#(P+_frfh# zrN*Pp3{wmy4aCCYW1KGb`U$Ql&8>d_a#}C{w>^+t&Sv>a&C6t}QykTtd-Ar@-1jQWNLY5ZY+P(d;b6HO>qAIox^`P{PN+S9)9fby@zi-eEs3C9=`bS z1&22sT8Gl%(+&%V)ZtGaLWhq!eDLAn;l{y#9Q@P4Ume_b@X3QeJovqXHyr%>!AlN) z;o!LkgM-dNIY=-?{&${a5e5Z2y<` ze`ep^*Y^4S%6?`a-;eB{-M@DK{`@Fy;tmA*!#J? zXYQGMt-bnQZjac*_8!0Y$i1ug_V?EO|L*^W|118_`#6R+DYy_ zaR=Htvvc*%-p-2e+rGc`eaZJ3-v@o~@V&wJO5ZKM7XV)f(zKriFKE!vu@1ee{ zd>h-}-TnsfkNDj7N4MX-{pRggZ{NE8!tLj7_qXNkr)}qf&jh-C!}hh?_uKYueShnl zTVLC{ZR-dHZMj?Bt!HeNwvt;<+=8~wY+b#zx3#kQ?aja4{LRIyzOnIT;DhnejdyRn zdE?a^w{E;}_^;;hSLg6o=J1#2@L$g1 zznH`Sdk%kT4u5eD|M?vL!W_PB4*%I4{`?&N(>eUPIsDl<{3mnxkLU1b=J2QI@TcbR zC+F}d=J3bo@Wm2^wIsBG6{5x~_&2#uobNILC@Ehmw8|LtD z&Eemi!>^yiubacKox{H|hhH;?Up^dbzcPnk zK8Ig6hi{$3FP+0LnZw}J^bT9TWe&f14*&8TzA%T+&*2x%;a{4=zc`0qIEQ~>4*!oi z{PT181#|fMbNJ`x@XyZS=gr}tnZrLlhi{(4&)r?$T%FEi&zZqz&)`in_^cUx<_wN! za5RJN3_3G7oWa2i_Gi$ZL2CwkGic7BF@yRHYBSiKL3IW@GpNjQg!O{$}GgzF#!VKnTFgJtD z3}$CAGlS_FOwAxYgUK08%pf&`T3_fWFe`*GwIDEH;A3X+(KGm{ z89XzCkDS3r%;0r1cce9#PDJ%bON!3WIX{b%rgGkD(_ zyw42�*|FgNHMCFoXLuxHp6T8Qh)0of-7a;Pwn|&EVz?Zp`5N46g00Z|+QI+TFbu zto*}Ter4}j;OSZfKK_}#r|cnn*Y7=a@2b5G|98Q&^_Tsh^M4fh{lD4&YX7bN7y6&; z?}I1nr}^`K!jJlI@L%h{pWnCp{oQYZ=jz-3FMD4C??_dppS-nF^%h*fb_eWn&^EqG zQb|=1mF%hP`z8>Qs!FP|RV9_3rBR;)xhJRU-dp#+syg36zlh#~eh|F@eJgq;$PehF zHS~NmfqK!iP#oQj?m|~kjqXv9GjNY?pYB%OO}h8!uGPI!cbV=&oup%R8IVVCwvN!9 zp?ikzB;5jX5P1l>7x_AJJ90C~EqE7l4e~nV5~Pjr$T>&^aUvA*0_0TW$q2Ii+vSIs z?_1u#{Ke&4mOr?B!}42~uUy^)IS94o^OqCL-sQ8F@#WpiyOvj$HA{~!9RPU=`<8B9 zx@qY>OV=*Fap|(93xWC}yOdc9fLsM)>5QdkESS*>C}t5xi0wTk_$R}R!#{j658pVcb%vs%S|R;$?0 zY8Crgtzti`RqSWAiv6rsv7gl{_On{Wepaj4&uSI>S*>C}t5xi0wTk_$Rp{D)xJ|*&R`8c9_)8T0#R`6wg1<R1kD!4_#%?eH^xJki{3T{wvT){UK z{DlgxSMV1o_#Op+zJfnb!Ou|ezg6((D)`?h_-+M%j)I@A;HN40sS5sV1%H-;KU2Y< zq2Nzf@TV#GQx$xdfBj@Kpt0QE*JbQ3cm2 zIHKUo3cjS^iweG=;PVPTr{G!z*KBdk9~Aue3jR9<|E+@mM!|or;J;Gvg9`pj1^Yusw@Q1E>U{uKrPvVwm}!M~{BUr_MREBNOW{B{NZtb*UB;Ga?OTNV7%3jSXT z{wWlQiP}M`z4jdK^TAW$_byzx-~&4UyXNl)Pkdi7ADMp^(Di?4`S)`l1QS-UBz&IbmdHlV{s#;A7NweXwqxl*swsw<9L%pm!E<+#NRlF#V4 zg?imw9Wid9UPEWna*$w;Sk|1k$<*Zz|)`6Qr%q z2(E}sYo9f4QOkHiG2N7JP%~!peT%)$aRO}`2K!-Gy3_DD{hd}TH?U_Dj5auFK@{+S zHB}5#27(-hLS?Ea#B$Miv{(%QMI$??qu^?EO_`QFKbf@x4N}hS1A5S}dk39dt{W@V zN^N5;k}Q}9A#*mwCg^^W+8e%}KzZd-*DAMj`YNavYzWcbp}U?M;NfmK+UU_)&Ez{G z{iMqp4aIqfwS{N;DW5s&bh1Sv>0vlmCPA`3TdA7K=i`ia+=GJ<6KCv6PfMaCJYMJ~ z94WHn@ByjZG|4uaiMmmC4spG|nTyUnABv9;Jh@_RIG{^~0VU+6CgJky*;dlUR0gdy z6XuLu5GSf~qUP#_#)skR8YPdR+-QpJgexpogn&#Jylgq?koATxs0IL1Py~>A;=<=X zJ83~gx^2s_V0?QGhmR}|+R&&gS zzJA;y6>_^>q%~2h*UMFtSRkCvl7Xb^mZmQj>l=zeZzc!yUSe_;t-}^G6v0{fxAAu* zoRMByv{)l78_`EIpoWXTq901RBJB!;di6z6#Y69I2<~Wgr9WkO>;32 zaFA@O6n3+;qe%#n0cWuD#X)&xb=)G@%$IY{tj}iP8*xY4TVRYJ)8CXzWe5k^4J1kt zg349$9%mUFmvtG`VuZi!%%-az7F0k86Zrxod-yDE0Le22p;kSgjn4 z*Je**+u1-NT9pRQM7(LscWtRo%+={JLraz$`Wy`J4b;l>KN?2`N^47zJwhs5b{NxP zvw^IJz2072@O9IM1{mSNm`sIhKxe0?J7>~@4CGRob|Bs7B1JCkj#^oXml~WmO_E)z zS1goDX4cPJ16A$I$1RG{fVmqgB&`8wX#h$M(2Sg?4OVwP5a1JoAb4k&A2j>=kW8aD zja!6GtgT_Pdho0zH_SVlOsU^$B|Ozai!n-d(;$|`&Do%%DA4G5r5)G;wE;)|w>Rhg!%Y)AxBY=GkuSso}Kg?gM+v{XXDVTkhuSDyo0^wrmCSv_U^ z9odi*ysz74yn$SY)#sU>&zKJNoOq7O1nc3TF;k;kUVFLPjV|9YZoyG{OR^3E1pD311y+!4C@hZpFnjE-K=Rxx7G}1?*M*iK2HX zRDE5y+)70(HCiS>MSU zFX(4`!&avrjRT)cnWqTtDZs^O82feSQoa5Xhq3qFFZc=#}tU~5c> z=B!=H#`?(e;(hB&$26`6OrxEnGA@Hr5CSa2(uri(jR)DBnT}Sne92yJ1F3_QN$><0 zNAD>A`pHp8qKT)&NTRIQGiGL@~z+d?pxYT>~Iuu@hrgjzylH(@-Y9Jmf z|9bi?;vpkJ)LBG8p#2O&Biq+km_d03IygcXzRk()Q6P`M9=uX+mIM(zR&MrK)n%>V z-Xd_gt(0=8!7`lGU>mrpmSed zildTz{bZ=T5#sv;A=OqXVm>-_pf3BmldoPI&80Vy+F7J|j^A7|{VC?s4B|k?XYS3Q zV6@%b`!f<%7aCom&5h2AI$3onGX1~Gs#~_5(uWl)*g^CLKRjF7XdQ)h%+~@VIVzd3 z?X@6NCLCF|AW_xW20w6Ybb135jSX)klL%~N13uC3jN=jfn6CxqZo*cu65v)# z+Sj6pMkzjQStUofkM~9;uDa=-%qJuRUrQROLd4_eTpOU0TBn}wIJ#`C4ir#vSt^TU zq+hF)GcjDsmCNhN@FUsvRDGHpVGf%zXbdEKe%?9h4g?zG4d%&+wr=37D zJ4i=qu^H;uWJ*dKHoWmYd%zzi9=YMxUH}e8Fkyev#v%5aWv4o&YwP&bkB3$sE-$zZIc=h7dtD}YCbx+aG zqQ7?h76#*=Vqy6I-ek?PvYZQs>k)&S7i6HUBgkgGmAb=q%vWW%li}q^w?~+ZRc=rv%w@YJ?j5+o#WY*rumZt1Ip&g@ zZlVsd_v0y3vX9foG~2WX_9U%V+UBcx1};-DW-u9;K@bOpGdDfp2eDG#@WZeti zEvJAQz!~eOuJ2kuX&qVDtQ}l?cI!7mB5U5Yv)8CK9Owg_y0&ZWq%~ws1F8)?yn0~u-qrm;CE)heTUKvcy>a!1 z)oWL;0W}CNTfGEm1xTy>>iMgg)yS%M_3Tw@6<<9AC8P%-;16N^8a!444?q83q1)%K(@j`paO6}cQ4QZ*r&T4C;{B0yAfysT&ueVr~zCC zDj)Pg<^m5C0W!J>&;&SJM*&rUGjyi{U4WBx2v7z%h&&9m0q#Zi19gDgkz0U1z>UZa zKq257fZ7RXgPaB&Xa$^#>;h^52%-Uc0S_-90Ez+om-hk9 zfLoSt0;&NwEME(B1Fl@Y3@8WmmnEPbaQ<=zs0VnL&j$Jd`0^P*L15SNNy`Yxb~p%B z1P(0S3v>kbE!_^31a4Zo5oifqyL1gu6S!>Y5}+p_E%886AhQ$!ngVApQ9xDTjHOe7 zuE0r42v8O{xcD&87PuGGZ`cR&AZ`Kr0yi$+02BtUS-cWx3|z9<2Py;n;`u;lAhPHM zN(0m)4zvbNUEBrK29QM!&>MJo;Q)BIWIxbzxMk(0l^a)XSh;rPnw2Y8E?c=|r4RB> z_?7cP=1FA53vy4W6&z%roC>rZPFg`$G#~@zVeA0NLD`S(16e4yU^jt0lpC;XK_<$T zpc2I;SRa!>Hp=-}2E1wF#m>g4g?&KT;Fg7(fVRO6pw7fKAP?g*pl{G$kbuI$`3o7K zao}Az8>k%M3ugeGgIxyFI52-NP&?Q+e>>1SxM}`Ipm=cY{53%H;IjEk zfa-w+Dpi~hGBhGU`QYqBH%XS~=jL?(gk1>nTI>Rd@4$u--;NC+z76X`d@I(2_>b5{ z5Z{7zA^ro_fp`tphWKVohWI9|1@UUEIYDWHBE&ah4Tx7^b%=kD)gZnBt3tdIt4vUU z_;(l&@d~UA@%2~<;_EOD;%hN>f(*phU`2?RV+DwpVdq19HI|2XDRv&jS7GNu+{Dg- z_)6?$5MP1iAif;SLc9daK-`O^AzqB7AYOzeAzp|jAYOpQAr7$^!~qtC*vBFedsrA^ z7Yjk`U_pp&EC5l){19J+Jq6;K*eMXL*vSwn?8y*G>`4$U*b^a|u_r(zu#+H~uyu$= zYz?9TTZM>YD-bs@45A)GA-(|9LEM8O5TB1NLwp{#1n~@P5#rxs3lPEEy%2X}a}b|{ zX(67DX&|14{sH2t=ICnA z_z1cW;!n}9KztbeGQ^*tUxN5!^otOGgnj|ygXrfc_&JCVptnQ(A^KT}2hiIf{s8?9 z#QV`(A$}kI^aTHFf}fh;Eff6Y1V1ssk5BOC34UyXAD!SwCU_IX`_TV{_&xN)5WkCl z2;z6p4??^b{SS!$gMMIw?}zwp^hSvHpzoXDdm(-c{db6Wqwj%u7y539`_X@ccqjU= z5Wk7uFu{L;_;vK3CwM)?uc7aP_*L|s5br>*gSZd*F*dy`Z|c8Kwk^-c)f^I^* z34P@RUopX#L;Ns$$prU8{1AFE#1EnuLHrN&LWmzgFM#-dbO`ZAbO7UWXPYcs|5y(LBU=pyy5ST!?Q+&w=%@r`H{;#Ft_;@_iTh;Kkc5U)go6AX+{*TQ@dn-~pI z!n_bg%mcB3xgpju7sMLogjmHK5G$A+qJY^T^4QrB%h*dHmavyVWU&`RWU#X!7O@vW zEMR9sJRh?{%wrV9^Dq+PxtImwIhYyZ%P<0B4l_Z_Vn&D=%m6Ws;Sf{U2E-)xLWl`W z4>69t0AdW=12Kv{A7TW19>g$q2E-8dw-5u^b0PY$zkx_&yCHh8=RkC0r$cmMr$KaL zr$TgK&xUBno(0i{Jrm;D*fSu$6ni?vmtaqW_+sp-5YNJPjS%@e)DQ6r6!s5t1q%BI zc|8jI2YDR|`v-X~3i}6n4GQ}Qxg3T4gItEf{s9R42YEFL`v)NGALLRL_7CzZ6!s6Y ziNgLtUWvl~L0*Bv{y|=j!u~-nL1F&@g#7~$_7Acbh5dtEjKcmwE<$1dAQz&re~=4M z*gpVa{{V#jgA7pEKLBC>Abk|}57I+n{~%oy_7BoQVgDd)6!s4yqp*LFCJOrpAnYGR zLSg?PA`1HlX`rxwkU9$c2dSa3e~>B)`v)NGAAqob0K)zO2>S;h>>q%ze*nV%0XXiT zD*6=IKUMUU37$N`Cqt~DPl70*PlU*$Pk>lPPl8xN*CBG~+5}f2GUy7#B8p8Ag;+p! z5YI;uh6_t9mJ&Ww-6J$ z-$0D(em%ioL5%4RLX7HuIl*5*jOcz2F|2zOVo3Khh(X;W5CgiOLiFn%p5RX)`gA{@ z;Ey2Ex`!Zobq_-H=pKOR*8LEoOLqXGQ}+Xi4&D8$%U{(jXkVlOSFVE#X8>~IpV~73 z>V!V&P@T{jYHyt8;*))Qr4g^DGE$-yt+g6wDsQ3V_z@L)M}=YKc8e?2^J0tTT0)}? zZde;h!*Q+?depc^ciN7A!}mxf-y?5C!`qFDI-zE001gk_njGDT-{}VTWBe)~-2QRv zgiR=?Z`dkdaesp~8{ZRO-1-(V;+lHVU3 zI0#U8(IgjXi;s8t%^jaVYT+EJ%2C5ZAKkEj?$u93xaiBD`nqRpXQ>*a=8nDCY1ag9 z)cK`CP3FLfZgC9HG1WY{9b91AZ*8lblA|vcN$q~ z*5M``Zk(`#H%9yQ7|-=f8(mAt-favwHd=8@Yop_+7zrvpeh|0)U2x@d%e>6Bz|_mP zWpJ_$W)jBaI-NJYOYW%J9XHW0`e5f1^{*)doI;N1y{<#uiGOt5T%pY>HyBMi?9cS86~8+m)dD6#|HzyykV;s!@XF^>S%OIy@EeejaD=ExK->BbTdMNJs%V3jo->S)~SqJ ztfLqnGnHp3f49@>`}i!OhdqtoY4@&T?Nz++vh%`=<6x*OUoZ-@HgPoiOGRmqK<28B zM1zeBg9zRWmm-uECk9r=)O1DC)eTeHL)&w>vn|H$wNBiXvNmgek70wry?#GaY2!Yc z+rt+^w7utx1^8IsVS^XxBPQ12-+$-@Cy7nwxvp8so~qM70rlNTNUO&?lMtnUSt1O9FOd27F4 z`x{RR33 z@Fc#2`p{i`(|R-?ES93fY|$qcLCKVXaKzu#+KaW9s5RZIeL0XL`R&m+ zQ70@;oPqy)XJ9f-9I~_Gx3@dVVym7jS7iGUGYVArz}1?*cx^+i=~-(pRBQU8HN9HX zGuK`)F+J*q;v(%{wWb$pFIH=Mf%YP`rbF$86H_}VK5naohmG){-wH5%?T9Ww4`OEZ zri5D4Ut%V;roX_9YE6HR8Pu9SiX9qRM?C{S!w!wCqnbW~9U56jHT@}eXk;DL^kMAK z$g*b)0KH?0_zjWTA*7POwcD1H2p0`a*8!aI+%v9-MolS&9 zpFep-0Gc0beyrB?N17k0HGN3)kXq9RH4myaeL(YoTGJnDeyG;;faZW&(;sMlpw{$$ z&HZXkzpweeTGRV9_o+4gp5}XMO~0%8u3FRYXuhM?^j^)qYEAz~^FL}$|6B9lYE8eb z`LzZm!37w?Y)T9&Dni_QtwWbDLePWvIh-|kk*KNM0-%xEU3;JQBir8(ICK|a>%IVZhx+65e?MOawc(yHcmLeS;C+F;;10o2?+qyb{?}fi zW&XE&1+)Cog?&krn#Eq7E$|9%5~%t&?hV<_^ac?z<1aVjprT_X6gy&&t$lv&^J+~$ zxAwWQX)9gtc0KlT-5q38U8WSN9^n;x4fYzfrk7)vt2Mn0yG*U=tFc$BHN6zORITZ& zuvbk?rBpVM@Oc<_i1iHJ!%XFfGk|u{u3FO$+EHuTMkk9+>!<<9=wz`ux@ilYEHj3KeqHS zwWc3k`lwpdk1Tyet?5lmH>oxK&!zuVYx?1(534o((9(z0ntpKUgKACxW9dKCntovE z18Pm*zw~~!rZ+C#I5Ewodz{DR;H&+9dpIaLkG!*_xn6U()^QJ(`z-?PE6CDUOdi~TDf5~>&Uvj@)468`8D!uwWhy9ex=s*AaYQx=`WFA zsx|!u@(Z=5KSzG9*7Q;2QMIN&Lw=^#^bzC{wWdEsemXWi`pSL#+U;shKfCr>wWhbN z-KN&`Gi#qwYkKS2t!hm_z4qyesjtzAX2XM4hfhiwryR&0Fq zlB1hGxHP$0a&*%NmL@k#WN*wXW`@~TD-y4z?NyOKB34lU`6rXZI;!c9(aG(iqkH`k zdgu%s)$2p(p)+t)(+AN*XW*!&51^{gzz@;M?V_U})&X>KyXfeqKR_q9i;ix3KRUTx zB-_$%sT)s5T>V@i#Y9}}5xuo?=gOUv-a4x3H&?!?*7O@I-%xA%^_8!yHT~Mk*VLMR zb>*vSP48H_L#^q)m3?E=RxjB}*;}Qy$Lq9*Ypz`Vi1WG}S`Mi-4K4@Ong*5wYEAvi zezm5)WuID8dYM*h>RtA#HT5ieCZ|cOhpw@JV4%M0t(1BXhKH67n+CzJ*>lMuz z^ABp)-@p9ar8}3dSb8bQ_WqZ}^T4zHFD|@t;mq~ZKt})NYiF)LxO&}cd=*{!6v)`# z06P9}!$RmE&<~?!beHZcx=VF0Mjl13M^ec0@+X$X`3L9Uy55-2&Yv>(*}4AQ^R#zs zz(x)JPt~8yhZ||n0kALfxrB@seKYtNE)+=*Q_=^biM39fq83?>etD8Ib>5Get z8r|G0c6d?Cbfk`uSSrl z=%qV2q7HXtv~D@N^Rl0f9MN~UH_zH~^x~Zy9e!wsIeK?{;)P99a)L zG^L~cC;#op(J5aDY+A>TMuXtY9UPH|J36e_gm(f*cOKXr{j8i8*d({yQ9HRK4~y$C zciw#?N1qxU=AE?Oy=mDxOmYWzlyPTw?=y`Yz0VZbG;hUa*})NcWJ-~u2cP%Jk)wZq zX!P@p+ufUnt+;k_XW99zee)YP0!QDx_3eSpy<2fzvVB};)8QjadA{y`!zIAcw{!vU z^X@lnx;HP{itFMX+!+r)U&>)|_lw^~Fan>=2!S+BrJwKabpf>;`Co!#T!Jb&vj3p+$-*m+jo^}bJy+`aEp?#=VI z9ObunL|6}>K+0jU|K=Bt99{i=_vX1 zf#%?PWu01dtuHODE?3vSzINT>zXH|1mw@ViFI@#S`&PT_Z(Vh*K56AUi@lZitz0z! z>}BhGbp6XK{*|WzjkXVCn^J^_cz*~Ek0@CI}7hy zxJb=8gHz2X6-eW>GAQT(;2GU22!_S?#jF+=gtL0tq18P|eo+e2>3;o&KI$d;Kw+tcq1 zW9^bfB!o;bp6vODO=sLhn2I$9-K&K)pW0QcIVVux7yn>h=w#KAUpcO&K{}Ht@-^8)$v9M8zWcVlsO6{P;_{A}G2vy7y|A zLxa4~<|DRT+gFK5!ymz)mU39nq0`zxP{oT)w>F(b2GF$St4 zu-{+yhRa634W#IpjlFinr8^V0kcU|}m9^p&C&x2ZDV|RB@}!Z<3j=Fln6bGk94+?Z zmV7b8AmegZZ9Hl7gtI2ABc2XOQkvEGB_?Y&nYZPB`1Ftz$5fpo`FQMRzr z8Iq+zM$8)r!D_T&l1)uNpDmZ7ULZAU@pnuEe%PmRgdVqu8d{Zjz$%0*F0($`vsX<) z!rrjy`IumIl{(&5-XY^rmJA0GYJ41NkX_vM(E4ClG&iU+QK1rI$1+UDscb)JuY~eh zlQ)oX=-b)l?=Oy{;|ve$Tsc6y%f76iFcsomKM*dhrHwXkIc)E{bO@NK z1~s#FY6HKbWAFs3Jwv+C?3W7!L#BG(BFlOMK3hAVHj)GGj(s0S{autys=D|S)7VdNuU$kc6*Jr zc(R-=WZi*WZh%Ln9-1D1M~;ozY@$BM&|YtaZu<2Ryj66V>|{wQJJYQs;cqnS26MGl z#l{7huVZ+g6zj0evPrz9WXtJGmOy7%YS1Y$V~yDyVRxfTW3h1$rV?gjn~sthyg|r3-=p!M$cNh$ zgZJDm+1PCjyh+wvV*2!2W88w}ESWwxC~~0mQJm@N=?Fu4Q>9u9_qYrGfs6Lh#%val2mn)|y$Eqp@2n377ZUd0{ug>X43l4e^n;^UKo>=X3i zZaHs|<0)o-T;iwP!G3W0UE>zTxDi}jjiqMI!<5_w z7pu>e!{CaSV!dM8?T z$n=Kp5Fd0&q|{rQoZale(aN{_-E5^!`2u>QuaK~L1bf#=^h|xGBKz5ByjKSD5ZSrA z$9sm1h0ELX-eF3LlqxESEBf=1$Ng27>u`0iVBQ+#=e}06l$6xkv<-#$vVV3tGU{hD(}*LZDc;mun;_Jt(&PosyxqgpXU0qSsrr ziJlbI9+roZ3fFej_`WYy9t2XI2y69boi+c^+>W_4f==99WL zxzw*@3m&n$Jgz3%Hc2^?o+z4Fye0KQ(Sf;12f~)DGuPxi9ihV5=tiYv_c;vZg*T4} zqS3|L!|s^SB`G(N5?X_3hbl&yrhCBD<29~gPRcfOt3*fS%J{$yotCkdqSN3?K!zwc z*jVcuS!cLq&7w$yO7R+MC)$BP>pdRv1nU* z*|;Ue9( zxU&Ls@bq@p*bY`3t#p_q>-};mlF62xB(-RQr%G;llSIAb4L2ICaM~DXTI8y$*~;2z ziuV@mhBO-p8aZn|RioC&Um<3VVTZX|?&^o8uFvDD(_uGLF}TepON5B0LU_H{qY|Nk zo>&}@b-9r)I~oN?*xB+3)m~ciP#rqp6s>Vyve`h@V97wr&XhUVq_wXYe@DZd>DZ%Y zy(6VhkdockE_)qKdpKOE)@Ua?=%<@8o^A9Jc4lFGu>y;E!Ck>QrkhN}XajHTb2W3M z5g$5a$!C-VYdtCCz`noGuq}-jLAlzXV{C?vl*vpzOL#2ZP>n6}p(Ngw8hE!6Vu@6( zmx;)tJpaaVRJ@_V`lNu|?`IP^GAkPjKDNiZq#hqAck|g{Gnf%nV|gd@-1%g^;~&(2Lbz4ClO{>SF*;QFXhECk9S&ZoI{i*+xFcwJZG;O;_s1%%EN8 z!wzTB>a1m$RF3z0b~0{xd0dHF&a(cpoOE*oscr2xYk2`@4GwTDMA+{k=qNiB zvjw-u*lbjn-#m^g_|N>uBHetG5cQT@LK?J1g-OOdB6yaSLE|G*gl4$s60uE4)&@s(U$Ck%^H=q z)?_^6!^@?Lp{=JPRZpcD?HO>cV-3^CP1?`6ZF!Q6-sKZ1!oa)5Y|h&unk<*;CS$E) z)kj9M#ZEB6lyXwN%~oMqq}?-!5{?A4c9)5GJR<7Tc!2VWopLE<@F!#@Y0SE^5xa#B z)Qh+?5biUQNNg#NDAk9e$#S#Jd$LiJZ1wefc84QK+M}(OvB2x=erHP`&Xd7%%^Ru* zp@SpDx3;MYGSawEsF*xX5{26Y0u)9b_wpo^g(647hTE1Wu`$M|z0o!gZ5zI=yW zH}3Stz*TdYHG*o=K?5lG+K;QhD&@wFZ8?iohc@tcT-?bMR~>?h(Z0giO~j+lst7s2`yOu%UXBV`|{Q>+Q& zDu{>50!_8g4(Z;!Y;+Wd2@#F1XxmFN0STe$XWI)RT8xH~=<2rfu1ckHdwjgVK--cb ze=OFr7$YVz7E6T;*+Pu0d8NpZ_cr6riFnZeB4-8yIb2KVkd_hU$)#Lj7qzF;qh!`S zXn0#iZXh_5A!jZ~3O&NI(FV5+$5%WkAdq8Zn2A0U0JQ{O%?s4)>H={o9EU??)Hcs@r9!9H z20lhYrPJm9f7VaE;KZGE=w+6umK%YrF1#D)NB3g!znp)d+w z>3ggO2Y)$=aU9PPL5?p3TY$I&yyKax+y&{o`D2 z%qH`Wbz<2)PFu0z4VDCR#u7C2!hFF@^X@vIk?XNN=^EFxWXrB%#h3+Uzd*>xa=EFd zBaCF*6B|e-vTh$V3mds%%4_a~H+;5gB^>H-1}|A4{H3PkFZzW@iFO17b<*kaNo;Hn z%QoGqM7){_3bhTgl*=`v0UNk;sOoYvI`q*p-@W?DaG4)yXW4}wx66&e`u}^mITxJ! z$4L0ia=jWkIO6iLC5ZPYxNvMEn>d;gf(#Vr29r5|`e>%=jkv)ICa^JYpbS7!SZ~Im zr1xWF8>{X$QWrX8hRQ|4>~Jd#SA z%!I|4bdhCa&r23Vfr?!9x6Fjm8*95mT&dJHf|V5<&auoqD$(iXWGmSnMvU!DlyApu zKnuGl`iNYEGUUvB-I^lUWU1l|_wNz2pWKc4OMm`<9XyT`&;L(6|3C5k|HSkEzkuieKOJAN{;1FY z`SEwm@ce&c+~TkB`TwWJM}Fe@|B2`SC!YVGc>aIl`TvRM|0kaR|CK%ef3~KeS>Fq^ z`!52T{TBeO{sGYF?*VQ84tQ5z2Fid_H%=(y^ssEil$iCDfit;AD*ZT&78S|vuVEI z@43_jz`y{S6lv-bWl<_o?!H$_i?(b7+|RYj)r>RU%<+BPgj@WPCQD?GcNY|A4&XR< zL1E?qj&m2B4+EIN>GAG@{LBFy=Po!81~9|i-hG?{ICp#oBE68o80e*nVb&?7U9!|_ z1T8V}R$|C4#2L{c6J{4>WkcYiW$8zq^EPaTn1L;r z3f4@SQWHQ92GFPvhtZ;w2XB^V3rpKZ{rWcn_SXXvV24l_iHOAx!INIaYA zJLH@T&-bfs>#(2G_i3w4ck2x*CF&a)Z_;DR7nwGpCV=eB0UYNp$jltTaqfaN48UJc zrU@Fiw`#E#XKm&54x!AIJXDY_x=gIQofp`G;1DWqDSN!TAT@IU$GHoVGY4>-yC4Ar znBn4byt^Pia{$M=3t}?|aGbj!3Ipi%?WIaF=^BRdR=4E{aAK)qi|Q>6uPX{Rl_8>0 z>-t%Sr|IK93nDWIaGbj!JaYiYxeG!tfEk<~?^zI>Ie_Eb1pyeq3@hC3;~ap0dBb@J_|iUk|zXdV1ynj_d1i7hms%0nG4j>+$V^(fj{e?XxuC|0n)E z<_x?ozqp|Jm|e3pezoO$%MWW8wAuwk^JfR+i2nTI`ZGLLf2|>%7#ym4UV|KZp=~tX z=e62-(Cp6*#u5Gb#r0=+s{UF-Ix#p@^}J@@WHciWYZtZJMMN{FwIDy8auRuD$|>Y$ zQ*K2bop9}(2}gcD{lI|yV# z%?#)p6lO-`4bx9d$W>ErM&3B(By#nXQ}b^cMQ0s>c8c~$kvFwZTc${^7bhwkatYEdH&jw zsRg-q`YRIorzxk9>!#d_ymP{}b507#koQix33=a?6UdEIZbsfeCs2$}Py3rkq5+JmnPfl_|F(`z9P-Htv{yU_ib)l@0@vZ3x2IQV8HzMDjauf33Q%)fNGvyZK-YKV$ z?@YNB`R;_ni^KP(9~hAPrrd~pf67hB{ZmdLKbUeea$w3W$PcHSL>`!O3VCqKt;j7H{qd!46;`VS3hku^DfV8 zkMH9NwKLEB*qOIYHs&_@l?<51ZEiHul=+p9)9ip>*#+}M5J!2*r^B2{7?C*JtW%~{ zj?_Yql2$o7++!W=xYS3U!icIxUf?Gtdml9>SS3=p}2!Wo;Uw!A6lTAEPo*yFXcWPB$Ci1##Pj zhj{GpKYbR>hS7j{{H20=WTqx3y~%zxUltqwNX^wWbr_!{n&ocG)@lgWf?$Ii-QoP-ob z;UoEzZ6_fUH#_X5Uc1!gEUfIynGM5S%0bqhK#4HGnCRi2YK0cDeWVuPy?Phi>pfGudxmjz^vA(RtT>~EecLH#`iq=$ zGYFY&H|5%#49B^0t$cjun{tJ-9N)QQphgb2olEUdAfBlL752K#yr;-l{AN1KxSAEg zK1^-Y@T|}`Sh`$E3L3|={aEtxp9U!*c@%S71-Fr zGIhr15F#6ld&6c5S$NaJ9Sg=t>KF7;B&pp%vW$T?x4lDK6i;eBRPMOS#4Ui>ZaLFU02e z&4=c`IOo&eu62XPf8u|8uktQ}6a!>;fXnH#bj;L>RQ;uFrQMYCj$Dxm7YxP}UUSta$h&NrK z=Fr9s3%Q1;+Ag||$T8S^m1i8m3|@CT3e3RGgq)RZz1@%G^o%)IaA(}{uDu@)wlXb8 z+X7yDO`CyKs+tJg;|MaMv78g!WzxaMiAJJFw8FKv#1d@S6w8>|ZiI|9<5kY*lFPgK zNC}8q$0B- zKjs{`#u3cmbvGwx$#~5h@N|cS!R3nf%puO(FG z!Rziqv>>`W)=q8!GCj;yvLktlc}vonw~A(jIp&;-S?ip;gV)`0(ZrXX?q1ItX~)8Dy{FWU zbwW`?e&{Y%TT&uuU@MJ8Ct~oc>V>n%5zOFqH=eQeTtX+BcSc0ATC$t{^;{*yrM=m1 zSIEj#y<@V*YjociD62UKFC9lPgV){NG~33rrF?%VCnB^h+Zh07mP)~SZGM8Ot6n<-EJWi5qf;d zC-gktezeLL>2Nz6FILs`f^{6h3|?o+7E}`fHI87G9K;!t<+TtH#neP# z8AmXK*W;Ukw)ZOYID$V}2H4)Kh;an@ZnVa?EsbQTGqBc@AybiWW(;0ARF|xop21xr zU2W1RS420yJE1;Z?C1{@_FiS0^n!Y?yS-Jr)UP&*sRGHn1TiKC=$1hDB3v%OdAor^ zBqo|m?S?bdOR9;$IF4Wjue)c+*HROKVI08>UU$!suB0Xcd>p|HUXO1o;NGh?#u3bt z12jXjikfrq!f^yMc-=iiu7;Wj^y3I-@OpeRK=xksf^h^hJTN@Ik2v;TwPzf`47ZYx z?=7#<-~WHJ@N~_3aedd?m)ADe&RYH1>N{5bE5BTM=Smp+4fZcs9Q_0OZZwU~>;4~m zZyw;dQRRTCgasSzamC+VI6mp5I8HdnrhC)t4YR-_2ZUCK}V{zEm6%%o9~2|> z>YU7wCt(s}Ofl%xni#>f1=q3#EZw4TO2-ng6?1V}W8rWh_MlXK7>eUZaw%KxsQ3`{ zH`yH(%C+7}n29l_-i#BvoNP!Q4Yw%HQR2mbuM!>DGW;qJJ2?VWEFIyhWlP}1A}Q7V znxH2Xx6`EE^5`T?e2iJpNw(eZ+F2N$N#I8S zQKXc>8(C1%XgKI(jJk6Y=A*`#MZ>d^p_28wg$`9KrtF%VPdDVW2O^9_t67Su*eKZ~ zQ`Jgs9?VCMF+o`o91m$m1?~w}tyFuaV}qILQuHLg+YO=%1EKAY1GVGM9}xVu7AWSOVpt2kEC*o@QSS=wV9hk zxGXxxEY|s|mPiz-3Po2Uv3OM{vtlSPlQQX-ab1EG$&+zS2uAi1qeTz1cui|Mi6Y(} zHOqsO-a9(R3>B+Vm2MDy$SjkCFpP&=H5$~NjbmD#@EXlZRgj{7T(v~wNKbb~P+tYF zcj`r(mNMkYn8+B@i}w6@4PzQ1X9UA}q}AxuJV9>t$q*YWBggV}=7y)v` zN}D4lV22HxZ>P({WaH$yE;7a>a92QjmYD@-7&ehY3ZR^%&kZtCslrqwHeEq73Dq*_ zK7kw|O{bc`>3{%LRBQae<>iz53AidjKk;tR@-joDsDwFI_e_Q!ri)~O4N%G)S+z=x zLw%jak~VY1&bov+w5(C84KkuCbtiukCaB#eoIe=JoZSdpZft0HA|<=&P{AA!{6NmL zwG0RkWQqt<={9NS1SzP}Ct+SR#xx;bWKm71m_Bd7Hd03N zxPzF*>d;z~LBtaAYn-OCWH>JzQM92Y;BsJf2Y#lQmWh)wLu1VP&;`{6qC%_PAcy(h zFiFd?a5e)9qfs3+nhJ19=S-Ugv%P&J&7`z6H?s4fu20_@_WLKVM-Lfe+H1u%vn%r4 zs2q$yaH3r>LFL>=f_5|QQ7a`!qmEf9!Y#dxAITRoL67L+?X)0t(!_v2**Dk5m;_BU ziT)a`dE#)CHE^=0TAe~9$iXN~F~2wpZv^h9}wc z!DCFW+^S*`PjEnqUN=jdL%N|>sHPvOi$~M&C~S%SHl^zV-i{yXwHb<2M-`JdLBUD4 z*FSkZde9iNYPE?#EB4C@iM9g`lF~L&3D{Ur;q#syZ7O-#aH^v>)EuxaN!mxc8M`hv z#TwaY;DK-CZDo`0$;&QW$pUU86p-~Jkr;x6!vnn25l?t5f*rB(0jP2#)K#%dClydE zxO2i25JX+}fEDgI5FCw*PEqhrcnaVs^^95J!Ql~2i`}-_F1sf@0YUU-7pw@!aX>~T z5|*TbwRvzI8D)^%Y{3#*#ct6qC;NaQY4a1pW!QKyED*gseH3Q={U2I->(b%Z9?l>7?V(p4;tu{2 zeE+8o{Pe(!4-or*w0~p&BldlF-_JgLi*=_v3e8 zu6?f&HJFeeR-|^SW?_4&P4*=*h|0Ay3J9h}?-XETRLwCNz zdYzi$>PBglM==a`6HzV(sd+b@36DytVm}^nm1>#^4O-%QV*>kwIbe-U1}wIY=fnp{ zs>$;uC2TTir3N{fmL}n3gE0G*mMgn$NnE!ku-~5p7Bwr9=hsv;LutNAh z%0&kSk18g4!o(^pHS9p*dVK=>y*XeTL?V&*QU)sHEzv2Vd04dglFXIZRwtwpX_PJ@ ze2(ZKG$pRrCa~X~1GX4u{E+J@ycg2@Ij9wQcy*N4lAz>ng9T+O!=-S60&7q-Ym4jE z3G8>~fDJGX=X1S5P3VJi(y0hiD~SbKFPOzBoNC0CK~K#JV7J4?;^MkFf&KRBx8diH z5^RWYjclrt<`kCBM~AppWcx@<#A5WIf+?W;sE5j;3UxpMSCD7%7i$`;H(MaM7s_0z{MbmsG z=0pp{Qe0~eLS{`|FHK;-e)^B6^Yxh8lcEX_#Vy64GbmdhI~wkpEi)QQ(^#s`=vQxLo8Zp#>xgtA$mM*G~=Tx)HhNi*vv(kTE8%s}tBS%mKSV8ko4QOkh7h2kZhFW8%6zf&JX+ zzxx+S0~6P!3G8R*fbBwhxF{D-7f1mT*V7Z& z|C|GMfvg&Fot?mbbPm`BQf0(-W&-<>Ibavae-YPH6WITl19pL=7IB@Pz%;{1hB;st$h{EP6BF1E z%mKSVLWQ`FPhj6a2kZh_65{%!Ca~|D19pMb2XXz86WHtLfL$P;Vf_7nkEN$99mWoI z4!-`tHxHb%U)*=a>WB9JZf_KD{=a6|*LU4>XLaSDc6@Hf;pG%~@J#S?`eOm8u1g&RqHSf+fO4kM=-2Ws%o)_hY?Ex4sO=Ql+`)OD zJG~;OcpvenJmhKmM2)leE-fu`V!K`UVkg*n`(d^Dxzk6V9J3slVEmMikDmoXh<|K< zH&YXwzQ3m&KQS$ebD3-YQSJvJ^J{tS|HB$$EtDS9t8}m)7~&V<*xi zr&r_@?_<1)Djrv3fqR78buT>Uu-g3G>4OiaP$LY~(2lFIK=5NbHRjw4hBrU=N2xL1 z3#!M}Sl|xTHflg8?gh8q(w_9(=|8Ere;d$vFQ`n^nCt}?h{F7_YHZ$WonDPoIXO^- zc$$3teJl{0*iMnn9h~>M(<^d{_YntbNXOM!;Cjt=-8*rQxBal%{M_k3q)(y7cxNRZ zS7U)M?(Ni=b7uu@e(sM_Bf4*IYl&ET)sEGB@BPN!tM|2*zPkMWW&Ys^LcPG_9(dbSa4;NkQMxawxqePyGZx~6J2{umxTR`H zOSLhkg_TMTFOsE%so>J^c?(^S3T5KQE)K z>R1t&Zem^@E?ZWR#}hQ!D|C}0L}rt!>X67x$GtC@32Hp)i;b?4$`pH*P|4`C5ryxQ zP|;91H_Js-hv}peH9MGDb?+53L8R^CX}e{lS^=%5xDwJfoAnsl$ci1V;N$gN5K8*# zQVUc8o4m(e^_gHf6f~&1LB(@ao=O3t20v+~MLjjLY&J;x9XS_u7$1pP_{^%$o(V=% z4oLK=AcLTm5?hj%&-Sx$Io$F!z2WGvsHF)NF4URoOmL4m6U?^bWvLR6fi!1*SoE@b zHB&-;N$9IalyA3+2{=eZX>U+C_8#vx6O6|)MOp1;l!2MkO(iK{6|e3!&6E<01@XRK zu2#`Xx{hULIxb&36Kqrz3`4_R(~ZR%5x7W`oWu_4O4=~91;z9QpmRAafjiVw9ryfr zCOCg~@oWIReKoIA!^l7AyOMiOxZEmMubP0Y8Ou-f}2X#4Y=fs$42>)S%a44 zD8+Us8@TI)dgYpjr-u+Gi^+H*5frJ67A=8L zL5@uWVkJEu#HWJG_dRACtKIi1U0_$i_J*WbXyc^-mwS1t*EdE{%^b0WQBP%@+Ej37 zYbGf3$s`rS#H#DoY`9fMNU|D^#ocI|&F2SUolB`jT&U*#*~r;HoC#9mK`l1y3tV&N0e|o7BW)Fcyz!!aT+4y z7B3t2C>rY$8Q(&tU0wS2Opq+*l2}=*J5&ym^Kyo>)RvoM!v$Pcdjq#?CW}NPNZ12r zrsLt6U>$Y_h9mk_M(pQ9f>!nub=e?M*c-WpP`(&LxdBrTlm5&fcYl2*NP~Q+tZvX) zz7<09db!hU;}k{7o~b4q-pCkKgG$w|G~L+%*!{+1ItCGs4%eFPR#&95t%jBHnR2R9 zlL$W)4w9Wnswi8z0=U;Q?dr<8Gr_D$s7^7PD?x)n*coZ10?VZ<{ZXgqW^-M|g(3+l zG_nRLHx=Cd`k7#vRsn%Qt`{~lZ3wJdFji)fSc9%-<6^}zc*ewnI9_36vjMRCwR>mY zK^JvrG-RsfVcaTtZG=jPDjkV38pDnPazrC02O+jEf(+tmlb6g|G@2l3TcyGg%gR)Y z3L{EE65=XFlo1+Qy_%?XL4v#tTlBH6-g_q44BUE3!&6?Xl!-7nBSCpOR!rnW<+uut zY3NV`ObFq8Hb0xmci(F!*c)nuoVKw7p|+*2oUmcj4Wwa@ujqv|+GnGEimLRKly__< z|Iy*+EyYh>;7|WQ(*_6N_77FeO5tKN%f>aNfF`?{OupJO(y-UF65XmeP=gUfL}ssX z`RFl02r0zsv4SEiFqf9lWT6kzLIXRk*oCB}OI{Jn{Hjzn;sMaw_hr4 z*BPi>8ujT6o?yLWExu+Z*km}yEp$PI+Gta?u$Yl68PgQCx?FGYebdvU4UkvOo9yg0 z9=dEMh-aK(4ABzk$VR15FHVH4bSzX?`-;NE2MsIbP$@vMQmW4;fJ5nHT{ShWRZY_c zt=9}fo~CuMil2$o1R5sTh@gW>uL3USSi{-I>d>+IIIX6sq@rYMb*Eh$und+7=ae2K zm!o*Dp_kQC+O$K2;AUsj>Y*Le76*eu6@%CSiSkG+!ZL0Y4$}!~nCrC7a#@NSSSLwg zh?_%a1Lxo`W?c>8nOrU2jf{kPDcT$ihf*t7FFX9D3+^Kav?!=?R2^k8hJ&Xp|V9Js{2tVlx$i_;aH2umd7D9$l(pS!SE7PwvZfL z^}|At>4#!POF-gozfStq!myT}O*sdzpS74$;#5}#gf^uPRrlIfETpvtWTGrLoRW_W z%u>3`m%CD-cdW%%&IDmC%@)W38*1aV1Ut~2u+$EPpk!8yhO2@mVM4v%FwiVIn;j1t zGeNhDkVrB3HA?3PZFJD>pR5iMl?VO*scMGeNFhsuf9%#l!Vx zflyLqBLc`rU|CiXA{J!CY7U7P8zq{Y2_Cq4CKza$Bo#3lqj`pPF4T@n&W}E#;uK;#O9!L()4@oNR%)Ddir87Y{lFfj$ zYEkV(eQ-I9!$OHL)A0b`5|WZtDul#v*RZIZKU=JpshOaorZh^F*`X8BN`q*^HBF0w zN8BJwDZM65rhA=EahTA=*#KA`91}#!PzD2KhT~x*HsbkOyl>@;LP5~&Oa`!Pl~|W* z2C&+h8E5CQ7A*$QO(llRaNwqfy<#!Qmz`=i;^d?^rD%G#0IWQC)}q;D zOKt=!6{DRbIif|r18XC=+afC%!o-|#EtA%oopS8hXUFot%mmA9h$jk7aHk`l@~Z*n zcZ(sL9BN^nNsW@OJcP1AR%rGaZMx(v{ct9j8%Pu;@P#^obs%*hv#3Cl6&;ai!=zL( zR$=j~t(HoKnQ?ZYGeOo4jglw>S)D8-S*20A-gNLtBw4V)MFyxYGI&0Z=-nYSn;v(+ zbtcFg1tTHR9WU-dg>oPga;yhtsZd6VRhn(ljrimU3Z>iNBEn=Vx1%`|9NOJ(rJ+*g zdO9IlNtN!@x`_;8MN$@HR$?Mule(JMkzjNRZN8M0KaNrXnywj zxc_D2^Z%X8JC^pZ?_JsX9PrEk?|z=SYvX=nHqqyvy?kbAMmDjDZpR!CLtC+lE|3{A zA@E&}6gE<&WSH8cGJi8foYiPkM2s#=nt-2Chi_2ur9(B^eQKwa=`9VCW+Ok>6OVwl_PSbG< zANiaT5JB0uS-qNU6}+^Gh%|u=D1pGup4nAeAj}r+Kp?4iKBJr)ZA4nP! z_FG3Cvq^w@5@XWjUUN)J4Cb+MISxRVUh4Yb*V)C-I5t*&%xS-G<8LMl;1NKlJ%2&; z)1N!c>v!@0lYY0z3cXeFiE+x8Wt5##*|ExhF6W!J4IJ;hQVHj?e5n+QoS0()K___Q zn0o$B^u|aO+m_s(Z{|4#rrJCng^H-D_~A++WgCe)mCB*)$U+#>#>&KK&6sxiju3K3 z+ueE>9~^Cmi%!!0dXE~Lz>cAVHWpCVf?2XSimd`BT+PvNv+sYgNW#?sP$e$Dc4Z%}FO>Ps=e)8XWg=+Mi9GoG#19 zJ3i@~ULIGtnfz*7hJEaJu)!`HW9=q?ndUx#e@zTNt~#sZ11h3zI~=LK;9sx#$1 zPzN9%wBe&|>*!I5=qd%iDC=&7jjXl6fezpkk&podnMZ4l(V*DNAI*_$lWNyGG^=Yo zF;^`OY^?g2|9|z$rE^xEwY2Z)`_5UtZuS0q-@SKj&-)L*@bKE9cO6O{{I`RJ10Oh` zf_eZ?-ha`N)GyXSO85tjUv?Kl94-L3>9*dkfsso!jKM zf7kkrE6)S%V4$4^uIGkEU)OsfR~rSCUWdV!4C(`QW1|^YppCAY>mJm9fNVy(z(_|-XCKgRUinOI@RCKRA1nF{oGDo1C@rl zAbZsS>1OVge*;CK7sW2L<- z&j$S&Zxk$10_06zGGsYVu-M6W|fX}j84IoWET|gE?b1v zNP61bO>X5nXMy&{uCu_|=iKcbcb&$S_Wq8b6!gH*FWE=l}0sI#6G|@2MgAqzZn$HsZnV#Mz9nPr9CB3Ar|#fW3$qr$B!qrzJjBVJ%*ItD&}f!*BX z`2}{PTjw$?FokXvY~H3A@t-Jx;l#SbzF2Dv7Lce1Pw7m9?Qfs$)!JlU7Z3I)gAnw9jhPAUuqXdt#VgfOEu zr;a!9`68%e)yK!D^2WL21Dh+bDVs*p@!GWi{eE+vgTo0=M?L1?o?2 z?Q^bfcJ8E&fS{Q z1Ded8)1#D9uj2rKX3uf{&_l5;9rT2Rhiw9R&{iG4qn;f_qubU-h9o#H7e_))uI6y0 z7B!<~P}EW9kM>D1*C@t7O>_|whl1zb@!H6Eeod(HR0D}Op&Rkjcwg;qM>#P|>r(L5HkybxR8oogD1x+T?{mfB0mL@GILMDc-;hdg5RvmaY zs55?Kl8^-Ij4v=;7xwIJGXo=A9mU?+p8ZF!rVD%kY~|To)fxZOETw^IBDg1xZmlj~j+sc<_-_3W@ zGP(IC`sQWwSnChH|0So)FS_&(9dCRdf3oA9u=<^NYBX;3j;?WNBs^YixAe@?T-Ju$a5f&ph5pR|mfX#x7JJE=3Z_3>HE zA+4`uG%P?6wQg<O~jpDHJFp*gyaLi9ujl9p`S0JF@`c-P~u;r<^QK%2KMWkCQvb zgYHgGrdU4!TAG{)H%5@9hP8IuhV6ED6v_dvZ5`Dj^}5k(L@}pnMp`*Rc3Ea_t+$#Z z_Jj7ubHtsTOtHRiX2Cl>nPPQ)6*M&-IDh_Rit_s2abJLW@5Drk)%86yg%>ylJbj3F zp55?C;d*aKyT{r`}qe_c9!^oj{=(x!zij;IA@3YM?&dFY=m(y0e*BHUedhOH`^_Wr zMtTCwWBCk(s;yc=CbAqr0d>gPQ7%Wn;;UwGh0g5_&2fsV=>>;%}!RzQ){vK@l)qFEW~IiqJrMs5!+ z4`n6Er#($ZN_+}US~K9k`))4rwpabuyZ7)bUw-(1zU<4`o4);q4Yk0&`sssPKlST- z0$^qW%+w93CyPk05}_180nZlGDVJz;`-D?+5Epiwn!=RABg~j_O`Lo7J>DVy>K$Kw zng(Be!3&=G4-dTMeGhul-(UW3FH-M$-tYYX`GCBUngI9cPG8}|E}1EjdQ0Nt=^P)| zM&@WlI#viDkVZajrblrcpK-&yz3~(E+3(f|Gg?##sB${k2~k=Clt>SUMgI* z+m<(yQ&Y@ek8cWm{C_)sL}$FK-YN-~|F2Q{aEy zb2aqY2Yoj9=^Nklu*=_d(e?Z889eLy-(Ga~%9o$`;miN+?5E2ci3#um(TOQ={!44W z|IGdU`#k=8zbbgSdw#om%X41FqF?*=_aE@2pI-jcUw=>Dz$d^96iJ@~f7E-}V`DG6 zr4?ztL4E#hM_%!~_y6MeZ+gj(uejk^&wKUH=~vw-Z#-%OyZ}e}6u9-)|1P}xickN? zi#+|;54dmk8IS)fBk{i9zqi`D;QlMfSHJo^dE=22;03}KQ()+458Qk6Cm!@f# zchv1@}uwi@GrN}a0Ux}zS9 zw!$sCM&uc?*zsl@N$w@@`s8zWTYgb?NZ%_I1Lmqzr>g#>shWf#WfA0aWf9XSh^X#);wo~4C z*aUciSjH6ig~1K)yuNsU4VpP@g6U{s3LD%JON%H zXfXx;)yppZr?*{oGk?xcuD54gnp&Rf5D=>>PY^p>yuZRCcxzODHxd1FlRlUXFdFa=({I&#(x z#1)sn@q@p7qW_}eS?&D0@5@Kt`on!+d+J*@-tnSm$QxtY!OZz;D^e?3$y$u46p*Ia zM><)j>s63S7S5|^CCJ5c?SgHxls}v3A9Kaat4ALDqYLxj?Ed7N*S-2}e?REn@~P6_ zw;p-j4_Ny0yZwi}5uE@p5UrT@_-X(9%l8G1(BF2S^osLdd6xJyMZM}BzdWaiQ%`?H z|EjM&{SER4IsrC)Emm++89EptVNeFMkMd@!U*|%t5H^$`CfbI2mYz}awHX)K=tCE? z!hhc9)?a-F|En8b_?Y|c&HU#1@A(L~_Xlsh=<;hnmt2uIkO}Yt6$WQ7Z}&C#{Bg6k z_Ok2#@#1%`zvQ-8uiSd*hG)O1dmaDXCqLqsOTU&k;0f>ok&7v?bpFP(*~fkDf=BVu z+dlr2){E}9`irxv2k39x^T{`z^TwUc`^X!S3Gf0TiYf4+5C6l@g3kBvRd{pby4Qc` z^YOPl(z&4cZTh-b2JcqO-otcxBRm0K;KI@rn7!hrCx^WoK79*$(-ZIgTjA}WL2nCR z@`863V8wXp#n1oDYvm1S0=&TGq$%*HJm_v~SKbPL@4xBy-~8HteD3F8^f%A>@#W8X z*o$xZ9h(oI|8{xf=mdCyXu}lvJvH}J&%Eu!7vKG1Pd@trXT9|8PyN`wR{BP`we4roczApE?kN3~HDEx(s zp78>CBQya{ql`4FHM`X;)J-EuG3{~uQ&;$}`PZ|b z{mb+I=kLAUVfUfu{_^*K-TBHpe({V4z3Cp$eBZa^jkO8z0x^Xt@FVu#mw)GXn0KCi zt@zsq1)-;{yy3iWU46f|eDa0+esQ1A{m)m=lQ$kb0j8R^ln7UwfPIjZ>p_-NJr-(v zM4Ix_rC65Wx;fM!ZMMnJXyNWQyvH8pyRUawJ~k{#g%v^=-R8IFet!`$FfM_gqWeP<&1K!`v^v_sSpi!9@Rn3Gf0Tgeh?L z#UGD9@0x$V>Q%d5lYZlSKK1U4!)GPB{KJ0zxR?BEuU=eRmNzb#051?Qm;%4;aW}r( zec3;M`U$VQ$i3w3*CB`5hc-X)kRK;scb_+Z=?7kRD{r(phJmYIu=e~UHFQtQDeS4!M{3zOZLg^s&q_>2A zY@he?OF=5=)W*3!AC6@Dfc&vSdJ5%xohBE}cMOeBS_Pv;_y|hHu?la_$ObdF9{jxi z6Y08_9@==?wJ-iI{3^SB8Fl%E58QVncK1(Qiry@59GL(wkcKuj#os>4`VjMF=}kA? z?ZJQj@@v&{nzgk_kMrR6L;Tz z=QS%2U;Z!f-)HLQskO^Sdv{!V=niM0Vg!uuIf8uFy)>k+=L z)AhiB$FxngK8H`*h)mi@K)se`ox1+2qL%wspOSRCUsXbH8(m#rUnkjT3v^Cl5NGMdQ7C! z&}|UM0k36~silltG3qVGOc$V}oe*jwK{~9O@vtErjWy|z2~&aY4E=I{Z0f=7re5C8 z)F&REK~Rp%tAd8(S@|wyYVi!2IvIx)QsxAS!{laNRis+1V#l{IHPOzXdLpXj`UIEK zQk*7gv^ey=Q89~>XugRgVJ|DB#5g*fn7V(vsh74h_2ygQ8Jbg)$#jk>+@(x?#wL)Y zl@;ZVNDR2L8!i|&S~TU>7N!PF{UJBv@jgxE_-2`~LS8aeB883uf$a14kWO$lFp~{& z!Je2pIA-eQ+qN-vc=OH23`?m+g(VgKE@bLK_Dq?Y9#pf9qNO$K%`Am6f~Z9cBYO)| z6OGX@CUS)&ALu-)X4CafkC9v5TAWE0l1^5DygX*&SV2fn=IY+Q1cLDq527ro5O8yM5IPx@p$T z04j)niOhs;F-{~*6N^BTQQf`W)W6%#)SGu8nFOzjNmgXUyOgQV*r?X}`KAq3?220s zGYt{0bc?REm8r#4KNrRVQFJO{q3VMQv@yDnlw2Z+4y#%kEyIa~rBu5itTtX2{o74_ z>vpEzoJ0c>S~Ft4z^1gjlBu~fHLA%vZ5Fy3FBTn1m7y%ebu!de69}&JB|BDU`va}e ztCAyKFZSiG*ONn<+6|Aw7}loh1K13g`uNz?o!d?Q+wDxfd07-Wn!xcqt8#ZKQ=h3( zP0B$*6k=S&hb0))Y>~p27@6$%H*fZ+cAqocdQ+C_T~*Ov;OFHEt zQra1%TTTMY#tS)*==jk-+iy+p|6jXw@b0Vsxa+~o*Pf{l{=eYdSrz~^=@(9JB?6lC z1%ia9PjlLIlWKfnZOi+{ThcWz5LFys*jXf_XY%|4!Nslb8!ytqOF<92LvKv3dW=~0 zc#iY9J7fA)Kq@l+`|0gE7Vynx1zcAK&(AxXjivtZ&hhOX7=z5nQvdMY)SXJqj6q>6 zIwlU-@}?Bd)7kLRRz|>MMw>vT#2{_EPCQ3vx<|7JI}}Vfkvc^#?KT7BDIj0+-`j~tm*tlS_%nz5(Y%1=&(@k`%3)72CH{(@wa+CLV z=JHDxTlcqEhNqe5f2N6J+V~x#{fLkId`jAn$T4$G?mB}(c*$7*6D~e4vUj*+SE4Zp z3Palx`lR||wU9#w!&cNDr4<^AbXy3W(NR!(Sy2&2uG9)OHCyZ?TxKVNTKe$bjlIU+@SfWa9oX~nJuf+U%^qt{Y|rl9pE=|mzH#3z z2j0B<6}!FNkJx=+>DRlywCftc8$j*4`_7wozWLync0OTe8dMPY?#f$NE?W^+?ziKX z9q-z4`3_~r1qYt6{BuxO;Q7m!F0UO(9}M;jb9_mMSIiyfn+3sF}*dh9v9; zUObsbp;*N@e0VCMvUrKgse}aEf`p>Qfnulm+DID=@~ErV^#t7P5H+1F$9Fw*DgkCB zTLZ}+o@zyMRNP0wp=KUYM{x=q&}R79^>tJpEM+Ez)j zu6bodO);sq4~oBLtVlbA*7|$iJC%^LM6;OUxS$t}m6dpv%aetWLE-IauiUj1Bv~sA zwh`7T05 zg4z4OGYKkE4;x9_qoUmwVT0_sPD<~X6yHsB3uX<^R0u_LqfFDSR=9S`z+=M7%2a{~lpv+lkPhZ_gCrIjdL}0AWuA~KQAkmQky6Pv1UI3DE2Lhj?Y?O$fk*0@pdb%LJT=Tjv#E@O z)_O{pf}?|?(}4_574pq&QPQ~;W-5UZO{3T`gfb|u-XE6PX4%B>lnWd75KF{lOyl%+ z&V_ka+x3B|M4St`M6#DDcTzBihoN$0L}-~%(18ufV-eadMYC`ep=*S!h~@Ehs8y5c zT#N79($cqP5=a4yM0&6%B$%iQQd6RtcqSdSL6~OML81lB!lV(esz$jQiFbPyTnx4L%%;Q;g-|u8(KVu2 zkd;Xx!spsWN{Z4AQ!%=zA25|(E*_E*XwM@iZJ6LXSVbfvLTmM#@9@ZmM5Q_lm4w z5Yr}{8a!fQwpAGnt7IX|G@-s?)@=bYN^FnDyIv{ZbR0KS@mC)_m1x+eQB-NKl4-;m z5=0r%a4#mOdzPc-^jbcr^&^8`D@UNhp5IO->T&^;;Vk#LViRw6Ytg2j4^)Cn766wf z+09_NBB)2)O0?VY<(cAjbdVX@^&(G0jUmE;D%u=a+d>`27O0-2_uXoe9hT%$U@U!O z+6Ji7+!Osl6BW2@q!EWmYN#i8vSPSe3Y<2ka8M5#%+gjJS^1}_gzowzD7}piTHUsw zr|9TV;*+!h&dZdDYgO7wg&R2m-OKiOe0C-gGhDVTC2%^#(x5YHtXLoA;{(>|g#00F z<3k5yR5k0@cE4jPQEGcb(G{5h&t>_VpN;phPAP^rR4)bR6Qe9@CLt9n0aCv`(U}B8 zFuJRwc(_BRn@zmkFf4FeArSmTS@hXHDAVjTvSBAL?S0@>LK`KCWHch=)VN=zqPiA! z?HE*UH)~R+&`o0QPz8%*cBp83T2l#CYA^yy4173g<vOOHpE}q*g5^UJe@&k!aGxepR<3K|~gIe10mCQOQ=rh7zrA zwTh%mg;JtJC*c4o)KWbwi-kO_UX_Ykx6bT7XDY!)(jDCa+p-RABq3W)Xm)C(Abm7R zXGjF>xw>#eFZ!V_wWBnZDCLEEB#}jkuFI!`INygz!A~lY+91m)IhL-DJS?L?S_Rqv z`>6!D(g@2`s}^d|4HcAd*QK%mrL93;fZ`gJN22j^5pUMeY;FIoQ;A%O8D;{hT2w?@ z9?3ZY@Jw3mM!Vhk{@p{6EMwy)# zOeJCisRr8&hNa5Qq#Pj}1$Ej&o$rjqHr4JZ36q2*s^;m!K6)yVbw)WaTFvGvDnW(Z zQm)CTaVV8i4Wk&Val;mbH_CYrC4~LcY1b^&GXyONOgOjZB$i}D$&g>o$6QHy{16fUW)1obS0c*sh}-b5t2wXT{7H-_J4XR zQJ@KyD1?w!Dbr414&3r_j;G>?)~_3pOw&$#%_^uCjgj{L=@MlQI8X%5N|-(0^3%D5 zpxJGVi3}^XIDyt07DqTWgu@41$=>%n@bM6y;{)ROBU4jF+ft_)YK74Xfp_vQZntTj zXc#fV9LWjaa!EJd$g(+xG4@WkF(wK=-hu(#i(4pDZFF;-+^$t532jQApKH|eI1;N^ zU4++KEA6S`MT8@BLd|PgB?d64CI}`HPwS2#WjQ|KMr>ZiDqXt4}HQ*S?@u_*$ zuBtKaKyJ2#Qf(kQA&ffkyJ;IW8zuTupD0Qt$d5L9a?sUrQicZNsFn?exEAIxzKp_k z?!arN5@f6!YT{T8$$4C6T!5r)4V{QO>>A)(DpM*MOnXpNd?bh;xNs`r$IF%(QEizF zwNggCN#{dO10+^+2}CxndWP+wslFXx>HdM0sRUE(3mAio8eFDIe2K4=oI1z3ekteB zd5@_0AVpj?_(q9anJt4fSJqRlQQb@qRHt0a4I5I&ObPXTnJA<+DVa?}VBUx^b#3+N zw2h?W_X*o8*{!Onp&17bTDc%mYQwmRg1xI=8pgtk)G@1YtTh{3Ip?0`UkA`rqjW7LtDlz}P zxP9-Noc}M~ed$1d@2_^f0sOMG^qjLc*2eL#zl$xOS@w`QBhSc|w6_Z!xs4-2i$v}w z&o6M4K6!r8bFh;l;S1b#+KSrqE))r$okowtc5_i+V`BrJG8Voy`|ez^aO|O*4tVZx zco2h77~M7&UbABoBGw0B!7D0!CtGWDvm_4~d%@>e5G#*jvu-UxhX_PS=iip&3xy>|g~ zT)*lB>)q8=-PJ%Kxj+cHm!6QM5_+WFH z2BXI#yO=;O&7u5k56mC&K)XTg=Vb%!W%`e`^AKi!&{^)k z2)R5h^26PG;1E*CKJC3nELWTc)RzWSEt{LNW}Pf8hLd7Bz!J}x)rbh}b!SA>V22NX z?=hE&vUOM26*WQHrYi|_AVGkn{s{*^;It{iNg-tEH1arQ)28PaXHt7FfX`U znOUWATtqxlPILsD^OAg*l#Aw27DBFbPqrP*OZ_hR%^+vJDa4>uN{}%lPi*t zFX2C|`{nZZlCF0tnGPIeQ|eS7%o3+9w(nX=Pr^oMnT&l&D#$3~V_ikA-N8o)^w8@H zn)NHNsMgV8(uRVC1(%iK%r17_McE#WsnWP!&f^jrJCVrA9Y}6iGmv_`dnap%3x95P znObLtn^7fC`HHQeQ?xU06JRu04p_0jy!E%1+{jlw(YCGDSf%|>M z{=FBUx!?1)A8p~oHQg_V|Fd+zO*Sd71je|qPJ3zw!;g{^UIGNTB8XTYgN}3mw6`Q7 zKZ}l-#R^Zmz=s6MC@Q)f2za*FBx9I{s)GVc=99Rar**VB3dg1-At2KAp&gMBDtH4_ z!|F^&9cR+Ral38j`8kZ`sAefDa|;p5LU2LAO)>_TJZ{^?3KOi07urO7I9T)vL;^P; z^C;_pTv6RANH9{Ut(E^D2WLWsW%a6~?VsA>w%)e2yz9UIhU=ey{iTP0d-(Rl|Mjr5 z{TGMO^}o9Ip=-Z%?VGRluNAKC9sK&iw;lKgpLKAw|2z9{+MgV~?x=Qjd%w8K2q%)FjH;;bfr0i%d=nO~fd}V|^lVW~9vq8ZnDw>=XXCl|-|e)3WtKp7Tq!sA~2# z#|Njogf#7Gv1o>>HeSG$oHy(uN2|k4rCYEtC~k-o7!H#;y6}sJ+?T4OcHeU7Md&OV zIS}6^8iDzouU_fswDqyq4rsAEEgInVv*^chQ=9og1Wjj^WJFZ3S=*772s!!wjU~Qs z=TEk>7vIJ+|9ho_kVthBHx=2h=O?XkZlMkMZei?Zs!6-opOglg0=gbZnMyo|UP<)i zkt*pve8;&fG+-mh7c}$^r~@PUG^dUz8@f}*qY`8knjim{D~S!>)$3OhjR?!zPIm7W>%F>H<#LK%#j4XCCfdZP4=7`J1wV#9wXP za$+T+TK){h+k9RR5o0zf*#bWFeZF1snLzD3IjUi~?l_9E=JD-~B|c+gi6>SP!kn>v zD1#*oB)2ouGTYH2ltQan6}F2w&1Eqy3-g^e99^GpEHT?yVwy_$eLinQok2wS^`Rh< zHI!IDdLb zez%&_d70^Pvo73P;DbYDrDFpLd1DD_V+nC30U|00ndLwhmZlTRq0~l4V|P%|Oe+>IqZX=8~`TuGEV#c4IS2rJfDw2Ya;^71Um*=!kPx1+{NULc2E zF58il=FtzP66!cx8p#bUVi~On!W!#7O|uy(*JQ(l*B!mb3Z{b363*r>rEfVUZTHc_#)J*jWnoOU^*Z zR7X*l;S0G44M#nsfzJt7w|Dk7mRQ{pDI4h6*;vQ+N&;27Ex9t5Gm_kRj~pkP~eFUQd-ogtZT{1!0<2;i@Y2yQdW)>c?6IS zs?lSAwz0&YZY;67pjO(xKM)E5KdwyGay#edyF#x>D@-lnCW)wK^&XZ;Q*4mPkNw+~ z4udc-s7EAW;`iMAbcTa-bgKnUlhitwX_a%OIW=i~v#^uiIGSYgZD}XxfQK z_6$n!#;}aUDp_v(y*h2$qz~?Vhq{cYx-wzMgBz=xLqMk%=?Xx{I6%IwEP@QXrH7K;JrH}>GrTh$M36)>q z6+lb2Q9vQBDRyq)hn8Qs!P zu$%vQ>-pFJ;pW?Ke$DpZ-PCS=>ehGMc<;5Zy8hKSzUM}G13mfPYd?GaZQGx8{GsEY zyZ*_?uRm6A{rvH#9Ojo=S2_>hbMn4J|ETbk9(=2Glmn49XVuS`!7fZ1w_r8M$qY* zcq6KrNro>DI096nERC|YMxU4Kf;No|wvlSQ);ZUh*h0Hd=zF6CE8&f?n1!W^z-0VR zu|CRE7#&*@g^yGseL4HTk+v}d4Y5(RWJfWcFrJ=KtO{c|ex(WsOOm8W<4FzGwOj-> zS8Y5xf2$JKC6t`$&6{jL9CKcj>!~3}EU=e2^E+j0fY0zA>5fT{0IPAJ4Bmx>K>&z*p(xhynNSCt3 znKK!NS$;6WhI6a$=x$o=;pi(;K|bGUkoH`d6q!9ce}d zon>IUEu=yK@;ryaC2j2UQ_ib{C;^)UNg>!K%Q>l6 z*U527ua`iYrs06CWHXG?0QoJ`RKO?KQm7@^<;i~Ds4tL=qYdaKH@;@XcA2bnx>%E- zB+%LFbC)7k8c+U1DyXqj4=a!HxS|hPDN}YthJ{OY4rH#BN3+U+g-x^27o}O+0gfL} z1skL#=f|cww^f|(7!*3PL$swdj9kx15J2VCRer#-jiI^Hc)XVix*05&sgrK9u&Tuh ziYIh?kel!{Tp`@zfQ|Hq)3V`+G}5%)`{$j!U+p!EMT73rY&VNyy$&U6nZ#mhRMaLq zI#k!g8qt)KMcQ+YUYBaL8}ocS!GyM?^Bol-6DJ;4QQAz#SrC%pByrnr1cC<^?klLf zy;QJQ$n`>pKni{-%p#nM7pai+DJYzlIunmd3?d#5^iTl||79>7wF-DAZ?OC@Frf%U}}pX{bml2?UItq;uQyXOU=xA zC;zQfunEuXWHK)rj-8lb=`gReo)85NI^t1sN+@GmRPs~C5K^}~pi;qxG^{eSuurs` z^;*6lwfmwHw+S~lgMu13=1i*8q&uk2gtP{llL(il=&U9q6}nE3i)qg}_|K`J7WNk) ze5#@-YL{*>GpOyi0}OGpddvzeI1zPhj7F+Oy(CP6S?FbkJ%N_%dA0RtUB|BGM6HFPmx<9chU4F;y5&uqNIp zk6dXK8O08>&n*nDf9cY`ir!URs}L4L1`vvC7{RcDowQV z$$w4T$ZIpE2WKZUC8j!sa>8UYAcjrT99EfiW&<0> z)t;Zpa(Fr;pY*^A27jm~r5a(!$b$PE4^8Mw$!kC|tnyvcEoN$r+#Dix-ssN3%Cd1@ zBIx9EQ$bRI$^uB?*@3wtD&*#}4$fL7i5=t+Vn)~?lCfjMv#eE0?RfH%^yMU@TrNjR zlVQVz3Ii4;RHoesFtj_ziJT@hhRwL3_3Lv#oyt$%fAYSq_diq{{}~CTZ&jqC`LtGK zhIy$o8bEQCVxW<)OsOfNC^DGkXf&ln5GN3mffYerY6$9Z;800UR0cRnDuQjIL*2YrO@ zu@UJHT8ZsyRf?tE^0Xm@uwzzRzBce7&F-X7cYZV#%p*2P%#TgQOg${lyA(B;jp9rn zWRk6F{KQfhRxLj%g-#*$j=gjZ&_XDmWVGczzgB5>AVe$Et-5aLc{&b+c|oWe^c)RL z%tQobm#2a-Jg)1a zHZwh_rh(Ok87_)s(pM3Xv@SO)cQc_;aYUrvOy|dgQ7V{;>!Oq|W#(8vDz~vNSiHAf zIukKyjqMn5flC%pPAqCz>K)tfIu|T!?MzV2GPWc-#d6c~)K(NwT%nq&f*XfcL}Wvm z@!2Fb$Nt+^g2qC$bBf2*3aC1?04?DLNF9RJ?HT z9=Iu`vTUv&m6#&s49nb9o{iJtaT`tr{T9xa`vW5IHM3=Anq39Yk<+r@?6qOsnYx8a zE!)&ZK1g5A&VM@>#F1!Di|9C~cIbI?(c{@sCvN1ki=5pv7M(UT;0DeNguAZXYVR*s zf~|Zv=a`YDwF60nKzFuzZb}CBz$xTvC3g|AxgK9?36&rnFHZ#=_BTu{Ghr zF2pO9m9yNg7GXNVDLVC0{hhP1sos9u(Q0?aGBFnFS)Aq&yHYgCZ4uGvy zACTJt6XUTlkF6*_K;w|@vNf@(1K1$gV;ung#sD^m^jZhN@C?8PwFEZ_Us(r$w=sZ6 z+y@3519-%Jz}*t%*y7kT#OMjL1 z?<73=>}O)8`Z;8XW}{7VxYIp}G zy-9@_IWcYsgusCuNsG!D$+KYt++c05QuCOKUMb~c2A%gB1C_BACssXSZ=6sHLL@2=FOs_!)GFM=hs&4 z5I`z1jt{*`S!hPF3l9cVZjsM6*+p!&VUTW$$||rraNywPm#ri!p=NMU2PQ!F7O@R( z2#ejBUMXWR<3fm7jre&p?ut#v9qf-&329Wbs%0UpOp#1PRz}07vna(-6=b`2`=boD z$amy0n~CjhES0ECsf-tT*bL$PYPD5|tx~>gLmoIR)d|Wat!XzH)`lbf=p8EwoN)fNCsOPYxtyXaO7f<7l};1HR&>uH6N6!gk7u3 z-~w;Ey+Xx~lUacftDHq3P*2J_8Mey}1+J>Asj<4uNR>Y?cMr3|QWn4fXh2+kOdLRb}c ze|&|C;&N5V14--P5!|+gj-uKvkF2Oh&#~?7i1(p|H*G1XiXFXXB|$2X;vwUOuo&fY zR7rwDP9Bk6P&~TfSw^AblDffBjMr%%ueii!aT?4_gDE)5IGmb|JWAs*V`X5xCnuIp zKyBK>WKv~w^v(K8M-imx=j!qZt+i)_H17I}81-Fap@ED+wB|0vOnxvbgNmcm{Z+a$ zv*;Riwp2$!Mof^%N0Tagsi@4Vgu$0tkT+lA94eU(CwRu(`N~vBm7^AR6{-~i1_q>r z8p8==23)m^i(+O7?mjv&Z%G|LXCA6639L8gXF~;J?P`xFbhbb08;Vd zq%`YrWvA?p$y$<^Ls-7{eJhDT<+7*1+t0|GhKppI#1>PX|Oentud z;c%5*$%Omrh!}Khb$MtN9Vr|n1y0vtx;|qAmB@wJ25jYgVOB3~uNdbh+~M7hIL+of z+)^7LH=Rux@Q|XYhKDaY@^Dfp3!t)|r8W;%yiyZp)rg(5Wt*OqI&oN?hCHCxDM?UJ9y=XT-~OYOjtQHes9w8Rs|-d3j;OhMeqkjL(#iD} znh*FTpsH)JKF(F;qt&?EndCi?1GCu|+ccgnwzBTfo9V4stcjhzK0xhq28=;gm2Wrq zpR>{dLLZ14JE`4ay9*AN(Q4gf*^Y8&rwsGia74Om+p)I4_7xbQ= zL*i-{WK3^{Ss%hjfzueZ@_lqZWXn#X`K8@|v(f>~uR}twT3C$nN!2&{TmrEm^-HZW zKqbjRDUX%>DP%ND2Ys5R_%+Nx|649B<)hHH%Ilv=#Tp!#t8FaCd=Ysuj zTuJyC*{LzE9F1LL(QpR((fd~tp6`K|*-h@$#yaL& zxEPykOi&@L_s5-br@xR)DVAu~rH_tM2@vA}RWU2ff~-|BL)d0g)9X~yYmsWRLFYtq zA(TRI!D2hn-CgVY?an||bZ|Q^fg#+Ydafu`<{Aq^)aN60ng>;P+Wj6T2f2KUSZNsK zn9(4Lid99(2BZ3<0MdJ9vuz@8$z`rPk4aNdddPgz6^>U?h8=f4$%8DvnA!9|?&pSv zH!=oYX~{-p4rbiQjjckB0SUNN$E7nTr>SfweQXSeb)hoxTs!uk{84LK`AQ_%S?gv@>nNp41R#-ewAoDs;RW zLpwDP9xe8w;@AL`%WSq;@|tA1ArFU(I0v%52CPjgCCV6w`qBTf(xK1tNylxgm?zZx z^-Ki<)fz)4X_YCXJ%NM<(#ec!lT41Kul+_UVbN$rjq_8)8WgI`obUMxlNH6lnaQ=d z8cX~F#d6jNw|BoFm1w{@9L~#GaFcKGV1DHY36vS{j6$wj)iSvNC!!iEgso_I6@T1O zD+!E%B+(_bp$sE+u6d3#1!Wvz!k`VR#g1r$m`+T;Y45&vr97r)MbqMOn3|mkMkdKir?G#oLQd8|_Sx`-()HBHfSIW5~gD91~s4b2R*G184 z8-l2@fDX1lE0quzbJi=0?$GbFLB7GToF(DTOrIw?d2CBXmK!Yi_MEEd*K;e0>cDpv zRVJ%NErQlKlqgYim>CafvMYvKS8fy&A_1Am;pz4_uO!NJ!8SVHcHGw;ai}}_ zN>&G1L1&3tk_YH05l&W#s5?ZH9avsD>sRnP+j6qml3TExLfJ;!>LP684QyPiYS2t< z?yORYb_j@?DmbOhO{-TQ3DcI1OcyPxGs%@0o=V6(TLc-MwPIdB`jz(?4ID z3KIUH5|lxr39Qj;W=gYaxi}Mvc#(rAER^rDBFGY144_3hWmFxew0fQxTDVDdLF(>2 zn$4kkChlkxLyO#QZ)l9j>I{eLZC4wevvK#|m}S5^A=;FBQzsjA=$G6g_h`?6hV*SLUGzBP_=TT;3p*X(pZ|MXKqZ zbAM7gHjgZtT~^fb0!Vr55k76(L3iT$3Z6CEAZE8xhMnTPhO1!jykb?SaTR=y?J!}H zg}n@kPqT%V!^|?}YCkdh1+S-Kd60H_+=lZ0Ii+s*tt+Udg)ilD4oD{7Quw?Bqe;0@ z&WzboCs%{5cBeNItXeT-2|>@#67;45fF)Ua%ZANUdA z*{QkiG=;kLmQ*l`3D$1FRY&#v{UMXL3zf3c!Jvs>r8#W#Vl8aVj{j z#Pf~|RhJ2a`_iym=ZmP36|Hiy)PB?XMmFKq6Q68;`7wp1RD|3A-mvYWa7L2q>Ugs)2(5+p2i$(r@;y( zU&3W=Tp!8}YC14md#_ghjy*J2aDY@3ySy0#S{uVXsDzdaaks+LEC`MP<#;Zm8#+ z%9I^ns^u%?VYOM}bSxkB>eam3m?WjexGP`=SX7lgyFBq?yfaTb`S#0FjWuu@){T0L z0;5})MhyEcS1jgaxH|=9Z5te1rEFPD2*yd>YWL--pfYMyNU70SXc?jFPgITbvJqBh zN0pXV!F)~cA{-Jf=xQo>@WE72n#KBLnh0H0&McIkNHSIpltO?gAI=0q7J}HQy)cdX zFon7&oC{XUB~V(@&k&`cJN48&ULGrkFeXMUBFtsXo9Dox9oR0WB);9B08`jULgFjM zT!)@^8)_~AtLL0Ta*PW1K{oP%U&MH);dR|Yqk!q!AccDKCst5-gO(;{RL@8_MQJ%g zuF+C7Y?Jh$pK&?_GcLopsaNQJCKWtMLkD;!X!mWK4_q>8%3WbxO+Y{w?e~eqWbtm_ z@LRHKv+aMuRil!r^U7qM8LmJ?=1S%T|poWe*seoM5My401L&uFTO$Et%KeL$A zUc*sZbDqmOV^F%UE0RUKBMLrL2t|6BH*)$Y6+C`#3RNNMG167&Kud-lq)0b94BNtM z7LhMChd}~Pbtt@m6EL2x0git+6&#m`wGLL{GnFBPA$%1C?VvQ&8WnX>ui_~T8$EeL+QvN3 zl4DR$dpx7Z?#M@HEM?i*BJYS}r*F5Nc5y;C2GpW^&c}a!DrkYlf!K;>h=eh&4AE0o zu1YY3aE3eSfEuiA-625saG{a%e|OWNgYS;I?o42N0#)_%x*mxn(w~9yCQyy7j?oe* z^Cp%HSs^%&9^3AqASiSDHs94i_?@7X(XKZN2NZ8}!L-rl<|Plv{CKs5_3H|;n_=UugDceRGvrUc^`4-2?(+T#l4oPEzvp1 zz*4pmn+3~CP`9Ho#m>j7om9~1j3s^MbkwqF#a5=ldfi+c7HCEn9FQbwMp{O%BD4f> zZnBE2IZg#ZnH$;T&Dz3@8!pJcJ2lHy-WVH9(20dOH_d1Jyboaf?DzumShIPNz_hKPiRk=HbCil7~puK$(#< z!LYeAOky*HCkmdRLryIvv&n+Zq(jaToeEk4skFJQ8+Q~Frcua#}AuE=9(FkmChtqkn*)OmN zz98E5paWLsZ2DHU6sg)CfJ$Le-{uTC0dVh3*4 z(ZCs+&h!np=FL&eDe6cO=Q>cOO(CftA68RA9__X>^Squ%E1s>1bCaXxBI|X);(QR7 zyVE>~Jp}oTup}Khc0X?=SW-!+k0$c4TlK3pf?^9Sm$wYL7uLZo7qrlEP|0S_V+A!G zj*lK&^8b&vu&opC#_t?c*MIQv`3JAt`|I5=-2Nr-%V+rd*B|$<-NRRYnfdsq)B8`8 zT|0|hNQx{_#H^W}CCw}Ywd;6XV$Xh#+AO{R>7x*lBBrEEOa-kP8Y~EMX1!s%UYnQL ziD${d6jZC5HnZNK#CE-oL*ONfE`!fpG>71MvIv{m+Pty+I|bP?3pT)6WFol4Ow?!~ z%l8BgoUSd1X+_gPt%)$|WUKuS+fU4#To5UHDiCKnvS&ItC2z92Evdym2!#gm+QL%T zT&SPv0-v3C4HaNuYFZfzX5s*^x9PCobq%AO17i|+*6_iz*?hmt)#l*YxNkC=(7_CX zn;9Az7e@7J4dP(97wf%xF%PcPKmoErsZ7a1sYdIyQkCKuIE%g<0{_91HTGV3=Y>^k z@`_gw(cCv*@d{4Uq`T0|5oCM#HNV^cc3>0dDx2g!hHWA&9+6GR2;rTHSM``teMrZp zY}f4!nwQx`uME3pKPZ8FA&40iWl&O$^7uw2#OI)%kI!bT9G!H$dDNZ;Al(X8SL&4t zx3*1sUkq&0zse@?$FfbdM`RNQgX&ZbBuf?Wn%{5KbsOytuuE)0jw<<1u>da3Nr7u} zLQ`w9+B^yuhM1@FijOcL22kcSEI*$HlAtu?CP9d`wQcgb?*cZlud)gBv22q^Hg++N zsMW?AITPALSTzTE!>R?B*n~2q%rF~PBaon}>bFTJ6Ea+jlE-z(kG;{HEebXmL_zTJ zU>bn<`#QsvrINa~O+M#?z$U#bYyx8+yEd7LkIE+OtS7pL)AakkNaH0Clvz+0gG+2e zx{Eog2`W=fI@NpwWMv!2AW7lG#!8&Zc%lr23T_d&DwpK|90oOvI#}gY*S5*0eig8Z zb(KvDAHz1Ox{qk=V!eDL$FwVr>J0EEax!NlhKgKflQHTChNDMAE<|g&;k4l-lQM5r z9KStnQ5ATgVPw&=@GLY9KoWO<@VaaE$kttKbE<%xINd>a z*oI542FjWlL;W(FXxvnS@kG1Pwu|Xo|{n&9!av!(Rz((z(he zAH#F}On6lD2~|r}O-4a2>YSeQ7EK*?wCvfw>Eix`9!&*v*az3-eQ;1BK<-H4k|f75 z#e(FOS_4qr7326mQqB{@?b`Zyo>e;mh|4@WcQ7`sertUYp#zdsZ53zWr!}6))6N zS(c%SCS$8TYz5=4H3qycQ0OI#B8AK9scaB5czL;@O+Vm5y^|-a!QEeIC@Cf$SPtu8 zVXQZYQP~tP%+1T9C|4Hmf-lY*PvOHgE|9&PiQDVEf&{$nGf9hc2KZiOt&G1Z0bx zSObulw~gIxgy1g87u={rYSg?-_WebGHw=TKc*y7t%I!M1$Q6gQMofx^?`!=@wWABX zNtENJ+zX13O#wJ-R5DsN9TZUca_8R5m!9{Pt?fq}B)hKX<)7+#%hEp=>Wwacb-_DK z;Kmc{Vg;aoBJVhJ$iy0hoYX!TL7vE;xz7?&Fkb$9?asTj)Op!1_`Ka`#UH`PeBQFz z4^)C&s(IMXjuqgit(uopa!u20mm8E?^{mk_aLeIBEMN+5gv6a*&o78>Y*t`Ddib(k z4Ui{)f!YgDJv&cM>dYV(hl&`4Agirl6iBVw$b@pY`mu0(l?qv=w`8B&=Jp^^T@+Ex%LQi@kG$|uu z)CeL2EwHewRov-V*9|41O?WtuOVg{PaWC}tW!7Z`(TNf&MhMD9)>FU+_}Q|FQ#^3YR`5X9j; zSi}=!7AM&Pi#pa_!Mz*ST$32tMhO<#alu~;@faV<%izLA=!Gavu$!+aUIS z*&jDa7;*XHbA!wTm;30jV~w9B7rnSmUaEOSi%;n4^4v@Rd#Xb}FwZ{C{QZ>u{Sx@Y zjs%4)y6mEtEQ37Q!JKlJ_5i)7&FcBlT@y1);hmCGEezMXmD%&t}~oj3ul5f4{hOi#qxJ6XE82ZiYAa zZ+!iY=YtIW|KjN0(TlJDaZa= zP(t}FVpO{A@!{C64?AXSq2zHqH!K!FZb}dzRIb2_7H3Sc?6BS|mol^9H8)N_bA^5D z=zY%?_6_iar!N9!6% zFS_lL#i9~&X1)eaT)kFa$!SitRTEl#FwTGowmjQHsuDRySQ?o2GtN!>2WxA4=5BfM zZtu1Mm!ixt1QeJ|XDUHK5)!>dxye|q*yS-$zo!MOv`o}M2txPti6xw6!BoGCu6%Fi8KFrs&1~e`y z-Pq(y6$A^|pfl=P?H&S5`)TKGPLxmOc8H*M^h!XdRqtv?j7`&O)pp^)-_r>iK$S;^gz? zY8Ukis?oxl{w!nJV1D`3RkN+5|9o|J+qHW1gQPFeqdRCL)(iD2ggGF3y^`nqGpX-S z=aj0o8k#}+*(Y9mb$D^e?JSuQH8 zvl5}A$*km_KKJ}p{@mKC9&r7NWn@6S2A)LYSydtOWway{%{jP6^rn+qcR=H1Mkth| zkX*X{Cjo6>jkR_3wzaiAv+G~HfWN&#KIrc0CobVEUH@m-hLgHJ**52lS*6_Oa59sO zjF|@;fd-={Ag)swH^$_oKt@JA?*&cJ?B|>}`z==|HD1g0ALJGS*WVxub#(d(0M1KZ z0-F8PYr|=KP8-x8^a8W(^@C{g3Jimw+wJueFNdce544{7(NC?d^%6hYAklFD^y7fa zGe7#twN>8dM;j#69iH9-s_Y1XK(lR2H9iX(-WZYYzX}Ewe>yV zw;SY11s(V%(01mxKeo2EXZG8R*KC_yZ+1^_Ea5C)_J3L%PU^QCTs?v*`*_uC>*zmT zeag0u-#+D>WRruwO^&{n`~Szbw5?m8fAT{o`!`>C;|FdW9`}ykdh}`6Uv>D~hnZ_% ze(;+IclN(>?~nF~-EZ0X;7)D(`?vRiz%zWk=4JQfTU*=K=g3=I*AKV%_g{Iqe|v)! zZTfeE**yY>MS{#}3ASbl%s%b#w$eQ_1|n%noJCEmMF;@!Kw!Lm5T`{aOdHO{>y z!gv4j^7|ir+1fZ?bWdF3%sq*7_x1)W>J;bw-hDOF-6h7~d-?MF?^A0dec`?866t3? ziF9X^we|xpay7=CCA#1FyXE(Hy=85TFSsY%+Pa=%yuR$Ew>Ma)rw~`8#nlj(*naEG z>c8c#jqve%mE~J}$&+vK==R(vuqI{I(K+JjQKOM%4`yGIhzku;@lYPyG7!Q}|-th~|@4xV-r^EP^_xOT)r6tDCc+%L{Zf}sd zD8)FjqRG{7@!AsG+rRbf`?s%+@J08yCBplLpMy;{NGZbm_g7aVJy>G=+0S2of7{E} zM*6aQ>=Nk%6VL|xsubxZ6A-D6=;~-eSc|sdvx7;8e&DbOg3czOp9z+D-=eLJ_x3$z ziT8mCXoJmJiub+==t`VtL(xyZbcyq)%(Zd8=pKEc2kdTA*eAvLzyx$9(z6NZKmGL* z>5t!D8|e%0QA?x`Oh6m#_)?^+rRmkZ^lSq9k8fRK{846Yj4!xHUYvk7*e#|I(+TLx z*SN&?BcFQqeYQ5j$L|%FZ}EW%2$V3j4TrH&va2G#HbuswHwn8)7v!pFx&L$^FK!6H;7(jH_4Q45xgQ33HUV9U@oWP6o-?H1 zd*kUaUfgxR<^}gYcZu=7!Ts>I6*W=S<0@>jKwxl=HqBQpZrT?zK?VoO*-C{G1d>}; z>WVF-7|#axD-oVeKi`#EzQyl?pAO+u-s0o;KIej|uWhnxPZ6fO|Eo7!XEV@u3QLf0 zeD2yHUv}@ammu#O*AH%Qu+L9HUNWv<`BKm3p>KQ667RQv{MvYL-}{$Ky!Vak`x)YpDLjUI1mtep7 zt!smQ(Y^m=3HH8a$nNb84k=Qw4@^Q=zSgrz=-&I5NWbZ&Ya@N(y;m%eJ}?PwP;@0l znodGjVmzCKzTs?S`i8Gt7vqZmzq56Gdbqpy)!RGIu&w|9=%2n~|6Xp%rh5JJwl^6| zuI}9zD2-1?od+!9S3S{=C;b+<^z|pvlD7K9CLMZ7q1l9beD?eX-SBdD)COmsm$9Rs zItDLceH3W=mjD4QMv#$bY;_-?F)pb_SCF2TO?XH$69}WimObQBG811COn#9a^(c*o zY1~XuKaXcea+8ewGMy#njdFra6T8$D9-7QlpAJB3+j7)54YCyLIBmCWEmU*_6oAib zWT~$X6vhp4)F|7zQB9eGy!#VFj&;33mWK;aSD5P8WwT&Op=+1oJBb~u>oMuinhvN) zubte>p2e`l+nY?1kIWGPxALE(BW`dRaG4`Mxw!Z!IpBu@EZM_h-0>Xq0l!;P!JkFH zgR!UgJDj*{2_*|;RMv6*PK6r!BdFaGC9#&E(;3wxveCR;=o4m>erUf7;e0vB*Fo-u zT2?6aba6sJG&{Rf88C4Uc5Nw&L<=O7HwxXw-JDsbS=v%cXoU-49=_YQD%#zFl0(Wc zfq-1>mE`VdXi#g>iQ#3_m!$rqd+^x=`1tlFi@rzaci2BmzuRERd70l`SS){(obPPE zwsgGDXtju{SZ%d3Pf6SKfHxw~vNyum(^7h4RWQkFTf57E6tgHEPLQe=-?>XM2$sxL zdpfG?v0EZ_iGFx*v{5)YO696X1qBA` zMtGx6GgJQ#kz|*_(B+1s)>N6#Y8*(i1=Flg%In0F!N460OLG6<9&|?TH@7!g1U@=% zL@)Ek4Q@l1J$aK$+RK-ho32lv3`jZS_1?d``zX0q9D(RI;99R*KbGwkF9hXsmH};N zvaPPwYscOMe8w_@=EJ*#S3Qw#4E^@d0)IS>(2h-_=y9e5kXt;kLb$* zMbF~o55efu#uACWh8E^(U4*zJ5Ofa{Lf=>7JS7(Z8EiOcb`1_>HMJZ%`|#nbMF<^% zvQltl&uG>`6p-yA>tSjXc(#JH-AS?R)!W371q%+PGPKIw1_%@wa*L$R7}{M@jRd6ZXafYfsM7i(*}e?+)lq6N8GD4Hsal_rMgXKog9PQfHN18EYqFZN`QN_KFF?0 zjasyM`o&8$py_|UHky>tvPqD`@#z<(jsCN>+1tbpQB!uGlG)c|+f32F-$^P1=_?pk#qY`)Q;9;Oaa3wfgc!CebF$CIDxc!g=?z1*dKb zP<~W^V=J%jH0f6JBaDM;Ug0n$vhI2XqXqMja zduyY)pE|S&^C&oVfv)AK@?X~0b;+o*0fWdsb%4g@sPemOYrJn%*?{}woc7cA_^!1z zJus?l5|?p&Y6E4H#BZ@qhahu!(~?f<_0E8E|-_oCeo?f%y8n|EKk z`vv=-ebhVp?4#%2_|Y3*ccXvfb8kF${1?aXIR3Wd*TNcyjFn*M4f}8+ZJJS0B_4UViK2 zPX74hXHLH9Bs}3xUU>6wZ~p4`%eN193;VtOH|+n1{pare`QF>Fzy12RUQe!9ufOQ< zLx;b0_~u)`bL$6hz3$c*-oo~hClT(vVEb>j-?{y!{dZqRPx#DTdFSyt(E}lKqmZvN z{-7Z<{KXjk8=t>=u40KW?(tq1$?hM`?&d) z@A;S4eD0T4&k>7;S*>tFJYqys1a*NfsJ;HTYd-hi*L?18*2MRFXV1;dZgtLA8M@mp zvdr9!iWl%5fAN~nebJiFt={w1-Q^Xl&%3f);cGsJt)2q~6Gn7oPQ?icir7zj%!T$I z@Mq7>>-|zxiR>x}o%iX=m{Ko1ck(>o)i1uxuP^>?^?9Itb-O4?)*>EH2fRWwE`A=n z=5x%N&(UYkU2Wf2ulae!HJ>Bbe2%)tZ1VoD^rOF9>Ali_e|630er3()-g)-i?8<&s zUGq6%&F3m>K3Bf}=UcOjee$2;-)H#dSK93#*L?0nYd-f6Yd-h)Yd-fs)_m@RtLHR% zSOKMkK*9EUxgx0bi@ANSzwrNM@6E#`*{=G)uIj4ps_w3CkkE>@_f%kuc6LQX#>#*p zBDaWKV#&0o#B*9x%pW#@Gy- zK?p3wB7p_SfUy8!WWacCR;JWjwO(e^(lgAfKYFRS=bm%Vz2~myoZpt^-neDCH*8t% z^;?#E-InD(d&_dK-Ll+kPM{s;gBPCH-f_sXy1y|^w-Ll;K zwk-E=w=DNFTbBFjEz7-k%W^-pWx4lkS?(v-Up)A>tABj;N3Z@b2g?KY;FVY2bM&>BKXmy!FMq{l{_^Yg ze|`VE_P=t!yZ`LoNA~{t75~Z?oP6_1c2Yih+0nncHN8dNx^nZqN7@l|^P9lQ{x9CV zaq|AF2YYWlOb(eV-+0x!ieLZB*Tw5^xb~aZ{^_;9a;58n6( zH@^J%^Nv2Y_m-otUDa^u_xFEe|GU?cyr3=YzxfjKf`uL^09w5x1$^1qKh+ykw~}RS zmG1lSSdA%pt|;CDt;AkO?lcEs(Qyc+j}$m)?`TZ{i}e_jy9I(W^rTaWJ3*lgFuct+ zK+i6D?FKzHeJ7C|RJ!q=>;L!?{D7D*^q+GU<47%fs45k+t0e5&RzPw zohd%LGsW+&DFSvfT4HsGpbDJQn&EPNQ7U7td8^ttt!`Z*r&PIU5e2e!?E^bg{Nm0O zzp$q8OJ22Jp=k9R6SJ5iYIQo|(Sr?-9Vm9^@$)-N^>aH@ zynjtGjntE)C`YuwF^xfsp$YFAOU{NzV>rudPaDSmiOp%atL ztKv1k$GG-F6~?UvujCD9nbmnrN2(2^t-AtJwaLT(xHH9X?M(3>&MEXA%>2DOQ~cCg zDlJ+BmR59Ui51Lw2HdZt<<7{jmFg1`tuS(l%RA+6cY)`xIM|uu^3D|dYlhWL=gQ~|A( z6-dzM6!e-UDcb9H6h^Iq)#_mrMe9kWGNE&|flCbAvsPTemcVbiN|RJ;5yO*L zt2gagKB8)^qjKz2!%)#$!ft7tuU64s@5W4e%T{n}b8 zR^>&F9;6h8;hso}NUg+&tr>3=sbHRxMT2sS;AV0j)0cj8XNuogQ`CgzFyqNC9tBeo zX=vp^z3vPpJPwz&A=YCFJ;Lp2HJwck|9C@DQ`Bs3QcXIWkL+a<2bc^sJOJqIQnRqA zN>jt97FaZD-TKa*DZXQ8if><2RIzSA40+2NdGfSbESIQsG4l$&vW-b%xnU!^li=mb zvP$0km7OU*v@^xOTT{?e)&-ujUT&J*{iPpz zp`GEMhVSc^vqiZIGUrmdSVqKz<{Ptl$wAG@tiG5;yst2LP8JCg?@2G;*vAeO|7B+$ ze{@bU-eE5J)ip&-LOw?zVLTQBS+DUbt7h)li)u4601;#;)5uk~gWXPp&-{s(-S2$Ryp)Ky_*X>O4cQzEZh|J1y zn}I6@+0Zmofm&5B=42gYVy@?2-(cr%POyFSn!PiHwKK)!oMJpT3QfWoG0l{Ma<3c= zojjgKjR-3Dd4SihOE3Xf%arI$f!}_P%j;~#R{U!F& z4_>+5e#_vP!t{+suHdv$WZ_Rj0?xOx4Ge@#C5*^|F_ zlAe@KKJ&&0Zv6e@-@fr>;B5cSf^UwJ-G6X$8S0M{i7dTpZeG5{nbqYnN=!D z5=>oQgZoLFeIy0GT!j6Ur&%qGipN71;|atDD@ZFDM!+x><#MswK#7^3TQtEnDVB`L%y15jHANK?8NqLsLpOj6J-4j4 z;Ym_Rr9yFHtA)vcm0Hbb)X^x==WIsf zp^PwH?%R=h z{Wq^E+I<64!kKNC@)=%VHc>yNJ)sG~^CZK^qk@a^T*^dTQ@Zr8*A%6)%y1MeSBgpB ztJu<*E%jvvnPQGvE{}>(W5gHj0s-LA_tqB~5?Nb7g_2Fkf-o)By@ouiqN-MIFC9i4 z8=Z<;pVt>+)#l>8^#Mo1Jij#$ct13-az<9!DO{er8QKSukVUcRi1oBjVDq90?R{)5 z6*ft^X(BacImX63ZfE_9P^1c`Ez^iUX)0ZY=SZ>Mj^q7*yrw|d5{O=;WeIq7sbk(1 zL>3|&QI59|w=>lzu*j>udfU^SgX1+t!LJ4-o^Q>9Xj-h^nG}zd+Dj7CS%8G$2H96^r`9uaA+~Pv?n$@>s4tICw2`M{|FqzhFk55~S#o%*uAW|1YFVm5gqW{GO*DLRi5 zRE%NlKzD|tZjq5~3Y7{r*M|ivG40VE3LUE~Q*%5|x666#+Imq-nHJcrphyZQ45zDDL)w?tJd!e_EXa1$ z#dw0NQ;apxG-NQ~(qgr4TE2;e4&hWWBa9E%<{`#up)|=DsX(Qrrfn{!9Wq+F{>&Z< zkl_1zG#A@KtLiVIE8n-~5&E6^ocD(S>CF?yw2KI(f-RQOp1fr3;zU4LWmuhX6-Pd( zt|@%inZfw73Omc8Uuh^}$t!lCu-&T_aY1d3P$F$l=RP%e_P=050TOv}E=M?2bL(hpy%R)@elIe7Ouh1A#lc9TU0vbrpLrLw>au+c|bQ)W@KZLQl@2py>jz&4$me{D^X z3^8H^u3l4+pxj)Jh*r5F)I6(2bW7Q^QxV0X?dFRz+LdoUvzE$n>wU$-Iz>|N6C&O5 zgs?I!&KqHUn#C%>I+)^}s3*aE>*j0MJYu(8ND2eW(VGQ7r>mr@MkqPykk!n`RYkGI zd?pmkr4lsId5L(U&9xTC*v-i|hkH|gdiaO-)F3(CS<7;?c=uyIo_yJOhI)y}fnI7j?Hu$n0R&gJu(+o7H5=z~)qq?$no5Gjr$((djLewm@IsOznAzjbW{X z^6g4Z=R{2>tCLY9nk_p9-?5_DVyhXoSh#9_;;ngPzQ@l^5_U_%d{ka6s|&+g#GzB; zV@Wbm7`N%HGf8TQeEntT6N8j;H6mY*YQ0{urU{azDKS)Ql=M~!pewlv1re!5^rP{R zd)=BvT0~pJDOVn3BbxGuvv#{Tj%0!1!(yYZfMDE8IF8J&!qJx-Yl_54VlbBgYcqrz zKVz7&Yt1A&wwa(;?ew57PVn*2wd~;PdRR$`E*oaG+Lc5yOao_ZH|=IBBmg)gQOJ^{ zlQN#9pu!RlQ=5TMfM;qrNhqJ2MtdqN3cT z=e|@MpUlrG#F_2Y`j#p$12^a9{jl9>cQwmy^H$pOiYc^M(DFdStSOL~x7c~P*d5VkMe0*lE6hrXOjRi~HWi%WjAd;KyPA?; z|HztxYhp5A>VSKJI%m4F63yqHE{bE%DfiP3Fk9Vil?aOH1lND)2KPj-M3QJ}$;d+R zM339`w$<%{-Wqsv&~4PZDa(cKganU3HXIIa{?b}1c@id8Z6Fn6sy@$|QmI8SQWPPo zRqz_+s33qjZ{o7N*Shw%Htk4q1>683UCC*iod~X!s@|aJ;(oo=8bo0BnuT(sNe9m4 z+PViNdMFMXS*aSk1*?gI+(9iRBd`jmkdaKzggmn5lGCA^uzcgY&UuI-W<<1DoT(T* z0S>2>!U#P$-%a$EOnw$okk0y9k;_6_u92-%uJSvKb+EiYrMai5+?|YXET6s!l`Lo zj7mH{Kw2YVjv^%$16zNMrQCXnrqIEaFIn?2#6ho>Hj8WwiA3ONltfg-siNBrdyW?P z7D(O{d6leIB@bS)rcis_yxeYEXpyP3XNm`<{kXzu^km$jnGUQ8p%g1j5?auqu%;OL zIk?^w@G@UnjuNymSeDG0MB!t$8b&oYwCD9I+Y~U>Ir`QOMYXOmUUvzwDN~ihm_?<) z+mnz%BWYO=WFbgK+iDy>hjwETbLZo|0spMf4 zGl89h@3=)?`r9vfIQ#S$SU0^qoGlxrj$fzi6x?n|CAt_*dT~FCxdfXp@+?;|@eFLu zC)YNI32vJCsRLvT9v`I%b-9`Jlk8G_frV?S6 z1W2k1n9`&dBP&yuV@c)?zj#eiw~jinhx7kut$FNl z{$KliymbEG=KSxhwfoZf|4-}rKU*8r4(I811kOXvTW&i_y8 zs$f0+ymbEG=KR0T|No{-H>~5=9lU$*O;4!{{>)VK&i&_!BSU(}<-h9fo$dgA^TN3s zE`SFBF99Q42RSgG#b9ccwlq#6L-j^Mq#Kd#Prw(1z|e~lwC<9P7*zb}#|zj2clM%m z$)0hJA@m~Ua`ocLy#%v~F;7-(?9TMQ(y>4?P||6V(!$Txo}Hg9%|VHL@+_rieG7&O99$rWVI9@=DI&yO zh*sv%SzSs58pr6l*J2$lpVbE)h3ms5>^5IE^IkGb^s~ONbF(3O+YWpxg1xRTrE5!q2YI zhYJV{f!z&|LXLpJ%d$v;2D&@@smhjl@x?J3*BT7afu2oyaeeY}_ zHcRrn2RyLIWh)z~C?s2BUxx+qyuyoZXWbF&ZzGUzIWwZsIf4p|0U1$*f-#6hK5mSk z$e;}HRdxX1x|g8(Ji9Sy=Ud+fs{9mSl{}mmYP|%gi7|4F^Qq=>DHksyyfaa9Pb$O|k07?PS zq`(6~MeeU@&e7VtK37|gE7^{gX+ zw-hMNp<&Hp6sI9}P|IU8;FrS6T@WtP31$PP!h7l&J-Ik9JKzWMxPw{eE7|9PwwZL2z%e#!J$631U%1jEFUDlDYv%Ryb?$!C+5zkRqLqFHa<>|$p91@O81!z% z=DxY`{>0-X`1JHva1@7DFeVp*`?!#D7LU6FVX97aD`DfA69(NTYj(8&u{G3!I&`Nz zruvihOc+0b`Kokh7+c1asuZW->7SysR9)I)fY30aK;oI8b{AR^3#d{#s==k<#Kf1e zB!mll(Qnr$#bFM7#+=F-lH0sKBd~ZjpV8ap8DBLW*wQ;c>v{AXWCYvW1y=Cs*%JJx z(w25eQFoCoZ8Gh>IOg>1$GNWn%|02v$lLBWywy(kQNvr~3T#%v`&SIGVgZ7ScL(=> zyLfj{B=N`ZP(WOFxlK5PJK(KcSB=^2Xi_#iv`<97o>vgB7}DLWDk)$c0Wv&^T|${w zBKuX;6%x3c(>cAUx@0|pdi8E@as(GO`d&{RnoPOXBt?!MtC3BJav6capS>uy%c04s znu!mqf=EY$27)agx2!$1F)^Amgjc@k%E{r+9e&GUepoqt`N1z;`ROC$B)j$XC*^~;-}vDEZ>^&CZ`5zTcmJQ= zc=e?}xc;kG{*RNFUH``?-+Z*U|GC%y^5qYoynp|Hx_WgL^MC8g<+~v6|9h@|)iveX z=k9&Yt?BXa?ft;5AK&j^`J#i@uDDsBE7^&|N zc381FElm4o#B|m}Y;jmQYvpuXFrno*J97-4%9{iE_?74PN@=kmkWhocTCIc}#-=r| z<0appcGWiBZkhm^9UX%ywlZzghwoZbz#xDVsX9etG73^L>eQJo9aP3z1vOL5S3rD0 zv4k($t~9uI>6{{KV6Ej4c&e0r3azI|W-f7F9W}fFpT@LT_Qzv_sI@VxTRHxnH4mcd zw4)}qRQg8C0k8T_Ae#f(w6H}TH@T)fn#Rrg>V0ro2aw$ZAF9q;eeuRHX6M z_3Kqyi*ls}d*$Y+tOYVpHbkBrERE%|?Air-ULLm_Mq^3R^yK)bH!T;1{vP_voXq*>+m{`omY2EaR{1RfPe0CKNm){GgnD0xB;cy=0b zFXg0$5QkI7r)v3CdCh~>NfllaP-8xHWvntPNj7S;RVHZ`DubG8CS@(jsy2u}*n8ia z2UPS#H=4Hb6qmwbZO~MMrXdO%QcAjI!*CR?(->%}?$cMl_+ZVX<#@Acts#=_zNeKt zS`;=a;V{yMa;dtEaaSxmcsU&uW9icWb?&&EHgJQi*2eV$k5`KT&BG=Wp;GlvwN;43AA~Gy__2eq` zBq=h|K038Lq^PyZ#gK2-lp4n%R@WM}Jy&*7Eo3}wsvt;nARYdnYoYpZv!5%#H@Qn= z5>g4=vKb8!Az5aTRXX+l23!ONn~L9wNey3)aW))e9#?Qv}qu7}BtuZd%K z;K-G@Q14F%9dxeE?W8N2VsAk~htIDm_yN33+Ug=JHiy%tj1*#m>1!rU!qd!xFmB_Gw9+9+-Ex*v-__~mL@EXOtcFVCt82f+G%Ps{yNA$|YIE3|l0}TgN+Lg* zXC2%bwac2IS#1VsG=`Jo<(kL9n3wIA-Q^V`*0P{YX^WYRlLd`Y{HmHHwH%$#EyTy9 z%YWybBEx|1w_l>nUT@%Fb(|^VkXh2Ycs*;n0Wy@dsMJxLtksgs753*&UDaT2AeRYLFyT@5Zus@{`QkCwL{=B+@z9%ptF;L=a>d% zsZPv}O-dcM^O5e3JbN&kbyE4t&)mGR_odq``!AJw5)GWs(^+$>HCeuc`%>7kYhW^O zR_5+>+ORQMH7jJGv}rsY!wMlS)&!ge1%fTKv*FNPh%7T)lwiFxf{bD_vkJW?Tckh~ zg>25_br@B~$)P7{4H-n{O~WG5$T3j>Tddrnx}~Ops2cCHg~AAF#Cu<}w!o4clM9(5 z$qox?sSV*P|M%vyi}obGkXAq}jX@`vjZovgSFeR)x@ia?vkLaGgmI=+;D!j_s2%LD z*Vzp34|-IcHVaW|L8GdWB4F;NGNp^RWd=26)=kTKQ8M7kmG`fC;G~|(1VWS1aq?)MbD~XZ82mhu7%6BpqfiQ z>6d(u2@cocE~(=#Bb1nzM9wQJ<5(kN_%~O&! z8d*IUO-B-bV&s_Q|mq2Fy&j9)O-vsjcK_J`z7lSPS;!@hAF8@7_6i+dB>q zjxHUo>N$GMO9%EKj2r}=BHAg0p2~P9jBL$6N_qtltYc>Bv!!DwsvQ7hLjN3 zq2qj_hH8I&rcj!p6bFQE2BNv)`xmz2`yiOsC)%4*MykD}8~@qGmCY2Dt25D0fB{+TJHnBW)L%xrLU zs#qZUsp|~w@yrctT3DS7Dn$=u;K^&l*$i3p{k*OCK7t3c;cF;%$ti+hU!1RaNo+D* z*Ei0Fyb-fZOBMZv5DIpuTAGAhh4$*I5Q+=H_aCHp-?A0o2Vc-Wq34C56m<*4bqQJV zjx3>PB}OprBqTcI?tl~uVAZ#?MP%H;`nW#i@2EJ%V>Q6{@1=MD{8oG)ev;UXXY-6U zXwtS?7}XCUnMJ2+RyTyM++Rq;u018N=nmg)2-PyB0bIhFG^jbNaRvDPxo!BO)I*tu zH+(ySyX;O~BgrzMfkv}otme#}$c5oK42kJDH>QN+)e>W@l9Qp`ZpG|;HLkuPz5BUa z@qGmQa^1hxQf8}XJ#hh|zrCp}6WnBQCq1Kc$>r?UxS<(~z{7_4)ayp2JHmwX#q;+5 z-SqC~Y{mC6c9bm>-tY8nn{=>AJvj@}iR0brvZ!!}@oZJ%X`~^rQDG^BYyQf?6eGowG6XQzhqrjnMmS`a1K_1Axiify~A!V+~2%TgJgImHeuIZf?4RgAP z>P&;Etj5*d^zNIt;`^BK+^&>5VY4wFOov2Abq9;I64VTVO3D*>27){1;Zg`%Rh+8u zGZh4XPNs(K0lr_J-hFl(zQxCkE4yk9${8$XV;<@!(~JbjtGq#hXnU*y;~PDn?Ijme_<2GVDxV zK>Tdlb^LzQwM-APTJCCG0lshCitl5_vsd)GJue6Gq;R>%qpi{sAA-ym7&2fo!HkR5 zEUTEUlG{Yvr74!!i-1X2>zRN%(0a<-`8)&_c3-fVI>?JC}nX1^2Kyxx+}+J$_Uh~C=q=} z9gi!1Z6T*p%Wl^EZmm@bonXcHOVhir+ludF*1;M(khr;OOuO3DH|LcQY80(>=ylBU zqNnk8nQzskX)UJF#k|>(uvw?sT&;tE?`Lnt_c8VxFHBD(HM};jv{6Jy1a20RDZ(ah zI-xWUhiPHlD}wOREV@%-Np~iaTnhO1)4Q+Titl6AUs74*ey7}FO0?)>Y%O6V#_QsTrm#JuB(m*KETVdCa`w4Ex|&OGc_-ud~2)X{NGf zC^SVIT-DC+5M*jkCMF8YQ#q5C zoUxo%a~N)&@m);}s6jiV1y>JKhp5$34a2$BK1WXPzH%$RkC`_Vx-X!xHD<->L?6pA zS|sEMNV=|ACFapBgk?6Y&EXB>D^as#rH9GG}Evpqsp2@-$6xz z7=s5_uWjgqwy6)61I3*#%v@OtA%TEwD$?032*k=t&zBc zqEP2yQn9NBzeMIi65W|qEj{+?O+1iurml_Yb|bG3SN03|zI-dbkFnn|o=wA2hA&yn z2#Vp{WtCRtj#H5=7pB5NQ8OpF&gAn7LHq2P=}7RIuv(AU^zO^H;`^BWWV=XJjRjjW z(vB3KO)R<9X+?LM^QfC)t=Uj@Bb9=Kp{7s!?HC$nZc8o++_N?7FU+I*l*@90Pdhb??o=i8}OEA>ly zaTfvK1|)pQefA!h=floa;6PR3!zFDj)uCT=60P#k(RxL&aw$LeF@N?k7hCYdhP|S? z&o}YhwR#k#uCa25wf)Raj0ZfeQBeK`{H9h_KB1=+0wKi}hYLLz&2i4nkfomFb1UE+ z#P70sQ0zIDiudM(K$EvM+*(lTcsh7uPbrFSE?P?5Rf45oz1>sQCc(u_d(I=!ZN^Q> zffZ-?)CU>)7Uj4ZjrqJh;qo)G0b?4tq!KGm(&AxDp~tst-J$JCMh#~k;{g1ir__1V zS1}0A^Pj)6yT38sJKbS0Jw3Z4F0#8%yB+Onh!5G({m|1dk|m`eP+;x)zO^j^k8lWD zIr*K_j&#FRQ+E|Vu?qD0#4Ph=&u-A4H-Qt_>g(oL548Kdx_jRqg(hCPphNdjKLqd# zKHp#WI)vl=mn#|9@;oHu+JW+(;OF$3>VLc{#~wQ$|952Q>Uh<<^Fo`s2DEimxo!aT z3Gk^1{{5+&ZeR+9S793$PB*cbw#K*F%&&yaPEi=t7&Uj~1)AvwF+M{@T;nEc z{KV4@cndYCGl3PC^`t>C44gW>c~#Onsn%>*P|`|Te)IO?Y$ng_S=)fdu)YxO+ma+kUdYGFY^oFFunca_w=S4{7*XFtX9$U zA@T_W0dY6G%vY-l9-2PxMX}r;9`I>ef-i`50upW)EQPElyGLTJ?zgBr+Z?vBoC4{oa|`&1K)uvb&}6AVr5x!+Z9!0CVKX>bot`V!O}h?yI~ole4wVllzO*&rXENoIRF9tKS& z%4^xe8)~xv9ATNkBaU!%ja7s#2z;9u4=(iQO$V0uKibceE6e+; z(cUh@_32q2_Ne7uU`rQR#~4HlgKZ&b>5E_;=PW@0UTg=eRpnW1CC9l$!78|N`;G7Y zMgfC5K84E+2xVMxxNxwCZ3=ER_~C->r)F8!nVCLA%6x}CQmEY)(yB3}qF+4UPG-S{^!{MV1)&wmHW%1|I8`87x5*MTEtH)?ydcN}Fl} zzpG>+RK=P%;EfJ+M)Pe>P@!!HJ7_u>?5p$t8!xGsZasJNk8l3>Cx3MEWjFrd#^U^HD!{0n~4u1W>zWm|Kll@=aH}*cXH@@`Cfc)`4 zZ-4jcfni9eOQv2Zd1Haq8!e??YvT)gFs$cj(4~mlT&EN(Y5M>P96*7bCbZ$VczFi* zwRxnXq^-qimIsO+*rUP%?bE=EaE^M#Dk?w2xwZOix&%lQbT!Sp^dxxb`1SU8zj{(&IRBG)*z`+f`?a0%xl>KG9M_7K^jnHN07aOA)#ZYlR5AL zecEH-jXMkQl)K;!I}7lXyWsWb0_-rdpLQ3#Zf60WaujM)Xq|2X0ZMt&D3;tZvOk%g?0&)TBod=4lIgLNB{| zae$t-L@p=^U9s74(rHf=xD@UzD?x7pvO|w2m5PMcl{!=&*T$6>-U7b;-LKqPfTw!N z`}TLgVrKz%IJ0e|fzR4mfTw!h_V#zb{9J$?`t|7^z~BDvm+dUTQyv4Kxw8OIc?^8U z&H_B;F>vc#fPS4t7SmQVTj0fs9>-#b@xmhC&XEZ(8WS(XptukOJ%1uS?J;n3X91pa z7o6-Yz*Fvm8|MNzMXj6y94)RUA`ZB0YtcG~wQ5telN3$HWTLpUK-!&d(|hV&aD8V1 zo^lsl+gX68+y%$y0_-rkJZ&pD+F5|7+yz&67T_s&!Ig6XcBu5}cERfW|AtHd^3tuh z-~6MS%af0tWH&x~Bf9>P>pqCqp91fH^XNlI+SLzU{n9Hxe`R?1zQf+Zdk@;c_n+VY z(f#_~5A9U}@r(TP+`G&}aK&(RDvzd8QV^5rteK$!VGa6Se_}Q#^%88w08FgkvZ1W( zkLZv)UBVTjTJq*p+DYqpzg95;{+sOZtQdBIzajyg`}D?`=nWb289Jad&(B+qHDIJz zE5*%+?iikXm$8;$hgwhF`Yf6#o;DsW+ZaU^JqB+PP`?cV-r(hk9p)tBDZszJFb}qo zKwnF+L#?OdW-~7r+?h+xQP$`A5dcBsFnt2K&0rwY~eXLm-#9feDBeeNgC{uRR$U3viu*#PVO0QQI5wwj2+FF7gYCWBn7^XdA$5@XJ zy6y;T7rQn;>a|AoASw(>4Xidod9f6swaPXUsA~y!sP%LRb8*3&Dg3OUh1vYD_5IvI64HLYU|@gh4V zm3i71B&ip)h^=g4w3fh|SNYJxZGdnR=6Zd$G&FvwhnY0MM5{RWtir(TkM(8CBTt2h z#C(5506;ok<#Lrsb8%4Ov~py-a;M#kY=HtGmn`gWapCsdyS{WS!49>aR=_1}VOG<| zC70-i@^sj&N`r2ZiQoOnVJ-8wtK-Ex``8p631BMg6+FNK%e2=ave??RHb( za1Zwi9oQ*P>UKhxmb@fwBf(qO66{dxDJl=|RBtTH|47DrU zNHADSutTk!RuK81Bp8#G!jLaUb6Ds43m`fCYxsP)tZ z=*Uu}I)x%VH1wq=j#C3!dMTzZdP)@_bPsyyh-a~?y_E#YT7n&FJ>@Lm_T1~Dsg6{X zuHe^)4c03G46l+qa>32N>rNDI1TU#Ew@okf))MSc>*)?TU$&7zUQ4h;t-HLKY$HK; zEx|7DFgx7WZzDlx(+fMi0_<>Iy^RFzwFEoVdb-1H?KTofYYBF!_37RLKld(iEy0U+ z2m9Q+gtY_T3yhsP*Z(TtD|NfPKC?x$bfWv%_7% z3+rHY{=a&Y)jI#(!VwTeZ7C;+oD)S{syAhXfWAo0;EpHc0 zw`w{tzjq!!{{`pf_o}_8d#2mc{4nZIrTOh}OuWeaE(kPxag6W+L4Ya7mI^}8&KH%B zhncNVGd`7u2tD5a-#2&6zBUIp5{Kv&GEBLCC1Je%UFZudgUU;%GF7P<-9## zVt9|{{j-h;vV?kNjM;7-1i)G=|Nm>(zWP#oANc>fx42u#&ELKGGdKUq&Hwf0{AT;+ z%TNAj{~J%f_vD*So;%S_-hAUjH-7BKU%wIFkgi47gllg({`KQ;J^tEb`ItO@{m~yB z{ld{V9F30NbaZ_6hp+zqtN-)W;3{O)guneBtFkzWhs<|Hb8ZU;ZC1zwPoH_CIjrW!L}k`aiq=zg@q3UA_M7_3PJu z^x8kX_O-Wu;np|Ydgs1+@`3&8{+mI}!H@0z^}TSfw^zLM(M!Lu|Hj8|)vtZ^-rdK3 zeCfldMFW5FaR1M}ZU6XW69>DFp?gdqTk+xSG{>)7lOO%-ZOEaAlaL(!%!d4rw;@L! z48cdJ(I+FnydnSHCnMkA^v3UO z$S>%Py-jcY_NK^vy>V%y?T>6e-LLJV|GJUvKW-)2BV+lGH`@NK4f#6Q@94j5YW)ve z)%sw%D}*}wqYe3QJ{kERZpeS*$;kg;qwQbckUy&JqyN0o_P^g0xu@-;k8QO5;mxN9 zwEg=V$$o7s$sSH4bo8G#+WyrI`9|Bnx2g44wyO2P=?rN5qnq}AXhVKMdw+M+-ha0# za$kFYXVcza-h6tXy}!MY?3cEZ?BVo9M<3a=_k$bqO?&_2rq&N^RqKPvwm^G-YeW8v zPe%S9HsrtXWaPiO(bvy!$nQV;9sS0pD}HYC@q(`S^-Wj2e^cbXuK4$xs(*I#>4C2J z@J8(SZ9aa|Y4wrBFGs(&>56~5A>VYxuWoAnnXPJlF#j0nieK4~|MZiQe`vFoy>~-? z;aYa|?>0*PsZFU1l={mXrM_oV~_j-{1*rq|>yZQA64f@edgZ|m3$bAj^k&P|hc%ahb&t8jqe0}rl zPbzBh;WhpEwVS!;+GAxpe$Dy&|K9S_@vj|T0)M^q^J&w-JNKWbSD5CP|Ejllx`Qv& z6FosB^9|bYvlw!Yn0ZlVKOFs2c`3Ss7uQ9Xq8@mQ0J&gZq~{1Pp4>}ulo<1K=wmI= z&*8o;!*zoP?nB|#yTPLuqEGq`h*H$*spGRD_mk^)pT(G`BmfRzj$SZ@Za16 zkLufpg>PepL7KSsdDcf$bz=i4dX)xp6!;^}uvFI^1?DO-&V%0J$E>u>N)-)-ZxX#ZRR8x<4b@4lbbQ zDtpxWKLxL-51x+JjoT^}*+qo+Cb*TPch((sKF*&2$~=Na!fvZRT(pDo)DW}KO=^xZ z_X2TjRqPty;u={;sSo49nB*J6T#4G4RA&QDX9A>BmrAgWP9+Hh_;3Kc4D^a?t)R?7 z?>%IQt0ZBDj$jzLqf|%ooU5${R?itgb~tE>siR5_0-+6~m$%!uJEhQ;W-(-F><~7P zMw0lqn<+16@dRHu<+GV>)KMt4NuI5UXH~<3MFoI2VetgX9_P9M_))8I8yPcK1#jk~ zkEe#)&CtL!_t#$W{1>g>TmKfZw~LK#Gs`7`V{A4T81%TL!8r1#VfOkHyBh7_m38q} z#M8ifvFdSPC?k87sP5Sp?t^nv`V%J@esKQWWc>u9>@WpCGK1gm!L%A0kIr3BJgHnf z4Zk4dJ5ofT$1VkeH))#SC3W7B&g38n9Xu&wrIt6RhgMt1xL|mDrW3VEV7AUDj3VCu_i4xm%~F(Gy^W@)nkOcVEW4}`vsu?i8R<1@y(v_|Jc z{NoeaTY=xkVs@U}`jOM|`S4CaV#ASRoNtR)zqo(n@QH);%3L-BccqUfNV*Nk9+Bj} z_3C~p9`lCz$bIg^(mrTs=lc{O>vmT5_$;DN($=@F-v9Uh=A~C&`l?IE&7&VWDqsEn zt6y;CyRKk|-*NL}H_f(F|@7TYxiiFrd6=$|r>szY44BVWX_rrFl-PJ6+&0A^9E2hw5LF1FA z%!;kk-oUKPQq>y{l4>Xf0FpjP7N)6dtwE20$bf(l1ps)K0eI*$d|h`2t=N%3vT{yw z6~}+z-Jzu7fHwPxDos`YRpffMM`rGqy;6@|W3$x9k|Ndtf8G;nW@0<%vyioA{%^kcV6 zH2Q*q7{OYL(=w7(O&l9)Hg}_Ri<(y;IRNnWR9&VDr+HRk9N1nYchK zzF1C-C7iVlcz`=B3O9}5RF4Fj>QIJCF4%B?JEbd;dco@Dt$GW_{_YA_~zHHG~}br|Gb(Q!CQ06AOZ#cY5#IvqyqE{Dou zuA2yST2PS!R7j_Kq|&|mWK?4b(}oJY6LSc~Z*MCwLb9C>h`U8XaB+A>4VL#*g~b)rzcy;aOs{5q2HO$K^#x1%X`9@ zb`haeu*EXklb5VroCpZ34675a;>f297geDq)zbtLMu{cQPsrSE52cQzIn5?KV@7qa z;%CKv1hTM*gI0;j#|xfYhPuru*+Y7`{KNNT@ZJB9y)%z@+$!(>y>l1o?t!v|EzJ!V z2n=RqTi#gXMYd(jmTb$mWERJkY)jT+*^+J9P)JzAKBb+IQkFnjQnscPXrKkc(h~Nt zv<(zWT4+h3EuoYWpnZ9dj}MvTCSk7AX=y&q{NZ1gd5#{PP5Pd5^gNp74Nj9@ZCW#_ zY?X@}{u0qIRzQAswvnrY@IS?_Yern{z??dprLd=1@pK4JLCLk1X`x+d=lO}u ziyoG$nmtz&_t_W-i_;z?;`xr4Y&UiF@Eo=LpSSTrCS_VLnwnZPC|(*?=y*9h6nHlh zF^QU?4dPQzj?ISH@ zGfq!B2h3n#cBF>t#4H-@!HlMQ++XQ7`qVhqpD0z3%`WPdVmUus1@UOwqBxl{=@Q$T zWMXcteHd|8&pYjdrk1(5C+kar=uZ9-UGkCRtWfo~$wt+T6-uHza@*dzFd2ylu~M-L z63^FK0oy-lx_LzyO#(ifn8sZ;nhI%JHJt$;jX5reku|23T(e!6ip@-~$;so1dZ-?j z+0#BK6>bVr<(i_$s%W)JqeID5KakOsc5u{eMO1>VO~Avc=td9RcEycGgXs<%93&%| zO0#0pUWWB|`h(%1lxO+SfCdlR7;TXISyxF(e3PmgPA8SORcT zYuYH$B?_Y=m81JCXyHhtXm(9DsDprDlGZs$a_lHEVwI9UZufh(Pl;!{(?GLW%tXVS zZx~k?Mh$XeB%#{-@j{jbQO2M1Y9mpj$pX8`cE9eN30iR;3(JuHLC}rl{zE!m&y=1~k^Kvf8 zM)xOGg|-=HN7k zKAoDalHTb?4x+FB{%&k<%K2hnxf#eSh(#)PHmSzi^nGeT#LcCk1?kAkj2jHIinv>_jo$ zL5m_Cw~^ruD#?cPq{-0@r4q^5;k&(TUZz-zAH_tLl6 z|#@#Ic-vXWK?2WrS&HuMRc>J?7just=hsM!ukK)Z7`W1|%Gw1)i!&tmG{~y>| z`5(;x=L3nE?lM3DTej(btCnG^O{xe|G30~IPDKFOyg55FHcluRot=W6_jrZB`vD4C zc7k*Ugy6^%Z<@?RD6htW#h`XI7&9gj0&EM?NziqB1Sd+IYLUsPp8!8tNriI{|Cb0-1Yfa&R^48u||GKjoW5`A z_w)a$T*JxL#=L4N0b%4ebUvIS2dQE^Wd_0mQ5~p7m9a+EW`YSf+G;qJ5yZY!tceEBCJ0JGpn+-X(kPp0%g!UA!mkaeL{#i}u2M#NLH_=kJ}ncg`NZ zw*<}$+`N0!?v1-Q>|Vcn-R`x!*X&-sd)4lhyI1U<+`Vk~5)e_q+EsQh-W7Ja-SqB7 zyWw48_rl%tchB8DXBXdH0+|PH-nnV##+@5>uHU(C=h~fXcCOyJYUj$GD|SxqT()z` zj=N*+C_5ML2s_+Pdgr2@@D8za;m-Lx=kA=dgYPVD-?Du(IHz#q_6^(DZxLG;Zk@k% z?$$Y5_}0?qEt@xQ-n4lmIJ0xxym(XC zY9coWFL za07Ndb{#mOaSe7gb`^Fdb_I44y9~Pob1@53u!}JP({SazZRVDxO)An^()t}SU*uWF>q~35tlhkJ)7p(7_rvvT*R5T40bJp;+rPW(jZ(hA=^~Tj3wy)d1cKe#`tGBP( zzHR_Ql)6Hn*MLzGyqVO>AGdeg5{j+vjZK+e;u5<;`0+fwLz! zY+b)~-PW~R*KA$Ab=B6DTUTtI+`4S*k}Y@3+ETVI-V(OBt@PGKTjAAP~r>V>Q4 zub#Vl&MLmTv~tVJ%^+jtjVm|Yv5$gG7T2v@yK>FS)hk!6T)A?^%E^_>PWRiDOIF+! zYeiYPc;16ogcWXASxK*4^o!M^U1j--U;OjE`oE_Ju$5!SHhvpVL;Wp01@%>U66$Z_ z6x3IO6Hl|h_X<1?_2qaB>dWvb)R*8FL47g)6sVWsPloy;{7Fz>fIkuH^YJG@eIEXJ zsL#bCP%pv5P@jW`pgtQ9Lj4Uq0QH$T33ZD5p-ylgR2TO`9pMDjA$|g?gFg z@y9~7@Z(T>_+y}&_@klf_=Qk4{83Q5_#>gJ_#>dU@dKzb{&1uhK$Y-)s7?HQs15wr zpSIheCZC{t&27#UBi{hCc|Zh@T5pz#j;;ia!8q8NWYN9={*d68>vY zi}-z^7V!H(R;m4pp1z(2xWH9^A%6Ss@bEr?m{vXuGWB&;?g8c{7F!nR3A?&A6gV;}? zl3?zh+552{L-k?*4%LhO2r7a78&nVWL!|!-^|9CwpdJS^|IGd|*!Q768v7Th7h?Ym z^%2-VA^jfI1MIs{ACCPa)C<6KV`jgPeFy6K*grsh820x_zYX=F*teiQ1p7Ov561o$ z>Vv>jXJ&sc_D!e{!2Sm6{jqOAy&v{JIjo zP`9zafVzo&1?mR&WvDpzC8!woMX2lGi8r%f#l8S_1^YbIW7y}QE@PjCx`cgZruct= z*h6#u6w>RU{tWvh)SqI14)rJ4pFzC^`vlYP^`Dp?(K@AJl)q-V62H*dIat z7WN*fe~0}c)V~E!`kDQW*dIXs2KH{G?}GYu?43~m8hZ!SuVHV8`c>@rq22)A6K3{* zg}n{xSFpDteGAktV{eA~CG7X0ei3^U)a$V~Lj3}G%b3}J9(x1S&tk8K`WftXP(O{m z7V4+4--UV|_8O?4#C`|rpM!Ulnf)iQS3~_cb~)6KVW*&e6#H$c*J8f~^&=o|)a>tn z82e4Ae+u4kX8-;t*ejua5PJpG4`45c`hM(XNM8!|eb`H&z88Bj)IS36M>G5PU@wCD zhu90Dz8iZ1(&s~c7xp};@5G)9^&QxyP~Q&Tu4eYXk39$K+puRteJl1GGu`+S{!FM} z#HUcN$0txfkGoJmhmWCt79Tb7fVyIW+b*O)UKLhH!@ux$5C;l|3@4%l5_3d~K>hI$s)VJXR)VJbQsBgh5 zP~VJ~p}rC4pp}qkxKz%*VL46(0LVYcshx)sC3F>R`9Ms>z8K_s_S*WkZGf*$b z>6zkh!JY;6%@}MS{P!@}KKPXwY#*SoeegG6uzm2?W3YYj*I}@I@YiCneemDKVEf>& z!C?E~uf|~e0EO*?Uk;+2&8~L}gYAR=HU`@V|1Au*5B@3));s>27;Go}l^AR%{1xDR zeP;J^47L;gG7Pp8{!$FK5B?Giwh#Vd47LycA`G?<{z4445B>u1wm-9fJ_g$de;x+g z2Y)UG+XufCgYAPq2ZQZ{KO2MXgZ~Bw+XsIZV1=3e1cU7Z6t)lOwXl6auZ8UcdM&JX z&}(5kfnE#S3G`CfPC#Khf!+$+3G`OjKA^Y4_5r;Wwh!p7uzf&powtt)IAwNy1xrEg zU`eQLjDjj*38ZnTO)LhrfkmNS3@B%2U&o#T^%>Zcp*|gZ64V;@M5IrEDq@d^Dqs<) zRlrCy`wA9D=hWm>(*K`Jl3x7iu05*33SKoq)<`_o-*dw7vu}46?2s?oK6zt(ppNL(6bRX&y0Nu^(ACLVy z)ChJS)G+ois3Gj3P)Y0|NFNN{{tqGk z4YtW>|de(9>o8TMf`sp@&98G|34b>|DzE9KN9;Mw0i{N z{{zJT4@dle0pkCC#Q(pJ`2RfY+tBV|i2omo`2Qh@{~wI_|3QfVABgz>0f_(akNE$7 zi2r{L@&A1g|DS{Se-H8hF5>?k#Q$4}|2Gl;Zy^515&vU||JM-zuOj|mLHvIV@&7X7 z|DPlN|9{x0X7`2vCs;3^>3<;p{~6-{pCbPM3F7}-5dZ%e@&Aty|Nk3UZJ*iy5b^)N zBL4pY;{Tfw|9>Cx|34%C|0nDN(C&MP|G$g){~r% zKZ^MOTEzb!LHz$A#Q%Sa`2SB3|9=qi|22sJ{}_8Iyr1_Y{(m3h|Mw#P|0Bfz??L>3 zHRAt2z@887-i`SGU5NkRiTM8=i2vV?`2Q-z|8GP5|7FDgUqbxYuC#Q&c| z{Qp_R|DQ(u|0%@(*CGD@B;x-+L;U{<#Qz^h{Qoh;|JNe^{|Msx52JN}521B{KSk>R ze}dKlK6vXo0OZS!KSuoje#HOpL;U|<#Q*O>{QrlD|F1^;{|AWw--Y=9orwS6f%yOJ zi2tub{Qow@|8GV7{}#mmzlZq$O^EB?h}HqFMC$-=KB5dZ%U z;{PiU|GygX|639NzXkFCn-TxN3Gx3M5&vI_`2P)v|6h;z|8p4&whS z5dXg#@&DzB|4$+Q|82zozlHe!RfzvjBL06R;{R75{(m{*|Cb^De<|YsmmvPX4DtVq z5dXgr@&5}D|34q`|ML+4KNs=;C5Zo@gZTg1i2r{B@&7Xs|4$MBPZ0mRi2uik|3`@b zhlu}e#Qy`t|9!;&7UKUN;(rtIzk&E)L;T-G{I4SZR}lYq5dXIj|I65%|0Tr#O~n5V z#Qzr~{;wnce+J_Jry>4-DmLf;8sdKu@xOrhzl!+3g809T_`ihszliw1fcT$7{Ldo( z&m;cNA^y)I{?8!(rxE|B5&x$U|0fauQ;7fLi2q}V|D%ZiFGBqPWW@hZ!sh({M8yA3 zK>YuB#Qzb*|6#=cLB#(7Y|j5A;(tHle;?p~>~l*`So*VL%a30A@$yHOUw-W2$G&&$ z1II2~xpakHrB}bX`mXgSt#7S;b?t3y!?gsa?%WsbvA=ugYhZ7FwEku6cXzk(Q{XJX z`tDbD-?aY5?Psm`R{m)DsaR+yv7K3Y*!H={%B#=Z`Uh-n>%Cjg-L=;4v;2dlKU?{G zuvdHDUU&6@n+F>|-1zA7%lBT3dpBMQuN2>S`o?kmC#&CFd)w-}u%9pe82{uMiNL$k zItD`M(K*^EIlZ*lCuvpEkj`@V>TC$hKgawq1N7nEGjvz>s5`=M6!6QVr0R$a&A2DR*IPp z<8LZVjU;P1qDU1s({rz6g)l)(4Up#{z(w0*AM3NGsca(8#zOs~ln6V9s?yOKRp?>G zxrJZMW=aV+ zbSIN?k5fv75hNqtL|V$^z~MzR)GKi1oqwEL96j~w=11oiWZ#HpwVssjjZ#7ye5%!N znMx?oW%)s_WJeWleX65o0KR81aT;{7VQ+(Vs+ReLCU9GbrKx7 zEzXI=x)S!t6=J0Yq9BIDe6i_^ciNtkuj7{KhAw5gzC1~{ooJdY80n-U`Rg|CQ2X<1 zghNcNA~+xyO^a)`s>wn`wiL^3rK_RPC_HX9+UFYNRJ)2_cO{*-+cZBedJ1C0jwux>5%616qvR>p zLV=u1E8c`1UH>NXO6`v=*#WIy(5y* zi#x7X#7^8-Wdq)j#-`KblA0M+y{1l$b45GM>L3C6cr!Y;kW8l*7AUVzRT){5X}&y4 z5;?gRpbK45X~#Ma;WaCMHI-X`(A>ft8A@7=rmbF`@zGJbOcYb0TCC`4goUh8PIe($@ZIZV|Di zsG)m?jgA$~PKZc_PljW$QH^56U|nxDxJ)+1NX_sly!_C)MOUsyA_1?{GD4xQ&tFR= za^=hC^%2)gR?r_6{|^fy#>RE)+_iDAh}@%V3dz-gi^Araqm}?Zzam)~!BC zWj*qsOiA6zpenH~Un@tDn$3g8eCqrfsZd{Kn^G!L?0LvyvoRRUlBg{l{AQgMyV^`V|%lPY&1<8$^;B*)ztKNRdBc+q! zpqtmjDIyV8^WZbMX)xr(8CxZn{(f$esBy_iLsWE%BPzuTlT(GBk<55Y(TXs14X2f? z#k-@J=F+QsbBkty35@cMa)e2=C)vD2OLm?1Mv`@gQTRzNN>JcCcGr`)>8;PtEgC6- zu2;FCx5e~{gjn@Q8W}RwO(|@cmvt#mw<4o(RL+Dt>%Tv@VEf@lSq_KHM7lYtS!}b- z)B|?8Fczd>zTd0G{G(VfWw|9Azwg{aFOt~~C|gU({#=?RL7p-@rHHh*5-2xRy;&W3 zG9V6SqtM%a|JrQ!Wj)owv>{IJZni3i*-}kAU;&ZY2{a zBocCyET^OvCGa6R6K!iky~ZGD(FZeA_TOn!nc|6N$Dr z6gGl=5dG8jHgjpA7A>@kwzf6dee$vDJ>bUfb@8561M`kJP@LvCIM9h0-|I)}p?J2F zRAZ9RSI1Slp!>a9n~W7@qEue`?A)Sfi6BN=xR$GcuRsPhpXk=SWT6VaOo;Xsl`WkRqcwqTBF zap`+=i(J3nsU^fr)T@}Kcs1RSd?2umzfuv%cxc4%ZH^4ry7g3|v;FP4MOMgC$!eA9 zD0;u+X6Oh5qHhj~N+THMy3K}A&$cvAykM~QEhmQ_ zYC_Zrg{Py%BH45%1hM|UxkalbioJF=RT78h$YY7(AR^mLI28|g;~8I}30{TdAURMc zz1>I6E!b9E&}%GLCtTO7PsMsElgrBE>0oHEoKrOjU##qzs%{{+{tjf}m!qaXk|M-l zZCa1_rM5c&-+*h4f#kCc|D-4q0T7O%snT0>V438lk`yba23FW2z{it(yb>A$cRWlr zB%6KS@Y#te;mP)Eg=6OY8hnBqS9`$-C(E3tAQ)OU+x1MXT9c;O8X0WbLEV!y)nq~2 z`Q+Yfmwbzj==n7ahfGvi*OoJUjEcDZK$BtOA-U3KlQJFelPWi9d1Fd*(%H$+EgGIt zmr^7u%uz)u?W+*rwJ^&!(^FN;jFR@C!Uc8OCVgrYf63fJ%qMKV>qu#)Hk3VeX`Jy@ z^Wy|jlZ2eX=6r%q57M!YHY(sRpIZoxuAU6{)LDu@j?&Zam>ZW`A(5)u2~}>9`2iT{ z-AJHFZK0Q?id7s8)x3u-^NJK1W=&r!n#`Elu3inq68{GNLU0=O25>^^ zG3#Gmf7bdV);_;BUORvFQ>%m3hpc>J#aMa3v5y>6j@@VZgUizLHYob+|KTqWuO2J3 zHh;EHDao>8cM8o|)fkv5qcJ@c;qV~f4BASLV2e?IJ7-s8s*&wlwQjW&v$IXj?#BnT zoKJbwE~oLAhhVWkJwmagVt6bX`P887QKJcFTIq0wm{h3wxn{jhM4g5^iiSj|)zLt) zL0Ifhj!_dE2Du#Hk7NQKkE>4zK9dANgvACp48TmvBM(#HFAueDmz|pI#2qk_q93k=QZte1j~$II`@-$YINdELbu;Wql6Ipz<@jntPBcnXqMC?D z`OC>87h97ojhs|j%P9@0PA#oJj|L$O#es{yAz@K1RyBL@dvPkz*BD-Nyk#VH8m zTxx^5eJm{Y<|7naPNxd}gwY4RKJNj$h#in-v0hSKB0COQiK2JtD;7f?BcUbB{N=}C zvESdf)5Ua8n2Lh|SJJC7t*0CwWtrX(?EQ+8>>n1?PF*I_+(Z(q{ZMsO40^d~5iI|= zgQ*Fb03GZxu-Jb&Lb0(?IYhSlbOxN}(0xNIZdJ<-X2?_m;8agLQzUX>F&kF<6$4B) zkA}tm^AU<=MYlMp=Hg_6EEOjW#*}H1wCZw_&!pp*g>t=j=S-iQ6_tlkl-|> zsHi0Kd^0f=XkRVYNO%3dxDaklz$kkJEcPFdP;AoYw5>`g;p7T!aj1tGDIB-Ey+U9* z&bDkft$7-rX*}-sHBh$)u-Kc9P%KU53SuxOmAqlvsp&c|mpVzI%r~v1m0>EmFjtra z!n!uhgAVp^SnPL>P^{G^&4kGc`bZD^GyZCP5bRgoF2S{=?zmIzMRZr;Su@eEgE`>> zSnNL>q1Z6f@PIr!O4CfMlE$U1VcXw{Cm5>XnFKxUa?>5jBVG#ke4xqpVX=R|AMHg) zhB?k@iWn={&A5FyL(W0D-YqyD&gojri0p+FQQ*x^8|<0Jy^7r{8A47aMhUYX?SrZ2 zd|2$ak5KH?ONEkg!;K6}!%R>}QEg`wENl5R*XL6qCge6+;7n4eQj39p`|GgSZyljn zJ!1_zRii1@r^P@d;x}T!T!;{gMwanT>*?A!>@%wEEbVc7{N?AtV*l<4#Yzk_PIz=n z54&XA%;)P>Z;KAd9YSY%VmX@V##>{#XH<+nn86+fi~ZXp6dP#CWy|ge0frK7K5Bv! zN_m@2L{z3VDp^!5nUA+XPBLmTAo$B43X8q*2*q~eu~^YcOMyzX$LsY#(a)!FaAfMO zdrMRyS_!gXVtg?o3p=DDHUDU1u%PTEIy>U=5HEVXJuf4Up4f_Uj{p-}1N zLZBx+2p0Q|GY5Y=Fx+ImZv@p;Gv6P}8Ee{V=4*0)R1gE4;&%k#eVe&#g2usPMR5g;tc6%Vyra0-eQqq z>7WrbJSDG|=wwtU8lUD1;I;XFu-F@pP%K#%!2u&aQY{5r`4Bipto6-wA+E-}>1tH- zrMZMenY!i6fFr4w{~9dzuZ~cxKa~vj27P-FQNm2S)(^!e!Im)6d}RWh@Dw#R>klcQ zW0NWv4)=w{{^b#h%}0t15fFNvSS8L*wMcsq0nrevOgBID%0^Sn=5$}Qp)_-GP`CGi z#s0++iq%=UP#82ck3U*#$&m1;7dwZsZp&!+=P6%>08EcPo$D3(^F z^02~_L)k}F!P6yE%chNZ*BQt4x=tE?UGG%sx_26pz%;%Gi~aHuiXEmv#MPYFNHFz8 zvsccCvs^wbPFvGyLRUReHz5_Xt?V=$je;ldE-dy-M<{k0%Xs33#HQtJu9`^|gOjLO z%Z$k$9|@&gIZ{=eq6wnx#|+RDc3`nzJVLRwsE7^~VU1ui958Y!>vP3wRvh*s3BYQG zr&S=OSZSnrCnSIQHZ1n~BNV$pSoj(LKX`R-Y3rw3>gLZkU%mOLjSp=+7R0FwVV?uB z|1R7;wd(~@_=4M?-+l)6g!R8#Pp*A^%~@MrwO996p1pFvV{be5l;tlhKW*tVXCLzy zE^#HXu*tm!_twC@HE?SUG)_j>TZKpMEFCPs$Y&;Ql+#rjv_KlyOLtCQG`q;t?|-lW zgPs{U&_(7^n(rz*Yvgv&?u*RQDi|l90{nRjxakFO@W}oss_pa!lln-t)uyVe&ZKqn z$>4e?!1Wg3#3Ntt)jnZ)--a&Pgw7@Uxnr z0y%W3!3oshXEiwek?#F)#OQg0pFjwWQG9AdJx@5`mw`1+tz%EUE#S+gLN59gYO_9gJ^=4C~Clip(Py?hvC!67{WQM10 z!fudaG&!imcwgGimRveo3XNR)$ai+zDe^I>2ANjPllG~Rkv(wp#b_WKq6<-R5|2jp zu~U&XDw0j+f;4BvkKUq(M$DtZrDjjPhFqbG{sE_D+hW)0+x2*@DX|SNIMu9Ey5I4~ z$jLZV^)>rPufktGMfz^nd44fPUO4jyRB^l^O_EYR(G?BHoyB4SRHwWyt8f-2G^T)n*~;~j(ok_cAF5m&g{v$ zz|z){uXCu|1ZT|zy3GR1T}QsuFYh+NJMA`snLpsx7g#4dvOj2|-8)Ibn|{#20^ogS zFh0^m8@Sy~`y#42Qo+}$C;h;qSx;Vo*^lheo%LiNxZbQMF97pLzTWAcJmdeBmHRGj zkJq08~F@+q_8&ywJlu zZwtHypPjsL3ry57LF1vsP7Zk2{MF=xyKTCe5J)`(Q&!zB2h-h5rR4X9{NcMwrPQZe zT7<09PP(L#i%uHpIaK=Pz%8S#+-OrggRNFzq>9r==hAeyi)X1#l ztmZh7tntIlSTT$3RwJ4X<;=m57URyaz}FPDLvxngsU?HFaGUF&Yo1q5-Ib3xe> z7&7Uv1c$*|RB7~rJf+1ZMk3Hk>6u8N&4pwUFhRI%-~EJ!Ao$TJALFCP<;ID)OQfu1 z$v)AMgi^Y|8pl%uUCnj*cu;C6q-4s?6E$yx>Q!o!PCA^;SIDftt)X}Z$5UbJ>9 zIeRd??19T?7pRW-U%Stob7j(uU~#7 zFgPoJE(?siLL zXZG>f%)9emH2eMatB&mBd8gWYOB=|?jajQNvQ39voG%rg*~OV}5Bb9E_o0tFvWMrN zY8|RYe34DQ+t=b5&$Qw*?;h~#+3y2Cc2_@6x8F-(0 zc@~@bcE79O?>p|w!^6wPm!5x0I#i462MZjCU9c9{XWrfCbF<(3CXejnV^1|_K0f7i zEv_xHS%G}K;{)x?b-Fh5bo+<1-`!Up+24axjhVl_GnB`tE9<*-toQZnS28 zZW2fK^O2`6p80vZ8FPWn66ELYX3R5=BRFHOUq15`d)bjaJ^xhwP%k~U$X?5Wy%f%v zs}Gy`xO&NveLU~fGY-#~3v9n27txG)#%i4Twrs)QPaN69{Zmh$)#9U1kE8o6a^(5e zTC{DjnsnyH)cXMUzWSdJfWNP}D<=;#(WU#Ro_5H|a}E|bK)tAw=gi#u*=%0=>d)@8 zfRj%>`lzb#ia!-Yq|x{r=>l1)QuMa&q@zfy3R4I=MS@@5l4sa~E(@Jmlog z!2(Cc7j<%H=H!q3@b^gzI4K-*a{FL`gXfDnxjl35hqKZB)gKNQaI$*H$*qG0jIk|DLz!CdJo!pqY_s<`J zzn@vaN&b+N_`w1P_7`<>_Wt|T?|lmX-X-bWVKP7ORB7hq?cRSE*sw!R!uMZVPi3s` zbTAr4lfy9|?euPU2!J4F@4sLD?t|g)c}K3s^G_9L9^Qt<))(0ZxwRUNW@~WfMkKg&)QGJ~8|LXFGm$ohZ#x)lF!@Ym*K{fEAol^|nDqOnqvX$jU=HoMWraM_j zbzHNq&Nlom2>9TNuafX4}Ef zoGt140>PPcrNrpCRJQF@pp*`AR-Q>os;@zII9qJfnOHcGQ58btgMvZd?d4Rx(*$3{ z>si9)wNzPYDFRXRu__U+6uA?v&WQ?L)uOfIjY2of`z9dHd&1ASg+kOQX1WvEj9YG8 z>jm>=DQ#90W-xA!^hWeZ%c=0zXZspEr?R&W$OSspe|bRqXMOKZ2PAoq9FPmV68-NR zkarw(pmFYEl=^Q!Ac?a(Abp{`+;!Iaifnna$F*(&WDo#HLO6f7qYWiZGoxw0HYKZk zR_>f|viZAz0;MrN9*OzeN{r2RJIy{>2%YE&TrQLBR$|k>?H#b$6E!B730KJg82oms zDwq?a?Y66YS*LS_XlPI#P2E1@2CAccGwXAG(fAPuB)Ik2fZRTnnGeY2z|+7@9qC@ALs#a0tGTG=k|U!PG{TlBjSA9VB;| zQgSZj?H9U*6LIfU7l`KFACKDLcsU&#C6s{V7LJ#uCwPC+wg_7CgO64GjX;VEiqT1H zAe7=1$kM}Aoov6*sv4HwHGFySae0FGbq$m3Oz5OHott*RXVgdAZ}iV@eLf!PTgT%9 zed@nF9=&%t9)0)7@wfo#-m}M}_jcoP_Bgxafq=XF!s_z|X9N6B$79adcT1)eV!IhL z*%Bm>e8ynRK;XDykmdC8RJP2~w(e`hz2Jca3YndUY&b)Zb_%}v-u6@!LBRiX`cxDv zM|-oEVj-8P`HFd66sNr)(WY`tT5a*8cBj@?i{%702%BPllB@Ys8NU_KoI$jmKVAy4 zwKU&4;pIzwput4roes#WQ7e(52*{Z>j)GHJXWsw!&u$%z$IWed>0wJRUfQ~G>BU=5 zTmH?>8#eik>o*wuy5$}0iFjh2T)TSjS-ZEaJ$Ch~)q|Bct~~76?;N}T`Zd_KyUxzd zJNoubX9<@tJ+|Iz74TyR*`}LLcuIk2x(tG!QKMYnNEp%s8$5gOQ^uK7U7f8a3nXey`BAp>B#LU*N(S4GTSTW z4QaItC(t$vh?l@Yk z$8=+1t1&8YMYX_03UV?ZW`hOM-Y;k%r`U8P54!0_GNny(&LJPR8n^nuke*yvjxlYn zWkdop)wH8+Hq?rZg2RZ{m{8e9&#_24NM`MWeoFNdjp{IzY{{vikDd5y#-utblzY5H zQ|(HFtVg2ZF{M_ueXCqbM$O^4Jk04JS&5k_9`XSTp7y~HVt3RjJ5?dd8HRHT>9$9) z8j2m}l$bs!*T-%xQ@2Ky0ddf*`x8M~>~>>rZt52zuEJ(3LaqZ=C(`3o$0;`|(GbyX zria>owOXrK6Y$wzd(yV5s?4~Dd^mQ&X&-!2&lP=kD6JWy+xFA>b~~79hK8N0UDEq$ z*-FwH>n6cz>w`WgrV4VP?ybn`5S--hN5GNAG0W8mGwF-Pv#vgLd_#%sIEDR2#btY- z!!V|6Ogl`bakwqko_pys92d(6jHlZq$hg1F#Cy5~p3l?zu;4nuWH7=9O{YL7B1wRu z*%3n>#MOiz6cWvRAz}79<7O-r_vS!`6fI1$T)7skfw+Io=v2&8#J;9=K+-vobITf2wwzI>sd1IEYKL4{ zdGl!>M$KT)Bl^vFZ5lU1y?{H4b#13R6{>Pg9x+aM=q;N5k{c`^Frq)rmO?clRG4Ub zwUzcJ8f`9RgkyzTuu~gMic}`Rd-JhQXTLO|t0HSiDK%QkYapXw?2r%3pSX<=jX-ECax|U?h1wu0ljf{`$GrHravqU_v z_!OeXO;Xu06=ZzgVkEpjY#O{=NJf=`QUePZOyY2t+&FpKhko8aB8HiA&UDKx-;WEb zz-v>2HTo5S7ixvp$QTSgmg>+412Rw7D%lRJagn%_>xk86mvH^OkXnb1W`Tj^zW5P(7wr#E# z>(*nY<0r#q>451wjE^W;k=URG!VQWA6~tR4y0X&_NL`xg%F!`+pcUG7m)w`qPG5~q z;%O?{i_y)vaX1LvvGrShU;^rNv( z515ITwIdOMtn&#s8Hza3z&J5%r}R`umD!@=2`7@}UTU0?seQG}szZ4+oU%;b=;X8Q z)S>>c`Mz6y$Z9bwGnTRgA#CdPQa74tE8!0KPT6(oR-rk`>RP}hJSnX#9%Q{kE?lWa z8!PEBA$yA0P_!BZeje*dt3dPW(kKxJ(39{Ip#B9z3UU3@TZaiC# zO{|;9r9uI#H~JnETUDCCSn7< zIA}!%HBVgawA!g~j&4$ZIq87q7(ORq2ju zrr9gj>1NK1`+4w9%)Zg4S)1eYCC%HTLaLsLe@N=bFZpe6pLbH>vC@z?)E8r`ifj8jvMk8ui zv5A<+48_C)V$e)Dn%o^oCYujwl2VwGQk1N*RU*`jDC2^+J_3=h7lhM!)Te`2T4Yh;VB<`4shlnS&aUQ(S z<^?TVHiURS;mw<&VyRS`6f0eVsdLi4(CIaEome+7C5jodC@_Z~S<6p5?L%8=1O^FD z47{`XCiP;inx-e70NvvzjK%A{B4b*fT? z%rP0!M|4@;2uDB=wCH|%XtM*xsn&R=X%uR%VI1m{_~vOJGLq4qh=(G=_8waD!i5N0SXe&E4xxv>X?J+qFS?!b? zYP&qE=^oYWb`E-;SPs094APR4E2lvm-*B%*mBcj2=v!p-T(p%bR@>8_p6_?|Gqv6@ z9V@!=VpDR(jM+2~`LK2)AcW_iB}%y4#oSsCOk`?jCloOW^qqnz^}z#)?J91eG?=DY zQ_p&hX)hjWN1`d}AYvz{qCYeNnRT0uYIhp6C4x~pG}Emuxr3#Nz!L(S zaRto{_>)RsDMiJ2IbCWOJ>_yxq$87Jk8#G1RNgNQas?}qOBShYF-I}u=HX}e`p-}M z5bBz#NTJgL?}V1l`71+0sO5Ttu%W3FA(R~3H9C@{bBU9qu$Q1-ReWu)|FIlN{@!F zZS=}&hi{~-jb@q&sX`z*?&?F4S16yx*TsW|onyS!9#>&v6I;zn-D+NQEw5dZd(Jdn zrl!#d>zGoaoEh&k$(Yh}GnNtSCkJC{n&J-ihn>fq_F>TVHW|A*i1953_*e~l{niV%N?ZGz-{1Vu=1Vt4 z5DV}}$3DFA@s$fUUbXv`mG5p`y#4Nt$F1=Ae=I)^|1^FD-d_3-oLs(m@AG@F-G0{c ztB&1#?83e7UU=o2y>9{yt}>gwVkVWMmvd}`)<$X4e)1$pX-@^=XoL)NQL(S zXyIxB5)2nYixYuBUuwo8jd4q2Bh5mTHM=9zZa4IDrIjcY$?bVcCRYu`T6EtVpAJZg zBgPrGB?iF=3Kp_mU#sVp#zp`n*eq-9@%C_j4V|fy<;r9dCcX7&#@h+UMvlo=TLws! zomQvbTsuc4`V2>OwolA0`Y|`n#%fGMPefXLNa-aq>4+BZG*VPijo0l~I8ku>Eq!UNkL>EYlKF?U<4t$j`F=?j3vM*t1O-rt4JEpG`>NUWtdo@F-y~NN82})Ml znrGp3lWK5WiI>`*fuAkanqVfSl(?oyUc%uW&Y)ng?JMLoqNxYcjF>gs6gM#FtpilMgH%JV*Ct{u8FyluCE4MkqXuNF z7&L}_uudcuS*0Am9^RVggIyr1`R14A*XUH_gkAObGJK2fGS#%}jwhy-thEcCd{8N; znpra?XjZ%=?Gn&p;uo`-Qo_x6`e0Q(1kFe*%_>vF*7<1i+r5dxnjU6$Nis5s)M}9E*)= z6e9-fAf_9a$)*^v6Eg}gKXh&Z0{xFI*%6sWL9-?azSNJ_N72f#NDar*G$^*FnYg5+ z8NIV=Ad6(d;*P5ou@m=I*?>2svFY@I zL!nW4+-$Vl0XeHg7;X7IbEkr75S@A=lSfFyS(#gqeIuIHdQ!SKN(t#)o$R+vB^2ng z{2*7dqY5|jB<*ayM{8?sXt79o*DH}lC8u@0su7w<7Ee`-tf$m+<9WAI%ybxkQ(3)RnMe6mg&t=PjWN93$=0{q#s+E#z z6D-9Sn|%>1L=e05+(L`C5))D&NF;CelQ{y##ZlQEqgLpPCb+VMu5Br0qlrv7cU8xr+RY&1stkd$eJfc!7X{+bV zEjqWp^kQG?N3#2zbt6>B!KEFyxr?$NdG{2rDWbi8*ptCg_z*eqj+cv}lDIcS_+% zf2c-sO%UFqiid9if)*;eUfHq*Nx)(Huys;)Fo<3ntQ?o(RcoO)OW# zy?iqv%kiNve>|mauFNfpc`F-@JM1J~Nre*GNwp3()_QJR|NrcLd4MBjnRj>kOkcx& zi7*4h*@R9dl}k{lRPMWys!CM=L*>3JN#*7kU{*v$rF+PTc&-QQiY^Ga?5-EGtg?Qn z;Ib|%AR>ZtC?X)Id~a1UM`!w|cc%H!{m36ZJ>C60&+n~zp7*VKPQh-_>k>xbkZVz< zS~8!X{6e1sbucCHGS~8e%w4uYIqRvYyUn|t&1%(bTDV1~E^5rbl5H?4_v6%4bS#%go6#5JJF!HSMVoro+iuQXAW^hwpUq}9m`E9;M6*2#(h_KfdA%u+NCZ>_Q_Q>F zIaQ||i8-zQ2&#_>`hv<<%G!wh?8kZ(FoRlq6#j~lqY_2~n2;4~N6P+K$`jQI6wzej zdS8W)n6(YF-d=8ngvp+9v>_%2{$pQ;Hi4D2CcB!|M>K_NRnng4s&P%ZsBH&X zwT|`TAP6_zQ{1*M9*UbOd$nvVt3zCa#A!nvcYzn0umJ?K$f8EDbkr($B|<1lJsI9g zG##<9E)2<(LU`E0#WfX;T1(RmiW?dpG8I6LkzCYnNzeSKN8x7NR$DmiNoNX`bj#&5 zd5}Q3uBJIX7RbdoChAV+RX9=f3$qXOC?MuB-YQYFfM<**qJq^Msj-?^i!XHIhLS!I zO8CG74QB3GTduepPIgG`iQBo zOqrDrC^M7KPhK`TGjZL-*72Li&mOybj8uG5aq94G!{-eBV2Ikisf@m{``fp_dHV|d z@4Nz)?PpF5C5fSvR4~O+>u0j{n+%;PW(*JH^dqV5s-Davx3MzvF2{hr?1{qzV5ikTQp?uP171CQ8=0?F~WvCG0rr4t3jt^$ItRQH9mmm;FraI|T zE#+6&+^An*cp+lY+mRgU$u+AK6$v)1agcXXcQ)6L$E_gf^j(6Y4Wf{#=}VNa-Zm8| zA9%H^Fo;6{Z|x?giP%yGYt5hm6WeU+^@ET52&!zi^#rN5@-9cgS<7acC7*+@^6{(? z>-cR%l}=z>%c!;%b3)eMiW}N)>z0gN2o%FijBiCEHGe^9u>;=Py#)>GMP<8nm&{U1+51pQ&-uJ?UEVJ+UZssghv`hYbMV- zF{3r)cJpA+A#Tlwe349?Z{l{XGgV9s_OpBP zj^65Vm|C$)gAelRjK5qmS3`uekP=$15aTR{FftMk_*!laimUl8t$4!McKK}91ZTF} z+Hqkp`)BVxNNAVhNg(O0U6LYtN4OP2oh38Y(g*bfZSxRTFk)2KF=>1qH|n-I2+&VE zV6|KKPtv<~NwT}O)o?I_ro#4AErIj4in+;Z33UaJR2;ev(L`$5u&?Q6P-07nvUKt( zn>h`(3(V(l1qY*GE3L`2OH#Svpq*4z0FIKdPGvIc{H>fWrE*&hu1cdy`a3RGn|6cu zZxgYl8ZI^(c5}LD^Csv(mS?7%z0hE>(Sn8>Yo+W?S`JH1(?Ph z2lluE&Io-^s0yah#sLX6RkrJP$%N>eaX>PSmF?&*nGkI-4oD_A3$?psLUgq_AerDS z)a;T8(WK&lWP-C0*(DRA=fnZY1ZSaom&^?<<6OZmch&2Hvv6UTOo;Nw16rr1SO5Rb zp&8(^9rF%<^MB`?+W*c~fnwu8ua*i_|8M9{w@&rHan)L?zs z+P4sS@8+t=%H37>x0p(&LsuM>1SJG#_X?)OAmwYKV~;tmgI5 zTq_;SQ~O;^g||7xl@B-;v~Ehn&4a9Cvp?+AgmfT`$T~2a&R?pR61eh8fQ0`Lw_wwEKpv^l9tRDGQ+OI_psXLU zgE?_&p;9p}L{f}L*#GXv3;9w*;2I6RM<0wVq%`(Ks$w$-JMnVHMMh}nLcC+ia-=s| zOkkY_AG#30^9F<9_GvY(YEf;PN9Q|y1@g@4bt#8X^;@l`hR@}xv$XyGh5}@)Gw! z8RjDkOr7&2UExfaEmmEX&_X8dppwS@9|EDVGV{eYZFH0EA{CgAwK;FyN{}I@Sk!tu zB(q>{`(sT*DsG^?4Tr9gO>=sy+k|D0v>7vaBcgVZfINX^C?B%)chKaNMurtQIcJE_*Y zk~ND?P5E<4j4l-H`g|(m3FUYpp^sQxksKI90#NNw{eQTykZt)Z79^y_aUonr%4v12 zmIt+(mUX8k6OQ|L4`^BhQG4j4IJXO3m^AZ@PVO4!R0jx44Pj)q2WGUP*y-?X5q zXd7}35-c4Ur1<`S#?Wm;oBm;wZ|;S;PtM`9PtGpR8fShvbLEU``u^#Qrcapq&#Cg% zk;-o?lggRNTPGuv!xLYc@Q=SZ{;6@x*i&OmW2VtZM&Cbr=E#4KTs(53;s=U~;;7;8 z45x-?0pmB}Lw8%F$)i16>qsRZ(%ACaWI77wA5=pwFyMvvhUYFhjIbTS4l9r-}^khJdx!!5)F?N0QOGm!*-*33)*yCpY^|m{n zF+BDg#m7&o-M;<0+y2j$>dJ3FN_QI)<8%ghTG&`2Tg8%%XxUn(P{Cx6ftndKp=;3b zu)~q?S*VKBukJCv;-IT$ul&siZ$9ojuUNk5JLCD(PrmY_cRv2Y`Ap>@RCoI3XXtKS zVr&{oY)l4Rp*{ zfTdCetzK2uc)YBqjP@A&ulxSBPmZ1ZN%PswpIr9j?bqU)?m1`^XaCJFZ~DgvZawPE zPt#pNVjL?O@fdB(QDwW9w|Waj zC(=e-m(FFqcp;5?v6eTQ?=imU&{JcRmv^2z;pWeO7yH2J&wu!q#wNw%laH!TyYP`a z5beu!w<0mNvmLXpQKN7+Tvau7SXPIb48~@z*lA;eisqS?GoA7zTC8F)uN01dUC$=(ATTRTi^&I&?ZfnBzo58;C_yYOKb!8km8tCDZvn<8K|W zj>T zA0&Q#(noH%|Hebc?MuqV({-*dcJxypc;jc=gd@t|zvKbBTaXxsVis4WSd2YRN!)*02$^I7A|D9ge^GMW9+>7KX05)n7rS-`_+u3^ovt~lyg?Nr;tb0elJTlQMjP?4zSUHv>?G*qq2Zl< z%RBq=Cw-HuEzYxE-g@i>?|ZfVgQxwkK6u_&$}j(M$F^_jKYKOZ%}IBKV`BeZrn^~*aWu@?Lv{mbuA-XXKN=|A*fx{v4va8HsVjt@Cyw zOhd4gOsK04dQ5cmJ}&Am^7@>B6)mNB++eT=bs5syWBkp3^8B#-hnr2WKk|ux{?==+ z=x+MK^&k2C&C2bc`uOugT+FIqsyuW z!zAif5P>VFojt}cbZXyw`5V7C9OM1d7yV~^^nny{JrkinbJGRQ(xF$q`vwKwO-YP{ zSO5>z>THlGg>|)PRofx+u7tjtQ~4TPDpPaj$zaE?1{LJ}Yit?0)Aft%tjeE0{>!I+ zi9UAH!#95P*L?P+;t7`f?s;a;`7GT{N{lIoov2uf6`)lIO4e)Z&4CtdL9$MXL;)x9?J zjSq!){Nhv35%>Q1sQJ6UOlRqCOk(V3Tfr&_0;3i*1R6hhN(m>6mZJ$9>9R*X&79fV zi8+lKznATswCQT%t8v96?>WSN@`pb4lcOH}%tME3Y_EOu#8J;LPQ2TPTt#;|i7`Y? zRuA!iEVk}G?cftH`SMlW`&CE%VoUhP$y;Y`cXHpp?nc}BCtd#?x_hC-*b%YlEBab7 z80<6)230QDvh%5Y%aqgwVsTAA>PLg#vjvXkycCC1eVpTb*sq)4H>$-vZNag5E_d}Pd0XPladmviK_ z8k{jE+kMOX>h;G@AN28$`%eAy*Pkigan#?1LWQ$Fc4EqtY5n%M7sN4iLp&n zOOfm^*lJ8&RBlFQOzW3j6yX*Ma?=DvT zSaHj{4tnD3sm?w29glzep3PtTvV-nkATf4Yy#%W_$3uwGS&I}3WFnLXg|%d?Y%9Aw zwAUCQ+B#z)U-$KQx6SxL@z0*`j!u64<4N!RpFO;OXlXIK_3NHzpVvI`pNW5YknWx@ zF@_jP)Vq0K{L3kG!pC;LeDQ;y`sviSzkJ?N&%gVL6Ym_&ym&t6`Q5+Y_(!^Xp2XPb z;Z)&5I1?*5vhjStUaqIKY24zma;mmom7-#;MxsH+jl8CR^Pc_ZZ%3%VFI66`;@`XB zi9cVq`Trbz(=AUuaX3OB(SIk7r@M^A z7-G~;kMXtF=P%EGLU`@ICw|$y?zpS}V7>3U&%N;DKmP3Be|Q7?+*Pk0LwCax-P)ciE5K(>hM@zFvIq zi66W0C&Axx=Z;)`%FT@rbg%y+-K8bQ5QA)bh=210dBfdDKK8YvzNS9q_h0((&5yi( z)$KRA7LPvc-cP^q6Uz_2MRzHQF~sDV9^7k$}Yd(`lP=e6tSU%u>v zkMS>|&))b5M|VkyaRhM+s$x0pE30D7fSPZVu~f&++LFqiq#4DmKN$6CH1{|8nz*gS}raeeUR6bKm_*_J@!EF!}SFu#foPe{t&l zj~{=*_XQ14cL|9x#2}R(;&*+=^yzJ{{`wmq&Rl)W@1Ndu;R)}U`XHk#yc-Pwfw`{kFUDs;`9IQ{SW@|XRmM%Z#(>7|MB;_?1|(}bk`>_hHy3a z7=QQTlOC-mfAEW|hR)uSzksmZdexJq8(vl=<`l8phHll3(p|5_7{X=NWBe=I5cd4* z?3tf7e4YD<>mS}Z^tlP*rwZ@;&o+#HI?_1hRVn_jXo&IuhsK@--@N^K`wG12SK!J< zmlVgpeyBYRGiq!7S!K)Pq=?u}dnKo@am60Y5L>R~Q!^NaEj+on)*MU})I=WPI1Wh)p-^}A)HO+d} zq}}hJg{(S9`AUv-t>RT@+f=cvY1!2^9VTxIwzzcQ?|omk8xk$=1JPj^b=^42TcO+X z1>lwp*pm380qf{ZuM^ewEwBzrWFuJ>vV2LjGAW!SA>!ezZdv}#u66GoM=maVnNf(# z-Ct!l7!c9yR#bK>9gM`Ng<_3fCJ6-rnaN##^Uq15j7aoExQw!7i|=mE=C*Yy1`zwacp>FbDo!VV__rd2Ic{Wocm5bwl)eHOW|(E+Pi#Y|1hMocf0Zk!PwRhAMnefuQzgWvA+b%U)A_7ilnXyu-OryTwY1s3r zvKD0I|L|h%$`4c z>df;qcg$Qp!^~`*es=ns(^pNC)0?NBp8A)mzncn79jpAU@;2pVN{{lW$;T#dp1gR{ zF?ra;BNJblXiZoq4jO-O{7d6C(D`p>?18Z_jFrdE9-AD!fAq7Xg;C?^=*YbzpB%}I zpd&+ydlc6x5(@S3Yr}UBUpsu^@EJod0a&s=$j(h8NySzWwNE%3@oF0cn9}70Zbs8w zflHCahF^~%jH47IxCH1$Uc>TAEwXb?q(MVr4{(<^@|lK8dCb-Ed3A`@CO8R8 zEt0NhTXt6?5%4NC$j(`j#>;1{I&%^iSWIt-dD8w^Ih}BVL1wjjTZg18nhK8>8{N<0!Mv$xR6ChiJeL$Af;r)e)(mYU zhl4gD9?*x>;`1;u#6nSdr}7MB=cGu}s>tr3r<_N2PKY!dE2u#?J0fmS@m$uCs)lhL z!}@~-t(D+?2>L$oOkvEVE1~6B&1gVC4>gJd?Seg>Mgg-UfuO~# zHV`#K!IbwV{dJPG*`2zqL*rGRi0mAa7E&nizqy4yq=HJTP$!C*t5tC<_oToTb$nxKd>SF7YLR!1Gh5YTWro$6RS zY2_BAd#OlcqMI4ST(@_8dZVgbK(!sv%R1)DXxkA2#PrlLvoDNzO*DZK%FRgk5|PHQ z)|5-$fJUGls$8426bm5)v#JY~dL#*2|Es|iB+ys49<_MX%Hxsl#Uf1{^b`#H>j_#P zc9r=`y5@;x%8W{D!Sqzd%w-K45Uz_Npf_?}`A(#Jkw{bV*kgp?PX@W5CB}o8grU{) z3QR4P^tn-6G-pdx_2w)SP^FB@W07u0r16%rs&K5?b{JZ!v?q)g63%v#h^F*LlOKfb z%5@{lsJwM^DS|2Afpps3J@Kv}>GHdOWvmHiDMhgB2 zA6FiObXy_~uJfZwEM4-~s@{5o7f5x+rz_X+c0Fh|#x#X+(^$d!lsOF;DPER>q z780aBfW)wZOy=1sHDq= zXpo6$Q;`PhWI%TZSKe-CigD!;NVg`^u-UYlD4UyElP(^#neEAFwB3p0#a7c$Y*wO0 zzs-y5HB2II!<2_3T|uO=*bT-&sg^C-+&<9T(h+x5nw~_&fva405NtHi;fN`2i@W1l zUU?YOtx9rY{&WcO`y$%1-lr zFj|W$&SvbYyr-kf+uX^P-B`v{=4jJQC=W)uyhu~2Rvp2*n*t#*bD2Szh^}KcTT-+c zbm_5Clrd%LSTz-&FYe1L4??;nk;djJstV?`$Dl1~nFN<_6&tLnmNB{Ab>8352I>rw zu(1)g*=Z^_A>E=#<4?4_?gVPYRbd-nYZh!-Tx-O0K_rJUWHU*bgtD(fgs?&iRn8&Z zf=GklwLmD-;OtzmR#jE=ZXE%-J6gRqXWD2paan>)=_@Se?v$I#S)`j6Y1H)+<^fx5 zu*!gD)qx5hD^gZ#Jr2s%5fJN3&_b2VwoRsbi%`xW-JD26Hmd2833pbABIr18a`RTq z8UrH$vxXu@vo0;tAnR&JrmZq!%4wvV6=@*S_>@yfHzU$OWaufCNH;CgKqTKOCy{PS zq=Cq*Q%)e=q(}piGN&9zx(Sg6A{$OQhIHd14Md`waun&tL>h=3HsuJ?0T((KxCdNhmmenq=85(Qw|~B_lh(S`C^lAAl(Z@8i-V|$-f}o^FCz$%L>|=S zi%6FeX&_RZCSO3hq(}piy(H3vL>h?1B#|a4(m>=KP5ue#5+V&mI*~{d5NRMXg(jav zx_*%cA_-{nk4V=i(m>?*Og@Wry&?@n>dxdJkgi9hfym04{5{fji!=}kH-VjqC9 zF*yLvg$4kznaBZf4m1FW9YYR)_do;KU>$h31OQ@R-)J2;8ydg{--5HC0c`Lsc$WkK zLbEse7MP#`Z162GLIc>~TVRj?Krr3tTcC#qu)()LCjo$XhT7=U5S0KxJY2y%Ag!M# za_J3PXaEp-RdN7mpaDQUhRFedNB|(7J7J#b=Y_8o&nYz?skhHdqH# z5&($zh>g~PGoS%%@GY2!2C%`m;B*N9gl2E_EjSGtzy{xfQ=tKD@GUq+0sz5uqi?~< z&;U007MvsjfOu!$*tAXY{eR2Qmxne9o0M~ZKZneIVfM(GTW3xNaei{@v8k}~2g=ms z&n9^=&+qz))5mWgKYi>|px*y0qvnyHkK_~&DcZwN4_`5S3}Eple~OFt@g#B1><~=A zU`xP}r>hLsPTBLeS|`M%>jMpRi)Tu?*O{zP_x6KfW5CXYcE(c zNVJFnETaBG1a4$wSxJA-1@c7wg$Vk{mbbV5o&)5F`U?>#k}YRIf8W!;oe=RQ*`n6I zoyfrL7r=7bX+toPIoVjaLY5PfLV=HSg$ys$-eokZ)x|{<5GmR( zL|{+0$d&BZ2;_U}l~RKx<74G|EPjb-;{q3tgiBC@%@?_ADiftZ#p zSYra4g{BV%BJA-dfhjH`0E=k35bk%`SXQ!JHIOG-E`+aHw!A%-n_gT1aztZ-aHz_b zGhj?-_HQSI=Tf$)HH`@|F4HZwnp;$fSirJ3)HU1J^WDaDMt{K&p3?O_U$TtJux!DS zG0peEK==&bBrwIr(*YLIm>_&tvazgWOs4^PqA@`@YGljX+n7!TaztZ-@J7g%Ghj@o z^lvA`Yrkw!Ya7#GHLdvmKQemF(DdTu(cquAKW|@wx39q4SKuwX0`DK|hQ<2ytB)Ms z*aK#H#qR2zAy#Y$3~?lgb%ro5TIug0&ViLXPe7cFE0qK8d+ds^-hw*&6@Y+hd(g_J znoKM!1YCJ^#YWZ6qiRsEx>{i%-9a=38^!8vbS;bReUk>9*Yr5Iy ztF&=J*zdx?c@3^f))>Qr zJ?E~(Fpt4$(R!!`L1{s4Tg7P$*%I?XeIn7+;j&uiOG^iZfmhbLwA7-dKD@C{UUCgl zw^Cj1U%es109>KT#h&NUx5xl@0}#7hlO<1|#Zkdrds3M6deikvi!h10jSCR#W?5As-J}(`vIhV{Q0#Asfpk z%mH75G8d4ekXa!wE*;q2RrxL@+TBOv!yEf1BG>NFmFx~8=(#Zm8tjQU&@5)}_pP_clIj9z z7bCS+S|ucg48DtjKStDQvHyfb8%-orT)j{OKNgGBcQ=~4=!Cu(p~MxF;Fmh_xBVY2 zTerk+&+k?|TY!9pK$-@Fe|E#Mr|#0J66c>;W!ei*aJl9O>IU=*hY3dvK(4 zadCChZ?dIEwk9q|x&XVjo0aEpY}|IE)~;IrKVKwjB(CRP$AUGhxl{*305k}O zbA*F#QgI{}adKt^-S1=GY-^ZfSsS%rF<3OZaLlaB*R)1wC@e%kmyS{_=wLD!E*9nkJQpcYWn+Wo8B+!Kbqq;HXKFeFB_f&A+k$}zF0Ubn2`n9KH&ftA-=H9d zMmLSzH?lZFj2x`EPqDb^j!luYynk);O+nZ4PAkA#xB@0rdWB))2jyq zBSACkW^7>x78ovm_?I)RLbVpXPN!Su8da;kSlD56d1+?^&_6i!pkjD!dUSx^W}`yR zfZ4%>0R1nXe_1iKHvRHAq+0D_@i2v94l^av-(wzrV{Q7uplZP4vC$Z5and6Fzk7!N zvNrwT^&`y;PMaw^9Te$rxoi0KcaNR7Wo&rm>#rMJe%Kv~kO2on+5!2k?gvy*)Lz0% z`0aMT&nHoT;Ec1!*wtkZT=_7|T7o{iH6mX5uhqGcHAOFvFR9g*Anox-B2<77MMtjC zta;f?gCPXc?sK}#E{|WlY!Cm>X^PX=reE%nSF1^Dz~ys$tZotf!~gYb#c6BP51#al zCrq;T2ptly`@{D>syGFXKI{uQsEC_(ghl%MG>TK#rXMsLM<8IMTxM@bynGMe^MK;y zwdn_wcf1ad*+;qk4pOAQ`&z|GYts*^z188O>~1i4N}|8>F~!!k=?B$5?53S|r$ z_wSA~6eq5!J%hKO2{|dhClDqW0RL9{K4eYl#zEx^hrC{|&*2D4SMOVK#R+TE4~oYf zu0YUE1u${bc=%tRP;6P7eo*^pyUXu)fJYFkNPqk7ip^`&4{D#qPnvxZmzTvw`mg`9 z;`p`c2epUscp@%4>vTw0|Caj|$E{62xa-&=?jRLp!i{+tge zj#`_3PChD|3hz76^E@&Kd8M9p#7}NV#?IL8sWXruGirJ}T_+f_Anpk92Yq{KA^j^@G|=P{7~nvoMTw z^^RyO=GLYk)LtjpcqylkaoE7>AAI9*#q8SjgWAhDXn%+aQa+DJ|M+(lGi%ciE}t-) z?cizC>Xzi+f4yRQZTdm&^@g0di-?%*qV_&`=O)F}+Vq3k>%#1qn{fHv(gX8XOA6)M z^n=rwo zgVl!=<7?9oYOgnhn*+GhZ57Xs2NQ=X#@5u{!OL%VIq{G`5(f8PGQeFgUK3S8yuMu6Ws{?Sv0VLErLpVo5G34Fa)$V%PqQ(Qqv z0^j3`J^f89Ij&*iF@Q02(>pG&x%iEhl(`w$h70uG~RfBlX1X{Voce)u6q3RGMUf=nQH#sxhHj zM6Zh`9`qhbF;}BsrG5{VpivFHf@mvUvg(z+8A$g1;062 zY;x*AJyDqKfobn3b?46meUS? zbOH$`f;MWLrIVrBwG0uc7C?9DtTSb|rW(eSf%CSS8ZN8JhnzH$pae#v)&yDs#$}I1 z>=8Fkx*X{yS*!$7#Y__=V`j$!>ancSh#km7jG|Aynk*N%cv6V&y6-2z)RBXk(j!+o zx~vp|e&;ZZ#>v@HYJ5Ifie?i4t#0*H5aV01p}+>d1_md!h_c689$bGGSOrzIo0P_{wJZBed`Ml9p@|z98a=nNY{79)T`13wb z$6d-gix#J)g4WtP4GZEbfXV)B05!yA$7ppkw-%sg=p^QD$3?p^$U-ZM8d z`}5f^%{COI;>t}w+Vsg;%j_XDkIekj%*8XVnWKj88jdLb1@r~TjNCGE*@$oY@#)*9 zub9wJ2d0k$^8x;C>Z&PvYOC@&tj_vS2KI8%;Hsyx$lJ z6%wSDHXcn$6h$i%s?^--jEihhO;ahY&&26~#}?Oy-9$aiI~>Nm+2AtKbbP7_P0{F~ zs>J;PCU1!6LN%2^-!ceZPl>f^?DmMS5{OoeZfnyRa|Y`r;^^P?C?d6BSV!5L9h zj^U*M5oTO8McDIA-g?m4&=hAuQ@pFUj)WeoBy;YRiBW;U1`ZA4ss`z7))Lo-9BE^Y z&KjtEK2ft|`AxU>D3YGKQSYu61OpaK)sSGIqjp-2u~?wk6mmkz5DL|jDCN_KoQM5e zj{@TAKG|D`)mQTPnlT|!$Yg5fq}f{3y0b2hB&Y>rwyD#5f`xn`X-<04LyqZF$k&oq z50wz}tD0pb-bB(W&`usT8?+%qDpV}8R&d9i<}y#0!T}OTutOh)rg#XN;=vvTXVlpo z5m#2j;>nm>lMaW?42o)^8jC&_PYX$2orsw8UOtJAT-c{j8-sdHF(YV=NU_TM%!NeN zPzYM$^-A6rYm+6lD??c;`A~Z7C}@f!p(&2&QABOcR2v+8WC^WTvbiXjE)Y;P^^O9r z^4Qa0-b1^oi(*15YaDwHnxbb>1qd0>&fP!!{JwkA+dui=d<7)EDyn@kR%;Fy>`c&C zcV|*qTT{VI4qori1&Sm~>k?U`)ugLuo5#d@nh3F9Jq=CqR1cMbOGgq0Cu1b3tUHn< z*#cVis0|4>U5j~CNTuOb=kbmOamS~>2Tk!GJqn!{cnXasU!dmpRT*n6)iOk~S{K9T z;;AaZS)%ETwx-sGIZDAmQ-q-@XlRQ56M#sL;Q2e&H4sFKZYg6rb&|)G}G0N(yN5Bk|N&stiGPM8} zwFP5*fujV>sr)uH#kZg-zS*PDG+a7$AQvP&p>jhYY*@=vY~xLD%ByO`k-Q5p3B|V0 zZ#1wobI=sC&=fO$3b()qgAQDuC!?OAsvN>i27T5{6-ySi)02)A2!>H{tToKf^>tTW z$g5&{6&M|&NBC0C*ed!;1xtj@=ToV8fOQ1YV3tWf9jvN{Kh=`~LGejwicd%sg@y|& z1UY}$!gNA5-bp7aV62qTwiJ^3c(CK9v{=FrVDU78jr5*HVJP~~q7X9bJyaR9sbFGj zUSrAawO3d*PnrzXN+no{+Tw^io#8bUS*WI}0yX>_Xo|<6DSq9fNENIs=E!=iXiVQI z(^*%P&4+SMBUdO@a&;TW8hsUYutr8vg#nsE4^5$ira*fX@q9jNt>mgE2WE-sSYO43 zl})y2$L-_sT9k?7HNBuw>pCvu#4XSiUxTK&xkmxSV z{@?KQ(OWshTTySPE&DjA5o`kxNHP zY>(Umx&>0!Qlet7W|M_lGlpjd*+LA~BA1Sk*d8H+EyV0Ca_Ml1?cp-mLX77kmkyKI z9wvh=!~`#L=}?L7p)%OA@`rqpONU5o50SwZVh$L&bg;zs;4NIO6ePWcY`(_PR0b3p zSOzr#VpJHpbdbdMAQ@~SriqbDn65AmeY$3XMl8f6!wv%tH-=Pm? zKpnYwsl@g#GT1_N;3O9>k=VX2gRL4zLcxg9)v#x2TQP{~W43xO;44}4IxkUG+th@! zkdHNdCLc;JUM#VFO$J+t>3QVhMH1UrWw3?l!$~f7B(|@}U<=W1lU!^|Y+sha7NWZ* zx!97}z9fS!L?2CZu_>|rvkbNnJu}J0hQ#(o8EhfiUy_S;iR}yPw-JOHE=DfaB(~4X zU>kEKC}Y0v<1Lg$P??(1YLrjd0)7u2aC;jKO$JTYh_pQx%~0f`AhG?E47Ly*G0DZM z#P&HEY$2Lkl8Y6I?H^^Zh3H{PE|w*>&&ps6(Xx_Uy=aUU;thK5U!byw0C-*;0QW+ro&dNflwiA&hvx5A`$=k z={bX?LL$}P>3ORqIqk}reRCT#p?bYe%*kCbZO*MtiE%VHW?DcOSgHjQ(tP@2jbn84 zXxLm!={(6Gp9lwALN8Bke^O`}0~*omIWkwR^8Q?lW=q+EH)6A5PCIY6(q`Ubu`Pt! zS)XCvm~qsLREBIjO;)uB$8}twjF%`Tk<;Qe9bUl_sWuhMIPx-6XvL+2OexTq=>pO2 z#Kuez%CvEIr&-DFAP%ODu_IVR9I&Hh4T^w5y#Sx1<-Iw=VgrUVyc5NIGMNeM64c1^{L>3 zv#Kw-ZI)P#$>2glW|pkDbg}xS(LG-}=G(&?I|EbhF^?SRb|apm`&6gE>{lxs!+UaZ z--3;!CtS#6%VMH2T;{G(+w~vPhh-wS?vzYUFgKU&YY`65oWH+n0(15?vKT+ZO|(%Q3xUPHDo?fi_z^y$zB#r zT&lPL{Cl35yBI8TR^(Lh^Jl>5rIy!qG&Fi94L1bmkuoN zecA4LqUC)cIt=rex^V}+dSA;ERbjxE#2*bDuD#g>qNcqC)**=mHKan87u)@vpp%|p zt4R*bRTKZeTKVNZ$}8TfT~D>*OUOD#A&S{wWk*rH*kN$RV}{9R6A=oHEjW|4V#jVV zrfh1e+-EFlnu@b^@2qO4mp%6;yeFd8>8L|s{cc?-- zENIcOpkIi$gPsgqH@S#~dJ@CK-W-oC)PlKsrcQC5rq|gD+5`o!OUW8#B+(RHvVvE`bdGobtC|`rl)dv57|}-aG!_cx3E>F?#g= z(cs9vBR<7F3fJ)6!!|(t7Js%~J35w35|djYf}2|*B9`k%6t`VF(nA2jbt^=0Qw{<} z4*>+%tq^fgIS7V(2*^e?T{7X$3dp3)6DBur#jG)tS(`N!F`9L0kp@{;J2GvR5!={N zj@zys>LGxLm~QNF$89^d^$G3cm0^|wjHe= z0tl`fJI!(1j%E)5#Q4UIooKjiN27-TB4WJJ2TJ?TsDSxNQgDL*U}; z7^Y(532mLorlSe9BhZes;ff*L2U)&*jvlq;zA8d>+@dQn|Giv7v1|A9QLp zm8^@QJi%PdNv8slnAclV7cz}0VYo-ho+V5zmE0?V(dHgVk<;ub!O24L@tOTCuNJ= z9jO&y91^)jI}oK6MNP;SwRbcP#-nlh%%TlQJLLi(ZCtjrp8tClKc055RVIvoBe7@| z;fRYLlMScm?Sye+fMr{DEqiqR>|i;=TD5}>R$;Qh%ZoUGBWd)AY&a`uG=#q+vuFk) zC5=|d7P*&3LpU~aix?0kY4oscQOh(M!iNEF?YX;d?T~C~Yismi?^_r@Nn-IF5f0Gk zsW)W9>1#AZQaV`nd-}_s`pf!n#IiJcuw^eyHhX#T-2jfL(NnL>hO>f3L-CR^lQ8V%tf$t|7*M2Q+b^{Q-9%QPCo7XohWyLR2$S7b|DTcZcNDZ}_%5{o7g zj=1=jWy9%fG(>VeShjK3vSs8y%h71fKFfx1#{fKrU3g?9PRqix+`=@btq{%`VNnm@ zoOT+x4Kfn0`*65ImJ^b~@&GK1o2R^}15zcseqsGI@4nO(>>9!YlUYQ8NXf3Bmo0KH zyM}PJ6c@EXlu1>I*gwtv8Wc|h!^`=*>L*%2t@h7<;KN@-m;TZ&#eDOF2`;`Q{BBS2-q!*3j^X+ zAKbDtcipn5-Yo18c*=_^07tU^CuPG~!TKS54VlF=fJn*ueE}J=c z`X8osQ+G@u%Fiheo&4(LaTB*pIL3cEZXf&ASa9_5(Thi?MlKpTTydr1nBkj;-!pX6 zoB#SRNw|D^Y4f{L=9N4KNI)jFRrvPelliM0)8sAQ9Mw zQbx6RU*g_o;VG&L=CsG4Eoqqqmv0pttf`hUx!rZ%-_Qo?43e<15w_WBW|!UxBmuKf z$|wNuOIl_X5V;WGwjSHRtuo5OW!=^TaYw|L-XWp^OP5hi-bb?^-iYa?WBQAhQC%); z(Ffv;;FpdDXhdC>QIy_Cv%($^;fqKw9R(zcx-6q!y)SVuU50Q)WS5Qvl0;pWQNrGr zv`m*_JQ34NNAz#2jKX$VxAj0A5%HzNMKoaPGV0;`X!gSoF}-wHf6+4P<7F*+`Ad>o z4daI3mktGJL|v9qR^Lanf-XaNA<|2S0Ewb5%c!{TOWaGBA)FA|rGtSaQI};D;rAsi z(`6VR#PrfZ{o5*|G+)+j-RE5#A~_?zv`It*mM){BzmH}=JP^}MbG=2&sqmMx=mT>= zOfSv$;mGI=APdfN(}MFqfTMb*4@X8vf&GI6(T1S9G!4)IcgUoSMg)B{2kZ^um!^PF zu)R#mXj9M^y2ADX;S5PHDSj*$4$n1}`}UA`l4Kl~umOQZco%jl%g zzoBctZytyn1Z*E8eK;~YFzg>3h_(#?j-n4oM(2k8g9G7c;g^O18gXNh(eR;*rLklGZ#paNU2e9qW5-PB>OB((0xQO{gbsHVLKGLT zXVaXVOU0sG^LB50Mt6?Td1yTW9B8YZC2Bm>-)$PW- z0LqnfnGntrdMA@NrQEHB9N|c^&XTsCv@vWVZxnn@Q-yC>@AV4qzV{a~SZH8?SW}_u!jrrp=%|;feK#fP{IRdw*NH<;A zgbU?huu;&q1D3qYphK{@kS#kkNfTN~HyPtR)~oVf9ut*ntM{Ejs#h1O5H}xVX?Mp- z=2DEYOkmBFvCiv>cru?{P?rq0nwxGJa#deaoi1cFetuqWVk%6KZ+Hs4w@Hu@*6X*D z9%6w*v=&~#SD8UNP}beQ^nXQT#|8s-;tmg<4wer<+hdjc-2-6n{z1#_O!N(i>MU>U z*y_y-_M@-SCF0nz_m6d>;=yqBk;5CC*Dm)V3w8}6; zbO;bKTop8D*!zJ@2XypJtp7KF5!XP%iuL~ulf50}>{uk`XZ1)Jb7q82c-~yVtXQQ= z69CfpRRq-%n^ZB*1klpm$Z(h7qNbA*XAUxdkqfHs$kS$GkpGG_Y!YiYojGQ@HFf{g>B>tc|9kSJiOWFUzj18m=yRivkJ$HHxruMs!ao-JW{gl`-5$RR9`1OOMtKl6Ck6pgCd$EXj zVGrI>m`d?J-aWB^^>L1h2;cW<@#~cr%f@+f_aYJJGFyj;H1=^Wv-Nc~K(h5KG9uFL z*ULt_x!W1gQUy#NG^CbFw!ZCsBF1eG$;NnGw>@a<5INL+h<#gM_cDsuE|&E2;_t~u zxVhUJxQdf7&CZu!#ilt=nZZ=kPRZYHRXy=cVZhcWMU3Ys#IG0LxeCU8qO6y1?l$+q z2$5>NKNur55#xDRtcG#@FB|7+T|vZY+H>nnk@!+Qh~toH-5!Jgc$ieQ)R@DS10 zKGy`(1 zc!=!wU3Xq=ua;aNqKM7xk-q*+Ho_CTym%E=dz5o(D@5OdK1A?F$Hi-)C*1l@-6;_u zdZYN&c;hNC_sPKr8)!*{xz`4&glUQ zc7rU&lGXJwN~a|f6ESKJla2AXZb8I&;-33G0TTf0V-y29>m!top#?%jxInIkaL`Q# z&hz}hRRqUS#Ga1gb0WsQj><6@4_6=K3P_@6^oEL-^3k!<#-i@yY2Y zroTSDeVUj)e&R>N-yJ!A?sJM~rkSjKZ29^ zV#g*4#w-lj3n$AlPQvms1Om)*48f2%0kRUFWAFeDU>0D&Q5~IoZ}+)<`kZs=Va6-B z`k(4k-%tC~xA*t$t*!lizw-~BnD^du`s=6v)9$Z5oS**H(=R#wz}~x0e*5GHAEdiq zck=Z|-}In$wAlHrz3+PXLq{Jvy1)0ehd+M!t%vK~M~CFT_~2U)-*)g*=ZoX{$d1}_We)n|D*k{JjPBMuWc&qe$US9|9S1N*MGUI{Lan=xuTX(j!6k90T@o; zn4mSHCTu*EvwBlSTbV@%Hj~%7vWLs}zv%*@gY#S&5hWWO9Ii+BNMlA&#%HXp=m?2) znvH>F2~>k8A^ZN9RBeb_M5oPEEmA9nJv3dEgdvDbPH~iU7b*|~XlRpY0~fs0@2e1t zKG5KM#;o00K-vO}kz&F*@(WLkp_L-v!wI6MAl6@vBoJ}w2rPFtG~I7Tdz z!i|w|Np)-OnL-*|>POAORH_ zI&SDo@$?6)Hhd@mR{ek$z?Bv0vTpW9YCqY~k&UN|{({S8&8G+;BE;jrc!AK{@(fJ} z3J=#=vqAS1cvDZMsLK^d?&bK(*YUYn{cDoI3ePrB&L;mvfcTyv${Ql+bMnpt5nj=~>fNBmv$UQlp|fYY^1Q z7gdNZF(Z{tMz$D`n2x939PR-_0YlWzgdp%_FdZA=+>=SZcham7T*KwpLdX+fFzyTU z5u=aRsM}tyIeH94)XHqhLvcu<8|nnC?gIvvp6J&d%>F_$#R;*-*L2*d zXS;sY21iY-!WgHa13)aRPg;CWadm)kI;sa^ttLCHbD7YGd-BP$La;L-^I`(0IF+F{ z19lpYKZMy+Y8&j2*8gxmCV1YU#0+K}$Z_XtOTJwKhbU zlH3#^dvRf5``-lYTe~mo=ul=veb|U~XWg0PZ2ZL({V=`tdO z>M))v#~-c`z_b9m7?8muV71g1lf}5!a~4HPz?&v8RD$`m+4HG2YeBoedWqoIXcuFB zS{x>U7}h5P&VnYRzSBktHZ=Wd+^f0fXr(Jh@4rB3MJsMO`H-zyX$@x7e$*OyOtelU zUUG?%$#8a~&Z}`B>K*?2oe{rQA>!#uo8{#gFvneoHzBw@mQYONNdX!Fr$KtQSQ;5% zWqEx3$1f33=&y_AXzV#rnADZ#SdRk&aP(Fe>EoC)#c8l%HkNaH=l`e>JQ-m!*hHn0 z8+x`xxD~u;vthf-DY8a6!{uGBVXwOl#7|4( z)dC>zsg6F%rLKMSZ!ZwKJgI}tIxIHYi_BuBiPFIZJLmP~qQmo24UC$2JVJYmX}n)l z2pk}@L6ejU!n)Dv3AGoyqID+QQp2hsoyrLmSa<^}()8l}=_E)*$a>vvll}b$QHT8E{Yq zA1xY@fUL)+8$^uzDnSv=64lUeYIR?+x0z*0YYn4_3T~p^a=@VFij3(`V zIkU3rX4P-W3!$ZE0I0Wmo3)EN_U@ml5Oaw{+upip&uvQ|4`81h+wj!N{JGYr>)xyl z3%vm@`Ga`>yDt&E4T@4+2xaY0^A7H`>mmk=izuA5P*w}t zus2+GL=s7~4l!TZyKaTh%VO^RdcR?FaA_}1@caGEvYeyW_H3^9AY=|@L5)ESB;Q4_yA;-16WR#!dtLX42sy2A5*V3#mZAnc_(N<`Xfp+0K?Wl{HH5)4z z^CQ>6G=)+2zvd#eYkZ@F*wYYO^|@N#9;GR%DP3`{y110AeWjm}s%SJ}V-piq6FtRd z<~6r7_c=~Rtl_*#`_fEHTI&q-*>RuX>l?cc(OP!!ofV=(V=S?n$AM-N#ssK=wIc5J zjec*4rHHI-&=7#d&6p9W-Jh!vG)Li44P9F;I9kqFI5HG*6Y>SG4PkH`@ri5o+HxOF z~y+WEOmeCteHt_ARBt&w7RRI(-rq$s@fo&rtW8MxmpT& zkDT^FE)#uKB3Rz(j|P1>bw;T+r13$va~T*2#X)Pr1|{ZwL6~+`Jm(rB(3!2NZUdtx zB;HayqZ`mfyz^@heth@+*RQwV`sH;CRJ{@Ry7h4+jJiY-yPPo}^IpEF$Eq16n~qFv z@Mb>=4N_*a2bUA=qCN)Iux}ym9Kze}xlrW85yu-74s18QZdP-n@uW8B40;bg&M4f%!`H$W05$e@PBEGG=ktceX zt2DZ;OsA=g6^Uii!x2qUw4!ZlQYI|K8g=iz)vYTrPy=YuKx>iac4n(_3!W(bfe%t* z#Md)oyiAd9x22lKz03Iqouo1{<^jwd8|^O4I=SG4nWz9=e7b@Iy^w8uBblHAo&Ipu z1~<`CJR4`Ru?U9*-)f1=WPvkPOqj4d z@aVzL!EYS=vx9FuNDtV9ckKWA{>ShCZ}-3b{+HjE?!WKs_s)Lw>|4*i^z@~Z-#q!} zC*O3Eopev$dHfs4|Lpi1_v8J}{^#xe>fR6ReZ$^zkKX&--G6iV4Ttd|d-%?yZ#>G5 zc!!C*vCWU##zo)0|3KW^duK6z{m&uQU6I2puaM4L=dIg{66eIFXxA`^$$U;}F_kak ziJx4#>gms){`_r4f9~|>ZY%n;r$2jJ(Vsc}ncIr~^yyDmq8pxA6bqId^nAkX8(n{V z<+k>JdHT9U75!KH|LV4)pVWT3zYb(~q5g?6#tt)6H!~ zi__w^qWNilThZ(^yRGQ@bbVXV^fbM#XmXl-{(-o=`_2vDViJ|t7oN(c>1^P%uGsb9 z;Ff~>`?nO_+pDy^d7%8t{;yo#(2WD-!{;A9<#&#Dc5Vxx&*|HWQs>kyZ|(T_mV!q| zw-h`)ywp;ktSvFhg^@E~a2?!hU+Jf_pFevwUf(GCb7!wQ;Ekd`d-iI)zESjN&R&hz zH;VrB*{kuoL)iL8Ry(o7x79U6T%Fv`@H4!!z~)BLkDPtvwxSc< zed6?TdcS#%iJxD(;mx8SIe*m+X|rUw^SMxVdqf^8EB;Ey?!RsSZI!N@8%6)n{vW!n z==uKnZAI_z-@mQsz5RQ)6+PKMxvl8Y{?Tnk5B3jkE4shGUy0tlY>H3g+lsDESC^ua z7<5TIS7T$$b$i3{Rc7c@;1sy6=;PDJw-x>1=?8BsT05=XR`dg>AGodPqti#X6@CBd z`)@1yzSH;JR`l~vKmWF(?>&9*ZAIU6`kvd0zWemuw-tT)^yPBK_}B}dHEwso2gmcFsD-zj^Ta<(IcUx3xgY6KcK66MBcETQ{H9x{|}R%^Qr9@}yTVjaH_$azndJ zvTI<#3+4&E@i5*tS?I=uuQ$|&-J+gMM7zop3S9`?$P@a;>pqz$^r__uh09!-Z64_} z^MnMIf81=2yhkK2#MEl2XiC3Ds$)7gNIhDuQ={qXdeXNs`%UKw3A7Y;;SpNC_7EM6 zv+AHYljER-;!0j{plH=fHs?-j%>a=6#u=*x_-h5o*KHqbt5YjdGL`>!p=PDpdqX> zjfv4{bcfs!YPQ-$=W)=KASLJGb?;(iJv%qKagifk{WguQRY}qNbb>4lV2#%bA6RQE zOty5*U;M7SoT7ZaCd5oT$Iz`J(;JSCZc$f~ZeJg8 z+o5(neRXNnr|GNZoM~@$ounkyVcIf0YP2Y3+#2hTeRV;5B4*{IxFOSzV0HeuL*%UW zxa9X*K}7j#bp5DU-jeXK(?;4Zdm|Oel@w>pfIDUQM$8W;xR^EV*@r4=< z`+~iY+A~LO8B}cBC>%cyQ#Kzw&RqpdpwX)Bv~|5}wmR$s5|t+n{+5EG^bHG)h$Q!XVYl54Lw)t zFl+>iQ|6r!jTa~AH zRxOn)bMF%cFRl*(0B~jE?VEVC9sL3b57!c3j0=Kvh|$WI%5y@U^6Y<`$r@|if!E~P z<|En^xyC5>qLqy#@Wdy*|%tSR^Se~wzpB>&)ivoJ7iAZ zMuF*t0(U4f{i3_zOYW?|3+{rC-dTYc+y#I7LV-J6^hI~U7vEWd7u*Hvg#vdd_x)nI z*uU&slnVv!!2NZXoZ*|d^}g&|CU;igh3+eU*|&`EtiT--d~aic(S-tc$iaWnT_E3C zffw8b(w!A}!Cf%CP(bMGOUbqqM)uI!P&6jW1htG+EKS$O7|FBRtS!w@pb;_IUC6y| zGX@5CR^SD9fp}*HUT_x(7Yf{={QHaUg8rQqc)?x3-&uhd+y%W01@2H6|3!B}_s$Bu z;4a|qtiTKI0`@|IH3#=L>SP_0VH0B0#(K6vcx}oB5i*bQfD8=-McV_|k*F8l1UVgy}kQWNvVaM?0JM5+1yt{a> zd~zrEzH8^<4?X;bhx3Qn!=v*bJO8Hh#rcQM??3qQ2jBW&z4uEG$Omt`|Nq?oj{E=d zefItzEl*JXcV}O5CY-&atPSv8_rCVtxXcy!wbSo8{kqeSo<2JHt&<-(dF8}7sUQFQ z<9~kqw~pOo^!V`TN00vYQFQc0NB7HW0N-|)9a4v%b?{RM|KQ2||Gn?p7x&+B{YJuD zzr1CErz~)h>{3n|GiM-h6A6fjI2#&DQ2rn z!bI%F!Z2Q}79l;%_N!V6Aq1+z7)ty7>5_%I@q}%YV6SGd_yUW|^m)4&sIe#wx{0@6 zSvLg1Srg)0l8tg*iXcNd00#p;3OsferA=gsN)$tFxr zdw^?70Y~lpN`)xa>2fWSK%D^BGQ!W;38PI0jMJZDM7Z4W8p|(8Pafd%{?Av4Z0snS zx){u3FH-3w4i+KCk#3g6#Rya~Yc4MU3Fo_OU@xx_Yq6`gRJgQy21;bDV+uji7Gr19 zqWk^M$ng5Yq+jkW2gYTsgkUY0Bcq-RMhIdPo+RasfJJPLTbpJ>7G#8Vv1!V7@JY14 zxJY&lV!4=>FP7W!G^E4SY7O{Nh738`k^O8Kv80*C)ig$M}7N@tS>)gOzcU5kcqi7u_q z&;S+#jaVBEoRS;BM#_%AtU}l#0J-I$u6rx4%qdRG`n}vzZA3&Dn1y?_lgk zIew)=7%Olf)EDiB>KkKW!ExM%T`Lp1F9X@KEFc241k&fmRK^@vX^ervnMla5VA89J zn5L3U*qP|!DrT1YlAj>$AuFTPnmWSl`wy!&bS+4f6}Iv9c`R6fBrrxxatgH(qIfUy z*L|F`5Yrhs(Oy*xCeQ*=$knJUA^?*#Y0ILCPx}2nIgyLZtO@X}X1T+VmlbV?oODnT<VRxucEDH-%iaPrK3L&S<>B=VXM$du7HJ8qppk>;vIZLVD3hsbFPcB~_)y$!C z_+MNil90~|f$Fxz-g<$$k67O?i?RfQ5%kB8qsdxL=abpvBJ@r!M~T$k4Axi&aMmia z0?Rb^b{}k)WqkS;U7%glVL)TT0C<}c4}YO*LtIGHkxNX=@(VMWFr=x4D_&zIA&t3N z-(biH=9N|X3Y(8EbHM`vV>%lP6+|zGLE2e5LtJTQWdQ47NJ>hu1f#|SEPSmm1s90U&iUPjl9s0Ie~GAs(&%wRKxxY+|PcEui{fRL>|?RH5Z+G@eq%Box;cX3KQd z1jUD4KGS2!0%e<{k#SH>{{hK2G;&Ce-E3{(D<5fyiAQgS+=S3up&*VY>_`^d zUKUZuf2L{!)oj&7nF%JzE~i?O;zO%0fSO!sy>6{uR`&qB)`qNe#$l^Mz`gRQ7}n&4 zgyUBeN}uJZ>VoVfnj5o~sF73K0TTcmYx3@AUn1}|){jLc9qCLDu2X!D(xB*C%d{JD zc^no)YrI*IHe($9(j@}qdJSP=5i2|G^)QA2Qbr71km?Xjuk=7;CDezs>V^2>utI?4 zS(-vFi~-q{X1YwVsXEXGO99U|P9oq_dZ0(XCdW8)FscxZH46j6d?0p*i*+p0c-{?) zR@R&KYU^@%u&po-3cin_%AtOV;Noa7=(G?|nsx`Z#jw>x5topIkOY{nt@`3*QqvS_ z936dIg#di7rXw}c50H8k&aph;L!VR?&zsE*LFov#YXhD=SOGg9zd-oq%F!Qa{V_S& z4C#m&najSuhzruU5M!|6WDqw|L220{zWY}yM6CB~*`jV_b-U))#t=~+8$)NqAW*33 zvNYWy8iiv)YSr>#?of)K|cvkg? zKaZIl?T8e<;p@$ORM&{wax_q&*eRgddgAplb?BQCIKs}py=ucDntoGBdRWNy+Diq9 z%98mjw&iphb#^rvxh`pd!;X#3M=mORo|%wtbo{OgVR+t@TFzI^4o>Q@!Y|8ZZj+vF zI6i76(}~eakhlTB8y4GlD};u@bVp^~hSIY1vRcr1OiUoCF>R2DZrHIE0j5fLy{N3m z^ye=T09IDL37icJ}F3Ey0y{(v;nrAO<96auUxQ%i4X?|ngqn9^BUSY@%2LMUCj z^J%X+5^5RJ@msZS+ZMrDk-<|up1IiG->eX7Ss4zN+u4Rjn^{@CscGmK3mP=mT#wB` z-c(?rthFo|3*&5W`~IKV`N5rs|LeoQ^Kku;dHBcAf93o;&%gA1eE$9izx&|(9{lwO zU-IDH_dj|6AKu^G$L}AX{qwW`{7fmI{eSM>-@CWGho1iK>Gz&~?P>q?vrm5Fe{%ObcK__|VE5&n-zZhO)8_-se(QZNO{X~D=cm(J<0=#S zlV>;p-~*kfrLXb&i(Fi_E_@9hQ+k!#xlpFTKtb%@odgSw60;^w-eEulIfa{SrjJU_wBX8)>4_=SFVZ_z0 zM?QbVVIQ~_*?9gK`rxDe_WNGSqE$H+Tbilq{`0-O{{H>m`(6s8@b-|}`}V2#z2v)b zQYLS0efatJQv3WX;?FmC?Y&pTZ?N%uuD8?;KJ)JDkvI6v%hw}s@R^scN8aEw@46m& zgU`J4dgKj0^Cz!IKED&OcU+IWp+En`^~mRsGwk!OMc%Nq!2bC4$Q#Dc=U$JzVGRAT z>ybB%p|@X;ykQLe(d&^njG;erJ@SSz^f}ifZx}p0 zo-_ z=!C^%Jb zhkxVYmppv={1?yv>G@Zce|YQjmIdCjz*`o0%K~p%;4KThWr4RW@RkMMvcOvwc*_EB zS>QPq0L6XreJ^S8#fz%fUdMvZf5OFtYj>ZZ=Wpq}ufN}V>513ASNCzOTniSy^}#aT zALFlYRuc;5hf+qR0{_@b@y+VET*mO6A6Qh#C>%E)Z! z^kBhHR79e&3t2TRWgD<#c1DFdBt)+kq*;p_f>=y!M%)l+dXA6NVpEzEm^7JiNRKHJ zhwR!>y@7rZDF4;>c5kxs)g7(RBrEskFUZPtnP=8$#-olS_hR=3sD@^YHDvTvH(V*3dU@_DX+UBV#HQjC zR{p&ktPEYs0o0kM{tvXwV z)$KG87!5zv0~##Ok6)zMy%pZHk6J;CM30EkDY1S8erL*)w7V z?q!>-AA1WrZ?z42?AKh`%$K$Dw0pvri9gbCMG#TODl7BJSn<0zSQ)zd?(s9o%0T}` zS($2D3`nmSP@VO|5vkFGT%262EsLkuaO4W;@}{=1v3{29Db242`onUeD^Ihcrn6UY zbhLEU0ojr^MZioG%*}hLAB-3P4xmj}Ez3xpnYqM#&~xH}@#I+k ztsATi|A8$p-b7sHUsS{(nN$PVMh}G2(Zq>OiXW(>XRYCI3FxZaSdT0qUJl1}GNRj* zJ}~tKE6Qwb8kS|l+hvjBUNCvW%6mK8_x}gGhdcMb`s9xsd};aR|Bs)qPF``hmHhuw zy8B}5P2IMVKlH4ctam8Ov8|hXm%hD@p^&LCo7cRp>)f|1+LWloJVdrU8TMi zZ`FCS?(iQF6Txc=Z0m`||K22+pIOX5qQmr=@NPeo^=~nY+mt8LlmA>}dv&*iR zrFg+ogvngpn8-sClq4IM|m0w#i zHIFBf$%s@}Y=o-;(X!G3l#N14au7(L%#g>;y4W-6ZkZ|=rqCwTXMwxy_1nM%P5Mn~ z*~Su~fFBdhCF*xOcFzsq!M0-Rwq$A=ZZ)#D<=a2@F3m2}F1AJB>u{N_05zKM_SUzg z0-y7Wv-P{b+}wTn4uk3DB4fApJMce9zq?Cd_-NS~SNzV<%CqaPq50E`#F|f&^Hpt^ zj(1H?%hg5HQ=SQxcl9asOr%-g`u;OL(>Im$F*|JbR*0B}y=cg!&1RIa1ea{!iIHez zq%n#cK3d9e+A|p+E%caQKaMtlyQWpu?8h<|)#o!J-AvpqX>jTd+*}$zQo0M@7;dab zh&d*>ZYzk&r135{?&VsWF*FcqOt8mNVWhY8%eV1ZY&z|A#Iz?Xbv_+JaWE*}tXZoM6IW8?3KenOwWAD0Vm+pVyC7x(^5Yvud!tucy0dw*t>!cZs+U|ZnNUbAK~qDrB5-Syq1*iH;(O6F_mtMTdSYgg6aFB;tHSm|>`$!dyzUGWRubS->UP0uyFSHD+` zAFuw;4cBh6y`2jKyt)wBPSc;ta04LMhMPx1Tu14Jf+DH35?6uMXt~l7^pjlR9u;jk z3xU3&a!iOoZ#vx2lVM{_31NLP=W-^FbJtKHaTaEe9u*~F$zw|t;kDtz2;Cg7$wZ(V zb)+nWHAX}T&}*w-dgBMN|)bHaH(%E_T1pIrF40d!R1+szscZ&0L|vLq=CI<7sbdTf2@YX z*wyg4)LFG2l}mY3iyHyv>7CBdETUqRzWJw!q)9!tdZ7`E_PEY;OjCXAQJqzTLmmz4 z+K@7OnH>%X<|?KA?6D+TkB9Ayuu!Z%=t9tFQ-y%-QVaf!d^cD#Ub?j;x|J8_~MD+ z4J)d<2E8|iflt9w>gkhF72o1hyM)&*Wu67Rb`AJuw)kK&LwlnDW;}613jQOatm4>i z8nWb#U5so)M2CbM8)uC4g=Q*ksC-qJ-Lej2yIt>+-Kd{XO@pRJh2t{ltTAVM2@`Ee ziOLc5@dnqr10BJ$))N=|d7rke{8wAw@|NCq zL-C_eM!eSuP+w)z`r?mXbg!0JQ_tG&Q9L!`pWNMl-fz#|`EMe~wJU>1q-(Z-7ROrf z1mntgbu3uE>gP$2?G+>gcOF?I@R4jq!LXv8doy9dXfbtg=&+-B-cx1)>&zFkrOX%0 ztlnCx95Ii(L`Y*sK9Gj^tJ$z+;;iG?K_vJebycg%A@PCj#dI!FWXZh$e_TI`t{wbIdub!E@y8eHLBBg`H zj#lo*UCb`8?i0MmjCj3UZm<0&m*_Btf?MYBXFmVavq@Ppc5e3S`lPuI%lgR$(wooB zvgG#AH4x6}7$temW2tYyd0y|Kxol8`htHt68E^W-#ay6U=>V03xy!^vt6t8QO-FGd zHs&>dTY!{a3V+-Eidg+MU^Z)$L$9K*@ zR8{%k`FHofV^6uL^#As=Pn_RB`$uPAx&I4i;_KD^ccMEPIN}EolU#l>IRL`jH)M#8aakssS!=& zl9in5YBXn4YkJ*xjWg$>jofbZ$q1Pyj)=IH$SG7uG-PgxkTk~Y(MC5{3X~yAugK1- zQk0oqkSexj6t*bF*laXoabUr%M~K?@p?M!Es{_j^7Rvn0K830_bY(pfr!m!26Q!Z& zk-ss}5D|tZFEbiWlq)sXz{Y)YmYu%8Lb#sIcGC$EwABtFaabA2(J?ernvxr%30XeM z+3d(pv$}lxCn{TIj6iqGQsbF1SctI=@C4t0-0@^iAh0#j>2*q|2!t9Ee)4-&8?=jL zqt$ds?)0m<+MD=eVzkDy^?E+3shOI$CvpZf zTji@WyfyHr!ptt9{%p1>c>Cm=t2XcfwSu`UU(JA4!)z7_9gax|BV)i0 z$#S-9G`M)QhKY#KH!?AnI!?dcH?eMB%i2JEP?gTj-J;W87bew7+(y@NSQ5ukrvU3- zn-eCsz(X=j)Z~RsWe3@%t+L)gh&Fk+7)^RPpwP6-3vTAjST}2Ta?x{irk^N7-8;GL zkDQpz%WAfpKCoJ^T3sue&2h1YgdIwptd=|!`fJWK9EMR&{%~d8kOoN!p;qk;#QL-D zsH1^xUaZxVWt$n-QN1(q#Td{;O1}3a6=KjTtJA727eo{oT;$`dqXto%WH><{(Ia?} zM>NoBGpZ-wn^yNR6aCCW_*ADrBW^AWGp)PjX@OO%52n%t>~+g>+Kv7kFtOABt3tSF zNieXiw>B%t5P*!$Ed|ldHX?HKh82~?YXi7Z>&jj^cK=p|aBOSU;&{X6sL2uokzqT@ z=zMNO@Nm4w283VlB3AMYG_7@^n+wj`&{k*UNX(oYN~Im! z3($l1{LA|o^`!ho9sv*g-pHM2kqr5T@CrZgszxI;S)>%?Y{S->T_N`+N$+3 z+zYYa$rmZO9QRVaPx_=>o5+C>7v<%G68IX zVjN)QLsXMU%()3Ld%t;!Fxo6HFakNKCn&thy3P6KF>3ZzMIK`jualzPlb%y<*slso|X&1D4+Rp8%1CfOE>*|o3!G2M?ZXtfI5?KnTUBelW=7% zGwE!~6%os1s}<^|dJmSdVc0Q2d-v~E2yWJ0$7`xfGJuq$&@}GMGA&;sHE6>VAgN^C zjY)L-<8^fWlNF*Jx5H_Tikz^(dmUk;QLI(agJLl9S|v#Y<#)YNa}&z*=;)7C#-)uZ z2ghb;L~W=fwZIoXYD)Qd{+6QmSDZM5+C98sIX!t=*H*PGO>o`;|8R|qs-PuYO8l_s|?L*u+; zm_Yhc05DiW57Suh@eD>c2t4wRzV#B}$*4GVCkj0i+?Y-|e57|T^VLuiOskj4yqKB{UA=O8s&YJ#q;Izd8Ux?8ZE{|G!+cku62Qt>dA*9Mek(SEN+0b&+X5oWi-H zG8n!gC)~O8|Jqq}s5?osy%YhR?8wmi3hEEvJ(hBGsiB zOA{>LG0)O?%^#KvhxHfxBlc@3U$H!x-w zjzST+2MMF8T0XHh@x6np4L#^g+c7pQ)-g6NWW>NKzpfjMw6z z4pTgquSfoJK9Z7U%EC-`aM^>ro5taQ=6bXil~Z3?eZJRUsGHS{t#esvLya5o9AAf7 zXz$&XR$Q6)I$REDn-+#)pEu{Vu&$Ylp817jxM{U0veJY%vLXoyDQL^`C>zM@lAQect|X52B%cuxN3rFl)D0n5w-zHahLJPp;uJK>NCEB zo4uZ9PY9azyiGed_P7eskxhi4+jX+#rad4?O?sQVDzQmA75hHlCi4YLC-b#r9DHGg zXj^L1lPJE<*O)NxuwBfvR%)=x2Vyd^!J-bO0w@`@i5>ji3PGuY+Fko*%~2bUt`la> zaGcKCaKJ2T7HelVfP#$WZDLMsa1MNjuXrcwtZq11m7!PuV#kQd(;?5)4m+*3=}7qTPzdD7Qx@8IQq; zl`Ll79e46PE5b#|Y9rQo#F5)N7q%zfqBS(mDmtbd+ptKnsHL{DFhyDyTRG8N`VE+c z3DPQgmWmo16oK8zfwQt=u(kJcj!5mKB$Q$6@wgQY+t`ryXP{#U+D2~I>?EaE;kr5T z$BXFfA6B;FC)8$I@-5;FJvNxE#;Q^1MYz?36q&1;+MM?FT06>C%GqD25Tp%_U1P$O za}193i1HWF7%AqY#ZH`L<2@~3{Mq*wo*|0juTtx|NvzUk}0&?&epDnFqyq(7?2kx}z(kHfh?aQmgfgbeho;7f;4|v)oei_C8S|)=U<) z8Uqe6Xi4ae2VN^S+P;Hl%Pvw|XasCWt27 z7rT%4#;5^iXv-0Je*fri z?|tq0j~y+JK791x@Fxzx?cvduBk&f_{}(qtC~`64xfE^=Ht~vNhMAL&Ei3EuZJq2O z&5_<<8QQ7=oA|7Xp~Hy+TUK*R-j)(AGoR!pN6IWe>$^Io#Aq7;bQpx=d1RlybkW8z zs+r}yx+=;!)|OLSY9?H) z0ys5-XUf^vREV^rBm-eK?PHvl5GGJ6uJ0Z7K|Vi2~en$D}dSk+zK%hmqS4g>yO;WCAE$k!H9&G zn@*?@Qk~(Fw1G!lGiFMTDdpsyl~z$LqH~a3EO4V^7jV)e6TT+-c)6A~yA~OArLHq4 za)iN7Dn2HOKx&b0={}2Q@=Dtb6sACiRKKqYW^T3`DK%aQn|TMop|jnpjev1y9w(p! zz#O*Je4fLSH(D0!0q5|EmCj2Rr(~808%ezPgB2oAu{w>ehi;vn+sG<_>wJ$Jg)LCs zc;ji`P8Qk_Yo#VD-uv$>MA2WknA!mHWn7ajDRAc!Z`Cxbl?*$AR*-?;1=UVXC+&L` z4bG6t6TH7tCS3^>I?bffC>OoBHf_u!GM-NgIZg}2K_?(#oPP46jlm`fqS6`Q`B&7i}BJ!N`9;<2B~ml&dVdKR@Oyyh=gzs8ctxQp#xJX zzW@)0;)l~17qmC}v-XO*qAF!Xvz#ICy8 zkki09Z1!dZHK5D<@MPT|Hsl$$7Bb9Ri)W(?gtX;9)5mbLTv*mxi{+FR=$ezR1dbd| z>Mda%f>u~AZ=w!U5ge?jqFd(@=|M5N<*oCxI`ULi;7Ce<3z z%~~@N_(H6ru7qb;YssLn1H%eF8Ji2i*=Y4V2~ZWm*j~obW7;b@^g>|2Vk?v272ju& zC|3)Q(A4fq2M231&S{wir1=IeYj*(V*wMI*I{Ay$eZ%vP-;Zc^k++1-Y-sn(W42UJ z*bum&$TJ@-XWK@3Jg>WePQJH7)Ps_Uui@kIbdkgJ=_c~sfnStehz*G*lBi>8fK9+D ztz}0)bBO>4i(a?ZjL_Dq%(kYRD|QRm)6||!4S5K%?ZAHUyBK6=| zvp?|7l4{G5>$=z>z4BF>vms}O5nCG6O_jaxt&CgKg{iCd!nQyaWR^RU)-3tG+GwY@ zHqiOposmO)8iJG_U54qRF0DwR!=Px6m(-anV#LY4qMe9kvOLq9lzeSncsvi-b+p&K zxQ#m;r^b8Es#{oYDPq$#q2mcua91Ta(37K>?ch0ZMb?;}@w5ZxJL>$1mt?(G zm8Fo2MN@ieYxh`3M+>JzIv35>O2f92o}b_|sOTfYpwo0Thxh4ObT}T+Y{Z6+hqP)t zS}Eh;i$z)*C?Xl6N&{sm$S^o*CSUJ$G#)P2TG~*8GxhE7Io456i#UuYu|a7BkFw!X zHOb9-R2eNYv5srSvq@`!%AsPZG+tGX5!FeI%aodReHyOzmG)?q0%Jj~NWsmxppX&A zj>^em1CE>gjd>`w%ogZWyqC$+yb!HRH8jn%z@GGh-4Qc`bQnPs(Q%_Uin9o09qTAI zE7Cy96BtQWVu@~4ZZ(o6wj575(+F6pMZ+o9s>b6PxxIEgKvj!OAz@S+TryX$6$A$X zU9k$&uTLsit7=OzyhSK|Se$~1pq1kr(NxPSOsj65&(RIl=#`UVtq(@UO~;D1Iyr+> znv=F%wfL0}A0yb#xQNBa1;j0sz-@#Xb2#Dq;9kl+Jz*Ou zy_Bw&P=)H331sa?Cy~reV!CPwrG`Yt3p3ZGDrvG=@34b`A(ymavNFPZ8HOD)>&KUN zs`#YqI9fK_GPHge2J>_%UkoLu!cem7daCDk`ckhus)|Eqt$$oAw3*6+Ibyldg?rho zTuPKXWi)Ff#l&5&xkb`HFMX_aV$$?3So^-WH!hD4`sB*EaWpt$_j zacWoN(PGO?PMT2^rJ_AJGis0-w=Oj?r#vhbxvGHZYMY4FX2f>$xE;~rqy;ukI(cg% zGPO#0AX4DFT}5co&V;ORL&3mjVUcbXQ!DSffza|`-nW{MMG6f>P7;ySRIr)@gmcAN zv>Uf7+CfI9ly-gFfJQ2wJLJsPPT6*6$EVknYG!O9s%QH3EU|=E%p=X>NMGh zK6I=j1YRE6s8kLksaYL|;n1v|5WuHwnX8f+rI7553A2(B7`?rDBU6V`H8GEs%e5F= z9!EmqVzdChQkpK@16j9^b##O*xJxxf$x5vbM|gB#R|f`L z-#H%tYq26cor0^WC)re4$h0c$o-)hIiW^H{74Sv11NtU%cQPp146`TlP z=!*@!RG&^dy+JI;)+3ccdQt)d)CoQfH(J!B+`cgx47$;czQ##VKcp2Kg7yAKXKd+w+nK8 zIpoGg)le#tZYI%IGJ{BI7KKY#QRT)&ca*N+waIwz(+h-BaElI8!kLOG3RSw?1V0SyajA^j;&^CdA?xb;O#CwuVC+oHW+%hsP6c)J3?| zBv+M{er)q!9M`H_viV_=9?|-+rqtCCRs(m9gG)|hRXq|1M`neisgN0Iw6*Zw56}1i zf7421?&klw@rN7FUsu<@1Nz0)FR#YIgI~qZ zTZ!wB?zoRUB0q2Y+R7Eq9m3gRlVIyHgr73t)5^5LI0!lGrkaUlq{wwraUmJ0L<+pm z`pMYoV;duBzvsNio?LcDoRM$QHFw%nsw7jld!9=Yj`xAhH_F~|+gd!9cq?{&wrI$+ z<_+Q76(WwhSl>nx$vjsEmw1+oOj+s=hSNA)?nb(8tU2mB@pO3E3NHKH`3;%xarbFP z95iEk(u}@SHd~-8TjgA-VA`q@vnHG~(2Fg%(x0@LsA^4%a)e8xF~91aK9a-VAKvFK z8+tY0)0lce$(MTVa$Yt({s?GB4A5$FLaRcqjnO70WGy?7xS^&JhB;CT^5*D~yoXUu)^uXw+w3s!Ln)w(# z14~TG#MCwk#?K$#3NHKH`3(v1M-((;bkdBpiKOGLSSLS7C1Km0={dH7aI;YZp1g?RF7^N98D=%BYWuH60Apx|4K{NU%%>aYNPK%c! z;Hb_}ZK{DOI2A^SaHBhPT%zNIHJPk|dCx4)`6k`dixmB{mG`;JBc;g~+AQv3!w!Ov zyE*TTqyfzc16uV?XjM>4X;T~)jRcr^ZFkE9Fgw$+LQxz^=oK_qO-2SyB@64tLfy~G z-s<^0cgIDV=QkseR$<_e?ukFJxEOPha!VZa_~EGC*TNMc-Wg!ip@OFJk%l==^(ra7 z+vug1t>CiHo!=0De?-%u8J&}6q`|r`R-ZClr`+imwS)xckp>xV<(X>BlA1_~WID-l zs%jH{w2FF9vh!<(_qod@s;(2QVwdVw-Ns;?$0|!_t7r-|qkYm0J1>@1SQMOSM+B4n z;7;;*%qa0jRLDv(xJTN&|N6k(y zf@X0miiZkzg$mWVS-cL$d=o0wK|*MDjLLr1JKaSL`+Cj$+~wS$rD%MkS|8*Ly}}y| zzPvZ)9mM8IGhl2Wsg+TV$|-|hs8`R#<#H)R@FY`Ck!jF(*Gh@5jbvt>v0pR1J=kTR z`>L8T?;ti#nxTz)KnlbN=R8E@lH1@6g zR4?MK-m=e~zbD53^%sHX%~j$OM8d2!)Ji6|Ec|d zwU6z+e~;e%z1_~v@9sRh{ioaCvi;nx_pW_@&4IoEy&Zb)>bI@FbmhChiirUabnDrk zKYab4U$?;5E$~0B1>Sn_1ik6XuXyBH7kdc6jnk%%_pNK4mPUNNQ zbA0zR;IV$A(NAOecK$f!25*8J?H$D(^H>A*ne%uEck60|tVi8)wmp=3DvN4Jn<(Uo zSZNqO&0}R?=oU|X+0hu%Qsy5G={QEs|t3hcSL*4V`*L=&~$D2 zB~(k-=N6B}8;yP%1K;yUcuVp4bQj`gJ{CZI<~(*MeA_NpQFqv{_A?SH+x2c5>?JIg zQ&_xQtMXW*9OID*pRF%lpR;((1ATKIQ}zsvj|Mtso4wFDj#T4FI++^RnzJd=4;gK1 z!jwx~ku8tHOY6Ix#~jcz=W%?{D^>Wgs32hRRo_Ib%}lN1)XXy4&*bayLJ zDcZRPtaDR+6yoHi>vN08%#B7rjVXf*>Q|iA=!1>VtlYcuw`&{Cjg9s9tj8~X-=*;3 zM-Jb3@Wq2C4qmzc3;WjI7xv!1chBy>+m&|yWanFUp1b{%s~=i@WSa({kAW>F{|G$^ zg|>cgYqIr>&G*h{5LUkPD;}r-aOEooKMkP!_?x#MxxBLR5@P%8VM8qGoex$l1!D!3 zmC@Dacm77`3%9OllH6Y7p8M_*+`$76ApL2p@W2DKrb+jQUlWVDkH6_WJ>G<>skEap zC{7DN&!_+TJD1e+=y`g)zBe=hY%?=;ZQhAfjVU-+xHVK$uwn6#8{x` zlYduSQcw9jJ>K{s98J!MvO*AZJ%9c)OX?||r^m}{2D4bw){gD=$=~{(CH1)H>G8%X zud5lIlu6l`x91aEOX*pp!qwE%$cSHm^o9l8>zn7eDCG@R6&#o6$V!G@gzDd4Udl!8 z^^NoNcwa#xNjs(k&P7Szv$Rd?K8(ywGkI&B0lklc&gR@me)Kp6W?fLYN{o0axrswJLrczDRNtKp)G&9%p zj`uIAXL6pNJM1QLrY&+J%g^;xwwKg1K2MLA2ZnAcq~cJ*Je7X>_206jp3!-FyeZqs zs03@v5+ly_(7(T=p5b|VyeV7Fh#Z5HGO_4dJ@ms%>KUA;$D6V##a1-k#GHBjeEMZS zy`-N0d3wBE9*QMMn^D1byt$tJA6ZgQ?;JgGZ+=5FXhtG!U8R7YPyP95Nj=^3^my|d zrl^SOu#Pm(Z=d?aN0!vnIZuz5*QkYjmg8x(YLLVOL>d?SvdjySY}pt`VueICH>`;`$kv=VC-x z%~LK`k}*0g_Tpuf>I{=1a9I&YuGOW85D&Y8)u9z8oSkNbDU$4!6_-p(wH6&`>+NwHdiLFC zvJ1P;9f#RmO^&)oqo13>8MTf^opApQlc8+5V~wKGnKdlNO~!O4xjH))v>{EDIt6v) zFg;@yvEoFfEVrST+=V8)D3)Qugfl9#IvsSJNF%1r^qyWni)DCjI;hLFj>62$VYo00$1NVuM?qk|`0g{AaADW_vKf^`qpE3y!1i=WF#Ap0I?H6( zxL6-!qqv1LZY*NBhN((nC7a<>Cpt>E^O>D-vK!Ny1E zg?FLJw2LyCP%BIak_|gHI2uA?Yl^IEaAEYI+jR@0ZVK)d<9u{dqca^- z76?16kz-^i$`yhKf%$^F&}0{t%SZ$qyIzce!`tATMMiB{0F$ZhvrIOYCBdpHSftsB z<?zxzzaUsPZcjX~b%P!2)Ha}Ct6O{Za- zsWVK5bw(yv2!V?X`!Nf}Y?K|N>U5URS4RX^*G?QvqwS z8;+?CSd%7M&8RRk9XrEhXd(hm2}Xwz3pPnBSxQFKSvhNUI+4WCRl?w`h+?)&h8469 zj(32bv$C=py_j*t|1whUI+jKGi((n=4BS>n)x+&Er&U0|On?iteFn?$s!3|mdSg;c zrA7kU=dfB=OHp{HAf(0JBy`X1Q7!9F_;&GhhF|w^Cj? zeDdJcdq2OcZGUd-&70r2{@%4~E9KQsTv)IjmBIH8=#k6U3o9$o;n^SE!9jbTZ6u0m z@T4H)j(iu*8_a}@3Ihy=CBw<(Yj#wj8Z)gqAvv8t%cmtO2`4zdSMEr~ag*xB(+S#b zYK1u2npPTeB*(Svyk6~%!H5o+dh?d4FZr3;JIO1tyhCv^hB|kXsl|&5A2O}Fb<{4^ zSQPv}$IVZg|kp&5ls16tlY4n<8^l zuiY~BzMrYR^Un%x^SX&J++AgA;i6296B7+DN&?j$=4ODVGdp|8{whS(Sz#T=pn^=8e z6ORNKI8E)bWD*m-%11;i4z~NA^&Ul$FadH*l`Tet|{&t4UNcEm2@i{%$h$g|V85M_sE(n6x9x`a~!YouOPm>-~#N zLP{NPfTPrrZaSGw#k+~BoloUBL5a}qSTBvu5Ju?^D7{;rCy~tJJP5#RI!n-B54LE=C7X_*pbFV9Y>I)kolVn|_46tya z(AR24iw_G!F%mwj2hx;Ft{aloRHu|^G^?;d^-N`o7AA3FK=i@{R?NHM*+{UZ@!ZsJ zzGdnyA67@apZ!cLBU+{{h}>Od>gk0|-_u1x#f_e56(%|r2j|}sO}%l}lovKordp&M z?^WTU+!+{gG6T-7w)i@yH~D6@-$^DDg(3k?cIeC#$m5UQGWDjPsl8t$Q3T7-3OFuV zxSLFUadX%Pr8H9mtwl_7Ewr6$q$>h-Ru2RnWF@#3BV%zP4;w_Kl>;YxS92|fu7UOZ zQN28p8hAyifNKC2rhd~cQ*Zd0+WV0i6I4QAdCM~HDpOz76dPsG3XPVSDL)WtL&?*P zy52f#?i;H&CqfaNo}y0mdasv===i9OIJ0good)+%nJ68O=V=%gYR)28KYGj5>wc#8 zjuvs*0=L|K{Nhdp1(X5RuW!fc?D$Qps+Y-}R8Xc6QdP|er2r5ZDFWZE@)G7|J;N!PG@M_=_ zu!tgmm%jP%ONSpkeD|Sq__Bj99Q=oaw;jj_FWmp!{?F{cb)VaR&fXvF zy>D-}N9{dx_jh*RyW8J=aCc|tlRH1S)7VMwK-(YN{=V(P_G`AjwDrNQcW*gcuh{&n z&7a$R`=+}2;*HO3{Pf0KHrS14t^dLL`_?Dx7*v6xtN*n6(bex=&91(B26SUy2rMo7k+Yxr`d{xJ3*7FpFr0?N;Yb!v!O3tq6i;~vTdU1?d@#1M z`sE-{#5-!f0s3el$Vbyh0zuwB?hQ>J4g`63Y25(*r^x!l8>>E3nrADzH%JfcUC41m z&xZm*-Z$S3&<6uSJ_i5AAkZD>LEO;v4}qYw?Xddyfgm4)e<{e|-d@>p?Oq+E=Z+I0 zZnWr&fgm3*{aqmF?AopVw?L4OYyUP7!kaysJqk4ZC z2=XD(7Xm>(B>IaW5}j@FR|V<0V~_fc7X5#LARiL_c_7G#M1K|tI=e}$pAQ82kmyeX zK|UnhA}FeE9Htfgm5({%#=1$F=`85ai?9-wAT<*-d)I(}%n~sA@6qdWsut z`0YTDPfGbOfgm5p{#GE!CuV;$5ai>d&ji`-Y(+17`j9I@RlDOll^f>%jX;o(xj!8U z@`>0_1%iAc_LG4iAIJWBAn0s*uYMvBbheXLKOPA33G-hI1o=?tSA+O)wxX8?>A7Rl z^#&h)B@pD}+K&Z-d|dm>fgm5({^$Ak|A)Zg{g*y)>F5%6Y5nj+hi^Ni4z~|JdhpJJ z%)$Qt$M)Z~FYZ5M@8f&ly;s_M#qOW%{^)LP_r9G!+j-wkYv=y$FKqwRc5gen^*3Ap zW^1&S-2CF^&u-2(U%T<;ji1|i^TtCPE9)OvKU&Au*VjI@_O>+&te5^M^iC)P?XP}p z^N8e8e){f!tB;S@AGz$es37SLA=bzkDr#XWIMro7YP|aRC4NruJ?S+#3~!Ea+keT*?W~137McCkXBhL=HDB$C7Cc9FKAgZwJA> zZ(ahkNKwIM^_KQl0Q`;)x2&@nM!+rWR@<9Fa4+i^h0=9KuxN1B#&P8vL2xhYEZPvk zc~rW<-GtYJ;9l0r;Nn}_Qi;qh?X@7dmvy|#V65z*)GgLkgWz7)u?U4^WLC_8^UjXj zUI~JGS*LJ@gE%rR-tv1n2<~N_qGRA(Djrd8RlXDi_p;8Gam~pP7zYkBI= z+BXEjy?&Q)jx-PrXTUj%$8CQk2=4W}2Hr3Rs#69yL+%*<`T+PHeR0$lRT3k4^_Jxy z4uX5L4OrKh0h@7c44m+EtlbHMdmX2ll%;EuEZ%Cn9R&9VEa_mpY2$_p&b2$%ZUw=; zmIoJU(KK#QI5=kM7;Xl@@90;H9C$y0P1(vV(-}c;c^h%>-Up&JU8J|5ZoKECreIl z!o?uCmo_J>BW}Wl0Qeo@=M`?SSo zwdL95O7SeuWE4!qZ>nO0RCzga^40GqoC$(^Il>r(Zh(%^Nk@hbf_piF@!$|?#pF0} zhUal`Q$cXAlOL3eO)WZ|us`GY}lZ**E3V(J-D6K}EZ zS`ggpI8!4nK_zwVR`2_25ZoJaat1>g#K7fSy|C8?!M!2F3KXYUJSN|=`~yL7Z^+oF zO^GBS3b)?IX@7Vu?RA_1h7Fh{5Xz0p&%gh7pf9bgfB*WU>)CZ={iW+0Yk$1<(Y5!j zy&J3sP}kyX&w;*l=?^Y_@X~uPz4KDzl5i<}>6wRrbNK1Qe|Pu;hi^K3;~{!@<#6-h zPY(X`!H*rh>!5R>9K;TuyZ`t5zrFu|@BhpFqy6GOx&O-j-Mv5C`!#Tn;P>v0_N=`J z_FlOAPrHA(`(ZGf@a?mRm$XY1#;erW6QEq9CBx^L@X^Dj0(zWJ{=pWGa8+MBQ4e9^{#-}u9g z4{iLbjdyI+H~5YFH@;#0uh&1d{%@h*g#H8c9_TGl4#J>UKs&3S2lEdgY7SEe|G!R+aKNj_mP#Awg3I@`F#HMnq0I6UbJ=%%C*|9P{g|&A)bIj z0i|*1n*&N?(0u`=5$M$cr7-lWfYLDZ$``D?cGaiOoIf&_hF%d+nu1;)P#T9`7El_6 zt^}0A&`bSm9LxA4qiN{gfYKCnk8f!LwDwyAN>kAFfYK!NctB|aItnO_LvIZzjX~cM zP#T5a5>Og}-W*U0LyrZNhM_n4`XCWA{E@&1j|P;cpjkj^5}F2-CZI_`X&f2{l*XV@ zKxq^j29!piK|m=C^#e-7P|x3vQOzG2NkQF!(j?RgC`~}^fYLbB3Mh?1&4AJ<)Ceez zK=pu97^(%7hM}szr6Q_75>7*vfYKCH4k%4RrGU}|;*Sicp*IASrl3axN)yn-0i|)s2`G(0uMa4VLRLU&1hNB4VJIL| zg~O2PZ>g~S{I&GiZ_UK>eO1QODab#lhPNJHs>(iD^mC{04ifYJn%2q=w1@qp486bmSgLeYTI2own@g&{bgGz^9P z?U)FN+;|GQ-(PM#3B6|i{hwa>@s&#-z4YCe9=>$t@Xrr_=I||t^x-!g{MNz$anLx3 z@Bh>Ohxeb@xA*Ve`~2Qd?>)9h?p@mb%n`4ednsW`ofjZf(o7a6T6;Yzjkfq%rQ^`j{5yQc0CsWi}=7IvFmRP zfWba6ICec701NxT!m;ab@P~!dXT!qj*!4#OU@0G1Dt7(#0kD`4EEc=|Z~!df153oN zI{~n`4=f(LZU?}8{1yg&vjSi~ED1+r*UbQ!kKe+;Z$<#jhb3Y7@VdVKoad}8z6dP- z6T7bY$^qSxGi?O^9=omvz>+?&WbC>U06W`}pkoueE(gHQb|mQW#I8#Lu(Lf9PXmvL z0kE@88&87(6arvpJ2IXIj^zDesj~wxo&o{L1;Bj#mI8ju1i*a!mI8ib17JRWO98(z z0WcrGrGVe)0NB}CBAxhO$NZu4)=HpggX%c^I=H}umlf)`LHAfSc3V( z!0G*GPAMhmd{~kMEO{sZ=EIUCV9A34FdvpA0ZXn0 zz@sSdsuNNd~}t zSdsuNNd&-rSdsuNi3h-ZSdsuNi3PxXSdsuNi3Y%YSdsuNi3GrWSdsuNfdgPZEJ*;C zgacqcEJ*;C-0u&I`?wqA*XyqdfcdyP4%{6IfcdyP4&43C0WcqT$AP==3xN5!I}Y6a z>HwIJ-{Qb;uL^+q_$?0n_R0X5kKf|JZ?Eu&#eAYW2BQ1r0WcqT$AG(E769{cKnytG zN&w6!aAF{EUK#-Nv1ANb@?L*f)W;-IV3NxLFdr&M0hR9wfcaQ53M~1O0GN*@qrj3c z4uJUtP80;rivnOi4u}E=yf6Uf6F5;2I4=l*`S>jg&%gg)gC;BMTWi0v_T<{@*Io+! zDfCm9K78q2m-I_7KK$(Aj~(_8A2?h+_{hP#4~&Bs?f=pK`}PO>uic0CKDzhBp1Jqp z-Ouj+`0ix)q20}$kL`T#&ci$Rg1La7+#YYgZhLF%SGJzqdi~Z*H~)0=r#2^>4{dI4 z{OZP28;@*US^xa{``4%IuY)GgL(ul>udY6|`pD{)mCvud|IC5^sXwm%r&Is@`ak|B zv_J|hTDf_GgXaT$B67!iAJjBdRn;UD`i=l#Jn3EObFw`HdN>g1UE_1IF#~b}f!UOs+NwT z7Ofh3AxIzWT|$O9EVz8eVhsZV!xjH9zGJ;*Mp80H20>68dOQ&5jel?f3`5|oPH50k zAka%Vn@cZwUl?38&+VNwK6s zGtip@fnLJdluj#xOezxeSRl~rf1Lm)+v_~a>(HA5fnLHnmIbaOQguau9t|R#7sskj z#z08Xq4Gic?pSO})2!qmCV>#pED-2jW~!(Hrh_eViVjT!fnMGysD|+p=4b{q4g`96 zgOR$flVD9T4vhkVUfzg?X``ZQIs`Nc1bTUs5iyLzNX9XtK_Jk}n+z^!n5kkS3k?H- zUf!SzYJ$Bi4#z{iK%m$E3TkI8(xedy>c`h@zeMG=scGA|B2a{*zA%7oac`Q!6@wEU z2V6gz3#cWSLHDf3*C>HuQI-=Gs1peEW+{!3SRum^85L>=0=;YlH-l?76e>zkHi&RuHwq*p>k8ObAZCMXddK-u97kB3#5$+~y&(|jCEUr*cj%Eoptq(c zd)}ef&%gh#ufBif;2U?of8*8Qm#_caDeT;hssc@(}(R7kzjFUxt;trqWn z;GTE{{^on`?GJixu9g)$av@uVAHNm@(O*tm8eN*Scvo9tuqw z)3C;p-A>WYHDH~a>Z1@RFP+Y~7vCQFD>)UbS3p&HvSC7jlhCyFYj!l?sPX%Cdd8H*$N~g=gV7IH55X zNhjfWJPhEKT)zTLFt1R*l8eL>b4mSZBNJ#zcy7_X|YtzNDO< z79#dEjZK+FmHX9lD>oSSZV2*JWG>q+M&fZW@XTe!BGR7gU`w@Bqbj94Mv*TuVzgnm z%v#<;NShM}WWUDWkHou^cu{pL4W}um3OKHz_fu-oL3802OxGyPEL8efnbwLWIyNi} zbrnJGeQa*RVQXF^cTRi2?6P}C(;s_m?d|m^JM*}EcVYGEu>E{~EE{+5S#kG2JdU1j zAbR4c(;fqEH9+cU{BuRp5D@&B!b`{E1NVRfOBYm`W3xrKlp8kYaXF8PTWRpoo5`XD zZW0HDi-@_AKTfrKAr2sKlvS$zLAy82j;j4?zS^h`=Ftiio;*>QFDs~mc+KV-)!e-D zx696ra)aDFxx^QBJg$$YlE;bQq~Z&FJ~z(87rwsytGS867z{W!#)elp0_j&+rO9R^G2vJ0La|FXVYeir`AVMs9i|=I=dw0ZWTH9Fx`E1x%WoD`473ip$iG3puPL6^kK%e?3p zO$U+pt<@)6^T>NgcJ=9WQ!gy?!s$CB?~GVFBRUG8w+>P@aNj>ebSzYYj$^;tUi2?x z^Li|5)P8y$-`j&?J_tD{DCTOuVzPs7Er6d%_L$faz`+>swAzISsRFp)kf-6iP{zt_ zvfecs4q;9;5uWC0;wvUQkH{F$I_#K=45=QKlBV}-?tKb=pF0#w_4`EIF>6xUD$Fw6 zK(E#@#!Yo7wn+}#35Cc+>TSv`m0S6SRjzl?N-xtIMiEqAj_jc6fb0hw_R51R?_a(2 z^OxRy>Du9!5C0?B(|`5g9}oWH!J`MS-T(6b&+R|9|KQ$#-}{BVx9*{PYr7xZ{nlM_ zcWdV(JMY+GclNe_dHXxJh3!jQzqa*VTguiqZhjK%#Wyyev+?L&@eSYrwR?;a)Xm*73)0KR0{rsCP=eT(1{fvr(~E zqjMxxpYa2KIJEi9XJ0$&p2Fk9E?w_vpkJcr8)|PN*ZlG8ThH3LcGNkA7jmtN(8OAX z(P=sg+tbKE{BuJ`?S1$*^WQ$V7>mT>@e^~eLmQV~aP6pdN?*TQM`wBPNk`NKb3AQz z{oS<>ySq%wl%;_nmr$~8%=CfeTi=b1XMXbPQR5VxbVRP6b=vK)CfTZ6GRblQ+Am>Ok1D6&h)s5T&B+*RkaDA-X?8aN{>)EaJ1Q^WHv_Fh zw~1CowkzXCp0&Cenec^gLYvS2o~uXhDfJ>o&`7NSE_oKFCPS!<>W|-irg`5j5+2|MJ13{3-3a zhJdp!d%}dts|^aBBK|pMXKn3L=r2~T9=-9n`8TrEtShr65>px~flHZ&EKPmuzrB0! zm#-dWZ@`a@&$f_^CH5NAYM<0QWpJ;$FMRVE_y60gM{hUEdg>vakrk115 zfj@EzPK~*u-o`tPsiC*a7CsyL!Z)^__0p?HuRjHEl_q$xP{vE`_GDT`W;xU!zWL1P z)uV?`!7U1Dwn{l;RKo`{I~yt*W2aXV|`@{FIyzuIgdZ2>bh%y{1|{3)WUd~Gr{JX`F>N**1$|U2_bVe~>i?eG+{6}PNQxPuWy6Rq?GmQ~Jy zGpFD=@WC^qQkF!uL9*}V3E><#eG1-jSzciW7CK_8*kB+u{oj(u0ZW~N52QLqv1q2* z)h)KI^jZO&U$mV(1(#b*u7@HMml+i4er{rx0xN&*h*-dH_An~sxLPjSPJL_(>ey{4 zv@d)U+I{(pt{&kx)Pv4Acu9A1cvG14MTg0NS7d%-1$5=NUjTaY*eSR;Ky~nf($y-? zn@WZe{onDM0DkQVJyrV|jh6E>-J$yxfpSI+>d)GF?SJ9HBjgr--o+?Xi__uMts66z zrbp9uGat};FckRy2ajHNNxpws6E&X%z8#?GxYfQbM&kP&d@91q-6YD0qIi$B)V?&EMy95LJnqX zuI%5@IriX#N2yZ+&i%-H+beHDf%*FXU1)3N@COcJdmrDe?`&-S;AR@yTL1O6=E@J< z?7#lA{(rUf&#L}^3FSw{O>i;iOMF)3gH2!_BHu;V*G7bkx}u&$ra7t@Q^+!$v&g6o z3z~?vGoYOG@$j%6ig#=w6u=z;+3@hWjGAzM%sy&*L z;aQ!SIEdYD>WOxiH~WK*R4ceWu;F@4+LQ8aR^B_$%D2y0&6hshRaU;JD`s(_R+tVX z8wMAIG*F4HDYCAW&#*F*wg%BCjSsqAw=n9a;9fD#M<+Ep(=la%u)`WTMuwtXA$a6A zEAO6X<>+0UG5Fa9UF3_k6LKW&=34Q(Mx^@U%wgr|SzN}5u9~M@tR!P}SnS2iDA^Md zror?{*vyZrWt3)N21Y97!tGYxKF`XLyV=SYl`)9MAa8Uihals*25Q)*)3D9d8C=FX zBa|L96H)Ra|!ZL;e zme(~LQysALdp(n^++5p`D1TAfZLF?5wMTcC^CE*VzP zdgk`G-{yH%zI{^kZp+L3h4nEi9nEBFIxNG5dOybLddYBObtxjm!>(X; zXoU%9rx{_2BztAWC6iLEMaS8Cdwg47-Z;u_K72j*5}!?A@&92jKNEDAFcdCCjHwlA z)EPWnmMLZitd?t8k>5N~-B%&P~Wudq%X!YGSNl968~tJh+0VRqS_ePZw+RZ&aa`f3tGw zM=vRd|8V%jhy1~RJNUr^djBu?e_$U2XZAn2_u%dy?>@1c-uZ)_Z{LY+|IYT?w(sBi z%+_1CUcLG2n{V2D*~YJIj5qFC|LA%TeC7Y(T668$(9c5^=o?o5{c2%#ABevAAMGi3 z9g^L>S6)7M#_CE;HMvHL8`UeLP>5?}({+TM349zT$UYO-jgGb{=sy9JZr|ynMK(J~PE$Edqv~K|P7N10 zD-YwkUI#|VF2vrugxE>9N9BrHXKFhnHbCp0LO-Pps_|k+Ni>IAA{!X}&FvPGp4CFHgMk5RKbOmH0L6bsr@k1WLAvxHbS z)MgOcZ6paRm2;?!m`V?feoBj3BdM8>(KE2}txQa9lK@6|{X*$CKTY$I zc2|y;hEcaq%^a%R$3A#F$q!OV*mh@n zj;$cvY}7y~SW<8_+RpcK`MfQIJJ_Cb7Ghttgjjjh0u%bB6x_hXNfw{Z$f;YIf+ZxB z(3j;>3{F&$ky;*55nX%AUWk3+5@Pj8qLNMZ&|$5dO&Q&htvFFw6rE|COxK&!Dc)`3 znSxWHVjw423$ZU)LaaKZ?SUedvSKQe8=%ER*~SDTDG!p3be(s_p{9`(6R*+{Al6)n zef|<+^=`#9I~>Q(FeO?dhUsdk)tztys@3n7;^9UF3m4*(c1%s!+Ed0t?DLio8;+Ix z6Ga&%Y6EnXt+{Z#JFc2SZP2ab1U-x)a7`$bOEY3t)}GQAVxPN&*t9XO!03>g35isw z(}+c9gL*N~R-#i!PF2Kk9_;3W`LrP;vi6j=5c{0V=g!<;5yO#Aip11?|bSfMD6B=icJ zt0p6ZrjmvAVxg`*r7XlgYYDMkj>^k@(N%H^o^RJMH#wQ`eQQv1g-ndh*2rQnfmSUk zR063+UWomsCB*WrYRJrwr*v`>Dsv{*C^A!5&Xy9XwCFVT&M;+LA%2Lq91vyFLhLs# zAyz3S+*EjIi31q!jHbz13w7j*By@8Xw@S=9)tMF5oQCMMK?V~SVxPH$SS&8aT%_C* z2R(i`YWKBpMTmC>*mS6%seGhij#Ir#O7Av$zzD)Z>^CeSHrby@*(~L<#R6BzxYKfB zhR39)rOb-OjH7eBL+ElcWLEni%J_xYXPp0O=SrHJgjjvbaGi3eU(^y3oJSgDxRqzB zElX-5C6ehR$Em7K0D^G~v6q$*>sXj8$9sjsw5+Cyyn`pZ02E5r~D@?IpyLTr<=_%Um+ridw~7ebOA+=|Lr($qd`#C@KvtxJcTLJ)VuVr|^Z? zttG^Y#R;D&#!^_wB-?{=gRElF6joE<8e(Zje6x)8e_V0vs#zanKn3rB%em3 z)npSfY&Hsyv|*U8OhYLUWv^R^U0Xuz5boC12w9K1^;8zskTy}s6S2}T-ZD#A zH;N^uN<#)O^oJH=!7}IbJ`G<{+o3i&iQ=6OQlQJz_<+X;LP%>MoqD6lxp?oGI`%MNW>R%EtENNyEY?9hN_Lq;egD`{|hEMUUbx!B$RzJypUUZWdg zn`Ao^OrlF_S?op|1TW1{M6&f#Bbo?xA{=jX9l+byF2w%R`8zkR_09c)}8QeyIe)xVZYkXNT_VryJ;pe zEtXSQyj-jDSfd={kqMs#IU&6e`{gCXK8z4(CDzpf$hkKU@ z?SJ^TIt-NFEX7W2soiSYM4x9!Mx0^{rb@$FqCRX*t5jmz5Yw?Mab052NpcqMNv$?s z;(5@cmCdcK`AP&&ivwL$)MALO6csiC=Q3c5Hb0RH zR=VFRW#L2&Omu5>a(sE#x17BhU5j6A3gLL+)q81!HQ9cE^l`%iUqC1{1we zA#ORf*k!ysA)BBcg+>{Se${c&yCQn$zLPo#70D{p-O5NF)3%sM4aT&RNw&uWupB%t z!dTRt3cb3MFzZ(e&0MR6bgHyTPwE)vOlEM{RH?cqFtco!ZidI5ur;F9RI70L+;!2r zpE;=mk!2CxsY`;XjN$ZX&`+`@D?RCq8KXqzN{O)6?-o&bL?Bn1)6uXylJPK%Mn`%= zD6tlkvlONR&L%-y-I?N!5zZcVUu9% zF@&Eo)w(rpFb+b_x~XO&87Xp|R9r|#Dv<(z1<_C!eCd}`20~Z#W(kYOB+&b-cf;Ul ziXPhHst?G~>|T0RQ|DlJDf&{oSOXNaq44oBKU``Q-D zYqsqO(MqnMU)iPfPy=K-%n-Srv(?!n_Hn?%yBZd+y=<2&?tnuBb#qHKc5$HJ)$kLT zwGJm>BDAdgc)EaFF@wi67rNgIiKgU(!u!93WGk-odjwS640ie}f{2J#t9 zOzf1o0O~9AjYMB|=TV!{OL;m1UA`5eYJ|E0}k|XaPZDl`MiuNxU45 z!h$Dfvs;_tRupsWXd&%rGC+C#^$JcWX?^T^#$xHXeH?;>M#^xsR2!pQk$$T=PNg&%s(ES1%{EzU zxe0~C(c~sIzW$D(lh5DB!9%*U5uyPoWIURU!o_yX97a=?kX<1}v7j$4^1*D;7UILn zO+QRmL7M}|3JDg?6=fh72kAs!%Wn@AL??UUA5Wu&orWFjU-hXW}UW%BHBq3bNRv{sl+<-j2*Iw%`y7n$bB3z z57GAP!Dj4OsZBbPt(ZLk1cFKZ5=q{vvAkV3P@ zr)11Y;(nN5%BXEqOz}j>T4iK1E$EwXY8;IvlC%hbyrqB@tTn64)@s@#V4@P-frcws zpwdR9wiqjUPnhiFa){gAVoMA|@%@^~yMFKE^y=@FKumrm}G^K}U&y+uJ3q%?+= zgS{@|^ZVI_7?Vj)zA6?`-V&)Gki88iHKpu~6M|DI z&8)rb^3^h4wBn*^QPxj&Q!Oo031w5IChO8UG^3jA3F5LSNhO@sR}MWoIs58;{UAmP zwVYUHtC*sHs3}kbBYkSCWle_~9!J_*3KSvD9?$sVmd%=7@p==8;^7^&cBakbr5aku zYCb;GqWC(Ka(V@Kfowyy5VoUPs^*w>R3`kz6dO={B-g`X>TCNr2v#p0w#TCOU`sAU zBro6=(~PI+a0O#x_bl$+$AOa($m(uY{2ZXP8d^dE0;OcWh1&G5 z7^^F;pQzLW^;o8+IySXj%j;>hQkb`v1F3A*=SC2Fwq*rm56MI`Ou)uLE;Co=u;vb) z&g#)Ps5GhHhK?nQq^I}$VfKOjIG8N$5l@!(OAkDno(h7WB4}r*sTU^^2 zt=5oDqQcHd9zb2^1`(TT? zT)>|b7?VKHQwBj7uwZaDlg@-#U|sb zH>qCteSBXxguIPLx{-5x@g`7A*vq=ldOl}MrNaWH;GnZY=sq^pRyB~ZZ1P^TTvMfN zQ-~zXl73)SMGU$q*I6ohTKVQV)metM}sod(D<|0*OePT+69I%^xZYB}mci zC9_?>eO7RBelDgcPW|%%c4eb9i28waQ;CJGa4jB5uw;rddy*}|Y8N1Xp0!%ENK4p( zMPvn+l5#d$2(i_tXw;TV@1W&Ct7q6bXRsZ83KmX`~kv*UXC#pXi zoM?iLywUr2J+aU_QKk+WgAncyk#fT31bcr6j}bVjfU$xTjoi5H1TYyLAF$V-x*J_( zzxaUvo!~^)jcr82i&u4$y4lxzqt~Tn3W(ihfRkY$u z>DS)*B2MRnb(j9-%_2tN#OD%)FpmpvwXHTG7Zq+abfdY_CA>^hO>nBz6)@IoY~;<- z@m^!S7gi4CYYZIu8tc3~MuZa^6l3pT#GmVH%xZ-O#@LhOMoY6XaKhp$fy6E)+(tk> z)K4LhtmPrBko7pD#b{B(n}p>#TegsDhnY6+z|vYRAadDOlgaZ;0!-?zTfSn1_q)_m zQp>dTs|pp}1rM<`+liEuO;+^!g%Bp7SjeNb5Pjhn0Kp^}ams8P87u6Vz0>rLWzX%c zb}aj!kDlI~KwT%c?14e7t^X62JwY7LgIo5bQ$vqsA0*JMZV>+iu>uc5q!ULvE9#+V zyIP&vj%wYB&NsqfJ4XLp4G{!EyAD6-_hbS`VYOVbL{bI+t_W5)nT%_u}Qmd-(y zmavPeru3*L3{;Yua5#ftOvhb?~W&RiQ#kv2SlV^W6o16K`Ovd!M zNt*uAbaLtkI(+cYc{fha>WAu^6L_p`PT+<%+D+`d8>hMiCJ?$gfy3Gu1a5f!!~`aA zZ5xNcdBzZo?}oQ|hzXqAB`|?!-R1;-ZKEB;&b#59i3v>LQ#KBPvnM7nfeYR^1lnB! z6L=wRPT;IJ4uRIh1P<{KXcH4S#6!^R5|}`<5A_f0o-i3uFiowoCCP$niYffMdHJy7Zr zn7|Kt^N{ZHpLauXVgiSF2nrJuIK)FBcL_}3vwf(CAU`pILp%hzi3uFyA;@+KOd#}7 z4?$*P0*81A(p>@**!Co6Y~xHsQkTF4-t~tz<=J^Rq$VaXK?al2+g0b?keryn1gX|W z6X?YMJJodL)XdE@(U}F)hfUJPEgNTTtm%0GePK;oJ8AVBt8ZGht^9E19V^K4 zua@7v99a7E(g&8#Tv}edYcacc^uqlM^@UA68o-6~z}#bVm(0Cp_UE%#&5|=un~t2m zce*;gW9q@FO9pQd9PniNO$Yq*AOHCK_W(1yJ2)T`APn@zLel*KM+87Ng^z_$$%2m`&bn0AbeKyUi+FoAtJy~hxE z=TL#Y*-*z2c>7R+2M!A8+%Ym_zUkIOE%2710(-+lj!sHo`p{5;y}_WyX!31`THpsK z6F4#tgXxZ;0(%eiN4s^K{&lFpzWH$EogUK%h6scly`ffjjLd#udjC*?eN7&j55e@l zp#po4LU)YJkYKubsKCD2W@OF;(@nz!_T^aKF*1vS>BgY~`(~Swc@<1I3>A3bY-6$P z7@29obp23)eUrn;+zY1n4i(s^ha=M|n68^l;K=k2rvEooV4q@-%-CRh&rpGViaj!i zgX!9#0{bS1ky#x~?;a|!&r(JvYA{_rnZS{WBTUx}69_wdzimd|Co^3&RA8U!jm!>V zde=~aeWo`uUxew(p#u9%Z)64u(-lJn_Pv)!=9Dm9K2%_zEsxAHVS49KfqkYoGVg>* z4@fg`SpmU)rZ+MZg=z0lfqkYoGB<_kvS9+D0~K5UJ{-MHx%5yAykw}rK1&(R@LxLX z|JO}VO-)bDYxB(7gKIafoj-l@hGUvp&947s{hrmwRzJFW>GZYJ+VrocKX1Bp{^a>7 zJ-6Sja~I9MZY{cISzFvVY~yuvj=95Te=>W|?B%oNS?}!eGrymCaOS3&^YuLi%f_u6 z7cIYT`J&}pSEiO9UUn=Ww)B&YhfQagjxl{yj}!RV%;M@9tH-STYUT4Q*RE(Q%*x5@ zm#>%Cz3a!X{eJ15rOTJfOWvj97k|I_;Nne-=PyPVEsKi_k1c$3;nIcd!Wj$4%>Qcs z^YhnEb#7Da_cPB-G5h^<;GZYW1Wh|4xR;Q^c(7HkXFQS%*CIWW*dlF|o&KIVQjX1O1ZS?&j8mizvgDl&&?ca3r*_1QjVIe5%+(3s_HW0tdySq?PHjhrWc zIcB+Ebjw8p>1MJmF@=_ZM<`UR_l9Pen;)~>+?eHN$1FE9W;v5l&L0TLX*C&)hVmJq zN{3TD?}eG;#w_=eG0VMp%yKUpv)r*`mV2R5j;Ivd1fGz+o*+Y%Tg~1?Y^F2EEEgNI zoG@m&=$PdqW0vE`EXR#mjvcdHc+7I4Zn;Rlz@k2$#(jF+AG9Xpz2O5)UmCOA7so93 zg)z&0cFc13jalygG0S~^%yOSI%GGmXMGth=aOnY0*h;Fd_So;zJH{-xXUuY!j#=)K zG0VMu%yJixS?-_5EcZ5}+{o|Z-D8$Zj9E?`v)t>9a+zqGj#8}th2u%*+<3M(zw-L! zW0rg8nB}&NawFfDzaO*Qe~nr0scyNE`?f2_cirf6#&_N5a>jSv=yJw)-RN@L-*qGP zDsJ@4$Z}^H)A4a*6>$kP;{r{;CPp#}(e8KFc z`XB!%e)Nlht!FO=PT)v5+9RdsVj!qr4msGxK+tlq>q8TG5_K-EOyWz^EkA*?*uk$4 zP2li1{>8vUbc45di?Y+=ZtDmedcXa@aWU{<#GmVZBq!_`c)z`v&NG~yPDc@I zDCCzpyDOa1L|ZJ$WnA$f;X z<`Q}$4I-QOL3j$V1l_E?LD`6m7zIGp1?&RqF{B8{mRjB}*6mTbjgqdUg{~xQdK6pB zE*AqjNFZv1$27!F({)39b8T<4V~B67Paj(U`<_?qKtpu=pESftz5zOMGkOd$ujESc zYP}$-9n0ANX71m~0RKwh1pPv8xvq!(Hln3Gdm*=D6@SAExpvsmx$=9^3%P<@w%S#{ z%^D%3Sipr=1GK}I4g))BxSVCed_~WwATedBUhzNY7a-=Y>cKA~nMfOU6!gep2o!Q+ zgc1ZgL7ssO-rR-TD=|$1fV@fu>luV;ay3HmF*a@|Ea}moUF^GSUTZay4ZM1N{jVEO_$LLP@=b`R5Jt? zU(uNvUQyB$o$3#v&eQGxI-uQMar>Sf^xdjgxUWP~EGea2QY^&9LQd?|wZClR1G@X4 z9gYT{9+GNJtV#R*Ul*ScOV5gub4mHxzxBKRVzSe^ZlPRBKep^Nr`rL~PB)#m!{2ba zft^sNhJ#KwA)8YzI!GU$wTn%Tu~d*s3gC-nIv~c%B~tTnWInu$O;rivIZrnT=Lj`x zyJC88{4?fd`7Oz~42(e<0wXV(&|pISX~rLg>o2$`r|0H&>p}G= zNI12v%|=YK(MhLF)5VLwy;|@7er$lX*?Jv9E_lh-F&k4$+Z>lV95*LOUbRgzS(S8+ zA5F2tZSjWA@1;ShS^F6tzjbs^FD^`y2R zTh&~L>3=bu-@j{(&3AL_$PV9K`}odoPLTU+o9|yg<-^X5M-${a+orfZAB{Bj4!57yy1yS9o8j?WhxPR0{3LnP4(!FWN9M3n zs^Y2Uq(ZHkK(jrEX@^*Zk9}(p$6lW|-PSic91pPd36ijFb3BNxkL266^>0sg z_-m9x!vKU*Kkvuo=QuXK1mJm_s{?c@2ft+fu%18jYQ zkUHBu53u!-OuM%JwXb%Ve*M>D_vrCkt36t3nk1mlgj(9Q^#^b2aQwGp$L9Ewt(9I| zpCIDSHpOjQAK8r^ZeM=1^ZP5}*bI-~TJGt^`AIV49oUN_lkRnG{WDK=INs|Y#Ie`U zTDSG34#xv*eS!ct+Z+#K>m&JgZT*wK=-2D8m~KU*Kk zvuo>jb$Nd5XJhkx+15gb=K;1pLCBPCo(I_aNTyv||L|d*9{tEakInS>t@$1;HBAz* zWkN0O+WPGmcR1cYa6alA^_OhT_1gLb5mmM+Zrl3EZtQTo)!F@h#{|7N+tZ8lljJ@; zuop*eRCaCs`gDim4U2;~_MWQgwm#G0cz~@>a6q)p@gTN7l5f}6uf3zg_qw+X;@i8I zquaWv!}kDNpWu{co9}+MK9XnG*00{%;d#wfM?b@BGa*y8lUO$+q=Z|AR`ch5aB_l`N| z>|?WUnSI5~*JsX|d8z4(ri$s9>AR=XdRoCdhHeu)$A|pZ=B)GrYSSCf;vpGgy^Kz*qfc#>Io6Y=hqYc?F-_rCv(Q=h&q^YKTiy|=8LuQPs0SFmnmDWZ|A zwxBGlXIn#Ewv1R1(`>Op!w3-dmJ&fRm2&YlL^cHbF1h-I-|hU;<>3q8=~}qxg?n%O z+Lgcl*bMi%tM7PW{Xy=IC4TG0UBR|+geo}nB*oc=*-RDUP8spF1AYj=aFqzUYq3l{ zr+Ivh6yrApfB(d{z4J4_e8;iR{Qj=j|MrZfFMrMX5K`DN_4D+~b)QWrwO{jFFX{^R zD_IP3NvJ9yusuO#=tMb_A~K9xtG6H^QvoXqT5MEekkeua1}}2|=;#YSbp9)Ue%oWt zkG-yT(Z+4xzU{}+mp}48)0IzO@#F9ETgP??l(U0?!fG&@A<$F z_^lUq1*@@4q=cyP93?Tyh!76hq8b&`lf9Irtg4j=YdG3U1wsA%!)6Hn(B(&6wQ~AJ zAGxHGKHT%BC(Z9-8_oCJ{({ec_r#~_Yj69_OZcr9bOk3x1@%Tefp}C%Ada#-%??_kbfo;K+_nBTSUi|0AvD3f2!Q&^NfBL&x(scTo3s3q~HuAZj zee`?$)-hedA=z1UScqsMnU~a<>Pq?IHG9h;qO3=zJg&SuKt=ToNE|0`cLh$Tf9*QD z`1$XB=v~vbcmMpCyI!~iy!X`CC0GCbn+o$#$i;6R-4(1=sva>FkF|stj@DdqIjQPN zdU8;z9D<8=7LHr+Y$}JGTJpL_IMUwAXN{9x+(FP(-zd{^VL_LV<+`Y(vs z*kS%&oZmXCD_9g%i`jw)lXkNr=Dg`@1cjmk9V%F{pb`L@u1KX0QxqCYC5=ALz0LMV zgi-IlDZk^r`(=@des9({(&RDfA#}cd}`r= z8&=P!E|2P6ab#C;qlmTjJWVa9A5vcyx^_#EUbo1JyA8&r(SQvlb*5~eh$)9g~@(N}%_Ew=}+6Q@4$%gd(k zeDz_U{@$CP$bI->gyFXi?+Vs~TNeXb+5r?$T8q0|ZM-FATWUr21k_+KZ3EyO(gj z!z;dc!MX1!+_rx68&0`jJoXLz)?r=2;ks9-g_v}lc93Gtm4Y(@8sP20n!VtZ{SsYg zS=>WFIWM>^*!TU1?>yqsKmBq!|KRSQ&6}XJ9{k8%=U)1`x8G&E@0hnIj!W`e8wTS_ zHWDdQl&_73tZA(7%$ZC2i=*Z#sg*QjDS)UY32BNg3~#?dLO1cTcYNt2%bxE(`HJ7} zy7B#o|NMn#zx?%ApZKu(ML&L_gS>&?TJH*;VB6p5U~ElKYJB&_wT>=%`0zx zYcl!an`aI?`=c|_uT&R)eT0MGTI&j)V6%Q(D_*+5*^Z<9pEWfqd6--;ea>>ttD(^MtTdccS2m=UaDOBn)2`k4eY!J`uJso#b z+w<|O{5M~9?{7a8eCCqPd#_~gobsRezQ@lxe5>Z4PHIowz4}RhYo#lAf_-s=apj~x zN1wPla{XIx_@`^{`0)3yfBi>R*DrhHQP=%aPJ8~$*1y1SEq4V^u$gTLzUf2na@~6k z_FlAf(Pz&7L=jZ;=_gMUg&wu3U`4b=b!(aHVrLN!!cAX8up!O-@6X0>5IPd&B zFWmd@U~A*iCmsl-rBgnB|Ixqy(gjyv%5N=p1y8U|YzW4_cg?TKcfRtw^&9^5@voKz z*AZX2`KGfjqJH`|=y(1r0_nH#TMJ#m6YT05f>-|KquSz0uYBaoAA9;#+UtM(U9K-_xIY*+9E+nxqvseBE3 z<#*JR5C0ARt&NXed*j{zw)^Tw)6XpKt{?s~nxSOMiOJ*@>${7l$8OD7SAl zf5Wr#1N?+m$PB^n``G93A6)iW>(ketcXZ~ePtV%FKL6*RJ|8>noYOx0 z$p7<~zs2v>qkHRuC)mj{1Rr_Vr!G5Y<(IAB?f%Al@0TC`?ZfAu{QjF>`R51jI(PT; z-yFU7G=A?nUBMG;iZP&RU*9T9J zo~;X>z}ejp9C-S%cRzU1%}dwq+3;O^#;o#S^KEyX&t2I3#&3S{pjQ3kJ)QOcj;SlA zHq?!c^$XXJS-W-ZjMdv$>6Oo{MD5?VW;^m8$g?kpP^Eb@9=I)#$XMZ@W z&OAONnEuO@oqlF|Yx-rnh(r5x!P1`Wyc9fX_tfSDi-H|ImrGPxLQaMf>8g@R1o@s* zr2{sr2JDj94%~eLoy9#Foy+NKQ=1d4BgW>^zbPf{N$d6P((9XGc`oW*36?y6BQnK3uhY47jA?>Z(%4)MW=wItzK$_Xuy`7~ zzWt5qEWMtNF>&LsrN*wO$C!3+59b7{tFfyZZcM#?RN|i3cDU%xo?t;XHkZE|)0x{1 zo?vk{+L>f5W9oBxTG|uaCNsf;?r%h4rWYIy}phyO|Uc^yT1L6 zDWccYF{TMtieuN)V@&+^a89sx9J{Ka#?u;}{khq8LaM7DR!NPcKE`QbEz;=TtSR{|mP&AhQ_WGa{_xN=#-TAj# z^@i#sXY-a^<)|$hgW4{aJI%Wq)-uju;~KhPa35aM7DR!4iIKE`QZu?{E#gi5;Texo_xd1Lq>-PoTJztuC z_5AAGx98qB*P8Rr9j)IUxO4W6v*FnjW}ck6XXcWbvu4buKbgK{x^m@@D_>l>VkNs` zUzuP2`tp01)n%6{XL6VproS_oj!c(`%|~>+gzBwYv14h`0M{n4_IbRyG^FX zfF-)U_q)Kf+W^P|0C4Y{#}qOE`9Xl*ncEaJ0J%Yc-nqfV7=Y{`K(B3?XakTL1nAwD zFa-=idJy2ib-l%6@*4nY5TJLlW}*y0Y7pSS#g1MBX#kP~0DWOmO+EvV7zF4WX~F;u za!rN$M%rrt&Kguh-$;85!0thSzLCZaKx`18Z=^8;APfQ=IMSfgRKtPYs@L#J1K=9~=vxAsoCd%<2+(H_b^|~R z0`!f8!vJ7|0Da?NGXVG?K%d1y1^^ud=rbVD0JsJL`bHWs0LUOfUkw%maK<3OfvTbF z(k=sV`XE4`#l6A+ym|lt>@%QK4ZvxG0DU99(*QUJ0s6+_0|55EHoVpV)CK|i+Hj@;PzM3}+7L4U)j@#1HV6iwG6>M8d{F~X9t7yqrHBDg z1_AnXi8lbHL4X6N8C_dB15g|Q=!?s5VhuoH5TI|?2pfPw*Dk@nUOLGDSO?Sq_BHxt z1^^rc=xg+e1^^fU0Q%anX#gyP0Da2$QUkDS5TH+&PA~wk7zF53jU5KSJP6R&=;IB* zsRICgsg_K~8GxOG0DZmm5(Dt^L4ZD+ez5^KWe}jRTVG@VP96m4v*}|Ez)6DueaiPj z12E_!v!ze@USI%DJa`Q@XW#67i~-mj1nBFfqYc1I2LbvTeUt$h5J1A_>}&M%4Zw~; zHS}rgk)8Gbt4-R}?83}fXZFs#R`=ikndw6tpWAr*2DkB|^&hUkZ@seaSo_P`{c9Jm zv1>10{qgDtSL>^%t%?#n&v( zEqr<5Ulw8uJLZ2ff6IJB-xHXh`^wy9b7#(-F#FTlTW6cIuQ6#R#I!j5fbK)EdwO%~ ziK*L0uV|<5A6@1jCH{B!fMa&|4%5Na6z=;Z<_R|=2m~GSnBFu52swH!C`wT=pGP3P zAyarLu-7o~7#ZS1(MS|Cg@yuqO)wS|yk5@Z_G6~tP+;$SAQC16u7DmbC}d)W0(&Q} zkbru9VVnuZO!QD-?~4Qt1vp0ceWw{y0G>T_$MnI6qi_8kz=|%aVW5N;tbO$5o0_A>M^}xD6n_HTkrM(nQnJyd(>>YmHDL#Vv-F~0P zbiq(y@9?uB3}tB=rCHPYLxH`+&l6D;i6XuTW_tZlVDIpgLDU-zajt00bly;4@9^_D z9gKPcAqF#@J7oC#GV?G#mX45YK!~0>tUj>!tHMVHB52U#XQQTbh5~zsKkAC`;c%E` zBBry40(-wQJbs*t5}YeSo7zKxy?R4M$v_yR-56nN4F&e<4T7*<&W~~gWzvQMd-aB= zJPbpj9OpJQhXQ-`hCmn^VM&zYOpT$yUcK=LBQziK_}yMpeJHS3Z*={R;6gwT>2InH z)0^H3!%@!Vih9F57aLX|(EFW*#C$%KiTZfnqz(o4Djdm#NS;Ixmtd+61@{U3y!+Ar2ARb~&g`vRS@#nA*fpdPBpEJorfxQYBi$<}KH_U|s zru;C4>&5xPdI}^2r(+&rSbcr#C9l`V#E=MvV5ZzqV6Vc3!U2?{VnT>BWrqTL^(N$E zQ9pymSl*N#3hdPzR6khe@QBatHc3N)y?R6Q5y3@=BQeaB84B#x8Cuvqv-L$s~u+{0pVhj@yP#W*Gy9l{cF_9_kQ2@pP)kD^1S_)uW)EX87sFF;Zl z88)3Y6xge6Q7Xa;o){nYm|i;(FcJuRIo+nb5!3FWz+N@=G7Qf7T>fy-H0+xJ>RZtI zy_74!k+hdJ4SQ)o&R(U#ygtrDvf(Ie8fN_ZjoRMPCwam{_<1H|8aj@S-q%zl?DzWv zUT-L98unQXIeHa9|9WRa9*#jc6F;n19lZ*`dwewQ4SIRZ#0>@ZDuA12U4fX-FQ6uN zdEw7fbjjzs^rk|QWZTY1ucdQ;Wzr6DJMqbYmc*9)X^q&h~ z+L)h1=bpFxvDrswKREr`rHhxIH1X@NSzldyM9($YSaYu(uICv1;OaS3_pd*+anf3Q-?u73E@f*rOAk`rr51qB$Gj!ky@@& zRINY`EqI)*CY8&qUuz&N6;L2@ZHroLO7>P+3AI8n*%nw2Y|rG)$&?;fxdH~GEhe+} z$1Vcb+8hcs#XtmZ<{{8rj4DAD%EGx!m||IhLO>`OFvA6*sI10}I#P{H%`K3iS&-o6n z8D~-}Gro)s&*Cv}JXViER5srx)tL_)2zyP-(&l*8np6XJsE!J4GoWE*Ekb0nNHv?} zdBEXha5aF<++ZMVt*n^IBPFmb3E4UUL|XNx*Ts3jbc_N$0v2vIfx7CF8UNz0F2ZYx z5MexlLtLa-DANT^b;UuS;0o9XOm++YkSe-!b$i{aVbgaQb+o*ZbU}9@if9RxapR$6 ztSs{_(1wSLs2>0jB_AW*G1Sl6ra#|B;Htx&DY#;J!3I$Z;}wKdrB#D+g}SSnu@Q}I zG#BUns?8o1=5q$3B|2S!j8KVFkzlFXCiI*AWq?9lguhvfMy*H#_HkhfkF}HY34;~V zY~bc7>bAv2wwB={PO(i*6~0iO1> zEd;^H(G1TOCA#L$^Qtuigsb+~O&7Q8#VX!YLHPthnZHm_1Ej zJdlr+yy>#a#m#c2V33LdU?|{2<6y#CsW%hNS|pYs<)W(d6$)mW<#&b4vBktQpM+)!Wr-z7xBh^igqlG$F*50%gu%4W_Ep zXeqCdL0Z--Ze_b~gDsqd=%A++$y=x}t>o(sI_Jvx${{E&79lBLs>X6eD%sLk^aiP* zKQw2!5mBMCZ13|IvMBY-yU=LhyN{L3L zPPQFrCah`BP|TZ+M9r}jZ7qlvV)_jR0*5Pd9c7YWgo3~Tn(>jXK&8&q>mHSH2>~88 zD?w`{KsS_W$UwN|gj?qQ(NZ9b1k(x<fx)G=ZGd7n^@dL1<<(P3A2*gUH zG;2z3IJ_2EDtIy$H=1gsY*Dt6gSB?boUmlGd5{S-r>`&&&Jfu|6DdoEj|S_NU^5e^ z;hch{{1rXC0aYxttb(VU&Y*D9bW<0Bw}q;fh*j{k#`7o^X)`qji2F+v-69LFERGaH zHJ^>7TqHO1_U-`Utzd>P(oltIR`tZAekX1&A3FU^JY~r$f4>DO8ipYh~4`W~&~zQ05b1B%C?Wz;mbNVtL#nWX2U9`X1_zTCG!Erk zk>Fz+lgVZR)sSc{%Q zA*O-hw2!qUgRt2_MzSrMn19AVNO@mF)1A2AS8Q*DY# ztnHN}4u2?IvRHtUAIPAkR6ByDNl2n|Xroy!*VN^A7_4eVN0WjhFc&I$tVE#-)Fgmy zJ00e*kcx)kB5h3xo}m71ATJsdyC+RMefqmREXyF6kAtW$n^XLDJ6>sdf;qUwsug#M zE^|IEIRAQs6>hIq>JFF<+bnb>!BtVbK5O5 zs)5MkPR$R7d?_o4R~Xe*ba`R4ooS<$c0>fs6<-BFY;BFAxS70xD5!`(Suu1(SBc?_h*q(+pLG!)0ydXi)dW%rlx(P=)>+2wV0eHCz)`;o zldJVuu;Q+fSsStTuLgoq6iC5{XDl&iP7=uW4@K1Tk;S zi5}8gjfAqcbi|XcrBDU0xSE1BXsMvFiWh1Jn?NlW2{g&5Ex6J)>VN|=x`~?U6zg?c z3rWchSK5wZ)|%HpM>%hUs!DE1B`o=KfXsN3bH^J9X8}N6 zzMMS`0|JlO`FhZThAFeVY!>KPu4GQf2|BBQO}nykUl)PbAv&ibA#1=D0h)lCfe|;S z25dqAq}r}{U6v?~mwXYGY|d?uN8MM1tC|VHxa~{r1X)Ct0z3u{JD;f`^ z!*RS3&E;et-$>IgeFYROgeV8#6}>XrmhAyuI~TXDHV7*sSd;2jFwk(I~^S}29LkVKIru`DQO@~L1P$riz; zRpOSnSDfygTEqQ#v6L#8DwzP`k!sakE$s8gN}}j3Sy9v$C1XU0v@Pel`TwSGn_4?! z>593R>wo;?=YM$*=!u!meKs-EA-x0ClbETKi0+`d4{Zla5HrCop>Beh39i?4%TI7M z=->%uCLuUcVy2NHSv&kysg#ncdUlfmzC4h%ch9tg+CTr{U$16$g#6tTGdXNd8#pLa z3I`N(3}SBx4W}EXn+~|33i%qoE9wC;LX=L6eO`@7bKVl(C}f*EVK*AnL~pF=4a!zF zVv)=BT{bEy=o2(niX}6UErrzG?9LQevbt&j1BLxnOw@OHpmbe@n?l0}R~$hXGEQQq zZqtp#Oy@kCm}vrK8htrzT)VR#%n;l?z|9yOk{AcUX(Abn)Jp9N z-?n4UmZ*BmTp$~2`Z0wVFELZM>7D$GjlE}gU-K<}F9@wM;cP?nh@o25T$M>Em`R&4 zKTc#ExKqpp*)&`c5VBov9=hoU{O?XTowsN=ch_Gtmz{aSB-d)kPeJ^5nmCNB#A-HI z?!1vYFXYZkI+at)1+lfAjO~O@3Nc#bKE9agKwsWUlfN;Gt5%;^+;#ddlpAj)3am#}6D1aVS`pRpFzrpD#ojd4P?u2^Cqa*-%TgVQCOfJ1n{zPOw0_>;AN0^^tt8#4<`Cm8l0Pr(bm8Zn<6u z=vod1#RTk&k`7lpj7D}A6Alba!J3BZiC8-`LA}(O3#3%{8$--U@oqjn0MduZ20FV< z*Ddc&xxMX<<-IjNJ&C#=I?Dt4E$<+<)cLQTm#cBVyrXIRXAb>4Sx2|ZhMw~^(&@=zm{4I36ui8%2 zb!A^(e(O|V>ORxrAC^9?NBaAZ+&$USbJpg zn1#PAJid0*!dIqGF+H*PIn!Nh%{6T8h}G|}-m-e`@`I;@5D?eVjbL9;y!Ik5t z?pyx(!n-!Ux{zD2FU-wy;%F^!Tk1xMf_ZfI-z|y?CQDTqtJi+?PWf%xW#kAyY zpw6JPSat!Sgc=T{xOUj$apWwtIkR$J7XjDcov_kU1z9b65sQ$v?%D;lV$q!y%B$=I zB$}eao?Ik_BaUUvr~?rQeFML(soqSHGrL8L)BiwN~#FL)>wup7hND&DTeWwJ;^vxd&s5l@*;E3 zH%N)?szW~yaq+A(5CMG&PrA-x#VV=C-e!wLRrGl{zt#wYS<80I#k^fWlDNGIms)K$ zg*e)FUp!$6nu}h!T(LGav4Id}(&a>^zhaOQ;~*)y^}Q>bua4&B8k@|xyj8N5FA@4# zcp_0um@AdK7?1m>jKfDej--Gr#P}h<+AcxvbO7*|)w-T9Tc3#hMMpWRS|Ti0v)Pcv zuuY7SvHC-&l{{XEE(k4d_CkT z)U9lzCFUFy94$o^t1Zu)MGxY1VJ%n^AZ*S!Xonjy4$~7YfX=q1Bsc?_Vy-5jS~;U{ z>Dv@NS2%;GON~TQaRld9j5=x=m9S|^jxIr>B%^SfsploTwVCE}QZWQY>v;~SsV$6E z=M@7{Y1lIMT9`FUzEZ`~uEokq9HQIqf|c_ZUA_ceZHB$hvcSkQS8gLbP&lO)6h(2? z0gVu_SlS}{xpoxx%jKNK)ixu+l7o|T^3?kbL@5TPQq2~c&FlVt9-8y>IlObK8%wuY zsn8^_u&3s?ks>b4{K7yKYgH_Y>AjZ}E5(+*$|zJA(GTZgBv8`*7^HTn?uQ&6sE#ad zw5ATc{=CGEFeZZ1SuFvNR+;hk+NUq1}f<~0;lb|wIj`R%-c4D0nF;3 zddoHfx00|vH`p7#KnQkARlbs=WoJRww=nX}I0lMlx#=s|QkJQA8i=SC=7g3#l_YIV zA(E4{s#Z{ZfD1u+z?_0hnv$sNid=$|Qx9(=Y=R|9L;Cb#*4u6I<8>C`07p6i3RyP_ z1|V;w4kz8oj9@YxoZtwZj&pn=Z7os0w7HCeULWMo;

    <>^i!uVp1Ob1%mVPRYAFCTxD;BZ+H9g@vK+z}y%9!%^gae9Tlu<^ zcX<-+nmZcGf&5&K$HPO-!Z1ug^{N4e@M1t_&6`&AK&4s&$5*a=s;E;)J*5y>B_a6; zx8qEBSg|lwL?;b+;LlNULZC2^49?7I3Kz@)nGC0{*8<*tgix|+s?9sjfQPDu3FJI} zV*o_t1sRvCPAaX0OLRx=#X;(~aFgT%(P)wrTz#O_11{(d%N9nDxm~F$W#Z1F;dJSE zqt-75ou!;E$>ne$mx$&u$j^8axXW;y4i6OzqZOfMwq?d42;?#GI=#FY!v~>KC|qd< zf*G}y=o4*TAPI%bI!=R!B@5$CyJAAZM{67@<_WFG3=%mE$HO{S>BJlT1dq9yun!S2 z8ge`q9?BNRgQY48((Xm#iX_MDIH!pL6D(N6O=l~M7@bZ&9_c}%TMoM&kAa7hg;7vS z^LnLPB#or_5Lp~_>M>WQoG-#z11W?1k;N1wmart0F&(GE!=i;@C%;f`VG0%|j^(34 zKBpzLw|aaIxftxK=|7!Cfrqcr-i|EsQ>S9y7+*pCOkjy_%-_{R(L}cr!V6^| z+EzPt%Fmi+sO8uX51(RT%z_@t6D@zL4+Iy|{Zchl(L|I2yFE&-;)1LOnnb8UL}YN+ zXdgVxSr`lnkW37V`O~o3#AGZeh6j)dhtVYGrRkl6s$LX<0(b*#X{w1x3=ELIM3zFHMrl?Mkwn&u?j#Rk(R-F~T5 zZ_>Rwi&f$?%dnr1H^?gRsuZ}Rh zg$aZ@fn*1agq}N+Y6pR?Kp|J{<-I{)E8LYJpHr(3oZxg|5MUik@Q|}GErw3DbR6XS z0oRk=)ncbdxSUu*0%>9jR59+Yfvg!yDI*FE%CQI!Qx?Y6qN9G`RDHij)Z1!+%@wPd z0X5YmI6j0)J`|`YOA1eWDaEk>50e%qmPE6vD<`_SESaMsF%$@9&;twtck3`A=OZ<& zPpcIs+AJH6d3eZlMEfv85}Q58ME(X8YY(3%!)!d71jwd%N6{NIe3_` zFoS$H>&`JcgdjbVaZkP}+}ov6f>79#R&@pU*VCE>Ag_ZiW#Yq>(GKJtn32eL^~t=%!O`(^O3^ zD>O0IF%1vn7AAnP9j#4-oZ*g$HF`#-XXvjIg&mYtHA;qX zOwM6SM0Zj_u>kqf#_av@Flb?bUP4&J%Nn>jN*IZ93Ci@MU8hKeVz_{mg%T`~Cgn!s zNE4d<4LrPJgoz_+i($Jy9b`902u~Ch$s)$a1Klp0U@MdmDCN{dqUTk$*>VCvvRA=HbH2m*eOXOROP{di96wFv!iz_QZzgJD|mRs!q~{FWMQ6cVQl0< zn!OJm9=0$xG8b8x%PfqITtl0~W@{CAHa~!$ZG?v2o#S_P^kv z&%)TqnKSz{c<3ErCeNKS`%`%6u`o6+Yt7yZ58Wfoo`tb-<81cZ zBmMvBlc!d0SUlZvJ^1o}{d3(l!wW2>+84}D+Zh?hzoM~Csdm@w1fHFgYHf@IOCr+F zEx*z4ZH$?nm1k`@#7@ey$6i0R3>t}ps*)w>dHj~O0LRt8e5e{bKpm?hy2S)gZc(h| zts62UrBz9;x68m9F91DM7y2RUq>oS>HsD9RmYVpE;;?B~++6^RO*va_cEfr_j$*ka z*i)36CY!(_MXY~F_ZIM4NHrg^IINLKD}}z9fOBf41Nqu51XTk)j|nCEnPkVHN@^g_N} z(E=afe(XkiJ|3Z(lP7CPq zjpo?hM0>Uiby%}71dXO>3!9hZlGv%X!Jn;kByv}-jb`d=j!NX$MBP%%A7!=L6bsl= z?-3n9?kXZNF7Ux$fF-X1Qay{+ya?jHEaQ*m9_!AJ{%F}awr87>#6V#NA#!f*=Jyo;1B( z$QwDtu-UX*&bBi?IUWa!;Yx&SM94=x-5e?-qL(CG5W!XIAR$;*Kz+zTU(=uQVZE}~ zcL+@+8NS={2s%hm=FXGW>Ku=kFR^GuIBOAW4$DKH+2Dsydi% zto{GRQ}3AC|AzhazF+No)xPlBkJoNk^RIqq^}1E(%9mHJSvha{KbGsuk6n8IlDxFK z`1Zxz;`G8lFEI1Jp1)}xo%_k$OXdPV4{*bA$?R8Vubq9u%qM3WGp9{|V7fHD28jO* zA7Pkq$fw8c9PEG2P>>Nk8ph-}LV{Rc=PF``CG3Fy{U} zCJdPov5nK8G4U^d^^>1GU7W6b`0Lq^-Skm7aKYhw`HdsLdda(1&-;+~)HkETkRB1+ zI3gMo--llEk}s^^fAGdr-dxU~`J{JU{k-?T^=UH~{5l-?(M=04jrD~gH6pfgjx;7d z=l8Yb?T6qmU;jII-hb}Z+?($IeBf8VeYW>Y!8dEZ({mMJ7#|VaIBXgdpOks*zyI_) zxB>m1aNKpn-)+3_S)JKaPw(FOF<8?+}fMh@`+1?Avq$pake!kzW-;dSDgOX_k8eX@rB>|`l%nkFaAN~ zebrBW?Dg#fsnXQ%p)-YHWJGM^0BlVB_Oq3bU1?z7eEO%p*S+_?bN}eJr;^*D+Pm9d=PJi|P3tmI7y#5*QfAbGruUVUZtIsb!;-={A8qIAkNzq$HWVTg{1 zZ5+ytiQzxI`olMu-w=5JckXcG4%c-TJolWF(f$qRoMpUV`t|qS3mhaoBDQgIHzrOS zA1-I4C+FvHzCZtkYsnAX^#aFh_-~$?|4I^aJ@ZrFe4j83jfp2e-WwBh@B7DFesJig z#+knp-*dn_tfMQx{@v%Fy!h$#*S<0Q<%5Pg`{5gJz17FP=l|_1 zUE%&8ocj1Xh2fJT?)$HedZ_Ag`Kd;LfL@RpMnuJRK33!iZ7%g=vT@E?riYVz-1 zcB%LlVR(2%Y~%29OuX^3=Qi5^a>}#1r+@EP7q)v}KRL1@KV^M|xx{hB*{^y3KYc_P zUN$1Oal$z!e#zV3apw!pe%A7p<~c8#|LUh6|Hiw2j(+uLpMJx?KHB*9yUu=IO&DG} zBDQf1Iwr2TKXQ+H=(h5A`|rHruEbZL{pveDocX5zD(Tu91E2J$m!BaF10!M^XQgA} zH`ODbFrWWZ`QSS~_Q`9OUi)r&@J3|i%f4sd)qB-tpZ4F*2}A#wc=7|)P2!)w>*w!$ z{wHsI>y_?h-z$Ioro{tS+&+0~D} z|6k`&?_WIUEiZc2OMk36-XDSY|0$`|kco15OB|KYdZb$aUX?eF=hFg!RSwsB%UCf>O1`HL4s79V)& z%aP7kesrtrG5gN={MCc=pWgr5^MCWsn@@RyFuZ6)Y~xscOnl(6&wKX8*W7dP;2HP* z19iof*h#f7Db2%gGzjd94*r5q0OAWr#5T^}$HbrcwHE&5=Z9y!?vfjR8TjIzH$49Q zd!(;9vB!Prrf(}JUzx(e$lNz_|z{S zD1G`F&%NY%uko!5!}CYPHV!Sv#DQ<7?|9iI2VVIkS^L}U-BYi7Y5F~<{JeJFceAf^ zUUd6C{|`Jto-`u1aU3%yenQ}L-|79++n6t$|H^m%^cR17@W&rN_kCaa%3Xi|^w*xn zJr0|`M;JbFL~P^r@|bwGdpLVzJ@eatYu@pcbid;GOo7l2v01P|M1oJZm+g5`R>dPiBYRlAQV;x-I8&_E7SW{DA zVskd9CL7r+z|?g4xZ1MAKe06P!&iHImI*tt({|#YL6;pl+GXB9m+dz8^OcP!19L`G zlY3%wwoFYnw&GxFIt)r1O--(eOWR>;vaw6I6U<%TxNL0Rlmq!w%(JoAFK=85iX2T% z@We$PGd0=R{Z}>uN4pxDxTq~tlZ`h5V4V9wX``v>(8Q%ZypOuJFCT0u%1awQ%N%QJ zx@2N=Hm4>VuNA=5JLY`B3r zqp9h_iOtzEHQ9KjP~UKY!bTgW3nngXhndO7%ZBm>0*V~D{`nIZd5r7Zc>S;p_UNEwUvqq<;}z<)7>>Xa%w37Mlw1n;2j2rVVbeU~0jdx6*}7p;c$dYb5zQ&l!y z@C{Ofeo8P681Y>Kf&al%_Z_d5x2M-Rp;~#b97IvA=kAc!GbVAXeaU8 zR-18}uA-6h;GLqLsSv455JV2TB`;J8Akjj5!1*bIrM=v-t^AAQw6bgaOQaL1l?Tk8 zwQ|&ub>041vX)ytw%Cu?`BJA_$4xiRP(V1==WTiFQKVr)b>3hI zp(fT5<(RSjU&m|Zz_zF63DwGbrLZ1$Hv}kYaxPDdhCQ(&k^`BCcgz-HI71#G91o^b zIS#~@Ayh<#vMmla3uIjidWnLr`U4HJ5N#hjmVbVnR(5X>Zajfn8QL>ZcC=nf>U2NF z)#E}MPUeJAS<-fDFABFE5ekOoU0}9corp6i6Uo&GHSZVvd4_& zUp`Bmf68;jv!{1?`fXqL{@EINobtv_dYu8I@0QvpR}s^!B1Nj^D18orSF(sOD4JI4 z9TKDmveNr_x&@uCT4{XZ93Cy=n1{wNq@efA6dc85#rkbjFCx`iLB}B}6RnboFqn$7 zeWjSzs|iG@)U<{x-_64%vCoAQTGz?&87JSRdd_rM`Qx+iY8gl1H4esFNmO8qT)hTv4nRd{r)69)-Sc5`HIsK$>#AS$CI(GJ zi@6jZUmN5CXq9o6vtl|H54VuuF*C-GkJrlozdidN!MsfERdR_4-p^I5U{)1kX}q4M zG1!}{?zF%Ncbs^>S;}yJ$On;Zi|ED?nli`;RBb6}1$B1gk-9fta~1GoTlqh&^Z(^3 z|J1(f>epA$rCS!yoF6*wn|rlRToxfuI3YHh3?%j=E?LS}s|7UMjKIBms2VmsorV?{c{D(y z45k7N_I-JtAHl5DZ;*}jY^7W94qvfXJO#`{ofPkQdIu|VU z;Fz~uPNJTI;_k{~*cD0iOd}S;1cAZHFh6mhbnXRxk~?0XoWR@u-rjBdF*uniXGw%@ zXZc<|RiQc}x6@1(boOe^e#Dar=+3;W8)ZV3Xp9y7Y}F+EH6|9)-DaQh)X-L$HLxh3 zic#TghMl-i_}79y$sVsyPT*BPNMO20euA|lajdS(e7!8|xJg$+swk~vArp#D+$V=#2>K*_ygoUBcN9H(ubwC2oRF!OT_h}d3rMm> z19e=cvQr|tU}V6iTh4AOSOW=G;aEST*J!C)_Z4W(jr17`^%EVv(09=w!EctLf@C6r z;mpK+a>-qwPlV(3$qBsw=+S$%{|NV5LWGVNU3ik`%b`>)l%Z8~hg}{zNGIJ1oJdqn zmhKpxQV~nh#X${HqLoU+!~47Cc)aef>S0$9D4YTX@N^`Yo_yW4_8QP9{PFta1m3Om z_HMTlYNWJ~CTXEW(uEB~zUP+Fo!)PRgbwbdLv58BAO%bcsl5o(?!s;vgT!nxPD;s8 zNYR*fg(6uaj~R-V zZ5y!1U7(E&ura^ybr)1@8?eV+pxQQIkGr5eGQbAuUUz|F+kidpg3`zU8><{UTJ4ie z#q!7iI}zhH!itX{wfyw!CEEtrP$8VefTC>!Y+S#d#DK!c02_htcC_6mF+j9!z#fl* zr`k4PkHE#*O>;AITGYb0_AHwMLG zFf%|J-4Ly(a)*jgw48KniUJ2RDAS=ksUp+&)eDBFi)P)mbO@b9!hT$Jpl$p7Q->E> zQra)`P1_kxkH{5w^gQ2R!t-pb(DuetZRkGj?5TP|ZvKTFb$qS3RaWbkyFTcslG>_Q z#r~$k_IZ{rwiAKi0MO?~@~|HUxe}ZRj96BWWwG;}W))D>I@Rj2`qh@Y^g~^GOTL(0 zq9@_)r;}D$k8CbL0s)W9Qqta0=enzO{aDyAD`9s^mV??sfsZH(T5Gtv2WuK^_*|7b znLX66(b^-P3z`%R(=|>>xx5VNhEjuAKqR#wTc*)gwrTWY9aB~N3YW_rBqCj|?ahHZ zUQiHR^r)xAgPHgtEr=Mjm?RGd8wON}7eZn@#7>mXVzcVwuFa301*7H%o%Iy(NbT1x z3H75A=?~tawOV^iH(7r;HeJvke`>*xNtEyi5)pv`^AJnM1M?wP>^4}oADeEscI!B| zY`1dB{Yy;MfAS*9 z#WQtcp)J>q%sV86!pU{*GX0-?rwA?{ETQOUBI7CMQl zfY+LXA|&&X3Q8-7ya>w_drh<~hm4;0pbsia1W4TuH;`Pk;R*NpJwxY=f<6Ig~$JlO8Bh9W{tR*ofynQ6(KrGt8%_PDFx*0@!byUjY z6`u(HYVHY64mt5~JKkdqjKR1hh89M7fRD zi1wEg(Av-ovsCRN3#)$T36VTeo zg28RnMzlYjgm&^A7~F#(Vq!H~8j{mmmfQ{%IZlgG&y>kLu8xbYT)4!Y|ramz>*PaW_oizQS z<$I>DTKb^lJC3(Hp0@VSYh6doar*4tv+taJ_AEPl_RPK0FP^z|=D9QJ)$gvpb?Or{ zPh2c4LJNP`x4QPt{h!_cs{NJy{{8d&zP#@r_tjUUtEa8pwepUYXRRcr4^BO>dhO!N zR?b>}@4}}SZd@oYco!V=U!1>rzBYe&etGWebFW`|(bAR6!=;NB?_d15^?mf;MQiBV zDHFUvfM?~I1DRy3PkORhD%!6k4)!{Xh7JOd=@Ioxwxx7DjVk4C44N9I@Tbg;5yb&P zG)cNr@|tX$@s&*|4upmLX4kAG;;NGt%p}!mic*a~Y1TGkW{ikNkbZ?~*85(UBDGqH zR7h{Q=%k4#bV642h6idn4MV6Y?4$OL5o|CMHe)7JXL8w6AU_D#f=*vgPv^5GOrwCh zU)^b%7^4@8YiExUkxV9B4k$>i3LCj_$fQGYB%ow@xzs5r;eemaka$vzdird^2Q9qKHsY5R;*Mw}Vlu*e) zs$8$gffkdhrhD;puOIN0kibBpwUkzgmoR0%XB%PKMs%TBdHO&mW~NLGjwmSzW2#j- zh;juKH%ONh_8|j>@b}{x94Yd_dJ(n@fb0SejVpnd*d~KlWS74K1x(EJ1jJaBZa8t- zn`>9uzQBnnDiT!`o6FlqTx}b1)fmwzWOWSlxPgcWD80bv+p0Ej7BfUti8tsVID*Xk z7(Ep37>{~j6OkxHsUT0ouv_jj@X9VSY#tc(OXipCmcWBwJk5icJj3=A&ilO0#O zse}eDE+cwee8dHDWMA^qY$MYPy9LCXNJ_?|%eE297*TPi(i+k#CUJK@s3$m@;mUX_ z&SeI!G)F|3hObu6@up;saxZ%6QN|+B0h4z_iFGn{pu2gt2%=ix%iCm_pME z;tA?(t>8cThqe(v7$dk&rJ$DT5?skhVi4+?akX9#Nxhac>MFa5WS1A)O+2CDEyw1> zZsSp~HZH>kVs(rt`PFtr3My1xuOe`VsMXa>%+M6E5^aRKY)wEhs8;5E)z+gI#)y`W zlm}s0^f$4AD)=GMU+=`+tVH^wh^tyE)x$}Qbt=4vTH|daINONS7||%k+5=tl1gcRo zo%DgB9}ZE`AW$9bJ9AONFKP_h)ksTv!EHUm5pV-!!}}e-;pWl&+QartKu$F_SKxlY!@#_FuxN)^WAj28^y?!CnBmY z7ZJn@7|N#k{WsV~Tt7zS(ni%l%1SU@%zBhG(D+P9(Oz5yDh)o2EEQbMa6_bu=?t~M zYa7u4?evJY5;n%wW}n+Y9NpUG0`*G1Q4Xi8sv#HE3~R>xE-snNq~oP_83-CxDxwcX zi~jj!uoIGf9(r-3!MCgxFgC zq;151*hbuD#s52|*QQqPT>SOiwcyMD<hv4(eF)J5{p*tMnBq&dj)m1$R)YnE`xuvB1Rt? zYW0wy$VV1?<$_#(2mvMR7<&bd_e+Cf4>FpSwqEX*>J9(FOuek>2YYm}Ew_2SZR$y) z9O6MhI3Q`MN=i0tssLD0x$r9r60QE~T&qzT7{OP=Opi*lR zIaDE`K8EX6f)dprA3fAHvt%!v%%}+yAw9kl#G&m{vq_^`mQ=`uL^b<5&Nqo}U67h3 zk;65nR>3@)J5cn6f)m7EjjIk~udeG3FR`W@>#B$ipWD0X2L4m0o2!lvkv}>JxVd3x zzOo3m#o#^|`T7@_IR2D4*->!mj&ZU_J<1yI$MGmTGu|WiJQuX<%@DmNw%Cl{@XLpq zez<8CB>zBeGBF%VBbji_IZ!aCKc?d zMK_yqHG0`(E=`ov^-`kzos7eWqBSLGDAjQ65EkKMNFB|&i!q!g2t6JQPUv}{ z>cI2Ulf((hlA+PCWJ(daRBdvI`!Ms$o4<&Kuo18(C!7 zdt5&aIKoksWOu>f6*3a}Op2~;7h z8^JgmfJMqHqu$md9_+zlBOJ%G@WEz8rIV_sAM`g`IhOH;^sXExBauVZep~d>U85?O zT9nTvDi!n~S{krC*n1bf?p(OcHlu;sAmMSv0t`<_sL*8BpscEc!9F~Fc(FCuUv&Aj zodd_?pEGV-)mrO?omREpz(WW7c?bU77ZB}SgWAFM&a-oQbEj4C&X2MGkG(gKcOAd# z1ns*oZ^>4v?5V1}RF+rH)4n9Bj6 zCnqoIGHL(`Q^ypH05YwKAm`e7=jo+nuyyRT>r>UqQfsP4{qdBhaCx(*@&7x~ABtQ! zIr!2p?EK?vKVS7<;Jt4LDByl-)pcjeypcJ9R_6E3FO6Oap;LFy{Lr)Z zQ(yJw7a~0IEb}|VgZ7m%!ZQSMOwn_pwcNeL<0l);5%8RC(rO~P)9nuiL92=pXKsIL z3R|J7t!C9ys^+2^FAz$*Z*zIhT&7xioSJt^9ZVHkK>p4hXolX`?M~(DH8wuNoaau7 z)D)8WnG-ZqaVmJXU`+<{y~MH#4-*N+dCaU>h_~WJ1sU7dl8TLI2o=i}QkaF8=IJKP zz(`GosbD9Y)2^Sf{RyLGD_h<@0o|qaUdIJjDnKTm!I~|5j8bMM-lB0pDb<>}20V9? z(MNPcEL)w#G^5=;TSlIdwj3fR_!(%+Mn`e{Vk`!j0VUfEwFB8U;g{s9-W-_R# zfS&9qWl*|W?y!@lRLNc=I#_YR>57<5Czu-1NT=y9D5 zcXk4-4DtPD^zK&QUUO3TUcJH3NJ9*>>Yw3Fe4>bj@fF(9J|V!$op@6fjmMYJ)o?wH zODKXsE6YA}2`!>SWkT`VwQB-QX37)JFbvsPFl`-P(91?z>BNH z88;<#=-MFJ9JR~v46&ThK_xy`%>+Qho$T^zBWE_Ss$J=$c)mbEl6Eys4`FVaZXspI zMc)I-Lc@7q=GwIyLkVDw3X=a11FNo|p8vPEd9VH-|5bnV{hEMZ`vPChU*N{!3FcL{ zd0X^KB0VEO-E;!8mwCF2q^f+XZ|aa>P-7JY2lP-j`c0kbYKU}48_Jt2+hD5+{4DP~ zex~<5%(~$a{_;C(LS*!vwZrr&|Mz7IKiMo{<9x$2YXaWvxbbnT-|6v?z){~lrM6TG z!+CxvaK$*z#k&Pn$vRKbAn`#dHE$6>r>$SZuL=W!tKIS1rjTl%x19%;C+#MWCl+R* z-RBa$gE$zHp#}_+1cNJ~q%0z_&KpqgJWuQix0 zoath-LuEVJVP9j&M5bzjcuGZ56rjyf5#vRC1e|TEXVw4va#^anVt%Lf|J?iU+eUnx zUDERwXRO~%+4?JfPsLS4fat0c;hFo~Qs$acFlIHgbB)ijc5a%vibA|S>*81OjE&fF zRZaENP@E?2+*Jg1adUwrSlYyzy-rO>q2ki<-)0ncHXEuMQJZn5M)yn2#t7tCp=MgM zzMbc}6i1;0wLc-HTt+s!N%X2(T^PxVbCY*1pwsrW>t99n|2BdbokifO*8kg}>5TT> zx)An{YdXLF--cZ8IIaJOJW>BI#Po4TGxN>6B-D48bY`j6r+cb?%2RKM+rP7?-ZSg} zeYxh|jbg|*?UHUT3V8D->Fk24ce+QyotRL+dC|c0dHEX3%&j7e4X=_pbXhSc%Z08} z_#o%xC=Od7D$cP9dFR1F`ZNV-beWnAnNF^f#nhaEw3lX@YLuX<%7|IAA~wdee$pCM ziiy0{*8BO|Rm*5vTEjJS>Be@n&ex;OB=7>}(bM1a1g*KO}6IV#42KqQVo0WyOXyP3! zIn6OylMzqe;F5hq0J3~%k&D@~tsqWtvH|2ea;IpLs3q3&Go)T>o9zzTO;?4ClgoPY zdYA0$1CT{B`FOQPs7avH=nyEwr4~MoT6seHK8m*G&GoD#RjD^@-&Ia z&c1@0ZPkvHm!H&(RJpDl5823 zg?LRofnBn20J4Y(8#AcCVCy9!!_UW*U9tu_L*w{zk(aRE)QPPh=V@7)JJLcfSttNm zup?UYa=XN!2sBhpxlyf-kT$i972!f$$_X~#=!i5vuPvQIeV|Jg3_#Xe&W0&+S)5K9 zw3UVNX)Ehk)})ujQH;&19X2Z)qkK{6r5$qsT(Up_vY?(xSNjX9)?NGBqNp6Yj3MMboF@)+@Cdslo zs=@<|fY{z5i)8DKe62N6dwhK`ON_(`?vQ<*OZIgE$c|#BykO;7vl44a{8AfFr>rf{ zts%rF7PCTs+)QK=3m#}Lof$0Vl8pr*%W+)3+Z)GP6I+IxO_7&6R7_YVdU>T-uhf@P ztqmueLm6NQ@#G_Y*_E(JqjR8B3|X7L)N0K&2j))$3#*Xgzj;v733P71gnXHw2o zbJZn#H2~SkGTTheW@whIE!l=Pt;a^B#up}&wuKpD9gu7gZ+B-_eSnB3uXV}3HUQaL zy}?!>Tc{7_iHa(NY^-6%1f#LciOG2fbIO_kS!A)u)o@2|U*nQ}&HcrJ-Kbd%BPO%l3aoOe?) zn90dzx=ljarIF8QG_P?rsO-#OuXM@2G5}fGsoUT@O>G(VWDiDC!=!50NX@Dt6Rifz z$Uu@>&4sSy)2w3-S6s4J0+1cgn0be=>Zx3zTU3ln-NsdMG+R)qbloa|J!=ULlLlVo zVQ0u*;gWsD{Us}B(^S4T;-;!(O{o5OT@wmSYg`{DMw2{)$%D=yl_RHot89%^jdX)& z+g5rifVe;n?523~fJ^p)0AzVduE@GX4#ve+C1E>@S_iPu5|Ae4uF~by%v4AwtRl!V z&XC>jlD$6wS(1yBPL?-O801Z;UXg@&IzQ5D3eA<{DTPfAigtC`u8c+nNj$mFC3{~0 zvKlCp?Q|6y`W+_wje@M6U*=#Fj&w)(WsAO z5{wpRLnIrQiH@^6yu>B@lKb0yHa{LVW?GJ$&&$b)SzT8!o+rClm*mqJT^q)+iZ&p7 z{dSL5;CiV!V4-nIOVhnV3r{-}_eUVG{MFGf?GD?h=?e3tgrqq6nt!RlhXLjfLe1=U-8Hvnhi((mG za1&<+d!bAAg#pM8atK`Nvvw-h9L3;4YBF3jsaRVcwA4<5lndE94G=t5NWqSRy}%{= zf&gUoCNQlTb{+uaaR!TFl9Ed;ny&?wR??q9D--@6;tjk8P&hPAP2+5RFMI4qCLcPk63N_PGJb4i=UyVT$qfMtn^5(QeitrRiK_GxLhG<%hEND&hYHkw+qz-*V|k zF5Po+a^aH~UVc0}`oz&ahvS1^Jb2aqyZ1h`_weqw?)=%#8@4~R_2*le=tm-7asWYo z9(&mdcR6yXzDkHh4i64@cON;p5~4ozXFD5_f6Dsa)o=UI$EgkhYuf$T zeJAYYNc2>~=t{z)Ftw&nMfCD*&aX0jw32T4JFDL#!be7sSeNnsll)4?*SKHNmGSUO z8slW=7A?lN#7q+IH=P`{$&wg!;9gx`N*T*h)Ff&!K(9J_LPpowcfOE^EAa+7_c!k+ zB)%@>eJ8oK4|#Bf4qjdR2_aYaG3U#8u#&F#YpdTn7+A*pPqJ$ta{o$zP!6DNx%L* ztKW@R2bTBBNqQykwI}7>xe}uG`Ki25gsd6o%ek|X@U0KKzyCh4oDZI))-+)IN{C|W zr*ht0j^cbtw^uU0<;PdQzb?3>_npuyNxk{qotd_-gs9JcDrwN!b897C?d|UG-wpgV zUv`pQUxPx_dOsEMbPZw(HP)%F+FWqVa?QpJXwy5+CD;C_t}+@xzG6tiLMKI z-wCz$ArG&FsFeSNknS3Eu8i(F$+(jc^O?Xh-hXm^?L!`1akjsCHPJ?6PC?jmu1ShL zJFzH14QES^Luhx(^G&%!!qdh)^oMj8$~3zAkm-j53;E#5n^!{Kv4&^! z4LbL8x~paK11m|Be;!!UhfdzKlJurEXzxmh=LSwCJ!1_zS6+9`r1C3y)#*v zyFPgGhBb}dz7pcag;P0iSu@X-)Lk=Cc_k_SXkbb2J9+&|(p%Qd5RW*VN_x6xo-3of zW006ry`!NndgeQlI_~RTKxvz7FfdjPKdRycpT<&iCe$o zxd9LEn)&KWD;Zy{`N_B*u;r|o_)5lG*31ww!B5EOt(oTv>8_a%N-H5BtoaGK9+0y- z|33ro|J_$Hc3J^kETJ4i?yDF(%>ZOWIfUF-F?JdO$cAzVxvygEC;=Qo1hA|NU6qR! z;}kE0v}VQ9OqtYr159v=k0$XkFmmp=oCofNZEI8yl{(?U4Xv zLp|B3xMaUB0NGGaHh#TJ_ALR(hI+EG?vj-Qy#Ggt0^OlReAu6pd>@B&J(fnA6Mn`Z zM5{fXa`Ul3_tgT=I3XKvb;;HOkPY<`xm#i3sY?zlIt1j8n`M%vN%X+Pb$ov3+pgF_B0iCCIW%RtP{g)Ju?(OSTYzY^awYMVBlefNYqTAWupz zSuOzC5T1VVr09}m&v#UX@y?4Uf=f0ZfNThdx_DA>$>su(4dDV8Pk5JXHUQZWUTg7$ zbICFR$cFGUizlp0HWPqs2&b@kl6T3b1CR~jyA@AzF4y?3)9S4dF=>PZBQKHw7RY!tEuV#9guv2Ot~5J0+e_F4;E*AREGoB%WM%$-W@~ z*$_S-@#M`e+1Cdk8^S>&p1jE=O9mku+^Hg-JnWJs&QF2~;|Td${r{T+8Jzn6uU{pM zuIm4XP^9w*daVe_sxZ`938TcBMMy$;-d5>NS3>Bjn-t{M*SNWWcUS^z1-x%|qOJtG zxlrD@&OkRo>)iVP>u%~5>gIH}^Xva_Q21K?f2Rb?m$Cjo2x0#9|3$jNUS&95(x&qK zD&}Ob^ye@*v<3*O^OD^6Hv zI6EIEGsVuVJ!cJ(rWO`8%F^U`4kKzsZNbG{wzcHBB347ES98It zx4K0;RzCQbY2tV)g_GUn z(%WW(KZW}L8}pU(dn} z+5*;V4{CQ_H@QD=0MIOHs-wKnvdU$>5Wl*dODtn(Bcp;;ari2en2KznT>KSP!7UGQjdud;m4! z#UyXWuTsUya01U-lj72jH;HBeTIBkfP6NG~0~0wbU*$?1!DWdFdKHEj!Ag9&U3Zk- zssE3!lzmnIKj9=9kEW(n?TBh_l8jsPB$cnq6q^KU@zit?8;R6V5qr3*pMTC++tscr zshzp2J>#*92g1CswbJ&`6^O!=K{>%p{qOo!^ZNexsVrmX7c~I` zn5^IZ^I4ML;W`G8An9Axgpi#F6qTGPW-HUq9Ln%ZrXdc~y;fr|Eaz>jH+)7H;MzZk`xhELDWV-LOS@}c_3Bf`B0 zk;s*THDfC6Jck<@JY^0lEe^p+14T2nmM+&tI4*TbM2K~nD&V7MFu4{R7_{B}MSj%n zkDce*v+Z84T`Dl$HA;O|vDC*B@4Dp5d(Xkckt_MOBGk>2NTpekZ&z}1j)i0Od~2?w zvyAhAvuKO(*hWjGYBQ)DLED`#?<)3c^|o9L0zlGvat^A41c(Db41!6g8yBDKhCPuA z&BeT*Y^0`ouL2b31B^~U3k4XLoj2H+>Zs8&My0&L`Q$wYl-B@q;l)6DU-MH{% zH$qe;@Dp!6yCG=1^NSnYW;Af88^=#}BSh}IpLpw0yg}QYpF)3|ZeUI~j-KpBh{SC_ z@z&RjpzY4j|GrH(5T_f5Pj(|j*0G;>>rdYVZFhe1@@={SIo&vTvKt|CTm8fX*XF~Z z?aoh;y-hbjryKiEb|XXvrH^>f`V17b-T8@_{@s99Gu7Uc-3Spx<|kf26NlE@oga&K zn{KR3eD}$2ga}pg>qbBmht}JjAMtdXZmdjv=gDq_2#E3PMnDsX*4v#QS96&*=J4yrdX{9+cIO9$+@>3V(~YesyFsV4s>Nezwo2P|x=EwswA{g=L`z`H>1v}_ zP@GywM#;%D@!JD#BvSfxW24>qx9zkJIyWtf@ zJbQEKbR_XECwJG`le>G|5aA0!1q|Bm{EpmaJe*zCWAA*j8zJHv{KQ+IN`tmL-+OqQ zZeWkmk3IUNT7?LB2r8amyLIQ?`Q5m!S~+XSWAAu!JVM-i1Qjr7yYoA8oAE%MZoK`; zZiMiw`H8o_v;}QH9CuFQqR%|KcBA z{Gp5QzF53?|AoK4@QDi_xbVn@hmOB+{0qnb=D2f=AMYLg#?k+J^uD9&(Zfd<4u9wH z2M!m9nZtVz{`lZy2k$v39XzoAH~ar-N7(_kzqI|!+aKQUZ@*#tXzTm7?5)(+J<v{AwXB>e#EGe#8m00}?9 zdh$;c^8j%_!1_Klih2Ob53v4OqlgFaOFct3MjCdC34vSD=U#_5y0_=idX5isVB?EB zuFDmzXJ3amdcNb)dX^up^+i6q?a{h?9p3mTTOO@TezZ0QHtNy3cpcvOUJ;Mh1wUHr zA0_f7kJj;Zc>PmGzUa|9@}sr>Q6m51(K@^it=m)NpFLU!ezev=mJX%|4(*ntjKK{K&D|#JTAF;^ad9)&a zwAN>b$Y)(zkuP1}II>3m)}X zSno&_^ZC&fcOD6M*3AA0QLiH4BUgB zy+GID4JCM`N6W7?&nDoCM+>+PZw&V8)18j`+3p~ITKfuOF-|GQh;Ro0l>F0ZZ2mAmVBYlqtxZe-3G1AZT z0QdO;Hb(ln9^mCZ0B~c@e~t%unIB+dCU~|7c&Q&?W2B$u0bb$<*cj={9^l1(fQ^y9 z0=LYuODDzq>ntn^L+ro#=sqVfO~uZ;D%}( zc!1~m0Dz5=-uKS`ACFxA%;o=h`5P`*F2Cl|7cTwMrSG~lxs-6~0shIwAHDegi;rBq zb}@S4*DrkZf_;H;A_V^8_!Gw;I&L1L$NNXWb@T&AZ#&8#z4Y)e4?l7E!9(Q`I@~_^ z)WJs&tb_EywS&n1ukL@(zOkR&zi01{_kLvWJNMqRC+=O@{p{|~?0)NRV;9_g{>~rm z{KC$^*?Dw_-FeCOpKbrx_WQQ0+YfJF*!r8TPj7vE>l?RfTd&@_82#Pozl|=VO!Nhj zKk+-|?!V3NFOle{eE;;dKRhG9fVV6D{>{T(el`b?T%V&O|Jnn5s~=!} zj*fij9Kc2&zQqH4;A{>Mus&TyzS;YL-*_F|koJ8Zts7_40?Cc<@m`PCd#{5V9r*^2 z);IXkT3-qx@9}88=Q_C2$9H?Q-t9+geU*s3%LDjSgL5Ks)~?edk2wzxJQ9sO=Eq_E zD=u%}o>*U17M?(U^ojK~BJw|+!(e^v-g!>@_3?STQ|B36br@V9)JI>i69uEjb#Q%z zrmx$HJ`x@Jd?I3f5C(2@-LLtrUlbyHZu6c`bA0`Sc5dH%{d;!Z<{cmE#JWDT+~#lf zX^!3cF(L91xA`M}&DZt$>)hsV@oT;=jC}7-^jfrfeSJJCE}4oCnGGL7*==6-Ykun& zgGkA3F8MXTRsSHeblc`@2W5yXy3Gqd)HjYn{Oz0HDhvQ{Zu6WU^;?Gz0J3iLtY7o> z@6Nc*ecb5a#_W}GoBKG?!430EyUl$(;!aro+RT-5oBQ~~!Hp@NcANY7s-4*NTc|^1 z+->gTsdmB|Z)pwyRBR{umZ*>0nOy(uZ+2VzIK{|y+7G*}ecVs@x`~l)YaibezCPs< zZfhUM6MpN&29dbi+Q;jJ-}(sw0KIMN^+Q*l) zF#`f_YaeeBwmv$qb6fj(ez5h?dB|<;<8IivgnEVB+Q-*`uG{kcw{3mv$OFIww`+~u zI)ehheQs-?(wag3L);?#{wb>Q`p69mqIiPNY4?ov!{p_=+!wo0m zv)tA`S0@`+K*w(DV;|b<-}}gI?Q@l~vE@8;Tl-X@-?;QSa9jIat!zx<`)+HWi_NuZ z699H!wiA6gx_jLiFSNd57;Dqy&g%T1-g;+br?m6H&i?kNw?Dc)+fHmhXX`Ut|Nf3| z`HiFa(X$W#%i#|mK6=O=zU1J~4u0(5eFs(NHGzNF|JnU-+i&he`_aAsXYafAhI}GcF-TC94kL|o?>z!NN)=Q&*9{qUq8>6-8t0Vsy`M;k$p>CgfkpEg7 zz>{C-8T2prEE4tEV!#`FhG@z=y!^u1Rts&Yf8?ib57_XzMSk7`e8PvrM(AebCq2MV z`T#a|Rgs_Y06*aeSf6?#KkflO?gQ9()FSd@9^glO02_Y8$Par5-v6+9=^pt%JX-(G zht`HmG4j8i16bcVMEUXKNu}OXW((|=G14pdyfgitN^aZSM%c7?j zhJJvJgYx57l71IL#QJtCdU}oMcOgWq@1desywB=)U4ySHU-b0q&+obhTR-GPPjB*k z0UN4udRORoU4w6o!|B!E%X|iIZNm!y(bKCxzq=7^eL{(z-n04LTx@Laqo=oEem57J z@mi<1U_SdiXk!~5J-yU=u1}{o!>>*+wfusCunpBXz0~rH_`%lCDACh9s>^;NZE!ff zqq^t^*wCfZD<_{FD}-(g-0A&}FJQw&Pj8U+{Y2WZhtn7{D^le1ej=@R`ZV@0@;N`i`k^uMS&xG+V0|JxjUV#o zu&zs|!At&tb=5eHobm<08=d|~uMdCa*Xi|;u>g{QCtD;Cp!47`UH42e2`4pF9V!F>pV( z%Kv|PM2TE}=cO-RddJ0ozWBBapTDp;{-kux~*;f@s{euVRp3nl^UD?kXM zcY6q)8(M(7JOs~i1qhMIc;zk&@N8Fr5GjaPLR85;Kb`H-4?Qci0C%-I+M^%399n?8 zT8H(~4_$Hv2$4Q{cPleK`k{-V1-Q#Pa3Qn+cUcFHLkn=1b>PSqAVd}JyR8F<;ROg@ zQ!~gsa1dI6yF3K@t^gq_mf!6m*b6Vf-5rA6&;s1$A=q&R2vObrZV$nBcmeM25Nw4O z;4TkA)DBGAQE1HyE+6Pd}n9@?(PtL@KINQ5Jd0t5Pa|*;RU#xL*Ui_|5)V0 z$-$R)k?6<#5At98%U8`8c<FwgRyzcW~?=<54U0sfA+J)oGgPW|R* z140;)ThTSdw$f`qj5Bjs&Eo@UI%BIj)gJWBbQzn7Odd?CNp#q4@`R1RDdFsQ4MJ=; z&Z_8o^K-^4fxZIo8r&kn>zy|blwo~U-NUU;{CGiS+>y;K%5Kb*nd(&af6J=At@|H! zU0pc;8&RxZ-I$FkadT!7zp>!!>a75;w=Se!G+5vev?=R+3}0(d8$# zc~&Bl+5UJk&e~0;&*qjyip8Aw2}?*i9~YcjsLnSrhpU$a)Ryy&90k|Z`pj_t38GH9 zOaycy5uC0&HB)b-Paa;G-#5QF8s@F(yJvpbGt&G*Y&Fj^zs)zgzd}ZMh9Hhft<2C- zoSQ3~ucptZ(GXD#0~`(=#ptLjHGSIyMn{2AY-Qk7MrVdNS98kf&cS87desAoW-*?Lw}}=@U1MNnG}3T-d97Pc z@uo%EX(quJaV9BGiGCOwU#tHEubnV;4+6K zvT-g>C9hefzCp7>VU}(#nOTygRi=wHm|;AFcbf@wY7*9TT9oPKpjvJ>(W@;c;ENoq zt~-YK-r30;R)+X~GkSNO-a)st$TQLq!|b_NyTG;AL{^JdeQNfd^~3q&S71Z?gaE5W z%h{>*yUkmc(A7@!X|y{S1AUiJqr_NBMv%HyMY}3zS$;9Q+MnA-4rWPWE;hl$1i;L6 zYEruMCA8flm}EZPuqc$xmZ4gPtd@y+y4vQ3SMxfFB+aWVi&HwipeIxp*1;s!CJ>Az zGGI}ksazHoD^)=+C(=cvorkh>jGj|kpe1zGb;s_Gj{h{0jr?5X_(zVv;ka~s|D{hI z{ngP=9KHXjw)KriuR8qu!=E|)&|&>BcJPIRpFj8?57YzrAR77n{;%wRcwgTq_xJaH zWA7td4@UoPZ?gC1t?J%ICzk*FckNv|`qs;PyU*SE%+3$)yd&~+JGq?~UM9BxWcy>= z@7XR!AKLoM=m)oceEYuWPha|;OXEv#x^&^#3C+4J%m9hT@|Y8X?BL`Vyxd=a7i|f zl+|u0pPix^TerxZef)bKqFxj_P@G2+vjLa02+jtQf|05~+MH)9g>1UL9FzG9P-`K_ zzv&^&^b{`BBQ4fT_o@|h*pL(f$aUvM0U&C05y1#WriL2IYsa5<5p6Io4O-6YWNjrW zHWCR{ZTAt(dC7^CmRz;kRN4?RGh37x8}0w9$D>eZ7I{l*%M~$QUX}|&Iu3Owy-CLu z=%Q4TGfcNm!5FR@$Di^Le1BOOV{N|BswU{zpk5eqISR;Rs?Da1YwThKP(7tMMiavE z&w7YbC!a_Q0#nRcYy!=78`H_^bl9kkY@XK&AUI;uX5eZc5 zb=gE;Ls)9g*7c+R&qEkBywuO+)7BCJXt*rF&|nIYSMLX_*BgRz`sRD;|#to@j{)C{ah*OfgfC zko*i^09e-2OHu}a24)6Bk`h;{jdmNrwi>;n4q-7t=0-_$1ZH@pQl$Hg)M~>5#c93F z7~y6aTh5tsR%pgO7JUs~X5s9pHD9!oC7u}qc17*z0?@2y%``O|*Nek`e-LXKhtKj5 zJzZ_e)%3WP@8LZOpqm(I;^my#?N94fGZWTWkUBBrV0Jj4sfo)UXHb zRCZA>*R5vc4?ILGOPQiEAEE-wLp=gW(u+o%OSYN=XFi)?Q5J{T>hYi_*a*4^dmD5-AK4 z3RS{r(^#X14Rjn(CPPiimu4w)u&5^4B)_QC_9YKdm3oYTCguj5>K1a4R9C9i(z2k5 zbeCUPWU6U48X2TzPmJB-DS|*jxHIWaC`r;V*4BEegeNPIXo>?qYak?==G0tPwDiMQ zcnC?>n$u~sS8L=4ge_ZKtqfD?x;mH+n@!OuB-&OERf~0j-uW*cqC~OXd7gx(V80~I zOLg9ypU-l3JjwMh@P;<%V!i*;ubS}=)0X7gti$ZRz~hmHa??h?4WrX^GX?17 zcumhND~OGz()lDJ!TJpDHDn_;qqpAbAsE>fNQM$?utL|lI@hZAJ1jde>VFH8kIctx>^>*C#NgqRUFFQD5L-rDmciaBw>2G+S*?8<~UwlwqI}8)TfP zbQnpUc1jDFLzd~H)q=_$TP3yVUwJ&@3>u^3b#j85{E*C#QbrTXFWOwe(32=OYK*vL zvN}QanY8~yr-*p1pJQpPR2y0aCr5wL81QrsjXR4O-<;XN1e~Oo3s{>_k-t4f#D;LH zT7X5&tk-qt1;5N#NS4RqypXR-@ibfF=4~`P9AVP-3q1sx%`vH7B~DG+ati{h+5|MK z0y$4(FamJc)X2`LPP~s{%+3QI0-sA7tTQB;QHH59l;~H`{zT|-)1^3}C(}w?O#%|F zjT^#I%tK%kX*!IJK(v*hro{oJPtuJ5Uw`b9-bnI?#$VMWJ$RdL&Q=uQ-^1x-n1|v z2pyGxk;LY!nGso7NRj{GArM4M3BV{L^HZt6q)a{sk29Hej)3tbty^*e?Bvp>L{^yn zH+u-!V5-=NDf6-_<>ImNa9+!xjHqx;YM>R^F6hkU1-@>Ls6*@&AzM>Q3aTVvcrHawP7zKHd54+UZJ^%HV~uIePHGjcjb#9Rs=y_E zpywL^*C8DH_-`Hp;KVFi6I!alHDMi5kRsJbRJpzAUM-gzBgD|yS~Z&li^9>XU4-+P zYBFc5g-TJPXPLB?E<){Otx+XJ!yl7Rpa2-Jj6VnrK?649%owyhqo$K zi>Y*T-7Gvcp^*|dxcr|u$@i<^i;cS&Uc-<)iRDd>% zTI4{fsu*YojoRd|g&5wIfCzS^a zc&|ruW2>Z>ij!r&H)zzNzwT}sT2r1POQfn0%ZzLSG~Y{SoB4ij*jh{pqpQ}2XgZf^ za&ud|cDWY;!|bOcu1xK?g~uu|Y!)ecZVJY7Atf+U>@)#I9Vl?IC4)x3r_|^^9rZTU z?YL>x>x2ZQ7-x@`w!pci z33Y8Q_G2V{!Q1S%$;7ZJPn&`Qs(E{%WQU4MQkenV)E8tNFOxO4F)0jSE?e9EgvWzm zV}nG>%q!Cj%u*nrK+8leAG4-#QyOQH^q`LMFa{A*`|x=l0v{?hyjmVja8rP6a|$LQ zh^qqZm@X?_Is;T&^<0@tYrJ%bxZ~Hxi*QCE`x$j9>+mqq>Sbsq)gefxlw~wcZ4$z) z*H2I9dExmU3$#^Gh`gS_8{>(xSf*$t)-{A#GuDN=W3B^?v~s#I;%r@t{NX7ADUlFl z!eb%MU{y^_V0j{+k}T7NV)?3&;im>QUiM5_I{JDK0SltinI#s(VF6ntlih4?is-Sn z!NpVK5(jq_0ju?_tic<*|LGKgCKnB34jIDO+3M8=uF`c5Pc$&hdr*?QdeN%43}9J>v`iOK>Z44uw*PY;0&I2Wi|1Zr-#q*r*OREEp|fT2ll}sYNdecFgf_&GHIh8m*kw zwlAHYZ+fhO)){or?-KPap;eToa|<=WEAmv4is-0h$Z%bOxSow%>N(c;?V&92%hGst z|NlVbk;vr_Tz<)=e}C!zi{Enbp$i|r03LtxSUUP|NAlr+K2#4reb962_}&O znR)MI`Xz9z=k{C)@m^E&=k z(#yfbo+4nk#SaEOoEe=w*WvPJhs(3he+lXI18-hmU0DhgFK}(H0b2P|QrY<#_q2a4b$W-7LjrOCpzGnl4y5!WzW01*;cNH9B~5 z)-}g0A=q}@&`zH1aJ=U9@ls%pPq-D0D|Ta$sizI1w6F}KvS|0?JzYt%%CN+${gmUe zA*bp1B%M)3rhD=%hto=3F9zmxi@MSf$?(q2G*JQVDXr3!5^vC&#?Dopj?a+6D7&mK zYD1N)=#IKxc9^Zy^+I4~w^!Hoq{AyA-sNhZTv|!vbp1H6G^gqcR+vnmP-fCRsoNzr z)*G~vRLyV!$eA4=}S3ZYXFFt zqw5t(mQdOpu?vjlWO{**nnYI^om@EOc=%K}l5vwPnvPMiA#yo~A6Y)J~*$+5$6rLG5oIo_nM2G5m@LCQIww!0G2Z<_No9HR@ZslhbZW*!?d z2}o;KmGXq`o*X%xR_eMRnA0ul3LDOg(n_0N5PEt%>ym=p?9(G)+ARvE2r^Qf92eV~ z#9^%FsOzD_Y^APyftlT2UDq>MuY`Dit$A{=lE&%!ZeVFn)ioCzmPL8OXZ6--01fz5 zs?)04ix@w#>56Xmv~)}pV(4(vAVw$q4wsd>?wtR!nZLTq;JC3w_(Gz=O{wI3oNU!n zJsft&5~$sz)Mndg5V?vuXN@=oH8PIJ zi;&u6w;eC04UML>lU;}7N?o@CbG%7iTUOv>PWNQT;j~iMXkbpas4G#gPB9qmP?c!{ zN)1!PrZDGA8iAK9nSxa*&1}1Ci?#BU794fmc9^ZyH4>QF?bUTXGx$n~clMemTPtZU zs@q=*EX}F9GKIM*&1{Qpm4w)`*ya-{we0N3f#eX3jaa9sYT8hiO}R1{Ih~Gro!c@0B6VE>?26ShJo67|}M(DWu3G z(l@6R*p?DV_r^ONX&m+2{^!8b+#GHfCV_Qy<57o~GjVKxAuz8qwq+qQR<#@Na5y?< zy#4vW9B(q?5Q&-H8*e}Tj-Lz6=@v5%k%9Qw@r}1R%p5b`{-?mqZm-enIo4s49Gf?m zD`}jM{Eva9IW^-DiG)t47pI;6hx6l%0-5o~K_)~Bq9e}yRGhzmy2J^Qj@P|mJJPJo z`0oNsbBh^=Nc$SyupC}1GyZH~UT2tbh_K_)4fB-a-#!(NA%ft%^Z&;pm%sh;;ib1< zdVy2z|NaXcEY0zw*$!je&KTe6dLfQ>oPdL|l zHE3j-^?YUyD2SRs&VRW1)RsJx9o*|I9CGmxp}64?C^%V8m31gVH_ucS>1M?-ze{)+-R+2 zak?GCy%JcK$5p23@K~u#2!~4G?NpOuP+aH0g}v!Djtlm^n^+ws@w^Er9&WynPZdMt| z;j>bi5RQz%eAZM(Tn&?>G9jE4fmz*NW!C*9%8k-W7N^@GTp597@l?jyX%tU89Kt~n z_zZX34sZBK9ASjVg$eGSc$$RKZWJ6@R!V2g$>F>_w~DQCY!ORAcw6XTY5^sg1ZLw+ z)wVi#y<1STSTnFJHy^A9H+YBB%4|cpEdp~o%WOG^-^y%5I4=V8yV-2n(=QmpaS@o$ zn%U+ZRx7g&QKKj@tJ`bwx?e=Okz2{)eBKbQi@>rxVYb=R4u^0^1U?wwzQgN25yxzq zQ(;25Bc3KO>mmX+Cta90GDDuZkxxbrT237Pr!Ku|`%jMl^wNcGVHJ-b z`Q+ujy^n0aeD60dTKoGKlb63^=Lau7XXhO|xmAq*!M9&{bpKbPpWP=9KEF4KUOm3= zGO@2mK6fF1;YCN^cvL-l(5W8y(}y3t{L6=LJuV*n+@(KxQ~xQ4(Bw zsBwkD+-^@xnPDn#$ZV3Tq|1q%ULI32DbW{j4^d63AcZcuLAPAWalDj+jQ*0Nl{uA2 zG%%r%q3eBk;vBn27p{8l7`mP;2mIt_Z8LoT=h%sw3?8 zJj4WA@{JczETY9oPYmq%DPnu0)@7p{c!%Q3<8TM!bkicYFO&}djPfTodDKi9> zH4%FOrsgHI!_xcz-aAxtopiUVe*OPBhKEX{*co*uzdC*;A9oNLb< zQ7}5$dL>U#Q7@qH*JZ+}aB@8i%W<8m0ij};GEG9M9DDk{LL7Rm^ z9WE(3l`aCigo~)g=Ys% zKuzFDv|28FBeibz<}y%aYc!M@5s~`8!W4zzMXc7I84qi-D6#w>7kCnZDkumuN#2tJQuB zpP*<(Xv{}jAMm6~#w)2xm$y>PvIW;C!<0hSg?JUs8PjEF9INJ(Mu$b313D~RH`u^GCRNMA>!a@#=$v?LPjJLPqQeXh zBa_%NPE+M>VV0X_^}3LdoNMORSSNe*RG4*mk=p;}Q$z_(QjF*X@3oa;pT*nhSSmM8 zg2TLJ!g|J0t_)nq8$H2}e5;3mJ8E~Dgkmf)T$JV|&`M^iDpO{g@|=aGNpB?EYHg}y zC&IWOj?*%n zoGcf)M5DuvrF1G)=90p!CiklFsLk0rlMIfJ1(UKdpXzkKj4nuK>}FtP87|U8SQ<+LyW|VVbhbvvcp+@ zpwmp$*xDTGWt(*>)9Tv^ril%l82G9_y7(~ z4EwEVOopKvJ4|HEvMgLOy-&qcB?|#YGieC-383JFw6~fqtxXa!u$-9UfRlh$mW*D9 z5-z>cLpU>HCO^%^+kK#JC(FsgtUY7Pb*dbrYV}Snwd{zbIvNy~)TL*8h+Ye13Yg62 zmb{2&)!3v6_JMf~vSI^jAhnnzTbLyCrR)ef_Ja8<9VL%+@tHJ+<04+F=tdy(U@dQ?F4Xis#Z-VmP_>)!-TZ3r=~z2@yT?NwNfZZ2jd&YNCqTw%mW&26T;iF8 z-l#O0k~uCnH4B%>(au{ugcIrLq>b3lCH|;wRK-z$4DjWYrq2ciXQRc$(lu*>! z!JXLkj*}8-(u*<77ShF~T_zeeT+bn0wiRpJ^BGesa`{7b>N>Fefv#lwLZSk8~HRmt#|OM)CkN7i$R#)ZgK; zNYPM}kWh{(IxA{zUYo+S1jkbOxd>)(wKrt@LelKklv!;n=CPo8iz8dh)?6}T1D#;{nLe-Ujs$!=t~D@rm5>#2|6_$xguu@mxIC&3 zhoczs*0P=WWX{fYRfy76V_mYNdf$+jt8D*+1)|K?=5D7B)~6w@&zmT?)5S~Dbe+I? zPwNxYWJQ8vz`+Y24pa+FCs?zSy1?E1 z&NCT$R*In>*q~xo%??M+aw0Uy$l?QO$_y37F^3^JMLUF8?N}yG8@DCAQ-n8dP>N( zyyjq__|+Traag?hT?Jy4H4=QJ)mkXorK}dXWx0@(Y=TZ8r7Y#tyy_SXZI(u&y|?`& znyQA&Nb_bqnGW3YG*8_Aa%$rXen<>>L*IJ(_bCGkvcS%Hme%^dASY9^0K7b49G-RdLYW|>mgh1uFT`o=RvkF&XU zK-L#J=Fiss1vg;YT(|C|@dRZF2J-Nftk_M>M~&m{$SZi?t$@9z7C}<2k4%HfqA!b% zWi~Yirq-&|ug=waDv!9MUZIuG>#KayLYGqx1haaDe?xBtEU2Ht)H&;JG<-+HZb;wZ1|f8@h^FOAi8$(U98+&ng% zBvoQPuUiXuXh9RLDUF2^+=v&`<#^aIo_%)r6ds#&HS7BuuDTath};mkBx&qe(|#Ba zo!Fmw;5RTh5Bgn6Y_r1|?9&*grcROo{=T<})n=M_uM_R0&MMvLF$jle+=~@Ab>bKN z+0qB(jBliiJENj`Guh4a@-&htxnY8QDm`2P3+N0<@eCvq8u3#E6|WNGI=Yx?80Y!=4odZ`bOSYP@*O{jEk4r9b9kPlse9syn_r>Msp^ zbvkh3x5`tUjqqgT&o?H!bxQD7hJG@iSozGFZU|Igv}WO~gg`I_8qFhpn1lf#XbFMK z8?CLSm0EFs5l_Hhf?e?BjXq6*KX|iCe-0Px1-B31)Ng7d>v>}VL6k4#Y~=As>03%uZF0#6`M@*-h=kTQN~QM`g8Nk)s-Cbf*Sa+$<{WwxVTOR`9pzn zGD~3yl?;tEj&m~xSL6w4=&r~mV$idy$@AG_(yB4K4$gpD#NF3=4!EE zJM>~|`)3qqpXJ2`Yid3}o8WEf=j&AxKj5=*&A8%kT zOPm>4eK$Y6XkyQI?b^K6H)&ENFW;oam7jS|+v0Ig?0hrK&7-eo<@n}LpK9gLGu97n zST~Pf``TicldR+H}8^2@~sHf(QlT~dWY=r(?jp?*j*CsxMA$P zaAv7*45%cq#Wx4k=Z=dofr1AUDl%(p;~#>f}T-%Ccbt z651rDkk1_#p>#-e0y0RYirE^uIysXbEXu;jh}8PX6PBY+ZGk|&hpN|GxKE z?nU>$7Uc6m9)IuuE13H>ng4>l@ATiE{@v3*eyW_laQi>s{@-u?(#e0j^+R|6-@PBa zo9%te$&a1<*}Vq`p8~7`|K#}FkH?4XF?oD+^pi*5bu>Tvnxi|1KYjS!hs)hQ!=C3T zSm2@+;n>Tu$6131fhIAHxxS1d*wDe@&$QYZ>$!zzB1w|BZ=D^kh-EnjJ!-4mWS}zs zG=Tj+A`iwZU!(i$Rkf^h{j9lKH>$kmc7I z-{_TLKPP#43W=^du+|QgND~H!ZWp;O!7_s;KaQ;_qeI=Of)i`RUWIe2UpFYDRU6=> z-05iDJm(IIyt!a?C*VwcuEt#0Xmmn+$u%2Eo}+Gi$&)TRlcxBJ=zEPmbmJF`chxA@ zO3-{z={l<*^%FQ1+jBr;6RwC^iD(R_6Iv5Ba8H)y|Y^A_){Z zOtH4!u}_LBksU5%95{tm6^D<8hAwJ478&Jwmz`;)QrQ-wQB8tc2NqHKJfIIu9f6q%X=h{ic8ffEbzx85lshxfo+hLD2AMKoh2Rj=5=P8 zEu|%_R`v=uCV`ZpO2@)9b&RzKLw*eR!dA+VA`>?=8?cYDtRpAb!iwzU;`mr7(<^=m z=?v15vYyzV0$!aV*HB}W9ja?O@TMb9s2hmT(hk1=F84gSM?vhAOQbRv-HA5rMGY>< zGo;Ea=&4~70?t^=twGg_k6b zDjZQ;R{j3K)nyy9y&9)~E zRGbC0GaCVZy;_7*wqDD~qaQ91szpyq-~(?tZ$eR`V$;Pqvh8k&w>3FlbaTv_dzLE0 zQLz671){CTlWv1`@+SC#PeyuKU*aVOms51c3nH@)C@73lxdOsfkr5RYLKQe^MS&s| zO_;aB6p*HN@x?3_g-O?kWSAIatZ3?_cKDe>Mynn&vf2`w!D6hpmjKjMw;jS$3ECx-$3C4l5#Uno`k8yK6M~7@^>95FGzXAwx_SE!fB%B41*FG*c() zcq5OO{U+3^^yHjV0qvNFm>>ml@OQR|xly+vB|`ODnwv|!o#225uru6VycgkzERm9F5Xc>}0viuhfJp&wFur zTx5wvie?awQQDq7Zt3^TTQ`IuhcS!$OC|^68Y1 z!5<9NIjzAHyJdz88S==LsE4$1{5{1uXaO?qhDnzjKC)n!-Wqc|jk4N-1~Djg3fs6X z$rWUbIF-}CQ6Os6Y7i~Mb_k7RkbjIjN`2xct460+pLQysaY9R4H4%(~!qY!jAaH-Z zM3qQrBOxF9%h*_=q%UUR%AYL2HsBtk<$5WLC?PrhRDoFa5<2c;Y`>q3nNmw4ZO<8X z&}C}R^T`Zw^g15WGGi{Q-=Pb{0-l0+7flBoj}gXlXwOH0lrgKe>_%XKZ}Vy!a)zKF zW|rLhi2^~4XFMP{almK%9Hf`@1t5tR!RhSz^|yYF$iG``%wH5Y7VbFQ!}r zFl~u>-4A;Cq;6KK>xi6|31leDNn9e35n_v{54H%iR6~4fnZzrpUBR0~U8xv3Q>jd{ zR38Q$)dXEz>a970-7aFHkl?=OdL%e|8uK-;l;a^Aft)_a(6%x|8X#e7#LCR;PPoHA zS;&avnP*#mO3&+nUvjyg0H*t~6>%9I<)bb$ti4R(x?BM&h2&9&3Z5R!B#sdD>tkn^B+=D}J7DCfmAJ7=D7`obN zY06$wAUr~yEK#=BR3a^`7!+--s_v-TPRUYOUZR>eZsov))|`FPC=jlt^cNkq!i%cx zcAI8XL`!{PxC-#JCgZG{spvezJDr|=P!v}P!`VQbjMj{hnW5^|^+_S?YUYd}H1)ZOumH2X&7!1tl{koYrVZxHlk{;x%iQeZ; zUGL=Dn(D=AWsfbKwlg3d1x8yeA}Gpe+2ML)Dsowz??w?wl-0CERFZ)@_3ch!B<qR2=JN_DNS1H)lTm9KW>rP9fVHt}oG%M@F0TztCy`oM_;5rYNP>q-q zLBvYr3gP+HaQRR}M0zRKIdoATlq)?d3>fh?`*!{R+x7o%*Z;p=|NnOV|J(KdyVw60 zpW3~*>;Hd;)c^nOvrR_#cYFQ+zb}UITU!4g7BY6I|9?+`*rEQvILGL|UH|_-Q2qa( zEv)42`v14<|KG0vf4lzwcY6K*?R$4_h&TSxjZJRdhDTegW|OfAgNX_m%s9d;gCetPg4j?>zXIhxFlJJ^IThKXUS| zcfS7g@0|YJ;ZGg^`tgt7`QoCl--bot)*m>1^|W>RWw(F+_V?d@?Y4gVrCa~yE_?SK zC%<|$I~klnhw1Ub(f@VhS8n{H<3E4!H;zZg<)aTC-3IjqKe;dMfBNwAj%LrQ12~Jg zVW3L54$WC=s9GS$nv0!w8=uVEUNuvF!2%a{1HI)BR!a7c;(Gcnh>sL98WKD(FpLE~ z!bQ{@t_%hm#%06~aS=n~<*cTy`9=;Cc<_$mPD!`}!rWPgQ6TDF!H@}>o3#RzkB3ND z9-6q4A`)o7zO3WnQf#&oWFd@{FZhO?5nsPE;==`^mUL?Hx}Eg}%Cl>gG3H}to*MX& zAWL1T6ED_yyKC3FQE;!wbcH)0Ufo&7D+Pl0B+x^*Ym`TviY)yIo8)4@hNeKh?sN+2 zM0YeFt1AK5?iZJ7!yOPs9&uh8MXnf)Nd+~bWS*>3WDUz?9f=fl9qZ^yho2Mv*G1yhta{EGiU~`+S(JlP6y50J}UA(;SPxJ*m)c;7KoWv zt0pSkp>VdkN~ZIsn~`jYS1jIcs$mH;o8WpQLsa?7ts+CTi&lSW=XX_P9zn;UIASYp zovl-S(B&&2TBV6$vdK87s=U_2$CF`o3N8V1^?OB8N$ANqJsBIuqVF^;1hy$JU5Ff4 zA7fn}ZVqDAW;$v=NkIm+xD~vMjN%q!sFx>=9xRlUzK>!yH(#td&uN&Abqj8=EkHb= ztM=5c%c}B@B3BuvM6*GaDp_YtuGmF0$XXdcEG-3v^;Rtd9gQ1gGlOhi2!GGND&}Jc zM3JiuogrmW5k2m<884XhH3wc&t_tO_*@xXlZE7voBPMDJCGDNtc%_UIrUu%ev^cy6 zmlYi8G1r}B6Ju{N>Luuq9ZeP!NfVa+MFV#$hecJ%E(oD` zS6q$7)~%>Z3Q}OeE~J=?DU|d%Co-s}1GruQal~xY@+0qX>x0A6U>FW(Br?`Ikb^T0 zRrYmc(eaIiI}^k(9neBD1&zMQgRQmifY`>X9S~nrjKiLl>K0X5w6xA(Y&Mz|eKf7k z2#PC>J58A_+v!LpY!_Ta-z#bo!W|GrDtH$}K~NKdN>53uXM-NmxAO#o!%9-s2yR4W zO&GB-CFiwd4G1dWZ2y~zaqNJ2ZD&Mr_c+wpGE>j6S+sH|LPHI?R(C+CAxX&{P9^)u z#On9iMZmI8@`Ae{{&}I*4zZ-DX$yI)%gy{Hx9;&>Daa*oQ=Te8t(F!z9KE1hW5>+t zR(%@V$8aHIhjIL~0>NZC9}m{EF=#1-YpS&DFutng?JhiFy7&s0YYOMIk_K#i;i^DX z)Kp?&$VE*(ELUM6WtYA2x+Iq&anf_A2ss_M8|zL>G;VC?e22*JU4@Jt5Z_rKcJSJN zSs)CI(><@zWXgTLK~2WcP^$1GuJG+v)v00Q_7onP!xck_2cw-4-gBn^L%rjM$$aLH zm!L5dY^2Pp2fO;mJ0pI4ixB%+tqO{1F}9|3 z>Q!>YYUaWobdW4WptkL=+zQt;rZD%eKUyGmu&bZh-2eZw&G-My_kQi(`tGmZUEcYn zJMrl+oX)`a`uNtryXBsI`ebnYspGFZ`s9&$_)~}a!A~Bv_kUtvy7}Wb8+$*tR|Vj= z_}`<)_Mzh_C%Z7(UoR#7qsN0ngB>~qUM~^-qsLaE0ST@9itk9JHeHcTm;=mMd|ju! z@wBWgt0R_}Lt)b(gqjpyFHQZU$A7fYULE1QG-# z9Csr4T{)(Q>QeZ#;^aJdUXR&^0oWXF;7 zshRa$m5BL(DKVIwg-)Z&V%JN&|LCz=Xs`pF|MgPwKYH9LG}wXQ|9VOKA3bgt8tg#* zf4$uOj~*+920L^FxL(HoM~_>D20Kvnzr+KsPdQ^s@=&d@=)`u|auD7SaqsPrcgB>~*TyK|w zM~@qY20QdFc(S$C zp}`JRVb@D#wE6x&*t>h<^k1L+%Hh|8|M(yI-)q5Z+=ejfTkO5-^|o{5Z5tAcrx8X` z*wa`@c4)w{p*PvZOP@~Yw1Y!FjaB50Jxw+WopJ6IJax}rpUwzco=Kr|zFWx#^+s}@ zMFL2j&S-irBFuTy6E<9V(1vN`4BH<%uCuv(M69efY&KQ_z+f3g7&8`=iep z_u1oK|J|GW*MKJjfv^9Cz2TVh0k2x$n)ud6`8Phhpx-(B;YPpZ-?1~a?9J%T{%Fg41;+YDs+A2h%A2WH-b9M@oZKr2f*~6}cq;cwJp?yX z$?n=4_%NM$!6Ki-BBSSB%rV@yBATIVM5Cb2iGb_`Jmh)!=|B!opA5CB3uw}DiK=nD zNO+9PnQqn`<5jJ~mKC27+q!DC4TWI92^tP^cRtw~q!eG(n^K)C8(!H7Ep0tqEmCkp zl=ZH{Pj@zSz`gQ~%h%XVxcNxFw~Nz0+lj6kZqVOsxcSf%6Zx!z{NWdOS)-p8J_@R; zw>P>x^9LsXSRjXP%CLvmsAj*Jl~XHZSmVJnG8v3ILzi^I#I+X!2CgivaP; zgIq8elC1mXfL(UjAW~?v%sE&ULt67{uo?(?H3_um_B^56W89XlEH(E*z#|Vd-oV!L zh?9^>cfDd8u|1L=K$DT1Fe8&#wH|Pq)(~iDoIvfe>Cxna)IpY};t@1z2NRkmmI8A% z&)Wj+&wGk5ZGLo)p|0qh^%i-@*-E|yORFDlR+SgA zvvn7+sionijCXHZY`*~v?1@vr;J<$hml=pGZ%7!QYV6(K>|wP+3;ci{^3=o{i?$4p zw3WgvS8xxKs5xGnW*F!O@%$8DJT+erI3qTrVbn;L8M~TAp`W`>d)Qo8>O;<#$DXb8 z<*r9}@LovKRj-98Ymt#6(1xiOCoWti=>bGzfG%OGFN^_3SFW_oY=#bu{k3WO(k>x9-e9e|%_#!4Tq-SPEb5eN`k-@G*$;Lm+uN|sK6O_MzuDMd ze90QCEWroMoF9A6pbJWM*{+B8ooq6;>@9lm#VGok&T`B#a4QCCSeIeis_lr?}pWyL{+feHp8n(HfK_M^4a^(^Zn{ z_6N?9>26-)Gxxveo7bKB;Ty;)e>2P2vq??iE4b`M>)6vYf6jFbCQ0N%R@3H^TGX0Z z?_r&x(_ucVN>eGXR2K_BtRc2t7i*vtp!u*0#9@k6FcS zjHC`q`41~|32-?z0Ylf&naoY|0qExTV97@a=nasN4bUWtf7p3gL&{5+VbyUhHs_#O z*Ok_>Cqs`Nz*1{F2`Ce_0Gl)j$-A5mfFQDe8+`9GdtZFx{*T`OmizkN7w`SU`(JwR z)AzpjUVM+bcjNAl-u+W|wY%@W^RMsx^*hlW^7J=Of8_L|r`qYe_kQgre*4q6zxQ^$ zr`)D)-?;UoCnUHtpxt`+$-h4N>nG9PpFIAJ;~zQx=&^eIdyjth=z9RKA948Whd+Gy z&4-=CFFE*E2j6`V-kcqj_y7C;KidDuetZ92H$Qdrul>$Bd>{{;cintR%>zQ1^+A^} zG8#FRst7`})d3VVg9RYYSjJ!?$a4Q?Wr6jh|> z?S-&zvP+g9;-b=QhdJAY*A`f^xxCxyjmt5NljWf_F6%wlNrs?Z($FMiIh^&%(^ zU!K=j$KJ@BM9jEqPNQkosw;-xOs0St23apPqwaDGYzwS!fszgSR0Q~(lY>SuO!#@F zGoxrDf<3W1&GfWGfSO^@US-7-{op~oJ3ht0FU0c1=g5(nmD7(=-P9K zY^PJrKq)hh2-y@`)}e^d&bl`e&;n5~)E zNo^9a^|gp`BOwdmDY6AtX@-Q|e5GJ+S;xj?2cEPl>$MS3SRAhxt5!|ycvRzI&)$yl zcnjp__}XKYF=|py%m_1yMH<;ia%9VosH`PT8!JR0L8q%NaL;%GhoieJg{#VDc3RbmaR7OC(=R!BV5w?KBah8uwB3QshekMI`gD?D<@7?1&86^*7*0B_#f z0_pCY!xmuEq<8x*d0cCaP?*RE15^*&wOIvj!$S+TsGhfdtGB;>3#@={DL55GJs6Ig zoSe99Aky$0pD2cg;o!o6n}gbOw&`w-{ODV@!16p)N8?pgCRQ9e92;vIRFe6wI9)1z z3-7})>sN(P=jg4ip2kmr5{H_uIm)Tp9H+q^uMLTQ7Y8>d)+H0r*2Ip%u2zF?XFJA| z_iTZvZZ13g!X#3oB>Ho9=3qgyL=n=e-Dt$()G3#TKJXrB>&W}u7Krp2t2zo=I?0EC zO&02{8q1O;(#@)ljC2>)>zRnL0upa+^?=y|VYwA{Sy41(4o zlOi}Z8W^CsftBq%-Y$UJn&|kxO?4Cp9afT7bu3Q?XxXvJyj^oLV%A*GcsB;Lom;os z`}!?#=_nkWnzb3RnzVIju^fm)Ws>9rwdB_#byZ)E#8s)0jknWr{I9maG?`ZWM%*q- zpplkW@;T70kP}7?z_H28%xVO-fq=9Q^|0-994k+NVcbs@&tA>Ra%-g8)k+ud4pJWn z9qER|q}(z4q%m8S9IqJT?H_sqluT9k9iKHtZ#BaVa>1(}hh#>_0UJ1|O1C;`R_RX5 z+a=)S+qS?YsfTG*Xx8~?K5f`plXGHEBb2pS0|yLpJ4l+~NJ*{h+n00tBp@e|nYV;; zw_BS}NGk!wv_r+1F*Z1d0$pQ$e>h)O@pY}XeqyWtWh*gOkZIi|7gKf}V5^C(a}FN+ z>WF}?=EA8t;c%(U13@ou$9S;V0;5s4I~Rn;nrWjT&{KX1lobmpKT7})6*NH@$i`P8 z)+ujo_4NK0*c-Vplaz3yAMo&UM!{;Cj^Liq6+6q?lGpGdH#N{?RAH&5Td4-E?lxHMf5#T+(3G1yE0P=vvo4x<2@FTA zj?C2;ooU<7a;|TxYgaCfw+rh{Y74a4p1q3JL$}1j^eE7?O^>f8>{d&3C4d?&DU7CR zZ>E~tD0lh?x4=GViqX|;N@H(E|b>GDI*N(ik0SQQ1Ym)$(?MMIjuid z(o!oO&xIux5+kFh&OBED+`sTB2|LrC=tD~u>@K1wtJP0$B^njBErV5!ZBo-rDbaAJ zG+)c*MQOPz4LvmzC`~k}DYx3j)tfiBz^)@#-BMapNUs#ybF~5*qcIx3#`!hfNi;-K zK|eZq8KT?iIQ-!)P#+V`Dhvh4Lb z;MZ&gf-T#iwwhw{x%OxRyVsND=KnnMUE{%GlQF}`AxMi+kHb>&RW~?y&_bq*rVaZ&V&ps1MEBCkcI$BX#Hq+k?OMK_ z<8g`BIiXvLqLHP-ff;m=FoC0F4JafjS*M@utNz$lB9XI=(yAI&Iogek%BYDoCit*B z!&S70>asC2zy}D#>*D~ea5&o6>!FWf)2xe47EWDpx2Afy-K}cqkSr68 z_Gq;TprFy6+LI>LNf1*+%Qn?Q`feYqmDX zkHw1Dtd(jj5>p-7te3jsaI5M~@(GalVpT{M{gs(X3gFgP$ZllhOQ_x8NFUtM?FdY( zy;`of-njpZPk?~emW=7e*z1#ev^3f#3d^#YhgB+=<~bWp*(PW>!(-b8;PmsiKqv#< zebsp>?pFG9B|+@zg6uYI(1si9R>yUugL9oJ--E_aOfKC5OJIwlH~4|Y(Y;nX!^;wp z%pI<8t*bU)Ygj#FQVPL>0B^lx&ntlHGWKQ^Q;(FXA1G8auAB-%E)7C4t;8Eh^wgAUxx8Z74nD0-DJfg1gR*hMm>KV30h$ArZaIJN+km+L0r0v(m7`WR|pc@Ay^ zgvAl-x?xw-6mL+*8k|4G{Sx5Wms?TfF3XA9hqMePTt8j*r`XdA2P8=n{PDarj4~6o zZB^Drm6Eb%#)8%Ecl&G2a8cHtnNXd4+L7r~lH}n1ys>=XneTTqj%Oa9qS(1vIe&cm z6h*+%8rZ;qwE5X-;gj6OX8wNbau*bY!W#)s&0R#Qh{HCFW|2Dgru=}IE(A1crGk%8 zgRUd#wML_ywA=?B8h@a;*e%&@;i6@2li4&W658k*BXcI!lo+_Fme- zx~|U=pOUD15!{>k9qx#`DEO2lxtiOj{Se@{Eno`wFgnHi!LU@ z5Y<&?WO%^E*NR*rSAZZHT7fKjeYB>L6-YBsG%f~17a?P+Y$afenam#uO^L>tfze8v z+45>R!?U4pa)!ZcFK_&AQwg_&FHk` zF?_oc4n&>b#!5Jnf}jga6uhe1M7jf^%#|O?b2^&?iu)1hOQqZhaJI~YO1K?-?)t*bQ=2635^kPa zLa^%t&V`SsSHk@^?SX$2B?RbMUC?h)CET0i6$q@)SwR507gHB*uvt8c<`LUl4{TxP z&x3MRUZ&GA$OM2EJ`WJR*`|y(H|R(a*wF$Vo~H&i?G9(@Z0<-&lXgL)3vZyRpcitZ zJ#X_Pa0Xb(W3@45(PguZ)zr_k-s-2S24AGxjG{?c1Nck6H6THUJLx_$D`Prm)cIeGu_ ze?9)h@i!f}LFWI{N8fj}Jo?(Blf!>@_~#Gp!>>B{<%1tMcznKjk**Gm!?{TzFhy#!Xi09M9c zWiEl?7r=1rRr(Sbb^#2-UaedLLoa}#*sFi&5*Ts;48dOg+Dl;Y1uz(U^=mGHK^MRv z?A5Qn7)DS}hY=L^>IW}@fdtLd+BOp;_Ua$J1a^Uy5Cr!saq!h&!C&xHge`hTzaW98 z(W5k-f8gOmXJt+26X*SQIDJ^e!}rf;9`-7AiTDf5h5%+mUIM$oYzSaBAGic|fo+3+ z(XW;-fn8v21h6*z64(XvPk{NyE`eQOZ3M73^kP`~0<$Rtvq3I_U0^n4U^eh2unWwl zOkl43;7eunVRe2h;t+#W3uG z>BhixzvmLz1=Edz>He}yU>A5d2E6-AFM(ZPB^a=hcV7a#z)CP+CBOF)*acRC0W0~E zOJEmR2?ng>U6;Tvuo4Vd$roP&yTD2?U?pF43G4zZ!GM*#^I{l!ft8@ZO1|(C*acRC z0xS7~OJEmFH%gqm`g;yO|MPF4WfU09J1%*`1$Ks(PhS1}vnSvvFoMs! z64(W{g96*RcM0qQ+d+Zt+`R;Lf$gBccJ5pPyTEo(U^}Omz%H;I6xhz~i($wGwu1oM zxpfKb0^31=?VMZ!yTEo35Gsx@fn8uQ2(YuGOJEmR2?DI-@DkVsR)PR4Ik*INft4V@ zO7<^-U0@~P+U2V^FM(ZPB?z#Ry-Q#hSP25G_CE|!XZd;1eAAlt!+7Y#{>%f1?cf*W)9#4vz)iAw@Ge(5&;EW_qJy)P;~TF! z??7Smw1S7JlO&esY;<{JYN*~aM261nT&l05pz%qj=?O}8$Am^YFn48mc) z$*TJ$~ z7qiWYFen)LzzZLK5zKNw-HrCEB4CqDIZCh@z{ z@pfQtdYa>T3kNh-uxxO3?(BByTi|`4qXO_k!2i)dbHR=6Jn^>jQ@!nT2m>}Q`@HUl zm8p${r`8uONjDIM9z0;#647w{#WKyREK~E~c9)1zXbyHZV^6E;!}UYRn;W6axd{9qL?4#1Jhf)) z2i?|#X;dk9RJ&&8jmi}%I5tBE?)UicHU7-~K%w6bHg$bO0OjA6?zaPN+0)$bDfM;0 zg5Clre2OA(MWG*pmM>=H@SRyzpGPs`K~lY$S7@&>p3X)iJR>Vqina|hVHU2>>Feh& z^fO2GaxZIYlo!O)(Zf|c#!dDCrZ#DFIH#0^Xw(D1OMhqB`6vWH3e zuqn+li9mZD8dIv7*TTrP+hMMhhddklW*tmltDaQ=fI`0=tn2z55&vE3h`a2NKLmX2 z^_|_u+ur}#I=i=O75uCMn~msid!ZkKLKl;Rpc5Z_qkB>}@Y^U#=;ctdM#HMsq*!eM z&m*BVS_$<@uimtudjUj+xk1jBP10$iW?J<_Bn~Mg%m$e`kS6wUsIX#1pmW;bNm_7b z@Bm!HqzAU$q=Z#;PC{=eD(U-nG3~4{tL&4$Nte*-}{<-cQ&2=?k?|r*ZuL$zqje`cl6KqKC-R~zWA6p z{khY>bNlB{U%mfNz~#a9-uq5l2es2Ld-CiXzXUoE9&Ne|Zn_JC&VTPdc<152I3`Yh z_2fq`|F+)y2RD$*|Kj{_cSoG{Rj{(wpiW|S9rV~+uN*6vd2}^VR{@3OJxRrBHs;M{ zxNc7>x01aA((GA_O4iL1dp_&X62mAFg_y&Zwa`HW9%9a*X$O_vVF|kPpg=eyt&4Vz zwvK8wX8~%d>EUW%%!$m)?YW?OxwV?ob~mhrr!U|9#xqQRsZ2)T8oMSfa%!SU=wJly zIoA?tI;VOWTy+U9C6HtsR-s!(i@JUqw6b7eqqsd8VmLpr)B4QHa6{z=vuVan#^Qt< z@#4J89eg{q@9e$QPx`LhqZbXzNk&-H5{-3(hI-3Bc9ARH`)@W2u!#GaZ-w z37cZx@gF=WAmw0agSoOt2vrU-j_6VAmEcwcSJCOE)N>>y(OcALBqk>xdV-J)F{DPT zR-OkH8?^kHG+Nz>+paTMh}VW}EB9GUsl&?A&lLzP7ol9v`|a{_U0eI$LY?5{%wU9N zY%I6z#EO@zT4v#lc2Km(vM{ZzVLVaprE9{a`>X1LGF1nd8tClk43UvApvYk>_2$rC z(d5HI$yqK>`VCtcwrbKimTEYm%{ji)MJTe{=6Q*f=S)~0V|Tv)Yz8f)t3%AdAHwxe zwY#a-oDf7GSxj=JG6^$hT+w)INGX(GFS@i?kU**wVF&CYHXBZfgTqxEObmM9`pk&a zIMiy3>e^Th$=l7dfe%Vn)}57BzBC|&#SDWIyXWfgLhj65an+G%C>NK5MkQTwCq<7a zYav&nv01l*418uR=z*s~T-JrD!0E@K3I$8d40FLnXzt{L_lmb$Z}q#Zqz$9#Frp{% zf*4Om%Bb0qRdA0>s9Mu}QGtb?kA$c1DiF0Q*`VWbGPQ=Z-GfSKDNh91mcVoo=Lf6b#SyBN~!ONye1QisSY)x`AeCy|Y)&+Oq(Vf{W4Y4YYoN(#T4*Kl13wHDK*L}l z0+S7@T-H$NC>1MZ%P_2*A6jbGnkyylcG3FKG7_i+RTY~kg_d7)<1T~u47oi)IVF_f zjP6E@vLG<#(7wH$k0#rXx?bLW{+-}yDJk9;2Jc~G?#JdJ-H^U$v3{$^F-ubr$VyM^KXxs{#t}#xdq|cZG zTee_ChbG(vO;H-lGop(OBIVZhT`5)w+zwp!0*3>5KrH6`pkLP5euZHDIq2jG5i^e^ zkV-4K^$!afbGVC|GubKoW30mS<5s@v0A5JD*Fz<7_Vz5t7hoNHA zV?>hLPLJ)DR=`Qel{h^7=>j1+B4as1)ln(Ouwr7GpW^=>w#u0D~8s`sl2Bge?<{XB&1gM`ZF??n!j$e2-XX)J_g(l<4Lorm7UI*s!mkbN||MbP8waA zmf*DqV^u3V_`CP!H+$#T?6?2-+i&1E`vwYQPecLkhAkazs}sLzk3pNd)f`UDVVJIK z8sOEZeKqL-t-VyG7}jS4}W2+m7$rfCF<8}B(ts< z<66Di<6DEi-;4|wN1Q=B>XA|xmFs)d7Jkf7_%j7!S}mDlkDbtD4j8L=xxKD8%yld?+PuY>8H5fPalQ13 zk+;_`5Lago-2Zria804LlsFj*ZM9z2bGPZ6O^uc#m6Ha5^#?LNJq;LoP~YD#5Oz!( z%SnG#tBiP90WJ3CfIo{N8rGD@gHBXa*m7#s=yB}r6`l61L3Jt1u#9+39E{#-O=Ni; zi3Yv|{8XlhfsGj1C)&$g@oxNwLWU(cv``Pc`9Odil7U;aZkp+`p>>dsQqe6p@6@AU zxuCMc?V{FSs*zy=@)A8~D`urfPSGcGRyTV8U-*@dg zn)j1lj@nSu!Uowq0ylJ=Y?U1U-cA1hJvXcy_aEK+^?ToN_t)-z_|AX3lb`Fj-a&%W`q0Dkemmp}G_ z1JImrmmXu++YRjHk5Oj|?9z&BhjvZxUAEiOO3L{`2zu&`B4rt$%hta9F~H6M-oQ=@ zlxDhWMl#^l3#Z6bu2x6GUY3{SDC4?8Hum_Erw3H4z5KD#nF71CqT8Wi+IyvHx2Y`6pq?!qd)DS;=1ptCSM97o zy|r3sX|@!H9i_BxyE0RC^k$4fpxJUM6cGsG6Kido-D!9g1s?3Izz$98uA;ynI8$Jk zUVPU)1;2l11+IAtzH(;;u6YW6-b;W> zz4yl9d$^Me+EF0qnc(z>YE~LDS2gygAN|4axd9`@`yuduaLb7&FFbfba?%fcppxV> z`vV`SscP#X^nMJx|7|zE;Jn(-tDpBQ+2>nfR5ZD*RC?Wpu#x?Ve|RO?&a3yIB?mvZ z2Q{UpT%p;9rk%;@Us2B8XURd&$*Idqms2Vl+ug|d?(e&zoIB5wgPfD2%gv5ZXFDBv zBj=;s73G{hOAdTaPMfQPibx?HU@XLhjoPgj(4@+>*zIXN6S&~A#1(Ni|#(f-O6DDUUGkd%HomZ5z|13G@vBs{ zH*&riyPBL=9z8=2d_L~9Y8_rt6q>DWFZ~}itv6jDy0;;%6F?5!`?QEWzTy5QUUitcG>3%-OR8^x{=KwV|vypS->#nH# zhhNwIyjPsp9ev?S&vaf{KTG%XajVkfE1D)6f(~^5_}|U0Bu9BAe_i+Uo_0?6kN^As z?MlydURga$_w%t(toP~-R;;p{Sora8_?9clQC`Vj*ZsWrp4a^^TrrNnvV4~A=i_Tl zuF^flXtJBQ_wn{mTv7MM>$;!!)bqMWKYXQUITQi3uP7VCPD5 zlvk3^>W-X`X6JR6zW7SdbY6*{rTcm7RB5%VS6g!B%x1piYWp?imFRWd&quEFx>p#{NYS? ztnc&Hqv!Yh4n4=pH&))Za_vfS<-+~3Bi`0^73e&OirkJ^*mfO{=H@6ZDe{@;V|J^1W{ z?7@c|`03%d9r)n>r324D@Pq@Q{l7Z$)mXCzVs}V}2s~ska z%6_aGMPzJ%TPxNi=2IROhbn)Coa(;{5JA_+l(DhAu& z*mNkRr>aAR(HRhIUINi{Q5rm4M%gtD!Ihn$26|QraJEumLgAjO8b-H0h!Ae7WFSK& z8IRRUe9UfA;pOa9Mpv?<^;lG>ri8Mm^n_uJ=*WRy$$3LU4TlDn9d^r+p`1i^PXo?D z%Y&33F^qcHY-)u@xIF4Y9ES4X>6V>GND*3s=^3Fs%@2q zBI(x9i?V3Y9|rN5Mb%26Q(lZ4q{F+>DJ#LVC0dV0W!B+xDH?B-TXZan(nU7Tc6i^3 z*E(W4?h+$x_sQuMEjJUY4{T;wuEo@}?pNeKnR0Q^!Z+eJ$tuyPW4aS1Rk*RzonFxl zg9c*ma5N0dx|FL88f+otI)g;QZVvNsLA2u~3`_dMbYpoM+AXt1sEJhATm#ewZX^o$_?TRGijEvL|SB!uW^`!xv|bOoVI+ykj1yEd&IqjCc_ z?3)Fu8ZLx1*k(DL&kuWet~a6vl0Q`BTC>R%L+QQ$Je7e~(o!xr&`NAsgQc?UdkpIc z<+7d+6I!U+BoaCYk~E5_zcid)k&p1@ZonaQE1HIex}R<4K}rLGG-{z_WmE>OvqZdC z5TF#bdwKuIcSX+XWoQ355uvD@5K2-Zf{14v8`u+wfWtGm?<%<2#KGSp7LIDUass86 zrimqr3KF7*HOWJh8Qx6vRE+lZl2`zVZCsY>k01|HBZUeGP!sk}AE2VP%vL>^;Ntp7)mw50N8*yNMrr{QvsrLmeBZo_h zZc4SLW+!6M?)x74_C$(oMa)s$i^;8`nImdW)oS?B0^Vq&;AuOl7)Vi(M86K>PUCQ4 z%8QGq!ujy1ZP_W%{juzI6e61Eigqeg=yr=qkO>TOxjICGunujSB3nnAXqQAm;Bary zr4ZdNAOi{&Bl#lY^(ZY0lK8tO9O;J-P48WZuh@}p2VRu=oYdaIoB>bXijn<&5>8AdH8{MhMj@Q%Y~!kK12luP&^n;T@tBz4tB zt62(9RQR~($RGw?Z|At>Z*D%08ma`RG&z8}Z?G3bhHx zi0i$6A=$$cjqpH87Wy1lM@2&O7sps-vV#I#5&SzvI`thh%q zOq?UFY%^AkAh;&```=w9WR54Tl8XL=hhJ2rb!>E{c{>yru8{%;pua-7+Ck%I3mF zDH81@J7lIh>Zf_7YeXEbW@j>0O$pE#we*SU71VGLPS7z(;E_yNq_R?-akV&vrCUt6 zm4iSMTeS;PxiXl(_dV0wpz0vhI>aSvg%WQ-ZoSc02`32kl4!o*=h}RRig!uSBC!#l z<|WEQQh>tjiTco@>|~jtkvOVE{CW;_wp9~tC4wB!Fmwi^c;&#)C!}Ppm-9H@Z-F%% z=xG*il=PhF6;qx_R6*(SE(6v$hCDhRrmX`%Jbdl%DZW__$~h=vp;@6LT3}f~5a7_e z&voo}k?3!%Re<0Qm^7)in8u)ggDG8#*4yyuEQagS@>~yp| zXbY*gxg$>p)5*ESH!Ey%L=D4wfwW*M((THFSk%Xe(uhL+w5H*uRu4|uey>n#96d5+ zRRn2?o({2bE|Lm@qR6!(N?-3+D2H+FTE3SkVaA|Vi)#3RT~i|=+F(V_w<|DLsxi%S zhHZ!2u@S5TM~TEC+LH>Nk&Zbj%-{d=sf?f-<;l2XmBqU0!-HW0XN-1KQfVyRY~+nV zFQHs22d2FE(ajYcAIISerP!9%#H3^^o5P~%Qrn;!Bo4a0MRkJBh<&t!Rh6UulvRXB z#U`2;V@a#2;0j9?!U;Cr@bsvwCL_KbcUr08C|t#a{nLaqrOgDgmFy)N*;-nG#U9eN z`?3REXR<=ZQ)Va{;+a&Z6^#6)*Gy%Upkl9Y$~5j9*{(G7{Q<}?V>@ZoWSeDjC>F~w zg!g-r9NyiUUQsZED4#51L#9y~V5SaMIq7DQvtmPUAViWvUjk_tN-^Hk_db1k1zHb+ z5Ci&sM~%TK6>F7ZX4cb&=|RtRa0~QN1gkWRat*>-9!#%*aUHZh?iQG6t&zoq(ZDXn zShXocI4i1x&Syd`+>()MnDtl0=@mJ|kU{T5vO84GP9YaAWqXlgx5M)mp+RP+?}qa^ ztHy$SjD61Z3eem$;*6Shr0pamDdaL7R*KQ0+-whoaAr8F4sqBlvR)Ti`pxu;l?=d7t<>e9i^7eO6{%|H=#Du&Ok6E zt65Ux-8LR8Mx9KwHfm?1FtTUr8I(jO+w6DoULGAJvU#s!7ve3r)y^hkAtah=4tN1H zb+3i>5VZ8Asf>t3>GhIraD0pz#jRLA7^1dYZMsa|Xq%N3fx@6wHpVjep4d44fA{kH zcO6vr{xkT`-9LA0;D3+?HtxSs9#@UI{_#s|3xv|`9Gf*$J#E~cXPfG2k?^){sul=Q znv@}2AfRXR{RM)+Ce@d=lr2P)1W)?85GmFRaLf=SIN2MeO*PhO@}ow;o2sDg3=w3o4${lHF$gSt>fK7i2|Lj&?tmH<-8wg-gQ8N#&bcs6T#uLZ zpeT2_q9%4FtpSfnOBa(dHg3p#5E#0w1ho@n#Wl5>lRS~H3{IR#VNo%Lb6$`!2V}pO zuR?OEp6e#@KqK|zJ!rndG@G{$_T5Gc)aPqXbU!W+w1&(dsR>Fj?q1)&QJPjc1zn{U zF!_bO!%UUaNO){!+q#Qw+f4ng-NgbytJ}MaMfAQ+k*PmymD8=|Ou@ssqx2~x_a)~t zt?OxhX);#sD7-j!_*>n;xsK*!=FFqnaf}lJ%=Ls^YrrO*LQqxe<(SnfvOVI27GX5R zH^CCH*Bgb;c{Hc=S~l!N+Ko!N9EZcl-8xE&ji6;P3}sdYyCJHTo@*14fM?-!VrUGa zHrSizG(A&{C=82ZWo}rr!o<)>w7}_5hLQ7gmYEvgIv9)B-DjgXo~U1(0u%N8!}y$r z;x;2MoXPwl%DXKxQ3HcpB(!+@ks2b?IF;4efsj);F4m4_Su_Jm8sJXE9i|ym%vI~W z0;ln;|9hXMwNKbPX=zSP!IFRGdd;GYDum;dZgD&_7xbZoh6+cE5i2rOU4Xm)-sI|D^^_ zF7E{e1NJYk6-Ncy)zXO&pX!2YCC;Ff%83L5`bj=1cTen zpvaE!HiB94Y6YC-Y)*crIjNmQc-O zLMY!&b(@N%L8ycq!>HMz^H5XhM6oq1)r@7@8IurNW-({k35G!}vyG01v=c{JPPRfk z!W+$qjjaY=+vp*#;FJAauh{L&`56v-o_~r%KjSfjQakApX(1M`#zS-`J)jgvC}d-O z-YfL8YRXSHLcYHiFUJEk8EH16vY=s=Oe)2=csh?pK)oAf*e6Q~ii2S{TMr?tD(Y5D zhHYB`s6Im)ZotiOIQZIA9Na*NWU4@@({k%mu2x z7M6t;Ct5U48XW{>i(L$l$gMgJWsrQaUuqb2g(#`P(9a^P{j#psyLAQbbRf?asyZ{n z;lKr_I5?xK%0|dhs3(%0psb|JDhAPV$-`_c>7}J|i%chz^;XGRYw81Cbc9jCEm}pk zl!zuq{idz-15ip2sVRyW34=G`8du_#)sa=FK(!y=gy9B~EA`vu84mm2eNG$-oK$Sr z!zC!z^@L0@s(>+|g<}{I$cz}O6pMyiq(F^K7}Dq9aN{WsHXUmh_-MaKrQK-OZzP)K zN-(5^dYNVEp%YScR}b|@)sC{pp?p?OOW~T!G5s>v&+yR<=Z(sY*t2qR5!2{iT@!?6 zk;PZ@5ZALuT0x@ufs#X8T4zQNE6gbl)&PP+oFK*ql%_PraG^33IHs3kylOI18C8>U zM^2){IP9-wy+$f7^{^rx(~RiA&e=?(RUVY-M5W!8i`6im=1OU;N1OVpTc{Q+cEmYO zrDiZ$-BM>b9QcpBaL_`fP|59}T?nt0!%CvvCWaakP!$(E`CP|^+=Lk?5}ou~)ska! z%%?3nQAPriElVrXji*D+^t)yy3YnWO6B#I1YX+KsiHH(Vdct` zOA|l2v{vjYP(RWEoq8J;BVm?$iBUNlDj4a2sF|@kDYhyhqvoN3*jUSoi9Dh-^%PMi zi=#@6C#VeU$#Ew2`TJpu%8@RpP6?K?}qmq1{oNU{oI>x$KM@maC^Y=sX*(K&l|4r799DnJpzD zffx5VI$|kp%jr{jyrz^aFYT`72`FL+c1{W7LdId6nAlT_0o8~m+rq#f%5tpi23bFm zWgDy9KyHyTC>}Pbi=AO3=aVxWcE92j2UV>$G8s_FFvAepe%XfE0_C;Xgo!sItR(2U zM1W9jE}*G3P(rFDsbMYEiohY*w-q{J42X_PI5a zjX(#}Nw-unkX~#c6m2WvdJg1tK^1B(opVXDudgC%Bgl5THJxVdxa0>|rgx*v}W+1GU1*O8q+`5HzxI`~agcYg- zLQXQH0RqK(?FPm52n=#ZY#g{rm#wH+WhAj|*&DP*BWgwu`~K_{hxwNTYaN{#czR6A z@v?#TGlQT+OCWZvpt}OhW~?kNm>EeZGeKFOWBG9TDGp*!1XZ$88Hb5(qS0{4TwDd< zM3_bi@B$Z9x+F`Wh#K&}YeE5XGRX>$Vop8NZ}ElPXp|tKP@>rv^R6`1F#=LRID9!Q ztQJgNfN&k}Fjch<7jbtuqlcyRDfb}Ovw?9rAUGNxNnFe9(@a!$>!HKR}~jN5JkDi^w;cAKjq5wVkT@zub$Kw?lGHH(;0 zXIpwO>mKmCsDYAFAt8;6PBUUTC=A)2!fB zLj@+68d_Mtq{J08IzXtf!1`%xtr}03p|WBbMc+@7%+MT4jgZMCK+R>?7`Qy7Bz;V6 zag5Vg4b-;RE5dSs*Id5R^9*r@!>)fh#er`-l}4boYb8<|NFf$AzyxY1k$eN!Mr1h1 zNxWRfo46*d*%>T80+V2x;^hI0sF50)h(;hhi`gC;63aO~Lv?B_NLj{L&AO2*^Hk21 z;Q{3qgTS8QuopWg4hlDSJklH-3g^V3GU(V@+XI7Fa4Miac0Pb8CBVG8N_X>p(a_pB z!Gd~#Qhy#j6z{?Tgxd)&6RKojxE_Nj8UZggS~)~lLt1-WD>;%88f8QcS!-O}&N67N zCP>JzH|%m$K7x{RIROgWwzNFmuXYB_R69gATj5oK%-8&KO%He^SH!(eho5;CM=n0a zfs2TgQ55KrFkm|_7x#q{QRI801fm2(jlfH(x&aD9moa5+fl}eCl9KaACA$rdkOv-Y zW$W!34oj`^{{NB7m+ad8&fQPkeQf#t%jU7SAA7>FBS+tNR6lyrk&hn+ktBi9NvHP{_g$i|C#%}*mS~ARE8y(e=qg`RvsmD_i)H{kq-j-W z7n4i$Vt@pp4M%mgfNP!;s8&awodTB{X9e7%LMpF;T!1{NrVc?+BiG3xv(LD`7+}d9 zRO>RDSBj#7!H|N@9(63$&I&l2V76+KPAOYarM@PY3b2V7_$}qRNEfV<(Rt4P*T)m8yOS6EL#Q^av)AdVLKR@bvRT~-wTkMcL zE8uLJ>#FXkl1z66(0oA8^}F8I#XNDo0r9f}&L*?Y!LWQ*z}XDbRTFmuyM-CTP_GQg zE<7}5A4=)0fLr|a&Va?UIj=1f%8t(Kg7XV^@3Y-*dNxyZl_pgj(#mpaSSR_mSwj6; z{b6SX+;YD=0~XE-xW&23vep5SL3q3FfHROD$;z_~(en+MKP%uC=ejdPOJdI!0 z`bExDJfb*ib!(V6dr-6lpA~S6V^EpG5UTqnc`)ia;EZ%QnAKtKtbkj_v4HAy-qyrn zHXAeo*Bzm=fZ4MGZW+fr143s7+%k@L2E6iY18<8N?l_L`bvAyfEd#IPNpyv;u|={t zBw(ps+477Ze^$UP1f8Ni_PGAv*N);xzHwx5Wcl!)A3k*G)rVpS{{d9~ z`}+eA-~Yb-)V{BS2!Jo`MM0eaYvtD~FIm~M`(?WywtVw)ap?<7=B}>+h`D|`>-N57 zDt1KLwbEOyc_kl#%A7?wQp4*(#_VE!GZw@K?a7>`0XnZ6?wUM*;`7PF>lTpq&})D+ zdv0mdc#kt|v0}4IUK@f}R=zFvxpIrr?IB#R^`NE)(Ss^7@P$#5lSUxeMlF;LMO^QW z>5OmQnwyT`NUgrK)~nZG74K7(flt+=4Ix<|m9RNP!d16vhZ?a&rQeMgvwT|B-E|X) zdHBPDnC_}f7Rz?G-B(IP5YTXMz|J_gO&zzuKHHUC!xCDCYPtPKrPJZ?8cAlfq?RGk zB*JyGNk%a<9YgLQtyr@rn0VB#DJasZh$%E~uNy$>JX1|UdS}NndGp5Djb9N8|u0aM4lLnH0Bn0XS9&Jmf;g=Yb0wXE#)O6 zgfmJ`CI(tiE*J2egEcb6Vzo}khC~lB*E>K|W-LmdTNLQ&ZFM!E(B?$DYi0?QBQS73 z9}=uk%*^+?LjelOyeUd*F2h%fO(dKqla+kT>a1%(-ejC~=9YKbIMIc$!?}Jr9Omsp zGL&qzO8HS%&gME!24Xe0GDQ0g8bg&j2gZpCq)o<&Hn+61j+5x@@^&rB#7Zn@A(x9lW&uwVy-`W|#Au<769) zrO;R{-c06nNG=R|B`cfFWDLx0mEtM8>#I--D`yI6C+$hPx-J2c6N{DR7I_zoEhEh$ z)Zq0B!Bjz)8LtmoGs|oyuzie{^DQ|UuDd-k8}SgRvn~Qr6AKdO7Im5hnIM0!=TUGt zk_GRzlZ0Gku@+m!RI-`Qc6%eDRU~X!Z&aZMupnXl$i|*lm|NP}EogRGwdR}PT{Y-4 z3er;yQn3ZS-G?$BSX_}oBcJh+-7LWr>NLqQ~Dk2vJMin~*RBaCmoYQM; z?`pR(p%Dd<6d@Dsh*z!&l#?Wz?W_$M39aa68cv|#g@92AGFtT4s*a)$YFXZEWJVDb zsetyuo|!FHyiPXXR#R|a8`yZ7^8Ku9uRn20Z`Vrmx1qOyNOiplL{2RH33H3Qi-r4b zv(QA{bZzLlX=W%J*^b_Vaj==gB_mHuTE;=MJ+vUzvCjGvfT)RuUo*F;(<~f`W-zjZ zl+p}%$B#$|)XKIIMZp6}N3#96X%$E8kn2jI2{f?qYk;(ggjSgIcbQ3vJ5UP!LXjiVyd{~y|=>^l0ABab-rfrIt^-`n>md+)vSmfeqC`W*P@^Y!yI1=Q7CzGn3q0zmxb z8TQs&YK0b%My>>vSu%&iP{2h4kBjEREhaO-(8=#^3(br~*ou=dtwv=7=-F_jB2Z5T%Fg`0p?XeM9qub~N6{ zJ5WH@5pZo$#_LKzj3B=Z3jdIG+igb)wT-Ht5TlYLtoS%IkrAE9C{z;y+E6OBtja|R zLx|yHSwoze$O71j4qpQt&|*)BBrrN6qP1wk7m&<=GbX%-d>4E-noz|8J>+XSu0wQ& zFeCP*VkZ#NOh~U*2MN1RYnb6NOinRk<$@$=MP9Lhs_i>f{hw!2_3ZYnQG`lW4q@Ti zAC;=%^Q3A7F)3Ye#aKL`>sf;^jYPc;O&-hXs)n*+U8pESHa(>4a41=Cv*08b#|$y- z8u=z;m61Fhr|kNO@yDvR?o{)WV08x%TPIueT2jaWk%)!0!{N%Ul%SF(}V0HfM6Rvnr#zXpR~T%hXi zovQx%nN&TyWjxhVa->xQFR1Q3rwE zy)H-UAo@@#4qG`3B;`m-FP#G~`U{*tv1;>9Ro{LlRnNYoIMr3;k4n|&%~vZ9-l)c@ zaIq@ZI8dU|N(y;y8>@yufNsz4mOId>n`-4-;k=82lBaOOa$80RbNgf?Tjtbs7-A=? zHttmQ&(5Uk*=^#fCN+Olsy<)7x&UP;xQf=p{ct_6gX(Ywh1a*UYB)L6^FsvZsZz0D zu8)eDVW(8d2`!#1^2{W6qhHIxQm=+VV+W$&sp_AeN!7D&jZXEj`D0Qwe!hHlo(G9S zi7-x!M7@xYv=Kg0&s*DAbxzT-YMTwZqfjRrQc9sg!4X;oM=_G&92sZiWS290#RQqm zQe#zj?o{LLQK7UlIK3~2Xp+_W=1#f|{KHY&pIo~YGqwY4UhDtrL9a33C zwxA*!A2C)D1|_47pymu~gGvYFIJMy-h6JhlMAh1zs{ZkrR6YC7>QpDAKPpw9FJFyh z>Wy%gXGeWpfE#9&ttM;X?G}4EuT=_)X#o;TASV-7Od1r5O&h8iq6SQpV?`om5M(^b zDcVHU>Yb|o(V0{|yInoi%;}Fx)#uAsqefgn^GGeM0$**|?Lw7e7-bu)&L(n|mgA7& z*02zhM9wfkDPE@#3GA$q^Am%fUT!tu3;~nKWU;5*sp=n|LDgsJXZ1&2ub4#`2Jlmw}9FsMHo_oP-X zR;YmVq4EBIZ6&K&$D#KfQV(4K;sA_;7asWV z0sFuu`#-k7f9!_+mw}l7VBgx_&+ff;@5A;#o4=$Tk#OV}+d)K&g|NF{WKfMcksJcS>%;>Jvx; zH)9?bB0VotP;|Q2RpLmN5$l@Bt@2#S>(~Zg9b$|((m}P?Ej-g>%!=2?7`aTO+(Ifp zFd31Er!sCLQQ$bo9WFS6Ev))8LF9skLNJE##|qx>j=;#`lN#YSr}HoCj(H4=YB&$Qk|Fq+9A_Y>e4( zC=f;@rw1lH(&K?4(1?_9w7Qy#v`Pc1AX%bE*?O5yDmw>aQ75Bg%mrehb`Dyh#x*X6fcBgbVlizNy^Kspf+~cDsicDfv|Rd zaZH^&F~O`;hziwHT_eblMk=c7Av7-Z$UYWNi?ZzyflVQXR3R~H=T~*q$WL~ROf~2Oe7C?ffT~&ILULpGd zsH?x#FC8Ca=9psAazM8fwJw!y-bqRNYF;-~dXU4q zvfPG)L6`BijH5@4F*9b8%tv|-n{`2nfmTB>+6^lz*Aj6?^o*tvO$mHs1kynTYE>#$ z>J3Fxy_(-5RnXLBHp!bh`S3AjIu~dhUBzv=qI3+Mup&VO6-HUA4Rq0fv`}J{)_fLY zi{VvQX4@Vlkd2}`LVZv&XEr;PI{C0MW~YcCLC4}piX`?Y`eNvn{a#N-?|CUQI{DBs=75s|Si`kC!KB=_XH_%pq1{NPLv4`NlW+C=s2>jkHQG&3 ztC~y&1(Se;S`97Gct@Vi?50jWWQ^HtXT)yTm&-}ZOpDn;MDB$nB|%F#1~%eiNzbQB zLo6wC2DXYo*=k4R2He2J)pE_pT3epcgU6Vv040hZqQ*hM712*7g$kN-LnK@BWra*a zG)FiQP#nW6l(E%3Mh?7@jjI+?9dv1qgSKFUn zd!`?Cgf@{FWje#=$O5%Lvi8s}!d2YJilk6Bm@SwO8e>LtfS1?6s>PZ-Vj~f#nW+t1 zWrnCkR8bv8ppJ(oNQUz>!fIV;SvDu*RxytV9j-ds>gO*XV>%d~t2IZlP^{7dw^JW* zP%R@S`nUcZu=aiuVw|Em(_6{qM?SX2n21#TpeS!>s)+T$8!$84 zAGUl)mYaiN*pL`Bu3<>0TH(w-S8A-5SR52UDT5Lwc&TSmY@@d2-Y*+trnF`N3rsg$ z%L;>5%S+?cDjGLpvA7G>YBe~H$5pqbXK+|qCFE8GK}(%(18&q?-pHB_qNh$iaEw`u zq3yh#MI%Nim+_;?rj`#vB|`OhHEGDTbRsVLPAi<~n%HU)RSl>i8$%*M92DjG+4KPL;Zc0+QRdqF=oRqb0ZGU#QT0pjj&|~uXzOzZ6rhl6smAl z2+nvil-Dbauyf%S>g4^$m@ZpD8{K$|D;CL`%VyG6AxYI!VT%RzD)0abYMp+)=Y@gS zTvcnMtgAUxs!;}oaeSd; zhWxUw#OvWzQyyVet(%2V)Zsk}8f?|)6=Td%ljGt%X@;ZG0K;PxDM{qerGyx1?eoj_(}wFy8;KEWK;jk$WF_`JP9BU+(_-BiF#Q`WuyTDc$P^OKS@R0_|)H zr`NoXM?h7|ZA$4z!inul=`Ii@G_GL@9AYqN^nDK(YgA}dl}V7_M3gSe`RFdvH9y>aKO~?<6o5Y7am!G~u+0)p79s9m?B| zAXbnuH?926g}zkGrU>YIu;*wStJ|=wSh06T-y{Sxiv8Q0ApOr z8Kk9k`%}Qp(7w%D)rs+6H~;Ch`<}p^`rFog0Duexp8Cd2A`$rWU9qPfEhhgs<~Zdu zLvmWW_!RNU_3c*o4k%p{^gDicB(Wuo?{4y?%}U(hUdP~f6v!WUr}$mEn`mO}(6)6q za6-vdg`S`3cU;-%FnGhT3fLekrM*g~+*1bqbheIly&juBXLpm1QZ|wgYI(Y+N2Odo z%=Hje8AXUv5s{+u@#7F85z#UV`e~susW_QfIiDjfe@J#~Y+7S&nIL#nO%-XSBW7BW zsy7UB!23;XsOxG~@E&p6Q=pn@_qYF9Cqu9Y)<2Zf`U))3jD!!vP6 zZ!4INCn;WFyZAYeJTs1Rb~wqIM%B)O4UN+nWpaoVwrmaak3-oC!IevN4W*bN3F3C` zR;JV)WNUg^7X-)5`X~CMuF>y-rk4egNxbUo1l$*7R5xGJCSsAJX4J`K6&nqSzy5r z(_u$(^Mz4mP(1FDNtmM|UNx%q2Q4iow(EHlEPT`5zUf89e1vn1knWzeQG$Ru+q2rcyCxr}{s>%iE@`~Umx`qr*vS08=X(St|$!+&%5-iNG%|Lfob z4w#_2--Y|Uy`KYb_@BD+*_F$7KWX_Z%Za7yz}NpL{hVCekdEzIl1@;&b{*Kiv}eyX z`&ZT$NTl6d!QHt5b9a6dv$AA9_m)qHT3&R_HI&~deogMj=9}(`Q)=dH^bG9)>w-VKc&Tei@dzMQ;V((R*^fu z%G)=lchf|fHwkAVIU8$y@{t=)9Fu(fDUy5F7RXlKB)N5qvoqhlW4?d)`0?j^u2_I? zbB6DpMKY#;3%+~COyBkIzo!y z%HE{7>6~_?IM&}=8dcA)ks&zX6z zGjUF@f7Y1f8}^-v!IHx~<_E^8Kg6G!n+6FV`dzy1vAbow4?_HeJj(VLqr-&Y0ha!rjsH6;v2?H|Lk#dGrer1F=l$2b1GF~PK)uac%8#|yikHolqE(; zIA-Ap&8ILO84_BDEIR6pbJ~&P#5uii|Cr+o#ko0Nx>28XP7CDIZz}VSmD5fGf6VQ< zFBpGb|K+(Eu5Q%ETD*&M0*jLQUyE*1+-yv`^TvHL*w?=^Cb>R$^t(D4ECpfZ_M-LSLWvVkd4Zi=V^m|ffq8HJWm_!J2IUN_CHCEnLY)do9Sg6 z}AIucJ#wXW1tg2YoD}lX|KI^@19qKE`Xm{$?X2jZe{uB%P(49 zUV0g*@&BP+wasIJ-Pit4Iv=>3>)jgo|3w4J`s!W@lzZ8=$idOh#~a)D%#%mfF9V_; zdd++;yr2W^?iQF#)q#U{5}`sI!~R z?BUkbY%Ut}0k^)u27GQlcbLunfsC=)EU-DEi@8d~EZk&ub3Drhd#stSkavc51Ruu)IcK+@C9 znan01WgxlzW=jwx*EQFV07+v@)|{M?Lz*hehoThQCv!=@>Gehio1Al8XOw&R=Av@+hjTtPmKsdN&8xrr)pGNFP9^Bwplcht`}n+cWyZ|4)7olHFkiw zoRs}u7W8*FL$OReTve!%s%b?mn@1!bQ3_@Vhg$7$im~Pvb#~*KEk!gPF$czcfQsp8 zGuO(idWBA-Rz;P=Y*6$16* ztnb@=-u+TK8uJX!3!5Re?v5}QSADye8jQropv{rI4uzXxo(MpXza2A%wiz+f`h`H! z*n$^0IGfw|-_?Tew|Pek9DL0!XU2l>yZJB|IJufz)LAV!I(q~>wcz`V`2ZC!a7;Bf zpF5`T3xJHV<6GbeXYP4rJI8l#Aa86o3mn1BE$@`sOyd8SpR?<*zyBwD_kv&U{<&KN ze;^HjkjMHRA&=+Pv&=+Tk7HN2&1}eQ4ZOO15udVs=D`9^X#y};$m5Pj|KsSZowE*R z-RUG|%j)()L^FuldeY@nXMAoUkI^yM?<(YRJL2aW@|cW8&lm+39>xTbWYcyE?Q}+v z>cv3&@HXh+Oeo1@kja!Y*%oNwPL5*d9&D@*Ouf$0VpXbCazoO$+v%K=VSCkD%Q0d6 zL__aN;XIV>3^5vI<#SKui7^$L*2>l>Qs?u%QV==O)e{s+rMOn5 z7>k)&T|PnOi&8wMS4wy^TQOZlO2uO+R*rNyq|@=E{jMx^2tAqwl{bdG9xXs~g*;Af z9T@xiu8rz`i8{IaI&Eo@w}v~fu4b%#9K6~Oy3J{#SR@je_~bt5=VLZ4N0c0}a|d2F z5(%fan*ZgCBpHo?7l~D#e1Cxho9!pI1y;h_Pi&LekTI7(Kt5gD5{KikWK$EQF$G{| zrzYOM6!cHau1kV%Y>h)c*9j+-q0FE)rim*NnPoK(+;zT zrqzt#=QNZl4Jg^vj8^ePzr&}QY+FxhcIw3O`NEE4<69q(!95$)c*1$k{g%$}hH9H3 zxb0Fe`9E^REwC5dZp3ZfX8wT&-!??RP;}K6NIg4k+>M9H)^T@QQq1@k&UGM0lgY7@ z-ZuVULZNC8&K0=gP%8{15Q`%vpc9H`>wynDF&rs#Fh!>IYy&yxfyfqXR#NPUlIOUU z6X8b6pgFWin_3}m8g)F!YGu{-m7aJ4bdyS<hn{rk z@`E2gXdk@af%hMf4jkV9?*07!rG0PNNAA6S@2mF0d;WdT3-@GJzOk~ta>edjcVD~v zLCc?5?k!)q^noRL=?DP+9sJN6*RDwSFRyXUHqJzSs%Zu}M&cuQs67Z#ZhKU!!-1MK z=}x#99}YpSBMX>TG?5f;|I}5+J^tIsa^D`rzWsy$JaqA=-uR)9T>2*Zu?N2O$UhZs zqBov20p{ngNHqnP9=P$seLwh{4?X&epIZCnYj&CM`|2~V_{(SXs5gK3h3}N!@u3IN z8>0!ZA0jmy7R}IrHgGHo%JCR^rO(<)lIM`RNV&}xUx}7%(D!Z%{Go5X{*5X5m;WY( zpZmV+4j@mv^?Tp_Hha%h<=wZx{PFF#)^DLVh7(|~+Umx0kzzkm<&u2@6c$nWTEJB@ zIj+ z+vts80$dWA6p>@y4h2>%h*1r+JRd2P_yH2-}c;n zuXq_oePosUg7fnCDF5~4SKju>VC7M}*{{&_hCkIY9Zect*mIk3Cl=3lVaew*ipRx! z3K!$!W}_j3&eW|kiesBPzUHPsv;X7WKm6AJIjJ1E?ZVp+efeMBa^crr{SXSh`V;x< zF8m<9;Z1<;J{G8HubD4Zv}(&`ntn>ETm2!FN1I+H((y)-F6^eNWDK7Izv*+4^-uoh zf;apN@>lo$&C#zD*EzeW54`WcgbzGcIr@+P^k|pf7)*c{s3|rDuHKk@+4`?iti{@Z@bTi*33S(Pt%yPmn#dL#W2<694WU+1%5x^dT^@1r-| z2`~w&h4OkMW7<`b$@%?cRSkQIS{TjnT1z9OfV{X&HI!D_(pJ`^eDu zF6@8c#W!C3#dhaGPq_5P-3Na<2tSA3a3;VRBAY4alj(|C9z>f=gUX_1OXooEK02Fk z;jmft*^$gxR;4jz{L{}q`H@d}-{aap_~)O!_{w`;_xk)r_qY|h&l`5X{F3{9`e$!> z?a%0q-UL{Q@EI9pqh_fP_`Rm6OFeRsZZ$%wbhX1oQQ41GqlkuO9oKkACY_ zmw)c-4_y0)pa0)SzxQyYIe7O^UXXx4_QIR5e@24duqMC@wEvs}fAV90`N5Z(PyE~G zzwnh$#~<<3|Nh~9{{Eg<-14bkwr>0QfsdhAu=GZE0=!7W&xv|K@*7{p+ukZ#_W@_rB3v=?#4Xyg;AMDe%u;eeD}k|MGI?nWN@&_+76_e)pY^eCMYQ zzUzXYzL3JN`El}f^hRd_yg)jNUtasl-QW4j%fIxxLti8x z`lI*{fAGBfjpz++0=z);%_;B;K7G9XsY9R7YY%+;P1Nh3`KkNea_~|AwG`a&_|?Cr zs(Vi+=?!%Pyg(PtDe(8qPt^bW(=Ui#^lL5h=-5~8L1kZ7_`wx7Jg-6=J#_Oc?(sEx zLzw_C&^L1m{IBpEp04*UdCvoan-C&@^DTeBcJKo?2%Rh5`{R#(H2j?*xJ9pS$Tl@!pgla`N!`UzZN{jr62aydzb&>!#8R_y5~P#OK(UM;04-lPJyp9 zKliQwe%kZ*ee|Z+zh&itPk!-r7ahO;d#~AlJ}KYsklPwBmR`99auulaZQwzqzE@V+bQ4PgSjKr-JH_|K33;4go= z`|}n5X5zWmzT{(CzwnIz`1Rh=yA!X+-k zc=Ze3d*$svdC`5Z`sPEQQh(#^Z+q9p!;skhsB9&9%pOk--edu%l^Bz}bUIjnx zH_uvaKJohQ^WVqje()k3H=Zy7UZ4l&6gdB$wZFfNyP0&q&^q?Mx1V^~y~y{6 zk?y4*`rG4gjeR)$4;RxL*GzyH2<@5z-}3kCzt$g9I)2-ajMu#Q!hcm`7rp91m%h01 zgzLWgk-e{5A4chot0%w<#Bxo6?`;**uRHi7)4lZ_mps3+Y1Lh@PP{KWM)UjLZEFZI+#m3uMIOFx6&U?;!}1awV- zfAiGu?5}@&(tX^5ZpngP8y?kQOur{^`Xx z=l}gjAH8&6@w1n_BL9__KJb^j-@APC*B<@LqWeoE^qDWv8}tNtfiQ$A@H@U$eOIgf z=r8?C?b%nq_nv?Ei`>`V_Q8uIAN=JjpZd2afA&@S4fIB10=z)n!4&xGAHsh0<_~}J z<9pxq<(r|GKKNeGJ^t(qt{|%y?E5P9Cwq>3_ha-%eFD6|yTB>%!|(Cs>pcFO$^GIj zpE%ljX(|%``qNh$um0Hc%g_DO*t6eu5xr5H0?*$iateI>d$;1Rxc(ui`sdp97qHhf z55NCWH>~jG2R+q#ZSDukTR%W=R42d-(_|xbOeXfE5Gv2A3dab z!Bf8cnmzZt;i*?11pD*K^55<{_Pt}DJ9g8t*B|@SW6m-D|FQQbP_AoNfoN6PH4hgO zGKUP0n*e$EURi@}1xO-mu;oFrY+06+q>U`e@*r8ZC0Vu!AzY@%kU(f)@fjLOGX#D& z!|!~$7t;@rA0R&rpQa%nnyCq4=nx>JAqgS*r^+hcy>^wK>-8l(dQn*`>pq{o&z{cN z=jfb$_P&+9^|D(}JNww#ubjQ*EI7-bJ@51br$2l8O{bmH&pkam`Gb?UoqXkqdUEGv z8=Me)KR6@cj$eHAp`)Ka`qm@o=v7BgJN%==|8)4(hp#!jbGUWzYX{$RFgeH{Ja_;7 z`#-(^_4}=TbboK}J$pY0Y6oz8FWUX!?mKqBdAGCs^4*j8@5X;5elIS@UmE*x>|L>M zj}2n4ial-Ty*od)^Vp8M^Eule-~QF@w`}{{%=WXl{%rf#xBuhH@xtDVw%)h$0P)y>%XppUXJ_Q`t5bl%K>v+zqAf|Mc?mP2fZAEz4fkj&@0yYjdjq=*Q#4T zy$*WCmj7iP^olKid=2zUX_~EHTL-;j%l~&B^olKib{+JJE#JKkdc~GMzYcoEmOr%) zdc~H%unu~~mOr@;dc~GMvkrR2mOr)z8p4;NqPKo=9rTJV|J^$16!4R``R~_3 zuh{b2*Fmq?@;lZ+uju_9rVih z{<3w@E93j#I_MRBgK7t_-M)GSOybM>p05GUED!J58a^^Bqx%i(x?Z;A7q0_eHstHp z0WWLMSqHqVJYya3gSvz9-WuTavhF(TfR}aGeKhv!>rA+;jKeh@ryc}`@z<^cUY-H3 zSqHqVIdu)o%N_5p=@@-b7cgF52mIhLg7Mlq;0KifLDCvv zkY@}J?CHd(gc?wXEQ_AJag;AQ#fb->F*Bfk!K+3Dui0Y4}pjAz#X zLk~`E2qK;lySx3WWqmfD6-(~C3Rz}j>qi&AFN1Hrb@BVMN80+{#qSSt2tiwKS^U1t z%+|lT_&vF-*sZTx{JuO!w!VDv`?7AgzVw-~r?ETi>zuSBLUUl#G?%(YG!tS^3+PkmV{gn87<3AREZ0AioU$6u3>}|hi`vP9X)sNNfcdh>RFTecr)4=t64+L8t>Dz1eZt&6y1LobwukRa1S7zNu z)!4Tk z{kPXcBiOQ(+izM2z0CLa*Id6suDJPUujv_maJv9O+h4s7dU?OF{n$F_WsPjF`PM+t zA~inhabeXnRc zSO>j4-?#mB(91J;d$bOEStZ-!b8*oav5mP7dU;2(-CASGWk!zH^t^Jixh<@N zUa{nB)!D$M#dBxYL9a-Ot%F{X5?KemGNzz)&?|ONtbxv~ zXz;VwL9cM~l6BB4qx<$c=oP*{a}D&$G0OI5t%F|C_lws-uju3jYoJprnU>D{Z(p*_>gwx&+Qr zbP@D9R~!IOf?^=BG?UNUDaaCXOwfW+zuY6HyhsDwLc1(lQ*KU#C6S$%siD_`xlSwB zndpd+;wdXE6Bjh#3mW+C1j>yxng28u9g)h%#|52 z(Ge1g;Hs3+;A+_%olU~&I}rF^vSNAQ|3Z5^|iz0Y6atB30&c~m9 z`Xj(5<`dXtWiR#=vdN4WADT@@dJ-z^Sg6x@px75mxQ%|dUg zXBvrqwHjbUU+S{Yy<1Q%H&55eBB=b#lbhP)Xa3+ffK7}ivdOZidP>>k zVa*eoc7(p}83;O+#H`IZHYSS1Rr4fkh5Y0IXQCFv@gkP?e~(1`8JoEY}Q(Lmw* z-B!&i%rw$%wnNhzWk}vL^GX=HM4hh+gxn~UHnz#ze-7A0dm@`G?>wJUHhF04i8U_Q ztEd1Kszyy2l}R_HGs+e7gh;enqi|5_5%WP>V>F2Ia#lV|VXmjyJ=XL2dbXr=G7uq$ zuF4g8B1;wO_04V41UC7CC$h=%m)%p!CJ$>p$rHFhRdXWbLn70k$pc#`i&xDPDmCjf zGrFt_#e!cF=LLM$D^^HdQ~840nD%o5WN>1hNw#Pe6B+l4c|ux!w%U@XgL$F{C#+&3$d7d`Kep@? zMDl2(of;_4sDSaQtl+Y|(4YmPPBk{R$y@(Du*tuABAYCKmp!Fy^03wuTAK58Hea$? zP^g|jYrSkv&3RYYgeZ=TR-z-@ZI>wI(0RixP{N3|r?v5{7F2pfxz?fzEImz&f-CXG zT8X7O~Wclm&DP@y~wVu$pSckHzJon2wRd=d#HC2PI+6$8ttvD@Y zbiydQ3AJoFlFC;ycE?EwL8Gb22A8%u9!)7$S8%H|D}vmsqP+RO>uW!96#viQ`~SPQ z&c5{&J^tmR&f(7fx9{OQ`!Q$hyFt@G{og%f4;(YR@Vans>?u5nljVmsiSr#t%vH;B z606tyN!4@;e2KQMn8Z{j)6U3(gV9OK2WJ+1ZCdJ+)3DW5Q^~o=vMR>wXo5^4;Dh!b zrta3qpGeD3;iH`7Lp;jqD`I_Ktrwu0V^rB>N8*^wRa&n3)+`5UP6`=wi53q}(qu8s zBuf@Q=+Q`O+8ioei?3<{_#Ap%%O86pEkA{wU$XqLoL?@)TX`kWa>H5GW-xwWlUn|& zv7AlG`DT%#%?1Bp%MdU$>=%!0G) z^Rs7c(sDTa{EWGp!}C=q32U~LQyNUWTrV)f#TjzJ(P4Ygau~>v3pUFzZV7i-jPt_I z%$#+`y>VgM%rIb&>?B|&6<~VPDpRd`i6pIm@E1>i0(Y@;n*S8qD?tzKESJj4qFfE6 zMtRhrr=^lyozX=r8`j-csuLK41PY1U+-HfVRe02^2ZxI*BA021zV z7k~FeT7C+j&O-dd`kJO7s6Mw@sa|bxb9LOZ@fmMlwe*p*M8~Te-LxR--42xqnHrYM zWfCS?16zu5KRrVV2;~klN#uSlfAop8{1iTKg&x{@D=o<)ViQ`|1qJ4 zv1~aA+v#SDZVPfcEEdI-XA=^u%Y3&6BPt15ISkc{<4`T%zw~|hiM0F_K0$@JhqhVH zHjFx_Dc$m@%BbU7-44_`dxgg!iUiVUoBULS!)(@s3~z#~Qc0pELLggea^_6SbSmBC z?ZW+9{>T$)`6+xR3ULo>8<*!Go-sVwt|*FaOO&{{O9a zY#qP(;5qTH2mknI`1h6h$K>;5qHp~4?T1@nX(^fLb$R2H$W_TiZ;%e-JcZ?r^2}er zZ(cR_>h!%g$T0Hcl8JtjvT$FWOtd?gbp54lqE~0}eQ4Q4@icT!;mT~Hb*o2;ZJsZ6 zW?02yWG;shHqsGig(*2|sa;2H*rYSV@G1MGvx(wkrVCMI1q70Wxj^O|ay<2tig~v& zWz`%$MMU4d%TGFw_A{hFw!^;L$`tQTlWiGOZQsi2GMpPLS^BP)#wrHWhYQ+}vNp;i zABjFThqealL^UsTdY)eSCgrUGrSrtVjmyNqHyEG~Efakb{%1LtZ;;gIpW$2vscb&U ziGefe>k|WW9y^>`2vQn7+73I@$yCoyIylGrM4KAu*}J4WByuLw$lV2V2lxZ8YYj9% z7@OWjNe$c8HM`?_{>w8N2)g*mG++z^q1alJHwgE9+8{VN(|ba1D5fwEpGt6>a?3Nj zH}0v%qjsKakNUa{fW};ESJa&{gy921Q{Oo7w-_&FKwtzFoDT-CwWnpJ*lpo;hB4{A=Q)S8CaTDVKzx^CjZ~ zw`rJdEwIlEF+t!ra{`JoEjo_o9^3#gFGaF8Enua=( zP=H|${SNq#^G~jSOm_o8=ML@49eZZp^|DxC57|e((mYkqCoK+~Sag_?01qCmIwjW< zGcMRU8(@BePM*v%k6S$uzna3uyT!uTokBAR%ZiWoDj=4e9*IL5D;0-n(Jd=zJ?CM3 z1`V<(HOWB3ik-%1&RsH%IjAvZ+jWji=|YAwk_}?B9eWyx4&2T8`~TPOeE(K_7MJ2k z{F$-ejD1V&OJdpBOJYYme|+nAZoU21mw;@5kAe&UzjF4rvp1jlXV~fAoc`YFcb$GI z$OcHCK5}||^6rx#KKaIz(Fu2wKK}6WZy&$?_&bj0$NKT@$G>ss9XsE@^JP2Q&Li7@ zxc%)Y`5Gqygy^#@0l^P>zPU+h1uIn&twfi;c)()3i(!1H%^kkg$wbwz>I zcUweKi{a=<)hHpUqGIE_##MF4{yYNt)9XRrAF2CKB9Qyk{ofZ&@$5=doQ}o;5jEX! zi8!?oI{vDtajhj*9*oDyg(c#zjL;XBh-ahG^@`}{RdM6QOLliY`?GiApC5r-ES{$K z;D=uPRZ&NucYQ~%j3ocu2;_04#9#5k-5neZ@|Qfmk|Ex%++Yx#s(`QD*^E0BKuO6N+jl{a#KwlyGOCqse z9CdV`Shu6$`JzkGOT+UsBC%f>wLc*CBazrIh#J?6y&M}Be{m%Cr(e=u49^!uVn09n z`D(FW7>WJ72kxVC1<9eA^+{mp+ zrsD`blIbjB_$d1M3YktLki+XiP9m8OB9QxJI*w%8kD4Bk=_rzEFKS#b(~288jAYu4 z&?A`+B8KD9&sWH_AA!WK2ic2c+KE8!lW8}SX*+6qK&E&k(^k~DUZ&+J-FR#vQ|uEF zdL+}%qSx5RH|aIG8~_#Hj?n-9$)Rs81de?yLVsX)`H6)tV}BPluBtos@r5p9AB{jB z*JbSQqpm*^HQlev*vBHaKO8l#)#XYezS!SIy8PP+eWA}uRHv@y=Q=b{L9}{Py^sl_%)Y@!Z)bu z@?yGO&>J12#}U<5kXOc|`cg9MrOTM}v-hjYr6#W|P<(@GEfzqEA)*zDA)PfUNP0wy zk0N2UAWyr4vE$XS%?^bxL_fd#ojmsHi@V?1_U#+YxrgU&uB^^{6L)idwR&-H@DH~p zuYTnAO*>d}AB+9MKat~D5cfLW;ko-c-}0O@Vst$aa5IZ8f*v{l3`@XJ5(l?Ba5I@n zfQ``UB>Oa`donz1V(M_uK?ncEXRz(cp3-j zS6;`cJs8x3Ei>*QFchSV)^b(FOuGC~4ea@}pthmAg2Tv!PCxmXH_$!Q>$fC%l&{Nn zDIncK!LN&W6;jO}GO!v#sV5;41>;@tE_ss5v7r39E%&NfFAoc&d8H;e8Y|9&G+7qI zV8oQ<3?Xb1b#Wm&n0a5`eM~){d0*AszR7TXcry=Mz5TsnE}j3hG&>gC;eVppaUMI- zoPhh*u6usf_{hbGzu?Hd*>ZjVq&Tk({*QO}h{7;_e!F?)U{50hGN0DNZVsJLZcU(D zdKy!CecUKv8L}+-RhCRK=&X&_pY+`$4Z;q6GefXk5exG6oGukJQ|wW*HPppUDI1LP zft8)Ht_w-th@0|CUT^am-zqCQ9a7fV8mH1YpA)*7RBiBRH?m;M{W{^ao+JKKa%$bMF_77 zmT*kZ(=2m&?tYRYjvUW=+?Dv|)IaB=D|P`#_u8Ki1@jj$y;a6mulIhbot%#h6C-FE6xyHl@p zkkPErF~fG!Y~Tf>T5Fq7Z$yBJ-&Q9x7D}aESn!IOw&x=XHGLGQ$98II%uQ(-9Zb}0 zUh}gX-T$NKqMzUY??_v@t#@v1zb1BX`(;}n-hEp9_v1ele_gx~fA+23t-bwEoDEOk z8hhL6G{Ox+G^cvXoFUXZK`xQZ1lzKcoLo@FEAR75G1i`6V0MIN%oCdMJfoY&osGi zhE1SOGMRy9e9cS@S$FT{5r!{juu!aMeI2P6XGm=%hI}v6hBB(1tu*2BAR(|)X)srI z-?6}`S85$W1&QSnf^G3LDeKIwPIY2clol^>Rm4=)yqs5Wxn> z1Z7cgt3$^(`sm?X7Z{U5Z&EK{RgVj6w3`Y!e1c23B-Uyshu7UXGD-$Df(nP_!*5*3 zF@ooOt6H{mc5yZnz-w5$45;ZdlJTjKoobuc$p4aob!N2z1#y{nCYY8$u`OwgP%KM9P(1bskEY zmXhul(QesDw8N>-DpjuJceU^oU0^sA0ra1ux>7bVCY-s;@%?~jCg{L6#t9XHl`_~w z1d=M8K7GN}NH6+xr;p)wfo)XNsbQ1MmJ0+;!@6G{RC0KxgfKAgTJ@9fT+q;bqa#k> zaxc#b64`R(A|_izvNXuH+d8Y5?W!mZJSuCU@!SGK9ov~X+34gf&4j~lzUu3iufj@= z@6(o)38BZqd>0fo^M25Yk}ga$~syYN1;muAg5nbb04dft^Xkd_$+ zMN}}@OaVshc6&%xX(3_NvGU&0g2pHX%&aq}S_WAc(#=AhRzSkbVJnR`$XvUqm@pxh zIeOG_cYb4mG2}WW!cBF=LOH1hwG1*2q^t`#3u=vAXH%cgBPis>We*|r4n*mBgWkXp9;6$ zw!mnoD{~Rin*+R!NKzmrHPc{NzG#qK4-IENH0xsBiBPqOoj+P&=<{4|%yPYUDinM^ z-3g%*c=w+PC*hz{>LxfTtd@hZW(?{Fp9aOu?c1^*65@O!r1Mk0%#7Oo31_wq!)@Sj ztJz5hOaf}@XsOKV<r!omK6 zE4AU;F5Iz*hOS^%V<6ELqcAOXvwb5?v+Wi#CP!LXZ^-5S?^$4g=l*QI+GPe!kt*lg zyj{W4Sa+;Sb335Dy3m5yeyu`#vc4BNxEo-6>Vk$;@QrlBpS3fil7|f&N{3gx4rk;c zv{LUgrdJpcA%&}LW$WKAFvOtO>b8eQvL_oF0ZqeFcR0)%Q!i0KI%uvsscZh2sq`6d z`?nVuLf>$CT56eYJ;=;P$-Z6>^Ssw6$ZZG*gPo~r1#Bj?(on1sVG!W4Mls}vAlXS9 zZ_dd9kw{M+knyQa_sf2s8z#b#=AD+l`r=gwmy$4$u%cv) zYf8>mBqUj_Vdbs&Eig)bN%7j1E{oSX!=8zIe9dl5OO5FaPq$%w4uV~4{a&d}TRZQH zFc8*E4-&dEVLEv_lYpt_q~T8rG+V>+S{Ffb1k<8H;^O-LFD<4=F|37jGH=x&nzwC} zpu`r%xGZ9jX?SksGP9D*zd#n^VA{f%XL=@*2_QVALk!o71#XmJ%#`Ze0!Rp} zQIwXmDSGGC3k+JPtNjccNN9I3Zw7P=c)$j0`ePfR(%_vu=4S}KSnndr-WwJeWEpSN zk&Iu2S`;;r^zyhk85`N2K2&4}1LMCT^CmAVYI!Rfh*=*OQF$bICM{tk-%n{hN)nnp z)$962Wi&AKLb9I^S(1)NJ~RbE4GjS=Z;G9|#t8hZ-_ZI)o#*-)F;hyCf;gx0dC&G}iK|YP3qr^N* z6H>S07srKuQy}ZG!yCgs-lNBY-{D1K@AnoMNXP7sDL7Hi3}&@yEjgj`vdKziU7ePZ z`l#osp{b0u!U)~h7Z^#!%8HVMLH!}veHyh6H7OeEm`o~poEi=%nMPQtR5Gjy#fl3I z$fFcumQ9kqL~=Ch8iR?FxBIm?mVs+mjorp4B@wIs=0} zfR>^BuCV({3yg5?)CY}puHaI6t1#B{DO$}`Xif4OgSs#@Ek8f&V+f+K`w@+yj0}f9 z*DR@mF`t-Pt~NI(TB0_FQo~7~>iP!WgG+dzXLkSdg2o`91!+D*cLd?kXfBgz$Lf=# z5Sy!PhA0GJ$w@NP8rCV(`}6|C5tTN{4U1#jX|+?%%xgC%j*bmT&X#pdX6GW`^05%4 zJdXe60;4Z?CK;?*ZU|~JF=j_o+faMypfb+Z?QD6*cYVG=nUdzAagYXS%lOCMVP9}1 zlde_DrR*~1=1hFjtH?EqH27Xtg>$s8DakSFcBN#!G)9leg&aL2$yQP-U!3zIRxlHz zYSM;4ew|6eA)LCwQsv5sD%A=udi41V8tqKi&l`kQ9y&Ih403~ES1!7JOY0Rz*@-Za zC>k1c>S^0Mik87qOL1r#L&swt_>5M)>393{gf8a_R%yf$;3T}6=AA4FzE{!g z=MhL`XGV$>M)H(IB{)^78t&0zMu81&qR7~@0a>4g>na=j5x|{5p z>3%BTpktr5;K~&nUPzB-^A0z$p^i=}Wn~KY3VM^s8+J&tddjg=DY@^S^cGy@Wtr^Y z7Cf+%F35`?a@i7$gTw%4J7`%Xp5ZfN0=Sb2fgayG+No}(w%)RJdiUh7Pk!=bcJhkj z-#dQ8@r#e%d-PpL!r?~`f9~)NhxEbw4u0TZeDJvkTl+t^|Au{f?|pkey65j{g?P8j_*8gdwZMO{>iPkY=3clC-#5EzAW~V zop!t5?7kG z$&Ou`FiNLicw&3)z4B6_8zQf>7m`|zSKoWZr9wCC{&9uwz3Nh-8=|f%bdR`H=!RV^ z6uS5M*sk!B^XaBlLqRn-!tTx{)%>vMU9C{x9d`BjFW|dE;zn3b%k$lV8UMpeC2dF# zyk-O!Y)wtKlGgycaN<*4Pb(Ui=p?ht7tS@1N@NUqyF?&$t2Y^Q-ar zyD!Jvh7{M0HyB@uhkVBkRr&aMi@)=7ylu$Y>UfL4>xSyQKSVJ8D>r1_9O&cO&lZXy25mr|->xu{Tbp=#HPJ%PJ!8*u-i+5MAC`)`Q6&i<># zH+TQ|Qi&UO|G52k|Mb%S8=|h-fA`NW6}n*;3p?z-PXb#H0{ELJLeuUznqdaRJHF}p zcP?T=e89#c#Y@w0LAIBH>o@g|Hn7ay_j91z=x**F_Eg0Mzw8a zHp_{;am}^>a5grYLv8HOnu{03*v_Y44xJ7AxPR#EJm+%gY>2#W=pQzj?{eU8*u`St?>y^s&-dKhU`Vxe zm6NiXUFg~*?HS{%7ux%W!|XL+PyFi5ao3H8l^2Lp@J)1hu|K}lO_%q=u;Z$uUPtfE zTXX$t4p((^{L)LyY}m!)mN|aL<#gE)^?v;vf7<1M+You3g3>E5500PyP`h8QCwTnq z8;XAaQhNL>V7k{|`+4(^-f%e?HdOp#G8{kcJpbP#TR*mS>#tKF z)2$|jAV1Mcck*4iRwU-A?9rkWoas9H?sPLBEFW;GqmM1_om$8LPO>)#(W!4F0F_nTBbRXG>?(*Zr zh%TNjq#1cKM$5KYN)vgy);q26I4q~!?TB7vO&7J5#LJ9JLI&Kt{9^Uq*~|p z1$1Au3Eh>IFrq6=lVu&h%N6ty!Su}%rfKyCuabs{utuSh;rYC5PqAF4(h>)X=gfMw zRu|CyuHfDaH=?`z=@HTO@H-rfQ?q$4Tf^q{AjlJzaEGmQz>^QWB95Vcq12fBcBMPb zV`WqDYW0o@=zeE#?*$vtT|N?t=(afxv&HgU?4+zQQ4XjKxRea^P~!bExFu3jSQ~Cg zf~AydB_;q=WRsy8h>SBQ_k&pjd9qO?=V?5$GgKxz;(SM zK4M!vaBFmjG>B?w)UdQF)fudeRYwcD&)tabHU6qIQ4O(@k_jlx^*}JhWKaK8PDLoLzRRuVXc4!BU1>I+FME4qhHJHG*Zj>|@+~PAWzg(*3h`xzh z{W)b?6OfEsP-%EH2;f@Dnzflh7YZ`xbo0TzXKh6H8h_OrrhBAGfIPn-f!U-{*KtB& zK)_*Fs(M+gHC96quBWDu?44}wu9V4+rXJ)h=st53y2$cQCmPRbyX!fXsyXYAF&{>2 zO3{S4pwcnxYCQ=Kps-q)?ck}BlkO?iy#Bnva}Zrcr5#rE(?R!t+XhVBHZrqFx>)*)mU;>m3j(guD6d z2(il9Nl~1iGY36#?^7RSZrOXC0|oa!Wh3U6aifs}qb@;E4qolebNUP}!vRw8?u-;( zOLkh^^bQ3NxcMDZ$br&eEZkMfj6S>r=)NqtcWWcM*Lc3ZKO7E5fhg*Ckvp~l?u{|B zU^+EOoyfL85QNvRF>bcVj)pyrmO<2A7dl_b0o}8W=w9RbD30rbV$&bB(R9EMMtuccc#*1PO(EW_y-swhkukm~aj}{7S8L{M4Va#Au zt%AEr(<@CmQ_q=xtyZg3MXpeq!xj&TYL1_U!ug5{=$>pu_ZrU!^JQpWsS+t7HRCa* zE4un!HJHsCrA*7lNH6%Pd zdg*Qo&O&oD9No!g3Wa>H<)rUQb1p-x$$o8|O@hY`bE}_m&j&rAd$bANm3r6Fc&^?Z zYFOZRiMko+9yb`5aJkFh@f9&Z^#;<*kIEG-Q@_(C`Ykjp@iZ~+7XjU);NIazbg%Jz zS=)g@5eODWbFD$CG#yP$sSVNl*hsd9Q0UQSR_BWz? zjZ<^-{2eGm)&)9SOpmx+OR{h)@L(!W(qK~q$Tg(oq+RexlqphoYD2zJI@fb7xVN_v z-D~`nmmWI?Hcq2idZ>@6mJB{-%EkJj7 zBf8i4tEx!|b}6T09GMK{@_dF??&2VvDNTb9zq_+kC^6s_TRn-S8e%&|rSJ4XAku5U z@z4K0xEJ4u?lt}jk+?L64m_jKofmm!RCMpSGT#DmK>}174!Bv%fJ>x2XFL*8l)JFU z(%lQX=lTE0?Pu=9DyN^=6>imI&pONO{O->8?$}2^2vV-S@l-s0-pPME`GJ$+_G2Ks z-zRpz`uLrPAHVhd<2N3Q+aKNjnZw%Qa}WOD;H?Ml?z6!O!Qbz{WB+URtNYK{`~AJ| z-y0l_&PHc{c>MhM@5R3_K8RznkHvoO*3ZYjdJntxrrnS2{@mFQ9{%5lUwf23`oq(A zZu8sEy!rrP_hVb*t6ToDe$B+q=j`0RL9hnMPz__L*epWaJZ^e{82_~eq6DVkFwh)T z@tG0t7Rs^tRYKg4k}|E zmn+rek=JX@YgR9Gs~~dng4OtkE)nx$$p`O2GiBKu81SH%lv@2>h0Ho>K0nKwD4q4K zP7_O(7l=D=i4eid2RejUB&!P~>i6hsiD$s$3y-T$K__)J*I_j=RYDr*0x|ZB5u#vC zVXbCqjlOU3e1B+g$6ci*mJO0~1oHk3k?Vd2Y-lm4f+tCTB=9XQEbhJ(AIm5KC~^ z<3O#La!|0ZM9peOjF(d0x_Q5U0^mUov!Bgy;*st@H$;K zw4RRkQ7he}8eWAb4YzAa>9CQeBa-{y79n!e>|m<4%Dq~tT24({p$fu@I^CY3giIpm z!Tm(Gh4qPv;4Vn+Z$*gZsf;S+YTzqS8s^6pmrd6o&vq(qvqrO~qtBZT1FbTcCoj zqWK{B_k1GnBqBseWkGpCjpT9z@c5=O4cp^de!!GG6%<=*J&!BE+wV5I29>v_RbN zMTps?>vKqvAW#=)6Pz~cs0}2c%Goq4_v%x!tEZEy*E4g`P>p|Agh*h`HUR=*>m!KE zH({r52Ti_gYHG1gCh@t*HJPEv`g|uc$DSM^rc1dYC@0-v45KrKtU0fH-3)jKA&I4t zKP`Z`z!BS2_{lUfN9^+=#8fR)E6fvJ;k1PbhL@@NnB5{9N-jNs`knLI@MZ&w zWPH{oVlF>y*VtB$H?q>Oh-90i9LG6xFFBLz&9vi9GhEG^P0N=a;CT@u1`6I$Fg*j9 zMGMJ74rh*-O3rsnHeZq9RwAKtl#R^EXaMXJ5h6^pOo!$bM=E!-JeFvqnUUYkE0#;a zda6=WxlV`=yOaK8Etfnk2$(Jtu_osTIli z^$}uH>vmbu_exY31S5O>P8Y>B1>D_Y%1}qfdIrsV#Ufu$UOIrUj1UufIbWUSjdVD! zR_bHX(#x_kt3tY4_SG7c?}M-MIc2k@$c{VTy+D-0#2Au+B&e3x-A*y(l?s`Wmg|FB zCx#%Hs^MlFq?Rr-5#rX5M~GuRm#d}FAfq>_f-W{nB03u>Wv>QSpb@55oF-D!iY8r+ z?6{kW5Jxqy!#9V%+L`t{{*XfCieJlWsalf82BT)pMv-~JPRSq(3 zbfuA1U1=n6gGQ-L>KF66c{#|R@>1EAenl%!8a#jc4As%4y&M5hm-sg zJQ+=FT274YdZOTf;Qd-OJN6rwtZw`gYr*QydoLFqlD7DArQi;$bS4j8Ur>E1O*TVm z$Vw(xnG@rD0i~v1=5iAddvm1LT21QqhiRlM5n`{^oVT+RyOSy4S%WD!ML3I-K`sT+ ztxhE5(SL||RX2WTw%|4X>S(p1`3Z*`bIKVGvM8U#Wh?>8ca|IaoEmUNOAN@=xQz$1 zPBa28M~+R-M|iq2U@(xzWmHY&1Xb*c#UeDY2t*d!X}dn?c>V@q$PW>}!9xq&{PbWGqg@fwHoTT`%a|P_|7F_l7JI+Ly&}IF{dSUm1$`utvVLSv6t~DO(xTB zrl zyiWMVNg7YI^Y!Ea_a^=Eq@2Yge;j{of!Nsm@#Vt)N3V-0mg)h_`*Un!vIQAcnQ=Cd zXKW{zaH|5vdh!fQB!?A=&an%`y|0cCIp0VXa!Pl=+d;8yKseQ@v}bP7mD=-iUCafE zxse4o2ldDtd-I4@GEcI-tf&QsFlmT`VMs#Q#BF*igVOykhFZmm*3X*s@ctf;I(jKv@9|T1|;bDh{QB$4THY@?!IN`lk_M$yUK~Px?hdbwSlH zLYsp3sRV_NGgNw-nMz&`>$42F$ckh<__B!A%^w3VD4sjT8jK$Wo) z8oFV%@k}Bqk>fPfo#|5D>vf1|K|Xuo(Q##~wDsZb;}>kdY5VO*e}426M_;vgxxfF3 z#oPVWm;2iu&p!F0*!yB{i`|P= zZvArX1&eq6$8Xz__P^~Qc`I{k@9fvl{w+8oc;(sY>2IGDPk-Pv1T_Voe)0z=KYaYh z$MKaptD)H%_9ruA65JIQpqba@bP4 zjFl#h8afa|bj;iS&_E3ysmho!siZ5TTHh?to^|ve7Z{soS2*=AFqFbnRV@@e?h<6J zGeL$3*DjUxfKJyPqSfQnCPLOKqftxH5Ag^?Z9%?8LYiZd@`xe3MMR&r`!zfz8<1Xt z`}Ra1`+Nb1_Mg4LU}vZ#P7;jVthIW5jSfo5tU-Y@`*Ns+gHgIk7iSRe*G>8WT43bV zY&&B@HBV{yCB?^87*{6qQo|UzBxXYWWJEWcw&e?*q0Naz>@yZL@M;mp zI}@*0G5qSl6{<|f9N;FM2#Enrb**s$7X@&T)XD69=>j7YltA^@+5~bPY8J|zI`u82 z6OP?dQ7d%3vQr)4fib|Q_1)(zFw$6cFeBK!l_-)9L81eG!jJs{4*X@E9!edUOJF)b z5T$T;@lIPoGz(VjNrIk(Ys2oq0FON(6CwP{7$+TEw#Q?$Mu`rRgZ54@XvlL=Pc5J- z#Y_s4**p#fX;3N+@RH7$!#avrThst7x~-Og?zAHeOic6`5uD!iEs$_)+H`|*w?U1^ z#6W3J$HPn~=lT_pETzBozb!C8B>jM~{EAWG<-)vCV2!jiE6lUv2n0o>Lbx>)lk*f} zPs`i?=K^E%{1`{CUtnzh{(pZlKV*ON_y2#gpmBru|G&7v*!=x}3|?St{{BDqq6Nn0 z@BjCoxxm=`{r}PK0^Q9?(YTPaqb7+z+jfT3@=aubfLY0%YVmF(d3=+b0MrpDzn~FsRMA%^S!oi{!?8vcjmjqL5HfB1bkQo*xa{4H z9ys41$3Qd?Z;)f)rx$W;o?~DqdN^P0LdixJ1BN;y1L}nIRV?3vY?$a}?Onm*#k>$D|3x zA=R2k!!~PYrmE{^tRbeHePnBaVuN&j;4lHCoEYVGKO?EGg-n#8J;di(j||FQR&Q3U zN}Cc6zJGy%4zrD1t)`pwY&07v0+Y<~?Gn~YbhR!6(h&B7X1bN&n;LYsSOyhU&m~fR zX~KtX5^k{Kq$pQ0BUeq%&^`y}pk$ebC-t05WX=}Lxsnm30x^SZn;VIv5hM0d*+49P z!qJ!kq8dRJ)DWHY$*kv|Aq%d$79 zA&z4U3^NE)`CQ5NQhnPSrwZh(Vz?cylJIp>2LVT7uRCXXS*xPQi?jqFIhR#S@|=nc zdAgexYzIT7c3DEWW(7@-871)YTH7s6`u*eW1r5#1fCxRO4l-m^V8kf1ZQNA5I?~g| z15<`l1Gb9xNv9H`JO6co(QK6)a40mLDcLa9WI53a5`x#77e#8?Nr8C_70}tNsHENa z>mrOo#gZA7Eu& zLDJ%48jOmGr`qEpmJvx(Wqlaxg!R);L>N7!-H->xepj$b%WkJRGrZerS$q)|I?Aw~ z83_#sDRyZ0@UJ6`?ttjqnfx@Jtxbl$d-47zu)z>vHk zkNO}A8gC~1N*?9gX|@A`%d(A`G8jz8%?UHf*xqDJY<=6stX0GW2o%8Zrkm4+IyrJg zv61V`$+?B(3qu8I5^cq#Qbx7oEi!5-m9|Be=IJ^NHGQ#Fztld zsEIa4vLqWr7fe;)3Q?Jfy4*slEbLcHb}c{jN4&w!y?;U(4Ce_`7$|X>v*96(y1XcZbaWsHISlINppRX)tb5f zl%H2rnqf$X?ggdMtWu{@9wgDxXb#$h0qMpJJY*BpZ_L0$Hi3A-fKx<~nq3AL;<}qa z9A-3_=|rNfI)>RHakWxt(olv^RT#v5*e0mmn1P3Eg6fSKc*rIYE*Q8$rw`i%l^Zkg zkWIi}FmQvp{jfbDcfo*=LuzOXbhALz)qaI*RKp}b@x64R zE#uQ+$by`bS~~@a-pI%%Cp7V}O;ETo0}t5*?2Q?C$R@~NFmQuTAGQf{H)h}=n}E4s z;0AO1_CscXzF=T35WK*(xmI#gs_DWw#W!2oAz@?Pf``|~Q>&bxHY|>Z3*p19L-P7J zQ#WSd2Gv8JxE#al-%Q?^frnc7>Gf~UUNBG{Tm7JxRVVfTpS?E^b7ej21AFg2J7DmR z@wFS9%dz@Ym8zsF12!d9skD`(TC|jmk)_(DS|pW9CCT=^xLJ;lG0P>8A*=&|WXvRl zVFqRb1jfXfn4JtfgogkDvm5ZR#R1|Z^OdTv@44N5yH&?+`})c4KlGf_t@nL@-|~L@ z`~HXp%+R%I37^`TVr|;bJSEY1Z%#=Gh!AY%i#;wJdg*Z{7$3EsQ{5cKSP zhXJq3=2LKjFxRXMxX7Z|t|PFjm20RbF=jF&Ca;Qxt8Rif#RhQ2P4FeL0bFqteDMlE z45e4y1YZ;zz?C*Zxc}eT{GpAT%Y*;Ei-Lc=@bkQ`0l=B^wr4m~phbr>)k=cTkkvL! zgQuEIp+V?${y;OP#x#0A$oUgc-Pf6!c0Y1!`k_>4%&f`o$;PW_BVt7=jI1bs- z>iaQh$gXGx_rOs*TZ z2qa-u@ziMA2SJ2%p$;#^E~csmZNYWSTTP^yiS+b?`jD9xH&w}0Rotc!VF5P z@+_{RkVWz=SJAUr^+H6CN?wHFSjg9Ut|K;zIBUt7yO0b`Jrn`4!w!OsDsZ}vmS=1w z+Z2*a-@z7U4fK4V#4~lLI{^=GJixm2_Saah+_k_9>d6~%9D-KAQWXTlVB(G4PE{e@ zkekV|0}~P|ciYr(CXN>qCzm6#rS4z`qs6A10hjzWTGQN=DQ6w<2kbv%K+QQcy#_43CEAKA9Japyv zyks+u=;dAQoZ0j|SB8mCOIMDuW%)F8W$>tWe9DzWNBac2GMYw1VzYByd5D9zYgx$f z5x-9vli75fVYMcDj~8%lP-06mfkRDS6aWvxBfGK%vgA1Ss8#Vn+IxqniU=jv!0Qop zNHK)k7(2ZtY{6!s1hS8pHQBfu$Pl@(L5g=MsdBk78l)>qA;88WC#Tq{agPBvnClT= zhD8UC_%)N|TSG_uT7UB}M(?^hN5nrZ9Wl-_`yA5FXVU(CissUf0SG6Qq2IGi`B_sa z`U&vz>IUQk-uGJMxZ8h3LdY7{|abJ@=4XMhe1q*K!ycx&Y71vP^FC{rj% zFH@cAy?HB}n;WJyFK5t9)kx+vi*!o}U!LaS21pckcd@`~=|)nWx6J|C^CTFuVR|eG z<6u4*q>CjedVI|6h3je*r7u3-A9w&ENn3^(W)~e-wig=l%cNqGe)? z_y37V;)VDBpOE+eQKXgx((S(R{{MfJ_y1x3zgrvT#_eCY^=&ux8|d}TgJ0bL_B~@4 z+u7RsrOodEf4Z8VV|eGQFM1i79K<7fT!S}NnSVkK2R`_=c(m5vQ z_Yic#_Q_clz-qQ;8)tg;{j!1_I}EQVeWsTe)Rn94~mRi*TRC090(m5n@)$xL0Z z2^m8|DwdpNLtAsUxv~XxWBs-eVrV0B1Iu&vuu-6Z3rCAg73B+@LdEmlzd~} z7BaJZ-^AfWQnZqFc_FkGMI9SyAWA+1C;bA~(+d3DP6h%+>F&n=_dKk!{uNbOPQfa< z_N1QYb5%i_^(-1LH_ofFY@p&V4?6+s%YSvMu;q;q;1ndtRKfm8023n$&pb)lTc==lr< z*lD{~zu--(^-NhZaEJs^N*jOvJghS6imEJ65*)=dA~b=-++f~vERGe=>t$4GKFG~! zw#}7_ld`xd67znkmX*8hYO&p%4Hzyr5wesz5(=cd@n_G!Dr2u|EX%c9jX4If4W%OH zb8wGtV`9!drb%jm|UN@wzAt1xNwNmS@(m^R~`Q)Ognr1TO-%hFu3$e@d+TLc;6Xlo{n=Yn7B zldRCAiUoR>7D1JL=y_OWS2dQ=gjk0%?bc$#%2eIvTYO4@$aAVJJMETcMO-G8lACC= zy4`43Yq(|D3C?eJq?S@iTa_xB0=ex#mHo-{u*$A#EMqKGW^%Lu8DK24hP^z`HJkc5 zRhGfBwJspNhPv~7QdK0&>QT}-56^}fD;r?`Xi)CfDWXDEX|Txna$2lr`o$90 zz&R^maHm?z*nsf9iaC^L0LT2p=U+bP zKk2t!a>)3#l7lCeyj#=T{s8hdD4-XEcG1c3`GpD=8GrCRtg@?`G4oa+m{3Zr6N#>c zRazjjPM{0tRhcMus-qfTqZ@{;M)JaLnI-9#+{^ zjb+((qh%S%0<5HZ2=7%uFnG0p-nxv;FSw?cmov4-hhWq=~{Dof#v zZ};ZO3JHe)-;3V=|M14G7hU^DyI;8Z!&kG}e?iM9t_HwMDdp*xQZd3vFON+<^<*)8 zkapgaMFczVrBsY)%J8XCoOr_3_hZB=p8wuI4!~G1r7n$n4)J@JzB)clpb|>St}{`c z$?E;>r}m{32Bop^IrF(MrN;Hyy(#Y0`U8{Tan5Yl^HMLD>K6xj{qBUa@TQi=8zhq= zy+?kbSVStRF*3{EkE4{9ZAWMJq}YhFvL_ZTu*=hgqJ=-{Q)a2&R|%vku>^UA$a_srkU4JZHxpZSK|bzH{;Z}K}` z%j|}>9@gERb|>BO>X#wL2+t{yQPjaRz*q9UuJg32fhiB3{x^91+?B)9t-ch|sDuBl zDnL<(69fDw2@HJ#A4C(WRA}Mc;YNoVxvo5(oY-bw`FIaeHnyo_V2DM(0QCRfwLG*OlTTSzG@F4mslW?p%%_l$J z;U=6#S6$>|4)T_G^Zq&0aKsn>DH=E;47Me`H9cJCiK(<0>K1u zV59@%a2kRV2$BlF8aZmmA6q~H;9RmocYWHU!l1fKJZkOn3aEP2s3HP}pEJB~Fp$PI z#2yYV^iYH`CAWBw8|C|Lf^1}4y%uKz zvL**Jtt6_d6S3Z#b7iwaahfupi(bV?7bRJuCN|k(AkOyl(yUO2(;hsTHA#9ZfE0n` zdZ7qm(ShfEwYltup7)@$8E2V#brU^u*7MHsrE|O^B6a^1dB zS>3G~5^fV!`jMxZOs<_C(mmy_%^Rgzj>mI6x^FNwV^?(xUfoz!ELYqsezsmzC&I@>OF)#9lX zQ76l#)iOu*o*jpH*jS$#!(HOQ=$mh;JJ{Mr&#uD`*>R_Alax~K=UcYMCQD^1*&`gE z?q_BTKb3ToizX%7KS2@>bHBXmWyiSBEctXnRgKpBK?$r&pGgk=n0z>`B%YM`>UYKCyW8@mO>detXWgCvr2_rDt{51?K?<_=?()CK(CoLfk@wJM)@mf|^s|bcF?s z)U`C%&gV$bU5|jVu)E$}+t|h3^$fA>Xi=1NlL=NNqy^j4$DM>r=vBa*p-oes*n#Q+ zoYqXhU}U56G7+KyMgPLJ(VTQwY5iQp?(z-5O4wZ=e~h;QE#9=^XkH=z=sG37cu@ip7 zVhadv1{NTNXbn@KzIEDU@YvepTD-Ti@WO?9`kksNm&jx^*U5PiRK(i z4J1OJO6W+Jl5Nzk*A>8k+$}R~L0-N-L<5R`xHg)T?mC>;l-vjN})GoUFZKn&3a3D_%Ye#}wXDG##+u>3d#7Q-I<*xD8!>aj#6LjXDoyk?hu zq|>Z(8K&$GB#>aZk;!#g%I1UCLTkEit!^z}3oM1pk5{j4>GbkLhueh4v=FGJZ`AsSHh&s`hMiGz$pfil<$SMvi-MCx)XU)3Rd zPD8xc0pvYNtTu7bVy2B7gYok7fR)feUU_*g=?mBA)hc18Orpx9XvxW>nz~K&Ev-*? zlN~{}S!5{a3=Q~-(hTY@Uj>YY4)Th%jh^EmF|vy5%g+TCLkD^J+7=&nkoB{7Tgz7h zOQD0jY;8-Y9VAAkYIFGtU@ml!m#%H@Sq>5-B{Oi4mjh#=gS=#IV;4FIf}eJf82ORq z<;y}epy)4N8_kJ>#7OPzE?)|)gnjjMESiy8eyViEAIc z_O;j0gWo#%nuB}$zYfj2cNN0`?YJRE%sqc=;X;sW;!J; z0j;uD0X)g_rFKDWRca-F7!(U>Wt7u6@xh&y+IL^R>px5<8OG?RQ8YUd^Q3}{SjKjQ zR9O}@dEsUCGTYFRzV7mxD?YfrQu|;HwK2}R#0R%lYL{!Mjd7qQKDfD3dw&hJF;1_< z2RBw~zh(`!F;1z(2iI3>zj_U|F%G1}2iI0=ziJJ&F%Fi*2L~&)U%7_b7$-^MgZ-7- zthbkG-7q@Lv4)H4RPtO)DG8B8{?EhTuxSM-8Iz4IF=BX z-b$@=`OfYz#=(QQ9Iw>cYp9KJx*#q`E49`dYGV+AiOb_YZpezq9}4d%wBY-u?C6#?G(q@Y}zWef zHgiDuQ~dMfa&s3@3?9U}LyvO-e)$^x$>m0bAjXCz&b9a&2<|@_A&9ZJiE|Ua4uU5l z1TnTjac;@iLGaE9L5!`jIGT&2;Lqch@llEv0wYfdmRLe z2tf?3hcPxx>mZm%2x4eGjIp;`2f-{t5Myg~7-PS5`3~X9`$2>thStLvTdQ>tOd|v_ zv>wLTg{_0Yj}XMrdKhEFwhn^HNk3TsR{Su=-fkTPUW6dV*6uLIe(mx#|C9H}5rP<6 z4`XcY)4uWBXAcoe%7#qfQ5V#S77+Mcw>@C+p;6w;wXpM7Oz77IA zLJ(tfdl+NKx()&>LJ(ua8izUi@(t3H_XiPz7<=2p7#r4g5cDGiF|;1W*xRmyz>E;Y z(0UkS8@vtzBSH{E>(#O$KY3q|5X9JrUo9K)llQd9phO5_XuaCt(^dTc)_XQ?1_yt=w+H_5!p{pe@F}VR5b|g|9r74sSh{?;Jr(ju zfQX@UGaH;s#uVd3@%+pKaTT13n2uggoet{Ks>Slq=}q)fWj1S2BfoX(;Y%oRdDel` zkFP?hhqDeu#vd)@5f7DpqCy_e1OA9nU>KfGJ|_zdIha+(<$-}ygQD6Wr2#vVKWxGH>?{xZby6xkddQ>V7Rp4gu2=%oAB)u8V5ko7;mP?#@{_3~lrD+WiDQhp zo=}E8+9O1d$(2DCOj+{kr5@iTYk6*Li5PFuliC=Qb^k7wT`SmlRrDz2QGYt*F-CX2 zI?p?Yed*%~dHf`K$D@Wkp6R@wypTsMjizwmm*+;-S4_YiaRW3YtYEqSKv1ShQfG}_%G@F7?s6CzG3wJBp-37yY4DBM+ zdXr&T^pp7imu}!2cWSr)*W1@_sW*T8=ItBW^&h?dnb+KdpF4PLf3o-Ud#~ILK)gS+ z{hqB4ZoP5y-5bBLfxqij%Xi+{xTe0I+t|2vaBX+@tp{`m3@G~c_jr7&aS%@_9sa22NigO=%f;8H* zUygArM3?zM_?dm}+8AH83{Lxfj0?*Xh$nu3Y4muA?Ms=}=Xb1)@Ui7Iti{KlY43wL z4_iN0i>uY?<###7N3*ALrWj1msDa^5r1+LwMKyEe|pm*Ws8@k|fhiF4a~g7Z|!=;f8V8a^LC3cubm>Xl zYGm5|i4f}kFFgmyGcm$otz?HFpEWYYxY0fVdCtgmDc*2odf!LM5buV+Hr~Uf723MA~2uiiX z^cH5W;Eg?|u9ym+&e9;izye*=M!Hz%hV`AViY@V#CnaCMJ$@jPu!v%&#I{rmQAxVq zUvTNBgiROTFo$O5Ghq^-EZ5$Cl6{i9z0nBzDv@yPC*!%B~6+BPUj0tOyKe z=Pf~J>!TVsrkmvchht0pc_h)pnAQYqr420OC)jqyD7QScUq(_2inBGTY)znqS%~AH~(pDiGLDX;`^a( zb@R~3IBg|AOxcNf4y4Vi%(@Opo~b}%*A~mwsWHdBbX_DZsT8#03N#~3X>XPin+Z89 zs4cE(WvWdN{Lh+fe)GINk+ zI>(o)u0|}7ggc%xlGvb?u01W8iW)uO3ZGviK@5bbH&(k-clS(=Hy zEMnk}A@S1xequwbV>KC@=fs4S=D0yMshSStih~9!^(@IR_^_h5>d=HXqKLY$@+9s#Fq2&Mw2rSHX;h+HF%jei@JgUU>Cuf?lA2d79r zK--2$PCSBi+O27jhvj^)Lrwv0WL@6)`lvk?L1WP9R6#3thjw{HyNOxNuQZZ2UNlge zNOAON%w+h|!h3NkwnQI0GgSp~bM0D&94&N`2VBP7)+n~b zFt&snNlc-9$1kO8l3}-qp4Tj?Wsk-OHoT~^vn0stE~iF=bZ%-6_P;u|#L46z!^X+v zA7g=Wf-1(M@~NmCF(kfmC1EBor-ElVRlvNWg4v@!k+(S)wk%}Nf*G&!p)e6Ki@h=gwU>T|7; z1kdR3&-o%C^bh0N_+)%LDo2c^?jJ=GqiL_WXvj$jmN3?wG#tFkw8d1tss?rwH9PY_ z%=43mMUV%R*b-iBiE$+1`uPRRV`@L;m!!c6tJt!cgw5$R+bYrMxLi!hn4jl#MZW&y zu_b;iw#2`T)+8=m%p2vt-791rbwVWwdek!X-qaC0V6vvt6HMljRw)TfRAeH?`rBAZ z#F%Pd7VXl^7*o{OMK)qcyeE=KwF$Kg)@iE9=p+S=b#I8|`7T%Bcpt1If?Bbgn%NU0 zF}U@nNCKVMrlezd-e8>4%po)u%4Ps(BugcR?zCPU(2ErxqkE7f4KGo*XGyq*Ish)hX+4$@Kp!G!E5&a za{vF{|Azf9-%sp)bnoZ(zHQIk!}s=ge{J{scPG2q-Ot+j-JKuWc@mr~eD3z2ZU6Z8 zySJt7FWCCX*88`Ppk%X{QucWv3 z-hA)4agoI7X9Zh((7ogOMH0y~HeP%0SiDH$^izPXy%*m*7A}%FeV@3scm3Wme~|=q z8rAN9x_8Vyw**2EXC(gQ-f`_BiPP^|w|4*Iz2oXd66rHG{?)zX%0&{V58SqPfAHQh zdy&MMHsAf8d&lLAB+j(??l&JDmv-KI@ErXLAx^jX?){_V;?7&IJ*U9wCg1()qvOIw z0;gMi_nk+_`HKWjH~8)oN5{+s0{H3n-klsB=PnXB-Q2t0(J_6I!0FcB9UL7~7YUqh z>|N{VID0`m;HTSq_bo@qnTsr(ZtC6M(J^_Ez?qi*vZLcKy-46pOAANGZ@x(2OiS}e z$8Wkw;7m(5j*h?N0)h0ImM$M1fAIy?nm*IgUvhN(MHg8()6&$@@f$A^IMdQ^IyxpU zf^?>(6GzARMFMA98b3NtUj*q)!(m6qsfz^8bdA)}F?Nx_nTC7A(J^|Fz?rTA3Z1+_ zAa$mtUvYGdTqJO&rC)XgSe2i9stw$wd~4_wmv3XIxbBGXn28I=*v} z@iYAcSh#(Wg){xbKRUj3k%cq;13%d*i(?1uqPKbN2^!XS=!G&))eU*!w@RQ{H*`_8)KmDBuy~ zw_m;W;jN$8diPdq>vfxdv-#7T-?;hZn{U|odjM{YpVi4nqs4VQyCYiiUP z>vAtO$NX-aaazuBfs8VW*8xPOyV1=fkjONZG3B~&zGG*-q|oKcwqIZivzDHoqjj<& z2lBg?OgM&*<7na+(UKg;Cjk)aD!vPIf4%eCtS0TsjR&<=-Xuv_= zvXD`Yv;q6mAUo7}-&EO}ryIV|^BV1W%^5UuXtu31KAC0aQq4WG;(N`ZQmz=E!=*tJG&s zAFj6Zpm$E3$`vY zeqy0yutlZr?Y{FwqCAkDVF9fOa1-NF=mdi>(q1TwMw>#ab(~Qeb9>t6WqIpEkwjUd z7M0F4(;XO0kjb|oX8KcOkbCI zh&7wo9C8+k_Fz66s}ePv^_qgT7riQJBpdCi!1(R-L`txL&CXHLK`B9`D>J@W9J96x zvecSm9)ZZM=%qvh>14Y(V^C}0j9W7_s5L826J0h_$~6$9p}~zpz@k(c8AIDA<$$ZX zsyA}#hQdrWeE}HTgn6dtWXRqC&`DKB@I)%2(4Yi!yLVRQs4vojY08AasY!+OYPo*Z z>LpXEjL-SJ8l;`l!eO*=&$hhHFN!3lDo4YUcHT+PEWQEx(;lmEmN)N-2{S(uXN9T3 z&Jx9-kM2Q{#KawUCY&zMhIp!Gh;oi>V$I&f#IRCDNn0tQ-N7YimQeMBdnXc=w%9OL zlWMd2%t0mGXGEPw0!dJ3-UP~JYT(@t*|bxGy=bJadtSkvMh6Mc5`mm>+8YRR+FLeUZwoG}KCpqZuh60GGC8FBy>YZZ57Pg;)H z4i2IV%X%m1&6|xpmly_c9?iKZ42(L96y9~z6p~G9ELk#o37x3#ozzvQ&rK4wNq<|S4S#hlWSL}ZW7)3xkv&`=xIMiI{6>!K?;WUJdJ1l&C4iC>RTf zp4~ZB%4_V6n~{y0>^M5c=#4?8KZAmS(Q<5!5z9Vr*7L}sz~puk;r8WSO{&uUJkG^4Nmvq+*mtm%43?CXYZjk5h*+aB3Lt-wi*l4tw57P6Su zr2^>G=yf6jl~?C-vS~^g9_vC0tyRmg8QE4BnA_p!of6Z`WqOsw1Xc!L9GS>7+=#MA z3e~M7@~yURS#C!#W>6_e4=En@5uQ)$HZ{oA56DP@Nw@+-RdL?|Z$Y@86lCcdC67f@ z$j!x!W@|mCByjW$_|tDh61j$yGs@*oVm!#lW&h|R7k&#`eL)1b>uax^l!A`BQq1;q8> zk++yGETY=T$eVADB+_%Ln3&~8IXa__B+@9MMGI4!4q9VOsg!B@a67f=%2d|f{enmW z?M(-Qo|_gO33N$_Y4it4lE#vqM!_{v)DD`c<*NlwBX|CvNFu4=sdSMyX8r&g6q~ke zl?cH@+H5i<6E&oxbMH)8?ZCfa3Yy51w2)4NsiIma7 z#~Ht`cksq&D)H?kT&=d}ZjKn(^EoxLsgm3_7?eG*B8dc;Wg11M*QGP98XsV(O4=K$ zbf>$>$xOc3ZAn(b1)~>CZ@%tCLZ;_Uq18`~VGK_xm42$x(o|2;8BuAct5w%&rTUAy zwIHu6D+zHvNSDFzHJsujLnU*pUIUBYQjYEU37qnsL^fTmC`rs84{p36l9-NlKn`6P zake?&h>}Z_bdSg+&9qH6q+$}DiawM#<_VlUsIMGK^j*foXDXS-rxM^G?s|H!lCj&B z{Jc8LQUYe@ZPKOLE~`6{36ta+SdLWk9)abcnv%oRK%5foWKs*{F=n=yDmStQ$u_$k zJ^vGRL3h(O2lEs;MgniFOmdT)FX6_hI&3B-m+#|p2`oj)!tQTJHnfIFBAVB)SY3V4 znKp7vrO=#Y3j~>Hsx^M%v>-h@tgAeE@UNp{sCCN0*{nj-M5QSvq-nEOqgq6@nx5A! zQ?Bw3T2|O2rKn<6GOuQd`g+?>EkS5HejPeeA94A;Od-%+(>8Dpz0 zT}W4I6;7yNnl0v0M$c+3Es>ZR^k(F-8#0v6+F)1~KauQLSKmgokMz zDbeoexiAA$x7@%+W>`aAlIHoHSY0GFC#Yv;n3{GA*i?yHv zBjupct18s6NqfVw!>0JLELC!*AiG%)#b}&RDj)%Vv;r4dIO&we`Lw`UiXJ4aHtO^- zugOejPNqDdu$e@qGsz&x(|f-e-v7ULDq5xV-J4s;Oz%D_P>2Uz4rrqUkX?NjGe#T`R1Md?eE-%wmz`c+WdpfcWwL) zQ1fy8=*ydXDxk64IE+CncRB6bNkGNJQ9kdww!8sMz5cBmhcT$;);1M|>74%p0ZE=d zg6S@=hgd+dWAM|hjU@`S9xkr|8;?J}aTtSD?(%&5CslhcS+c!604CoIiWP%#Zf%oi z!(-ItKClO#v&12RSliw+fp`7o97w`F0c@VF&HA&w)b!sodY{z7mdLKv9_I4U33Oa zg=%0*W0Y=f>Jto^}Zr*a%%B2FJtY84@mb2?v-A zT_Q#bjI~Xk=Mpxsw{nT~Q&_BR?_rm)fSu4KVx*v0+s+x67yx6TOTBXH|ID0_C|31BiH?Fe{;<{ z_^X4_{$K37dmrAjcmHg6u=AlE6WrPBTYtEvZvOtJyz#*cBS4>cYPJK&`Ql{WyV}X~ ziKoCtV@v>7I%7WZ)HF7LD_yBP@sz&;5Mx<))lD#o4d9BKz>5vwiko1(0uW=Rd(}-a ziVfh3n_w6lz!f)vy8;kH=~Xv@6C1!4H-WtZ5Myt0wR?vrp0ZW|Vr&+!cGdXAQ-jz5 zu5_vR#8ds)0Iqb2{=`$}3P6m_(^WTt5gWi2H-R1-z!f)vwgM1iKY7(npvDGp#Z92Z z25`kqAg=(#*lJ&O6ZB#OxZ)=0#s+Z3P4KoAfEY@zx(Pb50bFqteEAAMjB}BzJ-2w` zskg2G#5kz9+8yf?PkmWz05NhBUcP01;;Fa925_av15Z5FUIB=4aC5ayu;TyQ_{heM zr}qEz&XeFD|3mx$`Vs4C`jHq_e|d*G#Q^kNOmoTPdGsSOde(}-Cx$Dmz8~YX>3qVU z7>uotMn7_C-q5fdA3v+z*(1%63FsX9k@IPPya~v@+!?4V{-6-iBk};Hk{AL%7yZb> z8%@D`$5FM{(g~)Vw3{-6&XSc8KJQ}+c-QBryPTLJWrKZW`Vj?(QE5J+y@@!jC0qQQ zOjA=QAgFnkF7#`Q63ghEXOWCxV7Yd>H5E%ijh(7yep;(H=Yy)|aoLJ+PnT4I(wSuc zE{LGe*Wuz?6&=uze9dGzSW!Oun^(&+ysjf6pO%go<2?L4M||dpJ?y;?lW%>J9PncR zLcbgOJ@Djj)rl2#I5EKQFaT>fR;QhntASs<<*nd1SLSyJnod5K-@%vzO+|-9D@8Vw zZ1s54>>~k4U&f$~h1w??;!JS3nmwsSkROHiM}cat(>1!5rGnTDV!kNlnsQ!CwePl? zPEBrSMx$}Nrul_|VGCTVY2<{t<%$kBPzuaMbuug%78vgy?X+uXt=~_oI7Y5T`;!Wb z9`bL!c-db~fW+q2UaqX`ckrjB-^H1s!ybEz%JtNh)2GP!R@dsG$5Uv%Dl^E~<>)p#X(q=w zrExg3@Ysg;$TMO}KqfPj;)j-AT#P)`)ylcdKrm`zZ^64;mgtt~VowMF*a_94yVyGrvLJyxN<{bv+Y0 z&oe*iOU<}z)l;7K@O|s2$jy9D9ecpd-u{~BcnW;xE*5+lIAX{px03BBss@5hCg9f) z_?y(Ed1-qubx%zmAYsCcJ8Vd7<}d%r^lyf>yD_LR|Z3j5uQ^Z zqbd3s;45ssuJbg^fhiB3{`Xi_x^h^$)t5|XYEHu9tSaE?g+Mq>f8vH4j7Sk7N$a^} zxO2iN+0;Ukdedw+1Gl6yT}64jQNuFR0!K)dxy)B*E=Us({2M5Fl!XA46ft3eD$*ot zwD_KdGql8l_x&}vp)pfPp0+@K>3gZROBXpCV-ck|zl(BrTWyd;)hOSa()~t5t~T$< z*xlU11cbPJZMY`$b69jR+(hyJKX*F*e|Y;%oB!ta&0D__<@LYyqMN^a^IvQ|e)F9- zOE+H@Y{o1X+ z-1@1luUqp-VijPaIK}e3TddJgx?NBceNTQX^Dt@t5>>mYPFjYfTTCQDJKgcTkG4R(a>}-}iO|FSWYUugc(Pv}^~q^_ zJYQ68Lve3Kp^u8wf%9WY@U_&8gmpeI>MB_NrCf*>=njbFoCYFd*ZlmHz7>TG%0{~) zjX-!;cTgH)<3X#*v(zF{$kY0)jbq&^%1}+Hs>}%J){O|2U{-Q9T31t(7BLnX5OdbY zv<&00v(d2W5pIr}SXfmMH!?8;Ppls%VDF!O+J#q0w+72 zC4Jn)3R-_sV;dt~sn;>5KJ;pRohQ+>VouPC52f6unbsR=;`PEW#n9pk^Jej8G!L$uRQcH|q zCz^%5D6mwGA@Mztjigs-rQAZJ+E(kOOk(KFq&!v-yA_$V^%`r!4WV1BcBU?QFh7xi zAj~L?0uAbD@wNxG&jJru;U=PYYsAAv~dMgSiQWk(YB2a5hzD`f_44?u< zG)n5VaMANn!ds*gg+hA{FN!ViAPQMhW|<=0^33G0sCbpM(rir&O|xVclVcxoyBV)i zp5Yuq*|qxS<119kjLZ=Q5I5aRWmq?zl1Fxhpv%2{02fK5lN-Z2HRnf73=8%@8c9s^ zxZhwW5L+`_expJoIjS{m&n*(GLfLY<=%h1{H%=B?H3 zQnoyBRI$>b=VfAMwKQIs^^$sz)ijSDR)zkcsmx1-{V4KE8P0jNGEB@EqqWfOlF-kV zM$LAiu2*;=jlm-n(5J&iBU@n)qPR9?h{C!*LQ;*HpM!HsBY~I72{GBFyQJe;AWGQm zNR_@Z7@+&rs2FbE?+=IVQmxe}mltDywCL)3sj3#MYDG`gu*sOJlqzn%0|&BouHD z@3EWhNMg_m8n$iphLAw^7V})OGeW5z-w;a;Bt>XQQCpCdT}m_fengLIRBBzpt4BF_^E_*!+ z23M5IQ0~_HS-G6I&5W-ICM{aSb`o*NHA|`k8gG%dh+e|cH={%bK%!n64O2MAtj%rxRfI|&w8V@v%fj8V)@o&&5N^Bk(g2w+MrfL> z>O&O|dPzBj;=A8}B2g-(+O2dm*)juS*lyz>yd3e3w9)Z%bh#5uWj8%9rull|z>Fk% zO>m}Ag7a)aq)3AZz(Ijms;H!$M>Duz;}=1BjJV})IoOZFc9n*~vl1gyB;rvf9I)*k zolFJ%0h;v^VW(IhW*c zjkK|;&$63ui6lgQk*5oiK@>n>d0r#AW-ueM_T1-EW;3r=Y&-Bdo=zv_lkj6js1^u( zGS3QP)?(=dnD~n`Tgz24*;=z_4Y)iv;8MAKzaVcnB8TE3v8Rez#1ox%DcKaJ$+R)0 z#ZpH!C2!p17YV)V7aiF4QV}RF(GG_Fo{kr2Nj6J8CGRZqb3C8S z3~5P}C{_zHX4PiWY&k7-E5lUBXp5vOMS=E8zFVpFNXaKKEM?d58p32NBX(F(lY~3- z@uW&vNxD3qd88E;BA=}1G9XA*sZmI6LRLz{S$2SG`mi|H#}%iUNd;gs#8VCKdXz6f zVXBMvw1SJNvD>d9r5*)VNdu5c#v3h|HrB|em2t}K=lr3*^<9w-hG}-EGmR*d(^Z=Y|B5j&w2uXJ8L2Fh_MMQ##3eprpj&%&b&-I>!es#=(l| zqMj^vnJk~HK^+Hf84}!YwvD8BlB`5QD}4o(6|5)@)tOVSL35#^T6s8GGc)oC@-lEK zfzDG-n%jt236x}hmYajncS?XveN=5&N?K()3036-xwatlod(4!fk1hMjVM`(0$X_m zL1~t8hg=0@SDJO&xz>CtXL8kHv7H4!?mJ^)x|`bP?d?0@B}wO zVe_WO7aCaB=1XK>a|RP`fO5O#V7^FB`&+1KxF^jl^_R?9cH}3dD zU+oam^$$c6iBW+_D?=OR^BqGPYY=A(W+u-hRH#(&eY)Y$1-J9iQwEj2&0f^@!x?tI^gjdBP4&y`siTA(JU<>o1aSX2;g zXe1rH;ddRs(j4>~CH&fNtt8}`nkc8$PNwV=P+(JTdk}d2acK-ut%^IVDjb}K>H4tP zlJ=sxLk>DQsh2gi8ti7Ae5os7lZM#J=X+$U$I!iorH_gn$Zas^4kG3Q*~1~HkVZzH zTy^UTQ|acrQ%knT%n)~jiHv4SLDv$nyS{tvl@Thir~}z4@_AA%Q2#%BZvy4Wc9sb$ zGnHADS(RB{F+FW#=Oev5>d6%$g*F3Y6pBSsEK*3JC4`L#MT9~Mg(4J-B0}*Vrt4v- z%(iLZjM>g~vmA54)6leghMsYIror!d!GHk+Ha2sbZZqH+I5evb7>0(qk+oz!Rpv{n zTyOE2Qk|odmHGew{qKM8f0zH>`~7llUK!8YeGKUq^_kVJ1wj=bHd1P7#2dneKR6y2 zBn#;}6~B=lhLc>rNb)x4^aI7x5_;LhAX5kEL?v)cGm*if!c%??pkaj+Mvl?<%^(I7yG+L-Wnu}G5nX_%58i!tmP0LAwO4en)nj}WK_4nN5 zkXq&Pi1)n`U&@IIiDS}vTPlQbmnKO_E+rGyZnq5irK+^Gw=&1>O^&`jH;4U{4o(!z zfJObPJHr}1gDh6m90D+yie=zUJU`F)J2$Z|B+)FE+9ccS2>pVRnA9Pd23dFdO1p`6 zrJgyfi+{1o!8tfqsMYHZQ`{O4ERAwNm{^^)e+liPuerRFqoHkAV6yML}cqG zYL3)pDT=5fDcT<}bJEodz==~+9&cnaLCTZMPNkS5S}nAW$9p%+5VS#~k${@r3@oR4 zSr6-!=|7p@QgTCox6sK}Q zj?PKXv}MH}87hdl!Eh>jTF`fe+fGr8b(QN`r{6DF;U?5+$C}RwegcK=KG-}ropV%! zb8i-;7GIP_T#{*kGr1^``S{Ji?eR}%1by$7@G$86{04oqsaz&HUp_VH+ZL}irUkm4 z^@pLE?}|i|ZfV^d&u4W;O@^Lz_yWcVWIHJO3cpO2gj$2nqI znQ&CM-pErD%a5fXP%`NZ0Cr=?6|*`Q+C)KjZEMUL=ruP5VsH@Mps{OOc0$!V7*OZ< z;J}E&(29KA1w~cq9iJG$X>hQI056ZK9^Ja|4$Ww|dDnbx6{mf0!%gPP9d6zZ8u0jB z;LfSYH$Sq<<;c^9kDCwY7EgU;Q(ZC3YzYb=zc_`h&*zpIzY0$dC>Dnlvewm z^V(zzj!18w&h%!s4g?r8U5~oM?PHW^+&||SW$Ez>h`P`FPT3@cM4FuHc^YNSvn|UJ zZCWe31;K$+S4js2i*KkYakU}EyCuC{wri+x-=3#sFtBh9TQyGt;SrkQ@xGVLb=esT z7x7An%y4riWU35EvqmDdLETGw0}yeRXY-Cyz#8~e$8C8=5H)|Gc!EBa*i^l+oaaSV z2cGwxqeCO|yrbUQD&6(Ld0zTn&pXAJqF*hXsNQ_t`~rE$@l5W4^VE?U9VI@3M0y-+ zWZKdHm+riGJ4JN$?i{Cxin-6up2_EtGtU;MP27_R0PD&KO@K&gJNDEW=5jSASxM$9 zAR1F;%Eeq;Z;bE%D#NIT%NSb(o1JD$A2%_w36IrUDw`a)%=BC~lxdPnx5!*YY3Y4; zS|0Y3^MG}U7V7w;AVfROZr`d->Rbx0u~@SJU#-lSyxbR69dz~^uREMXo&DX++Jk)z z|IAe_a^@{t!fQpjnmW zO8HsEX4wpfah3bOfSVoQy4KAOlF4gT!DI8jT*)S31A+kX>n!i30xTsU z#MW^0GnVf+T>C%QnKdu=`PkztZk;bYx+23v@`o1q|Ahsv@80;vV~mhjz6kL6 zqTR^k#-l)==M!V*O`yEz17qfmpq%FoW9AK@u+;N`F|&K)^`NLXzWtRC_PN-tYlf>~ z%tZZ1ym1ie1FF8ldg}c5R=4!IxjC8K_*zg#)JHio)j)cg;oH{DqvPx5~Kp`lNE>5l~*#M`^rQDKxv| z8r!b>aNcvBY-P~N2UEY?8W7?j1M!X}qp*h;mM-tk8wp3e@eQ|Z_Z;G3-uU`kwtIf? zFqIo`xn;ZO8lN%`%86X=Imf5my^)KW3Ao$~k575i<+71Jpz6;hMr8(+5xHFVdBLbu zZlpnZk;|=sq_Z@r;fc$sHxN)x)NNLPi&?syyALYBOrMIH33Qtka9Nfv>RCtX)FNi& z&2E#7^Z`|Wpx`sl7^$Z>U{J>Mj2j7oBBP$V0`$rGakpog9Ik=#ju$X2nx~0Ot~Q}( z$zHq3p(JgN1rcXkC?PV~1Y9Z!2W``=HJ2{$q^D{(-VDll-f$xmps>g-9_T6MEh8?t z#bcnT$Sqbl6~c4V~wzZ6qrd*8qN zb(el&hrjqo7v8b;^7#8UzH#k0!LJ|G=SlVY6BpOtzW+`s7F*jqBV3CtW=t$%B?7B9 z>a{L|6#^j{ctVnZ#CCpGCXau1L^2=Bn2A)ef?w1MKDg{P?UJpTOzIP{-2xY?TwUQz zpTTfnNb2JK`KWQvwjkB8LbdhL5@5Is|*-k z4cM^Y(|j3Xu&L8evco<(^rRG*sbZ?n6pqVC9+y$7rzNtf)*9$gNV2+=VxqF8Ifv(=W7@6(Xp`xoTx!j`;L?{{P53FvY@~^qH0fw$ zqf?O68AQ%NgM6~mcSAnMm>iqoeG{1a@jIrDpJD2Ia*?))Hdd)Ls3P+sGIdzxACRew zh}DAXKA!@~!r(L|CA*@~=1w!UnLyL6B3)=Vy1ZgwNv1S{1c!(Het9YvGGu=;%H^$c zzv3R7I=o}*jk8R>n8meNL4_JusG^-0m8lHAtSKi>0Q3 zvM(l6v*82k)tKIH87;iulw2_-r@Am0$c*MGrY?A4S+?h%mTY;Q99)_6iEgWu_FT*w zgF9lwYO{wC9AUG4_t@0I9aFEJW$MMG4F$Tz;f+eYRD4mHT6;jICdz&@r6Wv=E!VN> zv>Fo0{`fRgV-=6*EF(3+^+Y#0WZH>(3D~5<*dQlkVaS*XsmOxkzlzIpQLpyzm^yZr zsTZ>+SA}}5Qo#j+d{LSDL5W0E=pFUQ}A(O`JJ99;?0ErjGsNS*Bjh^FYysauFj+ROv-!>IXGe zlZ3&;xKqbkoWxHJaK8kSpwq_cLOaOfir(pg`?RB+n5l(!a!jSvY|{X*{xFc!a43jv zFxWcDLe#5gcTD{cXPJ62V|SONn(ZdlZVE3ZQ!@{0tS%AruC9ZTwnepxX`992Ojkc` z1yYRC`gG`azA=I0u2nEXm>dpp92D)Hvsh+Nr(TW-Y#AQj_rrZFv;w!3dt!s_-aG zk;9DH?@w^LLa@n3vw3W4=Z>lW{wz~3ZXMAI!|-Li)8<}OrhZUkH8z|`p^1ncmKJm_ zD^0-gkrYlFt4E_Oc<+zQdXm=``Oa9yIz(#V4J~A5$;qh;9y-j>a!nou$ELRLnEL-c z%hZdz#V*FR*#=7D=!?qK4{WTi@Xhwn0f`rx<_I>0F)Ik|Q+qWzqA90pncOS@8G@18 zL``&vwrcVT%LY&O2%luYvxFg+5I8g1|L?^5u|56rXD?yf@4xu!t;5ajjoI2?gI_+7 z&v(4!`t;)4-zn|C7C&p_pIY1^y_g!X`k)#Rdrme+C%Z{FnSC<^{=qfH)?at^b*$pP z+;}abT!2k&?R6pAeATspw^jWAb^k?vAE@Wj;w!{OTZ7xC1hU8@LIjg!)%k|A)A@VDG(!!;-SmC z1WrR?*t2rgo+Qkmu4*Y{6(B>((yT+x^-Mz<8p)Px**yZiPnS4)4R8tlJeODuIXp*= za(^!Ipcb#sO`zczw z#*4~N9@J{;xd~mI#Cw)+6AiS<=)5*rjCQ=3+T=wZ<2VTnE!`$>eLrZE;dyPc7#4X^ zwaJ5;PoA4#$Vmig`8N4CpiR{C+GH_)@}g>!2Q{BOH&K(5P}9&3py7$=bmv%q0`@Oqw+kMmJFJAu8<#%81Tt2w;=a>GEONW>E zORwAcuR9;udFM`T=hfSPxczh6|7@Gy{_2Z=aPj>YpSW1M_=*dkyYMp?f(zJ%mu&r~ zt@mxYTi>*GdGoWIKe0L8{D#eo@lVBnEUw3M@%Y9kH~!_ua0A(ht$$+uht}nFXzlOU zKEC#zwQpT}EcPX!)>0q%D7&GoAJ7>zAZ4gkOg31*!}KP7t0vVrAz)DlT65H$eT)kNo02Cig^R*jPs#jb1fQaaJ}>IlGNI7&x6 zwXmj?9DRs;Y0bsjN~TRV-Hzp}O#ptAnuBXTL|yShuN#cx-vS>Y5zm;iZLFAqvq(NY z4GT_g0Q02ZVKI{sy9tww-e@(%qBfJ5_}k&5RKznIYx9t(%2uCJrg$M&)+|WTL(#6L zikU(shgd_lWY5N>J`(?C_$V3il#N=Rfm(K(iE%p4DSqeSjxDN#aWh_8i(T<(Q!$?kyj}um+sdZe#G-=#8U}NEoYQV zC%RdKR#1CJWs5S4_lwCSS!jTEbB9R;gY6+}#(DVYO~*WnqO>aoscxpqnVP1%#W`Ki z1dcID_B5v{wvbkD;J3@J>qk6qjCcT+-OL-Olwx1@B3z-)W^ZJOV zKh)(xEpLpny5v?Mp30z^N>@`L@O;T6ok9jvvWaS|V3(TlI(&2x@zf@Ae{RT@mF>bb z1rTkR_M`*=h2(ip#45TqlL^T0C;ZY}jdSqP*G4?FZ5t|pWp>eNnJN=r2yn?-nrPuZ zYR!l3qKp?{31w2%HXg6RM~_52;~~?lcp6)Ub>Bcs>246D-6;U0>esB+tR|FODiic< zv{r=TRrqK>;>ndzJR>lJbfF@^Sgn`}%g#7E(u-n%<71<5=?O2Wf@hW(6ldY1*F`+j zS{8OH_E@8wwCS>Vnwg|LAyM_bd`TU*EJFnlHEo$6$wHigk6s(`B>jAs)l*1WH}qfv zlXSx@meb8yAasOaOqKw`S2_UInM30^4Ih0?#M4G$AU;JE3W%96Cs@%McH5~$w}WQO z9jzG@M^YBabdd^i?TVP!6Ncp{ZDGeyt{ zsu{1qN3V`}e7@O*S%EWJvR~w!vM%7$qAT>~7>PF;A{F8QnJHE2Pn1Hu3?Kc|h-X@W zC)t|O>d$j{JxC>ZeJ*FRx@-**og({Y7ETGwG(B>uLc9bY{dW;h&2!7`0nKKeQkIxa z+A@n|^G-Tl66A5W$EVQdD35T7QMm)fN%-iWL_G9Bc4n zZmF8acyKLf>ZcnG;HLz9^wq~avM$%ki2(9Od@emBJNP7Uz4ow@$P)8vn#q6*gksp# zG`Sqd;iFeYJhQPL+DJAi*29D|R(D&nd1 zo2;AE!+9%E0&NJh);wpkp))n|^`;f%as#qj2_d?aK;lLC=#>$Vn*#8H38bHrA$#hx z+#F!1($$QBbwOZeHqxX*+0X`Pr3kBW6h3;zaXBHcHS;Zz^r>78ZT4u*DNz8|79B7{ zfPI=P6j-TF2FU`Nix=Rdmq$Fl)vQVLK*#z7=%{U|9!xTLF*z5ZY0Dc-%N?^XJ7$A+ z*j)UZ;G>sCJSiq1xD1D+xpLAR`To!gMwPTD(W#VfbET1jnWatxxNN!^e=B_S(ujwk z5re|%c?s{#3_}gF8hDt`3m!AI_&JCyO>oOZ=Os#KnfQMXAH5{vDS~NDYp3m$N15D^ z(da_2t9hB43J1B0p9T=4X3CLOCPN4Ky#LJVKBhwl*Yd(u59k z<9>pWH4hE_LLvT*@X=nxV^TvagTpziC*%>Rn{kO|$5icFx6B7gA}N|#FUKU+rcU7z z&u+xiAjAM^Hd^dJuMue}RNCY)PfW(sitDzx0Y3xqf|yN}E3l#av*Bh9o7MspT`T}jMv+&V{W8G3DIn=TU zQgr!JLC*1MbYe6zwA2_#p;J_8rllq|)vOQ9cm_V&ig;Rl5>HOtOdTM0fk!+5Jk82C z3ohjWyt6^62^)FG8`~a(=GBO2GvZWXe`yw8#7!(gK(Bf8C7uD zo?r_j)sLs)qjI;yJFzSQ}EGx!~+j`FK7)K6pl%5GLv=;IiuLS6+JC7P9`soA2Lz?9%6U_3cl_|EKutFUuQ0zVVtXPwenlp4j`+Qk(Si zh_0`1DGGNnnhM*QSykW*31@^(`Xk-!&IZ$oQO#IL9&XpUY_$dj^e%9MGmlB2Q1{PX}6zc=_-`eEoh#r3I@!n!xWsgBucE# zW&>qTHus05Q=7CqeKY8G-Ky>c!lDj8a-zd@p3rDIImuBaO6p?C0EJVqJnD#Sk;%FJ zX$ET~8@-U~=fKhes>4B<%?$8t56N=vpk6Da%E$=LL}2}z#m=|{YNkY}us@=duI~IdyEv$2`&`*6Ii0{iP&!Sm%%zKglOjenD%tFbolYaHsXRWa z&b>S#&HDWkBKwxJFOlOGF&)Ws6b+hz|XI^^$|p&AU7kWoh3-Nj<&O2+Q~GbgXBbTa-p6shdeJgVer;R;LFLdRh+|lDlf6=4%!xS zl*#m%eJik7fs$R-t8(&iqB`1w3hRd_I{2Z~q9<+3Oc$+ga+J5m9a%;ADelZ2mhFP2 zbDOQ$o@NOL_}HWzC=Y^ZvX2TTRbVHWZlv--hNQ^4)N$EvjO8N?Vo zgHk1W*cLM+t2U>wqiZ|>u$QW+TuOA{P6y4E6;0hA+e2m0G9(kX$3@rhocV$d@!vnu zVM5ULJh<~TMRiBWck0#@q@;xVBgt&{3;lM+%qSCj==%PASL;l5LP6HY1D=XeT{V2Wqa7ZfRsmP0Z6KX0;>+FQO7>BrDT$ zORzaZZR_|xgSHKys{2G}c=c%^4D1CR;{VTy4x`pEfoax^;m~1L>ZYL@SL`?E_1XX~ zxAO@r3()TwFe#SIgRYjyr4sF;=7kE2(%HO+i{q4A!C|_fp<~i%c1ArFc8XnQe=rzn zyisHNyp45AAdoy*)Zrs1IxN47U1j&W?>k_*I$XR<2XQ0}bQwhI zAiYJ;+*X(*>xwj}^x&3{?+`2+b$N1UXRCC8*k%;)#N#wGsi`Q08ip zC0nA}LnUmo+)uS&MeGkOMrol_qM-Qu zN~Oj()zQ#v_+oWf9@~q<taYoG8Ani&f0EvYlc%qA#;?@df+)^to%nr>Zrp+hs=uj1cy4_}q2!=H3bCEXW zvVcr`GQn5K9_?!=9`?(*VU-cogP=uIgq6a)G4uI?YA)*VpHFm{yS*0Ur_z1CJjodaZ0snOIz!}c&=?J}Nw(k)oMC6st_ug8 zJ9ZrqW~~~AQU#L$ic0!$$`!zZ+-!k02*hWyELdU>N9MlR^%+XQI+eQKpfrwc-?N0# z@90oVvO)t_wCWJ9`4kM5EH4dS!ZWJxnn|8%FLM0UbT z!&2y4YSK!{R=2~CQ$o&^;aRh)?w8uUF0w(BGBC$>^=i;uu)~Gy9UZEZxj&u53eoaF zVk1@V%m6}uN$-R%oS;&Ykn}8il*5Jz!#@z6o-Gc7G}DyZ^pKU2H0T=hp3|A9B#07` zzCX1jA5#3l*>BhkH^msDj#G68r~N*?sKcvIbO?uGi}zX5#7&T2S?Z;+1UPf-c(rOE zcX~`?kV>a`zELD|2actSeNt#1 zssw~E^(uyo7l++N9XfkpyY?OD?Zuwszn|Da4j4*Ca>YcOFkN3KM#WUFsMTSu(BXZh znRlkF#Wf2006Bn*Zmlu&L{hA~cETQ0X-*v^aE2(WIU+BPIDU|^gaQ>#{e96j>L5Zn z8oOev3DT977w3ze4N%2*KF8HOSNt72fRuuo%FrShekvoON~)GIbeq^rp1Ev2+v(3p zO?TRgn4jVY6Nv2#DYUJVojNwbZyL%svy=mj`*RK3;SBxvK-}_HnFW>y-&Br&B@!yZXCywvU_TI4j zncc(P{N+Et{NtDFmp3l`+$H7GSMB`b&O3IlZGUe22e(TX|MBAg3NrnD^@U%*@LdwCwl$L8RC+QcZ2WJ>w|w93Hsol64%Qof`0i7 zLD1Re|InxZIS}-rU%gH|dF76vwU?YBD06nV_~;M(H4p^8=_mK@SZeLc8G_Q!(othn zBpV=dhMPi?(7PF)PG+j$bTR4T{B^|jZ$GK#S-1(v*{<`E z*GwWoafrD7jwJ-0JvMyA{BR_Q{{($~ey4)5zdOTT$k~Sf@aQTK^x;3|qwn$06W4<~ zdd2?sEOBRdh!1c5C=m4Y-vYh-6TQw7lsa3lr+?~SL>e9aOeF5zzer#A&KHNA?TH`0 zFuWt~!xw&oxb7}d&9ld>52Jq>iTiq)zV0lc*WzQ9lYal++-Yb>l?bpPgm? zvwQ#3zy3p!xN77BPk%B=U)N8>{mB{P?tx@F(eCM=DM#Yov>A!}nIEOEYbWCV_$+bH zqFw%Tk+|3wBXO^ICw*N#5%-5@i96eNPYc_Tpx^m}=v!#h*Oe1-|Mywq&h8>lzx5v? zK_81oj;Fr~r>}p@iMaoAmbkOW*r#9q`bf~bqLJh2mw$}7{`Q;Vz?vuapU)C^_Q>({ zjW0xkZhV2h{|K&$c#QokU z>FeDk#GO6fKJ|OQb0Y5dDf)WnMBMM3RoSz<$WtHs)<_)pSX9|h{W^WUbt3LFXNfzz zi#+wdpN+(Q<4;85KKR9GmHVnwmb%3C(#>L`wmxei13ybKZ-~|(Pdx&bR^$?bo~8Qq zjYtsrdEz>{grH|><;kZaK_is7URXlV*+alnn~}FZ75kp({(pS!so3Sg#XsFtzz+{U zUs)CaNmkt3Nmf?4{c=7s4od z$;$JTENqD+D~n^uBQIEzm23`5XUWAQI zc`J)1RTAmo_nq}CMUSTW;%r{ZjlFBIT$Sx+r6i8$sx$6a4J_T6l(4x8L5>C^Zlzl* zv?-~dVY)NTYFF8`iO{r$gz9YOU7Z)HQo1nKO3Ni#Ij(w?2x;@sIrh<4fka5nZo4~f z<|os-+%Edy>go{k(hZ2rcSqugRZ2F86}>vll>GRGd z)>gT~`M?}8mH&!##1(F?p5}-rVC6561Kv>}$`v@B_4SeT48M!K;yLuY43dLD38!Wm z8`TFAV&x=99pMPq@%=j4N{+hI$u$~wJuk&uj5;RgUBkQow3j-#rQzoKx>J<1%+)kE zhw|nqUuUh7Q*2-pW?H{W(85&;OR4TOgfmq!i=j3E)U<^hgMh1x@|3pAB*!vW6>UnY zl-M%RrLv4=qpC*};8z`5#}i9OK=`JmD%mP^Tc5e{2pv})l*$ja@y%JF`o%kbCa&ZO9}>f^8#g#9d;(z8UR zQ!cXADb}Cok&z!3m-Ec1>cBI1c0Ukn#eO%oo7~;J{MofXUi*3Q4nSVZuI+4m_VUkO z{toc!KXG~e(q}IH49Nf2x^(qYZ0FNE|9a=!cle!0xBuJrC%50b?QNI0Uw!ell;>q~t#!qj&V}si`SpS>#Uta%L>(g~? z{Z&_f>&j1FneV+7sJz_gu}yAmeSKfk$%f3jt|SS8HlW9Td*SGCo;OFKvHmS>lV^f3y8J%MxGM{_E|(UY2-k`>(eDYFXmV?JsVBaarQ{ z_7}Fluq^S$_Fr!Q1By8Y*L%lvcy}PmCees#G9Mt&GNFu@y*g^X<6cpO>&c5mUw-W z*d%Uy>v^Wz#5eKVg24sO7Rh44*t2Q@5wy_-Gyjv=XJVgOmiVh{GE%Eld2$*l)#tYgyt~#C|jOo68cvJoX#0-&mITWwB4jKDjLMOJlzt`}Jjs zUlRMZ*sm>1d?og)v0q)5crW&e*e8}H-i`fA>{pg0z8w4I*e@?jd@1%zv0qx2cqjJp z*vFS8-j4lZ>=&0Mz8L#h>|@IkUxAv8Q8CFH5`; zdn)$Svc&7L55+!odyF}6+=%@`>=(Wctfki0_BA22N3y1jh7-Sr&V0JRFvh$@5SAsq z(&;Qqy4P+mOS;=?ElYa2*<6c<%OS;Lj z%aXMju(iLnf7@x!Bi`J4 z-PY@tC5~^scI&mbiO<`CtY3*;iQRU`^N9EUaql0&1}L%ICJ0b1tHtJ(jVl|=634Hs zUs=CJeD2bJYe(1-Zc9GzA@T~C0^fq z{od36;AmPw-+_&U% z5cF{We`1S=`~SOp#>4&pU2gJl|9_V;9`66|_LGPE|GTa6aQ}a|H6HH&?<_PP?*AX| z{~zxEkJsD}_y2b-CLZqp?=r@7z5l=RMC>)OquAb;_T1e+-_$=GBjC(0D7>&)$5_v zEaz~>FIZ5bWGR%hZ@G$IDCv!{4K7to^rF>Rh+JR(+Y=qe-WX28gjuhS1P1{~z7*OZ zS`taaN;0S7*c7uSGn7O_?m)vaB3WoU-Bw;y$McS0k;t%0Ry$k)ZNpPLg{T3RL+#X9 z*srN9rVWQ>w9ujK`HZCIi#mMG9Ubg+GjwKTo7Ly4Q7WV=^PFBPXiisR0*~P&NvV{n zaMqEsg#&7kh5Ey6a&Dmvz$cv4rbaqp+G!)vmZyx7_ppAQ%7C+oKHgqY3fX^1*O^6?ouJ0UlVL4|4h>nznwG?68#9BIJ_X;_`r4%bT>Zbe1 z5SF{Q)QTWY`MAPl?0HFPrkN4lpP;Iq9ZGrAre*^efTgs8 zsyQFgv=K}~m9B`3{b4gW(IJt?c+tTrt~HY;V{mmI7eY?~z}Ntfuviu{cM;0rFvWoDnsUZ@X8;GyrWcScWWB;Bm<> z7xDcG;{;Q&tONYq+6=r+nClBV#DDw54sKQsdPqO8obeFNP%W(um&OHaJeHV=Dw1rP zmpMC6_GRXvkYgGU$`1;e@o?U@dzns|B!ns_s%Xf!#|av@($#RtW88ROnKv4(Sy9sK zQxsEus@z=AVeK7vuox{jO{Q&s5;Q=9xeP0E4cUpGY^cR-$C!M9sYwn8sn4|?Rp z47OdW&!{F+GSF!U>2%W}ieiHn>ZPVcvzZ%UA)d5YRsus5K~EAos?u4Ii=ZQm<6z#S4jW(6G#rT%`YYlYJ~$hSDI zRo%dGThc;PZ2bI*4y}5V>=)ZWz?~QbsIT(@L#~90Yc< zm4oJe@Qg);Cgc88?oq0dY-CN5Z*c7jG%~T-G}{?c(!T6*&+zgZ{*nm+b8uFH#EfNNzHP-&z<0F3cHaO{&IXm)v;Wi4LPd-)Gu*8yZ6>G2uiulkWA%MqrX! zLz!4wCI#U3K{hFXdq)&>yC`?uHexlAxh3Zi(yC|`s0V{r5Rj*}v2hZIvjExF+@Cni zpdr)cf;ZM1je;SK7W%{5|9+x_I!OTtcWx-OL5kc?jY)e@zvJgx7Qzs7Y0&12`3^lL z25R{LNkN!C*UB0@L|{&?00{VYQ`UM)sZyE(>MGg^2D5%?!4C1-i5&)o zMxtdg(?Y@yaR)D^3ezH5tYFAARRK`J`BYD7CCySY0Uc0YKVv3@lu_)4#Yxso2Og?9 zt`>IMa#!|fH#dXpMye5Hn){7;xh+&DV6G@M*m14MFJ87<`{;=dVnc(+cx!-x=WI-h zR4h9zij|&R>IF=KEasI=JzpayeY9}UHcDvPbbQl-@{kpFipWqM7cD;>cFVa!Wrp~3 zmb*}!=+$dAz$ zq-4BtsYTl9p#~lJS}jjDCOLp=%GP9eIP%%C#|}s}X~PVdKXMH*m6HU;5tnHT_C$wH zA=3|16F!|cns9+Z2)Az`X^7&wgHby)bdqI|W+$VQg#)@?^1OjJ!b*e>Px{$u1yi&U zA`cn@Z4$}6RYo&Odv3ETv@fDXuhG%mZWFKRHL~Oh3;ki;J<*{l^zv9|OlD^lpRwCB z*-ixWmOU)Xb-vIFaWdVs;6@E9AqPF9LZ`x()AVK4%qu-HsHTR=wxpHxVrfPf3>-6B zZ59ByBKtl)aw~$TdTb>WjYip9yyF=E@QDsJf#l?L4{ph$StuJ;J~Sl1TI)gvkwlAB zpwHaoOhQzbI~eNu3Q92@MNcXPys3p;wk@PRkRpaa(0R~seZctF{c1{t zO-JZ!dOyVrRI!t449kPUKCf4bqM#SY)oLpwFr8k+58nE~i4Lr1Gd-t(^cg>0=1CIq zQYls+*E8v)&xaGEDD{mNn5+rsfXWJme#b7iWmI%!I-4MxBqW$pk#9*!JKIuKDCt%P z)#2FLH(aF<-~mcj1|nIW&WejVtbhMqI*g4*x91yh$u8+S<3yRROk!NZL9@^dG|-`$ zQEFtSJ?;R>gB(T;j7<49$&a*_qMPQpJau~#QSXc1v^5Nv0W>8Xe7vuVO+0X&uqm3w zY1f?N)W-b0labe@XZalH^()Ax%f79CGS|sAZe6C+UeCdUC z?u~C(V(}l?uUE~HKE*V;GSH|&qgGxV;U;g69(2ALq+{wU_8CYLsYMiyRZ<(thcAiL z*#+u+|I+G6wj#R9D%p^(zxwbBDCe!aiW2`H&IdfcRqcCUmadW+X_ZVx&fy+V@6ktr z8NP36^=`dWf96w|RdOm#4|i`GB^LkQrAs}6wqZR>9HM4tGF#si?f~Ub?)g=___m2a7e`StWfG@Z)Vz+OZ$M zd+E}i-H#VR4OU4_B_Cdl)QKwpuBFwv<;N>zvvLkEfMSmQC&FvPZr^;R!I>i zAFf5}L|y4SmR9GMpMWsdvRTEzd=~@d9M5-8o*#QVd%jyCE7>a9$fh^G{ib@c`1LPG zy%iFm4R5>y6nX4X|7__Z@A9Y>@}_Awz6}(0+{3?p>7t&|!&gYScKz~=CqQY(J^UR@ zm-g&Ed=V0Gm1J)6jmIN(q8|QjORICMhp&+T?fR>4grJ<`9{$9Bed6~8uUzKlH6838 zb$9XP=UIAqJy9O3gPAKaAZaX!5>9i4em@k4jf^!c5%p=V(Qw*r-=8z9h6Me0O7++SLqTRnV*OnuG`7Zh{6)|s6T)$puqofVMFrZ*f= z>hW5~UAoj$);cQyF%56npvdEX3lcqel#dr^!DMZv=~)GRy3kU$DtcGHT>;2wa>D{e z9j|rlrHfiz>#Ts+aufgWy|K&l?RRgzZ0)@dW)uHV$^*2(;rK9!z*D|QT3aPw=lPDm zfbf@!KrCDkxtR1!TUP9mp#qEw7)H{kfv2o6PDH>Xs|;VKBlfK@{+$Lwbobc4h=g+P z~`VW?CGwBQD7V!AQeBh6I^8ihUCtWN8V&{ty(QiyZUzWMv@E^hjC+ zC=stuex;&1QR9&`zWp=6LldQ_cE>~`!yCC#b&iG0%G4c#|3tm?xx;G8 zC1LnXtfm31XtIZ>^HCZ6{l2d$X;z^b^Gj8mzqP6f~x>f0;dC#>zb1}kM~Pf8>@=Nl3-TBsWv!G zz@1#Nq-3P@RT##X!)iLNdW4bY9s0)@Y4f#Joc6&DH^`Se+(b}F$6e%(gM9NNt7ITP zZTPskSbKp6j+=!$!_gEtz$4MZB2q*iIa6Jn-}3oKqPXBa=t{{{G9ta{sJ)r30|CZN z*P|ijwns$>NauLe(&H5nb)V>jIXIm?6DBk@nYu~DEi$Q8txR_Etv)+Uj`(h}C^;_d ziC1TX5Vuw6TF1O^&l6Hv`r72$)vMrWS)cQrd02%-t-$z{RH-<4NJ~>0ak^Ecki{TQ zZ~~MqdhM|lwiVrAm8-c1gS4F{xaQKrje6M?Ll(lgWzY$as}4NxJ4c6J4B+rWJa^fBS@sju}0=<)Aij>0iW*_ zQ8D-1*`a*uOejvpFZq44>38&g)>LxTyfz^FInLp82xYnXQP#bR7967AHg2LA-Vg8? zPmDXm>QrwktzxGr6?ii@Y)sI3HJxo{`ihX8Rwfh9CSapca&vX5+L{ZqEF9#oF@8`M zjbsu+3!$Qnve(js8Im6agui5GkE$L)0&X16E-Kvhjdz)AtE75Ae^a!utc`{@H!yl5 zIqDn$0yS#4Q`fo4d%OQ5t7Kr01Rp%_NY*QColl?G97j}@NcR^28ndENhDuH`EqTimZazM{}C#(B(!)z{}$^(I?B6EUdmy|cK+7`sJ|fT zUP7~hgit!&G`e<@84Ms7hVFaLF=e(oVCrncqf?ps)TBd3WLycVRx*JBF6BCTx_>nj z%-Dc0!kMcUKm+A@MN6g|R=cD@DGbh5Ehi+rRu{9b+0!9zVN2rwMO6oGc)b5#e{1aW z;l+R4d<^{X@bl0D%Ub{}S)RDPWLY6Lety_}VGxfd%TvefQvj8}oF&V-m%xuFk8>jd zi^mfY*WHnT`(3i!Blme+vSd@~2#xjBv2u`vW}(k<23!!kT}*fC9m*tH!x_>@DQcR~ zE4JmsZE|+){@3T~)oQhh)072^x@a*2-^yLZxWKOnDw50BCp^_^R4XMygz$`8Pfy3y zt~`Z(lNpv9l&9tg;97g#yef>c1E|(f>Q@zIiY6=_u_>$x{hS@1@78NoB3Uro>Xd_J~IqnG!3U!0@qRpmQ-pr>?WIZhh&qW8ouCU z^NNrSQl?f)u~MyDSGY-7OXiZ*3{>hbxrew_b?~l47bV5%9S0esb>u1spkQkrW#$MY zmwq?M%&|g0eQ++FJ#EkNqIc;P4r0FoUHX*F9M5frfLtnH5h@oA! z!GoqDEp$?)LAAz*0Y5RZlHLXAk3!Q#>Vr81skscQ*HX%iXpP2rifci$<@O=RRR@j; zGIPu$M~pIatkAn2oFitwA{}v+k>ZrN-N_ksG}XTVbLt%hj)MUAX6ATq@&9wFWcJK8 zErNw1K5()Hl(5}ly?}QIebuT$To+;>XNXja;k7z7n87*!zNb*qZv_nFgjscf=0?(> zREP3|Za7oYbQkw+F^$wYT5VQ>IYU!L10_qDE)J`Opv>lzaJQy&trQN`u7$9W#JqgI z($5koP$$|J%#0{72r>*ssRuxWr+0i*2%_u|gutoes^8lGzY4%YUwIq2{lBx@z5K5) zU%Vvmd~j!XTe- zUW|%IA7U#q~?46WAy#~?^#;eHyrIo%BD_~-B{&?#Vuv; zzTk1brW=ut{~vqr9q+hRrjOs=X9fr@5YkBk6Em`^O^w{tEZeeWSrSQ%Bo|38k}SCr z!iJhlCzLO}m$0z34V|SE%0lS;=!DKvLTBlG&$T_j1!wM^alXSY`}uJHxSz?5p7Xpq zI_Ev-yyu+fy)L`=`)QY-vZd$l?(A85@qbymG|%0Osil)`?33HLv}fH@s^j19K4lxv z-rYX3usOAHbI;>nILEz><6nM!aqHwJ?%DRlXYc)f(w$D(!t-{w_GabmR@-=YFZEu1GuDVUeq?2AQ8N1I%zj)8c z$6mIdk+W9?z`(gVW90gUbKFFk8hPA(p47;(`+S79XXGOve#%B(y1TJw`7M&M`y4)N&)8Arl#RV;cXiL$zjB{*T)deY`&aJs zq}4ihpS|(t@4xDlEj@R4WzW*Ta-Y3X&`w21`HrV2dn`h9Dr~J=^XvqH;XGS4c~hO?@POX-uvw-r)=S+ zyFevxrqj=H`)6w62|E2rH+{yP-nwzmUVGopG`lwgX7`IT_O8xz1L^<4-f`!z{BY0S z`l;J{;V#HQ@K;W1<-$4c6iw~@i#gs&>vi1k_0FE12D@K7xK+JgbnV0U?EI^KKgV^X zshxk-?@wy!xZjsQwP)!=zI@7-o_p=pdzSuHzn??CYHI1U-=EaRalgNOJY&B6`KM~* z-v0kN3);ewhadja;fEdi!=Z=n{BCEs{hRIn)~~m^AV-f*N-jfHOl`2IgvUw4<4Y87ZF(CL^A)OQk)shbgu$xs?fROOC< z)CQGYz0hpnhMIN+IG>zn5`6Nh+SS+Hd29e7#ke?Wq`Wr@0N7W9E7)2HbIMK$ME7s9GfKCF7A%j26&dgctdyDawPi zsX)D0feF$^Ig7;jd8W>%V!#<=1Ll}V&U0n;`4(epWZdBr#A%&Z!u1yRiXt%#u&{Cpa><0jK#B9G>5R z)B6byjSZM%;(xk7!Oq+UoaRrkJ--2`_Y-W54M0zQO>??G!RFisoaRrkF~0$)_Y-B2r}cE<)z>Y} zZNMB$j8mzB#jyc%$U~g&Pp~kz0jKv9JoVvo8*q9*!Beju8!*Qb_cVWkr#@_M15WcN zxN2?#PV*;t=-7Zc3O(JQU^KS@r}+~M#|F$16*#@3)_d#!OBZffIP%IPnZv(2{Jg{Q zLq9n5)I*V-ukSo&C$#<9?T2okzx9zVck3>j@7kNj^Uecdm< z{lQn96T7y5>`Ob(w%_)t;JxpD{W+INyZ0OyobGhmbwjg)n9FcXsyAxJ5y=J;%Kex( zG)wVHk_;O$9?6!HlY;N__U~PD<4bP18-2+&&P}84tSXHy^*B z;y&;8zpbA&eDe#xEBxyZq;GxUJbC9szkBcfL(=XA3T!*+I2J;D^*0RqD(A)*`I#( z`saN639pX6(A69lJ*M@udLP!yPubK&l?vE_oHg2iJ(lg-5j*S zRJ!2A8LZeHD6%qWP;|73U;|LaL#Qz+_{&EW{EFY4uYBjmRw{Jwr3;>MpASCz3pbLF z{q+^U{lLd>`gca!J$GDiDHaBXA{_%*W7FC^AGxP_)*M7kNX(ie_`jr&%RmOy~nsw#2<-NbZ^M5=u_}cw`ebewBkFC^y`Jb!wzkTD#OU`?kw0rk) z!ReYNl)D9yq!!_imGdE6qxd2@Rm0B}T9=1vK3UK@SLGhlmuo zr^(|WF074;wq2Qex6l`_xc`&i@af+??HA8|%;n4*-u$ZrX6xJLA9ZNk7~8 z`9)u?UwXfn{qdW>yW5Xm@~D-kKXnxR@dqR#b5Kz*L>wiuXwnSf6Y}t z``ME&e!%uQw@AAnb_^VSfVe6WLUp79b=-bN7-Ali&f-c!^{Q0YvJH}H+qD=Q4^Q3Z zTSm&)Z@K$dzk2kQ%ejAl&b61`@~lJ8dBNSj|5A79;wS!;*_NeU5EBLkvm?y`ga(b^~?h+iw?oV}mXJ+wbTrAM@2WKI-wW|GKmbVz{7Sgdd~? zTtG2_fgP*gH+ut%#d<~>L7E+bwyFt-mJ47o!KOpgS3Z`Ezbx{dCHy6qLXY`_e(`g^ za_nK>`Ip6S4(_{t#YK<2_!-hJh|Pk6=QtcUsfs(k>*7mdKR+vZ%L@kKTaYtuj~|1d z^R$mX>>1BuuU|j!qfh^gv>~N-s zC&Ll1tVB_e+ayR8WvAZBsf;*)Q?TX`AaD-Xi%o7in7-(G`}_P=um1G|zLgr?P`t}a zwmwmL)X%xoJ_K+6TNv20=% zB?~k>b({A+>v6wda~qHSkAMF7)}x;#yyI&Q|A-%d>z6Nl+i!-~RzG>pL!@00s{{ql zv1>E2_~y=guXys=N9YGX_9hywz4gc6`C+{Ov*0&(cxL8#mABvSi?^3{K@1WUJjZU# zq~ISzPa@xRUemn%mVWcK*83Ze7kPwAL z^7Ut*_nh~C<2nD9+h*VW?yIlrVlVvivFqOPvGp%VyC8N43Z7#VVNx*s?tg#Q!dLGT z{{No+3*wU3|EKfCTkZgPH{J4<=lx*goNrzBxE*O1#NZ#>{fZ?QVaFW-6A zkF4dD*FEdi=c3;cqM4Vz_H!4#>?!a0?l;nzpBeX+c0p_m6g)?Ca8mHQ?^nOeS3dRHSA5`|&;R|u ze7gL$-@Ww6yGGFec`@|N=8JAnp8#g`Am+7a@u`cnPAw*Hyz=sY(*E$hUtaV;pk$Z% z*!%zd$Lrqk<5yntfa@+O1oym`v>oVnhBME~N!Nl{5h!?$IKiagSHqXz`OCwLKaYgpdi2jfc)&k>>j$s;{qNrJ zpkEdaJ@o3M&Y#+Q>;JQszqfEiJN(nb4?leP(DM&ny7P`5cKeIl=GK31J!b1pn=ji$ zHr}@(9C`Bk&EOQkpVpqZcGl{vSMR&>krjFQd&|A$)upE|U9kAZMRMVj3*}oa_m{7| z)t~Rn(%>yv*(WYBbAP_9H~w*WBkoLgWG3-Y;``mtnpwV1(AYu5_8Vxf0 z+AP_9FzI=G=n_!>8D2ZuQ(ggkjKV&N+~9 z)`RTxhT+-g_9FzI=IjFrXItNXgtO1>M+jQ!Jq{$C9m)42oPBOTLNMZ=bs*tvbKj3} z_PPBCLASX3frPUo`F@16&+SJDM*O=SNI2WS_9L8qZa+fM56?W1aMmgBM>zZ3euThz z-}N9uD0*t6#jT#(hY*S$z2+_}3t{3iWcI7y`Y-1q;EP{!ka@Q*uiU)2yYkBI z53aPAUa^8L|6%$4hyQu0wS3pDZ){z^{bhSDn@OrD3^<%2-b=yufG~F}Vc;gh{u`+3J1?N$Vd_8Va5Qej&iwUE$)NI&k7d81PfbwxB)Y!0|O zU6{t>4`-%8*Wrs0U(4X5PBZ`tOD5feD7P?@MMYk!;qp-hiR zN7!acC@Ldz25!P8-&-P)Y8LbxNQLbK3sNq&3k&v6wkuJ0Xoa3t{kd^tksmDJ@ zKw&|lsl}vp!^3G(vWwWL+JnHxU~MF}ZM|LP3b9fL$q19+*VfafrV?_uh?g=@#3~t7 z8e>5|Y!hhbC=l3;>R>H>SgeyHv4SYmmb3Mq9g zQy>u+mcvz~%@0b`&$0OQDI=zJ8}(vG%KARYcRU;sD$B5*6W4pH&9*9Hw$hCkH41m8 zTSz+(oC4KMHfwalgOINm3VgWdBulwayBqGvT1eyTfrO@pS_+pl(|zjgH&1~hrjiTt ztS6(jezy;?wAf`6nW&LPTo)!1AT>RkDpe^aGBtJSc2i(KE4ON+lG}tdLqtJ};b>nk z*ZdI&l8A>rm1Es(wUc9F%(QZrKX4oviZwO9Z5P@Km9s*61BrOizNg#OK9MpYDPoQa z(XORurww57J5ykQHBC)W%&eA-!m%=Jx0-`uxto>1jfP@_sB_`ZQ1}H4Hmyn^FjHNMpdJtmuSyNz>4`rZCr0T*$z12;Tb)zR0ONm~s94{zx z63J#qJmWS;)N~(r$;W}S+PqM26!OJRGM|G9yPBxkSw1}!DQ8q2dWAj)?s02oEh0Dw zR-Sr*Dc`&&k5*ne1y;O9sZh^%`DU(2(;%O&mkZ@1l}1hEcsd@-4U2*^vI?W)Co)#k zQ=mS|m#c_g$fSvYOSe5D>ZNR}oG5aVk!_Wl7~Cpk991BuUvc$rQ(zfM;1$KTV*`GK z$k74VF_&b%4$_9ueX1T5HHjo3TL->((i@j5Q=r<1D#L28mrp8=ui%iJd+x-wRS!QXF-nGfgp7=-5fh2B1qMX zPLBaBKjb)24kOVpCcu>2ud<@7&}6PY$mpI+XEI_4aiXLTGIP7-DdX}Fr@%tG7e?b< ziy`EE%P$PYre*b6nGcg{QcaX8FR7CKz-TMS8LyoJ^JYS4hzdV2!Y*9G@p8}!azTG2 zRfk0g(uEX-g9PraMDloV#HYZVQmDX`0gafN5yxo;u|1V!xTddVJ=L}eBBMc2qQeu@ zT3x*V6qsqWtx+Zgi+-6*iZnqo36Lqg(@4fESy!wGa;!)8Qa&7>p6Xb9&J@Vja{Y!M zBa@uUq(*tR+?I-iM2l!E`4ENTY727P%_>=rO&J;)$YeYA%WUw$>fm6JKjlx~g zNpnMd&=`;%i&K(Hg7xvz)EzIWQ((%dv89eYsJ4kzT=%Pe)XM}Tt(Hc(_$b!yCb%+` z;Qe8B+NoB~oB~OOYE+vX+)hVJRM~=I#>T4_&eaL6Fmko19O+0I5gKHtj$`SpDUc`> z89^_=zAl!-Rx&V)1%*~nqL5FP4L8@|@uJ_$$$Wo$NN3?u$AQTX5m@0YTPH-9WgrBF zoJ?CB1Y~|>Gi)l4_uXMhPF1I$W4S#ALggWpjzE1(f;yh&%5Evdo9?h1!$wi4ptS6A zly8EY-_@yCT{<=a8ZlcfQA~-5F*218iMe!b|)_NFu@H^o80=_r$89%3P!4HMhjjd zmv30E8%Tx>v1J6(4OJ|7BAaQ4a$x#3@i`lBngI3iFyF)TnO;6y?Q`{*$$FqK$T%w& z%TcyhVbY9%5;{h9r%i7CXH#Htfc11+D@2-n$t;D^P$!*qQ@t{mP_+Q!WmFG&QV>F@ zY2~bcYYHTxL_Z%*h(n`VD|vjsTu1w(azYOfCdDRtag2%;R7;E>`8jFhNA zI!aYWbW|WRV6sE$`J37_^=adl~Xav)!O7E!DDMsvSG}|?( zE}3w{MYE%Gx;e@jxo$2=*;7-uj`zo0b~L1#v8>%{>slqKx*Qx2>x1I3hX|c|4(OFN zMyxhWq^D!YW^BsHCFSbSD0SN=gtDwI$1}WQ=@4VmDwsvYqii9}^>g5q+i~D=FIVOo zel8co3rfYp4LX;SBU-4QO%G8yYPR{7Uu!f-v4zH_-D)#DVKicWXJDu)+{%L^WQL3p z$!IuL4TYmMp;s-Ekd~}Qgm{5E-c}C<$ARFcI9(DvLed6gu2X5}vtaq4j=(6wBt?6a z4PgzBYet=^UtN9B^c^t>oHZT7f#GKAx?&1>DyFn!u2gU0(Euv3y>P146SbH*9dtIn zc=%fjuRg)@=kM`P(|3%u1g(y8DpICebz#`EN~}FBM+UK88ZH(xgF&yr2o1vPO}o`$ zd0MOSJd|&uoe@9sFeJg`AeR=rc%|MBU*Bo$fnAS2SD_4%` zoor4o+byWyiH_`WT|?FKbk?!{@Ui~?VrgL~w0YU;jo=^u_&FUlaBX9EWlzp@FK6Ez zM}$s(k9$TJp8!ISC(^~oPN*w9$I+j$^!FTRkj65JnBlT0st^Wi4y$>ntx<3tgH_MM3Tph6GKY;{eNR2P zx4S%6PX;;r=IC>$&)u9*J$W8?bAqHD=g)Ig@kBD?6XBfyA;&Qxzim1D@I9OVRyq4_ z<*xwOZOoq(Md9#XKF$;BmJ{i)DF|+R0Os~~JQ9bwR-JK)V_~=7NR>qKn1&Au)vh$` z@wd6>vB$bS#zA1Lr^Qkw3v0=>tQU?)t4G5i*2-W(jLHoMBI}SMQ2m7)BNVMWlA@|< zic_(!V-txygydVK(IHy8&=)KyeQLVp<5veg@6p!o(q7No%h@+adp~_W?*z$tPtcc6 zp#6Q~oPGaI!|GUS{5It5yH(@eQ@TE$8|-hJvo8ulKYP=y6E=1<;uQP&Oe>unP%K)E zi6E$)<{>AB%SBrW;+CxS2VU1}FtDzA#<7T7^}AeNb&Z6?XEGp-fE%;v znkJ$$-Rp~SJfo%iTxu9`I%b3_RP#wk7|1MckieWhpLVc%|M6EpK?0n48XM@+TVS=D zSc)&Ka4UCS{?_tqmmj*ET0UdxYfG;_^6?|jI#N9X9sc9tj~;%;;mYCgp+6n^$f0K* z(hl8c=eIjI?mTtp%AI>{|9boV+fUkl@b*Ppzu0=$*5kLNt$S|%eDmF#kJ~J5p1<*v zjkj++b|b%W&iW75-?ILQ_3ZlH*1os)#)Z3v0d1gUlo~;9UX3ttp-)G}Mo>|bCXY~LG0-eyb*a4;?&@9W; zH1a^6S;gOZVej0$o&tfFgQAUymiWRzXN$@L1Ui#9>_C$T6M}3J&

    GmtjDAiuF!3p^vIXEr5P`YRiJ?3o)Otim&g}}w+C$mxF{6*U_fq+TU zRz>hmc-WoKs^F2p@HhBsLjWBP;NjR6UjIyrt50e*zhaLx#HTUYoK(*=IUr^(s$_#3 z(q@FNAw)ATVT&pur&(0MVTkHXFTX(N$u7l}&W%G=Rm z!%QjB(_AMkm$C#(+XY@=PuI%>+At%?q|NA#8M&cl!9}k&vquLb(Btfb#;hMc$yr%O zf%#JPLp1t7vLBjJuIfvnsUOnYV{>fj4kz>Zb{%D6xuJ-aic`6*U{qw3s2#URI44~F zTUsXs0nFB>L#J(O(gocvc;&d@jp2f&{I4_d6DT_C*vEFeTv}uLilgv7yUTS2T^L%Y z%`AMexB9>*ehYKo;TnC`?-pI4pT9oG zLCn9w`4A`2*&D#I9dm>s9h5athU*NezEO_kCIU(@R-%?_WG14OjWkXtL|#ODH(h5h zVh@pyn)3ZFUdFhJQ>nH4PF`dyT&gPjG&Kp2&H&<^v@3glE#)U@ba;kh#;55*B`G2@ zb|yTGp6NA;`Kn*)Wu{a%p^W2;slCn~9dyhm=m+&#$9$>;)LwIpR_*DYlxbnfkdtPr ztyZI}v6L8=5IWvZO}uVfLtV69x?~+Qa%{(J_w0n=Hwk|*wo2_Zpf9QAlc^HvXgu1} zI#!a;O-(oD6ujB6yo^5dxy_;TcbArZi@vC%U9cnn+v#ODbtC@p-R+n|jG%Oa3SyO-Fz)$ZqZU$c2) z_u;#l-OEl=J73;;jzu!+Irg7L$|Kk z_~7QxHs81TM?h2X`776K=o{3=`ud00Uktr{<)Q1t_4}{yu6=Ut6>Huavv%v%|9~2+ zuU&oQYIgOG@K@nC!H ziD}6Tk2OB;aSgn*-9NNQ_H)H)5tHy(7geThQf=gUS|i0g(H;$_b(WU2NxPUWnLf4M zom+6M&l{N-kY&=le7{x6X9UY^Cn%$0J9bh9+K{>%PgG=G+HTG*;)!Mo8;qvzxY3J& z_lA}hw-~m?w0MXC@|_5vt1e>A6L=7c%2YHUhYD8XeK_d#(^2t5J?~1_c)@ zIzXs0DKrId>TRzcS^#~KIN^?Kw7`!-9ud=sSTsU;dT%<3*`fNN;j>CN+sP;{vh}mM z57Un2Bu6V#vA#%vTWOfDhKq%fOqKn92z**Z39+Jx^oZGg{@e#khWhbQJ7xg_L0Yh# zYB50->tQxeHga*s7&*<7l^Am>&hED77JbCV*|CKu8u@l4-i|X0%B3uo=O%?DX(Y^8 z$%w{dHLNA9D{~7oBZ+Ksg1JJxIOq-3Hdi&dl&e@gP~v3G78}Q`cFM`CgH`0n0#BBS z0VVZ{am^y8K1eB#RU9`pDznLq$C)fp4-&FeqG+FZ+}y%w<*|A!gE2T8ZGoKge7Mtf zI=0{Ib*rPE!i{}>S_>P+l(hQ$a|@NKp?1yHy-)`?X%b-v#TXaTvQe+uTC-Y)_P! zY1-haT)EOmkZ?a2R_J~QNP%}bLM~{u>kWrvd+X2U79Cd^CyY+Wts?D+jzlXx*K`b3 zmTHxTlu;%qQ#2~&M8dV#pM${i%Bhr;!xJqeHLNuf1XokU(e$EZ0yi6;v^Zj&p zXxCaHLwWGr1)yau=QDH*ZDW?>wY@RlR5+s-pEL??lF?kZ9Wo*U#beCMWNyJ{l}tw9 z*lM;F#j=9ij@x7D7YamYqJn^{lx?MJo0)vGQR+A{iN34n!X!H%AskwmhQdBG^m$fMVEX)$fkQzMd(()i}x z=1)HDSG%|_*JB2_fJrv!j)ziKMP$V3)>1A zOVsQQS>A_b4w*@?@>tPhqS%3XkP*bsD8Y6`|9pki7!d z5mF$9%QiOVlSgtK(Ii6l+U!`Vk0hXy9Cvk-s7z{PROTympQyTWBNh*f%E}MsQQ1^N zG2=~Lz=`ajm==VNH5n6{=>RTGoORWP)~e@hJkk)>UOu(gPrz@TWat%O-L zJt-T5k=}}DlekJcWshklA_%-cx9BomyNzI4NskVTm7D=MQW4RKG<_7D%2yGoM20Ai zS0@E_{l0SxeyVl53_)_ksaLEw=mOrHmfFoY8%|dGG+tDBSMX#P3k}vkFt@0?+Qc&p zwAsvLtPyCqMl4^zqo5zAixY$lE8+53qHHE=Y#hw1yxyzIDUXV(k|wlBsX47Dk!~{O z`l4R%kf~&7q}OU?qT-i!<}Rv{LNx4^gtpe4Cd_e@%yeQckgYt4;Vmny5Exa>Obof+ zjIDfRZc&+%^&XXpR(mL!i5bkO*Kex*Os|*C01@Lc-eO~;PI}PSN-O4(g_aAY!(vNggdtbikEb z6C(9mz6u6QE8dUGNfz3g`zRLNgj-N6rq{IwHmcH`J|)`aawbLKOS^jUNr5EIxQx}*>SB&DLAL3+-f%>MEMAf)|GyP7g6EiGQ41 zWCg6!AB7Z0nn7(@?V-4byG-u{>t9x?`hHKUnc*Ski2mG*+BKvhMTFMq>z3njNw3BH> zm;z383X-?>l(|Jh6;-`ijE^iLlCSkb9BC8z2-*ylCZ3oPWejs$JkDLcBmE>0&Vol205p)s>ALjhEYfi33s{enc*@d-2)*|0_;RPMmzk z$*cDNYM;FQ@2&e4h(1 zc^qv)U9^9GifD%jGMW#wAt}*wlbX=wQl83F=~g<0ghE(J_Q__XJs^=%@8Di%{r%?^ ze0{3dq9wp82@ zDgEG{z#leyz8_ryHfIKGqX$N4ZzA>LHCa90-}&2?r${8%4Y$&)Y_~+RF2$uhlPQhM zyjCXksa77|@(+}=(*4YmiF3by@#hs=y#_0*T$r-KdnuVvr;kuGCTUHl0 z^?bjCi5Vw4NwSriesK4*&VI0PNnYU04<)c%n$(+-I5i!DWi99{1Rv{KV8N{pLpcPU zx*=gO98%1qcMt9cyxrp=pd7!ya5>Je>fO*?zl()yPIXd+Y17S9RnXJ)BFeQBLMCH0 zOQda0Q%$0tj&w$(rYD`D0^&J!HlFV-T+~a(<5?v{3&;9^P&{ejF`??TLs_y_=(h-8 zXtGpnnrLt?-57Z+i07)a@qBmT+E_B4X1^6paVk)}?^mUk6cRe+C`Nl+U+9YAsOITp zwvGz5LS|rh4z8SqGi&zWSvVYNB#UGDR5CoKi&ey}C){SSWl;h%YROD4?jugIh^6{i zxL#4Tkrhqjor5dRy7~6PwQ%I7TrrX&Sf$2HR#t4v;(M-acv7p{osL?lJ|5K3mPlE( zH05;;?t0eEw-#=EM{d&NzCaV_#lP0KD8PH7XntcnpST^b%- z4%|FoR%qY6qCb{nKI6wznXZ^(hYsx@KUQ4-2Fodm2=&ySDn*ns9#TY9thR(Q(1`HW zMzKZ=jE*)aM~MWdAp`Z`cYw=CEFvxODMe3CDVv8 z60wTWGqPR$G|8dmj#)sH8l@gw23#Jt+%GNc^1PPI3rapUop`MjGU(ck z>cAW#g?bX)xb78uQ-uws1lJ{v*u2N;Dh13u;aL{sZH><;0%l*Q_-hOqa1vN7J$BDLP+>8-VSql1u4j26UTRWFVXxPD5i#@arDO3bl07&$iPtniwW+ z%9j!$H{X%-K>MVI6q&)JuQ<5%tOU-;GW5BHBRi^)>yDp&}&xLYT~B`Oge#2ixy91^M(-aBBiF&L+lnKTSAnR2dS!SB7L=} zS&DOTt9gtkpwIr=F)ra(Ke*+*)=og5S=jFdwYE%6-0t8OXWPi97xs2RM-)Vx;zwSu z7LtS_igj{qH!3$Z3Dd<3r#(3<|5>N~)WVVd>P~xc4$Csx zTK2&{2|glCwM@ds@L&&kJ)8wTv9Q-;W`QL#AJl_g;PP-5`1ryu z&zl97xX0W(*a5B%-&8-gu&ayTRF}A~sUK_ue}}WqM;G>YL1SOy+R*I$KY!vqt1I_f zc?VS6deGLrwoh$+f9st)Upl$IN3UE5UA6h1J!kV7Cw{ysZa;qW%*HP_KCt`hjpuIH zcCX#L<%YUJp8Vkc?#Al+zplUNq`r0C$>*PV&w79TzU$j-A6t9r-Y3?^D-VL+0zG~$ zzIMyi&+ff!|HZ4XUcGje1vd?bC#l`ruU-ayWB0T0m-p`vzX5*CE(;go%Qt_#^Orl1 z-YM+dW#xO@-`IZ3jczO)ZhNGMT$ma}vLGXF$dy_xET8CgSUwzyvsw(urHS=|m`vpr zXHeUv<`xwZmCCJdLJqZ=Qrn57+Hwb{QmJy)@38j7i#uE{nML}I*zVPHi`dv546_rw z$%k{9_E2V16Rw%4WL$!gr<&4<3plC#`+z} z#kZ}w1=tVI5+y=u%jHoaTuR29K2t1JYoQn}ITIo4v}zI4;fR>9^5R1aHjmkTK9uYA zYZS$Y@n+hgr7kf^H%X-ES0bIv6r41QO{K0t^R2FQVN93OX|$V@8Zyx9=tf8BvJlVE zlTt&-b5nd^&_YQr=UHL%kLOX*m|rUabugA;@MNDCCRisWWJc9!!%-zR1`Z!Ir99Na zjo9{&<`z?O67~q$@-i_Y(k*Mva%U)%xOQHovA8YEEKy4gkal=nf?hedn4oz!mYq(M zmghw?RKbR_v9P#ZJjg30jxOhM?h(sma& zYSpqO7{5DrlF6w#GtOf~2Wi?G<1-y}+G=W}WO3@+YR9Xf=AbJ^Gh-XR_1uE(W=cud zc5%GJae(17gT*Pn>{{G}>$aUSJBTJ-S*?yOVda{+MOwt-4WWY<2|gQ^{0NOCW8<8a zKvEU9)1O*esHkWSo*5%YXI*Tno@&s^SS26s6RI^-TcIgG?s0ULbF*|)$#V@hGf`88 zRBr9E`GY4cgEm_Z6*B0ChKIdk#mW->LcNHF$5I^|m1S_mZ>41t+>4xDj$tVy2_%&8 zuI*42C6Th5pt<-1oh&oMVv1-dhA|(hSvjditKl3K(8Twbg4(y8irFa@_2 z*tkw;{c6MMW>Q1e7YH4VN#M!>H#jlB50$nYQxeQ*qQ%p-nl4IML>sa0I4U?JY`2yPN+Cd zrK?h6Y9&=ZnAf48BgwZZw;B zb8zv{g5^*LoXgfEsfdJNDpgU-X+so^&5zHWux?=}J8dVy zU;{AW#7E^m#^|aV=Ix4<>iJ@!T@-r_RYu@X%v)lbz@z0_B-E6+j)REYRAwVmsS;5} z852~NMfPLkdT(rK!rI(L6O@x0sfGL^2KH4YE`k@(c&;#V@ zoAU!|796jQypA%-HN`R29t&h?9Bx+lPQ)ALw1hEDj$Kw1^fF!Bc*NWX2kvsk+}4Qc zO30 zN!_Ut%eM0I*i?2B^_XQ>DAga?T2oApnw{9r(OA+_lVyXbri+776mNHQy5OdgZg~`E zQWQGS2E)3`aN|O;Ozq5%g40cK4+x}Pm&;SnjD+;EpBi)xKPnT&wuBn30!}&2QG>RJ z!uB`jQ8BraK*ps?m}19G6Xb4Jlw_k;Z4Z;fOtzk*ilqrbnTC`1w!b?p5y^zJIlNq=@=~4MeaYM+FL^nxC2~V1nWX97h|h)^2p@`Q zl~gZA^sNw=4Ecx_O)0zct6=F8Elu39zU&5Jjkh=r0|+lY$ss$yGsZ&2Z!nu{Q~UNUqFuCn`Z| z$o9kL7V%_?=c4(hlO)GhU+?G$xR}r{fhx6XfMF-+#VjP0wq(xUpWn1&@kz#)vZ!qL z5(bfXIt^vmbJC?~fl0`<@HnLS6-O}hu4?b=b04v4E7Katm^H>aBgQ2Y;j9>Sc%210 z&ZCAs68o8!8sjS^WxqWyt6aS>0v9UzCej62{|?dA^F0jJsZ!M7Mx;M3n<2rC*ho6I z|Inl7@Ifw)O9QfvbUj~g4#%E~B|9z8!L%0DC(5;~lue-ZS|8ba+uVmn2%f1I`wqCQ zLQ{+|mh?s*uje(Z!bK~lWuvB%w7?Zg>FCBl+O$ZqTCOs!G{P}n&5Y!wYOOl%7j+)Z zkL-3>s`UyLQOMzY(A-BGjZYkQz$wBQrep8_a6?~#z6{ALUtEy@8?X2)-q0WUBx_ZcOQ)7NR5-Q7_VJDHOh58T!=(dQV%vU^ z00xHvEOExM%s%}h0rZy+;1X9sZ|MLoaTS<{0W5L$bE&JqSUP}9Tm|~l0bJrL&<+Dw z;!7`e6{t%GaEYrxISgQlGq_7V5_S&*SmwNJ2|~yl->ZJ)ljNlXkQdK9T_k|c(g7@S za-p7I)F=D1rJ&}fJobxVgLW@{Pp$o7b;id%+sE`u^1x{C)TUz6|;cD82HzmEp<>@HM~o zPwybSE`zHiCr(AciC~(u6|a}pbCDc6?nE+^3WIrw92(m!tMg$aK26lLCcg01-0R;t z=^Y?P{;*!J>{O?riH2(lJlRl7eITExLmfeSLrO!M%aMGoU<&m<*b- z7Xc^yIU|}8TImAO5~RM3E3JB=hDv><*_<|dG{-Jnp64#q$OoY#KX^GB0=vnMTg3aV z6cDsdGbvU;t7t+98)F@@`o_?)3W-(-S-2d}_jApWpZ;*1b&?IF9LWk2XNK%bsAYze zHr<(q3dywEXSw1e(U`KdvT*sG^P?Y}27aQT+-zAC)pV*GnY7wDkXM_J_rjrpkt-u| z8h6PoQm1-xM#U$I!NTQs(d{7p;OZl1OC+=`TzJ=WcCRC6OJuVwT!I%lQx5J4oME6C zm&j&WxERmB^rRfz=e&lJ1JeGwE@O)2?-a5D&aCMJ|oLDABW#Quc z)z=U&&XNJaoH`0-iTsp>OL*yEt~v^4iR6@pD`B}{#Dgnmv4BRtL*KCRK9;yRvT$pg`{*594t&h!_a%~57EYWvZvOin;BPknEs?RZu)lNj zg>rBg;Ab|_Es?CUu%C+tg*(rx2@DELQ^`Yi*rT9gFDS)0gt^zvdY4-%$v;U z;EppNXHG10hIHenB1f47N6&llgY^#X0G!U+=@Q2_3p+ioon8j~&D!Y_XA}$jJHMUY z9{8EH(dNHA?xQ!qxprUh z%m4oTzbS#oKj7eDfaX=aZU@LQEuO%3k@lWho4`f z!5{L%o=@3-es7(z7k3Avw$|y${R@c0!{Hku^ekbLh%?0~$g^{KEGE_gt5eS>8sd5Ph!`Z-M%AzaJK4NJ7nZVSQhYkM1Q6}i&7j)1Idd>rn z^LqTCa=waD3vfTX^5{ab3_Ki(&Qz|BWhzdEhl8>2q{&jOTR2kzNkXR!F5{m;(#c_m z$a8T~@EcY}o(={#S+Jl{lPb~xk6WzdG>W-ct&vM$YL!l9IX_8zN+DMOmv|LkJ7X21 zUf=BIQDIDibn0@v>ZUDwsuN?w&xAV(*Y5&S0lXsC7Go+t`{>8?*Jaw)tiRs!;Gwhr z`sB*WGR<<4Dfc)bL*lp5Uzf;hJ-)v#GX_2cw8)!5f1SPEUcbA7*e+6GjLbfCJ+;R3 z>BbKi4>!U$eo4DELLfItKwh$w8kQdO-B_wbxhb@4GF3EMC(lGoIT_>BBFndlVO-Y8 z+D-1(Q?{HbWh{zm=6SLWG!4#hm828(SUTn_awJ`>ovHQ`dQ|j48l#w@qZBXY#&lMQ zC#&I_FQc_HBpoArE;d2BdIF_I65Iw~M1TyRJv!)=kGuBZA)r&Xp1iiQ3^~IhopK?K zK>WASOP5tQxZYZFnQ8x+UV8Bg?$?=HZ$R7h#ss)72m#Eo;NYlr*pV;jk24|V8|ja; zPJI(+RV+$UH>m$tL(kNsO2;9M$W-#g$v^;vd0+`@WK=*89@mR?x!p#SQX>iEJwOb< zVigRg_X}6Sg-&cRTUeAUbupe|vxwn*aq215T zxc|4GuwCCiweveb3GjW;+c&YzmAzZd^Z@n}CqH=d`7>sKm8Y#dbmf|@rvirm(B{uK z-@pCM?YHg}fg->gb{@0&tWA021sldjbYo-vBPZ9_U$SnkKVbJYbKQXT{rwNGeQNEM zYyM1GV9#AkuH9z!^Q(Vxeb#^I?khiBdDo2lA9^CxSmcHLF!Mofl}jobFW~gJm*-q6 z?;!~=it?!97g)ZSNp+{gXqHDn*7R*}x3tA==N6`dO*2?#(D5hTM5Vy?5s#Nuql8GU zjFXCuhplqoG<$#<{1)A%E###w=K05fhMDiXMy!Dg;fU{NI)WHh#>Ti<8u5TjG-(kW zYdVz*s^xBxKlHJTMQ&+}Ig4tSry7PpMLWZWh87vh2E5|wGTxX{Od{Suy`rw-WP+Gt z1!K>f`>2V%Tq2iX$cotq9RB&@uv@jT8j-Y7*AC}^SY36DVI&~J-1^eItXf*uant0W z6>hs2Q6?3sia50nGbwr@hjPbR5pe(`fZO%%XdgYth}Wa8ksMc3zHY@pHx4mPp!Zim zYSb{Jk%Jt~b^(cJS=f5`(iZcywr++&)v%N_0LS?lh~wFf!6@Ax$xMD6A6H~0RxC25 zUd{zKO;+Zl_Fc9_I}HTfb}5ySbVy4K8$zy`oLD*9nwA{dn<#*#y2WQg)ZRQ_sY|=O zo8}dK6g9_gG8&o;TY$LPCfYz}IMF4_W`pU8oR+qz9eDm6OIXZviI(v(r-|;y9kyQY~m!^EDpqF}a0lWsZJIdZXQ)?LuZT=if_)zB-F;vaEmHMO_ zOHZ<7A)Pe|#o@;cM*2;2gh;tYsoXC#Ywg_nyO*|j*U}b$JGY3^eW?j}uEnsN^(JDb zFUrwax`#=|BpQ#@qe8B26?+&E^VpG=wm51fOZX5EGkv>}GL{yjW?G&$4XjUh^fbdJ zRVvC>GOVJ=5>*=OW@_YTOLHoUE>^0F>X@GN`$ck+NN5>ugd3r*osTDnl{}`3B~sAc zj9svI=0oB#7V{x-8H=MKG2Bu8Nyx}XOuaS1l18F5bRw~K!7Tf%8Sdz1B}GkAlX#?b zl+fQ@!eTysEMqaBK9;e#&AiG(49+N~K9LCPqJCu)G?ez5(*r7iyW zuzSdTtV&0@L4p{B(*=<)GbUT>fc6;!yO!x8o*$>ZCg(IWCF$YI8eF@q!F2u*x*FP9L&YFoxhN^HjZ3Q!wQrTq9bsRL5Z2*m$ksTXObqA~6>Y0bnv5dtZE^YCQr7fO5 zFWn{D6*IR`41)G*9^I_95Wwl<)ZKAAmZ&3sb*dEHNlXamqIr?P)ZFdnY%sE(vxyc< z^h}LvWhjLgr3&p>5xxm@+JO{Q#cpCP5zXr!euuxGKS#7PmJyWFyDXMT;UOK1V4X2w z&K)-dw&&OTgHlWJt7a(5-0nF`TReMdi)StUh9fi4%CEixPxU|I==1;yvyL#W!7Vn+$|NSfc`V%L=ck=Tm z|Lx@8oqXfTm!5pq$tRqgfVb}loy?xR-^qKQy!_-XPOj{KfBy@BweLOqZ`yy^{_2!v2X+U}?B8|&miy4&5B9#e_mRE7-+ME7yMNB!6Zfv&Gxr{{m*0E996LY4_iE-@E%)yRX=N?(UOzAHMzP+t1&=Zu^njgYAcHm$u{E*!HRI+itIK z{dnuETOZ&0hpo45y=v{odMmOdueA&dEo2 z-na9XomcKWZ|BK7kJ#z&w7`B!YzN)BYUegPYk-I0E88F2e*gAc;g`UF2r@2QSb-Zb z3*QGm4c{5wgZ>Np4)i(bU!ZqEe+m65^i1e+kO!%d0A--xg|30_0`0H-_sVzA?V+r^ z>)h}E``iD#64+l|gP@h(b#H&iC*k|S_Y2@#9}nLbzHb2E_)PeB;olA5>s|}m%Y6d) zvRA`VI2yp0-U_B*Du6GEz~FxYeBn!A5+(!qf&-XXYzB+*Jt>AmX_X?nW4SY}do&oHA2EGS;j{xeAf$t9AJ%DNtz8ieE0LpKH zPr;`GC`s^D@Kpg6A^1x8$^bs}S@0F`6#?YG58oBOYXGY|@a6F30W7@%{vG&t0$8{; zd>8mG0nDv|-hSr*viFDY1m7uu@qdHw2;VV)_fNogfbS5%`>w&4!IuRv8iQ{S-#&o& zz2V!zw+mqSnec7l+XgU{gKq=hCV+QG;akJE4&WW{58n#D)j9Y;_?GZ3&%yh`w}5XE z!2Nf_C*hOl;NQag@O}Wd9{}&cd*|T&;azw)fa@;21Mi%J4!jL-2M~HLyajKagU^OH z;mxz~ZNHG=4S3@ml;CxEJ%B%s!fWu_IrzKqD!dxNA3PieKj+}JFa$#Z{Mzf`6?o+w zd>woOK5-5j(0@b!9l-ziDj2K(>m2+F^h@ZM=iry1UqHVI;HL`E&!L~6gL&v@(9h1n zEc8?8rvd!;Pe4C`esT_e9QrZz<8$z%(0@Yz8Nd(S8~PFSqjN9>{Sf-$Id~291Ly|< zeE)UO_o45fgHM6J2Yv4xd>Zs!=(_>@yU&8D=sV}&XP|FG-#!OF4SfsxRsi4j3+S8B zHv{;VHuMeX8v%UN=b^7dUk~6L&Ol#-z81jO#h|Z3Uk%{PNa!ojR|5DaUkBVoUk>04 z-wk~U`ceR&|3>JG&=&*v+`B?wfW8pGKYAGSdFb;2eDYVo)cUyqKH=NYe?b2cz{h_S z`YiO>06zBR&}X2}1n^PUL7#>`9l(b_2l^EBsQ^x%27MCxWB{EHL!W>?5kUJs(8r;V z2XOc(=wr~w0@%AB^ik-e0n{G_{X6vU0qlMc`Uv!q06z59(1)Q92eA1Z=-;4!3t;{A z(7!_e8o=^DfrZ4s1hANc{u%n`0A^9>L(qo;$o?4mAoRfiCUxkapnnSB{VeDM&<6r| z-}gfQ2>oLKqZafJ&_4u_yc&8x^!@;b-wM4CdS3v~#Gv;=?+xHJ*Fk>|{ry?^*U5i{ z9|S)rfbqA(O}H7r2R;rKU?G6_TLEjQMgSuU%)@*DQ5&wq^#ES;D!2yM0(egbuENy- z-tEJ11+E0}DibJHmjig$EL?(10ldp=0kd^6fR}v~F2IEV-lhTP;d}sZp}{#g7r?ze zI16V3xP3Lu!CU~>CvXPNoP}@yFAppY*#Q1Rg41w1fIqFmDL56t|9laggp&dM!Si4S zW&-%l=fMd$5x}pdU>c?a_~kSlhvNbK!oR^WI2ORq{Stm4{J;QyOn@H%KOlhbjl=hc z?|&A)?dlgp?}6SEz0Dtt4&|g7+6~OOX(3_z*2k<*zf!+kYDS+R467)vsjRE``47~w*LjXVT zgN6KG2Jo||q1QvN58x-apw~gK3*blFApiO=0{G!)L$8Hi8^C|*L9c;c6Tp9Z81!oB z)dBpQcSEm&UKPN%{1WZ~2Tn1g~Mw3}E4x&>up77(nj3&@-TC z1n~YZfSwLLJ%IOl2lNNf9|VvnLQjL97C`Ko&{LtO1`v53^c3hR0lc>aT?btkz^k*+ zlc6UE@E)&(o&-H9fOoqLoUK1`p8sDuaq_t*cR>C>vG?x1?CzI$`JHb72L6-Vuh^!y zKE0LSe8(oT@pl`I^>3~}Vts$@g=@s>2UbP+JMbgm9q{B=th{O^dE#>?O5knk(K`q2 z^+QDrAezF-ag(o=w5(7YuvyNi#*f#s`jt5g5#%DHQ-*;wc&b_((^L(Rl8za9Fr*^s zX(!K-KxMp;@OzpZ^{~h|Cmo2hX!c|vk1|bm^0H@g{h9_u#rnc1f6*6Tkj`_en+!Lz zeP00kMJN@U7+$=Uq=`YEsZ~N`ahmP-xEK~UUOuz%GtZOTPKBqso(#vN zj_c;8lm|HZJJHA#(0}ICULnnu!6EiQP-A(tFO3dbzzY!ffIhu&*5&cq9$6z6iYP40 zg<^HUS?&79pfTcsh;Gvi#VT%*N~9#h>c{$^kPaStMz_`W}A2&AvO|@Ly|v z-?zT3to1E0HZDfj)Jijr26SG>5?W6X>;$O7Y9zRdisUF89zfRVi$kUV?~TvNu2w4X zV~ZS(DrTZB^(Q{qAwxFGC4*_^Sc$8XN-UopC*{tNB_!kYD}YjPi^;~jZ`{4h2l>{i zTVb4jd8qMSH}2aN8rP^?VVts|#^1Ve$F9)0M(qmelmQySMJ*e@dE;Jv;<(~ERV$>^ zUa0B6-?%sL)3iq23hA^PYWisRd`bgN;O?1?ciy=7E|g$cr+~%ylnT}T z#*Mr1qPjIoSd34}P~CsMaX(&Ew?+{Q`LqMnfs1uE-f`p3y#L_dIyEfh({`xq*KgdD z59nH>iiLdI3U&S3jl1yyU2D{_0MmODC<7N>ZM^-)%C4N=vlk0ZJiJCl2LPuL!gg(`u;Ymv1}-p_Vmj zS@5SssO7CU?%7bw8r3X}(+bcs&jH?Y<0=I!=EpT^TS%uk&^VtT-+W_@_su11RJD*# z%RtjS{JrVMnjQ!fYgD!Xj)2XF3xtU`-dNeyj&QL;!#dS3_)~O_1~mN*H%1d0&l+_t zoKs}3<({zdOY4>~SZdrF<Pw0+1KA#?FyS+qt1nN3ImOEoBhQbYrM~9*QjtI zpB90pxy`=r#+n|m*)^(M0Goy8;|FZ^zuZ{a)i!(a1mHTAF!<9K&C!6S|H6&Ygf_cI zjf=(k|DTK;fBOEbw`0+t{NL|;=>Lg`zs5ct&JRsJ?nZC7Ud$s8-G6ARFh72?;%H+! zvFgy&8m=}!v$IYmmNWV_s>rN9+PDtD6^Evth+uO#_aB+M_QLHsgDvPo!J41?dC?_D zrY=T{IoJKCl>ZUHT*$;7jAj0TL&<4C|M5F4mCk09IiO+X5#)PCMHj@9=;e(%X-@DK zRSCvD1y+L0z4G0%l~Euem??032o49%JLB?_0hb<@LS`^`uMXFYI+QT{S`WKRAh6P| zl+#9$H%!PPYEZ4rR!pIpHpa|7lVXHsldpMcicqE*Z=|Nj4JDxm`LZy`Rw!&rqNUc2 z&MMBE4h~CwopQ?0-R_(C=}SGnC9!TxsKXk9G_l7NG>>Hzo=Dl zV^@6Pvl8+6+;Y!&`$IwG+_3{3aULT7vEIQW4~0v}I;RQXVP^g`{L=&NJ`?xEv*mOQ zFaw28P+VB{xFCCRsQK)VbBqf-7eFp*6FdR@4BLRy&qb*+ub%&I2}6#bx9*Hc);-yo zH|MMaz(FZz1pZ@wZu#RFZ8G_E2549rZK&*BtY($etf1t2T)u$`@w;WYnUBZYyfaLy z(i9TMq>@%N^+`t?y)D4#nw;F||5fTfm&`kRmBWN4rguv-o?R^xd>>nsv6> zBRf#HRqZ5lbv+M_(+E^{P}sFZL?{y}I^G@y@Xeylyy^3k(eCNsY?7Zw*U1~NPZn<$ zZBm8*)o3%HLzhl2uM@830xm?4MZWz>iW>{%=Ve0X)A1b0g(?G=P408jD~k`@q@>7Y z!Y_Qtn(KO!XQDxIo%}qJSm}5@MyE-yY>70mrEsa4eaJ_TK&5GS|x|kpvy(U$!viXeNi}xy76kMO0 zj)TutW(IOpWIz`0c{D0}dEaYMSgUly%0Tm`1IIfZopN)>du2bmPLg_Sj+b9{ycMpr z!Z{YNf1f1hI7^5@;Tl^u=5xwJ={b%wM87?d~RK}kg>8x(;7>@ zs}~y96>y^*M+f&`2S`xFDk6vB6BiCvO#0U^GF)6~K8w-@5U$VjeeeP!Xaf+$!%}GL zl?j@=wq)iHGOu$+_%QBUFa7DSB!prfRZ_}nR zDKqG}Gxe+Jw45$bO1>@4c&#xW1p}Wh-Kg%%S<@Fej&z>mtYNO#e?7UOumqw)=_8aWaWS{8C#l*WG*V0v$L5lrGQs6z|pA*-4rMRU2mahyl1M9cmAT zBs$D2RvMqovYj-ld5oZUQ+HvNZS{yzacCq81E-CbN1o5h(_GT)@FTodPeQiu7z3Yg z6AfzCmF3=mVCso+Z0xa!{2)f7Lxur?Zdx;I*)(Vwo8X*eBsj)(RkC#lRkMVBz_JPg?Vt>l(+LN4qEMhq*-V()xe# z1a8s}P9kct*-J@IZ%_*)9&8i7+vO@Un=oiPm?6_ZBakPZ2f7VvkYsZevD23MPTH*y zev&|2flG91cR`gotC@A0W~0(W*>-A{bQoncgbBZtAnzhPOW0#tVD8l+TD#XYOt}my zp){NuR@I$1J$(OvW`v8}dD+RwPF{Na(c?#s{_bdc_>n^&oI7{-|7zdf`yYGe?w{`( zJAbyLfwS=H)*o%jn}4t=fireO^!K8CQl~cI9y}X>pB|%uiw}9=&SCm z4PcGkZ#MyOvNnJy6r%;xi)}X4n!Mg0M@YV+l~X#wE^669Eh9&Si`Vx*$p3m+1dbZ z*9{+k^bEin8~Go;T_b$_rDp)vm|z~hT^o4(C1(KEIOuV^S8G1{_#z+W4{Ef0aWHh^0m0{0BS8bWV- z2%NP6-0~3EX8_jt^mw}$oj&@wbp~LKW$D}9@IC+j|2bRH$j)na^4ss+hPU3eWo||9 zeC?gjKl#>^FFbzTar5X;j=t{b;lpn`+&C}~c0nz^)ZUx+nB70weQfs`o8PwygX$E| zj{a1%2G0N6moEc6@MPx)9{A6v{^O}0c&Z0JNj;!Fri05!KYie#;`lN3>^|4exZQ2Kp{m6vuA6RDpKsXU9#iIcKE9V7KPHFWd#jh1 zhHbw8av=Y)(Y!VHo_4!;EI!z~7mwbYT>&VZZUD`5Q(dDr#PthNq2_-)&0`Cv(NNDC zi{#fY`!4BGP9s210kmh0mEh|aS3^A)PADjky)0C=#+v7iJ8wzZqoJ}jR`+h)gBO%Z zkG&KqOMz}&V;$?p-FW{o5$Um)gqqe^FS>CbKB(!DP}3UgGB@tP`!#`RIRmPm`{K{I z-KD(;{rQr8@)t`@%zY35nudVZSP;2!FJB$LF98&UfYw+bx$$7E4~TorpW^`rf2-?Y z=W)XlI7i?f^FkfBy3%w(ha2j+)#bDYbZCz`K*v0tT4QbL`qh$9$MSb`;V~QNo6k{e zETr66-xG6`^q2+o%!A$_n7{4Z|uS)@~wIUdmTTU~p2 zz-+w7jJb|`pK-ez)IuF$n^!e%UjKi4e>ZaS>f`L;I}XO+*QfqH)dNrUz*9Z&R1ZAW z15fq9Q$6s1LJxcuzw_e9_-95hS!PRSFPycim7su01|E|{<6O7k;@H{mHis2v!;+D1 zlpr5gAiqY|CoD$0Lu{;ZU3!XR&RA%H>Ln$_XOd$T$A;s93>EMaLI9aFMg#P4F~KM4 zX>b0#6qi~U>JE}=r=~zbgG2ytMXZrhP@h45l|-g>WnvGK_0a$ud3>ssZImRx2A%0h zp6MvJGdxB~%}#N|B7JD!Mh= z+GJ)lEYnlAQ}XNRtXNICDILkhF;f5+!mH%~>EP)B#No8%LL8mdbycEhO-4eoWoIi*S3aqR)!Zk=I4;y#LPY@BF(vkKGwY-WQETUUvt% z^PD@oCx3AAR&cMucY~Y$zU1WO_SL)JA?||MO9PPh1`jY)0*?;+dYyY9WzubG{-oM-% z?G^S8cYhn)%J;I}=I*n1{&MFHJKwP*?G$znH>B%ocGo-E#D4K1E&8E+>ZHuT3S5gl{YK$%u4Bv5s z8QmPC<*~Ao`k@8zvTHZ2s?%l3>OgC;fu^mbj&3c4mo=4#YHCgCwnt>CQnRNk$?t~3 zM@$; zj8;-dKRYC6&Do?anS$DEIDzfZ{7Ul3D?;*~Xkolpfd_5enYOAlvXVTy9g=s+F;@>L zLND2tGj0UbO7h5uLh?p)I%1nVp;}%?E8$*mHTkcF9fPx^P=o_-QtM236QfHj$r~Xx zgw~6kg}F5Ac45iy)>cw~!Gap|dzfntrieCGHLc8-D=VqLYylket3%2tO~zB2GNfK< z=B}iUzBm*v2csrc^BkhnHHfz4`76oy&Z#>=#U|KMi!K*ci*D*Gsqv6nqUr;>XHRHy z(in5);%W~qgupw27t{rB0?WRJ!sV&GQg-y8h2&UoS}hOho;hkM&9=u~VlCesV!_C+ zKC6gcvsIK@W(i+yEuRw#FE%lzT2gyFWS1pXbUUlDG#BKkIv&w3jO(H}CgONT>?~Z-{zY zL$_9vpHt%v*PBghBLiu>I?XW9O6pk%1!By}OfCz+a3vVyu zB)FW0hZGG!o_jNNt3|&5^I^U0PS1UT<}{hlYk zwc4oK@o{b?_D2?2O0HQ}C#2Ygslm8o1+CRX8Z|?5Ge9-F*W||%W==Z(aP{;O4JWUX zA+v}D4X2YLRbvTGSRK0}kB8EAs%!+7fC)ieQ#$lywP!@$ACe1Wu?S%vY>_>w9%yxX zWlN*a3&}fZaiW@{?iE`D%&1YsYVvPiki%--Yv}`M(!_a}m`WveC3)m`7Tpd@bg|pz z+ZCgX@V-&yS7ZN@PgEg*W*nTy@{=Js#;eWpu*2}U!J}RqT8%s!SzswzwvAM41J3TuNOXet zRwpE3rxz!ri3^(DMOoY=d8u+q61cg*QZ%NsakmcKS?4DurC5?yL;r^mi#lsJ%0USd z34F#(wB~9Hz(V07g5ss3Ga9MBV2UGob!d$Icqp7#S#i{oEPg82MhFD0HeNrZ#s(Ec ztF-9~HfGV*fE89E=R)cd)Txe!JR#Qo04Cwi>UN$ifI|l4X}VuiM+|t49>8{WrR->C zAskW^tM2!-CJ9?^Ip8X*2ecdtXHe790v{futuarM%xdcBC=}im2eb@`w z@Hm~0pTEMsV#VY8-f+bgj}wLbIrmpBKThS6+4ILQKiK}%NIUYL==PhVhnwowkMI87 z?yu~A&#t!f1-oCm`zbr`-}q5*pZy2GMdK&i9|o7*lH2m;N4BWVw{Cv-#ur3C3|0)B zJIMA6?|kOThfaR_FUG|MkAH z|Dyd*-TU3W|Fritd)>V++zAaR!h*cc>7%*^>oh&l*mlJtSHHYPCkyggZ{!}to2h7qdPg{z%z7aMl@j_lRxDt@0qKe13@ZY;tA5sWaN93WlIk+Aw?XspgspTEG; zoEfH3ZwCT`m~wSWcUH&ks2d9J)uWC z%5cn#83J3KMTBq6O{rc|MkBlk^$2U~V#Fn$^sONlZbmqKnYCnM274anUoxbxT#yrd zuimYTgDTJ1MS?BktJC*r*fIDRt4x%c<#`b5`XnT+em97wL+Qf?!{`myho-HW=~L<@ z8PIow5c9kng_(uidS$3GHCt`@_yq5cyXxxUjJ`GO6SO!og33fInyBR?GwhP->=#1m ze1*~Qfuz_RQEAMI{3WlzKd~UknSfHKXt^PIqv~X+Hdep8MgQZ1oDdwCK?92nIKSS& z^oF?7Eu#N^L0*w5&WF*KZVdT}Z8yYAvgLmlVwcnwO;h!mF@@=-!>g-bQlhU}kT)qj zfH)b}2PQ`kI3K%Ynd09r$Qu=T+%ff*SCx!j$82+}T{`-27UTq8W{BEMZI`TyFHe2{ zlGnF~7k*K3?Z6Ze*pY`^bKEsYt8I~32(Rc6(Q8=ascfUvM5;7bFPTL9AvtTfpPMV}tF z6dR2pN*qt?0b*eo%dYNrXCb^JG7d+LVAQbdVp*i<)tP?eEg?BonyDgMlt8sosNM+L ztD|(Zw!qRUA`=UIw{XWYTJMN;ZS^3<7Q)+_KVUo@Z%vzHP`z*Ltd2&JS1yFNwc!X> zJj11{+7vG<+-h_D!caIzx{`z_y0LmDjr{A7ykW>=@a|KshzcQgn)oHL>l;Gy5(4!sv)7Vm?g*yP z<|SYf3-Z>qN=Xuk0yz#@MY zlB06P>Y}JXLJ&m33cUIiJMtq7@+R+Ta7m!4&a^TBi$wD3H?nBBMA&SxHIgO;m@#Xt zua>>l5h?Orq4cs!TRmthVk+v)I-IoH@uJ~kXcOTZ9Pbfak(l(9se`Oe?IQ0ErNa`X zu?-(2UnSc`&E9J4k?&cMH=H6h!UU=#mulTMfj2J61mVRfHO%IO9>cyy3?XXlTfNox z8@hFagOOngk~wGMGdw3zgO#K7j1Wq1f(?m*;iD7@kD5bmbwi_JI@Rc6L;z`|Rj%?f zKC~*A_{JZG$UEhcqhgdYpvg?bLftVI4rA$75k@;C;C80f%WTC(JL_O&&7H}9r#rLDZV#k0D`iK1Y9YKn>PiignIX-#4+ZU(w>p%5 zYDg_|O#~wpohnfz=go}OuNiI#MPN8sVCpDD!sgf-W2;9$@{y2yV4x^nom9%C;8Fyv ztd69S_lD%0($Y;v1J^An!3brz2Q%1YVUZq3-W5V;%afw%mP=vCCVs9cjhD;ODyxH=1n zgy~cr?Gi1IRdL@Fc)g7@RwvVuuU<&6nYJ^4t1R9^Wu{)X%=StHh<@gxrM2-)0|wY( z8**_9u|s0@>r`hUyw$B%Sgm3z;8J(G0TzER@uGLk_y0eC;~9~yjm;n3{L0Nc8^5rj z-T9F_|KiT^$uFE}C(k(kmE+m*^NxP&=xdL%hrfULorlQ5Umg6rgZ4pm|3B{c_fPhI zeox=~+}&T@ePs8GcHXt~bvwE34{m?Qc4_M`w_dZ=+IU9vSE4V8-i^FF^2%$I>gcy! zOY#)KC$a~cTQ6>Htlkls%-+A0Hh=klwjd2ga?xyngwkMV#$-9>yx=NlKe-BDc4>^SH=6iu3yfVUaLH(hYZnflf7=4@-nPImx>Df%>(*y}a($DZccs83 zKk3y)pPvcM@svTkRhV;^lC&?pQef($Szj;kxmOBYvg_-U_vhTUz%RH`;F2d_kMQ9u z1;#J-A^67I?3UPJaWLo+RYPZ&mUBM;wgo$U|x^GbnB;q`iipLeCeB~M1L-S6?aR|;J6WR`2F4vEu3 zTPpSf(H$)9PkhFe0+&4bdV!yFrNAY}W4W<2#2K1Z;Ia*ONp>j>`RpqNE;-)y0zd02 zKUo@+YyITTRru1&e&Y866>Fg?CkQ%9wkp;IPl`VMN`Xs$qFyWTX;%tdGVAO2zkKGE z0+%NF>ji$sl>(Py7k2Gtv`@cM;L;#pFYupTDR9a0uKy18X;%tda=hyWe(IG1m;Hnx zJY_Ow+f%SN7cAXRm)03R%s)RN;}kMQ_l z>nkH)7I{l#ofvF%^qe$BSCjcz|3+&b`cTi>|#l^fr_^SZ@5aBTC%_r07x_+Q}a zG{ zXb<$z1AS7s7*iu3xQhB>e2cvQD(a>F{=KWHFIsKncdww%Ew%FJFQ-n%mt_CtRn$v; z@)uW8Uz|iD|KC;AOFj6fS5YsG{2yOMz0@avbQSfI4Swh<>LvUB;8oO1zVrK$$mXAa zBG;^*()CmiJkXJuA*Lw^l!e3`oTz_EJWUTck|0HSw>D=9LlXLBzcSc}0(0bcCZTBqx@7WMeJr z=vCAg?GK!{xV(*ti@hEBhqb6bb`|xK{r%6Ys4tFWIKadvD(Rjyw6tN48#a^uDdi*3*x@%@1z=r|1_Qy=0F+s@zfT ze9zI-kKcXxq20F~{>0%|@8%9$JHK)G%-!w1#NOW?{OQ5Zo=hTdI{DIrSML79K{xsp zciw*R1;?fRzuf=D=r`_v)BcN3-g*4}8$T2MAJNxseBI_(Z8pK$!Jq8>-#f<6m+Tzw z-`#%K_V;gl+okKXnZ%~B@i5|uH90Lrx7ek105#0ztS&!%H>YUSfzcEaT7ogZ~+FFsY7i#Uz)X z@>!0tGu`fhvP2< zP2ilb%k6~T0lSUNelEUA#W#iM!$?Mt3-quwlG?*wHlFX)Tc$Ja7zvo`4I86o#nMm= zgQkrV^|=dFHEB|S;+x|D#AhVs{cyq`BCC2;&mf|(O zVDGD&h7OIw|?>E{#hs9amM-%+!^A;R+E0Iq%`p!&^n|8O}(rBSva1;+BP$X?> zwlHmILZBfTCc3xasPr?jY0(Z;7wg4Hw%;G5B)wnCsX3U?;RI4^lYNG46x*TGg%aoOqQ6;{3hLl8)&GF=KbO;GjLQ6Mfu($Kb@WX?3Tr3N}fn3zZREZcgLbgp_DIeBMS1>R##0p-@^Q z1flP>gt^wDaW`-}R6}=um z_`wCo1ZxP0-O;8Qtu)A(`Py`ByH!4C44_J$OpmihFAJBzCQs<(eG3jv?+=^9d|?PD zTI8sv8c7UlT2*wCt{2mpMnByw#m1d6qNY!Ne!)@9k7C1?-(;mkp(QYxlIYATH9R*M z6uG7ZjtDp0R9dczSnEJpa3C-`sHFKpk_w!f2^ zV!@&0+TCV=YScmN77U|Ji38XwnfUbgtyoS|a0) z^Hn|%+ltZ3%zQ$ygR&aqVlx;HOjSB~??T6cuu~vtzM9n#ct0PRGrO4<=GQAT7N(^4VgwHbteHee8xD6S;zc0PY4Uci4nSj$Q2L zg6>2~q_uj*1s=_I`O=V(@Z)jV4kzfs=D^XB^uRQ^?ob2AlczOIvBWl#1zXN~bShm` z@FaBffin)2j3@Hdw9#nhtat~u`m-GGk`xZMw^~j%MObcc*r}voRb) zygqGoCp|2y({iCzZGti~F48W8Af7`S>CCXf`#XyfL`x%Xq_EARWoB}9N{bO$J5$dk z*;s4pa}`cVrCcGy6sV508G2MH%`i0rOXCEns!?Y7DVSS3X0bMbVY%wyLnSsFv|!(7 zYdez#sv_M*tX6Zv3wod{y-cSf7R0g5GE7w&wuLDiK*^ZQ6pO9h{DK1-Tkx>wbkio4 zo|?ma8Z#?OCEicC!Z7YB-IAI_5;>(>GNOO6;7F$k6K@S-5Y}<)6=P5bM`LOPO|&s( zG8qgK zl`D-wciL#+Z7A6q3Eb3;*-i?nR8hZCpmrWva2S{Zsx$P!4*9NR#V1LnFe$b3;)o(i zr8dIL4PBaXNkro9-RCbjv?(gmMy!)@*jCTY&-!A3XAl-wX>FnrN?=#rY&B740zUes z3yyKsHR*&a<=CK}YSQeKceLWLN)dsG<5|8;Qbkl5imAR4eba(NH3&OiAWFv2FER!J z)wR-CPZfeJMqq8e=hWGJ%`EncT{|i)@-ev(biA?+&MR7yPU?cJRt0sK=S52`CE_ha z9d~Cum!eCp=Pp!?V$#TWM@Al#vh`uT=EpUXme~BvZ&x%@sdvXhuTpG`bsp_{L4t2Uq(Fk=29iHQRH?PSv(PcX`4LE6lM}yNPZj;34b^0| z-r*D~MUsP*P)zfnXqk?2_{Lin9B5oliXFOK)@F>_H~OVvGAXq|;kkOpi%`1YqNNFy(uM=3(1Qf4oWkpqb_Z!@r&@aF!=a9f3E9(X znvTJpAk&hNS)8V5i;_|()QB}DWPm2dRk0!(Ct;pnEU{S1ux6gq(;zyD^ucCW1n(o@ z4Cx?_#R~{9#xY)DNa^tILI<6rBw4g+glsn2LDw1&%pQ@?wR1%RjxjAe$yj+%v!fgH zcV2nMff9JoRVz>_kyOO4-tW{aj5!{Z@@mI+ZFD3OJ;f`QI)cI<4i-A(bX!JRLsp(J z+SK9r2JW=|9Lrl)O0tHMg2iVt>X7jcwO0sR-mOvUEUr$GqL#zDir9o+La(fi;Sa45*gUEP?g$eFwFal# z1Dz+Tl}eBkTKLHiFLXd98i^H)0w%iMydUHPY(Pnd;}#go04^hnB&Q4Wgg3wQngvG< z>2hkCq!1^Uvz20?S|uT)v_OV$;IcF{+hfLx)kkjGKH6Mxh(^|GOARI|Ddj?{B9RVV z9DuSvHlHSuZq{q3vqU>L<;wQK7cV%ZSS+be$dV+D+q6%+tUKujq(nhz)tx1Iy4W%c zRk9UpGJ799<3OR2QKxNCTeMrN!;mT(0%d0C7TnL4iw0j);7rVqVRAgp9CYUQ|7}IT zFLL;;d!MuQZQz%u{{2(zfz#3HcvookU)hh|?p?r-JmeUvp!enBLoa+*qL6z2vmUY? za3hP>H>{yx$+`soo=X>A`p83^>x}w>C+Ln1{`TcB2BZ^9@GktxnN9@o_@M``w|EH9 zT85!5)I4NM->0h_gq+yq_O=$R2UYOm;3e0p4Y%G)|ywvc%V6 z3FfCg$aK_k-4I;ZX|(PQvs~g{hP3R5=8$b!(mBVqLO~aR%w-G1WG0g+ypZcRhLADW zVPu+=!nFyTb9;BChG^F8k_u&0vwI1N6X=3Blk|HS<{9j8hU`I;2+79S zl53rc&vltr1Cnm!hx4WbKl~c|RGItX<8E{v>$?6XrW^WU?te=^T;qDIRet!yja#22 z|NBP(^T}Z1f@94518$f8IJ;dUpS{fOaIhF-*Xb5Z4;Zr>jH(P+l6TU0P$Jqc-3HYw zN(ppIS6vd=IS}$>roXgSg&oEgOXJSHL@HgH>Vxu#Qv#=i4uoDEQf!ILEBaVxY1X8w zsBO)ZyNo?E@KKxQbhA+%<$|Hz7?X)ng>tiAIS*=I+|2Dj(}CN4opLJA-R_(C=sL5` zt+`!xmD{ax^V!^P*SWcE^>nuGe0X6xyZ=JDPm&|KAiY~8aF?a8OH(08F5~gDL}||3 z9||Jpj_0Xt9Q5fwX5NpS=hix>&GXO9{ArkOKhW+oaZfy3IkN(VPf%RA==6eY&f^?z zo;PRCFfQ<10J%tmp8$S_ZNSa*;Im&83d+*Pq+YgEUdbhM$UAP{a>(vr>DX?-_Dzprm^STLD=kmhXt`ma>U7R&j zsM_{&4%#+Vs+o6hm~79R4x-Kc{r_o^{}#DJpS%`S?fVK)rSIv7&cUxAJb(YI_I_*c z1-q}5?W6X>_nBW840eUB00*22TMNdo`m!aE5?qdBM!94#%^WLnbyA7tv*V=P z8M1_Q0c4k+DU{GGTQgV@r{qApc${+qQk{eR;}72$+p&qIjN`>V6U$_KP4EE{?&IY&u9_LA)X7VgfsW3= zdh844uV^n=f%jT_Sm6ErIo{v^>(m5NBEw`{O9{#xCYMI63#VlabL!H?f5Tn z*5^2Xr?Cdk=@QO0Zh>FMdF{9_a8~Cy-}~Ad<9yy}FvnSVV%)dZxkWz2c_G2Le(<;E zF#r3z=Re=2tO3?v0=vd7^p}9W)&dq_-!%vOTW?-+D(28uPu|$HFuyNlM(_^Ksw^CfiElg3o zL#;x3tpO~M#^y+W;}vTlwU>|vu~Mp5&MIuKn4e90Ef;Jp$aEpm6dY?biPhrCnHOV> z$s~`gLVE4!FOWX}3~A)XNS}3T&5=I$iP7J9c!)J-tXI#IS<6tGNn%i9DixJ&Wo>hY zu^GCY8(LXeLEXvu*ck%k=HO?q;z z>d)F^G@!S&c zOYgVW`Q{QrT#QmIG=N>%yB+2Pp+{t9O8Unp1c<6Sxo=Q56qGN)TJ+Y7m?2Q|Cb`~ zjclavy!p;I-l6V1=Y94;kHrmX_kCHbAg+j`2X<>yZj}1cMuuDk|6qBB1TKFznjf#9V@gXuqH8`jGHOi zjrZ7^VdXVdp7wI6Z?kv-#cNKA4;d$+MoUQys&>~Zl0sG2Mv|_lhGPyMFs{R(bt+*> zy^0sti}raNJE6u}CmYMRCN*(P4SV2DHgyU%NWpGVWB6P)D9WaQYXduDmCm6)E!3!{ zG1rWFv09o$drVOk(x_H)V_+*GEkQ=LGJ*_k(k#|Tp~mBng&KpNU9`ICQonC2aI9I+ z^O7*?q&$b8Ovie;+!Lf^leG{ZU$pV)`5~jHH+a};&}F-oo@yFq!5TFn!A(M7?|c(f z)o#S4d{Co}VhDBjUx$o#MwUb=-^tJlxYIQ@%@`$<=4%LC&Gh3Hb@K(-LhZ?9>e;WHkDMY4-|srJI+E)W9s3%OOsI{$xX2RVbE|aGZ!iw4i6bKs9<6s2a+!;~3QOn)wpOXG|T^^{`VnBO#+wWm8~J z5owRc2|nw@&6a5zDRrj0XwSj?PC+7S`OKi%@fRAme(0PrS$3#)BBf$5SIekG8BdBz zFJVZ!5eHkUHE=@>Ttqufs+9Zg^VKC>bo5h!fIqg>=GH4W|!!DY1vw(zkKIy3WHHkTY$C;9ot+)zt8|m6LM8v#p@shB8DB zZ$ZoyhM=+U&_fj|5NIunHz#m7jt4&zG7j}YZ9JO6Ri8CT5vR>+!nJxiDxI2f)vOWc zGL8zlgE*+be-?6f&STYZJZ$2zAnwUzJ_AlhAX?yn^Foza4xKhKwJb;%xf!K^kH_I~ ze0QjkmzuqlO&YkufL-r(aN)Ru*0{pVMWB>9pjwPs$02*xOG^vJJvw9rM@ztV_mo?x zI`>N0%*QIdY(k#qv4#d-3CRIB6XJG#3}@mC#)G4fkz-W?Y_aAvn2vW%D3b&G*aBlx z=;KU*X_g0>e52E$y3^^oSH0z&u{(BT4&2v<7?n!L$an}*v8U6N*!Hlzn9}Q7Cdl=r zN> zJx}!HQW^CV+8{Tnlmtai55*2sHPB?i#EaRaHMQze*r^-87&5l-fKanN$Be6ll_ABf zhgQitnNQgbXBM-JiGsQEI6v*28_qjI#`>t)<5gkgwfr*E$mSit+$M4c=$SysG!k&C zJFQzPo9%|9aroCEBe;RlMV&@Jo?#%RFKcZwo@HzFbZQe+j#i~A#1pwrB?*U-W9NKY ztpuH!YuEZ6d6sIUg@i~q`#QMXM~jVy*|f=}{46~jq*dj77|)?rvgKHOGJv{5pgOXh zaBA%dqi2jDr^j-Yfi?!$*io!u6hg+GbFV71qLFeSN3i+pej`S-K#j4GEx9ukio@3`KVy_TPe4G|h zPRzzh+Z+}X9uk`B)-Z&Mbnbcaa&D4S%~CxdBrz-)diSVgt?1QoB;{rYS*TYqR5%Q) z2ZeJ+44>78bu5SIGa}DV*<3wQZ}H%cQ>9?Y-C839`9cZh`eEca+6ozq)i}#n*kZN< z4sVt`X_gi{l`Pqb4=k?GH`*D#-hh%yQV5|QerL!C379!#=o&?*O(u(XOF9*cW%;T( zGRvqEW3ZAsPNqd;be=lILdL?(CZX#l$t0E_i#pvm>;^c!cs2tZz5krCV+XZr z5jNRsY*;MKC=>Ga5ucsN0@+O!>ZXGgm0~&82;C~$IA?5$g>oUS$_m(}4T_RysDU3V z!1)Q%@rqV&g2S5c)x89H9&(<0&e(`U9WG6g3fNIS#FRwOXgYAd23v&BXJ*|}Nod$< zmor0?+x?M{F&<^vmRR1-X=^@|NW=7Sy4;q-X;%OPVrR?82IRk0~(h0G8c z*pS%B2}LjK782SNAvnXCr15N;2pM-j6f&k}vl1{TjE+;4YSU?z#n@=VjqH4L1U7|J zeLr9;{fS3g=iyN}XEdlOpHaC!LG|kOo;@UESh|@MgF=j}Xjoq+z}4;+pK-#@*?D=$ znBylQB2k#AWr5`NRQp;~PK4iS#QHQHRwA17_MTdueJ zt%*ezGcn9B$t5&2?5Z)kb#F&&0vol!B8JWNA{dfg#A^r}wYO|uyn&rkOvJ(d6$iF>jETI7oHrQGD8Ax-aTrnxhQ&p^!-CiM$azQ<fzX z$2VVbC%@U){M?N{K7RMcPj7tf27PA-+!Xki(O-;y)19}Uyk_q$dxJaQ6TQ3t&HMl2 z*gGyAf7;Rej$V88k|W~ivkpJB_jmIu0*B8!__KqbJNWv8-obPBU%wZ>?kyiYKRftP zDpz85XOMCFng~w9firKCk;P}c+Ze!BaS|)Gyha1gPp3|P|KBe-uw1^WJF<=vb*j(; zS81EzwrsWx$0``^usOO5b}^$o!OO`|GLYac zzsFjEkv4R_A2&(63ztPDE^@w%7@a1U&dJuOs;HwyyVFWMJJ2%xNHu0sS(py7N#JX+ z4RwjG2xqFXj8cdbP>GW^79)1uc(!SR= zG=|yv$%T$&0dEipk?$sa3lR{AD-IAzss)0I6rHqYRA#*?qYL$}x*4AGO~jl13TCDJ zBG{5<(_sD95`*Fh|$X8gNKcQJ>qINST==9?tYCx}+guquPF(W9AB3qNZ~Y z39iE0%x1r@7C!uKzWSj1`P|TCYSjYif zCWp&s0|#x6@s2D}Y~YS8mbAq6%}A(h34w$!O^vq}GBTV_7h?TMySbK)Iy5HZ`de^I z(bi?CM#)-m=RDwK!{su9gZ?_=EXDkFZ#)zxHI*2O)GEo?*k@%O92YYhdfr^67Hsl1 z@G{cf%!IR5leJ9yOjTEj)1})E3&;iSQLXm}TjuLIUWmp>#;LUzf(5XIb~x6bp=$X; z#*uR8EH#Ipu-F0Mdf7zT5%;k!Hc42$#v;e{Jk2yocpW{B*46f*hG0OI zlqI})&EHBI{hGKbR!v7@F>hPv>@~|Jt*@76ys7l@`h#c7?SF;t6ORj*`P{@K$WWK)$Pug25Y$n!+hARzpi3 zzj5kgvQ?Ha2<^ZdzO1g6h_t-Hu7-*@1eXr;xw2S3V5}86rRz^68dB-;nLBrxJ7F>6MUXET1qjG z#cZ>Ov0#FC#mycIYDlJv?WqH039Oeh>PTy<+y%qD5^>eq-fSh|AeBLp2qb#B8`11Z*E@U9J{$R}&#EZEu z8FTvW9l;dzqzbG{C)j!3mk&(kq)F7=ju$MMe2MZm5UPXLg0!*N^a_DOGuOhx&UD#P z@8%lrM7&dpNGiuOr9=XRUMG_JzTp0H_a&=EB zA||NoChAR_+wV%IsH)x55pn`kSy+;ESSiJ?O6PwpH&rwqi)yj7K31ftRDg*j8ikIj z=LD~$swC3mNuic3R>+36l%D_mlDyPfDaEgL=jGP2DyJowx5#$U!B`lr==ETFPoWj9 zP|A8Ujc~1iIqD3aM|FjuF&$#eIw3Z{{@PVJk-?lijar?Gi45kxHFx|Ns$hrok|J_F z!YNC=h9VjthYCSU3*>G$RkJLM_uPR<0G!SGn+;EfW6+Lv&Nwy(_JDhTUYQn<2Z-{A z!87ZO0m`4+Psp4PR*0we=gC$nr@NZwe;b#wLolErpK_LVU)s zPUq1uMu#uTV20xSI(LG$c3%2p|7qEdffmVh8?n-TB-ZK~tOmhW7?60rFh zmZz5PTDoefx^%|U;^KXapIq!LQi}(G+yCnpE?98SpbLl1KQjNh`8Ul6U*@I%e)cP( zM?ZVY%%6dW|E?K!W^DR9)9;@yPLrz0^z_sZr#`GYZmKqQ=G5ZkeUn#DwkNHV`%nCA z;@XMxCtMSUs(+=vQGKyGpgvml8`UkU?Q;dbGBfdB)XiH!{;VMQv96OPAo#J?`F|h? zPGp~mf}r`37aR(Lk7=EKY&ZCrlv6malXgHbILXTg1;OAXQ|(a@6g5XkJxIZGK0%O= z$Qx=Hf=sP!6$G!8Jhj6xoP9^up*+@cor#Kqpuv&3_FxE0`|^l_AkO)vECIofb&xCp z!H?CIC929<)b3c9P|hEBn=Apr!F8l80l@$$GZ|X{-dMZjdO+~`?I-WSp?SU_SfbV$ zwUqnwOIf)Z@LXaL%tv?2I+Qz6bMj8qs@#d1oRD=WKQUNb_g5g?F8L77s@(0Gl?eb} zSvFX^TyqG%vh`j2E6N|c&QS;9+xa{z9&Ya^G(La9IMv!wIcRD5p_9U6xROr8f6kSwi`h+SCVS z2?$oZ8`tmDASt!2%AMMU`E?bFukC7bEg-J#TZ&7}kIKK1$DF6Spa{och>xan_5G(UKFX&$W|GoVGd-?zO^8fGU|Nq~^ z|9`K%7(kqFeqNS<*i;r|3FWb1|6BO~7oU@B4&g!x>k`UcsP)!`IJtUA(gDN&FXu{t zId7J?$9wtz_wxVm<^TVO;s2K-oM0|jR^{mm;ymr|lmGuRc}j%1_#ow)LtJCL8vOqs zm#u<0zxiLo|1bLgF4g6#W!P&?H}sGd;Wy7Z-`%a&40#L~>-y^Eh- zykOD4xO3t63twA!&q870jD>yXADX{melQ=MKY8x?xjW}RIMP=J3)QOXSp1fo71C!NB)8qjY z4^Mny;?jxNPiQAp>buk*Q+I$_tcp<0jNd>0+3}0V!{aB6Jv;X8&9A7X-;RHM#2@$m z=^vv7M&z#~hjW~cdqG<-f(!*Ix$46z^B{x{&8G%i1 zu*5y8^ae{@qe>6wPaSuTDm|PLb=)zk^l*~Xar=lYr^B=*+7ZZxVW!P-+o;mR(Bsso z(i?K}T1S=M@C8~%mEO?r=24|L^t)+P=?(pE994Qlzn?X#^oD*vb5!XK{eH%Xejje` zwGqf0GKY`9W>o17{eJqW(!+*~|I?__8~XjUQKdKZJ2|TKhJH7UD!rlKiBY9D^t*mk z=?(pkjVisN-|-RsKHPw(jzHd!24-A0s`Q3_M@N<3(C^5o(!*wtkD8~54gEgq>n1kz z`^Yxg(C?a2c5mqSQ%05E(C@3GN^j`*lSlOXaFdL(8s9L_pESY_d_%vVII8r9em`MU z>0z_Sj~`WfLr)$zs`Q4QTp3k*Lr)$%s`Q4Q+&QZBhMqiTL{AP^a?}@vZRp7zBkaI7 z^yJZ_N^j`NBS)3qFoKU7ReD3eA2F)*aFdK5KC1MFem`tf=?(pU=%~^g`u&hmr8o5Z z!6W*8*z8ergKk5=A2h-a-G+WYa8&6H{eHmM*r@UwT5xex=?yKoFsk&17Mvf^g2RTK zJOUZr(1LTLN^fYv*}eY%KZaNQf5*xZ-|t@kUxR89669<_6Nmu;83Q%wUjKi%MfUpt z8^036@UlF<*Z&_%UpT(k|JM(1J;wL?|HEI&UjLuiu=gFc$0s)IefRqR!{5|i|9`ke z_WJ+Wh826S|3Cbd?DhZu$Ns;V|8GJ~j4d88cjDBw;2(Q`_O`&@7TBf*2F-p|N{!te zUx7$2w0$PYq14!7`qNEP)Z@C%a`ZwZzu7$JE-b)M&Rsbl(QvliZ8PhL0^6sJA3o<4 zrCypgUREyT@>P&fcBh!&MJ&PQg({!yt|!XfnJibz8+>DgAjP6`cH2a=&I5pQljmQW zYhKI*zfBt2ty05mW_YEZ=YWC0P`;8*^0|u8IL%Bd;8v!dXowkv5geTK zU|JH_>(DLJ)e?XZwi8(8W0gW&sN+E=-)+SJlX$^F(n2p&Wh!ab)Up!gfSk{Jcamc} zZ85-Y;|z7Pte-FSSX(w@&1cQ+h){I~x;4OClWc~(`l_pHWlPl>pgqmxi%q0L`@${4 z2FUpxRF3lT6^7N-Q4a4Iy|_hTx>~X7Vov&Izp`uKL1fI@J_D7)froB3@F4PHZ9ec| z0Td0qZIf<^U)$b+r*z;MKx^faLM|ic^p9$evLA;liENo=|5v>GaSNvGT3G~+4WkCHyL?8s#OY&~njT11bu zRMsp#3blc37k+msY+I}Q<0)$(S_j!5SghD!xt6CFbS4F}J!LZo6lJ9c1J3{kUO(3_ ziv#bH#5hbIxIH%Tkc|WH>|K-ce`iL2t@x{t_@obKS59i|Zoww!srnCXhRqMF@ZWC#S+1MyA2 z^|&MEHCs#OW|PY|0d+QK?$834D_YY9-dl3`3mJPg)%JiPAnf+H4zk}w0=|+TX@Zo} zU{NasTa7~8NaTWH3*ae9*@}dB)mF87$cC#ISgp5_o;BGtTVsKcw&L#?kf7jnRk8x$ z$v1hhaJY+fQYKE(w=Py){?<#ozIBK_(H`5~Y4_~6{wnRj{z1R>EjLuQWXa&5K>9PU z*<-dfn{-&a)^=oxv@X~nDSM72x}+Nmo6Y*zZh!0MXr=2$%C&BTjXEMmcQc-e>Vt%# z>+L!4TGF4#JPnd}G`GJ;xmga5aC~+e{#M&_xN04_L14_%r9nA(+^I+e(L_I z_~bp4%*0(2LG^dlUez5c=lD0qDIoke{<*NPpUh+eGb^;FPT_ci_TX947v$MUGMoz| zses*5Bncwdvt-k0A(O=1&De^K$(CIecekhShO!!jOuI2hDO}SPV7Bi$@vU8zz*RtK`exVvsV-Lxbf)i`bmvc4uFv^Ak%*Xs<{7){U~C}799 zGA2F}4&>djaK+(v6=~B33vk&2h$$W-g`Vz7{*DT+7O1H3oKd7Bn07VB9I z5#GZlO*YJ-qzy3H0*HF86dUeN%<0x<+D;SWak}_&mUkz^ZC}~04@RAxTs_dF(X=67 zt|(~(oooR_y;jPMEq_}O{P|>~&1zi)$Tx!*=rUR=l?~l^mB_?xoLx^Dm9zmOTL4k76*Abi zFm|0+@aG&%IZSmBzG!tIjj%HiNm@`=UvIYrRzn7=S|x3ul`Vj%*Gf88$c921Z8%OK zEKak0FX&1)@`0v3lsB02mY9PNH5?7p!8(+*!J2FVM7>rZ9^EQwgHvS-AnLVJENY_> z104nll)@Rz)&R8iRfFE%W4$gS9E$oZol-tSwsrc3M@buKWD6kbwPJ51v4A;5)=cga z;Qg*AT1{`+%*CTQqGylV1+R-qI%9T3Ln!(VPLVBusMiY8D-KV{ z@b^G&)hz3&+0#aSK*@J-vTT7{d6vx;J;;eBgEm;bvkUgZZEZ%=5FCy| z1Sy(&mT*3fZJ$K_!v0CJ1=?)72)?1FjqwznevIlchO!MQMKd7Ptef#vkfgI{)rSRh z%DAE{m4Z#Wr|IUgyal78xj@;PDsrAQN@2mMBUS2k@l2Pqfs6p#Z+b86pD0_vrTIFY z%Q!K9b<%A;q4j%;q%)QPi9!>xbXmZ- z1mg>%=`t6O_dwQPK;? z$`(Mh>%1oylPqyi68NjtcPn}d$p!T@^+7sUf3a90MW85CNWhiX6`l{JjoRD zHOz(}3}@pLwJgRLyybu{>1>i+$`J}H>4l?Z3m{r{rK7RvJa(cG*P9HzYBbO!bS^w% z%+@lEdakPRXwhCv6RT9K2CI@@I7+qvqGeZDo39sdc?~pCuN3^KD`kl}oyB;75NbVJ z9B1r$woZVoaLp>Gq!*5qEr4j*J$u!4VgCr(0*IDf*`sIv7xoXAEr4iQutzG#%s7(m zcC`t%q<3M)hNFqL@*a1)7i{SeTfQp7cv`?)K$NM8z=!AO=9I6$26^*#ryHg*N#HxarY? z-~IRX*SR}Or;7DCbv@nz7yr8ctC!VVg8S>=a(SUqsdp2typT`j`FumXLjnr_S|Tk* zPUJzTLL$lMlcMo2#g;=Sq+g0Q%JvKyDPK?h)o@8Zl@xEqq<Ef<`*E4J~E!Ix@ zOTOI7H!|S=qz2e^2_{CyY!i|3Ke+@m=m{KX*z^+2?bZ67QByGItE@$W?3z8!w_IAJ z>@HWB6z!u(cC}Ma849eh`;iv4fVo?;77LCh$WI%}RrPjLmT|K|Bom1Q!#%pWY9|bi zpo*d5N$CW>p}yBAxu>8HMZ03kR5%y?Jd%)X(ZEdRsuD%QPIVKSaonQ0A`!| zuGt1+hqnFRSK(}fZ#LV&1WJJJgV`=_KHEU-H(_U+S1CSY?`*ScBKft-+24X?Eg4zu4(+9OVn;3G)X#71+`AVy;OP@JyDcYn62cd{1N6tb5|Yqg2_+iXRs z7NSmYg=UTVV=il2x5^nqjZ&x7)%$FeyO-}aam-n%JMt|p<8tf#b*mr8^lKJxo6=gU zIM0z5W#J`Yw!y$`1LC@B;%xIa5Ksx>jcxB?DV%Mv&1M@2S8wy#0~TOtud;bSWw`6E zIHIK)zhDE~Rl@TQ2kFn7+f?On5siu#u=cf+dMe)pE0@fRk1oD*(XjB-g~5Vm{;v6R=XcJ1bFMmf@a&gnGqVdb*U!Xf#-~3q?VI}J z)cdC_laEbaK1ohIIPvC*Q`J9IpQk=n^(|FRb;$Ua$Ft*$0QeR7VFpJ`WDa$&pp}xP z%h3(O91bLUG)rr8l-Y!uqiMl!LA&`xQj>G>0)Zy9a=6~Tmt6d`IYfroH1!i!#1e`Hx$~r3Q*U>~Xktjy3b$c~nVXAb;?~aB| zHc!ag$>Pp(g$T>QoufZ~fA~8OoN>)xo;`TK&;I%>Jn!58+h?CeA;;D(yqx>+N14H4 z5-{#`;I5?B&^E^k7GK_CN@;om=LrenX0&JOv??{Pua_!jZMBLF{EN%UlRk6$t;Zj` z@W{oJU%vj|)%$(c_3k^4s9g17?|uO}QoFdXK5-{SqZzTx(%e_;j(Nx;oqOlztdtyO0sQsgz3I-kc49Mecx zb9{m%3r35(RZ6>Tx%RrnHy?yeT{iKN_x$Mc#KqS{Z~EP2>3MtRFF#F>p}&3aR|an$ zV+IFGzEG}THg?5t6psH=n#B}}&XZEwq7@)q*R z4t3?Hxj#IO%^Xy|?A8}Q_O`o@|4{99SAXHrGBY?p0*?25oG!>y451G)Nekj^IvH~z zW;4Vq0W3hLJ2s)+t!6B?YFh^8bRT@+P;2Cx?}b13hvOd#&k<8ky>FkNP2B(0qV`+w zzWA^!nZf=Nu!l~XculdUX;od?nm}WvW}%i&M=E$Q?5j5;T`XDUlC%%06=h(Axai%# zKKajoc7Fd;`#)6t@YP>G=)+ggr`__dZx61%=p%Rj=xS!Lp9Ji1dU75x>*#P+8yE7{ zj+I4wiFm$F`FkxP$F@T#U&$s(XG4&Icl^sSsGs@iyBpbWyy>g9)_4Ax`gG{&FMZ?e z+cY2j&>#Qf`1_c_z7lXqYY8yDP~PWmR}(@+A2b4Qy;pJe!l;GA{Bb_r1*zl7d{@5? z>^kea5$r0%7f*We_FF8|pZd#bv;XC}_~MIy{I7Ss|N3iF-@b(z>>~k}Qpr*yRyXwc zR6+1(w6x!mHq*Io94+gOo(A9Y8C@BI%`qStKbJ&t;=P~y*o|*}{!ZrS*Zt`R_wSCF zHPpHMb7SAmeDT4#b3dMB4r2z(5->!}kqqp6^!8uqUz&NZ^Yia~(?_rQ><6znG9{h`=8i_#^b$KmD$EZRfM~2k+hauI}Q&-dkR)eRideT~Ubq!e%;y8O%$- z5b-=R@ICHZh`X=;%zO8J`!8-k$p0x*DEWr_EkrWRYv+N&Udwcsrjh(wAypldTnsSsXp%XxAvI9tON`ZeIo_YgU3lE}2Yva*OP*i`GZHXFP>l@yvnyvl_Vkng{+|yO?mzOo z*L-=uJJa6tKlZ*0(d-94z~-hO_cMcO2^b<)Mh5=F5o-GU|GMrOJJX!}dW_xY?Ir&e z2jyP#{wKb;_Sv8Qhw1&yU`hgp2!WA-Yj63fW9^2(2X1U%@xZ$=XZ@bbzRm&pzGw{>_2o?p!|fs&ntxKk)-* zpq79k;!tGZqki;-vsV7&6Z<`U-;t{?KKJ%3PdxPXM_qqp?Q!g*@#8M~!*icu1}X^{ zB4|YhKJV<EwGa7+?L)lgO11Uw!KbT1@{!2^b>IMF#%fpH^P?;1h?2R#sk| zyghNk;jwpjAHy}F6MuLWyZ?=+%wEUzFOYyC!dGPAi@sU(9^&{Eea?$l8Yb?#^N7<9 zIrDSpCb+(Y+n$P9c%sYe>?rA zb5EQCBfWpF1Pl>FA_KeMywmn*@t@Bgf8iZB{aX0uQ`#@@xM1e;Yf6i2JC3{Q_W$ZJ z{hkC25gH-`f4u$q+wb_N$IN&9%lD(t3pc&+<`b@e%bD~qiF)Ty4u5#&DyH9+fFUA2 zWZ<72eL*IC%@kMUe*ektNjHD^;=lZc>31Yxh(Hb*_yhmu z`a<~kwtv5;di4dLUHkI81N`j$FB$?ryYh|i_y+#;juV-FTLOlN&yayX`|$hTRQrFw zUAqr?<4)|*8~=RCg3b2khhBW!_Y%iX*gM}ijp?@}V2H2_8Tgl9zUi?aG>`q2`h8EO zlD`{#<-ji<{nXcf@Y~NDAMbIWd*m?-({D<^5YZGe@HMwRWBd0H9<2W!PyTampZ@;e zJN&A_^Y`zky6%UG`=05%^IE3gkbohABxK;n%tzn-)HizHx%bzR@#^~(|eUO2t9zN&uZ}|6{e|$b;_~e77CNuug7eD^YyL6nnb;&LF z_m4S_>DMJ-hy!yO`0Ee;NqfPEx$Dlm^IyMv%V)mz%X5yt`my5R|6Te6{z2-1{O;m83r_-ue`ev7dH3A?bNt-tvvN8c}QC+1%Rg>fYI-VPQ2C($UL0m!j6>$FdLhfyW|GO=a zXibcrEo^abY#1h(Ra$O`J5y@b-4*asvD}y(JNu&Ltt5vD!jzWVvH8<`16meF>Kmq- z+DhMWxvlge<@zXn8z|(r(6^!77W(vLxi(6l;pOC5D7l5c4du4bXV8|bqx9*A31GL- zx1rn?`t*i!Wu(4gI_0hO4VT+WACV}J7y$ZVKIW~|4VT;D1D}39&$5|vxiqpu3Wu0} zxZGA1!V>(b3JsG$ZB?P+axbk=ab$&F{sn5aTWJ_Bw^fC-<@_jpgu*XyL%A*V5rz|U z6Ju+-vo_!k|6+%s%#E~cm@s;amLP`9ZDpChoE@p}<=_04W1ko>;Ny`GgYv*q(|B}JY=@AZ@Ao6_F?7JNPWX&W-EQe z<+jqNDJh7EYp@>H%i}zq!e4|+fZ%` zeYm0g+L8K(hvruLhRbcGk0{4S>Kh)bTj?7vx0ODu92==`_*2|U-*CCD^y$jck@|)| zu&wkBm)lAoS}u&zw;|Qd7Wy`n+d>~fivItxW8Jakw=W;I^l@G8pPl3hi==z(zs5x-xE z$PvYQhQ43NdUry^k|~N5Me6Egc{ber9eCBO4Jn2xz8UBchdD%Ki{g509Ol)&3FtfN zq_GuYugVst`dkifg&CRZIOd*Y!cf z&M2D6ZdMC+kfAHS2mHB5V}b>#uqW{nLRSushzf>^!@6r!Rj_ zRp|fIx-y6$9L1Fydb`E-PXo%tG;yxcD zc1LlgMlY!|iI zphH|GAOeTBe;u-G<=cFf(5~LV zT6#pAUi#kpReH&^>&f!YV|nnghY=A8q+N*-UOW2sz zcBNM=htT+{r;&=6Vu-cf$@a32mMfWwW4&w<&BXGQBZHCVRf9QSqtaCag}V!pKsOsO zQAE~(@0cAS4Utpwswt1hQ-BHXDr)A(YbSdex}`U<}-hm;U8G;PBe`DzO7>IvR&Pnpb%=Zpl8sS~7=R)s~6t zHcf7Ow4n3(n6+TsX9jr=8X2#Ta)LY%|;{4OLj1s!o1<%eCG+fbsc&-|I*We;i2^^ zZ9Df8Z~Q|LZ*0l`?~Qv)Sd?hz$)Kl}%lBF&*~C^?ORSAzvOSlNsauOt!b4b1yTA6Q zQaU!^>M+i-Z8e@R<-5@!ooOSM2v3`tUen5RhN!vHDW;iXJY;MkMw{80(fPF|%4oxD zPNv9El|0AUlWXZMzw+%gN{Z_sy&>cf!1>7q=f zP>vcoIXx{C^GmbQ6}Svw*#cZNXK+Z zYr>oGvI#!t@sxywpSK08W>%m)mT1~rNO(k<(?pqw-fgQoabJ#N*<4-A^?Yd|Z|GX8 z4DGHpI?kRZU2`O2zE-ELJ_H$%qKwzk^7&hs+gi~i^pcT_sb$@9luovtT!#x=%K`O4 z$Ura3*eu1ArO0@qWpBGA|As|!`tFvpjn(Njmrak}};Gzr@MU1E>%m$bM;<6=? zbeu{h-N|4*V6F>>Mk_%XvJqRrYpi?J`y&HPl&PU4rq9Q#*?=~~F_lO!(qhvs0<)D8 z=5$BH<(;&#VW~Uax`29LWS|peD!verF<}m4LldR&sHp%*7`SdPV`ieUuC{D3YSVn$ zVJf=1Y4twH02O6o{w!y9_w->@n`qD}Ct0Y*oPnOfMJA|}uNwEZOO{r$t}~e(>Sbhr zh%y4;4NFkEK#$eu#aczv(H`xNIRJ&OyAh?TEGpD8PCS$6i|Qq0pcQ4JHG-%n(o`ny zAyamJ!Wp*43g9zH>9L5}mUYE4sFC(~t+iZ5y@(9fr1~VfMbt!AC=cy2@}U;e@!*;U zuFslH0j5!lbZXjYBGENweVBRy8JsH0I2ldI=&iW2AqSSv_=$?K)g-)SvKmS;-E7Ls zX$>gvOP5_Kt$H3AXhfNsD`v5!L9|Ra7S)&l1+U4*=IM4dm}$oRm9~b+wLo&ZSi};p zsOONuDWZ&n(e-dv$o2>-oH5YGQZ7;OnREe!ZTlij)Q)&9T)@{d0#0Z3EHYRXWz1-i zu-NMjZ?qaVBJsN3t~EqLx`?2)m;)Ra%q21|dm~%Qy29!iWN@-56BAf%C{5VwX4*}b zis4?1&=kV0c(+)#q$!*t5R19%3e-bUt$G?6oFvL<3>u9YcL%|z(;{eJLT_tgotT$z znzS7!7{pn$8gFK4i$B;_Pa%U7MVW5ip+}9TuA|#2*}KhnycJCH*#^q{!%4jB#baKU zb|gwxo2{*$L%2B(0u6 z2FHss2|)2rqdvXXKo{5=U2tY%Ei~>+R;xMMVz!{An#Ui~=93jiOsz%+$4N4@841L# z5vN9@b>w0VLlc}}J9S(S3!9t<5dDiee1uNd3E2&56*5>6Wju8n$(8}3v#ExVt$flN z<(y8`>MPo#F|E@Q4Kgvh$WcbS7E_NSgJVUR3d#8Oi5gZ=s;Zo!eXvs~CLXpk0FN9}PY?++QRTGb24;Am0Ci zseX$L_7!CyVqjFiK?eJXG7vE>swa@avM2))qoR5o87zr15OF4|$B@CIC<75FqIwh= zEQm4?kshjFBZGNS1|nKR^$0SU6J;PGE>yol2D73JMD&E}VPr5P%0N($sD6nIrbQWu zfCkkskinEF0}+>?`Z+R~6lEYH3{(#xg9%XvB2+;2ATm&kG7vZVs-GbPl_--{o>4~i zQ)DnM%0S$ttA2tE#zYy2n{m|xNdH1n2I6X4bwAR-K$L;Fn^xV2^v@S%AUG~m_agoC zL>UPB3e}I1{<)$I1T%%|9;DwBWgti=RCgo&t|$Y+C!zWg((i~e5L6MWA0qv>CiL4;g1V%SeTo?VqP=%wK-z;W3%MU z9W$?+ejL30KWOU1Q`*U!CoK~nnRt!*N9u~|r>eSYdi;v9djN=%AGYtFR)L6=%$WLc z$Z%+w75pMY-zDlfJCji#rZ{YMba2gZ2rbO+0A}T__MO1=GsPx4RI%wBWAb1&BDuZ; zs9Y0O9->&~U&G~K_AiCL9jMZYst#7H>ZRwoFdLtX7Wy{%GZ<4Jq*xo|X8^IOV*8Y6 zj#&8v6`Lc6K*8+8*uHg_n*9_uMl&dlwziHv>IjKxEYW zD6Y;X0|H{V2(uFt`X*rBNhg6$T~^)`jHfahKL5#f`FQa5Bv9w%R{S?us$> zqT;%4(iIS!<7)rRT`Ct8S9BAV5PR!f{|ul~nhxg`*YYLPA;gwE*MH3}Rdb4K_YzeQ zJNZ)obfAh9C!blxwHk`yg7E@M{eRk}Z$@#|hV?;s5QY9}K%ca-Oe?P6%T^W$*Q3xU zcPX1vT)CGigYa3vGG+kUz;0$tJ*l{AqnEMaTYeaS2-u_FWzU4-+C$m{;cKydLNrJG zvek;sSzlHm+%vY1?^2^u+zab!AUryuj{!B(dNRKKwf?B+UKpQH=<9ZA9os(8ZlqQS zuTtov>snQR**>6Xq*e&OwAx32R&j@|dQou;Z?dF7xV6>3c9+T*6gTfCDj~e$YJY8) z%I6ig?ItQAoa$WvRG?B^I91OnZrGQsW)Qx3uCLjp>d%TB^d+hwT=-o7lwGQxRosv- zQ3c`om-?$fmAFQ$o>AO@!)qqQse$PKYsPLITfTZZxb)o8N0vN`e_Z^3i}r=57T&jD zo_~D)U+2%5`_Jx{n zzp5^&4^n+ul~e6Ie)D*Gd;vi3`5zjv)2ad)_4xLD05G>qG~lKcVaF7}LR>`AfSDG+ zg86M*_5zr@DjM+8im)##fQ7iuq5&(d2>XHpScn@h8gSBzu+J-ig}4->0VAyl`Y6G#INS`qg5+n?6J90$^XfmVclS^+G?eIgC`XGPfG zDS(Bza-;$KtO)y*0$7M!NxEN^V4qX~3vtFq_bU?YZxz5o+-=hRvIP6Y_B(%=t4z9I zl3;(cz25?JG)MP&3HEUXun@;$biXLUKBk}%AnsY|enEnLRDrb+SFv@O5p z3vo0=_tO&W&lOk;aR@~BQxfb$3ao`V?xFih3HE0SU?EOP=>9np?1Ks#0pfC;?k6PJ zpDM5x;;@D8zg~j@*%Isn+k00qrzdp(brS6T+qIOu*%g8i|AMu0fSp!-n?_8tX|0C849_ahSQ-3l53;#`96GZO5N z6f^?F83f%AORzsw&3~x4@ex`sa!d&@MfA944M- z`|ncO12m8i_S~JxW&?y;w=(61m~0%peTI2$DLz1Z)t`exjJeGoXKy@q9e%d_9Xtdb2>qTo4&T-s zhlx?!el=4#4t1N2Lx>lt(zEPW{oyFY+4pAS@UN%#!{hH&dN%ss27q~QXSJE)^KxEq zHdAfn#21yX?65--bjx99VQoRdm&~l0J>6U@Z1p<~rJT==#eLGn znsXt|Rda}UwM(XYz-scB^>kdwp%%ARSw>_+thzkxy1RxQ#Qtpi-JZf>hi*3P{_eR+ zAc9WehTW?~OY*lLc3VC~#++`yXEoq%nL8niZOvJaXyd+?uV{*TqDCb-7~+wvCl^AA6cNfJYteRQwb1lCVmViR)f8Eyl73y-5VB$+ zQ?^WVrJ%>QmNKD)CrPgA@w}&j6#NEv(5!5=1H(>yhzwS{&aPnxv6tFoyG~@YVfS~x z>x7_dfg5(O(r)u_KkP)VuT7b_s(})T(lxEck09AxS6>Nbb&XUc70^)*F1sf5D)x$> z&j;G2-QQ%|@&-c)H+KC=UD%!PxU`Y57d%&NgHf)}23GTgma!UZwWz;abho-h+m+(o zrZj7gP%(2n6?0KlYoHlT*S*nP$k4OkNUcJdxt-^q4=U9x3wf>#HtnbF0R9x0Nl2~~ zg6p+fOiPNZ9R`M-=>H!*mKY{qFUHej4*~J5e=fLmZ6cEiz`O*4dCjx^nd1eQ zo+=ptajXjy3b*~~%pbdaH&{Sf;DUtyYC(tktos@LJDzpJv<*%lc7;v&=0K};_ z%o8jnU2u|Q0K{oM%)2oqEpVb_0K_9P7!sfDoz4p`JptMP22s@zRtA8%Wg87LNgU-5ZLx zdfQyrzdTEpSi4F+&*jVFP2^U8O9+`d$eliPxQOGp4v24huIegPd#kj`=yMiRF{BIl z6YmIg!d1Fe#1_$>C07N{+Og5VJj2H=W?yw=^uQDu(;|O^g$mhu# zE26dSmacC06tMuGuJY#N5@I_HF)m-FyNs>IO^8 zZa3Egl>!-X>#%^HXidq}fW354apf2U4iDSS#y_#v@9{C8)YM zE+_iuOXCs*w?Q01Y=63^a9k3bT{A*#s-dT6cRK z#>&-#ld$D!W2a;_wHVTc^5M1Bx;GQR9pSW&LP9o7$W@9y3(28gu4D@Y^|}bjHJx=z z&{+al&}>O(D~@8zn78CpSg_(XD!X^o;UE{5G!D<(H4Y(mV|#4hif=X!|L*s#5RUTh zjl)-I3;Xvo4)HB>|IjX}&pzH{G@hWG@wDu&|bK7jYkv zTU#x)`PFKw$#wBg#vh}R5T7Y1%)|wPOvT(k|Df;xPZi%ns8^3M*-}nx$r)svHa<=T zQt`~ovNmhAFs#}1KqJAPtN<2*SeqW4BEg=d02YFKn;xu6uqP^kRZh=M4^Ea~Pf+lp z9)iJ}9-Jh>9prJ3dOB3I6{I|z?}+1kxCB^mtYm} zqr%Xk(u2b!SOr|DFvO?y;7|!x0qH3WH7z|jM1oa7UJAokN)HZ}U=@&+!ce}`gM$>q zLNM0Tg99a41)Qf)Ob~+uBv=I$r*Mt1zXYp*niPh9lpgFS!7AV$h2a;a2m4B}3dlua z2=?j0J`$_~0#O*|S9-84!73mLg`w%E2TKyH0zyz2CVzUcD8VWq|Ae9Wqz4NUtOAZt zm?&U+FfYL>pz?%C89)!_Bv=KcoiK68^k7zkRlv>(6J1OXW+YezB%3fHyYygMf>l74 z2@{4&52hqo1>~48;kEQ&Qi4@Lh6zR4Fqn{F6|h~xT&>arwFs+(&Jv2UVW5&=74TKU z9Bj`moiXMb`}&wFqB?H;=}GJ4{u4hNUmbgP;`|BM#G&e6sc%$YtPZG;Uc7f{Veu1- ztwqb?ehUvST(fZA_-*6w7(aXB+NG@=kzni^v_Mc~8H+%BTGc#YCxnd?Yqn&2By}E1>-uz=ZkkMAn9JI z+qKk67FRl+vtge7KPXAi=`ewiD9DS41vR}$B9{xf&8d>fOO^w+Bu(o1Tr}rwFo{Te z?)%UZ-;*U;b*@aB9koCyUks40iq&XMktSVUUotW=rw(b+O|3oG)fkNX*Ptb;&=M6{ z!r%5HsJqRie5|D!u?p3C*+T^D>6n+whtd_ZfZ+RVZ73ew3 z8i(K0afY1?8EBz-ZP(9eI)*&R5M1H(vAM6TOMvuu2EJL&fH#~0;7davIb2zl{r ziMC@l&YX>AHJVP&s=5qX;vLWu|13+SIa^wAmvJPUN_lm3#H7tLWGCgsvW}8K3V1dV z)5L|8#$8-I5nAE|Xo=(3C6tfBAIK8jqTXFj7^^KqEtfG;9?p*@LoUjbYHAFYW?5s_ z`@F0UZ`O;49w|$Nyjdzxu5032Oz-QOT$t8`*N}!YmO`+aG076mu&-JuXp>s?4rqy^ zp(T!zB*IKV@1hWG5#?!X2e<3ANmk4&XUS$8DXp)?GjT5&OX%99cIn5^68At$+`TTr zAo?a@p==o^M6jE1*&>uF<<&G0w%COg=)elJz??Ck>%wM(E>K#L|6tdclDO86iN@;Ar zcS1|Z6FZC!d18l=kSF#qmTDD49My7`V{|2^wK0XF&S=GT3C2;$V_2yj!T4sz5y%?n z&sndBa)|`A#Oq}V9cv);&S0+7ENY9+NGZ*`$dG`>Jvt&y`q4x-f!Q^^Y^7(Mk3dT> z&=O%;0`=EgCVkar&o<-P78MP;3Vs__4{$EK#@;btbjBBVXd)FBoA5zPc%dac>k>Au zPP>I>q7!d>0}h8RV2IRW-Ig}!v>Nh`cnNGw{lQMF*Pa=Jm6-mEETQ3p47eyuC9S3? zNGyvJUcFGwSQE(}mA2WP$!Lt%RLVU9v8p}{EpZjJ#D^pax>>OahEB$)H?(5_)@<&_DL3a` z#5K!>F;szf_L3s94u}oh`sNBkVtsR^Nn5*-P_#hQ5!^tq-U7iyvl)TqY&09!`+Svh zlqmGVe9w6BdnK#F?y8^T5_EyfaeU2OpyH)ksqJ?JVvI{DHK{1tYlUNuthI?9e6}nB z;c#9B8i9RDL%bsYgtqEKbHENaaQOe~?~gw%d;F^3h@SoUJC@D>p8vfs_x)vWANctf z&j;p@p8Jg|vV7;v+tNad-jy^+h+be^NpEzEnhjlI>XMO zGh@?ts&1LSa(Qam0DOPL^wiQ_Q+G{WHC3HDV`@S6`sGsy)p4pzCaxWOZtR;1n@at# z!h*Y5Oln+vssO&HR1w8J=>*Zs*4#`MEVu!qJC$gLBAJY{>u|LBa;|7tyhxS^nA6=> zqt+yQe4MW(YDGR)7jlhQsF&eP*jl$BNyeY`Q10}iQ`Ql0n!GuESkv+A>IpRM(qlf<`fW)~zkVg1D1pGg5F+_HsQc)RSdrB4><*0^GubvQ-|&S1)j6B(BYZ#Mezc zUhtN5Xu%L5i>^A+X#{yqtfd#UcH@G4V&E`!@(x4G34p8} znzuZ*g1}}-W3Xa1p)NB?M`#BU?6HC|Tdf#OypgY!bm4ffJ-a4LG}i;X%q3Dzh%zjY_{mayzw9t*6X6e#4YO*K{l2*m5R1-H-%Z!T6cxfx*R2!%bG)r zT!9TFjJXop)@8!8^>qnn2``3v;N&H1GTRDdjWnT^Ced=UA)y_%An8<26>FaF1)1WMQI@EMyqOeJ@tjw&;tXwPC${a~BhVHSofC5_}%b2hsFvbvA zp}=;4^asc$f^cALFoJLp1Xz}l*kq6dT965!x^;WH`@Z|8tDagj1I+!S-hF-RtM7MC zo$s8J-}3vE{t}f&bkZgn=Gu2pxR@s_I(PMOk|xE{i#FJ*+Mk6UA#dj!(>EvS7d_iAax5&x_|4iu2___Xks-q zyAJS<&Amp;R}Ir?WzufnpI%3cF0KvM^Cb0e9v*R66n8ZlCdBr-iWk!knlI-og|~zj zZPt3Hr+3AT(t&pKTEF$T52#>8)3RkAHW9ls)@jx4bVdlg^VimR;SXCWmx!IDgK1E(i*@a7#bJ7HJ@FP3E78XQhe34k%(mICO2*KtosX{{o(z~8 zmRL+^8fsfr1FkQZwpT})#rL*pCrvuVaOUAjMl7fC+2NUp+1X|s^-{C9LF~d7NDdj3 zd|yflBvf1vTf;5iM8(NwRo(va1F9WtteI_V#H4*uZtw-yXXf*sH?cbNRU4Mqe8n~6 zRnqMY?@SI7<%G7~SXmoX3q!|(H+FaCFhv`E*JYtHlBeW=o#5rFTV4CJ2Z`K3#>8~6 z#f1>wLFuwAq=;}9nu|7IlgK1~W^g*wZlTciKYftMz|z;c9fnr&xY=Yji*LGKq-e#O zqZ4+gcQ-I*#(}`c*S`NC;p559k5)6I=K|ZE!~&aoY&EhMg3XkIF`C5PbsmvYrQZAD zgM{U8=E;OZ)lyqnVOuFFp(-GX?-ko6LDU@Z<3!t`I#9B+!yTM4onA2&(H$kNmeqVz zcBh-)u z0->`3U9?6G%_t_B`xWS)zTe;&1A(X|BKAo+26ti{s&{%r@u*aZ)jb);rmF_3Yn zf9+=v5=+20=yw$c)c6dLm9-({lBUW@iVx{N9D8vxD~Eh8k0$;5-*j$J%z`gxT>}s5 zOi0^$(G6FVHsu**x{>Jis;1+ihH&%w%)~C1)q+H~XdX@`O%?Q7o5_xj*Zyu`cSkD| z_Lnl!FBYjrWQl(7A0I&JHn&nB9_vJ72vxE}F#uE0~j@>&MDtv2Ll$wRk)@SsEOr!f-dTe#D z+HQx;1WL_Dy!rPI5^_$A83ta1?b#0fbUjF!iH>tbJ=S!IZ;;q0bg&X6R=@MNE+l#z z+4R=IEXIdWzUYs*R;l}5AL{PrIXNe!<=Dj{%Pf(5-*J!-<)}B^lFQk+WoFiD9lLR% z|z*1 zXQz%MjAw^Kgo@^R-LYt$OO=6ExF(AkrRF)4#A|EVy`Mkmz}tq<9e16TJxS&En8Fvk z;b^R=EetV>S{I6?+6xN4i|J>FU>AesWt<2#vY59;Zk5<9UGoca%9Y)r-FS}G6T8`p z8&E!b&p+rG@Q@u8ZQiyfTUecShBY@XBqp0g8`CJHb^<4)Oc_enzkRX4ogndF9(44= zK_p^fNJjVtK44&Fxwb=kJb;PDb1wYKAK8_ekS z(rpj#tf%a96$fKeO&8lW>)&?|I$$h-=VmXGOFl7|h#*@;)f07Wg7{`$K$2{5@m*bVXu+gYzJJZrxdNlnZ};?HUrp{60YKee%Vv*5`E zAu7n(;S{9HRgVp1try6(V|KN4EHK(Eqf;y`hva6+)Dp$0l0y{z`+xF)s*IL+*A;wl z545EfMs2!LGA}0G$)=>I(^}*TVl4QYDMGa-543`YV4=Xk3Nv5JIc{JM0 zVJXd4ZBq^9siBL=gFknWNceU??g;{BB>Z@}>_}v2vGaBhXFUwoA!8;R6fHSLtUfq9 zNK6gboGjO*-C7ODtD;A*Yzyhr^hDWNY*trz!Gq^IDAKU||KcFAg7LawW8Tqoq(8)) zK*Sqg7rQwQ{FI^5+HR`834QS`V!~OIB*Pp$6?Y(b*_ZQFJeK&vSpS=@2{OO0*gTMdaci#U$@2mIz`aSXP z|8keQ^J8~@`|ZDQ8$J7jw|@B6x8D43Z+`5?AG`4l*MIQ(SAEE*=`TsK&s@I^xC-xH ze~jl=5BB)g(Bm|JhOv&Fw^bAbrrCwqbU9(ba8xvN`)(3hldZa3tJRwtQ$F8OUjn`Om8NC{Z4z>3#SiV&QG4TN!DOx{PyCGEgk6Q5WslN%Se7*I zgp_E_N^?E(FJZWEzr?^KnGq9%_Ly(-kXths>J6Ya_p zUZL~uN?FD|#q7kZnjIu{CwoNsfjcV*Hd?P&0Y3A;h#dtOk`@ngX$7MMzW*LOH zq8_e4^5u|FYR_w!?V8{$H<(<)aL;=M0}&C`6IxG{x{*zcEHp2j!W2sztJ%}dQ&)Qu z@2oL~ZibI{s!4Owh#irWcHZkYl`z;ePkWZU(A9ZAmR6)?5d|zgq5VhXc1MMoHZlVT zES_m))t52AFEL=@7%4nk4r1LW7-Z_BekSe&b!oAd%JCK=;F@RB6@ff9s?mz=BkYU@ zJpMb|sAamd3zVG?d>Wdhba67J7ku1I0_+iK2WuRoOCnV?N7{wb>@tS@C5B#qMvSvz zP3UPudSbJdS&BqDvp!7Kb+=sG@CL6Of<&Fi%}NaxJzjSQJ!%y7NjAv)f`m>QV}Nzc zHMA{T7)Dzb3T$)jmzM6^*;u9sZ0tAw<$1Wnyu^S?nmo}9M1=`&(n*M5K(Tf|owsC< z>d_3ZXBOIWMLt3voA_oir`b6;nKXLRQbvNx;**_(EjXTTL(quv$g&FaLCbt(%*)wi zW{~SZuWMQX`9YWF;X3gWLsv#@%!%YY6^k;?6v&mjNlw(o1n;llU|#i8x36x4wEwu5 z?MBNb$SSZ*!Q^dljyEJS3N_$)v-pK7h>M{*?zQG#CO#@Bp=ib&X%j_)k=52PFJZX< z@FfOBP+CTdUOQa|lBdbAg>5$=DW^RJItYoL8TtxJS;+0_j{$T2G+7w?sqfMAO7wiY zrr7}t=MLfOGF=V`-009K5B7%{HY6RAn{C6Lwy3dSI=_VB?7LpU&=K2RdciU~vpoxJ zKv^*)vsysgVa#WPO=noOys(GWQ|R%4vt)$mlBvsc6LwnoA~FmU$(%Y37b6Y3^2Ys! zt6?r=kN7TzuV83bOJqVhHHy!+8;4>zYw0rDe{R2J$Ga)MADMhcwNQxEu@@;iY-F{e3Snz&=Vc zX)bArinSMMkAeF0X=k26Br$fJ325rVGx<7P^Xg-;JfW$HGAW@5gV0x-5^ZhNa#8yc zX;NJZZIeXJkJG6Jy}88g`d|Od+4yTGwlH`p$&8&aQa|Q)y5q0ZA=RY8qTKD2)r8o@k7sR}lCy23 zx)xoookmryX^pQU99#LLqLM(GlFlKJ&$q{qhIK8)fkgX`*n=L6+wFP^wHIEfG+4{jVc#qZS}KO3 ztAOXf4TAf?^F3QQd(+HwKh38#Gvu_H7?eVM3Bz6gC59F~9)l?tyZn&ICrTMAgXN@; z+BCaeD|8sLwLb5lM&d({IWgyY(@v9aX@3KIif(x-3SnT{g`-f8cY=0dJaw=?F$LE+v|??TM_byYYIY4Fwx**C}{x>D4_b z-439~S<0wLxMt)iO655$Y|+dI$sjt$_Fcxn+jC10VSBKg1^q`!V3q_Utu}-&iQ+C& zE?;5Z`P?+%HIYYd3j#jVN($PJr1`uWYJ^0#rKmTVg2$1`Yy%0)k@?t~F_j831kpK( zjik27td?eAP(!_3m8&%sW8?lB3%g|@ijOoWUWpFAkxX*Jfr??_@=ksB$(I;PwTIYS zmYa3F04@?DnX86S2s7Y@iq_l&68k#e_qMape=O^}Njet^ghZy38h8sWR!5ShMJXJ; z6siqAY{04N%A)Q(%9sfgYmJ_!qiHb5{n)*<9Pa(hD;TDc8BYn*Ef7xW&*d^Cay}-F zfPyuM8S*Iu7vrjf$o{t5=Lu|%e5kD6jAOdJLTH9&q465BI=GtN7M#%o27c} zW2d(+gEa*Y?3`#s45>G|UJ{dt8ll=2YL*mBnRfI~6|iBKDFi_q=%%%OuNcq9)tQZ()uTrsFAcB;NzzPs!H#Y+r%7yw>* zw6yI3x=X+<3iH@e-&xfnxNN5#aDb#;+30l=@v%$ObGlCzJAp}*^%_&cUO8hSctIGm zImkBSR)L?VO^aAyk6dS?S5>u{bCOe~jhtWJ9)L-|O$A`FH-vA&!6h$KU%SuV?RHpC2OmH~+hv-vKiD|K{rz1&5sf_ikU# zFWA>19BK>BKDDnz0L2N%{G9#RG4J}qzUSN>BZ&DVZTjH!AH`9q4}qtEV&xh{(x#(g zgZf2Odo)@H@&I}8XATnFZrT)?xSV=TXF?6y>)B`-fyc?PEyL0A;w zv)hM?0in}0&rU|%HvzuL87%;K?u=V3q%~4#L#TOBmOQ&DRiKqQ9fqL0oCdB_)-{N`+esU zd^IXEOcYl$3k}F9H^eG{vtrIjljS%`6};P`E7Bt=6uXgJNZ?LF&n3bFj{k02P6TR- zGf-%$YcZ0Hc!#ZG6WMOciGKFe2Z?fp#x1=Q5sQWhdvSMH;+rhrG}_!+Sf!Bni#CXb zOn0by_D2sATd7k*1*eM9PL-7{p?R9(H;^1|XPTld#&#K|MJw@g_S#|X@>`PHv6E6u z`^{65c6vDMc(uQll({xQC}Srf;Z!%Koke`-;0)#ydZ#&x%Xitv^%X`_lx^vHxK?V- zIvd6KW{F362kEQk*`eBn57jxsLU7=Yq~G8zz1Vd`O>otkMK5BqTl_h z!#F^o7Ca{Ksn(Bz<#dCQYq#&Lp=`Hf?eVJS1HZe2TC}g~cmLCa#HN8ft*vi)zvF{Q zLu({Y*Q>4$yRHd#hCvY7a6B5%LXN$AsAJ$aB^-!znA;&u55jRdGC~Y6$Sl!eFBB_z z+e}Gufh4f4U;EJu9Y;HY?i`XOdAC1StE|oLlHG!j#`r|RjTOS#^<=ji7Ij%<{Um@@ zzwpn72OVq8=%ysdB5^UeUQQNshOJv&HXhYNq69IO>cdUh%28LmeMsTt*F9+E_TYMp zi-FmMqa^CK;xJ7Y=86K+|B8fxQgy6tcc|4T z55v%R1Xr5*AhhUcVC4(TP?m#rg%rJHP49Z^og(`|%ZoP7t^fQ&LX9UD?nXSCAc~S0 zAS)%wC6N$3S=OZ;)3bqB(g67h)!pwtNX$bX&PME9LWBrxh%f=7WS9Gn<8i41BSyt(rwP+eT}l=|*l@c(yb;8&e#M6&WPzZQ~#@op!5X zXDM?lOj*P*Pqt&viPLQ}uWS(2=3SI)yr|B*{_XZbVv-TFqMD|wWh>ew+u3H~Coowr zcNoap#yZpfmh1O~T2isQfBr&()VU&!R6)E1je~M^(!LQ zhQW5a>e3seNS+YG?F42@ZZMifbCrzPJAeFuO7v5ckC~1}$+&`pERvN5moqGLDGoe3 z&nklB1MbMrECCjdD#RvIX_yAjU#-i*+l{lK@9Btk_Lc|fFn6~cRxG*S^lNo>JFWPiB^5;zHY+#=s&RZEX&#-XiuL(@BbS*;I8^l2eK6SOO zMmIh|47T-dC3qk=ub*4dV8K>aBbvFt)i_|u*v)4b660V}PVG)94j34taD|<$N~Ewk zSCSNO1*!#`2GiNyMn5|wIPoB|ywXy3ZsA653B4Aj(vTjTgfcVXv^R4c2+p~+?E2aD zivy>}w+glE%*O+{CqZ4XA_r^Wd1HVt&0}Jcz=dKITKIGu-@5n)LB>@+=}witZzV9J z(=#4)h6MCy@eMmCk zThUBeYyrou6^C#rQ=e5a3L zk~yJwFoySU{I3Uzx?W7s{sdldNiwI9M&WxRJGDFt+bm#JU_1$nlqS#6@tubUi3%L; z2tu@CHZ~^|YqqGU7cHIG!)RL!YB=1?C~RqNVD{D_M$HxCfV7d-&Y1bQ9RwlLC298)s;&p7^Rna3HT|yKU<;*F!>Tx|!8Rw*t$gf9H_d zB@CAwrY`)EG@n%ML78VgZmkV=NyX0Al(9x3CEHp!xMgBD4hNJloIWifrkC{mEh+VP zFinG}&!;L3wmFd$dQw{&gTRHb8N}XP zl)G4Ela#R>-aC{^3b+6*Om2aroCgTBXKd8Be;VBxCRfwz0d16kGwb z>8+)MIM@u?>L@!WBem5+O=0V98EVtgY0?j11mwUBZypZw6QY^-{?=i3`{VV186dv5 zaYfJ~kLaqasx=_`$#7cYsg|%rSd~)QR=3YGP&S7Iz+Jk}LGTa}u*SoR_^!oUgh*#6ApZWoLo!EOi{qS4& zT+QDa+TqGh_l(l7Qa3jgAc?2~K2kjq-$i;VU2-z+1u{DVquYPh?=Ye`YQa!@0K(4j zvfXckhNLay#1t7VyZvxy!e773acKw8vxscE2RRLG1 zO-;y z-*;xhU^N|sUyQb)Hy{JNjcZ}G-AJ>#9(ex5gPsCXnoi!82i~OnG^LK@UfvJ=u-4CB zto66r@b}0=26{gS`mbiK^EMvL)v+h&YRn3}ui#f~>A(7e;NQ%({=q!skkM9Qxpb|n z%fpipl$BSkwU9RER((S7`b1-E3xrxVj~74mzE0<=k+ew^+G-`&PI6oI+ijxWPT8)J zsL^28fb5efqNB|uDPW6Qg=_1{*k3V=YFJ2GF4rDMhZ-5e&<512rp~(N2|#JCm_G^9Lr|N_r#BZ(d#^C*H{iBb+WxjU zg_zaCW=2)D8RDvaIb~zNt-sMw(ZLnw{pyYFh4#yRs@j+3zKH#By50Q^{L1@QfkYL! z169L87C~xT$H%T`cu$c|4$>oZE6b-le~K-hKAC;^GsvhLmyL*gr+YKITvffCqIeve zbQ6cnCd5=7AuLGSS|xpa?ph&&u!9Dlc@5||Y+xhA&KM}r&{bX_GCpRT#2&jfcHDgn z^r=EF`2WA=ntbiOPrm!--~H-$vWNfM!*6=<>HB}}{-b;K-M@bK(Vgw>zk2(dZ&f${ zkDE_#eE0RAx;{Mnude+95V-p1GmoEt&3o6*?5E>v*Y4dtyLIbRcW<7;hJ5j)vA^Cg z(;~35McFqDo%_qEuWib;rIUNZ@qQ?`Zqr4lHR!_^qG%6|LQ%?&P$v( zPN7%6z?sAq=zSJ|i(zmC(i?k>18o2LZRF8NKl1#m_DH|^CDQAsFgCyT?Dpo_yuL?= zeCzq^Up*S*H$4B!_paT$z<6)}t)IgFd;#%rgMKr_J+}7upTGXgqY-}O`EUH(EZ#d` zmv^h z@6znG%H-sxJ#Hz+4Blh>55DPxFuuN5pJV*>pTl@= z-}ml5K7l&>0^{LX{LK$-=kxe=zkQGKn||8|VZ7`)0Q30k-iI+<8DXx2gn?kS$woG$ zj93tca_a>g^)N9HN*UvA)N*GoaCGbs%Wu8F_{Ll90psu3WBh1vG{$dv{uO(SpLl82 zcTXYpzQFkYK03$F>2KZRef0N_#{1~`dwaYee~I_bDQw{{<}~n(3~cIeoYXsec<=sC z``54f*C&8{_kAGg**fB5p>BHhblqwXOaWm-b=&dvX^^J06A-nJQC!Q)#7iLG=-4`d z{7dH`f5XurzvcNmm%jPir!bj+@HhX)Nxi*CdjAvq*N6Y}33wmA4=+5dds1#E>B66E zY=xo%A{_^k>qrGj>3T!<=YeP$I1T3dU`-!pr5^B}TmM7&XuOY~KiK1a1VgRli0Mr?^-LpJoJ=mMp31IKO5A0x4ku#9jJ-@RD``R7U z)Hmztv<0aPv~6`23$XFo5I<6ENObN;5uV^geI4N&&E_28r_T{;M47uI`Sj`@Q3%W|MY{7%lA2L&g1{r{=I7tKYRCUZ~STS%PT*h zeenFwxzFyK&Q59E@#gRQNdNSey>d_3cJ^+pQ-Vn6??1s1uMV=F0`RIh((4E19@DG7 zyEmHqQNFu3MqAGp-9Eze?^kgeSip`gV-;L(u6XW`e$ill??`(?2z8I`LxzibpiM&` zh+pZdDAzIJX`l*BC>?$U9Qr&}}Ftq`FXcxLF%x1Q`VR<8; zk{rm}QP&--W2~`pRrd`&;GP)PFtE5S)+0F>je)~%BysBSsR>f#UA_v#URG0`>5zjK zh@;Mg#C<}7NbQ?l=;uQRuDbtd^ZfSS9RGlMc1pvWH=klhn&Xu%b5GcE&&}(Ev-oQB zI`soCg=t^CgIu<)H=Y7-$F_B;`aUmW-$nX! zVQG*gad_CeNQda||9bIHuZ{cs<6i#l{m*9LlYzpQ@3?e(2Y-I`vk#t<^MBl9Jm9$m z@(?zD3H)4sW$dHX=fC=%>A(0rwJpQkkM8_0UQ~ML;pg)Sc(L2;+t~ja?lw>d*@wTc z+-)pIrW9tF(hbQlNEP!SZ9SHWr*0?MjcX)(ib79GJ;W(F{?NP4kR5IVWH=|q9kd4N z7jV6z+$RR8iJd<&#i6%r4@JS4Z;=r;-H8bUYHM(GQ;Zi=TDQw;g6PwU!arx%e&2s zjpR#tx9NOf+Sg#^PqqvbPfO9+n562DSdxFb1;kR_pxciYQnfJ1rx+Y0{)gUeVtCUv zpCpyvpKrQP+nR;4`h-nZ-F!PrW1QO}CQ7Vki9B$In!LqfUk5oj?#uu+baDtN99VVG z+4Lc5B7=StS>(e{o8wYpz;4sorw4$1%bWY%W}k0)!U_M@?lzsPcAHb~Hv60W`FZrY z)A*AgJ*CIpRpI-q^DVzXx0HWZ@-2HGbj;Td;Lu&H07?%{FpTo+GV>cPfjru&<^y@AQ_}jDrq;%tGnDw4M9i*Iy zMIG+x;K@qgnCwpWs*SecqH;WD1TzN}6sWgOB;u%J9tHW9y^Hw&N3Z?pwfDB~-Fo*w zd-pfL^CRz!AO6(C;^B=4zyHD4-v5*Lv3q~#UUByy+->fD#hpKT=e^s1>h{;&`m?vl zn}6dbapUjakgos4_3Zk+vp;;c|Bx^6kNN!O?d#)je#gFX{mleHuQ_}kFBPIaUJU14 zs|B&H4yuwJGvGR)phm^HzHNg7a2f4%c2lOGKEDC9eEj3~jk7m5LVw+76U4yb?Z#RD zZT&t3SawpSy^a9q2fhU*MV>L#9FBuQt2;m_4soP5ndyq}P#6Ot@$>6I=_kO@t{tuP z{nz-%@nAOUEw`X3a3K`(kl1!pX9~Ct``b3AO>2M8ua9X<4e}u8`5Dmo^qqa<%ACwb zLdMI&$R6cGPyrRa7CVMs_{m=eA0BIhKhv}I$YVj3PO`BFnGEP)cEWsD4|;2O&^VGm z^*6$L=mgKN0Zjv->0ckM>CywpV;xW3dXi)@l2kKj;v@M6wTGmFEC!_3G418PkD8kV zl7e~9em79p0qXw0qt(6owDhv33wb~_>!ps7+H<)5W z=ra$VeJ4=%&7ZQb|H{$I4m<4!c@@!1Q~#JhzR1;l_8ogPVCetqXf%f`jK^9cxdIlW zuEL4!MKSGnQa}P0ciIN%asjvpx%m(@+QUM9T!BG<`^BJt`OSUjFD&Hq54wAK(CElO zC}w4>eUa&d))*Ml=^0DVAc!6j12yM&Z50pmvDRgRp&K^B4F4WLPMvJ*_<4N3G?{sb`N{ss`z^gs)qXim@&P57tbR(G^~k zAs?pnC??3tnjy+gjh__&&3-Na+|g*RSj*1Zr3aQpiADzJ9g*H{SWk@Ufs&i-U=8t( zAKKHU2YMl3&a)h7+^^-IJzC={YngN$9qTue8*e8?Kx?@q6t=of@L{CLc*2KH$E;YA z!V}SZmH|!swfxVI*7Vw1E?gbL6Kt4pn>pZRAk)C>f}H()Idp^NU;qd>1VDJs({6=< zuQk2+TL0P6%0B;Ee&5&XA$jQOPPfmvbj0tX!CHg}d;#bk$yO4VDz~L}8XqEPh?vi| zdo*C^|L@UgF4pp*k)6&Ypk`=q&(YcD3T{Shbh^KY$H-VQcXU>Psx6XED)U)#q2*`Z z{G9aS;&NUj_4tHrDf3x;q2-^x+4u9hmJ{;E;%5=ivNwc(a9gQM=|4Jpm>2QEOG-~jdy1d=7jyd$j~?k2 zbDL10waNgOg<9yE1!=I!qDxLJxoBrq3(yEk++;gzO_=cpiJxsQH2(C_!@NS{30X+a zGY@FoZ)-nw^cY{+)=tPDa-O*tn*PDjV|zu@2^m1aGY4qe-=qJZqsMgl9*rz2HYrC{ zmW@2nBFut#6cAl?H%bJDVbre|32HBBhv`vR@N9jd?kA5P*kyGmq~dtbY@lv`JpA8B zkM6bO;e<>Y@0oR>>nDyL+H1N_aFBze$OOvvj|l(Q(aJvmQFJ*0_LPd{`)9xF8h!RH z*IxAIyYokGjPLk&26yh<{_)q@{9bVRz4zBZFTZbjZ}VRN#_t8}eSh=aKmP9a-A}yx z&O3kSoj-Mxf2VqfdgnJj{E3G@bYK*`@!uaf1+RbC3s%7!KXw1Ru4^xu1@HZ?n_qkH zTMn#(H^1Y?Hv(S3AHDvuv!B2ASI&Oy?E3(`Up~gI0N#IMCra?iCrX^NE6^<3itu!W zZKVhy^sG=uWPdbkvl6O__|Q*_#du5Ryo<08ZGikJ^v8_w->ml>=IVq-hWtU|gtmnJ zQyb^pJXgV-&^YIN4-zLd&Nt3H(SfCsEV})1A^I`sP%z>z^R}a+a?w(R{xS#~ro{>I)|HUqjUPIoBK;NV&qOVf zTx`4sB+9B!+S6nc`Rh{JjvWL?VzG_(>;CmaCrX<1vYZmBFtU

  • @bRTBQr3;(ZLv*fDw)k!pH~=aC9I> z2B3$d12EEoGvH`{jLrb3!_j^ioeoZeqkS zI9kT&B%p;O5k?xIf+HbDYK)dJQUN6#2{2Lu1spA6qyTa_TEIvSWN^gCNCu?no*G6{ zAb}$uMiOu$98nmZ0K{;_#YhZ}ha(O~$AcYkL}Ii990x}LqvOD_a718qEI0-Z-+|FF z;Al9!0HdS9QE+%ZMn{5eaCja@+c7#9qitX-91bwr3O2)GAEPZ8^)T8DHo;*RqfOv& zIP73_1V(L)4hM(9VGE`VSFxm&~4TnXH_6B>wVF9DPz%m@>FbfWtIK3xE%YDUA34!eJ642zYR~6C(<6;V^*_7jWS49E>=C zgu}BjA_0KIvoHdHfWtE}B8b1kVH_diZ^U2WFow}ziPzvTiqUJtU*Ir;(O-yH;V_KR ztHdjC7{cfk;$=7tV)QccXE+RC^b+wWIP_!mC*nmo^kMWO@d6xrF?xY`9u7SiJx}}* z4&502k$4UcT^Kz_`~eP~82y2G77iU4Jxlx^4(%BIo_GchZ5Ta6{0M;5x@dO;6fzcDh zf5PGE7(Gt>0uE2Z=oiFeaCj<4j}bqI!&5N&Iq@hQo{Z6>#LwXHB#eGWJOYPWj2x;(N-(;Y_z@hQh|!OTd*JW{jP4=shC?w%cN0H^!{agf0dW@`?!f3S;`?xT97f+K z?u5f*F}joZ9vmKn(f5cu;1Kg4@m=COaCj6(-yyyYheu-cZQ^z~+>X)h#JAvZ8%EzE zZiB$#4T`m7)G}c zUxLF!G5QaT4#DWl7#)n!mx!C;5c3~#Gx0?@#QaBmk+=yC55VXq;tO!NKSp05J`acc zVf1<8-{EjyjBX@82Z#G$^f}@NINTef8;H-s;a(Vhmbe}cmod7Y_zWCk{v$p^TnC3j zjIJX-4Tnn@eTuji4h0x}5~D?ot|dMJhYJ{eg180_G5-=}5g#Q!0*9FYh>s8-hM4|{ ze<7}bnElWz4Bhg_!e*i-`9?OnJn6h<8KGc*MJjcR@^e#Jh+KA?7>c zLShACx+5mU7-F^~#>5C>vLi;s5Mr()hQvE5h6BJmE})7?TyQ>BK;r!Ks5}zqolE7A z81$(u68#=UBhl+p86>(LDvd;^O{I`%x2Pl%ttPb-iDrXJAW^ST=O9t5QfDJktx#tn zQ7KbrB2i+fI1)^eiXl-fP*Efbc`AZLE=z@x$kJ2@2|7ask;tT}01~Mrs25woIuZ^$bs7?O8+9rYHY;@s5>^X! zG7=UubrKS06QxDMWTZ4m81$4H3B8U|A)z~iQX+B2>68MA(@&%1NSt~KB}3wrlPM_@ zC!@&&5+`Y?6OquUsS}V;t0*xNDkXJ15=sTN0||wkIt~fBj5-zxnUp#P38{oS8i^B6 zppHV~1Tl3a65`{j?MNKIgW86~amP|ykvR4kY6}v_98GOT;^?EOO-LNMojL-E?c1os zk=VACIt+=eTc|^k*u0541c^;YPzNJ%#NpIINF06`bs!Rl9ZDU5#32V$`y+AiLDYUo z9CRSHFA@hHK<$IX0sB*XBeCDU)LuyJyAQRD#6EjdA|&?SixMKS*D|$)#IlGIAR%0$ z7LiyIPzy*17AZawiwhJ)Vu4TbkbpdjLIV9i7YT|>aggAm)A&emNHnB^1rpEygXAs5 z@=Iv;|HSgc$Pd7GmTz7DJev1kxqR93yO%FmZXv$_JD0=D&SgFF4{*Zrb~M-D3;79n zP4v9zX>?=Y0nuHe+eBX$-GKZCd>GB|C!zuJACMKDCGw-0{b?eF=s4u{{{Ybvn$y25 zd{+1*@+)w!@D4PazX|ylxJr1r@FL+m(LBB+ObMexH}W@d5}LsuB|J>H4|;LnZ%Z#O z{dVc`rH9b${r06>mTpAv4}5g#(xnU0`+^O0JK>zA;F2A^N1$3derfB{K}(_~uHaR{ zbLiy4qk{VccM86NUKqSyaJAqH!Nr1+peLvZGJ-gIv(PL!MIaL#BRE2^pI}iyEWWh( z40^%f;l+Cvzq5Gj;^!AXwRq*?WsC1#ykN1lSX|t>7+!QP>K8SOCoFDXJY;dNMIO2Z z@%+Nm3y&>4uyEJHZRmA_8y2ov`0&Cd3zLPxLTw?ta29&!z`AhSf@0ygh0O~GEG#XM z{FnL9@}J~C!oQb)2mfpQoA}r9ui{_Mzli@%eurP;r}$C6n{VWw#Fy}o;vdG}htG$9 zgD=A0!pG5hiyxvF6K;VwqT3c9g_pt$;d!tD^XUDAAhg3Xpb8!jx59&<2y)R~jOWnF zj7NF*p?4L&f&498&%2s;1@B_yccI6t@G`tO^1oo_ox+pxj^Q1_+mE-1?rpq;yfHjM zJxtw0eFu4E_&oI~>PqS|I7;#bqKW=#pAw)d^SAIeT@46 z_b%kS;mh0`xYuw$%)Nv=;SRVpZWeiR@N=!))3^%maoo+^1Gr0ElJhd>SzL# z?%;fla}(z}&Q+YtITvx>$?0%PoD?U@aih~CCvha4qd13g_Tli!zmYGJza<|hA0mH9 z-i{jT>K|OeHWBAo&M|NW+eB!te>7adHWBA&&QWj$+eApmk#I$a(UF|(a0S~$oNb(~ za0S~$oUNQKa0S~$oGqNqa0S~$oXwm~a0S~$XyxE=xPomW&f%QH;0m^hP>v3TE7&ID z9LhNau3(#pb1>&1xPomW&Ow|5;R?2iI0teLfGgN0;vB%)AFg1V2(3fx4Og&D#Mu`k zY!jjKA%txr&fc88;0m^hID2uH;R?2iILjOnT){RGN5m1r6>Jl6(6Jo2f^8zs5=Q`6 zuuX*4Hx}Ruwuv|k96nsZHW7!$QB!4+&1ad;dGu3(#pLvgrp1=~a%E{6kG zuua54!#P~RHW3GK2)Kf6BD8Mucet_zA@c9!-{1D_p@g5&2j0HMoLpBJwrz zFK`9hMC7aFD{uwdMC2>v%WwtTMC8llpWzC&iO4^bFToXT6QT8(Kfx7j6QQsB0$jm1 z5%~i7JY2yx5&1m%N4SD*BJz*qb8rRQMC5biAK(hMiO6Tk-@z4Z6Oq3se+yTzO+-F} z5w?lQ-;uw8E7&GNt>$UCf^8!5H{`G33bu*Jr^%<_3bu*JUz5LrE7&F?pCX@xE7&F? ze?|Thu3(#pe3E4#W6S#tHBJ!u?LvVtPBJyGKK{&xy5&0AH0XV^C z5o+c4!wI&F$Op;$-~<~+$am_G;RIVorMaDr_k^2g*4;RG8;PO#)6Ko-ocaqqkkuFgp)Hdx{>@GoWwEu z9C-tr#4x&n{4AVAG5RcdJ)A@^x}N+DoP;s@40#=#gfRLv`AIklV)QAD0vLUgycSOU z7+p(#0#1AweS*9OPP`ahLw+1iJQ#hPyc$m27+p<%3{G4aT}A#IoH#N1H}Xn2abR>M z`LA$d$LL?lkHU!!qmPmwffFl6A0a;sCl-u8O#Ta;m@&G7{1BX&F!~UAIh+_Vx}5wV zoER|rAbA;_VE!X7BR>EqnE%KRke9*<=0EcN@=({v+Q>g!zxWfIJ^gF#nO~ljp$+=0Ea0@?1E< z{70Tk4&VgyA2}fVaDw@d?2#Qf!Td*dF~a;ucE~oIVE!Z9WD8C(|B)@S2`5Km)Fc~l zauh}lvJNLlVpJomaIzhvDp`gT%ztDBBg}tfnJmEx%YP-3ffJVh7_tZ_EdLeB0-Uh? zS0M9n!t!5^%)$xFe_1jECoKQb7_t19A=7Ze@?V-v!3oQMDKZHsEdM3Rop8eP-%c_C zCoKOZ$aCO?<-c>tv*Cp0zq84+;DqJBGs!rdu>2P%V{pRqUyO{x3Cn*`G7Kjy|3xrj z`7ca{;DqJB5E+CMmj8le08Uu`3y^*|VfoKb`rw4+KOgCZ6NqcWOM2jhhmo6f!U@ZN zE{wSNzd1<WGkZL$)`Ab@j#>VbkSD@1%YP@5C%`ewe0+e97#$1MLHK^_jrEdL!&9tOuO{~bmi3dbz}9ZDVo$1MLHOdbTsEdL!u z9tg)Q{~bsk0LLu<9YF36$1MNtPwofDEdT9C?hD5(|Lsfe1IH}??L+Pj$1MNtP3{H9 zEdMQ&A~|IokSnB_mTGziBm{{fPKW0wC2@OL=I{0IIH{szaG z|G?kCU*Q<@ANVVH4URGYf!Dxa;285C_zQRyjxqm%SHR0~jQJ0|4E_wqnE$|^!Ao$A z`47AV{shOE|G=NXi*Stj54;FofMd*m;05qJ9Ao|ie*}MkW6XcxIq)nTWBvnw0KbQ0 z%zxln@C+Pd{sX@Uzk_4Uf8ZJLTR6u22Yv^B1IL*Ez;D6RaE$p6{2Dw3$C&@XQ{Y!{ zjQJ1z3OotNnE$|&;FoZW`49XOJORg;|InT6|Ab@Af8cTO3pn0}(J#PbaE$p6JO+La z$C&@X&%vW`jQI~d3VsI1nE$}fz$0*s`42n-ehSB!|G>lGCvc4U5Bvl?1jm^Fz(e3c zIL7=39t01-G3Gz;0Jt9_$F~UG5AK6w%zxlM@MAc}{0Hs@KZ0Y-f8a;p9yrGQ2krrP z!!hPRa5wlN9Ao|iKLB^ZG3GyT7x+FLWBvo*2Y13T=09*J_#PZ%{sVV_@4_+WKk!}f z9XQ7P2fhQojeOo?^lflE9Ao|iw}WrNG3Gz;EpQtgWBvoTfp5Yw=0EUF@C`V|{0F`P zz7EHj|G?M5*WeiQANVS`6^=O=-3q<}$C&@XSHLZBjQJ1Tf?m@bWBvpG0lo}JnE$|+ z!I$6&^B?#UxEYQx|ACvq7vTu=ANV4;363!Tft$b=;0W^{_&oS`IKun~{vF&1N0|S> zjo@=|g!vDA4%`4o9gJ=OpM|3~MxO=O!%+*P>%nK>sEN^Mz;$rc!00;gX?*@)jFu>u zKeOx?{Y7-G$R&J9_%We%>5piZZ4f*o_>kZcZgNQ<4gR=gkB3RN zdqWiEO_r))(#hmRz0-1hTTG!K7;x4TeHjoXW<69z}56VAVc1hg#a?ie?)r ztDz|g%Ow$U+3IwwZRn}%e4sCPD+U&SCnF2zyRW7R%8huTJ7W=Rh^@4b=8A(+UM^Woyco& zq|$QdE7FupmrX~o+6t@onK{&|75%oUU4D z(XI7`LcwvFRr|X+)K+MNr)Mc?)LBVcC#$u^DR;Oevxzf)gP$?k3$=1to{sdhngUW= zV%7e34z)UYIcPAMD)LyMsBWOAts)MU1i1~cgmM;zJ6<%X6E1VtU`#ig!7;vQU9JgreZe`F?OSm-E;5g5!eQL`d zk8^ELb;bHp(p4!2Dlv<oSN7{LFBhYa~l>Kx! zR!Uc6;eo4DNXZ)V@W7{R)O_fjY;jM5IU&ia{ly$=4P~V*Z1Tx-YMWanuc@@&n%dK@ zYBNr&xRA`c(feukuuWa|I8oj1WYs=4huU^Zo0hs;%9^vA$P5CxZlO@>)2)VEueO*y zHjB;S$aou7OEsbijuWifpUg+XV0`z z=RKQM`?ERJsu`oMr4skDrIdrAi}6md;!Nkd35BL<^E4D0#*?#!Oj)}$hla?rShbJL zp*A6IM?7AoP2AVyn~tO=Q*rncMU%K`R);OAcw1%d3?y=oKBz$r>`YeePv=nUuDJ@C zSdFeY^Zjr~D<3GM$%H20&*;Ji^ssoK64962jkLHDMBO3Is(pA4wK=6$pJ-KFb+e_d z)SEkqbQDQrGNo6^#U`0L(T=AFp;AD?h|z!^W7Ym-4z)~Qrl~qiH9alY^|H=H-!jk} zL#mRz7H-%(Epx}F@zofGIOj(rZ)r6w(G!-9}io56+=BZZZe_4Lg%ACKOIx+pn{#rM6(BV((f@ky5mp zGnCTuWTD<-P!^?|ck zPQ`-b5Uck7In)lq;da)QZ`heKp-5Sr&!`mjq`a#NXHp8Lh`K|7Rr}*P)D{MT znxibQtAj;}wNb4V6NZkoo2XfI2A|0ml`&R%qaKiX88fN`KdbiMIn>s2ew%I(Y&knd zX(CfcZ#?GJ4)g}1SF7_%o7sjpn(ig~wTL``h|$NY{m~q1)j^j>F6n7?*0A0oRScY& zhOJv{*!0F^w%V-(D(!~e%+TIo3sH@iReR4IYAcbtyN+Ihj2a_mMW&o^GHPoz>lgRY z*9#g4-mKa=5bKIkle!xmdswx1&!JY8EUII=v{~I%Rr-TqMyDC5?TM7chs=GZR+o2s z$&SuebLh3G^SW8JKism%Q`*~;CW)ujpnb?=Q`8YD2jvEdJLNN`%#w;&oi7yf#Za)W zNf)|)WPZ33Q<4Q^SqX`sVJ7t=Ml1_mpwGa9s^uGOSQ4${KLn7fLI(ZQ%c57wT-Z6(-eYV!lrid2thu{R4!B@B=1pc4-dlz?XfwV7i@qu9b23pOYtN+}sttvxT z5{1mxlQv5glH)gTZKCd;EGMds42^GJqyMf@s?I-mYqH(SC+H4(OSOsq+nHOpZ{W*q zMS8a@=6~0g4d1ba>SbTb5z1yv#kySW=qkOQVlCWdv_XTDmL*f}id8I5$Q;eBXQKpa z4Z8ED3~rz!unJoJ8jXs!wR{5hM{zbrdBL&LRdmC;(n8VB-TD?8LE5HwuC|_!0%z!2E02Pz5N2zk z*-Ex>QO?Bm!u}({(0!Z-th?GMHxlfpmMYPW zHiblm|I{dYKH1FExDA|(^B_~Iu%>uDrtqj0O1sW!YxdKsm@3O;(EH5kRy~sMXI#h( zsmC(WdRkRwv)nA?D#=#6v8rfIMzC%gy?c+|mOd$&@MLsVoQ6M@(#PmQeNa$o({w5( ztE9?-YQ?pqka9azrX93XVaTJIyh`62)bbTy%{0*I>B5dwCTT57y>ePvRYp6WM%&h| zxNP#QORFpyeOu4RRoJfJ9MQNNpr)Rq-=gT}pU=6lIX2^d_}(_yzJb=Y=Y}$8KUDs& z?1vi|Io_%tVrPtrM!STX3i?q01O4yy4B$pMy|(wJZl^@q`e$}KsZ8;9-A?Za#MF)2 z4ntV8!=kV6P-jY3Nj$4LF_cR-s&c!>q6qD9X}V60=^yTPg|NA#)r6aUy|Lc$>Lg`v zBw9Z)Z`Ot)J3{7WA)Jv^f)=YT6Ft!rwl+hdUNKxs1$#k##ht1vEuP4UwO+5LGUj~t zU_QCyL}%Hr>(1BhP|;Dhd)NMB6FdBA!S)R_rvI{Tr}$sl?KUz#<8flm@Q3^}R4WBE zJ|vRpUibf0-phaku#=c1)%6?K@^xw?k5nmt#?FF6Qy}B|%?7A1&$q zA#1}M8T6eoyH-|Fo9hx^^B?Y*=5|Y$)#?n^Qihi2rNyu_szk3UW(NgrGF;DEJ1M2o z-I8bvscx)h_p3`My84oqrQK8xlI@tQZP}sGlpTG0X-BZHmncnLt;}thuVdn(qmKFR z;@F5g=KIrNBj(_LS;v&WRmc2qw=*|tsIKW~wQ3icS|wy!m;R@ESsU4VQ;TkH8x>6# zBofwg!f_KBI-6{lu)T`s5U)=Y&fR59V}Ic2MT!$P;*HSm%8CBCbYKeDrNyR)cIIMPeQqu@02dvx&7$aNA_@-Vq8rnPqBFMN zMJErdq>(%roj&g6@?62;G-113fr! z>D8qlq0|3;>RPIg^MGz|q1y}JCc^*qYZk|GeI&WXnH)G;X=}Vua10Ecj-|WSn~v}l zp>N)xCkwsv20cmWnK!6b=$;03q@(VLJDYN*V@72xSR($Kb<$|1DuEF4SGN4C-VlqkMq#H zL4VA7a5d#k2K8=;S?k+{ z5>R{38+4J{OZel3=Dfl8q7Ilh=rXncyg^0Oe)9$uQv1#ubcxz$8r0d*`NOuFJx=?5 zy_AE_M%Os>LB2LR^9HT*+Q)Ex03hDlgR|UiTjvb8b<3Oqw``s>;O0$p2HbSSoB@wG ze9nM}AGZ2^_SpGKpgeC-29)Lx%B%*ZTZWjq*9i8K$mz4C;#uopT(ls3ZQh`K;a}zr z3WcxE8fm{U z?n50kZ_vG|19#=MUJeus*<{h}=(Te7-QI@aZQ~s~Z_us0W9AKdC~wQIpnGbB0^#50 z4Z0}&>#m@CD%K&q&GQC5n73)(pa=1eSPiRSi$4&N1YPZ`*ov&#nB+{u~y;j!O&5f>UU|H+=R`hkz zzt0==YoZ(H4f<8l=jIK1t7x?w?m4et5v_K^JqNu-wAu~#9P~d#tKD$VLBA~e|JeKP zILWFi|9Y=ebsi-Rs4xsoY|5@z@fD)XtDN(zM5w7;ufnSwfg#k$paNn-bah2n0a;gB zWd#$WqGHB8W{k_SuFJZv%Nl>@-l}GLdTOTMn{B}Tee6H(x2vm9-Sh6b=e|()e7{ep z;a1aS#}YXl0mqhC8Ouuq`u_h6NkX!G4cvWS4fpd`!d-R`?$_FS@5bP4|z&3~A4ABm3a|lky4v5&Tx7{Ilk!T0D zIRq~h?Z7sN;04$L5jz{VI|QeRc3_)BaH?nrwmAelu>&GLbh|@vif9M6IRqzT2Sl9V zR<_xJlduCK_Mona_%3eiZz1;`I8n3%BEEeT;=l=_9T2g#R)_=7#}0`2(CrSv^F%wa z%^}z!+5r(eFNB0(89N|iZ}ax1Nwfpo9D+sB4s3G>7O(>%_JMDA7R-xwV4FiQ zC)$B+4#6yTK*a9n?GAxdv;*55f*H{cY;y>vu>&GDzHV;_=HMIjYo?@<n(zzEn~t}mCCv%m@%gYWI# z%T{0pycm=KjDa^UUB7hIQX5zU3!n*b#Zm;A1D#9er9GeupjvwI(s@g}mtF`A zf;*PxmL?bf1T2ETUi>8}13U&yg5OyD^5P?lp9D6+4=&!jc+cWHfl=^wPzSha@djWO z>@PMKdC&()E?%}6TJ$d3fnD%ri}a#m@j_r2+_iWbXap=RN*5&yPk~Cnz7p`6CE>wYSaPLB7!Mk7uzCn6HzHr{cE?^wou^?TLz!Q@v zK`-F3`GfP10_)&I^AF75JAXIu4&FY0>->%L*8=lkbv{48cRm8#gVy;y^YpwN*avsb z@0{N;F9rU=r{H!84-Afv51`%%d}p z%sd24h4;?fJ#)v*?Z8!d_ zv+(ih$EFWXKMJgc4^2NXeed+$z*~6x^sUo3PG1Ymh1Kc&^xo+Ra2Hyq_e|5%a$qmq zHNA6s$Fvmq3!j>La_aG^$AH1`(WytK9-4XpI1KNex?}40sat`?@Y<>FRCOv3Jcf}e z@04|F4=@?Zr_P(&HMJAC45d>N*x~Xduo*r!c@R`A9|1nY2PW^GynFHvU^KjS^2W() zC%eFDn4jD`8JY9~tKptWdQv`l9`G9OoZK-fos-2(LbDo`yz zcqM2SAnb!;0m2^W6(H<_S^>fiXcZuAgHi#)7U&cpY=TMw!Uoa6SO={F^twjWFjhgQ z0KKk&N&!M1Gzt)wL7@O)3G@jN7D1f=A?yvo2pc~z!mbXCuw?@y?8m?e-*jP26KRYo zB84$YBrzsHlK_3*UScoCIOq|e*H;i%V7weu3DE1yK$8IBrJzWFFh;~MMnR1Ly^equ z0m3j5#ux%c0`!^%JpzP5P$NJX04)N9eo!Jn=p%d>y@VH|hwxx@gBAgL-vvqp2%Vrq zfY1Rd1PJY*L4eRk*f3f_i2%K}5EhJP!i>>Gm@pa%BSr&Zz^Etm7#V`WxQEz-@e)uV zK%et6&>ukfQcxd2crj=XAbbfZ40feidHh}N~&>BE^J}3v50X2)CAD$dC(F-I0s4s2xmb@0HG9A z1Q5=Eh5*88P!K>kMNDCw1RVkN8mLV%f_4B#5Dvg70sR1YU-D;A4?y@Q$)7O(5i|tQ z>py^k0K(scegMMXfqDSK{{ig)ginET0K)$U-2jBYmHZate}Q@cdi@*F4nX*ypd5hk z*Pt7K@IOE`0O6CM8G!KLK`{W~uRt#V;lF`e0K#8_Rsh0(1*HIlPe`7?_%EOrfL=ci zY5@rU8MFcr{sNQ&5dIu=0ucTTR00tGRPs}dKau`wRDChtn{0OK3Ap9_B03iI3fW;8(9Al-VL1p2=4;Me}s2R?!@>m$-6MV6IlPz-~SFc{}H|e82=Hz9r*qc-T`d? z2;T-={|NsUnEnyIRq|GhZvno4^!jgr?H}QrC2z*~CSdwUuWy&!j`59>H)6aE=>E~) z-vDg?2wx9m{|H|PT>l7P3snCIUn6-9##@2$AH9Ax5dI^)1^E6E-VAjA2yX(me}u0B zvVVlHl)Mt-D}eJKy}l7B{}CPl#(#u20O3EvmjmBF!u^u{7zaT6k6vF7tp5nF1JZwl z*8=B1aH3yx4Qlu8L+#a9qxPz+PK5Tpf;UGZ7PM@WD>QB z1Zwy0MQuEe+AFR=?d6xF_Oi=Rd+DX9jm1zKjiNRZL2WpU+E56!EQ{J;5Ve5-YW;rH z`h2MMdQt1~pw{h1t;>a4rxUdf2Wsth)Y@#QwOUbYv7pv$My<(&TB8xQ1_NsKdekxu zYWM6x?Io9>_GK?a?Mq*Z+KVqn?Mq&Q+KVnitxktpnntZwi&~8awQ4nLRVvgfm8exH zP)kv$mCI4PwuaglzZkVL8EP-Q5VfnTsJ-9<)SiDnYR@|lwdbCT+H=l9?b&Cec4Y;% zXPt%G-MdkH=9#G7wF|XpoPpZYPe<*GUWD2gz7Vx9cmZlpI}Np`o{HL?J5hVeDX2a9 zWYnH?5^7I85w$0rfZFFjAGObW9%^^&K<)A}YL}K!ySRwjg$2~k&!cv34z;thsFg}l zJ2QjY>1otXO`&#j615W(s70LLv$N>?|A~n^B@35J_XGU(=dUC1*Ae)O8-Z8E26j{v z_!l-`W<$3hrczh3=mZ%jxFvRTL!qX%x}BvAP*G^f=e4p3pbP#B4^ht2zBuL zGgv`L>l8HfFt&ow6{3UkuD_ygqzffoPg^V1WBGQgmgXZFmgefdzM{tXBHd{7`1w2Z z?#^mU(GD?1mm=J5C~UbxJ6rKZV-!%tx-DU!-#?7WfZ zs!;H|8*zc4|44HUm`c4-S@Z6rF#9OczYf>Dc&lE558zv+(#DiW3AsheAFU*Noa4t! z?L$+#qZSu;{8eu|)6)8#jbbUwXZ5td;^cHDw>KROmU0S{E@{{5?ZEQP9(QrEJhbLx zOmeDTl=t|Exd3aWsaVceS3NF8t6S?D{Iys<#kZ(Re=QOz*j158B-HbP8(MAB*ydv~ zf5e*6`7C z(>4b~W?Jv(0-<0rkx8nxYps0M-|R+m?ncV)4i{p_fA*%>)nYfPRx})0;P#@Uu~sLm zs1~htz0P6|8|5@l^?>Y{br)<_Q%D&K>nwU-#%p5~J$r{&w@uxi!JpH&bfHGXW($UG ztK}>sEJe|wLyDsN|D6)OWcf1qPF}j01&zI(^ToMO&7CmIOFu8&HPZ$Kc=^-~li!)7 zCawc*E1I|ko+s-M-6CCg`XI3^na!N*lSts3|K@+871}*9)(;kofAVSJ%(FjYl_}EDB$YvyP^Za#Tmcjn-7I*HFn{YQ|KSN`y5PL4Sv`H_={osmS;4vw50b4J?I zUwlStUxobb-YeMOm4OfWd)OICM4{ct-wkJ^E&ar2q~?jp&*rZL`+5Gri~Kz7j3lBO zZRF?XGt!pM;xkeeIJ9oQzxJeHXU`dUkh6!KkwnyOjhsE~jI^bv_>9D#i#)9u1$(+{ z;6|Pvc199WUp4Y{bVl0JM|?&q{wMOW6c+5`=>ym1Gm?n1uJyRsSavM^a_MH+oBx8vZ!W%J@ruP4z;5~L7L4M@#&hS&Sgr?PSTLy^d#Z1h>Ra%wUXXsBu$v6 zeCMAJA8(IvY&Dtml$=Rxxa`d}HvQ-5<433W!yj`IS6n>hQ!Wa3a^un~z2cH7pAH!d zbYas;hPYIsoASxf2c%8Ca=DaZ+l5>s-wyRkZg2{h_F2Jx7>QB0l2Ksa*Q#`lt!?`>;{_&icF%XgoZJ9A#c;<(o!I`ehAFI}wL9d`2ey(z;jM zGF!Eqy}EpTumyhnhQW@nhgzWNm{9Ls9}|HeF&dN0Mq$qM0)Y_d(UkmEy)oBu*w{^n zH)i!G3ZnrPYJ99PQ`F`amf$3k^>H!U0*RVipD%ej`AoeVXpZ;p$Z?sitw%-RM;waE zY;`>a1e$J4f!WIXWD{t7tg+E3Hjm9LzaDOZA3qHDY1Z+2@OjJz##ihYd*hwn7>8;5dK?5A zA1e+@jc!wq)2{Ua75MRo26Wo7KA-|kj~vix=lXyO{D|RzPFvRpG|Lr&yw{Zs){PNs z%@f&7r#L3*Cu{2iD$w{?1FE1mucDL9ZT)!7rg%+O*W)G7`$O@XtgpvQpy`qEnry7c zOW;S05}>jkuTF~zHYvKj#11iw?Sn;BCo8vWc+WLSB{P;ry zI`N|Q0TpO^S|3n>A2A%ziBs3F>V%pdE^IR)NOH3adh~`O$}&(D)1vl zVSVR%ScRHx3hQ0#VHIdUKCJhwhgIM$!^YDUu~#+aI}2Uf3N%K0eu%{STo7n^k+~reic-Qp9po}?}3VMdX!awaH6$fr-PopSKFjR4DmR%|M|$KWlhrws;`whx^UsCHpLA-PQ_Wx`leH6 zidqucrZL%Uo4K-sCH|S(|2$;lH}P#{+U@I^wG~^t;#!; zo3*vFaYwIWw{_}uE!RrA)GQs~t!ZVB_zAUt0ofRB%|s-ilod-!&f5rjiw`ix4o1jeoXD3M>br`uD1rlocSEmKUg%8Vy#X!#1hrcK9XR7GSn zi)=!Mw6!ba_)aoF`@=3e<&WwEdcGbD7fVH&)B9RUx~Mk0qK+uCks=$JD$MttsAZZp=Q;q59F%JXj<9Ms@W1t{D^`IG_cXz6edQc==7Uidi`WGqIK1*5jX zB>TKd&t!vrPb^COfZ9KWY#4*P9gm087B-Y-nO2(Pg8?63FBT2fnzL!r<}<8?PxaKL zGDCcy+CPbG>dAu55NXOw7QHveIQ*@&qO5Ijeow@&uf%1#derF(ws>c}>>|EL?Vmt4 zRl1t6hP2gQtEiyt{dP{rSwoSo#iMFv1Fe##A9nfcnQq4Hq>1lR`$=TOMj4~ulPyq? zsO64Nrl$h3YTu)2$=Y36Iv4CXb*w9rK_xD`JvOUt~ zO~_^+vN5ES*^a-E)7dIAmmwBSC-St_%u~@u++D4geNANscC~_9cY-FqO%1L_Hnl7h zkcDclDkH1+O%0Pl$y5z`gWagAD(&@1N~erxLW+u2A(Io|q6SwX8%H~-smP*A-Vo~> z%G!#$+twEv%|bsCPRG)1XOnJGeqI^qYFOf%)Zj{FewN;UpzD_H>%A(iaifGkcWmZmnl^S%Ajhr@1P1~6cG&-!2N+t6$M^~Ou`;biw*|cb{Cz7jY;#yW8%=wEl1tUw!`FgSx z_giDKv^l5BI-4AwHQSI)6Z@t!)NS>e!_3HBdR}ADa4CPv;EBX#jkLN_3sZT&sT$80 zOU)>GzvScF=nkWwB7oouIDYhyF@e4wXvtNIpyJFN)iI~iRL z+3?85l$Yi5omRd=TXGtA+U@Gd%fV1CM)x~@dpVd*g_QYhFIBqev<=VV-7dwVce5DYcySrbm_Qm9cLnWTP+FmDzkQA5WNF<)*8n?Wa;c zf7|G6(^Qymy7N4hbTQdfcxY2ZHdPoo#L503v z)+QA(w$u%(`KGqcx(wb#$VYsM8f1};!s2HjPhvW4#}Kge_+V9)0Y%uNHL0m;%`uCv z=2izS)wqkU5MQJQ8Dx{}$n>Rzi!TKDsKv|4l0_w5({*aeLZ+iNr{p0z-w)_oT++i5 zU!VqQWYZ40ql%EXYBfbYHhW%|^Tye%zNho1jQK=_sd@`cfc6+7l~NJeq>zo;p9xwr z5sjhNu=UJME(5~Wc4aX^N4QMNL&XhEzUK<5l=h;W_&ha8A{(EKH&+a9x2syI7nBxG z-wEh6R7Mk()rx&*s?@YZiwtF`#AqM!IcktVHYJ<=X#rUsWI8xcD-iBD03%aDzTos+~TsllbVpGuq6WL>p-nszli<#H-%DEndz zODRHXLtUF-i%nU?+O?-!72*@rAcky2Y$hZgq6Sf9BVuD8@o{PpK{g`x*AWj=gD|oY zvD1zC7&QnX8xdR2h>ubO7TH+S#X`91w`aQ$VSAjEaF4dAb@N{?2jTIpayv4w~D zCu-n9HX^pu5cg37H?k42tA+RgHEJ$&bP>|EnL+E_JVKmx>tHttsq<)hTPnYL|)_cT`#?Cbr`X zctlK&aRodZAEY6NHrD5Dx%USdG%|b|O8cO)Zv0o#6t(%`6|&k`pSRsLyMpSN4?FI^ zYL}vz4C|WWpa$CpIol?Zlcm&Jxm+dJXelj?)KSOQE(I!b-CEwHvh`NmYuP}s>}J$% zqu0{Sr;Oc5K(n^y)tQu3aJ6JR?()K*p;cDP&5>+3s&Om$NZOQQRQf_aomKSI;dCRU zgt}whDwFlL`@yoVz{PTFR83cmbwUwEzeGjqdA*%q)w|@CwJxIyc|tZ;SnU!%bf{gr zp*3)!)#g<_LQK+b+iIg2S6(P)wYePPhil!Q&W}rXi#bhg@zyvSto7I+Wh!BGG@}Nt;&2Do z943=?HLO&1mFDA@C=XQg#jrMKZ8Y^^&RnyUG*nZO(sn!!ebue6s7jSuX>B#%Xv>uz zj#k4n7K<{K%?E;h#-gO7ElW9?WI09J=d?S_RSlP`g}MeoMT7XzA@N?GA2?Csy&_JC znXcQGc+`=JHzt+PpEk*Y55M(zj*x!sZZcE7%bxKi&r&gATqM$U@Jr)gbTJxI2>O{TDF`cl{)b{r}r4T%5V3=ON zruK!kEqyO%q-$*0x0ZIwFmNM2H zY?4+wI&G>Pb6c(b9&2iLwaq%;uNz!cy~U)XoubBS$O&1T)(?FQ_l}s%gXL{*;ZY(p zu}8A}6tMq4zWf+)|3A9?2r&OYuzWA@{@($+0&ZQtars){{I4$Of$=}G>;=C6Jm9G z4lX_lnh_5zJ^+dlcQ4)ndJ(rS-Uw%rQp5!r>$uh-J0V79p81rNvV~)&W%#vA* z88U-0O{OuX$P~sTnZ%eN6BzfBdojkzIL0f;D==P8UXJlH@-mEGKw)m zMlgoSFvbuW!pM>=#vmEQ7$5@}{iGkGkMv>ml3t7+(u2`Wx-q&)7e*)P#ONR$811AT zqm8s-w31eg7Se*zOqwy8NE1dQX~bwC4H)&L9wS3C826BSFkV7lg7IbK%P_u_d@06@ z$%`?*gnS9ci^z*G>PQ_%nxrvmNi9YVslliw)fiQz3Zs%#VpNa{j1);>l#_CdYvdZn z7n3i>C?jPUFC;I-xJs^Kynws_#^;mI$M`(*c^G$)J1{Pj%NUo)C5(&YBE|)B0pmP5k8zHi z!#GRMVw93nj5Fj6#%XdI;}kiCagvPvY?}^`I{2lQ-jQ>OY55}j6r!f9E@!uGKOZ*n&e-ZzM@i)Y8F#ad;pBR5l z{2Jqb5dVSkN#aS2|4#fn#$OS?!uW5*zhV3(@k@;VO8hIvCx|C7{tNLh7#}Ad$N100 zKV$p_@e7PUCw`9cXT;Ai{*?GB#-9*B!T4k1#~2?Y9>e$};zt;NNc<4v4~QRN{66u0 zjNc=^hw;0_cQJm4_zuQ{#6gVTCccgFTg108ev|kn#%~bc!1#6I>lnXAd=2AQiLYXO zlz0^5SBS4*{4()nj9((Ygz<~S7cqW;_yWey6Q9TUIpT8|A0ZyW_*vq!7(YXN2IHrR zPh)(Tco^fSh)-esB=Jd%pCCSg@gd?Nj2|aHj`2a_L5v?GK8Eq5#78lHg!l-?4-+58 z_#xs$7#|=Wz<57#KgJIdAH?`i#6MxYkGKyZ`EwG-gZvqZ<3awE#PJ}1LgIKpttE~J z`51}gLH>xu@gRRl;&_lhAaOi^y$Q#I{2qzpL4KFS@gToL;&_k;NgNOI+a!($`7IL1 zgZw6m<3WCd#PJ}%PU3iwUn6lm$gh$(9^|7WjtBV_632u5GKu3seu>2KfIXf#9^@BD z91rsIB#sC9ITFW%e1ydDAU{juc#xkVaXiRRlQ zB5^#(kCQkaV`X@5#T% zcn^6G#&?tN#&|b*H^#fjyD;8K-ih&Dmp3UroLm<1OSZ7;h$T#&{EX6UJAOufq6B@|750LB0ax zjpU6O50D2i-ay`f@#W;pG43b#V;qnJjMtObW4w;M4&$}twIFBqSAC`Ri_^a#MDZa$ zK;XO~-cR7XA^wrTc|+Vw;JhK;N8r36-b>)TA^w5Dc|*L1zZco%{5hIl7|^M?340_P3!4g%*5@pb~|4RHs7^M-gEf%As= zTLR||@m2!o4e=HN=MC{U1kM}c%>>RH;!On38{&2X=MC{j0_P2J8-eqNcmsj+2KKY# zydhpk;JhJTOW?dAUPIu#A#Npb-Vm=QaNZEN5IAp$n+cpZ#7zXw8{$<2&Ku&D1kM}c z6$H*3;zk1J4RL_Lc|+Vl;JhJTPT;&D_7ga7hyj7~hPa-QvBGyK9nP{P?71 z@okIOEXF7PxOmCL!;5E3ylUak3qM-;_=JDqZ4;-GphH96MOH`+C`6sQkX?{HJ%Q=T z%&D-0BE+X$%1S^(#b6U<-Jkc{JNk^akfnM~t*IUC(NSl~P|A7|sYX7bP;2I2J2cc9 z^|=&nRQ7FZwZoQc7WEZpK@WSE+di+K^Eq|CwAoF2G`?tKXt;38{GTNCr@Mbvei7dmeU&{QjF^TU&i;PgFlyt=+J3iT&6Qc1F(J?N z6h$+w&DAm{nIfrA_(QHs>?r<| zuNZ|<&L{#&qurtnacNtm*$rpdWH>6f#?xJu%N_K|H1Z6SOgj3bW4vb6q0B@3jJ&Lz zi5DDvwHD3Pl`&b*9M`AP%6!<-FYpn$+sIHV+tAaAFAWVfeZFGJdmNlTnNelZDTg}7 zXZ;q~ESHXQMTc1dDR1>s?LcPa>C7ibhFwormek7}?S8{>QqZDdh_V3oXB;)KUi9y#z-)H4#2iQK||dS;E5T^roIB6IG8U z&eUf|hV2+@g5*```f@(1b9XDUdQ#o2=JJ$RRVnM#UUQajCsP%AG{vXpMuyE?!=~z{ zSP!pGWQqn&-`gr=w8~DF3aBg0x-mD4r@L0QJm*Ps?BR;|R<9w;ADvaYh; zp7hi^0jsVW^^OK-;!W#@7Ih%qC{-%3)t~CxqNZ3@UUaejh{jc{a&moN#`s`Mc99zy zO5Qy(XusyrI#B`)j7+^Fy}VpS}b!;rFwkllCfXyBZ zXBcfF%;}ZA(OjLmU}Tu-Iijv&B;jL=*+A1`FBPdyjHBGDPMmF~TD4wEQ3;kjDmdrh zbJfgChFPUabrPL4)o+%%6<@nyE%stXbDN4oVT{`v;uK6f74)|&A&vq97Ic!E9(9I_=HzGhXA26gJhk*A4%oz}=`eJH4NhZXjuCEE&`%5_I2 zSr3+N^@QERy1MOvA}=3$deW%FL_r&6Qf6&O!}t2ZG(-2b_IOh6gK~jlwWjQJ6K%#; zb?9}Y4v8j|}79 zAjc)?XeJh?yH$5JX)F|NrHWNv%Gv{oQcLCS27Cn%XBmy@jAmpQ&G^E}jI$9f6yzGH z4a{%>s+MgrnyePi0!$|xv-V{Qw@JC);|=SEW~D;|rw(n~?)GbpnR=+kd5XGHG44qh zI&qUCA%lvBk}5qK8_8`W!$>#4MtfRV-BcZ@`O0+b-BQI)=@~X0;5a8=P=jL5ij5tXmPy8x0D#+8Sn zvP#}*&&sL>h1ITh<;|*c$k4I}wHA&_sj>lG)7cr#)b$I8u+kCoYTd=4EfKdAyHF_! zdyvhBvZ>H=)_Pvnr1BWFiiF8MI>yQTsK-z}SvOKK##&LuO*UIBr)gNL_7+EX;AClb z2IT=;s~GL)i=zvNnaIeHZDiT7B^!^aph7GwE2|w|YsbKN+9h{I<8nA$PERAs#iRO> z;q=KPLvKQ5wk8wVqRUEWn>j|M)F$GduEF9p=-8&COR;)qE-EiMMsb`tW8KiB<>g*& z(3J41>%p{>&37v~rbekuw62s()a*&V*`yP6bbaQmFUP>_-)I%(rdT8B$QYnYk>@nFMdYN~Y3CO--XF*-YHob^E3;4!)!tw!72tUw8wrWvRKTR&~JQlV0+ z$rl@(rl5nYusm8iCcZzKtNMJ; zpzA8a4x7@&R$Pp>UCWlDIVG)Cl>F^vJ)VwQ(sox{GrE!^*3WV}S{YBd)Oxwklq^RQ zac?%Gb!2s{GGb{b%_djQ(AJgX-F$GA$dkVwjlt38$SX(hA8n4@IC}qRbL7D2{iDs1 z8!ni%p?glc7Gi$+%F#uj<4I}I;OJD=7*YEQ#cSX$sG-*s7QqSEp>xSXJW-> zJMWI#M^|!-;gMl2Wv%#q`HHSdQNCut9q}qXrDVlw?R9HegO<0&lPtwr6#QsGp1&IH z|KBZnmt^_2g(ntnnEx-R?%xH<^Dmu!&#YehL#b=|{H0GU`4)e;=$iTI%r(=$nm%Rf zeN&fAesS_8)B7jBJz<7t%1v?-o}etlhdfh1xxp#Z86dxth^cuyv)piH18Jv?X%|a@ z0K=!a4Ov9fa`_lOlIs=+Cqq}~UVf68QaQn09hU9mpRv#KNl(1^Y9cHgM2Oqk|SvPPjEM{b=4C z`=B?TQP&T;I#>jM&Ocuwrc_a|KVw;P%7X>y5~Yp^$p*n)j!T_+=ntiih$>0J{T)i3 z!e9>i(Lp~VDk}x|vuQVEZm4S#-UX@S43HkV1Gu# zd;Qg;;T2J=x%Kw&@xy!7Xn40(Ch_K3i>NQ-yMhS36W0O{!@KH#(M{PGhjr&1&7# zcWZO`e5aZB_yqT}X~Dg2@%lFOhL%AQYITBpJDxmkA%EcLRgHwoXD-I!Q50LuBwCql z3{=qkMWtGuu$YxhOkK(6y+OOi5*6&vFi-bh--JHShCZx4h15_CX7 zKPW*(a103UXH$aau5UnR=!ys^c{&bxAyhHM)J-#2>*l>Sx5*{vx_p45Th>}AWlkg$ zE~BdEZAFji+(1pfoB zQ4?{6hW}s073O%F1`+GixEi(5>W6&USf2JQF*L0Aehi(}%_oH2&za7uRxPKFTB#NF zw@kg1$!AP?3%-aops7YvVMp5=%4?j_a-?5#tu~x7K3H(bkGqW2(g#*4TeL>|LQZR` z3Pq$*gElxL$g73tC|k!k z0tMjkB^G=ofG2f2Q*Gv<$`WPCGgmRGRqBqOhc3CttWtNHLECDr9IbHmh7-tAJ+7*= z7v|DD?Q}Z4&b3&<8&(~69+uPvYcr?N`!y=o-da^zGu1G&y4I~bVpcWV$@4ysSIea9 z_O7~>3f77iOWonNH-lk1x)#*6*u1U6Dl3*I)ku2jgf^oKRx*sRJVb{Md3Zx(;6-`( zsur=m7mNb)Q2l>04@F$^j?2SCSJBT={ypsgq}c9iD_P=Fhov2=XnrQsPANyF&|?dg zgKJesC!{d1CQ_?zyE$vGuT`l|vDxjb>VBVE23vVxXdX`Qhk>YGr+uw9sODW3d>zK6x{b~!gSA`qeqM^f^ zt@#|SOjHy8DsAuyDOA=f;n30P@1%hTpZ;{j_TE1UOgq*8$+Q!5ejdkma=0$QbCi4d zN)_eZSPDk42vKpKHaKfByFzj(BW>wX)23V$8>NQfqSSP8KXG-N%A+g%* zG$?${TyR&|P!R~5-(Yqi;D^-9i~3TDEvgST#Ob&Y}; z1hnYTQT_ko^D+NFX!*T&;i?7u(!%0*7w=x!IsXgdL8v>35$Da{H-G)Scm8x>{QvMY zKX>C?Xl`Zp*Ru~z9Rv=5>FJB7=Vou2y=?XZ=~L3rEPZ3?9Z={0GU@B5^iw+~e>nM` z$^N8u^5lu1O}u~N+6ni>3&|&-%IXF(NbV+{l>Aom@YJ1CO=(gppZU|w7iZo)lb_Md zOiq7o`i|u5(w0r7}Ov`1FGZtTSXy;PpWO{wO)8!3ey&(|K zyF*o*FK&~XhZe4;T^msL>orTUpx}bhG{>6ru|lZS(*or?3#+CzDR(A}s_3!@Ti6HM@noF(q4LPMQ;KF*~%-XKM0btU)zeZMa-$_pOR@J#C0n zwy?iS^?f~4rEPGw{B@={^Pp&pkBPSU=(>d?*XwGvhDO~I4`hO@KIQl8TXIFZ=XI3Y zetC#Vvk6zX=aMExTO>qV>>XN&Sg)+2Ei6L|wcTQPzxmIGHP+ z@O#k~zZ+V}DmKQNDQQg+IuWxtJhZMEYS2Mdr0p#yRo1>H>dKjV(Xh#O!aYNaK(5`X z_DkjnW07ZCzHFn`iRa*&RUgf3EGk`}NrWp&dy%);B<~zqB#MeW)yU_pF<;wOqN18? zI7jImoHh{c+Kpj@I%w57O;sZ^Srcth6>U)&TKH3LOT-{gc^jRYD&c6dwV1aEMCV>d z$CXd&15Ibn(~iktfBDIy928+O%0XY-ms2#VxsK16_qM(9T1Fo?DjX(z-Dj@LEB?60 ztMq##GCzCb&Y@Q#EKU(^ak6NOlZF=hjNq(|4=otJ=meU@qK1xSqcU69?a+5UOslV? znijb_Yw;M>8hu6HcgU9mBa3oImolc!VWnEDP-@lYRx;R(1sjEQwj8S&++7_NP85PQ zS8D!T(H7^3wm5req3*^yGQUP&N_ET0csE<{+2swI$#A($Emx`fOy*Xztv5ysjj27om<&=Iw(%x3+V!`=+xUqTih|UP+F4as@_7C zWCmSFp^0TLW>tWqGo7628*07sH`@4xxer6Wu28c&p1Iogg*5nqAflw+Tuf^ zEgl$Jh`3t1Pqf7ch897+TF&QzY14`ru`zDILU&5&?(2fP+qi8-j7Zsa^MfFCcJAdF;%kCC zJN90yjkQ2%E3X*+v(zAfyn#dis$g%1JJ`f1rd0-h=mpKyM+H}djG3#va4oCczz02| zx%w5sJ#Lt*A{4N=ffss0ljqBVdpd0Lh)~Xg$>Txcg30qG!JQp@@@y{i5mRZD8n}@+ zaOhtY?9FKMh^QZ`3|!C)nmk_+ToW>8@`$J=$_<>*Bbq#)7u@59$s?kUh#NSdCp39J zC%C7>CXa|(BA7gO6fT%Nj|lGU*pp{-v5%N~qtw8Lyn#ditYB|OlSf1q5jU`o2KY0A z3l$C5P#9~0c!v8V3l!YMJeTdBdzkT-DX9~JD)Xw4H*k;M&O zIvU`Q2;TC2*Z^1hy1GhJjCghWc01A0fwZ;BW$CMAp|snWV;ZuAsgP;=ts1$H8(ci< z>%+&_mxwAbZt#*(UmrTYzC>se!o;} z!&*BmFcKl6F1X7hvnd1X?XbW|gaEtX{tjI~6kx3#78r>TZ5Q0nrmKe>thK}Z6ruJm zxU)_9se`Uieu@x@7u?mcr|ahDGznN=hxsW&d|t3G!~84{w9p4CfD`fXMDWf0xcRAp z{!js&h({=b`#Us0a|1Q>^Q@4t%nnp2OfY3c=#>jTq+?H+%}-|%110hWhF8P`6v4i% zPZfi$K6V<^k@a0*h zYe4t^6J$ZMaPEu^;IBV_9f4+Oa~qB9<3aJ|#x&8vpbixYeXK8IWI8T? zqvRB*Xg@miF+#dxG7k#rUJ)Av@$zOMRz#R?eI+4mdDH0rPgb~!xaJyHSpFBVytV!p zOT#f%zD*0pMrH0Jm&S3+)}cl1utB@6xzTMpaO!ZdDU1SE=0O z8dQjF*-Xb>$jBsS4lB@J)Q7Gwi&P=c@2>H+q#KzW5qHTLs&HJ+&~NqYw(4rF zUs0`PTWef7;h}RTMMDwkmu!isKU9b%{EV+Eu>Kz%IwUr_|35>LkSt%b^rxk(7yqz$ z<-+e4dh<`scjkUO*P8v!Y(x5MX>I1onF?_Km#2O?Rh)cclACyZB1is$%n&~#Qj(v5 z`O$y&9WYF0GCncX&MVvcpn2Z`J$69Ehi>olynP24(GG0uL%e+l_K0>s#E!46pCax% za0zxm#8dt4eI&i_z{^BCu+1TOsb~kbIRqDD2SjYZ-0m!RiD(D5IRqDpc3_)Bpc~GC zN}JVWUA20eb~QWYaw=&k`(g}BDMD&PU7KKwOA*IJK!Y6+@qO2Jhd?dbfo;wLm1qaHIRr}VfQaqO+Z_UhXa}}A1e9n8wmAfH z?0|?5-R=;qiFRO{L-1nkfQavHw|ga&VFyHf5warUyM(RpyZ0TqP_zRg7@CDRuqxVt zZGEn~@4yAv0TCa%-61$%v;*55g7ZW>AmX`^kU4NJc0j}vrR^nc-FM&|(GF~L2+kJm zz_x|}-T$8{$)o-Mrz{;<{I|ux!Y>ytoB#Lu{qxV8yJzmS*?#~Xz^_R2GY4mi)4!j- zb^1k9cTJr#dGF+|i4RN|$b;|<;NOY3JACHbfzH}(ucz-zrA8L zr8*U7sy2S3|1q~uC^o8I;wtQ}UuBK(wWhD@>;#!;o3*vFaYwIWw{_}uE!RrA)GQs~ zfj=>q9$1k(ix~;IDA=9hHkcKot(k}fl(J$e$$1+=Z_%NxnELssj0N#*E)q1EyxmxV zb=ylhIBd)MVb2NP=zq+^rZ$S9;=>LZ($=nw<2%U!?GL-?ls~Eu==pjqTr3rV$c9lOLRpK1lnUadtRfU^=L2O! zlVyCL8Q~V$t%z09`>bpM;zi+|{P9Y*rY9yB&{*(-tx+kg54RPw;~F4 zs)Lt8uPAScKMC&j|FQSp0a8`x|Np&r%FfJA0Yybc1ziz^mFY!MnLax^z0X*d>Am+6 zMKjnjVhwf@8yX8LDjEy6s1X%wh!smBc1=vwi2A$t*)_tt4EK_a@$>tJKb{w`XW#d^ z=bY!ai5Eb~0TVekkZc?~y*#Ha&>S$|u~e zlxQ-d2W=*OwB9{vd~ywy{GM1>LbyZppm#~TGuDGBr0$fuV*D`BgZ@l9)nI?o%K&* z-9rg?h#vGNX?MnY5QR*hlXve9j#hFU-L&J6V`Ecswy9(aIXzT9mg1hx``)uRcFr>@ zUr{N%U&Jq0bHDE@uaowycUKudQGm*dC)MsDxI1va^%`k+CcfXI@WrjvJs3YKj6JWC zKI#Nx4}}jr9Kpee5C4RZEyv>6VVD5_qJKB~OEgM9lD;c#Gyi%1D*g$47ylr9X27$& zYk9}>9J~YZ83NC6ujaP7HtqrVOo69}xdW(@b|39LT9USaHWm8|>oQiEv5>JFK3CuY z`UUhf_PhSy^f&u0{j=~{1y6ETa_TGtYwx~K`yR#D{4e(D`u4S90xi)!;29&5q_cRqjFDgT{*PsI~YQ zA0t~7<)dVaB7B5oQQmjF@5mPYmiH~$qTleo87u1WGz}iNF02VOy`E&cmKj%B>|5El zk}bNLy_#&%TiCaZ72RQL?Ze%NY|$Cq8Dxv@&E1=9(Y?5PkuAC>cTciKr*o%|79Ack z1&6JjtV>rhH`DEAvoo#lCs@ zm$H_Q72V;D-dgrrvPJJ=-$l0Qo$Ncw7QKUg2ic-)*lWlZy`6nK*`l|xZyPJRqvrzN z*SxOHUzyZkrr}>fo{h3-E|NHa58u(ug{QEUFm;Qs$C0OO|`sVt(z|`dp&bKtw;&q*}1A#=@N}}EYS2=vX1%gR8E`_iAx17 zzLRsi=6oNoQOh;V4@XpIk7?B%pQ z95F|&$su#5sU3IwOZ>u)qjmhU+r)2e9j!mg=_cHW$HGw}ffQEu_+d2*{6|}@a`AJ> zng3T3JMP-p*f(La`;R}b;AgjWiu^V$vF(q>O%}m_5noPZo3V-L)BnN2NhX#{@FPq( zIC*`EnAlL!hdthEH6ofTDb+d}oi|#`4w*g1S}JMCSDNNfsGNxI^x)JfH>ECPOxKip zLW;QC6fMUxExj*;Z*(ixiH&wsB5MnVnv#?wk=8cw%%{juR_Se2yrpza<~N&_#YTI6 z!4Pmqt@gYKpU)VT8Z}Kasqf0MQ=cI9ol4}5F6?pK_X~Sa_|?f8-V_rq>_OqvDhyW? zvL~SYzdgKBNcEg3^?jS5!h7(~WO&67No(&muE zVJ##Rs<(%<0rJabDHR5P3A#_zL4Oe|!RSV1WcX?F z|09s$Kw%Cjm3_{3JeTmq)54S;pfJ&rj020n0SZ%7sm!b-aY0ykv`Cb?SZ$c3_g5zyJmbQW=P@>47Ic}VHhIg!I7gL zP*5EZsZ)g10#dgn5CAC0apLRw~+JILh&d9O}oo+-T93HVr(u0Z-9jR7WFzNvX)q!$45ouce zdcQC$)EM&4uuU!-88NzBMPtSt&zCcDNipW|l&XWI>frEE4=AV(81R`8+E}3G$LCT= z+vS*2YAl;GX5oma9*^Oh68Lplv0kLeSad})t#H_=2NYBX9A0}CU-UQc9Z9r{gPM#v zRg*}PZg)al)#}xij8>a&4n`6|k6lW}1J$Sp6jTS|dTSt2iI}uip-&?859Uo4n_|ct zucX{5wJFSde94fq({!2h zoVPI+kt9;d)JWK1&DX3=MJF0{x*9r5Ln-nXYPM8C6tSukQ9n5k zWTPHXP#sW6+jV_f>mID8f@!ZxX$$-6fniC(ByHOQVU5i?;`NDjHkH%sCDRJhQ4c7n zPVN?}OS+O#52(BVrSPVXOdW_vJ)odEK;cCd84pCG9#Bx7+}B@Ay24Qp{%fzFmUQQj zdJt(2YI0#~A>%FtjgItSBr8#tRn2lF6K^UEQK3(n>bO<)bj0qR+?`pMbPpZ%fXW*% z3a@H*{QhT2cW~4L3aSGX-c*qBVBV+)6jUeo{l}8-+))qyYwsVHbPpNzfXWMqNu7km z!Jpl^=w%4@4F4(KCER4cl!LOK?z@zkV#w$i{)6y^h=3K@O%m@$_R`+e;pV=WhV7MoU` zz8MtfvO~rgzFwhPHKfxIjd9xt!eh@xU&J3#zgCrmnxhI&LStywebmffSrf7hI_ zdXg<$N8Pk|^kSQ?RE>yjsfaPvh)SGtyDr(Zs^J)7IEKk3iyFO2hg*{hify^5#7Z2i zZS}*kaBW#8q4=2BKutZnm@Do&v)@6Jo>5fS*+4i)gSVp%F?^0h;5O~qUc>FXkW zrJ7Oab3Wmyx&3EXWg=^5lB!IvaX9rVoh>WN z4vA!bzt7qVd+}6qyn45-GVO@lV~%-JI;Ao!%~p~vLt#)Z^vCkyVmTA96xxMcNoeyM zq)KhGh^+eARhf9ws>~GA+4P32-z{w03>rhYQ^Kc#6(^`NUCUu{sTyfRvgpfMVzqk1 z+H~R@5E+zhaYkd14R_M2q(v-p*vxn@yX|LKWtmCpWriVXNNlcJvNf$UW9ZmK=0TrF zI8l`)lzFAJ;B{pq20Q*>7PKin!zM}7Uk$s5M>@l9Nvl&dsq@l?3GZdM{tT-Oe~U0V zW0|Ywk!U($f6U{`mafEI7KhO}v+_{ux%;qz(VdTC`ow_&O!4SDey?vWA+=pq`-0v{IYRm5hnQO7UoIz@UZ2TiY?@Sge{&?GOPiuPzdJkPa7FPdyX9wBWg`1z^)i!7;+3c+9&aj`C}>g+ zpDbuGNhhc>bt4fd4$GpJVl%668p2V#$KtUM7yJc-MbrqlLgKj2rM1ePjfk^|-25}F zvdQXYTAL=EuNi#qVAESL8?6DS)MJ?FCPQr!`5Wr6%@G@J#FJHp%G(lV2ZiB=Ddw!` zhE0iJYf$O81n@)&d@Q@^=T~L6NxR7y%7pM+jG#YMc1nU7bGqcPnYjZ%YG*^NKLDx0h;rqLF%3eCZgTi9~v zMOK$VBMS?w6I7Y5<|^x21x>Q$Gl;|1rpFYrRLwD4$Q_h=D!Il`xZu#&trbhXsxBhG z{TWu-WQ}FIoHmHx##&9Gdfbr1H}Dm?W9|vZGN&r-il$>}gUk_3o6A+3(AIF}4BA9K zSq?{ibxFu?^J?YlmLZ}=Hj*<_7n27}h&jTJmTb2aEsRfjr0Q&2KAK`&DWMzVv= zL2<2>cC}4aWl>V~+3Q}ls1P+2rG{Z+s#v#6>RG=#sakv364WtBf*m8R=)O?ViWEraZ5vy9Dvv0jO^ z_4#%-pEpHK=5dLc0}*x5n2qO##RZGXZyZUB#X}i+JS`tiD#GqgDXa=8D?Xb?*s=#b z-4e)tI?Lw|TFoteBdxLNVrFf9*fUGb-`>hj4F;Ao~dkZ(aCvk=kj*AG39b)u<6I_J%M#3LqC$(QtWb0GU632${J z`*7ah2pT&YQKdBElX`15PrGOuNlJ&CIjhU1HVzf#!d5FJ^mMZz`vD1W9Z_2=9>e!A z?KF~YlOh}Hh+=w+DH4FK6Gb~6cYZIz&@Y_ewR6~EA2$|*{N_IgJft^~c}ia2e` zn3W!T$DybxeQHlP39|3*csGxMx+$g%i#iJ7a4VGa%Jd^zXIh#z88kJ^pgw2}Cu+J# ztzk&#hrOO|0%ZR}Le`}(G$b*bvu#imhZ=R48-EW|^qExQlCdQ<+8h&1EhJq={y|DKwirbu<3eEozsPN@9hsAyb30Y@yWW=fR znO)U1=-g~$Wqus#nU|o zWM3sAOJS!JPxok$eT9T9g*{O`-4MvWOhUG#EQL)zMX6oSibZC8ELWvXz5t%+DXk~m z^_;xpx4J6vR@`7wdb&Z7{SygU3R{_Yx&e^gxZ~+-R3=b*x_*%TBMDgwvm!lRAIQE$ zLYBf*M^D!avM-X5r7(Zd)AfMt3nXMIOiJ{0-5~os30Vp=4Lw~K$Ua9xmcsNwPuB^u z&ytX(kWlC8IzV>AjwdxxnI_`t+Clai60#I#X?VIekbRnjEQP5To~{*SpCTbkVLpYY zYXRByBxEV19(lUMAp7Kwzy4F1PvPmBLG}+MWGPH%@N|bj_6ZWQ6y_*+x+ai)oP;ce zi3gsp5o8}DAxj}igLwX@qkAFT(^z}be}n(!e}Bem;52$SMy&g?Y!8%5XWeo0j9b=f zdrP><&Y)`~*>O|wX%^+l#;UfjF7%e|#zah)D8_C2NF?jD)sg|bEbj4khP@WEtEke{H72*Y z-So`Qv>YmR-eT0KNv+^SoI1Wj$*I+Dl!(K#>iANMuDHh-56HFU>QLKIOUH-3X|p67 zRtD1Xs9iLotrgl@Q{-t^( zmmXb(?EA2j{)fX(q#y!&!li`@2A95O(FL38x>^x)MyvUFTO2pal|!o9+>TgW@#T%t zmN}xR?tIvp+QodxB}y9HK2<_j98Rh=d7sUk?<8$9QLfR_C@U^^pc!mNlX<_$SsQlv z+U7zhl(Mo1N{B)~T8VMgq{TYeH6XAM}Sb1`EnNWNY+pYc&mSx*&@wRL_ znuXOEae-~uZu?e*(~){5hYJ+yx!l$PMLhKEAfTu$BE7SV zH!Fm&lIw(l7f=Vq5=$^{*SYGQc-?PsgrfMrC9<|Y(Fz)3F{e8ltTmgtxkEc0c*d$V zsu1E5)n;rc+stJWPGdTr)@z(e@36XHsSK-v`b^Uhjpy3&U`-^w@ z$5{vToyhzXQ_468-^ou;TZw&*`OsVOulgD4uL(}xx7!vV?D#=W1Yxt-492l6D$84K zvp)Hb-!!86v+vrxnXvCEw%3!!?`M4StZqwypz(vjLzM7vmjMb{%wsO*M1x` z8s{X3O3pY{Cpm3HR2Az=laXIj@@~oIL7> z8w>`OZF7-`++t!> z((C`#P4kg>Wyd~Zgm(|@4Yo$w$9=oW@i*!eJ_3)q7=?|5cI+bI+r1kId*4B%J)G4| z5Vg3~jXHCHLNfoqYH{>NeTQ%2AlUDoK{$Cg>CKH!p4W{NPHq$I6!QJYoSY!ocjzw! z`~73@3o6N3G=JqSNCA3~@N}DCr|?GkU)>afeTP0muwM_)yf++4)<-=5 zA3c#lM+9FB{x0}L@Sfmp!E1s)3Z4}_DR@-yfZ%Sy?Sh*G*9m?txKwbV;2gmjg0A2M zK~qo`WCd}-h#)9%3#x!22`r4c?!4&-0$*J;r;G_dDJlyjytJ z^RD7u&ifVbT;5r{Q+P{wZC;g^=OuYz-qAcS&(0g-X?Tb8q`X1iY~Fsn>Aa~t4v)s& zg0Fe_A@?2btK1j3>$#6`@8z!H-pIY0dl~lv?lSID?qY6@o8!i~$8bGd3s=Woz?E|6 zaR<0FxYM{?F1>$C|L1t7#5?`3_P@}-zW9_Rj`WN&|`{(r!^v~#@*3a#ybGC3k$5&f?hx01u1|@v-wuP-@FJMdA^VkFI8SH6nE}PEU!up)` zA->MztE?AT>sgPm?q#iE-N?F{bs6gd)-u*o)?!wTm1D(N$FMvs3rojZz>>1&u?AQ( zSkqWs7QJsv-{*ZF_Px{hYTpZe>-!$*ySHym-;I4&_g&U^LEo~zrG1P0YJIuBSl=;y zo<2*Tu5Uq~v~OPDK;MkMX?@&2I&%y2bLNN4ckmS^Utq3hKEk}0xrTWo^J?a0%nO*y zm`m|2lNvL}j4_X4dYBfbj=6v-WzJ&`FlR8QF}X}SV+-SR#)ph|7_Ty3V6106!nl{Q zhH)d~YQ|-Z3mD57OBst9HAapRV;sZqFf0rmV*x|Tn8z4k%wSAoa2a&^7W(J(59#mF zU&Yt9Tu*<5elL9u{YLuL^vmcM(3jDd(ihWf^c+1#KZfq1Tj)Ca0=kqwk3K-3L7ztF z(&@ABkgM1WwZ-u%V)U zpRR#E8iwI-HBSVtMo$3Vf-VN$j2;iX3GD!HMBBjMqAlPJXcKrn+5oOX>%i;K8t_`Q z3cLob0Ix>Nz^l*_@Hc1?_-nKPT#4p^SE4!K6=)WCIhp}phNgj+qAB1dXcBlangFgq z=2c)S**> zT679fg9?CZln-2p@_rG4dTygnSDWBHsY#BU^xnB3}ask$(c`AzuOKB3}XzLB0SUjQj&Q2l+ej zAmnr4Y~*ji1ChT12awNz2OysU_eVYf&O$y0&O|-}?uUE`+!y%(xDWC^a0c=oaBpNY za4+Ot;GW1|fYXsb1NT7Q0q%~x4crZR3pfpV6Sym~3AhXL25>6!I&cc|8c=||3gjcN z0C~vEKrZqpU_Y`E$U*)HWFs#DS;&jPKI8=;6L}uUK%N8Ak!OK4WCIXGo&loB(?A4y ziXieGvL5&?@+9yZV?k;j4mL>>cvh5R1)CGsfn3*-^tKahuke@7kyevUi{ z{2Q_k_*dis;Oof!z}JxbfUhF=0$)LX2Yea12lyxCZs0~_E%0IFF5pARoxlf?JAmtu zHNXdu+ky8Xw*l`(ZUz1hSq;1gxdnJPax-u(aue__h3KI6X0AY{y6ZRM%VUP9_Hsm2} z&`sEYi?DttVSNt5dhLYu*a+*k64qrQtaF&K4l`lxLxi=N2x~PG)?y&+u%57H9bt#G zgf(dhYg7}~u#m9&qX?@zlCat%2&-8@SoPtAU3eH_k5Uo#NF`y9P!M*3oUn(>2z!{6 zuqp{*m14pwM1++K2`imXSjnM;6%P_tG>@>txr9CR5W)@~OxSsI2s`&6!X7f4uyYP1 z>_G#BoqYgd58R)y1G5Ob|4hQp+K;d^_a*Fp`w({D8HC+uZ^G`q7h(6>ldyYEC+ze+ z2)oDbgx!5N!tOSWu+w%W?5?{Ic9*GyojQfEQv`$+@CnQ35thd#EVrMq{T#w_*o0-X z2+QgtY#)=bOa@^Ybi&eUgr#A08p_5v+zn`)wh$3~Dfm>dS+Gg4QLsVqxL}=Ntzfla zm0+b{gXP?Awu}ka}dl5UpcCbzCg={5T z$ezQV$)3)hg3k>=SYNU}Wo>3{Vr^t?z-J+>W36SaX02kaWUXK=XPwSEiPd72SSi*b zR)FPTnOF;1N|ul{hc%Nmoi&BUVj+EB_I=v78Q-;FW8a3p$NSdxt?gUgx2kVt--^EF zeW&-G)Ys}O^`-h2^#%GIeWt#Jeab#z-<-agebf7<^s)L7=9kP*nVXrLm>ZcJn2$5p zG1oFzGgmQJGFLE{Gf!up#B4E3%oKAGGr)8(P0WQ%B~!?p!<@;S&YZ$zF%ia>j87Sx z8JqC^5;ia%XRKqaWvphbVytAWU@T{x&NzwDVw4yu#v(?5;b52;3mHm=kTHiblQEq! zg~4JV^e^e3(l^sL(KpgJ&>yF-qpzi}rmv!}q_3bar=LzgiQb}@=qdUldVubro9GMa zO1h9fhdz@&oj!%mq9e2~X`j+I(>BpI;yX1wPFqJ?OIuA_MO#T*L0e8coputfMJv%# zv_-T4%|SEK7IGiwuH&xduI8@duH>%ZF6W-kJ&D`mmbfYIB5r`|;F`D#xk|2(JBK@y zJDod)%i<#aU*fw&Z0_IGzp;Nq|Kt7Z`q%ca?qAivvVTSY^8VBNPwH>=m-L>9Q~Fu`2o{vUt2wJUD>*Ch)h15o zoWyByN}LpD5huWLa7>(q93@A{nZudMna-KQVQ~=lm+VjR{V6uFH?lXdA7`&)uVt@h zuVSy{FX9LI4!()Mkgwzm`E&R)`P2DR_$)qx?{4uaZ!>QbZzFF5?{VHb-df&j-YVWo z-U{Aw-s!xPcr9Lum*Oqr1$YjgiMNoaV>S`zd!bcN2FbcfYgs zunO>gtPH#lD*^AtiokoY0`P7u4_u4ofOlb8;GI|ocn6jSuEA2k+p#3@HY@?W6^jE` zV=>?@SQL0O76IObg@HF>i-5nyMu0b9#{sX$js>p5jsaeW9Syt|3jwdeg21b>0Prfz z5Bv@01O6KG0#{-l;FXvgcm?JHUXD3|mthXzrI;Of31$Ocj9GyzFbnWk*f8)S%nZB` z8viGYhSA@F!?KCpuw z3T$J8z!o+S*u>@n8`vShI(9IyhRp$1v4emWY&Nir9SAI81HdA70I-1V56ok;fH`a? zFpKR6%wYQh)7U=16gC5x#P$Xzu)TnBY)@ben+}X(djKQY?!Yj%8*mXe4LE}B3Oo+m z1$Znr6?hCb1$Zwv9C#G^H{g-zUx7!Up8*%3p8^j@KLH+wehgHh9|0BUhd?R%0Z@Xz4-}*C0Y&I$ zpb&i*I3N8B@KE&6z(MpK;5_tg;9T@A;34Rnz}e^~;DP8Hzyb7i-~s4s!2QuzfwRz8 zfHTpTf%~C<0`7xu1kOPJ2;3We3Ah*fB5+Ui1>kh_dEg%CbHLrvXMww+8-UZ$XMnq+ zPXl*Bp8`%r*8``ZPXYz#AAo%H2_O%B9LPZ*1G3TI16k;!z&`X5AQOEU$Uq+g($NQj zG;|#hK_4K9euv%<{1&|r_zik5a0~i7;MeFqz<;851HVGo0zW|S0=|#l349N|1GpJo z1AG^~oghtuL3_}|7_pmT7_pmdFlZ0jYz*3ib|41rK^wrJJ!l7D&>pn?F=!9kEDYL%HWP#PpzVi2 zd(ig9pgm~&V9*}485pz&ZEpl282JJzkVbC6U)kAyGPz>6GhG5Vh*mu}%M7v<$Vz&an!BzveV7CCj z#%>1w6T1oc6?P-=OYFD6FR&Yc|G=&X{vBHd{2aRu_&4lY;9s$8fS+Mk13$&C0)B%1 z2KX`dYv4!NO5lgsmB0_MD}e7~mjmC!E(31HE(N}eT>|_Ib}{hJ*b3k~*sp+ZV;2G6 z!Y%~9iCqBPgq;t313M4+I(9DbHEcQXRqPz#E7&iAFJor||AZ|AZp6+4{t-J9_!4#o z@I~x&;0xGkz~`}3fzM&50H4K{0ykh?;4|1SfKOv51E0c90GHxIs6f5I5)_QHUG#B^2TYeG!GYL0>>2ZqVmZh#T}d6ygSb7KOM$H=qzV z=rbt94f-?+af3dELfoM1QHUG#NfhD+{R0YdgFb;m+~B+7LENB^p%6Fd?@@>w^idSz z27LsDxIrIAA#TuzP>37!K@{Q!U57&4pbwxBH|YH+#0`2M3UPzpi$dI>ze6Ez@Rjx; zZqU0?h#Pb*3UPzpg+kn*ccKtC=p87;4Y~$}xIu46Pa4htUyg92>?Y$|botM#*ZjZd z4W4$H`abQF!a5;4o@X^K5sJuYtJd&2TKN%to%47lT}a|-I#Q)-qC_YPv%iQWAu1Ev zCSFB=!cry^B~EQUw`Y9%RK99WxMP+fbt9W|yVZqgdZ*S87@w9w941;U)Zzsq2l4pP z)Jgc;_GaSzNa25Jmw0_a-s$&A-v8OOOO-?@+3wm3c4;tCbQ{#(%y80@u`~^#M%7f3 zy39odPm-_i_ACZDxs3u7Yb&>)^I1Ts25FfEq)-2 zc32@UN|VWCNvD(q3l>*h+cLQ4hpOpFz$$5L>cK)jrSqBe371)z7vn4PYe}VD!l{p@ zT_(RzyQC0vlQ!BY|J~7M!n8{&!xbSgE@^w>w9B7p-1v9WE@i|Z^q)$*{LyfQA8RMm zE>&WgaJ%`=zMLr+s7T`hrM2eF*u=hkwLKK5#wrD6JUq84j1=vaq_8C(wh4DS@Y0%? z*n@8^I#=znd6dSwGAEZhjdR8Jw#uqYge|q8$`%e~O!25vRuwn(Ih|T0R$F`?wW&}q z+Kkr3T$NrJY}E}deY;qZN`_N;9jQf4;MDQJJ0;ak5P^5b2s%KayG~l*DSjMy6NJ(P z!7)0m`@eKig>^_8@k%vaEa2CQ{UOg7JStX*f0Mq{o`84W?=LbU{O>zRMJ7&nXZyNH zB_>uRm@viI;tgd|iIAg}5;gTEydw=Ow2hpoR7npPB6hc5-G;ogs!Vt%+DPA|CxzmWu{7)Nms&y?x1H-1jGFI(MsH~{Ww~8HaLYU1+fyr zgu&^Tr5pjFF7H+u1Ln$bp(Ij96kf4Ko={4Bk+!E%cIk>alfE4c?(~IdzFcMYxs8@w z#APry{HiW{+1}?9Cphsb9F6syHiP+%k#A!-EhTM zapk;WY1=F8$Q*8&IcFi4|F0rWeJuZ9j=B)R?*t8ji2ot~I(~$|5B`Ppe4dTR#^=H> z;U3oicl^t4wtqI~O?*bWpEHI1BtEBH!uptXJw7LVzrL6IF2uj0_cMRbJej$G@de{H zMxHT;{xnXO1-3~jDnnuLND$qGk!saiR(rv*Nb|} zj314*BO}!dd&-E(os?KVzo(3doQz0&DC214@a^;)6{+@CskDQ=L`XH0N}-+COJw|m zz=XA{l4+CB|@tAE9KhR zy+la0j#8#Qu$Ks_08&b|1HD8@HL+5nJ)oBesTfp>wfpxHA=OWmBJHdmA_`LdM4{5o z>?J~~pC}aC{d$RzYGQ>CeAnB)FT3^fh1Ad>muRQ; z64|!j2!#{&8@X7!YcCN}JyTD9RmeWgwRA-Zj zwCo-tVp2^kR%lthL`XHUSg!5sB|@r+#WF3kmk6mQ7E85^ULvGgM=aLTdx?iy6KmC)z_qM5>8Javp0BWIpmHhR>5fqfdk9`&R^W`S0*A=ll2q ze2(rMuT{xF^qEu(!*yAGd0z9)7T_8g`|zrkl26wx`z zoA~VLbMd+3qW<^$uflWRkKp`+a|`Dpj-A6|KZvYici3vyKUufq`Q!)g@X_D+LkZkX z)erxo@HhRjy`IkGr0QBE7wV7cB_ks>dW&RA{n0&T$TgZwt`GH;Ar(|2nN%O_DMKnE zL^6p!&{Kv~bc$qRy|1SXxt5X%_5PkRq#{%#6X`v@WTe{;!`r!Q6-gC(cTbsZ`^|P` zlzLZB8FKwbD%U%E%8=_fQmNk2Q-)l>kxKOTo-*WGN-EOZddiUNH&UVA+Dm5KP2Toe zN}|$Rddh6u;M=*m5=oT$;hr+vcE9b)DD>u@GUWP=M6Ms|DMPN`NThmGPZ@Fpg+#13 z_LL#lZzLkUp_h!9)Lp$utkSD`$%x3+jYz6j_LLzPQzD68(Nl(86^q0Og8gr)5(;-2 z|10{_)PO$Ot9#4I4U2MxKG9QV+hK8gW4&CikN1=z^?)Rj%l3&e<|BI}3%5P2P>9cL zh?n9OeYEFM$PF;~o1I8c8FB-RT%r&6lp#03$i;gs!p~WNEF6DWiGHM~KDohNF52U` zQT_4$i1qqj`eJg|XR#7N^xB?6;~R6r7a$_BT(9XVL#_{rWqNf_8FHOOEY&aUDKoyc zwsTSvJe9x4H&8LTTQadwe`GHid<%8<9$XL{AxVO)gUE7xa`NH};4W`onw5 zkn1-hx&E-8GUWP=NH#kE|6PQ81{-D6(06~DVf=qb`H5@bGV6vB#}y{ zM#2VbzGiJII?<@p)zDcQN|C=%v!x26h*h14`t#DAns2z(94wfiH^Ce%)r7k{QOI{7 zCKyqfI0@w`%y6DKEkPk|*Y#^8qYZ~!SKawJSLR^75vgT{^5S4D9hDbr z#bQL3X!?YSbbC0^_U5fZwaI3emb6)A&?E~5%r>_%r%4r*K~2gY_Te+J40d@dtufdl z1$W0FHr86?Hak)gr%p_WWOuXQ2P1_p_3s9wglOrBf^ovlkAC*SScq%wEn`lpNC@+4 zaj8BSvH5c1p~09->&oiQ=~6x~iTc|KW%~b4Fba1ZjFUS7Ql?ZX@m?|E1V~B6l8R*N zId7m;3ul!^qdU+xw~P&`w$yMLytV1K1N*!pLV+pw{uh1HenVKP>Xh-6Kj6G)7scmj&aBjM-DY}9kTSi$oXvOY| zU+9+=$ZapGB2JwMMtV0x%)}?Q`K8dMb}ASr3O><9vyp%H!AD^efQhbzKVHgX1!JF^2|E~ioyhD55CYB=w%NF0%NX@12pH)3^%=FW{YqLG-xE%7+2u3=Ss zSQTqjr510w~!WPBQzA!l~nNh;Q?oCgPCT z=9fb6nzT3+{-2CPDg(s?**rfi*8g9MzkfMEWC8r-+x)5|3KfxKI^pH7P}dZiN`u3o zH2BI+lU)|67?jqM+nNa29p;upS_%(&0z!kgx%2m;!^M^_CW|=j@t`N}3hAS1pTt_% z)%@mQNi;khYe&3Qt-&aZ>hqQPHsjn}E}8Ts4H7&lP0=)aqEeYT(l9pM@^;4Q4`!r8 z!n$8eF6@X?ADjO_4_SlYYy8{zf9GGu*Ye)SSI<{+-{79g9mLnOKfZr|&J*}bYmI0~UU z?aQ;?KIoCB*MIxbXAa-eT|l^QiYddQjzT!x3gx^q{fO3?mZnVxP0cc>4;sUXnl4gn z7}EJ+@2K$F=Hl!zUvIcEc%e)w(y1Wq(ia+% zn9bQXD2hXky36hMn2SD>DqJ$Qq(+;gVzn8wF|)zal8y@NhCX@idB@|&ZQlRubB6|= z__gz!%(=I2irQ~{*l_N1`uY3%mQDfTNM|VKsJYDLfHcvpH-%=Cr*6g{aiexgsU%kD z8Zx!n;Ex-JltZJ!Me#mIzA$wCiTgeDp>&}3@qpxn(ns%IS9th=a!; z4IYJEC2Bf@k#wk|%hf`1b4)#AOGVpi=}1|Wlu48xb2}UNg~sZ5wcZ-q*X_x0DaU&g965Q{^+es`;jJFy(MmoX)btYtm|^ z4R1LS$qO@?gvTh66s@BkYXskEUmP+#vHEYD?MJSevg+@BtV8F!u6yND)7vMA?s~oJ zTgn6BmP=|16v{Pc%hL3ygAMOcJux!eE}Dm$m5L%=k62}1rMqB?Ns^<&kM7%dT<0e3 zE}vSM^Brehx#h-H1Iy;#vi9cO)1J=`eyrK@E#-o+Zm1RxxFv(xTqNzNdK}qdMN!{L z1qbn@o?1HK(d!+pP{wOB=f;G&XN8U({PsNGTSq?f%<`Kz@2U)ZWtH4{;PX?opCx3x z*#Y0ueh^OA%^H*2Zx83Ic=~ubnpY&<4TH)b^MqRXv#+%6EUM$Nl+m1xj(U9NTk3rd z{ADJ);I%U^yL9a?_m24L@^a%Ks!hK;X$dt(6LAxgF3Y=nlvg4 zHGNvG)43bITFIetCrgoFOwvfmyt%Q+`}&mY*55z>OQEWQ-h0Kf`y;~V?mIfYVA{%S z?%e0~FTQeleM|d5STiKntEJ**r=}_7qzaj-9TK^7hJ@G`YNXtGZP42kN%G-P(>pc@ z(a&B=JA3uh82 zHIAfu*j0~-^NMQPWe>PC#gRc(C1|mF>IKV4ek}5CTKhQr_}T@F>?dL8-u>FOA3uJs zs;0i`yZdhX#d_O&PktNqEv12QNm&Y;e2P-Lo)wGCokmiXHu(ZAqg`50xa&E2$8U92 z;;p#Bq8#lN*Zm>1&vEbl;XU~amwnRRc$#;y;qbGrG{4t*DmwM1509JnfNv=V!rq`- z=dvd_H7Lpd7(_I-ctEGdEbk$X^%JVK` zrm9JHYK2fn9Y~Euo`zZZ&Ecghu!q%$d^f;9^2oVUZ+QAzUFt(7_oK^KtUGQG-%6hI5Z#BhNf_?cK}oYF?8)gfsOPU-twME+^FPL0{QgQV&)-`l?OWr)q>xyAEx)04rWa`LvUtu_8qT&vE+4s zrtg)HeBH$$Jlu%Li-SQ)K^y95^NOOZ)zD>6M9co4>Sk2cxeicmn|Y-e-+ zxJjf7CmhDSQsWHClc7r6r8mnwBAX^THqZ~A{g~>|KaH#rpXa?|KgY|9KfCtRX?woU z%sw%d{pTaBOqQ?P0pX}Ct_o&6#aeyP<&m1gxtc{?PS;f-tJ-QBmc=`kid1GS1;m!I zkS_0WUqrh3&QH#@pZxMg&#k**?tQU!+!;3?xBi|l-^utluJ(1?AgpSI3ay}6*s%u< z^@6x%#80}Ys)r=vf?M1kEEGm8v1&-|8flMpw=Z^2K6>_=^?za9bHc5A&g)$D%EfP_ zpL=EgbyxoNwOf}@{cg6e+X7*yEMKs;inc;IF4HxX1`{5Gtz1JIsB0=pi9?cgnCk(B ztR+c|HJm+;euDI@Q{&$RhkI?@9=r51kl>J-vi{*bUjND&-?HgaW3BnZiYaZq zT4P34R%yh=x*T6e!d-28b)sk_H8|W7hGk;|{a$Y+{-V3Ra?+LbSy%nz7WK<#-E;3< zuG<;37k~femp9h37x=n$5Uwj7_CT@{A5nNUo}?=~C{_ZV%aZBx%8!xR5n)|YrN+-{C59)&YUJ##ys&C?;kX^ zbil8^`(U5HoO#hp({$H;8N28uU$+XvPOZ+X8B&S7dPyyi*QcFPlQ!Vhh%>TmLZ=&% z2!}^(lJuZrL^oCy3unGSKd0`neso-TfAxb89lNk`&c*w2kNRT6El<5Z|94xYzHSAC z%jraGjd5W=J1rNgJT}waL_C39$k9$kdz)= zo_hW6?>;;4_a|R9^xh5U+_bs&`Fpqj#n&x^utA-xXk&qzUt0D`+vS*2YAl;GX5oma z9*+qd8NV(o){7Jwi*9V7pT^FzU;OCkGcS1ikjwve@(r`aweOC;qB;C_KqcSxxVPsW z=IfR~*x|Kjb2?w%JCbM@2Q?XUswR;n-R^|As@1D28Lc+m9E>D_9{bph_YvBwFOVhN zU;Oo|P0`tVH(#fpwA;k?b7v-+#MHd;RRb&DVz*INULO2nkC3Vjlxe=u*d*c3zF zcqQd-OKj4P(bb5FeW_+#H8xhXw0oR*=VP3wc6&pA|BA{}AD*)B&C_ms^>xXfd%yOl zK16 zaYQWl7nI&|IXS3}=jxV9%`Q#0^J7(U^FF6m5C7sU1}gma=38#M^p$I_`^>!~LkE$n+W zgbQaun8L@GQQ=RP-Mq`p^ZxmG;0*g>*)@e@ZhCjWWoI9|(kl7;ZW|>hsP(>X281cR zNFEiweA6!GY|6*yl87@5Ax_p8v7W^PF?ur+Te#Ri##a z-?zTC-i3;Y*KZ5Cn3ax*NF~ecu^CJYF)R}&>LKL z!M4?Dc99>ePS?sWj1t?SXgbd;#le8`kNN|Np<_HWtL?f<*OpD;tmA1n5HQ|#aDScV z%EWkrRO_~6I=sV;E3P-4wR+1kV=gGARVCY8uhB2E_L=W5jfnZ$jmoWYvZaO?5@UGc z1YTCu7CoES=+SIdtqV<~E(%l57*wS8yy8e_zOR#U^c!U`P7&OXUua_mwkaE~6LVTp ztu-pRi1am49Ig~&Vp_eqP#BS<)7Q^@KX>Kg({Ge+jZ}Bo=6n!J#HyP2oKYX{1#Ka* z$&n5>Ba7*DFoLqnxi}u?&U{CA9qFZ)Z@^%vr#}B-$DGoRH5JL_z`h|NE3@G$ui`CI zR^j@TL9>O#^728DOnWu|%r~`R>AL5F&3xf>veo)g1A~gU3=)OqR5mA$nxBg57|X>` z%XFNo8v9wgXu#mZ-WvyAgPS$Z{+51WgHESytqmIAD6H)Pzxp}CXlH&v8=9!`jr^@K z&Jj^N^Y6VG!?}?IV?6!o7mm*nTsx}i{pw(#bZDEzmB?->?mc_|*fPhy=6`;k+5EQ{RGXV5s%C;2a^mGY|EJUGH?Nal2&TP-d zEx`BY>9=nB95J;cQeg;fu5`TCqUCrjO?U3uFTUVS-z_n7ebdg3rvZD`etM2j+?nmU zjh{Xh47v8xa|G$mJmfw3=~KXXYd<|l?C#9t-PKQ@48~de={Z7qXCCKnetHZBTl?ub z!h2^P?3AB=DHv+)r{@Uxoq4D){K}qA0^a!Plh*ctTR%sf@XYq4etLB4rq2<9JL)FX zKw{?#VPf6(jH?ON`{JjkQG2ysee~KMaO>xYDxTS%ji1hMJPM4len-!-x4-w~l-q7G(YPN+2=k|9*=9Il`{@l-PI|Zqxq{vKjB5#t)w1ZSww0pO@mFzh@TY z`^u||~oW#&f(=U#vG)Zs(2nNP7Gq-W$(xR`J00 zM)>o+am^Th|5e;nN@(GBS&mcHXLHeDxf=L9JT0uoEz)YmWsV-W*GetUY|NYCL?R+} z&G`+K+?V8H0?r|AgGVgMg~y0ga<5NK*7wj_S>yDn(2?1~^=lomUlD~d$jmZq6pk2f zIkOpknuxkU4MbNK3lu!8$=5W~2t==roh{38ed}v)+`F;3>5ZWL?Hupa7we7X+j(Q@ z0hw%kjsw02wl@|(&l}PE*BJ}BwZeN$IfjRAEC;i-(41o!d#%$DX?MoQ>_m*^mSqoV zOKPubaIY7EE-&}d8Lz9Y;k7r`i;d7!(E z;h>Zw5U5DR`7$NNeAy;uxwzPNE31Y;kTbh!4=0#@rcD0|xb=-Q&hK=_bG%YttTW#1 zj&e^GIPe3~8P9Pj{=jy|!sj{Tx?1i2n;FYx__i~n)~JR>G~&5NsnO7gxRxuca&D%U z;kYj2C|_i+U++jlz_9nd{B2AXiyo}bs*S!Cvw}uN3sh3uR8uM19kD@Te5fX+{OX#) z&!TpR5+^eWE?(;dZpo$CVon$s^DrQ#dX!uP`G#W;H{jaYoN;~Yz!~?!PWN${!wXkm zbmfy*0?;4b+WGav_iexWP~C<>Z}+bP-Pwh`KiGNJUMurmyKma9gFg1p0G;TMK`;5o zLN9|J3Pk^Z^XH||wx;pB(6K~MNBE*qs1S93YP42pYucz_(!4h8`gp;jEF>P9`#TsP3)!TBT@8Bc)dxXJUZa_CIvt+2;n62H&_g*F z=(XHXjxQqN~Q0^jKI%!ZXCz7RkESGrIN4i1D zl4}Ers4Q|b!()0KZIC+>&4`HkW;E580baA3z2x(3D4^TQi49Z&_k}Rq5Jz2kIveB5 zR=%p%ktxeWQnYX@BfOd9(4eQ`$2p@|n7Z|T!!cWNJCU&tH?1;FIwAcaw_1hMnmz51 zb>3JaNA^lzL_JHB<8amLM+rIjJR5Sko#4a+e?BnXwH&mQxk4S@nF_-C?lF35rhCTlA*n$qSTyBJwu8AVZ(4+CWjKr#%Pkf3xwzSp~DK|aKuV9@?kneD- z9EBsY%*`Wz>MttYZlJ-vcDYL(8HCTx2cm4p!(qT$ek`4`VgI{NY^aM7F~ZGUfzP1@ zPmonC%&w+btCKa+44+{>F*B7oG2smp`&6ZD%NmU2=CfR)0@CxLxZ*pA>&m9GIpKJL| zw#8{&fDby+;?#41-gIIE+G!S0BlM@78gv#m-)R`y0@d(rge~y8se3dk@IxsukYfh7 zJ8jyhaayUi;nAvEBKSf}AIf@RQ6a76P~~94Y1foKd}IoIw`R874pR?wG3cqaQ#M@s z)zk*jRhq$SJ}aRXO$|DgvNNhx85+a!f)X$bjzE!Cu~A@cy?1O45Fg`;y+JLJmmqz( zGFoOEt42^-5@cU$>Lo8wXtLn*JbxsR{VQlJ9RUhkbdJg+?YJ-Fc(Q#G^tb8*EO4mxPjx(!d#jLnm zG8K*Ka4}OVNQ*8lt9etD{UffX_vS-38q9&0E*q|>pSIyUPi!cT^@3!DS$tJ)&CP{M z71f+p!~zZkJ?5xrd7jp-3-cn8JI2_~tWw}@v$ar)4(d#+(~{(B8fq3fQ6?H?q+S&I zq-!zE5uPM%+fr+-p4uQ+qEP2f*>Gu?+90kq=$yZvQNu{ut*u18I(cj`H0 z?l`d_q^DtzE_M8N)Wz*`)J8+JkM@ObS8Jm^dsTGv%Pv-r9Of9t+j3Z-l=*B??)phw zZYncz5+HHQ8BTMGVQLyeC3!n5OGit2E~tD1GdyEBBYLfRe9DHslQ~Na1TmWNl3|g6 z=E53ypoeT5%wEEDl3;^8-!7Gt2qo(1lscxyOsCuT@{=4@X_l92i^hpUH4c2H(b5yo zrQH}bAZzHuA%7&dyJA-ut1M$sp5!nq@{|qRzjg8){0gP5YWXqZ;VZL`doJ(U#o8Y*^pPsw7_fBkZ!O9TFOR(p$_4-)ujRJGF$|@p3>n*p9jh5c1!p^I; zrG3oR+;WLl%+Zo-pt{ziYkq*XjWOHJS7SvVl$0z8)w8l4Fh^`q?HPpD>zZ|D&}mZa z>0I9Z@?C5wgK0`>XhQ~-$;ycC;nFmzJ8}gc)QCw_(h8Wsv~kz4kL^)1D^yXt!3a&v z>W2B4DP@CFWm2p1{lFc>ZcZGH5^Tc4NAf~g_N!xz7C2dvhC%1_HwyaW6C3P;J~SJ8 zrKWb_g%H6WYXy!9t8B3&mei1|aBg>|(YUW48{DK2kwdrDDdi?*Y6>dGdx)iiX26RA zk$0*csh3qtE6o^3M`1OzXk6@d`DRa=`%Q&C^*-$V_K6MCYv{}Ju zbX$VBDh@zohjp_~8#5KL$sdpUV?%b!CL%XoqhNtjJBOff+h#o45XP%zGr$sE8@Yo3 zKEv96d}4#img-48wioD_QhK0<4;8&U-V$fD+?Kc!HP8zsY|bxa2pq*vxDol7!H?HadJIb%#M~W1*#`js@B#-m7XsnObDACHUeoVx9{k*E+tky^ zOlzsTwoSd6LU14Y9p6pw`mo^64yyNj((ec3KpV}po9$TFcHFn4|4Ob{$gN|v_jtZh zpwy(~Bw*+qhp=3h2Np+q!1J*NcU6YazCrYa)nY0wwq5u(4{Jm3n6~RskDI zoz)s8e&9q#SE*{H#2NIu9?_||NTpd_TrboDOip?;HL{l$UGdd978_=*O0EXIoJOn? zP3?KPu*zU(>36)j_4WD2!HwwV`NpNp@j0qdzsU2A(rwN+O1TH-`NlcQZ6Da@8~M*a z;K<*fCmfYr9?jqOxX~^d5oloFs^~4OkJEa&=yGMn;PHTR3KO*MhwXq4cVe-^ zd`w<1y3IkCs)iET@(eVdd8NFL`HbVy!{yRl^zG8vacED?@vhu%G?uGr&YZHLVjJkH z08ezs&PDz}Z8%F}AhhK(9XD=nedCVdo$h#!m+Fgk$Jf3V^ssG1w$J?a9 z<^1;X?Xp5uhLpstP_f@ z?qsm+S-f0g8$LhIlf7k9^Le@ekHO9fBqOY6>*ZoSUu!B@NqzN`#$Z2qb5WHqRdV3O zV{_{pcU<1-j^}u(zF2p>4f$;;_rN5-&7Gav;eq3hXJEh0-=Fli;KfACw_U7Gh<0Z( zQ`%#HmA}@x7TRL9-gd2?1|w3R@N3s*ge}0Wu1nbW*&SE?X@HUaNsmtSdU+C>2bmz;q-?sgt?I&M)>!s+@6EFVGMepKcw%)Kc-+DOoYmfmw zIP;o}0%$@gl@)(BtpDR`|H?v=y|#4+|a7pXcZO)l#U_ad(u*mp!8t z&N;J4_ta@K2Q$JQPq?Y}ara*4d>(28;f}}ORQuR7SbGiz1L2Ox-BkOSGgx~Lq5|QL z$KF)?=rdS*4qgJ`j>p_o`=~Qmdkz``;f_b&RQt#?SbGl60pX5E-BkOCGgx~LegWZ* zN8VKX@H1F@4weAnjz`>7`>-=udyZm%;f{yjRQu2~SbL88e&LRX-BkOKd#`9dPf5Q3 zNI}=%gfBUR+H;ic3xE=It@goZP(6!pDXHa{NvU>rL zfv(kFIfL4Bl-LV^3UsaZ@)^{gqf%Y~M4)T6hxcBJdwdRtOaahmb?2DRs?3+{<&7W zeFnAXD5Vzw_2*jcr8B5KN71|hh(FhAFP=f|ISS+jK>N8?yLAS&=cscR0O{viEp+dN zw1DB`%xstfp!{5`&748)Ik+$dK=`>Sx~c@333{PnG`{o&cyf4sM23LG}MReBj<+rc(#(>vkC@)&Hf! z&4ccHIBRaGFg2+r|=!h1aAnDXtmY@=M! zxW-5Mdf;H>qQaxHP_)Bx6LeG{OYLb=BtcogeU7Rc*NL7YQD&!8^H&{xCL*(f1&UAN zMx*PM2h5yXwI^+p?onPXn3Gi@4Ba-Um6)+fS8Q8Do>cm72d>*Sw1m>w5=JMzGgVb> zZhd{ov46wA(;Lt6PJOZ7c=J5#p3+|)n9YsOF*`nByzwSo%Ds5wI*IB2^2RcXBDb$` zq`M_1uF@JWv{r$-XbGsfu7klATrkrCd$rsWnXp&W>P6%}d*gMBax|o8R?tY9+19kw zQ{;FCQ=Jvh^2xOdN|2u0@AefJ4ric=SSL3v>BI8CuNgCMX&dcO;$y8am{+iJ4t0v+ zA|6)Ha^$$V^^G@rcY5PF-l;Fv8*d(G-Q62u6n#Jz9G_!R`GE1pn{+An;*IMS&;96) z@a?@Z-&)yrLPf5GwOlSgndMALMzAX7RM1KSIsiHO$wF<_{nCB+Msz?rlk%dXhg5GY z(t5tGk*rVh#@O}=a+sG4TH&!)MRufdQVsa(U{P$=x}Awb=vL1&wW@(QVLj58;x)qn zQ#Gz4^|PeM0dEAizVSx)PH#NNJM~3+BYblibWh%hJRrUC91LIo@x1Zw9tV8vHc2|= zn-fo(yt*#ezyG`usT6M8i~3rX=IvFPK^Ik9ExHLjW}HX|UB1HWL_u}xRiQiXS)8h_ zUw>fLeMweTRnTH{(orT%m2MU$VD=kaZ+YBx(o?8a<@$AML4o2uL<*LToW{lNL073! z9k`pP>iyi@_EDW|%yTZ%>0GngexuI?{iJ@j`v3K<18>~F^6U$Ll=<+5{h!)@`Tn=> zzj)u?f8jpAU)?Y6AMZbGe|zr}d+*!(v%TNld+px;+WW!1@7Vj=J!fyc*V(J>!Fx~M zd-&eY?kBHYzw*Q@5559j{>bHbU;g9EzkT^tmw)2&f4lrGm%sY5b@}<1+n1knId}Q! z@|RrR0-Xi^{_sx@-+1`y!=D8C1>bu3qTToJ{`u~kcVD;r)4M;k`<=Tl*>!j2-R^FE zx3v3|-AC;1?tE(JA9ns?=Pf(Gvhy=L|9$7XK;D73qwEMf_)dA}sXLF{+1vi~_6N5A za{KqTe|7t3w_maS|7?HVw!f`z_qH3`mF=f(KWcmb(q}IHc?!P<*b9Cb^een^ zDY&Fv>R%!*A(x(h>Cu-CF8=Gq4_^H1i@$&I*DwC>i$8MlyD#2xad~laad45mh+h1% zi;uZ@xb@ks4{iO;)*o#B#@5eo{g18xy7l#2;g-H7Zc$s$+`6{)*saUZ=b(Rr{ucT} z==IPqKtBq75A+RC1o9vxh=#rbdIt13=t}0_G9NyiAHLv_J$&wA{_shM4>`Pe@UesU z9Q^6Qn+|^Y;C~+cz`?g2e9gh)V06G8UwE3f|8)$hCdO;?kv)2q+B+PeCcSHJw~6Rtk!YUawnTzS`(w_o|KE5CH*$FIEX z$~RwGU720^sw)imk#GIEa`6%bW#-@h%Rl%R&=i`c@caK7G9fdCFZ&K)x{<=~{t%=? zdJ4b!qtFDJr0@LTUYcRzlUB3y)cFD zk3%D9ltTMEp%*|eNa5@cpyxx+PoWk;Uj=*sF=b(`+2AU6;k*oPlfVOK80`lDJTc!QuzBHfv!W>Q}}!T27Nj7F3>&xD=^JuQW=cA=+2 zPfg(~ThLRWr=;-b9|S!adU6VXT7izC;}rhXlb|n!zBGkD@fPSw(34X5W5>`DbdZ+sE- zXz0-?yz%qUqo7Bn@FmZO9tk}%g~=ZD21!txrJ@g&vy1 z=e-Pi2=tH?c81WGKwpx=_Af#Yh8~^tKMq}muBPxgH=rxfl@vbnRnTSV zatgovbI>7ln8K$q&;fLi!Y6{10BApjkNsL`586xN!#QXd+D+j@o(1hdJ1Kn7yP<7p zJB3%C30;CNrSRZ|;4JiF3U_5_3))Iy=G!6g=T7*l&;2u$fifxl%x9qs(1jF!^5bAm z`?nN+jL&>7^SKm$q?P$>=CdjMa3k}tnSV{;2j7tSOy)Bw{D7SKbmr43eBb*rpUQkH zh41>C%qKISOyN6UmH9;G6Dj=Lr)55#`FINd>c=u4%X}<_fARXvM>8Ky;hz^WAIW?q zg)e?c=Ixocr*QSA%-b?=OJVq;%pYa`D22g8Gk=)*!xTC%%ltv+4^p^zYUcMdzn{X{ zM>22Cyfua9e&+Wwzn4Pog_*Zx-jYJ)LgvkxH>dE0Pt5#o=66&0{IAaZPUd$~IJ}m5 zQ|3)66c00R%)Bv$y ziT7uIBl8<6#D6FA>zQ9qVf7QB%k!_L@VQ9lS2Mqw!e_lP^DCKON#Qet%w2=-Q3v}{!Q}~NN06CD8!k_&X$cF3`{`6&N0WDJa6YmCn!R9Ia z(T747WTo(jzXh5>vpeCd{uCT*X8t9GfBc-xKWF|qg@5$P%!e}{PT?OWnSaXsQwo2- zn)y)XLn-{dw`D$<`Ctm)GR*vA<{wk|MkVus%m-5VhNH|sWd0$AzwwOB`!nxP;jcX= z^S;dcQuw+*$h9o`yeEaf=w$vr^YDrSQkUG4syMJ5%^0zn}Tr%-^Q)6)(vAP3CV>`0{sT{yOv5Dg1ALlKHF5U#0MS z9-jHj%wMMPztEY#$oxeLzw?cmKhOMm3cvkfnLo??Sqi`9XEX1}yd#C*^!Uu5X8tsV zH$I&Clgyu_@TFwtk28OK;X>y1ckV3y^S=+oC$N9;OBZ?+p{b?l|ZiJaPX+ z`>)&omVI&mDVYylzPR_%z1Q!3*TFmXUa@6H`p zUU}u^SK=$el_vpi!na)hh0EWsLB}OcJ$cqUH!c)T&{_U3zqQfPb9`MawQ$K7utmi+ z+df-I&6XB+9emhm*ECe6@x;sR{oO4I9#n)Zd2~F8%j20*=oM-@7v!i&T@DIxb3pZ$ z6`9SW-0b3FBcX_{xX6(sKm)Wa)z!)%FG{n;oGOpyd4rb)a>%eFvRYmFJC`;Ra?6h}bOtAxd2 zU`-SI@Kv|0%9hG#J5Obm`aEoy(xgd5#sDElB$3Nzi@X+>hZPw#?Bk(VY$W>q;R+w? zl+^OvsioxSCA#D%1<>V=wijJ28F0z9c;#4QWUsN2=!Pv47Uv6t_Gm9V=25QNAZDw+ zr#H&YO0&`#Rgnf($cH^Bx+RfIy82wrdv&e09EPyJ0R0?stA{T63ZSA}4p)SakY!Gc zcDap2#}(P}B5x9PTd3!IIl%9j%QdHi){;>RrFqyT+MZTl)>hc1+ARr9AT+UDbA2jl z1g6&PnRIbBMnqFn*!-v#EE1*<7wK6pbL;o$jOzUv-)UR(t{N%5UQ(QwMl{*3CCw%Z z$N@ECD3_OoXq>zBn#~wo(i9!C7F0*YpyMh$;;lNOHHj6i1XS*a<}UydSvK2QLC66J+0Z`7xH5wFdq6&tlS0xW4eb8w^w4-l2e zm->EF7A=3?mPrCM!NWtZGpAPAB%a`p-q_TtM~Ft&oj}!^)nPP4+yN*=vg>-KUYT10 zK{2s42c5v<+`$Vr5>1wz81o9;uU3O(p6ztAt$C-WfP-|Qq+NYLU+`3ZB>DkkAyuRypD`7dH|_)FhT!#vv0^05mgZo|y`~G^uCpAXo1;RZ>^Y zDX&D?DATzmF~WT&Tc6Eh(3+AW@{G1@!5oYdW3&(2qw&ZbD^-=b8sd<%k!ZwPw+Sl) zMJL#D3AT%Q+x2q#ybzZZOSg@35wy4@3$ucaz)V@uBw30ki;(D8lxI>P19}lC?(bu%r6#ri!`{pvjBw6b}O#tC_->Oro#A{ z2XCyZ=1EG`#JZU4=S8H=#Ie1-Mi?+4ANaW`A%7EP&iQ> zmNdBB#I|3uvw0s4d{RWbInd6QMt~8rR^;-C6!pDAK!Zl3Qr*>-MJdeod*NpA(%r1e zY%bij+2&2F4nDEDaMz}nn+qSh-GvX{?!rHAE)-4=e(-_YUHFIFU3mZI!d?3TZmfFW z!Obh>{OLgszhd*OT)kG$i3Bc3eXZT@w&$7}7ffk>$2Py&v>4H|NecTGkpwwO04CwH`asP?$7B z(NpEIf9V6a-V#cSSkN%tj^K7wG5<3i&1{>w#iP*w9z}I78+rP6JL+=>< za^R2rWU`#M2U9s2=vE6e$fZ+XS-ECI%7%@iy<&2g-no&OwBUZT*00a)>=f1k_hO|@ zhFUUTWgU^|mF$V9gd&>d9VT<@=_&zH@JCpfbfQ5aln1;`=q9FCQ7VgIh!B-Y}sRM+oAsRd5rrFW5Fjr)jlXF!M5pvtF0ju06ec_evMrRPu+KAF^D`dCZE*1rh zi^7^Is#Py=nASK)Td zZ`x6Y>mJc(<<;1#7Y&qONm(Watr$YTG7bz*sG}7XJpb9kh7Y!HNx%!-Z81|HUv_)G z9gxUOSh0W>uVjuI#uN}ls3Jae7czd~UpICXPwcq+)K)*k->GeG^zJuN-I$cGH)dTx zH=dxK!LUrU%_=#~O%g$v=uJQjj-jC6u+l(Anza3WHy1eN+|31!cDIRkZ=*2BO!6a= z()`i5+*hhf8>ji8GcY@)>B{VQJ-1CdZZ2<<4D>@cB}z$XkMn|(Z!S7{njBiQdb5a8 zZp1Et9pL%NfiR!@A?@h9PufUCDBbE}Bdm)TDS5akTBhgK+C*1SmcweF88ZsiqQ{Xm zhPPh4kqEqr!Q|KuR-7>YplZ&WQ@}<#(5qk#Kl9z1N2zThYfib@-luL!j1scbm^r2v zb4pg7a?RFU62VpnE3!^sEEX!2!1uy(Vqf^wo&5h>uecIGFUx$_x&y%B`wrC&_kZSa z&HlgFS~LIeJZt;a+wi5IgC2UT1HfZ!QkGaOK!+~jC))>^WPNLZb#QvM*B*0E!I$%Pfz0tE0AgUBY<$V{q$=$0){ zYWh%B+Y`6at2q<4A2Uae5UO>MS!stprDAGao8RSCL_mxB%k2{z%H$yK!qlRQ#pOAaJK45C46N(!2MkZPUl%iEa1H`B+r8bYF$~JlKI%nBm8cAB4afB(dWa($v0ouS3n0rRlcFo#1<4cSD~=}~#oOOz!GwzMsOsPo5U zcU(|9qe)%M$~Z4CD}=16FhUR=Ayz3riK@#cpwZ1OMC@qg^7N!p>sVd9&NVHb`}`gR zx!nH8Pi#<}8JrAQC+LC&){va(^G!SQgb*f`T*0QIqo_LTRk10%TChp%wG`ttXYAf5PHZSJN(ElD zN?s|Ei7ryDgwtiKEwpJzGa7v!Ej8LXBT^j7J}xYsxNGY3I#V_(WFDW`VkPSq=6qq8 z>i~Y?29NL)u?|;r*b(7R>$)?J)kvBww23)5%^7?6izhaW=Vj2ID2p{Y(CeT{BzZ1x zQNvYOwd;{&S%oSyXC^|7*!poOGTfq9?9>u^VAk;(AXIJxPTF?9F>k1hJiy2Oa1?{@ zrGOj>meynw47WT-PZAH~u~U9=;V!?$xG^Dn&I|+V=LU>QXg`Z|M_6qk711T=TU0Ed zEfWwaOIhT2+^*Y5uZUKJS$8^dK{tZZsu0*ktT%;|mE4m;OzMqFXd?1QU60|NI@9Rj z%?^XNBDrp*mh(&!M3aKqXoSXSwVZ=BSD(s@X1=me{XFH4K-6quAGcYnZ>jTyUPSO_UD+jR3ZxAuiMoID4x6|xrf&>z*O zMHqO17D=ccO0cU-#UVWJ^;9L9jls=P$4)@S1`>`MP0s9${lO@zXo4LiSc4daTz(p^ z@{77(>99rSs51vGL_&(<6V4*7niZV(%`K4pajRF$u%|^_zcpG^v5mlHp|>O$qRaJ69l_zYUeb?MKq)R%7cpSsZcRk7 zHpn4!J88}e$;yQ*7^{P1`A9A*Wf(c?PNZPmGinXnZ%@ccuYY>Jxb**?*f1#m2sUP*=|?BzkNX-PB?<3n*_fIQU|%o$bDqUWykQNf zQ3W*D8)0-E7NZh!RO6d1zUOo$5b=)tbt^dC4DF8dZk6%aGB08_l^l-Z$#17>xw2pe*RrqgHe zGrSLvE2sc7W$$=^8-eR{2?Lcj*F%*H2cQViU%fW%SO<&hcvWO!)w)Y zOE;_JY6y^nOSJ|AzLRuqELpV~%l21|I%w2*>ie*Lmv1#)lEs>yFhQ|avtY)mfyoB8 z9jA*m`mF{663Z7W3$XJqk>hGLM&+^Pm}tPa%DUu_nGtT3w4oV!J-QQ>Ta&ry*5bJ9 zAB~3*)0FBW4Q9?JsOB0lr)`*?JO?Taaj&+@b{cVy$VSs#2_Im#RbtIbJG2*k9z-EA zV(enl@vO@gmMmFT)W`ub1hT5)mE5>1DNPxCRWaJeWuYG$HILMf8Z*lP{QuNqGLh@? zaxn-`*|7EM6B~$?Eg4cwTOF2>YbwFnyscZD+g5O{rf{J{50EBUC%5%un)Q5b$q+e= z-~*g16r=_N>)C2GjN(OYP_1=GM!WA&{D?a8C!!o;3frTN6%h?^=X4*XtsgtF0dI6W z9z9d0Be;bvI~ZBXQ-ZgkBhngJqOI2~r8qX-V1^v40a$VkM}1k$$EXNTSZ7)=8m&B6 znNIPx6Lx3aj=O42=b#5c*av3}iWPB`E7j%2oIBkwCi99D8|o%oDY=8ziYypxjnO)B zf3&c&5o%;P-0YK zVNIL*kvZXGa^A+2ZWlCiVWt!F^lxz&zo>;eGcK16%MB3#P9DFxVqTN1`%r#zUVA<)MKQ&8B+VH+SBA z7aIyoz0D3Vw_^h)d8Wq>7i2we$_rzHG<1C0Q~()0AuxFFxWgn3eUZx|VI#+nv*0*G z7JP6B(=K%LdKJaV0wNUh;3y^UAK{GAsBuO#Ukv(-X&{QHee+`F#0CV*7Um^qG9pJE zzu!ek)B+7J;K)*&6~ENixPq}92zf1lk7te!Xepgy4IF^xv`Mcz^Ad61PD-t0X0b(n z((;;caneG-T29kz;i~D#cr;&zb-OCar+$mfNpG(q;DwBYQDQZUx;^e&<8k6(Vu#PW z3NdlJ7U*%Ya`-Yu*O6nnC+3T2zQh*@C(>v^lt_%zLda`GIE_C%^yhq#IpYN3LFFA6JUL#@%T`(sS*ck4b#E znjH`4pubvd^>nL~C)+&+7;XJs!1reH?mov&QxM-Kw8+zmJ+joCt*V%pc?%~d#@B9Rm-wAi>J72Q>x7$Ah_ypqVUUONtMeeVVIK1K+(e)c{J{(Fes4CWvUM3cO# zf`by}TBC!QpnsTwjt$Nc!P#d!8T9eh9F3P#ow@pmjYO$BqcIHZZ-vR&M`*2FDOYj<>EV4Cqm@D6UQ=$?{g=j!? zU0WYE#Zu1eO6CG)J+9aeY=2s+4LE!{AP3bpcE#IB_$&eTY9Mawz>X#=t)`X)(?sau zvDP$d&81wNHV1s4pUkc(8>B=dnBJh zu1Fh+Y_f>ta3Q0)3afWHQ(2OsRS{yV>v$utXMiw?Et53iGnvk)8yf{GIq zYfqAFC9=9%Ui4bV#0Y8(U1Y}d>7vCPY>u%iF%24%O=Gt$VOaxIrZae33hOP_P=j2) zt%2MEXGV3EEIYeY*o?8PwM?>5bcdm(BvKx)%TbYCu3U8m5jO3SuP`aX=&t|tBCcCiq)2<#=H>D zqk7bI>~=mzcK*j^3^xD?D4o(cKl1QimkUP8a3rwfY&fbdiebBAk6n1tu^4N%qi!VZ zew1B}+N^FEEU}!9mt>NSo8UmWP`BHRmz_2zF<$G9bJFf-Zb~4rUByS;4iyhavjI^d zT9XM3RxANS(7kA?aZQc1#`XeV^$!1NBjL-mNxMi=B65QPo^2?RwOaDIdbpB`BXUt) z)FQoTgOhOy%4{UO>7s26FpeIoLbVUN4`qjBV`R1!&>X4Ls&U{je;5-W-3H!y?kx#; zRZ6g?zHnB{3gVB39e311IjP+%O2ceWuZ(9_z2^;W>eBaaBpifv^)RR{voJj{+UrCd zM79f2yWh|9Wwb`Pp2-jmZiOA(v5}aQomwar;}Sd%D|v0<4CriO=HjtksFRJjTdWF& zs_%AkQij?{SlYBdFTwdd>J=+)3u{I`!-z1~>q&w!uCS|_M1*c2!N}orZ=O9PGitsh zL1)T97KeogoJO0?Wh}&O!ht@nzAGsEw|)+NZUj!LYIbm^ z=n5XPM78c9k=dqJLSsK!P6iVqW}-IoEk2vC&EZN}!FOM` zxepDrLu8D&HVX1%ZQ4WoeYH@B^UGYzD)%+8km!OlB&KU~%x#MyusS7@K> z%P?FUN>i}~yvfC=!BPq4WhCs!I2vue@|HxG_shL`3nbRW{!;0eNAkE5P zIpg%UB~Sq$K|g#;qBEX=r_on2PM8km325SpnwC?c6m02W1-P{WH04f%>cP7gZb^Wn z3CvP^6548vO{v0H>>)VGT6m*z;PR`H5L8A>`*j+nXaOLCXMe*kx_dhIQJ7!HHEj#|lj{9+bU~KC0tgj*{$FUS;;j zw zY@qla?$&dMkK2qvR`IgLOiCCnq=jWH+B1}?fZY^B4Xg>XC2k}dL)@{D84-H8UB#Mg1H?pB zQZ5nYMuq1W$hf5SDKFQoz=yxJ`Rq3OWFD;Sr5fHIpqOk9e9Td(PV~ z7r72dAQX2bscc>_CdQwKJ>IG9<@f{nru-Gu;{|y@eY?1?lMhDRT%wXr4zRq%)rdla z1{>_T?SkD`r)e;Ct-V?{?g=;q4)o)EwGE;_Q7)0GLO$he)YI)?D(5W-EXeh3i_$ia zt}Gk(95eoiQk2{6!Mt!bJgUYcT*qE;Fm1jO^W&XS#?$kZ{Jlsn!2}!rqUk$wQO7>? zZ+=HE%I)F_ug6J?LLO}BDY}&+{j?1oI`biW*-u*C!7x`Z4#79&75D~&PrvPPl{s)i zraYZ2R`$#n>2iY`L@sB3RY>*>(b>IzHG=j@$4|xrl{4(P5*I0_SO3Rf@s9&R~?XF#3m{Wrzce zj&cPjS~ZHM$!a-x%#ELy%e8mIB|pXZiEVJA$yObXO~b;3JR_JnBrMi3sO}!F$aLga)G>u-dRW;9ND*z}O(l zv<#43BWt99fOLu=1uMO4D;L84ba7P6gZ$Mj)dfkcGvzFg1^eD?)v^9*+4!-;6PxmZ zA&*5YqZFCg#r(cx$r`E;b|EzXZCeAg z=vZC@!ReSqWX!yAm@n_fZ4G>`^Jqw#4Am*KJWX;EoN>B4O3EK|<6^f;3SgK6IsPR1 z=i}h8&#p~McV)5I%2tmRaIx)Lt<$HZqyrX`)^vuwM*&EMbomNR$*Ha;d?WLT@y;w+ zp3~zxG5?5=a*;c$&^bmA2yf+B$Af8mdR_N72u927T0q7Mg0B9->3;U95Zq^e77Kvy5!Ers2G@#?g6rp5h3W!TMS|9QfPofd$1 zf-c`5bxEm*w{MkV>{ddG7K*^>GEP(Y;}v_dZGh^gKFRfu2k-Qe|8eY2Q&!IB$UD2h zw0I>8b15tKq5`}8_tfURCQ%A%nNe5Llnf)xoW*y12;h+XO*%Pg=&SV zWBt9IV3qO-ScY%IY4CbZRh}*7SVF7=8boRe{GMaSo zwlugnrKidObEMwm;troRD6Y@2qw;{_s0u|>eJSNEFnB02J3JjN8z10(T$=QqV~jGM z8IbZga0q;it%7$ZFk9e-!sOrbY3a#-QJn$Rm#Q-^e6kFf&jEO5qm9Gilz7eNjy8o@ zH4F`N8DAXsr;uU26|?jZ4%!JdERj^x4VcJC51dCDa2LJe(FS+qn%+J&qS_Udj%H~A zD|Y)aa0vrr8uLWKf&Pt@^?FmFwk z3MmVWMgsuOxDTD46;P@=7;Rq0AB@JU?~!g>L(HAoXyf|#jyBRPdu(;AWw??Gm>E6V zWV-^Ex8T9!urij7a}BBsg5gGKoh$*VQXIImJ5oaa#*RAlKzU5RiDC*JKFIC{TO3&_6iObx9|8_R~TIMaadBpG%E0j%Q+wdgIqW- zIIcMG`gyF=@!Pm~qU~w;z@5&zw+1fjMT0%K*a+rDHtq|6E4cnbk@AKyU$q-6z!3`G zbFi^atmbCHH-R#S4@~NBV?V#!)@TC3!)0g0-b1&HXgDvCcog4pS z!?a#q`}4J@n+K*3n0|cqZjj~gCgVNeF2B?8Q~HPW5#28>eH|#w{_JmGe)72`9l3`t zEtyR^qw!9Yaa%)1EM?|+bVJ51P6}lNJa+#p@js>C$XeXqe(({o|JxHD5=JH;j($g zk*nJp(y=KR$2)0~TMTklg7=I|((mhk|0IypA2r6U{ganX63E*RTYF_&Lvl9-bVlmry5k_yyfzWg!9Nuq#+HRf^%lKwAiWRndnImN?@P-%c{ZNeAy_0Ju(w%NN%Tq zo!%HOMmnB}wr-PfD_7B>BOCR)Es!npp3YK+gkY?z&i zH00i!w7fhMErcjxGgZdF|3o#yn=b1mFCLkRH01G9ARf*{=@FbSCffu=`O=A2Dv&vL z?@$7$`>*5QKYtR;+4uST7cT20m`7$J4S4|{ZESfaS_o2_i2x_&`1iL@0y+IDcRYZe zzq~ZlBTd^HvJ5^7ay%0)#3;=~5B%U?q~AZ}RAanwaB0SotJ{HUr6yp_x))}4oSV;f}S)l@ndIjrQ#%*bFk!5bjYhPG+KoAf=h;0YD!Let++`PXc+YwSV&99tq@; znMgwdLfNrDL7|mqJ?0kndkvP3k!aKy{y`)Hy_+Bfju%4X?R1efITu3 zEkr8KL=OPQTJZa;KTwVI{DCMTJu(w%c)O(_Rc4}v7^Ru$0YIZG{r*?A7^U<7N!{H` zfbTxA{A9pz|5n2r4UFM=hIRdi^>3PR@##JZ*!VsOctn47tGjjk*5>9%H-BqW*bM3J z-hArDXEuIs;~xE!H)0#tu76?uj{u8cdi}!M*Vg`z?tkb5Yp+@>u07lQZS&jBZ!r(e zW%F~)X4Au_e=_}<={HQAsRZ6Id}s9?t3R`P*Xpe+KV12nmDjG+RvasOWXVN0#0;_2NM~)v&Z=_}tW={uBLkd>}*%;fj2a&dKq3oKJ)^ z4XU0mxV_FnArud$oHcs^b7wo2Uf0#E4SIHWG^oB|15q(;aD-0#J)bP+tw1h~!o!xx8 zY(oPL%r|8X4b*Jc$r_<_vu^7T!BNL58S92KE!-};!1Z1S8E_cn zLrP8!)ND_YZD^ooyY+NxHnaTLGbT16rgkOc6Hgl^*`5Z1wtYoIJz3YE%NiQUx?-}1 z2C}Z-8Xt>v;UepC?RxyVj5m?DAr+{`XF_c@%2e1)8)k6?%EpI-w5?&09qq!K?xK$f zy9HNgBxb5U&}ETw#e(j(Bdu=FE)4O35AH_&UZ3uDiixrvH3fY>goXSvC#Zx3pWzVpi=vIQw7ECh{3tdCfIL$j!!!+7V zQc!*6<+6z)iRLS0j4L*q78khIK~iaJug3daHFwzK_6fC8MBKF{LiRW}c9g3awntr^ zw%a{G5|}GUvbi3WYu&U9)#$NNu+putg9jxJ~G2=K?&EaYK}q zaMaS?eoR=AIk8-Mdo-7FCt4X8FGH?GNRkji1S{&c9gi7G9 zHyN-pBM~Vi-QKEgm~0_@Im5xBzQY^qcZ#~SqQR!Cfn+cq<+(w)&w$motDo)#u%0Ip zvF2!^-)V7_tLZH}^jFCmh%*@9^$K3x)2#vC6*1_GP)suEAb4<*St13EBZVB{sbo3B zP}T?qn}e$8-Hq1JcEHukRBF9=p$2CJd!y{NTdQ8c*5R=SGl0*HmNop1QNTO2<)g42 z9Ep$n+=E_?F4pZJnGlT)Dr~u0gsMn<;9{0OKR$BkntpXCk4cDJlouS;B;roL1r1f& z1-zW9k?v^Co3$69K_wiDgKMPV$nUU`i7yj!QPc0GCe@)iN#|3Y)lglRc4_(CwKJFQyJKX?PW5}kRHjySH5}oX(;1|yChq1H2dhD7*|95w(m-G> z55P_hbky>I;l$L_Q7g@%fsR^U3OF^;Q9n&~m3nq*Ws#lmwT2ZR?_qGH)ecy&{D`An z!A5NuY>wzmYiJ*48gQo7atp>^k)5l7kNPfILjxc6o~)sPkNP=^2JDRD7LO1pHiJnj z=&$UGTs@xVvV1}G4+l&ykrXSf8q{O-Z<95^MPISaCc(4`*pSK|mm}9=9r-X3NBn^7 zo3Xq6&PqpAgR^x+N&bS0TEM=W`<%>eD332=QPDTw14FFjS>0l+OzXLs3wsSZo9NfZs~k za-)LuJZPz-P?)dS9Q}CHH%w+iKo_aSGk~5#^a-&{2waWpaur*mQGpr*!j8ILfbmoJ%cX9Atw? zSZ(fGE#QCFyB9fmnr{1v=Hx{1jGPS84kW`lF_CZVB5jXY>q!5u&vP=Ml&Z$vVh46r z(gh+_bI|F4i*(fp%2%}8eH_yPZ^NgGvp)J*jpCQwI zEFEqjBsA>Ao6H<12ZBS)ACARRJx?I#3R!x#vfbW9!JF~IoKBy84Qj^as#B5uwyv~pUk%AIT@^Q`L;7sD?8#%0YU+#Vh>L2 zQpHl1_CyOE+G2Ou7zC>p^1@+H9xih7^bYKa=H$d#^<;=~;vK99F0}G-Yomm<J&asB%$$tW(%@?qYOl7_!LXQX!mcd14K1yx zkFJsf=un^vMY}ImDlo2c6&GzKrr>k}e%L%(skdsiOraTUy6SzvTlwg4(O=}`={J&3 zG$$v{YHblhSv(gS;?2gem?4Bzx|B%==JjO1tC-p)K)Nl#B^0Weus;d*OVVeU376EH1*2J8d7U52}XgnQaT)D{HwFQ$BV>!S{ z4d{uYjJxj%>%_cys7%>xaMMLnu4vtjr^PPrEqK^mB4ZgQix(iF z0P$>P6l#xJ?RJESAT#UGqc^a}H{7^BII`F-Gc35aPj%|jr8|dp-qkD4mhjn<(MqG& z9j3^UKM;0@3Z5KUD3nA3BnYFk?ToD)iF9*y>8{bX!xfH{T9rXC>K)`TSE}qs!{KtL z*9y1eo+`)zWf>OyJO|PQqGS0Kkrhp;)9+bp)Gn@2+h`v(|op z)XWCB`NRiURHS?;6ag=iic}Zvrf_$UZGsF^yem$1U?S#^BvRRM&YooE5g!~sf+oS5 zzUH;iy^_L00tDOa`x;4WngC%A5OCd#YvADI=J;CEU+Wae!dquSG zszv%2S@W#N0Sf@ac`7~jJulObw>mLr?JAgY1@$T4jy;21c zT5X}twRur&J0i9W-?ND^%)&+bQLgB&#+p6?fs5U&3wH!|dN!V^4J&w|7f)LWLWop% zvNg9~)fLEb)g>l`<;&Mi;!uc!SCi=;7Osa==}t0HZSHogc(t3Rt(}H5iNOeBMG1R8 z4|5lC`C=Rai7zRCB3MNNd~}wWd?pShH_wYh%e*)Yc9YR|eo%x&kPNX~&N;IEL}tW9 z%5_InpyL?kgFtRM%Wzm4Q)_G|*kYs?i8pyRZ$Y}WySobxI0SGOWT|68&blVz$$*0b zV0Oy+#&KB4@J)~bsav%+3su}_8459~+XY!dxn0EGa6s4qFtD@XR4aT!9PT>9g#m;+ zYTZ7&TOd1G1|^YlOY{db(MGV4X{H)^cQMg+cZ*jRhYQ`SE)HkkHa$_}(Bbu*$VigT z_(Kd9s<C zz9;taTvpwUNQYE)=_)dJ`P%VSWIzO@VU$0s`OrR>2$sTA2Vy&5MIrD5AMMQ4*rYoN*Y<2IRAZnpN1`;| z1a!bxJpB=gxWlvKPxwbddRPzSqj7tqk*~D^?NJj#_pB`riUvkue}K$nI()!asnQ@< zF;++V11rSza_M%?>Og|SOkq^yy=W|v3yJ|%7m>$Rm&5LwN!V!^)X!@Av(F_0dcseC zS^#PayZ@kR?{N-0yT@`O%}J1w>;>;G{ z-j&aGc+eBev_0{dWxxoLe!k@(1_81`x;r4J0#+P$ZShuK^x;FI9S*xGEZM2Y1)F71 zOQ20Rht-sLkg5*CPWt{|(m$}YTwHc7-(dKZ;q8Vu z8(wPY81@WL(SJk#Pv<^ED(tUJ5c?5<@xkIA>BV*H0qyyr(X8~<)M5v=Y#_($H)hDAN(<(4$6N2#P;|Cr|$1> zH0o~$e}13x=i_^F-3OEky;l*Ls?hrtz<;MaIs*883gGuB4^Ia?eJoabZLNE+0{GpE zIw1r8I|cCHDu12>_&o~XzfnY{0KZ!Xte-~~rQenT>o2GQY@dAvtiMSHtUrGQu>MA+ z)EiWlI_n1g1-ZWZXDaH)>#IL6*H?eNB63(?{S8WepP@XOZXo?L6=c_`LN@z0M}NIi z-)j|hxq)$4IP5slAHo01nn$spvQ`+RIipW%({FpL;T%|mkxw@#oT4}SVD9?|y*;AGJ zJz05pYEao{KOO6@QrhfE6WimU(mzEh^_;3wXTO!|pR9OoOHrTiwI?ZF+f=0HcE0vTT{@kw(%;- zcW;%bBySz_Na@W_HR^8ylf;UmA(2+;O$rL5DipKd@${>Tx#i>LRuprFX>&%!BYNeJ zM?A8uV9}|^>Hbx&pY8|B!_!-Q*4Mg!ky~8%{fTXP^7)}s>i1NYIy*z?ejs~I_lTlC z*K4}(D_;AqB6Zwry6-7o`;H=V=r!FVir2oaJeu{|cNMRFOL>08Yu{1o_f6&D>0XhVVpE7kpq^5{r) zzotBVXx_uGD!%x#^6+$D%+8LwuPEwYQl&mS4eB0JT=_*sU0MBnS^4u9j{f;eO2_)V z^5^*->x+t$Kc`5|X@@T;p8TvLGUepYD;?{>iRq(6zPisT&irTP`4LZlR`JGXl!vD~ zb9RE!J*ZUs(-Yfr$NFcb)K94?^%#?s?lX$lKB=hB_u8iwuYE$1n&Y)kDPH@yA~NN* zPbyyfnDS^g96zCW?W4-`BVPNsQooNV4^Q=)b#{=}eN6G%hbOjWuYFW0^+T#kJvKCc zMDf}`DeCjR_F=_q|ENgK@!E%E{{LTE+8CPtVflxE+wVs|XYK*R>#wxPmz6FrftR9} zUUidhTLY_&W?EF~<8FW0Da+*hPS^OG*70xkR{Okfgc`Uvq;fSUDbT=_Gyhw?)&h>v zu#U-rW&h8_+D@I5$#bqmo_3uPF5 z7_SM~1GB2``b;+{RYBzc)`~UDAAJWi{u5cXfwMc_ z+i*Ze;bWw)r=F%0XNFCd%Y&@lrL5BquKMS@qfV!aWD^AeZ4Yx|q)MhxPeZK5Lqx9y zTjF#u+UV7oe#HsL@j3)!bf}7w-3XHm$Gr~X%1}Z z+tIwho_nP2v7RP15KGMuYz;(e^8#C0gFZ%)J-d&enI>U<)1PEW4x#lbt% z`+>rhhN#9t(Z0TKxkNZaL1CT#=Ki!-?* zxb$j8lil!$<8ATYj=M+HTY(*wAu3f}3en|*7mq^}oI2Ie@y;wn=bmg#uw@HXL_h)8a9aj|s#@K#8;ewR5QozK|Ar-e z>D;BQf8BcV<_|aT-uV8;X#J6OVeLC>y!l&Z&h!ma4}6zrR=&E@Ha=u*Eq`g5GJL^M z*MCk=>K@b)OP>MiC;!~@a@{h>#;k>6Ukh*iVjlK;UcRImfCg506#(wLq#1w)j(;@( z?imBnQ0N&q!F?~*4B(8L;6-Bq8WIv{$qb;T3GN;P(7->xt%0(9F_rc`_lcSTXkesP z17M^XfCheXH2{WV02=7^w>8j)s{tTr25`nrFwhL(jGKTT1JIB>;H;aVuNlA@Hvy*^ zz!^6II|iU(a5?KH=xGLU#!b-G4B(8LfEfeOkm}*Ao1mi^z!^6|TQh(&ZUTA?KtrKt z-2^So0M57xnqvSOX1MJ$20)DgXn65{wt2blxv!xafQE#Ai{Dh-b6;IE01a;_)BvcB z0cd!mf40dP@41iE4B(8LpsE?b88-nj2B6^;*x5G0n(mL6Y)dcFpQ{>1rtZqWtUllL zWZg4$FW$Op^L?8?x9QsWp7CbmmsbALNE?55rM~=ED?hRE7aNy0g6p4MfAe~8{noW_ zuDxb0zkId+2ZoEQe`fe2{hzJgz4W4U?_BzG{oQ)c+IjQG&A)06m>x8}Y58C|ru$>v zzkquLzqv)}e{X5YZ$!bd=L<;37cC@NdPH&IY`2c2gqe%vCmvrg|M&}yC^(O;x`@*b zZ@4V82Y}4aQi_LZ2$5dE4hFt3FJydmWG-DkmgU&e)?wWyD5xwh1j1;C zsS;g|OV*w$Nm~1hN!!k50P1O1r1^OP6Q%*xcG%T_CD7R7*Y=xAy0&f3V~}Y#Y`83V>nY9B?Af z4SQrL(5I?+iq7{M-T9vAHz>AR?&55JRF<Quiuzl_ry z;&9C#XkQ(Lj3_vfenD;VbB|-K4ksN>CCK9)ZdI6cIF&^lcDU8S?M4(_nNeMbnL~1` z{3O;?nx2le)&8Wzs#HF`eP&P8uX2+PtFm~x!>+PQgQy*Uk2i?Pt~7|+A`TnGWSf9i zIsM6?&D&eZ(#9mVFc7FA_2ZA?)^gdGNG8hVUFY<%gE0^O+B?p4%9|ZpOctfmYW*cw z+O#+6d@9F?>CUIWankuzDxcQ*tj^g*g8t?+Eq-?F(O;N^yh`+kJxqVoWWZ6Wdc1$@ zwCaEvtHkgTKQ;_t6=GOKIu#YOoADPX4ppgqT6@`T(_LJ;IAnbN#GxvS zKXj<^{)t0XsvdW!@eLD)sw_fwsPT0ZhZbXTrqxU~N+r5oDGf5U`4il90IdAx#Gxvc zPj#qg_GZq?Z%iDjviL)XuDogDP?f639lG-7i9=NuAv<*C*C!5z8niE4qeqEhh=rh1 ziC^qc!=u{{mCC0%)aspesNvZYhpH_8(4mG~CJt4pdfcIgpO`pQWf8JN4L2_>>8_p3 zOF#PmX%8sj2diA_pVnJ!j@kLg@TsMxNq9}Ti<%o){{2h zxar<_=SFJ%ZR>Zey?gC0^ViMynXfkeis`o153Yt*{%8d;e!|!XS@~YOd^5-uXw|<{ zpVK|06Lm&Vi0YsH^@F=t^kC~w>b`d&fJS3zwwo^Et;R4Hr2B#SA$Xc!thU($V%uzl zB^-TkbQlc{;4~G4BBLJ7*4zz@3}>=vvLD16kQfN@l{$MM0yMYWLh2q-jb`o|f14@= z0`64OSBL=$l>&q`JneR^Fxa)0E#)xWf$XU?X}2|ZW6dmcFakzz-z0V4Rc-WmxNf8A zkPonG=7R&;6RTB2MPIR2??Rn!xEszzOV%0-!?~P5v7u1?U0x~ET6dPuGqOQg~ci*MKAv3ckN0a)?^OW#&)X=cN*jYq;B z#LqT4aMjXJ6fC>;-4Pc{63KYl!@IIY5hUSqqGBFSg{Xr8FlPtmzNOmS^bEP3YQTk% zWze>k?WNWr*tDPnEEI=(NyHuSda+KklXTIEa*k-#LD%JhF{$f*Q?;?vyYA5!Ll(<+ zIF%d(2gyd77HU0)vo!+Irs%9-7~=D!Y*i&3sr(Ho>V;-D{~0agEoYBchQz1?#Y z-KG^5+BPduY7Nmc+U^xnNu-v|VDU)Ln=iy26i@o`D90Rdz>3sYzrL8~>crP|TIeO2 z16Hy4@M4OelPzi>(yJfzfJG@fzNWg4Q_-P;Dy)9cRV;l~b={^cX<$d94j5ob3XHF) zuG5jg&_L@&9ds0152>!z5nCF#j6e{!fiWovzpT1qrw8HeR&`qgMJRwnD{#J~y6&gL znVsghH83R-2Q3K=X!S3uc8)S8Xy8#L4w{OUFQ~4wVnqW>Bzr&sE7H{b`NgNKlyPAG z)XYb!9^5LrEzx0#&N@To07^NiNZ8+x?S_gFUh4-d{Xv*&N5RzG07j+Z?sKY*&Y7CA zP-kGTCeTsX&Ahgjtw34gF3J%4L+Y$ZErw<9|NeK49^g#_+lBVVdRa=^!n!_1)(rXpa zVudP+V8K>$Ayr>F(I7@XN4aOMJMEqfGHQ{zl8-tdfjMae`e)VVj*JQ#4lRN(tO8@w zsPGxp#!e5z*;~+B&P5UjgoFlE{nM(^C;_73@FS>nMXB_s7N0>=BIMLcAG@Tzt>M%n zdw>HpQoMdrHJbVHs^Od-b8r_hD#hz3R2!WWuNsb})ep+Rq7<(mS8Z`BUNxM)q7PmO zEJ^YDG1Zo4<5j~+BkJHzU`~qHkE%9zBwjTfsR3R01;CgTuOCru?DVcXd;ea`sU70r z4hapY`iE7cQQ}p@$rw=Sl2YjpEk2^5Dqd$_aca5DD((NDx%7^ubN6iha7)~LWOK0b ztqpel;q}hiLu*a*7tA&A?Y^@5snr**d~Bs;{E#uX{J~|+@IFIA|86~^dzUT@lpp7x zO9!M8Trn_d*`aFLUM}8PT{@`B2sCV3wQM@oARuG}8n&`p_L*uBRAdAicCT9YplT4{ zGJ;??L3nLw$%e69+uF{Fd?ttt(`9Rj&o-Qcc8ani*=jRtam2SZ><||(b1xm-B_q&~ zNa1WrrY{|oWds`PwXI=Csm2E{lo4py5pQeQXD;5-TspW@Mxdcy+ZuMCYJBhl8G(j+ zY1we9@xdK30uA-jvd2{8gOZFu!$w+59uGA>D9Q*l?47k_Fi_)zf{Z}J=5t%awsG;k z;?hB0Mxde8Z4KK=H3)Jt0u7~Vxh<#$K~_efVc)u~Vc)3+K}JTPp;RqfO*IJ8GJ?nL z3f!dwOh!QQh(D6fC-FAtu@`$p+g(o&!rpu}QyNw&0^F`;d}Or&AuVECph8)}R-s@Y z<*+uyVy#u_v_-eCRms}PWG%p>O|%btDbmqeyk@_2fXWCol&WR#s|GF76jNoy*f^z8qmJy)!(uisHg-$&dMAK{;@i}%=6o>UVR~v(xoWCyiJEf+t z&>+s1hV9ZpL`I;2+)c~we(?(iY5%{ylwCS^aBgMmm$z=*{N>FX0F(du^K!S=VpdtvS0RzFR|)-9f0}bceBlKL&Tp4J2ew3#}o6=W(yw9kklf3t>3P z55#V~!GtirlPeZ|Gv@#lTC`g$OmzfEAU4L61gT6aQ5w{&OMDV0_mrcyUj+Mg-bo&=;YY zWYR(KDVr_EmUD5WkRv>mET_$s?7uoOF@$%mIl9#VNti1(XWP~v;R0K$mHCl{Ci9R3 zBqDEOsbafdjAk!H3-PSzi*@7ic(li$g>3pX|M3-?A{rIh&~@9TLaLnzgOr#)yqM29 zT`9bg#CY7-v=1#zuEX<#M6h3NC5CZAqal_W;C-@zPb|5q@-Rr`JC?Mc;BkE50h!St z2W_`IIA1!Q4m#qF28Xx2@nHQzqK2nJP_)^}2D2G_ATso6Rgx{$@0vIdN)xFL;={UP zimZo7PpDb<62oo^sXM}ra4AwQ@g--h(G;WHg&I*!6vO%OXxOfHqrO&$IL(smyz-<< zo=P!j4<-9(LG5K9SfFF?`QSWH^xH1aAU6zo1&xN}Dv>XgEnU4`b(P(z zI_AKFg-$q7!rK;Kk&7fmcVPsjP>!|ca+TdYA&^-8LLB5lO6T)XHBY3eK_VA}PpMLy zB1rPiiJ4|d%-6mBWP##|oUOwaP`lgH&JOu%Ic~xG?sk?+3V#crrH2vu6dTE= zDo%GcM%HTr(QNMuu~yxYv0P}^L>Qwwejfy}r2*>W7qv~hD4TiKb|oDfSRDbLg53qI z&80^fnxX`x5V!h32HRj6>V--^p-*&*^$Xz?ff5ZeK~@IU@SvUSu%|f9SL>7PdHI?M zU69S)w2gK7EX7%AG7a?;wp1#CQNda&m5MP$I+1U9C`X`7cgX>{tI?dFtT8NkUYeNN z`AOwaw&g>HA{M0Kg11(H?2S&O79BXt_T5CGgxEdqoI_04ifEg;Fi6(O5@6-(@{ttmzz{hz#Q<<8z{kD7=r73e$=4&u07U*#c(r~!=u(%7km_U<#~kAW^TH2 z*-7QX?LwO#kWI0hNQP{-cBO8GZIKbyV%TQc9}7DgE*=v=8@eqQLKKPh@j$qQAOSep z!cwz0e5U*1k;-KzmD}yXA=vB0X`+M=i;+mNJID}7F>dQ50I8u!SluA$Dr<#NGOo?s zbmh{M%Jssd&MsT^6p~4%jnv@~>%mb-ELc<4Zp@poQXQe}s3lt^q;(;I*6{(`rUI#K z4$n0?ZltzbVw1|1>V=V0nmq7S7OA8ifdR$06YdZ(U~OC=Ney@qyG)$U^k8k~rYna| zDp#*Sc$f49a)OVhMN+VU#BOA?4>7K2OGs7)$Z5BSYYsYDK`%7w&7vR(%{-E*3#^#z zpEe#IX}Q#-at#Q|L!K}{j77ScTFsYfy9YVgVk-r3kZUa5Kq_>r&x*T&l24ntsme8z zvb7twnI>C+Kpx4S$JMfk#eT}2fw8XN+sR?h2uPhZ>c@IjFs>oV`USx^ssNhFI+sj= zoUiq`@3e6XKi?+DbimbwZ0oA+5K?yTj{MxP#Y2VNOsQP1*z28g(A{zeqMd*>6vr~< zVg~h>gbQ$-B1FKJmuZsCN~c1OQm6D1j_+QI%U0m+tS!|W_}yJ7QsYatLAZcLn<>b_ zqV;HGH!}1%GGwv@@8+4R<3gv;3%P1`SQ8U)1CADP@dPWnbLXX`i|3_3 zoX5w5rtdsW*7`4w$pAvz?M!#qV|N#eg?clT7a|oK>J8+HT(@rBO%VP7U-nr`gEBO1 z2vUzBGlp1D3UNn(z1iY-JFw7xNG#IQ_99f7ULxqgHiuh`+ zLb;qKG@4iHKQUjC$8fp&_aGDd7R*Nas=Y!oVsCY0K5y9qvdTu9nb@e5bVjm_Kb6c< zyVXvC&uX-9JzD)E_V<@y89;Pf5P||<5O~CmReeaDjzriz=PxFm%*ev}Jare;4=}xa zxY=UV0~mJz1;AkkI0E7Bh>XCuT`OC#BFT-!k)_rm18F{6i0pDK?{s#{qi`@%?K+|` z>=H(v?Bb7g(_=lu;+;C*$2;74kazlghv}bhGo2PW;z&p~el_?$qxzfP;`j2?#-)rg zcZVl+)bMI>@hgsLGbh*a?$E@zhPMEVclPRxOZ)$2-5ZxS`sSCfoCE*((a#gh121o0 zUY3Yb{~Mt@+c$tm(}FuB{@Zz6#_o9}brNc5aqUL4NnCf4wda2_}D^a-U|%F_rD zhsZX8v9V69%O(*7o0%*n1#Tga|Ln;lNlOXAF;6znk*rLjeNwu>Vzg{adC&wjvs&n%D~{E#LO|WnX+Xn z^zk1hj53}XkTUoAA@DJ_3f`F%)uM>jX(^pKAnK2GC?Z2Zbn7I-Agr=~2By8D8FQaxOOqOOpK*=n=cPYk?% zhY#;Ws&+n^q?5KnBSr=pDB126{d^lsG+;*nyk4aVS0-}hxkw_2CGV`*qTxL2>!(so z(Z;nBwT?4VAVfA~OB(1;Uanv9@7N1)!xHYcQYm52Z?zzHX9SHk<0v?Z;qT$LwuLST;DSrfU%^T~kcvQZ zA>WY_hLBa@`@3(%>tuPnLa{pC77O^0WtFNfasQhx>&D#wH|W4I8`Ym)7jJoHCYh7c zb2yIo^&7V}(7H*juQ`j=zdJpLhRxx;Nlac|NpS+ukK;y8_O;lnlEUKk@9h0+No#8T zdnCZ5HRkbIM@A?uX>Zi>m0dD)g|TSyI<3;aXKpNZBBP+k2Mq@vS2R)Y(!@?9>Msv7c34hNOy5d;$)*ECz3D=u3aMw2|qB}d@&Zz26b+V3LjTnZU;dC_Z z6p%1RLD^!kBYKDkjPL<3;fDquhWAE#t&Sy{h`F$CFu=0i9z_LI#p1Z?3jhD@OTV*x z?j7I+fFGOz_?xYt+KO*IdGjAOpSt;)&BDfqHhy6vw{h+IC)VGv{(|-MYY(pd+FE^W z$NV+(@0izw(zWuUvswOvZN`Uu{epx0XM!{OaY* z^3x3;GhAc1!*IX;kLJ!0dGj{6ilct9$P?F4UbhKGD5+@A5+6 zvo}y@fEyMXpS?#uG48!rzfgD9_(b=4_v&QeCi96NpS`Pk2z>di7w=sf>zhw$v02We zL;clj1@Hc)h2UoaOu&2h?^&ojYkZ=6`~KY$aFgz2;Eq}5mP6or^Nrl|_FuHnb+h*i z4@)q-cIkQhFJ4%{EEn2T0k8A!iwmnit3Ro}cYm}<_gM80b%%>|owF66=-$3BjO%=w z+h>F9u+I9mXV<*@gGJUIvn5QdU)<*vkDoZcIA$Yd%DU-A7x(+)0#51f#~54=3ou=G zn|GgEi2mg6&MwrQMSocHo($Y{a?NM^${}#Wx!}e9?!ubSHu_-!s`a<1V@DtsO`?ZC@XZ!gf@Rg?)y!+%r z<0nq0`_+ZIv&IjB6Eg6VgUmkL#Sek&&83U`m4!8*?dOLDjDrkci2mf^<*tRgv&JVi z_wJV$>dw}DqI+@wg^Ma}J9#D^SNhIH#*Y;+G49=e!6IGz$)m#^W8kKfo7;Bs=wN#K zzg^rfNd_YUj1stNs78;)ol0)MwPqBjSjCVh?(Eh9k4(+EG+MgX_ z4(%Jy1-$#{LgTXqOpJT?Qww!xD?ib_xSw1Id^T7Pfv;XY^6n=V8b4{6@b1SK>YhA2 z#TM$$0za&DRIc>NQGSe3~ z$mVZu-o2UHd=|+6_uge{`Qq|bAj{wXF}94)*Z+zB)gUwA`_KJ@@oJF$@6VTCqrXjm zPWPbcuT1xwdZw`Hy48nQ-?{qwRc1A~dJV`E`1Y08uC!PD=iYMe(#EfEJ^Nhd+%4u` zGW?98Zg{TY$@rLO-7;PjrZUPw!|6cdMbw8yebROL%$UCU&XX7TFUX|egvqC?yjOz6l z3UIG2!r@9Q0--6mI}%zm-?{ZgeNnAwL0?cSn%CzQQA&V@XjEu|qduVk9nBs+A!+lj zK4bM6iYVl3cWQWU&c`)f|0Q!AR)XVr>k^cl6HX?6vV6#xVt9hSJ%pi*l1plGx@oM~9o&zgQ#t?18~enze6Pn&*Pt>~*wuU148;4*+n zrsxs4aS{vQJT-&WXf~SFikgfjwW6!WRkflk#uc@qMx#-!=(2HHt*F6hP(WYk_*t?LZeDWxu)j9#FB zfm+c!^mnKgE$K^&=)%e9S*y=dE4sb9tyc8H>IJo;*RM{R0P~d)k}3Hc5E+R^n>aMn zT{fP!@ieuiUbi}Ff`uNqcJ*4drCzgojUrlSD)G9%nIM7~(!mO`;0#vXD|N3_EBXrE zE7Xd`MC@YGYu6_cyKn*hz4h-YeqAW~$oeB{n_zuoT@hWlc6p8I zHEKnF&h&GN=)%$ECAyb@zM4Dx{;Jjkn;V;IMK?A!)QXxnCX1(qQDEAbES?rN?DyBd zzow5Z-MRGOlHvCauQ1?-+YDFfA3FEpbANE|Rp*Fv&pCJ1)|a>5zV(Y++*Wk!rp+I0 z{{7}}Zys#kv1#92-}vW^KiT-%jn+nBdUA{&C4*mW5 z{`%jp|JwT9>*@8Kb;H_6*Zy$rRcplBbJnghf7$$Y-4Asi)ctqeW!(#O4&BDmgZgdr z{bt^rFhAS8Wcsk__f4-b;U=%?$*W&necS5mR(q@P>V=i>t-OEbw^sI7iYwL?v+>i$ z|E=$XDxCIXc(wu6Ur5wjXfBsch|DOEOZACXd}gTIe4k3W_o|fpJC$qbWmU>$RLZ4Q%3&(yP?d5im2yeYR_cQ&83;vuO(I#yCq{WVl!ntYgW3i%w!2sk zRw)-zDF>;P3#*h1$#VXDt65`%fkeF+t^u}>+4q8apGvvstCYK4rQAi8a?evK_gs~7 z&rvCN+gL8a4pCmDgmhlyB76a4;hX6%#&4*U`?^ZGhgHgbO{LsdRmy!u9-bQ#!$wgI zr7*!a#L&TPd<@iy@#@6->clv8Vyrr`p6roDtTF~=x?d=#O!o`r-lfv+U#XP)OO+^$NwpOoeBP9xC{baJ_FyM!TZCOp$$ zHeRMu?xiZ_E~}I~P$_q>O1XWNaxYQjnrNq2Y(t|=bJz$MarTH`Y~GdcsFeG*O1W>T zl>4SiIb{aK7u7?V0T;?CGvGqGugSyxqU}jUofxD}EG*yuf9KLh-Sn`rp?l|9Y!!Y~ z#(%H}?z`c#X?&q7t~*<{*qIAeCvXJVtn)5ZX&@GruJCBN^*EN-N(pf6RCU~#)nNS=5{9ntk5GO+15^ACy6F}>h4*Mt1jJnT)wG-!xBk!+*ISNp*z@3`VP9B|f3vuF=ktS&#B z4+Ju`B4Q(wqF4~ofh)P!sG;c9Rc;bB|f79gtj@QyQ3qT#F+ zjkt;eapk-+UeOh7M|yFaokBS;91nAa{2(7sx!D#WBK7)CE$g&YJY1>b^#q4in=>EC zLRP*OaKJqpPxIb-R201dB0kC%`jMmn+p}FiU?5O&<=+9SE=_+|Up9`XKdEc!zt-h& zmwOL$Gy{Vo!nNp7tk@sY1l&(!=~iYhSV3t(dp(HuBY9}gzgHCsWV{sJOM5*YTRn~M z#o9YTb%ak+)j?pcZGL1aw)Czg-A``5dFfr71Kq1PgPTv$y>R2x8^62pV%@hkq8rz& ze}4V{TR&Jw*Uv9~f9pF%X!bm1giUOA__1z=hM%OB%fy{Elht1(1IaMR)w*1s;> zKwTETn-?1Fk$g0W@WiOh#RIwc`g>#zteM1|c!(?z$x6SM zD-mK0vNNNe1Bd-1vQuF?MAo+38)ob4e=Tcdk)Xqu7pvVu2~Q;x)mWgm3-S!JUbY>J z5&l{=H4;j7B%g5@mlchOHy;&Qmm>)mg8@eZY5ROZ8V4leP9(v$dNf1lQ_Ykct{cOO z#%_Q?ecUMI#eBKq$WsjO*7~C)6sifCxVr{ncm&3~WfvA-eS@r#jw8*yleG_`4BCh{ zW4>~!IdG-w)}aRxoNlDU*tu#w?tE~_x?cj7~i>!k`QM*^!QV(pP1 z#kw8muG5yGVhvZw@F`gXE>%)U%^P6DY^2g8@;<2M6tjR^DBQ%#Ey3r)V(}`|rATJ| zWwJ)3i?UWa%ZgcoN*58#V@-sE9rJxua!MSLhca?&In`}ftx9=iYr8^2+Eo*lu z8i7tg*nx`O%t%ZUz93(q*t9JSLwF^(*W7Cqc9>xm^|_j%wP(s2VMm4K{B{fydQ6*f z4&wt3_gD+XYJO1bGLD20*k$T?WCRPQtgPW{qYj=TlQdIo1tN}g%hlrI!&tUlg{xU4 zT6M(y1&V-#fMcyLYq&kR0Lq0%fg&q{yDwrb35SCYd(LAEk4DZxjkGoWLOl?4)lDW@ z!<8r`u_Eut+5!}2?*?dZ7$!Yzem6;l-B2W2V#1ObFlIBDEj)N3fq>Z@c9XCq>^2Y}#3sgmZ13pSZBLK7=eBC-!RF!H|J=TH ze)WB)&iT%Me!sy;hdN?D9OH|LT0-lYo;5?G4r+jXHT2|}F(NlXN^(8d=6qc3bN%5I zvnyk0xya5I>bQi{VvZ6J0+Hsqy~i#QWj$R>)2()%fg~*?)7Y?UrsuHcF50+An>D8n zMJ&`QIH~_|iO33Yp02RWya5zP+;+c45~D>Kfr9}*Dt5EvEbRpaY@tEhZ@fgnfY4o~ z+6$yaCd}plMXm44?L;}Ym30DUubil`mjE@jZG#2g90QA>Ufq_s<%UUCd zz?F7e?NnW!8MlHKZ|uAr+Hq4i@Jzd3mxeu80T()00Vhf=xMd*;?Jrr?E^`lo&T;M0CK-oi@-wBdKN7t4!x}y{_Wj!T#wb0_p*tNX8)n zEwoCs7ZYu{Kpd3_s)f*7%!=7*UJ!JeEI>zCj7Zap0*niJp2WwhJ`MC@RYeh?7(Z<3 zbP%u$$I2qL(1#8y7uhH+mm!la*Ft3q`ajoICC8X6E!if^E&?y2ykQj<1t=O8p=Vr} zC@t`DzmtY=%AbinxGmZ~Zv-|j60q1Ws6ZuUp_*N&T$LNC*T;ydQOLPNmNTw3c+4Z~)e?p23?E59rDm28ch6c-e$8QXEoUK$5ITV5bj*DWIR5X3Czu`=4O zF9gu+MOjeMqP$cqWJ6)+b7C9gR<1s6t39VZBjFB~Mv<-omL56DX%uGQ9)MTvDD7;f z;rT;y}UG=*wGdmzs(L@&an$ut84H;WEjGq%4Y zMg&o|)oFLCHA)D^!mO}tG!%78l&2_c^~YkT57z~()v$tG>Q7^Yhm4@nAexsUmzy|> zqAFZO%B}~iPI}C6X*10$YSA`{*~wOnXoZs&5H4x-^L)-l8e`l7no)})f-~qCFaR&b zLc7zMjXRQ(dS_hOn|X?CSN(!mmbj&yt%ypIn6`3k$(8EE3QUSbjY5D>5smG{CK?@Y zOs7ZVR;$}^q)2p!oWVMSay?4~(V|eZ)Ny^Gs`%X5{>2zkuTKj|OEJ2ACk<6FwThQ1 zu47Gvg43`E#SEiN(ygJH0Vcv2QA13e@#t|qa46Cz`=fNd*QWDfFHKjK>S8%jsp_Cp z!W*-#zlsrdUFsTSwuLLc+3GCaX1$EgSrkT%E)13i<~fb5_cEOk**YhnnR<1MkgLO`%?BM7owz#iiBxi&iZ)U&s2Wj38X#py0=;7lcKRfd!40_tjo<)p45xm(7c2V#X41E?VXxBP%vZ&oAf8O4o$?hEQ=4ztgDI9l46R z?ZMgGVuW2FBtpQn3TXZE1widTBNxknz}!-!(#oMtjOcPvwqi8NYpbg#PbnWxQJ>T^ zeOm4A>*YkIff z`n%{QA{&i3xY-s9=^me_#=$(|reJrgXEo8bBUJE4nk(d}0B`L7s~BN=rEoGDmUMis zW;obU`pwLY>b07#B8-tw+zj^kK!S1>({|+Hu!PR z-F;WNb#3d0MUj~XYUD*$B_qX|J5iQ2vT3S3*I}mMcKbq4Ats(v8$0(`w76tE|L{uh zsC(W7c8b7GFw%Xs#@8fJP(*L)Wo^E#@#-8*y-OGqQqgc!;IpGeuCHutXN?=Zeck}Z zvcQ_t&4WrY)cl*R*spF6?WijOL{15ob8xe7-aZ!FEbo5 zQ6o1l@^hY?vYJQG%mU0QT7{14Qo5l?V?yoFOPrk4+xHhB*(k&7d+6bL75MTgpR#qs ztKeX@Ny31_8!y;2*Fl4zWlz&^6{=EfpU%pfo6U67D`HvGoOuyRZk+wQcP8EQR{?vg_zW$W z90^E2(osRH_M08GSGUzRI&Rd5oavNBS<69KI)`S^jqTkTpRWXVR`FS5D3mKj-4WR8 zD>ZB=olP%BWvL+v8ahD?XIbb&d1Yy_^5*%vD?VQVjIH8R?yxoqO#`@^t@u--3n^2T z6xgA}74TwF?RWbU(i5v@!{59Pt;J^rm|DdrB}-FcDgkQOS!*~LWjZ+DUFPem%3-LS znc$sr*+XQfNO+@-zu(pIc^9M6xwG;GWP4a>gk6}O1sS+e@#jia35K>TaVZ zY$4}O+)@qbM&b$&hWKDaU-LV2ljx1{C!FDfpv&qS$7gVE0~@RQS;rjOcYA!MUoKj7 z)yrhqF5-F!-!9a&WmqfPXk>~yG?$!5=$TCe4TS|6Ec_r~8qE65(@Pt7*U#YGy0kar zu^Fxn!F-*PwLVxZ?56v0tr==PxZlZ3D$8-O(#TpCJG#Fl(39G8&rM)&6%Q)E^e}~H zn#92ILM4=Ig<;h7%f(8i>~s_3@PQ0w5oZx6P7eHB@fEkd4`( z+!(HXo~t`v0hnAhY%1Wn&o)J6K@|qu~loM zw5wI6XmaW(n4#&8EJunmlnn;Qb5mp3ss~WF3)UL5*@3XJv9&gU1*TSQz+BBSc7Hfl z=H;1=Ia7n3w5t4Kz_GI^DtME+kT+1R4@5;kAHQK0xjO9bB8NL?R=$926YRKVYuPl| zEcE1&F8h38KrTB@+JKg0&(=v&o>#ht$n`e&<--GG`ZB{N-el%!AV*r)P}LI9hU%2G zQXeQt_&p!>O3I{;`guxlc<26#A(t_6Wrpu!vbfZEa#?a0?RkrG}<4N z9!3z_)EtL$No$97rN z9N1aKXM$r_8{1in&ufQo1p0p;+&X>P;ZN`WKKSKhKmR1Y0MHvcT^-0;=?y*5CTi_K z777mV+(T9{^MLh+?%Rs54nN&j#3ZxcjIU64h?v9&{gdkr-TmG>&p183dP5m-^nPXG zo`(-B;M51s4ICR2=F^5tgrOPMnuyVc+$KNhy3;(R z#JoI?toCrw>gIFe!W z3%3pYR-M@B@Zm+)SBDQj@wF$1uJT3C`*I*SC|2B24vae2WM=lVjZ}`>6^>!Y8ajVuFxvL5|2GUePbr3b($m_Sk zVbpx)o?=8v4*r4nCcZn9V7zc~p7-Ji-kpki31o8bbs+fhkF)w>D-o|tF`@@_duE*u zd@YWj5PkO-BTAB4ke{b0MwEf!3+2mu25OVE2k2BXCu-llIlH-NLG6*L=XkoYu(-m6 z%VkT&Vbh=MIp`?|D#~)9FV*X)J;*QXEsLS>k;Vw0Ln7On3e!PoDAr~oco$g*feL!w&v>f6@9QeBJq8`x z7rf!6sT&Cj>?y_gc;3IPu=&D8$8wLj&VD3L9RI!JI_V46P8?rqsIR`sM>eX9z=ONb z4L`-mgp0ZClgflublxA5Wf()IxlU_pP3@&hW8gr@q$sy$SesjB0-`xW(9Gy5M`oow z$@!UZYURhnX~76QML=3}FOx6cL}n@-fW19Qbo)0+WjwbQHMP#^czt5Ag+Zj3Leu8z z{Xp&3U{*8RC9gXJ9LUUOUFb#DS6!&_+N0HQ@r~g8eu6skl%nfiy=3Mi5lsoEdH3!m z6HJBg)l0@R@Q+L|y(BjN)O2TyE1$2_on1`}?vBqV+&+DhJ;=LO9#*!VYG4;~@Jdqp zo?WiYJ2zReKkF{kO6!(vDnavBeb8O@2S}L;>o^8ank9B3`~FkEm+C2$oWEIM=1pdl ztw>`E8sdDWrrshoxS*rAvbP9jVRuENWcy93>1VSyM`)d%4B;s^mPe!*8H}4%4YVv^ z5S;Jf95{jmhU%*U&V95vS6N@3{|Ei8vh8kNfA@;5f5p_lV%a}?8(`*F&tA4-{l8Ne z;PjhfCjOHTJ~?au(bwN*?Y~G;BgItSUKRS4h%kq z`-p#mnB`LBn!Y29d}YRltktvp{@P;v*@w>l=cY+Nc=pGeCjHB^KiV|u2hRR*)1?3B z?7f#s@6YYG6KtBav(wu&X?w@tG-+$6yJ=E)$J;b%bEmUu(#B4E)1>vC)}~2oJMLvt zx!Dca)~IB%tUt9%%XQM}{oa1GY0~a~xM|YPet*-Xt^L~tdtYCh`?pK$zDeu*w~OGu zNo)JJi{QRV!4a3sY>D1tsdpH`n@01_pxX-9B6jcp+553glm77Dk8Yau`}Tfl)1=?K z_kVAi^n3PxaMPsk*!%aJCjIWcAJ{bMckO-uWm3xGm1)IiXH&P)S4nC8fW@x1d#SG? z-Z!bbYi;^gW%p7rdzi*oK-yZ$u zrb)ly=r=Y^`t?V@ewnnl4913|RV1&pz$#6CU5I4+SGND_rb&N!`&Tzj`b*pI-Zbei zZvV=rNq=Gcmp4uN^V`3)Y0{tD{>4p`{+I1vxJ+tyW`m(t)iC)t~X|j9T`mYKSf8PvilFpuMl&`&EquKl{D2Z#x^FJ$jb9_EXp1 zbj`f>38%k*`t7IDY4LRHZ> z`00n+2S0uA&kwAF7wx}y|Lyye{mf404sdSvSlx4!E>v(?l)?~~x81AFghzgea^echu8krTBX%3TRuuQyED0Jozk-BeTAtLn}Aw-Ho> z)Nn}^u`6?!Tdy}dj;SIiP$emqxysSpdcD!83TTZhHY23qD}n3vMptD+(gjJlKayM_pA= z0fchu)2;-rS2)Zm847IyB8k+^D}n134zq2`Pyxp>p8C`)f$J3xCm4#5G*zyoKIN(k zx1L1}+_G0_QIg76Mn-nM!da9>aGb{DLh6&Rax}YM;c%|x$Ol%-^+{I(*DD-WHWgCiR0dDoxDvQt;Uq_uInY>9bm}En0@o`XU&dIRpctM>J#r;* zy)-JIW3&w3GMv#Sn2uPCa}jaJ|BnI6#iTlD4I!UUVgJy~0%p#S*m= zO_oxha8-p{_mnY3L$%HCaDt$c|zO^IT0BlXNHf$J?C$r&0i;Et`N zo^d5`y}}uS$SEK;EGzZUmB9512M)m7a@n*?ThwzBdWBPUrvk>t7-;@at^}@^Kcz?|O64n>kUG8+ zxL)C8Mgv4KN{OIShgVg&b)3L5CMJq9L0mZ$gVwv$vSX7vE6JjsI=aeHXuZOTEMW^6 z33{s3!Ii-E3P*Xhrmil_P0 zk3Zcxee=m5pSDk4clM27-~a#GAMAhT_DlCqj^1?ggL}W0dd1$i>`nIYy=UzH7MTCP zdY9dO-p(JS-k17MJKwwW#XIuOi>`g^_MdG3(Dq|nKe26I`;CLWv(GyCyX!x9HoX3| zC(35KqVLt*HNBZ(+_Wp@e)rYe+(yTv9)E+XjoS1%C?AU~TZ#hNuo!0p%!pvY>= zV~CwKCsroMTdXlD)Frqm`@4S^du0N4q3l3I;7V`oL~2LnW+k`KZNZCjft>1qeXiWY zdo8KfIQh!hMu%)=(J08}^HRPl2Qxe?!>BVeqs*8Cw=x}K=uWcEG;JDNZ`h8FG)eyJ^O+X1*t6~6iK5cQ!k2EhO5)9 ziB1L)Kd47~z(zHWHo%c^Ez^OX6Pu7Fa0^>5=wwaKM|!htbmoILHiUB|TI{uJLvY4y zJVtA%(?00$$B^MP*Dj1TJ?#R5FL2wb5)P_)30ySG8bVOw>QXe*m}2P2{?3~(5p-T_ zj(UT^pg4i&tV06sI$toBHX`_;2Z%e;n9@fL-}AS=HAeISi+gjnz&l;GV$qTzm>xxs zP4f{$}<_Etg)PKxfmey@k-eRd+@0-!d>u=;M+CD z@LSrL=6XInt9Y=On^lc*V=`PcG6TIQyVNT7(X0lXY<+=V?(}F#8NT)4^pdUc$XMcLXMl@ zU4N`M+9f997u`O~Ii-3%sHca+Nv&EHw!S#dcvaRtuZ}uyrHal&F{~3^pY078sMq4O zT5pn*=bF-$Ji2{=#0bY#th!qsIo&RVfZL+3r@_P41VdT{3N2u>R)txnGa{K4wqAFM zpt3yD4+@1TC#neHM&{6y{9@0qO3m)9N@i+lf5!E4O=#<1#t3_^8P1~6Z1B06V|ng8 zn)e1AT*7b@PB)SMd{Cb(r_(;t*orAs5Ig5Uwv?wjQ5qBlzVPdL3mdvGJy(u(8q>>wujLaZ!r z$_3Ha*;7fTORqsx-%G87~@RehwR@a@&D=ys-0dj3|%%(Zq+y z%1Gy393Cu|oGlfK&CFc#xk*(YvE(>cERXw6D&}uO%8}N@0Ra#{8BMA2AdCWJtPOE> zh%yXd>T8Vq{>VeR#cFOlrn<}rEN{S++HS$QvQ}4}Vw#E4E?W`N37yYHtx8)PkD8+9 z&kla!!mGT;k~J9P+7qG!dqG&6O>l}X;Y5_i4XNc8bBs5&qh`G}+J8r!k8yj%=H?AU z>gQ{&k0VQY(scm`TFdajz(L(?rcfAWV?u;_UCDeC{;(U;f>iKcl?!aUt6duLb$>L(BNhq&nNgS#tvsvzqQaae!o7}gJ3k}!uH{NV;jnR z;mydP$O6EUv^0%`_z~u3YSwa+A9+o;;*X~t(c#c*4ycG1p!{f36DA_1cf7V`7mE3* zO$vmZZ)I(@VDzD3h;&=iVYa{F4Cw5+yrvr)$%PcZ0 zqp2=i%GnpiHX8QO z?n6vnP^b^p{OfVh2^Ix$qBS%DH?!okxly%%&Se051MJEuNlrER?A-G?Y$f6?cwQ z3U$CQp=^I3wqXggqSV(+X;h#)?a3lXG9DAQCxaTTH3JvnhD~>o=KH--DrU9Kn`UK6 z*4nI7j)vV{0}k?BezeE{DhF=R*RX{pFK*74VXuZBe@bk_)W(If3wZO!!?~=tnn)){ zmGgPRYbq*BOo9?ZHwk@YSm?FbD})#=m<}fuE3BRc9A(3CgDY5zW~SL$hKMwpHA>A+ z(4&Y5JG&g2=$R7SSA{rJvXk`ch0cl zdi}=u!JRXx_`#RQ5AK{H#SgwLesJduD1Pwz_`#hsocO_G7oP+vbryJWp0Ri~D;NU- zD@=ziz;T&H3z?oetw+_SLpM2LiVRYhqnsj|tu};w2~x;E#(w08h&HAAveqNAouaaI(!xCIWTvA+ZQPM{ zqF89cSrD0n-090M5xG7&EDPNcInj*KV9cPtzLdPQl=rGsr(4!qO`EI?0cCLNMX?{* z$NjjwEx8=Yzy#%MY&l#MRjY!~qE}+N5NH(iR-2#ohgcEt_ctiC>N*EZ>_=x{^*b2m z`UVL&&6o(Al4CGPnL`NflBNYOg?TW^WMxk{renkyxrq!GBgY>F42auS_SUWZqGI11 zmYKm#*pERC_zW(&)?QmKNkzG}c zx6^(2Zt2}&*N)q#yW53U{NQdx_2g|^XOCNrzi;BoUAMtE!Nd4irY7u!g(s1hR*EDrr->{a54P=2`iQ2kqC zuM$jzel$iTmOxSr6`vu+FzPcn?m>kUr?(hl)^Ib)YyJ!j8KQR zRO`bYO$>AbHXs^I%pksNl_vraMGB@GEk-Ue!I#)x+$-f(SEpSY!G|Mu0Z80k-tf4r zUqfxrZAfqxP@XRYGneIjDD``nHmZc_E2`KFwC1c~7>jI>uX2otFL=H<$$Wohbo0*htmcXu6sPvBpes17|7C>=+<1gD~*H)ZIdx zG@LiV49o6~0eh1vcq4==KTBp8Uq%(qB_RkV2OizHY@`?RNj747rlj3-!XeP&$ZhRl}c zg(Vcp{Q=Pu1fQnoVhIx4v^|k?a(PaU@s1>~qRhfb%ljRf=a=f#Kw>jampi@5|d`q)N-*?KWXJgsYm|Lwv? zKEXJThXL`&6@_W#SP{r}gU=*OSDIs>pe0dVL3KR6RmIrulX_y57}KXLR- zu>1cd7yJKz1MUUHCjj0I&H%Jee(1RQ{{PcDDg4#AJ|yUR zF8BWvOxWJB9UDn7DE-*}|HtvHkylT>JkC7Rr9$qDf4CVgPws_y6PFm?UGb z+oQFo|HJ^2eE&c3o_3OPYJ6fK$)foK-2aacQ6LEh`^=MV9$c;%C7uN!38s?P?fw5G zn{dAthe(2DruaZuKEX0meAFk&GE*!Emt_Co@8WzU*gyFH*BOH(Ck8GLoo&7{b@otP zukSuF@P_!o-6sYv4+h+QV&JiOn3`n6DMloi*S|0J>h4nsUm8ES`&7c~HrUjP556D? zRwP~)d!A%{VS}y9*T!#6uf^wIYnKC=H6yMMLQNxd8VaigE}hx&)FdTry8hsnKr-vo#6y0|6~*xbp?r%r}~ zYF2T=(igu2-OJ(Gv6-NYVTCp)O}ZDkwGQ)ks-tnw4^y z!KX8fT#!CCoe!~rOwlx^mzfd?X9_Qez&}`aquz^ezF2g}k3I@i?E8&JA7yl1z6HHJ zmrDh2{K5Bs|M^3~{fgwy#v27{Ngq{3LOh@%A>n}xWVvO#L+V{IU(cw+N)Nk7kq~O2 zg(#Da$d)$rg$76I4Ja++Bf0DlK`XQ1RlBl)`KIW%fu<+PGBPMDXX7Gy_tE)7z55l( zo!#a~Tak#%2UH|LNwsXXiwdkx8UZ!XY|-SpgL@PSPL#)W30$ABpiw%oTE)JH>n$HP zrX;lhCnQxSh%CC%7PI5_fFM{?c1+D+H!hMNo18!7->*pS987%F6-oC26$#3DkR;$_EY=A_2v>R636qX||sk-)1?rr1EV z%}T{_M4Q^UNWS-7=MQ!7S0r~1<38$&i=_9L=MQ!6S0w9KK0lg@M0j9*NvBrP#}nQs zhfRyJU8dr>_v}ls2BgW7NVY3vZeF7;pD#sW$1N!%gp<8i!5I{!p5#@00bQLg6P*&t zmF3N&q<8)Nq4xcXs%LdhBwLXS4@7b4Ny{=c4mOVuLLG>l1sY_mnH<#_axo{$v z2^Ax&+@mkS97Ach7Gv7TY@{FJsjIaf8-+TD`BDMy!QGk?7kcb3ni#b_m}P6 zFznEOlwp^oU3e9!U~4j6YZ|a0q1d|+^ zD4UGjD6V)TieepVgynJ@Iuy$xx|%T=^LLv*u(hB=)%D7rxbN{o8K(Ucv_wgcZ zb6!5z^XlQ48HM)jskb(RaM?g*L}mzC8@fMj1pxrQUcBi)-INu!KtYdKBa(T2L zl5;Lw;jD~j+-xkCRG6cQMY%QWmqh`$Z;=b{7SXmQw@RC}Hmj_I*5-AS*Iv6AeJ@fs z62#mC8wMbM-_~Z;%U;xw568zVk`2dKFIa0m?ydj#4>U6Vd+7gVpjBIUrT$;$_UF2Y zR1j{jSml2c{lB}lv;pub>i=c(*}_UG^q$>(KQIcDCZc80K`H3V^>KYvAbpuaJnJS> znR$_wnpyq>Yp-{& zqu2M(e&y^fXW`jDJ3GDh>(}0PZF-Hk_RQ1YK7IS?=bdt=&p&z3$@iUn@riu!u7j^R z=l~UfKRN!P(b#qgNi*521sD+hoY(cuMq6IMlvwREFiL5?5eFg7MWah{Y#^eWZms&og-I_+|krzA9O z7U#7QJws?_D1&A5rV6OOlovcVwvhnwoEX8+roNViA_x$5^@Yl4*p4sDYDCF=*;=25@D$o+_kb zq6CR4a*I$7a?8nbAyjIP(}T64!{b1Mm!g$IdGAxWU18PC^Y1OSwhD{!@t4s)uJl;)=*h!3tucAl^yh>f%u9fgH z=@5<$EzxE#!~4BrwY03Jn_8aN1_aZs&L$X9Y-A~Y^s=OYPf7~-#5mifY=H`n06UrnpX&vYgI>!+@L@WU7r$sUi;)0gc8c_MC?kBVLpk@d=lROxPFX%*bTY z-GxpBJq^Z4s5hKcTr#Xcxd z+Y6&RF&aq8_tH*Ip<4^F-^jM)%G5%4zb7%`9f=X&9V3Rc9W|B;neAkKc~SCa{#=+i z9@Z_(6gdt9L}0{D39+EYvy&rYb{jyC>ouLw?Uy?Xb6U(U^MFD}r?G8jRL5dDJm*x|IjT=vYO}2h2->#O(i~G-xGvOZPIvB;ho`Yu2@ofV5yvsYW&7aR zAueX@5$>8@!Jm=B5@%eD05rHWhAd3*hDkW$fZTagV#HS@M!Ydb)Vu(K;c-dz(5R+x zd@0MKl;41@c^YAIkzedq#3o>|#f595#E2*{Vi+TwTGW{kb6-oNj-{6ndsIPyK)v5z z6ziGR%&6E@MO52Z$ezYi3DKH#s7RagR({c#IlacfF_*&Bt7oFDxm5B|sPL0{dJY-J z&QHWP5+Hs&G2%bR2*s0`!XhV^t45E)EtW1e<_IexirTidxu`hXbHY#Q^{Qe-XSkdu`VNTIX0cc*cz6ht zlU)703rxLqWh%RYsh6+zpI^RO&Au_45wnRsn{WltFK|MSVIpf(a-{rV0Yw=w@@*=B} zk>bpqC`%gIG*zC1IZwgu_Jy88OgyJHc7pRJFjZXTF1@j-wf&VF!3c24*7%xa*d3xb z^|ChK)_8TEgJva+38`o}D)8CSBG*^C=M7*FRQ9dyPutku6DoVM3S0-)E-LWNjjdf> zf!8;cZ{(mZ9L;qflj?L>_5{nUh1HB<^CDP}5HOkUn`+0#T|Yl)p4V3HtTO+p8@tnp z+}hZ^A-mjcYDpQ0%np&vJkvs zj8Joc+{oM{dgFDi_{(jztFyg+k>N(fM2+0I$j^Cl%4!}#GmBv_Y85)FOX-FpjS00w zFL82GZ;#Haz@3Y@{ltykxp&-p6&x=B%~88-@q$fr9W)49_B0Jwp(@4p>8z}|*-STG z?i%6w=K!l0aeHH9tM`aoMI*79-L!~l4=uNvq=PxFc1ZU_Dx^%l0=FU-Ea`yde3I;( zzY3VVh})NLZ1Rq{4K0`)2?$t+Kn1PZZ+6sP-B#P^xKST+rc)MWEeB!g9GXFc^H%~> z7jgTNjZLk`ZH=K&t`v1gpqDH)Y$%;gFGgjlAqg5fK@4XJ6kGDj(qd(@d;SVw?;>s= z+1TC_;#TgkHVI7wxSFl_Q=$tgQ9(_HhG9*G`G4)X{pg)^IS&ba1}A%-2vFNw`=x)0Zyq1J=@oxG&7 z95~b6$XXUV>YSUv0t`suOA#D-5HqUp6L);Pu%?xx|RF zL77=;&^b45n|k5Kxx2$uI#1bCx|VB&<0x&rePf`wT-9He2%YZ`w%(OHR>2X2Lg;kQ zbztfuK3=eKuAUGdql}1UB);AmhI83nlyIs*+NkN;Hn_b5WSe?Mpl7NK6PoSrxpv#w z^Eb}d6O0j|#0S>Yp_e5%3ae($7>oL1z*O5ge%2FI6{CUX2Q;5oAP|fyuyzrQ&)Ya_ zR|n(zg`XR8L!7%!FkW9y+Y6@PSC(*-EHm|Hj_3--bY{_u;PH6THSiJeN4f3Kb2rZY z)&8vCcDZ5elWyH>g-y6?H)|L)pOw8Z1Y!-NG?(q4(;W-5a!hoGlR87;lfyoqWAwAF0q8tJ7VZX_NfpD&KUIr#FnxSWGZ1Rq_p}u6B@>u1D zcCIy>%pi`@CmbOb8&Wn5GX-Lr$2iJzX%=OJa~_zw=&K*v*wlJoo#1S1_nZUvF505& z8{2zA^N`?p=<(@yZ<$-~+B$vBY5DZ0Pd{$^*G~TETKeSQpM1%QcJj#a|2qB;$8R{U zoc`6d>hUKZeckoRVcW!^n!K2&w!QuX|?tjyMwEx-rXM6vB@7wp5d(7VRcK=}a`*y#0SKglN zzG&x9c7ABDpgh`_E6d%y6;#k$?y1^d&K3b8?TPA9)h$ljGlyAKW>;xR5?M{@wV&odfXr!SBQm z?i|>}4}Lp-aOdTu_`z?*5AM837C(sjd=k(G9mn}7+L;{0z>`p)ah@0~q9=$3?Y|bQm6mPg8o0Hd^I2ShZ zo6{Z_mhicCSYFunm8Am)5Xr?PrJFo;=UN0 z*qrtDlCUfRv-p?CP9+dn{J~4a<^qd)jM!XYao@j0Y%H*NTR?7eX7PPDwy`-+_}(B! zY|i?<{mYk#jip!jV-7VWff9)j+en~kb4jM0K%wSSV;cz+YA*A!Ibqn=uf#SI5QcqD zjM$v0YwJ&9#OB<9`^cp9k?KTmq%fy${7U67W7>${21g26XtXv5f>WNZlB*x$Mq<`x3Fa@Zq<` zhy=oiZ;uObbJ?AEhcll*c4rwol|Xi<495O1xaaske%0n$E2r@p)daGcr|}up1SFUz z@!8G+pWS49{ z&$I2ZUmBFVB{Zx}6v@Jf=mV|eW(7zxYdIi4?FH45*)S+0 z3=FuFPFE;l`{DRickWfjlba955AL3%{bl^%?n&DJiSvP5x z`gR7n>G6nVyB6%1mW1I|a@+5SUv>Ai?l0m8cdnhqUcEnlaOWac{NRt{2X{{d|0sTN z_eAgynb-AY*ySzu-9<^_59a7_V9%R30|6Hb72z)UX<8@ z1ltaqIPeJ<4kXwzx$%G<`1lJ466|awSyI0Lvh8ag`?$moBv{_JDta}qo7fTQ5q zi5+;rQSht_2NLWIKIkZTW?}~(a1=Zvu>%h{3Ld&}AVIl4=qR|J*ntNe1!oryB>1ck zx)NSX?7#!Agr^q{B-l2-@qmlqw~=)|C+~+5<8H9edGT7 zmalp2FtGy(_BA(g;NZf61e?wex(N0YJMe&uU@x%)54Z?+FC0j)8ULV*U?;Hy54Z@n z6Fcxgi{OI)Z~JeyPUna3-uol)%g25`_67c_e1R|Ny!P2E)|uB=QxA50@kEZR{EF>l zGzJI3gX$n^x{=p!ulR@ z>Am%8z~<99+dZ8%Z^ovOXAOy7;+^ZbFRjUjN^19XgC-HQ)~9Pc8C}5K(=Y1DbxAW> z&-7HvDw2h4y^@4Xr^)7n>ZSs{z^_gOdLhpB;e+S;QC-R&bgq*fdAM_3a&*u2As=>b z?p#0cru)%6*AFT8zw=xdi93#TuGgd9PPxlz^#{$4Xvb8eC>YFj*i}w*KssGfj@4Ds zJ_X@7G}XB2KR(xM#b~)FWi~i0R=L{ePwU;fax@<7=nWUm#+X+xvGzEXVW+s%qiau= z%!cWG*$%U^J)rc&l2KF4o{Ve4)8iFQ(c~k(Vi1UQ5pl=V&SrudNjr#NojBJo8{0UR zJlB`lut_cKu!i0<{PbLx9U+#lRD9^UKB1K$isZTb-yh4jg^keRV)>${h{?MDKM3SI z_|u6rop<1%B!|op7oeZMgh>9P|6$xQsYHI{xW9F)$B@XYEg40qQ6Vyd2qSlZL2 zOh@yso@AgFPH3b~Wk(?3&#CK~sWn@yGgBj?ocP4HY)LN3y1FbiU%$0vG5Fn6HO&oC z@>eH^D6Me}4AGI;q!v!Rqc%iE|6dN#7Bv7+#6q$V>mh=1!bv^2 zEF^>Q4lyeKZ^ofWA|lu3hg}wC(eg^3-$Mkd=yRo0by=E-_2oNiBF6+aY*j}M^SVXp+m4< zz~)DJTX|!-CC&?+iR=Ze7g(d2WyY%v5&cryn?ww;3-Pyc1-6XxDUsp8KNn7HoF=4T zy=_hkh0Vg~^m#1qq^`bpXjCR2N*rtt26A-GWJiS`cNmTa12RLMhU)S>IkAZvPWm`v}l zHeKIIjfrG^r~g=A`lMFgd>(l3^~YDShOQ@_tl|0zkZy0fo~Jb?ko8plSWj9j45rAAw(=Ads_cO;fel?#Dp|)nVGg}Z(sWJ7 zHO7%OJz(79tvoJ;YC7;f=g@V;$vUojn@n%kHC@M(8)JnOHmW0=yd1UiJkwy$L*Ry? zt4P-ErcscFny%sa#>r$W9x%l-CTVl_uu|gelDP7Pv^ednmpoc?)!j{mvhl{`<+LL% zHG3loi%4eM---iM{E#<8V5BcwO;&Pw<4Bb}_@2du(;6p{l{{c(qEBk!5iV58!^})W z-l&8Tf7N8NzDvtTs_((~DT(@yIi$WVJnwYm`VQH?P~RnE$ogKDI#PWPY2QhW(PVuO zm`Q1qT6j1J)%SpzbjWsv$!XEoWIY$pI8r?iY1c`O6A!6p3s2A-TF)Wd6Y6={Lb9Hh z8;(@ZL)!Dy#tCFSe=>=-@R$jz=T9clAzKtC(Tg7>YkKMRN2=)|EqZEW)FCx(;c<|| z)pW=S4kF059-H~c~NSlspj3jINlj**N$2L$+(R4p#Yr=G2g9Yh%N37!^tvRl7 z{2_I0;mMAl)^W&oggTb*BinJo5tqw{wByN*;|{(gweUm>s^Wn58Tw&QvTm6d$TYv^ zNIiW>E1ujKac~V=@B}}t;gF3;)-ClF)bqt7)$ouu9N!pD_VWQ3O=c^%cTpAhFXy8^ zwV&8Vo3ln;xRSMo`wt420?YY1E67UD;~c4y2fjfK`=V)$W64S$aM5J6a)S$1@-P?8 zA;0?o7kO6~Szq7N&DNJg?BgZ)atdu2^)+glIuidQzFfFPc)3t7w#PGJ6;MZnl}tXT8o^#tJaMVm`sVh-o3-`G1K~ zXN;!5L%)k2p`S*3o^~b8jTi9_oPs@xU4%`fd`Q`N;4K77JDmu^fseQ9)v;{VoJ*_w z_4$gfZgBjxS11(fnxEwR<^TMTYXKddh+u+`w*+k7y2_ff*=&~ntTW(s|MXN4{d7S8 z^zJ-WF^f*b&>%`N8G{yEJ?YZs&6RkqQaL2Vi}qabEM8|6O<*nc}Bg zeuU(I`}Jr#SWA=BA+_^vpxK{+oDHfWQX z|6PN&A2bF;BF?ieVu<4fau!Sr6?a z&hvwP)MOC@eZ+ZYaJcmLr`?HoUr)krsTQOCvBcr~X#a4bpSr(yo6C7%@6spl@7?Ay z|GVCmiVj|?(Z}rXisp*{u`B2&@9&D{vL4zM^s)Q9qPZdlx`IA>e}k%1ey=lb4SKUC zt0n1CAHFLNY*75_{SDe&=6~0q#P=@``ic0J`x~^m;(u&VeA)g6Z7%Df4T@j4zd@TT zVxU3sE6%19akI!V-ek6(Og0lIBK>y5A|1b`#UDHH$ua!K!39Ke!76bZx}oq zBAvltrn6L0)tvSIiZ>ZQsBh3RDmITj*JbdP zB8F7D$M36|48>HhGh4A~b(Nqbl{6V5sZd;{G--7fqRe_QO&qUL6JHea)dJ+dYei9~ zL0<|bA^z^9t#;_<*?M*-l>gNY%2H}68ITL zmpp9nMjdW-EGV(3!crUgvuZX!{4Anud7{fr)PV??YuD+Mez&M**Q@nC^_(oC&%&`? z8RA%pJQgwfYF%z^)nRoh%W-`)>q}W9#ZuW;aT1T6>lHOgLamn*@pqL`t3>Ruo1 z^e7~jLegXKYP>$9(NHle)BZ|#LK`=RwSIT9+u;fy*vgHeTKT|WALRYSpH(X-j#4Y@ zO|mYDxnNBeHO_>-ZWo)|d>+wZT3OqtNGphEoLtF}zBAgT2-+1Mi&0wf7kb>5?z+V- zt=6+fRa#ayRt`9q7Y)_Q2T%S#t5)_LrB*h$iXN%D-s6vWT*+M0X^1$y$>?EPS)J+b z%<7%ul+Ko@cSgJhb-koac@rw7(rM93ym5b5vD=y~YwVQ+&*e)m>S4@j^o+%hFfSjx zto$!)K0O1M1$jA?CLXyX zrhiW&R_)TdoNm8T=kex}LBCGa1D6nUIxI>F;#=c;+P2sl& zyoJJn)5S$Yweo?h@1I*M+mF_yY_NCPMCM?ZTU2$Y#Wt5-E$`icHKUGJ zeR@fct>Q6uSqtWfy~`bxdGe{UsVC#mm27!ysh~PwQoeAgRz7&w^j|h#w7!fJ+mF_{ ztk-KC-blvdGZ%vXSiw~H8G};d>3d?ychI>U=uWm(+9btl+*LCc6j^D(*IDwa#F?-@ zE7MyHv23YRT1xt@q61s`f{VhiSU%#U{4-uGi;vo3S*Lb5bxN&0DNmZja=+hatM+&a z4RlB=Yr2UL^AS%>t5EdFlKE&=pCN*y`6KC`Y=5FRpQ&Y1Ig#D3mnk#{%oyhn)yO~N zRh-y%v?dIrv)}3wXM>4c-ICPX?EYdQluRC`kyX}yXWSiB5}(`FXTx>kJr-@dSDsIl z%#oDOTlU1W(Lkkas=B(o2dpp78>*3i#)~+y?Wir{oRUboD`OESm7Nls*yD**?J31! z8d;;Y>h-0vU0u$X`?Vn(@r`8SiS~|OO)Vp9*Yzn2ZJMyBE9j3HD+ip(=aSF=b16}Z z;5z>4+~06KtdE%IF^1FDQP0GFPyF@K`g2kA!V_5dyzz}NQR3}KSTITtz5+NjPL;;# zusBseqiaF^s9g)RxwyHn2IA#OpIlMN5=}j7e^cLfq^3UjGGJA8c6F(}e$CISsgK$P z!H{xaSM4ri%+Q-U?7rUBDYcuF9TH>E8TW@gzHXJ@@9CAc=ku|u#x1fM zIy}-u)L`%Ej*?A1a(`2=K2lR3JWshhJB?kEe!iSl$v_(@oW%5)Vd3S%*?+N*=iq5QB(N_y~7y=z_c9B6$d`i+1Rh5a|7tz$m z?{Df`kJQu$Uw6%pPOn9+v$;G!tEN6`SKx@qh_$t z*Bx^d>H%4?81{HeE@f7$_j=56Q)jZ%ly9%ex;lMDbIM6J^>O=~`j#U!wdBA+z2u(A zp*I**9;=aP?Vr!>*IMV%x(@5@6}vZE?@h^dUGc88-<``kwTGLlHD-l8)2UO}l9`aX z=!wU)s;*wQ&e0k1by=-OO}V1d)pSW!x!SKHn|j3lroQK=bV((84lJQ?rd&T6eTmMc1x(VDI7(6*@p?P8}rE-KfP z=BTUR-_xb&Rr}LAmoC;0O+9>nQ?EKgQ%epE4NacaTCK_F2>RS!)6b}>Yxbih6%4*M zt3RrAie05}TAy_X>^^hia8iL-T^2)~$v(GVM0^(A->1yPHJ-kt*jv`=Yh5Of%b3pl z4dJL=T7=8uvHP2P<&m2Dz~^_!ruL|GCZmHGB*C9kQy;aPSUPdmLG0cgftb#du**Xd zSAW-GS0Gwf+MQDvrFmVSyFY0QoAsWssLi1%IIKZ!x=&KGxB^`@u_a+V-{E5m)k?oOCMhV5_a8;{h~2j42R2Yg*!!Js{$ z|5-Kl(OQ9Mla(H?+2Rt3MLnKKFL4XJKOC4*1oH_$0(^GjedYY2gjYp`-C1G}|00Y+d`|W}6n? zi-Xywg_jWzKijlgKnt_Ykgw`BH`^S1KatFJV9>~+XPZMx_+_^FZ=P+$N(DI#4m;a; zIu)6y)}-$6OM8kjy(FltOzrQT?#WEg6&1xuT5cn@+YXb&U;57j2fC_yeb5%_(_p3hGQkaRPX4`SZ!Sqxl3xQ1bd0fbQcTMA9u*5bsZj+QbqFOgpeD=q}|`L;;ynDn(v<1~1=a@UI~*-%T%$h@$?(aDXym z_arB>A9moG#Zr0Ilrv9_R}J}Idp1-K*LsY;v^X0J+H~=n#cD1G&2^J>s_s9&%vWtC ziLowKmb{LbBdm@~lj*qLEp@bu4RV>>YKXXUu6$p=M4?uydhNBonj#ajt1@w`TVAb% zL*hU((_@(G?uivtd7Vupc4^%@ZF84-@>d@ic*hM6ycRBYN9{5%JKVr)KB)6zdDNl zXQ6DP2wxPg5JrR|!MB3#g5`qwf@%CO`P=wQ`E&VGc>8!;cuRNz-X!iG?i1WaTn~2~ z=TpvRPJ`p%jA8F)Kg7O(ZDo&Q?P6_YEo2#4Bbe_oH!!PAEpr&-ZN@rAkujUWqrX94 zOV80~(OI-tX{%`|nu127?x3!s#;7tX1%DA=fk$u=_ARy@TaL}grcu5ms@2pVaig2s zi%)7R1c|qd3l)1vX--ANNt=}LkJVVhq)n-PmVT8g6?Wxp+IpnjhMysBoI~d6HA^h{ zP^wQ#v|m^+O8V5qU8%ZXR*~xsR;|n{O%|nzP*f?%1RH92%>=CUhAnqyl+a&!;iwK`A zZUo3Y9oC$w98&rGX5!riXE`3P`IMrtyWDB&_PZlQpek8dQ}u+cKAi$TRow8CdFs8@ zE_GH`R+SQ(TD~OqC**BCDU&$X+nEh3GN!CW>+!|XqO3fLpCWGf$UJVTCuFa-$3rqn zIHhuS=p>4oJ&-P{ti7FeX*_2sw7X#NPACbT!c>)H#}sX_7=W>Jwe=XlX<$cZ9cccmFcynoV^~c zO5I*D_Lbv-PK_&>2pJ27Y^mf@WZhm}6dx~cxX3(~iaMGz`0bjAG-8nF%ft@dtu~6% z5pSO@WADjURiSKH)}JW5g7`Rb!%60Glzb|;QWowpstw6>Iuh4b3aNNRN(76ydG%_U zq$Dn?O_3hC#fgs@L=p?X|zD&z|WZzLXZb!c?yjJ7VTJNly@uQTVxPZBrm zP`BQwU0)JsYWl8h#p6gOP0r4k%u}f4g6$T!TOIP$yZp6kq)#Hk$A}v?GEZBNq)pUT zN<{KKDxW#pZ7_E9#YJttdUsDu)z{t|Yxl)fd95?93F4#04J(;PZgPj~>3nB@U#c2Y zbSv6rzGPnJ*N1IpwJRylO54@)kgrat6CHk{xM3ml=wt~|dnhdr$CBB4)Yw~#7;@E0 zT2oN@E&e`@+}b8jC`A^PwkL|8Aa0n+JW@$Pmh6-JYu=<$lWQ-k{fVe7>8_cg(rm#m z4_j28e6FqPs03B`C~?C?=Fz)5yhXpUSJ7k2i)2PipWZKV))Fdl%^U0>J|EzbMw0#N zjJj8E!bgf5Mlw%Wq0A_aMol~MdF5_ZM=7uC3Rg@)U8pVSYENm5MP;sRFS?02OotyY zZWzcsW_M3g5)&63^Db@L2 zSFMM54klI=i4u`wpFEr}SrZXeMeK1q`w9seK3v?;l6kU0na&rDnk}04Zl771N{AwM zl|dnQ8e9&2uThq8rNxo1NLABe!;cj=G-RHXEY&CPmnUt0Rjj`%QneXnM9pj+)jAQ! zJu9y}dfg#KrMqTz>F{I34KNEeJ)2dqjK4N z^@^)#)R~P6RoNOaWwPyw{+>|KpuoA}#w;?ABa=$z#O-yxI9cz?=q!GRCM|V~`dvPQ zw7s*()-5Bh8i{H~Sxw>`abqT#CsQvH>wRe@6;`yld~H!tn=@wXi`AWdzCOFB7V!kz zI(7a`wwes$Y;j`-nJ3W|szt29L}ytgljdSZg;UiV33tWp{Z5%Ir8D$V2JU2^-E7Hx!UZ z;^;4{J8}kNU#Y9FtPi*>3F2mD-WjzR+l*C(%+uT1Woq+hBQl&JZgh}&(wnuZL@r6MD$*b5G)j{>t4i4|3fhT}O?#}hu1>kpn^hAJ zGR4~>nLx=)=8=+lGQ{gBW~;&J^}FrGv`Zy&)-|4r#?hwD1Oo+4M5$DF#5Fl0e58!b zBO&utD%wgZP$`)^ToJ;PrHsbBzn~6w%Cj|5UZse(8;Y82zh9q7W^t;x0bjq?7dzr% zd2c0TDn@FM+aV%k(WE)7_sZi}lb6IIkIV@{v%Cng|ip(e{X zvG2rPEN_)P|7xY}+aK9HV_=6!*ZFO^Z0^Kvn<-NwEZH%=$> z$V3TozUoP;Izxt>HE#3!dUD+oZ@^-&D39DP*1&f^%bEi5rv2JS{}Z zCi9$1=4l~pHJRrWGEd%^56g;5OVy$)o7{1&!C320<@-8fu3kN{`sgfbT`@ylqbr4? zWS&W6o))4yV_%9J6UjVIw@y_FMEu%vN@4Ab=z^+XAF(*g98-vjR|C)76Kh({}MOGlX+VB@;UaoxG|2*)56Efv3=skSTauwpZCW0iW?`Bd0P0) zH}+3)<0LXq3vp_(J>td~GEWO(W3hjT8>7iQErf0*f6j?yo)*HVVt*GmP9XEN5Xuz$ zOxzen=4nBEV}BDjMv{42&{x=};>Pi0o)$uFVxNc`$B}tjxPOLyO#1(6l!qw7yM%VZ z=YmxN4gX{QHGBo{UEZa6ao=QugeNcOMUy=)$96RV4bG1oCW8UJG3%+S(5 zpt6V3lx>`^aTXNZ!g6n*;5+tQ5uEYpJAdha@9Qgv zZTMlCdEArYzs;syxJHwC7JvRPPx%^WLcuMp2?q+kvGBp-vo>Ayx$`Xc7VXo8-;Op6 zyXIT`pMRK8pK;ZlE9Yl@jag7|3k%1Ag3qc&?mB1F$e^^ZpA*<~=bze6*Et{E{o#@y zuKG+rEBeI=zQ#-_xP{f_K*6`pRPE#Mx$fn&blt0W?iw%tvY`6BTR!V%+tpVc6W{%e z-q)A`1-GyS9Vqz9>;GANWa5lHig#8R{y{mp^h{6tTE+WL#@(We#@znZM?qgh2?e*X zP8}%N`QEwbPkvzUvzD9F|D5w{<)7AGHEi*;2~GdB?7qm6@u>FZ*)g!asLh6}kHQ+urdtI-uYdR=fiR-#FvEz308r zQ=NEBikXd^$&Ma(C;vS87WLzM zx}AUf{p|B}KYTaY*N{QMEiAAH3V!9JTd+Ugpg4Ee+Kp%apy#is?wo$AeOGA;ZSV5M ze~q5=7hgjP1-GyYA1HX$lE9<~mO3L>J`$Pz3h${-%GF;g-#BN>&0p+Tu;}#Cv7h-G z5-7NZrTRd@TPb@UTX@Fghvre1oie8W`o`l0A1--lgK*sLr26(dF1gC=YlxxX7S{6v z1t0&;`-K}erWVilJv!{d9oehSaXn{$vKG1M*5!Y%<*?V*`5Gc9xP?XkK*6`YyRv*) zbZ_ici{UZ$-|LspyU%>;8Rz9+y#4jxp7Gp@tA6k`rbEFk>Fn<$&grKRZF~k4+`{%@px{c-@K%N7ktDXcdz#|U+~*s|GKoe=4+e|1-G!J z87TMz?U91G$^=*ea}F_XTG0% zGW^I6+sB`z%ICjy;?>*dkKg;E_j~4Mz43PKrW4or8dIR)7B)u%1^0gc=y@wH_~7pC zcV(EC9h$YLZTLEpshw_?IAlg=8s^;TcwR4BNGZPq}+=YMxh&nJ?X+s0n=-nEbEp8fjy&;EMT z`P&`OmT%#VyK3VLPx~6DK*25S#RdxAP=EXVcg@>wn?8QZ#iJJ9bN!>=Tr%gVSyR%1J{>Q!83W0|m$W z7fw3yg9rO=ra!&!oAV#sxM`JQ+Y^u9G3w>V_WgzT`nZ4i8snhg7B-Os1)p{7$l;a` z|M1B``BzuUES@`&HOYFU2S z$`8+Kd(QdB^E)s0HO4@}Eo^KD3QjQ?*L-CDWB$89_Wid)6F=JhtJ4L4w9M%G{QRk& zSNCoE(AO9Z1-G#K9VmFqY7zVNYmUb{&4nw=XW#YOU2*fg=w-{U8Fy_-_0sEs#lFUg zP;d)drW2PPR(wQpn+aLQHCqTh1?4t(? z{^lD06MdV{TbcZ-a+j-d^5n0iG}-eF`OYO@`u=ut-)GnR8l#}#7B<@h1#b$^e&hX> zm)_HV=BF33&e_)e^f~j&h5X3(uQM%s>mSeF5%4ueLcuNU$Oj4*EW6^y)yFP#9?KKW!fE)(}x3IS#D0tq?HJQmDRRVt~t(HBDzklb_dC$Cj`8y{w z^q-&j$OD~A=aBLLZ1{x~;ZEV5!kW+~94FW-ct&ueAT5|J7|#EY{}_J>znd@RGk9bRpgpKyN7xrP(rbZ|KAci0cG8*D%Obk=vQS6FLV6_%NG z5_1o8EAx70oH>hl8sG!QBaF)#a~abaIQ@0{-Sma@PWlAe=d|s#m9z{^MH@l=EA?^e zRn#8hnE)p7ErR=qs{Qike{u^$b0lQGd_JE$>umV#a0<=3oZQ z(Coo%OpocCJ(z{*FkQ0;GchfuZT4UWrolAL9!$s7n7Y}6X_yLAHG41>n~lwG_Fx=4 z8#{adsWC)%V`pJ!4Hg`u!Yr0Tagoq3C#_Fx)k0%t80U=Rj2rC0Avbc2egCO3R5hsdQ|S9UaHxZE`ULuf!NwTsVES14{>JFc##1?M!fNhISo+)! z)4}2HS+tvJHxK@PLmkYdt)i`J_Fx9F2wd6x!8g%vYW83{?MB*-%^pmnt)Q)F_FyXQ z2HFkH9*onjr(HkTCqp$I?K;|Zg9U4KszAO!;EdNxF^yO6ICO^D%-P)R!EDZ>oJX5I zn8kU7^GLG?GdT})9&Ywv2InEpL(Lvc=WOC^YW83n=RwYc%^pnUJivLN*@JP;M$X2; z4ji&m?&sW(3U=14-e{Eg08hqi^{P4xy$5$7lYSfhwq_4z&{xw}H+wLhej|PG)E}}M zH2Mnq;Hf|4!BqMU^ubeq$b)hE_4L70f5?OB*U<-0eP^}5?#$}crmk4JZZsM4se|Tb zI`2Z>g@cVT)WI~~1-uKIJ(x=DcF%A2V4Qaz?>uy73|VL1xx8}+3(lmid9|wAYbr*J zg`TSa;7>Erxk+xa*@J1^1UJ#_!BlRX8~+J#1;S~yGCE))q*7fuqMEF2{~R>%`F zgcQNof_;L|1iJUa(E@Yr$s0gMtl$I|Zu+HxaQ0t`aN~TqIa1C<}6e zgdid~M-ULW1Xh8bcmiRDKqfdtFhwvyFh+2^V3>d-pz*&aA`|?R|0(fo!h8Hb@?Yiu zj=!D%6#ohSBm9m0d-!YlxA0f+uO*&ZxRieZzsAq=ll%yO9^c3B9Cf!inpAIdeGo4M&f?0r)3}qkW4I%@JT8s%EoUDQO<@=3ZO*Hl7dhKFPY^do zH*nT+R&kbdmJl%(7IKQ56!C<`T#kohs!`7)~7`5hPPR-vR-6uV?Duoh_!)udSex9Ico{4K}2*YvQn%FYc9*fva+K*=nrd|tC-7~OPCGjLS~VfVn&#A znI5Kj$rbbG{(1#eT+{TyNE~;uQFa_Y-2pZc!;rqv6iulv7E7l z(O@hj;zy(y5#p&455vmPGG;MkjA@KXj4_N63?73<|CYXw{waMI{cZZI^cU&d=ugle zqHiE#O{}6Xr!S#5h-XZS^b|cppG)`9t#mCBfkH-~MxR6FTG}j{j5duni8h8dg2p3a zSbR&}NBxw#i~2V8RqBh>ZPX{I4^cNz*AkH}mQ$Bd8^k*?MQUo`P7Jo<|ARX+|LdyA zq~bWX2|E|~AhrlKUN3cht+`hVpZSf3kA;EP zVIkmhtOvLZn-9De>jqwfodaBo%>!PI%>`bC%>gdKx`0<;LBs&?a?B50g!zD%VP4>+ zmLGq8@CfHlkrtYQXW1=9n| zm=0LNw7?>!0p>9^Fo&rSX9Kg?*}x2T7BG#S2~1(LfJtm7FoDeg#xNx?iYb77SO>5d zYX?R!IWUaLfFVo@?7<|!`Is2kjfsHsu<5|L*cre%SR1eln+6PGQ-J~Obf6DA4d}(D z06o}bpc^|C=)z6`Iwy2U@XlKnpe&XvR(k8nKgrdTb2hXrK-|5vavZ z0BW#NKs7cJI2$`2cs6z%@GNWu@Jwtta29qfa3*#Pa0WIEsKkW8c1(cC2g)%XP=;}V zQj7zXU~HfmV*y1N6F41X0NXG+a2iGfPQ|Fe(=i-)8ioO;keV9{`z@_kj$` zpMf;Wdq67XT_8?*2Z&Mr1f)>jCQ12$@)qzr${&H>Qr-l9LwN)EHRTV$uPCnrzofhd z{DSf-@N>#e;6BRlfqN;h0RKtZ0sIH$W#Asl?|^@&yafD=@*?nWlox=XQl1BXLU|7O z5oJ4YH|1I2Un$Q3KcqYj{0n6pa2I7O@B_+I!1pP?1^$_`1^6!IH^6r&zXtw^@+9zW z$`io1D31gGNck1;P0C}yHz=EduTvfczD9Wj_yXl&;O{99A#MV`LU|ClgYp3IWy(h2 z?3Gl-0n;DYpWDMY#pInQ}AmVah7Rm54V1AEMj{+(cOce2{Vj@Bzy8 zz>SpafcI0D1Mj6Q18$&P3tUgR26zu;De!K})xdR>tAKY=t^}^7EJ0ihTtm46csu2C z;BAyez}1w?fVWaE1>Qor1h|UQ0IsB547`bQ5%5OJg}@b*3m8-!qg;PJ;JWhw%g+TY zTL`#z0bpqzaCHrERTXe$1+b(HSX=^JUIZ*E04~b|F6{?gk^@|v1zeN?T$l!2kfKvD z?7SqIo|_=kg>f=n5F^uCluWCAWLoJZ({hANOJOoChRC$gL#FxpWSZ+H)9g89nwdwY z>A7T@oI|FGE;5Y=$ut%q(>_0$_WH;);w96thfI6iWIEqPrrl05J;y<&b34g&j-5=q zY-Ad=lBwT9ram*7dQD{NF_NjvK&DPTnL2c2+NmW|yM|0{YBIH|$kZ~MOwDJLsp%{- zHJ(YPhFN5)pGl^=8Dy$elBrrjrm7Ayo!w5RXUoa-EE$>3l9K6637O6ilc`cfrXADC zwEYY+mA8?pY#NzLr;@4UbTSp6My8@EWO~MAGHpAROsAbfrc)=8>1h+mbjk!WojjgQ zPaQ|5lg5(i#FNQ%!bxO0ehit89ZjYupGc-Boj|5zMv>|0kz{(p@nkycI5HhMf=rJe zPNv5lOQyq*A=6`rk?Ao)G94x$Qz4&B1w1n4amkd+AyW>UOxY|lWiiQ=!5~vQolI#o zGNn?<6vwGlHk-rSf%Q^nQ-og^zbANu*yV2*Y$5je8wKl#9sUZz zQeuC9fuKt4?qlo~?4|5QM6|#vJI9W(=d%NB2iwS=%~r5Q>?!PV>{0AtY!;is`jWMW zwVU-G>kZZp)^^qw)@CBI;5yc7)(X~A)*{vgtST$Vim~Ri0xSm+Z*VqC!4k2iu*R`Q zv4*i&EDG~W;;n<-L=?g|m^+x;nOm5fnH!0i1*@4Wm`j<9m=_SS33JRCb3QY`bTEy~ z*-Qmf#GJw$#~j5R#$+)mj4v5`7`qwoG2US8U~Fe>VQgk>B;pvZW~^W=Wh`P`z^F2E zj2L4+BfxMlj6`%p1w+J`!WhRG#Tdq5F(~ve>3itA>F*IS4|mYF)3?w!(>K!B5s&Sx zpf9B_qF+F-5|I#N^!fAv-9b0fXVVpQ5%DD8IQl61FglA)p?yi)L)%S6Rd|E8gLpS$ z3vDxPBW)cKHE{)PDQyw$0^;%S94$thPYci-G$U;`5lc}-n?f5$8$}yNW6>zoFR6Q| zyQ%L{-yk9^Zl`XcZl-ReuA{D|uAnZZE}~vQ#9Yi#tJL|_0M$V?QfKqF6A>Oa^EUF< z@mBLz@Rsrx5f92#c{yH;H=l_6=-?T7vv~@hh&P2djyH-ojK|_ph_^uYaCdXx$X&->&0WD=O2mk~fLrC}xH0a0Zh-6H8o9H%3a*Gdg*%QriaU(U;!-$Y za`td`bKc{;!P&vt&e=l5libKz$63u;!CA^##JPY|C8AEmIP*CHBFdzZGn=E}h=>Pi z$8kn+hH+RN3j0g;9wPSSd+ayZJJ{RVTiBb~8;PfKR}---0|JM@NZdVA2t>raqj7>! z#GNCSfI{3i+QZ*X+%8rrPc* z4frBn1-^t=fWO1bz!&fm@Oiuld=4)FpT+aQXYhXD(|8WJ4bK9f!ZX0%;%VR(JO%s> zo&^3HPXHgsM>+vA)9y|cN8}|d(;XdFUxEIj_T#LJbYj79vcH9ZP4R-)nF2{AiWw;i2Ev^AxgR6l{aTV|? zd^Ye({A}P7{4C&N{7m2#_$=V%_)OrX_zXlP@Df}BY~UThi}7~gMYtSzAua=+hf5J9 zz;kgia3L-NF2JV)>-ZVK8r}vh!$3_lszho1zD;A0R+1H<@LnoR5zLcH_qb&%uua z&cjCl=ibAV2q4RqivU?DSns6Lw#4(@&rvUZX4?rFEJy45%2h?ES0%v32Abt%z8~X})7WO6ZOzaEb zEbL#vnb_yR8Q4Cc659(@VE+VmVE+Khusw)>2THKd5dQ`gW1j*=*eAg0*vG&#u#bRk z*lyr7?61J7*oVN=u)hE&W4jPP0G^7y4?G3?GjJ029&jS|E^q?&4sbm7C*U~jZQ#k+ zTfmdBKLW>KZvsbSZvaok{s0_>y$&3Sy#_oUdlh&bwi7r4`#o?t_6qP=YzOcd>}B9E z>~}x`_7aefy$IxCF95mN^FR*v9FUD|2ePndfeh>!ART)eNW-=Psn}K^jy(m$u-^hH z*cOu557=*j-($Z9euq5?{1$rx_zm_r@N4W>z%Q}KfL~ynf&aoD1%8e_0^El^4BU%7 z1pFtq3HW#HL6TGp4*fwj;m{vcBM$vRHQ>-6R6P#;LDk{VA5<+4{Xx~>&>vJa4*fx$ zjYEG>&&HuYsAuBP9|WO4sIzeB59&-D`hz+HhyI`{ap(_f2M+x~ZO5TMsB#?ogDS(J zKd4e1`hzOLp+Bf%9QuPQ!l6H?XW-Bu)HWRYgE|d|{-939p+Bgnz&3aOe-}7##Y8IvR)mpq_|B ze^5u^&>z&1IP?egcpUnJdK?b@K|KbC{vZhbL2#fyhT#uDzXI85_zK`Z@f(2uz^@1H!LI}U9bXRo1YZXH z7{3P}Uk>~;ei`sRd=ckshQYYO zUd3SCU^_7wH`wnn7&q7}7>pb2Wemm*_B#y54fYZS;|6;XgK>jBkHNUXp2J|=5E1TS z++fdQFmAA?F&H=4HVnoMwiSbMgFS`8xWRsl!MMS;U@&fo*m^KtuwP>^ey}Go7&q7x z7>pb2R~U>N>@f_+4YnDBaf3aI!MMR5!C>5A4`b&O&;JwtpGpyaBm7+Wci~6E4}^ab z{z3SP@CD)1!rurV6K)dTOT-4eO}J8co$zYm<-&`F=L##reqmDBE9@2qg>E8dfI+Ad z&J@ap(}kxACkjszjuajv(30@XFC)g@@Qt+tY0l|8~ z9fDhlI0MTBR|+l@Tqsx|C<(HHxF9T;C-4iL0*gQ=I9s3;NCj1&0oX6nSTQj!EiDE68`zbj;^1d z;D?E?7hO}zEI zHN2I)WxU0_i+Kxp1zwUD=FQ=`c^00AHUCh0hh@e;?-r)#y=WyLz3s=LP$(3@aawl>}bBA-eTq@@q z&R!yl!UvqUI6FBnaJF(DC*E~h&soD+$yvr(%(F0hmAFnbQ$ z&9<;L?3rvSdn$V(do+7Eo6DxMzG3ZUeZu;H^%iR<>jl)3TV(w(VK*V}@oVkg) zp1Fp(lDUkzn0YaC0kgnNGQ-R{OgGcQ)G%iJ zniJ`x>BH&7oio}uw7s-XXdlqtqV1%;K-)@toVJO!p0}|2H80zaHuT zdyxLW8|nXbNdMo3^#2`5|F1>*e+|Hk}h{=XUN|5Zr;--Pu4 zjY$8mz#{Pb+<^4|^+^9;hx9-3X%+bQWk~;Di}e3BNdGTI`u}RA|F1;)e+kn6i?IOY zzXIw1%aQ(Hg!KPqNdI4o^#3JD|2L5SzZmKNi;(`m0O|kpk^VmqvqJgjBK^M*>Hh^t z|JRZJuOa3<8-|0bmWjY$6+u+flDkMzF| z>3=QK{~DzKRY?EOM*9D3r2o%C`u|L%|7RinKNIQy8A$&tkpAz$`0(@Ek^Yw>{VzlM zUyAg<1nGY<(*Gi)|Ia}BzYXdCX-NN1Mf(4Ar2kJt`hN=2|C5paKLzRkNtAD)9utxN zpMdoLc*Hp)A{y&cL8RQ>< z^#5?A|Bprb{}`nIhavqhMEakP^gj>je=gGh9Hjr*NdL2t{%0cnPe=NnhV(xb>3Hl|-{{Iuw|8FDx{}$5!e?&0Ed$E$k^X-L z>Hi%_|G$j%|L>6ge+lXT7m)sc9_jz*kpAC}^#8L+|38iN|2Cxmw<7)j6w?2{Mf!gW z(*M6k`u|C!|DQnm|8b=MA4B?oGt&Q$Qef*td4vL6AIif>|38HE|0bmWA4K~90i^#o zBK?0K(*O4&{l5X}|Mf`!--Go3-AMnhL;C+tr2p@rEGEYVWi8VGYmolG9qIqukp5qd z^#83$|KE)C|0<;aS0ep?6Vm@TBK?2Efd0RJK>uGip#PT-=>KH{`u~~%{l9cT|6e_z z|F0U*|5py^|0M(Z|B3y+HPHW6(EkK`3|L21Kp9A{83-o`$Le}38`rilo z-wXQR4f@{&`rirq-vRo+6ZF3w^uG=CzZLYq1@yle^uG!8zY+Am0rbBf^uG@DzXtTb z8uY&k^#5$o|7U~#KNIx-EYSZmLI2MH{jUK1-vRo+9rV8(^uKH>S$`?$e+lS+G3fv4 zp#RSR{oe-qe;Vlj(?S2A2Ks*r=>N%}|4#w^KMC~zM9}{eK>v>e{XZ7;|H+{LPXhfv z2K4`lp#M(*{XYuy|47jP$AkVK0s4P9=>KCu{~rVTe;DY0A?SZTm#jYz^gkE$KL_+b z8}vUD^gjdiKOOWx4fH=1r%^FB{)_+rU+Mp=g#VBI;{T75|9>6%|JRWJe--)voyhi~RpH$p1f${Qp+u|DQts|F_8hZ$bY5 zH^~1#iTwW)$p1f%N6CvT{ww7FA4C3sGxGnB;$isrN09%282SH)kpJI={QrZ<|385I z|NY4S--rDFy~zJ>K>mL{^8fcB|9>~~|92t(e<$vNpL+-L|7(%|UxWPr?a2S%hW!6( z?C(&n4EcX4_BZ(dlK;cryFfW^oo9ko)m_D^e%i8ZNtP_BTP0ai zyQ;tk_>fCw1AG7k2tGlAq~p*a9s~#i1o0q9%95tjHIB1(yg5l`H=E4PZ02N7X0o%J z+1-qHva^|FGf7U8O`JWM?BuEj9m{7`r%&}QkpK7p z_wnD?cX5B8{}228KjicOM?U|5==1*vKL7t7pZ|Z>=l`Ga`TwUkf6v2vx6l7S<@5hf z`uzXC&;LK+^Z!?U{{M>4|3BvQ|Bw3o|GRwt{}G@6f7s{$AM*MC2Yvqk0iXYWhtL1N z-RJ-B_xb;~`TYNVKL7t#pZ~w?^Z%E8{{N!S|6lO=|MNcof6nLs_k8|8Bh z_WA!KpZ`Df`Tql-|9_9q|6lX@|7U#u|7oB9f49&7KjriPPx}1-6F&cc)#v}O`27Fl zKL7uy&;P&6=l>t^`TvJ~{{JDL|9{Zu{~z%A|9ANO|NTDy|2Cihzt89Y-|F-KZ}Iv6 z%Rc{q$>;ws`uzU|pZ`DS^Z$E3|G(?={~@3M-|_kXZJ+-?>+}C-eExrP&i{ib*&dGx z&H;El=GI^P{QqD1{Qp;d{{Jt0{{JgJ|Nmv5|NnEJ|NoND|9{cv|No=U|NoiK|Np7a z|9`>f|3B~Z|Nq|S|39}i=l?(J^Z%dm`Tsxh`Tsxm`Tsxi`Tsxk`Tsxg`TyVd`TyVZ zdH+xQ=Ky}!=l_4l=l_4(=l_4p=l_4x=l_4h=l_4*=l_4r=l_58P5ysDYP`|@cgK^> zFZlfbfAIPL&-?uUzxVn7&-wiSXMO(vGd}T zJG=Q$H!i%q_gy>h1c^6)-moR`(f;YkSI9!8uGdItxyQqB>^+ZPH7!G6)y~l11w+<);5i(FUwq*zYg(Zf@dGN zx=sYqs{nVMD5Y0F2n2LP7%wSYBlwI5U`=%an0i$`G=@Xu7D|MpiErE_p{niJBUExR2YIHop3UofS`kL1iqzsT`6v;S)kB>*b7%}Q!w1p zdm%Uis1^u%5#V^T9bOPwd*Gr$;MptH@xmgL8mqc_hTTtZmv0a5)z2i^w1&pCGnP%c zD-UFA3WR~eSHHozS(U-PIURN&v##+sW3yw{V%;KvJsaot++w6@v6gVtGf%$DY6~bu z=9ER+5O|hzbLV|Y*2c2sX2%2P1>KUsA5mbO*ET$0@ZbY(Ynf|N1UCa$-Etoqa!+=Z zTpbK=zV_$U&pMzs#-K@f4LS^iDvUu1I2?x~KwM%?NtoBwJf4b+)Ykh)I-c)Ec_-4A zq7ZJ;QX>~;2GL5>X3B$EKdwc|WP4UfUUlotlzSR$*@h2viy>}15cmiThf~k7of3}> zJ!_-mI9nJGHL23G`V?&Rxm2&%L`N|~rAZvW#x%y*kvNNFI25X8I0HViH3csS@rp=* zIN0f2lN?pcN17I+3Yx<(wQFs(a`i>84QZ}Lg6r(Sp?kt@vi{~GrJxb5LJy~i36rf%1JsDj!B&JU2k4*kYlxhcuI+d}$7(5`_Yylm!+V{M7fVGFabU@99j`cu%Shy)Ng~@u;Aplin-U{pg&xCx#_QwjQUQyzNV zeayAPvuL>9V12VsgaO7~e8N@pdfR#ujwgZmZQHs;O>sh=@~wEmWXncLGy!nV6e(caVAU^j)B>zx*+I zbDg;IR~k%l8+mQpzq(G~b{BWQ zXteMVPW5erfe3w&&<{%XEX^9Ndc=sxc%MkprCB*)B^!wM{WXHR-F`~EMy$Zw4{D|f zrU9-kH#_cK%h)T#yUvEAx73U)b@ObvJLJ9DnkAT)` zs74Gw0?9G1z|23dtdkz^rkaCku+_QM=GeBFc1c$yuy zaIrQV=~PX!q|q=hMF@N#&-zyTnuv3eZmT8^W(l^Y68I$1u;RpMkTURODjrTA@uVPS z+SEASRFAZ4b)$@3YaXS0fd(5l>%als_5T4l-E;Y$T&`aF%S%6SiMsfui~sNXywM5-wa3RU& z4Jd|_VvdParhOzlt+x`5dXbLu$_Uj^A(y95Ne?z30G3Opv=ok~=(+EC4L2W0Pi&mPj~1lN1_n()4I7$!K%joQY<6i1s_EW<_JkI!B-4 z9&9cEY++;-B2Z0Fp@vYMvaO0NO(3YxTO!Q$^MyW-IAOY05zT&#KE*uPYyj9{E0%5a zdIXt{cN#=$(1-XUf+gV!RxRfQJl3zOVZO~z(g}(_&3Uky0I-ADK!iuILNSOmnn1__ z9s@-{B_^|I0vf`zvQXyVABC$MPpJ+SEF18Eti=CXe3F4f!4LdMKeJT zveRBX7Oj!(sNShS^l8R}MX!`lM(K-2CSB~ca$*-O)o*nNnL>I(+`vl;Y`44YNSXB+ z6JsIbNFE$5aZBAUTxISG(hwEf7+%NXy-1 zil5DpR({fN(Wj^f`~Cp17*psGM2pPRg-$A3XpP&pgJlUR9F%#yQ?0u6g(8n-G)4IM&7d*T#Gm;=uHeh>B}0Ib?9 z_vKl9)*Cmf<(`IAsm9EyT6mVglgPNx%xlAVf5diMSdu<{p9lMV0NAoNQ1ozomJ2Jh zww5KCa$1enpi#AfMdKxw>ckRuwLj5P6NNrK@nDYwz-luKO(9Wdz$Fi=je{O^P?0E6 znpMOGSmaE3x^+;>GTLwgiud$+5B9wQVEgUvlrOaUr7;qV>Jpx;AOg>|rQ)a(iw&~a zh)$<*vH-^$z}jOE_PHyXWjhVNJ`rcdyp$(f?5)v1_v-?h5~so>ndA!^6P+@N7#wA5 zrEK1=sa4LIW;<eroD>*D)Lo_v$1?@KJ!Nvo?PRo;e zHdQIsq;$%Fnv(_rS27vC8>?fSkZsZloU4XOP(?R5^~@M zWQkN5B4YkEFT`=kw`2{VMD!A1iO=yPCM6} zjyW_}(usCdiF8MJTrUV|;JlCrdl&$g$z>y4vJMyX)vQrzOS{*nz4o!JN*fkf;dhnq6<=2c}`eu-=*GGB~ft$b3HTOp~Ao zd)kA2Dgdm38(hpvkrOgI&e!A&hEGQWLrh3jAxGCMB`Z^vuvt25cR`=BDZGijz&tLlfOR#9%ReEwz$ya7w#`o$aV9ij5>559eWjTQdg7xq3KKj-pR5NgrzEz5S7 z>tk=UTzf8V?60vk5ZInMcXR#nji#6BhxXUlD7gEoFi@tUTQWW8{q-9Ru;`g*Hul%p zV+d@KcbY*kmnnJmMgypPA1LWH_8$VPeC=Ti;YJ|X>{cJnD7;Hg!h%JtMW_A~;kx}&E?Qnxf(x3sTc zx>58DdSHKz75m*MD#PWizAG*xV;r!&yjS|rF zgsUg`#!3J^^P^kt5eHD&gkRZftbqfoxN~JEZz)^1vh^EzV2|5Bt+99xY|o1R2?r|O z{%MVcbzqgZ>z^>7&uu+xthfW~ySabL0X<&-6nqIDSkG(vClV-g`=>RwXaXy{xqkwa zn}fg;9QH;9nB|W0Yiy(j?#t$*JbNSU>sVuzA2{p&Oyc4V)KvmHI37q z^-~1c*T=ptnkzK1C_3yuCay=tnzk#`aYn~n@0p+TnE;7v!9*-J+7awvDyx- z=QV!%JW%G%BZ9BN11r1PPr=Y~3{<&Ic8zs;U{!Y>T5dfv?3Olr z?x!yQ|Gtgy*|_|ZAhKWX@-B$b_wh@`OK-XO%NM^J#LRmKxa<6TLFBtf!9DN)2t=@Z z3f$QKLm(R6;oj%>et2)R7Y297{qtRCHx>FzaPQj-q4)0m)y|LaTnD$bo!$QF?T>)F z?azUjY99x2(=MI))R|A5DW18s^{K5-Y!$cO0&Y?K&duuP{Tu%lsDI7R@%t`Rw$Fk< z2D`g^n_E{fr-|7{cbZkFimKuqPTb<+o9ZV%^Bo)8PkqzI_V(rU@yU|(T~D&yB)xNA z`1tuH>2$AwG7MKGhIp29h6Qf4Qg2!TlI;R%ltyxi?GhZ-%M*R)))x<6Eq6EX zS3Yu*Sdt$%)EdtXoN|8D%5*igtgY1U%g2}G1-@HXT138$7E3a17YOri^6-*;1IcE3 z8KN;#C(ZU08Pu4S@_Sob=l6f`B(@|kOl!4}OA+OY$Po zl{58-uoN59D02CKlCODe0Tq?(V~1J$}!UG&zwlj_zg|Q1n<)=ZBM( zx_5UU{_ykHJZaElfPbLRP!$fL6spfuB|1Y;=5(cWXlw7$(DTnOiI*rvZqT+}$_jE0 zX&Cfm!4NSj^v6cYhZSxZ8Pbf!>qP^XXn1y$-XDO=;(PnP1@|3LTh5R zOXtcaw_5ks*6zchlP8ws>489&rbu>R=)LLGKq|;edC*y$zxvset4s2Etbvm)u4YeW zjq)Ie5BhhKKlj)_JifA&`-#*YkBE#op=xFuoQfFmE9FCbkHn52Uy?4>aLH1zdS9=n zl_qfA)!M-zNgqG9B#kg)w;>~Koi*}(HN!S;JyV=k?i(AL{FaR~cpU+0~^Udb|AmJ73Z6`WiPhF_M(< zG08S;1x?GV+uZKf?n6(UJoJjVJ3bg~#u2b=T2dwxYc>@#tF!L)u-|_C;F9q+VUbvq zF7jC|T`rce%<5ha^svtzKd>Y{CMqqpG%2A4Cr8d&N^y0Q0q*wf@jI5J6{VNWHtNJw z>)G@O88WL!7a)Cn{PrbjzOK~^xdH|T#s=@;mbSWg_bk7ENt$K4T)Wsn*#RLB(pi%? zR#uuP{kA1(WkP4{JZqMVdef4trYx?O-hF4{_`W4+XRL`NS~KbcFebHzf_#S=nOo*> zU6L+JOs_<@X4!JpK_=D8cy$N4y>s8m@mrRpO>UYqn_N9l$b~!w`p4A+izj_~Nt$Jd zk}2CnsaP8KNhaGbt^0YB-tW`@l71zA#^GHN@8)lPnSXXh>O_Z^>K(q3%i zIy$oPj7T7TnRPH_WzIocEgYX)lC~%0R5kFn(5$x7ZAVa6cW;w#XVYB&)r#v(j=d1-;J?T7wpe zt*-H%(EXo3KD#763{@&h|z0U3rf=GN%hdvYfr=daU5X8;57>KR@ zNf52>IS?iNUxB!D=$W%yKeP4GEn@2eh>q~_%>sBs;1@PNu~_~ezqC}2rr7L`5w27x zq#5QgS-rioBtqvN|HR3Qp7dg91}EXzl86ipo96q|dfHjt@ke2di??9&tJKI^1_mQ7D20hzGb&;m68fICx7c<>{ll>p8U6*%$DSH`I0@J z>6A3gR&DT}S8Mfz(6@iOBtI*n`cx+}Rce6pC9W`7y?XWZf7|iIlLw91o5<4~JJeeu zH6(IbuHUN8R`0ohc6f5^_|o%b0Lm$C5xl%?a8sF*vIw$T8cZjz9FLcDlTE5VLkc=Y zQRHY?Y1UVdQ=xN@{?M_#BwZBQauy@|T0K*z+3{?+dW;P1KG;9DmZS+x8X$FXFfC59 z28OlU_R3ZV`n>NvHkYI=h3${?Bt9%r3}#jZboH9ulQx#5MUF4Fa!hVIQF)z26m9i{ z&dc{`Nt$BPg&MGO5NL`w@;3>C26TPLrvM>%M5Nc`fRVcdZ!0SPmk3lX;qx% ztFne77-qD`6~tbhZ`bmZfhWD_Cvs)3!bw6==#Kf^SS@L*#~Ihp9-o~kOY-BAD7432 zOSf@xII2mxJG3@;pW`P_`b+XzS*TiUI@czMbiSli%lgWabN%#*e|;h^$rmdV9S$Y}5SS^Wu{DaHh4{v|cLO#SB|u4do7%2gHBzSn$Lby_8Te4K%A6_;BEW z*Ity|9kjbU!~BwTzR;~V+9k1EcGz~+Ac@tBUNE;xA9G964l?ZyOSzs|Dm#kWqmVn8 zzH?vvr29(7-C1Qk?U&s5S^}(9Tu5 zZDD1*0lRZ2#~)g<-O8Bdf=QMNWua_knX0qecEaAoFrQa=fHC(SHL*QbRc*aw_7a?X@f(YherS-P<# z4Q{?cCfNy!5h9D_bvu2>ZG!Yk-IHE)&$2~kN@;6IDY<%<&RB!h`SzL`{XFf<&)sA+ z85LV3Z}rBd3NNWWbM-dExvh)O_D^a{>Z!p@1v{v z=jxgMfKC&QQjSnqEo~{eJ8bamJ^Y);6<>Nj2V>FnxYh(O1J|?VK^eoxt5@7$CiAl= zX=%RE{)21V~}=Kl1J(T$7rtZT|Mc-lJv-C z+m3;0vu=@*9Hi8`!`>FqeL^nfoUqNwNXVuwnP`Tw*UeWKG&oJ$VJ}n4saVqMJ%okQW4Y-2-$saz+z9R0bZ{|npET8KtI94g=!KTIP z^|NdK{{L~D@#OdX_4k0PqDr?#WGXm#->Qw~Ri*Cu?^{Tg5r9BcM1mgc~6t-F$ zmp=vG0O(&%Uf#L%E0_M!CH>OLr3)8-W2*(?qmM6s@M7lTwF_SXk#%a09qlTqZ1r{O( zF*AiWN`?ISU+^U0dO43y5;MpsYr~94Q3{4liYQdX44IDrAJ)o zN7LX{Wjd1+OH(E_smAc0?u4U}Zu0!ZlZa=`DJ~JER69>c(b;fhaYTZd@)2H|>G6{3 za448s400I*IgiXGP-&V@O(G1Lk5cs^g(Su!Fc|cr$wUOaDc?0Zd<)|VaCgUWL%Cv>s904-uEPD9Tm=^|6>)t8g=L)DdeM`Br{f_TrnxYw zRl@|{9w-?yisVO5Ndp#u9Cs~RRn0M>_jdklF5#qzLJjF4wQMxsZEMkiY#8YxR--w) zjW#Hj;_?N8by{rfOm!|{8KOi%r798?DU(5EmF(;7KGQ=@va6V*w2|kF*n!+HE4u|> zqBF&-IYpO+Ntrv~`(?7Awwf6eLZT(T(TqkC3ClFb@#%2m({l-Pg2j7UAAwpEgVP!j zIDxC}G^W@@#hK)_Y+WzXb-p?iE9bunIwNfE!=qk012wU1*J%jFVy&nO`~=)+q`+uv zWRAf7M#)YI7Z3P2HLoXl6itEmPRUBV(t@lGuUbTY7|&wy5kXDcqh^#GR^zG#K{;pV zm*+;w`79eoh@PF1!m_Hfj-Ae7XgXsYr0Za#B^ATvans4fe7er}YM6{AT*1rAe! zJ7v{LN6X=4%V`)9i;zUQl^F?kv^73x)aU*L3w&KSdri2IZJ9KpB}Q`5EM-!1K^WKc zY&%y3Z&UL`c4|Po$+=P812~>4hVutpMjhwmmPlfb#3b;c9t2%n^D4yfm7Cle0uZh0cFyF2PVvs-)E#B{P9Z zJ$o9<;xexH>>-QQcvS!w|M0@}fYb)&=6B2`8Z~egmynIh)@V|uMLsUYas?R9$b6-m z&)6y&$M_6w)|uqFU!4~fxK*Yo)=&}|Vo0-v79-_oTp^%taS*czrES?#DOnYJl0e-i7*J`fUTs;Xbw|m4WpNflL<0W?V!mV5($s%TYkf>f*1Z-Ei7Z1A)N`= zAq=!Yi0m^3n*x_ys^c0~G=z+uo*`$yGS^WRXiR9v(<3uK%~`TQ)@359u*s$@aHVi3 zZD~`g5sMNEwE11WL?_cwD%Iiu91nB2dVLyI;SPZp(^Ha6jR;G?+TFMvaad^k`aI+1 zo`K*6x_wa0#B+dt?Kl+`Y?9`>d1G8KI*okYsR(LoR&uIy0dQJKSCt+TW0E`;h8w`` zngdfUc5w`x1k8?&s+yl6;zZ0oI=88aD3dN?Cx$L*NpwO> zd0J5t&NerfC}fW171iz$SBGL4I6nZObK=&R*t1GHq`*9I4emNfl*H7$Js5y*OAvuJZWKe zt5V_>vXC{+T&iVjStyIyCG&~#`P*06a zU~bSGO7WTf!6#*CWbqe?dVq-!& zcIF4>66xt6QDQ_GjZ=jjV9OJ7E|Mr`Vg@{$A_^33>g^d?J&T!u0OvwgMHVfg@tSZ%^Bv+F2y%G<0M>l?SuKcx= znD87brgy^_26dH*7R}|9DV!d%s2Hhn<3v|V;qiV%qoHcl>~ov&94hvjDaljw4_-UL zcwqj)&F?kNXCCkLl*gtAN?2(m3uaWF*24S*^ut|QG$gIdhcQ8{)QU32sRwk?Jpb0Y zgwu$`H9ObQwMm_}`HE$c5hiU#RTj4sdF!B69${@6^vJ!vdDX^DHde_g9j!pr6@j${ zu?;1)suG1^7RyDEHawKTWQ~YptxbEbV`LQgYAP-EvN2tdn=?6;R)s__JnTm&Sh|J} z2sk^&#c_$-`6qJ;U7T=Hy9(oZD>iNs&eWL=;OMlT)xZrpxIziNX^&y8;!rvNqRcgd7vtv$0TP-sE?v&{b3W$W;dS*6&$Xx4SoQn=;6tw;K=MrKe z)*#c3j5<8%$9j>mIdUkjn2)3n zl%fTu7fxmG9liwIPUaC|z(5LZHX)u#meWb0q?G#y9Xq0O-BP&=p`hfUokv{$e{=JJ z4e;;HpI1xZzZEynZv4yg#+fh_4jpZ8Zf@^xY=KbD&>|TNg%`i)1v^T_j^6ZjppT!Xgzg|H#hf+0D(f zyBlw~|CTJ8|FQJw4QTQk#H6jib!>6b?Cyhw;#4kxYbUi)1qN;YBhPdTE~A+)6>af5-23Hh_+Ao_9T) zn^0)W|Mt#2_z3V@5CixO$lV+5za@+2e=I$E1DgB>F=^{>xhh|;K40#(;=vz!4R@(! zEd3F@PHqufwr=^aO)#Z7(lF86p7s*uUzIk$EGrq&S z+Q95R9D3X0^GN9aMKT(C`yv?&J+Mf^JMVClKq|iTke^EIJm{yAp?A)|xZ%z>9-gON z1@BrUBcVqY$yn&oMG_7@wn!#Ik1vv`(3N>|V>20&UM-rdi!Ve%Pb`wr(EcJB3q83= z!l9=Y$$049i)147w4XeagrJ|_-VAMShBn?fzqf9(=jQYq$HK3LCF|z&&fh?rXp4NgypA+MO+5!nqkgKzeH{l6v+bPsthYci8*A;JvP2Ke{p&zmLiW(mAm z0&kYUnt2oPKHYPl?zvCF z!Txiu*MN&FctJZictv7k#}L$^C2T+Be$!q5e*?zdGnxf+T?Dqj~Vt;h~1NO#D^M*VRW1c`af53z997fANYb_1!UVjQ><4+A$6@^Z#8 z2QnY++9*}ep|OY^w$MD!!XyY}@~8*<=#{&}>V2l$8-x$oW>HI38V5BHK#k2-CrK?` z0;eZM3|tu_n#H;t0il@Q<-xuy0IXyure$y$9bf8T-I<)rn}&(id6};!(pgo@%wn_h z2%MIw=pZ!6BOdG{0bt8Qub8vL+BDZ5I&@i4@|BVlqs^(_ij%b(+NEb8VB3TjBOsU@ zc(DV7Y;W4&fDY_FZ?H&%=yfnWA~~s{rUUZSRaaaRhW~M;jD)XQy$YNj+!{@Xv-{!%- zEdcCVaVzihVDAe6yH?!Fw|cN|4FJ1FR15m_EgtM!0>G{joPs{R?7?2X`~B7HgrlHO zFL|(+0>G{jQ-VId=)qnL0J}!q2>SGb2YVp^>>A-A=+pBa?D+t&Yea*fPtSR<=K{d4 z5%+;U-Sc4g0>G{jb}#gsY%W zH$2#l0I+LBouF^L?80vT=K!#4M3$g$yyU_DbpY5k0!Yv|Ui4u9Dgf*{AscUe*n|CQ z0N8axHr{x_gZ;|@uM!G19S>>AMu-Sz+3&7a=5 z(A)jOwgeJy{(R$=z_&hjx*rlMosUzS_cpr83s(?L9;qT#*Jy-S0q#06F1+ur5u@Yk2Y~|J5XMUe zuF+L^0M=9&K-RAMA!z)NFRm@daH!79C_so(SHKiBP<$@fiU@#_o8MSVf>Um;6gLDn zDQOlE1I0a%SyPx7^%HIYu7e0PKMLFlC&S<*|3UcXO8`8G555RMctN)$0FeM`uSmSW zUEsk7+(Md1@lu4TYuj>EJlBGuA@^kMPM>v5!2r9C9j_IHVPHrMO2DxM=}t8M->+YHo4aK{#(P-zuD`qW+grc!@X)N|*qga~_Lup8F>uKcbi>^Jk=2|4U z9s;VRC)_6MFTQx@ThE?8;koDooA)*vP(T+=enYzG8pH1GT=caelDdajia@k`wSkr!<~` zSFf$osF8GDw;x6wK4lx=_Vk8g7FiHj-iXf9!(2Jhp0&uCa#YbL6Xz&QmJ&!@IU?Gn z2sV-kG*dRhBfJ^aGW99c=4KT>OVd;&94;RvI0Tpzzz^N513&zTd3x3L!;jgU`)jnW zyHC0T`eEW5(ht{|r{2yFUkx4hb@IPg3~&bn-R=%#ZprQ7udUlfQc-A?+eIxgUA3{8 z*#eg(6*v%nBx%5s(Gru{jKv;xxn#R0Dui;aRC(QQM@oV)h<7UdeOqSsgd} z1zmF-rc!EJ?6n3rvoOs`1so6EtOLh%*Z+@gup5_OzVz3ZUcC6#i!WUGiwo29Uq0`g z`_egk?~8lp?w{?BLSG0C!QFl8_UE>hv!6LDpZVi6y{$jo5;lK-liT<-5MTZC(nsHS zRuC%tYixh+ud$PL_kFRKK6{YWLUJA`D`{@!AI5<0^RX+6LU? zDtOK_V2xevdtC*owGFt(RghfUfO}j83D1Bv7=U|S1@W~FxW`ohuWi6Ru7a3nz#44H zy{>}j+6LU?Du}FYz&);luxG#;p!d28ptTLS$5n9T8L-9(cdzFIhn@jzgeAYXI|VO& z^k8iR?r8w%mp=NQwGCKfDRTF{l9xVu%`;#P4)ETFfKMh zzI?hhw?L+lo)9e*DKSd-Ez18Pnh1YF5v_-?F@ zaYD99CwKx8m1d_?_0bswrm1cV2d*yB+}FCeK7)GDfYpDs+>B^a*{(Q>IS zAMje!t_QJTJhxyC)crM1O9!#Qo?Ea6>i!xBtAkiz%`I31b$^XB*g-5X=N7Dix(;6v z!~$b(!8$C+8hk+z3r2Gb)UdRogs zx|8o(gUq-Mu7_9ZdUagcZwOguI<4r!!6dIRF|!vp)k+D$v$+A2?<6uvrGpTeSkr-% zci#Ye`9^!+K|$%iz2oBl94z%rsdKVu3uj zU=7rDUIhqZfi$;ZosHNvb_|19(3@MZ2I{>%y!rACac;pn+i+{_7u~%KE}s1~IRD?+ zxbW`XpWB{*#G60&pai~k_w;G+xYJ{s`)hPEcdxEs$DQElH{@}rH9EuFoiYSxX<5#a zhuYWYpwrFAmcrrNo?Dt*=pNd?y3Tmw=I8aEya#9RR~|iNa+UJ5z*$|n07YlduXG@Al3iKeM?`Q@Ce6MC2Q?hgf5>@<#6=T(5lX$;Q|1<`BHn&B;Edo8>+Y z^PD+|#mL;WfTdZj3wNzrFPm)$JSX&f6`5`IUbmY!nh8tI=C4Uq0nrV4)Gp(S1FI04 zQpYI}VUcmPF%h+rM~pDb4imh1)WGYbQsr7pQsQkZ$HK-;L1ADLVk==`mY+443^?!{ z;9!3OWZga3-#&c`IMmA@dt&q6&XjK%a-*Q$tcm|h+*LT`cFwnXCk_sOJsfO}skG-G zUIV=OrM-0lZaHzR3Z@BOf%+N^z&D4GyUjasHxIz6d+J^BXmsUkGb#x4PUOlNx*ji1 zOWk4uAuU6xDR^3KHz=@+m=P1;uO=?+)l4$kkd(MHxDu3YcBoi{;W4{usyp>=z2T4P;#yWvU|)Gl~S6cowV;04pW zIMda+a#NCZce?7nK;s(1%ceeHr|xxg?qm1%)7QRu8i~7KWyh?=8Xa(XBlxCf4+b~; zXA*qjwWHtC+o!AEjJ=)VuXosl;nnB=$8fpqWLUvahT}A8jP+Ke&G*B4In$~jSW6-y zjZML3laXKl?6_BLD$+zD$br(AQqjQ#lB4M+7j=?W4=wf5AmB4R>e6ts8jr_v{R)Uj zIYv~vY{m$vAs2gf6v4_3rK<~;+UpyI$eyvcJk~`oW;Qzfacck zY)M<0t;aY2@#g8~g^fQ3KzI4EuOD3e;Pq_~VGLYbxUsRjqL$+~NB(yE`u#|EAByeE z@T7*vg?h5jDn)w6Bz1VeA8KY}oFS8FR*feqt=%|0GRh>RCu4M}#}c&-J7euIgF=^?|#R=el3t~H%I!s$u?*hrE5Bq&cb0R-XqLeG-A~bOYjIYEizmq zr`T|U%LN%j#GWZPEEHVkcL?;RuUvn2$+qZCAx3@MQ2Y8bfo!`u*yv5RRqFXl!$}{N zOO0|rm1-X5MeE3>9g(N%3Y3%dNF^gSQv%g1PKGfKPSKoJ0=B*P%Jrw0Y`e8y=HGeu z(synxANM=<^{0Z^52tR)yI=3q44ll2uqLD`M!6P$prOTcQ>x zp`nc=v4JXMm1YF!joR0r45atgm*suENwOD5C(4+B4Wt&Kone=arR-!b!B-u*q2n|% zG}v~1aF~f#57k_Q0&zXnx!(OidK0(LuUX;`OGqi1kW=+Si(>6sHarw!Ixg(CQPx{1W9vqR1$wX9*PjTa_tsw2&u_d*sC=JG_Dg1jvPM!omsYxm)5cMw z+)D^U)@&bIh2l7_b|W$qYYjQ+$adTCv-b6?f%M*buYs?(T%x*}0+G^FuxwjnLLaoF zhH%6hOr=$36Ka_<${W7aAKY*x?9c!f77X zf;i)Jvj88)kz}uD>$S2(K*J-YnNg2mqDd4di8d*UX`uI=_VvdC>AlrYeZ7-@R|zMY zg(JtrqLHYQRfI%epa*3RhDj+ll%nReRGjt0A@l?!A1QJr3j!3|7|?skzW%NtdSfg4 zjc}C`$2nVyx8oJl7>Dx{R1^ddIyqIc;RrYo!iRftasVr%Ogv>K<+8+0?77}Yg6M@; z^tavVESyP7mE0^Uw(HGgSW8#Z0&jwN+W8)7(vVZB;-Y)5QV!4PNyiDb0O9f~#Cpy6q*3B!X~U5dBd{%X^{ z{?0&pSF~paLQMVr?w%ed}*G6bFJjf=~k#do(C$ivuV+82^ z?3e884+YY@!hbV`Vl1k#BhuDlXsZsg-{oz)ZcclZTol8#QlS8lp{2%941Nw^=5fbCQK}x84)Nyk5ZTiTV^43tAYf zCOHP^{j7ccfk1j!_)(UG29Q1`>jK61SegM-#d=yu!E`g3Kg^B_Qk~QwvYOExS)UFI zGqVzogMQN0`;I_*SG4C$OJSx*wL=yL1J0pk*T-yzvMXcTC^oDfQ-Pv!^T>c>a#wE| zMw8D~Q+d#DxO(3nL~nG(_$$O3sNCcFb{l46BPpqsO@$B|MGmdEXHa7jITTceOQ*rM zGM7S~Y&Whmz>hxryY}__1L$7^PA(-H$-|E2T4@J6^jTd^O;>2iHm?Dx! zCvtewD(I0?GDE{9!fnsLW?z3>AiXQb{jNaTrlca4B}Qv~&Z^sX3x#hx0$Q-?7lQ^>XSww7fjO%fH+Pk#S9h)ilQZLp#!Qfqf z@B~F2wT>YykBwjojT93TcqGVO-fho6XJ5Y@Nbd^&RZ5amh{TkPqm+a&J3O?Hu+br9 zcj?>_WJx_5Z{%Za8OrC=^bj}E+Tj#({TJxH6iDxi{)(&>GE&M;3Hj`-mB__`X7Q zyH1#nD}%~mtpQCdiRx5i$T&4ERH6t}Z`BXkD2gLYRt9?iIS3Q5abffCZ-l;YN4oTj z7r*yH?|kgs=I*Dq;j>#?zXU#z-SzT4`Pq!#6PW-1fP72%>?yZJWspfLG8wlbIf!La zb{_N6CX1zJd}bIcWe_LbM;wYS>TRrH^I-YQGL2~-vj#$C=ujEpbdiCQ$M`{4MpF4q zmIRoymI}U~M#EgBF>Sh^XP{mM%h}yX%%b{Wvzg8USwf`f-lRA~oC2MlQnYS%BSoo; zNnNE^!1YQ!TDe`3wd$3gIJK~bT+2mRGn!6! zl1fW;NTC8%$fljm(r9Ux&Q-yxY~y2ZxFQ2B?;aIdy3JC!G_I$eel;?TbZfD`kZIkf z$WSaF&b0f*+K?&19LHCa>X2e{dej^#%?dTJhN-cYrDdLMf+G92H(-(7t5%jF`VPL zjcv{*oqb<)WG1f}NcH`@Gx{R*}vxbC_89x(3CSe=k$t;h7Ndg%bvxFrBOo9m^ zg8{P>!hit-^BtACtNN5mrPH^r@8yB|Lr*Vy>-~M-_rBlqe&73hvl-HEHPdE$m1ANR z0TeRfRVmU{XKISUkZid?40DBaf-K5CT4(JlF)KBigFM;|{k@l6mrbtBG0CZu&*rK8 zJYS(>hQdc{Tv3NsSQ%RHv4k|z@fgYpC3%|b6nlk2tEo4Iw4&8#P7+qL=!_$}iE1}= z|7F)@VCB4`xr}54S?_iAXc?7_G{_+w_Vf(BqRWUOR*UBmhmt{rG2ZM)jLx*iCo8O6 zZKUi>YMvQ%6j5TvwF;Qao_`s3S>uYjj1Xpa3mX_sOf@J)HsaV2IM<3U!?NS_yp~E^ zEtO-E!vWG4(9BG2lZe>|ypfGUPb`tqJXgV@z{)=KGVHReS}{p?#J2^(DdYdS-La&i5jAjT`jxsH>m1#|- zAw|Pr#r)vQugmzWn#(e+3BsVyt~WtD?w}Z{J#-EN6p6ck>-Y;i zHP1R*8`OzgsyTU9bHS^J__=6hfZ}oh&gU=TY*1frBRJ=Ri2;hz0XVN)!r7oM-IKuy zP|R*{{91tKnP;4>4a(Uq(JVRkA7_|92vF*-d;A)p^gX~1Hz;m0%=6a`3J5Nht@YRQ*I5C`OiIUl9vaZ}4eAa)?CBGK4M2lZJ-!LF zc$Tw48N#+*c1FFodv#*ozIbFCq%NU-ybCnF8ED#|GU2j4-UHzvlqz5dZ!Cv!gF1y9 z4dJ;Px&S;R)#K~S&fTEA;rY%Dz_no?Ut4zW1{Dv_c5VPRjr#Ep(Bb*w*5w2NOC68- zVgTL@Fp%5Jfede~Y`rg@yYdV`qA@t$0%*KtWP@6Y%ciehMgs6=bdSS8DOg4}skFFM zx?&j#z>`ru4grl|G6-)_gmJ0y%rX*y52N+q13(j4M#3A^Wn5}Hw~V-2Q?XfF)N?hW zHLk~VY*l5IDGW_8l*r_1>n3WJ@|4D4QToX#UCH`^vN<3fUx(Z{@*u*Zol&8|8Ol5{)U&& zA^vYJ5>PDVZ434QkZcuzDCY9%)|blY6`NX>Y_+mRQ2UGz;-9;UNdSDoQ#VwCl1WiHS)nzih?@8P~DaCuoj|AgR4oh$3-Wma8KEXDuK zYLQa-u%zFMTPy{M#$YdPYi02;7aK*{!>Ov3c)2^|$=0lu?HmpgCIxugQd!3BbOa=& zk#R+T;w;FLUTY5EegbNf`Vp&{kkjYwTq9P?D+!x{)OqQsQ`6W%nn>{Vq!^DiTUo6K zWhyqVw)DevPv{?N;QNU>pc_>&S~MLbOFiE>KJ*;(Yf9m(Wo6pXF;`bH za+t~36k`w>?<28ktdfH4Mygh&D-v&1U}{9>>26b9KIU@51t%=lm3uKZT%0Ho7NUC_5E<)bdJkdTIBMGN|A8usG;4L2{9lzhYYgAA0$oi(me5w((MRFG=C_hW80mGZ9d7(@WxBIoqHKJIyr*hZb~%v(c`p^RPIo zx6-kRnU>Ow3adq{);)5t`IDC_RMZSihz+WwS2Il%qEf@$$kE!9Qaf?TAF+ehA-Eys zXKYIv@kiw`#t%U%3ujvL1V`enR@*$(=r-q!yPA@#+I2;0;lyS+*Ndimj0U6QFY;!a zubhRimVs@<*(R|v=Q_Y_d}HAyAmbG z8d`KPN{WXBt+iTJkl7}85(+16=y;2!1IPPd?f4nb@jhG#2ieA6+5Q_o?|3U*$@?eY z(t2`-`4TOvi`_CP{p@-STwn(adpH(|x@#+4pId6Z73|?6vzkZiF`j@IWa|)eA#D!E zk$Sgx)G@LpZ4kHeSttimQV|%Q_s7j9k}?UqS+<1#0U&3Z(_hEZbVF-xRWp~J9Ko|zt^{+eiq}Ba)2Pv~CJtAe zOlR0;^>EXm>C628*M`i{-7mZIeRp1QyR`rP`?qdY_WteOYj2Kse`@!c8^_mw<@!6X zebdf=-4VCHV>`U1g}*cO86dF!=l;E8^lm8ZJR^rfH+RF=u06YZBM9OBa$mFz#a&MM zeIwlds_)(TM(^`l?V?%Ie|Wn8w&O4HK)&Y`5IIjVOAN$Li_xV?^*%;?D$M5!VrcZJ11mW3V;`D8KF9*Bh z!Tj^T?|puT-5Bg!j^E{hjh}+u4noPl1bg;Wb2-v&596PP7oUHzG18}wzt}_i)>EWg zL8$weNKZB~mt)-W(EahJz0W`S@WvS5c>K=0p_@yLH$A@(!tMWX_6H`=8?>{GC@VK0k92PX8^({Wl(`J)Cbo9qm^Hxlvno zrFRg$9N{ZGY`^()-sf*UvoXSZ$0-ltnK}M$kRk?;>~X;vb~(tq9=u=wDev=BuiO~q zQ^!f4)7%MC=im`YkBaGXjCVYApZr!_Jl@K0b+^{2Wd+>fW zzxXU)1oE6c-aAhCK<)=AmJk%=z6bBWymj&U!Hq#49K#;SGb`k+AoUcUz`1XE2!Hw5 z`~1&8v@y;%9>+bLXPkR4NY#a9XKv=)dmgr5dhO!#Uu}%=Eypnr;hAOeW{|23ON5Ut ziV?}sDoQ~zRPoX;Fb zJe-ejhc~|iU*hy{hc5-Y*be{1Kk>l+qozt|3c{0$z` zAN$0{-TKt=p@;O0-){wplP-}ix5JnA=3+bi!@uca{1I|vjBh;tBL8+cKmhdN$e(P7 zFNNq~`@def`231Z5uV)tmqYu>o9wlJA1<#y*8lDQK1mONYr`*ocx@QqS>xsBA73Ef zyPaKepPyV=4Isc%LodP>SVfVN#93vJmUYqb5F?AF}l! zA=^{!C`z^y($Q$nOl6kH<;o~AiKY8TMtjy7Od+Y!<`Y`$XuR1Yg+4klAL%UB}Zwf=H`okCRpYC7L&0$hF~uU;f3qHas_7frG5R4g~2&MG7(L`!!C$rv} zX`xn*_4C=O*cE#OIoJE6YKff^V^Pl2lA@g?;(Vz)f= z>`#ro{VD9N?o*3<_^0g3&>1|1KGkw6!-Yvd@Df?h zbBAV^?VCilbyV!nlCfBJP7RueheRjC#e`8>ZpWhVlfP8K>~aIui5zeBM2)kJnJ!y1 zLJ^f{zMRG46}H;J_yj!~){}#T(=CzQkT%m)OXyMkxLTtqsSFK!TDMMQP)(xTSbpT- z8{OVNZ2G_f)fh^STUd;QH#9+_-C%Ls-n;$-AtLnAQ21!;^KSgJ-RF13-B<6u=K6A{tQCybbOB`F=F~i#OxD zKX~^??|$`N^JSiU$=FTI-7!Y6{)e|9OD9>&O~nzWt%Oc)t}GzlG#s8k~#H`48b<#rfu3SLjvb#rnec#U5| z<+7NIb#h%J-bq1>)Rod{d?eJ=1{|FgY0$L^BG&9F(4f0)<0qGb?x0hu#cUQqQc-JE zgV^~*9MqC6q1dRkW8?OOn6SOB5S^x$g8yhK*ygL_YNw&GI#`vo30|e}TC{7igE9%~3a+l=xYiYz^!3!(PoSHKuTQ;#w+X zmfP78DHQUKlryD7t*W*O$w=oWgQ1p=v8~v$jo~j_3ML}$TDw+CRuwo>wPUtcuoN=I zPHQ*Cxv2GCfH6dfyH3jC{s0M zK*-(ZNgKBxLWSl9x$X!N1w+Mrl&p>;S=vZf8*M}$XKF;U2X$F1$x*^mv!WzXoW#Y^ykDNnZAZ3^ zX*(m#24yn69LLaqSPHhLbiHc0O1(KO=|zdkRmfPfXSW*hNEPq)>_#ruuB#=nvb3D= zKU@kn33P}=rj=HaZ90woSnF`5>ZAatTv{B!<)&Drb+W=ltIMe){EJJ$1~*}$hSA0I ziD9PRpVlN=AbF@h?Wobzi0T`qj9s*Of_G1bBm4tP!Fpjzl*|sKVx^I7k5jy&WZ_mk z4!fwTsszl#HP>#Yd19$?`^T4p_LvomHF;hnrQ*2N%8TfLb*Dx-TeA~6WNNkhiIyNw zWqCOqTmO71s7=9p=vk3N;tW!+VJIJ+CX=Fp7?J@g-C-H6(Jq*|+R8(d>z)Xv*mN_c z7t1ot(K?slDcLEzgLa3o;h};wyJZQd0eK}S zh^a!h)ms|C&PVqCZRqZ!^&%8{W+&^d81X`PG;GxY`vZO^5_op)`2M9vcPbSMS%)c? zxDxBks--yrOYNL8FPGW8&hdFtmy;2*{?N#8U0=2_O68b>VRx)bD-tW@GMH-3D(1X_ z4(h!I%4r3Cq!7cjUS3Wew|?Sb8+kx+Jx!yOfw56|&XouBq%oi|!me~D9knwWV40>P zX~X4kgnw!&$o1%P5j9K(5Uk4MdVOZ3p^`cl>4K@2>v0Ay;Zv=|8RT-#*~%>i^G#)x zB?_?uQRmAlU2dv_N^L3wIx`7|8KhA`8~u4CQH7R*yAN$uDU&(NRJ&X}i4hrT7Ok2l zB{di|Hh2{lPH8sNGVU9FBUCXe-<93h}(@&VWWYVnz{) zTYAUt&o2ctd9v3^HM8RZ*~L&+%#Z`TOcfJZB$JfoPRdR=20eh-ya6fsgKdvFxGZi5Evl@ljG^akey*&wVqt=0dD-`rJx2W(J3t;6*5_Xld?Hg zBs5OL^(Zog1*_2)Msft}#%*Znk9&osVAq&L<^@5l)@ovQ)UZd?EHY&)bt^`61%41| zu$3u^>o)XoXY~imHga~vY?-xUhXUy+Qnoj$#H%>Lkv2dM$mELCae1tjQr*~6aPJeh z{wnmkGc&;HAD6qwv)9~bXMm3{HKr3v2aVWh$0VqvD->06te6l|W3%JhfaoXVlQ>#! zwHl0mGRy6KaM{MGm~!+ij8xLON*Wa^vq*B%QX_J1z||_XA*i4m=k0c*!!0M|Tg%}9 zC3V|w#_o&Vm<%;k*q(rmgs8F9)Ig1^K;B-oFd%hR8Wv{xf+pILaRKkd$3n7e_u-Dx;#*6%yJ0Qc zm?!B;rd-CQbWX+DF3k@mJTdMSU2WJXt9fK3(fAmG2!}oyu3dIH&OD)%LBT{PjW(*0 z!I-Sb5S;2)^YI2DMdK)zE(skLX`#;2mTwh6cMLY^lB@g95^4f#GAKIl%UZ>DQ;dTo z5lnZ}QxuA~BCSXiE)B{G-6v;Df4%cFOF^bubGvk_I-th$f!>89q|OeVu9Z@&sgkA7 zBl)%xsr85Ua)!9`v1P9UhVY5WNNTs5XLAV<0JZ8JSR6=k0W3p}1a3-X!=y}1Sa!}G zVkzi~*bt^4&s`pO#`|8ke+!Oc7P!1aKefE^YPB8Z;HpnhFyB0|Xhb zDw>L{xH)(_$fF-Gh!-SNYVrL5q$8`NsR7>4JswScyLadC_SIK{pQT9Z3%L?ZdEK}6 zO7I0#S3SFKd-WqyC$3PGa!he6#ARh}U%Qn&eH#N1kJUgbxRK?itvzFJSTj z8D8&u+8zySg-03c?|tzdJ19TExfJ05Z%$V^_VSgiXSpvGgU8}B@4Xw7cszUd?s9`m z6W=A_3%Ejw$Ds?aP_nk+G^f$hQ7@ZIbYM;4x)hdzdwj1(wa_|oL;+q2ovA_dCr^Nk zPsv0&1ykGv1d?YEmY|_bG&OdkPF04D5;ST~VkjYEOsyGpv#3eP@UW71$!tt#4>J@~ zVL;igR*5MZ4MJ9#!I16I*haVZps+c#sBFG-{Pu;5z9xJS!0@ihMHA`2mM$7}@%Ftq zIR8fcCGyr4K)@v$*i{1tjK}UWy&3jJyvmHl6Bj;PJ4(ml!^5NoKdNTsSqM=YnZ(dry-Q@LWxsvZ^7u?^WSyrzSt@+;iqi06l2 zISL1P#D8UenEGq!hXLrpRxOpMxeqUq|2+cW-Cg%a;OTDZF}L%a;ze}3XzF6OJDelK z!%jaoglN&OLCi!N6nUg~7*|GE$I17cRz9Qk6x%iFCw9ATF~;`OEf>7dN$`D!Nw=6u zy)z=}V!!R+^;%ON2yngG60rtGjAo?E(WIH>ELB9iog*kdV{9#9R|dW06bC_uWMgP_ zunj4#K+}QSdFTJ9Lmvv={aSDr|Fw7Ca{GsFf9d{D@3;4_-TLNRuiX3gy?5RG`J2w} zAMQH4;Tyvnd)GgDJ#p=auPHk}ypsZa0NJhY+mgbc3Qt474Sva!|JZ>3)d6IOp#ye; zReM=ez{~M+HZd40Tt>wxs9n}8d`GamQ?%GE!TrWqvF_#QetS-5icKmjOLT8GX`Z?5 z@lG#~B~aYI30Pf$l4nB)(#Sy7G+eSNEZc0i%XMPnw(8}0aRes53^m?qcF_!bongcjs9l#J8I;f43Gij!(WwDSg&g&W49k^M& zTrVq0oCFtcb9gTGtC}wN^o{jBa$~m&=wBT`*a`aBsS24caQm`onL59sqIlK=%HHr97KnKT~Efu1zb6QEMW#(I1SmDL9` zpv;@O0+fo_SlQz<*MligXRuS8)#n0Qz~vjY^1*ci*hnd`xL+28;r zATECx|5ys=MJw;wOH*&M=sg$#ES`S`s0^_&mJ9rI2vmBDeSp#s8>?L9pDxhn`DcIv z5gY3}>z@Ol$Meqsl_56PbIw2eK-pqv(P=e%QDmNpHs_#JJ!?vMJFY@~#mL4H(dNNZ zg^|*jcGA=4#>#yD*#oLP|Gd%!s!h!GT;&P-LD$0q`uIw7N*>!8I}bWQhi5OkgQY7o zXPQctxsDm1nMpViw^fm(M`~LsO=e^vugxoBq`_Z)@;=9=QFzb>dfx2miAnu2mh5Gz z$|zqMXL1VL00D)8TxF>UHBG^qBj)Ld*)78x=sCI2@z^lHPN20Mx&Y-KHde8AYCXro z=sajHyEQ;Thz+p#vjGYZ8q01CP!wVVJ*VATeNYD)Jl_kDW3jOYpYPR{19+u3p&R+0 z|MIK%pbD^fOGSXY#*LeO!BSBHD!rxRN`ukM=AU0Gyz~E?LU!ox2k!jko%i4V% z|JnZR)~9bx_WpQpbn}mHy5Q{Jzwrk*y4Qd2di&b%Tx;(9)=quAy5|p0&Hrp zcGLOX^U4B1fL-R*zK445c{wnE09m4!Fa6IwF9im0rCYA&o)=F5Y`y`%>LCyU1GwTL z-~$7=;vwJ`00JCPg78{xVuWm909QN&Oke<4JOuOtK!9`EReJ#y7{C<|K{haeD;|Q( z0ziNx>s1c{85qD74*?Mvz!eVxz5o#5mtOTmgarn0#Y2Dw1`wdm*d|l)mn;ATh;>}; z>lp9+|H{zELwDbC=PPgj@cv)yCvSE3{>9B-zWJJ6;l_uq|HAdV*QlLu-2R#En_FKJ zo`EmEOn>f2?%%oTJo~Kty4?`CdF$Zq3=E)@84bke%0=~LI*RU zq?Rx}JLsi)kZO-L+(?daCKln$WVzGV+A)`yLlL&0mp28n>Gv)N>;?SoPPY?@LD4j* zTNNurQ>u1QL;=_sRSF%YFyV=ncAAtX>$YF{;=YXE@kSH**ukAk*b>S<A0Fx z1+O$OVx@)s{drL{{MR~{H{+z=FKh^Y|JLbvl=K8cMq8L+V>7v&XT@3`nZjw*G*o?* zs+9ToBuW-jhB{4c3UJf!UEYv2;{gG)+BTzXm)StG|+>zyh!$ySHDZJ9}5JaXsV z(|&J@m0TX;D;X}8vr^>}l+9u3e7tLbbg{y)?V=J%LwI{a4oepS_5<1UdzS-V)9)!@ zJ~vLAVYKjhKb@31nM5OH_Z1{7=e4@VvQVdr#&cRF+0r`=S0*s`B6w%BZ@t^sO}}^f z+paauNCNY@e#U&VdIj7FT0$bqlw%p*O3b4pt;?}9WZIm9gniZ`ErM|o^Pvl&YCDm1RvU*W}`hjfv zy~_cw>30OI20P1s2dhDVS9TY{^FN!r=zIS6SeHMoTDKa+6TlC)Psby``?5^|Zu-5; z8?t6RU@#tAr{fXe)!0Sw{LjEH`ksIHboqmkwc`;7;}Jd`j{q;uHU+rp_bzY9n(>H% z@c_kf7Q0m79|7K~Z3^$A@A-FCmp5bWc*O4GA2=P30FRh9#kcAAE^o)0;Q*W0=T1i> zz{{kI(E0bI7kuyB8;{FhsjM9hu<3jM>1YIq?r(~3)9+p0j*CYlmcIYCFJH9cHQos> zKydUTbQ`YO=bmj`erj(y_Sm_Pzwflu1DxM4g1g~{FXsQ=<#5+@dTNpXA9{Oe@Bg_W zZ~qwh!`hz@z2+DONje|+$Q!~zYX4jgaRu$?aM~%(tyees@?JVj%y+|25e)ICwtl|LWqbD4oOEgavol1A$Jfr~yLT$Ht zn)Nqyrao@a*vibFC#3>Kl8FneMIVlgWVDxvk8yig6KA<#4(L zr)o0!7L>8$*d^qo>wTk0HZGnqgJ;WZ7xiZ&0N+2{VVrfgw&Jr_5;>(f>Cnggb zX^3vi7%2i-&gA0uB#LLt9S0rGG-p=aFpt%1x|eFca~yw|#~Q%Ep2#&IMs?|pCycz0`{LQ;^5~0OP!~a<)P*eYJ-k0?W{zgb5@O5~ zqp?_3h`JG_C!6ObuahNLq%tQ}61bC9~qq^4A+UP?1Z^Fgd!t~BLgYr{NNuj!s2 zZXL%Ke)xIeAQj!N%nw%`}`r+LZ7-?S%ZM zaQd&_KQ;lsz7IRGX8k@@-e>#!*xmYay2i}lSVI7G02-eR{C}wA!9C|FIBSR z#3+}Eni`X}@MsxE=0bXwu6APM%xKDuYub~u@Fg;>j$;cLMC(?<1!OxNzOOZ#34y4x zwUmwLU3d%)blYsv?xfe9&l;d$MT;B_gqYQJ8ob)Fa;A|T3?-&Aqem?)F5w$SO}wUi zo|!*J77Hv6h%%KLQKXY)zy=-d#Y_Y#48d<6PUeO)(;=uRXR?TjcG?6<2lmX1&TGk4 z>|ZaoC%)kG+Dw}2WLg%~8lE0Nxv9YCXtt-7*hDWE?No_AGUjTLG7gUp-AQ@=xl|~4i zmG~l*%d$rU#bD3@mF1J}l&yD5bdGa6bA5~q!M%bzYAbUamp$v48&XM~)jPcg$!K|E zGm5Zf(^ss!K`htv?$;bA7V9p!HVk01fxX+CmCvlZ(Tmny4nl3<=(54z1H_M9&O7CKl2nu|ZJGqg*+S^~sYs2&FDHyAV^#u9lHhcA5 z;@OD8qVDGTS>cJDJ0A1yAy+QEfUAF)b7?Idon?k5TxQ1YL1NZEYUGkORTtU<;Ef|I z<;f_)Kk=rTv$-mjMRq>M8V|lZDwBe>Y37Wn<|34MtLwAWg|MJe~-~L~3zj^=v z*w5Vh#I4%iAMO=z{`t-3?qBYH_3oQ*e9w({T>tiKzjy7+cK&3iy8UO{U$g!Ct&ePd zVfY_}|4!&*;1_s5wc}eiwg8Wa6WTH_e@M8Db$PE73WHJ0aSuRbq$o_0dJ&G5TBw{P zW2u>tYY$P*5$ExGj~ycocjUkZF1L^OfU2~o%Gg+yFRXR|Cy5l%8i0}+X3Bz1i%nE8 z7`Ju2+AqzjGh>)AK*|7Oi*=}Rd=u!4d;0W^^_`6l9w6L6E#o4=vm+ObXSX+KBSW>1 zEBzEjMU7Tz*lD_Tq>(Pb_xF!?fws3k3$$q)Yg7hdnsn(DEC@ zSz2@ehDXrs(8FePm%mlq$fEsEa1R1J>%E_P@PQLN&K7$`c>GU&4+6Z-4e|u`{{DmK z9`+^kVqp*PB-efLegNm0XMhRO8^c-2`4faFviIQ2f!4*GL~X2f#hes?D6;n8eL&@6 zPReeq^30qRfFrW~U=CC*R`txrs{E^Z0ItZ!gBj4bm;=d;^*uHR2H=VW20R7Y7OOh3 zv9|Sdp#NQW5JJiFgNX;nn*;HU;VkFC09=uv+2e=J#x8#$ywM!!e`tLW;PJNmU$m4J0D>?Ll^=9HIG*3W zb7MG5zYV}AbpMqPIzSJwA7|?wmxp4`J!uHSB^jh@{rx)++5p#^JzVJxaGhQz2D!Qo z@^%mGs#+%o;%p^1Mzr2Q{2|OBQSl%o4STm+p*Ms+9J=uXyC1*(2iHG#liSm-!~5lH z-*fG4cZT8Hd;cN)bvxg-^M$v5oX~ed&tp z@+bMH`+&^dk$1Na3QWZqj)rh!prS+*nabj@5h;PP&#GYDlFu#ZtiYPN+R{_@cw7XPw++o)Kz1*-Q2&_nM87gR@lIvMr}Vl0Dh-4oqFtrll5SXi*n0_2w8puBq@qZ^ip;Inv8>brFONQU@6Vwk)2_!ZZuY2~hdc zj2X1mKF0TOyv_~jaw9S?%J^iG=kIZ2R59otlXnz(T4bF{&WGXJcb~yf&&Oi*Q7btn zIl7gDz)N*drn-SejEroxVUx+1XByed)l&y*tT3`rrKQ)1xEj%6s4w@m?mU^yNBV`T zmL9Th9zsbYQL*mLgbHqByyVPBgGrN7P(OzK-#W!0)}=OFOR0Q&n6lD2w%^FkldWX5 z*iY#kGjYT+(@YrN84y9w zf=V88G4DKv0fM%E@)QG=CEz@1r3VRxZsQXslb_c+$yD2wwR~A82Z?MGiRmM*QaY#> zvv$u(;l>1QwdT$Q(mErD9cH@-+nk8y38+RF&s5-+Sd{P8sts#0nKVpBs7?lRaoF`^ z_{=GWY^Kw#!DJ=t;+}?UVndT|nV^7u1)`$W zNP!yXnd+>|@u?(_WJFVrlaV^6WXgtqkLK&8d9{ga4X07;6rGvm$Iv{*023WNqqQ+c zGC1IBu*Q-FQ_t%Oa@6H#2ruNxS+ZM7)#ZaUHcBfaQ0#jOQ|b^cwVTC!Yg$0iq>GQs zP%_f&%_B{HI8V#>#)=|Lg;}=PT9gGRRfQ~+dQy`| zbayP}_(|!WZ5B&;!NBYm1B$|uZrP8af7%U^kts?EIxQQmRr$4Lok7)sBg^M}tzNqmlO0lZsjlL}vtpU{@X5^vESD7k^MJ6U(EY_Hm3zc~@4SJ?-r0&_aGLiFT0d3W1V}FR>;uJ#{ zMe>}if$Hx%TnLwCX+#x8DB(atofW_`+JCkH4Ujp|ms(Q`<+ORCk5oXNzxQBYNy??f}htdJeV$HNI_A@Ds~ zE4Nj-*Wf$0)TQX+#BUEf#YY$lg4*sz;E9s7yYucq5r{-T)vianQHsoBg&`BAwH9qg zbh3Nk#_O~(C`VJ8Uds20w3DQ19FFFiM01Ex96iR<>>Q2>P?WrkM$9Y4cCzNW1#l%hk717B_G@i@sAiRR%mTdT9Bg&zgD#zJWoofn zJ`SF{kHA)Ls7g6WLWMr2i@K1e1;(zHQ-aJ@(R(C^RWX}aXPW8mN(KgZYim)+`o=ovv#ta}8=tst3~v1gptfQZ)@T&a-+VqfRhW7&lq7sb;`z z*lQ@ozLlq0=$?}$$zdj^=SF#gZa1BN%ij;LImM7D$Ll56(d@1yLSVB}WvB_>tv2gO z+@KqCHyf`obb;r(*1^>6G*fI&al2C_u5gn`JKH13bS&NI8!Tc?d-ZNDiAM)gL%%of zWOAJ*u2!_7s|xI3SoLApKV2^h5vZN&3v|^TrN^0;9#I@FldpF;p{k&gq)3CIk<_)g z3t0!FKHg;OMhvZHxV+Iw#S>HlmxStctRl^VLKMYfO~GO;o3Gxhv(B6zGqXlh=(F}L z)A0Mn&G()5L#8F!F+m~H>488gEnFEe?O_$e#oDY=#T})@^wRU>L@YoDYL4hpcGs9j zDsa9}G5vBQ(^VR1-Gb-=#z1^6*0Gz3c^+%t!+KhucG|9)$Ex;|+joDs8hk?FbzndlIgG-zxP z|i#^vX37xAZY*1rx--SolOiZ(@sdm zeiIrAL!cGPPQqqHct}Mc#~GTb(l$$O}RU^#VL#^ zIkZQc4ZNFUO83emoE_O?9#bnyQOihf$=46z&?yF-hgg1w%Ec&!CHeSF)?-mdlCsS> zj84&NFO?})qtloJjyO$K(Mb_3TTUT?ku&ht&PpfDS*y{<&0a!8I!GH4TRFEGr|;3$ zRBz5Qlvo|txL&y_qCO1!zpw|4>)l)L4sE^r?vLL6>bv^g*WdZoJKuiCz4MMc;oBd- z{WZ6Z+i%$a`1&)s{+Uijw6Z+_j)+|4)b{`&6! zvOC;;=kC^xAG`6jH_RJPUH^^i-*tU-{fn<}U;FWEUw18c?M*wszVqEX}>zU z_SbJ)+fQx%#@2UjO}4Iue=_{{!};*N(5FJ*9s1}Qd*A*~Xa2JIWB6rDLFh;NH{kE7 zooDZDt$u|UgW^78({X`Q3_+$dp+7=)-g_^6fuR4&o)&q=H07M4CD!RnJkC@HK{%#y ztVV!UJ{(&s=+6eoD;6co5=m&`=vqO4kPgjDnGDXE8XJbz3i_KUOBtFsM48KkkJbwM z@4RyiX&D(#Cv)MquNCwUhJgz@C1wOx4M)}r`mc8tUCD{MDJt3U+tv#DucjoPAgz3c z$T8u=wSxZ39g5Qll*?#_68@rfcH{3G9!!)fE-C^FuhR$n?MA~FydLE-$ zBK(DG1^u0Bn7WpiK>b-Ne6Uu~Z*Uo2$SS}9crN_rwSs{GdO<}RY9cWtRRY9e4RcYRVDC6*RB=xk48Sr>ZZ<`x)pxST0y_T>3WWobpfZO@T=Df z`V9_acwVE)oGFK2wN}t?aCt&D1r^JZSooFe49<^~G6Y@9sd>s;yTF~hZKOyYXHgFD zNrpdf9nf=JQ#ymPibduKHT=121^otRDk`cOJjb%(&si&Y&fs)WkkQ7Xkfj&`Y>S8-y>??1^)IO`MjD2g zp>aKYa~;sAe}*F&U1fRP%E{r~wSsv=r=ftQnRuwV?-wOSL+PUFUQlAfeL~|5NkKokl)}qD@SUy zC<$6Pv<@ibH#mVNay&|cl`8a?YX$uVm(eViCIOci9{S8$LBGMFxh$&dyu}%zKU*v4 zH#nZo(G0-S=XYZ-=GC0YoV-^Bf0SFL!t05{|z{L{|#;8TC?zb2&RZ8YlvVIuzr7>^m~Ys zWAhl!h+tt|yFZTk1y45*q2E~tHRhkoPd5*t-(D-|H~rJiL+H2G3i{{r)6GNZH`faK z=j7APL+DfM=5oKn7ruE2{l+?=Nq?WJc|I$WMou=@A6Alnq@+bMd|p=>YyDv*>F*j< zQ7uy@a9Iz3{<^O5uK_wnGAvE784g+3Ld3t!fxVcJ!3l*|)*p@#|1yVZGR2XZjHItW z93g(Byh5U!m{&A&{TT=G`ywu)sG{H`j)z~lwvB$5A}k9WdkD3e@&SV8=DBY`cTkrfpR99GuZjlWZ` zAT+x6jFa%&&6&{X+A~hVzdbq=8eMzFN%)(1CN#SCjFa%&&6&{X-a3Oz`0eIQXmssi zCE>T5GojJ7hn0liZq9^8*B(|9e!Dpn8oj>GZv1_71)^Hd6&}itd)>-PgnpCHu(e;Ow zb48U-L!+U;SgX-*aHpZs&}Y^eoWF@LJTw~m^L0SueuFy=jjlgcovZ9}8X67#=~|6` zgF6k4u0M{&{RVd$8V&u)T8(~#I}MG7{&=mRe>6@*qoMzAt)PE~I}MG7{%EbBe}+2^ zjfVc)T0y_TorXq3f4I)z{77GTXf*U+*8yD<8V&uyT0#E|cN!WE{r+0Pa|ZV)G#dK7 zwSsat`d_O>)lLZ~hIX;yDsdTIORF)}$e)=R4GnS zm8le4b=&t0VnIsdbjpBv2@oHxeghFbcV**AouyBDsQn#Jz4z%i4;^=St~g`NKb1cH z>{%uikJRWqk;(~wZqu19NSV4NyH)GD!*_UZ&piZB$g`46q~ad&{7Rk`TM!TJGHTP~ zN@4&bS>lyG!Obrn5GA!RpHR=SL8d|vyx*1dJ zqUjz5($4XF7B2dl@Ie5>yD}GDNqO|LchLYTsHlr zB@s=+z#K2~!+BoPTZe4|NAicGDobH)G|Q!r`knYZlTIg1K)EL?qo~4WpS(OnzGf$_ zS*pS{4tu$F-5nebdnDAJyG#wKXX`DLK4PRSxa==k?Qt2rl-8mRA=5`1L(4&8E}pEo zl|#1OOgRW$Lyp*JUj-Fho_@bKv5h(&hcoC;kH*`sI)$MN{uoVwLbWiITr?f{;a9lF z@AmxgD@S4Q^d4tT4LngF8B^TU-Di(dAA z7$7x!l^>oamA^#(_XvQuto24>@QB#+gz>tfK0m;oyWZL@J!q9=OqQQXA&bjW`68Uemqh z?^Va|S}cEQz`UE28ud9cuZ|5xs&-I?H|7&%NRbScLdRT=?sSnx-!59&)Rnp2%IZWf zd$$V$@L4bg`sy+G2o_!<-&^cdJ-^#@n|gMax&aC33vtYNA{M`BJw$TSSdb@4ESk@? zCb}!fX|c+WxhMf2N`q*XrzetCYQ;4C$@itBQB6r!4yz?o2JDUq$YM5Nu9k~YgCfnP z`>`%l$+P((KFc3EhgPwP6D`*&RulCxhiN^vsnYeP!_;t&Kg?!F&{4wDr<)P)CcUNu z$K2cfu@D#f)6niW?mFSG2|v62raSw)@!jx^pS~sCdgGm++yC-?X8*M}K6;~hBYoq> z^`F1~Ew_L1_P5?1-zKj2ufON|?Q5U7_HEZDcYo^c|9H2t_dD0fYoE9CU+$*vUcdG0 zxBj%&_nU=RFn;UC=lzP;!7 zjJ<=Ke|Yl;ZhrO6FTMFiyPv-EPw({ZygPgdfNk~@{=v{c-0YhVzZKnfwhkl+WEhUP z7-iJmK3OEIAYHCTS9^G&S#QAdC=r+0d@7?Bxa#ZvO<;+Cy^s)vtWZL1C<6~-bivG( zP-|M$y7lxJR!YNJB28PpK~1G5BLBI+A6Vk|P9%oed~c9)W93*OE=SE`p&8RHRluk9 zG#9HkCLKHy>rVQVMehAtV2Mu#miW~ZiQX*I;J8>j#ey9jNY|cX(Rj36&)EXbr-sEI zLZ~sdFlURY&m9Gp7zUPbPbAtIPDw{4r-uP&O6Qb*4;R#Ev0mz$MYL!lUBqc~waLVV zZhXUuM1T?egA<7+)*87or$R+Yc7-@$7iWd+5J8(Aah?J!t}sYJ&rbQc)!h4?6N$P} zix7?eh?lWivXoCil8hJinIcu9Aa^j*PZnEEe!wQE?%qdFBx*A@GRaSDxYFyp5L?UF zlunKr6W%iu;sV;;*YPN~8a zqlL7=v=C4-fG>7NSarJ}SfUqLqI)7y?&ab=dBiZ)cHSK2Os9nQ7$G-OxM{J@RZUwS zvctJMY{|Rt4J?rlEMc8Um{Mj+)U-ywk7!+{Q6GTBYp2z(_bQIknX6DX&g7iv6t8q| z{z71hpARhYb0-qUSn4(@0*8iUR!sGWpjZc25@Edr@+(Un@&9M zRo2p$p_C58+${GdschNz{pMzY60-069-%A^P! z7DNO@L`3|bdon;fon&%rP`>A5`aHj+^UitC@1Fg>=RNOHmCEs$pNSLa??V{#0uc*+e$ ztL=?A>-C~TVep!qUa2mt2>EqYwb2o+xhJM*r=ZR;&V4kukMP;gpquWJM9X0;=}W*K z7z<;%S&0P?G+oYiJP@{flQK+35MHfODSD1}iut1L)Wf8`CBveGxu7h9kVdscCDo9} zLv3#+sgOF<^<*s_NcufN;d~x6Qqa$1zWUn9L>B}mFn3Lvio%THP3FEBGrVOj`*_Gpb515Kqa8vcA@-IUJ!v z(d&0us?BUAIB_ZM6yNHnhy-GZQb+5z`hmM{x)jutT4}&-u&A@Le5`9~I%C!1tj^}l zxObbM(wr@wUsd_Vb{T;*@@h zv>$IcgM@mvmeCZVnS3eKNR|slCti`OBz})u;dG&Gk5<_<@7|%EqD?zRtDnMCEu=Ip z+#FYw>N2~{lQU?daZ6iQAxza&Ef;I$Oo?z@DVL^}A4@yMmuRQx_EXSUzmn*fienY9=${R9T znNpkeNV4hx(MtGKwX&n)_Nwzq`7ZM>A2d?TzkCGSo~qxjmFwhQPbXq4#?3)_IUF~D zTYbW|R#v0$M$O8!(;iIiqU*DwkwQy5g@)Sy-xrA>;^RdhijEV0ApDZxeL-jHy{Q)e zU4Dc24zI?2n_J<$$tkhlU>8`gvvSPWn3>5flc|YUCK8O788PG~kbLmZ#?8kuLEtVr zQL3kDM3-GUaP7v;$I>Q1BdYHx1il0b(20vpBWCg_1iG{djJXOrvGbP^C>^%vDf<1ZV`GA9od$X%iT86_jWb7;_aAApshL z%ebqcK%2mrs~}ICz?iEb2MN%)r(oPwkflvv%vF$~O<>GbkcI?kC>?hdq-YZua}^{Z zfxJAQ#41?M?pLQPZB#vLlBY8oyQ1BdDbNzubtSw}Wh>mVdGzC6mM0(q8aFr436j6G z=fK9zaoPmN>M^u&bBs0t8u9x_DS;?$0yJC_Mj;S^1ZemykGl;VLz}>us~}99z*wt* zdjFrC^dr36*pGsb&woBIfzM0e-!Fkv8a*v^Gwm?OyKva{LD48TIjSw#cI3c>Y~hJBDx0kjO!PDV=)P&n zA(Z-yc&1j)M>@qwAwjvQMT#X55*wAMBq%BkOOz7eJUk10R;WLXG$Zv$HQcD?!Cw{| z`TW*?YSb;VOPr<{aMEK@;yZfILWN7I7P06#iv&3r2}<<&P{q@pQImmeGNjF?EM1pU z8j&?y#&{;*GR){}Mg_XV=PdFTT$v*b!MHSz$|Fg#s++T@UHLTOZ=g{SyIWH7r&@%= zZSRs9Tg0j-owy?5^*ilo+iz1BGv!dUk;VL$k~3(U(PbkkrLLp}cV~~_-%UL_@Mk+Y z-P2GV@3b&uyzAA`I-WwceaE9bGZ!2_qJFiYbM!B0`Y&~kYN=ERHxt!brc|W*fgJXp zY9dlE4LhZ@ecRo-Pf#a*mgo~yHapzbQQ5$Y*|Ah6+3T~uzMO+#5y5c z(5>}YJ6643R?6d~%@W+)W~ezUg%bRwi;oyHR6$a+bt{1If;U)6Y-}Sc6<)? zwoF}j&K1H+`Z-IskZ`H=m?4E*OoSofZJ;fkCZ=zvajXiiJWi@dJIkN~j}F@UnMd^0 zR9pX=m_g?PyrtLW+qQLhWMq*^Mxorkt*@S@u{xz{pZ2jo8n0kb|G#}qkjBf&VrL%~ zpY;Airv$g1^fJX}rkC;l5l0j`h@JYQj~@ zZo$;aCLXOzO-2uq|{}~`Se75CM5S(^>cpx4i5#gPNWSS29$1(q*~X48)ghH zty^XdSjd*N;K-;N3Dh6gcys2es$}Ry!Hqu{?o^OnToKIA#i|;GJ64LuYJPBkOptJb zAV3IKCJBXOV`L3msSi?xBvnO4}!=4Mn)`YlFZ%H7OlI;0AXd7Dn)5ATg< zWEOc$+a_W*Qm>qEsq@F1m%ePC!zg_DmLtDG;ZgMgRU%s$IpM^EdI5^$;h&IXPj?gT zkftPWsvB8tr`nMCv)F7TZ@F(Ok0X2r^v&M$z0n$?Z>dF3p!sF(v;=Veq1n z`A+xz6yLc|__C(GG9v0|%PI8bM&@~7kED32KH5DWtV0;NpW;SbRi=%UD+cjrU!h&6kL;8nkBtT zPcdQ3yAy;?t-)KSWHSR|I(u^2sHs*fH5wje$?Man=9%^fo~1cXP#kS9QfSYN%yC7} zD@J(p9C=i~pSHF%sj|`U(8htcBvx&DYzD8!gcjnyq`l~fl-lZODJHLEo1WlDHBY^A zB$lQ~akDrfK07kQl|9dbQe;oN8l>8*lE=W!oyJ_D5YOmZwR|Qni#9ZNpFty+HY9b8 zIUd2RA9bSnQq0>unp`w!(YZ~jxF)1ASQc0a@!e`*Vos+UijJ$}^~RlHW3-zwm}}LV(HGCeJqoS9P}FthT~{SV_}pdR zJm0MbrX_j8zJACTDV~R>M&`Mu=cIUU)7M>zl*7=F6g!4csYSSQIWut7mlL&4Ifz@m zUQLwfhWwp&JSqKXVA`gyFUb`4a#fgOy8de;GhNwpENG<@)3XujEIQlB#;XxEX-S2R zCPgxX&XV15B&|;3i8M}T)K#53qy4BOjoM0KU&r64I4VvUnd6F{ePLh6%-&eH0Isi0 z=G$pySc%KYTvhHj#OxN0Cx?|}xCV=oT~EQQ-?pzWS&S4nIjnE_=8+k$?AaESBA<>2 z;ylIqQe4tP$)`BZdLdphh~vTtRVS|OSr>5RP1EoZq{)#-u{+=o6t4Xe4UU!t9J$jp zoaktB_HqC8hXKs(@b&PX2nUv$!nhDM=Q=HD;usjSO1M7x!)TjlRT(r zqDXEvu(PLWxIxX6Tx4Khk}sTy_H|QySFIYE?{v>d@!e`*XHC=a8Jg$2)xf?aPdEYX z)unjuz3<3859}Ezp4+TAX*kx*^IY&^ytF?joP~Bjk|MiDd}Oj~dU}fNHnR{77nFIj z+sr~s7Au^Ec56~hm%lYK)0I6P#dMn$CkPwtG1P8bic6Xd;poX-O>yKNH=q=^vrbU& z{|6yAAmZ(3+`&jPgh279@I&^12j3pH z>2wq`2I^Qqk;XKQS$oKlwgqbPtWAo8Qyg|z(3Y>Gvl*Kzj^o52On8~fAwAG&yhXgnFkb03uU*;WtA7rWXmSQqj62u zA5f_Ca$gv;7Ua^LSD^;?=XkA!U|;bwS=T#n-+b}O<++XDsPFky_`3aHbpCx^5kKeP zJJ(J;zAj1jfX5wBtWcKH-h9Rv0GDpg>f<5QZt%*Zvz1ICTq9#XgRflhNz${kA%nfI z_|FfnvINdM`m`tBGX@|1x#z_@t~$N+y6+b(&)3g;YTw)AUa|)~>40LpJ(v%h3#M#X zpNiC6v5v+nDJA2;pb{o#Evb?`uPsQ6ErTPB_7$V|?*GAQr+($FmD#(0cmK{ee*15} zT^@Pjy$jdA_EsqhuB!;@@~*JArLcQj z6@|v>kT(tUioblZAXIO5B)ec$0Xm`)Bbtush}u;#~4s7Q^rH(xJsgyxGNp0 zLs{3gWaezEnv7e?WGC!aYl4Ni1_aQ^mimmJzc2XR=Nps6x?jeQz2OkW>O*FZd;I+4 z=N?6tuc@zCcI$ew2Ry-mVn@xBX*4=Azop^s5UEthmu>iBxF(mG4a(idWXDmA$&#*6 z(?8!T@Oz@SzW$TIH_m=Qx$*(i8}5fsJa_KG7a8BW|2MyT_AkHrS&8fc4=5I%!k=7^ZnmH_@L)=SMD)) z_m8i=;)39f$6WFD7M)1>+rNE>>;ca$s*Gzvb;?|8Iie~{J}v>r23&qngITi9x4dDR zSU$ZkcJtlm7!EpY*XOLQFW;1Z|1q++0V*yylL0XDBn>uYAgGMTP-8%&3du@jDW?k8 z3pKS%Z$`C+mcAbAE57g#zfNiQ-Sv(y#O^!l(ZtXH`pqrmX@`FJo2!wxxQ`xq-Oj%z zdq+XV?vTL})f9+sHl}Vw%$TO5pLJx?1sAE56!l#+>e68b($LOm=gU}qjyb=_S07d& zw_K*;tU3%|n>;j(U2JBo%pIFYOd{Jsi^y&X`}AY&>!NEyN{XTqf9y z?1iA>sL_{HG(gOLOk#;k4fY_`l{AQw&Dz1+xwg8Ru?F=)ou!8R-F?OR#J+F598i4I z+=|#@7ytE^^;0tk|LCjBW;S2JJVu&-HBR<|P;oI()q9HW04Y_}J_`RqO#7972oXMW2k?@Uwr+=8_i!ig>}X+FMZndCidu^Ve*b_!Y z{kMu0mzBPJ@p;TI?oyIsookSvob^ci`|CdZ&h>w|nV2SfKB$<6VY#pP_t$+O3cY^m z$+yK$zHsJiKYwA(d3^rG4+_rz$@%2T&t0^Bmh2HwF%2VdU-6b(AFuDX3H`yO;-{{> zhrOkC`geY|ob0)wVjA|*zT(6F-x1d5_D+`#=VrdS`H+JyIG|O0!RB(cpZQaPd1~n8 z%gLS#DyCu7>?^(k`^HHdHfCz6zp^%(Uy7u4mAA`h{r2nM+VlKBZM>T}BSrR{P%#bb zV_)$pZ@hi@^ttsv-&<0iy!ye{_b^5qEAHAkbN3@>K5`ZFNzM6W&(SZ6(anW@#fNSY zF8@>Nelq=Y*~@45M5p#%fAE6!2Tq?@IOI1+9<<^30@<@e#WbvYeZ_yj=JqH4_?_lW zClD7NzWc`K&v2gq$h+TWe0a&J-;GXO_vq#|WX}c_)3C|)6~ElO_~Gos-~W5%)JHz} zv+66} z?cgVGj6C<&WiwBl_~5a=o3v~1{Pq63|H&tGw&GXr@$De1UJnGDo zeqR@0_UV1EKaA|@pkf*(lD^_^pT#7(ytN14L3}Im6{8&~sg8&{cYE=@FYj?<<7<_v zACo;TR7}I7(O3Mn)55P@8IeHE?`>SQd6zHVcj!MJKdW}=%is8m`0d%9$eso& zreTNZE8cjUhquq~1iL(P~xi{L)qTt#=d?H(m8rcf@0@O)yyHB{W3vnC!x#P5rLE&iqW z9`SAB8^l+OF9-4b&J&+0?um~T*Tn^KLcBrj6Whgl@u6Z&JS*N`ypMRfSR`hHr~&Va z-VnVadQS9f(W9aVM0bgPD!O6fv5CKnzAidWbc{$VS|j2KUl%?iyk2;|uq`}Fc(`!2 zkR{k6_@&@l!P$bUfDkAJdro~Y_1x5*QpsdZBV{+s;A`8V({;J5i9{vrGo ze1!KB?_S=OywiDko`Z+-cILjreVThK_hRlw?lD{qcbd!QY~efz=E<`;RSvJBG(L&N{}B6k)p`=2bUB+Jvb$%LarDhRU+RVB2^%l z50T=?cZNtY>d|DN!LO4v{L6lZHqY$O(H-9y(z4A;FIy zBE&Wh5z3DnB9t9FL@52z5Fy$fTtRs5A;t3UU6ZN4)ZlFFC$o13*j$B85V92%9ha9<@Gr7}* z3}@^DKQYEm@KcVw13zWR+wfD0ya_)g$Q$qzMP8?p%aE(6|AMan8vIluTi~Yxc?Evr z$jk5(LtcWPa^wa0DMOxvpHk$v@Kb{P27aQ*uPL=sAdr#H}tmjyFvMyuwSV@+NwT>lVzR7%?dBeoh6Sq!WJh5@& zmPK9dN-xO3~@eE$7^R|yQ9M`Wm!K@5~i zB~s+?L!=Vqy&+N*d3WHDCY2mGI3=n={x(FaME*KNszClSM2aJC4UuBVpNB~0$e)Ht zWyl|gNTtZDL!=Vq4@0CV^8W^xBf55AN*O9)c!x+)hG(E&&@Kk1l&Vw=_YkR);Tj@U zFq}iAIKweSiZSd%q;iIBh*ZX~4v|V3mLXCJ!#qTaGE4)@QL2&*PN`Hej6dHV+>;@XB<3)m5ebvL@H$*G(;+4%nXsDjCF&{5g)<{Q!$3jZkUoW zc+8e!3dRA0Ibj%Mu(ec*$r*#~hf++&7;H0>Vp7InOOOMy8G8+p${2eNkxChR43SD0yAP3~j4uo> zNBNMJEK@O-57sMFGIkpxRWNoPBE=ay50PSwWkaNLhIoin#t;pWN*Tf-QVByaM2a$| z2A893NS~Lg82rI{rAh{Gh*ZJg4w2#v&JZcaU=NYX8LS~v8G|`QDrHO#kxCd7L!>B! zF}NJ1>jtNks2Io)sS^3u5UB$BaEKH~{y9X7As-Bp%8`Ezk;;(wDgS>yqCrFj!821v z-VNMU?6a8vm`pQ%1%5fw&!%NH0s%~6Yk~?+kmNu(y*$GEEWF#ALW4;jalO40Mo=><-ExS(N3QF@ivoW5JX4ERP zPHzP|yTJnZd#y$ilaiePCD)-Tx6#muO67HXR^4_IdXUzVv=gbeqpNP&2t8`=mTOU| zBONuSn=zRicj{6tyBg*pg?X4fSPXY#N z*=8b}mYhT8p{@=0<>23jir{ahSUPSZJ0Y(&W7ancm?;^S+p{gB+Z$F_%#PViFyzZC zt$|`5mC3=)kDhqI-ztR+UW>+JBCSD*RBRgxW3m6-Bq+@vWbymgP)khUa@?V>$fTC7TRZI8xa zv$-ltEs5*V3b;m5mUSBlySr*BmQxuGu2Z;+A)TSBF6nGukI!ZzwC<6sePu{#pm1wG{C1IDPwE8qwu1avLOHxJA zbSF*NaHT@lRJuDcSH98on8NB(z!`Ly3H*%F+d(Nh1jist&VUVKFRF&w)R3!#q zgCu+kzqX$B=P_3x53Y>U1sACjU9uQU>+6B2&flnErVu6x$mNo9LLQ7%x@t|%jmhvx zx@!%^v+9!5=F^+Zgx{wgxk|oj+qA5{M3u;tpWV5n>l|B^Sd!iV=5fp71+UJ~C9`I5 z5!<2zm%H7{1cEZPD-22%jFG$djS{gB^o^FnvF?(XbW!z z;)Frt1Z$8mxCfRfwd3a5POIW{xoYhqxURQVB0>g(TT5E~qt7MrT{kVOE>$H9BOZN* z2ClAST+_Q&60(6~Cqyo$@aFOnufBwnCiP+iSIO12k?nF?s%~0KiaNOBtK|)a^r2t~ z%M&`!ESgN!6h1EzLt7=2-RRaD10nC|y~KR;re&3-s$^lTm(Ns{jBDW1mAk$|Jx9tk zSqJLwIJ;>}w9sELZ@UQASaPbEAyn{ra7+=CSFM^t$p$XSGiuDuWYy`nw;LK`I+r3b zWvc|D>8Z7Dm(@M`Tq66;re)=&s^l}g2=3a))k{oq)I$V2{%qbFH_989LQ4|#%NOY- zS`A*7B)n-?ts9Wn>oLOD@Tf{UgO9Le%+8$CT*W)`kdv%i^6rww<@8xKdb@`hc`jM= z(xzplrK;pJya?{v$F&GH)=df0;B%uA12`M!A>vuCGh&sLO}dJ?rSzpDq}i!(wQA8;O|FzFDxR20t&Rli*?g_!%GKwRYOEgAtM$rC z-q>`x)NOS$qr^R-LcS4KxfE@gVzkh;@X?PuZt|2`&j7DVXfWtR7+SiajMxul|10|; zjl*_}^}|mBGyRwP-^T=~Mz~eX-jBK+_UU#z39cG=Q9`ER2&!^3=6nc?1uXT(j1Mzg zS|Au;J7q|w>{hR%DUa(lO_Qp&8q2`oFT<(V3q&fF`_j0Pg?7+_zL%?}cmdZRSJyr`s`6rHZ;Y zV$u1~>&(b!iXn{fsiWSxYDPw;3T-tS=~e@tXsN6+m~%5ZO9AWZcRZAP&=toBhrwsa z8p?559!$V8SJ_;xI$I%~S7Dn=H!@bWJRLF_qIld9)0$L@99f(pm34=F##VL&yd6W+ zSx#0<8n+sqv&O50Qaf75q#hk~%+1-Jj_R0Si7}>WG`F#JOvz#$lg5E&YC@rN=h5Qp zOu7rz1s!ec7RHcMqR%W+&e_A5Mq3e+L6VoxC^H>RG~c#)GS)QMq?u{qwvwqMF{eCHLu0O@^mPIi zm3_4Pl9be=gVBb1|6hUJgNQeZJ`i<ZmH*ZEoA7G4tg;72(xaE=Bi z=|iliStRpGrf2feNyo%P6Bfq(3!pp5d!aG zWq&4aYcJUGIKH>C&wxh6H9A4mMq#jTp8<`~Y;>Znjly6>p8<^+Zgj%3jly7`J_8!T z-sr?{8->B%^X*{tsBqIX0=H7(e@9o}aKtt;^jhJzxF!(~B0S&Em?r9l?!SX%>8X@cGT*5L6gWdWJXvDUo zbFIoK40i1^pb-R*&V?zXFxaKffJU@DI=7&V!eHk<0~%rV=-h5H3WJ^c3~0pPqjP!5 zC=8bM8PJHQN9W#;RZHHYaC}ePXFwzH9-W&!Mqwc8GoTTnZ<@v(9iuQ1&ey`|q5SCF zy)g;{L7xGQn19nW!uXBCV5-l6MhrhXw^A%^hB&^*?=zqg?2pc!6QeNT^%>BJ3OG$8 z(BCKwxP1mRw2pU^^7tO7&wxgp!SQa-9N%O28PM2L9IvJN_#UgzfQJ2hye7Hhd(1uq z8iqbPwzZ{OBK7`1$@mZvY+&yJK0g2XKUD&oztFQ#w&t@AWsG-_-*)d6QvoyHA~y&t z6^q%LY4inXYo=?PrZL_uZkwhtZ7jC;+P@X$Q?g1gbZMiq+1l@+pLs2vDFegnYHAmj z`is%-1C~;My6EkrADL^ZdS1d@JIENm81Wr-s*o!c12>W4&8o9u4x01X6zMaBl4W08 z6-qftO{b)iV=|Q)7|DF{)r=5+LlEzpJ3u)sy zX*KPO1&Gx>~DW~zk@YT)Amw+4!o zn+I@GpE9N z{L(YdwnM`}_bB|cJ~V))PG>4!d?=x@{`-#)CEINSgC`z5l+e9geCfgD-?&_Stb>UR zrF_5^-6N7(bvD^dTbFmbw9-OGZpWpHlEkCPsk2$IEi;$(8{&}!o-&wr+(Rp7Qkj$` zMczqd?HMzst+gekKvAJHMT<#ksnCqe71d%T<&OE&Q9IbXmVtvFx~iq=*II*ytV1zV z)EVbw9>314Z3Sd@SKH(#bt4WY{YU@rTrR#{^P@(;9dP^u$-t1a?M3Jxdmnv@my2(| zy}KBbPZk5CTrR%7&Y^vh-t;aRIKA&ksZ*DWpSo|)1bvL`jPbrdk2r^5|BX3>uIB<= zFMYhZf8=U9@A-?(3G)|OQkD7N@-2c6s7J2e4+i005O!`>x?RWKK=tg8mXHeYeTgOH z0>@U6U`H2T}l$L_)7n>zX>p zjxQ9`GgzAps7%^A5m5#5&T`a7ly!Nu8pIn;YtCE`#C@hx-bBneB52j%aK`+Gtk#lo zX5zIOX*1mnd5SGPj)fdDqYYEo(sk#IPCe2>k$Uv`{r^ekB*IT~js+i||9oBopO?V@ ztr9q;(eqNX^cfw-c=z2F%+i!g_aa6h)uL~DG;Xw7{LPB4f(0wYPu68yPoe>HrO-7S zqB)b%?J}1;t=s}fAZpfKdbPChbS{-@@S7FARUAHW75C8$D+ReNF~bfr(kw=N2j^au zTqYYh_nOT{O{y7HC8B0uK~^(oaEaRoj?95Ga8|;jtmQ~rMH_$?0AbTk2ieOHa=O9QomZzIr_J%EL>s! zhdKxBk4@lPn<-IlrK{zz_fXe3m#WnNM{c`YuR1An;Szm<%4SE~x(ruPrqM;$nVCu_ ztDVW|yy#p!W(>89{zM=z4HQZmi$P~@ktKP%;Y>DdNkYEE21#up<*y~@a`9x*Fr&@8 zEKNf;V6dsudOW0!28$U-Ug`DPVkIRe4XL~BLZRGpxBYlB;rFNlX-g)bH#ZB`T*atD zy`j9S*s1GB^J-U7j}F@U8N2q}R9in!!I-Au1hMpn_JX!f4Q`7~GK-$Jq~Tpa)jpk_ zc=-1;JRRWQ)9?ydeA1)g+_BiJZTQa5yOVuXX7 zgN6=lky(C6hXScYGH}|{Wql@R)Enz$E#Qu0O+JNXrE{ueRA#A_oKe49ZKx*I)wE8f zFz@hCpqPoIGGsBK>C{ZBdT}n3wECh6vK@^_>P~yvsV%y3pIkDdOe>_3Ij7l?Q3Wft zuEE)_IDo@N^z`Llbk&$~!?%9+=ueB4s4piG3L9M1Hb=ON~BJRyc z%-*~r-INw*y-`IiU8vOx`D~+_EM#=ayg}w8dTpq-IfB}hk!ZJP{nlVtns0O!vWQhy z&@?k0B?#zLwU@w!+(n7m?5^bjlwp%TZF5uO*QXvRRY(s_i9jWWtAq^ z^*Bv!ojPeXcY|ecm-)g57AtBC28+yrYFyD+)?AHN6rq^I?N7%OAy+zJO(kUwX~d8X zJLE*K3e}cJP^%!?a&NQ~wc#=go=I!ec1c^IaFRIbB2iDN9#^N$wPeL#@PI<7K((b2 z)OH*(Z%pGeR?EsV>WwrBEvaZ|Ez((=UlA`>h=R1H>!P#PO2|$0%1~`_1hwUA$*9SM z8(yg?nr#|v@`x%IXn9DzD&CB{h?LKts1b=&-C73Sp#;?yMo^oMV#Yun3n;7D*|N#-Xk1hE2NdeO z+!w~I1-UfmRj93Mo7Y+h61@Ucn;Su`LRm_C^BG^DR3v8g@epb^c;(UAN~RF5kujga zS1$M@>Dk$k0Sq;Hs5U!-TDv`%51R|7Ea;_?nk&}PSS6)ooYX7B#H=M%lIOJrX|ZK+ zgi)fGgK9G)sLh62c7KtO<8_}aCQJG4tx{C&^96NzSJ>N9*uAZaLgRGEn+BjZ3)QAa zP#g1M7QG~|AtRcOJ07w+(;>VqSE>D^*Vj-c<91m~qK_&`ZyQuu2C7Yspf=ET)XWW? z%c{iDfZ3!%8wwEU!d~zw!06+Xt2|L%F{dp@qi)d4(ok)3>60SUHd(i;3+rn}67^YZ zuDaWloK2S_>6+4&*5s?caH}1%gv(8`3JM_w)h0$z8x6~Jcs!WRYTEX$ETC$qomMhn z2RHMiOf6%o?u(FW1)7bT8abkuglgj>sHJhbjOZnx+SmwcX&f0NdU2>WI)Yjn=fH?w z462Qcpq9qrE}|EOYL6K~?d<3$wuoK?stu2zmc}tGqIV2bd-MouX`HPhdSR$`!w70= z9K#}dM? zd{C`-1hq75y+n_IYCTIE!swXQh@Ka!b&sHyhE0j+d7xU?2x@8A#)zI9s&$T_mWD}> z=((U;#|Ua^SmubH6RNe3pq9qb9-`-fYHdq{Y^mEn+ES-6bSyn$jJVr@F6Q_~W3X9#cY&^gfp*lbx77bDy9?v8x{hH>eKB+K z4(=@|!Mz9rquK?`GiAT6($HHYRxGHt7X7#_?~i#}T6HMh${4MMd{yew2P^-Et8!M; zor`2G8h^CyavM#FdaDpm=Hk|f)D`!-DtUh=t&$f_T9x09yAm$6Uc`k_8srv*;4-xb&eg~ zQs1ua{^z#TK}b{Sz%$F9b4J~jOiTSqx91_WWM;MM_0i}lU~eJuVmC6tx2ygY0Ob= z!M0#mfhwD&xAYb}<}$u5b=i_z>JnTc68purLd7M>Ryuy#Y*;e z%Gv<*tB z+F7+$G^4q3&p-NTd+H10`%#Uo9;qk7xkQI@oucYs>-MulfkKyVw%Z*5SZvq*KeGQn z5)+#H|MWT~y_M+TLvu@)`gXiN-v8ehi6G+RMIVZe6Mi84lHh$oXX?GF7XMv-gZB=v z#(kSx;k?NyvEN`9Sg*5k%-5Ki$t{zqiB~2PjF%ZPU^~YTWyV52w<|^2QHi0o$!OoBXjXjWYSHVuS35>Z4meD3K<|+_F z0yLD4y9z|K35>Z4gpdG@8E(916E>bEfCOkbfsZ!^+s4zTXcM5ZS+%q&Y2#^p+62aG z%i4Gvk2V1s`)Z?<02dOVv9&nvHo&1xV9Zs(rcGeXRltG-XteBcR{@hYfiYLXBy9p? zu7U|jfQHg>R{?`IfiYJB0twLABBOH#XM}-h^YM@XjSX`;7R9Aats6IQq)mW^33O=! zd<0?K$vOcMzb}4A{Dydo_$Bf0#7~JI6+bAxM|`{ZX7TmntHoD{FBM-TK2Lm>c(eE< z@v-8jxGc_!V)`(V$_7v?Z5{bB?31FjmSNNvzHQ~#`=Y&rS9~V9(yjOUq@K)gs!fS8M5L^mQL!2i#OR!mRlHgcDQ&1LU z1#!XAf`Gsyun7!;BLwROa>2oZwSv`xy#>n!%LIG@bLvB2M|m5Zp?GELg{fzzo}7Ak z>i(&_rf!?MY3jPEtEMiWx@79YsdJ~!oH}LdgsJXSeX2N>o{CN#HAPOjrYuvssl%p} zQ_`t}rq)cYoZ540*D28ycWQ$F0kF)x$$yRiGXFXL)BMNz5ApBi-^ss~e*^zo{+0a8 z_!sjp0OvJM=lA#<`E7ocpXVp}$M8dZg74s)_!|Bpe4LN+*YT(MEBL$fcjgQDY(Bz! zAJ}@{0QWt-#QPoZDc+;J2YL7KZs*<1yPkJ7?+V_fyo-3}@y_CH=AFblme=H!d0AeZ zcQh}+^YCmu1MdjldY+tjFmEkyHE(a;a^5l?pU32W$bAo3k6!1#!hM1J4EIUy!`%D1 zcX4my-o(9*dlmO`?j_s{x#x1v1Sd*P;C8unZjqbjM!83ENv?});p(`Dag|&t_aN>X z?n>^S++DdME|)vO`GE5-ut&Ydd71MZIEV5$=ONC$oI5$Ua&F*U%ej(s8Rufo1)Q@v zr*nFojhr^8%E@z*oMSj44#9D7OdJj85Dv~kIqNvnoE4niIXiO%95x4Gzt4ULSh%*Z zUjp}BJjH&L{UG}u_U-JO+1InLW?#X+lzkEVJoZ`a&FquFnU^NJ%+9jo?4#KMwufzF z8`wv%*R$p9gV}4@tJ!x`}li z>nhgetV>uIvd(3l$vTB~0;|iavx=-VE6O^GMY3Eh3roj3jHP5rSqHJ!uvW76WbMil zvAC=W<_FAonQsCs+{?`8m`^hwXFkNdmw6}iR^|=NYnfLvFJoTJynuN&^K@p9xsllh zr*iVlB=Z<%h)FOVOcPVXJcNleQRX`4G;;-WcjnGa0h0{|f$m@v-sh4}uLM|=!U--3 zk$~uh=z-{h=zwU4XoF~hXn?4PsDr45sD^kr#KRyS3h|2&RS=aB6%a9qGKf-$5{PpU zXCWR0aR%Z#h-)G44{;jeeh^nfTnTXn#C;&{4RJ4sdq7+caW{y&Lfi%7P7s$t6hjn2 z6hP!d^oP;<55uuRtA;f<|`~c$n5Z{CNH;C^-{42!2KztM8pCP^h@lO!{ z2=P^jTOj@c;wunehWH}H7a%?l@i~aUgZNvB&p`YQ#9u@F6~reYJ^}GDh>t*g7~=mx z{3XN(Al?u0K8U}7csInmApRWUoe*z__*00tLc9gy%@A*bcmu>AL%bg1k0AaK;?)p; z0P!k_--q};h*v=TF2u_rUIy_}h~I*E3B-#bUIg*$5HEyy0mSnleih=m5YL8q7Q`<@ zJQLy>5Kn`63dEBk_8@M8cp}8(A#Q|t9K>TGb|JPPHX$}3)*)6QRv?xkmLL`&<{)Mv zW+0{@CLqQk#vn!@h9MpeaRbC7AqF7^Ao?Ma5DAEGh%Sguhz^K0h!%)uh$e_eh<=OS1LFG- z{|@mzi0?xD3&gh}z6J43h;KmrBgEGrz6x;*MA*mKzlYyng7_lD7a%?d@wX74f%qGU zPeXhP;*$`cfcQAX#~?le@ga!6g!mxD2O!=D@m`3(fOrqYyCD7?;++uhfcP_rw?X_V z#9JZW4DlxrZ-RIu#2-Vv9^#K6UI+17h}S@beV_dU`2G73uY~wLh*v;_y`KFY`28}7 z--h@th?hY8CPX*_urGq&zYg(2h+l(vKE$s=JQw0Q5WfNujsR>p0tAXZiEO&0CpFC??7xrY(j)10J{diS0Pp)mLZlP<{{=FW+0{^rXVIE zCLqGmfE|I~kAZkJ#0?OSf*67rfC$F{HVMDOae(cC-`x;h5aBq$w!!aKh;TGuo8Wf? zL_I_uL=8kW#3LXc4)IWka2#N-hu>8Y6%a9qa762@&>v7VPz`SK)8AKzs$_|AY7v#1|pH0P%TxZ5PuEv z35btFd<-J&>8yv~_g_MUJ)QLc{C+>g`yk#6@fQ&9fp{0hJ0acy@pg!}L4-Y>bu0XS z3&fv5ggu=Fdphd|_?zn?{s`iAl>h(UlQIOnr5w-t0`q%J`Q&%R1u;+57Kwyc2ywx8 z1cy&uJEi0QnqT7mi+3h(FYf8wHJtM~GvMuroRm%6IpJj7&Iln7AmMFS$*lfu0uxVe z`|HpD_jw8Y?=FGDrY1Y#K3s^5_x{BB8(g=)rS(MNrei>c2W;4Fytc>r4Eyd)gWU8M zdJR$jO65&qz~{gNk?{ugo#!(jzjoUouZ2xV&zCuk`)Zc9ehw`2#HI}(zd2CHW3{%< z=l6+kTXxe?ARiUThsGT=BR|&yf4|hGBSB^|kQt3DHb%~D>#K(on;P@QN8_~i(&v*G zDZcuA@zFTGJ#s$V6<=+>_-GvJ9yznEix2epD#(iJ@iY#4kDS%;9cL4 zV#BIl3~0P&z{>`PE|`Tqe!rJ+8FfCpU8imbrR`==YCt1?Rl{tA-)j-0svFmp9WXEi*=M(Ny%dXS6g3^4>Y>ell8MVr+(_4YgZmagXqoqhSo0R1-MJcE2*y05rEb7%C7xfPY74^c7 z7czJ)8jFdv20u$hZSRh$sCD(OgYcMbCbDVCIb{gf7Me22ypy{(z)Q)it zAjVwRX&}1}e|pwZPU(ODqSgeR(YRgFEt>JDHxP3r<$13bPvFT|Mje#N)U{I4 z={3|7m5jqp4InE&F6!?OD(Z!XA)U?Z@!3p-*8N#3YWuhb5K|tFn>3|_x9;&8(w2m= zU9@Kx8>_Xtwnt;I*<2N+mc;dFMH-gLvTg%mcULXNaw?<2bqaSe1jckVRn+@_T+~+% zD(Z!&6a&tn!(=e{!Mym{nHEtyF{S}TTazS;hP*Xz$p?!TSwK>%dGhAPT6Gn*B0YxoaXlJq*Y$jvCWDR&fLq*+nj;*LINpAr2xMlGI zSkHCItQq&Aiw+>}b}JJI%G9nPduBxK8aFd`RIcysxPS6{+O)1r&LlB;VY+vT)W z-L#ezb)|N;9K}EgLkVu3K2Dg^9`aet4 zV%IUQ#h%_>jgb0wB%5vriCD|!4G~`LVgrZ<54vV!5+B;aTY)%X&^W<+Ryc~fiBdam zp6#?MPM53JE>@K_Sk!xdT-4tgRMZPiGhUsaFxqvx&(v5A*1}_&7WEpNH|q%|0&bZH z)yxKD9MrAGEYL%$> z|5b<+5m&@4(Kkc~3NI4M1>Y5@rXHL+hW`e?!)Nna+%4Q7=P#T%`#tt)?0s3+u$0VS zF!9OnP3}H%+Qfd0^B8N9TYw7cC)GQYJpl|=iHR>R9n^W=Z1IWtX_`g^?R@VLkdq4J zw0`8A7T9g4X++k}^u7qPlY#71BWJg@y=$6Axa||Cdh0=E2W&`8C`Zn0sO?t1a9uM^ zBTRR!r=r+^M^}u@rf<2OrV+IpJhXEDq4AM-j)p$8OtpEjq?T zEm$jMXXV|3)I6IlW>AmaY1I`PwOl;vwc;JMAym}bvzXKe=-~5o? z*?#{`GEF1CdbKA5Ssr*`VnQ-*F`MLM(bQDf^RqUm$5L@=uxvbTRK?})N>mdp z_YMSXr~&kVk=blNfTDC9C91u(AWLchT{Ci)iw~eQoFz)V1E}W&O@IH9GhJi=rQs`) z>a77eQUmDp$T@B`fYR`i$oKXKIZ*@Xek13!Z~&#@ERpF=gY2jQboI#DZ8Lz<@Rb0) zWj~M^HGr-fIkVvd=t7eS9e;{gZ#BgRJo?I!*~|~1G`u9hL$8{D=zT}tVjup{3*VUO z_*0a6D*+p->#P`=%_3cghQCCrw=c+%>N@+3oa0tqhlZy_rnds*M78C;N6u-RwoJoY z0#xfh^VPc7$e9hV)`h!XbUZ5flix-5Vk~1!Ts*Nm=WuXB0A*gyT+6we@8>yrPl%7@ z{!;XJ#(8X|s3Cl7a;2~!cvX;?`aRb;6=p2s|CW6l^BU%X@Xdb=FmW-`8-tpEOT~#3 zY!I!GJ29=v;5D>XsIfpZnDghvsO2_9GBF!rU@)-$~#L2 zOS!FenUda=vm(pu?XIpy?F#ymc^N)imfGw=TUox!Wy+D&fVz`3<_z_&H^OlOD zynlYe!BVpqJ&|n27Dn|Xcnva`t&w=Zs!e+2GL<{r%w=<_ChpDaa?=U70@q?bcfzWR zbs8F7M{m_Qg8?kosn%?MRl@AY6&T*G`@*?ZmY~gE%4NaTT%mj+8fp^B1sphceau1K z)rVV}n9E*@n6pxAFzt%y6alYZ=ZJa}ot!FSB$9STF08lZrj`Gny*CeZTs!Lo@4i=6 zNJ4mlKwiDvY*fs(WZ9AvNFvMfB1^VxN#3$JlC@gaVoUOtNysDaBrkywmVpdQ6QF_6 zFwj`->7p+%pNeE&cH=eV5#kiA=Q2nZTnxh+@ z*sPJ}2r3T|iI$Aaz?|rjnxJ%BWPPIZ7^hcqrYM@XbAeat(|)bV(~?zgg0#mg7!FRJ z#E_~>rH(+xRHAHFRehq!5{-@W7=#HOC}xyFwmwqo!n#Llr-)bYaj}NfCpD1u%tTUy zc&*bFA|{!EpuB`8^kya*Fm@Cd5`MWdx+OgGq@fd2Kj1?2xxbkdCgO6$}TByBOG3Z&EK z6Huits!DD^B+E(5O+gjOHr*sh(n{%Gk)kRq7>?codiCF( zVG-?EnqwDb6_0mVN}i*0+(hSZu`!OPprrqL@ z98at?uB6&c6~SaY-J$xirXZ8IS*@7v=z2bd*9-k|kS&Z?n&IT-cOM0Tvg1H6Gf2VE zCe+u3$Kx+{K_~$8ez#t+E}Z#Kbu-=*Us0C2_)- zoUkJ+A4AiPh%|~=jZ+75h6OJ*v`N_-a7MRQLJc;bW|LJp-^n_RiWVA1(AHaR^S00p zK)O$m3^Z>S(tM{faaS?ucQH_j5uJ-QlvcDou~HqeKvu+FE^o44-wu%Ikgm6L9IV(3 zep;)i6$p<{ik4C33dNF8fW7XdNQxtzAf!Z@&5^BctXEYt&h1{u=#*0`GxP|SuWAK$ zb-Fly@m&mAs0CnEm|-lR8KRSRyIdKh=>Z1SY`+L{DoYHm zwrdu0iX!8HGzc!@=?z0jVnwkU)C(~?hm4DLF;!$*LKRLIlVw4;ZF!8!$#fncm|c#; z?Y6Os;iGpksBWOPJ679{7Z4@U_KBJ|%#M=@B8T~IzceYLnywFl*DRj`#}5Y-nWi0` z=)(rt#N=)|?P+2Qjk1H7i|Yl!h;kkeW4FOFL@*mvzL_eP3_hT#2ioDAAK#cSX%A0R zay&z7EfX!$C2GRPn#FPwAk=8doaBsBzk_7P*w;X1um7G7ND}CFYFX&mjYzbKS6T#}q4Zk7ppACj zNy52u5K((oc-9Ii*++c?Dp4BByHpq}N6Lz<-RMWn8ljl11QwpDJ8CqX6Ca`Y6@9-hv! z5KnqBZj!Vhvs-l{HM-s;6NAKrv!%K&5u-N5k6K8q&fG3>Io&t2LdO*dkbgO^cUIbA z<2UbO5c1&t5}#;9$e6*RjV9bln|vzo*c#K0YuTJ<*Gef&FVq{SmNOL1M$(EljYe6> zaW3!Y>Vz{Xv)mXqQUz4T7^E>Cd%fuG!I;RX0#6kh6qgp|jLxrM*!_;X7`RHU6dN>^ zxEJe7qoQ6PjN`Buoe-lEo<&WwmZQvG!XIVHQ#H$C3PA)$p-vlVg;s}yTD{+b6dI06qRe#-75XgoJ=LR>ea!qw(L=PaTj z4563>R>(s6w9BPCv3{~qGom_F@7Pjmg2$?9vo3YgsE11v8%g0RKPfdEw{vYKt61$y zpSEe>V;)>XZT{NP)(>sGcJt#KxBfL?_Y-ct=;o(x{>aV#&E(C^8^3bn8*fx^eEG?z zPJZM7J?Wn$Pd1MK_3`_Uh2s|;{oeK?NB{JwcZ41O&F04s|JC6)9P)q};6EPx@PWJk z*ZaS`|MmOa{+I6khpqVDhxeSl#FoALSG&Ko`@Y@E?h7~na_5tPXTaWxZ~x`?$G6|R zUEccS)(>sIceA|t$j0w({NP1<74ZF+U-b89{{~tB+fiP)bxIWB8beV1NjxgViZW@K zbet}Cs!^~RX(|a*Z;MicZQ+XMc)AG36E`PIMV*Fwkun^w>s8;yvXVbe>au7xy0KiY zr&Dn-W!4LcvMZBI#V=hduD^_3DBe()imfV2_;A)3O=7jWHyLww)f3BXz=#oy5ad+5 z?64^*YqXb8Pe@SB-ZT2Jmq>F>S8eu!!8qFC&`BVHjU>`>^0|(vAl|4P8Md+oVk{YT zZ`n4#zf=rtz79vFo|PJws!0-aygVDLjnjD(>(vLPQBIbvR5O$J8wT!T*MI7VqPmz)jVpI=<>qi=I<;O&73>b zXru)y-^*7NJJQL*qe(s&@NC@n$h?IWX;#Kjf7z;gZ(AxFV1E?#@HADdOSK_x5pgOq zPNXV>EUK8TL@$8Gl3XsgTJ43!edk;;-A&ZfQn8%29c?J3Py#eZqHAJEi4~fOCOl9j z5Dio$qsvzPz*13z>W1c;SkxF=ajQ$_JC$Cn<QNMzIym3?k=^bM(hcMOjjZm5!FoI}wIxHe^x9 zav2+Hc&(z_x5|D@>55~gK#JJ1RsVXaC|TA-C8|)flTHpNEm+O?O|av#$Wg8^&w_-G z2{qPA^UHC(UtKDS?Qy0JwwVpfAF<6yJKhvC^$a&ijY>?v-Y5J7;WcaVuCwf`M;}`% zHZ+6BEj%OFtTu;_hH#Lw!O>TrDClFQk?xy>&9-s@*wQYbZhhOiBAA_qLA0)Avv{TH zv$7!=6|OUhG$J0Ffn%BGq=l9uMtRwD4!-GJG0B7YDZ5@2XxknuSU@^Th4C0HnwmI7 zS@ujYJiEDUgjrbJ`{ku#%_=xCK}W_NImo#MeuDdHc-S4tk!&^Mxsi-ktxkl3US7JK z-LE}Yq!La)5b8Rw6ng2@V3fwe`U_kKk2X@7{y5_BxGnM>(Ek<|chGaiSl^={Su3KE zZXurWEwD5h6nM5LYELXnI`Ec@!-dE-=#lpA=x6ie9z#@@P>+9lsaPCDt3Eu2OBK5}YRZyT>p0?|PZx`1t6$K{ zavEd`)VX0{IXZT^b48gmn|`O$O&fGRf+xgcRIim;aJJFGC=}cQ?M9O&x-;{ki&lN- zQjr;RZd06yNgU1AhKvF7fo1c_JnUE$+mcBxosj$+m*mC2{# z;>h!QY@J1l6}3oP<5aog+VMyn<_%g;EPc-Q?=BVdO^Kz{B+<|Gb5f^W!?S99B2d^M z)2Xz~A)ctvjO@6EvK)`c?ou)5mitgw=ud>U!8ND~1{E`D%v1BS>Dajl3EMrA>&IYv zxTxdy^js0D300$pnP`h`X-+j;N;LCCOjj_l*?}#FIQ?V%EnXGr(KH zgeDCnQ!iz@0XQ=8a94pKi?o|ww{7MP4G|U=cM|7{wnWC_36S)d9sLZ|9RJ3AznuH)#9OGOl`5@nAZ6OH~PZ7>tHl+UD* zSQUda<8geTDsU$?)QEIqIjr{oYN?ng)=QN}fCw;QRAMgCO=j{6)~`iMMh4C13Jp(A zM0BQLFD-7pcc~aJAquY9CBM%E1IOc|dNJ<$#5ky=FbVF~hFD+6gkBfiB%gQk<+g`G^?Fs#-i3EpaXx z!F!!*WCVftUm8)&R>g)C?dPL9x||<3f9_l{#z66GGoJ%n^lDepY>`ZRV>$tWC|@tx ziIKyIVWIe#TXul0uRm9WhApI$(ZJd!3+CQdPwvucLH8?ltUGDp#WaEUOGL$~o)6^L zFBM@kk0PYGDFrBV;X zN*4-po*~jHHLGWQLyuA|M=A!UD>YN2Cem*$dro<2FzUrbNr^{rxK%9W6gOAI9BS0i zOi3U!lYX~37>#(TLUfknc<=aJG1`p}d$|UQV_GGxjRS5nPBbcMtA)D8c$kb0;$YpB zCDCOS56?$cBnvj%#ej(yNW!o11B*|#Dp*3v=6foiwXFQOQ3Jt9uDMh^C@rd(noQhW zj27MUpqQl;rBZ4Lqq54@Nm@xjV-QqbL&s>e-%BnPpR+)k8WX;)N1{Z=92UEfR;-%^ z!KxF*G*qNC$K^&Pw1FPbJD= zjRI!$s@8JpaP)iUik#hcIHIiw2*qQy(!e78f|C}z8Mq$Jk%fK^4$8wAQabPAZ$4Md zBp|!ZDC{I4>57)i(T2&vh@(^sY)xazF{Yl(Lir53^y539pWXl8-uae|<8MCrGVsr{ zKhL(nvn}v!3w$A4;N3e@c9z%r>%V04;nM5epVvA8&(q)b713%reOc)nwg!^m#*?v& z@>++u;Wf*m8-_$?E*~NV&c%7ELtJ;87qF72`oXJuGg$M)@P<6g4SfAwwVA^8X=qn3 ztIf3T%{hDl5MUiF<@$;|PtYtYuCLD?6i~b-_w=mnrA@JIy<+ zo$K5AijQJY2QPzNNRV-Gq?Brax7-P^b(7S1v^!LJJz9*{t46HGV)fA-oA&y>=m1|U zr}Vt)Aiw;(ylH7R4&N7SK3v+VHOC?R_i`MDaTIR?9c*PB-o3#31sZ=(0+;!OqdRBJ zQyz9BqA`w^N3~L~mJoBJd;=dQ00&ftHEBBI*a?0}_D8u=mGx=7ey8eT3X&$#a=OWa z5DUlma^T<_%_Uj1Hyp8ktb+`TTzbu62dWN+-FrII;%wM`V|(-Avaqc=?4XN=T?kIJ zS-%Z)1Nq|VEbM%^GK}uMo&5zGNP(yHJuryg`q*Uw><>(ZuYMGYM&Ab7Z?4;t6;%wn zJ}4s!{-z8R+g}v844k$_OPNiCvu~Gwy06~z;_iNT;misQ-j!Gh2U#gw9EiTypZ{@& zae-$Ar00via?ju9*d)E zxX{~x_dYV8)9@TtFT(_DiaOUWa>+`iI=GV?NOw|dyPAkn**kXrDSMlIHyukjK8#e` z{V}N#&5qD>dOF^&O=ua*=2GCarPeIfGft|M&GoZd(682&pD>xXYu zZhju*q%;qIe5zX~#qecAReZCl$f*!mY+`qme1{`1ZD<_ReJ;s5YcW@oFcY%#a- zPN!cS)-9GN`ay0aQoWT+7Nrb4)lth$>a>a2>FJVAyf7!+XvV43q=&% z7~!Y%yjXgTVq@7@1R5bOPq(}ADAvw5s4|yBNil|@<&?upEj%w#H7vp9`DtNZY+;RJ zgKDOhQL!O}l+nppuCdK{EJzn5H)cj9IOR0*EIyzpEIt5esd=%~8pYaCBq(Eiq@7m8 zL`93~{q!hS5TGVCj$*pWq0R)Cqy2ska2-wa^J4RB6l=NqkRlz!>J1<#|@O6ZBQf zNUPmUOyH;4d9m3wiq*3GXcVax8WXr1OwgEEh*_zDF~$J1PM>kW>75Pb^JTAFbogmz zUTkKKVvD*_5_$o_*UDBo;hJNU?Lc{wB7|bA;}oS9sr7NTn4BareoD@ZCD$lc>N1gH zw1_D#JeI5=gKOG-2{?cajCgrsu_`*C@6g*PDEd&L{emk0x*i zyxFUgP0Nt+jy*!^SSv?1E5$PI4nQLi^J0lLiXF;u+Kc9~S};r}BZ;nP!o`#^iDTMW zZ;+{xiLljHv!FRioS&xV#ip(&lQ?bLgq5LU#V9zqZ+G&2sfRY(#d1xTl_sm#3z`}W zU@_Ba6%l@l&x^&^D7Hjrid3&e7iy`hjM%(f$Q0~ii}vDBEtY{=8sNTJs(^V8SPi+$}H#kzebn=)Df2ZI-I zgA9#`NpNMbQYDk}KxTPOs3{(=Xnw&3qwH(u#eU5i#p+TV+|rKMQ9YgtB22TW({&Kr zK*I?c!5DCdyX!|I22FtFIzRpDd9h!;MzOHvmXmA&vGdg-9ZN`Zs$Z+4coz{`@q&fG zz=z=loSt2l=EbhT<^iY%)+XlQr!Suuy9O;s81fB%`m%Yk zYfx{5$wk6XUpg;#4RVbz`9}EZ>AcuA7&OA972&5ZnHReTT}GHJBK-8Rd9iEIVuZ;T z!cT9{i(P{NBMg_tg8%RPHg0(!=ihhTL~i`jjq=GSPq^cMbu1nI`jL6~*N5MB__Bka zI7sb(bpI6~CpxBSlvRrUr_V^*7hnZzp**FccO0K0ss&TK|PhFd_DrYm@z5+4>KFv8Rs!EVDFuA?P#L#`2)whTQ3w0E4qMc_FAX z*EV>GcY49Bj-ZDXy^0Y8oJCQPtNCPQn)p5~o$5ggCs9|fHyvRF! zF|aqw_Z4C@y0*Q0z0>o7omrZ^5WCa0?L6R}o(GK0l8c4dv#xDy#XCJ0n3{Q~FMR$? zar#9wETGyU*fZCz_LJV}Im-%v;d5sK?{sSkCd3AMZ6>{|z0)d3s6D+2usoAVJf}B+ z!Dlv!=k#P&N6^DU5L2$*-WPeNV_@$YNa8s?0(PFMB%aelVC)%5;(0hd0H$W%=?kC4 zbGH9~@y5qCZhgb8FS+^sH=lQ-cJi5%w;cb;@jH(nIr@R4v%{Y~ymR0j9PE$wkN3WF zFS`5lyV;$e-v0ddySM(!)(5sWHos-_B|!Zd|Iw#kxj!d|+}Ikdz3aU@@tuZv8Ej2o z2}}WE)s3xSZBr|A-)V@q#GUCYfIYxQxv}N1ZSUUc?=-|g$>S%}mji2nT6JT~Tie=| zKKOy7j4-Du^7LghIJ3(4*M_qsR}FDc0;>Je^J@3j-U?h@?bQQ~FmJDW(^CKk5HN3S zxog9DaBU2qhB!NU9Gkvm(FV$vv$oNz+hFxr<21y{%4vv$5AggRJAZ!m(}g_*9&dUZ zzyWO58(Y@ea4y_xoQ61Zai@>Ydd+OGbl0}};Cy}>;>bmxz8IJUq`Vtj=GrFj@w*{T zTde6<08?|n+gaPxs^1N9>e88hIj}ePyT;n~?(@4LPF+9;ei5)X_q+Pq)~+6WtLGwN zj%MWPm(AeJo~yPtoTc9laq0qm&X+E#t!$}lZ*s1z_5%kJVQvV7c|Q*x*$dAfneudD z4{@H;n?3^I%;yYwZ8#6k8KKgic+)QdHs^k>y|&GZ{alE{8+ZDGSxsl1Q(D{V1%58X z;f+51VqkLa=ft&5-s9&&oZDE_=L1u7KlhfkO|AO55a&0Y>GOcSxu0vTZSOum7vlT| z44UTxYjZ#M=C!R|J>dT~$xr0z7tP?zy3(80hO_i@AUf}0K90Sev|2rF>1M&a+f3fp*;Fo_eKOoGwb{=LNqQS0zE-P_X zvv4-gUkx)(fIuG*mJ#@XlH)wE=M9gAxezjsvkH+Rn? z`HW=dvSl{!j)$xK=d*fTA7;Fo-({xtl){V?K=J8|KuRW(@TKvNxj~&x$U59>SBpeZ z2?!Ey+j;}_dv`<^Cxm>w31>mvMUnHKas(3XXF=vwByaWGg>1Y`4h7Y+icM(TwgyhT zP$ra;;}+tDVjiI>2!RQ@uHhM{t_D(RJS>l`Xx^j&Jz&)J;yPSTRg%JpSStc)UiC$o zarHdRI7IJyNaHY`O#ZzbhaoaGT{I5wu2g3O^trT@+^gG)MO*VwXqv z$HsJn$dM@&DV8BT3UQRwPo0Tk!|zE$rfC)@)$X_=@JJ&1l!u*aAa`u4a3+>gdE4pt zazWY!;bU{fPdU!bu!U#HH- z-AjDM^CNH+N`f$8xCLPl2o8F09sib-ZC! z6PEkf_CPGx0v3C~^XPTadQh+TA(68SBr_t03Y18cD;l=ud^oH6BL1I0 zkN*$R-5%P&L@yec&-gks>_Rm}{QomF!(AW$|G*mc+4%owHvT_}VA!Sc|BgT=T}&Mc z3Dm{fl%%HhL{U_k$=Rga#XUyq6mhlL*C`0ldjOX;i?$d2psy@A8oEwf?CK!Wm{lEko1?==H%c4- zZsVZ7^^Sv=?f>cizuf=8{+st-wfDJA{@~}fethp&_P%XT-h=lxcR#WFk9V!z*X$nd z{Kn1??)W>Yo#$--&i0RPPqsd_o!frF@#nW*wE0JyAKQG_tzSF-#arKf{Nc^l+%j(^ zHy^vTzj?UvnS(#x_;)vd>gM!j<>u`h|M|wx-+14R#*LSs{MpIJPrm7RuvI*G?{WJ0 zxksNo`tOeZt0U^@k;6YY{K>O#qL;NIZVhSbTBs z3F6|5`r?awhvedm>f(!gQ)e!|5EftDdvAa71p{q^Y_Ey(Xlx8gE19oA)P$^Y92qTm zxCx83Nf7ry)~}}N5A-5Py>#+|g~r5ovU;gtq$CAVehWO3v~`cm9? zH63CR$4bW8jA{?0wu6;-hI5S&ub{VK4^sF2nZK$!c zv-r4Kztf|0&K*c8(KMGeYH|k04uz$S0ivNk$43;vFB_fsP1k9~OOtdrCNNftl1lv{ zmrJ@0*4#c@YGmV_>~$T-Nd^$08zKQYv&jgPdV(=s1F)J$@Uo2wreSV;<3hu3#$}Mv zgwQ+rW-VJ1n5vP@@6I1#|NaJ(fgxlf>6k64Vo|`D}ZCH z!5oSU4XsFMQ9~MmXn(ac%9(aT|8{ZV4$j_P##4t!$nq+Cy@fQXO5d@ zW&pJ`ept*^4HzG^`O3hl8j!ZT0M&L-BU|eV+yt#aqhwTW*Q!oJD-?<@Q;2p+As~#l z;P)%c-ft~6QVy0LjU|`qIkE*OVofdYS}7RrSzM-xw%H_sR#LrO9N&4z0+iT-3rgD0 zFbEY89nz9`rCDe|xC{K!PywA(DppP64??DKOCA*r~qa@q+F%fHM zd;emgLH9YC015vJfj7wb4#?t=@VOpQ>0!AX;Dv7cw&@v(PO^n;E&VzbPcxC^FezfL z+9s??8qXO{sf4rx76ut7TUCatSKF8*c-sq~o5+$fnU8sKshevH;~`#AAsu9Y6*--6 zClxzdZ&llJz}ddA{lWz*GTj--lVqfzmMtRD<0ZAn)YUvcjOndp43d*x5yE0=-WqP3 zp*1=Sjg&qnk|VfQ$j2DLVQ_TNQ@affaOA+<%0TM3$k?u7GDx0gBD@O21V2ndx%}>GF@};M;991 z5nzz7g5UzvZwnHetE(i8x?;Bzi)e)7ik&)a8C<)hlAGE>!%X9TA~I6DQo&R@Ac?O{ zw>3Z;j(Pb~ACb+HmKR8sf;4UO2NoI~dRT~dst(bN`x&D*(%F2GWTM8%<{&J{^{Q=< z1yp3xjJNszg@(}@rQ0<-%B9+YC=M{DQpqCSSPb!Sz9@$U^Z_quLmuxQRoFGE$20 zAd@SMyA#}lP?i}d$*ymdk=^$$G(fg4xrF(al(%3#gQ-oYg)*ZyOeR5I##T9(*4TUl z=|vskjph*$t}_Y`g1)<_yrTWVa-noV<(Pcr+TT4=1Fsr0C| z&}f>KAe(N%EkJ`Wb08f7*H`sXnO0LQf>4Q$ig$XGNL-6z`+vRA;F(e^I!fZXYN`Ms zfbpxJA9nirR9aM>q0QMrKb0{RmiL{_?^rB0?>(7WEH>%I7x!NMS$sj={Pf1{D|_x7 zL}E~j7J3<+PUo}{>Vbt`l4I-rNUra)2plQLOMWmkU6~)^Hx`2$^_%r{wlNX}KaZzl zwK(I}WtghxN(vh5K^7G2Su7ZKE+(9uE-DJrZnkw85R@h`s1fMV!em(L>u9$qb#x0J zSi|(N;5tQ+!(;D1Et`fYl_f!!vU(e}Wv_49X%X=&3Dc2TA|EXm=wbww3iM2un&;}_MG)N`IKtMot;i^;K`-_D}4)EpE{k&aAhHRgKY8j-P zvs*}{4JQM$UpKpW6|QMDrW)PKFE!YB$*WkMalEe$5E_m{oE~JENnT^)Du&fsO}HsG zBXv#K@66W+A@bhU7A7*d4C!%}i8J2Bj;GZk#JFA{O$LA|jIl7c?nP_7;3EjWPc1a? zvE}8)XLzF7)F3=iFfH)%?I>W~>49dX1j8bEcj18ko z#P{;~P7N|sleAzok*yzEXsn+tbn_<{8nJ9v@1O`m_|+vS`(i^L2kzQv|8jh~`@I2w{qRIg*GfxFi)OOm|@-Qo=x< ztB9qvItAP9H58CbI5N((Qf)b=gG4_ayB;m3aR|w8zj5~de{1i*Y@ECad_4Q}YzsWw z0?)RP%78?%>e-@yzQI4IX->HU&@8J%6QS+ z?+5Q>et}+q%ZBHIB$}-yf%ao5N17p7u-&&n^1In9oHxAnG4NY!0N$3%hCglv{qF3Q zjqdxd_v*8=C*SXU)%-UZWJEXYSH~~}NuISV(eE3hGjPan`#|?>0H1-vL2+k>H!tvh zbF*@XT~v-u%9S%T%U}wVN+)z^khjjxM*gsv=rl|lN+n|$P6y`fb<*XL5SD{X(CJk&8 z>Lv$e28*ZEVbgCmD18-dK2(xPmW3y{C>@- zeA4wUUakLahA0%B*}L$z5jb@BVyJH?>tuzR6sqx3wv!nn&9ns$X9hz|H_Qy*4RG*M z>M6hfAB`H~S+Y|<^R05f$=oT{Y_q4T20N+pNYYfns}vge>{6dF@8C%}h#C0+ppL5{46ezG0@;9HOA=V2IB4|Br0Ee&ZH<^G9zU+)z*c<;f$* z=F!KGo_A;;{Pzc6x7~#-^YJ+ec<8oru7X!^V7dK`}~=2y(F&&vKoZ^_^YPw4CCwWVmt~H zD80n^;PT~qyhk&nKlz=r&yT)*ZM=_8os~8{3=>ElvP}FMzXP+O5g}`X9U_1yD zz#S6f!3^Dhdt?6jzRNJ)dk8+ebo2P_sWrp+%Dc~fKTLS_664aPUH`E6XYfA!soCcb ze{gM(ubOsOI_F-Pfb1p62cv+l$GbN}`cIxe`~1Po+IUZ=<_z!I-A3IFL!-XL`{ey^ z?|PiOGlbte@1WnOu8s4R)6NWM@-EJuFud$boGUkWuLrv`1N+^-Is5#c53CLLWm97Y zHg*^6b{MwzCD?m|G_OaxJ;V4Nzcc&%u9vQj^wFt4Lwb+D-wG3FzC^l+bi5wp)(qVT z-#`ES!?iJf#Z<3|J2$vWynAxvw)A@|_j(M9t{`+g=y*+Ku@ZL8g zg$RUS;ys^{t_3-tk=~BZ8ujh#gM8IgT4~h1FuaFLkQdBI*W#VeNaK&p@V+&^cB7t7 z#TnlFW~4Cj_g^*JCwC`(~tTkoPDJVSclj1+?Q zafx&}BVCJeJ|j8bIK$}t`Pvx2V)~}l87Tyt!11#30XC!ev!`Oc0GK}|L7+=mv%^Ak~W+c1bCo5p% zUWHK)k*ZV&X=Ds6&|tDNEXrxT4vBt`fqTh9bcykT8R=TQ^BL*Q@0#I#^QYFuJKO)i zcthN{^|d$u`sUki{N;_eo_y|PeEiwt;nANT1&4on=pFpO2fh8z>^pmZyl3tHU%Tec zAMF_1pWfEC{$NYl{M2T9<39rR%YWYf#A|jGr5tAE@Nh|Q-~Pm_LjwpgHm@fUdHWN| z&;UYs+I0Y6a{wXunIA51?%SV0Lj!ook022mz(aln@i~AHYrTj42$0YK9`YlIg$D4D z9|1fE5P}5iVLt*WG=PWv2%@0@Jmg1kHU|)5asIF$!B>R_@Q@!tBs74B{0Lq(2M}W4 z@vtAkozMUt@+0`l&;TCtBY5Q;K!`#g_9J*jXaEoS5xjg3AjB^1;gaXS{fU>&0fZpS z50hE!`WM)5f8wQ~0fe~jvkrjM&;UZbIbR3BOXdJV?B5>lO|7>-@mOd8Ay^64X@T3J z0X$TKq_;ouXlMWrxf8xPG=PWP3BO_vAjAg!;d1uB{fRFR4d9_Zg4zCmd-K~j4!&pS zo#3BmfBtT?z(3NbZ=BtndZM-YaA&?1a>5zA-9_Yt@drs@W|!mU7jy3N70&*&LcFq| zN?pBtbnTJlOyPP`ztz`2Gp&0`{pRn)n&8aLQ@bUIMQ5xIK>fliNPxUSp}Pu`ElTCMWPzZx20a{&FwIF@@;KL)NYYi) zVyQBt{sM(O6$+AGwgbm3sxjzxGU*uC%?m*x9nh0zCdJVLVkP=zr_Wj$!RQURMl|CM zMsb}BI3`KST2-SGKBVTlXOlClSrSbQO=vkb4H!{ocmjKdNRdI?1FlO=Y5B8@wa|>{ z2#GGLJ|jGOWcr3#Uw{AWHp84aUq8XF^!3?g%!~Ru@?c+o`iVG0Oyu)Rek&vIu`uV} z7u^zE9D>g}?$dBdaP{DRb(}7Yd%EGyM&<0q+e~74EBpLXwrD%{1Kea#44sLJ1JM`z z%PZM-(CywgU}pqk*IXgI%oALM{Hc!YXd*WA9T$%5E{MHW=v3CPMyWJpBe^oFm%WomP8yWP)iw<&oX6M*~00!2H>;)N@N9t{cLHI~g`H%-UjSls}7NLy{CS ziU{H=+Ypx(8NYug8qb$I#@O#f#{)igr-DKC0p4Z_E~@4WEWX~zURJ%YfNS<`#e4FG zb+~Q|e8cnwJV#LQZwzL4_#Z@okjP6;0NsqVe8{ zPnP;xUmkX(stKi=#1NsPLt99|8iAUrj++Tm1ZoI|TTTx0xz4ae;WPf;xktSqPG<3i z;`%x;p9C%R1^2XQGMR|ZkX-0#1vAs6bg!t~Q8PFQCvT366<_AZsbba5Hz#m4PF3{u zh#~l=PUChbLva&&0yu&K0mD019>FJPwp>1|6}p6xNv2g>HSD^NHqKnX)@Nql{@1Yv5u#Gvg(Tt+nUeiPp(tJ`e*DavtDw3fnYYL z8i1FsHNQbL>)-caLlk=bGX8RLsSnpoPxfbS=qO+E#6SVTZo0PxJ;wLd{iwhS&Po>r01SZ&{o9;$o;}^FNK7RP~ zhwnRPj=${aKOOz_);qV#TQA=Hzc+t&^NF3|4!QHZ?fd%Fr#z@;QUnI6<53w=6IG;xaZs1VCzFPmMEd%*m!_!+QHiN!kTfE8Zv;5nZLg=sqg|MY zw&NoQ;S!DKedipNprn%OSQ%lMOxv~CxP~I}n8=mLe85?i_HazLn*2m9MLNt*CA3C4 zv6)3f}#%{Pb7-wds>AX%!?jr640 z8l*&1&r%h_7z3iOfz|Re@p{*yGLkYBo%GOUBE#qZ#N0+0jUQiVWQStfC6qQcuH=*< zY8NJUR_#kx*W3EUqUHmoOhSG$XQQKTr5X(e5+j$p-dN%zMy|(?@&lb2MW|s1x%otB zjrWAsc=tlX>ovzLov77vZWj~10XdP&4V!3-m4*Vbh9SFyOnj09oGdq%s6uEgQHAL3 z-!(^7&C{Je)%Ft2a$?jGST>(_yq2igsD$_l+oO#dnH%@L-q1VAEi@8<<*unV^>H%i z##0@wU5PSAEk2IglStDXkNQd@JsL?hu5t6PLTmixLL*jf4!CU4MEypDG2FDRAZ8(w zM2n*eMFF}SC6SPgeg%%=dq2C-fJ&K?*Rus(&JI*EvKgX3HI>npB$x%Z?I;dGJjO8zD445L#nF!&%J{uJ0-Ouu*Hf z3Rdwgp%F_qo!m%oG3E%ihm{f(@5Wdny8FR}jSw2&9$Mqu78;}*pJ?fFC)P|VeS)@& zX_y-7MWrMfz2uNBTGR-pF<-FTThq`Q?+mT6pkA#Ka&8nc?4iVAl^TP!D~%MFvir@N z*)=gERSqP~jDxG2g~s;h7dAp@d~U8G)Ra<2jrh4TrbV2#%i}RpAcXiNo}HvPPk@Vj zt=udP0MG5Qy|59&Gb{+(!g!wrsZ|(_1vi(_>XiKm>vvpwfH)}NaO7=Gr8SCxbkE_e zI%7l!X}g8@^3Q`q*L*=J8s>Q{IL4}YK_KEuvsZ_pet|4`YT9H{kQ?uo8v+tXSwbB{ zYLG6d$c^Jg>xP&)mn*Um-e>9I)?Z~j?{5}u)f!6US~XXWxI-w1WE=#LDya4(fr++~ zfQgvO;00ZZXi4GLKV539|2&Y;8Vk<1YO3avNFb9&wbo)wogAyjAapEAv8q$a**H{z zWHBZt>l*glUt6?pSsTG3r-?Pw*GeVR?e?m%EFhcgyGC+206cV)y66j%jWY+vLZehh zY1$KWawo6Cy~4<-_<1mSOG&<&5DV>QdnAgMEJ>1gy!5*vG#2c+Ve0X37O3*f(5yJP zFybl*R!erhXq}2`Y6tQo7;9!bQh5kPl^|KlKlgtOt?@sE*7((1-?dy^CR~TtJEL9# z9g#7))0m`M#@J99Yfx^9@fHN7jSTHn$&~PXZ($~cMt`BfaaF@~(U?)i`Hs|@sHs6$ z$EajV&QMkXP64XGY$8fZy7SznTMY5o&4rB+8l8nkh=IFc@B~YQwHL{M1om>@cIAHV z3-aaidGvU47|q90?J|M*9kN1~GPqFmio(e5KteOC=S4n2$7;y_PlbMh%f)4gXTDrq zhS=u(*aB6Ep6K5={?NvYw%LvCU%2(?&5zuC{f$rDc+1IWPTp}GJ^FIHLtCHRal?ryy4@mrT14#rn03f7f-^y0Un@u>aktL#DdrmdKT zCtYIi0l7=;L2;YE${uq6%MLhsiMkq)lIG@BV&%>3EN=%3Wp;v#BfXt@iWF z>bEL)nZ1OaztY}nKflc0s@!Gv5Ic9Jy?Y7QuNV)pRk_RT!FKjad#nBYvihycU1krm zGgsMz@6C{OMg1O-yTl%xw8<;&t@gW1>IbjNU1krni7V}`_PfjMt;$_yFJY&yw72Gb z3$MyuW-kubOztzeZnA4v)i1uNvj4 z-m2VX_AvXaue5jn`t#EH4_cMG%pPjL=1P03e&e$Gt;$_yFJTw1vKL#$eTlsXJV{cHgu6mR$^-0DOAq7j{0lqkbd0xv{VBKLWA~j`m)0(>?ys@vj_z=AeEM zJvi9^dQG{Ht||AE3%RaqX%HRYD$?b`B| z+xp`xoiE)U#%(kmut!`+Lz%Pru8#A)zg|;r(O=kW%Ukr9>*bdH<=XO=M;U&qI4*39@5PSk>xadfos8`sM%{KoZi3%_x_ z+@CJ$aqaW?fi>m6Z%w)HUsLX%oVGb?zQ`^oxfRAZaEKLw}0DN&V$#=E$6{&<(Bi{wQ`>Wqha)g z7!kAh|DDaljgxmDym03o;Fo`>KkxFUZ<$3Qzb^n8p)|gf>ZYi1!3^kPvfoRYgCSZ9 zT({ZuYlMXbI>?^fk&+4KdiK4QDCEl`?vodV6o#M(nni|$p$eGGhalj&IOHz`jnqXU ze@{m4cr!TnMj}5$i~&yqSP30E&k+;{u&xPkn<0OyA;L&9KFdOMVc?pW^Xlz#uP~9a zq(rlwai&_3h^+3o11LIl>0EADw~z`~rLdiTHn}{{CjUS7-aXoJEv*l%<2vpFag3VnGljpNLG@W z$%HH(Sp+gn!kUnoteM$9KKE8#-*eABx?N2-D}DZ{vr6Zu@3*)1_wB8%Z~s1~hi~X4 z;9593z&N_LF`<?)~kfhb0Enb91}a zA4<0?6Bk|r1MJPu0e%F0@6ApZ9)4pv9p$e$=1$Dv!|9my+RpXQb*WLqco_;2bTu$r~N--B&N#Nn^ZnqIWjsOf1}mqItU; z!}X+HZ=fxAX0E!O(F$*F{cUPI>e<`|YWnJ^-ro2tg<3$X`D0@}*`#qwu{4}dbd>IJ zorGt-rOA4csd=5DN?xik5O7S;^qnfl;^a$qj`>03=wXTB_S_s3JJXg6|Cu~XBU&r&}D|Gg8|cvb!9 zp9yv}c30018o#F9r?-20?e%p5_6DE+#eu>-xcuzB_s3sP|8vK2&*y;T{-Ewt;-`3h zwmWiv`m5&}-YdUHmN_$atvkJPcJd#zb$9buqD>wA{$Y$Zb+qw@N+AR$KBnr}T$5FL z7O`7lyn2)nZZ{lc9jU$MAL|xnba`E(-g>3Lc1zvNyH+dLHsgGQi-iTvYV~G1A=$@% zO_|keEKb#ZbU87yB3RG)M}2i>JjREVYQ%9PE#yUnBqV3(Dl=F6!x2X;E*WihO$X8D zYtoaKPFLUg(eu^1yyWV;`JqOemtGkmulmQk%Pq%uH#mqMPY<4d1ZL+C<(T?uiP3Y9 zslVb-wO6|9Rk4Cy`!99A{&nFB0N?8RyIzCeaGI|V!vzcVD2Hl4!g>(a$A_I+gF+UM zFnuoP5dxv>?sC!K?YHiD1d%WkG}AQ`w|RHIjFRoF=va+;-&C=MHq52rh};+}e9hG) zt36)uoqi2d9zzbU3K~3Om%UoEkwC2(u3Pjdwp}W~ffm>Id)%{AiMyRLoW@&A4N ztH(cd{4K{OIG6uB_kZR7cin&A{qg;G-usXD{?5H`xc4RZ-UX@${N20Xa@V|DyL;u% zFW>o&JBvH5JJ)aj%I)vE@(Z_Dx4+=_-CO_i*7x4p-0I%?gcn*j|Mkruy!mA>EN%|2 z{>IfGx%yRC`KzB<`Rx~ex$@(cud67Phe!Y6=%iZWZHMzlM;G!@9M%~Kk(hC=!7KX~CCS3ma+Z}+haAM1gS^}rkS zz^Sh_H)Dui_po7?w6|!y)#A3ut5KDVRyXT~To8pF9cgA$FSgH`)#bAWG`qKV!6w9#haI_1i z0mL#5x-nVKM}5-tOwIM^-cDqa(VBlxvgPSU8>+n^+Ku6`dAw$@}{LVh-;s* zM{FiC0ft7cT|i5%&2GbLq&Je4?Z7+|2JHsln=d!I5w?Wm?H-ZXF@mZl&aQXxrT~lvI?gSuIT!8nF;WL&PAC>wpSYOU4`~G)&?1l z1Sf1KFf1e@tMYZA>JgG;zFp5d=#rZ1GA#FNOX&9c4AF0hX6%Py*iPXsr4f@kWdgk?+iT<^+WfuOmZG)yzgJI;cdjQgC-C~mJG^Y~<5NS2>bgxF6Qgr4=Z zt1s_uIN^BdV3_5}gyfCL2-^@z&xy8Bvttp;7%rG?kk;zF%iPjWM`+W?W|;{WiN6hn zO-y?E#OrO~sV!wJskk+4V@RV-sJO7bKLXq|Le%Y#eOWBf5xi|frZHB+rcKj8>WYSZ zLQXrbAt%v@x&FVMl0vrwBxrzhv#f?yoxW@^1gmYjL(VZ12xhHjXT9<$(Wr9X@we>} zX=mt?wK~|p@Qqx9iLRd2xwKfDg%DW6OxXrKHjhlAja~W4JtE%n(`nxtn8whct=u+> zF2V=us4z`>A`h7fAq!}A?8eOPyJv`KX*jhR-E6jCEp;He$hgQseO6yML2VOc-Hcv^ zEWB-9|E)b@oouJmQDX*!L)Eg#C)0c~mAF}TGwm$v8x{>`Yh*d{8Q;6UABbqEWYuEI za<=1e{d6%)$-KIy=dM=khAb0R7xa8fbmSSN-F&pS5is_~2Aiz3TpJ@&*0ITs1BV(7 zyf&jF+#fUtu{T~|W8&_7kMOsYYb)`{^%6tYc~YGdHFP!e7xk*$30Jd)S_@p!;O4}U zvqyMcENND^b6=-DYcbhK%b{UsQ=}DBT!fe&XHzmUAHcSK^gVmTVwlm;LJ2#QM$&WF zTcA|Q5)9;Smm=Is$+cqiX8H`MNybNeDGbyew-U9dMzUMYn>}}f)sk8e(=sV^+3}E} zrU{+nq3EL7_21sxn0NYcgVPjki((!IHwA>NO0@=G!&U_r!zj<>ZNg7n0lD_|d&F!* zbGjoGI>GB3-=A<;M8{jj>^rlD+T3cvy5Y0JRuGuV7oQ<0ScsZUO}~)8^69sOcjNa2CG#Q9;vgrMQ%E6p&hJnc%;W7Ys!TP^;<5) zRqfkfx<^dBQQGfLYtydFm?C9*Vl;QQ!f=lB2r_6c=GbCgR2w$by0I4^Kqmy1^-QzF zw^gx2hZK|Wod&4o9ZSi=6erD;r#c%5>RQx|_wQ{;TSU}zeb}rIny!Om47^2APH8H< z0*%7N#2hM~ugAeCtn33dDluFg+O{!L!D$#NhGs-%i-^Lzvw4^;#bp}dJcYxhxHkR-OBbhMkb+ZV=)}oQM}vZbJzyjLi}QYPkIdBsrd_G zgTX9?2g0pSI76TbI)rmI?P3uM*MQKE$u#upy19(7?Pj|zz%&4KU_#it_JKWucQ@5d z$T9ph_H4beYABKlMbyYljR8Z{byatmd=EbuyJpvES3g8Xio~1Ivh=~#k*Y{#3D7kX;VMHxe zB;z9n9`_KRTuzayQER7Oq{~o`Yca*cR6mwD`_!Zv)GNn9Ltz&B?@uvD9kW zX-okP#$LF{+`t)iC7?hta3qzu^%?sm(X;0h_DiD4{#LEf5q(!y!=(h?@+J_|6A>^J z++Z3jhTM}^febs98N;>v%z^7D6o^Ujan`RY5uop;>+oyy;;d~HR({S&v} zcYA#Mowxqut)IE|!CNogdUW$Y-~7d!-+a@!3E%ktZv2BA-+sfrf!?@w{hwU_&g